diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 951eaf1d2..8f7caf09e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -15,14 +15,15 @@ jobs: runs-on: ${{ matrix.operating-system }} strategy: + fail-fast: false matrix: - operating-system: [windows-latest,ubuntu-latest,macos-latest] + operating-system: [windows-latest,ubuntu-latest,macos-15-intel] # lazarus-versions: [stable, 2.2.2, 2.2.0, 2.0.12] lazarus-versions: [stable] steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: false @@ -32,49 +33,193 @@ jobs: with-cache: false lazarus-version: ${{ matrix.lazarus-versions }} + - name: Test core and framework read/write locks + shell: bash + run: | + mkdir -p .ci-threading/units .ci-threading/bin + fpc -B -Mdelphi -Sh -gl -Cr -Co \ + -FU.ci-threading/units -FE.ci-threading/bin \ + -Fusrc/core -Fusrc/libraries/sphere10 -Fusrc/libraries/pascalcoin \ + -Fusrc/libraries/generics.collections -Fisrc/libraries/generics.collections/inc \ + -Fusrc/tests/experimental/core -Fusrc/tests/experimental/framework \ + src/tests/experimental/ThreadingTests.lpr + ./.ci-threading/bin/ThreadingTests .ci-threading/results.log + cat .ci-threading/results.log + grep -q '^0 unfreed memory blocks' .ci-threading/results.log.heap.log + + - name: Diagnose Unix threading failures + if: ${{ failure() && runner.os != 'Windows' }} + shell: bash + run: | + if [[ ! -x .ci-threading/bin/ThreadingTests ]]; then exit 0; fi + if [[ "$RUNNER_OS" == "Linux" ]]; then + gdb --batch -ex 'set pagination off' -ex run -ex 'thread apply all bt 40' \ + --args ./.ci-threading/bin/ThreadingTests .ci-threading/debug-results.log + else + lldb --batch -o run -k 'thread backtrace all -c 40' \ + -- ./.ci-threading/bin/ThreadingTests .ci-threading/debug-results.log + fi + + - name: Upload threading reports + if: ${{ always() && hashFiles('.ci-threading/*.log*') != '' }} + uses: actions/upload-artifact@v7 + with: + name: Threading-${{ runner.os }} + path: .ci-threading/*.log* + include-hidden-files: true + - name: Build the Pascal daemon run: lazbuild -B "src/pascalcoin_daemon.lpi" - name: Build reference Pascal Miner run: lazbuild -B "src/pascalcoin_miner.lpi" - - name: Build the Main Wallet Application (Windows/Linux) - if: ${{ matrix.operating-system != 'macos-latest' }} + - name: Build Classic Wallet (Windows/Linux) + if: ${{ runner.os != 'macOS' }} run: lazbuild -B "src/pascalcoin_wallet_classic.lpi" - - name: Build the Main Wallet Application (macOS) - if: ${{ matrix.operating-system == 'macos-latest' }} + - name: Build Classic Wallet (macOS) + if: ${{ runner.os == 'macOS' }} run: lazbuild -B --ws=cocoa "src/pascalcoin_wallet_classic.lpi" + - name: Build Experimental Wallet (Windows/Linux) + if: ${{ runner.os != 'macOS' }} + run: lazbuild -B --build-mode=Release "src/pascalcoin_wallet_experimental.lpi" + + - name: Build Experimental Wallet (macOS) + if: ${{ runner.os == 'macOS' }} + run: lazbuild -B --ws=cocoa --build-mode=Release "src/pascalcoin_wallet_experimental.lpi" + + - name: Add Apple silicon to the wallets (macOS) + if: ${{ runner.os == 'macOS' }} + shell: bash + run: | + intel="$RUNNER_TEMP/macos-intel" + mkdir -p "$intel" + cp src/PascalCoinWalletLazarus src/PascalCoinWalletExperimental "$intel/" + # Lazarus calls ppcx64 directly, which cannot target aarch64. The fpc driver + # selects ppca64 from the FPC 3.2.2 Intel and Arm64 install. + ppca64="$(command -v ppca64 || true)" + if [[ -z "$ppca64" ]]; then + ppca64="$(ls /usr/local/lib/fpc/*/ppca64 2>/dev/null | head -n 1 || true)" + [[ -n "$ppca64" ]] && export PATH="$(dirname "$ppca64"):$PATH" + fi + if [[ -z "$ppca64" ]]; then + echo "The FPC install has no aarch64 compiler (ppca64)." >&2 + exit 1 + fi + # Xcode's new linker aborts on FPC 3.2.2 aarch64 objects, so link with the + # classic linker through a wrapper around the fpc driver. + driver="$RUNNER_TEMP/fpc-aarch64" + printf '#!/bin/bash\nexec "%s" -k-ld_classic "$@"\n' "$(command -v fpc)" > "$driver" + chmod +x "$driver" + ld -v 2>&1 | head -n 2 || true + echo "Apple silicon compiler: $ppca64, driver: $driver" + lazbuild -B --compiler="$driver" --cpu=aarch64 --os=darwin --ws=cocoa "src/pascalcoin_wallet_classic.lpi" + lazbuild -B --compiler="$driver" --cpu=aarch64 --os=darwin --ws=cocoa --build-mode=Release "src/pascalcoin_wallet_experimental.lpi" + for exe in PascalCoinWalletLazarus PascalCoinWalletExperimental; do + lipo -create "$intel/$exe" "src/$exe" -output "$intel/$exe.universal" + mv "$intel/$exe.universal" "src/$exe" + archs="$(lipo -archs "src/$exe")" + echo "$exe: $archs" + [[ "$archs" == *x86_64* && "$archs" == *arm64* ]] + done + + - name: Package wallet apps into a disk image (macOS) + if: ${{ runner.os == 'macOS' }} + run: bash build/macOS/package-macos.sh src "$RUNNER_TEMP/macos-dmg" + + - name: Test bounded message dialogs on the default desktop (Windows) + if: ${{ runner.os == 'Windows' }} + shell: pwsh + run: | + $compiler = (Get-Command fpc).Source + $lazarus = Split-Path -Parent (Get-Command lazbuild).Source + & .\src\tests\experimental\RunWalletGridRefreshTests.ps1 -Demo -DemoCase operation-messages -FpcPath $compiler -LazarusDirectory $lazarus -OutputDirectory (Join-Path $env:RUNNER_TEMP 'wallet-message-small') + exit $LASTEXITCODE + + - name: Prepare the 150-percent native test viewport (Windows) + if: ${{ runner.os == 'Windows' }} + shell: pwsh + run: | + & .\src\tests\experimental\native\SetCiDisplayMode.ps1 -ReportPath (Join-Path $env:RUNNER_TEMP 'wallet-display.json') + + - name: Test wallet units and native workflows (Windows) + if: ${{ runner.os == 'Windows' }} + shell: pwsh + run: | + & .\src\tests\experimental\RunAllWalletTests.ps1 -OutputDirectory (Join-Path $env:RUNNER_TEMP 'wallet-tests') + exit $LASTEXITCODE + + - name: Upload wallet test reports + if: ${{ always() && runner.os == 'Windows' }} + uses: actions/upload-artifact@v7 + with: + name: Wallet-tests-Windows + if-no-files-found: warn + retention-days: 14 + path: | + ${{ runner.temp }}/wallet-tests/**/*.log + ${{ runner.temp }}/wallet-tests/**/*.json + ${{ runner.temp }}/wallet-tests/**/*.png + ${{ runner.temp }}/wallet-tests/**/*.bmp + + - name: Upload native display checks + if: ${{ always() && runner.os == 'Windows' }} + uses: actions/upload-artifact@v7 + with: + name: Native-display-Windows + if-no-files-found: warn + retention-days: 14 + path: | + ${{ runner.temp }}/wallet-display.json + ${{ runner.temp }}/wallet-message-small/**/*.log + ${{ runner.temp }}/wallet-message-small/**/*.json + ${{ runner.temp }}/wallet-message-small/**/*.bmp + - name: Upload binary (Windows) if: ${{ (matrix.operating-system == 'windows-latest') && (matrix.lazarus-versions == 'stable') }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: PascalCoinWindows + if-no-files-found: error path: | src/pascalcoin_daemon.exe src/PascalCoinMiner.exe src/PascalCoinWalletLazarus.exe + src/PascalCoinWalletExperimental.exe - name: Upload binary (Ubuntu) if: ${{ (matrix.operating-system == 'ubuntu-latest') && (matrix.lazarus-versions == 'stable') }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: PascalCoinLinux + if-no-files-found: error path: | src/pascalcoin_daemon src/PascalCoinMiner src/PascalCoinWalletLazarus + src/PascalCoinWalletExperimental - name: Upload binary (macOS) - if: ${{ (matrix.operating-system == 'macos-latest') && (matrix.lazarus-versions == 'stable') }} - uses: actions/upload-artifact@v3 + if: ${{ (runner.os == 'macOS') && (matrix.lazarus-versions == 'stable') }} + uses: actions/upload-artifact@v7 with: name: PascalCoinMacOS + if-no-files-found: error path: | src/pascalcoin_daemon src/PascalCoinMiner src/PascalCoinWalletLazarus + src/PascalCoinWalletExperimental + + - name: Upload disk image (macOS) + if: ${{ (runner.os == 'macOS') && (matrix.lazarus-versions == 'stable') }} + uses: actions/upload-artifact@v7 + with: + name: PascalCoinMacOSDiskImage + if-no-files-found: error + path: ${{ runner.temp }}/macos-dmg/*.dmg package-release: if: contains(github.ref, '/tags/') @@ -82,39 +227,59 @@ jobs: name: Package and create GitHub Release runs-on: ubuntu-latest needs: build + permissions: + contents: write steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set output id: vars run: echo "tag=${GITHUB_REF#refs/*/}" >> ${GITHUB_OUTPUT} - name: Download the Release binary - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v7 + with: + pattern: PascalCoin* - name: List files run: | ls -alF PascalCoinWindows ls -alF PascalCoinLinux ls -alF PascalCoinMacOS + ls -alF PascalCoinMacOSDiskImage - name: Create release files run: | + for package in PascalCoinWindows PascalCoinLinux PascalCoinMacOS; do + cp -R src/locale licenses "$package/" + cp src/pascalsha.cl LICENSE "$package/" + done cd PascalCoinWindows cp ../windows-dll/*.dll . - zip -r PascalCoin-${{ steps.vars.outputs.tag }}-x86_64-win64.zip *.exe *.dll + zip -r PascalCoin-${{ steps.vars.outputs.tag }}-x86_64-win64.zip *.exe *.dll locale licenses pascalsha.cl LICENSE cd ../PascalCoinLinux cp ../linux-libs/*.so.1.1 . chmod +x PascalCoin* pascalcoin* - tar -zcvf PascalCoin-${{ steps.vars.outputs.tag }}-x86_64-linux.tar.gz PascalCoin* pascalcoin* *.so.1.1 + tar -zcvf PascalCoin-${{ steps.vars.outputs.tag }}-x86_64-linux.tar.gz PascalCoin* pascalcoin* *.so.1.1 locale licenses pascalsha.cl LICENSE cd ../PascalCoinMacOS chmod +x PascalCoin* pascalcoin* - tar -zcvf PascalCoin-${{ steps.vars.outputs.tag }}-x86_64-darwin.tar.gz PascalCoin* pascalcoin* + tar -zcvf PascalCoin-${{ steps.vars.outputs.tag }}-x86_64-darwin.tar.gz PascalCoin* pascalcoin* locale licenses pascalsha.cl LICENSE + cd ../PascalCoinMacOSDiskImage + images=(*.dmg) + if [[ ${#images[@]} -ne 1 ]]; then echo "Expected one disk image" >&2; exit 1; fi + image=PascalCoin-${{ steps.vars.outputs.tag }}-macOS.dmg + if [[ "${images[0]}" != "$image" ]]; then mv "${images[0]}" "$image"; fi + + - name: Checksum release files + run: | + for archive in PascalCoinWindows/*.zip PascalCoinLinux/*.tar.gz PascalCoinMacOS/*.tar.gz PascalCoinMacOSDiskImage/*.dmg; do + sha256sum "$archive" | sed 's@ .*/@ @' + done > SHA256SUMS - name: Create GitHub release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: name: PascalCoin ${{ steps.vars.outputs.tag }} body_path: release-notes.md @@ -122,6 +287,8 @@ jobs: PascalCoinWindows/*.zip PascalCoinLinux/*.tar.gz PascalCoinMacOS/*.tar.gz + PascalCoinMacOSDiskImage/*.dmg + SHA256SUMS env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 4e6fac3e1..9467ebd98 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,12 @@ dunit.ini src/libssl-1_1.dll src/libssl-1_1-x64.dll *.rsm + +# Local OpenSSL deployment; canonical runtime is tracked under windows-dll. +/src/libcrypto-1_1-x64.dll + +# Generated by the FPC private-desktop test broker. +/src/tests/experimental/native/libimpPrivateDesktopBroker.a + +# Local temporary work. +/.temp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9108273..1ee60fb72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,107 @@ - Implementation of PIP-0044 (Induplicatable NFT) -> https://github.com/PascalCoin/PascalCoin/blob/master/PIP/PIP-0044.md - Improvements on downloading Safebox (fresh installation) +## Build 5.9.5 +- Ship the macOS wallets as universal apps that run natively on Apple silicon and Intel Macs. CI cross-compiles the Apple silicon builds and merges them with `lipo`. +- Rename the macOS disk image to `PascalCoin--macOS.dmg`. +- Rewrite the READMEs to be clear and concise, checked against the code. +- Align node, miner and executable versions at 5.9.5. + +## Build 5.9.4 +- Package the macOS Classic and Experimental wallets as `PascalCoin Wallet.app` and `PascalCoin Wallet Experimental.app` in a `PascalCoin--x86_64-darwin.dmg` disk image with an Applications link and first-start instructions. +- Sign the macOS app bundles ad hoc and verify them, inside the disk image, in CI. +- Keep the macOS `.tar.gz` archive with all four programs and list the disk image in SHA256SUMS. +- Align node, miner and executable versions at 5.9.4. + +## Build 5.9.3 +- Keep the Delphi Classic and Experimental log views following each published log batch. +- Sync the embedded Sphere10 library with the framework: translatable paging-mode captions with fixed component names, a constructed unsupported-dispose-policy exception and aligned formatting. +- Remove a redundant thread lookup from abstract-memory check detection, size header read-ahead buffers from the block header constant and create the Experimental startup progress lock without recursion. +- Key the RandomHash adoption regression on the shipping protocol define and wait for queued workers in the scope regressions. +- Align node, miner and executable versions at 5.9.3. + +## Build 5.9.2 +- Restore Experimental wallet account history stored in legacy blockchain files before, or within gaps in, the newer block index. +- Retain dropdown-controlled 7-day, 30-day and Maximum history limits, with no duplicate rows for missing archive blocks. +- Add three layered-storage regressions to the shared Delphi/FPC checks and the wallet unit runner. +- Align node, miner and executable versions at 5.9.2. + +## Build 5.9.1 +- Use the core `TReadWriteLock` for application guards across node, networking, storage, mining and wallet data sources. +- Allow concurrent access on proven read-only paths; retain recursive write ownership for mutable streams, caches, callbacks and operation sequencing. +- Keep low-level admission primitives independent of logging and preserve timed-acquisition limits. +- Balance operation-component lock ownership before destruction. +- Preserve the original storage error during failed safebox replacement and constructor cleanup. +- Report progress and honor cancellation during legacy checkpoint inspection. +- Skip checkpoints ahead of local history and replay available blocks; retain failure protection for unreadable storage. +- Add the framework's reusable read-ahead stream decorator and buffer legacy checkpoint reads. +- Enable its optional length-cache flag for fixed checkpoint files, avoiding repeated size queries. +- Keep read buffering and stream-length caching independently configurable, with explicit unread-data flushing before read buffering is disabled. +- Buffer pending-operation input, serialized block headers and optional RandomHash input without changing their file-sharing or validation rules. +- Deliver GUI log batches outside the producer queue lock and update native log controls once per batch in both wallets. +- Use Classic's configured indexed blockchain reader and startup index-flushing policy in the experimental wallet, retaining legacy-file fallback. +- Keep large totals beside compact grid navigation when they fit, with clean wrapping on narrower footers. +- Restore the banner logo's alpha edges in Delphi and add top spacing to the experimental wallet's Network Details in Delphi and Lazarus. +- Add actual-owner regressions for recursive list/operation leases, exclusion, timed wakeups and unsigned timeouts. +- Align application, miner and executable versions at 5.9.1. + +## Build 5.9 - 2026-10-28 +- Import [Sphere 10 Pascal Framework](https://github.com/HermanSchoenfeld/PascalFramework) for caching, concurrency and scoped resources. +- Add per-key and bulk-fetch caching with expiration, invalidation, eviction and synchronized access. +- Add read/write locking with recursive writes, upgradeable reads, timeouts and automatic lock scopes. +- Add cleanup scopes, collection helpers, typed data sources and UTF-8 file text; standardize String APIs and document public types. +- Add Single/Auto/Manual paging with a compact mode button, accurate row counts and scroll resets. +- Prevent Release-mode footer clipping and overlapping operation-information fields with large values. +- Reuse complete results when resizing and report completion once per fetched page. +- Add rounded-spoke loading cards, retain rows during updates and fix completion, cancellation, errors, parenting and empty selections. +- Keep wizard windows stationary, retain inputs and allow validation and completion retries. +- Keep generic refresh throttling independent and expose TThrottledEventMode at unit scope. +- Fetch account-key index ranges in one traversal and correct unsigned account-number and price comparisons. +- Handle empty binary values safely during serialization and cryptographic conversion. +- Publish structured progress and synchronization events; reproduce existing display and log messages in their consumers. +- Keep synchronization active through batch gaps and received-block processing; network-off stops new downloads while received work finishes. +- Allow concurrent mempool reads with a self-contained core read/write lock. +- Defer pending reconciliation during catch-up; retain valid indexes and bulk-build rejected-operation results. +- Add pending restore progress and cancellation, bulk marker cleanup and byte-identical buffered saving. +- Flush pending changes on shutdown and replace files atomically, preserving the previous package on I/O failure. +- Publish validated safebox checkpoints atomically and recover readable snapshots after failed writes. +- Keep incomplete file writes unstable and pause synchronization and operation admission after storage failure until restart. +- Report the first storage failure in both wallets; drain background index workers safely without certifying a failed index. +- Reconnect peers and recover safebox downloads after disconnection; log completed chunks at Info. +- Join network/download workers, drain termination callbacks during confirmed shutdown and preserve miner-job snapshots during replies. +- Restore classic wallet key filtering during synchronization and apply pending balances after reconciliation. +- Add cancellable account/history workers and join active filters before classic wallet shutdown. +- Complete send, purchase, sale, key-change and payload wizards with validated operations and explicit wallet unlock ownership. +- Add batch-change approval for skipped accounts, charge only changed accounts and offer custom fees with an optional single payer. +- Restore advanced operations and the RAW/multioperation workbench; merge missing signatures without bypassing validation. +- Use bounded, scrollable operation confirmation and result dialogs. +- Reuse mining keys during startup and selection; rotate only after a mined block when configured. +- Validate private/watch-only key import and backup restoration. +- Load startup data and resolve account keys on cancellable workers; publish selected totals and history bindings after completed queries. +- Refresh account pages immediately and coalesce operations queries after 250 ms of silence; suspend both grids throughout node synchronization. +- Add account-index paging, cached history queries, cancellable wallet totals and a bounded account cache that survives safebox replacement. +- Limit Recent Operations to 2,016/8,640 blocks for 7/30 days; search all available history for Maximum and cancel obsolete selections. +- Use bounded visible-row snapshots and cancellable workers for Explorer filters, history, totals and type statistics; fix My Accounts/key filters. +- Restore account/block explorers, RPC tools, message history and language settings; add Block Explorer refresh/reset and explicit operation Details actions. +- Share cached committed counts/timestamps between footer and synchronization view without data/status polling; marshal key notifications to the UI. +- Show dashes until totals first load; show Wallet Keys padlocks in every state. +- Align footer buttons, retain full icon height and correct unsigned key hashes and PASC balances. +- Anchor header/footer controls, preserve modeless focus and fix banner transparency and splash/synchronization flicker. +- Reflow banner status after native version-label sizing, center Details controls and release key-selector objects independently of native window teardown. +- Keep startup height fixed with horizontal resizing; restore normal constraints/maximization afterward and join startup/grid workers before shutdown. +- Build the experimental wallet with Delphi/VCL and Lazarus/FPC using paired form resources and conversion tools. +- Add Win32/Win64 Debug/Release targets with separate compiler outputs; keep the Delphi executable in src beside OpenSSL without build events. +- Embed application/tray icons and set application and wallet executable versions to 5.9.0.0. +- Update GNU gettext and regular-expression compatibility for Delphi. +- Document wallet features, Sphere 10 Pascal Framework and Delphi/Lazarus build and test commands. +- Add Delphi and FPC screenshots and credit Herman Schoenfeld for the experimental GUI, preserving Albert Molina's original attribution. +- Add runners for unit and isolated native regression tests covering shared components, operations, classic wallet, synchronization, loading and shutdown. +- Register all common utility fixtures and run the full Windows wallet regression driver in CI. +- Cover failed checkpoint and index writes, snapshot recovery, cooperative shutdown, quiet-period refreshes and synchronization cards through real block batches. +- Build and package both wallet applications in Windows, Linux and macOS CI; run core and framework lock tests on every platform. +- Build the experimental release with optimization and support pointer-shaped macOS thread IDs. +- Include language catalogs, miner resources, license notices and checksums in release packages. + ## Build 5.8 - 2024-01-17 - Accumulative release, including: - Minor bugs fixed diff --git a/README.md b/README.md index 997b2136c..027492b1a 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,219 @@ -[![BUILD](https://github.com/PascalCoin/PascalCoin/actions/workflows/workflow.yml/badge.svg?branch=master)](https://github.com/PascalCoin/PascalCoin/actions) +[![Build](https://github.com/PascalCoin/PascalCoin/actions/workflows/workflow.yml/badge.svg?branch=master)](https://github.com/PascalCoin/PascalCoin/actions) +[![Version](https://img.shields.io/github/v/tag/PascalCoin/PascalCoin?sort=semver&style=for-the-badge)](https://github.com/PascalCoin/PascalCoin/releases) +[![License](https://img.shields.io/github/license/PascalCoin/PascalCoin.svg?style=for-the-badge)](LICENSE) +[![Discord](https://img.shields.io/discord/383064643482025984.svg?label=DISCORD%20COMMUNITY&style=for-the-badge)](https://discord.gg/9YYZyw) +[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/pascalcoin.svg?style=for-the-badge)](https://www.reddit.com/r/pascalcoin) -[![VERSION](https://img.shields.io/github/v/tag/PascalCoin/PascalCoin?sort=semver&style=for-the-badge)](https://github.com/PascalCoin/PascalCoin/releases) -[![License](https://img.shields.io/github/license/PascalCoin/PascalCoin.svg?style=for-the-badge)](https://github.com/PascalCoin/PascalCoin/blob/master/LICENSE) -[![DISCORD](https://img.shields.io/discord/383064643482025984.svg?label=DISCORD%20COMMUNITY&style=for-the-badge)](https://discord.gg/9YYZyw) -[![REDDIT](https://img.shields.io/reddit/subreddit-subscribers/pascalcoin.svg?style=for-the-badge)](https://www.reddit.com/r/pascalcoin) -

-

- -

-

+

PascalCoin

+Copyright © 2016–2026 PascalCoin developers, based on original Albert Molina source code. +Please use at your own risk. + +# PascalCoin - A One of a Kind Cryptocurrency + +**PascalCoin is an instant, zero-fee, infinitely scalable, decentralized cryptocurrency with advanced privacy and smart contract capabilities. It is the world's first blockchain independent of historical operations.** + +**PascalCoin possesses unlimited potential.** + +PascalCoin is a peer-to-peer cryptocurrency written in Object Pascal. **PASC** +is its currency, and a **PASA** is a numbered account. This repository contains +two desktop wallets, a node daemon and a reference miner. PascalCoin's original +implementation is by **Albert Molina**, with significant contributions by +**Herman Schoenfeld** (such as the Experimental Wallet and core changes), +alongside the work of the PascalCoin contributors. + +![Experimental wallet 5.9 in Delphi: wallet totals, owned accounts and recent operations](docs/screenshots/5.9/experimental-delphi-wallet.webp) + +## Downloads + +[PascalCoin 5.9.5](https://github.com/PascalCoin/PascalCoin/releases/tag/5.9.5) +archives contain all four applications. Drag the wallet apps from the macOS disk +image to Applications. They run natively on Apple silicon and Intel Macs. + +| Platform | Package | +|---|---| +| Windows x64 | [PascalCoin-5.9.5-x86_64-win64.zip](https://github.com/PascalCoin/PascalCoin/releases/download/5.9.5/PascalCoin-5.9.5-x86_64-win64.zip) | +| Linux x64 | [PascalCoin-5.9.5-x86_64-linux.tar.gz](https://github.com/PascalCoin/PascalCoin/releases/download/5.9.5/PascalCoin-5.9.5-x86_64-linux.tar.gz) | +| macOS, wallet apps | [PascalCoin-5.9.5-macOS.dmg](https://github.com/PascalCoin/PascalCoin/releases/download/5.9.5/PascalCoin-5.9.5-macOS.dmg) | +| macOS, all programs | [PascalCoin-5.9.5-x86_64-darwin.tar.gz](https://github.com/PascalCoin/PascalCoin/releases/download/5.9.5/PascalCoin-5.9.5-x86_64-darwin.tar.gz) | + +Verify packages against the release's +[SHA256SUMS](https://github.com/PascalCoin/PascalCoin/releases/download/5.9.5/SHA256SUMS). +See [Build 5.9.5](CHANGELOG.md#build-595) for the patch changes. +[All releases](https://github.com/PascalCoin/PascalCoin/releases) include earlier +mainnet and testnet builds. + +The macOS apps are not notarized. On first start, click **Open Anyway** for each +app in **System Settings > Privacy & Security**. On macOS 14 and earlier, +Control-click the app and choose Open. + +| Application | Executable in the package | Purpose | +|---|---|---| +| Classic wallet | `PascalCoinWalletLazarus` | Wallet, miner and explorer interface | +| Experimental wallet | `PascalCoinWalletExperimental` | Operation wizards and paged account and history views | +| Daemon | `pascalcoin_daemon` | Node and JSON-RPC service without a desktop interface | +| Reference miner | `PascalCoinMiner` | Standalone mining application | + +Keep `locale`, miner resources, runtime libraries and license notices beside the +extracted executables. Launch Linux GUI builds from the extracted folder. They +need GTK2. + +## Wallet features + +The experimental wallet provides: + +| Area | Features | +|---|---| +| Accounts | Filter, select and inspect your accounts, which a new key does not create (see **Get An Account**). | +| History | View pending operations, decoded payloads and account or wallet history for **7 Days**, **30 Days** or **Maximum**. | +| Keys | Manage private and watch-only keys and wallet backups, and unlock the wallet to sign or add private keys. | +| Payments | Send PASC with custom fees and an optional public, key-encrypted or password-encrypted payload. | +| Account management | Buy, list for public or private sale, delist, and change owner keys, names and types. | +| Advanced operations | The classic advanced form handles account data, EPASA addressing, swaps, hash locks and recipient-signed purchases. | +| RAW workbench | Import and export RAW packages, combine multioperations, sign locally or offline, merge returned signatures and execute. | +| Explorers | Browse accounts, pending operations, operations by block or hash, and block timing and hash-rate statistics. | +| Network and tools | Manage peers, ports, allowed IPs and checkpoint downloads, and use the RPC client, peer messages and logs. | +| Mining | Configure mining and the reward key, which must be an eligible private key. | +| Languages | Choose English or partly translated Dutch in the banner or Options, then restart. | + +The [feature map](src/gui-experimental/FEATURES.md) lists the classic +equivalents, and the [wallet guide](src/gui-experimental/README.md) and +[Build 5.9 changes](CHANGELOG.md#build-59---2026-10-28) give details. + +## Screenshots + +
+Startup, node status and a batch account-type change + +| Startup and account setup | Wallet and operation review | +|---|---| +| [![Cancelable blockchain loading in the Delphi experimental wallet](docs/screenshots/5.9/experimental-delphi-startup.webp)](docs/screenshots/5.9/experimental-delphi-startup.webp)
Delphi: cancelable local blockchain loading. | [![FPC experimental wallet with pending operations](docs/screenshots/5.9/experimental-fpc-wallet.webp)](docs/screenshots/5.9/experimental-fpc-wallet.webp)
FPC: wallet accounts and pending operations. | +| [![Delphi experimental wallet active node status](docs/screenshots/5.9/experimental-delphi-node-status.webp)](docs/screenshots/5.9/experimental-delphi-node-status.webp)
Delphi: node status, chain statistics and mining information. | [![FPC batch operation confirmation](docs/screenshots/5.9/experimental-fpc-batch-confirmation.webp)](docs/screenshots/5.9/experimental-fpc-batch-confirmation.webp)
FPC: review account changes, signer and fees before execution. | +| [![Delphi batch Change Account Info account selection](docs/screenshots/5.9/experimental-delphi-batch-selection.webp)](docs/screenshots/5.9/experimental-delphi-batch-selection.webp)
Delphi: review 267 selected accounts in the account-information wizard. | [![FPC batch result for 267 account-information operations](docs/screenshots/5.9/experimental-fpc-batch-result.webp)](docs/screenshots/5.9/experimental-fpc-batch-result.webp)
FPC: scrollable results for 267 account-information operations. | +| [![Delphi account name and type change options](docs/screenshots/5.9/experimental-delphi-account-info.webp)](docs/screenshots/5.9/experimental-delphi-account-info.webp)
Delphi: account name, type, fee and payload options. | [![Delphi batch custom fee and one fee payer](docs/screenshots/5.9/experimental-delphi-batch-fees.webp)](docs/screenshots/5.9/experimental-delphi-batch-fees.webp)
Delphi: custom per-operation fee, total fee and a single fee payer. | + +
+ +[Screenshot provenance and checksums](docs/screenshots/README.md). + +## Build + +The source includes its Pascal libraries. The experimental wallet's +[build guide](src/gui-experimental/README.md#build) covers compiler options and +runtime layout. + +### Lazarus/FPC + +With `lazbuild` and FPC, build all four applications into `src`, named as in the +application table, on Windows, Linux or macOS. Run from the repository +root: + +```sh +lazbuild -B src/pascalcoin_daemon.lpi +lazbuild -B src/pascalcoin_miner.lpi +lazbuild -B src/pascalcoin_wallet_classic.lpi +lazbuild -B src/pascalcoin_wallet_experimental.lpi ``` -Copyright (c) 2016-2024 Pascal developers based on original Albert Molina source code + +For the experimental wallet, add `--build-mode=Release` for an optimized build or +`--build-mode=Debug` for debugging. `Default` is unoptimized. + +- On macOS, add `--ws=cocoa` to both wallet commands. Linux uses GTK2. +- On Windows and Linux, copy OpenSSL 1.1 from `windows-dll` or `linux-libs` + beside the programs. +- Keep `src/locale` and `src/pascalsha.cl` with distributed programs. +- On macOS, `bash build/macOS/package-macos.sh src dist` packages both wallets in + a disk image. See [build/README.md](build/README.md). + +### Delphi + +On Windows, open +[pascalcoin_wallet_classic.dproj](src/pascalcoin_wallet_classic.dproj) or +[pascalcoin_wallet_experimental.dproj](src/pascalcoin_wallet_experimental.dproj) +in the Delphi IDE. The builds write `src/pascalcoin_wallet_classic.exe` and +`src/pascalcoin_wallet_experimental.exe`. Only classic Win64 Release and +experimental Win64 Debug builds from Delphi 13 Community Edition are verified. + +In editions with command-line compilers, run from the repository root in the +**RAD Studio Command Prompt**: + +```bat +msbuild src\pascalcoin_wallet_classic.dproj /t:Build /p:Config=Release /p:Platform=Win64 +msbuild src\pascalcoin_wallet_experimental.dproj /t:Build /p:Config=Release /p:Platform=Win64 ``` -Please use at your own risk. +In Community Edition, select the configuration and platform in the IDE, then run +AutoBuild: -# Pascal - A One of a kind Cryptocurrency +```bat +bds.exe -b src\pascalcoin_wallet_classic.dproj +bds.exe -b src\pascalcoin_wallet_experimental.dproj +``` -**Pascal is an instant, zero-fee, infinitely scalable, and decentralized cryptocurrency with advanced privacy and smart contract capabilities. It's the world's first blockchain independent of historical operations.** +See Embarcadero's [MSBuild guide](https://docwiki.embarcadero.com/RADStudio/Sydney/en/MSBuild) +and [IDE command options](https://docwiki.embarcadero.com/RADStudio/Alexandria/en/IDE_Command_Line_Switches_and_Options). -**Pascal possesses unlimited potential.** +## Test -## License +On Windows with Lazarus/FPC, build the experimental wallet, then run from the +repository root: -The source code is distributed under the `MIT software license`, see the accompanying [LICENSE](LICENSE) file or visit http://www.opensource.org/licenses/mit-license.php. +```powershell +& .\src\tests\experimental\RunAllWalletTests.ps1 +``` -This product includes software developed by the OpenSSL Project and Denis Grinyuk (https://github.com/Arvur/OpenSSL-Delphi) and some cryptographic functions inspirated in code written by Ladar Levison and Marco Ferrante. +Add `-FpcPath`, `-LazarusDirectory`, `-WalletExecutable` and a new +`-OutputDirectory` as needed. Tests run offline on private Windows desktops with +temporary keys and data. The [test guide](src/tests/experimental/README.md) +describes the coverage, the portable lock tests for Linux and macOS, Delphi +probes and optional large-chain fixtures. -Go to https://www.pascalcoin.org to find out more. +For a separate experimental wallet session, use a new data folder: -## Releases +```powershell +& .\src\PascalCoinWalletExperimental.exe --data-dir="" --offline --no-startup-dialogs +``` -You can find the core Wallet releases [here](https://github.com/PascalCoin/PascalCoin/releases). +`--offline` disables peers, listening, RPC and mining for that run only. -There are testnet and mainnet releases. +## Develop -## Community +PascalCoin uses the +[Sphere 10 Pascal Framework](https://github.com/HermanSchoenfeld/PascalFramework), +imported into `src/libraries/sphere10`. -In case you have questions or just want to get in touch with like-minded Pascal users. +See [Pascal Improvement Proposals](PIP/README.md), the [changelog](CHANGELOG.md) +and [project website](https://www.pascalcoin.org). Community libraries include: - - Our twitter feed: https://twitter.com/PascalCoin - - Chat with us on discord: https://discord.gg/9YYZyw - - Join our subreddit on: https://www.reddit.com/r/pascalcoin +| Language | Library | Version support | +|---|---|---| +| JavaScript | [SBX](https://github.com/techworker/sbx) | 5 | +| Dart | [PascalDart](https://github.com/appditto/pascaldart) | 4 | +| Java | [JPascalCoin](https://github.com/davidbolet/JPascalCoin) | 4 | -## Develop with Pascal +## Community -| Language | Project Name | Type | Version support | Link | -| ------------- | ------------ | ------- | --------------- | ---- | -| JavaScript | SBX | Library | 5 | [SBX](https://github.com/techworker/sbx) | -| Dart | PascalDart | Library | 4 | [PascalDart](https://github.com/appditto/pascaldart) | -| Java | JPascalCoin | Library | 4 | [JPascalCoin](https://github.com/davidbolet/JPascalCoin) | +- [Twitter](https://twitter.com/PascalCoin) +- [Discord](https://discord.gg/9YYZyw) +- [Reddit](https://www.reddit.com/r/pascalcoin) -## Changelog +## License + +``` +Copyright (c) 2016-2024 Pascal developers based on original Albert Molina source code +``` -View the changelog [here](CHANGELOG.md) +The source is distributed under the [MIT license](LICENSE). Please use at your +own risk. -## Donations +This product includes software developed by the OpenSSL Project and Denis +Grinyuk ([OpenSSL-Delphi](https://github.com/Arvur/OpenSSL-Delphi)), and +cryptographic functions inspired by code written by Ladar Levison and Marco +Ferrante. [Included notices](licenses/README.md) preserve the licenses and +attributions for bundled libraries and runtimes. -Consider a donation using Pascal coin to development account: `0-10` +## Donations -Also, consider a donation using BitCoin to `16K3HCZRhFUtM8GdWRcfKeaa6KsuyxZaYk` +Support development with PASC to account **`0-10`**, or Bitcoin to +**`16K3HCZRhFUtM8GdWRcfKeaa6KsuyxZaYk`**. diff --git a/build/README.md b/build/README.md index 1ec980e7f..54c3f5029 100644 --- a/build/README.md +++ b/build/README.md @@ -1 +1,27 @@ -This folder contains platform specific build configurations. \ No newline at end of file +This folder holds platform-specific packaging files. + +## macOS + +[package-macos.sh](macOS/package-macos.sh) packages the Cocoa wallet builds as +`PascalCoin Wallet.app` and `PascalCoin Wallet Experimental.app`. It signs them +ad hoc and puts them in a disk image with an Applications link and +[Read Me.txt](macOS/Read%20Me.txt). + +```sh +lazbuild -B --ws=cocoa src/pascalcoin_wallet_classic.lpi +lazbuild -B --ws=cocoa --build-mode=Release src/pascalcoin_wallet_experimental.lpi +bash build/macOS/package-macos.sh src dist +``` + +Bundles take their version from `CT_ClientAppVersion` in `src/core/UConst.pas` +and metadata from [Info.plist.in](macOS/Info.plist.in). The image is +`dist/PascalCoin--macOS.dmg`. If `codesign` or `hdiutil` is +missing, the script only assembles the bundles in `dist/stage`. + +CI also cross-compiles both wallets for Apple silicon with +`lazbuild --compiler= --cpu=aarch64`, merges them with `lipo` into +universal binaries and attaches the image to tagged releases as +`PascalCoin--macOS.dmg`. + +The bundles lack Developer ID signing and notarization, so macOS asks users to +confirm the first launch. diff --git a/build/macOS/Info.plist.in b/build/macOS/Info.plist.in new file mode 100644 index 000000000..2cf469e95 --- /dev/null +++ b/build/macOS/Info.plist.in @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + @EXECUTABLE@ + CFBundleName + @NAME@ + CFBundleDisplayName + @NAME@ + CFBundleIdentifier + @IDENTIFIER@ + CFBundleIconFile + Logo.icns + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + Pasc + CFBundleShortVersionString + @VERSION@ + CFBundleVersion + @VERSION@ + LSApplicationCategoryType + public.app-category.finance + NSHumanReadableCopyright + Copyright Pascal Cryptocurrency developers. MIT License. + CSResourcesFileMapped + + NSHighResolutionCapable + + + diff --git a/build/macOS/PascalCoin Wallet.app/Contents/Resources/Logo.icns b/build/macOS/Logo.icns similarity index 100% rename from build/macOS/PascalCoin Wallet.app/Contents/Resources/Logo.icns rename to build/macOS/Logo.icns diff --git a/build/macOS/PascalCoin Wallet.app/Contents/Info.plist b/build/macOS/PascalCoin Wallet.app/Contents/Info.plist deleted file mode 100644 index 7ef9a8794..000000000 --- a/build/macOS/PascalCoin Wallet.app/Contents/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - PascalCoinWalletLazarus - CFBundleName - PascalCoin Wallet - CFBundleIdentifier - org.pascalcoin.PascalCoinWallet - CFBundleIconFile - Logo.icns - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - APPL - CFBundleSignature - Pasc - CFBundleShortVersionString - 1.0.0.0 - CFBundleVersion - 1 - CSResourcesFileMapped - - CFBundleDocumentTypes - - - CFBundleTypeRole - Viewer - CFBundleTypeExtensions - - * - - CFBundleTypeOSTypes - - fold - disk - **** - - - - NSHighResolutionCapable - - - diff --git a/build/macOS/PascalCoin Wallet.app/Contents/PkgInfo b/build/macOS/PascalCoin Wallet.app/Contents/PkgInfo deleted file mode 100644 index 6f749b0f3..000000000 --- a/build/macOS/PascalCoin Wallet.app/Contents/PkgInfo +++ /dev/null @@ -1 +0,0 @@ -APPL???? diff --git a/build/macOS/Read Me.txt b/build/macOS/Read Me.txt new file mode 100644 index 000000000..2d7a7969e --- /dev/null +++ b/build/macOS/Read Me.txt @@ -0,0 +1,27 @@ +PascalCoin for macOS + +Install +1. Drag PascalCoin Wallet onto the Applications folder in this window. + PascalCoin Wallet Experimental is the newer interface; install it too if you + want to try it. +2. Eject this disk and open the wallet from Applications or Launchpad. + +First start +These apps are not notarized by Apple, so macOS asks for confirmation once. +- macOS 15 or later: when the warning appears, click Done. Open System Settings, + choose Privacy & Security, scroll to Security and click Open Anyway next to + the PascalCoin app. Confirm with your password. +- macOS 14 or earlier: Control-click the app in Applications, choose Open, then + click Open in the dialog. + +The apps run natively on Macs with Apple silicon (M1 and later) and on Intel +Macs. + +Your data +Both wallets keep their keys and blockchain data in the PascalCoin folder in +your home folder. Back up your wallet keys from the wallet before reinstalling +or moving to another computer. + +The daemon and reference miner command-line programs are in the separate +x86_64-darwin.tar.gz release package. They are Intel builds, so on Apple +silicon macOS runs them through Rosetta. diff --git a/build/macOS/package-macos.sh b/build/macOS/package-macos.sh new file mode 100755 index 000000000..010f277eb --- /dev/null +++ b/build/macOS/package-macos.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Packages the Cocoa wallets as signed macOS application bundles inside a DMG. +# +# Usage: build/macOS/package-macos.sh [dmg-name] +# +# holds PascalCoinWalletLazarus and PascalCoinWalletExperimental +# built with lazbuild --ws=cocoa; CI merges Intel and Apple silicon builds into +# universal binaries with lipo first. The DMG is named .dmg, by default +# PascalCoin--macOS, where is CT_ClientAppVersion. +# On a system without codesign or hdiutil the bundles are only staged, so the +# layout can be inspected anywhere. +set -euo pipefail + +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "Usage: $0 [dmg-name]" >&2 + exit 2 +fi + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo="$(cd "$here/../.." && pwd)" +bin="$(cd "$1" && pwd)" +mkdir -p "$2" +out="$(cd "$2" && pwd)" + +version="$(sed -n "s/.*CT_ClientAppVersion.*PRODUCTION}'\([0-9][0-9.]*\)'.*/\1/p" "$repo/src/core/UConst.pas")" +if [[ -z "$version" ]]; then + echo "Cannot read CT_ClientAppVersion from src/core/UConst.pas" >&2 + exit 1 +fi +name="${3:-PascalCoin-$version-macOS}" +stage="$out/stage" +rm -rf "$stage" +mkdir -p "$stage" + +# Builds one application bundle. Translations and licences live in Resources; +# the wallets look for locale beside their executable, so Contents/MacOS links +# to it rather than holding data that code signing would treat as code. +make_app() { + local executable="$1" title="$2" identifier="$3" + local contents="$stage/$title.app/Contents" + if [[ ! -f "$bin/$executable" ]]; then + echo "Missing $bin/$executable" >&2 + exit 1 + fi + mkdir -p "$contents/MacOS" "$contents/Resources" + cp "$bin/$executable" "$contents/MacOS/$executable" + chmod 755 "$contents/MacOS/$executable" + sed -e "s|@EXECUTABLE@|$executable|g" -e "s|@NAME@|$title|g" \ + -e "s|@IDENTIFIER@|$identifier|g" -e "s|@VERSION@|$version|g" \ + "$here/Info.plist.in" > "$contents/Info.plist" + printf 'APPLPasc' > "$contents/PkgInfo" + cp "$here/Logo.icns" "$contents/Resources/Logo.icns" + cp -R "$repo/src/locale" "$contents/Resources/locale" + cp -R "$repo/licenses" "$contents/Resources/licenses" + cp "$repo/LICENSE" "$contents/Resources/LICENSE" + ln -s ../Resources/locale "$contents/MacOS/locale" +} + +make_app PascalCoinWalletLazarus "PascalCoin Wallet" org.pascalcoin.PascalCoinWallet +make_app PascalCoinWalletExperimental "PascalCoin Wallet Experimental" org.pascalcoin.PascalCoinWalletExperimental +cp "$here/Read Me.txt" "$stage/Read Me.txt" + +if ! command -v codesign > /dev/null || ! command -v hdiutil > /dev/null; then + echo "codesign or hdiutil is unavailable; bundles are staged in $stage without a DMG." + exit 0 +fi + +for app in "$stage"/*.app; do + executable="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Contents/Info.plist")" + echo "== $(basename "$app")" + lipo -archs "$app/Contents/MacOS/$executable" + vtool -show-build "$app/Contents/MacOS/$executable" 2> /dev/null | grep -E 'minos|version' || true + plutil -lint "$app/Contents/Info.plist" + # Extended attributes make codesign reject a bundle. Ad-hoc signing seals the + # bundle; it does not replace Developer ID signing and notarisation. + xattr -cr "$app" + codesign --force --sign - "$app" + codesign --verify --strict --verbose=2 "$app" +done + +ln -s /Applications "$stage/Applications" +dmg="$out/$name.dmg" +rm -f "$dmg" +# hdiutil occasionally reports a busy resource on hosted runners; retry it. +for attempt in 1 2 3; do + if hdiutil create -volname "PascalCoin $version" -srcfolder "$stage" \ + -fs HFS+ -format UDZO -ov "$dmg"; then + break + fi + if [[ $attempt -eq 3 ]]; then + exit 1 + fi + sleep 5 +done +hdiutil verify "$dmg" + +# Check the image as a user receives it. +mount="$(mktemp -d)" +hdiutil attach -nobrowse -readonly -mountpoint "$mount" "$dmg" > /dev/null +trap 'hdiutil detach "$mount" > /dev/null || true' EXIT +ls -la "$mount" +for app in "$mount"/*.app; do + codesign --verify --strict --verbose=2 "$app" +done +hdiutil detach "$mount" > /dev/null +trap - EXIT +rm -rf "$stage" +echo "Created $dmg" diff --git a/docs/screenshots/5.9/experimental-delphi-account-info.webp b/docs/screenshots/5.9/experimental-delphi-account-info.webp new file mode 100644 index 000000000..fd2c98f4c Binary files /dev/null and b/docs/screenshots/5.9/experimental-delphi-account-info.webp differ diff --git a/docs/screenshots/5.9/experimental-delphi-batch-fees.webp b/docs/screenshots/5.9/experimental-delphi-batch-fees.webp new file mode 100644 index 000000000..513f07188 Binary files /dev/null and b/docs/screenshots/5.9/experimental-delphi-batch-fees.webp differ diff --git a/docs/screenshots/5.9/experimental-delphi-batch-selection.webp b/docs/screenshots/5.9/experimental-delphi-batch-selection.webp new file mode 100644 index 000000000..6cecaf30b Binary files /dev/null and b/docs/screenshots/5.9/experimental-delphi-batch-selection.webp differ diff --git a/docs/screenshots/5.9/experimental-delphi-node-status.webp b/docs/screenshots/5.9/experimental-delphi-node-status.webp new file mode 100644 index 000000000..e64bb09fa Binary files /dev/null and b/docs/screenshots/5.9/experimental-delphi-node-status.webp differ diff --git a/docs/screenshots/5.9/experimental-delphi-startup.webp b/docs/screenshots/5.9/experimental-delphi-startup.webp new file mode 100644 index 000000000..69ca2446c Binary files /dev/null and b/docs/screenshots/5.9/experimental-delphi-startup.webp differ diff --git a/docs/screenshots/5.9/experimental-delphi-wallet.webp b/docs/screenshots/5.9/experimental-delphi-wallet.webp new file mode 100644 index 000000000..f07f94251 Binary files /dev/null and b/docs/screenshots/5.9/experimental-delphi-wallet.webp differ diff --git a/docs/screenshots/5.9/experimental-fpc-batch-confirmation.webp b/docs/screenshots/5.9/experimental-fpc-batch-confirmation.webp new file mode 100644 index 000000000..395dbb031 Binary files /dev/null and b/docs/screenshots/5.9/experimental-fpc-batch-confirmation.webp differ diff --git a/docs/screenshots/5.9/experimental-fpc-batch-result.webp b/docs/screenshots/5.9/experimental-fpc-batch-result.webp new file mode 100644 index 000000000..9f1f894b8 Binary files /dev/null and b/docs/screenshots/5.9/experimental-fpc-batch-result.webp differ diff --git a/docs/screenshots/5.9/experimental-fpc-wallet.webp b/docs/screenshots/5.9/experimental-fpc-wallet.webp new file mode 100644 index 000000000..ceef67be1 Binary files /dev/null and b/docs/screenshots/5.9/experimental-fpc-wallet.webp differ diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md new file mode 100644 index 000000000..a6f419249 --- /dev/null +++ b/docs/screenshots/README.md @@ -0,0 +1,18 @@ +# PascalCoin 5.9 screenshots + +These screenshots show the experimental wallet on Windows, built with +Delphi (VCL) and Lazarus/FPC (LCL). They are unmodified, user-supplied WebP +originals, including their balance redactions. No screenshot shows a +private-key or password-entry screen. + +| Screenshot | SHA-256 | +|---|---| +| [Delphi wallet](5.9/experimental-delphi-wallet.webp) | `f1ac26aec782529d416b5abbc346fc126f09effef0cca9ad50c162d2f991fb87` | +| [FPC wallet](5.9/experimental-fpc-wallet.webp) | `d7226f5c450191ee5966d46046ccfa5343ecb42924cb386a9d3be58c85b0c78a` | +| [Delphi startup](5.9/experimental-delphi-startup.webp) | `c98e126471df9abe2e5141265c8e2ef7274ec0e1ffcdd57eb413d600f093279c` | +| [Delphi node status](5.9/experimental-delphi-node-status.webp) | `4646d3077afe6155013178d9c7dbd9b3c71c0bfd3814495b1faeb6e4a069f97e` | +| [Delphi batch selection](5.9/experimental-delphi-batch-selection.webp) | `6b4eb21baaf3680bb3a984eacaf19df224159a5eff6d0319b58867db35ac05cb` | +| [Delphi account information](5.9/experimental-delphi-account-info.webp) | `88b7b4739d0cd26a0579bb305fcb6f001f1667be80454072c34321b0824e1f52` | +| [Delphi batch fees](5.9/experimental-delphi-batch-fees.webp) | `043c886606fbbf85eeb2dca09728b3d67c25ad85c0bdb24e24e450c83407d6c5` | +| [FPC batch confirmation](5.9/experimental-fpc-batch-confirmation.webp) | `44becf14a82fc1b75598e921ecbdc29da8020e7e982a06b29ddb9d7da6b94502` | +| [FPC batch result](5.9/experimental-fpc-batch-result.webp) | `d3cd803428cb360420df96e7acb0ac632aa80a194fb7dbb57df4a344f6e9c3b5` | diff --git a/licenses/CryptoLib4Pascal.txt b/licenses/CryptoLib4Pascal.txt new file mode 100644 index 000000000..6d67cef1d --- /dev/null +++ b/licenses/CryptoLib4Pascal.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 - 20XX Ugochukwu Mmaduekwe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/FreePascal-RTL-COPYING.txt b/licenses/FreePascal-RTL-COPYING.txt new file mode 100644 index 000000000..9a408cc72 --- /dev/null +++ b/licenses/FreePascal-RTL-COPYING.txt @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/licenses/FreePascal-RTL-exception.txt b/licenses/FreePascal-RTL-exception.txt new file mode 100644 index 000000000..675669fa2 --- /dev/null +++ b/licenses/FreePascal-RTL-exception.txt @@ -0,0 +1,25 @@ +This is the file COPYING.FPC, it applies to the Free Pascal Run-Time Library +(RTL) and packages (packages) distributed by members of the Free Pascal +Development Team. + +The source code of the Free Pascal Runtime Libraries and packages are +distributed under the Library GNU General Public License +(see the file COPYING) with the following modification: + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent modules, +and to copy and distribute the resulting executable under terms of your choice, +provided that you also meet, for each linked independent module, the terms +and conditions of the license of that module. An independent module is a module +which is not derived from or based on this library. If you modify this +library, you may extend this exception to your version of the library, but you are +not obligated to do so. If you do not wish to do so, delete this exception +statement from your version. + +If you didn't receive a copy of the file COPYING, contact: + Free Software Foundation + 675 Mass Ave + Cambridge, MA 02139 + USA + diff --git a/licenses/GNU-gettext.txt b/licenses/GNU-gettext.txt new file mode 100644 index 000000000..bcce554fc --- /dev/null +++ b/licenses/GNU-gettext.txt @@ -0,0 +1,38 @@ +(**************************************************************) +(* *) +(* (C) Copyright by Lars B. Dybdahl and others *) +(* E-mail: Lars@dybdahl.dk, phone +45 70201241 *) +(* *) +(* Contributors: Peter Thornqvist, Troy Wolbrink, *) +(* Frank Andreas de Groot, Igor Siticov, *) +(* Jacques Garcia Vazquez, Igor Gitman, *) +(* Arvid Winkelsdorf, *) +(* Thomas Mueller (dummzeuch) *) +(* Olivier Sannier (obones) *) +(* *) +(* See http://dybdahl.dk/dxgettext/ for more information *) +(* *) +(**************************************************************) + +// Information about this file: +// $LastChangedDate: 2010-08-25 15:40:17 +0200 (on, 25 aug 2010) $ +// $LastChangedRevision: 220 $ +// $HeadURL: http://svn.berlios.de/svnroot/repos/dxgettext/trunk/dxgettext/sample/gnugettext.pas $ + +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// The names of any contributor may not be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/licenses/Generics-Collections.txt b/licenses/Generics-Collections.txt new file mode 100644 index 000000000..1cc8d5f4a --- /dev/null +++ b/licenses/Generics-Collections.txt @@ -0,0 +1,25 @@ +{ + This file is part of the Free Pascal run time library. + Copyright (c) 2014 by Maciej Izak (hnb) + member of the Free Sparta development team (http://freesparta.com) + + Copyright(c) 2004-2014 DaThoX + + It contains the Free Pascal generics library + + See the file COPYING.FPC, included in this distribution, + for details about the copyright. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + Acknowledgment + + Thanks to Sphere 10 Software (http://sphere10.com) for sponsoring + many new types and major refactoring of entire library + + Thanks to mORMot (http://synopse.info) project for the best implementations + of hashing functions like crc32c and xxHash32 :) + + **********************************************************************} diff --git a/licenses/HashLib4Pascal.txt b/licenses/HashLib4Pascal.txt new file mode 100644 index 000000000..66a8b4b3a --- /dev/null +++ b/licenses/HashLib4Pascal.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 - 2019 Ugochukwu Mmaduekwe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/Lazarus-COPYING.txt b/licenses/Lazarus-COPYING.txt new file mode 100644 index 000000000..399f9907e --- /dev/null +++ b/licenses/Lazarus-COPYING.txt @@ -0,0 +1,46 @@ +This is the file COPYING. It gives an overview of the licenses used in the +Lazarus sources. + +The Lazarus sources consists of several parts and each part has its own +license. Three main licenses are in use: +* the GNU General Public License, version 2 (GPLv2) +* a modified LGPL (as described in COPYING.modifiedLGPL) +* the MPL +In general, each file contains a header, describing the license of the file. + +The license directory tree: + +/ + | + +- ide (GPLv2) + | + +- designer (GPLv2) + | + +- debugger (GPLv2) + | + +- examples (GPLv2) + | + +- packager (GPLv2) + | + +- tools (GPLv2) + | + +- lcl (modified LGPL) + | With the exception of the customdrawn/android interface, + | where some files are licensed under the + | Apache Licence, Version 2. + | + +- components/ + | + +- codetools (GPLv2) + | + +- synedit (MPL - Mozilla public license) + | + +- xxx There are various packages under various licenses. Mostly the + modified LGPL. See the license in the package files for details. + Some are not free for commercial use. + +The IDE files are the files in the ide, designer, packager and debugger +directory. They are under the GPLv2, with the following exceptions: +transfermacros.pp, wordcompletion.pp, patheditordlg.pas, outputfilter.pas, +inputfiledialog.pas, findreplacedialog.pp, findinfilesdlg.pas +These files are under the modified LGPL as described in COPYING.modifiedLGPL. diff --git a/licenses/Lazarus-LGPL.txt b/licenses/Lazarus-LGPL.txt new file mode 100644 index 000000000..92b8903ff --- /dev/null +++ b/licenses/Lazarus-LGPL.txt @@ -0,0 +1,481 @@ + GNU LIBRARY GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is + numbered 2 because it goes with version 2 of the ordinary GPL.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Library General Public License, applies to some +specially designated Free Software Foundation software, and to any +other libraries whose authors decide to use it. You can use it for +your libraries, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if +you distribute copies of the library, or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link a program with the library, you must provide +complete object files to the recipients so that they can relink them +with the library, after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + Our method of protecting your rights has two steps: (1) copyright +the library, and (2) offer you this license which gives you legal +permission to copy, distribute and/or modify the library. + + Also, for each distributor's protection, we want to make certain +that everyone understands that there is no warranty for this free +library. If the library is modified by someone else and passed on, we +want its recipients to know that what they have is not the original +version, so that any problems introduced by others will not reflect on +the original authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that companies distributing free +software will individually obtain patent licenses, thus in effect +transforming the program into proprietary software. To prevent this, +we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + + Most GNU software, including some libraries, is covered by the ordinary +GNU General Public License, which was designed for utility programs. This +license, the GNU Library General Public License, applies to certain +designated libraries. This license is quite different from the ordinary +one; be sure to read it in full, and don't assume that anything in it is +the same as in the ordinary license. + + The reason we have a separate public license for some libraries is that +they blur the distinction we usually make between modifying or adding to a +program and simply using it. Linking a program with a library, without +changing the library, is in some sense simply using the library, and is +analogous to running a utility program or application program. However, in +a textual and legal sense, the linked executable is a combined work, a +derivative of the original library, and the ordinary General Public License +treats it as such. + + Because of this blurred distinction, using the ordinary General +Public License for libraries did not effectively promote software +sharing, because most developers did not use the libraries. We +concluded that weaker conditions might promote sharing better. + + However, unrestricted linking of non-free programs would deprive the +users of those programs of all benefit from the free status of the +libraries themselves. This Library General Public License is intended to +permit developers of non-free programs to use free libraries, while +preserving your freedom as a user of such programs to change the free +libraries that are incorporated in them. (We have not seen how to achieve +this as regards changes in header files, but we have achieved it as regards +changes in the actual functions of the Library.) The hope is that this +will lead to faster development of free libraries. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, while the latter only +works together with the library. + + Note that it is possible for a library to be covered by the ordinary +General Public License rather than by this special one. + + GNU LIBRARY GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library which +contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Library +General Public License (also called "this License"). Each licensee is +addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also compile or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + c) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + d) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the source code distributed need not include anything that is normally +distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Library General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/licenses/Lazarus-modifiedLGPL.txt b/licenses/Lazarus-modifiedLGPL.txt new file mode 100644 index 000000000..0226a7684 --- /dev/null +++ b/licenses/Lazarus-modifiedLGPL.txt @@ -0,0 +1,26 @@ +This is the file COPYING.modifiedLGPL, it applies to several units in the +Lazarus sources distributed by members of the Lazarus Development Team. +All files contains headers showing the appropriate license. See there if this +modification can be applied. + +These files are distributed under the Library GNU General Public License +(see the file COPYING.LGPL) with the following modification: + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent modules, +and to copy and distribute the resulting executable under terms of your choice, +provided that you also meet, for each linked independent module, the terms +and conditions of the license of that module. An independent module is a +module which is not derived from or based on this library. If you modify this +library, you may extend this exception to your version of the library, but +you are not obligated to do so. If you do not wish to do so, delete this +exception statement from your version. + + +If you didn't receive a copy of the file COPYING.LGPL, contact: + Free Software Foundation, Inc., + 675 Mass Ave + Cambridge, MA 02139 + USA + diff --git a/licenses/LoadingCircle.txt b/licenses/LoadingCircle.txt new file mode 100644 index 000000000..c73003f82 --- /dev/null +++ b/licenses/LoadingCircle.txt @@ -0,0 +1,29 @@ +{ Native LCL/VCL port copyright © 2026 Herman Schoenfeld + + Rounded-spoke loading indicator adapted from Sphere10 Framework's + Windows.Forms.LoadingCircle, using its MacOSX appearance and fading palette. + + Original LoadingCircle copyright © 2006, 2007 Martin R. Gagné + (martingagne@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +} diff --git a/licenses/OpenSSL-1.1.txt b/licenses/OpenSSL-1.1.txt new file mode 100644 index 000000000..9601ab435 --- /dev/null +++ b/licenses/OpenSSL-1.1.txt @@ -0,0 +1,125 @@ + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + diff --git a/licenses/PasOpenCL.txt b/licenses/PasOpenCL.txt new file mode 100644 index 000000000..1b3a73371 --- /dev/null +++ b/licenses/PasOpenCL.txt @@ -0,0 +1,75 @@ +The code license of the original project was specified as Apache License on GoogleCode, but BSD inside the source code. Since it is unclear what license applies here, you can find both licenses in this file. + +The first license in this file is the Apache license, followed by the BSD license at the end of this document. + + +--- + + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +---- + + +BSD License + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/licenses/README.md b/licenses/README.md new file mode 100644 index 000000000..7ea650cd4 --- /dev/null +++ b/licenses/README.md @@ -0,0 +1,25 @@ +# Included notices + +Release packages include these notices for bundled components. PascalCoin's +license is [`../LICENSE`](../LICENSE). + +| Component | Notice | Source | +|---|---|---| +| PasOpenCL | [PasOpenCL.txt](PasOpenCL.txt) | [`src/libraries/pasopencl/LICENSE`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/pasopencl/LICENSE) | +| HashLib4Pascal | [HashLib4Pascal.txt](HashLib4Pascal.txt) | [`src/libraries/hashlib4pascal/LICENSE`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/hashlib4pascal/LICENSE) | +| SimpleBaseLib4Pascal | [SimpleBaseLib4Pascal.txt](SimpleBaseLib4Pascal.txt) | [`src/libraries/simplebaselib4pascal/LICENSE`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/simplebaselib4pascal/LICENSE) | +| CryptoLib4Pascal | [CryptoLib4Pascal.txt](CryptoLib4Pascal.txt) | [`src/libraries/cryptolib4pascal/LICENSE`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/cryptolib4pascal/LICENSE) | +| Sphere 10 Pascal Framework | [Sphere10-Pascal-Framework.txt](Sphere10-Pascal-Framework.txt) | [Framework LICENSE](https://github.com/HermanSchoenfeld/PascalFramework/blob/master/LICENSE) | +| Ararat Synapse | [Synapse.txt](Synapse.txt) | [`src/libraries/synapse/blcksock.pas`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/synapse/blcksock.pas) | +| GNU gettext for Delphi | [GNU-gettext.txt](GNU-gettext.txt) | [`src/libraries/gnugettext/gnugettext.pas`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/gnugettext/gnugettext.pas) | +| LoadingCircle | [LoadingCircle.txt](LoadingCircle.txt) | [`src/libraries/sphere10/UCommon.UI.pas`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/sphere10/UCommon.UI.pas) | +| Generics.Collections | [Generics-Collections.txt](Generics-Collections.txt), [LGPL](FreePascal-RTL-COPYING.txt), [linking exception](FreePascal-RTL-exception.txt) | [`src/libraries/generics.collections/generics.collections.pas`](https://github.com/PascalCoin/PascalCoin/blob/master/src/libraries/generics.collections/generics.collections.pas) | +| OpenSSL 1.1 | [OpenSSL-1.1.txt](OpenSSL-1.1.txt) | [OpenSSL 1.1.1 license](https://github.com/openssl/openssl/blob/OpenSSL_1_1_1-stable/LICENSE) | +| Lazarus Component Library | [Overview](Lazarus-COPYING.txt), [LGPL](Lazarus-LGPL.txt), [linking exception](Lazarus-modifiedLGPL.txt) | [Sources](https://gitlab.com/freepascal.org/lazarus/lazarus) | +| Free Pascal runtime | [LGPL](FreePascal-RTL-COPYING.txt), [linking exception](FreePascal-RTL-exception.txt) | [Sources](https://gitlab.com/freepascal.org/fpc/source/-/tree/main/rtl) | + +This product includes software developed by the OpenSSL Project +for use in the OpenSSL Toolkit (http://www.openssl.org/). +This product includes cryptographic software written by Eric Young +(eay@cryptsoft.com). This product includes software written by Tim Hudson +(tjh@cryptsoft.com). diff --git a/licenses/SimpleBaseLib4Pascal.txt b/licenses/SimpleBaseLib4Pascal.txt new file mode 100644 index 000000000..b99377d17 --- /dev/null +++ b/licenses/SimpleBaseLib4Pascal.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 - 2019 Ugochukwu Mmaduekwe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/Sphere10-Pascal-Framework.txt b/licenses/Sphere10-Pascal-Framework.txt new file mode 100644 index 000000000..dc5427fb0 --- /dev/null +++ b/licenses/Sphere10-Pascal-Framework.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2017–2026 Herman Schoenfeld + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/Synapse.txt b/licenses/Synapse.txt new file mode 100644 index 000000000..3351bfa18 --- /dev/null +++ b/licenses/Synapse.txt @@ -0,0 +1,43 @@ +{==============================================================================| +| Project : Ararat Synapse | 009.009.001 | +|==============================================================================| +| Content: Library base | +|==============================================================================| +| Copyright (c)1999-2013, Lukas Gebauer | +| All rights reserved. | +| | +| Redistribution and use in source and binary forms, with or without | +| modification, are permitted provided that the following conditions are met: | +| | +| Redistributions of source code must retain the above copyright notice, this | +| list of conditions and the following disclaimer. | +| | +| Redistributions in binary form must reproduce the above copyright notice, | +| this list of conditions and the following disclaimer in the documentation | +| and/or other materials provided with the distribution. | +| | +| Neither the name of Lukas Gebauer nor the names of its contributors may | +| be used to endorse or promote products derived from this software without | +| specific prior written permission. | +| | +| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | +| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | +| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | +| ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR | +| ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | +| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | +| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | +| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | +| LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | +| OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH | +| DAMAGE. | +|==============================================================================| +| The Initial Developer of the Original Code is Lukas Gebauer (Czech Republic).| +| Portions created by Lukas Gebauer are Copyright (c)1999-2013. | +| All Rights Reserved. | +|==============================================================================| +| Contributor(s): | +|==============================================================================| +| History: see HISTORY.HTM from distribution package | +| (Found at URL: http://www.ararat.cz/synapse/) | +|==============================================================================} diff --git a/release-notes.md b/release-notes.md index 7bcc0027c..1241de195 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,2 +1,36 @@ -# PascalCoin v5.7.3 -- WIP +# PascalCoin 5.9.5 + +The macOS wallets now run natively on Apple silicon (M1 and later) and on Intel +Macs. Download `PascalCoin-5.9.5-macOS.dmg`, open it and drag the apps to +Applications: + +- PascalCoin Wallet, the classic wallet. +- PascalCoin Wallet Experimental, the newer interface. + +The apps are not notarized by Apple, so macOS asks for confirmation on the first +start. On macOS 15 and later, click Done on the warning, then open System +Settings, Privacy & Security and click Open Anyway. On macOS 14 and earlier, +Control-click the app and choose Open. + +Wallet, node and storage behavior is unchanged from 5.9.4. The READMEs have been +rewritten to be clear and concise. + +## Applications + +| Application | Executable | macOS app | +|---|---|---| +| Classic wallet | `PascalCoinWalletLazarus` | PascalCoin Wallet | +| Experimental wallet | `PascalCoinWalletExperimental` | PascalCoin Wallet Experimental | +| Daemon | `pascalcoin_daemon` | | +| Reference miner | `PascalCoinMiner` | | + +Windows executables have the `.exe` suffix. Windows and Linux archives include +the OpenSSL 1.1 runtime, which must stay beside the executables. The macOS build +uses CryptoLib4Pascal and does not require OpenSSL. The macOS `.tar.gz` archive +keeps all four programs for command-line use. Its daemon and miner are Intel +builds, which Apple silicon runs through Rosetta. Linux wallets require the native GTK2 +runtime libraries. On Linux, launch the executables from the extracted folder so +the bundled runtime can be found. + +See [Build 5.9.5](https://github.com/PascalCoin/PascalCoin/blob/5.9.5/CHANGELOG.md#build-595) +for the complete change list. diff --git a/resources/logo.svg b/resources/logo.svg index b115423cc..fa584dc72 100644 --- a/resources/logo.svg +++ b/resources/logo.svg @@ -1 +1 @@ -PascalLogo-2019 +PascalCoin — Geometric \ No newline at end of file diff --git a/src/core/UAES.pas b/src/core/UAES.pas index a84f9325e..0bee61f48 100644 --- a/src/core/UAES.pas +++ b/src/core/UAES.pas @@ -106,7 +106,8 @@ function EVP_GetSalt: TBytes; try // Key first If EVP_DigestInit_ex(pctx, hash, nil)<>1 then exit; - If EVP_DigestUpdate(pctx, @APassword[0], Length(APassword))<>1 then exit; + if Length(APassword) > 0 then + If EVP_DigestUpdate(pctx, @APassword[0], Length(APassword))<>1 then exit; if (ASalt <> nil) then begin if EVP_DigestUpdate(pctx, @ASalt[0], Length(ASalt))<>1 then exit; end; @@ -115,7 +116,8 @@ function EVP_GetSalt: TBytes; // Derive IV next If EVP_DigestInit_ex(pctx, hash, nil)<>1 then exit; If EVP_DigestUpdate(pctx, @Key[0], mds)<>1 then exit; - If EVP_DigestUpdate(pctx, @APassword[0], Length(APassword))<>1 then exit; + if Length(APassword) > 0 then + If EVP_DigestUpdate(pctx, @APassword[0], Length(APassword))<>1 then exit; if (ASalt <> nil) then begin if EVP_DigestUpdate(pctx, @ASalt[0], Length(ASalt))<>1 then exit; end; diff --git a/src/core/UAbstractMemBlockchainStorage.pas b/src/core/UAbstractMemBlockchainStorage.pas index bbb1850ab..0736645cc 100644 --- a/src/core/UAbstractMemBlockchainStorage.pas +++ b/src/core/UAbstractMemBlockchainStorage.pas @@ -56,12 +56,17 @@ TBlockchainStorageStats = record TAbstractMemBlockchainStorage = Class(TStorage) private FFileMem : TFileMem; - FStorageLock : TCriticalSection; + FStorageLock : TReadWriteLock; + FWriteFailureLock : TReadWriteLock; + FWriteFailure : String; FAutoFlushCache: Boolean; FUseMultithread: Boolean; FSaveStorageStats : TBlockchainStorageStats; FPSaveStorageStats: PBlockchainStorageStats; procedure SetUseMultithread(const Value: Boolean); + function HasWriteFailure: Boolean; + procedure CheckWriteFailure; + procedure ReportWriteFailure(AError: Exception); type TOrphanInformation = record orphan : string; @@ -172,8 +177,7 @@ TPendingData = record procedure ThreadHasFinishedCurrentJob; public procedure AddPendingData(const APendingData : TPendingData); - constructor Create(AStorage : TAbstractMemBlockchainStorage; AAMBTreeTOperationRawDataByRightOpHash : TAMBTreeTOperationRawDataByRightOpHash; - AAMBTreeTAffectedAccountByAccountBlockOpBlock : TAMBTreeTAffectedAccountByAccountBlockOpBlock); + constructor Create(AStorage : TAbstractMemBlockchainStorage; AAMBTreeTOperationRawDataByRightOpHash : TAMBTreeTOperationRawDataByRightOpHash; AAMBTreeTAffectedAccountByAccountBlockOpBlock : TAMBTreeTAffectedAccountByAccountBlockOpBlock); destructor Destroy; override; property MaxThreads : Integer read FMaxThreads write SetMaxThreads; function PendingsCount : Integer; @@ -218,6 +222,20 @@ TPendingData = record procedure OnCacheMemFlushedCache(const ASender : TCacheMem; const AProcessDesc : String; AElapsedMilis: Int64); procedure OnCacheMemLog(ASender : TObject; const ALog : String); protected + type + // The file borrows its owning storage until all workers and the file are + // destroyed. A failed logical index must never become stable on cleanup. + TStorageFileMem = class(TFileMem) + protected + FStorage: TAbstractMemBlockchainStorage; + function ShouldFlushOnDestroy: Boolean; override; + function AbsoluteWrite(const AAbsolutePosition: Int64; const ABuffer; ASize: Integer): Integer; override; + public + constructor Create(AStorage: TAbstractMemBlockchainStorage; const AFileName: String; AReadOnly: Boolean); + function New(AMemSize: TAbstractMemSize): TAMZone; override; + function Write(const APosition: Int64; const ABuffer; ASize: Integer): Integer; override; + end; + function CreateFileMem(const AFileName: String; AReadOnly: Boolean): TFileMem; virtual; procedure SetReadOnly(const Value: Boolean); override; Function DoGetBlockInformation(const ABlock : Integer; var AOperationBlock : TOperationBlock; var AOperationsCount : Integer; var AVolume : Int64) : Boolean; override; @@ -242,6 +260,8 @@ TPendingData = record public Constructor Create(AOwner : TComponent); Override; Destructor Destroy; Override; + // Explicit flush ignores AutoFlushCache, but never retries a failed index. + function FlushCache: Boolean; procedure FinalizedUpdating; procedure CheckConsistency(ARaiseOnError: Boolean; AMessages: TStrings; AThread : TPCThread); overload; function CheckConsistency(const AOrphan : String; AFromBlock , AToBlock : Integer; ARaiseOnError : Boolean; AMessages : TStrings; out ABlocksFound, AOperationsFound, AAffectedAccountsFound : Integer; AThread : TPCThread) : Boolean; overload; @@ -282,6 +302,130 @@ TPendingData = record implementation +uses UNode; + +function IsBackendWriteFailure(AError: Exception): Boolean; +begin + Result := (AError is EStreamError) or (AError is EOSError) or + (AError is EInOutError) or (AError is EFileMem) or (AError is EAbstractMem) or + (AError is EAbstractMemBlockchainStorage); +end; + +constructor TAbstractMemBlockchainStorage.TStorageFileMem.Create(AStorage: TAbstractMemBlockchainStorage; const AFileName: String; AReadOnly: Boolean); +begin + FStorage := AStorage; + inherited Create(AFileName,AReadOnly); +end; + +function TAbstractMemBlockchainStorage.TStorageFileMem.ShouldFlushOnDestroy: Boolean; +begin + Result := not Assigned(FStorage) or not FStorage.HasWriteFailure; +end; + +function TAbstractMemBlockchainStorage.TStorageFileMem.New(AMemSize: TAbstractMemSize): TAMZone; +begin + // Hold the existing recursive file lock through failure publication. A flush + // already waiting for this write must see the latch before it can certify it. + LockCache; + try + FStorage.CheckWriteFailure; + try + Result := inherited New(AMemSize); + except + on E: Exception do begin + FStorage.ReportWriteFailure(E); + raise; + end; + end; + finally + UnlockCache; + end; +end; + +function TAbstractMemBlockchainStorage.TStorageFileMem.AbsoluteWrite(const AAbsolutePosition: Int64; const ABuffer; ASize: Integer): Integer; +begin + try + Result := inherited AbsoluteWrite(AAbsolutePosition,ABuffer,ASize); + if Result<>ASize then raise EWriteError.Create('Incomplete AbstractMem blockchain file write'); + except + on E: Exception do begin + // Cache eviction may write from a read path too. Publish before the + // caller releases the file lock, not only from the worker's outer catch. + FStorage.ReportWriteFailure(E); + raise; + end; + end; +end; + +function TAbstractMemBlockchainStorage.TStorageFileMem.Write(const APosition: Int64; const ABuffer; ASize: Integer): Integer; +begin + LockCache; + try + FStorage.CheckWriteFailure; + try + Result := inherited Write(APosition,ABuffer,ASize); + if Result<>ASize then raise EWriteError.Create('Incomplete AbstractMem blockchain index write'); + except + on E: Exception do begin + FStorage.ReportWriteFailure(E); + raise; + end; + end; + finally + UnlockCache; + end; +end; + +function TAbstractMemBlockchainStorage.CreateFileMem(const AFileName: String; AReadOnly: Boolean): TFileMem; +begin + Result := TStorageFileMem.Create(Self,AFileName,AReadOnly); +end; + +function TAbstractMemBlockchainStorage.HasWriteFailure: Boolean; +begin + // Failure readers copy only immutable published state. First publication is + // exclusive and reports to the node only after releasing this short guard. + FWriteFailureLock.AcquireReadLock; + try + Result := FWriteFailure<>''; + finally + FWriteFailureLock.ReleaseReadLock; + end; +end; + +procedure TAbstractMemBlockchainStorage.CheckWriteFailure; +var LFailure: String; +begin + FWriteFailureLock.AcquireReadLock; + try + LFailure := FWriteFailure; + finally + FWriteFailureLock.ReleaseReadLock; + end; + if LFailure<>'' then + raise EWriteError.Create('AbstractMem blockchain storage requires restart: '+LFailure); +end; + +procedure TAbstractMemBlockchainStorage.ReportWriteFailure(AError: Exception); +var LFirst: Boolean; LFileName: String; +begin + if not IsBackendWriteFailure(AError) then Exit; + FWriteFailureLock.AcquireWriteLock; + try + LFirst := FWriteFailure=''; + if LFirst then FWriteFailure := AError.ClassName+': '+AError.Message; + finally + FWriteFailureLock.ReleaseWriteLock; + end; + // Never stop or join a worker from its own error callback. The node copies + // this error and publishes notifier flags; normal ownership retires workers. + LFileName := FStorageFilename; + // A failed inherited file constructor can report before the factory assigns + // FFileMem. Preserve that original I/O error rather than dereferencing nil. + if Assigned(FFileMem) then LFileName := FFileMem.FileName; + if LFirst and TNode.NodeExists and (TNode.Node.Bank=Bank) then + TNode.Node.ReportStorageFailure(sfsBlockStream,LFileName,AError); +end; function Comparer_TOrphanInformation_By_Orphan(const ALeft, ARight : TAbstractMemBlockchainStorage.TOrphanInformation) : Integer; begin @@ -348,9 +492,7 @@ procedure TAbstractMemBlockchainStorage.BlockNotFound(ABlock: Integer; var AFoun end; end; -function TAbstractMemBlockchainStorage.CheckBlockConsistency( - ARaiseOnError: Boolean; AMessages: TStrings; const ABlockInformation : TBlockInformation; out AOperationsCount, AAffectedAccountsCount : Integer; - AThread : TPCThread): Boolean; +function TAbstractMemBlockchainStorage.CheckBlockConsistency( ARaiseOnError: Boolean; AMessages: TStrings; const ABlockInformation : TBlockInformation; out AOperationsCount, AAffectedAccountsCount : Integer; AThread : TPCThread): Boolean; var LErrorsCount : Integer; procedure AddInfo(AIsError : Boolean; const AMessage : String); Var LTxt : String; @@ -522,10 +664,7 @@ procedure TAbstractMemBlockchainStorage.CheckConsistency(ARaiseOnError: Boolean; [LOrphansCount,LBlocksFound,LOperationsFound,LAffectedAccountsFound]),False); end; -function TAbstractMemBlockchainStorage.CheckConsistency(const AOrphan: String; - AFromBlock, AToBlock: Integer; - ARaiseOnError: Boolean; AMessages: TStrings; - out ABlocksFound, AOperationsFound, AAffectedAccountsFound : Integer; AThread : TPCThread): Boolean; +function TAbstractMemBlockchainStorage.CheckConsistency(const AOrphan: String; AFromBlock, AToBlock: Integer; ARaiseOnError: Boolean; AMessages: TStrings; out ABlocksFound, AOperationsFound, AAffectedAccountsFound : Integer; AThread : TPCThread): Boolean; var LMessages : TStringList; LErrorsCount : Integer; procedure AddInfo(AIsError : Boolean; const AMessage : String); @@ -674,7 +813,10 @@ constructor TAbstractMemBlockchainStorage.Create(AOwner: TComponent); FOperationsRawData_By_Block_OpBlock_Index := Nil; FAffectedAccounts_By_Account_Block_OpBlock := Nil; FAutoFlushCache := True; - FStorageLock := TCriticalSection.Create; + // Storage operations may recursively load/save and move shared file cursors; + // these application leases retain write ownership even on apparent reads. + FStorageLock := TReadWriteLock.Create('TAbstractMemBlockchainStorage.Storage',lrpSupportsRecursion); + FWriteFailureLock := TReadWriteLock.Create('TAbstractMemBlockchainStorage.WriteFailure',lrpSupportsRecursion); FPendingToSave := Nil; end; @@ -754,6 +896,7 @@ destructor TAbstractMemBlockchainStorage.Destroy; FreeAndNil(FAffectedAccounts_By_Account_Block_OpBlock); FreeAndNil(FStorageLock); + FreeAndNil(FWriteFailureLock); inherited; end; @@ -767,8 +910,7 @@ function TAbstractMemBlockchainStorage.DoBlockExists(Block: Cardinal): Boolean; end; end; -function TAbstractMemBlockchainStorage.DoBlockExistsByOrphan(ABlock: Integer; - const AOrphan: String; var LBlockInformation: TBlockInformation): Boolean; +function TAbstractMemBlockchainStorage.DoBlockExistsByOrphan(ABlock: Integer; const AOrphan: String; var LBlockInformation: TBlockInformation): Boolean; var LSearch : TBlockInformation; LDataPos : TAbstractMemPosition; begin @@ -786,25 +928,25 @@ procedure TAbstractMemBlockchainStorage.DoBlockNotFound(ABlock: Integer; var AFo procedure TAbstractMemBlockchainStorage.DoDeleteBlockChainBlocks(StartingDeleteBlock: Cardinal); begin - FStorageLock.Acquire; + FStorageLock.AcquireWriteLock; try while DeleteBlockChainBlockExt(StartingDeleteBlock,Orphan) do inc(StartingDeleteBlock); FinalizedUpdating; finally - FStorageLock.Release; + FStorageLock.ReleaseWriteLock; end; end; procedure TAbstractMemBlockchainStorage.DoEraseStorage; begin - FStorageLock.Acquire; + FStorageLock.AcquireWriteLock; Try FFileMem.ClearContent(FFileMem.Is64Bits,FFileMem.MemUnitsSize); FreeAndNil(FFileMem); DoInitialize; FinalizedUpdating; Finally - FStorageLock.Release; + FStorageLock.ReleaseWriteLock; End; end; @@ -836,9 +978,7 @@ function TAbstractMemBlockchainStorage.DoFindOperation(const AOpHash: TBytes; va end; -function TAbstractMemBlockchainStorage.DoGetAccountOperations(AAccount, - AMaxDepth, AStartOperation, AMaxOperations, ASearchBackwardsStartingAtBlock: Integer; - const AOperationsResumeList: TOperationsResumeList): Boolean; +function TAbstractMemBlockchainStorage.DoGetAccountOperations(AAccount, AMaxDepth, AStartOperation, AMaxOperations, ASearchBackwardsStartingAtBlock: Integer; const AOperationsResumeList: TOperationsResumeList): Boolean; var LSearch,LFound : TAffectedAccount; LOperation : TPCOperation; LOPR : TOperationResume; @@ -912,9 +1052,7 @@ function TAbstractMemBlockchainStorage.DoGetAccountOperations(AAccount, end; -function TAbstractMemBlockchainStorage.DoGetBlockInformation(const ABlock : Integer; - var AOperationBlock: TOperationBlock; var AOperationsCount: Integer; - var AVolume: Int64): Boolean; +function TAbstractMemBlockchainStorage.DoGetBlockInformation(const ABlock : Integer; var AOperationBlock: TOperationBlock; var AOperationsCount: Integer; var AVolume: Int64): Boolean; var LBlock,LFoundBlock : TBlockInformation; LDataPos : TAbstractMemPosition; begin @@ -931,11 +1069,7 @@ function TAbstractMemBlockchainStorage.DoGetBlockInformation(const ABlock : Inte end else Result := False; end; -function TAbstractMemBlockchainStorage.DoGetBlockOperations(ABlock, - AOpBlockStartIndex, AMaxOperations: Integer; - var AOperationBlock: TOperationBlock; var AOperationsCount: Integer; - var AVolume: Int64; - const AOperationsResumeList: TOperationsResumeList): Boolean; +function TAbstractMemBlockchainStorage.DoGetBlockOperations(ABlock, AOpBlockStartIndex, AMaxOperations: Integer; var AOperationBlock: TOperationBlock; var AOperationsCount: Integer; var AVolume: Int64; const AOperationsResumeList: TOperationsResumeList): Boolean; var LFound,LSearch : TOperationRawData; LOperation : TPCOperation; LOPR : TOperationResume; @@ -1015,7 +1149,7 @@ function TAbstractMemBlockchainStorage.DoInitialize: Boolean; FStorageFilename := Bank.GetStorageFolder(Bank.Orphan)+PathDelim+'BlockChainStream.am_blocks'; end; - FFileMem := TFileMem.Create(FStorageFilename,ReadOnly); + FFileMem := CreateFileMem(FStorageFilename,ReadOnly); FFileMem.IncreaseFileBytes := 10 * 1024*1024; // 10Mb each increase LCacheMem := FFileMem.LockCache; @@ -1139,9 +1273,7 @@ function TAbstractMemBlockchainStorage.DoLoadBlockChain(Operations: TPCOperation if Not Result then BlockNotFound(Block,Result); end; -function TAbstractMemBlockchainStorage.DoLoadBlockChainExt( - Operations: TPCOperationsComp; Block: Cardinal; - const AOrphan: String): Boolean; +function TAbstractMemBlockchainStorage.DoLoadBlockChainExt( Operations: TPCOperationsComp; Block: Cardinal; const AOrphan: String): Boolean; var LBlock,LFoundBlock : TBlockInformation; LDataPos : TAbstractMemPosition; begin @@ -1158,7 +1290,7 @@ function TAbstractMemBlockchainStorage.DoMoveBlockChain(StartBlock : Cardinal; C var LPCOperationsComp : TPCOperationsComp; begin Assert(Orphan<>DestOrphan,'Orphan and Destorphan are equals'); - FStorageLock.Acquire; + FStorageLock.AcquireWriteLock; try LPCOperationsComp := TPCOperationsComp.Create(Nil); try @@ -1173,18 +1305,25 @@ function TAbstractMemBlockchainStorage.DoMoveBlockChain(StartBlock : Cardinal; C Result := True; FinalizedUpdating; finally - FStorageLock.Release; + FStorageLock.ReleaseWriteLock; end; end; function TAbstractMemBlockchainStorage.DoSaveBlockChain(Operations: TPCOperationsComp): Boolean; begin - FStorageLock.Acquire; + FStorageLock.AcquireWriteLock; try - Result := DoSaveBlockChainExt(Operations,Orphan,FSaveStorageStats); - FinalizedUpdating; + try + Result := DoSaveBlockChainExt(Operations,Orphan,FSaveStorageStats); + FinalizedUpdating; + except + on E: Exception do begin + ReportWriteFailure(E); + raise; + end; + end; finally - FStorageLock.Release; + FStorageLock.ReleaseWriteLock; end; end; @@ -1202,6 +1341,7 @@ function TAbstractMemBlockchainStorage.DoSaveBlockChainExt(Operations: TPCOperat LPendingData : TPendingData; LTC : TTickCount; begin + CheckWriteFailure; Result := True; LBlockInformation.Clear; LOperationRawData.Clear; @@ -1321,22 +1461,41 @@ procedure TAbstractMemBlockchainStorage.FillInfo(AStrings: TStrings); end; -procedure TAbstractMemBlockchainStorage.FinalizedUpdating; -var LTC : TTickCount; +function TAbstractMemBlockchainStorage.FlushCache: Boolean; begin - if FAutoFlushCache then begin - LTC := TPlatform.GetTickCount; - FileMem.FlushCache; + // The failure lock is independent of FStorageLock. Never acquire the storage + // lock here: producers already use storage -> file ordering, and a worker can + // publish a failure while it owns the file lock. + FileMem.LockCache; + try + CheckWriteFailure; + try + Result := FileMem.FlushCache; + if not Result then + raise EWriteError.Create('Cannot flush AbstractMem blockchain storage '+FileMem.FileName); + except + on E: Exception do begin + ReportWriteFailure(E); + raise; + end; + end; + finally + FileMem.UnlockCache; end; end; +procedure TAbstractMemBlockchainStorage.FinalizedUpdating; +begin + CheckWriteFailure; + if FAutoFlushCache then FlushCache; +end; + function TAbstractMemBlockchainStorage.GetFirstBlockNumber: Int64; begin Result := GetFirstBlockNumberByOrphan(Orphan); end; -function TAbstractMemBlockchainStorage.GetFirstBlockNumberByOrphan( - const AOrphan: String): Int64; +function TAbstractMemBlockchainStorage.GetFirstBlockNumberByOrphan(const AOrphan: String): Int64; var LBlockInformation,LBlockInformationFound : TBlockInformation; LPos : TAbstractMemPosition; begin @@ -1373,14 +1532,12 @@ function TAbstractMemBlockchainStorage.GetLastBlockNumberByOrphan(const AOrphan: end else Result := LBlockInformationFound.operationBlock.block; end; -procedure TAbstractMemBlockchainStorage.OnCacheMemFlushedCache( - const ASender: TCacheMem; const AProcessDesc: String; AElapsedMilis: Int64); +procedure TAbstractMemBlockchainStorage.OnCacheMemFlushedCache(const ASender: TCacheMem; const AProcessDesc: String; AElapsedMilis: Int64); begin TLog.NewLog(ltdebug,ASender.ClassName,Self.ClassName+' '+AProcessDesc) end; -procedure TAbstractMemBlockchainStorage.OnCacheMemLog(ASender: TObject; - const ALog: String); +procedure TAbstractMemBlockchainStorage.OnCacheMemLog(ASender: TObject; const ALog: String); begin TLog.NewLog(ltdebug,ASender.ClassName,Self.ClassName+' '+ALog); end; @@ -1413,15 +1570,15 @@ procedure TAbstractMemBlockchainStorage.SetUseMultithread(const Value: Boolean); var i : Integer; begin - if FUseMultithread=Value then Exit; - FStorageLock.Acquire; + if (FUseMultithread=Value) and (Value or not Assigned(FPendingToSave)) then Exit; + FStorageLock.AcquireWriteLock; Try if Assigned(FPendingToSave) then begin i := FPendingToSave.PendingsCount; if i>0 then begin TLog.NewLog(ltinfo,ClassName,Format('Finalizing use of multitrheads with %d pending jobs',[i])); end; - while (FPendingToSave.PendingsCount>0) do begin + while (FPendingToSave.PendingsCount>0) and not HasWriteFailure do begin sleep(1); end; if i>0 then begin @@ -1431,17 +1588,17 @@ procedure TAbstractMemBlockchainStorage.SetUseMultithread(const Value: Boolean); FreeAndNil(FPendingToSave); FUseMultithread := Value; if FUseMultithread then begin + CheckWriteFailure; FPendingToSave := TPendingToSave.Create(Self,FOperationsRawData_By_RightOpHash,FAffectedAccounts_By_Account_Block_OpBlock); end; Finally - FStorageLock.Release; + FStorageLock.ReleaseWriteLock; End; end; { TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByBlock } -procedure TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.DeletedData( - const AData: TBlockInformation); +procedure TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.DeletedData(const AData: TBlockInformation); begin inherited; if AData.rawDataPosition>0 then begin @@ -1449,8 +1606,7 @@ procedure TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrpha end; end; -function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.GetBlockInformationByBlock( - ABlock: Integer): TBlockInformation; +function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.GetBlockInformationByBlock( ABlock: Integer): TBlockInformation; var LPos : TAbstractMemPosition; LBlockSearch : TBlockInformation; begin @@ -1459,8 +1615,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphan if Not FindData(LBlockSearch,LPos,Result) then Result.Clear; end; -function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.LoadData( - const APosition: TAbstractMemPosition): TBlockInformation; +function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.LoadData(const APosition: TAbstractMemPosition): TBlockInformation; var LZone : TAMZone; LBytes : TBytes; begin @@ -1478,8 +1633,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphan raise EAbstractMemBTree.Create(Format('%s.LoadData Invalid FromSerialized call with %d bytes at pos %d',[Self.ClassName,LZone.size,APosition])); end; -function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.SaveData( - const AData: TBlockInformation): TAMZone; +function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphanBlock.SaveData(const AData: TBlockInformation): TAMZone; var LBytes : TBytes; begin LBytes := AData.ToSerialized; @@ -1489,8 +1643,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeOperationBlockInformationByOrphan { TAbstractMemBlockchainStorage.TAMBTreeTOperationRawDataByRightOpHash } -function TAbstractMemBlockchainStorage.TAMBTreeTOperationRawDataByRightOpHash.LoadData( - const APosition: TAbstractMemPosition): TOperationRawData; +function TAbstractMemBlockchainStorage.TAMBTreeTOperationRawDataByRightOpHash.LoadData(const APosition: TAbstractMemPosition): TOperationRawData; var LZone : TAMZone; LBytes : TBytes; begin @@ -1508,8 +1661,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeTOperationRawDataByRightOpHash.Lo raise EAbstractMemBTree.Create(Format('%s.LoadData Invalid FromSerialized call with %d bytes at pos %d',[Self.ClassName,LZone.size,APosition])); end; -function TAbstractMemBlockchainStorage.TAMBTreeTOperationRawDataByRightOpHash.SaveData( - const AData: TOperationRawData): TAMZone; +function TAbstractMemBlockchainStorage.TAMBTreeTOperationRawDataByRightOpHash.SaveData(const AData: TOperationRawData): TAMZone; var LBytes : TBytes; begin LBytes := AData.ToSerialized; @@ -1519,8 +1671,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeTOperationRawDataByRightOpHash.Sa { TAbstractMemBlockchainStorage.TAMBTreeTAffectedAccountByAccountBlockOpBlock } -function TAbstractMemBlockchainStorage.TAMBTreeTAffectedAccountByAccountBlockOpBlock.LoadData( - const APosition: TAbstractMemPosition): TAffectedAccount; +function TAbstractMemBlockchainStorage.TAMBTreeTAffectedAccountByAccountBlockOpBlock.LoadData(const APosition: TAbstractMemPosition): TAffectedAccount; var LZone : TAMZone; LBytes : TBytes; begin @@ -1538,8 +1689,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeTAffectedAccountByAccountBlockOpB raise EAbstractMemBTree.Create(Format('%s.LoadData Invalid FromSerialized call with %d bytes at pos %d',[Self.ClassName,LZone.size,APosition])); end; -function TAbstractMemBlockchainStorage.TAMBTreeTAffectedAccountByAccountBlockOpBlock.SaveData( - const AData: TAffectedAccount): TAMZone; +function TAbstractMemBlockchainStorage.TAMBTreeTAffectedAccountByAccountBlockOpBlock.SaveData(const AData: TAffectedAccount): TAMZone; var LBytes : TBytes; begin LBytes := AData.ToSerialized; @@ -1567,8 +1717,7 @@ procedure TAbstractMemBlockchainStorage.TBlockInformation.CopyFrom(const ASource Self.rawDataPosition := ASource.rawDataPosition; end; -function TAbstractMemBlockchainStorage.TBlockInformation.CreateTPCOperationsComp( - AAbstractMem : TAbstractMem; ABank: TPCBank): TPCOperationsComp; +function TAbstractMemBlockchainStorage.TBlockInformation.CreateTPCOperationsComp( AAbstractMem : TAbstractMem; ABank: TPCBank): TPCOperationsComp; begin Result := TPCOperationsComp.Create(ABank); try @@ -1604,8 +1753,7 @@ function TAbstractMemBlockchainStorage.TBlockInformation.FromSerialized(ABytes: Result := True; end; -function TAbstractMemBlockchainStorage.TBlockInformation.GetRawData( - AAbstractMem: TAbstractMem; var ARawData: TBytes): Boolean; +function TAbstractMemBlockchainStorage.TBlockInformation.GetRawData( AAbstractMem: TAbstractMem; var ARawData: TBytes): Boolean; var LZone : TAMZone; begin if Self.rawDataPosition<=0 then begin @@ -1624,8 +1772,7 @@ function TAbstractMemBlockchainStorage.TBlockInformation.IsOrphan(const AOrphan: Result := TAbstractMemBlockchainStorage.OrphanCompare(Self.orphan,AOrphan)=0; end; -procedure TAbstractMemBlockchainStorage.TBlockInformation.ReadTPCOperationsComp( - AAbstractMem: TAbstractMem; AOperationsComp: TPCOperationsComp); +procedure TAbstractMemBlockchainStorage.TBlockInformation.ReadTPCOperationsComp( AAbstractMem: TAbstractMem; AOperationsComp: TPCOperationsComp); var LStream : TStream; LRaw : TBytes; LErrors : String; @@ -1681,8 +1828,7 @@ procedure TAbstractMemBlockchainStorage.TOperationRawData.Clear; Self.rawData := Nil; end; -procedure TAbstractMemBlockchainStorage.TOperationRawData.CopyFrom(const - ASource: TOperationRawData); +procedure TAbstractMemBlockchainStorage.TOperationRawData.CopyFrom(const ASource: TOperationRawData); begin Self.rightOpHash := Copy(ASource.rightOpHash); Self.account := ASource.account; @@ -1694,8 +1840,7 @@ procedure TAbstractMemBlockchainStorage.TOperationRawData.CopyFrom(const Self.rawData := Copy(ASource.rawData); end; -function TAbstractMemBlockchainStorage.TOperationRawData.CreateTPCOperation( - AAbstractMem: TAbstractMem; out APCOperation: TPCOperation): Boolean; +function TAbstractMemBlockchainStorage.TOperationRawData.CreateTPCOperation( AAbstractMem: TAbstractMem; out APCOperation: TPCOperation): Boolean; var LOpClass: TPCOperationClass; LStream : TStream; begin @@ -1744,8 +1889,7 @@ function TAbstractMemBlockchainStorage.TOperationRawData.CreateTPCOperation(AAbs End; end; -function TAbstractMemBlockchainStorage.TOperationRawData.FromSerialized( - ABytes: TBytes): Boolean; +function TAbstractMemBlockchainStorage.TOperationRawData.FromSerialized( ABytes: TBytes): Boolean; var LStream : TStream; LBuild : Word; begin @@ -1770,16 +1914,14 @@ function TAbstractMemBlockchainStorage.TOperationRawData.FromSerialized( Result := True; end; -procedure TAbstractMemBlockchainStorage.TOperationRawData.SetToFindByBlockOpblock( - ABlock, AOpblock: Integer); +procedure TAbstractMemBlockchainStorage.TOperationRawData.SetToFindByBlockOpblock( ABlock, AOpblock: Integer); begin Self.Clear; Self.block := Ablock; Self.opblock := AOpblock; end; -procedure TAbstractMemBlockchainStorage.TOperationRawData.SetToFindByRightOpHash( - const ARightOpHash: TBytes); +procedure TAbstractMemBlockchainStorage.TOperationRawData.SetToFindByRightOpHash(const ARightOpHash: TBytes); begin Self.Clear; Self.rightOpHash := Copy(ARightOpHash); @@ -1818,8 +1960,7 @@ procedure TAbstractMemBlockchainStorage.TAffectedAccount.Clear; Self.opblock := -1; // -1 = No opblock end; -procedure TAbstractMemBlockchainStorage.TAffectedAccount.CopyFrom( - const ASource: TAffectedAccount); +procedure TAbstractMemBlockchainStorage.TAffectedAccount.CopyFrom(const ASource: TAffectedAccount); begin Self.account := ASource.account; Self.n_operation := ASource.n_operation; @@ -1827,8 +1968,7 @@ procedure TAbstractMemBlockchainStorage.TAffectedAccount.CopyFrom( Self.opblock := ASource.opblock; end; -function TAbstractMemBlockchainStorage.TAffectedAccount.FromSerialized( - ABytes: TBytes): Boolean; +function TAbstractMemBlockchainStorage.TAffectedAccount.FromSerialized( ABytes: TBytes): Boolean; var LStream : TStream; LBuild : Word; begin @@ -1848,15 +1988,13 @@ function TAbstractMemBlockchainStorage.TAffectedAccount.FromSerialized( Result := True; end; -procedure TAbstractMemBlockchainStorage.TAffectedAccount.SetToFindByAccount( - AAccount: Integer); +procedure TAbstractMemBlockchainStorage.TAffectedAccount.SetToFindByAccount( AAccount: Integer); begin Self.Clear; Self.account := AAccount; end; -procedure TAbstractMemBlockchainStorage.TAffectedAccount.SetToFindByAccountBlockOpblock( - AAccount, ABlock, AOpblock: Integer); +procedure TAbstractMemBlockchainStorage.TAffectedAccount.SetToFindByAccountBlockOpblock( AAccount, ABlock, AOpblock: Integer); begin Self.Clear; Self.account := AAccount; @@ -1901,8 +2039,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.GetRegs end else Result := 0; end; -function TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.LoadData( - const APosition: TAbstractMemPosition): TOrphanInformation; +function TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.LoadData(const APosition: TAbstractMemPosition): TOrphanInformation; var LZone : TAMZone; LBytes : TBytes; LStream : TStream; @@ -1924,8 +2061,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.LoadDat End; end; -function TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.SaveData( - const AData: TOrphanInformation): TAMZone; +function TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.SaveData(const AData: TOrphanInformation): TAMZone; var LBytes : TBytes; LStream : TStream; @@ -1943,8 +2079,7 @@ function TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.SaveDat AbstractMem.Write(Result.position,LBytes[0],Length(LBytes)); end; -procedure TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.Update( - const AOrphan: String; AIncrement: Integer); +procedure TAbstractMemBlockchainStorage.TAMBTreeOrphanInformationByOrphan.Update(const AOrphan: String; AIncrement: Integer); var LUpdate, LUpdateFound : TOrphanInformation; LPos : TAbstractMemPosition; begin @@ -1969,8 +2104,7 @@ procedure TAbstractMemBlockchainStorage.TOrphanInformation.Clear; Self.regsCounter := 0; end; -procedure TAbstractMemBlockchainStorage.TOrphanInformation.CopyFrom( - const ASource: TOrphanInformation); +procedure TAbstractMemBlockchainStorage.TOrphanInformation.CopyFrom(const ASource: TOrphanInformation); begin Self.orphan := ASource.orphan; Self.regsCounter := ASource.regsCounter; @@ -2011,8 +2145,7 @@ procedure TAbstractMemBlockchainStorageSecondary.DoBlockNotFound(ABlock: Integer end; end; -procedure TAbstractMemBlockchainStorageSecondary.DoDeleteBlockChainBlocks( - StartingDeleteBlock: Cardinal); +procedure TAbstractMemBlockchainStorageSecondary.DoDeleteBlockChainBlocks( StartingDeleteBlock: Cardinal); begin inherited; if Assigned(FAuxStorage) then begin @@ -2062,8 +2195,7 @@ function TAbstractMemBlockchainStorageSecondary.DoInitialize: Boolean; end; end; -function TAbstractMemBlockchainStorageSecondary.DoMoveBlockChain( - StartBlock: Cardinal; const DestOrphan: TOrphan): Boolean; +function TAbstractMemBlockchainStorageSecondary.DoMoveBlockChain( StartBlock: Cardinal; const DestOrphan: TOrphan): Boolean; begin Result := inherited; if (Result) and (Assigned(FAuxStorage)) then begin @@ -2071,8 +2203,7 @@ function TAbstractMemBlockchainStorageSecondary.DoMoveBlockChain( end; end; -function TAbstractMemBlockchainStorageSecondary.DoSaveBlockChain( - Operations: TPCOperationsComp): Boolean; +function TAbstractMemBlockchainStorageSecondary.DoSaveBlockChain( Operations: TPCOperationsComp): Boolean; begin Result := inherited; if (Result) and (Assigned(FAuxStorage)) and (Not FSaving) then begin @@ -2080,8 +2211,7 @@ function TAbstractMemBlockchainStorageSecondary.DoSaveBlockChain( end; end; -procedure TAbstractMemBlockchainStorageSecondary.SetReadOnly( - const Value: Boolean); +procedure TAbstractMemBlockchainStorageSecondary.SetReadOnly(const Value: Boolean); begin inherited; if (Assigned(FAuxStorage)) then begin @@ -2099,60 +2229,72 @@ procedure TAbstractMemBlockchainStorage.TPendingToSaveThread.BCExecute; begin // nLastBatch := 0; - while (Not Terminated) do begin - LPendingList := FPendingToSave.FPending.LockList; - try - if LPendingList.Count>0 then begin - LZero := LPendingList.Items[0]; - LPending := LPendingList.Items[LPendingList.Count-1]; - LPendingList.Delete(LPendingList.Count-1); - FBusy := True; - if (FPendingToSave.FAMStorage.LogSaveActivity) and (TPlatform.GetElapsedMilliseconds(FPendingToSave.FLastLogTC)>10000) then begin - - TLog.NewLog(ltdebug,ClassName,Format('Pendings %d (%d/%d..%d/%d) - %s', - [LPendingList.Count+1, LZero.operation.block, LZero.operation.opblock, - LPending.operation.block, LPending.operation.opblock, FPendingToSave.FAMStorage.FSaveStorageStats.ToString])); - FPendingToSave.FLastLogTC := TPlatform.GetTickCount; - FPendingToSave.FAMStorage.FSaveStorageStats.Clear; + try + while (Not Terminated) and not FPendingToSave.FAMStorage.HasWriteFailure do begin + LPendingList := FPendingToSave.FPending.LockList; + try + if LPendingList.Count>0 then begin + LZero := LPendingList.Items[0]; + LPending := LPendingList.Items[LPendingList.Count-1]; + LPendingList.Delete(LPendingList.Count-1); + FBusy := True; + if (FPendingToSave.FAMStorage.LogSaveActivity) and (TPlatform.GetElapsedMilliseconds(FPendingToSave.FLastLogTC)>10000) then begin + + TLog.NewLog(ltdebug,ClassName,Format('Pendings %d (%d/%d..%d/%d) - %s', + [LPendingList.Count+1, LZero.operation.block, LZero.operation.opblock, + LPending.operation.block, LPending.operation.opblock, FPendingToSave.FAMStorage.FSaveStorageStats.ToString])); + FPendingToSave.FLastLogTC := TPlatform.GetTickCount; + FPendingToSave.FAMStorage.FSaveStorageStats.Clear; + end; + end else FBusy := False; + finally + FPendingToSave.FPending.UnlockList; + end; + if (FBusy) then begin + try + inc(nLastBatch); + // Here will not terminate until finished job (or raised exception) + DebugStep := Format('Block %d opBlock %d',[LPending.operation.block,LPending.operation.opblock]); + if not FPendingToSave.FOperationsRawData_By_RightOpHash.AddData(LPending.operation) then + raise EAbstractMemBlockchainStorage.Create(Format('Cannot add operation block %d opBlock %d',[LPending.operation.block,LPending.operation.opblock])); + Inc(FPendingToSave.FAMStorage.FSaveStorageStats.operationRawDataCount); + // + for i := 0 to High(LPending.affectedAccounts) do begin + DebugStep := Format('Block %d opBlock %d Account %d %d/%d',[LPending.operation.block,LPending.operation.opblock,LPending.affectedAccounts[i].account,i+1,Length(LPending.affectedAccounts)]); + if not FPendingToSave.FAffectedAccounts_By_Account_Block_OpBlock.AddData(LPending.affectedAccounts[i]) then begin + raise EAbstractMemBlockchainStorage.Create(Format('Cannot add affected account %d %d/%d in block %d opBlock %d', + [LPending.affectedAccounts[i].account, + i+1,Length(LPending.affectedAccounts), + LPending.affectedAccounts[i].block,LPending.affectedAccounts[i].opblock])); + end; + Inc(FPendingToSave.FAMStorage.FSaveStorageStats.affectedAccountCount); + end; + finally + // Busy describes one borrowed job, including exceptional unwind. A dead + // worker must not indefinitely block completion checks by its peers. + FBusy := False; end; - end else FBusy := False; - finally - FPendingToSave.FPending.UnlockList; - end; - if (FBusy) then begin - inc(nLastBatch); - // Here will not terminate until finished job (or raised exception) - DebugStep := Format('Block %d opBlock %d',[LPending.operation.block,LPending.operation.opblock]); - if not FPendingToSave.FOperationsRawData_By_RightOpHash.AddData(LPending.operation) then - raise EAbstractMemBlockchainStorage.Create(Format('Cannot add operation block %d opBlock %d',[LPending.operation.block,LPending.operation.opblock])); - Inc(FPendingToSave.FAMStorage.FSaveStorageStats.operationRawDataCount); - // - for i := 0 to High(LPending.affectedAccounts) do begin - DebugStep := Format('Block %d opBlock %d Account %d %d/%d',[LPending.operation.block,LPending.operation.opblock,LPending.affectedAccounts[i].account,i+1,Length(LPending.affectedAccounts)]); - if not FPendingToSave.FAffectedAccounts_By_Account_Block_OpBlock.AddData(LPending.affectedAccounts[i]) then begin - raise EAbstractMemBlockchainStorage.Create(Format('Cannot add affected account %d %d/%d in block %d opBlock %d', - [LPending.affectedAccounts[i].account, - i+1,Length(LPending.affectedAccounts), - LPending.affectedAccounts[i].block,LPending.affectedAccounts[i].opblock])); + end else begin + if (nLastBatch>0) then begin + TLog.NewLog(ltdebug,ClassName,Format('Finished %d operations... waiting for more - %s', + [nLastBatch, FPendingToSave.FAMStorage.FSaveStorageStats.ToString])); + nLastBatch := 0; + FPendingToSave.FAMStorage.FSaveStorageStats.Clear; + FPendingToSave.FLastLogTC := FPendingToSave.FAMStorage.FSaveStorageStats.startTC; + FPendingToSave.ThreadHasFinishedCurrentJob; // Notify in order to flush when all threads terminated end; - Inc(FPendingToSave.FAMStorage.FSaveStorageStats.affectedAccountCount); - end; - end else begin - if (nLastBatch>0) then begin - TLog.NewLog(ltdebug,ClassName,Format('Finished %d operations... waiting for more - %s', - [nLastBatch, FPendingToSave.FAMStorage.FSaveStorageStats.ToString])); - nLastBatch := 0; - FPendingToSave.FAMStorage.FSaveStorageStats.Clear; - FPendingToSave.FLastLogTC := FPendingToSave.FAMStorage.FSaveStorageStats.startTC; - FPendingToSave.ThreadHasFinishedCurrentJob; // Notify in order to flush when all threads terminated + Sleep(10); end; - Sleep(10); + end; + except + on E: Exception do begin + FPendingToSave.FAMStorage.ReportWriteFailure(E); + raise; end; end; end; -constructor TAbstractMemBlockchainStorage.TPendingToSaveThread.Create( - APendingToSave: TPendingToSave); +constructor TAbstractMemBlockchainStorage.TPendingToSaveThread.Create( APendingToSave: TPendingToSave); begin FBusy := True; FPendingToSave := APendingToSave; @@ -2167,8 +2309,10 @@ procedure TAbstractMemBlockchainStorage.TPendingToSave.AddPendingData(const APen var LPendings : TList; LCount : Integer; begin + FAMStorage.CheckWriteFailure; LPendings := FPending.LockList; Try + FAMStorage.CheckWriteFailure; LPendings.Add(APendingData); LCount := LPendings.Count; inc(FTotal); @@ -2178,6 +2322,7 @@ procedure TAbstractMemBlockchainStorage.TPendingToSave.AddPendingData(const APen if MaxThreads<1 then begin SetMaxThreads(1); repeat + FAMStorage.CheckWriteFailure; sleep(1); until PendingsCount=0; SetMaxThreads(0); @@ -2185,15 +2330,14 @@ procedure TAbstractMemBlockchainStorage.TPendingToSave.AddPendingData(const APen if (MaxPendingsCount>0) And (LCount>=MaxPendingsCount) then begin while (PendingsCount>=MaxPendingsCount) and (MaxPendingsCount>0) do begin + FAMStorage.CheckWriteFailure; Sleep(10); end; end; + FAMStorage.CheckWriteFailure; end; -constructor TAbstractMemBlockchainStorage.TPendingToSave.Create( - AStorage : TAbstractMemBlockchainStorage; - AAMBTreeTOperationRawDataByRightOpHash: TAMBTreeTOperationRawDataByRightOpHash; - AAMBTreeTAffectedAccountByAccountBlockOpBlock: TAMBTreeTAffectedAccountByAccountBlockOpBlock); +constructor TAbstractMemBlockchainStorage.TPendingToSave.Create(AStorage : TAbstractMemBlockchainStorage; AAMBTreeTOperationRawDataByRightOpHash: TAMBTreeTOperationRawDataByRightOpHash; AAMBTreeTAffectedAccountByAccountBlockOpBlock: TAMBTreeTAffectedAccountByAccountBlockOpBlock); begin FAMStorage := AStorage; FTotal := 0; @@ -2236,6 +2380,8 @@ function TAbstractMemBlockchainStorage.TPendingToSave.PendingsCount: Integer; procedure TAbstractMemBlockchainStorage.TPendingToSave.SetMaxThreads(const Value: Integer); var i : Integer; ListTh : TList; + LRetired: TList; + LThread: TPendingToSaveThread; begin {$IFDEF HIGHLOG} if Value<>FMaxThreads then begin @@ -2245,31 +2391,40 @@ procedure TAbstractMemBlockchainStorage.TPendingToSave.SetMaxThreads(const Value if Value<0 then FMaxThreads := 0 else if Value>16 then FMaxThreads := 16 else FMaxThreads := Value; + if FAMStorage.HasWriteFailure then FMaxThreads := 0; - ListTh := FThreads.LockList; + LRetired := TList.Create; try - // Clean terminateds... - for i := ListTh.Count-1 downto 0 do begin - if ListTh.Items[i].Terminated then begin - ListTh.Items[i].Free; - ListTh.Delete(i); + ListTh := FThreads.LockList; + try + // Clean terminateds... + for i := ListTh.Count-1 downto 0 do begin + if ListTh.Items[i].Terminated then begin + LRetired.Add(ListTh.Items[i]); + ListTh.Delete(i); + end; end; + // CREATE + while ListTh.Count; begin + if FAMStorage.HasWriteFailure then Exit; + if FMaxThreads=0 then Exit; if PendingsCount>0 then Exit; ListTh := FThreads.LockList; try diff --git a/src/core/UAccounts.pas b/src/core/UAccounts.pas index e2753af27..fa5e574fc 100644 --- a/src/core/UAccounts.pas +++ b/src/core/UAccounts.pas @@ -184,9 +184,33 @@ TAccount_Helper = record helper for TAccount TOrderedAccountList = Class; TOrderedBlockAccountList = Class; - { TProgressNotify } - - TProgressNotify = procedure(sender : TObject; const message : String; curPos, totalCount : Int64) of object; + { Domain progress data. Positions retain the producer's raw counter semantics: + they may describe assigned work or an index rather than completed work. + Equal position/total does not signal overall completion. Consumers format + presentation text; the core publishes stages, counters and raw metadata. } + + TProgressStage = (psNone, psReadingSafebox, psValidatingOperationBlocks, + psCheckingSafeboxIntegrity, psReadingBlockBatch, psReadingBlockchain, + psLoadingPendingOperations, psReadingOperations, psPreparingOperations, + psValidatingSignatures, psApplyingOperations, psFinalizingOperations, + psFinalizingRestore, psDownloadingSafeboxChunks, psSavingSafebox, + psReceivedSafebox, psDownloadingBlockchain); + + TProgressContext = (pcNone, pcCheckingNewSafebox); + + // Create returns a value record; the compiler finalizes its managed strings. + // Asynchronous listeners retain a record copy, without manual destruction. + TProgressInfo = record + Stage: TProgressStage; + Context: TProgressContext; + CurrentPosition, TotalCount: Int64; + BlockNumber, BlockCount, StartBlock, EndBlock, OperationsCount, ChunksCount: Int64; + FileName, PeerAddress: String; + class function Create(AStage: TProgressStage; ACurrentPosition: Int64 = 0; + ATotalCount: Int64 = 0): TProgressInfo; static; + end; + + TProgressNotify = procedure(Sender: TObject; const AProgress: TProgressInfo) of object; { TProgressNotifyMany } @@ -197,7 +221,7 @@ TAccount_Helper = record helper for TAccount TProgressNotifyManyHelper = record helper for TProgressNotifyMany procedure Add(listener : TProgressNotify); procedure Remove(listener : TProgressNotify); - procedure Invoke(sender : TObject; const message : String; curPos, totalCount : Int64); + procedure Invoke(Sender: TObject; const AProgress: TProgressInfo); end; @@ -230,7 +254,8 @@ TProgressNotifyManyHelper = record helper for TProgressNotifyMany FListOfOrderedAccountKeysList : TList; FTotalBalance: Int64; FSafeBoxHash : TRawBytes; - FLock: TPCCriticalSection; // Thread safe + FLock: TReadWriteLock; // Thread safe + FConstructionComplete: Boolean; FWorkSum : UInt64; FCurrentProtocol: Integer; // Snapshots utility new on V3 @@ -254,6 +279,7 @@ TProgressNotifyManyHelper = record helper for TProgressNotifyMany function GetAggregatedHashrate: TBigNum; function GetOrderedAccountKeysList: TSafeboxPubKeysAndAccounts; function GetAccount(AAccountNumber : Integer; var AAccount : TAccount) : Boolean; + procedure ReleaseSnapshots; protected FTotalFee: Int64; Procedure UpdateAccount(account_number : Cardinal; const newAccountInfo: TAccountInfo; const newName : TRawBytes; newType : Word; @@ -495,7 +521,7 @@ function Check_Safebox_Names_Consistency(sb : TPCSafeBox; const title :String; v implementation uses - ULog, {$IFnDEF USE_ABSTRACTMEM} UAccountKeyStorage,{$ENDIF} math, UCommon, UPCOperationsBlockValidator, UPCTemporalFileStream, UEncoding; + ULog, {$IFnDEF USE_ABSTRACTMEM} UAccountKeyStorage,{$ENDIF} math, UCommon, UPCOperationsBlockValidator, UPCTemporalFileStream, UEncoding, UStreams; {$IFDEF FPC} @@ -711,8 +737,10 @@ class procedure TPascalCoinProtocol.CalcProofOfWork_Part3(const operationBlock: begin ms := TMemoryStream.Create; try - ms.WriteBuffer(operationBlock.initial_safe_box_hash[Low(operationBlock.initial_safe_box_hash)],length(operationBlock.initial_safe_box_hash)); - ms.WriteBuffer(operationBlock.operations_hash[Low(operationBlock.operations_hash)],length(operationBlock.operations_hash)); + if Length(operationBlock.initial_safe_box_hash)>0 then + ms.WriteBuffer(operationBlock.initial_safe_box_hash[Low(operationBlock.initial_safe_box_hash)],length(operationBlock.initial_safe_box_hash)); + if Length(operationBlock.operations_hash)>0 then + ms.WriteBuffer(operationBlock.operations_hash[Low(operationBlock.operations_hash)],length(operationBlock.operations_hash)); if operationBlock.protocol_version - ms.WriteBuffer(operationBlock.previous_proof_of_work[Low(operationBlock.previous_proof_of_work)],Length(operationBlock.previous_proof_of_work)); + if Length(operationBlock.previous_proof_of_work)>0 then + ms.WriteBuffer(operationBlock.previous_proof_of_work[Low(operationBlock.previous_proof_of_work)],Length(operationBlock.previous_proof_of_work)); end; SetLength(Part3,ms.Size); ms.Position := 0; @@ -796,11 +825,9 @@ class function TPascalCoinProtocol.CalcTotalBalance(ABlockCount: Cardinal): Int6 inc(Result, Int64(Int64(ABlockCount MOD CT_NewLineRewardDecrease) * LCurrReward)); end; -class function TPascalCoinProtocol.AllowUseHardcodedRandomHashTable( - const AHardcodedFileName: String; - const AHardcodedSha256Value: TRawBytes): Boolean; +class function TPascalCoinProtocol.AllowUseHardcodedRandomHashTable(const AHardcodedFileName: String; const AHardcodedSha256Value: TRawBytes): Boolean; var LTmp : TPCHardcodedRandomHashTable; - LFileStream : TFileStream; + LFileStream : TStream; LInternalHardcodedSha256 : TRawBytes; begin Result := False; @@ -817,6 +844,10 @@ class function TPascalCoinProtocol.AllowUseHardcodedRandomHashTable( try LFileStream := TFileStream.Create(AHardcodedFileName,fmOpenRead+fmShareDenyNone); try + // Combine small table-field reads. Shared access permits external changes, + // so keep length caching off and retain the existing file-sharing mode. + // Ownership transfers only after the decorator is constructed successfully. + LFileStream := TBufferedStream.Create(LFileStream,64*1024,True,False,True); if LTmp.LoadFromStream(LFileStream,LInternalHardcodedSha256) then begin if TBaseType.Equals(LInternalHardcodedSha256, AHardcodedSha256Value) then begin if Not Assigned(_INTERNAL_PascalCoinProtocol) then begin @@ -870,7 +901,9 @@ class procedure TPascalCoinProtocol.CalcProofOfWork(const operationBlock: TOpera ms.Write(operationBlock.protocol_available,Sizeof(operationBlock.protocol_available)); // Little endian ms.Write(operationBlock.compact_target,Sizeof(operationBlock.compact_target)); // Little endian // Part 2 - ms.WriteBuffer(operationBlock.block_payload[Low(operationBlock.block_payload)],Length(operationBlock.block_payload)); + // Empty miner payloads are valid; a zero-byte write must not index them. + if Length(operationBlock.block_payload)>0 then + ms.WriteBuffer(operationBlock.block_payload[Low(operationBlock.block_payload)],Length(operationBlock.block_payload)); // Part 3 ms.WriteBuffer(operationBlock.initial_safe_box_hash[Low(operationBlock.initial_safe_box_hash)],length(operationBlock.initial_safe_box_hash)); ms.WriteBuffer(operationBlock.operations_hash[Low(operationBlock.operations_hash)],length(operationBlock.operations_hash)); @@ -2046,6 +2079,17 @@ class procedure TAccountComp.ValidsEC_OpenSSL_NID(list: TList); list.Add((CT_NID_secp521r1)); // = 716 end; +{ TProgressInfo } + +class function TProgressInfo.Create(AStage: TProgressStage; + ACurrentPosition, ATotalCount: Int64): TProgressInfo; +begin + Result := Default(TProgressInfo); + Result.Stage := AStage; + Result.CurrentPosition := ACurrentPosition; + Result.TotalCount := ATotalCount; +end; + { TProgressNotifyManyHelper } procedure TProgressNotifyManyHelper.Add(listener : TProgressNotify); @@ -2060,11 +2104,11 @@ procedure TProgressNotifyManyHelper.Remove(listener : TProgressNotify); TArrayTool.Remove(self, listener); end; -procedure TProgressNotifyManyHelper.Invoke(sender : TObject; const message : String; curPos, totalCount : Int64); +procedure TProgressNotifyManyHelper.Invoke(Sender: TObject; const AProgress: TProgressInfo); var i : Integer; begin for i := low(self) to high(self) do - self[i](sender, message, curPos, totalCount); + self[i](Sender, AProgress); end; { TPCSafeBox } @@ -2650,7 +2694,9 @@ function TPCSafeBox.CalcBlockHashRateInHs(block_number: Cardinal; bn : TBigNum; LOpBlock : TOperationBlock; begin - FLock.Acquire; + // Block-info access can populate shared caches, so this calculation retains + // write ownership even though it returns a derived read-only value. + FLock.AcquireWriteLock; Try Result := TBigNum.Create; try @@ -2686,7 +2732,7 @@ function TPCSafeBox.CalcBlockHashRateInHs(block_number: Cardinal; Raise; end; Finally - FLock.Release; + FLock.ReleaseWriteLock; End; end; @@ -2817,12 +2863,30 @@ function TPCSafeBox.HasSnapshotForBlock(block_number: Cardinal): Boolean; end; end; +procedure TPCSafeBox.ReleaseSnapshots; +var i: Integer; + Psnapshot: PSafeboxSnapshot; +begin + if not Assigned(FSnapshots) then Exit; + for i := 0 to FSnapshots.Count-1 do begin + Psnapshot := (Fsnapshots[i]); + FreeAndNil(Psnapshot^.oldBlocks); + FreeAndNil(Psnapshot^.newBlocks); + FreeAndNil(Psnapshot^.namesAdded); + FreeAndNil(Psnapshot^.namesDeleted); + FreeAndNil(Psnapshot^.oldBufferBlocksHash); + FreeAndNil(Psnapshot^.oldAggregatedHashrate); + Psnapshot^.oldSafeBoxHash := Nil; + Dispose(Psnapshot); + end; + FSnapshots.Clear; +end; + procedure TPCSafeBox.Clear; Var i : Integer; {$IFnDEF USE_ABSTRACTMEM} P : PBlockAccount; {$ENDIF} - Psnapshot : PSafeboxSnapshot; begin StartThreadSafe; Try @@ -2840,18 +2904,7 @@ procedure TPCSafeBox.Clear; AggregatedHashrate.Value := 0; FOrderedByName.Clear; {$ENDIF} - for i := 0 to FSnapshots.Count-1 do begin - Psnapshot := (Fsnapshots[i]); - FreeAndNil(Psnapshot^.oldBlocks); - FreeAndNil(Psnapshot^.newBlocks); - FreeAndNil(Psnapshot^.namesAdded); - FreeAndNil(Psnapshot^.namesDeleted); - FreeAndNil(Psnapshot^.oldBufferBlocksHash); - FreeAndNil(Psnapshot^.oldAggregatedHashrate); - Psnapshot^.oldSafeBoxHash := Nil; - Dispose(Psnapshot); - end; - FSnapshots.Clear; + ReleaseSnapshots; For i:=0 to FListOfOrderedAccountKeysList.count-1 do begin TOrderedAccountKeysList( FListOfOrderedAccountKeysList[i] ).ClearAccounts(False); @@ -2935,9 +2988,24 @@ procedure TPCSafeBox.CopyFrom(accounts: TPCSafeBox); procedure TPCSafeBox.SetSafeboxFileName(ASafeboxFileName : String); var i : Integer; LOpBl : TOperationBlockExt; -begin - FPCAbstractMem.Free; + LAccountCacheLimit : Integer; + LHasAccountCacheLimit : Boolean; +begin + // A file reload replaces storage, not the caller's bounded cache policy. + // Preserve only this configured limit; entries and other transient state + // belong to the new storage instance. Construction still defaults to 50,000. + LAccountCacheLimit := 0; + LHasAccountCacheLimit := Assigned(FPCAbstractMem); + if LHasAccountCacheLimit then begin + LAccountCacheLimit := FPCAbstractMem.AccountCache.MaxRegisters; + // Surface write failures before releasing the old owner. Destructors only + // clean up; a failed switch must retain its original storage instance. + FPCAbstractMem.FlushCache; + end; + FreeAndNil(FPCAbstractMem); FPCAbstractMem := TPCAbstractMem.Create(ASafeboxFileName,False); + if LHasAccountCacheLimit then + FPCAbstractMem.AccountCache.MaxRegisters := LAccountCacheLimit; if FPCAbstractMem.BlocksCount>0 then begin LOpBl := FPCAbstractMem.GetBlockInfo( FPCAbstractMem.BlocksCount-1 ); FCurrentProtocol := LOpBl.operationBlock.protocol_version; @@ -2971,7 +3039,7 @@ procedure TPCSafeBox.ClearSafeboxfile; constructor TPCSafeBox.Create; begin FMaxSafeboxSnapshots:=CT_DEFAULT_MaxSafeboxSnapshots; - FLock := TPCCriticalSection.Create('TPCSafeBox_Lock'); + FLock := TReadWriteLock.Create('TPCSafeBox_Lock',lrpSupportsRecursion); {$IFDEF USE_ABSTRACTMEM} FPCAbstractMem := TPCAbstractMem.Create('',False); {$ELSE} @@ -2999,15 +3067,24 @@ constructor TPCSafeBox.Create; {$ENDIF} BufferBlocksHash.SafeBoxHashCalcType := sbh_Single_Sha256; Clear; + FConstructionComplete := True; end; destructor TPCSafeBox.Destroy; Var i : Integer; begin - Clear; - for i := 0 to FListOfOrderedAccountKeysList.Count - 1 do begin - TOrderedAccountKeysList( FListOfOrderedAccountKeysList[i] ).FAccountList := Nil; - end; + // A failed backend constructor leaves FPCAbstractMem nil after replacement. + // Cleanup must not calculate its hash and replace the original I/O exception + // with an access violation. Partial safebox construction needs the same path. + if FConstructionComplete + {$IFDEF USE_ABSTRACTMEM}and Assigned(FPCAbstractMem){$ENDIF} then + Clear + else + ReleaseSnapshots; + if Assigned(FListOfOrderedAccountKeysList) then + for i := 0 to FListOfOrderedAccountKeysList.Count - 1 do begin + TOrderedAccountKeysList( FListOfOrderedAccountKeysList[i] ).FAccountList := Nil; + end; {$IFDEF USE_ABSTRACTMEM} FreeAndNil(FPCAbstractMem); {$ELSE} @@ -3525,7 +3602,7 @@ function TPCSafeBox.BufferBlocksHash: TBytesBuffer32Safebox; procedure TPCSafeBox.EndThreadSave; begin - FLock.Release; + FLock.ReleaseWriteLock; end; function TPCSafeBox.LoadSafeBoxChunkFromStream(Stream : TStream; checkAll : Boolean; checkSafeboxHash : TRawBytes; progressNotify : TProgressNotify; previousCheckedSafebox : TPCSafebox; var ALastReadBlock : TBlockAccount; var errors : String) : Boolean; @@ -3548,6 +3625,7 @@ function TPCSafeBox.LoadSafeBoxChunkFromStream(Stream : TStream; checkAll : Bool LUseMultiThreadOperationsBlockValidator, LAddToMultiThreadOperationsBlockValidator : Boolean; LPCOperationsBlockValidator : TPCOperationsBlockValidator; LValidatedOPOk, LValidatedOPError, LValidatedOPPending : Integer; + LProgress: TProgressInfo; LBlockHashRate : TBigNum; begin Result := false; @@ -3630,7 +3708,12 @@ function TPCSafeBox.LoadSafeBoxChunkFromStream(Stream : TStream; checkAll : Bool for iblock := sbHeader.startBlock to sbHeader.endBlock do begin if (Assigned(progressNotify)) and ((TPlatform.GetElapsedMilliseconds(tc)>=300)) then begin tc := TPlatform.GetTickCount; - progressNotify(Self,Format('Reading Safebox block %d/%d',[iblock+1,sbHeader.blocksCount]),iblock,sbHeader.blocksCount+1); + LProgress := TProgressInfo.Create(psReadingSafebox, iblock, sbHeader.blocksCount+1); + LProgress.BlockNumber := iblock+1; + LProgress.BlockCount := sbHeader.blocksCount; + LProgress.StartBlock := sbHeader.startBlock; + LProgress.EndBlock := sbHeader.endBlock; + progressNotify(Self, LProgress); end; errors := Format('Corrupted stream reading block %d/%d of %d',[iblock+1,sbHeader.endBlock+1,sbHeader.blockscount]); if (checkAll) then begin @@ -3823,7 +3906,8 @@ function TPCSafeBox.LoadSafeBoxChunkFromStream(Stream : TStream; checkAll : Bool if LValidatedOPPending>0 then begin if (Assigned(progressNotify)) and ((TPlatform.GetElapsedMilliseconds(tc)>=500)) then begin tc := TPlatform.GetTickCount; - progressNotify(Self,Format('Validating OperationBlock info %d/%d',[LValidatedOPOk,LValidatedOPOk+LValidatedOPPending]),LValidatedOPOk,LValidatedOPOk+LValidatedOPPending); + progressNotify(Self, TProgressInfo.Create(psValidatingOperationBlocks, + LValidatedOPOk, LValidatedOPOk+LValidatedOPPending)); end else Sleep(10) end; until LValidatedOPPending<=0 ; @@ -3832,7 +3916,12 @@ function TPCSafeBox.LoadSafeBoxChunkFromStream(Stream : TStream; checkAll : Bool LPCOperationsBlockValidator.Free; end; if (Assigned(progressNotify)) And (sbHeader.ContainsLastBlock) then begin - progressNotify(Self,'Checking Safebox integrity',(sbHeader.endBlock-sbHeader.startBlock+1),(sbHeader.endBlock-sbHeader.startBlock+1)); + LProgress := TProgressInfo.Create(psCheckingSafeboxIntegrity, + sbHeader.endBlock-sbHeader.startBlock+1, sbHeader.endBlock-sbHeader.startBlock+1); + LProgress.StartBlock := sbHeader.startBlock; + LProgress.EndBlock := sbHeader.endBlock; + LProgress.BlockCount := sbHeader.endBlock-sbHeader.startBlock+1; + progressNotify(Self, LProgress); end; If checkAll then begin If (offsets[(sbHeader.endBlock-sbHeader.startBlock+1)]<>0) And (offsets[(sbHeader.endBlock-sbHeader.startBlock+1)]<>Stream.Position-posOffsetZone) then begin @@ -5046,7 +5135,9 @@ procedure TPCSafeBox.UpdateAccount(account_number : Cardinal; const newAccountIn procedure TPCSafeBox.StartThreadSafe; begin - TPCThread.ProtectEnterCriticalSection(Self,FLock); + // This public lease permits mutations and cache-backed reads. Preserve its + // recursive exclusive contract rather than inferring a read from its caller. + TPCThread.ProtectAcquireWriteLock(Self,FLock); end; { TPCSafeBoxTransaction } diff --git a/src/core/UBaseTypes.pas b/src/core/UBaseTypes.pas index 2b3fe1398..1d3ec2672 100644 --- a/src/core/UBaseTypes.pas +++ b/src/core/UBaseTypes.pas @@ -249,7 +249,8 @@ procedure TRawBytesHelper.FromStream(AStream: TStream; AStartPos, ALength: Integ begin System.SetLength(Self,ALength); AStream.Position := AStartPos; - AStream.Read(Self[0],ALength); + if ALength > 0 then + AStream.Read(Self[0],ALength); end; procedure TRawBytesHelper.SaveInsideTBytes(var ADestToWrite: TBytes; var AStartPosition: integer); @@ -260,7 +261,8 @@ procedure TRawBytesHelper.SaveInsideTBytes(var ADestToWrite: TBytes; var AStartP end; Lsize := Length(Self); Move(Lsize, ADestToWrite[AStartPosition], 2); - Move(Self[0], ADestToWrite[AStartPosition + 2], Lsize); + if LSize > 0 then + Move(Self[0], ADestToWrite[AStartPosition + 2], Lsize); Inc(AStartPosition, 2 + Lsize); end; @@ -272,7 +274,8 @@ function TRawBytesHelper.LoadFromTBytes(const ASource: TBytes; var AStartPositio Move(ASource[AStartPosition],Lsize,2); if (AStartPosition + 2 + Lsize > Length(ASource)) then Exit(False); SetLength(Self,Lsize); - Move(ASource[AStartPosition + 2],Self[0],Lsize); + if LSize > 0 then + Move(ASource[AStartPosition + 2],Self[0],Lsize); inc(AStartPosition, 2 + Lsize); Result := True; end; @@ -284,7 +287,8 @@ function TRawBytesHelper.ToSerialized: TBytes; if LSize>65536 then raise Exception.Create('Cannot serialize TBytes due high length '+IntToStr(Length(Self))); SetLength(Result, LSize + 2); Move(Lsize, Result[0], 2); - Move(Self[0], Result[2], Lsize); + if LSize > 0 then + Move(Self[0], Result[2], Lsize); end; procedure TRawBytesHelper.ToSerialized(const AStream: TStream); @@ -293,7 +297,8 @@ procedure TRawBytesHelper.ToSerialized(const AStream: TStream); LSize := Length(Self); if LSize>65536 then raise Exception.Create('Cannot serialize TBytes due high length '+IntToStr(Length(Self))); AStream.Write(LSize,2); - AStream.Write(Self[0],LSize); + if LSize > 0 then + AStream.Write(Self[0],LSize); end; function TRawBytesHelper.GetSerializedLength: integer; @@ -404,7 +409,8 @@ class procedure TBaseType.To20Bytes(const source: TRawBytes; var dest: T20Bytes) FillByte(dest[0],20,0); i := Length(source); if (i>20) then i:=20; - move(source[Low(source)],dest[0],i); + if i > 0 then + move(source[Low(source)],dest[0],i); end; class function TBaseType.To20Bytes(const source: TRawBytes): T20Bytes; @@ -453,7 +459,8 @@ class procedure TBaseType.To32Bytes(const source: TRawBytes; var dest: T32Bytes) FillByte(dest[0],32,0); i := length(source); if (i>32) then i:=32; - move(source[Low(source)],dest[0],i); + if i > 0 then + move(source[Low(source)],dest[0],i); end; class procedure TBaseType.Fill0(var dest: T32Bytes); diff --git a/src/core/UBlockChain.pas b/src/core/UBlockChain.pas index 65da5bd76..541f94446 100644 --- a/src/core/UBlockChain.pas +++ b/src/core/UBlockChain.pas @@ -336,6 +336,7 @@ TMultiOpData = record FTotalFee : Int64; FMax0feeOperationsBySigner : Integer; FHasOpRecoverOperations : Boolean; + FDeferLookupIndexes : Boolean; // Only an unpublished sanitizer scratch tree may defer these indexes. function InternalCanAddOperationToHashTree(lockedThreadList : TList; op : TPCOperation) : Boolean; function InternalAddOperationToHashTree(list : TList; op : TPCOperation; CalcNewHashTree : Boolean) : Boolean; Function FindOrderedByOpReference(lockedThreadList : TList; const Value: TOpReference; var Index: Integer): Boolean; @@ -343,6 +344,9 @@ TMultiOpData = record Function FindOrderedByAccountData(lockedThreadList : TList; const account_number : Cardinal; var Index: Integer): Boolean; function GetHashTree: TRawBytes; procedure SetMax0feeOperationsBySigner(const Value: Integer); + procedure MaterializeLookupIndexes; + function CanReuseLookupIndexes(AReplayed : TOperationsHashTree; const AOriginalHash : TRawBytes) : Boolean; + procedure AdoptReplayedMetadata(AReplayed : TOperationsHashTree); public Constructor Create; Destructor Destroy; Override; @@ -357,8 +361,8 @@ TMultiOpData = record Property TotalAmount : Int64 read FTotalAmount; Property TotalFee : Int64 read FTotalFee; function SaveOperationsHashTreeToStream(AStream: TStream; ASaveToStorage : Boolean): Boolean; - function LoadOperationsHashTreeFromStream(AStream: TStream; ALoadingFromStorage : Boolean; ASetOperationsToProtocolVersion : Word; ALoadFromStorageVersion : Word; APreviousUpdatedBlocks : TAccountPreviousBlockInfo; var AErrors : String): Boolean; overload; - function LoadOperationsHashTreeFromStream(AStream: TStream; ALoadingFromStorage : Boolean; ASetOperationsToProtocolVersion : Word; ALoadFromStorageVersion : Word; APreviousUpdatedBlocks : TAccountPreviousBlockInfo; AAllow0FeeOperations : Boolean; var AOperationsCount, AProcessedCount : Integer; var AErrors : String): Boolean; overload; + function LoadOperationsHashTreeFromStream(AStream: TStream; ALoadingFromStorage : Boolean; ASetOperationsToProtocolVersion : Word; ALoadFromStorageVersion : Word; APreviousUpdatedBlocks : TAccountPreviousBlockInfo; var AErrors : String; AProgressNotify : TProgressNotify = Nil): Boolean; overload; + function LoadOperationsHashTreeFromStream(AStream: TStream; ALoadingFromStorage : Boolean; ASetOperationsToProtocolVersion : Word; ALoadFromStorageVersion : Word; APreviousUpdatedBlocks : TAccountPreviousBlockInfo; AAllow0FeeOperations : Boolean; var AOperationsCount, AProcessedCount : Integer; var AErrors : String; AProgressNotify : TProgressNotify = Nil): Boolean; overload; function IndexOfOperation(op : TPCOperation) : Integer; function CountOperationsBySameSignerWithoutFee(account_number : Cardinal) : Integer; Procedure Delete(index : Integer); @@ -386,7 +390,7 @@ TMultiOpData = record FIsOnlyOperationBlock: Boolean; FStreamPoW : TMemoryStream; FDisableds : Integer; - FOperationsLock : TPCCriticalSection; + FOperationsLock : TReadWriteLock; FPreviousUpdatedBlocks : TAccountPreviousBlockInfo; // New Protocol V3 struct to store previous updated blocks FHasValidOperationBlockInfo : Boolean; function GetOperation(index: Integer): TPCOperation; @@ -487,9 +491,10 @@ TMultiOpData = record private FBank : TPCBank; FReadOnly: Boolean; - FPendingBufferOperationsStream : TFileStream; + FPendingBufferOperationsStream : TStream; + FPendingLoadProgressNotify : TProgressNotify; procedure SetBank(const Value: TPCBank); - Function GetPendingBufferOperationsStream : TFileStream; + Function GetPendingBufferOperationsStream : TStream; protected FIsMovingBlockchain : Boolean; FStorageFilename: String; @@ -524,7 +529,7 @@ TMultiOpData = record Function Initialize : Boolean; Procedure EraseStorage; // Erase Blockchain storage Procedure SavePendingBufferOperations(OperationsHashTree : TOperationsHashTree); - Procedure LoadPendingBufferOperations(OperationsHashTree : TOperationsHashTree); + Procedure LoadPendingBufferOperations(OperationsHashTree : TOperationsHashTree; AProgressNotify : TProgressNotify = Nil); Function BlockExists(Block : Cardinal) : Boolean; function Orphan : String; @@ -547,7 +552,7 @@ TMultiOpData = record FLastOperationBlock: TOperationBlock; FIsRestoringFromFile: Boolean; FOnLog: TPCBankLog; - FBankLock: TPCCriticalSection; + FBankLock: TReadWriteLock; FNotifyList : TList; FStorageClass: TStorageClass; FOrphan: TOrphan; @@ -603,12 +608,14 @@ TMultiOpData = record implementation uses + {$IFDEF FPC}{$IFDEF MSWINDOWS}Windows,{$ENDIF}{$ENDIF} + {$IFDEF FPC}Generics.Defaults,{$ELSE}System.Generics.Defaults,{$ENDIF} Variants, UTime, UConst, UOpTransaction, UPCOrderedLists, UPCOperationsSignatureValidator, UPCOperationsBlockValidator, - UAbstractMemBlockchainStorage, - UNode; + UAbstractMemBlockchainStorage, UFileMem, UAbstractMem, {$IFNDEF USE_ABSTRACTMEM}UPCAbstractMem,{$ENDIF} + UNode, UStreams; { TPCOperationsStorage } @@ -700,6 +707,10 @@ procedure TPCOperationsStorage.AddPCOperation(APCOperation: TPCOperation); list : TList; iPos : Integer; begin + // Published operations may be shared by several trees and read concurrently. + // Fill both lazy digest caches before exposing the object to other readers. + APCOperation.Sha256; + APCOperation.RipeMD160; list := LockPCOperationsStorage; try if FindOrderedByPtrPCOperation(list,APCOperation,iPos) then begin @@ -813,12 +824,18 @@ function TPCBank.AccountsCount: Cardinal; end; function TPCBank.AddNewBlockChainBlock(Operations: TPCOperationsComp; MaxAllowedTimestamp : Cardinal; var errors: String): Boolean; -Var i : Integer; +var i: Integer; LPersistenceFailed: Boolean; begin - TPCThread.ProtectEnterCriticalSection(Self,FBankLock); + TPCThread.ProtectAcquireWriteLock(Self,FBankLock); Try Result := False; errors := ''; + LPersistenceFailed := False; + // Once canonical storage fails, no later buffered/mined block may mutate it. + if TNode.NodeExists and (TNode.Node.Bank=Self) and TNode.Node.HasStorageFailure then begin + errors := 'Block processing paused after a storage failure; restart required'; + Exit; + end; Operations.Lock; // New Protection Try If Not Operations.ValidateOperationBlock(errors) then begin @@ -834,8 +851,20 @@ function TPCBank.AddNewBlockChainBlock(Operations: TPCOperationsComp; MaxAllowed // WINNER !!! // Congrats! - if Not Operations.SafeBoxTransaction.Commit(Operations.OperationBlock,errors) then begin - exit; + try + if Not Operations.SafeBoxTransaction.Commit(Operations.OperationBlock,errors) then Exit; + except + on E: Exception do begin + // A file-backed commit can evict dirty pages before the later explicit + // flush. This is a storage failure, not an invalid peer block. + if (E is EStreamError) or (E is EOSError) or (E is EFileMem) or + (E is EAbstractMem) or (E is EPCAbstractMem) then begin + LPersistenceFailed := True; + if TNode.NodeExists and (TNode.Node.Bank=Self) then + TNode.Node.ReportStorageFailure(sfsSafeboxFlush,{$IFDEF USE_ABSTRACTMEM}SafeBox.PCAbstractMem.FileName{$ELSE}'safebox'{$ENDIF},E); + end; + raise; + end; end; // Initialize values @@ -853,19 +882,26 @@ function TPCBank.AddNewBlockChainBlock(Operations: TPCOperationsComp; MaxAllowed TCrypto.ToHexaString(SafeBox.SafeBoxHash)])); // Save Operations to disk if Not FIsRestoringFromFile then begin - Storage.SaveBlockChainBlock(Operations); + try + Storage.SaveBlockChainBlock(Operations); + except + LPersistenceFailed := True; + raise; + end; end; FLastBlockCache.CopyFrom(Operations); Operations.Clear(true); Result := true; Finally if Not Result then begin - NewLog(Operations, lterror, 'Invalid new block '+inttostr(Operations.OperationBlock.block)+': ' + errors+ ' > '+TPCOperationsComp.OperationBlockToText(Operations.OperationBlock)); + if LPersistenceFailed then + NewLog(Operations,ltError,'Cannot persist block '+IntToStr(Operations.OperationBlock.block)+'; block processing is paused') + else NewLog(Operations, lterror, 'Invalid new block '+inttostr(Operations.OperationBlock.block)+': ' + errors+ ' > '+TPCOperationsComp.OperationBlockToText(Operations.OperationBlock)); end; Operations.Unlock; End; Finally - FBankLock.Release; + FBankLock.ReleaseWriteLock; End; if Result then begin for i := 0 to FNotifyList.Count - 1 do begin @@ -909,7 +945,9 @@ constructor TPCBank.Create(AOwner: TComponent); inherited; FStorage := Nil; FStorageClass := Nil; - FBankLock := TPCCriticalSection.Create('TPCBank_BANKLOCK'); + // Load/commit paths can call other bank operations while holding this lease. + // Preserve recursive write ownership across those existing callbacks. + FBankLock := TReadWriteLock.Create('TPCBank_BANKLOCK',lrpSupportsRecursion); FIsRestoringFromFile := False; FOnLog := Nil; FSafeBox := TPCSafeBox.Create; @@ -955,6 +993,7 @@ procedure TPCBank.DiskRestoreFromOperations(max_block : Int64; restoreProgressNo i,j, LProgressBlock, LProgressEndBlock, LOpsInBlocks : Integer; LSafeboxTransaction : TPCSafeBoxTransaction; LTempSafebox : TPCSafeBox; + LProgress : TProgressInfo; begin if FIsRestoringFromFile then begin TLog.NewLog(lterror,Classname,'Is Restoring!!!'); @@ -962,7 +1001,7 @@ procedure TPCBank.DiskRestoreFromOperations(max_block : Int64; restoreProgressNo end; tc := TPlatform.GetTickCount; LStartProcessTC := tc; - TPCThread.ProtectEnterCriticalSection(Self,FBankLock); + TPCThread.ProtectAcquireWriteLock(Self,FBankLock); try FIsRestoringFromFile := true; try @@ -1015,7 +1054,11 @@ procedure TPCBank.DiskRestoreFromOperations(max_block : Int64; restoreProgressNo if (LBlocks.Count=0) then Exit; if Assigned(restoreProgressNotify) then begin - restoreProgressNotify(Self,Format('Reading blocks from %d to %d with %d operations',[BlocksCount,i,LOpsInBlocks]),0,0); + LProgress := TProgressInfo.Create(psReadingBlockBatch); + LProgress.StartBlock := BlocksCount; + LProgress.EndBlock := i; + LProgress.OperationsCount := LOpsInBlocks; + restoreProgressNotify(Self,LProgress); end; TPCOperationsBlockValidator.MultiThreadValidateOperationsBlock(LBlocks); @@ -1040,7 +1083,10 @@ procedure TPCBank.DiskRestoreFromOperations(max_block : Int64; restoreProgressNo end; if (Assigned(restoreProgressNotify)) And (TPlatform.GetElapsedMilliseconds(tc)>1000) then begin tc := TPlatform.GetTickCount; - restoreProgressNotify(Self,Format('Reading blockchain block %d/%d',[LBlocks[i].OperationBlock.block,Storage.LastBlock]),LProgressBlock,LProgressEndBlock); + LProgress := TProgressInfo.Create(psReadingBlockchain,LProgressBlock,LProgressEndBlock); + LProgress.BlockNumber := LBlocks[i].OperationBlock.block; + LProgress.BlockCount := Storage.LastBlock; + restoreProgressNotify(Self,LProgress); end; end; end; @@ -1069,7 +1115,7 @@ procedure TPCBank.DiskRestoreFromOperations(max_block : Int64; restoreProgressNo SafeBox.PCAbstractMem.FlushCache; {$ENDIF} finally - FBankLock.Release; + FBankLock.ReleaseWriteLock; end; end; @@ -1100,6 +1146,7 @@ function TPCBank.DoSaveBank: Boolean; begin Result := true; LBankfilename := GetSafeboxCheckpointingFileName(GetStorageFolder(Orphan),BlocksCount); + if LBankfilename='' then raise EWriteError.Create('Cannot create safebox checkpoint directory'); if (LBankfilename<>'') then begin LTC := TPlatform.GetTickCount; {$IFDEF USE_ABSTRACTMEM} @@ -1130,14 +1177,20 @@ function TPCBank.DoSaveBank: Boolean; if (Orphan='') And ((BlocksCount MOD (CT_BankToDiskEveryNBlocks*100))=0) then begin Laux_newfilename := GetStorageFolder('') + PathDelim+'checkpoint_'+ inttostr(BlocksCount)+CT_Safebox_Extension; try - {$IFDEF FPC} - DoCopyFile(LBankfilename,Laux_newfilename); + {$IFDEF USE_ABSTRACTMEM} + // Monthly copies have the same publication guarantees as rotating ones. + SafeBox.SaveCheckpointing(Laux_newfilename); {$ELSE} - CopyFile(PWideChar(LBankfilename),PWideChar(Laux_newfilename),False); + {$IFDEF FPC} + DoCopyFile(LBankfilename,Laux_newfilename); + {$ELSE} + if not CopyFile(PWideChar(LBankfilename),PWideChar(Laux_newfilename),False) then RaiseLastOSError; + {$ENDIF} {$ENDIF} Except On E:Exception do begin TLog.NewLog(lterror,ClassName,'Exception copying extra safebox file '+Laux_newfilename+' ('+E.ClassName+'):'+E.Message); + raise; end; end; end; @@ -1151,7 +1204,7 @@ procedure TPCBank.UpdateValuesFromSafebox; { Will update current Bank state based on Safebox state Used when commiting a Safebox or rolling back } Try - TPCThread.ProtectEnterCriticalSection(Self,FBankLock); + TPCThread.ProtectAcquireWriteLock(Self,FBankLock); try FLastBlockCache.Clear(True); FLastOperationBlock := TPCOperationsComp.GetFirstBlock; @@ -1168,7 +1221,7 @@ procedure TPCBank.UpdateValuesFromSafebox; end; TLog.NewLog(ltinfo,ClassName,Format('Updated Bank with Safebox values. Current block:%d ',[FLastOperationBlock.block])); finally - FBankLock.Release; + FBankLock.ReleaseWriteLock; end; finally for i := 0 to FNotifyList.Count - 1 do begin @@ -1346,7 +1399,7 @@ function TPCBank.LoadBankFromChunks(AChunks : TPCSafeboxChunks; LMemStream.Free; end; Result := True; - TPCThread.ProtectEnterCriticalSection(Self,FBankLock); + TPCThread.ProtectAcquireWriteLock(Self,FBankLock); try If SafeBox.BlocksCount>0 then FLastOperationBlock := SafeBox.GetBlockInfo(SafeBox.BlocksCount-1) else begin @@ -1354,7 +1407,7 @@ function TPCBank.LoadBankFromChunks(AChunks : TPCSafeboxChunks; FLastOperationBlock.initial_safe_box_hash := TPCSafeBox.InitialSafeboxHash; // Genesis hash end; finally - FBankLock.Release; + FBankLock.ReleaseWriteLock; end; for i := 0 to FNotifyList.Count - 1 do begin TPCBankNotify(FNotifyList.Items[i]).NotifyNewBlock; @@ -1384,7 +1437,7 @@ function TPCBank.LoadBankFromStream(Stream: TStream; useSecureLoad : Boolean; ch Result := auxSB.LoadSafeBoxFromStream(Stream,true,checkSafeboxHash,progressNotify,previousCheckedSafebox,LastReadBlock,errors); If Not Result then Exit; end; - TPCThread.ProtectEnterCriticalSection(Self,FBankLock); + TPCThread.ProtectAcquireWriteLock(Self,FBankLock); try If Assigned(auxSB) then begin SafeBox.CopyFrom(auxSB); @@ -1402,7 +1455,7 @@ function TPCBank.LoadBankFromStream(Stream: TStream; useSecureLoad : Boolean; ch FLastOperationBlock.initial_safe_box_hash := TPCSafeBox.InitialSafeboxHash; // Genesis hash end; finally - FBankLock.Release; + FBankLock.ReleaseWriteLock; end; for i := 0 to FNotifyList.Count - 1 do begin TPCBankNotify(FNotifyList.Items[i]).NotifyNewBlock; @@ -1414,7 +1467,7 @@ function TPCBank.LoadBankFromStream(Stream: TStream; useSecureLoad : Boolean; ch function TPCBank.LoadOperations(Operations: TPCOperationsComp; Block: Cardinal): Boolean; begin - TPCThread.ProtectEnterCriticalSection(Self,FBankLock); + TPCThread.ProtectAcquireWriteLock(Self,FBankLock); try if (Block>0) AND (Block=FLastBlockCache.OperationBlock.block) then begin // Same as cache, sending cache @@ -1424,7 +1477,7 @@ function TPCBank.LoadOperations(Operations: TPCOperationsComp; Block: Cardinal): Result := Storage.LoadBlockChainBlock(Operations,Block); end; finally - FBankLock.Release; + FBankLock.ReleaseWriteLock; end; end; @@ -1441,12 +1494,17 @@ procedure TPCBank.NewLog(Operations: TPCOperationsComp; Logtype: TLogType; const function TPCBank.OpenSafeBoxCheckpoint(ABlockCount: Cardinal): TCheckPointStruct; var fn : TFilename; err : AnsiString; + {$IFDEF USE_ABSTRACTMEM} + LBlocksCount: Integer; + {$ENDIF} begin Result := Nil; fn := GetSafeboxCheckpointingFileName(GetStorageFolder(''),ABlockCount); If (fn<>'') and (FileExists(fn)) then begin {$IFDEF USE_ABSTRACTMEM} - Result := TPCAbstractMem.Create(fn,True); + // A failed copy from an older run may remain on disk; never serve it. + if TPCAbstractMem.AnalyzeFile(fn,LBlocksCount) then + Result := TPCAbstractMem.Create(fn,True); {$ELSE} Result := TFileStream.Create(fn,fmOpenRead+fmShareDenyWrite); {$ENDIF} @@ -1457,96 +1515,212 @@ function TPCBank.OpenSafeBoxCheckpoint(ABlockCount: Cardinal): TCheckPointStruct end; end; -function TPCBank.RestoreBank(AMax_block: Int64; AOrphan : String; - ARestoreProgressNotify: TProgressNotify): Boolean; +function TPCBank.RestoreBank(AMax_block: Int64; AOrphan: String; ARestoreProgressNotify: TProgressNotify): Boolean; var - sr: TSearchRec; - FileAttrs: Integer; - folder : AnsiString; - Lfilename,auxfn : AnsiString; - fs : TFileStream; - errors : String; - LBlockscount : Cardinal; - sbHeader, goodSbHeader : TPCSafeBoxHeader; - {$IFDEF USE_ABSTRACTMEM} - LTempBlocksCount : Integer; - LSafeboxFileName : String; - {$ELSE} - {$ENDIF} + sr: TSearchRec; + folder, LFilename, auxfn, errors: String; + fs: TStream; + LBlocksCount: Cardinal; + sbHeader, goodSbHeader: TPCSafeBoxHeader; + LHasUnreadableCandidates, LHeaderValid: Boolean; + {$IFDEF USE_ABSTRACTMEM} + LTempBlocksCount: Integer; + LSafeboxFileName, LAbstractFileName: String; + LReadOnlySafebox: TPCAbstractMem; + LLiveSafeboxAhead: Boolean; + {$ENDIF} + + procedure LogSkippedCheckpoint(const AFileName: String; ALastBlock: Int64); + begin + TLog.NewLog(ltInfo,ClassName,Format('Skipping safebox checkpoint %s: last block %d exceeds restore limit %d',[AFileName,ALastBlock,AMax_block])); + end; + + function LegacyBodyIsReadable(const AFileName: String; AExpectedBlocks: Cardinal): Boolean; + var LProbe: TPCBank; LStream: TStream; LErrors: String; + begin + Result := False; + LProbe := TPCBank.Create(Nil); + try + try + LStream := TFileStream.Create(AFileName,fmOpenRead+fmShareDenyWrite); + try + // Read small serialized fields from bounded read-ahead storage. The + // wrapper adopts the file only after its construction succeeds. + // Writes are denied for this file, so its length can be cached safely. + LStream := TBufferedStream.Create(LStream,64*1024,True,True,True); + // A complete header is not a complete snapshot. Validate the body in + // a separate memory bank before it can displace another good candidate. + // This can be the expensive pass: report progress and allow its EAbort + // to escape before selecting a candidate or changing the live bank. + Result := LProbe.LoadBankFromStream(LStream,True,Nil,Nil,ARestoreProgressNotify,LErrors); + if Result and (LProbe.BlocksCount<>AExpectedBlocks) then begin + Result := False; + LErrors := 'Safebox body block count does not match its header'; + end; + if not Result then TLog.NewLog(ltError,ClassName,'Cannot inspect safebox '+AFileName+': '+LErrors); + finally + LStream.Free; + end; + except + on E: Exception do begin + if (E is EStreamError) or (E is EOSError) or (E is EFileMem) or + (E is EAbstractMem) or (E is EPCAbstractMem) then + TLog.NewLog(ltError,ClassName,'Cannot inspect safebox '+AFileName+': '+E.ClassName+': '+E.Message) + else raise; + end; + end; + finally + LProbe.Free; + end; + end; + begin - FBankLock.Acquire; - Try + Result := False; + LHasUnreadableCandidates := False; + LBlocksCount := 0; + LFilename := ''; + FBankLock.AcquireWriteLock; + try + try {$IFDEF USE_ABSTRACTMEM} - Lfilename := ''; + LAbstractFileName := ''; + LLiveSafeboxAhead := False; LSafeboxFileName := GetStorageFolder(AOrphan)+PathDelim+'safebox'+CT_Safebox_Extension; - if TPCAbstractMem.AnalyzeFile(LSafeboxFileName,LTempBlocksCount) then begin - LBlockscount := LTempBlocksCount; - end else begin - LBlockscount := 0; + // Inspect every candidate read-only before opening any file for mutation. + // One incomplete checkpoint must not prevent use of a healthy live file. + if FileExists(LSafeboxFileName) then begin + if TPCAbstractMem.AnalyzeFile(LSafeboxFileName,LTempBlocksCount) then begin + if (AMax_block<0) or (Int64(LTempBlocksCount)-1<=AMax_block) then begin + LAbstractFileName := LSafeboxFileName; + LBlocksCount := LTempBlocksCount; + end else LLiveSafeboxAhead := True; + end else LHasUnreadableCandidates := True; end; - // - FileAttrs := faArchive; - folder := GetStorageFolder(''); /// Without Orphan folder - if SysUtils.FindFirst(folder+PathDelim+'checkpoint*'+CT_Safebox_Extension, FileAttrs, sr) = 0 then begin - repeat - if (sr.Attr and FileAttrs) = FileAttrs then begin - auxfn := folder+PathDelim+sr.Name; - if TPCAbstractMem.AnalyzeFile(auxfn,LTempBlocksCount) then begin - if (((AMax_block<0) Or (LTempBlocksCount<=AMax_block)) AND (LTempBlocksCount>LBlockscount)) then begin - Lfilename := auxfn; - LBlockscount := LTempBlocksCount; - end; + folder := GetStorageFolder(''); // Checkpoints are shared with orphan restores. + if SysUtils.FindFirst(folder+PathDelim+'checkpoint*'+CT_Safebox_Extension,faAnyFile,sr)=0 then begin + try + repeat + if (sr.Attr and faDirectory)=0 then begin + auxfn := folder+PathDelim+sr.Name; + if TPCAbstractMem.AnalyzeFile(auxfn,LTempBlocksCount) then begin + if (AMax_block>=0) and (Int64(LTempBlocksCount)-1>AMax_block) then + LogSkippedCheckpoint(auxfn,Int64(LTempBlocksCount)-1) + else if (LAbstractFileName='') or (LTempBlocksCount>LBlocksCount) then begin + LAbstractFileName := auxfn; + LBlocksCount := LTempBlocksCount; + end; + end else LHasUnreadableCandidates := True; end; - end; - until FindNext(sr) <> 0; - FindClose(sr); - end; - if (Lfilename='') then begin - SafeBox.SetSafeboxFileName(LSafeboxFileName); - end else begin - SafeBox.SetSafeboxFileName(Lfilename); - SafeBox.UpdateSafeboxFileName(LSafeboxFileName); + until FindNext(sr)<>0; + finally + SysUtils.FindClose(sr); + end; end; - {$ELSE} - LBlockscount := 0; {$ENDIF} - FileAttrs := faArchive; folder := GetStorageFolder(AOrphan); - Lfilename := ''; - if SysUtils.FindFirst(folder+PathDelim+'*.safebox', FileAttrs, sr) = 0 then begin - repeat - if (sr.Attr and FileAttrs) = FileAttrs then begin - auxfn := folder+PathDelim+sr.Name; - If LoadBankFileInfo(auxfn,sbHeader) then begin - if (((AMax_block<0) Or (sbHeader.endBlock<=AMax_block)) AND (sbHeader.blocksCount>LBlockscount)) And - (sbHeader.startBlock=0) And (sbHeader.endBlock=sbHeader.startBlock+sbHeader.blocksCount-1) then begin - Lfilename := auxfn; - LBlockscount := sbHeader.blocksCount; - goodSbHeader := sbHeader; + if SysUtils.FindFirst(folder+PathDelim+'*.safebox',faAnyFile,sr)=0 then begin + try + repeat + if (sr.Attr and faDirectory)=0 then begin + auxfn := folder+PathDelim+sr.Name; + LHeaderValid := False; + try + LHeaderValid := LoadBankFileInfo(auxfn,sbHeader); + except + on E: EStreamError do + TLog.NewLog(ltError,ClassName,'Cannot inspect safebox '+auxfn+': '+E.Message); + on E: EOSError do + TLog.NewLog(ltError,ClassName,'Cannot inspect safebox '+auxfn+': '+E.Message); + end; + if LHeaderValid then + LHeaderValid := (sbHeader.startBlock=0) and + (sbHeader.endBlock=sbHeader.startBlock+sbHeader.blocksCount-1); + if not LHeaderValid then + LHasUnreadableCandidates := True + else if (AMax_block>=0) and (sbHeader.endBlock>AMax_block) then begin + // A complete header outside this restore range is not evidence of + // corruption. Preserve that unused checkpoint and replay local + // history, as before; its body is neither loaded nor trusted. + LogSkippedCheckpoint(auxfn,sbHeader.endBlock); + end else if sbHeader.blocksCount>LBlocksCount then begin + if LegacyBodyIsReadable(auxfn,sbHeader.blocksCount) then begin + LFilename := auxfn; + LBlocksCount := sbHeader.blocksCount; + goodSbHeader := sbHeader; + end else LHasUnreadableCandidates := True; end; end; - end; - until FindNext(sr) <> 0; - FindClose(sr); + until FindNext(sr)<>0; + finally + SysUtils.FindClose(sr); + end; end; - if (Lfilename<>'') then begin - TLog.NewLog(ltinfo,Self.ClassName,'Loading SafeBox protocol:'+IntToStr(goodSbHeader.protocol)+' with '+inttostr(LBlockscount)+' blocks from file '+Lfilename); - fs := TFileStream.Create(Lfilename,fmOpenRead); + if LFilename<>'' then begin + TLog.NewLog(ltInfo,ClassName,'Loading SafeBox protocol:'+IntToStr(goodSbHeader.protocol)+' with '+IntToStr(LBlocksCount)+' blocks from file '+LFilename); + {$IFDEF USE_ABSTRACTMEM} + // Restore into memory first; malformed input must not rewrite live storage. + SafeBox.SetSafeboxFileName(''); + {$ENDIF} + fs := TFileStream.Create(LFilename,fmOpenRead+fmShareDenyWrite); try - fs.Position := 0; - if not LoadBankFromStream(fs,False,Nil,Nil,ARestoreProgressNotify,errors) then begin - TLog.NewLog(lterror,ClassName,'Error reading bank from file: '+Lfilename+ ' Error: '+errors); - end; + // Buffer small serialized fields; denied writes keep the cached length valid. + fs := TBufferedStream.Create(fs,64*1024,True,True,True); + if not LoadBankFromStream(fs,True,Nil,Nil,ARestoreProgressNotify,errors) then + raise Exception.Create('Cannot restore safebox '+LFilename+': '+errors); finally fs.Free; end; + {$IFDEF USE_ABSTRACTMEM} + SafeBox.SaveCheckpointing(LSafeboxFileName); + SafeBox.SetSafeboxFileName(LSafeboxFileName); + {$ENDIF} + end else begin + {$IFDEF USE_ABSTRACTMEM} + if LAbstractFileName<>'' then begin + if not SameFileName(LAbstractFileName,LSafeboxFileName) then begin + // Keep the recovery checkpoint read-only and intact. Publish a complete + // replacement before opening the recovered live safebox for writing. + LReadOnlySafebox := TPCAbstractMem.Create(LAbstractFileName,True); + try + SafeBox.SetSafeboxFileName(''); + LReadOnlySafebox.SaveToFile(LSafeboxFileName); + finally + LReadOnlySafebox.Free; + end; + TLog.NewLog(ltInfo,ClassName,'Recovered safebox from checkpoint '+LAbstractFileName); + end; + SafeBox.SetSafeboxFileName(LSafeboxFileName); + end else if LHasUnreadableCandidates or LLiveSafeboxAhead then begin + // Never initialize over evidence of a failed write. Leave all originals + // available for recovery instead of silently starting an empty chain. + // An ahead checkpoint must not conceal detected damage or cause an + // out-of-range live safebox to be reopened implicitly below. + raise Exception.Create('No readable safebox or checkpoint in '+folder+'. Original files have been preserved.'); + end else begin + SafeBox.SetSafeboxFileName(LSafeboxFileName); + end; + {$ELSE} + if LHasUnreadableCandidates then + raise Exception.Create('No readable safebox in '+folder+'. Original files have been preserved.'); + {$ENDIF} end; - Finally - FBankLock.Release; - End; + Result := True; + except + on E: Exception do begin + // Rejected individual candidates are recoverable; only failure of the + // complete restore latches the canonical node's persistent error. + if TNode.NodeExists and (TNode.Node.Bank=Self) then + TNode.Node.ReportStorageFailure(sfsRestore,folder,E); + raise; + end; + end; + finally + FBankLock.ReleaseWriteLock; + end; end; function TPCBank.SaveBank(forceSave: Boolean): Boolean; +var LStorageFileName: String; begin Result := true; If Storage.FIsMovingBlockchain then Exit; @@ -1558,6 +1732,15 @@ function TPCBank.SaveBank(forceSave: Boolean): Boolean; {$ENDIF} Except On E:Exception do begin + if TNode.NodeExists and (TNode.Node.Bank=Self) then begin + LStorageFileName := ''; + try + LStorageFileName := GetSafeboxCheckpointingFileName(GetStorageFolder(Orphan),BlocksCount); + except + // Do not replace the original I/O exception if its directory is unavailable. + end; + TNode.Node.ReportStorageFailure(sfsCheckpoint,LStorageFileName,E); + end; TLog.NewLog(lterror,Classname,'Error saving Bank: '+E.Message); Raise; end; @@ -1666,9 +1849,14 @@ procedure TPCOperationsComp.CalcProofOfWork(fullcalculation: Boolean; var PoW: T Calc_Digest_Parts; end; FStreamPoW.Position := 0; - FStreamPoW.WriteBuffer(FDigest_Part1[Low(FDigest_Part1)],Length(FDigest_Part1)); - FStreamPoW.WriteBuffer(FDigest_Part2_Payload[Low(FDigest_Part2_Payload)],Length(FDigest_Part2_Payload)); - FStreamPoW.WriteBuffer(FDigest_Part3[Low(FDigest_Part3)],Length(FDigest_Part3)); + // Empty payloads are valid. Do not index an empty byte array, even for a + // zero-byte write; omitting it preserves the exact proof-of-work byte stream. + if Length(FDigest_Part1)>0 then + FStreamPoW.WriteBuffer(FDigest_Part1[Low(FDigest_Part1)],Length(FDigest_Part1)); + if Length(FDigest_Part2_Payload)>0 then + FStreamPoW.WriteBuffer(FDigest_Part2_Payload[Low(FDigest_Part2_Payload)],Length(FDigest_Part2_Payload)); + if Length(FDigest_Part3)>0 then + FStreamPoW.WriteBuffer(FDigest_Part3[Low(FDigest_Part3)],Length(FDigest_Part3)); FStreamPoW.Write(FOperationBlock.timestamp,4); FStreamPoW.Write(FOperationBlock.nonce,4); if CT_ACTIVATE_RANDOMHASH_V4 AND (FOperationBlock.protocol_version >= CT_PROTOCOL_4) then begin @@ -1821,7 +2009,7 @@ function TPCOperationsComp.Count: Integer; constructor TPCOperationsComp.Create(ABank: TPCBank); begin - FOperationsLock := TPCCriticalSection.Create('TPCOperationsComp_OPERATIONSLOCK'); + FOperationsLock := TReadWriteLock.Create('TPCOperationsComp_OPERATIONSLOCK',lrpSupportsRecursion); FDisableds := 0; FStreamPoW := TMemoryStream.Create; FStreamPoW.Position := 0; @@ -1839,7 +2027,7 @@ constructor TPCOperationsComp.Create(ABank: TPCBank); destructor TPCOperationsComp.Destroy; begin - FOperationsLock.Acquire; + FOperationsLock.AcquireWriteLock; try Clear(true); FreeAndNil(FOperationsHashTree); @@ -1849,6 +2037,9 @@ destructor TPCOperationsComp.Destroy; FreeAndNil(FStreamPoW); FreeAndNil(FPreviousUpdatedBlocks); finally + // Clear and its callbacks may reacquire this owner. Balance our outer + // write before disposing the lock; disposal never releases live ownership. + FOperationsLock.ReleaseWriteLock; FreeAndNil(FOperationsLock); end; inherited; @@ -2090,6 +2281,8 @@ procedure TPCOperationsComp.SanitizeOperations; errors, auxs : String; aux,aux2 : TOperationsHashTree; resetNewTarget : Boolean; + LOriginalHash : TRawBytes; + LReplayCompleted : Boolean; begin Lock; Try @@ -2142,12 +2335,19 @@ procedure TPCOperationsComp.SanitizeOperations; // SafeBoxTransaction.CleanTransaction; FPreviousUpdatedBlocks.Clear; + lastn := FOperationsHashTree.OperationsCount; + LOriginalHash := FOperationsHashTree.HashTree; + LOriginalHash := Copy(LOriginalHash,0,Length(LOriginalHash)); + LReplayCompleted := False; aux := TOperationsHashTree.Create; Try if (FOperationBlock.protocol_version>=CT_PROTOCOL_4) then begin aux.Max0feeOperationsBySigner := 1; end else aux.Max0feeOperationsBySigner := -1; - lastn := FOperationsHashTree.OperationsCount; + // Replay eligibility and account/fee metadata remain exact. The scratch + // tree is private, so its two expensive lookup indexes can wait until we + // know whether replay changed the accepted sequence. + aux.FDeferLookupIndexes := True; for i:=0 to lastn-1 do begin op := FOperationsHashTree.GetOperation(i); if (aux.CanAddOperationToHashTree(op)) then begin @@ -2165,10 +2365,30 @@ procedure TPCOperationsComp.SanitizeOperations; end; end; end; + LReplayCompleted := True; Finally - aux2 := FOperationsHashTree; - FOperationsHashTree := aux; - aux2.Free; + try + // A raised validator must not publish only the accepted prefix and + // truncate the saved pending package. Ordinary False results still + // filter invalid operations after the entire replay completes. The + // node keeps an interrupted account overlay unavailable until retry. + if LReplayCompleted then begin + if (n=lastn) And FOperationsHashTree.CanReuseLookupIndexes(aux,LOriginalHash) then begin + // Keep the original operation registrations and valid first-occurrence + // indexes. Only metadata recomputed by the exact replay is adopted. + FOperationsHashTree.AdoptReplayedMetadata(aux); + end else begin + // A rejected operation or an incompatible old index takes the legacy + // replacement path. Never publish the private, unindexed scratch tree. + aux.MaterializeLookupIndexes; + aux2 := FOperationsHashTree; + FOperationsHashTree := aux; + aux := aux2; + end; + end; + finally + aux.Free; + end; FOperationBlock.operations_hash := FOperationsHashTree.HashTree; End; Finally @@ -2491,12 +2711,14 @@ function TPCOperationsComp.ValidateOperationBlock(var errors : String): Boolean; procedure TPCOperationsComp.Lock; begin - FOperationsLock.Acquire; + // Callers borrow mutable operations and signature/cache state under this + // public lease, so it remains recursive write ownership on every path. + FOperationsLock.AcquireWriteLock; end; procedure TPCOperationsComp.Unlock; begin - FOperationsLock.Release; + FOperationsLock.ReleaseWriteLock; end; { TPCBankNotify } @@ -2551,6 +2773,41 @@ procedure TPCBankNotify.SetBank(const Value: TPCBank); end; POperationsHashAccountsData = ^TOperationsHashAccountsData; + TOperationLookupItem = record + Position : Integer; + Sha : TRawBytes; + Reference : TOpReference; + end; + + TOperationLookupComparer = class(TInterfacedObject, IComparer) + private + FCompareSha : Boolean; + public + constructor Create(ACompareSha : Boolean); + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft,ARight : TOperationLookupItem) : Integer; + end; + +constructor TOperationLookupComparer.Create(ACompareSha: Boolean); +begin + inherited Create; + FCompareSha := ACompareSha; +end; + +function TOperationLookupComparer.Compare( + {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft,ARight: TOperationLookupItem): Integer; +begin + if FCompareSha then Result := TBaseType.BinStrComp(ALeft.Sha,ARight.Sha) + else if ALeft.ReferenceARight.Reference then Result := 1 + else Result := 0; + // RTL sorting need not be stable. An explicit original-position tie-break + // preserves the legacy lookup representative for every duplicate key. + if Result=0 then begin + if ALeft.PositionARight.Position then Result := 1; + end; +end; + function TOperationsHashTree.AddOperationToHashTree(op: TPCOperation) : Boolean; Var l : TList; begin @@ -2607,6 +2864,132 @@ procedure TOperationsHashTree.ClearHastThree; end; end; +procedure TOperationsHashTree.MaterializeLookupIndexes; +var LList : TList; + I : Integer; + LOrdered : TList; + LComparer : IComparer; + LItem : TOperationLookupItem; + LPreviousReference : TOpReference; + LPreviousSha : TRawBytes; +begin + LList := FHashTreeOperations.LockList; + try + if Not FDeferLookupIndexes then Exit; + LOrdered := TList.Create; + try + LOrdered.Capacity := LList.Count; + for I := 0 to LList.Count-1 do begin + LItem.Position := I; + LItem.Reference := POperationHashTreeReg(LList[I])^.Op.GetOpReference; + LItem.Sha := POperationHashTreeReg(LList[I])^.Op.Sha256; + LOrdered.Add(LItem); + end; + FListOrderedByOpReference.Clear; + FListOrderedBySha256.Clear; + FListOrderedByOpReference.Capacity := LList.Count; + FListOrderedBySha256.Capacity := LList.Count; + // Build each private index once instead of shifting a sorted array for + // every insertion. Operation order, replay and registry owners are untouched. + LComparer := TOperationLookupComparer.Create(False); + LOrdered.Sort(LComparer); + LPreviousReference := 0; + for I := 0 to LOrdered.Count-1 do begin + LItem := LOrdered[I]; + if (I=0) Or (LItem.Reference<>LPreviousReference) then + FListOrderedByOpReference.Add(LItem.Position); + LPreviousReference := LItem.Reference; + end; + LComparer := TOperationLookupComparer.Create(True); + LOrdered.Sort(LComparer); + LPreviousSha := Nil; + for I := 0 to LOrdered.Count-1 do begin + LItem := LOrdered[I]; + if (I=0) Or (TBaseType.BinStrComp(LItem.Sha,LPreviousSha)<>0) then + FListOrderedBySha256.Add(LItem.Position); + LPreviousSha := LItem.Sha; + end; + FDeferLookupIndexes := False; + finally + // Keep a real interface owner across Sort on both RTLs. + LComparer := Nil; + LOrdered.Free; + end; + finally + FHashTreeOperations.UnlockList; + end; +end; + +function TOperationsHashTree.CanReuseLookupIndexes(AReplayed: TOperationsHashTree; + const AOriginalHash: TRawBytes): Boolean; +var LOriginal,LReplayed : TList; + I,LPosition,LShaRepresentatives,LReferenceRepresentatives : Integer; + LOperation : TPCOperation; +begin + Result := False; + LOriginal := FHashTreeOperations.LockList; + try + LReplayed := AReplayed.FHashTreeOperations.LockList; + try + if (LOriginal.Count<>LReplayed.Count) Or FDeferLookupIndexes then Exit; + if Not TBaseType.Equals(AOriginalHash,AReplayed.HashTree) then Exit; + // Delete can leave a missing representative when the tree contained + // duplicates. Check bounds before binary search and validate each key's + // first occurrence; count/hash equality alone cannot establish this. + for I := 0 to FListOrderedBySha256.Count-1 do + if (FListOrderedBySha256[I]<0) Or (FListOrderedBySha256[I]>=LOriginal.Count) then Exit; + for I := 0 to FListOrderedByOpReference.Count-1 do + if (FListOrderedByOpReference[I]<0) Or (FListOrderedByOpReference[I]>=LOriginal.Count) then Exit; + LShaRepresentatives := 0; + LReferenceRepresentatives := 0; + for I := 0 to LOriginal.Count-1 do begin + LOperation := POperationHashTreeReg(LOriginal[I])^.Op; + if LOperation<>POperationHashTreeReg(LReplayed[I])^.Op then Exit; + if Not FindOrderedBySha(LOriginal,LOperation.Sha256,LPosition) then Exit; + if FListOrderedBySha256[LPosition]>I then Exit; + if FListOrderedBySha256[LPosition]=I then Inc(LShaRepresentatives); + if Not FindOrderedByOpReference(LOriginal,LOperation.GetOpReference,LPosition) then Exit; + if FListOrderedByOpReference[LPosition]>I then Exit; + if FListOrderedByOpReference[LPosition]=I then Inc(LReferenceRepresentatives); + end; + Result := (LShaRepresentatives=FListOrderedBySha256.Count) And + (LReferenceRepresentatives=FListOrderedByOpReference.Count); + finally + AReplayed.FHashTreeOperations.UnlockList; + end; + finally + FHashTreeOperations.UnlockList; + end; +end; + +procedure TOperationsHashTree.AdoptReplayedMetadata(AReplayed: TOperationsHashTree); +var LOldAccountsData : TList; +begin + FHashTreeOperations.LockList; + try + AReplayed.FHashTreeOperations.LockList; + try + // Keep signer-data readers under the tree's existing guard until ownership + // has moved. The scratch destructor can then release the retired metadata + // and extra operation references without replacing the original tree. + LOldAccountsData := FListOrderedByAccountsData; + FListOrderedByAccountsData := AReplayed.FListOrderedByAccountsData; + AReplayed.FListOrderedByAccountsData := LOldAccountsData; + FTotalAmount := AReplayed.FTotalAmount; + FTotalFee := AReplayed.FTotalFee; + FMax0feeOperationsBySigner := AReplayed.FMax0feeOperationsBySigner; + FHasOpRecoverOperations := AReplayed.FHasOpRecoverOperations; + FHashTree := AReplayed.FHashTree; + // Legacy sanitization installs a new tree with no changed-event subscriber. + FOnChanged := AReplayed.FOnChanged; + finally + AReplayed.FHashTreeOperations.UnlockList; + end; + finally + FHashTreeOperations.UnlockList; + end; +end; + procedure TOperationsHashTree.CopyFromHashTree(Sender: TOperationsHashTree); Var i : Integer; lme, lsender : TList; @@ -2656,6 +3039,7 @@ constructor TOperationsHashTree.Create; FMax0feeOperationsBySigner := -1; // Unlimited by default FHashTreeOperations := TPCThreadList.Create('TOperationsHashTree_HashTreeOperations'); FHasOpRecoverOperations := False; + FDeferLookupIndexes := False; end; procedure TOperationsHashTree.Delete(index: Integer); @@ -2752,9 +3136,11 @@ function TOperationsHashTree.GetHashTree: TRawBytes; P : POperationHashTreeReg; tmpRaw : TRawBytes; begin - if Length(FHashTree)<>32 then begin - l := FHashTreeOperations.LockList; - Try + l := FHashTreeOperations.LockList; + Try + // Readers share the node's read lock; keep cache testing, construction and + // publication together under the tree's existing recursive list lock. + if Length(FHashTree)<>32 then begin TCrypto.DoSha256(Nil,FHashTree); for i := 0 to l.Count - 1 do begin P := l[i]; @@ -2763,11 +3149,11 @@ function TOperationsHashTree.GetHashTree: TRawBytes; TBaseType.Concat(FHashTree,P^.Op.Sha256,tmpRaw); FHashTree := TCrypto.DoSha256(tmpRaw); end; - Finally - FHashTreeOperations.UnlockList; End; + Result := FHashTree; + Finally + FHashTreeOperations.UnlockList; end; - Result := FHashTree; end; function TOperationsHashTree.GetOperation(index: Integer): TPCOperation; @@ -2915,14 +3301,15 @@ function TOperationsHashTree.InternalAddOperationToHashTree(list: TList end; npos := list.Add(P); // - if Not FindOrderedByOpReference(list,op.GetOpReference,i) then begin - FListOrderedByOpReference.Insert(i, npos); - end; // TODO: Do not allow duplicate OpReferences? - - // Improvement: Will allow to add duplicate Operations, so add only first to orderedBySha - If Not FindOrderedBySha(list,op.Sha256,i) then begin - // Protection: Will add only once - FListOrderedBySha256.Insert(i, npos); + if Not FDeferLookupIndexes then begin + if Not FindOrderedByOpReference(list,op.GetOpReference,i) then begin + FListOrderedByOpReference.Insert(i, npos); + end; // TODO: Do not allow duplicate OpReferences? + + // Duplicate operations use their first occurrence in both lookup indexes. + If Not FindOrderedBySha(list,op.Sha256,i) then begin + FListOrderedBySha256.Insert(i, npos); + end; end; // Improvement TOperationsHashTree speed 2.1.6 // Mantain an ordered Accounts list with data @@ -3032,8 +3419,8 @@ function TOperationsHashTree.FindOrderedByAccountData(lockedThreadList: TList; const Value: TOpReference; var Index: Integer): Boolean; var L, H, I : Integer; iLockedThreadListPos : PtrInt; - C : Int64; - P : POperationHashTreeReg; + C : Integer; + LReference : TOpReference; begin Result := False; L := 0; @@ -3042,7 +3429,12 @@ function TOperationsHashTree.FindOrderedByOpReference(lockedThreadList: TListValue then C := 1 + else C := 0; if C < 0 then L := I + 1 else begin H := I - 1; @@ -3060,7 +3452,7 @@ function TOperationsHashTree.LoadOperationsHashTreeFromStream(AStream: TStream; ALoadingFromStorage: Boolean; ASetOperationsToProtocolVersion, ALoadFromStorageVersion: Word; APreviousUpdatedBlocks: TAccountPreviousBlockInfo; - AAllow0FeeOperations: Boolean; var AOperationsCount, AProcessedCount : Integer; var AErrors: String): Boolean; + AAllow0FeeOperations: Boolean; var AOperationsCount, AProcessedCount : Integer; var AErrors: String; AProgressNotify : TProgressNotify): Boolean; Var c, i: Cardinal; LOpTypeWord : Word; LOpProtocolVersion : Word; @@ -3068,6 +3460,17 @@ function TOperationsHashTree.LoadOperationsHashTreeFromStream(AStream: TStream; j: Integer; LOpClass: TPCOperationClass; LLastNE : TNotifyEvent; + LLastProgress : TTickCount; + + procedure NotifyProgress(AForce : Boolean); + begin + if Assigned(AProgressNotify) and (AForce or + (TPlatform.GetElapsedMilliseconds(LLastProgress) >= 200)) then begin + LLastProgress := TPlatform.GetTickCount; + // Keep decoding/cancellation independent of presentation and localization. + AProgressNotify(Self,TProgressInfo.Create(psReadingOperations,i,c)); + end; + end; begin Result := False; AErrors := ''; @@ -3081,6 +3484,11 @@ function TOperationsHashTree.LoadOperationsHashTreeFromStream(AStream: TStream; LLastNE := FOnChanged; FOnChanged:=Nil; try + // The callback also provides cooperative cancellation. Always restore the + // notification handler when a caller aborts a large saved-operation load. + i := 0; + LLastProgress := TPlatform.GetTickCount; + NotifyProgress(True); // c = operations count for i := 1 to c do begin if AStream.Size - AStream.Position < 4 then begin @@ -3142,7 +3550,10 @@ function TOperationsHashTree.LoadOperationsHashTreeFromStream(AStream: TStream; Finally FreeAndNil(LOperation); end; + NotifyProgress(False); end; + i := c; + NotifyProgress(True); finally FOnChanged := LLastNE; end; @@ -3151,10 +3562,10 @@ function TOperationsHashTree.LoadOperationsHashTreeFromStream(AStream: TStream; Result := True; end; -function TOperationsHashTree.LoadOperationsHashTreeFromStream(AStream: TStream; ALoadingFromStorage : Boolean; ASetOperationsToProtocolVersion : Word; ALoadFromStorageVersion : Word; APreviousUpdatedBlocks : TAccountPreviousBlockInfo; var AErrors : String): Boolean; +function TOperationsHashTree.LoadOperationsHashTreeFromStream(AStream: TStream; ALoadingFromStorage : Boolean; ASetOperationsToProtocolVersion : Word; ALoadFromStorageVersion : Word; APreviousUpdatedBlocks : TAccountPreviousBlockInfo; var AErrors : String; AProgressNotify : TProgressNotify): Boolean; var Lopc,Lprc : Integer; begin - Result := LoadOperationsHashTreeFromStream(AStream,ALoadingFromStorage,ASetOperationsToProtocolVersion,ALoadFromStorageVersion,APreviousUpdatedBlocks,True,Lopc,Lprc,AErrors); + Result := LoadOperationsHashTreeFromStream(AStream,ALoadingFromStorage,ASetOperationsToProtocolVersion,ALoadFromStorageVersion,APreviousUpdatedBlocks,True,Lopc,Lprc,AErrors,AProgressNotify); end; procedure TOperationsHashTree.MarkVerifiedECDSASignatures(operationsHashTreeToMark: TOperationsHashTree); @@ -3182,7 +3593,10 @@ procedure TOperationsHashTree.MarkVerifiedECDSASignatures(operationsHashTreeToMa if (iPosInMyList>=0) then begin inc(nFound); opInMyList := POperationHashTreeReg(myList[iPosInMyList])^.Op; - if (opInMyList.FHasValidSignature) then begin + // The same wire bytes/hash can have a different signing digest + // across protocol versions. A key-bound flag alone cannot bridge it. + if (opInMyList.ProtocolVersion=opToMark.ProtocolVersion) And + (opInMyList.FHasValidSignature) then begin if (opToMark.FHasValidSignature) then inc(nAlreadyMarked) else begin opToMark.FHasValidSignature:=True; @@ -3378,6 +3792,10 @@ function TStorage.DoFindOperation(const AOpHash: TBytes; var AOperationResume: T function TStorage.DoGetAccountOperations(AAccount, AMaxDepth, AStartOperation, AMaxOperations, ASearchBackwardsStartingAtBlock: Integer; const AOperationsResumeList:TOperationsResumeList): Boolean; + procedure CheckCancelled; + begin + if (TThread.CurrentThread.ThreadID <> MainThreadID) and TThread.CheckTerminated then Abort; + end; // Optimization: // For better performance, will only include at "OperationsResume" values betweeen "startOperation" and "endOperation" @@ -3403,14 +3821,17 @@ function TStorage.DoGetAccountOperations(AAccount, AMaxDepth, AStartOperation, And (block_number >= (AAccount DIV CT_AccountsPerBlock)) And (AMaxOperations<>0) do begin + CheckCancelled; found_in_block := False; last_block_number := block_number; LAccounts.Clear; If not Bank.Storage.LoadBlockChainBlock(opc,block_number) then begin exit; end; + CheckCancelled; opc.OperationsHashTree.GetOperationsAffectingAccount(AAccount,LAccounts); for i := LAccounts.Count - 1 downto 0 do begin + CheckCancelled; op := opc.Operation[(LAccounts.Items[i])]; If TPCOperation.OperationToOperationResume(block_number,Op,False,AAccount,OPR) then begin OPR.NOpInsideBlock := (LAccounts.Items[i]); @@ -3482,6 +3903,7 @@ function TStorage.DoGetAccountOperations(AAccount, AMaxDepth, AStartOperation, lastBalance : Int64; begin Result := False; + CheckCancelled; if AMaxDepth=0 then Exit; if AAccount>=Bank.SafeBox.AccountsCount then Exit; if AMaxOperations=0 then Exit; @@ -3558,7 +3980,7 @@ function TStorage.DoGetBlockOperations(ABlock, AOpBlockStartIndex, end; procedure TStorage.DoLoadPendingBufferOperations(OperationsHashTree: TOperationsHashTree); -Var fs : TFileStream; +Var fs : TStream; errors : String; n : Integer; LCurrentProtocol : Word; @@ -3568,7 +3990,7 @@ procedure TStorage.DoLoadPendingBufferOperations(OperationsHashTree: TOperations if fs.Size>0 then begin if Assigned(Bank) then LCurrentProtocol := Bank.SafeBox.CurrentProtocol else LCurrentProtocol := CT_BUILD_PROTOCOL; - If OperationsHashTree.LoadOperationsHashTreeFromStream(fs,true,LCurrentProtocol,LCurrentProtocol, Nil,errors) then begin + If OperationsHashTree.LoadOperationsHashTreeFromStream(fs,true,LCurrentProtocol,LCurrentProtocol, Nil,errors,FPendingLoadProgressNotify) then begin TLog.NewLog(ltInfo,ClassName,Format('DoLoadPendingBufferOperations loaded operations:%d',[OperationsHashTree.OperationsCount])); end else TLog.NewLog(ltError,ClassName,Format('DoLoadPendingBufferOperations ERROR (Protocol %d): loaded operations:%d errors:%s',[LCurrentProtocol,OperationsHashTree.OperationsCount,errors])); end; @@ -3576,12 +3998,51 @@ procedure TStorage.DoLoadPendingBufferOperations(OperationsHashTree: TOperations procedure TStorage.DoSavePendingBufferOperations( OperationsHashTree: TOperationsHashTree); -Var fs : TFileStream; -begin - fs := GetPendingBufferOperationsStream; - fs.Position:=0; - fs.Size:=0; - OperationsHashTree.SaveOperationsHashTreeToStream(fs,true); +const CT_PendingMoveWriteThrough = $00000008; // MOVEFILE_WRITE_THROUGH; absent in older FPC headers. +Var LTemporary : TFileStream; + LBuffer : TMemoryStream; + LFileName,LTemporaryName : TFileName; + LID : TGUID; + LTemporaryOwned : Boolean; +begin + // Keep the previous complete package until serialization, writing and flush + // have all succeeded. The temporary file shares its directory/volume so the + // final name replacement is atomic; a failed save never truncates live data. + LBuffer := TMemoryStream.Create; + LTemporary := Nil; + LTemporaryOwned := False; + try + if Not OperationsHashTree.SaveOperationsHashTreeToStream(LBuffer,true) then + raise EWriteError.Create('Cannot serialize pending operations'); + LFileName := Bank.GetStorageFolder(Bank.Orphan)+PathDelim+'pendingbuffer.ops'; + if CreateGUID(LID)<>0 then raise EWriteError.Create('Cannot create pending temporary file identity'); + LTemporaryName := LFileName+'.'+GUIDToString(LID)+'.tmp'; + LTemporary := TFileStream.Create(LTemporaryName,fmCreate+fmShareExclusive); + LTemporaryOwned := True; + LBuffer.Position := 0; + LTemporary.CopyFrom(LBuffer,LBuffer.Size); + {$IFDEF FPC} + if Not FileFlush(LTemporary.Handle) then RaiseLastOSError; + {$ELSE} + if Not FlushFileBuffers(LTemporary.Handle) then RaiseLastOSError; + {$ENDIF} + FreeAndNil(LTemporary); + // Only retire our cached handle after the temporary package has been flushed. + // If publication fails, the next getter reopens the previous valid package. + FreeAndNil(FPendingBufferOperationsStream); + {$IFDEF MSWINDOWS} + if Not MoveFileExW(PWideChar(UnicodeString(LTemporaryName)), + PWideChar(UnicodeString(LFileName)),MOVEFILE_REPLACE_EXISTING Or CT_PendingMoveWriteThrough) then + RaiseLastOSError; + {$ELSE} + if Not SysUtils.RenameFile(LTemporaryName,LFileName) then RaiseLastOSError; + {$ENDIF} + LTemporaryOwned := False; + finally + LTemporary.Free; + if LTemporaryOwned then SysUtils.DeleteFile(LTemporaryName); + LBuffer.Free; + end; {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,ClassName,Format('DoSavePendingBufferOperations operations:%d',[OperationsHashTree.OperationsCount]));{$ENDIF} end; @@ -3631,7 +4092,7 @@ function TStorage.GetBlockOperations(ABlock, AOpBlockStartIndex, end else Result := DoGetBlockOperations(ABlock,AOpBlockStartIndex,AMaxOperations,AOperationBlock,AOperationsCount,AVolume,AOperationsResumeList); end; -function TStorage.GetPendingBufferOperationsStream: TFileStream; +function TStorage.GetPendingBufferOperationsStream: TStream; Var fs : TFileStream; fn : TFileName; fm : Word; @@ -3642,8 +4103,14 @@ function TStorage.GetPendingBufferOperationsStream: TFileStream; else fm := fmCreate+fmShareExclusive; Try FPendingBufferOperationsStream := TFileStream.Create(fn,fm); + // Pending packages contain many small serialized fields. Own the retained + // file through read-ahead, but query its mutable length directly. Atomic + // saves still close this owner before publishing the replacement file. + FPendingBufferOperationsStream := TBufferedStream.Create(FPendingBufferOperationsStream,64*1024,True,False,True); Except On E:Exception do begin + // A failed decorator construction leaves ownership with the raw file. + FreeAndNil(FPendingBufferOperationsStream); TLog.NewLog(ltError,ClassName,'Error opening PendingBufferOperationsStream '+fn+' ('+E.ClassName+'):'+ E.Message); Raise; end; @@ -3657,9 +4124,19 @@ procedure TStorage.SavePendingBufferOperations(OperationsHashTree : TOperationsH DoSavePendingBufferOperations(OperationsHashTree); end; -procedure TStorage.LoadPendingBufferOperations(OperationsHashTree : TOperationsHashTree); +procedure TStorage.LoadPendingBufferOperations(OperationsHashTree : TOperationsHashTree; AProgressNotify : TProgressNotify); +var LPreviousProgress : TProgressNotify; begin - DoLoadPendingBufferOperations(OperationsHashTree); + // Keep the existing virtual dispatch contract for storage implementations. + // The pending file already has one stream/reader; scope its callback to this + // invocation so an aborted restore cannot retain a startup-task reference. + LPreviousProgress := FPendingLoadProgressNotify; + FPendingLoadProgressNotify := AProgressNotify; + try + DoLoadPendingBufferOperations(OperationsHashTree); + finally + FPendingLoadProgressNotify := LPreviousProgress; + end; end; function TStorage.LoadBlockChainBlock(Operations: TPCOperationsComp; Block: Cardinal): Boolean; @@ -3696,7 +4173,7 @@ function TStorage.MoveBlockChainBlocks(StartBlock: Cardinal; const DestOrphan: T End; end; until FindNext(sr) <> 0; - FindClose(sr); + SysUtils.FindClose(sr); end; End; Var @@ -3752,8 +4229,16 @@ function TStorage.SaveBlockChainBlock(Operations: TPCOperationsComp): Boolean; Try if ReadOnly then raise Exception.Create('Cannot save because is ReadOnly'); Result := DoSaveBlockChain(Operations); + // A false storage result is a persistence failure too. Never let the bank + // clear accepted operations or acknowledge a block that was not saved. + if not Result then + raise EWriteError.Create('Storage did not save block '+IntToStr(Operations.OperationBlock.block)); Except On E:Exception do begin + // A local persistence failure must not be reported only as peer isolation. + // Preserve a checkpoint-specific first cause already latched by SaveBank. + if TNode.NodeExists and (TNode.Node.Bank=Bank) then + TNode.Node.ReportStorageFailure(sfsBlockStream,TNode.GetPascalCoinDataFolder+PathDelim+'Data'+PathDelim+'BlockChainStream.blocks',E); TLog.NewLog(lterror,Classname,'Error saving block chain: '+E.Message); Raise; end; diff --git a/src/core/UConst.pas b/src/core/UConst.pas index b69e25ca9..19549e057 100644 --- a/src/core/UConst.pas +++ b/src/core/UConst.pas @@ -199,7 +199,7 @@ interface CT_OpSubtype_Data_Signer = 103; CT_OpSubtype_Data_Receiver = 104; - CT_ClientAppVersion : String = {$IFDEF PRODUCTION}'5.8'{$ELSE}{$IFDEF TESTNET}'TESTNET 5.8'{$ELSE}{$ENDIF}{$ENDIF}; + CT_ClientAppVersion : String = {$IFDEF PRODUCTION}'5.9.5'{$ELSE}{$IFDEF TESTNET}'TESTNET 5.9.5'{$ELSE}{$ENDIF}{$ENDIF}; CT_Discover_IPs = {$IFDEF PRODUCTION}'bpascal1.dynamic-dns.net;bpascal2.dynamic-dns.net;pascalcoin1.dynamic-dns.net;pascalcoin2.dynamic-dns.net;pascalcoin1.dns1.us;pascalcoin2.dns1.us;pascalcoin1.dns2.us;pascalcoin2.dns2.us' {$ELSE}'pascaltestnet1.dynamic-dns.net;pascaltestnet2.dynamic-dns.net;pascaltestnet1.dns1.us;pascaltestnet2.dns1.us'{$ENDIF}; diff --git a/src/core/UCrypto.pas b/src/core/UCrypto.pas index 883f23716..1ac1134ae 100644 --- a/src/core/UCrypto.pas +++ b/src/core/UCrypto.pas @@ -1009,6 +1009,7 @@ function TBigNum.GetRawValue: TRawBytes; {$IFDEF Use_OpenSSL} i := BN_num_bytes(FBN); SetLength(Result,i); + if i=0 then Exit; p := @Result[Low(Result)]; i := BN_bn2bin(FBN,p); {$ELSE} diff --git a/src/core/UEPasa.pas b/src/core/UEPasa.pas index c44d6658c..67cebe7a5 100644 --- a/src/core/UEPasa.pas +++ b/src/core/UEPasa.pas @@ -30,7 +30,7 @@ interface UCommon, UCrypto, UEncoding, - SyncObjs; + UThread; type @@ -142,7 +142,7 @@ TEPasa = record TEPasaParser = class strict private class var FEPasaRegex: TCustomRegex; - class var FEPasaLocker : TCriticalSection; + class var FEPasaLocker : TReadWriteLock; class constructor CreateRegexEPasaParser(); class destructor DestroyRegexEPasaParser(); @@ -506,7 +506,7 @@ class function TEPasa.GetEmptyValue : TEPasa; class constructor TEPasaParser.CreateRegexEPasaParser; begin FEPasaRegex := TCustomRegex.Create(EPasaPattern); - FEPasaLocker := TCriticalSection.Create; + FEPasaLocker := TReadWriteLock.Create('TEPasaParser.Regex',lrpSupportsRecursion); end; class destructor TEPasaParser.DestroyRegexEPasaParser; @@ -552,7 +552,8 @@ function TEPasaParser.TryParse(const AEPasaText: String; AOmitExtendedChecksumVe Exit(False); end; - FEPasaLocker.Acquire; // Protect against multithread + // Matching updates the shared regex capture state, so parsing owns a write. + FEPasaLocker.AcquireWriteLock; Try FEPasaRegex.Match(AEPasaText); @@ -576,7 +577,7 @@ function TEPasaParser.TryParse(const AEPasaText: String; AOmitExtendedChecksumVe end; Finally - FEPasaLocker.Release; + FEPasaLocker.ReleaseWriteLock; End; if (LAccountName <> #0) then begin diff --git a/src/core/UEncoding.pas b/src/core/UEncoding.pas index e4bf715a5..8c980c6c1 100644 --- a/src/core/UEncoding.pas +++ b/src/core/UEncoding.pas @@ -195,7 +195,7 @@ function TCustomRegex.GetMatchFromName(const AGroupName: String): String; i: Int32; begin Result := #0; - // carefull: E:\SourceCode\PascalCoin-master\src\libraries\regex must be added to project file + // carefull: src\libraries\regex must be added to project file // otherwise wrong unit will be used from: // Drive:\Tools\Lazarus\fpc\3.2.0\source\packages\regexpr\src i := FRegex.MatchIndexFromName(AGroupName); diff --git a/src/core/UFileStorage.pas b/src/core/UFileStorage.pas index 672eb3c31..6df94e808 100644 --- a/src/core/UFileStorage.pas +++ b/src/core/UFileStorage.pas @@ -41,7 +41,7 @@ interface TFileStorage = Class(TStorage) private - FStorageLock : TPCCriticalSection; + FStorageLock : TReadWriteLock; FBlockChainStream : TFileStream; FStreamFirstBlockNumber : Int64; FStreamLastBlockNumber : Int64; @@ -75,7 +75,7 @@ interface implementation -Uses ULog, SysUtils, UBaseTypes, +Uses ULog, SysUtils, UBaseTypes, UStreams, {$IFDEF USE_ABSTRACTMEM} UPCAbstractMem, {$ENDIF} @@ -178,7 +178,7 @@ constructor TFileStorage.Create(AOwner: TComponent); SetLength(FBlockHeadersFirstBytePosition,0); FStreamFirstBlockNumber := 0; FStreamLastBlockNumber := -1; - FStorageLock := TPCCriticalSection.Create('TFileStorage_StorageLock'); + FStorageLock := TReadWriteLock.Create('TFileStorage_StorageLock',lrpSupportsRecursion); end; destructor TFileStorage.Destroy; @@ -347,6 +347,7 @@ function TFileStorage.DoSaveBlockChain(Operations: TPCOperationsComp): Boolean; function TFileStorage.GetBlockHeaderFirstBytePosition(Stream : TStream; Block: Cardinal; CanInitialize : Boolean; var iBlockHeaders : Integer; var BlockHeaderFirstBlock: Cardinal): Boolean; var iPos,start, nCurrBlock : Cardinal; bh : TBlockHeader; + LHeaderInput : TBufferedStream; null_buff : Array[1..(CT_GroupBlockSize * CT_SizeOfBlockHeader)] of Byte; begin Result := false; @@ -356,76 +357,89 @@ function TFileStorage.GetBlockHeaderFirstBytePosition(Stream : TStream; Block: C end; iPos := (Block-FStreamFirstBlockNumber) DIV CT_GroupBlockSize; if iPos>High(FBlockHeadersFirstBytePosition) then Begin - if Length(FBlockHeadersFirstBytePosition)>0 then begin - start := High(FBlockHeadersFirstBytePosition); - end else begin - If CanInitialize then begin - // Initialize and start at 0 - SetLength(FBlockHeadersFirstBytePosition,1); - FBlockHeadersFirstBytePosition[0] := 0; - start := 0; - end else exit; - end; - while (start0 then begin + start := High(FBlockHeadersFirstBytePosition); + end else begin + If CanInitialize then begin + // Initialize and start at 0 + SetLength(FBlockHeadersFirstBytePosition,1); + FBlockHeadersFirstBytePosition[0] := 0; + start := 0; + end else exit; end; - Stream.Position := FBlockHeadersFirstBytePosition[start] + GetBlockHeaderFixedSize - CT_SizeOfBlockHeader; - // Read last saved Header - nCurrBlock := FStreamFirstBlockNumber + ((start+1) * CT_GroupBlockSize) - 1; - Repeat - Stream.Read(bh.BlockNumber,SizeOf(bh.BlockNumber)); - Stream.Read(bh.StreamBlockRelStartPos,SizeOf(bh.StreamBlockRelStartPos)); - Stream.Read(bh.BlockSize,sizeof(bh.BlockSize)); - If (bh.BlockNumber<>nCurrBlock) then begin - if (bh.BlockNumber<>0) Or (bh.StreamBlockRelStartPos<>0) Or (bh.BlockSize<>0) then begin - TLog.NewLog(ltError,ClassName,Format('Fatal error. Found a Tblockheader with no 0 values searching for block:%d at nCurrBlock:%d - Number:%d RelStartPos:%d Size:%d',[block,nCurrBlock,bh.BlockNumber,bh.StreamBlockRelStartPos,bh.BlockSize])); + while (startFStreamFirstBlockNumber)) - Or - ((start>0) And (nCurrBlock>(FStreamFirstBlockNumber + ((start) * CT_GroupBlockSize)))) then begin - dec(nCurrBlock); - // Positioning for new read: - Stream.Seek(Int64(CT_SizeOfBlockHeader)*(-2),soFromCurrent); - end else begin - break; // End of blockheader! + end; + Stream.Position := FBlockHeadersFirstBytePosition[start] + GetBlockHeaderFixedSize - CT_SizeOfBlockHeader; + // Read last saved Header + nCurrBlock := FStreamFirstBlockNumber + ((start+1) * CT_GroupBlockSize) - 1; + Repeat + Stream.Read(bh.BlockNumber,SizeOf(bh.BlockNumber)); + Stream.Read(bh.StreamBlockRelStartPos,SizeOf(bh.StreamBlockRelStartPos)); + Stream.Read(bh.BlockSize,sizeof(bh.BlockSize)); + If (bh.BlockNumber<>nCurrBlock) then begin + if (bh.BlockNumber<>0) Or (bh.StreamBlockRelStartPos<>0) Or (bh.BlockSize<>0) then begin + TLog.NewLog(ltError,ClassName,Format('Fatal error. Found a Tblockheader with no 0 values searching for block:%d at nCurrBlock:%d - Number:%d RelStartPos:%d Size:%d',[block,nCurrBlock,bh.BlockNumber,bh.StreamBlockRelStartPos,bh.BlockSize])); + exit; + end; + if ((start=0) And (nCurrBlock>FStreamFirstBlockNumber)) + Or + ((start>0) And (nCurrBlock>(FStreamFirstBlockNumber + ((start) * CT_GroupBlockSize)))) then begin + dec(nCurrBlock); + // Positioning for new read: + Stream.Seek(Int64(CT_SizeOfBlockHeader)*(-2),soFromCurrent); + end else begin + break; // End of blockheader! + end; end; + until (bh.BlockNumber>0); + // Positioning! + Stream.Position := FBlockHeadersFirstBytePosition[start] + GetBlockHeaderFixedSize; + // + SetLength(FBlockHeadersFirstBytePosition,length(FBlockHeadersFirstBytePosition)+1); + if bh.BlockNumber>0 then begin + FBlockHeadersFirstBytePosition[High(FBlockHeadersFirstBytePosition)] := Stream.Position + bh.StreamBlockRelStartPos + bh.BlockSize; + end else begin + // Not found a block, starting at last pos + FBlockHeadersFirstBytePosition[High(FBlockHeadersFirstBytePosition)] := Stream.Position; end; - until (bh.BlockNumber>0); - // Positioning! - Stream.Position := FBlockHeadersFirstBytePosition[start] + GetBlockHeaderFixedSize; - // - SetLength(FBlockHeadersFirstBytePosition,length(FBlockHeadersFirstBytePosition)+1); - if bh.BlockNumber>0 then begin - FBlockHeadersFirstBytePosition[High(FBlockHeadersFirstBytePosition)] := Stream.Position + bh.StreamBlockRelStartPos + bh.BlockSize; - end else begin - // Not found a block, starting at last pos - FBlockHeadersFirstBytePosition[High(FBlockHeadersFirstBytePosition)] := Stream.Position; + inc(start); + + // Check if blockheader size is ok: + if (CanInitialize) And (Stream.Size<(FBlockHeadersFirstBytePosition[start] + GetBlockHeaderFixedSize)) then begin + Stream.Position := FBlockHeadersFirstBytePosition[start]; + TLog.NewLog(ltInfo,ClassName,Format('Increasing size for blockheader %d at pos:%d (current stream pos %d size %d) to position:%d', + [start,FBlockHeadersFirstBytePosition[start],Stream.Position,Stream.Size, + FBlockHeadersFirstBytePosition[start]+GetBlockHeaderFixedSize])); + GrowStreamUntilPos(Stream,FBlockHeadersFirstBytePosition[start]+GetBlockHeaderFixedSize,true); + end; + end; - inc(start); - - // Check if blockheader size is ok: - if (CanInitialize) And (Stream.Size<(FBlockHeadersFirstBytePosition[start] + GetBlockHeaderFixedSize)) then begin - Stream.Position := FBlockHeadersFirstBytePosition[start]; - TLog.NewLog(ltInfo,ClassName,Format('Increasing size for blockheader %d at pos:%d (current stream pos %d size %d) to position:%d', - [start,FBlockHeadersFirstBytePosition[start],Stream.Position,Stream.Size, - FBlockHeadersFirstBytePosition[start]+GetBlockHeaderFixedSize])); - GrowStreamUntilPos(Stream,FBlockHeadersFirstBytePosition[start]+GetBlockHeaderFixedSize,true); + finally + try + // Return any unread header bytes before the caller uses the raw file. + LHeaderInput.FlushUnreadData; + finally + LHeaderInput.Free; end; - end; End; iBlockHeaders := iPos; @@ -490,6 +504,7 @@ function TFileStorage.LockBlockChainStream: TFileStream; while (iPos + GetBlockHeaderFixedSize < Stream.Size) do begin Stream.Position := iPos; mem.Size := 0; + // This whole header group already has a bulk buffer; keep file I/O direct. mem.CopyFrom(Stream,GetBlockHeaderFixedSize); // Analize it: mem.Position := 0; @@ -573,7 +588,9 @@ function TFileStorage.LockBlockChainStream: TFileStream; bh : TBlockHeader; errors : String; begin - TPCThread.ProtectEnterCriticalSection(Self,FStorageLock); + // Even a load mutates this shared stream's Position and can initialize its + // header cache. Keep the complete borrowed-stream lease exclusive. + TPCThread.ProtectAcquireWriteLock(Self,FStorageLock); Try if Not Assigned(FBlockChainStream) then begin if FStorageFilename<>'' then begin @@ -600,7 +617,7 @@ function TFileStorage.LockBlockChainStream: TFileStream; end; end; Except - FStorageLock.Release; + FStorageLock.ReleaseWriteLock; Raise; End; Result := FBlockChainStream; @@ -653,6 +670,7 @@ function TFileStorage.StreamBlockRead(Stream : TStream; iBlockHeaders : Integer; // Reading Block _ops := TMemoryStream.Create; try + // Header readers have flushed their borrowed view; copy payload bytes directly. _ops.CopyFrom(Stream,_BlockSizeC); _ops.Position := 0; If Not Operations.LoadBlockFromStorage(_ops,errors) then begin @@ -734,14 +752,12 @@ function TFileStorage.StreamBlockSave(Stream : TStream; iBlockHeaders : Integer; end; end; -function TFileStorage.StreamReadBlockHeader(Stream: TStream; - iBlockHeaders : Integer; BlockHeaderFirstBlock, Block: Cardinal; - CanSearchBackward : Boolean; - var BlockHeader: TBlockHeader): Boolean; +function TFileStorage.StreamReadBlockHeader(Stream: TStream; iBlockHeaders : Integer; BlockHeaderFirstBlock, Block: Cardinal; CanSearchBackward : Boolean; var BlockHeader: TBlockHeader): Boolean; Var iBlock : Cardinal; _iBlockHeaders : Integer; _StreamBlockHeaderStartPos: Int64; _BlockHeaderFirstBlock : Cardinal; + LHeaderInput : TBufferedStream; begin Result := false; BlockHeader := CT_TBlockHeader_NUL; @@ -756,41 +772,54 @@ function TFileStorage.StreamReadBlockHeader(Stream: TStream; iBlock := Block; _iBlockHeaders:=iBlockHeaders; _BlockHeaderFirstBlock:=BlockHeaderFirstBlock; - // Reading block header - Repeat - _StreamBlockHeaderStartPos:=FBlockHeadersFirstBytePosition[_iBlockHeaders]; - Stream.Position := _StreamBlockHeaderStartPos + (CT_SizeOfBlockHeader*(iBlock-_BlockHeaderFirstBlock)); - If Stream.Read(BlockHeader.BlockNumber,sizeof(BlockHeader.BlockNumber))_BlockHeaderFirstBlock) then dec(iBlock) - else begin - // Search on previous header... - If (iBlockHeaders>0) then begin - dec(_iBlockHeaders); - dec(_BlockHeaderFirstBlock,CT_GroupBlockSize); - end else begin - break; + // Coalesce the 4/8/4-byte fields without allocating a block-sized buffer. + // Borrow the existing locked file; its mutable length must never be cached. + LHeaderInput := TBufferedStream.Create(Stream,CT_SizeOfBlockHeader,False,False,True); + try + Stream := LHeaderInput; + // Reading block header + Repeat + _StreamBlockHeaderStartPos:=FBlockHeadersFirstBytePosition[_iBlockHeaders]; + Stream.Position := _StreamBlockHeaderStartPos + (CT_SizeOfBlockHeader*(iBlock-_BlockHeaderFirstBlock)); + If Stream.Read(BlockHeader.BlockNumber,sizeof(BlockHeader.BlockNumber))_BlockHeaderFirstBlock) then dec(iBlock) + else begin + // Search on previous header... + If (iBlockHeaders>0) then begin + dec(_iBlockHeaders); + dec(_BlockHeaderFirstBlock,CT_GroupBlockSize); + end else begin + break; + end; end; end; + Until Result Or (Not CanSearchBackward); + // Positioning + Stream.Position := FBlockHeadersFirstBytePosition[iBlockHeaders] + (CT_SizeOfBlockHeader*(Block+1-BlockHeaderFirstBlock)); + If Result then begin // For Backward searching... + BlockHeader.BlockNumber:=Block; + If (_iBlockHeaders<>iBlockHeaders) then begin + BlockHeader.BlockSize:=0; + BlockHeader.StreamBlockRelStartPos:=0; + end; end; - Until Result Or (Not CanSearchBackward); - // Positioning - Stream.Position := FBlockHeadersFirstBytePosition[iBlockHeaders] + (CT_SizeOfBlockHeader*(Block+1-BlockHeaderFirstBlock)); - If Result then begin // For Backward searching... - BlockHeader.BlockNumber:=Block; - If (_iBlockHeaders<>iBlockHeaders) then begin - BlockHeader.BlockSize:=0; - BlockHeader.StreamBlockRelStartPos:=0; + finally + try + // No read-ahead cursor may escape to the caller's direct payload copy. + LHeaderInput.FlushUnreadData; + finally + LHeaderInput.Free; end; end; end; procedure TFileStorage.UnlockBlockChainStream; begin - FStorageLock.Release; + FStorageLock.ReleaseWriteLock; end; end. diff --git a/src/core/UGPUMining.pas b/src/core/UGPUMining.pas index 5f05b5623..6fa750696 100644 --- a/src/core/UGPUMining.pas +++ b/src/core/UGPUMining.pas @@ -24,7 +24,7 @@ interface uses Classes, SysUtils, CL_Platform, CL, CL_GL, DelphiCL, dglOpenGL, USha256, UPoolMinerThreads, UPoolMining, - UThread, UTime, SyncObjs, UCrypto, ULog; + UThread, UTime, UCrypto, ULog; Type @@ -35,7 +35,8 @@ interface FNeedNewDevice : Boolean; FDevice: Integer; FPlatform: Integer; - FLock : TCriticalSection; + // Kernel/buffer updates share one recursive writer, including nested UpdateBuffers. + FLock : TReadWriteLock; FDCLDevice : TDCLDevice; FDCLProgram: TDCLProgram; FDCLCommandQueue: TDCLCommandQueue; @@ -149,7 +150,7 @@ procedure TGPUDeviceThread.BCExecute; sleep(1); end else begin baseRealTC := GetTickCount; - FLock.Acquire; + FLock.AcquireWriteLock; try cMVFW := MinerValuesForWork; // AuxStats := CT_TMinerStats_NULL; @@ -180,18 +181,18 @@ procedure TGPUDeviceThread.BCExecute; FKernelOutputBuffer := FDCLDevice.CreateBuffer(4,@FKernelArg2,[mfReadWrite, mfCopyHostPtr {mfCopyHostPtr mfUseHostPtr}]); // FOUND A NONCE !!! inc(AuxStats.WinsCount); - FLock.Release; + FLock.ReleaseWriteLock; try FoundNOnce(cMVFW,Timestamp,nOnce); finally - FLock.Acquire; + FLock.AcquireWriteLock; end; end; if (nLap0) then begin inc(AuxStats.WorkingMillisecondsTotal,GetTickCount - baseRealTC); @@ -214,7 +215,7 @@ constructor TGPUDeviceThread.Create(PoolMinerThread: TPoolMinerThread; InitialMi FPlatform:=-1; FNeedNewDevice:=false; FProgramFileName:=''; - FLock := TCriticalSection.Create; + FLock := TReadWriteLock.Create('TGPUDeviceThread.Work',lrpSupportsRecursion); FDCLDevice := Nil; FDCLProgram := Nil; FDCLCommandQueue := Nil; @@ -282,7 +283,7 @@ procedure TGPUDeviceThread.SetProgramFileName(AValue: String); procedure TGPUDeviceThread.UpdateState; begin - FLock.Acquire; + FLock.AcquireWriteLock; try If FNeedNewDevice then begin FDCLDevice := Nil; @@ -309,7 +310,7 @@ procedure TGPUDeviceThread.UpdateState; end; UpdateBuffers; finally - FLock.Release; + FLock.ReleaseWriteLock; end; inherited; end; @@ -322,7 +323,7 @@ procedure TGPUDeviceThread.UpdateBuffers; b : Byte; c1,c2 : Cardinal; begin - FLock.Acquire; + FLock.AcquireWriteLock; try FReadyToGPU := (Length(MinerValuesForWork.part1)>0) And (Assigned(FDCLKernel)); if (FReadyToGPU) And (TPoolMinerThread.UseRandomHash(MinerValuesForWork.version)) then begin @@ -362,7 +363,7 @@ procedure TGPUDeviceThread.UpdateBuffers; end; IsMining := true; finally - FLock.Release; + FLock.ReleaseWriteLock; end; end; diff --git a/src/core/ULog.pas b/src/core/ULog.pas index 90d3bd331..e29ecef29 100644 --- a/src/core/ULog.pas +++ b/src/core/ULog.pas @@ -23,7 +23,7 @@ interface uses - Classes, UThread, SyncObjs, UConst, + Classes, UThread, UConst, {$IFNDEF FPC}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}; @@ -57,13 +57,15 @@ interface private FLogDataList : TThreadList; FOnNewLog: TNewLogEvent; + FOnNewLogBatchBegin, FOnNewLogBatchEnd : TNotifyEvent; FOnInThreadNewLog : TNewLogEvent; FFileStream : TFileStream; FFileName: String; FSaveTypes: TLogTypes; FThreadSafeLogEvent : TThreadSafeLogEvent; FProcessGlobalLogs: Boolean; - FLock : TCriticalSection; + // Log writes and in-thread callbacks are serialized; callbacks may log recursively. + FLock : TReadWriteLock; procedure SetFileName(const Value: String); protected Procedure DoLog(logtype : TLogType; const sender, logtext : String); virtual; @@ -73,6 +75,9 @@ interface Class Procedure NewLog(logtype : TLogType; Const sender, logtext : String); Property OnInThreadNewLog : TNewLogEvent read FOnInThreadNewLog write FOnInThreadNewLog; Property OnNewLog : TNewLogEvent read FOnNewLog write FOnNewLog; + // Optional main-thread brackets let a receiver repaint once per delivery. + Property OnNewLogBatchBegin : TNotifyEvent read FOnNewLogBatchBegin write FOnNewLogBatchBegin; + Property OnNewLogBatchEnd : TNotifyEvent read FOnNewLogBatchEnd write FOnNewLogBatchEnd; Property FileName : String read FFileName write SetFileName; Property SaveTypes : TLogTypes read FSaveTypes write FSaveTypes; Property ProcessGlobalLogs : Boolean read FProcessGlobalLogs write FProcessGlobalLogs; @@ -96,7 +101,7 @@ implementation constructor TLog.Create(AOwner: TComponent); begin - FLock := TCriticalSection.Create; + FLock := TReadWriteLock.Create('TLog.Log',lrpSupportsRecursion); FProcessGlobalLogs := true; FLogDataList := TThreadList.Create; FFileStream := Nil; @@ -104,6 +109,8 @@ constructor TLog.Create(AOwner: TComponent); FSaveTypes := CT_TLogTypes_DEFAULT; FOnInThreadNewLog:=Nil; FOnNewLog:=Nil; + FOnNewLogBatchBegin:=Nil; + FOnNewLogBatchEnd:=Nil; if (Not assigned(_logs)) then _logs := TList.Create; _logs.Add(self); FThreadSafeLogEvent := TThreadSafeLogEvent.Create(true); @@ -119,6 +126,8 @@ destructor TLog.Destroy; P : PLogData; begin FOnNewLog:=Nil; + FOnNewLogBatchBegin:=Nil; + FOnNewLogBatchEnd:=Nil; FOnInThreadNewLog:=Nil; FThreadSafeLogEvent.Terminate; FThreadSafeLogEvent.WaitFor; @@ -162,7 +171,7 @@ procedure TLog.NotifyNewLog(logtype: TLogType; Const sender, logtext: String); Var s,tid : RawByteString; P : PLogData; begin - FLock.Acquire; + FLock.AcquireWriteLock; try if assigned(FFileStream) And (logType in FSaveTypes) then begin if TThread.CurrentThread.ThreadID=MainThreadID then tid := ' MAIN:' else tid:=' TID:'; @@ -183,7 +192,7 @@ procedure TLog.NotifyNewLog(logtype: TLogType; Const sender, logtext: String); FLogDataList.Add(P); end; finally - FLock.Release; + FLock.ReleaseWriteLock; end; DoLog(logtype,sender,logtext); end; @@ -213,6 +222,8 @@ procedure TThreadSafeLogEvent.BCExecute; while (not Terminated) do begin sleep(100); If (Not Terminated) And (Assigned(FLog.OnNewLog)) then begin + // Only this delivery helper waits for the GUI. Submitting threads keep + // appending to the queue while the detached batch is rendered. Synchronize(SynchronizedProcess); end; end; @@ -225,28 +236,41 @@ constructor TThreadSafeLogEvent.Create(Suspended: Boolean); procedure TThreadSafeLogEvent.SynchronizedProcess; Var l : TList; + LBatch : TArray; i : Integer; P : PLogData; begin If Not Assigned(FLog) then Exit; If Not Assigned(FLog.FOnNewLog) then Exit; - // This event is thread safe and will do OnNewLog on main thread + // Transfer ownership of the queued pointers, not copies of their messages. + // Never retain the producer queue lock while invoking GUI callbacks. l := FLog.FLogDataList.LockList; try + if l.Count=0 then Exit; + LBatch := l.ToArray; + l.Clear; + finally + FLog.FLogDataList.UnlockList; + end; + try + if Assigned(FLog.FOnNewLogBatchBegin) then FLog.FOnNewLogBatchBegin(FLog); try - for i := 0 to l.Count - 1 do begin - P := PLogData(l[i]); + for i := Low(LBatch) to High(LBatch) do begin + P := PLogData(LBatch[i]); if Assigned(FLog.FOnNewLog) then begin FLog.OnNewLog( P^.Logtype,P^.Time,P^.ThreadID,P^.Sender,P^.Logtext ); end; - Dispose(P); end; finally - // Protection for possible raise - l.Clear; + if Assigned(FLog.FOnNewLogBatchEnd) then FLog.FOnNewLogBatchEnd(FLog); end; finally - FLog.FLogDataList.UnlockList; + // The detached batch is ours even if a receiver raises or unsubscribes. + // New/reentrant log entries stay in the shared queue for the next delivery. + for i := Low(LBatch) to High(LBatch) do begin + P := PLogData(LBatch[i]); + Dispose(P); + end; end; end; diff --git a/src/core/UNetProtocol.pas b/src/core/UNetProtocol.pas index 4822edef8..f2e77fcdf 100644 --- a/src/core/UNetProtocol.pas +++ b/src/core/UNetProtocol.pas @@ -117,6 +117,9 @@ interface end; TNetConnection = Class; + TNetClient = Class; + TThreadGetNewBlockChainFromClient = class; + TThreadDiscoverConnection = class; TNodeServerAddress = Record ip : String; @@ -146,7 +149,7 @@ interface private FAllowDeleteOnClean: Boolean; FNetData : TNetData; - FCritical : TPCCriticalSection; + FCritical : TReadWriteLock; FListByIp : TList; FListByNetConnection : TList; Procedure SecuredDeleteFromListByIp(index : Integer); @@ -222,9 +225,9 @@ interface TNetClientsDestroyThread = Class(TPCThread) private FNetData : TNetData; - FTerminatedAllConnections : Boolean; protected procedure BCExecute; override; + procedure RegistryScanCompleted(AEmpty: Boolean); virtual; public Constructor Create(NetData : TNetData); Procedure WaitForTerminatedAllConnections; @@ -278,8 +281,9 @@ interface FOnProcessReservedAreaMessage: TProcessReservedAreaMessage; FRegisteredRequests : TPCThreadList; FIsDiscoveringServers : Boolean; - FLockGettingNewBlockChainFromClient : TPCCriticalSection; - FNewBlockChainFromClientStatus : String; + FLockGettingNewBlockChainFromClient : TReadWriteLock; + FNewBlockChainProgressLock : TReadWriteLock; + FNewBlockChainFromClientStatus : TProgressInfo; FOnConnectivityChanged : TNotifyManyEvent; FOnNetConnectionsUpdated: TNotifyEvent; FOnNodeServersUpdated: TNotifyEvent; @@ -292,6 +296,12 @@ interface FFixedServers : TNodeServerAddressArray; FNetClientsDestroyThread : TNetClientsDestroyThread; FNetConnectionsActive: Boolean; + FShutdownRequested: LongInt; + FBlockChainWorkerLock: TReadWriteLock; + FBlockChainWorker: TThreadGetNewBlockChainFromClient; + FBlockChainStartPending: Boolean; + FDiscoveryWorkerLock: TReadWriteLock; + FDiscoveryWorkers: TList; FMaxConnections : Integer; FNetworkAdjustedTime : TNetworkAdjustedTime; FIpInfos : TIpInfos; @@ -303,8 +313,21 @@ interface procedure SetMinServersConnected(AValue: Integer); procedure SetNetConnectionsActive(const Value: Boolean); procedure SetMinFutureBlocksToDownloadNewSafebox(const Value: Integer); - procedure OnDownloadingSafeboxProgressNotify(sender : TObject; const mesage : String; curPos, totalCount : Int64); + procedure SetBlockChainProgress(const AProgress : TProgressInfo); + procedure OnDownloadingSafeboxProgressNotify(sender : TObject; const AProgress : TProgressInfo); + function GetShutdownRequested: Boolean; + procedure StopBlockChainWorker; + procedure StopDiscoveryWorkers; + function DiscoveryActiveLocked: Boolean; + function TryCreateDiscoveryClient(AWorker: TThreadDiscoverConnection): TNetClient; protected + function CreateClientsDestroyThread: TNetClientsDestroyThread; virtual; + function CreateDiscoveryWorker(const AAddress: TNodeServerAddress): TThreadDiscoverConnection; virtual; + function CreateDiscoveryClient: TNetClient; virtual; + procedure StartDiscoveryWorker(AWorker: TThreadDiscoverConnection); virtual; + procedure StartDiscoveryWorkers(const AAddresses: TNodeServerAddressArray); + function CreateBlockChainWorker: TThreadGetNewBlockChainFromClient; virtual; + procedure StartBlockChainWorker(AResumePending: Boolean = false); procedure Notification(AComponent: TComponent; Operation: TOperation); override; Procedure DiscoverServersTerminated(Sender : TObject); procedure DoProcessReservedAreaMessage(senderConnection : TNetConnection; const headerData : TNetHeaderData; receivedData : TStream; responseData : TStream); virtual; @@ -337,16 +360,21 @@ interface Function ConnectionExists(ObjectPointer : TObject) : Boolean; Function ConnectionLock(Sender : TObject; ObjectPointer : TObject; MaxWaitMiliseconds : Cardinal) : Boolean; Procedure ConnectionUnlock(ObjectPointer : TObject); + // Balanced lifetime leases for owned downloads and delayed notifications. + // A transferred lease must be released before the NetData owner can retire. + procedure AcquireDownloadConnections(AConnections: TList); + procedure ReleaseDownloadConnection(AConnection: TNetConnection); Function FindConnectionByClientRandomValue(Sender : TNetConnection) : TNetConnection; Procedure DiscoverServers; Procedure DisconnectClients; - procedure OnReadingNewSafeboxProgressNotify(sender : TObject; const mesage : String; curPos, totalCount : Int64); - Procedure GetNewBlockChainFromClient(Connection : TNetConnection; const why : String); + procedure BeginShutdown; + procedure OnReadingNewSafeboxProgressNotify(sender : TObject; const AProgress : TProgressInfo); + Procedure GetNewBlockChainFromClient(Connection : TNetConnection; const why : String); virtual; Property NodeServersAddresses : TOrderedServerAddressListTS read FNodeServersAddresses; Property NetConnections : TPCThreadList read FNetConnections; Property NetStatistics : TNetStatistics read FNetStatistics; Property IsDiscoveringServers : Boolean read FIsDiscoveringServers; - function IsGettingNewBlockChainFromClient(var status : String) : Boolean; + function IsGettingNewBlockChainFromClient(var status : TProgressInfo) : Boolean; Property MaxRemoteOperationBlock : TOperationBlock read FMaxRemoteOperationBlock; Property NodePrivateKey : TECPrivateKey read FNodePrivateKey; property OnConnectivityChanged : TNotifyManyEvent read FOnConnectivityChanged; @@ -363,6 +391,7 @@ interface Procedure NotifyReceivedHelloMessage; Procedure NotifyStatisticsChanged; Property NetConnectionsActive : Boolean read FNetConnectionsActive write SetNetConnectionsActive; + property ShutdownRequested: Boolean read GetShutdownRequested; Property NetworkAdjustedTime : TNetworkAdjustedTime read FNetworkAdjustedTime; Property MaxNodeServersAddressesBuffer : Integer read FMaxNodeServersAddressesBuffer write SetMaxNodeServersAddressesBuffer; Property OnProcessReservedAreaMessage : TProcessReservedAreaMessage read FOnProcessReservedAreaMessage write FOnProcessReservedAreaMessage; @@ -385,7 +414,7 @@ interface FLastDataReceivedTS : TTickCount; FLastDataSendedTS : TTickCount; FClientBufferRead : TStream; - FNetLock : TPCCriticalSection; + FNetLock : TReadWriteLock; FIsWaitingForResponse : Boolean; FTimestampDiff : Integer; FIsMyselfServer : Boolean; @@ -398,11 +427,20 @@ interface FHasReceivedData : Boolean; FIsDownloadingBlocks : Boolean; FRandomWaitSecondsSendHello : Cardinal; - FBufferLock : TPCCriticalSection; + FBufferLock : TReadWriteLock; FBufferReceivedOperationsHash : TOrderedRawList; FBufferToSendOperations : TOperationsHashTree; FClientTimestampIp : String; + FDownloadOwner: TNetData; + FDownloadRetiring: Boolean; + FDownloadLeaseCount: Integer; + FDownloadLeasesDrained: TEvent; + FDrainingReceivedBlocks: Boolean; + FReceivedProtocolInvalid: Boolean; + FPendingOperationsRequested: LongInt; + function MustDrainReceivedBlocks: Boolean; function GetConnected: Boolean; + function IsPendingStateReadyForPeer: Boolean; procedure SetConnected(const Value: Boolean); procedure TcpClient_OnConnect(Sender: TObject); procedure TcpClient_OnDisconnect(Sender: TObject); @@ -410,10 +448,10 @@ interface Procedure DoProcess_Hello(HeaderData : TNetHeaderData; DataBuffer: TStream); Procedure DoProcess_Message(HeaderData : TNetHeaderData; DataBuffer: TStream); Procedure DoProcess_GetBlocks_Request(HeaderData : TNetHeaderData; DataBuffer: TStream); - Procedure DoProcess_GetBlocks_Response(HeaderData : TNetHeaderData; DataBuffer: TStream); Procedure DoProcess_GetBlockchainOperations_Request(AHeaderData : TNetHeaderData; DataBuffer: TStream); Procedure DoProcess_GetOperationsBlock_Request(HeaderData : TNetHeaderData; DataBuffer: TStream); - Procedure DoProcess_NewBlock(AHeaderData : TNetHeaderData; DataBuffer: TStream); + Procedure DoProcess_NewBlock(AHeaderData : TNetHeaderData; DataBuffer: TStream; + ABufferedOnly: Boolean = False); Procedure DoProcess_AddOperations(HeaderData : TNetHeaderData; DataBuffer: TStream); Procedure DoProcess_GetSafeBox_Request(HeaderData : TNetHeaderData; DataBuffer: TStream); Procedure DoProcess_GetPendingOperations_Request(HeaderData : TNetHeaderData; DataBuffer: TStream); @@ -424,6 +462,17 @@ interface Function ReadTcpClientBuffer(MaxWaitMiliseconds : Cardinal; var HeaderData : TNetHeaderData; BufferData : TStream) : Boolean; function GetClient: TNetTcpIpClient; protected + // Existing processing owners finish locally held block frames without + // admitting another protocol request, including matched-response callers. + procedure DrainReceivedBlockFrames; + // Close admission first, disconnect without freeing the transport, then + // wait outside registry/socket locks for borrowed download use to retire. + procedure RetireDownloadUsage; + // Specialized transports and native protocol regressions share this exact + // decoder/application path. Buffered blocks finish after transport shutdown. + procedure DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData; DataBuffer: TStream); + function CanRequestBlockDownload: Boolean; + function BlockProcessingTerminated: Boolean; Procedure Send(NetTranferType : TNetTransferType; operation, errorcode : Word; request_id : Integer; DataBuffer : TStream); procedure Notification(AComponent: TComponent; Operation: TOperation); override; Procedure SendError(NetTranferType : TNetTransferType; operation, request_id : Integer; error_code : Integer; const error_text : String); @@ -455,14 +504,15 @@ interface Procedure FinalizeConnection; End; - TNetClient = Class; TNetClientThread = Class(TPCThread) private FNetClient : TNetClient; + FStopAfterCurrentBuffer: LongInt; protected procedure BCExecute; override; public Constructor Create(NetClient : TNetClient; AOnTerminateThread : TNotifyEvent); + procedure RequestStopAfterCurrentBuffer; End; TNetClient = Class(TNetConnection) @@ -483,26 +533,37 @@ interface protected Procedure OnNewIncommingConnection(Sender : TObject; Client : TNetTcpIpClient); override; procedure SetActive(const Value: Boolean); override; + function DrainClientBuffersOnStop: Boolean; override; procedure SetMaxConnections(AValue: Integer); override; public Constructor Create; override; End; TThreadDiscoverConnection = Class(TPCThread) - FNodeServerAddress : TNodeServerAddress; + private + FNetData: TNetData; + FExecutionFinished: Boolean; + function StopRequested: Boolean; protected + FNodeServerAddress : TNodeServerAddress; procedure BCExecute; override; public - Constructor Create(NodeServerAddress: TNodeServerAddress; NotifyOnTerminate : TNotifyEvent); + Constructor Create(ANetData: TNetData; const NodeServerAddress: TNodeServerAddress); + procedure StartOwned; End; { TThreadGetNewBlockChainFromClient } TThreadGetNewBlockChainFromClient = Class(TPCThread) + private + FNetData: TNetData; + FExecutionFinished: LongInt; + function GetExecutionFinished: Boolean; protected procedure BCExecute; override; + function SelectConnection(const AConnections: TList): TNetConnection; virtual; public - Constructor Create; + Constructor Create(ANetData: TNetData); End; @@ -531,7 +592,7 @@ function TOrderedServerAddressListTS.CleanBlackList(forceCleanAll : Boolean) : I CleanNodeServersList; // This procedure cleans old blacklisted IPs Result := 0; - FCritical.Acquire; + FCritical.AcquireWriteLock; Try for i := FListByIp.Count - 1 downto 0 do begin P := FListByIp[i]; @@ -547,7 +608,7 @@ function TOrderedServerAddressListTS.CleanBlackList(forceCleanAll : Boolean) : I end; end; Finally - FCritical.Release; + FCritical.ReleaseWriteLock; End; if (Result>0) then FNetData.NotifyBlackListUpdated; end; @@ -559,7 +620,7 @@ procedure TOrderedServerAddressListTS.CleanNodeServersList; begin If Not (FAllowDeleteOnClean) then Exit; currunixtimestamp := UnivDateTimeToUnix(DateTime2UnivDateTime(now)); - FCritical.Acquire; + FCritical.AcquireWriteLock; Try i := FListByIp.Count-1; while (i>=0) do begin @@ -592,7 +653,7 @@ procedure TOrderedServerAddressListTS.CleanNodeServersList; dec(i); end; finally - FCritical.Release; + FCritical.ReleaseWriteLock; end; end; @@ -600,7 +661,7 @@ procedure TOrderedServerAddressListTS.Clear; Var P : PNodeServerAddress; i : Integer; begin - FCritical.Acquire; + FCritical.AcquireWriteLock; Try for i := 0 to FListByIp.Count - 1 do begin P := FListByIp[i]; @@ -611,24 +672,26 @@ procedure TOrderedServerAddressListTS.Clear; FListByNetConnection.Clear; FNetData.FNetStatistics.NodeServersListCount := 0; finally - FCritical.Release; + FCritical.ReleaseWriteLock; end; end; function TOrderedServerAddressListTS.Count: Integer; begin - FCritical.Acquire; + FCritical.AcquireReadLock; try Result := FListByIp.Count; finally - FCritical.Release; + FCritical.ReleaseReadLock; end; end; constructor TOrderedServerAddressListTS.Create(ANetData : TNetData); begin FNetData := ANetData; - FCritical := TPCCriticalSection.Create(Classname); + // Existing list leases expose mutable pointers and remain recursive writes. + // Only complete copied snapshots/lookups below use shared read ownership. + FCritical := TReadWriteLock.Create(Classname,lrpSupportsRecursion); FListByIp := TList.Create; FListByNetConnection := TList.Create; FAllowDeleteOnClean := True; @@ -637,7 +700,7 @@ constructor TOrderedServerAddressListTS.Create(ANetData : TNetData); function TOrderedServerAddressListTS.DeleteNetConnection(netConnection: TNetConnection) : Boolean; Var i : Integer; begin - FCritical.Acquire; + FCritical.AcquireWriteLock; Try If SecuredFindByNetConnection(netConnection,i) then begin PNodeServerAddress( FListByNetConnection[i] )^.netConnection := Nil; @@ -645,7 +708,7 @@ function TOrderedServerAddressListTS.DeleteNetConnection(netConnection: TNetConn Result := True; end else Result := False; Finally - FCritical.Release; + FCritical.ReleaseWriteLock; end; end; @@ -662,7 +725,7 @@ function TOrderedServerAddressListTS.GetNodeServerAddress(const ip: String; port Var i : Integer; P : PNodeServerAddress; begin - FCritical.Acquire; + FCritical.AcquireWriteLock; Try if SecuredFindByIp(ip,port,i) then begin P := FListByIp.Items[i]; @@ -681,7 +744,7 @@ function TOrderedServerAddressListTS.GetNodeServerAddress(const ip: String; port Result := False; end; Finally - FCritical.Release; + FCritical.ReleaseWriteLock; End; end; @@ -708,7 +771,7 @@ procedure TOrderedServerAddressListTS.GetNodeServersToConnnect(maxNodes: Integer l : TList; ns : TNodeServerAddress; begin - FCritical.Acquire; + FCritical.AcquireWriteLock; Try l := TList.Create; Try @@ -764,7 +827,7 @@ procedure TOrderedServerAddressListTS.GetNodeServersToConnnect(maxNodes: Integer l.Free; End; Finally - FCritical.Release; + FCritical.ReleaseWriteLock; end; end; @@ -777,7 +840,8 @@ function TOrderedServerAddressListTS.GetValidNodeServers(OnlyWhereIConnected: Bo currunixtimestamp := UnivDateTimeToUnix(DateTime2UnivDateTime(now)); CleanNodeServersList; // Save other node servers - FCritical.Acquire; + // Random advances mutable PRNG state; preserve serialization for this path. + FCritical.AcquireWriteLock; try If Max>0 then iStart := Random(FListByIp.Count) else iStart := 0; @@ -819,7 +883,7 @@ function TOrderedServerAddressListTS.GetValidNodeServers(OnlyWhereIConnected: Bo end; end; finally - FCritical.Release; + FCritical.ReleaseWriteLock; end; end; @@ -829,7 +893,7 @@ function TOrderedServerAddressListTS.IsBlackListed(const ip: String; out AReason begin Result := false; AReason := ''; - FCritical.Acquire; + FCritical.AcquireReadLock; Try SecuredFindByIp(ip,0,i); // Position will be the first by IP: @@ -843,7 +907,7 @@ function TOrderedServerAddressListTS.IsBlackListed(const ip: String; out AReason inc(i); end; Finally - FCritical.Release; + FCritical.ReleaseReadLock; End; end; @@ -855,7 +919,8 @@ function TOrderedServerAddressListTS.IsBlackListed(const ip: String): Boolean; function TOrderedServerAddressListTS.LockList: TList; begin - FCritical.Acquire; + // Callers may mutate the returned list and pointed records until UnlockList. + FCritical.AcquireWriteLock; Result := FListByIp; end; @@ -864,7 +929,7 @@ procedure TOrderedServerAddressListTS.ResetConnectAttempts; i : Integer; begin CleanNodeServersList; - FCritical.Acquire; + FCritical.AcquireWriteLock; Try for i := FListByIp.Count - 1 downto 0 do begin P := FListByIp[i]; @@ -874,7 +939,7 @@ procedure TOrderedServerAddressListTS.ResetConnectAttempts; P^.last_attempt_to_connect := 0; end; Finally - FCritical.Release; + FCritical.ReleaseWriteLock; End; end; @@ -953,7 +1018,7 @@ procedure TOrderedServerAddressListTS.SetNodeServerAddress( Var i : Integer; P : PNodeServerAddress; begin - FCritical.Acquire; + FCritical.AcquireWriteLock; Try if SecuredFindByIp(nodeServerAddress.ip,nodeServerAddress.port,i) then begin P := FListByIp.Items[i]; @@ -978,26 +1043,26 @@ procedure TOrderedServerAddressListTS.SetNodeServerAddress( end; end; Finally - FCritical.Release; + FCritical.ReleaseWriteLock; end; end; procedure TOrderedServerAddressListTS.UnlockList; begin - FCritical.Release; + FCritical.ReleaseWriteLock; end; procedure TOrderedServerAddressListTS.UpdateNetConnection(netConnection: TNetConnection); Var i : Integer; begin - FCritical.Acquire; + FCritical.AcquireWriteLock; Try If SecuredFindByNetConnection(netConnection,i) then begin PNodeServerAddress(FListByNetConnection[i])^.last_connection := (UnivDateTimeToUnix(DateTime2UnivDateTime(now))); PNodeServerAddress(FListByNetConnection[i])^.total_failed_attemps_to_connect := 0; end; Finally - FCritical.Release; + FCritical.ReleaseWriteLock; End; end; @@ -1046,7 +1111,7 @@ function TNetData.Connection(index: Integer): TNetConnection; begin l := FNetConnections.LockList; try - if (index>=0) And (index=0) And (indexMaxWaitMiliseconds); + try + repeat + if (nc.Connected) and Assigned(nc.FNetLock) then begin + If nc.FNetLock.TryAcquireWriteLock(0) then Result := True + else Sleep(1); + end else Exit; + until (Result) Or (TPlatform.GetElapsedMilliseconds(tc)>MaxWaitMiliseconds); + finally + // Failure/timeout has no matching ConnectionUnlock. A successful call + // transfers exactly one pin to that balanced release, including recursion. + if not Result then ReleaseDownloadConnection(nc); + end; end; end; @@ -1182,27 +1259,28 @@ procedure TNetData.ConnectionUnlock(ObjectPointer: TObject); l : TList; nc : TNetConnection; begin + nc := nil; l := FNetConnections.LockList; try for i := 0 to l.Count - 1 do begin if TObject(l[i])=ObjectPointer then begin - TNetConnection(l[i]).FNetLock.Release; - exit; + nc := TNetConnection(l[i]); + Break; end; end; finally FNetConnections.UnlockList; end; - Try - nc := (ObjectPointer as TNetConnection); - if (not assigned(nc.FNetLock)) then raise Exception.Create('NetLock object not assigned'); - nc.FNetLock.Release; - Except - on E:Exception do begin - TLog.NewLog(ltError,Classname,'Error unlocking Object '+IntToHex(PtrInt(ObjectPointer),8)+' Errors ('+E.ClassName+'): '+E.Message); - end; - End; - TLog.NewLog(ltDebug,ClassName,'Unlocked a NetLock object out of connections list'); + if not Assigned(nc) then begin + // A balanced successful lock cannot disappear from this registry while its + // pin remains. Do not dereference an unregistered, possibly freed pointer. + TLog.NewLog(ltError,ClassName,'Cannot unlock unregistered NetConnection '+IntToHex(PtrInt(ObjectPointer),8)); + Exit; + end; + nc.FNetLock.ReleaseWriteLock; + // Release FNetLock first; no registry ownership is held across socket work or + // a lock wait, and the pin still keeps nc alive until this final release. + ReleaseDownloadConnection(nc); end; constructor TNetData.Create(AOwner: TComponent); @@ -1226,13 +1304,18 @@ constructor TNetData.Create(AOwner: TComponent); FNodeServersAddresses := TOrderedServerAddressListTS.Create(Self); FLastRequestId := 0; FNetConnections := TPCThreadList.Create('TNetData_NetConnections'); - FLockGettingNewBlockChainFromClient := TPCCriticalSection.Create('LockGettingNewBlockChainFromClient'); - FNewBlockChainFromClientStatus := ''; + FBlockChainWorkerLock := TReadWriteLock.Create('TNetData_BlockChainWorker',lrpSupportsRecursion); + FBlockChainStartPending := False; + FDiscoveryWorkerLock := TReadWriteLock.Create('TNetData_DiscoveryWorkers',lrpSupportsRecursion); + FDiscoveryWorkers := TList.Create; + FLockGettingNewBlockChainFromClient := TReadWriteLock.Create('LockGettingNewBlockChainFromClient',lrpSupportsRecursion); + FNewBlockChainProgressLock := TReadWriteLock.Create('NewBlockChainProgress',lrpSupportsRecursion); + FNewBlockChainFromClientStatus := Default(TProgressInfo); FNodePrivateKey := TECPrivateKey.Create; FNodePrivateKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); FThreadCheckConnections := TThreadCheckConnections.Create(Self); FNetDataNotifyEventsThread := TNetDataNotifyEventsThread.Create(Self); - FNetClientsDestroyThread := TNetClientsDestroyThread.Create(Self); + FNetClientsDestroyThread := CreateClientsDestroyThread; FNetworkAdjustedTime := TNetworkAdjustedTime.Create; FMaxNodeServersAddressesBuffer:=(CT_MAX_NODESERVERS_BUFFER DIV 2); FMinServersConnected:=CT_MinServersConnected; @@ -1250,8 +1333,8 @@ constructor TNetData.Create(AOwner: TComponent); destructor TNetData.Destroy; Var l : TList; i : Integer; - tdc : TThreadDiscoverConnection; begin + BeginShutdown; TLog.NewLog(ltInfo,ClassName,'TNetData.Destroy START'); {$IFDEF DELPHI_SYDNEY_PLUS } SetLength(FOnConnectivityChanged.Handlers, 0); @@ -1272,17 +1355,9 @@ destructor TNetData.Destroy; FThreadCheckConnections.WaitFor; FreeAndNil(FThreadCheckConnections); - // Now finish all DiscoverConnection threads - Repeat - tdc := TThreadDiscoverConnection( TPCThreadClass.GetThreadByClass(TThreadDiscoverConnection,nil) ); - if Assigned(tdc) then begin - tdc.FreeOnTerminate := false; - tdc.Terminate; - tdc.WaitFor; - tdc.Free; - TLog.NewLog(ltInfo,ClassName,'TThreadDiscoverConnection finished'); - end; - Until Not Assigned(tdc); + // Execute's global registry cannot see a constructed but not yet scheduled + // discovery. Join the workers registered with this owner before Start instead. + StopDiscoveryWorkers; // Closing connections l := FNetConnections.LockList; @@ -1295,7 +1370,9 @@ destructor TNetData.Destroy; FNetConnections.UnlockList; End; - + // Admission was closed by BeginShutdown. Disconnect first to wake any silent + // response wait, then join the owned downloader before any registry is freed. + StopBlockChainWorker; FNetClientsDestroyThread.WaitForTerminatedAllConnections; FNetClientsDestroyThread.Terminate; FNetClientsDestroyThread.WaitFor; @@ -1312,11 +1389,35 @@ destructor TNetData.Destroy; FreeAndNil(FNetworkAdjustedTime); FreeAndNil(FIpInfos); FreeAndNil(FLockGettingNewBlockChainFromClient); + FreeAndNil(FNewBlockChainProgressLock); + FreeAndNil(FBlockChainWorkerLock); + FreeAndNil(FDiscoveryWorkers); + FreeAndNil(FDiscoveryWorkerLock); inherited; if (_NetData=Self) then _NetData := Nil; TLog.NewLog(ltInfo,ClassName,'TNetData.Destroy END'); end; +procedure TNetData.BeginShutdown; +begin + // Transport-off still drains received blocks. Application shutdown is a + // separate signal, observed only between complete validation/commit units. + {$IFDEF FPC} + InterlockedExchange(FShutdownRequested,1); + {$ELSE} + TInterlocked.Exchange(FShutdownRequested,1); + {$ENDIF} +end; + +function TNetData.GetShutdownRequested: Boolean; +begin + {$IFDEF FPC} + Result := InterlockedCompareExchange(FShutdownRequested,0,0)<>0; + {$ELSE} + Result := TInterlocked.CompareExchange(FShutdownRequested,0,0)<>0; + {$ENDIF} +end; + procedure TNetData.DisconnectClients; var i : Integer; l : TList; @@ -1347,16 +1448,15 @@ procedure TNetData.DiscoverFixedServersOnly(const FixedServers: TNodeServerAddre end; procedure TNetData.DiscoverServers; -Var P : PNodeServerAddress; - i,j,k : Integer; - tdc : TThreadDiscoverConnection; - canAdd : Boolean; +Var j : Integer; nsa : TNodeServerAddressArray; begin - if Not FNetConnectionsActive then exit; - if TPCThread.ThreadClassFound(TThreadDiscoverConnection,nil)>=0 then begin - {$IFDEF HIGHLOG}TLog.NewLog(ltInfo,ClassName,'Already discovering servers...');{$ENDIF} - exit; + if (not FNetConnectionsActive) or ShutdownRequested then exit; + FDiscoveryWorkerLock.AcquireReadLock; + try + if ShutdownRequested or DiscoveryActiveLocked then Exit; + finally + FDiscoveryWorkerLock.ReleaseReadLock; end; FNodeServersAddresses.CleanBlackList(False); If NetStatistics.ClientsConnections>0 then begin @@ -1375,24 +1475,253 @@ procedure TNetData.DiscoverServers; end; if length(nsa)>0 then begin TLog.NewLog(ltDebug,Classname,'Start discovering up to '+inttostr(length(nsa))+' servers... (max:'+inttostr(j)+')'); - // - for i := 0 to high(nsa) do begin - FIsDiscoveringServers := true; - tdc := TThreadDiscoverConnection.Create(nsa[i],DiscoverServersTerminated); - end; + StartDiscoveryWorkers(nsa); end; end; procedure TNetData.DiscoverServersTerminated(Sender: TObject); +var LFinished: Boolean; begin + FDiscoveryWorkerLock.AcquireWriteLock; + try + TThreadDiscoverConnection(Sender).FExecutionFinished := true; + LFinished := FIsDiscoveringServers and not DiscoveryActiveLocked; + if LFinished then FIsDiscoveringServers := false; + finally + FDiscoveryWorkerLock.ReleaseWriteLock; + end; + if ShutdownRequested then Exit; NotifyNodeServersUpdated; - if TPCThread.ThreadClassFound(TThreadDiscoverConnection,Nil)>=0 then exit; - FIsDiscoveringServers := false; + if not LFinished then Exit; + // Publish the completed transition after clearing the state, not on a timer. + NotifyStatisticsChanged; // If here, discover servers finished, so we can try to get/receive data {$IFDEF HIGHLOG}TLog.NewLog(ltDebug,Classname,Format('Discovering servers finished. Now we have %d active connections and %d connections to other servers', [ConnectionsCount(false),ConnectionsCount(true)]));{$ENDIF} - if TPCThread.ThreadClassFound(TThreadGetNewBlockChainFromClient,nil)>=0 then exit; - TThreadGetNewBlockChainFromClient.Create; + StartBlockChainWorker; +end; + +function TNetData.CreateClientsDestroyThread: TNetClientsDestroyThread; +begin + Result := TNetClientsDestroyThread.Create(Self); +end; + +function TNetData.CreateDiscoveryWorker(const AAddress: TNodeServerAddress): TThreadDiscoverConnection; +begin + Result := TThreadDiscoverConnection.Create(Self,AAddress); +end; + +function TNetData.CreateDiscoveryClient: TNetClient; +begin + Result := TNetClient.Create(nil); +end; + +procedure TNetData.StartDiscoveryWorker(AWorker: TThreadDiscoverConnection); +begin + AWorker.StartOwned; +end; + +procedure TNetData.StartDiscoveryWorkers(const AAddresses: TNodeServerAddressArray); +var LRetired: TList; + LWorker: TThreadDiscoverConnection; + LAddress: TNodeServerAddress; +begin + if ShutdownRequested or not FNetConnectionsActive then Exit; + LRetired := TList.Create; + try + FDiscoveryWorkerLock.AcquireWriteLock; + try + if ShutdownRequested or not FNetConnectionsActive or DiscoveryActiveLocked then Exit; + for LWorker in FDiscoveryWorkers do LRetired.Add(LWorker); + FDiscoveryWorkers.Clear; + try + for LAddress in AAddresses do begin + LWorker := CreateDiscoveryWorker(LAddress); + if not Assigned(LWorker) then raise EThread.Create('Discovery worker factory returned nil'); + // Register while suspended. Even a pending, unscheduled worker belongs + // to shutdown; Execute's global thread registry is not its owner. + FDiscoveryWorkers.Add(LWorker); + try + StartDiscoveryWorker(LWorker); + except + FDiscoveryWorkers.Remove(LWorker); + LWorker.Terminate; + LRetired.Add(LWorker); + raise; + end; + end; + finally + FIsDiscoveringServers := DiscoveryActiveLocked; + end; + finally + FDiscoveryWorkerLock.ReleaseWriteLock; + end; + if FIsDiscoveringServers then NotifyStatisticsChanged; + finally + // Completed and failed-start workers may still be leaving Execute. Their + // destructor joins them without holding discovery admission or the registry. + for LWorker in LRetired do LWorker.Free; + LRetired.Free; + end; +end; + +procedure TNetData.StopDiscoveryWorkers; +var LWorkers: TList; + LWorker: TThreadDiscoverConnection; +begin + if not Assigned(FDiscoveryWorkerLock) then Exit; + LWorkers := TList.Create; + try + FDiscoveryWorkerLock.AcquireWriteLock; + try + for LWorker in FDiscoveryWorkers do begin + LWorker.Terminate; + LWorkers.Add(LWorker); + end; + FDiscoveryWorkers.Clear; + FIsDiscoveringServers := false; + finally + FDiscoveryWorkerLock.ReleaseWriteLock; + end; + // All are terminated before the first join. TThread.Destroy also starts a + // never-started terminated thread so its native handle is joined safely. + for LWorker in LWorkers do LWorker.Free; + finally + LWorkers.Free; + end; +end; + +function TNetData.DiscoveryActiveLocked: Boolean; +var LWorker: TThreadDiscoverConnection; +begin + Result := true; + for LWorker in FDiscoveryWorkers do + if not LWorker.FExecutionFinished then Exit; + Result := false; +end; + +function TNetData.TryCreateDiscoveryClient(AWorker: TThreadDiscoverConnection): TNetClient; +begin + Result := nil; + FDiscoveryWorkerLock.AcquireWriteLock; + try + if AWorker.StopRequested then Exit; + Result := CreateDiscoveryClient; + if not Assigned(Result) then raise EThread.Create('Discovery client factory returned nil'); + FNetConnections.LockList; + try + // Discovery retains its client through Connect/finalization just as a + // downloader retains a selected peer. The destroyer skips held leases. + if Result.FDownloadLeaseCount = 0 then Result.FDownloadLeasesDrained.ResetEvent; + Inc(Result.FDownloadLeaseCount); + finally + FNetConnections.UnlockList; + end; + finally + FDiscoveryWorkerLock.ReleaseWriteLock; + end; +end; + +function TNetData.CreateBlockChainWorker: TThreadGetNewBlockChainFromClient; +begin + Result := TThreadGetNewBlockChainFromClient.Create(Self); +end; + +procedure TNetData.StartBlockChainWorker(AResumePending: Boolean); +var LPrevious, LFailed: TThreadGetNewBlockChainFromClient; +begin + // Late producer callbacks deny admission without touching an owner lock that + // shutdown may already have retired. Repeat the check under admission below. + if ShutdownRequested or (AResumePending and not FNetConnectionsActive) then Exit; + LPrevious := nil; + LFailed := nil; + try + FBlockChainWorkerLock.AcquireWriteLock; + try + if ShutdownRequested then Exit; + if AResumePending and not FBlockChainStartPending then Exit; + // A replacement peer's HELLO can arrive while the previous download is + // still unwinding. Retain that intent until the owned maintenance thread + // can start its successor; the completing worker must never join itself. + FBlockChainStartPending := True; + if Assigned(FBlockChainWorker) and + not FBlockChainWorker.GetExecutionFinished then Exit; + FBlockChainStartPending := False; + LPrevious := FBlockChainWorker; + FBlockChainWorker := nil; + // Construct suspended and publish ownership before Start. A shutdown can + // never observe an untracked running worker or join an unstarted one. + FBlockChainWorker := CreateBlockChainWorker; + if not Assigned(FBlockChainWorker) then + raise EThread.Create('Blockchain worker factory returned nil'); + try + FBlockChainWorker.Start; + except + LFailed := FBlockChainWorker; + FBlockChainWorker := nil; + LFailed.Terminate; + raise; + end; + finally + FBlockChainWorkerLock.ReleaseWriteLock; + end; + finally + // Both joins/destruction are outside admission. Start/constructor failures + // also retain an owner until the failed object has been disposed. + LPrevious.Free; + LFailed.Free; + end; +end; + +procedure TNetData.StopBlockChainWorker; +var LWorker: TThreadGetNewBlockChainFromClient; +begin + if not Assigned(FBlockChainWorkerLock) then Exit; + FBlockChainWorkerLock.AcquireWriteLock; + try + LWorker := FBlockChainWorker; + FBlockChainWorker := nil; + if Assigned(LWorker) then LWorker.Terminate; + finally + FBlockChainWorkerLock.ReleaseWriteLock; + end; + if Assigned(LWorker) then begin + LWorker.WaitFor; + LWorker.Free; + end; +end; + +procedure TNetData.AcquireDownloadConnections(AConnections: TList); +var LList: TList; LConnection: TNetConnection; +begin + LList := FNetConnections.LockList; + try + if ShutdownRequested or not FNetConnectionsActive then Exit; + for LConnection in LList do + if (not LConnection.FDownloadRetiring) and + (not LConnection.FDoFinalizeConnection) and LConnection.Connected then begin + AConnections.Add(LConnection); + if LConnection.FDownloadLeaseCount = 0 then + LConnection.FDownloadLeasesDrained.ResetEvent; + Inc(LConnection.FDownloadLeaseCount); + end; + finally + FNetConnections.UnlockList; + end; +end; + +procedure TNetData.ReleaseDownloadConnection(AConnection: TNetConnection); +begin + FNetConnections.LockList; + try + if AConnection.FDownloadLeaseCount <= 0 then + raise Exception.Create('Unbalanced blockchain connection lease'); + Dec(AConnection.FDownloadLeaseCount); + if AConnection.FDownloadLeaseCount = 0 then + AConnection.FDownloadLeasesDrained.SetEvent; + finally + FNetConnections.UnlockList; + end; end; procedure TNetData.DoProcessReservedAreaMessage(senderConnection : TNetConnection; const headerData: TNetHeaderData; receivedData: TStream; responseData: TStream); @@ -1489,6 +1818,7 @@ function TNetData.GetConnection(index: Integer; var NetConnection : TNetConnecti l := FNetConnections.LockList; try if (index>=0) And (index=CT_PROTOCOL_2 then begin @@ -2136,9 +2484,25 @@ procedure TNetData.GetNewBlockChainFromClient(Connection: TNetConnection; // High to new value: DownloadNewBlockchain(my_op.block+1,True); end; + except + LProcessingFailed := True; + raise; + end; Finally - FLockGettingNewBlockChainFromClient.Release; - TLog.NewLog(ltdebug,CT_LogSender,Format('Finalizing process in %d milis',[TPlatform.GetElapsedMilliseconds(LTickCount)])); + try + // A matching response is consumed outside DoSendAndWaitForResponse. + // Retain this real downloader's physical scope through its local suffix, + // rather than briefly publishing Offline before its owner drains it. + if (not LProcessingFailed) and Connection.MustDrainReceivedBlocks then + Connection.DrainReceivedBlockFrames; + finally + try TNode.Node.EndBlockChainUpdate; + finally + FLockGettingNewBlockChainFromClient.ReleaseWriteLock; + NotifyStatisticsChanged; + TLog.NewLog(ltdebug,CT_LogSender,Format('Finalizing process in %d milis',[TPlatform.GetElapsedMilliseconds(LTickCount)])); + end; + end; end; end; @@ -2156,7 +2520,7 @@ procedure TNetData.IncStatistics(incActiveConnections, incClientsConnections, incServersConnections,incServersConnectionsWithResponse: Integer; incBytesReceived, incBytesSend: Int64); begin // Multithread prevention - FNodeServersAddresses.FCritical.Acquire; + FNodeServersAddresses.FCritical.AcquireWriteLock; Try FNetStatistics.ActiveConnections := FNetStatistics.ActiveConnections + incActiveConnections; FNetStatistics.ClientsConnections := FNetStatistics.ClientsConnections + incClientsConnections; @@ -2168,7 +2532,7 @@ procedure TNetData.IncStatistics(incActiveConnections, incClientsConnections, FNetStatistics.BytesReceived := FNetStatistics.BytesReceived + incBytesReceived; FNetStatistics.BytesSend := FNetStatistics.BytesSend + incBytesSend; Finally - FNodeServersAddresses.FCritical.Release; + FNodeServersAddresses.FCritical.ReleaseWriteLock; End; NotifyStatisticsChanged; if (incBytesReceived<>0) Or (incBytesSend<>0) then begin @@ -2176,17 +2540,23 @@ procedure TNetData.IncStatistics(incActiveConnections, incClientsConnections, end; end; -function TNetData.IsGettingNewBlockChainFromClient(var status: String): Boolean; +function TNetData.IsGettingNewBlockChainFromClient(var status: TProgressInfo): Boolean; begin - if FLockGettingNewBlockChainFromClient.TryEnter then begin + if FLockGettingNewBlockChainFromClient.TryAcquireWriteLock(0) then begin try Result := False; - status := ''; + status := Default(TProgressInfo); finally - FLockGettingNewBlockChainFromClient.Release; + FLockGettingNewBlockChainFromClient.ReleaseWriteLock; end; end else begin - status := FNewBlockChainFromClientStatus; + // Copy only the small managed snapshot, never wait for download completion. + FNewBlockChainProgressLock.AcquireReadLock; + try + status := FNewBlockChainFromClientStatus; + finally + FNewBlockChainProgressLock.ReleaseReadLock; + end; Result := True; end; end; @@ -2289,20 +2659,33 @@ procedure TNetData.NotifyStatisticsChanged; FNetDataNotifyEventsThread.FNotifyOnStatisticsChanged := true; end; -procedure TNetData.OnDownloadingSafeboxProgressNotify(sender: TObject; - const mesage: String; curPos, totalCount: Int64); -Var pct : String; +procedure TNetData.SetBlockChainProgress(const AProgress: TProgressInfo); begin - if (totalCount>0) then pct := FormatFloat('0.00',curPos*100/totalCount)+'%' else pct := ''; - FNewBlockChainFromClientStatus := Format('%s %s',[mesage,pct]); + // Synchronize record/string ownership separately from the long download gate. + // Formatting and notification callbacks must run outside this short copy lock. + FNewBlockChainProgressLock.AcquireWriteLock; + try + FNewBlockChainFromClientStatus := AProgress; + finally + FNewBlockChainProgressLock.ReleaseWriteLock; + end; + if TNode.NodeExists then TNode.Node.SetSynchronizationProgress(AProgress); end; -procedure TNetData.OnReadingNewSafeboxProgressNotify(sender: TObject; const mesage: String; curPos, totalCount: Int64); -Var pct : String; +procedure TNetData.OnDownloadingSafeboxProgressNotify(sender: TObject; + const AProgress: TProgressInfo); begin - if (totalCount>0) then pct := FormatFloat('0.00',curPos*100/totalCount)+'%' else pct := ''; + SetBlockChainProgress(AProgress); + NotifyStatisticsChanged; +end; - FNewBlockChainFromClientStatus := Format('Checking new safebox: %s %s',[mesage,pct]); +procedure TNetData.OnReadingNewSafeboxProgressNotify(sender: TObject; const AProgress: TProgressInfo); +var LProgress : TProgressInfo; +begin + LProgress := AProgress; + LProgress.Context := pcCheckingNewSafebox; + SetBlockChainProgress(LProgress); + NotifyStatisticsChanged; end; class function TNetData.OperationToText(operation: Word): String; @@ -2367,7 +2750,16 @@ procedure TNetData.RegisterRequest(Sender: TNetConnection; operation: Word; requ procedure TNetData.SetNetConnectionsActive(const Value: Boolean); begin + if Value and ShutdownRequested then Exit; + // Explicit OFF -> ON is a fresh connection attempt, as on application + // startup. Retain normal automatic retry backoff while networking stays on. + if Value and not FNetConnectionsActive then + FNodeServersAddresses.ResetConnectAttempts; FNetConnectionsActive := Value; + // Stop new acquisition before closing sockets. Received work retains its + // synchronizing state until its physical scopes drain; only then can local + // pages resume without the discarded remote target keeping them suspended. + TNode.Node.SetSynchronizationNetworkActive(Value); NotifyConnectivityChanged; if FNetConnectionsActive then DiscoverServers else DisconnectClients; @@ -2458,6 +2850,8 @@ procedure TNetServer.OnNewIncommingConnection(Sender : TObject; Client : TNetTcp Until (Not n.Connected) Or (tc + 5000 < TPlatform.GetTickCount); sleep(5); DebugStep := 'Assigning old client'; + n.DrainReceivedBlockFrames; + n.RetireDownloadUsage; n.SetClient( NetTcpIpClientClass.Create(Nil) ); sleep(500); // Delay - Sleep time before destroying (1.5.3) DebugStep := 'Freeing NetServerClient'; @@ -2474,6 +2868,9 @@ procedure TNetServer.OnNewIncommingConnection(Sender : TObject; Client : TNetTcp procedure TNetServer.SetActive(const Value: Boolean); begin + // An enable request queued before Quit must not reopen the listener after + // shutdown was published while its preceding network task was draining. + if Value and TNetData.NetDataExists and TNetData.NetData.ShutdownRequested then Exit; if Value then begin TLog.NewLog(ltinfo,Classname,'Activating server on port '+IntToStr(Port)); end else begin @@ -2488,6 +2885,11 @@ procedure TNetServer.SetActive(const Value: Boolean); end; end; +function TNetServer.DrainClientBuffersOnStop: Boolean; +begin + Result := True; +end; + procedure TNetServer.SetMaxConnections(AValue: Integer); begin inherited SetMaxConnections(AValue); @@ -2501,7 +2903,7 @@ function TNetConnection.AddOperationsToBufferForSend(Operations: TOperationsHash begin Result := 0; try - FBufferLock.Acquire; + FBufferLock.AcquireWriteLock; Try for i := 0 to Operations.OperationsCount - 1 do begin if FBufferReceivedOperationsHash.IndexOf(Operations.GetOperation(i).Sha256)<0 then begin @@ -2513,7 +2915,7 @@ function TNetConnection.AddOperationsToBufferForSend(Operations: TOperationsHash end; end; finally - FBufferLock.Release; + FBufferLock.ReleaseWriteLock; end; Except On E:Exception do begin @@ -2534,11 +2936,14 @@ function TNetConnection.ConnectTo(ServerIP: String; ServerPort: Word) : Boolean; Var nsa : TNodeServerAddress; i : Integer; begin + Result := False; + if (not TNetData.NetData.NetConnectionsActive) or + TNetData.NetData.ShutdownRequested then Exit; If FIsConnecting then Exit; Try FIsConnecting:=True; if Client.Connected then Client.Disconnect; - TPCThread.ProtectEnterCriticalSection(Self,FNetLock); + TPCThread.ProtectAcquireWriteLock(Self,FNetLock); Try Client.RemoteHost := ServerIP; if ServerPort<=0 then ServerPort := CT_NetServer_Port; @@ -2548,9 +2953,17 @@ function TNetConnection.ConnectTo(ServerIP: String; ServerPort: Word) : Boolean; nsa.netConnection := Self; TNetData.NetData.NodeServersAddresses.SetNodeServerAddress(nsa); TNetData.NetData.NotifyNetConnectionUpdated; + FReceivedProtocolInvalid := False; Result := Client.Connect; + // A connect already in flight may finish after OFF. Do not let its new + // socket begin another download; already received block buffers are separate. + if Result and ((not TNetData.NetData.NetConnectionsActive) or + TNetData.NetData.ShutdownRequested) then begin + Client.Disconnect; + Result := False; + end; Finally - FNetLock.Release; + FNetLock.ReleaseWriteLock; End; if Result then begin {$IFDEF HIGHLOG}TLog.NewLog(ltDebug,Classname,'Connected to a possible server at: '+ClientRemoteAddr);{$ENDIF} @@ -2585,7 +2998,9 @@ constructor TNetConnection.Create(AOwner: TComponent); FTimestampDiff := 0; FIsWaitingForResponse := false; FClientBufferRead := TMemoryStream.Create; - FNetLock := TPCCriticalSection.Create('TNetConnection_NetLock'); + // Legacy ConnectionLock leases serialize mutable protocol/stream state and + // may nest on the same thread; a successful write still owns one lifetime pin. + FNetLock := TReadWriteLock.Create('TNetConnection_NetLock',lrpSupportsRecursion); FLastHelloTS := 0; FLastDataReceivedTS := 0; FLastDataSendedTS := 0; @@ -2594,9 +3009,11 @@ constructor TNetConnection.Create(AOwner: TComponent); FRemoteOperationBlock := CT_OperationBlock_NUL; FRemoteAccumulatedWork := 0; SetClient( TBufferedNetTcpIpClient.Create(Self) ); - TNetData.NetData.FNetConnections.Add(Self); + FDownloadOwner := TNetData.NetData; + FDownloadLeasesDrained := TEvent.Create(nil, true, true, ''); + FDownloadOwner.FNetConnections.Add(Self); TNetData.NetData.NotifyNetConnectionUpdated; - FBufferLock := TPCCriticalSection.Create('TNetConnection_BufferLock'); + FBufferLock := TReadWriteLock.Create('TNetConnection_BufferLock',lrpSupportsRecursion); FBufferReceivedOperationsHash := TOrderedRawList.Create; FBufferToSendOperations := TOperationsHashTree.Create; FClientTimestampIp := ''; @@ -2604,12 +3021,13 @@ constructor TNetConnection.Create(AOwner: TComponent); destructor TNetConnection.Destroy; begin + RetireDownloadUsage; + if TNode.NodeExists then TNode.Node.SetSynchronizationPeerConnected(Self,False); Try {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,ClassName,'Destroying '+Classname+' '+IntToHex(PtrInt(Self),8));{$ENDIF} Connected := false; Finally TNetData.NetData.NodeServersAddresses.DeleteNetConnection(Self); - TNetData.NetData.FNetConnections.Remove(Self); End; TNetData.NetData.UnRegisterRequest(Self,0,0); Try @@ -2622,15 +3040,53 @@ destructor TNetConnection.Destroy; FreeAndNil(FBufferReceivedOperationsHash); FreeAndNil(FBufferToSendOperations); FreeAndNil(FRemoteAggregatedHashrate); + FreeAndNil(FDownloadLeasesDrained); + // Keep the registry entry until all fields used by a retiring downloader + // have gone. NetData's connection-drain completion must not race that work. + if Assigned(FDownloadOwner) then FDownloadOwner.FNetConnections.Remove(Self); inherited; End; end; +procedure TNetConnection.RetireDownloadUsage; +var LHasLease: Boolean; +begin + if not Assigned(FDownloadOwner) or not Assigned(FDownloadLeasesDrained) then Exit; + FDownloadOwner.FNetConnections.LockList; + try + FDownloadRetiring := true; + finally + FDownloadOwner.FNetConnections.UnlockList; + end; + // Wake a selected downloader's silent socket wait without replacing/freeing + // its transport. Already decoded receive buffers retain their ordinary owner. + Connected := false; + repeat + FDownloadOwner.FNetConnections.LockList; + try + LHasLease := FDownloadLeaseCount <> 0; + finally + FDownloadOwner.FNetConnections.UnlockList; + end; + if not LHasLease then Exit; + // There is no registry/FNetLock held here. Main-thread destruction must also + // service existing synchronized notifications that a downloader can emit. + if TThread.CurrentThread.ThreadID = MainThreadID then begin + CheckSynchronize; + FDownloadLeasesDrained.WaitFor(10); + end else + FDownloadLeasesDrained.WaitFor(INFINITE); + until false; +end; + procedure TNetConnection.DisconnectInvalidClient(ItsMyself : Boolean; const why: String); Var include_in_list : Boolean; ns : TNodeServerAddress; aux_s : String; begin + // An invalid session must not gain a second acceptance path through the + // disconnected buffered-only drain. A later explicit Connect resets this. + FReceivedProtocolInvalid := True; FIsDownloadingBlocks := false; if ItsMyself then begin TLog.NewLog(ltInfo,Classname,'Disconecting myself '+ClientRemoteAddr+' > '+Why) @@ -2668,6 +3124,10 @@ procedure TNetConnection.DoProcessBuffer; ops : String; iPending : Integer; begin + if MustDrainReceivedBlocks then begin + DrainReceivedBlockFrames; + Exit; + end; if FDoFinalizeConnection then begin if Connected then begin TLog.NewLog(ltdebug,Classname,'Executing DoFinalizeConnection at client '+ClientRemoteAddr); @@ -2683,6 +3143,23 @@ procedure TNetConnection.DoProcessBuffer; finally ms.Free; end; + // The socket may have been disconnected during the atomic current frame. + // Its retained block drain has finished; never allocate a HELLO/request now. + if MustDrainReceivedBlocks or BlockProcessingTerminated then Exit; + // Pending exchange may have been requested at the end of a block response, + // while its outer catch-up scope still hid the overlay. Service that one + // retained protocol intent on the normal I/O owner after reconciliation; + // never make block download wait for a pending replay or use a UI timer. + if (not FIsWaitingForResponse) and (FLastDataReceivedTS>0) and + IsPendingStateReadyForPeer and CanRequestBlockDownload then begin + {$IFDEF FPC} + iPending := InterlockedExchange(FPendingOperationsRequested,0); + {$ELSE} + iPending := TInterlocked.Exchange(FPendingOperationsRequested,0); + {$ENDIF} + if iPending<>0 then DoProcess_GetPendingOperations; + if MustDrainReceivedBlocks or BlockProcessingTerminated then Exit; + end; If ((FLastDataReceivedTS>0) Or ( NOT (Self is TNetServerClient))) and (TPlatform.GetElapsedMilliseconds(FLastHelloTS)>(1000*FRandomWaitSecondsSendHello)) then begin iPending := TNetData.NetData.PendingRequest(Self,ops); @@ -2702,6 +3179,95 @@ procedure TNetConnection.DoProcessBuffer; end; end; +function TNetConnection.MustDrainReceivedBlocks: Boolean; +begin + Result := FDoFinalizeConnection or (not Connected) or + (TNetData.NetDataExists and (not TNetData.NetData.NetConnectionsActive)); +end; + +procedure TNetConnection.DrainReceivedBlockFrames; +var + LClient: TBufferedNetTcpIpClient; + LReadBuffer, LPayload: TMemoryStream; + LHeader: TNetHeaderData; + LNeedMore, LPhysicalScope: Boolean; + LReceivedBytes: Int64; +begin + if BlockProcessingTerminated or FReceivedProtocolInvalid or + (not Assigned(FTcpIpClient)) then Exit; + LClient := FTcpIpClient as TBufferedNetTcpIpClient; + LPhysicalScope := False; + LPayload := TMemoryStream.Create; + LClient.PauseReceiving; + try + // No registry or buffer gate is held while a last admitted read publishes. + // An outer legacy/recursive FNetLock may remain owned by this caller; + // the transport's fixed TcpClient_OnDisconnect callback only publishes + // cached status/statistics/notify flags and never acquires FNetLock. + // DoReceivedData runs after the receive reservation has ended. + // This also leaves the IO worker reusable if the + // same transport is connected again after the balanced resume below. + LClient.Disconnect; + LClient.WaitForReceiveIdle; + TPCThread.ProtectAcquireWriteLock(Self,FNetLock); + try + if FDrainingReceivedBlocks then Exit; + FDrainingReceivedBlocks := True; + try + LReadBuffer := LClient.ReadBufferLock; + try + LReceivedBytes := LReadBuffer.Size; + FClientBufferRead.Position := FClientBufferRead.Size; + if LReceivedBytes>0 then FClientBufferRead.CopyFrom(LReadBuffer,0); + LReadBuffer.Size := 0; + finally LClient.ReadBufferUnlock; end; + // Disconnect already retired the live-peer counters. Account retained + // bytes only; a first buffered frame must not resurrect a dead server. + if LReceivedBytes>0 then TNetData.NetData.IncStatistics(0,0,0,0,LReceivedBytes,0); + FClientBufferRead.Position := 0; + while (not BlockProcessingTerminated) and (not FReceivedProtocolInvalid) do begin + if not TNetData.ExtractHeaderInfo(FClientBufferRead,LHeader,LPayload,LNeedMore) then Break; + // Keep ordinary protocol validation. A complete frame is not proof + // that its protocol, error/type or full block payload is acceptable. + if LHeader.protocol.protocol_version>CT_NetProtocol_Available then begin + DisconnectInvalidClient(False,'Invalid buffered Net protocol version'); + Break; + end; + if LHeader.is_error then Continue; + if ((LHeader.operation=CT_NetOp_GetBlocks) and (LHeader.header_type=ntp_response)) or + ((LHeader.operation in [CT_NetOp_NewBlock,CT_NetOp_NewBlock_Fast_Propagation]) and + (LHeader.header_type=ntp_autosend)) then begin + if not LPhysicalScope then begin + TNode.Node.BeginBlockChainUpdate; + LPhysicalScope := True; + end; + if LHeader.operation=CT_NetOp_GetBlocks then begin + TNetData.NetData.UnRegisterRequest(Self,LHeader.operation,LHeader.request_id); + DoProcess_GetBlocks_Response(LHeader,LPayload); + end else DoProcess_NewBlock(LHeader,LPayload,True); + end; + // No HELLO, requests, reserved callbacks or fast-propagation fetches + // are dispatched here. Full blocks and fast announcements whose + // referenced operations are already local can finish offline. An + // incomplete/invalid suffix is safely discarded. + end; + FClientBufferRead.Size := 0; + finally + FDrainingReceivedBlocks := False; + end; + finally + FNetLock.ReleaseWriteLock; + end; + finally + try + if LPhysicalScope then TNode.Node.EndBlockChainUpdate; + finally + LClient.ResumeReceiving; + LPayload.Free; + end; + end; +end; + procedure TNetConnection.DoProcess_AddOperations(HeaderData: TNetHeaderData; DataBuffer: TStream); var c,i : Integer; optype : Byte; @@ -2712,6 +3278,13 @@ procedure TNetConnection.DoProcess_AddOperations(HeaderData: TNetHeaderData; Dat DoDisconnect : Boolean; Lopc,Lprc : Integer; begin + // A first HELLO can announce a newer chain before its downloader starts. + // Defer pending work rather than execute modern operations on an old overlay. + if (HeaderData.header_type=ntp_autosend) and not IsPendingStateReadyForPeer then begin + {$IFDEF FPC}InterlockedExchange(FPendingOperationsRequested,1); + {$ELSE}TInterlocked.Exchange(FPendingOperationsRequested,1);{$ENDIF} + Exit; + end; DoDisconnect := true; operations := TOperationsHashTree.Create; try @@ -2729,7 +3302,7 @@ procedure TNetConnection.DoProcess_AddOperations(HeaderData: TNetHeaderData; Dat DisconnectInvalidClient(false,errors+' > '+TNetData.HeaderDataToText(HeaderData)+' BuffSize: '+inttostr(DataBuffer.Size)); end else begin // Add to received buffer - FBufferLock.Acquire; + FBufferLock.AcquireWriteLock; Try for i := 0 to operations.OperationsCount - 1 do begin op := operations.GetOperation(i); @@ -2740,7 +3313,7 @@ procedure TNetConnection.DoProcess_AddOperations(HeaderData: TNetHeaderData; Dat end; end; Finally - FBufferLock.Release; + FBufferLock.ReleaseWriteLock; End; TNode.Node.AddOperations(Self,operations,Nil,errors); end; @@ -2941,6 +3514,30 @@ procedure TNetConnection.DoProcess_GetBlocks_Request(HeaderData: TNetHeaderData; end; end; +function TNetConnection.BlockProcessingTerminated: Boolean; +begin + // Retire buffered work only at the existing between-block boundaries. A + // storage failure prevents any subsequent commit without killing a worker. + if TNode.NodeExists and TNode.Node.HasStorageFailure then Exit(True); + if TNetData.NetDataExists and TNetData.NetData.ShutdownRequested then + Exit(True); + Result := False; + {$IFNDEF FPC} + // Delphi rejects CheckTerminated for main/externally created threads. These + // public transport methods are also called by UI/RPC code; only owned workers + // have a cancellation flag to inspect through that RTL API. + if TThread.CurrentThread.ExternalThread then Exit; + {$ENDIF} + Result := TThread.CheckTerminated; +end; + +function TNetConnection.CanRequestBlockDownload: Boolean; +begin + Result := TNode.Node.CanRequestBlockChainData and + (not FDoFinalizeConnection) and Connected and + (not BlockProcessingTerminated); +end; + procedure TNetConnection.DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData; DataBuffer: TStream); var LTmpOp : TPCOperationsComp; LOpCountCardinal,c : Cardinal; @@ -2951,7 +3548,10 @@ procedure TNetConnection.DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData LBlocks : TList; LSafeboxTransaction : TPCSafeBoxTransaction; LPrevious : Boolean; + LOperationBlock : TOperationBlock; begin + TNode.Node.BeginBlockChainUpdate; + try DoDisconnect := true; {$IFDEF USE_ABSTRACTMEM} if (TNode.Node.Bank.Storage is TAbstractMemBlockchainStorage) then begin @@ -2980,6 +3580,7 @@ procedure TNetConnection.DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData LOpCount := LOpCountCardinal; DoDisconnect :=false; for i := 0 to LOpCount-1 do begin + if BlockProcessingTerminated then Exit; LTmpOp := TPCOperationsComp.Create(nil); try LTmpOp.bank := TNode.Node.Bank; @@ -2999,7 +3600,13 @@ procedure TNetConnection.DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData end; end; + if LBlocks.Count>0 then + TNode.Node.LogBlockChainBatchReceived(Self,LBlocks.Count, + LBlocks[0].OperationBlock.block,LBlocks[LBlocks.Count-1].OperationBlock.block, + DataBuffer.Size); + if BlockProcessingTerminated then Exit; TPCOperationsBlockValidator.MultiThreadValidateOperationsBlock(LBlocks); + if BlockProcessingTerminated then Exit; LSafeboxTransaction := TPCSafeBoxTransaction.Create(TNode.Node.Bank.SafeBox); try TPCOperationsSignatureValidator.MultiThreadPreValidateSignatures(LSafeboxTransaction,LBlocks,Nil); @@ -3008,9 +3615,22 @@ procedure TNetConnection.DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData end; for i := 0 to LBlocks.Count-1 do begin + // Network-off stops acquisition, not this received backlog. Explicit + // worker shutdown may retire it only between complete atomic blocks. + if BlockProcessingTerminated then Exit; + while not TNode.Node.TryLockNode(100) do + if BlockProcessingTerminated then Exit; + try + if BlockProcessingTerminated then Exit; + // Preserve this path's existing no-rebroadcast policy, but protect the + // bank commit AND synchronous mempool reconciliation as one node write. + // Readers must not observe a new confirmed base with old pending deltas. if (LBlocks[i].OperationBlock.block=TNode.Node.Bank.BlocksCount) then begin + // A successful bank commit clears the supplied operations. Retain + // its original header for the same audit record as announced blocks. + LOperationBlock := LBlocks[i].OperationBlock; if (TNode.Node.Bank.AddNewBlockChainBlock(LBlocks[i],TNetData.NetData.NetworkAdjustedTime.GetMaxAllowedTimestampForNewBlock, errors)) then begin - // Ok, one more! + TNode.Node.LogBlockChainResult(Self,LOperationBlock,True,''); end else begin // Is not a valid entry???? // Perhaps an orphan blockchain: Me or Client! @@ -3024,10 +3644,14 @@ procedure TNetConnection.DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData FIsDownloadingBlocks := false; exit; end; + finally + TNode.Node.UnlockNode; + end; sleep(1); end; FIsDownloadingBlocks := false; + if not CanRequestBlockDownload then Exit; if ((LOpCount>0) And (FRemoteOperationBlock.block>=TNode.Node.Bank.BlocksCount)) then begin Send_GetBlocks(TNode.Node.Bank.BlocksCount,100+Random(300),c); end else begin @@ -3042,19 +3666,21 @@ procedure TNetConnection.DoProcess_GetBlocks_Response(HeaderData: TNetHeaderData LBlocks.Free; End; Finally + FIsDownloadingBlocks := False; if DoDisconnect then begin DisconnectInvalidClient(false,errors+' > '+TNetData.HeaderDataToText(HeaderData)+' BuffSize: '+inttostr(DataBuffer.Size)); end; {$IFDEF USE_ABSTRACTMEM} - TNode.Node.Bank.SafeBox.PCAbstractMem.FlushCache; - if (TNode.Node.Bank.Storage is TAbstractMemBlockchainStorage) then begin - TAbstractMemBlockchainStorage( TNode.Node.Bank.Storage ).AutoFlushCache := LPrevious; - if TAbstractMemBlockchainStorage( TNode.Node.Bank.Storage ).PendingToSave = 0 then begin - TAbstractMemBlockchainStorage( TNode.Node.Bank.Storage ).FileMem.FlushCache; - end; - end; + // Restore batching policy even if persisting the accepted batch fails. + if (TNode.Node.Bank.Storage is TAbstractMemBlockchainStorage) then + TAbstractMemBlockchainStorage(TNode.Node.Bank.Storage).AutoFlushCache := LPrevious; + TNode.Node.FlushSafebox; + TNode.Node.FlushBlockStorage; {$ENDIF} end; + finally + TNode.Node.EndBlockChainUpdate; + end; end; procedure TNetConnection.DoProcess_GetOperationsBlock_Request(HeaderData: TNetHeaderData; DataBuffer: TStream); @@ -3242,6 +3868,25 @@ procedure TNetConnection.DoProcess_GetPendingOperations_Request(HeaderData: TNet errors : String; opht : TOperationsHashTree; LLockedMempool : TPCOperationsComp; + + procedure SendUnavailableOperations; + var LEmpty: TOperationsHashTree; LCount: Cardinal; + begin + // Legacy requesters parse this response without checking error frames. + // An unavailable overlay exposes zero available operations, using the + // existing count/tree encodings rather than a misleading protocol error. + responseStream.Clear; + if b=1 then begin + LCount := 0; + responseStream.Write(LCount,SizeOf(LCount)); + end else begin + LEmpty := TOperationsHashTree.Create; + try LEmpty.SaveOperationsHashTreeToStream(responseStream,False); + finally LEmpty.Free; end; + end; + DoDisconnect := False; + Send(ntp_response,HeaderData.operation,0,HeaderData.request_id,responseStream); + end; begin { This call is used to obtain pending operations not included in blockchain @@ -3258,6 +3903,7 @@ procedure TNetConnection.DoProcess_GetPendingOperations_Request(HeaderData: TNet DoDisconnect := true; responseStream := TMemoryStream.Create; try + try if HeaderData.header_type<>ntp_request then begin errors := 'Not request'; exit; @@ -3265,6 +3911,8 @@ procedure TNetConnection.DoProcess_GetPendingOperations_Request(HeaderData: TNet DataBuffer.Read(b,1); if (b=1) then begin // Return count + if not TNode.Node.PendingOperationsAvailable then + raise EPendingOperationsUnavailable.Create('Pending operations are unavailable during blockchain synchronization'); c := TNode.Node.MempoolOperationsCount; responseStream.Write(c,SizeOf(c)); end else if (b=2) then begin @@ -3302,6 +3950,11 @@ procedure TNetConnection.DoProcess_GetPendingOperations_Request(HeaderData: TNet end; DoDisconnect:=False; Send(ntp_response,HeaderData.operation,0,HeaderData.request_id,responseStream); + except + on E: EPendingOperationsUnavailable do begin + SendUnavailableOperations; + end; + end; finally responseStream.Free; if DoDisconnect then begin @@ -3319,12 +3972,17 @@ procedure TNetConnection.DoProcess_GetPendingOperations; errors : String; i,Lopc,Lprc : Integer; begin + if not IsPendingStateReadyForPeer then begin + {$IFDEF FPC}InterlockedExchange(FPendingOperationsRequested,1); + {$ELSE}TInterlocked.Exchange(FPendingOperationsRequested,1);{$ENDIF} + Exit; + end; {$IFDEF PRODUCTION} If FNetProtocolVersion.protocol_available<=6 then Exit; // Note: GetPendingOperations started on protocol_available=7 {$ENDIF} request_id := 0; cAddedOperations := 0; - if Not Connected then exit; + if not CanRequestBlockDownload then Exit; // First receive operations from dataSend := TMemoryStream.Create; dataReceived := TMemoryStream.Create; @@ -3335,6 +3993,10 @@ procedure TNetConnection.DoProcess_GetPendingOperations; If Not DoSendAndWaitForResponse(CT_NetOp_GetPendingOperations,request_id,dataSend,dataReceived,20000,headerData) then begin Exit; end; + // Matching protocol errors are valid responses, not count/tree payloads. + // Their text is already logged by the header decoder; never blacklist the + // peer by interpreting an unavailable/error message as operation bytes. + if headerData.is_error then Exit; dataReceived.Position:=0; cTotalByOther := 0; If (dataReceived.Read(cTotalByOther,SizeOf(cTotal))0) then begin inc(cReceived,opht.OperationsCount); i := TNode.Node.AddOperations(Self,opht,Nil,errors); + if i<0 then begin + // A new block can start after the request was sent. Retain a + // later exchange instead of adding a negative count to Cardinal. + {$IFDEF FPC}InterlockedExchange(FPendingOperationsRequested,1); + {$ELSE}TInterlocked.Exchange(FPendingOperationsRequested,1);{$ENDIF} + Exit; + end; inc(cAddedOperations,i); end else Break; // No more inc(cStart,opht.OperationsCount); @@ -3422,6 +4092,9 @@ procedure TNetConnection.DoProcess_GetPubkeyAccounts_Request(HeaderData: TNetHea responseStream := TMemoryStream.Create; accountsStream := TMemoryStream.Create; try + try + if not TNode.Node.PendingOperationsAvailable then + raise EPendingOperationsUnavailable.Create('Pending operations are unavailable during blockchain synchronization'); // Response first 4 bytes are current block number c := TNode.Node.Bank.BlocksCount-1; responseStream.Write(c,SizeOf(c)); @@ -3473,6 +4146,14 @@ procedure TNetConnection.DoProcess_GetPubkeyAccounts_Request(HeaderData: TNetHea DoDisconnect := False; SendError(ntp_response,HeaderData.operation,HeaderData.request_id,CT_NetError_NotAvailable,'No OrderedAccountKeysList available'); end; + except + on E: EPendingOperationsUnavailable do begin + // Busy pending state is temporary, not an invalid peer request. + DoDisconnect := False; + SendError(ntp_response,HeaderData.operation,HeaderData.request_id, + CT_NetError_NotAvailable,E.Message); + end; + end; finally responseStream.Free; accountsStream.Free; @@ -3518,6 +4199,9 @@ procedure TNetConnection.DoProcess_GetAccount_Request(HeaderData: TNetHeaderData DoDisconnect := true; responseStream := TMemoryStream.Create; try + try + if not TNode.Node.PendingOperationsAvailable then + raise EPendingOperationsUnavailable.Create('Pending operations are unavailable during blockchain synchronization'); // Response first 4 bytes are current block number c := TNode.Node.Bank.BlocksCount-1; responseStream.Write(c,SizeOf(c)); @@ -3586,6 +4270,14 @@ procedure TNetConnection.DoProcess_GetAccount_Request(HeaderData: TNetHeaderData end; DoDisconnect:=False; Send(ntp_response,HeaderData.operation,0,HeaderData.request_id,responseStream); + except + on E: EPendingOperationsUnavailable do begin + // Busy pending state is temporary, not an invalid peer request. + DoDisconnect := False; + SendError(ntp_response,HeaderData.operation,HeaderData.request_id, + CT_NetError_NotAvailable,E.Message); + end; + end; finally responseStream.Free; if DoDisconnect then begin @@ -3664,6 +4356,7 @@ procedure TNetConnection.DoProcess_Hello(HeaderData: TNetHeaderData; DataBuffer: if op.LoadBlockFromStream(DataBuffer,errors) then begin FRemoteOperationBlock := op.OperationBlock; + TNode.Node.UpdateSynchronizationPeerTarget(Self,FRemoteOperationBlock.block+1); if (DataBuffer.Size-DataBuffer.Position>=4) then begin DataBuffer.Read(c,4); for i := 1 to c do begin @@ -3695,9 +4388,7 @@ procedure TNetConnection.DoProcess_Hello(HeaderData: TNetHeaderData; DataBuffer: ((FRemoteAggregatedHashrate.CompareTo(TNode.Node.Bank.SafeBox.AggregatedHashrate)>0)) then begin TNetData.NetData.FMaxRemoteOperationBlock := FRemoteOperationBlock; - if TPCThread.ThreadClassFound(TThreadGetNewBlockChainFromClient,nil)<0 then begin - TThreadGetNewBlockChainFromClient.Create; - end; + TNetData.NetData.StartBlockChainWorker; end; end; @@ -3786,7 +4477,8 @@ procedure TNetConnection.DoProcess_Message(HeaderData: TNetHeaderData; DataBuffe end; end; -procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuffer: TStream); +procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuffer: TStream; + ABufferedOnly: Boolean); Type TNewFastPropagationBlockOperation = Record opReference : TOpReference; @@ -3831,27 +4523,23 @@ procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuf DoDisconnect := False; notFoundOpReferencesCount := 0; if (oprefcount>0) then begin - // Try TNode locking process - If Not TNode.Node.TryLockNode(3000) then Exit; // Cannot lock... + // Reference reconstruction uses operation bytes, not pending balances. + // It must remain available while a received catch-up overlay is deferred. + If Not TNode.Node.TryLockMempoolOperationsRead(3000, LLockedMempool) then Exit; Try if (operationsComp.OperationBlock.block<>TNode.Node.Bank.BlocksCount) then Exit; // Meanwhile other threads have added it // Fill not included operations: - LLockedMempool := TNode.Node.LockMempoolRead; - try - for i:=0 to High(nfpboarr) do begin - iNodeOpReference := LLockedMempool.OperationsHashTree.IndexOfOpReference(nfpboarr[i].opReference); - if iNodeOpReference>=0 then begin - nfpboarr[i].opStreamData := LLockedMempool.OperationsHashTree.GetOperation(iNodeOpReference).GetOperationStreamData; - nfpboarr[i].opStreamDataUsingV5EncodeStyle:=True; // Flag to indicate that opStreamData is saved in V5 format - end else begin - inc(notFoundOpReferencesCount); - end; + for i:=0 to High(nfpboarr) do begin + iNodeOpReference := LLockedMempool.OperationsHashTree.IndexOfOpReference(nfpboarr[i].opReference); + if iNodeOpReference>=0 then begin + nfpboarr[i].opStreamData := LLockedMempool.OperationsHashTree.GetOperation(iNodeOpReference).GetOperationStreamData; + nfpboarr[i].opStreamDataUsingV5EncodeStyle:=True; // Flag to indicate that opStreamData is saved in V5 format + end else begin + inc(notFoundOpReferencesCount); end; - finally - TNode.Node.UnlockMempoolRead; end; Finally - TNode.Node.UnlockNode; + TNode.Node.UnlockMempoolRead; End; end; if (notFoundOpReferencesCount>CT_MAX_OPS_PER_BLOCKCHAINOPERATIONS) then begin @@ -3859,6 +4547,7 @@ procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuf TLog.NewLog(ltdebug,ClassName,Format('Too many pending operations (%d of %d) in Fast propagation block %d',[notFoundOpReferencesCount,oprefcount,operationsComp.OperationBlock.block])); Exit; end else if (notFoundOpReferencesCount>0) then begin + if ABufferedOnly or (not CanRequestBlockDownload) then Exit; // Must obtain not found calling CT_NetOp_GetBlockchainOperations TLog.NewLog(ltdebug,ClassName,Format('Pending operations (%d of %d) in Fast propagation block %d',[notFoundOpReferencesCount,oprefcount,operationsComp.OperationBlock.block])); sendStream := TMemoryStream.Create; @@ -3948,6 +4637,8 @@ procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuf var c : Cardinal; LRawAggregatedHashrate : TRawBytes; begin + TNode.Node.BeginBlockChainUpdate; + try errors := ''; DoDisconnect := true; try @@ -3977,6 +4668,7 @@ procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuf TLog.NewLog(ltdebug,ClassName,'Received NEW BLOCK with height: '+inttostr(operationsComp.OperationBlock.block)+' Accumulated values '+IntToStr(FRemoteAccumulatedWork)+' '+FRemoteAggregatedHashrate.ToDecimal+' from '+ClientRemoteAddr); end; FRemoteOperationBlock := operationsComp.OperationBlock; + TNode.Node.UpdateSynchronizationPeerTarget(Self,FRemoteOperationBlock.block+1); if (FRemoteAccumulatedWork>TNode.Node.Bank.SafeBox.WorkSum) or (FRemoteAggregatedHashrate.CompareTo( TNode.Node.Bank.SafeBox.AggregatedHashrate ) > 0) @@ -3989,6 +4681,7 @@ procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuf If Not ProcessNewFastBlockPropagation then begin if DoDisconnect then Exit else begin + if ABufferedOnly then Exit; Send_GetBlocks(operationsComp.OperationBlock.block,1,c); Exit; end; @@ -4003,12 +4696,14 @@ procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuf // Really is a new block? (Check it) if (operationsComp.OperationBlock.block=TNode.Node.Bank.BlocksCount) then begin // Received a new invalid block... perhaps I'm an orphan blockchain - TNetData.NetData.GetNewBlockChainFromClient(Self,'Higher Work with same block height. I''m a orphan blockchain candidate'); + if not ABufferedOnly then + TNetData.NetData.GetNewBlockChainFromClient(Self,'Higher Work with same block height. I''m a orphan blockchain candidate'); end; end; end else begin // Received a new higher work - TNetData.NetData.GetNewBlockChainFromClient(Self,Format('Higher Work and distinct blocks count. Need to download BlocksCount:%d my BlocksCount:%d',[operationsComp.OperationBlock.block+1,TNode.Node.Bank.BlocksCount])); + if not ABufferedOnly then + TNetData.NetData.GetNewBlockChainFromClient(Self,Format('Higher Work and distinct blocks count. Need to download BlocksCount:%d my BlocksCount:%d',[operationsComp.OperationBlock.block+1,TNode.Node.Bank.BlocksCount])); end; end; end; @@ -4020,6 +4715,9 @@ procedure TNetConnection.DoProcess_NewBlock(AHeaderData: TNetHeaderData; DataBuf DisconnectInvalidClient(false,errors+' > '+TNetData.HeaderDataToText(AHeaderData)+' BuffSize: '+inttostr(DataBuffer.Size)); end; end; + finally + TNode.Node.EndBlockChainUpdate; + end; end; procedure TNetConnection.DoSend(ANetTranferType: TNetTransferType; AOperation, AErrorcode: Word; ARequest_id: Integer; ADataBuffer: TStream); @@ -4034,7 +4732,11 @@ function TNetConnection.DoSendAndWaitForResponse(operation: Word; was_waiting_for_response : Boolean; iDebugStep : Integer; reservedResponse : TMemoryStream; + LPhysicalScope, LProcessingFailed: Boolean; begin + LPhysicalScope := False; + LProcessingFailed := False; + try iDebugStep := 0; Try Result := false; @@ -4046,11 +4748,13 @@ function TNetConnection.DoSendAndWaitForResponse(operation: Word; iDebugStep := 100; If Not Assigned(FTcpIpClient) then exit; if Not Client.Connected then exit; + if not CanRequestBlockDownload then Exit; iDebugStep := 110; tc := TPlatform.GetTickCount; - If TPCThread.TryProtectEnterCriticalSection(Self,MaxWaitTime,FNetLock) then begin + If TPCThread.TryProtectAcquireWriteLock(Self,MaxWaitTime,FNetLock) then begin Try iDebugStep := 120; + if not CanRequestBlockDownload then Exit; was_waiting_for_response := RequestId>0; try if was_waiting_for_response then begin @@ -4074,6 +4778,12 @@ function TNetConnection.DoSendAndWaitForResponse(operation: Word; Result := true; end else begin iDebugStep := 1000; + if ((HeaderData.operation=CT_NetOp_GetBlocks) and (HeaderData.header_type=ntp_response)) or + ((HeaderData.operation in [CT_NetOp_NewBlock,CT_NetOp_NewBlock_Fast_Propagation]) and + (HeaderData.header_type=ntp_autosend)) then begin + TNode.Node.BeginBlockChainUpdate; + LPhysicalScope := True; + end; case HeaderData.operation of CT_NetOp_Hello : Begin if TNetData.NetData.IpInfos.ReachesLimits(Client.RemoteHost,CT_NetTransferType[HeaderData.header_type],TNetData.OperationToText(HeaderData.operation),HeaderData.buffer_data_length, @@ -4122,6 +4832,11 @@ function TNetConnection.DoSendAndWaitForResponse(operation: Word; if TNetData.NetData.IpInfos.ReachesLimits(Client.RemoteHost,CT_NetTransferType[HeaderData.header_type],TNetData.OperationToText(HeaderData.operation),HeaderData.buffer_data_length, TArray.Create(TLimitLifetime.Create(1200,100,0),TLimitLifetime.Create(10,40,0))) then DisconnectInvalidClient(False,Format('Reached limit %s',[TNetData.OperationToText(HeaderData.operation)])) else DoProcess_GetSafeBox_Request(HeaderData,ReceiveDataBuffer) + end else if HeaderData.header_type=ntp_response then begin + // A timed-out chunk can finish after another request has + // started. Discard that obsolete response without banning + // the peer; matching chunks still undergo full validation. + TLog.NewLog(ltDebug,Classname,'Received old response of: '+TNetData.HeaderDataToText(HeaderData)); end else DisconnectInvalidClient(false,'Received '+TNetData.HeaderDataToText(HeaderData)); end; CT_NetOp_GetPendingOperations : Begin @@ -4165,6 +4880,13 @@ function TNetConnection.DoSendAndWaitForResponse(operation: Word; else DisconnectInvalidClient(false,'Invalid operation: '+TNetData.HeaderDataToText(HeaderData)); end; + // Keep the outer physical scope across an OFF/EOF boundary, + // through the final retained frames below. In the ordinary + // connected path it must not cover the next socket wait. + if LPhysicalScope and (not MustDrainReceivedBlocks) then begin + TNode.Node.EndBlockChainUpdate; + LPhysicalScope := False; + end; end; end else sleep(1); iDebugStep := 900; @@ -4174,15 +4896,25 @@ function TNetConnection.DoSendAndWaitForResponse(operation: Word; end; iDebugStep := 990; Finally - FNetLock.Release; + FNetLock.ReleaseWriteLock; End; end; Except On E:Exception do begin + LProcessingFailed := True; E.Message := E.Message+' DoSendAndWaitForResponse step '+Inttostr(iDebugStep)+' Header.operation:'+Inttostr(HeaderData.operation); Raise; end; End; + finally + if LPhysicalScope then begin + try + // This call's FNetLock level has ended. An outer legacy/recursive + // level can remain; see the fixed callback dependency in the drain. + if (not LProcessingFailed) and MustDrainReceivedBlocks then DrainReceivedBlockFrames; + finally TNode.Node.EndBlockChainUpdate; end; + end; + end; end; procedure TNetConnection.FinalizeConnection; @@ -4206,6 +4938,16 @@ function TNetConnection.GetConnected: Boolean; Result := Assigned(FTcpIpClient) And (FTcpIpClient.Connected); end; +function TNetConnection.IsPendingStateReadyForPeer: Boolean; +var LStatus: TNodeSynchronizationStatus; +begin + // Use cached node facts: this check must not take a safebox/mempool lock on + // the I/O thread. Pending exchange resumes once our tip includes the peer's. + LStatus := TNode.Node.GetSynchronizationStatus; + Result := LStatus.PendingOperationsAvailable and + (LStatus.LocalBlockCount > FRemoteOperationBlock.block); +end; + procedure TNetConnection.Notification(AComponent: TComponent; Operation: TOperation); begin inherited; @@ -4229,7 +4971,7 @@ function TNetConnection.ReadTcpClientBuffer(MaxWaitMiliseconds: Cardinal; var He Result := false; HeaderData := CT_NetHeaderData; BufferData.Size := 0; - TPCThread.ProtectEnterCriticalSection(Self,FNetLock); + TPCThread.ProtectAcquireWriteLock(Self,FNetLock); try tc := TPlatform.GetTickCount; repeat @@ -4314,7 +5056,7 @@ function TNetConnection.ReadTcpClientBuffer(MaxWaitMiliseconds: Cardinal; var He end; end; Finally - FNetLock.Release; + FNetLock.ReleaseWriteLock; End; end; if t_bytes_read>0 then begin @@ -4392,7 +5134,7 @@ procedure TNetConnection.Send(NetTranferType: TNetTransferType; operation, error {$IFDEF HIGHLOG}s := '';{$ENDIF} end; Buffer.Position := 0; - TPCThread.ProtectEnterCriticalSection(Self,FNetLock); + TPCThread.ProtectAcquireWriteLock(Self,FNetLock); Try {$IFDEF HIGHLOG}TLog.NewLog(ltDebug,Classname,'Sending: '+CT_NetTransferType[NetTranferType]+' operation:'+ TNetData.OperationToText(operation)+' id:'+Inttostr(request_id)+' errorcode:'+InttoStr(errorcode)+ @@ -4401,7 +5143,7 @@ procedure TNetConnection.Send(NetTranferType: TNetTransferType; operation, error (Client as TBufferedNetTcpIpClient).WriteBufferToSend(Buffer); FLastDataSendedTS := TPlatform.GetTickCount; Finally - FNetLock.Release; + FNetLock.ReleaseWriteLock; End; TNetData.NetData.IncStatistics(0,0,0,0,0,Buffer.Size); finally @@ -4430,10 +5172,10 @@ function TNetConnection.Send_AddOperations(Operations : TOperationsHashTree) : B begin Result := false; if Not Connected then exit; - FNetLock.Acquire; + FNetLock.AcquireWriteLock; try nOpsToSend := 0; - FBufferLock.Acquire; + FBufferLock.AcquireWriteLock; Try If Assigned(Operations) then begin for i := 0 to Operations.OperationsCount - 1 do begin @@ -4471,10 +5213,10 @@ function TNetConnection.Send_AddOperations(Operations : TOperationsHashTree) : B end; end{$IFDEF HIGHLOG} else TLog.NewLog(ltdebug,ClassName,Format('Not sending any operations to %s (inProc:%d, Received:%d, Sent:%d)',[ClientRemoteAddr,nOpsToSend,FBufferReceivedOperationsHash.Count,FBufferToSendOperations.OperationsCount])){$ENDIF}; finally - FBufferLock.Release; + FBufferLock.ReleaseWriteLock; end; finally - FNetLock.Release; + FNetLock.ReleaseWriteLock; end; Result := Connected; end; @@ -4486,8 +5228,9 @@ function TNetConnection.Send_GetBlocks(StartAddress, quantity : Cardinal; var re Result := false; request_id := 0; if (FRemoteOperationBlock.block0 then Break; + {$ELSE} + if TInterlocked.CompareExchange(FStopAfterCurrentBuffer,0,0)<>0 then Break; + {$ENDIF} If FNetClient.Connected then begin FNetClient.DoProcessBuffer; end; Sleep(1); end; + finally + FNetClient.DrainReceivedBlockFrames; + end; +end; + +procedure TNetClientThread.RequestStopAfterCurrentBuffer; +begin + {$IFDEF FPC} + InterlockedExchange(FStopAfterCurrentBuffer,1); + {$ELSE} + TInterlocked.Exchange(FStopAfterCurrentBuffer,1); + {$ENDIF} end; constructor TNetClientThread.Create(NetClient: TNetClient; AOnTerminateThread : TNotifyEvent); @@ -4766,11 +5540,12 @@ constructor TNetClient.Create(AOwner: TComponent); destructor TNetClient.Destroy; begin {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,Classname,'Starting TNetClient.Destroy');{$ENDIF} + RetireDownloadUsage; FNetClientThread.OnTerminate := Nil; - if Not FNetClientThread.Terminated then begin - FNetClientThread.Terminate; - FNetClientThread.WaitFor; - end; + // A disconnected socket can own a fully received multi-block response. Join + // its ordinary completion; explicit application shutdown has its own latch. + FNetClientThread.RequestStopAfterCurrentBuffer; + FNetClientThread.WaitFor; FreeAndNil(FNetClientThread); inherited; end; @@ -4790,54 +5565,72 @@ procedure TThreadDiscoverConnection.BCExecute; ok : Boolean; ns : TNodeServerAddress; begin - Repeat // Face to face conflict when 2 nodes connecting together - Sleep(Random(1000)); - until (Terminated) Or (Random(5)=0); - if Terminated then exit; - {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,Classname,'Starting discovery of connection '+FNodeServerAddress.ip+':'+InttoStr(FNodeServerAddress.port));{$ENDIF} - DebugStep := 'Locking list'; - // Register attempt - If TNetData.NetData.NodeServersAddresses.GetNodeServerAddress(FNodeServerAddress.ip,FNodeServerAddress.port,true,ns) then begin - ns.last_attempt_to_connect := Now; - inc(ns.total_failed_attemps_to_connect); - TNetData.NetData.NodeServersAddresses.SetNodeServerAddress(ns); - end; - DebugStep := 'Synchronizing notify'; - if Terminated then exit; - TNetData.NetData.NotifyNodeServersUpdated; - // Try to connect + NC := nil; ok := false; - DebugStep := 'Trying to connect'; - if Terminated then exit; - NC := TNetClient.Create(Nil); - Try + try + if StopRequested then Exit; + Repeat // Face to face conflict when 2 nodes connecting together + Sleep(Random(1000)); + until StopRequested Or (Random(5)=0); + if StopRequested then exit; + {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,Classname,'Starting discovery of connection '+FNodeServerAddress.ip+':'+InttoStr(FNodeServerAddress.port));{$ENDIF} + DebugStep := 'Locking list'; + // This worker retains its original owner; shutdown must not resurrect the + // singleton through a late discovery callback or connection attempt. + If FNetData.NodeServersAddresses.GetNodeServerAddress(FNodeServerAddress.ip,FNodeServerAddress.port,true,ns) then begin + ns.last_attempt_to_connect := Now; + inc(ns.total_failed_attemps_to_connect); + FNetData.NodeServersAddresses.SetNodeServerAddress(ns); + end; + DebugStep := 'Synchronizing notify'; + if StopRequested then exit; + FNetData.NotifyNodeServersUpdated; + DebugStep := 'Trying to connect'; + NC := FNetData.TryCreateDiscoveryClient(Self); + if not Assigned(NC) or StopRequested then Exit; DebugStep := 'Connecting'; If NC.ConnectTo(FNodeServerAddress.ip,FNodeServerAddress.port) then begin - if Terminated then exit; + if StopRequested then exit; Sleep(500); DebugStep := 'Is connected now?'; - if Terminated then exit; + if StopRequested then exit; ok :=NC.Connected; end; - if Terminated then exit; - Finally - if (not ok) And (Not Terminated) then begin - DebugStep := 'Destroying non connected'; - NC.FinalizeConnection; + DebugStep := 'Synchronizing notify final'; + if not StopRequested then FNetData.NotifyNodeServersUpdated; + finally + try + if Assigned(NC) then begin + try + // Cancellation used to skip finalization and orphan a real client + // plus its buffer thread. Every unsuccessful/stopped attempt retires. + if (not ok) or StopRequested then NC.FinalizeConnection; + finally + FNetData.ReleaseDownloadConnection(NC); + end; + end; + finally + FNetData.DiscoverServersTerminated(Self); end; - End; - DebugStep := 'Synchronizing notify final'; - if Terminated then exit; - TNetData.NetData.NotifyNodeServersUpdated; + end; end; -constructor TThreadDiscoverConnection.Create(NodeServerAddress: TNodeServerAddress; NotifyOnTerminate : TNotifyEvent); +constructor TThreadDiscoverConnection.Create(ANetData: TNetData; const NodeServerAddress: TNodeServerAddress); begin + FNetData := ANetData; FNodeServerAddress := NodeServerAddress; inherited Create(true); - OnTerminate := NotifyOnTerminate; - FreeOnTerminate := true; - Suspended := false; + FreeOnTerminate := false; +end; + +procedure TThreadDiscoverConnection.StartOwned; +begin + Start; +end; + +function TThreadDiscoverConnection.StopRequested: Boolean; +begin + Result := Terminated or FNetData.ShutdownRequested or not FNetData.NetConnectionsActive; end; { TThreadCheckConnections } @@ -4910,6 +5703,14 @@ procedure TThreadCheckConnections.BCExecute; end; end; end; + if not Terminated then begin + try + FNetData.StartBlockChainWorker(True); + except + on E: Exception do + TLog.NewLog(ltError,Classname,'Cannot resume blockchain download: '+E.Message); + end; + end; sleep(100); end; end; @@ -4923,6 +5724,47 @@ constructor TThreadCheckConnections.Create(NetData: TNetData); { TThreadGetNewBlockChainFromClient } procedure TThreadGetNewBlockChainFromClient.BCExecute; +var LConnections: TList; LSelected: TNetConnection; I: Integer; +begin + LConnections := nil; + try + LConnections := TList.Create; + if Terminated or FNetData.ShutdownRequested or not TNode.Node.UpdateBlockchain then Exit; + FNetData.AcquireDownloadConnections(LConnections); + LSelected := SelectConnection(LConnections); + if Assigned(LSelected) and (LConnections.IndexOf(LSelected) < 0) then + raise Exception.Create('Blockchain candidate is not in its leased snapshot'); + // Only the selected connection is retained across download/network waits. + // An unrelated disconnected peer can retire immediately. + for I := LConnections.Count - 1 downto 0 do + if LConnections[I] <> LSelected then begin + FNetData.ReleaseDownloadConnection(LConnections[I]); + LConnections.Delete(I); + end; + if Assigned(LSelected) and not Terminated and not FNetData.ShutdownRequested then + FNetData.GetNewBlockChainFromClient(LSelected,Format( + 'Candidate block: %d Aggregated: %d %s (My %d %s)', + [LSelected.FRemoteOperationBlock.block,LSelected.FRemoteAccumulatedWork, + LSelected.FRemoteAggregatedHashrate.ToDecimal,TNode.Node.Bank.SafeBox.WorkSum, + TNode.Node.Bank.SafeBox.AggregatedHashrate.ToDecimal])); + finally + try + if Assigned(LConnections) then + for I := LConnections.Count - 1 downto 0 do + FNetData.ReleaseDownloadConnection(LConnections[I]); + finally + LConnections.Free; + {$IFDEF FPC} + InterlockedExchange(FExecutionFinished,1); + {$ELSE} + TInterlocked.Exchange(FExecutionFinished,1); + {$ENDIF} + end; + end; +end; + +function TThreadGetNewBlockChainFromClient.SelectConnection( + const AConnections: TList): TNetConnection; Var i,j : Integer; LMaxAggregatedTarget : UInt64; candidates : TList; @@ -4930,20 +5772,22 @@ procedure TThreadGetNewBlockChainFromClient.BCExecute; nc : TNetConnection; LMaxAggregatedHashrate : TBigNum; begin - if Not TNode.Node.UpdateBlockchain then Exit; + Result := nil; + if Terminated then Exit; // Search better candidates: candidates := TList.Create; LMaxAggregatedHashrate := TBigNum.Create(0); try lop := CT_OperationBlock_NUL; - TNetData.NetData.FMaxRemoteOperationBlock := CT_OperationBlock_NUL; + FNetData.FMaxRemoteOperationBlock := CT_OperationBlock_NUL; // First round: Find by most work LMaxAggregatedTarget := 0; - j := TNetData.NetData.ConnectionsCountAll; + j := AConnections.Count; nc := Nil; for i := 0 to j - 1 do begin - if TNetData.NetData.GetConnection(i,nc) then begin + if not Terminated then begin + nc := AConnections[i]; // Preventing downloading if nc.FIsDownloadingBlocks then exit; // @@ -4958,7 +5802,8 @@ procedure TThreadGetNewBlockChainFromClient.BCExecute; if (LMaxAggregatedHashrate.CompareTo( 1 ) > 0) then begin // Search by Aggregated Hashrate for i := 0 to j - 1 do begin - If TNetData.NetData.GetConnection(i,nc) then begin + If not Terminated then begin + nc := AConnections[i]; if (nc.FRemoteAggregatedHashrate.CompareTo(LMaxAggregatedHashrate)>=0) then begin candidates.Add(nc); lop := nc.FRemoteOperationBlock; @@ -4968,7 +5813,8 @@ procedure TThreadGetNewBlockChainFromClient.BCExecute; end else if (LMaxAggregatedTarget>0) then begin // Not found searching by Aggregated Hashrate, searching by Aggregated Target for i := 0 to j - 1 do begin - If TNetData.NetData.GetConnection(i,nc) then begin + If not Terminated then begin + nc := AConnections[i]; if (nc.FRemoteAccumulatedWork>=LMaxAggregatedTarget) then begin candidates.Add(nc); lop := nc.FRemoteOperationBlock; @@ -4976,14 +5822,12 @@ procedure TThreadGetNewBlockChainFromClient.BCExecute; end; end; end; - TNetData.NetData.FMaxRemoteOperationBlock := lop; + FNetData.FMaxRemoteOperationBlock := lop; if (candidates.Count>0) then begin // Random a candidate i := 0; if (candidates.Count>1) then i := Random(candidates.Count); // i = 0..count-1 - nc := TNetConnection(candidates[i]); - TNetData.NetData.GetNewBlockChainFromClient(nc,Format('Candidate block: %d Aggregated: %d %s (My %d %s)',[nc.FRemoteOperationBlock.block,nc.FRemoteAccumulatedWork,nc.FRemoteAggregatedHashrate.ToDecimal, - TNode.Node.Bank.SafeBox.WorkSum,TNode.Node.Bank.SafeBox.AggregatedHashrate.ToDecimal])); + Result := TNetConnection(candidates[i]); end; finally LMaxAggregatedHashrate.Free; @@ -4991,11 +5835,20 @@ procedure TThreadGetNewBlockChainFromClient.BCExecute; end; end; -constructor TThreadGetNewBlockChainFromClient.Create; +constructor TThreadGetNewBlockChainFromClient.Create(ANetData: TNetData); begin + FNetData := ANetData; Inherited Create(True); - FreeOnTerminate := true; - Suspended := false; + FreeOnTerminate := false; +end; + +function TThreadGetNewBlockChainFromClient.GetExecutionFinished: Boolean; +begin + {$IFDEF FPC} + Result := InterlockedCompareExchange(FExecutionFinished,0,0) <> 0; + {$ELSE} + Result := TInterlocked.CompareExchange(FExecutionFinished,0,0) <> 0; + {$ENDIF} end; { TNetDataNotifyEventsThread } @@ -5058,6 +5911,7 @@ procedure TNetClientsDestroyThread.BCExecute; Var l,l_to_del : TList; i : Integer; LNetConnection : TNetConnection; + LEmpty: Boolean; begin l_to_del := TList.Create; Try @@ -5065,17 +5919,24 @@ procedure TNetClientsDestroyThread.BCExecute; l_to_del.Clear; l := FNetData.NetConnections.LockList; try - FTerminatedAllConnections := l.Count=0; + LEmpty := l.Count=0; for i := 0 to l.Count-1 do begin If (TObject(l[i]) is TNetClient) And (not TNetConnection(l[i]).Connected) And (TNetConnection(l[i]).FDoFinalizeConnection) - And (Not TNetConnection(l[i]).IsConnecting) then begin + And (Not TNetConnection(l[i]).IsConnecting) + And (TNetConnection(l[i]).FDownloadLeaseCount = 0) then begin + // Do not stall this one destroy worker on a selected peer's lease: + // unrelated disconnected peers must be free to retire meanwhile. + TNetConnection(l[i]).FDownloadRetiring := true; l_to_del.Add(l[i]); end; end; finally FNetData.NetConnections.UnlockList; end; + // Observe only a completed scan, outside the registry lock. Production + // ownership and selection stay here; a probe can pause this same worker. + RegistryScanCompleted(LEmpty); sleep(500); // Delay - Sleep time before destroying (1.5.3) if l_to_del.Count>0 then begin TLog.NewLog(ltDebug,ClassName,'Destroying NetClients: '+inttostr(l_to_del.Count)); @@ -5108,21 +5969,39 @@ procedure TNetClientsDestroyThread.BCExecute; constructor TNetClientsDestroyThread.Create(NetData: TNetData); begin FNetData:=NetData; - FTerminatedAllConnections := true; Inherited Create(false); end; +procedure TNetClientsDestroyThread.RegistryScanCompleted(AEmpty: Boolean); +begin +end; + procedure TNetClientsDestroyThread.WaitForTerminatedAllConnections; var LTC : TTickCount; + LConnections: TList; + LEmpty: Boolean; begin LTC := TPlatform.GetTickCount; - while (Not FTerminatedAllConnections) do begin + repeat + LConnections := FNetData.FNetConnections.LockList; + try + LEmpty := LConnections.Count = 0; + finally + FNetData.FNetConnections.UnlockList; + end; + // Only the current registry is authoritative. An earlier empty destroyer + // scan says nothing about a discovery client registered since that scan. + if LEmpty then Exit; + // A client may already be waiting for its synchronized OnTerminate while + // the destroy worker joins it. Service that callback on the main thread + // instead of leaving all three participants waiting for one another. + if TThread.CurrentThread.ThreadID = MainThreadID then CheckSynchronize; if TPlatform.GetElapsedMilliseconds(LTC)>1000 then begin LTC := TPlatform.GetTickCount; TLog.NewLog(ltdebug,ClassName,'Waiting all connections terminated'); end; Sleep(50); - end; + until false; end; { TNetworkAdjustedTime } diff --git a/src/core/UNode.pas b/src/core/UNode.pas index fb248a9d5..e936c353f 100644 --- a/src/core/UNode.pas +++ b/src/core/UNode.pas @@ -36,7 +36,7 @@ interface uses Classes, SysUtils, {$IFNDEF FPC}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, UPCDataTypes, UEncoding, - UBlockChain, UNetProtocol, UAccounts, UCrypto, UEPasa, UThread, SyncObjs, ULog, UBaseTypes, UPCOrderedLists; + UBlockChain, UNetProtocol, UAccounts, UCrypto, UEPasa, UThread, UConst, SyncObjs, ULog, UBaseTypes, UPCOrderedLists; {$I ./../config.inc} @@ -48,24 +48,79 @@ interface TNode = Class; + // Pending account deltas are deliberately unavailable while a received + // blockchain backlog is applied. Callers must retry after the typed status + // reports availability; an old overlay must never be used with a newer tip. + EPendingOperationsUnavailable = Class(Exception); + ENodeStorageUnavailable = Class(Exception); + + TNodeStorageFailureStage = (sfsUnknown, sfsSafeboxFlush, sfsCheckpoint, sfsBlockStream, sfsPendingOperations, sfsRestore); + TNodeStorageFailure = record + HasFailure: Boolean; + Stage: TNodeStorageFailureStage; + FileName, ErrorClass, ErrorMessage: String; + end; + + // A selected target survives gaps between batches. Already received work + // remains synchronization even after transport shutdown; local pages resume + // when that physical backlog drains, not merely when sockets disconnect. + TNodeSynchronizationState = (nssOffline, nssDisconnected, nssIdle, nssSynchronizing, nssStorageError); + TNodeSynchronizationStatus = record + State: TNodeSynchronizationState; + NetworkActive: Boolean; + PeerCount: Integer; + HasRemoteTarget: Boolean; + LocalBlockCount, TargetBlockCount: Cardinal; + // All physical work, including blocks already received before networking + // stopped. Connectivity never discards or conceals that processing backlog. + ActiveTransfers, PendingOperationsCount: Integer; + HasMempoolSummary: Boolean; + PendingOperationsAvailable: Boolean; + CompactTarget, LastBlockTimestamp: Cardinal; + ProtocolVersion: Word; + Progress: TProgressInfo; + StorageFailure: TNodeStorageFailure; + end; + TNodeSynchronizationNotify = procedure(Sender: TObject; + const AStatus: TNodeSynchronizationStatus) of object; + TSaveMempoolOperationsThread = Class(TPCThread) private FNode : TNode; - FPendingToSave : Boolean; + FPendingToSave : LongInt; + FWorkAvailable : TEvent; + function TakeSaveRequest: Boolean; + function HasSaveRequest: Boolean; protected procedure BCExecute; override; public + constructor Create(ANode : TNode); + destructor Destroy; override; procedure Touch; + procedure Wake; + procedure Stop; End; TNode = Class(TComponent) private FNodeLog : TLog; - FLockMempool : TPCCriticalSection; - FOperationSequenceLock : TPCCriticalSection; + FLockMempool : TReadWriteLock; + FOperationSequenceLock : TReadWriteLock; FNotifyList : TList; + FNotifyLock: TReadWriteLock; + FSynchronizationLock: TReadWriteLock; + FSynchronizationStatus: TNodeSynchronizationStatus; + FSynchronizationPeers: TDictionary; + FSynchronizationGeneration: UInt64; + FSynchronizationScopes: TDictionary>; FBank : TPCBank; FMemPoolOperationsComp : TPCOperationsComp; + // Mutated under node WRITE plus the short status lock. Status readers may + // inspect it under that short lock without touching the pending overlay. + FMempoolNeedsReconciliation : Boolean; + // Announced blocks defer their old bounded pending resend until the final + // coherent overlay. GETBLOCKS imports never request a rebroadcast. + FResendPendingOperations : Boolean; FMemPoolAddingOperationsList : TOrderedRawList; FNetServer : TNetServer; FBCBankNotify : TPCBankNotify; @@ -81,7 +136,20 @@ interface Procedure OnBankNewBlock(Sender : TObject); procedure SetNodeLogFilename(const Value: String); function GetNodeLogFilename: String; + procedure RecalculateSynchronizationStatus; + procedure RetireSynchronizationSession; + function IsCurrentSynchronizationScope: Boolean; + procedure NotifySynchronizationChanged; + procedure CaptureSynchronizationMempoolSummary; + procedure ReconcileMempoolIfReady; + procedure WakeMempoolMaintenance; + procedure RequireMempoolAvailable; + function LockMempoolOperationsWrite : TPCOperationsComp; + procedure CollectPendingResends(AOperations : TOperationsHashTree); + procedure BroadcastPendingResends(AOperations : TOperationsHashTree); protected + procedure DoFlushSafebox; virtual; + function DoFlushBlockStorage: Boolean; virtual; procedure Notification(AComponent: TComponent; Operation: TOperation); override; public Class Function Node : TNode; @@ -98,18 +166,56 @@ interface function MempoolOperationsCount : Integer; // Return Account based on current state (Safebox + Mempool operations) function GetMempoolAccount(AAccountNumber : Cardinal) : TAccount; - // Locking methods to access to the Mempool + // Readers may overlap; all changes to shared pending state require write ownership. function LockMempoolRead : TPCOperationsComp; + function TryLockMempoolRead(MaxWaitMilliseconds : Cardinal; out AMempool : TPCOperationsComp) : Boolean; + // Core transport reconstruction may read immutable operation/index data + // while the account overlay is unavailable. Never use SafeBoxTransaction + // through this lease. Release it with the ordinary UnlockMempoolRead. + function TryLockMempoolOperationsRead(MaxWaitMilliseconds : Cardinal; + out AMempool : TPCOperationsComp) : Boolean; procedure UnlockMempoolRead; + function PendingOperationsAvailable : Boolean; + // Reserve the sole potential writer before inspecting a conditional mutation. + // Enter/exit write separately inside this scope; never promote an ordinary read. + function LockMempoolUpgradeableRead : TPCOperationsComp; + procedure UnlockMempoolUpgradeableRead; function LockMempoolWrite : TPCOperationsComp; procedure UnlockMempoolWrite; // Function AddNewBlockChain(SenderConnection : TNetConnection; NewBlockOperations: TPCOperationsComp; var errors: String): Boolean; - Function AddOperations(SenderConnection : TNetConnection; AOperationsHashTreeToAdd : TOperationsHashTree; OperationsResult : TOperationsResumeList; var errors: String): Integer; + // Both announced blocks and catch-up batches use the existing block audit + // format. The caller retains the header because bank acceptance clears it. + procedure LogBlockChainResult(SenderConnection : TNetConnection; + const AOperationBlock : TOperationBlock; AAccepted : Boolean; const AErrors : String); + procedure LogBlockChainBatchReceived(SenderConnection : TNetConnection; + ACount, AFirstBlock, ALastBlock : Cardinal; APayloadBytes : Int64); + Function AddOperations(SenderConnection : TNetConnection; AOperationsHashTreeToAdd : TOperationsHashTree; OperationsResult : TOperationsResumeList; var errors: String; AProgressNotify : TProgressNotify = Nil): Integer; Function AddOperation(SenderConnection : TNetConnection; Operation : TPCOperation; var errors: String): Boolean; Function SendNodeMessage(Target : TNetConnection; const TheMessage : String; var errors : String) : Boolean; // Procedure NotifyBlocksChanged; + // Balance these calls on the same thread in LIFO try/finally scopes around + // decoding, validation and application. Targets survive inter-batch gaps. + procedure BeginBlockChainUpdate; + procedure EndBlockChainUpdate; + function IsUpdatingBlockChain: Boolean; + // A retired network session cannot request further data. Its already + // received blocks still finish and remain visible as synchronization work. + function CanRequestBlockChainData: Boolean; + // These setters publish facts only; listeners run on the existing notifier + // thread/main-thread bridge, never on the network producer's call stack. + function GetSynchronizationStatus: TNodeSynchronizationStatus; + procedure ReportStorageFailure(AStage: TNodeStorageFailureStage; const AFileName: String; AError: Exception); + function HasStorageFailure: Boolean; + // Persist already accepted work; failures pause admission until restart. + procedure FlushSafebox; + procedure FlushBlockStorage; + procedure SetSynchronizationNetworkActive(AActive: Boolean); + procedure SetSynchronizationPeerConnected(APeer: TObject; AConnected: Boolean); + procedure SetSynchronizationTarget(APeer: TObject; ATargetBlockCount: Cardinal); + procedure UpdateSynchronizationPeerTarget(APeer: TObject; ATargetBlockCount: Cardinal); + procedure SetSynchronizationProgress(const AProgress: TProgressInfo); // Function FindOperation(Const AOperationHash : TRawBytes; var AOperationResume : TOperationResume) : TSearchOpHashResult; Function FindNOperation(block, account, n_operation : Cardinal; var OpResume : TOperationResume) : TSearchOpHashResult; @@ -123,7 +229,7 @@ interface Procedure DisableNewBlocks; Procedure EnableNewBlocks; Property NodeLogFilename : String read GetNodeLogFilename write SetNodeLogFilename; - Property OperationSequenceLock : TPCCriticalSection read FOperationSequenceLock; + Property OperationSequenceLock : TReadWriteLock read FOperationSequenceLock; function TryLockNode(MaxWaitMilliseconds : Cardinal) : Boolean; procedure UnlockNode; // @@ -151,6 +257,7 @@ interface FNodeNotifyEvents : TNodeNotifyEvents; FNotifyBlocksChanged : Boolean; FNotifyOperationsChanged : Boolean; + FNotifySynchronizationChanged: LongInt; Procedure SynchronizedProcess; protected procedure BCExecute; override; @@ -183,12 +290,14 @@ TNodeMessageManyEventHelper = record helper for TNodeMessageManyEvent FThreadSafeNodeNotifyEvent : TThreadSafeNodeNotifyEvent; FOnBlocksChanged: TNotifyEvent; FOnOperationsChanged: TNotifyEvent; + FOnSynchronizationChanged: TNodeSynchronizationNotify; FMessages : TStringList; FOnNodeMessageEvent: TNodeMessageEvent; FWatchKeys: TOrderedAccountKeysList; procedure SetNode(const Value: TNode); Procedure NotifyBlocksChanged; Procedure NotifyOperationsChanged; + procedure NotifySynchronizationChanged; procedure SetWatchKeys(AValue: TOrderedAccountKeysList); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; @@ -198,6 +307,8 @@ TNodeMessageManyEventHelper = record helper for TNodeMessageManyEvent Property Node : TNode read FNode write SetNode; Property OnBlocksChanged : TNotifyEvent read FOnBlocksChanged write FOnBlocksChanged; Property OnOperationsChanged : TNotifyEvent read FOnOperationsChanged write FOnOperationsChanged; + property OnSynchronizationChanged: TNodeSynchronizationNotify + read FOnSynchronizationChanged write FOnSynchronizationChanged; Property OnNodeMessageEvent : TNodeMessageEvent read FOnNodeMessageEvent write FOnNodeMessageEvent; Property WatchKeys : TOrderedAccountKeysList read FWatchKeys write SetWatchKeys; Property OnKeyActivity : TNotifyEvent read FOnKeyActivity write FOnKeyActivity; @@ -216,17 +327,20 @@ TNodeMessageManyEventHelper = record helper for TNodeMessageManyEvent TThreadNodeNotifyOperations = Class(TPCThread) FNetConnection : TNetConnection; + FLeaseOwner : TNetData; protected procedure BCExecute; override; public - Constructor Create(NetConnection : TNetConnection; MakeACopyOfOperationsHashTree : TOperationsHashTree); + Constructor Create(NetConnection : TNetConnection; MakeACopyOfOperationsHashTree : TOperationsHashTree; + ALeaseOwner : TNetData = Nil); destructor Destroy; override; End; implementation -Uses UOpTransaction, UConst, UTime, UCommon, UPCOperationsSignatureValidator, - UFolderHelper, USettings; +Uses UOpTransaction, UTime, UCommon, UPCOperationsSignatureValidator, + UFolderHelper, USettings, UFileMem, UAbstractMem, + {$IFDEF USE_ABSTRACTMEM}UAbstractMemBlockchainStorage,{$ENDIF} UPCAbstractMem; var _Node : TNode; _PascalCoinDataFolder : String; @@ -234,17 +348,18 @@ implementation { TNode } function TNode.AddNewBlockChain(SenderConnection: TNetConnection; NewBlockOperations: TPCOperationsComp; var errors: String): Boolean; -Var i,j,maxResend : Integer; +Var i,j : Integer; nc : TNetConnection; s,sClientRemoteAddr : String; OpBlock : TOperationBlock; opsht : TOperationsHashTree; - minBlockResend : Cardinal; - resendOp : TPCOperation; - LLockedMempool : TPCOperationsComp; begin Result := false; errors := ''; + if HasStorageFailure then begin + errors := GetSynchronizationStatus.StorageFailure.ErrorMessage; + Exit; + end; if Assigned(SenderConnection) then sClientRemoteAddr := SenderConnection.ClientRemoteAddr else sClientRemoteAddr:='(SELF)'; if FDisabledsNewBlocksCount>0 then begin @@ -253,6 +368,8 @@ function TNode.AddNewBlockChain(SenderConnection: TNetConnection; NewBlockOperat errors := 'Adding blocks is disabled'; exit; end; + BeginBlockChainUpdate; + try NewBlockOperations.Lock; // New protection Try If NewBlockOperations.OperationBlock.block<>Bank.BlocksCount then begin @@ -262,7 +379,7 @@ function TNode.AddNewBlockChain(SenderConnection: TNetConnection; NewBlockOperat OpBlock := NewBlockOperations.OperationBlock; TLog.NewLog(ltdebug,Classname,Format('Starting AddNewBlockChain %d Operations %d from %s NewBlock:%s',[ OpBlock.block,NewBlockOperations.Count,sClientRemoteAddr,TPCOperationsComp.OperationBlockToText(OpBlock)])); - If Not TPCThread.TryProtectEnterCriticalSection(Self,5000,FLockMempool) then begin + If Not TryLockNode(5000) then begin If NewBlockOperations.OperationBlock.block<>Bank.BlocksCount then exit; s := 'Cannot AddNewBlockChain due blocking lock operations node'; TLog.NewLog(lterror,Classname,s); @@ -281,62 +398,18 @@ function TNode.AddNewBlockChain(SenderConnection: TNetConnection; NewBlockOperat if Result then begin {$IFDEF USE_ABSTRACTMEM} If Not FBank.IsRestoringFromFile then begin - Bank.SafeBox.PCAbstractMem.FlushCache; + FlushSafebox; end; {$ENDIF} - if Assigned(SenderConnection) then begin - FNodeLog.NotifyNewLog(ltupdate,SenderConnection.ClassName,Format(';%d;%s;%s;;%d;%d;%d;%s',[OpBlock.block,sClientRemoteAddr,OpBlock.block_payload.ToPrintable, - OpBlock.timestamp,UnivDateTimeToUnix(DateTime2UnivDateTime(Now)),UnivDateTimeToUnix(DateTime2UnivDateTime(Now)) - OpBlock.timestamp,IntToHex(OpBlock.compact_target,8)])); - end else begin - FNodeLog.NotifyNewLog(ltupdate,ClassName,Format(';%d;%s;%s;;%d;%d;%d;%s',[OpBlock.block,'NIL',OpBlock.block_payload.ToPrintable, - OpBlock.timestamp,UnivDateTimeToUnix(DateTime2UnivDateTime(Now)),UnivDateTimeToUnix(DateTime2UnivDateTime(Now)) - OpBlock.timestamp,IntToHex(OpBlock.compact_target,8)])); - end; - end else begin - if Assigned(SenderConnection) then begin - FNodeLog.NotifyNewLog(lterror,SenderConnection.ClassName,Format(';%d;%s;%s;%s;%d;%d;%d;%s',[OpBlock.block,sClientRemoteAddr,OpBlock.block_payload.ToPrintable,errors, - OpBlock.timestamp,UnivDateTimeToUnix(DateTime2UnivDateTime(Now)),UnivDateTimeToUnix(DateTime2UnivDateTime(Now)) - OpBlock.timestamp,IntToHex(OpBlock.compact_target,8)])); - end else begin - FNodeLog.NotifyNewLog(lterror,ClassName,Format(';%d;%s;%s;%s;%d;%d;%d;%s',[OpBlock.block,'NIL',OpBlock.block_payload.ToPrintable,errors, - OpBlock.timestamp,UnivDateTimeToUnix(DateTime2UnivDateTime(Now)),UnivDateTimeToUnix(DateTime2UnivDateTime(Now)) - OpBlock.timestamp,IntToHex(OpBlock.compact_target,8)])); - end; end; + LogBlockChainResult(SenderConnection,OpBlock,Result,errors); if Result then begin opsht := TOperationsHashTree.Create; Try - j := Random(5); - If (Bank.LastBlockFound.OperationBlock.block>j) then - minBlockResend:=Bank.LastBlockFound.OperationBlock.block - j - else minBlockResend:=1; - maxResend := CT_MaxResendMemPoolOperations; - i := 0; - LLockedMempool := LockMempoolRead; - Try - - While (opsht.OperationsCount0 then begin - TLog.NewLog(ltinfo,classname,Format('Resending %d operations for new block (Mempool Pending Operations:%d)',[opsht.OperationsCount,LLockedMempool.Count])); - {$IFDEF HIGHLOG} - if opsht.OperationsCount>0 then begin - for i := 0 to opsht.OperationsCount - 1 do begin - TLog.NewLog(ltInfo,ClassName,'Resending ('+inttostr(i+1)+'/'+inttostr(opsht.OperationsCount)+'): '+opsht.GetOperation(i).ToString); - end; - end - {$ENDIF} - end; - Finally - UnlockMempoolRead; - End; + // Reconciliation will expose the accepted pending sequence again. + // Until then, announce only the fully validated confirmed block. + if PendingOperationsAvailable then CollectPendingResends(opsht) + else if FMemPoolOperationsComp.Count>0 then FResendPendingOperations := True; // Notify to clients {$IFnDEF TESTING_NO_POW_CHECK} if FBroadcastData then begin @@ -360,7 +433,7 @@ function TNode.AddNewBlockChain(SenderConnection: TNetConnection; NewBlockOperat End; end; finally - FLockMempool.Release; + UnlockNode; TLog.NewLog(ltdebug,Classname,Format('Finalizing AddNewBlockChain %d Operations %d from %s NewBlock:%s',[ OpBlock.block,NewBlockOperations.Count,sClientRemoteAddr,TPCOperationsComp.OperationBlockToText(OpBlock)])); End; @@ -371,6 +444,9 @@ function TNode.AddNewBlockChain(SenderConnection: TNetConnection; NewBlockOperat // Notify it! NotifyBlocksChanged; end; + finally + EndBlockChainUpdate; + end; end; function TNode.AddOperation(SenderConnection : TNetConnection; Operation: TPCOperation; var errors: String): Boolean; @@ -385,7 +461,7 @@ function TNode.AddOperation(SenderConnection : TNetConnection; Operation: TPCOpe End; end; -function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashTreeToAdd : TOperationsHashTree; OperationsResult : TOperationsResumeList; var errors: String): Integer; +function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashTreeToAdd : TOperationsHashTree; OperationsResult : TOperationsResumeList; var errors: String; AProgressNotify : TProgressNotify): Integer; {$IFDEF BufferOfFutureOperations} Procedure Process_BufferOfFutureOperations(ALockedMempool : TPCOperationsComp; valids_operations : TOperationsHashTree); Var i,j, nAdded, nDeleted : Integer; @@ -432,9 +508,30 @@ function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashT LOpsToAdd : TList; LTempSafeboxTransaction : TPCSafeBoxTransaction; LTickCount : TTickCount; + LLastProgress : TTickCount; + LReservationIndexes : TList; + LCompletedApplying : Boolean; + + procedure NotifyProgress(AStage : TProgressStage; ACurrent, ATotal : Integer; AForce : Boolean); + begin + if Assigned(AProgressNotify) and (AForce or + (TPlatform.GetElapsedMilliseconds(LLastProgress) >= 200)) then begin + LLastProgress := TPlatform.GetTickCount; + // The consumer owns display text; preserve synchronous cancellation here. + AProgressNotify(Self,TProgressInfo.Create(AStage,ACurrent,ATotal)); + end; + end; begin Result := -1; // -1 Means Node is blocked or disabled if Assigned(OperationsResult) then OperationsResult.Clear; + if HasStorageFailure then begin + errors := GetSynchronizationStatus.StorageFailure.ErrorMessage; + Exit; + end; + if not PendingOperationsAvailable then begin + errors := 'Pending operations are unavailable during blockchain synchronization'; + Exit; + end; if FDisabledsNewBlocksCount>0 then begin errors := Format('Cannot Add Operations due is adding disabled - OpCount:%d',[AOperationsHashTreeToAdd.OperationsCount]); TLog.NewLog(ltinfo,Classname,errors); @@ -446,6 +543,8 @@ function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashT errors := ''; Result := 0; LTickCount := TPlatform.GetTickCount; + LLastProgress := LTickCount; + LCompletedApplying := False; LValids_operations := TOperationsHashTree.Create; try LOpsToAdd := TList.Create; @@ -459,43 +558,65 @@ function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashT // 3 - For each not repeated operation, try to add to mempool // Step 1: Add operations to FMemPoolAddingOperationsList - LLockedMempool := LockMempoolWrite; + NotifyProgress(psPreparingOperations,0,AOperationsHashTreeToAdd.OperationsCount,True); + // Preserve the original batch lock for ordinary callers. A restoring + // caller reports progress between atomic admissions, outside this lock. + if not Assigned(AProgressNotify) then LLockedMempool := LockMempoolOperationsWrite; try for i := 0 to AOperationsHashTreeToAdd.OperationsCount-1 do begin - ActOp := AOperationsHashTreeToAdd.GetOperation(i); - j := FMemPoolAddingOperationsList.IndexOf( ActOp.Sha256 ); - if (j<0) then begin - LOpsToAdd.Add(ActOp); - FMemPoolAddingOperationsList.Add(ActOp.Sha256); + if Assigned(AProgressNotify) then LLockedMempool := LockMempoolOperationsWrite; + try + ActOp := AOperationsHashTreeToAdd.GetOperation(i); + j := FMemPoolAddingOperationsList.IndexOf( ActOp.Sha256 ); + if (j<0) then begin + LOpsToAdd.Add(ActOp); + FMemPoolAddingOperationsList.Add(ActOp.Sha256); + end; + finally + if Assigned(AProgressNotify) then UnlockMempoolWrite; end; + NotifyProgress(psPreparingOperations,i+1,AOperationsHashTreeToAdd.OperationsCount,False); end; finally - UnlockMempoolWrite; + if not Assigned(AProgressNotify) then UnlockMempoolWrite; end; + NotifyProgress(psPreparingOperations,AOperationsHashTreeToAdd.OperationsCount,AOperationsHashTreeToAdd.OperationsCount,True); // Step 2: + NotifyProgress(psValidatingSignatures,0,LOpsToAdd.Count,True); LTempSafeboxTransaction := TPCSafeBoxTransaction.Create(Bank.SafeBox); try - TPCOperationsSignatureValidator.MultiThreadPreValidateSignatures(LTempSafeboxTransaction,LOpsToAdd,Nil); + TPCOperationsSignatureValidator.MultiThreadPreValidateSignatures(LTempSafeboxTransaction,LOpsToAdd,AProgressNotify); finally LTempSafeboxTransaction.Free; end; + NotifyProgress(psValidatingSignatures,LOpsToAdd.Count,LOpsToAdd.Count,True); {$IFDEF BufferOfFutureOperations} - LLockedMempool := LockMempoolWrite; + LLockedMempool := LockMempoolOperationsWrite; try - Process_BufferOfFutureOperations(LLockedMempool,LValids_operations); + if PendingOperationsAvailable then + Process_BufferOfFutureOperations(LLockedMempool,LValids_operations); finally UnlockMempoolWrite; end; {$ENDIF} // Step 3: + NotifyProgress(psApplyingOperations,0,LOpsToAdd.Count,True); for j := 0 to LOpsToAdd.Count-1 do begin ActOp := LOpsToAdd[j]; - LLockedMempool := LockMempoolWrite; + LLockedMempool := LockMempoolOperationsWrite; try + // A block may arrive after this batch reserved hashes or verified + // signatures. Never execute against an overlay dirtied meanwhile. + if not PendingOperationsAvailable then begin + errors := 'Pending operations are unavailable during blockchain synchronization'; + if Result=0 then Result := -1; + Break; + end; + If (LLockedMempool.OperationsHashTree.IndexOfOperation(ActOp)<0) then begin // Protocol 2 limitation: In order to prevent spam of operations without Fee, will protect it If (ActOp.OperationFee=0) And (Bank.SafeBox.CurrentProtocol>=CT_PROTOCOL_2) And @@ -579,9 +700,14 @@ function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashT finally UnlockMempoolWrite; end; + NotifyProgress(psApplyingOperations,j+1,LOpsToAdd.Count,False); end; // for i + NotifyProgress(psApplyingOperations,LOpsToAdd.Count,LOpsToAdd.Count,True); + // A progress callback can cancel here; the finally block must still + // release this batch's reservations before the loader can finish. + NotifyProgress(psFinalizingOperations,0,LOpsToAdd.Count,True); + LCompletedApplying := True; If Result<>0 then begin - FSaveMempoolOperationsThread.Touch; // This will indicate to thread that mempool needs to be saved LTickCount := TPlatform.GetElapsedMilliseconds(LTickCount); if LTickCount=0 then LTickCount:=1; if Assigned(SenderConnection) then begin @@ -589,7 +715,7 @@ function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashT end else s := '(SELF)'; {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,Classname,Format('Finalizing AddOperations from %s Operations:%d of %d valids:%d spam:%d invalids:%d repeated:%d Miliseconds:%d %.1f ops/sec', [s,LOpsToAdd.Count,AOperationsHashTreeToAdd.OperationsCount,Result,nSpam,nError,nRepeated,LTickCount,LOpsToAdd.Count * 1000 / LTickCount]));{$ENDIF} - if FBroadcastData then begin + if FBroadcastData and PendingOperationsAvailable then begin // Send to other nodes j := TNetData.NetData.ConnectionsCountAll; for i:=0 to j-1 do begin @@ -601,23 +727,46 @@ function TNode.AddOperations(SenderConnection : TNetConnection; AOperationsHashT end; finally // Remove LOpsToAdd from FMemPoolAddingOperationsList - LLockedMempool := LockMempoolWrite; try - for i := 0 to LOpsToAdd.Count-1 do begin - ActOp := LOpsToAdd[i]; - FMemPoolAddingOperationsList.Remove(ActOp.Sha256); + LReservationIndexes := TList.Create; + try + LReservationIndexes.Capacity := LOpsToAdd.Count; + LLockedMempool := LockMempoolOperationsWrite; + try + // Other callers may have reserved interleaved hashes. Resolve all + // indexes under this lock, then compact only this batch in one pass. + for i := 0 to LOpsToAdd.Count-1 do begin + j := FMemPoolAddingOperationsList.IndexOf(LOpsToAdd[i].Sha256); + if j>=0 then LReservationIndexes.Add(j); + end; + LReservationIndexes.Sort; + FMemPoolAddingOperationsList.DeleteIndexes(LReservationIndexes.ToArray); + finally + UnlockMempoolWrite; + end; + finally + LReservationIndexes.Free; end; finally - UnlockMempoolWrite; + LOpsToAdd.Free; end; - LOpsToAdd.Free; end; finally - LValids_operations.Free; + try + LValids_operations.Free; + finally + // Cleanup precedes saving, including when later broadcasting fails. + // Cancellation before completed application never saves a partial restore. + if LCompletedApplying and (Result<>0) then FSaveMempoolOperationsThread.Touch; + end; end; // Notify it! - for i := 0 to FNotifyList.Count-1 do begin - TNodeNotifyEvents( FNotifyList[i] ).NotifyOperationsChanged; + FNotifyLock.AcquireWriteLock; + try + for i := 0 to FNotifyList.Count-1 do + FNotifyList[i].NotifyOperationsChanged; + finally + FNotifyLock.ReleaseWriteLock; end; end; @@ -644,8 +793,22 @@ constructor TNode.Create(AOwner: TComponent); TLog.NewLog(ltInfo,ClassName,'TNode.Create '+NodeVersion); inherited; FDisabledsNewBlocksCount := 0; - FLockMempool := TPCCriticalSection.Create('TNode_LockMempool'); - FOperationSequenceLock := TPCCriticalSection.Create('TNode_OperationSequenceLock'); + // Block application reenters through bank callbacks and signature reuse. + FLockMempool := TReadWriteLock.Create(lrpSupportsRecursion); + // RPC reservation/submission and notifier mutations preserve recursive writes. + FOperationSequenceLock := TReadWriteLock.Create('TNode_OperationSequenceLock',lrpSupportsRecursion); + FNotifyLock := TReadWriteLock.Create('TNode_NotifyLock',lrpSupportsRecursion); + // Small coherent snapshots share reads; state publication owns write mode. + FSynchronizationLock := TReadWriteLock.Create('TNode_SynchronizationLock',lrpSupportsRecursion); + FSynchronizationPeers := TDictionary.Create; + FSynchronizationScopes := TDictionary>.Create; + FSynchronizationGeneration := 1; + FSynchronizationStatus := Default(TNodeSynchronizationStatus); + // TNetData starts enabled; its explicit setter subsequently publishes offline. + FSynchronizationStatus.NetworkActive := True; + FSynchronizationStatus.State := nssDisconnected; + FSynchronizationStatus.PendingOperationsAvailable := True; + FNotifyList := TList.Create; FBank := TPCBank.Create(Self); FBCBankNotify := TPCBankNotify.Create(Self); FBCBankNotify.Bank := FBank; @@ -653,15 +816,13 @@ constructor TNode.Create(AOwner: TComponent); FNetServer := TNetServer.Create; FMemPoolOperationsComp := TPCOperationsComp.Create(Nil); FMemPoolOperationsComp.bank := FBank; - FNotifyList := TList.Create; FMemPoolAddingOperationsList := TOrderedRawList.Create; {$IFDEF BufferOfFutureOperations} FBufferAuxWaitingOperations := TOperationsHashTree.Create; {$ENDIF} FBroadcastData := True; FUpdateBlockchain := True; - FSaveMempoolOperationsThread := TSaveMempoolOperationsThread.Create(True); - FSaveMempoolOperationsThread.FNode := Self; + FSaveMempoolOperationsThread := TSaveMempoolOperationsThread.Create(Self); FSaveMempoolOperationsThread.Resume; if Not Assigned(_Node) then _Node := Self; end; @@ -719,24 +880,60 @@ class procedure TNode.DecodeIpStringToNodeServerAddressArray(const Ips: String; destructor TNode.Destroy; Var step : String; + LScopes: TList; + {$IFDEF USE_ABSTRACTMEM} + I: Integer; + LStorage: TAbstractMemBlockchainStorage; + LStorageFileName: String; + {$ENDIF} begin TLog.NewLog(ltInfo,ClassName,'TNode.Destroy START'); Try + // Shared network producers are stopped by their caller. The node's own + // accepted-client callbacks must also finish before their template/locks + // disappear, and before the saver captures the final dirty operation batch. + step := 'Desactivating server'; + FNetServer.Active := false; + + step := 'Destroying NetServer'; + FreeAndNil(FNetServer); + step := 'Deleting SaveMempoolOperationsThread'; - FSaveMempoolOperationsThread.Terminate; + FSaveMempoolOperationsThread.Stop; FSaveMempoolOperationsThread.WaitFor; FreeAndNil(FSaveMempoolOperationsThread); + {$IFDEF USE_ABSTRACTMEM} + step := 'Finalizing blockchain storage workers'; + // A draining index writer can still report its first persistence failure. + // Keep all node/listener locks alive until those owned workers have joined. + // Inspect existing components; the lazy Storage getter could create a backend. + if Assigned(FBank) then + for I := FBank.ComponentCount-1 downto 0 do + if FBank.Components[I] is TAbstractMemBlockchainStorage then begin + LStorage := TAbstractMemBlockchainStorage(FBank.Components[I]); + LStorageFileName := 'blockchain storage'; + if Assigned(LStorage.FileMem) then LStorageFileName := LStorage.FileMem.FileName; + try + LStorage.UseMultithread := False; + except + on E: Exception do begin + // Known write failures stay latched while later ownership cleanup + // proceeds. Cancellation, exhaustion and programmer faults propagate. + if (E is EStreamError) or (E is EOSError) or (E is EFileMem) or + (E is EAbstractMem) or (E is EPCAbstractMem) or + (E is EAbstractMemBlockchainStorage) then + ReportStorageFailure(sfsBlockStream,LStorageFileName,E) + else raise; + end; + end; + end; + {$ENDIF} + step := 'Deleting critical section'; FreeAndNil(FLockMempool); FreeAndNil(FOperationSequenceLock); - step := 'Desactivating server'; - FNetServer.Active := false; - - step := 'Destroying NetServer'; - FreeAndNil(FNetServer); - step := 'Destroying NotifyList'; FreeAndNil(FNotifyList); step := 'Destroying Operations'; @@ -748,6 +945,11 @@ destructor TNode.Destroy; step := 'Destroying Bank'; FreeAndNil(FBCBankNotify); FreeAndNil(FBank); + FreeAndNil(FSynchronizationPeers); + for LScopes in FSynchronizationScopes.Values do LScopes.Free; + FreeAndNil(FSynchronizationScopes); + FreeAndNil(FSynchronizationLock); + FreeAndNil(FNotifyLock); {$IFDEF BufferOfFutureOperations} FreeAndNil(FBufferAuxWaitingOperations); {$ENDIF} @@ -940,12 +1142,18 @@ function TNode.TryFindPublicSaleAccount(AMaximumPrice: Int64; APreventRaceCondit function TNode.TryLockNode(MaxWaitMilliseconds: Cardinal): Boolean; begin - Result := TPCThread.TryProtectEnterCriticalSection(Self,MaxWaitMilliseconds,FLockMempool); + // Preserve the legacy exclusive node gate and its maximum wait contract. + if MaxWaitMilliseconds > 60000 then MaxWaitMilliseconds := 60000; + Result := FLockMempool.TryAcquireWriteLock(Integer(MaxWaitMilliseconds)); end; procedure TNode.UnlockNode; begin - FLockMempool.Release; + try + if FLockMempool.RecursiveWriteCount=1 then CaptureSynchronizationMempoolSummary; + finally + FLockMempool.ReleaseWriteLock; + end; end; procedure TNode.MarkVerifiedECDSASignaturesFromMemPool(newOperationsToValidate: TPCOperationsComp); @@ -953,10 +1161,17 @@ procedure TNode.MarkVerifiedECDSASignaturesFromMemPool(newOperationsToValidate: begin // Introduced on Build 4.0.2 to increase speed using MEMPOOL verified operations instead of verify again everytime // Will check if "newOperationsToValidate" operations are on MEMPOOL. If found, will set same FHasValidSignature value in order to mark as verified - LLockedMempool := LockMempoolRead; + // Hash trees can share operation instances; signature flags require write ownership. + // This lease touches signature/index metadata only. Reusing a cached + // signature must not force a replay or expose the deferred account overlay. + FLockMempool.AcquireWriteLock; + LLockedMempool := FMemPoolOperationsComp; try if newOperationsToValidate = LLockedMempool then Exit; // Is the same, do nothing - if newOperationsToValidate.OperationBlock.protocol_version <> newOperationsToValidate.OperationBlock.protocol_version then Exit; // Must be same protocol + // Signing digests change between protocols. A cached signature result from + // the pending template is reusable only under the same block protocol. + if newOperationsToValidate.OperationBlock.protocol_version <> + LLockedMempool.OperationBlock.protocol_version then Exit; newOperationsToValidate.Lock; try LLockedMempool.OperationsHashTree.MarkVerifiedECDSASignatures(newOperationsToValidate.OperationsHashTree); @@ -964,7 +1179,7 @@ procedure TNode.MarkVerifiedECDSASignaturesFromMemPool(newOperationsToValidate: newOperationsToValidate.Unlock; end; finally - UnlockMempoolRead; + FLockMempool.ReleaseWriteLock; end; end; @@ -990,6 +1205,10 @@ function TNode.IsBlockChainValid(var WhyNot : String): Boolean; Var unixtimediff : Integer; begin Result :=false; + if HasStorageFailure then begin + WhyNot := GetSynchronizationStatus.StorageFailure.ErrorMessage; + Exit; + end; if (TNetData.NetData.NetStatistics.ActiveConnections<=0) then begin WhyNot := 'No connection to check blockchain'; exit; @@ -1011,13 +1230,21 @@ function TNode.IsBlockChainValid(var WhyNot : String): Boolean; end; function TNode.IsReady(var CurrentProcess: String): Boolean; -var LLockedMempool : TPCOperationsComp; +var LLockedMempool : TPCOperationsComp; LProgress : TProgressInfo; begin Result := false; CurrentProcess := ''; + if HasStorageFailure then begin + CurrentProcess := GetSynchronizationStatus.StorageFailure.ErrorMessage; + Exit; + end; + if not PendingOperationsAvailable then begin + CurrentProcess := 'Blockchain synchronization is active'; + Exit; + end; if FBank.IsReady(CurrentProcess) then begin if FNetServer.Active then begin - if Not TNetData.NetData.IsGettingNewBlockChainFromClient(CurrentProcess) then begin + if Not TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then begin LLockedMempool := LockMempoolRead; try if TNetData.NetData.MaxRemoteOperationBlock.block>LLockedMempool.OperationBlock.block then begin @@ -1029,7 +1256,9 @@ function TNode.IsReady(var CurrentProcess: String): Boolean; finally UnlockMempoolRead; end; - end; + end else + // Readiness diagnostics are separate from the UI's progress presentation. + CurrentProcess := 'Blockchain synchronization is active'; end else begin CurrentProcess := 'Server not active'; end; @@ -1067,11 +1296,529 @@ procedure TNode.Notification(AComponent: TComponent; Operation: TOperation); inherited; end; +procedure TNode.BeginBlockChainUpdate; +var LScopes: TList; + LGeneration: UInt64; +begin + FSynchronizationLock.AcquireWriteLock; + try + if not FSynchronizationScopes.TryGetValue(TThread.CurrentThread.ThreadID,LScopes) then begin + LScopes := TList.Create; + FSynchronizationScopes.Add(TThread.CurrentThread.ThreadID,LScopes); + end; + if LScopes.Count>0 then LGeneration := LScopes[LScopes.Count-1] + else if FSynchronizationStatus.NetworkActive and (FSynchronizationPeers.Count>0) then + LGeneration := FSynchronizationGeneration + else LGeneration := 0; + // Nested block application inherits its download's session. Reconnecting + // must not turn the remainder of an old download into a new session. + LScopes.Add(LGeneration); + Inc(FSynchronizationStatus.ActiveTransfers); + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; +end; + +procedure TNode.EndBlockChainUpdate; +var LScopes: TList; +begin + FSynchronizationLock.AcquireWriteLock; + try + if not FSynchronizationScopes.TryGetValue(TThread.CurrentThread.ThreadID,LScopes) or + (LScopes.Count=0) then + raise Exception.Create('Unbalanced blockchain update scope'); + LScopes.Delete(LScopes.Count-1); + if LScopes.Count=0 then begin + FSynchronizationScopes.Remove(TThread.CurrentThread.ThreadID); + LScopes.Free; + end; + Dec(FSynchronizationStatus.ActiveTransfers); + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; + // Completion is an event, not a UI timer or synchronous UI callback. The + // existing maintenance owner reconciles only after the whole selected + // backlog drains; gaps between its batches must not trigger full replay. + WakeMempoolMaintenance; + if PendingOperationsAvailable then NotifyBlocksChanged; +end; + +procedure TNode.RetireSynchronizationSession; +begin + // Retire only future network acquisition. Received blocks keep processing and + // keep the UI synchronizing until their balanced physical scopes finish. + Inc(FSynchronizationGeneration); +end; + +function TNode.IsCurrentSynchronizationScope: Boolean; +var LScopes: TList; +begin + // Caller owns the status lock. A fresh producer has no enclosing old scope. + Result := not FSynchronizationScopes.TryGetValue(TThread.CurrentThread.ThreadID,LScopes); + if not Result then + Result := (LScopes.Count>0) and + (LScopes[LScopes.Count-1]=FSynchronizationGeneration); +end; + +function TNode.GetSynchronizationStatus: TNodeSynchronizationStatus; +begin + // Only a coherent record copy. Never acquire mempool/Bank/SafeBox here. + FSynchronizationLock.AcquireReadLock; + try + Result := FSynchronizationStatus; + finally + FSynchronizationLock.ReleaseReadLock; + end; +end; + +procedure TNode.DoFlushSafebox; +begin + {$IFDEF USE_ABSTRACTMEM} + FBank.SafeBox.PCAbstractMem.FlushCache; + {$ENDIF} +end; + +function TNode.DoFlushBlockStorage: Boolean; +begin + Result := True; + {$IFDEF USE_ABSTRACTMEM} + if (FBank.Storage is TAbstractMemBlockchainStorage) and + (TAbstractMemBlockchainStorage(FBank.Storage).PendingToSave=0) then + // The backend rechecks its private failure under the file lock. A node-wide + // latch alone cannot prevent a competing flush from certifying partial indexes. + Result := TAbstractMemBlockchainStorage(FBank.Storage).FlushCache; + {$ENDIF} +end; + +procedure TNode.FlushSafebox; +var LFileName: String; +begin + LFileName := 'safebox'; + try + {$IFDEF USE_ABSTRACTMEM} + LFileName := FBank.SafeBox.PCAbstractMem.FileName; + {$ENDIF} + DoFlushSafebox; + except + on E: Exception do begin + // Cache eviction/flush is persistence too, even after block validation + // and operation storage have succeeded. Never reclassify cancellation. + if (E is EStreamError) or (E is EOSError) or (E is EFileMem) or + (E is EAbstractMem) or (E is EPCAbstractMem) then + ReportStorageFailure(sfsSafeboxFlush,LFileName,E); + raise; + end; + end; +end; + +procedure TNode.FlushBlockStorage; +var LFileName: String; +begin + LFileName := 'blockchain storage'; + try + {$IFDEF USE_ABSTRACTMEM} + if (FBank.Storage is TAbstractMemBlockchainStorage) and + Assigned(TAbstractMemBlockchainStorage(FBank.Storage).FileMem) then + LFileName := TAbstractMemBlockchainStorage(FBank.Storage).FileMem.FileName; + {$ENDIF} + if not DoFlushBlockStorage then + raise EWriteError.Create('Cannot flush blockchain storage '+LFileName); + except + on E: Exception do begin + if (E is EStreamError) or (E is EOSError) or (E is EFileMem) or + (E is EAbstractMem) then ReportStorageFailure(sfsBlockStream,LFileName,E); + raise; + end; + end; +end; + +procedure TNode.ReportStorageFailure(AStage: TNodeStorageFailureStage; const AFileName: String; AError: Exception); +var LFailure: TNodeStorageFailure; LChanged: Boolean; +begin + // Preserve the first persistence failure until this node is destroyed. Copy + // exception data, never retain an exception object owned by the producer. + if (AError=Nil) or (AError is EOutOfMemory) or (AError is EAbort) or (AError is EAccessViolation) then Exit; + LFailure := Default(TNodeStorageFailure); + LFailure.HasFailure := True; + LFailure.Stage := AStage; + LFailure.FileName := AFileName; + LFailure.ErrorClass := AError.ClassName; + LFailure.ErrorMessage := AError.Message; + LChanged := False; + FSynchronizationLock.AcquireWriteLock; + try + if Not FSynchronizationStatus.StorageFailure.HasFailure then begin + FSynchronizationStatus.StorageFailure := LFailure; + RetireSynchronizationSession; + RecalculateSynchronizationStatus; + LChanged := True; + end; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + // This publishes existing notifier flags only. A bank/network producer never + // calls GUI subscribers or acquires the node writer while reporting failure. + if LChanged then NotifySynchronizationChanged; +end; + +function TNode.HasStorageFailure: Boolean; +begin + FSynchronizationLock.AcquireReadLock; + try + Result := FSynchronizationStatus.StorageFailure.HasFailure; + finally + FSynchronizationLock.ReleaseReadLock; + end; +end; + +procedure TNode.RecalculateSynchronizationStatus; +var LTarget: Cardinal; LChainBusy: Boolean; +begin + // Caller owns only the short status lock. No peer object is dereferenced. + FSynchronizationStatus.PeerCount := FSynchronizationPeers.Count; + FSynchronizationStatus.TargetBlockCount := 0; + for LTarget in FSynchronizationPeers.Values do + if LTarget>FSynchronizationStatus.TargetBlockCount then + FSynchronizationStatus.TargetBlockCount := LTarget; + FSynchronizationStatus.HasRemoteTarget := FSynchronizationStatus.TargetBlockCount>0; + LChainBusy := (FSynchronizationStatus.ActiveTransfers>0) or + (FSynchronizationStatus.NetworkActive and (FSynchronizationStatus.PeerCount>0) and + (FSynchronizationStatus.TargetBlockCount>FSynchronizationStatus.LocalBlockCount)); + FSynchronizationStatus.PendingOperationsAvailable := + not FSynchronizationStatus.StorageFailure.HasFailure and + not FMempoolNeedsReconciliation and not LChainBusy; + if FSynchronizationStatus.StorageFailure.HasFailure then + FSynchronizationStatus.State := nssStorageError + else if LChainBusy or FMempoolNeedsReconciliation then + FSynchronizationStatus.State := nssSynchronizing + else if not FSynchronizationStatus.NetworkActive then + FSynchronizationStatus.State := nssOffline + else if FSynchronizationStatus.PeerCount=0 then + FSynchronizationStatus.State := nssDisconnected + else if FSynchronizationStatus.TargetBlockCount>FSynchronizationStatus.LocalBlockCount then + FSynchronizationStatus.State := nssSynchronizing + else + FSynchronizationStatus.State := nssIdle; + if FSynchronizationStatus.State<>nssSynchronizing then + FSynchronizationStatus.Progress := Default(TProgressInfo); +end; + +procedure TNode.NotifySynchronizationChanged; +var i: Integer; +begin + // Registry membership is stable during this read. Each listener publishes + // its synchronization flag atomically; message queues and other flags still + // require write ownership in their respective notification paths. + FNotifyLock.AcquireReadLock; + try + for i := 0 to FNotifyList.Count-1 do + FNotifyList[i].NotifySynchronizationChanged; + finally + FNotifyLock.ReleaseReadLock; + end; +end; + +procedure TNode.SetSynchronizationNetworkActive(AActive: Boolean); +begin + FSynchronizationLock.AcquireWriteLock; + try + FSynchronizationStatus.NetworkActive := AActive; + if not AActive then begin + FSynchronizationPeers.Clear; + RetireSynchronizationSession; + end; + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; + WakeMempoolMaintenance; +end; + +procedure TNode.SetSynchronizationPeerConnected(APeer: TObject; AConnected: Boolean); +var LHadPeers: Boolean; +begin + if not Assigned(APeer) then Exit; + FSynchronizationLock.AcquireWriteLock; + try + LHadPeers := FSynchronizationPeers.Count>0; + if not AConnected then FSynchronizationPeers.Remove(APeer) + else if FSynchronizationStatus.NetworkActive and + not FSynchronizationPeers.ContainsKey(APeer) then + FSynchronizationPeers.Add(APeer,0); + if LHadPeers and (FSynchronizationPeers.Count=0) then + RetireSynchronizationSession; + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; + WakeMempoolMaintenance; +end; + +procedure TNode.SetSynchronizationTarget(APeer: TObject; ATargetBlockCount: Cardinal); +begin + FSynchronizationLock.AcquireWriteLock; + try + // A selected target is meaningful only for a currently connected peer. + // Reconnecting must not revive a discarded session's remote height. + if IsCurrentSynchronizationScope and FSynchronizationStatus.NetworkActive and + FSynchronizationPeers.ContainsKey(APeer) then + FSynchronizationPeers[APeer] := ATargetBlockCount; + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; + WakeMempoolMaintenance; +end; + +procedure TNode.UpdateSynchronizationPeerTarget(APeer: TObject; ATargetBlockCount: Cardinal); +var LPrevious: Cardinal; +begin + FSynchronizationLock.AcquireWriteLock; + try + // HELLO/new-block advertisements update an already selected download peer; + // arbitrary advertised heights do not select a synchronization target. + if IsCurrentSynchronizationScope and FSynchronizationStatus.NetworkActive and + FSynchronizationPeers.TryGetValue(APeer,LPrevious) and (LPrevious>0) then + FSynchronizationPeers[APeer] := ATargetBlockCount; + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; + WakeMempoolMaintenance; +end; + +procedure TNode.SetSynchronizationProgress(const AProgress: TProgressInfo); +begin + FSynchronizationLock.AcquireWriteLock; + try + if FSynchronizationStatus.State=nssSynchronizing then + FSynchronizationStatus.Progress := AProgress; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; +end; + +procedure TNode.CaptureSynchronizationMempoolSummary; +var LBlock, LLastBlock: TOperationBlock; + LCount: Integer; + LChanged: Boolean; +begin + // Caller owns a write scope. During catch-up this publishes confirmed facts + // and the retained raw operation count, explicitly marked unavailable as a + // pending overlay. It performs no account scan or disk access. + LBlock := FMemPoolOperationsComp.OperationBlock; + LLastBlock := FBank.LastOperationBlock; + if FMempoolNeedsReconciliation then begin + // The pending template intentionally still belongs to the old base. Basic + // synchronization facts must instead describe the newly committed bank. + LBlock.block := FBank.BlocksCount; + LBlock.compact_target := LLastBlock.compact_target; + end; + LCount := FMemPoolOperationsComp.Count; + FSynchronizationLock.AcquireWriteLock; + try + LChanged := not FSynchronizationStatus.HasMempoolSummary or + (FSynchronizationStatus.LocalBlockCount<>LBlock.block) or + (FSynchronizationStatus.PendingOperationsCount<>LCount) or + (FSynchronizationStatus.CompactTarget<>LBlock.compact_target) or + (FSynchronizationStatus.LastBlockTimestamp<>LLastBlock.timestamp) or + (FSynchronizationStatus.ProtocolVersion<>LLastBlock.protocol_version); + FSynchronizationStatus.HasMempoolSummary := True; + FSynchronizationStatus.LocalBlockCount := LBlock.block; + FSynchronizationStatus.PendingOperationsCount := LCount; + FSynchronizationStatus.CompactTarget := LBlock.compact_target; + FSynchronizationStatus.LastBlockTimestamp := LLastBlock.timestamp; + FSynchronizationStatus.ProtocolVersion := LLastBlock.protocol_version; + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + if LChanged then NotifySynchronizationChanged; +end; + +procedure TNode.WakeMempoolMaintenance; +begin + if Assigned(FSaveMempoolOperationsThread) then + FSaveMempoolOperationsThread.Wake; +end; + +procedure TNode.ReconcileMempoolIfReady; +var LCanReconcile: Boolean; LPreviousHash: TRawBytes; I: Integer; + LResends: TOperationsHashTree; +begin + // This method runs on the existing owned maintenance thread. In particular, + // OFF/disconnect between batches must not replay pending operations on the + // main thread, and a selected target ahead must not replay after every batch. + // Reject ordinary per-block wakeups before entering the writer queue. The + // same predicate is checked again after admission to cover a new producer. + FSynchronizationLock.AcquireReadLock; + try + LCanReconcile := not FSynchronizationStatus.StorageFailure.HasFailure and + not FSynchronizationStatus.PendingOperationsAvailable and + (FSynchronizationStatus.ActiveTransfers=0) and + ((not FSynchronizationStatus.NetworkActive) or + (FSynchronizationStatus.PeerCount=0) or + (FSynchronizationStatus.TargetBlockCount<=FSynchronizationStatus.LocalBlockCount)); + finally + FSynchronizationLock.ReleaseReadLock; + end; + if not LCanReconcile then Exit; + LResends := Nil; + try + FLockMempool.AcquireWriteLock; + try + if not FMempoolNeedsReconciliation then Exit; + FSynchronizationLock.AcquireReadLock; + try + LCanReconcile := not FSynchronizationStatus.StorageFailure.HasFailure and + (FSynchronizationStatus.ActiveTransfers=0) and + ((not FSynchronizationStatus.NetworkActive) or + (FSynchronizationStatus.PeerCount=0) or + (FSynchronizationStatus.TargetBlockCount<=FSynchronizationStatus.LocalBlockCount)); + finally + FSynchronizationLock.ReleaseReadLock; + end; + if not LCanReconcile then Exit; + LPreviousHash := FMemPoolOperationsComp.OperationsHashTree.HashTree; + // Full original replay is required once: protocol/height-sensitive rules, + // account seals and conflict removal must match the final confirmed base. + FMemPoolOperationsComp.SanitizeOperations; + if FResendPendingOperations then begin + LResends := TOperationsHashTree.Create; + CollectPendingResends(LResends); + FResendPendingOperations := False; + end; + FSynchronizationLock.AcquireWriteLock; + try + FMempoolNeedsReconciliation := False; + RecalculateSynchronizationStatus; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + if not TBaseType.Equals(LPreviousHash,FMemPoolOperationsComp.OperationsHashTree.HashTree) then + FSaveMempoolOperationsThread.Touch; + CaptureSynchronizationMempoolSummary; + finally + FLockMempool.ReleaseWriteLock; + end; + // Availability is published only after a complete coherent replay. A later + // producer can dirty it again, in which case observers retain their queries. + NotifySynchronizationChanged; + NotifyBlocksChanged; + FNotifyLock.AcquireWriteLock; + try + for I := 0 to FNotifyList.Count-1 do + FNotifyList[I].NotifyOperationsChanged; + finally + FNotifyLock.ReleaseWriteLock; + end; + if Assigned(LResends) then BroadcastPendingResends(LResends); + finally + LResends.Free; + end; +end; + +procedure TNode.CollectPendingResends(AOperations: TOperationsHashTree); +var I, J: Integer; LMinBlock: Cardinal; LOperation: TPCOperation; +begin + // Caller owns node WRITE and a coherent pending overlay. Keep the original + // bounded selection and counters, merely delayed until reconciliation. + J := Random(5); + if Bank.LastBlockFound.OperationBlock.block>J then + LMinBlock := Bank.LastBlockFound.OperationBlock.block-J + else LMinBlock := 1; + I := 0; + while (AOperations.OperationsCount0 then begin + TLog.NewLog(ltinfo,ClassName,Format('Resending %d operations for new block (Mempool Pending Operations:%d)',[ + AOperations.OperationsCount,FMemPoolOperationsComp.Count])); + {$IFDEF HIGHLOG} + for I := 0 to AOperations.OperationsCount-1 do + TLog.NewLog(ltinfo,ClassName,'Resending ('+IntToStr(I+1)+'/'+IntToStr(AOperations.OperationsCount)+'): '+AOperations.GetOperation(I).ToString); + {$ENDIF} + end; +end; + +procedure TNode.BroadcastPendingResends(AOperations: TOperationsHashTree); +var LNet: TNetData; LConnections: TList; LConnection: TNetConnection; + I: Integer; +begin + if not FBroadcastData or (AOperations.OperationsCount=0) or + not TNetData.NetDataExists then Exit; + LNet := TNetData.NetData; + LConnections := TList.Create; + try + // These existing lifetime leases also keep their NetData owner alive + // while shutdown waits for every leased connection to finish retiring. + LNet.AcquireDownloadConnections(LConnections); + for I := 0 to LConnections.Count-1 do begin + LConnection := LConnections[I]; + if LNet.ConnectionLock(Self,LConnection,0) then begin + try + if LConnection.RemoteOperationBlock.block>0 then begin + // Transfer the lease before construction: constructor failure also + // destroys the notifier and releases its transferred lease. + LConnections[I] := Nil; + TThreadNodeNotifyOperations.Create(LConnection,AOperations,LNet); + end; + finally LNet.ConnectionUnlock(LConnection); end; + end; + end; + finally + for I := 0 to LConnections.Count-1 do + if Assigned(LConnections[I]) then LNet.ReleaseDownloadConnection(LConnections[I]); + LConnections.Free; + end; +end; + +function TNode.IsUpdatingBlockChain: Boolean; +var LStatus: TNodeSynchronizationStatus; +begin + // Physical scopes have one authoritative count. UI refresh admission must + // not add a second activity clock or infer work from elapsed notification time. + LStatus := GetSynchronizationStatus; + Result := LStatus.ActiveTransfers>0; +end; + +function TNode.CanRequestBlockChainData: Boolean; +begin + FSynchronizationLock.AcquireReadLock; + try + Result := not FSynchronizationStatus.StorageFailure.HasFailure and + FSynchronizationStatus.NetworkActive and IsCurrentSynchronizationScope; + finally + FSynchronizationLock.ReleaseReadLock; + end; +end; + procedure TNode.NotifyBlocksChanged; Var i : Integer; begin - for i := 0 to FNotifyList.Count-1 do begin - TNodeNotifyEvents( FNotifyList[i] ).NotifyBlocksChanged; + FNotifyLock.AcquireWriteLock; + try + for i := 0 to FNotifyList.Count-1 do + FNotifyList[i].NotifyBlocksChanged; + finally + FNotifyLock.ReleaseWriteLock; end; end; @@ -1196,8 +1943,10 @@ procedure TNode.InitSafeboxAndOperations(max_block_to_read : Cardinal = $FFFFFFF opht := TOperationsHashTree.Create; oprl := TOperationsResumeList.Create; try - Bank.Storage.LoadPendingBufferOperations(opht); // New Build 2.1.4 to load pending operations buffer - n := AddOperations(Nil,opht,oprl,errors); + if Assigned(restoreProgressNotify) then + restoreProgressNotify(Self,TProgressInfo.Create(psLoadingPendingOperations)); + Bank.Storage.LoadPendingBufferOperations(opht,restoreProgressNotify); // New Build 2.1.4 to load pending operations buffer + n := AddOperations(Nil,opht,oprl,errors,restoreProgressNotify); TLog.NewLog(ltInfo,ClassName,Format('Pending buffer restored operations:%d added:%d final_operations:%d errors:%s',[opht.OperationsCount,n,MempoolOperationsCount,errors])); finally opht.Free; @@ -1252,10 +2001,14 @@ function TNode.FindOperation(Const AOperationHash : TRawBytes; var AOperationRes procedure TNode.NotifyNetClientMessage(Sender: TNetConnection; const TheMessage: String); Var i : Integer; begin - for i := 0 to FNotifyList.Count-1 do begin - if Assigned( TNodeNotifyEvents( FNotifyList[i] ).OnNodeMessageEvent) then begin - TNodeNotifyEvents( FNotifyList[i] ).FMessages.AddObject(TheMessage,Sender); + FNotifyLock.AcquireWriteLock; + try + for i := 0 to FNotifyList.Count-1 do begin + if Assigned( FNotifyList[i].OnNodeMessageEvent) then + FNotifyList[i].FMessages.AddObject(TheMessage,Sender); end; + finally + FNotifyLock.ReleaseWriteLock; end; end; @@ -1354,37 +2107,147 @@ class procedure TNode.SetPascalCoinDataFolder(const ANewDataFolder: String); function TNode.LockMempoolRead: TPCOperationsComp; begin - FLockMempool.Acquire; - Result := FMemPoolOperationsComp; + RequireMempoolAvailable; + FLockMempool.AcquireReadLock; + try + RequireMempoolAvailable; + Result := FMemPoolOperationsComp; + except + FLockMempool.ReleaseReadLock; + raise; + end; +end; + +function TNode.TryLockMempoolRead(MaxWaitMilliseconds: Cardinal; + out AMempool: TPCOperationsComp): Boolean; +begin + AMempool := Nil; + if not PendingOperationsAvailable then Exit(False); + Result := TryLockMempoolOperationsRead(MaxWaitMilliseconds,AMempool); + if Result and not PendingOperationsAvailable then begin + FLockMempool.ReleaseReadLock; + AMempool := Nil; + Result := False; + end; +end; + +function TNode.TryLockMempoolOperationsRead(MaxWaitMilliseconds: Cardinal; + out AMempool: TPCOperationsComp): Boolean; +begin + AMempool := Nil; + if MaxWaitMilliseconds > 60000 then MaxWaitMilliseconds := 60000; + Result := FLockMempool.TryAcquireReadLock(Integer(MaxWaitMilliseconds)); + if Result then AMempool := FMemPoolOperationsComp; +end; + +function TNode.PendingOperationsAvailable: Boolean; +begin + FSynchronizationLock.AcquireReadLock; + try + Result := FSynchronizationStatus.PendingOperationsAvailable; + finally + FSynchronizationLock.ReleaseReadLock; + end; +end; + +procedure TNode.RequireMempoolAvailable; +begin + if HasStorageFailure then + raise ENodeStorageUnavailable.Create(GetSynchronizationStatus.StorageFailure.ErrorMessage); + if not PendingOperationsAvailable then + raise EPendingOperationsUnavailable.Create('Pending operations are unavailable during blockchain synchronization'); end; procedure TNode.UnlockMempoolRead; begin - FLockMempool.Release; + FLockMempool.ReleaseReadLock; +end; + +function TNode.LockMempoolUpgradeableRead: TPCOperationsComp; +begin + // Ordinary readers may coexist; another potential upgrader waits until exit. + RequireMempoolAvailable; + FLockMempool.AcquireUpgradeableReadLock; + try + RequireMempoolAvailable; + Result := FMemPoolOperationsComp; + except + FLockMempool.ReleaseUpgradeableReadLock; + raise; + end; +end; + +procedure TNode.UnlockMempoolUpgradeableRead; +begin + // The caller must first release any nested write acquisition it entered. + FLockMempool.ReleaseUpgradeableReadLock; end; function TNode.LockMempoolWrite: TPCOperationsComp; begin - // TODO: Must lock WRITE EXCLUSIVE NO READ !!! XXXXXXXXXXXXXXXX - FLockMempool.Acquire; + // When called inside an upgradeable read, wait for other readers to drain. + // UnlockMempoolWrite then returns to that retained outer upgradeable scope. + RequireMempoolAvailable; + FLockMempool.AcquireWriteLock; + try + RequireMempoolAvailable; + Result := FMemPoolOperationsComp; + except + FLockMempool.ReleaseWriteLock; + raise; + end; +end; + +function TNode.LockMempoolOperationsWrite: TPCOperationsComp; +begin + // Reservations, cleanup, signature caches and final serialization need the + // operation sequence itself, never the unavailable pending account overlay. + FLockMempool.AcquireWriteLock; Result := FMemPoolOperationsComp; end; procedure TNode.UnlockMempoolWrite; begin - FLockMempool.Release; + try + if FLockMempool.RecursiveWriteCount=1 then CaptureSynchronizationMempoolSummary; + finally + FLockMempool.ReleaseWriteLock; + end; end; procedure TNode.OnBankNewBlock(Sender: TObject); var LLockedMempool : TPCOperationsComp; + LPreviousPendingHash: TRawBytes; LDefer: Boolean; begin - LLockedMempool := LockMempoolWrite; + // Bank validation still completes synchronously and exactly. Only rebuilding + // the separate pending overlay is deferred while a received backlog exists. + FLockMempool.AcquireWriteLock; try - LLockedMempool.SanitizeOperations; + LLockedMempool := FMemPoolOperationsComp; + FSynchronizationLock.AcquireWriteLock; + try + LDefer := not FSynchronizationStatus.PendingOperationsAvailable; + if LDefer and (LLockedMempool.Count>0) then begin + FMempoolNeedsReconciliation := True; + RecalculateSynchronizationStatus; + end; + finally + FSynchronizationLock.ReleaseWriteLock; + end; + if not LDefer or (LLockedMempool.Count=0) then begin + LPreviousPendingHash := LLockedMempool.OperationsHashTree.HashTree; + LLockedMempool.SanitizeOperations; + if Assigned(FSaveMempoolOperationsThread) and + not TBaseType.Equals(LPreviousPendingHash,LLockedMempool.OperationsHashTree.HashTree) then + FSaveMempoolOperationsThread.Touch; + end; + // Confirmed status remains current even when pending balances are hidden. + CaptureSynchronizationMempoolSummary; finally - UnlockMempoolWrite; + FLockMempool.ReleaseWriteLock; end; NotifyBlocksChanged; + WakeMempoolMaintenance; end; function TNode.SendNodeMessage(Target: TNetConnection; const TheMessage: String; var errors: String): Boolean; @@ -1393,7 +2256,7 @@ function TNode.SendNodeMessage(Target: TNetConnection; const TheMessage: String; s : String; begin Result := false; - if Not TPCThread.TryProtectEnterCriticalSection(Self,4000,FLockMempool) then begin + if Not TryLockNode(4000) then begin s := 'Cannot Send node message due blocking lock operations node'; TLog.NewLog(lterror,Classname,s); if TThread.CurrentThread.ThreadID=MainThreadID then raise Exception.Create(s) else exit; @@ -1418,7 +2281,54 @@ function TNode.SendNodeMessage(Target: TNetConnection; const TheMessage: String; end; result := true; finally - FLockMempool.Release; + UnlockNode; + end; +end; + +procedure TNode.LogBlockChainBatchReceived(SenderConnection: TNetConnection; + ACount, AFirstBlock, ALastBlock: Cardinal; APayloadBytes: Int64); +var LSource, LRemoteAddress: String; +begin + if Assigned(SenderConnection) then begin + LSource := SenderConnection.ClassName; + LRemoteAddress := SenderConnection.ClientRemoteAddr; + end else begin + LSource := ClassName; + LRemoteAddress := 'NIL'; + end; + // Receipt is separate from acceptance: the whole response is already local + // before validation/reconciliation can continue after networking is stopped. + FNodeLog.NotifyNewLog(ltinfo,LSource,Format( + 'Block batch received: Count:%d First:%d Last:%d PayloadBytes:%d Peer:%s',[ + ACount,AFirstBlock,ALastBlock,APayloadBytes,LRemoteAddress])); +end; + +procedure TNode.LogBlockChainResult(SenderConnection: TNetConnection; + const AOperationBlock: TOperationBlock; AAccepted: Boolean; const AErrors: String); +var LSource, LRemoteAddress: String; + LAcceptedAt: Cardinal; +begin + if Assigned(SenderConnection) then begin + LSource := SenderConnection.ClassName; + LRemoteAddress := SenderConnection.ClientRemoteAddr; + end else begin + LSource := ClassName; + LRemoteAddress := 'NIL'; + end; + LAcceptedAt := UnivDateTimeToUnix(DateTime2UnivDateTime(Now)); + // Keep the original columns: the elapsed value is the block's age at + // acceptance, not its processing duration. Batch downloads must log here + // without adopting AddNewBlockChain's announcement/rebroadcast behavior. + if AAccepted then begin + FNodeLog.NotifyNewLog(ltupdate,LSource,Format(';%d;%s;%s;;%d;%d;%d;%s',[ + AOperationBlock.block,LRemoteAddress,AOperationBlock.block_payload.ToPrintable, + AOperationBlock.timestamp,LAcceptedAt,LAcceptedAt - AOperationBlock.timestamp, + IntToHex(AOperationBlock.compact_target,8)])); + end else begin + FNodeLog.NotifyNewLog(lterror,LSource,Format(';%d;%s;%s;%s;%d;%d;%d;%s',[ + AOperationBlock.block,LRemoteAddress,AOperationBlock.block_payload.ToPrintable,AErrors, + AOperationBlock.timestamp,LAcceptedAt,LAcceptedAt - AOperationBlock.timestamp, + IntToHex(AOperationBlock.compact_target,8)])); end; end; @@ -1466,7 +2376,7 @@ constructor TNodeNotifyEvents.Create(AOwner: TComponent); destructor TNodeNotifyEvents.Destroy; begin - if Assigned(FNode) then FNode.FNotifyList.Remove(Self); + Node := Nil; FThreadSafeNodeNotifyEvent.FNodeNotifyEvents := Nil; FThreadSafeNodeNotifyEvent.Terminate; FThreadSafeNodeNotifyEvent.WaitFor; @@ -1494,6 +2404,16 @@ procedure TNodeNotifyEvents.NotifyOperationsChanged; if Assigned(FThreadSafeNodeNotifyEvent) then FThreadSafeNodeNotifyEvent.FNotifyOperationsChanged := true; end; +procedure TNodeNotifyEvents.NotifySynchronizationChanged; +begin + if not Assigned(FThreadSafeNodeNotifyEvent) then Exit; + {$IFDEF FPC} + InterlockedExchange(FThreadSafeNodeNotifyEvent.FNotifySynchronizationChanged,1); + {$ELSE} + TInterlocked.Exchange(FThreadSafeNodeNotifyEvent.FNotifySynchronizationChanged,1); + {$ENDIF} +end; + procedure TNodeNotifyEvents.SetWatchKeys(AValue: TOrderedAccountKeysList); begin if FWatchKeys=AValue then Exit; @@ -1505,12 +2425,23 @@ procedure TNodeNotifyEvents.SetNode(const Value: TNode); if FNode=Value then exit; if Assigned(FNode) then begin FNode.RemoveFreeNotification(Self); - FNode.FNotifyList.Remove(Self); + FNode.FNotifyLock.AcquireWriteLock; + try + FNode.FNotifyList.Remove(Self); + finally + FNode.FNotifyLock.ReleaseWriteLock; + end; end; FNode := Value; if Assigned(FNode) then begin FNode.FreeNotification(Self); - FNode.FNotifyList.Add(Self); + FNode.FNotifyLock.AcquireWriteLock; + try + FNode.FNotifyList.Add(Self); + finally + FNode.FNotifyLock.ReleaseWriteLock; + end; + NotifySynchronizationChanged; end; end; @@ -1519,7 +2450,9 @@ procedure TNodeNotifyEvents.SetNode(const Value: TNode); procedure TThreadSafeNodeNotifyEvent.BCExecute; begin while (not Terminated) AND (Assigned(FNodeNotifyEvents)) do begin - if (FNotifyOperationsChanged) Or (FNotifyBlocksChanged) Or (FNodeNotifyEvents.FMessages.Count>0) then Synchronize(SynchronizedProcess); + if (FNotifyOperationsChanged) Or (FNotifyBlocksChanged) Or + (FNotifySynchronizationChanged<>0) Or + (FNodeNotifyEvents.FMessages.Count>0) then Synchronize(SynchronizedProcess); Sleep(100); end; end; @@ -1533,9 +2466,30 @@ constructor TThreadSafeNodeNotifyEvent.Create(ANodeNotifyEvents: TNodeNotifyEven procedure TThreadSafeNodeNotifyEvent.SynchronizedProcess; Var i : Integer; can_alert_keys : Boolean; + LStatus: TNodeSynchronizationStatus; + LNotifySynchronization: Boolean; begin + LNotifySynchronization := False; Try If (Terminated) Or (Not Assigned(FNodeNotifyEvents)) then exit; + // Clear first, then sample: a producer arriving during the copy leaves a + // notification for the next pass. No producer waits for these callbacks. + {$IFDEF FPC} + LNotifySynchronization := InterlockedExchange(FNotifySynchronizationChanged,0)<>0; + {$ELSE} + LNotifySynchronization := TInterlocked.Exchange(FNotifySynchronizationChanged,0)<>0; + {$ENDIF} + LStatus := Default(TNodeSynchronizationStatus); + if Assigned(FNodeNotifyEvents.FNode) then + LStatus := FNodeNotifyEvents.FNode.GetSynchronizationStatus; + // Pause queries before block/operation listeners run. On completion, retain + // their latest requests first, then resume exactly once with final inputs. + if LNotifySynchronization and (LStatus.State in [nssSynchronizing,nssStorageError]) and + Assigned(FNodeNotifyEvents.FOnSynchronizationChanged) then begin + DebugStep := 'Notify OnSynchronizationChanged'; + FNodeNotifyEvents.FOnSynchronizationChanged(FNodeNotifyEvents,LStatus); + end; + try can_alert_keys := False; if FNotifyBlocksChanged then begin FNotifyBlocksChanged := false; @@ -1571,8 +2525,22 @@ procedure TThreadSafeNodeNotifyEvent.SynchronizedProcess; end; end; end; + finally + // An unrelated refresh listener must not consume the final state flag + // and leave a grid held forever. Deliver completion even on that path. + if LNotifySynchronization and (not (LStatus.State in [nssSynchronizing,nssStorageError])) and + Assigned(FNodeNotifyEvents) and + Assigned(FNodeNotifyEvents.FOnSynchronizationChanged) then begin + DebugStep := 'Notify OnSynchronizationChanged'; + FNodeNotifyEvents.FOnSynchronizationChanged(FNodeNotifyEvents,LStatus); + end; + end; Except On E:Exception do begin + // A one-shot status-handler failure can recover without another producer + // input. Retry on the existing notifier, never on the network call stack. + if LNotifySynchronization and Assigned(FNodeNotifyEvents) then + FNodeNotifyEvents.NotifySynchronizationChanged; TLog.NewLog(lterror,ClassName,'Exception inside a Synchronized process: '+E.ClassName+':'+E.Message+' Step:'+DebugStep); end; End; @@ -1628,7 +2596,30 @@ destructor TThreadNodeNotifyNewBlock.Destroy; { TThreadNodeNotifyOperations } procedure TThreadNodeNotifyOperations.BCExecute; -begin +var LStart: TTickCount; LDelay: Cardinal; +begin + if Terminated then Exit; + if Assigned(FLeaseOwner) then begin + // The transferred connection lease pins both connection and owner. Keep + // the old randomized send delay, but let orderly shutdown wake it promptly. + LDelay := Random(3000); + LStart := TPlatform.GetTickCount; + while TPlatform.GetElapsedMilliseconds(LStart)=5000 then Exit; + end; + Exit; + end; Sleep(Random(3000)); // Delay 0..3 seconds to allow receive data and don't send if not necessary if TNetData.NetData.ConnectionLock(Self, FNetConnection, 5000) then begin try @@ -1640,49 +2631,164 @@ procedure TThreadNodeNotifyOperations.BCExecute; end; end; -constructor TThreadNodeNotifyOperations.Create(NetConnection: TNetConnection; MakeACopyOfOperationsHashTree: TOperationsHashTree); +constructor TThreadNodeNotifyOperations.Create(NetConnection: TNetConnection; + MakeACopyOfOperationsHashTree: TOperationsHashTree; ALeaseOwner: TNetData); begin FNetConnection := NetConnection; - FNetConnection.AddOperationsToBufferForSend(MakeACopyOfOperationsHashTree); + FLeaseOwner := ALeaseOwner; Inherited Create(True); + FreeOnTerminate := false; + FNetConnection.AddOperationsToBufferForSend(MakeACopyOfOperationsHashTree); FreeOnTerminate := true; Suspended:=False; end; destructor TThreadNodeNotifyOperations.Destroy; +var LLeaseOwner: TNetData; begin - inherited; + LLeaseOwner := FLeaseOwner; + try + inherited; + finally + FLeaseOwner := Nil; + if Assigned(LLeaseOwner) then LLeaseOwner.ReleaseDownloadConnection(FNetConnection); + end; end; { TSaveMempoolOperationsThread } -procedure TSaveMempoolOperationsThread.BCExecute; -var i : Integer; - LLocked : TPCOperationsComp; +constructor TSaveMempoolOperationsThread.Create(ANode: TNode); +begin + FNode := ANode; + FWorkAvailable := TEvent.Create(Nil,False,False,''); + inherited Create(True); +end; + +destructor TSaveMempoolOperationsThread.Destroy; +begin + inherited; + FWorkAvailable.Free; +end; + +procedure TSaveMempoolOperationsThread.Wake; begin - FPendingToSave := false; + FWorkAvailable.SetEvent; +end; + +procedure TSaveMempoolOperationsThread.Stop; +begin + Terminate; + Wake; +end; + +procedure TSaveMempoolOperationsThread.BCExecute; +var LLocked : TPCOperationsComp; + LSaveFailed, LFinalAttempt: Boolean; + LRetryReconciliation: Boolean; + LLastSave: TTickCount; LElapsed: Int64; LWait: Cardinal; + LLastReconciliationFailure: TTickCount; LRetryWait: Cardinal; + LFailureFileName: String; +begin + // A request can arrive before this thread is first scheduled. Do not clear + // it here. Dirty publication is atomic and is consumed before capture, so a + // later mutation cannot be hidden by completion of an older file write. + LLastSave := TPlatform.GetTickCount; + LRetryReconciliation := False; + LLastReconciliationFailure := LLastSave; repeat - if FPendingToSave then begin - LLocked := FNode.LockMempoolRead; + LFinalAttempt := Terminated; + LSaveFailed := False; + if (not Terminated) and ((not LRetryReconciliation) or + (TPlatform.GetElapsedMilliseconds(LLastReconciliationFailure)>=10000)) then begin try - FPendingToSave := False; - FNode.Bank.Storage.SavePendingBufferOperations(LLocked.OperationsHashTree); - finally - FNode.UnlockMempoolRead; + FNode.ReconcileMempoolIfReady; + LRetryReconciliation := False; + except + on E: Exception do begin + // Retain the original raw package and retry on this owner's existing + // timed event wait. A transient validator failure must not leave an + // offline node waiting forever; persistent failures must not spin. + LRetryReconciliation := True; + LLastReconciliationFailure := TPlatform.GetTickCount; + TLog.NewLog(lterror,ClassName,'Cannot reconcile pending operations: '+E.ClassName+': '+E.Message); + end; end; end; - // Wait 10 seconds prior to save updates on mempool - i := 0; - while (i<1000) and (Not Terminated) do begin - Sleep(10); - inc(i); + LElapsed := TPlatform.GetElapsedMilliseconds(LLastSave); + if (Terminated or (LElapsed>=10000)) and TakeSaveRequest then begin + try + // The saved package contains the complete original operation sequence, + // not the deferred account overlay. Saving it during catch-up or final + // shutdown is safe; the normal next startup revalidates that sequence. + FNode.FLockMempool.AcquireReadLock; + LLocked := FNode.FMemPoolOperationsComp; + try + FNode.Bank.Storage.SavePendingBufferOperations(LLocked.OperationsHashTree); + finally + FNode.UnlockMempoolRead; + end; + except + on E: Exception do begin + LSaveFailed := True; + LFailureFileName := 'pendingbuffer.ops'; + try + LFailureFileName := FNode.Bank.GetStorageFolder(FNode.Bank.Orphan)+PathDelim+'pendingbuffer.ops'; + except + // A diagnostic path lookup must not replace the original I/O failure. + end; + FNode.ReportStorageFailure(sfsPendingOperations,LFailureFileName,E); + Touch; + TLog.NewLog(lterror,ClassName,'Cannot save pending operations: '+E.ClassName+': '+E.Message); + end; + end; + LLastSave := TPlatform.GetTickCount; end; - until (false) or (Terminated); + // Termination during the quiet wait returns to the capture above first. + // Normal shutdown therefore flushes the latest completed operation batch. + // A failing final I/O attempt is logged rather than retried forever. + if Terminated and ((LSaveFailed and LFinalAttempt) or not HasSaveRequest) then Break; + // Existing ten-second persistence pacing is retained, but reconciliation + // is woken by real scope/connectivity events. No status polling timer is + // involved, and stopping wakes the owner to make its final complete save. + if HasSaveRequest then begin + LElapsed := TPlatform.GetElapsedMilliseconds(LLastSave); + if LElapsed>=10000 then LWait := 0 else LWait := 10000-Cardinal(LElapsed); + end else LWait := Cardinal(-1); + if LRetryReconciliation then begin + LElapsed := TPlatform.GetElapsedMilliseconds(LLastReconciliationFailure); + if LElapsed>=10000 then LRetryWait := 0 else LRetryWait := 10000-Cardinal(LElapsed); + if (LWait=Cardinal(-1)) or (LRetryWait0; + {$ELSE} + Result := TInterlocked.Exchange(FPendingToSave,0)<>0; + {$ENDIF} +end; + +function TSaveMempoolOperationsThread.HasSaveRequest: Boolean; +begin + {$IFDEF FPC} + Result := InterlockedCompareExchange(FPendingToSave,0,0)<>0; + {$ELSE} + Result := TInterlocked.CompareExchange(FPendingToSave,0,0)<>0; + {$ENDIF} end; initialization diff --git a/src/core/UOpTransaction.pas b/src/core/UOpTransaction.pas index ae00de962..e6c3f6d97 100644 --- a/src/core/UOpTransaction.pas +++ b/src/core/UOpTransaction.pas @@ -351,7 +351,7 @@ interface function N_Operation : Cardinal; override; procedure AffectedAccounts(list : TOrderedList); override; function OperationAmountByAccount(account : Cardinal) : Int64; override; - Constructor CreateOpData( ACurrentProtocol : word; account_signer, account_sender, account_target : Cardinal; signer_key:TECPrivateKey; n_operation : Cardinal; dataType, dataSequence : Word; AGUID : TGUID; amount, fee : UInt64; const payload: TOperationPayload); + Constructor CreateOpData( ACurrentProtocol : word; account_signer, account_sender, account_target : Cardinal; signer_key:TECPrivateKey; n_operation : Cardinal; dataType, dataSequence : Word; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AGUID : TGUID; amount, fee : UInt64; const payload: TOperationPayload); Property Data : TOpDataData read FData; Function toString : String; Override; Function GetDigestToSign : TRawBytes; override; @@ -519,7 +519,12 @@ function TOpChangeAccountInfo.DoOperation(AccountPreviousUpdatedBlock : TAccount exit; end; account_signer := AccountTransaction.Account(FData.account_signer); - account_target := AccountTransaction.Account(FData.account_target); + // No mutation occurs between these reads. A self-change validates the same + // account value twice; keep that exact record instead of rereading safebox. + if FData.account_signer = FData.account_target then + account_target := account_signer + else + account_target := AccountTransaction.Account(FData.account_target); if ((account_signer.n_operation+1)<>FData.n_operation) then begin errors := 'Invalid n_operation'; Exit; @@ -2849,8 +2854,10 @@ function TOpData.OperationAmountByAccount(account: Cardinal): Int64; constructor TOpData.CreateOpData(ACurrentProtocol : word; account_signer, account_sender, account_target: Cardinal; signer_key: TECPrivateKey; n_operation: Cardinal; - dataType, dataSequence: Word; AGUID : TGUID; amount, fee: UInt64; const payload: TOperationPayload); + dataType, dataSequence: Word; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AGUID : TGUID; amount, fee: UInt64; const payload: TOperationPayload); begin + // FPC 3.2.2 for aarch64-darwin stops with internal error 2014121702 when this + // 16-byte record arrives by value on the stack, so it is passed by reference. Inherited Create(ACurrentProtocol); FData.account_sender:=account_sender; FData.account_signer:=account_signer; diff --git a/src/core/UPCAbstractMem.pas b/src/core/UPCAbstractMem.pas index 54767539e..be36fc39d 100644 --- a/src/core/UPCAbstractMem.pas +++ b/src/core/UPCAbstractMem.pas @@ -104,9 +104,11 @@ TPCAbstractMem = class FFileName : String; FAbstractMem: TAbstractMem; FCheckingThread : TPCAbstractMemCheckThread; - FLockAbstractMem : TPCCriticalSection; + // Protects checker lifetime; backend file/cache guards still own their data. + FLockAbstractMem : TReadWriteLock; FStats : TPCAbstractMemStats; + FInitialized : Boolean; FBlocks: TPCAbstractMemListBlocks; FAccounts: TPCAbstractMemListAccounts; @@ -142,6 +144,10 @@ TPCAbstractMem = class procedure OnCacheMemFlushedCache(const ASender : TCacheMem; const AProcessDesc : String; AElapsedMilis: Int64); procedure OnCacheMemLog(ASender : TObject; const ALog : String); protected + // Separate writing, durability and publication without exposing temporary ownership. + function CreateCheckpointStream(const AFileName: String): TFileStream; virtual; + procedure FlushCheckpointStream(AStream: TFileStream); virtual; + procedure PublishCheckpointFile(const ATemporaryFileName, ADestinationFileName: String); virtual; procedure UpgradeAbstractMemVersion(const ACurrentHeaderVersion : Integer); function DoGetAccount(AAccountNumber : Integer; var AAccount : TAccount) : Boolean; public @@ -190,7 +196,7 @@ TPCAbstractMem = class implementation -uses UAccounts; +uses UAccounts{$IFDEF MSWINDOWS}, Windows{$ENDIF}; const CT_PCAbstractMem_FileVersion = 100; @@ -199,7 +205,11 @@ implementation function _AccountCache_Comparision(const Left, Right: TAccountCache.PAVLCacheMemData): Integer; begin // Compare only by data.account number (not for content) - Result := Left^.data.account - Right^.data.account; + // Cardinal subtraction can underflow or truncate the signed comparison, + // including ordinary account 0 versus 5 in a checked Delphi build. + if Left^.data.account < Right^.data.account then Result := -1 + else if Left^.data.account > Right^.data.account then Result := 1 + else Result := 0; end; { TPCAbstractMemBytesBuffer32Safebox } @@ -266,7 +276,7 @@ procedure TPCAbstractMemBytesBuffer32Safebox.Flush; function TPCAbstractMem.CheckConsistency(AReport: TStrings) : Boolean; begin AReport.Clear; - FLockAbstractMem.Acquire; + FLockAbstractMem.AcquireWriteLock; Try if Assigned(FCheckingThread) then begin FCheckingThread.Terminate; @@ -284,7 +294,7 @@ function TPCAbstractMem.CheckConsistency(AReport: TStrings) : Boolean; FCheckingThread.WaitFor; FreeAndNil(FCheckingThread); Finally - FLockAbstractMem.Release; + FLockAbstractMem.ReleaseWriteLock; End; Result := AReport.Count=0; end; @@ -337,6 +347,7 @@ function TPCAbstractMem.DoInit(out AIsNewStructure : Boolean) : Boolean; i : Integer; LHeaderVersion : UInt32; begin + FInitialized := False; // Free FreeAndNil(FBlocks); FreeAndNil(FAccounts); @@ -446,6 +457,14 @@ function TPCAbstractMem.DoInit(out AIsNewStructure : Boolean) : Boolean; FBlocks.FPCAbstractMem := Self; FAccounts := TPCAbstractMemListAccounts.Create( FAbstractMem, LZoneAccounts, 100000, Self.UseCacheOnAbstractMemLists); + if FAbstractMem.ReadOnly then begin + // Counts come from the candidate, not trusted runtime state. Reject impossible + // values before integer multiplication or allocation of its block-hash buffer. + if (FBlocks.Count<0) or (FAccounts.Count<0) or + (Int64(FAccounts.Count)<>Int64(FBlocks.Count)*CT_AccountsPerBlock) or + (Int64(FBlocks.Count)*32>FAbstractMem.NextAvailablePos) then + raise EPCAbstractMem.Create('Invalid safebox block/account counts in '+FFileName); + end; FAccountsNames := TPCAbstractMemListAccountNames.Create( FAbstractMem, LZoneAccountsNames, False, 31, _TComparison_TAccountNameInfo); FAccountsNames.FPCAbstractMem := Self; @@ -476,7 +495,7 @@ function TPCAbstractMem.DoInit(out AIsNewStructure : Boolean) : Boolean; Move(LHeaderVersion, LHeader[96],4); FAbstractMem.Write(LZone.position,LHeader[0],Length(LHeader)); end; - + FInitialized := True; end; procedure TPCAbstractMem.EraseData; @@ -492,6 +511,7 @@ constructor TPCAbstractMem.Create(const ASafeboxFileName: string; AReadOnly: boo var LIsNewStructure : Boolean; LCacheMem : TCacheMem; + LProbe : TFileStream; begin FStats.Clear; @@ -505,7 +525,7 @@ constructor TPCAbstractMem.Create(const ASafeboxFileName: string; AReadOnly: boo FBufferBlocksHash:= Nil; FCheckingThread := Nil; - FLockAbstractMem := TPCCriticalSection.Create(Self.ClassName); + FLockAbstractMem := TReadWriteLock.Create(Self.ClassName,lrpSupportsRecursion); FAccountCache := TAccountCache.Create(50000,_AccountCache_Comparision); FSavingNewSafeboxMode := False; @@ -529,7 +549,24 @@ constructor TPCAbstractMem.Create(const ASafeboxFileName: string; AReadOnly: boo End; end; + if AReadOnly and (FFileName<>'') then begin + // Reject invalid/truncated allocation headers before following any offsets + // or allocating a block-hash buffer from a damaged candidate's counters. + if Not FAbstractMem.HeaderInitialized then + raise EPCAbstractMem.Create('Invalid AbstractMem header in '+FFileName); + LProbe := TFileStream.Create(FFileName,fmOpenRead+fmShareDenyNone); + try + if FAbstractMem.NextAvailablePos>LProbe.Size then + raise EPCAbstractMem.Create('Truncated safebox allocation data in '+FFileName); + finally + LProbe.Free; + end; + end; DoInit(LIsNewStructure); + // A read-only probe must reject an unrecognized file, never treat it as an + // empty safebox. Mark initialization only after every owned structure exists. + if AReadOnly and (FFileName<>'') and LIsNewStructure then + raise EPCAbstractMem.Create('Invalid safebox header in '+FFileName); // if (Not AReadOnly) and (((BlocksCount>0) And (ASafeboxFileName<>'')) Or (Not LIsNewStructure)) then begin TLog.NewLog(ltdebug,ClassName,Format('Opened PascalCoin AbstractMem File with %d blocks %d accounts %s aggregated hashrate and buffer %d size (%d blocks) at file: %s', @@ -539,54 +576,74 @@ constructor TPCAbstractMem.Create(const ASafeboxFileName: string; AReadOnly: boo destructor TPCAbstractMem.Destroy; var LFile : TFileStream; + LCheckingThread : TPCAbstractMemCheckThread; begin - FLockAbstractMem.Acquire; + LCheckingThread := Nil; try - if Assigned(FCheckingThread) then begin - FCheckingThread.Terminate; + if Assigned(FLockAbstractMem) then begin + FLockAbstractMem.AcquireWriteLock; + try + LCheckingThread := FCheckingThread; + if Assigned(LCheckingThread) then LCheckingThread.Terminate; + finally + FLockAbstractMem.ReleaseWriteLock; + end; + end; + // The checker borrows this object and takes its lock when it is destroyed. + // Join outside that lock before retiring any of the data it can still read. + if Assigned(LCheckingThread) then begin + LCheckingThread.WaitFor; + LCheckingThread.Free; + end; + // Failed constructors are destroyed automatically. Their partial structures + // cannot be flushed and must not replace the original validation exception. + try + if FInitialized and Assigned(FAbstractMem) then FlushCache; + if FInitialized and (FFileName<>'') and (FAbstractMem is TMem) and (Not FAbstractMem.ReadOnly) then begin + LFile := TFileStream.Create(FFileName,fmCreate); + try + FAbstractMem.SaveToStream(LFile); + finally + LFile.Free; + end; + end; + except + on E: Exception do begin + // Explicit Write/Flush/Save still report failure to their caller. Cleanup + // must release ownership without throwing a second persistence error. + if Not ((E is EStreamError) or (E is EOSError) or (E is EFileMem) or (E is EAbstractMem)) then raise; + TLog.NewLog(ltError,ClassName,'Cannot flush safebox during cleanup '+FFileName+' ('+E.ClassName+'): '+E.Message); + end; end; finally - FLockAbstractMem.Release; - end; - - FlushCache; - FreeAndNil(FAccountCache); - FreeAndNil(FBlocks); - FreeAndNil(FAccounts); - FreeAndNil(FAccountsNames); - FreeAndNil(FAccountKeys); - FreeAndNil(FBufferBlocksHash); - FreeAndNil(FAggregatedHashrate); - FreeAndNil(FAccountsOrderedByUpdatedBlock); - FreeAndNil(FAccountsOrderedBySalePrice); - if (FFileName<>'') And (FAbstractMem is TMem) And (Not FAbstractMem.ReadOnly) then begin - LFile := TFileStream.Create(FFileName,fmCreate); + FreeAndNil(FAccountCache); + FreeAndNil(FBlocks); + FreeAndNil(FAccounts); + FreeAndNil(FAccountsNames); + FreeAndNil(FAccountKeys); + FreeAndNil(FBufferBlocksHash); + FreeAndNil(FAggregatedHashrate); + FreeAndNil(FAccountsOrderedByUpdatedBlock); + FreeAndNil(FAccountsOrderedBySalePrice); try - LFile.Size := 0; - LFile.Position := 0; - FAbstractMem.SaveToStream(LFile); + FreeAndNil(FAbstractMem); finally - LFile.Free; + FreeAndNil(FLockAbstractMem); + inherited Destroy; end; end; - FreeAndNil(FAbstractMem); - - - FreeAndNil(FLockAbstractMem); - - inherited Destroy; end; procedure TPCAbstractMem.DoCheck; begin if IsChecking then Exit; - FLockAbstractMem.Acquire; + FLockAbstractMem.AcquireWriteLock; Try if Not Assigned(FCheckingThread) then begin FCheckingThread := TPCAbstractMemCheckThread.Create(Self); end; Finally - FLockAbstractMem.Release; + FLockAbstractMem.ReleaseWriteLock; End; end; @@ -609,52 +666,101 @@ procedure TPCAbstractMem.FlushCache; LBigNum := FAggregatedHashrate.RawValue.ToSerialized; FAbstractMem.Write( FZoneAggregatedHashrate.position, LBigNum[0], Length(LBigNum) ); if FAbstractMem is TFileMem then begin - TFileMem(FAbstractMem).FlushCache; + if Not TFileMem(FAbstractMem).FlushCache then + raise EWriteError.Create('Cannot flush safebox file: '+FFileName); end; Inc(FStats.FlushesCount); Inc(Fstats.FlushesMillis, TPlatform.GetElapsedMilliseconds(Ltc) ); TLog.NewLog(ltdebug,Self.ClassName,Format('AbstractMem Safebox flushed in %.2f seconds',[TPlatform.GetElapsedMilliseconds(Ltc)/1000])); end; -Procedure DoCopyFile(const ASource, ADest : String); -var LSourceFS, LDestFS : TFileStream; -Begin - if Not FileExists(ASource) then Raise Exception.Create('Source file not found: '+ASource); - LSourceFS := TFileStream.Create(ASource,fmOpenRead+fmShareDenyNone); - try - LSourceFS.Position:=0; - LDestFS := TFileStream.Create(ADest,fmCreate+fmShareDenyWrite); - try - LDestFS.Size:=0; - LDestFS.CopyFrom(LSourceFS,LSourceFS.Size); - finally - LDestFS.Free; - end; - finally - LSourceFS.Free; - end; +function TPCAbstractMem.CreateCheckpointStream(const AFileName: String): TFileStream; +begin + Result := TFileStream.Create(AFileName,fmCreate+fmShareExclusive); end; +procedure TPCAbstractMem.FlushCheckpointStream(AStream: TFileStream); +begin + {$IFDEF FPC} + if Not FileFlush(AStream.Handle) then RaiseLastOSError; + {$ELSE} + if Not FlushFileBuffers(AStream.Handle) then RaiseLastOSError; + {$ENDIF} +end; + +procedure TPCAbstractMem.PublishCheckpointFile(const ATemporaryFileName, ADestinationFileName: String); +const CT_CheckpointMoveWriteThrough = $00000008; // MOVEFILE_WRITE_THROUGH; missing from older FPC headers. +begin + // The sibling file is already complete, validated and closed. Replacement is + // one same-volume operation; never delete the previous checkpoint first. + {$IFDEF MSWINDOWS} + if Not MoveFileExW(PWideChar(UnicodeString(ATemporaryFileName)),PWideChar(UnicodeString(ADestinationFileName)),MOVEFILE_REPLACE_EXISTING Or CT_CheckpointMoveWriteThrough) then RaiseLastOSError; + {$ELSE} + if Not SysUtils.RenameFile(ATemporaryFileName,ADestinationFileName) then RaiseLastOSError; + {$ENDIF} +end; procedure TPCAbstractMem.SaveToFile(const ASaveToFileName: String); -var LFile : TFileStream; +var LSource, LTemporary : TFileStream; + LCandidate : TPCAbstractMem; + LFileName, LTemporaryName : String; + LTemporaryOwned : Boolean; + LExpectedSize : Int64; + LID : TGUID; begin FlushCache; - // - ForceDirectories(ExtractFileDir(ASaveToFileName)); - if FileExists(ASaveToFileName) then DeleteFile(ASaveToFileName); - // - if (FAbstractMem is TFileMem) then begin - DoCopyFile(TFileMem(FAbstractMem).FileName,ASaveToFileName); - end else begin - LFile := TFileStream.Create(ASaveToFileName,fmCreate); - try - LFile.Size := 0; - LFile.Position := 0; - FAbstractMem.SaveToStream(LFile); - finally - LFile.Free; + if Trim(ASaveToFileName)='' then raise EWriteError.Create('Cannot save a safebox without a filename'); + LFileName := ExpandFileName(ASaveToFileName); + if (FFileName<>'') and SameFileName(ExpandFileName(FFileName),LFileName) then Exit; + if Not ForceDirectories(ExtractFileDir(LFileName)) then + raise EWriteError.Create('Cannot create safebox directory: '+ExtractFileDir(LFileName)); + + LSource := Nil; + LTemporary := Nil; + LCandidate := Nil; + LTemporaryOwned := False; + try + repeat + if CreateGUID(LID)<>0 then raise EWriteError.Create('Cannot create safebox temporary file identity'); + LTemporaryName := LFileName+'.'+GUIDToString(LID)+'.tmp'; + until Not FileExists(LTemporaryName); + // Only this unique sibling is disposable. The live safebox and the last + // complete checkpoint remain intact on write, flush or validation failure. + LTemporaryOwned := True; + LTemporary := CreateCheckpointStream(LTemporaryName); + if FAbstractMem is TFileMem then begin + LSource := TFileStream.Create(TFileMem(FAbstractMem).FileName,fmOpenRead+fmShareDenyNone); + LExpectedSize := LSource.Size; + if LTemporary.CopyFrom(LSource,LExpectedSize)<>LExpectedSize then + raise EWriteError.Create('Incomplete safebox copy to '+LTemporaryName); + end else begin + LExpectedSize := FAbstractMem.NextAvailablePos; + FAbstractMem.SaveToStream(LTemporary); end; + if LTemporary.Size<>LExpectedSize then + raise EWriteError.Create('Incomplete safebox write to '+LTemporaryName); + FlushCheckpointStream(LTemporary); + FreeAndNil(LTemporary); + FreeAndNil(LSource); + + // Reopen exactly what will be published. A structurally valid empty safebox + // is a real checkpoint too; malformed or incomplete files never qualify. + LCandidate := TPCAbstractMem.Create(LTemporaryName,True); + if (LCandidate.BlocksCount<>BlocksCount) or (LCandidate.AccountsCount<>AccountsCount) or + (LCandidate.BufferBlocksHash.Length<>FBufferBlocksHash.Length) then + raise EPCAbstractMem.Create('Safebox checkpoint counts differ from the source: '+LTemporaryName); + if (FBufferBlocksHash.Length>0) and + (Not CompareMem(LCandidate.BufferBlocksHash.Memory,FBufferBlocksHash.Memory,FBufferBlocksHash.Length)) then + raise EPCAbstractMem.Create('Safebox checkpoint block hashes differ from the source: '+LTemporaryName); + FreeAndNil(LCandidate); + PublishCheckpointFile(LTemporaryName,LFileName); + LTemporaryOwned := False; + finally + LCandidate.Free; + LSource.Free; + LTemporary.Free; + if LTemporaryOwned and FileExists(LTemporaryName) and (Not SysUtils.DeleteFile(LTemporaryName)) then + TLog.NewLog(ltError,ClassName,'Cannot remove incomplete safebox temporary file '+LTemporaryName+': '+SysErrorMessage({$IFDEF FPC}GetLastOSError{$ELSE}GetLastError{$ENDIF})); end; end; @@ -723,13 +829,31 @@ class function TPCAbstractMem.AnalyzeFile(const ASafeboxFileName: string; var AB begin ABlocksCount := 0; Result := False; - if Not FileExists(ASafeboxFileName) then Exit(False); - LPCAbstractMem := TPCAbstractMem.Create(ASafeboxFileName,True); + if Not FileExists(ASafeboxFileName) then Exit; + LPCAbstractMem := Nil; try - ABlocksCount := LPCAbstractMem.BlocksCount; - Result := (ABlocksCount>0) And (LPCAbstractMem.AccountsCount = ABlocksCount * CT_AccountsPerBlock); - finally - LPCAbstractMem.Free; + try + LPCAbstractMem := TPCAbstractMem.Create(ASafeboxFileName,True); + ABlocksCount := LPCAbstractMem.BlocksCount; + Result := (ABlocksCount>=0) and (Int64(LPCAbstractMem.AccountsCount) = Int64(ABlocksCount) * CT_AccountsPerBlock); + if Not Result then begin + TLog.NewLog(ltError,'TPCAbstractMem',Format('Cannot use safebox candidate %s: inconsistent block/account counts %d/%d',[ASafeboxFileName,ABlocksCount,LPCAbstractMem.AccountsCount])); + ABlocksCount := 0; + end; + finally + LPCAbstractMem.Free; + end; + except + on E: Exception do begin + // Corrupt or unreadable candidates must not hide another intact snapshot. + // Resource exhaustion, cancellation and programmer faults still propagate. + if Not ((E is EPCAbstractMem) or (E is EAbstractMem) or (E is EFileMem) or + (E is EAbstractMemTList) or (E is EAbstractMemBTree) or (E is EAbstractBTree) or + (E is EStreamError) or (E is EOSError)) then raise; + ABlocksCount := 0; + Result := False; + TLog.NewLog(ltError,'TPCAbstractMem','Cannot use safebox candidate '+ASafeboxFileName+' ('+E.ClassName+'): '+E.Message); + end; end; end; @@ -903,12 +1027,12 @@ function TPCAbstractMem.GetStatsReport(AClearStats: Boolean): String; function TPCAbstractMem.IsChecking: Boolean; begin - Result := Assigned(TPCThread.GetThreadByClass(TPCAbstractMemCheckThread,Nil)); - FLockAbstractMem.Acquire; + // This getter only copies worker presence; creation and retirement use write. + FLockAbstractMem.AcquireReadLock; Try Result := Assigned(FCheckingThread); Finally - FLockAbstractMem.Release; + FLockAbstractMem.ReleaseReadLock; End; end; @@ -1245,11 +1369,11 @@ constructor TPCAbstractMemCheckThread.Create(APCAbstractMem: TPCAbstractMem); begin FPCAbstractMem := APCAbstractMem; - FPCAbstractMem.FLockAbstractMem.Acquire; + FPCAbstractMem.FLockAbstractMem.AcquireWriteLock; try FPCAbstractMem.FCheckingThread := Self; finally - FPCAbstractMem.FLockAbstractMem.Release; + FPCAbstractMem.FLockAbstractMem.ReleaseWriteLock; end; FErrorsCount := 0; @@ -1262,11 +1386,11 @@ constructor TPCAbstractMemCheckThread.Create(APCAbstractMem: TPCAbstractMem); destructor TPCAbstractMemCheckThread.Destroy; begin - FPCAbstractMem.FLockAbstractMem.Acquire; + FPCAbstractMem.FLockAbstractMem.AcquireWriteLock; try FPCAbstractMem.FCheckingThread := Nil; finally - FPCAbstractMem.FLockAbstractMem.Release; + FPCAbstractMem.FLockAbstractMem.ReleaseWriteLock; end; FErrors.Free; inherited Destroy; diff --git a/src/core/UPCAbstractMemAccountKeys.pas b/src/core/UPCAbstractMemAccountKeys.pas index 19f185906..61538593e 100644 --- a/src/core/UPCAbstractMemAccountKeys.pas +++ b/src/core/UPCAbstractMemAccountKeys.pas @@ -7,7 +7,7 @@ interface {$ENDIF} uses Classes, SysUtils, - SyncObjs, + UThread, UAbstractMem, UFileMem, UAbstractMemTList, UAbstractBTree, UAbstractAVLTree, UPCDataTypes, UBaseTypes, UAVLCache, UAbstractMemBTree, UOrderedList, @@ -41,6 +41,9 @@ TAbstractMemAccountKeyNode = record public Constructor Create(AAbstractMem : TAbstractMem; const AInitialZone : TAMZone; AUseCache : Boolean); reintroduce; function Get(Index : Integer) : Cardinal; + // Snapshot a range in one B-tree traversal. Repeated Get(Index) calls each + // walk from the lowest item and are quadratic for a complete account list. + function FillList(AStartIndex, ACount : Integer; const AList : TList) : Integer; reintroduce; overload; End; TAccountKeyByPosition = record @@ -59,7 +62,7 @@ TAccountKeyByPosition = record TPCAbstractMemAccountKeys = Class(TAVLAbstractTree) private - FAccountKeysLock : TCriticalSection; + FAccountKeysLock : TReadWriteLock; FAbstractMem : TAbstractMem; FPointerToRootPosition : TAbstractMemPosition; FRootPosition : TAbstractMemPosition; @@ -156,14 +159,16 @@ procedure TAbstractMemAccountKeyNode.ReadFromMem(AMyPosition: TAbstractMemPositi LRawSize := 0; Move(LBytes[6],LRawSize,2); SetLength(Self.accountKey.x,LRawSize); - if AAbstractMem.Read(AMyPosition + i,Self.accountKey.x[0],Length(Self.accountKey.x))<>Length(Self.accountKey.x) then raise EPCAbstractMemAccountKeys.Create(Format('Not enough data to read AccountKeyNode.x at %d',[AMyPosition])); + if LRawSize > 0 then + if AAbstractMem.Read(AMyPosition + i,Self.accountKey.x[0],Length(Self.accountKey.x))<>Length(Self.accountKey.x) then raise EPCAbstractMemAccountKeys.Create(Format('Not enough data to read AccountKeyNode.x at %d',[AMyPosition])); // Read next inc(i,LRawSize); LRawSize := 0; if AAbstractMem.Read(AMyPosition + i,LRawSize,2)<>2 then raise EPCAbstractMemAccountKeys.Create(Format('Not enough data to read AccountKeyNode.Length(y) at %d',[AMyPosition])); SetLength(Self.accountKey.y,LRawSize); inc(i,2); - if AAbstractMem.Read(AMyPosition + i,Self.accountKey.y[0],Length(Self.accountKey.y))<>Length(Self.accountKey.y) then raise EPCAbstractMemAccountKeys.Create(Format('Not enough data to read AccountKeyNode.y at %d',[AMyPosition])); + if LRawSize > 0 then + if AAbstractMem.Read(AMyPosition + i,Self.accountKey.y[0],Length(Self.accountKey.y))<>Length(Self.accountKey.y) then raise EPCAbstractMemAccountKeys.Create(Format('Not enough data to read AccountKeyNode.y at %d',[AMyPosition])); end; function TAbstractMemAccountKeyNode.ToString: String; @@ -230,7 +235,7 @@ function _AccountKeyByPositionCache_Comparision(const Left, Right: TPCAccountKey constructor TPCAbstractMemAccountKeys.Create(AAbstractMem: TAbstractMem; APointerToRootPosition : TAbstractMemPosition; AUseCacheOnAbstractMemLists : Boolean); begin - FAccountKeysLock := TCriticalSection.Create; + FAccountKeysLock := TReadWriteLock.Create('TPCAbstractMemAccountKeys.AccountKeys',lrpSupportsRecursion); FAbstractMem := AAbstractMem; FPointerToRootPosition := APointerToRootPosition; FRootPosition := 0; @@ -265,7 +270,9 @@ procedure TPCAbstractMemAccountKeys.GetAccountsUsingKey( i : TAbstractMemPosition; begin AList.Clear; - FAccountKeysLock.Acquire; + // The nested lookup fills the key cache. It must recurse as a writer even + // though this outer method presents an account-list read to its caller. + FAccountKeysLock.AcquireWriteLock; try Lautk := GetAccountsUsingThisKey(AAccountKey); if Assigned(Lautk) then begin @@ -276,7 +283,7 @@ procedure TPCAbstractMemAccountKeys.GetAccountsUsingKey( end; end; finally - FAccountKeysLock.Release; + FAccountKeysLock.ReleaseWriteLock; end; end; @@ -287,7 +294,7 @@ function TPCAbstractMemAccountKeys.GetAccountsUsingThisKey(const AAccountKey: TA LP : TAccountKeyByPosition; begin Result := TAccountsUsingThisKey_BlackHole.GetInstance; - FAccountKeysLock.Acquire; + FAccountKeysLock.AcquireWriteLock; try LNode.Clear; LNode.accountKey := AAccountKey; @@ -308,7 +315,7 @@ function TPCAbstractMemAccountKeys.GetAccountsUsingThisKey(const AAccountKey: TA end; Result := LP.accountsUsingThisKey; finally - FAccountKeysLock.Release; + FAccountKeysLock.ReleaseWriteLock; end; end; @@ -322,7 +329,7 @@ function TPCAbstractMemAccountKeys.GetKeyAtPosition(APosition: TAbstractMemPosit LP, LPFound : TAccountKeyByPosition; LAccZone : TAMZone; begin - FAccountKeysLock.Acquire; + FAccountKeysLock.AcquireWriteLock; try LP.Clear; LP.position := APosition; @@ -341,7 +348,7 @@ function TPCAbstractMemAccountKeys.GetKeyAtPosition(APosition: TAbstractMemPosit FAccountKeyByPositionCache.Add(LP); // Add to cache! end; finally - FAccountKeysLock.Release; + FAccountKeysLock.ReleaseWriteLock; end; end; @@ -364,7 +371,7 @@ function TPCAbstractMemAccountKeys.GetPositionOfKeyAndAddAccount(const AAccountK Lacckutk : TAccountsUsingThisKey; LAccKeyByPos : TAccountKeyByPosition; begin - FAccountKeysLock.Acquire; + FAccountKeysLock.AcquireWriteLock; try LNode.Clear; LNode.accountKey := AAccountKey; @@ -407,7 +414,7 @@ function TPCAbstractMemAccountKeys.GetPositionOfKeyAndAddAccount(const AAccountK Result := LNode.myPosition; finally - FAccountKeysLock.Release; + FAccountKeysLock.ReleaseWriteLock; end; end; @@ -419,7 +426,7 @@ function TPCAbstractMemAccountKeys.GetPositionOfKeyAndRemoveAccount( Lacckutk : TAccountsUsingThisKey; LAccKeyByPos : TAccountKeyByPosition; begin - FAccountKeysLock.Acquire; + FAccountKeysLock.AcquireWriteLock; try LNode.Clear; LNode.accountKey := AAccountKey; @@ -449,7 +456,7 @@ function TPCAbstractMemAccountKeys.GetPositionOfKeyAndRemoveAccount( LAccKeyByPos.accountsUsingThisKey.Delete( AAccountNumber ); end; finally - FAccountKeysLock.Release; + FAccountKeysLock.ReleaseWriteLock; end; end; @@ -545,6 +552,36 @@ function TAccountsUsingThisKey.Get(Index: Integer): Cardinal; Result := i; end; +function TAccountsUsingThisKey.FillList(AStartIndex, ACount: Integer; + const AList: TList): Integer; +var LPositions : TList; + i, LCount : Integer; +begin + if not Assigned(AList) then raise EArgumentException.Create('Account number destination list is nil'); + if (AStartIndex<0) or (ACount<0) then + raise EArgumentOutOfRangeException.Create('Account number range is negative'); + AList.Clear; + Result := 0; + if ACount=0 then Exit; + LPositions := TList.Create; + try + // The inherited cursor may include one extra item for a bounded range. + // Honor the requested count without restarting traversal for every index. + inherited FillList(AStartIndex,ACount,LPositions); + LCount := LPositions.Count; + if LCount>ACount then LCount := ACount; + AList.Capacity := LCount; + for i := 0 to LCount-1 do begin + if (LPositions[i]<0) or (LPositions[i]>High(Cardinal)) then + raise EPCAbstractMemAccountKeys.Create('Account number outside Cardinal range'); + AList.Add(Cardinal(LPositions[i])); + end; + Result := AList.Count; + finally + LPositions.Free; + end; +end; + function TAccountsUsingThisKey.NodeDataToString(const AData: TAbstractMemPosition): String; begin Result := IntToStr(AData); diff --git a/src/core/UPCAccountsOrdenations.pas b/src/core/UPCAccountsOrdenations.pas index 66d54ad1d..26bafb8c0 100644 --- a/src/core/UPCAccountsOrdenations.pas +++ b/src/core/UPCAccountsOrdenations.pas @@ -198,20 +198,23 @@ constructor TAccountsOrderedBySalePrice.Create({$IFDEF USE_ABSTRACTMEM}AAbstract function TAccountsOrderedBySalePrice.DoCompareData(const ALeftData, ARightData: TAbstractMemPosition): Integer; var LLeftAccount, LRightAccount : TAccount; - LopResult : Int64; + LLeftPrice, LRightPrice : UInt64; begin if (ALeftData = ARightData) then Exit(0); FCallReturnAccount(ARightData,LRightAccount); + LRightPrice := LRightAccount.accountInfo.price; if ((FSearching_AccountNumber>=0) And (ALeftData=FSearching_AccountNumber)) then begin - LopResult := FSearching_AccountInfo.price - LRightAccount.accountInfo.price; + LLeftPrice := FSearching_AccountInfo.price; end else begin FCallReturnAccount(ALeftData,LLeftAccount); - LopResult := LLeftAccount.accountInfo.price - LRightAccount.accountInfo.price; + LLeftPrice := LLeftAccount.accountInfo.price; end; - if LopResult<0 then Result := -1 - else if LopResult>0 then Result := 1 - else Result := ALeftData - ARightData; + // Prices span UInt64: subtraction can overflow or reverse their ordering. + if LLeftPriceLRightPrice then Result := 1 + else if ALeftData; protected + function CreateChunkThread(ABlockStart, ACount : Cardinal) : TDownloadSBThread; virtual; + procedure WaitForChunkThread(AThread : TThread); virtual; public constructor Create; destructor Destroy; override; @@ -133,18 +137,22 @@ procedure TPCDownloadSafebox.TDownloadSBThread.BCExecute; FOwner.FChunks.LockList; try if TNetData.NetData.GetConnection(Random(TNetData.NetData.ConnectionsCountAll),LConnection) then begin - if Assigned(LConnection) And (Not LConnection.Connected) then LConnection := Nil; + // ConnectionLock validates and pins this raw registry candidate before + // dereferencing it; a disconnected peer may otherwise retire here. + if not TNetData.NetData.ConnectionLock(Self,LConnection,100) then + LConnection := Nil; end else LConnection := Nil; - if Assigned(LConnection) then begin - if TNetData.NetData.ConnectionLock(Self,LConnection,100) then begin - TNetData.NetData.ConnectionUnlock(LConnection); - end else LConnection := Nil; - end; finally FOwner.FChunks.UnlockList; end; if Assigned(LConnection) then begin - if DownloadSafebox(LConnection,FOwner.FSavedSafeboxHighOperationBlock.initial_safe_box_hash,Self.FStream) then Break; + try + // Retain both the request lock and its lifetime lease through response + // validation, including when a replacement peer disconnects mid-chunk. + if DownloadSafebox(LConnection,FOwner.FSavedSafeboxHighOperationBlock.initial_safe_box_hash,Self.FStream) then Break; + finally + TNetData.NetData.ConnectionUnlock(LConnection); + end; end; Sleep(100); until Terminated; @@ -182,24 +190,45 @@ constructor TPCDownloadSafebox.Create; destructor TPCDownloadSafebox.Destroy; var i : Integer; Ll : TList; + LThreads : TList; begin - Ll := FChunks.LockList; + LThreads := TList.Create; Try - for i:=0 to Ll.Count-1 do begin - if assigned(Ll.Items[i].Thread) then begin - Ll.Items[i].Thread.Terminate; - Ll.Items[i].Thread.WaitFor; - Ll.Items[i].Thread.Free; + Ll := FChunks.LockList; + Try + for i:=0 to Ll.Count-1 do begin + if assigned(Ll.Items[i].Thread) then begin + // Cancel every worker before joining any of them. + Ll.Items[i].Thread.Terminate; + LThreads.Add(Ll.Items[i].Thread); + end; end; - end; + Finally + FChunks.UnlockList; + End; + // Retrying workers acquire FChunks too; joining under that lock deadlocks. + // Keep their shared list and streams alive until every worker has stopped. + for i:=0 to LThreads.Count-1 do WaitForChunkThread(LThreads[i]); + for i:=0 to LThreads.Count-1 do LThreads[i].Free; Finally - FChunks.UnlockList; + LThreads.Free; End; FreeAndNil(FChunks); FreeAndNil(FDownloadedBank); inherited; end; +function TPCDownloadSafebox.CreateChunkThread(ABlockStart, + ACount : Cardinal) : TDownloadSBThread; +begin + Result := TDownloadSBThread.Create(Self,ABlockStart,ACount); +end; + +procedure TPCDownloadSafebox.WaitForChunkThread(AThread : TThread); +begin + AThread.WaitFor; +end; + function TPCDownloadSafebox.DownloadSafebox(AOwnerThread : TThread; ASavedSafeboxHighOperationBlock: TOperationBlock; AChunks : TPCSafeboxChunks): Boolean; var LDownloadedSafeboxBlocksCount, request_id : Cardinal; LreceivedChunk : TStream; @@ -210,7 +239,8 @@ function TPCDownloadSafebox.DownloadSafebox(AOwnerThread : TThread; ASavedSafebo Ldsbc : TPCDownloadSafeboxChunk; LTerminated : Boolean; LTerminatedCount, LTotal : Integer; - LFileName, Lstatus : String; + LFileName: String; + LProgress: TProgressInfo; Begin Result := False; // Check @@ -249,14 +279,13 @@ function TPCDownloadSafebox.DownloadSafebox(AOwnerThread : TThread; ASavedSafebo for i:=0 to Ll.Count-1 do begin Ldsbc := Ll.Items[i]; if Not Assigned(Ldsbc.Thread) then begin - Ldsbc.Thread := TPCDownloadSafebox.TDownloadSBThread.Create(Self,Ldsbc.BlockStart,Ldsbc.Count); + Ldsbc.Thread := CreateChunkThread(Ldsbc.BlockStart,Ldsbc.Count); Ll.Items[i] := Ldsbc; end else begin if Ldsbc.Thread.Terminated then Inc(LTerminatedCount); end; end; LTerminated := LTerminatedCount >= Ll.Count; - Lstatus := Format('Downloading Safebox chunks %d/%d',[LTerminatedCount,Ll.Count]); Finally FChunks.UnlockList; End; @@ -266,7 +295,10 @@ function TPCDownloadSafebox.DownloadSafebox(AOwnerThread : TThread; ASavedSafebo if Assigned(AOwnerThread) then LContinue := Not AOwnerThread.CheckTerminated else LContinue := True; if (LContinue) and (Assigned(FOnProgressNotify)) then begin - FOnProgressNotify(Self,LStatus,LTerminatedCount,LTotal); + LProgress := TProgressInfo.Create(psDownloadingSafeboxChunks, LTerminatedCount, LTotal); + LProgress.ChunksCount := LTotal; + LProgress.BlockCount := LDownloadedSafeboxBlocksCount; + FOnProgressNotify(Self, LProgress); end; end; @@ -282,7 +314,11 @@ function TPCDownloadSafebox.DownloadSafebox(AOwnerThread : TThread; ASavedSafebo LFileName := TNode.Node.Bank.GetStorageFolder('')+PathDelim+'safebox_'+IntToStr(LDownloadedSafeboxBlocksCount)+'.safebox'; if (Assigned(FOnProgressNotify)) then begin - FOnProgressNotify(Self,Format('Saving Safebox %d chunks to %s',[LTotal,ExtractFileName(LFileName)]),0,0); + LProgress := TProgressInfo.Create(psSavingSafebox); + LProgress.ChunksCount := LTotal; + LProgress.FileName := LFileName; + LProgress.BlockCount := LDownloadedSafeboxBlocksCount; + FOnProgressNotify(Self, LProgress); end; AChunks.SaveSafeboxfile(LFileName); finally diff --git a/src/core/UPCOperationsSignatureValidator.pas b/src/core/UPCOperationsSignatureValidator.pas index 200f129b2..a55ef1cbc 100644 --- a/src/core/UPCOperationsSignatureValidator.pas +++ b/src/core/UPCOperationsSignatureValidator.pas @@ -52,7 +52,7 @@ interface TPCOperationsSignatureValidator = Class private - FLock : TPCCriticalSection; + FLock : TReadWriteLock; // FOperationsList : TList; FLastIndexOperations : Integer; @@ -88,7 +88,7 @@ constructor TPCOperationsSignatureValidator.Create(ASafeBoxTransaction: TPCSafeB begin FSafeBoxTransaction := ASafeBoxTransaction; FLastIndexOperations := -1; - FLock := TPCCriticalSection.Create(''); + FLock := TReadWriteLock.Create('',lrpSupportsRecursion); FValidatedOkCount := 0; FValidatedErrorCount := 0; FProgressNotify := AProgressNotify; @@ -103,7 +103,7 @@ destructor TPCOperationsSignatureValidator.Destroy; function TPCOperationsSignatureValidator.GetNextOperation(AValidatorThread : TPCOperationsSignatureValidatorThread) : TPCOperation; var LOp : TPCOperation; begin - FLock.Acquire; + FLock.AcquireWriteLock; try Result := Nil; // Search next Operation without valid signature @@ -115,7 +115,7 @@ function TPCOperationsSignatureValidator.GetNextOperation(AValidatorThread : TPC end else Break; end; finally - FLock.Release; + FLock.ReleaseWriteLock; end; end; @@ -233,33 +233,36 @@ procedure TPCOperationsSignatureValidator.SetOperationCheckResult( AValidatorThread: TPCOperationsSignatureValidatorThread; APCOperation: TPCOperation; AValidated: Boolean); begin - FLock.Acquire; + FLock.AcquireWriteLock; try if AValidated then inc(FValidatedOkCount) else inc(FValidatedErrorCount); finally - FLock.Release; + FLock.ReleaseWriteLock; end; end; function TPCOperationsSignatureValidator.Validate(AOperationsList : TList) : Integer; var LLastTC : TTickCount; procedure DoNotify; - var LMsg : String; - LCurPos,LTotal : Int64; + var LProgress: TProgressInfo; begin if (Assigned(FProgressNotify)) and (TPlatform.GetElapsedMilliseconds(LLastTC)>200) then begin LLastTC := TPlatform.GetTickCount; - FLock.Acquire; + // Snapshot assignment progress without excluding other snapshot readers; + // work reservation and completion counters still acquire write ownership. + FLock.AcquireReadLock; try - LMsg := Format('Validating signatures %d/%d',[FLastIndexOperations,FOperationsList.Count]); - LCurPos := FLastIndexOperations; - LTotal := FOperationsList.Count; + // Preserve the existing assigned-operation index, not a fabricated + // completed-signature count. Presentation belongs to the consumer. + LProgress := TProgressInfo.Create(psValidatingSignatures, + FLastIndexOperations, FOperationsList.Count); + LProgress.OperationsCount := FOperationsList.Count; finally - FLock.Release; + FLock.ReleaseReadLock; end; // - FProgressNotify(Self,LMsg,LCurPos,LTotal); + FProgressNotify(Self, LProgress); end; end; var LMaxThreads : Integer; diff --git a/src/core/UPCOrderedLists.pas b/src/core/UPCOrderedLists.pas index 20e4dc8f5..f4d9a8399 100644 --- a/src/core/UPCOrderedLists.pas +++ b/src/core/UPCOrderedLists.pas @@ -98,6 +98,9 @@ interface Function Count : Integer; Function Get(index : Integer) : TRawBytes; Procedure Delete(index : Integer); + // Indexes refer to the original list and must be strictly increasing. + // Validate first, then dispose removed entries and compact survivors once. + Procedure DeleteIndexes(const AIndexes : array of Integer); procedure SetTag(Const RawData : TRawBytes; newTagValue : Integer); function GetTag(Const RawData : TRawBytes) : Integer; overload; function GetTag(index : Integer) : Integer; overload; @@ -338,6 +341,32 @@ procedure TOrderedRawList.Delete(index: Integer); Dispose(P); end; +procedure TOrderedRawList.DeleteIndexes(const AIndexes: array of Integer); +var i, LRead, LWrite, LDelete : Integer; +begin + if Length(AIndexes)=0 then Exit; + // Reject the whole request before releasing any owned entry. + for i := Low(AIndexes) to High(AIndexes) do begin + if (AIndexes[i]<0) or (AIndexes[i]>=FList.Count) then + raise EOrderedList.Create(Format('Index %d out of range 0..%d',[AIndexes[i],FList.Count-1])); + if (i>Low(AIndexes)) and (AIndexes[i]<=AIndexes[i-1]) then + raise EOrderedList.Create('Indexes must be strictly increasing'); + end; + LWrite := 0; + LDelete := Low(AIndexes); + for LRead := 0 to FList.Count-1 do begin + if (LDelete<=High(AIndexes)) and (AIndexes[LDelete]=LRead) then begin + Dispose(PRawListData(FList[LRead])); + Inc(LDelete); + end else begin + if LWrite<>LRead then FList[LWrite] := FList[LRead]; + Inc(LWrite); + end; + end; + // TList owns only its array; the entries above own their raw bytes. + FList.Count := LWrite; +end; + destructor TOrderedRawList.Destroy; begin Clear; diff --git a/src/core/UPCRPCOpData.pas b/src/core/UPCRPCOpData.pas index 0f7c9c89a..a88a7a0c6 100644 --- a/src/core/UPCRPCOpData.pas +++ b/src/core/UPCRPCOpData.pas @@ -113,7 +113,7 @@ class function TRPCOpData.OpData_FindOpDataOperations( AAct_depth : Integer; AFirst_Block_Is_Unknown : Boolean; ASearchBySender : Boolean; ASearchSender : Cardinal; ASearchByTarget : Boolean; ASearchTarget : Cardinal; - ASearchByGUID : Boolean; ASearchGUID : TGUID; + ASearchByGUID : Boolean; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASearchGUID : TGUID; ASearchByDataSequence : Boolean; ASearchDataSequence : Word; ASearchByDataType : Boolean; ASearchDataType : Word; AOperationsResumeList : TOperationsResumeList @@ -366,7 +366,7 @@ class function TRPCOpData.OpData_SendOpData(const ASender: TRPCProcess; LTargetRequiresPurchase : Boolean; begin Result := False; - ASender.Node.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent operations + ASender.Node.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent operations try if Not TPascalCoinJSONComp.CaptureMempoolAccount(AInputParams,'sender',ASender.Node,LSender,AErrorDesc) then begin @@ -436,7 +436,7 @@ class function TRPCOpData.OpData_SendOpData(const ASender: TRPCProcess; end; finally - ASender.Node.OperationSequenceLock.Release; + ASender.Node.OperationSequenceLock.ReleaseWriteLock; end; end; diff --git a/src/core/UPCRPCSend.pas b/src/core/UPCRPCSend.pas index 51b832c43..07eb2f877 100644 --- a/src/core/UPCRPCSend.pas +++ b/src/core/UPCRPCSend.pas @@ -139,7 +139,7 @@ class function TRPCSend.SendTo(const ASender: TRPCProcess; end; // Do new operation - ASender.Node.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent sends + ASender.Node.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent sends try if Not TPascalCoinJSONComp.CaptureAccountNumber(AInputParams,'sender',ASender.Node,LSender.account,AErrorDesc) then begin @@ -218,7 +218,7 @@ class function TRPCSend.SendTo(const ASender: TRPCProcess; LOpt.free; end; finally - ASender.Node.OperationSequenceLock.Release; + ASender.Node.OperationSequenceLock.ReleaseWriteLock; end; end; diff --git a/src/core/UPCTNetDataExtraMessages.pas b/src/core/UPCTNetDataExtraMessages.pas index db8835ff3..244fe7438 100644 --- a/src/core/UPCTNetDataExtraMessages.pas +++ b/src/core/UPCTNetDataExtraMessages.pas @@ -251,14 +251,29 @@ class function TPCTNetDataExtraMessages.InitNetDataExtraMessages(ANode: TNode; end; procedure TPCTNetDataExtraMessages.OnTNetDataProcessReservedAreaMessage(ANetData : TNetData; ASenderConnection : TNetConnection; const AHeaderData : TNetHeaderData; AReceivedData : TStream; AResponseData : TStream); +var LUnavailable: Word; begin TLog.NewLog(ltdebug,ClassName,Format('Received extra message from %s Operation:%d',[ASenderConnection.ClientRemoteAddr,AHeaderData.operation])); + try case AHeaderData.operation of - CT_NetProtocol_Extra_NetOp_GIVE_ME_AN_ACCOUNT : DoGiveMeAnAccount(ANetData,ASenderConnection,AHeaderData,AReceivedData,AResponseData); + CT_NetProtocol_Extra_NetOp_GIVE_ME_AN_ACCOUNT : begin + if not FNode.PendingOperationsAvailable then + raise EPendingOperationsUnavailable.Create('Pending operations are unavailable during blockchain synchronization'); + DoGiveMeAnAccount(ANetData,ASenderConnection,AHeaderData,AReceivedData,AResponseData); + end; {$IFDEF TESTNET} CT_NetProtocol_Extra_NetOp_GIVE_ME_MONEY : DoGiveMeMoney(ANetData,ASenderConnection,AHeaderData,AReceivedData,AResponseData); {$ENDIF} end; + except + on E: EPendingOperationsUnavailable do begin + // Temporary catch-up is an ordinary refused request, using the existing + // zero-word reply instead of disconnecting a peer or reading old deltas. + AResponseData.Size := 0; + LUnavailable := 0; + AResponseData.Write(LUnavailable,SizeOf(LUnavailable)); + end; + end; end; function TPCTNetDataExtraMessages.RandomGetWalletKeysAccount( diff --git a/src/core/UPoolMinerThreads.pas b/src/core/UPoolMinerThreads.pas index f8526bf10..5fc34e14f 100644 --- a/src/core/UPoolMinerThreads.pas +++ b/src/core/UPoolMinerThreads.pas @@ -25,7 +25,7 @@ interface {$I ./../config.inc} uses - Classes, SysUtils, syncobjs, UThread, UPoolMining, UAccounts, UCrypto, ULog, UBlockChain, USha256, URandomHash, URandomHash2, UBaseTypes, UCommon, + Classes, SysUtils, UThread, UPoolMining, UAccounts, UCrypto, ULog, UBlockChain, USha256, URandomHash, URandomHash2, UBaseTypes, UCommon, {$IFNDEF FPC}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}; type @@ -157,7 +157,8 @@ interface TCPUOpenSSLMinerThread = Class(TPCThread) private FCPUDeviceThread : TCPUDeviceThread; - FLock : TCriticalSection; + // Hashing changes the digest stream and nonce state; each job owns a writer. + FLock : TReadWriteLock; FJobNum : Integer; protected FCurrentMinerValuesForWork : TMinerValuesForWork; @@ -771,7 +772,7 @@ procedure TCPUDeviceThread.SetMinerValuesForWork(const Value: TMinerValuesForWor nextmin := 0; for i:=0 to l.count-1 do begin cpu := TCPUOpenSSLMinerThread(l[i]); - cpu.FLock.Acquire; + cpu.FLock.AcquireWriteLock; try Inc(cpu.FJobNum); cpu.FCurrentMinerValuesForWork := FMinerValuesForWork; @@ -787,7 +788,7 @@ procedure TCPUDeviceThread.SetMinerValuesForWork(const Value: TMinerValuesForWor nextmin := cpu.FMaxNOnce+1; cpu.FDigestStreamMsg.WriteBuffer(digest[Low(digest)],Length(digest)); finally - cpu.Flock.Release; + cpu.Flock.ReleaseWriteLock; end; end; finally @@ -853,7 +854,7 @@ TNonceResult = record If (FCPUDeviceThread.Paused) then sleep(1) else begin dstep := 2; - FLock.Acquire; + FLock.AcquireWriteLock; try LUseRandomHash := TPoolMinerThread.UseRandomHash(FCurrentMinerValuesForWork.version); if (LUseRandomHash) then begin @@ -936,13 +937,13 @@ TNonceResult = record if (TBaseType.BinStrComp(LResultsToCheck[j].PoW,FCurrentMinerValuesForWork.target_pow)<0) then begin if (Terminated) Or (FCPUDeviceThread.Terminated) then exit; dstep := 5; - FLock.Release; + FLock.ReleaseWriteLock; try dstep := 6; FCPUDeviceThread.FoundNOnce(FCurrentMinerValuesForWork, ts, LResultsToCheck[j].Nonce); dstep := 7; finally - FLock.Acquire; + FLock.AcquireWriteLock; end; dstep := 8; end; @@ -967,11 +968,11 @@ TNonceResult = record PascalCoinExecuteLastChunkAndDoSha256(FInternalSha256,FInternalChunk,FChangeTimestampAndNOnceBytePos,nonce,ts,resultPoW); // Note: RandomHash is handled above if (TBaseType.BinStrComp(resultPoW,FCurrentMinerValuesForWork.target_pow)<0) then begin if Terminated then exit; - FLock.Release; + FLock.ReleaseWriteLock; try FCPUDeviceThread.FoundNOnce(FCurrentMinerValuesForWork, ts,nonce); finally - FLock.Acquire; + FLock.AcquireWriteLock; end; end; if(nonce)8 finally - FLock.Release; + FLock.ReleaseWriteLock; end; end; // Not paused Except @@ -1004,7 +1005,7 @@ TNonceResult = record constructor TCPUOpenSSLMinerThread.Create(CPUDeviceThread : TCPUDeviceThread); begin FCPUDeviceThread := CPUDeviceThread; - FLock := TCriticalSection.Create; + FLock := TReadWriteLock.Create('TCPUOpenSSLMinerThread.Work',lrpSupportsRecursion); FDigestStreamMsg := TMemoryStream.Create; FMinNOnce := 0; FMaxNOnce:=$FFFFFFFF; FResetNOnce:=True; diff --git a/src/core/UPoolMining.pas b/src/core/UPoolMining.pas index 970e99405..133d2046a 100644 --- a/src/core/UPoolMining.pas +++ b/src/core/UPoolMining.pas @@ -79,9 +79,10 @@ TMinerValuesForWork_HELPER = record helper for TMinerValuesForWork TJSONRPCTcpIpClient = Class(TBufferedNetTcpIpClient) private FLastId : Cardinal; - FLockProcessBuffer : TPCCriticalSection; + // Waiting for a response re-enters the mutable buffer processor on this thread. + FLockProcessBuffer : TReadWriteLock; FReceivedBuffer : TBytes; - FLockReceivedBuffer : TPCCriticalSection; + FLockReceivedBuffer : TReadWriteLock; FPendingResponseMessages : TPCThreadList; protected public @@ -133,11 +134,15 @@ TMinerValuesForWork_HELPER = record helper for TMinerValuesForWork TPoolMiningServerThread = Class(TPCThread) private FPoolMiningServer : TPoolMiningServer; + FWake: TEvent; + FPendingWork: LongInt; protected procedure BCExecute; override; public Constructor Create(APoolMiningServer : TPoolMiningServer); Destructor Destroy; override; + procedure RequestCapture(AClearJobs: Boolean); + procedure WakeForStop; End; @@ -152,11 +157,13 @@ TMinerValuesForWork_HELPER = record helper for TMinerValuesForWork FClientsWins: Integer; FClientsCount: Integer; FOnMiningServerNewBlockFound: TNotifyEvent; + FOnStatusChanged: TNotifyEvent; FPoolJobs : TPCThreadList; FPoolThread : TPoolMiningServerThread; FMinerOperations : TPCOperationsComp; FMaxOperationsPerBlock: Integer; FMax0FeeOperationsPerBlock: Integer; + FStopping: Boolean; Procedure CheckMinerRecover(nbOperations: TPCOperationsComp); Procedure DoProcessJSON(json : TPCJSONObject; ResponseMethod : String; Client : TJSONRPCTcpIpClient); Procedure OnNodeNewBlock(Sender : TObject); @@ -166,11 +173,17 @@ TMinerValuesForWork_HELPER = record helper for TMinerValuesForWork procedure SetMinerPayload(const Value: TRawBytes); Procedure ClearPoolJobs; Procedure CaptureNewJobAndSendToMiners; + procedure RequestCapture(AClearJobs: Boolean = false); Procedure SendJobToMiner(Operations : TPCOperationsComp; Client : TJSONRPCTcpIpClient; IsResponse : Boolean; idResponse : Variant); - Procedure FillMinerOperations; + procedure NotifyStatusChanged; procedure SetMax0FeeOperationsPerBlock(const Value: Integer); procedure SetMaxOperationsPerBlock(const Value: Integer); protected + Procedure FillMinerOperations; + // Copy the current job while its pool owner is retained. Network sends use + // only this value snapshot after releasing the job list, never a borrowed job. + procedure CaptureJobValues(Operations: TPCOperationsComp; + out AValues: TMinerValuesForWork; out AJobsCount, AOperationsCount: Integer); virtual; Procedure OnNewIncommingConnection(Sender : TObject; Client : TNetTcpIpClient); override; procedure SetActive(const Value: Boolean); override; public @@ -182,6 +195,8 @@ TMinerValuesForWork_HELPER = record helper for TMinerValuesForWork Property ClientsCount : Integer read FClientsCount; Property ClientsWins : Integer read FClientsWins; Property OnMiningServerNewBlockFound : TNotifyEvent read FOnMiningServerNewBlockFound write FOnMiningServerNewBlockFound; + // Producer-thread notification; UI consumers must marshal to their owner. + property OnStatusChanged: TNotifyEvent read FOnStatusChanged write FOnStatusChanged; Property Max0FeeOperationsPerBlock : Integer read FMax0FeeOperationsPerBlock write SetMax0FeeOperationsPerBlock; Property MaxOperationsPerBlock : Integer read FMaxOperationsPerBlock write SetMaxOperationsPerBlock; End; @@ -242,8 +257,8 @@ constructor TJSONRPCTcpIpClient.Create(AOwner: TComponent); inherited; FLastId := 1; SetLength(FReceivedBuffer,0); - FLockProcessBuffer := TPCCriticalSection.Create('TJSONRPCTcpIpClient_LockProcessBuffer'); - FLockReceivedBuffer := TPCCriticalSection.Create('TJSONRPCTcpIpClient_LockReceivedBuffer'); + FLockProcessBuffer := TReadWriteLock.Create('TJSONRPCTcpIpClient_LockProcessBuffer',lrpSupportsRecursion); + FLockReceivedBuffer := TReadWriteLock.Create('TJSONRPCTcpIpClient_LockReceivedBuffer',lrpSupportsRecursion); FPendingResponseMessages := TPCThreadList.Create('TJSONRPCTcpIpClient_PendingResponseMessages'); end; @@ -336,7 +351,7 @@ function TJSONRPCTcpIpClient.DoProcessBuffer(SenderThread : TPCThread; MaxWaitMi Result := false; ResponseMethod := ''; tc := TPlatform.GetTickCount; - If Not TPCThread.TryProtectEnterCriticalSection(Self,MaxWaitMiliseconds,FLockProcessBuffer) then Exit; + If Not TPCThread.TryProtectAcquireWriteLock(Self,MaxWaitMiliseconds,FLockProcessBuffer) then Exit; try if Assigned(SenderThread) then continue := Not SenderThread.Terminated else continue := true; @@ -388,18 +403,18 @@ function TJSONRPCTcpIpClient.DoProcessBuffer(SenderThread : TPCThread; MaxWaitMi end; finally FlushBufferPendingMessages(false,0); - FLockProcessBuffer.Release; + FLockProcessBuffer.ReleaseWriteLock; end; end; function TJSONRPCTcpIpClient.GetNewId: Cardinal; begin - FLockReceivedBuffer.Acquire; + FLockReceivedBuffer.AcquireWriteLock; try inc(FLastId); Result := FLastId; finally - FLockReceivedBuffer.Release; + FLockReceivedBuffer.ReleaseWriteLock; end; end; @@ -481,7 +496,7 @@ function TJSONRPCTcpIpClient.SendJSONRPCMethodAndWait(const method: String; para rm : String; begin Result := false; - FLockProcessBuffer.Acquire; + FLockProcessBuffer.AcquireWriteLock; try nId := GetNewId; SendJSONRPCMethod(method,params,nId); @@ -509,7 +524,7 @@ function TJSONRPCTcpIpClient.SendJSONRPCMethodAndWait(const method: String; para TLog.NewLog(lterror,classname,'Not received a JSON response Id:'+inttostr(nId)+' for method:'+method); end; finally - FLockProcessBuffer.Release; + FLockProcessBuffer.ReleaseWriteLock; end; end; @@ -564,9 +579,13 @@ procedure TPoolMiningServer.CaptureNewJobAndSendToMiners; params : TPCJSONObject; OpB : TOperationBlock; LLockedMempool : TPCOperationsComp; + LRegistered : Boolean; begin if Not Active then exit; if FClientsCount<=0 then exit; + // Catch-up intentionally hides the pending overlay until its final replay. + // Do not rebuild miner jobs from it or turn block notifications into replay. + if not FNodeNotifyEvents.Node.PendingOperationsAvailable then Exit; doAdd := false; P := Nil; l := FPoolJobs.LockList; @@ -587,13 +606,18 @@ procedure TPoolMiningServer.CaptureNewJobAndSendToMiners; end; end; if doAdd then begin + // A catch-up notification can arrive after the first availability check. + // Finish the guarded capture before allocating the registered job owner. + FillMinerOperations; New(P); + P^.OperationsComp := Nil; + LRegistered := False; + try P^.SentDateTime := now; P^.SentMinTimestamp := TNetData.NetData.NetworkAdjustedTime.GetAdjustedTime; if (P^.SentMinTimestampCT_MAX_BUFFER_JOBS) do begin @@ -664,6 +699,7 @@ constructor TPoolMiningServer.Create; begin inherited; FOnMiningServerNewBlockFound := Nil; + FOnStatusChanged := nil; FIncomingsCounter := 0; FClientsWins := 0; FClientsCount := 0; @@ -677,22 +713,34 @@ constructor TPoolMiningServer.Create; FMinerAccountKey := CT_TECDSA_Public_Nul; FMinerPayload := Nil; FPoolJobs := TPCThreadList.Create('TPoolMiningServer_PoolJobs'); - FPoolThread := TPoolMiningServerThread.Create(Self); FMax0FeeOperationsPerBlock := CT_MAX_0_fee_operations_per_block_by_miner; FMaxOperationsPerBlock := CT_MAX_Operations_per_block_by_miner; + FPoolThread := TPoolMiningServerThread.Create(Self); end; destructor TPoolMiningServer.Destroy; begin - FPoolThread.Terminate; - FPoolThread.WaitFor; - FreeAndNil(FPoolThread); - FNodeNotifyEvents.Node := Nil; - FNodeNotifyEvents.OnBlocksChanged := Nil; - FNodeNotifyEvents.OnOperationsChanged := Nil; + FOnStatusChanged := nil; + FStopping := true; + // A synchronized node callback can run while WaitFor services the main + // thread. Detach it before the worker's wake object can be freed. + if Assigned(FNodeNotifyEvents) then begin + FNodeNotifyEvents.OnBlocksChanged := Nil; + FNodeNotifyEvents.OnOperationsChanged := Nil; + end; + // Accepted miners can still read jobs and templates while their callbacks + // drain. Stop/join that owned listener before freeing any of those fields. + Active := false; + if Assigned(FPoolThread) then begin + FPoolThread.Terminate; + FPoolThread.WakeForStop; + FPoolThread.WaitFor; + FreeAndNil(FPoolThread); + end; + if Assigned(FNodeNotifyEvents) then FNodeNotifyEvents.Node := Nil; FreeAndNil(FMinerOperations); FreeAndNil(FNodeNotifyEvents); - ClearPoolJobs; + if Assigned(FPoolJobs) then ClearPoolJobs; FreeAndNil(FPoolJobs); inherited; end; @@ -801,62 +849,79 @@ procedure TPoolMiningServer.FillMinerOperations; LLockedMempool : TPCOperationsComp; op : TPCOperation; errors : String; + LWriteOwned, LUpgradeNeeded : Boolean; begin - LLockedMempool := FNodeNotifyEvents.Node.LockMempoolRead; + // Reserve the sole upgrader while comparing the existing miner template. + LLockedMempool := FNodeNotifyEvents.Node.LockMempoolUpgradeableRead; + LWriteOwned := False; Try - FMinerOperations.Lock; - Try - tree := TOperationsHashTree.Create; - try - if (Not (TPCOperationsComp.EqualsOperationBlock(FMinerOperations.OperationBlock,LLockedMempool.OperationBlock))) then begin - FMinerOperations.Clear(true); - if LLockedMempool.SafeBoxTransaction.FreezedSafeBox.CurrentProtocol < CT_PROTOCOL_6 then begin - CheckMinerRecover(LLockedMempool); - end; - if LLockedMempool.Count>0 then begin - // First round: Select with fee > 0 - i := 0; - while (tree.OperationsCount0 then begin - DoAdd(op,false); + while True do begin + LUpgradeNeeded := False; + FMinerOperations.Lock; + Try + LUpgradeNeeded := (not LWriteOwned) and + (not TPCOperationsComp.EqualsOperationBlock(FMinerOperations.OperationBlock, + LLockedMempool.OperationBlock)); + if not LUpgradeNeeded then begin + tree := TOperationsHashTree.Create; + try + if (Not (TPCOperationsComp.EqualsOperationBlock(FMinerOperations.OperationBlock,LLockedMempool.OperationBlock))) then begin + FMinerOperations.Clear(true); + if LLockedMempool.SafeBoxTransaction.FreezedSafeBox.CurrentProtocol < CT_PROTOCOL_6 then begin + CheckMinerRecover(LLockedMempool); end; - inc(i); - end; - // Second round: Allow fee = 0 - j := 0; - i := 0; - while (tree.OperationsCount0 then begin + // First round: Select with fee > 0 + i := 0; + while (tree.OperationsCount0 then begin + DoAdd(op,false); + end; + inc(i); + end; + // Second round: Allow fee = 0 + j := 0; + i := 0; + while (tree.OperationsCounttree.OperationsCount) Or (i<>LLockedMempool.OperationsHashTree.OperationsCount) then begin + TLog.NewLog(ltDebug,ClassName,Format('Cannot add all operations! Master:%d Selected:%d Added:%d - Errors: %s', + [LLockedMempool.OperationsHashTree.OperationsCount,tree.OperationsCount,i,errors])); + end; + end else begin + FMinerOperations.CopyFrom(LLockedMempool); end; - inc(i); - end; - // Add operations: - i := FMinerOperations.AddOperations(tree,errors); - if (i<>tree.OperationsCount) Or (i<>LLockedMempool.OperationsHashTree.OperationsCount) then begin - TLog.NewLog(ltDebug,ClassName,Format('Cannot add all operations! Master:%d Selected:%d Added:%d - Errors: %s', - [LLockedMempool.OperationsHashTree.OperationsCount,tree.OperationsCount,i,errors])); + // + {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,ClassName,Format('New miner operations:%d Hash:%s %s', + [FMinerOperations.OperationsHashTree.OperationsCount,TCrypto.ToHexaString(FMinerOperations.OperationsHashTree.HashTree),TCrypto.ToHexaString(FMinerOperations.OperationBlock.operations_hash)]));{$ENDIF} + end else begin + TLog.NewLog(ltDebug,ClassName,Format('No need to change Miner buffer. Operations:%d',[FMinerOperations.OperationsHashTree.OperationsCount])); end; - end else begin - FMinerOperations.CopyFrom(LLockedMempool); + finally + tree.Free; end; - // - {$IFDEF HIGHLOG}TLog.NewLog(ltdebug,ClassName,Format('New miner operations:%d Hash:%s %s', - [FMinerOperations.OperationsHashTree.OperationsCount,TCrypto.ToHexaString(FMinerOperations.OperationsHashTree.HashTree),TCrypto.ToHexaString(FMinerOperations.OperationBlock.operations_hash)]));{$ENDIF} - end else begin - TLog.NewLog(ltDebug,ClassName,Format('No need to change Miner buffer. Operations:%d',[FMinerOperations.OperationsHashTree.OperationsCount])); end; - finally - tree.Free; + Finally + FMinerOperations.Unlock; end; - Finally - FMinerOperations.Unlock; + if not LUpgradeNeeded then Break; + // Rebuilds can mutate shared signature caches as well as legacy recovery. + // Release the child lock before promotion, then recheck the template. + LLockedMempool := FNodeNotifyEvents.Node.LockMempoolWrite; + LWriteOwned := True; end; Finally - FNodeNotifyEvents.Node.UnlockMempoolRead; + if LWriteOwned then FNodeNotifyEvents.Node.UnlockMempoolWrite; + FNodeNotifyEvents.Node.UnlockMempoolUpgradeableRead; End; end; @@ -938,6 +1003,7 @@ function TPoolMiningServer.MinerSubmit(Client: TJSONRPCTcpIpClient; params: TPCJ json.GetAsVariant('timestamp').Value := nbOperations.timestamp; json.GetAsVariant('nonce').Value := nbOperations.nonce; inc(FClientsWins); + NotifyStatusChanged; Client.SendJSONRPCResponse(json,id); finally json.Free; @@ -1001,6 +1067,7 @@ procedure TPoolMiningServer.OnNewIncommingConnection(Sender: TObject; Client: TN inherited; inc(FClientsCount); Try + NotifyStatusChanged; TLog.NewLog(ltinfo,ClassName,'New Mining Pool Connection: '+Client.ClientRemoteAddr); bClient := TJSONRPCTcpIpClient(Client); inc(FIncomingsCounter); @@ -1019,44 +1086,58 @@ procedure TPoolMiningServer.OnNewIncommingConnection(Sender: TObject; Client: TN end; Finally Dec(FClientsCount); + NotifyStatusChanged; TLog.NewLog(ltinfo,ClassName,'Finalizing Mining Pool Connection: '+Client.ClientRemoteAddr); End; end; procedure TPoolMiningServer.OnNodeNewBlock(Sender: TObject); begin - // Delete Jobs cache, because a new block was found prior to us - ClearPoolJobs; - CaptureNewJobAndSendToMiners; + // This receiver runs on the UI bridge. It must not enter a pool/node lock. + RequestCapture(true); end; procedure TPoolMiningServer.OnNodeOperationsChanged(Sender: TObject); begin - CaptureNewJobAndSendToMiners; + RequestCapture; end; -procedure TPoolMiningServer.SendJobToMiner(Operations: TPCOperationsComp; Client: TJSONRPCTcpIpClient; IsResponse : Boolean; idResponse : Variant); -var params : TPCJSONObject; - ts : Cardinal; -Var P : PPoolJob; - i,nJobs : Integer; +procedure TPoolMiningServer.RequestCapture(AClearJobs: Boolean); +begin + if (not FStopping) and Assigned(FPoolThread) then + FPoolThread.RequestCapture(AClearJobs); +end; + +procedure TPoolMiningServer.CaptureJobValues(Operations: TPCOperationsComp; + out AValues: TMinerValuesForWork; out AJobsCount, AOperationsCount: Integer); +var P : PPoolJob; l : TList; + LRegistered : Boolean; begin - if FClientsCount<=0 then exit; + AValues := CT_TMinerValuesForWork_NULL; + AJobsCount := 0; + AOperationsCount := 0; + l := FPoolJobs.LockList; + try + if not FNodeNotifyEvents.Node.PendingOperationsAvailable then + raise EPendingOperationsUnavailable.Create('Pending operations are unavailable during blockchain synchronization'); if (Not Assigned(Operations)) then begin P := Nil; - l := FPoolJobs.LockList; - Try if l.count>0 then P := l[l.Count-1] // The last else begin //TLog.NewLog(ltInfo,ClassName,'Creating new job for miner'); + // The guarded capture can defer while catch-up is active. Allocate + // the registered job only after it succeeds, as in periodic capture. + FillMinerOperations; New(P); + P^.OperationsComp := Nil; + LRegistered := False; + try P^.SentDateTime := now; P^.SentMinTimestamp := TNetData.NetData.NetworkAdjustedTime.GetAdjustedTime; if (P^.SentMinTimestamp'+TPCOperationsComp.OperationBlockToText(FNodeNotifyEvents.Node.Bank.LastOperationBlock)); P^.OperationsComp.Free; Dispose(P); + P := Nil; raise Exception.Create('ERROR DEV 20170228-2'); end; l.Add(P); + LRegistered := True; + except + // A failed initial job never enters FPoolJobs, so its allocations + // still belong here rather than to ClearPoolJobs. + if (Not LRegistered) And Assigned(P) then begin + P^.OperationsComp.Free; + Dispose(P); + P := Nil; + end; + raise; + end; end; - Finally - FPoolJobs.UnlockList; - End; Operations := P^.OperationsComp; end; - l := FPoolJobs.LockList; - Try - nJobs := l.Count; - Finally + AJobsCount := l.Count; + // ClearPoolJobs can run on the pool worker after any node notification. Copy + // all managed bytes and scalar fields before that owner may destroy the job. + AValues.block := Operations.OperationBlock.block; + AValues.version := Operations.OperationBlock.protocol_version; + AValues.part1 := Operations.PoW_Digest_Part1; + AValues.payload_start := Operations.OperationBlock.block_payload; + AValues.part3 := Operations.PoW_Digest_Part3; + AValues.target := Operations.OperationBlock.compact_target; + AValues.target_pow := TPascalCoinProtocol.TargetFromCompact(AValues.target,AValues.version); + AOperationsCount := Operations.Count; + AValues.timestamp := TNetData.NetData.NetworkAdjustedTime.GetAdjustedTime; + if AValues.timestamp Value; inherited; if (Not Value) then begin WaitUntilNetTcpIpClientsFinalized; end; + if LChanged then NotifyStatusChanged; +end; + +procedure TPoolMiningServer.NotifyStatusChanged; +begin + // Publish actual client/active/win transitions instead of asking UI to poll. + if Assigned(FOnStatusChanged) then FOnStatusChanged(Self); end; @@ -1131,7 +1253,7 @@ procedure TPoolMiningServer.SetMax0FeeOperationsPerBlock(const Value: Integer); TLog.NewLog(ltError,ClassName,Format('Invalid max zero fee operations per block value %d, set to %d',[Value,FMax0FeeOperationsPerBlock])); end else FMax0FeeOperationsPerBlock := Value; TLog.NewLog(ltInfo,ClassName,Format('Updated max zero fee operations per block to %d',[FMax0FeeOperationsPerBlock])); - CaptureNewJobAndSendToMiners; + RequestCapture; end; procedure TPoolMiningServer.SetMaxOperationsPerBlock(const Value: Integer); @@ -1143,7 +1265,7 @@ procedure TPoolMiningServer.SetMaxOperationsPerBlock(const Value: Integer); TLog.NewLog(ltError,ClassName,Format('Invalid max operations per block value %d, set to %d',[Value,FMaxOperationsPerBlock])); end else FMaxOperationsPerBlock := Value; TLog.NewLog(ltInfo,ClassName,Format('Updated max operations per block to %d',[FMaxOperationsPerBlock])); - CaptureNewJobAndSendToMiners; + RequestCapture; end; procedure TPoolMiningServer.SetMinerAccountKey(const Value: TAccountKey); @@ -1151,14 +1273,14 @@ procedure TPoolMiningServer.SetMinerAccountKey(const Value: TAccountKey); if TAccountComp.EqualAccountKeys(FMinerAccountKey,Value) then exit; FMinerAccountKey := Value; TLog.NewLog(ltdebug,ClassName,'Assigning Miner account key to: '+TCrypto.ToHexaString(TAccountComp.AccountKey2RawString(Value))); - CaptureNewJobAndSendToMiners; + RequestCapture; end; procedure TPoolMiningServer.SetMinerPayload(const Value: TRawBytes); begin FMinerPayload := Value; TLog.NewLog(ltdebug,ClassName,'Assigning Miner new Payload: '+TCrypto.ToHexaString(Value)); - CaptureNewJobAndSendToMiners; + RequestCapture; end; procedure TPoolMiningServer.UpdateAccountAndPayload( @@ -1168,7 +1290,7 @@ procedure TPoolMiningServer.UpdateAccountAndPayload( TLog.NewLog(ltdebug,ClassName,'Assigning Miner account key to: '+TCrypto.ToHexaString(TAccountComp.AccountKey2RawString(AMinerAccountKey))); FMinerPayload := AMinerPayload; TLog.NewLog(ltdebug,ClassName,'Assigning Miner new Payload: '+TCrypto.ToHexaString(AMinerPayload)); - CaptureNewJobAndSendToMiners; + RequestCapture; end; { TPoolMinerClient } @@ -1510,34 +1632,72 @@ procedure TPoolMinerClient.SubmitBlockFound(Const MinerValuesToGenerateBlock : T { TPoolMiningServerThread } procedure TPoolMiningServerThread.BCExecute; -Var i : Integer; +var LLastPeriodic: TTickCount; LElapsed: Int64; LWait: Cardinal; + LWork: LongInt; begin - i := 0; - Repeat - Sleep(100); - inc(i); - if (not terminated) And ((i mod 10)=0) then begin - Try + LLastPeriodic := TPlatform.GetTickCount; + while not Terminated do begin + LElapsed := TPlatform.GetElapsedMilliseconds(LLastPeriodic); + if LElapsed >= 1000 then LWait := 0 else LWait := 1000 - Cardinal(LElapsed); + FWake.WaitFor(LWait); + if Terminated then Break; + {$IFDEF FPC}LWork := InterlockedExchange(FPendingWork,0); + {$ELSE}LWork := TInterlocked.Exchange(FPendingWork,0);{$ENDIF} + // Traffic never postpones the old one-second timestamp check. The existing + // capture method continues to enforce its ten/thirty-second job policies. + if TPlatform.GetElapsedMilliseconds(LLastPeriodic) >= 1000 then begin + LLastPeriodic := TPlatform.GetTickCount; + LWork := LWork or 2; + end; + try + if (LWork and 1) <> 0 then FPoolMiningServer.ClearPoolJobs; + if (LWork and 2) <> 0 then FPoolMiningServer.CaptureNewJobAndSendToMiners; - Except - On E:Exception do begin - TLog.NewLog(ltError,ClassName,'Error ('+E.ClassName+') Capturing job for miners: '+E.Message ); - end; + except + on E: EPendingOperationsUnavailable do begin + // Availability can change after the initial check. Final coherent + // block notification requests a fresh job; this is ordinary deferral. + end; + on E: Exception do begin + TLog.NewLog(ltError,ClassName,'Error ('+E.ClassName+') Capturing job for miners: '+E.Message); End; end; - Until terminated; + end; +end; + +procedure TPoolMiningServerThread.RequestCapture(AClearJobs: Boolean); +var LOld, LNew: LongInt; +begin + repeat + {$IFDEF FPC}LOld := InterlockedCompareExchange(FPendingWork,0,0); + {$ELSE}LOld := TInterlocked.CompareExchange(FPendingWork,0,0);{$ENDIF} + LNew := LOld or 2; + if AClearJobs then LNew := LNew or 1; + {$IFDEF FPC} + if InterlockedCompareExchange(FPendingWork,LNew,LOld) = LOld then Break; + {$ELSE} + if TInterlocked.CompareExchange(FPendingWork,LNew,LOld) = LOld then Break; + {$ENDIF} + until false; + FWake.SetEvent; +end; + +procedure TPoolMiningServerThread.WakeForStop; +begin + FWake.SetEvent; end; constructor TPoolMiningServerThread.Create(APoolMiningServer: TPoolMiningServer); begin FPoolMiningServer := APoolMiningServer; + FWake := TEvent.Create(nil, false, false, ''); inherited Create(false); end; destructor TPoolMiningServerThread.Destroy; begin - inherited; + FWake.Free; end; end. diff --git a/src/core/URPC.pas b/src/core/URPC.pas index f653cbe79..6a1333a90 100644 --- a/src/core/URPC.pas +++ b/src/core/URPC.pas @@ -176,7 +176,7 @@ implementation UEPasaDecoder, UPCRPCSend, UOrderedList, - UPCRPCOpData, UPCRPCFindAccounts, UPCRPCFindBlocks, UPCRPCFileUtils; + UPCRPCOpData, UPCRPCFindAccounts, UPCRPCFindBlocks, UPCRPCFileUtils, UProgressDisplay; Type TRegisteredRPCProcessMethod = Record @@ -1331,7 +1331,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; Function GetBlock(nBlock : Cardinal; jsonObject : TPCJSONObject) : Boolean; begin - FNode.OperationSequenceLock.Acquire; // Added to prevent high concurrent API calls + FNode.OperationSequenceLock.AcquireWriteLock; // Added to prevent high concurrent API calls try If FNode.Bank.BlocksCount<=nBlock then begin ErrorNum := CT_RPC_ErrNum_InvalidBlock; @@ -1342,7 +1342,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; TPascalCoinJSONComp.FillBlockObject(nBlock,FNode,jsonObject); Result := True; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end; @@ -1352,7 +1352,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; LOperationsCount : Integer; LOperationsAmount : Int64; begin - FNode.OperationSequenceLock.Acquire; // Added to prevent high concurrent API calls + FNode.OperationSequenceLock.AcquireWriteLock; // Added to prevent high concurrent API calls try LOpResumeList := TOperationsResumeList.Create; Try @@ -1372,7 +1372,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; LOpResumeList.Free; End; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end; @@ -1384,7 +1384,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; LOperationsAmount : Int64; i : Integer; begin - FNode.OperationSequenceLock.Acquire; // Added to prevent high concurrent API calls + FNode.OperationSequenceLock.AcquireWriteLock; // Added to prevent high concurrent API calls try LOpResumeList := TOperationsResumeList.Create; Try @@ -1404,7 +1404,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; LOpResumeList.Free; End; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end; @@ -1425,7 +1425,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; LLockedMempool : TPCOperationsComp; Begin Result := false; - FNode.OperationSequenceLock.Acquire; // Added to prevent high concurrent API calls + FNode.OperationSequenceLock.AcquireWriteLock; // Added to prevent high concurrent API calls try if (startReg<-1) or (maxReg<=0) then begin ErrorNum := CT_RPC_ErrNum_InvalidData; @@ -1476,7 +1476,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; OperationsResume.Free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end; @@ -1650,7 +1650,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; errors : String; opr : TOperationResume; begin - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try Result := false; if (account_signer<0) or (account_signer>=FNode.Bank.AccountsCount) then begin @@ -1675,7 +1675,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; opck.free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end; @@ -1820,7 +1820,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; operationsht : TOperationsHashTree; OperationsResumeList : TOperationsResumeList; begin - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try Result := false; accountsnumber := TOrderedCardinalList.Create; @@ -1872,7 +1872,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; accountsnumber.Free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end; @@ -2527,7 +2527,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; errors : String; c_account : Cardinal; Begin - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try Result := False; OperationsHashTree := TOperationsHashTree.Create; @@ -2560,7 +2560,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; OperationsHashTree.Free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; End; @@ -2572,7 +2572,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; errors : String; c_account : Cardinal; Begin - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try Result := False; OperationsHashTree := TOperationsHashTree.Create; @@ -2605,7 +2605,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; OperationsHashTree.Free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; End; @@ -2617,7 +2617,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; errors : String; c_account : Cardinal; Begin - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try Result := False; OperationsHashTree := TOperationsHashTree.Create; @@ -2656,7 +2656,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; OperationsHashTree.Free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; End; @@ -2668,7 +2668,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; errors : String; c_account : Cardinal; Begin - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try Result := False; OperationsHashTree := TOperationsHashTree.Create; @@ -2701,7 +2701,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; OperationsHashTree.Free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; End; @@ -2712,7 +2712,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; jsonarr : TPCJSONArray; i : Integer; begin - FNode.OperationSequenceLock.Acquire; // Added to prevent high concurrent API calls + FNode.OperationSequenceLock.AcquireWriteLock; // Added to prevent high concurrent API calls try Result := False; oprl := TOperationsResumeList.Create; @@ -2754,7 +2754,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; oprl.Free; end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end; @@ -3156,7 +3156,8 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; i,j,k,l : Integer; account : TAccount; senderpubkey,destpubkey : TAccountKey; - ansistr : String; + ansistr, LBankDiagnostic : String; + LProgress : TProgressInfo; nsaarr : TNodeServerAddressArray; pcops : TPCOperationsComp; ecpkey : TECPrivateKey; @@ -3483,24 +3484,20 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; // Create result k := params.AsInteger('max',100); j := params.AsInteger('start',0); - If FNode.TryLockNode(5000) then begin + // Formatting a pending snapshot shares read ownership with wallet readers. + If FNode.TryLockMempoolRead(5000, pcops) then begin Try jsonarr := GetResultArray; - pcops := FNode.LockMempoolRead; - try - for i := j to pcops.Count-1 do begin - If TPCOperation.OperationToOperationResume(0,pcops.Operation[i],True,pcops.Operation[i].SignerAccount,opr) then begin - opr.NOpInsideBlock:=i; - opr.Balance := -1; // Don't include! - FillOperationResumeToJSONObject(opr,jsonarr.GetAsObject(jsonarr.Count)); - end; - if (k>0) And (jsonarr.Count>=k) then break; + for i := j to pcops.Count-1 do begin + If TPCOperation.OperationToOperationResume(0,pcops.Operation[i],True,pcops.Operation[i].SignerAccount,opr) then begin + opr.NOpInsideBlock:=i; + opr.Balance := -1; // Don't include! + FillOperationResumeToJSONObject(opr,jsonarr.GetAsObject(jsonarr.Count)); end; - finally - FNode.UnlockMempoolRead; + if (k>0) And (jsonarr.Count>=k) then break; end; finally - FNode.UnlockNode; + FNode.UnlockMempoolRead; end; Result := true; end else begin @@ -3541,7 +3538,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; ErrorDesc:='param ophash with invalid length (Expected 64 chars for a 32bytes hexadecimal) value length = '+IntToStr(Length(r1)); exit; end; - FNode.OperationSequenceLock.Acquire; // Added to prevent high concurrent API calls + FNode.OperationSequenceLock.AcquireWriteLock; // Added to prevent high concurrent API calls try Case FNode.FindOperation(r1,opr) of OpHash_found : ; @@ -3561,7 +3558,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; FillOperationResumeToJSONObject(opr,GetResultObject); Result := True; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end else if (method='findnoperation') then begin // Search for an operation signed by "account" and with "n_operation", start searching "block" (0=all) @@ -3570,7 +3567,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; ErrorNum := CT_RPC_ErrNum_InvalidAccount; Exit; end; - FNode.OperationSequenceLock.Acquire; // Added to prevent high concurrent API calls + FNode.OperationSequenceLock.AcquireWriteLock; // Added to prevent high concurrent API calls try Case FNode.FindNOperation(params.AsCardinal('block',0),c,params.AsCardinal('n_operation',0),opr) of OpHash_found : ; @@ -3587,7 +3584,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; else Raise Exception.Create('ERROR DEV 20171120-5'); end; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; FillOperationResumeToJSONObject(opr,GetResultObject); Result := True; @@ -3811,8 +3808,8 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; GetResultObject.GetAsVariant('ready').Value := True; if TNetData.NetData.IsDiscoveringServers then begin GetResultObject.GetAsVariant('status_s').Value := 'Discovering servers'; - end else if TNetData.NetData.IsGettingNewBlockChainFromClient(ansistr) then begin - GetResultObject.GetAsVariant('status_s').Value := ansistr; + end else if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then begin + GetResultObject.GetAsVariant('status_s').Value := FormatProgressInfo(LProgress); end else begin GetResultObject.GetAsVariant('status_s').Value := 'Running'; end; @@ -3820,6 +3817,11 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; GetResultObject.GetAsVariant('ready_s').Value := 'Alone in the world...'; end; end else begin + // ready_s historically exposed the complete network progress text. + if FNode.Bank.IsReady(LBankDiagnostic) then + if FNode.NetServer.Active then + if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then + ansistr := FormatProgressInfo(LProgress); GetResultObject.GetAsVariant('ready_s').Value := ansistr; end; GetResultObject.GetAsVariant('port').Value:=FNode.NetServer.Port; @@ -3961,11 +3963,11 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; ErrorNum := CT_RPC_ErrNum_NotAllowedCall; Exit; end; - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try jsonresponse.GetAsVariant('result').Value := _RPCServer.WalletKeys.LockWallet; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; Result := true; end else if (method='unlock') then begin @@ -3981,7 +3983,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; ErrorDesc := 'Need param "pwd"'; exit; end; - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try If Not _RPCServer.WalletKeys.IsValidPassword then begin _RPCServer.WalletKeys.WalletPassword:=params.AsString('pwd',''); @@ -3989,7 +3991,7 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; jsonresponse.GetAsVariant('result').Value := _RPCServer.WalletKeys.IsValidPassword; Result := true; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end else if (method='setwalletpassword') then begin // Changes the Wallet password with "pwd" param @@ -4011,13 +4013,13 @@ function TRPCProcess.ProcessMethod(const method: String; params: TPCJSONObject; ErrorDesc := 'Need param "pwd"'; exit; end; - FNode.OperationSequenceLock.Acquire; // Use lock to prevent N_Operation race-condition on concurrent invocations + FNode.OperationSequenceLock.AcquireWriteLock; // Use lock to prevent N_Operation race-condition on concurrent invocations try _RPCServer.WalletKeys.WalletPassword:=params.AsString('pwd',''); jsonresponse.GetAsVariant('result').Value := _RPCServer.WalletKeys.IsValidPassword; Result := true; finally - FNode.OperationSequenceLock.Release; + FNode.OperationSequenceLock.ReleaseWriteLock; end; end else if (method='stopnode') then begin // Stops communications to other nodes diff --git a/src/core/UServerApp.pas b/src/core/UServerApp.pas index cc6520494..7c45feb72 100644 --- a/src/core/UServerApp.pas +++ b/src/core/UServerApp.pas @@ -31,7 +31,7 @@ interface Windows, Messages, {$ENDIF} - SyncObjs, + UThread, UOpenSSL, UCrypto, UNode, UFileStorage, UFolderHelper, UWalletKeys, UConst, ULog, UNetProtocol, URPC; @@ -45,7 +45,7 @@ interface TPascalCoinServerApp = class private - FLock : TCriticalSection; + FLock : TReadWriteLock; FOnLog: TPascalCoinServerAppLogEvent; FTerminated : Boolean; {$IFDEF OS_MSWIN} @@ -99,7 +99,7 @@ implementation constructor TPascalCoinServerApp.Create; begin inherited Create; - FLock := TCriticalSection.Create; + FLock := TReadWriteLock.Create('TPascalCoinServerApp.State',lrpSupportsRecursion); FLog := TLog.Create(Nil); FLog.OnInThreadNewLog:=OnPascalCoinLog; {$IFDEF OS_MSWIN} @@ -120,12 +120,12 @@ destructor TPascalCoinServerApp.Destroy; procedure TPascalCoinServerApp.Lock; begin - FLock.Acquire; + FLock.AcquireWriteLock; end; procedure TPascalCoinServerApp.Unlock; begin - FLock.Release; + FLock.ReleaseWriteLock; end; procedure TPascalCoinServerApp.Log(const LogType: TPascalCoinServerLogType; @@ -152,11 +152,12 @@ procedure TPascalCoinServerApp.OnPascalCoinLog(logtype: TLogType; function TPascalCoinServerApp.GetTerminated: Boolean; begin - Lock; + // Reading the stop flag does not mutate server state or invoke callbacks. + FLock.AcquireReadLock; try Result := FTerminated; finally - Unlock; + FLock.ReleaseReadLock; end; end; diff --git a/src/core/UTCPIP.pas b/src/core/UTCPIP.pas index 5bb1d3ead..23c80f1ee 100644 --- a/src/core/UTCPIP.pas +++ b/src/core/UTCPIP.pas @@ -60,11 +60,12 @@ interface FTcpBlockSocket : TTCPBlockSocket; FBytesReceived, FBytesSent : Int64; FConnected : Boolean; - FLock : TPCCriticalSection; + // Receive/connect failures can call Disconnect while already owning this writer. + FLock : TReadWriteLock; {$IFDEF Synapse} FRemoteHost : String; FRemotePort : Word; - FSendBufferLock : TPCCriticalSection; + FSendBufferLock : TReadWriteLock; {$ENDIF} FOnConnect: TNotifyEvent; FOnDisconnect: TNotifyEvent; @@ -85,7 +86,9 @@ interface FNetTcpIpServer : TNetTcpIpServer; Procedure DoOnDisconnect; Virtual; Procedure DoOnConnect; Virtual; - function ReceiveBuf(var Buf; BufSize: Integer): Integer; + // Transport specializations can observe an inherited read before buffered + // publication without replacing the socket implementation. + function ReceiveBuf(var Buf; BufSize: Integer): Integer; virtual; Function SendStream(Stream : TStream) : Int64; Procedure DoWaitForData(WaitMilliseconds : Integer; var HasData : Boolean); virtual; public @@ -124,10 +127,15 @@ interface private FSendBuffer : TMemoryStream; FReadBuffer : TMemoryStream; - FCritical : TPCCriticalSection; + // Mutable stream leases stay exclusive even when their caller is reading bytes. + FCritical : TReadWriteLock; FLastReadTC : TTickCount; FBufferedNetTcpIpClientThread : TBufferedNetTcpIpClientThread; FOnReceivedData: TNotifyEvent; + FReceivePauseDepth: Integer; + FReceiveIdle: TEvent; + function BeginReceiveCycle: Boolean; + procedure EndReceiveCycle; protected Procedure DoReceivedData; virtual; Function DoWaitForDataInherited(WaitMilliseconds : Integer) : Boolean; @@ -138,6 +146,11 @@ interface Procedure WriteBufferToSend(SendData : TStream); Function ReadBufferLock : TMemoryStream; Procedure ReadBufferUnlock; + // The processing owner pauses admission before disconnecting, then waits + // without holding buffer/socket/registry locks for the last read to publish. + procedure PauseReceiving; + procedure WaitForReceiveIdle; + procedure ResumeReceiving; Property LastReadTC : TTickCount read FLastReadTC; Property OnReceivedData : TNotifyEvent read FOnReceivedData write FOnReceivedData; End; @@ -198,6 +211,7 @@ interface Procedure DoRemoveClient(AClient : TNetTcpIpClient); virtual; Procedure OnNewIncommingConnection(Sender : TObject; Client : TNetTcpIpClient); virtual; procedure SetActive(const Value: Boolean); virtual; + function DrainClientBuffersOnStop: Boolean; virtual; procedure SetMaxConnections(AValue: Integer); virtual; function GetNewClient : TNetTcpIpClient; virtual; public @@ -259,7 +273,7 @@ function TNetTcpIpClient.ClientRemoteAddr: String; function TNetTcpIpClient.Connect: Boolean; begin - FLock.Acquire; + FLock.AcquireWriteLock; try {$IFDEF DelphiSockets} FSocketError := 0; @@ -292,7 +306,7 @@ function TNetTcpIpClient.Connect: Boolean; End; {$ENDIF} finally - FLock.Release; + FLock.ReleaseWriteLock; end; if FConnected then begin DoOnConnect; @@ -316,9 +330,9 @@ constructor TNetTcpIpClient.Create(AOwner : TComponent); TIdTCPClient(FTcpBlockSocket).ConnectTimeout := 5000; TIdTCPClient(FTcpBlockSocket).ReadTimeout := 5000; {$ENDIF} - FLock := TPCCriticalSection.Create('TNetTcpIpClient_Lock'); + FLock := TReadWriteLock.Create('TNetTcpIpClient_Lock',lrpSupportsRecursion); {$IFDEF Synapse} - FSendBufferLock := TPCCriticalSection.Create('TNetTcpIpClient_SendBufferLock'); + FSendBufferLock := TReadWriteLock.Create('TNetTcpIpClient_SendBufferLock',lrpSupportsRecursion); FTcpBlockSocket := TTCPBlockSocket.Create; FTcpBlockSocket.OnAfterConnect := OnConnect; FTcpBlockSocket.SocksTimeout := 5000; //Build 1.5.0 was 10000; @@ -333,11 +347,11 @@ constructor TNetTcpIpClient.Create(AOwner : TComponent); destructor TNetTcpIpClient.Destroy; begin - FLock.Acquire; + FLock.AcquireWriteLock; try Disconnect; finally - FLock.Release; + FLock.ReleaseWriteLock; end; {$IFDEF Synapse} // Memory leak on 1.5.0 FreeAndNil(FSendBufferLock); @@ -354,7 +368,7 @@ procedure TNetTcpIpClient.Disconnect; if Not FConnected then exit; Try DebugStep := ''; - FLock.Acquire; + FLock.AcquireWriteLock; Try if Not FConnected then begin exit; // Protection inside lock thread to prevent double disconnect sessions @@ -376,7 +390,7 @@ procedure TNetTcpIpClient.Disconnect; DebugStep := 'Calling OnDisconnect'; {$ENDIF} Finally - FLock.Release; + FLock.ReleaseWriteLock; End; DoOnDisconnect; Except @@ -399,7 +413,7 @@ procedure TNetTcpIpClient.DoOnDisconnect; procedure TNetTcpIpClient.DoWaitForData(WaitMilliseconds: Integer; var HasData: Boolean); Begin - FLock.Acquire; + FLock.AcquireWriteLock; Try if Not FConnected then begin HasData := False; @@ -426,7 +440,7 @@ procedure TNetTcpIpClient.DoWaitForData(WaitMilliseconds: Integer; var HasData: End; {$ENDIF} Finally - FLock.Release; + FLock.ReleaseWriteLock; End; end; @@ -476,7 +490,7 @@ function TNetTcpIpClient.ReceiveBuf(var Buf; BufSize: Integer): Integer; {$ENDIF} begin Result := 0; - FLock.Acquire; + FLock.AcquireWriteLock; Try {$IFDEF DelphiSockets} FSocketError := 0; @@ -509,7 +523,7 @@ function TNetTcpIpClient.ReceiveBuf(var Buf; BufSize: Integer): Integer; End; {$ENDIF} Finally - FLock.Release; + FLock.ReleaseWriteLock; End; if Result>0 then FLastCommunicationTime := Now; end; @@ -529,7 +543,7 @@ function TNetTcpIpClient.SendStream(Stream: TStream): Int64; Result := 0; unlocked := false; // In order to allow a big stream sending, will cut up in small blocks - FSendBufferLock.Acquire; + FSendBufferLock.AcquireWriteLock; Try Try FTcpBlockSocket.SendStreamRaw(Stream); @@ -537,7 +551,7 @@ function TNetTcpIpClient.SendStream(Stream: TStream): Int64; TLog.NewLog(ltDebug,ClassName,'Closing connection from '+ClientRemoteAddr+' (Sending error): '+Inttostr(FTcpBlockSocket.LastError)+' '+FTcpBlockSocket.GetErrorDescEx); Result := -1; unlocked := true; - FSendBufferLock.Release; + FSendBufferLock.ReleaseWriteLock; Disconnect; end else begin Result := Stream.Position - sp; @@ -548,12 +562,12 @@ function TNetTcpIpClient.SendStream(Stream: TStream): Int64; SocketError := FTcpBlockSocket.LastError; TLog.NewLog(lterror,ClassName,'Exception sending stream to '+ClientRemoteAddr+': '+FTcpBlockSocket.GetErrorDescEx); unlocked := true; - FSendBufferLock.Release; + FSendBufferLock.ReleaseWriteLock; Disconnect; end; End; Finally - If not unlocked then FSendBufferLock.Release; + If not unlocked then FSendBufferLock.ReleaseWriteLock; end; {$ENDIF} if Result>0 then FLastCommunicationTime := Now; @@ -623,6 +637,7 @@ function TNetTcpIpClient.WaitForData(WaitMilliseconds: Integer): Boolean; procedure TBufferedNetTcpIpClientThread.BCExecute; var SendBuffStream : TStream; ReceiveBuffer : Array[0..4095] of byte; + LReceivedData: Boolean; Procedure DoReceiveBuf; var last_bytes_read : Integer; total_read, total_size : Int64; @@ -643,6 +658,9 @@ procedure TBufferedNetTcpIpClientThread.BCExecute; last_bytes_read := FBufferedNetTcpIpClient.ReceiveBuf(ReceiveBuffer,sizeof(ReceiveBuffer)); + // A readable socket returning zero bytes has reached orderly EOF. + // Its already returned bytes still belong to the buffered owner. + if last_bytes_read=0 then FBufferedNetTcpIpClient.Disconnect; if (last_bytes_read>0) then begin ms := FBufferedNetTcpIpClient.ReadBufferLock; Try @@ -660,7 +678,7 @@ procedure TBufferedNetTcpIpClientThread.BCExecute; until (last_bytes_read0 then TLog.NewLog(ltdebug,ClassName,Format('Received %d bytes. Buffer length: %d bytes',[total_read,total_size]));{$ENDIF} if (total_read>0) and (Not Terminated) and (FBufferedNetTcpIpClient.Connected) then begin - FBufferedNetTcpIpClient.DoReceivedData; + LReceivedData := True; end; end else begin if FBufferedNetTcpIpClient.SocketError<>0 then FBufferedNetTcpIpClient.Disconnect; @@ -668,7 +686,7 @@ procedure TBufferedNetTcpIpClientThread.BCExecute; end; Procedure DoSendBuf; begin - FBufferedNetTcpIpClient.FCritical.Acquire; + FBufferedNetTcpIpClient.FCritical.AcquireWriteLock; Try if FBufferedNetTcpIpClient.FSendBuffer.Size>0 then begin SendBuffStream.Size := 0; @@ -676,7 +694,7 @@ procedure TBufferedNetTcpIpClientThread.BCExecute; FBufferedNetTcpIpClient.FSendBuffer.Size := 0; end; Finally - FBufferedNetTcpIpClient.FCritical.Release; + FBufferedNetTcpIpClient.FCritical.ReleaseWriteLock; End; if (SendBuffStream.Size>0) then begin SendBuffStream.Position := 0; @@ -692,7 +710,19 @@ procedure TBufferedNetTcpIpClientThread.BCExecute; while (Not Terminated) And (Not FBufferedNetTcpIpClient.Connected) do sleep(100); if (FBufferedNetTcpIpClient.Connected) then begin // Receive data - If (Not Terminated) And (FBufferedNetTcpIpClient.Connected) then DoReceiveBuf; + LReceivedData := False; + if (not Terminated) and FBufferedNetTcpIpClient.BeginReceiveCycle then begin + try + // Reserve the whole wait/read/publication sequence, including + // bytes an Indy wait may acquire internally. No admission gate is + // held across socket operations or their disconnect callbacks. + DoReceiveBuf; + finally + FBufferedNetTcpIpClient.EndReceiveCycle; + end; + end; + if LReceivedData and (not Terminated) and FBufferedNetTcpIpClient.Connected then + FBufferedNetTcpIpClient.DoReceivedData; // Send Data If (Not Terminated) And (FBufferedNetTcpIpClient.Connected) then DoSendBuf; end else FBufferedNetTcpIpClient.FLastReadTC := TPlatform.GetTickCount; @@ -717,9 +747,10 @@ constructor TBufferedNetTcpIpClient.Create(AOwner: TComponent); inherited; FOnReceivedData := Nil; FLastReadTC := TPlatform.GetTickCount; - FCritical := TPCCriticalSection.Create('TBufferedNetTcpIpClient_Critical'); + FCritical := TReadWriteLock.Create('TBufferedNetTcpIpClient_Critical',lrpSupportsRecursion); FSendBuffer := TMemoryStream.Create; FReadBuffer := TMemoryStream.Create; + FReceiveIdle := TEvent.Create(nil,True,True,''); FBufferedNetTcpIpClientThread := TBufferedNetTcpIpClientThread.Create(Self); end; @@ -728,6 +759,7 @@ destructor TBufferedNetTcpIpClient.Destroy; FBufferedNetTcpIpClientThread.Terminate; FBufferedNetTcpIpClientThread.WaitFor; FreeAndNil(FBufferedNetTcpIpClientThread); + FreeAndNil(FReceiveIdle); FreeAndNil(FCritical); FreeAndNil(FReadBuffer); FreeAndNil(FSendBuffer); @@ -741,14 +773,15 @@ procedure TBufferedNetTcpIpClient.DoReceivedData; procedure TBufferedNetTcpIpClient.DoWaitForData(WaitMilliseconds: Integer; var HasData: Boolean); begin - FCritical.Acquire; + // Inspecting buffer size does not move its cursor; release before the socket wait. + FCritical.AcquireReadLock; try if FReadBuffer.Size>0 then begin HasData := True; exit; end; finally - FCritical.Release; + FCritical.ReleaseReadLock; end; inherited DoWaitForData(WaitMilliseconds,HasData); end; @@ -760,19 +793,74 @@ function TBufferedNetTcpIpClient.DoWaitForDataInherited(WaitMilliseconds : Integ function TBufferedNetTcpIpClient.ReadBufferLock: TMemoryStream; begin - FCritical.Acquire; + // Callers may consume, clear or reposition this shared stream until Unlock. + FCritical.AcquireWriteLock; Result := FReadBuffer; end; procedure TBufferedNetTcpIpClient.ReadBufferUnlock; begin - FCritical.Release; + FCritical.ReleaseWriteLock; +end; + +function TBufferedNetTcpIpClient.BeginReceiveCycle: Boolean; +begin + FCritical.AcquireWriteLock; + try + Result := (FReceivePauseDepth=0) and Connected; + if Result then begin + FReceiveIdle.ResetEvent; + end; + finally + FCritical.ReleaseWriteLock; + end; +end; + +procedure TBufferedNetTcpIpClient.EndReceiveCycle; +begin + FCritical.AcquireWriteLock; + try + FReceiveIdle.SetEvent; + finally + FCritical.ReleaseWriteLock; + end; +end; + +procedure TBufferedNetTcpIpClient.PauseReceiving; +begin + FCritical.AcquireWriteLock; + try + Inc(FReceivePauseDepth); + finally + FCritical.ReleaseWriteLock; + end; +end; + +procedure TBufferedNetTcpIpClient.WaitForReceiveIdle; +begin + if TThread.CurrentThread.ThreadID=FBufferedNetTcpIpClientThread.ThreadID then + raise Exception.Create('The receive worker cannot wait for itself'); + // PauseReceiving closes new admission; an admitted read keeps the event + // unsignalled until its bytes are appended, even if Disconnect has returned. + if FReceiveIdle.WaitFor(Cardinal($FFFFFFFF))<>wrSignaled then + raise Exception.Create('Cannot wait for buffered receive publication'); +end; + +procedure TBufferedNetTcpIpClient.ResumeReceiving; +begin + FCritical.AcquireWriteLock; + try + if FReceivePauseDepth<=0 then raise Exception.Create('Unbalanced receive resume'); + Dec(FReceivePauseDepth); + finally + FCritical.ReleaseWriteLock; + end; end; procedure TBufferedNetTcpIpClient.WriteBufferToSend(SendData: TStream); var lastpos : Int64; begin - FCritical.Acquire; + FCritical.AcquireWriteLock; try lastpos := FSendBuffer.Position; FSendBuffer.Position := FSendBuffer.Size; @@ -780,7 +868,7 @@ procedure TBufferedNetTcpIpClient.WriteBufferToSend(SendData: TStream); FSendBuffer.CopyFrom(SendData,SendData.Size); FSendBuffer.Position := lastpos; finally - FCritical.Release; + FCritical.ReleaseWriteLock; end; end; @@ -884,7 +972,7 @@ procedure TNetTcpIpServer.OnTcpServerAccept({$IFDEF DelphiSockets}AContext: TIdC begin n := GetNewClient; Try - n.FLock.Acquire; + n.FLock.AcquireWriteLock; try n.FNetTcpIpServer := Self; oldSocket := n.FTcpBlockSocket; @@ -901,7 +989,7 @@ procedure TNetTcpIpServer.OnTcpServerAccept({$IFDEF DelphiSockets}AContext: TIdC ClientSocket.ConnectionTimeout := 5000; // New 1.5.1 {$ENDIF} finally - n.FLock.Release; + n.FLock.ReleaseWriteLock; end; DoAddClient(n); try @@ -934,6 +1022,13 @@ procedure TNetTcpIpServer.SetActive(const Value: Boolean); {$ENDIF} end; +function TNetTcpIpServer.DrainClientBuffersOnStop: Boolean; +begin + // Existing server types retain their cancellation policy. Protocol servers + // can instead finish the received unit before their accepted callback exits. + Result := False; +end; + procedure TNetTcpIpServer.SetNetTcpIpClientClass(const Value: TNetTcpIpClientClass); begin if FNetTcpIpClientClass=Value then exit; @@ -1013,7 +1108,11 @@ procedure TTcpIpServerListenerThread.BCExecute; // Finalize all threads for i := 0 to lSockets.Count - 1 do begin TTcpIpSocketThread(lSockets[i]).FListenerThread := Nil; - TTcpIpSocketThread(lSockets[i]).Terminate; + // Network-off must not turn an already received blockchain batch into + // application cancellation. The protocol callback exits when Active is + // false after draining its current buffer; shutdown has a separate latch. + if not FNetTcpIpServerServer.DrainClientBuffersOnStop then + TTcpIpSocketThread(lSockets[i]).Terminate; end; // Wait until terminated... for i := 0 to lSockets.Count - 1 do begin diff --git a/src/core/UThread.pas b/src/core/UThread.pas index d916bccc7..790893425 100644 --- a/src/core/UThread.pas +++ b/src/core/UThread.pas @@ -4,6 +4,7 @@ Acknowledgements: Herman Schoenfeld author of TPipelineQueue (2019) + Herman Schoenfeld author of TReadWriteLock (2026) Distributed under the MIT software license, see the accompanying file LICENSE or visit http://www.opensource.org/licenses/mit-license.php. @@ -34,7 +35,7 @@ interface {$IFDEF LINUX}cthreads,{$ENDIF} {$ENDIF} {$IFNDEF FPC}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, - Classes, SyncObjs, SysUtils, UBaseTypes; + Classes, SyncObjs, SysUtils, UBaseTypes, UConst; {$I ./../config.inc} @@ -60,6 +61,102 @@ interface Property Name : String read FName; end; + { Copyright © 2026 Herman Schoenfeld + TReadWriteLock is ported from PascalFramework's MIT-licensed UConcurrency. + Its core implementation uses existing PascalCoin thread identity, admission + diagnostics and monotonic timing, and imports no framework unit. } + TLockRecursionPolicy = (lrpNoRecursion, lrpSupportsRecursion); + ELockRecursionException = class(Exception); + ESynchronizationLockException = class(Exception); + ELockDisposedException = class(Exception); + + { All acquisitions and releases are thread-affine. Ordinary read ownership + cannot be promoted: acquire upgradeable read before a possible write. + Stop/join consumers before Free. Dispose rejects outstanding ownership or + waiters; it never releases another thread's lock or terminates a thread. } + TReadWriteLock = class + private type + TLockKind = (lkRead, lkWrite, lkUpgradeable); + TThreadCounts = record + Reads, Writes, Upgrades: Integer; + end; + TLockWaiter = class + Kind: TLockKind; + Signal: TEvent; + constructor Create(AKind: TLockKind); + destructor Destroy; override; + end; + private + // This gate protects ownership/admission bookkeeping only. The caller's + // resource stays protected by the granted ownership after the gate is left. + FGate: TCriticalSection; + FName: String; + // UConst makes the existing core thread-ID type visible in both interface + // and implementation; an implementation-only import cannot change it here. + FThreads: TDictionary; + FWaiters: TList; + FPolicy: TLockRecursionPolicy; + // Reserving upgradeable ownership for one thread prevents competing readers + // from each waiting to promote while preventing the other from completing. + FWriter, FUpgrader: TThreadID; + // Readers counts distinct explicit read owners, not recursive acquisitions. + // Upgradeable-only access is tracked separately by FUpgrader. + FReaders, FWaitingReaders, FWaitingWriters, FWaitingUpgraders: Integer; + FDisposed: Boolean; + function ThreadCounts(AThreadID: TThreadID): TThreadCounts; + procedure CheckAcquisition(AKind: TLockKind; const ACounts: TThreadCounts); + function CanAcquire(AKind: TLockKind; AThreadID: TThreadID; + const ACounts: TThreadCounts): Boolean; + procedure Grant(AKind: TLockKind; AThreadID: TThreadID; + var ACounts: TThreadCounts); + function TryAcquire(AKind: TLockKind; AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseLock(AKind: TLockKind); + procedure ChangeWaitCount(AKind: TLockKind; ADelta: Integer); + procedure WakeWaiters; + function GetHeld(AKind: TLockKind): Boolean; + function GetRecursiveCount(AKind: TLockKind): Integer; + function GetCount(AIndex: Integer): Integer; + function GetIsReadLockHeld: Boolean; + function GetIsWriteLockHeld: Boolean; + function GetIsUpgradeableReadLockHeld: Boolean; + function GetRecursiveReadCount: Integer; + function GetRecursiveWriteCount: Integer; + function GetRecursiveUpgradeCount: Integer; + function GetCurrentReadCount: Integer; + function GetWaitingReadCount: Integer; + function GetWaitingWriteCount: Integer; + function GetWaitingUpgradeCount: Integer; + public + constructor Create(ARecursionPolicy: TLockRecursionPolicy = lrpNoRecursion); overload; + constructor Create(const AName: String; ARecursionPolicy: TLockRecursionPolicy = lrpNoRecursion); overload; + destructor Destroy; override; + procedure Dispose; + // Pair every successful Acquire/TryAcquire with the matching Release on + // the same thread. Promotion adds a write acquisition to upgradeable read. + procedure AcquireReadLock; + function TryAcquireReadLock(AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseReadLock; + procedure AcquireWriteLock; + function TryAcquireWriteLock(AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseWriteLock; + procedure AcquireUpgradeableReadLock; + function TryAcquireUpgradeableReadLock(AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseUpgradeableReadLock; + property RecursionPolicy: TLockRecursionPolicy read FPolicy; + property Name: String read FName; + property IsReadLockHeld: Boolean read GetIsReadLockHeld; + property IsWriteLockHeld: Boolean read GetIsWriteLockHeld; + property IsUpgradeableReadLockHeld: Boolean read GetIsUpgradeableReadLockHeld; + property RecursiveReadCount: Integer read GetRecursiveReadCount; + property RecursiveWriteCount: Integer read GetRecursiveWriteCount; + property RecursiveUpgradeCount: Integer read GetRecursiveUpgradeCount; + // Counts distinct explicit read owners, excluding upgradeable-only access. + property CurrentReadCount: Integer read GetCurrentReadCount; + property WaitingReadCount: Integer read GetWaitingReadCount; + property WaitingWriteCount: Integer read GetWaitingWriteCount; + property WaitingUpgradeCount: Integer read GetWaitingUpgradeCount; + end; + TPCThread = Class; TPCThreadClass = Class of TPCThread; TPCThread = Class(TThread) @@ -77,6 +174,8 @@ interface Class function GetThreadByClass(tclass : TPCThreadClass; Exclude : TObject) : TPCThread; Class Procedure ProtectEnterCriticalSection(Const Sender : TObject; var Lock : TPCCriticalSection); Class Function TryProtectEnterCriticalSection(Const Sender : TObject; MaxWaitMilliseconds : Cardinal; var Lock : TPCCriticalSection) : Boolean; + Class Procedure ProtectAcquireWriteLock(const Sender: TObject; const Lock: TReadWriteLock); + Class Function TryProtectAcquireWriteLock(const Sender: TObject; MaxWaitMilliseconds: Cardinal; const Lock: TReadWriteLock): Boolean; Class Procedure ThreadsListInfo(list: TStrings); constructor Create(CreateSuspended: Boolean); destructor Destroy; override; @@ -88,7 +187,7 @@ interface TPCThreadList = class private FList: TList; - FLock: TPCCriticalSection; + FLock: TReadWriteLock; public constructor Create(const AName : String); destructor Destroy; override; @@ -111,7 +210,7 @@ TPipelineQueue = class(TComponent) TStageQueue = class private FDirty : Boolean; - FLock : TMultiReadExclusiveWriteSynchronizer; + FLock : TReadWriteLock; FItems : TList; function GetDirty : Boolean; procedure SetDirty (AValue : Boolean); @@ -120,7 +219,7 @@ TStageQueue = class constructor Create; overload; destructor Destroy; override; property Dirty : Boolean read GetDirty write SetDirty; - property Lock : TMultiReadExclusiveWriteSynchronizer read FLock; + property Lock : TReadWriteLock read FLock; property Items : TArray read GetItems; end; @@ -175,7 +274,7 @@ TPipelineWorkerThread = class(TPCThread) implementation uses - ULog, UConst; + ULog; { TPCThread } @@ -358,6 +457,29 @@ class function TPCThread.TryProtectEnterCriticalSection(const Sender: TObject; {$ENDIF} end; +class procedure TPCThread.ProtectAcquireWriteLock(const Sender: TObject; const Lock: TReadWriteLock); +begin + Lock.AcquireWriteLock; +end; + +class function TPCThread.TryProtectAcquireWriteLock(const Sender: TObject; MaxWaitMilliseconds: Cardinal; const Lock: TReadWriteLock): Boolean; +{$IFDEF HIGHLOG} +var LSenderName: String; +{$ENDIF} +begin + // Retain the legacy helper's cap before converting the unsigned timeout. + // The read/write lock waits on its event instead of polling every millisecond. + if MaxWaitMilliseconds > 60000 then MaxWaitMilliseconds := 60000; + Result := Lock.TryAcquireWriteLock(Integer(MaxWaitMilliseconds)); + {$IFDEF HIGHLOG} + if not Result then begin + if Assigned(Sender) then LSenderName := Sender.ClassName else LSenderName := ''; + TLog.NewLog(ltdebug,ClassName,Format('Cannot acquire write lock %s for %s after %d milliseconds; waiting writers: %d', + [Lock.Name,LSenderName,MaxWaitMilliseconds,Lock.WaitingWriteCount])); + end; + {$ENDIF} +end; + { TPCThreadList } function TPCThreadList.Add(Item: T) : Integer; @@ -382,13 +504,18 @@ procedure TPCThreadList.Clear; constructor TPCThreadList.Create(const AName : String); begin - FLock := TPCCriticalSection.Create(AName); + FLock := TReadWriteLock.Create(AName,lrpSupportsRecursion); FList := TList.Create; end; function TPCThreadList.Count : Integer; begin - Result := FList.Count; + FLock.AcquireReadLock; + try + Result := FList.Count; + finally + FLock.ReleaseReadLock; + end; end; destructor TPCThreadList.Destroy; @@ -405,7 +532,8 @@ destructor TPCThreadList.Destroy; function TPCThreadList.LockList: TList; begin - TPCThread.ProtectEnterCriticalSection(Self,FLock); + // This API returns a mutable list, so even callers inspecting it own WRITE. + TPCThread.ProtectAcquireWriteLock(Self,FLock); Result := FList; end; @@ -422,12 +550,12 @@ procedure TPCThreadList.Remove(Item: T); function TPCThreadList.TryLockList(MaxWaitMilliseconds: Cardinal; var lockedList: TList): Boolean; begin lockedList := FList; - Result := TPCThread.TryProtectEnterCriticalSection(Self,MaxWaitMilliseconds,FLock); + Result := TPCThread.TryProtectAcquireWriteLock(Self,MaxWaitMilliseconds,FLock); end; procedure TPCThreadList.UnlockList; begin - FLock.Release; + FLock.ReleaseWriteLock; end; { TPCCriticalSection } @@ -560,12 +688,12 @@ procedure TPipelineQueue.Enqueue(AStage : Integer; const AItem : T); procedure TPipelineQueue.EnqueueRange(AStage : Integer; const AItems : array of T); begin - FQueues[AStage].Lock.BeginWrite; + FQueues[AStage].Lock.AcquireWriteLock; try FQueues[AStage].FDirty := True; FQueues[AStage].FItems.AddRange(AItems); finally - FQueues[AStage].Lock.EndWrite; + FQueues[AStage].Lock.ReleaseWriteLock; end; NotifyPipelineAppended(AStage); end; @@ -612,7 +740,7 @@ constructor TPipelineQueue.TStageQueue.Create; begin inherited; FDirty := False; - FLock := TMultiReadExclusiveWriteSynchronizer.Create; + FLock := TReadWriteLock.Create('Pipeline.Stage',lrpSupportsRecursion); FItems := TList.Create; end; @@ -625,32 +753,32 @@ destructor TPipelineQueue.TStageQueue.Destroy; function TPipelineQueue.TStageQueue.GetDirty : Boolean; begin - FLock.BeginRead; + FLock.AcquireReadLock; try Result := FDirty; finally - FLock.EndRead; + FLock.ReleaseReadLock; end; end; procedure TPipelineQueue.TStageQueue.SetDirty ( AValue : Boolean ); begin - FLock.BeginWrite; + FLock.AcquireWriteLock; try FDirty := AValue; finally - FLock.EndWrite; + FLock.ReleaseWriteLock; end; end; function TPipelineQueue.TStageQueue.GetItems : TArray; begin begin - FLock.BeginRead; + FLock.AcquireReadLock; try Result := FItems.ToArray; finally - FLock.EndRead; + FLock.ReleaseReadLock; end; end; end; @@ -684,13 +812,13 @@ procedure TPipelineQueue.TPipelineWorkerThread.BCExecute; if not FPipeline.FQueues[FStage].FDirty then exit; // Extract items from pipeline stage - FPipeline.FQueues[FStage].Lock.BeginWrite; + FPipeline.FQueues[FStage].Lock.AcquireWriteLock; try LIn := FPipeline.FQueues[FStage].FItems.ToArray; FPipeline.FQueues[FStage].FItems.Clear; FPipeline.FQueues[FStage].FDirty := False; finally - FPipeline.FQueues[FStage].Lock.EndWrite; + FPipeline.FQueues[FStage].Lock.ReleaseWriteLock; end; // process items @@ -721,6 +849,430 @@ procedure TPipelineQueue.TPipelineWorkerThread.BCExecute; end; end; +{ TReadWriteLock } + +constructor TReadWriteLock.Create(ARecursionPolicy: TLockRecursionPolicy); +begin + inherited Create; + FPolicy := ARecursionPolicy; + // Admission must use the lower-level primitive. Its contention path must not + // log: the logger itself uses this lock, including in HIGHLOG builds. + FGate := TCriticalSection.Create; + FThreads := TDictionary.Create; + FWaiters := TList.Create; +end; + +constructor TReadWriteLock.Create(const AName: String; ARecursionPolicy: TLockRecursionPolicy); +begin + Create(ARecursionPolicy); + FName := AName; +end; + +destructor TReadWriteLock.Destroy; +begin + if Assigned(FGate) then + Dispose; + FWaiters.Free; + FThreads.Free; + FGate.Free; + inherited; +end; + +procedure TReadWriteLock.Dispose; +begin + FGate.Acquire; + try + if FDisposed then + Exit; + // Consumers must be stopped/joined by their owner before disposal. Reject + // active use without changing it; never unlock another thread or join it here. + if (Assigned(FThreads) and (FThreads.Count <> 0)) or + (Assigned(FWaiters) and (FWaiters.Count <> 0)) then + raise ESynchronizationLockException.Create('Cannot dispose a lock with owners or waiters.'); + FDisposed := True; + finally + FGate.Release; + end; +end; + +procedure TReadWriteLock.AcquireReadLock; +begin + TryAcquireReadLock(-1); +end; + +function TReadWriteLock.TryAcquireReadLock(AMillisecondsTimeout: Integer): Boolean; +begin + Result := TryAcquire(lkRead, AMillisecondsTimeout); +end; + +procedure TReadWriteLock.ReleaseReadLock; +begin + ReleaseLock(lkRead); +end; + +procedure TReadWriteLock.AcquireWriteLock; +begin + TryAcquireWriteLock(-1); +end; + +function TReadWriteLock.TryAcquireWriteLock(AMillisecondsTimeout: Integer): Boolean; +begin + Result := TryAcquire(lkWrite, AMillisecondsTimeout); +end; + +procedure TReadWriteLock.ReleaseWriteLock; +begin + ReleaseLock(lkWrite); +end; + +procedure TReadWriteLock.AcquireUpgradeableReadLock; +begin + TryAcquireUpgradeableReadLock(-1); +end; + +function TReadWriteLock.TryAcquireUpgradeableReadLock(AMillisecondsTimeout: Integer): Boolean; +begin + Result := TryAcquire(lkUpgradeable, AMillisecondsTimeout); +end; + +procedure TReadWriteLock.ReleaseUpgradeableReadLock; +begin + ReleaseLock(lkUpgradeable); +end; + +constructor TReadWriteLock.TLockWaiter.Create(AKind: TLockKind); +begin + inherited Create; + Kind := AKind; + Signal := TEvent.Create(nil, False, False, ''); +end; + +destructor TReadWriteLock.TLockWaiter.Destroy; +begin + Signal.Free; + inherited; +end; + +function TReadWriteLock.ThreadCounts(AThreadID: TThreadID): TThreadCounts; +begin + if not FThreads.TryGetValue(AThreadID, Result) then + Result := Default(TThreadCounts); +end; + +procedure TReadWriteLock.CheckAcquisition(AKind: TLockKind; + const ACounts: TThreadCounts); +var + LInvalid: Boolean; +begin + if FDisposed then + raise ELockDisposedException.Create('The lock is disposed.'); + // An ordinary reader cannot promote, even with recursion enabled: two such + // readers would each wait for the other. Choose upgradeable mode in advance. + if FPolicy = lrpNoRecursion then + case AKind of + lkRead: LInvalid := (ACounts.Reads > 0) or (ACounts.Writes > 0); + lkWrite: LInvalid := (ACounts.Writes > 0) or (ACounts.Reads > 0); + lkUpgradeable: + LInvalid := (ACounts.Reads > 0) or (ACounts.Writes > 0) or + (ACounts.Upgrades > 0); + end + else + LInvalid := (AKind <> lkRead) and (ACounts.Reads > 0) and + (ACounts.Writes = 0) and (ACounts.Upgrades = 0); + if LInvalid then + raise ELockRecursionException.Create('This acquisition violates the lock recursion policy.'); +end; + +function TReadWriteLock.CanAcquire(AKind: TLockKind; AThreadID: TThreadID; + const ACounts: TThreadCounts): Boolean; +begin + // RTL thread IDs may be integers or pointers; use their typed empty value. + // Existing owners can recurse without queuing behind a writer that needs + // them to finish. New readers/upgraders wait once any writer has queued. + case AKind of + lkRead: + Result := (ACounts.Reads > 0) or (ACounts.Writes > 0) or + (ACounts.Upgrades > 0) or ((FWriter = Default(TThreadID)) and (FWaitingWriters = 0)); + lkWrite: + // Only the reserved upgrader may discount its own explicit read ownership; + // all other readers must leave before that thread can acquire write mode. + Result := (ACounts.Writes > 0) or ((FWriter = Default(TThreadID)) and + ((FUpgrader = Default(TThreadID)) or (FUpgrader = AThreadID)) and + (FReaders = Ord(ACounts.Reads > 0))); + lkUpgradeable: + Result := (ACounts.Upgrades > 0) or (ACounts.Writes > 0) or + ((FUpgrader = Default(TThreadID)) and (FWriter = Default(TThreadID)) and (FWaitingWriters = 0)); + else + Result := False; + end; +end; + +procedure TReadWriteLock.Grant(AKind: TLockKind; AThreadID: TThreadID; + var ACounts: TThreadCounts); +var + LFirst: Boolean; +begin + LFirst := False; + case AKind of + lkRead: + begin + LFirst := ACounts.Reads = 0; + Inc(ACounts.Reads); + end; + lkWrite: Inc(ACounts.Writes); + lkUpgradeable: Inc(ACounts.Upgrades); + end; + // Publish counters only after the dictionary allocation has succeeded. + FThreads.AddOrSetValue(AThreadID, ACounts); + case AKind of + lkRead: + if LFirst then + Inc(FReaders); + lkWrite: FWriter := AThreadID; + lkUpgradeable: FUpgrader := AThreadID; + end; +end; + +procedure TReadWriteLock.ChangeWaitCount(AKind: TLockKind; ADelta: Integer); +begin + case AKind of + lkRead: Inc(FWaitingReaders, ADelta); + lkWrite: Inc(FWaitingWriters, ADelta); + lkUpgradeable: Inc(FWaitingUpgraders, ADelta); + end; +end; + +procedure TReadWriteLock.WakeWaiters; +var + LWaiter: TLockWaiter; +begin + // Per-waiter auto-reset events avoid lost wakes and repeated wakes without a + // state change. Admission is rechecked under the gate, with writer priority. + for LWaiter in FWaiters do + LWaiter.Signal.SetEvent; +end; + +function TReadWriteLock.TryAcquire(AKind: TLockKind; + AMillisecondsTimeout: Integer): Boolean; +var + LThreadID: TThreadID; + LCounts: TThreadCounts; + LWaiter: TLockWaiter; + LStarted: TTickCount; + LElapsed: Int64; + LRemaining: Cardinal; + LWaitResult: TWaitResult; +begin + if AMillisecondsTimeout < -1 then + raise EArgumentOutOfRangeException.Create('Timeout must be -1 or a nonnegative number of milliseconds.'); + // Keep the original deadline across state-change wakes; contention must not + // restart a caller's finite timeout. Use the core's monotonic tick helpers. + LStarted := TPlatform.GetTickCount; + LThreadID := TThread.CurrentThread.ThreadID; + LWaiter := nil; + FGate.Acquire; + try + LCounts := ThreadCounts(LThreadID); + CheckAcquisition(AKind, LCounts); + while True do + begin + if CanAcquire(AKind, LThreadID, LCounts) then + begin + Grant(AKind, LThreadID, LCounts); + Exit(True); + end; + if AMillisecondsTimeout = 0 then + Exit(False); + if AMillisecondsTimeout = -1 then + LRemaining := High(Cardinal) + else + begin + LElapsed := TPlatform.GetElapsedMilliseconds(LStarted); + if LElapsed >= AMillisecondsTimeout then + Exit(False); + LRemaining := Cardinal(Int64(AMillisecondsTimeout) - LElapsed); + end; + if not Assigned(LWaiter) then + begin + LWaiter := TLockWaiter.Create(AKind); + try + FWaiters.Add(LWaiter); + except + FreeAndNil(LWaiter); + raise; + end; + ChangeWaitCount(AKind, 1); + end; + // No protected resource or admission lock is held during an event wait. + FGate.Release; + try + LWaitResult := LWaiter.Signal.WaitFor(LRemaining); + finally + FGate.Acquire; + end; + if LWaitResult = wrTimeout then + Exit(False); + if LWaitResult <> wrSignaled then + raise ESynchronizationLockException.Create('Waiting for lock ownership failed.'); + LCounts := ThreadCounts(LThreadID); + end; + finally + if Assigned(LWaiter) then + begin + // Remove registration before freeing its event, while the gate prevents + // a releaser from signalling that event. Failure never grants ownership. + FWaiters.Remove(LWaiter); + ChangeWaitCount(AKind, -1); + // A timed-out writer must not keep new readers asleep indefinitely. + WakeWaiters; + end; + FGate.Release; + LWaiter.Free; + end; +end; + +procedure TReadWriteLock.ReleaseLock(AKind: TLockKind); +var + LThreadID: TThreadID; + LCounts: TThreadCounts; + LCount: Integer; +begin + // Every typed Release balances ONE same-thread acquisition of that mode. In + // particular, ReleaseWriteLock after promotion retains the surrounding upgrader. + LThreadID := TThread.CurrentThread.ThreadID; + FGate.Acquire; + try + LCounts := ThreadCounts(LThreadID); + case AKind of + lkRead: LCount := LCounts.Reads; + lkWrite: LCount := LCounts.Writes; + lkUpgradeable: LCount := LCounts.Upgrades; + else + LCount := 0; + end; + if LCount = 0 then + raise ESynchronizationLockException.Create('This thread does not hold the requested lock.'); + case AKind of + lkRead: + begin + Dec(LCounts.Reads); + if LCounts.Reads = 0 then + Dec(FReaders); + end; + lkWrite: + begin + Dec(LCounts.Writes); + if LCounts.Writes = 0 then + FWriter := Default(TThreadID); + end; + lkUpgradeable: + begin + Dec(LCounts.Upgrades); + if LCounts.Upgrades = 0 then + FUpgrader := Default(TThreadID); + end; + end; + if (LCounts.Reads = 0) and (LCounts.Writes = 0) and (LCounts.Upgrades = 0) then + FThreads.Remove(LThreadID) + else + FThreads.AddOrSetValue(LThreadID, LCounts); + WakeWaiters; + finally + FGate.Release; + end; +end; + +function TReadWriteLock.GetRecursiveCount(AKind: TLockKind): Integer; +var + LCounts: TThreadCounts; +begin + FGate.Acquire; + try + LCounts := ThreadCounts(TThread.CurrentThread.ThreadID); + case AKind of + lkRead: Result := LCounts.Reads; + lkWrite: Result := LCounts.Writes; + lkUpgradeable: Result := LCounts.Upgrades; + else + Result := 0; + end; + finally + FGate.Release; + end; +end; + +function TReadWriteLock.GetHeld(AKind: TLockKind): Boolean; +begin + Result := GetRecursiveCount(AKind) > 0; +end; + +function TReadWriteLock.GetCount(AIndex: Integer): Integer; +begin + FGate.Acquire; + try + case AIndex of + 0: Result := FReaders; + 1: Result := FWaitingReaders; + 2: Result := FWaitingWriters; + 3: Result := FWaitingUpgraders; + else + Result := 0; + end; + finally + FGate.Release; + end; +end; + +function TReadWriteLock.GetIsReadLockHeld: Boolean; +begin + Result := GetHeld(lkRead); +end; + +function TReadWriteLock.GetIsWriteLockHeld: Boolean; +begin + Result := GetHeld(lkWrite); +end; + +function TReadWriteLock.GetIsUpgradeableReadLockHeld: Boolean; +begin + Result := GetHeld(lkUpgradeable); +end; + +function TReadWriteLock.GetRecursiveReadCount: Integer; +begin + Result := GetRecursiveCount(lkRead); +end; + +function TReadWriteLock.GetRecursiveWriteCount: Integer; +begin + Result := GetRecursiveCount(lkWrite); +end; + +function TReadWriteLock.GetRecursiveUpgradeCount: Integer; +begin + Result := GetRecursiveCount(lkUpgradeable); +end; + +function TReadWriteLock.GetCurrentReadCount: Integer; +begin + Result := GetCount(0); +end; + +function TReadWriteLock.GetWaitingReadCount: Integer; +begin + Result := GetCount(1); +end; + +function TReadWriteLock.GetWaitingWriteCount: Integer; +begin + Result := GetCount(2); +end; + +function TReadWriteLock.GetWaitingUpgradeCount: Integer; +begin + Result := GetCount(3); +end; + initialization _threads := TPCThreadList.Create('GLOBAL_THREADS'); finalization diff --git a/src/core/UTxMultiOperation.pas b/src/core/UTxMultiOperation.pas index c011e636c..c86e96725 100644 --- a/src/core/UTxMultiOperation.pas +++ b/src/core/UTxMultiOperation.pas @@ -125,6 +125,10 @@ interface procedure AffectedAccounts(list : TOrderedList); override; // Function DoSignMultiOperationSigner(current_protocol : Word; SignerAccount : Cardinal; key : TECPrivateKey) : Integer; + // Returns added signature slots, or -1 for a different operation body. + // Only encoding sizes are checked here; CheckSignatures/DoOperation must + // verify all signatures against account ownership before execution. + function MergeMissingSignaturesFrom(ASource: TOpMultiOperation): Integer; class function OpType : Byte; override; function OperationAmount : Int64; override; function OperationFee : Int64; override; @@ -818,6 +822,41 @@ function TOpMultiOperation.DoSignMultiOperationSigner(current_protocol : Word; S end; end; +function TOpMultiOperation.MergeMissingSignaturesFrom(ASource: TOpMultiOperation): Integer; +var + i: Integer; + + function HasEncodedSignature(const ASignature: TECDSA_SIG): Boolean; + begin + Result := (Length(ASignature.r) > 0) and (Length(ASignature.r) <= 66) and + (Length(ASignature.s) > 0) and (Length(ASignature.s) <= 66); + end; + +begin + Result := -1; + if not Assigned(ASource) then Exit; + if (ProtocolVersion <> ASource.ProtocolVersion) or + not TBaseType.Equals(GetDigestToSign, ASource.GetDigestToSign) then Exit; + Result := 0; + for i := 0 to High(FData.txSenders) do + if (Length(FData.txSenders[i].Signature.r) = 0) and + (Length(FData.txSenders[i].Signature.s) = 0) and + HasEncodedSignature(ASource.FData.txSenders[i].Signature) then + begin + FData.txSenders[i].Signature := ASource.FData.txSenders[i].Signature; + Inc(Result); + end; + for i := 0 to High(FData.changesInfo) do + if (Length(FData.changesInfo[i].Signature.r) = 0) and + (Length(FData.changesInfo[i].Signature.s) = 0) and + HasEncodedSignature(ASource.FData.changesInfo[i].Signature) then + begin + FData.changesInfo[i].Signature := ASource.FData.changesInfo[i].Signature; + Inc(Result); + end; + if Result > 0 then FHasValidSignature := False; +end; + class function TOpMultiOperation.OpType: Byte; begin Result := CT_Op_MultiOperation; diff --git a/src/core/UWallet.pas b/src/core/UWallet.pas index f4bb3f607..37cadf2d0 100644 --- a/src/core/UWallet.pas +++ b/src/core/UWallet.pas @@ -61,6 +61,7 @@ interface FIsReadingStream : Boolean; FOnChanged: TNotifyManyEvent; FIsReadOnly: Boolean; + FMiningKey: TAccountKey; function GetHasPassword : boolean; function GetKey(index: Integer): TWalletKey; procedure SetWalletPassword(const Value: String); @@ -68,6 +69,9 @@ interface procedure SetWalletFileName(const Value: String); Function Find(Const AccountKey: TAccountKey; var Index: Integer): Boolean; procedure SetIsReadOnly(const Value: Boolean); + protected + // Publish only after derived wallet indexes have incorporated the added key. + procedure NotifyKeyAdded(const AAccountKey: TAccountKey); virtual; public Property Key[index : Integer] : TWalletKey read GetKey; default; Constructor Create(AOwner : TComponent); override; @@ -81,6 +85,12 @@ interface Function AddPublicKey(Const Name : String; ECDSA_Public : TECDSA_Public) : Integer; virtual; Function IndexOfAccountKey(const AccountKey : TAccountKey) : Integer; function TryGetKey(const AAccountKey : TAccountKey; out AKey : TWalletKey) : Boolean; + {$IFDEF INTERNAL_USE_SETTINGS_UNIT} + // Ordinary selection never changes keys or settings. A successful mined + // block may explicitly rotate NewEachTime while the wallet is unlocked. + function TryGetMiningKey(AMode: TMinerPrivateKeyType; const ASelectedPublicKey: TRawBytes; + out AMiningKey: TAccountKey; AAfterMinedBlock: Boolean = False): Boolean; + {$ENDIF} Procedure Delete(index : Integer); virtual; Procedure Clear; virtual; Function Count : Integer; @@ -96,11 +106,11 @@ interface FOrderedAccountKeysList : TOrderedAccountKeysList; procedure SetSafeBox(const Value: TPCSafeBox); function GetSafeBox: TPCSafeBox; + protected + procedure NotifyKeyAdded(const AAccountKey: TAccountKey); override; public Constructor Create(AOwner : TComponent); override; Destructor destroy; override; - Function AddPrivateKey(Const Name : String; ECPrivateKey : TECPrivateKey) : Integer; override; - Function AddPublicKey(Const Name : String; ECDSA_Public : TECDSA_Public) : Integer; override; Procedure Delete(index : Integer); override; Procedure Clear; override; Property AccountsKeyList : TOrderedAccountKeysList read FOrderedAccountKeysList; @@ -132,6 +142,8 @@ TWallet = class {$IFDEF INTERNAL_USE_FOLDERHELPER_UNIT} class procedure Load; {$ENDIF} + // Release key material and its file only after wallet listeners have detached. + class procedure Unload; class function HasKey(const AKey: TWalletKey) : boolean; class procedure DeleteKey(const AKey: TWalletKey); class procedure GenerateNewKey(const AName: String; AEncryptionTypeNID : Word); @@ -142,6 +154,7 @@ TWallet = class class function RestoreWallet(const AFileName, APassword: String) : TRestoreWalletResult; class procedure BackupWallet(const AFileName: String); class function TryDecryptPrivateKey(const AEncryptedKeyHexString,APassword:String; out APrivateKey : TECPrivateKey; out AMessage : String) : boolean; + class function TryParsePublicKey(const AKeyImportText: String; out AAccountKey: TAccountKey; out AMessage: String): Boolean; class function TryParseEncryptedKey(const AKeyHexString, AKeyPassword : String; out AKey : TECPrivateKey) : boolean; class function TryParseRawKey(const ARawBytes : TRawBytes; AEncryptionTypeNID : Word; out AKey : TECPrivateKey) : boolean; class function TryParseHexKey(const AHexString : String; AEncryptionTypeNID : Word; out AKey : TECPrivateKey) : boolean; @@ -152,7 +165,7 @@ TWallet = class implementation uses - SysUtils, UConst, ULog, + SysUtils, UConst, ULog, UMemory, {$IFDEF INTERNAL_USE_FOLDERHELPER_UNIT} UFolderHelper, {$ENDIF} @@ -209,7 +222,7 @@ function TWalletKeys.AddPrivateKey(Const Name : String; ECPrivateKey: TECPrivate end; end; if Not FIsReadingStream then SaveToStream(FWalletFileStream); - FOnChanged.Invoke(Self); + NotifyKeyAdded(ECPrivateKey.PublicKey); end; function TWalletKeys.AddPublicKey(const Name: String; ECDSA_Public: TECDSA_Public): Integer; @@ -230,6 +243,11 @@ function TWalletKeys.AddPublicKey(const Name: String; ECDSA_Public: TECDSA_Publi P^.Name := Name; end; if Not FIsReadingStream then SaveToStream(FWalletFileStream); + NotifyKeyAdded(ECDSA_Public); +end; + +procedure TWalletKeys.NotifyKeyAdded(const AAccountKey: TAccountKey); +begin FOnChanged.Invoke(Self); end; @@ -243,6 +261,7 @@ procedure TWalletKeys.Clear; Dispose(P); end; FSearchableKeys.Clear; + FMiningKey := CT_TECDSA_Public_Nul; FIsValidPassword := true; end; @@ -368,6 +387,63 @@ function TWalletKeys.TryGetKey(const AAccountKey : TAccountKey; out AKey : TWall Result := True; end; +{$IFDEF INTERNAL_USE_SETTINGS_UNIT} +function TWalletKeys.TryGetMiningKey(AMode: TMinerPrivateKeyType; + const ASelectedPublicKey: TRawBytes; out AMiningKey: TAccountKey; + AAfterMinedBlock: Boolean): Boolean; +var + i, LIndex, LEligibleCount: Integer; + LKey: TWalletKey; + LPrivateKey: TECPrivateKey; + LDisposables: TDisposables; +begin + AMiningKey := CT_TECDSA_Public_Nul; + // Receiving a reward needs its public key, so an encrypted private key is + // eligible even while locked. Generating a new secret requires an unlock. + if AAfterMinedBlock and (AMode = mpk_NewEachTime) and + IsValidPassword and (not IsReadOnly) then begin + LPrivateKey := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LPrivateKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + AddPrivateKey('User Key ' + FormatDateTime('YYYY-MM-DD hh:nn', Now), LPrivateKey); + FMiningKey := LPrivateKey.PublicKey; + AMiningKey := FMiningKey; + Exit(True); + end; + + // Match the saved bytes against existing keys rather than parsing untrusted + // settings. Missing, malformed and watch-only selections cannot create keys. + if AMode = mpk_Selected then + for i := 0 to Count - 1 do begin + LKey := Key[i]; + if LKey.HasPrivateKey and TBaseType.Equals(LKey.SearchableAccountKey, ASelectedPublicKey) then begin + FMiningKey := LKey.AccountKey; + AMiningKey := FMiningKey; + Exit(True); + end; + end; + + LIndex := IndexOfAccountKey(FMiningKey); + if (not (AAfterMinedBlock and (AMode = mpk_Random))) and + (LIndex >= 0) and Key[LIndex].HasPrivateKey then begin + AMiningKey := FMiningKey; + Exit(True); + end; + LIndex := -1; + LEligibleCount := 0; + for i := 0 to Count - 1 do + if Key[i].HasPrivateKey then begin + Inc(LEligibleCount); + if (LIndex < 0) or ((AMode = mpk_Random) and (Random(LEligibleCount) = 0)) then + LIndex := i; + end; + Result := LIndex >= 0; + if Result then begin + FMiningKey := Key[LIndex].AccountKey; + AMiningKey := FMiningKey; + end else FMiningKey := CT_TECDSA_Public_Nul; +end; +{$ENDIF} + procedure TWalletKeys.LoadFromStream(Stream: TStream); Var fileversion,i,l,j : Integer; raw : TRawBytes; @@ -510,22 +586,11 @@ procedure TWalletKeys.SetWalletPassword(const Value: String); { TWalletKeysExt } -function TWalletKeysExt.AddPrivateKey(const Name: String; - ECPrivateKey: TECPrivateKey): Integer; -begin - Result := inherited AddPrivateKey(Name,ECPrivateKey); - if Assigned(FOrderedAccountKeysList) then begin - FOrderedAccountKeysList.AddAccountKey(ECPrivateKey.PublicKey); - end; -end; - -function TWalletKeysExt.AddPublicKey(const Name: String; - ECDSA_Public: TECDSA_Public): Integer; +procedure TWalletKeysExt.NotifyKeyAdded(const AAccountKey: TAccountKey); begin - Result := inherited AddPublicKey(Name,ECDSA_Public); - if Assigned(FOrderedAccountKeysList) then begin - FOrderedAccountKeysList.AddAccountKey(ECDSA_Public); - end; + if Assigned(FOrderedAccountKeysList) then + FOrderedAccountKeysList.AddAccountKey(AAccountKey); + inherited NotifyKeyAdded(AAccountKey); end; procedure TWalletKeysExt.Clear; @@ -584,41 +649,10 @@ procedure TWalletKeysExt.SetSafeBox(const Value: TPCSafeBox); {$IFDEF INTERNAL_USE_SETTINGS_UNIT} class function TWallet.GetMiningKey: TAccountKey; -Var PK : TECPrivateKey; - i : Integer; - PublicK : TECDSA_Public; begin CheckLoaded; - Result := CT_TECDSA_Public_Nul; - case TSettings.MinerPrivateKeyType of - mpk_NewEachTime: PublicK := CT_TECDSA_Public_Nul; - mpk_Selected: PublicK := TAccountComp.RawString2Accountkey(TSettings.MinerSelectedPublicKey); - mpk_Random: begin - PublicK := CT_TECDSA_Public_Nul; - if FKeys.Count>0 then PublicK := FKeys.Key[Random(FKeys.Count)].AccountKey; - end; - end; - i := FKeys.IndexOfAccountKey(PublicK); - if i>=0 then begin - if (Length(FKeys.Key[i].CryptedKey)=0) then i:=-1; - end; - if i<0 then begin - // Generate a new key - PK := TECPrivateKey.Create; - try - PK.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); - FKeys.AddPrivateKey('User Key '+FormatDateTime('YYYY-MM-DD hh:nn' ,Now), PK); - PublicK := PK.PublicKey; - // Set to Settings if not mpk_NewEachTime - if (TSettings.MinerPrivateKeyType<>mpk_NewEachTime) then begin - TSettings.MinerPrivateKeyType:=mpk_Selected; - TSettings.MinerSelectedPublicKey := TAccountComp.AccountKey2RawString(PublicK); - end; - finally - PK.Free; - end; - end; - Result := PublicK; + FKeys.TryGetMiningKey(TSettings.MinerPrivateKeyType, + TSettings.MinerSelectedPublicKey, Result); end; {$ENDIF} @@ -644,6 +678,11 @@ class procedure TWallet.Load; end; {$ENDIF} +class procedure TWallet.Unload; +begin + FreeAndNil(FKeys); +end; + class function TWallet.HasKey(const AKey: TWalletKey) : boolean; begin Result := FKeys.IndexOfAccountKey(AKey.AccountKey) >= 0; @@ -664,14 +703,13 @@ class procedure TWallet.DeleteKey(const AKey: TWalletKey); class procedure TWallet.GenerateNewKey(const AName: String; AEncryptionTypeNID : Word); var privateKey : TECPrivateKey; + disposed: TDisposables; begin - privateKey := TECPrivateKey.Create; - try - privateKey.GenerateRandomPrivateKey(AEncryptionTypeNID); - FKeys.AddPrivateKey(AName, privateKey); - finally - privateKey.Free; - end; + CheckLoaded; + CheckUnlocked; + privateKey := disposed.AddObject(TECPrivateKey.Create) as TECPrivateKey; + privateKey.GenerateRandomPrivateKey(AEncryptionTypeNID); + FKeys.AddPrivateKey(AName, privateKey); end; class function TWallet.ExportPublicKey(const AKey: TWalletKey) : String; @@ -681,7 +719,12 @@ class function TWallet.ExportPublicKey(const AKey: TWalletKey) : String; class function TWallet.ExportPrivateKey(const AKey: TWalletKey; const APassword: String) : String; begin - Result := TPCEncryption.DoPascalCoinAESEncrypt(AKey.PrivateKey.ExportToRaw,TEncoding.ANSI.GetBytes(APassword)).ToHexaString; + if not Assigned(AKey.PrivateKey) then + raise Exception.Create('The private key is unavailable. Unlock the wallet before exporting it.'); + if APassword = '' then + Result := AKey.PrivateKey.ExportToRaw.ToHexaString + else + Result := TPCEncryption.DoPascalCoinAESEncrypt(AKey.PrivateKey.ExportToRaw,TEncoding.ANSI.GetBytes(APassword)).ToHexaString; end; class procedure TWallet.ImportPrivateKey(const AName, AKeyImportText, APassword: String); @@ -707,74 +750,93 @@ class procedure TWallet.ImportPrivateKey(const AName, AKeyImportText, APassword: end; class procedure TWallet.ImportPublicKey(Const AName, AKeyImportText : String); -var raw : TRawBytes; +var errors : String; accountKey : TAccountKey; begin CheckLoaded; CheckUnlocked; - If not TAccountComp.AccountPublicKeyImport(AKeyImportText, accountKey, errors) then begin - raw := TCrypto.HexaToRaw(AKeyImportText); - if Length(raw)=0 then - raise Exception.Create('Invalid public key value (Not hexa or not an imported format)'+#10+errors); - accountKey := TAccountComp.RawString2Accountkey(raw); - end; - If not TAccountComp.IsValidAccountKey(accountKey,CT_BUILD_PROTOCOL,errors) then - raise Exception.Create('This data is not a valid public key'+#10+errors); + if not TryParsePublicKey(AKeyImportText, accountKey, errors) then + raise Exception.Create(errors); if FKeys.IndexOfAccountKey(accountKey)>=0 then raise exception.Create('This key exists on your wallet'); FKeys.AddPublicKey(AName, accountKey); end; +class function TWallet.TryParsePublicKey(const AKeyImportText: String; out AAccountKey: TAccountKey; out AMessage: String): Boolean; +var + LRaw: TRawBytes; + LText: String; +begin + Result := False; + AMessage := ''; + AAccountKey := CT_TECDSA_Public_Nul; + LText := Trim(AKeyImportText); + if not TAccountComp.AccountPublicKeyImport(LText, AAccountKey, AMessage) then + begin + if not TCrypto.HexaToRaw(LText, LRaw) or (Length(LRaw) = 0) then + begin + AMessage := 'Enter a valid exported public key or raw hexadecimal public key.'; + Exit; + end; + AAccountKey := TAccountComp.RawString2Accountkey(LRaw); + end; + if (Length(AAccountKey.x) = 0) or (Length(AAccountKey.y) = 0) then + begin + AAccountKey := CT_TECDSA_Public_Nul; + AMessage := 'A public key must contain both curve coordinates.'; + Exit; + end; + Result := TAccountComp.IsValidAccountKey(AAccountKey, CT_BUILD_PROTOCOL, AMessage); + if not Result then + AAccountKey := CT_TECDSA_Public_Nul; +end; + class function TWallet.RestoreWallet(const AFileName, APassword: String) : TRestoreWalletResult; var wki : TWalletKeys; i, j : Integer; + disposed: TDisposables; begin + // Failed passwords and duplicate-only restores must return deterministic counts. + Result := Default(TRestoreWalletResult); CheckLoaded; + CheckUnlocked; if NOT FileExists(AFileName) then raise Exception.Create('File not found: ' + AFilename); - wki := TWalletKeys.Create(nil); - try - wki.WalletFileName := AFileName; - if wki.Count<=0 then - raise Exception.Create('Wallet file has no valid data'); - Result.ImportedPrivateKeys := 0; - Result.ImportedPublicKeys := 0; - Result.Success := false; - - // If password required, set it - if NOT wki.IsValidPassword then begin - wki.WalletPassword := APassword; - if NOT wki.IsValidPassword then - exit; - end; + wki := disposed.AddObject(TWalletKeys.Create(nil)) as TWalletKeys; + wki.WalletFileName := AFileName; + if wki.Count<=0 then + raise Exception.Create('Wallet file has no valid data'); + // If password required, set it + if NOT wki.IsValidPassword then begin + wki.WalletPassword := APassword; + if NOT wki.IsValidPassword then + exit; + end; - // Import the keys - Result.TotalKeysFound:= wki.Count; - for i := 0 to wki.Count - 1 do begin - if NOT HasKey(wki.Key[i]) then begin - if wki.Key[i].HasPrivateKey then begin - TWallet.Keys.AddPrivateKey(wki.Key[i].Name, wki.Key[i].PrivateKey); - inc(Result.ImportedPrivateKeys); - end else begin - TWallet.Keys.AddPublicKey(wki.Key[i].Name, wki.Key[i].AccountKey); - inc(Result.ImportedPublicKeys); - end; + // Import the keys + Result.TotalKeysFound:= wki.Count; + for i := 0 to wki.Count - 1 do begin + if NOT HasKey(wki.Key[i]) then begin + if wki.Key[i].HasPrivateKey then begin + TWallet.Keys.AddPrivateKey(wki.Key[i].Name, wki.Key[i].PrivateKey); + inc(Result.ImportedPrivateKeys); end else begin - j := FKeys.IndexOfAccountKey(wki[i].AccountKey); - // case: existing wallet has public key but import has private key - if (NOT FKeys[j].HasPrivateKey) AND (wki.Key[i].HasPrivateKey) then begin - TWallet.Keys.AddPrivateKey(wki.Key[i].Name, wki.Key[i].PrivateKey); - inc(Result.ImportedPrivateKeys); - end else Inc(Result.Duplicates); + TWallet.Keys.AddPublicKey(wki.Key[i].Name, wki.Key[i].AccountKey); + inc(Result.ImportedPublicKeys); end; + end else begin + j := FKeys.IndexOfAccountKey(wki[i].AccountKey); + // case: existing wallet has public key but import has private key + if (NOT FKeys[j].HasPrivateKey) AND (wki.Key[i].HasPrivateKey) then begin + TWallet.Keys.AddPrivateKey(wki.Key[i].Name, wki.Key[i].PrivateKey); + inc(Result.ImportedPrivateKeys); + end else Inc(Result.Duplicates); end; - result.Success := true; - finally - wki.Free; end; + result.Success := true; end; class procedure TWallet.BackupWallet(const AFileName : String); @@ -794,20 +856,46 @@ class procedure TWallet.BackupWallet(const AFileName : String); class function TWallet.TryDecryptPrivateKey(const AEncryptedKeyHexString, APassword:String; out APrivateKey : TECPrivateKey; out AMessage : String) : boolean; var parseResult : Boolean; + LText: String; + LRaw: TRawBytes; + LNID, LKeyLength: Word; begin APrivateKey := nil; AMessage := ''; - if NOT TCrypto.IsHexString(AEncryptedKeyHexString) then begin + LText := Trim(AEncryptedKeyHexString); + if NOT TCrypto.IsHexString(LText) then begin Result := false; AMessage := 'Invalid key text. You must enter a hexadecimal value ("0".."9" or "A".."F").'; exit; end; - case Length(AEncryptedKeyHexString) div 2 of - 32: parseResult := TryParseHexKey(AEncryptedKeyHexString, CT_NID_secp256k1, APrivateKey); - 35,36: parseResult := TryParseHexKey(AEncryptedKeyHexString, CT_NID_sect283k1, APrivateKey); - 48: parseResult := TryParseHexKey(AEncryptedKeyHexString, CT_NID_secp384r1, APrivateKey); - 65,66: parseResult := TryParseHexKey(AEncryptedKeyHexString, CT_NID_secp521r1, APrivateKey); - 64, 80, 96: parseResult := TryParseEncryptedKey(AEncryptedKeyHexString, APassword, APrivateKey); + LRaw := TCrypto.HexaToRaw(LText); + // ExportToRaw includes the curve and a two-byte secret length. Keep this + // existing format distinct from the legacy bare secret and AES exports. + if Length(LRaw) >= 4 then + begin + Move(LRaw[0], LNID, SizeOf(LNID)); + Move(LRaw[2], LKeyLength, SizeOf(LKeyLength)); + if TAccountComp.IsValidEC_OpenSSL_NID(LNID) and (LKeyLength > 0) and + (LKeyLength <= 66) then + begin + if Integer(LKeyLength) <> Length(LRaw) - 4 then + begin + AMessage := 'The serialized private key length does not match its data.'; + Exit(False); + end; + APrivateKey := TECPrivateKey.ImportFromRaw(LRaw); + Result := Assigned(APrivateKey); + if not Result then + AMessage := 'Unencrypted key data is invalid or corrupted'; + Exit; + end; + end; + case Length(LRaw) of + 32: parseResult := TryParseHexKey(LText, CT_NID_secp256k1, APrivateKey); + 35,36: parseResult := TryParseHexKey(LText, CT_NID_sect283k1, APrivateKey); + 48: parseResult := TryParseHexKey(LText, CT_NID_secp384r1, APrivateKey); + 65,66: parseResult := TryParseHexKey(LText, CT_NID_secp521r1, APrivateKey); + 64, 80, 96: parseResult := TryParseEncryptedKey(LText, APassword, APrivateKey); else begin result := false; AMessage := 'Invalidly formatted private key string. Ensure it is an encrypted private key export or raw private key hexstring.'; @@ -816,7 +904,7 @@ class function TWallet.TryDecryptPrivateKey(const AEncryptedKeyHexString, APassw end; if NOT parseResult then begin Result := false; - if Length(AEncryptedKeyHexString) div 2 in [64, 80, 96] then + if Length(LRaw) in [64, 80, 96] then AMessage := 'Incorrect password' else AMessage := 'Unencrypted key data is invalid or corrupted'; diff --git a/src/core/upcdaemon.pas b/src/core/upcdaemon.pas index f71b28703..9a37a35b6 100644 --- a/src/core/upcdaemon.pas +++ b/src/core/upcdaemon.pas @@ -72,7 +72,7 @@ interface FLogsDate : TDate; function GetDataFolder : String; procedure OnNetDataReceivedHelloMessage(Sender : TObject); - procedure OnInitSafeboxProgressNotify(sender : TObject; const message : String; curPos, totalCount : Int64); + procedure OnInitSafeboxProgressNotify(sender : TObject; const AProgress : TProgressInfo); procedure UpdateLogFilenames; protected Procedure BCExecute; override; @@ -113,7 +113,7 @@ interface implementation -uses UPCTNetDataExtraMessages; +uses UPCTNetDataExtraMessages, UProgressDisplay; Var _FLog : TLog; @@ -151,9 +151,11 @@ procedure TPCDaemonThread.OnNetDataReceivedHelloMessage(Sender: TObject); end; procedure TPCDaemonThread.OnInitSafeboxProgressNotify(sender: TObject; - const message: String; curPos, totalCount: Int64); + const AProgress: TProgressInfo); +var LMessage: String; begin - TLog.NewLog(ltdebug,ClassName,Format('Progress (%d/%d): %s',[curPos,totalCount,message])); + LMessage := FormatProgressLogInfo(AProgress); + if LMessage <> '' then TLog.NewLog(ltdebug, ClassName, LMessage); end; procedure TPCDaemonThread.UpdateLogFilenames; diff --git a/src/gui-classic/UFRMOperationsExplorer.pas b/src/gui-classic/UFRMOperationsExplorer.pas index 4a28a3126..8adf4c02b 100644 --- a/src/gui-classic/UFRMOperationsExplorer.pas +++ b/src/gui-classic/UFRMOperationsExplorer.pas @@ -70,6 +70,7 @@ TFRMOperationsExplorer = class(TForm) miLoadFromBlockchain: TMenuItem; MiLoadOperationsFromBlockchainFile: TMenuItem; MiTools: TMenuItem; + MiRPCCalls: TMenuItem; mMenu: TMainMenu; mOperationInfo: TMemo; mOperationExport: TMemo; @@ -106,6 +107,8 @@ TFRMOperationsExplorer = class(TForm) FOperationsHashTree : TOperationsHashTree; FSourceNode: TNode; FSourceWalletKeys: TWalletKeys; + FOffline: Boolean; + procedure SetOffline(AValue: Boolean); procedure OnOperationsHashTreeChanged(Sender : TObject); procedure SetSourceNode(AValue: TNode); procedure DoLoadFromStorageAssistant(AStorage : TStorage); @@ -118,6 +121,12 @@ TFRMOperationsExplorer = class(TForm) public Property SourceNode : TNode read FSourceNode write SetSourceNode; Property SourceWalletKeys : TWalletKeys read FSourceWalletKeys write SetSourceWalletKeys; + // Offline consumers may prepare, sign and exchange RAW operations without + // exposing a network/RPC execution path. Classic callers default to online. + property Offline: Boolean read FOffline write SetOffline; + function TryImportRawOperations(const AHex: String; out AErrors: String): Boolean; + // The form owns this tree; consumers must not free it. + property Operations: TOperationsHashTree read FOperationsHashTree; end; implementation @@ -132,7 +141,7 @@ implementation {$IFDEF TESTNET} UFRMRandomOperations, {$ENDIF} - UPCDataTypes, UFRMRPCCalls, UFRMMemoText; + UPCDataTypes, UFRMRPCCalls, UFRMMemoText, UMemory; { TFRMOperationsExplorer } @@ -456,7 +465,9 @@ procedure TFRMOperationsExplorer.ActExecuteOperationUpdate(Sender: TObject); Var obj : TObject; begin obj := GetSelectedOperation; - TAction(Sender).Enabled:= ((Assigned(obj)) And (Assigned(FSourceNode))); + // The legacy resource also uses this updater for the local Sign action. + TAction(Sender).Enabled:= ((not FOffline) or (Sender = ActSign)) And + ((Assigned(obj)) And (Assigned(FSourceNode))); end; procedure TFRMOperationsExplorer.ActRemoveAccountFromMultioperationExecute(Sender: TObject); @@ -497,6 +508,7 @@ procedure TFRMOperationsExplorer.ActExecuteOperationExecute(Sender: TObject); Var op : TPCOperation; errors : String; begin + if FOffline then Exit; If Not Assigned(FSourceNode) then Raise Exception.Create('No node to Execute'); op := GetSelectedOperation; If Not Assigned(op) then exit; @@ -528,32 +540,124 @@ procedure TFRMOperationsExplorer.lbOperationsClick(Sender: TObject); end; procedure TFRMOperationsExplorer.MiImportOperationsFromTxtClick(Sender: TObject); -Var i : Integer; - raw : TRawBytes; - auxS : String; - errors : String; - opht : TOperationsHashTree; - ms : TMemoryStream; +var + auxS, errors: String; begin auxS := ''; if Not InputMemoQuery('Paste a RAW hexadecimal operations:',False,auxS) then Exit; + if not TryImportRawOperations(auxS, errors) then + raise Exception.Create(errors); + if errors <> '' then + Application.MessageBox(PChar(errors), PChar(Caption), MB_OK + MB_ICONWARNING); +end; - if Not TCrypto.HexaToRaw(auxS, raw) then Raise Exception.Create('Invalid hexadecimal RAW'); - - If Length(raw)=0 then Exit; - ms := TMemoryStream.Create; - opht := TOperationsHashTree.Create; - Try - ms.Write(raw[0],Length(raw)); - ms.Position:=0; - If Not opht.LoadOperationsHashTreeFromStream(ms,false,TNode.Node.Bank.SafeBox.CurrentProtocol,TNode.Node.Bank.SafeBox.CurrentProtocol,Nil,errors) then Raise Exception.Create(errors); - For i:=0 to opht.OperationsCount-1 do begin - FOperationsHashTree.AddOperationToHashTree(opht.GetOperation(i)); +function TFRMOperationsExplorer.TryImportRawOperations(const AHex: String; out AErrors: String): Boolean; +var + LDisposables: TDisposables; + LRaw: TRawBytes; + LStream: TMemoryStream; + LImported, LCombined, LBackup: TOperationsHashTree; + LSigners: TList; + LIncoming, LExisting: TPCOperation; + LProtocol: Word; + LNotify: TNotifyEvent; + i, j, k, LExistingIndex: Integer; +begin + Result := False; + AErrors := ''; + LNotify := nil; + if not TCrypto.HexaToRaw(Trim(AHex), LRaw) or (Length(LRaw) = 0) then + begin + AErrors := 'Enter a nonempty RAW hexadecimal operation package.'; + Exit; + end; + LProtocol := CT_PROTOCOL_MAX; + if Assigned(FSourceNode) then + LProtocol := FSourceNode.Bank.SafeBox.CurrentProtocol; + LStream := LDisposables.AddObject(TMemoryStream.Create) as TMemoryStream; + LImported := LDisposables.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LCombined := LDisposables.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LBackup := LDisposables.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LSigners := LDisposables.AddObject(TList.Create) as TList; + try + LStream.WriteBuffer(LRaw[0], Length(LRaw)); + LStream.Position := 0; + if not LImported.LoadOperationsHashTreeFromStream(LStream, False, LProtocol, LProtocol, nil, AErrors) then + Exit; + if (LStream.Position <> LStream.Size) or (LImported.OperationsCount = 0) then + begin + AErrors := 'The RAW operation package is empty or has trailing data.'; + Exit; end; - finally - ms.Free; - opht.Free; + // CopyFromHashTree shares operation objects. Stage an independent decoded + // copy so importing signatures or a later conflict cannot mutate the UI's + // current package before the complete import has succeeded. + LStream.Clear; + FOperationsHashTree.SaveOperationsHashTreeToStream(LStream, False); + LStream.Position := 0; + if not LCombined.LoadOperationsHashTreeFromStream(LStream, False, LProtocol, LProtocol, nil, AErrors) then + Exit; + for i := 0 to LImported.OperationsCount - 1 do + begin + LIncoming := LImported.GetOperation(i); + LExistingIndex := LCombined.IndexOfOperation(LIncoming); + if LExistingIndex >= 0 then + begin + LExisting := LCombined.GetOperation(LExistingIndex); + if (LExisting is TOpMultiOperation) and (LIncoming is TOpMultiOperation) then + if TOpMultiOperation(LExisting).MergeMissingSignaturesFrom(TOpMultiOperation(LIncoming)) < 0 then + begin + AErrors := 'The returned signatures belong to a different multioperation.'; + Exit; + end; + Continue; + end; + LSigners.Clear; + LIncoming.SignerAccounts(LSigners); + for j := 0 to LCombined.OperationsCount - 1 do + begin + LExisting := LCombined.GetOperation(j); + for k := 0 to LSigners.Count - 1 do + if LExisting.IsSignerAccount(LSigners[k]) and + (LExisting.GetAccountN_Operation(LSigners[k]) = LIncoming.GetAccountN_Operation(LSigners[k])) then + begin + AErrors := 'The RAW operation package reuses a signer operation counter.'; + Exit; + end; + end; + if not LCombined.AddOperationToHashTree(LImported.GetOperation(i)) then + begin + AErrors := 'The RAW operation package conflicts with an existing operation.'; + Exit; + end; + end; + LBackup.CopyFromHashTree(FOperationsHashTree); + LNotify := FOperationsHashTree.OnChanged; + FOperationsHashTree.OnChanged := nil; + try + try + FOperationsHashTree.CopyFromHashTree(LCombined); + except + FOperationsHashTree.CopyFromHashTree(LBackup); + raise; + end; + finally + FOperationsHashTree.OnChanged := LNotify; + end; + Result := True; + except + on E: Exception do + AErrors := E.Message; end; + // Rendering is separate from importing. A callback failure must not claim + // that the successfully committed package was rejected or rolled back. + if Result and Assigned(LNotify) then + try + LNotify(FOperationsHashTree); + except + on E: Exception do + AErrors := 'The operation package was imported, but its display could not be refreshed: ' + E.Message; + end; end; procedure TFRMOperationsExplorer.miLoadFromBlockchainClick(Sender: TObject); @@ -589,6 +693,7 @@ procedure TFRMOperationsExplorer.MiLoadOperationsFromBlockchainFileClick(Sender: procedure TFRMOperationsExplorer.MiRPCCallsClick(Sender: TObject); Var FRM : TFRMRPCCalls; begin + if FOffline then Exit; FRM := TFRMRPCCalls.Create(Self); Try FRM.ShowModal; @@ -597,6 +702,14 @@ procedure TFRMOperationsExplorer.MiRPCCallsClick(Sender: TObject); end; end; +procedure TFRMOperationsExplorer.SetOffline(AValue: Boolean); +begin + FOffline := AValue; + if Assigned(MiRPCCalls) then + MiRPCCalls.Enabled := not FOffline; + ActExecuteOperationUpdate(ActExecuteOperation); +end; + procedure TFRMOperationsExplorer.OnOperationsHashTreeChanged(Sender: TObject); begin UpdateHashTreeList; @@ -712,7 +825,7 @@ procedure TFRMOperationsExplorer.UpdateSelectedOperationInfo; ms.Free; opht.Free; end; - If op.OperationToOperationResume(FSourceNode.Bank.BlocksCount,op,True,op.SignerAccount,opr) then begin + If Assigned(FSourceNode) and op.OperationToOperationResume(FSourceNode.Bank.BlocksCount,op,True,op.SignerAccount,opr) then begin jsonObj := TPCJSONObject.Create; Try TPascalCoinJSONComp.FillOperationObject(opr,FSourceNode.Bank.BlocksCount, diff --git a/src/gui-classic/UFRMWallet.pas b/src/gui-classic/UFRMWallet.pas index a5b655faa..1df165aac 100644 --- a/src/gui-classic/UFRMWallet.pas +++ b/src/gui-classic/UFRMWallet.pas @@ -41,6 +41,7 @@ interface Const CM_PC_WalletKeysChanged = WM_USER + 1; CM_PC_NetConnectionUpdated = WM_USER + 2; + CM_PC_MinerBlockFound = WM_USER + 3; type @@ -229,10 +230,15 @@ TFRMWallet = class(TForm) procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Test_ShowDiagnosticTool(Sender: TObject); private + FLogBatchLines, FLogEntryLines: TStringList; + FLogBatchActive, FLogBatchChanged, FLogBatchTerminated: Boolean; FLastNodesCacheUpdatedTS : TDateTime; FBackgroundPanel : TPanel; FBackgroundLabel : TLabel; FMinersBlocksFound: Integer; + procedure OnLogBatchBegin(Sender: TObject); + procedure OnLogBatchEnd(Sender: TObject); + procedure SplitLogLines(const AText: String; ALines: TStrings); procedure SetMinersBlocksFound(const Value: Integer); Procedure CheckIsReady; Procedure FinishedLoadingApp; @@ -273,6 +279,7 @@ TFRMWallet = class(TForm) FRPCServer : TRPCServer; FMustProcessWalletChanged : Boolean; FMustProcessNetConnectionUpdated : Boolean; + FWaitingForMempool, FPendingMiningConfiguration: Boolean; FThreadActivate : TObject; FLastAccountsGridInvalidateTC : TTickCount; Procedure OnNewAccount(Sender : TObject); @@ -294,14 +301,16 @@ TFRMWallet = class(TForm) Procedure UpdateOperations; Procedure UpdateConfigChanged(Sender:TObject); Procedure UpdateNodeStatus; + procedure OnNodeSynchronizationChanged(Sender: TObject; const AStatus: TNodeSynchronizationStatus); Procedure UpdateAvailableConnections; procedure Activate; override; Function ForceMining : Boolean; virtual; - Function GetAccountKeyForMiner : TAccountKey; + Function GetAccountKeyForMiner(AAfterMinedBlock: Boolean = False) : TAccountKey; Procedure DoUpdateAccounts; Function DoUpdateAccountsFilter : Boolean; procedure CM_WalletChanged(var Msg: TMessage); message CM_PC_WalletKeysChanged; procedure CM_NetConnectionUpdated(var Msg: TMessage); message CM_PC_NetConnectionUpdated; + procedure CM_MinerBlockFound(var Msg: TMessage); message CM_PC_MinerBlockFound; public { Public declarations } Property WalletKeys : TWalletKeysExt read FWalletKeys; @@ -319,7 +328,7 @@ implementation {$R *.lfm} {$ENDIF} -Uses UFolderHelper,{$IFDEF USE_GNUGETTEXT}gnugettext,{$ENDIF} +Uses {$IFDEF FPC}WSControls,{$ENDIF} UProgressDisplay, UFolderHelper,{$IFDEF USE_GNUGETTEXT}gnugettext,{$ENDIF} {$IFDEF Use_OpenSSL} UOpenSSL, {$ENDIF} @@ -346,7 +355,7 @@ implementation private FLastTC : TTickCount; FLastMsg : String; - procedure OnProgressNotify(sender : TObject; const mesage : String; curPos, totalCount : Int64); + procedure OnProgressNotify(sender : TObject; const AProgress : TProgressInfo); procedure ThreadSafeNotify; protected procedure BCExecute; override; @@ -354,14 +363,11 @@ implementation { TThreadActivate } -procedure TThreadActivate.OnProgressNotify(sender: TObject; const mesage: String; curPos, totalCount: Int64); -var pct : String; +procedure TThreadActivate.OnProgressNotify(sender: TObject; const AProgress: TProgressInfo); begin If TPlatform.GetElapsedMilliseconds(FLastTC)>500 then begin FLastTC := TPlatform.GetTickCount; - if (totalCount>0) then pct := Format('%.2f',[curPos*100/totalCount])+'%' - else pct := ''; - FLastMsg:='Please wait until finished: '+mesage+' '+pct; + FLastMsg := 'Please wait until finished: ' + FormatProgressInfo(AProgress); Synchronize(ThreadSafeNotify); end; end; @@ -384,14 +390,16 @@ procedure TThreadActivate.BCExecute; FLastMsg := ''; // {$IFDEF PRODUCTION} - OnProgressNotify(Self,'Reading Hardcoded RandomHash file',0,0); + FLastMsg := 'Please wait until finished: Reading Hardcoded RandomHash file'; + Synchronize(ThreadSafeNotify); LRaw := TCrypto.HexaToRaw(CT_Hardcoded_RandomHash_Table_HASH); TPascalCoinProtocol.AllowUseHardcodedRandomHashTable( ExtractFileDir(Application.ExeName)+PathDelim+CT_Hardcoded_RandomHash_Table_Filename, LRaw ); {$ENDIF} FLastTC := 0; - OnProgressNotify(Self,'Initializing databases',0,0); + FLastMsg := 'Please wait until finished: Initializing databases'; + Synchronize(ThreadSafeNotify); // Read Operations saved from disk TAbstractMemBlockchainStorage( TNode.Node.Bank.Storage ).AutoFlushCache := False; TNode.Node.InitSafeboxAndOperations($FFFFFFFF,OnProgressNotify); // New Build 2.1.4 to load pending operations buffer @@ -602,11 +610,17 @@ procedure TFRMWallet.cbMyPrivateKeysChange(Sender: TObject); end; procedure TFRMWallet.CheckIsReady; -Var isready : String; +Var isready, LBankDiagnostic : String; + LProgress : TProgressInfo; begin if Not Assigned(FNode) then Abort; if Not FNode.IsReady(isready) then begin + // Keep domain readiness reasons; reconstruct only the active download text. + if FNode.Bank.IsReady(LBankDiagnostic) then + if FNode.NetServer.Active then + if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then + isready := FormatProgressInfo(LProgress); Raise Exception.Create('You cannot do this operation now:'+#10+#10+isready); end; end; @@ -630,7 +644,9 @@ procedure TFRMWallet.CM_NetConnectionUpdated(var Msg: TMessage); strings.BeginUpdate; Try for i := 0 to l.Count - 1 do begin - NC := l[i]; + // Keep the outer registry lock while copying the row. Membership + // alone includes retiring entries whose transport is already freed. + if not TNetData.NetData.GetConnection(i,NC) then Continue; If NC.Client.BytesReceived>0 then begin sClientApp := '['+IntToStr(NC.NetProtocolVersion.protocol_version)+'-'+IntToStr(NC.NetProtocolVersion.protocol_available)+'] '+NC.ClientAppVersion; end else begin @@ -875,13 +891,20 @@ procedure TFRMWallet.FinishedLoadingApp; FPoolMiningServer.Port := TSettings.JsonRpcMinerServerPort; FPoolMiningServer.MinerAccountKey := GetAccountKeyForMiner; FPoolMiningServer.MinerPayload := TEncoding.ANSI.GetBytes(TSettings.MinerName); - LLockedMempool := FNode.LockMempoolWrite; - try - LLockedMempool.AccountKey := GetAccountKeyForMiner; - finally - FNode.UnlockMempoolWrite; - end; - FPoolMiningServer.Active := TSettings.JsonRpcMinerServerActive; + if FNode.PendingOperationsAvailable then begin + try + LLockedMempool := FNode.LockMempoolWrite; + try + LLockedMempool.AccountKey := GetAccountKeyForMiner; + finally + FNode.UnlockMempoolWrite; + end; + except + on E: EPendingOperationsUnavailable do FPendingMiningConfiguration := True; + end; + end else FPendingMiningConfiguration := True; + FPoolMiningServer.Active := TSettings.JsonRpcMinerServerActive and + (FPoolMiningServer.MinerAccountKey.EC_OpenSSL_NID <> 0); FPoolMiningServer.OnMiningServerNewBlockFound := OnMiningServerNewBlockFound; FreeAndNil(FBackgroundLabel); FreeAndNil(FBackgroundPanel); @@ -892,6 +915,10 @@ procedure TFRMWallet.FinishedLoadingApp; UpdatePrivateKeys; // LFoundAccounts := 0; + if not FNode.PendingOperationsAvailable then begin + FWaitingForMempool := True; + Exit; + end; FNode.Bank.SafeBox.StartThreadSafe; try Lpubkeys := TList.Create; @@ -1334,7 +1361,18 @@ procedure TFRMWallet.FormCreate(Sender: TObject); for i := 0 to StatusBar.Panels.Count - 1 do begin StatusBar.Panels[i].Text := ''; end; + // GNUgettext's proxy form duplicates managed-record finalization. Follow + // Classic's explicit ownership and free these lists after its logger stops. + FLogBatchLines := TStringList.Create; + try + FLogEntryLines := TStringList.Create; + except + FreeAndNil(FLogBatchLines); + raise; + end; FLog := TLog.Create(Self); + FLog.OnNewLogBatchBegin := OnLogBatchBegin; + FLog.OnNewLogBatchEnd := OnLogBatchEnd; FLog.OnNewLog := OnNewLog; FLog.SaveTypes := []; If Not ForceDirectories(TNode.GetPascalCoinDataFolder) then raise Exception.Create('Cannot create dir: '+TNode.GetPascalCoinDataFolder); @@ -1342,14 +1380,18 @@ procedure TFRMWallet.FormCreate(Sender: TObject); TSettings.OnChanged.Add(UpdateConfigChanged); FNodeNotifyEvents := TNodeNotifyEvents.Create(Self); FNodeNotifyEvents.OnBlocksChanged := OnNewAccount; + FNodeNotifyEvents.OnSynchronizationChanged := OnNodeSynchronizationChanged; FNodeNotifyEvents.OnNodeMessageEvent := OnNodeMessageEvent; FNodeNotifyEvents.OnKeyActivity := OnNodeKeysActivity; FAccountsGrid := TAccountsGrid.Create(Self); + // Classic key selection remains usable while pending adjustments are deferred. + FAccountsGrid.UseConfirmedAccountsWhenPendingUnavailable := True; FAccountsGrid.DrawGrid := dgAccounts; FAccountsGrid.AllowMultiSelect := True; FAccountsGrid.OnAccountsGridUpdatedData := OnAccountsGridUpdatedData; FAccountsGrid.AccountsGridDatasource := acds_Node; FSelectedAccountsGrid := TAccountsGrid.Create(Self); + FSelectedAccountsGrid.UseConfirmedAccountsWhenPendingUnavailable := True; FSelectedAccountsGrid.AccountsGridDatasource := acds_InternalList; FSelectedAccountsGrid.DrawGrid := dgSelectedAccounts; FSelectedAccountsGrid.OnUpdated := OnSelectedAccountsGridUpdated; @@ -1486,6 +1528,9 @@ procedure TFRMWallet.FormDestroy(Sender: TObject); // step := 'Assigning nil events'; FLog.OnNewLog :=Nil; + FLog.OnNewLogBatchBegin := Nil; + FLog.OnNewLogBatchEnd := Nil; + FNodeNotifyEvents.OnSynchronizationChanged := Nil; FNodeNotifyEvents.Node := Nil; FOperationsAccountGrid.Node := Nil; FOperationsExplorerGrid.Node := Nil; @@ -1535,6 +1580,8 @@ procedure TFRMWallet.FormDestroy(Sender: TObject); End; TLog.NewLog(ltinfo,Classname,'Destroying form - END'); FreeAndNil(FLog); + FreeAndNil(FLogEntryLines); + FreeAndNil(FLogBatchLines); Sleep(100); end; @@ -1575,52 +1622,15 @@ procedure TFRMWallet.sbSearchAccountClick(Sender: TObject); end; end; -function TFRMWallet.GetAccountKeyForMiner: TAccountKey; -Var PK : TECPrivateKey; - i : Integer; - PublicK : TECDSA_Public; +function TFRMWallet.GetAccountKeyForMiner(AAfterMinedBlock: Boolean): TAccountKey; begin Result := CT_TECDSA_Public_Nul; if Not Assigned(FWalletKeys) then exit; - case FMinerPrivateKeyType of - mpk_NewEachTime: PublicK := CT_TECDSA_Public_Nul; - mpk_Selected: begin - PublicK := TAccountComp.RawString2Accountkey(TSettings.MinerSelectedPublicKey); - end; - else - // Random - PublicK := CT_TECDSA_Public_Nul; - if FWalletKeys.Count>0 then begin - i := Random(FWalletKeys.Count); - if (Length(FWalletKeys.Key[i].CryptedKey)=0) then begin - // Not valid, search for first valid: - i:=0; - while (i=0 then begin - if (Length(FWalletKeys.Key[i].CryptedKey)=0) then i:=-1; - end; - if i<0 then begin - PK := TECPrivateKey.Create; - try - PK.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); - FWalletKeys.AddPrivateKey('New for miner '+DateTimeToStr(Now), PK); - PublicK := PK.PublicKey; - // Set to AppParams if not mpk_NewEachTime - if (FMinerPrivateKeyType<>mpk_NewEachTime) then begin - TSettings.MinerSelectedPublicKey := TAccountComp.AccountKey2RawString(PublicK); - FMinerPrivateKeyType:=mpk_Selected; - TSettings.MinerPrivateKeyType := mpk_Selected; - end; - finally - PK.Free; - end; - end; - Result := PublicK; end; procedure TFRMWallet.IPnodes1Click(Sender: TObject); @@ -2078,7 +2088,18 @@ procedure TFRMWallet.OnAccountsGridUpdatedData(Sender: TObject); procedure TFRMWallet.OnMiningServerNewBlockFound(Sender: TObject); begin - FPoolMiningServer.MinerAccountKey := GetAccountKeyForMiner; + // Pool callbacks originate on a network thread; wallet keys belong to the UI. + PostMessage(Self.Handle, CM_PC_MinerBlockFound, 0, 0); +end; + +procedure TFRMWallet.CM_MinerBlockFound(var Msg: TMessage); +var LKey: TAccountKey; +begin + if (csDestroying in ComponentState) or (not Assigned(FPoolMiningServer)) or + (not TSettings.JsonRpcMinerServerActive) then Exit; + LKey := GetAccountKeyForMiner(True); + if LKey.EC_OpenSSL_NID <> 0 then + FPoolMiningServer.MinerAccountKey := LKey; end; procedure TFRMWallet.OnNetBlackListUpdated(Sender: TObject); @@ -2198,9 +2219,21 @@ procedure TFRMWallet.OnNetStatisticsChanged(Sender: TObject); procedure TFRMWallet.OnNewAccount(Sender: TObject); begin Try - UpdateAccounts(false); + if Assigned(FNode) and not FNode.PendingOperationsAvailable then begin + FWaitingForMempool := True; + UpdateBlockChainState; + Exit; + end; + if FPendingMiningConfiguration then begin + FPendingMiningConfiguration := False; + UpdateConfigChanged(Self); + end; + UpdateAccounts(FWaitingForMempool); + FWaitingForMempool := False; + UpdateOperations; UpdateBlockChainState; Except + On E:EPendingOperationsUnavailable do FWaitingForMempool := True; On E:Exception do begin E.Message := 'Exception at OnNewAccount '+E.ClassName+': '+E.Message; TLog.NewLog(lterror,ClassName,E.Message); @@ -2209,24 +2242,114 @@ procedure TFRMWallet.OnNewAccount(Sender: TObject); end; end; +procedure TFRMWallet.OnLogBatchBegin(Sender: TObject); +var LText: String; +begin + // BeginUpdate suppresses painting, but native memo Add/Delete still shape + // every edit and can stall the UI. Build the batch away from the control. + FLogBatchChanged := False; + LText := memoLogs.Lines.Text; + FLogBatchTerminated := (LText='') or (Copy(LText,Length(LText)-Length(sLineBreak)+1,Length(sLineBreak))=sLineBreak); + SplitLogLines(LText,FLogBatchLines); + FLogBatchActive := True; +end; + +procedure TFRMWallet.OnLogBatchEnd(Sender: TObject); +var LText: String; +begin + FLogBatchActive := False; + try + if FLogBatchChanged then begin + // Publish once. Lines.Text would normalize bare CR/LF that native Add + // preserves, so use the raw widget text and retain the append caret. + LText := FLogBatchLines.Text; + if not FLogBatchTerminated then SetLength(LText,Length(LText)-Length(sLineBreak)); + {$IFDEF FPC} + memoLogs.HandleNeeded; + TWSWinControlClass(memoLogs.WidgetSetClass).SetText(memoLogs,LText); + memoLogs.Perform(CM_TEXTCHANGED,0,0); + {$ELSE} + memoLogs.Text := LText; + {$ENDIF} + memoLogs.SelStart := memoLogs.GetTextLen; + {$IFNDEF FPC} + // VCL EM_SETSEL moves the caret without scrolling; the replaced native + // Add did scroll, so keep following the newest line explicitly. + memoLogs.Perform(EM_SCROLLCARET,0,0); + {$ENDIF} + end; + finally + FLogBatchChanged := False; + FLogBatchLines.Clear; + FLogEntryLines.Clear; + end; +end; + procedure TFRMWallet.OnNewLog(logtype: TLogType; Time : TDateTime; ThreadID : TThreadID; const sender,logtext: String); Var s : AnsiString; + LText: String; + LLines: TStrings; begin if (logtype=ltdebug) And (Not cbShowDebugLogs.Checked) then exit; if ThreadID=MainThreadID then s := ' MAIN:' else s:=' TID:'; - if MemoLogs.Lines.Count>300 then begin + if FLogBatchActive then begin + LLines := FLogBatchLines; + FLogBatchChanged := True; + end else LLines := memoLogs.Lines; + if LLines.Count>300 then begin // Limit max lines in logs... - memoLogs.Lines.BeginUpdate; + LLines.BeginUpdate; try - while memoLogs.Lines.Count>250 do memoLogs.Lines.Delete(0); + while LLines.Count>250 do LLines.Delete(0); finally - memoLogs.Lines.EndUpdate; + LLines.EndUpdate; end; end; - memoLogs.Lines.Add(formatDateTime('dd/mm/yyyy hh:nn:ss.zzz',Time)+s+IntToHex(PtrInt(ThreadID),8)+' ['+CT_LogType[Logtype]+'] <'+sender+'> '+logtext); + LText := formatDateTime('dd/mm/yyyy hh:nn:ss.zzz',Time)+s+IntToHex(PtrInt(ThreadID),8)+' ['+CT_LogType[Logtype]+'] <'+sender+'> '+logtext; + if FLogBatchActive then begin + // Match the native physical-line count before applying retention again. + {$IFDEF FPC} + LText := LText+sLineBreak; + {$ELSE} + // VCL preserves an unterminated user-edited tail when appending; LCL + // always appends a separator. Keep each original native rule unchanged. + if FLogBatchTerminated then LText := LText+sLineBreak; + {$ENDIF} + FLogBatchTerminated := Copy(LText,Length(LText)-Length(sLineBreak)+1,Length(sLineBreak))=sLineBreak; + SplitLogLines(LText,FLogEntryLines); + FLogBatchLines.AddStrings(FLogEntryLines); + end else LLines.Add(LText); // end; +procedure TFRMWallet.SplitLogLines(const AText: String; ALines: TStrings); +var I, LStart, LLength, LBreakLength: Integer; + LBreak: String; + LMatches: Boolean; +begin + // Only full native separators count: Windows keeps a bare CR or LF as + // part of a line, and the retained text must preserve those log bytes. + ALines.Clear; + LBreak := sLineBreak; + LBreakLength := Length(LBreak); + LLength := Length(AText); + LStart := 1; + I := 1; + while I<=LLength do begin + LMatches := AText[I]=LBreak[1]; + if LMatches and (LBreakLength>1) then begin + if I1000 then begin @@ -2435,22 +2569,21 @@ procedure TFRMWallet.UpdateAccounts(RefreshData : Boolean); LFilters := FAccountsGrid.AccountsGridFilter; LFilters.MinBalance := FMinAccountBalance; LFilters.MaxBalance := FMaxAccountBalance; + LFilters.UseAccountKey := False; + LFilters.AccountKey := CT_TECDSA_Public_Nul; + LFilters.indexAccountsKeyList := -1; if cbExploreMyAccounts.Checked then begin - //FNode.Bank.SafeBox.StartThreadSafe; - try - LFilters.OrderedAccountsKeyList := FWalletKeys.AccountsKeyList; - if cbMyPrivateKeys.ItemIndex>0 then begin - i := PtrInt(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]); - if (i>=0) And (i0 then begin + // Capture the key itself: background index changes must not turn this + // selection into another key, and index lookup must not block the UI. + LFilters.UseAccountKey := True; + i := PtrInt(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]); + if (i>=0) And (i0 then begin - lblCurrentBlock.Caption := Inttostr(FNode.Bank.BlocksCount)+' (0..'+Inttostr(FNode.Bank.BlocksCount-1)+')'; ; + // The typed record is safe during background commits and final replay. + // Status painting must not acquire the unavailable pending overlay. + LSynchronization := FNode.GetSynchronizationStatus; + if LSynchronization.LocalBlockCount>0 then begin + lblCurrentBlock.Caption := IntToStr(LSynchronization.LocalBlockCount)+' (0..'+IntToStr(LSynchronization.LocalBlockCount-1)+')'; end else lblCurrentBlock.Caption := '(none)'; - lblCurrentAccounts.Caption := Inttostr(FNode.Bank.AccountsCount); - lblCurrentBlockTime.Caption := UnixTimeToLocalElapsedTime(FNode.Bank.LastOperationBlock.timestamp); - LLockedMempool := FNode.LockMempoolRead; - try - lblOperationsPending.Caption := Inttostr(LLockedMempool.Count); - lblCurrentDifficulty.Caption := InttoHex(LLockedMempool.OperationBlock.compact_target,8); - finally - FNode.UnlockMempoolRead; - end; + lblCurrentAccounts.Caption := IntToStr(UInt64(LSynchronization.LocalBlockCount)*CT_AccountsPerBlock); + lblCurrentBlockTime.Caption := UnixTimeToLocalElapsedTime(LSynchronization.LastBlockTimestamp); + lblOperationsPending.Caption := IntToStr(LSynchronization.PendingOperationsCount); + lblCurrentDifficulty.Caption := IntToHex(LSynchronization.CompactTarget,8); + if LSynchronization.PendingOperationsAvailable then begin favg := FNode.Bank.GetActualTargetSecondsAverage(CT_CalcNewTargetBlocksAverage); f := (CT_NewLineSecondsAvg - favg) / CT_NewLineSecondsAvg; lblTimeAverage.Caption := 'Last '+Inttostr(CT_CalcNewTargetBlocksAverage)+': '+FormatFloat('0.0',favg)+' sec. (Optimal '+Inttostr(CT_NewLineSecondsAvg)+'s) Deviation '+FormatFloat('0.00%',f*100); @@ -2529,6 +2661,7 @@ procedure TFRMWallet.UpdateBlockChainState; ((CT_CalcNewTargetBlocksAverage DIV 4)*3),FormatFloat('0.0',FNode.Bank.GetActualTargetSecondsAverage(((CT_CalcNewTargetBlocksAverage DIV 4)*3))), CT_CalcNewTargetBlocksAverage DIV 2,FormatFloat('0.0',FNode.Bank.GetActualTargetSecondsAverage(CT_CalcNewTargetBlocksAverage DIV 2)), CT_CalcNewTargetBlocksAverage DIV 4,FormatFloat('0.0',FNode.Bank.GetActualTargetSecondsAverage(CT_CalcNewTargetBlocksAverage DIV 4))]); + end; end else begin isMining := false; lblCurrentBlock.Caption := ''; @@ -2577,12 +2710,19 @@ procedure TFRMWallet.UpdateConfigChanged(Sender:TObject); wa := FNode.NetServer.Active; FNode.NetServer.Port := TSettings.InternetServerPort; FNode.NetServer.Active := wa; - LLockedMempool := FNode.LockMempoolWrite; - try - LLockedMempool.BlockPayload := TEncoding.ANSI.GetBytes(TSettings.MinerName); - finally - FNode.UnlockMempoolWrite; - end; + if FNode.PendingOperationsAvailable then begin + try + LLockedMempool := FNode.LockMempoolWrite; + try + LLockedMempool.AccountKey := GetAccountKeyForMiner; + LLockedMempool.BlockPayload := TEncoding.ANSI.GetBytes(TSettings.MinerName); + finally + FNode.UnlockMempoolWrite; + end; + except + on E: EPendingOperationsUnavailable do FPendingMiningConfiguration := True; + end; + end else FPendingMiningConfiguration := True; FNode.NodeLogFilename := TNode.GetPascalCoinDataFolder+PathDelim+'blocks.log'; end; if Assigned(FPoolMiningServer) then begin @@ -2590,8 +2730,9 @@ procedure TFRMWallet.UpdateConfigChanged(Sender:TObject); FPoolMiningServer.Active := false; FPoolMiningServer.Port := TSettings.JsonRpcMinerServerPort; end; - FPoolMiningServer.Active := TSettings.JsonRpcMinerServerActive; FPoolMiningServer.UpdateAccountAndPayload(GetAccountKeyForMiner,TEncoding.ANSI.GetBytes(TSettings.MinerName)); + FPoolMiningServer.Active := TSettings.JsonRpcMinerServerActive and + (FPoolMiningServer.MinerAccountKey.EC_OpenSSL_NID <> 0); end; if Assigned(FRPCServer) then begin FRPCServer.Active := TSettings.JsonRpcPortEnabled; @@ -2620,10 +2761,24 @@ procedure TFRMWallet.UpdateConfigChanged(Sender:TObject); procedure TFRMWallet.UpdateConnectionStatus; var errors : String; + LSynchronization: TNodeSynchronizationStatus; begin UpdateNodeStatus; OnNetStatisticsChanged(Nil); if Assigned(FNode) then begin + LSynchronization := FNode.GetSynchronizationStatus; + if LSynchronization.StorageFailure.HasFailure then begin + StatusBar.Panels[2].Text := FormatStorageFailure(LSynchronization.StorageFailure); + Exit; + end; + if not LSynchronization.PendingOperationsAvailable then begin + StatusBar.Panels[2].Text := Format('Blocks: %d | Last account time:%s',[ + LSynchronization.LocalBlockCount,FormatDateTime('dd/mm/yyyy hh:nn:ss', + UnivDateTime2LocalDateTime(UnixToUnivDateTime(LSynchronization.LastBlockTimestamp)))]); + if not LSynchronization.NetworkActive then + StatusBar.Panels[2].Text := StatusBar.Panels[2].Text+' | Network off'; + Exit; + end; if FNode.IsBlockChainValid(errors) then begin StatusBar.Panels[2].Text := Format('Last account time:%s', [FormatDateTime('dd/mm/yyyy hh:nn:ss',UnivDateTime2LocalDateTime(UnixToUnivDateTime( FNode.Bank.LastOperationBlock.timestamp )))]); @@ -2635,20 +2790,39 @@ procedure TFRMWallet.UpdateConnectionStatus; end; end; +procedure TFRMWallet.OnNodeSynchronizationChanged(Sender: TObject; const AStatus: TNodeSynchronizationStatus); +begin + if (csDestroying in ComponentState) or not Assigned(FNode) then Exit; + // Failure visibility is event-driven. Keep classic's ordinary status/update + // behavior unchanged, while reporting storage faults before any timer tick. + if AStatus.StorageFailure.HasFailure then UpdateConnectionStatus; +end; + procedure TFRMWallet.UpdateNodeStatus; -Var status : String; +Var status, LBankDiagnostic : String; + LProgress : TProgressInfo; + LStatus: TNodeSynchronizationStatus; begin + LStatus := Default(TNodeSynchronizationStatus); + if Assigned(FNode) then LStatus := FNode.GetSynchronizationStatus; If Not Assigned(FNode) then begin lblNodeStatus.Font.Color := clRed; lblNodeStatus.Caption := 'Initializing...'; + end else if LStatus.StorageFailure.HasFailure then begin + // A failed local write is restart-only, never synchronization or peer loss. + lblNodeStatus.Font.Color := clRed; + lblNodeStatus.Caption := FormatStorageFailure(LStatus.StorageFailure); + end else if not FNode.PendingOperationsAvailable then begin + lblNodeStatus.Font.Color := clGreen; + lblNodeStatus.Caption := 'Blockchain synchronization is active'; end else begin If FNode.IsReady(status) then begin if TNetData.NetData.NetStatistics.ActiveConnections>0 then begin lblNodeStatus.Font.Color := clGreen; if TNetData.NetData.IsDiscoveringServers then begin lblNodeStatus.Caption := 'Discovering servers'; - end else if TNetData.NetData.IsGettingNewBlockChainFromClient(status) then begin - lblNodeStatus.Caption := 'Obtaining new blockchain '+status; + end else if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then begin + lblNodeStatus.Caption := 'Obtaining new blockchain ' + FormatProgressInfo(LProgress); end else begin lblNodeStatus.Caption := 'Running'; end; @@ -2657,6 +2831,11 @@ procedure TFRMWallet.UpdateNodeStatus; lblNodeStatus.Caption := 'Alone in the world...'; end; end else begin + // Recreate download text here; bank/server failures keep their own reason. + if FNode.Bank.IsReady(LBankDiagnostic) then + if FNode.NetServer.Active then + if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then + status := FormatProgressInfo(LProgress); lblNodeStatus.Font.Color := clRed; lblNodeStatus.Caption := status; end; @@ -2670,6 +2849,10 @@ procedure TFRMWallet.UpdateNodeStatus; procedure TFRMWallet.UpdateOperations; Var accn : Int64; begin + if Assigned(FNode) and not FNode.PendingOperationsAvailable then begin + FWaitingForMempool := True; + Exit; + end; accn := FAccountsGrid.AccountNumber(dgAccounts.Row); FOperationsAccountGrid.AccountNumber := accn; end; diff --git a/src/gui-classic/UGridUtils.pas b/src/gui-classic/UGridUtils.pas index 83324d8a2..b560945ac 100644 --- a/src/gui-classic/UGridUtils.pas +++ b/src/gui-classic/UGridUtils.pas @@ -30,8 +30,8 @@ interface {$ELSE} LCLIntf, LCLType, LMessages, {$ENDIF} - Classes, Grids, UNode, UAccounts, UBlockChain, UAppParams, UThread, UPCDataTypes, - UWallet, UCrypto, UPoolMining, URPC, UBaseTypes, UPCOrderedLists, USettings, + Classes, ExtCtrls, Grids, UNode, UAccounts, UBlockChain, UAppParams, UThread, UPCDataTypes, + UWallet, UCrypto, UPoolMining, URPC, UBaseTypes, UPCOrderedLists, UOrderedList, USettings, {$IFNDEF FPC}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}; Type @@ -51,6 +51,10 @@ interface MaxBalance : Int64; OrderedAccountsKeyList : TOrderedAccountKeysList; indexAccountsKeyList : Integer; + // Opt in to worker-side resolution of a captured key. Existing callers may + // keep using indexAccountsKeyList (-1 means all keys) without a UI change. + UseAccountKey : Boolean; + AccountKey : TAccountKey; end; TAccountsGridUpdateThread = Class(TPCThread) @@ -59,6 +63,7 @@ interface FBalance : Int64; FIsProcessing : Boolean; FProcessedList : TOrderedCardinalList; + FBalanceOnly, FBalanceReady: Boolean; protected procedure SynchronizedOnTerminated; procedure BCExecute; override; @@ -70,6 +75,11 @@ interface TAccountsGridDatasource = (acds_Node, acds_InternalList, acds_NodeFiltered); + TReadAccountEvent = function(Sender: TObject; AAccountNumber: Integer; + out AAccount: TAccount; out ANodeBlocksCount, ANodeAccountsCount: Integer): Boolean of object; + TReadAccountSummaryEvent = function(Sender: TObject; out AAccountsCount: Integer; + out ABalance: Int64): Boolean of object; + TAccountsGrid = Class(TComponent) private FAccountsBalance : Int64; @@ -77,17 +87,25 @@ interface FColumns : TAccountColumnArray; FDrawGrid : TDrawGrid; FNodeNotifyEvents : TNodeNotifyEvents; - FOnUpdated: TNotifyEvent; + FOnUpdated, FOnRefreshStateChanged: TNotifyEvent; FAllowMultiSelect: Boolean; FAccountsGridUpdateThread : TAccountsGridUpdateThread; + FRetiredAccountThreads: TList; + FAccountRefreshTimer: TTimer; + FAccountRefreshPending, FJoiningAccountThreads, FShuttingDown: Boolean; + FWaitingForMempool: Boolean; + FUseConfirmedAccountsWhenPendingUnavailable: Boolean; FAccountsGridFilter: TAccountsGridFilter; FOnAccountsGridUpdatedData: TNotifyEvent; FAccountsGridDatasource: TAccountsGridDatasource; + FOnReadAccount: TReadAccountEvent; + FOnReadSummary: TReadAccountSummaryEvent; // FBufferLastAccountNumber : Integer; FBufferLastAccount : TAccount; FBufferNodeAccountsCount : Integer; FBufferNodeBlocksCount : Integer; + FBufferUsesConfirmedAccounts: Boolean; // procedure SetDrawGrid(const Value: TDrawGrid); Procedure InitGrid; @@ -98,13 +116,16 @@ interface function GetNode: TNode; procedure SetAllowMultiSelect(const Value: Boolean); procedure TerminateAccountGridUpdateThread(AWaitUntilTerminated : Boolean); + procedure PruneRetiredAccountThreads(AWait: Boolean); + procedure PollAccountRefresh(Sender: TObject); + procedure NotifyRefreshStateChanged; procedure SetAccountsGridFilter(const Value: TAccountsGridFilter); function GetAccountsCount: Integer; procedure SetAccountsGridDatasource(const Value: TAccountsGridDatasource); procedure UpdateAccountsBalance; protected procedure Notification(AComponent: TComponent; Operation: TOperation); Override; - procedure BufferGetAccount(AAccountNumber : Integer; var AAccount : TAccount; var ANodeBlocksCount, ANodeAccountsCount : Integer); + function BufferGetAccount(AAccountNumber : Integer; var AAccount : TAccount; var ANodeBlocksCount, ANodeAccountsCount : Integer): Boolean; public Constructor Create(AOwner : TComponent); override; Destructor Destroy; override; @@ -119,6 +140,9 @@ interface Property AccountsCount : Integer read GetAccountsCount; Function MoveRowToAccount(nAccount : Cardinal) : Boolean; Property OnUpdated : TNotifyEvent read FOnUpdated write FOnUpdated; + // Optional busy-to-idle completion, including cancelled/retired readers. + // This is readiness only; existing row-publication callbacks are unchanged. + property OnRefreshStateChanged: TNotifyEvent read FOnRefreshStateChanged write FOnRefreshStateChanged; Property AllowMultiSelect : Boolean read FAllowMultiSelect write SetAllowMultiSelect; Function SelectedAccounts(accounts : TOrderedCardinalList) : Integer; property AccountsGridFilter : TAccountsGridFilter read FAccountsGridFilter write SetAccountsGridFilter; @@ -126,6 +150,15 @@ interface function IsUpdatingData : Boolean; property OnAccountsGridUpdatedData : TNotifyEvent read FOnAccountsGridUpdatedData write FOnAccountsGridUpdatedData; property AccountsGridDatasource : TAccountsGridDatasource read FAccountsGridDatasource write SetAccountsGridDatasource; + // Opt in to confirmed Safebox reads while pending reconciliation is deferred. + // Other consumers keep the existing suspended-query behavior by default. + property UseConfirmedAccountsWhenPendingUnavailable: Boolean + read FUseConfirmedAccountsWhenPendingUnavailable + write FUseConfirmedAccountsWhenPendingUnavailable; + // An optional snapshot reader may return False while data is unavailable. + // Painting then displays only the known account number, never a fake balance. + property OnReadAccount: TReadAccountEvent read FOnReadAccount write FOnReadAccount; + property OnReadSummary: TReadAccountSummaryEvent read FOnReadSummary write FOnReadSummary; function GetColumns : TAccountColumnArray; procedure SetColumns(const AColumns : TAccountColumnArray); End; @@ -134,11 +167,20 @@ interface TOperationsGridUpdateThread = Class(TPCThread) FOperationsGrid : TOperationsGrid; + FNode: TNode; + FWalletKeys: TWalletKeys; + FPasswords: TList; + FAccountNumber, FBlockStart, FBlockEnd: Int64; + FPendingOperations, FMustShowAlwaysAnAccount, FAsynchronous, FSucceeded: Boolean; + FGeneration: Int64; + FResults: TList; + FError: String; procedure DoUpdateOperationsGrid(const ANode : TNode; const AWalleTKeys : TWalletKeys; const APasswords : TList; var AList : TList); protected procedure BCExecute; override; public constructor Create(AOperationsGrid : TOperationsGrid); + destructor Destroy; override; End; TOperationsGrid = Class(TComponent) @@ -154,6 +196,13 @@ interface FOperationsGridUpdateThread : TOperationsGridUpdateThread; FWalletKeys: TWalletKeys; FPasswords: TList; + FAsynchronousUpdates, FShuttingDown, FRefreshPending, FJoiningWorker: Boolean; + FAutoRefreshOnNodeEvents: Boolean; + FRefreshGeneration: Int64; + FRefreshTimer: TTimer; + procedure SetAsynchronousUpdates(AValue: Boolean); + procedure PollRefresh(Sender: TObject); + procedure StopRefresh; Procedure OnNodeNewOperation(Sender : TObject); Procedure OnNodeNewAccount(Sender : TObject); Procedure InitGrid; @@ -187,6 +236,10 @@ interface Property BlockEnd : Int64 read FBlockEnd write SetBlockEnd; Procedure SetBlocks(bstart,bend : Int64); Property OperationsResume : TOperationsResumeList read FOperationsResume; + // Opt in before assigning Node. Node and wallet keys remain caller-owned + // and must outlive this component and its joined query worker. + property AsynchronousUpdates: Boolean read FAsynchronousUpdates write SetAsynchronousUpdates; + property AutoRefreshOnNodeEvents: Boolean read FAutoRefreshOnNodeEvents write FAutoRefreshOnNodeEvents; End; TBlockChainData = Record @@ -279,7 +332,7 @@ interface Const CT_TBlockChainData_NUL : TBlockChainData = (Block:0;Timestamp:0;BlockProtocolVersion:0;BlockProtocolAvailable:0;OperationsCount:-1;Volume:-1;Reward:0;Fee:0;Target:0;HashRateTargetHs:0;HashRateHs:0;HashRateTargetKhs:0;HashRateKhs:0;MinerPayload:Nil;PoW:Nil;SafeBoxHash:Nil; TimeAverage200:0;TimeAverage150:0;TimeAverage100:0;TimeAverage75:0;TimeAverage50:0;TimeAverage25:0;TimeAverage10:0;TimeAverage5:0); - CT_TAccountsGridFilter_NUL : TAccountsGridFilter = (MinBalance:-1;MaxBalance:-1;OrderedAccountsKeyList:Nil;indexAccountsKeyList:-1); + CT_TAccountsGridFilter_NUL : TAccountsGridFilter = (MinBalance:-1;MaxBalance:-1;OrderedAccountsKeyList:Nil;indexAccountsKeyList:-1;UseAccountKey:False;AccountKey:(EC_OpenSSL_NID:0;x:Nil;y:Nil)); implementation @@ -299,13 +352,49 @@ procedure TAccountsGridUpdateThread.BCExecute; LAccount : TAccount; LNode : TNode; LAccountsList : TList; + + function ContinueReading: Boolean; + begin + // This worker reads confirmed Safebox records, never the deferred overlay. + Result := not Terminated and + (FAccountsGrid.UseConfirmedAccountsWhenPendingUnavailable or + LNode.PendingOperationsAvailable); + if not Result then Terminate; + end; begin + if Terminated then Exit; LApplyfilter := ((FAccountsGridFilter.MinBalance>0) Or ((FAccountsGridFilter.MaxBalance>=0) And (FAccountsGridFilter.MaxBalance= LNode.Bank.SafeBox.AccountsCount then Continue; + LAccount := LNode.Bank.SafeBox.Account(c); + if Terminated then Exit; + FBalance := FBalance + LAccount.balance; + end; + FBalanceReady := True; + end else if (Assigned(FAccountsGridFilter.OrderedAccountsKeyList)) then begin + if FAccountsGridFilter.UseAccountKey then begin + // This lookup may wait behind a broad index traversal. It belongs to + // the owned worker, never to the key-selection handler on the UI. + i := FAccountsGridFilter.OrderedAccountsKeyList.IndexOfAccountKey( + FAccountsGridFilter.AccountKey); + if Terminated or (i < 0) then Exit; + FAccountsGridFilter.indexAccountsKeyList := i; + end else if (FAccountsGridFilter.indexAccountsKeyList<0) then i := 0 else i := FAccountsGridFilter.indexAccountsKeyList; while (Not Terminated) and (i=0) do begin + while (j_min>=0) and (not Terminated) do begin LNode.bank.SafeBox.StartThreadSafe; FAccountsGridFilter.OrderedAccountsKeyList.Lock; // Protection v4 Try + if Terminated then Exit; + if FAccountsGridFilter.UseAccountKey then begin + // Keys can move within the index between chunks. Resolve the + // captured value again while this chunk's existing lock is held. + i := FAccountsGridFilter.OrderedAccountsKeyList.IndexOfAccountKey( + FAccountsGridFilter.AccountKey); + if i < 0 then begin + FProcessedList.Clear; + FBalance := 0; + Exit; + end; + FAccountsGridFilter.indexAccountsKeyList := i; + end; LAccountsNumbersList := FAccountsGridFilter.OrderedAccountsKeyList.AccountKeyList[i]; if Assigned(LAccountsNumbersList) then begin @@ -325,7 +427,9 @@ procedure TAccountsGridUpdateThread.BCExecute; Try LAccountsNumbersList.FillList(j_min,50000,LAccountsList); for j := 0 to LAccountsList.Count - 1 do begin + if not ContinueReading then Exit; LAccount := LNode.Bank.SafeBox.Account(LAccountsList[j]); + if Terminated then Exit; if LApplyfilter then begin if (LAccount.balance>=FAccountsGridFilter.MinBalance) And ((FAccountsGridFilter.MaxBalance<0) Or (LAccount.balance<=FAccountsGridFilter.MaxBalance)) then begin @@ -357,6 +461,7 @@ procedure TAccountsGridUpdateThread.BCExecute; end else begin c := 0; while (c=FAccountsGridFilter.MinBalance) And ((FAccountsGridFilter.MaxBalance<0) Or (LAccount.balance<=FAccountsGridFilter.MaxBalance)) then begin FProcessedList.Add(LAccount.account); @@ -366,7 +471,7 @@ procedure TAccountsGridUpdateThread.BCExecute; end; end; Finally - if Not Terminated then begin + if (Not Terminated) and ((not FBalanceOnly) or FBalanceReady) then begin Synchronize(SynchronizedOnTerminated); end; FisProcessing := False; @@ -379,8 +484,13 @@ constructor TAccountsGridUpdateThread.Create(AAccountsGrid: TAccountsGrid; AAcco FreeOnTerminate := False; FAccountsGrid := AAccountsGrid; FAccountsGridFilter := AAccountsGridFilter; + if FAccountsGridFilter.UseAccountKey then + FAccountsGridFilter.AccountKey := AAccountsGridFilter.AccountKey.GetCopy; FisProcessing := True; FProcessedList := TOrderedCardinalList.Create; + FBalanceOnly := (AAccountsGrid.AccountsGridDatasource = acds_InternalList) and + Assigned(AAccountsGrid.FOnReadAccount); + if FBalanceOnly then FProcessedList.CopyFrom(AAccountsGrid.FAccountsList); Suspended := False; end; @@ -392,18 +502,37 @@ destructor TAccountsGridUpdateThread.Destroy; procedure TAccountsGridUpdateThread.SynchronizedOnTerminated; var LacclTemp : TOrderedCardinalList; -begin + LWasUpdating: Boolean; +begin + LWasUpdating := FAccountsGrid.IsUpdatingData; + if Assigned(FAccountsGrid.Node) and + not FAccountsGrid.Node.PendingOperationsAvailable then begin + FAccountsGrid.FWaitingForMempool := True; + if not FAccountsGrid.UseConfirmedAccountsWhenPendingUnavailable then + begin + FIsProcessing := False; + Exit; + end; + end; if Not Terminated then begin FAccountsGrid.FAccountsBalance := FBalance; - LacclTemp := FAccountsGrid.LockAccountsList; - try - LacclTemp.CopyFrom( FProcessedList ); - finally - FAccountsGrid.UnlockAccountsList; + if FBalanceOnly then begin + FisProcessing := False; + if Assigned(FAccountsGrid.FOnUpdated) then + FAccountsGrid.FOnUpdated(FAccountsGrid); + end else begin + LacclTemp := FAccountsGrid.LockAccountsList; + try + LacclTemp.CopyFrom( FProcessedList ); + finally + FAccountsGrid.UnlockAccountsList; + end; end; FisProcessing := False; if Assigned(FAccountsGrid.FOnAccountsGridUpdatedData) then FAccountsGrid.FOnAccountsGridUpdatedData(FAccountsGrid); end; + if LWasUpdating and not FAccountsGrid.IsUpdatingData then + FAccountsGrid.NotifyRefreshStateChanged; end; { TAccountsGrid } @@ -423,19 +552,71 @@ function TAccountsGrid.AccountNumber(GridRow: Integer): Int64; end else Result := -1; end; -procedure TAccountsGrid.BufferGetAccount(AAccountNumber: Integer; - var AAccount: TAccount; var ANodeBlocksCount, ANodeAccountsCount: Integer); +function TAccountsGrid.BufferGetAccount(AAccountNumber: Integer; + var AAccount: TAccount; var ANodeBlocksCount, ANodeAccountsCount: Integer): Boolean; +var + LUseConfirmed: Boolean; + + procedure ReadConfirmedAccount; + begin + // Capture the account and its counters under the existing short Safebox + // lock. No pending-pool access is permitted during deferred reconciliation. + Node.Bank.SafeBox.StartThreadSafe; + try + FBufferNodeAccountsCount := Node.Bank.AccountsCount; + FBufferNodeBlocksCount := Node.Bank.BlocksCount; + if AAccountNumber >= FBufferNodeAccountsCount then + FBufferLastAccount := CT_Account_NUL + else + FBufferLastAccount := Node.Bank.SafeBox.Account(AAccountNumber); + finally + Node.Bank.SafeBox.EndThreadSave; + end; + end; + begin - if FBufferLastAccountNumber<>AAccountNumber then begin - FBufferNodeAccountsCount := Node.Bank.AccountsCount; - if (AAccountNumber>=FBufferNodeAccountsCount) then FBufferLastAccount := CT_Account_NUL - else FBufferLastAccount := Node.GetMempoolAccount(AAccountNumber); - FBufferNodeBlocksCount := Node.Bank.BlocksCount; + if Assigned(FOnReadAccount) then + Exit(FOnReadAccount(Self, AAccountNumber, AAccount, ANodeBlocksCount, ANodeAccountsCount)); + if not Assigned(Node) then + Exit(False); + LUseConfirmed := not Node.PendingOperationsAvailable; + if LUseConfirmed and not FUseConfirmedAccountsWhenPendingUnavailable then + Exit(False); + // Read mode is part of the one-row cache key. A confirmed result must not + // hide a coherent pending-inclusive result when reconciliation finishes. + if (FBufferLastAccountNumber <> AAccountNumber) or + (FBufferUsesConfirmedAccounts <> LUseConfirmed) then + begin + if LUseConfirmed then + ReadConfirmedAccount + else + begin + try + FBufferNodeAccountsCount := Node.Bank.AccountsCount; + if AAccountNumber >= FBufferNodeAccountsCount then + FBufferLastAccount := CT_Account_NUL + else + FBufferLastAccount := Node.GetMempoolAccount(AAccountNumber); + FBufferNodeBlocksCount := Node.Bank.BlocksCount; + except + on E: EPendingOperationsUnavailable do + begin + if not FUseConfirmedAccountsWhenPendingUnavailable then + Exit(False); + // Catch-up may begin after the availability check above. Fall back + // to confirmed data instead of exposing the now-incoherent overlay. + ReadConfirmedAccount; + LUseConfirmed := True; + end; + end; + end; FBufferLastAccountNumber := AAccountNumber; + FBufferUsesConfirmedAccounts := LUseConfirmed; end; AAccount := FBufferLastAccount; ANodeBlocksCount := FBufferNodeBlocksCount; ANodeAccountsCount := FBufferNodeAccountsCount; + Result := True; end; constructor TAccountsGrid.Create(AOwner: TComponent); @@ -444,8 +625,16 @@ constructor TAccountsGrid.Create(AOwner: TComponent); inherited; FAllowMultiSelect := false; FOnUpdated := Nil; + FOnRefreshStateChanged := nil; + FUseConfirmedAccountsWhenPendingUnavailable := False; + FBufferUsesConfirmedAccounts := False; FAccountsBalance := 0; FAccountsList := TOrderedCardinalList.Create; + FRetiredAccountThreads := TList.Create; + FAccountRefreshTimer := TTimer.Create(Self); + FAccountRefreshTimer.Enabled := False; + FAccountRefreshTimer.Interval := 50; + FAccountRefreshTimer.OnTimer := PollAccountRefresh; FDrawGrid := Nil; SetLength(FColumns,7); FColumns[0].ColumnType := act_account_number; @@ -464,6 +653,9 @@ constructor TAccountsGrid.Create(AOwner: TComponent); FColumns[6].width := 20; FNodeNotifyEvents := TNodeNotifyEvents.Create(Self); FNodeNotifyEvents.OnOperationsChanged := OnNodeNewOperation; + // Standalone account dialogs also need the final coherent block event; + // their refresh must not depend on the main wallet being their owner. + FNodeNotifyEvents.OnBlocksChanged := OnNodeNewOperation; FAccountsGridUpdateThread := Nil; FOnAccountsGridUpdatedData := Nil; FAccountsGridFilter := CT_TAccountsGridFilter_NUL; @@ -473,18 +665,33 @@ constructor TAccountsGrid.Create(AOwner: TComponent); destructor TAccountsGrid.Destroy; begin + FShuttingDown := True; + FOnReadAccount := nil; + FOnReadSummary := nil; + FOnUpdated := nil; + FOnRefreshStateChanged := nil; + FOnAccountsGridUpdatedData := nil; + FNodeNotifyEvents.OnOperationsChanged := nil; + FNodeNotifyEvents.OnBlocksChanged := nil; + FNodeNotifyEvents.OnKeyActivity := nil; TerminateAccountGridUpdateThread(True); + FRetiredAccountThreads.Free; FNodeNotifyEvents.Free; FAccountsList.Free; inherited; end; function TAccountsGrid.GetAccountsCount: Integer; +var LBalance: Int64; begin if Not Assigned(Node) then Exit(0); case FAccountsGridDatasource of - acds_Node: Result := Node.Bank.AccountsCount; + acds_Node: begin + if Assigned(FOnReadSummary) then begin + if not FOnReadSummary(Self, Result, LBalance) then Result := 0; + end else Result := Node.Bank.AccountsCount; + end; else Result := FAccountsList.Count; end; @@ -524,13 +731,14 @@ procedure TAccountsGrid.InitGrid; end; procedure TAccountsGrid.InitGridRowCount; -var LRowCount : Integer; +var LRowCount, LAccountsCount : Integer; begin if Not assigned(DrawGrid) then exit; if FAccountsGridDatasource=acds_Node then begin if Assigned(Node) then begin - if Node.Bank.AccountsCount<1 then LRowCount := 2 - else LRowCount := Node.Bank.AccountsCount+1; + LAccountsCount := GetAccountsCount; + if LAccountsCount<1 then LRowCount := 2 + else LRowCount := LAccountsCount+1; end else LRowCount := 2; end else begin if FAccountsList.Count<1 then LRowCount := 2 @@ -543,8 +751,9 @@ procedure TAccountsGrid.InitGridRowCount; function TAccountsGrid.IsUpdatingData: Boolean; begin - if Assigned(FAccountsGridUpdateThread) then Result := FAccountsGridUpdateThread.IsProcessing - else Result := False; + Result := FAccountRefreshPending or (FRetiredAccountThreads.Count > 0); + if Assigned(FAccountsGridUpdateThread) then + Result := Result or FAccountsGridUpdateThread.IsProcessing; end; procedure TAccountsGrid.LoadFromStream(Stream: TStream); @@ -681,7 +890,9 @@ procedure TAccountsGrid.OnGridDrawCell(Sender: TObject; ACol, ARow: Longint; account : TAccount; ndiff : Cardinal; LNodeBlocksCount,LNodeAccountsCount : Integer; + LHasAccount: Boolean; begin + if FShuttingDown or FJoiningAccountThreads then Exit; if Not Assigned(Node) then exit; if (ACol>=0) AND (ACol=0) then begin - BufferGetAccount(n_acc,account,LNodeBlocksCount,LNodeAccountsCount); - ndiff := LNodeBlocksCount - account.GetLastUpdatedBlock; + LHasAccount := BufferGetAccount(n_acc,account,LNodeBlocksCount,LNodeAccountsCount); if (gdSelected in State) then If (gdFocused in State) then DrawGrid.Canvas.Brush.Color := clGradientActiveCaption else DrawGrid.Canvas.Brush.Color := clGradientInactiveCaption else DrawGrid.Canvas.Brush.Color := clWindow; DrawGrid.Canvas.FillRect(Rect); InflateRect(Rect,-2,-1); + if not LHasAccount then begin + if C.ColumnType = act_account_number then begin + s := TAccountComp.AccountNumberToAccountTxtNumber(n_acc); + Canvas_TextRect(DrawGrid.Canvas,Rect,s,State,[tfRight,tfVerticalCenter,tfSingleLine]); + end; + Exit; + end; + ndiff := LNodeBlocksCount - account.GetLastUpdatedBlock; case C.ColumnType of act_account_number : Begin s := TAccountComp.AccountNumberToAccountTxtNumber(n_acc); @@ -793,6 +1011,23 @@ procedure TAccountsGrid.OnGridDrawCell(Sender: TObject; ACol, ARow: Longint; procedure TAccountsGrid.OnNodeNewOperation(Sender: TObject); begin + if FShuttingDown or FJoiningAccountThreads then Exit; + if Assigned(Node) and not Node.PendingOperationsAvailable then + begin + FWaitingForMempool := True; + if not FUseConfirmedAccountsWhenPendingUnavailable then + begin + TerminateAccountGridUpdateThread(False); + Exit; + end; + // Repaint confirmed data during catch-up without cancelling a manual + // key-filter request. Final readiness still refreshes that request once. + end + else if FWaitingForMempool then + begin + FWaitingForMempool := False; + UpdateData; + end; FBufferLastAccountNumber := -1; If Assigned(FDrawGrid) then FDrawGrid.Invalidate; end; @@ -846,6 +1081,8 @@ procedure TAccountsGrid.SetAccountsGridDatasource(const Value: TAccountsGridData procedure TAccountsGrid.SetAccountsGridFilter(const Value: TAccountsGridFilter); begin FAccountsGridFilter := Value; + if FAccountsGridFilter.UseAccountKey then + FAccountsGridFilter.AccountKey := Value.AccountKey.GetCopy; UpdateData; end; @@ -875,6 +1112,10 @@ procedure TAccountsGrid.SetDrawGrid(const Value: TDrawGrid); procedure TAccountsGrid.SetNode(const Value: TNode); begin if GetNode=Value then exit; + // The old worker borrows the old node and key list. A node handoff must join + // every owned request before the caller can release either borrowed owner. + TerminateAccountGridUpdateThread(True); + FBufferLastAccountNumber := -1; FNodeNotifyEvents.Node := Value; UpdateData; end; @@ -882,36 +1123,106 @@ procedure TAccountsGrid.SetNode(const Value: TNode); procedure TAccountsGrid.TerminateAccountGridUpdateThread(AWaitUntilTerminated : Boolean); var LTmp : TAccountsGridUpdateThread; begin - LTmp := FAccountsGridUpdateThread; - FAccountsGridUpdateThread := Nil; - if Assigned(Ltmp) then begin - if Not LTmp.IsProcessing then AWaitUntilTerminated := True; - if Not AWaitUntilTerminated then begin - LTmp.FreeOnTerminate := True; + if AWaitUntilTerminated then begin + FJoiningAccountThreads := True; + FAccountRefreshPending := False; + FAccountRefreshTimer.Enabled := False; + end; + try + LTmp := FAccountsGridUpdateThread; + FAccountsGridUpdateThread := Nil; + if Assigned(LTmp) then begin + LTmp.Terminate; + // Cancellation does not transfer ownership to the worker. It still + // borrows this grid, its node and the requested key index until joined. + FRetiredAccountThreads.Add(LTmp); + end; + PruneRetiredAccountThreads(AWaitUntilTerminated); + finally + if AWaitUntilTerminated then FJoiningAccountThreads := False; + end; +end; + +procedure TAccountsGrid.PruneRetiredAccountThreads(AWait: Boolean); +var I: Integer; LWorker: TAccountsGridUpdateThread; +begin + for I := FRetiredAccountThreads.Count - 1 downto 0 do begin + LWorker := FRetiredAccountThreads[I]; + if AWait then LWorker.Terminate; + if AWait or LWorker.Finished then begin + LWorker.WaitFor; + FRetiredAccountThreads.Delete(I); + LWorker.Free; end; - LTmp.Terminate; - if AWaitUntilTerminated then begin - LTmp.WaitFor; - FreeAndNil(LTmp); + end; +end; + +procedure TAccountsGrid.PollAccountRefresh(Sender: TObject); +var LWasUpdating: Boolean; +begin + if FShuttingDown or FJoiningAccountThreads then Exit; + LWasUpdating := IsUpdatingData; + PruneRetiredAccountThreads(False); + if Assigned(Node) and not Node.PendingOperationsAvailable then + begin + FWaitingForMempool := True; + if not FUseConfirmedAccountsWhenPendingUnavailable then + begin + FAccountRefreshTimer.Enabled := FRetiredAccountThreads.Count > 0; + Exit; end; end; + // At most one query is active or retiring. Repeated filter changes retain + // only their newest request while cancellation releases the old query. + if (FRetiredAccountThreads.Count = 0) and FAccountRefreshPending then begin + FAccountRefreshPending := False; + if Assigned(Node) and ((FAccountsGridDatasource = acds_NodeFiltered) or + ((FAccountsGridDatasource = acds_InternalList) and Assigned(FOnReadAccount) and + (FAccountsList.Count > 0))) then + FAccountsGridUpdateThread := TAccountsGridUpdateThread.Create(Self, AccountsGridFilter); + end; + FAccountRefreshTimer.Enabled := FAccountRefreshPending or (FRetiredAccountThreads.Count > 0); + // A cancelled filter can retire without publishing rows. Signal its final + // idle transition after committing timer state, so consumers need no poll. + if LWasUpdating and not IsUpdatingData then NotifyRefreshStateChanged; +end; + +procedure TAccountsGrid.NotifyRefreshStateChanged; +begin + if FShuttingDown or FJoiningAccountThreads then Exit; + if Assigned(FOnRefreshStateChanged) then FOnRefreshStateChanged(Self); end; procedure TAccountsGrid.UnlockAccountsList; begin - UpdateAccountsBalance; - InitGridRowCount; + if (FAccountsGridDatasource = acds_InternalList) and Assigned(FOnReadAccount) then + UpdateData + else begin + UpdateAccountsBalance; + InitGridRowCount; + end; if Assigned(FOnUpdated) then FOnUpdated(Self); end; procedure TAccountsGrid.UpdateAccountsBalance; -var i : Integer; +var i, LAccountsCount : Integer; LAcc : TAccount; + LBalance: Int64; begin if Assigned(Node) then begin + if not Node.PendingOperationsAvailable and + not FUseConfirmedAccountsWhenPendingUnavailable then + Exit; case FAccountsGridDatasource of - acds_Node: FAccountsBalance := Node.Bank.SafeBox.TotalBalance; + acds_Node: begin + if Assigned(FOnReadSummary) then begin + if FOnReadSummary(Self, LAccountsCount, LBalance) then FAccountsBalance := LBalance; + end else FAccountsBalance := Node.Bank.SafeBox.TotalBalance; + end; acds_InternalList: begin + // Snapshot-backed grids keep their last completed total until their + // owned worker publishes a complete replacement. No core reads on UI. + if Assigned(FOnReadAccount) then Exit; FAccountsBalance := 0; for i := 0 to FAccountsList.Count - 1 do begin LAcc := Node.Bank.SafeBox.Account( FAccountsList.Get(i) ); @@ -924,16 +1235,32 @@ procedure TAccountsGrid.UpdateAccountsBalance; procedure TAccountsGrid.UpdateData; begin + if FShuttingDown or FJoiningAccountThreads then + Exit; + // An explicit refresh or changed filter must invalidate even the same + // account number; its query context may have changed without a new block. + FBufferLastAccountNumber := -1; + if Assigned(Node) and not Node.PendingOperationsAvailable then + begin + FWaitingForMempool := True; + if not FUseConfirmedAccountsWhenPendingUnavailable then + begin + TerminateAccountGridUpdateThread(False); + Exit; + end; + end; UpdateAccountsBalance; TerminateAccountGridUpdateThread(False); - if Assigned(Node) then begin - case FAccountsGridDatasource of - acds_NodeFiltered: begin - FAccountsBalance := 0; - FAccountsGridUpdateThread := TAccountsGridUpdateThread.Create(Self,AccountsGridFilter); - end; - end; + FAccountRefreshPending := Assigned(Node) and + ((FAccountsGridDatasource = acds_NodeFiltered) or + ((FAccountsGridDatasource = acds_InternalList) and Assigned(FOnReadAccount) and + (FAccountsList.Count > 0))); + if FAccountRefreshPending and (FAccountsGridDatasource = acds_NodeFiltered) then begin + FAccountsBalance := 0; end; + if (FAccountsGridDatasource = acds_InternalList) and Assigned(FOnReadAccount) and + (FAccountsList.Count = 0) then FAccountsBalance := 0; + PollAccountRefresh(Self); InitGridRowCount; if Assigned(FOnAccountsGridUpdatedData) then FOnAccountsGridUpdatedData(Self); end; @@ -941,30 +1268,64 @@ procedure TAccountsGrid.UpdateData; { TOperationsGridUpdateThread } procedure TOperationsGridUpdateThread.BCExecute; -var list : TList; - i : Integer; +var i: Integer; begin - list := TList.Create; try - DoUpdateOperationsGrid(FOperationsGrid.Node,FOperationsGrid.WalletKeys,FOperationsGrid.Passwords,list); - if (Not Terminated) then begin + DoUpdateOperationsGrid(FNode, FWalletKeys, FPasswords, FResults); + if Terminated then Exit; + FSucceeded := True; + if not FAsynchronous then begin FOperationsGrid.FOperationsResume.Clear; - for i := 0 to list.Count-1 do begin - FOperationsGrid.FOperationsResume.Add(list[i]); + for i := 0 to FResults.Count - 1 do begin + if Terminated then Exit; + FOperationsGrid.FOperationsResume.Add(FResults[i]); end; Queue(FOperationsGrid.InitGrid); end; - finally - list.Free; + except + on E: EPendingOperationsUnavailable do begin + // Catch-up began after this request was captured. Retain the old rows; + // the node's final coherent notification schedules a fresh request. + end; + on E: Exception do begin + if Terminated and (E is EAbort) then Exit; + FError := E.Message; + if not FAsynchronous then raise; + end; end; end; constructor TOperationsGridUpdateThread.Create(AOperationsGrid: TOperationsGrid); +var i: Integer; begin - FOperationsGrid := AOperationsGrid; - inherited Create(True); + // Modern Delphi starts an unsuspended thread after the complete constructor + // returns. Starting it here would make AfterConstruction resume it twice. + inherited Create({$IFDEF FPC}True{$ELSE}False{$ENDIF}); FreeOnTerminate := False; - Suspended := False; + FOperationsGrid := AOperationsGrid; + FNode := AOperationsGrid.Node; + FWalletKeys := AOperationsGrid.WalletKeys; + FAccountNumber := AOperationsGrid.FAccountNumber; + FBlockStart := AOperationsGrid.FBlockStart; + FBlockEnd := AOperationsGrid.FBlockEnd; + FPendingOperations := AOperationsGrid.FPendingOperations; + FMustShowAlwaysAnAccount := AOperationsGrid.FMustShowAlwaysAnAccount; + FAsynchronous := AOperationsGrid.FAsynchronousUpdates; + FGeneration := AOperationsGrid.FRefreshGeneration; + FResults := TList.Create; + FPasswords := TList.Create; + for i := 0 to AOperationsGrid.Passwords.Count - 1 do + FPasswords.Add(AOperationsGrid.Passwords[i]); + {$IFDEF FPC}Start;{$ENDIF} +end; + +destructor TOperationsGridUpdateThread.Destroy; +begin + // The inherited thread destructor joins before releasing request snapshots. + inherited; + TThread.RemoveQueuedEvents(Self); + FPasswords.Free; + FResults.Free; end; procedure TOperationsGridUpdateThread.DoUpdateOperationsGrid(const ANode : TNode; const AWalleTKeys : TWalletKeys; @@ -978,16 +1339,55 @@ procedure TOperationsGridUpdateThread.DoUpdateOperationsGrid(const ANode : TNode LOperationsResume : TOperationsResumeList; LLockedMempool : TPCOperationsComp; LEPasa : TEPasa; + LAccounts: TOrderedList; + + function LockMempool: Boolean; + begin + Result := False; + if not ANode.PendingOperationsAvailable then begin + Terminate; + Exit; + end; + if FAsynchronous then begin + // The async reader must not serialize other readers through the node's write gate. + while not ANode.TryLockMempoolRead(0, LLockedMempool) do begin + if Terminated then Exit; + if not ANode.PendingOperationsAvailable then begin + Terminate; + Exit; + end; + Sleep(10); + end; + if Terminated then begin + ANode.UnlockMempoolRead; + Exit; + end; + end else LLockedMempool := ANode.LockMempoolRead; + Result := True; + end; + + procedure AddPendingOperation(AOperationIndex, AResumeIndex: Integer); + begin + Op := LLockedMempool.OperationsHashTree.GetOperation(AOperationIndex); + if TPCOperation.OperationToOperationResume(0, Op, False, FAccountNumber, OPR) then begin + OPR.NOpInsideBlock := AResumeIndex; + OPR.Block := LLockedMempool.OperationBlock.block; + OPR.Balance := LLockedMempool.SafeBoxTransaction.Account(FAccountNumber).balance; + AList.Add(OPR); + end; + end; + begin - if Not Assigned(ANode) then exit; + if (Not Assigned(ANode)) or Terminated then exit; AList.Clear; Try - if (FOperationsGrid.MustShowAlwaysAnAccount) And (FOperationsGrid.AccountNumber<0) then exit; + if (FMustShowAlwaysAnAccount) And (FAccountNumber<0) then exit; - if FOperationsGrid.FPendingOperations then begin - LLockedMempool := ANode.LockMempoolRead; + if FPendingOperations then begin + if not LockMempool then Exit; try for i := LLockedMempool.Count - 1 downto 0 do begin + if Terminated then Exit; Op := LLockedMempool.OperationsHashTree.GetOperation(i); If TPCOperation.OperationToOperationResume(0,Op,True,Op.SignerAccount,OPR) then begin OPR.NOpInsideBlock := i; @@ -1000,18 +1400,18 @@ procedure TOperationsGridUpdateThread.DoUpdateOperationsGrid(const ANode : TNode ANode.UnlockMempoolRead; end; end else begin - if FOperationsGrid.AccountNumber<0 then begin + if FAccountNumber<0 then begin opc := TPCOperationsComp.Create(Nil); try opc.bank := ANode.Bank; - If FOperationsGrid.FBlockEnd<0 then begin + If FBlockEnd<0 then begin If ANode.Bank.BlocksCount>0 then bend := ANode.Bank.BlocksCount-1 else bend := 0; - end else bend := FOperationsGrid.FBlockEnd; - if FOperationsGrid.FBlockStart<0 then begin + end else bend := FBlockEnd; + if FBlockStart<0 then begin if (bend > 300) then bstart := bend - 300 else bstart := 0; - end else bstart:= FOperationsGrid.FBlockStart; + end else bstart:= FBlockStart; If bstart<0 then bstart := 0; if bend>=ANode.Bank.BlocksCount then bend:=ANode.Bank.BlocksCount; while (bstart<=bend) and (Not Terminated) do begin @@ -1030,6 +1430,7 @@ procedure TOperationsGridUpdateThread.DoUpdateOperationsGrid(const ANode : TNode AList.Add(OPR); // Reverse operations inside a block for i := opc.Count - 1 downto 0 do begin + if Terminated then Exit; if TPCOperation.OperationToOperationResume(bend,opc.Operation[i],True,opc.Operation[i].SignerAccount,opr) then begin opr.NOpInsideBlock := i; opr.Block := bend; @@ -1047,16 +1448,28 @@ procedure TOperationsGridUpdateThread.DoUpdateOperationsGrid(const ANode : TNode end else begin list := TList.Create; Try - LLockedMempool := ANode.LockMempoolRead; + if not LockMempool then Exit; try - LLockedMempool.OperationsHashTree.GetOperationsAffectingAccount(FOperationsGrid.AccountNumber,list); - for i := list.Count - 1 downto 0 do begin - Op := LLockedMempool.OperationsHashTree.GetOperation((list[i])); - If TPCOperation.OperationToOperationResume(0,Op,False,FOperationsGrid.AccountNumber,OPR) then begin - OPR.NOpInsideBlock := i; - OPR.Block := LLockedMempool.OperationBlock.block; - OPR.Balance := LLockedMempool.SafeBoxTransaction.Account(FOperationsGrid.AccountNumber).balance; - AList.Add(OPR); + if FAsynchronous then begin + // The core helper scans the whole pending tree without a cancel + // callback. Use the same affected-account contract, checking each + // operation so a superseded request releases core locks promptly. + LAccounts := TOrderedList.Create(False, TComparison_Cardinal); + try + for i := LLockedMempool.Count - 1 downto 0 do begin + if Terminated then Exit; + LAccounts.Clear; + LLockedMempool.OperationsHashTree.GetOperation(i).AffectedAccounts(LAccounts); + if LAccounts.IndexOf(FAccountNumber) >= 0 then AddPendingOperation(i, i); + end; + finally + LAccounts.Free; + end; + end else begin + LLockedMempool.OperationsHashTree.GetOperationsAffectingAccount(FAccountNumber,list); + for i := list.Count - 1 downto 0 do begin + if Terminated then Exit; + AddPendingOperation(list[i], i); end; end; finally @@ -1065,12 +1478,17 @@ procedure TOperationsGridUpdateThread.DoUpdateOperationsGrid(const ANode : TNode Finally list.Free; End; - ANode.Bank.Storage.GetAccountOperations(FOperationsGrid.AccountNumber,-1,0,5000,0,AList); + if Terminated then Exit; + ANode.Bank.Storage.GetAccountOperations(FAccountNumber,-1,0,5000,0,AList); end; end; Finally for i := 0 to AList.Count-1 do begin + if Terminated then Break; OPR := AList[i]; + // Rewards and other records without a destination use -1. They have no + // recipient payload to decode, and must not narrow that sentinel to Cardinal. + if (OPR.DestAccount < 0) or (OPR.DestAccount > High(Cardinal)) then Continue; if TEPasaDecoder.TryDecodeEPASA(OPR.DestAccount,OPR.OriginalPayload,ANode,AWalleTKeys,APasswords,LEPasa) then begin OPR.DecodedEPasaPayload := LEPasa.ToString(True); AList[i] := OPR; @@ -1083,6 +1501,8 @@ procedure TOperationsGridUpdateThread.DoUpdateOperationsGrid(const ANode : TNode constructor TOperationsGrid.Create(AOwner: TComponent); begin + inherited; + FAutoRefreshOnNodeEvents := True; FPasswords := TList.Create; FWalletKeys := Nil; FAccountNumber := 0; @@ -1096,16 +1516,20 @@ constructor TOperationsGrid.Create(AOwner: TComponent); FBlockEnd := -1; FPendingOperations := false; FOperationsGridUpdateThread := Nil; - inherited; + FRefreshTimer := TTimer.Create(Self); + FRefreshTimer.Enabled := False; + FRefreshTimer.Interval := 50; + FRefreshTimer.OnTimer := PollRefresh; end; destructor TOperationsGrid.Destroy; begin - If Assigned(FOperationsGridUpdateThread) then begin - FOperationsGridUpdateThread.Terminate; - FOperationsGridUpdateThread.WaitFor; - FreeAndNil(FOperationsGridUpdateThread); - end; + FShuttingDown := True; + FNodeNotifyEvents.OnOperationsChanged := nil; + FNodeNotifyEvents.OnBlocksChanged := nil; + FNodeNotifyEvents.OnKeyActivity := nil; + StopRefresh; + if Assigned(FDrawGrid) then FDrawGrid.OnDrawCell := nil; FOperationsResume.Free; FNodeNotifyEvents.Free; FPasswords.Free; @@ -1119,6 +1543,7 @@ function TOperationsGrid.GetNode: TNode; procedure TOperationsGrid.InitGrid; begin + if FShuttingDown then Exit; if Not Assigned(FDrawGrid) then exit; if FOperationsResume.Count>0 then FDrawGrid.RowCount := FOperationsResume.Count+1 else FDrawGrid.RowCount := 2; @@ -1261,6 +1686,15 @@ procedure TOperationsGrid.OnGridDrawCell(Sender: TObject; ACol, ARow: Integer; R procedure TOperationsGrid.OnNodeNewAccount(Sender: TObject); begin + if FShuttingDown or FJoiningWorker or (not FAutoRefreshOnNodeEvents) then Exit; + if Assigned(Node) and not Node.PendingOperationsAvailable then begin + UpdateAccountOperations; + Exit; + end; + if FAsynchronousUpdates then begin + UpdateAccountOperations; + Exit; + end; If (AccountNumber<0) And (FBlockEnd<0) And (Not FPendingOperations) then UpdateAccountOperations; end; @@ -1268,6 +1702,15 @@ procedure TOperationsGrid.OnNodeNewOperation(Sender: TObject); Var l : TList; LLockedMempool : TPCOperationsComp; begin + if FShuttingDown or FJoiningWorker or (not FAutoRefreshOnNodeEvents) then Exit; + if Assigned(Node) and not Node.PendingOperationsAvailable then begin + UpdateAccountOperations; + Exit; + end; + if FAsynchronousUpdates then begin + UpdateAccountOperations; + Exit; + end; Try if (AccountNumber<0) then begin If (FPendingOperations) then UpdateAccountOperations; @@ -1287,6 +1730,7 @@ procedure TOperationsGrid.OnNodeNewOperation(Sender: TObject); End; end; Except + On E:EPendingOperationsUnavailable do UpdateAccountOperations; On E:Exception do begin E.message := 'Exception on updating OperationsGrid '+inttostr(AccountNumber)+': '+E.Message; Raise; @@ -1344,11 +1788,7 @@ procedure TOperationsGrid.SetMustShowAlwaysAnAccount(const Value: Boolean); procedure TOperationsGrid.SetNode(const Value: TNode); begin if GetNode=Value then exit; - If Assigned(FOperationsGridUpdateThread) then begin - FOperationsGridUpdateThread.Terminate; - FOperationsGridUpdateThread.WaitFor; - FreeAndNil(FOperationsGridUpdateThread); - end; + if not FAsynchronousUpdates then StopRefresh; FNodeNotifyEvents.Node := Value; UpdateAccountOperations; // New Build 1.0.3 end; @@ -1390,14 +1830,98 @@ procedure TOperationsGrid.ShowModalDecoder(WalletKeys: TWalletKeys; AppParams : end; end; +procedure TOperationsGrid.StopRefresh; +var LWorker: TOperationsGridUpdateThread; +begin + FJoiningWorker := True; + try + if Assigned(FRefreshTimer) then FRefreshTimer.Enabled := False; + FRefreshPending := False; + LWorker := FOperationsGridUpdateThread; + FOperationsGridUpdateThread := nil; + if Assigned(LWorker) then begin + LWorker.Terminate; + LWorker.WaitFor; + // Cancel only callbacks owned by this joined worker, including the + // classic queued InitGrid. Never remove unrelated main-thread work. + TThread.RemoveQueuedEvents(LWorker); + LWorker.Free; + end; + finally + FJoiningWorker := False; + end; +end; + +procedure TOperationsGrid.SetAsynchronousUpdates(AValue: Boolean); +begin + if FAsynchronousUpdates = AValue then Exit; + StopRefresh; + FAsynchronousUpdates := AValue; + UpdateAccountOperations; +end; + +procedure TOperationsGrid.PollRefresh(Sender: TObject); +var LWorker: TOperationsGridUpdateThread; +begin + if FShuttingDown or FJoiningWorker or (not FAsynchronousUpdates) then Exit; + if Assigned(FOperationsGridUpdateThread) then begin + if not FOperationsGridUpdateThread.Finished then Exit; + LWorker := FOperationsGridUpdateThread; + FOperationsGridUpdateThread := nil; + try + LWorker.WaitFor; + // TPCThread marks Terminated after every completed Execute; success is + // captured before that finalization, and generation rejects stale jobs. + if LWorker.FSucceeded and (LWorker.FGeneration = FRefreshGeneration) and + ((not Assigned(Node)) or Node.PendingOperationsAvailable) then begin + FOperationsResume.Clear; + FOperationsResume.AddRange(LWorker.FResults); + InitGrid; + end else if (LWorker.FError <> '') and + (LWorker.FGeneration = FRefreshGeneration) then + TLog.NewLog(lterror, ClassName, 'Unable to refresh operations: ' + LWorker.FError); + finally + LWorker.Free; + end; + end; + if Assigned(Node) and not Node.PendingOperationsAvailable then begin + FRefreshPending := True; + FRefreshTimer.Enabled := Assigned(FOperationsGridUpdateThread); + Exit; + end; + if FRefreshPending then begin + FRefreshPending := False; + if Assigned(Node) then + FOperationsGridUpdateThread := TOperationsGridUpdateThread.Create(Self) + else begin + FOperationsResume.Clear; + InitGrid; + end; + end; + FRefreshTimer.Enabled := Assigned(FOperationsGridUpdateThread) or FRefreshPending; +end; + procedure TOperationsGrid.UpdateAccountOperations; begin - if Not Assigned(Node) then exit; - If Assigned(FOperationsGridUpdateThread) then begin - FOperationsGridUpdateThread.Terminate; - FOperationsGridUpdateThread.WaitFor; - FreeAndNil(FOperationsGridUpdateThread); + if FShuttingDown or FJoiningWorker then Exit; + if Assigned(Node) and not Node.PendingOperationsAvailable then begin + // Retain the latest account/filter intent without querying or polling the + // pending overlay. Final node notification resumes the normal path. + Inc(FRefreshGeneration); + FRefreshPending := True; + if Assigned(FOperationsGridUpdateThread) then FOperationsGridUpdateThread.Terminate; + FRefreshTimer.Enabled := FAsynchronousUpdates and Assigned(FOperationsGridUpdateThread); + Exit; end; + if FAsynchronousUpdates then begin + Inc(FRefreshGeneration); + FRefreshPending := True; + if Assigned(FOperationsGridUpdateThread) then FOperationsGridUpdateThread.Terminate; + FRefreshTimer.Enabled := True; + Exit; + end; + if Not Assigned(Node) then exit; + StopRefresh; FOperationsGridUpdateThread := TOperationsGridUpdateThread.Create(Self); end; diff --git a/src/gui-experimental/FEATURES.md b/src/gui-experimental/FEATURES.md new file mode 100644 index 000000000..b77e74192 --- /dev/null +++ b/src/gui-experimental/FEATURES.md @@ -0,0 +1,83 @@ +# Classic and experimental wallet feature map + +The experimental interface groups the production features of the classic wallet +into separate windows and focused wizards. This map identifies equivalent entry +points; it does not require the two interfaces to have identical layouts. + +| Classic capability | Experimental entry point / behavior | +|---|---| +| Accounts, key selection, balance filters, refresh | Wallet account grid uses indexed account-ID prefixes and visible-record reads for default pages; arbitrary sort/search queries only the current key selection; **Tools → Accounts** provides the full explorer | +| Grid page modes | Shared Single / Auto / Manual selector: all matching records with scrolling, complete viewport rows, or a remembered explicit page size; switching mode starts at the first page | +| Whole-wallet balance and account totals | Overview totals use a separate cancellable worker with short lock-held chunks; selecting a grid key does not change the total's scope | +| Synchronization-driven grid updates | Node status suspends grid reads across download/application work and final pending reconciliation, then resumes the latest request once; ordinary selections and updates start immediately and keep loading feedback until the current page completes | +| Pending work during blockchain catch-up | Full confirmed-block validation continues while pending balances and new-operation admission wait; the existing background owner replays pending operations once after catch-up, including offline received-block backlog completion | +| Initial wallet totals | Total PASC and PASA show a dash until the first completed balance summary; later refreshes retain the last known totals | +| Overview layout | Both caption/value pairs are centered vertically inside the native group-box content area in Delphi and Lazarus, including scaling | +| Find an account by number/name and inspect details | Account explorer Find and quick search; account information menu | +| Recipient account selection | Paged confirmed accounts with indexed public-key filtering, explicit empty-key results and cancellable background searches | +| Account type statistics | Account selector Type Stats computes the global confirmed report in a cancellable worker, retaining the original report text and responsive controls | +| Next/previous higher balance | Account explorer Tools actions and balance sorting/filtering | +| Per-account operation history | Wallet operations grid and account explorer history; one pending-tree traversal for the selected accounts, worker-resolved wallet membership and responsive selection/depth settings during background fetching | +| Add/remove/all selected accounts and batch operations | Account explorer selected-account list; wallet multiple selection | +| Send PASC | Focused wizard respects native caption boundaries and provides custom fees and optional public/key/password payload steps; **Wallet → Advanced Operation** for advanced addressing/payloads | +| Change key/account name/type, sale/delist and purchase | Focused operation wizards; batch type changes require approval of skipped accounts and optional single-fee-payer selection on the custom-fee step; confirmation and fees cover only changes; advanced form | +| Multi-account key change | Wallet account popup opens the shared wizard; selected-account and confirmation grids are parented before loading in Delphi and Lazarus | +| Atomic account/coin swap setup and hash-lock helper | Advanced form and existing hash-lock dialog | +| Recipient-signed purchase, EPASA/named/pay-to-key addressing | Advanced form using the current classic operation implementation | +| Account-data editing; string/hex/Base58 payload input | Advanced form | +| RAW import/export, multioperation assembly/editing/signing/execution | **Tools → RAW / Multioperation Workbench**; returned partial copies of the same operation retain each party's missing signatures | +| Local/offline package signing | Same workbench; offline mode blocks RPC and submission handlers | +| Operation history and block range | **Tools → Operations Explorer** | +| Pending operations | **Tools → Operations** | +| Operation-hash search and payload decoding | History/pending-operation menus and payload decoder | +| Account/operation information and JSON | Account information memo, explicit operation **Details** button and context menu, and payload decoder; selecting operation rows does not open a dialog | +| Block range, timing, target and hash-rate statistics | **Tools → Blocks**; Refresh/F5 and reset-range actions, persisted averaging and h/s–Eh/s units | +| Connections, known servers and blacklist | **Tools → Nodes** | +| Fixed peers / node IPs | **Wallet → Seed Nodes** | +| Peer messages, encryption, recipients and unread count | **Tools → Messages**, retained by the running application | +| Logs and debug-log filtering | **Tools → Logs**, controlled by Options | +| RPC request client | **Tools → RPC Calls** | +| Network/miner/RPC ports, allowed IPs, fee and checkpoint policy | **Wallet → Options** | +| Mining reward key selection | Existing eligible private-key entries are reused without startup/lookup generation or setting rewrites; no eligible key leaves mining inactive; configured NewEachTime rotation is limited to a mined block while unlocked | +| Generate/import/rename/delete private and watch-only keys | **Wallet → Private Keys** | +| Wallet lock/unlock, password, public/private export, backup/restore | Private Keys dialog and lock toolbar button | +| Language choice | Banner and Options; existing installed catalogs, applied on restart | +| Protocol, block, peer, mining and synchronization status | The banner shows exceptional node status in white, bold 16-point text aligned left after the logo/version, leaving normal online status blank; the Delphi synchronization details use buffered solid backgrounds to avoid blank erase frames, and keep detailed progress through traffic notifications; synchronization view and footer share cached committed counts/timestamps, and secondary statistics retain their last complete result while core work is busy | +| Existing-chain startup and saved pending operations | Cancellable background restore through Classic's configured indexed reader and legacy-file fallback, with automatic index flushing deferred during replay; separate decoding, validation, application and finalizing phases, with bold white progress below the logo | +| Application and tray identity | PascalCoin icon assigned to the application, main window and system tray | +| Grid loading feedback | Shared LCL/VCL bordered overlay with Sphere10's rounded-spoke loading circle; retained rows and a disabled grid during node synchronization | +| Balance-cell rendering | Pending state is captured in the row snapshot; painting does not query safebox/mempool or acquire core locks | +| About and exit | About menu; Wallet Close and native window close | + +The classic advanced operation form, RAW workbench, RPC client, grid helpers, +protocol operations and JSON serializers are reused. The experimental UI retains +its wizard flows, disposal/event/collection idioms and explicit component ownership. + +The workbench executes the selected operation. A multioperation's full contents +and signatures are checked together by the core. Unselected drafts can remain +unsigned. Returned partial copies must describe the same unchanged operation; +conflicting bodies/counters are rejected without changing the current package. + +Classic TESTNET-only diagnostics, including private-sale/swap search tools and +internal operation/memory tests, are conditional development tools and are not +promoted into the production menus. Unreferenced old experimental forms do not +represent live application features. + +See [regression tests](../tests/experimental/README.md) for executed coverage. +Offline tests establish preparation, signing, package handling, presentation and +startup/shutdown behavior. They do not establish live network delivery. + +The experimental sources retain Lazarus LCL and now provide conditional VCL APIs, +paired Delphi form resources and GNUgettext translation. Separate native checks +load all 48 form companions and exercise a real translated catalog. The shared +framework package and checked VCL grid/wizard probes pass; the classic wallet +compiles for Release Win64 with Delphi 13 Community Edition's IDE compiler. +The actual experimental VCL application also compiles for Win64 Debug and passes +separate fresh-data offline startup and native Close/Yes checks. Win32 runtime +coverage remains unverified. + +Current Windows checks pass 525 FPCUnit tests, 56 native core/transport/history +cases in each actual compiler, and four actual sparse/dense main-form processes +(both Delphi and FPC). Fourteen formatter tests and 36 Delphi literal checks +cover legacy progress wording. These are controlled fixture and fresh-startup +checks, with no universal shutdown deadline or live-peer throughput claim. diff --git a/src/gui-experimental/README.md b/src/gui-experimental/README.md new file mode 100644 index 000000000..47a612174 --- /dev/null +++ b/src/gui-experimental/README.md @@ -0,0 +1,307 @@ +# Experimental Delphi and Lazarus wallet + +The experimental wallet is a shared VCL and LCL interface to the PascalCoin +node, safebox, wallet keys and protocol operations. Its wizards send PASC, +change keys, list or delist an account for sale, change account information and +buy a listed account. The [root README](../../README.md) lists its features, and +the [feature map](FEATURES.md) shows where the classic wallet's production +features appear in this interface. + +## Build + +### Lazarus/FPC + +Open `../pascalcoin_wallet_experimental.lpi` in Lazarus, or build from the +repository root with Lazarus/FPC installed. The example uses FPC 3.2.0 and +Lazarus 2.1.0, the versions verified on a local Windows build: + +```powershell +& \lazbuild.exe --pcp="$env:TEMP\PascalCoinBuildConfig" ` + --compiler=\bin\x86_64-win64\fpc.exe --lazarusdir= ` + --skip-dependencies --no-write-project --build-mode=Release -B ` + src\pascalcoin_wallet_experimental.lpi +``` + +Choose a build mode in **Project > Project Options > Build modes**, or pass +`--build-mode=Debug` or `--build-mode=Release` to `lazbuild`: + +- `Debug` disables optimization and includes debug symbols. +- `Release` enables level-2 optimization and strips debug symbols. +- `Default` disables optimization. + +Units go to `src/lib/-/experimental/` and the executable to +`src/PascalCoinWalletExperimental.exe`. + +OpenSSL 1.1 must be available at runtime (`libcrypto-1_1-x64.dll` for Windows +x64). In a new checkout, copy the matching DLL from `windows-dll` into `src` +once before you launch the wallet. + +### Delphi + +Open +[pascalcoin_wallet_experimental.dproj](../pascalcoin_wallet_experimental.dproj) +in the Delphi IDE. In Project Manager, expand **Build Configurations** and +activate `Debug` or `Release`. + +The Win64 Debug configuration builds and runs with Delphi 13 Community Edition. +Win32 is configured but not verified. Community Edition restricts its +command-line compilers, so use its IDE compiler. + +The Delphi project defaults to Win64 Debug. It creates +`src/pascalcoin_wallet_experimental.exe` beside the OpenSSL runtimes and the +`locale` directory. Win64 uses `libcrypto-1_1-x64.dll` and Win32 uses +`libcrypto-1_1.dll`. DCUs and compiler-generated resources stay in +`src/lib/Delphi//experimental/`. + +To distribute the executable, keep its matching runtime and the +`locale//LC_MESSAGES/default.mo` catalogs beside it. + +## Start the wallet + +Start the wallet offline with a new data folder: + +```powershell +& .\src\PascalCoinWalletExperimental.exe --data-dir="" ` + --offline --no-startup-dialogs +``` + +For the Delphi build, use the same options with its executable: + +```powershell +& .\src\pascalcoin_wallet_experimental.exe --data-dir="" ` + --offline --no-startup-dialogs +``` + +| Switch | Effect | +|---|---| +| `--data-dir` | Selects the folder for settings, wallet keys and the blockchain. | +| `--offline` | Disables peer discovery, connections, peer listening, RPC and mining until the process exits, even if settings change. | +| `--no-startup-dialogs` | Suppresses startup error dialogs. | +| `--help` | Prints the options without starting the wallet. | + +In offline mode, **Tools → RPC Calls** is unavailable, and the RAW workbench +works locally but cannot submit. Saved network preferences stay for the next +normal run. Start without `--offline` to synchronize. An empty blockchain +finishes loading and shows disconnected status. + +To stop loading local data, click **Cancel startup** or close the window. +Canceling leaves the saved pending operations unchanged. + +## Use the wallet + +The **Tools** menu opens Accounts, Operations, Blocks, Operations Explorer, +RAW / Multioperation Workbench, RPC Calls, Logs, Nodes, Messages and +Synchronization. The footer buttons control, from left to right, the wallet +lock, network connectivity and the synchronization view. In +**Operations Explorer**, left-click a data row to open its details. + +### Grid paging + +Both wallet grids and the recipient [Accounts selector](UFRMAccountSelect.pas) +use the shared [`TVisualGrid`](../libraries/sphere10/UVisualGrid.pas) for +background loading and paging. In the wallet grids, click the square paging +button to choose a mode: + +| Mode | Rows displayed | +|---|---| +| **Single** | All matching records on one scrollable page. | +| **Auto** | Only complete rows that fit the current viewport, with page navigation. | +| **Manual** | A chosen positive page size, remembered when switching modes. | + +The wallet starts in Auto. PascalFramework maintains this control and its +checks. + +### Recent operations + +The **Recent Operations** dropdown sets the history range for the selected +accounts or the whole wallet: + +| Range | Searches | +|---|---| +| **7 Days** | Up to 2,016 blocks back from the local tip. | +| **30 Days** | Up to 8,640 blocks back from the local tip. | +| **Maximum** | All locally stored history. | + +Pending operations appear in every range. + +### Batch account type changes + +A batch type change skips accounts already at the requested type. A review step +shows the skipped and remaining counts and needs your approval. Skipped changes +incur no fee, and the confirmation lists only the updated accounts. + +To edit the fee, enable **Allow me to choose the fee**. The fee step it adds +also offers **Select a single fee payer**, which charges every fee to one +eligible account. Leave it unchecked so that each updated account pays its own +fee. + +### Caveats + +- **Single** mode loads every matching record. +- Searches and non-default sort orders run a full query of the selected key's + accounts, or of all wallet accounts under **Show All**. +- A cold whole-wallet history query can cost much more than one account's, + because the [history adapter](core.utils/UDataSources.pas) collects every + matching record before paging. +- While the node synchronizes, both grids show a + **Please Wait / Synchronizing** card and disable input. If you turn + networking off or the last peer disconnects, the card stays until received + blocks are processed. +- Final confirmation defaults to **No**. Escape or closing the window declines + submission. +- An empty export password exports the private key unencrypted. +- A storage failure pauses synchronization and shows its stage, file and cause + until you restart. Toggling the network does not clear it. + +## Developer notes + +### Events and synchronization + +The [application controller](UUserInterface.pas), [main form](UFRMMainForm.pas) +and [synchronization view](UCTRLSyncronization.pas) update from block, +operation, connection, mining and settings events. No periodic timer refreshes +UI data or status. Network producers only update state and set notification +flags. They never call UI handlers synchronously. + +The node publishes a typed [`TNodeSynchronizationStatus`](../core/UNode.pas) +record, and [`TNodeNotifyEvents.OnSynchronizationChanged`](../core/UNode.pas) +delivers it on the main thread. Within a notifier, synchronization entry +precedes block and operation callbacks, and exit follows all refresh-producing +callbacks. `GetSynchronizationStatus` returns a cached copy without acquiring +mempool, Bank or SafeBox locks. + +| State | Meaning for the experimental wallet | +|---|---| +| `nssOffline` | Networking is off and no physical backlog remains, so grids load local pages. | +| `nssDisconnected` | No peers or physical backlog remain, so grids load local pages even while behind. | +| `nssIdle` | Connected, with no physical backlog and no selected target ahead. | +| `nssSynchronizing` | Blocks remain to download, validate or apply, or a selected connected peer's target is ahead, so grid reads are suspended. | +| `nssStorageError` | Persistence failed, so synchronization and grid reads pause and the first storage error stays until restart. | + +The [wallet control](UCTRLWallet.pas) refreshes its accounts grid at once. The +operations grid's `RefreshThrottle` waits for 250 ms without a new request, +then fetches the latest one. Refresh requests and synchronization status flow +as follows: + +```mermaid +flowchart LR + node["Block / operation events"] --> requests["Wallet refresh requests"] + requests --> admission["Accounts: immediate; operations: 250 ms quiet"] + admission -->|"Not suspended; worker available"| fetch["Page worker"] + fetch -->|"Queued completion"| page["Publish page once"] + facts["Node connectivity / selected target / transfers"] --> status["Cached synchronization status event"] + status -->|"Synchronizing"| binding["Wallet sets RefreshSuspended"] + binding --> admission + status -->|"Work drained"| resume["Hide synchronization card; resume latest page"] + resume --> admission + status --> labels["Sync labels and footer"] + changes["Connection / mining / settings events"] --> labels +``` + +### Threads and locking + +Connectivity, listener-port, mining-payload and reward-key changes run one at a +time on a single settings task, which receives a copy of the public +configuration. Wallet key selection and the configured post-block rotation +policy stay on the UI thread. + +The node guards mempool access with the +[core read-write lock](../core/UThread.pas), and core does not import +`UConcurrency`. PascalFramework's +[`TReadWriteLock`](../libraries/sphere10/UConcurrency.pas) is mirrored for +application code. + +- `AcquireReadLock`, `AcquireUpgradeableReadLock` and `AcquireWriteLock` retain + ownership until the matching `Release*Lock` call on the same thread. + `TryAcquire*Lock` accepts a timeout in milliseconds. +- A plain read cannot be promoted, so take an upgradeable read before a + possible write. After the nested write, `ReleaseWriteLock` keeps the outer + upgradeable read until its matching `ReleaseUpgradeableReadLock`. +- The framework `TReadWriteLock` factories `EnterReadScope`, `EnterWriteScope` + and `EnterUpgradeableReadScope` return the managed + [`TScope`](../libraries/sphere10/UCommon.pas) record. Keep it in a local + variable so that it releases on routine return, `Exit` or an exception. + Copies share one acquisition. The lock must outlive them, and their last + release must run on the acquiring thread. +- `TScope.Create(CleanupMethod)` also supports custom cleanup. Its method target + must outlive the scope. +- Keep initialized [`TDisposables`](../libraries/sphere10/UMemory.pas) object + and memory bags in stable locals or class fields, and do not copy or return + them. + +Read-only queries can overlap. Block application, pending operation changes and +miner recovery take exclusive write ownership. Each bank commit and its pending +reconciliation share one write scope, so readers never see a new confirmed base +with old pending changes. + +### Progress reporting + +Core progress callbacks carry a [`TProgressInfo`](../core/UAccounts.pas) record +with a typed stage, raw position and total counters, and the relevant block, +chunk, peer or file data. They do not construct display messages or +percentages. The +[presentation adapter](../libraries/pascalcoin/UProgressDisplay.pas) formats +the record for both wallets, daemon logging and RPC status text. The startup +task queues only the latest complete record to the UI thread and drops +superseded ones. Cancellation propagates synchronously through core callbacks. +A phase that reaches its total is not completion, which the task signals with a +separate event. + +To migrate from the old callback, replace its message and count parameters with +one record: + +```pascal +procedure TMyProgressView.OnProgress(Sender: TObject; const AProgress: TProgressInfo); +begin + ProgressLabel.Caption := FormatProgressInfo(AProgress); +end; +``` + +### Conventions + +The shared sources select VCL or LCL APIs, and each form has paired `.dfm` and +`.lfm` resources. The embedded [wizard](../libraries/sphere10/UWizard.pas) and +[cache](../libraries/sphere10/UCache.pas) compile with both Lazarus/FPC and +Delphi. Wizard captions, validation messages, callbacks and grid bindings use +native `String` in the wallet and PascalFramework. Protocol bytes and explicit +encoding boundaries keep their own types. + +### Large pending pools + +The experimental wallet sets a 250,000-account cache, while core and other +callers keep the 50,000 default. The configured limit survives safebox +replacement. + +On a copied public chain with 185,831 pending operations, offline FPC +sanitization took 6,766 ms cold and 3,531 ms warm, and closing the wallet +during active grid work took 8,734 ms. The +[block application probes](../tests/experimental/native/block-throughput/README.md#results) +report block application timings. + +## Regression tests + +From the repository root: + +```powershell +& src\tests\experimental\RunExperimentalWalletTests.ps1 +& src\tests\experimental\native\RunWalletStartupNativeTests.ps1 +& src\tests\experimental\RunWalletGridRefreshTests.ps1 +``` + +These scripts accept `-FpcPath` and `-LazarusDirectory`, which default to the +paths in the build example. The tests run on a separate Windows desktop with +temporary keys and data. They never use the normal wallet or broadcast +transactions, and they do not cover live synchronization or transaction +delivery. + +The [test guide](../tests/experimental/README.md), the +[native startup checks](../tests/experimental/native/README.md) and the +[interactive grid refresh demo](../tests/experimental/native/grid-refresh/README.md) +describe coverage, fixture limits and output locations. + +## Credits + +Herman Schoenfeld developed the experimental GUI and its shared +[Sphere 10 Pascal Framework](https://github.com/HermanSchoenfeld/PascalFramework) +components. PascalCoin is based on Albert Molina's original implementation. +Copyright © 2016-2026 Albert Molina and Herman Schoenfeld. diff --git a/src/gui-experimental/UCTRLBanner.dfm b/src/gui-experimental/UCTRLBanner.dfm new file mode 100644 index 000000000..af880e6f4 --- /dev/null +++ b/src/gui-experimental/UCTRLBanner.dfm @@ -0,0 +1,13210 @@ +object CTRLBanner: TCTRLBanner + Left = 439 + Height = 80 + Top = 23 + Width = 992 + BorderIcons = [] + BorderStyle = bsNone + Caption = 'CTRLBanner' + OnDestroy = FormDestroy + ClientHeight = 80 + ClientWidth = 992 + Constraints.MaxHeight = 80 + Constraints.MinHeight = 80 + Constraints.MinWidth = 512 + OnCreate = FormCreate + OnResize = FormResize + Visible = False + object imBackground: TImage + Left = 0 + Height = 81 + Top = 0 + Width = 992 + Align = alClient + AutoSize = False + Picture.Data = { + 0954506E67496D61676589504E470D0A1A0A0000000D49484452000003200000 + 00500806000000E1BA1134000000017352474200AECE1CE90000000467414D41 + 0000B18F0BFC6105000000097048597300000EC300000EC301C76FA864000000 + 1974455874536F667477617265007061696E742E6E657420342E302E3139D4D6 + B2640000D4D649444154785EEDFD67DBB55771740BBFBFF5D9F603D8D8C6188C + 0D88240122E7201028A29C73CE39E78072CE3927D6EE51553DE75CD72DBF9F0C + 48DA6BE1F63C2FFD827B1CA3AAFBFFF7FF7CE58F9BDDEC6637FF7BF30F5F3E64 + F30F5FADF72B876CFEB1DE7FDCAFE6AB876EFEEF7E99AF1DB2F9D47E876D3EF5 + B543379FFAFAA19B4F7FEDB0CDA779BF71D8E633997FFAC6E19BCF7CF3F07A8F + D8FCD3B70EDFFCF3378FA8A997EF6F1DB1F9ECFE47D6F8FD97037AFEB4F9D79A + 7FFB76BDDF3EAADECC778EDA7C8EF9EE519B7FAFF7DF790FE43D7AF3F9038FDE + FCC7778FD9FCC78135DF5BE7D8CD17BE7FECE68BCC0F8EDDFCE798E33C3F3C6E + F3A51ABFC76FFE8BF951CD8F4FD8FCF78F6AEAFDF28F4FDC7CF927F5323F3D61 + F3951F9FB4F94ABD5FFDC9899BAFFEF4A4CD7E357A7F76F266BF9F9F5873F2E6 + 6B3F3F69F3B5FAFB6BBF3879F3F59A6FFCFC94CD377ED173EAE61BBF3C75F3CD + 5F9E5273EAE65BCCAF4EDDECFFABD336FBFFBABE7FCD7BFAE680DF30A76D0EF8 + 2D73FAE6DB35DFF9CD199BEFFCB6E6A0FAD67BC6E6BBCCEF9833370766BEC7FC + BEE660DEB336DF3FB8E60FBC676F7EC0FCF1ECCD0FFF70F6E647353FFCE3399A + 1F1D72CEE6C763CEDDFCE8D073373FAEF949CD4F790FE33D6FF3D3C3CEDBFCEC + F09A23CEDBFCFCB0F3373F3FFC02CF11F5ADB960F38B232FD8FC92F9D3059B5F + 1D79E1E657475D58DF176D7EFD27BE792FDAFCE6E87A6B787FABB978F3DB63EA + D55CB2F9EDB1976C7E577310EF717C5F5AEFA59BDF33C75FBA39B8DE83798FBF + 6C73F009976EFE70C2659B3F9E70790D6FCD89976F0EA9F9E349F532275FBE39 + B4E6B093AED81C7ACA159BC39893AFDC1C7E4ACDA9794FB96A73447D1F716ABD + A75DB539F2B4AB357FD25CB339F2F47ACFB87A73F4E9D76C8EAAF7A833AED91C + 7DC6B59BA3CFBC76734CCF59D76E8EADF7D833AFDB1C7B96E7B8B36BCEF11C7F + CEF59A13CEADEF73AFDF9CA8B96173C27937D47BE3E6C47A4F3AEFC6CD49E77B + 4EBEE086CDC9F59E72FE4D9B532EC85C78F3E6D40B6FDA9C56EF6917D57B11AF + E7F48B6ED99C5173FAC5B76ECEB8B8BEEB3DF392CCA5B76ECE622EB96D73D665 + B76ECEBEF4B6CDD997D55C7EDBE69C7ACFE1BD82B97D736EE6BC2BEED89C7BE5 + 1D9BF3EB3DFFCA3B37E75D75E7E6FCABEED85C50DF17D4F70557DDA5B990B9E6 + CE1ADEBB36175D7BF7E6E231F7E8BD84B9EE1ECDA5D7F7DCBBB98CB9E1DECDE5 + D7FFB9DE3F6FAEE8B9F1BECD95992B6EBE6F73D54D7FDE5C7953BD7CDF7CFFE6 + AA5BEEDF5C9DB986B9F581CDB5B73CA0F79A5B1FDC5CCBDCF6E0E63AE6F6876A + FCDE5073BDDE873737DE51EF9DF5D6DC74E7439EBBEAFBAE473637D7DC74B7DF + 5B98BB1FDDDC724FBDF73CBAB9F59EC76AEABDF7B1CD6D9AC737B7D77BFB9F99 + C73777689ED8DC715FBDF73DB1B993B9FFF1CD5DF73FB9B9EB81273777DFFFC4 + E6EE7AEF7AB0BEEBBD87F7A1A736F7D4DCFB20F3F4E6DE879EDEFCB9E63EDE87 + 9FD9DCF7707D3FF2CCE6FE479EAEE1F53CF0E8B39EC73C0FD63CF4E8739B071F + 7F6EF3D063CF6F1E661EAF7962CE234FBE50F3FCE6D17A1F7D8A7951EF63358F + 3FFDE2E6F1A75EAAB7E69917374F30CFFA7DF29997364F3EFB724DBDCFBDB479 + BABE9FAAF7A9E75ED93C9D7986F7857A35AF6E9E655EE47D6DF3DC8BCCEB9BE7 + 5E7E75F3FC4BAF6D9EAFEF175EAA79D9EF8B2FBFB179F1957A5F7963F3D2ABAF + D7D4FBCA9B7A5F7EED8DCD2BCCAB6FD55BF3FA9B9A575F7FABE6EDCDAB6FBCB5 + 794DF3F6E6B537DFDEBC5EEF1BF5BEF1C63B9B37DE7A7BF3E65BEF68DE1AF3DE + E6CDB7DFDDBCFDF67B35EF6EDE7AF7DDCD3BEFBC57F3FEE6DD77DFDBBCABB7E6 + BDF737EFD5BCFB7EBDEF7F30E67DE6830F361FBCFF97CD071FFC65F397FAFECB + 079BCD5FFE52DF351BFFDFEEF709FFED006437BBF95F9F02907A05215F2D08D9 + AFFEAEF9C7020F06F00042FE2FE091F95441C7A7BF7EB821A4DECF08400A440A + 3C18E0E39F008F6F1E2900F9E7868F7A051F01917FFD3603801C29106908F9DC + 77FE2408190052F379002410F2F9EF2DF3FD63365F38F05841C8177B0A3E3C06 + 902F31C0C70F8EDF7CA9C0830142FEBBDE0690FFFEF1F105210691AF0846EA05 + 407ECA6B08F9EACF0A3C0223028F9AAFF3168C7C9D69F8F865C188DE86909A5F + 15880020BF3C4D20F2AD8290FD0521051E3D0523DF2E10F976BDDF0144009002 + 0F20040039100039C8F0F1DD028F03818FDF9DB539F04301E4ACCD0F0A3C3480 + 48CD8F00907E0F05460A3E6A7EC214781842CEABEF0088DEF3373F3DFCFC8291 + 8047BDBF680839B2BE0B3E7EF1A7F30D228290821101C8859B5F1784FCEAE87A + 0320BF013E8010E0E3D8FAAE39E8981ADE06100D00728900441012F8E0FD438F + E0A3DE0008F0716820040039140039D97378CD61051E4700220D1E3587E73DF2 + 5400A4DED399828F9AA3F402209EA36B8E610A3A0091A3051D05204C81C7B167 + 5F6BF8A8EFE36B4E08849C704E41874004F0A80140808F829093160839B9DE93 + 0B3A7A4EB9B060A4E08339151011804C0801404E1778640A3C7ACEBCF4160388 + A6C0A307F8B8ECF6004881C7E5051D7A81900690820FDE028FF31B4278AF2EE8 + 003E781B3EAEB9DB73ED5D051E4C4188C0638110A0E3BA7B3797DE708F01A4E0 + E3F2828ECB6F2C10190052C0017C0021051E3D0340EABDBA5E0600B91AF80880 + 083E78051F81909AEB6F033EEA2DF0004204207718400421F57DD39D051E0290 + 87051F37DFF5E8E666E04300027CD4D43710725B7DDFC60B7C0C00794C0072FB + 7D8F6DEE2C08013C042035771578780A3E6A808FBB1F782A105200C200211900 + E4DE028F3F3F5CDF000820520072DF23CF0640EA6D00A9013E06803C660879F0 + 89E70C20F53D2004F868101180183E1E2B10611E6700919A2704222FD55BE031 + 00A446E0018030AF6C9E7A1EF078B900E4D5CD33CFBF5ADFF53680D43C5700F2 + 3C2F10F25200E4E59A00C8F34048CD8BBC05212FBD5CE0017C084698028E7A5F + E17DCDF36A818801C4F35A81C8EB998690D7790B38F68590828EB7EB7D871708 + A929006100110348BDC0C77B1F0444800F838800A406F8F840F0512FD0210001 + 3E7604F2FF85DF0E4076B39BFFE5013EFE1FC14700A406001184C88200228709 + 40984F01215FAFB786F7D3C0470D2F26441604FB0188001FDF32847CB65E9990 + 03EA5B10F227CF624298CF35887CE768DB90820EEC870CC80221981000E40BC3 + 801C53F071DCE68B00C9F70B3C6243BE58E0C108420010D98FE3A60501406A04 + 1F7A818F7A01909F6041808F0610CF7EEBC88430A714801498083E022035DFFC + 05F6C3F0610B02801478C884F86DF8C080F83DA340A466181083C881B220051E + 8190037F7F462CC8591E002410220352F3FD020E9B90020E06003904F83867F3 + E3829036213F3EA4C0639890804700A44DC88010C0A3DF800720F24BC0031B02 + 841480FC0A0382112910F90D466440485B9000C831976C0E2AE0981664019182 + 0D43C8A5028FB6207F38E172C1C71F800E0CC88957147C30DB1604FB017C3087 + 9F5230028860406A0420051EB62036217F2A080140800F81880CC8D50290A304 + 1E05231810A6BE0D21061040E4B87302228B05C1809CC0002301102C08F07132 + 1072BEE7E4F36FDA9C7441810700324C882D8800A440E474D91000A45EC1C7CD + 9B332E023C6ED99C291B322D88E632438840040BD226A406F000446C42EED89C + 7779810606A42044166440C862410090AB0D20175E5DF0C13484D46B0352E0C1 + 0840EED95C5A0062F8B86773794CC8E537D40B8864042280474348BD57DD74FF + E6CA828E2B6F0980001F7A818FFB0D1EB704406AAEBBADA680E47A4188C10308 + C1800022370C0B5240221352B358101B100388C04330F2D8E6967B1B3EF21678 + 300D21B7DF0784D880C8820022051F771678DCF9404188060BF2C4E66E0C4803 + 080644F06100F1FB8C00649890479902907A1F1810F24C41C733B2200F8D796E + F310E0D1F324EF0BB6204FD4DB00F2640C48BD8F3D5DAFA62104F8C080BCAC11 + 84F002206D409E7F79F334200284D43C9379B640A40DC833B21F051E350340C6 + C482BCF29A008479091B8205098060400C2101101910C0A32DC89B0520AB0579 + CBF01103F246E0E38D028F370A3CB01FD3820442021FEF1478BC5DF30E0684F7 + BDF70A3E980690F7653FDEC77ED4BC2FF8B00999102204D931C827FCB70390DD + ECE6AF31051F86900920981086181620A208163684F895C606845706A4071352 + 10620BE2089663581342DA807414EBDF8010C0432F16040372B4A3583221DB10 + 22F8C0841C088438860588184212C7023E3A925500621302806043B020C71578 + 148808408EDFB2205F01460A42BE2208B101F90A0012109105F9B9E183B70D08 + 11AC358AF5CD000806E41B0210E0A3FE1B11AC0690B61F9AD3653F88617D5B31 + AC58100C482C880D0836A4210400D98690011F02917364411CC3C27E00201342 + 7EF24747B00423879D53C0810D690352F0D123FB719E62583F3B822816112C40 + 0403C27701C712C7FA2500220B6213027C6C9B1020A4E0E368C7B0042180C702 + 1F5B512C01482CC800902582C5C8805C61FB51DF0690408862580048CDA98C41 + A421C416A40600A9F728A6016435216762413205208A6235842886550072D6F5 + 0520C0C73684288655108209C17EC884001F6D41341810DB10D98F0610C5B10A + 3A0220A71578AC16E474D90F9B908E6211C13A53F6E37643C80A203121189073 + B01F00487D13C51A0664CB82001F051DFDD65CD416A4A0A3BF670CCB0072E980 + 903620BC051D6D428864DD582F731310621B320C086F81082664404800A421E4 + 9A028F6B031F9890EBEB5BF6032372872D08712C0C880004FB111B72D31DB120 + 7766F82EF0D0DC83092900C17E1488288E9528962CC83021338235635831214C + C7B16241EE8905B9B760C42F43140B20491C4B16A4BE1F291029E0C084002003 + 426240643FFA157C3CBF79A84DC8938961C980D43C6513F258E2583221020FBF + C007512C2CC8E344B1885FD54B0CEB2926160400792636442644101200594004 + 080140560BF282661A105B10E25844B09629F87839431CEBD5C58200230210EC + 07E643F603087947630831880846B01F05236FBE536F43C83BF5B60579F7DDC0 + 070604F858A2580291258655D42108C93882D5E8B143904FF26F0720BBD9CD5F + 6D0A3C02211A8107D12B2258DB83FD700C2B5D908E636140000F4626C420820D + F96C5B900121D88F23653FFEB55EE0437D90EFD8803886D551AC820D2064E982 + 084402205FF85E7D633E6242661FC42604F89005014478051E3386250829F818 + 7D9074411A40BEFA93091F6D421CC33288740F4416441052336258051C4C4004 + 00217EA50E4820846FF540022107147CD00911882C06E43B1DC1EA510C0BE828 + 00490CCB16A4E04320420CAB4606C400A21E082644E6A3ED4743880D88E25835 + 44B03418104D4044B34048BA20B2200211EC478107430C6B3120BFE5BBE0E337 + 051F00C86F88620122F5FEAE206400C8D205A107F2FB132EA9A96FC1C7328190 + 3F9E7459BA20868F01220210DEEE7F0021AB0171244B00C28BFD900931800C0B + 722633014416A4C0636F144B3D90C4B08E3FBB41C40604F8D0BB46B134B621EE + 82B8072200690BD220721126040071140B0B0280A80F72C9CDB21F2B80D88200 + 2105237BBB20D80FA2573121C047C7B00420F57DBE40A406FB7165DB8FFA0E88 + 28868501B9B65E1910C30710320CC875770B3E068800214047A62DC8153710C1 + 4A1C4B1D10F740AE6C080980A803320610E9081616A48083BF818F8E6461410A + 42643F808E61416AEEC28400206D410A3A0220B7DC4D17841EC823238625F890 + 0959202416E48E7BDD05E908D61DB120770740EE1684B80FD200421FC416C4F0 + 314C08F0F1500C88A258B307727FC1C8E8820C10710CAB2D880084F8D5785F08 + 846CF7410090470B3A0684D0011188BCB47942F6C3037C18420C200D21EA82C8 + 84388AA5181600F2E22B1340B02144B2E883603F6A8605D1147CD4BCD47D909A + 9719E043F12B8C484C0810320C48BE6341DA8008400091589037018FB62080C8 + 808F0540624218C7B0E6C8801484CC18162082FDE03B0644FFDBE1C727FDB703 + 90DDECE6AF315FE63D6444B1BA880E842882A53706A4635889627D4663FBF14F + BC051CDB7D909AFD534617801C2103326258BCA30B12FB710011AC029161410A + 3E80105990821045B00C22FF018C08400C21DD03191624B396D16D400C209A82 + 0FC651AC7A0190C582A8949E28D657D203E928D656178432FA3021B6201AAC87 + 60A4FB2040071D903D3D908290FD31201DC33A0808590064400871AC828F3621 + 80C7EF6C40BE3F2C080388D00301441CC3FA418D0AE931200D22EA810021810F + 4F77402680183ECED3EB2E488107DFB22029A307408860312AA30742041F8008 + 268418564C882C085D10D90F8388EC470C88E0236F7740187A209E4088E0C333 + 7A20BC29A33B8A5543215D7D90069082113A208962C984603E1A40B01F3D0008 + A5F4828E63CEBC6ECB821C57EF71322135C00720D2F12BDEAD2EC8F59B93F2ED + 32BA8BE836206B211DF371E3960539B50DC86A416ACEE41BF8208A05785044C7 + 7EC4849CC35BD0E10142EE30880020F58D01391F1372557DD760415C447717A4 + 6358CC453221052035C4AF66211D00B10511785C77AF6004FB41215D2604F848 + 0F4413F8C08010C5521C2B00220829E0582DC835BCD80F4C48C7B06441886219 + 3E801095D0896375040B0BB21A90310048BD94D1554877144B71AC981017D217 + 0B52E0711B1D90C4B0EE004428A5D7F7B02080C70090B620800720D210B2D88F + FA5604EB91800806241664000826A4E1A3DEEE833CF8F8B3029007BB07A229E8 + 609E7CCE1644F0B14288A358EA82A890BE0288A3583221F5CE2856ECC7F30521 + 00C80B2F1778248E0584685ED3ECB5202F64DC03E918D63680D8802C10220B92 + 32FAD2056903A22856010851ACD7042036208290FA7B80081D10FA20EA820442 + 28A4D7BCFD5E0308EF07DB06A4BEDFFFA00600098418407857083188EC7E9FCC + DF0E4076B39BBFD2A888CE2C164406441BB128A3B30DAB2184D7002213D253E0 + E16D5840C87617E49F0A423E5BDFC3847421FD00F740BA0BA2423AE643162453 + D0E1AD5840482CC800906396AD58D380ECBB15CB00D2512CC0C37D90B6207E65 + 416240E881B80BE218161BB104201A03085BB1B40D4B3D9058107540329890B6 + 2080882C8823589A012086119B9044B1461C8B1856E258356CC31A3D90358675 + 707DD70021A30B42095D6FC1870CC86242983D16A47B203FA10772E8F90521E7 + 6F4108312C810836E4C80008F62316841EC82F6AD40751FCCA7D10F5408EBE30 + F6836FC3C76F8E75174485F42EA3CB7E00201342B40D8B218655E0D126848D58 + DD070144041FF58D05F114789C74E5D888650B12F8E877B520C047BD58106C88 + 01E41A6FC51A1052F011006113D6560C8B08562CC87131204C17D1471744E071 + 7D40A4C063400831AC0048ECC71AC552044B31AC5BEA6D00B10971193D23F828 + 20218AC5B700E4D6D103E96D5880485B101910225800489B90BD1604F38109E9 + 972E88E257776D2E0E882886251B620B7229A5745E45B152482F00710CEB5EF5 + 3F3A86058410C51A0644F663CED53705407A008F5BB01F89606141D4016913D2 + 5D90820FC5B06A0A386EA0071208C18210BFC2828C3E880C48008497427A0004 + 0B82FDB0050980001EF73EB1654126842C31AC7A31200291828F7B1F783A1624 + 0380F0CA82C484B00DEBE1BD16840E887B20F4415448D7F7F3B620A38C5EDF80 + 47FDDD512CC1478DA358DD05A9F7990211BD0521896109426240BA94DE10220B + F23C712C9B106DC39201592004F8681352E0F15C97D1471704F0A0076200995B + B15E2F0049111D1029F800463A8A250352E0210342197DC4B0624302226FBE59 + E0411C2B1664F4408090028FB7B02000C8BBEFAB0BA2327A3662B501E932FA07 + 98101910E06346B0C6462CF04304B2C3904FE26F0720BBD9CD5F6BBEDC06841E + C81F37FFA8B5BC400846C40664AEE64D0CAB464574CC872CC88C613580C882F4 + 6A5EF540F600C8FE476EFE75FF8047BD5D4677117D82883762D9807CFEBBC7D4 + DB3D900921DD03D1FCE01801C81756034207649810F7406442808EE5F55ADE3D + 10D206442FE051EF009140885E6258312118915F1474300520DFEA18962CC802 + 22051F07D4EBF89521E4DB8C2258359890D582D4AC2B79796D40022231213FF8 + FD1AC5023ACE4A140B03421FE45C41C88FE981044464415608490C6B5810FA20 + 0210CCC73282106FC3A288EE423AF1AB0B6440F6764114C7C282C4808C32FA56 + 172406A4E677051E8C2DC8041047B00C22DE86D51012F38101398552BA01E450 + A02371ACC362413A8A75A460241052D365F486107541643F3C63256F0DF0714C + C7B116083956168418D60D312180480DF0212029F0608010C1477D5FC0B80B02 + 849CCA9B1EC8298A6179460C4B51AC400851ACEE8214706C5B10774130210691 + DBDD05B90C0B928D58C007DF0D205810EC872CC85DDA866503B274415444AF6F + 45B16C422E0140B40DAB216402085BB164400A44461704F0480CAB3762D984DC + AF0896A358B120D810E257F5BA0FE2E908962610A22E4843C8B02040888BE8BC + EE816413D69D1DC36A0831888C1896B661D508401E5D002416A4E6766DC3EA01 + 4052469709710C6B9810599059481F26040B92F136AC19C5023E3A8A757FE0C3 + 0042140B00A9EF2718208408560D103200C410E26D58CBA484DE3D102084B5BC + C4B00C2158100F10D200D2657447B10C2200C873238A8509013CE61840DC0779 + 5136C465F46140B492B7A68003F0B009F15A5E97D113C1C284C48208400091EE + 8060420091B70B44042044B012C36A03F236112C5B1045B1DE0B840840DC0779 + B7E0636B25AFCAE88110C0634CA3C70E403E89BF1D80EC66377FE5E11E488348 + 6FC3C280EC05904F0B42524667B01EC4B102217D17A4EF810022A303120061FE + F5008388BB208048C147BD6CC0EAF9F7B621B11F8290C0878AE83220F58D0529 + F8501F44C3562CA62DC802228190FFAAB76358829011C5A28C7EFCE62B811056 + F23684001E86908C0C48864D58028F8291028FB99A77AEE4DD17406C41340290 + DE86357B20BC028F31060F4C089BB06C4112C5C27E0840800FCAE88B05D1776E + 82043E0422C0474F03C88861053C78633F341DC34A19BD6F82001F06115B902E + A3EB36C85117C78610C52AF800440A3EE882D8805C5CF051B3F440B6BA208961 + 6935EF89D89094D1B12101907113A400441BB102225D4657213D10027CD886D4 + AB8D580522C4B014C7B20951170400E9E90816F0512F06441644200280B8907E + FC59868FB1116BB51F1AEE81D8827419FD445E1990029200C8C9581040649890 + EE81D880B00D0B00394D5BB01A406C3FDC0571199D1E88202416E4ECCB0B442E + 4F1FA4C063AEE67514CB16647B2DAF6258018F7E6D41EEB6092182250BE2D14A + DE820FD6F37617841E88A2580521A30B72A34D487741AE64DA82DC0884B0110B + 0B322144264411AC0290DBEAED1E480AE9029005428601594C880AE9B2207EDB + 8410C1EA38963762193E0C20B1203121821075401E1B6574E0831E0825F46D0B + 028034845048CF10C30A84782316F0F1D49E1E8841E48140880C0811ACC4B06C + 400C221A95D1BB070280603E80101B102258F32E48BA204F1942BC0DCB26E4C9 + B196B76689613D5DDF7B214406A4A7E0E319000413C24B0CAB40E48517F7F640 + DA82001E89622982552F1BB100908E6311C5521CAB635836210290B5949E0816 + 20420C6B6CC452113D65F400487741DE7EC716848D5802110C48E258EA823488 + 083E1A40BC96D7F0C1FF76BF4FEA6F0720BBD9CD5F71B4925765F440485B909A + 2D08510C0B030288D88210C51AF081058909E951115D1052C02108E1FB4FDE88 + A5AD582B80D005392A8574C348C7B10C201EC7B06C421CC3AA170B72A023588A + 61D109E92816EB7913C1C2842886D516E48727388655D0017CB405193741346B + 193D37416240F6237E55DF7BBB2073252FF06113027C3480A888CE0840EAEF02 + 10CF0410AFE5C5842C1644066489620120DC0401426ABA0BE2285620E48F6D42 + F6C4B01845B00A485603B205209E9F1EC13480741764B5209E5FB209AB6044F0 + 211029F8C084ECB90BD2512CC5B014C5023C0A448E0342801147B17E77BCE358 + 0D21BE09526FC1C7EC8310C3F2C886C4821C52F0D16574E258EE82B88C7E98CA + E88E606DDD040984782D6FFA20988FDE889522FABA925720C23ADE7AB50D8BEF + B19297DB20989006110CC88C60692B56E0C331AC828E6CC4EA289601C4DFC4AF + B40D0B001184D880F4B405218A354C484107712C6DC55A0C0820A295BCAB0521 + 8E0578B09297DB20A303920140460CCB51ACB19277F44112C3920501440A3C18 + F540BA0BD2160410B96F73392052F00188183EF2C67E8C1E8820C4168408D6D5 + 6CC41280D42C202200D98A613D3C3662011F37001CC00716A4E1A3DE5B6A3021 + 1890DE8A6500690B6208B935F06113E2710F0408A18C5EE0512F374130216D40 + EE29086158C97B6F5B90879EDCB220F7E6260873BF40C400A295BCB220B38C3E + 2104F8A85731ACEE81C482681DAF0D88BB20CCF35AC9AB285620C4A5749B1077 + 4130208E5FED0321CF721764059018100084189600040B1213B2D50729F85840 + 641E277C7DF33240D23D90C4B04617A4404451AC40882D48669F185677409814 + D2051E5890F71CC1EA29F8700CCB00A2AD58C4B13A862510B10101426441729C + 9081423021A6901D8A7CD27E3B00D9CD6EFEEA5300D2130BD2EB787D151D0829 + E0D075F4828F00C858C99B28D6808FB10DCB6303D2712CE0A3A3580048BD14D2 + 0B44740F4445F4020FE258E980288A956D5894D1FF1DF0C87CBE6063ACE4ADF9 + 22037C640420EA8118406C41BC11EB4B1DBF0A84682D2FF663B90BF2D55C47EF + 3EC87E40482C08F0E10896E1C310E26D585F2FF87017E464191017D14FB101E1 + BBE0439BB08862FDFAF4828F5315C1D25D9040C877E88300200523ACE4DDDA88 + F57BCAE8C00730D20072B63A21DE88B596D113C3EA327A62585B65740D774138 + 4A786E01C8B99B9F1D4A170403D216A45F5F476F00D1362C5E9910C7B1860901 + 3E28A4AB0B52EF51B120C75CB439480380248A55F0610849172436C4007299A2 + 580048C7B046144B93423A00C27D10DD0531840C13A24E882D48DF051180043E + BA90DE1BB17A2B5643C88C61398A75F499D7258AC51474A80F420C0B10013C1A + 426E1816C43D90D81020A46F8304424E613020B1200D1F02110C08200284001F + A30BD2512CDF05393326E42C46312C22580C106203B20D2035F5ADAD5857BA0B + 42074485740DF0B1BD9657164426A4A0433D103A20C4AF7C9C10032200218295 + 18565F46BFB4E0E35295D1DB806412C3B209B96F73D58DF7EF8190828EB6209A + 358A45117DDB826C6DC4D2EB32BA6E82E89D066446B13C6B17846125AF0084BB + 208A61AD1604F8F0DC191069006908197D9011C1AA17E8E8185697D217009105 + 510FA4AFA34FF89810E26D58DA88150071172433B6611584F09D28960C087741 + 02212AA32B86C5388AD506441BB1EA5B6B7913C3328004429E33843CCBA807D2 + F051DF818F358E4521FDC540C80A20A3889E28D680102EA4BF5EEF3020CB9B28 + 1610C255F4B996D72300C9EB6D58EF1478F0169074044B0604FBD120922E487D + 8F3E48E0A36358EF031EB2213621D121BBDF27ECB70390DDECE66F35F440623F + 0684A8909E5111BD5EE0A341E41B8E63691B1636641890020EA0A3EF82287EE5 + 28966C08112C2C88E0232B7919C0A340A47B20FFF6DDFA063C00912D0B327B20 + 40C817EA2582E52816F6E318AFE32D00F9524308F041191D18118064356F8188 + 62588B056900F9D0B5BCDD03F9E9C90691820F6C88D7F19E640BC2682D2F2072 + CAB0200088DFC4B07E55F0D1312C018921C465F4C4B0786341BEFBDB33078474 + 1F6496D2B11FF5CA82AC006213B242C80A20BCC0C78F0B3E181910754100917A + 971EC8761CABC0A340C40092C384BD0D0BF8A8D76B796D401CC3AA5707097B80 + 117A20860F4D0C4877410E3ECEF0D11032EE82043E5C460740180C88CBE8C4B1 + BC8EF7F27D006416D2DB82D4ABF815008209E1FB9A0048FA205D441780D886F0 + EA20A16258051E800810820501426A6441301F5810C0E39C1B27806802210191 + AD6D58821097D1DD0131809C7AF14DEA819C3120A400A406F8C082001FB620B7 + F930212604F000448090FADB5D904008402213D2312C47B1E65D105B900B6342 + 2E0242DA826830201FBE96F7D27A05218CEC07657400C416A40BE902102CC812 + C5F25D1000846F5B90D103D902106FC5EA1EC875B7C78410C5127C7846115D00 + F2C8E882B88C9E4E4862582B806C45B1041FCB2C9BB07C17841896BB20DD0111 + 8C603FEE7F4A45F411C38A01B907FB516F5F453780EC85105F481F1BB19844B0 + D886C54D90871E7B5ED7D10521F50A3EF602882C4877407ADA801844D405791A + F07014CB6B79D30379F69582106FC46AF87886DB20E98110C17AB68083D5BC44 + B1041FF5DF0C2018907A818FFA7EB14D08F0D1D7D1896101204B0CABA358BA0B + F2DADB03403A86E52E08F0F1B6412400A2C18074044B10D2312C4388D6F20642 + 1CC372148B0BE9C047F741D403A9B7ED876258350D1F0291DDEF13F5DB01C86E + 76F337988E62FD63BD2AA4073E6C426A808F0542DC05C1807846199D3816F1AB + 5890061159906FDA8408403A86150BE295BC00488FBB202EA31B400011366209 + 406A384ED810A208D6F702201DC1D214780848808F189000887A2002105B90FF + 6AF85800644088E0A34124312C0108312C5B10BA20F440D6C3844C6FC392F9A8 + 9105F9B52164F44030214088A60104F8E801400C1F2AA30F003923F0E13896E2 + 5702911AE0A35E0308F0E118D60F74949042BA0144A30BE96B140B0029F0C082 + 14808C42FA7A1DBD00C410620BA20E0831AC4088635801903D10E22EC8C59BDF + 147CB8909E95BC29A3FF5E6300D1147418406A808F4008712C6DC5227ED5A574 + 5D483784B80B121B220009840440DA82B807D25D10E003037295014411AC5C46 + C7820020F440461724D7D10520D88FEB0A3CFA6580106C0800D23072832FA463 + 40CE2786E528964D08F01108B9B05ED6F1168C8C22FA8862B505A9E1327A00C4 + 16A480635810DF0391090144041FB620BE0B526FC1878F133A8A759E2EA3AF10 + 3257F20221D380248625F8C87BDD5D051F7717781488A403A22816112CCAE8B1 + 20EE8300210690DE88B51E261484B405D175745B104348BD3A4ED8205210322C + C8435ECB0B7C10BF0A84D0036900118C1474DC18F81084DC5D10027CB405A977 + F4410A36FA2E0831ACADCBE800C80A21BD194BF6A301C406C416A4A640E4EE02 + 1095D115C72AF8D046AC020FF5416AE8800840964914EBC194D289623D40091D + 10013ED403A1986E08511744E6C310220051211DFB91779F18568D00C41644F0 + 5130D231ACDE88C5714200E46945B01CC33288001F6D420C216CC4D261C2FF3F + 31ACB6201342DE50FCAAD7F1CEEBE8BE8CDE1BB1B48E971E48A2587D9470D88F + BCBE0D0274F43882354C48C7B0DA82C880147CB00DEB3DC7B00C207E895F7D50 + F4313662ED34C827EAB70390DDECE66F30021075409843C74A5E1F279C066458 + 90AFCD8D582EA2031F1810AEA21B4246144B262410820939806900A901400EF0 + 5ADE09207BD6F20E03421C0B0049119D49197DFB327AA2583F307C7094706B2B + 5601870BE981901F12C7327C7C58148B18160032AEA32FDBB07421BD21A4A358 + C4B00A3C0420E982E836480C485F466F08C17CECCF462C6DC5CA60427A54422F + 10A9F7402C48C187FA20C37E2CA318962104007121BDE660ECC7EC81B015EBC7 + 81907DCAE8D986C55A5E01083052E0F153D90F56F3A68CCE66ACBE8E0E840846 + 0A420A3E98D105E9117C0440643F0C22DE8855DF051E0711BD0A886CADE66DFB + D11052037CB88C4EFCCA112CAFE44D0F2416645C47A707D265F4061186085603 + 4856F21A42AE711C6BDC05C96A5E0C886C8821640048C063858F8E621D7F6EE2 + 586D4262413C8E60358430AB05F17BF3760C4B001208B9A45EEC870E13D67B29 + 30729B475D90829298106FC4023E624104218962093E32051E7BCBE8BC029011 + C372FF63989002904B8865298645242B16A40D88861E082052D0313662258E15 + 00611BD655B220C4B06C3FAE8C01690B32625835732D6F0024512C45B0042286 + 915946F7462CC7B00A3C3218108AE8371574DC7C8F2D08EB78BD92B77B208F1B + 44000F599019C5BA13F0E04AFA9E32BA2C4841878C88202431ACB6201DC5023A + 64426C400010BA20ACE4EDAD582EA42F062400F2C0E3DC03C184CCCBE88F3480 + 04424604ABFB204FDB888CB5BC8190BE0BD297D19F4807645DC9AB189600C410 + E2C384D810F740BC0DAB060B9232FAF3EA82BCEE9B20189097DE8801A9BF0B3E + C65D90D10571319D95BCDD030142461C4B3D1017D1BB8CFE065310E2223A9332 + BAC600E2C384051B9890BDDBB0E880001F9A0FD4070140B6A3588110C0A32708 + B2FB7D727E3B00D9CD6EFE86B3DE05711C0BF8C0806042B020F4406C40FA2E88 + 87089637630D00C1840020C382F414742C166446B1F65890011FEE831842DC05 + 991BB1886251444F244B20B200484D17D15D46F72886B50288FA2099C4B16440 + E87F2CEF1AC3B209592D88E3585BD7D1810F4C08164420620051140BE8A8771E + 276CF80048B601A40F13B20D4B6F97D13121B11F5B17D20B3CBE1703D200F27D + 5990EE812486A5991BB1B6D7F1BA0BB2B5154BF081098905117C6CF741BA0BD2 + 6B7955444F195D1D10F5400222020F47B10EEA75BCF5DA82D001098028827549 + 225835CB85744188A2580088A3588706426C400C20DB5D9002918E61011EA718 + 3CBA0F32B6613100484C08D0C1857497D1BB0F4217E4BACDB14C606442886358 + 7D1B64DD8AE53E880F13BA90DE36A4DE0B12C55A63585810E0431BB16A7413A4 + C0E3623A2198905B1DC54A0C0B1BD2512C7541800F8D0144B35810E0030BD206 + 44A32E482C08AB780B3CBC961713D210920856A2580D20FB94D16340B496B721 + A4466574C0836D58052144B00C22D382EC53489701691B12039222FAB580C7AD + 0F8D38D6BA116B5A1000C410D247096541EEE67B5DCB1B13D210420724312CDE + 0120C008F60308217E05842C00E295BC94D15D4857144B2604FB51EF0A210D22 + 638861B907D2C70989618D427A4108A3227A6FC35227641A905EC9AB42BAA258 + B120F440021E8FF551C2DE8A953E880D882D086574E6E9FAD6F43D90E758C75B + D02108E1E52861E0030869001184300690E7010F599065252F3741F4268255E3 + 427ACAE81810A258B11FEA81641B16F3067D102C884C08E6A3608437312C6DC4 + 2A00A1904E24CB512C0C4883487A20EF278A250BE28D5863808F2EA32B7E0584 + D4BF5845203B0CF9A4FC7600B29BDDFCCD26457495D11DC372172416040829E8 + B00DB101D982108AE801116DC46280100C8840E4C87441EA9B55BC40884C488E + 122E7D1083C804100D1092F8D50A205AC57BE0B19A5144DF1BC38A096908F9CF + 020F95D1797F745C22584B191D001184C480D4CB46AC11C5AAB10189FD6058C7 + 2B10E13B512CC5B0DC03D15A5EC187CBE8F33A3A20120B5200E2B5BC8E620121 + 238255AF004411AC09218A621584B88C5E2FF00178E826480DF00184F446AC1A + 1990C00716E447BD0D2B10A22EC88090F33D877FF85A5E83480107F60310097C + 1840660C4B266418905890D5840C08C95ADE0088B66169A605D986905810DE82 + 8F2EA3AF6B79759850100280D4F09E7AC5560CCB3D108FA3580C10922E48A258 + 1DC3D2562C3661C582EC2DA43784D00739412604F8700CABEF829CA0B5BC051D + 5D46AF6F1990F3BC0D4B9BB1809006110D2604089900C2D00799870901903D1B + B1EA6F97D11B4466146B76403294D11B46B020B21F7ED50329F8D056AC820E01 + 8820C48709C775748107531072FDDD631B5603C8A501908E62B5011971AC44B1 + F6DE0431841470300B80001FC4B08605B99D4E48414741882FA3BB8C0E7CF446 + AC6D0BE20EC8384C48213D00327B20804740A4E0E356599000480D00D2B74114 + C5E23E08F64320E295BC5ECBBB5810E2578A61D10729E0E8CBE899752DAF4611 + AC3D164410D206C4312C008479247F1B42267C8CEBE8DD0349146B7641B020F5 + 5DF0D1870931214FB2050B2352F0610B6208D11474C88400236D42460FC431AC + 17042180477D1778A80FD29318D68BE982BC5CDFE33861C1C7CBEA806C6FC37A + B521A48004FBA12E48DF049101A9370684ABE8DB16A463580D214C41C75A461F + 0052B3D78010C3CA51C20122F50FD71D827C327E3B00D9CD6EFEC6A34D587B37 + 62D10711803001104108366481100AE980485B90B1112B2082010142041F7CC7 + 80248635BB2094D13DEB61C2CFF30220DF39668208F683489656F2328008E031 + 2D8857F2D6083ED2039105A9EF98106DC50A7C68041F361FF336480348CAE8E9 + 836C1990020F03485B101B90BD16A44DC8EC82B011ABE1C3D35D100F10120392 + F98EA2588CE143E6431BB18860398A45117DC4B004226D4212C7A203120B62F8 + 5820A48BE80CE64300620BD2DBB0BA8CAE28D66240041F35BFE65B0012085941 + 04FBD110A20E086F4108E0A11856410736A40084A3840D228610C7B1BC11CB16 + 44C709010FBA2001107541D20351142B312C1D251C0072A501040B028C9CB61E + 269C06A401E4184108D0110B420F648963011EDE8865009971AC40C87A1BE4FC + DC05197D1047B086052908D15D10BA200D212AA3DFB2394D6574EC47A621443D + 1003882C08F12B7A21B90BA22E48B661298AA51710710F4447090B3EB48E577D + 904CC387EC477D0B3E0A3AB49AD7712C17D2F794D199011F8E6201212EA30321 + 8962315B06E4CF8A6201211A00E496FB0A3E0C213621F7177C148800210523D7 + DD120B320C081052D0B15810A60184DB20C08760445D104064BB900E80B40551 + 07A42D48669F323A064436A4802326C45D90744204201EC187A25840C853BE8A + 4E0CAB4D487A20BA8EAE710C6B1B42D88415F8180684185686081630D2312C75 + 411CBF72211DF8700C4B0022083188ECDD88E5081626A46358051E5D4667D407 + 2908117C248625F8A8BF653FBA903EFB20C0C70B7441D4078909E9C3848008D1 + AB40C8E884083E0A46BA075233225829A48F8D581C265417242300C188A48CAE + 0E08C709810FC7B0BC198B1856004431AC362036226B1744FFDB19904FD46F07 + 20BBD9CDDF740ED1C880D4DF7A13C5DABA8E3EFA2036216D4014C5FAE62CA46F + ADE4053E28A2C782288AC58B05013E5608D13D108AE87BBA20051DBE900E801C + A3F5BC8A60698860D982B80F4214CBD345F4B196B721A4DE8E6111BFC28210C7 + FA32D31092BB208E64053E34F55DF0C178256FC14801080388183E5C48EF3E08 + 3D906F01220521868F1A2C886C4840E4372EA47FFBD7051D0520DFE9B5BC8CA2 + 580CF071BA635818901A1F273480681A40B021820F60248709818F4302200D21 + 0088A258E72486B5C6B11600610A3814C3A20BC20C08E928960DC834215C48EF + 323AD306C4EF6F8E25864521BDE00313D206648D61A507E2A3841E97D1E76A5E + 7A2018101D26EC323AE66344B16C3FB88EDE1BB1B80BA2227A06FBA1EBE8051D + BA0B8201D9B22044B1E8823886E5285681474F2CC8302105208690828E2D0B12 + 00A9D1362CC187A3586323568DEC470AE91DC37217040BE26D58BD96D75BB16A + 6441BA909E91FD701CAB0D0810E21E4820A40D0820220B9218D60A20571A40FA + 364897D16D42001017D227806043800F2C0853DF3700213621EA820021980F59 + 90DC05612B962024230B6208E92EC8886211C1AA573741980540B48E17034214 + 0BFB911E886358051D804801C78D773DB4B9593052D0C12482A5973E88E6B1CD + 2D89610D0829F0B86DCB823C312C888AE80D20F7B903A2385603887A20D3828C + 2816DBB00A4028A3DFF750C187408437064411ACBCD88F479F1384D005992B79 + 9F0D8010BD02427A2B16AB796B624028A24F08013E5ED8821075410421CB46AC + 820F01484348CDE8826041B01F7929A3B315AB23586D400680083E3C5D48B705 + 9910D2DBB0B492370062F8B00DF1462C47B08090D7B01F0521DB512C4084B5BC + 1EC5AF348190028E6941D2056130208210F740B6A3585EC9FB41A6410408D961 + C8C7FFB70390DDECE6EF31E9814C0BB26EC43AC43D10E257EA830440808F7ABB + 943E57F2D6B08E57106213A295BC8C20C406C41BB17AB01F0D21B120ACE4157C + F41C1D0889094907A40D081BB15C48B701E92856F74104211AC7B0E646AC6941 + 660CCB83015111BD60848D58181000A4BB207D217DDE057114AB2D087D90752B + 963A200D2180872CC8E98B0949174420C23A5EC3870DC899CB7142A0E30C4108 + 26C44709EB5BF6030829E860041FBD92B7BEB30D8BAD58C007168438D6800F0C + 480348C3478108C70965410A3CC61C79BE23580290FA1EF08105610A42808F58 + 100CC804112C48C147BDF4400EE2BB2D08AFA6E0A3376205427A1B56CFB80982 + 01592C884D08712CA2584088616416D2E771C2B50B72C462407C98D010D23741 + 58CD3B6D88E1C31604F8B8D60012087119DD16E4F8FFA98C8EF9A090BE00C88C + 61DD5C0390B096D785F43621A38CDE008205690861041ECB002001912EA2DB86 + 04403A7E7579410726445D1017D22FD0462C22586CC332842882D500A28D58F5 + 1D0051211D101184D001317C0C0029E87014ABA0A3A72D88EE82043C7A7A256F + C307112C660B42B2114B130849117DDD86A5B70BE94088BA2073302163235601 + 08DBB080106FC5720CCB06245BB152466F133201842896ED8701A463583DB61F + B2206358CD4B0CEB195B9014D2BB072200E1323AE6A3A358BA0B1208798C08D6 + 62413020DD07191664CE2CA21780148C3CFE349D908E62750C2B457445B09886 + 105B10C7B06C42D884A51896265D909810C347E6E5D70A3C781DC11A5BB15EA5 + 074204AB01242624D311ACD75E73FCCAF740808F1C26C484F0624132DE8635E3 + 58DA88C58C28D634212EA5278685FDE822BAC0030029E8A86F6FC56AF8501E4B + 266407201FFFDF0E4076B39BBFCB1478D4FB0FFB6D17D2671F0400A977B98CCE + FB996F1C2A00A18CBE5A10D6F28E9B20051D8C0AE9C0470F060408C180C4847C + 2E17D2D742BA2CC80090FA2EF0F07574BA20D80F6258F55D00A24E081644112C + B661C5846812C1AA5716A42FA30B44B0210088AFA40B407E4C14AB0D08F623DF + 0048BDB38C1E03D210320084C384BCC4B0B296B72D886258E983043EF6C77C00 + 208961614014C36A03D22042044B1D100C8867C4B132BA09221B12081180603F + 1A406AB48A3726449D10DF051180148C001FB6207441969B2089618D52FA1E0B + B21E269401A190AE227A8E1366BA03E228560188C61BB1B6EE82603F8E2B0829 + F8181072FCE5C38278239601442032E0A36358B62087D7B70D487DAB0F028404 + 406442AE9609991BB10C217D215D774154482F004917E4C30C4847B11A4058C9 + 7BC2B9D76D4E94093188B08E57312CE04326843706849775BC051FC38000204B + 19DD1032E70C3A21B21F0008F6C310320AE90D215810006458108AE80D213E4E + 384AE97441460C8BB7A3586D3F0220D88FC580D005711FA4C02320A232BAA258 + 8610A2580690F441DA82B0116BCB84603E0C2057D1FFC8CB5574ADE40D805C53 + D0E1323AF683127A00445D900921D71778CC8D58EE81A80F4219BD6358350210 + 7A2002907A333A4C780F5BB002220B7C6C8DE257B30FB246B098711704FB1103 + E21816D7D1D3070980F4462C17D13B86F59C5EC3C7BE00A20E880C482C487A20 + 7ED305594084C38494D1B120F4409E6425EFB3F52E10D2064497D1051F6D42D2 + 01490F4416E4C55736DC02E995BCEA8314748CB5BCB21F19CC071002808CBB20 + E98130000885F411C3E22E4843480C08112CBE653F6C40E68574C7B0E882E82E + 4800A42FA4633FDED63A5E225840C807B38CDE20F20120620079FF2F1F2C4709 + 7BEA5FAF3B08F9D8FF7600B29BDDFC3D0603522F5D90F5386147B0041F3D8210 + 0E12D6AB18560C88D6F2DA80D001F927C1C711E3ED42BA2700D2712C20A40D48 + DECF11C7D23D90828F9890CF1D088C001E36207335AF4706E47BC7398655AF2D + 88EDC7560C6B352059C9DB2664CB80D0034919DD2604F870146B16D2A70561BE + 0180347CD4B7D6F1F25DE0D1D7D129A4DB86044034818F74410420C08760A400 + A4A6BB2063256F0CC8D888151071142B65F43FD88010C7720F843856204431AC + 86108AE8BC3121C3822C00D251AC5890B192B746005243295D16E448C0631A90 + D905B101D1462C991040C4300280F836488108FD8F11C57207640C05F4404803 + 8863588CE1C300525330220869F8C8462C6FC5227E556F17D16B5600993D108F + B76215882C65F4637317E4981C261C003256F2C682083C8010DF05B10501440C + 216D3F460C4BF68328D60A2144B1009019C37231BD5E0C48BDEA8264A6059931 + 2C6FC3EA1E48BD57DC36BA200340460C8B223A83F9F03A5E6FC4BA7B0F84D4AB + 2EC8BE10E2998574430816842385B303C2ABA384B220F72F26A4808342FA2DF5 + CD6D9000C81AC3EA523A10726DEC874D08512C0F00E229F8481FC497D16D4366 + 19BD460032D7F2DE8A0D1180CC18962C0851AC11C7621356C147BD1DC3C282F8 + 306117D26D3F741344F3F4B4208961719C50512CEC0726240072FFC3CFA6908E + FD6813E228165D105F487F7EF3708308F603002192A5327A83C7F39BC7EADB37 + 41DA84D4D00119060410797944B1DC05318CAC31ACB1150B03A29920E2E38440 + 883762E92E0853D0B182C80B6CC65204EBCDCDCBF5EDCBE82EA3CB841478C884 + 740C4B51AC1810C14746F62310A22E48B6613584A8983E01C451AC020F8DBB20 + DD03E92EC8FBBC1DC31A85F42EA3D7BB074076BF8FF76F0720BBD9CDDF610C1F + D80F261012FBA12896E0236F8AE81AA04300120B52E0F199F440D62E489B10DF + 06F1AC37417AE675742C4822589AD9050144741784D903206B1764C4B01A4280 + 8F7A47115D006210195BB1042127063E7A0221632B56C7B02684EC270B02842C + 4574B6612D1BB1DA82782B1600027C4C08612396D7F2DA80F0DA82C48074215D + 11AC0690B30C215D4A17883480B40971FC4AD300820901406443DA8014781C62 + 086115AFD6F11E3EB762E930612CC880900088AFA3D7147CFCA2E083527A97D1 + 6D400C22BF49144B112CBDEE81C882D4EB6D588960E92DF828189965740C48BD + 80C73A6D3F741784013C0C1FDE865503800444B48E1703027CC882A407220871 + 1F0408D9BE8CEE218AA5CBE8C4B102215D4697050980CC2E882D8856F20E0099 + DBB078D986B57D13A45E4148A2586D407AD205990684C94D90BCD88F2EA5DB82 + 147CA490AE285622588E611584C882D005C1801840461764989044B156130278 + 288275972D0836A4A0C32042272471AC51460F78C484CCB5BC0611A62FA3D303 + 5117A4060871172465F4004897D1D728565F45770CAB868D580B805044A71322 + 0322F80046008F054288621574D007E9C3846CC4C282F07D3B26A4C0433022F0 + F0001F63256FBD2EA3F74ADE19C55A6F82B8900E8C603F80115B10434800A4A7 + 2D480148BFEB4A5EC5AF80105910E0C351AC476342E64A5E608475BC0D214B0F + 447741EA053AE881D0072182D50052DF0D1F8A633578D4F485F45EC9FBEC4B81 + 90828F86908E62B90B921E48BD2F2D512C414881C74B35BCBA0DA229F0A879A5 + 21A4BEB98E2E1B52E0D11BB1DE8C0D6908D13ADE00081BB1BC15CBF0A12856BD + DD05792F5D90B992B7670288E0A3A3583B00F944FC7600B29BDDFC3D47001208 + A10B522F3D907F8CFDF854BFC4AEBE76B85F41882358EE821C660B421C2B5D90 + CFD64B040B08611D2FF031E3581340DA84A8944EFCAA46374198BE0B82FD208E + 55EF28A3F30A400A3A0421732396A258051E4C03C87F021E8B09F9EF02927DBA + 207D98909B201A2CC8DC84B502882D0811AC7A05208961298A3501A4B761B994 + EEB5BCB38C1E13D2101210F9CE6F0C1F1C27D475F4EE81C480CC95BC99011ED3 + 848CCBE8CB5D901F6A13964D880D8841E4A74088EE8118446C420A420420BD0D + CB7D90F526886358051D238A953E485B10818801A421646B35AF225880482C48 + EC072604F0D82AA46B252F65F48E62012006116FC52286C53ADE0610E0639A10 + C1C7E88160407294B04765F48E610122D76CFE047C10C1D2EBE918D6318960E9 + 2DF8F0700B0403C2DC5000D2A5F41B6C4000112CC8F936212EA60742B411CB00 + 420CCB85F4EE82AC105200027C0442B02014D11B403C8960F1021F970740D205 + 11800842B6635802917D00C417D2E9825C780D6F2C480C88E083013AD405A103 + E232BAB662C97E60410C20BD925731AC1E4108168408D6B4201AF5400222F56A + 2DAFC0C343046B76411E3288A4907EC36D0F6FAE277E75BBFB207BEF82ACE001 + 88B401D1462C5E4C88E08318960164F440328A642586D516A401641A10434847 + B05C460740DA84147C084488613D650B12081920A21816066489616141885F01 + 224088CC0771AC828EC76D40BC96B76084F89500A4C02300027CF0EA2648A258 + 9EC57E6042BA0BF23C16A4A358D9882508491F24F60310218ED5A5745910ECC7 + 28A4176C248A350F136243009058100C484D1F26C480781B56202416C4F6C306 + E4F5828E8E61BDF5264574F74046146B8961B509910559BA200D20C4B0D632FA + BC09F24100A42619AC1D877C7C7F3B00D9CD6EFE4EF30F5FAEE17B01102EA30F + 00D9EFB00122F43F6619DD10E2710F64ACE42DE850044B10120BB2FF11051DF5 + B78AE8358961719C5006841E48CDE70A44D68D588A60011E8008C0B105210190 + F440F4FEC010A275BCDF8FFDF8C1F19BBE09A222BABE4FD87C896D58F43F0421 + F4406AB01FE9810840F4D62CEB7835BD8E37112CBD2B80D4B08E57A574404485 + 74AEA2BB0F3236623580B00D2B2002807C5B00120B2203E2FB20BA0792AD58DF + 053C30218110C0638D62792356EC475ED98F40880AE9EA84183C7E1C00590BE9 + B220051E13423A8A757E8187BB2044B0B496376F17D26542008FA32E4E0CABE6 + 18C651AC11C34A1F642DA313C372213D3D907ADB828C1E08912C01C8650520F5 + AA8CCE184074255D0709032074423A865573642044C33D10E00303A237312CE2 + 58DD01E98D58DD058905E922FAB1051DC79EE3F5BCACE4EDBB208C7B2000C8EC + 81F4BB6EC3F261C2A50B2200A9B700C465742024DBB0808F00886C081D90DE88 + 25F8A8B72FA3D76825EFB020F44012C35214CB068495BC5ACB9B323A0072BE36 + 6201223580C7551DBFF25CA808D632C3800021DD037127641B40F2D2FF60E882 + 686241800F4C8862588110265D100C083741DC077111DD9BB11E721C2B264406 + 8448960C8867F440460C8B227A83487741FCDE8609611B16C5742C08A3F815F6 + C31BB1B663584B0F8438160092F79EFB810F0C484F018720841E0871AC6734BD + 92B7A35873256F4F4308E0F1ACE12331AC8730202AA3DB866C77413C6B146B6E + C3228AE51703B216D2192EA28F75BCE983CC95BC362063800FD990D705202EA3 + FB1E88BB20F537E64326647641886231F440801097D17D9C705C460F80BC1E08 + D14D904088FB2044B13A86B5AF01690BE232FAFB8E62A90B6200218635E25805 + 1EC0887A2044B10A3E184C0823F2900AE1F5B3FB7DBC7E3B00D9CD6EFE4E23F8 + A8F9C7ACE5A503A28D5844AF8605E9381636A480A341640088FB20631BD61AC5 + 620A427C191D0859E0A3BB201941C8B02047EB750CABED474C484F41876358D9 + 8885098901F9E2F7D6189667DC04217AB50CF0F15F3FE1B505218EE59B20C4AF + 6C42B4116BEB26C889A307D210028050486F00B10529006903027C60410A445C + 465F0AE9EA82AC85F41890BC02916141B64D883662FDAEC003080988347C8C8D + 588A60B90F42291D13A2BB2029A58F8D58E984FCA48063AB0B52F3F311C5B205 + 711C2B512C0044B3C00706A4635802900B15C1127CB401098010C3D2DBF0A1D5 + BC362007D7DBA57400C453F051AF6258B11F7F043C8862A9FF71B9ED47604437 + 410090936D41B656F26A8862194064426A7C1D7D3B86A528D68861D577C7B0B0 + 1F44B1CEBABE00A4A00308193D104C4841C879DD03690B72BD0BE97B0064AF09 + 195D1001C84D9BD3EBFB742EA40B406EDD32209E0048F741800F4084EE073052 + 00C29C7359418800C431ACF358C9BB18900BD405A96FB6600540B6625857DFBD + 7D17E41A6FC352140B08491FA43762D982F85D2DC8E55C471F10322DC87A9890 + 527A0388225879077C60430680248235BA200FE73648C1477D7717E4C6028E61 + 41544AAF77B1209A820EF54112C1A2FFD11BB11A42A6013188E84AFAB2114B10 + D22002841478F471C236202AA263406241000F6FC52A0079D47741560BD25BB1 + DC0131848C4D5818909890BE8E2E13F2E48B13400421F5F758CBDB106200F146 + 2C0AE9C0478108F12B6C882C08105230F242DB104348AFE6ED95BCBCBD0D6BC4 + B014C16202228A5FE52AFA302136204047C7B08605E9890D5921C416C426640B + 4062401A4466193D712C81480C88A2582EA4CB82A8881E0B420C0B0B82F9501F + C4ECD11CB2FB7D3C7F3B00D9CD6EFEAE73C8560C6B40C86A42808FFD0E731704 + 08013A1A42BE7E78BDD88F061183874C4800C48574AEA4CF181636848D588008 + 2574ADE505403020EA83002081107540786341787B1316F643E3B5BCEA82C482 + E838A1E0C333AEA30B3E12C5E27B2B86057CB4FD98F0D15D908E604D1372CA84 + 90FAE626C80A218A61614164426C40661FC4066446B102215890831843C828A2 + 0F0BE21E88004416C4266400880CC89CD105612B967A20004862589A028F9890 + 8E61FD0C1B8205218605848C1E4806F0582C888AE8F56A8010C108B741022047 + 011E06902EA3AB907EEC453620CB5A5E15D1D303D136ACC4AF5C46B701B10549 + 197D8961E91DF0D1E0110B82014921DDF0E1E384BD110BF0E808D69FDA80D41C + 1D10E13688E0A32044312C4645742004FB51EF39F441022180870C48DE8E6201 + 2005227D17441BB12EA817FB51DFEE811840DC056908E92856C1474F4C882C08 + F0D173E92DD3822C8574C5B0544AEF281606C41032BA20572D200284E4C580A8 + 1382FD50242B26E45A8F2D88FB1F32208A63613F6617440052430764C4B0E880 + 2C31AC2B6E9E7D1075410A42B0205715743484D880002158908E61D97CC87EDC + 1108C975744C081D9006901BDA80D43706446B79818F11C702400A3C7A00104C + 081022F8C07E04421603A2CBE88962B5092182755741872084F8D50220F73C68 + F8681091057984627A9B108F56F26A721724060413421F4451AC40C8DE2E887A + 208A6219421EABD716A4804326C4112C0AE9860F5B90B61F2EA4D30901420C20 + C4B1E65D102C8801C4166496D1051F581045B13A8285FDC860400011BDC04743 + 08712C43880D0865742EA263417A2B56C1076FAFE565B411CB2644104211BD21 + 04E8D0BB1DC39A51AC693FBC0DEB7D1D2794FD28F8185D105EA258FC0F20D13F + 657718F271FCED006437BBF93BCE8860C982001E13406C420C208E6119400421 + 8A621DFEA15D105B107741BC11AB8023512C6FC3C286147C08406241D203317C + B88CFEB96F1F3DA25806907AB72C086F4C88A25829A3D307A190BEDC04510F04 + 0342191D1029F0606617645A1075411610F165F4000853E061134209DD16C45D + 9036201DC75A6258D88FB620F5021FBE0DD21072FA66FF028F037E0B88147824 + 8ED5312C56F3022044B0BC96D73741860529F8F8DE7298F007BFDF0321D88FEE + 83F46D90439909212AA5634114BFCA5A5E410800420CCB16C406245D901ADD04 + 1971AC39D3842C06441092523A16441D909AE35C46DF672D6FECC758C90B8008 + 481CC71A3D9013801003C85A4607403A8A057CF83580A80BA228D684101B10E0 + C320320CC899051F7A8962011FD7398E951896D6F20A426C41B6EE821480A80F + 52D0314D08E36D585B16849B20E32EC802201719425C4ABFC51BB1021F021081 + 47BD5AC75B73C96DEA829C89015119BD0184185640845710D200C2FB61870901 + 90D9052186E532BA67ED815C7C0D11AC751B16160410711C4B26E4060CC86241 + 6E3490782316330D88A3588E63C982DC6C03A242FA9807B596B757F2B2154B5D + 904088E0432F77410090B50BC2184614C1C2880440886169885F1584B8906E08 + D15ADE00888AE88B0969F8900529F8F0DB0062083188AC16A45E3A2018900291 + 0FED81D4B09AB7CBE87D13A43B200D20EE822C00B26CC452142BF0D1D7D1B11F + 069069401CC3AAFFDE1002743C5BF0A16F9B0F4DC187EF8204400A3A3C581062 + 58E982F46D90DE8615182186D57D103A20EB36ACAD2856CAE8DD07218AC5FB6A + 41872DC80220BCBA8C6E00E136886358F506420C1FF5EA2608F051A31EC87B06 + 90F7F92EF0E89109097CC8847835AF1258C382EC20E4E3F6DB01C86E76F3F79E + 3620BD92F72BDE84D55BB1B496778008E05130F2212B793F5DD031E358D38474 + 0C6B0008912C0C886258D98AD5657400443D1020A48612BA2CC832051F2EA27B + BE70A063580D205B5D900088E687C72686E52E884D48C347F7413CEA82248E65 + FB51F393F5268801643FE023F3F59FED5B469701A96F7A203D2EA2D3054914AB + 20647F5D45B7059985F48E6005447EC7B701648D6069335641071D10F5400A46 + 0C2044B198FA0E801842EA3B65F45ECB2B10A9E11E08312C0004FBD14317C487 + 0999820F3A2180C711178E32BA2D487D0320051FE32E488DB661751F0400D1B0 + 112B10A2B101711784185641080052D3008205D14D90020F45B10220EE84B00D + ABC003F8108000223E4C3862586D42008FC4B1067C7C48197D98901890DE8425 + 0819312CC3875F6FC4D24ADEBCD382A40B027CF012C36A039217F818312C998F + 0C7D9074403020CC561764C4B0560B4214CB00C22D104148C1066B7901917305 + 21400710422F04035200B27513A4C003001184D4288295327AC1872CC8D50008 + 06645A90B192F7FA6942BA0BE2A3840690BE0BA22E089BB000106DC42AE8A877 + 14D215C5720C4B2B7915C5327CB88CEE18D675B73E344C086574DB104C8841C4 + 1BB1BC0D4BAB78019018104CC8DE423A1684CBE80690F441DA820021F761420C + 211A00A467F4410C222386F50000B26CC3D27404AB410403620899112CCF8090 + C79E11803CC875744D43882358C0C788610942961E0836A440E4D1820E991081 + 48C3484C886E83D4FB9C6D0800E26D582FEB36085D10CD88612D5D9061407889 + 61BDBE79EEE557D5077117E40D0388EC07F3A67B203220F33ABA20E4B5B7B211 + AB8023D300D27D90711384690342219D4816F0B1AEE4158CC4840020C0886258 + B12080C8FB05225D484F0CEB3D4C081D9094D18963F54C08D9FD3E4EBF1D80EC + 66377FEFF9EA1F661F0400C9CC28D69C4F011ED890869044B1DA84E830A100C4 + 332358DD059931AC6140BE5D1052F0B1BD92B7C04320E2BB202AA20740E646AC + 4088E0A35F4348F7410C1F79818F7A05216D41E8800440F68D61C580F4C48278 + 25EF898A62750F04FBD14709870919112C8FD7F1C6842C312C0DD12BD990F440 + F81B0342217D004897D1D303218A05801CCC00211810E66C9990B19297F78F67 + 157C74072416241032EE82F44ADE6141DA80B888BE759850AF21442022F00044 + B01F94D15D4827828501F9556258BFE652FAB0205EC7DB5D9015407E1F0BF23B + E0A37B2035C37E0022B2208960F52882C53484D4603EEA3D8CF895262042013D + 312C2C88D7F216746838485830320024DBB046193D11AC9A63CFA40362002186 + 650B9228562CC80967177C04405C483788B096775890F36E1284AC3D104148BD + 8A603198908BD3071910C251C236210D21B7A407728B62583620F56A0C20EB56 + 2CF54112C59201E155193D5D10E04300C27417E4EE74418091FA167C786C41D6 + 1E0843FCAA20E48644B1800FB66309408091FB369713BD1280388E250BA22896 + 21E41A9990FB460FE49A5B0A4088640542D886A5BB2018902582D52604101997 + D133B61F7BBA2080C83DF50220010FC347CF7213A4C6F0C15BE0D185F4D81001 + 083D1086F85520C4DBB00A446A1A44D407D15D9014D255467FB6DE670B42384C + 68FBB1B70BE218960BE9EA8000220342EA550FC4F0F108E0110869130278A80F + A21E883761792DEF8B89637933D6384A280B420FE4E5092135820F3663D52B00 + 6908D1BC6E0B52EF8B0C209232BA0F13BE6E03922856C7B0C68142F541000FEE + 823892E58D580C16C45D90BE8C8E0119A5F4C0C70A2014D147140BF8C8B40569 + 03F22EF0911E883662258A3557F2C68408417618F271FAED006437BBF9088C22 + 58BCE9820842F62B20E91816F0A1085662580211C34777427498708190F538E1 + BC8CEE32FADA01611B968BE8CC51C382686440F67441F6C4B00C211E0084E384 + C0C71758C73BCAE80B84001F02106C0811AC0690869044B06A7A1DEF04902EA3 + D7F7CF811000C436A441A42D08C709B11F6B170408B109690BB2672356BDDFC6 + 86D003D9021043480348C7B0BA902E139218D6F7B01F0B80B80762F3017C10BF + 0240D403610090988F9E71217D8590C3CEDFFCB4E0435D900048C7B06441FE54 + 2022FB11F8600D6FAFE2950559008408568D4AE91DC32AE8181022106125AF63 + 58238E952E4847B1B6376201206CC57204EBB0EE8300216D4112C3E22E88A6A3 + 58BD8E5716C41072141052E031A3584008DF0521021147B10422D88F11C30A84 + 740C4B20620B72625B909A931A420220EE82788863C986E8427A6258F512C11A + 1BB10A3CB4150BF0B8E856F740B4098B0816EF6D32205ACB4B142B20221BC200 + 1E29A3FB3ABAC1C3EB785D4A3FBFFEDE820F3A20BA0D520042291D0B1203A22E + 4881C8258A63C5805C5BE0412F84E81531ACC0C7DC88D506E45EF741B4098B71 + 1764EB30A10C48FD2DF82810217E952EC835B7DDAF1EC85A46D75A5E0C08F041 + 19BD0D4820E426C5B11E3184D46B00B10591015920447741FA364866DD88A51E + 488622BADF271DBF0A84B88CFE8400A4A358DD03611B966E820842E88030C087 + 2184EBE85890FB1A3E8863E9DB45F4694296185697D1B11F05227D17645C466F + 0079BAFB208E62093CDA82083EBA0B92511764C2C7D3CFBD3A0124F0312D8863 + 582AA42B8AE5218A3557F31680B409C180D007013814CBC280BC916D588E60B5 + 0979EDB574423A8255F346463741BA909E95BC8A63E53648C7B03480470E137A + 808FA50BB25810414800A49E804861471B901D7F7CAC7E3B00D9CD6E3E0AA308 + 1626C45D1043488147BD0340F6F3462C2004FB2108013C64416C40FA38A120E4 + 1B051EC4B0F6F74D900690719C50107284E0C35D9098909ACF692D2F06A42164 + DA0FBFC7043E0011B6604D005921443D104C48810700D20664F640BA94DE712C + C083B5BC36205F063C14C5CA5142FE063E0A386C42BA908E0DF10C00097C0C1B + 12F07019DD6B793B8675404F01C8FEF440349810DF03E9523A1684A3842EA503 + 1F051D6B1C2B3D90AD3E4841882EA4178000238A6211C3024614BF8A01D98210 + 6C48434897D0DB82F44ADE0CF1ABD103B105C180FC92127A81481F262486057C + 30BFA5940E7C647C197DB120DA8465086113565B9011C5DA0740161352E0C156 + 2CADE4D5387E65F88809218645048BC17EF41B08C17E18440A3C4EF74D109B90 + 1890828EBE0B620B8201890DD98A61193EF4760C8B392F5D1040A4BB20D88FF1 + 3A8A357A20B12006104FC7B114C14A1CAB01040BC251C2B19257AF21C42B7913 + C502427494D087091DC36A00B1F99009A977981045B0EAEDBB206D406441D209 + 910509800022CCB020EE813040C815D71770C880640221B220020F20040B6213 + E2227A1B105B104D01083301641B44BA8CAE1EC8624214C3BAA3E003085114CB + 16C46318711FA4C00318E9189620C406E4B6861045B1B211AB7B200048C1C88C + 621942DA82A807D220D210D27D9040C8FD40C812C5023EEEE73A3A0644D7D163 + 40B2156B44B16A6441860D6900318474195D10B2F4410090C7818F31D9881500 + C184288A55D0D12B79394EA83EC8009057BC0D8B2EC85E00E155142BE0D126A4 + 214426C491AC2EA2F7462CE0C35D9037B723589A77A601511C8BEF77376FA88C + DE1092F8151644EFFB9AEE81F82E884BE858100C08470915C16233960084EF19 + C11A00B223908FD56F0720BBD9CD47654617C4512C6FC5023EBA8C7E48C10763 + 0362106900E175178432FA67000F19903E4C681322F81080D4F7014C7A20FB03 + 1E31218190D9053180A80F72A02D8827D7D1978D588A61C97E306CC5B205D92E + A4D30529E00044B01F7C031F3DACE5ADD98A62C57E288295E94D583D5F2B18F9 + 3AC375F4BE90BE58906F14807C430062101951AC8E61C980B813820519008209 + 511F84085641C6B0202EA5BB0F421CCBE342BA37627D3F3D105E6FC34A076400 + 086FC107B34088FB200521BA8C8EFD700CCB85F4804781085D10E063DF95BC1D + C10246BA0B621BF2EBB50B121372D0721D9DD185F4828E836AB4925773993662 + 318A60C984146CD408441A42C65D10E0C33D105B10DB8F9E2D08510F843816D0 + B1989002114C08A5F45EC52BFB1110A103620312006903A23856CD391DC56A00 + C184B405A96F0C4817D215C302460A3E12C5EA423AF68338D669B1205D46F775 + F4581045B13021F44102206D40D205B101E1C57E388645176414D1353E4CE81E + 48A601A42DC81AC5023802212EA4534607420C208E61DD9D2856814720445D10 + 1D285C6258371590A80F924E8820A401A4E0430052DF74411A40741BC431AC61 + 410A3814C52AE8D066AC028ED103D13ADE820F199019C35219FDCE14D16BE880 + 8C18D60A201C265C2C481F245C4D487740D88A65F828F010806041789FAA3711 + AC8610E2573120F712C15A212451AC09218E63AD8709D76D583D362005218FBF + E0171BF2540AE9828F0292C582CC1856010816840E88C023F0B15A90E70B403A + 8EB515C36A13B26CC3023E1A4054449F00B2DD05612DEFEB2EA4EB28E11BB307 + 42FC0A18E928964C081BB1BA0B1208510C2B0644208209611356E04353E0C1FB + 6E2024F12B0F36A4E67DE258BC4047EE82001E3121DC079109A9FF750C4BF8B1 + 63908FCD6F0720BBD9CD476A000F00C451AC19C72AF0E81856BF80884AE9338A + 350E137EE3089B90800810D27D10E063AEE43D220062F86803D2003236620942 + 6A80102C081D102084B7BB20356D40E884740FC41BB13A8A95189640C41644F6 + E387C0478107B346B1961E8857F36242B01F5810DB8FAF76048BF9500B72B201 + 8408568D21E434CD28A26B12C3EA423A0052B39A9011C352197D8960258635AF + A3DB80001E8610DB8F11C74A21DD006213D200C20B84C87EC88078460F447741 + 6C41885FC980F0CD1B03B246B17E0D8C08400012C7B0544AC784681B1685F48B + 0C1FD80F014886ED58C7A78C0E84F00A3E661F44008211118078356F6FC3D24B + 191DF81084D886288E250001440090C4B18090C0C72CA4D307B10571111D0312 + 1889FDA090DE3D90514A9701098400201AE003189916443D908E6211C38A01E1 + 20615B90799870CE3020DD0519160403C2002014D1EBC57C0024C087862E48BD + D9848505F10B8CC4800442E646AC3D71AC828F0B46072416840E48BF051FA38C + 2E0801403C02907A89607521FDF28E61012229A3EB3648034862586D41B620A4 + 4600A221866510690BE2285617D23B8E952E884AE81F022082901E0004FBC100 + 1F7323D630215B105200820959D6F23684084414C17A72B3DE05910D117C6435 + 6F00A457F22A82B5764102222EA407448861652BD683051F7325AF0D485F471F + 31AC86101990004820642B8A952E8857F2E6327A810816E4999890678862A988 + 9E9101611B56E258B11F32202F021E44B1301FF53D00C4EB78E74D105E00040B + 4214CB1684389600840856BA20362118106258793120190C4847B014C35211BD + 5E4108EF84101F276462420A42DE4F2764C6B0EA1DDBB06AF89FDEDDEFE3F2DB + 01C86E76F3911AEE821478248A057C10C30240DA840CF8500C0B00C1841CAE71 + 0F84013E32DFEA690B4224AB8BE8B61F8290828E7FC9B8905E33B661C5840840 + 5C4A5FCBE88A61A50BD297D10780D4F446AC2FC9842492250B72826D8840E404 + 1B91A590CE614262582EA4177C04468609C182347C74299D95BC829000086F4C + 88BA20980F6024160408511F040B42040BF8D0001E86106FC43A7DF35D0610A1 + 905ED0D11BB13A86D551ACBD7741544C2FE020868509D11BFBA15E4897D1653F + FCB2114B5BB1301F0D218A63D51B03F2F3B621E981C88008420A3A0688D88470 + A4B0235874400C20FE267E35A25829A433DC03F18142FA2080480DE0D110220B + 12F8481744B741021F5ACD1BFBD11032EE82083EAE9806A45EF7402684B888BE + 5D469FAB793D5B11ACD5820CF8481704FB51AFBB2098101B106FC55AAFA3DB80 + 9CC28C18562084B7E0E314CAE889617198D020E21EC8E9828F1C28D45A5E47B0 + 644288620123051F675D9E427A81C7D90190D584183EDA82044274199DB97373 + 21431CEB9A3B3717C9844C10E932BA2084181691AC0122B1207A6D3F2E0B7CF0 + 7D05DFB2210521ACE41D45F4C582D45C73F303EA81F475F4B6205AC72B089900 + A26D5829A3AB88DE47098191589006110348BD8047F741160BA2523AF0B1378A + 95DB2044B06EEF18D6B021069051489709013A9EDCDCDB85F46142DA82001F4F + 157C30B620DD0599F0F18C2C88A3584C81C7886065644028A207421A44E880B0 + 0D8B285622588290675E880509840840161352C0A19B20CF6342623FDA802882 + C5CCC384E338614308DFEA7FC4825048073E80118AE8021036613584740FA407 + 13D20684F12A5E4F4C0816A45E774098A507F21600D220B244B1DA7EA40FC246 + 2C0E136AF85614ABBE311FB220F4408860D5DF051E1FD43F68B1203B02F9F8FC + 7600B29BDD7CC4669F323A5D90154014C732887C9A17082920D12A5ED90F0F1D + 9011C51A06E4480DE6433D102631ACF526882EA30B40DC05F93722587C633F96 + 3E0863006903E2D9DE88D5112CC0A341C400E28D58060F366211C19A5D1047B0 + 2680D88210C5C282EC4724ABC063428801C4B741808FBD16847B20C4B00C2202 + 90020F41082FF6E357060F5910999018905EC7ABB7C02351AC8E60ED5DCDEB95 + BCBD15CB104214CB1BB10C21BD96777441B409AB804326E49C7AD303C9514295 + D06541EABF157C8C28564CC8342035580FBA208190B992D7312CE0E3D7034228 + A36341808E19C31A7D10018821A42D08E0310E13B296B7635894D06340A60521 + 8A85F9680801408863F932BA4616A4A04326C4F6C3AB798110FE0E80D4CCC384 + D78EDB20F3423A06A4E0E3CC028FB3AEDFEA8360414EA857E0A159BB2000C8D2 + 050144783B86A5C17C1840D40561801095D10B3CDA7E68B8928E0171215DE091 + 57312CE258EA81001E3DB61FF44046217D808801E43C62588C0C88C765F48290 + 15407A80102CC81AC74A17441072832D48C7B0B409ABE0E3F2D88F3E4EA82816 + 0042FCAAE0C38709E981B0150B031200910929F8D80290820EF5408863F995FD + C0826425AFED470084EF58909BEF7E58E0D11072EB3DF441B211ABFEC684DC9A + 95BC2AA4DFE7B5BC6D413020BCC4B004210F00214FC982DCC30022051B069075 + 35AF2D089D10C1870E1462400C206325EFDA07C17C3480C482AC77411E910D79 + 61F3A84AE9B6208A61A90B52E0C1B70C487741DC03590D88BA20E983782D2F00 + E228967B2013445C46EF1E081D1000A4E6C5DE868501C1882486C5262C461624 + 200280D4FB4A4044DBB0B0208961F940E16242FA282110F21620E21816112C60 + 849B207323D67B3620F5B28A1708E995BCEE81D0072918013E6442800F47B118 + 4308F06100E98D585088186407221FF9DF0E4076B39B8FE018401A420E358460 + 43D88A55D0E19708D61E0B027CF40B7800228A60AD0684ABE80D207EC736ACF4 + 408010B6617DAEDE5146C780083E62420E248AC50B7C148C70981000C95D108E + 136E5B104004F0C83D901F1840BA90EE28D6342013424E9C2FB74004218B0111 + 8C147C0442E881782D6FC148EC0700E2B7404320922EC8AF01911C255C6258C3 + 82287E0580B0112B20F25B00A4A3588963FDBEDE020F0608193D10BD29A13307 + B70559BA208760430C213F195D901C26043E30213A4CC81B035283FD981052F0 + 01883480001E01110148C6108209C956ACC4AF5446278E251B6210A11342195D + 57D13121051EBD0D6B6CC4521F04FB8109F1AB2EC830200D20051C8110010831 + ACB62035732316312C406401100C886C48E25867F636AC6B041E6B176435211C + 281C26443D90EBDC01594706A420446F20041352F02110510CCB712C991045B1 + 0C210D228E620122051E896261416C4202210048DB0F7DA70B028000225710C7 + B20DE9BB20E7B19A17F80042AE9A10E2A38431210D218B01E93E48AFE5D53ADE + 860F6E823480ECD9887519E091A3842EA3F76D9069411836628D9B20829019C1 + D251C2860F6C48204487090321D7B39A778D60A90BE2AD5803460A380C204C01 + 48830716447741B01FB620B7623FD618562064DC05612DEF302019C0A340842E + C88861691DAF214406A46F82C884781BD6DEA384DD05D1462CB661652B1600B2 + 751784B72084F136AC44B0326B199D6D581AE2571DC1023E64400C201DC3EAC3 + 84EE83BC6A1322F86800A9FF06780021EA810021181047B1042205218A610940 + 660CABA358BD92B7D7F1BA8CBE1A10C3C78C6319405E1B3D9098104DBA20B220 + 860F5B9098903E4C480C8B510C0B003188A80F12F09001E1427A6258DA88B582 + C88E403EF2BF1D80EC66371FB5013CEA35801C6A088901F15D90020F46F06100 + 690BA23E0825F4B620B21F13426441800E8148E0A38718565B909A6141022106 + 1043481F281C85F4EE82603FB415ABFE960D990022F8E8B5BC321F3D8048C7B0 + 3C5BD7D16541325ACB7BC2B296771A10599094D16540D205E132FA37EAEF791B + 24002208B101F1B8883E56F2AA887E9AFA2074400E8801A10BF25D9910E08328 + 56BD58905ECB2B0BD20052F0A1281610E22E882D887B20BD96D7C7080D212AA4 + 1774F4CC2E88AFA3BB90EEAD5840485B9011C35A8E130222F326082F37418010 + ECC78C61F536ACDF147C288A25FBD1512CC0C316E4600D112CC7B07A1CC3EAD7 + 51AC6143749010FB31BB20C4B0C610C11280F45CED1188D87EF451426DC3EA1E + 0836445D90B911AB01A4D7F2AA8CBE2786A5CBE8E7DCE01E88ECC76A4112C36A + 0091F97014EBE40BD98C557FD7AB48966258BE09B2AEE43D3D7741D68D58A30B + D2F01100218635AEA303206CC36AF8900149174426C4F60300D10B7CE8EF0211 + A0A320A40DC885C0870C08C7091DC36A08B10559400408290061D8880584CC2E + C86240DA82248A850DB99A185640A4AFA3770F04F00044AEBFED8105401CC55A + D7F2CE32FA5CC98B01B105A1135283011188001F0523028FC048FA20DE88E552 + 7ADF05D185741D24640A446440E882781BD67A985016A44BE92382D500121352 + E0312104F898164431AC40089BB0D40779E2D9820EFE6E0BC2782DEF5A465717 + E449C0030B1208197D90B9114B312CBDC4B0F6B520AB01D136AC5ECB0B780C03 + D20052D0A12EC89C175F2920E92E4841471B903E4C48144B71AC577B1DEF9BF3 + 2821F02100A9EF37DFD23ADEB58CFE465F470742DE7A6FCB8400227BEF822886 + 45242BE0E17104EB3DC00310E9EBE80D21C087B258FE07EE0E413EDABF1D80EC + 66371FC1D161C20FB1200690FA5B16C4312CDF04C17C002179E981681B564CC8 + 801083C7B80BC20B906040FA2EC83020470E00013E14C762021F3220DA846500 + E918D65617640B427217443072FC8862E91E080042140B030284147CFC970CC8 + 84105F46277EB50C6574410885742C484C88A25880886358C0C7370321EE81F0 + 1A401895D131212B84686240643F0A447A1D2F33366105403A86C56A5EC5B06C + 4288607D4F65745B1083C8BA0D8B317C084016F8E895BCDA881500E99B206D41 + D61896B661D5FBCB23B121051D8190EE818CDB2029A2BB0F52730C530012F3D1 + 1604F89885F4069118904C6FC492FD208A251352E0A12896EF82084202203E4C + 087C10C3620C21EB46AC69416C425C463784F44D90A3D401B95A312CCD8090EB + BC15EB6C97D11DC14A140B00D1147C604006846040808F7E1B4030208961AD06 + 8442FA00909890820E6600C825F5623F0422361FEA82A803E2E932BA8F12760C + 2BA3223AEB78F90640962E88C0A37B20D9860580709CB00D4847B004203121D9 + 86751920027C1474288AC500218A62012313405C449F1BB1644312C5521744F0 + E177DF8D58D382F475740DF0A1285640240082017117A4E0A3FE1E16445D908C + 2024037C288E9555BC40C87D8E6301208A612D3D9061426440DC031184B40559 + 2084289656F23EF4CC665C470F80780C20B2218A60614180108388B6623D66FB + E1323A1D109B1077400C23BA882E039259FA20BD92B74DC8808F74415C4AC782 + A40FF2BC57F21A42DC03995D90D736CFF63A5E665810E0C385F4B6205EC73B63 + 586CC4D256ACD75C489F5BB10C21B62006915ECB3BE258051DBA8E5E100280C8 + 86083CDE51046BDC06097C0844885F0540068408440C20ACE5D5C484300340DA + 80883E7608F251FEED006437BBF9284E8107AFCBE8B990CE61C202104148A258 + 0210602416442604F85021BDA0A327F0A1B5BC00C837DD0571199DE3840D1F8B + 0919F0C16B0011840840FC7E9E6F20E47F04107A20C7CCBB20820FC62664FB32 + 7AD6F2CA82B015CB1644F1AB9E7D0E137604ABBE052198908008F02108711F44 + 06A44726C410320C0811AC06902D08A9510FC443191D13A2F81510D21BB1D207 + F9DEEFCF3080001F3121B221357D17C4374162416A04207F3C37008211A9EF00 + 888F121A44C685F4028FEE830C0059366301216325AF2C880710E92E88366101 + 21C08740C4164433EC47E023E375BC9E069011C5D2D8847825EF15FBF6410090 + 828FD905F1E8423A5BB10A3A7C21FDEA09208047008475BC1A2C48FA20BD0D4B + 3D90B312C71280604362413A862508A9A103220B62086903A2E384E713C1BAA1 + E0A346112C030847097D98B0C04316A421040029F818657462582EA433EE81F8 + 32FA8862D1018909F156AC1C26940529E0C85D90693F62406AC65D90AB0D222A + A25F7577BD6CC4A2904EFCCA103236625D130372EDBD052135C0874CC8BC8AEE + C1800021051F35570A426A021E57C5866C15D2D503E14D1C2B0042046B6B1BD6 + 5A449705F1B8889E38569B10D98F14D2B11FFF0380A807520082F9D8DA860584 + 147830820F5E00A4BEB11F8E6201201EDD052900F171C2589007BD96972816F0 + A1CBE8C382248E85FD28106903B2428863582EA42B8A55E021F878FC0547B0BA + 0B1213C26142ADE4A59CDE10A24D5829A2D73CFECCCBB2203221051D86905704 + 21BA0DF27CBD2F3040481B908E645144EF327A8108F0A10E087D90404817D2DB + 82002029A473170408E928D6D8882513C260400A449608D6EB82901890821015 + D263413A8625081945F477671FE43D9B90EE81F82688FB20EFAB0F525300E2BB + 2000C8DC8805770844F20FDDDDEFA3F9DB01C86E76F3519C0048AFE51D162431 + AC614080116258DA8A8501A9D146AC09210344B020051FEB7574E258FF220B52 + D0512FEB78B5190B081916A4C083C3846CC3521F6446B028A56340FA3A3AF031 + AFA3AF45F41A20E407C7B80B52D0E1EBE8EE8110C9D2462C204416C465F42F33 + 051EFF4DFCAA5E591001884BE9A30FB2C6B062400680A88C1E13A22E48C3077F + AF31AC6D03C20B7CF0ED18964D48DF0491050144808FCC76191D1392391823E2 + 2896EF821478A80F72966358FC9DAD585D467711BDC083327ACDD64ADED584AC + 512CCC47A2588E60D5347C10C52AE01885F45C45771C8B281611ACC4B1644116 + 0839AE675A90011F322059C5ABB1019105510CAB8F12F23700E2185697D1818F + C34E650090D905117CF05DAF6E83002214D00320DE8A5583F908841C0D80248A + 450CEB98B1112B97D1D305997D101B10CAE8279C77A33A215810DD039105A9BF + 81109990828E362144B0EA3D4D512CE25737E91E8862584B0FC4EB780D206B14 + 6B00480C8801A4A00313A20E88EDC739D88F029069406C412E8805399F02BAA2 + 587E7B2396BB200B80B40519512C00C4D3DBB00422BCE981A80BA20816006218 + B9F266A6E0A37B206352442F00D98A62B5052192250362081965F444B1E88060 + 416E52291D13D211AC7A8701490C2B10E2A384356D41801015D281102C080082 + 013180B88C5EDF8961F930E1BC0DA2AD580F3C2D10990042040B0B923896EE81 + 18405C44E740E1D203510CABA65E01884CC8F38A643D24009920F27043082F10 + 8209217EA54E08712CC6F7409827B7FA2058104F1B101F279C312C204400D231 + AC5890751B9636623584044046215DF08101797D5A10404471ACB704205D427F + 85ABE831206323561B104D204400B244B112C352044B106210E132BA4600421C + ABE063F441DA821478BC67FBA11856BDEF177460418A3E0A3EC08F1D827C947F + 3B00D9CD6E3EA2A31EC8B28EB7EF81F4ACDBB0B601840E082604F8384CF0D131 + 2CDF03890561D407D9B6209F1D3D1040E4A89890028F258AA50856BFC0C777DA + 82043E62410C216D40524A177810C53278F4C884FCC885F4B6200088CBE827DA + 80A80B62F0A00BC26ADE06909E01213F6B00218EB5581095D1DB820020A76CBE + F54B5F467721BDA00310098470195D0052D0D126A4BB20829136212382D50002 + 744C13A23E88E257B120F5EA3861C1873A2144B062421A40D618960AE9051E7A + 0B3E345B3D9014D11945B00C20BCBE8CEE1816112CC5B104215890FF0940B01E + DB10F27B59900611A258C0C7022327DA828C327AA663583220275DA91184248E + D56B79E982F4462C41884C88CBE86CC63A4AA5F4C0470044B74114C7327C4C0B + 326F82D880D41478B80F027CF4CC2E4803C89CED2896EF8304407ADA82683020 + 74416A8860298A95B9745A10E66C8E13021F97B20DCBF60308C184781D2F1664 + F6401A42980B001101884D880044105233FA208E63011E0D211DC3C284C88008 + 3E6A6EA86F0C487DDB7EA407821149117D1C260C80F82E083D9000086F0C8800 + 44C5747A208E61F93061625877D4CBE428A1A258B22031217731404841490064 + 14D26B28A3DB84B80BD27741D888A52816112CBA20808756F2C682F062426241 + BA0772F783B98E9E089656F2A60762033221441624712CC1C76242BC0D0B0B92 + 123A301203420C6B40C8938C23585B85F40211191045B1524A7FE60545B0FA40 + E193F57A1B16F1ABF440800F014877416C417AD40769082900F15ADE40886258 + 4CC10706440052D0B10521BC80874144EB78634158CB3B2C08D7D16541DA844C + 087119DD43117D14D255448F09E12648FD0D7CF45D905EC93B3762AD51ACEE82 + 388AB51DC31287EC30E423FCDB01C86E76F3119DFFF3953FD46B00F987028E7E + 470C2B00D2A3A3840D20C008512C0D06A440E49B0C106200F9AC60C4312C1F26 + 0442B66358F441D69B20BA0B220BE218565B9011C3521F040B92185636626D99 + 909A2F00217B0AE95C46D7FBA3E30A4078670CAB3762C98404427A352F512C22 + 588A6201202AA21B40B02032202AA5177430C0476258BA8E4E0C6B9810E0A35E + 0AE81890D8908E62CD4D583121C0072022000984248AA52E48BDD3801480C482 + D00761E8818C4907A45F438867ACE4DD8A61CD2E08E308967B2032210290F363 + 41BC967742C80491012044B20A3E98838E4E19BD2D8860A421645E473FF8F84B + 644034051D5ACBCB083C80905ECBEB2E0865F4BE0B620B620051195D06C4512C + 75414EE332BA0D88D6F26A1CC5128464E675F486907A01102258671670D4B701 + A4FB20B621EE801840041F1DC75A01E47CA2588047C7B10C20A79ECF65F41BB3 + 0D0B131200A9D726A480431012F8C8757445B18860C984A488DE312C75405246 + 67067CC482D49C7F45225889636142B4150B08C180D4F82688E143512C45B03A + 86D500D216C47D90DE8865003184A8947E4381072313522F0684089640C400A2 + 327A6258BE0D922E4881072072DD12C30242460FA4E6066DC47AD89D10CAE84C + A2586B199DA384D890D582740CAB23593220C382B009CB16C41BB1BA0BF2A420 + C4F740EA950DF91F0E130A409E0C84147468156FBA203D0D21D890860F45B1B0 + 1FCF662D6F00A4C6DBB0DC05A188AE55BC196DC31284D880782BD64B8A61CD9B + 208E63613E5C48B7FD50142B630079757641860529E8900531840C0BC22B0BF2 + BAAFA403204B0CABFB208A606140E882083E00918292D7DB84D41B00797DD986 + B55A10F7419877376F04428862D98418444617A44D48C7B0D205D14D104D8387 + 4164DD88E5D91846C08F1D817C247F3B00D9CD6E3ECAF3D53FCC42FA7E4C1B10 + 002430B200880BE9860F45B10A3A30219F2EE89865F4028F9AD1070984782DAF + 23580D21C007160400612DAF21C40022085107C46300C17ED40B80C884043E78 + 5542073C1A440A3E0221DB5D108F3762D980A80BF2A3130B404E74217D0C06C4 + 103262583F7517C4312CE6148D6358D98A952E888F13063E00917A0D208048E0 + 031029F07021BD2004134219BD21645890FA3B06E4BB818F036540808F58105E + CAE8744178895E011F829136200B80F0022144B10E6B136200F136AC8690F3F7 + 9810CC071052D081059101C9362C0DF0E142FA56199D1E08736C4D9B905810F7 + 403A82E5185647B1FE701CF08105E918D6022144B10091F44054460F88782356 + 0149F74064410A3A78570BC241C2DC06E975BC3386D510E228165D9063D507A1 + 03920146E88260408091733B8E1500118C001FACE465BC118B3E88E2580521A7 + C482C880C8821480002314D103211C25D460420090F440DA8410C11A512C45B0 + 6241C61478C886B00D0B0869F8A857A5F40210E0A35E8E1262407C199D95BC59 + CB5BE0411FA47B20000873090052F0713105F402904B0521EE8174115D16A466 + 4088A258F71A3E801075413CB307323B202EA31778DCC23A5E0C882DC8384C18 + 0BE259BA200D208047BF1F0621051EEA84043A6440F26E0148E60EBA200340B0 + 20996CC5720C6B352101910788601942EE611DAF62580520B2211C24B405E9C3 + 84F7298E5500C2EC81106DC312803C3FD6F176291D0BF27041876358B6205D46 + F7462C20A4A083385640C4F74088621570C882001EF51678C884002035E33821 + 3022FB31D7F30A40041FEE82A813C246AC58905148EF2EC85A480F84D8800021 + 6FF926880AE9EE81288A55DFB30B6200E9C384632DAF0C08AFA358EE831488E8 + 3A7AE0431604F8E83700027C8C2E88A3587D17A48BE8D380EC08E4A3F8DB01C8 + 6E76F3119FEE80388A4521BDA0636CC40240FA36480148430806A4EF81C4808C + 2E4841872FA35344EFAD586B142B0664FFC4B0D20519512CC06358101B105B10 + 1B90FF900D31887C4151AC4048A62D88060392990082FD387EF3DFD98835EE82 + FCA4DED80F95D10523C0C70923862500610A3C544657091DF028108909196B79 + D301F9460187ED07512CE0836FE063DE051965741910C0A3261BB1B48A572FF6 + C326641890828FEDE38486908E60094228A4CB820023E982084026888C1856BD + ACE30542BC92D7F031D6F2D6E84A3AD0912E8821A4261644F1AB18903E4CD83D + 10DB1047B1B4116BB5208088E257172F11AC02910210A258EE82001F33867508 + 5D90C4B06614CB0664DD884531BD01A4CBE80290F4405C480F88EC8590C0471B + 101D28248AA538169395BC80C759D7C782B80702846CC5B0041FB620BE0952D0 + C150421F006208612DAFA358361F633020C047A258BA0D027C642D2FF071E625 + B76D7C193D4307A45E56F2DA84D4DF5AC90B80D4642D6F47B03C051BE9818C18 + 16F623EFBA96F7A2820F6DC4BAEE2EC187206419F7403CEA812486057CF471C2 + 2B3121E32EC89F47046BBB900E84603EFC12C35AEF813063256FBD37DCD6EB78 + 5703E262FADC86C5CB657497D25548AFF141C2069140C8E8823C1A0889010978 + 8C1E48625877CA84D0FFA09C3EE16318906142D20501460A3A544A073C96013E + B02144B17C159DE91E48C18636621942C6562C0AE95B31AC028E3D168455BC98 + 908E60312AA20744460F4463F8F0462C03882358CCB2114B003241841E084664 + ED8168808F829019C3A28C5ED0F14AC1C78860B98CBEDE0671040B100142DE5A + 4CC8D203D16BFB211322F820829529F00042FA42BAB66109400A380684783A86 + D55D90BD10B2E38F8FE66F0720BBD9CD477E0ED13BCAE8238615131203620B62 + 03D2DBB00C21051FC4AF068C701DDDD0310AE9FB1FEE75BC31218E60F102211E + D9102C48E0A34104F09001118074146B42C8B807C2AB1E488D2258EE827CE9FB + 59C9DB070A0B3C28A213C59205013E7E7442D6F2C682F44ADE4C5B1062580691 + FA2E00014674981003322C882104FBA11EC82F0A400A3C461FA40D48A66358FB + CB82D005711F8428D65CCB1B08013ED88AD5312CC0A3DE2EA3AB07220B620819 + B7410421051CDA88E56D58820FDD04C182D8840844D205E95921E4E7C382AC5D + 10EE82184000915F1F69F3A195BCEA82C482F00221EA817814C5023C58CB9B18 + 96EE82288E75A9EF82AC1D90C087A7A0A30700013E326D41000FF7406A64417A + D6EBE880083D90860F1F265C37627519BD0BE9EA81F05DE06113D277410A401A + 466240644112BF1A97D103228E6115881474F45D10E654A6FEDBD886150332D6 + F2021F8A61614278311F1342C64A5E4D5B9014D213C132801478D4B40939EFF2 + 3B37E7167800208E61DDE5423AF1AB028F0BAE291001420420051C821000C446 + 8408962EA38F18D6DD29A23B86358E12021F4088EE823044B11A3E6A3A8A1500 + E932BA56F2CA80387E352C487A205D46571784510F844E882144E01103322C88 + 4004F800426C42BA0732BB2029A23780F0B609B9AF57F2F21A441CC38A052186 + F500268495BC1981C71AC59A97D18190D903A184CE183E54467FACBE3B8ED5F7 + 40BA1312134217040851148B0856FDFD88002420120B620879C90052206200C9 + 562C45B01CC3F24D104C48E6F9976540E65D908E61CDD9B2205C471F7D10E0A3 + DE020FDD04D16ADE980F00843716A46F82B092777441001119909A370191860F + F74074213D51AC69413020EF4CF85019DD1BB1860529E0D88E6101226D400C21 + 00888AE88290054070203B08F9C8FD7600B29BDD7C0C46312C2258B220990210 + 6258EA8204442680D4B72C882358DD05F94CC1C767885F258AE51E480C88E257 + DE8AB56D412680083EEA750FA4DE028F8E620D0B722071ACD9035114ABA0836F + CAE87441DC01710FE48B051E1DC5FA2F81080012F8C8B80B327B20CCDC880584 + 10C50A7C741704F0A8F97A7D4F136200F185F4820F0AE9DC04610420B1216D41 + 1600D161C211C5B20511802482E53EC899F54E00510CAB0D8800A40DC802206C + C3A288CEE82A7AEE82A88C6E03220B1213C25ADEAD1816D3512CE0A34104F810 + 8414781C017C3886D55BB1D40301407498D010328F13623F5248CFFC0E08C180 + 083EE6D88400213620DD07690B62FBD12B79D988E5D700E2F136AC0540DA8214 + 7408423021A30B120352E0C16C97D119DB8FA3CF347C1C536F97D18709510FA4 + 3762F1167C3488B4011188741F04137253418801E41445B23A8AE508964CC880 + 0F2C4847B118D6F21A423497C684D007D1604228A33B8AE52EC8EC8334885044 + EF3184D49B32BA8F1232D380CCEBE88E61F92E08F0018814805C6F033256F216 + 8C5CAED5BCB620EE811488AC9BB0009080085D109B904C47B11A4202221DC5F2 + 85F4EE82C484A8905E004221BD608452FA28A337881474D88610C1C280248EA5 + 0BE980886D08E0716BA258BD0D8B32BAD6F1AE2604FB715F418740E4C9DC0431 + 88E830615B901A6FC4C27CA4882E0871191D0B420C8B0896FA20000836041382 + FD8801910529F89885F494D10B46C6614240A4E0631C260C80CCCBE819E0838D + 58CFBEB8D13D100064B5200290577C1D7DCB820021F5BE9895BC4048BDBA8EBE + 7441E626AC9A976C406C426C401CC3F24A5EDD04799DBB2036208E61D5B7BA20 + F4400C20DA882510B10101421CC77AD72F00F2D65246CF562C01488D8F12D664 + 13D628A4CB801842461F44FD8F15408A3C34F957EFEEF791F9ED006437BBF918 + 8C3762B501490C4B7741EAEF3D3741041FC3821490604264416C40B63662F1AA + 0BE212FA8C61D5AC168475BCF52D0B420F0413D216A4FE268AF5793662012100 + 48BD8A6215741844EAEF8210C5B00010E257EA806041EABB8083C180B8940E84 + F830A14D882D88A35806918E60C980F4C8843480D4A890BE44B1F6E9829C6A0B + D210A2289601C4374188619D6E10191604E838ADE0232604F8108470153D2684 + ABE80D210190B19A17F088051965F4FA1E1BB1B01F03408863011D9E86906140 + 0A3C7A5C46DFD78074215DDBB03021C4B08090A32E30842C00B27641FA38E16F + 0B3C98B10D6B00C8A59BDF11C15A00A47B20079F90B5BC277A3A7E35AEA30B3E + 302101902D084917442B79D3099109B97AE983147C289255F0D13D907A7B1B96 + 21040B921856BD034204205ECDEB9B203D8B09A9E126C849DC06098450446F0B + A2CBE8F43FB0201C251480648605B9C551ACC58260400C1F3DD88FFAEF051DF3 + 3021DBB00C1E034064406ED78574467D902EA36F01487A20B120632316366440 + C83D2AA4CB84740F448309490FE4BA828FEB819119C7EA32BA8F131688603F64 + 413A8A650B22F850197D2F8060421A407C1DDD3D105B10DD04E90BE9051D37DC + 6508B9E90EC0235D90BB010FC3870BE98F4D1BD231ACBE8C8EFD003E6AD88CD5 + 3D1095D1FB323A10220B9218965E0CC876148B51048BD5BC808720A4A0A34706 + C4164451AC989019C3C2844C03F2F0E3CFA60FB21DC37AF8A98210ADE4CD4D90 + C04747B13C8E5FF5388635BB204F0742BC116B4248F741FA26883761157CBCE4 + 999BB0801097D10D228690B50B620BC2562CA2580611ECC7BC090284043E1A40 + 3020F5AD6D58C4AF801145B06C41460F0403C2C882183C384CF82EDF80C76241 + 14C7EA0896CAE81FA88C2E03028874142BFFDB41C847EBB70390DDECE6E33205 + 1FE32821DBB1808F0291FFCBAC512C7540BA0712F8E026487DAF008201E942BA + 63584B04AB2710322C083D100C88EE82B894AE9B20DF2D08690B72A047F12B75 + 400C215F004212C5E21EC8EC814C0332D6F1326CC3024214C36A0BC261422258 + 7441961E488DAFA2278655F3B59F9EBCF96A36620944042005220B80B405E126 + 08FD8F01202B8868624330219A0211C5B06C403021DA86250B4214AB80A36358 + BF3B6B818F7A818FDF9FBDF981E25874411600E928169D90C4B0BA07A2691009 + 84FC44E6830BE933863541C45BB11A3E64401A401A42FE5410C256AC828E0121 + 804783481B10FA208A62AD97D10D216CC53A98B74610A238960D48438837622D + 0072A20DC8D6462C45B1DA8074217D96D165436441D63816001213D2312C2258 + DA8AD5F0E148166574C0C3009249044B037CD4EB75BC1DC58A05097CC8840021 + 988F74417A5C462FF0C8B88C6E1BE2BB20BE8C3EE258EA81C480001E89629D75 + F912C5A290DE1D10FE26860588C4804C08A9B72144F0916D58820F408418566E + 830C0B52F0316258D382A807C2562C0D51AC75256F7D633F7AB02123863521C4 + 3741EE5707843E08F0411784CD588E633DA4317C3C28F860252F26A4D7F2761F + 84527A03C8B80B12089911AC470C205B97D1011040A4E0E3DE27963E484D4107 + D37D1020040B2210C94ADE51486F0392289657F2D207890929F09800E2B101A1 + 17028010C1DA7398F00900C416443664C4B00A3E0A46BA0FB2654258C95BE3EB + E8CC021F7965423020820F225844B1E87ED48B05290819470999029067DB802C + 16C4B39810BEF7B12081102C48FA20FB1C261484388AC551C22EA4EB26886258 + BE09F2C6021F1E6C4881479B10410830922E4800C411AC7A13C17A9F4D58051E + 6D416442408F0121FC7614F251F9ED006437BBF9184D97D1F78D6251465F00A4 + CBE8F502235ACBABC3848610038821040051119DEF82903E4EE80856BF6B0C6B + 19191060A4A08342BAEC877B21BE0B027C4C10511FA4A0E33F6A886119428E1B + B741BEA4F7F8ADBB20B61F0D212EA40B4000110024D7D17BBE2A10B101014264 + 40EA5B5D90828EADB5BC8A621578D44B17843816E06110C96142DE44B106808C + 1E8801A44BE9A30BD210D28311117C1844644012C56A08F901470905218E6201 + 21BD114B57D10111C187CBE886907A870D29E8D8DB07590C0800F22B6024312C + 47B17298308574A063744114C39AE36D588C01649F9B200290CCE8825CAA0E88 + 3762B907D236A4A358870122A75C2E0851297D6F1C0BF0900D490C0BF090FD98 + 712CC1476694D165406C4158C73B0AE9051E0C4574DF059911ACB6200D2127E9 + 1B03420724066481108108D60300E91856BD0342009061407C1744E6031B9232 + BAFB20BC06102C884AE99703218E61A98CAE291011840020B76FCEAFB763585E + C94B1724104214AB0D8800C416E4E26BB90D62F098537F0B3E3C97A98C8E0131 + 80D882B80FA232BAE02371AC020FF7411CC3EAAD581DC1D23D10460664E98228 + 823501442B7963427413E443BB20DB1032D6F2167CF400211DC372213D36A401 + 44631B328AE8F7156C740CAB00C4D7D19F1288F45A5E01C883B11FDA886500D1 + 71422004F3F1307741267CECB3969711807864418861D53C9A1E88A2584047BA + 2032215C451F06A4FEEE75BCCCD32F6F9EA003C277BDBD9217FBE11856208477 + ACE4CD001F0111014843C85A442F18B101B1FD00427A25EF0A212AA4B38A5753 + 00C2714201C89BB220EB4D1047B18861316D403C8E60D97EA8941E0079872BE9 + F512C7C282BC33CAE880C7B420BE8CEE6F5B90C4B1EA1FBC8D20BBDF47E3B703 + 90DDECE66332FFA786C3845AC9DB26A42358F57298D0C7090FD3347CCCAD588B + 01E16513564147C7B1E883683A8E058804403852E86D581E03480AE98CEC87BB + 20828F1C26FC77C02320D24709E7AC06A4DE020E5910810817D299C4B0EA157C + D47C5916A4210413028800211344BA0FD2E33856C107262406041B6200C9362C + 95D10B440090FADB312C1B109B905810814881073D105EC0634088E143774130 + 206D413A8255C0F1BDDF9F6D0B0278084266199D1E88AEA2CB823086901F336D + 4204234088C163F641CE57140BF0B001A957168438D6B420BD92F7978960692B + 1610221332EF8230EE82AC1042293D0644F7400C21BFD73D9009201C275CB761 + 0D00D13ADE028D5C47D7248A250BC2043C7C94D0E071F86901101D25DC0B22F4 + 40D62E08430784A96F81870D888BE87D17241BB1D40349044BDBB00C21DE8AC5 + 1488083EE64A5E6DC34A148B170011880C08C95C5C3022003188CCC384C00730 + 6208D161428D41443D903D51AC0610C1470D312CBA2000C8B88CAEF8152062FB + A12BE957DF1D03920856BD970844560BC2B0056B5F08E92EC8E5B9904EFCEA0A + C06381902EA3AF312C41882EA46714C37AD00384686604CB3D9019C302408862 + 3584682BD69D0048B661011FBA90EE69033256F262413020357D134410A242FA + E31B15D1F51A406C41E87F00210D1E818F6140E60022F7012300C8C3C0474C08 + F0018460420420CFE9550C8B2E488D3661054458CB8BFDE038611B90D105A957 + 8709759CB0E04326A48BE829A367B01FB2202AA4B309CB00D24314EB59D910E0 + 030B027C603FE64C0049172451AC171901482258CC00904C43C80091B7741744 + 5BB11845B10C1F5D48970959A258F3264862580211C3880C8862580D2036211D + C3DA0B22589011C51288D4BF78F5FF76BF8FCA6F0720BBD9CDC76D6240860501 + 3AD40971170403B2F640E6462C47B1E64D90C3D401F14ADE2352462FF8A0908E + 01619618D6BFB08A9728D602202AA20F0BC260428E0984D42C7741660CAB61A4 + A02310420F64C007DBB00010A63761F1268E2503C24D907ADD07A107E238D6E8 + 836040B8074214EB67CB5D1095D21703F2CBFA1680B015CBE66394D17F7D8AC0 + C3657483887B20400800725ACD1902910121F57A2B1606C410F23DADE5ADF97D + 47B10C22DF3FF84C45B10C2218909E73F48E32FA9E18163D9036211C2694FD38 + D4F0C165F4613F042189618D28D6855B85741B10BF5B51AC00882DC8455EC5DB + 73ECC50210B661792D6F0E131674F4D882B88CCEF4362CEE81CCE384D9848509 + 29F0D0462CAEA4CB82F83AFA612AA33357B90B0288604400107541160B923EC8 + 5CC79BB780E36800644F0F44512CC0832E4836628D227A0613A29B2000C8FF68 + 4112C52AF860231623F8500F84B9256B7903200B84388A8501D9B6208E6111C1 + 9A857447B0E8811478F00E0B52E001800842301F01900121776D2EAA6F45B0D2 + 0511843480083EE6E83A3A7D908690021019103662C9846040EADD63411A4028 + A51B420C228A63011FAB055921A4014410D23664B120773CE2DB206D4004212E + A3EF6B41E65D105B908610774186011178184214C1D24B04ABBE1FE005440C20 + 632356C30711AC3621051D8CEE82247EB537864504CB005220326E82304B042B + B396D1810F99104004EB21009926640B4014BFEAB101A113E20E485B90B50B52 + 130BD20644EB781700197D9098104108DBB0B4116BAEE67517C410A24D58DA86 + E552FA6BF5AA8C5EC34A5E9B9002909A3718C18741A42FA31B40885F011EEF18 + 3E042018102258D81097D17B28A1731DDD31AC0C10F2970F643FDA84388A55BF + 1D877C247E3B00D9CD6E3E7653F0810959B7622586B57723D6D886F50D62585E + C93B6E836435EF88622D00A2CBE8322186907113A4635880C85619FDE8C0474D + C1475F47A70BE228966358DC0591FDA090AE89FD900501400A3EBEDF6B799718 + 96CAE88960054246146B810FCD30200111D90FC3870F1312C33A296574C7B0B4 + 92370644A5F440886258829000C81AC3DAEA8100208610DF06992644F0810901 + 3C74A0B057F2D209E92816F6A3E79C5148177CB096573684F5BC8690D10589FD + F8C9E1C4B10A4230217440740FE4026DC5F285F45890230A40B66258051C7B2D + 488D36621D55F0210BE26D580D1FBCDA88354C4817D13121019013EAC57CB019 + 8B57101210490F84381626C410028078E881F83AFA55238635CBE818902B6704 + 0B0B2200C17E4C0839EACCFA063EDA80E4360851AC3E4CD83D9063032027148C + 8C9B2002911B14BFEA1EC87A17A4CBE81A01083092327A5E5B90D903991BB132 + C380F855148BF85547B1EA157C0844B020F54D1F249D105D484F17C410E23E88 + 8AE88A61F932FA80100C48A67B202EA3D75C9F18568D8AE880880C8857F3F661 + 42DF065901A4BE6FE6260810924D5844B00220EE82A4907E9BBB200088E1A3DE + 828FEB54482FF8A8B961C4B13C00C8F66142C6312C2044DBB0EE2900013ADA82 + D4ACDBB08010D98FFB30211342E656AC27470F449332BAFB20051E4B195D7741 + 041F3620BC5AC9DB312CBDB98CDE2664896375195D10F2D8F3B121810FDE27EB + 6F01C88410C7B01843C80010CD2CA2AF10D200D2512CEC873B2180C7CBDA86D5 + 068418D628A403209A828E3E4CB8173E64417215BDE73562587E653FD88695B1 + FD2086556F6FC3623A8285112186B5B7903EA25899362005212AA5031FBCB21F + 40488FE1C337411CC352044B830401427604F251F8ED006437BBF9984D6FC31A + 7741EAD53ADEE538A1E0A35F004471AC1810C5B0EA5B007284A258DA86C5D4B7 + 0D0810B22786D5B315C33284B4053180C48460410091820E03C8EC82347CB401 + 1951AC58902FA607F29F8A61C584FCD8364465740D37411608D913C1FA0A1052 + DF02100CC8B8900E884C0BE2B5BC6CC372244B6B7901909A6FF1B6059109B101 + 910591016900612B9601E4BB9890828E5ECBBBDD05C18018405C4A9F1BB17C98 + D0512C5D479705E130610C48C7B01A3EB021B220EE8200205EC76B0BA295BC0B + 80ACDBB06C4000107A20061143880D884C080684385601C7E88314808CBB207B + 63587BA2588008E0A1423AE6832E48C1C71F301E8A635D2108191BB1EA157C9C + 12109105E11E480C88663B82E5B926109228D61989620540E88160400C20868F + ED8D589810D6F212C54A0F2451ACF5268801C451AC130B387A25AF7A20052436 + 208E618D08D63021B620C4B164402EEA95BC8B0519512C47B0F46A35AF23588E + 635146CFB0094B6308D161420188E1C3712CE0E36E83087D108D0D8841A4A063 + 8961093CAEB5011931AC820E5E36627521FDF2C4B0F6B120ACE4BD65BB90DE7D + 101D26D4603FFCBA0BF2D0721B24512C0D112CBF80872184327ABDF4401A40B0 + 2140C8DD59C91B032208E10542EE01426242042306104D9B10F5408010F741BC + 92770110625899DE8A45295DEB78078434882CF0017400208F7B1E7A14F89816 + A48F12EE6B417C13C45D90860F8C48C106108201D13B21C42B797B2356CF848F + B111AB0BE9828FCC4B36201C241C166459CB6B1079C3932EC88863357C8C0816 + 25F4B6203D3620238A250BB244B1BA0BD271AC40884D0816841703F26E2258C4 + B10011774178B120A3905EE03163581B4348BD8D1F3B04F9FBFF7600B29BDD7C + 0CA7D7F2FA32BAE3582AA22B8675488147BD8A6151444F195DDBB0325D468F01 + F14A5E0E13C6800842302029A40F08C184001D470EF8E871172491AC8210DD03 + 0980B092B701A42D888AE859CDBB0D2135980F2CC88F384E6803E2423ADFD80F + 2C48BA2040882C8821A48F128E95BCB12014D1051F3221995F043AC6D8828C18 + 1616643120DF023EBA0BD2003220C4E32816B740624246048B4904ABE08348D6 + F73A8615F8E8B7376275299DD15142006444B0CE7111BD6358CCE1BD118BEF8E + 61D97E288E350004E800423C5ECBDB93181623103184F82E488107064465F41A + 45B0B88CEE28960DC87A9C3000027CB401491F844D585D44178430828F3620B6 + 1FE3286122583E4C58005230328E12CA84603F0C218E5FE52DF8D05A5E4C08F6 + 436FC0E3ACEB37C71678302EA44F08393136E444604410E20816AF22584C0188 + BA200C264406C43D90D10519112C7F731304FB2100C96A5E19102004FB2103B2 + 8048DB0FDE2B98DB46040B08A1883E2CC81577B90B322C08512C2C4801C73021 + B1212AA3DB848C3856CD58C95BD071A9D6F0021F18908CAEA3DB808CFB2000C8 + 0A21020F9B90DE88E56D58F5063EFAED2EC8384E2803F26020C400323B200D1F + 06119B901A6258829082927B1DC77219DD2B79A709491FA4A063ACE45D2D487D + D307B913FB21F848213D7D90868F11C502441A40460FA40F13DA827825AF2184 + 95BC14D17B2B1610D2EB78077CD4D882CC1E88B76105408601E138612EA30740 + 7C9C305D107A200344808F98102600D231AC59464F0C8B0984CCEBE86FF83021 + 131322F800448861BDE232BA412406442052F306E328168301013E749430F021 + 000984D005E130A1206458908E623584AC31AC14D2E980A813420C0B00D9B620 + F58C18D60E40FEFEBF1D80EC66371FC3F9872FB7010980E826880D88EF821C26 + F8208A25F88801D126AC98901540FC022058900008D02108F9D3E6B3051FBC8A + 63D577AFE47529DD066444B11603E2317CF832BABB20B220E981F83ABAE143B7 + 413020E98100227D9C1000F96F6004082182F5A3693F26841474686C3F14C50A + 84781D6F0348FDDD657419900008F051AF01A45EC5B00A3EF41678B405617E9B + 18D60091020F2084E855E257BCBA8CFE3BE257FBC6B004201890FA56144B11AC + 820E2259AB055117640248C387CBE8D88FB58C0E7CC482E84D148B910561B88E + 0E7C60401A40EA55042B162400D2C709F7AEE42586E52E484D00E460AEA42B8A + D5EB7829A4770C0B2352C0D120326E827414EB726DC4DA8A62258265084914AB + 01648D61D5F43A5E6696D11997D17D1D9D2856010726241042040B00195BB1CE + BD6E73C2D9C4B0B0200D1F1900A4462B7915C3A284EE39F9C29AA50B725A7D8F + 1E08F623312CADE49509C17E183E14C3BAD410E2B5BC0611C5B0042135819073 + D5077104CB97D10190029144B0DC058901117C3886E56D58F4400A4462410C20 + D382603F5C46BF5B11AC1E59104AE858105E014820640110E257338AE522BA21 + 84EF59461FB7410A420C208610774102215ACB4B217D2DA3C78010C3D2F70410 + F7410A3A62416E0B8460426C3E9622BA4024512C41484D01887A203577021E98 + 10754002236D41808F981017D20320B2210520B221B620F7011F7B0AE9C0475F + 487F486308E92EC8841097D1751D1D00A96F20843E886E830C0B82113184709C + D051AC1C271C06A4DEE72780F43D104C486FC57AEEC5576240EABBC0C326E475 + 8D0CC84B0520F512C3EABB208E6215881480E83061E2586D4258CBEB127ABDDA + 88B544B018E0630B40DE1D1032EE82147CBCD506A4A0C310D2DBB00C21322023 + 8A057C3484147CE42E0817D2F796D17710F2F7FDED006437BBF9D88EBB20A307 + C214802882B5DF61F51A42D675BC9F5207C420D2512C7741021FEA82184214C5 + 5A63584B14ABBB200D203620D88FDC054914AB6358FFC10B84001F31202B808C + E3844008060410690B5220C265F43621238655F02110C96D105D468F05195D90 + 5C48C780781B56E0A3BED9826500C936AC012058904C81C7DA051178D4B7BA20 + BA8C6EF8F0562C00E40C9B10D6F112C76A0BF27B0F1032AFA31B42BA0BD21BB1 + BA0F322C48CD5A461F3D90CC4F0112225831217D19DD6B792FF056AC11C5A207 + D25D1020A4C0433604100142BA0782F9701744512CEC47BDBD15AB57F2FE16F0 + 208A150B4217E477800736443D1000041BD21052E0110B42217DAEE3F526ACEE + 82ACF031208428166FC18701649D408840E49A8210CF16840C009926C4006210 + E9EBE8EE82DC100B620871191D18690BE28D58189093E87FC88078E646AC7441 + 0220EA81D009593A202EA403208E5F0946042080880144312CC5AF80105E7740 + B809B296D189600D08D17D10EC47A601A4600408E9AD580D216B21FDD2B620DA + 865500027C300B7CB80FC2DF86906140DA82AC112C665810BA200008DBB0A601 + D151C2BC869019C5D236AC3B1E0980781CC5327CDCA442BAE143070A15C13280 + F82608001203526F97D16FC77CFCF98969430420352AA233060FC1C7801120E4 + E9F441DC0199B74178B120008821C426C49D1015D2810F0C4881C9D65D9011C5 + 32843C3CAEA30321CF177CD4C88278D6CBE82AA317803CC92BF8E09B1856A258 + 0D22CF4F080140B020C0071684EBE8EA836041D6C38432210521589011C30242 + 6C4254449701097CE41E08D7D15FA96FE0C3D7D16D41D403A9EFD73121C4B096 + F161C29A35868509E142BA0C882DC83BF5F64A5EDD04511FA46D484C486258EF + 6141C661C282911C2604407608F2F7FFED006437BBF918CE3F7CE50FF5B29237 + 2664BF06903944B146193D00E21E8821E49F3A8AF54D40040BE295BCACE21D16 + 2431AC719C10E8C84D100D312C600408C9462C20443741643F1A426C41D401C9 + 6C4308F6639A90EE8230A38CBE0008B7410C1F6D410A4262406611FDC4CD7E00 + 0865F41A45B17EEE4E88366301201AE003106115EFFF1CC592051184D4147808 + 46800F4C081D10204411AC1EC7B0C64A5E45B10C1F1C2794058901E932FAB809 + 12009916A42024008205F909712C19106C0816A48083BF0B3EE65D1083480388 + 7A20C0876E8324869599870903202AA75F5CDF73256F5F47FFEDD18E6175195D + 3D90F441D403D18B09013C62411613D26B790F910D490F2426E410B660157C18 + 42E881B80B3201A4E0A3FB200B7CA80702842482D55D90A34F2FF00884F44D10 + 6DC40A808C6D58677925EFD88A2510218A553082F9E83EC8B9DE88E529F01084 + C47C2C106210710C8B395D512CA6C02326C416040031849C2103621342140BF8 + 58A3585B6B7915C5023E000F47B00C228007AF2D88AFA2DFA918169BB02EBCC6 + 00A208963662DD63109109A929F0C080741FE432060392E918D665051E860F0F + 2B79B59A7740C8BE16A4376211C31A1BB1009011C3AAE91E488642FAF5E981C8 + 82D46BFB9138D6DD4C0C4847B098ACE3551F648D612D16C4E0F15862588E6269 + 1BD6038F2F657447B03431205B1604E8A86FC307857497D259CD7B3F03902886 + C5001FDD09097C10C35214ABA063896269256FAEA3EF5B48073C3A8A5530D226 + A480E3F1C0475B10C043EFF329A377111D133200C485745F47278EB52F84EC7B + 1DDD31ACBE8CFE62C1C7DC8865F878E535B6624D0BC2614241082F470965401C + C5A207421C4B00F2665B100C8863586F032232206D430A388011C5B1E883C47E + BC0B80CC2E886F82BC6FF81080388AD510324164F7FB7BFC7600B29BDD7C5C07 + F09001218A1500690BD217D21B40D4075920441D900690CCB71CC7B205395226 + 0400E932BA21C400D25D101B90020F402431AC7FC37ED4CB603F384CE8D710E2 + 9B200523828FA30522EE830021DB068428964C48810710E29B2035394AA8B721 + A4A62D08B741BE027C6800901810D910C307264497D119191020C41644009208 + 16EFB70A4474197D3521400806041B2200C1800021DE84A51856BA20DF29F0E8 + EBE8DF0540B01E0520C0C8D886850D19168438D63902911F09440A3EE8820021 + 871A4214C51280044216F89831AC6D00C182288A5520A26D584B1F4410420724 + 26445D908007AF3A2018909A831A4264421CC39A85745B900120996140524867 + 041EE9821C5ADF80072B793B8A25134217842908A107324AE9C047BD7FD26B03 + 32A258A7031DF54D01BDE0835700A2EBE800487D634054468F0149248B9B20C7 + 1570A80B22083180F82E884D48AFE43D99B9C0912CA04320C29B2EC8297D1D5D + F05173A1FB20BE0D6200A103E232BA21440604F8C8EB1E0893423A130851199D + 3710B26EC39AD7D1EF0A88D88468042106118147BF1C256C13D223F8F8732024 + 9D90613F26805CA9F77E01887B208B090142180C4800C49D90ACE3E52DF8587B + 200D20828F98909B6242C6462C2CC8B021312094D30B3CB492F76E1B103662D9 + 82D8807419FD8ED81043C8938290DE88D571ACD584F832BAE3581DC17227A401 + C46B79D503013E6241B4050B0B5200E2B5BC80475B106FC352140B000138643F + E68C1E8862588CED870E12AA071213922E880608D14D90821045B1A60551FF83 + D1857447B09E7D9EFE472C48BDDE860584B401A9BF5F792DF051208201491764 + 7B2B56F7406A5444F7AB627A00C41BB162417A13D612C77AF3EDFA6F3220B620 + 6335EFBBD8102C486631202AA42786F57E7A203221051E2AA4176CA8943E00C4 + 0CB2FBFDFD7E3B00D9CD6E3EAE53E08101E1A58C2E081906C47D10DB8F0291C4 + B0FE2FF6430388D0077117641E28C48210C72A0829E8B00929F09009F1388255 + D3264410D231AC9A14D17D98D000F2EF14D26BB4110BE0003C04229E7DB6626D + 5990820F26F031CAE8DA84650099107262E0C31644AFEC872DC857818F18902E + A38F281677411608F14ADE6940042014D165400C218A61F12A86352D08112CD9 + 90800810D25D1017D26342800FC043F7406C40FA28A157F15248A70382093184 + 0C13A2527A20E4B0736C400011E25703448861012005220D214C5F470744021F + BF043AB620C43D1075416A1A3E460C2B85F48E631D440C6B408841C45D108AE8 + 404801C812C5D266AC988FDE86350AE9051FAB05014030213A4E180B027CF461 + 42C0837B20BA09D226A4D7F202219810EC87E2577D13243D903621E764352F16 + A4E1A346DBB0EA258ED516E424DE828F5ECB3B6E828C427A2C8840E4E6CDA982 + 0F4388ECC7DA0391050140D886C5248E356258B620028F400826C4EB7879A7FD + F0002119BA200342800FF74114C96A00A9575BB1B600E46EC187AEA30F08A9C1 + 7E08406A808F44B1D403592C086B791B3EDA84E828A10E13268A2503326358DA + 84D516A4DE01208290BE09C21470147CD007117C3018908210E0E3E6820E0148 + BDDD0119DBB00220B7011DB22074415246DFBA8E9E1856006444B0B80FF210DF + 1340560BA2FEC7E880F0FDAC8BE98960DD9FF8952C4820C4AF2358F440DC05C1 + 803488C47CC882B00D0BFB6103E27119BD7B208F3FFB62014817D10B40F896FD + B0013180248A0580AC16A4CBE8F5ADB5BCAB010980C88208441CBFEA57B74116 + 0B2213B21890DE86E5423A06C410F2FA9B6F4D03B2D50759E1C306A4ED87A258 + 44B0EA7DF79D86108308F6C3BD10438801A4869B20B2201FA40BB298909D07F9 + BBFC7600B29BDD7CACC73D907F4C1744DBB05610D1462CDB0F7A217D9CF0335F + 033E8862B90BF24FB22101107540660F44EF1AC31284B4059985744600C23480 + C884B88CEE8D5818108F2D0800326358C3827C9F55BCB61F0088FB208690711B + 4406642F8400208E627D596B79D98A7552C6F1ABBE8CAE1E8820247D90ACE59D + C709032003420A4A384E58DF2AA317840020C384A80712F00884782D2FE06108 + D95ACB3B00E4CC11C1C27C0020DFC77A044274172400B2AEE5158C1474FCB8A0 + C35BB1F696D1EB6F2C4820E4E76B1704FBB15A90362035DC05D110C50A7CD884 + 7407A4BE0B3CFA3688635840C82CA30B4264410A3E8EBBCC362451ACDE8AE52E + 488AE8B220EE84B092B72DC8BC8E5E23F39139AD60441313A283843E4CE895BC + 6CC4BA66F3273661F1DD3D10004436E43AF740EA9D312C1B10C5B10421E982A8 + 07620B22FB2100B9B1BEEB6DF8C8001F94D1D5010980504857140B08E9323A9D + 105990BD008205B9251BB10010C347DF0551148B323AE051206200F13B00A460 + 645A106FC51278F4BB0740F47604ABE0A321E4525EF5407C1BA40F13F63ADE2E + A2B709B1054904AB01446B798960DD973E48206400482C489B100D1BB11EF26D + 9000087D10C107C709051F18109B8FDE86C5F73C4AE81703D251ACD107D14ADE + 4747144B85740188635877DE5FAFC6264465F4D580B09617F078F0E911C3BAF7 + A16712C3620A381A421EC5841484C882640A3A14C5A2888E0169FBD10644DFC0 + 077FD3057114CB00820579D1312C8AE86B174410620BF244AFE3C584603EBA07 + 02802486F53457D16342BA883E20841856CDB30190EE820022CF032005235810 + ECC758C9BB2786D500B2BD116B1E26F451C2BCB21F79078010C5EA381651ACC4 + B1D205997741B01F051EF51A448860F96D08F92020E22E882D8867B3F9A0FE21 + 2C06D9FDFEE6BF1D80EC66371FF3611DAFBA200D215B1BB11CC3E2FD1443144B + F6A3DE820EAFE62DF0F8FA110610C5B06C411CC50A846043DA802882651332A2 + 58CC6A410090011FF5021F0B80AC2B79FFE3C06337FFD1EB78DB80D40C030284 + 084012C5FA1100C25574E0C3004214EBCB5B6B79633FC636AC3620868FFDB022 + 818FB6207A15C52AF0D01474D4DB112C406418106DC3C282C4840440384CC874 + 148BCBE8C38200216D400220828FBD1BB114C13280F82E48410771ACF441DA82 + 8C9B2087A40FA2D9D301C94ADEF52E88A601449D1040E4C25944CF8C2EC80220 + DE88D526E4928290028E800816C485741B10ADE5650A3A46195DF0C1370052DF + 01106DC4D2C480C8825C2EF838F414C7B0BA0B72C4C9051DBCC4B0B02005225D + 466F13D21BB1FA30A17A20B220EE816823960C480AE91C26940DA10B12005117 + C436A42FA3BB0F32A3585EC9EB28D64944B17490D0712C0D20A2C180DCA42E88 + 4630121352E0717A0004109101A9B7D7F2CE3E48625802104388400400C9AC3D + 1017D1170B220889094917E4C2820E5B900088E063C6B0B411AB0D08DF189061 + 42021F01112CC8384E180B7265A258C08740243D90DE868501B98E976D587CCB + 807098302604F81080B096B7C0E37643880D48CDB8096203E2323A2624200284 + B40161D20751193D7D109B10DE1890FAF665740388BA200D210210DE7441EA6F + 599087194C8801A4D7F27619FD01225918104108712C6FC3D26A5EDE828E07B1 + 208190711B24712C6DC20A806814C39A16C410121392389663582B7CB405F108 + 3E02216B17C4F6233D90868F1AC347BD6D41E880D4BC007C00210D20F5ADBB20 + 0340B020860FDE57DA80A807F2967A20BA8E2E00490FE4ADB707787419FDCD77 + 0C20589011C32AE81088001F82105B10F541DA80F43BD6F2FE45060400B105C1 + 7FEC08E4EFF1DB01C86E76F3311F0AE97325EF1AC50242FC02215ECD4B0C0B08 + A96F0044DBB0DC016115EF67808F6F001FDD056908E9327ACD014714781CB1F9 + 579990230B3C8E4C11DD65F4EE836C4388475D1020E4BB1C27741C6BEB3AFA0A + 20E332BA41642DA36340B6A258322006101D276C0BD21052AFFA20020F9B90BE + 8EBE1FF051EF2CA3D7AB1E88C76574834847B16621DDF6A36358AB0551174410 + 326358362000486064BD0D52F0F1BDDF031FB121810F9B1043880124109208D6 + 8FE984083ED618966D0810C2362C83882D88EE81F06625AF8E126658C92B10D9 + 5AC97B6141477D634030226D426A8861FD561D101B10D910E0A30BE90D21EA81 + 741CCBE031BA2032208E62F1D205596358DC06510C2B00A295BC8A614D089105 + 09808C0988F44A5EA258476141E881C884183EB809620B9208D6E88234807823 + 96212431AC36218B05D161C26CC4EA2E8862588C002416A4A0C35BB102208A62 + 19427C13A4FEBED4657475410A48042097DF6A03220B4207A4FEBBDEF4410A3E + BA0B721EF0411F04F8B8B2A0A3C6106200611B169BB0E882B80F920816E0314C + 4881874C08AB78DB82183E74178401420420DC04C94ADEB620BA90DE112C8CC8 + 0381908048018720A4EF81AC166474410221D988C561429B9005403A8A45FC2A + 10D2DBB098DB1A424604AB5EC107DFB620B7031F034018FA1F01102C08D0B140 + 8801A460A4E083F136ACBE09E2D7112C5B105D47D73A5E366201208087B762C9 + 827421BD00C41BB1560352F0A1A9EFA70091C4B06A1EDF0B21A30732FB204FD0 + 01C186A80BE208964C081644060408990684081645F46142008FC047BFBA0D92 + 1EC8D886B5008857F2167CC884781396FB20B1210D21B1205EC94B140B13621B + 220049211DFBB11DC5A2906EF878B7BE05201A17D215C1A2909ECBE8338AB56D + 403A82057EEC20E46FFFDB01C86E76F30998614134876A2B9636636513D618EC + 0726E41B36207D94F0D394D013C1FA0C31ACFAF665F4FADEDFAF8AE9051EA307 + 52AF2606E4DF0EC080001F89620120DFE52DE0A8612BD65E0071048B22FA760C + EB3FBF77DC8C61C584F44A5E5D451F26C4F6C3374188629D38A2585AC9CBDB17 + D265400220BA8A7EA2AC87BB209810225800884700326258B1200290D3C6AC77 + 41B0201E43C877186CC8168004426AE88428867530EF597AD507014400105990 + FAFB8F3E4C8801F91110A28D580CE0B19810C0437D90ED95BC3A4E08840440F4 + CA82D007B101B105097CAC130BD2312C01882024C709010FC5B0624014C70A84 + 083E3A8A058004420A40983FD6B7204400C2DB1BB10A3A62400E652B5600C451 + ACEDB5BC3E4ED826640F843480D40840001119908CE0639A1046003266F64034 + 8190061097D119E023101203A23EC8001047B04EAB170B722A00A22E486258B1 + 20675C7273A2588690DE86354044062400921E887A218B01511F2410E2185660 + 04F8D0CC2E880E130A406C4130228E61614200111B1017D27D0F44870915C5EA + 327A2EA32B86150091F9880111786040EA2DF06800C18050445729FD96070788 + B88C8E01F161C21BEABD81F7F68715C5BA718110B662011F37021D7CB3112B26 + 4445F41890D582DC7ACFE3EE82083E1A408011F7406EBFFFF1CD1D4B046B5890 + C0870BE9818F61410C1F7E97285601C89FD50501423CDC03910969FB916D580F + 001E5B06C43D9061416440EA2D001184287E15132200C936ACD1056903620871 + 0F0408217AC5D001E9710C4BEB7881905EC95B00A28D586D40783B82F5528107 + 230BE2289620648008E0F1460108F0911E48CFE883BC29003184604462407865 + 413A8AC5D005794757D1B762584C81C70A205EC95BD02113D2F623D3312CF541 + 0A396A421F06901D83FC4D7F3B00D9CD6E3E0143FC4A5BB1DA80C882D8807017 + 44F051AF62583514D1B120E33821302200F1FC3316A4A657F2EA26C8B0200520 + 14D3B7B6617517C4312C20C46B79338A6161430A3C0A42B88CFEEF051E7D9CB0 + 0F13AE7D1001482CC817819061403CB390CE4A5EBF3220E9818CAD58F4407E52 + D0810D0984F44D102C4847B1041F7C0B3E26808C75BC44B034C0870DC8010048 + 0C082B79BFADDB20B120BF9937417C1DBDE6779E11C3EA2E88E0838D58989033 + 373F1088504C7714EB8707CF08D63E65F4C582C88414740C08C96574C5B18E70 + 146B5810FA200B80084262405448D72A5E1B109B905EC9BBC4B00A3814BFD218 + 3ED4031184D005B101D15D90020E6258A3885ED0314C8820E48A4C0C488AE95D + 46EF22FA3021E98268235600441BB178D5092908590E13DA84148C001FCB5D10 + C7B0AE338474140B030284A40B22F84817649890F3AE8F010988F4362C604431 + AC1B0B3EDC07E928962C486CC884901A010826C4F0A175BC8A60B907D216C43D + 90B90DEB6CDD0329F0C8362CADE5DD82100C484D01470388EF82D47700C4EB78 + 6D4158CBCB388675770C0885F47B375AC92B0B526F810736A40D48F741AEEC37 + 106210B9DF20C265F4D88FABB8882E03C23B7B20D7DC068060411E4A0C8BB77B + 20F33A7A5B90B90DCBF031004451AC028F9E7B9840080052EF8C6219427A1B16 + 0032CBE8DD0529F0A81900526F5F462786E54D583621C3800C08890D69F8D0DB + 00421FA4DE61409EF551C2581000C465F4828D61413A86D526A4C063E9826041 + 1EC77C60410A40DA80388E95EBE802910920BE8C9E1896C08338D66241067C14 + 88F456ACFABBBB20E330618AE8322140480107712CADE5651396208438160002 + 7CD88210C79A87090D21338AC5778147FD4D144B10F24EC14820441D907758C7 + 5BEF88613982A55706A45E19901E4C88CBE898105910DEDDEF6FFADB01C86E76 + F34998028FDE8A45217DAEE405400C219F6A00E91EC8D70A3E8865258AB5F73A + 3A00320BE9051F4048C1474FF74028A2FBC582183C3C47C584B807A2BB206CC3 + 9209E1258215089109A919312C6E821CB3F9CF11C5C27E2C10B258101F263CCE + 16A4E358E981CC2E8847DBB0B48A37312C4C0810820969089105711C6B744102 + 201AD98FF4406440EA5B10E242FAD886D5009238168709870929F0208AF53D40 + 04E8A8BF15C30242BA0B42042B0032625881104CC88010591043887B208E6101 + 206CC4721CAB63580D217B0004F018004214ABA0A33763C982F0DA823480288A + C56A5E4C480CC841C7F15E9A32FA02216D41D40701402EDDFCF1F8CB0D2227C4 + 80E88D0529F0D0604114C59A3D10A6B761093E4EB95A857419906CC55221FDB4 + 6940D888352C884C0811AC7A818F4008F0E1AD580523E75C1B00990644774104 + 2199820F3A20DE867583B661750C0B080140460C6B4008E03157F376044B31AC + FA760F04F8880129085117643D4C8809E90E082092189622580CF01108E90EC8 + 05FDF6C480B4051985F46BB1200521D80F1910E0A3DE820E75415600A9A190EE + 2E487DCB82D4B405218A1503420C4BB7410A42AEA9F7EA820EC6EB7813C51284 + 60426241885F0122051E6B0C8BEBE8DE86E52E88FA207BBB2018106DC45AB662 + 09426AB8110284305B31AC028F1A3A201C25B409317C34806888600D087111DD + 2684323A20320D486FC55A57F22A8A0578AC0032A2581E5F465F4C482CC83020 + E982347C2882552F257420A42D88EE81F446ACE7980542D20599B741D2074914 + 4B26A4E1E385828E4088C08342BA2258F50D7CA40F32B761AD6B79DFCA5D9000 + 08512C0DF0111069006908A113320C488AE84088A258EFD5FB5E81880D88BA20 + 9A028F8E60D53B0124100278D44B215D16A446D7D1739C50FCD1B3FBFD4D7E3B + 00D9CD6E3E290380D4ABAD58051F8A60AD00B2C4B1C65D901A6DC35214CB00E2 + 3EC811EA8310C33284CC2E0885F47FF9D69F369F0D78CC227A5B907A0B3E6C42 + 0090A336FFFE6D4388FA20ACE3158CD884E836880C48CD816D40729450008201 + D913C52AF8F8AF1F603D96C182D00389097119DD202200218655F0210B028428 + 86057C00240D2035BFE075FFE3EBBF5CA258DD01290891010142808F258635B6 + 61158C347C7C27F741C646ACC4B0DA82F82E0873D6729C101B1203C214740842 + 6240B4092B26E4C70111C7B1624116085117247D1041082B79EB7B44B032A30B + 820101426441021E7C031F589044B074193D1BB1BA84AE4D58322117E7206122 + 586CC2C28410C1920901382EDDFCE144A66D4897D10190408800E4F26523D676 + 044B31AC82105D471F312C8AE8B6216CC39201C926AC06112C88C700326258DD + 03E919657403C834208962D59C344CC88D8110C7B1DA82740F440052F0410F44 + 87091708390DF0481744112C99900690DB6C4112C5520C8BEBE889625148670B + 5643C8B4206CC68A0129E8E0A50B6200218AC5462C7A202EA463428605690011 + 84F45048F78CBB20B11FBC338A450FA42DC87217247D90BE09E295BCC4B06243 + BA0B028064F8DEBA0B028450445F2C086B7959C9DB166414D1C72CF05173DB3D + 8F0B423EF42E88BA204048774198A50FB2C4B05448CFD884D8825048178010C7 + 5A3662011F74411CC3E20D8804401E521C0BE8A8EF4008DBB0384CF8085BB104 + 216CC4EA2E882DC8A35AC9BBDD05F1462C20C4112C41488DC1E325CDD3B120CF + 14802882352024008201D14D10E6D5799830038010C79A06A4C00308A97979B9 + 0BF24A6C8821840E48BD051DAFBE56E0B1C7828CDB20B2208E5FF975044B97D1 + 47197D89622586E5A384F512BF8A096908D98A61F55B10B2C2477DED00E46FF8 + DB01C86E76F3099A2EA37B23D62CA1AB88CE8BF1108CAC11AC1890C4B064410A + 3CB80B6200317CFCF337394E880999166414D2810FEE82641DAF3762D105F953 + C107E01110013C643F301FBD0DCB1BB11CC38A01592104F80044301F0D211DC1 + 1A16E484651B56002410A2323AF01108196B7915C3F2F44D90DE88F50D15D20B + 3A02228E62B505396D94D1C756ACB620357EBD0D4B102200C92486A51E086F20 + C4512C4AE89E719C505D106C4881C7C114D10D2314D1B7A358051EB2210610F5 + 41041ED88F8291028E69413C8E61011F1344B62D884D887A21475F38D7F10A40 + 7A6C3FC65D908610C5B02E55144B10028CC882D0FF985D907DA3586CC5C27E18 + 40C6362CBD0D20744128A20740F80640041F6D41660F44931896E043E38D58BE + 0B32C76B7901915C46AFD90210591000C4F02100E10D80E8386120C47D90020F + EE826435AF57F2268A25F8B8C911AC8B6E1588E82E882C48208495BC32218110 + 2EA36340348110C107EF1D8962C580D49CDB9BB01A42DA8004422EBCAA008457 + F0D1F7406617A42DC83C4C9842BAC631AC861059901B28A3C782604052485FFB + 2094D1AF66D4057124CBEB78DD03D1288645040B0B320DC880100148818720A4 + E0A3BED503690352E03141243D100D11AC7A0520DE88352C081D1020A45E5B10 + 8388C08397BB20B98C6E08D903200FCD8D58EA816C6DC4328478239601646CC4 + 1A06A48628568D8E130220D80FBD05220520CCD888150819C709031FDA86A512 + 7A7D2B8A150891017119BDB762B98C8E05013CF6DC04A9D1462CDD0509880021 + 6D4296223A6FC7B00422051DDD0351142B06E4D574428863F92648AFE56D1362 + F8E82EC8E881002129A2CB8414788CBB20EFBEBB79E7EDF753483788CC32FAF6 + 4D90694272943011AC7A0222A18F1D84FC4D7E3B00D9CD6E3E4923F8C82C1664 + 85906140341DC13A54DBB0186DC39201A997B5BC18906FAE3D10060B02801CB9 + DC0529086123D68010DE8E61D97E741C6B6CC5DA82103A208110A25832202EA2 + AB0B923E884C480188E35840C809A38CCEB8845E8309590D488DB6610121E982 + B8901E03220849115D312C77410091B109AB2700C2CB462C00E45B14D1D30531 + 80CCC38407681D6F0AE9051D8A62054464418860613F7A256F20A40F13EA2E88 + 4C88376375144B1082FDF8E3B98210C5B186018905590D084317A4E617394E68 + 00A1940E845CA848D6AF624280905F1F79916D08E0B1173E786340E883A8940E + 8010C3A207220029F01800027C388A2513520042197D1C26540C2BB365426240 + D886556F5F46D76D108AE80C30721AE30896EF82044404207D13C43D900120BD + 156BB520C4B062406C420A48301F03443A86757D00C431AC2EA4DB82642B9662 + 589890820EC5B1DA7ED8869CAADB20338A757A6C880D886F82682E05421A3E02 + 208A611942C64D10A640445BB064426A0A446443041EF5623F5448BF737351BD + 8A63C5825C7CF53D3621E981AC166402882358238A5573855E40C465F4ADA384 + 0290EE82A48C1E08F16A5EDB0FAFE56DFBF1502004F00044267CF83ABAA73762 + 398E1500B993081610E228D696050980ECD3030984F461C2B1114B00620B7267 + AFE4650221BD116B5A10A2583559CD7B5FBDDA86C5C4823886C5557443C80090 + 1A7541968D58BE0982096123D60B4B291D08F1CC2816AB78010FC30816C48709 + 0B3E3A8EB507425C4AEF1816F0516FCD73C0883A203D051E820F9B90AD28964C + 88EDC7886175217DD988E51E483662157CE836880CC85B9BD70B40344B14AB8F + 132A82250089FDA088BE581022586F61403A8685FDD00020051D35D80F5D46AF + 5731AC1A1F272CF09009A97F11F3FF041F3B02F95BFC7600B29BDD7C82E6FF14 + 74B88C8E01318010C1FA07E2573220DB657459907A7D19DD00322C48CD3F3323 + 86C560430C2082105910EC47E2588047B661C98250405F0D4840A457F28E18D6 + 384AD8D31604F8B00551115D1052D01108B1090980D4FC37D1AC820F6FC55A00 + E4A71344B48EB74DC83020DB10E2B5BC0D20BCA7DA8210C3AAF75B981020041B + 52D0017C3886D587098190799C70DC06D9E72EC83420C0475B10BD988FACE55D + BB203F28E87021DD1BB1B4158B95BCCB65F48610F54004219890869014D16541 + 7221FD48BE0B40008F1E991020A401A4CBE8B6215EC97B7101C8459B830A4446 + 111D13221B6200E9BB20BF3FFE9294D1DB800022B11FACE30546041FAB059900 + D277416C400C2033869528169D90EE81288A6503420FC42FF6C3364417D2EB35 + 80F4562C97D1BB882E08C186083A384CB84008405200C2814262588690020E06 + F028001916A45E17D25D429F10724BBD35BC0110C7B00C22EE82D88274046BDB + 82CC1816112C17D25D46C7840840F4021FF56A12C34A19BD0F13CE75BCF56240 + AEBB6B1C251480E82582552F9D902D03420724537FCB822C06648008112C4108 + 9BB112C502423A8685058909B94E5BB180900210A663585B16A45E0148FA208A + 61D5DCFD48AEA337803C6200D16142E0A301E4511F26148404440220C087CBE8 + BE0B22089105013A3021B6213A4C58E0211079C8D3D7D18190FBEADB062416A4 + A0830142D63E880F14A607F2D8F3B30B120851144B26A4A043DFC4B1384AF8BC + 006494D19944B0DA84C8822C857400E4698DE1831896CAE81A5B906754444F17 + 041342F46A40C8EB1342800F6DC4C286604080102CC8EB32202FBDF6A6225914 + D2BB07220029F8B009C180D4248A057C681396A67B208B0D890901403402907A + 0B4046295D10B216D2D303C98C28562CC85FF40D763480E47BF7FBABFE7600B2 + 9BDD7C02C70684BB20EE83D8821842FEAFBA2087054028A33387D7B76F82AC10 + A2BB2000085BB1E8841484F83A7A8147DB90E52648F741C65D10CAE8790D1F05 + 223220C4B01CC51284641B56DF05D136AC028F01203220B30762036210E93896 + CBE894D0B1208B09A9D14A5EA0433052E051EF7E05221890AFFEBCBE55446715 + 2F3072CAD886D5B741DC0729F850211D088901C17CA48CFE2DC02320B2AEE355 + 048B775890D3554877199D49048B590BE90110F541DA826825AFA35802100C48 + 2CC8886251465F20A42DC85A46E7368801843751AC820E8F2DC80410C0C3712C + 7A207D1D5D7D1001884D485B10C147834803489B10CD650342E882F485F4BE8E + 6E00A10FC21A5E8F012416A4C606249BB106801842341DC76A0BC2001EA30BD2 + 06C42303C2A888DE26A4E04385740C48B661ADF03100C406C4D30684E3840611 + D90F6258B21F1DC3023ED6013E6EF6C480388E650BA2E38444B1800F2C480F10 + B214D2CFA50F7205D3E06103C23A5E15D1E9818C18D604902EA46341B4961700 + D1462C40843180B0154B070913C3BAF4867A818FE53ABA6358F7A607D2932E08 + F051DF57D75046A707322CC81E00193D10E258051D7E1F76144BF0D106C4DF6D + 40BA90EE75BC5C49FF100BC2C884D882DCB6D503E1E53ABA2FA4F746AC36206B + 14CB47093B8A6503728F0C483662C984603F28A5031D4F7B741DDD31ACFBB98A + 2EF8680B0284740FA45E95D11DC35217A4DE19C37204EBC3BB202B84BCEC423A + E663B520CFBF340C08512CC5B014C7F26823564348810710621029D80884E842 + FA886319405E043CDA80647413848D5882103661D51B0B62009910A2281630D2 + 0644F740D888E508966F82BC330CC85BEF62430C1DEFBCC75D90EE82C4847019 + 9D32BA4C0816E42F312100482C08112C2084B7FE71BCC38FBFFE6F0720BBD9CD + 276E6241D403E12A7A7D0320051E8E613588348410C19A65F47D21A4004420D2 + 3D907A018F021015D2654012C19205F161C2EE83D882B08E770510EE821C23F8 + 701FC40082FD18C70913C3DA829000C8978862011EBA0B52F05100F2DF3FE43A + 3A10620019112C41C88932205F2EE81080D00959FA2014D2B72D8847F01113D2 + 37415C4667B020A78C1EC8EC836040B62164FB3A7A4147BF8210E0E38CCDF70E + 3278CC327A5B901A0C08DF0522EE813886B516D1554C073C62417E7208F62326 + 443D90D582B409E128E1B2114B3D900B35DA8CC5347C2C16448709DB80147C74 + 0FC45D109B100E14CA80AC16A42124512CAFE4053EB020B121051F2AA42B8675 + 59D6F1AE10820929F0E8013CB411CBAF01C45BB1885F6DF7410A3EB0203221CC + DEEBE8051C67C682AC8574E04326E48680888F13623F460FE47CC7B1808F1542 + 4E0988C8800021BA0B924D58F5FA2ABA21C4D7D16F15849C9E95BC44B10091B3 + BA0F820569F890010142B0203620E71474F44D90B1114B712CCFDC8895227A40 + C41BB15C445F7B207D9C5000A228962D88EE8204403CC0C79F7D19BD20E4F2C4 + B0FA3AFA00905B7831213620B6208E61094014C732802882352C88618438D6B6 + 059931AC2EA3DFDC10C26D90020FA25874400680C8841478D4DCCEFBE747739C + B00104F0787C73E79F0B3A022003423A7E55AF36613144B03021B21F3620B620 + 4F0F03A29B2005232AA3739C90F8557DCB7EC8843CA7217EE53E484187A258F4 + 4158CB4B04CB26646CC5DA8A62011E2F6C384EF8386FE0E3717A20D80F5EE023 + 06646F1784F8555B1045B10010E685D736CF001F6B0F241032BB201EF5405E4E + 0F2445F47DBB208110E2570291B7C751421B9098101D26DC0B21733A8AE595BC + B61F5D4877293DF0A17B20C0475E00842896C0031342072436A43762811F3B02 + F9ABFF7600B29BDD7C12A7C0C320820101420C1F7A0191AFBB0BD27D10814701 + 892EA3A790FE693A20B9902E03922EC8D6857445B1660C6B1C26CC5D900921DE + 88E528968F12760FC4100280C4842486A58D5814D1394A981E48430800E21816 + 1D1077410C2101104188E143C70913C5EA3896E1A3461D102C08F0B107401603 + A21E0816A4A3586D416242660FA45E4C4800E4DBBC028FC047C1C8E88250441F + 1052037CA8030288347804426440E67142C7B1302106102258C380288A952E08 + 10027C8C1816057403C8CFF729A41784ECBD8E5E00D26574DF05013C021F475D + EC523AAB78D507B10119933E88218475BC808801C44709896031DB857447B16A + 800E9990028F98104AE9879D04780022F5760F44F0C13B0DC8B420DB00324D48 + 366231051FF441008FD1050142D4053188001EEA849C73BDE0633D4C78C2B937 + 1A44BA0B22085923581E7D17740020EA7F001F800800A2710C6B8D620940C665 + 7420A480631F03120B92997D903B36E75F7EA700440709051FD8908E61D5001E + 3D23868501218AB55D48D7757456F35E7FB74C88461072CFB020744130200211 + 6DC45A00A4BEB7366209425C4AEF83846D41AE191BB16C3FF6C6B06C418860D5 + 3B00C433D7F13A86354BE90048E25835DD0351146BED820844D888E518D6B020 + BCD88FFA0642C6462CC5B0022294D1D50559CBE80D218E610D132208013E3021 + 81907A052144B1F6C4B0B88EBE6EC4023EF40220D88FF441E882F44A5E0148A2 + 58EE81D884B092576B7903200D21860FDE5810DD066130212EA1D309E932FAB8 + 8C2EF8C086BC3156F362410C1F0191BE09C210C38A0971091DF0E00D882486D5 + 7741C67142F540B2112B9D105910E25701907917C400A2890171111D10F9CBB8 + 0D32A358F5821F2A84EC7E7FEDDF0E4076B39B4FE8ACC709670CCB06445D10C1 + C721338A250352D0110B32B66271255D06842E48018700647B23165D9069411C + C3023EBA88DE008205E9EBE87441FA1E880BE980C78410CAE8BCDC03690BE295 + BC00484064818F7119BD672B86057C78239600A45E7741E881002206105B10A2 + 580048EE820C10F136ACBE8CDE5D1043085D100004F83875734040646F14CB47 + 091900A4DE020FCF8C608D95BC804803C80A21C4AFB4116B1E27B405A917F0D0 + D88400213FCD782B1673FE001145B146191D03324DC8E883C484288A350E1332 + 0048C7B00C217A6340B4114BAF0D88A6A003FBF1BBF441FA3A7A5B109B10DB0F + ADE4950529F0A013A295BCB620F33A7A0C48BD879F7A8520A4635847321DC3C2 + 849CE1B5BC0D1F1DC5D2514245B0DA84147C9C59A0C15BE0E11E485B10A68043 + 2684B72DC88D8963711BC406E4E4F30A38963E084311DD130891050988D4E82E + 88E258860F97D1F9C67C1480F4BB02882C0885F4054286092182C53ADE8E6215 + 70248ED50072A1C60032CAE83506906941D407013E04210C51AC0210C5B1720F + A4E0030B72598106F0D117D2D76D58C087376201200FA890EE95BCF32E880164 + 2DA4731D1D03028C0020B6203772215D112C9B10C5B0EE7844512C4148BD94D1 + E980A890DEB302086FA2580D1F034214C1AA89016908F13A5E5EE0C3E0316601 + 902EA3FB402110E2389620040BA2189601E481C728A513C1EA17F0F078139663 + 580F157CB80F6200E138A13762B907A22E8862585890744000124188BB203D36 + 218E5F09440A3C644014C38A05A190AE1896C1C331AC7442885F655E5C20645A + 90DE88D52012F8480C6B14D2D503A1906EF8D8069077EA2DF0C8F826083D9086 + 90744102226D42741B24F081FD9837418860D5BCC7262CF7416440B01F1F6CEA + C57E2C1664C7227FB5DF0E4076B39B4FE27CF50F89611940FE613F20A4A04346 + 04F8A007E2770288E3580C10027CFC534188C0A36358980F01081BB198820F26 + F6A337621942EABF01214B0F4400C24D907A653F00110C48A6BB208C004406C4 + EB780D21F33ABA4D4806000988184052445727C43363580D224088B7602986F5 + F305440A3CF6A38C0E8808420220989061401860842856AEA3F74D104148C1C7 + B88CEE42FA886165B80B027C68356FBD8A63011E03460A3A80114108312CB662 + 797A25AF0084323A26040BB214D27F7CC87905211F5244AF975B207D17E46774 + 4134018F7A077C2492354C48A258732BD6C52EA4EF31215B2B798FBB3816C4D3 + 97D115C52AE8B009F161420308F051DFB1202EA5634232051C3A4E480F44E6C3 + F0B116D26D4228A2C782D41C557F0B40F81B0839F3EA0290581041484665F4C0 + 47BD1DC712806040343386A5B5BCF57777413A8645046BEF7142225832228087 + E25840C82DEE8110C3AAD151C28B6F16849C1E00910549174400C26A5E0E140A + 4258C95BD0C18BF9B8ACE04316C4112C83480DDBB00A38F6AEE46D037201D091 + 95BC3221744034B90D2208C95BE0E1323A0092081616244574DE5EC9BB6EC512 + 7C002205208290028FAB008F7CDB827823D628A30B42886165302080C8560CCB + 2644F0919916040001460C205D463784308F2F00D2512CAEA317746041B29277 + 8D63614174A0F0C127460C4B85F48E613DF8B4BEFFCC8142EC472C886258DA88 + 85052182058C044278895F3580C484701BC477416A9E6412C352113D06040891 + 0129F0900589015114CB112CDF040980741744037CF0760CEBE55C472F08E115 + 80B401C9462C0148FD5DE03140443D108CC85246DFEA82D882E842FA3020588F + 092106111F26EC2E0806E4CDC04817D1810F77410C1FF44128A3B705793731AC + 2D13F2BE4DC8BB29A2AFD7D13F78BFE0030B12E810873484EC7E7F95DF0E4076 + B39B4FE8001F7A0522DE8A05807835AF0DC88861297E45199DD72664ED810C08 + F996A7235874406441B80752F0318E1312C14A176416D1311FDE8AA518562C88 + EC87E61843885E225886101D26FC416E826400902F01223F740C8BB105596358 + 0B7CE45B0092089627160400D15A5E5B106FC102440A3C0680A407B29890EE83 + 183E6C40BC8EF7F4A58CBE5810ECC79EBB205B65F4ACE515846830218B050984 + F446ACED32FAB9DA88D5E60313D21D100148CAE80D22A3072208A9F7701B1081 + 48C1C70491021045B02E1817D17D9CB001E442F7408863157C1C14003184147C + D4DB3D10CC07D3312CDD058905014214BFC28400203121828F752B565B100024 + 2308D1B80BB27517A446112CC5B0EABBC04330C24B095DF06100218A35376211 + C30A889C631B322258EA811478680C1F5B85F4029013051F6D40EABB2358D80F + BDEE828C8D580C25F47442E880C88068E883D87EF838E1AD0690FA1B03322024 + 068422FA79BA0B620839F7CA7AD507B9739A900690AB7BB6A3585D461784C482 + A80B02805CDB3D90828F7A0110D990B6200291828EFA9B3EC8DE9B2074417A25 + 2F312C004451AC0120DC05C17EB80B62107107446B790B423A86A53E08D1ABBD + 10420F44EF1E088905013E6E490FC4160408A9118064EE2B20690B72DF930340 + 7C94B0A7C0A37B2194D065421A441CC11280288AD506C4E33E0891AC6713C1F2 + 0020EE820440348E61F5362C8DA2587B4AE98A624D00794C3D9058906763411A + 4214C5C28438820578E88D01D14D90D58204447A2DAFFB207441624198C4B086 + 0111802C2644006208B1016908013C12C76A0BA27967F440A601013EFCBA0BD2 + D7D15703027CAC0042048BB7B76161420091BF782DAFA2580C5D90C4B04C21BB + DF5FE9B70390DDECE6133CEB4D90B111AB0F1416788C28560AE90340E8800C00 + E13D22208201F16ADE6141B292F7B344B0BEF52745B11CC7AA57112C5ECCC734 + 20B3906E08990082FDC0841C6B132200C9043EBE8005A113220BE2327A47B11C + C13AA160A48003E8D01474348CA80B6210E96D587441D403E1EF828FAF167C8C + AD58F4417E6123326258034030208E610122DFFA7540642DA2CB80D49B9B20DD + 0769F8F8B600E4F474410C1F632B1606E46020E4EC09205ACBCBCCD5BCE32608 + F0F18773DD07E91E48604445744D434841C71AC352148B995D10C1C79FDC07E1 + 2E08F64351AC11C13284A807D2D3162465748AE8B220D88F4008657475410011 + 41484DBA2044B1787D94D026C416A40164B52044B01CC31A00027CB4052918E1 + 28A12CC80090CC12C1C2824C08490CABDEDE8435200400690839779A1041C80A + 1F6D41F69A1000E44203C93E10421F040019F0E118161072FA25376723560C08 + 00220B621322F8D08B01A96F45B08863613E009069427A2396BA208A63D10549 + 1F241BB198BE8A6E0372CF12C5720CAB01E492EBE98300220690B10D4B16E4BE + 44B0DC036913B2D782A8075233D6F162433A8A750B262431AC7442DC0761D882 + B516D21B420C1EA390DE264431AC09208E61B98C3E0AE944B11600A190AE527A + 81874AE99A0048208432FA5D051DDD0531842C262410F267DD03E1B505F14D90 + 06901AB66105407A2B960DC8F3F552420F84A8075223F8A8F729DE820F41086B + 79032044B1780B3E180188A2584C6FC5AA2900E92E0897D1B5965716A406E0A0 + 0B22F0C086701DBDE02385F4D1031180BC362C8822587A0D212F2A7E552318C1 + 82787A2D2FD7D17513843E48F740D40529F85014ABDEFA7E43EB7899FA0640D2 + 011943040B100140000FDD05C186184078DF7BB7E06340887B20033C349B4DFD + A5FF893D7600F257FBED006437BBF9844F03484FF741643FF22A86D520923896 + 20E4EB051D4C5B10DEB620238A7564810706A4BE0F680B1210191072E46242A6 + 05E92856AFE305404614ABA0E3F3DF3B7A5890BE0BD2512C5910004426040B62 + 08F9AF7A3B8A353662298295013E34E982083E0C218A6301208A613578D48BFD + 0884AC16642B865500A275BCC047C631AC94D16B644132B30B120B322084DB20 + EE801840DA827825EF2CA1670420DE8AA59B20804722585C46FFC96242DC0561 + 1CC3228E05808C8D58C087C6F6A37B20BD11ABCBE88A606513D634215EC9AB48 + 5600E42075426242DA8204421A3E3A8EA575BCF4411841C8DC86F5C702106FC3 + 327CF461C2EE821CDE512C0C089BB07A1B967A20051CB98EAEEF06905810AFE4 + CD71C2ADAD584B146B0110F74000906D0BA2C3842B84147CF45574ADE45514AB + 2DC8CD1BAEA30B42624198D399EE8234882886756B2C883B218E6165C65D905B + 0520A30782FD0884288645174410027C0444F65890D580288645093D30A21856 + C14717D1654200109910ECC76240FA2DF0D036AC44B1D695BC8690A58CAE1E48 + D6F2F6657401089D90C08726112CDE44B014C3BADD06C4EB786BB4098BD90690 + D905796C730BAB781B42BA07A2BB20CB1480DC39FA208E61DDA5DB201C27B4F9 + E8CBE880C8B809520380782DAF014483FD780813322144D7D15546073C9EF116 + 2C8D2D0820220BF204EB796341800F46DF2F08440420EA82C484D0030984CC1E + 882DC853C382603F0A409E3580D8801840DA82AC77410C1F6D402680A88C2E00 + 2182B540481B1045B228A167252F57D1D3073180147CBCF6B64184081626E44D + 47B1BA0BA2427A7740560849215DF051A31856A258DE88E50E88E78314D2E983 + A403320C482EA4177560413020F0C78E41FE3ABF1D80EC66379FE4F92AAFB761 + 298AB50088B7626140D203D10B7400200522988F6CC4E26D08F161C2F4410A3A + 5C464F1CABA0430082011180A4075200F2391D28646C40DA828C1EC802210008 + F08109511704F8088478239621E48B3F9C5D10C04326E4471340DC059971AC51 + 46AF7700083002842482E53979CF362CDEC4B07881100D5BB1BA0772AA0E130A + 40D2056900D9FFB701108C0836A44D884AE9670A420E6C00613B1626644F0CAB + B7620D0BB20088D7F27614CB10A2ABE832204B142B11AC2EA403219A8E63C984 + 9CA7C384731BD685B120F56241800F40A40D480189EF826C1B909E8300903D10 + 3256F2B60111746C034843C8BA118B32BA00440684AD588961157CF84A7A0C88 + A258819011C5721FA44DC85A441FDBB064411A3E26841C53C071EC59D7BB0F82 + 0111844C03E28D5889612D0684BB2094D10D2159CB1B03D2457401C845ACE5CD + 6ADEC4B0F696D1B1208690DB46199DBB201890B32EBD3D06A4CBE8352AA2F74C + 10910561256F2C88FA200D20CB6D9001210211E258F440DA8400210520829100 + 081604E8A0901E13C2362C47B1BC8E771810ECC7B20D4BA33896A358D81060C4 + F05153E031D6F28E1856BD32200F16883C5CEF1AC30244EA5D01A4C685F4820E + 4D7DCB823C52C33B23580211591003884C4841C7D888150BE23856C3C713F326 + 08E0B15890BE0982FDD000214B0F44A30EC8EC81288AF578A6212420D2F740B4 + 92573D1020C431ACB1098B09803CFE4CBD3503408862157C304F31051E58100E + 134E00E12EC8849067EBED28D600900FB320051D7BCBE82F3680001E5ACF0B80 + 147C0021051E630A3C5E2D08710C2B3D9011C5B209C1802892057C248AE56D58 + C0870D087741C6462CC5B06AE880C484782DAF4D886258C382D41474305AC70B + 83F0BF1D81FC557E3B00D9CD6E3EF153F091F947460052F04119BDDE4FB50DD1 + D880E82821130BF2696DC23280B09257512CA0639D02102CC8D888C50020F440 + DA84C47EFC5B41873A21DC04592C48C7B06C416A041F1DC5328068356F01486F + C40240341DC31284044014C5C28224822503D22052F3D313643F14C7D24ADE93 + 65411A445C42E7751744301208F9C62F4FD66A5EC347BFDD0559A25882105F48 + 5FBB20A38C5EE0A1C9462C0348C1882EA31780D4AB22FA0A21D89036217FE48D + 01013E0421DE86C55A5E1D25EC28D6618691860F17D21DC7FAA9B6619DB7F945 + B662A9904E242B16A421C4DBB00220FABE3806A4671A10BD2AA3D77700C4A5F4 + 0540D20761FEC00022008862580D211959109B104108062436C406A4DEC4B00E + 3FED4A6FC2AAEFEE82CC28161BB1B020051C0C2022F8088060418860D5B70D48 + E25880072022FB91013E001115D20B3A624106801478A8883E2C480348BDB21F + 3544B1801022582AA207401A427A80917440E6462C060392C9362C8E12FA8D09 + 61D68D582AA4B705B9AB20E48E091F7B4C081BB12E690B0280C880645244D756 + 2C56F202208008D33D1045B1BC19CB5D102CC8DC866500B9CFF6030B52D0E118 + 964DC8357441EA1D1604E8207A05883077C486A403A229F82086D511ACED2E48 + C7B01E338CB405A9F15D9044B16240660F04E89897D11B406440460FA421A4A7 + A3583502104C4800A421A45E1D252CE850213DC709F796D1B90BF220DBB06241 + 28A46B1B963662A507922E086B79B909621079C965746DC5AAB7BB203220DD03 + C17E0023DE8AA5227A41874D48625835CF001F8290D736CF8E6D5819D98F4048 + 66CB82A80762F8D8BB11AB0BE99890EE84A80BF2262664AF05E128619B905946 + 5717644F0CAB0DC80A20EF2A8665105121BDC0C3201203D21092EBE80D22BBDF + FFEE6F0720BBD9CDFF1706138205014464410C205ECBDBF09118960004F0C846 + 2CCC470C8820240032204416A4A358732DAFCAE8051E732B16F6030001446C40 + 661704080988C4827C9E1EC800109B1015D23B86D5F12B0188BB20DD07E942FA + 7FFDE83845B0B62C883A20CC6A410A4014C5023CDA861470D4DBA574C7B10090 + 69420421C4B004206B19DD77410420051EEE83D4A40BC21C50F0B1F641886069 + 1B16DF01111910C5B18860790C20E980C8849C65F8D04D900088E0C3512C5F47 + 0744B60F13AA0F42044B3082FDA817F0E838560187A258820F2C080604082182 + D585F4D907E9323AA3CBE805229ABE07B25A9063018F3D264406840E48000403 + B25A900048DF04390C00910909886880905810CD8C626D9B9044B1B296F7E833 + 969B200342AE33840444643F34061043880BE9E3323A16A421A4C0C320820549 + 17A4BE4FE1053E0A468860698605013C0011C3C75A461F2604F0D0D8849C2D13 + 82050140BC0D4B1012236200A9C17E30051FE75E75FBB601E1364840A457F236 + 7CF47142B66161422E29087119BDE6DA7B6D41D401C936AC9E05422EEF8D58C0 + C780907A010F2C88EC07F091B7C0C3312C4AE9B307B26D4118C0E341C7B00012 + C5B158CD1B08F9D0285681072FF74004228FC984D0010140B4929718D61E03B2 + 0521828F020FC5B00A42BA0B1200F1CC18166B795546C782D4A8884E0C4BF663 + 4F191D00013CEA9B18964104007127C451AC82900C26E4D19AD90331803CD66B + 7915C5E2AD79268574F5406C401A42DA84388615002918191684327A6258EA82 + 6040D62E88AEA337801842860149FCCA1684B70044E0E1013EBA90AE52BA7A20 + 01902D03D210820929F8D07142BE170829F00042B0206F6B0A420A3E7A25EF2C + A4D7DF051D1DC39A53F011080140E00E01882064F7FBDFFB6D36FF2F9FACFB5F + 64BE0A320000000049454E44AE426082 + } + Stretch = True + end + object imLogo: TImage + Left = 15 + Height = 64 + Top = 8 + Width = 284 + AutoSize = True + end + object lblClientVersion: TLabel + Left = 304 + Height = 37 + Top = 19 + Width = 186 + Caption = 'lblClientVersion' + Font.Color = clWhite + Font.Height = -27 + ParentColor = False + ParentFont = False + Transparent = True + end + object cbLanguage: TComboBox + Left = 877 + Height = 23 + Top = 32 + Width = 100 + Anchors = [akTop, akRight] + Enabled = True + ItemHeight = 15 + ItemIndex = 0 + Items.Strings = ( + 'English' + ) + Style = csDropDownList + OnChange = cbLanguageChange + TabOrder = 0 + Text = 'English' + end + object ilLogoImages: TImageList + Height = 64 + Width = 346 + left = 584 + top = 24 + Bitmap = { + 494C01010200030004005A014000FFFFFFFF2100FFFFFFFFFFFFFFFF424D3600 + 0000000000003600000028000000680500004000000001002000000000000068 + 0500000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF06FFFFFF06FFFFFF0400000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF06FFFFFF06FFFFFF040000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00007FFFFF0262B0F9313FA1F76D349BF69C329BF7C52F99F7E22C98F6F42C98 + F7FF2997F7FF2D99F6FD2E99F7EE309AF6D8319AF7B4379EF78A44A3F65684C1 + F419000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000FFFF7F02E4914831E07E276DDE751A9CDE7418C5DE73 + 15E2DD7011F4DD7112FFDD6F0EFFDD7113FDDD7214EEDD7316D8DE7418B4DF78 + 1F8AE1822F56EAAD701900000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000007FFFFF0254A9F745349C + F69C2D99F7E92394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2695 + F6FD319BF7CD3CA0F67A6EB2F61E000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FF7F + 7F02E58C3E45DF771B9CDE7113E9DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6C0BFDDE7417CDDF7B237AE599551E00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000007FBFFF083FA1F76D2F9AF6DC2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2695F6FC359CF6B25AACF63E00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000DFBF7F08E07E276DDE73 + 16DCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6D0CFCDE761BB2E68F453E0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000AAFFFF0347A4F5682B98F7E42394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2494F7FF369CF6BB58ABF92E0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFAA03E1812E68DD7010E4DC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6C09FFDE77 + 1CBBE390422E0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000053ABF32E329BF6CC2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2696F6FB3DA0 + F78DA9E2FE090000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000E38A3C2EDD7419CCDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6D0CFBE07C258DFEC68D09000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000000045A4 + F5712896F7F82394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2F9AF6D558ABF92B00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000E1822F71DD6E0DF8DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDD7215D5E78E412B0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000007FBFDF083A9EF7A72394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2896F6F448A6F851000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DF9F + 7F08DE7A21A7DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6E0EF4E284325100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000007FC8FE0E349BF7C32394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2695F6FD43A3F56A0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000ECA36D0EDE75 + 1BC3DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6C0BFDDF7F2B6A000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000007FB6EC0E3099F6C72394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2494F6FE43A3F76D00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000ECA36D0EDD7317C7DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6C0AFEE0802C6D0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000AAD4FF06349CF6BD2394F7FF2394F7FF2394F7FF41A2 + F8FF4CA8F8FF4CA8F8FF4CA8F8FF4CA8F8FF4CA8F8FF4CA8F8FF4AA7F8FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2595F6FD48A6F65C000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FFD4AA06DE761ABDDC6B08FFDC6B + 08FFDC6B08FFE17F29FFE28636FFE28636FFE28636FFE28636FFE28636FFE286 + 36FFE28533FFDC6B08FFDC6B08FFDC6B08FFDC6B09FFDC6B08FFDC6B08FFDC6B + 08FFDC6B09FFDC6B09FFDC6B08FFDD7011FFDE761AFFDC6B08FFDC6B08FFDC6B + 08FFDC6B09FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B09FFDC6B09FFDC6B08FFDC6B08FFDC6B09FFDC6B08FFDC6C0AFDE082 + 315C000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000003B9EF69A2394F7FF2394F7FF2394F7FF2394F7FFAAD5 + FCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF349C + F8FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2896F7F851ADFA380000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F7F7FA6CF7F8FAE9F7F8FAE9F7F8FAE9F7F8 + FAE9F7F8FAE9F8F8FB4900000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000DF7A229ADC6B08FFDC6B08FFDC6B + 08FFDC6B08FFF1C69FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFEFFDF771BFFDC6B08FFDC6B08FFF5D6BAFFEFBA8CFFDC6B08FFF2CA + A7FFFFFFFFFFFFFFFFFFE69650FFFDF6F1FFFFFEFDFFEBAD76FFDC6B08FFEEB6 + 85FFF6D9C0FFDC6B08FFEAA76DFFF9E4D2FFDD6F0EFFFCF2EAFFF0C096FFE89F + 5FFFFFFFFFFFFFFFFFFFEEB787FFDC6B08FFF9E4D2FFEBAB73FFDC6B08FFDD6E + 0DF8E3883B380000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F7FA6CF7F8FAE9F7F8 + FAE9F7F8FAE9F7F8FAE9F7F8FAE9F8F8FB490000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000048A6F65F2494F7FF2394F7FF2394F7FF2394F7FF2394F7FF7DBF + FAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62B3 + F9FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2C98F7E378BB + FE13000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB5000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000E183325FDC6B09FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFEAA76DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFE6954FFFDC6B08FFDC6B08FFF5D6BAFFEFBA8BFFDC6B08FFF2CA + A7FFF3CCAAFFE07D26FFDF781EFFE59045FFEDB17EFFF9E6D5FFDC6B08FFEEB6 + 85FFF6D9C0FFDC6B08FFEAA76DFFF9E4D2FFE89D5CFFFFFFFFFFF0C096FFE89F + 5FFFFCF2E9FFE07D26FFDE7418FFDC6B08FFF9E4D2FFEBAB73FFDC6B08FFDC6B + 08FFDD7113E3F1A15D1300000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00005FAFF7202A97F7F22394F7FF2394F7FF2394F7FF2394F7FF2394F7FF50AA + F9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8FC8 + FBFF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF379E + F5A9000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB5000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000DF974F20DD6F0FF2DC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFE3893AFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFEDB481FFDC6B08FFDC6B08FFF5D6BAFFEFBA8BFFDC6B08FFF2CA + A7FFFAE9DBFFEEB98BFFDC6B08FFE28635FFFBF0E6FFF1C49DFFDC6B08FFEEB6 + 85FFF6D9C0FFDC6B08FFEAA76DFFF9E4D2FFF6D9BFFFFAE9DAFFF0C096FFE89F + 5FFFFEF9F5FFF2C8A2FFDF771CFFDC6B08FFF9E4D2FFEBAB73FFDC6B08FFDC6B + 08FFDC6B08FFDF781FA900000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000369DF6B52394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2997 + F7FFF9FCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBCDE + FCFF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2494 + F7FF4FA8F84D0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB5000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000DE771DB5DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDD6F0EFFFEFBF9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF4D2B4FFDC6B08FFDC6B08FFF5D6BAFFEFBA8BFFDC6B08FFF2CA + A7FFF8DFCAFFE9A467FFDD6E0DFFFBEDE1FFF3CEAEFFDD6F0FFFDC6B08FFEEB6 + 85FFF6D9C0FFDC6B08FFEAA76DFFFDF5EEFFF9E8D8FFF1C49DFFF0C096FFE89F + 5FFFFDF7F1FFECAE78FFDE7416FFDC6B08FFF9E4D2FFEBAB73FFDC6B08FFDC6B + 08FFDC6B08FFDC6B09FFE487384D000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000000050A7 + F8492494F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FFD2E9FDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9F4 + FEFF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2E99F6DBD4D4FF0600000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB5000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000E3883749DC6B09FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFF8E0CCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFBF0E6FFDC6B08FFE89E5DFFF9E6D6FFF5D6BAFFE59249FFF2CA + A7FFF7DCC5FFEAA569FFE07B23FFFFFFFFFFEDB380FFE9A264FFE59148FFF5D4 + B7FFFAE9DAFFE89F5EFFEAA76DFFFFFFFFFFECAE77FFF1C49DFFF0C096FFE89F + 5FFFFDF6F0FFEAA66AFFE17F29FFEAA66AFFFBEFE5FFF3CDACFFE48C3FFFDC6B + 08FFDC6B08FFDC6B08FFDD7314DBFFD4AA060000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000FFFFFF013099 + F6CE2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FFA4D3FCFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFF3A9FF8FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF46A5F76900000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB5000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000FFFFFF01DD7316CEDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFF1C299FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFE07A21FFF2C6A1FFF6D7BCFFF6D7BCFFECB17DFFECB0 + 7CFFF6D7BCFFF5D7BCFFDD7011FFEEB584FFF9E6D6FFEAA76CFFECAF7AFFF6D7 + BCFFF6D7BCFFF2C8A3FFE79853FFF6D7BDFFDF761BFFECAD76FFEBAA71FFE591 + 48FFF6D7BCFFF6D7BCFFE59045FFF5D4B7FFF6D7BCFFF6D7BCFFEAA66BFFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFE1802E690000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000004EA9F84B2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF77BDFAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFF67B5F9FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF3099F6DFAAAAFF03000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB5000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000E387364BDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFE9A366FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFE79955FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDD7315DFFFAAAA0300000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000359DF8B92394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF4AA7F8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFF94CBFBFF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF4BA6F851000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB500000000000000000000000000000000000000000FFFF + FF04FFFFFF08FFFFFF0200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000FFFFFF06DFDF + DF08FFFFFF010000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF01FEFEFE0EF3F3F317E9E9E90C0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF01FFFFFF08FFFFFF08FFFFFF0100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF01DFDFDF08FFFFFF0600000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000FFFFFF02FFFFFF0CFFFFFF0CFFFFFF0CFFFFFF0CFFFFFF0CFEFEFE090000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000DF761BB9DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFE28534FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFEEB787FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE287355100000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000000000000000 + 000000000000FFFFFF04FFFFFF08FFFFFF020000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000FFFFFF06DFDFDF08FFFFFF01000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF01FEFEFE0EF3F3 + F317E9E9E90C0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF01FFFFFF08FFFFFF08FFFFFF010000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF01DFDFDF08FFFFFF060000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000FFFFFF02FFFFFF0CFFFFFF0CFFFFFF0CFFFFFF0CFFFF + FF0CFEFEFE090000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000006AB4F61F2696F6FD2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2796F7FFF7FBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFC1E1FDFF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF349BF6B4000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB500000000000000000F6FAFA3CF6F8FA9DF6F8FADCF7F8 + FAFAF6F7F9FDF6F7F9F8F6F7F9D5F8F8F993F5F5FA3400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000FEFEFE09F7F9F962F7F7FAB4F7F7F9E9F6F7F9FCF7F8 + F9FCF6F7F9F2F5F7F9C4F6F8F876F1F1FE130000000000000000F6F9F957F5F8 + FA71F5F8FA71F5F8FA71F5F8FA71F5F8FA70FFFFFF0300000000000000000000 + 0000E2E2E209F7F7F964F6F8F9B9F6F7F9F0F6F7F9FEF7F8FAFFF6F7F9FEF6F7 + F9EAF6F7F9ADF8F8F84EFFFFFF01000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FEFEFE19F7F7FA6DF6F7 + F9AFF6F6F9DDF6F7F9F8F6F7F9FDF6F7F9FDF6F7F9F8F6F8FADDF6F7F9AEF5F7 + F76BF3F3F3170000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F5F5F51CF6F8FA7DF6F7F9C7F7F8 + FAF2F7F7F9FCF6F7F9FCF7F7F9E8F7F7FAB0F6F6F959FFFFFF0400000000FEFE + FE07F5F8FA71F5F8FA71F5F8FA71F5F8FA71F5F8FA71F5F8F851000000000000 + 0000F9F9F92FF6F7F9FEF6F7F9FEF6F7F9FEF6F7F9FEF6F7F9FEF7F8FBC10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F0FEFE12F5F5FA37F8F8FB49F8F8FB4BF7F7 + FB40F7F7F723FFFFFF0200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000FFFFFF0AF3F9F92CF6F6FA3DF6FAFA3CF8F8FE29DFDF + DF08000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000E69C + 5A1FDC6C0BFDDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDD6E0DFFFEFAF6FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFF5D5BAFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE751AB400000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB500000000000000000F6FAFA3CF6F8 + FA9DF6F8FADCF7F8FAFAF6F7F9FDF6F7F9F8F6F7F9D5F8F8F993F5F5FA340000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FEFEFE09F7F9F962F7F7FAB4F7F7 + F9E9F6F7F9FCF7F8F9FCF6F7F9F2F5F7F9C4F6F8F876F1F1FE13000000000000 + 0000F6F9F957F5F8FA71F5F8FA71F5F8FA71F5F8FA71F5F8FA70FFFFFF030000 + 00000000000000000000E2E2E209F7F7F964F6F8F9B9F6F7F9F0F6F7F9FEF7F8 + FAFFF6F7F9FEF6F7F9EAF6F7F9ADF8F8F84EFFFFFF0100000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FEFE + FE19F7F7FA6DF6F7F9AFF6F6F9DDF6F7F9F8F6F7F9FDF6F7F9FDF6F7F9F8F6F8 + FADDF6F7F9AEF5F7F76BF3F3F317000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F5F5F51CF6F8 + FA7DF6F7F9C7F7F8FAF2F7F7F9FCF6F7F9FCF7F7F9E8F7F7FAB0F6F6F959FFFF + FF0400000000FEFEFE07F5F8FA71F5F8FA71F5F8FA71F5F8FA71F5F8FA71F5F8 + F8510000000000000000F9F9F92FF6F7F9FEF6F7F9FEF6F7F9FEF6F7F9FEF6F7 + F9FEF7F8FBC10000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F0FEFE12F5F5FA37F8F8 + FB49F8F8FB4BF7F7FB40F7F7F723FFFFFF020000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FFFFFF0AF3F9F92CF6F6FA3DF6FA + FA3CF8F8FE29DFDFDF0800000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000003E9FF8732394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FFCCE6FDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFEDF6FEFF2494F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2895F6FA87C3F0110000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF8F8FB50F5F5F51BF6F7F9B4F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAB0F5F5FE1B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F9F9FC58F7F7F9E6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9F0F5F7FA6A00000000F5F7F9C5F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000000000000000F8F8 + FB4AF7F8FAE2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8F9C1F5F5F51B0000000000000000000000000000 + 0000000000000000000000000000FEFEFE1BF5F7F8A1F7F8FAFAF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F7F9F9F6F8FA9BF4F4F418000000000000000000000000000000000000 + 00000000000000000000FFFFFF08F7F7F989F6F7F9F8F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9D7F6F6FA3AEFEF + EF10F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F7F720F5F7F987F7F7FAD7F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9F1F6F7F9B0F6F6F956FFFFFF0400000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF10F6F8F875F7F7F9CAF6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9FAF7F7F9C5F5F8FA6EFEFEFE0D000000000000000000000000000000000000 + 00000000000000000000F7F7F722F6F8F9BBF6F8F9BBF7F7FB8F000000000000 + 000000000000F8F8F994F6F8F9BBF6F8F9BBF6F6FE1D00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF08F6F8F9BBF6F8F9BBF5F7F8A500000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DF7C + 2373DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFF7DDC6FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFCF3EBFFDC6C09FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6E0EFAF0A57811000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB50F5F5F51BF6F7F9B4F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7 + FAB0F5F5FE1B0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F9F9FC58F7F7F9E6F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9F0F5F7FA6A0000 + 0000F5F7F9C5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 000000000000F8F8FB4AF7F8FAE2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9C1F5F5F51B000000000000 + 00000000000000000000000000000000000000000000FEFEFE1BF5F7F8A1F7F8 + FAFAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9F9F6F8FA9BF4F4F41800000000000000000000 + 000000000000000000000000000000000000FFFFFF08F7F7F989F6F7F9F8F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9D7F6F6FA3AEFEFEF10F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 00000000000000000000F7F7F720F5F7F987F7F7FAD7F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F1F6F7F9B0F6F6F956FFFFFF040000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF10F6F8F875F7F7F9CAF6F7F9FCF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9FAF7F7F9C5F5F8FA6EFEFEFE0D00000000000000000000 + 000000000000000000000000000000000000F7F7F722F6F8F9BBF6F8F9BBF7F7 + FB8F000000000000000000000000F8F8F994F6F8F9BBF6F8F9BBF6F6FE1D0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF08F6F8F9BBF6F8F9BBF5F7F8A50000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000329BF7C02394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF9FD0FBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFF3EA1F8FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF42A2F6580000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F9FB88F6F7F9E9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9EBF6F6 + FA3C00000000000000000000000000000000000000000000000000000000FFFF + FF03F8F8F997F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAA7F7F8F9CBF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F5F8FA71F6F7 + F9FDF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FADEF4F4F41900000000000000000000 + 00000000000000000000F5F5F851F6F7F9F0F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAECF8F8FB4C0000000000000000000000000000 + 000000000000F4F4F419F6F7FACCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FAF6F6 + F878F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000FFFFFF0CF6F8 + F992F6F7F9FAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FADCF8F8FB4B000000000000 + 00000000000000000000000000000000000000000000FFFFFF01F7F7FA6AF6F7 + F9EFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAEAF6F6F95F0000000000000000000000000000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DF74 + 19C0DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFF0BE93FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFE07D26FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE27F2B58000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9FB88F6F7F9E9F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F7F9EBF6F6FA3C00000000000000000000000000000000000000000000 + 000000000000FFFFFF03F8F8F997F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAA7F7F8F9CBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F5F8FA71F6F7F9FDF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FADEF4F4F4190000 + 000000000000000000000000000000000000F5F5F851F6F7F9F0F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAECF8F8FB4C000000000000 + 0000000000000000000000000000F4F4F419F6F7FACCF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F7F9FAF6F6F878F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000FFFFFF0CF6F8F992F6F7F9FAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FADCF8F8 + FB4B00000000000000000000000000000000000000000000000000000000FFFF + FF01F7F7FA6AF6F7F9EFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAEAF6F6F95F000000000000 + 000000000000000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000AABFE90C2896F6FA2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF72BAFAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFF6CB8FAFF2394F7FF2394F7FF2394F7FF2394F7FF50AAF9FF5BAF + F9FF309AF7FF2394F7FF2394F7FF2394F7FF2495F7FF54ACF9FF5AAFF9FF2C98 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF349CF69B0000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAF3FAFAFA3B0000000000000000000000000000000000000000FFFFFF01F7F8 + FAA2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF06F6F6F959F6F7F9FEF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B900000000000000000000 + 000000000000F7F9FC61F7F8FAFCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCF7F7F96100000000000000000000 + 0000F3F3F316F6F7F9D9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000000000000000000000000000F8F8FE25F6F7FAD8F7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9FDF7F7F9C7F6F7F98EF8F8FA6FF7F7FA67F6F6 + F878F6F8FA9FF7F7F9E2F7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF6F7F990FFFF + FF0200000000000000000000000000000000FFFFFF06F7F7FAA8F7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9FCF7F7F9C0F7F7F986F7F7FA6AF7F7FA6CF5F7F98BF6F7 + F9C8F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF8F8F999FFFFFF03000000000000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000E9BF940CDC6F + 0EFADC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFE8A061FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFE89C5AFFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFE3893AFFE59147FFDE7416FFDC6B08FFDC6B08FFDC6B08FFDC6C09FFE48C + 3FFFE59046FFDD7112FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE761B9B000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAF3FAFAFA3B000000000000000000000000000000000000 + 0000FFFFFF01F7F8FAA2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF06F6F6 + F959F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B90000 + 0000000000000000000000000000F7F9FC61F7F8FAFCF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCF7F7F9610000 + 00000000000000000000F3F3F316F6F7F9D9F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000000000000000000000000000F8F8 + FE25F6F7FAD8F7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDF7F7F9C7F6F7F98EF8F8 + FA6FF7F7FA67F6F6F878F6F8FA9FF7F7F9E2F7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9FEF6F7F990FFFFFF0200000000000000000000000000000000FFFFFF06F7F7 + FAA8F7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCF7F7F9C0F7F7F986F7F7FA6AF7F7 + FA6CF5F7F98BF6F7F9C8F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF8F8F999FFFF + FF0300000000000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000055AAF63C2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF45A4F8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFF9ACEFBFF2394F7FF2394F7FF2394F7FF6EB8FAFFFFFFFFFFFFFF + FFFFCEE7FDFF2394F7FF2394F7FF2394F7FF81C2FAFFFFFFFFFFFFFFFFFFBBDE + FCFF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF319BF7D40000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAEAF5F5F51C00000000000000000000000000000000F6F8FA7AF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF06F8F8F825F7F8FAE8F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6FAFA3C000000000000 + 0000F7F7FB48F6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCF7F7FB4100000000FFFF + FF04F7F8F9C2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 000000000000000000000000000000000000F8F8FE25F7F8FAE8F7F8FAFFF7F8 + FAFFF6F7F9FDF8F8F998F7F7FE21000000000000000000000000000000000000 + 000000000000FFFFFF02F7F7FB46F7F8F9CAF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + FA9EFFFFFF010000000000000000FFFFFF02F7F7F9AEF7F8FAFFF7F8FAFFF6F7 + F9FEF6F8FA9BF6F6FE1F00000000000000000000000000000000000000000000 + 0000F8F8F827F5F7F8A9F7F8FAFFF7F8FAFFF7F8FAFFF6F6FA9A000000000000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000E58C3B3CDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFE1822EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFEFBB8DFFDC6B08FFDC6B08FFDC6B08FFE89D + 5CFFFFFFFFFFFFFFFFFFF7DEC8FFDC6B08FFDC6B08FFDC6B08FFEBAA72FFFFFF + FFFFFFFFFFFFF4D1B3FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE7416D4000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAEAF5F5F51C0000000000000000000000000000 + 0000F6F8FA7AF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF06F8F8 + F825F7F8FAE8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6FA + FA3C0000000000000000F7F7FB48F6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCF7F7 + FB4100000000FFFFFF04F7F8F9C2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000F8F8FE25F7F8 + FAE8F7F8FAFFF7F8FAFFF6F7F9FDF8F8F998F7F7FE2100000000000000000000 + 0000000000000000000000000000FFFFFF02F7F7FB46F7F8F9CAF7F8FAFFF7F8 + FAFFF7F8FAFFF6F8FA9EFFFFFF010000000000000000FFFFFF02F7F7F9AEF7F8 + FAFFF7F8FAFFF6F7F9FEF6F8FA9BF6F6FE1F0000000000000000000000000000 + 00000000000000000000F8F8F827F5F7F8A9F7F8FAFFF7F8FAFFF7F8FAFFF6F6 + FA9A00000000000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000003D9EF56C2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2695F7FFF4F9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFC7E4FDFF2394F7FF2394F7FF2394F7FF69B6F9FFFFFFFFFFFFFF + FFFFFBFDFFFF2B98F7FF2394F7FF2394F7FF7CBFFAFFFFFFFFFFFFFFFFFFF1F8 + FEFF2495F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2996F6FBBFDFFF0800000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAECF8F8F993F6F9F95CF8F8 + FB4FF7F7FA66F7F7FAAAF7F8FAFAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F9FAB3000000000000000000000000F9F9F92CF7F8F9FAF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F8D0F6F6F873F7F7 + FB46F7F7FB40F6F6F95FF6F7F9AAF6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F9F9F931F6F7 + F9F1F7F8FAFFF7F8FAFFF7F8FAFFF7F7F9C1F9F9FC57F0F7F724FAFAFA33F6F8 + FA9EF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9FB8D00000000F3F3 + F316F6F7F9EBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7 + F9E7F7F7FB8EF6F9F957F6F6FA3FF7F7F743F7F7F961F6F8FA9EF6F7F9F2F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF7F7F984FFFFFF0600000000F6F6 + F872F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDF7F7 + FAAFF7F9F961F7FBFB40F7F7FB45F5F8F870F7F7F9C9F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000FEFEFE0DF6F7FAD9F7F8FAFFF7F8FAFFF7F8 + F9E9F6F6FA3E0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF03F6F8F87FF6F7F9FEF7F8FAFFF7F8 + FAFFF7F7FA670000000000000000F6F8FA7DF7F8FAFFF7F8FAFFF6F7F9F6F8F8 + FB4D000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F6F9F95FF6F7F9FBF7F8FAFFF7F8FAFFF7F7FA670000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DD7A236CDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6D0BFFFDF7F2FFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D9C0FFDC6B08FFDC6B08FFDC6B08FFE79A + 56FFFFFFFFFFFFFFFFFFFEFCFAFFDD7011FFDC6B08FFDC6B08FFEAA76BFFFFFF + FFFFFFFFFFFFFDF5EFFFDC6C09FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6E0FFBFFDFBF080000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAECF8F8 + F993F6F9F95CF8F8FB4FF7F7FA66F7F7FAAAF7F8FAFAF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9FAB3000000000000000000000000F9F9 + F92CF7F8F9FAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F8D0F6F6F873F7F7FB46F7F7FB40F6F6F95FF6F7F9AAF6F7F9FCF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F9F9F931F6F7F9F1F7F8FAFFF7F8FAFFF7F8FAFFF7F7F9C1F9F9FC57F0F7 + F724FAFAFA33F6F8FA9EF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9 + FB8D00000000F3F3F316F6F7F9EBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F7F9E7F7F7FB8EF6F9F957F6F6FA3FF7F7F743F7F7F961F6F8 + FA9EF6F7F9F2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF7F7F984FFFF + FF0600000000F6F6F872F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F7F9FDF7F7FAAFF7F9F961F7FBFB40F7F7FB45F5F8F870F7F7F9C9F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC200000000000000000000000000000000FEFEFE0DF6F7FAD9F7F8 + FAFFF7F8FAFFF7F8F9E9F6F6FA3E000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF03F6F8F87FF6F7 + F9FEF7F8FAFFF7F8FAFFF7F7FA670000000000000000F6F8FA7DF7F8FAFFF7F8 + FAFFF6F7F9F6F8F8FB4D00000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F6F9F95FF6F7F9FBF7F8FAFFF7F8 + FAFFF7F7FA67000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000379DF6932394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FFC7E4FDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF1F8FEFF2495F7FF2394F7FF2394F7FF3EA1F8FFFFFFFFFFFFFF + FFFFFFFFFFFF54ACF9FF2394F7FF2394F7FF51AAF9FFFFFFFFFFFFFFFFFFFFFF + FFFF40A2F8FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF6CB2F82800000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAA3EEFFFF0F00000000000000000000 + 00000000000000000000F8F8F82AF6F7FAD4F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6FAFA3C0000000000000000F7F7FAAEF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDF5F8F871FFFFFF01000000000000 + 0000000000000000000000000000F9F9F931F6F8F9DEF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000000000000000F7F7 + F740F6F7F9F7F6F7F9F4F6F6F95B00000000000000000000000000000000FFFF + FF01F6F7FACCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9B500000000F6F8 + F997F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9FB89E2E2 + FE09000000000000000000000000000000000000000000000000FFFFFF11F8F8 + FA9BF7F8FAFFF7F8FAFFF7F7FAD6F5F5F5360000000000000000EBEBEB0DF6F7 + F9EEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE9F6FAFA3C0000 + 00000000000000000000000000000000000000000001F9F9FC60F6F7F9F8F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000F8F8F993F7F8FAFFF7F8FAFFF6F7F9EEF2F2 + F82A000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F7FA67F6F7F9FEF7F8 + FAA0FFFFFF1000000000F8F8F82AF6F7F9FAF7F8FAFFF6F7F9FDF8F8FB4C0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F7F9F961F7F8FAFFF7F8FAFFF7F8FAF2FEFE + FE1A0000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DE771F93DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFF6D9C0FFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF6EFFFDC6C09FFDC6B08FFDC6B08FFE07D + 26FFFFFFFFFFFFFFFFFFFFFFFFFFE48C3FFFDC6B08FFDC6B08FFE38A3CFFFFFF + FFFFFFFFFFFFFFFFFFFFE17F29FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE59952280000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAA3EEFFFF0F0000 + 000000000000000000000000000000000000F8F8F82AF6F7FAD4F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6FAFA3C0000000000000000F7F7 + FAAEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDF5F8F871FFFF + FF010000000000000000000000000000000000000000F9F9F931F6F8F9DEF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 000000000000F7F7F740F6F7F9F7F6F7F9F4F6F6F95B00000000000000000000 + 000000000000FFFFFF01F6F7FACCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9B500000000F6F8F997F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F9FB89E2E2FE0900000000000000000000000000000000000000000000 + 0000FFFFFF11F8F8FA9BF7F8FAFFF7F8FAFFF7F7FAD6F5F5F536000000000000 + 0000EBEBEB0DF6F7F9EEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAE9F6FAFA3C000000000000000000000000000000000000000000000001F9F9 + FC60F6F7F9F8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC200000000000000000000000000000000F8F8F993F7F8FAFFF7F8 + FAFFF6F7F9EEF2F2F82A00000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F7 + FA67F6F7F9FEF7F8FAA0FFFFFF1000000000F8F8F82AF6F7F9FAF7F8FAFFF6F7 + F9FDF8F8FB4C0000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F9F961F7F8FAFFF7F8 + FAFFF7F8FAF2FEFEFE1A0000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000349BF6AF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF9ACEFBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFEBF5FEFFEAF5FEFFEAF5FEFFEBF5FEFFFFFFFFFFFFFF + FFFFFFFFFFFFF1F8FEFFEAF5FEFFEAF5FEFFECF6FEFFFFFFFFFFFFFFFFFFFFFF + FFFFEFF7FEFFEAF5FEFFEAF5FEFFEAF5FEFFEAF5FEFFEAF5FEFFDDEEFEFFB8DC + FCFF82C2FAFF3B9FF8FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF48A7F74600000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F7FAA0FFFFFF010000000000000000000000000000 + 0000000000000000000000000000F2F2F214F6F8F9DDF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F7FAA600000000FEFEFE1CF6F7F9FDF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FA720000000000000000000000000000 + 000000000000000000000000000000000000F6F6F61EF6F7F9E9F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000000000000000 + 0000F8F8FB4EF6FAFA3A0000000000000000000000000000000000000000FFFF + FF03F6F7F9D8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B9F3F3F316F7F8 + FAF8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FA77000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F6F8F878F5F7F98DFEFEFE09000000000000000000000000F7F9FC63F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F6F9F9F930000000000000 + 0000000000000000000000000000000000000000000000000000F8F8FB52F6F7 + F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000F7F7F721F6F7F9FBF7F8FAFFF7F8FAFFF8F8FB530000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F3F3F92C0000 + 00000000000000000000F6F7F9AAF7F8FAFFF7F8FAFFF6F8F992000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F7F7FAA8F7F8FAFFF7F8FAFFF6F8 + F9940000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DE761AAFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFEFBB8DFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF1E8FFFCF1E7FFFCF1E7FFFCF1 + E8FFFFFFFFFFFFFFFFFFFFFFFFFFFDF5EFFFFCF1E7FFFCF1E7FFFCF2EAFFFFFF + FFFFFFFFFFFFFFFFFFFFFCF4EDFFFCF1E7FFFCF1E7FFFCF1E7FFFCF1E7FFFCF1 + E7FFFAE8D9FFF4CFAFFFEBAB73FFE07B23FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE18632460000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAA0FFFFFF01000000000000 + 00000000000000000000000000000000000000000000F2F2F214F6F8F9DDF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAA600000000FEFEFE1CF6F7 + F9FDF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FA72000000000000 + 0000000000000000000000000000000000000000000000000000F6F6F61EF6F7 + F9E9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 00000000000000000000F8F8FB4EF6FAFA3A0000000000000000000000000000 + 000000000000FFFFFF03F6F7F9D8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B9F3F3F316F7F8FAF8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + FA77000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F6F8F878F5F7F98DFEFEFE0900000000000000000000 + 0000F7F9FC63F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F6F9F9 + F930000000000000000000000000000000000000000000000000000000000000 + 0000F8F8FB52F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000F7F7F721F6F7F9FBF7F8FAFFF7F8 + FAFFF8F8FB530000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F3F3F92C000000000000000000000000F6F7F9AAF7F8FAFFF7F8FAFFF6F8 + F992000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7FAA8F7F8 + FAFFF7F8FAFFF6F8F9940000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002E98F5C02394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF6DB8FAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFBFDFFFFADD7FCFF359DF8FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF45A3F65C00000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAE5FEFEFE0B000000000000000000000000000000000000 + 000000000000000000000000000000000000F7F7FB43F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAF0FFFFFF04F7F7FA68F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9C8FFFFFF010000000000000000000000000000 + 00000000000000000000000000000000000000000000F6F9F95CF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000000000000000 + 0000000000000000000000000000000000000000000000000000FFFFFF06F6F8 + FA9DF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FA9EF5F7FA69F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9C3FFFFFF01000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F9FAB2F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6F87F00000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F7 + FAA7F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000F7F9FB83F7F8FAFFF7F8FAFFF5F7F9C2000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F3F3F317F6F7F9FBF7F8FAFFF7F8F9F5F2F2F214000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F7F7F720F6F7F9FBF7F8FAFFF7F8 + FAF1E2E2E20900000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DD7215C0DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE89D5BFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFCFBFFF2C8A3FFDF771CFFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE0822F5C0000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE5FEFEFE0B00000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7FB43F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAF0FFFFFF04F7F7FA68F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9C8FFFFFF01000000000000 + 000000000000000000000000000000000000000000000000000000000000F6F9 + F95CF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000FFFFFF06F6F8FA9DF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + FA9EF5F7FA69F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9C3FFFF + FF01000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F9FAB2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6F87F0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F7F7FAA7F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000F7F9FB83F7F8FAFFF7F8FAFFF5F7 + F9C2000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F3F3F317F6F7F9FBF7F8FAFFF7F8F9F5F2F2 + F214000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7F720F6F7 + F9FBF7F8FAFFF7F8FAF1E2E2E20900000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000349CF6D12394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF40A2F8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFE5F2FEFF41A3F8FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF3DA0F76400000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F7F98100000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F6F7F8D1F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF9F9F92DF7F7FAA0F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F6F95B000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF04F7F8FAEAF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000000000000000 + 000000000000000000000000000000000000FFFFFF01F8F8FB53F7F7FAD7F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F9F95CF7F8FAA9F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB5100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000001F7F8F9EBF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FAF2F2F21400000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6FA + FA39F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000F6F7F8D0F7F8FAFFF7F8FAFFF9F9FC55000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F6F9F95EF7F8FAFFF7F8FAFFF7F7F8A700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F6F8F9B7F7F8FAFFF7F8 + FAFFF7F7FB4800000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DE761CD1DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE17E29FFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBEEE2FFE17F2AFFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE07C26640000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7F9810000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6F7 + F8D1F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF9F9F92DF7F7FAA0F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6F95B00000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF04F7F8FAEAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000000000000000000000000000000000000000000000000000FFFFFF01F8F8 + FB53F7F7FAD7F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F9 + F95CF7F8FAA9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FB510000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0001F7F8F9EBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FAF2F2F2140000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F6FAFA39F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000F6F7F8D0F7F8FAFFF7F8FAFFF9F9 + FC55000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F6F9F95EF7F8FAFFF7F8FAFFF7F7F8A70000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6F8 + F9B7F7F8FAFFF7F8FAFFF7F7FB4800000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000369DF7D42394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2494F7FFEFF7FEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6EBFDFF2695F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF3DA0F76400000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFFAFAFA3D00000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F9FB8BF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F5F852F7F7F9C4F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF3F3F317000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F6F7F9ADF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000000000000000 + 0000000000000000000000000000F8F8FB49F7F7FACDF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE2DFDFDF08F7F8FAD3F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDFEFEFE0D00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF0FF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FACC0000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF04F7F7F9F6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 000000000000FEFEFE07F6F7F9FBF7F8FAFFF6F7F9FBEBEBFE0D000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F8F8F992F7F8FAFFF7F8FAFFF7F7F96400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA75F7F8FAFFF7F8 + FAFFF6F8FA7C00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F82800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DE781ED4DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6C09FFFCF4EDFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E3D1FFDC6D + 0BFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE07C26640000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFFAFAFA3D0000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F9 + FB8BF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F5F852F7F7F9C4F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF3F3F31700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F6F7F9ADF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 00000000000000000000000000000000000000000000F8F8FB49F7F7FACDF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE2DFDF + DF08F7F8FAD3F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDFEFEFE0D0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF0FF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FACC000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF04F7F7F9F6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000FEFEFE07F6F7F9FBF7F8FAFFF6F7F9FBEBEB + FE0D000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8F992F7F8FAFFF7F8FAFFF7F7F9640000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F8F8 + FA75F7F8FAFFF7F8FAFFF6F8FA7C00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF8F8F8280000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000003099F6C72394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FFC2E1FDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6EB8FAFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF45A4F76300000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF5FEFE1B00000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7FAFA6AF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF4F7F964F7F8FAD4F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9F500000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7F98EF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000000000000000 + 000000000000F5F5F51CF7F7FAB5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF7F7FB4600000000F6F7F9E9F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAEF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F7FF20F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAAC0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F9FAD8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 000000000000F5F5FE1CF7F8FAFFF7F8FAFFF8F8FADB00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F6F7F9B0F7F8FAFFF7F8FAFFF6FAFA3E00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F5F8F853F7F8FAFFF7F8 + FAFFF6F8FA9A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF3F9F92B00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DD7315C7DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFF5D6BAFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE89D + 5CFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE0832E630000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5FEFE1B0000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7FA + FA6AF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF4F7F964F7F8FAD4F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F50000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F7F98EF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000000000000000000000000000F5F5F51CF7F7FAB5F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF7F7FB460000 + 0000F6F7F9E9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAEF000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F7 + FF20F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAAC000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F7F9FAD8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000F5F5FE1CF7F8FAFFF7F8FAFFF8F8FADB0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F6F7F9B0F7F8FAFFF7F8FAFFF6FAFA3E0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F5F8 + F853F7F8FAFFF7F8FAFFF6F8FA9A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF3F9F92B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000329BF6B92394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF95CBFBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFDFFFFF9FCFFFFF9FCFFFFF9FCFFFFF9FC + FFFFF9FCFFFFF9FCFFFFF9FCFFFFF9FCFFFFF9FCFFFFF9FCFFFFF9FCFFFFF9FC + FFFFF9FCFFFFF9FCFFFFF9FCFFFFFEFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2DAFCFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF43A1F54F00000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF3F3FE1700000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F7FA67F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9F965F6F7F9D6F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9F200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7F98BF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000000000000000 + 0000FAFAFA3CF7F8F9EBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAF3F5F8F8520000000000000000F7F8FAEAF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9F1FFFFFF0100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F7F723F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAA90000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F9FAD5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 000000000000F7F7F723F7F8FAFFF7F8FAFFF7F7F9CA00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F6F8F9BBF7F8FAFFF7F8FAFFFAFAFA3300000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8F849F7F8FAFFF7F8 + FAFFF7F8FAA500000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFFAFAFA3300000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000EEEEFF0FF7F8FAFFF7F8FAFFF7F8F9E100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DD751AB9DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFEEB788FFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFCFAFFFEFBF8FFFEFB + F8FFFEFBF8FFFEFBF8FFFEFBF8FFFEFBF8FFFEFBF8FFFEFBF8FFFEFBF8FFFEFB + F8FFFEFBF8FFFEFBF8FFFEFBF8FFFEFBF8FFFEFBF9FFFFFEFEFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3CB + A9FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE7D294F0000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF3F3FE170000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F7 + FA67F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9F965F6F7F9D6F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F20000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F7F98BF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 00000000000000000000FAFAFA3CF7F8F9EBF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAF3F5F8F852000000000000 + 0000F7F8FAEAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9F1FFFFFF010000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F7 + F723F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAA9000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F7F9FAD5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000F7F7F723F7F8FAFFF7F8FAFFF7F7F9CA0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F6F8F9BBF7F8FAFFF7F8FAFFFAFAFA330000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F8F8 + F849F7F8FAFFF7F8FAFFF7F8FAA500000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFFAFAFA330000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000EEEEFF0FF7F8FAFFF7F8FAFFF7F8F9E10000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000339CF69E2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF68B5F9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFF77BDFAFF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2494F7FF319BF7FF73BBFAFFEFF7FEFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF5CB0FA3700000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF4F4F93100000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F6F8FA7FF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6F958F7F8F9C9F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9FEFEFEFE0D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7FAA5F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000000000000000F5F5 + FA34F6F7F9F4F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F8F9BFF8F8F826000000000000000000000000F6F7FAD9F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF3F3FE1600000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F2FEFE15F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7F9C30000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000007F7F + 7F02F6F7F9F1F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 000000000000F2F2FE15F7F8FAFFF7F8FAFFF6F7FAD600000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F6F7F9B0F7F8FAFFF7F8FAFFF7F7FB4200000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F9F9FC59F7F8FAFFF7F8 + FAFFF6F8FA9A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7FBFB4100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F0F0F012F7F8FAFFF7F8FAFFF6F8F9DE00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DE75199EDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE79955FFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9A466FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B09FFDE7417FFE9A162FFFCF4 + EDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE78F45370000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF4F4F9310000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6F8 + FA7FF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6F958F7F8F9C9F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEFEFEFE0D00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F7FAA5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 000000000000F5F5FA34F6F7F9F4F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9BFF8F8F82600000000000000000000 + 0000F6F7FAD9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF3F3FE160000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F2FE + FE15F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7F9C3000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000007F7F7F02F6F7F9F1F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000F2F2FE15F7F8FAFFF7F8FAFFF6F7FAD60000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F6F7F9B0F7F8FAFFF7F8FAFFF7F7FB420000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F9F9 + FC59F7F8FAFFF7F8FAFFF6F8FA9A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7FBFB410000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F0F0F012F7F8FAFFF7F8FAFFF6F8F9DE0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000399FF67D2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF3A9FF8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFA4D3FCFF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF64B4F9FFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2594F6FE93C9F11300000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7FAFA6900000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F6F8FABAF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6FA3AF6F7F9ACF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF4F8F84A000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF01F7F7F9E0F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000FFFFFF06F6F7 + FADAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FACFF5F8F84FFFFFFF0100000000000000000000000000000000F7F7FAB3F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7F96400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF02F6F7F9F5F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F4E2E2E20900000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F3F9 + F92CF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 000000000000FFFFFF02F6F7F9F2F7F8FAFFF7F7F9F6FFFFFF05000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F8F8FB92F7F8FAFFF7F8FAFFF7F7FA6900000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F9FB84F7F8FAFFF7F8 + FAFFF6F6F87D00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF6F9F95A00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE1AF7F8FAFFF7F8FAFFF6F7FAD700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DE7A207DDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE07B22FFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C299FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE697 + 51FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6C0AFEE4AE86130000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7FAFA690000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6F8 + FABAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6FA3AF6F7F9ACF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF4F8F84A00000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF01F7F7F9E0F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000FFFFFF06F6F7FADAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FACFF5F8F84FFFFFFF010000000000000000000000000000 + 0000F7F7FAB3F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7F9640000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000FFFF + FF02F6F7F9F5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F4E2E2E2090000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F3F9F92CF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000FFFFFF02F6F7F9F2F7F8FAFFF7F7F9F6FFFF + FF05000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FB92F7F8FAFFF7F8FAFFF7F7FA690000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F9 + FB84F7F8FAFFF7F8FAFFF6F6F87D00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF6F9F95A0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE1AF7F8FAFFF7F8FAFFF6F7FAD70000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000045A3F5512394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FFEAF5FEFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFD1E9FDFF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2696F7FFF9FCFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2F99F7E8FFFFFF0100000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F7FACCFFFFFF01000000000000000000000000000000000000 + 000000000000000000000000000000000000F7F7FE24F7F8FAFCF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9FBEBEBEB0DF6F6F87CF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F9FAB0000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F7F7FB48F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F9F9FC5BF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FAD3F5F5F852FFFF + FF01000000000000000000000000000000000000000000000000F6F6F877F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9DADFDFDF08000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F8FBC6F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FA6600000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F8F8 + FB92F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000F6F6F9BAF7F8FAFFF7F8FAFFF7FBFB42000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F9F9FC5FF7F8FAFFF7F8FAFFF7F7FAAE00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F6F7FACEF7F8FAFFF7F8 + FAFFF4F8F84A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7F7F98600000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F2F2F82AF7F8FAFFF7F8FAFFF7F8F9C800000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000E2812F51DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFFCF1E7FFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E0CCFFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDD6D + 0CFFFEFBF8FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE7314E8FFFFFF010000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FACCFFFFFF0100000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7FE24F7F8 + FAFCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBEBEBEB0DF6F6F87CF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F9FAB000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F7F7 + FB48F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F9F9FC5BF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7 + FAD3F5F5F852FFFFFF0100000000000000000000000000000000000000000000 + 0000F6F6F877F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9DADFDF + DF08000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F8FBC6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FA660000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F8F8FB92F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000F6F6F9BAF7F8FAFFF7F8FAFFF7FB + FB42000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F9F9FC5FF7F8FAFFF7F8FAFFF7F7FAAE0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6F7 + FACEF7F8FAFFF7F8FAFFF4F8F84A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7F7F9860000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F2F2F82AF7F8FAFFF7F8FAFFF7F8F9C80000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000077BBEE1E2595F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FFBCDFFDFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFF9FCFFFF2896F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FFF0F7FEFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF329BF7B40000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F7FA6C000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF03F6F8F9BBF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8F9C400000000F5FAFA37F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F5F8540000000000000000000000000000 + 000000000000000000000000000000000000FFFFFF10F6F8F9DBF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F7F7FAA2F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF8F8FA6EFFFFFF03000000000000 + 000000000000D4D4D40600000000000000000000000000000000F7F7FE24F6F7 + F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FA9EFFFFFF010000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F7F746F6F6F95EFFFFFF01000000000000000000000000F7F7F982F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAEAF6F6F61D000000000000 + 0000000000000000000000000000000000000000000000000000FAFAFA3AF6F7 + F9FBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000F5F7FA69F7F8FAFFF7F8FAFFF6F7F9AD000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F4F4F419F6F7F9FCF7F8FAFFF6F7F9F9F5F5FE1C000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FAFAFA3BF7F8FAFFF7F8FAFFF7F8 + F9F3FFFFFF0A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF6F7F8D400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F7F7FB45F7F8FAFFF7F8FAFFF7F7FAB000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000E5A1661EDC6C + 0AFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFF4D2B4FFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFBF8FFDD6E0EFFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFFCF5EEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE7519B4000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FA6C00000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF03F6F8F9BBF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9C400000000F5FAFA37F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F5F854000000000000 + 0000000000000000000000000000000000000000000000000000FFFFFF10F6F8 + F9DBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F7F7FAA2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF8F8FA6EFFFF + FF03000000000000000000000000D4D4D4060000000000000000000000000000 + 0000F7F7FE24F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + FA9EFFFFFF010000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F7F746F6F6F95EFFFFFF0100000000000000000000 + 0000F7F7F982F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAEAF6F6 + F61D000000000000000000000000000000000000000000000000000000000000 + 0000FAFAFA3AF6F7F9FBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000F5F7FA69F7F8FAFFF7F8FAFFF6F7 + F9AD000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F4F4F419F6F7F9FCF7F8FAFFF6F7F9F9F5F5 + FE1C000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000FAFAFA3BF7F8 + FAFFF7F8FAFFF7F8F9F3FFFFFF0A00000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF6F7F8D40000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F7F7FB45F7F8FAFFF7F8FAFFF7F7FAB00000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FFFFFF013099F6DE2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF8FC9FBFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4FA9F9FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FFF0F7FEFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF3A9FF6750000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9FBF7F7F9640000000000000000000000000000 + 00000000000000000000FEFEFE09F7F8FAA4F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F7FA6700000000FFFFFF02F6F7FAD9F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F6F6F6F95500000000000000000000 + 0000000000000000000000000000F3F3F317F7F7F9C7F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F6F8F9BDF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9BB0000000000000000000000000000 + 0000F7F7F722F6F7FAD4F7F7FA6700000000000000000000000000000000F7F7 + FAB3F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F7F8A7F0F0 + F011000000000000000000000000000000000000000000000000FFFFFF01F7F7 + FA6AF6F7F9F9F7F8FAFFF7F8FACCF7F7FB400000000000000000F8F8F827F6F7 + F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9DAF8F8FE270000 + 00000000000000000000000000000000000000000000F6F6F63DF7F8FAECF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000FEFEFE0EF6F7F9EFF7F8FAFFF6F7F9FEF7F7F7420000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7F7220000 + 00000000000000000000F7F9FAAFF7F8FAFFF7F8FAFFF7F8FAA8000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF05F6F7FACCF7F8FAFFF7F8FAFFF6F8 + F9990000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7F8FAFFF7FBFB47000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F6F6F87BF7F8FAFFF7F8FAFFF7F7F98B00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000FFFFFF01DD74 + 16DEDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFEDB482FFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE38839FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFFCF5EEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE07A2275000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF7F7F964000000000000 + 000000000000000000000000000000000000FEFEFE09F7F8FAA4F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FA6700000000FFFFFF02F6F7 + FAD9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F6F6F6F9550000 + 00000000000000000000000000000000000000000000F3F3F317F7F7F9C7F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F6F8F9BDF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9BB000000000000 + 00000000000000000000F7F7F722F6F7FAD4F7F7FA6700000000000000000000 + 000000000000F7F7FAB3F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF5F7F8A7F0F0F01100000000000000000000000000000000000000000000 + 0000FFFFFF01F7F7FA6AF6F7F9F9F7F8FAFFF7F8FACCF7F7FB40000000000000 + 0000F8F8F827F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9DAF8F8FE27000000000000000000000000000000000000000000000000F6F6 + F63DF7F8FAECF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000FEFEFE0EF6F7F9EFF7F8FAFFF6F7 + F9FEF7F7F7420000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F7F7F722000000000000000000000000F7F9FAAFF7F8FAFFF7F8FAFFF7F8 + FAA8000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF05F6F7FACCF7F8 + FAFFF7F8FAFFF6F8F9990000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7F8FAFFF7FB + FB47000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F6F6F87BF7F8FAFFF7F8FAFFF7F7F98B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000379CF6942394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF53ABF9FFB8DDFCFFB8DDFCFFB8DD + FCFFB8DDFCFFB8DDFCFFB8DDFCFFB8DDFCFF5BAFF9FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FFF1F8FEFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF62B3F92C0000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6F9BFF6F6F95CF8F8F829FEFE + FE1AF5F5FA34F6F6F874F6F8FADFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9E5FEFEFE090000000000000000F7F7F962F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF7F7FAB5F6F9F955F8F8 + F825F5F5F51CF6F6FA3CF7F9FB83F6F7F9EDF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F7F7FAAFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F2F7F7FB45FFFFFF05FEFEFE16F8F8 + FA75F6F7F9F1F7F8FAFFF6F7F9FDF7F7FA66000000000000000000000000F3F9 + F92CF6F7F9FAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAEEF6F8F994F5F8F852F4F4F932F3F3F92EF7F7FB42F6F8F876F6F7FAD1F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF6F7FAB1F8F8F82700000000F6F7 + F9ADF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F6F8F8 + F993F7F7FB43F6F6FE1DF0F7F723F8F8FB4DF7F8FAA4F6F7F9FCF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000F8F8FA71F7F8FAFFF7F8FAFFF7F7F9E7F7F7 + F721000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F6F9F955F6F7F9FCF7F7 + FAA4F3F3F31700000000F9F9F930F6F7F9FDF7F8FAFFF7F8FAFFF8F8FA700000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000FFFFFF01F6F8F997F7F8FAFFF7F8FAFFF6F7F9F6F6F6 + FE1F0000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7F8FAFFF7F7FADAFEFEFE0E0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000FFFFFF01F6F7FAD1F7F8FAFFF7F8FAFFF5F5F85300000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DE76 + 1D94DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE48B3EFFF4D0 + B0FFF4D0B0FFF4D0B0FFF4D0B0FFF4D0B0FFF4D0B0FFF4D0B0FFE59047FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFFDF6F0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE7964B2C000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F6F9BFF6F6 + F95CF8F8F829FEFEFE1AF5F5FA34F6F6F874F6F8FADFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9E5FEFEFE090000000000000000F7F7 + F962F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF7F7 + FAB5F6F9F955F8F8F825F5F5F51CF6F6FA3CF7F9FB83F6F7F9EDF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F7F7FAAFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F2F7F7FB45FFFF + FF05FEFEFE16F8F8FA75F6F7F9F1F7F8FAFFF6F7F9FDF7F7FA66000000000000 + 000000000000F3F9F92CF6F7F9FAF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAEEF6F8F994F5F8F852F4F4F932F3F3F92EF7F7FB42F6F8 + F876F6F7FAD1F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF6F7FAB1F8F8 + F82700000000F6F7F9ADF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F7F9F6F8F8F993F7F7FB43F6F6FE1DF0F7F723F8F8FB4DF7F8FAA4F6F7 + F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC200000000000000000000000000000000F8F8FA71F7F8FAFFF7F8 + FAFFF7F7F9E7F7F7F72100000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6F9 + F955F6F7F9FCF7F7FAA4F3F3F31700000000F9F9F930F6F7F9FDF7F8FAFFF7F8 + FAFFF8F8FA700000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FFFFFF01F6F8F997F7F8FAFFF7F8 + FAFFF6F7F9F6F6F6FE1F0000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF7F8FAFFF7F7 + FADAFEFEFE0E0000000000000000000000000000000000000000000000000000 + 00000000000000000000FFFFFF01F6F7FAD1F7F8FAFFF7F8FAFFF5F5F8530000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000004EA8F7412394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FFF6FBFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF3199F6D9000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF9F9FC55000000000000000000000000FFFFFF03F7F7F9C6F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F6F8FA79F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FBF6F7F9FEF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDF7F7F96500000000000000000000 + 0000F6F8F875F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F8F86E00000000F7F7 + F721F7F8FAF2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000FFFFFF04F7F8FBC1F7F8FAFFF7F8FAFFF5F7 + F9E2F4F4F9310000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F7F7FA67F7F8FAFAF7F8FAFFF7F8 + FAFFF7F7FA680000000000000000F7F9FB88F7F8FAFFF7F8FAFFF6F7F9FEF6F8 + F874000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE07F8F8FA9BF7F8FAFFF7F8FAFFF7F8FAFFF8F8FA720000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF6F7F9FAF7F8FAFFF6F6F9BFEBEB + FE0D000000000000000000000000000000000000000000000000000000000000 + 0000F6F6F95AF7F8FAFFF7F8FAFFF6F7F9F1E9E9E90C00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000E389 + 3A41DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFFDF9F5FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE7417D900000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF9F9FC55000000000000000000000000FFFF + FF03F7F7F9C6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F6F8FA79F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9FBF6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FDF7F7F9650000 + 00000000000000000000F6F8F875F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F8 + F86E00000000F7F7F721F7F8FAF2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC200000000000000000000000000000000FFFFFF04F7F8FBC1F7F8 + FAFFF7F8FAFFF5F7F9E2F4F4F931000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F7F7FA67F7F8 + FAFAF7F8FAFFF7F8FAFFF7F7FA680000000000000000F7F9FB88F7F8FAFFF7F8 + FAFFF6F7F9FEF6F8F87400000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE07F8F8FA9BF7F8FAFFF7F8FAFFF7F8 + FAFFF8F8FA72000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF6F7F9FAF7F8 + FAFFF6F6F9BFEBEBFE0D00000000000000000000000000000000000000000000 + 00000000000000000000F6F6F95AF7F8FAFFF7F8FAFFF6F7F9F1E9E9E90C0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000BFBFBF042F99F5E22394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FFF6FBFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5E3FDFF2394F7FF2394 + F7FF2394F7FF2394F7FF3BA0F67C000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF8F8F9940000000000000000000000000000000000000000F6F6F61FF7F8 + FAE8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000F5F5F51BF7F7 + F9F6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCF7F7F722000000000000 + 0000FFFFFF01F6F8F995F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FA9B00000000000000000000 + 0000F5F8F854F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 000000000000000000000000000000000000F3F3F316F6F7F9D9F7F8FAFFF7F8 + FAFFF7F7F9F8F7F7F985FEFEFE13000000000000000000000000000000000000 + 00000000000000000000F4F4F931F6F7FAB5F7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9B0FFFFFF010000000000000000FFFFFF05F6F8F9BAF7F8FAFFF7F8FAFFF7F8 + FAFFF6F7F9B6F9F9F92E0000000000000000000000000000000000000000FFFF + FF01F8F8FB4AF6F7F8D4F7F8FAFFF7F8FAFFF7F8FAFFF5F7F8A7FFFFFF010000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFF5F8FA6FF6F7F9F7F7F8FAFFF6F7 + FAD5FAFAFA37000000000000000000000000000000000000000000000000F4F8 + F84BF6F7F9F4F7F8FAFFF7F8FAFFF7F9FB830000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000BFBF + BF04DD7315E2DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFFDF9F5FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D8 + BEFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE7B257C00000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF8F8F994000000000000000000000000000000000000 + 0000F6F6F61FF7F8FAE8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 0000F5F5F51BF7F7F9F6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCF7F7 + F7220000000000000000FFFFFF01F6F8F995F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8FA9B0000 + 00000000000000000000F5F8F854F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000F3F3F316F6F7 + F9D9F7F8FAFFF7F8FAFFF7F7F9F8F7F7F985FEFEFE1300000000000000000000 + 000000000000000000000000000000000000F4F4F931F6F7FAB5F7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9B0FFFFFF010000000000000000FFFFFF05F6F8F9BAF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9B6F9F9F92E0000000000000000000000000000 + 000000000000FFFFFF01F8F8FB4AF6F7F8D4F7F8FAFFF7F8FAFFF7F8FAFFF5F7 + F8A7FFFFFF01000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFF5F8FA6FF6F7 + F9F7F7F8FAFFF6F7FAD5FAFAFA37000000000000000000000000000000000000 + 000000000000F4F8F84BF6F7F9F4F7F8FAFFF7F8FAFFF7F9FB83000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000003EA0F67A2394 + F7FF2394F7FF2394F7FF2595F7FF56ADF9FF57ADF9FF57ADF9FF57ADF9FF57AD + F9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57AD + F9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57AD + F9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57AD + F9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FF57ADF9FFF8FCFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3E2FDFF2394F7FF2394 + F7FF2394F7FF2896F6F874BFFF18000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAA1F6F7F9FBF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7 + FAA0FFFFFF03000000000000000000000000000000000000000000000000F3F9 + F92DF7F8FAE7F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9C8F7F8FAD3F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000000000000000F7F7 + FA6BF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF8F8FA7300000000000000000000 + 000000000000FFFFFF01F6F8F87AF6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8F993FFFFFF0200000000000000000000 + 000000000000F7F7F962F6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + FA9CF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 00000000000000000000000000000000000000000000FEFEFE17F7F8F9C7F7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9F8F6F8F9B8F7F7F981F7F7F964F6F6F95DF7F7 + FA6CF6F8F992F7F8FAD1F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7F8A4FEFE + FE0700000000000000000000000000000000FEFEFE0BF7F7FAB6F7F8FAFFF7F8 + FAFFF7F8FAFFF6F7F9FEF6F7FAD0F6F8F997F6F8FA7DF7F9FB80F7F7FAA2F7F8 + FAE1F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F7F8A6FFFFFF05000000000000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFFAFAFA34F8F8FB49F7F8FAF5F7F8 + FAFFF6F7F9FEF6F8F9BFF6F8FA75F5F8F853F5F5F853F6F8F873F6F8F9BDF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8F9C9DFDFFF080000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000DF7D257ADC6B08FFDC6B08FFDC6B08FFDC6D0BFFE48E42FFE48E42FFE48E + 42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E + 42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E + 42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E + 42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E42FFE48E + 42FFFEFAF7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D7 + BCFFDC6B08FFDC6B08FFDC6B08FFDD6E0DF8E99F5F1800000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAA1F6F7F9FBF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F7FAA0FFFFFF03000000000000000000000000000000000000 + 000000000000F3F9F92DF7F8FAE7F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + F9C8F7F8FAD3F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 000000000000F7F7FA6BF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF8F8FA730000 + 0000000000000000000000000000FFFFFF01F6F8F87AF6F7F9FCF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF8F8F993FFFFFF020000 + 0000000000000000000000000000F7F7F962F6F7F9FCF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F8FA9CF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC2000000000000000000000000000000000000000000000000FEFE + FE17F7F8F9C7F7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F8F6F8F9B8F7F7F981F7F7 + F964F6F6F95DF7F7FA6CF6F8F992F7F8FAD1F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F7F8A4FEFEFE0700000000000000000000000000000000FEFEFE0BF7F7 + FAB6F7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FEF6F7FAD0F6F8F997F6F8FA7DF7F9 + FB80F7F7FAA2F7F8FAE1F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F7F8A6FFFF + FF0500000000000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFFAFAFA34F8F8 + FB49F7F8FAF5F7F8FAFFF6F7F9FEF6F8F9BFF6F8FA75F5F8F853F5F5F853F6F8 + F873F6F8F9BDF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9C9DFDFFF08000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000071B8FE122996 + F6F22394F7FF2394F7FF2394F7FF95CCFBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9CCFFBFF2394F7FF2394 + F7FF2394F7FF3A9EF69E00000000000000000000000000000000000000000000 + 0000000000000000000000000000F8F8FA76F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F7FB44F6F6FA3FF7F7F9E0F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F6F8F8FA70FFFF + FF01000000000000000000000000000000000000000000000000000000000000 + 0000F6F6F61DF6F8F9BEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFDF8F8F995FEFEFE09F5F7F9C5F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF0600000000000000000000 + 0000F6F6F875F6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9EDF6F6F9570000000000000000000000000000 + 0000000000000000000000000000F5FAFA34F5F7F9C5F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8F9E1F8F8FB53000000000000000000000000000000000000 + 00000000000000000000F7F7FB43F7F8FAE2F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F1F7F9F960EFFF + FF10F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8F9B7000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000FFFFFF06F6F6 + F87EF6F7F9F6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F7F9E1F6F6F957000000000000 + 00000000000000000000000000000000000000000000BFBFBF04F6F6F87DF6F7 + F9F8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7F9F2F5F7F76CFFFFFF0100000000000000000000 + 00000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7F9C4000000000000 + 000000000000F6F7F9CBF7F8FAFFF7F8FAFFFAFAFA3400000000F9F9F932F7F7 + FAD8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F7F9C0F0F0FE12000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F09B6312DC6E0DF2DC6B08FFDC6B08FFDC6B08FFEEB889FFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFBC + 90FFDC6B08FFDC6B08FFDC6B08FFE07A209E0000000000000000000000000000 + 00000000000000000000000000000000000000000000F8F8FA76F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7FB44F6F6FA3FF7F7F9E0F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9F6F8F8FA70FFFFFF0100000000000000000000000000000000000000000000 + 00000000000000000000F6F6F61DF6F8F9BEF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFDF8F8F995FEFE + FE09F5F7F9C5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9FCFFFFFF060000 + 00000000000000000000F6F6F875F6F7F9FCF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9EDF6F6F957000000000000 + 00000000000000000000000000000000000000000000F5FAFA34F5F7F9C5F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8F9E1F8F8FB5300000000000000000000 + 000000000000000000000000000000000000F7F7FB43F7F8FAE2F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7 + F9F1F7F9F960EFFFFF10F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + F9B70000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000FFFFFF06F6F6F87EF6F7F9F6F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF5F7F9E1F6F6 + F95700000000000000000000000000000000000000000000000000000000BFBF + BF04F6F6F87DF6F7F9F8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F7F9F2F5F7F76CFFFFFF010000 + 000000000000000000000000000000000000F4F4F92FF7F8FAFFF7F8FAFFF7F7 + F9C4000000000000000000000000F6F7F9CBF7F8FAFFF7F8FAFFFAFAFA340000 + 0000F9F9F932F7F7FAD8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F7F9C0F0F0FE1200000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000003C9F + F7832394F7FF2394F7FF2394F7FF2595F7FFBADDFCFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCFEFFFF48A6F8FF2394F7FF2394 + F7FF2896F6F870BBF72200000000000000000000000000000000000000000000 + 0000000000000000000000000000F5F5F853F7F7FAB2F7F7FAB2F7F7FAB2F7F7 + FAB2F7F7FAB2F9F9FE2F00000000FFFFFF0AF6F8FA73F6F7F9D8F7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE4F7F7F98CF6F6F61D000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF01F8F8FB4DF6F8F9B8F6F7F9F9F7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAF5F5F7F8A9F5F5FA340000000000000000F7F9FB89F7F7 + FAB2F7F7FAB2F7F7FAB2F7F7FAB2F7F9FAB0FFFFFF0400000000000000000000 + 000000000000F5F5FA34F6F7FAB1F6F7F9FAF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F8FADFF7F7F980F1F1F113000000000000000000000000000000000000 + 000000000000000000000000000000000000FFFFFF01F6F6FA3EF6F8FA9FF7F8 + FAE9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAF5F6F7 + F9B5F6F6F95BDADADA0700000000000000000000000000000000000000000000 + 0000000000000000000000000000E7E7FE0BF5F8FA70F7F8FAD1F6F7F9FEF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE6F5F7F98AF3FEFE1700000000FEFE + FE0BF7F7FAB2F7F7FAB2F7F7FAB2F7F7FAB2F7F7FAB2F7F7F980000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F3F3F317F6F8FA7AF6F7FAD1F7F8FAFDF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAF5F7F7FAB2F6F6F957D4D4FF0600000000000000000000 + 000000000000000000000000000000000000000000000000000000000000F6FF + FF1EF7F7F98BF7F8FAE2F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F7FADAF6F8FA7FF3F3F316000000000000000000000000000000000000 + 00000000000000000000F7F7F724F7F8F9C5F7F8F9C5F8F8F997000000000000 + 000000000000F6F6F89DF7F8F9C5F7F8F9C5F8F8FF280000000000000000FEFE + FE07F7F7FA6CF6F7FAD5F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8 + FADCF7F7FA69FFFFFF0300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000DF7C2483DC6B08FFDC6B08FFDC6B08FFDC6C0AFFF4D0B1FFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDFCFFE284 + 31FFDC6B08FFDC6B08FFDD6E0DF8E89D5A220000000000000000000000000000 + 00000000000000000000000000000000000000000000F5F5F853F7F7FAB2F7F7 + FAB2F7F7FAB2F7F7FAB2F7F7FAB2F9F9FE2F00000000FFFFFF0AF6F8FA73F6F7 + F9D8F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE4F7F7F98CF6F6 + F61D000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF01F8F8FB4DF6F8F9B8F6F7F9F9F7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAF5F5F7F8A9F5F5FA34000000000000 + 0000F7F9FB89F7F7FAB2F7F7FAB2F7F7FAB2F7F7FAB2F7F9FAB0FFFFFF040000 + 0000000000000000000000000000F5F5FA34F6F7FAB1F6F7F9FAF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F8FADFF7F7F980F1F1F11300000000000000000000 + 0000000000000000000000000000000000000000000000000000FFFFFF01F6F6 + FA3EF6F8FA9FF7F8FAE9F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAF5F6F7F9B5F6F6F95BDADADA070000000000000000000000000000 + 00000000000000000000000000000000000000000000E7E7FE0BF5F8FA70F7F8 + FAD1F6F7F9FEF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAE6F5F7F98AF3FE + FE1700000000FEFEFE0BF7F7FAB2F7F7FAB2F7F7FAB2F7F7FAB2F7F7FAB2F7F7 + F9800000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 00000000000000000000F3F3F317F6F8FA7AF6F7FAD1F7F8FAFDF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAF5F7F7FAB2F6F6F957D4D4FF060000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F6FFFF1EF7F7F98BF7F8FAE2F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF7F8FAFFF6F7FADAF6F8FA7FF3F3F31600000000000000000000 + 000000000000000000000000000000000000F7F7F724F7F8F9C5F7F8F9C5F8F8 + F997000000000000000000000000F6F6F89DF7F8F9C5F7F8F9C5F8F8FF280000 + 000000000000FEFEFE07F7F7FA6CF6F7FAD5F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FAFFF6F8FADCF7F7FA69FFFFFF030000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000000075C4 + EB0D2D98F6E72394F7FF2394F7FF2394F7FF2E99F7FFD7ECFDFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97CCFAFE2394F7FF2394F7FF2394 + F7FF3CA0F78C0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000FEFEFE19F6F6 + FA3DF8F8FB49F7F7FB40F6FEFE1FFFFFFF010000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000E5E5E50AF4F4F931F7F7FB47F7FB + FB46F4F4F92FFEFEFE0700000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000E9E9E90CF5F5FA36F7F7FB48F7F7F742F6F6 + FE1FFFFFFF010000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000007F7F + FF02F7F7F721F7FBFB43F9F9FC55F9F9FC58F8F8FB4AF3F9F92CD4D4FF060000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000F2F2FF14F6F6 + FA3AF8F8F849F7F7FB42F7F7F723FFFFFF010000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F0F0F012F6F6FA3AF5F5F852F6F6F956F5F5 + F84DF9F9FE2DFFFFFF0600000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF01F6F6F61FF7F7F745F6F6F955F8F8FB54F7F7F742F5F5 + F51A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F6F6F61EF7F7FB48F6F6F956F8F8FB4DF8F8F826FFFF + FF01000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000EB9C620DDC7112E7DC6B08FFDC6B08FFDC6B08FFDE7214FFF9E4 + D2FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEEB98BFEDC6B + 08FFDC6B08FFDC6B08FFE07B248C000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000FEFEFE19F6F6FA3DF8F8FB49F7F7FB40F6FEFE1FFFFFFF01000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000E5E5E50AF4F4 + F931F7F7FB47F7FBFB46F4F4F92FFEFEFE070000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000E9E9E90CF5F5FA36F7F7 + FB48F7F7F742F6F6FE1FFFFFFF01000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000007F7FFF02F7F7F721F7FBFB43F9F9FC55F9F9FC58F8F8FB4AF3F9 + F92CD4D4FF060000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F2F2FF14F6F6FA3AF8F8F849F7F7FB42F7F7F723FFFFFF01000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000F0F0F012F6F6FA3AF5F5 + F852F6F6F956F5F5F84DF9F9FE2DFFFFFF060000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF01F6F6F61FF7F7F745F6F6F955F8F8 + FB54F7F7F742F5F5F51A00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F6F6F61EF7F7FB48F6F6F956F8F8 + FB4DF8F8F826FFFFFF0100000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000049A7F6572394F7FF2394F7FF2394F7FF2394F7FF3EA1F8FFEDF6FEFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFEFEFFFF9ACEFAFE2495F7FF2394F7FF2394F7FF2D98 + F7E189C4EB0D0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000E1833457DC6B08FFDC6B08FFDC6B08FFDC6B08FFE07D + 27FFFCF3EBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFEFFEEBB8EFEDC6C09FFDC6B + 08FFDC6B08FFDE7113E1EBB0750D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000FFFFFF01389EF6AB2394F7FF2394F7FF2394F7FF2394F7FF57ADF9FFFAFC + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFDFEFFFFC0E0FCFF55ACF8FF2394F7FF2394F7FF2394F7FF2594F6FE51A6 + F345000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000FFFFFF01DF781FABDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFE48E43FFFEFCF9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFEFDFFF5D5B8FFE38C40FFDC6B08FFDC6B08FFDC6B + 08FFDB6C0BFEE188374500000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000078C3FF112D98F6DE2394F7FF2394F7FF2394F7FF2394F7FF6DB8 + FAFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6 + FCFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6FCFFABD6 + FCFFABD6FCFFABD6FCFFDFEFFEFFFFFFFFFFFFFFFFFFE8F4FEFFABD6FCFFABD6 + FCFFABD6FCFFE6F3FEFFFFFFFFFFFFFFFFFFE1F0FEFFABD6FCFF9DCFFBFF70BA + FAFF389EF8FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF40A1F7860000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F0A56911DD7114DEDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFE89D5BFFF1C6A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6 + A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6 + A0FFF1C6A0FFF1C6A0FFF1C6A0FFF1C6A0FFFAEADBFFFFFFFFFFFFFFFFFFFBEF + E5FFF1C6A0FFF1C6A0FFF1C6A0FFFBEEE3FFFFFFFFFFFFFFFFFFFAEBDDFFF1C6 + A0FFEFBD91FFE89F5FFFDF791FFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFE07D27860000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000054AFF92D2997F7F12394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF8DC8FBFFFFFFFFFFFFFFFFFFE0F0FEFF2394F7FF2394 + F7FF2394F7FFA1D1FBFFFFFFFFFFFFFFFFFFCDE7FDFF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF379DF6B4FFFFFF020000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F7F7F720F7F7F8A4F6F8F9BDF7F7F962000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000E28D432DDD6F0EF1DC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFEDB380FFFFFFFFFFFFFFFFFFFAEA + DCFFDC6B08FFDC6B08FFDC6B08FFF0C095FFFFFFFFFFFFFFFFFFF7DDC7FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE78 + 1DB4FFFFFF020000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F7F7F720F7F7F8A4F6F8F9BDF7F7 + F962000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000052A8F7442895F6F72394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF62B3F9FFFFFFFFFFFFFFFFFFFDFEFFFF2B98F7FF2394 + F7FF2394F7FF75BCFAFFFFFFFFFFFFFFFFFFF3F9FFFF2495F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2F9AF7C58BD0FE0B000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FFFFFF03F6F7FAD9F7F8FAFFF7F8FAFFF7F8FAFFF8F8FB530000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000E48A3C44DC6E0DF7DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFE6964FFFFFFFFFFFFFFFFFFFFFFE + FDFFDD7111FFDC6B08FFDC6B08FFE9A265FFFFFFFFFFFFFFFFFFFDF7F2FFDC6C + 0AFFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE7417C5E7B9 + 730B000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF03F6F7FAD9F7F8FAFFF7F8FAFFF7F8 + FAFFF8F8FB530000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000004AA6F7482896F6F72394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2C98F7FFBADDFCFFD8ECFDFFAAD6FCFF2494F7FF2394 + F7FF2394F7FF349CF8FFC2E1FDFFD8ECFDFF9CCFFBFF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF3099F6C76FBFFF1000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000F7F7F724F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF6F8FA9B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000E2833148DC6E + 0EF7DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDD7112FFF4D0B1FFF9E5D3FFF1C6 + A0FFDC6C09FFDC6B08FFDC6B08FFDF771BFFF5D6BAFFF9E5D3FFEFBD90FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDD7317C7EF9F5F100000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000F7F7F724F7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF6F8FA9B0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000052A8F63E2A98F6EE2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF369CF6B77FBFFF0C0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8FBC20000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000FEFEFE0BF6F7F9F2F7F8FAFFF7F8FAFFF7F8FAFFF6F8FA740000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000E287 + 393EDC7011EEDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDE771DB7E9AA6A0C000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F4F4F930F7F8FAFFF7F8FAFFF7F8FAFFF7F8FAFFF7F8 + FAFFF7F8FBC20000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FEFEFE0BF6F7F9F2F7F8FAFFF7F8FAFFF7F8 + FAFFF6F8FA740000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000005CB1F724309A + F7D12394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2594F6FE3EA0F58AAAFFFF03000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000F3F3F92BF7F7F9E4F7F7F9E4F7F7F9E4F7F7F9E4F7F7F9E4F6F7F9AE0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F8F8FB4BF7F8FAE6F6F7F9FCF7F8FAA7FEFEFE070000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000E28D4624DE7317D1DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDB6C0AFEDF7D268AFFFFAA0300000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000F3F3F92BF7F7F9E4F7F7F9E4F7F7F9E4F7F7F9E4F7F7 + F9E4F6F7F9AE0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000F8F8FB4BF7F8FAE6F6F7F9FCF7F8 + FAA7FEFEFE070000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000007FB2 + E50A3B9EF6912595F6FD2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2C98 + F7E64FA8F84A0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000FFFFFF02FFFFFF0600000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000E5B27F0ADF7B2391DC6C0BFDDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDD7112E6E386374A000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000FFFFFF02FFFFFF060000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000056ACF63B329BF7CF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2695F6FB3A9EF69478C3 + F011000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000E58A403BDE7519CFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6D + 0CFBDF7A2294E1A5691100000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000FFFFFF0243A4F6572F99F6D52394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2695F6FB399DF7A861B0F22A000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FFFFFF02E1802B57DD7215D5DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6D0BFBDF791FA8E697 + 4E2A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000AAAAFF0350A7F849369CF7AE2897 + F6F82394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2E98F6E33A9DF58667B7F7200000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000FFAAAA03E388 + 3749DE781DAEDD6F0EF8DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDD7214E3DE792286E7975720000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000000088BB + FF0F46A4F657349CF69B319BF7D72795F6FC2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2C98F6F2329CF6BF3BA0 + F78156ADF538AAAAFF0300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000EEAA770FDE802E57DE761B9BDE7518D7DC6D0CFCDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDD70 + 11F2DE7519BFDF7A2381E38D3F38FFAAAA030000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000A2D0E70B61B3F42F48A5F54D3DA1F65F44A3 + F8703B9FF87046A3F56D3FA0F65951A9F74273B4EE1FBFBFBF04000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000E7B98B0BE3924B2FE181 + 314DDE7B225FE1812B70E17A2470E0802C6DDF7E2859E0873942E69C621FBFBF + BF04000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000424D3E000000000000003E000000 + 2800000068050000400000000100010000000000002C00000000000000000000 + 000000000000000000000000FFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFFFE003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFFFE00007FFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800 + 01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFF800000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFE000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFFE0000003FFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000 + 000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFF80000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFE00000003FFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFF000000007FFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0000 + 0001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFC000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFF8000000001FFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE00000 + 00007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFF0000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFC0000000003FFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFE00000000007FFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000 + 00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFC00000000003FFF07FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFF00000000000FFFC1FFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFC00000000001FFF07FFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000 + 000007FFC1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFF800000000000FFF07FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFE000000000003FFC1FFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFF000000000000FFF07FFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000000 + 000003FFC1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFF0000000000007FF07FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFC000000000001FFC1FFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFE0000000000007FF07FFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000000 + 000001FFC1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFE0000000000003FF07FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFF8000000000000FFC1FFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFC0000000000003FF07FFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000 + 000000FFC1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFC0000000000001FF0780FFFF03FFFC07FFE01FFFC0FFF81FFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFF00000000000007FC1E03FFFC0FFFF01FFF807FF + F03FFE07FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFC0000000000001FF06003FFC00C0F0 + 01FF0003FE0030381FF801FFF807FE38FFFC7FFFFFFFFFFFFFFFFFFFF0000000 + 0000007FC1800FFF00303C007FC000FF800C0E07FE007FFE01FF8E3FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF80000000000001FF00001FF00000E000FE0001FC0010381FE0007FE001FE38 + FFFC7FFFFFFFFFFFFFFFFFFFE00000000000007FC00007FC000038003F80007F + 00040E07F8001FF8007F8E3FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF80000000000000FF00000FE00000C0 + 007C0000F80000381FC0701F80C07E38FFFC7FFFFFFFFFFFFFFFFFFFE0000000 + 0000003FC00003F8000030001F00003E00000E07F01C07E0301F8E3FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF80000000000000FF000007E00000C000780000700000381F83FE0F07F83E38 + FFFC7FFFFFFFFFFFFFFFFFFFE00000000000003FC00001F8000030001E00001C + 00000E07E0FF83C1FE0F8E3FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF80000000000000FF003803C03C00E0 + E0300780700F00381F0FFF8F1FFE3E38FFFC7FFFFFFFFFFFFFFFFFFFE0000000 + 0000003FC00E00F00F0038380C01E01C03C00E07C3FFE3C7FF8F8E3FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF00000000000000FF00FE0380FF00F3F0201FE1E03FC0381E1FFFCE3FFF1E38 + FFFC7FFFFFFFFFFFFFFFFFFFC00000000000003FC03F80E03FC03CFC0807F878 + 0FF00E0787FFF38FFFC78E3FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF00000000000000FF01FF0181FF80FF + F0207FFBE07FE0381E3FFFFC3FFF0E38FFFC7FFFFFFFFFFFFFFFFFFFC0000000 + 0000003FC07FC0607FE03FFC081FFEF81FF80E078FFFFF0FFFC38E3FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF00000000000000FF03FF8181FFC0FFE0207FFFC0FFE0381C3FFFFC7FFF8E38 + FFFC7FFFFFFFFFFFFFFFFFFFC00000000000003FC0FFE0607FF03FF8081FFFF0 + 3FF80E070FFFFF1FFFE38E3FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF00000000000000FF03FF8103FFC0FF + C040FFFFC0FFF0381C7FFFFC7FFF8E38FFFC7FFFFFFFFFFFFFFFFFFFC0000000 + 0000003FC0FFE040FFF03FF0103FFFF03FFC0E071FFFFF1FFFE38E3FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF00000000000000FF07FF8103FFC0FF0040FFFFC0FFF0381C7FFFF8FFFFCE38 + FFFC7FFFFFFFFFFFFFFFFFFFC00000000000003FC1FFE040FFF03FC0103FFFF0 + 3FFC0E071FFFFE3FFFF38E3FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF00000000000000FF07FFC103FFC0FC + 00C0FFFFC0FFF0381C7FFFF8FFFFC638FFFC7FFFFFFFFFFFFFFFFFFFC0000000 + 0000003FC1FFF040FFF03F00303FFFF03FFC0E071FFFFE3FFFF18E3FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF00000000000000FF07FFC103FFC0F801C0FFFFC0FFF0381C7FFFF8FFFFC638 + FFFC7FFFFFFFFFFFFFFFFFFFC00000000000003FC1FFF040FFF03E00703FFFF0 + 3FFC0E071FFFFE3FFFF18E3FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF00000000000000FF07FFC103FFC0F0 + 03C0FFFFC0FFF0381C7FFFF8FFFFC638FFFC7FFFFFFFFFFFFFFFFFFFC0000000 + 0000003FC1FFF040FFF03C00F03FFFF03FFC0E071FFFFE3FFFF18E3FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF80000000000000FF07FF8103FFC0E00FC0FFFFC0FFF0381C7FFFF8FFFF8E38 + FFFC7FFFFFFFFFFFFFFFFFFFE00000000000003FC1FFE040FFF03803F03FFFF0 + 3FFC0E071FFFFE3FFFE38E3FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF80000000000000FF03FF8181FFC0E0 + 3FE07FFFC0FFE0381C7FFFFC7FFF8E387FFC7FFFFFFFFFFFFFFFFFFFE0000000 + 0000003FC0FFE0607FF0380FF81FFFF03FF80E071FFFFF1FFFE38E1FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF80000000000000FF03FF0181FF80C0FFE03FFFC07FE0381E3FFFFC7FFF8E38 + 7FFC7FFFFFFFFFFFFFFFFFFFE00000000000003FC0FFC0607FE0303FF80FFFF0 + 1FF80E078FFFFF1FFFE38E1FFF1FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF80000000000001FF01FE0380FF00C0 + FBE01FF1E03FC0381E3FFFFC3FFF0E387FFC7FFFFFFFFFFFFFFFFFFFE0000000 + 0000007FC07F80E03FC0303EF807FC780FF00E078FFFFF0FFFC38E1FFF1FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FF80000000000001FF007C03C03C00C0F1F007C0600F00381F1FFFCE3FFE1E38 + 3FF8FFFFFFFFFFFFFFFFFFFFE00000000000007FC01F00F00F00303C7C01F018 + 03C00E07C7FFF38FFF878E0FFE3FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFC0000000000001FF000007C00000E0 + 00F80000700000381F0FFF8E1FFC3E381FF8FFFFFFFFFFFFFFFFFFFFF0000000 + 0000007FC00001F0000038003E00001C00000E07C3FFE387FF0F8E07FE3FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFC0000000000003FF000007E00000E000780000780000381F83FE0F07F83E38 + 8FF0FFFFFFFFFFFFFFFFFFFFF0000000000000FFC00001F8000038001E00001E + 00000E07E0FF83C1FE0F8E23FC3FFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFE0000000000003FF00000FF00000F0 + 00FE0000FC0000381FC0701F80807E38C3C1FFFFFFFFFFFFFFFFFFFFF8000000 + 000000FFC00003FC00003C003F80003F00000E07F01C07E0201F8E30F07FFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFE0000000000003FF06003FF80040F801FF0003FE0030381FF0007FE001FE38 + E003FFFFFFFFFFFFFFFFFFFFF8000000000000FFC1800FFE00103E007FC000FF + 800C0E07FC001FF8007F8E3800FFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFE0000000000007FF07807FFE01C0FC + 03FFC00FFF8070381FFC01FFF007FE38F80FFFFFFFFFFFFFFFFFFFFFF8000000 + 000001FFC1E01FFF80703F00FFF003FFE01C0E07FF007FFC01FF8E3E03FFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFF0000000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81FFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFC000000000001FFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFE07FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFF800000000000FFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFF81FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000 + 000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE07FFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFF800000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81FFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFE000000000007FFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFE07FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFC00000000001FFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFF81FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000 + 000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE07FFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFE00000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81FFFFFFFFFFFFE7F + FFFFFFFFFFFFFFFFFFFFFFFFFF80000000000FFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFE07FFFFFFFFFFFF9FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFF00000000007FFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFF81FFFFFFFFFFFFC3FFFFFFFFFFFFFFFFFFFFFFFFFFFC00000 + 00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE07FFFFFFFFFFFF0FFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFF8000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81FFFFFFFFFFFFC1F + FFFFFFFFFFFFFFFFFFFFFFFFFFE0000000003FFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFE07FFFFFFFFFFFF07FFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFC000000001FFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFF81FFFFFFFFFFFFC3FFFFFFFFFFFFFFFFFFFFFFFFFFFF00000 + 00007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE07FFFFFFFFFFFF0FFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFE000000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81FFFFFFFFFFFFE3F + FFFFFFFFFFFFFFFFFFFFFFFFFFF800000000FFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFE07FFFFFFFFFFFF8FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFF00000000FFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0000 + 0003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFC0000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFF00000007FFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFFF0000007FFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC000 + 001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFFC00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFF000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFFFF8000FFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE00 + 03FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFF000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000 + } + end +end diff --git a/src/gui-experimental/UCTRLBanner.lfm b/src/gui-experimental/UCTRLBanner.lfm index 7c30110f4..16ad6fa15 100644 --- a/src/gui-experimental/UCTRLBanner.lfm +++ b/src/gui-experimental/UCTRLBanner.lfm @@ -6,12 +6,14 @@ object CTRLBanner: TCTRLBanner BorderIcons = [] BorderStyle = bsNone Caption = 'CTRLBanner' + OnDestroy = FormDestroy ClientHeight = 80 ClientWidth = 992 Constraints.MaxHeight = 80 Constraints.MinHeight = 80 Constraints.MinWidth = 512 OnCreate = FormCreate + OnResize = FormResize LCLVersion = '1.8.4.0' Visible = False object imBackground: TImage @@ -19,8 +21,8 @@ object CTRLBanner: TCTRLBanner Height = 81 Top = 0 Width = 992 - Anchors = [akTop, akLeft, akRight, akBottom] - AutoSize = True + Align = alClient + AutoSize = False Picture.Data = { 1754506F727461626C654E6574776F726B4772617068696366D5000089504E47 0D0A1A0A0000000D4948445200000320000000500806000000E1BA1134000000 @@ -1758,13 +1760,14 @@ object CTRLBanner: TCTRLBanner Top = 32 Width = 100 Anchors = [akTop, akRight] - Enabled = False + Enabled = True ItemHeight = 15 ItemIndex = 0 Items.Strings = ( 'English' ) Style = csDropDownList + OnChange = cbLanguageChange TabOrder = 0 Text = 'English' end diff --git a/src/gui-experimental/UCTRLBanner.pas b/src/gui-experimental/UCTRLBanner.pas index 6bc2b220a..9ef9d0d43 100644 --- a/src/gui-experimental/UCTRLBanner.pas +++ b/src/gui-experimental/UCTRLBanner.pas @@ -1,4 +1,4 @@ -unit UCTRLBanner; +unit UCTRLBanner; { Copyright (c) 2018 by Herman Schoenfeld @@ -13,15 +13,27 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Displays the wallet logo, version, node status and available languages. + Language changes update the saved startup preference. } + +{ Public types in this unit + + Type | Purpose + ------------+------------------------------------------------------------------------- + TCTRLBanner | Displays wallet branding, node status and the language selector. +} interface {$I ..\config.inc} uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls, - Menus, StdCtrls, UConst; + Classes, SysUtils, {$IFDEF FPC}FileUtil,{$ENDIF} Forms, Controls, Graphics, Dialogs, ExtCtrls, + Menus, StdCtrls, UConst, UWalletLanguages; type @@ -34,28 +46,147 @@ TCTRLBanner = class(TForm) ilLogoImages: TImageList; lblClientVersion: TLabel; procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure FormResize(Sender: TObject); + procedure cbLanguageChange(Sender: TObject); private - { private declarations } + FLanguages: TArray; + FRefreshingLanguage: Boolean; + FNodeStatus: TLabel; + procedure RefreshLanguage(Sender: TObject); + procedure RefreshNodeStatus(Sender: TObject); public - { public declarations } + property NodeStatusLabel: TLabel read FNodeStatus; end; implementation -uses UCommon.UI; +uses Math, UCommon, UCommon.UI, UUserInterface, USettings, UCTRLSyncronization; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} + +type + TVersionLabelAccess = class(TLabel); { TCTRLBanner } procedure TCTRLBanner.FormCreate(Sender: TObject); +var + i: Integer; begin + // LCL can defer the version label's AutoSize until this hidden form is shown. + // Reflow status after its real width replaces the design-time placeholder. + TVersionLabelAccess(lblClientVersion).OnResize := FormResize; + {$IFNDEF FPC} + // VCL's label default depends on the current Windows theme. + lblClientVersion.Transparent := True; + {$ENDIF} + // The DFM retains the LFM logo's full alpha colours. A 1-bit mask conversion + // clears low-alpha RGB and creates dark fringes when Windows blends the icon. {$ifdef TESTNET} imLogo.SetImageListPicture(ilLogoImages, 1); {$else} imLogo.SetImageListPicture(ilLogoImages, 0); {$endif} lblClientVersion.Caption := CT_ClientAppVersion; + FNodeStatus := TLabel.Create(Self); + FNodeStatus.Name := 'NodeStatus'; + FNodeStatus.Parent := Self; + FNodeStatus.AutoSize := False; + FNodeStatus.Alignment := taLeftJustify; + FNodeStatus.Layout := tlCenter; + FNodeStatus.Transparent := True; + FNodeStatus.ParentFont := False; + FNodeStatus.Font.Size := 16; + FNodeStatus.Font.Style := [fsBold]; + FNodeStatus.Font.Color := clWhite; + TUserInterface.StateChanged.Add(RefreshNodeStatus); + TUserInterface.SynchronizationChanged.Add(RefreshNodeStatus); + RefreshNodeStatus(Self); + TUserInterface.AppStarted.Add(RefreshLanguage); + FLanguages := TWalletLanguages.Available; + FRefreshingLanguage := true; + try + cbLanguage.Items.Clear; + for i := 0 to High(FLanguages) do + cbLanguage.Items.Add(FLanguages[i].DisplayName); + cbLanguage.Enabled := Length(FLanguages) > 1; + finally + FRefreshingLanguage := false; + end; + FormResize(Self); +end; + +procedure TCTRLBanner.FormResize(Sender: TObject); +var + LLeft, LTop, LStatusLeft, LStatusRight, LStatusWidth: Integer; +begin + if (not Assigned(cbLanguage)) or (not Assigned(imLogo)) then Exit; + // The logo's left inset scales with the form; use the same inset at the + // opposite edge instead of retaining a design-time position after docking. + LLeft := ClientWidth - cbLanguage.Width - imLogo.Left; + if LLeft < 0 then LLeft := 0; + LTop := (ClientHeight - cbLanguage.Height) div 2; + if LTop < 0 then LTop := 0; + cbLanguage.SetBounds(LLeft, LTop, cbLanguage.Width, cbLanguage.Height); + if Assigned(FNodeStatus) then begin + // Start status text after the brand and reserve the remaining width up to + // the language selector, including after DPI changes and narrow resizes. + LStatusLeft := lblClientVersion.Left + lblClientVersion.Width + imLogo.Left; + LStatusRight := cbLanguage.Left - imLogo.Left; + LStatusWidth := Max(0,LStatusRight-LStatusLeft); + FNodeStatus.SetBounds(LStatusLeft,0,LStatusWidth,ClientHeight); + FNodeStatus.Visible := LStatusWidth>0; + end; +end; + +procedure TCTRLBanner.FormDestroy(Sender: TObject); +begin + TUserInterface.StateChanged.Remove(RefreshNodeStatus); + TUserInterface.SynchronizationChanged.Remove(RefreshNodeStatus); + TUserInterface.AppStarted.Remove(RefreshLanguage); +end; + +procedure TCTRLBanner.RefreshNodeStatus(Sender: TObject); +begin + if Assigned(FNodeStatus) then + // Normal online operation needs no banner notice; retain other states. + if TUserInterface.State = uisActive then FNodeStatus.Caption := '' + else FNodeStatus.Caption := TCTRLSyncronization.CurrentNodeStatusText; +end; + +procedure TCTRLBanner.RefreshLanguage(Sender: TObject); +var + code: String; + i: Integer; +begin + FRefreshingLanguage := true; + try + code := TWalletLanguages.ReadPreference; + cbLanguage.ItemIndex := -1; + for i := 0 to High(FLanguages) do + if SameText(FLanguages[i].Code, code) then begin + cbLanguage.ItemIndex := i; + break; + end; + finally + FRefreshingLanguage := false; + end; +end; + +procedure TCTRLBanner.cbLanguageChange(Sender: TObject); +begin + if FRefreshingLanguage or (not TUserInterface.Started) or + (cbLanguage.ItemIndex < 0) then exit; + if SameText(TWalletLanguages.ReadPreference, FLanguages[cbLanguage.ItemIndex].Code) then exit; + TWalletLanguages.WritePreference(FLanguages[cbLanguage.ItemIndex].Code); + TSettings.Save; + TUserInterface.ShowInfo(Self, 'Interface language', + 'The interface language will change the next time you start PascalCoin.'); end; diff --git a/src/gui-experimental/UCTRLNoAccount.dfm b/src/gui-experimental/UCTRLNoAccount.dfm new file mode 100644 index 000000000..112515e05 --- /dev/null +++ b/src/gui-experimental/UCTRLNoAccount.dfm @@ -0,0 +1,330 @@ +object CTRLNoAccounts: TCTRLNoAccounts + Left = 0 + Height = 257 + Top = 0 + Width = 349 + Align = alClient + BorderStyle = bsNone + Caption = 'CTRLNoAccounts' + ClientHeight = 257 + ClientWidth = 349 + Visible = False + object lblGetFirstAccount: TLabel + Left = 16 + Height = 48 + Top = 64 + Width = 321 + Alignment = taCenter + Anchors = [akLeft, akRight] + AutoSize = False + Caption = 'No Accounts' + Font.Height = -20 + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object btnGetAcc: TBitBtn + Left = 96 + Height = 57 + Top = 128 + Width = 163 + Anchors = [] + Caption = 'Get An Account' + OnClick = btnGetAccClick + Font.Style = [fsBold] + Glyph.Data = { + 36240000424D3624000000000000360000002800000030000000300000000100 + 2000000000000024000064000000640000000000000000000000000000000000 + 0000DD6C0936DC6A07AFDB6A07EFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDB6A07EFDC6A07AFDD6C0936000000000000000000000000DC6B + 0766DB6B08FCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDB6B08FCDC6B076600000000D96C0936DB6B + 08FCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6B08FCDD6C0936DB6A07AEDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6A07AFDC6B07EEDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6A07EFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFB357 + 06CF8641049B8641049B8641049B8641049B8641049B8641049BD76808F9DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6A + 07AA000000000000000000000000000000000000000000000000DB6A07CBDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6A + 08DE000000000000000000000000000000000000000000000000DC6B0896DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6A + 08FDE5660C140000000000000000000000000000000000000000DA6A0762DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6806490000000000000000000000000000000000000000DC6B0B2DDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6A087D0000000000000000000000000000000000000000BF7F0004DC6B + 08F4DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6A07B2000000000000000000000000000000000000000000000000DB6B + 07C3DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDB6A07E6FF0000010000000000000000000000000000000000000000DB6A + 088FDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDB6B08FEE267091B0000000000000000000000000000000000000000DC6B + 085ADC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B06510000000000000000000000000000000000000000DC6E + 0625DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDA6A07860000000000000000000000000000000000000000FF7F + 0002DB6A08EFDC6B08FFDC6B08FFDB6A08FDDB6907B5DA6907B1DC6A08F9DC6B + 08FFDC6B08FFDC6A08F8DC6907B0DB6A07B6DC6A08FDDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6A08BA00000000000000000000000000000000000000000000 + 0000DB6B08BBDC6B08FFDC6B08FFDB6A07B20000000000000000DC6A078DDC6B + 08FFDC6B08FFDC6B06930000000000000000DB6A07ACDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDB6A07EDFFFF0001000000000000000000000000000000000000 + 0000DC6A0786DC6B08FFDC6B08FFDB6A07D10000000000000000DB6A0556DC6B + 08FFDC6B08FFDC6B07B20000000000000000DC6A0675DC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDB6A0724000000000000000000000000000000000000 + 0000DE6C052FDA6A067FDA6A067FDB690679FF00000100000000DF6A0A18DA6A + 067FDA6A067FDD6B076B0000000000000000DE680627DA6A067FDA6A067FDA6A + 067FDA6A067FDB6A0581DB6A069EDC6B07D4DB6A08FEDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6A0859000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000DD6B0626DC6A069BDB6A08FDDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDB6B078E000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DB6C0650DC6B + 08FADC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B07C2000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DC6B + 087EDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08F2AA5500030000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DF6F + 0010DC6A07F8DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6E052C0000000000000000000000000000 + 000000000000DA650723DD69078FDD69078FDD69078FDD69078FDD69078FDD69 + 078FDD69078FDD69078FDD69078FDD69078FDD69078FDD69078FDB69078FDD6A + 0678D76806270000000000000000000000000000000000000000000000000000 + 0000DC6A07D0DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B07610000000000000000000000000000 + 000000000000DD6E0B17DB6B08FEDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDB6B08F7DB69083A00000000000000000000000000000000000000000000 + 0000DC6A07CCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDB6B08960000000000000000000000000000 + 00000000000000000000DC6B07E0DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDD6A069E00000000000000000000000000000000000000000000 + 0000DC6A07CCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B07CA0000000000000000000000000000 + 00000000000000000000DB6B07ABDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6A07AF00000000000000000000000000000000000000000000 + 0000DC6A07CCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDB6B08F7D47F000600000000000000000000 + 00000000000000000000DC690676DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6A07AF00000000000000000000000000000000000000000000 + 0000DC6A07CCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6B07C3DB6A06BADB6A06BADB6A + 06BADB6A06BADB6A06BADB6A07D1DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDB6A07AC00000000000000000000000000000000000000000000 + 0000DC6A07CCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDB6A07AA00000000000000000000000000000000000000000000 + 0000DC6A07CCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6A08F9DC6A06DFDC6A06DFDC6A06DFDC6A + 06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A + 06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A + 06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A06DFDC6A + 06DFDC6A06DFDB6A069500000000000000000000000000000000000000000000 + 0000DA6907CDDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDB6B08FCDC69064B00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DA6D + 0007DB6B07F0DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6A07F1DC68052C000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000DC69 + 0761DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08DDDA6D0C150000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000DC68052CDC6A + 07EDDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6A07C2FF7F + 0006000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000FF7F0004DE6A0765DB6A07F0DC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6A + 07A0D96A0530D96A0530D96A0530D96A0530D96A0530D96A0530D96A0530D96A + 0530D96A0530D96A0530D96A0530D96A0530D96A0530D96A0530D96A0530DA65 + 07230000000000000000E5660014D96A0530D96A0530DB69001D000000000000 + 0000E16B001ADF680531DA6A0654DB6B0791DC6A07E4DC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6A + 08D90000000000000000DB68064EDC6B08FFDC6B08FFDC6B08B9000000000000 + 0000DC6B066FDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6A + 08FCDD66000F00000000DB6A0724DC6B08FFDC6B08FFDC6B07EBFF0000010000 + 0000DD6C0744DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08B7DC6A067CDC6A06B8DC6B08FFDC6B08FFDC6B08FFDB6A07A7DC6A + 067DDB6A07C9DC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B07EEDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6A07EFDB6A07AEDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6A07AFDD690935DB6B + 08FCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDB6B08FCD967093600000000DB6A + 0765DB6B08FCDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDB6B08FCDB6A076500000000000000000000 + 0000DD690935DB6A07AEDC6B07EEDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B08FFDC6B + 08FFDC6B08FFDB6B07EEDB6A07AEDD6909350000000000000000 + } + ParentFont = False + TabOrder = 0 + end +end diff --git a/src/gui-experimental/UCTRLNoAccount.lfm b/src/gui-experimental/UCTRLNoAccount.lfm index 0efd5de1c..0dcd2aca6 100644 --- a/src/gui-experimental/UCTRLNoAccount.lfm +++ b/src/gui-experimental/UCTRLNoAccount.lfm @@ -31,6 +31,7 @@ object CTRLNoAccounts: TCTRLNoAccounts Width = 163 Anchors = [] Caption = 'Get An Account' + OnClick = btnGetAccClick Font.Style = [fsBold] Glyph.Data = { 36240000424D3624000000000000360000002800000030000000300000000100 diff --git a/src/gui-experimental/UCTRLNoAccount.pas b/src/gui-experimental/UCTRLNoAccount.pas index 80de89e3c..99ec1cea9 100644 --- a/src/gui-experimental/UCTRLNoAccount.pas +++ b/src/gui-experimental/UCTRLNoAccount.pas @@ -13,12 +13,24 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Presents the first-account view when the wallet has no accounts and opens + the account-acquisition page. } + +{ Public types in this unit + + Type | Purpose + ----------------+------------------------------------------------------------------------- + TCTRLNoAccounts | Shows the first-account prompt and account-acquisition action. +} interface uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, + Classes, SysUtils, {$IFDEF FPC}FileUtil,{$ENDIF} Forms, Controls, Graphics, Dialogs, StdCtrls, Buttons, UCommon.UI, UCoreObjects; type @@ -28,6 +40,7 @@ interface TCTRLNoAccounts = class(TApplicationForm) btnGetAcc: TBitBtn; lblGetFirstAccount: TLabel; + procedure btnGetAccClick(Sender: TObject); private FPBalanceSummary : PBalanceSummary; public @@ -37,7 +50,13 @@ TCTRLNoAccounts = class(TApplicationForm) implementation +uses UUserInterface; + +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} const GNoAccountsText = 'No Accounts'; @@ -45,11 +64,26 @@ implementation procedure TCTRLNoAccounts.ActivateFirstTime; begin + if not Assigned(FPBalanceSummary) then begin + lblGetFirstAccount.Caption := GNoAccountsText; + exit; + end; case FPBalanceSummary^.TotalPASA of 0: lblGetFirstAccount.Caption := GNoAccountsText; else lblGetFirstAccount.Caption := Format(GHasAccountsText, [FPBalanceSummary^.TotalPASA]); end; end; +procedure TCTRLNoAccounts.btnGetAccClick(Sender: TObject); +begin + // A PASA must be transferred or purchased; creating a key alone creates no account. + TUserInterface.ShowInfo(Self, 'Get an account', + 'Create or select a key in Wallet Keys, then copy its public key.' + sLineBreak + sLineBreak + + 'Ask an account owner to transfer a PASA to that public key. You can also buy a listed ' + + 'account in Account Explorer once you have a funded account.' + sLineBreak + sLineBreak + + 'The public key is safe to share. Keep your private key and wallet password secret.'); + TUserInterface.ShowPrivateKeysDialog(Self); +end; + end. diff --git a/src/gui-experimental/UCTRLSyncronization.dfm b/src/gui-experimental/UCTRLSyncronization.dfm new file mode 100644 index 000000000..9ae45ddb9 --- /dev/null +++ b/src/gui-experimental/UCTRLSyncronization.dfm @@ -0,0 +1,45748 @@ +object CTRLSyncronization: TCTRLSyncronization + Left = 0 + Height = 428 + Top = 0 + Width = 728 + BorderIcons = [] + BorderStyle = bsNone + Caption = 'Synchronization' + ClientHeight = 428 + ClientWidth = 728 + OnCreate = FormCreate + OnDestroy = FormDestroy + OnShow = FormShow + object paSplash: TPanel + Left = 0 + Height = 428 + Top = 0 + Width = 728 + Align = alClient + Caption = 'paSplash' + TabOrder = 1 + object imgSplash: TImage + Left = 1 + Height = 426 + Top = 1 + Width = 726 + Align = alClient + Center = True + Picture.Data = { + 0954506E67496D61676589504E470D0A1A0A0000000D49484452000007800000 + 02760806000000BE519F650000000467414D410000B18E7CFB51930000002063 + 48524D0000870F00008C0F0000FD520000814000007D790000E98B00003CE500 + 0019CC733C857700000A396943435050686F746F73686F70204943432070726F + 66696C65000048C79D96775454D71687CFBD777AA1CD30025286DEBBC000D27B + 935E456198196028030E3334B121A2021145449A224850C480D150245644B110 + 1454B007240828311845542C6F46D68BAEACBCF7F2F2FBE3AC6FEDB3F7B9FBEC + BDCF5A170092A72F9797064B0190CA13F0833C9CE911915174EC0080011E6080 + 29004C5646BA5FB07B0810C9CBCD859E2172025F0401F07A58BC0270D3D03380 + 4E07FF9FA459E97C81E89800119BB339192C11178838254B902EB6CF8A981A97 + 2C66182566BE284111CB893961910D3EFB2CB2A398D9A93CB688C539A7B353D9 + 62EE15F1B64C2147C488AF880B33B99C2C11DF12B1468A30952BE237E2D8540E + 33030014496C1770588922361131891F12E422E2E500E048095F71DC572CE064 + 0BC49772494BCFE173131205741D962EDDD4DA9A41F7E464A5700402C300262B + 99C967D35DD252D399BC1C0016EFFC5932E2DAD24545B634B5B6B434343332FD + AA50FF75F36F4ADCDB457A19F8B96710ADFF8BEDAFFCD21A0060CC896AB3F38B + 2DAE0A80CE2D00C8DDFB62D3380080A4A86F1DD7BFBA0F4D3C2F890241BA8DB1 + 715656961197C3321217F40FFD4F87BFA1AFBE67243EEE8FF2D05D39F14C618A + 802EAE1B2B2D254DC8A767A433591CBAE19F87F81F07FE751E06419C780E9FC3 + 13458489A68CCB4B10B59BC7E60AB8693C3A97F79F9AF80FC3FEA4C5B91689D2 + F81150638C80D4752A407EED07280A1120D1FBC55DFFA36FBEF830207E79E12A + 938B73FFEF37FD67C1A5E225839BF039CE252884CE12F23317F7C4CF12A00101 + 48022A9007CA401DE800436006AC802D70046EC01BF8831010095603164804A9 + 800FB2401ED8040A4131D809F6806A50071A41336805C741273805CE834BE01A + B8016E83FB60144C80676016BC060B10046121324481E421154813D287CC2006 + 640FB941BE50101409C54209100F124279D066A8182A83AAA17AA819FA1E3A09 + 9D87AE4083D05D680C9A867E87DEC1084C82A9B012AC051BC30CD809F68143E0 + 557002BC06CE850BE01D7025DC001F853BE0F3F035F8363C0A3F83E710801011 + 1AA28A18220CC405F147A29078848FAC478A900AA4016945BA913EE426328ACC + 206F51181405454719A26C519EA850140BB506B51E5582AA461D4675A07A5137 + 5163A859D4473419AD88D647DBA0BDD011E8047416BA105D816E42B7A32FA26F + A327D0AF31180C0DA38DB1C2786222314998B59812CC3E4C1BE61C6610338E99 + C362B1F2587DAC1DD61FCBC40AB085D82AEC51EC59EC107602FB0647C4A9E0CC + 70EEB8281C0F978FABC01DC19DC10DE126710B7829BC26DE06EF8F67E373F0A5 + F8467C37FE3A7E02BF4090266813EC08218424C2264225A1957091F080F09248 + 24AA11AD8981442E7123B192788C789938467C4B9221E9915C48D124216907E9 + 10E91CE92EE925994CD6223B92A3C802F20E7233F902F911F98D0445C248C24B + 822DB141A246A2436248E2B9245E5253D24972B564AE6485E409C9EB92335278 + 292D291729A6D47AA91AA99352235273D2146953697FE954E912E923D257A4A7 + 64B0325A326E326C99029983321764C62908459DE242615136531A2917291354 + 0C559BEA454DA21653BFA30E506765656497C986C966CBD6C89E961DA521342D + 9A172D85564A3B4E1BA6BD5BA2B4C4690967C9F625AD4B8696CCCB2D957394E3 + C815C9B5C9DD967B274F9777934F96DF25DF29FF5001A5A0A710A890A5B05FE1 + A2C2CC52EA52DBA5ACA5454B8F2FBDA7082BEA290629AE553CA8D8AF38A7A4AC + E4A194AE54A57441694699A6ECA89CA45CAE7C46795A85A262AFC255295739AB + F2942E4B77A2A7D02BE9BDF4595545554F55A16ABDEA80EA829AB65AA85ABE5A + 9BDA4375823A433D5EBD5CBD477D564345C34F234FA345E39E265E93A199A8B9 + 57B34F735E4B5B2B5C6BAB56A7D694B69CB69776AE768BF6031DB28E83CE1A9D + 069D5BBA185D866EB2EE3EDD1B7AB09E855EA25E8DDE757D58DF529FABBF4F7F + D0006D606DC0336830183124193A19661AB6188E19D18C7C8DF28D3A8D9E1B6B + 184719EF32EE33FE6862619262D26872DF54C6D4DB34DFB4DBF477333D339659 + 8DD92D73B2B9BBF906F32EF317CBF4977196ED5F76C78262E167B1D5A2C7E283 + A59525DFB2D572DA4AC32AD6AAD66A84416504304A1897ADD1D6CED61BAC4F59 + BFB5B1B411D81CB7F9CDD6D036D9F688EDD472EDE59CE58DCBC7EDD4EC9876F5 + 76A3F674FB58FB03F6A30EAA0E4C870687C78EEA8E6CC726C749275DA724A7A3 + 4ECF9D4D9CF9CEEDCEF32E362EEB5CCEB922AE1EAE45AE036E326EA16ED56E8F + DCD5DC13DC5BDC673D2C3CD67A9CF3447BFA78EEF21CF152F26279357BCD7A5B + 79AFF3EEF521F904FB54FB3CF6D5F3E5FB76FBC17EDE7EBBFD1EACD05CC15BD1 + E90FFCBDFC77FB3F0CD00E5813F06320263020B026F0499069505E505F302538 + 26F848F0EB10E790D290FBA13AA1C2D09E30C9B0E8B0E6B0F970D7F0B2F0D108 + E3887511D7221522B9915D51D8A8B0A8A6A8B9956E2BF7AC9C88B6882E8C1E5E + A5BD2A7BD595D50AAB53569F8E918C61C69C8845C786C71E897DCFF4673630E7 + E2BCE26AE366592EACBDAC676C4776397B9A63C729E34CC6DBC597C54F25D825 + EC4E984E7448AC489CE1BA70ABB92F923C93EA92E693FD930F257F4A094F694B + C5A5C6A69EE4C9F09279BD69CA69D96983E9FAE985E9A36B6CD6EC5933CBF7E1 + 37654019AB32BA0454D1CF54BF5047B8453896699F5993F9262B2CEB44B67436 + 2FBB3F472F677BCE64AE7BEEB76B516B596B7BF254F336E58DAD735A57BF1E5A + 1FB7BE6783FA86820D131B3D361EDE44D894BCE9A77C93FCB2FC579BC3377717 + 28156C2C18DFE2B1A5A550A2905F38B2D5766BDD36D436EEB681EDE6DBABB67F + 2C62175D2D3629AE287E5FC22AB9FA8DE93795DF7CDA11BF63A0D4B274FF4ECC + 4EDECEE15D0EBB0E974997E5968DEFF6DBDD514E2F2F2A7FB52766CF958A6515 + 757B097B857B472B7D2BBBAA34AA7656BDAF4EACBE5DE35CD356AB58BBBD767E + 1F7BDFD07EC7FDAD754A75C575EF0E700FDCA9F7A8EF68D06AA83888399879F0 + 49635863DFB78C6F9B9B149A8A9B3E1CE21D1A3D1C74B8B7D9AAB9F988E291D2 + 16B845D8327D34FAE88DEF5CBFEB6A356CAD6FA3B5151F03C784C79E7E1FFBFD + F0719FE33D2718275A7FD0FCA1B69DD25ED40175E474CC7626768E7645760D9E + F43ED9D36DDBDDFEA3D18F874EA99EAA392D7BBAF40CE14CC1994F6773CFCE9D + 4B3F37733EE1FC784F4CCFFD0B11176EF506F60E5CF4B978F992FBA50B7D4E7D + 672FDB5D3E75C5E6CAC9AB8CAB9DD72CAF75F45BF4B7FF64F153FB80E540C775 + ABEB5D37AC6F740F2E1F3C33E43074FEA6EBCD4BB7BC6E5DBBBDE2F6E070E8F0 + 9D91E891D13BEC3B537753EEBEB897796FE1FEC607E807450FA51E563C527CD4 + F0B3EECF6DA396A3A7C75CC7FA1F073FBE3FCE1A7FF64BC62FEF270A9E909F54 + 4CAA4C364F994D9D9A769FBEF174E5D38967E9CF16660A7F95FEB5F6B9CEF31F + 7E73FCAD7F366276E205FFC5A7DF4B5ECABF3CF46AD9AB9EB980B947AF535F2F + CC17BD917F73F82DE36DDFBBF077930B59EFB1EF2B3FE87EE8FEE8F3F1C1A7D4 + 4F9FFE050398F3FCBAC4E8D3000000097048597300000B1200000B1201D2DD7E + FC0000001974455874536F667477617265007061696E742E6E657420342E302E + 3231F12069950000F51C49444154785EECFD59B325C99A9E87AD710F99B973CE + CAAACAACAAACE1D439A7FB8CDD8DEE3E8DD313D1DD001A430307204810A00835 + 8D244083D030402069040CBCD08D8C26E9423293C94833DE481732DDEA463F44 + FF403F24F4BDEEFE7ABCF1C5E711B1D65E3B73679E7DF158B87F937BC49A22FC + DD117BB5DE9C776DCEF2767DB98CCD85E4CEB3595F745BCB5176DBCBC2FD6EBB + BFEA36FB07DD6A77BF5B996D65F12B8CB372E3163693706EB6B5FE34F78CFB6F + 85CDE64162B53D011B3B4E20F2DD717A360F87ACAFD276BD7D9418F9DF37B03F + 64C2C7FD3D649F35C713C5BF75CA3E87F3DBFDB2F0F8A4ACF74F42A6FCB447FE + A93CB2397BDA6DCF9F25D08E62AE0BEAFADA53735B32EF1E8BB3D8ADB1DB3EA9 + 6C77366EC2F66FFFDCB6B67FFB17DDF6ECA5F1B1D5FFB87BF0E8ABEE2FFDEC6F + 767FFF1FFE6FBA1FFCF48FEDFDFC91D542CC47166B39C6DAF200FB6A5B6D6D9F + 4ABBC9BEDFEFCD999F3BED16538ECF8A583F2336DA75CB7621D5C0F1A38D318C + ABF1DCF6A4399C039B37DE0F659B28FE14A776C7EAE2F90210176063B3BDBE04 + 2FACFD91816DCCFAD25EB36B10D5DBDC7B39F0D1E6ED91CFE37DEC2BB06F6DBB + BFFF49B7B9B03EF6F9DCDE67B0DFFF38C1B8685CC5E7D0C6788CB37BF049F2A7 + 6D89D158B6530C6AD17F89B9D9D88475CDCE78E646A4793DB018617DDF6A1A6A + DB5ED9BC0CB5018D851F6DC6ADEED95C4A7FF7F0D36A573FA08DA00FB4F6B168 + 3DB545E379F4B844B16A432CF691D0E7DBDAF7ACADC6CA58A36DA09DB8EA5963 + 6BC784A04F728CE55D7DDAAD1FBE325E5BFB95E353AB7920F7035B616D352336 + 3636405B6369F3ED08AD3105E3D2FE612C6C13659EF6194EA4397C66B19F27B6 + 8FBE48A00D7BC2DACC8FC6C176659F51D46ECF4DECF63AAC1E19D896F9ACEFBF + B2F7CB67F659FFCC5E771BCFB68F3FFF71F7933FF8B3EE37FECADFEB3EFAE637 + BACDE337DDC6E6B3070F5E77BB7B9FDA77D1AB6E87DA183791C7A8FB5DF6BD9F + 4719336D7D5C817D43F33C033FC795DCE4B7F709D834D83EC07EDB71B4F6EAD2 + 3E4362036BD430B6B68F00FBAA3929BE6C2BF8BEB4CF887E2EF9D9CC9F376CED + 339A3E5FFD7EF23DC376DDB7B45F78BFF0F3953F8BF53399EAD818B6DD96CF33 + D8DB6B0CD827E3F9F436DAC1E601BEE7F05D08F27766B60FE3D43605BF3BFD77 + 286B601EACE5EBB67219AB7DCF921CDFD71CCE85360F63BC4D73B5A68F216A9F + 8A53A21CF5F1F7AEFEAEC1CFDF5AFC1EA7DF65A3D8F4779D7DA5656F81D8F47B + EE7EF3A338C2F3163D8761DFC7D287B68E41FB1298E3998AF33E0FE7AEF39FB2 + 7BDF1206B9E7D63FB37341DBAEB035B04D6DF80AB47934666DE7947AFE5ECFFD + FDF581C218A5D8717DE0AF1BD1E6F5247D11B58E10C529ACCF768BA96B47AD37 + 57A707F562743D20F29F021DE33AE3B4EA789BF6D5EE49BEEDC3212E0F0CE267 + 086B7BFB2D265A0F23BA6EB68494B7BB4A6CF6D6DF916C5374DD75BDCB6C309F + 123FF4E77AC4FB97726CDE1D37C8B5348E7B967B3904B684D4E4184AF595F7F7 + 1AEFE146AC417DA7E62D02F19C4F033F7F039A556A4BDC7A030DCBEB5A77DC31 + E4DD0AC0885F9D8D04E0EDFABC7B78F5A2FBF8D3AFBA874FECE4180230C4DFF2 + 26DFD89B7BB3BED76DED038336C78F855F322D00AF5656DFC8FDB72700833B01 + F83DC59DD01C72F2F35E31B55FD149E00C1AEF89E25B1C94A727F101A946C3EE + 894E8C3F2C7021727AE28BCF7C411BF9D53EE79FBAD83DFC82F866D179CF6371 + 161B09C0EBADF992485BC4DBB38FACFFDC3EAFCFEC3DFFA27BF8F43BDDEFFDF1 + 3FECFEFC9FFDBBEE2FFDCEDFB6988FED3DFDC2B6A0177CC9F62CDB21EC52FCF5 + 2230FB7D9ED9CB3C57E522B627EF038E7BBF20822DFA58A031D8870FAF4F5AC4 + C1024EDE2AB0615166901FE2C620C82DED54EBC2F6F7F245B786680B9FD18BBD + 585C0268635ED9BEBAB0BA09B49760B50764FBFAD26A63ECD6221AF61FE3CB62 + D929881602FD0221E7E0FDEC2B9AEB811F8B9BA9D6C547DD591280737DE6C2EF + DB446B296B21F72DBE2C1243C0482286D54A0BAB583816520EE2E0B7B81D16BA + 1157EAAC20CA1738078E839A692BE43A56039445732F4CF8056E5D5C57D4C636 + B7AC41E1A3E58F725BFE4399CB83984320EAD00E71278B46E27730A7E6268A8F + 7347BFC21880FA43D6109E8C2A3695BE07718C8D190A5C1EEF43BB8A78D667BB + 52C4D316BEAE429FFAD5067C1DD28A27DE1FD5A18D768ABF1E0AC02B6C1F4108 + CEB5778F21145B9FF98F108FE385DA1803E32BB90E456008BFEB47162F02F0E6 + C16BFB0C7E669FBFCFAC6D633FFCA25B3F7ED3ED5F7CD35D7CF46D1A63857C1B + F7ECE99BEEDCFAEB0B7BFF5CE233DBEF5BB4EFDCCF164B62223487C79D684C8A + BB9F1908B506C55CA0E2EEF9E32FBA33EC731185CF1EDAB1619EF93368B39FDB + 751C7C7ECA6772F019B33E3EFF200BBAF97DCCD7166DDD07CEBFFEE10572CA56 + BF0760C3E77353BED3F0DDB6E7775CF9DE209A97BE8B4A4E6A9B4DD9DA77C4F6 + CABE9FD336C7787CCEA1688D561BF07B770A9D13B7FCDEF3F83CED6B0C6BFA18 + A5E5F3E3A8BD85F7B7F2D4C6B6CE837114816B2CB0DF5CFEDEE7DFEBF21BCDF3 + 86E42F7D891BFCFE6B7C03FEBE7B5A71FEDC45CF9F4854C7F7A3BC165ACFD769 + C5457E053198839F076D87329B0F71379D67E7AD92445D8BA1C09B445FEF1F60 + 76BDD6A8D71C0D18ABF19A636DFE61E8DAFA40AF250FFDA362CD8DD0189FBB14 + ADB7BC8EBF6EEBD17581C87F2AE6C688FC517F8EA938AD55E3B00EE4E23C83F8 + 1946B54BFD436ABC4BA2F53050D7D90E1580B75782F5935D6DC6CEE26DEB596F + 383E6D795DF614023072EE04E05BC8B5340E1151759BC835EB6BDE1AE3C60460 + C4722E3360DE42D2AFD837FFDA505D6C8E5EF7BAE3978DD5664B6134E2A6EF00 + B6EDEA2C6D0104606C57AB7DF7C9C75F743FF9B59F759F7FF5FDEEEC025FF297 + 49085EEF1F741B7B836F210207E2EFB630147F11C3B9C56FF629019877E992C8 + AEF10A7D1A4B346EF065802F11EDDF713B919399FCA3603F0EEF5204E6F8C13C + 78A206D4DE6269FC2135AF45705C0F99637432DB243811AFE338FBFB03F6E9DD + 135D8046714A94E3C1C52E637D3E892E88E72E94A33A9E286F0E9FA77318D6B5 + ADB529006F0777FF9ADF586D9FD9F659BEB377F7DCDEA710723FEECEEE7FD13D + FBE407DDA75FFE46F7ECE35FED7697AF2DF685D5B41808B71075F157F4E86FED + 3814F13709BC80BE625B3167841D7B9BD70A7311707730C5D6BC386335CA5FEE + 57AC3EEA26C1F61CE268BFA0E3637BBB2231A895B679AE2907F5B8D094B6E607 + B05FE605AFB44045AA50ABF1560B77EF2A039157A8BE5C870B60A4B7E7F1EB38 + 752C19DBF0F9D705FB5B05CFC01F8D3BB570A8F59A94D86DB97387E22AF3D99E + AD53E05D415C205E3F30B0C05B169075E1B8820566B718AE8BE3A946A90356F7 + FBFA759C00E4529425D162B6CF011C5F618CDA18AF396A03AC1BF99456FE127C + 1EE74AAAC8039FC512083559842D4290E4D4DC929781708478884DC8290C8459 + 0A4CA83D14D8C05888EA7DDA56D14A89EAAB8D77C2C28E7C089B006D9FA7686E + ADA55058B358CE4DF1F53CCCF54CE54739753E05EFCFF5B2D8BB7A80BA59AC55 + 01186C210A5FC1F6BADB3F79D3ED1E7F61EF1DE47FD6ED9E7C69FD37292ED770 + 4272A9055F2209B9B685009C44E057F69ECCE22F44E08D6D57B80BD8F2128FBE + 306CFB186DE4DADCF1DADFB3F72448FBD61F6FD0DA5FDA237CEC1CCCE16BE261 + CC600C0AB4050ABE684300A61DE22F44DFB48FE63F83E85EE26A2D370F1D2FF7 + B105FD67583FBB6853C4C776635BCC3BBDFFADBD4D35AC66DAE65AF9384B0D21 + 09C118C7DA55EC6D8C3B80B1A85172D3F75BF90E86F89BEF06EEBFBF8EF9EE63 + 5E949BC675B6284E6B78BFAFE163B87F642AD6D7F6FD08C4F0778BF5A3B1087D + 1A835C8EC53AF43187BFD96A233E9F6DAD91FAE9771ADBBE26B71EC432476BA4 + 3F100BCE2B143D3F590AE2FDB99BFAB83D06ADCBDAA784F36CCDB135A69F9767 + 108B73F3728E7B487EEDBB7363CF48009E82713EDEB785B5D9145EB3F17A2DBA + A653D4D762691CF16392684CDAC6C097A110D91224BDFF5DE1E745A67CC0E7FB + B6D2B213EF63FC870CD7C1D25A58B0D6062663641DAFFA9398567C8D753E6F1B + 427F11DF92009C05DC4CB0763B037323DF1DEF90F21A87BE590231B5926BD6D7 + 9CE378D6E6E37B54FEE86084E6787B14B3B13988A84B31770E88BDB8DB57F328 + 0013AF75116A5E43EDEB8E5F269200DC16816F5E0056E1977701AFD767DDF367 + AFBAEF7EFFA7DDABCFBEE92E2F9F743BFB42DF5F3CB49314FB90E24D8EC74097 + 37ED40ECE51BDFD94F290053BC8D6C1E8DDDDA17868FDFAC72DCE0CBC0F7F50B + 44A12F8A99CA5396C6DD31464F42CAC9CB1ADB95F98213991BA7CCA1223E3D29 + 9B3FA16A9CC459CDB5ED1B48FB28F1378EDB1F10CED151FDC1C96C8BF7F3641E + 73BEFDE8052989E294280717AE73F6A575F097DCDEEE73A7F0B9C710CD3BF76D + 6B6D08C02AFC26F137F92DAF08B159C0C55DB21FDB3E7DDAADCF3EB5BA05F4CB + 1DC02B8BAD2229C557DCD96B600B2000D7C7B10D628750484E22346AA476A987 + 3644580AB749BC2D353D14681517C33A4AF2712EDCAF123BBB6044CA62943288 + 4DB56C9F9C009CEFE2CD0C04607B3FD53182DA9EC1582547DBA7E63A0B9135F7 + A2EF2F126EB1D09BB6F61EB46DBA33A7E44F820560671B2CF2960562DD92C1A2 + 6F5954E6C2326D1AA344F574515A1988BF0617A7153F36F136C4628B5ADA5F4A + 2B9EF6D63C9610CD15A898437146059C48CC19E6C26EFE481815E1AA0545AC88 + 560CED91E8C6B66ED59F90B9C24701D8C73137C13CDA1FBE4A8259CD713539B7 + C4A37E9E4A648B88E2384E1A5B887CCC21795E438116ED81000C11B66CAB1D62 + 6D795C34613C855F321680AD6DE352004EF3307B12806D9B85E62F92F8BB86F8 + FBB808C0785D201C438CA4F85B0452DE5DAC709F0FC5D76981581C3FFEC180BE + 366CFBDA8442AF0AC0D8E28E5F7D14F4DEF66B87183C229B31A5061F3BAD54DF + C08E3E90CF6FF93C6FACC60EEF79AB8DCF28EFEEC66F6F3AB6D88FBA5F78FD30 + 465F2304DF05A98DAD7D3F5CE5B160CB63621E7D7C9D57FAFE403C62ECF8D896 + 8FB2D6EF2C7E071E0ABF3395393F8862D486476EA7C76E9B9DDFF70ABF23E167 + DBC7281C83D0CEDC28DFE7688CDA35874CD5C4D6FF3EF3379B7173F9A415473B + D173028098E6DC210ACB3985FEAB08B4B7F72CC69DDFD46D69EB1676C4FBF338 + D6607B293E47EB6AED53724CDD684E912D9D2F96F3789E9747F1D703F5EDFC9B + E05A4AFB8EFA541FDA66E2C13A5D730D89AE05B3B02AE03A4D726E82687CF5AB + DDFB978AC0DEF7AED039F9B9453EA0F94ACB3F67F73EB57FA8E89A1BD86C6DBF + FD9A5BBD33B7671883BED8B9B6375AB7CCFEFCA4B34CEFEBE9EBE775D9FA2869 + DC255CEF0E5672DC147702F02D057AC5D19AC55840ED7135398E03EBFB698D1F + EFB7B9F712F37C3FC4E620226E02BA14B6E17C059F6740C79A128055F8A5EEE5 + 63A7F2EFF83078E777002BF511D0C6C5C5A3EEC1A38FBA8BFB4FBB2D9EF78F37 + 23EE00C6FF02B6372C445DDCE90B117828F4B6E0DC28F20EE3F583704A0198C0 + 3F2900CB97C49A5F0AFCD2A0CFA3311AE7FB531C127BC710390949E2287F1CDE + 95000C306E63ECF60959A1919BE29167BE2AFE721F1B634DA1F358CA5C9E1FC3 + C74727B31F16B800391E5E0846BE53C27114F569ACDAE7C0C5ACC6FA1AEA5390 + 0701388A5F4A54F750F4627C58D7B6D6F6023063D35DB65B9B7F115DD35DB0E7 + 2F93089C04DFCD47667F697C64755E9ACD80508ABB762196629BD0B6CD85626A + 8BB29883459D7AE7B013801957C5DA8BBCADE26CFA8BFDECCF226B8EA9147FDD + AA8FC08E3B8B6D2C88D1ACC7470DD7714AFC60D1A9F868F33027831A561F62AF + 08BF2311180230E6E3F7C5D0DA58E023DAD7989BE05A6360F1B6E4EB2392D3A3 + 91B1E05AD03B69753136DF8D95FD58E0476C5A40850F31097BDF9698B4488BC5 + DA528370A1978BBD3E86FD3CE63C9A17F549B4304D3B488BF822003306304673 + 19A335B4AF313E9ED01EE1633946161BE09F6298CB7CEDAFEDB51ADF999B198A + 47B93D8C29B9293F8B4F143F4F054428B687C2D6AB418C0A705330C7C31A5AC7 + FB92AD08BC6843D08528997C4590E3FF924D6DD84A4C8A7B5C444CB35701D4D0 + FC29EA1C64DE2D10EB6D513DFD9FBF91009CC45F83426FF225A1F6B36E677DDC + FDBB7FF26582A2AF52EB94DA4D0118770E23F601E2B3F8BB7E64DBC759045EE3 + D821DEF6637BEF55627369FB793FEF078F8DE2F77F1211EE81AF45F435401BA2 + 2940DBE7B23DFADFC2F72DD7B6107477E9FF1F97474043E4C5FE407CB5F6991D + BFC71F7FAFFBF4AB5FEB1EBCFC361DA7F4C71410BC311EE662B93A1EC71C025B + F9BC56F03D803B8F3FEBCEEC35DCE231D356EFC1C7DF761F7FFDD3EEC5173F4A + 8FDBAEEF758C65DB3C86AF95EBC5DF0BF9BB2389BAE9BDCE7CFD4E41DD9CB796 + EF5F7E5FA53FCA29DF5BFABDC8EF427E974DA171CC8B7261E378F4FBEDFA3E7E + B3EC77D3BE63F3E3A9FB27483026C7F5DFE35ACFFBBD1D309E204E7F2B61F3F3 + 04ACA9755ABF050AF37C3EC6C49663A81F361FAFA88F77FCAA5DA1DD9F1300DD + 3F4F8A4BE72FC0628DFE7C866D1BC3B6384749FE124BB2ADCF4DE746C1F99E27 + D52EF1ECABDFD33A8F3C0553B5394F9DAFC66B8EC628C97E69769C13F37C5EF2 + 7CBDE3416D3BFF8EC0F5616953F85506B100F111F5BAAB67EA5A9042EBF09AED + 705827F2298CE3FF2856BBFAD9CFE0E95820B76F33DB32C7680D8431910FC017 + 89DC51ACA715DFB27FA8702DADAEADE13DCF35363ED2DAA87E47F6A39DF3D23A + DD680D11F6ECA350DCC7D3AEB1B6E55AE4F6AAC4FB38A10AC16DDA02F0DD5AF9 + 3BE55A7A05F2020135B1ACE63AE936D64EEFB5B17F12CE3DDC07F4653E45788D + C4DD262537DD016CE029B9E949B9A27311EA5DC957742F686CCCE1137669F3F9 + 777C18CC08C00588A741F2885309C03B7B33A679D99B146F68AB9DDEACE58D88 + 36DFA43A3E45E0E80D9F056008DAC37DA9F1E50391EDA83B146FBD703B658FF0 + B13E7EF4A5D0FCA210E8F37173790A6397C6DFD1E34E2C280013EF7F6794B9F0 + C468F2E42820C503D491FD3B663F750EA7C34EBE27C6F227B11F36B81019830B + BD965D89624E851F4B89E24114BB045FA3558B17C33E9EA8DDFB7DFF3AE805B9 + D6A5E01BC1783C8239DDB10B211802ECD98B6E8DFF07BC3336CFEDB3F0C23EBF + 2FCDF671B73EFFB85BEDAD4F21D753C4D4DCB65CF5A9BD00D135FD2F601B37CD + 810230846489ADC2AF5FB481000B3B168510EB7C5C2CAADB425D38624C125DFB + 98915FEC039FD814C60FB1FD0B84DF014904C67C306E9F5BEB6291D2B67EB16C + AE3F673F25A8CFC554850BAE912D5A5C1DFBD036DB95E5199B0760B8483B578B + F8B1D5471BF00BC0E87B60D79CA80E41BCB7EB023EEB4DA1F535875B5F9FB153 + 36DFF6F13AC7DD438C03BF82D742FB7D6E13084A4538F2504052716D10238299 + B72D8A9DC18FC9BEDAB1C5FC748E53B09687357C1DF8067DEC07405FC4DD81B0 + FBA86C8556ECC02EE37838373FEF16C8896CBEA60AC040855B6EF1086808BDE0 + ECE957290722F0FE51B6A17FFEECEBD466BE0AC649F4A5008C7DE07E639BE662 + FB96EE00B6311193E2F0D867BD0318589CC56FEFDBBCEF15AC6EF4BA9168FFD5 + 56E1EB6A6D8D619BB91C0736B421FEEE9F603FFBD786B19AEF811DC22FC06710 + 400C3EC3F12EF6AD1D97EFFFDA1F767FEF1FFDD7DDB73FFD431BC38EC13DCBC5 + FF49B63808E5FC63031D87FDDE0EB1D563C7CE7CE9F1DB57367F3B96AB8B8FBB + AF7FF4BBDD7FF25FFD9BEE0FFFF63FEA1E7EFABD7447321F15ADF3A760DB8BBF + E2E3B194E339F91A01C6271BEA60DCFC7D8AEF3CFDCEE2F722BF77F99DBB048D + 8D72F53B56ED4A2F007F64711481FB5AFCEEF6DFDFBEA6EE87DA237C4DE4709E + 44E33437CF73384ED4F6B9DA9F8A61BB0573942571DE8F39101FCFBEB77B1F89 + FC2D01F854E76AACA9B5C129EB4776D427E8FB79109F47BBE6279B9C8FFBD8EB + 82F3EFFA241E0F44DDC8DE02D78411B83E2BAC6F21BC9E256AF731EC53C06CDD + 013CE77B9B6C30FF747D3C5EFBD0B8291FF7C3C74CA1F9914DED774C938E17D6 + E5B84E276B7603646D51E3E7A8EB7E816F29B88318FF3F78BCEE8B75F2BBB5F2 + 77CAB5B40A1159472CA89B741BBC2FD13F701E65DE5944465BF3D176F389445E + 258A29B9FE31D0AA8D51EFF2374F52538BD0B8A5A8CE76C7ED64D91DC020481E + 714D01982230EE004E7EDBEEF757DD6E87BB67EFA72D4560F4D1D6BF64987EE3 + 61CC5800C69675B2FDED0BC06020C646D42F8BF265A236DFF6F12D96C6DD31C6 + 9F38A8282A272FB7059E188D4E8E16CE55F371D2763B04609C4CDAC9771D03FB + 96A97F35189C847EB8E042649EFEE2AF4D94775DA271C8929816C30BDAE5B578 + 31CCF808C64EF9AE8BCEDFD71E89BFC5BE2E73DFECF197DECFADFD223F660D77 + 01033C0E7A0FDB4BEB7F6CFD17B6358A28BB088ABE037A7FFD1FC2A008C04904 + F639888B0460E5D2F565B1A86E0B730B47D5AF351A70218A44318973DB0FB750 + E405E06C1F8F59EBE351784016D5141FAF35D47E1DA27175A12EF9CB02A7177A + E7F00BA4B40D818D0C179481CF9F437399EF6D44C7F2E31E0AC7D18565F5E93C + 18437BE4635FED51ACC6444439142788C66B1C89FC6010B34000AE424D10D342 + 05B184884103FB4238078A486A9F12973C5A93F9847514F5839A1788BA112AF4 + F20E606FAFB8B194D67C80EE8BB6D9671B688D5C330BB6208B8A43608790EBEF + F48548BB29222F855EDF664DDE459CE1BE635EF63E0149F084985B04609B17C4 + 5E158079CC108F3B65770588983C361EDD6FC2E3D302317C0CF81488C5182301 + B8BCCF9790FE8F72117BD363AD2FED337EFF55B7C7DDB8B05BFBCBEFFD76F707 + 7FF20FBA37DFFE66B787E06BB67A2CF13AE03D843B82DDDC80DA78B76DBE0B37 + 1FB3FC7867AB73CF8ED7A51DFFFBAFBB2FBEFFB3EE4FFEFEFFBAFBB53FF85BDD + 834FBE6BDF41B9CEF878E67A40F72922C5634B067586AF49B659BB7CFFE13B8A + 4227BEBBF8BDA876FAA688E268A35DFBB4F9F8DCC6B6F7CFE5294B620EA1AF87 + DF8AF1EF117D4B0560DDAADFFFE6C0C63C6D2F81AFA1477D3E678A560DF6354E + DB0ACE8F0EBD03F850F41CF0D4B50F65E95CE0D373CA569EB71FCBA4007C20E9 + AE605C1746E01AADB036780D49607B171C3F0F5C6B4E735B046012AD7D4CF9D5 + 17ED8B8F5F82E61F5BE34364F1719175C139B8DE17F93CA3D823D663538DC11D + C0DABEE39D72B46681BC22AE862CA8BB29EFA37497F803F71E99A1E83223BD27 + F9B18DE6647891B7C5206778736444AF77650682B143E3EEF870B8950230EE02 + DE987F6B6F64158029FA02B6C37910FB50E436E6857D79BF04E0C896F05F2C40 + ED5371779C0E77D29038E264E3C639763E9657C56C3B21C25F5E6EED828130E6 + 90FA3C393B1D769239730730084F403F4870C23D4F7C213824CA3B05D158B799 + C32FA80F47EBCF1D23CE27E514B0300181158F3EE6DDB6A97FF9517E1CF2D90B + FB0C3FB3CF82C52581F679A608B99522D68E6C8550E00DE246989F738A166FC0 + 683148C5DBB28D168CD242925B7C6BD2126697E4F231D013642138CACDA4FD07 + 365E6B616C0E9DF329D0453AA28B9B9100EC1747D5AE7DB5CFD1C7C7353C9ABB + 6411F9D0C5660F73B54654B3353FC6797FB440AEDB1ED4C0368B18D9DFC7FA78 + DAB657B93E88E286B1B15F63580702940A304A4BA459422BEFD87A007914A222 + DB75411DC0FF053C579722AEA2E2AEB74702307D691B8CA1707E1EFA55B8A49D + C7C8DBD956B19682AD421BB6BC0B3809BAD6BF78F19DEEF35FF99DEEEB1FFF7E + 77EFA36F6DAC5CCFD71A0AC0D8677BED70073BEF9CB43942F8DCDDB77C0AC4B8 + 8B3A1D2FABF3D06AA06F9F01FC6FD89D7D5E76F65E06F85C707F86FB35248D5B + DE331E9FCBD79E798AE6202E7A04F421A0CE99ED63127DEFD967D238B336FEF7 + EFDE8E278ECBEACCBEDF2FF1B9C51CCAF1B4E38B6D7A0C743AAEE3FDE49C15EE + 5706EF737BED0B678FBFB4B1BFB0DF0A7CBFE1FB21D763BCAFE589C6E45CEA1D + BE46E82F249B08C078D2817EAFF23B97DF85FE7BAD857E6F02E669BED6F1FEDE + 375D87A8DFC7D1A73173E4587E6747BF4FF61E294FE320ADFAB4D3C72D6BFADA + F0FBE3CDBEAF81AD87394A344E84C629AD389FA3319C0B7D3C2F22E99CC9CEA5 + FC79DAE8BC4ACEBB5AF87CEF7F17703E6CEB1C81DFCFD6BCA3DCC179FEB589CF + CB8FA25C5755D157DBA55FFF08D7B12ED76A24B29D0A3FF6E16365A16E4AB0F3 + 31B70D3FC7A9B9BF4B01D8E72CCD7B9F58BC7FC19A9D47D7F0D23A5E10D3846B + 93657DF290FC349E8A7B10FEEE44E0DBC1D1DA8608A4210BEA9E40001E029F6D + C3F914BCD01B31CA395CB4A5B61611C5DFF1FEB3EC11D04E346D720D0178BD3A + 4BA43B808D2C06E7DBCFFD387843CE0AC0E65BE1FF055B2D90C71C0AC07AB7F0 + AD1080DD9745B5FB2F0D8D53E89B8BBBE39AF038971F830F9424FE96FFF79BEE + F8B593228ABF10830F3E212BA493AB2369D5D0FA514C7802FA418213EE654417 + 8351DC4D108D3D47540744B1A7E0FA17D6CBD1DAB3FB84D8C27051C2E6789145 + E0EDE547DDD9FD4F8C8FBBCDF9738B7D9649FFDF170442AE4251D789BB8BF27C + BBD498127F437461C82F10A9CFF00B4EB4FBBEA2F1AD189263D0B6FD998002F0 + B27AE371BD6D10C77D16DB75F18B76C42F72468BA24B6CECFBB8883E0E5B8CD9 + 625CAFB558BC04ADE5EB2C190B7DB545391EC6204F17C45947F369CF7762F18E + AC9EFE2E2D1DA3B7652124D3C7A17E0FEDD3FEBE0EC4B448842173424DCB7F88 + CF13C5B67223DB75A0D0352B0043348748451117EDD257A1B7FA9DDDFB923F1A + C7C1F92DA555035B1E3B08B52D019836DA11070138F98C27AF7FD0FDC92FFEBC + FB7BFFD95F749F7CE72F594D8C3BCC01430118C7E913C3DE9FE5739004DD7447 + AFCD07732BC72BDD053C385E9F5A5D8BB5CFC6CEDECF7B7CF62C57F78F44C782 + EF2332154FE8F779F4EBFB446B6A7C8B54C780E00B01B83E02FADEA7DDDEB6E7 + 785D2E6DFFCE5F26DFF631C4F00C8FEF06FF2F391D57D88673EE896C10B0BF48 + 8FF486F85B5FA37B36773CF61962EDBD8F4782ED12FC3CEA3E8B00CC183D1635 + 1EEF91FADD9ABFFB5ADF8768FBEF390FE399CBEF61D69C8271AC31AC93F3A3DF + 89FEFB3EFE4DE1EF45FF9B916B4570AC7E7F516BF87B1A09C01E9D93CE0BE83E + F8FD412CE7A9B6A8AFD017A1C7C2A3F98CF368DC925805363D2F22E99CC9CEA7 + FCB91CECB83B987708336E0A9F1FE52DA97308D11844F7C9A371ACE16B457937 + C1417700EBFFFB4D7DE707836B2B43AFB7045E1BE27FEEB2ADD773BE7F6A585F + C78FE23CFD35745E3B9812EC54D07B5F68CDFB14E22FD01A4BEBF89CA579EF13 + 8BF609FF0B3858B35374FDAEAEE30571212AFE1E25001B87887B77BC3D8ED636 + 90E784D2010BEA62CD3FBD9FD03F701ED46546D8D84D21B7D897E0F2A86D2D45 + 055F258ABDE3C360E1FF008E934754A175192A002B1481CFF60F8CAB2A04EFF0 + 86B47140158721DA56E1B6277F28B2F89B3F0C9C5B1FDB1680412F02B784DB96 + 3DC2C746F1F1974340F4E502E89B8BBBE39AC8B10D4E1E6E35FE64484E903CF5 + EE5FF1EBC918D19CB7C9700E44FD7682CD3B84A393D00F12ECEBE18C2F0A6F16 + BD105D4A540744B19E2517C54B2F9C3DC7E6295A23DA2F2C40687BB00821363C + BA6C73F1BCDB5D3EEF2E1F7CDC9D1B5BB3839DD9B7E74FBBCDDE72968ABFECA7 + 473CE7ED28AEF493304C5B405300A6B0E9FDB4B3ADBEC6FFFCF50B4E5C849A62 + 6AB14AE34E2600A3D63DA1D4A7BF1F6FA286A17187E017EA3C88D1C5CDD6A268 + ABDFA25F98CE0BB0CC1BE7DBBE0DC01C14AD39AC017C6D5DEC6D8DCBB6FA614F + 8F2507721CE867BCCFF1718CF171D80E17F2FB185F03F4F9B063711F5B7B3D0D + B47765B13F2DF8D7768E4BFE87BDAD7F2DFA5ACC05BD2800B20FF9B986CDCDE6 + A2C24B15604494613B89374EA8516A5C2346ED5E206AC19856DDA9F196C2B970 + 2C6CF5AE4E1F5F81CF09B9E843E4DD3EF93CF411B5577F348643E7EA69C5B66C + F9B8A1DF0BB61415F9FF7A014442C2C740A7B87B9F760F9E7FDBFDEC8FFE7E12 + 819F7DF1637B8F609C5E38660DDEB1BA7D6460FC871080ED3D5BDEAF3B7C6670 + C7A91380EBF14AC7CC5E6BCB5BE3B366F1107FF7F63D1209C0D1BECFC11C0FFD + 7CAFF1FDA631919D39CC8F606CBAD3F69E1D033BA63B88E8D6C65DC11738EE38 + 2EE5D1D03BEB43AC4D227039B6148371372FEB2979ACE1B1C8F3325B7A4DAC26 + 1EBD9D1EBF9DFBBBC75F747B03F929D6EDC7DC7E4DC15C0AE7E813CE39FDCFE2 + FA9D89EFD3FC7D85EDFE51166133FA3DA8DF7563728D4CFF1DCAEF61C4F03B54 + EDFA7D9ABF7B732EEAF9FAFDEF02BFE7D5C636ED80BF1BD9AEFB316C73DFC7F3 + EED1DF1A1D83E3D3AE7DFFDBA4682CDA5302B0FA35C6C7728E8AE6B0AD681DC4 + 6B3F8A638C8E01A273208F3E02DA9F6B25DFA5D50EFC117A7EC773329FBBB4D6 + BB46CF575B4479C781EB81F8BC7C808ABF04D751D86A1C6C80D7570A7D06FF18 + 17D770115BBBDE026BCBBB09D218E7CFAA001CC5289C577F0DFDC8ECB8DEFC70 + F1A2AFA2EB27533ECF21B1C4E72CCDBB5560BD2DB22F646ECD50D7F5006E3639 + EA8613B77E79483EC4DF3B01F896722D6D4344D211BAAE0F0D45F28AA6B25E43 + B7C1FB09F6897994F844641B61E353C8F5847315821C3CFD965AD91453FF0B58 + 6DC7D06B6977DC56160AC01046A7280521AE46F90D22F1175000DEEFEE1B0FCC + 96DF943BFBC0A5375679C3EA9B54776A381FCE8F7393F90A6301186411B825DC + B6EC113ED6C7C75F0A0DF48B266269DC2F0DF8213F2552373879B8B5B893A1E6 + 0991C6154631C6648D08A97B10FAD78292CFF1F31CCA3C643ED5572E727E39C0 + BE8EC1855FBA70C5F1A8C7E4DDC10B51BD309D23AA03A258CFA163BD6D747ED1 + 7EE1B5E3D62F40D485082C025C3CEF366521607FF9A2DB418CDC95C7B69F7181 + C0E2030197B6FAA8E6EACB39EBE4CFFD04DA2526DD195CF223F0BF82D3A3A851 + 37127A19EBED2D0198BE564C6973714817D288F7A15DF383DC2CEC42E0CD0C16 + 8B0A4D01B8CC536B62E170038A8D63EAD8108B993BA8273187C2FD89608C0AC0 + 5C00E542A94717427DBFB75B8D425E78CD8BB0BE4E94AF31DEEF7D4BE0B81080 + B08D6AA19FF61DE22F28C780715C3CF63046819D8BDCB4F1B8AA4F17CA19A773 + F2E3561BED968F3B73D3A36EAD5DEB31B6B47B81644854DBDBABDF6A7B81C653 + 059A6B0AC00A851E12C500FAB4B6D65F32D61C7E1ED85200A64845DF200F3EB9 + A397C0D6BA03D8DB3C7E8C083F57EF27D1BCD5B6C2FF9DBDC2FE6501310981B6 + 4D826D118093AFD8774FBEAC311081D3E3828DA7AF7FD0BD78F3937427297210 + 039248897CA31780CDF7106D7B1D8B9895EE6AC47BDCE683FF478DBBABD37C31 + CF72CCF271430EB078FBCCA7BB80F1B9BC6F9F33D9478FBE67B8FF1E8D6FF958 + C7D7521FDABC7B9C795ACBC3583CF61A8F62DFC166DBF47F8DAD1E84E19D8147 + 416F2EF179469EBD06E575C031CD77EEF6352338170F5E678AF6DBC76F12A8B5 + 83509FE691E738CCE98F03F7194C8DA57361DEA05FBE5B68CBFF57D85ED7021F + 918FEFC2FD231C27F95E4CFE79FC771F497F7C205BE2F3866DFBFD2DC26B1667 + F3EF03E33CF5BBDDD0B92B83F8095B04FCF95F2EE4D825C787BF4DD16F94D68D + FA5331DEAEBEA9F17C9FA8DDE77918AF6360CBDF69DA95417E1180D33914E271 + 1E65DB747E87F8725EC53FFA1BC43AD2F94639BF4B71AC013FEB229735A56E13 + 8D0D38C5796584D66D11E51D07EAE13CD99F9BD356EC5EF825D56EE7F146EF43 + DBE0754FBAF619539FB254D838D6E51AEED4E09AEE7A0270FF58E429A1F44345 + D74FA67C9E436215C4EAB18E626E2D5C5FC39A1C89E2148D05657DAE85AEEB81 + A30460AE35CA9AE321F9EB1DB0BCD1FAF1A16BCA8CBFE3645C4BDB40EE14396E + 6D6344BA4CB65B4C7A2478A9E96246F6B97E0242AE22826E127A91876D038D37 + 9608C0F5E64912C45C87A19676C76D64E123A06748E22AEBC8FF0D9E6124FEC2 + 26F6FCC6B489DA166F28157CF9570AC966443B3784F3B36DE06F0BC0867D4007 + 543BBE283C8899EA3BEC4B0631832F06FD82F0B6A55C27F783C2BEA8831FF86B + C11FF7C8F781914E9A40704235C09FE4293E36621DD8986F2791D1C91B6C24BD + D63BFB2CEDF09AE3BD5FDA665BD9C9DCE8A4F483255FD0792002AE403A56EDB8 + 085E3846BE63E185286B2F21AA43A2F8254417C3D7A9772C7A2C96CC211281D3 + 5FA1631160DFD7DA20CE6AA58BA874A10A3BE29F37190AC0165B6AA58B55D4AB + C0878511C1D5CA35B21D2270B551D484285AC64B63EAE2900A9FDC7A9FDA7D4C + 215E2C6AE3F3B858B7490B69661F2C28F550201E8C2DFBB631B6E71F652EAC6E + B92B4417F37C9F68CD54D7F90F8163786A0CF6130B8E862E80EA0229FB1EBF90 + 9AE32CDF585D6570F75E222D86E73CD6CBF90158602D0BD578C4670279251F0B + E5AC45741178301FDB572CD8A6455B89073A7FEF03F46B6DF6BD0DA4F9C15E40 + BF2E28972DF2F665D1BD8E6B6DC6EDAFCC57E68B47BE9EE14E51B3A56362C706 + 0BF6106457C84FE47E12691DB8432EDF25376683BC26DC87DC6FDD01AC50B021 + 51CCA150E8A90298F419C3B1603FE5F81C2B827E8ABF1AEFE790E245A06C51C5 + 5DD79FB313CE9BA47101FAE6DF582E88E2522C6D882D36DC2D9A04DA22CC2641 + B188B6101493309884C5EC473BF98B0D77025F3CFF266D2120AEEEE1FD9405E3 + 74977079ACF0C6DA83470C43587E80FDB2F79DBD8F9348056C7EB8F3759B04CE + 32EF725CD84FFF03381D133BFEE9B3F5B1E5586E118C199772B9CF856AC36B57 + 5EBF91EF40A6C6E27BBAC6C97E44200E39B8EB17A22B6CE9BD761FDF0B66C76B + 753FBFF7708CD3EB876365B110F2D37781CC01B5F4B314D18F0D011A77FCBEB1 + 1C7BCDCC978EA98D9D4460C4602E0DA2DA1E8E95C71BDA5A75F81D97BFE7F2F7 + 55FA1FB8F61EC87F2453BEC3D2F733BEB77B5B448EF3DFE5780F4250CEE4D8EC + F3DFABA0DAED3BBBAF65DFF509F87A7BFA6DA831D9876D7EBF737C01F58DFC7F + 7E7BAABD01FDF8E389DC6EFF8EA93DCDAFFC4ED5DF2B07637C3DF6E76C117EAC + A91CB5CFD5F570EE446D68A39EB6A33C3D67F2BE39D23997FCD199D6A8E70E8C + 2BD0D662102BE7820763F9A3F3C509FC796B4494771CB69FB5AE9E9BEB78B4D9 + 35891780431AD734C9EE19C6ACF1D4A4C226F5791D753A78FDAA447111BCCE4C + D787EF8B007C66733DB739729ED61FC52C205A3B591A470E895510FB5E8ABF00 + 6B475C8393F5B9491857C8EB75439BA2EB7A157B8F629B62640D30C4AF5596F5 + 4AD6F2E38DE19896932879A85D855D6BD7CF8FD4649B7349B15C8BBEE3242CD2 + 36AEA97FB43499E4E37B80FD123B9513E1F36A2EB6107521EEA2EDEC114E004E + 22F040CB8A51DD6BAC7FDDF1A133230043CC6D09BA87DAC754A1977D9B4B4204 + E01504609B2885DF167EC7C6603F0B1470C51F0BC0F983D4FCB01DFA2533F890 + 6720026BFF18C24755B871DE4B4E706C6E5EA82D2706444E386E0B3CF121C9AE + F374731EC54EC13C9EEC45681D8EC51326AD2173A82753463A01D3134043E788 + 93B4D5DE3E977BFBCCEE887D9EAD9FB1F701E7F2C1C393D221FDA3B00F23BA70 + 6C11E5B740BC5EAC1E72E1AAB0D6B1F0A2D9CF6D09CC3D76EE8AD638640E4341 + 36B3D96586B1A899EBF68B15C1C205C4DAF3E7DD1A626AF2E585909487E3836D + AAE1164754FC8DC460DA225F117E1709C06A2B60F147FB4BD1C5236FF37179D1 + CBB6F0FB7926E1B7E4DDCBFD9A6BB149FC3D7BD16D8D5D0162F0FAA21F9F5461 + B22CB2CD2DB645316A9B42EB30AFF6EF5B8C2C5C6A5B171CB938898550425BC6 + EA8AF8BB7A60C7E5CA80289C1EEF3CAC93EEEA7BF069222D4C17F098D34C1180 + 2D96D40572195BE7C239EB0228F6B51E6B898F6A287A7714E7CEF9330673C902 + 2944887E9E09E6581B42541ACBE2768F5EA536DE3BA9FFF875F659ECF9C3D749 + B0C2E35DF7F75FA7FFF589FFFB099105C786425624882421B008620989F3689E + 671CB33C17681CC59C56AE8A3DAD18402148817D2A3EB22FC58F45B10C6D8EE9 + FB8C650D8862497485B0E7EE02CE226546ED7A677084E64DA1B168B32E6D44EB + B24D54F025EC27019840B09518C601DC2DCA9C244A163BC0DDC2B84B386D4B1C + EF365DE17B00C296CD0BA21B043DB4B749883430BF72BC41BA0BD580809DF6AD + BC67D3EB405F79CD085F27906A145BBAD3B4DC6D4A1843529CF3F998C8863E19 + D9D33E0E6322D2FEB8F903DC1D3CB6D9FCB83FB24FF0E91F30F87DE13CB0454C + B2D96B833B8DB776ECF17F8635C6D788A05F89E2A6411E6AB5EBA5B9963929F4 + 4FB1028D9AF911D9C37D9D67F89B30FA9DB0EFFC41DF68FE9E50C02DBF030AED + 8BB17A53BF6DFECEE0565BF1310A6CA8EFFB1CD3E7D0A6A89DB9CCD7762BDFC7 + 4776CD453BFDC195F4350FB472D5E7F1FE7AAE12F87C0CE3A6D0D814CFF3BC82 + 9E170ECE03718E8AF3D8B4B538FCD1A2FCE1A2C60FCE891DB55E808E17E52EC3 + F28DF0094321F91A6778A76F845D6B9C8074CD10D6EF997A8C3458A7EBB84CE4 + F3FD39FAEBDDE5D79B370FE7A3F32AED339B2FC4DF0B3B5EB65DB10F317542B8 + 8ED749C644B98712D55510E3C5DFB4BEA5F958DF2A7D6FCFDB9CF7AED0B5B7C8 + 7FE39435C0BA56E8A9BEB2D638F2A3C604E973812D5E1B77FC097CFADA288CA9 + 4F25C47AA75F97BEE36816691BD06A22FBB160CC32AE17808F857AD062AD8673 + 102D2A62A9008C181F57FA490BC38D9551CC1D1F0C2B15646F1DFA78E91BC60B + C055E405D1076CA9F8AB1FEEE8837ED017C001B0AE12C5DD62EADDD181EF30F4 + 24A0FC287FC85064B5B69EACD593361FABFD63404D7F12A4704C7702957DD21F + F88B6D11160B91B7629F51157FF7B68F61DE87084E481BE07548AF55E07BCB8C + 2FFC0E67AACEA1E3307E2E4F7DD105F75CFE6D2009B7145283C50A157E21F60E + 720720C6E5B6845EDF7750288D176B8EE7D04525BF10C53EC163AC07F3446C6B + 610DF965BF70070044601580FD18D1A2DA127CBCD6888872810ACFD12222D085 + 482E4C4E83383B36148093086C583E19D662DBB66561BA177FB1D8EA17C07B90 + 8705572EBA6A9B731E1D9FE2CB63666A6C8176D6F335E94B0BC3B6E5627DBC60 + 6F58CECAF60FFFCF14F1A91616D7CBC272AA45413889D4AFD2FFF84CC7E2C2E6 + 62A43B8171D75F393E147A92C04071F6A1D91E39817020440CA1E0E169C5B4EC + 118CA310035AB95EA8D118E6F83CF693AF885AEA3F15981BC42F45E7EBE7AE31 + D8EE9E7CDE51CC556137BD3678DD4079DD5A306F90DBC8A12F42E3588B6D3F46 + F515A1D60BBB6C5300563F61AEA73E42D862702769BD3BF81C9F131B13B9A871 + 85F6F098123DE66A3FD41FF9A2F79BC2B896BDE527ACEFC7509BAFA578BFD620 + EA6FC5001E57F6A379C1AF71D8EA63CF69239A7B9370AED19C2338B768FF3CEA + 57FC98F3B8DF82C2E0F722FD3EC47EFD4DC1EF037F23BDF87BAC004CFCEF1F7F + DF28022BF4FB5CF6E927ACC358F531D7DB5947FD406D3E26F27922BFDAB0D5F1 + 81EF6B3E73D516C5701B51CF25ADDDCAD773CE563CFB6C8FE2E53C30229D9BE2 + 3C55CECF1372CEEAE35B44F11151EE419C590D3BBFF67F24DA06D7241414CB35 + 8FB6E51A6596E0FA468F1B9FA694A8F505B3AB08BC46BFC03E7D7CDC33D1D863 + 98AB75AA710E258F69D7D445EC5D5D18147F1D14E43CF13A494F94B394A95A03 + 5F59E31AF9EA7A97C44E8158ACA545BE5F36740D52D717E1639BC7EB00FC7A69 + 7DFC74AA57B69C43EA17682BE49B2EAC8DBAFE8ED1C1FAF42F39723CF8BF978F + FAFFCBAC83EDFB728C8F98EB1A4FB514A137D4A780D7B13C3E9E36EFF7310B19 + E968C576134C8D13DD24AA7DFAA3B80F9D3B01B8F04E05606F3B05FC6251A2B8 + 5BCC9D007C0D9C003CF29F92E0E467809D30A5FF251CF91AACF799C8378DCD47 + 05619C4884711F2AB8B87807E03D90DE07B91F5DCC697CE43F25878EA3733B24 + EF5D5D185F87A6009C445AB4B1906171888DB0FDCD94050F8ABB1183DAD277D4 + 45927091E678261796780CACAD8B50513CEDDBCB97E94E831A2B2262B4B89672 + 6511887701AB00ACF59529DF14038133A015C73B627957EC14BAC0D8A25F28B5 + 63331080878BDB203DF2B2D4AC40D80C16AAA37C108D1F2D1EB7F6B115AF31C0 + C7A91DFB81B9F8C57A25DDFD0B1E7E9AE32FED7D61B1E7CFDF747BDCF96BEF1F + DC698E183C367495846F8B850077F5CA7C58447F9D4560DC8187C7BA428CA0F0 + 4B8AC8876D45FD8617345A1C121BA1E3A9A8E2EBAAE8D28A21F0B784C0A5F38D + 72E7C0981EF5FB7DD53808C0DB22027B7195AF91DA7C8CDAB8651E608CB67D8C + F7A18ECE096D421B48F145CCA5C84B1157ED2A00ABD0DB42E3D2A384AD8F4741 + DF7FF9DD041F054D0198C7347ADD68D363EEE3223F5F33C62ABEB6DAF5759E83 + 755AF839B00F9FD63904D68E7C73E0D8E8BC5887C7CDF793CDFADE8E366CBED6 + 1C8C3D2627F24D81394E11E5109DE33CC1EF41F9ADA8BF17F69B331B63A4DF90 + F29B780A01B8F5BB46BC7F2ADEFF6E228EC22F45E4FA7B5CEAB46A01EF47DBFF + 3ED3EFC7A6CFD738C4AFF8181D8F73F27388E6A4B17AEE58CF21C50F34A715EB + 6B4FE694F3C029F0078FFCA3C77C7E8EF3E57CCEECCF4DBD8D7D1F3787E61D4C + 1280E31AF118B826B16B995308C0017AEE3F2B0017D6E627BCBE9C43733CAD6B + 5B5F43FF8FF0B1E8B8D7A5AF69F386C87B2201388A21FA186C9F174291516CBE + 668B56FE2487C67FC0706DB3AE71063109B75EE9F33CAD98A84664EBB1D79931 + 7702701B391E470BC0EFA9C671CCBC1709C045D30AF5AC28670E9FBF80685CCE + E7D44C8D1309BBDAA73F8AFBD0B913800B1480F9A119BCA1A20FC3A904E09B84 + 5F2E248AB9C5DC09C0D720128027EE0CAE79446267417E74E245169CB40E4F8E + D1B7B1D349408939F8A417F1007522FF870A2E2CDE21E9F5C26BD85FC88D629C + FFD41C33D631399E535F00DF0469F1C12EF2EBA204172B54B4D5858A16B6AF55 + 0096FCF52E33A8C718C5F9EA7C64C1E6142C5954D285210AA4CD386927E1F4C1 + 70E1315A60AB356CFF70173068EDAB0AB2B4B1CE52B4C61254F85571B4B528AA + 7DEF035C78646E5AB4BEB2FD21E60BC1F14B8B9556A321FE8230D7A8C2AAC1B1 + A3F9E1FF05A7FF83E8FCDAF776B6D9E72236A12F09BAD68F16ECFD3CE1A3089C + 1EFB6C6DDE417CF6F87502B571472B840608BD69611F0BFC1057703C2006E311 + A40F8BE867BEF43F3FCB9DA4238A10E1450C32E73F16ADABA28A8EA5766D7BBF + 429149C526A0795368AD88B9783F2E988A4D8F29F6A2EA0451EC923CD2CAF36D + 157D15F80614C136127769577C8CC2180AC6E92E606B43FCFDFA277FD0FDEE9F + FE27DDD73FFEFD6EF7F8CB149372D2E376ED7D5F8E67F4DAF03551E88BFCB0F1 + FDA97E5F5BE36B4EF96CD2A668EC1C3A961FC7A3B191DFA3F17339919FC7863E + CE4DFB6AF376F5F95A11AC719BE0BCA3B97BBF47FDFEF780BF1124D9F13BC898 + D21EC518E937A7FC267AF137FD4E14DF52A2DF33C0BEFAD85634D6C7A8F8CBDF + 4E0A93CCD178D6209A07B4ED6D3EF7585AB522BBDA78FEA3FD743E18D83D8C63 + 9B7DDD6A4CC4546D65C93923FEE011A47F7D62E8BF37993B375D1A47301EB69A + 771038A73EB77C3C39C2F9FC58BD0FD720762D73620198D733E93CBFF4C3EBAA + 3A4ECFDAEC00D7934B0559E61C8BAFA37D0AC34BE6C2FCEB32AC8BE364D7E527 + 12804114E789F2421A6B535A8BC2B2E2C75834E69DF83BA0AE5F063E12AE6F2E + 80B5A3FC397BC65ECFE831D0EFE15AFC8D22C7E34E009EE74E001E33354E24EC + 6A9FFE28EE43E74E00163E380118F00B864431B7943B01F8FA442729B42BA398 + 772400E71363D8EC750329D7FA837C9C2803BB1008597022FD81922F22780CDE + 2EB870E356F17151CC31B42E3A8F19EB989CF791B4E86017F2BA38A1626D166F + 972D5444770053001E88C065516484F3A5F9C8A2CD29182FF80CED9ED94528B9 + 63788B05B2AB7EC1322DB4050B6C9ABFC67EEECB0251B99398F5802EBED59CE2 + 5B8AD68888045F8F2E5412DDD708EC3FB78C05BA68BDBA2ADB09D2E38CEF7F1A + 736FB880EEA1B8AAE303CED1DB097DBAB0AC39D872FFFC62B4E21F014D748EE9 + B1CFC5B679F469B779FC2AFBF0BEB0F7CDEEF9E7DDF9B32FECFD6536BC97E4FF + 3C42D8DD4050C131C29DC110CA212C40DC5521E29609C08075BD70429BFAD857 + D43787CF6D11E54644F12A70291AE3C74AFFD3B508AE237155A0301BD93D511C + D171FC989A4FC157D93DB5F7A0F4539E136FD9579B476314BD53184000C6239F + 2F5E7CA7FBB5DFFFB3EE17FFD95F74BFF25B7FD58E69B6E3EE6008C0E9F3E08E + 2FDAD16B41181FC5799FF641541F7DFDAC683CD158ED7B34278A531FF797A86F + 0AC4FACFB5F7EB56DB7E3F3927EDABCDC71D0A6B705C1DFF5DE1E7B174CE8CEF + DBC3DF03FD8DA8768875C59FB0DF4C1F07D2EF53F95D38A500CCDFBD39F4778F + 79DAD638FC5EEE1FBDAAC0C6DF53A06D45EBEA6F2EEB791BB6BEC6127C1E6B7A + 7CBCDF02BF2FE8A7F341B7BF113C779C83F151BDA5F50F39672488E57929511F + DB3E46C76AC11AC0E72FC3722F6CDF81ABC1BAE3FA760D726ED7324958B42DDB + F51AA7BF3E39045E5F29B37F588BF14A7B6D0C45D069103FC754DC541DFA9610 + E52FC55F7BF775CD07D1F74402701413E1F33C93716EBD4B855FB0B5F719B6CC + C756FB779C9868DD7286A9F5D1397BC6DE1F8C3991007C94387ADB91E3712A01 + F8833C4E85A58F80C6FFEEAD318AC5AC19A77953F81A0B18E968C576134C8D13 + 09BBDAA73F8AFBD0B913801D1FBC00FC2EE670247702F0CD80C731A76D3951A9 + 3E157C0F128017109D9439F2493F4EAAED35C3FFEFDDD9678D22F0201627C976 + 11B0B50BA616231154F33F5CF24589DFF7E3E1C5D8B1689D53D605AD8B4E1D67 + E958C7E4BC2D5AFB792C0301188F7CA6505BB1CF0FE2888D1D92E286B923F117 + 040B244D64D1E626F10B424017B2A21C85F12A9662814D17B1062007C71BFB78 + FEBC8ABF846313F51D8AAFE5F1626F04F789700154173AA7F0F183056DC77871 + 7BBC88AEF8785F2F1A3F9A97F7FBBEDAB5DD5A7C062D01988CF6D772D6108151 + FF9ED52FB5CFB0B5FE1AB6ABF2986788DF766CB610EEF07F6E71F72104451121 + 86C7A92D6444BE4350F1C313C5628B71A338ED477E8DF3B6A5F8BAADFA0A852D + 90FCE911C4B19FFF03556BF171B8CACA5E2F88AE2361B501FDDC52B455F1760A + AD3F351E7D0A0560158229DE46C2AE8AB9BC637709CC617BF7E4CBEE936F7F33 + 89BFD8E28E60DC190CD2712DAF038FB3E25F13E2E3F47FD602CDE36BC57E1443 + 3B63158DE7383E4FEBB1AFF8F8680C1F3387E6029F8FADFA096CFCCEF0766CB5 + 867EBF680CD1D816C801ACA535DF157E0E7EAECBE97F0746BF05B443A8137F4B + 004E7FEC44F1F60402307FDF087FF314DA19C3DF3D6FD758B6199B7E27CD96CE + 978A5FDB8AD6F336AD45BBC6CDD915D6F47DDABC5FE3740BB82FB4A1AFB46C6A + 57A1B60AB612EF896A2AF40FEAF19CD1DAD17962849E6782299F3F5F3E16AD33 + 8DC55FD831BFB47D0DEA78728E5D9F804BBB965131B85EE7B8EB9305F0DA4AC9 + 3ED62CE83596C7FC9BF3F88EDBB5F95BF69B201AAB4594BF94E8DA3BD734FB09 + 05E0290ECD9B8A553BE33CADF83B8E63700CFD1A2458B05EC93551AE8F8ED649 + C5A7364F8EB1F9A07F4D015885D10F4ED894E371F43E3A6DE3833C4E85C5FF03 + D8F70B1086379B62D7BC29247F29231DADD86E82A971226157FBF447711F3A77 + 027083C11B2AFA309C48005EBBFEC9E117A3278ABD65DC09C0A707E2EF6689B0 + 7B88003C074EACE424AD0D4E8AB1B5D77C679FFD33FB423EB7F7C099BD86F831 + 4FB0164EF4EC426024FE8AAD5C508CC7F990C1311C5F5479D43F154BDFB14435 + 41147B4A0E1D47E776481E99BBF8BDCEC5312FAE4914B3940DB6F2E8E62AD8EA + DDBCA96DC7C462754162D4D7474013157E23F137B229A3459DD3112D0211EF8F + 7223DB60010C0B6DDA977AC3FFA36648AEAFA3F608EF6FCDA7C548EC0DFC5C90 + E4C2A72E70FAB6F615B57341BB0516B4F138E4F448646B131500FA457859044F + 31C35A188F7702B7E0BC39472E926A9B5BDD0FB4B9F0CC7CF6778833FCFC3C69 + 7F8D1DEE847A82FF996A75AD7FF6F4F364C3EFDA85D97FE537FFB0FBD99FFCA2 + FBE4DBDFB0BA160BB1CEB6B8E317E3AD712CB0D85F841B303C4E5E7CE8FD918F + 2C113718B314CDA308E46BF9BAF4FBB8290E89D7581FCF39026FD3B84AF1D55A + 8F5E772B88B422D0EBE39F29A82651B5D83CEAF7B14B0560C27C5F278AF1FD81 + DD89B70077EEB24D319782AEA765D71A3B3C0A1A71579FA5FFFF7BF6ECEBFC78 + 68D4C5F1C7DCD25CF26B8263AFAF216C2AFE82FA1A1428CC2247E3F535A41D5B + E6A1EDED88F59F2B1FAFD0E7D15C8DA3DDFB75CE0AF35A30C6E7011D83F8B1D5 + 863673D1E671600C8F09F334B6056B7BE6FC4B69D5E1DC15C6FB58E263E7897F + 13F4B72FFD8E41ACC3EF44E96B2C49BF7545BC3D95008CDF2FFEAE11FC56D5DF + 53EFB73927AC0D9FC218FDEDF43680DF59FDFD5518AF31DA8FEA31266A2F416B + FA5C9D87C2B8C84FDB14518D08AD4B68C75685E2969F31DAE6B91FCF2923F41C + 31C2C7F3BC94E7A6D7416BB5B1D80BDBE74B3B8EC1BCE21CBB1E51013861D737 + 1082D3354E708D32818ABE4AF6B366837A5D65D778E7CFEC9AE1F9E8B1CB6BC4 + 94EB536FF7A87F2ACE5FFBB2BEC27C7D1C74F448689F770CE35A362788BC6F41 + 003E14D41FDCB90B51507C6C675FB00E26C223FA6F63CEBF34C8710DFB0D28EC + 52E425692DF3A035D19203AE2100AB28BAD9DBBCB00E1AC4BD37F8FD97BEEEEB + 20E68ECAC102B0B71B49E3F2FE29247729231DADD86E82A971226157FBF44771 + 1F3A030178B3BD48A8ED9D72430230FEFA418962066FA8E8C370220178B39910 + 810FFCA19804B59428E6967127009F183B7959174E26EE1AA393A4C0179D6829 + 3CF15DDB09CE6A6F9FC973FB2E3256FB737BFD0CB3ADF6F65E00383948793851 + 3692E06B176E49F4C5054DE9A736188FF7E18263D85F4CD563E4D09829A25C10 + F9698B2E06354E636F0B4BE637B55F5331D7853595286E395864280B164D0138 + 2F44D48509526C58A0585D583C16514AAD93305AA0391DBAA0A4443191CDDBBD + 8F448B4FC0E7A95F73AA1D7132868FD17A5AB345ABCE08593CD4C5508205432E + 2A6A4C140B34160BC969819B984F17BBB910EE856085B1DA8FD0710F21E5A5DC + 717EDD4FEDB38D6D99D314696E46128D1FBFB65C1CB34FBADDD3CF523D7CCEEE + BD78D3FDF5FFF09F74FFF2DFFF8FDD4F7FFEA7E9B76DFBF8F3EEECC917DDF6BE + C53ECA8FD24C8F82BE7A95EE0C4E2284C105FD4814180B1243288E4C891E40E3 + 18EB6D8AE65104F2B57C0DFA358EB973CCC5FA31D827A39C228A12BD0B77007C + 12B36ADCF1CBADE6B236ED8C63ACF747794BD07AADFAB40FEEFE357B126127EE + EE4D7E1179D957341EC22EC05DBFF06D8DBDB53788BD87F7B3ED1FEEFE4D79F1 + 9CEBB128FD28661067243FE250D3BFEEF85FDB80AFBDC4F00F2E547CC5FB4745 + 40AD77087C2F724CF67D4DFAB7DA6E30CA71F6EA2F5B1DD38FDDF2694D3C9520 + 1D87923395D7F2B538262782755AB5F85AEAEBE963888F9D67FC7BE07FF306BF + 611A276D3235B6CE6D19E331F318653EF23B1D51E31063F0B772F0FB2976927F + A77B3117680E411C7FD307E2A5F53537AAD1B247688CCFD139688CFAD43F379E + E62C8179AD79F198682CDBF46B4C05B6727E18119E273A7C8E3F37BD712E6CCC + 739B0710BBCE630CAE638C1308C091F0ABD4EBA929E43A0BD75840C5D6357CE5 + 7AF3BAD7834B73751C157F392725CA5FC254ED248842E43D81001CF9158D5D02 + 72060230D6BD4464AC7EF4B1E6E60548274A8EE6019FAEA92D584BBBE37A701D + 936B97750D13C77FC9BA698D2939E04801580551258A7D6FF0FB2FFD53EFA3D6 + 3B55CD77CD6201B8E53F065F7301231DADD86E82A971226157FBF447711F3AA1 + 007C6B44E01B108023F157FFF72F19BC99EC0340EA87E12001B8A0ED0244CE4D + F483009BE2FDC6415F68562389A913F56E1B7702F069D093972A00BB18323AE1 + 59C0543CEE384E27BF0D78C25B4F94D3C9BC7D4E2FCEBAFDFD8BEEE2E1A3EEE2 + EA49B7BDB0F783D9D767F69EC023A2EB9DC0061EB34201B88AC1C4FA49A41B8E + FBE18263D95F5851A054D4BFE4C26DAEC6944F6BB7628EE5547580CE6DAAAE1E + AF289E17AEEC9F028EA94471CBC142837D368C2A00977E028F28B6FDE8172588 + ED33FAE931691673F9222FB0607103FF1B375814494CF9148C2B0B3711F1228E + 80B1A41FE73306DB385EEDD8CF8D2C90697CF6F73E102D8AA15D73B020C463D7 + A8813B84D3BE9463A23549AD57C01C937019C4B616F8E81FDD15FC205EBCD445 + 43DAB818E9172573BC8D936281B50D7DC4E560D118B965115C17C5497A5C3230 + BFD2D7B0B6CD3D817A658EF1FC745BDAC96F63593F6D11CB9C9AC7FDEA8F416E + 9B1D585C9E17E6A9C89CD35DA11064CB9CCA7EC106513889C18F5F771F7FFD93 + EE7BBFFE07DDF3373FB2E3865CCB4942561612CE9E7CDE9D3DFABC5BE1D1D096 + 87FF018CFF93BCBD6FF56DBBB32D6809C05CF8675F859129D103C05F859F47C3 + BC08E6318779AC55915ACC2183B81652BF62C78C829DBF6332AC61C0C7D89D1D + E3DD84A0A86D0A8F8CA51F422AC554CD5FD96B4EB416FA6823DEE7A18D98A5B0 + A6F6B5961779D9561F6BE0D1CEF87FBC5EC005E8ABC8CB760B157F416AC366B9 + D8261BEA5C593D8BDFD8EBC179F8FD009CB36FB7C01F54E035E6FB90EF317D9F + F0BD802DFCF88C211EFEFD13CCD3F2E1C3EB27EFD19AA3C818F0937407B3D9D2 + 7BBFD4869D778F53A84E60EE882DF53846F507E8789A5BF34BDBF7F9DDC0FCE4 + 2BFB49B46EFA3EA100EC7CE8F3FB46EBD28EAD07FE1651FC52E6EA45F389E61E + E5CE23BF0389FCDB567FE3AC9F7EC70A1ADBFFB6F4A43921C75E9711B02FA1EE + 1BC7CCF3E05CFAB1F26F6BBA2358E690617EFEEDDD587F6773A87707A30E720D + FEB6F2B7368118813939AFFF1DD7DF5E8A99B0D5A77014D2EF6AADD3FF6E7B58 + 4BA11D5BE46B3DCDA59FF6A84E94E3ED8CE536427D1C331A57055EC66A8CFA49 + AA8B2D907346A59E175A3B3A8F8CD073D3C87F7220009F1564EC69EC3AA72900 + E31AC8CEC1171009BE9E5C6F827A9D85B1ED1AEF3C0BC01BBB06D81A7C24F4DA + AEC3FCF520ED4B41BC0AAD919F30CEB3B36B24FA19EBEB2C8135506F6FD74567 + F6FEC4967593200AD1570560CF420118C046D1D6C7F9FE1C83D8ADACB5893FFD + BF5FAC13C00E7FF179588BF5C235B589FC5F56F4B8454439B3D8B1E6BA2649C2 + AE5B3B1DF869AF3162BB8600CCBB7EB51DC5BE3704FBCF7DC2CD70DC4F1F7310 + 32068FDD7B7FDC0A9302F0C628FA55D5B37C4CFAD78AF7FA3A4BE0580750C70E + 6CFDDC6C1E837EB1AD81D867D031EA38958B91B0AB7D157F7DDC87CE2FDD23A0 + A3378AB6817F1378C118841F9208196B92E0837E0CE1979D7D1966A1D9FCB67D + 5F04E0D361C7823FC4EF42009E105C417447AE9E580C4E2EAE03C709C623271B + D3FFA523717FF10870928613F27C916327F37BFBEC9F6FBA575F7EDEFD8DBFFB + 77BA5FFCC7FFB07BF39DEF59DE791282771736CF7427B0D6C189DE14FD78B78F + 68BE87135D5C457EB57934778A63F36E3BAD8B58EC232F544114E3612D1E9FDB + 709C30A7681FFDDDBD5900760B1D1087530CFCFD224A5E80B136044B9F53FCFD + 828B807862FD5C0782E734FD42CD34512E8862C1544CB6CB6253C12F94F90532 + DA74F12CEF7F5F8FC783B18CD36D04EF10E6B1E39DA89EB4F087D7079405C070 + 1150E297D8142E4872B114B63C8EEDE77D9B5F1282AD0F01183EC32F2263011C + B9F06DF17864089D8F3EADAC1E9A2FF12A5317AE33BAD09EFAF7ACAE511F7159 + 501B44513C56B9558336B62BE57FF0569C5F05022F12D0C658DAB8856882F6EA + 9E1D272C6A5A0C6C29D68E114418DA2016B14ECAC5FE94FD022A00EFE47FA7B6 + A06803189BC675710911E15494633BB2CDB5B125E8AFCA6BCDB829B486DABC28 + A8C0AE681E612C7D1AC31AAB3247AD1BB529A8B206F298EB7348CBA67DD623B4 + D1CF1A73F878F6516B85B996BB71014560157055F84D627141E329FCB24F9BD6 + 607F93FA2516626B998BA2F3D539AB7DAA0D6A3DE7D3710073F13A7A611EFE95 + 7BAFA24FD8670DE6C1467B04E743608B6AFB3C85F33984957DFFAECABC68ABED + F21D80EF08FD9EA80270B1F37BC4E3F397C2BC28DFFBF47B710E8D6FD5F17ECD + D55A9EA93A53F18AE6DC2453F3A9730F7EEF7C0C6BD14E5FD806EE7794BEE437 + 3BFE002A9F63E07C239F87E83909CF3F08CF4B3CCCD15CC01C9FEFFB3E8F31DA + 9FC2D703BE9E021FCFD5688BF28F416BD45A38AF2CE7A57A0E7A1D01D8DB59C3 + FBD957221F6D4AF59FDBF102129FFF9813E7F16A23C3F3FCFCC7ADEC47F186BB + 6E990431722D14D10BC4122B0CFCF8A3DD7D2FCEB65897EB3AB5A9500B18A371 + 68F35A175098E5756FAE91FFD019222D62E8673EEBB11DC16B61C6618B7A97F6 + DEBE67E7B817F65E44DDECB39C0B8B9B0022B01775B9BEA2B64319AED38C410C + C78DFCF081C81771C8D840E397E61D120B0E8D3F05878E89381E6BCD5D9A3FA2 + AC638ED64283755A6E95ECC7D650F1173404D0D1DABDC54108DD9E590DB3A1AD + 62F020D6F31EADF1639FF617F6DA41F4855661FBB43BB7D7AEB56F374D3976E1 + F17DE7C715E33B6D49F5266853B8A1716DA8BEA494D8490198712E67007D0DA8 + 9DA5BEC46F6CDCAAA5593BDD5C5974B5CDEE81BDF6F63EC77E32AF35167D2E86 + B540BEB9F3BC5BBB1B3C55E353F1D76B7F1F3A7702B0D818B3E44D9072F8A6F3 + 6F44A5D49E25FCB01F0EBFB4065F5CF8D2C217EB3AF3EEBFC4DE36762CEA8F36 + 7F9CDF220DB195CC3D92B93FA1B8061C4389E23C4BE33C91001C88BF209FA8E1 + E4CD2E6E7007F076D7EDEEEDBA9FFDFECFBBFFF3FFEDFFDAFD2FFF8FFF7BF747 + 7FFD6FD87BFADC622EED0702AF293E5F36B7541375DEDEC9E9E9C1DCAF0F8EA1 + 278A9B22AA11716CDE6D0617A15317AEF0F90B5D4F742CDE87E35405606ED35F + A5FBC588B24062C7401740F222CC8BDC67AC2C8E840B358035A4CE603126604A + C08D505157515F2B96F6A1AFDF174031968C85DE7EFF37D862E10D8B80587083 + ED02768BC7B6E4B12E6BE9E25B04E3D3029ED56F2D32CE09BF8C8BA09FB5FD18 + DE869C3C9EEDFF7D9B5F890314802902F3EE278ABE89C75840FEB483E0BB7964 + BE27AFBB0D307B1227287E60C1F8AA5F7026C98E856327005314A63D09A65864 + 2E0BCFBAF8AC7DF5CFC17C9D0F6D111AE3E3592FDD25984418B3151FE254A84D + FF27B888BD115E00E658DE768800EC89C4262F42F1B5539FFAD5A6F6C8A7501C + 53914CFDF4010A78DEAF31BEEFE7413B7DAB2296A1EFEB443625AAC9388D8F6C + 53B00E50D1925B12E561ABFBBCC26B26426E24DC020AC055047E60B9B69D8A67 + DBF7FB3644E37EDED17CA37D896CB463CB7D533BD0FDA69F3E3D8E446BB5608C + CF5995CF82C6D1CFB6E630CFE7D2A7E8380AFD2DBBFA75CB363EFFFEFB43BFFF + BC0F5B7EAF0CBEB3246E09C851A67C1E8D55E8D7F96B8EF65BF89A0A63585FC7 + 69C57A34E7A6F073213E4EF7610AC6FB3A516C15804189ABF1220067F1379FE3 + F07C62AAAD7DA0E733F42BD11DC4C0D7603BF247B4EA28DEC6BE0AC0DC6A1BDB + 63D15A208D55CE41795EA9FD43F0E7BF1EEF67DF5F13549FB329D96FE0EEDF9B + 16800F0173E63551E42F50E4556A9E3E9DE94801B8176FE358B6FD75AAE6ED6C + 7B86EBBCB286B6B563C43B7501EF08663E6BB2560BC6A3C6B9BD072102638B7E + 8EB139953B7D29F6AE20AA161BF1E21FD757D47628C3759A3173B1EA6BC52853 + B52218AF4471CA21B1E0D0F85370E89887C6CF52D631476BAF6E3D947E0ABF24 + FBB1352201385883E7BA3D855E8ABD88DD14BB8F4D7EA951B9ED6BFC363FCE1D + FB06C1172230FAB043F4AEFB5F6C35F7A629C70E638EC67DE7C715E33B6D89FD + 624BFAD40201781153395A7309258F3753429CAD42B0EDD766FFA0DBEE1E74FB + BD1D77D6E7584B28E30C743A08BF44ECAAF1A9F8BB44FBFB90B81380C5C618FF + E660DBF7B54E62E24D394BF8613F11F8D2C217EEFA4E00BE8D02F04182ECA9D1 + 71FD1C8E9D5324FECE8013FD2400E3046EBBEFF6F7CFBA9FFED6AF77FFE25FFF + ABEEDFFCDBFFBEFBCD9FFFBCDBDDB31F8B33FBD1D8DB0F087E24D28FB311D47B + BFC049EBF5E1059512C54D11D5883836EF7D21DA275CCCF262D8FB4894F7B68E + 132FB895282EA28ABF4404E07E51C2FA5C20496D2358CC6851176A086B943AD9 + 6FB54F880ABA2D34567368579B2E3A4D311066D1461D3B7610CA770F3EE9F678 + FC61F141DCF5796CABA04C5B0BBFB0A77DC52FFA45761FC33E1631A3472C32CE + C77B92FF1EEFF6B13E8E83D523FB279F75674F21C07CDAADAE3EEE56E5EEDFCD + B3CFBAED73B33F7D3D102256141265C19A8BCD5C5C467B63DB96088CFF9B5B17 + A1D9967EADE5FD85BA50EDFAA700FBC77D8498834711430CD67D85A092628D64 + 0BC45FC078D6663FB2A948E36349128270ECCBEBA1AF8B12D9688FF25A76EF53 + 418C0219998AF3368FAFC13ED0B968DC4A5E23B0B3F7704BAC5401B115035AF6 + 39340FED29344F8F6FD35784590AB91473F17F8179A72F45DB64B7B6BF0B1876 + E2C5E098EC9F128041B45FBEAF766CB95F3E0E767F3C38B68F5D0A73345FC761 + DBD7475BC7D6D8957C3F305EFDDA57BBFA239B6764B7CFBF1773F5BBC2FBD4E6 + ED9A3B85E64630AE653F165FAF45941B71482CB989DF96A570EC43896A81411C + 7F53B195DF571F8FDFE87CBE81F388F6F985A2E72744CF4FE8635C047398AFF8 + B843E03C743EDE3E151B8DA97E8D9F43E3D31FEDC1E6CF2DCBF9E752788E1FF9 + 5A8CAE0B5CFE54CD3ED7F6E13A02F08028FE40B01FBC8ED279068C845FB25000 + E6B5295897EB3ADA15C644F8EB54CD81C07B6EFBB0B37DDAD9F5E1FEDE8B24D4 + A63C8C657EDEB5BB5E70ADEBAF8B510763F18E62C6A43521DC1840B1B73CEE99 + FFFBB7F5F867AEAF44BE16C37599D37048DD63E670E8FC0F8D0787C65F97938C + D7B8E16411652D73B37D9460DFFFFB3C2F00A7B6B129DB14E705E016659D1E6D + AC6D26C1777DAF5BAD2EAD76BE5396A2A432AA036EF91A3FF6A50ADA65BF21FA + 729FD25DCFC5D7DCC79B425E83D1D8EFFCB8627CA72D39BD296953B751002645 + 8CC5DDBE5BDC01BC3EEFB6FB7CE76FBE83F9CC8E7B1684C3715B94FAD4E7D271 + B8138027B91380C5C618FFE660DBF73587355A6FCA59C20FFB35295FAC4DA29C + 0F12FDD1EE7FCC6F84634453E41C2BB62E8027279E28F6244C9C74B54EECB2DD + 2E5CD0DF5F74DBCBB3EEEAE9A3EEE9472FBA672F3FEAEE3F7962171C76E27086 + CF0AEE022E2710F8816ED29EC7ED02C7E3FAE8C59512C5B688F2238ECD7B9FE1 + 453268F95AB6B7719C38DE1CA33C6CCD9EEFFC0536DFF47FA8FA4588BA305116 + 660688ADB93013813C62FD1CCFC597D3D00BB76DA258AD31F40DF72F42F71162 + EFD9C34FBA2D1E837CEF45B7C1A39071978581FFB9365878737071CEDB08FF6F + 9BDA22B84038B56818C5B6163697D6618D4C2FBE5288C5C22E16C379D72FC4DF + CB175F76E72FDE74FBE79F77F73FF9A67BFCE657BA7BAFBEED762FBEE8B6CF41 + 16432221A80A18B220CF056502DBD680203C1080DD0274A2F4A33AB47B229FE6 + 706E9C9F12D95722EE6C9F7EDEEDB1EFE57F84AA1832C891F1E957225F2B9E36 + 8EA7E8B157F83A805599FF14510D4F14EB453225B2D10EE6C6E57800797CCFD1 + C738D6642CDAFAFED43189FAB5EDE3229B423FEB604B7C2CE35A3EA0FB1AC526 + 5B116B21DCEE9F7E95FA49E4C55DBEE54E5F157AB1455FA18F35B085AD4DAEC3 + 3944C78CC7DFCF9B7DB5A95D6D2D10C7F1388E12E58056FDB9B1B97FEC33DEE7 + 61EC9508C0F4715EB42BEA9F82E3EA58B4275FF90EE077917E6778BBFF0E619C + DAD9F73096B99AAFF628C7A33180DFC9510EFDD8EAD8C4C733C7E3ED73F18770 + 8A1A84C782FBDCF21D83AFA744F1C0C7E00FC5F27907CE2D407F6EE1CF35F4BC + 843EEDEB790E98CA41AC9E8B710CA0770B6BEE1C3C47F2F3503446818FE333CE + C74FE52F218D519E3A435A22F0528198E7C3918FE879B38FF7E3787F8AB92CE2 + AFA176FCD1A59EBB6B7D7F9E4FF23580C60D19D799265D3F05F69040F41D62D7 + 70E55A6E091471952806DBB5BB36D47888BA0F1EBFEEBEFB83DFEA7EF0939F77 + 4F3FB2DF6EBB76038CD95B7B7F663998A3E5B3BEAF0BF49A18313A87A1CFFA45 + F8ADDB00AE2DF935266FF70CD7628EE7266B4F71E87887C6BFD750043E4008E6 + DA2884DF6D01626E627D95C45D0F62227BAA75A8000C7657DDD9E5D3EEE98BCF + BBE72FDF74F7AE3EAA02A917829BB522FB2D01FB5245DE2272D307B17BB52BB1 + EF623FCA98E1F1E5EBA3B6B7451DDB694B4E6F1A69521E8D9F632A476B4EE1E2 + B7DBFBE92EDFF3F347DDD939EE00BFB4F7346EEAB27D8370BDCA82F0E60CC75F + F65761DDA03EF73F1D833B0178923B01586C8C89DE1CDA4E7DE4D89B8E7140EB + 1E4CF481BF2EFCC26811E57C90D897B8FD1867F28FFB6D45C5596D5F8BE0AFD6 + FC38D54F319AD0BE04D4094EA894F0C4D34ECE20FCE6137F3B913ABFD7EDEF5D + 74BB0BFC15D3CED8DA054F7EFC337E2CD6E91FD8E344C1E6871FE849649C5B0B + 8EC7F5C1F16B11C54744B911C7E6BD2FF80B52DA88C6D287F8C8EE6D378DCEB3 + 4514DF8BC0F6FA3A01B88ABFA02C54D44510B18D7C53B05EC9CF39CF4E4A2FDC + B689627D8D0D1E650CBB6DAB186BF3F5E84255DA27F41FBCEC760F5F76FB479F + 74BBC79F769BC79F746BBBA05BDDCFB5B8E8866DAA8DC7ACD91636C0F1180BB8 + 08E8171FE957608F160D237CBCE64DC5B14D3FED202D945EBD4EE81DA958DCCD + 0BEAAFECB8BC4E77FEDEFBF8EBEEC1A7DFE91EBEFE6EF7F90F7EABFBFEEFFC51 + F7FA87BFD55D7EFA75127FC1FED99B8A8A2555C0708BC7BAA88C45738CB9C396 + 77C64A1C637471FD3A0BED3A3E60EDA8A6DAD3D658A9005C04202F00235EE7CE + 7AEC7B22DF547C3A5EB8FBB2C071754E1486F4755815714AEDEA679F44B514CD + F1DB08D6F3C0C75A911FB46A695B69C5028E453496682DBEA7B506633456EDCC + D1BC565C64675F63FC7C535C117529E0A24D611736FA890ABF84F9AC017CDE10 + 0AC098671648F9D9E7BC3957CE7F30E712A3D0AE3EBFBF8C433D3FD64A3E9784 + F924B26B3CD1B9E878EAF3390AEB324EC7A34FDB24EA6B9C1F7BE02BDF3F11A3 + EF09F91E896C51ACF713DAA3EF2B7EC74E7D97D1E739243602F1DC3FBF2F8C69 + 7D477B7C5EC49298EBC0FAD1383CCE8AFAA3B8C80F3446F1317804743EB7C079 + 07E8CF2FF43C035B9E8378A67C1EC6EA3914EDFE09288C9D62498C87632B4B62 + 8E8535EB7EA14D01183138FF9C38DF9DB2F19C5F6D3E4E6334163114A17D5DFA + EBF6C28E0910DF26CD1BE7F57D0EC7D2F37C3DDFF7F153F4B566E03554E01BD4 + 420C845E5E77B1ED04605E9F4E5DD311C412EF239AC73A9A83F59D97AFBFD7FD + ED5FFCAFBABFFB0FFEBCFBFCCB1F75584F3BB3395FE27D826BC6EDE3FC07C5BB + BC46C09AAC4774DD007E1D877EFA36E9DF0E990DE031B16CB32F3615373DD1FF + 87F5C4EB33CB407EEB31D427E30021B3C58DCEEF1D12EE8F137F67F759D63355 + 00A6C03B2700D3CF6DB2E34694705DBAC78B8D782CF293179F77BFF53B7FA5FB + BD3FFCD3EEB337DFEFF098641F3F1228C97BB0BECFBB802FEE3FEF1E3EF9B47F + 04B4CD1DFB45A13BD9B8AF6F63BFA6C67897C71563A7F1B3D85935270AA0629F + D4A3347E09AD1C5FD713C5DB76B7BF4A0270FFFF81F128E8F3EEC1D5D3EEABEF + FCA07BF3CDAF76F7EFDBEF5C5443F1E3943EF7BFB2B2FACEA67DD5F57CDC87CE + 9D002C36C6446F0EDF7EAF05E028F683063F1CFC611FFEC8BF551608AA2ACE92 + 286E168C65A4479610F1CFD695783F1F4F7822D5A075E2996DF9047A7B6E274E + E717D63F333B84603B59C0FFFDDDE1CEDF47DDF93DBB38DB5B1EE6E7EABC9FE4 + 7D5778A1D4E2D81C6D4768FC14C7E6BD2FA40BCF095AF1DEDEE290D863E07CFC + BCBC5D991480FDC2451129470B20C1024773A104B1129F63ECC4EF1AF48B37D7 + 23D52B8FBB463F09C0F6BDA30230F75FC1821317ABD27E619F2C8E0B6B3B2CB2 + A1368EF1A56D53BD97D96F60218EB5B91057C792C53AFA74C18E3ED83476B0A8 + 5772C89C5DFBBA553B1744FDC2E8204604E0DDFDB1008CBB7FC1C5476F8C2FBB + CB8FBEEA3EFEF6A7DD57BFFEBBDD47DFFB69B77BF159B77A62F59E59ECB32C00 + 9D3DFF32E1C519BF78AC8BCA58CCA6E040F157E318A38BDE7E01FC10FCF840EB + 476329697F20363DB63EE6FC702C9830D7D750C186A20DE3311FC6D1C6DCFC9A + F4A800A3355857C1EBC071FDF8112A302DC1D726519CF6310EDF273A26C53685 + 311AE7FB3E97FE25F3F1B596CC956DC5C7FB1C0FE31813A175B82F9C5F8A2942 + 2DC55BDF5741973E8576FA7C9CF7657BD9DAFB4FF78322B0DF3FCE5FFB8CD158 + E27D446318C7B61E17E26D9AA77E8D619BB1C0EF036323E8679E8F47FDB91A0A + E7871C45FDD8F27B40F1DF17EC7BBFCFD1EF150FFDFEBB2762EABBB6858E1BE5 + F8BA5A9BB1DCA216F1350073D516A1B9AD581F3345943FC7549DE838288C67DC + 54ACC62883186C9D00ACE7190ACF39227C0CCF8D229FF6D9067A8E431F7396A0 + B98A9EAB81561CF0B16A53D487C73A7BBF82388E3739BF24A82E87E7C57A8EAF + E7CA1AB314AD3FAC61ED4B9BFBA5CD77908371715EDFDB381FBD7620FD7580D6 + 68D3D76A93E2A2EBA81616C7474213BD1EDB9CAB988BEBBAF8FA8FFD3E767C2D + C87ECB568559E3C9CBAFBB1FFFFAEF773FFAB5DFED9EBEF82A89BE993C1F3EBE + 39AAA3E8BA01E290A3793D88B758157CD9667F8100CC63D422CA5982AEE34CF9 + 235F0BAD39E2840270E4BB362798DF312C3E7E0DD23A26D73E0D157629EEAAB0 + 4B1FDB241280D38D2AC1BA34454D4FF2AFEFA5BB7E7FF0E3DFEE7EFD377F3FDD + 058C3B6621027F08770093171F7FD9FDFCF7FF5AF7D7FEC6DF4F22F77A9F1FFF + 7C7E619FFDB26FCD7DBC296EEBB14BFACD50E8CCDA5111408B083AAB4769FC75 + 886A2B1A27F1B8AB77CDFFFDBBC7EB6BFBB5DA77DFFBFE8FBA7FF12FFFDBEE9F + FDF37FDD7DF1E67B663BB3CFC1455F47EA0DF64FECF40DB813809BDC09C06263 + 8C7F73B03DE823C7DE74686BBD01F2A69C25FAC05F170ABEFC42D3B68F51DB07 + 87FD80D88F7166F8437F6A46E2A8FA9D08AB30D6E7131FDF0463147012520560 + F5FB78ED3BA2B978A213AA88C913B472E298FDF603B1B7CFFFDE3E6FFBFBE971 + 1049F0C55F8CD909D0DE2ECEB610677C8DF716EC738FBF586A71AA3C258A8F38 + 36EF7D627C319A2F54B18D62B51DC52873FE6358F25A706E11C708C049E09445 + 0B1F77C842498E192FC82CA55FB8E999F31308BCBCCB17A41C2F00E331CEB268 + 9680302BFB98EF36E8F71B774EA405B5879FE64538D4C38203C649F5700CF168 + E8B2A8F9A0177253DFD0F1FCC29CA231DA669DA93B641843BFB65BA03E625A77 + C4700E691EF72DE69E5005602C187FDAE10EE024043F7B6D7CD6ADCD76F6FC8B + EEDEABAFBBFD479F77ABC73626C45F6BEF9F67E18722B00A1F2B88A5B2789C16 + 90CB823217A62321215C780E6C4B995A04A74FE3BD8FA8A8A031DC87685F984B + B1664089C57C18471B73754C128A30417DBC0EB4634BD4AF8292C6B1AFE29747 + F394B938D8BCA8461BA12FC2D763BEC7EF8B07E3B4EE5E8D7219832D7334175B + DA086D5AC7D7980231CCD1F924BF88B32AD6827477AFDCE1AB42B087399E3836 + C7AFEDBDC7F9E8BEEAFC68F730466389F7118D89C6258C678CFA148D535A391C + 8779446368E3BC746E3EDEB72356E5CE66C6B562F93DE0D1EF0BB6BDCFC72356 + 6DFA1D069807F83DD5A2F55DDBAA4760D71CB5B3A6676A0CFA14E6447ECDE5B8 + 4A14B7141D678A68DF3C73313AE65C2DA0F594410CB6F58E5F9C7B0C05602FCA + EA790FCF5588C6693F8A553FC620ADD825B09ED6E07C151FA379F59C4A7C6CD3 + 473FDB38F79C128159A755ABC615C1B5DE192C226C0B7F8E9FCF93B38FFD3934 + 1F797E7C9C73AF2ECC170AC0C05DBF54FAEB03D25F0344F1311823B293E40FAE + 8F067E6F730230C1F5D8FA7C2800E7B595F1759EB6D5D6BA568C6269C7786776 + 7D7369EF91FD855D13ED9EA4FF05BCB16B45DCF1BB35DB1ED732923785CE41C7 + 5431B8AE17E131CF2AFCF2D1CFD2E7FA5284177C5B44B94BB96EBE325E231A32 + 17335567E47377C91ECDA9EA90036A719F46FBB614B7EEA9A22EE17AAA8ABB51 + 8C6E5B02B00AB811107921845E3DFE24DD1D8B36EC782C3444608DF5B513EF7A + 4D7F6AFCE2C3A39EBFFDFEAF75FFE53FFB57DDBFFA37FFBEFBE14F7E96F607FB + 777969DFC32556F7B1B9BFA784737FD7C7D083F93801983AD480E2ABDA922788 + 1FD996E0EB7A344EF30C3EDA196BFB5B3CEA7973D1FDE4A7BFD9FD37FFDDFFD0 + FDF3BFF86FBA2FBEFA7E7A143404E0D163A0AD5EDAF7061C9FFD48D8F51A9FA2 + 711F3A7702B0D818E3DF1C6CFBBED619216FD645441FF8EB92BE300A6A8BFC6A + FFE0509173FC637F4A46E2A8FAEDA460D0177C9E27CA09C118869EACD03640E3 + 353FB047F351C213AA80D6C9596FE3163FF4F659C33F8187186CAF5FFA2B535C + 14E0E2C12E36061710DBBC1D321CE376D3CF5B2F90082F8C22DF5C6E84E678A2 + F88863F3DE2778DCC9F0C2741CABED284699F31FC392D7029F1FCECFD30BC0E0 + 99F1BC47162ED26205C45F2F0097450B2E74E485978CC6B4C871E30599A570E1 + A65FC099F63306DB5000D6BC24D68297439C00BCBEC0BE5B8EED4F5A9CBAFF71 + 5E343456D6DE3F79D57DFABD9F769FFFCA6F7497CF3EB3FA56DBE2D2E21C16DF + 0CD4C5B1E5421C8F35EC5CA8A34FFB9C936F73618F306F8AB958F8501F5B5D80 + CD7D6CF31CEADCD3C29CD99D000CF1778D9AD85A2E1E8FBD7BF6BA5B3DFAA45B + 3DB4FC27B635B6CF5F75672FBFC8027011D0B0F562DA4A0460BF00CE3EEEA44D + 8F5796C5E6D555B0F01C2C462F81391CEF903ACC615B85886DC2FA62A7CFE727 + 9B884B146E92E814C58A4D6B47309E35158C45614B896289F7AB88A5739F42E3 + 9983BA6C63ABEF13803E615E0BE65460B73CF6D146DCDCFE22C6CF4361BED6D1 + 1C823E73D4AED03F1717C11CBF2F4361D6E654B6107FF1FF7FF5FF00FB3B8415 + D8081F0B1DC5E5D83286BDF7567CF432E628F34C141BE69F8E9781AD1E2BCDF1 + FB3A857F3D14DA18EBFD80AF1F6B31B6150F34C743BFEF3397C0E7EB446D6EB9 + 9FE8131F9BDAE57B20023E158019AB6D1F471BD0EF226D6BDFDB97FA01E7A1C0 + CEF8C8DEAAA5306E8E2856EB705C258A5B8A8E33057FA7FC6F95AF31F57BA663 + 4EC511C6780631D83A0158C558BF8DCE55725ECEF5FE2972AE0776F8E3F3B143 + 617E74CEA6714463744B5F0405E09608CC3AACA5F6415C115CE70460FA07E7C6 + D740AF11B43EC7CB02B0B5AF2900F3FC3F8A471D6F5B02E710F9E6F0E26F4FBE + 0B385FBBE1DA8E0264B9CE4BF6FE1A93D7ADB4B5AE1519A3B180D7BE588781F0 + 9BD685368FBB333BEE80B13E2F6A739BE690D68FFAB9305F63D2FA9008BE5C47 + 52BBDA2228F0CE11E5BE2BA2B5A2393F5952A3DABC707BAC88EBEB5C87236A4D + ED3389FC690D93EB9E058AB71476158ABBBEAD7D6EBD00ECC54C858F444EE22F + B688C5FFC7DD40042DFF3737F98779AC37E05DAFE74F8D5FF406ECE7EB2FBED7 + FDD53FFD7BDD9FFDE21F756FBEFE41DDFFBD7DA6B76EDF9AFB7A6A38F732CF91 + FF5D91E6D36B4A031D2A10481937C2C586F625686E848F2D6D08BA7B3CE5F3CC + DEF3D6C7FF04DEECEE751FBFFAB2FBADDFFE83EE073FFEADEEFE23FBBDB41C3C + 267AE30460EE770B8ECF7E24EC7A8D4FD1B80F9DD57A73663FE4C48BB0EA7B5B + C8F8D7148097BCA07CB3E81BC6D37A73D43725F2E50D3A20A8AFB64AF0817F2B + 5F78FC92BB4D5F7427C58EA1FD10F7E0C75E51DF81402425D61F88A2D26FC59F + 8EB22F6B6B03EFE7982B3B1E059CA04473E39C75EEDEE6F12754D70127683C11 + 4F7D3B11C05F986E764FBBDDD9733B317861274176A1B6B30BB6EDD331E562C2 + D7BD69E64E3CE7E1BC71129B2F983C7A610434672ACFE3F39428BEC5B17973E8 + 3EBE0DF4B8FAB1D1AF17BF4234473D16915F99F3674E775CD37E9C3D4B7FB1DD + 3FB6AB07FE3427DBB795C50FEE00D6BF5AC7420BC5482CBCC096C4622CA09418 + 8D379A0B204528667C8EC90B31C7A08B37FD22CE7C0CC5DF4800A67F7B1F8F64 + B67D2E0B6D5C18CB8B4F5C64EAF793761C2B2CAA2116ED279F7FBFFBEBFFF17F + DEFDD93FFEA7DD8B373FB0E366C7B91C4F2EC0A19D6ACB38DAF771EA879D7187 + C0BCBCB839DCCFBCD099F79F63AB7F6C83789D056C2ED2ADCE6D8EF8DF6CF72C + 0E77031701388345DF8F131081D74F3F4D77FB620BF1779DEEFCFDAC3BFBF8F3 + B4DD3DCD4204840D1580215CAC8A00CC45682E48EBC2B4429B2E3C4FD9A21A4A + 1ADF621047A2388FC61308BDFB879F25F0F8ECFD83CCD955B1410C1691228D5D + 049C48B44994588E19891C2A7E286AD731404B28D2F94431DC6A8CC7E7787CBC + 8FF37DB547B1DEB636560DD616ABA8CDC7AA8F82B18E33D5E7BC4024E8A98FFE + 43D09CBE1DCCF1F19B6EFFF42BDBE6FFFF0B91F7ECD9D709B493106C60EB855C + 40A15751BB8F575FAA6DEFBF95BD57A2639BC0FBCDB61BDB071C676C41149BE2 + 0CEEB7EEA78731A4758C971E7B3DC6BBA7F82EC318F6191BC4E471750EDA660D + 6F8BE6C0188D8DEC1EC6F8CF6724DCA2AF76FDBE68F9CF9EDAF7B7D918B3047E + 57018E41A2F8164B72E8676C0BC4E8BC96A0E32853BE63F0826A047FEB94280E + D087DA6AF7E3AACFE3C75246F1E5A928FE7C035008DE3F7A35108087E743F9FC + A5453EAF81A89CCF7DE6E873733FE74D9F13B5FCAD18EF533BB6FE78D0760CAC + 13A171384F5DD979309E568373BA29782E1CD9B57D08BC4600BE667EFCB3CD57 + FEFF2FE3B0ADD71C5243AF0B12B8FE29E7FF9992876B1E22752A133E8E8579A4 + F3FC3AB6C4D56B20CC43EC868ABEDADF9C81A1A8ABA89D6D5EBBFA58C6B0ADD7 + B89A8BBB7BF18867C6ED6CCEE7B62FF81FC0B886C41FE23356EB79E8D3F5035E + 37D39EDBD89AAF88BC833B7F1B3087788137C2E7DC36A6E618AD254DF961F3FB + 5EFD58072CA2EB686D8FF6565DC496B5C26A9F14705123983F72B82629F33905 + 1BBCAFCA3CE750619742705A33153B6DDA078857963C021AA227FEBDDDF60CC7 + 10EBF1D005A021E06988F4617D16EBF4F9AED8497DE05DAFE9B7C62EF34A2237 + 1EF57CEF59F7F8D9EBEEE98BCFBBCB07F6FD72FE3841FFE43EFED28163D76B46 + D49392482AD08EBB83077D236B4D0D8DCADB67A835D1670DC5C5330E73DAED1E + 74FB0BFB5CE126AF12BB3B7FD89D5DE2B5C7BE66A198F81A4BD0FDBE23E676DF + 010C5404DE58DBDB26188AB62537C1FC0B7BA39C277A9FD2C70DEC661BBD21B5 + AF046F48B52552AC7ED0336FE5CBEF5DFF50BC15EC38DA0FF38D602700DAE789 + 13515F15625DCEC0DE42E33D55F8EDC71D9DEC580D9EA0F0A4055B5F9BF98710 + 9D6C1D0A4F2C413A31859D277F18C3F60D7F79BAC5DDBF76A1A1170B2BFC45EA + 40FC0D4E2CDF23F4584C71AABC4C3E6E3CAE730C8FF5F2BCDB082F72D9F6765C + F4025E004F5DE0F258447EDA5837F24D3115E36BB24F7851CFB92BC33B7F0916 + 44CAC2445A9C78911739B008240B194900D618478D33467E9793636441E64086 + 8B379925316089009C17FDC60B64E3C5A8B2408585338BC1025D5A40B3E3F7E4 + F5F7BA3FFD8FFEF3EEEFFEE37FDA7DF4E50FEDF8D97865710DF5A2453F2EC0B5 + EC0AECA83105F3231F0560CC9BA26C144F9BA273208C5D5D6654001E2E169B1D + 0BB88F5E75ABC79F769BA79F75EBA7667FF22AB17DFEBA3B7FF9A6BBFCE4AB24 + 4040904842C4D35EECA8C245A9C9C5E4E138C3C565D874013F8AF5FD085DC046 + 2C6B5220D0D8A81E6D1E88C0FCFFC94904C6B608BF3A6FB01201381254796CA2 + 717D3F8263A67179AC0306AF85B31315AFA2B97A5883B1BA8F11510D009FE6CD + D5014900B65C8AB74A1513A58FF81A53FCD52EE8187E9E2D718F7FF000BC8FB9 + C4DB188B2DF6597D8075990741576B200FB62C006771F6FCF937DDBD97DF4DA0 + ADC22D48350CF621E2F28E5F8DC1D6DBD45E796CF936770ABD3CB609D8ED3390 + 7C98AFCD7D4A00E6DDC23C2EBA9FBDAD3F267A2C14C67BE8F331E8E77A653F07 + B5ADFDC48E4189C9B6F118EA579BA276B6A76C2DF4F381BC345F8C51BE7FC2EF + 07E97B3FDB107EFD3E7898A368BDF47FD12D2ED52AE2B28F5B82E6F9FC295F2B + 467DFC6E8D7E8F3C4BE2A2F1A7E0F88712D502512C581AA7BF9511E37860E750 + 3847E0F947D9120AC13CDFD073219E8BB0DF82F9BE4D7C8D1453D03134866DEF + 271AD7F2A93F8AF5369E872D4573B48E02DF120138BAF357FDB4E939FF12526E + 4340D5B81651BC5E17E8F93ED940683DB7B121BA2217226CB9D649FD8570BC68 + FF07F86BA8325682D762B64D02B0B5F55A6E2DD782C0DBA7AEFF08F318C75CCD + A71F8F82C6166B33F803FDBDCD075B5CFF6ABD29F45AB99D8398C7B6DF195DC7 + 588A0A9E4A147B28BE16D7537C8C6E4FC9700DA7A7E5D7DC510CD701B7FD3AA0 + E66EACBF4DAF99D6B5F83DD7E990F730AF3D5A3FADE115500BF6148F756DDA07 + B50A328FA30460E629ACB914592F55E0A310CC7554157B3536F58D741C031198 + 22AE927C653D1E7D88A0104321226FECD8A19DFA1AFF3E61FB86473B43F885D0 + 0B1BF603FB8436F6178FB9E67E0F720BEFE57E9F8CA108DCEB470DCDC9FA1481 + 27E3077D6C3196D802423D8B7EB67D6D2389BFF85FCF06C4DE24F26A0DE87216 + 37270073FC3B8E6715196F0D147C3D8B0560FCF3E720FF04ACB67893DA01E41B + 52DFA02DDC9B966FE4ECF71FF43B32F6656F3FA63127FC2108EB2FC009A8A09E + 3C397B8A0DE20F85F5FB710AA5FED64E9CF1581E6C75CC9D9D609F992D9DC8E0 + 2E60CE51623C7EAC88F024EC087832383A29C418D8BF24FCE2A40176B471423D + 16233517F1CDBA37406B0C9DC7B1B0468BD35C60E4633A873FE651CCFBC4D445 + 286CBC00E645F07561EDE8184671115EB4C5DDBBE90EDE85F690F47F6F9F0F17 + 22E6C0C244D0E7628767104B7C8D26FD424DBF70238B3669A10963F7B6EB4061 + 38D5BC6FDBF2D8BFD66259B4905617E8CC7EFEE4B3EEF9E7BFD2BDF8E257BB0B + 2CA6DFFF24DF256B3E2EF6458B7C00765DE8F47153B953F8BC2CFE7E5217647B + 1198B6220EDB9CD94E77EE1AB9DFC7A4BCFB9F26E117440BBC242F1CF702AA82 + FF11BCB7E375F6F48BB4E0AFA204DA49142A022817C8391617A46907C331737F + 65AF0573386E7A5474A98B2DE19815D87127B3CD1F6DD65738968E19F9D4AEC0 + 978F45115F30BF32BE9F9B8A284A9DEF0C2BAB13E57A3BC49B293496B5D9561F + 8420889BC8611C886AA94DED0479AC47810924E1AAF4D5BE1408866BCB83A0A8 + 8F7CC6985ABB05FC9813F793F3A54FF3B5EF63342E8A65BCF745FD680E9BA75F + 76EB276F2A146555A0451BE2E5E547DF760F3EF97E1680711730FC882BB11472 + BD98CB3A8CA14FED9E54E7A9B5CBBC75FE00AFC5AA7C0692CDFC5588472CC0BE + 1BE8EB5DD87A5C721FC7A39FDB14DC270FFDBA7F14C0E9C39DD4F4694EEADB3C + F8BA705E7CBFA1AFF8F9F35830DEC3B8A5E8389A4BBBDA746CEF8B40DCB1ACF4 + F59EA8E9FD0A635672BC6ABF7CE7122F32E37B597F67D4A7318A7EC7FBDF28DA + 3DF403D6D578FFBBDAAAA5754814772C5A1373027E1CDA09ED2D18936BA06DBF + D357384FB1F316FC2119FC0FEDFC05BFBDF51C44C9BFEB7E2CDAD4E763067138 + 3F28E7081A9FDAEEBC4FCFA9BC98CC98560E63B0E5F99EC6B03D87C6722C4563 + 15CE21CA4F6DA0822E8459F1230F6DFEB168FA83D1E29F437314F8D2D84E444D + DBE24F31655E53F86B10D6E3397FBA9E38C7DD681F75FBF38F8D97A99D9E3A56 + 04D82DEE381ED5E2F548848D83BB938D3E7E0CE792FB929FAECB729BD773FE3A + 51FBBC568DECDAF776C46B2EA18DB1F84368B4B116433FF301DA047DBDCEADB8 + 759C082FB01E4B54A7B5C6D2B25F87636A4463D3760AB466DA4228854F84F61A + 6BECEDF5BAD83CE9F61BD82DF6CC38372EAE727BFBA0DB598D8BDD538B7DDCED + ECF5DBDA31AF75D31DDC16BBBB974463ADAFF835453F97C514F137D59B58CB9C + 5BF3F4FE24843B28FAEA4D35BE8EE26B562C772DFD7A234FB1E77CDBCE08C073 + FE770D045E8ABC0476CC19F63A7711C3997B1278E35B8B28E7D680F9513BF273 + 453F6B4DBDB654F0FD10AB31DA7FF1FBBAEC7BFB0410A4D3639F6D4BD0F750B8 + BEE3E6B8E50230C456F75868B24004CE77003744E46B90EE1AA6F80B967E08CC + E7E7D8C7EB07EE8E9EFC63DAE6843F0C61FDDB879E48A493038ABFE6C3090384 + 5EDC295B73785262B13829DBE3C43AE5990F8F842EB9034A8E1F2B223CF97A0B + F0C470EAE450639A713C413D01AD7126C73F00D689388D003C457F61E62FE6D4 + D7422F0AA788726F9AA9F1D537072F84235A17C968F31832CEC7AEEDA20A0B10 + 6911625FC05F8517F86830B52DF125E1B58285062C32F40B124BA98B16ACA536 + 89F1B68AE4CD811ABAF8C2BA6ACFE4059D43480B2CA5ADE22F05602E34D505A7 + B2E085AD421F414C5AC8B36D12D86D5F31D774C7C403CB4FFF536EB8F8A66368 + 1D457DA7242FB4B6174B3D1A5BE365B154F1B144FD5C34D6C5636CB1F01D2DC2 + 7B7C8E029B9F4B5E24CE0BC6D877C49024EE7A20104028A0900011C1EC4BC5DF + 297C1E050A8A1391A0B2242642F33CF0AB80B32AFB4E1FFD1485A660BCAFCFAD + 17B008FAEA537C3CEDAD3CB4290A73AB685C944F9B8AB8AB723C68878DF3D179 + B196E67A7FCBD6827ED6465BF37D1CDB185FB7EA83F8DB128077650B2862E2AE + 5F88C0D89E3DFD2AC590941308A82A748225222BD9A53B72F37EF2BDA3FBAB7D + EC8FDF3F3DFEB8D31663733E2AD4A2CD3B9D9318EBE64CE8CB8CFDA84310A377 + 40E7F8A1000C72DFE2DCFB0A7DDD67BFAF7C3D616BC5021E0F12C5281ACBE319 + F968D7B161D31C3FA7A97902F5CFC5720E8C5B1A8F36E256F2FD56730DFFFBA1 + 44DFD7DE3F452B4F6BD2DF027EFF7B46BBC7E7B6E28EC1D71CFFC6E6BEDA96C0 + 783F5F5FABE557E86BF93D83B8E87CA688C2FE3C4DC1791A855C109DB7690CFD + 6AD338B615CDF1313C8FD4F3496F239A47BC4FE355A0650CC6A7CFC745353C8C + 254BFDB96EBE06207A5DA0B6E85A62805D2F6DF61F75DBDD47DD6EFFB2DBA57F + 3DF5BCDBDA365D4B955CD6C9E0DA6982738B3FB7F983415E0FC7CF7F841BD400 + 1083716D58AE1F3D7AFDD862CDEBCA72CD3907635BB5B59E8FF33E32B7763055 + FF1470FC69A6D75C9612D5995AD3A1CFFBD57E2A6AED24989A4DEEB6AE22AA1D + 8BF3CD93EEDEEA4977BEB6D706BE73CBBD30FFA5717ED56DB657DDD9E6CAFC8F + BA8BED93EE6C6BEFC52DE66CB5524DAB8DFF5FBBBFD76D2C1E8F368EE6E3E1FC + D27C027F13CE1DFB54D635476B97BAE6B910E661AB026DF2BBB5548EE3D19844 + 596F1DDB81E619769CE704DE39FF6D80738C88E2EF3884FBF65E99D0A2C0C05F + F21A0278BD0B1739C4F7699BC262A87DA9C08B36EE0C56220178B55A76F3E71D + CBB89D0270BDF3D7FF5F60692F1680D93F5C08E6E3A1C7B837BA7E00BC4F319F + 9F631F3FFCC0DD41F0A3587EF806F047F2843F16FAC37B8BD193887A52504E1E + 7042921ED3627DB4F73811B7183E8A04DB9DD9E1AB271DFEC4436C1C6B8AF0E4 + EB1671F409E411B4C639D5F8ACE399F29D46189EBE78F2FE08BDA08B98BB60BC + 495AE346F38BA04FF78177D56A1C7DACEDF759E3AABD88BF55F89D128081F357 + 9FC45728BC2A5C90D0F60458B0C0224AEA4B8DB49081361E276DDB7E71635C43 + F3E6E0785A8BB5878B3959C83D965309C01AC7053E6DD3E7DB1C837DFA598F7E + F689C647447135DFDA144175B194A40572B66551546DD5D780B19EE477F5142E + F0F2719FF54E5859F4F5F1BAA84E601FCF278BF3E958585BE710D5676D3C8E59 + FF4F2FD8B858C67BFC9C229F0A1649A4282471C26C9120C336FB11F4ABD8A168 + BE8A4A6A675B6D11DEAF7DEF03DC276C29D811CEC5A3B99ACFB6F6D5B6048D8F + E682E382366C88F173228C613DC6696C648B400DD6D1B6FAB5AFC087799C3DFF + 72301F6CAB00FCECCB6EF5742C02138A95144AB185000C70277012838BF0D9CA + 559B8FF37DE6E011D038367C2FFA7DD363877DE36B42B8BF3926CF9F73E198E8 + A3ED0560C6296A473C8FC5F6316BA29FC9FF371939C8450C09C628FBC2D78A73 + D6FD630CFB8801F4B1EFEDEA675B6B47309EA85DA1DD7F57D0A776ED732EACCF + 1C05B1006DC6F135E63102ACEBC7F268FC2A127F4B9C7E4F7B06DFD5EEBB5B6D + 2D3456818FBF1BADDF0F25FAAD519BF72951DCA1CCD54DBFB5452CE5EF6E1417 + C1DF621FCF3AECB7FC0A7D9E281678BFE6A85DCFA9EAB994D800C55C3D0754D4 + C73C8DA54D6B138DF144E7896A53E0632DAD17C5824884659EFA7C9C12C5293E + CE433FCECF5776DEAED70A73F4D70A3DD5776EE7DE67E445B7BFB02DEEFCBD28 + 311CBFE6D9750EB9ECEFD8ADDCB000BC0EAE1F717D195DB72216E835BC8F5118 + 8F366BD21611E57A1FDABA7630177B6A74DF7D5FEDF17AC83CBA2613ADCF4CAD + E9D0E7FD6A3F15A936DA65BD0FE26EB619454445DC6EFBB83BDB3CEEB6469ACB + B9C59F3D48ACB0DD5E755B03EB8B97762DBFDFDAEBB6C69D9E78BFD8EB081F38 + 7F60AFE995D504E3F9788EDEF72305607DC473E44F145F1580BDDFD031D8565B + 138C6DDB148771D0AF79861DC3F75D28C5DC71D72FF7C3F7352EB58B28A9BE3B + A638440096BC9B16800D08BB843A18FB7AF7AFFA23A8EFB58872EE18F21E0AC0 + 858305E0E58CDED411FAA69EF2B998709C1433FCC0DD41EC0BBFFE38961FC0B7 + 2900971FE0D0775B48FFFF37C3C793ACD70FBA4BBB28FAEA9B9F249E3CFB223F + 125AF340B47F62E389C714E1C9D77B024F2E9B27987202BC8456ADC93116B024 + 9F3187329D3FBE7002F185D234BC985B42947F9370CC686C9D570B5E64734BF1 + 372175D907B880AF8F662EB604FA031BDA16A788884B81B725F2261FDA2E2FA4 + 2C42A4058905A22C172C0639B685109BFE1798D806719E056311D450019875C7 + F6A1A07B285E00C6FF00EE179BFAC5B4250B66B413C472C14F6D5A878B5BF4FB + 9AEAD3BCA5B4E2619F1380B9F0C945512EBE02F545683D652E86F55B8BE551BC + 8F653C73D258E57F1253C04D8F95341BFF57B1DA09FA147ECFAE0AA5BF540056 + 219BB6D15C9FD8F649114F20543C1A0AC0143F18C32DDB2D184354F8008CC116 + 2289CFA58DF9EA5734570517CD99AAC51CEC23054BC6B4E22906A98D76F53157 + 6315E6A98D357C2D6CB5EF612D9F17ED436423AC133117471FC6E7B1E45CC852 + 0118A86809207CAA008C188AAA7320AE15AB636CF1471FB60F7A7C74DF60E767 + 83FBA47124E7E739E7BA790E1C87F3E9C71DCFC9DB98437F14435FCB4E5F3FC7 + 7EDEE87B1BF7AD655B8AD6D6BE1F8FF8788E4B3B5F03CD455BED1ACFBE47C7D3 + EFA6E8BDABB10A6B4DD9599BF3AB3186FEB1D1E0BBD97D6787DFDF25A6858F27 + F0B57E3722A2DF1AB5799F12C5294BE2D5A7B1B4A5DF750AC0C0E54C31754EA0 + F85ACC53D4AFF898A978CE9BE3214ECF9DF45C8A36C0F33D8ABA519CF7319736 + F5B58862A7CE1DA7620E0575382E0552E2C7D1B6C6699FFE286E18836DBE16D0 + 6B8539F4DA81D0BEBDB4E36E6CF0E866BB864AE22FAF2F6C5CDD9FD53D5CBBD8 + F5156909C0674612816D7CB9DED1F153FD0B574F2902F0E03A51FBB419E1BFFF + A1CFC16BD908FABD98EC6B306E2A46EDADB503DA5B354E85AE6178720CB6195D + 233974BD258A9F5A77A1CFFBD57E2A526DB4CB7ADF50009638F36F3688B7E396 + EE10865857FE47E7FE81C5E4C7F7EEECFDB9DB1B9BC7DD0E77024300B69C9DD5 + 3ADFA33ED6BB2128D9780BF6E9BAFB9EF6816B9BB2A6A9B64459F7ACE22FFE4D + 5E59070D636D3B1280612F3E8EE319AE6347C838AE4EB2E1B839A1F47D83F3F7 + 44B1771C0384DC86164522EDA921000F7206BA55C33601C55D1578D15E2AFCDE + 713A6EB900AC3831F84402F0E0CD5B08FDC11BB92279CDD8E26FD7F41FB83B12 + 1B833FAA11372200DB0FED689CFE07F9245CB7E6605EFD4903EEFEC589D6DA4E + 5E1E3E7AD5FDCECFFF5AF7877FE5CFBACFBEF8D5E4434CBAFBB7C4CFA1271F2D + A293AE9B802781D73919F4F0E4D2D73C768CA8D6947D294BF219D3228AD37CEF + CB8C2F9A407CB1F4FE307551C98BCEEBE02FB62BE24BFFC7C92EF4472230636A + 1EDA88791ED214808B583CF2A9D04AE175CA478ADD2F5610FA21FCEEAF3EEDCE + 1EBECA227023EE58B008437CED5A3FCDB77F9CF3314C09C05CFC0158F0226A67 + ACA2FE560EDBC305AEBCF8E56B440B8ABEA6A2636A5BFDB813361280D3A268B9 + 3B561748B110AA8BD73516B8BB7E98C33CB635873E5D6005E8AB583A07EBE8DC + 94E45351F7C1EB6E6FECEEBF1A6CD9DEDEB3E36C2471B888C05E1CDE589B632B + 9C938EADFD165EA8F01CE2633B8A5701842208F33DEA63BEFA898A32DA47DBE7 + F8717D5DE451F0A14DE3345EC7D118FA68679C425F0BCC01E2A912D50151BED2 + 8AF1FBC1386D7BBFDA233FFABE2E45341E57B6AB085CC4DF29111850B884F839 + BC0B368EA750DA1242016B2AC9F7C4DAE5F58C5E53D8F83EE27E12EE1FE21083 + BB74390FD6D7FFD1EB7D9C3788FA84399A1BD95AE47A1657F64FF70D5BEE8BDF + 3FA2F111F42BDEA7F16AE718DE4E68D7CFB3CE4BF31847DB1CAB72972EDA3A66 + 84CF8D988A579F0AC0FCDE56FC77F6545C6457A6F2A7887E67D4BF04FDBD52A6 + E2F4F7DCA3F1FC0D57A27A118C8F7C73B472698FF0B10AE7ECF74BCFA7FC7995 + B7AB8FE7761ACB36FBA0251A7B18AF363D878CC6D3F8561CDA511DA0316CB3EF + 513F63789E0BD4AEF1EC13CD2138F76F5D1378708D10D99515845A08B672EDC1 + EBA0E4C7B8976683F8AB38C1B617612D3E613133D754D93EAC538100ECAE15FB + 31DCB5A3F6BDAFB076F00E5FA2BEE85A57FD533056F3A2B503BFA670C81887A0 + 75DBEB18B00DF1EB254B9FB0C6FF43ACF1F19ACB788CC87E2A6A6DDEE8E0FE07 + 30D7F992D869C76463FECDEE61B7DD3EE8F6DBFBDD0EC2CDEE81F91E1AB95E5E + 3B7CDC5D9CBDE82EF6CF92187CB67BD29D9D5B0D08C658D76D08C01CDBE3E30E + 21AD53BAB5CD162A00D735D6BAD63A840270549F76C5C74468ACE6669BF181DC + 01ACDB10B9EBF77DDED777C3910230582000271DCBD9FA7AD350E48D845E3EE2 + F9EE51CF6F875B2400E3CDE0DE1023215844E0B72900076FE2019217C68BBF5D + D37FE07E3998FC724F7F8D6231E50739E46001185F6E8DE35D7F84ED4736FDEF + 05ABAD54FF3539A25E7F0210F97B3B8E091EC102CE2F9E27E117DCB78B3CFC6F + 6080BFE46B8DEFC7E0B85344275C2747EEC2E5C9E0E0A4107E429B233A89E4C9 + A5FAD4E67D73B4E20FADE35992CF18457D511C6D916F7C313484174DF185D318 + 8D6F11E5DD04AD8B4A5E9CF2E297FD285E6335AE5E68038B1B203E5CB05701D8 + F986E0C2FE79888ABF03913788497DB7F050FB80FEC827F678B1A2F7ED1E7C92 + C4DFF347AF53DB2FCA68BC67499C177F691FF4B14872717A01980B522DA2452B + B5ABCD2FD4E9621CE380DA740BFBFED1AB84D6D2B6873E6C9B8B8A572FBBFC3F + 71C78BA39198CB456B527DF73E49B404608FF7FB585D7C9D828BDFADBC243A94 + B9D6473743C02D022FC4DEF3879F277067EF400086286CA8385CFDB0430CC618 + E54EE9D6BCBD4F8FDF202E10482858703B8A313BFE3F3163E05F22B454C1E369 + 2F3E791FFB6A6BD929FCB09EC7E74FCD91F5207CF1EE55E6F93840818C761E03 + D69982357C9BA8F88B713816E2B44F5823AAD58A619CD66EF94814A33E1E1FB5 + B1BD7F66EFF5B23F1480A7446088945EEC8DC4500FF318BB2467C093E17E72FE + 7C9D097DD1EBC178BD0398A2EC94004C38DF25F3460CE35087FFFF977D3E023A + 8336E271F7B4C596F72BE17EF1BDC77D89DE23DCC7081F87ADF7B1AFDF234473 + D44E1BE0E74D63E843DBD7D0ED148851A29825201773F0FB1FF95A02B07E674F + A1394B599A3B1733F51BA4302E82FE25B111CC2373E7019E7C2E823F4A8BFDCA + 92DA8C6931150B1B8F81DA79EE84F3290FCFB314C6F31C8F7DA231684F09C02D + 3B698D0190CB7CC669BCF6ABD02A368FD68EFC2D18AFF301BE4F98C739ADCA79 + 3FCEFF978AC084D70FA36B8874ED6473BBC835B3D89BDBE90F5B6183207B69D7 + 1978FC74118071EDA0626D2FCE5A5D235D8BA5F11093DB3A9F3A3E6B7854002E + 0C9E2605D45FAF239DCFF78D8DC16B5FB2361BE0F5AD6FB3CF36685DDB73CB78 + DFA78DF6B701C71B8F89FE18BF5EE245DD169100DCC28F11D94F45AD2DEB79BD + 2DAF0326A1F3C2E67DCFF6E302FF660EFFEBF77E776F75BFBBB4ED7EFBC0DE83 + 167B69EF9FAB8FBAB3471FDB35D1CBEECC3E3FF776CFBB8B15FE8770BE39058F + 82C61DC1E9469460BD8E637B7CDC1C3E27ED9BAC6D4E318AE59AAD5B37E51AE8 + 141A3F096B977172BEED831D37ACF1F6356DDCF75D14157197B6DA4EEBFE85E2 + 53DEEBFD7E5BD8B14B5AD3486712AA0E35CE1DDA245EB4AC6A37DABAD6182FFE + 7AB117FDD50A1A5F6FF3DC09C4A7E14605E06BBD48737701DFA0003C2278130F + 2871B566E01BA13109FDC0DDD1635FF6FE8772C0097F0C7CEDE0077F80FE607B + 9F32E79FC3F2FB1FFFE0C4C4B57162951EB96227D56717CFEB5FA95501586BB8 + B94563D0D6424FB2DE099803E7DB38A1F436C59F60B2AFF878ED2B51FC3176CF + 6C1C4E0A7192982E62A663E98F62D497E1C5D0F5D10BAD29A2DCB7052E4471D1 + BBB3CF0D409B17A7FE22973087B1A05E5873EB317BBF30E0E20739E83F1F4151 + 57C55D6D47B1914D493E158081EF47B898E80E60D8878B1B318C998A6DF9635B + 5E90399681005C16A8B830A50B51BA30457FCBA7F8182E7869BEDAB65788417C + B66361501718B576CBC66DB430497BFE5FC09F8EC4DB4AB1734154D1C5D3DA96 + 3AB491684195BEC8C6C5712ECA133F3EFB1E8DDD5EBDCE8F6F7E60ED24F07E62 + EFDDD749FCBD7CFC266DB3D80B20067F9EB68807550486F88BBB837117B08CE5 + E15CE7E6883B8993885C8414158354ACF0424BE4D3186D13D655D41FC578BBC6 + AADD431FE328B454B1E5296CE3BAEC234E052FFA358EDB28466B44C2A08258AD + 832D4552B45903309671847EF635C6FB145F8B7DB5FB5CF6E18F6A6B2EDB6A43 + FCD6D8583B33168001454D12DDF58BF6D9B3AF0736F5D1AEED08FA2B4FCD6673 + E37CD39C6DFEFADAFBD7DAD3C75BDF6AAAC8EB05606CE9072AE0D2E76D00B93C + 2E7A6C588731B48704F3A78DFB4D1BF1FBEDDBECCF6DF518721BC11CA595ABF6 + 2918E7C1587C4FD3A639D8C247B4AF713E37B2D71CF71B03FCEFCE14FA9D1EF9 + 5B309EB94B7FCF3CB3BF3385A91A1ED65C8ACF8BEAD016F93C519C9E27E8B902 + 7DBEDFB2B5487FC886ADE4300F3E9C37E93916FB6C033D57632C637C6C6F7B69 + 39D9E7CFF3D81EDA705E88F343FCB1623EAF842F1A17E7962AEAAACFC3589E93 + 9228D6E3739428DECFD3DB90A7F3AEEDC67581672EAEFA2F6CDC4B3B76F76C6B + E3628C34877B784D70077B16FED7657C7F17F0E0FA2E5DDB597DDE052C7F54AB + F3A1789DE752045F8ABEDAE675A227BC862C36DDAA5D6CBC96E5752C599B0FB4 + EC9EE87A7EC9B57D94B7149FABF3D3EB73DA3CED3161BF1E2AFEAA081CAFB964 + 7BB426A3F653916AA39DD6CFD02F733BC378650D1077F6DEB363F4C0DED3174F + BAFDE67E77B9BADFDD371E18E758C3DE3FB0CFC3F3EEC557BFD27DFD93DFE99E + BDFEBEE5E2FF063FEDAE762FBA7BDB67DD6E559E527886D702C71C73E8F74BF7 + D5C398EB90D62BDD9A665D3B1406BEC61AE952347709518D21161309C013A2E9 + 62E66AA41BB2AE3FCE067780376A0C8460E7BB632176EC465AD480CBA4A3E5F6 + 30AF1E77BC1FABCFE28A6E35D0CE6ABDE20FB5AD212A0067B177ACE5DD09BC6F + 87DBF9086870430230DEA0BE3F4BF0261E30118F37FAC8AFD4587ED0EE1832FC + F11DE37E04AF83D5C30FEC788C8CFF218E626E8A705C9C98F0E4C4B6E97F74E0 + 24B6C4F1AFEC68673FD970425472A6C6505B8BE844EBAD8239C87E781178C909 + A4FA19AF68EC14ADF839FB527C7EE5080138F229E30BA1EBA117685344B96F03 + 5C1CE22211C2EFFEF24542456012E52929DEA817D7C0E2F442BBE26D9A5341AD + E703B1D6A37E3EFAD98BBDEC576411C2FF4FAA83D03A06173220FEA6BF9C2F31 + BAD0B1943AC602A2BC48D45DCA40FC35B008D42FB40D17A4D85668C79678BFDA + B426DA1E2E20A6C5A712E7E3BD5DD15A808B82C0C75501B8454B1836B8388A05 + 5A2E2CABDF2FDE128D69D9583322AAC9B15AE07F3CEE1F7DD1EDAE3E2FBCEEF6 + B63D7FF845BD0B180230D03B82EBFFFD2566C35DC4F8FFBFD1389C0FE73A3B3F + 0AC04594F0A22DA15D450B6CD51789BE0A73542801DE4FC18DF581E669BCB6B5 + EF6D6CB30EEBB3A6FA0863D48FED708C2CB0514CF3A29B0A739E2AC005A898C7 + 3ED018E2EB92E4C7FC9E0DF765B46F1335764F7B215AE131493E97A3F538C73C + 977EECEDB3AFEADDBF91000C9BD6047A2C59136D08C053C7C6E7A9880ADB14FB + A7782FE63645D508F8B536E7439F9F9F1780351F440230D11C12EDA3E6338EBE + 2DEBD871F677FAF2B5655F3F3F807DBE969AA76D8FC6F86DFF791AE6D0DEAA49 + BFE66A9B79B469BC8739CCF3F39A8A535B0BE4EA77246CAD5C8EE5519F8F5F95 + 3FE4C1F73EEF26F6F07781E037805BE2630073354EE16F0CE369F3BF3F73353C + 2DBBB2A48EB2343E8AD1F3049E2BB488623437227A9249C26C1480899E5791D6 + 39584F167A3343B19631DE4E5FB4651B44E7793CF7ACE2A93B375518EBD1185F + 1F4439C4C7A0EDF3A39A8AD63B8500CC7EDDE2754DFF07D8EA97BB7FB1C59DBF + 1C371D5BDCA13E270003FC0FE073CBBBF8D8FC790C653C2FCB8DA0003C79CD68 + E875A5C605B6B5B541BD7EB5ADA2FEC8EED16B79BF555A36E27D53F8BC63E6AD + F4B5AE8F177F01EC799D651C3F5C8789EDA722D5463BAD9DD956C7C35A1AD601 + 712C2EED983CB0F7C6C5D36EBFCD77004304C65DC0179B07DD667BDFAE855E76 + 6F7EFCDBDD8FFF83BFD97DF29D5FEB566B3BDEDB17DDE5E5ABEE027F50B17ED2 + 6D712730D625CAFEE463FD281D139D17A973097C8712AD5F46E83AEA7519AC4D + 2E60B6C64D08C09A375583631C3B8E3257E31463FCD282D748F5A580EA97BCE6 + 6B5BE28DAA9D692DF1CFB14400BEE3EDF04B290013DF0FF16FF288899CD6D81A + 93F11FB83B326F5F001EFDE016F407B9C6B8BF0E1B30E53B90D1B800F53946D9 + 6A1C63D9F62735B54E21F2A9AD4574A2F5B649F3907D51119827904B4F2219EB + 4FD497D01A67CEBE149F4FF83A2C8A0DFCB16D7841745DFC45D81451FE4D8371 + 71F10BE1F7CC2EE88117817D3CB799BEC6480006169BC0C514DB8ADA7DEE1916 + 135ECC9284DC2502B0136D47FD430872078B19CEAFBE39B46688D48E7255D0C5 + E289F6E7F00230EFA868A10B521188D1452DB5132C28E191CEDCF2F1CE00362E + E6315F17F0B4F6145ACFC7D3B736468B9D078045552CD07261D9FB22E6FCC02F + 5473617B6A015B411C17CDB1DD3F79637C69ED2FCD964598DDC32F9208BCBBB2 + 982BCB79D03FF23989C1B8E3174271DAE6C748B3BFB5361E2F1D8D0D96CE35ED + 2FDA22265050F02203F08205E388178035C6E761EB4516F4559CA4DDDB3CF0D1 + CF3ADA27A8437CBCB7D1AE5B8EC7361EA14B918D506CA3E0A67D8FCFD578B429 + E2A9C047718F3EC67B34C6C39896DDE3FD53F1B473CE67058DE9C5DF2C044702 + 30D13C3D0E9C036DEC2B9A43A1985BFA585B6BFA634B1BEDDA861F757C5E1259 + 0D7CEEE9277302B0DA15E6B7FAB4B186F7D73EB7767CF56E73BEEFF99ED7F7BD + B6B1654E069F0BFBCEB236ECACC178E600D6D1AD8F212DBBFA5A31795EC37DE0 + B6950328D2AA8D753417687D6E3DB4239EB5D1F671ACD9B27BE0E3F8E8B336EB + 53F425FA9BC02DBEFFF5F7803E0FF37D2CE16F26A18D304EFD1E1FAFF5141FCB + 78ADA3E7016AF7B16AF74431B56E116935DEC3D86A93BC262D3F0460DBF25C8A + E7557AAE06F41C2B0202F0FE11FEA5471FAFB96CB37F4A7C4D3F0ECF5179BEAA + 44B19E299F47CF67E7ECBE2EFED89477D046D0AF71836B0960D712D96731F7ED + 3D92C45AB3DB385B9C973FB6F33CDB222689C2664F734B73B0FE94008C3B80F1 + 7F85811FB7C0B9E6BEE546E81DC07AADC86B47B9861CCF611C87C747EBB56D7F + 3D3B0F7308C5573015471BE335265D07A7F50CAC47F4F5E6603C73B426E74168 + 9FA2AF757DB896A4C0AE6B2FCA701DC6D9DD0D0ED7A5D62E7507E3510046FF9E + BD777007F0E5D36EB7BFEACED6107FAFBA07AB87DD85C56C2D77679F85C75F7C + AFFBE4577EA37BF0E9F7EDBD6ADF9B67F61939B7CF0FEEA6B7F7FE76FFC262ED + BD2EE3001C937CCC1BF373F663D075CB39B896D8EA2F857989056BC2518D2116 + 3325001F2A9C1E9A77CC181E5763B42FE0BA63BC8F9CE2D8265027D29984EA97 + BC707C8937AA9E45BBF33326DDFCE8EC147FEF04E0DBC1AD1580379B8B6EB33D + A1008C37A07B332E42DFE003F821897C3D3A9F764DFDB0DD3164EE0733F8E138 + 16ABC71F591D63F8E3EB7ED08BDFDB06E2EC89188D2363549FF507F32CFD1463 + 2411D8D995966D8AE844EB6D53E7C2799F58005E9ADF8A9BB32FC5E727704258 + 5E87D95883BEF9B8E105D175F117612DA2DC9B8463620BF176EA0E609F4BE827 + 830B6E6271B38CF270016F174B4EC4559B3210795D6CB539C1B42E40C05E7CB3 + 04B1BEDEA8BEB32F21CAF536E27333BDA0CB0514B54DE105E0F5FDF20838872E + 4A29DECF762B8F8B69401711191FC15C3C9E3AD9A406F0F1EA6BC540FC5D5F45 + 8BA85870FD2CB5B1F839F667D4C7455A5D34A58F7E5DC865BF05169A7D1C17A0 + A385758DD378F8F7F87FA269C11EA28B883410808A4884473A2771F7A1C5597B + 7BDF5E0F88C25687FFEF9775D33EB8C7362B552000108A255749F33698332538 + A8D8C0388DE796ED29344EC522D6F7E3A0AD62136C3A0EDBF46B5C644F3E1C7F + 1CFBE247FDA1A09589F607766CD78FEDB83DB2F1456CA3D0E605372F347A7F94 + AB7EE4B30645428EDBCA81DFDF494A34D6E7B1EFED51ACDABC8F7DBFEFF0F5E2 + 6F2F00ABE84B70A730F359538F858EC371F5D800C633C7B7D5C63E73D1D6BEDA + BC4F5F0FDD466D7D6DD0F77EA073D2B969BCDFD7169AE7E17BDFBFC7F533019B + B6B1F5A81D6DFD9CAB8F7518D78A551FDB1EE669CC54BCD655DBEE69DFD7EF51 + 3F4FE0C79BC2C7A1EFBF6F235F94E7F17EFE1F659D3F7FB3C8E0BBDFB683DF46 + 9917B6446B31D6C33AAD18DAB155E84FBF698DDF5B6D6B4E0BD4D1F300D89017 + 8D415B441457EB3A017832B601E314DA35866DE0CFA526CFAF78AEE640AC3FDF + 63BE9E0F6A6DA2F1DEE6A16F49ACC2734D9DBFCFF77EDF8EFC6AC39662AF477D + 9A0736E569437CEAD01CB83688AE2100AF1D20006F70E7B7D92E5F7CD9FDF067 + 7FDCFDEC8FFF6EF7FABBBFDE6DAD469A8BCD9922F0AC005CEE004EB831417FCD + C2B915C1D7D37A047470FD389A83BFDE3478CDBA2E6D6CD926B806D6EB60C258 + 853ECD8962B08DC45A0AAF84F625B4723827E2FD117DADEBC3B524258A23C375 + 98D81EB1346E8AC1784500C65DC1AB0B7BFFDCC3FBCF5E57EB9F99FD72F5A8BB + 675CAC1F76BB6D3ECE6B5C2FDBF7F0E6F117DDA3AF7EDABDFAE1EF76579FFFC8 + BEBBDE746797F6DBBDB7CFD4C63E238363FBB06C71DCE3F978FB318CD6090DDA + 94C8DF8A25F5E98ACEAEB59610E5AAED461F01BD94B731CE4D8FD1AAFFB68EE1 + 75C13C9BDC33A6F42B8535AD5D73641CC651BF32929E45BBF3ABE635D0BD22DF + 9D00FC4EF9E513801B6FDA26514E157F7BC22F64D8D77816BBFF700A1AEFFB77 + 187322EAF8981F4D58DF41D195988D3FFA610C6DD7E1D03A13F183799E0AD40C + 4EB6DE1AD13E89000CE64E2097F879D21EF98FE5D4F5C0927D2153BECCF0A2E8 + 26D10B335E7C5D07ADED6BAAFD908B428DD57AA0DACB239B3D2AD48E44D906A3 + D8058F761EE528106D65A161B0E01088BA4D247654A781C69D0AAD8B851D3F5E + 156F8BA8CB4514157A818F535BF53504609016A1A41F2DAC21860B586AAB8B58 + 62637B296B803171C730EE50280B855A7B6DB67C676F59B412B2E89B7D4B0560 + 2E842E451762918B36445888B1B4D1AE0BB6EC9368C1197D8ABAE00CFF2BD4FA + F0F9B1117B8EC7C762F11982ADC5F30EC795C5AC6C3F93000491266D3F1F8CC7 + F12BA86B7556052ED673C15EC5031501D8561B60AEDABD9F6DB5011546BCCF13 + 8928914DC7D19A88A318E54528CDA78D76E6334FE3780766249E4114C31DAB7B + 0890D6470C63D3DDACCF20C4E175375F793D5B02268088B97BFE75027DC64FE5 + 21471F938CB6E6AC1E7F9188F27D1C63A29A209A1B6388D660BF15ABA418FB0C + 4CC5B226E713F9F19AE0F8A7CF0B444B27886A7C6B2CD8F4B8694C1AA31C07C2 + B9305661BC8FA97D1B03F3DA9679FBF718F7857DEEC712D27E1B3AAEDF2F8218 + 8079716EECB38DFFC3CCCF8DE23F6FC0C78CB0F94DED23E6CE3BB081C6697CAA + 833F9C91CFAC1F4B7DFCACB768E5EA770DE07721803FFAEE005A4BF37D9CFA7C + 8CD660AC8EAFB11ECDF1F85CEF9FF24576D6637B0A8D618EAFA9BF711EFF9B07 + 5BF4DBEC5952536DFCCD27915DE3BD4FCF49E0E3181C87311A771DB40EEB7A11 + BADA8A3D82B1E98FCCECFCAB3FB7C3B9E2F0BC937E9CD7510C555FF6E3BC0FE7 + AC76EE2AF95ADBE7B1EDE96BE61C65CAC7FCC8AE798C896215EF672E48F5EEDB + 361080F55AC1439F423B04E0ED3D7B7DEC7AEEE9E7BFD2FDF95FFCDBEEDFFEEF + FF2FDD6FFF953FEB36B8B6420D9B476246004E5B1180FD989E3CBF7CAD92045F + C0B6D58BE0781C7340BA2E8DDBBCA6D56B5B4F4B009EC2D7D0EB6E1F13C503E6 + B4FC6F07CC619AF4FF720FE11CAFC1D85EEBB975195D9B896C8AFAAF0B45C7B4 + C661C762B5CB6C6C9CDDF67177B67ED49D17CE364FBA1DEEEC3DB76BCAFDCBEE + DE9B1F77BFFFE7FFA2FBE7FF87FFB9FBDBFFF4BFEB3EFAFE5FEED6F7EC376767 + D778BB9736D785C73698D7B1A47DD1F5D960ADB4DFE7EB93EAF8BA32968731A3 + B8C17CB3D6E009D7B481D31BA25CCFA886E76D6815373D861C93C158D1B8373D + 971B64FA35155DABA579A98DED569CE175B896FD8E77CF2F9100ECDEACEE4DDB + 24CA09046030FA90F10B86A88F7E6DB7E29672DDFC5B097EF0E48770C4821FAB + 4594711A2705234A4CF883CD1A519DC8DE8A75846311CD5FD9FE58BF155B6D0B + C65C04EA05273F6F0DB78F69FFA2B842DD7F27124F715327A4C7C2791C331FE4 + E85FA17AFFF02F54791174F3E8451E45D563D1BA514DB5735C6CA30B4DB563DB + FAAB66C2BB76B9C8908058EAC5D885A45A808F76161138A176632000D3AEF145 + B81DD1B2B728F1C3458B18C6E882CC527C0EEAE9E20EB758FCD138D8B7106D45 + D8F5E2EF40E0153B1673904BB088C645272E5EE90296B77B18E317AED4DEF201 + ADA35BDE15823EC55B88BF55CC2D82735AA42A7E6C13621B32BF38EA172E15BF + 288B3E445488AE17CFF1D8D93735065B3057CFE3178E69C3381482D167BEFA30 + 7E9A83F956B6AF2BB3AD20023F3570F7E8E32C78ACF0BF7D8B28026178F3A88C + 5B16CC0117D55702EC5E40E062BB2EB8FB0579C64D890853204F8511DA392E6D + D8FAF9A95DFD2A34692DFA7D1CD15A6C6B2DADC17E1676B35045518D42541585 + 4588A29FE2155FB3F4386FB3A9D01601B18DA222FA2D914EA160A7F8BCA5F89A + BE1FCD8D3E12E547680E6393006C6CEC33B97D311457D53FB0B9B9A7B61DF7F4 + DAC8EB525F1FCBADE3949C848DB92936D6D4BA2D74FE11F4FB7AB4AD7067387C + F619C77B0A739EA2EE47C3567D652C9DDF689F0B3AAF417B14DF7F9EA6A89F9F + E0330886FB80ADD52EAF17811F9FA361ACCDADBCB63E9EB5EB1832DE14519CCE + 99C0CEEF254F140F7C3DAD411F814DBF7F118FE3A7C750E3D0A68D76429FCF8B + 603E63A2EF7FF57BBBDA0E45F797F5305FFAFD6FAAFF1D26915FF37C1D053EF5 + 6B8D08CD8D6C6AF7B4E2A6CE352218CF9CB9BA1A5BED13E22F606C12809D08EC + CFFDD8C6F92205609E3BAA7F95FE68F1B9F571FEDAD7621BCC9D7712D6E5983A + AEDABC1DB9DAD65AEC6BCCB1A4BA8DBB7F793D105D8F28FE3A0577FFEE2000DB + 75CEE357DFEDFECE3FFAAFBAFFE22FFE6DF7A3DFFEA3E4E7FF054E73403EC55F + 235D439CF7626C6A5F58DC85ED2B28E355C26B2FCBF522F04201980C621A35F8 + 08688ABCEBE09A963E5EFB2A913DAAA1F8B82847AFD5E7EAFA6BFA53D08F89DA + 6D2221F7202806DB766DDB54D3ADC9803A5E6053D47F5D866B69764C36764CB6 + F975D8DA31C75DBFBBCDE36E5F04E0B39D5D2FE30EDFED47DD473FF8CBDD7FFD + 7FFA9FBBFFF7FFF7FFD7FD8FFFAFFF4FF7A33FF90FED7BD0BE2F779F746717F6 + 3D6AC7368D51E6AC5BA51FFFFA0CD64365FD137672C8BAE012B4761D8373109A + 7ECC533954007644B99E286F31A7D2206E5ACB68D58FECB01D3A1FE618871CD7 + 93BC068B115DABA5792DB515BBD7E158D7DBEF78F7BC1301589F01AEF076F07C + 4BB8177FC175EF00F61F607C38FB37E818BCA14B5C7D93FB1A867E39E856D1F8 + 8843625B5C37FF56821F3CF9311C71AA2FCA324EFD919531229BFA227B0BD6D2 + BCC826E889C11429DED598F29D0CD40F4E7CDE26DCC7B4A52D3881ACC782882F + F9E5C433CA3F84EBE62B3A9FEBD69DAAA1BECCF8C2E87D851789D7B958C4454B + 74110C18932EAC2FDC05BDBFC0479F78BBB4D7672FEC42DEDA5EE855415750F1 + F72001986304BED16285A3E587FD185AF95A5B1778081780782740C672CB9D10 + FA17FA6A1BD82F7B3F44DFB1009C17AFF4517C5CD4A28D7D6DFB6D047C3ED7D3 + FB70F7C54B1B333F1E70730551178B8CBAE8F8A9ED0F160221F6F60B90D52FFD + 4339240F8BA4105CEF7DF44D77FFE57792088C45E0A5E3470BAEBA255C5C0610 + 7A75A1196D0ABFF0B1F60A3CFEACBBF8FC57BBAF7FEB8FBB6F7EF38FBA475FFC + 288944AB87BD004C0132817CCB411E16D493F08BFE13C32DB0EBA27DD4F77615 + 1100FB6A63AC47E33556619CF6355FD1B17D5D8853116BF381746C5CDBC7693C + DA3B888276AC217EA99007D10E7751E2D8EF1EDB6BF8FC9BF49A2451EA91BD9E + 1647A114712AC24D81181559158CABE21C616D85B11AAF36456B11D6897C4073 + 755CE668FB1028382297C78162ADA7E6D83C78472BB7A9868A83461512AD36F2 + 57F8838B522381EF80C69CFDFE70FF898F63AC8F03F4718B7DBCF8F8BBDDF9CB + 6F6B0EB6513EEDDEA776EF53BFCE8FE37B34478FF3C68E977E365A3046E3D84F + DF49F5B3D71853DADA27788DC9D0D78FC1F612467336A2EF19C57F1FB1AF36C0 + EF31DFD778023B0551F411EFFFE7B28F518118BE55F0C4070F7CF46B9B7DD6A7 + 8F7EED47688D43D0B1588375F4F713BF99FC8D54F477368AF17E456334A705F3 + 5AF5482BCFE3E3E6E0F94942EEEA6DD5652CFBDEAE3EB579D2A38709CFFDD456 + 58DFFBA452CF0D8B808CF3548A933C678CA8F50B537EF629BAAAE85B855087AF + C71A0A635B366CA3710636A3753DE0AF230063D0AED715B8DE2AFE7C8DF16977 + 71F559B7B7D71EFFF7F7A3CFBFDFBDFEFAC7DD83E7F61D66D7075B3BF7C6FC20 + 04E7F9E46B075E47A8089BDA45005E351E013DBEF6B25C2F02E3DA321071B3DD + C533C7DB6877F9FE7F012B2DF1B74554E350BC00ECFDCA4D08C03DA81D331273 + 8FE1400158998A8B7C73AC1371EE0677F8AEEDF52D02F06667FDADBD3720006F + B200BCDFDAE7666FEF71E3EACB9F767FF5BFFCD7DD7FFF3FFD3FBB7FF23FFC1F + BB2F7FF627E90F342100EFCEEC7B0B734C626B1EEFD8391FC2603D54A0BDFA10 + 7BA2F968EDC118A4ACFD86BE087904B4E2D7B4A398089F776DBC0671AC2E11E5 + 9C52DF68D56A8D3B153F95630C8E752BFE6DE0E694ED59B8AD54ADABA036B66D + 0B5D6D148F7EF111DABCFD8E77CF3B1580BD087CB30230DEA0EEC300E403D124 + CA33D6C62609C40597931EFD4CDF1266C69BE5BAF9B712FB928A7E042B27FC01 + 8BEA07A269F3877C09E5C75EEBAEF1E3EF6CD527634D31C82B75467E5F3F18EF + 28503F38F1799BD47D94BFDE0B4F84FDDC61434EC9638EE2639610D50151EC12 + A25A4A94D3622A4F7D197F317487BFD8253C5EE982BA5E6CEB053D2EF0E522DC + B1C65F945F60F1A1C4958BF4351606CECC07815604DD91D85B04DEC8A6AC6D0C + 2E74E882478A2F8B106A5F8A8F670DA565279A3F421648B880A30B396C63E167 + 07A1160B40E9F1773D147715BF70C318C4477700F3AE5B5DCC425BED7EB14B6D + 80B1F4334663D537047EC4615E794EEB24FEBAC5442C5A2A669B5A946C8318DC + 193CBD78EB7DBA480AE1F5F2C5D74904C69DC058D0F5F36AD5D63A8AFA518FA2 + 2FE11DBF683326DDF96B6D8C8B4740EF9F7FDD9DBDFA7EF7E5CFFE6AF79FFE9B + FF5DF70FFFC5BFEB3EFFF1EF0D440F082378B4EBEEB1B51F99CD6A525CA977FE + 3E31DB53881A3636C6748BF32A24F8857785E205FDDAF636E6783FF1B510EBE7 + C01ABE0EF22878008823835AD6A6A0E3518107F058A93F128EB2005C44A8223C + 42381C0A4F6F928877F6D17792A896C4618376C01A14B55A209F392D812E8282 + 9EE27D9C2BEDADF94435086BF85A3E06DB285F99F6C357E6CD630E3112AF07B1 + 38D6E07CBCA8EBC5DF3D0460C4979AF83C6DF019325B1A03BEF219E3EBE0F163 + 92349EF90863F8B9A59F35C0FEC537DDE527DFEB9E7CF993EEF19B1F271198B5 + FC186AF37EF6D506F43B632ACEDB35A7F7C39E3F23FAD9D2CF10D0CF11EDFCBC + 29A8A9C7C9C3F9F8B99095BD6E6B7B3D7BDB787C6DB7F073A4004C61750A7E17 + A5EF1FE41AFA5D453BE319C33E611CBF931913D527EA471B79AB0901986331DF + D7641CC7573F7DEC7BE8670CF7A30573B406811DF34CF5CAEF26897E7BD58F3E + 7F83FD6F31FD8AC6B0EF7D0A7DAD7A8AE668ACAFE5EDC7C07C9EB7E8D69F4FD1 + 16F9067E3C89A4B423B137A2DE2D5CD85DBDAE241138C50CCF37797ECAEDF8DC + B28D3F2FF542ACC6D0C773E6A97C1FEBFBB455B1B7F8D84F635B9F82AF5E0760 + EBAF31F45A016D5E53A4B65D63608B5C3CFE797FFF55B7C7EB8271706D57AEEF + F0C797EB8759E0EEE732BC8E384800C6B50DA9F67C5D58A1688B9ADEB7803427 + 5CDFA84DE768E4EBD8F1352F05E0C8A7305FC55BDAB5B6F747304789E2006BCD + C5B5FCD379A81D331273E7881EFD2CB65AD7ADCBC46B33716CCBBE94B49636B2 + 3DEE76EB67DD7EF5BCDB6DECFD62C705E0F8E0D86F2C2709C01BBB6EDED935DD + B97D473DF9A67BFCBDDFE93EFF9D3FED5EFEF40FBB8BCF7E68BFFDF6FB7A6EBF + 9D5B7B1FB2BEE52E99EF75F689E89AA8C7FF0FDF567E649F04EB869BBC1EABDB + 4A59FBADE306BE81CDEA515054742D3BF2B7D0BC10A72B8C740F8FD7204EA94B + 9CAA0E68CDAB656B8D3D55A7508FB5D846F16F03CE07D4D71EE2ECB951845A08 + B93B27EAFA761173AB5DFDC597FCA5EFED77DC0E6EB1006CB68D6D0702B0B0BE + A8026F15791D4301B8F1812B1F889028BE30FAE25C90334934EE922F67E5BA73 + B895E04B537EFC461C788CA608EB8FE10FF5E8C7DA537EB807B1D10FBAB369FD + 9A17F868DBC01ED41DC5FA714F05EA47273E0771C0C99D136BB31DDB560DB117 + 21578F5FDE87BEE6F011C8650C173787E6A6FC20E6107C3D258A9F62900BCA31 + F1BE8C5E08DD3C53176FA7E23A63F04211F8BF88E6F1CA8B047691145ED467EA + C243C39F6C81784B54E455A1B7651FF8ADBE5F04A9F3293EED5F07D65A4A54C3 + 8338CE5DE7CF7E5ABC11E156F1E2AF3258C0B9FFDCE22102672138E75A7D5950 + D3052DB6D5A6783F69C5F9760FE690179ED49705E057857E31B18AAC22B4EAC2 + A32E469E1A5D70C5A22CC45788BF783C336C9C1F62A7E6A2755A709158E1FF03 + 469B31108437780CECC3CFBA8BE75F765FFEF4F7BBDFFE5BFFA8FB9D5FFC93EE + 3FFA8B7FD7FDD99FFFABEED5AFFEE56E95EEF47D93EF3A85980581AC085CB803 + 980BE8E94EE027B62F4F6DAE4F6DFBF8755A749F5AA0878D8BEEAC03B8185F17 + E427D03CE6029FABF5A2F816CC032A7CA83F12A422280CB5E2B27F2C3C5194A2 + 6805018B76DC994DA171C7D7C5DA102B53ADC7B99ED6F07881B015A3BE43FB9C + 37E07CE8A79D7DF875CE68ABA8483BA10F6DAD41BFB6E15741356DED58F9B129 + BCAB008CC743B33D882DF55197F654DB5E8BF4BAC08E5A12CF18CFD66211AF60 + 6E9C1FD11AF4EB3EE831611DBE171007D117E22FEEF2E75DC00AEB738C3934BE + 9537E58F7C69CEF61ED6CF0DD8429C34F4739444D4D2E7163692E3725D7FAC80 + 7F6FB1AF687C6FEBC7CA63F4C0A77354BBFA959D7CC728FADDC336BE7FF4BB8C + B1FC5E635FBFAB14D8F4BB388A439FF55B35E827EA23CC678CDAD43EE753E0E3 + 38FABBD2A25543A976F9FD54F8C754FC832AE07F83E798FA6D6FD9C192F1A23C + B5F3FCC2DB89B769BCDA9574FE5240DF8FA7B4EAF8B814EB84DE2CE45AFD199B + 8AC0DB0776BCEE67FFEE2A9FABEA392BCF1D7DDFD3B22B1464D1662DDA688F7C + 9A1BC11CC6A9E04B4639720DC036E03582BF56603F5D5BD835176D9B7B1F773B + 3B7E59B0B5EB8312BF135119DB2404BB6B8774FD50C4D6CD45662000BBEB198E + 3DBC0EEC45DA0110815B77F5CE3010805D1DBD8E55D6E5BAD7DF014C7B0BACB9 + 689FB9EAF7312DE6C6D43AADD8A9FC69507B9E81A07B0D6A3DB76613AFCD4CC7 + 45BE65E4DCB446566C5900C61DC0B8EBD7EAE37F019F197BF8AFBACDEE617A24 + F4668B636D9F970BFB9C5FDAE7E701CE11EC5CE6A1B5F1B402FBCEC37753FA83 + 76BC6E6EEDD0F74FCD40E0DD0CD74855008E728F25D7475D6276AE2746046BB9 + 03EC58535054742D1BFDCD7E1C17D9348FB9B57F8CA6E0E36FAB2ED19AD73173 + 3D601FD79B4CE4BB7138C73A5708B710679D00EC045DED276D0D375446F12586 + 7135466C37CD942678C790F74C00BECE1DC0C187931FDA297C4E61F4C5E9E293 + 3FF872BD5166E6FC7E62C730FA11AC9CF01887F5C7F084A1DA267EB0193B8A97 + 18CF287E062F00EB98835ACEDF8A3D94E8A4E746716265B6972DC54C85798476 + 1C13527CACA90C7217728A1A8AAFA744F19E280FCCC74517456F9FB90BB8E32F + F03287E6235E515F1580C30B7BC7941FF648C06D08BBDA6EC56DCE3FEA3665D1 + 638E704E85393FF0F596D0AAE16D587CE1428C427B5A9C09046085C26F648300 + 4C8676C4F58B5968136FD33EDB8A8FA98B58255FE917B930FE3026E51BD162E2 + 9C00ACB65381C54E0F1771898FD7BEC7D7F2F8DABA48CD36E3D887308CBB917F + F38FFE5EF78FFFE5BFEF7EEB6FFFE3EE931FFFBC7BF69DBFD4DDFBF4573A881C + ABF4D8E737E94EC6F37207301E398C3B7C75317D2D02F0E6496F1FC4C8823C16 + DCB180AF7EBF20EF8504A5E5833D823ECE837DE64573405F6B288C5131C78B3C + BE3D452F560D4526A0C215FAF5CE5FFC6F3EDC9DF3E29BEEE2E5B7D59FC4AA72 + 57B017B32872A9A0C5FA2DC19130DEC75064445BEBB0CF38F635866DCDF1E81C + 3506F6B4B056F691FE39381E8E9B1F3BEDA3137E15D8929FB1656ED1FC3996B7 + D3A7B4ECEA5B02E7C479B10604601E2BF4F10868BE8FFC189AAF30D63315437B + 544F811F73633FE7621EF9F3C1CF98A29F1F0AC3148787E22FF3FBF9E9B81EBE + 9F18C33CED67FA79C4E30D7DF4C3CE79729BDAE69B4305606FC7F712BFC7FCF7 + 15602EDACC432C0552ADA779DA57BF8EB9C2E276F95ED518CD89A07F2E8EFB45 + 60638EF745687DDAD8F775F8BBE989C45FDAD50696FC6EEBEFBBCFF77EB5CFC1 + 5AACA775B496B71F839E43695DB503D8E6C6D5788AB973206EF01868118121B2 + 6C1F606B7D77D7AFDEFDCBED12101BC547E7ADFDF9EAF08F15B155DF145ACFD3 + 8C97F37FE0AF0FA6F0B9DB4B9BB7B1B9B0BAF5A949C3BA109A715D1089BF2A00 + 2711F8E2A55DD315E47AA632B816C4B6176F071C2800EB7C888F695DCB927729 + 0003E66A8E9F0BDA87CE731E8C374D24E41E4BADE9D665E2B599715C14B38461 + 9DDCF76B7B1B88C09BC7DDD6B678FCF31AE2EFFECA7C10121F26F174BB7FDAED + EDFDBDBBB0EFA633BBE63CC37B1D7FF4605B08C3F89CD9E7746571E9B57CCBEB + 872A007B6E560006A85DEAEBDAA3ACDD2E62FB30E90B1EBF9E1DD9A3B8495A9A + C294CEE07D418DD69C2B41CEA4FD18A6C6F0B63916CFEB7E8727C3366F48BC49 + 748E757C0AB7176613B1D689B9DA4FDA9A0AC0CECF98815FECA7A225F21E2A00 + 57BDB101B5CA8136F981F04E04601009C060280053F4BDBE00BC59DB58FE83B0 + 04CD91EDDC17E9EC17DC4DE0E7FC4160C770F003E87E3CDFA6001C08A873306E + 94C31F7CF9E167CC28D6C31306A2366B873534D6C55C97E8A4E75D323EA1CD36 + 7FB29BE6CEE333134BBBF60F6530F681B5381F258AF34479CAB2787F51741A4E + 7791B60C8EE7C73CE442748E74F18476BD90770C2EF21782781176ABA0CBC741 + CBFFF855F177100B1F88EA0BB840ABFDC63CB9F841A218E0E39632957F48EDCD + 3D1577FDDDBC56A3803EFE927FE8B36395E8E3D2E24E118095D12254810B5F0A + 16DFFC021CD01A51AD6C431BF6F1389BF498E6F16222D085C62866CACF3E1730 + E71632818F05D182ACFAD5EED1380F177B75215AC7529FC69C3FFEA2BBF7E2EB + EECD0F7FDEFDF4AFFCA2FBE4C7BFDBAD9F7FDDAD9E4048F9BA5BE30EE0476FBA + CBE7DF767BDB6E1E7CD69DD9F6FCF197DDEE912C7AA785731BEB891D3363FDD8 + E66B7DBFD0CE3EC0A23B8862D8569BA23E5DD8D736FB2A78703B3586B7618B3C + 5F9B78B187ED08F84964CBF65E60F2421445286CF118DF479FFF280979C96EAC + 8A307966AF21EE0EC6DDC05EC852E043AE8A94DCFAB6073E0F7D2AAAAA4FFB6A + F3F1AC437C0CED983FD1D808FA35261A3789A44FAD5E4300A608ACF558D3D79B + 827114A1D1F6FBC2BADAA74DED511EEDD8FA7D0468A73CDB5FDAE6608D088DC1 + 7868EBBCD8667FC8701F596F9BEE9C1E7E5EF899F16D2F006B5EEF1B8EE3E1FC + A2CF9D27FBFA39A8F8CB71D96E09C00ABF635ADF334063348EC26E144B1FDA78 + 843DFFC72F62F89DA7DF7DFE3B8F608EBAAFBA0FE90910781434FEB8C7FC119C + 97A2F53947E2E3B0E5DC8EC5D7617F640F045DB6233BE2097D3C77609C12D550 + 68F7E700803E25B26B2DCDD558DAE7E0BE10D858436B69BBE6C91FDD79FF142A + F24E01C1D703BB3E069AE79773D4F34839AF9C43CF51E3F3D5F179AC8E17D1CA + 8BF0B169EB445C156B5B4439898B8FBBB3CB4FBB8BFB76BE78CFE60DF1F7C2AE + 156C8BFF0B8C7F3383FCFC3F80F3F5811780071CF43F807B71F654E85CBCAF75 + 6D4CBCB03A87CF6FD923A662E704E008FA8F03E3B58944DCEB50EBBA7599F6DA + CC6988C6F3E031CFDB44114A93FD2AB1C69DA5465AFB387BD1EDCEEDF3616CF6 + F69939B3CF7DFA230AFB0CDDB32DDE7FC88590F996D70FDFAD006CFD0DB765DD + F1180E1080BD6D29B3B92DAD21B207B6D69C2B519D29FB314C8DE16D732C9ED7 + 3B148001C7ADE317E1F60801983A5B145FED0EE6509B9B82B12D96C49C923B01 + F82D9205623BD803015838F40E6008C0ADFFD7AB387FFA3FBEEAD3F612187F48 + CEB1BCAD71DE2AF62331F801F43F9EC7FFC88D18D40D7002EBC8EF609C927C4E + 8C6D09B3D51FA1F9EC17DBA8067D8D98EB129DF45C97E824D513C5E3D1CDDB73 + 9CF8E38220A33940F352EE827D98CA5F8AD63894A8DE12901B1D0312C58FE1C5 + D034A7B9E0EAFF3A5869D9E7D0BA989BFF4B64E0E35A70FFE6F6B1FEDF5E5ED4 + 3784D4C520BF88BBE92FD1BDB82B42307D21564B173E746E03BB8E1D80182C90 + 688ECFF33E10E55C87681C1D634E00AE0B36D656BBF6D596816D7E212B823911 + 51FC18C4A2CEF0FF10EF1E9ADDDA5C40D4054B2E407A16F9EF7F92DA58B88C16 + 68A35CFA5B685CB4501BA1F911ACA3F303CC859D8BD4F4A5FFFFFBE48BEEFC39 + EE86FC4E117FB3E001610AA2E2D9D36FBAFBCFBFEDCE1FBDE976F73FEBCEAE2C + FEE19B6EFFF073C36A72CC24F8DA58C6F609806DB8688F36EF36C3A23B409BC0 + E7EF4653BBFA19C3C57B6DAB4F05108D25DEA671BAA568A27D0021640A881FDA + 665F05953E662C385188C2EBC13E04AA2F7EFC7BDD1FFCDD7FD2FDF0F7FE5677 + EFE577BB1516BBCDBFC763A1719776502B82E220B6047DE64673A2CDC7682E7D + 2D3446C7559FF7FBFA1AA36D456D6853E48BE2E99F128061E31DC01ED488E6AA + 768EC5B68FD55ABAF5BEC8EE7DDC57D4C7FB0777FCEAB88C610EF35A689CC7C7 + A6E368EF538AA9C48BABF9FF2EF7C745EBA900ECE1672AC2C72581F5797C2C23 + FC9C69F7F3CBFEF17C747CE0C561A0F15B03DF251468F91DA4DF3F80DF7B4063 + 5AE8F715E07716DA5A8B711AAB7DCC2F09BDD6F74236F757ED7344E3715E3AF6 + 14D1DCE7606DE6F9B1AADD89B91EDAB1F5C0AEBFD9510C613D1FA7768F8F699D + 1FCCD501CC9D43CF9378BE13CD833E6FD3BCA5A8C87B28147FD3F909DAE51CF1 + B0F3CC7CAE1AD9F1C787FE5C1878C1D60BB3EA9F9B8B8F575B139B171E095DC5 + 5B41AF0F3CF0E77F19D3C33CDCFD0B01F80C625612B5B2F8CB6B30E6F663F402 + 6B4811809B8F813638AFC93B80AF01E611D9E7AE6DAF2B0047F8385FC3FB815E + ABFB18B45B7364CCE160BC3691887B1D6A5DB72EA36B33BE0FA2B52ECD3D9E61 + 9DF51677FA5E75BBF4C8678878F76CDE97DDEAFC7EB73E7B609F8B87693FB6BB + 27DDC5E659F760FD34716FFBB43B37F6781F5DE2F538D5FC0EA3AE5972DDB4B1 + 161AE51ECBA06E194FC73F982D1EE53C7E9C73B8A6EDD0F81651DE8896D610D9 + 9D6DD158519D29FB319CAA0E583CAF772C000BEB1DB6106E21CECA23A0811374 + B5EF75B61A3383CF9B83FA5D8B28E7D47C88A2AFF2410BC0439C004CF8C16D50 + 05609FA3FD08D6F0FD08CDBB0EA7AE772BC882659B853F584B08EB0B453C05F5 + 475C6C1EFDE11FFCE88B10EBFB514E649B6249BC8FB90ED149CF75D093D8297C + 0E4E8079A1D2830B807C72AC27C89ABB04D63F2697706CD6623D9D578BF4C8EA + A0E61C512D1D7B497C7C61747A78A146F1F614F8FAD105A18F6BA1EF2B5F436B + 43004EC22C05D622B28E2FF28717FF3E8EA4C506038B0FBB7B1F0F16212802FB + B607FFC70AB5B8B8016A7D9B8BDAC3B9099C3F3926E7BA685D9DFB10F3DF1F8A + B92AEA2E158087F48FB4D3452CB5D18E8529FA0917ACD08670BB7B887A997C67 + EF745D0ABF60FFE855222DF2195C40D485475D7CD47EE457926F8100ECF3E98F + D078B5477D457D4BFD9C2FFD5CD80659F8FDAADB3EFEA25B5D61F1FE4D127FD7 + 4590C902D157E9FFFE9E9BFDE2896D1FBDE9F60F3EEFCE1E7C311280B710661F + D97E199BC7C0E680B1CDCE0576B457C1A342B998EF055EB529F4690DA00BFCF4 + C346E14363E9236A533182683EDA106ACE5F4008CA024816437A5188E288A2C2 + 49BD7BCEFAB4A9E084D780F5283A51BC83EDD9D7BFD67DFF677FADFBE2873FEF + 2E9E7FA75B43F4458CE5ECEDB5DA42C07C6675ECF5439E078220EF3E251C836D + 8039E4F9E4BEDABC9D79BA1F1AA379FD7EF6F94ACBC75C021BC7D51CBF1FF433 + 0F6D10C5F9FFFFEB510198B5B8457EAAF7E2EB5C47EC8079ECAB5DFB9C939FDB + 1CBE3EC7471DFE1101044E8D870F30B705733CF4632C45730163877ED4E8F795 + 7119C4F2B3D10B88DA479BFD960809FFA102704B042643DFF073EEC7E71C221F + E78CEF98F3F29DC2EF2B7EF7F07B07F0FB8FDF49847EE6F03B90DF83EA43DBFB + D4CF18B20166F37327D1318F6C446BEB98A371658E3E863EFAB51FE17359135B + B6AB5D7E27F91BAAED16FCDDE56F7014E361EC12744EB4B57EFF97C05C45CF4D + 686BC52AE9BCC69D2B318FE73C1ECD672CB6A0DEBDBB90E82EE04AF9FFBFC49F + 57B2EF99F2E5F3D4E1B9B08FD1F35C6D039FC33E6D3E9E7D6FF3FF035805607F + BD10813808B8B89317A8989BAE2DF028DB739BDF99C5050230E20E1180577CFC + B3C1394C118AC0E5F1CF83BA3E062C794C346200AE65E5FA752DD7B544AF7B0F + 01B9BCBED6EB6CF57B5B04635AD7F46C4773D4F8C3C178D37811F73AD49A6E5D + 46D7667C1FE89A0ED1DCE3D13A58837A60B5EF775BB0BBECD6C6EAECC2DE47B6 + 3DBF6798FFC2E663B197EBC7DDC3D513E351F7C8DA0FD68FBA73AB037FBA5BB8 + D67D7BD4354BAC83066BAEEC47B94BF1C7DED76FA2EBC2DAF61C2900330E798A + D62051FE8029ADC1DB83D8D9715AF55BF677CDD23971FE6F711F5AC7BA0AC081 + 583B128005AFAD8D62981B103DF5F736F0218BBC53DC5A0138C1FF014CB1B7F4 + 37D65FFAD700594886F8EB3E70D107516DB4AB5FE35A3EDABD8FB6161A7B2CA7 + AC756B68FC082616FC501D4238C6F518FDF0473FECAEAF270C1183BCD6498231 + 8A0F7CA7404F764E859EC8126FF7B1BC00D8D9C5DBFEE265B7BFB4AD912F0ADA + 27C8DA6E91EBE7FCC8BF048ECB5AC3F9A0DD46F37DDD0CEC63FA1A1C6788D668 + C7D27E3344176CD1BE00CD8BFC44E3881F87B47C3EDF5F5C7A3FED550006E776 + 917EFE222F1A38D1325FE4F722E99460AC8B156827BB81BA20147F31665D4C18 + D69B43E33DDEBF3447897CFE98D01EA131C823384624D56B08BAB4AB004C1BFB + 9E6C1F2F5811DA78D7852E4E01C6E57E7EECB492C7816FBC08463806C7E15878 + 04F46A62C151F10B935C886CF9B830497481D3A376C6126F672CC6419B304E63 + 35DE8BA2115CF8C676F5D0EAC8C23705CCBDB5F118D82C107DA75B3F91470FDB + 16FFEF776736FC0FE033EB43F42549FC2D8BD269613A89BE76BC6C6C8C83F138 + A6F601E6C00578CE977151BCEE938F035A0F300EB696483287CF571F6D55D428 + A2D5581CEA052D8A47ECAB8DF4B943218AF168E37FFBE27FFC6EF188EE8738F6 + 369717DF241F6A9CD9EB9863ED383CC5DC6CEE45002314FD00EF0CC51DDFEBA7 + E52ECDA736B7675FD7EDE629DA36BF05E228C05DB4208A579FDFFF08CE99FDFE + 18E5FDE57ED03F85CF217A5CD25832DF11AE26E7A63580C6D2E763E94FB692E7 + D15CCD613F8A459B82AFC662FFF1FAB20F1F7359F714B0B6F6751CCC833608C0 + FC83849C87B995D7A1C412AD09A2180F62F458683CFB4AFDFE0B7C8AAFE1DBDE + EFED0A7CE958D87B66873FDAB0B9EA7C7DAEDAD5873BFF778FEDB8E00F420C7C + 7703B4F1C73C00EDB57D6F00DAF1C878DA999362AEF05D8AE381EF922C62B7E0 + 776104BF7BFD77B0B6B58F6DF45DAF3EFAD99E221AC3DB10872D41DFFFE6B01E + 6D9A8FD8151E855D7E6B15FDFD225A537D8CA54DDB3E07ACCA1F55CDE1CF2B14 + 3DEF511BCF4B688B609C0AC0CA2026406B691E855DDDFA36FB40FF1730FF1F70 + 42CE19D1D6F34AB52BB42B3E268BC0386FEDFDEDD858C4F568AEC645B96843F0 + F530266DE5BA205F0B8CC1983C8F4ED7561C0FD719106A9308FC51777669E396 + 6B2DFC11EEAE5C5F8CAF5370EDD68BACBD50DB5FB38C73DAB4EE04EEEBC67EE4 + 35F329FCB25FAE592902AFDD752DE1F5ED21447540E4D73C0FFCD1B5BDFA355E + D1F8C3C178CB88045D4F9417D1AFC90CDB511FF8F524C69C9E87868A850FBAD5 + FEBEED9B6DC1B9CDEBC28EB9CD05FF2FF87CF3A43BDB629BD9A61A5765CD15B5 + 7CFD2359582BAD572E580B8D7297A2F9A98D310A2BD62F63CFD2980BE7A90CD7 + 83798C7BF07AA9F0BB3D7B540560FA159FBF18AF4104BAC4B5C7B84D70FF16EC + F7BBA075ACB30D7314F136CDD9B6106C07F61EAFB10DFC4EF08DA02637871FE7 + 26F102B0DEF9CBB9D0B67C7E45BF6C321C13B0F6B2FAD7E73D1080FB83E4455F + CF20170CDE90F241E407D37F405B7645FCE1DDC11233787C740B9F7B1D6EA2E6 + 2F13FEC77729FCD175626CFD61165B28D80E7EB8878CF2E7602DADD7A8DD3AB9 + B83DD81CF1233502F63EAE3F097E6627351F65F1D7C0451BD8420C0B4FEE33B8 + 48D07A113CC93EFEE41A7998E778FC25A006F7735C9BBE3837BA08BAEE7C96C2 + 7138567461A636B593B9FC43D11A1475B155E8F779DA8F400C05600AB15C3820 + DBCB97F52FC947A2AD4131B73EDA1922B25DCCF32FD575E1212D6CE0421FA2B1 + BDFFF1FF775468567401620E9FEB3924D6A3B9CCC77E40B455BFE610C471FF7D + 0C6B28BAD8A38B47205A3422E8FBC52CDA5B301E5B9F4BE883004CA1D90BC088 + D3B978722DABCF45BEFBD646CD99C5C6165C808C6C9EB4382982AC12C502C442 + 24F57D1575D907BE06ED8C8D1678011791B9588CC5692E16AB9D82261794774F + D1FFD2FCE8E798DE270BE5E687F09BE6EA7CC96F36FC4F483ED694768E0DD0A7 + 808A78B5133F36FB910D6DBF3F0AE3E7E09CD0D65CB57B1FC40D0830106B2231 + 4A7D14C054D8A15D6D406B509062BD6A83006CDB54BFD44A7550F329EAD93C9F + 7DD69D3DC7FCBFCC733736F65A3316639FBDC0E3BEAD2E44620865CF332A00A3 + BFB678C48C04D102855DF655FC8D1EA58C79EA3E13EE2BF7B7EE978B531F8FA1 + E601F67D0DBE16EC03D651AA30A9F39EC921794C8C91B7F8030B90C5CD1EF87B + D1730CFD99E1D87E7CB561DEDEAEFEC80E58DF1F4B853EF5F735F2BCF31F31C0 + 9F6D3BBCAFCAFE687F38D76CEB19BEA67E8EECEB3C3CA85BFFC8A1D8FCDC157C + B6B0656DE2E3A66AC08E3A2A26B386E6A90DDB089D03607E448A3728F642D8A5 + E84BDB9C008C6D15804B5FBFF300BE0FF9C86AB4E1C377B8C62C817595399FB7 + F1F7630EC6B3BE1F87BF91ECA34D1B7F5F3506B9FADBA0718CD12DD15A3A0EDA + 84B1DA675DB68F6155046AB4792E82F389E8BC27EAB30D343F62CACFDAE97F04 + 03192BCAA9F112E7FD515CB21711788D71EC5A98A848AC4232FB7C94B4BF1B39 + 9D779673590FCF6D89F7F3FC358AE9CF6BC7F1A475AEEEF175384E3427D8AA00 + 6CC056EBE07AA25CAB6DADADD71A00D71F7ADD425BA688AB729D82D861CCF0FA + 65CA46BB8F51BFDA01C6C35390FAB9E05A92D7A7C6B9B3D9F56BA55EC73ABBB1 + 3E10BDBED66BE8E8FADAA37E652E6ECEDFE6B163CE7F28BE9E1DD3C11AC93CD1 + FA0FA0081CF9C821F5A663FB396B4EDA07DB22B6FF7FBA586B8408656D08E183 + F8E318CD6989006C31793ED3CCC660AC29EAFA6ADF86F8BBC1D66AB7605CFEDF + CA66937DACFB0A5C4E45D77ED39AAF21EBDB140255048E84C101C7680A3E87FD + 43EBBC0FE8BEF9FD8B6CA764617DBEEE91BD0AC04923337BAD697DB655E03554 + 63EBF5B5D3A3E344F8B99C1A157CB7769C28CAA20D7CFC980BFB3C47C22F317F + 90F74B230053619FC70E48622CFA7A7C6EFFC6B537557D73CB0741FB68DB3809 + EB875F8E2506E2EE068F942EF9830F196C465300D67AA7E4A6EB7FE8E88FE975 + 8944D7FAC3DC9F0878BBC298C10F7D23B642FF540C404D3DA979C7A4939B91DD + E6A99F41B42BF0F5B138914E27FBFB17E9914D7874D3FEC22EEE6CBBC1FF460D + 4FB6C7B61683933019B7679CA3701C8E79C8D81E9FE76BCEA1B9402F920EBF60 + EAF17515F851D78BACCC9D1AD7D7988A5D82D69823CA9F0239DBB3E7E991CD91 + 08ECDB1AC3761580895DD8639161FF303FF2775B44E02A727221C0DD3DAC8B01 + 35A64114A3B9D745EB6AEDBC48D1A339118C8BEAE158C0C7E3928E8D2C0C795A + 02B02E0C611B2D1C11F5F97C4563F216E30E05E00CE2C635999731BF2ED881AB + F182E3683130B0B7E2D5AE706133622A46855DF6E9830DA2AA8AC2B4B3AD7041 + 375A0CE622B1A23E2C2873E11B8F6AC6FFEC857D677D3C1A1ADB41DC33080B45 + EC34B6A566027DF3218E357147F1C6EAA04D747EAC4B584B6B005D5C479F71F4 + B3EDE314D6048C89FAAC4160F3F9B4D18E38156A5484F1766CB3C0954528C652 + D4A14DFB1EFA6A4C115A01045F8838E92EDE22AE6E9FE2D87C6694D7D1E68DD7 + 2BDD91879C17361FCB497379F665F7F0CD8FBAEFFEECAF75BFFAF3BFD95D7DF1 + E36EF5D86A3DFDC662BF63B1DF29A2F0705CCF40280DECDEDFDA57C0FDE47163 + 9F39EAF331515FEF02A5DFC7681F4038A4084C9152FDCCF1F4316867BC000CB1 + 5345510FE33C53E30D8F5DDFA65DE37DAEC278258AF1369D27F6012230B6BA9F + 3806671F7D9BB6693F6D0E492C7D64EF53BCD7F03E337BF2A59C7E7CCE979F1F + DA35C6837A2A00C316C5D3A675BD5FC7659B7DC5DB5903362F0C4F91FE8042E6 + A07548BDA31EF52C1EFF071CDF0510715504467FF3C8C6BEB2DF838776ACAD0F + 92C02B300F6DC46FADAEFFFEC3771FC55FFFBDD882B1DA669E42BFB711B561CB + DF85A5309FD0AEBF8F4A348EDA95961F36D6679B7DB6190BE8D3FC9588B784F9 + D82A9A0734870CCE27EC3C63EE9C27B2A93D8A218CF539E95C6B4200D6F61258 + 475101B87597300560DAE604609CA3F29CD69FE77A78CE3AD7E739AEDAF5BC57 + DB0AEDF4F9737A0FED3AB6EF23A6D628D71BE9DFC904D722DEE6AF4B786D0218 + 1FF9BCDDA37153B11AC379AB00BC3EB72DAE6B01ED2A0A03BB8E4D689F5B5C8B + DB761D90AE7DADBEBF7666DFDB5B7EE2E394B9D8C8CF35846910A34431A7836B + 1BC48FAF77F512AE39CDF5231833076229287BFB1C7D3C8FB9CCF144EB8EFD18 + C339F97EA588B34BC69F8D51913760B05E6B6CACBF2DA00D7F44CA0532967F0D + E604E07521AFFF1AB2BE4D8D824402B0EF2FD514C23C6D7B34D618E5BF45AE35 + F6D4BEF9FEA168CD39A2FC25A4FC4B6B67CDABDAD0D7FA554773DA9A08B62747 + C69C42F5BE960E7808105FF7FBAB24F252003E3F7F9480FDECEC61DACE8BB4CB + 04600ACD911E7AA8FD506EF71DC0828ABC53684EFAE7D6F50D8537B5BEE91D91 + 9DF1CA9C5F59108B2F9F77F9E57787A03FA6D7A5FE0897B66DF5C75E19E409A3 + 18D6F3A89DE3B6620BA9A69C68DC4E6CAEF86CF0F3C176023E8DC5891F4E92EC + 847FFFDCB08BC933BBB03BB3B65D00D07F2CC393761D3722AE414627FEE93536 + 7B61B5B11A787DD26B24710EADA3F39BC3D73805ACA5B57D7D7F5106D4A7DB08 + CD9B8AF34CCD892CA93D554373F385732FE052E055A197760AC1DA1EE4412CB6 + 0BF9FDD5A7DDF9A3D709B47937B0DE11EC05E09B4017180EC1D7C88B133D8CA3 + 70EBFD4A5413D0876DAD53167488EFD3A60B4F6CF3AE005D1CD2B6A75593F471 + 36FFF2A86988BEBB87F87FC0881F0AC0516E8FD9B88007AEFA85C66871700983 + 8543E9FB3845055B6D7B188F7AE8237669AEFA2012D7455C59F4559B5F0026B0 + 61615A17E0D5CE3C8FFAA6F2BC8FE302CE0F768C1F2DF2D38EB6F6B516DBDC57 + CDD15ADAD69A6A27ACA177B5793FEDDEA7C20E441B156A928853841ADAD8072A + F6C0A7A29CFAD4DF122371E79FC6E3AE5FBF3F148071173005C8F4D8E7275F74 + 9FFEF0E7DD2FFE8BFF6DF70FFED97FDB7DF22BBFDDADEEBFEA762FBEEDCE5E7E + 2FDD098C3B8023713709B981C0BB7DD1C743944EC2B43098BB83C7C18BAF0AE2 + BC0F36EC3BFB3C6EAC493F6D8C450DC6308E6D1D835BFAD5A7B036C7CA76C465 + 2114D046615445CF16C8E1983A1782F1F4BD455B3F878CF699EBE1FC89FA7CAD + 9E3C4F807DA100CC3EB76C73FF1187FF578D478DF7BE787E988BFF0CA95FE746 + 78AC747F34AF6557BF8EA7E85C7C0C72F575824D0560C66A8E47E7C11A80F949 + FC65BC7DE6F1F8688ABD203DBEDF8E27045DDED50BFFB91D7B9044608B3BB3E3 + 4EE117B60BF39DE1F5C0E3E6DD7726BF0F41FA0E2C3E7E3F12C64FC1DA7E8C29 + 5B1403F81BC1DF090FF340343F1FAB31B0F1378CFB479B8E3B05F3231BE0D8EA + 27AB86000C98D7CAC75339F07F99BD9DE7157A3EC3F30DED138D233C3FD25A3C + 6FA28F351933550FCCF981F7B3AF63A7F1CB3922055E157F29FC7A54F0253BFC + 1EDAD69F9B8ECF4BC7E7BD4463785ECDF3E52886B4CEAB7D9CD6D37825B2FBF8 + DA966B0E5E5B2CC55F9B00EFD3786F673B8AABFE72AD17F97BE0475C2F02B780 + 20BC3EB336AE7509AF67D9565F616DE8F5EF14AD3FFC8E809FD7DAECB7F2D4B7 + C47FDBE8D71596C3F5A6963DC2C7120A8D5E70F444353D87C68F80C01AB505D4 + F6738DE28EE1A0B5511583D136EA7AADACB52ADEAEB5D69A8F7A3A96C6957CD6 + 4A70DD77F5C0B6B059DBAF711FC312AD4399D23D5AF6F711DD17DFD618B60F85 + 35A7D0389F7F28ADBA105A2351B66A6B3743D5F1744C87EA7D9E481F1C2002AE + 8AA9107E29F6D2BEDB5D190F520E38F51DC091A04B5BE43B15BF9C02B0BEE17D + 5F515F14C3BE8F39000AC0271181755E771C8EFE98DE12463FF24B98117FC1E0 + C4E3D662734D626F047C1A8B13C0C2D62E54B6CFED33F5A2DEFD3BF01F495F47 + C78DC8B1FEE4BE497A8D0BECA7D7A78FD17A514DEF9FE2D8BC2996D46A8D8B0B + 34BD33587DD745C78CC6269CC3B1178D9A5705603CBEB988B92AEA72AB77027B + E84BDC7B9984DEB387AF121080C948002E8B0637852E2E1CB230E2F399ABC04E + E1B60AB88D38E2E7067FCD758B3B2D10A70B4A5C6052E89B83F5C0541D8AC07A + F7AF3E02DAC78F31BF2CD06D0C5D109C820B8C1EFA478B88410E63B9B0E91738 + BD8DF1512DB024176D08C0750157167C7551D82F04B37F2C5C04D7C570AD4D9B + FA3D8C674C14EB7D84F98CC156F7D5C773E11EBEC8AF70319F8206618D280E5B + D4A540A3820C6D5EB0D138DA88E6799F02BF0A5B7E0CC6A4B8A76EBEB60F107F + D31DC0A9CE37DD1E7CF44D126A1F7DF993EE877FF067DD4FFFE817DDE3AF7EDC + AD6C4EFA38E8F478688BD3473B6B1BECACD6FEE577D296E22F3946002614D122 + D4875C3D4E5A93C0AFAF95D6D038D6844F99B3038EA37522011877C1C2A682A8 + C2D85E341D1F1B85E37AA2385F8B360F6BB0AF393EB79FA79F77EED3466118DB + 8B8FBFD77DF31B7FD4FDE40FFE4EF7E4CB9F261BE250977367FD7E9C7EDFFC6B + D9C2E78156CD963D82E32BF421DF8F7FC81DC040E7C21ACA604CF3E369002AF6 + A2AD023044DE0B7BDFF1AEE0146FB97A973080000CB636577E07F2FB34FA4ED5 + EF477EE7001FC758B6B5A6D6F57642BF4763F8BBC0783F0FCECDA3318CD3BED6 + 461F6DFC0EADEC9C14ED08E6B03DD5F7CCC54FE5CE815CFFC41185E71BD17988 + C2F3A3569CD6A09FF838ED2F85E36B5F5101381279D15651587D112D01B8D557 + 5B849E834FC138C6B2AE8EA363A95FB744EBF1AE61DEF54BA6AE3BAE8B5E0B4D + D9A6F0D73DB1CFAE35AF2300035C0B9F9BCF1808C206FF6FF029C1F534E1F5BA + B74FE52CF1DF5EB0BFD7235E93CA44F14405558DD7FC2568CDC87F28511DD838 + DF642B62E929C6AC6BAE816F04C6452CB644ED65AD95F87EB2D57A7D4E8ADB98 + BFFA86D45C894F0C6EFEC13AE909EEAE3D5453A00E11E5B5ECEF23BA2FBEAD31 + 6C9F9A68EC0308DF175AABB687C2ECDBA2EA7881F04B54EF3B15107721F602F4 + BDF80AFFA90560C50BD37702B01189BD119A332B0047F04DCF0F41C3BF36F411 + D08742D1978F61B8F6973428730B7D77CCE37F4C4FC0E0479A3FCE0B04DAC5CC + D56AF887271EB71D9B333E1F036C1FCA49663EE9138A009C45E05E501CC4187A + E2AD7DF5B33D249AA332ACDDAEB30CE4E3228602292F687CCC528ECD9B63EE82 + 4BC7D5B175DF80DAD93E163FA68EABE8DCE798CA47BB0AC013A8C8EBC55FF525 + 3FB676810FB1D7DFFD0B2076EA45FF4DC20506CC87022D6D4BD13A0035885FC8 + 89166C145DBCE1020EE25983715335FC18CCD779F8D88868DE2D5BBED3D7F63D + 3D021A649F8EA1B939878C17E94E29007BA258409F2E70464479516DADA78BB3 + 237BB0F03B25007BD2E2EFD3E162377388DAB18D16C9592BCA89D05CD69B8A51 + BBCE1D5BEE2F73588F736CD5C6E3AC3D78CCB5B27B0EE105A24A79F4B58B497D + B347220E6D5EB851F166CA4728EC10DAA772D4CFB9FBFD617FF7C26220C4429C + 4AA2ECD7DD0E8FE67DF9DDEEECE5B7DDD9C7DFADE22FEE04DEBDC0FF01B67915 + C157455EDAD15701D88BC0406DBA2F8A0A778A8FC1D68BB0F4A928AB3E1C233D + A6DE4F68077884301F070DFB52B4968ABAD8F291D0685324655FA1689A19BEFE + 7E3CA2311AA736D6D2BC288EB1C4E70C63D0867D3C77B6753F570F3FEF1E7EFE + A3EE77FFD67FDAFDE2CFFF55F7F98F7ECF5E17BC26F9B8B3760BCE8977DFEB7C + A2F9CD7D96BCDFE3E309F3A25C9FA3E26F544BD1DC282EB2731EE9FF823FB671 + 1ED977A91D530ABCD80288C19B87F67D60717B1C2B118DF198E8DD6388B876FC + E0C777867D7710FD3E05FCDEA5F84B01D8E7683C6D1E8D8FEC113E961C1AA77D + BF55BFC2DF250AC1407FA74894E37DBEEFED9C83C6715C1D9FBF8D04B1BE8F7F + E530250093C8AFE72F145AE7EA00CD539250DBB0B788E2951A57045D8ABB5EE4 + F5768531CA520158D173676EA37C6D2BB0214FCFEFB5A647F35A76E6137F0D01 + F4BA04E835CB314435A2DAB445F1FE7ACC5F4BF93815804762301EFD0CD4E7FF + 1F70BA96B5EBDA7327000B7322B05EDB477ECFBA5C53EBF5BADAD98F7296FA6F + 2FD8DFC3C1BA53ABADD03E87CF3B84EBD6D15C5F877D0FFDA7A0AED952CCBD01 + AA803BB0F7EBB34A8D757DB5C55CE575EDEB72A8A6401D628A28EF7D43F7C5B7 + 3586ED4361CD2934CEE79F8CA1305B05586F5F8A08B853541D8FB6205FF53EAF + FFCD11E986BCBB17822BDA7CD433B8BC7CD23D7CF85177EFDED32A0E4F739800 + CC71086DEA67FB54DC09C011FE0316D8AB00AC7E8FCF1728FA9E4CFC059C5FE4 + BB6392F41A843FA2C733FAA13EB500BCB05674A2906C83938FDB8CCD19AFCF00 + DB8772F297FF0A1027810244E08DD96DCB936B1FA327DEDA573FDB43A2392AC3 + DAED3AFD18AD18D87101135D44690EDA53E84590AF7F0A38279DDB5296E462AE + 87D605BAAF248A5B7A710AA27C255F18F762AFC78BBCECA70BF6D2AEFE73BB98 + 2F8F76C6857DBDBB551609002FF86F1A5D64E0D86A5B4254B3EE975BB489166B + D2E2CC848F0B3D7E114863D4EEFD6847B9BA00E5D1317D6E2B46E3721B600E76 + 4CEE4314EEE3F2E3A8ADFDA0B530172F04461CB23809A278058B9F11512CF1B5 + B58FDC2901980BC0CA607157168F75C1587D5CD46E2D6C03F5B56297C4100A05 + 8CD33663D8F736B601F797FB833AD15D679A432082521C65DB43B19477CCD246 + B27D28FA506CF23615A228D250B889604C74A7A08F89600CF25678AFD8DC2900 + 8324FEDA76039E8B786B735C3DF9CA7260FFA6DB7FFCDD6EFDE21B6BDBFC5FE0 + 7F00F7C22DEFF4052AE8C24E2201D8DB38578F1E373D7E3E865B8AB48CA570CB + 3E055CB4A33A5ED8553B6B7B01983153F463202F438194FF075785511FC376CF + 78EE11AD18B5F3BDA27E4563359E3086F6FCFECBFB4A811773A600AC7DB4B1FF + D8DE7FF5ABDD77FED21F77BFF61FFCA27BFECD6FA418BE567E4C0FFCFCBCA1AF + F3F7F98CCDF31C7E56D4AF316C731BC5734BD0F7684E2400D3E7517F2B261A17 + FBCEF72A7C4920B76D7A4C7C895F3DB2EF17DCED5BE2F1BFDBD778DC3384E02B + 3C72D8BE639F980D8F8BC777C804FCDEF5680CBF7F239BC25C6FD73C8F8F9D8A + F771FE771144716A638ED6C4EFD1AA2100334FA16F09CCD1F1E8E3EFFEAA8C0D + 689BE2180198E728440559CD69C11C5FA785D6F744F18AC6E2DCB025EE7ABB47 + CF3153FC110230E0B93560AC07712DBB9E7F473097E3116FD37AB4A1CDDAA714 + 807D5E54238AD16B3C8D05E99AC93DE9C9C7D4B8E4CBC2AE520560A1FA280AA7 + 6BD89E2A00E3FAD6C31823BA8E8ED62EBC5FFB6BAB09F47A5DEDEC47394BFDB7 + 17ECEF61446B502D9FE64DE1F30EE1BA7534DFC33B9489E6688DEB30582F0DFC + 374719D3ADEB720D59E755FB936BC0660FD6BA0FE6504D813AC41451DEFB86EE + 8B6F6B0CDB87C29A53689CCF3F9651BDB1F09AA8DADA8114E17654CF5163827C + FA9548039C427543E6F1EEDE2C005FD6FF057C71F1B8FBE4932FBB2FBFFC7EF7 + F4E9A7372A00730E5EF0BD138017A039470BC0403F046C7BE82B39271573EF78 + AB4C0AC05322EB84CFFF68D71FEBA97A1173F1DE2F7DCE61308F628F4F426E2B + 36EF22FCAABD3F1114F07F742900970B8881BFD8082F10B8F5B163745E11C378 + 5F53FB682B911DF38A2EA25AF1118C07CC5992B714CE49E7361513F9496B5E73 + 792D745FA76A47C7D8A3F1DE46F20571167B2354E455D205BBF44702B0BBD8E7 + 45FEDB40C7BD2E516D2E84705146176722D4EFE3B18D1683E8F3394AE483CD43 + 1FC689D07C258A25B9AE1DA3F478E83E16C2EFEE21FE07DBEBD182DCEEEA7502 + 8B7C4B170749B44039E5575A0BA7B09396CFDB01C7839F0BB33E963E5D14265C + 1006BA481CF969D305E5A8EFA15F17E9B5E6543E73405487680DD4D5BEF7E9B8 + 517D9F832D45DE290198F8188AA8B93F145F28D2D006D0A718A5020E63B5AD71 + F4F9785FC7A371183F093FE9FFFD42E4512CCE629210FBC2E25F7CDD9D17418E + FFEF77F3C2DA16B32AEDAD88BA1078CF3EC65DC2DF5641175B0AC21480ABD06B + 6D85F616885111B995A37E10DD9DCC38D83C7B88BA45C8563BEBA9F8AB02308F + B10AC2686B7F08EC19089CD84200A5084C0158D19C1EAD39465FFF08FA417ACF + A05DF6D5C36377089C37F68902AFEE0BDAF0512056119C79D967E3CB7BDACF5F + FBFCAC219676F52BACA9B55BBE8843E389E6F00F3BD0E61C395FCD21CC65FE12 + 588F3593986B63B206DEA78FBEF851026DD8F218E6B31C88C049A8463DF93E55 + A6BE773D8825E83356EDEA8F7CB44F11C5FBDF8388393F7F6BF87BC3B6D647BF + 8AAB92C77604EB44B9EC038DF3685C0B1F8BB61780FDB906819D3E7F5EC4BEDA + 18AFD0A7A2AC3F4FF3F57C6C84C6CF91C4DB86A8ABBE16352E1080B5DDA275FE + ACB08E87FE282FAA3767633BEAABDDFFA1ADBF8E994373A76AA81D6D0AC09108 + ACD74C53F4D75710767B81B70ABD2D01187D8AC022044FDD014C5A7702EB75F5 + D4B535195C4B97EB75EFD378B5AB4FC753FFED06FBBB0C3E02B93E06B9AE3DE9 + BA98AE4BF5F6081F7B2CC7D6D4BC39B8DF6847B5AE435D2B6DDD01CC473D2FBC + 43D8CF3D8AC9583D59A325BA86EB19AC27FB75609B9FD723E6FA21A2712C4235 + 921651DEFB86EE8B6F6B0CDBA7261AFB54B066220BAE212ACC1E00C4CCB09E90 + 343CB4837C8D215EFFF3DA608B351EB18CAD01E195FFEB97422CC460DCF5FBE6 + CDF7BA6FBFFD51F7EC999D072D12638F1780B5FE4D3DFE19DC09C0530C3E0485 + C85FFAF8325506B16F83688E772C477F3C4F44FD910E7C87E27FFC53ED204E4F + 0A5AB1A91F9E84BCAFE0C4AAA0FF4FD7C089F5C02F76A27D1F3746C78D887232 + 3AA6327591421F2F6C685F523782F1C7E41E0AC7E0387E1F344EDB3E3ECA3916 + AD790CACD1BA98CD17CB59C4052DD1D70BC3CD38F717DF9EF8C2BF8DC6DF3A82 + C518102DE4B0AFF63998075A0B4DDE8FF6927CC67A3FEEDE8DDA1B635D5801AB + 3F008F85C61D35BA3888C544DB52F8DD42FC2D7EBF30C8854742FB5C9C47F37C + 8ECFD7054FC66A8EFAD9A70D5B7F570EFD03641159E1422FDB58EC9D8AE1C237 + 17BFB54F1B73E8D7C57EF5D10F3806D13CC6B2AF36D6802DD731DB63F317F8F8 + 523CCA147DFEBF4AB4D3FFB39C600F91C3B6148C281AB5A0FF50F145411E4418 + E6B76A328E0222FD0A051DA03522E04F028E8D47D1378BD7685BFD1756E723DC + DDFB55B7323B84D3DD33B33FCF8F7CDE42847BF96D127EC10E8F85FEF8BB3507 + 5B1580537EB1C14F2196C00754608C6C53B0966FAB1FE3726C1F87B128F672EE + 40C55FFA68D33E6DACCB9A11F055207C1620AEA7E30BB113E22744D217DFA976 + 6EC95808463FA3EF15DAF8FAD3CF9888148B76307FC0F9F318AA9D361E870AE6 + 68FBA3E236855D6CB13FE7F65EBAFCE4FBA9CFBB8171D739EE46651E1E03CDCF + 0AE6EAF745F79D9F21C62A3C1E3C26DE46BBB729A88BB9703E2D343EF2719C48 + 00668CE6B3AF4CF980AFC37D5E3DFA3C09C0F43FFDEAA7DD0F7FF76F76BFFA97 + FFB4BBFFE9F79340CCFF05BE333FBE2F530D032230BF7FF5FB546DFA9D3C89FB + 7ED7EF7C6D473E6C99C736FB6A8FD058B5793FC6F1BF5DB455E1B4F439270536 + B533CEDBD54628CE46361D97F8BEA27973E07C82E71A1E7F6E33751EE37D843E + 9E734D9D77D14E34C7E363A788E6D0AAE1632AE6C31F19E2DF8F6CCABF2B3925 + 7A7EACE7C8C4C7B1AFE7F6FEFC5CF171FE693E514D8AB624BC5E9900392D3197 + EDA8366D91CFC7B01F5DE76520ECCE0BC04D2804A7B661D7B74DECBA58EF0806 + 148EC1C6A0281B09B3E8A79CD2F63EB5B1EF6D915FEDEF8253CF83B5781C614B + 6B2658D32BE458D81E66CADA941723530CD7AD2644CD51EC0461ED05685E04E3 + 28FC0EEC13733F8AA95A58433D602CCC31CDF9DC5EA7F3F1FE545053D668751D + 17AFE9C6D8D9EBBCC5DA185E63BCD61A3FC0FC784AE2AE5FDFF6FA0419AC8147 + 5C4753A026F1216A13BA4FBEAD316C1FCAFF9FBD3FEB952449B304B17BAFD9DD + 57DFC297EB7BACEEE1BB7BEC4B4666E59E959955959955D5D5B57577F55457B3 + A7313DEC597A9A982139D303102481990141800401BEF1957C20F84C3E10FC59 + C2EF7C2247F4E867A26A66D7AF7BB867F8C381887C9B88AA99A98A7CC7449531 + C7A076D1FF7951E3CF92ADCEAF15798512B443883E8BA2F8AF4E31A62E4E1C93 + A2C50F2A68A7042B495822EB37D3E6E6617AEBADEBE9D2A59B696FEF7C8FA01D + C6F2043049678DFF4209E01719FC34D1FA005B509FE7268081B11F57D0352FAC + 43BE2F0263637D83F968DE485F3CC66FE419BCE1CFC33C5F6DCF4C405E6BD884 + 0AC7E4C758A0B23041F6760027D82DB996EDFE15C5B7D10FFB20165D140C8D99 + 68F9B4C0FE4EE23B068D3784965F0BD19E0B1EB539091883F14E0A8D47E8C276 + 2902781D8BEA59F2576558C8EBE2BF2EEEB933B8A5B385BFC708E4B1DA3E2F96 + 8D196D5BC90D2466C61EF5D6C258822782891E2692144C00B1DD236B8B2E624C + CF18EB87C733892D257F670860F3CD30BB467290B288968D2622DD06C9BC5DEB + AB24F65A3EACABAE65AFA0AF22DAE8585807E14BD297758DA172DA699238C293 + BAA5F424B0D5997866F29936116AA7B6046D34A1AE368CA33E80FAD00698275B + 3B04AC7E647D15A26188008E808DFA6C80543BDB7FB4728BD82179C23A81B6DA + AB5C75909178518227FA52C636FD582799A964AA926DDA87FA0188C9B84AFEBA + CCCE0577FF220E88CD55C4872F09BC4B779CF8840C44E4D477CAF6C95D128294 + A14E921432858EDBC72EB611D4471F80B186E2AA5C63A04E1217C0B16D5E7EBF + 3906B58B809E3163DCA8AB6301F159408217E798E42FDA4AFCD296ED1EE4F85A + 50BD8F0D08DF0F92A90ED3C32E22C6852C1E23EBF471D8F1F0D88016098C72EB + D207AEE38E6090C200ED216F8D377EDFFBDFF38E18669D72B6A9676CEA86C03E + F03B8DD70CFE76D93F6D09B5D178D197ED18836DC5980E501D62E278B1739D63 + A7FF95BB9FA51FFDEEEFD20F7EF34FD3D91B0F6DAE60F7651B9FFFB671FDC4B5 + 93E3355FFD934DBC0653C6F622E0FDC5EF1FD2C635BF558FE03D8268E95406F0 + FE42A89C3EA8F39E156D28531D4AD6B55FDAC538D14675B0C5FD9A7240895CDA + A91E601F80DA47A80FB1667309CE335A7398387F21A21DC0B9514B37E437C15C + 268CA185A179D7980FC1D86A3B3697A32EA2DAD89C74B5CC6D75DEAB6DD655A6 + 18D247F910388FA61FE6F5F4D739B6D601B58F04B0826B0592AC916C55441BB5 + 437D8C005650DED245A80D7DB8168B88C42F3143F4060CD929A1AB44AFC3D6C4 + F348606F170CFA483BAEB3D1A60C6BED28D3BA82F21789D8E7185AFEC03C3DA0 + 7108CF9994DC96DAC6DC14C9C788456C00B51BC398ED3C5D44D4834C9D6CE2F3 + C7B1E338C5E63449E02130B7BA605F3EBE8D0EAB36E6B5C66753096012BF247F + 0B40FCAE1B4004A3CD7CEE30FAE42E7989B5F5838ACA538CE10DA7D086F22DB1 + AE36AC2F0BC61C83DA45FF39E0F761EE77A041B446DE3012B583A0DDB2305F90 + BF910006741CCF8B161F4A627663E3C081C74263973064D1B60F8BD5247E897E + 5FE887A00CE321D4F6B4E004F08BEEE434D0227B5B509F532180C770C21FDEA9 + A375217815C6F5BA21DC40953475E88D39DC9C676C1503B6116A338A12AF1563 + 1ED46F6602F2ADA23FD19B0FD8D30713454CA88ACC09603B4F62DBD3172C2353 + 747187D0F757C01F1376C6E2043EC76DDBAAFDF3E2B4E31171EC8A96CD900F6D + 151C7344CBF6A468C58F58D4272F743301ABA46E84DB80002E586DD8B717F5DD + A29F756F833CDDA65DEEDF81BAF813EA0B689221261816452BEE5228C99745D0 + 4AE2503E94E05119EA43ED98348AFA21441FCA666259BB237A05163F93C099A4 + 45D24D1378D889B15A76056B528F5019EA9A6454BB889EAD81C95460A5BC5B4F + 1175AC534EB492B194B792B240B4575BAD0F818962F56D2590A957A85E6D54C6 + 584C68F7ED00EB87C9FE3340471C60272E0039890312B99968C01841560492F6 + 0C088E619205A592349081C020F9E28486F9BBDDD96CA3C40889134265A813B0 + 878C0489EA805E7F0D3D7D8121B9EBCEDDCAE32C84AB927B4384A3B6F198E7EE + 91CF40391683DB147FC4727B1066E73389E7BB7D2FA29EDB248CD927EB00C785 + 3AC9508F2FA02D314F4F1BC68EF680DAA164BF712CB4635B1177002B68833AEC + 00D4D9073F17C6A19CE341E9F50B16E7AD4CF692DC459D20B9EBBBAE1BA08FA3 + C46CA1D76719BF8FFB621E37C74C3D411F007A42E5006D79DC2D7F1D6B3D06AB + 83D425019CC95D8B57E4533B374EFE1A60EFE70276A54F7E061C3FFB47C9DF47 + D7BFC9F1C873B3DFBAF241FECCCAEF8904B0FE26518FBF3BB5459DF6BC4E28A0 + 6BD9461B6248C63A63B4AE498BC457E8758A76F4C1A39FDF7EFA83F4F6B33F48 + 7B97EFD8FDD8EE493C97763D9EC2DEFCE08B38BDEB640163A1CEEB780BF9A90B + F9FC214E24A333ECDAEFD77BBB8F34EABCDFB4E0F723E94FC17B0EEABC1F2999 + 9D6D0CE13EC6FB5CBE97817CEDEE7B2C87D0F9CE22DAC47900EFA184CE21A2BF + EA68BB52E61F1A8F76B4A54E6D38DF61ACE84744396D57AC5FCC9F5A9894B997 + D675CEA57330400957FAB95CFEB447FD984F84DA132DBDF6D14620800D98CBA2 + A4AC85381756B9CEB96917A1BAE8A73294314E4B16D7073AB7AFF5C67A284209 + DE48F2026AABF2085D93CDB3D73556AC2BB21C6547FC0E11BB0EDDED3B205332 + 770833442E40D922503F435D575B9D40BB45005316EDC7F20BCB22E62BD89FF6 + 1D41DFD89E07ED4BFB9C87AE1FD89F0E98DF6AE914FD5CD82C16B1831EE46E25 + 780BD98AFABA7D7F3777DF724CF19D9C136B59CC8D57F2B0433954F847F40860 + CAA26FDC015CFA2040006307F0AADA3472D1B56DF1D6D633E1DB22F994005444 + BB375CC20022DFA275B561FDB4D1EAFB7930186791DDAEDBE63B82CAC32D81E0 + DB238045DF1ACB2288BE245EC1832A194BA2971C2977E9AA6F1BCB11C0437891 + BCEC1B02F8A4E08FE5347E78CF031D4744CBFE0D86213751BDF156B9DD58570B + F4C6ABB63DFB06A22DD1B29D87569C453133F9782D8149540626BDDA3E4D7413 + EB8189DB0C6663105DACFE6260C84E6D5E75C4F18FB5C7A031153C176318F289 + F208EFDBECEAC213F5D8860DDB5A2FB61E877E5BB6C82E3B7495088EF5A11DC0 + 8AB8B027B8F01F4A0044A84DF445C281098C7909881709256F5B6082668CE06D + E9296F416D588F0923A092B300DA0D546277119B26A03320E167D0441D937443 + B208E891746C251E5B6DB58D895024398198FCA40DF544B4E9255C035417FD54 + 3F06268723A08B717A3AC38AE90824DE0195A99C40621E3B4BB9C3B4F381BE9F + E0F7B681E4048805121A4E4C9C313B03EC5691A42F7E4A5A8CC95AE482F643D0 + 3682F60A8E8F7D28A29E3E04FBA33EDA50AF3624B6487CA1449B32925E28A127 + 2AF955FC485CFAEECE12BFD527081EC645493FC6C2FB71B12313445D26296D2C + 785F70E857DB2841BA91B453B90232F61B751C03F4D10675B6D907DAACD3076D + 273FCB58144372C64259C94B83EF64C50ED54B1F783B939BDD6E5ED541A6E4A7 + 8F0DE7B1909BADF8243CEBAED9428646D29870521E31025AC7A1C707998F47DA + 7A8E8148AA52471FDA23B69E471F838D2B8E358FB71C837F87AC8F72DE78DC3D + 1B01769D6B7C8E477F27F83EC6DFCDF6F19D74F69D27E9C2FB1FA5DD6B1FF674 + F8EEFB6FADFCE6F9BBD4DF063144ECF2F7C43AED681BE3AAAFCA5B71298BD7A5 + 68C33665B4611DD0188CAB76EE0F193E8F72EDA5AE353E0563112D3D63681C8E + 49FB02D8561DAFE7A813310E095D92BA2476B5AEE474B4870E4F86503DEF91DD + 7DB27F6FD5FB679447DF588FB6ADB90440B9EAE8A3FA159B6750A73E40B455D0 + 07A5DA50AE505D0B551FE654C4A4CCA908B57142B510BB94510F1963021D019B + 417BDA72AE08A89FDA4544BBD8C730F2FC15F35885CB7A73D90C95555BDACB7C + 9FF36C9D9B4779B4419D73F54CF4A2ECCBD7F6D0CE4FFFA11F10D719D956206B + A165D05AD7CC435CA7A96CC82EEADBBA8EF41D247E97C00CC95B304AFE2AA8A3 + 5F6857BDD8B7D6F094A90E046C2480995320340640BB65A1F1D8DF228871C6A0 + E3D6FE1607EC4F0F9EDB12229665D52D88457C684312D8E5D8D16A2548DF2DFB + 3D03D805ECBA32AE57051CBF1F27C6BF8952DA28D5BE91A38DC0CEDF49295BF9 + E01E3C376D9874446F97E7CEDC8092BE8A6AB70406FD1B9CC4A08DCA16C4C2E3 + 6DC51FEA73593930143FCA296BE94E8AA1382DF9BC7E077DFADC9AF36B85808D + 68EA2B0F07B4E2877EADBDBADA902BA85BB398D26FB3FF9931B4B12AC42FC058 + 5A673BCA66713A04F08BC41B0278099CE685EBD4C031E858B4FE06198B9C8FD6 + 8D54A1C42F61F278A3AEB6EA2B68DA9F0031CE32D0C9C7EB0B4CA2E66119DB36 + E284BA3F8616666300FDC9799F9C1CB23DC9C2E1DB423C0645CB7E59F05C8C61 + C887F5A8A77C66F1D96AAB3C8276D51E8BE14CEC92DC8D44AFB74F9100EE2501 + 483ECB23A09F2741F1B280A44C4CCC3C0F98CC69E95AE825800298C46AC27C23 + E9ABC9B108B5EB7C519A6EB74BB60D25EDA2BC05D8C5C423651A037585EF306E + 2439990865396443449B1634D98A76CB576D5AC9E616986C6ED9AB9C04B012BB + DC81A55039ECD9CE8F17CE703D08A4337D4280497B944CFA6387ABB65B50FF31 + 9010601FC0503CB519B38D88F64A6E50CF7A8BFC507D0B24B294B0424940AE64 + D75C94580AC811576524FB9C20933EB9FB777A1EC45FEE1FA05D0474F0235908 + 598D156C5546DBE8C3FED4963ACA58A7AF8E91042549C445003FAF17F255C95F + 80722012B7242EE9839236D30BEF5BD92739A71710ABF3635CC603D0CE7A94A5 + 5E8E7D0C6AC373C4F3C4E3D4F34B19BE0B33BB6A831F4A3DAFECC76DCA98BB71 + CF4289DF5807D49FB1018C43C7AC722D0F6E3E4897EE7E9A2EDEF924ED5CBDEB + C702B98F557E9380FE4E5BA00DCAF81B1EFBBD47D0577D54C76B1263A8ADC68D + 7AFA693C42AF87F4639C31DBA8635F8A211BB6B53FF6A9FEB12FD511F10F3D40 + B407694B4257496005C9DD1EC91B74A8D718767F6BDD2BF59E19659447C4FBF7 + 90ADDEEB878039C14AF96399CE0FC6FA07D49F75803AC6A39EF1189336F453A8 + 2FE752113373AB868D426DC6FCA24CE78AD1A7E51FD191BBF3D19ACF02BD39ED + 00AA2D6073DED69CBB3527D77A267A95F45D9E00869CF53128B1BB2C5AEB9B21 + D4759B21CA86EC5AFA6873AA04F0A6AD9F6DADDB2280EB1A58EB436D91CFC4A2 + 9DD8E81A5ED7EC1143BB7F1563BA65A0790DF6795AB15B38591E05F6A707CD71 + 8DE9E661111F8D5D0960219FF90868F579D5C0F1C79DBF2E6BD937F2B411207F + 4F8B0056CCD82C09FACFC420174114DBA68DCA16C4C2636EC51FEA7359393014 + 3FCA296BE94E8AA1382DF9BC7E077D0A89BA002AFFA6722159DBF143BF94B574 + 6AE3A5C50C7DCFF40FF4C6D0C66A88F17C78CD08E096C1AB8248F40E417D5EF8 + 23A081A12FE7CB42FD1158D9AABF41C6DCF36137AF72931DBEA14A5DDA7A83AE + FE23043030637F02688C65D19A80BC3EC0046A082D7BA0653B1FAD09753BBE62 + 360ED08FD52D1C501FB37F910B8BD3C6D8713C2F78BE8630F408A6B147331175 + F1A9F5D82E75FEDB99A836B473409E89DD884AECA27D4A04B0261DE6D90EE124 + 3ECB626E62A4918489C8499DE1040EF5112DDB882E69D44F2EB93F12488699C4 + 96F975246E6737869E9FC62A8F7E8E60526FEDE09AE1BAC5B8667224F6D006BA + 5D229AD8EBFC3A4C060860B657F62DC640D273089A7465E234CA5536A4674255 + F59A90D552E58A1897328D57E58648F2F608DDA0631B20710CDBF50BD80D8704 + 7D97C48F60021F20E90068629F2575EA3F0F8CA388368C49FD1851A17ADAB77C + 296FE9D49768D982C08AC496925D6C930456322C62C84E6347BBA8631BD01880 + 937D02DAA84EFD5B804D246721671F80DA0294EB5874EC1C27EA2429631FF3E0 + 3E65C72E08CD2102D809CA620390C8843D09E02C43D947267FD1474627871FFA + C9C87AB5B57A382704E500CF8D02729E0FB5517F9CBBF87D2068A7E75463BB5E + CE0FCF1141B237966A3FE357E2031C1BC03EB56FD4A1437BEFFA3D277FE10342 + BB8E5976EA02BA7B57E511FADBADBFD786AC65DF6A473B5E0FA21CF5D6D8684F + D05E419DC600A8D7786A1FFD5BD760DA4468BC88186308B45DE4FAAFC4AD4249 + 5E2012BF2D545FDCCF047A8FD5B205F52378DF8DF764F58BFA180F3ACC09382F + 19B26B8171D987F643799C6FA88E7A82B208CE9D2238B78A73AC4510FD589F94 + 3FF5457BB5A1CF904CE79671CE381F24813BBF18AFA23CC946656E0F3F934F76 + AFA4C9DE7181CD73ADBDBA63B105518676E7D3F9E6F9FA10016C6B0FAB638ECF + F93C11D700A709FEA1D5FFD4DA5AEF08E2BA6B681D46794B3F2B47794A04B061 + 15F03F3787F56E6FED2B32454336B87E963AD6E75CC3230F00A84C417DC42236 + 8BE079FD5F2E8E16463B8FD58090B0407D4473B41BC1BC3EA98F98B12D6339C9 + 185E16E231102D5BA295AF8D68E5837B5892005E180D5FC66F3E6A9A5C0451EC + 9B362A3B6DB4E20FF5B9AC1C188A1FE594B57427C5509C967C5EBF433E42923A + 860855255C153DBB46FCA17E877484EBE6F44DF4C6D00676002BDFA87CE2F278 + 8D08E096F2558292BC63509F3E016C98F7655A16A71DEFB4F0AA8EEB1543FF46 + 34400093C82D65EBE64BBF51FF1780D8EF32684D3C5E0F60F23484963DD1B29F + 8FD684BA1D3F23EBC762B471129F57152FE238B8F0D285DC906C08BA608C8BC7 + BAE0D4FA58DB1049605DCCAEADDB827B6A6D10BC810CAEC42EDA8B12C0B29357 + 41E2772EB1BA0062EC96CDB2D0F1295AB68E46128568256B5AC99C281B422BD6 + 18014C72B74FD80A10C74A2579D59E7295CDEABAA49A26D650CF89BFEB090430 + C85F12C013246E5DDEF90D2126025B3829013C548F09D508D5B7FAD51827C150 + 2CB4F3639BAD5FB4CFDE7090FC25E11BF5941368E351D04A00C7047E2404E625 + F8E7E923627C22DA794C104542802871411BF60D59D4D3863A94046DC7106D9D + 9C11720D2416C92B25BC2857403704D86BDD493A036341CE382A8BF62A533941 + 3975AD3A1163B76CA29C6DF862AC2429E3B8B4CD734930C6422844264848D449 + 022BC149F2123280A4A5CAB26D47F02AD1AB50B912C07D22B84F00033A669547 + 3DEBF3CE05ECC6CE29E36C5EB6F361A0ACC28FB703CF074ADDDD8BBA12C03857 + 4A9AE331D1FE38EDD22F4A7EEEFC3D50C6EF2E8E0B3292C018A7EA5D264428B0 + E18F39CF40BBF5DB1C9251CE6B04F51A476DC7A0FE2AD3B6CA1701AE717E2EEC + 9CF0F8E231F6ECCB752DCAF45AD8B2191A9BDA2B54CFD8F3FAA07DF46912B8A5 + CDDDBD7187AFDAA85D95D9FD0C20B98A7703479295BA79509FD6FD97B1A3AE15 + 037ACC0DA29E72C62068477FC6509DEB4B0CD5A144CC8868D38390AC8A49216B + 01CA30C7529B28F7395899F3B56C351E6DA26D94139C4F46B46C97C150BC9E5C + C960217541E8124AF4520782973AB6A7FB571D4A00777376CCE5F5CF9CB16DF3 + 6D99F3EB1AE079C1F88CBB0C014CB4D66171EDA43AC5AC1CF5D323807B286BDD + E7C10C014C94F536761C63AD1ED7EB684FB7CEF7E4ABB06F40FDE6D9FE7EE168 + 61B4725943A00F1FCF1CFDE7C51CD3118C11D1B2C34EE0579904562C927B6DE5 + 6955E6F2463EB887532080E9D7F30FDC82EA4F93006EDA40BE2A8F06569FE03F + 83967EC867593930143FCA296BE94E8AA1382DF9BC7E877C94475B04A3046C88 + CF3E86FA1ED2511FFB1CEA7F0144BE51F9C4E5F19A10C02DC5AB062579C7A03E + 7309E0A12FD52260ACE789F1A2F02A8EE915036F5ADD8DC6EAF1E68A9B682070 + D586F508B59F4123E649D1EA7B51E884E3F502267943B06393C9621F3A31B6C5 + 45D37FD6AE8F1C2B8E8932ED6BB178BF9F183A9FAD85580BEAF3BC68C58FC022 + B22E3A592A866486A1052CC8DF4A00178217F555217AABBC811902B8012EF2E7 + 92AA0BA215FBA4D084C5104EEA370F7897B1BFCF5892316388C99B48FC1291B4 + 8D3B7E59EFD9083A929736251966D0641AA0893CEA73E2EFBAC96709E0E9D10D + 2B67937F1A63A82F80FD557F4978B6407D4C8E02AD042964CDC4E909C0580A26 + 740126963DC16B3AF58B72258087104962DD214C90009E8274018123897A4FD6 + 3792F92A1BD2B18D182D3941794BAF329248248722A023F14A3B259B285F456C + 6B2B01429245C996211D516D0A8945820EB15B04B0925F2A8F400CC46AC909B4 + 630CC6A73FEDD4A7D5D6B1AB3CDAB23FCA5417E5D19EE73F1EBBFA28E298D88E + F21954F236D741FE6E5DBE53494AC8415492C0D4B6CAB26D47F4124A0647F277 + E322FAB9E3F54508606DAB5C118F9704AECA5AE784DF05C664095FBC6B9731DC + EF2DD397C754C7F341E07CE01CAA9EE789808C0431FA436C941C8B7EEEA8E3BB + 00602C5B57ECFC95F1A0ADC7E07E8DDFE36910C0BCC640A671D4F6A460DF1AAF + 25A37C0AE0BCC8B9415BCF979F0B1B27DB43C06FCDAF41E51ACE63E5F18E416D + 15D4235EEB31CF6AA7C74A39C7022891AB50F297E42E88DF559B37B448E25EDB + 8E97F748BD572AA25EEFB7511E09E4586FB523A807624CCC3D56CAA3A1799F8F + F77ADA465D1C1B75B453FB8868C3B9D3A490B343D079D6105A733396112DDB28 + D7B99F426D16452B0E10ED388E96AD63B72379152BDB362F36B0AD0430EB9100 + CEBA3C679F1E60DE6EEB8811025889599DFBEB1A80A07E19A8EF6913C06AC3BA + 427D72DBD6932DF2F6348147F136D6C08BA0AE9F654D5D61B2E72180D53E22DA + BE8A78FE311F2D8C98D352441BB64F4A002F02C688707D78DF6FCB6E08EAF7A2 + 10FB69E55F09B58BF62D1DB00AAC59BD9113EE23E7B6FB796EC382FC80E6C8AB + 7FC397FA17BA03B8E8D7D6CC7FB5D8ABCF82FE736527910343F1A39CB296EEA4 + 188AD392CFEB77C84779B4451109D6AA0BF1D947EC9B3285EA2B202FFD0DF5BD + 209C3704275AC00DB24350BE7116A66F12BFC4B7CFBDAEB484AF225A646F0BEA + D37C04B47E89E297EBA4A85FC43778153173C32932A2CA9B37D13EF4E65D6FD4 + 41EEB6A744F28E41FB5C16AD89C5EB014CAE8611277C1DBA0935EA6DDF38818E + C8B1E298FAFD2C12E7F71BCB9DD30E275BE48C0331E3E231027A5D74FAC2B380 + ED9EDE4A2C5C15554F9B7594908F10BB4117B12801FC2A4209D921B4FC48DE3A + 81DBF02186F4EAEF3692CC2122B91B1337D0471B2012BA8412BB95DC45F247ED + 623B3CEA795E022DCB9040BCEE00F14B7404704E30B6E2A03ED6574C5092D865 + 6255DB2D3993A25AC63ADB44ABAD09D9A857A80D93BC4C0E2BA8A39DCA216B91 + BD2C7B753B46006D12C04A028300CE40B23E27F87BC9FA92C8D7647E6C2B8674 + 633E0AF48992F6EE53480D928811D40DD9445D8B5422A1445209FDB6746AE344 + 4721699C4C2BC455ED23905DA813D0C13E424930D8A82FDAF4537FD419973138 + 1ED6E91B6D358622EA5AB1A30DFD68CF36EA3C06DAC7F1694CB635A6DA11B4AF + FA42F492B004F9BB7DE56E2581496062972AEAB40769091977B7767174476F26 + 7B87C8DFCD4B771D4A020F11C043E81D8B807A12B894C1163220DAF35CB30EC0 + 0E8F580648BA4EEDBB4C52B7059E2790E93C873C4F6A93CF8FF50794B1B15F82 + 7294443C5EB4F99DA78D5E0BF89B54F8EFB0E814EA43A8BDEA296FE9588FD725 + DA2B3826DAD307BA211F8F873A8ED5CE138F5F816B17CFE1900D50AF75E55ACA + EB69BCAEC6B1D5718C00F64AFE6A4C9468AB2D65AC134ADE8E21DA91F825B833 + D8DB272480F55ECB769413318EC65679B4D55890611EB052C85FDAC53902C7A0 + 3ADA12EAD3D2EB3800DA71EE0370DE3484C982E42FA073B3FEFCAFAF6FD9B764 + 8CA118B31F432B16A17118B765E7C04EE01D9B0717C2B70510C0247C95FC5590 + 18CE64F1F311C000E7FE6ABB2CA2DFB2043031B4CE1B5BFF45DDA20470DE218C + BAAD5B1B7A60915DC4C3EB61594317F46CA98F76D6E61A1D25D6E4BA6E679D3A + 42E5116AF7AA417326CF3F6EC45A0C31A73506D893FC2501CC18ACABEC248871 + 0663810C36B4EC5B68C63865F4FAC1D834EFDAC8DBAAEFA2A8BE2127DC47CE6D + CFE6B94F9FB368E6D38106573268A3B208D5CFB36DA1E5331467593930143FCA + 296BE94E8AA1382DF9BC7E877C2A8FB6041A046BD685F8441C1BDB11EAE3802C + F40D34FB1E07DF01DCE21397C71B02F8D4A024EF18D467700730BF44DA7E1ECC + 7C21DFE055076E4233FF585A7077AEDEBCF5263CFF867CBA88E35806AD49C5EB + 014CAE8611277C1DF2A49813E9B6AF4E9E5BC8B1E298FAFD2C12A71BC7EF2B16 + 3B9F2F1EBA801A436F012AFE6CF716A5062E5E6716BB04C8DF3904B022DAA92D + 17F5F3A0898016C6EC5A646ACB6E51B4E20D41FD2A71DBB09B07257E6B9C92D8 + 517489A10C4DDC680288A05D8BC88DE46F258015C127639C002654AFEFFF0548 + 06AF1DDC48D819CC24A3FA3311A7503D6DE05B939448684A4254939D63758009 + 524D94B2DD92C57684EA69A375804961267B235A7ACAF41DC0247B87EA6C83F0 + 6D91C0193951CFC43EC1843E40594CE82F82480C4430AE93B4566ABF4A6C44C2 + 23EAA20D756AE3444A213C5082B06991BB2D5DB4613F24B3485A699B36E89798 + 9A0E7A820418C93CFAF67C425CFAAA1C50328D75E83516407B8031088DA172C6 + 64D9B2416C1C2F6DD84609BDFAD1468F9B7AC6529942FD1D85A0CCE4ED7B8304 + B0EF52B53A895EC84800AB9CE42E495D258049F2D2A6450013630430C6AF7585 + 1E27F4A877C46DF6519BE8C312E710401B04327701C3DF7542E4122AC3B92001 + CCF385734522983655573E6B7EEF09C8D8278F89BF498C07808E3BE8798CBC1E + E86F520199EA1551AE3E2D3D65DA27DB04DAD14FC703680C85FA10AE43BD9C1B + 9E2BD6798E783E5447F0FA46DB358BA96326380E6DCF8C4564B46589EB340960 + B5A38E71B5CE98C4BAFDD694D81D42247C01F8B2CEDDC16E5B0860BD47F23E1B + EFB70AB55799CA631B88B1C7A03E3A27D0B900EA9C8FA0CD3E877C50EAFC85F2 + B1B1A96D85CCAFC630D179D600746ED69FFF653D65A82396DA8FC5508CD98FA1 + 150B501BC6045AB60E2714E749B00000FFF449444154800D5B36370606086012 + BFFDC73DF709E06EB7F0720430E7FC2C8138EFE7DA406DE6417D0827811758A3 + 295AEB35CA866245DDC204B0D9ADEE98ED8EAD5DB70D0D1B601E09DC5C17CF5B + 43D3464B81AED3B126D7B6023AA2A527D4EE5583E64D9E7FEC88B518624E6B0C + B08FE42F63689BB29320C6198BE53B621BF6112DDF17819A5F05395DF2B413C3 + 1A73AF92BBF53A76F3C2AF31CEA1B1ABFF30726E7B86983D216FC138BD58F374 + CA93943E076D543606B16D8DA58956FCA13E97950343F1A39CB296EEA4188AD3 + 92CFEB77C84779B4451109D88A3286D85794A99DA2EAED3B3EC1E7DFE81B88FD + B66C02F00EE0C8239E1C6F08E05343247A87A03E1D018C2F8095F8F2AC96327E + 99E661C87E99186FF0CA406F580E5C4822011CC9E0D28E37DFD68D78A11B74EC + EF04603FCB224E285E1F6032340C4E9866D19F1CB77DFB36407FD29D63C531F5 + FB69C78960DCA17E158BD87C171017408B2C8A68038CED04AE0B4EF301E8CF76 + D4F764AAABED8EC82599ABA46E44B4EFD96EF517F743E0A2BF8579762D42B565 + 370FAD38F3A0FE4D1277C487ED961FD04AD0B4923B40B489C9A048E22E44FE16 + CCDA8C13C0B38939B4AF3B48FE62E7EF18018C3A138E118C3F23872FE421D9D9 + 227A9124D5760B513FD43E09188389E121D02EDAA24D82770C207651920066A9 + 04306DF10E604DE433C14F50C6C43E13FF0ADA32F14F689CA88F6D273044E77A + 213AE681E4488B240148A2280904A01D09DE08B525D007C68C92641BFB88049C + C2C750483094B06D91BFD457406F503DEB13D46D4C9B1741D8C14EFC0CDA3741 + 9DCA008C8363A13E8E47F571DC00E52A6BC5463D12C068B7F4119013202895B8 + 8C8F80561DC1B6CA61CBDDBB2D02987210BE902B014C1298B28D8BB0C9642B8F + 87C7C43A74AAE7F9D273D5B2896D943C278CDFF59377105307FBB50BF84DD8E7 + 528E3D1F773E179110D77729434FA81F75FC9DF133E76F4FDB28312ED6416C63 + 6C94013C465E0B5ABF3F8072D5B1AD7EAAA70DAF3184CAD43EDA4439647AED50 + 39EB6C47B84E8E17E70325CE851F7F391F94D386804EC95F9733EE00F41ADB1A + 13013DAFF9F4E37D40ED2867DC08F60B900056627711C08F409B3B802747B7D2 + 861D7B246AF5FEC9FBAEDE4787C038C0C6798B8DD724880C805D8CA7FD2954CF + 71680CC8749ED2F251A80FDBD167081AC751E653F330E13CCBEA9CA3C5B91A65 + 0AC808CE19E9D3B253D93C2C62DF9AA3721C84EA388E211B6F83BC15B297846F + 9FD4ED76FE5217D1D92A016C6B097F1730E7F49075F37B9DF3B7E6FEDA6EAD0F + C6A0B6F405E6ADD12286D66B2DB9AE89144D02188F6C268AAC2380ADEDC86D12 + BEA3C4AFC6B3B5EFA22430F53D0238D60DBA4EC79A5CDBBAA6878E501B85DAD0 + 2ECABE4D68AE07631B3ABEC580588B21E6B4C600FB1601DC42CB7F112C12033A + 8C41DF01DC82EA5A714E1B9E5F457EB610C0207E95005E2BB25E1E77CD74C843 + AE97181C3B3EC74DFC1E4C5700BB9AC79518B3D8EFE5BA6BFE7B49EEA39727D7 + 38014D3DFB923E076D54467994058C8DA78765E22F2B0786E24739652DDD4931 + 14A7259FD7EF904F204AE72292AF3D9431C4BEA22CDACDE8ED3B7EEA04709F43 + 7C3EBC21804F8416991B75112D5B7C98FEC1AED907DBD21BD6EC0BB5EA5F2AA2 + F1E51B03EDDFA089E64D6151E006566E62A7098E29021794D555AB1B70935E05 + CA8D566FB6A8AFF94DD860376EDCDC57FC9D0C1D688F58F4EB81E46FD437ECB5 + EF283F09E284E5F5022671C3189EF842DE47DB7708315E44CB6731CCEB7BFED8 + BE7D7081F23217545C18112B3651EE16269DCDEC22AA3B9F94577DB5E9A08B51 + 6F97CF03F6556E7DF84296752177E7A192BE9B0173120011D14EE1368CBB0D5B + 2CE8515E2C242A920817D2DAEE8534C13FE691B468C4990712B28A1E212BF29A + 1891C7A32169A2F6006C18A7FA4A7FAE87DFFEE5EA433B2570C7401BDAB7B056 + 304BE6F6A1FA16B21DEAC756CE26CF0026D0BA249AD9BA0EB2EBBE63C7DFE957 + 71CDDAA637A82F1284D3A3EB3360A2914947C2FD4A329489D098EC6412342656 + 698FA42C6D5AB62C1741F4575FB6A9571B62348E01A42D085C12B9BAEB97F248 + F4AED8F95B3D83DDBE4CC82339DF91BD9AC867329F49FB28836D244C4906D007 + B61A4331A4A33CEA189B6448253A0A014290202149126D7D9CA623B9C2B8F419 + 3A2640C78358DA07E36B7F9978CB607CDA0168531F893BDAAB9E68E9D937FBDD + 80CDC5DC1FED69CB7A941194B12FEAF5D8D4B63536D8A04D52AFB337BB8BDD71 + D29FC7CE3A516DCC8FB6D12EB7DFB7E37D2FADDAE7E4A4263E3F93E9A38B414E + 82ACCC646E476492C42491E9842D76ED5ADDC75A085DDFD17BC1E21936DFEA64 + 247B2309DCD56193C71C8FA11E5F69FB23992FD8F71F8F65B7F305999F0793FB + 98412E1A40E8D217E79A9F09CF09CF0B638360E5EE61C8F819792C9C2B397E27 + 71CB0E5FB4790E29E779CCE72603B24C1A97DF20FA3539FCBD8DFE0AFC33C271 + 1439C604F0FB0519C7E86DB3D7DF22EAFC7D12946B1BC8BFD75CD2A68EC78E9F + 7D127E4EE02BB1E2B54065BC1E40CEEB86DAB3A41D01B9FB9B8EE42D3F43829F + 2FC74A3D4AD561DC247FF53838DEA171C46BADDAD396D77DEAE9879231A33CDA + 6A1F1542E60E0124AF12C5906DD867497DD4AD171238DE53F55EAA7AB729F747 + BF674A493B9404C96040E30FE20836B3E3615C9418D78ACD7D8016A18B92E3A7 + 4E63B530CFB6172FCCA3E601F3ACDE9C8F7FAEB39236AAE77C0EE09C1175B5F3 + 98E6EF105F274F6DDEA8B21618B7855592AD8855C651FD00ECE835CCC42B72C6 + 89BE8C4BCC237B153DC2D8E0F16D6E9D77FCE6B54487BC4600B8EE20FA761DA2 + 8D962A077C7D50EAAA572CB24ED3F54DCB9E32D5A94F1FB011A2D6E0EB3E9405 + 3D39495E27823BF474247AE91BDB2A93F5718B00064814930C76D0B6D8F7D6E7 + 26D3366544D411AA573BCA895EDF9217525BEA1923A2F5686AAD8F41733D1A83 + BECBE58260DB07F36843F245107DE7A115631E5ABE682B998BFAC6CE85B465BF + ED29BE7762AB88715E38CA63A96B9EB6602C6FBB0294E39A01485F806D7C0782 + BFC7903E1CC825E311D49AE306C7B1B667407D36171E73E58A456C886AD7E049 + 66627C5778149E8BC6396961D173BD309EBB6F214B17215449BC2A7A3616B38E + 89F632B6AA13D4B1608C19FD9882D837D0B22BF0DDBF052DAE7079BC21804F84 + 316257758A6847F083F7FAC80B9CF397A3FF057B8325203F4E5C3C889ECD5230 + DF78439B07255587003BB939037A03F57F671940EA027A83C5BFB6A89BDA4DD8 + 6DD672E964700F8D1BB2A1C6937ACF46C7D9B061BB85E664C4B088CDEB034CE6 + 663136F1553FB553FFA89B453FE62C667D62FC8896CFCB426B3C112DBF21B416 + 2B43362DDDB2602C90BE94F9BF23EB4439D8F5FAB5E3837D6D77B6D3ADF3BE80 + E3220EF2B830551F97C10EF6E64BDB5510AD2475CB6EDEB878AF18D32D002EF8 + 637B06D0B9BE2CE8B1B8DF35B913B0685F4893FDB7D2FA11FE497FC9DA173BDF + 05B056762ACFC84B824293145EDFCD09144D9290C005A67B360E10A5B0437BFF + 8AB7E13BD9319B986CC163DE8A9FC730FDF4E04A4DCE689286F0D8410690F455 + 782CC39AA0237487A1362481D7F630AEAB8EB592F4F2C45781B74D8747DF21B9 + 55773914F9DA5EC6C4304589C7432389A849C08235241499A82CC94BC8571AC9 + 52D45BB21654873A93A448CAD297724DA06A3BFA2B684F54DB23D339AC1F4F5E + 0339C9CDDD4C1D0A616BF64EBC02FE98E69C9027985CCFC4AECAFBF6FAD84E60 + 2871CF78115DDC6EA717490CDF710A82EBAD779BF1352EDBB4691113F4A55CED + 94E46821922804E4AEB778B4CB3B204B7C2154A68861FD792CF45BE0BBEA88D2 + 0FCA586FE9D8663D027D8304CB0420FA32F9051B0FFA06A97431939920DEA0CF + BA6C9BC797DB245833A187E331197C3C9EF5E3F23ED81F89C0FC5E583B275667 + ACBC53D4C68F7EDC8EC8F628B5EF3A964B992074BD02C70B3F8CBD1C3FCF81C2 + CF0F62990F4854109B24339D18F5CFCC603A60CDEC26D6DF147DDAF9DABC7CC7 + 6CEF3819BC5E48DDC97912BC99A0E498FCD8BCFD81D9C02E9FAF8EFCB5632904 + F0BAC7C071E133E948E0ED2BF7D2D6E57B56FFB0CA00C6C777408F538F9B751C + B37E77B0E31BE770C389E93C462794719C562759EBE32D72E8737F38BF728C38 + 5768977E3AFBFE67C47E725F5987F8EC07F268EBE3C077C4E2AEE173C37198AD + 7FBF703C685BDF6B06946C6F5FB57375F903F7C7BB6BF12E729E279E8BF50BD6 + 5F0508581BB7608A630F36F91A8232D7F95DCBC76DB14B7CEFA3809F01FF70E0 + 7AF8F1B3B0F1E6B2C4725D27A72C5F27ECDA5264FC3CF36FA8D81579BD0E898C + E3A86328801DC85E7C5FF01B800C8FCC869CB64E265B9BD7D37CBE4CE7E7239F + 079E1FDE1B50F7DFA3C1AF9172BD868EB13486CAF5FEA0D779B6A38E71A6A817 + 4CA033B0EEF6767FCCA5F91B401CD326A38C416CF2BD127E2831AEEEBDC5DE46 + DDD0D9E5F1B98DC1EFCFD0C1066DAB3B91DB838DB5DCB7F3FDDCCE61D1F11EEE + F778EB6F686EC07901E73B2D5D6C53D68AD9B2A14EA176EC7B11543F10C105D4 + AD943FF04D202F6429A1E42A10754EB2DA1C79C5E7A59D5D2654818E6055704E + 39414CABFB7CD3C0B9A8EB8122675CF4C7BA3F3906A58C89A4AF427DABCC80F8 + AE03898C3E8A2D896BB5AFA4B2D9D1DF4B9361EEEE6B1A436B4EAF6DAE1B627B + 08B089315A68F93AB0E6298F9E56606D03789D71D076FBBEADAEABB88E8AE8E4 + 580F76C42CDA133B475C57E5B59490CA9B85BC853DDE074CD418B6962DC87F1E + E67A9275AC77CD9F3110CF4A92BB5E066298E42F6D22F0A76AAEC323B8B627E2 + BABD650F7D4B8E926BF90AB151D08EFD52CE760475D53FE45734E7A2F2E705FF + 0C4FF4F35519CCABC5B64275AC9F26627F3320A16A75DAA3C431AD6F9F770218 + BB8069F3CA80F956C9BF02D83C44E0F1D5337EF380B8251FDCCA113B62CE1BF0 + BCB7C85791671FE03C94702B75E4F3675E9BB828188F315B3A6DAB5E41DB319B + 79781EDF9342C7DD42CBE755848F977C5943AFA09DA247BA4AAC86CDEA14DF39 + 9C1BF171A2B8C09FF0DBF799418C3D02E5035F364E97785E1CAF3C013C44F08E + E914F1036E91BFD5CEBF34AFD18FF11540EF66307431A3BCA56BC262EACD6C59 + E8CD8F68D95574374F257F2BEC468DB2A9837C15FEB809DB0D7D510258D0B223 + D4C64B9D08D04E65DF196022C8C9232692B313E00EB4ED00794BF65D413CF616 + 5A7E63880B1F5DF4A0CEC5599447D91068437B90BFF5B138F88C4B3DB7B38F2E + E8BCADD8C0A2D36075E8303E10C03324B095EE6F58B33ADA555FDA207FDD8EB1 + 5107B1CB5DB7BEC02E75952D002EF65BBA21D0671658A8DBD8B0B0DFB57A81FF + D31BBB7FF72FA6F543909D9978F57757C90E5DAD039E68C02EE22DAB17A8AE67 + 17C084872736F00F7DC83690A4C8642F76DB82705D3F3C7680809D4206821725 + 923125218371317902E217A5DB1B98B4194BDEA88EB15B883B8009257C23A857 + FB899DDFF583AB697DFF5A9AEEDAB115ACEF99AC60C37428273B760C05D439E9 + CBC49C617A60714A5252938D04E43179A9724D82AA2DEB288928A72DFCD16649 + B94293A9DA27DBDCD133669713C519DCB9344400AF9A8C497582C96A26ACA34C + E58A2CCFFA5EF2BD2026EB09CA55C73ACA4C2CBC97B6B0D3128489B5C77C551E + FB1FD2B31E090707DA41463FD82B7ABE24630A9CC029E409CA9EAEC8502A540F + B46C14D13E02FE4E0282CCBA60B10AF8285EC849C4A91E70920D0498E9B07B95 + 362875072C40B28F800D09BD4DDFA99A6D27E7723CB501DC0F72C42E75926E51 + A6769477B20C924E2871FCDCB9AA8428FAE758703C9B85FCDD00016C76207E41 + F0AED9F8818DCB2014EF9AEC4EDA3AFED009C6CD42024F2D4E266E49CE62276F + 2671F3B903CA7B7B710C3E6EF3C3A39FCFD9184CBE09DBF3EFDB39C2B8FAE46F + 2680EF9BEC43D3E547409300E639E4F74DBF33F558831CE071A3F4CFC66280CC + 46BB1F378F57656E6B6D92DF5E37D00E25EB0AD8122AABF10CEAA7F61833BFEF + 751CA8835C34809CDF3AB6F36458B371410602B88EC562F40851F327910B9D8F + 690ED4CFDBA89B8CE71665FC9EB13FEA5062DC18CBD06F97BEEA4F1FFACD037D + 58275A71B95B186DFC06509200C6983DCE5B6FDB79C46F3813B3BCAEA2E4B590 + D748C86083B693C44516AFC52D19632A544F9B68475944D4A98FDECB68A77DA8 + 1DF51A87FEC04A017D5C6F700218AFA428F7E799FBF55127E3BD9BF7EB419910 + C0751E20C07C6368CE3104DA6B5CC6D158B14FB5A93853D0D209743EA66DC504 + 7FD81302D889D0522AA857BB269C1C6D93BF04FE68C8F7EC72576DB401C9DB23 + 620BE2383896B8FBB789A2EFC52E328EBFCAE7C1FCF21C1EEB8A6E3DA0737A95 + 2D02AE518018635E4C5FD70854E6EBA8019DC65039D05C7F61DDD4586341D627 + 6DF18A1D5B77D8DA8A805DB76EEC6C5B609F2D547F258087E0365D1DEB66AEB1 + 8780F5790BCC0500BD357BC3967AB6B956AF8FA0564056E41AC37323A2579D8E + 85501DCA567E25CA68AB71285B0E88378B45086145CBEE79A171637F11FE8E5F + 20F8E21840FC82045E24CEAB021EC789C72C3963E68799235E183527DECABF1B + 34675FF2F6CF45001312AF291FD2BFC1D278AECF6914F87C16F88CE2675A39B6 + 39046DB46961517BB59B83C809BE4CBC2180054AEE2E42F22A663E58F922CCDA + 738BF696D82EF0C57E83E5C18B404B3703256C71637B7E90986DE95656733FD8 + 953B43F0961DBE40DC1D5CE5AB9DAFDE9023BA632A5880A0EEF9CA3FE1F244A0 + D8BD6AFF7CFB5680C9541F3A196EE901D5ABFD8BC2C927F3AF365AE734A2B598 + 69C9E621DB5B9FFA6E94F09E142ED27A8B4BC808B4F9E8E58D42E29A8C0B48D6 + 41065772D7642CAB4D296B1F802F80B168E6C2DA10C9DF2509602EE8FB0BEE61 + A86D8742FE6ED9F8B60DBB99F4CD04B0C90DF9DD59ED0406FE8D8E0484272140 + B89644C30C018C475B5B7F3AE66A2BD0D8E80FC4EFBA93BFE81BC798CFE974EF + 62DA3842A2298F499323EED748C2C0AE95A0A12CEA625DA12472247609955317 + 6D5C66C70660F7F004BB3D4AD2CD136C280FAC8D641C08DD430376F662170476 + 45186AA24E126D80EFFE4542B124168956C29232241BD96612742CD9497BA265 + A7894CDAA87D8C4FD950FF51EFB2923CD664314099268E35614D68C23A42F5D1 + 878972EA34794E825493E8046D00FA288100803420F9AB3150275A32D5719C2A + 67FC580768AB72CA783C2DD05E0915A0122D42B850877A246286F42D2C432439 + 2175B1909C06105DBEAB12BBDC406409D9E6E49695112A872D6329595789B612 + 0B401BEFCB856D9403A8D73158DDE3943A6D6BECA26B417D71BC9BBE43186547 + FEEAB9E33901EAB1802C2CF09DBD8E3B9500DE3ABE5B095F25803700271ABB9D + B940DDE15B90DFF15B60E3ACE4E3797CD76D1C6F99FD05EC12C679825D8EB775 + F943C7E6A57B26FBD07488CB4745E773E890CF1B25DB0A1E3B76E8C21763C179 + C367C0CF413F5B0272403F13A265A37ACA80F8FD8A76B1CF9E6D3926479175E4 + AFD9DA67B47DF57EDABDFED03EA3BBFE4868C83D96D96ED93161772BE2E0BBCF + 18FC1D783C2959D77160D771B537E47AF75B57C4F3CFF622BFD9E8471F05648B + 22F64140A7F15052EE3B7E8BAF9778DF33BE3B72DDE3F511D76AB4B1D315A593 + BE38FF05B0D36B29EAF1FACCB6DA6A0C057DD47F5190EC064072A3E439F0F350 + EC742CD83DEE3BC8F1BD9131408771F7C79E6D01FC410AC0FB84F3E3A7CD57EE + D17CEC341F3D9DEFE1A8C3076DD8C1CFE21D5A1F0610C699006ECF0B508F738C + 3A5708B6D11E6DFEE94CE51A8B3108D5B3AD7ACA5AD0F911B0521E5B4D3D7CF1 + 8A8E48A82AEA9CAFA18BA0ADDB6B3DD8C476B453D918D4BE49D24680EC2D846F + 4FA6ED82D8D76C1F798E9ED72F79FECF39FD22A03DE7FD2D5D94A99C6D95EB1A + 87E01AAAAEA51A3600E31290B97D5DCF0DA3AEB11A32456BBDA8EB4885C650FB + CE076B5AAE7FB1CE9C457DFA1400196CE3FAB9015D93FB7ADBD6F0118BE855D7 + CB0744985D85B5677C8A8E7280F115AA43399423A12CFAB7107D878198CBA195 + 5B53FD10713C86564C609E1E807E8CB006867443F2563FAF039C040FB964B6A3 + DCF3D9BD0D4976DC9EEF365DC97777B67388C292B707A1782AA4E2C23CC01B3C + 0F5E1C013C07E47A5A201F27BCDC0CA24DC422F62A2F76E4FDAA5F409F1B7CB9 + 7843000B4E95006EA0B3CF04F0CA1B02F8C5A35E001ABA198010C50D8AE08D6A + 49E046E737BB40A4460801EB2FEC07E91B10C9DFFA08E8007F4CB4416FCA8A5E + BF0A25818514EEF90D11C00065DF596062B73C3859D6FA1B2C8FA1F3AA682D66 + 225A7E2D60723F4800630C665317695C5C4256E5B65815C0A68B2DE42FE45854 + 5ADD6DD8460C1E2B4B93C10E8FDA6A2F90C7316407B92EBA5B3611B4ED03723B + 5EECFEDDB1E328E4EFEAAEC90A018C9DC0FCF73C93109A94203C1161F084C009 + 09E098DC40E266E310BB762FA5FD8BB7D2C56B77D2D1855B69C3747837F1DABE + C562DF9015DF9884613CB521B82B5849DD0826705AB62BE81FA56186DC5D40CE + 7A7E843492582559561281404C123AA8337882CDE0ED920803010CF23712C063 + 405C949E743CDB4F8E52C636C7C136401F8032B55519FC86007F246189969E70 + 59491CE7E471975CA68C0430C064B527BE4B9DC9F02E813D0CF73DB258566722 + 9C3A24C4D14682DF77605A399B48CF7E4A22B4DABE73536210D4038C17F52829 + 479BB1353EE5ACC7182D99C651D09EA4CADA05F32DC48913380D32873A028F89 + A51FA0BA31C096E40DFB57C0C6093F906B0DC24F49AE288B75DA2949C77894C3 + 46019912C08C435FDA310641B9EAB4AF21B8BD1D33485FECE8E5239D498A124E + 84963144F23712C0930210C000F4207E49085396095510A7999C8D043050C955 + 8C15E7D709253CEEDCCE93EF18C67B6F719E703C5DAC4C0063C76F8E0919495A + 3F761CD7A57C5CADE3E531778FB846FC7CFCF0D7CF8563D4F3DA42FC7C50671C + 95AB2DA1DF27EA59E7E742B9DBE27A60E3AFDF6DC6317926803F70E0F3CA24F0 + FD4AD06F1B78DDD9BB7ECF8F5FC7C97A94013A3ECA490013BAF31A68FEFE8A9C + BF6F8236D4B31E011D7FE7242A219B89093B406506DA46A85EED498AB27FD4FD + 3E61F70D3C2E194F67D8B6EF3CAF93FCB30EEE0D6C9304D6EB346C20475DE56C + B30ED0B6778D0D805D6C0FA1DAE0FC591BFDE17E16FB651CD5131A67C88EF753 + ECFED5F853278081EE1E3D848E00EEEEE96C3B291C08E0DE7C4030335728B2F8 + 64119D97447B85CAA3AF8236444B8F52E746285764BE071B1FCB0909E049D939 + 0CA8DD22D03863B27950FB3E393B821601ACB2528F7DC53EFC51D236C7C63A02 + 4F329A37BF27866C86DAF4E3DA20DA508F3AD7211191FC650CB5D198D4ADD81A + 0EE8D6746D708D156528E31F9021F357E6E0B1D3D0CB3A526DC6D0D9DA1A7204 + 247F7D8D0C5921807B808C2832E60716217889211D65351F4098CE31D45614F9 + 501FB12FD66BCEA0C82360C7631C42CBAF0DF4338C16B1DACAADA9FE2468C504 + E6E9018D1311ED86085F42ED5F243C5FDB903F2F48006B7E5873C451A7F2AAF7 + 4740C30F7500759349FE7D88383C3502F80DBE1B50E2B7F23F85940DE46C0FD1 + 46B1AC3D6036CA11023146D4BF6CBC2180052F9A00263A02D8DAF50B832F69E3 + CBFC064DF46E08F547DEB7E95F0082AE098BE93726BD490DA37F839B6F336357 + 6FA6D0D94482583DCCE4AF955E373F020430496092C34E10DBCD9444F0689F4B + A0FA8609C18C4DD47F6780C9DD3030196EC9236627D0CF8FE527EDBFBFD045CC + 105A7E11B48DBB7EF344BF2CD000C4B3C514D05BCCB9DC16A078447325814BCC + F23D407DB295495F8D038217722787212FA871EB22B7BF408E8BF1934017E4F3 + D0F2CF802E2FC22720709DEC35D9AE1D672180F9FE2C262134294128018CB83D + 0218654840A0CE2483271A1A3181898D6F6263BAF6FEE3F4F10FFE30DD7DFA55 + DA3BBAEAE7D775925841A96D4D9650463B00FA08D5B1CE76248011CB0960C06C + 48EACE02C92926AC5022410539EAD906E46F2FC15692668E42F492ECAD3645CF + B6BFEFAD24C65C76D44E3E8E814947255F55CEB62629094D8ED2877545B4253C + D9D9C05C3B490E33594C1977FE027CFC73DDF524096BD6E701FE4C6A33014EA0 + 8D64BD820972454CA0D357DB1A833A920144F45530067C08CA513246B517F2A3 + 05922F43F03885548904B0BF6B55403B922B80BF2B54E305FD10A24F44B5B3F1 + 71D7230936925A4A6E6542CB7C06482F25E8A8274946A8BC129EA13FB4A1D7FE + 08EA684B19E50ACA7B3A3B5E9C67909D044938EA1CE6E3632BA46F7EBFAFC52A + 6D621DBA4B779C4C04A98CF70F23E6F6D5BB69FB38CBF01977C782B1604C2051 + 3B709C4E4E3B6166C787736AA5EF00AEE7291F3B770283F8C523A0372F7F687D + 5B7F4EFE82144689D83CE6427C170298E071439ECF07F4792C00FC799EF5F3C2 + F1F098684B7B85DA310E106DA91FC23C1F1E0740598B00C60EE0AB0FBF4EF7BF + F975BAF6E87BE9E0E603B3B5EFB05D7B787E34B6F615FBD53AA1C7EA90DF998F + 6DE03718E5D4E9E71475EACBDFB8FEE62370ED89E42FA071156AA3A432AF7718 + 17CE17EA7EDF287F1CE2751425DAA8E31A4B3909605EA779DD87AC450E03F425 + 788DD6EBB4CA518F3E68B33FD68968071BDECBA21DF5116A47441DDBBC57F66C + 009CAF70AF56F07E3D0AC428F77F9D1BE81C837305257B39AF611B507FF55B46 + AEF31BED7FCC06C01C4AEB0AC8BC3F79FCF31838EF83BD82BA4A8C9679E31834 + 664BAEBA21DB08F63F17033B7E235A7D006A93E7EF98F3DB9A43E6F22D50BF8C + 4DB4550258417D4BE7EB8842000F91C0D19EA83E657DC535565CF7C5F51751ED + 8A2D7DE611C00063B4906D504769EB4802242ECB526F11C0758D2E766E5BE45C + F32BB04E570CE987E4354F4098CED1D2010D1D63B5FAD1BE581FCA3569DE81B6 + EA7F12E4BEBA3C59244449002B71AAF6437EF3D08AD1C23CFB966EC887F24510 + 7D4F1BBD9C6F43FF3C701238E47D7BFD3508E058679EBB6B4B0E7F04A74A002F + C507BCC169E3A510F9CAFBD4CF5B88592070727D2E6E00CBFA987E1E47A87A60 + 59DEF175C5EF1D010CC40F553F6845D66DC997055FD0C617F90DE6A3F563075A + B673916F6027218097413F0EDA363998D8246F621322DC20AD5D4BD33B296C93 + 2ADDFD4BE277D56EBA93D53DC7D4DA2D12B8F65509E75CA74E6D896A0784C9C0 + A8EE35C7E29336E866D14D80FBF531E8E4F90D9603172EBA8019B3896D45F4A3 + 2D4B222F18EDB315F217A80B34DA5480E8C582120B4B5B806EDA825A7600E39F + B7FE5DC0EFCEEC49FAD2DFE39712FDC31EA42448615FB0169FBC786D2F9AB930 + 6F2DCE1551CF781A73A8ADB259405708E05D2CFCAD2F43B713B84F002B5A4989 + A163A19C3A94F30860FC737FF3E84A9AEC5D4CE7AFBD9F3E78F265BAF6DE237F + 04F48A2D52D7AD5F265C60AFE301C6122EAC47529772D699B8016843AC599C15 + 1B3BC8DC8ED0CDC8846E3B41D58226D65A09354D7A453DDBBDC497B53DF97726 + 27239964043421A9C946C890706492948952DA8FF9A94ECB08EDA705B5455B93 + B5437E31393C944866B21B3B9398905E164C6AA3AE896D24CD5B097BEA154C94 + 2B9874676CFA134A1CB4FA89F118436D29E718E8C718630081DA92F7004205E4 + 492154D0568247891ECA5992786981F6EA43B4EC5027A9431D085CBCF717E455 + 24F394D022990739F594A3447B1928A18836FB5168FFB0D1B111940DC95947C9 + 7341B20F88E7DDE518D7651B5F217641E2460238EBCA239E0DDC19BC7D7C37ED + 5EC3FB66EF5ABCF7137679E3F1C07817B08E0F646E258421B363C47B54F3983A + 521C636F9D2B94D8FDBB737CDFFABC6763C1AED60FAD6EB882C74C33068ED93E + DB726C28639D6438647907701FDA3FC745400F39C7C6BADAAA2C1ECB18B48F51 + 94CF0FA04F7EF4B3E9FC33C824F0E1EDA7E9E39FFD79FA47FFEADFA78F7EFA67 + 69CF3E271F9FC5C06FC2FDED3A12BFD33A7EB4A9E36F42EDD5AE8EA9FCD65AA0 + 8DDAA18ECFA5A51B036DF91B6F81B6AC0FF541FBA13FB8C09E63DCB97A375D78 + EFA374FE9DA77E3D5D39B4FB59B996A2D4EB31AED304DAB00160C3EB3865F447 + 59AFA505940DE9E817F59445A83DEA84DA44DD10D4B6E5CB7B655386FB33FC04 + 6BB8F73950CFA0BD023693B3B3F313CC2D08B4A1DF387F2B6D5EB0F32EF30840 + 7DE39C84F154A6F268A3D031CC439C43455D9597F9DD3CC4F9E1A44100AB5EC1 + B924314F3E86E8D302E7A9333AC885007699C46CFA34E076863C57C79C3EFF21 + 54A1F379CCF55B6B0885DAB2D43AF5444BD65A6B70BD13413D6D582A51CC32AE + AF74EDC7F55FB451ACD93A467D69E7FEDC218CB531EB408841C47EB9BEF47530 + 09DDB28646DDFF24BD95D7DC552E7A92BF1526D7757FC42AD6E5654DBE08681F + D1CE1934403BF10516ED4B734DD18E75CD4B2968B708B24F972BD37C9A92BF4A + 02AB7DCB6F11B4622C8B5EACF8E4C3019C66FFAF3A5AB9628239616DE7BA9D9F + 1EE863FA974108462CCA0F444EA18596DF1096B53F0DC4F146B47CE6A11547D1 + F021894FB46C46D1EA671928310B3478B9191BA2653B07AB40E101231F18E58A + 93F08EA78D9595ADA6FC34F15A13C043BAF801B33D0AFF92E10BDAF8D2BFC1C9 + 603FF8552B97BFD008417A427437B761D0AE239A6DD2E404B0A14CB840F8E266 + 093DCAB5E999343159DCF9BB56C8DF0ABBA9C2A6D5670F85008E365AAFFFEA8A + 9320C6587082F43A81933945CB4E27C6AC47CC4E8ADB726DBFC172E042262E68 + 5A3651E644AA619E2F4B421764B4AB9F2FEA80D8ACACDB621228A4EFCA862D78 + BD9D77F67A4C7C0F001B4F5DA096381C23FBF1F1D06ECBE4B6489EF822B82C98 + 01591C770BE4F6E25931A623184317F951D7461E2309606075C7C65A1F01DDED + A645B281C98819EC9A8D2412141C13752841FCB6E2B08FE9C1E5B471E6D8646F + A58DC3CB69FFC2CDB47978C58EC73E6F1BE3BAE95BFE1C2B932E514F500F4472 + 97ED28573801ECE4EF6249A931CC4BA469422CEAD9A60DE07A24121B49C59A58 + 2C324D366AC233264DA1271887A01EB1D42EFAC77EC6FA68E9295379247A01DD + 514430093D0B24A8C7D0B7F724B4B491E046D29D44AB26E7A90798C0665B6D98 + B8D7788C03202EC95FEA54CF380463704C3A2EDAD3A66537241B82DB5D7CB712 + C04ABA90802354CE52ED95A0517984C64349B20F6DF5F53AC627A416092CB4B9 + 33987AD6D59E50E24BEBDA56441230C66FC9E8AB3631D68C8F01BB68ABAE1C3B + CF51849F2B87C574B09D65243041B2F211CF80BF53D6CA9DEBF7D3DECD87AE23 + 0159FD510FE3E7189D00B6EF31FA47FCF562077BECE8D55DBDF91DC6B0C3638C + B10BF8438B7FD7FA329BE3FC5E6010CBF8FC7C8776F90EE2F850B28EBE48FE02 + AEC76FC97729CF824427EA1CFBBCCF27EAF85929A0D73811D11EE0E7803A8F09 + 9F2DC7A804B03FAEDB8E69EBF85EBAFDD18FD2E7BFFAABF4DEA73F493B76AEE0 + BFE364BDF563408C7ADE2C16FBD7F1F01C783F861601EC3E654CCB00E42A4AFA + EAB1CDC332B6008F59FD5846B2978F7AA62FDBF80E5DFEF0B3F4F8077F9C1E7E + EF57E9C0BEFF2B07D7FCBA074217D753DE17788D858E0430A072806D007EBD6B + A9417D00EA55CEFEE2F55C6D86001F1DAFCAE3B144397543FDCC8B413096CAE9 + D39203FE0A863337D2469903E89CC1E7176741FEDADCC0000278E3C2AD3A5F98 + 993B882FDB9CD350166D5B7E51C61843501BC6E0B8A8AB73B332BF232685D48D + 88F343D8D1967348EA19BB02B28295F2C745C6415DDB6372EDEFC428E3041897 + 6D9DE3F2781455676B91B14740F7E6F145C6750741BBE813ED5B7AB58B7A22F6 + 15EBF4511D4002B892BFD4A35DD65628C7D6862AC73A6C7DFF4A9AE24FACC58F + F62079B16EC55AD6FF24BD617E24828B8D22F6978136D6C4194DA277080B10C0 + CC17E87A1C509B31D07E08357740984F0F2253BF65FAA25E8FA3E553F31902D5 + 8F03B6CBA195636BD92D8356CC79785EFF571A73F2B535F7DBD011B489A839E1 + 92AF2662BBF3C9E46F443B2F7F322C127F46E664A1B42394549C67FBAA60C931 + B7CE530F315E44B0D7F3AF88760BC179B3469F3D346C1621758BCEC9DBA85B00 + CA053AFFB7B255C9D4313BC518EFF832A0637E91F8BD7F047494AB6D4FE75F3A + 7C411B5FF6D700CBFC904FFCA31F41EB6202F2774C3F0CB353E2B3DED49687DE + 1C5BE8DED96B37C909265C366135787D6293314CBA400EAF82E8CDB624803309 + BCEF50F2776AF05DC1612CCDF1B46486E8E788938660E77EAA7F4DD09AE471F2 + C77F26B627829075684F82F3649B75B7C579F27335ECB32CE64DDC5B32CA5F36 + 5AE31842CB7F08710113CF8982B1E9137DC7E07686DE224DC63ABB68B3DFF33A + EC516241698B594759DC229E2DC2F451CE7CE72F63A0BE658BF3837337D2CED1 + D534C54E59EC3E469F1EDF4A2CE6F0EF672C5A4900036591BC0CE2E25A750A5D + 78B764B380DEC66858DB51F238CBF17E292418FA4908D42F17C2F7729A20D1B2 + 776CB89A263B26833F920C252101306E8E5DDA1E1725629A9D213FAAADC08968 + C82F5AECDCFF0A1E1566E5FAD1B1D95CF23E503251D2AA0F416DC6885E05DE47 + CC3A8E5D93554360122B26C65A09AC1EE827B2569CA60C89B8908864B28F4947 + 4D34AA8C606293A00C65B46FC9296B8131094DCAAA4C93A2F49B9181F01D2180 + B9C328269C15FECE423C0AFA5C4EF8CFE8C53726A999DC8EC977EAB4AD32CA09 + CA14F4F13648830B164777AB993C3FDED8E465FC80CB4D4F2203587714E284BE + B001D92076B99FEC9B8932F3814D912B28773B902A86AA83BFB5232857C29672 + 05E500499C08D56BBC18034422C845925C2C416AB5882DD41551CEB6127D6ADF + 23EE444E504E728DFD533704C4842DC7DDD3BF95C93A7E163C474EB60A32496B + 3AAFDB180B3866C5D695EE5DBF1BC777D27A29F76E3C4C07B71EA7ADABF7D29A + F53D05790B52D6901F139DC95D90B8043E835AF731E4368E79F362E9AF10C038 + 1E9E8B2D8CCD772167F21724677EDC31FAC0AE561075763EF03D453C3B367C0F + FDDDD3E538FDD8B1FBD7E03B9D310EF3E70E677FCC75F83CD037C712BF278A96 + 8CBEF427A86B21DAAA3FCE1DBFCB1C9F8F099FB5E9F271E773847700EF5C335C + FD30ED5909F27CFD42FE4E62172B62F835448E897DF258181F32B629E331A074 + 5DF8ADCE03ECC7AE11D4AB0E75B6B57E12D01FD071E03781F302D00EF515908D + A6BBF6F0ABF4EC27BF4B8FBEFFEBB46FDF7B10C0B82EE3B3410C5C7F715DF5D8 + 280DB8E6FA75D3CFB7F555CB626F2580EB3F0963822430F5ACAB4C31A62354C7 + 7B0BEF492AE73D2EC6845CEF7FAA632C0563524F7FC65040C6F7EBB77C68E736 + 768F5FB7FAD4E4536B1393439B0F4066F0C744DBFD7FDDE24CDDD6401FDAC0C7 + 3E5FCE39309FE01C863205E59C7B102DBB2120BE8272F5EBE907085F827ACE05 + D18EE8CD0DC5B7853AEF64BBE1A7F15AFA165A3E11B46B11BC2A635D6D54E64F + E4A97F30C51A03EB18945C5FE4F587CEEDB91EE890EDD486765C0B459FD61F47 + 5D6EE3C11A86ED352B49E2AA3FC0D81A5F652D38516B6BB579EB3DACBBFC894B + 222301AC7212BF5C43D6B5A4BFD247DAC59EEBBD1590C478E50FFC4DCFF5DE0C + 89BB398BBC4E0E887E00D6DF585FFB5ADBD6E66647D435BAE84741BB067CED3F + 04F86A7F80C9D53F12B99ACFD0BACA86486CD4350F43D06E31C07E3E4E7B07F0 + 2268F5A33889CF6B03E4714B3EF7A4395BCD157B0CC90377809CE8E79933210C + 98BCE4E65B68E7E717432B1EE1364E08CEDAAABC85EAFF1DC632E740CF6D0BB4 + 897EE3E87F46CAF5E0F35B5DDD4D6BA5ACE4AF7FAEC082047083AB530CFAAE65 + B476FF4644FF96CDB70112C02F9A08FEBD26805B3295F7F4FEA5C39753BEC8BF + A7D01FFE69813163DC2C5BB64FB39BB9999D1EF446E884AEC16F889878D98476 + 6DDD26C26B5607095C6E9EEEB76A58C1CEDE8334B59B3089DFB535D9F95B08E0 + 89BF687F16DAF73CCCF84702B84C041837EA5E672C3601CC93E26526C66E8F73 + E5E76B71BF65A1631B43CBF745A3358E21D0471726F3A07E8B80B1178D0F3B2E + C0B838E362AC277360C166BF6710BF53D859D9238181B2A82BBE58E4F98E6083 + 2FFAACC4A39E77CF5C4B672FBD93F6CFDD48EB5888DBE214D78C352C9C4130E3 + 51D4B690F258E6E38BE6E7208189BCA06EEB082ECCB5DE06F49900CEF52CC742 + DE6349826265D7E458D0EF5C76ACED5C7180FC9DEE5F4D5310C0BB905DCC0BFF + B2F807286342C0DF17DC4B6CA08D640B922405450F3B244E688B36132C8CA136 + 2AA32DB07E68E3143F056411B4D512BB92A95FB5E366F2690C8B24C134794599 + FA116309BD2A839DEF824112B49D70D444E510318C9224ACCA22541E13A05A57 + FD18D8A7EFDE31A00E798C51EB2079470860269463621920D94B0298A0BE0526 + A3632CB6159A206FC9A33ECA191B75900993B7CCF64206490C007296B55EEC40 + 62A01D8910EA622CFAC206C487FA443BDA28E81FEDD47FC8AFA5675D494C3ECA + 57F50AC8544E328D645626D53AA291045724B708F52760A7600CC627D45EFB54 + B06F407D09C8E10B8294F6EC8763A3DDC665038EB99C1F9EAFBA03D6DA95182D + 04682658A1EB43F59BD73F4C5BD7EF79B97BF371DABFF5246D5EBD9FD62EDD49 + D3CB77D306DED3EBC8B17C5770E91FC0FB82F3E706D818ECF3C18E713C8E98C7 + 4602B8C2DA78D7F0A6EF42CE00D19991C95C909F138BC59DB03C3E1E23EBEBA8 + D7763E7692DB4EA2961DC1FC1CFC5C9636CF37650A95D10FE031A86D0B7ABC94 + C5985E96EF34C70290F4CEB018783CB67D26382EDF115DC641E216A43062F835 + 418E877DA3ADF1392E95EBB85C36F07B8EA8E3377B5C1F788D88BEB4A13DDB04 + 7D21E7774BFD89960E3EF457686CF6A77AC8F02E65EC027EEB838F7DD72FFE54 + E47F8031DB3A16F661D7605E5B015E83A717B32DEAAE3728D1CBDDBF94B11CBB + 67A84E650AEA116F4C8F3AEE37B8C7A11EC7431DEF77D18F883A22DA445DEBDE + 0A19E5B57D845DC08D7BFE51B9E71F9ADE9075160FE5A1C530545B43E7DBCD31 + E21C09E01C2482739288456DD1579C57C5F9D4E4C864658EA79808C10BB46404 + 749C2BB2BD28EABCB26048A7F216D47608B4F3C74003322F6ECA06D1AD17326C + CD51D6181DBA797F7B1DD05E17C0AEC6B5750CCAE89BC9583C3DA9AC31FC4FB3 + D9C7895294B091F5157DAB4D01FD801ACF50E5B093B51FD77CBAE62240F2022D + 9DFAD67A694764DD5B69425FC864FDD8C1C6071D1FFD5C48DC6ECDD987EB5BBB + 7E15B69EE6DADFEB9B99882574EDDE5FFB0B542F60DC1A1F790F9444F16FF517 + 7D358F41441B05F44A00B3ADB2562E6611C09FC4AE12BCAD7C5A4BA650FD69A2 + D517A17633E434F2A02517AA768BA0C6F83681B13F67CE9639E2889A2B1EDD38 + 055B3B1FD5CFC652F2F34368E5E987E4448CA1701B27041B768C51F46F7032D4 + F35CEA4494B17D6A2864AF7F8EA55EE13699E0CD646D9F9BEBB8B801DD1834EE + 49FC0D911BFCB6F086000E68D9B5D0FA502BE44BB1BA62254E2CE50E7E417FBF + 702A3F72BC287E047E1371D80D053728C31A801BAFDC6434E6F00508315B37AE + 1360CE2EE2EE26789826987419FC51D02BA607E18B1B6539066FE3E66900098C + 7FB72C43006B5F4494E391D3F9B1D3723327CACD3FC26D1BF2D71971E206CCDA + 4196D19A04B7A03ECBF89D041A9F75ED5BF52F13710CAD710CC9C730168F68E9 + F36265FE628925501776A3C0620DE4AC9543A824B0D95B7C5FD8610186C5BC81 + 8BBEA92DBA376C11BE7D789CC9DFD2477F216BFEB658C90B5AC8FA8BF465E08B + E8467D0CDD227C0CB0B5B11AFA04B08D3724284000FB3FBDB7AD6E98EED9B1EF + 5F35D9658713C2BB2891684062A04B4268D2C1E397B8B3890D2458BA840C895B + 2446D06682843A806DEA341E64B45B2F256301EAAB3EEE0759A92B200362F28A + D0841792512A6B817604E3106A8752137831C9E7B0BA261D99648C894642F50A + E810A3450033B6F6415BDA534F44D9C491ED35C1BACED264FE9847C433AC9DB5 + 7E0C9373D9D77738235E49E46A52B77B9F60973CAE49E5F379976F8FF0355945 + B163029A50D9909E49EF56E25B750AB523A26E1D6D24EF0D1C2748042536482C + 00241D706C2B768ED1A64E6D28577F252C54AEF115B40140828CD9D34E491992 + 34D1BE6547928525FDD40E600CD4B36D26ACB0131225482D3C1AB7FF1E598B13 + C830925D2D228C71684F79D4D187F1680F40A6A0BCA5673CC4E2EECDE8839DAD + 385E25C8FDF8054A8ECE90BC02ECF6651D8422C85F60FBFA83B477EB8963EBDA + 03B3FBB0870D3BA7206EB36F473C8300761218BA320EDFA56B63C62E5F2580FD + D1D038168BB573ED5EDABD7EDFF0C0DAF79C0406D199771C77643308E1BCCB57 + 624B998F37036363DCEDAB1FA64D8BE3C474F96CF473641D727E063CDF5A07F8 + 59A9BDEA69A3E5A23A7C96FEFB311D41D23B2313E32082714EFCB8EC7CEAB1E0 + 73E0EF8C31109BC78536BF5BF0A11C368CA163429BBFD531D4B117F0F7495F7C + 1F6043A86DB42760C7EFB4D6B51D6D108BD73D05E2E19A873AECD0461DF6F045 + DBEF1306CA351E4AFEE6E80BC43A7CE238FC690C2879CD2F6DD5F1690D3D4056 + 10EF1711D493C81DB2D5FB59F4897AFAC458AA8BF2967F4BAFB24800032B901D + 75246FBDF7C317A510C05507F2D7C0B6CB4CDFD9A06E7E47D7F3AB33CAFC22CF + 53FA738E16E27C87F31ACE6D38FFA11D4A9D6F45300EECFC0F7A98E319F40F7B + 152D59C1A4CC0D5BF3C721E8DC937595452C623334BF55D0D6DF053C80E8D3B2 + 71D89A23BFA2056B175BB7606D80397F81CEF57D5E1F64799E7FA5B706D0F503 + D60EBA8EA1FFB6CD4FDFBAF161BA88FBA59D7FEAB856A9305FF503101B3292BF + 8A9EAF02FAC6DA8FEB2D6288FCF5F598F8695B65116BDBE68F12712D4EFDF334 + 7C6AEC127F8000669B326D0FA2ACCDB1FEEFD6F6194AC8F610EC7A72B52B406C + 8F8FFC84C8ABDEFC6ADE016DC807F218116AA3802EEE1A8E3B828156CE641EBA + 98A8678CE5D35AB2A83B6DC43E623BA2EA27071923B643A831BE4D2077EC246C + 43B720982F1EC24CEED8D0B2EB007D0B16CBCA56CEBF97B36FC898DF6F619E9D + EADFE0E42099AEE7746DFD204D37EDF7F0AD9D67F26D1D1FD7C3986E081AF324 + FE052D4EF1DBC0CB207F81EF0601DCF862541F6C17AFBADF0302B8F74F8BFEC5 + B5A59F8B7AC19FBDA10C0104E674FD6C5AB7C9CC1493104C60A0F31B0AC6D2E8 + A787E5FAF39B5443DE0409E1420AC317E39B60E234B189D2FAB9B4630BA6FD33 + B6A0C3AE38D3E3185C0FE2D78E6BCDEAF05D66077077A3ED639E4EF5AD89C06B + 054C2480966E696042D74D78E78193E865FD88E7F1D5097D2B0E652D1D101704 + CF8BB1BE9E171AB7D507DBDD02A50FB51DB533F416784DD802B345F6AAACCA61 + 8F7A6EFBFB8F1CD9C7DF89640B5B2C7CFD9158B0D39DBD5E37D916CA4EAE0B68 + A2FA04B46CE76119FF9C00804F1EA3CA7DF72E920E567AE200C90F2468762ED9 + F15EB285FFE5B4B17FCD70D57C2D0688DF3DE82F3BF93B4532C34A261D34F1A0 + 098DFE23D9ACCFF24FFDD55D3B869D0B95BC65D225264B9830613BEA80EA5FA0 + 492024873C192472B5F3E451941BB2BC9F8C621B092C24D95C66E705C92C26B6 + 208FF08497C4513099A575C69A1E5D6F023B4634C1C884612B41D84A44A20D30 + 01491B02368C85B6263AB96B973E28A9431B7E1E07A42412EA67CDF6DC6D07DA + 2E4372F71049D81BA6B7BADB9A1E097A109328DD178961EB4792B89E34B61209 + 633C2A12E8258FE167FE8412C04CAC33F1DD4A4A6B1D36AA475B774B457FD50F + D944DBFA4850D8DBD856EDDC72CC182B880400E482920DC4D03102F4813F8909 + C6A0ADC6655D65D1163122E1C2F1B19FA86BF9B00FEA2B7967757FBC2FF4C51F + E41E011B94D8590AA00E9210E49E938A971003B13E705273DB1F597C271359E7 + AD2FB389A45D24C1947CA51C7582F6243455AF31A35CF50ADA103A06D449CC65 + 64C28AA4AB9E57A24F88DA38407C1692D749C36B1FE6C73D835C35E0FCE191C2 + 9BBEFBF75EDAB9F9281DBCFB513A78FFD3B473FB59DAB8F1C8B175DDCAE37B8E + 2D8B017215242B76F0FA58F059605C7E5ED0AF1D9BC9F099E8F9C267C59DADFC + 9C766F3E48BBD6C7FAE57B690282F3CA8769E7FA833C5E7C7EE5338D04B0EF30 + B6127D41978FF97D1F171E617D78FB71DABFF9D062DFF7EF82BFA718C01F05CA + 9F0588D66743D0A6EEC65DC047116DD9E677C9BF2FF8CC00B1E3E7978FBD7B47 + 324961EC8CE63BAEF17DD9B8547E3306FDDE301E6CF86E60E8B5AF685BE538BF + F81CAD8CD709BD2E789F56AFFD171BFAD346EBB4897684EA5B72F8F037807A8C + A9761CABDAA1443CEA71AD8B72B6F93B439BA01EBEF1DCCCC8715F083AF417AF + DBD55E63D3C6EE272DE07E42CCBBFFA85FD4B510E3473FC622911BC95CB569B5 + D50FE8C9EDBEEF3B81E17356FAC17DDF759067A0AEEDBECC7CF1BE7FABC37FC5 + E65079EEC0F943466EB781F71233AECE9D08CE9D748EA5509DDA449F38978BED + 165C2FF34FCE211571DEA96D42ED190B808EF353FAAA2DA03AB5551B1D1FFB54 + 5BCA00FAB4ECFAC07C3D63751F313AAC1D208EA1B4DDC67C22018DF9BFAE297C + 3DC135014A03D62158D79CBDFA41FAFAA7BF4D5FFDF84FD299E3F77D77ECC4E2 + 6EE2554166AFEB951634164A82EB2642EDE2E39FB9D66A813675AD276D85AF23 + CB3A37C2635909FD9A8D61D3BE5F9BF6194C6D7DE8E3605CAC0D112792B80A21 + 875BC4F00CB0362FF90027647B6BFCB37D129872DA36EC676036885DE30B5C36 + 90C78880BC95AB00D477CC4E770013EAA3311603C6B538905B6BC997C16CAEAE + 8D966F0B2DDF8865EDBF4D782EF745E773D987A261C7DCF2109883EE01B9F395 + BDB4BAB66F009FA0F97AAB973CBE720F119AEF6FE901D5A9FD1BCC0788DF899D + 37806DF03E38973BFB6FA5833357664960F242CBF0432742E6E06A1951F9B897 + 0F3E367A59DEF145E04593BFC0776B07B000B61EF7BB4A002FFA231F257FED06 + 01A233ECB07502D8262B208033B12A371290BBAD7E7A18EBF3393140006792FA + 289DB145F5475FFC2C7DFDC33F4957AEDFB7F36617493BCEB5096CCE6402D84A + F8FAFB83D766096000EF00669F7A433D295A37EFD70EA74A00DB79B1EFD62293 + E2A1897AB41B42CB7F51C4497C2B86C656A8CD69E345F431EF9CB23FD871A1A3 + 8B1DDAB13D0FCDC55C051685B6B014545217F50A5BB4E28F1E66DF91BE196BB6 + 9805B21E8B54939705715EE446641B05ED23A2DD982D304FBF086A4200B11AF1 + A807F08EA8F5BD6B99FCDDBA9C36F0B8E7EDCB69B273251D5DB9936E7CF8593A + 7BED9EEF0076F2D78064402480359981521FF9DC7F3C5BB6639203A54213252D + 5D0B206E2B798B31A034F4E40AD81022CFB64C287509294F1459C944164A9511 + 9AE422344EB4EFE93D1995EB8CA398B21E92874C124639EA9A9CA45E93962869 + 1FA1B600885EFA511709E015F35B45E2F5EC2D277773D21749DD92C0B5F6CA61 + 47DEE6046C26457382392499B9DB876D03FDE8EB718508255AE42893E335BE20 + 8E47C781367D09EA6007A80EA03FF56A8F5DBF78D42880B848F2AF0502986401 + C16388323D3ECA414E2894BCA01D7DD497F6D1963AC42139D2027D341E7D48A2 + D0D6E5206F203390D4239C042920A98952F5D8DD097925E7703E4190A12FBB76 + 813C830CBBAB9DB873520B25617620D92E8144466C8C01E3C358B3AD13962EC3 + 317CE0807D066C32AAACC62B3AB30199D623FC605F40E28D241CC93BB5F1F7E9 + 7AFF99F02209AC44B09FDF824C886690000671BB79ED6EDAB86AE705EF8B0579 + 8B77CA5E7F68BAFBFEC8E79D5B4FD2990FEC7A7FF7CBB4F7EEA769E3C6D3B471 + F371DA346C5C7D603E208A1FA41D90AAD8616BE7DFFBB5CF05042F765D031D21 + 8BF161773088624321A2776C2CEB6633C17932397617E351D3EBD8057C7C3F6D + 596CD8C29F9F693E961C97DF037E2F2803406A6347317700FBB11BF2E76DF688 + 259F1B09D58A62D7B50BC406EF648E9F113FD7F819473B6D573B8C1DDF73D355 + 487F207E41006F1DE7C77043E6DF77FBAE90CCD5DF29BF43D493FC451D9F1376 + C8A36FF6C5F128FC38ECBC028889DF347FBBDAE6E7AFBF7794F1F78F7AD4471F + 94DAA7DA10BCBEA04E5B8D49BDFAC51811F027D066BFD1AFD5667FB15FEA50AA + 8C507BD6D9862DEABC8FF93D0DA5DC4F08BDE79000E6BD6ACC16B1F41EDA82DA + 1390AB0FE3F8FB7B839CA05F04EFB904E5EAABFEDE8F8172FAA92DC0380AB557 + 445F62D616321B8BCC89149803E9DC49A1F32AD5C736E772946BBB1587FA6A63 + F3C338BFECCD310B5CBEDFB781AFFB0325D6E4C0FA08BEF46FC90125579BF147 + 7C09FAB0AD3187A0FE19F0EFFA6FC6C163A7FD0FAEFDB587D655B67BE156BAFB + EC9BF4FE93AFD3D6B9EB95449D5A0CBED6866B12F5D736A06B1BD449FCA23E03 + C8C39A6D08BAC61B83AE45E9CB36F4FEE7E762BB63DFED2BB71FA64B76CFC771 + E635B6D9E20FD55B580777E4AEC209DD1312C033286BFD2601BC2CCC17B98718 + 1F7906C8896A57F227318F12731404F5F3EC86A07E1A671C18D7CB472B57D702 + ED5BBB93152DDF8845EC178DF55D432BDF0C307FDDCBEF97DCF96AC55E5F0F94 + 3CBE720F8BA2CF030C6051EEE2BB0C7012937D07C85FECFA05D6B7CEA4A37357 + D3854BB79C08AEE7DCEC5756C18165FF853F8B13C1FA010FC732A2F2712F1FE0 + 03C90D2EC33BBEAEF84E13C00EB4EB17E0F7E0C2B208B9BBCC05B44900E3E650 + D0208071F300F10B64B2546E264EEECE1B63ABCF61E046C41B534BDF44B1C523 + 9E394690BB576F3E48BFF8F55FA7DFFDE37F48EF7EF8891354B003418C1DC16E + BB2A374AFF17D47E258249062B01DCBAE9BEC1F30293BD38E11D466BB2BE28D4 + F724FE3A691FF38FFD285AF62F038B8E212E56B43D249FDA42B1F56F576D8F01 + B6BA58CBBB790B6676F8DA4253C8DD0C5BD03AA2BC237F49003BCAE217D0C5B2 + 2E922316B51BC3588CA516FAB0C363BCF4D866EC6073C9C9DE3503086090BEC0 + CAF47C3ABCF87EFAC91FFF6DFAA7FFFA7F919E7EFDCBB471702DF97B80E1E38F + 85EEB06AB298B8F047467B72C57CE0A7804C12209A1C0134791265AC03DC453C + 44F40E12C023C83B08FAC9242698984C624249DB113DFF830E6B488421C186A4 + 1F4ADFD97BCDDFEFBB82BEE18F845E4549CA516EB62D3089A86D2626A39C6D95 + 03ADA4668CC13A750A244AB93B17895A257A9988ED12AA19ADE42B81DD3E208C + 5B3AA0C62CC4A992A19E341740C6C4B8FA121A5313DD00DA116A1FA14978D667 + 6DB2CEC9ED42FA12240148162849A032DAA0542899A13ED4A3CE73A272254214 + F457DB08EA394EC6863F499A5A2F5092B70512BD24FA50AA3CC7F9C00931EE2C + 451D3202ED358CAB90BBB423D49EEF92555FDAB8DD953B3D028E7600E56E477F + 94D6262918110937D6954CDDC6CED9A223E1456C012081053CB73AAE4DBC6BB7 + 10C0197937F0F6F54769F7D6D3B479F361DAB8FE306DDF7E968EEE7E99CEDDFB + 5EDAFFE08BB471FBA3B46EFA0DC3FAF5C769E3C6C3B405C2F8C603F3BFE771D7 + AFD858AC5F10BC201601DF99EDE3B993B6AEF2DDB5901B40225BB9813AFABCFE + 204D2F8310369B6B368EE3FCE8693FD73887E59C7A1F1617E0770125E4247FD7 + 2FE13B91FBE5F1C7CF49113F2F606A6D20CA1D3826FB3C48AE2A791A3FCF2162 + B5091C0B20327E3F33D03FC8F34C6A733CF57B61257F87FE1BB3B68263C5F7E9 + E8D6E374EEEDA7FE39514E68FF1E17E717DF259CDB129B607FB4615BD1B2075A + D711CA68AB36A847D09668D900F4A71D63AAAFCAEAF545FCAB4EE443A07D4B47 + 0CE9F5DA09E01ACD7B9BDFDFCAFD26DE8B00CAF5DE330F43B122341EEF5FF445 + C97BE1D03D31FA11B4D7B980F6437D6CB33E348760FC08EA877CB404189F731A + CC47E29C87D039529C2729541F6DD98EFA2128F9EB6569CF00F3D1823A9F8C08 + B17DAEE9246A7B9EEBF3DA42A4AA0DE7BC11EA3B66B7082A81DB40B4D5FE66EC + 4168FA1AA55B7B707DA17500AFAB21A6F8E3ABADB1B0F6F01DBB650DE36B12FC + 7914EB1FF365A9501DA0E42FE4EC9B3A97CBDA4FFF78AB509BD9B55E1F716D19 + 658C09821B8FBA7EE7FE67E9DD0F3F4D9BB6D65B99600D8E75B6ADB7DDFEBCF5 + 0922B98F48F6CE10C0F2D8E80A5FDB97357E03430430E4554768CE40ED51479E + 23E8564B1D25EBB4A70C88B909CD6BC41C47C4589C21A8CF308EBE15B4F3757D + B4FCC6409F186751689C37E883646FCD69977645CCAD5BBD23801B79F792C757 + 627751F47880164730973B78838AB5B2F3D730D938746C6C9F4D87678FD3F98B + 37FB04B0DB8F9C579E77B519AACF8590AE819773A8FE25E355DA01FC32F07B4D + 00B7642AEFE9FD0BB0CC97F81545FC91B6B0CC8F75DE0EE052D79B06EADC558B + 9DB3AB8524CE3717C49BD7FF589FB3E8DD88E2CD6808D50E63B2311622F8F0EC + 8DF4FEDD4FD383C7DF4BE72EDA621BEFCF30398E058476F6ED6E968C87C74167 + 08018CDDAE281B37DD37380D604297D19E009F0EB49F93F4A513F6E8DF9AD0C7 + FEA2CFF3827DC67E01ED2B8E81507B602C9EEAB53E84211B2EA262BCBA207364 + A2D7A104B090C07DD802D7D1972B110C50C6C5F0206C81EB8B735B2C73C1DDB2 + E3E27A513CAF7F051201330430E29598D65ED984FE9213BFABDB97D3FAFE357F + F7EFCABAF53D399BCE5FBB977EF357FF2AFD8B7FFBBF4E4FBEFA85BF1378BA73 + 5C49E23512C706BC3778158F90C663C2089365B9D900BB0210C7921061F2A426 + 510AB1CB644884DA00247A49F66A1975F3901F1F379B30D26412F50B01B6928C + 4372AD97F03B938957D469437D2B0938246BD96B7BA87F944C6262372F1EF3CC + 9DBD00758C435FF5A71F1EDF5C09600313A84CB8B692ABD146E1EF0334B412B8 + EA8377E12241DE4AA0B70860C462A2198849F3D89FDA12D4D19680AE3EDEB900 + B167ED24318EF1BD65711BA400C163625B89894864508E92BEEAA3F1D47E2896 + F64D5BD5034E483A1108922C9346AD78785C702472818EC4EBA03A927EF4EB48 + E04C8681B00359A6C41DEB902BF90B39EDD5C7C952210F29C70E5227334B495F + 94F4A11F6484C66802C49DD8B2BD65B1B06B9600E9081D095E9E079CD74CB476 + C8E46FE91B6D10A657B1F33793BF5BD741BCDE4BDB37EEA7DD5B8FD3DEDB1FA5 + CDDB4FD2FACDC769EBF647E90004F0833F4847F7BE49DBEF7D96D66F7F6CFA4F + D2E6AD67862769CBB079F351DAB0FB037611232E628298ACE32DE702EFF905E1 + EB3B570D20762797EE38B0E3F8E0ED67FEBE61ECFA5DBF02E2F7619A5EBE9736 + AEC0C7C68938E5DC90FCD563441D327E5FF088683F6EF3E167C2F3AB9F11419D + CAC6E07F0EC067F4969DFF0BF6BD2CA429895612A74384AA42F524516B1BB17A + 7DE39C8240CF3B8021838D3F569BFDE21CD87722C732B901E7CFCF21F4E6B363 + 9FC599B79FA6B3EF3EF3CFABDB155FC62CBF0D3F2F38A7168FBF5DFE96D917FB + D33641DFE8836BC8D8750432DA107AFD611B65F4A53E827AF6C7F14460BCBCBE + 445F82721D4FCB4EA17643A01D8F952009ECF730BF6F58BF0D74F795B61E58C4 + 46D1B2650C05EF83AD7BE2143BCF077C69AFF772B58DF1C6DAAC4740A77DB76C + 86C071CD2380E39C28CE93D486FA965D94C77A2C2338C753B4EC229A7E98E796 + 39ACCE7B09C8756EDCB2A33CDA5116F5116A4FC4BE235AF64D9F42FEA2AEEB0A + AE39B40E80F8DD38BA9A1FC9BC712EADEF6552188F8AEEF9808CB5BA12BA1A07 + 722292BF0AEA5D27E42EC95985EA899935A160DE1A9331400083D07ECBE60B6F + D97D7E8AB5315E9F566D505EB0318034CE6891BFC00C01DC02D6F6BD75FE00C4 + 86E46F8F0466BE20823AE437A22ECA4B7CFA301741304F815211ED88558B41B4 + 722BC03CBF6120DECB473B57D747CB6F110CF9C6F8118BDA7DE710F2F69AD3AE + 887975AB8FE6DC4B1E3F92BB8BA0C7038CF104CB701811F09D8796DF69E0B4FA + 89711AD073CA3A76016FEF5D48BB0717FD11D03331B5AD08B19B32CAE74248D7 + C0CD755CDCB7833704F02B00257E97FD20E297A9CA2768E7EDDD55569EB1DD7D + E916FD02BFC258E487B8CC8F759400EEE0370EA0DC0C78335945BDDC24721BFA + FEC56916A5CFD6CDA501DE8C866E48F5A6A6F252C7F8F02867BE27018F7B5EB7 + C9D33A265A684FCD0613B8759B74155BB7872FF4A50EC23783EF05B63EDE10C0 + 0BE1E41333F865B427C0A78393F4A393749DB4C718ADC9BCF647449BE7852E4C + D0E6185B7D2B621CF56DC987EAAD36652887164DF4217A8B3212BD85F45D9D66 + CC2780B178EFE42D02D8E1FF4EB65216C9DDA2D9FAD8B6FE76F94FE7D9453541 + DDA2A0CF49FD89352406F098E6210278FB62267E410283FCDDBB9AA607D7CCEF + 72258EB7CFDE4AB7EF7D99EE3CFB8193C1D8FDEB76BBD72CF6714700E35FE142 + F47ADD62F7C85FC899702960D2032513244AFCC6C409A032B58924AFB6D72C1E + 405D7CECB322FBB4934C9A4CA29E36AC47E01D643389B546028E09C2211BCA5B + 893FC814B4679BBE44F441DF4AF69200A69C76F48BC9CC2C2301DC91B8780434 + 13A84CEE3209CB44ACEA6913117D08F55B84000634B1AC71C70860B58BFA1610 + 07A42F1FF14C1238FAA1ED7DA28DE4FE0801DC2211482E90B84049D91868C738 + 4A7EB04E1B82E793FD463DE3E091C0BBD740DC7DE0F1181365B51590C825C11B + 017947F4E6B6FAA14EB28A24AF12BC4E625DCE441F751DA996E590511E4943C6 + E2638449BEA94EFD50D778B465BD058D11DB9BE8D3494FE8EC7CE2BC1602746A + E702842408509C77400952F84CADEDEFFB0D0430C8DF6DBC1FF7F6B3B4FBCE47 + 69DBB079FB69DABAFD71DAFFE0AB74FED18F1C7B56DF7CFBD3B4F5CE6769CBCA + EDB73F363C333BEC06BE9F36F0595F0791FCC0DFBD8B3EF53C651212842E085E + B377A217BB7D6D3CC7F7D2B63F7EFA51DABC8A9DBF0F4D8E770D3F48DBD72C9E + 13C7F0E7B9E848603F7EFBEC6709E06CCB7388B1F09C7A8C32AE08E8876C5C8E + 3A6C0AB11BC95F10B1246349C82AC1ABF5266CECFE9B2AF13D46E93F8F09EF43 + CE04B0CB6D2CF0A3BDC72F31705EF4BB99EDBB7380CF098FC7869CC7EDB078F4 + EB50AE0BF89D19D8878FB5C8C6AE1B2AA74EAF5D43E0B589800CFEFC8309EA2A + 8FF604E3B16F05641CBBCA50FA3984BFC1FFC053F480DE5F5A7D118845A04D9B + E84F1DA03A02F7AD55DC1FD02EF78D08DEBBF4FED2B21D9247E8BD3042E313BC + 277BDD616DDC7351CA7DD9EB724FA59F0272F6D3D203F46FA165ABE38EFA31F4 + E61847E33B8001CE89E21CAB05B58BBE94C77AB48988733DA2650B201EA076B5 + 1EE6B9793E9BDB2C756E4CF9E4D0E21A288B765A2E0A8D45B4EC80962DD1B3E5 + 3A646F768DD16AA3C4DA047594EB5853D85A0A8F7EDEB0797E7D04748917E310 + BAEE0148F2C25E019B4A0083E445B9334E006BDDD78152D7F6D8FAD26D502F7A + AC23F1CA9FA9AF19B31CBB9F73DF283BF29704B0AF8BB14E8E8F80A60C650BBE + B62F6BFD99357FAE3BC92BF219F2B7D8AEAABFD8F774046DB46EA5C65C35202F + C17C859611CC6128BCCF05F22BD1BF65370BC47BF968E7EA66D1F23D295AF115 + 8BDA7DE750F2D7319FED18C8A7F7E4518F7AC9E333C7BF28DC4F7989318E6219 + 0E4341BF7968F93E2F5AFD002DDB7968C509C03905E11B7D209B6ED8674D5BD5 + B31E415BF5D152E57321A46BE0EB3A2EEEDBC19B47407FCB50D25731661775F9 + 8BB455C0B600362BA65BDDB4B6D8F89760D12FF12B88A57E840B620902B8DE3C + E20DC15F189FC95627775BFDF4B0589FF5E61311EC66C6166C40FA663D2E8A56 + 42BF6280DFBA61E34C9AD84474CD261078B7317702C367ADF83ADC1FF1EC8686 + D8E5064B7D7D076E4195C79BF277003A793BF9C42C4F9AE3E479B949F27CCCF6 + 43E4BED4B6D5AFCABA18C3E3D4BED4FEB4C07EB56FB65B7D03EA3F0F2D1FED6B + 081CC322E81667167713042D1665562AD95B8860D6FB842E4A90BFB678DDB4C5 + AF90C06E5316C30EC40784FC55E445B2D5B1C0F5C56E5E34B76C092EAC5F26B0 + 40F785BC10C0990436D898570A01BC6258DBBE9CA6BBC7667FD9F497D2FADE71 + DA4022878F3233D974FF5ADA38BC99A67BD76C916FB6BB5732B11B48DE8A420C + C36EC551922D6A63B109243B62028460E28488326F1B48F892C865A904B0C3E4 + 7DC2B70F90B66BFEEE33BCE7F7D8C1F789ADDA79808E7ADAA054196D26873961 + C7A41A936C0A26E1505F298F89A61D7D541E63D01F68251209FAA93D41BF16E8 + DFD211D3B339C6F4DCAD9A3CF6C7564A02154052352662A39EA09D42F544F547 + B2998972269C91848E90713146ABAF16980407867C23018C36116D351EC64B22 + 600CB423D10028D9A084434B46A0ADC42C6D2220571F8D19E341869824566209 + 90C40594DC558293841E893E927B68AB7FAEC3369357F1F1CED4510F74A45A47 + F402689330547FF57132AD003E59DED7694CCA14EC5B7D5057B292FD6A0CD4B3 + 1D7C21CBE76BFBEADDB453803A08633C965989DF4CFE82AC05F97B2FEDDC7C90 + 766E3D4EDB207DDFFE28EDBDFF59DA7EF7B3B4F5CEA769E7CE57E9DCE31FA70B + CF7E960EEEFF206DBDF785C93F4F9BA6DF30FDE6BB1FA7ED77B01BF851DAB058 + 9B370B017CBD10B6761CD8750AF2777AE56E5ABF0CE217BB7B7309C2178F7AC6 + 4EDFB58B66831DBF571F9BFEB1E99FD8389F3A219C1F190DC20FE4AA9DA3821C + 1BE7237F7F490802F98F07B98D721BC429C8CF8BF9DCE9395F04BDCF117592A4 + 205C11D362F71ED75D40121825EB113D9D8D97BF3790CB40B7F3197F3EE80860 + 1C838FC3C6A04434CF81FF76D02EC7108F9BC7B4086ACC3236B6979129A0D3EB + 45DFBE6B47408FEF37FE5C02E073D76B0E4AC66DC527380E40AF4900F5D187A0 + 0EB1F55A4D59F4ED5DF38A0D7D151A4365BDB6C36472BF50F0FE827AEBDEA288 + F71F6DB7E42D3FBDF7E57B6A81D5FDBE2FE8DD7B695BEAD196F613E90FFDAC02 + 72CFA66E08F36C387E05E3EBBC20EAD60C3ECF11F0CF72407B6ED49E93453BCA + 6289791B4BFA2AA0A33DC039A2DA479B08F5A1DFA410B991F48D75950D81363D + 02B66048EEC0BAA1D41923C68AF221D03E0204B0AE37B8BE60A975AC5550DF3C + 63E7C54ADF216B639C5A7D7DBF7B22518CA36D8DC7988CDBD9D95AC4644E001B + FC11D342EE2A286FE9E3BA30CB519F5D9B72BD5A7DAD9E495E5B2B95F5F504C7 + 0A82BBD8700770446BB7EF323B8057B1DEE7DA7F044AFE4602B892B92D19EB01 + E897809DC62501DCC26911C080DA2F0EC4FBF6D1CEDDCDE600C730661FE3B6B0 + 8CED77099E7F661E9CF9E8D2AEB9F246CE7C14934CFEB28C189357141EA3A97B + 8385D13AAF904DD6F1799B3EE87AB6013DDBE70278B6C2B7092FD7E7EF4E07BE + A357507585F08D78B303F8158092BB431FC4905C3FFC31647B2189EB97E00D01 + DCC3497600F3666165DD9D4BDBA50860DC8CE4863404F63780DE8DAD8CA57FB3 + EBFA59B549983FBADA26508EA94DA4CC0E32EAE8E7722F212BF21AD3C01B2DDB + 01B0771FB9217F17A093B7E79B98C1AF8FF64478188BF8519F6DBBF167F9FC89 + B9EA637FCB4FEA5F6DF0F8E2712E8EFE0269087581E6C4AF2D520B79AB846F95 + 19560D95F0DC042E56F8238FBD6E8B54432545C322D851C9E0D286BE2CC6F3E2 + DC4A10A9D1CF1065DA07A17AB569D90FD90E028BF3B293B707D3F9E27EE79213 + BF843FC61964F0CE155BE083FC3D4ED37DFCAB1D490E8BB37B35AD1FDCB03293 + BF041EF5DC23750DAA479C5EF2C57D8A9D273FAC4F498A68F28475EA2354E7FF + BEB73689DD3182771E40004FF0DED983EBD6BEE6409D6DAD032B76BE72BDAF07 + A647B7D2F44CF778672D637D45485E26EC5007A8A39E3E006D8156B2913168CB + 642531E4CB7AF4A1BD627A36DBAD9FBD9D774C5DC8EF989D49A086A4EA900E18 + 4AD40235916BF0E430E4481C97E43913E74C34571DE4655CADF150C6F80013E0 + 4389706D033876EEFA253441AF7150BADCC031FB380B01A0C4018985215D2420 + 54AE2404E2AB9C760AEA00253286480DFAB1DDB2479D6D27A9042433512AE9CB + B6EA215702D889AA406C913C25A81F227341B2F9238C0DD0C14E1F19ADB60049 + 350575B1DF960FFA8B6DF40DBBD8B78E1F250960007590BE287780529F1E5B1C + D4AFDF73E297407BE7E6FDB477FB51DABD8DC7393F4BDBEF7C9A0E3EF8D277FE + EEBCF745DAB9FB753AF7EC27E9E2A7BF4C478F7E94B6DFFF326DBEFB45DA78EF + F3B4F1EEA769EBBD4FD30E760CDF7A9CD62DD626C8DF5B0FD3CE75BC9FF69E63 + E7F85EDABAF2619A58B97EFCB0ECF07DE070C2D764BB379FA6B3EF7F9E2E3DF8 + 81955F9ACCE25D7D9CB6AF3F4B533C0EDA1F7B6CC7ED9FB91DBBC38E1BE7C0BF + 0BE53B857381D2900960D871B7E8DDCEAF9C6B7E1E8AA8E3E7C7739EF5A6B338 + 4EAE1680C025018CBE00AF97DDC0DC19CCF610FC3761DF672741CB8EDE4DC4B7 + 7EF1BDC0AEDD9D6B0FAC8E77298354B7CF17E32BB62480EB79407FE578786C99 + 48EEFEE0C06325F498EBB15B4C8E2D8FAFFB4DAB2C82E3883294F0E13543F543 + 603FF83C41FEEEE18F0656D7388CC5FA3CC43E867C5D1E6C788DE375B405DAD3 + 27FA6A0C94D0F1DAAF32DAADDBB500F711BD7F10BCB7F03E13E511D0C57BD63C + A80FEF95BC6FFA3DB810B700EB4AEAEAB111550F5BF15D3D9F63F6E31BCAFDB9 + D777033AE6A1638D3E88C9C73C032DBDC3EA3A17025A04B0EA29D73915A0F6AA + 675DE76CF421A2AC653304F6A168DA01B29357C9DC16B1AB76248E15B423681F + E58E42FCAE02C16EACDD428DD9006D48DCC63587D649D4A25E6D6DFD4242148F + 830631DC8AA525C1B88A4E8E7E6C1DB78BB51E48556B07609DA76D92B52A8BEB + C22C6FC9FABB82350EEDBA5DBF19590EFB4CEC4644727748DE03D6FB4BA24900 + 039E33086DCA86740AEA10D7DAADDCC4182219BC6A318079F99268BF1810EFDB + C76CDE2EA365DB42CB77599C66ACDF27D41CB4E6A3439E7A3CBF0EFBE023B97C + 2570958CA45C653D14A272AEDD1B8C42CF1BCF237600036CABCD287FA476CF05 + F06C85730BBCDCC258D477881FAC7C5F1F248001E51F87A0B15F47BC92043070 + D293DCFBF05BC0076DA5DBB35DF11A93BFC0B7480057E066C11B06C8D682AA3F + AD1DC0125309DD65D1C528EF2D86BCC6CCEFFD9DB8BC4FFE3AD15B6FA4DDC4C2 + F5DCE54B7DE923A28E8176AF2974B27652B4E2CE07FC66C109F0D08499F2213F + B5033A9BDC2FC68B7743FBEE701F3FEA90F5FBF3F7471750A67D313E7511D48F + D99C26B4BFA17E5B360AB5D5739AF528D1867C0C5D0C3D87C064D3E294C51716 + 62246D41EA3AD18BDDBB207DCB8EDF4A003BB16B8B578392BF4D02B82C6E89BA + 0866BC4200BBBE12BFB9EEA5F80CA1157F1ED4A7E5DBD22B40FE4E400097E3F4 + 63054078DBB8FDF1D0E53DBE2C41CA4EF68F0D579DA45DC3E39E418482D404C1 + 093297E4AD90BC445357122C0A4FBE986EB26F63144C0F905801F18B12C9908C + 9C0C9945D6B35D485F7C2E80D55BE4EE22500298E42E095D92BE63A02DB05E09 + E04CE2928C65E28D6DC2136F9298237A89B922531FCA35E1A8762D1B40939743 + 800DD1D203EBE7729FD333F91D7E48347BA2B7245C1749DC029AA405866C98D8 + D58433C0E432DB4C2C438644BA26C9B59F08268F5BA09E715A60B23D12C04360 + A29FE3F7A4FFC58E5050D056116D4838B410EDB5CD3AFC49E2B4A0F1E1833665 + F4CD04DC07D59EA5037501C9DE4CE86682176D27DD44CF7A8B008E2417492CCA + 5546D28BE418013B94D19F32FA8094A3ADDA6B495BAD0F01B1940026BA58FD73 + 50C95F8313A0057ECE4DB671D550085F457EEFEF8374F0CEE3B4FFCEB3B47DFB + E3B4F7FE97E9E8EED7E9E8C36FD2EE075FA5BD7BDF4B6F7DFAF374FCC51FA5B3 + 8F7F9C763EF8326D9ACDD6FB5F183E4BBBEF7F9A76DEFD283F06FAE6FDB471EB + 7EDABA8947391BAEE2F1CD0FD30E76FA5EC63B7FEF3BF9EB3B7AFD51CF0FD2E4 + 12DE037C2F1DBDFB59FAE677FF32FD47FFFE7F483FFDAB7F93CEBCF7595A7BCB + CEE9D5C769F3DAE3B48ECFCC8E676AC73BB1F330C1E78EE33FB6F361C7885DCE + D3CB764E4A3B23D73771FC20C1713EE123E7BAF579E0FC13D0F1B3E7F9C7A3B8 + 41EEFABB7541D8CE90C0069485D0F5BAD9FA8E6094865112189F295164EE6FDF + 093CAE79DFCEEFDE8D478504CE7F52F0E39031E8EF0B6D1EA7027EBDE32AC71F + ED08C66C5D136A5FD28E76B4199229A807704DD1EB0AAF4B7A4DA10DE4EC7708 + B469D9B2CF21BB211DAED3285B63A00FA0C7413B85DAC56322A617F3BDC47701 + 17E8BDC27D8ADE5F27207245BDDF94FBD422F7BA688376EF9E8C76E953E1F7DF + 42F25286310CE9F2FD3ACB9CF4E59C01B1D1EFD9D2AFCD31382E8E816D453C06 + 451D7B01FA21F9DB9AA7508FB235FF2138E7429DF325CEBD380F5B2D7ABC3243 + F5007D558636E4B98D18199333D99E765A1F03EC225AFA3590B8FE449B3C6F9F + 47E89250854E0960D401DAB7E2A9DC63E16941A5ADB651A6BA2130B6C71D00C9 + DA48CEB28EF59EEAB0F6F377021B4086624DE6BB80ADBDEA6B93FC0756ECE4A5 + 7F8ED9F5C1587D7DEE33AF6B6C1D0760BD56485710B22C23209FC2DF9F4C9565 + 33EB428BB5863FED963F2E6759B6D5B5A5F76763C52E60AC81519FEEDBB92A36 + EC4B09DF087F4D52C1CAB6ADDF77AC6E40DD31460063CD5FD6FD4318247E09C6 + 609C569DED286B602CFF01447D44B41FC3B2F6EDDCCAB703CDDB31E7D9B26BA1 + FA6A4E35A069DFD0B774DF75300F1DD1CA592F862EC7AF04A3128D3DD21108BC + 05FD08D5BDC16298397F7A1E5B3C91F0472FF69CA38FC2B929371710B9BD1ECC + B72917689C353CDA59623B2AEFD7E13408E079FA5709AF2C010CACAD6E395ABA + 21F43EE008FEEB00A832FD02347E14AF13E4077C6A382101EC3710A0B4FB76B8 + B8008DFE1CD1BE012580E5A6B52C3426C85E7FBF2FC76CF5894D1A9C14F6BAE9 + 41E0A13D6F32318700EEF5AD7EAF2138B97A1EB4E28E033E1920EA7BED32011E + 9A3053AE3EEA17ED00F4D98DD3BE13D52EFBB92CF607B2D3894B9BEC4FF1DDE9 + FA543BE887E484CA4F0B31B6F6A7BA569D18FA47ABDACCA23F8ECE27239F679C + 8B7C3EE803F2177DE9228F44267601AF02A59D6599FCED48605B9C1ABA77FFDA + 02D71F016D32DAEBEEDE008F09BD10C02BB6B076D8227A8CFCE5C2BAA53B0918 + 8F0BF5D6C23EC2EDACF4F1D77362B14800831C06F9BB7DC50960DF018CF6DEB1 + 277D563CC95276B792FCDC818D9D572BE1E300915BC8DE55A090C0154CB22079 + 4014D9A492B81999FC25018C24489647E297A01E0997FA58E7190218C91F00F5 + 3EDCBEA0AFC3B12371540062D7CFC10D936752B812C3D676B2B7ECFE55021824 + F2F4E8465A3BD3DFDD0B68F28DE827FA24116775FAA98E32CA1943A1315A3695 + D8A51D4A9113F5BDC0C11F72EA723F5632C97AA19F6845F214F29848A67D0474 + 9AC06DD92259CCF7FF2251ECC9E2924CCE09E5DCCE288970AB4FCFE531781F16 + A747C696F1012A639F2D3BB55D3F5F4AA224DFF11ECD0DECBC2BF0847DD10131 + E94F9038501241FDE84B3D090DFAD35E6D54A7A01DA13149BC28B9419098D136 + C849DF915AE4D5B780E42E404257895F10992849F866B237D7FB801C3E993023 + 4146A20B7592B524B65A2418DB2A6B918268A30F952B594B3FC6D07804E34619 + 4AD8139D1CF502936F9B0CBB21717E21E379745218B26B40B7EB97C0A39FF76E + 3F4C87EF3E4D87EF7D9CF6DEFD341DDCF92A9DB9FF7DC30FFCF1CFFB0F7E90AE + 7CF1EB74EDABDFA4B38F7F62B2AFD3F69DEF7909FDCE9DCFD2CEFB1FA5AD771E + 3BF9BB6ED8BCFE206D80E805C97BFD918DE361DAB89277FC768F7D0601FC28AD + 5F7998D62EDEF35DBF7FF36FFF37E9FFFCFFF8FFA6BFFB77FFFB74EE832FEDB7 + 6C9FEF956CB761E71844361E653DB5639C5C86CE8ED364383ED4D72EBD9B2645 + 064C8FED33BA6AE7C3F577CCE77DD35BDBBF4BB3E0E7C0EF0D3F57E8BACF8176 + 5622163E672180D1DEC4F702A42D88589381BC457DA7EC08D65DC12477D976F2 + D8E2FAA3AE81A203E18C9DBF7B371E08016C9FA38CAF17C7C766E7E842FE4DF0 + FBC3EF2DBF9F04CF01A1BA6AE3C031E6DF35EA79AC9D9C3294BC66A82CFAA2A4 + BC758D0178FDA12DAE4BB89EA3A43DFD35DE10188FF5A82768A7F68C8DB6F64F + 1BC878DDC4751DDF05CA3966F553C00EF171CC6CB7E031708F413F80DD57F263 + A13B38498AFB1EEE53E837E81DF15E3507B4A70F4ADC03FBF762C4CEC748F09E + AB7FC072141DC95FD5A1EE6D03FB01306EF69D77035BDF23F30685FBB4E60D36 + 176901B66A8F3AE44E0E1FDA9CCB4A9DFFF89C48DA9C6F691BF3AEDE3CCDE49C + 53C106732E47999F4DCA1363008D990139E29000CE7684B6A32E22CE1F59F731 + C3176D9F0B674C0A914B62D549D5B25357650465D3233B1F06EA2967BC999816 + 2FC6D192EB07FA0C21C6ADBE65CC8A8E78ED48D908E8481493B0759FB203D81F + D55C6CB33E03AF0A22BA754BEE0BA0BDC65CABEB1E6B838C7580E4BDD87B0473 + 9699ADD741CA6619F5DD9A10EBC80C10C099046EAC29B13EA4DCE2710D8A3E11 + BF8B071FAC713371ABC46FF72868D43332119CF53DC23702EF07DE20195BEA03 + 58DBC29A36E7067A3AF725D016DD8CCDAC7CB580366C03CC6BA82CEA86D0F201 + E6E917C7D12B815EEE0EB9CF924F5B14D96F38573A63FB060B43F3E1119AB35E + 0A2597DF232023C85704DE823E4DDF019F1361C8FF79E38EE1B4C6AE18883773 + FE688372157CD79CCF670C037D2E0EF80A0FD7408BDF231621577BB1C8F189AC + CFFD652801FCAAE23409E6579A005E5DDB326C5A7D3112B8F7E13AF0810BF0A5 + E317B7CA8AFC75277F81657F944EEECE01C95045EB82DF026F20E2DBBFB12056 + 1FAB7693077A7108E97FD91B14ED17017DB82358C7EE0470E3263A88702CAD7E + 9A7EAF0974E2F5F22660E8A3C0CFA5B417447BB26C93FB41BBAE9E1F3B6C98DA + E2C0B0B65E08502C1C26A637A08E4786FB63C351F747846768EC7E1F2F0E8B2C + 26D44617205A8F18D301D4478CD9A2BE82EF91D5574816DBF95EDBB4C55D252E + ADED8B3DC016B1243249E4D20E25094FD33B9CC8855D295D063BD4E700765676 + 8BDE61CCF8B6E0FDB663CED836A08B75CA621CA2DAB14F83EB405CEF96C53E12 + 01F8A73DEC04FE58E68342046F2341716CB2E34CFE82242EE4EF1A7405990C86 + 9DC98510AEC4B000F13C26FE61BE67B282B5FD8C5C47FF4884145216499200DA + AF581C00446F576792C7C6EDC8891EC8F93EE016F99BDF175C124D04DA48541D + DDF05D11683B418EE412924C826C773DB74BB9825D0192685330B1A7C9C2A843 + 3DFA471DDB00938F9E5C145BD53371C9F69A02642E4AD85B493051B98A3E310E + F8224E81EB34918A046F49A66A625593C800DA0413AF04E5EAAFC08E212780AD + F4E4B1C97C471292C4C5A6C636BD27A4312E414D964BF2BADB699593EE9EA476 + 5F8B8978B0AD3605C55FE1E42F12F88656F29FC97DD6551EED019005F461E21C + 507B1222B4D3927ADA0E01B6B447E92405E29636E48CE33A276E325153ED0B99 + 0322D877AACA237B41E4A2E4B9E1EE561004A83BD966804C09D6AE9E11895725 + B3684F1D492D056D22604F7210802DFBD27E00F5839DFA41AFFD1190AB1D00F2 + 516D282329096C8248BC0442118F05860E63CBE714C804308853C0744E067724 + F0F68D8769EFD6E374F0EED374E6834FD3C1DDCFD2FEDD2F9C003EF7E08756FF + 269D79F0A374F5F33F4937BFF98B74E1933F4ABBF77F92B63FFC61DABEFBFDB4 + 7BF77B69CFECF7EF7C9276DFC3FB831FA5AD5B0F32016CB1376EE0BDC0D8C10B + 1217EFF1CDD8B63AB0756CBAAB8FD3F4CA4327807FF497FF26FDFD7FF37F485F + FDEE5FA69D5B9FA4958BF7D3F4F849DA3806617C3F1F23886C3B8E75ECEEBD6A + E7EDBA1DDBB5FC98EB1E296CC7EE8F8BB6F3E0BB81ED1CFB639271BECBF903E2 + 39CEC8E73203E71B9F2DCF69965386F3CDC74A6F5C32D965EB0B253E1B03DA1D + B96B9F87C9B62F9BAEC85C6EF6007F27F87DA8DE61E3C758B0031824F08E9D0F + 8C45BF53FE38EC6A8FB61D2F48693936D813D15FA1DF630562FA75C1E2FAD8CD + 16C87F64E9CBFCF75D8E09A8BE06EAA8A7AE93D1CFFAC535C1C038A86712D3AE + 2D654CEAAFF1E80BB08DD21174F3C07E60AFD750D401D4190FF7929503DC1B6F + 5439EE49B4A51DA07EEC0775B5859C705BBBC7107CB2843F5D82F737BDDF9DB3 + 7B02FE8864F50CD6BB1804EF8F5E2FFD137A8FF17B5CB1653F5E967B62B62971 + 68576C2AA98B7BA841896192C1BC3767FB8E10F671DAB8119B327FE583CC0738 + 3FA04CDB94A93C12BEDAA62DED21F7DDBF80D539B76A21CEB35072CEC6B958B6 + C51FEB6C1E86F6996B690A9DCDE3BCB4B693B767F2BC0E7335F8AEDB58A618A7 + CD3B3DCE191BCF21FAB23161EC363ECC511DA6832C93C90290D884C5E4BC72F5 + C8E6B4F227C509E6AF98E7FA9C3993AF9001245395589D058857D81CDB316512 + 18F68CA3F1088DA776AC43CE793C405DD76787E8AFB6EECF474CD31EEB890225 + 6359EA239D23098C358AEF8E758237DB66F2166B2D5BA7EDD8FA0B24281EE76C + C8BB7B674960C06382FCC5BACC6176209611DB7C1113F6FEBE615BE7E09DBC93 + 1DACC1CCC791D76299E4B5580EAC09B1FE3B676B3D12B4B6E6F4B55E077FB551 + 9181FCEDFC33B06624D99CD7D758CB66E296E42E90495FEB03F502D5F7085F8F + 11649B160F3997CD33195B679BF09DC50D79F5DB28650BD499FDEA6606DACC6B + E5DC9FD599A328B99BA19C06E50AF755041FA2E7D3D02F8F7C0CAF1256368072 + DE0B5CBEDEE5FAA24F0FCCA336ECBA5CE12C066D90AB1D21997FDFE1E7D3BFE3 + A53E825E4EBB40659ACFCF39F0DC9EC1EA5E07CACCBEFA35798BC2732CC375B4 + A03134562BEE49FBD23EB47DD2784358369ED92F43FCCED89ECA31C01F71C0C5 + 6D5B5D61E35B9DBFCB770C337C60207B9B30BB56AC88358C8D1C6245EEA7B303 + 7FB92024F61894FC3D0D7CAB043006D09257F8C9599600D60FA47C49E317B527 + 0FBAD719AD631D825F6C71619E0392AE8A965D0B6A5B7CEB0D04E8E90D45176F + 303D149F6A57E2CED805D0BEFA8DC8DC073721DC0871732C37A34CE695494AB9 + 692E847A331B90ABEC350227526B36710338D924C1F92A4327C73AC91EB2AB13 + F24D5BBC60A1034271620B84E939FB4E9C4FD34D5BC459B96AB2B555B35FB372 + 0DE5519A08F9AB0470ECE355C2A28B8F213B95651B9CA73E56A766A380CCD0FD + 43D7EA1576BEF55DBF2E838D7D6620D8ADED04263F1B03DF6FAB84691F66D794 + CF02B1635B31249B0B92B1568FFE046D631DD0C578D42968E30B7B2CFE2B8ACC + 8064852FF44BD221FF3BBD5FF77F80232616FDBEF02FC905241984FC65D261BA + 8BC7A11D9BADD918480AB3EDB23DB3314CCB2E632567159E984132AA2491F2CE + E14CEE76246FDF9FC99C2CEFC72394EC6D93BF00763B5C4DD801B1626D26C5B0 + 2B8209BDD5431B97954CEA559CED27FEE057FDC59E3A2D158CD74A2C2EA20318 + 3BDAA1DEEDD8CD7DE1B184207F279023E16875C8564C4FA01D652B764EE89B65 + 3949CAC4AA265363529576480C3369DCB25550DE4B309F3759A9B7EC690B20C1 + CC64B3279C91B40E496F454D7E6B1B7549D64730B14FD297C97C920900DAD1A7 + 97EC2FA08C50A28063035AB6ADD804C7C0782D7DD797E941F818D62F827445DB + FCD1BE04421684AFE9AD8EF7D6424EF216C8EF2FBDE776F02701051BDF1166C8 + 641BDA386FD986641BEDB36C96BC828C24E9900D7451464462977212680475B4 + 8BA01FEAD12782361837CE0D483ED4A1C37850524F78CCCB1FA6CD4BC03DB3B1 + F81ECF3E279C2B9C7390946647F29704301EFD0C4275EBFAC3B47BEB493A78F7 + 593ABAF3593ABAF779DABFFB693AF7E0EBF4D6A31FA6B3F7FFC0EA3F4DC79FFE + 36BDF383BF4B57BFFEDB74F4F4B769EBC1CFD2D6BD1FA7BD0FBF9F0EEF7E95CE + DCFD3C1DBDFF49DA7FE7A913CADB371FA5CD5B0FD3C6AD0769C3EA20834102EF + 5C7D94B68FF148E88769FBCA83B475058F897E6CE330BFB73F4DE7EF7F3F5D79 + FA138BF74DDABCF9719A5C7B9A366F7C9C766E3EB373F220EDD9D8717E40026F + 8184BD66B073E1C7E8C76AE711E7077A90AFE5B3F09DB4B0C1B906215BCE2565 + 849F5307FCF0B9E4EFA562FB6AC6D6B1E98FED3B6D3638DF7847311E71BD71C5 + 3E37EF1BF1731FFC3D38418B31A12C042F770D677D262F2301CC77162316BF1F + 28797C7A0C6E6BE078FD9C94E3525BC69BF11728F15BFF5C41998FDBEC1CF8B3 + 47D1C31763B0327F1FBB63AEBEB043693179FDA03D903FAF726C66876B4EB5B3 + F3437BECC6863EC72DD7D6720EEBF5AA00F5FC27916CC3BA5F178B2E225EFF88 + 7C1DECAEA7BCEE4247193EC3736F3F4EE7DFB3EFAE7D1FBDFF00C6A00FFB8BBA + 3806B7C3E70132D400E277CBCE01E0247091EBBD8E76D4A3F47E8B2D517D5097 + 31007A8F01F14A82577DFD9E8CD2F4EBAEEFFAD7F8BC77D73F51153BBEC3DF77 + F6161BBF97E3FE6CC8F7F75C2760EB710AE23D1F65256E653EC27987823A602C + 1E007B9D474550D742DFF67A5AB7B913B051EAD3A34296A27E06F3369B9B95F9 + 2488DB099EAE62C09F0CD6CEDAB86D2C2B78DA8C95EB67ED5CE2D1D8A69F1AD6 + AD4EF86B390A268716E3D0C609D8B9C11F149D18F539768613CC459EE7CC685B + FF0E9BEF953936F4B4D3B97786CD69EB7C1A403BDB3146F4EBE6E859E7A471D1 + A14E39FBD431463026F40AC8DC06E4AAE85B442CD7474024805907741D8532C7 + C11A0A6B275BA715029824707EE54DD797D673BF56DADA097FB8F5274DE1D1C9 + 7BB6BE36ACED9FB3BE2FF9A3A7D76DFC78FA12FE94EBF65BB6A6D942DB8E1D4F + 5F2AC79089675B43EFDA1A7AC7D6CFBB188BAD2FAB3E00E3B0B5A2BF26C8C642 + 604DC95DC02080BB5DC591E04589F569AE73ED4DFB28CF401DEBFA1C0324727D + 547403338F930E7207DA2078032A092CF66B20C74D97735B9900F61CA0E4BA1C + C875201761F58A228B503DEB1A8BF920E44CF86435D5BF1AC0189F1F4E00F3BC + 13904B5EB4E507F8E69C02DF6811F43157490CDAA1BF922B56FBEF2A626E3C82 + E70AC4EF5A418F04463E1F32DA96F622B9FA8CE2D7E02E4EB463B58521AEA425 + 5F865B215A3E942D1B6B1E4E3B1E51E2CEEC147EDE63A8643FDB3B56221E0119 + FAE888D5F950EECFDA4EE88A7F247B7B80BED819FA9C6244217F9B710CD5AEF0 + 9773B118BF19C9DBD3C0B74E00032D9D630902981F4CFD6246F08BF6FB8C658E + 75E682BB18866E048E2588D8DC2EF6247187E236B0A81D41FB7970FB1152168F + F46DC95B189B883864A2F33A8293A84C007713D54870BE2CF427A91DE6D92D32 + C9E6318258AC8F020659893608CDC9D934DDB890366D81B6BE7EC1AE6B67D264 + CD26F120800D20829721805FC4C41F3163DC31599447A85DCBBE7F7CA69BDA22 + 6EBDC3EAD4165803F0736B36247309DFE98BBAE927B648DCB605FF16C832213A + B90B78E28FA4EA88D15341216BC7A0C46B0BD57601F21758366E4BD7238007D0 + 251BFAF55EBB3C82CCFFCD6EEDFC6FF32B3DD257EB208001257CA77BFD76957B + 82A84BCA4450A73635C163FE4CF254596947590476092321D5267E15E8D76215 + 90A445228F246D246E35D1C736EDA22D652D9D8231199750B9EAA30CB1A36E26 + 8E95BEF337608C00EEEAD16E310258F54C166B92785160F79127984B5DFB449D + 31DD06F50B1935B95CDA402BE9AD3A26F18792F644CB8EF59ACC37441FCAE94F + 50AE809CBE181FED189FFAA1183A0EDAA82D8907B449EA02D38BEFFBAE4AB641 + EC4C0A1104ACD9E70839C836124D7977E587E6FB9EC7829C645425872EE79DB4 + 884F3D7C5967BB05925D2DC20B501DA1FEE857ED288F7E3146047C58A76FF451 + 39CE090062CF494EAB230609F068EBE5652B2FDDB7CFC860752722CD767A6CE7 + EFAA9D4F0044DA353BFF05207F81CD6B0FD2D68D8769E7D6637FFCF3D1DDCFD2 + E15D10C09FA5F38FBE97DE7AF2232B7F62F8C374F1E3DFA55BDFFFE7E9DAF7FE + 2E9D7DF6E769E7F12FD3CEC39FA583FB3F4C47D82D7CEFAB74F6CE17E9E8BD4F + D3FEAD6769F7F6537F27F0FADB0FD2FA2DC38D074E006F5F2D04B095BB0610C1 + 9BC0F5C769E3FAD334C5BB7EAF3F499B373F4A1B373E4AD3AB4FD3EEAD4FD3E1 + DB9FDA39B171DBB1E071D70A9E4BFF8CAE648214D073455046F0B325689763E2 + F39B077C57EC33B2EFEBD4CFBF7D0EF6FDC6FB8A37AF600C791C889DBFD7F9BB + 55DF410CF2D43EDFFC1BEB1E95EEE4AD1D8B13A1C59663C6F8F8FDC8E3CCE031 + F8B8ACA4BD0276791CB0EBFC281B030960ECA08E3120AB0471B177DBD0AE310A + 780DE864B0CBF6888B316BACACCFA5F69DE5760DBD846B13FA844D21780B54E6 + E46E694F8A1EF5085E1389D675325E739D903C7B335D78FFA3F4F51FFD75FAE1 + 9FFD73278121577FBD1EA34E39411DDBBC06C7188C83A731F0890C94FBBD0AF7 + 52A0D83970CEC58EE46C25698B7DD5175BDEFF00BF5FDA3DB4E757EEAB6C2B19 + 4DF8EB160CF59E5D7C08BCDB37A3CC07E0237DF35E1D0152D8FD4B5C404961CC + 43381FE19C84F5089D8B006A8B3ADB88C77954732E55646A1391F599E4ED13C0 + 203D332620F848FEDABC0AC4EEDAA1D5F15A91729C1C0F1E4D8DF7229300CEAF + 1E99258233196CFA03EBBFBEA6C4004219736ECCC99DECB5FE9C5CB5799EB5F1 + 5A1392C6F9F5266637806C9FE7D99C5B4F0E01EAFBF3FC385F47BB8B65BA9D4B + 4ED682005EB7DF18CA69191F10FDEBE39D4BC93E6ABCA0635F2473958C8D887A + B675EDD4D960EDD491C044DEC1DB11C0EA5BE18F67B6B82480F76C2DBC6FEBEA + BD432BCFA6E9C1C5B46E6B12FC9116442F9ED2B4BA63C7B66DC784D2D6624E22 + 7B3CF46FFE7BB6C6DEB3757A2580F1B427E842DF0210C05C4B9204C6FA12EB4E + 5F5F96F526D7A211D447B46C336C9C2080B183186314627706388E9D590218F2 + 1E4C3648006F81F8B5BE1CE72A01ECF09C07617EDE365F00F90E82B2964E6DA4 + 8D5800F32588FBBA12C0AD5C6113D8E9EBBB800590336F3B40022BF95B49E060 + 03F4FA2A18D2AF92700444FE5D452B37AEE0B902F13BB136807A3D878631BF16 + A25DAE5B795A846FC41057D2928F712B433ACA0D954015D98CFD4931146F4E3F + 0B9DD7E25FC71FF52701789FCA15959843E3F77A24560D6CBBAE1CE79AF84E4B + E972B51D02E26E1AB6B2ED189AFE826ABB28010CCCE73895B83D2DBC120430D0 + D23F0F01BCBA9AC176FD62FC3E639963950BEFB2D00B35D1B26B21DBB30D3FF5 + 85CE6ECE20C9AAAC8D65FA0474ACA3C0788494D589C2D0A4A285A189460FE847 + FA7ADDC0E3D3093140E2EF4580F1BBC9681F184744CB0E68C56E21C729937DC8 + 6C618277EAD4DDA6202BED3BBB363D9736B62EA62988CED533693AE908602581 + 17ED7719E8F18E01B6DAAFCA5BB1866C80F8FEDF68D33F3ED3D9F919238071FE + 00D433015C20E46FAD1BF6CFDD4AB7EF7C9C6EBCFF2CED9CBBE90B465F50163B + EC001E5F5C0A16217649FC37740A5DD88EA2E1C3C535D1B30F887D6A3DFAF762 + 86453D909310398991DB5D92836082A29690E35FE085F05DDB39F6D277F916F8 + 0E60241FF0EF733C966CD7E47B3951110960401330F3C0444E2FC9B320D437C7 + 832C93BF5D920A63819EC83B7701256991C043D9226E5B32DAAF9458AA233439 + 480CE9986C9C9764048662C3B73E02DAD0227EC7641DF98B1249D28E008E4953 + 26631563BA9A142E6DB5A34EF5633633B6A86B32BBD40926BFD9EE259BAD5D93 + E90626ECE943A84DCB8E3222EA5A507B2504C6EC22944400A20FEA43E300C14B + 8286E40EEB206740EA90E801018C3A8935C8D41E3AD4494EA98EE430763BAA8E + F516A28E6DF68B7E300EF6C7B6FAA1CEF1D28EB66CD36E1EB49F28477C929524 + F4D807F5B0655F943386FB1FDF2F3B690DC7F72AE95B77FC5EC3E3912DAEBCF7 + 77FBE6037FF7EF267601A37EFBB1EFDE05017C0012F7FE37E9ECB31FA5B31FFD + 3C9D79FA87E9F0D9AFACFE9B74FCD55FA7CB5FFC553AF7F437E9E0E1AFD2DEC3 + 9FA7BD073FB2F20769FFE1F7D2C1BDAFD2C1079FA783B73F4B7B6F7F92B6DF7E + 96D66F3FF277024FADAF8DAB0F7CAC208041FEEE5D7F92F6AE3DB6713F48533B + 8EC935EC167E9AB66F7F9C766E7DEC84F0FAB1E9AF3D335B93DBF165D2B53B8F + 04CE113E63D4E379D4F34B1DEBEA1FE52412898EF42DC00E5D2BA17302B410C0 + EB57F0BE61C811A7FB0C41283A316B7EBEBB177DA3F447A1E7DF240960EEDAE5 + 78001E8FCA51E73164E4B1E5DDBF9D3D011B9E2782E70E25A03A85DAB18C3AD5 + 6B9B325E1788561B80BD8E99E550EC3C0EEA103313BD4AF6667D96F3338D760A + E8F551D28A784DA48CF705DC23B0EBF7F11FFC71FADE1FFF8D13C0D8A5AA31F4 + DA1A63D226D6FD91E6A50DE8355AED288FA09D96AE2BF7C64ADA1639EF836803 + 68FB1333B0E3D68E913E95DC2D71EA4E6383C7870EB10C7CD5C2DA79BB4F17FB + 31782C1C2BFA2F6342DFFAD8681F2760F67AEF77798903599C67E8DC44413D7D + 218B7319CA5AF3299D8FCD9B9BF9AB3550DA7C695D08DF0C9319407282ECCC76 + 16BF90BBEB676FFBF9C15857F00868C8ADEE6303395B88DA692180159904CE04 + F0EABED93A015CFAB132C2C9DE6A67719DFCB539200863999BC39631B8F39773 + EBEEB830BFCEF3F008CED15922E63A1E5D6D255E07E3E4AFB54156F31532B9AF + 463CC6923502DA1E0B72C802398C3AD639DCD5ABEBA079507BC600F411D011BA + FE027AEB3390AE36BEC936D654366E3BDE89C59AECDB1A77CFD6D5BB282DB6D9 + 64F2174F6942DD8EC3CB824000FBEEDF5D5B9BEFDA9ADB7715430F82BA1B471D + 0F00DFB296E4EE5F5F4F1619D79A80AE4D755D3A84967D071BDFB68D17642E1F + 093D047F04F43C581C7FBCB39DBB4D8B6D20593BD942DEA083EB25BF31F1BC47 + D70694C87540464459AC0B5651225F62D0F890BF5AB0318EA0952B54789E95B6 + 24DE37AC1D0860D8B9BDC4065A043010EDB4CF31F4F2BFA5CFEF3AE23989E067 + 74520278BEACABAFAD9BAE1090A74B440EF02543B2216E652C4E411DB7C866EC + 9784C744BD156FCDF4ABA7D04FF1D7F3CEF2C4F00D7FE57B3229B1FC1840A0B6 + C6DB20574B7B759ACF2DBE23FE3D71E4761D270960F19B05F40B12C0403386C1 + 741D2FB90801BC51CA9311C02DBB65F04ABC0378F04096208081190298FF0858 + E447B088CDAB8E72EC4D5D44B9E82E837C61CE6544B45D0CF942DF4165D1F6F9 + D01A7313F146281389A5261566CB7F0B7242A2A8B6F8875394BD26D0E3C1C490 + 84DF49B0AC7F7F429AA19366020465CB165824269063E1F3C4C4FD5CDAB605F3 + 95779EA44BB71FA40DFCABD964533C6E18E4A593BE677BE4EFDAAA4D909400B6 + EFD922FD2E033DE621CCF36BC9635D41025865389E689FDB7DF2B74900435EEA + 4304B0036D93EF9EB9918E6F3F4C97B09BC916FF2067EB621236FE78A9B2C0D4 + FACC02D3B02001CC18A7851ABBB4B9A8D6C5750BD4457FD5AB3FDB55AE090603 + 120848402061C10486D623A8C33FC257B74CB67D394D778E0D1DE9EB25885E21 + 8049FE4612D8DB85006672868924266A28573DEA9AD4617D488636FD5966D838 + 049AA4CA892A935BB952085D257699B88B64AFDA515E137E565F192080A16702 + 51D1B269D9B28F16E00B1B3EEE39CA1823BFBBB70F257FFB68DBF5DB4C4A7689 + 5726296B12D5DA9AA405987C25A8A37F538FA4F039B3411B7226861D25095D6C + 95006EC113D7011E3F249EC7ECD72FB2CC09744DA4D307C96D257221A32EFA45 + D097C97CCA5846B01F104E2A9F17AFF383CE807A216448C0781D3B820B894392 + 067592389429B90380CCA22DE240C712E41DFB511FCA0895B31FEA58873FC933 + D8B28D923668EFDE789076B1E3148FF22D641D6C7C5726C9BB02971B545675A5 + 641FF9D1B6D91E3292933B56E76E568CC17D4A3BFB651FF867FD07E673CFC6F8 + C04AECA8C53B75B1ABD78EAB0125809DF4356CDFBC6FE5A3B477FB4921803FF7 + C7399F7BF8A374F8EC27E9F0E9CFD3C1B33F4C071FFF3A1D7DF427E9D217FF38 + 5DFDE22FD385A7BF4D070F7E99F61EFC34ED3EF861DA7BF40769EFF137D6FE2A + EDDDF9321DBCF345DA7DFBD3B4FDCEB3B471FB91E1615AC7FB809D00B6633DB6 + FE8F1FA6BD6B4FD22E76FBE2FDC0868DEB8FD3D6AD8FD2F62D10C01FA59D1BCF + F2FB812F3F48D34BF7D3961DAFBF7BD7CE813FC619E7C6913F233E2619E715E7 + 4ECF733DFF866A63409DE79832CA4912E6EF3ABE3BB0EB90FB80BF7DC7CC7EEA + 8F80BE67F8304D6D4C9900D6EF45FEDC480077DF23C888128FDF0119938E91DF + 1B1E138F8BF073A4ED60A7F6E84B7F2F1C27F519DDEF8C805C63F2370F5D17AB + 8BA1BE04EDB4CD38EAC77EA22DEB1C6F96C33F13862472794C4AF6F2B3A57D04 + 7479C739CEA78D01E70072E811C7AECF00497B975B9BF620495107F9BB67BF3B + 5ECF797D651C0563D236CA7CEC053A0ECA889E4F01751B97BA923148C4824CDC + C4351831CA3D4AEF45282B01EC3A9337D0BBC7E25E29E808E0EE1EDEF2A7DCC7 + 63E3C4B1A34FF8F918100B31788C66EBF10D758E80B19738285D5662B7DA2AE7 + 3C0450F2973E28BD6E739C0A9BF3F4DA02CEB9B4EE733094360FC33C71521EFB + 9CE79E28CDFEC860EB1DC4C623A0D7ACBDEAA4EB8DB47164E339B2B11D96B161 + 5CE7ED33C2B84D3735F954085F94DCBD9B89E15C2216DB2057DB24B0D9147F90 + CBD8099C7703C3D6C655E6D5B0857F7F9E8D32D7BB63837C0E2C16CA0D8B7768 + BFA9F376DFDAB3DF0FCE1BC85DAC1DD66DCCEC0FE0786B0C03E7FE04F4287D6D + 308700D67590D681BC7EEADA007D48FE2A01AC981848006B2C8D99D74E360EAC + 93FC694A3676BCEBD7DF1B6CEB567F8CB4AD056D2D96C9DFCEC7770D83AC15F2 + 37F765EB46277E6DDD6D753CCDC9FFCC5BFA04E2717B8CB2962401ACEB4BD697 + 01FDB88E9D05D6F419F97DBEE79E1F207F919FC06BA336AD6D756F5B1D0430CA + 4CFE5A9F66935F3195731A13ABEB9FDE5799F7204C3643EE52A6283AFA2BF24E + E31C5FFBA8FA86ECE5E06821B472853D78FEEB3013BE24800D6BF85CE00F7DC9 + C5B6E203A74D00E71279E203CFC9469BEF1A66F2E20135AF6E20098CD2DF07BC + C4934063BC8AD52C5B9D1662AF10904493D35816437CC949B8A056AC81F8E0A5 + 9C9C6D929D8BC3CF4543EEA87D34742704CFFB739F7FFD7E50E6E70A246A6BCC + 7D8295F5D502F8E03CAC4DEC7CD871AF791DE3CCFEABAE17DF26810BD956298B + 0C76F3D08B011F1B53E52441262BD94B80F4552CC66F9E16E9AB782508E0413C + 0701DCFBE1357E843358C4E655473CEE31F007B804E245BB79E17E8958A6FF38 + EE21B46E8627C55C02B84C3466E4AF09BA31CF477B42D9C7901DE53116DB449C + 30A3BE28013C24A70E04703ED6B3E9B22D407FFEA7FF2CFDF13FFEFB74F5EDC7 + 3EF99C6C9C4FD30D5BD04CCF39F9BB6E76207EA7133C0ADAC606F2776A71AC9D + DF59D2F5F73226F4F1BCB0AD75B58BF208EAA30DCE97CA51E605932DAE362ED8 + B177646FACA374C2D7EA247DFB307F2B1D9B176CB1660B683CE6B910C3584C4E + 7D116C0B47D85BE90BCCB298E4A2737691B9381803406C42E52DB4628DA11543 + B168BF848ED5FD3C19A0C809024D008C8189017F24D8B6D59D00BE92D677F323 + 9F0992BC4A0057C2D7EBC7D6EF95FAB8392660082671D8669D72257699E8A1AC + 058D03E43868DB180CAD5D0A80EF0A3E34FDA1F55FA064AC27F14A028F3A2D59 + 8F7680FAB3643D260E554F9DEA5BF611D0D7DDBED6662C2580210779AB8F76EE + 08DF8CA1F702473BBC9B8E89D21601CCC42ADA4CAAAA8C808EC95795F500B995 + 9ECC463218096A248391142E3A26848175942579CC443793DD8A9C2CEF27FE61 + 8744B30232E8153116A17DC14E0956EA21679F848E43C1B8510EA80FFBA04C75 + AA5790E020B101D9C665F3B9D491469588C17B41B57DA54D0C91D00120EBC89B + AC27C1453980BAFA451FEA514246447F92686803AC6BDFFB371FFACE6327619D + 94B33E9D7CCCE0FB75FDBDB326073A99D52D5E45B5B17E8A1DCA2D90B3C557DF + 63CBF7F7E676D1518F71E278F01E5A3F470FACCC0041CA77FC0E11C0DE461D7F + 9CB2B9CCEEADC769FFF6D374E6834FD3990FBF4C47F7BE49671FFD381D3CF949 + DA7BF2D374F0F12FD3E1677F928E3EFD6D3AFEDE5FA7EB5FFF75BAF8C99FA6A3 + C7BF4A078F7E9EF6CD6EFFA9D93EFD61DA79F84DDAB9F355DA7FFB732771B7DE + 7E92B6DE7998366D9EB475E3918D0FBB7FAD4F90D5574002E747426F5D7F9476 + 6E3C493B65F7AF93C0370D379EDA397A6CC7F4C8CE031E138DF716DB6757CE89 + 02E7068FBEC6F9814D3DB7267794F3CCF3A7EFC555F0BB51895DFFAE6728F9BB + 63B10010D240EE1FF8D0C69A49609499B4EEC7E4F7AE12C0E8F77207FF4EA34F + D85BECDEF704C76F9F1FFEA080327F37CAB1591CF810ECC7FB2AC7D73FC6DCE6 + EF437F2B04FDA39C3AC64339F47B23184B11E3A1641C85EBC556EDBB76063F2F + 92B84AEA92FCA52EEA15D0F1BAC76B1FC0EBA55E7321E3B51225AFA7BC96531F + 413BB527B49F78BF00A06F8D65086A1F819DB5245B9D70C5EEDDD227FB8D7DB9 + DCDA208B49DC12B83FC7FB3591EFD51D29EBF7E2E0AFC884F47BDE1FEEB39580 + 2EF75D1D4FBD17973E343E4AF48F71513E04D8F8E3940D9C8FE89C0436B93460 + EE24C0535480DA2E25885E277B8BBEB631CFC2DCA9CC1333010C123393BF13EB + 778AC71D174CB0FBD5C63505B17B68E302190B62D6E6576B67DF4E2BFB36EE03 + F46FE70FBB7C41043708E0FCF8668BEFC46FB723580960CE930190C3D8798C5D + C52BFB36DFAB0430EC1113ED6E8E4D3FEC10667DE272CCABB39E7371A2DA1138 + 37B676D8B2F89FFEF04FD29FFCCD7F9C3E78F67D3B2610C097D3868D95BB83C7 + C07101580BF464617D807A9F00C51F64733B82EB26B6B946A28C2472257D9DB8 + CDF5A9FBE63AEC192B13B55C8F1532173B7C7D9D6632C3641F36B65E74F2D760 + 3227817D7D976D48EAE61DBCB61674B2D9D6977804B483E4B3F697C1E3F13139 + 4CAE6BC8B2AE8C6BCD6540FFB8F6EDB76D2DDF2272058BD8F471D64B12C04E06 + 83ECB53A00027886AC85DC302925731C9EF75098BC578F6DD60DD5BF976BEAE2 + 6B1FAA53D922BAD301C6F7FC70921739C44000E333A9BB80438E31621EF9DB82 + C66D03F958C31B02D8D1CA8F139A5B7702B8944E02FB0E4F81C9C610E339B043 + 14BE7C946FE52D32BF712A24F0105FB22887A268C51A887F5A04F05C9CE4385E + 06F473AEB2F6B9CAE72893ABAB6BDB05AC670278B2B16F3874027805E775C5E4 + 2B205FE18B1DC2191D49DB206E5B04F01810A3194774BE9B3892BF8092BF68BF + 2180DB785E025831F3C5FA8E437F846358E242FE32D1BA110DA28C9D3E6368DD + 084F0A92BF2AEB4D461A04704FFF8AA31BEB7CB427941DE64D6CB5245AFD006A + 3336216EF94674F668E3B8CFA48B373F4C3FFDCDDFA43FFF27FF3ADDFEE023BB + B9989DC927EBE7F23F44D78E9CF85538F90B48FF1CC3D81817451CF718D4AFB5 + 8B97328C6B686CD445A80DFBCA72D307D257E17290B69BB6E873B2B7055BA095 + 5DC178BC33885FD4F14FDDBC90B485A92D18519FA06D36BE882C8B4D5D549E14 + 5CB802E887401F2DFBD380F6A9D0FE9745267D87A149004D06505E81A4402180 + F5B1CF247F9500C63FD3EB8EDF4AFA1EA715245D0E72E207C9174DD8B0AE0921 + B699A861E286B60A4DEAD0466D733CE832F1DB22802B318C52885B92B40489D9 + 797AB589FA565D4959D593B455327751684CF6451993924A0063976F2476E711 + C09373B4E9EF9E213489CAC4AAB6698F36E1BE280124794B72DA13D148F09624 + EFFAF9923CB6D249E1E2CB386E033D1F4F2909648F253200496F26EC63D23D27 + D03BBB56925D6345D0464901FA3396C6D43A81383A6ED5012D7FED6F0C247F9D + EC2A2408FC91F8576297F09D8D85ECC9A4CC2C010C1901191E13CDC73D934C42 + 09021675FA538E927D3006EBF3C0180065AC738C688300DE01616AF20D3B663E + 56D9893603DEAB5B71D56C44E68F5EA61D74D7F018666B0394415FDA0088593E + AE39EB4A5974206D618FF8FE7861AB83D075E2B760738400662CC4ED08603BC6 + 5B8FD3DEDB4FD31108E0BB5FA5C3FB3F48078F7F96B69EFE3C6D7FF48B74F0C5 + 6FD2C197BF4B67BEF85DBAFEC3BF4D377FF0D7E9F8F33F4BE79FFE3A1D3DFBC3 + B4FFF12FD2CE27BF30DB9FA6EDC73F4C3B1F7E3FEDBEF37926722DEEF6BB8FD3 + E63B4FD2E6AD873EC6DD6B8FFDF1CF7C14B43FBE1AEDEB8F9D00DE7AFB99D93E + 4D9BA8DF7C9CB66F3C49BB379F19AC8D4781E3B8ED18B6F0E780D6F195E3AFE7 + D64AB7C16769C0B9F7F367BAD67788DF8D1DF341A94430CA4A0003168304F046 + 217F81CDAB780FB07D16420073B72EFB53789FC7789CB795F88381D93989CBE3 + E2B114E073F747785BA9DF1380DFC1E9B17D67F1BD35A00FC4E4771F63C9C796 + 6504CE4104C7D892312E63A99E25E46AAF800D7E7340DCF50B50A77AED9FE3EE + EC733C7C560A90B9F97A65D7435CFFCA67D9228C156E6720A98B92409BD7545E + 7BD1C6639FA1C3FD097668FBF5D2F41A03D752B415B053A83C5ECF5B3A5EFBA3 + 3D01792B16E0BE761DEE7600F78F4D7DFCDC44209693C6760F2AF771DC73EBBD + 56403B007AB5D136EEE7F95DC5DD5848F09200E6D85002D4AF5DB07BBD953EFE + 12B3CE21168412C09C3B44E04F6D3A27E2BC48DB04E64A793E95E74D15DE8FCD + B50C98836D18406AAE9B0E00E91AE18F7E3E6BC77464E304018AF19C7D276DBD + 75375DBBF74D7AF7A39FA423BBB6E371D12478234800FB63A2AB1CF698177773 + 65B4A74E469B0E733310CE0610C024933310AF9B6FBBDF11FC322606CC374174 + AFC24E6CB52FC2C760C0BA70DBE2FFE2CFFE79FA4FFEEBFF6D7AF6CDAFD2147F + 18B575C5D48EDD095C10A4250ED70563607F6E5F7CD45FD7412048B906227477 + 30EB1DD99BFD58CFA5ADD922CA8EDC4C0A773E99DCCDA4B3CB6C3C28E33ACD09 + 61277D33C19BE3EAB83B90002632114C9F3C863876B6572DE6AAAE1F1BEB51A2 + B7CE9476B453C4F5AEB667C9DB3EBA1DC26D7D0B6E0FBF2DC2E41B85AC8D402E + 03B03A486082F90ED53BA9BB899C4489A7FA06E0EF40AE44F22511B46BB58774 + A70F8CEF74D17BFFEFA6F5E139B69C17D51C63C48B2180DF20A295230734BFEE + BB7E0D33E42FF4CBF006EA4B027886B7C85C8E13C0CFCBED0CF99F24662BD640 + FC9746002F8A817146F88EE38096DD5CE8675E65B36358B5767E92EF7606383E + 10BB06AF1BD6D6F7D2FED1C574E6FCB5B47B70296DE09DEDD809BC0A9218310B + 21ABE4AC92B515909F80001E45E12F0781E398E53649F446B2F70D013C076F08 + E025A03FC211E0824EB4F42F1D651C7E2392F628D4670E5A37C193A235F168C9 + 5ABB8455FFAAA21BEB7CB427941DC626AF94477DAB1F406DC626C42DDF16D41E + 13FB8D83CBE9DCF17BE9FA7B8FD3DE395BD05A1F53FC83B4BCBF1ABB7DB10318 + C4EF9AB5270DE2F7B411C73C06F58B646F0B6A4FB4EC00B5D1FED6408CAF9FAF + 8804F00AB0610BB1480083F0857C0372ABE3B14C4EFA5ADB0092777DFF8AC31F + D9847E6CB1894773AF6DDA42123E330BCAE5A18B525DB8128B3C42FAA468F507 + E8427B616C5F745FFC6B3C26137292A0AB3309A0C900B62BCABBA9D622015CC8 + DF48029300C6AEDF15FB8C1C21198352A109A15E62A8246B62BD65431D65ACC3 + 6E15DF1D3B2E803B818708E0B523F39F43F00EE96913F5F4537BAD0F01894212 + C02A03D48EF2D8A68C7D79DBD04B4C22B986E4E259243591C0CC894D05EC9428 + CEC42F00C2B59F48D5E42A92A79A9865D297FA4102B824775122B94B30018C5D + BD48306B321A3B81BC8F528218F65D4B923826623C00764C74B38EB2936BBD9F + 600734560BF4D5A47EAB0D301EF528D1E698B55F0036242CD8A6EF10D81740C2 + 026417EBDEA7C5022943D287880430403B256E50AA1C2577FD420F220725C825 + DF0D697AB47DC767F18FF186DA0AC604D8567BF4031DC8671F8BC9D641A691B4 + 6D906D80CA502A3919415B45B401C9A73AEEE045DBFB28043048D18E001E7E04 + 3463396168D8C6FB7F6F3C4ADB7804F4DB4FD399F73E4967EF7E2F1D3EFC513A + 78FA8769C7B0F7C9AFD399AFFF341D7EF9DB74E6F3DFA49B3FFE9BF4CE8FFF36 + 1D7FF1DB74FEE9AFD291D91C3CFB65DAFBC8EC3FFA79DA79F2B3B477FF4769F7 + FDAFD2CE7B9F1A3E4EDBEF3E4DDBEF6067EFE3B473E3B13FF2198F7FDE35A004 + 01BC798CB15BDDF43B66BBFBCEB3B473FB49DAB2B16D5EC74EEC2769D774207D + 37AEDB310057670960FD1C089C231C2BA1E7829F3781363FFBFAF9FB7703BA0E + 2481490E7B1C10C0C736167C064E00A31FF3B718DBD66717ABFF7DE477110001 + EC3BCC313639AE08C8F5FBA1E0F7112437817EF83DF77E30262B29435BC7A7E7 + 8388E789604C94AA6FD9AA9C20B94B821736311E106D00D870CC0008E06C67D7 + 3E5CE3FCD8D147278BE42FA0E42FEDDC16A5B5E3B590685D3BF92719F8A1E475 + 9371D40F6D9604DBDA2F0119E51A8372BD970068B7EE090AC670BB0B99FC7502 + D8EE6990F5EE85B0331F9424C7190784F126DE0DCFFB2EFCE82B6D27740D2E43 + 9FE5BEBF617DD7FB7581EB607B318FA392BB05EE5FFAAF6334545B90C0E8B7C4 + E31CA2CE25C2FC22CA59073406FFA04664FB0E9C2FA94CE59C4B65FF5CE63952 + 9E876DD89C6EC36202D891DB237E8FECB88FCC0FEFED05897BE6765AF178563F + BC952EBFFF65FAA7FFF3FF90FEE3FFE67F4CEF7DF4639BBB628E0B02D7E66405 + F057F823A42B196CF636377652147367CC954DE68FB6F63EB2ADDB4B4C12C013 + 9F7B77736E10BF7547B1012437E6BF2B6603B4E6E87D583C5B236CDA31BCFFE8 + 7BBE0BF8CAFB1FE59DC4F8A329E6D2C157632AE1AB50BB68E36D590B39418BF5 + 8102EB012BB1265202B8B75E2AEBA4DC06C18A9879D72D48DEFCDA1D594F55DB + 2E4696716DD6ADDB566C3D85F7FC6658BF4EE65E305B8C39DB663218809FAD25 + 2BF90B1D495FD667096C8E0704B0EF022EEBC72170BDCAF5667C57F010E29AB7 + 0F5BC38FA04700E331C241DF027D667C91B7203646DA25D731A32F6811C0B08F + 68E9C6F22AF3DA2F1647A702256C9500EEBD03B83C8A59FD142721808198C37C + 83F968E6C935BF5E50F3F144D08F42FD1C45D6E22E80D3E0764E8B171A1B4743 + 87C715677272C0E76563C171BC740218E4AF43783E90BF9500DEF2DDBF672F5C + 4B978EDF4947E7AEA56DDCFF36714D28637BE509E0EED894F87D11646F0BDF0D + 0258BE544338F197F97585FE0847F0AD12C07E2308FD96B6DF88A43D0FF10636 + 84D60DF0A4684D38208B3B83DF10C027C332FD2C6AD392E5097699681B7CD20E + 5B9BE4AFEFD8620793F0A9D94F8E9C08C62E6010BF00FED509DD50BF63E379D1 + E0E26108633B81D54EA13640777CC30430C95E7FC72E09E042FC5602B82E06B3 + 1D1EF50CF84E605B404EF76CF18A8526DA054ECA1A3C6E6F41B93C7491DA5AD0 + 9200569982BEC43C1B958F2DA0A95B04588CE391CD4E887B02A05BE4134AFEB2 + 4DA8BCC365DFCD4BF2B7BEA76ACFCA42FEE29FFAC4FAFEB5E4BB7E4BC245132F + 48C8E4E45197C82120A78E75266B08EA50461DA17D11A7410093C00586F42C55 + 3F8648DA4690C455BBD856796CEB239F9548CE3B7949FE2259D9254157CF74BB + 79490C679B0E5DC2B34BA02A01CC442BA04955944800ABBC692F495E02C95E4F + F49A1C096327773DD16CB688093BDA58DBE521693C0F4C3233E1DC4A3C471D31 + 664399EA34C1AFA02E42FDD96609BF21B242A1F122E0AF80CCCF1F880221729C + B4E5636D0BA857A28675D8EFDD7890AE3FFE26BDFBE94FD2850F3E71F2078F7A + 9DE273BC0402A5906206EC8EE40EC9563CD65B7A05FA50020C8876D03B3186F1 + 589B04B0927224DB9478247147C296A05CFDA30F10FDA847DD495B2BB3AD8DB3 + D880708C0470D595B6036D8313C0D71FA4AD1B0F3B02F8DD4FD3D90FBF97CE3C + FA493A7CF6CBB4FFF897E9ECC77F922E7CF1A7E9EC67BF49E7BEF84D7AEF0FFF + 69BAF7EBBF4FD7BF31D9473F4F474F7E968E9EFE329D79F6EBE2F38769FFC14F + D2DEDDAFD3DE9D2FD2EE079904DE79E759DABD8D9DBC4FEB0EE03D2B01D4B7AE + 3E4C1B20816D4CBB369683773F4AFB566E5D7F98F058E5ADE3FB69C7EA4E0063 + 37ACEF88ED8E8BC709B2533F0B003A92BFB4731D3E6F879D5FFBBE0168E3BDCA + 7B2080AF599FD8BD8BFEDDAEFBFEFBAEDF42F4398968757F44374854F3C3D8F2 + 38AC2F1BAB8F51085602BF0B7E17F377AD10B365DCF01BFA7EA14E793C66FA50 + AEDF75F68592DF79B44978ABAD8E95B2219DB601FEEE87E4D4A18CE42EE3A93F + EABCC650467463820DAF4D76FDC0B54D6428D12671DF11F8D9467D51C7750ED7 + 505E4FFD5A6831F5BBC0EBE77669F33A4C39EABC4F310E63C6FB81C7B7186336 + 11B4851DEF819001F4655BFD08D57117AFC3EADA37CF899F1FF8C8F111F4CD3B + 883B39C073C071F29E9DEFD7C5DEEA4EF8C24E40FB0A6BC7B89073BCEC837022 + 194F03B1B23797801CF30141B441C97908DB98F3704E833AED09CE7B1691734E + 05798E75CBE662207FB1C3D7FA24F1CB3ADEF98BF7FD3A010C62FA5DDFFDBB72 + 6873B7BD1BE9AA5DC3FF93FFE5FF94FEED7FF83FA5779EFC30ADECDA7C73CFE6 + 9B4AD416D2D777FD5A4CE83A5237CF3F394F5DE5E3A8414E639CB0C54E5FC661 + 4CC314F1EC9CB8BDF9A28C0430E7B72B0B12C088B161F3CA2D3BFE2DEB7703B0 + F6A69DA329C861AC39CC067DA06CC588605F5A6AFFA8FB7B81B1F6C13A07046C + 5923102B2607B816CA76A62BF50812C024656749DED9BACA40C082ACCD24B0AD + DFFC294B764EF1D98200C63A2F10BE247A73DDD695EE8B32A34F42F7FBACFD1A + FC0FC33B86B0FE8CC0DA35AE39B5DDF20174ED3B8B4CDA0E6186C40DFA21C067 + 6D076B6DF145CE82D818696B7D7A54EBBDDDBF04750D50A736ADBC4A84C688ED + 1787A353C10C015C1E01ED6DE416356F5BEC22DE10C02F17337972F98C9A397A + C2DA33F62D982D1F210DCC238007DF7DFB6D608C676AE85E490278C1B14402F8 + 44BC193F73A0CAC6C640D2B57BF4B313AC26C3A39D41FAEE1E5C4C1BDB67FD51 + D03E268BE5E7D91F215EFC4781782F99000EFCE52CE96BC757A1F2D3C31B02D8 + 30F4653ED197FB7581FE08C7102EE633BA281B422B06652D9DEAA3DCD0BC118D + 40ED596FA175F37B6E947FB3A18E498812C03A3951F4FC5F5174639D8FF684F2 + F9B0683F8BD8004313694EBA4186A23DD9BE9036F76DD1B9753E6DD8A268DDDA + 207A41FCAE83709C583F6B767E4CE68F7D0601ECE76A36F6D8785E24784C84EA + 48FC2E4B00AB5EE1BA8DD9C73E2BF9EB242F096090BE248001590CFA8271EBAD + 4A0067C2188B395BC8E211D0B648761FFB2CEA021371E72E32E703317431ABA8 + 7D8D40E30C21EA5B7D016A3F6F6C8413C058900365611FA18B7E4009602582BB + BAD91702184908ECF2E54E5F12C0EB07D7D2E6D18DB475E696BFAF0B3B0962D2 + 8589224DE078124608627D4F17EC68DB2290DDBE40DB2D0278C58E81C91C4FE8 + 08014CF217F2D5033B3626C50C48629150D5BACAD4567D28A74C013D89598D47 + 501751DF27B700604FD257011DC85F26369998D40426E5485C52879250DB1686 + F490D5DD402591ECEFF093DDBB9EB02D095E4D2403A8BB1CFAB2638832BCDF90 + 505F944C2453AE7505E5B457D046FB545B26D935D9AEFAE84BF20000E1C03AFD + B5CE7894AB6D942B6111C158D9BEABE7769FF470397C0A91D323672E75C40E40 + 72868493EA503FF7DE47E9CB5FFF75FAA37FF69FA6F73EFB69B6311D8837BC7B + 17E45B47C2957A257C723CB793B6DBC2466C01925F91E8C2E377D15619405B10 + 7C7C6C7025101B641BC93900041EC947D601F890A824341EE36B0CD6FB44A68D + 91BAAB26BFF6C0C6FFA0E737E35301D9A34A00839C3D7CF7132780CF3EFC69DA + 7FF20B2773CF7FFA9B74F18B3F4B673FF99374E9EBDFA5277FFEAFD257FFE43F + 4B777EF9B7E9FC177F6872B3F9F88FD3F94F7E9BCE7FF427E9EC935FA743F3DF + BBF755DABDFB79DA79FF93B4FB6E2680B1A377E7C6D34CFC5E7F92F6AD7E8036 + DEFD6B0081BD71CDC6E5E379EC8FA6C60EDA8DCBF81CEE9B1DDE139C1F011D8F + D18FCD64F15CB6CE236CF2AED8F2B9E3B3F6EF0CDEE9FB61DAB5CF1B80AEFEE9 + C0BF1FF8FE64E8E3D0FDF78078E5F3C88FA9EE08DADE98AC5EBFB340F97DF07B + A6DF4B8E5563203E3F4FB4711C2DD21BBE8AFADDAEC7928131F0370A02187FC6 + 502298E3E4EFA1C6C1F88A5CCB68CF7AF48BB62823B9CB36AF13EA8B7AAB9D65 + 99CCF56B3DAE7FB8BEE133B2D2495DBF6E659912C0D4E7FB875D9771DD830EB6 + C587FE3DB9F4E57A03BE13D0F13E00F03ACA6B32803F22691B7A7EA718DBED8A + 4D94518E92F710F6A576B1AD40CC1A173210A1B8FF8A2FCA8901C758EFA345A7 + 3179DF7642B7E8A28DC7927B9DFB057D257ACB7DBDDAA30C7DF3B861CB78007D + BC8DB984D8B92DF45E5ABC1ECC06F313F4ED846FFE031BE74280CF7F4CB66673 + 20CE65BA79519F1C06D40F25E7402DA0EFE9199B0B81EC2D20518B72ED10F347 + 2BD1C623A06D8C981BAD996EF5E876DA3D7E90EE7EF28B74E7E39FA57DFBBDAF + 80D83DB4B192B01540A6806C8A39BACD0F7B73C6322F5DC3DC104433C660C782 + C74193FCD5770967D83C3320CE757DBE8BB9BCCDC327784FB097AC67C0CF095F + C4E4FAC0E6F0A83BB16C36759ECFB542016488EF3A9103DE6FD0796CB5C5DAA0 + 90BFBE06B052C95F10C45C0FB1BD023DD64756D63F8F1AA6B67EC864EB956E7D + 554AECBCD5DDB75C676107719519D64A0C2790FDCFB5760C1837C60982D6D776 + F0453D93BE99F89D2580619377FFF6D77DA813DE2F8EC9FCF82AA59340D7A0DA + 561BAE65679109DB21F408E07900C95B48E21902D8F5193D22771DF91B03EA11 + 9023AF546CAA9FFA041B85E64988985769A1E5F7E271E4686D583929EAFB7F01 + E645992F454E36D813F30860CD4F46F93C2C6AD7C2F3F8BEEAE8E5C935BF1E73 + F4CCDB1B683FF3793562F151D2BEB16B2DCBC9C1448EE6B5E0668483AA63477D + 35E3757B0434C0E350B4EC46E19F6F414B3F8340BE1AF4BDBE751C7A0C38CF56 + 3A010C3236F8CF02362F91006EF097B300F1DB7D5FDA36CF873704B0815FE2F8 + 653ED197FB7581FE08C73040C02E0DDE145A3285EAA39DCA70E3909BC749A137 + 22BF19356E7CCF853A9931B4F4866527282F03ABEB1D5A7AA01BB74E124F0E12 + B44A8AAA6C9E8EF28845ED22D45E27DF2080376C61041D08D20D6B3B590A3DFC + 6CF242E2B742E22A34AEA2657B5A183A07F3C6A1B258573BD577280430DEFF5B + 5077F792F825E13B4000D745224AF7E974BE0397B62841C4736189D8B40B604C + 2C46AB5CE23206A18B562E62E3427608D1B6F6D7E88B76EAC33ADBC0987FCB1E + BBA5F958B28EC4CD6D2EFE5B72D56B62C0130EBB57AC0FC376267F01EEFC7502 + D8CAAD3337D3D67924B59020BA3E93744122867205F424812379AB3EB42349AC + A0BE05E822018C640DC9DF48006307B027ADCE641295E825CB0A601749DE4574 + 402468B52F45D51BEA0E5C26E650D7B6610DA0BC608ADDBA484A7A62122889C8 + 92B4D484654B46B9263B7BB68889442A12954CAE3AB28EE876E8BE9D13E348DA + 96842C7DD1D6843292BA35A16B750513C06CD35EF594B774D137F6ABA03EEAD0 + 565F26DAA9A75C7D00D88104D0C7892AC9B0596239516075E87B89FC22D3BE19 + 6F13A48AEFB8421CF371200EEBB9AD365B2089B0B3D74BD32126FA136206440D + 096012BC00C91F80640EEDE17BF4F69374E7CB5FA44F7FF117E9EAC3AF334966 + F2EDEBF7F27B50710EAE99AFC9B06B136DBCD794729082782C34E5D42106DED7 + 0BF2166DB7B7F636C82D037C7DD7A6C9F23B650D6C9792A49CC7464CF80840B6 + CD23DF483E129029A9471F8DC1BED447636459A7DBBCFAC077C8EE38B1DB11CE + 5A27F9BB7523EFFC05C19AF124EDDE7E9AF6F108E8FBDFA4B38F7F9A0E9EFC22 + 1D3EFD65BAF0D96FD3E5AFFE3C9DFBE48FD3956FFE347DF977FF59FAC5BFFDEF + D2C3BFF887F4D60FFE285DFEE677E9D2977F9E2E7CFA67E9F2E77F912E7EFCBB + 74F8E0A769EFC15769F7FEE769E783CFD2F6BB9FA4ED773E4E3B6FE33DBE1FA7 + BD1BCFD2DEF54202FBA39D9FDA189E3A210D0218BB80B76F196E3ECCC7766C9F + B11DDBBE8D730FEF11BE91CF95EBCA7941A932CA793E790E28F3738FEF87C576 + 94CF1B25BE07F8EEF97780DF21C0EDED73B98AEF1DC8463CAAD86420A44DB67D + 239FEFCDEBE8BFFFC7008EC3EBE5FBCF3F39A01F7ECFF00868FEA6AA7DF1C5D8 + F1DEDF5D3B07DE8F8D137AFE31A1857A2E2C1E7E7F3B57AC5E76E853C6DF24EA + 7117304AD65B6DDA028843C236DA11FCDDB37FF60B50475B2580D1663CFA02D0 + 69BC6C8712B1E06FD72F3B3F0ACA7B64AEC8EB35B6C808F7411DF616BFE58312 + 72BC3F1A3A252A21F7EB35003FF888DE6D4C3E06F7B712B6B84F3006C07B478C + 497DABCDB894FB3DD6EEB568B32FC4F47BACE9788CA8D3DFCF8BD9E07E59FFBC + 857B0774D297F6A7721D378038BCA703DE2E801EE7CFC7613E8C196328D837EA + 1A3BC7EDEA0AEFAFD8E15DC32B98DBD85C49E72BB96DBE76BEE2FC877595518E + 52E74E11D083EC8D04B093B34716C34AFCF9CEC764E52AE29DC5186CAC56AE1C + DDB6F580CD7D37309FB57926F4E8B790BC4AF6B2CE7E403C4F3127B4B9A5CE47 + 7D8E7AC662D9FCDDFB3DEACA3C2E8B85DDC306D47DEC36379F94393A4ACE7375 + BEEBF3D8F2A750EC0E265C5F91C96310C39CF3F29DBF2E379BA1F93ED7096CB7 + EC50AA0FEDBA36EAF00191DBD5BD6D00F9EAEFE12DFA155B13F9FB884BFBFCCD + FBE996DD0BF72ED9EFA8AE9B0CBEA6C09AC5D6550612C0D071E7B1C2D72765FD + E5B6D606A13A85EC00716D8D570182F7BCD966E237EF04CE3B88B913B9DB91DC + 5FE3D5F10580608EEBC988D69AD3C768E782F03F218B1D6DEB5A56D7F58E7373 + D1DBC52B242F7544D40D61900066BD05E8876C904B6AC8912B015AB926FD033E + ED544FA8EEC502E33BB23E3186AE3D84E17C618EA1BB7F09F72B36D1471109C5 + D85704FD5A3AC5A2762D3C8FEFCB03F2D0FD7C34C7BDC8F86B9E7C815CBA227E + 5EAA9B89E5B9FE2C0707B3B69E4BA2CF632CC6F1BC54703C1C9BA18E1DF5D58C + 6F9B00D6735AC7D7B03B75E867DDD2CF2090AF40218033C4B69CEBAE6DB62464 + 5B712AA03F6D0218BC24B84B8BBBB6E9588CFCCDBB7EF5FBE3C76558C32EE8A6 + CFC9F08600FEAE427F84639847CCB6E4118C11EDA32CD65B883ADA8F41ED828F + DE88FC66D4B8E99D1885F86DDD5855D6D2F701DDCB854EFC86C6A036A781566C + 958DE9286B41EDE6D92AFA3E367129136E9DE8E391D06B65A28E3AFDF0CE5B10 + BFFE38682182631F3A9157304ED7FFACEF49A13135F6BC31B4F40ACAA29DCBB1 + 4B17C42C885F4208E04AF6020D02988BC9FEC2B080E42F485FD4CB02B23EF679 + 5B6C03105317A1DE47F16F2D4E690350AFBE6388B68CA9505BD61782F802ECAB + 623B13DCAEC3023C2CF4630220270BF8AFF0AE9D9165F93DC2560701EC24B0D9 + 97BA12C093BD2B9EA8593F8B9D02D766922D4CC050A718B34359934BD6F6C40E + 1EDB56123C84FA6A3CCA735207C99A42F4E2180B01CC52096092AF444D902159 + 55126694B5EA6C93FC55BDDA31D905B00DC086A00D885CEEC845C2127042B524 + 3009275ACD66E528EF52D1E4A4271ECFE7242EC06424A0C9C9681BE5D4119A58 + 4592D613AB25F11A93AF4CB8D216503DDA4CFA32A1ABB6636042789E2EF6473D + 13C9942954CF582D441B26C4216349A08D9D862413EACEA7E28324B8D7492858 + 9B716117C7051B2746AC3E7D2B63FD226241861820A53A40362BCF32C4407F4E + 6209D9E344CCA56C077B2764282F048F923590C116E3C0B138510B62CB8E1B8F + 5C2601E6649795A843B686E333FBC3779FA6B7EE7F910EDF7BEA44D82A8EDF4A + BEE756E3384A8C8D6B4026C7488C42966DF2189C2CB5323F7638C7AB849AE948 + C20124DC38D628479D04A492B211F4C1385087AD8276BDF6352BB1FBF73A76F3 + 76729085ACFB78608B76D9F5BBE37862C03B773F4AFB1F7C9A0E1F7C93CE3CF9 + 493A7AFAF374F6C9AFD2C54F7E93AE7DF917E9F2177F9AAE7CF3DBF4D5BFF82F + D2EFFEC3FF903EF917FF697AEBC7BF49C73FFC47E9EA377F952E7FFE97E9FA57 + 7F9D8E3FFD47E9E8E1CFD2DEA3AFD2FEC32FD2EE875FA69DF73F4FDBEF641278 + EFED4FD3DE6D2B6F3E4EBBD7F263A0776F3CB1F1008FD22648DFDB8FD2F6DB36 + B65B5682AC2EC7B677EB493AC0FB816FE75DCE3CAF38469E673D3751AE0049BB + 6EDFBFF52B765E7CD7B47D96F61D9EDA79EFDBE7F30619BF43A87317F2B6EBF1 + D9DAEFD4CF3DC7966DFDBC17F8588ABF93C0F8ED5C31BFB223D77F0FF88DE1F7 + E17DF4BF3F381E7CA6801F83E86943A81CE3406CF4E3EF18B63E9454457FFC4D + F67EC7A55DC7D6682B598C984A00BB4D894D5D8FD435B9C7C398626C3B2FB0F1 + EB0FC7023BC8CD9F32B729B1D0861C3170CDD4DDBD04654E4E5E2ED730C8CDAF + CA0B79E9B63EEE6CE7D72703E3009093F4459B36EA53E396B6DB998CBEF4A10D + AFE1A8D3973AD6716D27214BFBDE751E7E681739F5D0B5DA84FFF1CAEED9FEC7 + 9F2B381799ECE5BDC4FF8C85B6D531360063C0BD09F76EDCEF4100FB1F86CA18 + 788F647F808E977AC8BC0FABEB3D38CF17AC34A02F9E77E8E0AFA438FD198F7D + 50C7F9858FB5F4833ACA66BF62BF5AEC54C7BACE81744EC5791341B94275F09D + D83C6EBDEC0046E9A42C80473F5B09A217C87338CCFD6C0C906117304860F3F3 + 5DBAD8F90BBDB5950026F1DB91BE19F911D33696330DF2B7023B7F2D1E8EDFC6 + ECC78AB6CD25276507301E5B8DB1E79DC1369FC5FC1CE8C5E9CF73016DFBD36C + 8A9DCF93CD1F8FA27619EA265BC123A431C74789397C799C34E7FA43737E80EB + 0294D196EB86317FC574CF6CF0C757D4717EE06F6BA01590C2B61E79FBE157E9 + 8B9FFE693A77E39E13BB4EA4FA7A22AF2F7CED51D660BE06818DD57BA0CC4AAC + C1D60F6D4DB57FD1FABE90366D5D323D807F267741F8AEECD87A7B1704F05BAE + 9B1E8060CEE893BF184B5EDF65E4F55F0BBE6E2CEB495D6B2A54CF3A48DF75FB + 7C80E50960ACE7CF663488DA19346C7B04B0C847B161312ACE14587DDDCA82D5 + 822A533D7DC7E4861AA3E4585067CE2412C02C29231823B64F1F181FF240E82B + 83B216621E53759500566CE69271A38F229289B1AF08FAB57484C66FE9C7F03C + BE2F0F5DDEFBA40430E0B972C9A1139A479F87E83B8B6C0332EF4513C033319F + 173A9E32BEDED841EE01B4F996C0312966EC9638BF4DFF16F819838B69E909F6 + 1D49D5824AC42A915ECE75D72E36B41F04F4A74B00575ED2B949004470278FA8 + BE0DF2D78FCB8EE70D013C823704F032B01F49BDD8CEC110D10AF9908E509B79 + B600ED0BF41DC44368C651A84DB09FB931356E782746B9C9B66EAC7AC38DBA59 + 40FF72C1491F27942D9B9506A13986B1096A8C3D241FD3511EB1A81DC6C649B7 + 4EB23B9BECEB9377C4C124169F9DFB52DFEF6332357FBC0B18360B9EAF38568D + F722A0C7C8639E3DF6B6FD905C63B87C038B2A5B8091DC1592B7EE0076F2D6EA + BE08641B8BB63EB9A9EDBA602CF6AB1B5616191E15EDF50508602D89D6E2B4A5 + 8F7ECB40C7D2EAA3DAA1443F0178E435803A120BFE1830E864E1EE703975206D + 2F5BDD16FB06D4152E47DD16EBFECF768F8D18E2E78432CE812DE8B1FB97C4AF + D5FD3DC04EFC1E5B8C634F88AC1E64C2B626844A7265A80E20A1C2440DA03ACA + 5506A84CFD55A6C832D8D8711570E72F923384EB0E4D7E94495726AB0026AA00 + 9533B1457BEA5BB69445A87FCBD67748981EBB7A7D370870CE80364AD64BDB93 + 8D67F3230D4F8B000648F6B680242993B20A26519948D5E46AF4A10E754D1413 + CBC8506A3CD53151DCF2A38C7580C9654D6C2F8B1843DB486E63D72E12DE4840 + E3DCB8CEE448BA034CC43B49007F946603A0AEFA4CC4807830B81EBA4CD0A024 + EA2E45831332821CA3F46175B7050105C2C9C6EA7EB043DDE51FBA8D03E4078E + 07E3913A085D00E42B082C9056131B2F7737927C23F1B58ACFC57417EF7F916E + 7FFAE3F4D6BDCFBDBD6272EF172418E2604C80B41D251E7424FBB49F280748AE + 1120E4381EFA459B16183782F100D891C48C20B1DB918120A2E19FDB5977DF77 + 8A7A1B36F02D2576006FDE7898B66E3E4E5BB79EA4ED5B4FD3CEBB1FA7830FBE + 4887F7BE4E074F7E98CE7CFCF374EEE9AFD25B9FFC36DDFC83BF49377EF437E9 + EA8FFE227DFE3FFB2FD33FFB3FFE5FD3CFFFEBFF3E5DFEC33F4B6F99ECC64FFE + 3EDDF8C1DFA76B5FFF9374E9B3BFF2DDC2871F7D3FED3FFD2AED3DFC5EDAFFD0 + CAF7BF48DBEF7E9676DEF9D44960ECF8DDB9F638ED5F7F9AF66F3DB3713EF1F1 + 6CDE7A9CB6DF36DD3B4FAD349993C0D8D9FC30EDDD7CE444FFC1BB36E61B384F + DD39C2F9E2E74619C0F3C973D5C9ECB3B86AE7198FCE06716EF0475097CF9A7E + B0EBEAF973A9F519393E5FC4ECBE2FD4031EBBC8A0C30EE42D90B27CEFAFB537 + 2F1B7C57701E03C733140FB27970FFF23B463FFC7D63E731C0DF257739F3B78A + 76DE256FF202F72932F729BF73DF798FEB4201637AFC968DD56BFCD2672FB6B5 + 69C7B1C00EED29AE3D905B4CFC090476F0F5EB8CC9BC5FFCC1E3D86C70FD23CC + 0744AF5F0771ED7322DA60F6D8C10D7B5C3FA720868B9C44AD5E4F3D26FAB732 + 5F4BED1A6E751093942BB14B396C5112F4A79DB70DF0A55CED3426EAFE48E622 + 771D7CC48F3E8C45A214F71C8D453FE841DCE289122080F1272DCC1940A65FB6 + EBEBA53B9FF8BDC8EF9B25AEC3EA7E2F32390867BE2399F732DEE7D8C63D54EF + E5EC9FB6BC37137EDF837DB1E5314087369F92417FDEB309D8B1E47C4375C490 + 2DA1B6DAF6BACC8D38A7E29CA905DAD21EE09C0C64EDD46CD6CBEB2FF2A39ECD + 07BB7F39C733AC1CDAFC0D04AFD910248739F783EF143685F4F5DDBA8857CAFA + 5EE1529F38C9DC9E6FAE3AD006E96C7698DBD9387D4772217F33699DE36758AC + 43E8CC0FC01F36CF74F35742FBD3F9329F9203F2B7D631672F64F00A485F95CB + BC7E68CE4FA8CD22A01F0962D6B13319EB1CD880005EA11DD64EB65E7AEBF6A3 + F4C113BB1F5E7ED7ECB19EB27598ADC9B8CEE811BD055C87A0CC3210C0B6BEDB + B5351776D21E5E4AEB8717D2C6D1F9B47906642EDEE77BCEE417320E20CB8F78 + 06E9BB7E08923892C0286D4D1708E016098C771BFB93A36C1D38B6E6859C6B58 + 257AB90B18A5CAD537AE8333B0B63F97817A8BAC6DA145182F4422A30F83E72B + 02D641DE766DFDC33FCB4AF02E02F814C01F88F927C8B4043437459DEAD93E7D + 647256C19CD710348F39A30B0430658C3BE40744021832ED2B629E1ED0F88BD8 + 2B9EC7B7029B858096EE7981DCB6E4BD55B7E8D855B756085A45CCA58F21FACE + 22DB80CC8B04709FE05B90E719D1F7E23D2F742CAC1BEAB801C89CE40BBEF320 + F116422B4600C735780E968CD592CF809F31B898969EA8C722E4A8A1727EDE06 + 211BFCE238E8DB22722BA05F82002618BB81CC490AA23DF42B5BF5BDBF598EB8 + 3CEE72DEA5BE6A31EAF19F02DE10C0DF65D48BED73601E013B42BE36E5A80B5A + 846F442FD69298B931C90DEFB4D0BAB152D6D20D03762F079CF47142D9B201BA + C9E162189AA00EC5D3BEE6D944F9D0D8A31D017B4EBEA7B608D04938D0C5B0CF + 0FA89F1FF4ACF7FBF0473F6BDBE229285774FD643F8D775A18EA5FC7D6D22F8A + 9938BE68B28595EEEC2D9825806D4188C59FB70D56D785E2E0829131A37C041A + 4B6353A6EF3D8A365CC4C685ECB2886351786C5B80FB23C1E6A012C07375B600 + 07615BDED9EB8F6F2E046E27CFBA95ED4B0694B9EEFF74C7A39EB7ACBD69C76E + 7590C01E07C4EFEEB1A14F002349C2E4081E018DF779A10494F48D4919265C08 + 953309C3440CE4249709DAA95E6374F28EE46D91BF80CB0FCDB724AB5AA4ACB6 + 81960D9355B44749A81D41025893620ABE9B6D72FE962400734251911FFD0C9B + 9C7C6C91BF0008DB650960257B09B5479294095700495A266A63725513C06A47 + 99D6D9D698EAA3F588A1184C44036AAF711817364E2C0898845E06DAA7C213F2 + 06F4891D37B0451D3B6FFC31A4D0C11F7D9736090342E5590762000482F58164 + 3FC8152745B25CDBAC93382249D511C016C7620224DF48AEA28EDD942445B3AE + D8984C7D94ACAAE418FC4B6CFA3016C0FAB93B9FA4CB0FBF4A67DEFFC8DBB4A7 + 0FEBF403341EEA4AA851E7E4948C05D01800FD008D3764D3922B49893A49CA58 + 57504E52D36317325475FEA860B46153649900BE9F36F028E5DB4FD2F6DBCFD2 + CE3B1F39017CF8C197E9E8C3AFD3C1473F48E7BFF845BAF8D91FA7ABDFFC657A + F7577F9FDEF9A37F91AEFFEAEFD2A7FFE6DFA77FFB7FFF7FA6BFFBBFFCDFD23B + 7FFD0FE9F21FFD47E99D3FFD77A6FF2FD3951FFECBF4D6F7FE59BAF8FDBF4C17 + BEF9793AFAFC07E9E8D91FA4A3FB3F48FB1F7C6D7D7C9E76DFFE3CEDDDFEDCC6 + F271DABDFE2CED5BB97FFBE3B47DEB49DA7002D8C6743B8F69FBDD67BE1BD81F + 577DFD61DABDF9D07C1F393231DB2680793E153C577E0EACED9F8313C0767E0A + 010C8010F6C74017E2979F15FB61BC08271FAF59FFD8455EBE033DBDB5FD7B54 + E5A57D0CF2D762FBEFCBEC4A1BA470F509F0B1171DEB4360FFFC5DE0B307710D + F09818C77556FA77B9FC56D1F6DDF256CF7F9EC0319A0DEAE53B5FD1FA7DC3DF + 648A1CA73B1697C11632893D3DB6DF3F48D91217405CFE310436932B56B71271 + EAD8E1035FE8CC761D3B590D4E089B1F085E274E0190BD161F80FDC4DA6B97ED + 1E049BE3E28BEBB15C4F59AF0468D177D758D3151BF4470218405D89E216108F + B6359EC97AA4ADC8088E05A09E4F8D603CC8DDBED800D0E1DEC2FB0DEF65B82F + 83443CF7EED374E7CB9FA73B9FFFD4EBB0E59F8FBC5F01FC18076DDCAF702F63 + 4CF7B57B2E62F35EAAF73CEA6750FAEAD9980CFDECDA771924358E43FBF17B65 + 985FB04FEA6B7C01E51C23E350CF988A3837F2794F99030D0284ABCFD73A12D8 + 61F3377FB47481B761E3765D7CCEE19A04B0D9B92D08DD4AC676E42F00C25709 + E0AA0B043030F5F9A6CD3F412A631CFB985F9BCCC6E664F0A1C53EB4792AFCAD + 24D9EC40BD60CDC68C39ABCE61753EACA08E7A6DAB0C70FB32E71F9BEF2B74AD + 40D0BE05FAE9DA43D717843FFE99B656DF38BC9EF6304FB631FB4EDEB21E03A9 + 5A495FC85837F40860ECD43D34D981AD134102EFD91ACEC9DE435B4B01B6C63E + 3863E307CEA6F5A3B7D2CEB9E3B479C6D631E6ABC46F06C85F12C0D68FF547B4 + DA159035D6BCBA76E55A167A12BDD4B14E7FD501F4EDA3C8911FF03676F2E652 + 01594FBE79AEA7EF93C863603F05E8977985F55256141297F919E46C201B03EC + 631DBE06E4499877023C6691B36C81762F1E7D0258735E6360FE7246DE20807B + FA013F400960278145473FC5905CA13116B18F781E5FC78B268091E306B15A36 + 26113AEEFCAABA05CFE19C5CFA18A26F0F9ED3CF3620154709E0CA65CCE1795E + 160FA4FD907F32C4F1E371BEB4C5F101D56F08126F21B46204E8985A7AC782B1 + 16867ED62DFD0CDA446BE5FC9A3E0A8C1F7663B058A74C00CFA061BF5A08E0AC + 93CF2DD6EBB1BC218007F186005E06CF479C562C4AC0C28E68C955A6F245107D + 97C0CC8D496F70A704BDC156794B3617B07D79E0C4AFA52316B151D03E6248AF + BE6336510670723CCF5F011F4EC0DB0470FE2CF433CDFA5CD7CFC97DAA7EF109 + 3CFD9785F61163462C6A771230B6A212C0BAA01A40258429431D3B7A65B138B3 + 6004510CDBD6226E041A4B6353A604745CB00E2D6497451C8BC2633B693B0C2E + D649F2B21D6DAA9E04B0C1095D8393B545A6BA960CBB839D2006114C52B8D832 + 4E25804B7204C91510BE9BE76EF9BB80490243CE64091EE1A6499531B4123031 + 91433BD5A3A4AE9377442F132F2A03C608604D42B1CD7A246F871258EAA37E7C + 04341F034DDBAA4709DDF94CC82261D824804BD230EB4B124FE404F491C0A55C + DB91E8659B32DA791FF6793391CA042E4A265A9974056A72D71013B58CC33AED + E8A75019EA11F48BFE4C442BD44F011D13CE0093E78CB528627F8C8B92FA8B77 + 3E498F7EF047E9DD4F7EE43A4F7E97847E4CFAB3CEE43E490410B74ADE2AC94B + 396D884800AB6D8B00269943A206A04EC91EF551F286F51E69567C08EA234868 + C1A6159FD078AA675F2D19FBA01CD0BED57E9E8DEF720C3A00630741C9E3D0BA + 0336C58E7A129AF407726CABDFECC83EC8F0AEE4BA6BF5F663DF65BBFBEEC769 + EFBD4FD2DEFB9FA5C30FBE4A47F7BE97CE7CFCA3F4D697BF48C7DFFC697AFFD7 + FF901EFFF57F9E1EFD93FF227DF057FF267DFDEFFEDBF4DFFE7FFE7FE9BFFA7F + FDBFD317FFD57F9FEEFFC3FF2A3DF8E7FFBBF4C13FFAEFD2F55FFDE7E9F867FF + DAF077E9AD1FFF513AFBF58FD259FBBE9E79F4C3740002F8BDCFD3EE3B5FA4BD + DB9FD9B8ADCF1B1FA7FD5B9FA6DDDB9FA4CD5B4FD3FACDC769E3D693B44552FA + BD8FD2E66D6BE391D5D7B19BF961DABD4502379F671E33CFE9D079057AE70B76 + 8500AE8FCF7602F88193C0433150D7772FA3E47764E3BA7D9FAE5BD918077DF9 + 5D02618F1DA6FE086A277D01EB83105F07E219D817E3F3985B507FFAF11C1018 + 176D796ED0A6BDB6F99B89804EFB435D7511D033266DF97B55408F92B600AF2B + B467C998D917B2420083C02D6D02A431085E82369500BE64F7A02207414CF214 + EF169E79BFB095243C09C84846BACCEC01C6619DD76805AFDFD10E32274083AD + 93B905F405A82771EAE391586AE772AB73E72E8F01F746DF016CC77CE9EEA70E + D4B54FDC07798FFAFFB3F7DF71B625D959209A99C7A4B737AF4B73D3DBEBBD77 + E5ABBAAAABBBAABBABDA56ABD5DD322D835C0B0172232F10C23402092384101A + CD3C041ADC931904C24A4F020DA0C1887933128F37C0CC030D66183192D65BDF + 8AF8F65E3B4EEC9327F3DE5B750BF4C7F78B88B556AC886DCEDE11EB3B11DB8E + 3B02E5DC7B12B690757B6FA780BC209BD517EAE2FD8F3CFA33AEF7ED38FEECA2 + 791C8B7DE73EB6C576BC2FDF16F42928F7E3166F0B39C71FCC83B0F5DF070E50 + 9BA82BC64F0EFC448721EA39764AC76D1E4DB5078C7CD576423DED5B8600C6EA + 5C5BB11B41A297E42F09E082AC057204B0CA9A333AFED436F1ED5FDB627A7CD1 + 56215B9F00D425D9EBF294E3FBC058291C560297E35E8E85D336A9F3E3E5B4DC + 512F8EFFF167530FCAF702E7073970CEE0E71EFE9333367F19D7BCED2814E616 + FD3A17B23FD10E611E5692BB46F48E2B746E6679A6119E001EC0F6CE935A7F42 + E76FE34875FE36A1F3EA8911ED3330AEB2314DA7A4A172AC006E4F61D52F812D + A3B16A784EC27786D395BF61BE087802D893BF26732B77FD9C97F3573F8FCDD9 + D5D912BE0EEB2135DB58173B4F75F393CA0B601E8F3882E6C31FCC4B74D8A6B0 + BAA13E3E63659FB2321CAA92B84823EAF21502D8C9FA63DC24B7F080602C2517 + 9B7A7330A56D86743FC8C7CC54FE2612C0BDC0FB38889F07A96B7894047084C5 + B8730430CF3FBFC3ECF4B5801FC6D133B1F46EB07A75B17B93071B4F50123D91 + A529DE641EA87F00886D6A9F4D961C07FBD4684F1A8ABAD13E0BFA44BA17D2BA + 19147DC9E80AF4E8AB67F86B9DD377204FB406CE4FB1E7B794A1571FB5800F90 + BF04CA39BB0C923EED17612B6B05BE63ECEE89CA352CCE3F6455CEF341F19B04 + F07FA9F03FC207C17E08D81C618B97015F08B461DA2B58B71B6AEC3A5E4CB917 + DD03828382E2C5CA76BCAC27C0F6CD03077E391DD18B8D07ED7BC583F8E00079 + 3FF5A1F7036D3FC80EF5ABD73345EE3AB13EFDA6A0BE1B529F39C06E2F9FD473 + 1291B34991F3473F1E75F2006DCB13BB9995C04407014CE4266715F46253859F + 3822F5934FB389933FEA68FF6602A4AD27735370C2BE57D908606C219610C09E + B02581DB0BF6B4553DB743634084AB802D2833B1684193423FB5283EB842790E + 69500679067150667DCA69CB7A4408102544AF8327811974AA049C146920CA97 + 49D4B24C5D6D00CBD53182D711C05E5EE89182002E02812100C87C1A48F4E55C + 8011753CB1EBE5444AF622E5AA617CFF0F655F878154045D110CF66030D69719 + 584D03AE00F51EF44DF8A06D1D527B5FCFF78FF07598A78EC16D06CE7DBD6EA0 + BF9C8E7EB0F52556379D7FF2DDF2FAA7BE4A9E7EED1332B5765EFA66968BA03E + 83F90C90E7001D885BDB663692BEC8B34C5297C46F0AD6F1B6B645EB7C49F290 + A82131B31701ECC91BCA5826614642C9D7A52F5F07802DEA793BDAB23EE1F56C + 2705EB20F5B20269B90EB08BB6B60237E68BD59190ABACD02579C26C00E81438 + 567F9E2CAF3002122B5897B5DED2A98290841E7EDA481D013CBE754DC6B76FCA + D4CE5D993EF584CC5C7E4666AFBF20479EFCA0AC7FE8D372E52BBF43EE7EC377 + C9CDAFF9BDF2F4B7FE3EF9E69FFE79F986BFF1F3F2CA67FEA43CFD6D3F20E7BE + F27B65F993BF47563FFB77CAEAC7BE45E65FFB12997DEE15397CFF79F5F3BC4C + 9F7F46C677EFC9E8D6CD7215F0F275195D06F97B5D46D6AEC9D0EA1569AF5C94 + C1B58B32AC7D1ADDBCAAB82CC3EBD8A23A7CAF182B9A6D55338F59D36285B396 + 79AE2DCFB282E787E791C0B793C38A5FF5B718885F5FB66F2BEB79E4EAEAC28F + F92DEFAD700D35BF14CEF9909EF3F05DE0609F03EAD87D1B09E0216D93E42FF2 + B695B4EB3BAF39F2D6F798E2DA162B6AD11707DA9138863F92BEF40B197F0B5E + EE511CA303EA405EE9A3B3CFD52152BD9D870C527BC0EB50E6EF9F7E52D2377C + 8B39E40992BBD051CF94F5E9032B88F18C0519EB096092B35ECEAD884DAF29DF + 09DE9EC811B15EE6ED29E7F31D65FFAC473E7DF6E7EAE4DA851E32FFDE21D2F7 + 1D3080773DECA22F6CF9CC7718EB20CF7A7C9FD2A7B7A31E79C2D7F100010CA0 + 6D8E0D708EC6F47E9BD07B7A02DF0DD76B501CA7DAD117C07102C1B6FC38C2DB + F422F3796C97EDC729069541EF75786F13DCA6D9B66AC63889D0B1533A6E03B9 + 5B8EBBB43F8A30CED33E14F07563AAED74238049FE82980DE42CC6AA188726E3 + 521D6722EDD33133C679D31B17E510766838B2194860F8864FF8D67C6B12D0F1 + 29E40A12C2B6325875A11D6D238E813D3036465BA98C29F5B461DE10E700247E + 09CAF7423A3FE8360FE998A7602EA368687BD437C7B5AF98070D61D5AFCEBBA0 + C7B1802C1E3B5E12BFE391148EE540FC1E577F48B53CA173C0099DCF410E3277 + 6A4A8FB725A347C7647AF1A88C1C99D6733D298333B3327CE8A83427C30ADF96 + CE5BDA530BD2D6636B299A68DF485DCC09752E387638A625E1CBF9624719C031 + B83972659EEAE6D3409D4D0EB4F7C8AD20862CE78736A9BC0074516F9F42823D + 5220B5ED02D81A8A184542E646F427F1124F14A7B69E00F6E42EE07D50E66DBC + FED103B1B1903E0CBC1504F05E7AA0179B1C7AED432D8C00563F46D00241EEFD + 3E90FF1A80FCED1BC47DE9D28C5D16E82BFAADF93496DE0D167FF731F98E587E + B02141E9E1570467B98D1CDE641E2810C0198215F908AE024E5700F7745CA8EF + 7C15E59CEDE386E21A97C75C0B3B47812C4D11383FC5DB98000E2B98473AEF93 + ECFD5A9E8B92F37C40D0D16389DF24801F1DFC8FF02D42C70B01E08BA0E385F0 + 80A8F1E9FB60FD485F700F1BF158C3561C4E96DA6581177315F9819ABEC433B6 + 1E7BD9EEA5277AB1F1F07E7B419D0F0E7EBB0D82699BAB9F43CE0EFECB7C0006 + 603E9F03BE8FEC7DB08DBA3CCB29BAE952785F75A06DDD4483E59C2C67D33BEA + 09E0424EB2354EB28A6D9920B7723DBA4EFEBAC04F1C9172F259F8AB21807DFE + 5183043057F0EE4508A7E004BE844EEA4703F94B02183808015C676FFF7C8F01 + 1203FE291FFF2D8FC08A0548F06F790442222CC0930BAC24407D066B52198032 + EBFBC08E97A30C04321A7AF4AB1308B8F0DFF8215855069E52F840544E0F90B8 + F536AC435D9D3C0712C0FD87B8A573205D3D184CF42041CB0023CB247553D4D9 + 204FE29740D9EB182C459A0676011F1CA62D82A50CB032780A1D02B7B0F30163 + FAEE8634B8CCFADE4F0EECA3AF833CFB9B82FA5EE0FD11835871E67C23E88DC0 + F6D68DE7E4A9D73E21979E7DAF4CAC9CB5EF2232100F30988FB40EB0AB2380B9 + A297642F759E04F63AAB8380BB22DDCE95644C206440BA94844D4AE4E4401BD4 + F1A492CFC30EBE61C3958B6CC3DBD38F07641EF40B02CC23D5FB3A404AB80124 + EA906F01DA1EC93923E4E057535FC7C3EB90F730DF38D6D806FB69FD431EE49E + E9B50C221258893AB5E196D0EDE5B3D25AC516D0E76464FD928C6D5E91899D1B + 3273F29ECC9E79528E5C7E460EDF7A41A69EFE90CC7FF4ABE4E6B7FE5179EF0F + FEA8BCFA037F5E5EFCEEEF976FFCD9BF2FDFF473FF483EFC033F2AEFFEA33F2E + 17BEF54764EDAB7F504E7FDD7F2DA7BFF6FB64FD53DF280BEFFBA8CC3FFF1E39 + 72EB65993EF7828CED3E2923B60AF8968CAD2B566FCBC8EA0D195EBD2EC36B8A + F5AB8A2B0A6C477D3910C09A8EACE33BC5611B68F4DD084C3BC6700E089C2F9E + 6B7FCE782E3D782E0717B5DEBCDA2D9429560107F2172011ABF65A279C6FE471 + 4F04F05EC0391F5E39ADC7A4FDD3730E32D8DA639F608376236CAB68FCBE62BB + 463AA31F737A1C4472DF14C78AFA0A7F7FE5C0BA76AF6A1969FA5B41CADF28E5 + BE9FB061DF8B7B2D91B38CBAF4EBCB007FDB757A96BD1DE500EF75E47D9BDE1E + E9803EE748EE06923E90BC61C57800C95D2FCBC1EAAA5DF9EC0C642F095D8204 + 30F2B4436A04A9A6F63C574046A0CCE7B2D9395096D3A3DC8DF04DCBDE8E3E58 + DFFB418AF728DE47FEFD63A4B602EFA1FEC3FA0E56DDA096F96D64F3196D01FF + 3EE3FB8D800CBEFD377BD3776C1D509FDF3C362258F338E70501ACCF36F6D38E + 45538E1FD2F101FB837C4E4FA4F5D332E0FD709C42787BCA8CFCD531A7278289 + 920C0EB031542476AB043064EAB34200AB7F1D9FE5C6794D4D9B246115CDA9B0 + 5AD764206249FE460218DFED6D62DCE9C0F16A9F6242DF1D3B775E9493775E92 + E995F3E5B7884128C3AFFA698F2F497B42FD28D00688E162A5F1A41EAFCAC2F7 + 8EC3F896FE0192BC2C031C0BD7950B991BEF7B705E4050CE7CAA639E7391743E + C2FA007C8364B6F2B8CE294800EB7C07F2B61EA7FD1956CBB447DA0799CEC738 + C728086000725BF9ABF335C3513D77C7D4DF9C42FD4E8FC9D1CD1979E63DF7E5 + 635FFC3179E2E5676476695E5A931332388DEF021F97E1432734D573AFD7A3A9 + D7C3D248008795BF9847EABC57411298C8CF23B50F8E00CE01042D495A823AE6 + FDFC9BF076441D01ECFD5BFD5C7C20CEDB91A76D510773E8513797469AD6DF03 + 652CA34AE4FA727F8C97989C707A2FA72DC0D88C076228E62BC69400CADE3CA0 + EDBD8158654E9EC293BF40873EC63D5339F0A004F05E760741AF7DA8C5432480 + 7B8E65B71438FF247E63BED7763C7C0C9DF93A30265FFF39C7604362AC0E596E + E361E121F0471DFD8C3EFB7D39D115E51AD0A727C2899C7DAF78183E7A42718D + 15397D07704E1CE91A313030A2E711F9BDCE99AB0FD2D3E70D438A410794A97B + 94401F70CD433F2BE73EBD0FB042D811C040E03C1F102989FA58E13709E04707 + FF237C44281EF419A42F848A2D5E0484ABF340F03E13BF953E242FB5878EA4DD + 4286FC9EEDE3855C223F48DB3F52BFA9EF9C9EE8C5A61B7C3B39D4D5E1E0B76E + 10EC6D7D5DCA7248ED3CA027E9EBC14112074C254ABFBE8FF495E601D83D08BC + AF6EF0B6AC9B4E3688D4EEA0B02DA049FE262826539C5C71A2C60997E6FD44B0 + 903BD4C9F782F799E60B3B27A7CEF7C783FA83027E537002CE09AB4D5A8B4979 + F8A736F37EF25E573680A44D08600FEA523DBF038C7C1D01DC41FEBA000A0320 + 16209950B923802DF013032C39A01E832704752C7B3DEDBD2CF5557E87B85CED + BB1701EC834E95E093439D8D91B6C96A5ED661395717F076CC03038A7E3D0E4F + EC3220C87C1A14F43604CA21B858ADEBED6893DA51463965072580699F961170 + A5AD0FBED2B60EB4055837075F07808CFDF4F07DF648FD7583AF477F1E856E6E + DB02DC87362FD9EADF825C8840E0DB973D3CE9E0C95B4FF21A991B0960E487E3 + 76CFA9ADAF0390A4E1B77E49D878828836D47939521238C8FB7AB4F1754902C1 + 0E321057945386D4DBD5F922BC8DAF0B783941123125E68C684BCA39328E3AEF + A3AE8C94A46E1DD81E50C896031949427244EF1D107F637AEF8CAE9E93412DB7 + 8C0456DBD58B32BA7E5526B76FCAA153F7E5D8F96764E9E68BB2FADC6B32FB9E + 4FCAF1CFFB1AB9F5FBFEA4BCF1A33F239FF513FF2FF9C00FFD05F9969FFFC7F2 + EDBFF04BF2F11FFE2979F90FFF845CFDFD3F2117FFE04FC9D5CFFC6539FFBB7E + 58CE7DCD77C9E94F7D85ACBCFC413976E71599BDF44E9938FD8C8CEEDE93B19D + BB32BE754746D76E69DF6E6A1F6FC8D0DA35195EBFA6E955195CBDACE9452382 + F17D627CA7B8BD7C4E5A27CED86A669E1F9E239E27E6D3F386F3E27500CFD590 + FA04014B80FC1D5D3E2F234BE71C091CEC40F0E21C06603B69DC23E19EC0BD62 + C4F42A0960BDC64600876BCF6FD4F21EB27BEB84EA715F3802187992BFD627AD + 83FA381E5E637FACF6BB73F716EF3582F241D48DED5A3F35E5EF00297F83767F + 47B907E476FFC4EF2F23CFBAACC3DF3FE1EB534F9B548794BF375F9F7DF6F032 + D8D027FB11761CD8D632FCC317F438FFF019803257FD7A99B7A18C04309FA578 + D6A618D667614A0CFB6734BF07ECEB97CFE42A7C5D92B3F463BEB4EC895BFACD + A5D07B4006B03E403BE4F11EC57B09042DDE3B78FFA13F3836D8F41FD577EDBC + D68F68834887DF88F4FD95BEEF00C83D014C19574CD317EDFD7BD8BED91CEB59 + 9DD8B731BD17F01D601C8B11BF11AC8F947E3D3AFCC73CC171074042D7CB9167 + 3DCA5250470218309297647042009B4D5C39DC0430A6822F2384AB28B78006C2 + 58AC1CE7853C64B6EA17E3B7292D27E807118BED9C15C8E31BBD90DB36D01867 + C631691867EAB85DF3E3AB6765E5DAD3B272E92919D767631FFC80DC451B133A + 261D3F21ADF140FE820C06E90B02980434FC83FC0506B48FF44FE23725802B63 + 5D371ECE228EF35370DCEFC7FF94736E92433A7F01BC5FF821BC9C7F7845DEFE + 648AE3CBD4F7730C2381019D9F5509609D174602B8353D27AD9905691F9A920B + F74ECBEFFE23DF2A3FF1333F2E5FFB1D5F2FEBE77674FE39A4F34390C0C76564 + 764986D0F6B8F66F4CCFDFD8A2E59BE3733A07C49C50E7AA1584796237E4E6A7 + 0488D9E6D8F12C71EB0119E7D2DECECB28F77AE4BB11C04599B22E04308EC5CF + A78B7AFB01E217ADE92ADA7982B7177D7F04E2253E1E4340E7E351444EF66880 + 36F606E3952CE762666647C23182F2429FF8F1781002B817BB83E0817D1B01AC + C8E9F6098B2D8348363239CA72FDDB8300F6B61D75BBC0C7D473B09877FFB83C + B604F03EF9A36E7D29FA0AA8BF81FE4000F74702D7DAE81F09E8A13DFA4B91B3 + DD0F1E969F3D515C63454EEF61D700F961859E1F87DE0860E81CE1E9507087E4 + 1989C837E6EA3C5C94D7BBE3DC47B9AD0E6FE23EE1B9C2F9287D14C770403C62 + 021827B237F2368BDF24801F1DFC8FF01121FBD0AFD1117524ED0323FAB4974C + E2BBD27EE665F650E1DAAD95B997761578F996C80FD21E0E8CB48BF9B45D0FB3 + C16AD78696898C5D1DD8461DEAEAF8FEE590AB47E4EC819C2D017D3180B5C1AA + DE2F3503A4127BB791B3F12081DA2B723E7A416EC291F37F501801DC0A846F8E + FCED460023EF27825EF7A0A03F3FE9F43ADF6E5A27875E6CEAC03EA42826DF9E + C055708B2EBF4D570E1DF523B16BDFA672846E41E0463D41796ABF1F0218818F + 8E2049247D910208E030489202F60CB414FFB67732D8B01DC8B9ED5B5A17F075 + 4B021876F8773DE4214F14C4B00B343118C540950F5291C82552796E3B675FDF + B7E3E16D3C4800F74DADC4605C0992B1048384246E91324FF29632CA7376A96D + 4E863C0960044AD3402CCB08E6123E308BBC0FAED20F6D5250EEED5987799653 + 78BD87F7C3BE324DC1F691EE07DEC7D05C190CB75481403702ED7D08FA6A7F10 + E8861C32007906FD8954071F01BB5A06C910F22C73752FC9DF1C014C598113A7 + 84240C803C891B0FCA3D280769C3BA24783CA827C9E36D20F3A8F3C13609DFDF + 54CFFA1D3205C937126C75C49C916E8A0E22CED950C6143E523BC8405482C0F5 + 84661DA837A27449FB5DAC48D5FA8AB1F5F332BE7E4146D6CF09BE010CA27568 + E58A0CAF5D91F1CDEB32B5734F8E9E7B56D66EBD4B4EBDF209D9FD926F96D3BF + EB8FCB8DEFFBEFE4A51FFD69F9C04FFD03F99C1FFB19F9CE5FFC97F2FB7EE957 + E4533FFA0BF2DCF7FE4DB9F0FD7F472EFFE97FACE9CFCAF61FF84B72F1DBBE5F + AE7DE9D7CBE6BB3F4BE6EFBF4F8E5D7F45662EBC20E3A79E94F1DDFB32B17D4F + C6366FCBE81A08E0EBDA1710BF57A5B57C5971316E057DC9BE053CB47AC9B6AB + B6FEEA31741C6B3C5F945347E0DCA5764688E39C2CE19BBF8178459A12C08198 + 0D0430BEA71CC8DF12F8CE32EE15DC1FD073D535C97790C6B88E760FE07EC135 + 8DF69E0066FB4C81A1C56ABFFD7D02D02FEF43C0DF6F04E4BCA791A2DFD6BEEA + 28471E7D611E29EDD96FAC1E1F5FD3FB470112D8D703FC6F9432B68194BF392F + 67FB48D92F5F973ADA41E6E55E86FEA14C221704B011B80E1562370109E154D6 + C27335022B5F8121D50D691F8141CD536E88655B218BE7B0C256242BFC73D89E + D728C3AF0375009FE17CDEF3B94EF2D6DB7B1BA4DEDEBF07521F281776F0A979 + 12C056D63AD6674D5B7A0C207E4902DB7951BBDC7BACA8EFF27C37D286B2B40E + E0DFC140E388BED3A1D736D13FEB23EAA18CEBA0791E13EC793CB97E11BE9D6C + 9BFAAEF5C42DC7025ECE3CE529BC7D5A4E413D61C70CA8BC619FF928D19CC518 + 0B7FFAC3D84FC78033614C17FE08B854946D6C16533F2E4C61AB92A7C29F086D + DB68F5EDD13F73426DB42DCDDBBDA7BFB9B68E0106747C67DF19C696CFD3615B + 69DB027A5CC7BB20824DBE6268AA3C2580413C636CEB895FC28F7969C3B13075 + BE6C8863FB149C47109C0720CF7983479D0EE59C3F0072F8850DC8DF7EA4B1BE + 9F8FE0B338B00D804D58F9EB896003B67C36E8FC1064F084CEF5F41CB466E665 + E4D8A49CBDBB2EBFFDDBBE58BEF787FF907CDE6FFD849C38A9D77874481A1393 + 4600635ED81C537B90F10A90C0F504B0CE3FDD5CD183F3C640EC02615E6A7353 + 37774DE7B0D4F93931536F47DB3ABBB44C12B8D045FD5EA09F1C011C74BDF931 + 805CB698C67415ED1E885F0FA7EFD714286226315F178FA1FD7EE2490F06B451 + C6233BE35D01A9DE93BF163FA31D09C708CA0B7D9776520298B6DD90FAC8D93C + 081ED8F74324800D2480633CD9F7CF83A46F712D623E670B74B493C0C7D3EB60 + B16E1793EF8CCB479B84B738104979100E681FDC51D73E453FA607340F02B8D9 + 9A90C1D159690FEBEF38CA0B0218C8F98A607B292A767BF8A8806DA66DEFC7C7 + 7E505CE3CEEBDB81A25F25E9E9C94FCBEF450027F588823B7CAB0860D7EFCA35 + 8CE71DE58200B6DF71E7F114C770403C5202B8AFAFA56828DA8A0310C1BF4900 + 3F3AF81FE12340EEA10F78BD7F81177ABE082A2F838700F5C7178DBD6C9C8E6D + 5BFB9917DA83A2F2E2746D1636AE2F86F8C2EE047C94C80FD2AAC056C439790A + 0E440B59CD00B303B46F687FD05643E17486A21CC1BCA6457B3528EA3AE4ECFC + 8098C8D505523B82FADC201B2806B17828E3DF5B460297D7C79771BDBD8FA27D + BBEE499EE725C2DAB381B26FBBEC531D78CD8A32EBC6B40EB0E504A311F37593 + 8E83437DB566B5BD437A7F60E214B68026F93BA0932F3FD1F3932D945378FD83 + 00BE3809054CEEFE395C91475937F46A9783EF87879F847B7802B809A252D3A0 + 837D89EA445E6D46E68CC4ED207423AC4C6899E81B3E5E218441FE22C0D0189D + 0F32F56BBE611B831F048221030894689D208B819324E01388D8CEA00D832B00 + 83293E5F694BEDAB0470B06B4D9FB0D4D7B77662DF40F282006E4DC3BE248273 + 0430D00FFFAEEF0848A5C42E915BF58B3A5EE6E5F48594E0B780A963D9ECE216 + D0248019CC03094B18411B038116184CC85A2371B19DA09311B4838D91BA8EEC + 65DD9CDCB6858EC1561F90A58C65802B83D2402D83A64843A096C15AEF9341DD + 3210CB3621639EC1D5B49CCA98D21765EC2BC0BEA6807C2FC08EC767B2A26E80 + F9523982DCD033686FC17C95B1CC003EE183FC006525F110E0895C944100233F + B278AA42F4E608E0427602845420A78CB841AAC8913500655EEE6D01F8818C24 + 9B979318F2FE6C2B5E109E6617F4B423D23E60EB5D4F46414FFFC8B34FBEBD2A + 1987630DC74B428E7AC848C281E433A20FBEAC1CCF55A10B65A4DE7F2833D57E + 831C03C1E8DAF2F6B429C83D939D94111091CBDA86A66D10936B676414AB8037 + 2EC8C8C625DB6279740D04F0452BE33BC0B3A7EFCBD2B517E4CCAB1F9727BEFD + 8FC9CB3FF2D7E5A93FF75372EBCFFEA4BCF297FFAE7CF9CFFC43F963FFEBBF97 + 1FF83F7E4DBEF9EFFFEFF2C65FFCA772E7477E512EFDC83F91D3DFFF33B2F987 + 7E544E7DCB1F97B39FFF55B2F5AECF92F97BEF93A357DF25D3E79F93B1534FCA + D8C927646AF74999DCBD27E31BB76474F546F806705CFD3BB876598637AECAC8 + D67519DBBE2EE39BD76474EBAA8C6D5E56FB4B32B17EC1002212AB9903391E8E + 9FE72510BF40207A834DA947197547B1B534885690BC0A12C000E446C44267DF + 020EF710EF4BDE4FBC5F70BE074FE8FDA240DE08603DE7B816768D23681FB680 + DE2956FB926C461F4040A36FE823AF3DAF7528EBEF827EA38CE514A6D77EB27D + F6997900F7B92770FD31F2370082952B8081F02D666D13F77AB409DB6207DF94 + B1ED5E0960D605A8A31D64F441197DF995C943B80E35446F90EB33D2807240D8 + 12BA2C53D68A76A90F1C2B8E1FD71B762079418AC2DEEA4106A2147205CA68D3 + 48623EAB23F88CE6B31BF0C4AE3DF393E73A65B4CB81BE4ADBD207DF25DE86EF + 23F4A9784F29AC3E6CF43806E6B46E3C26D44BDF653C26CA90D2C6BF5B69EFF3 + 2CD3CE6340D1A7EFF1A6BEE747706FA84FF3A575700EAC0EEA3A9FDE37E5F487 + 3C41590ABCF749CC32253C710BD01EF0F53D8A6F02277501C8592F85E9D406A4 + 6F09AD8755C315C2177910C23A3EB5316139FEC378CCC8DD88B07A38C80B1DFC + 991F8CDDA0F72470F89361BFB6DDA7BA3EB5EF433DF8C0981364EFF4AA0C1FD2 + 730EB29704F0444900071258DB03018CFA3A46ED7363688EA3FB75EC8D94E360 + 5B79EC6C6867BA6857D8635C8FB90211C7F90D9D03704E61730983CA6299F30C + EA5319C1F9452A03D8166D98EFC39C097F96D532FC72657000EA06021829CA9D + 04B0CEDD2615133AE7D3636FCF2CC8E8F14939B63D22A76FAECAF5672EC8C6A5 + 75199F9B56FDA83427A7A4A5F64DF4617441E7683ACFC03C4DD3065210C0E398 + 13EA3CD511C09C27FAF926CB36CFC4EADE3D08600FCE8553D03EF5E3F5DECECA + 3A276739258053D04F1EF0A3E948274C9EADE3105716F7B735DF2E095CC45800 + 12B9455C8C80ED20764143DC23E65D7DE6112B410C0669A57ED47914BAE65409 + 67FFF03125652CB28C6BA648F59EFC050A3B128E119417FA2EEDA4B163DAD621 + E72367571F7BDD1B7BFADE0BDA762D016C04D03EFBC63AB19EEF9F477A1D3CB2 + F6693B0E3E96DE0D16E7461C3E4511078F76096FD18FAD706D3BDC7DE0001C10 + BECFDBDF9FC872442B90F4C756F746D8EA55F8521BAB8BBCCA5B43D332A2CFF5 + 91F123962FB7F8DD9BB3623F5254ECF6F05101DBCC2167FFA028AEB122A7CFC1 + 919E9EFC34CEAF270218DB3AD76DEDFC166C019DAC5CAE5CC378DE07B41C0860 + FE6654677F12703EBCCF832025510F0292BB29C93BA00DB45AA3328025D85167 + CBB69D4D5718018C7AF49BE69DADE2BF1C021837CA03C2FF081F0495877689E2 + 411F1FEC45396393CA3B5F060F071D04704CFB01F62379A13D0C545E9CF1855C + 69CBBFA801CA3B001F25FCE02C0CACE2E0CA746CB36AE731D0D24124485BB782 + D7CAA62BF5E683509B01950D208DF986A2093462AAF58C748C753058B541ADF3 + DB800D74F015C1B63DFCF112A90DC001B11F18E7EA02D4A7E0C0DA83BA502FD6 + D76BD1FD5F7AC1D6FB60DB298AF3EA807384F365F58AEB12E43C7F39846BA9B6 + A8EBFA5EC9E710F5D65F9D9854005916B02FCF590304AF91BC5A06588E30E297 + 0430C8DF82000E9329238273132E45DDC4AE63D2E788DB0E9DAB5394E3A412C0 + 64D2CB690BB9D7D1BE0E391BD6DDAB3E27B51D709371E63D48025B191376ADD3 + AF13F502EA171840E061744EF31EF3060428F0FDAAC678CC83A04520436DC28A + 61FA51DFD62EDA9B0BF6E3B03BAE93E86306D8710BB43EB533980CBA9837F95C + FC0E70407F247FFBB51F2942B004011690B725815B045562D0054099F27E10B8 + 53F3D29A41806A59F3205A419C2270B5A47DD1B6F1E7033DDE16825F91681D3A + B4226D04D2261088D1F3AB3E9AD3DA070B4E05F44F6A7B1121005592B545402A + 963DC1EB6D7CD9EB537BE43DE19B02013B1FCC2B827A38E618046C20D008587E + 4D6C6B4110B6965F17236F0D619B42D61B50D8F6841618AC277B994FED7CB015 + C15806873D20330218C15C2D9B7DD4214570D702A2316D1D05C1BBA67AF884BF + 1D691F4550586D6737F418B4FE912D43F330F2047C94416893B97EB16F9031F8 + CAE06C6A97450C9A1BB0AAB60B8AE0BCA64320559D0EE72104F7F5B8704E60AB + 3E8BD5680AD4B1D54FAE1C08DA18BC471B7A6ECC4EE52079F99D5F4FE45A1B51 + 069217F03A0FD8B02E56000D22180F7BE415E82B64C87B0287840D499B540790 + FC21A9E301FBA0D37AD8BA57DB04913B84F25220E9DAF35A5F01D9C8F259B369 + 1EDF9191A5B332B176DEFC8040428A2D7C3DC9847C2096020106190965CB9F08 + 18425B0986B5AD11FB4E2D8838F5A5EDD33E00E41C48C2509F3684B7F57E43B9 + 94B11D00F9B035B1DA811C5E513DFAB0A2721061969E551DBEF97B461A38B77A + 6CD08FAC6245F03919DBB828139B176478FDB48C6F9C95E9939764F6CC7559BC + FEB4ECBEEF0DB9F23BBF4B9EFFB1BF212FFECD9F93BB7FF9AFCB7B7EFAE7E5DB + FEE9FF223FF1EFFE83FC9D5F13F96BFFFA57E54FFCC2BF924FFDC43F94A77FE8 + 6FCA853FFA9764E70FFE69D9FAF6EF91EDAFF81DB2FBB12F90F5777E50969F7C + B72CDC7A498E5C7E560E5D785266CF2A4E3D21333BF7657AFBBE4C693AB17357 + 264FDE93E933F765E6FC5372F8E2D3327BEEBECA6ECAD8CE1519DBBE24E35B97 + 647253B1AEF9B50B32BA7ABE3817E9F90BE7CA91C07A0E8C1C56204F0218F789 + 9D47D4C54A5FBD5720B3D4F2E7EC1A7982D4FCC01FF20A92AF2D90818A36C860 + 9C6BAC08565BE8C3750CED1764ADDD739A8FC07D1CDA0EF7128EC1231C5BF55E + 21210CBF6C83B076D13F6D8B28FAAC79D4B1FE689E042D743C47E6D395AB403F + 79DED40F52DCE3F86EB21ECB209E0931B5DFEA1C88521C6F90873ABEBEE6F15B + 88F666077F49FF019C37A6E1377B3A12D2E15802110D5BDAE1DA9520418BE30D + 36F053E64B5940B8F6FC9EB93E5363FBDC021C65E8408A96BE83FF202FEB991C + E4299ECF38DFF0ADB24140652464C3B3BF94319F02CF6D222D87F74BF0075DF1 + 3E29E4C1A6AC17DE4778D7E13D4702D8DEB90074C7F5DDA832FB6400DE875AC7 + A03AFA423B789787F71ADF45C19EE07B34F73E850C36FE9D6BDFFE455DD5C31F + 64782F9B5E6548F9FD5FD6B3D401631CFF2E371BD48B32C0BFEF91929CA5ACA8 + 97DA23D5F10E528C29DAB0D3D4C65551C7F1573FC66646B6EA58C964C1075293 + 61EC64F51CE0BB82751D6771BC15D274F56FD826BA1C97A563B4502FC8FAA757 + 8D94B5F64D86348C1F6D3CC971A4B64D39BEF90B60EBE8FE091D9FAA8FF68C1E + 3FBE318C6FFF62FB67A4207E8DFCD5B1ABEA6C15307C8238D631F300C6EAB612 + 38A01111F27A3C9A92F4055232D8EBF0999602E6B7CCDB275C5C6AB03982CE1F + 74DE40601E0299A5714EC179852F73CE61F31805F400E5451E7328D343067B10 + BCB0810C69247C23B80574DF98CE3BC703300FC176D0360FD1FEB7678E4AFBD0 + 84E68129951FD274568F13D0B9E4C431BD069CCF619EE6E76CDA8EFD3158E73D + 119C4B728EC9396505A88FD5B2190C606E6779CC3D35CDCC6F01CE81FDFCD6EB + 00AF034C1EE7E4561F887508D8796298B6665FB14559E55902B86C0344716833 + 0164881B206D0712B77F10318E100721898B384D7F44F1677A8511C1A843441B + 238163EC06F5BD8E601BA6F73ADAA7F2886C1D876EBA1445DCCACA483B5112BD + 016CBF94475BCD77231B194FEBA8A730E2974863A9CC3BF8BA44CEAE7BFCB58A + D4C79EBE3308B611380EC4A1337695F8F03EFAE8ED7DFF88C20EF90CEAEA581F + 5D5FED3A400F19DAB3987A158CC3079DDA31EFE54599FA84EF1870F904FDCC6B + 1DCBC7BA94F76BDD0E447D019505A20D2B75C7A401120EE45C03849C3E6B5B93 + 2643B9A1E5861EFB80F67340EB35346D2A1AFD9AD7BA96D7E3349DFA0E5BF96A + 5DF5D11C9A91C1D1C332A4CF9DD6A0FEFE54677D87BE475438A183F25C399F29 + 77A669A18F325FC7E4F4D70BEC1C47E4F484EF87717B55F45B9AA957007D850D + B8C121B5D734214DFB8D508DFE8C94A5BCB47924B0B6144DA4F10F0238CEB815 + F880F6BFA9E7152BC59B6D2C96C4EF42EF41D5157D4B8EE540F004EA410042D7 + 13C00D3D98564B3BAE07B4B0B02E376E3C21274E6C46FDA0DAE7FD7878927820 + A25F7DA37E40C843D6505BC0EAA02F3CB11DC8DD20358837F9630B7B303E2620 + 991A115E00FA52F0FAC466BFE8F0E964A93C8B5CFBBE5F5E1F5F686F07F89732 + 0748A6C3CBB6B0A9DA11463236F5C50372318191BAAD40D27A99C91BD3D21A00 + A6F40517D056D9A0A2DD3FA9F24969ABEF16067CDA4E43F34D1D28075FEA0324 + B1E65BDAB611C611F05B90CFFB1890A660DDEA40B52CA7728F72A05A6DDFD709 + 79A4F5F075F702CFAB07CF3DF2FEBAA4D722070CC48A7F95127A3C454A243AF6 + A7D0C7C98AEF2B11CE1152E6717D75E20682B7A9F514C87B80FCB56D9FB1D533 + C10955DC0E9A932B3F79AB43755217278FB1AE976332E86D987AFB3AA4F559CE + 2167C372EAD3DB78A4FF6AA6ACE126E69CA47B9472AD0718F1AB93FC0ECC19B0 + FAB66F0CC1090434160BF419D95A063A2CB05110BB7A1CEABB6F4C31AE6D4C68 + 1BE3EA33A2FC17BB0229013964945B398F10143991A0EC1F806F8C41CE600BFA + 49A06C811E05CAB67277EAB8A17508AB6D11785BB1001A0257D83279F8E8B64C + 2D9E96D163DB410E1C5A95E6E492B4D50F48DFC6F49CD655688A004C633A07ED + 1B024D91B0F5C12ACA8DA88DFA5E51217913992F3762D0CFA308DE5186A09E0B + 6856E0029E80277C3D014C18B1ABB0A0206C9C0EB0736881CAE09FC153046E73 + 40E0970167233B55E661815AF4D1FC20F0BBACD07E80043EA2364776A4797857 + AF9FA687F45A2A5AB3BB8A1D691F869E01590470B5CDA35B321849E3C16321F0 + CC4035FAE383BC00FB48BDEF7745867C0CC05B90BC062454695B94634A3216E4 + 2AC9229234204986411881EC00D476F03802EEA59FA1B95D1952D990DED7838A + E1E36AA3694B8FDBB7672BD2344D6524795314FD029913FB6EB288C24EFB4BE2 + 08644920663A091D124C80D7D19684137581603A23ADB95312564E8238C26A4D + 6D670144D259195DBE5059C50919C9212090BC657B81C809E9E8CA1903CA2066 + 822C906F83200897413882A003E958950D6A3B9013D47B047955E7EBD01731B2 + 72BE921F5DBD60296C47B05A55CB83AB67A5BDAABE56CE499BD073D05EC1F773 + 55AFF6F88E2EC8CCB1D58B3206D2F4843EF3B4DEE8869ED3B56D99DC3C25333B + A7E4F0F9CBB2F9F43BE4D2E77DA19CFDCC77CBFD9FFADBF2FEFFE997E4B5FFF7 + 2FCB67FFF23F97EFFD957F23BFF0EBFF49FEC5AF8BFCAB5FFD75F9C7FFF63FCA + 0FFECFFF427EEBCFFE23F9E8DFFC7979EF5FFB3979EF4FFEB4BCF1633F299FFC + E1FF4EBEE08F7E9F7CEA3B3E23AF7DC5D7C98DD73F21AB775E9223679F96E99D + FB32B5FD84E2499939A565C5D4A9A764F294CACE3E25B3979E91D9F3F7646CF7 + B2B437F47C6DE8795E3F27A36B17EC18FC7900784E8796FCF94359AF73BCBF50 + E6FD846B6AA4A1E641D6FA15B3806DF9ACE707801CDFDB0590F73E0B3217367A + CF002DF50718311F6D8A3AAE4FBC1FADEE42F8630200C2B9893F33E05EC7BD1B + 8F07C7091960D75FAF2F8E85F53A8076713CEA9F32B609F037461B3C63486E7A + 1DEAE15CA5BF437E23D97E5F38FFFA1B6CCF9FB6DFE6A0A5207E4B40C6D5D65C + F1CC7CD80A3BA428536EBFF7D837B4EDFB90F617699A27995CA22476ABA8D7F1 + B94B321732B643504FDB1C822FD8C007CAB89FE00BED681EEF9F087B8FB8721D + F80C0ECFDDF2F94B54F5A92E20E4E96FCB8856BC6FC3BB0CEFABF23D6CEFBA28 + E33BD948570564D0C10FDEA5208181CABB9229FC447945EFE41C2F5086BAC3D1 + 37CA7E4C81946319CA2D8DE314C36C2053ADCF11C8E3B30A968FF53D30EEB1B1 + 4F1C37B15E0AF8F66DB5994FE428FBF152659C04323512AD20780D8742DAC0CE + 2C8A811920E803518BBA411FBEFB0B9FB4853FC848F4A672F421C891F25BBEC8 + 079BD2AE1F32F447654D2372F167CA88C998A2EF4E3E806F0A2BBCAC00EB2830 + D6863F10C2661FEB84FAB029C7BA04C7D63912B800C6E40AB357F44739D2A2DE + 94CE8340CA123A6760DE7CC679466EBE41A237079B3B4594323F67425EE73D6E + 0E853FD6A67322027AFCD196BB128148A6DFB27F2843A76D3A6279408139E400 + 48602382D59FCEC5F8C760FB7330F41139F2D7CF37B985B2077D16A99BD77A70 + 6EDB0D7BD5F1F3680F4F00A3ECE7DCDDFC11B40F38247D838883440C012A2BA0 + 65C44706E36E699A2F1639584CA444410067E228159D8FDDA88E795FC7A3624B + B01C6D7C1B697DF62F953F28E83745D5AE8C59EE05D8E56C19E3ACC0C53EF742 + 4FFE32E8B5CEDE36D097E7A4B89E166324923A38BE7D1CE3A382276F2D0E6FC7 + 12FB4BB2977D25621DABE762F7F4C57C51B6187DA92B6C62DD1C586F00F5627D + EA06FA33507B02646D6320C8B0480CC46D4B8FA7D5547DFFA8EAC6A5ADE56663 + CC085E10BE0DC4C01B53160F1FEA0F68299A7DAA5340DE523D7CF783D4C6B9D3 + F384FBD99E17386F680FD0739412AB84ED38E9CA9D9C50244A53992F53E6E5CE + 67875F96D5DE5634C326EAAC4C19ECE837FAEEF0B5177C9FBCAC4E9ECA3A009B + 11B50D4467C12B5664E019CB36ACCF4D94837D1D58BF0315BE31419DDE08E0C0 + 5D924B05900787DA6EE93DD79EB03CB855CF8D76B47F10F8033B08D821761C9D + 1D1E9EB67463E3B4BCF4D27BE5E4EE453B00236DFB835DEAC7C31F6495002E81 + F2FE08E05E6E9A889E6EB0B710FAB0785C51791003F610C603A26AB71F74F874 + B2549E45AE0F757DC20BEC6D020E303C7AB52B0843472C7AA47A96B1DA172FBD + 014543E50695E12588175EBFBE70ED458A979B0E921B0AAC082DFEDD66754AE2 + D7301008603F78F603A3838003A8B49CCA53E406ACB47F18FD4AC1F3EAF14004 + B0FA2C06921EDAF72CA29EFD29E49830684AB9873F470125E19B23801B48DD76 + CFD5899682C46F1D9C6DFD842D8FFDD8D3AEDB24D14F243D72365E96C2D7F548 + 6D6C42EB26D99C607B506EDB3D8F69DD5A025861AB72E72BE02A5B12C065C022 + 12C01624405B084620A8A17E101430A84ED35CD0A280D6CD91BFA50F02C10B90 + BB25520298C8056448FE521E8225DA6F45FBD0096983248D8468DFC4A20C1EDE + 9033B75E92D73FF969B9FFF21B323A7F529A518F15C1CDC90569CF2238356FDB + AEB566104C02215C87724B67049D9032188572EB50D0791BDAA5729F325FB547 + DE11CB1902B803DD08E00823809300A50F58D20F88720B7E6A1D0BAAC22F11EB + 30B0692B581024459038066C19B4651E415EAE00261048F68166A27944FD1FD6 + 6356201FC8DD6D6D675BDBDF52689D59ADABE5B6CADB47CB0076B0DD320238E8 + B60B02182802C7AEAFD411BECFA92C04CD1148AF22258029EF0CCA97725BD19B + 211B8CDC88E484ADB645BD635A4F8F29F56D3E40044732D8886FD5995C01D216 + 3A90C3209121636A759C9DD982580622D98B94A4308961D3C53EA7A413511C47 + CC578E2DCA485A19A8B35585817C1A3971B620A1ACBC744EC65602F98B3C084F + ACA604A9D40439B2180824F8092BFED89F40568FAD629B5B1084E8B39E5F05F2 + 56072B15970219E789B840D2415FC208492096535BFAD90B2039C7D602D949C2 + 13F276F4090218FAC1B5B3D2B26D9ECF486B29ACF86D83005F559D9E8FE1B54B + 619BE7B50B321E57CFE29846574FCBE4D639397CF6AAACDFB82F9B779F9653CF + 3D2F2F7DF2E3F25BFED077C917FDB5BF2A5FFA4BBF245FF72BFF5EBEE13FFCAA + 7CE77FFA35F9B1DFF835F9C5DFF875F9E5FFF4EBF22F7FED37E49F89C84FFE86 + C81FFEB7FF51BEF3FFF8BFE53BFE4F91EFF8B7BF26BFEB97FE857CE3DFFA59F9 + A63FFFE3F2F57FFCFF211FFFBADF25B73EF4B9B27CFB9D72E8E45332BE755746 + D7EF681F6ECBD8E65DC51D195ABF214D7C0778F58A0C6F63E5EF3519DC382F4D + 3DB6D68A5E77ACE8C5AA5F43793E703E0994FD2AE992D0D5BA207DE3F6C5B62D + 34EECDA84F4192B6206B9D1CBE48E44266A4B0A62D4D5BF8E300C9DF04B69533 + 740ACBDB7DA8F75FF40D589F14E687F7963B3EDE533C56B38FED239F82725C6F + 234F355FDEF7E56F0F3AD8799DB7B1FBC5DB1881ABF71C52FCFEE26F112B995B + 20AF2DD573633648F5F8E2EF9618C4F1E177011B238003688755C16195B0CA5C + 3FFCB1B18F24657D99F072D421BC1CF07552782237F503407E2002D8C1E4EA83 + 44AC07643914CFF9F8DC25F83CE63319367528DF1765BB3E0FF8F73141E2D783 + 3AABA3F6246BF9BEE49FAC7CDEBF6B3D28E7FB95794F0023A50F1B8FC4773776 + 06B13C10C9D78268C59825DA02248421A71F8E873C01DC6DEC0472DAC619A8AB + 6DD8180AE320CD43669FC2C0D8C3F5C58FAF50A63CD8693F2AC42FC660252843 + 9A12C05EEF650465A92E27871FF3ADF93E9515AB79919F2A49DC94E8F5361853 + 538694C43001C2B719EBA53AD34F29749CEBC7D2BE4C80B0ED9BD0317F2CD38E + 2848DF085F2F05FC202DC9D5CEF94637EC4500DB1C88F322CE8D625A9D135511 + C8DF2A8A4FD4683E5B47E1C9DC72BED809CE2753FBCEB966B0A1DCEBB10A389D + D7127E7EDB0B58AFA973CE160870CC45337640DA4E3AB7F773FC1CBC7D41F456 + 48604F002B101F193C54D931ADB21A37451A4B5159A5AECA2AF11A574EEB7A5D + 11B3619D98C20EF1192398354D7DF8184E5AA6CC809819E2775DE2663974F889 + 0831B87C5C723F48E39CFBC581FDC1B607FBBDFD42079BDCF92172F51E0390D0 + 056969F74615B955CC0316FF0D28E2ED8CCDA7A08E7609E8C7A3D0F78D89EDF2 + E97C413F0072B70B010C12167D6CEA35082B7741F04237AEFEC65437AE408C3B + F8B7E3694FE9EF6B465A5AA7A5FEB008AA8DD8B811C0E346FE367DDF908FE7C3 + 487394AD9FE07640A6265C4FC49E846A8E2CCDF15839BB1AB04D4BB13255F38D + 41ED33CAA91F96A36CCFFEA6F0BEBCAC4E9ECA3A001BED73243A0B5ED1919F61 + F56FDA06CA659D1CA8EF4007E7B807621D90D2E85BC17B468E1365F0A64045BF + 573FF603EFF0E028095D76181819999643B37332363EAB3FA86AE779303C30B2 + DB446117D31424861B00EB283A4E7281789179C1FD85AFCB3FAEE0C3E43144C7 + C3F82120E793B29EDA722F823DE15E588F3BFCE065BF2808C3845C24BC9E792F + C3CBCB08DB960EF020D7175F4361FF72C27551591838AB3DB678C16006D70AF5 + F40569AB871BFAA2D53AC8BF1904704EEE41795D1F1E56DF3CD2730B74238053 + 5B0FFAAC9D14309F29177529E764C6E90A405E01262F61952FB7782ECB9108CE + 11BF75E88100F6B26EA89FE0E56D004E205319E5CC7BD4D9D6C1D7F548F50501 + EC26DEB9497780DA2BB8D5731DB025592E4051062A7CD0426D8C34468002F658 + 39CCBA415E041AB05D72CC0356C7EAED1DD808D0FE2504704A0273D5721A9801 + 3CF90B2020C2D5CA838796A48D2090FAEB9B548C2FC8D8F11DB9FFCE37E4B77D + FBF7C887BFE86B646AF95C20C1A79705DB440F62BB6590BA13F362DF058E04F0 + C014824C01FD937AAE0A84E013495B4FDC225FB77DB3470B7076DCF6D907B282 + 0E79F427D8F542001B397BB81AFC44C0330D7A7623802D9FF11388E088D90C5C + 5B3EC0CB002E03BB08327B121881E48E60F311ED87278055160860B53B8C2D9F + 036CA52F0851DB269A40A05665204B1194354234003A1E4FDA376F43E4E4814C + 0E01F51C5222B85B40DE30AFE764B15CC107B2818484110E0886A39EDA061217 + 087E0A32602EC0885E90BA0ACA0A39EA92044EEDD4C7C8FC4943410447E09BC1 + D686231C0CB1AF84275A50CEE9B9921228C830E88A3C481AD882D00291775EF3 + 67F45C0482093210BF90C106245941BA9E0021184830F3857E688A3CC03CBEBD + 0B82F0D0EE553972F6864C6E5F94B69171EA5FDB2348C811242249D011247E8B + 7E387B0FCA3DD00EC95F12C0909B3F1CAF1E2BE443ABDA4E244887B06A755DED + 372ECBD8E6554DAFC8F8D635CDC76FFD9A4FC5CA3999DABC280B17EFCAD9E75F + 96E73FEB73E4952FF8B4BCFF8BBE487EDB377D5AFEE49FFB3EF9BE7FFC33F207 + FE3FFF44FEC0FFFACBF27DFFEEDFC8FFF337FE6FF9855FFFBFE49FFDDFFF41F1 + ABF2BFFDDA6FC8FFF49F7E4DFE9BFFDFBF966FF827FF4CBEFA17FFA57CED2FFF + 8AFCF67FF84BF2B93FF2A3F2CCD77C8B5CF8C8E7CBE9777E44D6EEBD5B662E3D + 21E3276FC9E4CE5D99528C6FDCD1FEDCD47E5F97D6D2553D86ABD25AB92CCD95 + 4BD25ABB2C831B97A4BDAED75331ACC785ADAD41EA82F8E6750078FE782EB9F5 + 33AE7771FF681EDF3F1E8BDF0E2681DB0BB88D34F2760FC5327DC03F53E6696B + DF275E09C08AEBC125BD97F09DE0480663FB63F409603D5CC736F4B887EC5E2E + EF27A2BC4FD40EF7328E116D66C07BBEF08F7E6A1DFB56B5C2CE0F6CA31D61F6 + 381E8511C0386EFAB2BEC53EC6FE206DEBF1B4F01B45DEDDFB04ED5987F54828 + 138100D6738C3F5EC4FE02BE7F00642469ED58A25D5A26783CDE670EA9CFD457 + EA037A4FF4129DB248F426C46FD36D1F0D1218EFBDE2DD8234BE1B3B517DD613 + FE598C32EDBA23F8C46705FCBBD7CB48F042067D0AEA710C789F93AC2548FA02 + CCA7E38214760E34C57B9965D6A11FC8533F2CDB9FBE1C016C6394F89E074800 + 03B02FE46A477859AA471DF40324B0D5875CDB29C65BDAB6277FD997745C05F9 + A08E5900EC6A4262D793B39E944DF3753694A536A9BD97793980721F08D9291D + 8FAADC13C025511BCA006C881C599CA2BB4EE1C6BF00C95B02B20A711B65DE9E + E42EF584B7F736264FE618615E51E63D52793AB72AE6437E5E444419E641E52A + DF2AD2F9524EEFFD58AA28E787DDD1399FACEA9036B51DC293C9AC0BF47599E3 + 727EBB17FCDC9CE46F5BAF2D09E0DCFC3D6DC3DBA4B639501F6C1DD19B238051 + 6E078004068AEFFAD62189A3982CD6B3FAD126179FF175BDCC405BDAC714B688 + 57752380B93A98B1AD9C3D176FD4C5CD7A01E36E251E2C7E09A4F1D0FDA29B3F + CAB8F2D8EB7A45CE6F15D0C1A6F37C113C578F1D705C2081333A1E770AEC0859 + F96EB38FBB33469F4275B4DD33D6CF3A7D631D3E002378FB8140FEF65BBC3AC8 + 01B3D136B06BA591BB5AD7E49A820C06318CBE9B4FD50FD8B3407F279AF6E379 + 3030294DD5B710CBD6769A5A6E5B2C37FE86707EB023278EDFA03E3405316D5B + 4F93A33A08729C559D2C9567EC40E0125C7D6CDFA38DA0CEEC23395CAC183E08 + 7AED2BE5A9AC03B0D17E45A293BC61417C9A2EE7076D3ABB0C3C0F59D155F8C6 + 3DE0EA81942EB84FF88D04B091C06C47F3BEDD8EB60F8AD4E9418095BD7EEB67 + 002B80071A3888A6812431996DD8A3CC03AF2380EB4002D84860D651644F36C1 + 1B4A81A5DFC5F2EF1C3A6E8CC708F161F558213E6C8DFCC3C32ED1D7C97B41AE + 2E653DF944DF32F26C7DF7227B3BA0F282ADD92A85F0750AD23021183B48467B + E995286C548E018A0D14B5DCE89FD2DFFD4CB9DDB3DADA60146DA27DB50BE550 + 1FAB886D25315EC6AECD7400F4B0C08156B741579D9CE00039A77B10F8F30BE4 + 08E0D42685F7D731290028CFE9B55CD4A54C270146EE524E505F01262FB35D91 + 5DF97B009413B40CBA10C684D7799BDCE494134A8F3A9DAFD70BD2FA444E9F4E + CAFD64DBEB1AE3470DFB258073810B062FBC5D5831ECCB004960ED83052210BC + 801FEA4A842D9E9956C17ABD12C08D48F6E6C0E3407F1A537346DEB667688F95 + B86BD23F15024BCBA7EFC8B5675F93ADEBCF1BE165DF339B5991D6EC4A205AB5 + EE20C85F23773BD18D00F60128CA084FF2361C9A91F0F576BEEC65F46D2B8BB1 + 2A3792B5B5985DB5C065111875F041CDBD08606C558800A60F6862CBC362DB43 + 45116C256ADA6540974010D913C00C4AFB803348DFE6113D278655F51188DC96 + 6D058DE0E9B6FA6600196DE05BC1081E03DA2E53D73E52DFC7B45FB421EA747B + 11C00409602F43109E01FCC20EC7BDB05B101E241D200301DCD0B66153256943 + E01FE42CCA61D56E20736D4B693BB7A11D9203BE1F04FBE3C9040272F31F0960 + 6F0799EFAB4F011E4B7A4C20A4085B55A93A4FAA21C5EA57104624FB403661BB + DCD6A2FA590A2420F4DC4297768170423DD40FFE3C1168BE715E15F8CEECCAB5 + A765E7899764E1D23DB50179051231F84AE1095A23B3D096B577D6FA40E20BB6 + 9EB0DC0BF48F3ABE0D03DA5D09DB218FACAB8D626CF3A24C6C5F96E993D7E5D0 + E9DB8A3B9ABF2553BB376572E7868C6F5D9551AC28463D4DA7D4F6F8C53B72EE + B977CB739FF5B9F2EA177EB9BCEF539F928F7FE9C7E42BBFE5D3F2657FF28FC8 + 17FE853F2B9FFE6B3F29DFF18BFF587EF857FEB5FCDCBFFB15F95F7EF53FC83F + 975F937FFA7FFD47F9EFFFBFFF42BEE667FFAE7CF8C7FFA67CF4AFFEBC7CF8A7 + FE9EBCFA677F429EF9BD7F444E7DFC4BE4D853EF96D9ABCFC9CCC56764F4D41D + 19D9423F6EC9E4F61D195BBFA5B829C36B378CFC1D5EBF2EA33B37D5E69A0CAE + 5F9226B6B2C68A5DBD1EE38AB0AA578F155B5FF33C28EC9BB927404E4277D688 + 4A5C5BBF5A14843789D642A7F0642E53DE1300F2B66A18759C1DF3BC77CC36C9 + 9B2F4DD19611A82847A0BEB50179EC176CB18AB8BDA2F2D570FD6D656DBCA7F2 + 08BF29FFBB627B768CB1CCDF17ECD947DCEBFCAD0190B37F84F5117E3BE481B8 + F5F72A6594E3BEC7EF93BF01FE0EA8E7B5E4F523E95BA680B6AFFD2281CA6324 + 705C39F09C7443AE5E37B0CDD407FB9692BF29826D49FC92FC4D0960CBEBBBC4 + 8856BC5B22FCBBB04449E0F239EC9FC5C87B9B6EE03B97642FFCB35D968B7EC5 + F6D37E157D55700B68BE1FF95EF52430E0E5A98E801EEF677BC76AD9BFA77344 + 32F3850C7FF67263148E7708D8A4320FFA41DE93C0D471856F6827A4D9719003 + C7551C63D9382AFAF02B8073642D741EDE86A46DAE6E5D9EB6445A861DB6884E + 09E094DC4519F004701DB1EBE17D74CA3575636900E360A49EC8050AE2369669 + 4F5D1FE6264E4F9B5C6ABEE25C22CC27CA3986474E97ABC3B9520E9ED4E58ADE + 9408A68DB7671DEA3BFC2AFCDCB19C2B96E4AD5FE54B590AEA3C019CD31B99DD + E31CB7639EEBE47E6E0ED21724B0FD195AED8AB97B9CEBB38EF753D824A0ED5E + E8207B53021888F18F82008EF9322E92C0C751EA6229A9CEDBD721575F81F80D + 6356B9B8156429F99BCA60C798D95EF1B16E487D84B437D4C53419CF3C28BAF9 + A32C4700DB2297B8D0C5CB53E4FC56519E0FC29FF780BD7CBC35E8D627F63907 + 4F007B5462F0408CD9A776595B2272101D707A92BA247C09C4A0E91F44B511B6 + 2065A31E842D896196C31F23D46648EFE7F89C18189C0E2B88B1C0496D5AFA5B + C20AE1CA3980DEE2B6AAD77C0BBEB1CA18BC537FC24379E4B820A04E5F27F372 + 96135B12BC44A3ADE706042FB64DD61424B097B18EF7B12F24ED17B23A792AEB + 006CC8FB95BC61417C9A2EE7A75A2F07CF4376E859772F447BFA218F0998DF3E + 92C0FCE46DE04FC98D22ADB47B50B0030F02760EC42FB77F06A1DB6C8DEA0F06 + 9D1DD21B3DEC610D3B92BC39504FDFBDC0EC15D9135DC05D6CBD817E93007EC8 + 700F5A7B480389ACEBC3BB0B727529EBC9A77F0944E4EA5B3EF3427BDC5179B9 + F440005B1D7DE918711809C60E4452D10623CE2E1DB4D86013FF781A38A42FBB + A3D21AD681BAA6B6DDEF30302BFD43013660C700077E41FE46C0AFF525B6F928 + 911B74F58ACEC1D9C38327731F19018CC90990DA68BEA81B6536A9613D0FD629 + 80090FAE6B77F4C7C9576E52F6D0F036228081D487F75391251373943D4A1DF2 + 8A1A02185B400F601BE884C465B082F0726F974795E02D0320047C04D01FDBA1 + 8D0F92A4E42FE10960947B218011104110050430C865B4DD9C3C214387378CC4 + EC9F50F9E4B2F44F856F9F21E8D53FB164DB44B78FAC496B16C42BC8E313D29C + 52DF53F35626CAD5C04881103C22518B7C80DA63A52E318340197C2FCB80DAF5 + AB7FA44403AB7B63DA3472773996035006F05D33C0F43188D8151902D8073201 + 2380B1720548EBC63C8296086092ECAD062E4B5911EC8C0153DFAE4725D0AB60 + 209A4803D26D7C03F8D8AAD605F98BA070080C87D5BED0878071207FB51F6ADF + 545BA005F218DF0E3EAE758FAFCBD0F1728B49F61179C818586699798232AFEB + 95006E1ED5F6405227F2108877A42C8E399207241D484240CE6D4741BA8E9D38 + 1DC8574FC4CE69DDE33B7ABCF0ADE751E5F6DD49B45181B6A76DC2AE89630129 + 80F30F1201FE4080A00FB18C3AF00F54880790CF903982C4F79B203945807C4A + C928924F5512AA4A3491542A08A595F332B17159E6CEDF37A00C3218BAF175AC + 803D6BED00F08976D09E6F677CFD82CC5DB82B1BB79ED7F48ECAD037F429B449 + B0CD6E04B0B7859E76A89702F23AB00DC0B671564CAC5DB26FE20EAF81243D2F + E35B976472E78A4C9FBA26874EDF321278FA24C8DF92001ED2FA834BDA1F90A9 + 1B8AF57332BB7B5D56AF3D259B779E95D5DB4FCBDAD3CFCBEEFB3E2A17BEE29B + E589DFF703F2DE3FF563F2F93FF13FC8B7FDC37F297FFA7FFB3FE5AFFFEAAFC9 + 4F8BC89FF9B7FF56BEFAEFFE0379E74FFE75B9F397FEAABCFCB7FE81BCFFEFFF + B2BCE3277E56AE7EE60764E38BBE56565FFDB8AC3FF39A9CB8FB6E397CF95999 + 3E7D4F26B66F697FAFCBF0EA35195DBF21135BB76464F386CAAEC9D8F6752380 + 87F41A82006E2DEB395EBB28139B57640CC76BE70FE74A8F59CF1961ABA123F9 + 3B06C256D30A8CC40D3AD36B9EC46E4AF0962467B8E7A8F73AEA0993D98A5EDC + E7205F4148C35F4809ACBE1ED3FB6B58FDB5D516D76250EB2205116CDB77AFA9 + DD66D8FA7B588F13F70E81FB84F755B8F703A13B823E44A40430C0DF167F57EC + 7FFA7B63EAC1363CF84D6CDED700FAE6EF77E673E0B1D86A6EBD76FC8EB087AD + 06B66789F60F7DC4EF14EDBBBC7FAE508794C74F1B0FD6EF15ACE7DBF1BE3CC9 + CB67752AA31C5BCB139E004ED19ED36730DF2B1129D11AF2782F06F8E7B097F7 + 0ABC67FDBB9640997D20010CD4C92C55F0BD98BEEFBD8E79BC6B4918DBA720D0 + AE03ECF07E2EEA6A9AF395B645707C528C5354C6314F77ACD99828605DC7463A + 3EC218C8C6353A5ED214728E873ADAF1E3A0088EA172E328DA632CEA096092B2 + 04742089094F027B5BD6F5A08D076DBD6FB61FBE4DBC52D9021AC46C4AFE0220 + 7CDB5A87C42F805D77004FECA6F03E3AE59A1663E9003FBEC6B89B48C95D8F3A + 5DEA83E58312C075753C019C23703DE19B12C0B4AD90BB49FD546F360A3F7704 + 40F8A6A89F530650E6EB781DF5BD10C040769E1B6575E05CDDE6EDC95C1F72F8 + E820899D7E7F80EF43013504B0277D7B2680EBE4D4E5E22C94D5A1C6B61702D8 + CBBD6DAA7B3498EA097B2D6C017C6CB35774F3D14DC7ED8F1F3C5EDC198BEC3C + EF5DFAF136848FBB77431AA74FF3070278803A2E00D701BED9D778AED926496B + 7E937870ECA84CCDAFCBCC1AC62ACBFADCC33346AF9BA2A1CF065B59AF3E6CD5 + 73BC76BC7E167B575F0DBD8F5A80FAC33785F7E4A4802C1FB40F79EA87E5C416 + 646E0AC8073405F96BF998F25BC0AC7B2024ED17B23A792AEB006C22F7375072 + 8505F1E938C16ADF5DBD583785E71E3BF4BE6E37247E80CA6256CD7B0218652E + 92355BF09D69DB07011B3F08AC13B1732C93FC851C657618AB8301CAF6027D76 + 05EC22AA271817D15FC870A10DBCA9FCCD55977F5CE11E5E8F0D320F560F3E48 + 73BABD90AB4B594F3E93BEF9BAAC5FE4F902789BC20F168A174E4C2B769158CC + C10F4C6C40D2D4016594D9C012727DC1D9774BECDF4DFAB26B1E91C1E179698D + 2E4863644E5A3AE1193AB42C43B34BD2C6640693051BE41F0EFF30847FB66769 + D2BF6200D41BD28194871F50F963F3A0AC7300E68EBD0BBC7D3778DF693B5E4E + DD7EFD03C524407D78F8894A05BE6E94996DD4D1A6B0AF00FE74B204E87DE049 + 5F0F5B011C276CF6E780B603CA0F6985708ADCE42EA7CF4D40214BD18B2E0574 + 9C98A6F2145E67E9B0A6C9849C136E3FF12EE55ABFCB3780AB0430C9DB2A091C + 743902B8AA4F019D0F7810F49B22671BB698469D452D9FA8802470770218366C + E3B834A6166CFBE7268245538BD24480078125C520025C086C21D83483D50A6B + 2A5F9696CA070FAF49DB88D625956BDD48F8A60430BFC35B02046F084E85547D + 4F69FFADCD10E00AAB2456D5275660807C46FB25429972AD836FD019591C8032 + 003D10CA08DE0530C8D8811E096082014BF38732E4D10FBF6B57042E91776523 + 7D89E89B6D2250EB83B50CE002B9E0B30F4A630BE841F3012020CCD5BDEA0B3E + A3EF006DF3B8B6AD69E3A8F61D24F0113DF7C722013CB7A67ED51F82EDB14EA5 + 2FE883A6EC33F2A5EFAA6DA18B242EE089DD14B915C084AF6FC4AC2310404078 + 72C3743847AA2321CB955F9E08680138874642E4090AE841289354868CF69EF0 + 60FD400CBBF64C56D62158B720505C6A04138063815FD4413B11B4016105820E + 2B2541369148F2E41352104B87766FC8FDD73F57EEBFF6B97268E7BAF41FDB36 + C2740A04A8EA5B587D17DBB1F6635B2400A1437E620304DC1969E07CE0181352 + 0B65B44962D6EB9127D837ACBA453F0289D949EC42EF015BA4B0838FC20FC8DF + 8DCB46008348B4AD9FD700ED0F56036F9C9351EDFBD8A6B6B5714965970DC30A + 7C13B80D62126DAE5FB46FD5A2BFE3EBE765725D8F41EB0C5D7B4E665EFDA42C + 7FE1B7CA956FFA53F2CCADBB1A840000FFF44944415467FE92BCF447FF8A7CDE + 8FFE23F99E7FF6ABF2C3FF5EE4477E5DE4F7FFEB7F231FFEB9FF419EF93BFF40 + 6EFC8D9F97677EEE9FCAFB7FF9DFC94B3FF73FCBA9CFFC3772FCF3BF4616DEFD + 31597DEA5559B8F592CC5C7846A64EDD93C99DDB32BA7DDDFA33BC7659C6B7AE + CB3856FE6E5C91B6F60FE4EFE4A95B3279F266E8F332C8509C0B3D0ED5E3DA74 + 10BC0AC84BB235FC460A7217DF76568CE21CD979D23A20CEA9D7BA46E26640F2 + 97F78BD7F97B3390832A3FA1FE9614C956D5E83BAE33CE3DBED5DC3CA1E77E55 + EF65454BEF135C2BAC786EAD6AFF70DDB40E566BB33EAF3F52DC572480D1071E + AF077F6FFEB746A4F73E8F81E5D406BF892A895CBDB7D92FF489AB7E715F4146 + B00E7F1F4CED3BBF8BDABE23825136E83902B15E1C07FAA2FDE0B30C325E7FFF + 9CE139A08C489F4DF499D311BE7E9D3D9FA1B44FE5D4A504701D5A0B81F8E43B + D0BF133DF89C4FC1E73ED04DE74102187E41E4226599DFF725C1CB721DAC7E7C + 27EEF5CE273CE1EB616DC67AFEBD8B72D12FE73B6DAB0318A7C4BC1FFBE460A4 + EFA1808611A2491A81714E311672E31F968D108E798CA1B8F217B2211D0700D4 + 0560AC5585277B016C133D883FBB61E71395C386E42E6D58D793BC0475B06319 + 79F8F23EA133BFB0D77C3312B520795312980430F224711F1601DCF9C7CA4E78 + 22D7085C0792BBA95D1D4800E7E71925529B72FE51959300F6C42D895E007F32 + F5A4AFCF03AC5790BBB15C91EB1CB0D8523A3B3FEC9C37D6E953F46ADB37523F + BF3D08E0CBCFD7FDFCDD033ACEB3D3FAFBC7AC21257D2BC8C643BA636008B110 + 571771951C5C9D83DAF54A00A73A2F4F41BDB7EF05F53EA6F60409E06EC8D54B + E1E38B21C658AFEFA633B8ED8FB3FA9ED04B6CAFA6FDB7291867DF0B69AC3ECD + 1764AE47ACD3619B45B58D8E7E0238EF2D2DEBB5367FB856D06B9DE32BA7E4FD + 9FFA52F9C4D77E83EC3EF1ACBE53F4F93BA8F6437ACDF4D9D018C1EF5C7F874D + AD4B5FD137DB1EE81F9366DFA834FAC62C1FC85FF03BCC27C872417BE872B254 + 9EB1F3A428085ED8B487676472665E4627F5F94FBDCAB11218A0FD8190E983C9 + EAE4A9AC03B01951DB407492332C88CF94174CEBA5603DE7ABE2CF23573F85DA + 153E2287DA8D0086CC13C0A64FDB3D08E8EC4111885B3D793C98782000B786C6 + 01F855C0A99D07FDD60127AB4FED3A4F2E2E206E4E00F9DC45DE033DDD606F21 + E283EBB142F2004EB1F703B91EB9BA94F5E4D3BD207C3D20DD8EC2BF04DE8EE0 + 8061AF4183277C53F88109076DC5605307AF8DE1C3D21C3D62181839AC83DA23 + D21A9A93A1D125698DEB246D6C518667D7646AF1A44C2EEEC8C8E195B085AA4E + 526C503EA875B04D34DB8CD7A0D23F771C01E9C0A8C45E03293FF0ACB3A52CFD + FE0975FB85AFEFC1BEF83ED5D920BF973F22F5554C0454BE277CBD28EB20806B + 81C94C95EC4D5110BCFCD7AE076C72722033C103FCA42EB5A11DD31C52DB5E90 + 9B4C5296D3A5367E62EA7590E77485CDB0CAA1EF32F92E756A3F06D9F12CF95B + 80DFF42D8032020C7312BEF35B12BACC07F217E42A021C01E996D069C003F0C1 + 903A78FB1C010CB2D693C021DFE9BF6132D8467F937346D4B667C3B6C9EDD915 + 1DA82FE931A2CD13460083DC341256DB684EA9CDA110B41B985C347FAD232AC3 + B7660BC257FDC655BF9DE42F8254440856D94ADF2904C9CAA016B7C803016C2B + 4140F03A923704F1FCF67C2160483048083D50060DF70056F53A42D6C3074453 + 0298E5812843BEB03F1AEA37B052C6F933441DC13A2918FCB5C02E82BE31E0ED + 83D0248111EC2DBE9367DB38C3AFF6CB1003C2585DAB764D90BFC755863CCB73 + 7A0E14CD393D86B97569CF6D683B553217ED5A1039A6E82352D3411EE1EB143A + 47E0029ED84DE1EDBAD97B529604C3B02321483020B81F56EAAA0EB2E813DB3F + DBAA5D23203A898914247F01DAE7488F42674470A7DC74B15E5177A9249DEA40 + 92CAC348BF556CE31B5746464209A927C5402A4D6E5E914B2F7C50AEBDF41139 + 7AEAB61E077C94E4137D2135E26E3990BE681B65B447C20BC0B7876D25E74A68 + 9740BBEC0FF26C83C8D9A604B007E41E2480511FFE40AA211D814EE5D8FE19BA + C1D5339100D673BB1A81E3C036C220CCD7B53D90C01B570CC3F11BC1935B578D + F885CDE4CE2599D9BE2213276FCAA1FBEF9613AF7FB12C7FD6D7C9F6177EA75C + FCF4F7C8CDAFFF7EF9F0F7FF55F9AEFFF1DFC81FFC87FF4ABEFB97FE77F9BDFF + FC5FCA17FCFC2FC847FFC13F95D7FECE2FCAAB7FFB9FC82B7FF9EFC9137FEA47 + E5DCD7FF0159FDD857C8C28B1F96F93BEF9623575F9089734FAAFFBB3275F29E + 4CEEDE96B1CD6B46F08E6D5D93F11D10C297A4B984ADACAFC9FC9567E5C4F517 + 64E6D45D1959D63E9F00F97B498F17E70AE740CF2B8ECFF201F68D6090E120FE + 62DE560BDB16D95AD6F36458D37AEB5A074430C870D8D680F707C07BD1EBEDDE + D4740C76B8E68BEA13E4AF623412C0BCFEBCEE83DA2790BF4DD4DFD06BB87E51 + 5A7A5F82046EE97135D417F2A857B9272219CCFB21A0EC13494F23C3631E8425 + 40829C7D2E7E9BB13E74F84D787F94238FDF037F2328E33AB00FE80FFB84DF17 + 0960EA78ECDE86301F48B1ED330860D45118190CE8F519D5EBC863429FF98C1A + D4D48E4DE53C563E6B78DC5E96439DBEEE19E6F5FEF9E891B3A32D56330F4778 + 323847080F45F0DD9703777B489192BE75F0EF1EFAB4F71EDE69F1DD4B029832 + EA534047982FE4A1D3F722DEDB614CA0EFE7F8EE2C085EBC371DBC0E79D631A2 + 17FE225A5147F8B104EBD2A71F6FF8318987B7296C0F6BDDF869890E781D6C9D + AF620C85B2EA6CBC029FD0610C85B1958E33DB18CBEC410093CC25014C0C1DD3 + EB83EFF5E3F3175A6E66EAB04CD297F036B4833C2703FA91A20EEAEA38196358 + 8C95811C098CB44AE276969B314D41BB608B3F4BAA3C1967A70061EBF324707D + 9AB3A12C57B67A9C4B74413A87A12C9583A44DC95D92BF69DEC3085E9DA75588 + DE3A808075C8CD133BE78D55790EFBB1B334CE671F36D2F93190CEE5F7AAE3E7 + FFBE5E150F40002366C2B849CCF3CFF724800B1298B6B071F98ABFC4AE822E76 + 072580539D472E3EE68158156DBD9C328FA0436CCBA35396237C53A475EAD02D + C6D8ABCE632F7D77EC1DDB0B36B9BA8F2FFC794B81787BB19A368DD3035D885C + 9FEF06DAD5DB62456B482D0F321FB6FE384C1EF32080B5EFD8CAB989DFBD96CF + 5CBA277FF22FFE65F9733FFBF7E4F98F7E42DA2080F16D5CBD178D3406113CAC + BF73AC0A8EF7A75D4BB4C576FB41FE0602182B806DFBE782E3D17C0ACF013902 + B64347E4E4DDEC3D6813ED41F082FC9D3FB125B347F51D0DB23BDAB6F4589B28 + F7E2B70EB9BAB1EDAC3C95750036236A1B884EE30CC12B7AF233CB0DC67A295C + 3DCF437A793DEAFD3025C06B02A603F19BE15369EB7D1D1874F6202043ED3B07 + C2176510BEDC163AB5A9436AE34F4A01D8F80B64081714FF4E78A025E90F7223 + BF19C83ED4DE62742180F77C201F803CDED3A7474200F325E45F46852FC0BF08 + DE66B0976C3220CADA39C23705072600076A36A8C46A5F1D1437468F486BE2B8 + A109D269E49834474FC8F0E4AAB474020792665027B1132776651CC18D599D3C + 620234A6131A0CCA410836D517DB50DFB64D463BC2063F4803C2F174C2F71380 + AC1C5896F0034E6F4B1DE5A9EDA300FD776BC7EBD2FEE690F3E52702D9090464 + 84968B7A515699A474056C74A2D40505F99B4CBCFA86639A12BF446AAFC84FD6 + EA49DDBA49602AEBE69B487DB1ECE1EDF7B2419E043049605F9736BD12C0E11B + C0C8EBEF2CD9FE19681874A0AAFE2C40A0753A0308810C0E607E5E8100C6A2FE + 8E971420620319EB4960063D3CEAE47520D14BD2B789A050CC5BBB6320A1D18F + 32C8624090C5485BD48DFA4995C7EFF382B0B5D5032063D55F6B7AC56001A7F8 + CCC22A0596CDD794B68F15C0B3278C440E2430824455F217AB8B0BC2D7560723 + 5D0ED03602E03304BFAC6C01B548F46235AF237E73646F3719CB2078FDF77B3D + A86760B212A08C6010B3D0C74025B684263C895C04495D1DB3717A045FA9F775 + 7C60162983B508F286C0F66E25B08CF20802ECC81F53594102C387FA3EB6AAD0 + 76403A6B3A706C431AC7B5ADF92D4D376540F3035811BCA0F6F39B2A838DF619 + 325B4D5CF6216CCFACC785BE6AD9079D011F8C461DAB17E5D8FED907D5537862 + D797BD1E2983F358B9EB57E43225A1EB490604FE47E26A39C89A713B5190C1DC + 221672DA13242BBC8C762968CB7653B997A13D9236A673F032924ED40582A90A + 9053D89A796C8DE469488D408A280825457B41FD2E9E91F1D5F0CD5BAE48441D + 10ABC107C829D887D59D00CAC57762413A29B85D30DB24E1853C085AC0CB53B0 + 4FB0210998B3F3804D6A9FFAC171991C44A66DE77CB684CA4800636529085F90 + AD20804737915E549DD659D573B87E5E26762ECBD4EE55993C7D4766EFBC5716 + 5FFC62997BE7A765E1D5DF2E9B1FFC46B9FEC57F503EF25D3F26DFFCDFFFA27C + C99FFE6BF23BFECACFCBB7FFA35F92CFFDC99F960FFFF8DF920FFCF8CFC8FBFE + DCDF96673EF3DFCAA52FFF9D72E113BF4D765EFD7C597CE60372ECF6AB72E8F2 + 4B327EEE299938754F664EDFB56F13634BEA11EDD7D8F615C3A0F6B5A5C736BE + 7D43E6AF3E2B8BD75E90999377F578D56EE98AA6E1BA19818BBE6BBF71ECC80F + 29B0127A508FD908714B03601308F280416055EF3910C446EC97A85EF7320FF0 + 5EF43A96B1ADB491C9275417C95F238023095C5C4B9C6FACFAD57BAFA5F5B0E2 + B7AD7D6F9CD0DFABFE068E5CB82B0BD79ED1EB715506D51FEC40F817F0F781DD + 2BE56F04BF35AE8625204BFB6FBF39FCCEF0FB4C74007F8F2480F9DB840EC7CD + 55F8E803EF577F7F92FC258AFB55013B4FFEDAF58C329689E017B2700CD60F7D + AE908C26216DCF92D85F1C13C0E7CBA3009E8F780E17CFE244EFDBF7CF436C25 + 6F04F032CE65C813C34BB0C1B3173EF55D807993CE9F909200B6AD911340E789 + 5F22257A7B43F08977ACBDDB40A23A5097E6F9EEF375D837FCF9089F65B07102 + DEA70AAFC731B13E411F80E9F47D4CFF06E4A32DDFD3F4CDF778F15EA66DD413 + 188B6477277136247741D2DA672514B6E216842BC608247F0F9775BC2F963D38 + 0E02A02FFA76446D1D310C32B8E5C85882E42F56E882FC1DC1671D8EE979D232 + 562293B0F52091CB3F0C52469FD47BC2D70372DB061A798C5F156DCDE30F94DC + EE19B292B40D635CA444390E0E7F7E641E9F5BB1F137D3E8A3F415C6CE1C6B13 + 9EB025727A8CC5BDCEE6004E5F07DA602EC1F9441DC2BCA55E461F20779B535C + F13BA73A00A470207503411C40B2B75FE773615E56335FABA0736E07E2D86FDB + 8C6FF7D66DDF9C83AF0BA4F6BE5C49E35C3745A34719E7F2A93C9D1F13DD7429 + E89BC8D99000CE91C0858C3110C654183FC9E5158CA954622BD031BE93D64D41 + 5D8F36F0977ECFD783F2BD741EDDFC017BC5AA806A7DD87BE4EDFB2A9F390B76 + 8C8D0584D8673774C440F7D0F78207A99B9EABEAF110499DC7283EDDB1B2D588 + 5485E54BBBA2AC28E2EC698CBE063E365F5B8FF17D17E3EF6ADF0047E4F994C8 + A9147D0F79DBAAD98E2F90BF0DBD0F1B8845ABDF53E76FCBEFFB133F24DFF367 + FE82DC7AD7FBA439AECF04F8448C1C64687B52065007CF10BBB6E80FFAA67D34 + 9E6A54F381F86D68BF074008A31FEC531718BFC57237AEAA8E3CDD8BDFA20DED + 3405013C3679CCC8DFE9597DAFEA31369AF876F278C8BF1D560047CED048602D + 1B310BE4EA9220F5FCA2F71551D8793879373BCA2D05D10B1E55F39EEBACE413 + 5E34F5772078870F0BE82808604F028300E616D0B93A39F064F89300D4AFFC4D + 2F643D1E88207EAB913ED0DE4CC407AD7D4CDD3D703BEC1279EDC338FAE806D4 + 4DEB5356EBD7C1FAE9FA0AD413C05A872F828782CC4B7CDFE8DD0707247BC28E + BB1E1C80D8E084E706790C00B10278ECA8A1318201F431CD9F90214CD62696A4 + 6F44270B532764747E4B467512DFC2CA364C88F06D526C690B728FFE1520806D + BB0CA220B091D6C3F7B1E8ABD331CF01A7B7F7C8C972A80E5CF707D64D5167CB + 8176C771C53AACEF07E4D892DB4F00EC7A2528746AEF270DA9BE324949413B2B + C346AF6717D46DF1CCC9562AEF15D5095B1E7593C7B47C10FFDE37FDF97CCE26 + D5A504B0D7F7239F4CAE099453997DFF775C81DF19C8DEB1794341008F04D80A + 60AD5BFC435CEB56820891D00D0105A4FEDBBB4B96960021EB88D804DDC8E11C + E817ED00CDA910580239DB07C2793410C0A57D08B6F44D6819FFD837E2765153 + CD5B3047CFC1F482B40E85800E495EF80DA93EB362DE079E060F637BBE55571F + A46E0C2E219834A37648639EDFE5C54A63A039C34057089A111600B3805A959C + F564AD07C9DD6E329653BB147B11C01ED015A46F52C793BCB4F575586680D3DB + 3030EAC1A02DF208E432408C94815F12C016383EBE2343C77735DDD5BA5A0F24 + EE71ACE8D5F37D5CDB9AD37E1DD776B1B533FC2CEECAF81ABEFD79D682ED08DC + 37E7400AAF49BFD6C5EAE032608C5545BD13C05676BAA1E3DAF739F4BD1E9EF0 + E556D05E8F603C82F883F37115AA1D43201148D0621B63920D94B34C991D67AC + EBE5F401A0EC512727486C00EC57DA2E6524A48AD579114626457B236D483A45 + BF2C939002F903D2696203646B209E8AED7433E411CA037A8F3414D0FB55BA24 + 96603F642B0C41E405A229F44DCF95B609A2AB24F9020AFF66AB32C5A8968D88 + 555FEC0364B4F5849DB565ED0539CA1E90230DBE63AA65F49DC780B6B0F2773C + 7E0F7758EB18D1EBC85F90C186B8BA7778ED82918E6D90921B576462FBAA91C2 + 908D6045B2CA26B72FCBF4AEA6676ECBF8951765F4E6FB65EADEC7E5D83BBE48 + 365EFB1D72F1737EB7BCF8F57F4A3EEBBBFFBCBCF29DDF279FFEB1BF25DFFC0B + BF28AFFEE09F9127FEF00FCAADDFFFFD72F5DBFF985CF8EDBF5FCE7CCED7C9D9 + F77FA9ECBCF439B2F8C46B72F4C6CB72E8D28B3276F629193B7557A64FDD91A9 + DD1B32B279518637F4BA6C5ED2F4A211D18358ADACFD9B3A7D5B26776F69FFAE + EBB9BAA6C7AD58BD2CB6E5F5AADE13766C7ADE22700E8CF45DD17B01E743656D + CDB7964F493BAE8A1ED273635853F98ADE6B2B58191BAE33BF0D8CFB2B7C2338 + E4791FE04F00E5FD18EE0F2B839C8C3623B8FE712B6390BFE3B8568AE25ED0F3 + 8D6B41602BEE01BDE7DAEABB053F9B17E4EC8BAFCBFD8F7C4A56EFBC280DD535 + B09ADFD78BF788DD2F96C6DF0E10FB47F2D74869FCFEEC1E2EED409C22F53A23 + 5255C63CE1CBE69BC78AFB2EDED7C5F145197E7FEC23EF61EA29A39CBF4BDAD8 + 3D8EFB3ADA85DF76E8B7EF17E1C9601CBFFFC3893D4B704CA8EF64A92EB5499F + 731E7CBEF9E724E4BE3EFB405B23730190BC0A2382F5BEC49F724696D136EAA3 + 6EB0C5733510C3C1C788B663642A009DBE172BDFCA25818B770708C1582EDE27 + 9019344F5B22DAC30EEF2FFFEEE53B0D796B5FE5DE867912B1FE9D88CF2A843A + EADBFEE4A43AE815A1CFB1FFD14F592FE8087C0BD9EAE33D89773ADA51D007DE + BDF63E8E728E27BA028432495645315651D89FCAD255BFF0ABA9278041FE3640 + DA1E0671EB7CC43F931190715CE4C753B02F511D5BD197ADEA8D04AD41F3247F + D10F8C83EC1BFFE893CAB14217DFE905516B2B762348EC92FCA5ACE23B226757 + E820C73856C792207E87B44DDB450776D0E1CFA171FC5C8C7575FC8B71704914 + 9724B07D0205980E047081A9AA9F002DDB78BE0A4FE6DA583C0109DFB4EC7DB0 + 2EF37EC5B0CD27226C4E11095DA47D9877C47988FD013503C441301FE1E767C2 + 2768A0C3DC057531378A732D05CBD4515EA24AF002E5CA60DA9772ECBA06D297 + C46F1D01DCD0B628A70C5B3993F8A58F72AED91DF4C1392DE7BAF83333E7BAC5 + BC5781B92EED30F7AD9B83FBF939F5F4C5BCB727D27A1E397B4F007B12D8E78B + 78C860C857E22711659C44F5ED4E02B8B0433ECA0CACEB415D37387BF8658C88 + ED1045CCA807A4F57C3905636C1E39BBFD22E7D7A323BE59031FDBCDEA5D2C35 + 875C1D2067B7972CD4DDEB18A33DE3D288693FD4F8F4C1C0E36E681F2BF1F318 + 632FFAE9FBEDE0EBEC17E6C3F3068CEFBB387F568FBC11B0194EC5E4D13E9E5F + 5B5D0E4257AF8DC559718D548FEFF71E59D8921BCFBE4BAE3DFF2E995D3B29FD + 43B87E7A5C835A67487F377A6E60677D1E5060C571413A8FAA0CDBF9821F0BE8 + 95FC2D4072344386EE7B01A4F795F82BFCA8BCD19AB06DA0B1E217F916166BC5 + 7EEFBBCD470E1CC788F6DB919D5A06090C3E913B15977C62790CE1CF01E41423 + BC1F87D287A2018436297F947818ED3C320298C08906B8FDF37E08E03AF44E00 + F30256E5B8C8F880F5E375C3EE037CB0BD15880FD38746002BEC21E99093D7D9 + A7E51CAC5EEC776ADF4100272FBA07877B891F180FE6832FEB0AECB83B61DF27 + C0B9F003129C173B376A83012007C011FD43D80E7A515A632774203FAF138739 + 9D1CAEC9A1B5B332832DF78EEA447342273E4600EBE01AA421FDC227FAA32FDB + 92008EB23830AA03FB57A7F3650C5C73B675F21C38207E10A4FED263C8D95A3E + 733CCDC159DB9284650EF4CD27D268EB6172B54D270CA9BE98A43899B72FE56A + 93217D0112BF8F8A0006AA93B6125E9F4E2CBD9D07EBA480CEFBD80B66A79367 + FB161226C2AC1BD30EB84930EB79D844DA4DAAEB015B9DB48FE9647D7C5EB160 + 00098CAD9FEDFBBFFADBEC1B89410440FD06A08C3410B625108808A42B89D992 + FC05490B590C90B8C007095AFAF1BAD4C6DBA56D0105F90B9D3E63C26A64AD07 + 1F930B05F9CBC08D0FF880BCC5AA5C90B2FC162F009F05193CBD6229C0A053EB + 10885AD4090430B77506D9DBAF7EFB14482BDFEE05410CA258EB21B885A01801 + 42B881950C9ADA4A8F4308B4A97F94F559C9EDFF7C20CC48DB84D865B94ED615 + 31F89806202DD8188132039169B09272CABCBD07ED539FD451E603A59E00F601 + 61069511ECB51543082A1F3F2DC373E76474E1AC0CCF9F52FDB66DE7DC5C5893 + F6C2863414CD796D736ED3BEDF3AB5795976EFBD6C185F3D2F030800AB9FC145 + 90846A837E22706B6D96816FB69F82E4706A833C09DC10502F89DEE03780322F + 67409EABB72C5578A28179C093AF240A90928808C443096F97FA832FFA4BED7D + BD14A883FAD0DB4AC30CF9021949609238247400904A80E51D68071D482A4F00 + 07423610464809964940E5C8A592544A89299052B18F96AA5E53D894503BBD7F + B02A9324B0917A5A7F50F3432A43192B3587412AC36F07BC5CDB37FB506774ED + A2C987B42DFA434A1D5781228FEF008FADAB4CCF0709E082F88DE42FBE030C80 + 381C447B6BE17BC0207CB12218A9ADBED5DFC7C4C62599DCBA2C63BB5764F8E2 + 7519BC765326EFBE20F32F7E48365EFB946C7DF8CBE4FCE7FF0EB9F9E96F904B + 5FFE3BE4B7FCC51F976FFB85FF515EFE9EEF956BDFF09D72F1ABBE4D763EFFEB + 65E7B3BE5ACE7DF8ABE4FCFBBE544EBDF37364E9C9D7E5C8F577CAE485E764F8 + CC7D19D9BD2B13276FCBE4C9EBDAF665233D81E10D3D07C86FE931693F46B7AF + A9ECB29E0FEDDFCA759958BBA9B86EE72A6C6FADE7C852BD5691F0C5317B12B8 + BD7C5A5A4B7AEFAEE8BDB6A6D7D29D232BEBF874349EFF513D4701C80719C8E6 + B0C534BEBB8C6B1AAE09803CAE1FFA01D8F5C41F0AE2376C41FC4EE875C2779A + 8DD0C47555FF23B8660AAE026EAA1FAC7C0640826F3DF5AADC7CFD9346006345 + 30B68A0E447DA84712B8BC6F4B8296BF2992BF4664A3FF6A435218BF2D90A5FE + 7797D607480E9320A62D75F83DE07784D5BED5DF51206E91F24F0B5E46D0D6FF + 56B1329F5BA4533788EF24C7FE75038E09DBCBFB6708C0E712774000D26754F1 + 7C822FF563CF47056DD3E71EFDE47C793BCA0B19F211761E63DFBC2FB4DDC07B + C4D5C7F6FAE1DBEAFAACC5F3D6DE0F01F846BCE954862DFF3D4A5DC8C386DBF5 + 87CF04689FF00C8FEF21BEC7F0AE05114B908C25D94B19DF7FE1BD57DA877278 + 67A3AFE11D57B56D2BC23B33D4E1F1D8FBDDDA2AFB00923A90C861CB678C1726 + F4BC4DE3CF227A7E480A732CC13CFFD415FED815740690BCF8266F04C72348B9 + C216842FC85D5BED0BF257DB1F9ED77BE3D8762147BE7544FB04C218630AB40B + DF11F6AD6105C63DD8E56500E33E239C437B36263259091B5FD92A60B50140B0 + 82A805F9ABFD208C8CC6CA5F4DED3BC487B48D99D58200B6EFF54690CC4D5700 + A728DA8A656FC7FA4602DBD858C75AE893D9AB4EC7CCFD3A17308218ED601CAD + E9B0F66F147FD8D33EA3DC8731B78E6DDB3846D473635DA20FB2291D4F639C8E + F13AC6E55AE6989E246D6EBCDF39FEAFA21702D8036D3438BF8965A67D3AEFF0 + 609F3C2A6D63472110C2C57C49FDB83950F8B48E2FFB7911CA25595CAE10EE44 + 31A7D37C208041E282E40D2B7A53B216800D095ECC2539F7A49E24300961CAEB + 90CE4DC33C37E6A3DEDB16F3DC3827C63C3837B7267A999BFB397C37A4F502AA + 0430895FA6961F44ECC3C54A18234940BDB72FF4451CA5B4EDD07950B717D4B6 + 3F82F1A41CF6B2A17E3F604CAB0EB93ABDA0BB1F94AB30022F414FB151456A47 + F4620374D84552B1D3A6FEF802923A11A9FC4D8591BA48B52F09185F0F360910 + 878F48EBD521675BF8614CDFC3EB988FB0FA46C4AAAE8353A9DA87D5CDFEFE52 + E07A0C80339896E6D0ACB4B0FBE5A83E0F86F5F7067BADD7684C454C1A20339F + 09F18C15BC204F3DBCBE2774216E7B46EA83A8B37365E3CF28CBD5D90BF45987 + 5C9D9E81FA0947E848DB1C01DC5937F601D706361EAA0389DFDFEF65416EA9CA + E9FF71C62321803D4800E774074179A23DD29B25FED8EC46F2F28092E9CF3C08 + DE0EE043E5AD407CD03E4A02783FE8B5BEB595BC14B276FE45F75090BED00F8A + 9CEF07801DAF0E4012840FD4C326B66BE7847D50E8A0D01007C186C1591918D2 + C9C1B04E6086E705DFD81CD689EECCCA2999C23FDBF1EDCD719DF48008C2E01A + A4210646F01DFDDB2A600CD01E900066D97C1E70005B8774707C10A43EFD31D4 + D99A0DAE97B3331D8E51D3A64E1AB8129875AC5E92AF4C20927CC54E7D151316 + CAEA603ED40EA46F2BA639F237B3A533275BA9FC4190FA43D94F10BD7C2F785B + FE5B39B5C98113584EA239B9655A877432ECF3B94975273061C7BFB6F5B73816 + 885F82DFFF3512D8820A0C14681B06E4F92FF212083C04B21640D0E244014FFE + 86804649EE120C64209FDA524718D9AC7E016B03C11F0481A20CE81B8F64F084 + FA74419A1465A0074198B82D33023153A54FFAF7C46F40B9DAB719EB5A1EA9C2 + 0860B40BFF5A2E08E0E9A00BDB397727712DD0A5ED600504086112C09403BE6E + 0EDE1F015F39983E217419F4646014F92208190392456034D17999B74D7D03C8 + E76CA0F38159B3451DE4231830B640F07C208147E7CF2B2EC9D8E205195B3A2B + A3FA8E692DA8CF85154DF51C9CD0F616D6A4FFF8BAF4AB8F433BD7E5D2F31F94 + CB2F7CC80899BE59F58B40F38993B64AB81957FD32B8ED81F67D7F3C18D0F620 + 794B7237257B910EC346E175A8E33134AF3EB48F0579A020518BBE139E28E805 + DE1F001F9E00CED50168CFB2273B502699E1ED69672487A2206A6003C203C407 + EA301F613E34851DF4207DB1FDF3E4E625C338C8304724710BE641B7CDF31848 + BBD50B816802891A6DFDCA5A944DA6F942B60C02F6ACF641CBA8071BF5150847 + 6D1364AAF6A7ADF94195E37BBA43563E6B32907C904107FBCAEA4D0589BF0E1D + C80C470C6275286D521F006C47F55C8C6D80D4D5DFC3E63919DB42AAFD5D473F + CF19F93BB6A9E7C66CF4BC6D5D53FD55CD83FCBD2A135B37647CFBBACA827C6C + EB8A8C9CBC28C397B4CEAD8B3271E7B6CC3DFFB2ACBCF26139F1DE3764ED439F + 90DD4F7C81AC7FFC73E493FFED9F96DFF977FFBEBCEB33DF23973EFD5FC9B92F + FC1AD9FAF057C8FAAB5F2C275FFE4239F7CE4FC9C9173E214B4FBC5F0EDF7859 + C62F3C5B12C0BB7714371420A02FC9E8961E8FF61F24F0B0E687B13278F38A96 + 15AB576574F99A8CAFDED4EB7E53C6B4EF38FE705E709C7A9DE236D0386E10BF + 0408E000BDC7D406C42FCE8F9DA30D94B55DBD77783E0B823582D76918F7575C + 314D99BF36762FC04EEF2DFB762DB681D6FB08F71FC84CBBC714612BEE4BE69B + D7172B9E41FCB6D50FBEFF7BE8DC1D99BBFAB411E483EE9E63DFD89E11CE76DF + 9604297F3F247B496C02248491E7EF2AFCE6F09B0DBF43FF5B2C56FAAAAC2083 + 59C7647AECFAFB28BE4B1DFBC3DF12BEEB8BDFA511BAD19608BF33B5D3F384EF + 0517BF59B5E76F197690873F6584E363DFEAE0CF018FDBCA4899879D7B6EA19E + D929F8DCB215C5FA5C2CEC218F3AD629DAA47DD459393E978BBA518F3C52FCF9 + 0875D927CB23855E75D8F29F3E7200793BAAE77D04E47894997F05778CE07BC2 + 749A5ADEF59DED426EB6F67E0AEF3ABEFB02815B92BFE53B2EC858A62D095CD6 + E7BB0DEF3DEC7281D423BC73513FBCFF46F4B88092340EE0EA65F8B477AF8E11 + C6B5EF33FABB19D5631A88E30A12BDD0236FAB84D10F27E7986310D0F691526E + 4856FF627B6510AD781F034D10AE208E8FAAECB89EDBE3BB5A56FF2AF7F5E087 + 24AE6DCD0C221504AB96A14FE504CAA90CF0BE7C99F6247D7DB98E08AE238089 + 940826016DD032C69C36AE8C634A8C61D3312BF3F8B6F108AE9D9E6B23BBDD18 + 157A3F4E0E9F420932FA034CAF636B12A97ECC8F3CE51E9C1B305FA7F7C8E9D8 + 460AB60FF4752180615BB4EBE63F849F2B714EE4E75E39147A9BAFE9BC2F6E13 + 9D2383410087CF02615EE8096010C281CC6D4C1C97D62410085ECC25D3B92AE4 + 9E002648E212BE4E0AEA7DCA7CC7DC187352C8DC9CDACFE353A472CEDD3D7AD1 + 95E82480894AEC8B319408C64D48E4D6958B3A492CA5A25799C564628CA88CB7 + 74017DC5BA1E3EB694C68A0E82BD7C14FD4E90B37D70C06F15390218721F074D + EB10DEC6A3171BA0C30E0468D6A67A1C9DE72BB5DFBBED470E4FE67A8297C8D5 + 51E4E2ED7BA1364E8FB6231751807D8AFD4AED2DED20809147DDF1608BBEDA71 + C036BD167AEE1B7A5F1565D8285A5A4FAF49636032A07FC2D0D436016CEF8CB6 + 1E3AEFE4C9D21C727552A4B6BECC7C2CFBBE17C7E06DA25D4FD88FED8100FF09 + 4F0802B87FC810BE050C1990F4BD207CA33C87C2B69415E7A7903D7A12D8EF92 + 7C103C7202F861C2F6D64E2F2A2F44015C80F0003018A1857C6AF736068EE7AD + 427CD81E8400EE66E751277F10F8F65254ECF8507F5880CFBDE05E30F580DD43 + 84B69B92BF005700FBB62B0313E45B71B08981EA20069D18081E91E6B04E6246 + 741233AC1385894569633B547C33132B1C4774C230AA930B0CAE411062C003DF + AE0D1BD8B4D106CBDDE1FBC7C168D14FC5C318E47A70E07C50F8ED9AE98FFDCF + F9A71CB0EF4F607B111DC462D52FBE4961FF52D3675C7BF8B0F9F6E7C2B62B71 + F5EDBAA9BC325170F9C22ECA8A89086404ED3B80094B207E73E42F5137E94AE5 + 0F13E5842E8F6E765EB71F02B832915570729B4E7231F9F59368E6A9F3795FAF + 1E98E463B23EA7BFB9F982F435E2D76F095D90AFC7C3AA5F80E42FB6874E030D + B11C4860042A10B820AA018C6E800FA6792C04325641C297242DD18F2DE6B112 + 380657883470C3723FCA08BECC04F236C8E1AB5CF9EBFD87ADE6622088F57C5E + 61246F01B577842F035604036200F2BE4C1B06C5BC1DE5DD407FDE2F836B1D40 + 30CF11BB9E90F50150947344ADD701DDEC01EAD37AA90DC95F0668EB0860069E + 11601E5FBC28638B576474F1824CAC9E9789F5B332B8B8218DF925692EAE486B + 49DB39A16D2C6EC900B6A25C3E2B0BE79F9213179E91A1C5F3D27F64576C7BE5 + 18500E81E63298ED41B907E5A92D602B98164EC655586AA7B0555B088C23903E + 1FF45C7DC51558907BB415246519C44F03FB8011120F00FAA12F121139B94741 + AEB83240BFAC334AB9E6498020B57AF011750694B17AD3A5D85E17AB2FC737F4 + 5A6F5E92E99D2B8AAB9ABFA2B2CB06AC86C56ACC3608A365D86375EC85B08A73 + 2510B85CE169642EC82705756155EE39CBB33C08A258EF1B93693D926F20E5F0 + DDD682FC8DB06FB92A20B3ED8CA33DC1FA1E241201DA14ED441989BF94A0441E + 2B77C7B7B08257CB1B2036434A023490C01765740B2B6DD576FB9A8C6D5D531B + 7C07F8AA7D8317E5D1CDEB96E2BBB35327AFCAF4B95B3273F98E4C5F794A8EDF + 7D45169FFBB02CBCF886ACBEFA49D9FAC817CBF2073F256F7CF70FCA37FE959F + 93777EFB1F92D39FFB55B2FBB14FCBE66B5F22CB2F7EBE2C3FF309597DE6B365 + E9898FC9C29DD7E5C8CD77CAD4A56765ECEC93327EF29EB67347C676C20A6060 + 74EB92F64FFB0DE21AAB78B5CFC3F84EB1F67168F5AADE2B57F51ED2BEAF6B9D + 0D10D8380FE1DC8CE93D115639E33CE29841B4E21C82603D5360587D97E42FEA + EAF50601ECCE29AF0BCB046DFCF5E2F5416A64AE6254EFA9D165B58F44304860 + FBC632EE374D41FE8EEB7181B4B77AF0B189EBC32DB0B52EBE85AC29BE0F8C2D + B2B95537DB2150B6FBD7FD7EF81B34E2D701BF312373717FDAEF4A6D717F401F + 654821830E082B9CB59DE27719CA41A7FE706EB40FB65A5EFB83BEE038B1621D + BF43C8B15ADD56A963452F8E55ED024A3B2390F1670B9C3B4798976431108E8F + C0F1F0B8FC31A636A98C305216CF2897C7717A39E17D14B65D80E71B9F71A863 + 657D5E5386D4769F887AD62BDA80DE3DF73DFC7315E42F522F63D9DA8BEF0894 + 7D7D5F87BAD03F7D2F69BB807F9FE5CA9D446FA71D4002979F396039258659E6 + 9F9F90671DD31F2F7DE3BDDBD0B103F2207F91820086CC8F25B8F2D723D50F39 + 79A9D736D057ACB0550C69DBEDB95D1D976C07A2179F9E983F2DCDC35BD27F48 + C718AA6FCDE9358BA42E096012B440419E2A52B911B4518E342572598F79EAD3 + 1440BB24A221E76A6012BA9ED44DC115BE00CABE0E57449B6FF49363508C5315 + 7D3ACFB755BB6EBCEBF3F873235644E37C830C06298CBA7D3A7F20510C1801AC + 80AF0AF90BBD2380C318BE3AD6F7E01F367DBE9B7D2AF7B639781BCC43FA2201 + 4C5D9D9F460DE9EB919F1755C17999CDD58AB91B08DFB042387C5B18732D9DEF + 81B455DD8082693FE6B10AE4016EEF4C821773C91C019CCE557DD9DB7A599D3E + CDA33DDF26E4854CE7D49E00CE91C03939E7EFE9FCDECB527DE9A74AFA7A7802 + 9871921C181F49F3BE6C70F115AF2F62328867D126DA55E0E52ECFFA441A5F02 + BC3CB527EA74DDEA108CC511399B8703F8CE234700A736293AE2A535757276C0 + 5E364157F69FD7203D5F9D75BAB7FB2851B48DF8708C9B173C0B90A993C2C7DA + 1F0495187E42FE9A3ED356D90FB503C015595E65DAFFB0EA97E716725E8304F0 + A57688C136F5DA359A110393D2EA9F9076FFA434B54FCDFE5169F685EFFC16DF + EC557822F5ED02F417BBE6BE7D76CE05499B70857105B0C1CA25799B057DE574 + 39F8F64DF6E808E01C997B10BC650470BA15343AE3CB397412C08E75B7135F3E + 083A7EFCF15F18FF59C03DECDE74E0611BC9DF5E08E014BDD875D3E5503ED8F3 + 301BE7B31699BA0F8EF8C2E886F4059305EC1E22B4DDFE01FC9B4907202909EC + DAF58314A018600218B0E8A0AA0F3E5AB3D2B255C0F32AD3813506D7984CE860 + BA6F4807CE3AC8C6EADF3E10812D1DEC681DA2F06FE42FFAD7D96E0EB93E52D6 + AB8FFD8083B41C3CB94BA4756143D006FDA4ADB7A18CF2B69EBF162620560E24 + 704BCF2B885EDA779C8B249F9D30F8B297ED0B98B0E8C408AB7C332B7DEB904E + C8F60BD6EF86D43E3B49D43E7B594517914E3059F628EABA892D27B74C8B496D + B449C14935EBD2CE97EB807F79DB778031C9C756CFC33A898E00015C6C090D22 + 776241B0E2B6082624A8041A9C8CC8052F18E048011DFDF934AD0F4299AB70C3 + 6ADFC58204A61CC4AD91C38E0046B0C5077D5806F98B95BAD8AE19C02ADDC266 + 0A2B82573A49E0E813846F2078830FE4FD6ADF1231408500960B82F97C0E2065 + 992F026A11B6B24251AB8FA42EB6E2F33605D9DB058D239A1E592F08D91C79FB + 2020E14BF2D7A34ECEA02CF2390298642B83CFB6F277F18A8C205D3D2FE3D8CE + 75596D96D7657855EBACAC2B366578FD94344EEC4A3F82CD8B6765E8C479691E + 3F29ADE3A7D597A6EADFB694B4A074840B367743D1AF9882E8851C2B9548DEE2 + 3BBD2989CBC0BC2F93D80552596AEB01B90FE633B86F8402D2445F27F3287C38 + 3BC8482E01B4F132BF8D2A613A47CE78B283F50A1D00722A92972430B9953156 + B20213DB976572E79A1C3A7D4B664EDD340294C41D883A10662408B13274487D + 0C8384DA0808AB7323F9A67540ECB17E49FAA10CC2AF24004741326B9E041CE5 + 4851D708B928A3DCC3DB52C6763D284FFDF9FAB4E38A52AE7C4DBF8B4B92D4BE + B1BBA9E7701BC4AFFE6EB0B5328E07BA0D2DABCC56006B3AB37D438E9D7A528E + 9F7A5A8E9E7D87CC5F7E55166F7F50169E7843969FFFA4ACBEEB8B64F15D9F27 + AF7CE31F96AFFCA19F9497BEFA3372FA035F26275FFF32D979F797CACA739F27 + F34F7C5C8EDCFD2C397CE70D397AEB3D72F8FA4B327DF1599938FB944C9CBCAB + D7ECA68C6EDF94B12D90D0576CE5712081435F47548E2DA047B13A79E386FEC6 + 152B3764744D659100C6EA5FA4A84F9214D7053290C23827618BE800BF257620 + 8C2143FEB2D5C1F9C43907D2F3CBF38EB40AC8CA6B07E212DF680EDF85C68A56 + BD7EB8E7A23DDA437FF10D663B065C1FDBF25AF5E817887A455BE5B6CA1784B0 + DAA14FADA53305F1CBF6CC6FFC7D019595BF11241647E33D81736184B8BB4FF8 + 5B639E801D57511B911E65C11EF76838E7EC13D24A1FB57FEC27F5B4A1DDE0B2 + B6ABF02B8801AE140E0470383E3E2BEC5871FD71AC9A02DE06488F3F07E8618B + BC7F36793F2CD386442E90DA015EEFEBA472AF0321DC461E3AA45A468AE72C9F + B500EC292B746939D64FDF0FC8B3CC3C415BBC5BF99D7DBEE7904F617F728A79 + DA017C279608842F895C9641F062952F402218F0324F00B7B185706C0FEFD9E6 + B13046401B7EAC40594AF6025E867106CA5C1D4C983F3B0EC8353D86B67674CC + B02B0387370D430BA7656C49EFE5637A8EE74ECA88E6B1DA9DA42E096082C42E + F5DE8E3A00C4AA277429F7F07AA494D1DEB709992780B9B535495D4FFC7AF2D7 + B679866FD5B11EBF398CD5D06D6C798D71A48E376DEC8971AE1B03A7E018B8A5 + 633F10C0C338AF47B47F2AEF9BD4F135FE3C1989DF02C90E3A2099618F31FD5E + 842E6D087CC60529ECFCAADCB49E9731EF6DBCCCEBD232DB03D2BA0D371F2238 + 0F42DA2B72F3B13A0CD8EE4F7336FF03FC1F8209FC319F842E50CE27ABF352CE + 4D73E5EA3CB4ACE7E16DBC2D523F2F2628EB3AD78EE8C58676393950AD8F7841 + 49FA7A78023825826D41443646D2894A9D8842067D2B89DF10F4E1CB3E8D795F + 1F408C2845375D0EDEDFE385A95AA404701DB27152875EEAE46C80BC5D7A0C7A + DD5CFC322057AF53FE66A0681B31EB8118F7773A4FB0D7A1E7587C442EBE6F32 + 92CE3DD813EC7F5D1F2B3A6BA3AA67D95609B726A5A5D76BA839234303D3D2EE + 9F92C1FE4919EA5318093C2ECDBE51698000C616CF391EE77147426A926BAB72 + 6E8F2BD077CF15E6E08ECF13B9C97157506797D631FDC3278053023767B31F3C + 162B80EB0E866C7DA59CBB783CE1FD9A4682D21E087C48D883A2F3867DFC6FE2 + 1A240FBD3715F1FC3E2A0238A707BC8D47E501DD0DEE6551D475FE0DB97A0F0C + BC501E0672BE7B048EBD43A6E72012C03912B8DF13B33E9FC0069BFA126C340F + 49AB7D441A6D1DB86B1EA460F11D170C1C0190837110D9680458DB06BC7CE113 + FD63BE3B786E72B25E7DEC07B901711D7AA90FE2B6C30E3AC835ADC8F51CB11E + 0877A46D3DBFF816057D797F9C08A4E50AE00BC8C9F642F1BD6694316929C9D2 + 5EC189574ED70B58BF1BBAD977D3115EEF278F5C114C789DAD78CF4C7A39A125 + BC8EB6BE8EB7A3BF6EE03780F1EFEEBEE1E37902786C416D3B030504830B75C8 + D52110C808DB429F90E6D49281E5F2FB564803AA2B89111C89A4EF2456FA6A9D + 886E0430832C840FF218F90B9FDA0F92B8157BF4CFFA183080BEE23BC1B12EEC + FBB41C886045CC7B02B8712890AF0C7675237D01DA3462E06D8081B4084FD616 + 726D03DF092648DC623B6683FAC0EA5ED3BBFA1D80FE68206141C67A1298A01C + 36B4F3F032DA7B39EB113E089BEA3C0A9B040C2633181D561F9D575C94E11320 + 5DCEC8D0EAAE8C6EECC8C4CEAE4C6E9F96B1ED1D19DAD892A1B593D25A3E254D + AC34031614F3A7D5D769F51502DDDEBF015B3E2200CCB2036D81A27FECABF3D3 + 56BF16D48FF081765F4EE55E4732380DE07B7B96A1F74484910CB10EEB135EE6 + 8903FF8D4CC0EB49D61A611B7545BB890DCB464CA0ACF064070910EA40FC1AF9 + 4192A9209802F9EB0960E6C7B72ECBD4EE750356C1824C02C104B2CC8835239B + 48649D33020A5B2007B234106FB68217045A24A7CAFAB00DF9A155108B9085BA + B0B1B64056994FB61508AF50BF136C87808C7D497580F799D313C13FF2E83F08 + CDF3769E40800660AB6710ABDA77128C9AC76A53FBEEEC2A88E02B328C6DA141 + BA2AA6766EC8E193F7E5E8EE5372E4F40B72ECC2BBE4D8F5F7C9F1DBAFCBFC53 + 6FC8C20B9F90B9777C5CEE7DC9B7C967FDDEFF5A9EFEF2DF295BEFF9946CBCFA + 05B2F1AE2F9213CF7D528E3EF1864CDFFD884CDE7A5DA6AFBD2433979F93E90B + CFC8E4992765F2D41DBD7E37B46F37B56FD7348DDB4F6F0790FCC50AE1F1DD9B + 32BE734BC6376ECAD8EA751959539D11D8E17A0038469C0710F63C5F900539CE + 0DCE633837A35825ADE78172AC8E06E90D3FA887FA04CA6C83BA70BE53A83FD5 + C306AB7D8D04D67B06ABD0B1DA17DB74D31624AF11DCD12764A35B57F45A5CD0 + E794F611F67A8D707D40CEE31E805DDDBD00791BBF97F85B22194AF293042710 + EEE9709FF07CA4842FCA1E909104E66F9379FCAEDACB09E1ABF07DAD2B13289B + 4ECF996D091D57FD92FCC5B91C513B7F3C7876F8E7079193519E823A7F8E20E7 + B389F0CF2A96539B14D4D33605746C9F76F6AED067A33D1F23EA9EA97C7617A0 + 2E965137F77EF07A0F2FC3B781410093E0C5BB8CC42B91CAF86ECC03EFD740E6 + 92F8F5E4AF017D0529A8A82780A33FFB43D5B67DB681EF5F003ABE938B773074 + 1C5F508E77749423AD90BF906BDA98DB9426F2DA6EE3C8A6F41DDE32027870EE + A4AD00EE3FBA655BBC9FB8FC8C5C7CE183B27DE7DD32BA7251C7563AFE519084 + F5642C895EE6EB40A237B565D9EB41F0524652D893C14C6143F237258049FE7A + 504E029824F0A01E8B6D87ADC78FB124C6A51CCBDA78166350078E5DA143DAC4 + F834A987BCD9AACE13C0F6C746027EEC4F9A3AD6C698BD0BEC1BBB3A26F7C811 + C09C1F10A89B2B7BDF399B14BEBD9CDE13BF443A87EA059C87E5FC74CCD722E9 + 4BB29704708ADC7C32454AFAE6D06DAECA79AD07E5B93931E555623680736D9B + 6F67F480B7F1B675F22A30E7EF9D00AEC011BA053AE2278A4C5D4F00231ED311 + BBF13EBAC914A80B306E94432F367560DD14FBB17D7898EA0A4F027B50DF111F + 4DE07D79F46297DA9476E93104F81866AEEE5B85E2585C2C1D79C8B13B2156C4 + 0EC026176F8EA88DBDD721E3A3826857F8CDD978C0266717E5610B6845E2977D + 461AB6809E94865EAB76735A0607A6A4D53F6904F020C8DFBE71C59834FBC3EA + DF7405B0A10980C34A78ACC7146F0FD2D703E7D593BD39B8734F4237878ADF7D + 40EB7A3EF361E16D4F00E756FFE60E685F0470FF880CF48DAA6FFDE19200E643 + D47EBC6F53B23787F8707A4BF0800430D0CD96FEBBD978743CC8EB8097527C71 + 593DFAE5CB8CFA5CDD03A2DB4BDEA3FAC2AF43BE8D5E11FAE2A03E41F4D611C0 + 945357E4D96FD73794DBAD43D2520C3415D8121A833E1D2C0F0C1FB681563150 + D5B6F1026D36A60D2081CDBFF9E571069F25D272806F3F9575FA78703CE860D6 + D745BE42D8DAB989E7296EB16D721E8F9E23E81A389F0AE4E18304304863F357 + F153B6BD97AC9840D4C14F666CF2A275002B63F25292A9BD8293AF9C2E073F61 + 3B087AF1E92781AC43796AD78D004EFF1DED27B7B9892EED7213E5E067EF493A + C8DF3E4C5CB5FDCAE41EFFF28EDB3F37B035B4968B00412690E0030805B0DD98 + A60C3404C03EAC24E6F78149A636A7960D05B93A4120109207ECE007042FC85E + AED0E5EADC4008877C1A846160A62F6EC70620D0536EC91C4860DA118DC9C510 + 3822E25673FD53A81BEA17505B12C381082E035F16A8B2C0593E10463DF3A63F + BC66DB17FAE0178958A008882178168956C0EA81F855D0B6E956F656485F0FE8 + F6208001B6473B0F0655EBE40082A975F0761E853E822B8111B865503790BFBB + D29E3F23830B2006CEC9F0EA4969AF6DCAE4E95372E8FC69993D7F4E662F9EB7 + F2F0D64919DE382320C8F0ADC9D6DCAED63B2D83F3A76CA5EEC8820241670B56 + 8720320860B6C9767D803945D167D802F381FC4D57F402A98C81776F9306EB8B + E0BF9379D00F890523092252F2C103BEE8DB90100DE61F76D4793D7CB83ED017 + 090B232D1CD8174F00739B67104AADA540007BA2090414492B20107781E09CC0 + 4AD68D40AC7992AE4A36211F082CC8ED3BAE20E1221155DA310F5F204C03698A + 32C85F90C0208051177661EBE7D2177C8304439E7D816E2FA4B6BECC7E21A53E + 07DA87BA812407398E15A68491C071752DB65BE62A5B2319B11218DFD9DDBC26 + 23D80ADA56DDDE90C99DFB32BDF38CCC9C7C41662FBD4B8E5C7B8F1CB9FE3E39 + 7AFF8372FCD98FC8FC731F958B6F7C953CFF55BF476E7FE1D7CACA7B3E268BEF + FEA82CBEEBB3E4E8F3EF97D9A75E95C9275E91D19BEF94F1CBCFCAD4C56764FA + FC333275F609FD5DDE91F1DD5BDA2F6CE77C4D816F105F95716D777C072B8343 + 3F8C003E79236C158DFEAD693FF59A0CDBF78BAFE871427ED58E3D5C5B10BEF8 + 3E7020856D6571B1BA37DC4B3847207D832C5C2F92C581300E7202652F0BE7B9 + 0AD31736B0579FDA0FF4CDB6D8DE0CFDC07D05827748EB84EF43A33F6AABE77D + 48FBDC5ED1EBA7791C9F6D05ADB6F069F741E65E801CF71D7F3358098C6F6503 + 29090C8CEAF193D405F8BB828CBFB99C8E3280E560DB49E47AF8F386B2CFA720 + 71EE57FE06F217B240D092A8659A7BBEA5FA1C783E68E7419BD4A7D7E5405B96 + 61CFB297FB7C052AC7F3D33F53FDF3147902B695E76F2C57640EF44D3FB425CC + 6FCC93FC1D8918D67715C9604F0A039E04AE93070218EF4ED5D92A5EF884EF93 + 86E1F9801C31CC7AA1AE42FDB5F57D0D18097C3C8C11F05E66FBFE9DEDC71A36 + B6E0FB1936F1BD5DD82B5AF0A769736E53068EE9F8E628FC6F4A43C70303DAF7 + 26B6813EBE6B0430B631BFF0D247E4FD5FFE4D72FF835F2053FA2C22598B6D92 + 094F007B4216409EA0CCA39B0E803FA4B001014CB01DA0D207F8823C12BB2901 + EC095FE42B503D7C602530B672C66A5E8C5F49E4027ECC0B709CEBF528877134 + C6CF6EEC9C12C0716C6BE35BD58715C07952D593AD1CBF43968236390238056D + 534097238F736DA57A93B979969F6FED179C87E5FC54E66CB08BC4AF879F1BDA + 1F8531DFB59DA3CAB9646E852FE594A5A08E7553DB745E4B9F48EB086080F371 + 8F742E9ECAF60BEF3B00F3FEEE0470375D01C64B621CA5207853BB889400EE00 + E331B11C165644FB04B698A0477001414EE791DAF5A30F09BCBD47CEF6E1616A + 4FA4E42F405D1A2F257CFD1C1833CEE9887A7FD563E079F2314C5FF7B100E3E4 + 2E7E8E6301F9DB2B015CD4ED05191F15B03F399D07EDCC2FB820C808CA3B51F0 + 0C5E1E658863371A8C6583040EE4EF60DF98E641FC8E064E8A2430BE2D4B22B5 + A5F9560D97F566A38EF04C641512B8D045DBC70EE833CF6F0DDCB1198A6372A8 + F8EC0E9C9B81E41C793EF371C4232380FBFA862AE594F4F5C811C0247F2B0430 + D000D20B03C2502F80FDD3427F9CFAC3B407AC3DE01528DB0320DA3FC0457E2C + E01F466F362241FBB009E05AC4F658AEF8C93DE8EBC017C05EC8D53D00EA5EF2 + 39545FF875C8B7D32BCAFE44A8CF94E83582576539790E2481F1226C69B9A968 + B4C220716050F53A20082B5943FB3668853D5E96FAD2049A4D1D34686AFEEC7C + 049FD573949603786E72B24E1F0F8E071DC8FABA7EB087726570AFCF2D1BDC13 + 2AC339B5FB5FCB46020FE96403799C6BCDDBC441CBE6C7813ED272C5B668B706 + 9CC4104380D6F304F09BB002D84FD87A052774C8E77C7ABDB7F775A8F3801EC4 + 6F13DF5052204F9D91C236F9553F8A3091559DA67EA29B4E8AFD643920D4071A + E3BE0CFF01DEA65F27AD7D9888A22FF8DE769CE417E46F04749ED04D0309E1DB + 52419F220419D4AF7D6F0A4187C5080438960AC2B79910C0395408605706F9DB + 9A5995C1D97503CA207FC396D05502381F700940BE248001B553D0DE82397ACC + 081CB5A697A43D837FEF077D3FC85E237CB9823804AE6C5531EAC28723800310 + 442B61C1AD18082B036D31A015495610AEFD2E008632E40C80799907C9DA9CDC + F7A102D53120EA7DE4003D6CBD6FCABC9CE5144590B50675F61D042B83B431B8 + 8C40B491B98BA76578FDAC0CADEFC8D0E6A61CB97C5696EE5C96F5276FC9C653 + 7765E1E655993A7B4E46B741509C91FEE3DB3270743B90C073276548CBC3F39B + EA4F7DAA5F0691BB11C000C95E048C8BFE411FED6C9512FAA8FE3CD9EBCB4819 + A0A73E0DD83358CF3CCBB4635DF3893282F86A93120E86A58054CE720AE8B82A + 2D67C7FA04FBE9C95F6FCF7261EF561162A52EC9DF94002648FE229DD8BE645B + 40833C037904A2C96F030C9209645E209B405681BC4AC93CA424B9600719083D + 7C4F17DBF3062211043052907A24AF8CFC75AB384982C1774108AA9CEDD501B6 + B4474A12D2FBF07E689F435937D42FCAB19FFCD6EEC4AE1E9F9E3FAE04C6AAE0 + 6110C2387FDB57357F458F0FDB2EDF93C9AD6764F2F4F33275F19D327BE51539 + 7CF5BD72F8CE6B72FC890FC8E2D31F919D57BE40AE7DF2B7CBA54F7E859C78F7 + 47E4D84BAFCBE177BC57669E7D97E265997AFA9D3271E74599B8F2AC4C5F784A + A6CE3F6904F0D4E9BB3279F2B65EC39B32B97D4B2636B115F47519DFBE692B7E + 41F88E685F8C08DE41BFB0453456FEEA35C17901B98AEF15C37EEB861EDF153D + 3F385690C1581D1CAEA111C0B6BA3710BB7ED530CF2FCF156C790F797DEE1AF8 + 9476B6D53148DB75F808FD08FDC1BD14FA63E508BBA7EC7EBB5A10C038AEE953 + B7B51F2081718D705C81E885FFB42F9007F23592B30A92BFB995C0D079529724 + 2FF2E96F8FFADC6FB094390217C71FF32484433F437F7DBF73B6A65780F8E537 + 8503F9AB7EDC318CBAE343D9BE4FCC678CC26C544702D69E35B48979E860E79F + 51C813853DE4B15DAF07D80EC17ADE863ED807C8D8A7F4B9CA67A77FBE536780 + 6D82C26E0F197CD33F52CABC9E7DC0377549FE8EAA1FCB6B6AA4704CCBF76278 + 67127C3F5765FA6E320257DFB1F6EDDC40FC8EEAFB1B18D67731E0895F82A430 + EBDBAE1A58F93BAFEF7040FB9012C07ECC10885E8C2BE2D802B2639487D5BFFC + E35748E13FF86E6A0AF2D7BEDFAFE7A4357F4AFA41048304C6378AF5DE5CBEF9 + A25C7DE5B365EB8957F53EB9A86318F5ABB01D44225026494BE4CA845F350C22 + D793BCDE8E32C213BFDE86447496D455A404B05F25EC6D4000577C1F41DF746C + AA636A8E6F39064ECBB4E118D46418932B20C75898635F3F06066C0C1C7DE20F + 9C1CBBD7817FE2B4BCCE0BD2ED9891729E4022D813BAB061EAEB5196DA03B44B + DBA29EBE4CE6E6599C6BF97C8A3A39E761ACEBED3AE66B6F22014CB02E6D99FA + B92E41B9CD89150389DCF26E4E4EA473F154B61F78BF251E12011C5190B49538 + 4AA71DE41D846E2E461363375902D8E918674A9112B9BD22AD8338520AAF4F91 + B37F3898EA090725807D1E60DDD42E05F5793BDFFFF2BC215FC631ABF5DF4AD8 + F1C4187911C7472C5EE5FE9CA6F53C8AD87D8CE7EF899C0FF4C3CB7A8CDBB35D + F2167D03A15C805C83D3A3CE00FAACE5809037598C9563714E43AF5513DF008E + AB7F5BB6FD7349FE56B78156FF20809B8188EC3742729F3C54CA61E590AB9722 + 570F505D7F7F9927AAE46680D7576CBAC1B755875CBD9E81FA8EECF528B84594 + 5D9D83B61F8FB71FDF47561472F541EEF251C173A707C15BBA05343A90965319 + 515CB4E242860B849BCDC85DFBC1EA85D01F3A1E420D7D9962555C78A0E28151 + BD301D88BE0A9BC71976AC807F80219F03F50F09FA7024F96B0F52A490F3E11A + 53E67330DB58B7F0EB415D8D4DC557F2907F7C807BEE6122D7466F08838D1246 + B666485EBED0529D0D4E62BEF8E07D84C9F002C487EFB11586FEDE5A3A886D60 + 2050793187E300698C7A5C010C34B01535DAC4B7817B1824A683A43AD9C306DA + E8A57F006DF7AAC3C17A65008FC901D228875D73E8B07D079803705BF9AB790C + F29B24845107C79FFAD47CDA2EE0F545EAE5849B9C1886218B7903F49DE4EA5E + E0042CA7F3F093358093B454BE976DCE27F594797BD661DEEB01237A23BC1C93 + D73081D53AA37A6D0A92D64D6AA38D0749DD00D8687B05CA327484B7E116D0C1 + 9F4EBA93897F208017D4664EFA0090C1B61533B0280D0438C69300420C2C04C2 + 97D0E330800846DD139A829C3D1188DD880AD18B7C0D210C5D58DD1BECB0EAB7 + 7D684D860E6FC8108251910036F2773CD822B89206631064412066E8C88A8C1E + DF90C1C321D8D437B35C90B846DCB24E0C9E206804F23710C008E2685BD1BE02 + 2385F51CA90F12C02138C520D77A11E4620A794008B2C166E0D0BAF4AB8E81AE + BE432B065BF110650489E00E798427679B8743FB3ED8C6BE002DD5E3FB6B205C + 2BF514246253791D689F03C9DC3A5456E138F8002E5094150C302328DD3A714A + 06164E4A7BE5A40C6DECC8E8CE962CDCBC28373EF84E79D76FF9843CF7B96FC8 + CE73F765FAEC5919DD3E23C31B672D983FB4785A6C15D1F14D1956FFC3F31B32 + BCB0254390A9FF80D82EFAA0F9521E82C5A6D7D4B6A19CD7F222085CADAF18D4 + 3C40B297842F895AC207E9A907284730BE0ED033D00F22A189BA9A163A0382FA + E52AB4A1656D6755FD2E3BA2C1C1930D94D511C02C177D5A417A4A46B43D92BF + 83DA0E74F4E50997C125EDABD609C4552081483C05220A72B535BDD65B03B91A + C85F20AC7205A916484E124B3918510582A620A040A2A11CF2A10CA00CF21404 + 5E0048BAC1B802187A1255240947D51FFB5FFA2F017D4A1412DE8E806D41DCC6 + 3620A73D6594FBB23F175C856A795BE5ABE54D90C07ACEB6AFCAF80EB65AC677 + 7F03E9687A10C20A5B9D0AD9D675193D794BC6CFDD97C98B4FCBCCE517E4F0B5 + 97E5D8CD5765E1F6FB65E5FE4764EDB9CF96B3AF7FB19CFBD097CAD24B1F95A3 + CFBD2EB34FBD57669F7C4D8E3EF17E3976FF4372F8D66B327DE50599BEF0844C + 9D579CD5DFE4E9BB32B5735B7147A677EFC9D4F65D190711ACB209B4B9ABD8B9 + 6104B0F5790BAB8391D763D53E8F6E5DD332560D5F579DDA693ABAA97A959360 + 05898A32004218C73902621684B0FAC0B9C136E0610BE870EE3C788EFDF5F2E7 + 1B65AF0FF706FCE15A94442F885DDC4358658D32FBC47BCCFFD100E5A9DD9B32 + B10D525BFB8BEBA3FDB3ADBA8DA80DF79F47E843F8FDE037C615C0637ACFA34C + 1851A9F270EF07900066FD14DE16E0EFAFACE7FB509E0702E550A73C57852DFE + 70A136F816F710486090BDA803C27759DBD23200127854F5389E71FDAD4EA8AF + F1787C768CF1793302C25853FBFEB1CA4B0216CF3CF7BC8A723E8FF87C42BEAC + 13EC8011F5C56F9B5306A00D9CE789D82700327FCE61E7FD21657B78AEF2D94A + 5D6AE381E7751D8AE7B0623F3A0FB631B2A875F4B94F1218A42F895F00EF5F23 + 52F5FD17885A92BD246C212F77EA20893B085BACF8857F03F27AFCA81775CC57 + 81BA783787F733B6666EE8BBDBDEB7206AE37B1AED62CB6610C29061C530FEE0 + 652B8715B6923812B2ED480283F4C51FBFCC7F3C06B4C3777443F50DB5431E5B + 3EB7F51CDA9803ABD4F1BB5EB9A0B273D25C3CA363013D878EF8F5043008DD94 + 9825B99BCA61EBEB14C038CFD99228A61FEF8BBA22AFFECC076CBA00FE730470 + 6A13C692615C6BC4701C6392AC4D095FE421830DC69F00E490D9987A5AA1E368 + 92BFF0C331A68D73A3CF60A7636E1D77935425B16AE548FC7A9D077504C6FFDD + 08606F97CA3C726DC09E7E2B3A9D3F009C77002C93D0E57C2BD57974CECDEA31 + A0768D9180743E08D82782E29C35256EBD8CE55E11EA210FDFC13FC0F9AF9F07 + E7CA4C410CFBB935E7DAB9397CDD3CDDCB7B4727B94B5408DB5EC158494EE701 + 9B5C2C46F388F558ACC5C9ED8FFF90D10E80BE39D511837AD848E34904F5DDC8 + E2871BA383AFDEE0894A206703E4E2A6DDE4FB43DAFF2A18C30CA8C672AD7DC4 + BE33F2106F8D31D71E49D183C262EF8CC9A3EC8E2FB5F528E2F62E9EDF15191F + 1E7BB55701F890FE0919E89F9401CD23E61C085D60A2E033D0AEF11B9A82E86D + 201D1877185379D0E15BC0F01DEC26A5A5F5B0F5B3ADFC55F40315F237C2B67F + 1E515F01FD9183F2306EEB5172522907D60D99FAE8B3AD6ACEE8F644EA3F875C + BD9E80BA8EECF5F90A204BDAF4E881940FFCA3E6CD5E535CAFE29AC14FDA6627 + C87192FFF41CA897E540BB83E26D4D00F7377101F4C78A1F361E16FAA3C403A1 + A92F51102400B625B087041E8876414A14FF708817EFED43008787D45B82F890 + 2C086095D9031D8865C23FE8F9E0EFE9E11FDB2890E82BBED287FC6305BEC47B + 47E700C023D7C6DEE08B9930523747F46A192FC9544E60BB6692B65CC10BB4F1 + 11FC3EFDDDF5E90B50AF4913832BF437FE1EC3404BA13ECCB7C213C078199B5C + 6DEA068C1ED4F1BCE464943F4CECD52F0FDAA6757CFF7C1F8B817BCCDB3F3B63 + 3DDCF3833A21DA3A73C3308E89B0EAF82F4E1B68C74980D92B8AFA94F9B69007 + 54574C1A8058B722036CC56F9CA06481495195B4ED059C6CE5741ED58959890E + 5B7C633A91A575FCC4D023D8624570754219FCC02F267FF5F50D3A61B509AA5E + 2B3FB1F565E673E0643B37E1DE6B125E6D4B7D60E21F57FC72B21FCA8B6A876D + 9B03AA2B78117008410AA00C2490F43D2EFD13084E84720842943EB04297AB78 + 8DA47528B674762430F295729435A6566C0530D09E592BB681361F518F004B5F + DCE6B97F5253F46B6A51EB2C19F13B36B7254347D66500C123049834B5E0D434 + 82316540A68920CD5458016C882430EAB18EADD855F07BC00387D40F6C14169C + 9A0A69088685A01789570B8EC5FAD059106C162B38F4B818E84A48DE34EFE103 + 65007D000CAE857EA8DEF583C137B3419BD1BFD53BBA61415100F99C9E72A220 + 6DA3CE833A4FF47A5D4A00176DC32EA28304860D82B60B202677A471625BDA2B + 5ADEDC90B193EBB272FF92BCF1755F2CBFFBCF7EAF7CC5777DBB5C7DDF4B3273 + EEB48C6CEDCAF0C66919DF382FE3203B16D48FFA1B06093CB721230BDB32A8FE + 5A9A86ED2577A439BF6528561BD580A46F95002E83C88027780904DB19982799 + EBE1F50CD2E750D4D13C48589347E2D7BEAD8B95BFCBC8ABCDAAFA528C82DC81 + 1C3611242ABC2CD57980AC002912005219ABC702B93C4A792441CCD6112285CF + 55D5E9F50844AFF659FB16C8A7B32E1F6C483AF92D8E2777AFC9D4C91BF61DE0 + 891DAC080DDB1D8F8070528C6D61CBE30B9A3F2FC3056105720AEDC16720F586 + 8D0406310562AF248149CC210DFA50DF082DAD67AB3E6DE5F005237BE1AF58A9 + 197D932C0CA45724B9609FEA14EC0F901291947B400F221CC74D42DC885FED3B + 48485BFD1A57FA02C8632BE8B01A38E8866CF56F281B290CD9CE65193C7359DA + E7B57C51FD5CD2737CE59E1CBDF1BCCCDF7859166FBC47D6EE7C4036EE7F4476 + DFF1B972E6952F9413CF7E448E3CF59A1C79F2753976FF83B270E70D59BAFD09 + 99BBFE861CBEFCA2CC5EBC27D3E7EFCAC4997B32B17B4726B7EFC8D4CE5D99DE + BD2B939A4EECDC51B9A627EFC9F8EE3D19DBB92520A187B10AB620ADB59F384E + 5BF58B55B3207641AAEA7868FBA680040E44AFD6C1F503B9EACA4622837CDDBA + 1A8855FC0140CF0B57FFF29C73A530D2702F6919E72F96B9FD37F258955C5C27 + D305521D642FDA045104204F5298FD415F40549304C63D3701627B03C471247E + 15B83FEDFAA10D77BF10FC9DE0BE2601ECC9489295247BD33F5620F53EF85BA3 + 2D752893040E72D40FF732FA9192BFB65A3B9E2FFBAD4419CF1DCF6F207E41DA + AABF256D630904EF05C3A811C2678DF49D52FB293D0F205D51068C9CC5B305FD + 8BCF217C13DE56EEE2D96379E8EAE19F6B00643C77CC17EDB83A38AFE883F507 + E741CB1EDE37F3FE79CAE7ABD7E5FE70636D2245193A07C8F81CF67AFF1E4965 + 1EA9DCB665D6F7130960CB9B3C96F57D86F734C85312A8442153903C2EC85B2D + E3D87C7F00BE0FD136F339F05D4A52B60D2256DFB50D1DE335F12E5639B76CB6 + 6F062BF0BD5A7CE6C108E8E30A9397E30B1B5320D5BA7C97F37D8DB4A91850FD + 007C9DD06BA4D7DAFEAC843F05E96FB679E2ACF41DDD91BE636ABBA8BF9184FC + 25014C786296C46E4AD842C672D86A3921822BFA72753156007B1D90FAEE46EA + 32CD21B5B771651C5FDA27450095E37BBD362E2DC6BAC186636896AD9E8E4761 + D3AF63794F02733CCBB1A28D7FE1D77CC43F6F2A303EC71F3DFDF7763D019CA2 + 20601D48EC0275F254073F29EAEA011D24709C577980E025D232E1095D607F04 + 709803364743EAE7839E00C6FCB2A9F3BA745E99C2CF5173A8DA633EAA736603 + F2F9F96F98BBEA5C3BEE8CE5E7C9968FF36BCCADB1DB16F27BCDE9737FCCF675 + 7CDD9C3C47FC021DB10F12BB40AA5374D4CBD9791F8CB9F8584CCC67C9DE1CA2 + 7D1A83EA058C17E5409B94D805BAD9A73669EC2BB5F5F2DE005FBDA11BE94BF4 + 62F360F07DEF8E70AEEA63BA595D46F6D08158BC712A8A9CBE063E76DF0B3AEA + 7739EE3AB00EE3DB030358AD5B028B971A460C97BC063E25DAD0636B34C60D46 + FA3A18610CFF6DF53BA8D03C48602388FB41FC8E1448B7490EC42E7C8C482322 + 4700BFADE078B4B71EE80779420747BA062EB14B7F8D1C46BD8C2EC2F8421C73 + DF9002FEA2CCAEAFA6693FD2F6233CCFD90BA94B9EB417DBBDF0D813C0C589F2 + 27B20048603DD97808F5EB45C18F4F7F88587D0812B881170A7EF84D1087B820 + F1E2E9452B96AFBB1B375CB4F2023FD6C003F8AD0049D9F8A0CCDA44743CD033 + 3659B8360C191BFACC3DF053142F800439DB8785B29DFC8BDD8303220E8A72F9 + 807C5B7BC11F33FB44FF8011AFC8430644021884ADA5D0C7BC2781895663465A + 03AAC34BD45D1B7B29EA6FD1FE8481815DF40D701530FCE1856CFED1CE3E0682 + 3C2FC87B7B2FA7AE177F7B613F7E689BD661DF7C1F71DCC5201FB6AE9EC9F5BC + 8E4E2DC8B53B2FC8ED275F96A38B3B850D52F383C901CAF0612BA931480F40D9 + 50D8A31F0AD55580B6D23C2625E984A5828391BF00275B395D8AEAE4EC60F5FC + C4303B391C99D53C49E0606F7E86F5381503585DAB13D2B4AEC14D5ACB492DE4 + D5B2C932765EDE1FE1ED7A874EC441FEEA84DF4FF401ACFE6D8C2E6A5B25019C + 12C1206B9B46B462E56E083E0C18E9AB20D13A311789D318C400798CBA9A7A3F + 4CB9CA97042E60AB8123F8AD5F9607904EAF1669C3E95A660B9B106031683FFB + 10F040C0C40235CB327864CD561183C83532578FA73113DA096DE13841FA62BB + E940FE2248C314B644D30257084405D2B71109E0B01278C982583EF0E5036094 + 630530026A5881DB06091B095623727D3E8204AC9701B69A1984760CB8C1C648 + 59A491E825098CD493BF56D6B4A1E786F53C294B621672B64FBD054B356F6D29 + 28B35537CEC6EC22ACAC3EADAC36563FA6DE7E2F02982B7D6C15AE056755BEBC + 2B43EBBB32B2B525E3A73665EBB99BF2E9EFFE56F9E19FFFABF22D3FF4C7E4C6 + 07DF2B47AF5D96919D33D258DA952602D2274E6BDD5DF5B32543EA63706E5DFD + 69FBFA3C05A9DC3222380D4023288CE07058F54B30685C12BF207611E40EC16C + 06ADABBE42B03B0DE8A77A1F246770DEE48BA5BC84FAC28A5FD82E284E9CB52D + 540797CF487BE58CB4B49D01F5DB00410C32C3C809100EA7F5984BD201E5B072 + 38A4F8F626C8E4928C403E003A231D0CD039320264C96A6887440AF3A54F904A + 818402A9CAED64B1B526485510B4411700420AC4D3F816B67FBE22D327AFCBE4 + C96B72E8EC6DD9B8FF4E39FFD20764F5CE3B8C281C06D1BF73492676416C5E92 + D1AD0B2AC36A4A6C798CE03DFC0762CA082AC325DB9217641C51258003091C48 + 3FCD6F46522B7E1FD84830D581B0431B46D811F08DB64080298CD02569A87DF1 + F2B02A196995F42539E9894A005B61CF9CC256C8E5AA52908C58216BD0F31188 + 5F9E0BF55B40DBDFBE28637A3E6137B17B5DF36A7FEAAA0C9DBD22ED735AEF9C + FABA704BA62EDE97A3D75F90B91B2FCBFCCD5765E9CEEBB274F703B2F6CCC764 + F3C54FCAB1A73E2087EEBF570E3FF19A11C0F377DE90855B6FC8B1AB1F94D94B + EF904317EEC8D4B95B3271FAB64CECDC9489AD9B32B9755B26F1DDDF6DACF8D5 + 3C08E0D34F28EECBD8A97B32B47D530641EA82243582FA9291B8E35BB7F4DA5D + D77BFBB25E031C6F58093C6CC4A95E075C3B05C95712B23C2763EA03C4F260BC + F663766E708EF4FC58AAD71FD06B5712E7A16CF70EAE1988732DA3DEB8AD520E + 7ABB8FD603C1EBC95EE6D92FA4E8CBD4493D27E85724836DC5B2D61FD4DF80DD + 4BB81F70CFB97B81F712C9D6B6FEE60A8256EB8CE96FC548E00823850B521775 + 02B19B237A918EEAEFA72479C3AA7CFA0FF7A7AB637E2023789FE3DC86DF047E + 27F6CD63B4AD36C3EADB9F57106B58A56F5B3E63D52F885FAD0F6005EE88EAC7 + D5D7949EE7099C73EDDFD8BACAD527F4B6B3037621C04A5F85ED4880E713FA16 + 9F3FF60C42AA7212AE48C33349CF9F3DFFB47E222FCA06D40FB2C236EA690B58 + 5B763CC187D577FEAC5ECCC38F977365306D58D713BC4CF99EB0B2C28E01CF72 + F870EF12DAE5405F4519B6FA7EE40A5E6008D0F7181076C0281156E882080EE4 + 2FF4B60A58DF89F015DE8FE53BCDCE5704647C47B29FEC770AB3411EF6FAEE1D + B43F6D81B8D5B181E641E2368EEA9845D18A5B3E0FD93B1EFDC17B5E531B3FC4 + F1848E3F82BDA62AC796CF0509ACED60578D86B6D5C02A64ACFE3DA1BFB325BD + 7F57F47E56203FB8A8F2791D5BCC9DEA58013C84EF1D1BE95CCA480293B43552 + 56E1495ACA491A370F071DC9E04267B2E00F403DAC02A63FFAA2BFCAF8CF807C + 0992BB69DE97098C63FD274C001B9BC6F128C6BAFCC3640A92C1C8DBD858C7E1 + 28733CEBC79BB88ECDC3B00975423D8CCBE338DD11C0061D87A7242DC9598F3A + 5D4ADC92BCA5DEFBF5B0BA3A07C2DC8375523FA97FD817403DCC5FE29F596D87 + 21F8C15C2603CCC77A257F89C6B8F6016924804902872DA0410007629704B09F + 93FA79692F288960D4ABDBAD2AF5897674AE6E3B58B1CD601FCA612EDED03CE7 + DED82E3ACCD9D58EF379D4333B96435A9451CFC5033CD2B801803F847BF2B742 + E412246E092F8FF96C3D82F51097417C6630CADAD3454CA800F52ECD03B6652C + 294511934A11DBB198542C7B4006F297790F2F4BEBA4321FFB3A08527F9DF1CD + 3CF2B1D040F8A6C8D93D3CF8BE77473857F9B82E50C45AE9FB00A4EC41D06BEC + 3D45D1DF1ED1513F1E672AB705683575618F054941AEF90E0278A2885D139DA4 + 2FCA012086B102D8161FB6B58DB69E6F7CE610326CEF0CE2AF7F44FA48022704 + 3008C2014DB1F2B7930046FD1449FDDFC41EC0B92C39C22C8CDC459E843C495B + 007AF08ECEDEC1C8FB56B82E0DBDFEA3134764687456F37A5F4479673F1CC869 + 3A78CEB317EC45000752BAFA19DE1C1E6B02B872927227D25F9882C80D8430C8 + 2743EE07A43FC80A011CE56F2F0258FB9A4549923E32F440D066B15FFB14AE7E + FA907F3351BC6C7B40AE7E37E4EB23ED866093D6CDA31C5C7070960ED282AF32 + F52011EC612B7BE93FF6A96C0B03ACE0BF4FF3F85E0209E65037E4EDCF1A0E69 + BBDDB0579D6295ACF3DFCDBE0EFBA9E7DB4991DAE1BBC945396E835DB1D77384 + FE4FCD2CC9CCE11519C2A449E5E9C4C0EC5B9AB6663BD0DF560CEA8406DFEA55 + 1B83CA2AF032CD5B9D21CD17E0A40813A4527650749B7CF5023F81EB1546D6D6 + 204C42ABE81BD17310F39CC0FA896A3961D56B128172F16F667C6B49279F218F + 344C4C6D8B6645BF02DFD4B5EFEA465F7D40D4D1AE278C023A89CF802B820746 + E7B58D79F5EF81C002020C2714084A202DC95C10A56D046DA616D556FD4C2ECA + 20084695876F4CCD4B53EDDAD8A2192B8CC74F04D2D791C0DCDEB93FA2CF823A + 0800213804B2340497FAD4AE6F4A75C8238DFA50174113045D4260864116C848 + 30F78320B56051083A19890BB9FAF54433ED4128FB950484057860AFFEA06F60 + 5B686DD78076D5266C030DF257F505E18B3E831C864CDB8A72E4B1755E0B412E + 90B0B05554885C90AED19EC1BAB27E8005D262C08C41382357515FED19EC4B41 + 7F2DDA33B57C0295157E1516908B794FE032986AB258869C328F22F0AA7E2D70 + A879D81688646FE10765046D15D8F6D193AE08FAB64FECCAE0DA8E0C6F6DC9A1 + 0BA7E4F4CB4FCAA7BEE3ABE577FFD93F211FFF96AF95AD773C25B357AFC8F0A9 + 0B3279FA861C3EFF848C6FDF90F6CAB9106847E07851FBB0A06D1481EA5D198A + C1E8BD82D27540A03BADE7C9601F0867E0DDDBA2AEF9887E10480FABB0006ED3 + 19B6D304213CBC74C656CE619B4A7CABB0BD74D608186C55891589B367EFCBF1 + CBCFC8E4A9DB02C27274F542241DCE6A1B67F45C803C08655B85B702BDD655D0 + 863A0FAC300631C4D5BA4662C0EF2ADA074153AE8603593314EB992FBD0624B1 + 487E8EAE5F91A19580E13590B020BC2E1B1905D2696C3310C02002F12D5B9098 + D3A7AFC9E5777F585EFED4A7E5E2CB1F90F19D0B32B87E326CFBBDA96D6CAA7F + AD872D75B1F213C4AB6DAFBBA1EDA8FF404805320F2B7B03C986349258E8C7DA + 5503CE6520AED076F033A47A127AC15FF045C08EAB6BABD0FE6CA29D40C2013C + CE40A085734290FCE52A67928120800F9D0E24A211D78E701CDBC46AD740B2E1 + 5C91001EC1F950041238EAEDBCE871DAAA6185FA1DC1F7824FDDB455BB5317B0 + 0DF4F372E4C6BBE4D8EDF7CAB1BBEF97A3F7DE2F0BCFBE214B2F7E5C0E3FF901 + 99B9F79A4CDF7D4D0EDF7F5D75AFC9ECCD5764E6CA3B65FAE2B3327DFE9E4C9E + BDA3D7EB964C9FBA2ED358C1ADD703C4E9C4F635FB26F0D4296DE7CC936AFBB4 + B537AEED0EEFDC94A1B812780CA4FE0E08E33BDAC7DBDA5F3DE60DEDA741FB4E + E2D58072793E005B71BB03925BEBE9F10D6AFB58013D5A9C8B8BAABFA4E035C2 + B9E0F90B087F16806DA883F3883F1C84738A731C561A03D86E1AF78091C2E893 + 5E134F04FBBE51677AFC0E704FE11A6B9BF011FE1CA1BFA3782FF01E08BF21DC + 2FD5FB684CEF6BA480AD4E4DEE2DFEEE980FDFE20EBFDD910DB483F602F96BB2 + F573DA66D87EDDFCE37C69B9B5A2CFA0F5B3261FC3FDAEBF09F689043C564CB6 + F5998274103AF4239E471C23CA26C3BD8EDFA69E1B5B351C7F9B20E5C777AFE9 + 6F5ECF8DB61B7ED3B0C733449F278AE135ED23761430F257FB8C6351996DFF0E + F215242A9E45782E699ECF2A238FF1CC02C98DE79982B62090B17AD8765238A1 + EF02D4B17AA16EA813FE404390D0F5846F41E23A195090C8899EA09DE921435E + 9192C128C397EF83AF6BF9684BA04CF87795BDA7409882D4D5F250FC6313FE1C + 657F90C23B5351DD2103846C407B11C03B3ABCA707B54EF85354D91EC1F69052 + 86BEF05DE901E277587D0E2F6CCAD0F175193CBEA67D5CD3BA3AAE5059737E53 + 065436704CC738F853D7DC860CCD8104C6CE26C88755CB6D10C6F33A06519B01 + F58315BE4600A31D3D4E237FE7776DABE7B0DDB3DE1FAB97657059EFD345BD27 + 17F5FE5DD0DFC6BC9ED739BD0E7367757C725AC7327A8C8EF025814B78390859 + 8F40F096B628C3B679784BC787DAFF637AEC73DAB723DBD298D5B1D111B5531B + 1B33636CACE366F821E10B90040E5B4387715F0ED5F1A1D6852FD385720A1BA3 + 630CAAE34EFE4110798E61990F63FB724CCC712B11C85C5776F52BBE21D73906 + 77D8B171B60263F292840D65FE71D3C38FE739A6AFAC1A7676368F20411BE16D + 80749EC03C8959A40D9DA310268F7EBD7FE43DBCBE2F12C18648E07AF23727E3 + 8A604F0CFBB2D9C12FCA3A5F23302FE4B780317F6C2838070D73D6385FB5F9E5 + 31698CEBDC6F2FC08F23803DCA7935CAE57C39B50D73ED720E6EC07C1A7FCA8E + F36B22F42BF48F3A9FEFC070EFF186607B28912145DC03F23C7284AF27912BFA + 84FC45BCA5680365EC6AA66949EE76C2EAC1C64867605AF311DE3692CD85BDD7 + 018C2745F4D78071411FA3027A8DB3A13E90EA29275219ED52041DFCA19CB7DB + CB8707FA44FB1439FB47816ABB498C5861E70E768885A38C3C62E2FD21369ED6 + 795C50C4ED7D5CBF9B5D469745F4C57A1E25F90B68BE5F539CA702E3B638CDC0 + 3AC8434E68B9A172C256FB3641F8A1FFAA4FB9A3FE51C588F6A9E49752F4834C + 54BDC16475DC48E2FB314449A0E6F5291E3DD786F35E7FEEEBAF0BE420E4870D + 9D9C63C0807DBF79548E1C5F95AB379E90B317AECBD4217DA7626B6FAD37D01A + 33F46B19E8F0415E33C2F3A0BD025C6937A2F73F0302580FCEA0F9F404E60056 + 3FFDB7450A5CF81A02F83F0BBC5924301109D95A623795A35C67BB175C5D3EA8 + B32F83478830C0E80DB9FA7B21E7231CA71F1074223788D80B1844F8011B6548 + F7E3D7885DFB2794E69D1FFA62B95B7F699FD6EB865EEA40DEEBC034D5D721AD + 9743AE1ED0D18F48847BA43E90B7F3C67B5FF3F0837F6DDA401D3E75C2107CCF + AA0FD525E803094C029824EF5E48B654F644AA971F140FEACBF7A70E158237A3 + 0B2956FDAA3F3FE174F093534E503199E444330574F867B54DACFD247B38A494 + 9B2E4ED40DA3C74B441927EA0F13F059B4893662D0A00C2A2C1A40DC86ED9997 + A501721724AEA6CDA965DBAEB91FC4F1909E4305C8E541D8C62DA6591729EC49 + BADA966FC0F44A20792D5DB1C015025D336B17651681E4F9D3D28F6FE5AA0D48 + 60D46F8258451FDC766F163C519941FD5A006A46DB4280ECD09A6D236D751561 + F57049000381E42D0341F0C7E00EBE054C8074EE9F9ED7BE877FE9F7215883B6 + D18F43680F44700064D0D98A05048D6641042320863CCB6BAA2F836069708E40 + 708E4134947D1D93A9BE0D323506F26C85718491B916DC0B813B0403531B0364 + B1BED947F297E42CC95F4F00334F72D7C3CB9907918BD4563BAB7F007A4FFC7A + 32D8ECE7AA20013CBC8C80FC49692D6F0AB6809EBD784676DE71479EF9DCD7E5 + 5D5FFAD972F1F597E5D0D5F33276EEACCC5EBE254F7EECB7C8FBBFF29BE5C23B + DF3052A56D2B5C1174468019416C12C0D802F364850466709CE514D47B3B06B3 + 892290EDFCF83CCB1D7A05C8DF31F537B6784A46170246E6433F87350FE217C0 + 3695382E10DCF83E6163F18CADB6BCF3FE4FC987BFEA3BE4C66B9F27F8BEE810 + 5632454297042F80B27D7373F582A505591B89DB1483161C57AC9D9536B67006 + 591249A4A148CE94C056B5E8574020A0403C2105418555AED76474FD860CAF5E + 535C312228905D816C02E936B60DF22D607CE7B211C1F3579F948DFB2F697A5F + C676B4AD4DBDB65B7AFED64F497B43DBDA00510722168423883920906D5CA518 + 562582C803890652498F5BFB358C15A6EBE897F6692DAC2CF5641D577492041E + 0681B509F2176D9604B04F6DE5AD1E8B91B024E954366E045A24F140F4810034 + 6256CF8DD6C34A5310C0385724862777AECACCE95BF6EDD8706CA14F201503E1 + 89ED93D18FF2BC7902D8FAA4758C40DDC0F6C3584D8BEDB4AF1B316B04F0E93B + 3271FE2999BCF4AC1CBAFE92CCDE7A450EDD7EAF1CBEFF015978EEA3B2F4E267 + CBD1A73E2887EEBE2E53B7DF23B3775477F71599BEF5A24C5E7D56A62E3D2D33 + E79F949933771537E5D0E91B328315DCBBE15CA08FF8730256FE4E9E7952A6CE + 3D2553DADEC4D9FB3272F2960C81DCDED2E3D06B0D027764F396F6FB969E0F4D + B76EE8FDA1D0FE63CBEBB0F575C8733534097FDC5F63EA6B74F7BA0C2B866C25 + 30FA50BDA76CA5B1A624FA79ED00DC47E1BA067B90BF76AF6B8A3248CAA953D7 + 0DE5F5461D4DB52FA13F25E96BF74CBC5E294C8E7B49AF337F37B827F96700A4 + D0E13714EE8740DCF29EF208E46FA80F5B7F0F117E65306C4900835C1DD5DFC5 + 38568B83F8D67C20C02FE8EF5E9F3FB1DDF0DB471BEA0FFEF59AF0F800238355 + C795C0013E8F3A3C76FC410364AF9E6B3D8F208047F59A0C6DA9FD267ED301B6 + B5BBA5F8BD6ADF410227CF22E659065A7A5C5CD9EC53E66183738014B2E609BC + 6BF499AEBEF16CA01F00F6205AED0F3B0A10B9E90A6023716399769E302EFE28 + 13896000C42DF446E47AA83D495D90BF58D94C7F00895FFA317BB4E37D44981F + 05DF392040F98E35E256DFB1062371D1DF00FCF9AAA540DAC6A71896F4DDA618 + 5CD2779622ECA4B1AD3E41EE06DF39F09DE709602F27F01EB46FF51BA1BB66B0 + FCA28E1B1640E8EAB80298D731C7FCA6EDEE010218E4EFA07DEA21C8F067AFD6 + 09ADB3847A184F6C4A03A9FA6F69BB4DED4343DFB37877B697F17ED267FB8ADE + BFFA3E1A5ED2FC82CAE6F43ACDEB799DD3F37A5CCFDD31EDB382043049DC94D8 + 2548CE7A02980431EBB78FEEA80EB628877C6316B6A13EC68EE51F2975ECA87E + 500F63686E076DFEE338AE8E04F66341E4C318337CABB7A9ED81F4E5778101F8 + 25014C78F2B620846DFC1DC6B41CBB7AF8312CD017C7EC1522D8D9A584B1DFC9 + A7848EAB41043B921663798EE701E852021865E449BAD2D6D70368CBBA94211D + 88642DE64E0D470003369F8ABE01FA458A327CE093354347F49E56A04CDB3E90 + B6716E96A29CABE99C04F3B83867A43E9DDB15366E6E697A2D57E6A3C9FCD4C3 + AF20AE83FD09D9E6C1981F63BE9C2173E31FA8ABF366CEAFA19F4D10EAF92DA2 + EBE6DF94D306755260CEDF6B2CA1CE8624704AECA6A823800B9D23802D7666B6 + 69FF108BA9DA7520B613762B9B567B0710A36DF51B6D6A096002F1A6187342EC + 89D84FFC8E48ED3C521BEF3F07D6A17D5A2F957BF462E341FB1439DB14B97ABD + A07B7DC486437C18BB9A36D4AE20BC556FDFB16D4C4A130B48CC0FFC857A8F25 + B04A99A8D3E7E439C0367204052F90944BE839C12743C92578447BF8EB1F08C4 + 6F5FDF584557C0CA81F4CC9299C633ED875B0A6DD423D74617A4B68F188F1F01 + BC07787DD26B641CA2E320733C23D03FA4C7302A2BEB27E58597DE234F3EF392 + 2C9CD071125600F70D19F9DB688F5788E0AC9F3D48E06E3A4F00F742F6E6F078 + 6F016DE4EFA00269E6E4F913C8FC5EACBF5D70A47A517237C07F0E281E1AE583 + F091233E2489CAC332A2F84E306DEBF2444E1651F8CCBD101E36E2CBA89797EA + 83BE7859BFE207C76C7A94BBC30F4472E86560B61F7F01EC6FA76EAF419EB705 + 68BF571F016FDBCD3E47FEE6EC21836D6A9FCAD27A39F8FA5D912180C300AEF4 + 83738BC15ED10F45B1650F7C403EAC132DE8B09AB81151218031E0D789C47E60 + 931C3F11E982CCF777F7423999CAEBF702EB77433702B8C46C8F04B0FA3019D2 + 004E3CB9F515FF8D9C9D442793EB0AF90B7082DE31494FFC3C20720470082820 + 5810885FA4F65D5E90A55AEEC396D22A6B636BE6F1253DC645993CB62D4797CF + CAE4D14D19549BD6D88280000602597CC27C907805790CBF61CB64F5A3B23E04 + B140D0CEAECBFCC99BF2DAE7FF36F9E4577D9B5C7EF635C1F7DBA0830DB683B6 + 2DA161AB7D28832FE1DBC306F5EBC95F03EC15E8435BE52084D1174F02633B68 + 067500F80EC0F7BB028C00D6B44F5312BFE50AE092F4F53282243089E0400207 + 42D707C67C60CE07E728277C9D16827458511B4172D608DA2803D96A8139B479 + B8EA7F10D72E828140236C1D21EB7D0294997FC850CEA042EEC63C0961D623D9 + 6B762AEB460003085023586C01EFA52D195ADF92E9B3A764E5DE35D97EFE8EAC + 3F7D4B8EDFBC24E3674FCAC8C9D3B27AFF05F9F2DFFB7DF2DD7FFE6FCB07BEE2 + 5B65F6DC5D69CC9F0CC17704ACB1BA090160069B63103A0D46A7F26EB0E0BA0B + 6893004EE1EB10AC87554FD8767364715746B55C00A42FFA8FAD3A6D15F01919 + 5CD0F6341D0569B98ED5BF17A45F6520CC5EF8C4A7E5CBBEF34FC8339FFDE546 + EAF915BD483D2023F9CB3C08E1B06AB8B4A77ED8112500CA234634A50804707B + 35C0BE6DAA7212C1D83ED648AD8D9BB6DA36AC6A0451859585817802391B48E0 + B0F52EBEFB3BA9CF0B10756DF503A26D62E78AD98DEFAABDDA0E82D8C20A5190 + A35B37023603401EA22E08E0B02A51FB096269FDACF60F448FF66BEDB28CAE5F + D5FE617BE1406491C823814702D856A06EAA8DF6178472F8DE6E200103294C5C + B4BED90AE0CDB0AA7202C7A3E0CACA3110A39174240119C85C6DD7F94379EAD4 + 4D990681BA8363C26AD8402E8EDBF9B95EAC9606E91BCE5D4960A23EEA8CC56D + 94C3F774B5CEC96B8A2B3271FA86E2B611B253179F91992BCFCBECF51765E6C6 + BBE4F0DDF7C989E7DE90D5173F2E734F7FC888DF995BEF9643B75F91D9DBEF96 + 29B59BBCF29CCC5C7A56662F3C2D87CEDC93E9D320806FCBCCA93BA16FE8E3EE + 4D3D863B3271EABEB6F5844C9C79D2307EFA4919DEBD2DC35B81E8852D08DC61 + 10D57AFDF0DD5FEBEF16B694D6FEEB31071258AF114873235D79AEF41C2604F0 + A0FE16466CFBEB704EAAE706A9D6B7738FEDA3719E404E067F3C7F61F5EF79B5 + 01991E56A683FCC5B1D9B9359FE11A1AA11CEF1B3BE7B8276319D7CC978BEBE8 + 88DAF08704BD6F5406F037456298E46F588DABFD730430C0DF21CF8D07FDC126 + D8A3BDF0DBB5D5BF7ABF1604B01EB311AE1BE177C2B602710AFB78EEB0023A12 + BA00FE1C1156FC96E46FF8BD695DFDAD61DBEB401E87DF997D4F19BEED1AA80F + 3D9758CD8FDF16486710EF1E23DA9711238041E202E5B32925787DBE037C9625 + 7541FE0E82FCD5B6786E0202E10BA295642EBFC34C1961642B9EEFF1FD851424 + 6DFADD66DAD6813E90A72DFD51E6DB24BCDC837AF31B495BFBA63CB7D5B62DFE + 4B8014B6EFCA1BF4FDB9ACEFAB153D2694B56E2080F5FDA679EC52C1F75A0ABE + 5B7B79C7DAF6CFC7756C0032176430DEDB8AA6E61B5C918C9D3C747CC02D9F81 + F06D62AC6AD6F1C5A2DA2DABCD8AB605F25ADB6CAA8F861E7343DB682DE15AE3 + 9AEBFD19B7FF07013CB4ACF7DE12A0F7EA82DE03F37ADEE6F47CDAD6CFEA4BD1 + 8D0066D9232580ABB648CB55C0FD333ACE525B6C058D744ADFBDAB179E90C533 + 77644CDFE7F419085AFCD94FC7881817EA78CA3EAD813FDD61AC18C77DE97892 + 323F060DE367D86B1F238C60569B7E1DC71A498B3165246D41FEDA5853532371 + 2153DB82B4D5BC4741E846C2977E08FBC3A2B3F575F204B042E52469393FE078 + 9E29095F12B1BE0CD21579963D20A35F8F4ADD0CF95B10B58A3ECCAFE0C7B589 + F647F5BE9ED47B7002BF196C7B1DFD997D24700B123799B37939655E8ED4DBE4 + D0CBFCB22478F3FA129E000EE46F4039870E2849DB306F461E7ACCAF672B605D + 3FEF7E109004EE1575F186DC4AE094E4252CBE90C88A7A9EDCAD8BA30C463B0F + D601E86B58F3005701576C00B6A940FCC8C3DB022A438CE9A008B1ABBC0E600C + 2C95A731421F27F4F6CCA73639F4629303EBEDA77E5AA757F45ADF62DF03587D + 3A254DBD9620831127B7AD8B714E8B782CECABF1E5B705F643FE2A8C07487801 + 5BE95BE4AB002F5259DD4BA07EF453700F84F9864F3DA7E4558C0006B997E160 + F684FA34B8B2F925623F0A785BC54058C0588B47C96BBD1D78B36EFD4BFBCFB2 + 6144CF5FE01FFB80946F2440CA36466572E6B8AC6F9D9195B55D999C3C2A4D7E + DBB96E0530F9CA0C0ADE93ED77D111FFD913C0FD03839AD703D4B29D0C7F113A + 4E2ACAF1C2FA0BEC91CAEBECDECE281E1AB907C923427C4812D507AEB303DC43 + B5ABAC0615DF9917C2A34049723EDA97AA6FA7682B1E736E30D02BFC002537C8 + A0AC2EED86B08D7348414CD60DEC72BEC2F6D18F0EEC4B0E397B60BFB639791D + 527B92BE1559C506D70028E57E805E7CEB4507F4033A29B02DA41DF1FB600430 + 271F3A61C1EAE142EE740F80722295D7EF053F193B28400C37EC1B43EA2F217E + 8972E27A4CCB611299E603F1EBE4233AA14D26C7F8B735D262321D27E70F3A41 + EFF6AFED1CEA570007E2D648DAF85D6092B886A980BE9139694FADC8C6F97B72 + E3C95764F3EC1D193BB4A6D74427F2C30BD28CAB84E9CBC860AD4FBFD8E61924 + 6DBFDA581E2B8CB5FEC69567E49BFED00FC9F7FF85BF25EFFD9CAFB415C1207F + 11FCC12A0010BC08F418893BB31402255899AC364610234064412304B1D6A531 + B3663AF4C510C95F800430B7A66640A61AE821F91B83422AEBD394E4AF277B91 + 2F09601C4F0992C00408606E05CDC01BC0E01B4072D613C06900AFA80759247B + 0112B52918A0F37629D1DB4D97C26C91A20C9B0812B829A0F3B61D3690457958 + 995BFDEE6E8163BBD29A4320FA940CAFEFC8C4CE493976F592CC5DBF28B397CE + CAD4B9D3327EFAA48CEC9C96F96B4FC9FBBFFC5BE42B7ECF0FC8F31FFFAD72F8 + DC7D692E9C32C297AB931008360218F918702610A8F6E55EC000774E07E217DF + 1C449BB4F3E0CA2C0BE6AB7D85FC05B4EFF82E637B11416CB58B04F0E022C804 + 1030176570F592B49641D45C96CDFBAFC88DF77D8EACDD7B9718C90362771924 + 03088A4E780218C4EFD8DA4503CA29016C2447244A0090179485557941877641 + F692B00AC42F02EC81680279852D64C736C24ADB406A05F9F85695002EC934AC + 22BD2DD3A7EE1A664EDFB36FAA4287EF0383FC1EDEBC2A43D82A78F3A6617843 + B1AE794DC7B600108A20A5B43D47000320A6AD7F20A248F26E04628B445D20AA + 0202D105920B646B2004530238E4037948C211C08A53AE3E0D3A94412096F549 + 84191916FD218FD591205141DC924CC40A4AE82654174863F42910C06CC7DA80 + 5EEB85EFCF820456E8790B04B0FA3DA5BA53B765F2DC133279F11999BEF4AC4C + 5F795E66AE6125F0ABB2F8EC8765EDA5CF96F9A73F28B3775E91E99B2FCBF42D + 85A693D75E94A9CBCFCBA14BCFC8A1F34FC9F4E9FB7A7D6ECBCCEE1D99397947 + AFD94DEDF70D9361FBE7C9334FC8C4A9A7F577FB9461ECD453327AF29E8CE1DB + C0DBB7F558F00DDF6B7A1C0124AC03400007F217C09F0940A063F5A89D2B10BA + 203A41ACAA8FA1EDAB46008FEEE2DBC73C1F0181DC0DE71BF712895910FC762F + C4EB8BF3C9ED9F79FDB07D34CE7938EF209CE93B5C0F92BBF62DE2E817294179 + A18BE42C5683872D9603C28ADDF07BC26F6B4CEFBB70DC7A7D2348CC92100E2B + 7B4B02D8CE118E2FB6110860D40965D813F4017F26D3DF09886110C09085D5EB + F0AD7A90C7768EC23D6AC46FFC73442883FCC5EF8C386F7FB608AB87E19FF583 + 8F701D02C2790CBF21238241461B50B74A00DBF3487DE339E209DEF03C823D9E + 43E5332AD5791BE86C3BE94467DF2E568CE2B91BC9DB9400F6C42B89D6826C55 + 78D2B717A4FE98D21F65F48932516C63AD683B7B7BF73005691B09E0612D0F33 + 9FC093C0461247B218DB629B5F05DF797CB7A520F1DBED0F5100885D6CD38C77 + FFE022B79556DD097CAF77DB08E0966D3BAD809FE3F80EEF968E1F41FE064218 + DF2C6EE2BBC1DADF36FA89FAEADBBEF7ABEF52ACF20EDFD3C6BD88F755208187 + 962FA8ADDE474B5AD67470416DE6D4F638485FED47444AFE0224750192BD3978 + 3B003292BF00C8E021F58FB11EDAB9F0E4ABF2B12FFB0679C787BEC04860F8E6 + 4A60D435A256C7BA038757C378EA4875EC18C68D9DE3CE6A1E401DF519011977 + 92E13811E42DC6947E4C68846E246AD9468A82CC754432FEBC692B81ED4F9CD5 + BAB42FEA290AE2D78DE9C34AE072B52EC1313CF205611BCB36B657991FFFD3DE + 83BE08D6F168D411C098E34DE81C0B75310F507FE803FC8C1CDB90711DEB8EE9 + FDDDC69F44A3CE10E766563F3367F3F0ED156DD6D87A3F9887E6E6891EBD13C0 + 40980B97246E984387EF0097C42F51FE791A65D4C3BC9B754B023857977370CE + C373F2CAEADF58CEC502EA90C61BF867F2208F846A8427772D1FD36297B1A827 + D2FAF5B1954E5B5BC9CB5810E524804902B763BC88A431EDB46E96FCF5654DFB + 132006E591EAF78387E103B03862469EA2573B0F1FABECA57E6ADF2B7AF68538 + B062A03969C46F43654DBDC64DBD9E9059AC38C691D3D8F2E38432AE5945CE76 + 2F145C40851B505F2957A0E5002D73EB67077EFFD78860AF833CFA4C09E0C0B9 + 80437A501E692FEE836D45D84A556DB31BBCFDC3845B691CB6AE46FE006D3A3F + 59E4EAF482BDFA92EA5936380298BC62C23792D405D16B5B45430E8EB20F5CE5 + B0FE26C7A439A4BFD136FCBBBA054F59A2E03A93367DD9EB531C94F825DE7E04 + 30509C50951B20872C5ED05EE02F7A4EFF76863D30F090F20FBD470C7D5002FE + 819B43B6EE3E51F19779213C12A02DFC332987C22623DB27B22F44F8C3B19B0D + 64074731B0B07317E578A9311FD1751092A0EC6FE7202645477DBC5491666CF7 + 82F743991F5072805907DA11DE576A57A7A3FE20B0FAC98ADF2AD01E1164C520 + 1D69F14F502D9B5EFDB5F08F409D70B86FFF92D0ED650BE8C2C6FF1315790024 + 3289E4BA7FAAEE03E904AB17787BD6EF065F37AD13C8DF63D2D4492748604FFA + 7A60E25A4E4E0338C90C134EA425ECDB491902D86F0B9D12C07E526E182FF3DC + A62B47F4EE6F621E50B70218410BDB4A795C5392B58A662450072643A0A41F04 + F0CCAAAC9FBF27D79E78B76C9DBD23A3B3EB7A7CEA7F18DF033E61F658750BF2 + 17AB878D50A66FA45361E56FB1BA57FDCD9FBA251FFCA2AF952FF9AF3E2337DE + F1410B7621D8C3E094057EAC1F4BD29C469046A16583D64750A8156D8D044610 + 6C1AC19650C782520814C560D180F601801EDBC7C1DF00033E332ED083800C02 + 3C9A92F025E9EBF381004619ED235055DA028100C6F1683F23010CF80017F238 + 5E10BF5C911B8EA75CC10114013B407D21084790C0F54849DC9C0D415D41C846 + A476C47E08E0AE7690518ED53C8A1C09DC9A3B29438BE76468E98C0C2EEFC8D0 + DAB64C9C3A2953677665FC64F82EF0D0D6B60C6D9E34B260EEF273B27CF36599 + 3DFBA48C6F5D97A1958B020213AB9D40C672F56FF86E61B91209416A10B2D86A + 1379CABB017E9802A887FA84B761209CA08C01FE11B48DB2CAB115F4A89647B1 + 7D355655A9BCADA97DC317A9FA6CCD6B9B7A4E405481B004090CE01B9C6195EA + 2519D5FCA8CA40F47AB21748652CFB6DA1BD0E04C8D04A207F89821C01911A65 + E92A46924D01A18C558746A06A3F511F2B0D41FE4EE0BBACB6C23290A68184C5 + AA4C909E00BE2B7B57A64FDF939933F7E4903E8B66CEDC9209AC0EDEBEA1D79F + E4EF0D195ABF29439A220F59580D0C9215A45F209949048F8060B2958A58218C + F37959657AFE409869FB845FE588BE05D28AA46E2071498691D0C27194A42180 + 328842E8030149D210754088D93752A31CC4A46D2B0C02CF56A8968462E893EA + B6B145762023B13215ED04BF680F65ED9B02A42A494783119720231527D5EED4 + 0D99387B5726CE3F69AB80A7F4B7347DF54599B9FE6E997FF203B2F68E8FCAC2 + D31FB0ADA1A740FA1678874C5D51DB0B4F19B93B79F2BEF6E3AE4CED8004BE25 + 337A7D40D84F9D0481FF844C9D7D5A7FBFCFA86D24814F015AE7E43DBD076ECB + E44E208047F5F73BB279DDD2F12D5CBF4896C655BAB83E207F717C3C66928F23 + 386F5B57F5F9A0C071DAB9A912C0E11AC036D4E33536A2DFEE95404096C07D13 + 80F33C79F2864CE939E3EAEB707DD197F29EC1F50ADF33C6319508ABB8F5F878 + 2FE97D67BF591C13B6C12E56226BBB7A3F7490B9B8BE0ADB32DDC85890B5FAFB + 45DEC850A4A19E91BC3846DC57B8C75703F1356AEDE277A975F5FE2F56E1E3DE + D5BC11BE684765D80E192B6321338258ED719FE27783DF4A207BC3B3C8E45A2E + 09607D3618F07B0B6D05687FD406BF8370EEF49CE39EC73535625DCF61410AAB + 4F3DD6B09DBC3E678C003EA3CF3B90C09AD7E7088E1B3A12C094911C66CA6715 + F21E94B14E618FE79E3EA30B8040C5F35AE55CCD5B3CC7359F92BCDEDECBBC8D + 97791FA93FFA2228F7BA8A1DDE350A7BD74017F548B902D808567B1796E50220 + 7949FE8210D677D210DE4BD17F685BFB67F2EAFBCD83C42FC1776605FA4E6B62 + 0C10F5C197D657E04F505C6D1CC60558F91B08DF61B5C7778CB17D34EAE1DD48 + B2B76575C33BD36448D52F0860FC068C04D6DF0AFE60D15A38A375F5BC2CE83D + A4F93676DFC037798F61C52FA06D28521297C018D6AFF0AD05C63D9A82C0855F + 6CFD3C7048C77EF6DD5FADAF727C0A052B8DAF3EF7BA7CE2D3DF5C10C02067A1 + 0301DCC41F063166C41F20311E3CA2E3C623613C598E1B018C39E3981A634895 + 734C198032C69A18876A1F34C51F2A49FE72ECC7F1A61F1F8278B695BDEA9B7E + 3D38D6B5B61D011C56FE86FE10B0C19F3F53605CEF7705224004A7E42F4012D7 + 93ABC85367046E2C57EAF93CED22BC1F83CA52E2D7037304FB03A82380E107A4 + 2F56FEFAD5C8057C7D374F64DEEBD3B669EBE778851CF34B859501B5C9CD1589 + FD12C061CE4CE216E05C597511246B07E27786C3F783912FE7E239F29773703F + 0F2FE7E27E5E1EF229F96B48E202297C9C2027A33C25748992689DB1B48E004E + C1B6D85E5946CC25C47B825DE9DF1600B0EC0860239F07035837D45119746DCD + 238ED405FD31EE44748B7DF91819D31469FD14B93A1E6CC383BA5E7DF46243D4 + B5D5CDE6A0E8E6D7CBB1D0A385EB88B8701F489C7169EB3DD1C0F56D4C5A3CD1 + 5601BBB8F2E3866A6CB344CEB617784EA0E00612BEC0CAE029A29C9C45517672 + 4F0AF3BBBFC5EAE0022465C1215579A4FD6E899C25802B7DF3BEC05B81FF8AFC + 551D2AFE1F1EFC4AE3818312B5DA3FEF27876CBD5EB0D7F1E7F4463A0792D673 + 928157CC6340AFC300F291970C9CE588DE6BA3FA5B9CD0E70C08F2401817F5A2 + 6DA54EA5BD4ED9A3C45B4A00EF89480097246F827E90C381750F329CE0787173 + 17F9BF14140F8DF221F866C03F7C3DBCAEB0C7C3ADF280ABA2C33E01F5669379 + 210085FEA0842CC95CB69BF697FA5CDD03A2FB0B11B2EE839254D715766E14B8 + 4FEC3C96BAD45FAD7F9C63953FC8A02A45AE4E0EB93ABE1F1C107A509783F795 + D6EBA6F33EF68BE23B1E199D47D196A2189C63608F3C6C9AAA77C46F4AFE56C8 + 5D972F646EE2E1D10FB4354F3F46002B3021496CF78BEA44AA7EA2F5A0A0DFD4 + 773D01ACE7A7809F845627A2E52433D4C5C497242DC9DE3A60A2DCAF136A4EE4 + 531413F784FC655D8213F3BD26E785DDA8FAD732FD333810B0605B2993FC2511 + 0C22172B806D1BE88913614BE7A9659938BE635B404F1DDD92C1C913D2545B90 + BF032358057C42060FAD19516CBE14DC521A811A23812702098CE015023D58E5 + 707CE7A6CC9FBC2D1358693817026B08F8201884E011EAA18F3EE84358F047DB + 839D05900E87EDE82C05C1EB024A0C400145B049010237DDCA1965C093B90457 + F5F622633E10C0207411104350AEB35F38064F00A3EC09600F04E0000BF8C580 + 9C0FD2F9E01CF2D8169032DAA6753C2C3819C1BA446E0530E1E57536404A04DB + 8A9D0C01DC38AEF65A1E5E3A2FC327CEAB0C2B78B66568F5A40CAD6BBAAEFD53 + 3457B6A5B5BA2B836BA7C5B622DDBC21EDE5AB32B8ACF995AB32BC7245464102 + 23E80BD2D7520489439B24620104B5F19DC5101C0F721FC44E6141EB0894599F + 607DFAF7F6AC033909630BAA475BE4590E04B0B609DBA53346EA36E7C3369620 + 5FB07AA9B5744E9A27CE4AFFFC2923826D95A4CAC7562EE879D073E8085D92BA + 247BA9F75B437B94F502B1D28D4021E944B2094454B007391310EC03014492C9 + C85F4558F90B420804DF1501F13BB97B578F1324EE2D9939F584CC9E7B4A0E5F + 785A8E5D7E568E5C7C42A6CFDC95899377B5DE1DB195BFD85E7A53CBDB77646C + EB96B68195C020DC401E8204D4768D04C64AD94038231FB66A0EAB6A410292C4 + 23615A90BF6E85688E00E60AC640BE425745683FD8A580CF213D27C14FD9760E + 462C46C2912B804146866D894399F200CD3B123210CCF17BB8BB97F51C5E9789 + 33B765F2DC7D99D4F33B8555C0975F90E92B2FC9B13BAFCACAB31F92C527DF2B + B3375E92A92BCFDB0AE1696CFD7CF57999D26B3175FE69993EFDA46DF13CA6D7 + 6312AB7F7703700DA776B172FB49BD5E6A0B02F8F4D36AFB948CAB6C5CAFAB11 + C0F8DEEF26FA897EDF96F15D851E27BE551C8E1BC781F3CA63AA9E635C5F9CC7 + 11835E27F5836DA0C77661EBC9F8E0A3B8667876C4EB6BD73DB613FCE13C05BB + 40629ED363BB6ADB5C038104C68A60FA0C2BC6E18B646F9AF21AC206A4E92056 + 201A79AAC7E0AE2D8E05692099B58EF6977DC63D0B42D5C8D50DFD3DE2CF0C51 + C6DF5FF13B8C44B2E5F5B787DF60497442AEC7A9BF5B7E5FD9BEAB8CDF318871 + 90AFEA13407E0CE719FD8A6474F089DF347ECFF8EDE3B74DDF5A4FCFD9C8863E + ABB49FE3EA7F12DB901BE11DEC41E24FE9359A544CE8B11A1C01CC631EC5F9B7 + D5D0E1D983E70AB7780EED575700F3189932CF6718EB002C7B3B6FEFB79226B9 + 8A67B6BD2F4026E2798DE7B89653D09E7AE48D948DB214697DC0EB7D3DEF1B29 + 6CC76CA5B2D6834DD49B8E75A38E242FC9DFF05E8BEFBCF8DE29DA407FED0F48 + D8821AFD479B80FAD2778FB513DF67C14FF5FD48D297EF3CA49E10363DFED404 + 921775F16D5E10B2D8C103EF344DED1DB8A0BA39B53DAEBE40B8620570F41BFE + 1CA5FD537BECFCD1D07E36B59F4D4DCD0FF45AC6BB12BF37EEF6807C4BDF9DCD + F9D3D29853BBB9E0A38531A9FA27F0991280442E57F4A21F206B0912C1D4334D + 019B116D07A432B774460A7297FAD98DCBB27CFEBE2C9C0EE3E482A085BD8E25 + 3166C3B6D7189FD978102B810B1258C76B29091BC797442088C3D814F6A14ECC + 83603E1AC689E9D8B132FE443E8EABBD7F968BF641FA62FC1B65DE1665FBAC8B + 826363A429EC4F9E1CFFEB381B842AD23A2298E42DF215B9A6BE1ECAFD87E2F8 + DDD5F380ACF033A536139D442C001DE700F667D2580F69DF84CEF7B0DD73F447 + E4085DC2CB6057676B733CCC21639D42CE3621EF610EB997BE82B1E33AFF3B6A + 00A11B76C78A73DE642E4B041DE6D325090C0422B94AF4E6407D3927AFEA2A72 + 1703E80575710990A929814B909CB5D4D975AB1350B6535D690C54EB176DB876 + 0CC50A60272360EBE24705411C65055AD3C146E1635D29FA5D8C8A60AC8CF1B2 + 14BDF820521BEFBB5B1B75E8A5BD9C1CC8B5E7FBD10B727ED2328F399513D8E6 + 198B66DA2387657C7A418646F45E411CB68955876AD3D67A8C4D3FE4F8F4C342 + 1AE72672B6BDC0F30116F31F4838021FB74FE1B98834AF69D687D9695AD9C6F9 + A088ED75C0B7F930DA3938F64F683F24A47C5E8ABD6CBDDED9F50FB8D5CB94F5 + 039915C01E24728DD88DE4AECA6DDB6710C22847C2D7C85F676F887EBAB65103 + D6E958287B40D8D6D139C5E38240FC8615C0397D208887F464E889B6938493EC + 2E6A82B7EC267EB3917D803C5AF8876D8A9C9D97E5B0971DF5669379210085FE + 415E82A8CB76F96288ED67ED1F10D91762F1220F0382BA810190DAF46467E708 + ED57EBE4065EB97C6ABB17583F879CFD838203AA9C2E07DA7BF4A2DB0B39DBBD + 7C505FF9FE2F529D0C1036886F2972DB3E7B2444700543D5C98D9F787490C036 + 11E1A424DA54262ABD216D2795E7C04911F3BEEC51E72B571FE46FB715C07E22 + CAEDAA3A2695986846B216E4EF4108E0DC04DEE4F8A73627D7119C907B74B369 + 8E2F1830D9A7DF1C014CD2D7B66D06D91A495F00242E0960EA1ADA4EFFA01EAB + 1EEFE0D4B26255CF4FA80FBB823C8E8432C9DA40E462D5ECB2FCFFD9FBD3584B + 926DBF0FABAA33CF73CDF33C9C3A539D9AE7B1ABABBB6FF79DDEBB6FE01BF9DE + 23F9F4488A04694A14699BA6C847119465CA16654AB2414382044830640B86E0 + D9A60DDB9041C3860DD81F6418106CC082E10FF60703863F84D76F45FC33D78E + 937B9F73AAAAFBF6BDB73FFC9191112B222373EF9D19B17E7B45F22E605F0E7A + CEF63DDFEA2F9E4FE338BC0E5FF6F2FCDE603BAEB5457D402F5BF6A39AB2E268 + 22C2C09D55C1F9544B0EA7EC74C2A984C32947F5E2B0515E4EB7C2A113DFED9B + 15D351E4634B9DDE63E6E3668718A05AB0DAE16E29D37E8C0C8EF0374AE0B6AB + 2C42DCDA46F56A09F6AA6E5417CC652B301CA57207BC7DEA7601E008811D009F + DE48632737D3A1A338946FA4897337D3C8596BFFDC4507C16317AF9B56D318CB + 810207CE13D5F620CD5E7E94A6CFDF4DB317EFA5D90BDB690667F8E916A44607 + 75761EB70097ADF276134E6FDF5A9B0202921CDDEE1CE7D8D5F1C88BF58802C6 + 56CEFDA61EF6B465229FC85E07B32641557F97A169029054B62CE7ACF7FA3ACC + ADE06E8CF605FEEA9DC175F958C91324C940B7372D09EA641094A30D797766DB + 4FEA900724C2762BF13E5C20305056CBC512719901ECBD3471F1BED9DF4FB357 + 9FA4958D37E9C8ADB76979E3755ADE7C91566EBDB4ED738F0A9EB906047E9426 + AF000D9FA5D91B2FD2DC75CBBFFAD88E9197811600CE8A4030C34300E3E4A51C + 092A008C7AE0A029BFE7F456EE2751928254573280243DEBD03103B4782C0160 + 40A120A4609700320074F6AA2247B56D05206439688782DE46EEBF60A1CBCA28 + 6FA43AB675595ED3B71B77D21C11C00500F32E6020F0DCAD4F6C6BD7FBFE17E9 + F4AB5F4E279EFF282D11F17BFBAD036287C477DEA6B9ED3756EF755A5CB73A6B + 2FD2343077F5495AB8F1C42381E76D0B049EBDFEDC8E5322800B00CEDB1756C6 + 12D00FF232C8F639100D3C6BF566CBB2D75CAF7C9D75AE5C3794AF6B8C7E7668 + 6BB693763D266F589BF659F47EE6F99A3BE40D9F79F3395B9E3E537D3EF933E2 + B3533BF9DDCBB491F3F2E74EF4AEBE27C0DCF899E93360DFBF470514474500AC + 63EB33549E478A037AF9E3424F9F38F7FCFB8BBF432289DBFD9C974170FE1D0A + 00137DDC02E0FC3D8FD1CF3A16ED017FE788AE061213C96BF573F430E912294C + 9ABAC0E34BEB765FBE9316AE707FE65CD5C67DFB8EE4EFABBEBB9C27BF857C9E + 1CD3DAB5E3D077BF9778BF915D8320015BDD9FA4DA6637D5B6F11EE711AF761F + 169C0590726F76B8AA7B7791CA958E5B07AB1D75A4BE657DF2B1F73E95A864F6 + E917CF0F7F86D87ED3265B227A4D4DB42FCF176CCAB3A669CFA1B78957100066 + 4FF3AC58B72DF9766D4C19CEB6CFB65A11FEE663F73E63DB67231018E8BBEE02 + CA0E1F271A373F2B3DD2F884D9015A4B04F0B8FF914BC7E67CD7D28869D8FA35 + 62CFAE61DAB37AFC71608C952C0AFCE53BCF76943F549DD2B1ACBE1D2B03606B + AF40DF1E15C02B017DF903238A1038825FF6951E3D9AF7B1633F4702E77DD280 + 60B6FA53244018E0ABA59F274A3D01585FFA9AF114E32A87BFA62379FCC8B8B1 + 1EFF36E3491F8FB2651FDB2C20F3F811CECD8E6DED701C07B726815FA57D3C49 + 5BE118B534DEF5B4DA2965714C2C00DCD8967AB5F4E7CF51B319B1F1732FFCCD + FB02B502AFA4958F18EFAB8EF601C03E0F307B81DB06D056F602B10E7B4B1A31 + 9F238F79C241B305F4AA2EED1C080038AA9E076ABE16E76C6A5BC789F648F694 + C5F20345A43F2E00C6CEEC673467CEF365A27B99F7322F3ED4CC8381BECC8555 + 96E7C50E8B7D6E0DBCB579BA4920B706BACA6BE6E003F21A9579FF5E849FA09F + 2FA10BB046985BEFEF4D5DC7916FA46D5311C5F1D82D0C3639048E79950D3EA7 + 316BCBD4037EA32A1FD45EFC54836CDE47759BFD7C86D166503FFAE5EFA641C7 + 8B65BB6990BDDAABA5F3917CC5C4A1B974EED2467AFBF92FA78DEDA70E8159D6 + D82381EDF3EFF11FD7FA1640E1E8E38EEAB2DD8BE4EBEFA7C687DF4F11FA567E + FEA67EC8FF38E0372AB4DD28F6FB631F6F7FFA4500C09EBF1B00EE5201BD0E80 + 4BC430EFFF1D1EE39DDC5C33CB537B0502EFFB1826D5F98506C07AE1715453C6 + C52D1FA87FC05578FA7700F8EB537DA3D5BEE7A172638DF95E16DA88DACDAE29 + E3E6DFF5F088E0F67D1F7802AF6A43EAB2FD488A0FC3E69C9A63B2DFAA6BB0B0 + 5FD56D4A5DB6CD0024EC77E5D7DAADFCEBD2A073E992FA59CBC16B477E97BADA + 1DA4AE36BA3464837FE06F04BF4336596800B0D9789BC3B6ED078023041EB589 + 4633B1E89D7468F27380C987967F0EB65D8AF5FBA95F9D7EF9BB294ED26AC5B6 + 64D7CFB6599EA903FE12DD5BFF13B95633D96C26B6599A14F7CD0F1379294ED4 + 954643EC33012F13F166422E95361B051BD78CB5815D6997F66B674206C019F6 + 3AF09D39E590F720937AE02A90D5F2D93A289E3D958671784C9ACD38FFF43E99 + 4617CE79D4EF30709676CC4E696F8B3C87B540E0BC1D9A3FE7521EE9D1A58BA5 + 0CE7CD850C880BDC95B3470038471367712CCA59125A8E2439B710CEA2E864AA + D3D1A9E4D1051D69A9AB2EDB2E45BB46EC07E7992B38D26219DB0869056DA51A + D4D6366A8B7C3906EBB6B48F625B5204BF92A06D84B7C05C80EF10EDD27E9120 + EFF8099CA63B21706C47E0B70B008F1E5BB3FA5B69FCE4A63BA0474E5D4F874E + D8353E753E4D10F96B222A98F7428E9CB999464E6FA6E98BF7D3CC857B1E393C + 75F6569A3DBF95E62F6CA659963A2D8E7177729FE29D83D75DA4714A37CE791C + E0C54669EDC73C39B3BD6E71C4FBD6F665A772A529974DB3AF7E21DB8F4B8D0A + FE36E953AB19CE9EDFF408A731002E50D5F226CEDAF68CB505EC05EC5CD8EA01 + C112F05802F08E9FCD6A2280CD06D1E628F91758AA39039708496A3892A16E0B + 9E326002A664184C5B38E19B65A1ADCC81EF152BB7EDD885F5347ADECEFD22E0 + E84E8164771391C040C4A58D17E9E8EDD7E9F0F68BB4B2F5381DBBF7DCD2CFD2 + C2DAA334BFFA28CDDD6049E827696E15D0C8BB689F3A481400CED19CC0B20CCC + 10E010580B58F3F202EFFA09484704708EC2CC708ABA510E71818BD63E361150 + E6E352DE423D01BE5C1FF829588BEC9C0A3C8C309172DEE3EBCBE6FA79E563D3 + 0E51A9393215591D6F1BB896AF134B3263D780C30280E76F3E4E736B4FD3BC5D + 6796829EDD7895A6D75FA6A5BBEFD2E997BF944E3CFB615AB8FD36CDDE7A9301 + B169711B48FCDA6C5FA6F9F557A69769EEA65DFF9BD64E01C0BCC379F63A3017 + 304FB9D52D00781E08CCB2D1769E7376EE7397EC1AD9759EB1739FB9FEC8B6F9 + BC5B009CAFABF7DB616E0B80F332CFB7FD9AB81DE778C3CEDDCA54071BFFBCFD + F3B16D89C49572F4B795713D4D005DAE15EF9B26F297E370DD78A7B4A07D8E8E + CDDF831869ADCFACFEECD8725CBE4F02BEDA6F6CEC7B41FB28B79B2134697DF7 + F2F9E4E863FAC779791D87B15CC79DCA70D7CED3211876769E7CB72D8FEDF425 + FB4E5DB4BE20FF1C38A6CE311FD3DFE76BBFE70C8DAD6F5CAB027FF332D2F94F + 1D4A0381A72F6D38F82502788E76AD2F4476038001C9285F673EEB0CB9B90FA0 + 0CA101C0776C1F009D61F50CD746C7B063C77B93C3E772CFD2364AB612F7AF08 + 8F6B3BE5FBFB80ED7ECDF2CFF1FDBFC8E16A5186A7765F2CF95EA748CF8041D2 + F3A1E739B18B4D3CBE9E1702C00E69CBB304F03B799636B354DF9F6B5ECF9EBB + A6DC16CF05CEC3AE01B2678443D3D3EBF63CB6E707EFCB3D45DDDE6763AD9E7E + EA583B040C06C4B224F3BAA54D27EC7896C71FB8F45A848913F6BC3E71DD5F99 + 90A131CF72CED5FA7ECA9E2340E9333C6F3673C42FCF3A7B16F15DCAAF1EC802 + 068F62CBD882286113EFD977086CE30260B303E7328641C0DA28016081DF5154 + C0AE60AFE40098766CCB3E4017E04B9A77FED30EF59080B022827DA967D32870 + 96366C1C15B72C853DE211BB173C6FE488D50DE3411F575A5B9E5FC0AE2FF34C + 9E493078CC8E3575FC469A2422DBDAC1AE59B65963D7304E75005CC6BC5DE3E6 + 3826F6316F95471ADB26CAB76AA34BCD0A40B6CD02EE9EE9498FF0474CDB0260 + 15ED2B18CB7C4275C9D34A40BE05D49AFC8F9FA8D4E901C02681E1430B2D0046 + 0E69CDCE5F275300B0207584CA88F203253A37CAE76BC052DBC6B96154579D3A + 4FF9D22193FE20EC73C47A2E19343439586EE751BCCC97DBF7FB3AD42DE5F918 + D68F49EBAFCD1D01BFC060CA9AF97133BFCEF3EF7AAE5DEFD7EA57EEF9C11F30 + 48F20FF4F349F4C057C155DBEE84BA19DCD6EAB2EB02C0F958A80F00AE212F00 + 7012615F1D7BB4C05DFC4563D696C9A37DA5502E00DC041F50A7E445353EAA01 + 367B513F7F9DDA8F3651FDECA36259B4DDAB763B66546D1B35A8BC6E272A9E47 + F67FCFA6F55B4FD26FFFDE9F4FAFDEFE30CDDAFDE6C081E9C4D2CFBE0474B3B2 + 6056F4317F1B14FBF631FA17590192EF3FA6F72558856D9BFA855F34E5DF01E0 + 6F4602B9B554B69B6D2C0F76990F76D8B384B3786207846DE4AC31C05E130058 + F500C2440337798153EEDA761FA9EE7700B8526B0F5DAF3ED0F0C17F0780BF06 + C59B6250D7CDB8CE8F8A365D767DCBEDC1A9875DCF432102DB06A0BEBF3EE6C3 + 6A90E2719A6335FD67BF553D48E8523D008A79CD806240BBB16EB457DEB741B1 + 8F515DE73348BA26BBA90B08770D7E5516F763B9F2F6220DD6A3047F35583FC0 + 77A600E041F0B7D1D8EEE0F5DB0680E3C4AC9EA845A92DD9F4B3CBEA02C066AF + 09A3A9130033512D93564D5C958E795D761E395B26E09A94338197C3A09ED8A3 + 388197E244BE56977DD44E3B8E71CAD544EC56E057E9BC7F3A8D2E9C49A3D871 + 8ED606E5C338524CDE5659FA1965680CA8B5FD007D0FCD66083CBC703E1D22CF + D3945B19ED593ECA36391F474F03814D6C69DBFB2715C710CE24E0AF1C4F7234 + B19573C91D51C5368A3C8F42E0FDC7B457F2645B3BAC945F2BDAB948E36C0B12 + 888D0EBA9887E30DC53C2942DB0875A5D85EB4EB27D9D4C2A9A8E8127734F691 + 20701700C6715A03E058D7F32B002C7964F0D19B69E2F8569A3973DB97A01C3A + 666527ADDE69EBDF194B9F32BBD357D3C4399CC7667B76334D9DBB9D264E6FA5 + C9536B69F2F4469A3DB799162E6EA639D37481C038937156F35E40DE39E81018 + 07B43BBFB3937C2F72E7FE00A77674D46B5F1040794D3B38ED4DA361C9511751 + 5A564E1DB644424F9E033E6CD93E7980063B2F407089E29D2891B62847F8B5C2 + 114E242E3056725B1CE4A50EE5510014019D0853047E0127199EF41E2F4619BA + 039EF6013A9758FAF64E1AB3CF03E80B5CD2362F636BF52F03DFEE3BE87468B9 + F6382D6D3C4987B79FA6C3B79FA4A3779EA6130F9ED9FEE3B4B0F6202DAE3D74 + 10BC6076F3379FA6B95596867E9CA6AF6550EA000D38780568DB4A919B0E9880 + 63C0B0208141005D86854427E63A19F2E634026021C1BA08EF94DF05F404BE80 + 8702829C778E16CD11A30283D2CCF52CCE8DF720237F772A7D209212006CD70D + 9BA9ABD66F8099A581BC02C00D105DB5E33B00B6631205BCF62CCD9A66D69EA7 + 49DEB97CFB6D3AFDFC47E9E4D31FF8FB8167375FE5F704DB76C9B6331B2FD394 + 5D6F967E9E357BBFFE2CEFEF50D73EBBEBD6E68D67768CE776AC0C8017D6F352 + D00B37AD1D80B19DEBC2953B69DEBE67B35C1FCE9D25A07D6BE7CC12C17E0D11 + D74FF01710DC82D059CB03EEBA1D3071D5AEDD0D3E7B002DB6F9DAF3EE64FDC9 + 409FADD25C6B7DF6D8B410380360B591AF6106B27CAEE47B3FCB67C439359F57 + F90CBB4439C7D3D2E3B98EFEB090CF85EF07C7E3383A1EC077DE3EBB859BF679 + 977287C3F67B130476386B7DCCF097B20C5273D4AFB5CDF916017FD1E485FBFE + C70B22F0FD9876DDD0C97BAFD2E5975FD9F68DB7C51F50F85DD37686A5FC0964 + C3EF1311CCFABB8A2FDBFD973ED9B1A7EDB8B3F69D9CF33F38706CFA91A5C875 + CE2F460013A9ED51C9F4DBFA0A3CF6EF09BFE3724F12B8F56386FB54BD5F4B90 + 977B98DA88EABDC7D97DCA6CBA00B0DFEB8B0480E3BEEAA84C6995C77AB12CE6 + EB58B5B0115CE6D9D23C5F1C9216D93EB0343F8B7A01306DE467D1AAB5C57101 + BE76DE25D29767EBB83D6780A6E3E7899CDD4A43A736EDD9891D7DCCEDE89987 + 384EA372DCDD04801D01FA9EB663213BC63800D8EA4FDBF76DCA6C26EC19C876 + DAAF07E704BC65A9677B9E9D0048DB33EED42DEB3B80D7B6E7F83301F7F716FC + A2313B27CE0F702CE95A7944B28D0DF8131BE3148D6DE2B2D05A1A3A43E21C21 + 3C7C84710EE31DC63F390F3BE5E5766CFC5280B000EF24EF1C2EA058D1C10781 + 9C56AE7CF280C23EBE3BD246FF6A1C07FC1D39626360C6601CA78C07353604FE + 02A5B32DE5266B5F701900CCEB59A6ED3A4CDB351D671C66B6D415F48D10D8DB + 0500FB52D279BC2B90ABF1761C13F78C8B2B61BB57008C883A1E0E1098790122 + AD3943D71C22DAA95CD1C7F915308CFF6DDE023036F92A4001FA469127281CA3 + 819BC8DE62233B897CC15F14E76AFDA4B9A1B76FFB714E17E78DCA8FE59E3F63 + 699F8FB67F168E73D6F792B5EFF0977933E9299BF30280ED1887A6AC4F9580C0 + DA3673649F6333DFE6CFD71924F7CCBD4D11EC6AAE1ED35D365E167C02BBA9DB + 5F90D50356836AA8BBC3660028EE0780B3F0532CDBF5CC2B96F505C07A07F084 + 1D63BC3A3EBEA222F9913E1400A31E5F554779976AFB7EFEC728D94445FBAE3A + 289645DBBD6AD0F1A2A25D976A9BB81FDBA9D5733EEC9BFD89D3D7D2F6BD97E9 + B28DD52767ED7703243C34E7659D3EF19F634556D0A59A1FF455E0193D750300 + F6FC61CB73082CC15E627ABFB27AE5B8AD62BF43BB35E3EAA79EF63F4C9DECEC + 6B3EE68E76EBFD7EB651B55DB1F5770CD7B6AEBD45E8B2F4B396771E1A9D4EC3 + C3D369C8EA7B04B0ECFAAD5C6C8A6DED55AAFB0B0D807BCAFD04ACCC2F5081BF + E143EEF9D04DDF01E08F28DD1075C38C69936E9EF1261AD35DFBB554DE65D394 + 71F38FB054DA23001EF49054BB8807AA34A8CE87281E4FF2B20E00DC3548E852 + D780470309D2B1CD2EC99E01A0F29AFAC1E6DBA8780E7B55BC365DD2B5905D54 + 6DB757E5F6A276DAC477FF4A19FE96013CE7CA96FD1AF4764960B703EEC649C7 + 7EF30729D689F5EAFDBD6AD0246DB736E3242FCBF203FCD50432AA67121A26BC + 9AB446293FDA494CB68799689BEAC9BA9C059AB8B7F919A2025D0FCD32D9A7FC + A4D5A78DDC4EAB9CA7497E946C94EE2DA7CD0C7F119057D1BC02C2805587ACBC + BF177B6B03F8EB11C6DEEF0C87A5089191227501BFC373E7D2A119CB9B3E9D86 + 66CFA691791C3EE7D301EC4C0DE02D51C20E8801C6B465F9EEC0C161531C372C + 1D7D609273CAA0594BC7B96368B9371241CE25A5E5848A4E29E5E1803A202794 + 2FE56C75897C28759B7D537470A1AEF6E4288B4E3829E6B34572DC09C2467BD9 + 908E764ACB8EB49C8292DAEBCAEB2787BE65EBED59BA0BE02295A1EC24CDF017 + E7A91CA892A0B0C0F07089F89522001E3F7ED3B491264F6E78D40FEF0AF4E52A + CF58F9296BCB26C663A7ADAD53391F300A089D32CD9DDF4CB3E772F4EFDCB99B + 69FAEC4D77AACF0020CFE1D8EE8D8E42E44972AC779537FB2639DB2539907166 + BBE3BE38E4DDC18D4DE520D7B17837237001E83B8253DBB6C8A183C961803BBC + 2D8FF7FF0266CF6F99367D3B7D76D3DFC7A808DE06B60618C456203602DE1AD2 + 2ACF457D07A5441D0267AD7D9675058C5C003417084CE42E80DDEC7354648637 + 19E6E67D80AF039DCB80D47BD6BF9C4F845F8C3014E402C6CDADDE4FB3A685F5 + FB69E5D6C37478FB613A72E7613AF9F0693AFBF4793A7EFF715ADE7A9096361F + A485B50C3E5B20FAC801A947C90206AD6D491011B0056C06ECB6D02FA7014DF1 + 5DBCA4BDFC4A86B6F3F4AD440F3BCC05EA5A3E5BDE439BA160BB2C3375047D05 + 101DDA967AC0408E13E1A1F2D87A5E736E19009306F422C06F86C0762C8E1900 + 30D721C2628E9D2347AD7F05FEA285B527691E000CC45DCF207861FB753AFEF8 + CB74E2C9576999E59F59227AE3A5953D4FF36CD75FA4C91B8FD31411BB805FC9 + F689FE9D5BB536576993E5A15F38FC5DDAFCD4B74401F30E6707C0D7EE997234 + EF0C7DA41E51C97E6E769E25A2996BDE02E0F673CD10B805C0D3663B7DD3AED3 + 2AE7CFF72AC3443E971CE16BF9F65DD4E7EBD798E3009D2DED9F379018FBAB1C + 9376B966F7ED7CD417AB6B9F27EFAE6D3E533E3353CFE708D425CD67563E57B7 + B3E30AFE6A49E858A67EE9BBD3FC3638A67F8659FA5E512E9BDCEF5E00CCB1A6 + EDB7E76DF31DE7FB6C226FCA0130EFDB46392A9977298FD96F9DEFFAEADB5F4A + 4F7FED4FA7B5773FF1F3E73DE4DC2FFC3EE15BEE1B2D7075085C3E9B0C83AD3F + 4418FBB1EC7BC767CE676B7DF7A85EEBA3BF0BD8CEA7F71CF2F5CFBF4FFA6ED7 + 0770ECE7C59F4D32A815BC8DC0D7616DD88F52BECA047749FB7D8E6DB9C7711F + E67E2C9B29EA73BFE3BECC3DDEE47FEC2969015BEED9596BFE07A4E952CF6170 + D9624799E7957DB6CA9BF408DCFCEC605B4BC7F3E7431F9B71FACEF384E78D6D + 23FC6D9E416AA702C02CF93CC1AB052E004D4D67B6ED99743B8D9EB9ED51C0AC + 1EC152D17AAEC5E79BA4659E3DB2D6F6F5BCEC1500185BCEC1AEED19FB6C4EDB + 6770C2FAE891B6F68C3F65FDB6FECC9CB7EF967D9F78F5C3A83DF386CD6EC8A3 + 85A967DFF533F67D314D9EB3DFD605BE37B6CFFBDB01BF762E4407EBFC1CFC9E + B4EB63FD6A01707E2DC5A88D5318D70CD9B886B18D80AEE06FDC17E8ED2A6F01 + 30E3A40C871535CC5611BF405EF248131DEC11BA25DFC1307D296341C6654060 + C67379FCC7F8D0C6A2B66DF3768A36468E526EB2F62304168C9E3C6963183B7F + 8E411D1DB3674C6BC7E05DC1230E8F778EBBBBC6C63E2E0E3675DE5EA53A4302 + B9361E17D4050C93AF3201E061EC6D0ED1081BDB0A00B7009AB4CD5F8A1C0A17 + 689BA1AECD4B0AD81DF2B2B0F5D7DBD81CABD846182C58DC037F292B73BF760E + D83B37D4FC8D3CDF9F3A9623842D5DD7C9F3BBDEF95FACA315A3BAE6AB3DC246 + 2A7902C7AD980FDA1CDAE6D11E193C61761EE16B7D2A3A607912FB43D3D64F93 + 96853E687DCAD1C3FAF3759E7B3710B7A40FD8BC5D52D9AEEAF007D4DA9B7FA2 + 17DE0258637ABFCAF506F92D320056F46FD77100BE44FE1E9C6CFBD26337660A + FEA32680A08FEA77001F0C3EAE7E8AF65D75E4978BBEB99817F3BBDAEA67DB65 + AF3A1F43F571A52E5BB45B793FF5EBB7F27907309097ED887DB6C3F6B90E8D51 + 663666E7E55FA39FFADBA8C80A9018410F37288C82C8DE9EFC0EC5761AB6B11F + BEB12F10DCB6DBABD8FFD09E60654F1B5FAFBE5600DCAF8D7A7FB7BC5AFDEA44 + 9B98EF65FB00C0F15DBF81477A5E01C4B049CA00C3BEA55DCBF7E8E1AACDDDA4 + 3E7D0780558E861017B4FA207F91D57903F9C8AA6E88BBDD20A39A3A51AA5FB5 + D3A51DEDE941D00FF4EE02807700D79F92EABE48EA3F8380AE4185F262594CF7 + 536DA3FD1ED9F1876D5039CC40D1AF7506C07A0F05FBB1CD6F52EA6357D9D725 + 0DC2FC1A84BC7EE571BF556FBEFFC37274A5D1A1111BE4071D04D88ED8801C55 + 2097F7F766E84B19DBDE32A9274FEFF30D1058930C4D40A27A2721ADEAF258A7 + 4715688E6D44083B346593BAD04EB48BF95D65B5F66ADB73EC3ED1BF3B1427C4 + 2641DFF8EEDFBA5CF9BEE55D4765A2AB09789C946B82DFEEB384D8B906766600 + 9B01AB24682B50DB2DCAB203A07604E838710968DAD2B1BC3E5BA57DDF26EDD6 + D7116B7784F41C8E8E16F6AA6EECB3C3DF194B9B80BE4A0383874C2CF37C608E + 48019C30386A70D25CF0AD94A386F33B861D50DBFED8CAA5347772359DB87237 + 2D9DDD48A34B17D201CEA771DE58DB96663F425ABDF78CB4F6EB3C54435B39BD + E4009364176DBB1C657539224FCBEAA92CC2592410DB2FBFAB9CFDE814A4ED58 + A7CB36E6CB4E65B42345888BBA607004C082BF02C002C4B11EE9A1635911FE4A + 63C76FD8B159063A47FDF09EC1FCCEE02B698CE85FA2814DE3A7712A67C72D9A + 3EBB9666CF13FDBB6EB2F439F232009EBD742BCD5EDCF2B49CE382BA72844787 + 385B015FD9D77938CDB3E31C677900C496AEDB509EE4CE72EA17C800FCAD0130 + DB9887709853CFA1C285AD347DCECE0908EC00D804D00DF0B70140A6087B1B80 + 5349F53280C9F077FCA2F53144EAE63CBB8EA6362F439B5AE43B102682EF3211 + 96F7B353BEB109B088E3D9FEE495ED3477F36E9A5FBB67DBDB6969F3763A7CFB + 6E3AF5E8615AFDFC5D7AF69BBF9E1EFEFA2FA5CB6F5FA52377EEA685F5ADB4B0 + 419DED3473633BCDAEDE4EB3569F258EA7AED1A65D2713D0504BFA66B0045402 + CAE5484CC15FE06906C8F75C1357EEA489CB80C3BB09C00A140350037C1D329B + 00B053B63F61E70B04661F5B2903DAAC0C6981B8B49141ED941DAF81BB0514D2 + 07C15EA47EF500DD922F3BA5333CCD0035B6939789CE1058FD20827A71FDA98B + 28E0F9F5E776ED9FA785AD97E9F0BD4FD389475FA4C377DFA5F9CDD769E6E6B3 + 340DE40514132DBCFA244DAFD28FDCBEA0F4C22A11D9D859F90DB3BBF922ADDC + FE2CAD6CBF4BB36B2FADDE732B7B66FDB0BA570180B7BD5F2C210D605EB0BE78 + FD724EDE57AEDD75602B1014C0C9778EA5B633D877100F10E57A3B00B6CFD03E + 738FFA2D4094C853C158FF6C89F46DAE57B97E5C6387E7A54D3B2E5B7D8EA47D + DFD282C1883A8AB8A62D5D0FC16CDAD5F70CE02BF0DB7CF73AD480DC3ED26FCC + FF4C60D2EF2A96FB7E01CA887639F6384BE21215EC7095EF9C9DBF89DF054B2E + 8FDA3D86EFC7C567DF4B1B9FFF6A3AF7F8B3E618F1BE326DFB793968CEC7EE21 + FE9BCE7DE0DA036E7917347F0AE0581C6381CFD93E0396D4C68EEB28981DCFC3 + 23A1CD9EA8725FD29D7300C873BF00343B60E6DE0584CE12E88DE5CA8BF27CBB + 7FE93E26F97E682BB6A97CA02BF7619E2748F77BB60E6F833D5BF5857DC164E4 + 209A74A8AFE7823F3F786ED8B30079BE897C97D962AF6703CF0C3D37B4CFB15C + FEECA06FF4D9EA015DCBF38E36F98314CF1300708EFEB53CFB6D2CAF3D4D4BF6 + 1B06FC1E3AB99146CE12457BC79E4B5BFE8E5D8F1C567F3AD4F55CD4B9B63656 + CE33DE9EF744FD4E9DB6E71B2B5A00808FDE4813C756D394AFECB19966FD9E7D + 378DDA738FEFEFE8B9ED3464F96367EC3B71D17E6F17ECBB629ABB64DF97CB0F + ECDADB6FFCFCB65D8B0C7FC74EE7F3E3BDC682BF598C21F2388331491C9FA008 + 7C05786BB01B6D047EA5582F46F60AFA9246A4057F5DD67E06C519CA768FE118 + 5FDA38B9006049E3528D453DCF9781362DE763080437C7ACEAC7F16C23C6AE26 + 1F3797B1771C6FC731761C67CB262AD6DD4DB2ADC7F8CAF7B2452B33A94C7DF0 + B26A7E201BC981302017591A3BDF32A7B1F98D03679BBF90F63FA09639137994 + 1D04C4760060E02FDB5A4305DEF6137336E6700E7B99CB4DB7F34795938E51C8 + 9A5BFAFC4F734F936CE3DCB047619E5A8BBA51876C8E09B81D9E39968659EA79 + DCE6C52640EF30F34BDBF644FD4E0091AD7FD3B9CCE7C8361FCF001818DCCEC7 + 95373CCB3132144671BE2EFBBEEAF00374497E01F90850AF4D86B18AC6DD095B + 175B29AF92EAB4F55A3F49EDBFD8B19C7387FA01E0B6FD22F717ED227C5AB6C5 + 4725FF9614FD575183CAA4DDDA407B09B2E8D26EF61FDB5F281F64545D16ED6B + 75F5B1EE7B6CBBD55CF6BF36BE61CB1BB1ADA581C3BE8FCD7BA8F13B779449B2 + D961477F06F8DBBF2ED54C6090FCFA74E477A9E10E359F3051EED1D6A1BDC829 + 3E5CB13D3B6617EBF986F4B5074E76C2D83DE645EDB54E579E8BFCC0182B08DB + C8016E86BA51B13C42E246A57C87FD1EA43ADF016095A3EF00F04E75DE403EB2 + 7423B4F4A01B5F7D63EC6B1BDAEB27B555AB7910F47BF0C4FC3E0FA8AE87D98E + 87DB7B4A6DF7D320BBDEB27E8381FED280428389BD48031004F8653026E09BDF + 2F6183C3F76CFB632A9E633F75F54FF56AD576BB49D7A85697AD946DDABA9E37 + 6283DD0AFA4A0E7F51C732CE0E769924005707D845F500E01D138C9DFB6C774E + 7C7AEDDF57B1AD38C1EA2A1F94AFBAB19FB56D2C8BEAA9DB4C12AD5E49C70965 + 84BA5180DCFD48EF42F2BABB4CA6B3F27B742300AE85CD60F89B952171F77100 + C08055DA6BA06FAC5BE4FBC5E9316475FCFD51EE746801706C47798D58F23908 + F8EB00D8D22C0FED10DA237DADBC4060B6883CF647172E78DB5C8FD1A5F369F2 + C89574EDCE9BF4D59FF8A3B4FDFCFB69FAD83507C0386B1A87CE72AFB3886D74 + 4445E754CC4738C80E2E67385B3BBC284302B9DAAFF3B4551DF6A333AD9F045F + 07D9AAAC9F7008226C3936796A37B64DBAD779D8E64B6ACBEDD81EC779D9AACE + 13D4655B03E028E55347E017872B8ED7E87C1DE33D7F386779E7DF492B331105 + 9C01B0B575D2FA78EA6A52245386C3D72C7D234D9D594DD3A699B3D7D3FC85D5 + 347BB18DAA9AC6315F1CE8D1312D47B49684C6263AA9A3D3BA7666D74E7995A9 + 0DD23A165BD9B9D3197BDB027971D63740C04494AD1CF92367CD06673EC72AFD + 688FCD396DBA0475D92A0258E0B71FFCAD417053079079D9F22E6DA6B10B761E + 050003770481518EAE4419D8E47AAD1A9003CCBA92A30C8140802F950BFE7A64 + EE354BFBF2C400E03B6971E34E5AD9BE9B96B76FA5334F1FA52FFEE84FA53FFE + B7FF51FAE37FF7DF4CBFFC57FF52BAFAE92769F9D6763A7CFB9EE9A143E3D955 + EADD373D70103C7535F73103602273810880E8FB0E091C7AB10FC402A401014D + 02A7ED16208CCD1D5F5E185BC15F015D0142B611166A5F12309D5DCDD0516057 + C7163C6CA064C88BFB318DD446048FB2637F91E5B26F9A8D5D8BDC8FFB969F23 + 803D0A1830679A597D9C66D79FA5E53B9FA4E30F3F4F47EEBCB5EB09B87D9223 + 7E3D4A17DBC769DACE3D9F1740390060B3213278E6E6F3B4B0F9262DDFFED421 + F2D48D67697EE3755ADAFAC4DB00E6CDDB67CE92DE390AF9459AB5BE706DF279 + F1FE6296B0B6F30C005851C000C3261A96CFC8CED5A1B4D76F3F13C4758FD784 + EBA56BA76BC979F0190BEEAB6E2D7D96126D29EA9A7668539F83DA5739BF07C1 + 5F45FC768988579D5B2DF2F5FB529EC06F04A9FE5B6C40B38EA9E3DA356864E7 + 6D79443F8FF3A70DAB07985DDE78E2DF1BDAF43F67581ED097FBC5344B77DB67 + 9241B8E59B0DC7A42E70DDFFDC60BFAFB9EBCFD2A22FF96DDF99EBF6BDB8C6BD + C0FA6CE5FCA1427DD779A91DFA3967DF37DE2B3D7315084C19E79CC1AEC06A84 + AD31ADFB299A29B6A465E70AF7332410ACF6551F5B0155EED7DC7F79AE004F1D + FADA7E0D7F057BD50669DDEBB151BEEC544FC7221DA563EBD9104599CAF5C721 + D2CACFCF0C130098FCF21CF13E5B39AB6800458778B7ED99F57464F379BAF0E4 + CB74E6DE6769F6CAA3BCF4F359CEE18EB57DCB01B0FFF9AA3CE3D45E943FE3CC + A679EF7EB1ED913DB73D0AF7A49D17C7B6E73FAF72983E63CFEDD3263BEE04CB + 3C9FB26B65F7EB29FB1E0C03752FDD4D6397EFA5910BFCA1C1EE3B979FA6F98B + F65B3B6FBFE18BF67DB632DE77DD46FEB60078DCDA57E4AF3F9BCBF883310963 + 90F8E7341FA304B81B616F0D776553C35F4951BF82BD92006C03816D1C377AB4 + 1DD7F99FF1CA984DE3B4766CC858D3C6D10500AB9C743D1ECC79A6723C1DBB85 + C0BD63411DA3D608B231B4C6DB523DB6EE02AD92CAF6521EA5F13E36CDF8DDD2 + 71EC5E8FE37DBFD48DF5D55FAF0FF02DF0371E8BFD267AB8481058F324949792 + 2E70B7440AF700DF92A7FCA1006D6B315F631EA77DE65F63D69F61A290AD0ED0 + B7B68952593D1754DE0E7501E03E50D857859A399A0E11B95BA28289EC25DA17 + 00BC64F790F3F6CC3EC1EA164B17ADCC8E3BCEDC18288C6D06BD40DE1AF44ACA + D3DCBC99A3F7C98BF91E2D1C7C00F5BED4F805425E136DECFBCB569EA371FB45 + E4F683BF11CC460DF271E4E5A177B655CB977E9EB4F420DBD1007AD1C842AB90 + D715011CF7DF47B43148D8EC0680637A3FFA107F5F97627B7BD1A036BACA50AC + 9FED4CF885E5172F79D9AF489DBC2FDFF17EA4BA1FD2C65EF4B1DBEEE2025264 + 08CD35EBB0ABB5A39E1845E01E9D75F6C032F6A6D8076BAF8BF57C438A11C09D + D1C05F87F6056E8BBACA06D9EF10ED07C65841D85AB2EB8CECD57E5455AFB1DD + 83E2B17E2100F0C1432CEF3C66270F08DE79411AF9C5DDCF87FC73AECE1BC847 + 567523DC5116F78B7A6E92B5F670D3547DE9105B7F285A9B0314EB74B5EB365F + E3034F6DF7533F281D956DF736E08876712021ED6520A501D7F01820D20683D6 + 1691C0E35347D3280352CBD320ADDF71BAA4765157F95EA4BAFDCE31B65D1F47 + 75BAD4653FA83E651EBD5B245BB5D5256CEA08E02EF08B7AE06D05769BC85E26 + 0C11004775D87B1D45E44A65D2514F3CB45F4F82A458AF6E478AF6B19D2EDB5A + B16EAC53E76BA216D55556FF9B37E6E50860ABE7B26354D1C05D20575274EF5E + 04F465592CFE19CDD2C9F5E4B95B4CDA5BA0AAF4F0FC5917FB82B4B13CDA4B39 + 0AB89DF04765009CEBC5F6E2160DD11636D85B3D003091C079A933006F5BDFED + D597B2AD01709422831B15F05B8BF706ABEDB1C5F3697CE5523A7CE156BA71EF + 6D3A75FD7E9A3872391D5A2CFFD05FB0EB84B389C8DAA59D8E24CA46706EB9B2 + C327CAEB564E239C668D7030D9D61D5FC5A9564BE56C555FFB4A47C75A74AE45 + E79CDA8FE9B8EF79033464B688748CBE45E4719C06EE5ABAD68E7CF6B1AF806F + DC4702C0314F75757CE02F4097ADA27DDDB95A00B09CAFE36E0304161826D297 + 48A31B69EADCAA89652C71DAD2861DF314D1C057D3F8996B69F274D6D499AB69 + E6DCF5347B61CD1DF44400CF5CD87480AA2595110E6801DC21FA7CEA46E3B846 + D1918D6D04B9E46708DB0B8111FBAA8B5457F5BD4D1CF7E4DB16E73E91908BD7 + EFA5F96BC0D10C0BDC797FC154A2D280C05E57C762D9CE7344A161DF0B7723F8 + 4594B1CC730D7D6B3BC432D28A8603002B4A4EC057D19779DB825E8FF635B500 + 187B400EB0690000F6284EE02F11BC774A04F0DD74F8F6A374EAD1D374FCC1BD + 74EDD397E99FFABB7F33FDCFFE93FF38FD1FFFBFFFEFF45FF9EFFEB7D2CB3FF9 + DBE9D8FD7BE9C89D7BE9E8DD477949E80D96867EE86209E9196B6FBAC0C2BC4C + 3010EC5E9AB8F8204D5D7A98C192C3BBBB09F82730286590C77B6981C4D6CE35 + 964F56D4670B80A5081D1D22DAF1A659BEF8864061D1AAB5CBBB7757757CCA0B + BC2DEF351638640B4874481B00662E03BA3E4D73379E783EF53378CC69F2DAFA + 5646FFB81ED6BFB6EFF91C59B65951BD3337ED7ADE7A9D8EDD7B97966DBBB0F1 + A20700B354F3B4B53B5DCE2B2F419DDBE27DC37344FF5ABF266F3CB3EDF334B3 + FE324D6FBC4ED3B69D75B1343491B177EC9CECF3E69DB60E95ED385617804B9F + 80C44B2C496DFD01DE67E048F4ADD279696600A47F76D687696C4DF91A64409D + CFB705BE92D7299F754E635B3E37FB4E34B0D7B64DBAB4970171FEDCF267D17E + 2EDAE73388656C3916D1C20E794B64AE947F2B5944C966B89ABF6F5DA25F02A8 + 5139EF6EFE4DE28C2F4BAFF39BE35DC3B44D642D7F80C87F88201A18C86BE745 + 5DDA36F13B1DB17BCEE879BBCF50D7F2D8F2479159BB4619C6D307EB3BC743DE + 069F435E227CFA2A7F18789196D65FD967F5C2AE9BFDF680BBF6592DDC7C50EE + 17DC4FF89D66F88B6877CEAE191078F6DA63BF26CDD2F1768FE41DE54880565B + 0153D2405C6CB8BF22415DD9708F8AE05720D8E12C7D2AF6B4E560D6EA0800FB + 72CE886B53C15FC41F671CBE967DC158D9A1D8F7789C5816EB0B000379874FDF + E8F903916C652300EC791CFB2CCFBB027DAB6716EFF41D3F4D9EF5CF9E017C47 + 96D69EFABBBC672EDB6760D77F69FD99EFF37EDD915300DCFC072A9E497A0EAA + DDF8BC8CCFBFA85CA6572AAC3B0006A4027EE7CF6DA7F9F3F6DD3A77CFCEE54E + 1AB6EDD44DBB7F6CBD4BC3D79EA731FB1E8DADBD4CA3579FD9E765F73FCB5BB0 + EDEC8547761D789F35EF9C2762D93EB373F6F996E59F2758DA1A08CCB1AD0F80 + 69C1DF5AE423C62311E4D6C037E6D5B62A630BFC8D51BF02B011C67A34306326 + 1BAFC5B15AD758D0C7838719630A00F78EEF64A3F19CEF57D0D7DF035CD2A3D4 + 075A177B1DB33EEE286219681B3FD7636D0159DF0F6592E0ABD2713F4A654800 + D7D305E262A3E3F9983BEE87BA51AAABFACDF83F9491F6E3DB31995FB014B4DB + 5700B816F057CB4747D81BA1AF40B0A72B681BC5BC90AD47F732FF9AB67998A5 + 15ED2BBB766ED75B3FCEFBA47ADED955D6CFB6D194CD7389009EB579B183D163 + 697CC1E6689CFFC4F13466D766F3C167E977FEF09F4BEF7EF827D3C2F11B1EFD + 9B01709E1F1FA20D9B931F2CCB3A0BE046881BD3314FF90DA8ADF29B6D99FF93 + 9694E7F9EE3BC8521A00AB7633045EB6FC5E00BC1F75D763BF9F4F23DB7602DD + 2022800F4D595BB61D14857C108D597A64A1113EAA088101C0BE02A0A986B2D8 + 4ACAAB6DBA14EBED45D489FEB541EA67A3FCE8BFAB6DDE57B1CDD8F67EF2516C + 336A872D3EE310ED9B8372B0A58D56D1771C7DC983D4AF8D6FBBA28FBF56A7CF + FF63A92FB3E0B87B55577D14CBEC385DACE71B92A06F5497DD475517ECEDCA8B + 2A653DFD1B64BF43D8B64B3777B3C616E63A8C0D91C0B1CCB7C345211F507CE8 + 60AED3B4BB07E918BF7000D8D51105FC0B1F01DCEFC7D07313797FD537CF9EFD + 026C594F7F879DE51FB2EDA17073EC7B332EEDECC8EF90DA88EA7A18D4EAB1AD + 616B497F1D0FBCF8201D28FA56CEAFA78DD0CF6CDB3118F03A39DD551E0712FB + 95EA6BF03432BE92C66CE03B3462FB43F3697874C9050C8EF5B0DFCB60ED7D55 + 0FE6625E54DD0FED47C532A507291E53E057521B2AEF523CB6DB0FDB20DC14C1 + 6D036A435E0F00A6ACC05007A2B56DB48FF5429D08803314CE130F265DCD2487 + 098FD9B613903C19423D6DA9BD3A2FA8FFA4A6B7AD836C4B5B3A566DA7FC585E + AB2E03F04645DB667FDAEA840961038003C0AD15977CAECBBA44F4AFC35F9BB0 + 034F074EA61B619721AC24A8EA60B5CA1BB64936EAB1B1FA3982D76C59DAD9DA + 65A92E4DE8B3700EE4B6B48419F57C825EEAB7FDA05DB63817704604D0AB3682 + 7D93CFD6FB93C5FB7F7D6BED7974EF74CE8F105891C0C8DF1B6CFB385C58D2D9 + 97A9B63A40609659A38FBC536B6C05E86BF6EEA8C1E1B4D3E98313474EA106F6 + 16475784C138B75816CFDB31DB0C6D6DDF74D0F60FDAB10FD11620B7D8D74E31 + 81DE0890C9C726EECBB126E79AF25526C75D74E249DE5E1F017D599A4FF01709 + FA0AC0C67CB55F2B1E8F7DD58D8A6DA2087A1D027788F688629904EADA16E7AA + 226CA2C86BE07181C3FE9EDFB3BC6B712DCD5EDC48F338F42FAC99BD9DF3B173 + 69ECF4C53471F6729A3C77D5B680E02B69E2F4950C81CFDE70F0EB0098652ECF + 87772C9E5B77C73569B6EE083ED53AB451EDBC5699F24947C7772C43D9B1DEEB + 0C6FEA9C5F6F9CF438EE8114F38042DB46677F74EC0B2044103076DE8EA13C3B + 377FE7EE05600260A45517E845E4D5366E578008621968C11107A9575B081CA1 + 6F4E63970170062B5676E94EF2255CAFE09CCF0018C0431B3300A41BB77DC9E6 + 1CF99BDFFD8B0E6F3F4AA71E3E4BA71F3F4AAB5FBC49BFFFCFFFB5F41FFCEFFF + A3F4BFFA4FFF93F45FFAEFFCBBE9E51FFC463AF6F05E5ABE73272DDDBE9766D7 + EF9AEE791B7337D9DE4B73A619BBAEC0BA0C700156C02AE0E3E33477FD499ABD + F63003B86B19DCE5C8C5AC08EF006200C505DE9DEBD05160D18E0DA82B029E4D + DDC89A26C2F1A6D9AE3D740157276FD83180AC6B8F5DEC4F5CB3EB02A0B4FDF9 + 8DA7C5F6BEB5918F2F00BC781308ACFE00189FA4B91B4FAD4F2F72BA0F00CEF9 + F91C1C0E967E67804AFBF91873EBD6861D9F3E2C6EBE74CDAD3DB773C8EF08CE + 11C2C05F00750B28BDBD1B44D9728DEC98D64FEA4CAC3E4B331BAFD3FCAD4FD3 + C2F6BB3477EB9334B3F53A4DAC3FB3F3A64F767CFBEC01E40063968B061C4E03 + 4E6D7F61ED595ADA789E16EDDCFDDDBA768DF252D6F9D8F99DCC768DFD7A64CD + FAF9E76B30CBE744DB0E765BD0CBB5E39DC5F99DD1A59E5FAB6CCF7784763937 + 8EA3F3CCC7349B72CEF93AE6E86745FBC66BDED3AF924FDABF5BF69D73C84B5F + 00A605FC0A062B1F65C89A2364635FD8AA4CF0547644D8FA1F1E2C4F6013C8AA + 6302572374CEF6D60FFBFCC6F83E96DF8CDA44DC5FF8834886CCFCAEB90F980D + BF17CECB3ED3C9EB8FD384A527ECFA4EF099FA92DF2F32DCB7EFC33450D1AE7B + 7EF7F27DEB57BE87E43F8CD87D83FE3A0CB6DF959F3FBFCFFCE7118ECDFD8873 + 511478BECFE47B95D2B9AD7C9FAA25E81BA5FAB5046201C12802D95ADC83D976 + DDAFB55FDFD3D536FB2A977D04C0E4EB9E2F0902938E6D46BBE6D8F6DC01B4F2 + CE62FF33946DF59CF2679B6D7D35097B26E43F0B6DA51145CFDA76DEBE2F4736 + 5FA445FB4E8F9FB77C7F169AEC58BCAFB87E062AEA373E03752C3D0BF31FBB72 + 3B63806C7BE64F9C5CCDCB409FB567D8F9BBD69F8769C4347CD9EE41DB5FA499 + BBDF4BA3EB6FD3C826FA348DDE7893A6AEBDB2EF92DDA72E3D4D93671E58DF79 + 87B5E90291EAF93DF4FE8E7BFE3055FAC3B8428017690CA23FA4E53F9FF502E0 + 18F14B5E84C0CADB4DDE4E01BE11C69216FC55C4AFC6616C359E24E2378AE59F + 33003EE73671CC558F2BD9AF8FABF700B7FD30F9B2D2797CAB712A5BA58780CD + 94DB78388EB351B36FE2552DF93DBC39AF4B71AC1EF310E0573AC01F3E59E2B9 + 805AEC74CC1A0093969DDA6ADA645E316F73108E65E7E263F692DFD306653A2E + 91B705FE4600AC28609F4F2D64F81B01B0A06F0FF8952A68DB239FAB6500CC96 + 79E2A8F5E3A8DDFB4E735FB7CF73282CF9DCD42BEA9DF765F95C3344F4F6E49B + 62DD7E366868EE781A99B37CE6E0E387D388CD1F47990BDAFC9779EF992B77D3 + 934F7E9236EF7F96A6572EDBDCD9EA4FD83C7CF2A8CF8D0F4D30AFB6B93E9015 + 301BE06DE39B2865CAAF9783AEED621B5DE9D686397FD601002ACB29F7A4A36F + 005FC4FE217003602D5DD7C9D0D9FAB243761EC52642DC1D2AD1BF1E013C09F4 + 5D74C8EBA0B7B2ED02C04DF42F0A3EABDACFE5BEAB522E29BF56AC83EA7ABBE9 + 43EAD692FF2EFAF13E44757B1F22B5599F6B976DAF5FB85BB2D9AF3EB4FE0E1F + FB3720F9F8A5C69F1EF841A301BCA16F9DA2A66D137C03D651DB7C3CA92F768C + 9AF17CC3AAE16F543FDB3A7FA0F60A6977B3EBE4617DF2A49E32F601B5049DF6 + 32C706C60AE6BA5DC88F8A36515DB603148FAB7E44F82B18FCBEFA0E00FF2CAB + F30B4C7E7D23F938EAB9390670BBE3A6596E8CF1E6888DDB21D50DE57B91DA88 + EA7A18D4AA6D3FC643723F8AC7ABE5363AC7AA1EEAB51F3CF0E8CA8F75F62BB5 + C1BFFF3420212278C43454F619DCC5F26F423AD65ECE5183A9FDA8AB9D2EBD4F + DDDAFEC0B06988C1B60DEC0BBC1500EE04BB01B40A72FABEE5F7D847F05BD5EB + 52D316DB32C9F1B4DA8F36A1DE5EA47A5D131B9545C53A5DF975BDBACD5A35FC + 957AECAAC920EA01C215C8ADE591BD40DD5D40B02280D96A021D27CF51EDE49A + 093E937926FB5982AD7B55AC9BA380A5DA39D01E478E8B5CA706C0527632D4EF + FF8D365D79805E343465E52601E1087E23006E9541B01C4BF40F1B87DE0B96BF + 783E8D2C9D35D93196CF7924024EAB4396EF8E9DE024C2A9931D3AD6565186BC + 390D04DEE10473E757969C5D87CAD2D01E85E08EA15C16E576EE18B37DB64511 + 08A3D8B684632E3AEBFAC9EB1509BE6ABF06C02A8F922DA23D390A072982DD7E + 02D86A69E7A8B8E473CC632BD81B21B09CB0B2551E8E58DE59C892CFD3E73304 + 260A78F4949DE7C90BBE05FC3AFC3D6DC73C73354D9DB9E6F077FA4C76786BA9 + 4E0030CE6FD2725647A7358E61D2A8765ACB992D5B24DBA8E8E08E52DD26FA0A + E1F077B54E7F410039F00504542E47BFEC643BE6ED910FB468A12EEFE6E41D9D + 92BFAB13C01B003079F1BD9E82B7AD325841199E64C0C2BE0070864CAA97DB50 + 99035F96EFF40860A2B3EC58BC7F15A804FCBB0E38BCEFC06E71FD89E9415A30 + 11D14B14F0D17BF7D38597CFD3F3DFFEF5F4A7FECEDF487FF8F7FE387DF917FE + 28AD7EF9593AF2E0415ADCBE97E63701C0F7D2DCFA43877E00BF795F5618209B + 419D94211F701460F7D8F2B06F216204C02D3404F2E577E67A24ADA501A82D04 + B463030639F6AAD52902E4CE6F3C4E0B9B4F6C0B147DE450D7972926CAD7C4FE + 3891934493DE7CECEFE1651964226CE98FE0625EA699A858FA2DB8C89672DE9B + 9BDF9D2BFB9D0092F3E0BCECF3B891A1357D569BBE14F4E6B3B4B8F5BC117D99 + BE9181EFDC5A794F7079FFAF8356AB0F88A4DD0CC0019FD63FDE17BC6A75D75E + A7B95B9FA5A5BB5FA5E57B5FA685DB9FA799AD4FD3E4E66B2B7B669F334B0CDB + 795ABDC9EB4FAC5F5667F56901CC3902D8FB45847205645D9C97C3DC28CE1F71 + ED903EDB5E9B0CCC899CE65AE5EB954539F5D8724E59F95A717EEDB5CCE2BB9B + AF5FDB4616C78DDFADF6FB646D3888353BEB9740ACA03091DE44523BFC0508F3 + 1DABE4DFBBB85F00305BAE13D1B92C7B9E2170FE8D6297CB729D59A29C1DE6E6 + 686AAECDA4F56DD4F2898AD7F75B9F3391C000351D93485EDE9D4DC4EFD4B5C7 + F933B4CF6FE28609F8BBF1CA7E9FEFECBBFFD63EDB6769E2FA53DB165DE5BC7B + FB2FE5FE707DEC33E09A7994F2DD810058F727E4CB9C9772DDAB544E5E2C978D + 245BDE69CC7B820569F9738E2282E3BD987DE5E95E5DDFAF6359CC537DB6EDBD + DC9E255646BEEAE85E8FB48F6D8C00D631A29DF601C03C7778FEE819149F4DBE + 3A867DAEC05D97A5272EF1ACE00F499C1FE7CE72CB5B764C6BF78CD987E725ED + F9732D3C37E3734FF93C1B494B0E64B1B57C9EF140E889D3B7D2F8293BEEB987 + 69ECD2F33472F9751A5DFD224DDDFD511ABFF7A37468EB7B69C83472FBCB34BE + F1BD34B5F699DD33DEA4D9CB2FACEEA334727A3B8D9CBF65DFD52D7BB602C479 + A542E9831D6798B1C6A93CB690E238447902C013442613B9FB9E0098FA4A37EF + FF05F69628E07AECE763AFB08F3476CCB271CF51C68E190803801165CDD8ADD4 + 611BF705802308F6F1B0A5E94F1E03B7F057D218D6C7B52B36A6B571AD606997 + F8332510580058E3F15A83F223F8F5713DE3701BDBC7F1BD6CA59EFAC536D661 + AB57C7F4AB8B741EAA1301701708DE0D00D71A2A4B40774991BE12F3C5591B13 + AF3FFC34DD7DF97D07C1E46B0E493ACEF3942FD5734F29DAB38DEDD46D645999 + 6D876C2ECD1C7B8436CA9C98ED21FECC3C71341D18B1B9FC18EF08B6B9ED38F3 + 794B5BD93036361F3F386DF3F699FCBE5FCDCB05736BB01BEDA2AD6C641755E7 + 6BBF96DA896D79BE6F33F4E51DC02882DCBD28826381E07E00F8E004C29FD202 + DC6E2DDAB5469606007BF46F5BCE310E20D2A396870F0A8D2C64C57DDBC625A0 + DF1700F7535D5F6DD4FBBBD5D98FE433DC8BEF702FAADBDB8BBAEA2A6FEFE749 + 9D6E291AB8CB9FBC9B623B5DE57BD64F19004B911FBC0F736854EA2AE0AD0970 + 8B8AF61F45EABFB55D339E9F8222F4DD37E0DD4D5D1CAB4BBBD975F2B03E7952 + 5D064BACA0ABF3C692B703EA2295D5DAAB5D1FC5E3AA1F805BA53F54DF01E09F + 65F5FB02EFB891BCA7E24DCDD2DCFC7ACA4A79BF1BEDAE0038D6A9ED0694D5EA + 7A2044453B3D20A5E641F775FC6B698F6D7ADFFAD8C507721C00C4C1C320C53A + B5BAEC515D26C83B6203D0059B449E387323CD2EDA84C6CA587EE49B06C05DAA + 074E3A8F5836487BB58B52FB51FB29779B0111C03B007081B8117E2207AD0500 + 3775F60980F9C76DCFA487B44F3CC2310668477F3A44599ECCB4B6F544A72E97 + 541F75D9C5FAA886BDD2F0B44D16C3BEB753CE7BE7C4D0DA2EFB02B844FCF65B + F2997F321FB2092DEF3BCAFB79F25B4BF6072CED13ED99DE49B7C4843A4FB47B + C16B0D54B5BF9B648F5AB8AB09BD4DCCF9D7F61CE9F65802C0516A2BB74B5E0B + 8001B13A5EA3E00CE11D5082BF9D0AEF03EE82C043F317FC1DC000DDEC84B998 + 46972E7AFB07A74FA6B1252BC739B2702A8D1DC60976393BA40A6815A86D443B + 05EE027F73342FCEA98B29C3DF2B96B6367C3F3BBD242DAD070056FB0E8069CF + 8EE94EB1E0188B8E32D9C7FCA8789C28CEA771D475E57748007810FC951C0097 + E3AB4D498E4344040A128C1D24815EC15DA559C2594B3EAB4CE572AC46C9E93A + 71CAEC8AC8EF71CA9EBEE15BA277047BC74E5BDF91DBB31CF4B534736E35CDE1 + E8C631EE4EE502810BFC9DBBBCEDDBE8BC565A8A4E6A39B3EBB268AF7CC15FF6 + A3C39BFD1CC99C1DEFA367CCA638EDE5B097D33E3AF3B50F20906219CA75C907 + 4200138014BD0018D083483BE4351BC15F8161D5F188BE004404459AA8380112 + 93604DAED34A10788AE35DE69DA70FD284A27FFDFD9F024E590EC46EE4A84904 + 505BDA789296366DBBFD281D7FF4345D7CF349BAF1C5F7D2E5B79FA6732F5FA5 + 934F9EA5C3F71E79F9F2F6D3B472FB795ABAF5D201E59CC343DA02BC153809C4 + 45E198C038C15EF541902E2A433E0035A0AF6D1798CAD6E12020F3660B8049CF + AD3F72F8BBB8C5BB6F89706D01B047DB9A484FD8F127007DAB4FD2FCFA8B34B7 + 46B4241034471B3B80A34E591619E508D60C165BE898A126E5BEEF00ADBD061C + 9B4863F581FE48F36B76BD379ED9393EF56BBFB2F5C2F4AAB4C57B82ED1C2CBD + B8FEDCB70E66D73238CD50357F7EF3AB76FDD75EA699F54FD2F4E6BB34B3F579 + 9ADBFE322DDDFB615AB8F3FD34B1F1591A5F7F9DA6375ED9B5799516ED983337 + 881CE55A3C4FB32E3E43C1593B0FDAB7F3F0685F0786F6395EB3E335C7C5AE3D + F70CB515D9CC75C9ED90EFED057B96D09E27B2DAEB64E573CAA057EA02C01990 + 726DF3B9EBB3503BE4EB7BC5567F2AE0BCF2B93DB17CFB4EB02C725396DFCBAC + F6F2F7D37E374102B3C83F5B5384A81994F31BCBC0378255C14F00F17CB19180 + C51E8DEEE76EDF15FB2D035D339005C2F2BBA67EFEBDE4FEDD4FE357F813C3A3 + 3476ED491AB7CF8EC8EF71227E373F490BDBF6F9DBF760F2E64B2B7F6AED6700 + 3CE9E74CBB76CDB89EDE37F6EFE5EFBC5F6BAE33BF5164F791CBFCA9C4EE8797 + 37ED7C32B8D57D4960B706BCBA7FB1655F605CEF92969DA47B5E94C0AF24702B + 30AC951BE23D3CDAEB9EAD7B79DC577BD4ED02BABAC7CB5EF9E4018195AFFDBA + 3E7913B69DB964E761CF403DEF780EF53CCB00C3F659A3FC2CE19C75EEF90F3D + 806100F098EA99D48E9E814D7B2607CBB6EFDB505E0BF83BC36778D9BE7F67EF + 589EDD0F2FDBF7E5C6BB7468F58B347CFB2769F6F51FA499777F94869EFE4E3A + F0F057D3C1873F4923777E9C86D6CC06F87BE1659A3EFFDCCEF95E1AB5E7217F + 8A1AB363027FF52C1EB1B1C4D0091BF79DB4F1C2E93C96F07701DB580591CE79 + 190A8F9A264F585E59BE193530B70060B68300B0DEFD2B791B05043B0CAEC65E + 6CEBF1196AC78419008F1D631CC798D2C6CA26C06DB4ADC77A5EB7805F1D5B10 + D8FB617D2202B86B8CAAB1AB8B316F80A44A3763611F27DBD6F2FD4F9BA6F60F + 9CBDF56AF02A3978ADC6F33EAE2E4096ADC4BEC47E53DFF6F332CE36A729F9B4 + 43442F4059763DC72CEDB0AF3EFA7E99E730071AE15C4C9AF720BD03180D02C0 + 2AD392CE512A535DCFB7B917F3C509BBAEE76DDC71E5D6F3B460DFD1A10AFA4A + 11D8D673CE5A759D7EED649167B239F6A1E92336D73B9A466C6E3BCC92D0C05D + 2B1BB5BE8EB05AD484CD81C76C9E6D5BFE103DC46B8E7CFE9CFD0FBC437878D6 + E6E01E696BF3FB30378F40967C01E01A1677D9C4765416F7EB7CB511F33CBFEC + E32318065617082C90DBA516D266102BF0DB5B37FB336A1D18476D1B757B51ED + 3B8081BF0B26B626CFB77AA56E0F001EA0E8BBFA9800F87D541F73BF8A7ECDE8 + 23FCD9137D1FAC2E7FF26EFAE0FAF8B2F1D77F6C5F7A51F4E577E547D5FCA01F + 73D8934A5DB50D0076BE512BD6F968B2766BC6F353D04787BE519D20B643BBD9 + 75F2B03E79525D1600F00EBD2FD0ADEDF758BF619D21ADE85DED7F88BE03C03F + CBEAF7056E6E1C1D37C10F50CF4D35DCF03A6FB68334E066A91BECA0B25AF5C3 + A0479453BF3C9080BE4336184282C0D9AEB5F9688A6DBE67FBF1811C0700F540 + A69F629DA82EDB5AD831E0D376D406C3C76D42BCBAFE28AD1CBD64E7431FFB1F + E39B541C20C6FE0F92EC24EAC7016E2DCA54DED51E926D571952B9DB585B0DA0 + DD4D40D400717B2707A5ACAB1E521970B8D4EF116D9409532326244C72266DB2 + D3552728F6A5A74F7B90EC1DC486FAB56A5B29DAC4F2087925E06F2700A65E33 + A1EB9E1036D07600003E386993601310384F665BE8ABE8E05E006CEA98744BED + 849B897C2F801DC6B110F6F7AA16F822EBEF9C4DD81DFAE6E3E488DAD319D486 + B6EBE323FA5003E05AD8C819E2F6B305F49A4666ACAC6874F65CD69CA58BC88F + 00D8DFFD3B77DEB707AC2D74D0DA75186C69FA968F77268DAF5C48E3872F6447 + 143A9CC1AB1C54A481BDDE866D057FA5361218F85B00B09C5BA6DAE9D5E508E3 + 180061226F731E11BE6C7BEB45C79DF2C993934F79B57AECD5CE1E44546E575E + 935FDA521FA2E47CEC078015F1BB4325BF5F9D28CF2BD0B75686BBD71AB18F53 + 76E40420588A8EDA6C2F608C881606FCCAF18C887802F8B21434F077E1EA1DDF + 2A1A0AE19C9693581057CE6977649FEB5DE6B2167611FE6A5FEF3016FC659BFB + 6EFDC4E16E7972DA0B2A481132B095435FCE7F39FE114E7B77D217002C91A708 + 5F8160015F2430DC0B80AD5E25015FD202258013C149C127C15FB6E4094C4DFA + 7B77F3BB7F1B4065F5324C03BA0283B2A6AF01ED9EA595AD976969EB595ABAF5 + 241DB9F7349D7CFC3C1D7FFC341D7DF8D8E1EF99E72FD389872FD2E1BBCFD3E1 + 3B2FD3CAED576971F385D57D91166E0229018805E25DB763010C1D5A593FADEF + BED4AC5D8B4922180300EE1210D181B4A98E00F6F32C80706615B86AF90100E7 + 08E0A70DEC8D11C0E4CD5A7B44FBEAFDBA2CB90C0466196587895C27DAF763B4 + 0018789821708E665634B0C0A24348A261CB75F6FED979B0FC7484BFEA9343E0 + 023FA9B3B269D7FBF6276979F365733C451B038301C52883CE1668B21CF5FCFA + 9B34B7F9699ABBF5799ADFFE32CD6D7F95E6EE7C95666E7F9526B73E4F535BEF + D2ECD65B7F3730D1C2F99C01C26FD2024B465B7AD63EC3991B19D0D2EE5C8988 + 457909633E9BF67CE9139A5B7D66799401ED0189F9BC24CEAF1B00939F457DC0 + A3C079AF2C6FD53EF7F2F90380F9EE006FF91EE5EF4B7B2CEDB3CD7F26A04FBD + 9F1FDF7995F9F50F75D536C793D417D2FE8706F228A33FFA7D957CFD2E5186C0 + B7AD8CA8DE7B6991A5D2ED5CE688C4BFB69D26AFDA6FBD44A2CF99389F894B19 + 1C7BFBFC4EAEDAF7B0FCB160DAD213574CD71EA5F1EB4FD28869F8EAE374E8EA + A374E8DAD3347EF3759A5CFF244DB9DEA409FB4C27880EB7CF89286F7E7B1962 + F33BB27E5B5F16D6EE7B54B8DF57EC98006F7EA77E9D3D9AB9BD1721FD2985F3 + 12D4555904BBBA6745295FF6B568D7EB15C81BA380254160C156B6DCBBA32DFB + BA9FB3D57D5C79A8BD97B776CA43F17EAF63C5E7400D80751CF2A6EDF930CFE7 + 0F002ECFACF8EC029462CB7B8B7D450A964CF66749EED7A8E5513669CF50A0F0 + 24FD28CF463D27FB8967627C9EBAD86FF2ED9979DE9ED317ADCFF6FC193D7FC7 + FAC2F7FE5D9ADAF8611A5AFF2A4D3CFAEDB4F6077F2F3DFEE7FE51BAF0BB7F3B + 4D7EFA87E9C0A35F4D43F77E298D6C7C910E5D7993C62E7E92662EBFF2952678 + E7B5478BDBF98E733DE827C7F3676F5EFEB9FD339989677711761A778C9A264E + 5A7E80BD02BB02BFBB4951C2480078A26C5D362ED2980BC5F1613B5663DCC6B8 + B085BFD2E8D13CF68CE34DC68D1A13C77C00AFE06F0D80515704B0D70BFB8CBB + 81A3FE0A950273EBE5A07B2280CB1C81F4A869843F726263E511C04AED1F2E7B + E5E763F5A8D3056EEBBC5ACA5734711EC3E776D93A1C9E3FE311C282C60D10B6 + B904E7112528EC00D8E65902C035F4951AF8BB687324B6150046CC6F80BBA459 + F699347345B663D63FF2989345384B99E67DEC77CD3595AFB414EB45D56DF0AA + 227F25CF8CCDB3676D7EDD805160EEF13462F34D20300078DCE6812C0B0DFC1D + B1BC21DA644E5CFE889E61ADCDFB0BB40408D780577373CDDBBBE6EFCA8B00B8 + B689025CF7DAAC58FA70506F5BC05F4910B70B02EF84B4DD11C0B57FA457F870 + 16F3D2CD7DDACDEFFA35017F27166C1F597ACCCAEC5A66286CF58B1AC81BF262 + BE968146F8AC6AFF98CAA24D4C7F0CC563C663BD8F6A9F5F97CD6EFA90BEECA5 + DEDEFA4679B7BAFCC8FDD4551FF5D8C8CFBE8BFFBAB10BFEF68FA9A66DD45116 + D5D8ED437BA9DF532E8E1155D97F1C59BB35E3F979532788EDD06E769D3CAC4F + 5EDFB6C8EF0367056EA52E1BA9B6DDCDBE430DEB0CE9EF0070D17700B8E34BDC + 73E378BF1BA154DF087BF6C30DAFEB86D973A3DC879A7AD50DB56E4FFB9E573D + 0C7AA43A96D6C34DE017D50FBB8FAE3D3C08D5AFBA2F313FAB1D00C4414C3D60 + 88791F3268A22E51BFDE06ED0FCDA7399B949C3E7B33CDD9E4C3BF5FE5181F72 + 9C0F557D7C9D7BB48952792DB5D34F35FCADCBE331A24DBD8F682B0FB46D10DF + 4F21829777E9C4C9400D3D1DEE56751CFC4ABEDFD6EF5100BF3D1322E068B453 + 5B31CF54F7256A37BB58B657BB08801B805B496511F6D60038D675FB32C9CBEF + F7D135A8216F9798BCD6CA651100D7109808E07A225D4FAE0FCE983D6076E6B4 + 2947020BACB21D0480B1919DCB7EB3870AECCDC7B1FE3551BF190047D08B1429 + EC2A51C83813D4FE20008CEDB06D7BA0AE4DFC7BC0AF696CF65C1A9F3BEF1A9B + 653F4B360E8179FFEFC279DB5EF0AD00F028D10A2B97DCC932B67C318D2FE340 + 3ADB385414197000674EE5A83AB8D40B800F2CF0CFFF2CF615019CA3804957CE + AE22D2AD238EFD0B6994480B779265B9530CDBE264CB0E3BA06AEBC06B9C6726 + 95D7F951EA87B757DA14D08D91BE3560452A53790F8CB5BCDAE1D84FB1CD28B5 + A736D51FF655379F7FAFFCD8D429C057CB2E6A29E8EC94B5BA8DAEA7D113AB3D + 1A3B79D3EC6E581DEA028A6FA489332C132D65C7B220B183D7F3F9DDBF8AFEC5 + 110E0C569EA2817384507E77A19CD68832C15AD21106A3E8E476A778517428D3 + 0F96A01604F66DAC4B9D0BBD0E7E39F1330CB0FC8B666FC2113F7E61CDF7739E + 9539040158E448AD2801D90C7F32C8E9054200DAB65E04223520210D649172E4 + 60064F824FB98E8EC5D6F2AEDCB363008733B801BEF87B7A81A4375886F9B1E5 + DBF6EA53ABF32CCDAFBE488B6BAFD2FCDA0B8F9C5DDC7A9256EE3C4F87EFBD48 + 47EE3D4BC71F3C4F271FBF49271F7D6AE977E9F0ED4FD2FCFACB3479ED99B561 + 75565F5ABB40B616D6B2ECED2C7DB966FDB3E367F86BD7C0F2807DC0B65A19C6 + B12CF593B4BC9923638902263AD1015C39EF7CAEF77CB9E9C9008059027ACEEC + 33E87DDC00D769DBEA1DC05A0A1AF8EBBAC93B8001A244DCD27FDE3B9BA32205 + 801B78798348D9E776ECA776DE003BCE953A1900F3BE627F6731FDB5BECC5A3F + FCB8D6AE8E0B1016949E27CFB69CCBD2FAD374F4D66B3B67A07A0B2C735A51B6 + 80CADC1707C337795FEFEBB4B8F92ECD6D7E9EE6B6BE4C8BB77F9C96EEFF4A5A + 79F4EB69E9E1AFA6D9BBBF9466B7BF9FE6B7BF702838B3F6322DDD7A9B0EDFF9 + 3CADDC7EE7E9F90DF25F583FF3FB6317D64B1F80BDD7EDB8D7ED78E5B83A3691 + C98B7C5FECBB33EB303C83D20881F3F78165BCDB88E65C9F763270F5CFDD0133 + DFCD02DE7B54BE4B96CECA7500B51100C725A175ADBC6DDBCFE05C51DCF435FF + F940F67C67B5AFB63916F97C0F04F5F99C24FA467EDC675BFF7EF9CD2EDC04B2 + 62CFF71618CA6FD66CD8DAF7CDBFAF766C07D29C4F588E5AF03DFFA6EFDB77DE + 6C6FDA6FCF346CBFDD61BE8BDB9FA613AF7E3D9D7AF91B69FEF697696CF54D1A + 374DB3F4F7C64B07C053F65D1EBF76DFDF353C451FEDD87A0FF8E2861DD7FA08 + 1C9EB07B13BF557EA35C7BFAD90260B640DA5B56C63D897B1AE7B937E9BAD05E + 3F09F0CEF2FE645304BBF11E4D5AF095FDDA56D2FD5DFBD4571B3A16DB1C91DB + 0262D2D4557DC15F3D2BD846008CDCC604009EB37B3BAB60F0EC417A1E391CE5 + 59447DB31D3D77B369371E63E4FC4DD35A06C456C79F8BAA6FCF2F49CF42A4FD + F8C72ACF374D20EA589F47CF59B989A8E3D18BF7D2F865FB7D6CF0A7915F4DC3 + 777E2D1DFEC15F49BFF18FFE71FAE37FF2FF4CBFF9EFFCAFD3A53FFFF7D3C8E7 + 7F94465FFE6E5A78F93B69F1D16FA585AD9FA4991BEFECF37C64CFB11CB93EC6 + 35A07FE5588868633FB63DE7D194A5D164795E337640C327180F587F2D4F7F40 + 5319698D4194A77C9575C921F2511B238588E07ACCC498AF67CC74EC9295F36E + 60C6928C1F3300561EE34C8D3B7D9C68E9388E8CF90E800F5BDB053E2B22D841 + B0950143058EA394CF1F1E0F15C80AF41D3B626DF3A7C7027491036113EF0066 + F9E71E006C7911004B35E8ED399EA9E9471F00ACFD1A10EB38CA738563317F88 + 695F6E9A08E1F9330E7EEB08626FD3CEA506C2076DFE15977F966AF82BD5E037 + 2ACFC13204CEF3AF0C7FD9676E97E773799ED72F1DD595AF3C29CE53BB657598 + AFFADC79390DCF1C49A3B3C7D208A0D4E69A40E0D17900B0CD85278EFBFB7EFD + 9DBFCC9535DF3629FAD72380CB9C7CD8DAA17E84C05182B6D2A0FC98DEA9152B + CBC7CCEAADEFB037F405E81B01700D8163BA86B6F57E06B83BFD2B59C56E3700 + 8C1CFE16F594B19FF31CF48E5A7A6421435EF64D0DF82D8A4B40D7DA0DCC46DB + FD68503B75D97E55FBFCBA6C7E1AAAFBB57BDF28DFA9C1FEE3BDAB69433EF6E2 + 771FE4CFEEF1C97F0D00B8E943473F7AFAB847C5FED6EAB247BB957F54F53010 + DB46CEF353D0CF6504709DEF22BF02B6DAEF926CDEC7761739E70C691401B0D2 + EFABEF00F0CFB2BABEC43D3791F7BF5175DDE87AF675730A90364AB65DEDF493 + 6CDDBEA3ED587E9073DBC74366D043EE9B54D7B1FBC168D9B66A0700FD060C5D + 791F2206792336003D346C6D1F984943B61D9BB081AE0DC6F87EB104F4C71898 + 7D88BA0687F5F9EFA58FD8C4416D97B053BBECD7F6757BFDF29043E0026B772C + FBAC259C0BD015008E9053F209021387605F035F6F5776A69E3626F3924682C0 + 9AECF405C052286BFA11F66B09CA46C5F2417563D9A036A2288B00180900C7BA + B4EB69BB06CDB933192C107777D98439E8804D6E1525ECCB5B9998E846084C99 + 0070AD66424D84B08965B57807707E173093711C006D546E04AE713F6A781E88 + 9AFF899EF3B2E3206A68F6944D6A03F04561E969F6E331BC4D07BF38234A9FA4 + 624724B1435FFEF13D77D6F74767F8F77786BF02C082C03D00B8C0624501B710 + 18270D8E161C37A6C5EC481A5DBE984616ADCD65B3B3ADFF337F2903609C3772 + 10B993CBE4CE23EA7BA4EF4507C1FECF7F1C39B425A7978360202B8E379C6B95 + 23AD488EB4318FE2C07166C72B8E291D53F5A2F3CE9D566627453B14EBD6EDD4 + 02B2AA3F82AE11CC4A82BF2A8FB0963CFA26A763ED7CEC11F91DD2B2CEBE9CB3 + D9D117F2E550D5D2CF5DF65E5696588C105811392CE39C551CAB0E7ED782561D + 0213098C633647FB0259B1B73E7B544FEBECC5018D835AE016902BE8AB7D60B0 + F6E5C4262D6123A9ADB8DF554776488EF089D216E0594B714651368913BE38F1 + E5FC8F8EFF89027BB3365A20DC40E02DDB76C35F4159447E2C632B081C23E304 + 7A05919497978B8D91766DDB19FA46184CE49D2284B1275A6F3B013559D67886 + A57FAF3EB13C60E6EBB470F38DB5F1DCF29EA6E9EB2C9BFC2CCD02D05896F8CE + EB74FCC1DB74E4EECBB4BCFD3C1DBDF7693AF1E0CB74F8F6E76961CDEAADBE4E + 3337A8FFD2DACD4B0B7B143040F6869DD775FA4F0424A08E084A94619BC4BE04 + 8003E4E568D727BECCB303C502E032A8CBA00DCD583A2E013D73D3DA03F6EEA2 + 060EDF789C238101921CCFF21CFA15885B0357817220F0CCB51CA12B484BBF33 + 7804245A3FED180EF6D6EDBAD0AEA505A61591CC3B803DBF9CF702EF232EC753 + 842DA0D541B51D47D1B780E8F9D5E7767D5E5A9D37696EE3B334BDFE459AD9F8 + 2A9D7CF1BBE9F1EFFD71FAD5BFF16FA5EFFD957F902E7EF147697CF3CB346936 + B3B73E4FB31B9FA4A55BEFECB3FC9EE90BFB5C3FB5BEBCB43E719CE7BE1CF63C + 401C58CBB95D076CE728E41CF11BFA77D3EC01E2DEB7FCF9B04C7506BEBA1E2C + 2FFE2C80ED160C0BB8E674BE6ECDF5E3732E222FBF0F3AB72DB84BA42EEDE978 + 6A93F2765F4B3CB79F976CD40EF03A7E0F51EE57F8CE99DCAEFCEEF84E3AECC7 + 863EFA9F0600C194F33BBCE5DF7DA02F0078CEB6F9379C212A36B3663BCBBBB3 + 59AEDC8EE7409BDF4181BF8AC0CEEF25CEBFDD39FB0ECEACBF4AA3F67B3B78D9 + CEEFEEF7D2D33FF5D7D35FF8D7FE83F4DB7FFBBF912E7FF6FB69F8DA2BD7CCD6 + 3BFB5EBC4A13BC57DABEE3441A4FFB12CF1900CFDDBCEBF07779CBAEED7ABE0E + 5357ED777B8DDF2BE7C6716F37F7BAE9EA9E94EF3D6D842FD2BDAB96EE4DB13C + 825F24F80A90159C95D8E7DE8C48EB5EAD7D0160E5ABADBA8EEEEDA4D536E53C + 07941FDB274F505669B5A1FC98E7CF97F2BC8ACF1A9E416CEBFA5EC78EC33368 + 9867B56DBD9F76CD29A75FA3F64C03E8D6CF3D449E9E794AA3088155CFFB64C7 + F3C8637B9611513E7BE385FF7964E9C1AFA4A567BF9716DFFEC5B4F5E7FEF5F4 + 37FEA3FF4BFA0F534AFFE0FFF6FF493FF8AFFFE374F4B7FE561A7FF3A7D3D2DB + 3F4CC7DFFC613AF2F037ED39F1799ABCF2C8FACF6A18760DCF6DA491D3365E60 + 2CC1B15D790CC2586412599E2030D1BE0DD0D59FCEE8B7F28A22E4D579491C6B + 98F2224161D40380353E626BE3281FE331B6B3312369E50179274E3066238FF1 + E16000AC76629EDA1A3D62E9727C01601F03339E0DAADB214FDBFCA74B1BEFAF + 989D0365B3A9A2814711A03740D2088005531DA8026919AFDB383E8E95637F10 + 797EEC850C6705743517882037B6E5F9C596E3453B07BE26D979B9DA0C00185B + F5D7DB090098F33AB468F3229347F79A047ABB00B0EF07C85B8BFC66DE56EC86 + A853EA1D321D2CF3C838B78CF33D54E7F7B3555E0B7B77CE5DB3ADCD7101A333 + 80DC0C467D2531E02951C073276D6BF6BC26C9E6C43E7F666EEE8018B8CBBECD + DF778058DACBF0B79F62799CCBF7CBCBC0B75B5DF63A2FFA93FB6BF37AF203F8 + D516382BF81BF322B46DD21EB59BF39A085E07BEF279E47AB28FE92E1DB4BA2C + 01ED8A753D123868042D648D5ABA4302C0EEE7329117FD5712FBF8B76A9F5B97 + 642FC9BE56F4B3ED66BB57C92F2875D97C1DEA77CC985F2BDAED14E56D3093F6 + FBFB8EF7A7C60F8D2FBEF8DEDDDFDECFE7AE32F9EBF7E19BDF97FAB4EFFD5319 + 8AEC20A47B18C21ED4B411D493DF877F7C14F53010DB46CEF30D2BBEFB57EAB2 + 7B6FF585B19576B3EB2ADF6B5E8F280FC03642DC2895EF6627D5F6BBC8396748 + D7EA82BAFBD17700F867519D37882E75DFC0F6AAFA06B8971BA2D26C2595ED50 + FDD030791DF5BB94C7B6A2E2036037ED78B07D438A0FD5787CEDEB01BEB78778 + 1C00740F66BAF23E543E60B3C12211C0F93B65E23328E5F5E0EC630DD6F6221D + AB3EDE7ECF3FB63348BBD589E55D8AB65C57007084BFFD007084AE82968DC817 + F4950AF0150C6569A51EBB501FB87C5051C04C7AD0C491B6AE6CD50729B4514B + 7523A015748D52198AF5EAFCBACDA67E99B0B9421DCA22F8ED52DD86435BDB6A + C2D702DEDDC444B6D501DE6F346E6DD8B679C71112042E51C00E826DBF9E4837 + 93EC0E00DC46E1B670578A70B6D630CE08D7B97468E17C3A387FDEA1EA089016 + 67C182B5BD689378DB1E3091373E75324D8C9BA6CEA40996679E39EBFD383487 + 43E3823B380ECD5BFB382DE6EDD8761C49F0D8FB6B12088E40D89782B63611FB + 63D6371423861BF86BF2770317082C00AC7703E33072870CE7B3606DE0582AFF + ECC73123C969149D521E41BC5CDE018C1D6D0380717A59B93B7E7048B114F411 + A06976CAE1307387973BC7AEA6A1A338F558D6772B0D9FDA4E074EDC4A074E6E + A783C736D2A123AB69F8C875B3CFCE3839F4E43093238A7D39D610368D632E38 + DB941FCB01B7BBBDEB7710008E65B4D917FA9A9ABE998DC0AD5F13F64D5DA0D7 + F74FDDC82AF951B29F3C95A37FB223B675AC6A1FE7ACE0B097C9097BF26689FE + CD7515553BE9CB55E264C6297BD53E23EB27CEDCE2F07607344B4C9E6D6170E3 + 782E5B2D0D2D28ABBA127992A0AECAB41F2567BB47F9DA967711F26EC7111CFA + E7D73D1ACB5540B03BE28102BCDB12906B75A2235ECE79D2530E2680BB666F69 + 1CE72CCF99F7737E1436008E7E404465759EF273846F0B7B3344BAE36089AD40 + 09E5EC3720F4EA7D7FD7AF0073934FB4AD29DB5AFA3A4BE8B294EC23CB7F6CC7 + 026A3EB57CDE8B4A04205016309A81EAE2E65387BF87EF3C4B4BDB8FD3CA9D97 + E9C83DA247DFA4E5AD9756FE2ACD6FBC4C33ABCFD2A4D59D049E01DC6E5ADB00 + 59CE0550E47030C33917D0CDEC7C7964601BD02B9447380778F34850401B70B0 + 0042076F9656E42FAA21AF406F4FBE8963E4B6D9E634C7F37CD30C5BEF438E58 + 951CBAB254325198D7DBB20C808970CD6DE4E35839C0D4E4FBA53F12E017003C + 0FF45D23AAF3A989A57A9FD8F1ADDD5217B13C33E51C873ECC5DB73C8E7FF385 + B5F33ACD6E7E9AA636DEA5D9DBDF4FAB3FFE8BE9CFFDC3FF30FD3BFF87FF57FA + 97FFC7FF717AF167FF6E1ADBFC5E1AB9F9495AB8FBBD34BFFD2E2D6C7D928EDE + FD2C1DBBF7595ADA7A63C760096C8E815AC88B80BF8BD6877C7EA50FA60C84E9 + 0F20987486B0F99DC680EB606FE7B86079FCA9C081B79DBF3EE3567CEE02EF7C + C67C3EF6190357D7AC6CC3DA296A413AD7355FC3BA7F5203CD4D1954E77DF537 + 83DE76696E2DCFADCF89EF97FFC180DF14BF0B7E5726BE7F82C002C182C1BC67 + D7DF816D5BE5E7DF63F9EDDB96A5D159DE7A9AEFEF3A91C980597EA32D64CE75 + 2CCD92CDF63DE5CF19336BCFD384FD660F5D7E980E5E7E904EBEF8E5F4BB7FE7 + DF48FFD63FF9BFA6BFF5EFFD2FD3D62FFF055F02FCD8939FA4A38F7E9CC66F3C + 4F23573827FBCCEC3AF977DC8F6DF7158FFE7D689F6FFE4D6959EDFC670CF2EE + 9B2DF722EE57F99E45FFA95F0360A5753FF3F30CF738DD8794EFC0B79449BABF + 912F388B621D29D669FF7493F7752FF676AC4FF45DF9F13EAD76C9F33FF5947B + BFEEFF52CCE759A17CD2522C5724AF9E63FE5C2B652EFE4454D2D4A12D016080 + 2FFB406007C156CE7B7547CEACD9B3B53CF34ABBC8C1EF597B4E9FB5E7B9AFD0 + 919FBB9EAF34FDB163D28749C036809BF327BA7FF35D5ABCFBE334FFE837D3FC + 8B3F938E7DF5D7D393BFF26FA7BFFE3FFA3FA77FF33FFDFFA5FFE2FFEEFF917E + F8F7FF7BE9C42FFDF534FEE8B7D3CC833F61F6BF6ADFD9AFD2E4951776DDEE7A + E4EFA8F565D88E357A96E7AF6D6DDCE02B7C3066291018F82B4D9DB6BE9DB4B1 + 4559E259D2184512CC05FE8E302EB2BA6380E3602B1B893C6FEB988DC58ED8F8 + AB0060C664712CA631601C1F522E1B8D15955FD71924D9A92E79716CAAF6635B + 4AC7ADE0A9EAA208697D7C5AA2823D02D8C7DCE77354B0CD2D7CBFC05441631F + CF03604B7B1CC3C7F2B465E3F42E68AB717F2DD9C6B6A2BC8F763C815E006FEC + 8F6F6D9EC11CC8FF18CBBCA70064415FCA24F62300AE61AF00B0D24D9E83DD62 + 6B6987BFA5CC576EB23C968406BE1E98B1F9E4ACCDE128331D00B49A7C6BB65E + 6673C92EB01BE79DDAEFB28BFB5DF92E3BD6D0EC719F431F28F3EB21E6D4D63F + CD6B731D9B0B9B32183EE1D1C1A47D2ECEBB6A4D2D00B6397B00BC515DF99AD3 + F7DBEFB4B1B97C4EE7630A420FCDA015EBEB61EBA795CF5A1DEBE790F519683D + 341DDEE34BDD027E057F0580A51DF0B606B3B1CC14216EADC6AEA36E6B977D27 + 192A87E38CB6520470BF28608F162EDBE8C38AC2BF15D56553ABAED34FFBB5EF + A7E81BDD8F7FF04334E898755954B4DB29CA774ABEE36EFFF1DE15DBE951055E + 7BF26B75D97D4D721E103842976A7EB017F56BA72BFFE32B1E87FED8B1BF2E00 + BB8BE271BF96E37FEB0030C2A602B85280B43DEAB28DEAAAD357D85B1F425ECD + 403F54DF01E09F35C51BC4C700C003EB07C92ED8EB2679C81E5628E6F593D72D + 0F077FD870032FEDC5B25A2C41CC319A763A6CDE47031F76FB951E7CE1FC22E0 + EDB109E57E1D76516E63FF83A0F71934C5E368DB3520896DC73ADF9474CC8F7D + 6CB535A85D95459B7A5F03B9580EF41DB6813872005C47ED46C57C41D7AEB2AE + 7D607281A10E766B00ACF65C56467980AC757A3F52BDDDD455B79FB017B4252D + 701B27724851CC2AEB376944F91FBDB9DCEB96A85DC1D9286FBF2A6B0170567E + EFAF4D884917081C41F02120F0745EF6AA89062E13E8A806003713729BF047A0 + 8AC3A143FDA16FD6E8C2F934B270311D5ABC9C0E2E5C4EC33317D3E8F43987C0 + 8796ECB82BA665EBC3D2F1343E7F22CD4F9C4C8BA3A7D2C2F8D9343F792E8D9B + ED8159E0EFA534B674D926FAB63F6375717EB8B233443A50FAAB3ED327EC1C1C + DB36DAB28F9344EF0AEE911DC7E16F01C10E7DDDD172DEF347E6F37BC470EC68 + 293677C014678DE72FE63A887C8F402810B805C138A7CE97E5EB70F26447CF21 + 9C46D6DEC1C50B767DAEA5E12337D2F8B1EBA62B5ED79787B6BCE1631BE9E0A9 + 3BE9C0E907E9C0859769FECEAFA613CF7F3FCD6CFC201D3875371DB0F2A1A344 + 55F43AD2909C6372A2A95C8E3DD2D1460E3A95791B26C15FC15C015D01E198AF + B23ADD6347DAE4FD3109F0AA7FCA43D4439409E646B0DB036D07283A545BE89B + E5E5A73320CED1C1B93DCADC895CC4BE47119FC9C2D9ECCE65AF77DD9DBC724A + BBB3D9CA01C0B5A31A2738F05511C1EE142F65D196B4F6A7CE6FDA366BE2EC46 + 8FC64FAFA5519CE266C3FB13C7B12DEFE2F54824ABCFD6DF4B581CED72B613D9 + 0B009EBA8CA33D3AF9731B7A3FAFDEE5AB3C8EA3082D80438EECDD091C2208D1 + 7E57BE0084EA090093EFED59FF14450804561B11BC38F8E0DD9E268F1ABC9EE1 + 6F1B959861D4F4D5DB1EF5E73009B074C3EAD14E595E57D18E19B402DDEEA785 + B58769710BF0FB382DDF35DD799A56EE3EB7ED33CF9F5F079465083B095C5EB5 + 7A9B4FD2D2E673078102AD8AAA245A3343B5870EDC102078DE975ACECA603603 + 59C4BB68E7AD1DDE913A7DB32CE96C12449D030E3A3C6CDB10DCD3F1631979CA + A74DB72DC79FBBF9D4DA7E92A6AE5B1F7917EE9A00EC5387B288A8E109DE9F4C + E4305011B0E630F45963A37CC02470D5A32F4BDD0C7A73DB0E7FCB7E86A4AF6C + FBC2CBD406DBFCCEDEE71E45BBB46EF7C31B567ECDCA81C100E0AD4FD3D4ADCF + D2DCFD1FA41BBFFC17D21FFE83FF76FA47FFE4FF9EFEA5FFC1FF29BDF98BFF62 + 9A7FF0C334B2F126CDDE7E97666FD9D68E7178FB753A72EBA5835A227917EC18 + 82A8005201549D5B4CFBF2CFF4B7639F2DFD644B3433D791086BDEBD0CBC75E8 + 1D202EF988A5BB05F27B97F1E6FB153EEF22A079BE9EF9FA0862777D5EFC4101 + 1BF6F9DCC8D3674BDFD41F01601DCB97F0B6EFF6D48D7B2E7FC7B4F585DF8860 + B02297F3F7DCBE1326FE5801A0CFC0397F9FFD776575FCF7E99098E84BFB5DDA + 6F6DCACAA6CB6F1698CC96DFB520F09CFDAEA62C6FD27E0B9376DE13761E6376 + 5DC76F3E4B471F7F953EFDB3FFF9F497FEE1BF9F7EFDBFF00FD3E5CF7E2F5DF9 + FCF7D3D3DFFDE7D2EA57BF9FA6ECFB327A85DF1911E3767DAEDBEFE19A1DC7DA + CCCBC2DBD6EF0F77D3E495BB0500E773A17F80E20CA1B9FF68A5819257A47B4E + CC438059A4FB91EE6DBA5FEA5E16FFF482E2FD51229F3A92EAEA18EA97565660 + 4B1F04DFF32A0D76BF2FF75AC168BFCF96630AEA728FE77920088B54860468B1 + 69606F7986B0AFBCFC4CC952BB1ED16B928DEC94960D5BF2792F30CFB6B1F376 + BC73A6B33C27DBE7AD8BB6FCB95C5EC350CAFC196ACF34DA6129695FA2B979C6 + DD4B6397ED5E76E36D9ADAFC411ABBFDAB69E4E1EFA5F94FFE725AFBDD7F257D + EF8FFF87E937FEE1FF367DF9B7FEFBE9EAAFFDDD34F5F04FA6D1AD9FA489ADAF + EC5EFF491ABFF43C8D5F7C64EDDE4ABCC378D49EC743A76C4C60E9317B5E8F9E + BA9923721953F0AA08C605B6F5F184893FA745003C79C2C61D2C031D20AEA4F1 + 0AF6D9CEF2B4A47325DE03AC36F5AE5D5F8299F19D8FBD48671BD2234719A7E5 + 7CE46D1CC9D1BA48003947F09A2D63C232368BD2F88E721F43963126658CF3C8 + 13108DB61A87C63A5DEA02AB48757DBF8CD325F625C6E431AFCB4E363EBE2FC7 + 602BD02BE81BA170B493EAFDE61865BE2290CB5C0339D42DF31881DE5A944B2C + FF8C6A00DC057EA3A28DB60E7401C096EE0794293F00F40DFB51CAC7C60540B6 + F9670FCCADE440DAA4FDDEB92B109ABCDEF9AB8B3F3337FBD8722C9B1B03524D + 5A52396F999F17984A04B12BCFD735CF97E27C5EF3F8AEF2AEBC26DFE7EEC7F3 + 9C7ED6F6670FDB79ACD876D93E43EBC3FC925D9F05D39C5DDF236974E9641A5D + 66BE6AE7C1BB8EAD4E5CAEBA5180BE02BFB522B0ED82B852977D93875477477D + EA74FB54E40FF2C0839E3CAB376AED041DC49F65120066DFF32ABFA0FC5F8314 + ED0749FEB3AEB27EDACD5E6DBE4FDBD287D6EFA7D86E6CBBCE8F65FDCA3D72B7 + 2BBF51EBB7AE7DCDCA8B36B56AFB1D2A3EEE6F4C9121981AD650A5A3C412BAA4 + F2BA8EF2A34D546DFB61A2BDFA3896DE0B8CDD13E0DC833E563BBB69AFC7D9CD + AEAB7CAF797B5073BDA93F48B15EDC8FE54AEF49191ED70CF443F55305C08420 + D7FB312F92EF2EB91D17A7BEC83FAFEAB931C41BC57BAA1FFCDD03148E37BD08 + 667793D7EF7A380430FA33A7704D7CBFDF796017AF6DB18B0FD3C1EA85925D0F + F63830F8581AD4F6D771DC788EFB19307E9D8AFD60396C3434B2D8296CEAEB22 + 800FF48DCAFFACCC03F1FDA8275A5862F01E07F85D12F40D79EF0364A3EABA6A + 6F2F8A750477637E2C8F00D86D0AE8D5A44EE99817F3B55F8B32E0AF14A1EE7E + E51349D211FC16A95CEF3E1208F689A74D8CBBE12F6252DDC2D408550701E0A8 + E1799347CB02492FA58373976D427DD1A37A4798C82F9E4A07964FA7834B2C65 + 76368DCD9F4E73D3674CE7D2CCE485343171CEEC2FA4A1C5CB6974F1924D7ACF + 5BFF5A78BB9B22E8ED728E286F14E70A4E16D3B0ED23A27E5BE57D2D051DB738 + 807C29B6056BBBC05E49795D8EA40883B5EF8EB4954B39ED0EA68BE9E0B25DB7 + C3D7D3C8319C702C8B77D12385DDB176C2F24F6DA5430E7AEFA703175FA5B55F + FE67D2F7FF33FF5ADAFCD15F4AE3975FA603C7B7D2C851B32BCE3139D46AA759 + 144E3239EAE43443AA8BBCDC344C5B5626910790ADA1B0402D1218AEF76B5BD2 + 02BD724A2ABFAE47B9835F93D23843A5E82495A314D551BF4AC7BCAE7C89FA35 + 04D6B28E723A0381B50C2351C08D33BA4382BB19EAB6CB320B02D7369E77CECA + CE64D5F0D701F099F5344EFA028EF82C016077985BFD898B967F79BB1B02976D + 74F24F5DCAA017F0ABEDB4D5579AB627ACEDA9CB1918B0942B4B434B9300E5B2 + C42BD1BB2CF3DA0F64083AA0166A089C641092978A6D81AFF273BD0C3C047EB5 + A53EF982320233CD1610530096C47E8CBE442CB9CBFB7717371FA7A55B4FD2D2 + B6E9F61387C00E806D7F9EA563AD1C60374534E3EAFDB4BCFD22ADDC7E95E636 + 9E6438B79AA38B014A0BEBCF12B02F2FD9AC48D90C6F056D81C10059E02C7581 + BFBEEFB0302F9F4C3D878926EC23E8A58DBC843400330338E537E097762DAF96 + C0ACA2610509C9CF20314245CEAF85B95DCA0038D7AFA57CD92D004D375FFA96 + 7D1D2B1F87C8E36769692D47E31285BCB06A5A7F659FC1DB34B3F9499ADC7C97 + A6EF7C998EBDF99D74F74FFE8DF4FDBFFEAFA54FFFD2DF4F977FF067D3ECC31F + A6C93B4408BFCDEF845D05DC66489FDFC79B616F8C9255A4AC4030E91C39DB42 + 5E815EC15FA47CA24D73FF395FAB1BA0AE206E2BF232048EF9D87649DF9DF819 + 908ED79A7DC15E95B1659F286BBFA6D6C7782CDAD41F0C048351CEBB6FDF39BE + 4700DE0C72818BFA5EF107072268B96E5C07B61908E73F3FF8EFAC28FEEE74CC + D9F21B5434B17EAFFC19C3DFCF6BF653250A789248603E47FBBE00F44F3EFF51 + BAF2F9EFA493CF7E351D7EF0A3B4F1A33F9B5EFEC17FCE01F0C2ED7769CACE75 + FAC653FF63C334EF14B6DFC0821D378BE3597FACEF2C099CCF01004CDAEE47D6 + 0FDD1FE833FBE413AD8C6423451B97D9E4777E5346DAEE872C916DF7338F82E6 + DE66797E7F2BF747DD17A578DFD3BD33DE17258E25209CC1337FA8C9F7C40C89 + 7BFF78A3FBAE969066CB3341CF05F625F655A63CD2F1B922804B5AF6CA978D44 + 5E5D57F53B0130F0B7FC198AE7A63F475D005FB305AE9ECE7FD09AE0BDC2BE12 + 873D57CD66DCDA19B5EDA86DF9C3D4CC85ED347BF9BEED3F48872E3C4943D73E + 4D431B3F4A43777E2B8D3EFA3369E9D3BF924EFDF86FA5F3BFF6F7D2A91FFECD + 34FDF8CFA4836BBF9C86D77E68F78FEFD977C8EE37579F9B1E5A1FEDF96A6DF2 + 67AC838CDD6C8B80C1C3AC4062E304FAC938C2C70A2601602458EB30D8E491BE + 0500D76312D5217A98AD003240D721AFC9216E694F80D7CB4D02BD02C0757E54 + 04C3D8E4BC767C27F938AD486348C4985380567FF423EDA0360052ED6B2CCAB6 + 1EBBCAA64BD186F7F712D9ABF1B8C6E81AA7B3D5385E6375B64D642E6505F2AA + FD08729120B0A473A8ED24B7E17805F46A7E120130FB9ABB905F4B75A47AE967 + C1DAB85F2B96D710570057E56A5B65D14E69EDA30302BFA6087223F04582BE51 + D1BE4BF53CB65B40619B27973F620B004B713FCED53597AFE7F5D82B3D483A96 + D72FF3F6A64F33875D87E68ED8677C348D2E5B1D00F0CCBC95CFDA7CF4709A39 + 713E4D1DB3CF1FE83BB5626DB13C35D077C554E0AFC923880BFCDD016D433AAA + 05B7BDDA6133B6D89B877600606C7BFD3BDAEE5081C13DBEA41E597BEEABEA10 + FEAE921E14212CE10F8B8ABE32CAF18B75D90D52ED6FABB55B79EC5F54974DCC + FB26A4E3EA1CBACE2396F5B3E96FD76A87CFBA8FDEA7CE5EF5A16DF6ABDFC321 + 3E9037C4B69A36E5D7C7C7BF0786B2D38E36A27AF37B8FD70B823B79D177FAF6 + 482037A6F7A29EFA6D04F15E19E9AE52433F0D7D0780F7A99E1B82D21FA0BDDC + A4FA28DE8CBE03C001FE16C507A4E4F9BA0E03EC6AC9B6EBE15DABEBA1FFA11A + D4763CF6C712EDEE65A0D55517459BAF63C02600DCA52E7B1423B8A3F2C0D906 + D8FB545F008CBA06F8D25E6C827CF240BA4F9D38B9A8F3F6A24175623EE01700 + DC535E45FB6A4218C170AD3811ECC92FFF00969A08DD2A1F69D2E813C70A1ABB + 0DE901009876FB4501F74E90FB03E0087E631EB03796F50AD87AD626B4E7CCCE + D444D4E2C4C0698153E5523AB878251D5AB86676B65DBA910EACDC4C87966FA4 + A1E56B6968C9F2E6CF7B7BC338354C11EE46202CC7085BF28908AEF36B09FE36 + 10D885B3057B39567AF3B48FC3663F0038A6057A259675EE01C0963EB4C212D0 + D7134B40033C7D9968DBBAF3EC044E4C22155FA6F9CDCFD3DCADAFD2AD5FFD67 + D20FFED97F35DDFF8D7F36CDDFFC241D3CBE91860E9BEDE18B69AC38CDE4408B + AA1D685D365172DEE1D8A35F08071FD2BE9C7E283A0225950D52170046F1985A + CED9ED4ABD1A00770947A9D4558E70AEC6489B087C63BD9827D0AB770AA27A1F + E174DEAD1CC01B213079520B8137DCD91D357DE1966B8A68A8F338DF71C8DF4A + 1100B3D532946D7937F09543BFCDC7D1BF9D32F895B69B3CB6B497016F0B1188 + 2C8B12AC88C021828A5A194460A308BBBCCC2AD1BF35E4900477794728517DCD + BB420B0C76015F88E8B33C2F77109501285B608FA08D200F5BA5FDBDAB1BA65B + 8FD3E2F66307C04BB79FBA00C06869FB595AD8247AD28E637516B79EA70513D0 + 6FE6FAE33465C7986079EA6BD6364B066F00F10AC003A6AD3D4E2C19DD00E002 + 63D547C4FE02B0B2446ACE017F37EDB8EC073BD2B491DF390B906CDBABDB8FF9 + DA020933446C2370051323F48D5BF249AB6E3FA9AD2EDB2E001CED3D82D58E45 + 1F89D815785DDA7C9DE6B7DE38009EDA7C9B266F7D96661EFC282DBFF88D74E4 + D5EFA495E77F22CD3FFA499ABDFF8334B5FD699ADA7A6DB65667E395B50554E6 + 3AB750572057D05740977CE5C5B2C6D6B62CC5AC7373B06A5BAE8F0360EB7784 + B76C055EF94CA394A7F26843BA05C0F9BAE7EBD37E0E751E5BFAA23C49009876 + 63BF046405812701A196261298EFAE40A82FFDCCEFA37C7FF8FEE528F77C3DD9 + 6690DEFEDEA8572B4710DBEF20FCFEF85DD7A0D8FF0061BFA969227F6F3C4993 + 40DD8D97F6B9DB73F2F6DB34B3F5368DDD7869CFCBCFD2854F7E335D78FB9B96 + CFBBA15FA5B9CD57766E44A287CFC9D21EFDCDF79EF3B3634ADE073BAE206ED3 + 87922FC02B00AC7EC67B49CCCBF9D6EECD9D9A5DB5FA41D32C397DCDEEAD7DA4 + FBAAA4FB2169EEBBBAF7C674BE5F66D88CC8D3BD5A00B886BCA4952FD8ABF2FC + ACE85557396901DE7EF9B50482331C463CE75879C2CA9B67A100B03D27CFDE4C + 9344D896D539267945410F00B6FAD64606C096F615386EA7397B3E8C5FBA9F46 + 2E3D4CA3D75FA7D18DAFD2E8F64FD2E8ED5F4FE3F77F274D3DF9D369E6D91FA6 + D9277F90C6B77F258DDEFC2A4DAC7D2F4DAD7D9A26AFBEB4E7CB53BBBE0FACED + F60F57C3A7EDB84D3A43609EFF4425F3EC1700468C2904713DB217B86BF56379 + 14793E5629705775D846988B642370AB72C15DD928AF1F001EF6D794E4B20C84 + 7BC77A521C3BFA58CDF635EE644B5E84C01A6F52D66F4CAAB628D73856F692F2 + 34BEAD15412CFBB2655F6DB12F98AB72D26CFBA96EB72B4F696FCBE606830030 + 69CD6724CD7D6A5136C23B91172D5D802D12C0151CAECB2541DB1AE24AB55D3F + 45107C400098F6D000E05B9745D8DB4F79FEBA734EDB2A03E08326C15EC1D998 + 469AB3C7F4A0BC41D2FB899B3C9B93D30F9703E015FBBC8EDA676C760BCBF699 + 2CD9F59AB3CF6F394D1F3F95A68E9D4E1387B9D6D67FC02FEF3A9E396A73FA5E + F83B0800F7530B6F7BD565D39577606231CBF7F7EEE7D955F87FE4AF1A5968D3 + 950EBAAF6BA79FAB167652F4DB0DB2DBABEA36623B31DDCFB69F62BD6FA3A20F + B4BE9EFDCAB34867B5BEE6C17A9F3A5FA7627FBAD4D8C2189C41F4D6AFD5D546 + 8FB029BEFC28F191AF4BEDB1BE03C03F538A4057E9BDA8A7FE7700B8476EA78B + 142FD6CFAB9A1BC147BAD14400BC4F181C6F7ABFA800B8E7BC627ED703A343FB + B145F5035C6A1FE45F8F061D27F6E36348EDEE65C0D5552FEAEB1AB47582DFA1 + DEBEEF553D00778F12FC7DAF08E0BD489038C2D60EBB1E18DB273F0A881B2378 + FBA9CBA6AB6E0D805184BA522C533ADAFA7E81BB02B45D8A3611FE0A003775B1 + 01F8D61078B2BBFDBD0060BD0358EF018E12E0ED971F358C1363BE440497F490 + BF9397F7EF5EB209EFD5746011E8BB960E1CD94C078EDF4A074E6CA503C76EA6 + 03CB57ACCC6C16CFA5D185D3696C817752B54E8E087FEB34F0D7FB5EF2A3D324 + AA1F006ED502DF08807927B040EF20008C6A002C45100CDC65BFC93B4C242DCE + B5AB1E518B9308479496C9E3DDC0636736D38D37BF957EEDAFFD57D30FFEF2BF + 9CD67EF84FA7B3AF7F27ADDCFB511AB9F0280D1D5DB3B6AEA451AB2F002C8716 + 92134DFB2A97D897830DD58E37B683802F69C1DB2E512678DBAFBC4B1C8F6B82 + 4D7E872F4E4C6B837ED9362FFFDC3A480567A393343A4FEBFD982F08ACF22E00 + 4CDADFFD7B8E68DBFC5E3FC9ED4C38763D62D7CAA322F44511067701609593CE + 4B5CE2E4DE6A04F89DC5717DE9769A354D5DC4299F81ADC06F3F004C14158E77 + 39F379AFA41CF172B6E7F26DD36D5FF659D164D3449305284CBA0B3444104C3A + E6B3054268896714C16EB60562E4E3CA767EB55D129ABCBCA4F3BD52FF9E0BF8 + 2B00DC2C035D6070DC7715502A58D54627B6F9C881550162B3EBB6BFF9C0A37D + 1DF8DE2602F8795ABEFD222D6FDBD60400267A139806D49D5AB57A6B396A76EC + D2DD66B9E4299696057299AD80DB7C01688028C13429F6C7ED80800506028049 + B384B46CDCAEB44574ABDA5444B0DA57BECA9427205BC3421401B0A432076AA5 + 6E6C43F90E1A0B14D57EB4531EF0570058E559966FDB7C1E61B9E50DE01E50F7 + 4D9A6119E8EDCFD3C4ED2FD3D4FD1FA6A97BA6BBDF4FD36CEF7C992636DEA4A9 + CD57F619BE4D2BA6A58D97A5CDDC1EA077799325A15BD84B5A7982BD804D4976 + 00E0782E35C49EE7FDBF9BBDA01598EB10BF2C09DDA50880772E1DDD7E5EF1F3 + 519EF2D527CA659355AEB9B54B9F80BDEA9BFAC756303847AD130D9CC1A6FE20 + 517F57B91E02C059EDF732DBE4FA2D1C059852CE77B805A702AED8005A27AED9 + 96DFF9FA8B34C332E15B6FD2C2ED4FD3DCED4FD2DCADB769E2E6AB34B96E79B7 + 3EB7CFF8B334BDF13A8D5C7B9AC6579FA7E9F597390A1800CCE762FD23A29C77 + 382FD8396AD9691D9B08641D5BA0B70BF6B2AF3CF59D6D3C977C8E28C35DC4F2 + F37909FABC1FF3625927002EF6BABF0AEAC67BADE470B702C0BADFEA5EEDE5E5 + 79A067028AF057F9318DEA3ACA8BF580BA315FF682BD11FE4A2D00561EF6564F + CF427BAEE5E85F7B46FAF3D69EAB05FE4EF1AE5FEA926FCFD0FC4CBC95976AE6 + CF53D6CECC397B1ED9B374E2D2BD347E9525C53F49A36BDF4B43EB3F4807D77F + 98466EFD4A1ABFF75B69F2C1EFD8BDE437D3F8D62FA5D19BDF4B6337DE9AFD33 + ABF7284D5EBAEFEDF9FB7EED793E64CF75802FFDCCCFE00D8F008E630C2408CC + 582346F23A00266DF9514DBD325E2142589036C2E028F20578A516E2320663BC + D596C53CB52DFB76BF1DBF691CD88C07199F31AEA39CBC30DE244D9E84CDB49D + 0F1214461AA7CA8632A4F6FA291E27EE23406CDC8F3611DC0AFEAA4C10B79F62 + 7DD2DA17F4254D9B12730281DE5A35189634DF89F31FCD776AC01BA1AFE0702C + EF0774BB007054ACAF36625BB1BEC360CA5100BE519A9B22816014F3F7AADEF9 + EDDE00709CB3A398DF951E24E0AC00700F043669F96734EC4B3D038017D3E8F2 + 4A9A3F7D3A9D5E5D4DA7D66C8C7FCCAEC3F4B25D1B2B9FB736AD1DBD2F9828E0 + 8353CB452D008EB07607B4DD4583EAEDC873F8BB58F2C9A3ACD76FB31F35BE96 + 0880FB698FFE2FF9C9BAB41FDB7EAADB88ED74957529B6F7B3A4E8138DE712F3 + 659BCB49939F7DD1D18F3D48BD7EE96E9BDDF421757FDAAA5F49E90CA062243B + 237C3FA6BE83BE3F538A4057E9BDA8A7FE7700B8476EA78B142FD6CFAB9A1F7F + C7CDE643B5CF9B55BCF1FD4202E0783DAA7E773D18B5DFAF6C6F6A1FF4DFE420 + A51E38ECB5EC43B4DFF38BF67960F3F50DE63A01706D53F5A14B79E0CCC07AEF + 8AF077070426DD3188EF94206FC8EB19EC9732E5797EB04511CAC6FC5847C2AE + 5EC2B99F62BB5D79CDB1786F719F68DF2E388CDA896098F8611FA278BB447984 + B7CA8BF5C81F9E39998681BA11FC22A030F527DB25A0F7068033F015FCDDABBA + 6130D0F7741A9D3F9146776222020000FFF449444154E6AD2F96069E8ECC9D4F + 633317D3C4ECB53436B7960ECD6FA603271FA5893B3F48C73FFB93E9D8BBDF4A + 13DB6F2D6F3D1D58BA944696CEA589456B63C6AEE7AC9D43718A4487879C20FD + 146D259C243D4B4037C2D99295F371CC5CF0BE2B7F37008C6AD0AB7D49F037C3 + 6144DE85BC3CF4119C5D19020FB9CDF9C43B80274F5E7747D201EC4FACA5FB3F + FA33E9EFFE7BFF8BF4B7FF9DFF79BAF5E33F9F0E9D7A980E1CBD9D0E9EBC95C6 + 4E6EA4A993AB6902C71C511AC5B185E484D316C5E80B49F92A93A36DF4A8955B + BA0B00CBF9C73682DB582E91DF0581633D093BE02EE51C933CE0AF00B03B04CD + 867D3944BB84631547A900AEA4F2E8548DF9DADF9167C2910CA805DAB2CF52D0 + FE5EBF520ED005E2764160298260441EF6D4AB0130E594F54400BBC3FB569A3D + BF9D662F6EA769932FEF8C53DC016F0B8091A2906204308E771CFD12CEF8C94B + 4444D9F11B686BF905C246204B5ACAFBDBA60C75A3041D0422D8AA6D40832084 + 404406C8399AB78DFECD6502C55D0038B791EBCC01700BF09DBA7CD7B7F3371E + F54405CF922E804AD0B70153D8DA56E539DAB18574F9FDAC66BFF1C0DFFBBBBC + FD2CADDC7E990EDF79E9F077E9D6738FFA45D813993961C75BDA7E9DD6BFFCED + F4F8D7FF6CBAF8EA97D3D4EAD30C86D75BC0D6C23CDB3709A609CE9266B9DA59 + FA6AB60E008B5A009CE15A04BC4D3B379FA4C5628B380EE7A6B6BC3D1DC7EA03 + 0A052D5B5098216254B4D33E3051E0963CD553642F924D84BCB10DB513DBCBF6 + F9DDC0198266400BA09FDBB03A9BAFD2CCC66B8FFC9CBDFDBD34659ABDFFA334 + F7F0C769F6C10F5D53B73F4F93EB66B3F5262DDF7E9B966CBB68F5B45CF362D5 + C71D7D429CCF0D2026D7D6AE9D09D0E9B23ECD991C2C5A7D4532ABFEC2A69DC3 + 96A5C3E78D9ACFA07C17B45FE7D765FC81200260FAAAEBAD3CA43CFA441A00AC + 73634964AE25DF5BBE1344FA0A02D7C7A3AF80DF29A2580B1CCDA0B3FDAE09F2 + EE04E5D65E29535AF5F57B6489F3453B46CE1300CE7F6AA09C487DE0AFF77FED + 799A36CD12C96D63097FA7F3ADB76972ED95E57D92166FDBF7C0B6D3F67923A2 + C3A737DEA4899B2FDD06303C639F3D91C1D3BC3799B6EC5ACEF8B17344EEBCDD + 7358165A50581058C017A0AB7E2AAD73423500F67ABC7B3C80DDC9ABB71AA04B + BEA27FB51F6DE3B691DD1351BC47F62EBD9FF7958ECBF1EBDE1CEFE10E66C3BD + 1B455BDDC79546B18CE7C8A8BF67D79E5D05F846281CC1EF20451B81E0A61D7B + C64DBBEC7958C4FB7EFD4F5AA629B39DC68E67AA3DFFFC9978E9B6E94E1A67A9 + E60BF9350633D4A54FFCD9893FE7D87763ECFAAB74E88AE9DADB34BAF6451ADB + F82A4D6CFC304D6EFDC0BE3B9FA5F16BAFD3E8E5A769F4E23D7BF6DE499344FE + 9E5DF73F4BF10782097B9E1E627C0010B67C7FFE5A9A677A04B9FE8C2F791E19 + 4C5E197B68FC518F43941721B0E02CEF04061C47982CB01B41700B72B364A332 + D9C7FC5EB5E3358DD30601608D3111E35A40287094FD1A00CB4652BD5AD106E9 + 38FD54DBC676046FD52FD9A86C2FC256625F00184500CC9CA11F008E8A11C111 + 06EF5001B211FC46A95C12A4DD4D8AE815D8D53106B553973117646E886A000C + ECCD73D116006BBEFA3E6AE7B71900C725A06BC539BAD25D36F53CBFBF3200CE + CB350BDAE63CDEE30BFC25FA17007C706ED1AED1423A79FD7A7AFAC517E9F997 + 5FA6731B767F386CF3FD692B9B396C75682BB7E1D1C01D005890B60B0447985B + ABB695BA6C9ABC8F0C805B597B63D65EE3B70A1A59E8D16ECB4077F9C806E5ED + 57B18DBAADAE3249AF65235DDB6B7FBF1A54773FEDCACFBA177F6B6DEBC2F78E + 2C2DBBF65CC9CB8A7EE9417A9F3A511F5A7FA0BE018ED0C536500F1718C4543A + CB6CDF594F9F3A8D384E75AC1D525BDFE95BA1087495DE8B7AEA7F07807BE476 + BA48F162FDBCAAF971EFF6E3FFFA156F7ADF01E0DE7EEFF9C156EAC587E160E5 + 07F7870E4AF62B0D22F65BF621DA6DE014CB6B5B956B805397BF8F621BEF0380 + 3B07CD9EB601FA3EF44D00E0085977CBABCBFA95A32EB0DBA52EBBAEBC7EF017 + D500384E26254D20DDBE82B9DA8F8A0098B46C1BB86BF902C0805E146D942700 + 1C21B0B7D93339EE05C05DDA0DFA4A8AFE75F06A93F7319BE08EFB24D78EB1C8 + 84FF429A9CBE96A6A76C723BB56DFDB89B0E5E7897CEFCDA5F4DDFFF97FFFDF4 + C3FFF27F339DFB8DBF980E5E799C0E2E5D49238B67D3F8DCB134C2A479EE648F + 93A30BEC46F503C1AADF0B7EA50C791DFC967D01E083B69FE1EFDE00708CF255 + 84AFD4C2DF160AB33F4674EFB10C8001C1A3876D7B3847FF4EE100C4B146DE89 + B574F1C98FD24FFECABF947EEDAFFE2BE9CA9BDF49074F3E48078EDD494367EE + A4D1D31B66BF9A2671E6E1EC3ADA3AB7A2A34CFBB42BD81BED50CCCF4E32DA6B + DFF78BC30FF82AA71F12C8255FAAF7633EF65DE058656C01C0D86BBF06C08A08 + AE1DA3517292D6109832C1DFE88CC501CB7EAC176D7B81EC7A03700571B55579 + CCEB126502BDECC73ACA279DDBDBB0B24D536F243022126AEC3CEF33CC4E6EC1 + 5F9CD1B357EF2622753D1F187CD16C71A817877D5474EC677800206881AF806C + 94402DE533D7328895F4AE499673561BA4E33EAADB74D0022829FB0089088B01 + 11E4013A048F391612E4ED0B800BEC15E09532ACCAF053708A7C016087604056 + 876D0F6DFB2811E9BB742B03E095DBAF5C82BF0E5859F6F9E6D3340ED4BAF52A + DDFDC99F4EDFFB73FFD974EDDDAFA7B1AB8FD234F06DFDA96D59FE39C3580460 + EB04C0AB446C5A9F02941300A66FB396477DF2D507EC684FED92D7F4CFF6EB3C + B72FD1CB40434157414401C43A5F224F90B70BECEE0500AB9D58A6F29CB6BA2C + 696DFD1590F4CFC8AFC34B07BB4481CEDEFEC2DF033CFFE08769F1D18FD3C2C3 + 1FF9F2CFE4CF6E7D92966EBF4DCBB73E49F36BF4953E59DFCA31755CF5BB568C + A0A55FAAE3B276E650E977BC166CF3F963D74255AE7B2D95451B7D66B5AD80BD + A47ED6796CE9432C675B4700D70018F5F683DF05EFFC05E666C0C9EFCC65DF59 + 4567F7C25F6BC3F6FD3B6DDF6557F96E2B7A98DF9D7E87190E67298FDF2FDBF9 + 75CEE9B9FD3E9FA5496B77D63EBF5996F4DE78E51078CE346F9FEDA26961D3F6 + 37EDFBB0F5695AF0CFFE5D9A587F9326373E49D35B6FD3E4E66BFF43C0A47D0F + 80C02C2B3D63BFFD395617F05507EC9EE4F71E4BDB79D2D745FBFD73DE02C280 + 5D01DF1A004765FB1C01CC92CF02BB88FDF9B5FB6961DDCED1B608101C01B054 + 0360815BFD298634F7C9AEFBA624A08B54A6FB697C2E5016459EE06F94EC490B + 02B315C805FAC66860C15D45F70AEC2A5DE7F5D35400C082C092EFDBB38F3F3F + 11A10BFC9DBC74D79E85777D3B7BF94E5AB0E7DC829DF73CCF2D3E9FEBF6FDBF + F6348D5DB17BF7951769FCEAAB347AF5A5DDC7DFA4A9D54FD2D4F517768E8FEC + D8D6CE05BB1EF65C15743E7EEB79BAF0F48BB46CBF6DDE2F0CFC75007C1AF89B + 0130CF747FC6331E619FE7BFED23D271ACD26FFC52DB016501B600E018F91BA1 + 6DCC8F8A3671BF4EF7DAF68ED9047F251FB799EAF1A5C69502A6EC33DECCE3E0 + 16D6467B8D5F65AF76637E2C47B19DD8AEECE23EDB091B6B32E6260DB015D48D + E95A11F0CA4EEDD7E55D00B886C0CC53E2FEE8D205576DBBA35E01B3528CFA95 + 0466239CADD5555603E041EDE4FC2A0FF83B67F915FC455D10F743D5CE6F6D6E + 5CE6E128BEEFB79FF6B33CF44E6548DB0F000BFE0E2DACD8E7B19C268E1C4D17 + B7B6D2273FFE71BAF7FA759A3FC19C75251D9C3F625B4511B7ED202DFFEC2A80 + 76AFAAC16E971AD81BEC625E9B4FBAD7C7F3FECAC7E80B8151F17DB5BEAD5E1F + 9854FBC7D897BAF2F62BEA47DF5F6C2FE60D52DD5EDCDFABBADA8AAACBE44BAD + 7DAA757E2CEBA71D75F0BDA3AAAE2280A51D3EEA3E7A9F3A51B1FEFBB6D14F1F + BBBD2E75B10DD4C3050601E05A43ED92CEBDDCA7AB7E398EDAEF3C46DDCE77FA + A92A025DA5F7A29EFADF01E01EB99D2E52BC583FAF6A7EDCD58DE6A7A078D3FB + 455F027A47FE8087DBA0BCDDD53EB8BF496910B1DFB20FD1870EBAA2BAEC3EA6 + BA8E118FCF60340E929BB4E9E0980DD423C4DD4535FC95BC9C6DE7E07D77F580 + D52AAF0BBED6EAD756D45EDA415D36E4298258E511F00AFCD6D2C46FD7C96100 + BA48D037E621C15E29025C87BF450E74A76C421E146DBBD40B8049EF0E751DEC + 86BCC1EF00B6F2D9936994F71BF924D7FA6F13FF91D98B6972EA6A9A9ABA99C6 + A76F99DDED74E0FC9B74F137FF5AFA53FFD63F4E7FF46FFF4FD3EAEFFDB31900 + 2F5FF308E0319B848FDB351DE31FE305DEEE45110247F03B48110067674A06C0 + C05F016045EC4600CC7E170016EC8D2038DA45C9560EA3D1A357D2F8D1AB69EC + F06507C393C7AFA5299C6EC7AFA79163380CEFA4D91B2FD2CCEA9B347EF9451A + 3A67D7ECDCFD3472EE6E1A3E7E338D1DBBEA0018E758547474B5C7DA097A5167 + 7E8800668BC34F2057F982B482BCC0D95A2A93C3508AEDB1258F2D70977ACAEF + 69C7F2B41F1DA2B57A1CA44194E16C95D356CED71A00475BCFA74DDAB6B4049C + 55C4EFF8B9B5347E36E747D0EB70B7DE37FB087C95A772B59FF389662292A917 + FC4A0060777017017B599A79EE7A5EBE14302AF03A71E9561ABBB0E9915931DA + B7D7910FFC8D11626C338CCD403643DFAE740D80C98FA0B706C382C1AA0BFC8D + 00B8B75E0B2F22D8A04CF2FAD71F3602FAF6EC07E88B804B0E948AD817F80580 + D511C059A4AD1EEF03DE7C96966EBD482BDBAF4DAFD2D256867D1291A0D36BCF + D28CE9F0DD77E9E4E32FD3C2D6AB347EFD71038067D69E6420C776AD406013EF + 02CE202D6B1EA00C782CD031C3D0E77E0C34B3666D593B828C7EFC02F8D0FC1A + 80CFCAB6AC5E017968070006B6155819DBD2F1945FE7699B016796ECD40FF296 + ECFCD992A7F350FDB8AF3694AF32C02291CA731B194AF69C871FF3759ADBFC24 + CDB1F4EFF6E769F68E5DF3FB3F484B0F7FE4DB59CB9FDF7CE3F07779F3755A58 + A74E7B8EEA33E9E6DA593A8A3CE06F0D8029E31A2FD8F760DEAF753E5F95239D + 57EE2BF6765E02AB45A4055CA5FCD9B4305FE79D6DDBEF03D2F5EA525D9ECFB1 + D4B5E3D07EFCDE47A95FFCF98177002FF267819BF90F14FA4D91563435C09728 + E916FE660174E3775B12E8050EEB37990171CECFBF593B8EFD1600C073F6D9CD + D97524927B61FB1307C0805FB4B0655BDB9FB969CFCDB5D7697AC3CA373FB5ED + A7F61DF83C1D79FCA3B4F2F00769E6D63B07C0532C0DED5039FF91628677F2DE + B893E61D00DB7DE89A6DEDD8876F3D4FC7EFBCF22DE7AE8860E0A160780D7D63 + 3A0260BDEB17C02BF81B01706D53C3E026AF44F5E6F7A4738FCCA056D056F7CD + 3A2D1BDD53959F9F033B01B0F22877B8DBFC6128E7CB5EED91769B8B3B01B000 + 6E3F001C25BB317B168E9DBF9946CFADA651DB8E51973F3E99CD9489E5A09B67 + AB89F7FDF23CE4BDC144FD027D272F11B56B9FD9657B5E5CBD9B16EDB35BB467 + C73C9FCD152BBB723F4D5E7B94A6AE3F4B93D75FA4D1CB4FD2D86522E8EDBBB4 + FA224D5E7D64CFCEFBD6A63DD7CE6DA6193BEE8CF563D68E7774F3693AF3F053 + FBEE3FF66347003C7AEA66B35A883FDFD996F185C61EEC47E82B1B496318093B + 9529E257A03646F792D72C2B5D40701710665F75B54474F792D079EC568FE750 + 17048E6350F258E10669BC49B9C02A696CC88F6358A96E2F4AE5523CFEA03AB3 + 76FD976C6C0404268F7E38AC5DB6B17DD98F40B7DE579EDAEC2A97982F08EAD6 + 30376A2F364840B686BE52D7DC07E94FB2876CAE25801BE12DEA07806BBBBEA2 + AECD0DBB00B0F2D9B6E0B61BECA2DDCA5B0D06C0CAD3DCBDDE0E4AF717903602 + E00C6F01B92373762C07C0C7D2E8D2B13479E4449A3B753A1DB974399DBA7E23 + CD9FB0EB3CBD900ECC2CA5A145A285F12FD05EAFBA00702F986DA525A2A5BABC + 4BDED6D8A2AB272F28E793A6ACF5CDBCBFC271DD6F95617004C2F8BDDC1FC836 + F8B9A22F0CC93F5647DB2A5F79EFEB57541B5183CAA4D806926F5352FDDAAE4B + 83DAEDA7FA7883D4557F90BADA90A28F39FAA507E97DEA44C5FAEFDB46ADBACD + 5A5D75FAA9AB7E8FB0095CC33940C548F626EA991C004F9BFA71AC00793B816F + 69A7D9FF0E007FAB1481AED27B514FFDEF00708FDC4E17295EAC9F57F5FCD895 + 7E7F7DC80D2CD6FB4505C083141F167E7D941E6570C043393F90FCBA94B2DDD4 + 3EB0773EE0BF4E0D3AEED7D5A7BD0C9C74EC78FC38F092629D8FA9AE63A12EDB + 66401CD3D81600DC40DC018AC0B796DBB0ED1CBCEF2E81D6981761ABB6FD14EB + 759523B5B75B5B75B9EA00807B207000BCFDC06F2C1F3441EC01B115E48D8A10 + 38D6D9299B3C57EA076E23C0655B47F5F6B3AF25BB2E0186331CB689BE4D740F + F1EE22D3884DECC766CEA5F1A98B6962F286A5B76C12BC9D0E9C7C9CE65EFC89 + 74EF8FFE6E7AF067FF4E5A79FD6BE9C0B93BE9D0D19B696CE5721A9F3D95C667 + 4EA6B102711B2747E5BC9028938324E6C57AFD8130E5D8C9C19281EF5E00700D + 81952F108C623DD58DB692C3E02397D2B843E02B69CCD397D214E0F3F88D347C + E47A3A78E4463A786C3D1D38623ABA910E1EDF728D9DDC4C63447D98EDCCC91B + 690AC761717AB9832E38C0704CC92926C931E60AF952A7538F764DCA8F792802 + 5BC9C1AD49F5621D95A91CD5F550D35EC893A3545B494E51C1DBE83C557E0D80 + 65D74F02C0FEDE3EF26843CE66967A3CB39A864FDF4823B61DA33DDA3609E4FA + 314A5E53AF94C7B6BA04009ED172CF44F3166999E7BCB4739BC77296199AF646 + E14E5AFEF8C50C806B082CE73DCB8666273E2000A73EC0E0AE4B9055FB28825D + D2B5A81F41AE6CA3D4AEE06F6C4B656C811EC090068C50A7C095AC16E8028DD8 + 276A50700AB12F457BB75DCDF0B7560642665BC095A0D4E2FA335F3218C8B7B2 + FDC6E580933CDE31BBF53A2D98E6599A78ED799A246A140867FB73662370CB72 + C1112ACE9A583EB77E172DF90292112A021B675902D9FA2308582BC247F60500 + 95CF5640B0AE17F79517F3B52FD14E2C634B1EC759BEF5BAA70F94EBF84A730D + B1132C565BC801B07D065AFA5A8014888DFDFCC6CB1C09BAF536CDDFFE3CCDDD + FD5E5A7EF803077E4BF7BFF27CDE15BC62EDAF78FB80FBDC271D5FAAFB18F333 + 38B57E5839F2FD55937DA6F35B665340F0E22DBE07ED39C47604801B085CE410 + B8A433702DE50DF0CDF9CAAB01B0A4BE21E5C5CF379F0365E55CADAD1AF6D679 + B9BF6CE90B11C2394A98727E235ECFDAE29A715C41F21D32BBA8850281897457 + 34B1A284E36F7A9E63B09CB97D6EFEFEE65B9FA4A5ED4FEC9ADB67BEC6757F63 + D7DBF6791FF42AC7B773E5BDD0A6A975A2833F4B37BEFF87E9ED9FFF17D2F69F + F84B69FEE10FD388E58F97A5A1793F307FCA98BBC9EA012C011D23801FDA77E6 + 593ACAB2EFB7EC1CD7F8C3485EDA99FB4F86E1F97DC848D037DEA3B2BDDDDF02 + C015D08D6AE06E9140702D2294057DF3BD3B2FFBCC963FF368E9676C62B980AF + 60AD802DCF0229825D14CB0480F5DC503E692D454DDB5E9725968B00BA312DE0 + 1BF3E2BEF2048047CF0900DB73F6BC3D3F0B4096D8CFA25D7B2EDA3313F8CB52 + CD539781BC0F7C3B73D53E8B6BB6CF4A17F69C9BB1674DFE93949D1BCFCBCB76 + 3DAE3CB4BA774D767DAE99FDD5FB6902C87C9608E38D1C656C7D9AB7E3CD5FD8 + B4E7B3F5D7DAE18F58876C9CC07B80474EF37CCF101800DC8C1F184F589A7141 + 1C8B689CE2651A1F04FB7E631D24A02B582BF84B7E0D80B52F7BA4FAD41D3EC2 + 1FFCB2E2FB7F73BA77CC27696CA7F1A2C6879E2EE34CC6958CE1A21897323EA5 + 9C34121CD63E6AC6AA26C1D6A8583FDA22A06C84B4B10C00BCC867C7F5B5BAF4 + 111B80ADB692C070ADDD00B08ECF983FC2DD2EC0CBFCA42B4F6AE06DDFB9CDEE + 8A6DB80680E05A7B05C1076D5E0894F5F9611F51DEFE41B91BF2AA2CD6D176A7 + B03DEAF2397805736BC532A5F72B402F4B37D7F097E8DFD1F9E3A663696CE178 + 1A5F39E14B3D4F1E3D96E64F9F4953C78E2722820FCE2EDB6772D8BE0B768EC0 + DE09DB2F6DE463585E03808F343E8C7EBE11F9321A9F4655DE256FABACE0D693 + 571DE3002A76B17C4F523D17796C17B304992D2F4260F773E18FAB7C5CFDFC63 + 8300709D1FD55556FB016BC5F2DDDADFADADDDA4F6773BCE7EA43EC573A86D6A + EDD687B6BD56B51FBB9FDEA74E54ACFF21EDF493B306A5BF8E63C12A063091B8 + 5FDB353A8828CFED0900EF0902F7A8B4D188BCEF00F0B74A11E82ABD17F5D4FF + 0E00F7C8ED7491E2C5FA26A50FAA9FBAEABCAF7A7EF04ABFBFE20D6BD79B55A5 + 58E73B00BC533D0F9A72BDBAF2BDACE40D526E97ED4E753DDC3FA6061D27F6E3 + 63897635681934D0D9ADEED7210DA876535DAF0700B32D3A6803EC4E985BA9B6 + 893A346E037EDB3258AF27035103270B214FDACBA444E5B15E6D23A9BDDDDA8C + 92BDE06F5404BD4A6B5B4F1A6379573DA0AD006F9754A6A8E02E00DC8261A280 + 4F3712003E64E926FAB548F0F680B601FED600384AF5A26A1B415F343C7FD645 + C4EF419BE41E9CB7F3300DCD9D4823D367D2D8C4C534397E3D8D4F6DA4A1C5ED + 74E0C4833474E5559ADCFC2A8D6D7E910EDD78990E5EB8970E1E5BB3F24B697C + EE6C9A983F93C617CFD9C4383B46709044A785CE15A93C3A4AA25325D691A343 + 6D090067E170B13ACB1713E0F780D53DC8713B007004BCECC73241DD1AF2AA1E + 51BFFEEE5F13FBB2C369C4FB7B278E5D4993C7ED3A1CBB98C68F5EF6A8E061D3 + C123ABE9D091F574F0E87A3A707C231D38BA960E1DDD48A32736D2C8F1D5C432 + D2440EF3FE6039DE70C835A0B4E42B8FAD9670963D521EF6755D44BA6B5F79B4 + 0BA4D592CD92602EE5B5749C584EBA01BEA56E9317EC78079E846354CE517786 + 06C76A74924A2A67CBBE1CAA5DF9B4EBF52CAF71BA526EC2498B4377F8D4F534 + 5400B03BA5CF65D8EBC0B76CBBB413F66EE4A52B4B6431E25D8700E049DBE25C + F6285EDBBA333A4060967B9E06F45EC121BF9DF292CFD99EE8DF0C63735416F0 + B706C0BDE000877FAE03FCC8C000C77E06C00059893C60ADA27A059C0571DB74 + 1B1D062060B95281024439ED090093268A99F72AB2057834CBA3DA3177C21581 + 5E80D153DF4E5FCB79005E0161DE238AC817FC7538CCB6C02E29464202A89658 + 7E184064B6BC0B76613D433D80E5E1DB9FB896808437016CCF1351888B2C4BBB + F1324DDF24DA3603AA194B037F81C0F31B40D2B034AF09000CFC4580E005CA0A + 008E4092632D023E375F24961D06AEA91D8138B76B60E71B077E736B003ACA5E + 5BDFDF98DD2BDBCF50903600777A4F6C04776A1339342C7992EA0AFC453B815D + FA13EBB2AF36B141C07441E0783CCED7DFA37BCBFA4EE4B27D26005360A5479D + 16003CBFFD695AB8FB3D87C0805F0030FBBCF775C1CEF9B09DF3CA56BEBE00FB + 85728D24FA44FFE275541FC857DF77C8DA9CBB657DB03ECE6DD9F9DCB2FE93C7 + E70C14B6CF20471C677BB69C879F8B89E5BC7DB9F1B225C21C294ADB413779DB + 7CEE396F1E204E7FAC7F7C1774CDD47FB6EA7B3C1FD200E0FC5E652B2BDF73C1 + 65F50B40AB7CDFDFB4DFCC86FD96D6ED77E5D1F0D9BE81D7E178A4F5794BB3EB + D62FB39BB6DFD654F99305EFB65EBC699FE30DFBBD95C87DF6FDB766BF51FD86 + 17E887FD6E6A00CC6F8C3F5BACDC7E67F9EFEC1A7C62C77E69FDB67E58FEA47D + B747AE3D49C79FFC28FDE6DFFC37D2DFFB0FFE37E9B7FEF81FA5132F7F2D0DDF + 7C99C6D75EA52996815E250A18C8DC0260DFDE0C80B740DE45406F1305DD825F + 4546C77B14F9E4CD5CCFC0D8E16D59F6D9EF67B61F61B0C06FDC17F455DDD955 + EEA3ADB87F234161015FD2E4E5D51D6E797A9E7668B7D44102C6B95EBE57EBBE + 5CCBC1AE49F772F2048455A779AEF09E78C06A01B400DDB81474AD2E28DC40DE + 8B56FF92954BF60C9B02DE155BA281FDB9E6F5AD0FA609DED37BC99E2B57ED33 + B9C6F78B3F18D867C477EFAA3D13EDD9387B956719CF867B761EB7D328AB6B5C + B2EB6FCF54FF5395D9F0AEE049EBFFF485F5346FE919227C4F5E4D53A7AF5B7A + 2D8DDA3860C49ED7636737D2F0E99B1EFDEBEF013E65E9933646609CC238A38C + 2BE278238E39A4E60F679528533DB6315F9077FC18E3A50C7BA51AF622415D24 + 602CBB7EF91A7F754963C1389624BF2BDA17D84A9E8FE9B0632C49DD02520568 + 0575952F295F76D13EEECBBE81B026D991A60F4424CB967DB607166D5C5F01DC + 1E981BA46320D5896A6C980794394A9CABD4AA6D24CA34AFC9F319F6CF59DAE6 + 21B3CC75C86FE73775BB9AFFA83ED27CEA10D1B901EE6AAB48E0180DECD1C5C1 + 56F9C86DC92B11BE5D72580BD89DE29DBDBB0360B69D6D54F27700DBFC1BF91C + 3CCCD78904AE977A8EE5B2575EB4192C007086C05919FE8ED83C7874FE641A9D + E54FD147D3E4CA098F001E5D39924696CD7E71C57568E1B0F57DD9EAD871AD3D + EF3BED069F84E42B94053F06E5FBF17DF493FB590A9CEDC9ABFC2FFB02C0D8F4 + F13B651BDB2FE0B701C025F27837008CBA7C635DBE32A0B05497495DF9B50FB0 + 5F39E9AEE34A83DAA08EFAB51F75B5D54F5D75BADAEAB2AB15CBE335DD5997F3 + CDCA7EE6DDF53E75A262FD0F69A753C0D43EBC61D7638943ECA2A66DA92CC5DC + 735CD30E3B14976D160006D816F8DB1F00A3524FED785A6D946379BEA9B3FE77 + FAA8EA627E51B5DD5EEA44F5D4FF0505C0D1A6E9F8905D0C57B8483FEFD20F7F + BF0038DC707483D28D29A677A8B9C1EC546C675F00B8EBA6FBB3263D08BACABA + D4C7363EFC76535B8F74AFDA07F9C757D7F16A75D5FB696AB781118A03A1BDA8 + AB7E3DA0EA1AB4368361C4F58A5BD34106F10CB219708F3090EE1D7CF713D037 + 8ABC1D83FAF790260FCD24A24F3E129CAD2735B1BD7E75A5D846DD565759AD06 + E0DA24AC6B72A87CD9F5CA8EA17481B711FA46F52B573EF277FC4E31F93D9581 + 2B937CCB63F9E7119BBC8FD864DF97839EB1097899CC1F60425FE06D0D72BBF2 + 958E12E455B9F6958770248CE25098B363CEDB047CE1A4BFF36974FA4C1A9D32 + D9766406E7C4A57460E97A3A70783D1D3A72CBB6A6E50DDB6EA443C7B6D21091 + AD8B97D3C8F4B934357B294D587A6CF9A2E95202CC468786A4BC1CC1BB53D156 + 90B685B540DD36D257E90C7C01B3805BD26CEDF811E8CAE12308BC9CA302E4E0 + 69CA4BBA7602D54E2981E10C8701BF97D2D8113B77B319C3F976EC5A1A3A7AC3 + AEDBCD34740CF07BD335746C358D1EC78187E3EAB2BFFF37C3D8D6A18714B921 + C5B258DE2F5F8017E79C9C7872B0299AC3232638A7D20777F8593EC0B607009B + 22B08DE056525E746AAA4C79DA9788ACC1B98A62948D202BFB92C06DE35CB5AD + F6BB14ED631EEDB2ECF3E8D99B69F8EC6A1A3A6D7D386DC76019688F40CA114B + 0061F27CF9CA4AE4CBBE717057C2C13D7911277886BC71B96789C85FDE4548E4 + 2FB03582D80864733A3BEE63149720308E7F4040040019F2DE37E1E4C7B19F81 + AEE0AC807054AE9325981B15ED142D064850BB19E266788200263992F77E862C + EB0F321CA10DCBCBF025C322806EAECF36C35D29E76790A4A842A5158908E862 + 2BA815A32F17379EA7A5F517761C20F0338F46050003BB806EC0DF2377DE3ABC + 042ACEAE114DF8DCE12F51BF084885C8230A78610BBDCC02E25ABB510033C13C + 8F0E16C00384967ABE0CF4E673EB67066E12F5047F81A982AA8273DA7ADAE475 + 4A9B403A8F4E7658C73528A0D6CA14DDEAD0D9F21B9B92AFB298BFB4CD79BFCC + 11B141B295C8C356DA69CF31ACBF26C1D2C55B2FD2F2F64BD7E26D6B8F73DC7E + 9B16EF7C9E56EE7F950E3FF8BEE9ABB478F70B5F1E186008003E6CC77658CFB1 + ADDD45E07481BF5C1B295E4F202BD1D6B35C1B07A8F9F3E0FA70AE33D85BDBF3 + A639228BED58CB269DA77F6EB453AEBB5F7BDB77205CBE731EF95BCE4BE729F0 + 4B5A11C22A6729718E7DF4FEBB74E5939FA4CBAF7FD96C5FA7A955FB0DD0A712 + 659E8FCBB13220463A57CE2F7FF71FF9B1FC5ADA3647FCF2F9E7BE21A27F17B7 + E27E5BCEF5E08F03B4C7B9C56D93A6BF762C2946D7FB1F3DAE015F1FA52540AF + 03E0FCDB9DE17C6E72BDEC73F3EF438EAED7EF0A08BC72F79DBFDF597FBAF0DF + 9E5DFFD9AD37696CF5453AF1FC27E957FEFABF9AFEF2BFFE1FA6B7FFF4BF9096 + 1EFDD82380278810B6EBC21F33FC3ADCB47BCDEADDB4881CD4DA3D670D58CDFB + 91EFB948F32E64BD173CC3F07C0DF59E64D94839FFBEB57FD77E73F7ED3EC1B2 + CF4060BBEFAD027D791F308017200CFCB5FBB06D55A634F6B18E20AEE02E7993 + 57EDB970CDEEE5D606766CB58F1DC788F91357780E6C5A3B19FC0E920030E9FC + E7A00C7D951FCB55C63326FEE948CF18B67A0E51877DB5851D523AFF71A985C0 + 53D6DFC9A62EF5F27364F212B274D1F4159E5376ED4DF3D71EA705BEA3F61DE3 + B93275F55E9ABCD23E9BA84F1430EFCD9FB860EDDB73983F644DD9737ACA9EDB + 6CA7792EF35C3F95C72519CA32DEB0F3334D9EB37A26D23902D89EF5D518843A + 520D7F91EC35A6519A3108E38F69FE2466DBF6F8559B01FE7AB4AFE569CCE480 + 97F112E3268027E3BA5226D5E5288FCFB238F7711BF38CD39E8DFB34F6438C05 + F52741CF638B8DE569EC4B9A3CC694AA73883173516C03B13F445DDB7A595133 + 9636695C1A8F23695C2E1BB68CCB95273BE5EBBDBD1AD3D7529B718C1FEB21F6 + 65EBF6713E413B655F795DE97E622E5383DD1AF0C6BC682BF02B3B9FD31500DC + 057D23E0954DAD1DF6B457D4056F057D635E4CB750B71B0E77E793677373539C + ABA3AEB4DB06C02AC94E36512C311D05F0F5A59E4B9BFAF336AF6D1AB1F9FAA8 + CD8D89041E5F3C69735B4BCF71FCC336575EC99A051E53CFE6F8FEE76EBB06D6 + 8EA43EE9F554076D2B1F88FB2B8A6FE37D54FB550669479D0A04C7F28308FF11 + 7EA74E584C5E2FFCED9196852E7EB0087EBB24DF5897A22FAD0B5A4AD13E6E6B + D5B6B1BD68273FE6209F665D672FFB5D3EC19FB6E84FDB27CEB7D72FFD4D68C8 + BE335A0D736462D9B73D7EF243B3CE1594F7D3EAE72075F18D2EF9BB970FCEA4 + 83764EC376DD47F85E73EDC96FC02DC01696358869B58099B65A3E13DBC96543 + D8C4F709D760518AED77E5F5D37E6CF7A2AEB6748CBACCF60F1EEC531655CA1B + DB8EB21DF9B1ACCE8F8AF507A9AB6E97A2EDFBD4DF973248769E1AE1EA37ADEF + 00F03EE53F76FDE095DE4582BF01E6363791A09E3A52A853ABAB8DBDA8EB46FA + B3A29E87D07E0070870476F7AAB65E3B48193458F958AA8FD5A5AE7A3F4DED65 + C0A541D05EB59FFAD1AE81BF88EBA5B4D9F960B8CF3F2F5104BE5135FCFD7900 + C0FA776C6C4BFB83A409609C402AAD7D4DCC946E65C7503A80DC419037AA2E8F + 0058E035EAE0D4499B18DAA4D76D6CF25826F3405B6D25D551BEA2816BD57675 + 9EE440BA38130EF0CFEF059BC05A7A0C000CFC9DCE7607CCE600A015C8BB703D + 1D5AB861F6D72DCFB4BC9A0EAEDCB4F415AB73314D4F5F4E93961E5BB9D40380 + D9F28EDE1AEE0AF84AB14CCAD0B7769EEC9400702B6BAF0850EB2A8E1CD2D1E9 + 139D3C923B788A6A1B9C4CEE74B23C01E09195F369F4300200037571645D4BC3 + 476F64087CAC150078C4B60060DE01EC0E3677A45DB33CC1DB5EE5E88DEEF21A + 004B72BEC9B18714E1417E03772DBFB615045639DBDAC9182587A31C9A02BE03 + 8553D3B6384625F6E564C5512A1B24805BE739D02DF9B2517DB5213B45E64600 + 1C41AF802E5B415EE54B5D0038964B39120A4774EF32CF350C9E341BC15EA27D + D946E8EA4E6DCFEF8DD08A0E76000251615938C37314AEA4765004BD11E84691 + A73A6C635EDE07560092330826CF977B2EA0360260602F5B45D86939D5087F05 + 8079E7A81421B0E02F7644F2029C3CBF0060C08F967C16048E0098C843815FBD + E734468F1EBEFD361DB9F3CEC53B668946CDF097A8D337BEED05C119803A1C64 + 0B880CB00C98C63683BBE73D001850493D87BF26D2199016A019A463B410B5C0 + 48DBD60017E8BA7C9B68CAB66D14DB214D3B82BAEC2B8FBA2B7788C86499EB5C + 37DA22F695C7566DC4B27ECAB6F4B5152074E5B61DBB40CB59BB5EB35CF3DB9F + A5E57B5F39045EB9FF655ABAFB858361A2460F6FBF49476FD9B9F61C9BF3E638 + BDFDD079C4FE5127E67BBF2C3D431E4B81DFFD344360D3D2ED4FFCB39D33AD58 + 3A427824189B2130DF376B63B305BC9CA383584B037E6700A7FC21210060FA73 + F2F1F7D2DA97BF95D6BEF8CD74E4EE5BFF4CE9AB3E5FC15E22C0D9F29D55B4B0 + C36CC0A7B50B68E65A724CFD0ED84AFEFEDFADFCDB9064E3ED94EFADCE4DDFE7 + 7C6C3B879EB63238E6B7A8C859A27F89D05CE4F7869DFF7EAD6FBE5C75BE667C + 778EDEFFCCB66FEDDCF36F8ACF0FF84BC42FEF079E5F2B7FC22810787ACB3EDF + BBDF4B57BFF883B4F6A33F978E3CFB9534B1F5691ADFFC244D6CBC49D31B5C2B + FA4BDF5879E09EC3DF45607081BB005CA06E14799475C15F14E1AF975B7BFC81 + 85285EBDF357CBDA2B2A98AD228095A7E85FD28B1B76DF5B272F0360202EC097 + B4C0B0C0AF60B1EC1C189B28ABEBE725A35BD0DB4F82B435AC658B941FED6523 + 002C3BE5C7BC3A5FFBE39700C83C17ED99892C2D78CCB38D67883F8FAEDA33E3 + 4AAB990280591162E6AA7D9FAEDBE760DF3301E0F14BBC1E81E3716C7B66997C + 39E8F2272CA27E27CEDB58E01C91B7371D04B30580025EF32A1F8C0B32F88DF0 + 372BFF112D8E2D22ACADC5D826825F298E6F58829AE3F6AB8F0480A74EDB58C6 + EA285FE3ADB8AD253BC0EF908DEB22001E396163BC0280D98E5A5933F6631BC6 + 81883287B94582B44DDA04DC05FCA206F25A7994F28750194B4731A676A06CED + B1CF985B705763718E890DF91A93CB46225F63F63876D7BEF2E27E04BF926C90 + B75DFE38EA223FCC3190E627313D4882BA5102BE0DDC35D500D8FF28CCDC8EB9 + 9CECE65BB8FB4D0160B65D3611EABE0F008EEF00D67C3CA691435580EA2EAA01 + 30DA09804FB4D1C50D003E5D00F0C9A2E30514E74861C06F56F12FD87C3C02E0 + D85FEF07E0B74400CB07E2FE8AE2DB781FB93FA502B9FB516CA7A74D84FF08BF + D37B016053F167B9A2CF2B2A94E31BAB7D65CA8BF03702DBA8AE7AFDCAA46853 + DBC98FB9179F66BFBA4A0F6AA3AEFBD3506F1FE8EB4EFFF4D72D8E0B041EB6EF + 8F5E8918F7B1D136FADA7F5AFDED5217DF1824A0EFD8C44A9AB0FB0310B82DB3 + EF0AE7B80700EC80D7EAF4F0189692F67CDAC965DE6E04C051FD00E37EA0E37E + 6CF7A2AEB6748CBA2CE6D76551A5FC5B0F80A36DACABBCAF43D6B6F3D40857BF + 697D0780F7A9E6475FDD0006690F00B8C77E8FAADBD8ABBA6EA4BF88E241B61F + B5F57A0719FD061ADFA995067CF5E02BE6EF4583DA8B793D836164E53D8360EA + 300070D960BCA806C05D8AF6B5760EDCF72F4D0CEA09429D8F1A086BE9D84654 + 6DDB25D944A92C82E1A838E9D3C42BCA27696552D6DFEEEB01C043365107EE02 + 634798C8DB249E28D9C3E7B7D2D18BDB696CE9824D4C993C67E84A44B04FEA2D + 1D216EDCEE05004B3102B816C739306F7DC3C9306BFD9A3E9B466D223B3C6B13 + 729B141F98B7632D9D4D40D5F1998B696CE6521A9DBB66E771231D5A5A4D8756 + 56ADEC7A1A99BE9C46A72ED924F962E3B0185DBAD8805FB6DA17DC1D048095D7 + E39469228149E344C1D1D24FBD8E1677CA14C78F9C3EDAEF71F2986299F2A21D + 0E277740092C17B5D1C0567E188717BA92461C04130D6CDB63D75D23C732CC1D + 3B7A358D5BBE03DE133BDFF9A6E5FBE40C24BD17F82BA79BC0AE9C78EE80B3F3 + 613F46F80E923B013D72A4D7C9584B0E4A391FEB3CD4E3E82C927354EA71B4E2 + F8ACF6EBBC088069ABCB1E017E23AC8DD1BC11FE4AB28B79FDCAB4AF36C9CBCB + 6266F02BB1B425225D036020AF24202C914794545CB613A73AC2494F7E5E2E14 + 980BA4C5799FE1AE24782B47BBA06E3F455BBDCB57F91CB38E00F67AD71FB832 + B06DDF9DD902DFBCD46AAB08809F361204CEFB8F2D9D41307600A785B5A769E6 + C6E366C962967E9DBA01B8C9105840CCC130FBBC9FD4DAF2F7F3AE3FCFF0D7A4 + 285B96A43D7CFBD3BC342DB00930E540F18D4B91BF0E9980610122F2EE58201D + CB3F0BF20AA0B5E02EE72D5A1B406780227569072056035549C7204D796DA772 + 014ECA49D7F9D8D6A058EDA81E79AA8F1D22DDA5D886EC48D7C22EEE6337CF12 + CB4047130052D1AAECCFDA3572E877EBD3B474E70B07BF0E7F6F7FE611C00042 + C0EF6140A7B7958FBD44E43651C0E1B8F15CEABC5C279F43D66B6BFF759A31CD + 017D81917CEEDBF9987CE6CB66AFA5C201C17CB612FBBA1EFED98668E708839B + F3EEC97FE1E0FDDC8B1FA60B2F7E940EDF058CE63E6B4BDBFA0EF9F7C88EC9F7 + 367FBFEC7A1628CBB5448059FD06046AB31EDBB56EF304806953E037B76FD7AA + 2C394ED47186CDB9CCA17069D73F3F4BF35BF655054CF14F1AFE5B2EEDF23B20 + 0278C57E67271F7F65E7F9CECE8D6BFFD6C56F8CF76CF37BF6EB6AC79D5EB5DF + 39E93B9FA559FB6D4EAE7F9226D6DE9A3E49935B6FD3D4F6A769C2EC58267ADA + EA02D701CF8BEB2C437DCF4130405851BE11F20AEC0A00C77C140130F2F2D57B + 0E76057C6BF81B01B040B1F2E64D0E7F37AD1FC1A60B000BFED611C6FD003051 + C0F94F40F9CF395111E422E545482B808BB089F651AAD382DBFEE057E56A6B02 + 007C71BD44FE5ADA95DBC9CF37EB933FB7EC7CCB52E292FFA9886D59067AFAEA + C3347985E8DFBB45774C79A58C099EB9E5799CFF8495238027CFAFB93200B667 + B48D093200B6E77889FCF5ED990C7F79F76F568E00D6B8C2B7456A4322CFC73E + C53E4A0098745D272A8E977CCC57EC55A6B15594FE80473AD627AF85BFD86600 + 1C357CDCC686658C28005C8F1B23F0451A032B5FE34A8D3B057B91F21036B5D4 + 16F26353BFCA47759EC6E631AD313A69C6F0CC1B48D3267694D5E3FB3A2FEEC7 + BC1ED84B7ED8F7BC32DF88E981620E16E0AE006F976239F5047F1B9B054B0F00 + BC2AEB5AFE19BD0F00465D6528A6BBD40F00036501C083967B56DA978B66BB47 + 79DBD62E8A10987D3F86C3DF1CC9EB0078E66C1A99B5393B2B78B90DD1BE48CB + 45A35CCF57F27259FB76ACFCEEDFEC63601B0170F467B8EF22F839A22893BFA3 + 9F1FA5DE1FA458DF35081EAB0C3F93D28D80BC8BAD22FC95C8AFFD5EFD54FC60 + D167B65FC9EF260D2AABCBBBECE447DDAF3F35FA61A36219691DAF3EEE4F43BD + 7DA08F3BFDD35FB7382EC0D78FEFD1AFB3E96001C12A6F0070A56F0B47A8D946 + 3F11C98C2200261238975366E73D3C6BD70096D58F69B5FC66A7AC3F1500DED3 + 12D03560DC0F74DC8FED5EB45B5B3A5EB4DB639D9F2A00465DF5513FDB3A1DEB + 7C2C59BB7E5D225CFDA6F51D00DEA77A7EF04AEFA20E008C74738A79FB91EAEF + 575D37D29F77753D64C9DB8FDABAA4B3E243FD3BF597067C5175793F3B29DA0E + 5AD68581ADDE892261DB03808B6A00DC80DC087D59163A2C0DDD652F750EECF7 + A97E93883A1FC5094D6CA3B66FEC6CE0D3254D969A4953C9D704302AD6ABF3EB + FD5AB13CAB1B00A3DDE06F9798185287C8DA11006799B8036F8F5EBA9DDEFCF0 + 77D39B1FFC4E5A3EBB61D788492A93E80C6D1DE86A5B01DE3A2F4AF55104BDC3 + 38118A62FEA17900F0393B96F50FF83BCD3F9FADDFFE4E60BB4E8BD6AFA5E369 + 62EE785A1C3F9E96C64EA5A5A98B697EFA4A1A9BBE9C0E2E5D4FC3CB376CC27C + CDEA5E4D23F39732BCB6E38C013F572EA503B60FB01D058A9677F5EE0E70B304 + 7F79A797A4BA4069DA63BBA35E881C6E9C2738668A53C7DB8DCE9D201D93B49C + 4DB15C79C325F2374B0098FCAC21EB1BDB6E402B071B201865A00B088EB03702 + 6041614503A3EE760B1CB6F6878E5DEED1A1A3F6F99870042A0204276006BC59 + B583903C2D97189D8E72366A3BC841A972393DA36314A72A8ED128F2D90ADEC6 + 485EED0BFCC6B46C6A5BB6E30500474570AB3412C0AD6D637E17348ED212D082 + BF44FEFABB7ECBBB093D9FF2CB38CD71A067E01AC12F6AE0AA47F566677EEDAC + CFCE7ECAA2F31E477F6F74AF006E2CCB6DB7220FE00BD091D88F1018C986ADDA + 24220B000CAC05F87A54E04D6B8F762A00ACA59CB37AA1AFD25AB219C572DEA9 + ABA85A493058518C004F96D09DBED96B1B8194409A03BC02F51CD86E9AB65AA8 + 093004A80AA22A9F745C5698B633E86AE5C0ACE43BB403A695B6017F02878295 + 9240A5849D2274BB6C7BFA0490B6FEAA8EF2B0130895BDF695A73695A61DD9EA + B8A469973A310F91C77125B51BEBD3260071712BBF2B57E0D29706BEF5495AD8 + FE342DDEFE3C2DDFFB9E8B25A1E7B6C9FF24AD583B876FF1B9E5B6069D13653D + E5C8F6F99CF9BC595A7B857E926FE7336D5B20F0F2FDCFD2C29DB769D63E2BFA + 33B799A371A97BF4EEA70E81BD0D2B67C966CE1370BB64DBD9AD176966D3BE13 + 25129877FE0A007B44B0A5058073593E0FB5C156E710CF47D7CDAF5D396FD515 + 58D6312268EED12DEBD3B6D96CB500B7FDAEDAE753D20060445E86C1B9AC01C0 + FC864AFDC50DFB9DF23BE75EB006E054BEF5133BABC3B2CFF97C32F03D7AFF73 + EB678EDA5DE68F1776AD89DE9EB6DFD1E4F5877E6D8F583EC0986B7FF8C1F7D2 + CA832FD2F4C69B34BEFA3A4DADDB75BEF5D6238327AD5F93F6D94CADF1870F3B + 3FEBD7D20651C8DC73ECBE77D3EE5BD6AFDD1401318AF0573098EDD40DBBC715 + 201CED5497AD6CBB6CDCCED28A08661B977B16E0D532CF713FA625F63D0FE05C + FE941325E02B488B22C865ABE74854B4631BDB8975F51C92D8573DB5EDFB66CF + 3B7F59AA3A2B9713FDEBCB36F31A84F2FCD08A0F3D2AEF989EB96AD7F6CA03AB + 679FC555FB5CCC9EF7C2F39C04248F5FE08F55F65CE6F90BFCB56309024F9DB3 + 2D2B8030363069E9E726FAF7ACE98C3DBF7B2280511E87D4630C8D3DE2F82382 + 5E4965319F719040B4C64E5DD2B8497D8E658CAD047923E8ADCB5BE5311DD057 + 634207C08CF3D81E2D7F1A2CC03682E0060E17988A48CB16296A98740D80B51F + E1AFF2681F9B060A577524CA18B73BF02DFBB14CF27D1B9333D6A79F112C6B0E + 40B9B7439D32B6571D95C7740FEC254FE908754DD16E50B9FFF1B66385A3A8F6 + 4FAFD98E2DF554B7290B00B806BD715FDB087F513F005C03DE4160B7CB1675D9 + EE0680057D057BA3C88B00B79FBAE0EFD01CFD6C01706B73BCCCD90B009EB639 + 3372184C74EFC9D21FFC06F809B2BC7F53768E13593EF79FCE36F231F4F81D2A + 1F48E30B09FE9168D3038005608B3FA55FBD7E6ADE012CBF4C686F475E9DDF53 + 5600EF20002C05109CFD5B59D1EFE565A67E11BED19F569745F5B3D37EBF7229 + 96D78A7ED67E7A9F3AFD54B7F575AAF7DC397EAF6FFAEB16C724D257D1BE876C + 9FEDC4CCD134BB782A8D4DD9F7D6ECC8EFEADF4FA3CF5DEAE21BFD246602F815 + FCED652996EE623EAE41F01751DFAE5568F3BDDE011CC1E36EDA8FED5EB45B5B + 3ADEFB1CB3ABDEA0F6FAE547C5FABBA9AE13F63B97B226DF74E850A9A3FC8F29 + 8EC1B1235CFDA6F51D00DEA7C20FBE4D772802DF0AFC7E2CE59BD6FED57523FD + 368987CB6EDAAD5E577997F66B9F853DF57A1FEADFA9BFEA815FEF0068B05D94 + 6C7603C08DD857BEA4FCC6CE06D8410DCC55DE4700C03E19E8C8EF523D89F898 + 12DC1D047A9B8953505739E938618CF96CE3315477A798D4957400B9E87D00B0 + 4404B0435940A86D01BDA76E3C4C5FFEFA3F953EFF953F9D56CE6DDAB5B64928 + 4B419BCD3090D6E4F61D8037A6A5AEBCA8087DA5E179DBFA71CE99CD7907C02C + FDCCBF9D0F2EDA3573D9F55B389A262D6F79F2445A1A3F9516A72EA485C94B36 + 30BF60E597ADFC721A5DB8960E9FBD9FCEAEBD488B17B63C2216000A9C050003 + 6581A382B602C1EC4B11E04A0E7C0B90ED9195F5AB935580AF6D2300EE71F2D8 + 560EA0E80492C3474E2EA43284AD3BBF0EA30BBEECB347FDDAF909FE4A4402B3 + 1C34EF0496B32C3ACC468EE22CBB968814266218801B016F14F0B706C082BD52 + 937FFC6A1A3ADE0B7FE5F4632BA81B216F17008E4B0546A762743222392955CE + 564E47ED4747667492D64ED5E86865EB91BB675B004C3AE6C5FDBA2E79CA5764 + 6E86B319E20AF846298AB75F5E04C0D14EEDA2A94B44FB22406F86BD3B00302A + D1BE2DE4CDEF31440E54CB360358206E76CECBE92E67BD00B0F2B2B37E27D065 + 4B9EDA7470635B6CA254B65822FB6299DA241F1BB53173F5BE3BE271DEB760D7 + CA0A0016F87550E46A9DFB405D54C35ED2FEBEDE908714FD2B90DB03A6481750 + D60033CBEB928057865E190CAAACA9BB59206B290766ADDCFDA485B1948576A4 + 781C441EF0573052C0AF06A96C23C494ADE4C72CB64DDF6C5F625F0098B68FDC + FBB4A9A336641BDB268D9DDA8CC7919DCAD9D2766C8B3CECC8E7BC746EB1EDD8 + 26D763692B2F018D58BAD89759BEF3CE233E89FC3DFCF0AB74F8D1F7D3F2832F + D3C2DD7756F6A903DB15E0A97D16FE99589B3A86C4BEFAABFEE9B851EA339FE9 + BC6D67ADBFF3D66F3467F5A7AD8F7A1FF03CC7B3360EDF79EBF098FE3710D9EA + ADDCFD342D58FF67AD6F33765EB3C0D52D963CE6B87C1E4F3D0DF8659FF4D276 + 7B3DF8BC04E9D56FB612FD8CE7405AA26DBD5F180934738C65AEADC98F77FBB9 + 5D433BEF6D60B695DBF791EF74FEBEE62DD097F4DC9A1D6BDDFAE160983CFB3D + 84DF98D7B773596049E9F5F2DB26C278CBDAB6367967B1BFB798A86DA27CED9A + 2EF39E6DFB0C01C1BC631B000CF4E73AE89CF9D306F578FFF2BC1D97C8FB25AB + B344C4B089A5B9276FDA35060ED30EC0DDAEDB8C0360C036C767596AEB13CB34 + AF03607B816E94402D91D1393ADAEE5B26015DD94882BC317218C52862D920D5 + 9F2AEF1E6ECA6C4B4430F747A28AB564B4968F6E238481BEC05EBBFFDA56E95A + 2CC7CF33203F0BFA2B425AD2E44560CCF34360D7C16D81B894E563F05C6A9F3F + 940383C9D3F348C75099BF3FF8627E364EDA733183DF0C7FFD8F4E57ED9964CF + 0EC15C3D47E233C2977DBE62D7F1B23D234D93E88AA5AFDE33B19F57CA18E719 + 7FD1C60482BF0E9B79EE0380890406F0DAD880F1C169C60D19004F9DB7FE0400 + 2C6538DC8E2F34B690E21843E55AC9448AF61275F6028011E551B18CF1551700 + 66DB82DF361298B11CE33C8D0B35FE633BCCF814581A24A8DB883CC6AE360606 + B46A2C1B6D490BCCAA1DEDA3086A6523E0ABB6D8463BE5316E8F7525D9368A63 + 75DAA47DDB1F24EA08FAD612B8F57945D817AC95E2AA403D75AC2CEE37F0B6AA + 2F51564362E5ED17002307BB655B6B3F0018F582DBFEF017D5B6FDB51300F7CE + CFDB39BE00EE2009FC4A7B01C0C05C20F0D0945D3344DA75A2F4071FC28A6B68 + EA701A667E3F69E709FCED00C0B5047CBB24BF4857FEC7F0C9C436F6A39D7597 + 4D05F0EEA63153E3DBEAAFBD026054974B5DE5755E974D5D566B5059ADBDB417 + 416F3F75D5FB66C4F1BBFCCC5FAF382E0098F7FFB23F397B2CDDBAFB227DF2D9 + 2FA50B57B6D2A1A139538E9AADEBC5FD9FA66AB6D14FCE4C0E4C3B8B69F2E032 + E495FC0307613C8565016F1DE0EE9DDDF41CCB990FF569739FDA2B70C4EE63C2 + C9DDDAD2F1DEE798FDEAED373F4A367B515D27ECEF0900C7B28F258EC1B1235C + FDA6F51D00DEA79A1F7CF9A1F793DF50F67EF3D84DCD8DA5237FBFEABA917E9B + 2428DB4F7BA9D365D34F6EEFFFFED9454D1D8ED1F520FFFA540F566A75D5F936 + 290ED2761BAC75D9A2AE816A54D720D7656DF6883CFD93D2CB193017D0DB0178 + 057EA36A1BA977D09EB5DF49416DBF17D593977E8AD037A625267CDA2A3D685F + 1346E5AB8CAD00B06CBBD51F00F7535E36CA26951D6528FF7BF8B4F5E1A4835F + 81DDE963D7D2C5CDE7E99269FAC8152FAB61ADF605770578BB406F6D53E7A1D8 + AE00F0F0DCD934347BCE74BE6CB1B1C9F8825DB745BB86B63D609A58389116AD + 8DF98953696EE26C9A9F3C9F26672EDAF95C4A07A64EA7B1C3D7D2EAE3EFA797 + 3FFEFD74E3D1E769D2CEEF801D877708036A057F596A9A34D1C002BFB504717D + A9E7027C6B670C0018D13E763BEB67670B5BE0EF88E5B11F1D3D6CDD8955F206 + A9B61700E67DBFFECE5F3B1F60EF0E15E753B6B1345BDB57B4EED0916B69F8E8 + 754B5F3365C799606E5CFAB9166582BD3152581AC3E1778277BF6509E86A5F79 + DAE200CC90373B0763DE20008C33D21DA9417250CA2EDAAACC1D95B6AD9DA371 + 5F693958A3333502E06813A57C206DFD2EDF7E1058B6B53D69843D6097BACA27 + 4FED09FC4EF34E43D7B6E5A35B2E2D01CDB2CF5AF612182BE7355BF6057F2587 + AE38E70B1498BA66ED98040A3218001690B79DC14100C0C821AD6DC9E73D89E4 + 016FE74B64558EAECA2017B19FCB6C3FB4E3E952B6C0D2CAB6455AAA33035D2D + DB6C762C8D0A28F6A8DF1C159CF5D8F280BC2CD9FADCE5EFE6B5AD47F9DE0020 + 037272196987BF26A26D055605A514E14BD4E10E78BBD58233813687A4563F43 + 2FE01790CBCA0BE0DA01BC365F3A3023E273059875FFAD477C7A7463006919A6 + E57601842AF3FA05E401BB88309E077CDD799396EFBD4D0BB75FA7B95B40AD9C + 2FD5FB75BEEA11793A5FDAD7F9034101B18288804ACA754D28777859A4BEB165 + 5F103596534F792A8FD2316B008C480303176F595B5C4BBF9EF6996F09285A7B + B73FCD00F8FEF71CFE1E79FC03DBFE202D3DF83C2DDEFDD4A375731D3B073BFF + 2560BCE52FD9B1B896515C9B5ABAC6887DB7B5EDBCB533FFE05D9AB5CF62C2DA + 0600B314F4AC2F07CE79731E76BD6EDBF17857B0F57DA500E0658E7DF74DF3B9 + CCD97598B5677B04C02DF4B5F28DA7A52C5F6BAE9BBE975C5FE571BD74DD751D + C9C7469F03A26D00705484C08D6EDB7506FE9AFC7DC1E5FBA9EF6DFEEE5AFFD6 + F81DB0CDBF0B34C73B7957F3EFCCBFDFD63E9F1911BFC04F07A84418DFE2BC39 + 677E6F56DF3E5307B8A6E5BBEFD2CABDCF1AA80E145EB863D7DECF877A0061CE + C93E131351C20E8FAD2E4B72F33E60DAE33DC12C179EBFC7FCA60B00E7BCB9B6 + A54FEA5704B5B50473058025D5112896621DB6B2612BD04B5AE59491876A000C + A006DEEA1E1E97908EFB887DF2A51A04B3EF51C401F60E9280EEC24DFE1CC4BD + 3DC3E1C92B2DD015C88D0098ADE02F75281FE33DBB56877CD511FCA56C9477F0 + FA8A180059C1658E75DB216E86BE769DAE3FB2E7DA233B0FAE29D7CEAEEFAA7D + 4E5C3B7BEE603FE1CF4F3B2E00F8E21D6B373F3B1DFE5AFB6317EC595D94DF39 + 6CCF789ED1F6BC9E3A0FD0B53CC6088C0F00C06704796D3C50227F59FA996D0B + 82CDBEA8FEB3594C4B711C13A5FA6A633700EC63A45AB41D449E20AFC65ACA8B + 505865136758A69AB23C0ED418CFC78836FE14C01588ED92602D225DDBC63CBD + 4644792882DBD806A24E0D9195CF568057653DD0B7C8FFDC6962FC5D8FF1D98F + 11C11ADB4BEC7781E0086FEB7DCF2B90B60B00AB0C292F02DC2E45D01BDB5119 + DBA6CD010038425E87BB212F96ED0700EF55DDA0B79F760260CDE9E33E12C0DD + 4D82BD350CD67E5F001CE4F9A50FEE23985C71018079CDD3F094D94D9A9D4360 + 6BDF7D0F2B65DB2B7C2244F576BDCA0A9F48575EADBA2CDA0F526C23FA77A29F + A79FDF47F959CBA6027877D39829FABF6A055F58971FED43D4CF8F3728EF7DB5 + 9736B019142CF24D28FA68EB746B47BA55F44F77A9B6DF4B9D5AD4F1E85F9679 + 1E9A4DF3CB67D2DBCF7F39FDF6EFFDF974EBCE8B34042B3898816957FD6F836A + B6D14F075061324D9E180D82A7F44B77C9DFF90B136A6D9A767D9FFCDDC161E7 + 3B82F70A1BF7D0FEBEB45B5B3ADEFB1EB3AB5EBFF6F6721CD9EC455DF5919575 + 02E0AEB2BAFC43A5F6235CFDA6F51D00DEA77403D82B001E7413916A9B6ABFF7 + C6B2532ADFABBA6EA43FEBD2C3AC7E20C6F440D5B0B7436DDBEDC3FD9B90060D + 83D455EFDB220DF86A75D9A22E5B540FE4EAF21D035C4B772E7F530F9847910D + B00700E03A7D68DC2601414D79CFA03DAB1ED8EF1CDCF7AACB7E37C5094E57B9 + AB03F84A82B512794CC054AEB483DA3231939457DBC67DD551BA955D3FA503C4 + 1DA4410058652336116619E843B326DB2A0A3843589BC4036B4D94A12EF8ABFD + 581615416FB4AFF725DA199ACF007874FA5C1A9DB960E9F3767C2B5FB089FB92 + 4D8E5DC7D3818563760EC7D28C4D8EE7A7CEA6D9C9B3697AEC749A9C3A97C667 + 2FDA753DE970F3EA8377E9E9F77F3B5DB7EDF4891BE9A0B50D9C25DAB701C0D5 + BEA4A86024882B00DCE38429CE1901606CB08D75B3B2C30427CCA8EDF39E6596 + 68AE9D3CD1F123C969149D40A4C973785BCA057F7B00F0111C5DA69297A3845B + 65FB2B669305003E74E4461A3976A3005DEAE7322030B097A59FB5FCF35E357E + F27A1A39696DE1D42B305760578E3F29C25E0160D9C67C39166BE18474676A50 + EDA0ACF75DEC638F13B43851C943FD9650545AF5B4AF36E45C9583D56D0ABC45 + 02BA82B628E609EAF6B315E4153C569E24002C1B2280A781B825EA571058EFFD + 05F40254498FFBF29801001709D84E5F6F018022C67602603B9683E15C4FE218 + 82BF1230B701B701002B2DC8AB747C27A3D4C2DF02829BC8DFC70E8379F76F8C + FE15FCD53EEF09CDEF0BCD90B75F04F03200D5B60DE4C5BE0057F6056A056B05 + 40057D05A404D9883274005C6CB42CB0D7B5366837025DED3BC44580B83B6FD2 + D2ED0C3681C2B53D604DEF8B557DECB05FB8954121D0160809C49CB7ED2C7945 + 73DBAF7CEB118E402EDBC7465A007016F9BE69D1DA0126730CCE4D1051A05020 + 97B440A3202D12588CF9AA07D0D57E2C278DEA36633965A489FE74A0E700D8CA + EC1AF9750396D25F40EB3630FC5D5ABCF7450F005E7EF8455ABAF7CEEA5BBB80 + 53BB3644ED72FECB00608789F95AD4E2BA2C715D00ED5C33CBD375278FF239CB + 07FE2E3DFE221D7FF9C374F4C997762D3F4B87D1DD77766ED6D6865D8BED4FD2 + 913BBC333A6F1DFE963623585ED8066612C90AA8CD694991BA2C7BCC75E1DA72 + 9DF41D8DD7315E3FD2DAAF05406D4068695F105802FC02691100B8B9FECDF736 + C35E4160BD039865A049CFDE04025BFB44E9D2BE2BC3D3C5AD0C7FF3395A1B5C + 0B6B9F25B4058097EC5ACE17A8CEFECA7DBBAE5C2BABE37DA40FD61FC0F12C11 + BE26FFBED8B5E6DAF08EE079B3E137B568FB82BD2C254EE42F00D897B536B13F + BF41BF72DF0464056F2501DFAE32445E04BA0DC02D692400CC36425EE549319F + 65A023F065EB7F963109EEFA3B8389622E79BAFF4B1102E7729E073BA5F704FB + BB826D5FCB48F37E612D2D4D19EF22CECB515BBD12C91B15237C63A4AF80AFD2 + 5102C1805FFF53943D0FDB679C2278EFDBB101C0ACEC90EFF14473CFDA3362C6 + AE27CF2B9E812C153D1E9E9D3C4B59F2798C659F2FDA33B82C2D3DCEFB850B70 + E619DDF34CB76D8E00663C72334D9EDE485344FE06002CD50098710510D9C715 + D5D843E312E5C5FD985FDBC7F152333632F97E95E7F9C15E75EA3FD921F22214 + 266FD2CE7BF22C75F2EA2F8CEFF4073F8DF95C05B60ABC0ABE4A5D60568AFB8C + 6705809B764AF96819473B882DED518EED2000AC3C5443DF28C6F2B45D835E1F + BB5B5BE46B8CAFB1BFECA438FEEF01BB9497F985E61A0818DBD884BC4112F0AD + 15216F6C4BE5CAF76D0580FB415E87BB1F0100EF15100BEEC67497B23D699B97 + 87F97C142B53F9BB784304EF6E6A00AFCDFD07BD3338CEF97DB9E70600E7B97D + EE43790730E057E2154F05000F4D5ABD49EB5BF037D4C227D20F0077A9CB7712 + CBE4A7897506A96E4BEA2A8F7EA05E01801773D042EDBFAA35668AFEAE5AC53F + 36C8E7B65BB005AA6D06B517CB63BA9FEABAFD846D973F14C5B694FEA6D5D5AF + 2EE157EEF25F7729DA4475D90E928E479AEDD4DCF174D5C633B7EFBF4AA7CFDD + 4C07E99B584455F77D8EF775A88B6FD4D2FB7FC54B4893BF633FF29A986ED4D6 + C900D8CA2B3ED4005DC1C25DA0E1B71E00C7FEE978EF7BCCAE7AFDDADBCB7164 + B39BBAEAEE451FB3AD2E597BDF01E09F3575DD0CBAA41BC8C01B4A519D5FEDEB + 4616F3A254BE5775DD487FD6C503E98300F01ED4B6DD3EE0BF09750D186A75D5 + FBB64883B53860ECB28BAAEDA3A24D543DC025BD2BFC4502C04582B90DD4FD19 + 07C04DBB1F00802526650DB03569A2A8FDDA5E8A757A65D74FE93219DC4D8300 + 706EC7FA64E5C333A71CB66A59E7432620F001268C9626329848D5A6BC40DA08 + 70058CF70B81BBE40098F666CFA4317FF7AF1D7FF66C3A38773A1D58B0BE2DD9 + 647CD1CE6DC126E62C9B657DE5FDC0938B17D3D8ECF9343C66E734713A8DCD5F + 4CA3F3D6CEC2993477EA663A76F94E5A2082E2C8155F025A2097AD2F911C00AF + 60ADE06D84B8947BDD0100F800807817008C6D04C0387FA293273A7E24015ED9 + 48CA971CFCE260B27395B4A4F308FB00613BE75EF89BED33E025FA77350DA163 + D7D308EF02B6FCECF0C21186E32C2FFD5C0360A27C3D6DF9E3A7DA7CC162F246 + 4EB6005810574EBE2EF0ABF26E6527623FC989C976904333E6BB6D7184B215E0 + 95E4205579ED3815E4557917FC4572FECA012C70EBB0F74286BCCA97C3B84B35 + E08DA03796C53600C00E7F4BE42F9AB972274D9B14D98B62A42F9A4544DB4AD7 + 582634030041DE18059C95D35D00586AA13230B837DA579097A85E69F1E613DF + 0AF62AE2B785C0EC3FF1ADD72911BD35005E5CA76C27006EED9FBA047E23FC45 + 44062F14382BE02B7025F02B01718145089016B702C011FE524634698648196A + F17E598F8EB463009F1DFA92B6E3795F005CB75FA585DB6FFCBDB43500264D7D + 458AB2CFD6A3574D5E1FA06B12C4CDEF8EB5FE017B1D6EB6368DED6D3B8ED902 + 2E01BD3D51AE9626EFC883770E14756E02866C23C8F5F3B6FC086995C7BE241B + 45F492C7BE202F623FD6EFCACFF5DEDAF613BB1ED6F696F5D7AE915F673B5FBF + E65CCB3BEFEC9ABE4BF3773EF3A59F571E7E65DBAFD2E2FDCF3200B6767DC960 + BBFE82E4BCAF9777F8CE135DDB71EDBAAE1769F256EE0327DFA469FAF0F4CBF4 + F28FFE5AFA93FFE27F2D3DFEBDBFECED2E599F8F3DF8C22356E7375EDA676AD7 + F8CEA70E7F8FDEFB34110D2EA8CC67477F688F885B60A8472BB32D51B802B5C0 + 590160AE15D787EB24E93A93E6B3D4E7A0CF24DAE43C3E6FBED32DF4555AC7D7 + FB7F3D5217605BBEB36CF377D4DA3209F8027FB59FE170FEBDF96FACFC560450 + 696F0900CC71F94CEC7A00DFB98644FE2EDFE5BDCA9FA6293BCE3411F75C4FEB + 3B9FE1C26DFA06D07D9EA6579FA6693B06E5BC1F9848E005FBEDF07B9DB1FB0A + 90D7970007F8720F69006E2ECB7D6A7FCBFCAEFB0160D5452A57593FC9A601B9 + 05EAAA5CD057652AEFAD67E537EFF544000B0047D02BB02B505C43E008807339 + 795911FE4A82C0A40582A34D06C3A55F21D237825ED251941309CC36DA28DF65 + E9992BD6BE8977D9F38C9B76F0DBC2DF89AB0FD3D475E06FBE8F4FDB673D05FC + B5E7DEB483E23B69C29E91E3F68C9DB663CC5B1B7336DE9CE6550A17ED997B29 + C35FF5C1D366CB739867BF56F598E4596EDB89D337D3D4E9B5347D76334D9FB3 + E7F6F90C80919684CE020EF78E2950D7D844F9716C528BB14D5CFD64D078C9C7 + 4221CF556C631DC667314F6D92AFB11B7900E00CBE69AB77DCA7B1E2F049DBDA + F850C056127C8D1036825929EE0F04C095621DD92B4FC7521AE0AB3E4400CCFB + 81D97A7918CB938EE3780030DB98176D10FB1F02806319F9FB5517E8555E2CF3 + F20E005C83DE0F01C0BBED7709B0EB91B561BF9FB20DE90C6E057C35A78FEA02 + BC312FAA07FAC674B5CF5C5DBE01CDE97DDE3E655B6B27EB88E5ADD89C7E39C3 + 5FD751B301FCDAF5A800B07C09DAF7BCC9F707C0F2C77495F553E343B1F4A07A + 7559DFF700BB3E0000CB1F16241F5AF4A5ED05FA46BD2F00DEABBADAA8855D97 + 2FD4B725FDD354DDB7A8588E5F39FAAF4977F9A051EB87EEADD3653B48D465F9 + 671D6BC8B65A129A2D4B3F3B38ADEA7D9BD4C537A49A8F88ABF405C2405D07BB + D40DDA9197EB7D2C00BC0302EF153462B757DBBDA8ABADEF00F0C769AB4BD6DE + 4703C002B735C0FD500D04C0E40FDB490C779CDCB7489DFFB2785FE9C7DF056D + 8B0E56DA611BED635A6BD1073BDDBC9A9B5828EB6723F52BEFBA91FEAC2B3E14 + 6B75D9D7EAAAD7ABDE07FBC7900630BBE5BDAFE220E9DBA19DD795C187A40125 + B66CD9EF1D64CA8EB276FFC05891D934835CD2458A02EEA7087F6B09EC766907 + 046610DF3178D7003FAACB4EEAB2FF282A60B74B35FCED924FD46C4296A12D6A + 275A9A780D1213BA988EF2FC2EA0DBA1385964F2D75366FB3E219CB0BE4DDA64 + 12706B3A3073AA792F6D84B211DC7A4470D0D0CCE934327B268DCE016A330016 + 1076FB52374A9017C5B62460EFC8CCD9343579364D9A862DCFE1EFA2D55FB47A + 0887C0CC49EB33FF46C7F171D9CA2F5AFB76ECA9D369DCB6633814966DF23E6F + D78F77259BFDA1A50C7C1DE206481B2381C913B015C08D223FC25F04F83D443E + 6D94FAD15EDB36FF822FFF0C0476A70C4E9E0E474F94F2E43C72E7547110C57C + 803260D7D387714CEDD44881BF51EDF2D0571DFC1E3ABE9A868E5FB73411B9C0 + D9ABE990A587CA967D1C7872EEE1D0F377CCC92158A4F268979700EC05BD82BA + 5D0E40E5E3209472793E1EEDF61EB7B71F3819A34353523E8A0E50779A5A1E4B + 21AA6DD521CFF3690F9B0EB8DBB51F9DAF6C71FAE2048EF0576A2030EF28C481 + 7C312BDA280F09F0B2259A49FBCA93D86FEBB3BF6DF638C1EFA419006C90A06F + 768AE73C2DB72C79D9B5D6B12F477E74F2030400C093963F89039EBA37EE37C2 + 81CE129A80608F06C6A97ED9D20502C7885E0160C1DFB96B0FD282E52F0280CB + 3E5BF2960A2496FCBDC105127B5B375B00AC77064BF978005FDB02816F3CB336 + 9EA7C59B2F1AF83B07F005389914012C08D503A4B0DDE89520A82408CCB6B7CC + F2368048C0A0BC3CAE47327A74F2D3B4BCF93C1DB6FD23B79EA7656FE78D4736 + B25C31906A89A8D502C916AD0C48B8B26DF95BC04EA0DA5B6BD304E044D44580 + CB7B44417E9A66ADFE8CD567BB78DBF2815F8DAC7DABC7564BE9929EDB7AEDD1 + 946C899C5DB17C07B52C4B5C00B40B201BF2041105100511EBFD1A34D612E8E5 + 9844C43AE4B663381CBDFB691301EDC7A65F44EA721D4C5C238FA2B5B2C366B3 + EC36EFD2CAFDCFD3C2DDCFD2DCDDCFD3E2C32FD3E2A3AFD2FC832FD2AC9DF7BC + B53B47342870B1C05DC16F802EFB02C002BD9EC636EC23016004209EB1B2536F + 7F927EFD6FFF83F4CFFFFBFF93F4FDBFFA77D3E283CFD224DF17CECDFBCB39D9 + F5BF95AFEB61EB0FE7BF68D787E3FABB7F819F006013CB13F34E5CDEC17BF8AE + D5B1ADDE9DBBBC9DA383B906F9B3C9D74F7F16D0E7455A224FF92DB4B5328068 + 11C740FE1D06FE9AF2FB952D8F25A01DD496EFB8D5D7EF00D0AACF66D1BEB70B + F6BD45CA43BE0C36DF3597D5B1B689C05DB8F5D8CAF36F87DFCD3CE2BA731DEE + D9F561E9EEFBF6995ADF67ADEFA75EFCC8051C9EE6B768D78C3F61CCDBEF8CA8 + 5F7E5B00E3C3F61DE07873F61BF468DECD2769C97EA3BCDF777E2D47C61225BB + B8C19F492CCFEE337ECED6DE9CDD1B66A947FF1C10B710967D07E0050C23D202 + C12DA8ED85B7AAA7F26843998EA3BC2E008CC8A3BFDC1779973AF777EEF3A4F5 + CE75B6BE6CBF95E97DEBCAD3738174AE43FAAE2B02E09816E4CDCF8ABC5F0361 + 45052FAC13756BE59737EDF8405B00B03DDB4CC05D2D170D688D005879B2C97D + B23E20EC78DE5DB5E75979E6CC5CE3B904007E68CFAD8769DA9E052C333E75E3 + 719AB067C3C4F50756565E9160CFDB89ABF62CBD6AED5FD94EF3576FA7854BDB + 69EEC2569AB167ECB4C99FBFC0DF028079A6B3F4731E1FDCF47700CF9C23E277 + 3D4D9F594B3367D6D3DCD98D347F6E2BCD9EB76B51A2808908060A4F9FB7B62F + D8F39DFA659C526F69D3DF2D5CF27C1C5324E0EB63202BD73EE39FB8CA89C63C + BB49ED4731668AE3B33ABF1EBBB551C179DCC79F01F52E6007C0FC39D0C69DE3 + 367614BC8D0057523E129845B59D24804BDADF231CCAA847593E4E6E4FB62A17 + E8952DD23155E6E56865E7789D741CDB33AE679F7985F23427E827815DFF3329 + 2B0A957D24D81BD311DC46802B70ABB4EAB0950D529EC4BECA621BCCD78697CE + DA5CC7B6CCDB02E48D40588A654A238160B79B679EB813DE0AFEEE06806BC536 + 6A651B9B378779B8007004C1A4FB2DEBDCA51AF6F6E47BDE8ACDB50FDBFC76C5 + E6FD87ED18ADFF2047FC1EB56392C6C7B062F3FCA51E0D5B9DE169E6BDD67F5F + 02DACE87397050ED0F900E4D5859D0C1713BEE8E3C3B57D38131EBF398F57F3C + E7719C21B6A15CB6BDA29E9DA7D53D3461C2AFB247FF4EB489753E7809E8E813 + 938FCC245FDB872AFAEEEA36EBB2AF5311B0B670B5DBF69B50577F76AADBFF5C + FBA8A3F66BDF25EA08F67A7D56BA2CF95AF19232F7B15675BF2DAAD9463FED58 + 021A3642DAB611001F1C226DE7DFC0DEC2660A4BF134D706703B349D1CE0F530 + 22F6151869EA030E1BF0BB5736D5D54E47BBEFAD3EFDACE57D0E763BD8DA7EFB + 54DBEFB11FAEDD6C556EF2CFCA97EC8EE25C7ACF0761DBE4599D9D9FF14750E8 + DB7700F8A7217ECC5DF9FDA41B40BC1174ED735309F2FC4152DD3AAF28DEC4FC + 2615CA6A9BAE7DA5A3BA6EA4DF660D7AB851B61775D5452A174CECA7AE87FB77 + FA30C541A234A84C03C90886E360B6479449F57E510B816DC06DEA02C06810F4 + 8DDA3968CF8A037CA9CB2EAAABCE87C8FF696A8AA0B706BE4CC0948E798354DB + 30618C6969505993DF35612B13B9664217E1AFA92E97862907B80258A7990833 + 7166729D355C20EE81299B3C867701A3086F472C3D060026CF14E16E0D7F1165 + B48D64DB5387F667CFA489E9B369622A03E0830E80D9DA31914DF2BD3DFAB878 + C1CA4C0BE7D3B06DC7E64DD31CC3FA7EC4EA20AB23B87B68BE05BF48C0579036 + 4260F280C3516EB7D24260A2813DEA97B6AD0E22CA58525E941C2863DE7E7608 + F91271A54D543B79507412F505C0212F3B9C7AE1AF03E0602B583C6C5BEC89F6 + 1D39762D0D1DBD6E6AA37271D02180B0A0F0D8C9EB69EAD48D348913CFA16CAF + 73AF5674F6C9B127F02BC82BB12FC79FCA00BF448AB4D122FD948FAFBE440724 + C261294769941C9F8D03548E52ECAD3C8A3C04D8D53BFC54D6BCD32FE4C9BE69 + AB80D87E00D8DF0F787110C06D01B0F2D9CA2E8A3CBDFF977D5F869265A02FDD + 4E3397717EB7B05792333F4261146D3C8F28AA12F51523C06AE5324BDFC8D1B7 + FE1EDE9B0F7D5F11C55E763D47FE36A016B8EB4036C3DBD96B8FACBF0FD3F415 + 6C287BD253AE3A1E255CA27889DCD512D04A134DD60060208DF545FDCAEF097E + E250D7977F06FCAEBDCC02E85A5E5E723647856117DF01AC88DB1AFCC628DF0C + B75E3530B317FC029D72B420F057003883AD9CBF6802001FD97E958EDD79938E + DC26AA35BF8FD497B125BA1140BBF5DADAE2386F124B1D238F7605AC1578EB00 + 17F00B6003B00252EF7E9696EF1161CA7B4E3F69C032221A364260C1DF9CFFD6 + 6C01C02F1D02536FE1B6D5056C06407BE4DEA7E9E8FD77195406A02B788B14DD + EB20378836A823002C7BD2312FDA23D21C976D53FEC0AED3BD7C3E44D536F9B4 + 69E7E190761B906BE5764D96EE7FE111C06C17EEE5E851402C1091A59F057963 + 24AFA27C912FEF6C22ED30D8B640605F4ADBD4D4A35F5677DED2879F7D956EFE + F21FA447BFFF97D3F51FFE6E3AFCE87B0E2F01CFBC87564B582F6E01724B446E + 81CA82CB0EA0E917FBFE5D7B66F55EDBB972CDF82EBE68F61D0AD357BE0B769D + 755D79CFB0AE27FBF5B5261DBFCB0E5D0B7CA56DA46863E503A197EFD8EF8BF7 + FF5A59EE4BFE3EE4EF04903BAB89D4B6EFAEBEBFC8CF93F32E7D023E67E06BBF + 157E33D6CE1CBFA9727D17ADAF1E9D6DD7104DDB77F5885DD307BFF94FA73BBF + F647E9F8D3EFE725CE01BFFEBBB37EF22701FB7EF37D06BAF35E6800F0ECFA23 + 4B3F4D87ED7A2E0163D71FDA6FFF81A79736B8073DF2DF6A3E2F00F0F33463F7 + A539C06C00BD82B84070C15CE5B1D53E5B815B49762A03E4C6A59D95C736DAC4 + ADCA686BC1FAAD3FE7705FEE5D1D21DF2B29D7BD5CE99827B5F7FEEEE7028A65 + 836C67AEDF4EE397ED7976E596DDEB33C805EA22D2BC37587902BE6C514FE42F + 2B535056A02D51C0FC01CA9F6BB6F51529B41AC5F5072EDE053C71F55E1AB767 + E584954FB28206CF53227A698B152E6C3B7BD974712BCDD81860BA7A4E3BF845 + BCFF97E77E1827306670597ADA347B6E23B7E13656A7270A38D7632CC1F84463 + 0C49ED69FCA17107B64D942FF52CEFFFCFDE9F35DB9174E9991880730EA633CF + 38389872442672C03CCF3390092472CEFC862A56178B64B1383629D2BA9BA6EE + A6599B5A2619AD255DB6492D93AE6532932E752FD3CF0AAD67B9BF112B7C7BEC + 330089CC8FF55DBCE6EECB977BC48E3D84BB3F7B7930EE8975FC89AFF4892AC7 + 502A97D2F84B60378E09A35D65C6931AE791027F91C69FA4072C3D64BE006049 + B0D7016ECECB1E61ACEA0569CB3AAF370198D58FFCA8933FE36E87D0D45B59E3 + 72EC82C7B1EFDEF89CB17828C731BCFA511EC5BAA868531BC1DF28C159CD3748 + 81B34A5B501B24BBFBE4F90D65E609EC1494E64909F8C67E635BA4F2C4B2CDE1 + 9612008ECF00DE4A11FCEE040047BBFC90EAE523C53EEA3A6ADA7001826B0018 + C9674802BD11FCCA16E1B1AF39609F03F4B2760000EED60710C74EC74F3E8A00 + 9614059C80AEBD06A0AFD62C0600306057E99080C102C2A40E71F727901B41B1 + FCC689367BF29A9034B4BEB3BD351FEA575A55C1EF805290C3E8FA97D6C5E21A + DBEB6ADCFA1D8AF5CAEF4665BF52CD578AD0B556FF4B281EB394EAE33A75546D + BD5AA2BE5B9BDEDABF26B5535FCA2BB80638B677FFCEFB7D9BAAF18D21898FB4 + C037E78974EEEA49A33ACED2C9FA9B84ADC08532C42B7991345027003C02507F + 6BCAE7BF6FCA5E6F017F95FFC55E8BC3D9317D72FCA8A1FA60E375F86B912D9C + 77EFFCD5AED22F6DF78DC0E49D09A82CFD1900BF2D55BFCC3B54096B0B60DBB3 + 459F21995FFC91EA7E883A8DAB8BF565D96D951FCDDF86BA1B6152CD2729DEA8 + A4EDF8A0EDDC24A3FFB0D20D7DB7DA6A70324E3B6DAB6B5AABFBF5C4A03343D5 + 291B944E8ECACFDB7C7BB2D72E45102BA9BF9EC27106EB4CDE3E0CCAB7527D60 + DED7B876E3ECCAFFD2AA81DF12E252AEF9443F4DDADC9F7C9EE4793EF8F42774 + 56CE5B3145F16FDEF48FDE6EC22645A8EB13BA19B6763ED653AC67FB2B26B669 + 020C48259FC06C82B6A90E805A4E9029F727DA969F3D61E79E01AE09803B4EF8 + 8C1311C03C97786AEE1DD3A9D6CE76D4A8F5B3BE2616DE692696DE4DF075912D + C7DEB1F37ED7CEE71DF339D56DD59C812E7E005EF235C9AFA60883BD1F7C33E4 + 1D02BD4302B4AAAF76A125435F5FCC61B128A85DE4C96501602D20453BA9FC59 + 809ADA50F402110B69016A6ABD7B0E595F2C927DECFEF812B9B07FE3C3E6C026 + 511069014D8B65128B6D87D8FA1940EB70362D0AC6C53E49B6A8582FB138C882 + 608A14EE160A515A6024DFF9C7BAB430D95F8024AF054BC15EF92A8DD1328805 + 502D8AB2401A17537DA1D5D2D896855AA47C5CA4659B47E4603700DF12FEC6C5 + 6214C16F947C23D88D91BF11F64A9459E8E699872C5AB3585DD3DC19A2A352B4 + 97802F115D12766CDA1E1A9FB47D33D1C049B340DC4FAE85C5FB6B0E03000711 + B4020B28A7E8DCDC376D3F25420D5001F04891B502AD339FDC6DA6CFDC69664C + 3CFB33493E3CA39148B64ED817CEA6885C20ADEB33C0871DFB9C9D4FD03C5B8C + BA002211E83E6AB4DDACA27AA5786EA4442FB6DBD75E044425F5C15882976C31 + BC743E415D221F89885484644AF14F021EB5FD64C04CFBB52B4F9A0DA2121DBC + 3E72900860640BE06580DC653BEF2C8147409843B32B766ED71E372BD7896C34 + 7FA0DF55ECD63F90EF32505770F7B947FB2E5CB4F60038605CAE5BBDF6A5A55F + 3A4425E23741653B96A58B179F7A24F0FC65EBD7FB7EE2C702BCAEDFFCB259BD + 9EE0E82290557D92DAF5012C026C1179B6585EB1D7E1795E2330F7FA17DE976F + 999C5F1F221F0554C54FBE9491D711096AAFC3CF3BB6B5E32CDA79B38533101B + 50BC72FD853F331618D8BE762F03CA9FA4A8677F7F1338058C3A74CEAF210253 + 6C88D7CA7B4B5D6CE310F68A9D23C7BCF1A259BC6ED718686EF9F59B2FFDF89C + 97BF2FD6478A32E65C9396ECFDE5FA8E0068EF37E57DCBEA8BF639669B64F7B7 + 73BBC1750162F3594A309BCF0FF09876BDBE4C5C4F848D73405CBB95ABBCAF9C + 53FA4CFAF1CC87323EF4B9728DEB4AFF6C3B9DAE01D782EF0A6ABF2B265D1FBE + 03FA1EF97782F3CA3E12E07EEE025B71730E7C6E397F3BBEBDD7C05FA03D79C0 + FD61FB1E13F9FBE5BFFA8FCDE3BFFD0FCD3B8F7FB23ED36B71086CDF45DE07FF + 8CD8E7DBCF83CFAB7D2F3D9297C8E54BF6DD4717ED37E382FD6E652D791DAF03 + 5FE0EF5DFBFD4900185F802B1079EEACFDFE9CBF9D403811D1795B6CC9B7C7CE + C7202F3B6D164C405B01DD1AEC2DEDAA5319801C6133DB58F33CE00892A9537F + D879AEB014B79756DDB4DD030E7F7CA539CC23002C2F9B546BCB36D494A5B61F + BB2FA5FBCC95746FC9F7A1F61E65F718003052E4AFD481DF6407080BC63A1CFE + C87CCCCEFD8F7BA3435DBB57CE985DF739CA82C988ED9E67F2FD1988ECF74F7C + 280380F3BD997B2FF7793DEA81B2EEF7BA5F93F73A1B2F08049397627B5249DB + 47ABBF584F5E651F7F3036B3718B835DC62E26C63B8C5722248E10388DB53AC5 + F191C6518CAD6AFE1A93692C478A9DB6A4EDD80D1B8F07D1E343F2A344340E8D + D2E3476A75FEF8923C8E15B8950474E3B8B5AC4382BFAA8BBE3E36662CBCC138 + 3B8DA1E3B8DC8F1BD2380E17ACDDB7D28DEF75AE92FCD5568AEDC7693B7EFA73 + 292ACBD29E459EF36BF3AC3C4FE1CFAD87363E6E3EB4DFEE3376BF5E3875CEE7 + 509A2B91EACFB3E411F52510161446FE1CE13C8FDBC73C3347FD96A05811C082 + C2520986917CF7CC6D6E29B5F17C98DF6EA53467AE29CCED2DDF03BA41F291B6 + AAD7FA40F9A76E49EB06291A38C162B4777A75645D62CFE1154BD37A455CB7F0 + F509D2C3611BE84A5DDC8AB9B74E42997ADA84FA712AD7777623F5D1D78A6939 + 89ADA0B7A311104CCAFA97D5A1BCC62680DADA8354A7C00B3469E722C9B6D7FA + DA8EBA35DFE1754A1DB356F7A7A2ED9F3FD761782D3AAE55F77DE65CC3F5A36D + A3BC9EA8DF1CEDDBB3932FEC6F5DF9DCC80FB18CC83A6A8A4CA4E4236539C9AE + 5BABCEA755E036631519CF38FFD2AF500C26745818C1E43895EC6B48D95760B4 + B599466C96024927F6CF27BBD9289377789A7DE4A77C4FB9EF4E4449132D1DEA + F75ACAEBAF5DAF287C6A7EEDF5B2BA3DF447FF965AFFF17C517C2D2E9D87CEA5 + 2C9B7AF0B628EF467F06C06F4BBD0F4AE5CBBD5D151F3AFD78B475457DAB8ABD + FBE1097D8CF11957DFB353AEFC60FEFAE286B27D71831CBA91967EA56D9C3DD6 + BD8D81C6765E433CA7A89D9EDF56FDFD5AAA01D91E00C616006D4F455D0F00D7 + 148E11DBAA5DDBBE3AD0AEAB36502F556B37A4D8AE3609F925A4C99120ED90F0 + A9A9F5B189DBD0C4CE6DC1571339F211FC4A02C0FB0E8D426041DF7112F875F1 + 8F6626C079F21B27B211EE461BA97C01C3D196ECC77B90B794A0ED76D4B539E5 + 52FB09CB4F38100604B355F5BBAE32DFD6CFF7051C8E80578B15B269E122DA10 + 8B1BA5047D1DE4F2AF7E4B230456447009875596DABEF2024DB90044B9B62854 + 2E1CC5B26CE5B674AEDC3EDADA362AFBA2575A049B3CF27EB3FFE807CDC14DA2 + 1E4E37873681BD29BAC2A320CCD7B7CDB3FCFECD8F9A14A5DB2D006A914FB6A8 + 582F69D1907C04BFE3DAAA8DFCB44019FD4865938FFC25815B2D6C0A085347CA + 968B02BCB10ED19FDAAB4E8BB25A9045B2C5C5DA6813AC8D8AFE487E5A3C4682 + BBE4F18900586DF0F74827EA58F4CE8BE3DDA2B7F5F3D1455F249F0F0BEC3CE7 + 9772DA3613A98EC5F4B4F80EF415048E5B3CC7BC00B0C0AFEA47B61135CD7C7A + B39971489B41EE67698B65602F5B71A2B9CF1E343C0F14CD7F4E3E0159FC01BC + 887E12004E3EA95D82C4805E81DFC5F3B79AA50BB79BC58BB74CB71358394774 + 5F8AD6F5ED5FD9D2D852CA1C07001B013079415FC0156D4B80E5B0EA4207AE80 + BA4462120D09FC224212504484648A864C7E82610E7F733EF56D7D5D7AD2B0AD + B347FCFAB6B640E004DD12E47DE2020C63672BE0F40C54EC4059EBE79AF573CD + CE11D877D55EDF653B27CEED1210EF8B66EDFA0B079D005287B9441567B006F8 + 5CBBFED2EA5F785E10D7412EC0CCCA7E2E57AC3F3B374FAF2468088C5D06C222 + 6F93A28817E9DB5E2BD7CC41A9892D8681E25C17AE550B1E01BA26A26597CCC6 + EB2255DEC1A5A51E559B8F492AF8ABBA74AECF5B7F0900BC64EF8BB707001305 + 1C5E1FD706F9B364ED7AF835B33A00BC80A5C4EB11D426FA1991F7CFC78547BE + 45B5CBAE1911C8CB7C462EDBEBBCF995BDC697F67A8836E635BFF4885540B060 + A63FB796F7D6AE8FE02F6015980BD4A5CCEB1088F5D76CE2F50A003B84B536F8 + 0B0053569EEBA1B65C43CAE933D65D33CAA4EBB75E341BB7ED7371C35E9FF90B + BED3B66D07B8CDEF0165AE11D747DF1349DF1F5DC3F27B445B9E77EDD70BD9F7 + 90686EC0F9BC5DAF99F38F4D8F9A59E0BC5DAF18FDCBB50300B315F4C59FFE69 + 73F69B7F64AFED8BF699D7FEFC64F2F6FEA408717B0D7CEEF9EED935E3BB0BCC + 8D90965479A29B972FA7EFF43CCF077625682C5F20EEDCB95B2D008E7681DE5A + FFFE3B653EF3B4CD6056505751C102BA02BEF8A82C7FF2F255FB28C1D9988F12 + A88DE0B685B719E00AF60AE84AF88DEBB36C339BEF1BDC6BC6DD77D2BD2B2ADD + AF00C6BE75F3074060402FF742B6934EF743CA80E1048153842FE2DE48C4F034 + F7DA7CBFF53F54D187EEADA1AC7B30F9781F26AFFBB97CC93316908FCAD12695 + 75883E750CF9CB4F7F3E3B600226FB9FD7827CAC621200D69FDE5A506C696D6C + 25E12B7F893250589AB4B11C29757E3C8E43BB3C363B7CDCFA06FE1E3DD3D3C1 + 8D8FFBC296E1700D041F205D3F6D6366C6AE8C6501BA9D6FF2EF20B0D403C056 + 56F42E753E0EF631387F9A4C9A5A67AC6E738EB56ECCAE7177298DE1511CE347 + 7FE5A96BC7E3B94EED4AC53E558EF535D580AFA47A9FB72CDB39DA35646EC2BC + E5F8E7779A1FFEF1BF6F7EFEA7FF75F3AEFD16FACE4B36C7F1B90B739B3C6792 + 0480B7D2BEFCA75E3D2BB804C03509F40AE24AB247953E35699E3B4EDD3CB75E + 8F14291C61B0CFFF6D9E5F6EF5DCAB0F92BD36878FD29FB9C9B77F1CCF4A6B0A + 80E0BC0E41BF0E84D376D2699D2295FBF67E2A51061E4B1E61EBF077F44FF06D + F950B75E33B46E3364DF4AB57522979E0FEC6B5896D67CA2E43F64F7F5B0A5A6 + 1668D196839D35C808805104C0BB5D43A55D94FA56B9D6E64F45DB3F7FD66ABB + 3572CA48814BB5BAA439B3CD2639E320DFF90E49ED952FEB7F3312000E9CA3F4 + 11FBD88D62BF4336954724CE33F87C607CCABA8ADA7EEA6A41261A00923DC5FA + EDA86C57F425B04BD42BB0945436C0E93EB3B56D83DCA7627715C718ADA7FF70 + 1D77215DB7BDC0E4701CCE8B73F6EF8ED9D9161A5BF57C2BE7A7E8DFEDA8067B + 4BED7340BDB70F5877A3DD02E00873C769A8CD9F24001EF7A5DDA1F891601B81 + B8AD80EC51B18DCBBFD0852D28B61BDB4FA86F6DF95F33BF2D2520B95371832C + 6FA435BF11713D50ADCEA4BEDED64063E81843E715B5D3F3DBAABF5F4D82BC03 + 7076276A016EA5CE15FB0DF9087FC76DDB5C533958AFA9D6AEA6B25D9C4C4495 + 7EAFA53C998993A7529A6869D2832DA6DD24AC3FB92B251F14FB04F8F2BA48A5 + 21003C393D3A39C4A609620B7EA7996C32314D1359C16095C74D6E4B9F9D0260 + 81DD328277483C8B78CF2C6D81C19453DE81300098BCF7F54E4AADCDA4A5930E + 8279B6EE7B090813092CF06BE5B428D101DE9AA2CF20FC35BBA0AF3F3337E723 + 042E23840580058163DEDBE6C59C9AB41084B438542E12A9EC3096452DCBB380 + 25081C25E8DBE6F1D74257F0F105B14DEBEF98E501C09B941300D6F67B02C2FB + 4D6CD1A7ED9A23B825455A3CD402A2160025F9C77A897294DAC63AE5631BF525 + 7B3C168B913A1FCAA482BAA4005F602E7652CA82C02508563FE491DBC342AFA4 + 85DA688FB65AAABC44590BC75204BEF8C846996360A3AC45EAB9D329E2290160 + 16B959F0EE0360606C7FF1FCB2D9BABA0482155515A37FBB85F8DE827CC5DECA + EA80C36C2F3A6F29E0761A086C12C89DF9E47633FB295B2D037101AF09EA2245 + 01CF7D7EC794A171568ACE4DE05851BA808C12FE2E5F02BE00813B009CE02F02 + FC260934B315B400706BBB9060156D6688F2B33402AC08B63C5218507BF941B3 + 70E9410BD980598263887C0BA12EDA390401FB886C9CBF9061ECD5270E5801BD + 09AE3D73B0B57CED8B161226F885C82341D9040D97C85FA66F8EDD07C0404F80 + 2EC76AA35DDDE7CB0C41FB91C2F8A608D9A70E9805807D2BE20C121D485A9B04 + 54BF7490CC76CB4404530768F4085A40F095B445B183723B77C0A240A640228A + 36AE83FBE6E339343695FEB2F5AE7D16D78928E114E99C203079C15FD9897E06 + 60AFDFF8AA59BB613ED69F2266D527C70284A6E8E7742E7E8EF6DE44388D529B + A7CDDACDAF9A23B7BF76F04B0432E91A11C044046790095407EEF3DE13C5CDE7 + A90680F59AB9FE82EFC996EA386EBC0EE9BA71FDBAB69C23E74A5E90345E37FA + 387AE7ABE6D8BDAF9B23B7EC7CB37F7C6DF1FAFB67CADA29429A6709B7CF6806 + ECE6EF8FBE43B19C64EDF003D252660B68B32DD9FB3273E1A903E074DD1230E7 + FB807CDB6EBBBE7A06F0BA5D67DECFB9F3E9FBC67B9FCED35E179F47A0B28EC5 + F70FF86BBF1344E0CE5FB0DFAEF31DA4258A37C1DF1CB17B1E586CBF11D62EFD + B9E381F59940AE60AE202F294078F6ACFD46055BF489ED1C1467C02B489B7EE3 + F296CE5982ABB2CB9FB2DA0BDEAA0F495056523BD54D67400BAC2DDB5316C88D + 7DC8862FAAF9443FE491C0F93E834A08DC964DBA7FA53CF72AF2561FA27AB9E7 + 0900A7885F45F9628B4A7F9662070DEEAFBA97EADEABBCEED9F1BE4C3EDEBBC9 + EBBEAC7BB6C6039E371D78EFAC4B3E28FAA84D0D04ABCF1E00B67C0B7D199F64 + 09FE321E52EA63A3EC7BC86C1A4B491A5FA11E2C365106F84ED8F89154F59EC6 + 76792C860E1691BF8A06268D020A1FDEEC407109881D003B08EEC3E108802522 + 8619E7C631B0A471F0E49134164ED1BEEF5AFA9E8D9D1300963446D7185CD2D8 + BCA638AE8FBE65DB380F28157D047855A77294C3DD659B0F857CBB43522EB700 + 7825CD65F8232B2900F88B3FFCB3E6F9EFFFCEF30060E6499A2391962038CDA5 + 52B4AF47FC3AF47DA7FB732CDB472F31674B0202036D23EC150C8E5057755598 + 9B7D6A75A5DAFEF29F9EA3E2FCB73FCFEDDBFBC2A783BBA836EF4743F5B2FB7A + 4031AF97E2DCDE65EDE23A42594EB60473797630F6B45E91D62FB692A06E5469 + 2F7D594B29D7698654AEC10CADFF48718D28FAB679AD7B85FA6D49303803E0D4 + 0F6B6201FC4A531DF89558838C60561A82B56539DA77A25A1FBF15C575D69A6A + 6DEAC2BF5B2B17A09586EBE65C2D046ED5F9978AED6BF5D256F56F533DFE31A6 + EE4D287295F1C27727FE4102BA5B30A0A4E21835AEF53A12E4242D4029E57DA4 + D9E691B2B9CE41AA95270ED8672997BD4DF67D23DA26081E7AFF1C00FB16D2E9 + B5E99CDBF3E4F5EE396C3ED3FE3AF4FAC6691C009E989875A95C03BE35FD1900 + BF2DB51F0E7D79B7F9051EF365E54327002C085C53AFDD165FFE5A7B699C7F6B + 235FFC50FE36946E8C3B55EDA659F31B915F878ADDA47E7EED81C6D079BD8EDE + 747F35EDEABA95D1BE4300B8662BD40E842B7523FDE6BCDA4802C04303EF52F2 + 1DA75ABB9ACA76B5C9032AFD5E4B6132131527559A58D5FC509C84B99FA99CE8 + A98E546DD4A75ED74405000BFA4AB50962294D5EA3220066F25A9BF44AB11D65 + 7CCBEDA153FB0EFA6E070097F57D3B797C4F797E0F76DACF017C33FCCDF5F439 + 699A5A7C37C1DFA5779B49162614116CD262846F43161629242D5E08FECA16A1 + AFF2AED5B44022F00B0426F2403058909734E6A34DF2F67931A7B6E813EB242D + 1469B148F9088081BF310A38C25D95B5F8A50808D5B138C622D9C16366DF341F + 8F024E0038AA6D97E1EFA4F9EFAF6CCDAC85BF438885424BB1C545C0D627D4D7 + 54B6D182652BF95A5E8B9D5A9854440A75D8B4F8191731C9930AEEEA9C047C91 + 470A9302892DAFC5526C94B1C7455A8972B9981B176C951F921695B5F8AC4567 + EC487E71E159C2779E45F08F2E37F31F599AA16EDCF6927C0980B5F565AA8BDB + 410380AF3ABC2DB7816E816F962FC493A75D8E048ED1C0F45702E0991CC5DBCA + 9FB17BCF81AF227AD90E3A4504A7AD9D5BE81B9E9B2B00DC426220B0C38B047F + 0580D182431C3B2F8FAA03DAA4A85B3D7394286045FE0A004BBEED730650B471 + F8EB51BA392A31C25F952F0172AD3D30C8A16B82BC8269C031413595A388BC04 + 4CE919BD6C01BD48B42151AB5701872F3C521458C8B6C582C15DD4284AFDACB0 + 15F4B527E6637D5BEA11A5402FF3079C21B6401604765D4A3018F8090445D4F3 + CC5C5E07E7B86AC704FE0122576E00E5EC9CFDDC3B282B000CFC259A38C15480 + F653EFB38B96257A9636B9EDF5046D13A44BD7C4DB64E048DFD14E993A81486C + 5C57F2F2A5DCF689727B2271D3792600CC7976003845002F0363B95E5C6BB670 + BE0EEC4DE7E2E793B76376795DB2EB18F1DC10409F3E53C4EF2B07C1BC8FE9BD + B4636600CC7BB462D767C1DE473E3B6CD50CF005DA027F11D78CF3F1D76CC772 + 785E5C8BF6755BBDAE03F59C23A9ECA4B4296D1276A27F3D0238F74FAABE5526 + 4DF9741D14FD8B047715F51BA37FB585B6C362EC7C76F8BC84364491CF9AE6CC + 7EEAF14FCDA37FFA1F9A07FFE4BFF6AD9EF5D9E7F32D20EC7F8C00F05ADF9C27 + AF896BB9AA73E57DE0BD379F55F355B43E10986DA00566057E57AF3E74515694 + BF22FB57ECBB2E308C62542FA22FE02FA2AC2DA1552FC0DCDA2B91BEE937AE03 + C035BBFC5527080B74955FAD4E70577D08E046682B3FD9693B6DF70CF2B4950D + C94F65A43EA21F0200CFD8BDA4DC49224AF71DBFAFE49D2524D5A5FB12A03802 + DE088001C3DD36D2BA57C60860DFF239945179CF952DDEC349CBF140AC8FFE92 + C613511A43007791DACB5F76C98F69D2B82502608D87546EF379FCD3FEB12DDB + FCCF6E566EC75D598AF815FC553F5ECFD8CA528DEF348E03002B0A5880561038 + 8261D925CA11F08E03BE3509020F02608D918F30C64ED1BF690B68E06F7D6C3E + A2F827D1B5648B637B8DE1CBBC14E7026A53D697B0B72601DE5846EA5775444F + C7B9097316E0FADAE9ABCDCA07973DAFF95129CD9D90C35E9B97ED993FE11234 + DEB364A9CDC322009E32DBD4728AFA6DC1ACE5A796B1DB1C2FDB057FC98F83BA + 350DF94E64E82BC5B9AF14E7BF35A576F8A4F9BE54CEFBA5A17AD97D6D607A74 + 67AF12FEC67587B8A6A07448F28FED652BEB23E48DF511FC4A1100A3B84E33A4 + DEFACB168A6B4371ADA85586B7D5BA71AAB5F3B2697F80BE48E5425AEF1BA791 + F5BE01D5DA0EA9D6FEB7A2B8CE5A53AD4D5DF877EBE411D20ED7519E1D50E75F + 6AA4DFDF64B0D8A89C6F0CD87F49B54C6584D75017EAB79298CF88A8575F43CA + 7DD498D66BAA05B7199012E50B0C9DCC60B7F441530717DDCFB756DE0307ECC0 + 712FBF1D09F212BD5EAB1F239D57A99ECFDE0C592DCF6B429C3760FBD0CCAA6B + 92D74C5470D1B6A6ED460047C0BB95FE0C80DF96F4458A5FAA372081DF1202F7 + FCF4858FE5581F6C6AAB3ED5DF489F59659DE72B3F96BF1DA51BE47655BF696E + 7D03AEA9D6CFAFA95FE2FCB6D3DF768E570EC6B652AD8FA816CC9612A82D206D + B40FF95735E05BF6DDE6E3C0780B9583F652B53643AAB51FD26E261435691253 + 9BE030F989B0B6AC53BDF22AFB04CF7C906CDAF239B62FF3EE1B21F016F057F6 + F6D9BFC54495496C19F91B27B671125CDA6B3E2AA7A8E00EFAD6B49D08609E0B + 852F51BE7B01B726227C5DDEFE1D8FEA05EA02823DC2973CF60C8001C1FBC913 + 010C04CEF037C25DE5E3A245B9804119D8CB737A7BF0372C8C482C960802FB16 + 7456AF4594E8136DCAA785214B83E2028FE0EE906A3ED8C601E0D68F34006094 + 16C154369F0D3B1F939E01EC5B3F675FCFB3D0679ADAFCA8DDB6B9545CF46331 + 917CE9131707E5BB2DE19F173051B4B3F0582E72CA5F0B93AA535EED648FFEA4 + CA6F0580B5081B1769B5705B2EE852964DF921F902F18709C8FA82F4E9B44DE5 + C1F7ED5826F20E6C597CC62F2F4E3BF805003BF84D0BDE282E789790578BE4A9 + 9E36579B854F5954BF69F537C2427A02BF51BE184F6AC2C7FB0CF03742E0286C + 3182B795C35BA26D1F363C8B17A00B002EA37BF1F5E7F72AB5FA2E5A3801619E + 2B1CC18883608FFC05005BBB1CFD2B584BD49F43E0F38F53E46E86C03D65D82B + 08A53C29FD68DBE6AE4FE06D02C01102CF5DB8E7CF1D8D1195026B11D04982C0 + 6C5F9CB65006F0599B1CFDBB7A2381DF040C5F989D686000305BE1A6C851DFE2 + 38478C028297AFDBB1CD360F54BB461F29F214397CC496A127B01608BA4E54AA + A50E81EDD83A6FCAC0BDF59BCF9BB55BCF1D027BD4A9F938B8B4F69D00C809FE + 6A8B695E17DB5BAF5D2712B883995C07E061BC26B2713C224F49654BA0B183B1 + 0290D4C7BCDA23B5F3FEB95EF97C5204708CFE4DF03BE5EDFC2FA7F7863E388F + 28F5CB318902A6ECFD9BBFEC3A8794B773BEF9B2D9B8F3B50360DE3FC4B553F4 + 2FEF0BB01FA02BB8EE1018D0C87B6AD71C00CF7BC07BE391ADF958F1B54B3A3E + E7579EBBFC4929479B5E07A9ECFA1C60D3E7195FFAD6EB270516BB8F7D3FF41D + 6A3F43F61915004694A39672BDE769633E00FB79A0FD8D17CDB91FFFB6F9E37F + FC3F353FFE87FFD47CF0C51FDB88DF1439CDF7C0CE9DEB99DB731E6C994E1430 + 11DCFEFACC96E0AFBD2EF34FF01958CD773A415EE02CF077EDDA23D7EA55B62C + 57BD946C82B725CC75A06BFD68FB67D96427953F228A98E70CAF5CB232CF1B66 + 6BE9FC3C5F4910574037FDE6A5DFBF16E2866DA4A31FC2AE3AE5C7D5D560EE74 + 8E0E56FFF2C11E616F54EC033FEF23DF4FB84FC44707F83D28DF8F28631F52BA + 77716F4A50374501A30481756FD53D52F2FBA003DF0C76B9D79AB8CF727F45DC + 770F9BDF61BB4FF38C60EEBFE40F7E90EFCF01D0C67B3AF6F63E6C6D6A1018D1 + 4E630A8D1FA24A3F81DF28EA356E89631AA91CF31C320900232FE7B10EF5F84B + 405FFDA10E1F8DBFBC3E8FCD6A633C3F0ECF02CE7016C00B14AE81E108806B8A + 3E520D0E0F4501479BFE547968F343D3E9BC4D74184333AE2EC6EDA96CF93027 + 487F101D1DEB23B52BB51D5FD905768724002C5FF22AAB1FEC53EB0956EFE7DC + 4D02B9CC8914BDCB9C48D0578AF32B94DAA5E85F45002BE58FB8FE475DF361EE + 3665E5047A81B33627658EB9381A011C017054097523E81D92FB8679AC54CE8D + A5D2AF145B409720B79DF7677BAC6BEB2B75BEC650CCED510B7E0F9B1F0AEB05 + BE5690DBC66DA1A35D7E3BB1B5EB10B94EF53A765C2F69D74C2CDD77C8DA0D80 + DE682FEBB6526D9DA8D56EE0EF90BC2FD6C732E4F5F5395359CE1A5AEF8BF9DF + 8ADED6F9C475D621D5DA8D0ADF6E7D3C42DA5837647F2D005CD1B8BA5F4DDA0E + BAB08B7DFC32B2EB6469642D5589ED4866531F23BE41DDD6CEF5FAA4D0478D69 + BDA6DA88D80C6E01A1070E2F3BE4F58858B3932A3A165F01E083E637B778B499 + 99B7DF4F80307DEC1400BF86047C87D4FAE673D26B254A7775ED6473E6F3ABCD + E94F2EF96B00ECCAAFD7B65004C065C4EF906AD037EACF00F86DA9F7C57A0D0D + 7CD9C7C2DAF28BAE726937A9ED967D6695F6D6B7F8B1FC6D899BCC9B53FDC63A + 5EB57E5E47B5636CA537D147A9ADFA8AC71BE7A7C1DD7655EBA31303CB0EC6BA + A68A67004F269F086A1DD066FF367238FB9480B7B5938F00B8F0934FDB7F3B20 + CEF96D6AC703F8311AEA23DADF84CA098D544E8434019234614A5B2D51EE227B + 914FF282AF26699463FF658ADE240046E596D065BDF2B20379CB54DABF74AA39 + B06C13789BD8036A4BF0EB40D7ECDB91FBCF3231B7FC12F0D604E85D30F962C1 + 293B7EF2D3D6CF1319F44628CC76D002C0292A98ADC74E99589C787F64F14212 + F81D920030795FE430D50030F91AEC951CFA66693186548B31E3A485A7B80055 + DA597C12FC8D605750D817B842BBB4C0F57173E8D8194F3D8A181DB5FE8E9A8F + 2B2D7E09FE4600ECD1C0BEF57302C080E0120697B6B8F8575309839567F111C5 + 85472D56C6854645F2B68B98966A315236B545BE58F9DEF9DE82A9B64F247FE8 + DD624B4513D134070B9BFBBE97B661D642AC1663B540EB10372B96C7E5BD8FF7 + ADAFFC7C4274E843B35B19A94E20372D56273F522D68A7056E7C929F94B6730E + B0D601302920970570228313FC5DF8E456D6CD66FE4C06C12CC25BCA023D5B74 + 6A0B4E2DC6FB627D017C05813DF2D7CA1E0D8C4F8EE0EDC06F82BB8AFE050093 + 26B89BB68176B07BB68BF29DFDF47633F3E92DF759389BFCA94F91BA96129127 + 08729128BB24F209D410AD9BC0AE9EFF0B00D656B382C09E06B02BB82B09006B + DB6645F802B688D405FC46018381C05EB6FA79EBC3B721CE302CB54BE54E0900 + 0322FD79BC0E7C132014DC5A01E23AC87DE95BE30A22E2B7CC73681DE0D13FF0 + 97E312010C4CB67EAF27F82B01843B286CC7BCF9B259CBCFA45D73017313E84D + D1C3CF9BD59BCF1C0093022A1394EE8B484E206B1420D8233CAD8DE02F00B126 + 414901453F8F6C8F75028FD8D2397E61AFE1CB411F4404688A10B57286BD4976 + 0D02085FE375F0DA4DEAAF94FAD43112104FD72B5D1B6BA73ADAD8B5050013CD + EDD7C9DE37016052AE717A263075BCDFBC97E9FD24A27B39477713014CDF7E7E + 1C6B409C9B2278752D101058E71C7D63AACF27793F7F3B96EAE375519F6A479E + B67C37E4975E476AAFCFBECBFA4F51CEA9ECC0DBA4EDB6D19C7DEF664C6BB75E + 36EF3EFD9D835F22818FDC7E65D780ADA0F96E70FDEC1CF8CCDA671800CCF1B5 + EDB7CED33F4B965FE2BBCF36DFF6DD48DF6DFBCE07B0BB742945FF26F89B227D + 57F8AE5FE4CF214929127814FAD6E4BF4526451763A37FA47AD5713C1D57FDAA + 8D47089B9F7C49A3B0F91F5FB2BC1D6DF2EFA380AFCA02BF405A40AEE0AFECB2 + 09DCC654FDE017C16E097F257C906FFD8C1FF708EE1D26EE2982C0FAB351EF0F + 4785FC3E9515C1AEEB8CE91364F72DBB17F6EF8B69B78CE9D3E95ECA3DB9DC65 + C3FF64C51FAC725BF9A14396E7DEAC3F67F9BD3BDFB765579D34F341DA4E5A63 + 06E56BD25881BCC62E71BC41AAB14B1CDF94E3A0086B7BE325AB433E4632E1EB + F6ECAB31579BCFEDF507391DC3C76579ECA7719DF2F44F1AC7873EE6C3C7CAE3 + C6946DDD1800ECCF0FCECF109622EC15F095E218373DEA84BC00B0F9E09FC538 + 3A8EDDC97B79D5EA6D3E70E0888DBF3537083E3D5FCBABAFD86F69970DA92D62 + FE5093EA34C7503BD521F5878DE85CC15C52C47C0718CC7CA59C23A94C1A95DA + F2C7DB510180F963AEB67F9E62FED803C036EF5C383E0280510D02CB7F2B405C + 4AF3DB529A07C7393062BE9BD2BEBD84BF318D005880576AEBB38FECBE66E073 + 7A7B4DAE34AF1F0780B5CE90D6152887F5835C97F29DBFEA55A6ADD625629DEA + 958F6B13D1E6EB18612D653780772BC5B59FB8FE53DA62B914F53AAFD6CEFA16 + DA9F012FF03702E0ACBD0758375B4C65D2BC7E376EBD2FE6C769BB7E7F2A2AD7 + 516BAAB593BA7A7CBBB5F10869B70763670B757D8CFAF6157DDAFC6F3C2A38BE + 36B18EAD14F9C838F5DBD971CAB623BC26D76397B27DABE38E00E098AFD96A4C + AB2681D8EDA8F00792B611BEB95E001811414BF4ECFE434BCD479F5C6A9E3CFF + B6B978E56E7378D67E6BCA68E0F25CA2FD0D28C2DE5223FE8058CE61EF747360 + 7AB9397DE662F3F8D937CDEDFB361F3EFA5EF6816576ED6BFD0800D740EE6EF5 + 4600B0B453081C61EE380DB5F9D305C0DBFF5168A52FAC94EDF1474375ED975F + 7E6AA3724DB9AED75F28F77C83AAF5948B1FCEDF86F8E1AEC8CFB762DFA67453 + D5E044CFA690BDA65A3F6F42E571C69DC7566D77A3B23F5D93713EA5A2BFDA6F + ADF258F4DD9D431A800609D246509BF382B382B5B5FAD62E09E296F6316A01F0 + 2EA501FA5683F2116940AE731EA3788CD75206B1517192C32468CA267F4C92A2 + AD4BF1B7C945AE433EB143DEDF6A6B57BB788C78DC684740E0B805B4268551D8 + 7B13C430911DA738992DC5A4376EF95CDA01C0071D00DB84DFB76E4ED1BA1100 + A3087A150D5C2AFAF07C5F6D01EDCFFDF5ED9ED33FC6BBED9F53E4AF47FAE6BA + 2801E0B84011172F4A45C08BE28286EAFDDFEF96D6227B1DE88632AA01605403 + C05A88D10250B9E013EBB40815ED02B771418B452CC02E9056765FDCCABE8A0A + 8E7EEA63BF6F05FD6173F01802F66AF12C41DFB43066E5E32CD8B120F7513369 + FD4C994D8B7B5171F18EB22FF065953E2A977EBE10991725DBFAB020C936D36E + 27A54D5E7CD402A72F609A9F47A984BEBD6D5E28455A4CD58229E558AF455495 + E3E2AA7CD547F48DF55AB8D502AF168C25D9D1218FF0B5B61F583B7F666FB798 + 1C53169FB5A89D1693B5B533B6B28EA827EAD2A279B738CE423AC01780DBD91D + 089F01FA26CD7D7CB399FDE8BA83E0C54F6FF9227BDC8693857AE0EED2D9DB2E + CA8A0E76D86B65EC2BE7EEF842BE6FE969B6048073446F06BA09EAA6885F80AF + 227F53F46F8EEC55342E6D4D739FDF75400B30EED4016045C70994B45046C0E6 + FC438F042622778EED9F8191805D9E316A75AE0C784B090A23F7B90CA84ADBCA + 0ADC3A6CBD6CF579BB5E9581BFE4FDD9A3B4CBFE5DBB0CBEBC9C00698AFE05BC + 6630E99036015AF280AE05A0D75580611FE026A0080C7B66B2FEEC5C78F62CFE + B45B363FD20E1AD3269511E5153F563FAA5510D0B71E0644DE7CDA2CB1C5B4C3 + 3D4067826F1291C5E9B5581DC7B214E8AA7E1278EC22654B512F481901B0FCD5 + 87848DF3A4CDC69DAF5A2059FA625FE7FC78AD26A29D63C473B2A7EBCE36D9AB + 44ED8663209D87FA8B365D2B40B4EC4A972D75007C9B0860DEC70EFCEA7DF376 + 0E7FD37BE8F0F826C7A74FBBE64475DBE74910D8CF291F4BE7A8634A7A1FB127 + 99CD5EF3113B07DE677F0F4D6A2BE973495E7D94AF9954F6D89EFED4A7EADA7A + 4BA953FFFA0E20B543F8FAD6DD0070FF5CDAE7D4CE7DEEC2E366D6BEB76CFD9C + A27EBB6BE89F5F537A8E72DA869D7EFCB374CB3E4B443FD39F7DF753E42F0206 + 13ADCCF79BA85E80AF7D8F2F3FB094DF117E43521DA24E11C3A96CFE19E4F67E + 7B72993ACAFC36C5DF2701E0E84F3DF9DEB6D36613DC8D79FC23F8557BD9FCD8 + 97ACDF00801165D9057A23EC95649780B8D319F0524FDB088A23E415F4AD49F5 + FC41487F24521430A2ECF78F7C5F4991BE09FC2ADFDDDBB200BB00DF4F2EDB7D + E78AC9EE83280060DD2F93F21FAB827C870D60303B64D83D967B26ED04752581 + 5EB5934D8A7E5E267DDF94C709A534B688E308F2718C213FA553273FED89318B + F28C7F348E628CC4632E14F1EB63A45C47AA28DFA843B95E3E1A6F51D6F88B7E + 346673FF6CF3F157511FC578B085BC797C186D23B23A7FAEF0A68DEF6C2C1AB7 + 925699348E9535DE3DB891FFD8989540B08D958FDA38D9B77F4EE3EF52718C4E + A46F84BF6C9BAC3F864E1DF9C0C7DE28B689E3F90865871421AECAD1465A53F4 + 896D24FED44AA42F51BF405CE63B2DFC652EC3BC27CC8BA26A8FD701F646B510 + 38CCFB261701BD7D700BC81D8A00C646DA83B9A15EFEA8F4916DDFA2F9DBFC95 + 396CDAC1CACA59E55C779CBA79347FA84E005712F88D7077A8BEEF333AAF27AD + 01E0DE1A81B5F54745594A39AE23445FEA2541DF087EA3AFFAD88EDA3FCB1FFA + 65E02F1AB7F6136DD1676762BD29AEAF2D5B79D9D3A8160A4F2D8EA85C1FDC8E + A23F6BB3515A3394A22DF6F14B69B7C78A6BA835C96FBB7D97ED22E8DC5AB359 + A9DCADBD6EB7FD2E147883F3879ACF2FA0F6B591CFEC639CE279F654E131B5F6 + 684B76F3BA1AECBB38FF1AD7AA2982D752355FB673CE752DF8C466F2E8D86C77 + 306C79B64EBE7AE361F3177FF54F1DA4CE2F6D367BF61CF43EDAF63A16A9F26F + 501C679CE4C3396BFB67E5E7978E36C74F9E6E3636DF6BA6A7579A89A97EBB78 + 8C587690CC357116DA07B9BBD59F01F0DB52FC22ED06006FA1F843D1FE7050B7 + DD1F8EECA7B6358DB409FE3D3BE5E287F3B7A1EE06F926D5DD3493B67B53AFF5 + F5BAAA1D67E85CB6D376A7DAAAAF78BCA89A6F295DD7BAFAF5DD2024B58D0077 + 50D9A71BA0768A7E65792B3B8A7D49F541F2CEC4807CA79381A17E6A3EA57DD7 + 2A00B02641284E9A34512AC53F6F6B0018A572BF4F697C9F7D3B1018693288E2 + 64513645F9C6E7FD9693DA9AAD549C14AB1CEB05812799D467783B047E558ED0 + B7A6E44F9A003012D4957DBB00D8EB4CFA377B5C9C10E8DDAE0488057F51097D + B1B110E211CCC126DF525A786121468B3B830B4BA14E0B46B1EC361696ACEC0B + 5A9617AC65214B76F202BD1100473FECA9BDD98F9D6E0E1D2342588B68110013 + FDDB01606D055D5BC443CA936AE12F2EFE957ED157E961DAE48545161FDB05C6 + 42D855A7C5492D50AAADF78B0472DF359FF7CF356CF54854EDD43B9FB98DBCC4 + C26C5B7E4FE9D9B68E05DC087AA5088149B195D057528491A28CD202F1794F7D + E1392F504BD858A456B492EC09F0A6C8A4A8AE0DE5046CE3E2787A0EB0C0AF74 + BD99FBF8464F80E0854F6F370B9FDDF285772DC42BCF227D04C082BFC987C5FC + 3BCDD2E7771C147B4430D1C001FEEAF9BEED737E43D46F7CFE2F120416E415A4 + AD01E0A4BB0DD171808F7940C7C5FCBCCD8B0F1C100BE4A6A8DD14D1DB03C017 + 013AA3A21DE05810D89FCD9B4195A055825866BF6CE7070C72B0495DB225284C + F9690F780974B5E52BCF3CFA9748D4F51B5F352B57BEB0F3B5FEAF3C776098B6 + 0E7EE5309767A2CEB37DAD00B0B501E42610060403BA01DB0068CF9B45B32D5E + 7FD12C39A07DD982E0E49B0030C7C1D765E723C0A8F373B00794BB667DDE301B + 02083BD4EB0360CE6329472C3B10E618971354A41FFAD5EB7738E83E097AA6FE + 32380438DE1AAD4BA0B493EAF105186393AF842D01D504D4D700BF969774DEBA + 267E7DAE5A1FD64F3C4EECCB5F47388EA27FE54B1EB9DDCA6BB7BF6AD6EFDAFB + 78AB03C01C2B456F77AF4D6D3906AFE9C82D526C7C6EF43CE0BE7FBA8EF19A70 + 8E804F003A50DE8EC5B9D9F1366E7DD51CE55C00A201EEEABDD1FB2300ECEF85 + D9623D7995D55EED90EA048F631BF9F8E73EE7D546EDDCDFE4AFE1CEAB66C55E + C7827D879681F47C9EF26795EB98DE2FB644B77AFAC8D7537DA6BED27B11A38B + D371F99344FAEE02D779267084C188C860FFB309D0377FA7F1F73F7850BE4CD4 + 3FD1FEE693FBE219DEFA0308E5B43DFC7D3B9EF50BDC352D5DB1DF15206FD642 + 06B882C4E47D4BE71CCD2B61278D91BE82C892C3654B057D256D2F8D5D00B784 + B902C0B22341DE9AAFFCE513ED512310D8DA0804B7F70D53BCE7283238C25F01 + 6169EE13BBD765009C20B0DDF73EC38F7B60FC9354BAAFEABE596AD644F42FED + E423A0AB3E6299FAD2A6D4C70E8C233204D678409097B426D5C57147ACA3AC3F + A9914669ECA3F111E096C85DE47F98CBE325A500564976FC22F0559D8FB1AC1E + 1D22355B3B8EE378D916EBA234061C52CDEF00E34F22818F9E690E499B090247 + A56DA0D39858622C2DF81BC7D6E939C0E693C7E0ED783BFB483E4E67AB671BDB + 0BFEA203EB1FD9189F63740058FEA4A50D09D0AA1C25702B1FF9C93E4EF8E87C + 296B3B68AFB7F98ACF4B6CBED2539CDF14F32869140077E0771C009E584C4057 + 60B75404BA486098BA08784BFF58576A827E02008E1038CE6FB792E6D0493657 + E7CFE14102BD1DDCEDD4DB1E9A35876C77E89B77F19A9A498A7FFA667E0F0066 + 3DA016B13BB486210DAD35A0D85612D875B86BE5ADFC584B11002ED77D6AB69D + 28AE07956B3FD1167D76A5BC1ED646021710B8067EA36A6B82D1364E00DEC983 + 2B830078277DFD2968BBAF67741D963CB6DA3A7AA9D9ACF9B6DDB8B6DBEF778C + 027FA8D6FF426A5F1FF9CC3EC6A9C745B650F41F69BF5D8E13957950AB9ACF58 + 15E75FE35A3B9440E708DC94046C5106C0F22505A212257CF4F887CD279F5F6D + DE79DFC64D87ED3703605CF6A354F937A8F83A4AB94F3E2E65A29727D0E45C17 + C14B7D669A139329D2B96D2BE9DCD597D9F6ED3B9C35ED7D75E5BE22331DA75F + 0D00FB4906983B4EB15DCFFEF70500E72F6FEF47217CA15B7BFEA2AB2CDF588E + F69A6ABE52CF57C7CA6D7A75948B1FCEDF86D24DEE4DAA76132D157D6AAAB5D9 + AD6AFD0F693B6D773A201AD7178AC78BAAF9968A83340DE0BA415CBF9E72BA59 + E7F6DAEAB900B33E203D9007A401124750EB7565BB8A86FCCABEA49181F1380D + 44EC3220DFE9A0BFEC43FD9475B1CD90B63DE9C8131C544E8CA45A7D9C50C5AD + 975A31A9CBA21CFB90BDAD9B2BDB530E5059D0B70282DD6E13C43829DD090066 + 621CEB6BD20439B675DB7C82BB25FCED43DDAEACEDBFB4B5F3A85F82B882BD82 + B9E453A471678BD0B76CE7FE26FDA33D2E50948B1CA55DC0370ABB2F7450BFFA + 41FA57BFA95D1CC97511F86AF144E59E2D2FDC684126E6A34D2AEBA4B88004BC + 9580B8110AA32100ACB2B689F64534227FC700608F9260C1AD8D024E51187111 + 2F2ED0451B0B7EA564978FDAA89E85422D526A41B294161CC97B744AF0975DA9 + 2F3C9A7C21F4BDB3BEF0CA42EED2E7377C4176FFBB9F3B04665196455A2DD0E2 + 2BF9626D5ECC6D95175ED9CA31825C2DCE0A046B91560BB6F8486DBBBC68AC85 + E321511FE1AECA2845F34AE6F3F19566FAA3CBA62B0E65517C7EAF20ADEAE678 + 6EE22737ADFEBA6B96AD9FADBCF019F0F776334F0470906CF25974C87BA7593A + 7BD753B58BF6A5B3C085BB0E71E73F4FDB39CF7C72BB993E73CB453E3E07189F + 088A4901C3B4EFE02FE514059CD20E000340801E8091D9B3371D8A380801005B + BDC3DF73768C8B809914FD07008E91BD312A50F0577508F8ABA85F81CC0E6611 + 119C804F8ACE046E25F0439D00B0A0530F72793F564744E325EB8B67930274AF + 7EE15ABFF5AAD9B8F34D03B805D21EB9FB6DF3D157FF4573FAABBF6A8E5A5E51 + C04449462896002E40EC0B87BF8BD6CFCAEDAF9B955B5F356B9647AB37D96E38 + 45153B8C047EBABA485D5E2FE25C1DA0011E6F9AEF2D3BBF1B763C8E7303D068 + FD66980A9063EB698E2DBB4701E7EB2620880465053D25C14DEA639DECA580AC + A9AFE44F9954129CE5DC385E3C5F89D7B11AF2E9F9C68AC44DFD229539EE8AA9 + ACC71ECFD753B301328F00806F7F65D7CEAEF975DAA77EE4DBF69BFBF1BEED5A + B3E536D1D7AB379E35476E735DFBAF5FE700D44D6037BD06B64AE6F3C3EB4D75 + 2FB3ED2BFBFCA4F4C86DB3595DF7794CE2B3CA7BA5A85FCAFE9E71CCEC2B9B97 + 4D719B735E93EC7E3D780F4CFA1E2845F173A17CBA16F65AEC7A71DD884EF7EF + 4586BE02C1FE79A72E474E6B2B69FAA13F1D87F3D2D6D4D892F85EF3DB90E02D + 1038D9ECFB9C6D405ECA7AC637F57CAF97AE76BEF1F9DF883C5BC0CF5B4A7EF6 + BCFDA6994FFA3DE8FE34D26B67E7C16F98A280F95D130016D815004682BFD805 + 802923CAA4DEB680BFE423DC55BDC06EAC9364571F9415F14BBE04CAA4405FA5 + 02C0F84FE74862EADCC6FD09C01B80AF007004C2DD1F8E12106EC1F0A7D61FF7 + C233975D8A00C6366DF74EEEE3DC4775FF8C7FAC4294058717AC2F52FC350688 + 3ED8192720DD9755AF3C3EFE67B231003896A3347E50CAD842A24C9BE8A3BAE8 + 5F83C01A3345C53FDD455FF2D8231C1600C607E01BC77887684F9FD9568E0F35 + 668CF69AADD47EC6A0EC5E13002C0058CF139618C312DD1BC7C63E96CE63E138 + E6F6B175184793C72FFE6172729D3A7C688B3FF509464FD9B968ACDD4617D327 + 7D99E2F89D634690AB394214B621A95D94DA007AC9EBF8D4E9F9C0296F732A9B + 9F307FD19CC89F016C79A298BD2ECF934A6D07004BBE05B4E67C0B36575C4C60 + 97ADA0D376D01DC82DC1AEFCB03167DD33677351A06EE127D85BCAFBCCB01745 + 001CE7B6718E3BA4BEAFCDE1770280CD5EDB025AF03702601401B0CFF5F12D00 + B0D6166A6B0CED1AC280AF54AB2F0170AC8F79F7C9EB2D51E55ACB907D2BC5B5 + 9F72FD27DAA2CFAED4AE652DF7B453001CD701BBF5BEF16B8971DD30B58B6B86 + 49DB5D8FDC8D748C5ADDAFA5FA3A6CBA96F575F4616DD56EB7FD8E287307670F + B5FA5F483AFF5F0A00D734E21BD84F5599CDF454F80CF6DD53515FE35A6F40DA + E2D90128B033DB1C78024EF7CFB7F5E4F73AF8B43C7035FB6F054F7B756F401C + 6F48EE531ED7F2005B9EDBEB91B77B0EFBB6D5CE34E513DB55D407C0E31599E9 + 38FD3902F86D297E917619013CF4A52DED2AD714DB0D6927EDAAF5948B1FCEDF + 86BA1BDD7655DE20B714AFDD5F7F67EB6EAA7545DF9AB6EB5FFA49717053D66D + A7FD4EB4555FB15EF99A7FCD165F07EA0FE2CAD749FB05CF4FEC4FF0373EC7B7 + 0667DD56D4EF446D1F39BF95AA83E31D4A03F23858DFA9DED444629CEFBE30B9 + 292739A54A3F94265AA9AE9C70A95CEB471334A9DF4EB2BA99047891B6828EFF + 0CAE4D5665D3E45636D93579D6A45993E2D836E655DFFA78FB4DEBC726DC19FA + 46088C04774BE01B2170B4A7BA3EDC8D65815DD5C57AE4CFFF9D4FF608802304 + 5644F0D0BFD96B005860D81747B0AD7EE0F0570B268845901EE0B57C94EC6DFD + DA280096648B76F25A2CD28251CFCF2488ABE7006BC14A1200D6E295EC6A474A + D97D36CD7FD38E497A14689C16C7F6E7452C01E0832758D463018EC5B9FE625E + 5CB013C8551D8A0B817161709C5854D402A316237D4132D4CB270260CA072CA5 + 3CFBDE79177907B2EF9FF345581673173FBBEE8BB200DEFDEF7EEEA916675104 + C05AB0F545DBBCF8CB3301E7890A0A9A03B886C5DB723197BC40719BCF8BD05A + 48E69CA4B8D82CC98EC86B8119017E3DAAF7E36B5677A5397CFAB24BD0578A20 + 58F057C016A02B45E0EBB64F59AC6771DECAD91621F0CAF9FBCDF2B97B7D086C + 295B3E037E1781BF0E8081B60F1DE40A022345F93AC4CD801725609C40F0CC27 + 5D34307EA5F05FCE805710841460A208398FDC739F87CDDC851401EC700A5520 + AF47055BEACF075694B049B0A8055B015A21B67806E220E00E10589027019FD4 + 36B6179C4A7A6636D3159E9DFBDCCE9FFEBF688E3FF8BE79EFD91F9A538F7F6E + D8E679EEE293E6D4D3DF372FFECDFFD8BCF8B7FFDBE6DDA77FB0B6C0BEAF9A25 + 406FDE12D7C1A51F03F896A27FE7F1B9F9B259F628D46F1C001341EC30D852FC + 1224048C020E13144CE0CECE958858C01BCF01BEF565AB959B80E02FAD0FE022 + 40F1EB06A8BC70398161CA1EBD6CE7C06B062612A59BB6E5E538E999C3116602 + 0C0540635E12EC54598031814FCED3CE99EB1BFCD58F606104BF48AF9DD7C0F5 + C44700381E5FFDA47327323B8154840D1F1D1F3F9DE7FA6DBB16F7BE71013357 + 8954263AD753DA7500371EC7F376CDD90A1A084CF9C81DBB76B7ED75DD321F14 + CE27BDD6F41AF59A00BE299F6C94FDDC01ABF46576DA4500ABCF27EF17A2CC67 + 0A1B7E82C2D197D70C7C7500CB77843626F2A5E23542D8E8A7770E967A0430C0 + DAAE1BEF0DEF9FFF5921C35FA27EC9F37A79DDF4C3F77CFE827DF7ECBBC06797 + EF1DE7EECF05CE003889EF2220367D77174D7C670567174C02B682BA483ED117 + 918FBEC05DD4F7CB7DE7BE90B7E538F8D9EF989E032C88BB68366D195D4260FD + D64508AC3C7600303ED83C2AB888FE45659430A21DBFA5AE0C70F163FB68FAA0 + BD2279D51E1B8AFD0BFCA208823DE5CF59DC9F733FD81CF4FABD2A815EFD7989 + BCCA8AFC6D01B0F591A0AFB6824E9A4161BB67EEA3FAF394EEA548F764A43AB5 + 89F75F099B8F0D2C5FB62FEFE7EC40A231006309D2729CA0B182806EACC726C9 + 26C93F0260D91CD49AE29849A9F28CA148057DE52F00AC311965DAA82E8EFBCA + 31E038A08B86EA4BFB7EFE88C8B8D5C7A949DAF63941DFB40D344A70368D81CB + 3132E53816C64F7695A56803024F10019C61B0C6CCE4E5A33F4E329E17B4D5B8 + BE1DC7336ECFF51AF7479BC06E991F92DAE2178F45393E820618BC67C9E64596 + 4EAEA6B6FB966D4E643E936C6D9DFBE8C15CEADB72CC8F4A11C01326E67C53CC + B5164FFADC5391BD52097D658BF912ECEE441336678D73DA38D78D7364D5A9EC + 7579DEBC87B4F5451DF88DEADB927FFA73362018853F699BDA797E80C194057F + 6B5B402B9A38DAC6A95C8740E5FA038A7ED1B7B42307C5ACA5E47599A8B8D6E2 + 8FB43295F6AD545B5B2AEBA26D48B18FAADA4086047E7B3AC0D6CF9646EDCFCA + 65D6021D0687F5BEB4E6C73AE1424EEB8AFEFD769D587B8CF69ACF9B50D9EFD0 + F9FCD22AD761BB73C016D7CF5F5F5A97ADD5ED44CE1C2AF6B7217F0DA4760E5B + A9C7454AF1BC6394CBB5F6A8D7060D00DDB65CC2DF8A3FAAF6DD53515FE35AAF + 23C0A6A523E0D4524161D52B9F7CAC1E1B6D32186DEBA23238559FBF8474EEBD + D780C271FDB5581D8A001890EDFC32F8B67DE89C9552370144AE03DFA8C84BB7 + D2AF02807DBF6E443E00DD21F5DACA3E41C8371ABD50ED058DB65F5BF18BB44B + 00FC46B48D1F86A8F8435453CDBFF6A3F9B654BBC1C8166F7AE354DE106BF6ED + 28B67D5DC5C141ED38AADFAE62DFE3546B5BAA6C13CF6BBB75B57EB652FF3CBA + 3EBA32FFF85B6A260EDA8093C1E3641E5C06683BA83CD81C91EA19C446FF4A74 + 6E4FD1773BFE8534986F6D45FB7210BE95E2E4619C6A6D8734E4EF7D3129B109 + 902641ED842608BB7CE264A9B3593FA1AC099B266D7B0ED9242DCBB77132BB26 + 7B718297EC01F8DAA4304D046D325CD104A095285C4BD15EB349942788AA05CE + 5A79CF8C4D5C996C8E993C97F6B2ACC9B9EC7B994C07F8BB87E36429F237825E + 45FACA267BF48B50174DCC9B2D83DD52D18FE7FEC6325B40B33011816F04C0B2 + 9512F01DD116603742E1E84B5EA0B8F5CF0B372CACC4C51F2DDC441B62E1A605 + AFC58291E7CDA6C5AB12006BC1AACC4BF8455F8FF23DF65173E838792DA8657F + EAB3C601605289B22F24168B7F82BEB293F20C3A89E7D069BB40F9B5FDE63EB5 + C8889F7C631BB76BA1D2CAB3EF5F68164F5F76CDBC97B74ECC1037AA04BE82BD + 5AA8950F9018A9DE177959DCFDA813CF0954BF2CF6CEB308CCC22F8BB961D118 + 3FA40561D26EB198C567169B8948BA62F5442AE19B447E86E3591D29FDB1D89C + 16B0D336986CF14CE46F5402BD29522A0160E02F5B36B378DE45FA2A62977C0D + FC3AFCFD9CAD3F89D6351FF39DC5CFCAC05DCAD8E7CCAF05BFE70103779B59F3 + F1F6FE4CDFFB2DB015E045E425D50BEE52AF886160B0EC3D5D00DE5A7B7F0670 + 8A04767871C9FC0124662702CFA3F82E3D6E161CBC2400EB6906C0EDF6CEE159 + BF40DF55B69BA59D95056D23BC8D12DC218DF01709006B0BE81A04067AAD1109 + 4AE4EF952F9BA5CB40B017CDB1FBDF35271FFDD46CDEFBAE61FBE795EBAF9ACD + FB3F36F7FEF17F689EFDABFFA179FFF95F366B665BB90E004E80363DCB97F6F4 + F9C2A32617AE7FD9CC5B1D5ABEF555B376F79B66F9E657CD22E0ECF6D7CD3A11 + A240C23B492B3701822FECB8DF3447CDF708F0F00611A509A412F9BBCCB3696F + 03E75EFA318866E519B747AC3F00703A8F14818AD87A983E01A69B1996266809 + 7005C0268029882901F41C7A06BBDA92A70E91F7E7FF9A9D73C4465E75B44FE5 + 040B23F8552A50CAF9031523A0D471754CF21E419BA368154D4B9DDAC5E3AEDF + FEAA05C012D71A088CA8D76B141CA52FBD9658B70A10BE4534B85D8F3B00E2FC + 1AED1CE86B9ECFAEBDCE0D40BFF54B94B1BFA7F935A673E2BDE73D4CE7A963E9 + D80E69F98C727C53ACC397366AA7B6D1C7DB999D7C0985A3AFFAA18EE861FF3E + 98FC3B62F50EAEF9FCD8EB4AEF09C7E273D8C97D4C00FBF63859EDF7FD8ABE93 + E90F1A8AD45FBBC1F790EF27C0387D5FFBDFDD0474C7095FF58914454C1D0098 + E39C7C68EFF9ED2F47DA29CF715689F0BE9A9E29ECB0F6727E1EF0B5477D209C + 812F12E4F548DF0C7955172170AC13389E3FD7415ECAF8479F160067905B0260 + 07B65627F02BF88BDAE8DE1C258C6FAC271FC1AFD7E7685FC15FA43F32618BF5 + 7E9F1318FECCEA3E4766FBD4EE97311218186C792281FBF7D00E04FBBD3ACBEF + F705D48DE5E81B455D0FFCE631806CB33636D09FC700B58C5380B894652B216F + B445451B7F78D373800582A76D1CC45808314E625CC51FE41883312EC3461DE3 + 288DA500BDF829C51681B08FF7727D1C074AED38CE14C781F223AFB27696C107 + C5F6B13FFCF66F76BBD1C47AF731DBA48D652719F31E3DDD4C6CD878D9346979 + 69DF910F9ABDEB365E675C9DC7C3EDD877A4CC58DAC6C01CF388F5B7C61F2919 + 77E397A29101C2E40FDBB94CDB39306E66BCCE18BF3607A849F302415B815BCD + 1524CD15D49FDA95F5E4D50F797CD537A9FA2E8FE36D02D405004BD15E13F077 + 4FDE029A39DE7E9B0B4D2E01843BB01B01AF54DACA7AA4F969698F73D6D25693 + E6B47B00BA56C65FE5A86813289EE01C02444E5038D5CBB62D01844DC060DAAA + BD8362E02F6B095A97609D02B166319B20F3766070096FDB358B31BE1C2FB651 + B93D17CA43EB2B5982BFE300705C338A6B48A586DA6CA5D8C7782D27E05BAA5C + 2B93C6ACC7C5B53FD2216D552FA535C44E359FED4A6B9EA5A20FC7A84525471F + 546BBB1B6DB79FE4D75F63FF25545BB7FFFB2407CA435C6880D5942CC6D9CB18 + FF52BD3635ED2DCA35AE354E91C3ED90C745983A02574D111047957E35D5FADB + 9176F85A5CF97882D5A49E2FFA19776E8A008E3CF475F5D601700B7F11E5007A + 87D46B2F7B0B804D7A435C9483ADB888BF9AE217E94D03E0DA977DE807403F0E + 5BFD40641F6DF13CA4B29DDB8A1FB7B7A9F1379274D32BA51B5D79E38B65D9B6 + 2BFC876EE2BB51ECCB8FC175F66BDD1D6F9CD476A7E7B3DBF3AF9DD756E7BA55 + 7D29CEAD1B34F5EDA99CFEC9B78F416565D0E8F9097B7DB5ADA1837F1B398CA8 + 03BE4AF257B93AD03595ED82EF7606CD433E71E0BD1BC5C9C3D084A154D9264E + 386A6AEBC74C7C34D9515D9C2CA53C750524CE5B35234DDCA222008EF037DAA5 + C9599BF85614016F04C00E7A5D80606C01CCE6096C5439018E36D96359365788 + 008E205812DC8DF0179576D2A9C501005C01BF52E92B09FE0ADE3A7C25AD00DE + 1AFCAD4500C76D9E25D90480B52D74AD8C2FA943E1BCD0A2C51A498B2D711107 + 0D0160C9ED26402EF03702601421AF847F4D00E0C327CE3433A73E69A601BC1E + 39C14297E918CF55D3338023000E8B70964A6C09A8C8105F0CB4BC6C5A38D442 + 21D242A22F26B22898E57EF8D02EB78DD2C2A3DABBBFD9C9EB997668E6DDF3CD + FC87979A0580290B9E797134C25E01DF9A047211E51210FB026E5EC425AF055D + B5A1EC8BCC7901588BBE5A1496A28DA85DE02D0BD87A562FB6E9D3F876653DEF + 50905811CD8B9FA5C8272D8E4709FE26017F592CEF00B024101C237FE703F415 + D45D389F60AAE02E76EA01BC339FDE74457FD50900A76D9E13E015E48DF0B783 + C0D43F6A7DB41D34757A56AF7C7D3B67573A37DF069A73CC11BF6CE9BA60E9BC + A5738AFA05286520E4F94B8F5BE02B181C238101C04B979F583FA93D8A5B4077 + E0F60BEB97BE013809268D42A451000CFCA5AD222C01A4CB57015AC0CD14859A + 001751B880CB57CDFAED6F9BD59B5F3747EFFDD09C7AF2074F57CCBE64F5F801 + 2E5B3900B63E6FBC6816B35689CEBD63FEB75E34279FFFAE39F3FD3F6A8E3DF9 + B959311BA050021E020C895405541E0154B235B4F5B962E79AA27E9F7B7AE4EE + 57EEB76C76C02320DAB7976ECF85F357D46602CB00BF165A3AD8C42F41554145 + A5003D81424145FC80AD2D5CCD6520AC40A9EAF0EFF74DDAC15E94A02851BC29 + 5A195BBAF6A91D7D493A2FF202CE3ABECAD4EB1C64E3B9BFDA02DAAFA9A5BAD6 + 1E41EDF5761DEC9A023079AD3A96CE9F346D73FCD47CBF684E3CF9B639F9F4FB + 66C3DE03BF9ED69F00FE8AF9738C0499AD2DE760F5BC977E4E77AC6C7DACDE7E + 6E9FABFEB54702B4BAE6AA8BEF41D926FA49F401D8F5ADA1F9DC5B7BF5ADF715 + 3F5E735B4F9D95B113210EB8F6CF6406BE51007DB6B5D6B1BC4F8E1DFAD1F753 + A037425741DBFEF7B5FBFECA5F91BA6A57FAA99FD28EFF915B5F34EF3FFBB139 + F1C0BE2756A77E1425EC5B457BA47082BF8058015C07C059B14EF502B5A4D40B + E0A20873058D4B804C1D80189BFA278F5DED9407FCAE5EB663D8EF2A7FB6593C + D7C15C415C4515936FA37A03248E8078EEB37E74F002CF97FF3CD9FDD9C0F91E + 8700BD714B68DDE3FA00D8CA9FD9BD324700038093CDEE9340609EA36F7681DF + 12FE4ADCA34B489CEEDBFD67022B55BBD2A6B28F133EB47102F777FEDC65E313 + 8D532823015F45042B5218A82B51969FF2F4A367000B001FC2378C931877CDBD + 6FED6C0C3561E344C6518C95183709046BDCA5719BC660B26BCCE729E335C676 + F86409F0A2F8E73D8D1B5B68CB1892B166E843F6D24FE5B24EA29F491BDF4E6E + DA38D924E84B5E6580F0BE0C8681C471AC1CD3FD3CDFB780BC2A93CE9E3ADB2C + 7F689FA1139F3914667CDE3E3A85F68CC7F3D87EBB625E30346F287D658BF309 + B517FC25AF7AF51BF348BE88E8E0087DA36AD01729F2571200260278323C03B8 + 5484B6355B94E6A42A0FF9977E28FAA95EF35D6CE4F764D88B34EFF5F2ACCDED + 33B88DB036DA62793B2A1FDD54DA5DAC4B48331DFC8D00581058EB115AAF405A + DB90621D8AFED4C7351049B6A871EB315A6769D75BCC56F38F6B47710D49F658 + 5FD645DB90621F5BAA5C13435A53CB6A77CCA31CD6E5DAF5387CD8E12FAC0DBE + 8ED2FA6192806CCD6FBBD2BA66A9E8138F592AFABD098D3B8F61E197545F5B7F + 3DFD927DFF96D5BEEE10093CA2015E53B218672F85CF6B692B003C8EAFB53CAE + 50CD77072A01A9F225341D82A86F44AFFB5A780DA43BEC4300783B41B6DBD57F + 1E005810D82F146F0EB67CE1DEC087EE8D287E91DE660470FCE1D00F4954F41D + A3DA8F0DAAF9D57EE8DE96C6DF48D20D6F449CB39F376DFB37C878138CF6ED28 + B67D937A13C729FB1852ADED2FA59D1E97C151077FCB369465632015069151DA + 1ABAB4E701A60F2C058055570C5CDB7E299703DC2D541B400FE975DA6E479A48 + B4369B7010B98BE2C4244E64A2B6AAF3094CF0D12448FDC6FA7242557B5E6F04 + C002BB11FE962AA1AFC4E46FDFAC4D466792C80BF60AF82ADAB7B4752018506B + 750E6881C069123B4E9A0CC7097129F7F5EDB46C121D606E94EC682B30EC7944 + 642E02AE9ACAA8DDA8D64FE99000B6392FA02B9576A0EFD4EA07AD048123F895 + 220096541600468A0C26755B58A8614148AAD9951700D6225294002D0B5482BD + 438AF0576DFAFAB83974FCE366FAE41907C1DA46EFD0B10482E333806304705C + 7C6B17E3B2B450A84541A445432D1CD67C91A0AE160915D5CB96CEFB896631E9 + D9BABD94854A2D724AB9ECDB18E6BC165D4BA0ABB2E06EF4D99FA37E691BDBCB + B75CD42595E20270B4B168AC8566F2405CC1DD9AA813F4555E9098286016A313 + 04BE917CF2E2785404C009FE76D01709064720EC00D8349B238081B88AE82D55 + 4260F7152CCE7549963FF7C094A26B17CEDD4F694FD69F09900BD05D04FEE63A + 41DEC50B8F9BE54B4F9BA58B8FAD0FAB43B97EE13CF50F9AE50BC0DBA4C50C69 + 25415B815752002F12EC5DB9F4C49FFDABA85FC06F1B4188AC1DFD681B690160 + C1DB659EF97BCDFAB901B47AE6CF6A5D646B599E116AF62500B0F503E0920055 + 40AB16225F7B6EF624A26701A60888BA7EF355B376EBEB66FD3630F63BEBF745 + 337BF199C3DF552283CD07A8B9711B80C956B95F3B180302AFDE4A11BF47EE7F + D7ACDC7ED5CC5C7EEA91C077FEC97FD5FCF5FFFEFFD25CFECB7F9DB686262216 + 2809342472F44E7A5EF0118F0EFEDA5ED38B042B8194772C7FEB8B66D15E2B60 + 11A0E991ACD6C77BCFFFD87CFEFD3F69DE79F23B3B0F403070BA8BD204F65206 + FCC5AD93294778A8B260A80028220FC4547BF2C05601D7085EF117B44C6DF1A3 + 4D82BD82BFE48976E6FA51D696C2F47FEC3E91D08A9C4DE053C7E61CE3F9E8B8 + F15CFCB56400CCB5DA7C40047002B31E4D6D623B6D603A5B45A72D9F3B88AD63 + 90A6D7F1DCDABF68DEFBF287E6F4D77F684E3CFACE3E3FC0F917D6F777A6EFAD + 1F40B0BD565E8F9D17EF8F00B11FC7DEC7F51C417CE48E407C77FD399E8EA9E3 + 235DCB58AF3688B243DCECC7E79CCFBB7F8F4CF2938FDAB7F03697D57E83F3B5 + F7C6CFDDAF477A9F057D51EF3CE8D353FB7E64288B52B42FA0D77E3F009F00D0 + 2B3C93B783BE2B26FC525B8163DAF40171F249525B6F2F089C7D0580D76F3E6F + 8EDFB7D761DF19415F01E5760B68FABEF2D001AC402E798932105891C035C0AB + 76D8D446BE081FC15E0165D9A38F802FE5D8672CD3AFB7231AD87C79F63A6AA3 + 864D1C4BE7A67EE7A973D89BB69E06042B42D8E170DE4A5A10D9016F80BF1102 + B70A11C0C05E3DFF1751466DD9EE9F1100A7FBF2A8542F9FDA3D3EDEEB6BF77F + 528D15E4E376E02E20378F35BC1E086CF6E84B9DC62648E310526F677DC448E1 + 2807C28CAD00C036BE9AFFE062B371F6B68BB11275EC9E524602632FC5986DD2 + C6867A1EF038F81BEBC80BD62A1FC792F22FED5B49BE02C0A94C943079FEB098 + F22D14DEB471B8E5CB717137BEB67A93B69A46FC499214D8CB33873FB6DFE747 + 3FFD634F934F1ABF3356673C5F4A80364A73819A6813FB521B41DCB28F689322 + E08DD0B754E933B962690502A3ED0060CDFB00C000589EEB2B682B09CA22D934 + EF8C75E354F6556B5FABAFCD7FF7147F9CA6EC72200B7CB5F97AD832FA4DA907 + 7E4DC976A40A7F4B08CC5A4409804BB88B4A9F28F96C475AA3D98EE2DA8E34AE + 4EAAAD2F8DB3BD96047CC37A5ABBCE96D503C048F037AEC9F5D6FE5E5F6F12C2 + C675CDA8E8F3268EB35D0D9D8BD2BAA8ABABBED6BE33BDC9BEFE1435C453AACA + DC466DA26A7E6D7E48833E451F234CEB35D89ADA920EA80A71A91BB06D097D43 + DF23B6B60C4B0C5CB1A6964D92DF4AB9DF6D6ADC6B8800582C35B2D1DDE84FF7 + 19C0ED1B42990BAD3757175EE5ACE262BE75E94BE4DAE6175D8A5FC2AD6C522C + 0FF9947515D57E64A26AFEB51FB8B7A5F13792EE86D713E7ECE74DDB74C3535D + BC09B6FE8586EADEE44D5D7DD6EA50ACDFCE31E51FDB8DB3BFAE76722DCA7388 + 2A7DBB3E535DE7276133BFA99566627F1E440276F3A0D2350087057F232076BF + 387835A91FF78983DBA03870DE8D34682FFBDC6A30BF5BF1AC993DD3968F938F + 6D4E586A939BDEBF58B38F2644F229EDFD3C756F1600FB04B09DF47500B885BB + 399500BF8A065644700B80F1C99016D09A00F0663BB94565BEA638716EED6C3F + 9DE1AD24A03B641FB4B140B004F07DB7D903E0259F45792BB55018281BA43E4A + 7BA918F51BE16F2BCAA612F67ADF0102F7406F1676D9F6AF770B370E6F595C2A + 1687A25DE5D25765CF93E26BF91AF095220066414B525B8967FF1E3C66C7253D + 8A4F8802CEC72202F8D04916F05898EB16ED9047E9520ED2E2A0F27121115B84 + BF714151009885C8E977D296855AB4643B4314C1AF44394261DAF802A9B53BA0 + 0898BC384A1AC12F65445E76FC548F8D7A165891FA38E40BB56CDD9C202D5B33 + 1FFA001F168589ECBDECB6047129938F51BE44EEA66D9B81B911F62ADFF95EF3 + A8A714F51BE16FEA9B4561FAA3AF087C13E8BDDACC7ECA02B7B6CF0402F7216F + BBD5731036DF0AFA73B6E44C11BD02BB8AEAEDA06E92602F72309C23703BBBD9 + 1CD43E6A80C07367EFB900B78AB205DCF601F0DDD6AFD38366DEFA58BCF82441 + E04BA96D6AFFD0A5B2DB80BB193409FA4A005C7F36A883E2047E1DFE5E7ED2AC + 5D7EEAF0D70130B2F651B10F5245EF3A68F288CC2709FEF28C561379C0EF2210 + 1860732D414C41AEBA047EBF68E62F01CD809E0053A2515F3447EF7DD76CDCFD + D6236C17D95E1930CCF6CD26877CC04C6019368F207E61E74774E9CB6605D8F9 + E07BDFFEF9D005BB0E0E80FF9BE66FFED3FFB5B9F937FFBE59B1368A1006146F + 3EFEC9F38061E0F1EAED5709125BFBE5DB2F9BD57BD6FF3D3B2F4030E0F00E11 + A800DD6F9B0B3FFFB3E6BEF57DFAE55FF9F9AEDAF9031F019F00560160209EC0 + AA60A2409E44592030C250F2B4DBBC07B44D605600507502B1087B0F2806E82B + 08DCE54D76FD783D4BD686E85A80AD435BCEF58EBD164B8F3DF8D66D4049B6C2 + 06E81EBD6F7D588A4F8A8C26A29AE85B7B2DE6B769AFF7F8C3EF1CD87A5BBB7E + 44514B6AE770D6DAF15AF4BA530AF4E4357D69C7F9B239F5ECDBE683AF7E6E8E + 3FB2EB4C04F8EDAFEC1CECBCEC3DD46783CF0D9F0FA2683B086C7DDDB5EB78CF + FA22B5F3A07F5D275D635DBB58563D795D737DB6758D25D5EB332E5B6C572AF6 + 9D5E37E74B847A7A9F795FA5780EA95F7B7DFEC707EBCBBE9302B609CE6ABB67 + A26FEF374B0052CBAFF0FDCC3E09EADA6BC8E27BDE4511774079ED06DFFF24DA + C82EA9BFD82F1098F351B42FD0175F9D23D07995F6765E82B702AD11BC0ADC2A + DF82D8ECA332A9FA011ACB370260CA02B46AE79036835BFA543FAA5BBA9C6CD4 + A91DF616FAD23EF721E918EA1B081C23878900F628E1ECB768A9470363031687 + A85FD211089C01B060EF1000C6BE687EECA211016FBAA78E46FB7A0A980D5056 + 7629DA4B692C415EBE2AC7314659AF3C758C4B24FC3566211D02C03EA661AC64 + E3248DB388003EFBE4FBE6CACB3F344B36A698B0F11EE3304509238DCDA2B031 + 7EAB01E0DEF8B0C847E12B7F54FA5016D4954D65F519A5BA1200037B057E2300 + 2665BB681F2367F978B795C6CB8CA1CF38F015FCE5F12BD3763E979FFDDCFCC5 + BFFEEF2CFDC9238419776BCCAE71BBC6EE8CEF23A415B48DD2BC209663DB086C + A3E4AFF238002C5B54B4B56D030016E88D1038D6B5E037FB4D92DA7CCFFF049C + 0130AA4160549B739675CAAB5E76FA54BF657D2DAFF96F6D1E5CD6A13D010003 + 7E2717ECB8F3E69BC12D692D3278BB12F48D4A751DE48DD0B70680FBEB14FD7C + B4457B5CF3D0BA87ECE37C62746F4D5ABF29CB3B516DCD2ADA947F23CAEB6AAE + 0108DC5B5F93AA0078A15DF37B5D69BDB2B66E595B871C27AD49961AF22BEDE5 + B994E7B353C5731852D9261D979432F57DD5D7DBB7AF37D5CF9FAA224FF15D57 + 035319D210836915F94E603E7BB3F6EC99E9D94755F4D1E359C34C0D88B92D18 + AB7448D9772C14AD1C6BD0BF768C9EE08635C65828FA8CD1DEBDE939BFF11CB6 + BC2E63B45300BC1D40FCC600F04EC0AF942ED2A8CD555CECB61E8DBC11E46B6F + 685671217F15E94BF46BABFC92D77C82F42333A4A136B51FB9B7A16DDD48F4F0 + F5D6367AD31B67DBAE62DBD7D5B87EC7D50D29B619A75ADB526F625022A99FDA + B9948AFE9DA8E37310FAF2FC6A02C09376AE533C33C474D006CA260697A4BD01 + A60F2AF320330E34E3E035C3DFB68E7CACCF1A1A54EF54B14FF5B5DB41FEA8E8 + AF38BFC3761C971DDBD27232D24D52CC773AF9459FA858E7139930218A7EEA37 + 4ABE13B3A11D93A043960609FE0E01E0C9E95100DC4DFC2833B14CF2285F54D8 + 220476B06A656DFDACA8DB7D3649F5896BFE47736D821B27BEA534B96E6D0300 + 78AB48DFD2E65B3F3B884D00B784B631EA57753D5B96FC4704A01D500FF266C5 + 3AC160C0AD806E04C1F45D2A0260F2127D1C38F251BB80532E0645C50522A9F4 + 8D3E11FA6E057AB598A5BCC06FD7F683E6C0A6E928F53C139848E0A4B6BFE33C + 972D02E0FEC29B16E70E91E68541415ECA5A48D4C2A1542BCBD717204DBE70F9 + EED9763B4387BA01FE0AFA629FB4F322A58C5DBE0E80591C05DCFA62684AE376 + 89085FB79B9FB6533C94D3691656C3E2AAB66516BCED9ED59BECB22520DC015D + 44BD9ED7EB5017584B1B13FE3CB337FA96005851BEF81D767F3B26A037835FF5 + 9964FE19FE027EE7D99A3903DEB8D5B3B67EEE74C774B799B774F6F3EED9BF0E + 7A01BEE7EE36B396F25CDF088501BE7AF66E8C0C5E38974524EF85C7CDECB987 + 2DD0C506F84DD1BAA449652470A7872E40F0D2C527A60EF646F916D1A4397257 + 91BE51F344F0DA7167CF5BBF9C4306C0AB19FEAE017832F08DED53F41F6027C1 + 205700448BD67609705300E04545000388F02F80588C08260F00E659BA446D2E + FBD6B680BEAFCCFEA5435F22538F3FF8A139F1E8A7E6F8C31F9BA3F7BE4DE097 + E85EA2766FBFB26303EC9280851B6E23BAF74502B9E6B768F9C55B2F9AF7BFFA + CBE6E25FFC8BE68357FFA059B3BE00BC02BE279EFEAE3972FF7B2BBF32FF97CD + DCF52F9A3940F71DEB0FD87B3743E0FB76DCFBDF78D430E70E7004FC9EFDE16F + 9B779EFEDEA1B40360D3BAF950264A958857C0A8E0ED11077D0976225D2BF282 + 7CE475FDC8D316800C04569D00628CBC2545B4A3AF14210D3C042206007CCB04 + FCB5D7481DD770D95EF7913BC06BAB2BE0AEDB7279DDCAC0DF0880D75C09FE3A + 00B6F2B107766D1F7D67EFDFB7EE4B3FD40900CB8F6372AE511B80503F2F4027 + E7F545B3F9D0FA7C443F5C23AE21209B08662299BFB36BF343B379F77BB7F91F + 093C9A3C5D0FC0AF440430C720629CCF6679BD49A9D7755459F5E9F3FBCC4559 + 11CD82DF653FA492BE1BA4BA067AB6F4BABD8F6B1CD3AF51D707E7A1EDBA79DD + 1C37BDF77CF6CD2F47FF02565B30CBF375392F4B97AF3EB0F2C366ED06E035D7 + BB80B940DDF4FD8E1018110D8C1F4017F80BD42D017004BF92FCC947F88B1D9B + 2280D9027AF11291C90FCCCE56D3095613ADECF60C633D05025B3D7907DA5646 + E4E781A801D0027B1539EC10D7EA4A202CA01BA1AD00B0EC44F83AA03509D6E2 + 23B0AB36F2C15646027BFD252287F9A35086BC797BE805FC4C82C3123E6C09CD + BD4DF0B704C073405EF4A9DD5F3FB17B2AC0B707802F373C1F9828613D2B9867 + 03CFDBFD54F097DD355A086C7568E6CC25BB5F23BBFF5A8ABCEC7549B2A3C31F + 5D4C3ED9DE963F4A11BD405D4160F282B982BF2520A61E3FA4F60E78733BD918 + BBF8F8278F717C5C433BFE64C7B8C6C6321F3FF8BA39F7C54FCDA28D27346E8B + A0B7CC6B2C87F095BFB67816A88D634387B1568EE3C238EE435EA62F6C26415D + F58164D3D8534AB037D9A736CF64009CA27E81BD1102AB8C00C01A2FFB18DBA1 + 6F5F40DF982A0A9836A76C9C72EDCBDF37C7CFDD6BF6D8FC03387C00604C5F8C + B9837CECBEDE016024702BE9912E2AD32E8EFD63DBA8B6BDE581B82A475B19E1 + 5B2AF6E7CA005850D745993E2A0078CF82CD07976CAEB76C7E0061E67CCC0D03 + 009634BF8C529DE69CD137E6353F8D3EB15E7548F3DCB25DAC8BE59AF6308FCE + DB40037FA7D8D23A006004FC956D3BAAC1E26E1D601800971200D63A46FA83FA + F8358CB8C6515BF38836E5A34F0DFECA366EDBE79D48EB4BEA3FDADEA822F08D + 6B67D19E6D2300B8D0DEBCFEA747C1BDEE9AE4B8F6E51AE476B5DB763A97A89A + DF7615CF83E0181E83B76782B4AB8BFEC3C7C42FA95B4BDF9DE883F7AE56F7F7 + 41639F013CA092C53877A1AEC67772B9077F4DE4F7D157A86BB527B7A3FD08CF + 7A0DB616D9DC38D5DABEAECAFE437EEFC474B36FEFB4BDF6C3764D0E593973C6 + 716A19644DF814C77F0DF5B780E659C0DD76D0BBD5AF0E80A5B2DCBFD09536BD + FAFC66BE6589E6B7545F1FAA9AF445FCB5C5175AAAD5476DC7A7267E888A1FB8 + B7A5B137A41EF48D76D2EEC6A77C79238CF6ED8836AF7BB346659F6F42B1CF71 + AAB5FD25B593014EDD97F3E673106DE66303C8BD53299A17104C5E00D80790E5 + 00B304BF9206A9B5816951279503ECED4A03F07103FB38402FEBA4A1B65BCBCE + BF05BF40E04E9A98449BEC9AAC681213EB46EBB1730C264F9DBD05BD4379AE49 + 06BF6514F0904AF85B4EFE1CFA06E03BA416FE6601610580BD6C935126AE9AC4 + C6896EA93829D604BA13766BBF4504F090D2B37E9368130170097FA34AC83B0E + 006F0579A532DAB76CA77A01DD12EA22C1600161D2D287761EFD1B2280515C0C + 2A15FD4AFFB80084047DA37C012B2C6645D83B2C3BC6E687CDC163760CDA1E23 + CAE1138F749826A56C3AE8DB3E27A545B5FE029C16E67C918E853BCA59E5229F + 2F1466C8AB8544F9445FEA0E5A2A282B519622E02D0130DB3ECBA75D00B594FC + C10C965B98FB7E02C2A92EF90A00236F1B6C02C08AC4254FF4AF22800580057F + 299712FC15AC05DE3A00A69DA96CCFB1047FE7396EE14F590058109845F0B6EC + 8BE22C90DF7609FC028323F8F5A85F87BFF72C7FCF01308037425E69CEC16CCA + 7BC46F9600B0E06FAF3D75179F9ADFE316E8A608E0BE962F3D74915FBC60FD02 + 942FA4E85EB67F268A18A53CB018BF54EFD017907BD194812EB03646ED0AE0F2 + 0C52203011C044F256A37F015926FC635B6C6DC46F8642117AB105F4CA8D270E + 7FD76E3DF7D4B77ECEDB42AF10DD47DB2C6015EDA504B0805B4037201B026CE1 + 0F78FBDA6DC7EEFFD09CFFF99F35677FFEA739A2F7953FBF97C84F87C7D7BEB4 + 36767C3B06908C6857401A919F4778EEEC836F9BB57B5F5B9BAF9A55E02BE5FB + DF36AB564794F0A2F9911E7DFCA3D5FDD82CDFFDB6D97CF687E6D4CB7FD0AC3F + FCD1FCF0A7FDCB66897E81AFF40160B4732052983C91BF1E996CE279C048DBF8 + 722E3C9F76FD6E8AE6742868E7ABA84E8147813E80A1848DD7465E00386ECFEC + 7D998ED24F6E43DD66AE4FD784F3A20C2C4D10D165E77A9473CFF5EB3739B714 + D9BB713F45F70ADA022A1D52027989D40D40B805C0668F3A42DDC36F9ACD0C7F + F153B46F029C099C028DDD6EE7AECF8B0372BB6E02C07C9ED6EF3CB73EED5ADC + E55CCC8FD7EB9098D765EFDBBD1F9B630F7EB2EBF3631789CD6BB23E00CC44FF + 4A1B56F6EBEB7DA7EDB675EDF55E9097CA3A7D0F1284B53EEC9C05733DCDAF43 + FD90AAAC3A97E06FBE9EF1FAACDEEAFAE073C2FB7EFCC177FEFEAA9FD437AF23 + 815981D74ED6DED347E627009C2070F2EFC3DF12002740DCC15EF5AB63C5BA28 + D5017BBB88DFD446B66E3BE804730580974C82C00E8201B2C1279611F0771690 + 6ABE82B182C002C0A4EBF6BA81C2F844681B41AD228DC92BC2573E405DF52D1B + 7EB11C013036D41DAF83BCBEF57386BE3D202C996DF173FEBCD4C15FB4486AF7 + 3A00B047FE7E7EBD99B1749A08601311BFD8890E06F8229E11ECCF0976009CA2 + 7F01BF520B831D1C275F7FC6BFF945A88B28738C1E0406185BB9F505FE52CEC0 + 1709F446C0AB7294FCA23FBEB4D18E25521CBF208D79D86ADA6DD666EE637BAD + 67AE3687C2B82C8ECFA26A76D9A649AD1CC783805C449948610404AE8DF94835 + 66A44D6F5C9945794882C0FEFC5FC6BF19000BFC9665D978C489C6CF878E9E71 + 1DDCB0F17096FF91D3C7F2A76D5C6C63E313766E9B36063ECA1F2679648A9DF7 + D14F9A49F3210AD8E1B08DAFA3DAB17B00B348F6B65E69AEA7EDC8F83FB42F25 + 983B64DF3600AE44FFB62A9E11EC73B91C011CA380FD313F360704009791BFA3 + F3CCD1AD9C87EC71AEAAE8DF5A047084B8B2457BB445C576518AFE1D0780B71B + 055CFAA91FD4D9EBC037026107C096766B13F5F58B525A0729D742CABAA8D687 + 7C5E17AA89759B98DF8EE41FD7ABCAF524D5457BB4FDD2EAADB355D6E5BA75C0 + 859CBE9EC6AD43BECEFAE8B876B1DFD24FE7F3E69F496C6507C04193FDFE87AF + 05E7471FC57AFA2EF426FBFA53540FE06E5311FCF6DA0FF19D6C17E0FDD500F0 + 76A563FCD2C709F288DD7D87ED5A968CB1509543967AB3E73DFA0C602070C746 + 77A3DF0C001E7B41735DAF2D6A7DDEDE076448B530F49E2D7E117F4DF185966A + F53B54EF870791AFFCC0BD2DD56F24BAD9C57C5DDCD8CA7254ACDB4A65DBD7D1 + 509FF1C6BC9363AABFAD546B5B4A0384E18142DFA7562F6DC7478A7D0EB5D987 + F6E701A30D1C270EAE3593876D201F079182BDA54DF92806A3E5E037FE53B132 + 30D680B9A6713E1A984BA55D79D2AD8EB33BD9B985C8DF087AA3AA1395A0A13A + 6CA9FDAA95EDB5984D13A7E10954EAC7A17886BF1ED99B21F038180CF8DD9727 + 78B5C96209802947B576526C19B40A00B7AA44FFC6C9714D9A606BC29D64B625 + EAD371B602C0D12EF03BB5F4AE2BE6270A881B616ECD1E25001C01AEDA95B6A8 + 12029775AA8F505790B704BDB2456117381600F6725888D1224ED4903D2EFEC4 + 0520E0AD206F04BDD12610DCC1DE4EADCFB1D3CDE113E919C0D3A4C72D3DF669 + 73F85886BF2C88D18F697F00C0E57380B500E78B767911AF5DC80B0B8594912F + 160261830DA99DDABAED9DCF3BE89A172D056A1DFE5A997AF2718B68F939007E + 2FD922308E8BA1333C7BCFFA40D4A9BE947C105B3A036891A27F0580058625C1 + 5B29DA23002EA1AE83607CB31D95BE514401AB9EBE891AEE454405008CE276CF + 9DFD4EB398E16F1B015C6CFD2CB04B0AE85DB9F4D0A5A85F01E018012CFF4587 + BB4F5C80DA0EDA26F09B402F90B803C1CAE30F9C5DB9FCCCEC4FADFCC4217007 + 9153C46FF23109E09AFFD2654B798E6E86B711E008F022DA79E4AFF9AB1FC15E + 5402E0088422B8053EADDCB0BE8148379F35EBB7BF7008ECE0376B9988446B2B + 5006ACA28C5AF8E5919D09002F5F036801B3AC7C0B9898A3376F7FDDBCF3EC0F + CD7B2FFFB2D97CF463B37A17704B542EA01738C816BD099209D2F13C59B63206 + 12AF3FF8D6C12FDB36FBD6CDF7BE6E56EF2708BC72F76B4F8F3CFCBE59BEF34D + B300307CF84373F56FFEEBE6E57FF33F3517FF8B7FD76C3CFE9DD5D9B9DC49ED + E98B63B335345B4603A539A7E51C6D4AE429F071832854A030E7E90280025A79 + BD09223AA8351B708FF34682C202C38287AAC35F6D047CE54F9E360E8AE56365 + B614E6BC36EF7DDB6CDAB9B22D35A27C8CEDAFED3CD3B38069F3CACE3B8148A0 + EDF147DF79EA90D2AE812065A91EF8CDB68D07D6DFC36FEC3DB036C0E020B501 + 74B6FD727DED7D8BEFA56F876CD79AF778FDB6D9EF67DDE558E97372E48EBD8E + 074489FFDC6CDEFFD1F483D9BEB3CF1191CF76ED899CBD97CF15F87BDFAE49BE + 5E92AE55792DC9733D51BA96F933963FCB3A57520160416095F53A29AFD136D7 + 911738E7DC04C9B1C91EDF7B00F0C947DF37C7ED33AD7344EE63FD119D5B0A30 + BC7EF3A9F93D4ABA09C4458AE8A59D9D23DF47BE3359DD6F4802B951250416EC + 4502BC528AF2ED00303EB17EF13220B883B92D04B67C097A5517EDA47317EC77 + 1C886B79015C6D032D380B80A50C04060C47802BE14B9DC07104B8F28F65841F + A24E52F4AFFA95AFD79FEBC35F7FE66F0180292F9EBDDD2C9980C0AE0C7F1D00 + A3CF6F98FDBAF95B7B1320789EB243DC6BD6DEEACF0289EDBE09F805E866A83B + 8F32F47501853D7238C35F8163B39126B06BF77F1351C5F810254C99141B6A6D + F8704C22884F27B0ABC7392081DE18F5EB4037DB69A72DA1558714052C80ECCA + 631707BF79FCC29847631DC64DFB00A33696634CC6F37F5BB1134B18BF9563B9 + 68733F6C8CDF4CE415154CBE8C002EC77BA465BD974DE300B0C6A16CFD1CD5FF + D362572E6D3EB6B5FC416BE38F40C991BF5202BA560718F63135FAB8397CDC5E + EBE627CDDE151BBF6FD878F984BD46AFC33F8DD9356E1F37FEAF29FA977DD414 + E16D0973637D097DA5E8EFB621F81BB4A50FF3C479FEF4DBCD2735C72C15E79C + 4375B5396B6C87623BF9C47EA27D9C5AE81B6C13F3C71DFE0281357717B0A52C + C5F9FC904ABF087FBBBA51F0EBF037E491AF45E4B58ADA7A4569D35AC8D09A88 + 54FA495AFB29555FBFD99E621F5A8F8AEB49355B697F2BAAADC76569FD4F11C0 + 6F42B535C5DAFA68CD6F683D32B61BAA1BE733D4EF4E94FAD75AB89503FC4D8F + BCDBEE31D4CFEBEBCF00B853E42BE314DB6CBB6DE43F9101957657B6531FD856 + E2596F11CCD68EF34B1DDB77131EE0909258E5567EBF080086B31E70451EBA5B + FD69006057A53D7252FF963E88A5B6F810FEE60070F905AFF9EC50233F3CE42B + 3F706F4B4300B87E03AB4B3760B58B37DD58B795D4E64DEA4D0C00DEB438A7A8 + 9A0FE2DF6BE3FEC1B69D3EA2F0537F653B95270E980EDA2071CA063636503C68 + 03FCC33689205F1B480EC92388516DF05BD9B206C5C1724DD177AB3671904F3E + 6E05846DABF635A97D5587D6D3738003E0AD015F549BB4449B2641B14EF9688B + 132949FD24E197FA2B01B054426045FC92EF9EF7DB4DFE34E94B13BF0EFE3AE8 + 9DB3C92B6015A89A21700FF4E293816B0B81FD79C0D66F017FA3E264587EC97E + A2D9BF72CAF44EFF5FD5F39C478EE0CDAA41E00887494BFFAD00704D11D4A221 + 5B19195C0A3F81DE9A541FB7781E8AF22DE16FF411046E617058D8898A0B442A + C73C8B4DA5BC0D795FAC62816A14F20AF00E8945AE43A6C3C73F76009CF45173 + F898E9E82769116CB33B26DBE469C1AC53077FB538A7C53E2DEA958B855AD853 + C488FC62BDE4EDE8EB1DEB23C057CFE72D9C5184B535789BDA279BE0AFA43E05 + 82516C2BC9874553F9C5E7FC6AAB675446ECAAECA037DB0480E527981BC16E84 + BF355B2C4B110053E6988A8A12009EFF9405F33B2E80AF22813B088C3D016087 + C09FDF75F82B008CC80B0A03799701B500DA0C7E955744B0E4F50E721F3BBC05 + AEA6085DC02DF64EDAFE997C078115A1FBD4F4CC21F0E2850491057F0580B57D + F3C68D17CDEAB51709FE3A083665780BBC7198E6118009EED066FD9AA557ACCE + FA88BE0E79030C96B00370057F1575080006F4027ED94E1709020386790670D9 + 0E508522FC05EE79F4A9477AA60860B64C667B67DF9ED8C4337D01AE6CD5BCCE + 36D0A6A3F701936C099DC09CC09D474302CE4C09C059FEAEF57FFF9B66E3E177 + CD1113D1BFEB0FC87FEF292078F1D6D7CDCAFD1F9AB5873F379FFEFC2F9A5B7F + F7DF35677EF72F9BF5C7BFF3EDA38920061ED30F0098ADA357386F22844D3C53 + 988865CE9768D48D7B3FD86BB0E36400ECCF03B6F315FCE49C818AC03C015D07 + B744799A9FA023F2D7E610B78391FE5AED35B26530FD010C49056F1DB4E6E338 + 24B5D7E8CFCAB5D7CBF372F5DCDC630FBECF36A0A25D6F8E455B13CFEC3DF690 + 3676EDAC4C1E20ACB28EA3E34BD88EF29A1E587F0F4DA4A6A3D60EC8E9CA3EBD + 73A72FCB0B72A7F715484ED4AB95EF99EF7D7BCD0F5E587F09C4AF3984FFA139 + F6E8E766F3E14F96FFB1390200B6EBBF71FF7BEB93CF8ABD3620F63DBB164400 + DFB77EB94EF973C3F5D47B80C863A35EEF13E70124A72CE82A382BDFB50CB391 + E02FA2CC7BA5BC0030795E3329AF5FD7354263FD5982E394E7A6E3AFDD02263F + B7FC17F639F9D275041B9096EFE8CD27E6FFD844CAB37981B609D6028053B4B5 + F5C377344B203845088F42DE510185531EB02BDF528AFCEDB680467DF02BB05B + 46FFF2EC62A47AF912013C078CB5BC9EFD8B80BD8060C15C4161D20870058C51 + 04C0C05BD95596047B655759FD96BEED96D0A6A50BE6739EFB8DDD9B32FC2DA3 + 81292F9DB3FE0581B9D7E53F3B71CF5B02129FBB697E969AE6CF5A1D00B8A71B + 1EAD1BE5D1C144FEF60030F757BB9767F02B2D7C76D5FA4851C1092027F88B8D + BC2280F5AC6141E039EB3345173306608CC01FC7CC272B962304261518965D75 + 02C182BFAD2D8F6334BE219DB3F10B6320FE14A73FC0EDB731D8D4894F3C1258 + 0098A85EE0ADC6711AD34971DC869FC06F29C685F2553FB16D9957B9078DA9CF + D0378E3B2304EE8F49B17763D6EEF125C0E214017CC0C6C00908D7D53E4F9888 + 5FCBEF038EAEBD6FE363EB67D38E7DCCAED9C6996662FDE366EA08620C6EFDDA + 38BC1DAF332EB7313D205663FB688F36955104BC9220ADF24A0572CBB27CD427 + 65493ED197B4DCE2390AB0AB28DF5A9DEA1173BEA9C5776D0ED9CD23A3B04931 + 921795BE28CE5387E6B551F2297DDB39ED403DF912007BDEE6E7E3E06F9CCFEF + 4465DB7EB90F7F057CF74CAF2759BEB65EA1B4CC0FD96AEB20D126F9DA4B6DAD + C6545BDF19A75A1F88BA722D49E5EDAC2F45DFADB4D336ED1A5C566F7DAE5D17 + 5C08F93727AD2DD6D678E3BAE37654EB63485AC7ACD5ED56E9B8695D5C003881 + 5FA97EBC78CE5D1F6F467FDF0130124BE9F194318AFE3B6937A892090180950F + 5C2BF1ACE9A4D2FE06B56F5F52AD0EEDDDFB0B9CC36F16004FDBFB3B9D228027 + 0E367BF71D7823FA4D00E0BEAD762151DF2FF94AD41717EC2D7C4021F2357B27 + BE3C7CA9DE0C6C7D6D9500B8FCC26FA54A9BEA0F0FE5CA0FDCDBD03800BCDD1B + 69BCC9518EED629DEA87F2BF863460D84AB5B652E917CBDB51D99FB415008E2A + FB1CD2D600D8EA4DFEECDF836BCDBC4DB2166D524624305058DB409703CA16E8 + 6A80A981676DD0BA03001CEB7B0A6D6BEDA218A80340250DDCB7A338D8DF4AB5 + 4948DAB2B9AF38911124DE63ED9352BDEAD44F39F191ADA66E6215CA03003842 + E01200EB9FBE2504EE26917DF82BC567FE027A2501E0085EB1ED9DEB26B64813 + DF5271224C7E6AF9A4C35F01E0AEDEFC33C88DD271A3748EE44B5FCF3B90ADC3 + DADA96D025B8AD45F046DF2195FD291A38F6E7F5E44D11FE46A02B002CE03B24 + F9C4459DB8F8D383BAB9EC00379489C095BC9E7EA8A76C12D42DC16F84C1F8F1 + 4CDFB4BD73DAE2D97582A8850C808FA7FE12FC3DD3F6E3E7676DE336D0496911 + 2E2EF2690BC072212F2EE691C6C5435496118098054AA0AD24183BC3422820F6 + FD0B6ED736CF0E66B1E7854E415ECA6C79C8B3EF88188EFD6A715470D71743F1 + 7DB76B3F7BFA5233CF368926164EF19903BA7E94B6694611FAB660D7524161CA + BE2D7306C13D7F5307713BB84B5E4037C2DF18052C61432C7E53966F8CFE15E8 + 05FE2E9FBBE712082EE1EFD2D9FB56FFA05922BAD6FCE6FD19BE29FA57001801 + 84057D23E895AF47FD669BFB00861DDA027E25C06D02BD2B3C0713F8411FD62E + D900BF09F26A8BE60480491344A64ED1C4CB82C0D79E35EB375E366BD75F342B + 57BFE8207088E0150496003B4788A4E4D9BB562F5F01602400447463B755733F + 9217F0040026FA57F037460103807946F0D2D5D446919288F69481BD405FA063 + 026BAFCCF6D2DA5ADF1E010CE4FADAA368817C3C97B783C0DF390406AA1255BB + 79FF9BE6D8836F1D2AAEDD4EF00CA08612247DD9ACDF4D10F8E8A3EF9B0D13F0 + F708F0D7DA2FDCFEAA59BC63E7F0F8E766996DA6D942F8D91F9B23CFFEC2F23F + 34F3B7EC1A03DD1E9AFF9DAF9A65F307FAAE9B8802E69C8804DEBC6F023812F9 + 7BDBFAC3CE36D401BE1EB33E007DBE25B10918CA73723977EC2D60B573172425 + D5EB115C954D005130512093BE1D725A3E5D0FB36708ED4034E739A7A35C3BCB + A7E7CFBEF073D2796C3AACA5ED2BB79D78CCEB4CE056C7472AB7368E6DE9DA7D + A0ADE51FDAF1F2EB4CF5E697FDF51E51CF7B79D2AE91A25C01BF271FFED89C7C + FCA39FE7DAFD170E71E9739D73B6EB7D04A0CD33A21FFEE4F077FD9EBDC70FEC + 3D341135BE69E7EC91C8762EA92DC035C15DAE17D0BD0680057A65F7EDC5B30D + 3994B5CF02BEADECDAF0FA0471D3B54F905B79EA2200D6358B00987A6FDFFBCE + A46DCEE33978FD4DABCB5BB11FB1EFE106DF49B68FBE6EDF71DF92FD917D1E1F + 9B2DC15F2280134C7D6CBF1F4408DB6B3109044729B23846FC9611C0EBEC02C0 + F17D1B6A7E470497533BC15FC0B0A02FC7EFFA48DB52638BD1BF110063C747E7 + 8E8FA28101C0F8B66D337855C42F00977284BDE4C795056F639D6C4850973C75 + 02BF51F225AF2DA15506F22AFAD761EF85519B203091C0FAA313A9036140F159 + 2B67080C00F628E0BCFD73DA023A405F2B1315BC0C30A6DD6756661BE90C8181 + B6C937015EE47EB9CF59AB271258FDE00BFCE5D9C01D004EE9BC296E2F2DE85B + 93606F2C47E85B83C0A4F23B646318C63112E317C6348C7B183BC946CA98E720 + 361B7FB14B4B1C93699CA6BAB88B8B8FEBF031310E6CB783CE754A952FC77CB1 + 1FD9C8F700701E73D6C69ED10EFCC5B6FF18CF03EEB67BD6D815189C6CDD1F19 + 8724003CA1E70603532D3D70ECD366E2C8C7CDC486F91CFDA4D9B7FE71B3772D + C1DF088025CA80D80863A3E4AFB17FCD07C53E9407DCEAF9BE2A47B08B4DFDAA + 8DFC22F4457B97DF7191671BE828EF0FBF0C7A510980E336D0D8A6ACDD7EEB4F + 734DCD439597986B6ADE19E7A65125948D2A7D91EAD44636A5EA2FD60FB6C3AF + 374F4FF3F68979ABCF5B3FD7FED8BD538DF69101F0F47A9BB24E5102E0C9B9A3 + 9E76EB141DE0553EAE7144DF58A77AD9BAB5944E0E802BEB3A52BB7653A98B8A + EB3C659B721D4AE5EDAC4DA1E8FF26D5AEC1956B75A85D3B5CF07C577EB3D2FA + 6E5CE7D5BA63992A5F9649CB3E6ABE35956D76AB5E3F00DF1E0036153ECAF715 + D7D85F4F7F06C01DC4DD2EC88DFE5BB5297D4BD5DAF435DB675B6F83AFEDE579 + BCC3C7796D00ACD7A03EF2F6CF5B835D53CB1EC709DF70BCD714DB52EF73FD67 + 02808704E51E7D0073DF47173995CD3FFE5320BEB1F10D7E83DA3B31D7ECE347 + 8B285FEB9F8778EFB31F45B75B8AD217872F5754F9C5DA4201BAF6BEAC1518FB + DA529F51657DCEEB3C067F4080DFD4851FB6DA8FDE9B5177532A6F7035BBF2D1 + 36A4D27737AAF5FB3A1A770C0D22A24A488A6A7D6CA5A13E4A7BA9717EAA43F1 + F5D5EA65AFBD1E249FF61A6133EDC9759336889C5D3AD1CCAD9C6A26D8DE18FB + 819551001C81AEA441E82F3CC84571305D531CB4C74860497EB53E4B5FF7F789 + 45AA8B9391BD876D2262D2B6CB09F85A5F23B26369D292272ED8F71C5EB1F676 + 7DBD6F6CA95F4D72BAC94E3A4E7BDC01BF28AF2F9E012C39044601FE0A00EF25 + 123883E17D050C8ED0374A0038425601556DAF5CC2E1BDF349D827F33FB2272C + 9D5860729DCBDE1F7D254DDAC47D2A8B365D9DF92F98AD15C74C9AB072D4DE39 + CE03584DFB64633BB0BD1CCBCF81C5830463E3337D1DC0025DB3E423601B259F + 588EF535A98D7C23008E1038026041E0369A77FD745B0FDC2DC17094FBACA57F + E04FADA7342EE6F4167BC8B3001494806C02BF6EC337FBF1BC5EA02EF59423F0 + 2DCBF8E0DB87C092D94F20CBBBCF27D6370AE0D917D53A1D3AD92DF6A5488B24 + 16FC7C31CEF2488B7C5ADCD3621E791611633DE59EDE4D51BBBE20E932BFF7CF + 36D31F9E376550FB7E8A04E6B97707F001E6B2F889DD2130F517ACEDF966FFBB + E667F6C3B4331DB0FE0EBE63C7F545CF73CD2CF0D7FCE73EB868F98B5D048CD9 + 1D009FC951401F67E00BA005B266D500AF007104C01EA1947D5A7F530773C927 + 09DE522F00DC45F45A5F9EBFE17DCA8ED49F4361DA63277A97285EA276CFDD6B + 9680B200DB73D84DBEDD73A7052280CFDE772DA073F79B257F16EF3D87BE739F + A5A86000AF227B791EF09CD525E89BE02F628BE6C5F3805FA27A89EE05D42668 + DBE96180BB88BC94206F5F4FBC9DFC17CDB66065449EAD9A5384EE3387BF6895 + 67E1025CDD9E2377738A0480013EC063EA7A00D8EC824CF8026E01C0D8049C28 + CBB6CAB35B6F5ADDED2F5AF88B6DF5A6B5B73ED8025AC098F6115CF9313C0298 + A846A22BBFB63E80B7696B656DAFBC44843351C0F7BF6D784E2F5B3F13ADCB73 + 78D7EE27B80A0006BA01CF48D789F204BE9928A316C0590AC43DFEF4A7E6D8E3 + 1F7D7B6800B06F09FDE0FB66C5CA4B569EB7632EDDFDAE597FFCB3D57FEF90F1 + E803A0F3370EA757ECFC8EB0CD33ED1C487FDB6C3EFCA139F1F8A786A8DA8D0C + 7ED3B19312FC25DAF65B8FA43D72DF5EAF9DCF1A5B133BCCB4EB90B74C76FB5D + CE37695D3EF7B29FF9A064B36BE57EE663F52D2C069EDA6BA1CD2ACFD8E55CE8 + DFAF99CE2F03604F01D5D69F5DA7638FEC1A3D49D054C7E5DC361F035A79BDF9 + BCB107D15650D3CB761E9C9BCE390260879E9C273EF65E9102985D5C2B60BD9D + 0F309632E7B961B6753B8FF5872660A9BD9747D882DB5E0BDB70AFDBB5A7ECB2 + F7E3C823935D7387FEBC16DA3DB4633EE0B87C46BE688EDA678A63E97812E7C6 + B3A4D76FBF48D7D45E975F57135B37F339E739C41BF679F3A869DAD9B92A025B + D2B5E0B3E0D7C4FA510430F5B22B4F0AF81540D616D8EBFC29A28D984FF09AEF + 13DF25FD1983EF602B2BEBCF18AB379FDA39526FDF5DB3AD005C6FD8EF8BD5AD + 5C972D7F571D06735C1320D95E2330D9456471FCAE9BBFFFA6581B87BFFEBD06 + 56D317E790BEEBEC0600885EBCFAB05900AE5A0A94263299735AF7C861CEE781 + F9027613205684EF1211C59C2B7E37C8F3FBD7450C2F006081B45626251A1828 + 4C44F0AAA0320037F7D79633BC8D703796490584230826EF103747FCC67AF553 + F685AFDA929F3B7BB399F9DCEE81E76E37CB17CDD704249E353BCF100606B73A + 7BCBEE6300DF2CB3CDE3079C05D29E37DB79C07182C0739FF1E7A8046491A0ED + D2B99B2E415DE45B3F036DC967692BE916FE7E6AF7E733362EC037DB01BD6E33 + 25704C7F298DF0378D29BAC8DE087D552E85FD306D6C2CA33FAEA98F5202C3FE + C7364BF1F73FC565E0AB4763A81F22807D4C96C76D1ABF291FC76B2ABBCD95E1 + 70A88F5037F61125981C7D7D1C69E34541DD984787E80B5FCB4768ABBA03C7B1 + E70860C6B1FCB9D1EA7D5B676DF14C997E890CF63A1B3F33A696AC3C65E3E849 + 80ABA51A4B13113C61E5BDEB1F5A0A2036594ABD8FBBF165ECEDCF10EE80AE9E + F15B539C0F68AE205BB44B02B882B882BDB10E1BA9FA8AEDA3AFDA0B04AB5C4A + F5FB02F41D277C2691E5D90A7ADF0290D5E6AF36A74C73D830EF041823F765DE + D8D523DAF0C7E634B705CC763057F05612C41DCA97F037D647BF5E39C3DFA889 + 0C80BBF97B57275B94E6FA437ED4971078CF4C82C0BE1D346B0FD3569EEE0030 + 6B1102BA5204C248EB15DB59D740F21B81BFEAE3F0689DC433823D3080F594BC + E6B35D956B4571FDA8AC1B126B48B576AF25ADC145E0DBAED3D939F9DA5D5AE7 + 630B68CFB30E18E4EB8159AC15CABF6B575F4B8C8AEB935165DB28D62A91DA2B + 2D15FBAB69A7FE3B919FEB545F11C8D6C16C61F3F57E93E5770372E3F1FEBE6B + 90A914921FF2E7F8066DE55FAAE63F2AF84FD6DE99A4682B040B8B1AF181A5ED + CDCF1BC61FED0DBC6E0B76B72300ACBE6AEAF952964A06599103E32185BE87F2 + 3BD221BB467590BB5BFD2601B0AB47D253197BB475DAED05DDBDF8D24CD98FE7 + 045FDA3D33962E3493FC904E9ACD7E40D128FC95CA2FD61815D0B5F7652D01ED + 36A43E46FA1952681BCBB19F5E5FF233953E52ED47EFF5C48D6367D2CDAF5657 + 53BC61EE44DC5C6BFDBD8E6AC74143759CC3B8F2388D3BEE901DF9A02268A83E + DA6AC78A36D987FA44BD819FF92020B06F059D0763930781BE44F5F2DC5FDAE1 + 9706953E98AC0D42DF926A83EA718A2057AAF9A1AA6F983C289F40AF4D200EDB + A4C7C4A4070900EF73D9B502FECE589B19EBC7263AFE2F59C424C5ECF29D20E5 + 59C26D5F0059A549E9581CD3DA1F4A693C7E4CD324A8ABF7BA43F2497037AA3D + 464EA35A1B7E36B9454391C002BF09EABED31C5879DF05046662AC6DA1CB3CBE + BEF57200B46CE92C40EB90D6CA4911DE929E6A26E76D525E28B68DEAFA49D017 + EDC9EA1607DE6DF6A0D66EE726B04A9A416D0980654311E2AAFE8DC8FAE33C80 + BF44249317FC1500A62EC2DF9A1C04AF017E59986121262DC6C4C51716665C2C + F064B188A448DC148DFB7133B96E7D1E01E826280CBC1D078065234DFD252510 + 9C52EF2382DD36CF62DBA709FCB238C682974991BF2CAEA5C5BECFB36F5A9843 + 6DB4068B73F8BC931604E362A0EC12E568737F04B0FD80F49CC35F44D901300B + 981F9C6F664EB3487AB9394C44CB07175D2C741E7AFFB2E95273F0DD0BCDFE77 + CE37FBDFBBD84C028381BB1F5F697D3D9AF8BDF3CDFC87179BB9F72FB8E6CDBE + F0E125D79C09003C93175DA73F6251B5DBEE19902B45C00B008E7E1112477F3D + AF303D8F97E8DDA4F87C5ED2E98F8810B636399277E13322796F370B8867FB7E + 7ABB992722D7EA66AD3C03F8FD9C6D9AD356CD8AC4450BE781B22687BBF79AB9 + CFEE5635FFF9BD5640E0C5F344F93EF036F36C0DFD195B74D29FF57301B06B29 + 3E171E364B26F7CDB624D5037C47A16E04BDE4D98EB90E7F73C4EFC5871EA55B + C25A4158F2E999BD190413D50BC805BA02684D31DFDA4C8AE66D6171B627B09B + 200EC710C0053611014979D18E8B2D46210AFAAE598A1DE938A4F86013B8523B + 8F3AA46FA0D93DC0E277CD32F0F5F62B07BE4B965FB9CB337F13FCF588CFFBDF + 38FC05DE1E41E4893205B6990068803B7FBE2A50ED5EDA4EB8057100D847DF37 + C71FB36DF00FCD8695018968ED3EDB047F9380A2B539FAD07C3CFAF4A78667E5 + A6ED9D919D2F11CA77CD27C349E0EE71EBD79FF36BC7159026CF71E356CA9B8F + 3826AF81D764FD3C48709628595207BA19B2AE9AF0396A6DBCFEA11DD3F20818 + BA72EF65B3CAEBB7BED680B519D8026EFD18760E0E601FD9B11179AE83D98946 + 06F602A58F5B7F7E7ED89F7CEFF263DA314881BFD1E6CFF5E59C798D59095CDB + 7B60C7F03A3B8EBF2F5647DFBA2692CA4051D2934F7EB0F7E53B7FCF281319ED + D1D1F6F958058A5A5FC0DCA3F6DEF1FEBBEC35B08DF706EF6506BE94D7729DFE + 2C0034DE786CD7ED89F5479E48E0DB5F58DF2F9BE376AE27EC5A6D02C5F3FB07 + 784FE0DCAE97D5F39E900780935F06B2DEB3CFEF7DFCECB37CD77CAD9D3EFF7A + DF255E2B36C0EE128034FF4181CF0529B097EBA076C0787FC6F12DA03A7D587B + FBECAD5236DF0DCEC77C134CB673B9F3DCFC12945EB949FFF65DBE95A382AD0E + ADDF36BB83629EDB9DFEBC814F8AD8B7DF25227B795DB7F81EDBF73E44F6FBB5 + B2CF1AE27B9EBEEBE95CFC1CFC7703E08BF86DE8FEEC0110F66DE1AF3F323D6C + 966F3CB4F330DD7A68E79B40F08AD996AFDF373DB0368FED98C0E9007EEDBCE9 + 6305006C5ABA863FFDD96FEE55FBED372D5DB3FC15FB2DBF6ABFC7395D42E6AB + E862416340F1C245BB6F006B81B200DD9C17B815D8258A386E2DAD6DA4057AE5 + 3724F5A73E80C640DEF259C1946527F57C06BE0B4404B375B469DEF273E700C8 + 76AF3D77C3FAB0FBE5C5DB56C7FDF07A330D9CCDF0574017FBACE5BD9CE5CFF9 + 459FA5E85FEA673FC3CFEED39FD2C6526B3743F42F3B847C626DB8FF7BCA1FBA + AC0DA28CBF893F6F69ACC038E0B03F4E82710CE304C61469DB67C1DF5A74303B + 91CC22DA65080CEC55BB087E2308E60F72FE2739C650B99D40B07643F13FDED9 + 38CC7749B171197F942315283E90775C419E37DB21DAAB0FEACC96DA599B3016 + D4582F4A633EF22D2426651C598C2F11368D3FF1433EF6CCE354EA5B3BE3D963 + 8C95F98363FAD3631C5BBBFD848D676DBC7BC8C6C51A4FC731B4D4FE1153764B + 81C21265EA80BCC056F73F62659B33A4B1BBB5F37AF3CD730934C5D89E3983A5 + FB95CF8A1038CE1B22DC95349F88B038DA65AB29F613D583BE159510D8812FD1 + C215FBC41273C16EFEAAB9ACCF2F1DF826918F924F6A17DA0266173B481B156D + E54E57DB55098881B502BEA50473633942DCA8717E250056B9078567B0B3D691 + D63B046D81BE68FFC2B1666A7ED3F325D08D521D12242E7DB52E93D664FAEDCA + FAE8E73EF853AEAC030DA95C838AEB5265DD905853AAB57B2D090047081CD6E9 + 38EE9E83CB69ADEF40B7EED75358175450481436AD2B0EAD2F8E5B838CEDA24A + 003C4EEA6B9CD46FAD6EB74A7D927612908D1A5D730FE50C7FD1A8EFB0E45B3F + C6DF6F392B895C25482C659F64B62887C03586F3A6B48DBE758ED2A84FD7DE21 + B0F767AAF0B66D090E2859B9F708D65D897E8654F31F503CAFA821BFD2EEA2AE + 1FF9BB67EFFE5E7937FAD501B0DAD5DA47C85BB3753A5CB960BF80F4E6106D6C + 1FD609FB50237D78F7EE230AD87E4CD9537F9FFDE0F7C417402ABF086F4FBD2F + 235FB82115ED5A7F4BE397BAD79F7CC6F8A1DA8FDDEB891BC7CE146F80A52DFA + 9575BB51ADBFA85A9B52D177680050F6F5BA0385ED9E9FFCC6F98FAB1B37A829 + DBC9B7F48F033C97D5BB2CAF760CC4DAB696D7C0CF8F3145B90F80D3C0320F40 + B3ED97507B9C9CDF897A30774C5D4D9A54D42713660F801520BC6F66AD9970AD + D80403006C9AB1F39EB3E3CDD9A48449D1ACA573D6BF696276BD999C5D6B26CD + 6782ADA203781D271DB3B40BE6027FCBBA7D79CB67A984C0ADB44D7488104693 + B34C2213E81D01BFB29BD244384D9EDB67EA5ADE81EF8C4D28F1B57A52E593DF + 7B36A106147711BAA56A75137375008C22F8A5FFA9453B46B0EDB309FC1E9322 + 7D7D8B67DF62ECBD662F8035DB0579B7038025CABF14008E10987C5C94D90A00 + B70B3B94D7597C79CFC4428CBD07EBE4D3620CD2E24E6FA1C917A1FA62D1A87D + BED8D18F1CE0968B551276D5459F047F533B8F4EB0BC16D552746E0779D93E2F + 41DF6EE12D2EB0950B709ECF0B76BE8097616E4D71E18F54C098320B811E7D9B + E1AFC06F077F89004E6201332D765EC930F55603683DF8C185E6E0FB97CDFF4A + 33758A05CFCBBE5DF101F3DBF7CE39AB6711352DD8CE7C78D98F49E42FF07796 + 28604B0580E74D6D740DCFF363B19668DC007523D015E0A55ECF06C6AFF5CDF9 + B4D04B1490A2776F3BBC9DF9F8BA09087CB3078251B425087CDBB5C8737D690F + 00C6CF34F3A9A55C8F2CA2765334EE5D8FD29D3F7BD7A12E02F6CE7E4A54EF28 + F88DE5E4FFC0F4D0D3D6C7217006BF171FF540ED22A016181C147D14C18B78E6 + 2E69AA4FE5657F5E70BF4FB4645ABCC416A929220F700B4C6D217006C12DC005 + B49ACAB2B673F63E1CC4740038C25E840D9117A0551BF202B72AF7E4D1BC4909 + 80BD34BFD4A7FA57A420F54AD316B3D6F7BDAF9B8D873F346BF77F6856EE7FDF + 2CDFFDB659B8F5AA59622BE507DF376CB7CC36D040623DC717F80B0CF6F401E0 + 36C3DF3B09FE02CE1CAAD9793B78B63A4434275B18B365F4E6A3EF9BCDC73F38 + 544CD1A4DF361BC04422741F70AC6F9BA30FBF6F8EE173FFBB66DDA37E3394B4 + 73020A037E4F3CFEB139FE880860B6534EB08F48D663C0DAFB29B29332A9C3D7 + 4709007B54ACE5A33C52D55E8F525E9700B0DA0063D111B33988A59FC756FF28 + B5A1BC417FF8F01A2C2F78EB30D65FFF37CD497BDDBEDDB2F93888A4AD897647 + 9F0055D3F909FE9E7866AFF3E90F5EC6EEFDE514D19673F573B6BC834F7B6F05 + 7F057C91CA1190B2BDB48028E5E3766E1EF9CB7BC7FB4B9D5DEB047A81BB40EF + 6F9A8FBEFB87CDB57FF8BF6ACEFEFEEF1C0E2FDFF9CAEDF80B141F017E3FB6E3 + 3F21B5FE1F02C15FDA757865C7F9BA3961F540608ECFF9F01A56EED8E707006C + 76FF7CF19ACCCFEBEEDAF7CADA1351BC7A97CFB089F30C9F37FA693F77F69A48 + 01BFCB44C7DA6753AFD38F97017082C0B4030A9B3F11C0FCE1204792BBCDA170 + 92470A03AAEFBDB0F7D6DA02B6EF5AFF767E6B006187C2405DC1E02FED7A7E69 + 9F5F229B01B87CC7ED77E33AD1B8A95ED0D7238073196DDCE579C7766DF97ED9 + F9F27A80D42BF61DE6B9DD9CD7EA4D7E272C257ADF7E1756AEDBEF08C0D88FFF + D4FA796C6DECB7CDB47AEB812BC1DF94A2D59B8FCD6EBF8537ECB791E8E42C20 + F53250F806C0DA7E6BAF0177ED77FB8AFDDE9B1204BE6FF5F83DCA3E02C54940 + 6501E0F94BF6DB0E9CBDFAD0F38A22A63EC2DBF85C606C6B962705DA0AEE2281 + DC6893564CAA07002B72983A45130BFEF644B4AFA58A360654CF996DF6EC8D1E + 0046F3E7B82FDAFD19984B54AEF96013FC95F4ACE014B16BCAFE1E3DCC73833F + E7BE4ABB246070F7C805EED3805EEED5DCCF79A403E30DEEFB8C0192182BD066 + DAEC8C17D2D841E387F4873301E00876D1AC8D2DE64EA73FA7B511BD01F6A208 + 80692B002C9BE031F25D5300BF19EC3ADC05E002728BFCFE9C7AFED4672D1826 + 95AFEC93273E69A64E767F0894CA723B2EC45E8831651C8392D67CA25F5B2615 + 003EFE69337DDCC699948FDA58186DF247C84F3318263278740C2DB5E3E62CC6 + EDB1CC6E398CD5058B3DFA97B2E5DBADA1BDDEF2363F000EB7F384429A43C479 + 02699A4FA414402B802B185CD64BA57FAC438AF08DF95AB9077FE5E3F3B10483 + 05807B5B4787BAFECE53295F825EE6A83E97344D2C589B6C571DF34A6F03580E + 9017B195B4806F990AE4AA4D0B768386EC48D0364AE037425DD95B606B8A7EF2 + 2D7DD038001CEBBA7202B731DA3746FFC67595B8D622883B4EF28DED6BF5D260 + BB624D684843EB4FB20FD5D754B629B51D9F9E047F2DDFEED437E2B39415D6FE + A2E2DA604DE6C33AE176555B8394ADA6E8A3BCD636E31A67AC7FBBE2F8E335BA + E61ECB4975DF610DF7FF67392B115729249602FC854145F82B25A05A6F3FC278 + C6F94AFE4C60CEC9447E8B363AC7EAEBA0AD4705E77C54C9DD6AAAC1D2B100F5 + 5794CEABD4905F69775137BAF5F39ED75504AE6F5B25F02D21701DF676EAFC08 + B7AE5DB45F50F60598B41FCE0307579A8949BBC1ECB51FB18945B32D3553ECA1 + 6FE53DFB4CFE45D117264A1FF85C2EBF20A5F0CD797DB9A3B0F5FCC7483F1C65 + 1FF118232AEAE297BBF6052FEBA5DA0FDDEB891BC7CEA51B5F698B37EC58177D + 76AA5A3F51B5365B49E719CFF7B7A4785E4A6BAF597EA554DF89367A8F729F26 + 0DE0B09703BAA8B2FF5EBDB721FFEB46009703E99DC881AEA5EA8B727C4E70DC + 2A3A96D94E284D1AAC1F57C803620FD9A4C7B40FE87A3845F2A6885EBB566CEF + EC51C0764CFF47EC66B367363F8F8763D88464CAFADB6F3E931C376FD11C416D + 94A06F0DFED604B8ADD9A304775BCDD80450D0376E134DB46E560DFEBA32F88D + 7057B017F80B04968FEA9830EF5F4E91C2FB99ACE73AA5B19F520906DBE43A80 + 60521780384FCA873401F035017E13FC7DBF99F04581BE047A7DCBE55C962D0A + 40DB9681B5BBD056C0382EC408062BAF946347BF58AFC51DD70680F7C3E6E0D1 + 0EFA6AF1C8453E4B0038E5BB05A34E943B1F2D52956A216FC51E21305BDC09EA + 6AE1AC9416D2DA05B56C8B0B6FC05FB6FCF3B249650162F9CBAEBAB64DAEA73C + 2B00FCDEB9669A45C9F7CF360701C001FE1E26FD206F57683E53EFD8B9589985 + 5380EBA1F72F9ADF554B13003EFCC1A566F6D35BCDC18F6E3493EF5F6EF6BF7F + A999397DAD99FD28F9D34F04C0B3962E9A7D11080CC0F5E89C2C5FCCBD922030 + 0BBA764C443EEAB0B5432CECCA96A0736ED303C009E67AF4EE991BE683583866 + 8138015FEA49658B00B827FAF0086016A6D336CD0800EC5B37F7CA82BA09020B + E896920F5A3CF7C0F4A859268A97A85EDAFA3382AD3E43E0087A5D3CF3F7FCC3 + 8628E125B674BEFAD452B66D4EDB3563235DBEF2C4E12CE5549FB775763F13DB + 3A037681C428FB7BD42E7096B626FC6843598057C0372A46FD0AE42A7A58F037 + 02596C44F7923A9CBD9D806F04C3F2F5B6B7004300D6248FB8F57C02AFAB0E85 + 921F908A0841B6854EB6E4EBC7A0FECE57BE153311BF6B0F7E68369FFCAE01FC + B23533DBF9AEDFFBD6B7825ECF0018792430D19A40DB87E6F79068D757CD1A60 + 8CF3B03EFDBCC8DBF136CC9E60E3D769ABE50740D31F9A634F78466C0781C9AB + 2CC89C8E67E7008433DB319E838BDFA3E42BFF638F690F58FED6B75026BF41B4 + A91DCBA37E2D1550440E50735A0A3BC051D1C002AB0E21D5D6A1A6A544B66639 + E82CFA948FFAF467ED5A19007C82D79C21A4470753F7C46C02C63A96A54060E0 + 2FF928016287D2D63FE7DAA626A26625BFEEB4B13E1179A287794F549FF2D47F + E769F2B7633CFDC9E12F10182DD9E707B87BFF9FFF87E6AFFFD3FFD27CF1EFFE + 37CDF1673F3B000610F3AC67DEE30D6BEBDB463FB2CFD7E35796F23A01B429A2 + 1518BA71FFCB64B3E3A7F7FD2BFB5CDAF706B04BDBFC9A78BDE9BDB0CFEF03C4 + 7B425F09F8EA79C1FCD100C09B3E739D046E2304974DB098CFAE6F2BEDE03789 + B6A92E7FBF00BFFE3957DB24CE03080C905EBF9BB47A3B45FA0271058093CCAF + 04BC190A2BEAD7BF37F49D61F0913BFD63AFDA776B85EFB07DDF89FAA58C96F9 + BDB9C2EF96FD7EB55B54039901B80F9BE51B0F4CC0DA0756F7C86DA4AB379FB8 + 14959C2293BB08E014059C80B5A0AE40B0E02F5B5E0B02472D0183B525F49504 + 813DE2D7CAE42300668B68B6906E6D96B611C3D957D1C3A4D4CD5FB0FB4E86B4 + 92FCDDCFCA7319E6028253247102C002BED48D8A3A206F4A936E36B367AF7BEA + F0D75260701BE97BF686DD977896303037835FB3F97383AD9D9EF33BFF598A14 + 4EC0986DA4B98702926FB8048081B7405FFED0C5CE1E3C8798B2435EC60126FF + F35765BC00FCED3DC201BF1C0D0C046EC71F5286BA2E41DC007B912030A9F211 + 00AB1D623C152380230C1ECAA308866B76E0EF94203180D8C6792DF4CD62EC47 + 993A493E5E872D8F3D355E45D890FA297D7C0C8B8FA50E7F4F7CD6CC9EB071A7 + A53102B87DD40963E23C16AF8DAFB175E3F3D31EE9EB658976405F1BB7A7AD9F + 03040E6379F2FA33A7AB02814B00ECB263C4B28E4B1EA8EB3036035D81DE087B + 637D540B7243BE5404BF2300987219ED1B1441F094CDDBF8337204BF12F35194 + CA9A43467BAA53D91F55B46CF90C7405814BF05BAA07750B5BCDA7AD0B80B794 + C0EEE4821D976DAEB34D2057D017C91625B0ABFC9E69D6323A5B6B0F4A76EB37 + C05FC4E3B2BAB594D522BF9A416D1F1C0BDE2AAF543017C927DA86D4C2DFBCDE + B39576B20615DBD5EA77A2785C1DBBB48D4830782428633909089CD7F514E4D1 + ADF175EB89ED1A6161AFC9D71A2B7654D6C5354629D6FFF6C47A6C6D5D3DA95B + 77AD6BC8BFB4978A7D20D6746B7E7F9FE5BC444CA5E03391A90C46006F47E398 + CF386DA35D3C4734EA936D7A6D528DB74911A096C0D4F2D56DA14BBFB72D1DBF + D4381FB38D44303BF73CF8662588FA6BE84F1500EF9D9AF3ED9E01C093930BCD + 9C0D504EDBA4E1FD0F2F3587ED063C611F6CEC7B27EC87D03FD47CC84B850F3C + 1AF9720CC87C4B702B8DF43350E6CB38419ADB55DB66BFD226C52F76CDAFAC97 + 6A3F74AFA70414772ADD00B76B8F753B55ADAFA85A9B71FAAD0D326AE7A3AD4F + A26D5CDBEDF8A21EFC0DA24E83B976701754F63DEA47DE0695D57F17566C6F50 + E5C078B78AFD69901E0170543B88473EB9B0D4272A669FB1F63CF717887AC826 + 50878E3693078F349387ACBF433689E14F2FA46CEB6C29EDF698AF43D06900B0 + 4DB4A669676D0EAE3553A6C9835B03E09DAA85B763EA80BC93A459D1DFCF27E7 + 057F07A37FA9233F7F3245FBE6B2A4329363015CF240DF436B1F3687D74F7BCA + 045CF5EA2FFA4BADCD040016FCDD376B7644DEFF8DADC97A5F82BFADEC3C12E0 + 7DBF6F371105CC56D0DA2E9A32F6087FA3046B01B1DB05C18A16DE2E0016E8F5 + FE6997EDAAD3C20DF9286C5AC0691787363E08F92407C17931A9AFBC3064D2A2 + 515AB0229FDAA072812AAAE623F8ABEDA379E6AF6FF16CD2625A4D82B5ED629D + 49795F64A3BF6C77E505395FD4B3F62CC2F9D6CFB99D0B10FC6E7E565CF6C536 + 83CD74F81D2BBF9FA02FC01700ECDB414BC0E1BC50B99FE7F9B24DB46FEB7CC1 + 174F173EB9656DAF7914F081F7CE37FB4E9D6BA63E64BBC6FBCDE133D7CD8FE7 + F3A648DDD9D3973DD217F8CBF181C1CBD6075AFAE8AA6FB7A8455541DC120013 + C5834D65B7153E480BBEC0DF6ED1F7B6035D415DD214F19BE0AF227EA3AD04C0 + 8B9FDFC951C0B79B18F58BF42C5E8F00EE9513DC05F22AFA37C2DE12022B0502 + 2F9D7FE81018B00BFC250A58A90410F6E85DA278F1BDF8D0016A096A3DBD9CD2 + A5AB1DD8A51C25BBEB5A4A15ED4B1EB00CD88D36E0AF4707D326D7CB26384C1E + A82B908B280BFE46002C382C3BA0D75F8B1D4F6D048E3D0AF04E92E095E09452 + 24A8A5C84752F7771141F8A259B6BE8079CBB75F351B8F7E6E3EFAEE1F35677E + F8C7CD89E7BF77C0C7F35D57EF000A01BD697B5FDFD697284F8FD84D11A9C039 + 078EF7FAF0CDCBB201195D44C512D1FA5323884BD9B71836D1A7B69AA6CCB156 + EFB2C5B2B5A51EBFC729ED4071074B81A13152D6F319C2CA2FD6CBAE3AD980BE + 00C728EC6A0BF04DD1ADE66F02747A047010F56B0FAD6D0698EADF9FB70B6005 + BAFA354AE095285F45FAC67E744E805001DE783E3AA7D8C6A1693E9E6CE4058C + DDCE6BC8BEB5FE488F3FB5F7EAD9CF0E74D336CF7C06EC5CCD76E12FFE45F3E8 + 5FFDF7CDD5BFFEB7F699F99DBF279BBC97BCAF967A24B05D97B507AF9A954776 + 3D1F7E65C7B0E3DC076E027F931CE6FAB54CAF9BCFD29AD9FC9A728EF975FB6B + A27D7E9E30797DDEFCB9C5269E290CDCC5EEB0D7FA3B66E7E13E56E6DA13F18C + 8FBE33EA43DF25EF0B489CFB88756A43993CDF2BC02B11B9BE1DB543ED94AE7B + 747282BB25F425BF699FB14D22D533E8ED01E0DC6E335F2B20B36FC79D21B01F + FB36BF07E9F744757CD757AEF20714FB0DCADB4E13014C74AFA27D13F095D8F6 + D97ED3B2FC19E201F8A63EACCF9B299F949E219CA07182C8294298F61DF48D11 + C1C05CB46A7ECA23B67F8E0098BC836013B6454B15314C1E7F602F7ED8A8A3EC + A94439FB0818CF6B6BE7F3B79BA54B779BE5CB09060381CB08E14E1D04160866 + CBE7F9F376BF0C11C0D8781EB047FE027ACF017401B9767FCE00581078F12CCF + 05260AF88A6F05EDCFFF6DA37F6F343344FD0281CD36E3DB3D5BFB0C8097CC07 + 084C79E499FCB92C5176681C8031E383344660AC9020704F3C5B38E77D3B6822 + 8103FC15008E36F200E01602DB5829CA1FB911A4F1D53809F80ED509042B22F8 + 601EE7B580D724E04BAADD6054A7F12263C7087705780FE671A544597E9EA73F + 2B13F93B73E273938D4179BC4904C0F81663658D9D3566468CCD057D492781AB + 412DD4B51408ECE3F70C81E3985EBBF9B83F699857B4E037CF2D4AD8DBFBF3A8 + 29D60B044BAA130476581BC06FA9B25E8037AA04C0A47BD9D169899D9C6C3E58 + 01C0510060E6A368080497D27C34E5ED583E2FCD4078C9E6AF02B419DE0AE0C6 + F256521F6A17CBAD3D03DC9A0478A7164F36FB97ECBC02041608567E14E25694 + EBD436B6896DA99F983DDA82DCA4B49692402E5B431315DCD923E455BE54AC2B + FDD2FA4CDF3FC2DF28ADF16CA572ED28AE1F0D497EB15DD9C756927FA9B2AE57 + 1E5A8B6B2380B59E57A8B6E6176C5A5F44E55A612C47BBEA94AFF996EB9A28FA + FF36C43A747F5DBD04B45BA9D636DAA262BBA89AEFDF77B5DC640BB6F2AB00E0 + 6DB6E5FC6AF62D0170094533181DA9DF8DED4D497DD7B4954FAD1FE5639DE49C + 739483BE9604517F0DFD4902607B73F64ECE3513FC70DA87957F1C9CFEF862F3 + 77FFFCBF6AFEF6EFFE7D73EADDCF9B3D7BF827C26CB38F2FB142E647547E1906 + C417C2D2F64BA42F891F3BC05B29B62D15EAFD61E156F6F6AA2F34FCC54D75BB + 51ED47EEF59420EA387193ABD96AF621E1BBDB1B78ADBF9D2AF6C7396C1798BE + 0995C78F525D097CB72AD754F68D641FF135F506725C275214EBB2F0976AF57B + F6A72DA07F49001C07BF6F4B71D05EC25FE4FF12F58984F94F037EC99BCD262C + 0E510F1D6B260F6D365300E083EB0E8127EDB54C5ADF13EA8767F3FAE409509A + 612891B600566B37718076963A701D05B682B202C4830AF036B66BCB44F46639 + F8AD6804009BC8F36FDE088001AF250416E4150056994931692DCF44FBE0EA07 + 0E80890266224D9D6F17CDB17C429D5403C09DDD26E03912D8E12F40B8027EA5 + 11001CA4AD9F6359CF03DE090046BB01C06321307526FA75601D6C5AA4898B36 + 92DA449B1661505CB841BEE8C4824EA91C3530AA8F9AA94DAB0F0B555AA48A65 + FC58A4228D762D640180A74D3CF3372E9E6901AD14762DC251D6A21C2A7DDC6F + 8C6284470B811DF89EED892DA40FBD67F90FCE3582BF0E83739E05465F5CB4B1 + 0E0B9A6971F3A2C35EDF4EF1CC75B3A5A81BD2031F5C6EF69FBED11CFEF46E73 + E8CC8D66F2DD8BBE153475D31F5C6AE681B744C2BC6FED3FB8E0E077F9CCB566 + 01581BB6634C913909E2B68BBA794137DAE2C26FE98716B24F120BBF09E83AC8 + 75987BA7F1E7FA9EB9D9E913F3C1D7D302FE662D8428DF087E253D07D8150070 + 04BD29CA3745F7CA0E1C669B68F9008081BB44FA027A81BFA5A85BB9F4386DEB + 0CC465DB664B1DFA66408B046BDBBA02F6026D958FB66817D0251500563F51B4 + 912F5B2B4739B4350170A56803F022F2110047602C5FCF3B704A128CAA096885 + C8476824304CB4E3EA2D13D1B50F7F6C8E3DFF6373FE8FFFA2B9FE37FFAEF9F0 + 9BBF6E56EF7FD3ACB0F52DCFFF7DC0F35E2DBD67655302C0440927B8E8D01060 + 481E70068033397804DA0580C716CF1EB5FBC48E6972080C2CCCFD39D4350111 + 0589BB6301263B50CC79A4F37965F5C0C10439052F633E424D20A8406B6CA73A + D2D897A476A482BEBE6573CEC76D991D3A9B7C9BE86CA35F3F060098C8DA8766 + CBD78A485B6DF54CFFA568A73E4875DD552F1BA9FB3F4D625B69CECB65E7E3B6 + BCCD34656D610DA4267FEC19EF47EAEF98F99EF8E2677F7FF41900F2027F890A + E64F0144041F7FF63B2F4B0EE7797F78DFE81B90FBF8956F037DECC9B7CDF1C7 + 6CD56DF9473C8B977336712E765EA49B007EBB3600645D5B2032AF2F0160DEEF + 0E00F37916E0D5E7CFB7C0361DB77340E481C31E7D6DD71A5F07A8F69D88DF15 + 44DD71AE859D9FBF3F5646AD8F47FCDA39DC66CB6540AB7DD7E987B66607EA6E + 02BB81B739DAF9183613E523771318A69EEDB081C53C43986DA3799EF07A8EFA + 05FC0A126FF2DDB9DBDFB23A8A087CAFE33C6EDB713C9AD9EAEC352A3237C1DE + A4F5DB405C006E2760F1EA4D2287F9438AFD065EA39DFD36F9B386B3BD85C282 + C639B23883DFA8180DAC67010380C94B02BE11002BAF3602B9CA7BB42FC036DB + D54EA2CE95FD3CAAD804FC9D3D4BC4EE4D87B644012F5F4EDB4147181CCB3142 + B80F85B101846FD93DC86C44FD66F09BB67446E4B125BB20F002F018089CEBE7 + BC1E809C20B06CBE05B4DDD315C58BBAFB7BA7380640D8047F1D005BDEC70F59 + 0B9F8E4260F2487601E06E8C92C62B8C89CAA860CA6D247006BD9240B02282B7 + 038105780582A322F86DFD18D7D978CF5394C7853EE6AB8C0F3DCD3A68E34CFD + E1303DB22447F806698C1AF30E8A79C6AFA5E959BF562E003063EA03C746C7D2 + 71DCEC51C2798CEE7FD404AE4A3CBBD76C535ED7978FDD0B088C1C0007E08BCA + 6DA1356F68E70A394FEAC7CFB057769591E61DE4057563BD14A16F4D25042EE5 + 3018280C083695DB3F2B3298F27600307614E7A2A81F018CAFCD41F333801DD0 + 2EF6B77B16DC95862283D53E4ACFFF8D12C075E81AE0AF602F29F017085C0260 + 493037AD556C0ED647C01B6D51AA4F0018508B12AC45F199C0887C7A8C5652F4 + 4502B9657D9917DCADD946B4832DA04BC535A29AE4F73AEB5CB5B6358DF82A02 + 384A11C0F959C0DB5658336CD716AD1CF32A6B0D53EB98AA1B92FA1852ADCDDB + D6D0F37E77AAB8265FB3C567040F29F657EDE3EFA96A1CA55404BF55F82B3E24 + D57C76A2A17EB6EABF5767AF4FB6A8C0DAB6A59ABF6CD11E6D6F42FB661AF8DF + A0C4246BC7DED539E233CA415F4B82A8BF86FE9400702F1C9B373E7F8879933F + FCF862F3CFFFE57F68FECB7FFBDF371FD9A460CF5EF3D9C387431FF29AF425D8 + 5AFA928F7C516ACAFE651F5515ED4A8DEB47E7B453D57EE05E4F0990EE54BA01 + D6EA4A7537CBDDDDBC6B7DEE54B13F8E5FC2D458FF26553B9752F8C581D1568A + BE4ACBE3260D5C7B53097BF1E9D9CA7A3B8EF2F53A1B449AD802BAA6DE00F435 + C460360EA0DFA4E2E03C1E2FD66F0F00239B5C004C0F1D6BA64CFB0FD9442703 + E0FD1ED16BBE87CC8FEDA1A76D02356313AE995336314A5B240347B14D4E9BAC + BD40EB3845808B6A3E4302EC2AC257EA6DF71CF2AD742CCBA7C9DE78002CB05B + C25F26C5A4AA970F29D056136BFC54A7E705A33EE84D92ADB3EB1FD8D475E51E + F4CD36C0700B77B322EC2D01F0905DC037E64B00BC5D09FCEE677125E76B7E40 + DC2109022B8D8A9058F2859DBC8853AA5DD4C96A179D8E9EF1452316A8047FA7 + CCBE7FF3A366FF714B8FA5C5AA21E1E70B51C0E2603F78FC8CCBC1AFC3DFB448 + 16A5C53349511631AF053987BDB91D7945EFF616F6ACCC96CE71814F511ED8F0 + A7BE05C148BE2C24BE67697EFEAF835F7F1E701779B2FF1D3B672285F362678A + 02BE98202D11B81F5D6E3E7EFC7DF3E0AFFFCBE6D3577FDDCC9C7BD01CFCF846 + 73F0F4F5E6E08744E912250C086611D5D20F5824BDD8CC11156CEDE758546D17 + 565978BDE67DB378EB0BB561515700382EF6AA4E79E48BBEA1DDDC272C027720 + 97FC12794BE702F805FA0286B13B20B654ED96CEDE752D9E4B51BE31D25765D9 + 1200B6BAB00574D422CA005812005694F06278E6AF40B0F21291BFDAD619E0BB + 00040E9057305620173B12AC2D41AF6CD14E7B7F462E4016F86A65FC8660B28E + 27084CBB8DDB2F9BA3BE9D6C92E06E82B900DF047D230016EC950D7FB5C5EEE9 + ED14E957426081DD522D2032BF088FD892F9C85D002AD0EDE7E6D8B3DF371F7F + FF8F3CAAF39D177FD12CDF79E50078E55E02C16CC57CE4BEA27FBF6D046081AF + 828EBECDF0BD57FE9CDB4DE0DDC32EBA748D2D72336CA53D1017C02B08ECE097 + FE2C051E02124F7EF1FBE6C4B39FBDDE238549CD5F101825389CFA15606D2168 + C80B92922AD236FA08A6AA9C606302AACAF77C8095A59E9A421DB0B53C9ECB7E + 3BD0F12766CFFD21F99D78FA6373F2B9BDFE20C1E10889C923F23A4FEFE799F5 + F3DCFC4C02BEF19C481DFE5A7AEC991DDFCE1B50EBE7FBDCAEB1A50E8BADEEB8 + 1D1B189FA27A532A002C3B91C10906B3ED7384BFE13A3C053E13E5FC9DBD3EAE + 2320D852DAD9E7C53F27404ECE837EE933F7D14264E4EF35A97DAF42043022DF + 025BCAF9B30868F73A13CF37F6E86BFBFCAF11496FDF09F737A5ED9CADCCF389 + 1F7F9D0030C039F7EDD097EF957D96D331CC6665AF0730DBB180CDC7AC0E60EB + CF06BE9B227DFD59C7F69ABD3F8F10B6EFB3D52107C0214F0A0CA62D8018989C + 0070F77D46B14C9ED70294268219F96F870360FBAD7320DB45FE26109CA27879 + B66F02BB005D229081BEFC06F15BD4FD46F956F23D084C19C89BA27D4BA5BE73 + 9E34435F406E84BB11002B8F5D3EA53D0260D9A39FEC8A06E699BF3C4BB88BF2 + 25FAF74EB37C3945FE4A1100C7670477F03729F9DA7D2E6C13ED11C2E7B917DE + 72804BBAC0738433D84D1058E28F54D4DF345FAB03D45A5B247FDAFA36D09FD9 + 7D1F880CFCB57B3AF777E0AE4703D336DCF3251F0B64004C4A84B0DB69EBE384 + 3E0016FC8DF2F10911C01F5F6A66CE5CB67188E53FB2F1D0E9D1E7024B11020B + E60A004BDB01C002BD1102AB1CA53AA4769EB73A1F27DA984FE34945096B1CE8 + 36C68D36FE14F43D90C791C86D59F8C6B1E9417C721DF94300E00C7F05800F02 + 80511E1F7B3E8C9BE3D899BCC6D753474F3793368E9FDCB0B13F2965B7DB789D + F17786BF7E2C1BD30BFC628B005851C25284C0CC1D344FF0B9429E37904600AC + 3906C05665499037DAD54E8AC0B7A61AF88D6A7D81BCABCCB552BE04C05EEF20 + F8DD66CAE6791DD04D73D4710038F9A479E61422DA78C9E6AF8B49FB966C9E8A + 32D48D90178DDB12BA067C4B09D20276A380BD92E0EF38002C705B53097BE51F + 6D655DDACE79AD99984DB00B8B310000FFF44944415450363E0358E5A9F94DCF + 0365595B915DBE11E26AFD45F658561F48E5E833227CC3FACF4E14D7886A8ABE + 717DAB5C731AA7B2DD90467C15A8811CFE62DF01008E6B7FC11ED718DBB546F3 + D5FA236B959307573C957D9CCABE4AD5DABC6DBD29008CB4265F965B9510B828 + C7BE06FBF87BAA1A4BE9092653309AAD44BB9A7D5B1AE2466378D2A8F2F1D546 + 6A395B86A24370B4B40FF9D66C6F4A70C721B53E95638F3BC7A173FD7304705F + 9DDFDB8B0076106C6FD0DE7DB3CD143704FB019D99DB6C3EFEE45A73DE263B8B + CBA7FC43BCD73EDCFB78F66F0FFA4AE51761BCF425F7325F10D5C52F8D94FD5B + 9F712ADA6D573A9F711AF2ADFDB8BD9EFA203296C74937C05A5D29F9A2DDDCB8 + 6B7DEE54B1BF7220B19B73DAAE6AE752D376CE239E6FA99A7F52D7B7FB995A80 + 8B284B2A177500DEAD00B043DE0100EC03CD38007D0D9583E021EDA65D39388F + EDB0572380DB8907F09754138812006F5A6A9399C31BCDFE83473C2278C29F11 + 6C13A1699B6CCDDA046FFA944DA4DE6DF6CED96FE0CC49B3DBA4F1F0C9666ADA + 267A876D8235F06C5F07B03995785E6F2CA3E81F6DB53CD2737F631E4D853CF2 + B63E01EC0030020247102C602BB82B78AB0972B46972BDD726A2A94D1F1C5317 + 21EF38A5FE13DC8DF057AA016081DF1200D75482DD58261DF253BDA759B12EAA + 05C0391D02C002B9511102FB31CC16FFA91F7D7BF0370360E4E0F7485ABC894A + D1BA79A1A8D0FEACA963A60C7401C088B2E78F9B0F0A755E1FEB58BC2A745010 + 9867A29DA803DF76E1EC641F14C70536D5FB76CE2616F0E4E78017A85B5990C3 + 469D6FBD6CD23378FDD9C056C702E181F73E7725F09B160FA33C7A85C54CDF02 + DA8E45242F20F663E0EF25B39F6F9EFEC37FDDFCC7FFFBFFABF9C37FFB7F6C56 + AF7DD91CF8F85633FDC99DE6D0999BCDCC27B7CCEFBAF95D6A0E6778ACE702F3 + 9C3DCF136163FDD1AFB666D4E2ADC4C26EBBBDB3E5B723FA48DB465F7778BB7C + CEC66E9FDDB6FE6EF956CE1EC9FBD92D138BD319E04A191697F057C0B78C0056 + 54B02070521F002BF2374600937AB4AF29D62D65E0BB9C237C57AF3C759147D8 + F5DC5E002CF0779E7200BB82B282B528425AF9294F3F92DA025B0129005787B4 + E6EBCFF1C58FF6D6570B642DC5A7DDE6D9D442E03B6CEFFACAA30F79AEE8DACD + 97A61719AA24B0A2761E4998859D9473A00F1D83728AEA7BE122EFA02B1F2345 + 1FA64847C014CF05F636BC9620B68076A0657E6CF3BC768FA8DBEF1B9EF3CA56 + BE00BDA5DBD6FE01DB317FDBE681BEAE4782AF09000B3CFAF363AD5F001B51AE + 829282C0E4B109206B2B685287861900D33F80F14400C00940A6ED8569EB4038 + C360FA151C05860AA60A8C9252264F9D7C4847E4D1A776FDEC9CE54FEAFDE436 + 5E7E0A384DA07403B89ACB11B426F0893A004C7F3A3636D991C3DD0C754F7DF1 + 7382BFCF7E6A4E3DFFB9F515F49580C8EA13797F26CE071DB17339C239723E44 + F79A64273DF6DCAEA1D95456BD003111C0005F3E17BC27BC4FBC0FD8DE79F187 + E6DD977F69EFC94FBE6DB78030EF1F9F25F773104D9F764D1F7FDD6C12F5FB84 + EB69299F13FBAC6DF2E70207BBF4C17BF0A3F9E5A8703E6F762E0E814D1BD6C6 + DF1F402A12F075E89AF202B58051CA8AB446E489BE268F0FBEAA3B72DFBE1FC0 + 592273AD6F602DF0F5A847EEDAB1B2D2778763A7CF0820FB389F597B2D006D5E + CF060096E85D6032AF35EB28DB5DB335B4C92380BD9CA281D3F1ECDC33044E40 + F8CB7C0E44FFBE6C566FDA6F44FEE307E71F21B0FFC1C37C047F796E707A7EB0 + FD5E79B4AEFDA6E62D9BB5F5B300B06FF97C9DDF3EB6A8B67EFCB78D08677E03 + BB5D0980C409FE2650DC01E1B82D74523A6692DB80C0D7F9ED4ED1BD4800B7DC + 1A1A88ABBC7CE41FE1AFD29AA80300F34C619EFB0B045EB7E320A0B000AFA06E + 2940718AF84D3EF213204E11C5B77254F1ADE477C1FC4833CCF568DE1125C80B + 0CF6ED9E4D005F415FA4B6DA12DADB00816D5C00D85D3A7BBB590624675B1C03 + 50C6EEF0378F096A005891BD3579742F50F7231BAF007F4DE40F9DB671912946 + FF027FC90B021FFE208160C64F31F2571A196B15525D84BBB15C6AA41E386C63 + BDF64F7F8C1B7339DA047C81BDFEE7C16C5724F0FE3CAE3C94EB621AC7B1FEE7 + C33CB676107C8CBA341EDE7F22A7C5381AC9CE983B0AE83BE1E037A513805F1F + AB27081CC7F202BFDB05C05E6773831200B7F38602E446604B39C2DE68937F54 + 6C17CB520DFAA2B825B4FB027ECDEE02F806001CB782C63E816DB183BBD27800 + ACB96602C540DD7D405C60AEE52797CDC752C06E097CA33D0A1B10780804AB4E + 907622C05F1481EFA4F90DC1DFAD24A01B81EF90AD0F8081B369AB67802C5057 + 6B2811E84A02B743F057F5526C2BDFD236A869F3CFEB3FDB555C1B8A2AEB9597 + 7D372AD7B8B625806F0B7F635D07807DADCFB594A475BF2D14D719CBC014F2C0 + DF9D006014FB8CAAF9FE1A7A5D005C5B93DFAA7E48B1DD6EDAFFE7AE92A54445 + 3EB31DEDB65DAB216EB4239E948FAF365285BB6D5B11A48EB3BD4DE9D8F13CCA + F3515DF0197D06F09F01704F9DDF2F0B80F792F6DEB0E9669FBD395307975CFA + F7C5C414F9F4A14E5F30FB01EB815FA9FC22448DD6EFF4CBBA6DFF6D7E51D5DF + B87E4B9F21D57ED85E4F0940EE54BA01D66CB5BA219FEDA8D64F54ADCD4EF44B + 0F28C69DE3D06B903DD69583A052657BB5F10117F5A616DE4AD8A4B26CEA815F + A9DA077E1D00EEE5A71858C6C1E6EE541B040FA9D61ED57CA5AD06E4E5E07DDF + A1F566CF613B964F40CCA77D06709A40EC658BE443C79AC9C39BCD94E501C093 + 39A59C22686DB23573D22645A7ACDF53D697A5B3EF5AF91DD7DEC336C133EDB5 + 7E00C07BA76DF2C4042A681F9329202C132A974DF49898B5E5247CFA7EFD7259 + 578A086152FA9600C07EBC793B4FE02D10B7028223004E4077380A98FC14CF22 + 5AB476EDE4D6FC9968725C9BF4EE5F66126D6D98B09A48F7DA39787FB9ECE2BC + C644FD46F0BBCF26E6AE0C6C87E06F097501AF44E62A3A3782DEE81BFD55D7FA + 6DA116D2C6BCA9E7475FD98E6A80577D45C82BBF644BF6B8902300EC0B3C2CE4 + 64C5A880DE62511B95F04973E0445AC0F285A51C01ACC5282D4E0173E3E21579 + D94BF5EB32ECCDA9F22E3FB6D94E66F07BEAF3D68FBCDBC8E73A5F9443F23761 + 3BAC7C2EFB31AC6F402F00D821B0692E43E0E9777214F07B679B03EF27084C24 + B04701170B902C7CB2A049143012A44DCF0DBEE011C2D77FFCEBE6EFFE77FF73 + F3E5BFFC8FCDECB947CDDE77AF34931F5E6F0E7E7CB399FEE476C333750F9F4E + D1BB091E133D9CA28213004E91BF2DFCCDA07768F156792DF2C6BAD66E6DC90B + 187BE4EF59A26C6F591D0BCA1DC015D0453CDB572017E0BB94B77276F86BB6E8 + DB81DE7E1FDEF63CC0B883BF48A05772007CF6813FEB17A0EB11BDFE3CDF3EFC + 5DBBFACCB572E949B3E4E0F771B360F582BAF3171F3AFC454059015EC15CF991 + 179445E4A32204F63C7DE15B805DEC02B5C09116AC0A12737CC0ADC901700B62 + 80B8A45F9BEF57CD1ADB2E03823D7DD12C730C60B01D6FED7606CF66D771CA08 + 60228BB1092E037BFDB9A7F7BE6E36EF7EDD9C78F89D6F738B8D7A3F87DB5FB5 + E7EBE765E743B422F06AD5DAAD00A7EF7D63F96EDBE704F0D8CE9708CFEF9200 + 728272A6A319F2027E05778FB93A20898D3A001F796CF4013004E20212057675 + 1CF2C0C65EA4A9F990F7085320B1F9090C034F4F024CBF2492993E7F6C4E7CF1 + 5302B444945A8A4D40D6E12C30D6CB09986E026D4D9E373B5B26AB9D7C88AC3D + F13CF58B00A4EB19AE6E589D6F9F9CEDF201A8FAF900422DEF11C2A616F8E6E3 + D1FF493B67FAF0AD97CD5F6D782D1ECD0B28363FCE015F5E2F3E7E1CEAB2EFA6 + F9705E68CD5E3F02F06EE26BE25CBDDE6CC05FB7591FF8F86BC979C0F131A28F + 9FFFCEFAE73D4AEF8160303AF9C51F5CD4F15EE0878E3D4DD058EFC39147AF5C + 4781C08FF9AC008381B1767DD8469AEB6BE7E1EFF91380777AA6307F08D0F574 + D89E3F67F49120B09501C27C9EF99CDDB7EFD9BD5726FBDE10A96B3607F78FED + 7DC97E6CB7AD329F5DEAF519A6CF7507C11902036DB1B175F37D3B7FF3E1BC8F + 3CB0D7C3679A6BC467D05E0751CB1BF93BC276E91B762E6C75CD96D702BDC0E5 + 047F5F3810A64E70987C04C0A4B4C34654F2BA7D8F57D88ED924001C6137F017 + E8ECC7BA2BF8FB657314886CC7DBBCCFB6D2805BC160D204755778EE6FFE7DE3 + 4F2AE9372769FD167F5C493B13F83387F3338CD9A25ACF34569F11FAF6E0AF69 + 05DDB0DFFAAB09F8AE11790C98B5FC11CE23436045FEAA2CC82B184C593EE4A5 + 087E95028B81BD006024004C1E3B40B784C01DEC4D36D5295DBD6AC7BF722F03 + E09B2EF2B10F8F0676C8DB9700AFC02F8097B2A28565978F0030750E892D0500 + 2F9FBBE312002E257B1C1F781DE3843361AB67A27D4BF01B61EE473616220238 + C8A382B3BFA27FD5A615EDCC4624F17480C0250C566470694702BBA55D751100 + 939611C287A823B5F1A18F250B002C693CAA3A45046BCCA9B1A6E7E9CB52C69B + C8F3268DA5DBF1751E0F0B007B3E8FA3E3585AC01745082C8D006013C720CA18 + 4500ACFC7EE608D824FE201A1500B0A06F4D405B80AC006E4DF22BDB95B017A9 + 8DCA25F845B2B7CF01CE20B8E7473980DF2881E17D6CE50CDCF5796517019C60 + 2FF3CA007FDD2F41E2FD5953D6BE1550D9EAF7D9BC57E057D0772AC36149E5E8 + D7C2DE8A2632F01DA7087F5B406B79249F12F6BEAE046A15D13B04804BC02B5F + 41DE58AF36B20F29FAA9DF5ED9D2720DA8545C33D2FA9082045ABFB0F6D4B357 + 14EBCBB5ABD752DCEED955FAB046575109802B6B84525C7F64CD31825EF2BB01 + C07F1262ADB5B2B6BE5B188BAFC0F250BDE74304B08E514AF5FFB9A9F6FAB6F3 + 9A6B4C05D598CC9076DBAE27C1DAEDDAABCAC7571BA9E56CE30520EDEFCCDB07 + A823F668FB3534EE3C62DDA0CF9F01704F9DDFDB8900D6070E4D1EB01FB9A9B9 + 66DF84E5ED8BC9FEEBFEEC5FFB524DDACD641FF08B0F38DB408F287ED84DFA12 + C42F45D04EBFB055BF78CCB26E8CE2B147FA0D7D957E52F4078ED76E34AF277E + 30FB8A37B7D23654962DC2C858B787D782B2DF4ED4EBA7A25A9B525BF9EFA4AF + 5F42BA6EBA76B5FA982F35610335E5633B81DF0EE4DA6B35FB88BC2E0FE4F0F5 + 7FFFE57F0362AFB591B876A4FB979B7D6C6D6C1A82C15EEE0D3CB7AF3850DE4A + B5F652CD7F9CC6F5E1009867C54C9BFD305ACF130C6C36A13864931CD3E4E104 + 7FD9CA796A7AB3999A39D64CCED8E46ED62688F336099D7FBFD93BF741D2FC87 + 965A79F63DEBE354B3E7D0093BD631EB7BC3FF3D5B2A3D7FB79B98F944CD042C + 6D65764DE6F6CE999F49654DEAF698CDD5F69BFA6BCBB2F1CFE2DCCE41F31C93 + C30479230016046E2381B38620705B37DB9FCCFA4477A9B331D91D9C0033516D + 6DF8D37707804BF83BB9C0B527D2D8649374C9C16D96406D84B8310FF43D0048 + B514401BEBA2AF6C2500EE41DC01B50037A76D39F8C467FE2A150016E0C59E40 + 6F82BB02BDBD459BFC4F7ED5B1D842FEA02FFE983D2C0291027D7D91E8D89966 + 3FDB3D93E6052A2D5EA10327D242140B534A599C22925770372E5A614B5B3D27 + 1D3E453F2AE788DCBC8036A2BCF8A64538455BE0DF2ED4991CE2E66D9CDBED9F + 4F4AA92F6C80DDB81DB4B67D76994DD1C05ECF6220C097E7FE9AA63F1C5D5C6C + 172359FC34A585CD2BCDC1F72F3407DE635BE8B448BA7AE16E738C45F2EB2F9A + 839FDC6D0E7E7CCBD23BCDCC67F79AC31FDF6C0E7D78B53974FA6AB3F0D92DDF + AE91C89BB4F87ACDDA5FF1285DF2A58DC5DA21001C854DD1C1E4E3A26F92B5B7 + 63CF7E76DBCE8945E7DB0DDB342F5F7CD02C5DB8EF796CD39FDC70CDE09F412E + 7057CFF4A52CD09B206FB2234160B559BA806D3402B804C008D8EBBAF438C9F2 + 6CF9AC6D9F3DEAF7D2132B3F6AE62F3C48C0D7D4C2DF0C8089F455E46EDC9A59 + 20565B38CB877C94203072288CCD14A16F0B771D8E74DB34977502B46CFDEC40 + D674E436E0E65573E40EFABAD9B8FB4DB371EF9BE62890D5D22300D7DB5F35AB + 0E6FBE4E60F6DEAB46CFFA05EE5026A277CDFA17E8218A179B4700039B00BD56 + A6FD49B61726D2D1B479DF8E6775F8AB0D652275815EC0AA15F35977A09B2271 + 5324F0EF2D0F080582FEDCC238DFDAD77DBF6F8EB1853100137098E12E6053E0 + 374ACFA0057ED25E11A302C002BD1BF998B221E5E379A4F3FCDEDB003E1D8666 + 008C1C983E033C7ED382D116A43EB5730F7EC966E79C41237645C05256DAF9DA + 399AED88BDD623440C730D9EDBB99A54AF685ACA1CD7CFCF523FE6538027DB3A + 135DDB9DCF71AB77201B8E85687BEA4582A92ABFF3F2F7CD7BAFFED0BCFBD5EF + DB7E91E7CDA785B841F4AD68DF35BB2E4060D96A00D8E1B6BDC7C7FC3C23F4FD + 7D73EACB3FD8FB92DE333E2BD8789F1C02E7F7F5C4736B67E7C2F523FAD79F01 + FC0CC86EE742D93E130E631D6ADB6BE5F5593B60B2FA7600CCE739F7C336D17C + BE4ED87BEB5B40D387D9DBCFD9C3F46783B5FBF6997F68DF277B8FFC1A5B9BF5 + 07792B72F3E97D4E734A7993D70C847D60DF13C0AC9DEBE613BE27D62E1F9BB6 + 0916DB67D8FF0441D9AE199F4B9347C99B8ED87771E37E02B2FE4C603B3E2097 + F2B147AF12F435B135341200F6ADA3EFBDF0FC31E0B1F9D016B8BD76F7A5EB88 + 7DEF15D14CCA6F07C0D9DBDBB1D681C8B7F84D7A66DFFBE7F6BBF0D27E0FF8BD + 000C037201BFCF5A80CB96F4FCDEE90F30FC96A5DF333B6F7E3BEC3767ED46FE + 838AFD2E6DDC79617D61C70F280CECEDA28A6B8ACF05A60CF40502AFDF7CEA79 + 520161A4B200AF0030222F081CED02BF31BF62F5710B68A277150D4C8A2DC2DB + 5856B46FB4A72DA5ADFF4B76FF3B6FF74553D9D6A132DB3A13119CB7775EB2BC + 227D23EC254F3DF6087D0581491D1E9BD80E9AFB3C10585B3CF7EFFB291A58A9 + FE0CA672FA33DA6507C088686024181C41AE47F77E0C00BED4CC7E72A599FFCC + DA7C66BE9F5A1F569E0506CB2FA40E7EC331D83E5A91C0BE0B4A90C65B3EE60A + E33024005C5304BECAF35CE0291B83C6E703337614F8D518B11D8B32868C79C6 + 98A19EF123E3486F878D316D90FF1131AB1DF73AFC4D63E6385E2615FC45B279 + 94EF86CD01F2B8BB1476B6808E0098FC418E61637BDF369AA8DF30E6D71C00BB + D765F95C8188E00C7E2581DB684382B5AAAF497E5102BC25E48DF69AE4A33E22 + FC2DFDB682C07B1D02DB7C316B2C004639CF16D2BE8D34E52C1E77C44E57CC81 + 2716BAB9AD80AFE6BC312F455FE5A314E92BC82BB5F3F85C1E078165D37A4029 + F9EF44FB6603CC9DDBB43441E1716056FE250016D42D6D43529F928ED58A5DDC + 0E27A8BB134500ECEB4582AE61DD68486A87E27AD356DAD23F1F3F9E8B94DA66 + E05B510C02D95BAEFD65618F1200EED627770F80ABEB9ABF15B1669CD7E93D0D + 6BEB0292DB0193515BB5A9D9C7F9FFE7A8DAEB956DABEB10F98A14398B6B0BCE + 536DB7133EB413DF41E5E3AB2FA9C2DE50E46FA56AFEADC681D7DFAAAAE7CBEB + C8FC73E20DC16041D45F437F6A005802FC4EECB70F2F657BA32626E69A49BE50 + F6E105FC4EF0C34AF42FB0D73FE452FCF0A3DC67CF96FCE297BB54DF7F8CF832 + 292D55FAEE40EDB9E47CB4291FFD7BA2AEF2A3B67BA51B9900A820E8B81B6EF4 + 2DB5A58F9F7FDF16FBFEA5148FAD01451C58FC52E732EE3A0E49E71215EDD1B7 + 7B1D7D30ECFE96A6C85C6483BE363FAABD0C0A55F641A2A53658733160A34FEF + CFFAA5EFB67FE5ED7CCC6FC206931344C6FAF1729F44F6BF01003C240D98C729 + FAC6C1BA54FA0FA9775C445B9F4C58BF33E4994C50B6890011B76CDDCC737E0F + D984E2B04D5EA68F39FC9D9AB5C9D61CDB3CDB846DE1FDE6C847779ACB8FFFB2 + 3977FF77CDE23BD79BBDF31F347BA6DF4911C1E6C316D1B5095492602B93349B + 44F9448F095DFCE72E1339F24CF0EC9844C65A590220EFC92A813251C77B0E1D + E9EAD95ECAFAEB1F9BFEEAE2789336C12D017094EAD01ECEA7DDBE2A45FCC689 + AD40B060706907FC4E2D3351B6E3FA365D4CC413FC553EDA98C03BF8B589BB22 + 7E5B389B55C2E028606EDC96B984BD35FF9D0260015C5482DF0878A558874AE0 + 2B3F815E6DE1E68B36F8AC257FF75D375FF279F18754A20CFC65A1C8178F32F8 + 8D62D10A78CB62D4FE63D646CF02CE0B57874ED6A27B53597904F89D7E07689B + 16D9E2821A6037AA5C68939D725CA863E1CD013065EA88E6C83A68365FE4E3DC + 2D7FC87CB5E59FE0EF483FD82D05004B8AFE8D0B8B6921927A6B93173381BE7A + 0E30A076FE936BCD81F72F347B4E9D6BF6BE7FAD993A7DB399FEF46E73F8B37B + CDA133B79B431F11FD0BA425022781D9451669F302AC4701037B599C05F062FB + F0B2B549D2A26D097965934AF8CB313C6571F8B3DB1EF93B0304FEFC763307A4 + 3D7FCFE16F04C08AFE551AE1AF20AF40AFEA80C82B971E7A3F65DDE279EBFF5C + FF59BF40E0E5F38F5CDAEE99485F40AFA0AFB4109E03BC6C5AB0FCDC05B6FDCC + 91BF3905F646E01BA378057A15F58BE403E4954DF05730D8ED0063FCA9CB8057 + 404451B843001838B279E7ABE65886BF44DF6E0279EF7CDDACDE02C00277BF6B + 36D87219207CD7FCD85A99E8DB5B2FDDB6F900A0F375B37EF72B07C0005F8FF2 + 23A2916D5ECDBE7CCBCE8F68E1BC0D2DDB2B9347443DFAD6B4402EC016918D56 + F67AE09303E1AFBD4FE0D5E6D31F9B234F7E6CD61FFFD01C7DF293E5819FDF3B + A004B00AC6025C5BF06A027A6D9A9F433407BB2992D2C1DA53C02CD0CF8EED75 + 09A0923F02A4B3B6F483D8C6D923457354E95113C773E86BE723F04B7D8283DA + 6238C15F3F3FEB5B7054F09432A0D4C1AA95C9B7109188583B1700ED3180A9A9 + DDFA38CBF3E6EB203447D03AA03439447D0A38B56BF4CCAE4DCE6F3E4FC7DB04 + 387E01F0E59C898005E466206A65C0EF89A744BAF23AB9EE09003BC0469C939D + ABCEEDE48BDF35A75EFE3ED507DB49B39152268A97363A47CA9CA752E5796D80 + 5DC15ED5C7B244F988E928E76FE72CC0CB7BF1CE8B3F7AEAEF93BD1600B0BF7F + 560F00567436EF47D2B7760D789F783F788F529D7F3E9E7DDFBCF3D51F9BD33F + FC4DF3E1777FED7DADDB6713D107C74ED7D1CE85F7C1CE8D6709F3791E05B209 + 046B4B6907C0F6FEC84776FE90C0679608718FE4CDED1CB63A44A56FDA038613 + 8455D42FED755CBE0309F626F82B25286C7922891F98ACBD477EDB31FCF9C00F + BFF2E8E24D9E356CFD1FB7E3F8B1FC38005EC0B19D433E9FF5FBF6FDE7FB6D6D + 91C35E07BE40636B4BCA311EE0F7C2C49F47ECB7EA8EFD76998EDEB3DF112B1F + A19CA12F7F3261CBE791E791DF4CE2F9C1BE8D345BC0F3FB0410E6770B28EC7F + 247961BF8180636032D1BF8FAD0DDB430391EDB7962D9F83047E5516E82DA5C8 + 60015EE523F0258F1FFED18E047E0582B10900A748DD3B19E272FFB0FBCE25BB + 4F99162FD3CEEE3517CDEF82DDCBCCB684ED92E9A2E5CD0EF4151416EC8DE0B7 + A70B779AA50C7E258160E5058405891DF466E01BF30E872D3F63638F5993EEF9 + 88BCC41841C037D67763870465173F633BE8B415342AA3829147049FB91820F0 + A566EE5340B0D55B3A636501E3087F93BA670B7B3FC05F1E85411AC06F097DD1 + 505D1CA7A13202586AA13065CB1F3ED58D2B5D79FCE9CAE3D13866C587743AFB + A88E31F341C6AA9473DA83C0D937EDAE93C6CC9260AE245B19F92BF0DB6F9FC6 + F0131BA79B7D80D73C9E2732185B39EE57DE0130692E7B3DF30ADA6478CBBC22 + C25C89B240ACC0AEFCA27FF4EB01DA6D005F49BEB11DA9E0EFBE6C971C0C131D + 9CA37D81BE7A3670A9D6A79047083307256FDA6B79D4966D0EDAFEF1983F21E7 + B9B0CF5BC3DC163107DEBFF28E6B0800CB4F699B0FE0573057E552AA8FEAD61A + C66B274038F974F097E34C5879C2ECFBF8F3BC89FCA4F9B6B65C964D525D04B8 + 11EE0AF896F0B72C4711013C51AC11ED74BD488AEB4F52AD1F6C718DA9D62EAA + E727B85B00DE21F5FB61BD8FB5BFA52E9FC5BA9E2070BBB658A8BF16595784C2 + F86F47651F359FA8B8663A4EB5B6BB127D4DB0966F9A340508B913D5D7E63B55 + 7D2A11C0CAB73E0392FF906A6D76A481736BEBB6927CDF842A7D8BB748239C65 + 1B8C67A4DD4ED8D06B32A4D43E1F5FC7953267DB36E00D7E55DF5F1B00D78E2D + DBD079657BFF75613BBC3355B8694F82A8BF49ED3B6C1FD2F882BABAEE45F022 + EDC298F666A93C72517F11D91B631FE2F4006E7D198714BE00487D90F72F4457 + 577EB96359B6B12ABE54C0E9A86A9B6D2A9E433CA79ABC0DC793B0953F70BB16 + 3FCCE926E6FD724C2B9737DBF2E659CA617D6E5FABDF8E74ACA858A7F3896DCA + BA717EAE7C7EF894FF42934FB46D476DDFA17DED7C6219956DA2625DF4897949 + FDF58F23F02B08BBB51CCC921F02B41A580A1267ED351BDA676DF71D5C69260F + AF7ABAD7EAF699B8CE0C10C9A3E45FE93FAB3AB0DD86621BF5A1C1BA06D5353B + 924DF535455FEF4BC7F40860AB9B59B34987D5B70098C9C546B3677AD364131D + B3FB33846D62C256CA13FCD317D879807FB4BED3DCF9E61F37FFD3FFEDFFD3FC + 8FFFF3FFBBB9FAE55F3753AB9FD804E9DD66CFDC7B3649B249E4619B98CD3219 + 638296C06A82B45D5980376D29A57F17DB24D024F09B6CFCDB18DBC9D05702B9 + 0E6495F7E35979C6522B03AC531BB663B6E35A9FF277789BFB2AFB56D96D2641 + 5F49E0D76513D50472ED98212D27B7D804895597FC5379FFCA2917DB6AED5F7E + CFD445FD027EDBA860BB4E82BB11DC966A7DB26417CC2D15EB0586CB362A4798 + EB7D873212C4956497AF00AF835B1662D63F6CF6AD5ABD69628D45930F7D91C6 + 61EDE6279EB650173B11BFAEB4B043DEFD29E7FE265980319B16787C9107604C + F42FD1002C1E99B478A5C524443E95D38213F59D8F16A1D4B66B5F8A3A16C0F4 + 9CDD72514D903746FACA2E1F2DCE797B93CAFE3C372239DE4D0B7D5AEC538447 + BBD8475B4B0F5B3D122C3E0CFC7D9FC5C7A4C31F8C2E2AD25E29B6697C3E34DF + 2C6DD98C0E7DC073EF2E9ACDD28FAF37531FDD6A264FDF6C88005EBDF245B37E + EDCB66EEEC8366EEB3BBFE4C5D0030D1B8C8A1ADF521C89B44FE72739885D45C + 472AB82BD08B3C5A98855CCB2B7A98C86207BF2CF84A5EBEE9DB3E037F81BB0E + 814D8AD84511FACA16E16F94FAC17FF1C2FD66F9D24317F979DA9B16CE03841F + 24886B4A6902BB2B971EBB286307F0A21EF0CD50182D22602F3EA419F8BA02E8 + 8D5A05C866A85B460293CA8FB22283058479662F368FF0C53F4B36815F843DDA + 04806503046B9B664500AFDFFEBA59B2CFC6C2653BC71B2F9B35B311F9AB2860 + 00702A7F9D80EFDD14F18B78D62F36C4F6B6C05F24E0E500388329221F11602B + 012A606D07C41C4AD1CF03F301CC3EFBA939F2ECC766EDF1F7CDFA1340205055 + 80F787E668865B8AFE75F0EA75A409BAE93C88E6F488488E095004B001F82C2F + 7897C0DB0F0E0E3748018B9C83E77F6A8EBFF883E9F7CD31E0A2952300162416 + 20F63E389FD03F12FC256A56E05791B180DF353B6F2028905340551055A0F444 + 06AB4769C3795BFFF203FA6E3CB5E3DB791D7DFE7373C4F247791D767E006C6C + 275EFEB13969AF85ADAB1D98DBEB6B5F87A5FDD7F1839581B776AE765C87AFF9 + 3C8E73FEA6F67C32CCD5B9E89C75DE52B4C957FEA51FAF0D91C747C7A49EE866 + A280397FC479F31A80B4A409CE92A67C7A8FECBA3C4A709EF7A383F0C07EEBC3 + F2C0ECE35FD8F1ED337ACCDEE70FBFFF9BE6DC5FFDABE6F3BFF8E7CDFBDFFC95 + BD47DF35AB804DBB9682CB0E7E69CB39DAF902640578D376CE19CCFAE7D03E67 + D69EB26F2F0D04E6F36E76FFCCF2D975A572FAECD206189CA44860A42DA7DBAD + 9FCDD7BF5FF61AD7EFF31DA43E7F5772DEBF0BDE37E765E7C26BE57B6336FAF1 + E3F25DB1BEA8A7BF637CD6BCDE8EEFC749F955227F89B8B53CE23B9EA0B0FD66 + 1051CC793FB47A227C7DEBE81441BCF180DF8E97FE1CE32377EDF7EB367F5401 + EC26A0BB7A83DF337ED7D2EF16CFFF253A1890AB2DA401C1091A7FD91CB5F3F0 + C85FFE8C72CB7E87ECF793A86187BA371F59FFF69B6B5AB9F5A4077D057E1521 + ECFEC05B4FD9AA3F835AB311F94B8A0DB84B59B6087A29475B84C46CFD8CD4C7 + CA55F3CBB017A0BB74D9F2590B395DBAC21F8BD85DE27E33675A00FC526FA9E7 + BD1D5018F87BA759B8D06DFB4C4AD46FDC4E3A41E06C2F60EFD2F9BBCD72AE9B + 8FCF0FF6E70E9BDDFC047ED58632D1C1BE6D3491C39FA73F7B4508CCF8807B3F + BB8E2CD2CED2120C2F7A3BF28C3112A015AC050A03875B306C3ED36778FEAF8D + 854C8060E0EFEC27692B6887C3DAEED9FA9925DA97E7063B0C4E7607C48CA9F8 + 535D86BB82C08CBB340E4B63AF3E2046B2CB6F48D4479F83EFD9B8CEC6871A4B + 7A446F966C0E6F2D65CCA9B1A98F3F49C3D8D5EB18A7663B3A186CFAA3A2B7F1 + 717117F98B184F0BF2D6A0EF50D9DBDB587DD2C6FF44027BB430909668E00C80 + A9574470BB4B10F3853C6720EDE60776EC007001C035C02B5B0977E557D6C5B2 + 406DB4C7FA716A01EF18F9D6D005E8DD4AF80B063BE0CD8A7995932F7F3A7EA7 + D9637376E4B0D814E1ADFFE13ACF75B145F8ABB96F9CFFC67930E2514A1319EC + 920E2902DF2109DE0ED5ED61CD63663C006E7DDD8F2860D4D9BAC75CA53A3DCE + CA6DF11158964F6DA9932F80781400A7F518ADCBB04EB3B5F62176770BEB4051 + 5A6F8A6B4BB11CD78C76A2D86F4DF22BFBAEF96EA5AEEDF2580902B3BEE8811F + A451611D529037E66987627EBB525FBB69FB56C49AACAFCB6E5FF1D9C1F5B5F9 + 2D04C8CCFC40B65A5F43C790BDA6D2F77515FB6C9906928FC06CB6BFB19D4EAD + CF1E4BD171789DBC5FA1CEEB75FCD7613CBB6DF726D5E36B5B2B42D2044A93AD + F5796B2C704043C7DF31A7C437F2D08A60A243F69A7AC0F537A67D7B39F1312F + 6202850B4844EDDE7C51DFD69B3EF201E68B38A4E007381EEC639B6075405E1F + BFC8E4F38F82FF30C4BA1DAA771EF69A7AE58A46FAA8FDD0ED5AFCE0A79B98C3 + 458E176CBDBA31EA3DABB952BF1D95C78CC725AF9B7F6C23C581C1383FA93698 + 505DB46DA5D867147D2B5FFAD7DA469FB22EFAC4BCA4D7D27F3D00E00EEE6E47 + 0E6501BC9541A2ABD206D1CEB77DB69401A2C3DF3CE8635B6AB67C6180D89E9B + D5957D9783D7AD06B323F5C57953AFC1773908577D14F5C0D9D25E53AFAFBCFD + 33F288DFD93CD9B08903CF05DEC364C32622FCCB95890D7593E6E3CFCD9D01A2 + 9EB06BB3D14C1D3ED93CFCEE6F9BFFE5FFF9FF6BFECFFF8FFF6FF3FC8FFFA699 + 3976D1EA3F68F62E7E60931A9B18F20CE019264909CEFAD6C9590E5AE7136045 + 930BA7F2F385FA5B4D45108B0468D58FF715FA244D7E36E9A40F4B3BBBF5677D + 528E7D532EFB57D96DA6B10098C864499359E0F582D511D9CC56D08B767C9BD8 + EE050E5BBE9D0C038FF1F5086026C9266038D7C204FC9D5A7ACFE52098E85F9B + 7C03611DEC027F1160B5501B1D6CF9120093C688DFAD806F5916D08D50578AD0 + 57C7968FEA88E4150006D2027DF7AE989F0030B01778AB059AB050833D0A9B6F + EFB699E02E623107C5ED9F115B42BB6F80BC48E0575BC945614F8B579FB63E7D + 48DCEF0B3F292E886951CCCB597111CD17D24E25005C6EFFDC2ECA99D8BAD923 + 30046791E5A3B4D0D72EF8C9F6FEF9D6BF5C542445B31FA66D09D54E6A171B3F + BCE051C0291298454D00F06587BFD479FEA36BCDC133B77DEBE7C367EE34679E + FF6573FEDB7FD46C5CFFB299FDF476B3F0D9ED46F097E7F32A0A27C25D2272D0 + CC99047DA33A9F24DF469A7E2C9F167A3BE8AB056089E3CD67E09BB6824EF016 + 1B29AA45FD2A3A186177386C72009C4519F0BB7411C86B7EA604841F59DDFD66 + EEACF59381AE6FE56C5ABDF2B45906DE5E1805C0A4F22175106C52D4AFA06F04 + BF02BDB10CD015D48D368FEC35C937025F89AD9BB1E11FED483058F01709F422 + EC92EAD0D1BB5F39005EBBF5AAD9BCFF7DF3FE177FE13AF9F8672FAFDD79D5AC + DEF9CA9F177A04DD45AF52A42F5BABF29C4FCBFB36CF6CD96CE219A60E7F1E98 + EDF1B7BEC5F19AE5D71FA6C8404122C012CF9845E41D7A21F3775F9347E33EF9 + A1597BF29D0360A026D19E805F45D82AF25765EA885C65FB67B6C64D11BEE83B + EFEF08DBD102D2ACCFA814054A1F56A68FE73F277DF9FB66DD8EBD62ED485354 + 6D8E4476C8DA0160520161E501A800C6F6D9BE4820330343C14D41CD128C0A86 + AA8C00AE257415083EFA1C1BA0D7DA7E01B04DF0F7F8977FF0D703C43EF5D55F + 34A75EFDA59D8FF9D9B58BE054F034BE26DF66D9FAE27802B23A0F9D93CE1DC9 + A6B2DAE8B5443B69F9FA90FCF08911C0D1E76886DD471D5227184BF42F5B409F + B27C3AFFF41AA8C37EF239DB415B5B3E2FD6E771BB4E80780030D1BFBE35B6F5 + ED00D8EB7F6E3EFCFE1F369FFFF19F3767FEF0B77EED78FFD131BB26F447443A + 51C07A4F7DEB71FB9C91FA679CCF98C981309F23FB9C259B95AD5ECF72A6DC45 + FFA6CFADB7E3B55BDD51A27DD99EDA1401304018480BD4D5F72845B4F37DB4CF + AE3F37DBFA06023BFCB563711E1CC324504D4A5920973CFDC94FBE48DF59BE53 + 1EFD9B453BFFAEDFFFCABEEF2F5C40E0B475B4FD96DDFFB2D97CF8320160B321 + 228349FDD9C277CDE651BDE9778A885E8FEAE50F2B77F8DDF9A2D9B86BB2BED8 + 329A3CDB462B72D8A3828908CECF0A660BE8D59B8F9BE59B8FECF7CC7E5FEF3C + 6B566E27E88B14158C1258A6AF04991D189BCF125B2A5F7B68BFCD696B686DFF + DC0260B347D0DB825D13360164E5A396AFE263F78F0C7A5D57E8238972CC0380 + D10800763F3BC615BBF75DEA80AF34B81DF4C55C9761AE03DD73293A18FBDC39 + BB0F5BBA80BF9E3B6C750E704901C639625851C1BE75F4E7B79A25BB8773FF77 + B80B0806EE9E4DC770806CB6087F290380D9CE99885E6DE53C97A37F15154C9E + 14183C673E6CE37CF8631B377D62BE9F325E61FC63632AB3A10E2227083C7D9A + 3FCEA1345EF2B1147FA663FCC54E2A8CA332AC7570CB1FF5C2F88CFCC8B82E4B + EDF4E7BFB24E361FD3998FB68116F86D217018736A4C5A05C039F53F2AE63AC6 + B91265FAF331AFDB181FF777C9110496047A23208E75519336C6070033EE9EDC + F8B0D9C37C6AF99DE6601EA70386FDB836164FBB05E57900F3849CD7BCA29D33 + 98B415B4E0ACA07084C0A411D0CAB7546C13FDB7D238FF12060B2C47FB6480BC + 4312F4DD8EF0279A5811C5D83A380C044E7F764625D4955D12E88D3E2AB7FEBC + 97736F0600FB5A46CE4B65793B7200ECC038E55B7B96CAF1B8A5E42309000BFA + 46B56077A08CB49EA33ACF23D67BC2FA52A9729D69C8B61BE9B8A5A8D331CAE3 + A85EF921753EA3D0374A0058B0B7A6B4E637BAF5737F6D72E7525FAFDBCF2FA7 + 855DABBE2EBF0D557840ADBFA1E3C89E768EECBF86D2F78D29405ECF0FD605FB + 0ED57B0DE5310AD5580B81883C8E74D79CE735F9D0EE94CEBD55E46BDB5004BF + 55BD4D1E58D3D0B177755EF88B89928FE5CC487BF5C15E530DBCFE56B467CF41 + 3B4153FB82C20B696D950BF856DFF0DA97850F74F1A12E15FBA8D567955F72A9 + E62B557DE2177B275F70FAE187C865F9CA0FD158C51F4654F3D9B5F8B14C3731 + C1C51230967523F2EB64F2D756A9DFA6E2F174A31FF229ED3B91FA2F0713AADB + 89CABE6BDA8E7FF419F22B7D24BD96FEEB6140D607B55BA905C001A6F60690AA + 93729B28068182C0FBF2C0CF078487563D6D078FB9FFF2184A7732688E7D49AA + 8B8363F559F3A76EBB00B827FA0C9388F4AF53CB6BF2C084C2C52467A3F5F37F + ACCE9E7000BCF7F0F16672E6DDE6F8670F9BE7BFFF37CD57FFE0DF35A7AFBFB2 + 09F6599B04BDD7EC9DB3C92210972D8B01A4D646CFCE15608D12BCC5770800D7 + DA44E82BED21F2D76CDECEDAFB713384A63FB5A77FF55D4AED75FCF100D8FAB4 + D4C1B297A9537DB62D24BB3F23C904E075D0CBA439B74BCF4FEAFA2155F4AF00 + F01410B590FF1B9BD752DA4D02C0E516D16A83C80376E356D0F844E0AB7AF9B8 + C89B04754BB8AB2D9D2384565D1BF96BA9FBAD7DE0E0B7068091F25AAC914DA2 + 2CF02BEDDFC097059EF4AC5FDFF2D9EC0E828F25D8EBE0D6ECCA47E82BA98E45 + 24C15E445E8B5C1DF84D3616BB4AA96DAC2F21B0CA681C00D622DD745ED4D3A2 + 1F52E4AF16FC1CD886853FF9C6323E71817116A81B16214B3F162E058015F1AB + 2D0E3DB5F281F72F350779DEEF27779A031F5E6F4E3FFD7D73E19BBF698E5C7D + D61CFA18D09BA374C3222C4A409734EA46336375D356377D86A89BABBEDD2336 + 34FBE9755FDC9DB3BE7C1B48167DE9D795802F797C5C1E3DC482F11DDFFED9B7 + 80CE40D7A12E3660AEA502BF283E2338FA9107FA0AF892579974E53210F7B103 + 6024F80BD45D3309EE0AFC52AF885FF7CD3E0914B358DF01E0769BE70C7001BB + 2500F628B40C807D6BE60C7F159D4BEADB449B9F43DE5B297AD7C12E02E05A4A + 5BF21E0D673E6AEB7E01F00AFE46B5FD6530B2798F67F27ED7B0F5F3E6831F9A + 27FFECBF6D7EF85FFF1F9A2B7FF9AF9B8D7BDF35CBD66605007C9FE8C5EF9A0D + A207EFBD6AD6EEBC6C566FDBB1012DF7D906F6DBE6E8836F3C7297E79A7A64ED + 13806B82BF48F95500F0C36F1C769DFCE2A7E6D4974464FED0022F6097C015D1 + B03CC316F8CB36C6C03660A6477C9A80AB403C0779805640A58338002C79B303 + CEB2BC5F3B3F876E19D4F996C85647CA56BF1B00B0A7092A027C1DF2DA314FBE + FC63F3CEABBFF47350F42700303D5BF7770E4781A91E156CFE887C127E097C0A + 6A2A0530AE73AD807C016C22F90B7E0A1053A6AD7C48A95FB36B96EC19FA726D + 5FFEC1A37D8F59FEC40BCB13C1FCE5EF3D05641EB373E7BC4F98EDE497560F38 + 7DF1C7E65D6B032CE57AFAB5B5FE80BD9B5F74D1BF94D9E699F38AE78204A875 + CE92FC6A6017DFB22FD2E8A76BA7E3A4BE00C1005B3B1F5ED7CBBF30FDD15F13 + 51D01B4FCC8FCF86BD067C8E7FF17B7BBF8986B673F2EB955EDB8917F8A4CF0A + 9F0DEA3826C779D7DE7B20F0BBDFFE55B361FD703CAEB32BF77BC2FA24FAD7F5 + 2CFDB9011DE79C3977933E8BFACC29FA387E16FDFB931523948F3EFDAE5907FA + 3E7A6576FBCE3EE5FB920420162476B09C8F417BFD998268669E574C7ADCDE53 + FDF12242DD120047F89B6C6CCD9EFC1DF4DAF73FD65316185EB3EF3AF0771DF0 + EBF0177D69BF155F9812F8650BE814114CA430F5E67F9748DFF47BB5C11F4CD8 + CED9FADAC0EE80F885954D80E407D6D6D2F53BCF4CF63B782B6D0D9DFEA40220 + B6F3A19DD5AFDD7962ED9FDB31EC37D252B426D09B237F2304160876004C14F0 + B5871D30B674D9CA441093C7BE0CEC05FA12F5EBF9544F3B00B297B37D29C3E0 + 048CEDFE728D4704703F492057926D3B005865A0F2E225BB0F06E82B612BB788 + 7608ECBE09E62AA2972DA25104C84A81C2440D6BBBE865B69FB614F88B521470 + BEFFDBBD9FF141BB75B4F52F488C7DD6C60FF39FE49D41182B30AE38C3F6CE09 + E6229EED4B4430103745F2F20C5FA2816FF81866F6632030B0D87C187F587AF8 + 23A280AFB8A89FA32E8B3FC8A53FCEF1A7B9F4873AC4D8492222983FEB75E3AC + 547F98F19CD7D918EEBDB3AD282B92F8A0E50FBCFB79FAA31FF511FA9ABA9D61 + 521A41706F7C9925002C09047B39E75DD461632C1BC6B9D196D4C1DF088123F0 + 8D656904FE1E3DED636AFAF731B4F9CCDBF92EBD6FD7CA52EAD8169AEDA07D9C + 6FE3798F06CEE037CE27926C6E92E1AF00B0C0AD406ECDA63C02CEC632F551D1 + 2F42DB08776B525F6A2B45002C3FCA440247D03B2CFCFABE25F895047F5B006C + F3478F02B679EC3EE6AE8B36AF5E665E9BF211EE46E83B4E1106A7C742597E17 + D0B754095FCBFAD86FAC97BFE4FDF82E69296AB8E6ABB28E594A7EAD3F5B480F + 00E09AB47E234500AC7CAB43AB499EEFEC1E293C6DFDB18694D799487B36CA87 + 72DD18D5D6AC506FCD2AAFC1457F3D7738B6A9B62DD4F9F4816FA91A006E6DB9 + CC9A5F098047D72677A637D5CF2F2BD66777AFFADAFC78399F287842ADAFF258 + 5182BF250096CABE5E4B25C7D80D0BD942ED79EF37EDF03544DED30BF4CBAC47 + 4C486A5F47A99DB0A137A6E27C221BDB86C6C25FF45679604543C7DEF579D126 + B653D9E401B3B13ED85D999F4A35F0FA5B1100782FEA01DF2CB3B13DF4DE495D + 04131F84FC61D8BB0FF1C2B3E4F38B68875F9A18FD8B6A3E8576F2E5DD966F56 + F48D6A7DC8577E70B6ABB63FA9E2F37AE2477254F10657AB6BCBED798D96637B + E5DBB6B14D51DFF31BF051B9F4A9A96CF3A6061343C7799B2A0747FBB0EF6750 + 9620ED968A50571A191C8E0E1CD92ABA0F7FAD9D9D830F040FAE34133630DE9F + 75D006DA8A0476157DE9386868103BA4717D0D6927BE35B5E718260E523B59B0 + 7A87BFB3969F31C90710ECDB1D25903B31FF4E7360F5639B6CDA64FEC8B9E6F0 + E6F96672FD339B789E6EF6CCBD6B3E277DAB68079A0E428380A74054EB27E605 + 69658B30B6147E43A21FB5632B6B6C94892E9E5C3895CAB91F1D6348F823CE7B + 180027016C3B08DC49BEAA77B09B9FE59B9EE74BDAD5A1048EA90B00D8E16D02 + A902B92882DC9E1D996D8F4FD2F99776571F01B06CC05DD5918E05C0E433E415 + F48DB65817A37FB1017DF5EC5EF2F80800934A11EEC67C54F7CFFDA4B8A833B9 + CEC20E65E0EFA7CDE1E39F79CA229020EE10EC9522E42D1517B750B42B1242C2 + D603C4D882E2E29A2400AC72F46F17E758F8CB80362EF269614FC05620571037 + FACB46AABCA24C4A0186912F44168B96DAE6303DE3EE926FD77CF0E3EBCDFE0F + AF3553EF5F6A962F3E6C8E5E7FDECC7D7ECBEC57D2737E7D1BC56B499F4408CC + 96D0E919C1AD3EBBE10BBB873FB9E69AB6FC0CB66C9FFDFC46B7B0CB4230D140 + 525EFC55845152B7B5B3035C606D50CD2608ACB2F7F1F96D87BCDAF699C8DF12 + 002F5F7A607A64F90478DB48DE007BB14551A7FA556CC05F20AD47FBB20568CA + 47D08B048025A0AE47075B5E72584CFD8D14B1DBB365A02BDFE55C46D4014622 + 208E36A4ED9DC9037ED356CF0918BBEDFFCFDE9F75D97174EB7A18802AF47D57 + 2814AA41DF13EC49100489BEEF41102D41823DBF7EEFB3F791F639C7D2B1245B + B264FBD81EBAF28586C7F0A5FE817F5DFA7D66C49B6BAEAC5C85024970F33BD2 + C53BA28F8C956BADCCC87872CEF858E54A4FC73E9DF79BED9FDC6D769C7DD09C + F9E1DF3577FEE57F6ADE7EF69766DB27779ACDB85FC51D7375198B75ED00E4DE + 0AC013B007F873E16EC05D6093E11375B763050834BA3C00C280ADB0B2BC56F6 + BE057065D86570B5F30AD231AF14B7C6404DC01B0016388B55EA6CB5F22CAE8A + D1A366E6CAE3106E8BA7AF52AF1CCBFD1237A86BC11B808D71E87858F8027E0D + 7A81B8C79FFEA139F9C3BF6DDEFBE61F9B7DF75E443900310020A0F1FAE37055 + BD13AB5B8DD3632DEA40CB1AE2BE79705C8D4DA1EB3ADF65A403C02AC43DB45D + 44531E7DD57A00D4595C54DF505D69E6F617CDDCDD2F9B29D2B870D6587101CD + B81823E3270FC06D400CF8DD77FBCB66EFADE7E5DCEA7B9FC63A18D87B53E93A + 7E0360835EC35BC2D8FFB753869CF6982DF7E772F25CDFE7C37D5BD3D7C91F9C + EF62E55CE036E1E03B28657CAFFEDCE10E3BDA16F01CC7BEA5CF75A31C6796E3 + D5730A20062803CC39A7134052F5C17180BFF4C36F604EF55A97DEFA2DCC291F + CB5EFFEE50FC37F49BA44EFE1DFA778FBA00983CCA896FC74A96FD79AFDC579E + FE2F75BF62AC98635F66CE25E7AEF61F9FA1F6D58E83FE78E9827885B72D0066 + 0C1C13CBE32ACAED9A3A2C7C252CFCF96F63E93B64C15F558030D70E5D7FB0F4 + 95227EBE405B2C800BECBD9100B0FAFB54D735DC3CF382096E9FB1065679B76E + 01C8D75B61D56BA84B1D80F30496C1674A9FE4EF3C8700D0051E2343605BFA86 + CB67A905BDC92574B88BAE8098BC2D15F01247C4DDDEE03742A95BE6F2A8F361 + 01C08406BF1650D7B0D710B80B7C5DCFE9B26F30FB04EBDE2A61BD8B8863BDBB + EDC3F3B1C770B6022EAE9D01BA4500DD90EA008BA99BE1EFFA37759FE77E4E7D + B507FE724FB70BE8980F300F88B9C0603E902D84A39EC27017CDCB61C062CD3D + C2BBC831F6F2FDB0D974E2A3564060AC7D5B007CF4C3B01A8E17D6925712CF61 + B2979275876BFC88CA238DC714FA02066B2E54AD8143BC44575FA4637E6500EC + 347017B92CE661B5CCE52B352F5C59EBADDAAF795E9DEBADDE3B3C3FB4021653 + 4773CA2100ACB966CC5F2DE6A173292D75E7A6D4E9CE7D99F30EE6D2C3B07755 + 6890D742E19A97AD7F57ECD23C9F797A1510987ACC9397ED3CD4ECD07778E1D1 + F7CD99CFBE6E361D7CAF19571D00F0CA5D0512B33730793C1FC4F3477D6E183C + 53F403608357C79D1E95D7CD47B92F441D80ED622070EEB79B97EB39DF00D870 + 38007102BC16CF7A58EE86F56E0F081ED42B00B89BE7BD818B1BE8E2E699BD7E + BBB0B70F083BBF5BB70F007721B001EBAB08D00A7CED2B430BF59B616D0B7213 + 0046B91E71FA1BAA9FE47A6DFD9F0980BB71E4B51CE75B5ECB092F6F92EBBB0F + 1B00E4BC925F6170CF9A92E5B5A5768DA9E6E7352EAFD38D6A97F3D150DB8E06 + 7506B0B74F430018B1DEE83CAFFB2D20D6267F8E7EAD7E5EAF58FBFD75D5BF56 + BFB0FADA75FBCD5A08FE5ADDFE7E966C00D7E125BD75B3DCAE93DF37B6D87F19 + F06BAD24D4678BB5F9E1F6BD623C66364B7F0100FE5751673C998DFD1A7AEDFC + EF251A75FCD7C1265F76AC160457F581D7DF8B7E31005EEA0F3DE2A4FC9ACA3F + E097695EFBF2276D55FFB4DD3FC64BFFBC3D7FF66E9D5CB618F55E6C7E57E202 + C905B5DCCC7E29CCEC03C0966FE0718C1175B216EAA31B7F15E5C9448CE525E3 + 789DCA63598CBAF5BB6D170D80631299268386BFA867829895CB739F79421856 + C00A791B709526BF80E0B63CB5EFEBFFD7D2EB3A4E3BD126EC4CF207005802FC + AED3F1D7EAFCAC2554FB75AAB7410F4FEBF520B6818731AC58F72B6FBFEA027C + F729DCA3500F821BF634CB37EE6EC67920DAC0C35081AD06A286AB6D7E15E036 + 2B43DD6EDD5C96E572FA270D001EDF545C4BAFDAA6F1D63AA4A39F5A378BFA39 + 8E32CCCD00B8C0DAFAB96897D25DB9BC5B27E75B40E100C3C058207040D961AB + 5DAB9B6EF3930200D31F207601B92DF1850070C413E4ED03C0C05D5BF8E6722C + 7F0D80C3FDB3F232F0451902B330D385BF19F666E85B1670CA824E01C047C3EA + 77F5F41BCD9A9913CD2A852BE659FCAA5E84EA27169DE6CB70D72A0B61F3E5B2 + BC009617C242C4EB421B1A5A3C4BEA03BF160B732CD0D14F587F24A0CBE29E17 + F80C7533E845CECBF97D79B608B680BF1B0E17C85B162C59B82C8B972C8ADA42 + 26F6B7537CCDE10F9B5507B18239D9AC3EF47EB3E2C0BBCD6A5C219EC0F2A6B8 + 4AC46D622CB286054E51B8853E362C166CD7027E252FF0B2989B177409C3CAA7 + 037B739EAD8CECEAB90B80FBC0AF654B608B3CEA67ABDF0C7E81C140615B0067 + C00BD8B5CB67BB7AB695AF01B0C170E44919FA66C8DB85C06DF947D786003071 + 8B34E0D6201739ED3CDA77A1AF416EAE1B60B7C2DEA933775A00EC7C2BCAB1F8 + 3D7BB79906EC9EBDD74C9CB9D76CFFF47E3377F559B3FBDAB366FAD2A366D7C5 + 87A1C9739F35EC1B1A6E822F3F6C70498B7B5980AF2D04C3F22F542C800DAC80 + C1B6FE053601A1C2B2B7A60DC00CA99C677005240EB079191057AC72272FE38E + 59FD02AEAEAA3D96AD4058C54358E586E56F51580403E242A5DFEE31424AC7B1 + 34F662D90B6404963E8A3D74F7DDFFBA79FBC55FA5BF35FB3FFBBA09D7CA1C1B + 0BDB9BCF02AE0205C9B7656840C6008EA57FC062E8460DA501CC2C309231B82E + A1E386A36E97CBDDC675A23F85018035BEDDF7BE6A76DD2C901A383C77E779B5 + 0A561E752AF83534C572168BD77D775F449C7D83233FFA2F639DAEE3F6D81D47 + 80DF3D7700A6F45BC66465289C3F076186C6E4B92F445F28D7715BC656CEF7D3 + 08F91C250DE8E5185829973C97873BEC880FC63D7353C79462AC697C9C9BB9DB + CFE35C4EE91CEE00FE475BA5B13056BA9C37F5C1F7A1DF3130352C80D5DEBF35 + 8B34F9C5EDB4DA711CBE43E06BFDEFE434A20D6101B9BC2841C8FF084BE0F2E2 + 44EED3C7212CFF3DEA0282F9FFDD8D7880688EA5FF498C298D93B8F3B20B6AAC + F7F99FC74B20528C87FEF93F56488C624FE1BA4F71D9B358752FDE5278AB99BE + 205D04E202620BD09DD6356117D79200B658ECEADA858781B01A2EF0176B61DA + 4C63017D1E187C35A02E16C5B6EA2DFDA9FCC26DC5B1182EFDEC380320BE1AF5 + E8C310385B03070C3EADEB7285B5405DD2D91A382BD7C94038435EA75DD77164 + ABE2E2025A7915025BB6F835DCCD56C0CECFF1E17ACA97486F7C57F7C8F72A44 + 56DD6D559BDFE55EA97BAA426B6BD536D5DDA636191213EF8260C360DFDF03F8 + D67981EFF7F9DEEFB9429E2F445ECD2F2F9F15D8BBF9CD01F875684B60DC42B3 + 3F30DE47D61CF950F31BBC92148F24B1C54485C006C4F1B21BE515F866005C20 + 7081CA592F03C065FE55E65DC8733297BB5D88B95DA8CCEF90E78B6DDA791216 + C161155CE79BED5C95F966CAB75AEF35A4151A20AF61FE2A75E7D1C457007743 + 9ABB4BCB01BC0A57CD1D539FCC7B8F372B66354757BE356853B432CA0F374B76 + 1C68966AEE3F79E274F3A1AE936FEB7AB161FF3B61F98B0BE8953C13E81901F8 + 1B2F7AF2FC8007A2FA1C4158C4B3457101DDA70C6F11F15C6620EBB2ACDCDE72 + 7D340A00E7BE48674BDF2C97A116FAD6742859EF5AE5794FCFA35BF42C2A8D6D + D5F367A83C2B0EEA0D0360A797AA2D5AB2496900B0FA000077AD80FB00B0E16F + 572DFC557CB9FA1C65059C9561EDCFD1108CED29472EEF03B87D80D76DBA652E + CF02007B4D66945A20DB538600C8A80FFEF6D5CF729D6EDDA1FC1E483BB4AED4 + 5386F27A56774DABAF5DB75E6E3B5FACCB8D56BB76B77CF31004B662ED9175BF + 11A2BCABBCF63AAA5EEEA3AFFCF7213EC36FABFEB5FC8116AA9BF372BD3EE576 + 3F4B86B809E62EC445868E9BDA8CAC632D9702FCEA77850C826BDDDE36598CA9 + C37A160380FBF27E7B758E3D8F8FFD42FD16FC6F218D3AFEEB60932F3B56577D + E0F5F7A25FEC029A3D81BB65BFAA38B68FDF81B8AD7AFE58F3FA9172F90800DC + 2BD76DDB148DFA43E73FFC62D47BB1F95D898B2217C77233733AE7BD4CF9C69C + DBBF4A1F8B517732908FFB32B96EB7EDEB18679FF271BB5A4CDD6E594E3B1E69 + C2C502E0BE89604F7E9E54CE9B4CA63E81BEE3ABB71757308A037F19D34A4D7A + 6D05FC3A00B027CE9E04E7FEB3725DBBCCC9F5DDDFCBD41E4B9FAB4CEC07F156 + CA5BB25E7DAE579BB5FADC219D1BA5976D601F60F6ABA9C0759D1EFCD6EA2170 + 9D1E0A37EC57BD3D7AF8D0C3D8DAE96685B4520F3A637AB0319CB50C6903C02A + 6DD8DA0780733CB71B25F795FB6DF3E86BB51E92141FB206EED4233FC79161ED + 4200F865CA80B79B6F2B60E70500AE0FD9F1905E012E0A8BDE0A6A0D6B0D6F33 + F4ED8A3E0C75B3DAB6B57FF799CB09470160AB0B805960B10C8247E657A88B0A + BC1DC0E03EE0EBB4DB0CD2AE7B54E1D10801C058FF028057EE3ADE2C9F92760D + 20F08A69D5AF0B545DD90A376071AD4F7E2C76D5C52C835A54DCD0A96D2DCBD0 + B705C1847541AD5D581B916FE57216E1222EB1E0D65A6F78218F05BC24E77741 + AFCBFAF247016043605BF9027A0D8089977DEE58D854193A4CFAA366CB898F23 + BE42FD037EB7BE7DBAD978E26400601655B7BCC5DE7BA75B1510FC49ABCD12EE + 1AD72600EC455E44DCF95EECB55CEEFA8345E10280375537CF01723BCAF01775 + 01B021F07AF5817025CD1EC008080CFCDDFEC1A5AA02720D780D8101BD806014 + D6BE9D3A06C0B87E360006EEE2AA19B7CCE1D6B902DF2100AC7C20AEC12F69C7 + 0929CB80D6C0D679845DC8DB2D771C65F88B727E2E9F3E77BF9961DF5CF2CF60 + FDFB59B3F57401C19B4FDD0CEBDFD92B4F9A3DD7BF6826D90B58F9580017DD0F + 8BBF695CC956E863101C30F80270A9582D1A003B5D4054B146240EB0DA0D0CBC + 56A019753230B30BDEE2FE97F8A366878EBD9D7E81C82A07DC01DE68377B5DED + 549F7E0AFC7DD8B0376B710DADF855EA0C801B9690F44F3F80BE028081CE034B + 5100F00EF2D46EF7DD2F4358D5CEA23B5F947805A8C052E0E22C7BD0DE7E1E61 + 818E80C8026B0D1511632F80B1C0D10C23731D971B8AEED571F702716BBD62A5 + FAA8D97BF7793307E8A5DE2DF5039CD6F800C0D3CAE733202C82F77EF6750B34 + 81D606A4F1B9150289F7AA1D2E950B482DE398AB63B138AE3F579457580B0046 + 8C277FAE80B775DCD427CF9FCBEDA8E3FAEED3FD21EA95B63E978CA340DD80B5 + D59AD99FC9F9E5BB71B9BEBBF8BE3C36F57383DFD3C372FCF86CE4F379745E75 + 3E6674DC69B59F567F337CB70A775E7D1CEEC97D7EF86DE1AA99FF080098DF37 + 4096D0BF75EA382F036044B9212EF12C60EC5C6D8BD52FE0D7601790EB76F417 + F56B483D603156C3EC1D8C05F14E85F4C118FCFF89BAEAC7FF438B7440E11AF7 + 4B1C88F44E1D9F1740A6746D28D7847BCDF4E5A25D1A5F112F86E85A70E97658 + FF0281775D00C4028335165D13A6D57E272E9B4358040FF606468060DACE5CA2 + 2D7B005F6BA6A469AC8ACF0172AF467FBBB0908EFE4B5BFA08B8FBE99500BEA8 + 580097F8907023FD71B1E835FCDD055466EFE2333A4685BFAE471D1440B70260 + F23308767FC8F0D771CAB75400BCFD23DD77A48953DC5BB89F0C80B0E16F16E0 + B79B06F252DF6D9D1FD2BD70335B282CA4778A3613028E2BEC0500DB757486C0 + C0DFB5ECC3AB7BFBE0BE5EEEF1E146BABEEC65CD03BF0ADBBC13A79A4D9A7B6C + D1BC60F39BC4D52FB0F798E630D50D74CE5F872B67B55DA3F94CCC4B6ABF86BE + 06C0B6068E50F9B6FACD0078EDA1F2E25C76316DA06BCBDEAECA9C2BCFC586EB + E7B841708061CDC12CE679599EFFB52F11D67967581057EBE190CAD6EC1ECC4D + 99EF324F36088E36754E1C619D170FE6CCAA5BB57CF648B37CE64833AE793802 + FAAEDEA37692CBC9EB95CA57CC95B93916C01C23C0F3EE724C606F5812EB9960 + 4CCF113187274FCF04ECFF1BE19078BED0182A7005DA2EE40EDA75FA00B0EBE4 + 30E777F32CDA5BCECBF503F426457ABB9ED16A68F03B0F004B19FE1AE61AFA76 + 0170AE3BBFCD40CB249E2FC7B614E81BE0567D18E6668BDE3E00ECBA59AF0A80 + 5117D8F66954FD21189BF2B35C0EC025ED3E9CD7E716BA2DEBC87D59CBD6BDDC + FAD76B34C40D7B737C9C7D8479493FB559ACF21A509B8FE5AFF3EADA91D79E7A + D7953AF9A3346ADDCAF9DDF26EFEB006B0B74FBD00388975BE2CAF5376D72B5D + 4698D75EBBF5BAF9DDF2DF9718EBE2C5BA7B5FFEAB6AFE5AFE408BAD67E5FAA3 + D4D7EEA5EA01B8AF4506C03D6AC7FFB2CFC05833EF794500ECFC5CFEDBA873BC + 3E3EF6F72C58E4A8FC51653F572F3B56577DE0F5F7A425A80B7F5B088CF42146 + 7DD09CFF5AC431185B4DE71F712B7EDC8BF881E7F256C3EDF29FB495FFE41D00 + BC9016FAB3BBACADA30BCB2FBA80BE6EC518191B632C3734A747E53BDDA7DCEE + 65755F550B4D065E76AC516D7FAD71F6F5938F958FDD556E33AAAEF3BDBF46B7 + 8D3512002F4F79A3E02FEA5801E789655751A7F6891B68A02FC71FD3F8B6EF3C + D0ECD18332E16A26D51A770061D569DB7694F3F37116A33C79CE7D66E57ABF26 + 000E77CFF1B62852594CFC55B64E75433A2F6BB6E99813CDF8FA89664C6563B8 + 828E87183D446DDADF2CDBB84F0F3D00D73DCDB2F57A18DCA0F81A3D88AC2A6E + B4A3FEFAF960D7B096701C285AE35D2D0600BBADA12DA23EFDA2256B76E90169 + B6594EBA96BB2CB7E9535B4F02CCF601E061A0EB74512ECFA1E34E0F0070116E + 9FE30DEB0A65E7A942DD78F0AEA03603E0ECF619D96DB4A16E16F9C87D77EBB9 + 3CA723BEFD400B752DE7017633E435E8753C2BF213C8CD00983802EE7A9186B8 + F3AD027D0FC7024EA9F746B36A4ADA85FBE7A3CD9AD913CD6A00F0F41BCDF2A9 + 6321C3DCE27A8E70A016F81297EC3ACE7986BD79D12BE70DE57B11AD2A16C514 + 66C0EBF6CE5FBBF7AD56B9DE90E86BF71BCD0A16BB2AB4ED5BCCCB8B7D40DD0C + 7B9D07D85D7BE05DA529D3716BBD3E05083EFC5EBB5859F6B243EF361B8F7C50 + 5C231E6381F3BD66FDE1F79B2D6F9C6AB6B0B88A550C0B9B47941F56BF279B0D + C73F88055500F046D509BDF17171CB8875AEF5C69966BDF259A8F502AE177A11 + 8BB97961372FFA52CF8BBD84C800B885BCEF14AB5D94016E2BD2352F2C7B255C + 41873B68C5BD8FB0DB5A2C6A63FD5B5C4363F974B957B606DEF22E96C2979B89 + 0FAEB6103842698BE2C05FF6FFDD96006F88B8417005BC2E37F8755DD22ECB30 + B76BCDDBCD777B9777216F57E417E07B6F00866B1A4D52E7CCDD66E2CC67D283 + 66FAD2E3B0FADD79FE41C8F11D67D8FB57F5C3121877B1C5C2D1EE610D804963 + 2168C00B0046C56A51FD57C865080C5002589147390242B95ED4551E7BE4DACA + 355C420710567F6A1F2011F095605AE89A7445E97005FDA8B886BEAA347538A6 + FAA24FC35FC348A01FD010A0D75A1D03456FA8FF3B0504CE4A80E080C015280E + B950C67DF2DD17010E0B64044416506A196E92EF78E8B6EA5637CB0810397B4B + 79377083ADB12ADC73F7B90480A49EC6C7B9ABF9736A4FDE0C1093310180EB58 + 81C07C8E030FBF6BDEFCEA2FCDFE07DFA8FF3266E4CF1DAEA2959E531BC61E80 + 18CBE1DBFA6C77107D4A8ACFDED267D0B1095D3E471DCA25F2701BCD58C36AB8 + 7E66CE019F378070FDECBB55D790374360E294B97CD0A6941B009773AF7373E7 + CBA1730FEC457C36EA52CF16C083F3CFD8F4DBBB51C655C6C898F9DC3A26DF3B + DFF72DACA7F5FDDFF94AF95FAAFC59B8FDE638BB39D71C13180CFCD579DCC3D8 + F99C8C57C701B0F21B9FE1B8F53CD8029D635267B7DAB87EB875AE65D4C7E27D + 063176BE2BFD3667F8BE22ADDFB47E0BE1E25971EA4F6B6CEC6D3D7DED331D83 + 971DF4DFBBACEBC115FE7F0340EDFFA05FC4205DE07101BE7E61833CE296CB71 + FFCE35A05C1778498438D78AA25D00E100C0BA5E017E03D0027681BABA260193 + CFE391E06E33754EF51507DC52565C3E1B1623B5B908C4BDA6BAD754F746C0E0 + 894F2F07D49D02D89EBB510170E9A358FB5E6D41AFD34063DC47DB85B42D7D0D + 7681BE06C09439DFD037C35D602EF9AEE77CE2F4493C0360A701C0805AA02DF0 + 77C7C7BC585420B08170D73A388B3294017006C8CEDF0404AEF74BEEA3F10215 + D03789B2C1BD58F7DB77700D3DB0FEDDF21ED6C0BA47270B60E29BD008F8EBFB + BFEFFB83FB7FB1182E730ACD0D94C695F476DC51BF89C52FF057F395EAA58434 + F986C00061FACA7392986354E09B5D42131A060FAC7E33002EF0D620B70B8133 + CC759D6E3ECA7DE4B23E009CE780CE432DE465EE68089CEA455905C09EAF127A + CE4B9CD0F35FAC80B3079C70EF3C27ED561D43E00E0076BE45DD5CDFA2EEAAB9 + 721CE6E7633B0F85B0FA0D4B5F8D8963E202DA1E7E62DE5FE5678AD5D33C2794 + 67059E210C5CBB00D8401665F0DB55AED7A75CAFAF9D0170572E07FC2EDBAE67 + B664E9EB72C221F05BDB46BC82DCE5DB95D7C6F74A00DFACBD5136A8C34BBF3C + FB15E88B06E5CA570800CE60D720376B310038D75F08FCF68161C3DA3EE57A59 + 940DC1D84E3BCBE506B86E8B02EC26008CDCC6E92CF765BD0C001BF6E674CE33 + 00EEE62F4683F5A0B20EE4B5A0F0FCA63CAF1BB5EB473D6B4A7D6559B92CAF6D + 2DA4C5D51FC0DE3EFD9600B82F3F97FFFEC4581727AFBBF795BDAA86D6F117D9 + EFBC36B6CA4D757E8D3D817F69FB57D53C17D05DF84BBD1116C5AD32005E2413 + 9AC77B6A5EB7DEEB55E7787D7C6C218D627DBF09035C84161ADFAF3DC6971DAB + AB3EE8FA7BD1B2656B9AA5F8A96EAD7D07654300B87E18F6FCFD4DDD3E6700BC + CC7F367ECCF907ED74CA9BD70FED5D3F2BF7330C6E5BF50160C7737E8AB77D74 + CB9CAE8A7ABAB0FC1617C09F2DC618E3648CE586E61B70BE1177F39DD755AEB3 + 50BDC568B1ED173A9E270D8400C82E401DD5EE55F5AAFD784293C7D25796EB10 + FE9600D813C7518AFAB5CF652BB7977D7EF5BB678C07F4D0FDC9B95BCDC1A31F + 346B8097FA2F8E2DD7F85567693E5E578B3C76576EDF97B798FE1653C76A27E9 + ED845F5AABF6EBA43AE9070017F84BBEBEB3D5DB9AF135DB9B31D55BAEFACBD7 + E82163AD1E58D6EA016CBD1E14D7EF53DDB980C0CBD6EB0170831EE0D6ED6A56 + AD996C56E998631C5769802C62FF60C703E00241690308ED91EBBABEF30D7FB3 + 72BBD0A6DD61F53BB676BA99DCF34EF3FEB97BCDF193579B759387DBFAE3C0D8 + 6EBB2ACAD072A068C0D98501F0527D8E0270F7D49032DA0EC35DE7C567AF6980 + 6FC9633C4A2BBE546D96A004720D631150977D7E03BA4AE4B9DE4200D8ED33D0 + CD727EB73CA723AE6376C1EFF289415EB78C7008FAA67886B95DEB5FD405C086 + C58E037FAD80BEBBDE6C56EE94A6DE08F7CFAB2AFC5DB1EB7833BEF358281688 + 00BAC925742C0ED545A22EEC75DA8B5CCEF74297D54D53B74F2DC4AD6DDC2FF9 + 2F03C0EB6A190B74586A609DE145BA79DA3788037009BDD8E7BC7507DF93DE0F + AD3D40FCDD90DB0DEA150B60162701C01B8FB180E985CA77C23DF4E663279BCD + 2C8E1E551DA5B71D3FD56C3AFE61B3F9F007CD66AC66700F2DE545551654D729 + BEF6F8076531F5C4E90254DF3CDB6C3871A6D9F0C6B9C8F3822DA1177BF302AF + 1761590C36008E3AB5DC8A366F2AFFADB3CD26169C25E2C820D7B0D70BD286BA + CECF00187911BB2C5E17912E7B0353560030FBF9DAB5F3C487574386BD6DFEFB + CA7FBFE44DA86CBBEA00770D80C30AB8425C8425B0AD810304235DEBB2A25E02 + B908988B5C66B88B881BFEBADD0EF6C5FCA4D60957A925C4752A9A3A7B2704DC + DDA17A58D3CDB07F6F05BEB844A57C3A408B74F67E58F9EEBAF4A499BDF245B8 + 7F4653173E0FF7CFAD2B68F531592DFCC202D800F842D917D4F00717D1802100 + 2F60C800D80009D97A30835ECB90C975016393570A002E7BBE16704B58006185 + C0378B85652BC0DD75E9DAD366F6DA13F55D6058A96F7058DA1A321A44060405 + E2DD78D6ECA9209174B853BEFB65B10655DCF0742E00638DDF02600E03E0021C + 3916E3F77807C78CCF710BC8AAF0B642A02AD05402080354A76F3D6EA6D407A0 + 973475884F5DC73A59FDD67C97017F01BE8831EFBDAFF15410FCDE77FFA6B9F8 + 6FFFBBE69DAFFFA6312A0F6829014409AD00C20A711F5D4032E21CE8BCDC7B1E + F2F13C4E64F86B05D48E311505DCE63BE67BBACD39E378058293CE79FE6EF6A8 + DF502DF37745798C53DF319F23C3789FFBF88C150007308DCF0700E67B1EC0F8 + 0C8001AA80F0697DCF584CEFB9F75508F03B7BEB4B852F9ABDF7BE0E188C1575 + 1C47C7D87D5DF5AFEA9C2B6F2FE79EBE6BFF7C473BB1DCADBF5F8038BFE5D60A + BDD66B3F17DFAFEBC6588BC2D538BFC7FA79C34259E3A00EFF155E90F0EF9ADF + DCD475FD7F242C9C775DD7B1149FD1EF668EDF0CBF4589FF9E81AE01B0F3F8FF + 3A6D85753EF529C70258FF73FEFB06C0C502B88060840B68E0EFAECB379A9D17 + 755DBAA8B8B4335C312B0EF0D5F50800BCEBBC8E8FB7816AF90BFCC55A18F8BB + EBC20D5DB3AEA9DE55B5BDAEBC1BA1C97300DC2BA19D005E2C7603F696FE0B40 + 2E7B004F4A76214DFDED9FE81AFF6981C300DF0C7A893BCFD6BFE467909B612F + F500C6B61636FC35006E55FB27BEF564B1D435B81D580017883BF191FA515E86 + BD86BB39CFA0779BFA73FB6C194CB8F5FD8B05F4726F4CF7CCC84BF751EE9DDC + 6B81BDDBDED73D342C7F957E0F6B60DDB7DF2E8A3D8001C3B889E685AE78A9AB + DEF3EBBCC07302D2D6A05C65C701B9C573C8D6773F6DB6ABFF3227618FDF0F34 + 77613F605E543300E685B55311BAFF0C810300570BDFBC0FB02D83FB017081B9 + 561F005ECB4B7A9D3294E12F723EF10C7F3300F68B819EDB793EE8B22169BEE9 + BAABD95BD873CE341F65DE9AE7BAEDDC57F27625B16509F91500AFDC5D20AEC1 + AEAD7A33E07539D6C086C096CBA35D9D9773CC98BB6B6E3F98DF1F2DAEA001C0 + 354DFD98DB6BEEEFE78A229E33A40A7C81B206C019D866306C786BB99D61EDCB + 94DB76CB0C7E87B443CF600A97A1EDC5D2B75BC740780800135690BB42CF6968 + B9CAFB0070773F60C3DE51007819DAAC67D7CDB30174FBAC7A0D7747016087AE + 8332E005A83A4E3ED6C1C87996A1EC42CAF503C0AE57BED5531F19D61AE076CB + FA206F37CF725FD6CB2D8087616F17FE3AEDB8D778162BC35FF78980BF79DDA8 + 5D3FAAEB49DDB29C3F4ADD7A79AD2B6B31758A06B0B74F430078C59621F8FBAA + 00D8CA6BA6B95E5F7E2EFFFD89B1BE5C79DDBDAFFCE728F7D9D7AF61AEC36EFD + 3E003C4AF3DA2EA005DB2F04617F81E2383DF0B73DFECB8E9B01F02265A664F5 + D579FDEA1CB78F8F2DA4E07D29DEE6FD160C70111A358ED731C6971DABAB0C5C + 7F77020067CBDF2E000E515E3E4C00E0D7EEF6B92B1D9B7041009C43A4BADD7E + DAB25F415DB0DB8D572DCDF97DA28E2E2AF32E44F58213178CD774317C25310E + 85DDF1E51BB1E339CFF9847D37EFC56AD40D7EB17DB95E9FDC27A1E1693E0E75 + 1CB7BA7546A97B2CF7E530D7CBE951F271BBCAE57C06E20BF7499B6DA116FA76 + 84C56ED6D064B042E0BE49639E5046396DE96385CE2D6363CC0A77E9A1F4E89B + 1F373B781B997694EBB863B57EB7DF561500BF8ABAE3EC8E11F559FC8656D7F6 + A84ED45FA6A1C97AB451B856ED01BECAE35828CAD6A87F846B6CA5C7F470B05C + 5AB176AA59BE4E0F606BF4E0B37ABA195FA787B60D7B9AA540E0357A9852FE72 + D5592DAD515FB42900988724AB00DDB0EE557A492D5FB661F8410E91B720E405 + A256800CACB50C6DD90378D5D67DCD8993D79A6FFEFA5F37F7BFFC6B33B9FFBD + 6188DB698B22CFF9EAABC0DA028081D84E1B0023007081C083FC70E51C6319F4 + 01DC655FDF65CE07FE0269955EEAB20480B1EC0D805B016C4869435DBB6626DE + 5A01D317EDA400B8A92DEDDCB685B93DB2BBE75C8F7E68EBF21580DC8943CDF8 + F6A2E511D77822BF40DF2C035FD7310806F6E63D7F0D8233000E55F0CB620D6F + EBF3D67E81C2879BE53B553760B0F2761E6F564E4A3B293FD6ACC4E5F3D4D166 + B934BEF348282F106585BB3AC4029142BB6D36A8F54297D3CE63212CA0ACCAF2 + C2589BAE71EA625511EE9BB1E09DAB7DCC967E5D8FBED6ED7DBBB8B4531C7921 + 2DE03060D70B772CCEB1B8C782DE817752BCA40959008CC542E2515EEBB07808 + F0C592B76A6DE4B96EE9671D8B8F87DF0B2B5E16200D80C3AAB72E3A1207F66E + 614115387C58E9A32795A7B4E21BD566032E1525F6D1230C6B1A00F071DC3B17 + 778A1B4EB050CB42EED966D39BD25BE722CD826B170077177B2937E445610964 + 2BA1884B519F7280AEF416D017B792354CA0D78BD305100383CFA94FE5BDCBC2 + B6E2B16F6171F58CB52FF501C384A4ED027A6BB5DC0D985BF70306EEEED0B509 + 190607100606A30F2EABFE956602A0AB90BD7C37A9ADF7F4CD3037002EA0B6C2 + DA2E0006186798EB741B3F7D23F6E60D6B5DF6C2FCA40060FAA36CBBD23B00BD + 61DD4BF9ADD024F5C36DEA9D02750129E76E37139FE2E6B4EEAF79FE5EB343E5 + 13676E3693E7EE84BBD55DA1CFAAC5EFC366F6CAD326F6F9BDF0A099B9FCB805 + C10060F2775D02EE14C55EBB61F1EBBD7FB1F02B4008683475E97EEB169A3D83 + D923D4503700B0CAED6E16B7CDB68AB4D523215690583BB2D76FD967B5C05BD2 + B82A0E4B48A958CA16D0461BAC2EF7DE7EDEECC552F3C6B36696BD61D5169016 + FBF1E2EA17D85A056803CE150BD5C78A3F6E26015C3A46ECF17BFB0BE5AB8F0A + 426D091C16B201829F4A58C3AA0C188785F09D2FA37C5AE3030EB2F72E630FC8 + AAF89CDAEDA19EEA6339CA7100BE005220AA812A0015D06A906A19A6B2D7EFEC + 6DEA170D006C81BD86D60877D53B3506AC7FCFFCEDBF6ADEF8E24F71DCD807B8 + C2D202508BFB6A806A4055B5E3F3ECD6787D9C3DF780CA8051F2CA78E2D8717C + E0702903160FC6CDF838973A9EC6B19B7E942EC093F354CE0BF984C87980788E + CF58E23B91CA79D779E5372061ADEBEF8238A2DE9EBB5F85DC26BE27292037E7 + 8FEF5D02FE02EACBEFA28C7716C82DEDBDFF95F4B5C6AFF3AAF383F5EF9EBB2F + F4B9BF08001C16C8D6558D59FDECE6BCEAB7C9EFD6BFD3A9F80D16605F00ED00 + F21AFE86F5AF7E27C57AB794D10F507A8A7DA543C05EF6C4060EF31D694C7C26 + DAE9FFC1F1CA3ED0FAEF5CD37FFCAAFEB7FAED4FF1FBBFFEB98E533EAB2D85F9 + 5FC67FB0CA69FE9F8458093BEDB200C697D947586DC2E5BAFAD635210030FFEF + 0A80D903B8EC057C5BE93BCABF5B003175747DCAD72DAE53331735D68B5C97B0 + FCC582F7763373A908ABDFC970E57C2560F0CCA59BEAC770B7A800E35BBAD65D + D7B54ED7C700C9D42996BCC53AB8A4BD0F7029573DF61BFEF4BA04F0E5584061 + 5DD71547C4D1C4E902772D83DDC94FAFC6310A302EF919001B1A5346DFF40900 + DEFC81EE5758029FBC18E990D201773F523FA7AEA8AC0B805101BDC4B7E81E87 + 486FFF08884C5E72010D000618C7BD106F1703F81BF7CC7851AA88FB2DF7D9D8 + 17F883E2FA796B6B05ACFB72DD17B888BC4FD5867BBBEEE9D2E66A111C7BFB9E + C0BAD7F3015E00330C26CECB669A2FBC792AEA320FD8ACF9C4E6F048F251B359 + 7537D356F14D40E1B7B0FE2D5B56A0F56F94798B0170CC4D082B005E8B971443 + DF0A825BEBE03A87DA70A47852617E6588CB3C8A32AC8FF198B216A05B65006C + 65E0DB55990BA6F95E02BC03A83B00C119FCBA6EB88226AEF62BF7BDD9ACD893 + BCCED439AEE7B2ABD3DC98B93369E6ABF1C222F358E6B6CC77D9C377B642DB0E + E02D6097F202839D1FF5AAB0FA65FFDF50EC054C9EFA9088338FF7FEBFB60826 + CF60989739E3854EC9CF16EC114CD805C086BD06B48486BF946560EBFAAE0B78 + 6DCB79BE695F901DB459A9E79F953CE7D4B4DB8C4FE8396B626F00DFE512E962 + D5BB3F344E9D5ADFB0D7E198EAD2072175CB8BB905FA662D57FD15284030CF75 + 805D9E19F51C2A79AF5F2B03E081EA73E7663DAB0264A39E9E7FB7E879778B9E + B125E2E3849B919EED23AC10782B650060B50DF8AB76A1B40E0010DE385D42C0 + 306D710FCDF17AAC84339C0D404BDBACDA4FE977B468D72A81DE2178EB63D474 + 17F646FDB5524EA7BAAC8FB055D6D8FA027003C0AE05E21AC6B286B2A385BB6D + 9D8E32C0CD70D7629DA8AB6E19F1A1F6AC159197D68E62FDA86F4D29AD358D52 + 5FFDBCAE95EBF695CF5782BC3DE017C5FADEF2CDCD923030010603823B8A75BE + 61639BBC8E99E35EC32C798445651DBAAC6552D6B76EDB27D7598CFADAFFBAE2 + 330CABACB597F5FEBEF29FABC17A7D512EEB02E09FA741DBEEB14669B8FD4051 + DE730E725B6BA82CE20ACD4C1237C9EDC312D852FDE1F625DE0D23FE8A00D7F5 + 4769A87E8709CD53277FA8FD42ED464AF5C7D6E91C9470A482B911C2FF30C4AC + 5033CA527C945E56FEAA1A75CC51C71955FF5711FDD2BFE2ED39EAD11070FD9D + 690079E76B50A75AE0FE2ED4F7431FF5A7A46EFD8147BCAFCEEB55FCD9094728 + EA10FA22E38B50BD804559BA90FD1A6A8FD153F6EAA29F7E7153C871DF5447D5 + 71BA4F0BDD98176AD7A7EEF11CBAFFEEC4A45B2FD7753A2BD7EBC6FBFAEA4BBB + FFEE71BAE951CAC772FDB62DA12662CB96E38EB940E03E10BC74852689114A7D + D0B542E056DDF2AE9810AA5E58F7EAFCAED0447C8D1E0AC6566B52ACFC65AB34 + 0995BAF0374F4E5F3E49ED97FBE86BEFFC3C5906FA2E5993A53A4CDED304DDED + 5EA665A1DA867E941E537C6C8D26FFB53C26FF91A7878EAA713DC88CAFD503CB + 1A3DF8AC56B8560F4AEBF4D0B60EF84B9E1E64D64C35CB5567A51E6C96D7879F + F6C127C2027CEDDED965515E1F8CB20A041E686C230F7CB87F02A6A2E20A6A1C + E09A04300600AFD403EACCD153CD47973E6FDE3A7DB3D93CF34694036897A10E + C88D7CF280B59BF5E0ABD0C0380360D71D405E20B0C6C6986ADE723D18A371DE + 8E567E88BA86B33504AA1AD8C6C37455D4515900D808531DF5436860EB7CD777 + DAB03680AE3426B51098B6353FCADCAE96B54AE90C80633103F0BBEDB0DA1708 + 3CBE9D050E164158D4900C7CA50C80234FF1553BB1E23D140018D86BEBDE814A + 1E8B332B769066A1E668005DE0EE2A5C3A2B6F0CB04BFE4E29DC4F1F89BECBA2 + 8EEAD25E2AEEE1D40F8B3F580E2479D10A116FD32C5C7961AB2A2F3A5196A1AF + E34369EAD63E02FC4A6571AD2CB66151E1853442E0EEBAFDEF346BEBE21C5AB7 + 4FDA5360702CECD18E853DC3DD2A2FD6399F90BC80BD2C0CA6BA6BBAE9A4A82B + 799131DC37D74547C200B900DD0A76371EFFB0EC970718264FDA78ECA3665358 + BF5470EC3A5558D26C7CF35440DBB5AABB36F6F22DEE155B37CDEF9C0D90CBC2 + 2D90372C6D12E4757EBB089BCB80C4C995E466148BC5B889C41249FD13A7CE7B + 679AAD40DD58801E58F216180C0866711A5796179B4DEF5E6836BE7B5E21F92C + 62B3788DBB67DC3803788B00B8E196B95D30BF1A212078BBE2DB4F5E2B2E9D95 + DE71EA7AB3E3A3EB017EA3CE47C55D330037E0AFC41EBEC40D8129DF5601B0AD + 7791C12ECAF12E048E3E4E5D0B6B5E00F0AE0A43C2AAB75AF94E9C91CEDE6EB6 + 137EA2F0E3DBCD0EA958FF62395700F0E4596007E044F967811A05A24C2A6FF2 + FC9DD8AB13586BCBBE62B1FBA0616F5FEF97CB5EBFD3E132F989CA1F8628C385 + 32E1D4C5CFD5172E9E8B256F00DFCBB8752ED6BEC0DE80BE2A2F400820ACF0CA + 83109683583D86E5A314100A1856AD63CB7EBF40AAA7CD9EDB5F36583B02806D + B9E9FD794B9CB2CFA35D01BC650FD7FDF7BE6AF603FDAE3DA9569F4046AC27B1 + 98D5386E1408971556B7015655EF3AAE9F81676A0B389400C006AA58D462196A + 2B5BF201AEB887761C101C651270D516B640CC3D77BE928AABE2BD1A276106C0 + 19FE1AB43A3F6B16F0473F1A0B16AA0554BE081902B70058E39AD279A0DEA187 + DFB5798C937AC57299B1302EF5774BBAA17C2C5EB1A2D5B1E27329DEB609D04B + 3F9C037D5F35CD31080B30D667D6580B9806E42A2EE1B23A5C46530728ABFE01 + CDEC3D8C384701A3A30CF05B8076F9CCE5F88E93EF310099392EDF2510946372 + 5E184F1C1F40AD36845197769CFB80C0BC00507E0BBBEFABCEFD2F9A5DFA3DCC + DEE6BC95CF1DE75A624C7C5FF1FBC10A5BA29F7811014B62FAD3EF12F01F40B6 + FE46394E1C03904A5DC65C05006E2DD869AFFA68F7CD470186A7AE339E2F9A9D + 2ADF7AE941B389173BF45FD8159FBF826F1D0FEBE529B5E7778CD5312F4D706C + C2B072BEA5E3C7B8EB18D4778070D501F0DA9D3AD6BEE46545197195E1EA7A0F + BF2989382F800085F9BF17CB7FAC818B657009B9CE7C169A02F2C6CB2A55BA36 + 4D2B6FA6D69D0AB7D1EC6FAC765700C9BAA69DBF2E5DD335E586A4EBDB458517 + 155EB8A1EB4D290BF7D217751DE5F8BC6CC23EE4E72485BB740DDD459C6BA4C2 + 5DBA46F182CC0C9E0D747C3C244C7CA2EBB8AEB33B756DDD49C8CB38A7B9B6DF + 8CB6D38C35AECBE4E1B5816B3FD778A030D6C2379B1DCADFFE31D017880C40D6 + 3DE523EE1900DECBCDA4EAED54BD89D3D702EC6EFEF07CB8901EE85285CBF451 + B4ED94EE51A7B82F5D6AB6706FE3FEC5BD85FB0AF710EE65BA7795FB1B901818 + 5C2C838700B001326EA7D30B52136A935FA28A7B3E2F6019FA469AFBECB97A2F + 475800176119BCE91DDDF701C05806EBBECEDC014F1FE1ED233C8A3027000033 + 7F00FA02893F6DD6A9DEBA13ECE5CF760EBCEC75AE59FB86F2A50D8AAF7FF34C + B48FED224EE8386AB759DA7082B9CA8731E760DE52E62E9A83681E1396C0CC63 + 8E6A3E522D7DD72BBE2EC06E9D1F317762AEC4CB729A1FAD657E152FC6313752 + DB70435D4030656BC27B4A998B19005BE4315F737940E00392E7895586BBCC27 + ED118610B93CE0AEF21D67EE18734AB4B7CC5F57D24F9DDF469F9EEB3287D55C + DA2F539206FEE679750B609973038EE90FA82B018657B4801817CECCB34B3CAC + 87C9A77CBABCC0197377F589DC2702FC1AF8461D8E69009CB48217486B3FEC17 + 0C000E6F426A6FF8BB5A7906BE9127B5903729206ECE07C84E223D5B91AECF40 + ABF49CB44ACF3E2BF5CCB34ACF4BBC2C6B285C20AF34A1E73BA9B4D773DD0EC5 + 01BFDB542FFAA17E6987656F58F72ABE4279568C47F905F8721C4254C02FE5F4 + E3F601916B7C9C3154ABDF257A6E455D209C05001E969E9B37EB9919A81BA0D6 + CFBF9D380A385CF2DA676BB51B632D06484B3CFA292AA058F92E03024BA4A37E + 0B710BF05D4A7E6DB718B9EF21002C2D59BF6BDE7A4586BF398EA25EC0DC411B + B76BE3D298D2E3D218E978095F6DD64CC63EBC050CBF9A756F17048F02C3393F + 03E148B71AAC1BC5DAD02BAC338DD2CBFAC8EB60795DCC79837AAC036E8DF5C3 + A5ACE325B1B647B88CF438A017004CBE443C44BCE48501CACAAABA2689B01C8E + F549C2BA8E3958BB24CC2A75E8AB0B9217A3BCCEDB5DF345A5EF5F4F4BC6BB1A + ACA30FADC9B3EE5FC16456AE9BDB8C52B77E56B76E91C7DA57365F83FE880FC6 + 34C81FAD6E5F59A3EA75F31C6FF3F59B18E377C16FA67213F296AD9438FF95A9 + 98B9B475680F5085D5D096BE08C9E358E477D4329E0C5E0D62AB46B5E9A6DBB6 + 399DF36B59DBA6F6ED3AF13952BDC58B7E140653931C3A8E96520E775B9B9479 + DC4B0400EDCBFFB93254EDCBEFE675F25F0ABB5F5970518C635FA20C5C7F6F5A + 08FCB6755EF54B7FED4A3F50D4FBE3CE5A4C9DD723FFD91752D425AC17B250BA + 584598CB5E26B7ED2B93BA17D45F26FA7975951BE220CCF9BFA5BAC7EE9B0C74 + EBB89EE35DE57ADDFCBE321F27F799F372FE62E4E3E4F6DDC9CD32DEC2D3642C + 43E0DE499BD388891EF204D1E99A3798288E984476F2A21DC75008F86DDBA36E + 5BC9135997BDAA68DFD747CE6F45DA93F3F4C626EAAB4F3F399DE5FA688C374D + 533ADA01BDE301440F1D7A7859B6A623E5E15A19100CF08D30C0B0CB7898D1C3 + 97C278F8497BE3A0651BCAC35281BAE5A12997E78728ABD42FEE9BC6F57066F0 + 3B7003854B283D24F30009B4C5CA58C70704AFD8B2374030217590A12D0F9C59 + 40DCC143A81E6E539EC3B6BC2AF7E5F430002EE55147F180B784C05629035B8B + 3CC35D8B7CEFD56B00DC6D9BEB3A9D013032C8C575174098B4F35BF8AC72435F + FAA01E75C8675161A5E22BB61D6A96570D00F0E166F9047BF91E6EC6A5082754 + 0728BBF348940189114077D54EDEC23F1AE5C880D7228D0AEC3DAEF078B3C2FB + F82A5DDC3AAB7DCDC30278058B36B4F11BFDE1DE0D087CB4DD1F8C051F2F48C5 + E28F422F5221F2F202532C7491AF380B4C06C0E4E7B29C6705FC45C4592C9302 + FE2AF4021C8B6F2BF716F77A585C7417E900BE6BD9A357F9438B7A7571AE05B6 + 0A0D800981BF96EB74E50543CA5D2F2F32B258E910451940F7B8D201724F36EC + 8987C85B473B95038281BF40E0B2088A450C0B9ED4FD48ED80BF03B7CC86BB01 + 715BD0CBA2EDF0BE7E7DA20FF71379D495BC4F60B116C2CA57E52C10BF7BB6D9 + CA42720060D57F4775DE3BD36C63A13AF6EC3D1F96C0E18AF21D169DCB1EBD5B + DEBBD86C9236122A1DE13B050C53BEF53D16B2AFA89FCBCD36C5B1E4DD52E16F + 0B6E81BBA7AABB66C571E90CD835AC75DC6E9D0D8033F4A50E22ED3C4360BB6F + A62F44BC85BDB5DE50F969A5B1F0C50534E0F71C30F74EB383BD35CFDD6D26CE + DF897D36B700123EA66FDC3D032DEE373B3E5519C7C3E533D66E58B229DC7106 + F7A8779BE96A85875AB7CC978B5BE65D573E6B660142D7014D0F94F779B3F3E2 + 03099003D079A4BC471107A6CE025C55BE43C70D900CCCC1AAF00A2E9FEF04F8 + C54DB45D456319380774525840B154AD1A637FDF4B6A0708B6A52310F6C6A370 + 650BBCDB7BEF85F29EA9EC71405400EA1450F626C0AC587102848B5530A0EBF3 + 005A7B8174779E6B7C0F9B49C601B4BAC51EB8C05E1D4BB2EB64033F5BD5DA62 + 159898016F818F05A80258819CCE1B00C9E27A9934E54E87B5E975F5ADCF60E8 + EBF1035E51F7F8A84053FA37F0A5EF120F6BDA3A2E1F6FDF675FC7D81C92E730 + 403063664C8CDB309371AA2F006C80CD5B0052F57547ED02FE96FD6EB17CC592 + 16D7C765FF5BC6F845F40F84661C710C2072840500F399F80CFB3EE35840573E + 171A7C0E40EE0C2058718FBF58ED0277CB79CDE7DFE71695730FA07F1C7D0F2C + 93CBF9DC738F3EF9AC1C8BF357E4735BBEEB27FA4CFA8E740C2C7AF94D0080E7 + D476EA262F013C8E3E01C9073EFF363E4BF9AEF8DD00521F8470B30C5C2D16B6 + FC66D5AF7E47BBF96C8A9317717E5B15BEEE0D20AD71B85CE11ECE1969D501CC + EED1F177EB3CECD2F7B1F3E617CDA47E47879EFDA139F5D7FF43F3C19FFEA5D9 + FDE0EB6687FE173B551797DD807E3E0710B840E824FDDF8BBB6B8D3BFE57FC2E + 0BA03670B65570B87BD6FF131107FECEF15FE17FC67F5BC704FEE2EA1A381C56 + FD52BC08120058FF63A50D8C67D40F02F0F28208FB87FB7AC18B23780E8834D7 + AC7A6DE1FAC41EC63B2FE99A78F196CA6EEB7A724763D2F5EE92AE71576FAB9E + FAB9724BE5BA865E50A86B1CD71F5E78E1E599E22541D7BDEA2DC1695C4D6371 + 1C56C7BAA691B70357F9BAA6EEF844D7DE33BC90A3F19CBD17E1F4795DBF142F + 75D42FDE18D0E95BED35DD6EFCB79F22AD3A9FDC517853F70EEE21DC2BAE353B + 4EE91A7DFAB6EAF0F290EE355805E386FA8CEE3FD552182B62AC840D80AD00C6 + C05BDD4BF04C81B56F81C1BA8FE898DBC21A58F7C10A7B0981BFAD1BE8948FB6 + A30A816D01CCBD9797AE8A657081C1E1A9235EC42A4078EBFBC5F277007FB9AF + 23C5759FE7FEBE25EECBB43FA738E933BAE7339700026BBEF02680F84CB3EEED + 73D2C5D0DAB78A56BF714E3ADFAC7FFB72B3E1DD4BCDBA13EA43E55BDF3AAFF9 + 8BE61447B100D67C273498B3781EB241F39EC89380C1B88066BEC37C89ED2FD8 + 2283D073A80D0A81BD21E6546CA31175070038F623AE80D7EA03C0580A7B1B0F + E4F9A5E7949E577A6E99E3DD7929A1E79831AFAC73D400C0BC88585F4664AB91 + D8B264B7E6B6CC8F25E6D100602BCFB189977974991B13A280C2D451FD5C372C + 7D43A56FE6E66CDF02F4C5BDB3C3569AE7479D5A3FAB85BF2A373466EE1EE2F9 + 6052CF26C05EC5CBF34395D261E16B005CEB21E219068FD7F2A5937AA602004B + BC18BB4A5AA3E7A4357A665ABDED40B34AE2596AD58E83CD6A5E500D6B5F3DB3 + ED3CD88CEDA4EDDE66C98EDDCDD28939E5178F4F05E616C5739D9ECF9C1F16C5 + 529427B81C617D960B55003C049D01C0CAB725B15D4C1BEAF6815F545E4C2E10 + 78D916EAFAF97490C6AB151EAFE629B6411A7E9E0E55186BB7D05D19D2864877 + F3A45C37B75D4C3ECAF097F4920E04F6BA44778DC279ED9AC67A94D3C3EB1D63 + EBF1A6A6766BD5E79AA2B2CEB2B359D6B1FCF59A4F96416E57DD3ADD74AE97E3 + C86B4CB1CED4B386F43A14EB5F9DF5BBC84B1A6A83173FEA06EC253ED0128C45 + 1CAF90B7555D4B8CB2A8EBBC620D5C54E06FABBA4E3ABC666991EF3558EA15EB + 5597CF5FEB7C3571DCD721AFCB0E345847EFD629E9E1B5E25CFF97C8E76C5883 + E3F4E7CDD7A0CFFECFB490868F5DD457AF4FAECF77DEC61993CAC63486D5EB76 + 341BB7CC34EB364F372BF0AAA8B228376B411DB0DAE5316D3CE5F7C9FD45DC7D + A6BE4394A5BA43CAF5BAED729E948FE1638FE973A1369FEF20B75F8C68EBB02B + F25BD6D6C7E2AA1601607F15D15F5F9F8B387E1F001ECA7BA5B1328EF9ECB457 + 5DA0FA5BAA77403DEA6B8BA2FC7707803BCA3FE6DF997CA158485197B073A17B + 5D00F8D755BEF8F7ABDC2C86D3A3D477435E487D7DBC8AF2B8CA7750C6E7FE5D + 96F31C771F7D7279DB77ED83D0FD38DFE9ACDCD7CF51B7BF2ED4CE0078AC8643 + C0B74EDADA344A93C53E0D4D1647A8ADEB76EEAFBA59068AE6BA8EE7B221789A + CA4729FA4F22CF7DF429C3DF3C396FF37A26E9A3FA05F866E83B9CA61D9AD4E7 + E72D541E46F460030406ECE2AE488A340F2EA455A740623DA0F0E66A7DE3351E + 6202000FC3DFEE83921F965E26EA8D05E82DF0777E1C18BC47E9DDA180C000DB + 2AD243F1111A02C00BA8DB2ECB00D875BB00D87BF4E2E2D90FCB28F6FCAD797D + 0038C35EE296F30C7C5D1761391CE98E020053A7C6090D81FBE2592C2CACDA76 + A859291902E30A7AC58E23A1E5932C64A0C36D88652E7076E5E4D1028825EAAE + DAA93C835BAC775D2FD2833CC02FFBF922A0AFB5BC038081C3E1221A60DC2EDE + 54002CC5A20F79B3D5BD338B4F7561883C8B7406C0592C1879D188852AC35FD4 + 07805D4E1C80CBA21A8B6C5E6C739C053A2FC61127DF0B775EBC7318FD00762B + F045805B0B906B009C41AF170AB3BC90981711BDB068E8DB5558FEC6C26559AC + C435A2172CAD0D474F06FC8D05D080C0B84124CD62E8A9B09459FFC600DC76E5 + 85D5B5C7D4FF5B9FC4026E5786C2D427DC4C7EC0DEA2BC5F208BC31BDED2B1B1 + 128A05E2EAFE99C560DC3EAA2F2C8CEC92D296C0EC57B84969802FF0977D7809 + 2DD2805EA02FF037EFE9BB19A91CC88B056FB870A63E9017785B21EE10D0FDA8 + 58EB5A405A420030750C6FC9330036DC75BEE1AE459E453AC302E0EDF6D3250D + 58005C006A03B25698427AEB999BCD362CD23E55FED9CF9AA970E1ACB2B3AA8B + C55AB2FADDA138F0176B3BEFC7497F13E700B5F71B5C2FCF5E7FD0ECB9F52800 + D32CFBC92A2F2C822F3E08E88B45B0C3B0009606799F3733D781C7EC0BCA18EF + C6588BB51FA0A7580802890C87804101992A6C62BFD280C00A816E0041C0DA24 + 5019A804E40B508A056D81BFB8FC059E1AA6220360FA05A21515A81560986306 + C8B3541E00AFC0BFB0FEAD620C05BA0E00A3012F3097D090D2A013195022D2D4 + 717E94E18EBA8E19E06BAB603E4BB837AE60D7F0171952BA8CB15A1E4FB87056 + FF86C03E36E33308268FF200C00A39AF7BC847CAA38C3EA8C7F9C6E277DFED17 + CDDE3B5F35EC676C00BCEFDE37CDFEFBDF36B83F9ED6F869C331F7DE7B11C096 + F6883C54CE61F91C06B37C0FD337F5BBA810B680EC2FE273F0793C7EAC80D973 + 17A05CFA2ADF4394491E73B1FC2DDF653E8ECF1971F2F96E0DFCF9FEF379468C + 03ABEE197D3F01C401B3580E57EB61E0EFFE075F37871E7DAFF38AFBE7F23B01 + 2CC7EF89CFD5F60D505639C7C7EA98FE484BC05E42C361D284FC66C9639FE3BD + 8CA7FE8ECBBED6580AEBF3DE79D14C0181756CE0EF57FFAFFF4F73EB3FFEA766 + FFC3EF02FA06DCE7372111F25F29AEC735B690E237F45F0458F31FE3FF54CBFC + 1246EC452CCD91AF3CFEBB76FB6CEB60602EFF71FEDFB3F5FF1DFFED2A43632C + 82FD9F8FF425AE450FE3DA109E01741D0AF7D1EACB2038DCCAF3020C2F936031 + 7C8DEB14FDF202C99DAADBBADE5C976EAAFC8EC6744FFDEBBA0724BE50B453FD + E0F27E3B16B9E121E16E85BFB7ABEE04F4455340DE733A9E443DCA267891E7D3 + DB513EA73123CAB6C5CB364065F511FD4AAA1FD7F10481770414E6651DF678E7 + 7E41594D7FA43AA74AF9F6D3BA1F7C72459F599FE7BCEA29BEE5E4F9B0F6C542 + 18CBE170FF7C4AF7B58FB937291D7B10D77BCD87D79A2DEF2BFF03DD5B4EF262 + 92EE6F1F0280752FC47DB404E80500A32E1C8EF4FB179B6D15FC7665805B04F4 + D5BD1997D0EF9F6DC1EFC6B73F8E70EBFB6742DCF701C65B75BFC6A238443BF6 + 140618038A7949AC6EFBB041FDAED37100C01BDEBBA2F379BF99BDF0489FE946 + B3E6CD0BCDFA772E35EBDE641C9A1360117CECE33A77C15389E633E9E533449C + 79CB9AA3C53A78A3E6389B98E7C47C89B91156BE1500576D388E86E74FE102BA + AACDAFF334AB3B970B25F0DB85C0317794487B3E69E53929656E0BE0E585C270 + 01BDF78454E6AC6EE73E81C000E0D5CC81A576CB12C9736D425C42AF535E77DE + 6CC53C19ABE1DA26CFC5FBE6E843E0B70A17D0017A559EEBBB8DE53A2D28061C + 037A3B32E8CDEA9687F56F4758FE223C22E1EA1900BC5ACF4F2B81B300E08803 + 6815DF79B889FD7D01B3D3FA1C53079BA5137A369CD0F3A8B47C47DDF227A06E + 85C015F2AE883E944FB9E4E7403FC7B5CF85AD0AFC45005F402F3208761A0DF6 + FB5559EBFA79582EEB42E100C37E3EED01C0E31B09879FAD4315C67601701F10 + 36B0CDEA963BCC507794966CD815CA796E1B6500DDB49691D7289CEEAE75388F + 17EB97ACD9192FC563FD4B5901C0AA5BD759583729628DA5805F43605B017781 + ADD35686C656BB5EF492F6B9DE4200B8BB86F5AB88BE527FF91821AFF94906BC + 2F93D70BDB7485BF082F7FE1E92FF24BBD50AC376E1EA8AE97E635CC6151C65A + 27F50C0317AFBE3EFBEABD0E79DDD79FB1AFAC9B9FCBFAD7E1E7CBE76671EA1E + AF2FAF2B1F6390D7378E3E0D1F7BF19F09B5EDF49D2FD118F8EEC7B0245FB6A1 + 5936B6B1D9B475B699DE73ACD9397BA459ADFF172093FCB002AE5C2603CE3E1E + D3CD1B25F7D74DE7BEDB63D5BC21E5BC5AA7B7AF9ADFA6894BCB576E6D01F052 + 09001E654B5EC6C5E8AB0A2B5F875DB9FE4200D850B60B50FBF27EA946F539EA + 382F397E0060F7F992BAC3C2BA7798A18E5417AAFE96EA1D50525F1BB4C41A43 + AF7262FE1534F4C3B6D20FBC555FBDD72BFF991752D425EC5EECFECE0070B901 + 94D0F19C9FCB46A9EFC6BC90FAFAF8398AF1F11DC4F730F84C1675F2319DD795 + CB73BDBE3E90F38977DF5A737FB94DCE7B99725F2803E0127600F0780FF0ED28 + 266DC4BB93446B01389CCB62A259D38E7B020A18EDC6DB3E6A7DC3D45C772151 + AFAF8F0C639D17F19E49FAD064BD6792EEB65D0D03DFAEF4D9707FBD5A0F0E15 + E28E04C091D6C34D85BCC56D51D1B2D5F48F4ABAB500AEA11F98AC287358E532 + 3F408D55E0DB4D0F0B97D0650F608B3CCB0018656BDE3EF53E94268D6AD3978F + DA76805E4BE90C80BBEAC2DD9CEEC6ADF6C19F877CD2356FF0E03F80BF06BF39 + 9FB00B7CDDDEE9E5B17871A868FBE1D0CA89027F5101BFBC493F881BEC62C50B + 04462B76202C7E59783916750C7B9DCEA12170805FA511F0D72A79656F60EF0F + DC9517815810F2A214712F5A59E479D1A90B77F3C2552C52D53C97A3D8A717EB + 88547FF59CCAEAE259585154E10EDAFBAA7961CE65E47941AE2BEAB2E09701B0 + 17060D8129775E86BC59DD32A7BB6A01705D8CCC0078E31B6571D379B15879F4 + 64C05FAC6102004B80DF580C95D61E2BD6335E342D8BB2036508DC025E206D05 + BF86BF79F1352C7F03F61619FE1A0863050C08DE48DE3B67D54785C084580AD5 + 85675B22B50BD100E0F707C017CBDFB0FE05E87E70B9D956C1EFF6F7AF34DEBF + 17E81BD0B602E08813D63686B6282C812B0036B075B981AF01700B6E956FA0DB + F65361B0410065C870C07D11D207FBF886F52F16BC58930116CEDF0D6B5A2C75 + 77B17FE79507018127943F79E1B366E2CCBD66EBA95BCD36D59FBA044079D84C + 5D50F919F579EE96E277CA1E9A9770C57C47FD291E56C0F78B6B662C7C2F61E5 + 7B3FACF3E6B0B49380C2D33A1E90372C7E25A0AFC3A138ED004537B016A4DD67 + D11E17AEA10A800144B38031D50F5D571AF8751B00FAA875090D8403DE114E6A + 0C3B55178005FC654F5A00B02D81E794761CA06A90656B46C3ACDD15B8E1EA36 + 5CF4DED2786E0F206F019005BA1AAC9207342CE07018F002D50C4AF73FF826E0 + 2996B480C300AC354486B14EC7DEB5090063D1192EA101AEE136B980CAAE9C4F + E8711A327B4CA47DFC00B2F78BD5AFE3ED381432DEB0FEA50C885D81AA152017 + ABE4DB5FB60018ABDF80B112F0973A763D1DA0F97E81CD599C9FE27679F05938 + D700E0D9DB8F5456602D60957E0CCF3DE6BD77B1022EC7A67F8FCFFD53AF5807 + 97EFB2F435388F06C08E73DE0CF903D0D672EA9736CFE37CEC7BF06DC071CE15 + E9FD9F7F1BC78D73A26396CF55E07671B95D3E23DF4DD993B980688F63F64EF9 + CEE2580183F53BC02A98DF91C61EEE9FF93F90A7F43EFAD5F70308E6B7C19ED6 + 5804871B6CACAF359669F5F3C637FFD09CFBE7FFB679E7DB7FA3E3AAAF7ADE0A + 14D77956BB5DEC097CBD8064F65E9EE5F357003CCDFF41F9FC6F8A7BE8F29F89 + FF2652595C1B1402A181BF88BD8D234E5DA47806C5C840384031E512D7315C43 + CF5CC6125863509C6B5106C0845C9B02025FD4754BC2BB00D7A6595D5F765DC1 + D2F76E84C0DFC9F337D4EE96AE63B7AB74DDBB785B523B6030EEEFA59DBAF6B1 + BF306ADDEB4BC5757EB1E6E51A4C1EAEA0A967584C7AB7C63B7BE941D4DBF291 + AEEDB5AEF72D2634042EF0B75CCB711D5DAEF5B87C2E2EFEC98F7BCA29AEFF65 + AFF66D9FE83E73E66AB3E3ACEA7E0AC4BDA87BC8A500BE805FA02F715C49E36A + 7ADBC7B8872EF792AD1F5ED5FDEC4A88F8D693BA8F851BE9010006F6027ABB56 + C0910F30561E7BE9F3A255BEFF027DF37EFC088B60F6052EF772EEED58FD9E0E + 4BE06D1F9C0D9517BF545EDB449F8461055CE60CB13584E60AE1E543C7DEF0CE + C566F59BE7754EEE36E77FFCF7CD837FF77F6FDEFAECC766CD897305FEE2D943 + F7EDF5273E69D61ED7FC25DC4A63F10B081E4060C49C0531BF61EE12790A0BC8 + 7D3FE64A16E9754735DF8AFC0FA4E117E9723C94E6617D73B998BF55E06B6508 + 3C3477AC1E6532005EA5392756BD6D1DC900B8C05F09181CF352B5933C475D55 + 41AFD585BFA8CD5718507684DAF9347367CDC13D17A78DE5B9BC5FC8B400BA01 + 752BD8755D43DFAEA25F8E47BFBB8EE8D98167089E39FA017006BC7D791696C0 + CBB0E8551CA81C16C4784202D876AD7469A37EC6761C6C96A8CD32A5C776AA7D + EC03BCAF5931C9BEBEFBA35D0060D58B3D85C3DDB3FAAAF93C9FE567BA216D57 + 99549EE98893A767BE047C9D8EBCED3AB6EAE0F1C932D4CD321C763A83E19257 + 9F53A90BF8D5B36D7E29BA5F7A664E10D77A15009CE34EF741DDAE96F400E0A1 + B25F0280574F364B2520B00170EC039CD657DA35961EF8DB05BB5E17727E5E2B + 223DBE612AD46D67B97E6E3794BF46EAAC2DA1BC7EE535AC5F53DDFE5BB14E87 + EAFADF6234AAAEE16F0B80EBBA62A807006777D0F3C51A276B9DACAB5AF3D741 + 47A9AFCFBE7AAF4B794D7831F9B9AC7F1D7EBE86CFCD429A7FAC9FABC58EAF3B + 86BE3AA3D4B659814A7A1979CB3604005DBB6167B379624FB379FBEE66256BAC + 89C5441C589AD299C5BCAA72DFDD3CE71BE0865E523F97B5798CB7E6E1DE3A18 + 45CD5BB97A7B33AEDF6EF4BD645D33AE7260F768004CBF0BA805BF395FE93EEE + 668D02A8AF0C5517A1517D8E3ACECB8EEFFEACBE3ABDFACF1000B7D017BFD5F6 + 6F1D7A9513F3AFA0793F72947FC0E987FC1BCB7FDC85147509F385CE1037CAA4 + 5CF632BD06003CFA22CDC5787003C837D2C10D6BA05CF7F724C61D638CEF61FE + B8519E2838AFAF1F83D65CCF75FBFAE8B6E9D34265A3E463B9FF5100182D0500 + E709598FD81338E209E6A298407AA298F287E47269DE1B87E4D57416909430F7 + 93CB5CFE3251AFDB07792F03C079829E27EAB8A8CEFDE7B60BA91706070056BF + 80DC0A7A472A1E54F4F05341AF0500660F9B223DF4007EAD0477AD78303224AE + 72991FA0C62AE4EDA6875500B043BB7D266ECB5F9767004CBC0B84FB1F4A07CA + 75BB6D162CD3836F0B8035B616F62690CBC332797E70EE93012F71B71BA51608 + 572D04800D79493BDE0780EDFAD9F0776585BF5D005C20308B24C5B5734058E2 + DEA357F5FA0030A07790EFB680DD7E00EC340B3AA300B0F7FAB258F0F1C25496 + 17ACDA72D223644B8576C14A71C35F6B60CD7022F64F5BCE5E67EC7D56AD27DA + C537C9F10C7F9117EEBC585716E80AE005F6C6C2DFE1F99621398FC5C36EB9EB + 18F2E636CEB30AFC1D5E9C4406C02C603A2FEA60011CAE0F4F05080602B700F8 + C47C00DC95A1AF6130794E3B6F5E7E05BF5D792FC1B29F20AE1F95FF168BC303 + B7938456405F2F1E137F0F0B265C591615005CC0EED6F78B0530F0775B85BC59 + B6F6B5C2EA57F9EC018C9B6714FB042B9D61ADD32CE89306FEB278EF45FEAE0C + 80DDC640C0B017511600A0E661F9BBED63D5070057303171B640E062018CE5DB + 676111C7BEBB3BD87FF7ECFD66EB27779AAD4006ACD62E1680429B1DB1DF6F05 + 1F15FEC63E986A6FABBB002D8A035A627F5E2CF580B715E4CC5DC7ADEBB3D0DC + F5A721C0AFC302801FB7C0873D4FD9A71457B0405FE04FC0239591173049F1E8 + 1BA004F4BAFD2C0014D6BFC06DDC3F1BD401CF0E3FFEBE39F8F9B761F9083805 + 9622E2A8EC030C347BAE1050AC6304E4027A15E804FCDD7FFFABC82B7BB4021F + BB907700271D2FE58405721AA0DAEA96F8BE07DF0424C4DA9210301850587103 + 5A801CB030B7C7DA771FF9E4013C1516A809B01D8C87B8812621E5E4176859FA + 76BFC8C7337C2DF117E1B2B8A4BF6EF64861F1ABBA404EE0269F83F38C65EBC1 + 87DF95FAEAC37B136389BBFFB36AF5ABDFC3CCCDF2D90C62398E8F4B5B8BBE38 + 7EFE1CE51C3FD378740E3E2B9F893A58D63246F71902FA027FEF299F5079AEE3 + 6396CF4D5F80D9722C8E51CE5139AECFA7F3630CFA5DE4F34A88F67FAE738185 + AF8EC3B90A20FC99CE9DB4072B6789E3F2F901CFFC46E8A7580117F85B8E5DFA + 236EE81CDF69CD9BA50EE3521FF19B0DC0AB71497B353660F36E7E2B12E9FDF7 + 94AF3E6775FC399DAF59FDA6E73446D23BF55F9DBCA6FF99BE27836B7E675854 + B31730FF83F2A201FD732CA56FEA7FCE4B19FACF049C05D846BDC17FA8EC656C + 0B64A5553E0F06A732FFB7F37F3DEAD5B42D8371013D5B2D80E365947A4D427E + 69846BD1E4E5028177D5BD800BDCBD150078FA2A79EC417E2BF276A9EE74E4F3 + 928BAEB31780BE5C1B15D7F52FF6FABDA0EB9E04B0E51A9B650F0CC4A903EC9D + B9A8710176CF62295C202F00986B75BCB0734663519EEBEC505BAEE1BE17ECC4 + 8D74B8790602732FD038CEEA5A8DCB7EA5B9FEC70B3F9FEAD86754E78CEE0967 + AEC73EEE58050370B10006FA12B297F0CE33F45DD213C0E0B847018C712F5D5E + 6002106FFF98B0C0DF0C80AD0C8089878BE8D8DB172BDDC1BD98D0F0B7DC872F + 346CD1B0ED032C80CF04F475381069DDE7DF2D7BF77B1B87E8275EF662DEF049 + ECEBBBE1ADB3D2F980BF58F90280779DFDACF9EC5FFEAFCD1FFFD3FFB739F3F5 + BF6DB6E89EBE5E7536BF7BA9D9FEDE65CD35D4F60DBC91A8FFB798A7144B60BF + A84698E7239ECB50B6EE48B1EA65AEB449ED36BF8975F0870900938F37958F42 + C5B30A2FD07D182EA0CB1EC11F681EA5395605C0F6F892E77321C02F7B004758 + E3005FE68D0A234D5B95A170F3CCB62128E203300C00F64B8659B4719DD87284 + F9709201709E571B081BCA7ACE4C08F835C00D705BDB782EEE39BBD5CEE54953 + A7CA20983287D610F44D7DB56354FE1A95A3D52F01C0005EC72DCA5D9FF432F6 + F0DDA9B25DA51FAC7C81B5E1A679F250C0E1953C9F786C0AB15E5E023C9EAC96 + C3D1978E852B6783DE1DEA37003096C5A54FF2BDA54F56FB1C978405B0216F1F + FC45E1265A9A0F748745FE42D6C16D3D695CCA5B302DA80A6EFB64106CC8FB32 + 2DA66E17F6F6E52F0100D7F509AF4578ADC27984B94E5ED7B0F56F5E0BC19B1A + EB29F300B0EA18DC2E56DDB5A23E009CEB7495D795426BA49EF5A5BC7EE535AC + 9F2BD69DBA79DDFEE7A903737F8E320046B1A6A8FC5853C44A38AD5D765D42E7 + B5CC22D637A9CBBAAA3568FF32CDEFEFD5D7577F89BAEBC2DDFC9CD72DEB5F87 + 9FAFE173B390E61FEBE76AB1E3CBC7EF2B7F99A2ED8A2AE2CA0302C71EC0D2D8 + CAFA1B515EFC4ECC5910F0B4B299C5329A51729F7DF9391E8016F5D46FCB6B59 + 2BE731DE5A0F001C1018D8ABF3BD69CB6C883816C0CB5467CC7DF68AB2AC4E39 + C71C1A4BADD3C7DDAC5100F595A1EA2234AACF51C7E9C9EFDD0B7854BF23F59F + 19008EF410F4CD7A9513F3AFA0F6C7FA73D4F327F81595FFE8A31475093B17B9 + 9F0D805F83FA2ED4E5023E7C03E8DE480737AD8595DBFC6B8871F78D27E7217F + 3EA7DDD6F5897701B0CB5CDE2DEBB6EFCA65AFAA85FB505E40DD2243DE3EB593 + 34AB037A6302E9BC4ED9501E61553BF1AC71CA9D6731511DEAA7CA65DDFAA394 + 21AF459A7CE2B9CFA8D73731AF794BD70D03608FC96D5F4563480F1D3C7C2C04 + 805BD02BC5BEBFE12EBA23EA55C5834FE7A127AB2D27EC94FB216BAC42DE9CD7 + 9501AFAD7C01C0285B02C7C3A7EA2C59AFBE2AF4ED02601E3E1DCFE9A187D3A4 + 5C2FA7B3DA321D1FB80B000E985B412C9035BBEFA2CC1018881B75935AA8ABB2 + 516AC1ADEBD2F702F262415E3C707B972103E0F1AD0AB1004EF017D9F2D700D8 + FBF85ACB5904517EECF31B70F798F20BFC259E0170C9B71BE702805756081CA2 + 8E42F256CD9485A5A83BDD01BF2CAE10CE0C1683F2C24F2BE579812A16A0EA82 + 5500DCBA504518F949B9CCD0170D0160A0AEE2C05F002F0B675DEB0CF232F8F5 + 025B06C005FE160BDF6CF56B7911B09B4708C8CD75723D97E5B455F6F51DB648 + 31ECB50B685BAF100E01E0373E2EE0B7C2E02E00264ED85D4465FFDEE2D299C5 + D6B2986AAB5F420362E2B8774419F42203605C3E87DB67F2DE3ED3AC3FC1E230 + 0BCBB888BC1C6E223300CE32002E10B880E08D582D7D505C3987C5EF7BEC6178 + A9D9A2781700B38FAF412F7183E080BDE455284B886CC1EB3471CB0BFCE4131A + EC22B7E9D625DE578734E061EBA96B0110D8EF17B7A21358A5E106F57CB1DA05 + D00281775EFAAC9938772F00F0D485CF95FF30040006E616E1EA597D9CD3D814 + 0640B9FC200470A14FA0CB0C6E936BBF809980B6009BEB8F03FCEE01F455006C + CBDF0C8067AF29CEBEB6D4BFF938DCD662AD080032FCC90029C3A1004E0023EA + 4B5823EEBCAACF80F5B1FA7AEFBBBF3557FECBFF5373F2A7FF22801F96C058D0 + 024CB16EC44D3171E024408E32002F2E78816901E324A0DB3E601996B3EA17CB + CB0080C0B87B0060E0E0209D812160D1D6A8C5E2B3005C5B000307016EB8DDC5 + 8A16980AA08CBA0056858C9DD0A21DA1012D7DBA9E41A4C76728E97101380769 + 34185706C0F45D40292120F6FBE6003016CB56E587DB6769EFE71A83B4EFE1B7 + 517EF8F10FCD91273F4608C0DD774FF50200BF680E3CF86E0800FB33F87394F1 + 0F64080CD8359CF5F819FB81CFF93CE5F30180CB580700D87D029E033EEB78FE + 6C915F8F6B00ECF3620B69CEA18F4B19E7D3E715F16280CFABCF3BD096F1722E + F6EA5C71BE76730EEEA89FCFBF6B0E3CFA21F2383E6EA9631FE5DB00E562855C + 2C91CB78182F21C7643CC8C7220F91DEAB31C46F13300B3CAEFF07606D01C3EA + 53FDEFA30D509763F17BD1B9E5BBDC75B358FAC66FB3FE0EF84DB2A772B8D406 + BC4ABC20B13B00337D2A7DF3F3F8AF18001BFC3A8D1B68422C81FD9FCEFFE772 + 9DE801C0B58ED3AE431E69AE317E39C42FA11066001C2EE35567F2AAAE7397EF + 363B75FD9ABA724FD7BE3BCD8E8B3775BDC22A58D730953B0E109EA99AC20218 + 0F085C03CFEAFAAB6B2900174D47C85EBF05E822F26DED4B1A000CFC05021B16 + 037C91EB859B7EAEE78A3BED17797CDD2F0098FD7F79E987EBBEEA295DA4766A + 5F0030AEFBD5EF595C4EEB9ACD98CFDC68369FBC18AE9F01C0A805C000E18F75 + AFFA58F7A8D384DCBBAEEA9E05002E56C3910F00AE02266FFE7018FE66184C18 + 56C01290B7C0DE8125306A5D3957005CAC7D4B08F8F5FEBFB886DEF6C179D52B + 2F7079EFE04DB892C62A188F206F017FD9F7F76CB35EF905005F8CBD7E81E36F + DCFFAEF9E08BBF36FBAF3E6936E97EBEEEADB3CDDAE39A8B28DC1A56C4C0641D + F36D5E5263DE320C809993E0A564EBBB3A7E9DB71400AC7A478BABE70C80F19C + 52AC7F0B00267FB3FA3508A67E0B80EBBC8BB918733403E03C7F634E17798415 + F016C8ABF923F3C85AB6F650696B319FF4D6210174B1F4AD7299457FCC41F3BC + 95B96EB87746FBDE6ED657190423CA5A00CCFC3BCDA5891B021352CFC0D8F3F3 + 989757319FC785731F000E2B60956500EC76DD747E2E08002CD90AD8DBC818EC + 5A19FAE67CD747A4632F60B55DB1AB5A0007CC9576E8F949F1657A16F258974D + 1D6AC674EC716999CAC6D436C630C5F10796BE61313C813570C90300DBFAD72F + F5869570959FDB909FE3E25910B80B04AE00B80F028F29EF657B005B19F8F601 + E0C897EC9DCB5B25CD87C13C2FEB597E8BC2047DB33200EE9619DC662D0618BB + 7D06BE88B216FE76AC7F515EB718DBA8BE6A1EE9284B6B1B783A5BAAF2B64CFD + B146C2BACA3C008C05F0FAB2EE03BCED82DCBE785E1F229D01702EEB8A3EACA1 + B235525A5FEAAE33597D75162BD69EBA79DDFEFB9461EE6254D60A4B48BA0B80 + 515B2F00F09666E978599FEC0260346C0DDC5D37CD6B9B8BD3A0AF413FBF85BC + 766CF595E5BCF965845E7FFF3534FF583F57658CC39CA04F1CB72FFF5504F445 + 11577FF11B3010A6FFB10DA58EF3E02D663309747679CCAB6831ED5BBEE378A7 + 4DE40D41D7AACEF888037F71F94C7AC5AA6DCDAE9923CDEC6EDDBBB996280F00 + 1C21DF435F9F2F533A2F25F4F87BB85BD62880FA4A5075117AD5E3F4E4B70038 + 97B9DF6EFD9EF66D7E87A58E5406B2BFB57A07344ABDF0178D3809BF072DFB19 + 3FF2210DFE94AF43FEF32EA4A84B582F66215BF1469994CB7E07F245365FF8F3 + 8D20E7FF1A1AD517F98BB98177DBF4A9AF3CB7B375F02F9930F44D38DC7F2E47 + B6DA3550B672DB85D48E3BF53F10932A4DBCEAE46B284CCA13BAD60298C9609A + 340E4D202BEC1D9A58D6BC5E1900D73D809127A8BDF5935C3F2B4F6E5D671400 + 761DD7232FF2D3043D4FCE23BDAED6A9FD943D7C076D1754C702380070B81D92 + EA43499F0C79898F61019C0070D90B58F928DC43D7079FAA3E804B5ECE771AE5 + 7659B97DD1C002D8D6BE58FF0280C95BBA9E07333D38F280A9B200C015CE7621 + 2DCAF95DE57AA8FB103BAA3EF1A50A87C0AE1FD8F5409E65B86B78EBB4D5572F + 973B2FA4785BBF2BCA3B798CA74F2E5F2C0036D0255C3E7934F20C86CB5EBDC7 + 87EA101A00A301001EE4459BA468337D22E0EFEAD93703E0027D57DBDA98C593 + E9027FF3424F4EB308443B1698623149F2A2D2EA3D6F0E2D2EB5F92C4A75E47C + 2F68A1B0024E715CEE0176EDE29905B4F587DE0B08EC85342B5BFE5AD437300E + 2B8D036FB7C0D60B82619DC1221D0B7D7541D0A037435DCB6D89DBBD20C2D224 + 74F4C366FDF16CED6BD83B881B0067AD575BA0EFE60480371FFF58E9C11EBF01 + 7A130076DEC613A70300036F71D7CC422A6D5878CD2A0BAD679BEDEF9F6FB6BD + 77AE05BFEC134818FB0106002E0A201C8BB705006F7A9B8566E0ED60E1D98BCE + ADC80FD7CFAA5317AE374778A9D9F2E1E5669BC26DD50AD816BF5843110FD00B + DCAD1018F80AE045405F5BEDDA2A97305C4AA73432F08D457BC06D0FE8B59C9F + 812FF9A45D4E186E422B3C00FC06E4BD58206D80DF4BC55533B01637CA73379E + 84CBE71D67EF373BCF3D6870D5BCFBFAB300B213E7EFC41EC19317EF363B2FDD + 53FA56B850651F4D200B9675F469001C7B772A0F3033838BD5EBD562EFDA9366 + F78D2F2A0046CF22AFEC03FCA499BBF6740088B17EAD500850D4C2A00A935D46 + 18600810A4FC70391B16B92ABBFD34E0177BB39207A03BF9D33F3537FEC3FFA5 + F9E42FFF2180DB0EC679EB59C3BEAC76718BC5638185B8617E16ED81C77315F2 + D9156F00C07B80B90AFF14B70CEF00720682585A46BDBAF7AC6124A1016E1BA7 + 0C18A972F28BE5AFC605A8942837982D50B0B4A73F42835BD2C0C132A6022A01 + D7054C560B5789B0A8B471BF5811473F4A1F7850A029D6B37BEF7F13F03260E6 + FDAF9B39B599E17C612DFBF05BE99B66778060B591B0DA3DFCE88706701CAE9F + 5B17CCFA3C52F429EDFFEC9B0A780BB83D082055BA7C8E6C094CDB2F63EC0380 + FD85D25F689C5F49AACBF9ABE7C9E787F43E8DF7E0E7DFC758F279433E6E7CF6 + C8070617D86A6B5B8EC7F1397FF97B27EE34E3A11EE734DC39AB6FCE0556B601 + CDF599D9FB987378E4E94F05F2AB0ED6C1EC15EC310F7F8FE51C31AE0284CBB1 + FCDDE1067A8FF2E2B726316E7EABC300184B77FD9702DA16B7D058F14EF2BF53 + DF071FFED08EC1E78B310400065AF33FA14F9D0FFAA13FFA99BBCD4B10FCEF1E + 941726F45F445107F7E6FA7FF2DF0CF1FF55FE1ECAF8EF2A8D35BFFFE34E5316 + 7097EB88F20226132A6D2F00C800989074B8879702F85E2E16C15CA780C033B8 + 8BBEFEA0D909D0BD723FD2580E4F8735F05DA5EF05F89DBA74BBC132388030F9 + 57941FEE9F79E905A8CC75B458FF027367CEABAFF3BAA6B2F7EFB9028581BD33 + C93A785785C21670362C7E759DCE795CBBEDB181325B1163056CB80B0446E1EA + B9EE05CCDEC0E4E10E7A4AFD900E205CF71DDEFE89EE57D2968F751F03009F1E + C0DF29207158FE62E56BE9FE754A753FD2BDF0E4C51600037DB300C09B3ED0BD + B402E0B0FAE5A5AA0A7EAD4D84BADF026FB9EF6ED7BD15957D7CB91FEBFE5CF7 + FAE51E0FEC2DFB01736FFF24EEF9DB3F3CDF4CE0C69A2D1AE23E0FB455FA5DFA + E445B173710CAC8E37EA381BDF2D1E3D36BD7F45E19566DDDB179A356F9E6BD6 + 2B0400AF7FFB5C00E08D6F7E5AE6189A73AC03CCBEA5B9CA9BC502982D2D62DE + 122FA5D9730916C7CC5D3E2DF39AA39A27319F52FD4DA8C2DD02840BF82D16C0 + AA03E4AD2FDA790FE0759A9311D23EDA0181993B6AEEE6F95AE429CDDCCF20D8 + 6AE16D9A0F76EB38EDBA2D0CAE61C4E983B9E8FE01005E13E05773D17D92E201 + 7F55DE05C0A403022BEE79B3E7CA5810CF9B4313A71EF95519DC0EEDE14B1E73 + 7B8BF9BDF2E2992029CFFFB302264F1FE905C006BA7D02EC06DC4DF101ECAD56 + C13B077958F5A2B100C047020263F1BB64EA50B354C75C3673BC590A00567D3C + 1995F687D49EE7AB01043600CEA03703E0F19ADF7D8E2B757029AD6739C9B0D7 + 00D83078E9D63DCDD22DBCB03C00C0005DBB7CEEBA7E76B9C32EFC8D7C8967E5 + 9701E071ACC8B614689BD5CD1B55272BD7EBC2DD2CB73710767E06C04B01B669 + FD81785EA318AB003897B345D612D64D6ABBF14D1A8F449C7CBCA80DAC7E4B3A + A43EC7EABA0FEA02E03EE5B5A15C2F03E06E5DE7E574AB35525D5B7A1DF27A57 + 5EF7EACBCF729D652B2583DB1A8E52BB86D8B3A6885C2F03E0D0F86685492B46 + 01E0EE5AE8E2D745B3E6F7F3FAE4E3F4ADC9E6B29C9E2FCA7F6DF51DE7E5EA1B + 63F95CF379C16B53E524E58500C65040F01816C0D50A38EAB15F31E7169ED201 + A37D4C66B1EA6B9FF36CB16B392FD7CBE31802B029DFA22D009810D7CF93BB0E + 35D3B3BA7772EDA08EDAF23929EFF6B328E5E34758C7D7C7DEB21602A57DF93F + 57015E7BFA5CCCF1BB6D9D5E404BD1C8FA3D0CB54F7D60F6D7D6B2AA882F5B13 + 61EF604689FABDF017E9C3FA24FCDEF48B017047F50F107FA4AAF64F91D5D7B6 + 47F9CF6B75F39DEE5ED85CD66B1DFCAF2C5F688B7483E682D323DF18F2CDA2AF + DECB34AA5DB7EF515A4C9B5CDE979F01B0DBE77857CEEFF69BEBB77D77CAD17C + 00DC3FEE7C33F777427CD0B7BE2F85CBEADB754523007067C29601704C08C963 + 52C8240FB04BBECB921633B1CC72FD9868D649705FBDAE723BA7BB75DC1FEAEB + 3B9705A01D353997D8B7977A45B4A3FD701F167D457F6A37B6BA02DF9A973578 + 101928BF99EAB0EC112CE90167A86EA8D45F2AF901C90F49F901098DCA43B9ED + 427D00800D78110F98006004F03514B6BA9016E587D0BE722BD7EB7B88CD7939 + 9FB6DEF7D70038C403BB1EDE5B60DB515F590B741537EC6DFBAB72B92D8C43E4 + 6575F2184B4E3BCFF983F2836A7BA8593EC1DBE703081CE96AF96B985B00F0B0 + 6B68C3DE6817907720B773D882DF5A3EB4EF2F9A2A16C101816700C2EA231673 + 6A485E5577F128167F28AB0B4311528785A6BA1845E87C97393FD7CB8B5A59EC + 7716D60C1500037EBDEF6F584E1C280B6906BF19F86619FE1A188745C7810100 + F6C25D06C079618F3A5DF89BC502E226A02E8B9416D62915FC96C5C962B5626B + 15C737C422E8C900C506C0B88BC6DA1700BCE10D2C81B16AF9B8D9F4E6C07573 + 16791BDF429F44080046EBDFF4C26A75F32C65F81B7A8FFD018BC52FD0D720B8 + EC1548BAB88ADC14205865B1C87C2100306171057DB102602942E5BDAFB2F70B + F4DDC262735832B1908DF5520959D0DE8EFB4B406E05BEE1EAB9A62D406C0B80 + 2BD845018BA59C36147668E09B0170CE37FC35E47588DA72F2A4012018000400 + F04E00ED857BAD6B544311400AFB801EB8F7BC390890BBF3A299B9F6B499BCF4 + 793371FE7EB30377A5ECA5790D68F33440CCE4255CA84A17EE46BF05AC94BD37 + 672E1537ACE172155003FC0D080C007EDCECBEF1B4D98B852D8009D8AAF4CC55 + C06E81D0E4EDADFB9366C80BDCD975F5F3562D14AAB2B521D6B853371E363B75 + 4CC0ADA1A741D8C187DF34EF7CF3B7E6AD177F51FA8B00C084E1E2F6CEF3B08C + 245D54DA22DCE8625569706AC8D7C254A01BB00F1077FF79C062E031FBB4CEDD + 7D56CB8BF604102CDA87252B0033D2405F8D436D19CFFE47DF35FB81A9F7BE0A + 77D0B377BF2CF0F0732C480BE405D061AD6C6068306C7018AE861F50AECF8045 + F2BD2FEAF1549FCFC0183E074A9671C478A24DB1888DFE25D240DC430FBF0F70 + 0AB8DCAFF83EE5017FD1ACFAD9FD48ED9E7CDFEC7D4CBE3E8BB49B7ED4EF413E + 8FC68ECBE87D5284FACD61057C006B624209E81B2056ED701F8DF5B05D48C718 + 94C778F90C7C363E03E76CEEAE7E5B00E0872F9A438F555763E19CC6F7CA77AC + F3C4E703641FFAFC8708E31CF1F9742CCA3926C7202F00B4CE3F20D7DF39A035 + 0030E7B0FE160C6051FC26A8A373C16F0DEDD7188B95F4773A17CA7BF4A3CA7F + D0F7A9E3EAB31ED4E7DBAD71F2FD62190C801DC0FF72FE0DA68BDBE832567E67 + FEFCC4FD1B24F48B097E5901300C20E6778C4A9EBE7FD5233DADFF1CDF07BF2B + 2CCF0D7F396FBC1411C74380E73BFA4FF0BFA21FFA0BA8FCB8893D80AF5500AC + FF22B20B6A4030FF51BB848EBD8001BEFC676B1AF7EDE5BFACFFA4F2E37F4F9A + FF7AED8FB8FFFFBE0684D5AF01B0EA043C0E6F03EA5FD7ADD87F3C20AFC68627 + 801B9F3753D774FDBBFE40C7C59218B0ACF6D73F5388BB7A3C23E0FA59D74A69 + 2AE2BC3453E0EFCC15AE9F5C4795E63A5AAD7A11F15D17F0A2A0FABA36469A17 + 6E24BC30A029F6EBE5A51CD23504125B538060E54D9ED3755BE55CC75D2FE0EE + 990279A76B9CFD80D9B7178B5FFA0E187DEE9EEE0DDC4B74BFF8F46E33F1C99D + 66FBE9EBEAE7567340E7F9A0CEF7AEF3F43D0C80274E037981BFBA6705FCD57D + 50DA760A0B60F28A00BF8E634D0C000604B3C77017FCB6A28EEEBB5B90D2DB71 + 15FD212F57A99DF20AB43D13DAC4BD9DFB3C2F7B499B745F475BDED77D9AFB35 + 2F6DE93E0EE4DDF47605CAEA67E2FDFAC256945F0800CCDEFE1BDE655B87CBCD + FAB72EC6DEBFB87EA66C83DAB347F1261DB3EC217CBA59C75C07EB5FAC780304 + 6B7EC37C25E635BCDC8687135E66A3FE99C8630EB4E1F847ED5CA9C0DF62D16B + 00CC5C6ABDF2D61E7D3F5C435BB888A6DE600FE1D23E3CB31C79AFD9A8FA9EBB + 31975B73B0CCFF3C3744CC01FB94E78816F347CF210901BF2BF69E68564AE126 + 3A79A889B8E6BE6B81BBB899AEF0365B04E73930F500C6EC37CC96282BF33C5A + 72DB3C9F8E72CDBD29474330B7CEDBA993E7EE11E7B8EA87BA018B77E9598539 + 7F929F0D223E5D60AF01F06AE25240E00A700B8C2DF180BD84E4037BBB52BEDD + 41935E3EA5F494C6218DAB4FE0EFD8AE63CD26FD56E63EB9D16CD36F6DE5BE77 + 9A25BB8E2A5FCF27537ACEE185DB8903EA7F007F03FAEA39CCE037CB654EFBF9 + CD32240E80AC31D9123883E016004B19E066F0CB9EC08E039497017C81C8A43B + F0D71079E9960A927936D6B37006C17E4E8E67E82D05DE2EDFAA7405B3C820B7 + 2B206D5F7E5686BA7DCAF5BAA27C090038AD4D789DA2BB2EE170ACC260F6FA75 + 3979CB37E3898C75089593278DAF57FDBAAED20260E2751F60D405BADD74CECF + E5B14E54EB39BD90DABA6B5537AD1F595E5B7A1575DBE5F5AE8594DB74B50C55 + 703B144F629D90B05D4F1CEFAC334AAEC33A621C97F5C3B65CF503E6D6B5536B + 05F99B03025BDDB5D0C17A342AE55DE5FABF54D127A1C6CADA6AACAFD663E73A + DD38615EEFCD65CECFED8655D7736BB890BC269C355CA71CEB55345853EE6F9B + CB878FF57A15DF435DF736008D7D81DB3CD583A5BC84E3642EB318E576B1F76E + EA0BC538AAA24E8A6731960869475CA1AD95733DB78F7C69FDC6A966E3A6E992 + C7E759B2AEEDDF637825CD3B2FB59F3EF6966528DACDCBE9D7A9BE63BD6C3CA4 + 5F2200F0B2A5251C2E2B8CB5AB7EB6DA53F1D7D6CF05C06EFF770B8051FB63FD + 85F28F5FFA5707C092CBFE1E00F0BC8B7F8C5917A27A63187D435BB8ECD7521E + 1BE9BE63E6F26E7E8412EDFAD46D839CDFED977C43DDBE6376FB1CC4E78F6D90 + 5FE4B6E56638288FBD1098A4901F4A13B3AC0E00468309DB6092D7BA8DAEF5DB + C9DC2B8849658EF7A96F32DCEDC3CAF95D511EB075441FEE3FEAF44CDA9DD705 + C0CBD6D0BE8EA396B9BF16F0125FA3386DEA385AB16F6FB5E81D82BA7A38413C + A858B92CD7751EF0371E90E20DD881FBE7026C071A95176DEBC353565FFD22DE + AA1D406004ECEDA60D83A33E61557E001D95DF551FE8CD79399FBE0C800D8103 + 04D730CB50B7AB6E7EAE6BF0EB740B80556769150FFE79B1C0F2A200712F2A10 + 76DF301F6EAF3289D03200CE3097D05038C35FE2E3136A57AD85737906C0B87D + 5E3373A2EDD3F0778CF6B4A32FFAC43D745DC85931A37E580092BC6894178EBC + 781461150B495E3C62D1C90B59C45D279779618A3016B6EAA2552E8B34F591CA + 5930B3F5AF17D0DA45B49A9E077F59305308F4451B0EBF1F5ACB3E6E87062E9C + BD58E7C5BC9CA6BC0B7C912D47583CB40AFC25BF2C4E7AC1D100D8E0B78492C2 + 752C4E2A4E48BA0060D57DE3E3101630EBA4F52758282DD6BC06BFB6EEC53277 + E3DB8A4B86BF2F03C058E4008037B3302BD196D000182BA1D652E883B3CDC6F7 + B0083EA7FA17D5077B0C16008C1BE8ED1F5CA90BC31762B19805E758948E456B + 602F007810028403069F64AFC3819BE7B0F405F80268A516005745FCE362A56B + 209C0130791669035D835CC35DCA03EC56017459EC47C4735EB43953DC8C86F5 + 1960010B32E02F166467CBDEBFB82CB505B00514D97DFD7173F0FE57CD3BCFFF + D2BCF3F53F84A5E6F4F5A7CD66F5B1F5FCBD66E6E6B3E600E009F7B4D71FC6DE + BAA18BF7CA1EBBD78AD51E9065F79587CD1E09001C7BFF1AD25461F5B7F7F6B3 + 21C03BB0E42D6E5D01D29157DB10DA85ACAD08891BFA3A1E968D6ABBEBA6D258 + EC56F81BF05371F280C2403F801FEE9E71696B00484819C08BFD780D800DF6F6 + 02CC2AE073BF58F592B7FB339501FA3E7F11E1DC7DF57DEF99A4632B4EBECBF6 + 008271535CEBEF51BCCDFB9CB65F4AAAFBF0DBE6E0D31F9BC34F7F0A888A856D + 01BA05CE7ABF5DF200778C3D003071601D9F0B70F810CBD2E7CD6E20A18E439A + E33066D2FB943EF0A8D4A37EF449BBCFCB3EBEF48F481F7AFC430320653F5BC0 + E57E698FD27B80D20F55F7F177CD8167CA7FFA4333F7A0EC29BB1BA8ACFC23CF + FE10ED6DE1CB71009C073E575FD50DF2FECF0A0C8E63EB981CE7E8B39F020203 + 7E71237D5871C61BE794CFA2F3C8679BD3F9DE03007EF4A239F444637D425FDF + B6E32F9F0B2BDF6FD5CF4FD11F5093E370BC385F8A03D91917FBF616905C60B2 + 43E033DA8F15323038D203584C3EE33BA4FAB43FA0735D00F00F3AFE0FCDFE47 + 7FD0B87F6CE63ED339D371F7EA789CAB69E02AC017B7D540F02AA02FE389F1D7 + CFC177CF678EDF8FBEC339F60CE6F7A4B190A62C7E97F5771A70987405C3F17B + 9600D8E15A9AEF49E7C2FB50F31DF31D910EAB781D8F7C60EE1E003110B8C2DF + C17FB058F9F212C68CF2F9BFD96D7640E35ABFB51AA62DFF5989347D4E2B8EB5 + 3E00382CFFA9CFB5406186C8581173DCF8CF2BE43A0308F6B52400B2E233EA2B + DCD05F2B2FA910EEC202F8DA6761093C734BD70D09284CFE342018ABE06B0F74 + ED012A3F50FBFBA1A9CBBAB60280F1A67041D7E28BC5E381AD8BA725D2EDF595 + B8AE8F4EE3818110400CDC45C05ED2051A17A04C9D1DBA4EC7B55AD7F2B8AE57 + 0B61AEEF405EAEF10192758D0E0BE14F74FF60EF60C0B0EAA0D813F8632C8879 + 1108087CABD97BED7173ED1FFE6373FD1FFF63B347E762EBC7BA377E722D40F0 + D45940304018D7D0DCC780BE174213A755EFE301F8B5353022CD7DD4DA8AE78C + BA077076014DDA56C25B43256D688CD5F04620B0EEF9712FD73D1E6DF9E05CD4 + 617B860DEF9C0DD12F6DB12ADEF0362F8B9D6F263EB8DC4CB057FF7BEAFBBD4B + CDE6772FAA2E6EA0A50A8237B3D7EFDB179A756F7D1ADAA079863D896C7987F9 + 07F316CD6D14DF881B686903D25C25E62E6F62F10B042EC27278237B0E6BAE53 + BC9CE032BA005C5EB01BB8802EF3A7B59A77A1027D4B79A9C37EC19AB70504AE + 0098F41000561F9AEBF11220F33ECFF12CD219F41A0C77E78C96E79694ADA6AC + CE553D87CD2AF355D5D15CD773E4765E5CF34ABEE6C07BEB4B907B3467279F79 + 7495EB7A1E8D625EAEF9B7CBA30E7949AE6BE5F93A73FD0280CB0BA1DD678036 + 3EC5B34805BB520B806BBA0B800D780D7943D5B216B88A553065B15FB084DB67 + C02F56CBCBF48CB25475D6681EFFE9F33F377FFE4FFF4B73F1FB7FD6FCF8D3B0 + 045E8A7B683DDFAC9AD4330DCF56DB06CF6B86C03C8FE5E7B66C21CCF35FDF33 + 9D5500B0FAAB0098D0CA403843DFAC6C0DDC82E3ED1D00BC7977B3644B85BF8E + 57A83CBE69D812D8CFC9F10CBD6500801DCF3037C3DA1C3A9E352ADFE037D7E9 + D6EBE6479BBA36C19A435EA3C879682CB9835EB26EE750BECBD05885BF837514 + D65E24BCA8A95D17E8A21C779AF520D7EBAE13399ED3DDFC3EB5EB4535EC53AC + 35BD44DD7A79BDAB4F8BED9FF5BF97096387301849EB836D5C6A21F1EA34068C + 4996A73AE375EDD462BDD2A1D405C079DDD36BD27D72FD5F2AD66CDB7E2B000E + F7D53E76ADD777DC517984793DB85BC7657DEBF17DCAEBC2D6701DE70F8E91E5 + 71E6710CC6E0BE86FB985FFEFAD5B214AC7C19CB9854F94B949347DDCC6E4670 + 9CB6AF45CA6D080300E7FE539943C7737EC455BFAD431C90DBADDFE99334FBFD + 8EF11D504779ADFBE7DAF695C531EA718AEAB1FAB85B1650743179AF4B0B1CBF + 77CF5FE43684594375D47EE9DAD070BD013BCDEAF2D5505FC55F5B19005BBD83 + E9A8AD8B7AE12FEA9C94DF9BDA1FEB2F947FFC55C0DF659DBC56AEDFEDA323FF + 794729D7EBBBB045794FD9BFB6CA45DFEABBF817B9AC7B33CB5AA8ECD752774C + 7DC7A4AC2FDF7984A8EB92397FCEBE63382FE70F5BF60E1FB39B1771A06DAA33 + 4A2D90EFF6E97894CD87BC7982D6CDF784CD6A2777A8D60FF7D061112CB513CB + 9A9E975FC484B29BEE2A4F4C3D39CD6D5E459E5477FBE81EA76FF2DEA699A4AF + A19FC1041DF0BB5493DE88336E40706D176F74BABD8E1FFB072B2FE06F8C67A7 + F2C8A79C8791223FA0587E63D5CA75ADBEFD7FFDA094E5FCBE723F2421D7ED96 + 0DD27A48EBD1380F94552DF8B54680DE51F98B5186BFF9C196BEEC02DA003887 + 5919E476E16E166DFBEAC4DBE155190067D0EBC5832C2F0EE40505E7E5B2B000 + 06FA02705107002343DD0C7C87D3879BB10A805DE67203606400DC5A024B00DF + D60A58F170079D00F0CA99A365816876B068E4B4F3BCF8E305A3EE62520B73E7 + CA62522C28CD0ECAA25DADBB76CF30007639C7A5CCF09745B2584463818E4536 + A7F7AA8DC270ABC7229AE259B4C30278FDC1779B8D06C08707F0775D5DD0F342 + 1EAE01D7D6C53C2F04F629E0EF1B27A58FCA026200E1B460D842E0BA6049DDB0 + 02563AC0F087B1186A001C0B9B274EA9ECA366FD71EA7D1C8A3DF0D21E7906C0 + 06BB61F9DB01BF86C101753BE017E80BFC0D00FC5E01C06EC3622D79B11760D2 + 960A80B1E661EFDF2D6F5F505FB88ABCD44C7C78B5D971F25A81C0B61C523D03 + E0027EFB5416B3BB003820303037E5674B60606E1F00260F586B80DB4D4F9EBE + D54C294E9EC1EF8E0A86A983D5D7AE33775B085C40C0BD662710D800B882022C + C880C2405FF6FC9D50B8FDCCADD64DB3A104F00257A9FBEF3C6F4E3CF943F3D6 + 977F690E3FFAA9615FD01D2A9BBCF2B0ECF979ED71B353F1894BF795F740F1CF + 8A35DDD5CF03B810D2DFECE5CF9B3D58DFA94FC0CB14F005380B78BDF5B4D903 + E801FC0072687BF349B3173854F3B2556080E30A73628FDDDB0518056002F428 + 6F7705490191B054BC8BE56DB17A1C0260157CCDA9ACB866C6FA11F808602B16 + 95010AEFBF507BEA027D816258CC02F08095C50AB405678C9BBE2B58452DD4AD + 108EF6C4B1084594B710581AAAAB708FD2005380E0DE47DF35C75EFCA579FB87 + 7F6A8E2B0408CF314E8DD1C0D262BC00BC0C6C8903330F3EFE76704C8580DE80 + C01C4F226E00DC8252851900139266DF5F00EA010030AE8B9FFCD8EC03A4EA38 + 06C0FB9FFD1000788FFAE3B304C87EF65373ECF99F2AC805206BFC5807F31D60 + FD2B95E300597F680E3D2E109863018D81BE0877C9845813035D0F3C2A630768 + EFF95C9FEFA13ECFA317016AA913FB14AB9FB09C6E3FCBB7EAE3A7E8D7E78863 + 199A033E0B88555C9F81F3074C062AFB3C9573568E7DF0F1371536738EF47994 + 4F9B833A1700E0B0C0E6B80F75AE2420F0D1E77F6DDEFBF15F9AB7BFFFA766BF + C631A763CEC67759C6C7EFB280F0EF348E321ECAF6A81F7E0301FFEBF769B8CF + 6F89E312C6EF4CE558A1F31B6D5F6240FAEDE23E7BF07BD6EF46FD1AF20604E7 + 7CD434BF3742F6260600F39F8BBD8E2BD4E5BF078CB5F83F520FF81BEEA73586 + 785102C01DFF3D80B0DA61A1CBFF36F5C7FF9CFF37D703AE0BCECFC7F20B1F06 + C09106002B0400FB659300C194E93AE3F49CFA06F6EEBCAAEBDFB5CF742C80F5 + C3883B6FFA2A96C05CD7E8F3F35E006C0B605F470D82C3CAB85E5F092D5F6B01 + C5A4B90E734DC6830265786640406342AED9B157BB2D84712DADEB3AF170135D + 2D83F1F06037D16892FB427D5168EAECBD66E23480180B62F61CBEDB1CB8F9AC + B9F72FFF4368B7AEDF1B4F5E68B69DBEAAF29B3AC6AD66E6C22D85BADF9CD13D + EEE34BCDB653C0DF8B4300180180BD873071F2B0048EB253BA0F7EA47B5E85C0 + 59E44DD4F26E9961F1C00B07E9F22216F5B77C70A905B73B3EBED6CC722FD339 + 0C8B602C8075AFDF1EF05763D11C2003E08D8A6F020ABF7F29DC3EAF03DC62F9 + AB3946999B7CAC794781C01BDFFCB8C1AB885D4F038779516D037394B0383EAB + F90AD0973D801130188BE0028F63AEC47CEA0473A60F9A75005F80AEE6606BB1 + E2655EA53ACCB33CF732F045CCBDF0B03200C01FB40098799DE781C499C79597 + FAD486399DF27299D3B4E903C094A300C19A7FE6F9A95F5CF4FCB4F564A3F92F + F3E1F5FBDE09B7D03107DE5DE6C461F92B0180FDD2A3E7CECCB5635E8DD27C3C + E6E45506C028975B9EABB7AEA0A50C7ADD8FD3C8CF0DECD50BB0B57B675BFF46 + BA825ED40261CB659340D522F2A963001CD6B6538AD39F8E07005EB263BFBEBB + 93CDDDBFFDD7CDFFF8BFFEFF9AA7FFEE7FD46FF35CB344E3C03DF42A3DD36C98 + D17999D4F3CE563DCB015CF5FC65009C9FE70C7FC93700F6F35AE4190EBBDE0E + 954DA8BD416F07003B8EA5AFAD7DEDDAD91A07F486F41C5B2D87C30A5879E13E + 7A4B85BE15020714565D5C416305BC4CE21919103CF40CBD65D87DF342007831 + EA5A122FA4DC8EB48F4DDA00B8BB26E17589E1F58854EEF58F1ABA0E02008FD7 + 35146FA555B6D32A007894EB67AF05793D68685D289559A3CA9C9FE563E5352A + AF2F75156B4CAFA876BDAB673D8EF285FA1FAA97D6014769080067A5F545EAB5 + 7DE731B9CEF86685552B0BF4CD2AEB9883354EAF99FE5200BC983AA3B46C25EB + B98A2FDBD82C1DEF5F4BB64695F5AD1F77CBFAD6E3FB94D7E8FBCAAD5C2F6B70 + 2E3D86923F18E3A0DF41D9FCFE5FB75A9612829714CB5BBE83A1BAD5AB6A6B59 + CBF714E1CB39CD2875194F9713B94E1679B6D2CD8A72B75DB2AE84B57E94BB2C + E5473DC5BD2FF0987E33D18FFB727CB14AFD17D571F571B7AC2E38CD79199C76 + EBBC8A723FDDBEFAFAAE79430038B7779BBEBCB6BEDA4BF3EB0DF869E6A9BD32 + 647DAD5AB2EA7F07C0BF54FE13A2FA47888B454F7E1B57C89FD96AEBA5F2F60F + 5CF3BBE910E97CB16AB550D9BF9E7CC12FEADEA006CAF9AED7D52FB9E92E56DD + 3175C5181652AED305B7B9EFBE632C549EFBCB6DB2CAB1FC565C8FC84F654B02 + F032911AE4017D4B599D8C65D549591B76B42000466EE7495CCD5F9A2776569D + 4C224F36731EF2C4B35BD697F72AA26D9EE88E1470B63E00741F02CA245DF97A + 5858B65A6580DB6AC54B3A40B0DA2F9596D057BC51AABA3C8404E8D567A67FA5 + 03FEAE52DEAA1DF300F0BC8713A53300EE83C02D00E621C80F429D871FE4FCBE + 721EA2C63A6FCCE6B2E1B41EE080B619F04AE3C0D714470B5900E7BC9C3F4A19 + F43ADED57240AD42AC7D33BCEDCA00B89BEF3606BD86BE3CE4F3B08FFACA100B + 042C1478B1C0CA795E2820EE05032F1A389EF356006D771C69C6278BEC027AC5 + E4B0B5AF816E01BEAA4B3BA0AD34A6F8B209F59FEA028D0D7A0D810981C08E1B + F61A047701305A3D57167ABCE09301B01780A22E69CA52F92AE5131AE2E2A66E + 85FA5BA13CC27059C7E213DA53B4666F718117A0977612F98480DD505D248B85 + B64365812EAC2A70D92C016CA39C45B4C8C3A262B01847D9FAC32CF67D182A8B + 72EF060846B1B71B3098FC23D5B283E3B070A7F2F52C1CB2B8188B86451BD44F + DE8F6EFD51C02F8B85653171033A7E52F92C360283A98B3B67D567B15169162D + 81BEEB81BEE4DBD205105CE36B680F10AEE03740F09B9F349BB1B079F3E3661D + 753BE0D7EE9B0D8001BF56C0DFF7CF973AE1DE39EFF7AB3496BF1F60F57BBE8D + 6325B4F97D5C445E0C8BDF6DEFE10AF27240DF1D1F5E0D084C1C77939BABA551 + 596C2E00188BDFCDEA0B6DFFE852681B3AA936274703E000B4922D7C0D7F1169 + BB7C36C405EEB26F2396B9ECE9B8FD63F5771AABAB02745171E75CDC3A4F0081 + 2B242EC057752A00B65B50C300BB0ADD59ADC71030C1B0612B5662E7D8C7723E + 9440ECBFBBFFFED70DFBB062F9B713887BE34933A970DB85FBD2BD70994C3E10 + 78526D770243AE024914BF08142916C5B861251F4BE129B501D6ECBE5D2C0101 + 44409B9D150EB3072921967DD4A1EE14D0069853EB624D585C2A4BB7AB7521F0 + 48E9B2F7A904240AC85B2013328032EC2A79943D8FCF89BB61E06F80AECF0A00 + 05AE0528FCFC9B806C58E886AB5DA099D200B6B0FEFC0C78ABFCCFD18B66EEBE + FAAF20D7618E1362A5495D42978748870A309D03C63EFEAE39F1DD3F34EFFDF1 + BF684E7CFBB7E6C0931F5457E3C3121820FBF0BB0867EF7D193218066A02F108 + B1A03DF4F4FB66FFA36FE2383E7E81A62F220FAB51CACBB8745C7D6EDC4CEFC3 + 7A567D90C7B10E006501B52A03FE1E7AF687661F1055C7C015F46ED5DFA73102 + 7F193BFB00EF571BC68D0EAA2E60332C641F039839BFA5EFFDEA03D1CF01C0AD + DA717C5C23330680F3E1673F850E720CDAA8FCC0E3A27D80D9C7D253D57D427D + 7D1E8D9BFEF93CAE4F9FEDF180C33ACEA1273F461DCEE16EE0B78EC771F9DC9C + 1B9F9FB036AEDF5D9CC37A6CE26179AB7C14609831C5B14A9BDD7C86277F543F + 3F35B3F7BF693EFCD37F689EFCDFFE97E6EEFFF97F6E0E7FF9E7665ABFC1191D + BB80E2323ECEF31EB5633C6D9EC270ABCD38D56FF45D7F4306C088E362153C8B + FB7195156BE5AFDA10850B69FDA68BE53256C60504037F0D7DC93BC0F7C46F0C + 101BFF35C22F8A457C15FFDBF8BF4AB88A0F17D1AA63057856BD39FEDFBCC871 + AB82DB7A5D30E0F54B1DC05FEA07FCADC7F331A21DD275C8D78DB03C565FF1B2 + 88F27891C47B8717EF023A8EE2BB81D2B7D4066BDF1B9F6B3C5C470600987CD2 + B886060063010CF80DF87BA580E05DB15F30FBA7ABACC25BAEA7DE7BD8909732 + 64E0EB6BADDB704D761DD77319F50D81799927DC482B7F8A6B7ACD030E73CD0F + 8F0F8A7B8F6014F788B3F75AED8AFD898BABEAA3F75E3447EE7EA5FCDBCDF64F + 700BAD3ECE737FC102F8BAFAE37E8465EF8566EB47E72B04BE2C61157C352C7E + 6D056C4B60E7479AFC0A800933F0E57EEBFC1C225BF53A9FB8F712767AFDDB67 + 02004F7D7AB3397AFB8BE6A0EE353BD43F009897BBB8EF6F55C8B60FE1021AD7 + D2E1ED03985CB4B1BE0006FC0D2BE36AEDBBE5DD4FD5F653859A8F786E523D8E + 308759F7C6E916006F96263537D8F1E1E576DE53E63EC5D237AC88DF56FE9B1F + 85C52FF3A8C18B72CCA3CA8B741B9566DEE53997B5FEA8EA1F616F60CDE5DA79 + 9BEA539EC02EE0D610F86500D82F141A041B0A97B2C1F623314F656E0AEC05E0 + 2A9DF3F05AB356F175CA43CC8D990B63ED6B8B610360DA503FE6CCCCC32DE6D9 + 496B90F2BB003883DE3E616D5B9E090610B99DEB57515EC2EA065A6D56112A9F + 6D64964FF1DCA2671504F0A5CFA415AAB3823A49A4B3A570C060F5C99838DE32 + C5D9F777D5FE779A43971F3617BFFBA7E69DBB2F9A75FA0E97A8BFB1698D79E6 + 44B3962D6EB6EB59696B79668BE7B3AAFC3C47FECA491D43CF677EA65B1D2EA4 + 4B3DD21107E8EED0B39C64F8DB05C061ADEC7805C043E0D796BFC05FA02EB0D7 + 2EA32B045EBA6D9087CAB36F51D4AF56C1C0E0E5CA6B9F91798EDE3200BE1904 + 3BCC50D6A0D6F13E018057683C84B62AA6CD28B99DE3B6148EB0AE3B78EDA16F + 4D03E5F2BCFED1AD3F1200B38E922C80BBD07614BC457D6D16525F7BD68D587F + F23AD7BC7529C9EB58DD3A39CFF939DDAE838D588BB3FAFACAE5ACFD59794DD0 + EB8256BB2E98D7147319F5DD7F1E53B4435B8A58B7547EAC25B246DA81C14B87 + D648AD41BA5BE6FA59D918A7AFBC4FB96EB4A58F55DBC2D066C9D20D011FC795 + 0E20C77ABFD2B9CDAB1CCBF2DAB1A16CDFBA7C565EA3EF2BB772BDACE13AC375 + 3D068FA3AFCDCBF4AAF54729C6C3F149F701E09A074BA1CED29A8EB85599CCAB + 2AD8CDD2F561791B1096B4E16D85B3AE6B17D15D5E44196DDD57848E4BEDB150 + CD733BC7E3F32AED3EDDAEED6BB14AFD17D53EFAB85B56179EE6B4E3B9FCE728 + F7D3EDABAF6FE7E53656B7AEF5B2B25695A18ED570216528FBDAB46455680961 + CDEB1D4C476D5DD40B7FD10227E5F7A0F6C7FA6AF21F2B2BFF3173BD1CE63F49 + 6ED36D37FC475A40F4CB85EAEF565C68FB956F5C39EFB7D6CB8ECF0DF9E788B6 + B9EF9CDFA75C9774EEABDB6EA84F2652E34C8C9854D589524C9600C3C05D97A5 + 0957E45539AF4FB9CD4B9427784313BD54C70AABE09CC7442E4D2C3DD9CC13CC + 76A259EBF49559AEB390FAEA79D23C520B02E00A637958A8D037C0AF1F24E221 + 420F170062DE285DBB4B0F1BD3A5BE1F32AAA81FD6BF8B00C02897F5816003E0 + FCB0D3A76E9DFCA0340C78871FBEBA02EA7665F09B01306F1B87EB29C501BC4B + 3B20D8E24134878E67F1C0BA420FE57E781D25CAA30E0FF03CB0035F9536EC35 + E0CD79D4CB40D850D7801719FE76F3B378F8EF03C09681AF170A0817CA6BDF20 + CFE0565AB9B3B868CEF96D7902BE76DD4C481E60977264C8DB2797B7B0B7CA69 + F6000EA82B018009BDC797177A487B01C80B442D04AE8B42AE6F00CCFE662BEA + 825584B8AD632FB46ABDB0AA2E82396EAB88C84F8B6421D27561CD8B6E06BCB1 + D076A8A6235E002EA1CBBD98D72EDC017A251601236421B1C6D7008581C3356E + 6B5E42EAC4C2A1E2D9CA647D0062E02F16C1A79A8D12AE9CD71D29F99B4F9C6E + B6BCF949B3E98DD3AA7FAA59872AF425BEF65859C4B4B50BE0D600989085D0D6 + FA37AC658AE54CB879AEE0D7CA00D856BF9B15D26E0B7BFDC5C26C01BE13272F + 363B3EBA14EE9ED91B705305B52CDE1ADA7A31170B9FAD00E00A7F27A4EDEF4B + 580529AF2C0217779306C05818453F6A0F500E007C4AFD70CC00C0C5BAB7B5F2 + 05F456100CFC05F666B7CE06C04E235BFA06D0FD54F1B3779A894F6F36DB3E56 + BBD337C2BA6B86FD76CFDD6DF7ED251EFBF7D6857EC781C5C8D65FE1EA39CA95 + 4F9BF3C3F0178880F5EF96D31A73850E80886CB1368975D9A5CF9B5DD71E3753 + D78B70FD8A765C7BD86C555F3B00C54A038529C73A78C725407081C0616977F5 + 61581487F59D3483F5F0F527EAB7C09A991B03CB3DC202701E052426BEE7CEF3 + 668EE3AADDEEDB0516477D5CC95698043CC24D2C224E5E58E4D630F635BD532D + 1069576114F078E6B68E0B6802F2DE07947E13569FE13EF9DE5745C035148077 + 007DC3AA1278A678587B3EFCBAC06060A3E2B3F7BF6C35F7D95755A49F37B335 + 04F262E54BFD62ED5BD2CE0314EE79F86DECA73BF7F9D7CDC12F7E6A8EBEF873 + 8480E180B358BF0241A53D4ACF00E7020A0355BF0D88C7F8F91CE1AA1900FCF8 + DB3816E238079E7CD7ECD5B819DF3E959106A2C6310082C05E8938C7C01A9934 + B09738F0F7C8F33F35073A56C0C40F3CFD3100303AF8ECC7E690EAC478297FA0 + F1E973ED0FEB59C02DC72D60134BD8038FE98BB1E156FA1B8D97F3543E3356C4 + 87248F6BBFFAE773F17902B63E56BDC72F9A7D4F08751E751CB735842EE7AD1C + 8F734D3F713C85FE8CFECC45E57BE1DC94FEF8CE4A9E8F3D839B6F9D47E7EF7B + A2F69C5B7E43AA4FDB7DEAF7E0D33F29FC4333FBE0BBE6BD3FFDFBE6C1FFF4FF + 6EAEFFD7FF29CE2116C031267D7F760B8D70AD8D38479C9F32EEF2B9387EF9CE + CAB8E277A9BC837C978A07F85519D6C986C2B6100E30CD6F593AA4F3187B0B6B + BCFC7E00C000DF39E0ABFE4B580FC70B14E97F56FE8B0520EF53DD0C70FD9F2B + FF45FE5780E4A2F2024601C056D4573BC35FFA0AB04C9FAACFDED5B1A736FF5D + 5D23CA75A4BC0082B5B0D3B4455126018103FA4A73AA630B60035F602F30D840 + 18001CF0F70E56CCEAB3BA7F36FC258D7570406085D37840C01342BD8E724DF5 + F58FD0D756AEC37EF1863C035FAECDB1373060F7427919274030E54AF3220FED + A8E736BEA663394C9CEBFD34ED7DDFA8223D754E6DCE714F21547D2C82751FDA + 7AEAAA84DBE7DBB1E7F0AE28E705A2EB6A7B35E0EFE4A7C0DC8B017F0901C093 + 9F0E2C7EB3EC0ADA20D8B016017F0D8001BB19F012BABC0B80B79D2CB098FBB4 + EFD5BE6F036F69B39773AEF31316C0EF9C6FD8DE01106C484BBCDBDE7D70BFDF + F8EEA7CDC677988B0CB4F53DCD3F245BFDA278E98C17C6D4EFDA631F37AB8F7E + A479C5A5E6D0D547CD51DDDBA64E5D4973A1F743B8922ED6C4CCA3CAFC680080 + 0B2C66DB0C80E006F5E7B95881BCAAA3F9584060CD0503041F664E5820F026A0 + B1F2996376616F58F22A8FB00B7F493BAF9D9356ADA39F1100D871E6BDC0DDA1 + F9AE447ECC9F25CA9D26A42D7365E6D6BC6CD95A01D779BAD585BFD4B1B56FB6 + F8753A5B037B5E9F9F059C6EE7FBF4499B19B69029CF0D886786F1A9C3A17023 + ADF45058E53A59E12E5A65DE4F18A86C003CA6F8D2D963CD523DAB8CEFD3F380 + BECB5587DE6F96690C4B74FC65D3CA9F543F5BF5ACB5B53C7F759FD9E2B9AFE6 + 130280FDDC66F9255EEAC473E0169E03F5CCBAA3805E2BC3DFA17482BDD66200 + 704060E701809576FBD625B421307DF28CAB67DD7876AF2036BB80CE71035CA0 + ACEB5AB9CC1AD5DEF5FBF2DD376158FE56E53587BC6ED1D5CBCAAD0C80E7A902 + 60D67AFA802DD6C1C8699775D78A72DB5CA7AF6C091EE4223E5813EB5BA7222F + AF7D75CBFAE4FAADC8EBCBAFEAEB03B5E569ED6F21F8BB5079D4A12FF253DF2D + 005E39BCC6D9B689386B8D0902B34ECA7A296BA455AC9D76D753BBE9AC0C80BB + EBB32F13ED96AFDE1EB077D9CA2DCDBA4DD3CDC4D4C166ED46FDCE6ABF4B6B5F + 3EFEA8712C5606AFA3A0ABF37FA9BAFD5A946500BC98365DE536597D755FA6B6 + 2D02EC62D93B8636943A15F816083CBFFD2F5100DFA5C550706CF996665CDFED + 1AFDC7D7F3FDF33D2B9FB10078E143E6455D06D497174A9C09CD2BB7CC9A52B8 + 60FD51A27DEE6B690D6D05BB581994F695FDDA1A02B38B545F3FA3D4DB3EF1D1 + 05B86A61AB29F1BA14D6BFBF0400E7FCFCE1427CE09E13F37B51F747BC48F94F + 95D507727379E4F94F22B57923F4B2F210757A2E2E7F3FD2F97A05F5DDD45EB7 + 7EE971F34D9BF8A874B76C94BAF5912721AEE3F31537D9B0EAD584A80B5B239D + CAB25CA74E9E86C4644B9396D64D732EAB6D3DE9CAF2642C4FCAA81F7B02D3CF + 4BD437A9ECD3ABD4F939CA93E73C898E7858F02A2D7942EF893B653C28005BBB + 70B685B47AB8185FA787A0B5BBF49935C15FCEB9DAD1ACDC30D3AC0264025BB1 + 1AAEF5ED02BA6BD56BC09B8F61E5BD81DDEE55017006BF56B76E7EF8EA6A14F4 + 45E48FF3596BB81000CE70371E486BBD5C66A00BFC5DA9076B03E06E3DE43E0C + 8081B9962D76FD463771C35EC70D865D8FD0C0D8F5723C2BFA254E3BE29DC501 + A7BBF9392F2F3AB4F968C7E121F7CD86B8C060445E2E27BE8C7485B8CB43C786 + A02F96BE396E2D0480AD0C8057CD0E166F4ABAC0DF48B300941687108B492DF0 + 551996087649678B852C16AEF2C297F3B27279D461DF3369E53E95D73DDA9017 + E91C6F17DCD2429DF3BD4857C202742D035E16040B182E90D7F1510078A8ED91 + 93CDDAB4908858745C4FFCF8A980BF5BDEFAB4D978E274D9D3179DA88B9575F1 + 1219FE22435F2BDC384B2CA0225BFDDA8A97348BAA86BB018315C7E2863CD705 + F6524E08FC45B87A06D2027A37521F601BD6BF2CF296BCB0EC7DB7580163F53B + 81EB6785B88534FC6591392F3687BBC99308E05BE31F21957F74B540DC048001 + BF93A771D95C5C3487DB67E2127523AD7CBB716EE3B8E954C8C2BB17EC81BF5E + 88C73A8B7C202F61EBCEB902E058E0A7ADD2E413DF4E9F676E85AB6780414001 + 4382149FC0DA4BF508C90356180217B880451B96B78F9A6DA4B1E2BDFB65337B + FB8B66D7CDA7017D67149FBEF94C654F228D76AAFEA4FAC19A17CB3B2C7F0943 + 5802DEFEB299BBA53EAE3D0E8BE15DD73E0FD013D6800A6DB9D7421BD5DD7D1B + CB3F2C06BF52FA59684F854F0880C45EBE0060EFF51B60E98EFA0568E2AEF9BE + FABFFBAC893D4F43C51A72FAF693B0642CFBE4526F004D6DF1D8A635C6D84BF5 + 01569DDF84956786C0FB1F02DD808300BB022B2D03444357435EE2C55AB5C0BB + AE0C2D71A70C00DE03247DF27DD12360E977CDA12F7E6A8E7CF9C780ABC062AC + 55B10AC5E216108AE52AF01715305A40A5C740FAD0332C770BE0240D2446F45F + E022E91F4386A205BAD20E10F97D80CB0300D92758FC2A1F4049F8F487E206FA + 0BB5D7580F7DF187183396B68755170819E7536181CADF35B3B837D679DF1F7D + 1700CB79A48C3A05DE0292CB78CAF8C82B9F8DCFB2F7B1FA78ACDF0DD6C5CA07 + F40680D6B181C7071897F24A58F62EDEADE3ECD539765DE035FB15038301E4B8 + B18E713ED138AA488738663D87D4E39801D215EE8B7ADFC5393DC2677FAEEFEB + F99F9BBDCFFED8EC7EACF3F0E26FCDDB7FF897E6E8577F6B66F55B3BF4C51F9B + C312E780341038CEE7A3EF751E742E942E301868CFF13CA61A6A3CF11DEB3719 + BF33FE074A1FD2F18FEA7B0817D51A6B06C0A4797921BE0B9D6BAC83B1CA2EFF + 9F2FE33F64B7D1B187B0FA8C7805B9806097F3D205CA9037CA19C7675816EB7F + 5361700064D571DC1098173EA21FE5017211FF7B4033F5A937ADFF3D2F8FE049 + C0ED01C3B4B7253240180F02E1169AEB511540784EF580BCD905F4EC2DF614D6 + 75F0DA035D83480395957715005C2C7D67556E97D006C37615CD8B2F4060DC37 + B3EFF91CB0B96A4665EC03CC9EBEBB2ED397EB72CDBD1F79935CA3A5C893C80B + 57D0ECB19ECB0C7D49C735BB4064F60E0EAF0FBA1FC40B4212F793E90B0F9A9D + E7D86FF89EEE0FDC6FB88F947B49B9C7A81EC700329FD57DE9ACEE6B6771ED7C + 45F7A92B0AB1FAD5BD57217B02B34F300202CF83BE150463051C6EA2752F05E2 + 96FB6B09330C4691AE611710E3EE99FB355B301037C4753DDFBF03F05638BBF9 + 1D5E22639E51E2016DD17B121E45A470F9FC6E99436C0A17CF9AB360F1FB1EF3 + 91126E7AF7743B7F5987F714CD73B67FA0B1BD7721AC800977E9BEBD5FF7EB83 + 571E36531ABFE742DEE3370360AC822963DE641908AFE5A53A5EA0ABC037CFCB + 4247CB0B7AF1925ECCDBA853AC803D5FCC2F0D9297E5396637AF2BB615C9F037 + CF733DD7757E77AE9CD3AEE7B2981B2B1E80B87AD831008EB97895E7E828CFDF + A36E95EBE57621EAD667833E1900132EDF559E13564C97E7872CEA3A0EF4CD65 + 19065BF300B01400587D2F9F39DA8CCD1C6F962A8DC6344E2C7F974CE95949F9 + 2BF46CB44CCF5BCBB6EE6F56E9B969D54EF5999ED78AEACBB88AFB65609ED500 + C1C0DE25EBF5DCBFED40B4259F6D80C6C29258CF9309F6F629003096BF15F01A + DE5ACE6B61F0763DA726F89B21B0E36300E10A7D0300A7FD817113BD4CCFC9CB + 36E9197D7381BF5D689B4599616D16658486BF06B884393FD7CF727EAE47D807 + 80C7AA773294D727BAF1856400DC5D6F0925006CA89BD306C096D789BAEB45DD + 7639DD6D03000E08CCD6619DB5A8BC6685F2DA97EBF6D5B35CBF5B8774EE2BC7 + BB725994D7353FC285002F6EA057AC9B6A56EA7CA3EC163AEAB7FD6E1BF4EF35 + 4242D613AD764D93346B9303F03B0A0077B550590B6A15F79AEB62D68B69334E + DBE55B42EB37CF34EFE939F5AAE6597B74DD5E32568C77BAEBBAA3C691E5F166 + 0DCA18E746C51963515E8FCFF9BF44B9CFAC57A93B4A7D7DA0BEBA2F53DB1625 + 003C047C5F13001ED7F701F40DB8BB6C43B35AFFE7E3274E35A73FBDDE4CCFEA + 7EC377A6B138B4E03E6DDA1CA85A0CB77CC9F9556ED79B9FDBBAACA78F57528C + A386AF0A807F4BF501DAAC9F5B37B7A9F1813BE9C447333BAD32570DB69A13BF + B60C7C7F2E00EE55FE70A11127E6F7A2BE1FEF2BA8FD2356BD0A002E7F8E41DD + AEDA362314E53D1796BF2F71E15EBCF28DEEB752BE81FE1C2DD4BE7B8346FE9C + 2E73790EBB1A35511802C08B91274C359D27666D1D265979E2E57C54DBE6765D + 2DABA1DB44DCFD25C5442FCB799D7ADD7CE2597902DBB6A913C636DDD36E943C + 71CE727E9E9C0F4FD84B3C00F11A4DFE2B98359CCD90168BDF157AD058AB07C0 + 75C0C4B5BB9AE5D2CA0D7AD0C222383D80B89DF3BA6AFB4C795D001CF5A4BE07 + 9EAE0C7AFDC0D455AE9BF379E81A4EEBE10D009BC0EF420038CAA85F95A1ADC1 + 6D06C0CE33FCCD0098D0F5FAFA42D197DA18FE1AEEB6A056CAF0D6655DD8DB97 + D72DB7DC6F6BB53B310C7A737E2ECB750D800D81235FE51900AF30C49D92762A + 8E2B685C44AB1C706B409C5D362F9F79A319EF01C07D3200263E0A00231662CA + 824E59D819A48701705E3042C459586AF722ABE52C46D972C1D60B9617C0BCC0 + 45C8C25677418C05B358E40200EF53DB7DEAFF80F25954EB5974CB00D80B76AE + 9B17F1C2ED5F58EF028001B845C0DCF5C780BBEF47BA0FF63A6FDD511D3B9761 + 51521713B3B549E8F8A9B00006FE7A7192302F5A66C88B9CC682D769C26DEF9F + 0B88DB05BA841900DBA2D790983CA02FA0773B7BF0AADC71C2E2EA79007B09B1 + E4B1FBE60280B11EAE0038AC75AF36DB3EBCD26C799F05E56201C4C2B117A159 + 4CDE7E4AFA9845EF6BCD7689BD0C51716909CC1DB87CB6DB67F6EA8DFD7A01BC + A7AEB7B0D796C040DF70DB8C2BCE4F6E37017FABBC006F0B5EE2B60026EC4260 + E2B4A12EF077E64271E5C922BFDD84021832F445589C65B7A2883AD91ACDE5D3 + 973F6FA6AF004E1E05D4C50534D6BE00DFD93B009CAF630FD06D3AEE0E950385 + B1A4B5AB685B011BFE025FE86BE6C6B366EED673854F9506F2165083D5202007 + 006CCBBF02819F281F08A463DE7BA17CA0CE974DB89E050E55E064D0641015D6 + BD779E86D5A3AD1FADBD58302A04E6CEDEA3FE970D16C033F4DD02DF6FD497FA + 07C63DFE21F6859DC30AF2DE172D000E50A6BE0D810F00D280BF8F0A98352445 + 4E03EC80BAC0B9D652937E3A32C0DBFB08880740FC3EE0A9E12FF19C3E00AC94 + 88037E390E6E82DB7190E673C5D80656A28440C9C35FA8FD13606F190F796115 + 5CC74C3BE0EF61E02DF0F409E0F7BB80BE7B1FFDD8CC3D0090967130DE391D7B + EF931F25D5030A3FFF83A4760A0F218D1DF7CD479EFDD01C045AF2993576FA07 + EAEE7EC0E7076896E3F81C72EC23CFFF58C7502C8D3DB6433AF6C1A7E5F3610D + 7CF099C6F7C5B73A4F9C2F7D873A96CF13C23535701A187CF8F99F02CAEE577E + 9C53E037E752DF27DAAB7301C4DD1B3057E3D5710E3C2BFDA27D4FA9F775E493 + DEAF32E27BB03C8E7C7DB62FFFA0E3FCD81CF9529FE1AB3FE95CFC59E7E68F21 + 83E0695E34D0673DF6D55F0200037AB106064E03D30300037EF96E4973AE7101 + CD6F45C7423136C6AAF3C00B06589ACFF11B551DCE0D56C1005FC35EFF4780C1 + 19081BCCFBF71D6DF8CDF01FE2F74E7B5E78C0B2BDFEEF881F001E3F501F55FE + 6F028DE93B64A85CA12D614061E5018B5B60CCFF9B3A1500739DE85AF4BBBDAF + 05B3ECEB4DBEEAFB9A62F7F2A4B375715814DF7EDCBA80C6FA77E62656C5B891 + 2ED6C5C86EA50D91E76E1497D080E099B44F700B80753C449C3628AC956B3F93 + 00DC5A36234D0179557FE7E5FBCDE4455D9701B1D489BDD53F2FE552E4716D55 + 5D035FAEE34300D8B058F78B29DD2FB86784652F00F83CDE1DB84FA81C37D0E4 + D77BCCE4B95B017E273E05FCDE547BDDC3CE0275B9EF1500BCF3CC15DD73AEE9 + 5EA67C7446F73AD537F86D816FDD17987416F75AC35A0360E2399D017011EDAE + E85E0D0CA68C17B028D7983EE6FE4BFA72C0DF0D005F855B80BCBAFFB377EF96 + F7CB8B63CC3762EE51E70928C06F55F13452C02F201837D0117F7B301F59AF39 + CD8637D9CA8279CDC5E6EDFB5F37577EFA2F9BA3BA9F9187A5F146C2B73E092F + 26EC1FDCBA807E8B7993E6566F16EF28792E9501706CA1A1D01018B5003829C0 + B0EA1AF8A2BCFF6F9E47127A9E99D39E9B32CF443127C58D73479EE75AC061C4 + 9C37CF8D5D9EE7C384E479EE8C06736CCDC7994F2B9E21AEE7E87D0038CFD951 + 6E17F5F40CD055170AFB190170EB6785AEBAED887705B80E78AD386EA0632F60 + 207005C0ABA6D56EAAF437AE706C1A087C34E0EFD22909F03CA7CF30A367A4EA + AA79D524EEA401CA1A9BAD7C299B2CCF6FE4391FF0BB52791BE7F43CA33E80B8 + E3137AA6535DFA2A6105BCB6F2ED83BF52B8874ED0D756BF86B640E000C013A8 + D64FEEA4BB00184B60DAB9BF028507FD953EF5AC5CE16F06C006B24E8F02C096 + EB1BDE12E6FC16E876F2BB6D9D7E1900CEF979AD22AF5FF489759276CDA4AEAD + B4C2C35B85B600DE0C6F492FDFB86B0800933F581F2AEB4519FAE6B8CB736875 + 01705E937AD9BA15EAD6EBAA5BA79B5E68FD6C687DADAEFBC51AE00800CCFA21 + 7900E0D5FA9E56E93B195F33D9AE15523616DBA6A96FBC04D2C66B795E4B4C61 + BB9E99D634BB0038DC41D77554AF9F66798D75A172870BD5CBC2E2778CB559D6 + F0976E6C36E93FF2C147979ACB9AB7ECD9FF96F237354BC7077D469B45F48BDC + 266B50CE3AFAC608FBE475F9BEB257D1607D7F58AF527794FAFA407D755FA6B6 + 2D7A4DA0B74FB88E5EA1EF63E54A5E02D0F7B2747DB372F5F6E60DCD37CE681E + 37A7FB6AB87656BEF9912D81613F84D45FB14ABF757E435D006C99178D8847DB + 9CD70D7FAE68FFF700805F455DE0DBD5A836353E0480FBD8699F0C645F877E2D + 00DCB641FF1B03C0FEA3F88FE43FABCB9D6EF3E28F9154EBCD53A7DFBEBA51D6 + B9B0FCFD890BF7E2956F7468F8E6F67AD47F232D1A35AEAE468D33F7EBB0F459 + D27EC32CD7CBEDB2BA6525AEBC0E006E275B29AF5567C2E4BA513FF2A40C4EEB + E4ABEDAF8643ED225FAAF9639AB82DE38D3CE531A15BA21B599E285A0B4D26AD + 519351E4BC5CA72DAFE3CEF591EB2D24FA1BC39A57A1E57C4FD0BB93F5B1F5A5 + 6C69B5DEED53805A69C9CA8966EDE63DCDA137CF34C7DFBBD86CD483A02D8197 + AF9D6E56012D558F7D81F383881F4CFA1E505E96BF1800DC7D58EA535F1B941F + BACA435801C05906C0E300D82AF2095F06800D6D0D819DCE001865004C79B72F + B78FB042DF90E258041BDE7A1FDFAEFAE0AEFBE8A6B39C9F41AF21AE41AEF3BB + 69148B0CB52E72DB1593AAB7B380DDD62AB742DCD55303794FE0284FF5EC027A + D92E89850BC50D80BB1038F252DC003843E09C8EB7EE678E356B76B348531674 + F2A24DEC07C6224D5A28B2C5C19ADD0500AFDDF356296361AA037DB3F22297D3 + 7961CB8B5D5EF0C2EA77D58162F9EB85388B7496EBF40160E4C5395C3E17D8FB + 5E15F1810A10261C4E17F03BD006F5C35E737633188B898A871B68E2617952D2 + 5E9CB432F8B580BC7679487A1E0006D4B2E8FA6E71F36C4B5EF2585845B1586B + 685B175B2D835F2FE0128F74951770BDA09B1777B1F2D9F6C1E566E203C02F96 + 48579A2D580C7D78512A0BCF5E7C1E0060C53FBEDA4C7E7243BA5916B44F6199 + 848BE75BADA52F9A04ECE2CE39292030E5150213B7CBE75D67EE06B405E01AE2 + 5ACECBB03716E66B5EBB90AFD01078D7D9BB2D009EA9D6BB00811D170A00B6C8 + 33E445B6F6A50C4B6042CA0D1328C705F4E4E5CF9B9D5701244F9AA96B8F9B69 + 00EEDDAF1AAC49E7EE7CD5ECBA09F4FD22E0291015280C04066E20E0062E5781 + 1F0500035F00C058E601779F0500DE7B577D04F0C5ED73D9F3B38017DC437F31 + 048089633D08EC39F8F09BE63060F011AE6A0B78221F2B5F606D00DF0703E06B + 0B5E8328F6F305FACEDE791E0A8BDF07C0D26FC27216680A00C675F25EA05585 + 65B1AFEB23EA5538469EC6C2BEB28695015E03C21648692B56BB0EEE53405F40 + 1B204EFD05C423FDF487E6D0977F6CF63EFDBED9F3E4BBB0A63DF0C54F919E55 + 7FBBA90FB894B0168E313C2E0015B04A88356BB83C56DDE85F7D1362FD6B006C + 48489E2D802DAC6C01B405D2FED0ECA5DFE8F30FCDC1677F6ADEFCF61F9B77FF + F0CFCD41950380E7B0E67DFA5373F0CB3F0500DEA76304040E780D08FEB1395C + 8F13C0569FF1E8977F0EF059CE19105A6D38FF3A0E3AAABE8EBFF84B4060F61D + 2E16C800CF52CE67E033D1D791AF7E6A8EBCD031BEE25C716E2ACCD5B100C061 + 8DAC711D565F47BFFA4B734CFD026939879C6F447DCE67C92B7D18001B2C17B8 + ACDF4705B0C45DAF1C93EF0F105D40F1DE27FAADE8D8FBBF0000EBDC7CF1A766 + 4E9F65469F6396DFB1C673E4C55FD5E6C780BD40DFB0B226E4F3D6EF92737840 + 697E573EB6217419036558A0EBF7AEEFF5803E37DFAF2DD039575802C77F44E2 + 370DE4E5F74D78589F9772FF57F8BD0F016295F1FFDB8F057205B0C0DF83D453 + 3EE23FC1FF83FFE2EECF9EB77DF1BF9BBB6FD7D105F41A009336000E404C5BAE + 09F5459100C2CA8B173E6A1DCB6038F608C74B81AE295C4708C352B882648BFA + B300E01BBA1E56003C7DC3F01777F5784128EEE881C6BC9082AB695E6AC1F277 + 265C3FDF8BB8F70806CE72DDB30AD8D5B55532088E7DD2A5708B4F1DCA9406F0 + 5ACE430B01E02C5FE7C3957455B1FED57D0397FE97742C853B2F6A4CBA6FEC54 + 3EFB08C73DA9C2DF0C80B1029EF8E46A4060DC414F9D91CEEADEA8F8C427D79A + EDAABBE353EE6F37020273AF34EC258D75B02D84B9876EA39C7BACEEB571CF0D + B83B6C119CEFC52E03FA0281B7E85E3E80C0151EE3F2B9AAECE55B5E0E2BF306 + CD17DEE72532E2651E413E56C058FF7AAEC0DCC3F30BE0AF0130E106857E29AD + 58169F6FD61EFFB899D07CE2E2F7FFDC7CF57FFC7F04085E854715E56315CC7C + 887910F3A50C800BFC2DD6C186BF9601302AEEA0ABDB675ED08B39593F0036F0 + 455D008C88339F3400A69CF9E46200306EA091E7B5CE37008E7CF6F4452AA7CE + D05CB8E6770170772ECDFCDBC03703DE9C66FEEE302BBB8326642FDF3E40CB33 + 4186BB847E86707D97BB8EDBB91CB94F6417D5D1768A1764795E3AA4E71F3D57 + 492BA78E04D0258CAD70F41C04005E3A7DB8598655B0FA189F2DCF2A0190F5AC + 65704B1B9EBD80BF633C9F0173638F619EA30E2BEF40B36CFBFE66F3BE779B53 + 9A239D7FF06DB343BF93A5DBF7052CA67DE9AF405EC35A8706C006B9C87B00A3 + AEDBE71030B702E02E4036000E25084C1FADE570ED2FE02FB098FE3A00380359 + 64080C9C1DA56EB9DBA021A05BE3B9FF513200F63AC4D808F89BD729BAF9B93C + C2BA5ED2D5B2B59415A88B3200F67A10003843E03EB0DB15F5087379AC2375A5 + FCBC26D55DB3F2FA54565F1D8BBE1CEFB6EBB6F5DAD94265B1B696D6040D7F51 + CEF7BA23D017EB5F403016C0B18618EB885808EB73AE040AABFF90DAB1B698B6 + 8D8BBE88A7B5CC56B1477001BF56ECC32B790D352BAFB17A5D755479D710A75F + 9BA48DAAB7B940E071C5958FFBDF4D9B7635ABD74E342B75EE96734E585B96DC + 7F7F7F45791D7AF4BA34F91B23EC93D7E5FBCA16A3C1BA7EBF7E4E9BAEFAFAB0 + FAEA2FA4B62DE7CB6CA5A75EA8D66B794E5F9D458AEF7CACF603CC0D2B60C5B1 + 02DEB0695ADFBD7E8B1C83F12CB59B68FD5EF80E954FF93A5D1BD6728D519EEB + 45DDAA2166F4B2780D5B8BE2C52AF795548EEFF21EEE260D80E84BB41060FD2D + E4637B1C792C2F1B576EE3BA999F9A9BE6BC2CC3D5D7A95F0B0047BBFFEC0030 + 7FC2FA47EC53FCD0077F02FE9CED1F37A5735EB74DAF543ED4CE6D52BB28EB5C + 58FEFEC4457BF1EADED05E7EB3FDE5EA03B0D6A87175E5B6DDBAB9DF5C07C504 + 8137C57A2615DD76B9DCE922E5FD4200DC86CECFE0B482D45CAFABB66FD2CBB7 + 05000E088C9804F64C16ADBEC9A4D37D134ED4D7362BCA7F65006C7527E69EB4 + 8FAD2754DE4B00307BFD2E5D35D96CD87EA039FACEF9E60DDCA5E94110F8BB54 + 93DDB13553E10E3A00B0EAF2F031D4FE676A312EA0DB87A09E072494DD415BE4 + FBA10AF921AC0FF876E12FCAF570FF6C8D82C17D0038435DE0AFF372DDDCA60D + 1300E64D6D03DC3E0B60CB30B79B97F39DEE932D8001BA405CEF056CE06BB88B + 32FCED4260E2911F8B0E474306BBB6E235FCC50A78E5E4D1B0085EBDEB8DB61E + 6D7003CDC2C772F6B8E24DF85A6600DCD56AA4BA4EAF940C7DAD815BE923B1F0 + 521660CA82CE4A85ABEA9EC006C0B17054E574B87DC602988529C5CB5EBF6551 + CA560C5EB4CA7979D1AA5BEEC5AF76118C05B20A730D76B3723E6140DE94F6A2 + DE7AF2628FDF77DA7D7E9121AF2D81B1EA2D16C258FE02810D800D8995669190 + 7AC78A554980DF373E6E369D385DF701FEA8598F585C245D21AFC542256106C2 + 2C7612AEA54F8540DF2E0006F0DAA2D7218BAD84082B61166201C02CDE1AF81A + 0EBB1D7913272F86A8170BB8D4ADF10C83C9C3652496BF88C562DC47028063A1 + FAA362F5DB2E46B3E8AC3ADB4F61F174B5D9813BE64F6E295F65A7AE15901B00 + 18D07B7308F81A085BD4CD1038DA540BE0A9B3779B5DE7EE8500B958018F02C0 + CE230EE40DD07BF1B3E2A653FDB1F76FF4051CC662B85AF0861BD10E1820DF10 + 983D2911F91367750CD5310046D3573E6FE6AE3F6DA6AF3D6E765E7ED84C5E01 + 883C6B666F3D0FE8BBE7B3AF9B8FFFF21F9ACBFFF23F34C79EFFB999BEF3BC99 + BEF5453375A3580ACF4A0174AF3F2A2E578126580FA300C0005F800AA2BCC05F + 00CCDEFB58FA15200C80C10D7401C05F377B14EED7B10F3EFAB639F4E4DBE628 + D0F0D98F111E06483DFA26F631C55D2DB017B865F8EB7801C0450180EF7FD5CC + DE03E4025FBF559D17CD5E2C1BAB6BE180A958CF02DA941F654041857B1EBC08 + B8863565B884061C534F6D80BD03085CF66A0D6B4D8540563480BF050CE33A1A + 17D205882A7C520030B015803AA7CF3843BBA7EA5369B4FBE9F74A03FC8AC24D + 34B04F32C834F03424B585AFE34E034F098F3CC7D2B6ECB14B9BE26679006249 + 976360F5FBC7E6C477FFDC7CF24FFF4D73EE5FFEFBE6CD1FFF59E3D15801C740 + D6AFFFD21CF8EA4FCDDEE765BCFBC21258FAF2C7E690C2C3312E8D5163E778EC + 271CFD57088C7C5CCA81C008504C1E309AF6F93331F6635FFFA139FAB5FAFB4A + 7D3C2DE711717E381F87BFFC63ECA77CF42BF497080F6A7C3E6F8700AC5838F3 + D9E31C16D05B006B01BF1900037B0D62A973E4AB3F346F7C0B54063417D7D3B8 + 8ADE834B6A1DFFC017C0DF3FEA9CFC51E74BC7E13CE83C1DD25800E67BF5D9F8 + DE8BF81ED586EF837A9C577DC6E2A69A3243DF029DCBB870C7ADEF40C724E4FC + B017322F15F03D03FD01F0F1FFE07FC1EF5ABFDDF83D4B0060B43F7EEB03AB60 + 42C360E2BCFC505E80503E69893E2CBF8061CB61A781C2F192C67DE54B86B843 + 20F8CED380B440DF7DFA9F0280EDEE39B7A16E81B9BA66C5BEBE4F9A39C58B0A + ECF5BEC4E55A542C814BFD87CDD4F50761058C0B684030D71DAE55C553417971 + 05CF03B8A58EF6EC377E4DD751A47658FE1A00036C0D7F89870BE72AE7E3FA99 + 907D880D7E0D7A6D399C43C3DFD63298FE1462E93B795EF7105DC3F1E490AFF3 + 841107FE4A33BA8ECFF2320F2FF5E85E616F1193553BCFE97E52AD80D1D479DD + B3B002C6D2F7932BBA0F5DD37DE97AEC09BCE38CEE8F58FF028ACFAA1E6D3E2D + 10D8223DA532441A285CEEAB6A479A17AB4EE27DE3B2EE99805FEEC3C05E206F + 11F76F2C7DB9FFE2490308CC1EFC00E02D8A6FE205572C7E3FBC18EDB6C4FEFD + DCF7CF697E7056615180E00A81B7BE0FE8D59C22AC83EB3C41A15F4ADB5CF702 + 2ED29C0718CCCB68314FC1FB888E51F7159ED5FDF718BF13DD8B37BEA5398AF2 + B004C60A7883E6541B4E3047621E5504082E79E5C53A428B3410D8DE56D04642 + CDC56C05ECB0C05FE265FE1721F0F738A018EF2E1FC49C11914F7ACDA17794E6 + 05C00A8035CF5C7D50F3D43AF75C8F9718CD2BD731FFD4DCD5F0D77103DF3E01 + 92E96B352F3EE2F5C6F3E5148F749A3F23AC80099987AFDDA77973C72B8F0170 + 1704671900C73EBD9EF35719D21237F435DCB55CD71A0580735DFA75DC65B880 + 06FE0600D67353405CE06F58ECEA394AE1B8CA964D1D6AC6761D6E96ABCD72B5 + 1DD7F3F9B29D7AD6C27A18703CA9E72FDA07FC05F40280F55C0618AEF09772F2 + C6B6EF6F66F4BB7BF6D7FFAAF9E93FFC8FCD51CD250B303E100098E30276C3FA + 3681DEAE281F9756603D2C018081B586B719080700AEF0D700D87D7401F0D22D + 7A0EAE7D8468AFE7E280C0150EE32ADA001878DB05B15DA09B41AF95F3735DE7 + 67F86BB97EF75839BD74435997E8AE43585EB7E89693CE72DE28003CA6B2B1F5 + 03E89BC1AF05CC5DBE913AA44B7977AD28B725A44DAE93E57EDBFABF1200A6EC + 75026043DF5E005CD70A89037E5D1EF975FD71E94AF5CBF6682D00465E83ACEB + 93B59F76CD12B579C30018EB5FD6583300CE6BAAC3EBA9C3EBB096CB160B80A9 + B3620DFBFF96FACB576D6B96B19EBF644DC0C0D5FA3E57AE9928C04FCA7D12CF + 727E5E8BCEF19CF6BA7B0B3E3B7A597957AEFF5BAA6F1C565FFD85D4B6E5DC98 + BDF4D40BD57A2D97E9ABB3480500E698F42319029BEFC4FEBF52FC262AF7C900 + 18CBDF1600EB3710E3A6DE9275CD1000462E23ECAA93DFF2A6C56A44BFBF0800 + 0FC1D21A77FA55A4E32E5DBA7620F5114A79BDC7E9CBCBFD2E46B9ED3CAD910A + 2B5DB6B4681E73353FCD70F5970AC09B216F5640E0A4A1C12C426DBB16FC5AFA + C07D27E8F7A29E1FEFB0F843BCE44F51FF04BE30E43F515F5ED4B79CD72D537C + 64BB9A8EB2CE85E5EF4F5CB4172FDFD07E4B2D74537FD53175EB776FE2031500 + 5C34FF669FD3DDB2E13A9AD47400F082EA9B304931B9727E06A79E9CD54917F5 + FAE4322C7F81BFE39AC4A1314F02EB64314F1A735E371FE509679E7876EBF529 + EA2C0A0093379CEFE3761513F034392F13F5E149FDD2B5C3D0B74F636B778595 + EF063DC8ADE7816EE34CB37CBDB44E0F27AB29D743447DF008785BDB75A1EE42 + 725B6BB17B002F24435FCBF9F9C1AA0B8009DB3D7E9526B4D5AFC5DE4819FEBE + 0A00EE4B77F3BB6ACBB7EAA1381E7679C8D5C3F3B661A8BB10D025DFA0B89B37 + AA0D8A3E2BBCCD0038C0AEF2320036F88D058A145A2BABCBE77873BD861900AF + 54DAF017AD9939118A7D8081BEAA372EAD9E7BABD974E08366FDBE77A31DE5F4 + D15AFD4AB6025E3DC53186AD8433FC0DF04BBFEA9F7DBB587CF102CFAA59B59D + 53B89B7859C889721681A476D14621AEE7588862512ADC35D785A8BC586579B1 + 2A2F5CE532C76DF9C06256585124004C188B64129617D9EA1719F652D772198B + 72C0DFD587D45E22BEF64801C105FC66007CB202E0926775F3CAA260594C0400 + 6F39713AB4998548291621EB42A5954170BB40A9B801B0172F9D176185B67DD0 + D771C26D1F5E888558E02D2E9EDB3D7E5974FDC09639C3009838C03803604360 + C2ADEAA72C2A17B86B2B212C7F597CCED646D4238F85E4D8FB97C5EA0A7DEDC6 + 39DC3F9F02E80E007000629777942D7F71F91C96BE676E355367EE3633E7EF87 + 2659B8B705B0B4F3CC9D665785BFB6F60506139FBBFC79B3FBCAC380C0B1BFEF + D9B2BF23003880B1040000FE4608FC250E34B878BF85BB40012C7C01C0C06003 + 832168A07C00F01E2C7EAF3D6D66AE3F6B76DFF9B299B9F945B87D0664DEFFEF + FFE7E6A7FFE57F6D3EF8D3BF6BA6EE3C6F26AE3D6E76D0E7CDA7CDDEFB5F05A8 + 99BEF1385C9E16C8FB4C69F6ED2C20987D7D8130E15AB5C217DCB1EEBDFF45B3 + 4F627F4EEA61318CF5EFFECFBE0DF87BE8E1F7CDB1673F35C7BFFC43734C624F + D3C3CFBE6FAD130D976CEDDBB5FC2D106A00A376E3C2569F275CFC3EFDBE997B + 5020ADA1234037DC3003341F7D9B802DF905A6A1027495AFBCD88B577D449D0A + 841D0F00AC3A80CA01042EF0777F58B37EDF1C78CADEBE80BCE24618C8BBB7C2 + 5E00F0DE67DF35FBF5D90F7EF5C7E680422C6B818A804A203010D3725E404C40 + A2C68A85AFC74C98213060F0E8739D57F58BA52D16BF05B45AC056C0A38E8745 + EA53D5FDF6DF3427FFE13F36A7FFF9BF6BDEFCF1DF360734AE3D1A0F5013F86B + 08CC580F7E59C67C00302A1D0664727C1D1BC00BD8050223AC7C0FEA788CC363 + 2104001F537FA4A38D8E7588DF40059D8CFF8DAFFFD81CFB8663E8B3EA7C01AC + 81FA714EF4F901C0B8623E1240F92FCDD1177F098B60806A00738E2FB5F09CCF + 0204AE00D840D80098FD770D5F71F37CEC6B8D5102001FAE96C607DD86EF8BB4 + 8EBD5F9F611FC7E0F3690C40E180E7FA5C7CAFBBF99DE83B39A873C69803ECD6 + EF144B62F20C9919DB0000F3190A1CDE57F30DA771036D2B6F2C82E7F41F8817 + 0FF89DE8FC007879A1E23075C332B85800230360C70D8749037CF93DCFF11F94 + 88EF9578D902EB7C5ECEE03F17D0B9BE94B1971731A8CF1EDD77BF084B7E2CFA + 01BC00DA00C08A1B0C93A61E712B60B1EA5AAE63017F51B1E22D2FA4449ECA66 + 6E3F6A666E3D523BF60FD73587B8AE61BC7812D7ACB004E65A447B5D8F54B69B + BDC66F0070F17050DC4197B8A4FE81BB016F255BFDB62EA095676B60BB77C625 + 34F511D6C5DE3BDD003843E080BF493B00C040DCFA12CF2EF66FAF1E1EE27A5E + 5FECA12C5FF3FD72108AF20BBA974840E0A9F3B7DBF4E4392C8381BED7C22278 + 078AF835D5BD51A031D6C2EC098C3570D5CE33BA4756017EC32D7405C3580593 + 666B055EB89A3C7DB5D9C1B60B4300182BE0B2CFFEB68FB8F7D617AA745F652F + FE2D78F4F85021FBF1B24503F7ED0FCF2BAEB9C4879A2FBCAF39C7079A5F7C58 + E610D6D670095DF51E61D992C20038E61A55657FE0C19C0237D19BD13BE702F4 + AE39F671B3FAE8A966DD1B9F6ABE8434170A17D09A97BCADB66F1147CCA100BB + 274306BE9E43399D0130F988B8E16FECFDAB102F2D00E098CBF1D25F7DF16FF0 + 02205B7F9417059D66CE48DE7ACD1779299079E46AA030D0D72F092ADCC07CF4 + B0E215006705F0DD5F5D4233DF557BCF67C3A25862DB130B00DCCE8B15D28E79 + B4E7D27605ED9726BB00786D07005B1900DBF237AB3C0F1450EB797F06B8C4A3 + ACA6B302E6F6F4E17E723D5E42B556562DC7F2772796BF9242E23C5BAD020C63 + 258CF5EEA49ED1A85FFB58B6EB90DA1D6C564D1D08ADA45C2ADBEF100285AB1B + E80A81799EE3D972CBFEF79AD39A235D7AF47D33ABDF1E2EA0C754BF58101F6A + 9655389B81AFAD7633BC1D930C8033B425BD5263402B00C1B57DB6FEA50FA0EF + 920A800D8397E0025A632CF057E259580A0B60CA0200EF6DAD7FBB50D6E93EA8 + 3B4ADDBAB97E06C0EEDFC7EAD3921E00CCFA83E35EB770B9D72C4669C805B4DA + 396EF86B20EB784E17AB5FD203009C35AA5D17F822E7393FC2B41695D7AB165A + B75AA85E379D95EB8F5E3B1B9445795D0B5C08003B2FAF25C6FA226AD7259507 + 001E82C0A55EAC75AE625D4FAA6B92F3B4923E2A0466ADB4AE9FE635D49C763C + CBF5BAEA82E451A28FA837BEA959B67C4BB34AE774B9C6BB6C6C73B35CE76A5C + E31F75BC9C9FCBF23AB935AAAC059F1D795DBEAFAC4FC36BF9BF8DFAC681DA3A + AFE0C6B96D1BED69B7F8B6BF4400E070F35DB90EF097FD80E3F750E358082F33 + F7A9BC27984FAD1F2EA0F90DA53AE10ADA7142C7CD919CFE2DF4735D40778169 + 5F9DC50800ACF60B69DEB1FAD4D7F7425AA88F8893B75AC75FA3EFB7681E7335 + 3F3558FD35F43A0170AB21F88BD287FFDD891F277F8C85D4F9512FA079C036E5 + F59585F29FB4A3916DAAA2AC7361F9FB1317DEC5CB37B4DF52DD1BF0AF29FACE + 9F2B1FABDCE0098B4A7E390F06C3BE8195BAF3C719F09A895007E886DA09558A + F7D4EB4EC496D29F2776CE274F72DDA5E4A5BE9C37AE89DA720030E1AA1DCDB8 + F2C3C58B27899E34A663F44D3ABBCAE5B9AFAE5CCFE5AEDBDF86B4C61E1AEE63 + BE709BA370ADEA2461F58B5A0BE00A8097AC9E0C19DEB61097070BF608F6E496 + B71D757ED8AF77B9848530807868FF5E806E48E936541D958DB36770CDCFC077 + 9E12B06D1F76D24352CE735840EE40AEDFD766B8AE1EE600B755E38057606E05 + C02843E1970160449EE16D17E48ECAEB6A792A0F2BE12D0330BB10B8E5E1B82F + DF0038A02EC036A5FBEABB9EAD780D7A5B009CF2882F048079833DD23B130096 + B0E0B5156FC4771C513FB43BD242DCB18943C5DA57F115336F34EB0FBCD76C7F + F39366EBD153CDDAD93703EA662B60B72BF9A56FE7650B60DC3EAF9A3951DC3F + 737CE5B110138B440181D527FB69B158A3340B43614550457EC455DFEE9E0D71 + BD20E5749617AC2CF202F2D6052DE445AEB086201F55D80BE84579A1CC7986BC + 86C058FB66001C7947DF8FC53B16EC5081BF45B1A0975C426F7CE383BA0858C0 + B0455EBB3818E962FD8B250996BF03003C6CDDEB0549441AB58B928A63E16B00 + 8CBC70495E94613953C5C22A40D7F170C5580130B0960556802E69BB6A441327 + 2F0414761B6031D6BF658FDF336561F68362C943BCA4B10252BB8F8A85118BC3 + 05001797CFA45BCB5F2C8CB0FEFDE86AB315507C1210ACF849AC8EB0E41DECE5 + 4BBA58F496856820EFB68F8A7552B6FA753ADC3E9FBD1B70D7A037206F05BB86 + B861CD2B4D9FBB1760D8E53904FC0600C6D25779B1FFEFB9FBCDECF9CF021003 + 85D9FB77E23C7B004B17D4FF65F565805017FD0D8101018002C3822C60C1F4E5 + 87CDDCB527CDECB5B25F2F60172B5FF6FC9DB9FBA239F36FFEDBE6E67FF3FF6C + DEF8E61F9B5D77BF6C76DC7CD24C5C7DD84C0148EE3D0BA0833BD6A96B9F37BB + 8019B891C6024D7D0257F6DFFF3ADCBA86A56F75BFBAFB0E7B7A3E6DF6DC9300 + 3FB89BBEF36584073FFBB639F4E0DBE6C8A31F9A635FFCD41C7BFE5381BFC043 + 6026B029C125435FE7110E003071AC17BF51FCEB66CF032C23BF0B4868506B00 + 5CAC5F8B806476D36C01506D791AE5EA07018277D31756C035AF88F4D7CD4140 + A0C68E4526969905FA7EDF1C7EFE7DC0DF435F280F00FCF4DB80807B9EFDD0EC + D567DEABB0B5A4FDEA8F01094B1A88588065B87E7EAAFA40CF2758866AFC4A97 + CFF04D730880A810404718630915788900C186AEECD70BFC05B6B63036F6CDFD + 530060AC580FBFF85B73E4EB7F680E7FFDD7E6F0B77F6D0EBEF87300E00C810F + 7DFDA7E6E05780EB3F34870268FEA4CFC979C6FA97CFCEFEC0AAFB4C65F4A9FE + 8F7CF9C7E6B8DA868B668EADF451F5C73EB9E1323AE0EA1F940F1C06B6326EFD + 365EFCA139FA8DCABEFA49E7E5479D4BCE4F01E5FB00A31A1763E09C1D563FC7 + BFF95BF3C6777F537DA0ACCEA1EA01585DE700A057DF0FDA8FFBE7E7A47F8890 + 349099F4B16FFEACE3EA737EA9E3005E6BFE217D66AC828F685C05DCFF49E7EA + 2F0AFF18DF2520F880B447DF1D00987CEAF17D87EB6CC5198BF729E6BB2D96C0 + 7C46C6A8316B1C1CAF8C8573AA31EADC027F710F8D0204130283EB6F80DFF101 + C0B1C61BBF8327FA8DA067FC5628D3EFFCD18B6AE95E416FFD3F118F3D9CA3EC + 6BFDE60B000ED7D3357F3FFF3595F9FF17ED24E7F1FFB4556FB87507D25777EE + 9601B0AD7E490394032AD7FC0C7D1120386070B50206FC72BD218E35F13EB5E3 + 7A8378E9245E3CB90DECE5FAC30B2AC55301D7AE12A78FA7CDDC4DF571E361B3 + E7D6E366AFEAEFA91098342EEF81BBC50DBEAED7F59888975DC8DF01C88D72DC + EC7F36D8339D7ABA56120620D675DB60D8D771A75B008C15B0447C5AD7F25D78 + 89A8D7F1789947D772AEFBB6100E489C34C5FD44F70AA0AF357D51F79C0B7722 + 8E65F0E459DDD7CE5C6B2670FD2C0182B10E2E6EA2752F04FE7600B0DD3FC7DE + BFDC837919AB5A070384BDB73E5B2E4C9E06F09697B10AF80504174BDF6D2775 + DF3D79B399F848C73B7DBBD971EAA6EEF1974340610071B9B7DBFA178BE00280 + 09B77600F0B60F0600983D7F8B8897B907F21CA27D694C7D07100EA8AB10C0FB + 0E40F862B3E9EDF3CDFA1367020293BFF53DAC8435C75008082E8097F9132FC7 + 697EF4569D5B690EE6F952A9330C85FD121D56C0DE9A03101C521C18BC4EF343 + 5EFA2B73BB027107F3BEB2FD87F3B30A1C4EF34840B1E699E13EBA02E0215500 + CC8B8DCC755731EF65CEAB396C7E693120707DD1D10ACB63E6BFCC89D5CEF3E8 + BEB937DBB204E4D53C7D2D52DC56BF3197A72CC900B8F5EC2301800D7047C1DB + 6ED9A83A0BD533BC1D9F3A1C5A31ADE72304D89D3AD2ACD1B3D31A3D33ADD5B3 + 11E13A3D2FAD9580C2E1A2997AAABF7C1716C100612C7CF7362B77EE531D80AF + 9ED97614E85B207011E9507D565BBDEB58B37EF684A431611DCC73DD4E950389 + 77E0265ACF8709FADA72375BF086A853A1AFC12CF12100AC387B00F741E43E00 + 1CAAAEA311F1569BD3DEC25B06A03683DB6EDEF2AD83BCAEC6B7381C6E9FEB03 + 7573FC650A589CD621FAD627725937BF2BD667C6595BA96B2A658D455AAF7800 + DB0278BDDE33A4F5AC030DA02EE016285CC0F030F875D8CDCFE90C81A33F2C66 + D39A545EB3CAEB56DDFCAEBAF59CCECAF5FBD7CE86CBA2BCAE0DB21ED8077F11 + 6B81C8E976ED30A5634D6C1E00264E9DE163B6EB92F463AD645D726100DC5D43 + CD50D775FBDA905E0C00769DB296AB7E96AB9FB14DCDF2155B03002F5D51CAA8 + 938FD13D5E16F5BB9A5FCF63A66CBEBC2EDF57D6A7E1B5FCDF467DE3406D9D9F + 03807BCA5EAB34C6A500D26AB18B35F0727E07631B43A46D051CBC2733A20A79 + 5B3E9419520E1D975EC6925E8B7EE91EC07D00F5D7563E469FFADABC4CA3FA69 + E384AB9BA54B60AD2BA555FA9E57294FE158095B6528FB4BF5BF03E0AEF871F2 + A758489D1FF502EAFB9339AFAF2C34E28FDAD5509B54AFF7E2F277A572A35AAC + E6DFD0FEF514376EE965377CD7EB2BCB9FCBF5E6D72D37A9C18DAAC40B141E6E + 3BDCAE0380BB70D7795DE53A529E80B513B134C9EAA6439D632EAD13BE00BF58 + FF0281B10056DB98F079B258276C21A5C9EF9B742E5EF4A17EAB156FEEABB7CF + 35395DDA2E16002F5BA3BA6B075AB2469FA1C6790058A249BAC16F1F0006C402 + 7497AE569AC9ED2A26F9535116F5A9A37243DF2C835F1E4C00C52BD6CF342B37 + CC8696AF9B290F2CB4EDB4433CC0D0EFBC879DA187A2122EDBE032F53504758B + F243547EB3760C974D6DBD027E330006F4668BE01500D89ADF077CBBEA0260F2 + 86802F7189BEB30BE8AC80CE842A2F758AF56F76FF6C8B60CBE0D6F10C7381BD + EC156C6518DC55D405EAD63A017A894B2DF4AD210B04A1EDA5CFE8BF2E260C00 + 70D98F2ADE229F3A562C7A13006EB5E3888E71A8199F509B0A75715B06A05DAE + 38E19A3D6F375B8E9D6A361EFAB05937F7560B7B0D799DCE727E86BFAB67DF6C + D6A83D02041700CC1EC015F0B258A3D00AE05B1790BC78B46EDFDBB1C8147BFE + A685A72EE875BECB723971035F422BE0EF81B79B95FBDF6C5660FDB05F634DB0 + 372F9465E5F2B0F8ADF92CC66D3A7EB2D90C7CC5729732A92CE065A97E55C0DE + 132787AC423629BDF9CD8F429B58848C74B1FADD8CFBE7E3A72204FE6E5468B8 + 1B8B94D51AE5650098BC1C27C45A06D8CBFE7986C0B87436E035D4C5B5A2E1EF + 865A0F308C00BED4C3E29736E11A1AC8FB81DD3D130E435F168007618997C55C + F22E86022213E266126BA38FAE361358F87E7CA3D90A083E794D02F8DE6C767E + 7AA785BA760BEDB4E12F695BFB2283605C3E0700AE96BE61E50BF045D5B57358 + EE2A1DF0F7C2671192C635F4368D8976D49BBB542D8013006EDB280F2B5F4000 + 003820308BFA978BDB515B93854558158BFF06C0C401065888CDB1FF2510F8F2 + 43E5ABCDA587B117F0146E51D9EBF7CE97CDEC5DDC197FD7EC7DF86333FBD9D7 + CDCCFD17CDACB4EBEEF366E7ED67CD34567558D3858B55DA158BBA69DC4A0353 + 6E6391076021ADBA370BFCDD7B0FCBBD0281F7DD7BDE1C643FDEBB5F35B1F76F + 58017FD31C50DEC1C765FFD2D60D6D054996A16F6B6558A1D300FED63D4D712F + 2CED0F57C3DF4518B097B8420030D09390722C750F006B75ECB0A0E5B800AE0A + 81C37A32E0EB8FE16A38F6E30566015F5B156B4C5B8F3A7EE84BF6ABFDA139F2 + 82FD7E0171EAF7D9B7CD5E95010101A958FD867B60A5B1B0251D00F18B1F15FE + A139F4E24F6AFBC70096B6144540610B2808E03DA463336EC6132E8A01A75FFD + A139FA02D7C840DE9FC2CAB6C05EAC7201C37F0A107BECAB3F07283DF4E2CF01 + 32F7A12FFED4EC7DAAF1D0E6BBBF3547BEFDABC657E026F0F2A0C676E06B8DEB + 05691D0FC88C38B6C61FA18E73F099DAE06EFA99C6A2E31C03FE4AB67EC56A17 + D7CD8C83907CF2DED0F14E7C475E85B55FEA3CEA580775ACFD8A1F0404AB2CF6 + 23664C9CBB3A2E3EC7916FFEDA1CFBFE1F9AC3DFFC45F575EE54E7D0D77F0EED + FDE27B9D6F7D6FC05E2C8AF55DF11970638DF62B7F1FF055E1916FF4F9BFF973 + 1C8FBC03FA4EA94F3BC0F0D16F55A66302C88F7CFBB708F91E71978DCAF759C6 + C7F749C878E305009DA7B00656390AD8CF771CE31A8C050B718ECD778C8535BF + 2F5E3400FCF27D6309BC9BFDABF9EFE83710A058BF5FD207F59B3B88F5B9C2C3 + CFBFD3770E98D76F18080C34D6EF997A80DE3DEDEFBFFE1F140728037F0B54D6 + FF817CE559E12E9AFF1EC7A59DEA95FF65293BA8F421FD6FB002F6DEDE06BC01 + 82A39EC6C27F91E3479F4A6361FC99FEF7151023E06F5C7B24F611C6AB800170 + 58042B3F3C0FE8DA03FCE5FA337BABB890DE7DE779A8B87F2ED71FDC40971759 + 70E1ACEBB101B0C2D91BD5D2B7C25EDC4817CBE1C15EC4655FF387CD4E60AE42 + D2EC116C184CDDA95A1E7B0757CB60AEDF65DFE1722DF7F53CDC424B80E19970 + 4BCD1EBFE57E50E06EB9D6038027AA9BE8A90B837B008A17822AEC45805F0070 + 81BFC51A981097CF5802A3A9F337AA853065050253A775077DE666C0DFB0FC3D + 3580BFDE0F78180A5F6F263FD67D34E0AFF22AFCDDA1BC1DBAFF6DFFE87AB3F5 + 03DD6B3FBCD16C3F794BF7FFEBCDC677D8FBF75ABC880500DEF8AE612DF77ABC + 7F9CD3BD59738C535527D1B96682FC7956C09F860A083EDB6C46319F30FCBDA8 + F4051D437394F7F04A72BE75F5BCE5DD0BCDF60FAE349BDFB9A074B10CDECE4B + 66EF5DD07CE8ACE642654EB4E9AD5312F325CDB1DE39DDACD3FC6B6D0F00DEA0 + B4E756CE8B2D398E963D8099AB6D3AAE32C50D8099D701800D770796C0787B29 + 797E019097FF8AC56F796170008295565F7E09D116BF1B0E15F81B16C1070B00 + 668ECBDC36E0AEE6B79EAFA2EE0B8EB8990E004CBEE6DE9E4707EC354CF6DC7A + B7E6F809EEFAC5CE9533C5C2D790D7E5C8F3FC2C0361C35A0B2F4006B706BB01 + 9AD3B186EAF3CC40FFA98D451A0F4380DFB19D874206C0AB7615CBDF357A365A + 3B71B859BFFD70B3565AAFF43A3D43ADA66CEA68B3765A9F89B63BF685F5EF9A + D9C37AD6D2F3DF0E3D674E2A0F57D0F45B2D7E03EAB63058CF6D8AAFDE75B459 + 337D3C9E3197E819765CCF747E91977A76C3BC6CBB9E472BACED0260C35C2C80 + 634F5E85E3DB794EDC1FC017B5FB02530FA94DDBAEC6E358DB0AFC6DA57CF20C + 8363DF5FE58F6D537DF2AA25F0F896E2021AC06B77D003296F4B01B9A403EA56 + 889BD585BD398EB2D52FEAC2DE91AAEB18595EA3E8D312B4AE7A325B3FECD52C + D64BEA3A0B2FD7876736695C65CBC30A780080B358FF1943C4A52E00465DD0DB + 82DD9A8FBAE9A1BC35755DAB82E0B1B43EE5785EC7CAF2366C11EFD4757C547B + AFF92D5416E55E3B5C311A00B70A985BEA12BAFEB23088A0BF1DCD9200C035CC + 52BD7CDC7CECA26D6AC7DAE4CB013079ACA176D7789DD7CD47A3FACACA750863 + 4D775CFD0268D1CA9E3E6A9B79F9555E4B761D8B3176F386D79387E5B285C531 + CBF1FAFA789D9A3F96A2BEBA2FD32F69FB8B1400B8B86AC6CA17E06B8DEBDC12 + 76994FAB11CC68A84EA76CA13AAF4DBF1400FFBD2B83DF7965AB2B8F5D19F0D7 + 5A360647FD7B05C0F3E0EFEF1D004B7D3FDC9FA9BE3FD9827F3E2E02293FEA2D + ABF51DBE44BD1797BF1B951B48F706D62797BBEEEF41BE99F6DD60B33C51209E + DBE7CFE6FEFAEA59DDFA561C67B926229A4064E1FE794CE2ADB780B95DC0EB74 + 96CBB29830E5095CCFE4AA9B179334F26B1F4B95C7E46D99E263805F4DD2C635 + 8963B20A1CA63C835FAB6F52F972A91D93C018179341E54971AC981C6BD2ECE3 + D63CC75DA7C8FD01800763F1047958A5CF258A63B11B222EE1061A2D519D9701 + E0D01A6995CA512D735DC35FA75D7F295A5B00F1323DC08CE92167C5A6D9D0F8 + 063D0455C85BC4C34651D9BB663AD47D101A3C2CCD54114F8A32EA0C1EB2E201 + 4AFD5A8372C0AF1EDC3AD6BF56805E1D8378405A60ECA662191C2037C15CD286 + BC5DE5FC0C80C7F4103D467C8B1E8E81B51127BFA49D37A8A38762854B955EAA + D08037C3DF2EF475BA05BA15FA5A86BD5606BB86C42DF00D575D0500479ED251 + 9707FAC9C37AF057DD789B9B4500B56DDF281FC0E0028007EE980D7D5BEB5FE2 + 9347D5EE48405F03E0B68E1496C3BB80B405DA76012FC26DB4E32E5FE5346DAA + 0C7F01C1A481C22C0EB58B325281BD5E0C527CCF5BCD4AE2CA5FB7FFED028001 + BB15EAB210C52254EC5DB6A7E439BF4FB99C05AC61E1E6B9B8C0A33F16BDD6B1 + 90C6E2D9E1BA8856016F06C219003B4DBDCD6F7CD46C3981752E80F6A36613D6 + 1980E02A16FA5086C11B8E9F6C36B1A8781C0B5F160681BE6AFFE6C7CD26A4F8 + 4689F866406D85BE28161F8F0F60AF17201D9685CC81B52F02FA1AFC6E7BEF5C + B315E85BEBC562EABB6702FA1A0023435D0360A0AFAD79D1A677D567ECD157AC + 71624F60EABE0F00BED06CC33D342EA0C3E20711C7B217A00BE005EE16E85B16 + 72ABD817F0A4FAF80837CFA52E7D6DC54AF8E32BCDE427D79A0969DBC7B89CBC + DAE0021AE0BBF3CCAD0617CE86BA8838CA96BEB1BFAF44DCF54A5E01C0B1BF2F + AAF03700306E9E93662E618D75BFC15534E19E6B8F9BD9CB9F2B7E4FE18366EE + CA43D579D04CB1680F3C4617D5160B3295030400BF61FD5BE1015000119F063C + E818C062EACF5C551956BBC00209008CFB5120300078F2A28E751997A8CF02DA + CEDE42CF9B99DB5F365337BE6826AF3F6D76917FEFAB66DFC3EFC292761AD7CF + B79E488FC30278EE0E2EA1BF6CF6A9CE9E3BCFD51E37ADF4834BE8024B00337B + EFE2EAF55983F52FB066EFBD2F9B839F7F1DED703FBDF7BEE2ECCF7BBF42DC0A + 7F0200298EEC96D66008001CFB8C7EA6E3A77A8E03920FE3D218688B752C50EA + E90FCD91A77F08B006F4C4DD3000D8AE9CB1A82C7BCE16D126C01AC77EFA5D83 + 6BE07DCADF4B9934B038FDB1C1A2749FDAEF7F56F54509C35AF34B0070D181E7 + CAC392F30B2C4181816A8785A784F5EC3EC031F92FFE1869F2D121A58186015B + 752C8E1930F84BAC44B120453F354700BCD56AF4C0738033C7FD437314B7C5DF + FCB939F62D80973496BD6AA3F811A0EBB77F511E65C58D73EC59FBF55F9A23DF + FE4373583AC83EBAAA471E61B1FC25ADF1A88F032F7E6A0E7DA3FED05758D7FE + A4BE50B1083EF2E59F25B5ADEE9701CEC5C5715158BB4A6105ACCF186303944A + 4735EEE3DFFE7590FFB5FA7FF1838EAB73FCA5CEE70B9D1F8D03D0CBF8F66B6C + 07151EFFE19F9A633FFC1B8D09AB65AC94CBF801C10868BC0FE8CBF7025CAE3A + A473C3F90E8B66F254BE4FDF1BA0B71CBB7CDE835FD38E32FD5E48475D9DFF6F + FFA673F637E593D698E3F83E777F6ABFEBC3F5DCEDC67297DF0ADF1FEDD33929 + 0098FEEB582A0CF6F9058817CB607E67FC06BF6B76576B602031B0FF4005BB07 + B0FCD5EF11F7E387D4EF117E1B0A0F3CE5B7CF0B0EDF05E8F58B0FEC2F1CA097 + 7CA581C2C061D2F40B5CE6BFEA172F88F3F20696FB0060FF5711FFDFF26286C6 + A0BE0E3FFAB65C0302EC16F80B383EF058790FF51FD667F07F9EB2D867986B0D + D6C152B6060E28ACEBCF1CD7325D7BF6F202CAED67BA8EE91A78FB71B347D79E + B9BBE585955DEC477E5BD72D1D13EF037BD923FCBEAE276A53F605D63559D7D4 + B99B4F9A3D6A0B08660F73435D406E715F8F1B695C483F6A21AF2130218AEB73 + CAA30E2EA2C36B437D8107017DD933D83204E69A3EABEB75B88DE6BA5DC1AF5F + EAB125F0E4B9EAF259F78622F600060ADF52BEEE5F17EF06F89DBEC43DA502E0 + AA49D59938A37BDEA7D7752FBBA17AB7741F0214038D6F15F82B1922531F2B60 + E02E2F56017EED0EDA10B800E1A289D358FBE295C39E3924F60526FF5475037D + 923D80AFE99E5EAC7FB10EC6729817B8366B2EB0FD142F73692EC0DC42F7F889 + 8FB12EBED86A87D2131F696EC05C20E02F2058738C2ABB82B6FB67AC890B50BE + A8341E47FCB299E29AF3ACE3E5B9B7CF34131FEAF8EF9C6FD66AEE8436BC8525 + F0A7CD06E6570AB7BC73A6D9AE39CE160900BC5E73AF759A83116E7CABC25F89 + AD37429A87AD632EA63A5809AF050063F50BFC5579CCD598A7850BE893B16D07 + F33CE67E9EF3459A6D3EAA9CBF41F5D732070DCF320520AF3DF241B3AE82E370 + 15CD7CB49DBB7AFE2A294D99216F17F6B2E549A4D93398B9ACEAAFAB7DADC11A + 78EFB0F52F7366F615264E1EAEA00999AB2360F0EA0A8657D4F93DF3FA32C71F + 28C35F8B7C03DC3E988BFA00B0EBE73686BD796F60C7BB16C011223D3BADD273 + D61A3D17ADAE61686781A561FAEC0000FFF449444154C3AB761D6D56CFA88FA9 + 83E1AE79C5F4E166ED6E3D3F4D036EF50C28AD9C3AA467209EA978D60204EB19 + 2DDC42EB994D21F0389EDF789197674509F8CBF35EBC74CC735F7D168C7D7B27 + 78FEEB5786B88486C3B11F7185C40BA905BE58FA3A5EFB22B42530CFC6CB3536 + 54F603E69959CFE33C37ABCE388037C22CF254674B479BF58C5EB5AC6A6C0BE9 + 01F4CDB0D700B84FAED3A7B1F472FA28B56B1E84AC5B8C12EB2DD44BEB2CA8A4 + 29AF60164BDDA4A506B611679D060D03E0AEB5AF017096CB73BD41A83A183060 + 8CA0BADE92AC8DB31655CB870D1D48977A21E2D4CDE50B68A1B5BAA1B53C54D7 + 1097AEE887C079CD31E7C7FA1D75639DCE7DEF503DD6F986D53D6E5783636D09 + 65B0BAAC6F6DB58A34EBAF4E7B5D37D7B7729B3E75D771DBBEC655363638465E + F3ED8E6D94DCD6F29AB295EB96FE076BF219A82E28D698DB3EF29A3E1AEEF3D7 + 56EF78A4BEBA8BD12F6DFFB305F70102C3726A1CD7CEDEE7D7A0B7653D9513B5 + EE9CAB68835C2FD7EFA6DD67ABD4CFAFAEFFAD0360AB1700AFD1B9592D0DF358 + EF076CFDFD00E05EF88B7EEF00981FE842CA3FE84EBA933FEFCF96CAB2DA3ABA + 082CE3E293FEBCE1BE807A86C3DD8BC6DF85CA057560A13AAC6197C603F966D7 + A76EDDDF52F9A6D937A6EE4D35E767E532B777BEEBF866DDD619273E5FC0DDD8 + DF97F872C5C735691AD3842189FD761193AD766295A5764BC7E96334F88D8992 + A16C8E5B31912A7573BE276965A255CA62F2A67499C8D54918EDB26ADBE887B7 + F46A3A4F4AA3AC231FAFA499086A72182E949924AAED2A4D9257EF549EE258D8 + 0257954FB874658D2B8CB405C4EDF43F523D70D790362C7A2BC8B586CAAB0600 + 58F1387E4F59D5A05D792859B2567DEAA162005E13C04D0F3821AC7843C30F41 + 59D45BA2879A02708B75EEB20D7A489396AE2FB0D661292B0F5843C7A12D20B6 + 02D9AC0C7197D63C5BFA026C97D5F280B855436058EAF6D5A7280F885BDE565E + AA074F84DBAA78539A076C54012EC0D720D7E925557DC0D77919EC66E81BF52C + CA956739BF80DD027FA913F077426DB3F4101FC017C0BBB3EC4335CEDBE4EC2B + 55DF325F1E60B8C0DF90CA330006EA5A06C0ABA78E87C8CB00D7401717CEE1AE + 194DE2FAAC5811BBDCFB065BE1F259E5B401F2027B0D7C6D016CF8EBBC15332C + 0EB1F0A3F46ED555B87296452480EDDBCD721690A4B5581BEC56FEDE379B4D47 + 3F6C769FBAD2CC49C056F62A63812A835E2F566520ECB82D7E077A2FB4663FE1 + BBB1B0652B8AB0A4385C2C295810F3C219C7F5DEBF1679917FEC836623961C52 + 5801BFF151B5043E19961AB1A85717FA88C7825ECDDB70EC94EA9445C3B01EA9 + 16255E5C8C78B5F4CDB2AB67D7B10C740D7B9DC7C225C017B1C8090036040E28 + 5C81AFAD79114037A0EEFBC5BD337906C0B6E2DDF43EF16285931761D9BB97C5 + DEB01C3A79A5D9FA6109BD4F20E9AD1FE0EAB928D26AB3FDA30BCDB6533ADE47 + 58059D6BB6B3307C9A3D7E2F2B8E2E3513A72F3793EC73F889FA8AFC2B4A5F0F + 8BA61D9FDE50FDABCAC37525564D15047F724BED6E4AB8BC2C7097302C813F51 + 1A287CF64E08B7CF86BE0060DC3A07C4ADE015208BDB66F6EB9D02E29EBFDB1C + 79F04D73FAC77F6E4E3CF9A1D6BB5F002E0BFA4A037D636F5FC9A0176000F805 + 1E845558CDC332D8F9D197F2A6AF7D3E0412286BF3A4E9AB8F9A9D5770590ADC + 78AABCB2772FD67080E069E5EDC41DB4E2BBEF7ED9ECFD0C50F37533A7789463 + F9768BFD38071026C398DD779F879BE83DE451873D3981B5D22C6E5B958F7BD7 + 3D58FFDDFFAAD9FF79B1068C3440F761813F2DF4AD7100912D83A9E7FD80731D + 03A7368F364F0A0003E41EC1ED312E7975EE81C0EC8D5AE0EF8F01740D87B18C + 05F80280F73E7AA1B8EA3CFBBED9A3BEB0EA3CFC3596A07F6CF63C557B60EC57 + 7FAA569CDF87CA5EBEEAEF39793FB42017F7C1E1D617488AC5ACD27B9F7EAB3A + DFA9CF3F4AEAFB9B3F360715EE7F5EFAD9FD44E70198FB028B562C72E9131048 + DD3F07CC0C97D100CA17CA53FFD97A358E45DFD54A9578004C8EFFD58FCD11E0 + EF7740516025FD148B64A0EAD1EFB09EFD6B7344E587544E59B8A4561F941DF9 + 16D8AAFED5EEE8F77F698EFD0084C5FA9663D2079FF50FCD91AFFED21C7FF18F + D2DFC205F481A73AB6CE39A0F3F837EA479F0B681D105BEDB1B6656CE5BC69AC + FA7C9CF3377EF837CDF11F00D31AF77715C2625DADCFF0C64FFFA4F01F9B3D5F + FEB939A0F0CD3FFDFBE6C41FFF5D7358F1032F18D75F35E662093C80D65835AB + BF0A8FC36A58C7C1CAF9089FEF3B9DDF6FA90B28FE5EF569A3F313D0F887283B + F46DF9BE426A7FE4BB7F6C0EAABF59FD560EAAAF137FF867B5F96B7C471C8F7A + 71DE39DFFAACFECD00CE8F6A5CB157F30BB563AC94EBF361617D58E73A7E1BF1 + DB2970180BE600C24A23FAC1A299BD900FE9BC1DFE4275158F971B6A5EB8BAD6 + 6FCE2F3584E5BA04F065BF63BB0CE7F74E99152EA61F03917F688EE97332D680 + C3FA6FF1FF021C1FD27100C001739506F80282C32A5875C36A58FFFDFD9F7D15 + 0018B7D061F10BF005FE3ED675E1A1AE398F5447FF3BFECBFCD7F7E87F1EED80 + C15C27749D21B4F6DDFF3A20EE9CAE61073ED375E081AE576A139E021EE8BA86 + DB7AAC8271030D008EE37FA77EBE8AEB1CD7B2F2228CCA79B905B7D25197B4AE + 91CAC37A78E63AD7CC27CD6EDA542F0761D55B01B0017240E40A767911C67B06 + E71777288B6BB3F2A25E2DE7BA8DBB69A0AF3D37D89B03F70AE2E479DF5F2C7D + 775EE4C5A0A2C98B6C15A07B983489252FD017AB5E42EAD510380C249EC0D5B3 + 04E09D56DB994BBCC004F82D16C0C800784AF5A9CB8B5393A781C105FEA26137 + D1C05FDD8B751FDE764AF7618568BBE2DB745F2EF762E5A90E7589F3C2D6B6A8 + 5FDA443BDDBFE3DE4DB9D2DCBFA9B78997C03EE4FE4FBF9A239CBCD46C268F39 + 052F89E12E5AE9818790B21545BBE76F2DDBF04E81C2ADD27C66DB079AA7A8BC + 805DCDC9DE3E1D65CC7DB6C4CB714ABFC91C8AF827E11105C55C8863A98EA170 + 56E4BD09503E1DA018484CC83C0E40BC5673C1759AE36DD47C6F93E673787041 + C4D9DE03ABE19807B62156C3D575F4900A38C63B0C5E63E225C4C39ABF1ED55C + 3434B00C8E971499C7028601C9D4D59C9679EDBA3A17F69C973970CC830F2ACE + CB9175FEECB9B5E7D1F102662D33086E61709D6FF3726640DD1A66F5C25FD58B + 342F852A0CB09BEBA77CAB6B054C1D03E30C7F4729C3E071C59757108C4BE870 + 0BBD0BCF487AAE52384EDCC20DF44CED1BA8AC7601787123AD3C00F0600FE062 + D54B7A15FD93A7E73EC06FEC375C01703C132AF4B31C1037835E54002F2FFFF2 + 3230CF8CA5DCF0D7798E13E6F6F300AF14CFC580DD2ABB930E2B642C7DF51CEC + E7F0E59B55AE309ED72957BD36CC22EF25B23BE9A55BF44CBF79BE1BE82EE0CD + 60D8F15C27C3E2B1D6F35851779D22AF7BF0427CBB7596C2AE80C0AD8570ACB9 + 94B516C47A49E4F1C23F3038AC87BD2E53343826F078BE8B67C7B3320C26DED7 + C6E15220B0E4F2AC0C92D192351343CA651900F7C160AF8BBD6C6DAC5BCFC097 + B53E2C71890FC9F5C3CA7758EE6F7EFFAC1B0EAF212E4EAACB5A2BEBAECB070A + D05AD763BD16DBA785CA5DF6B2F606BFF3D67D3BF5A22CAD3B1376EBF5E521AF + B5B7FD74CAFBD6E8ADBCAE5FD25563F4E3BE727D2BF3803EBDBC6EEEB7F4AD7C + F848A7DE280DB5EB96056F299CA5AFBC4F036E31BADF5791794F3B16D890CE47 + 7CCF350D3F2AE5AA9BF891E34E67F5D56939146157B9DEAFAA3A863EEEF6F7A2 + 3E789BB598F25E00ECF81AC5075A1A0058221E90B803667F895E1B009E077DB3 + 7EE700F865EA42E0E416BAFD93D53F51F74F98CBBAF5DA8B40DFC5E4152E4AFF + 9A5ACA26E6D2F0C59CCF536E2C839B4DF98C56B9900E6E90AED7ED27CB7DFE6B + 68D418739DC58C33B775DD7C0ED06022A03A636A07A0B5C68A0C76B3287F6500 + EC7EBB0078A18954CD1F4CBE6A5EA77E3B41AB93BD258AB76F2156F72EEC99BB + 5C93E2C84B6D43B40100D7BEA9E389E850BD24970178D1920A7F97AE60D2A9C9 + 33798A1386856D05C0AEE7F4409A785708ECCF93958F5DAC7E0BD4ED82DA2EE8 + 759E95F36913EE9C81BFC8797AB8C8FD86D60C835F3F5C2CA4FCB0D3951F90A8 + 5740326D786002F0EA216CC3EED0B2F5737AC8509EC2011C2E0F56C3527E85B4 + 5D19CEA2AE8BE75C467C1C98AB386156AE63F98114B99FF6E13289874EC3DA80 + C0C0D80A740D751D0F2BE078C82DC038D7277F1400CE9037AC88A947D9C4C178 + C07719797EE85F866BAD2EFCADE037C4033F8B12017C0F854ABA80D9E558F44E + 2A5D0130F919FC66513F60EFAE375AE84BBEE3395C3525ED1C5811475E2DCF20 + 18B7CEEC398CECF6D90018650BE000C2BBDF6E56CDB128A47C0977D3A457CD9E + 6856EE6151E9ED66C56EE21AC7DE379B6533FA7C73C79BA9F7CE359F3CFAAEF9 + F8E1B7CDB6B74E37CB6BB9C12E6117003BF4A2D54003003C5059FCB2EC468FC5 + 325CE711E2E239F65C4BE0D70B6EC5C257F170E1F74140E04D00E0BA80172E01 + 8F63E97BAA614F38D284398D754806BA06BD8E1BF05AE4E5FA00DE1C3708CE71 + C35F80AF01F036163581BAD5F2D7D0B74F7951B505C0912ED6369BDE2BF0174B + 5F602E0A10FC21FBF6E236923D020D80AFA82EFBF70287CBFE81580FED88C5E1 + 8BCDB68FD5C74717DA05E5125A979A49F636FC04ABA32BCDC427579A5DE76E35 + D3584A9DBDD16C3BAD6300814F5F6FB6630D5CDD390374C3B5730D71DB4C18AE + 9FCF15F7CE017B2BF4DDE934BA50DD735EBC1716BBADEBE68B776381FF987E9F + EF03BD3EFF26E0AD212EF016010548632D66EBB016E42A340C76DF017D6B3932 + 5CA0ADA10261A4810ED58529F0D7B2555B4060C5C92B8097BD34812C80971701 + 4C70153D87C51D56BFB7B1FAA56E01BB76D16A39CFA06616087CEF59813A0F94 + F779B11EC48AD040D70037CB8017918EF692CB87A06FAD636B62EFC70BA40288 + 0180C32DF4931F9ABD8FBF6FF6287E0077D000615BF5469C1038F66D68EFB36F + 9B3DC030C01AD6A42FFE147BF8E2B6B975E95B611E2A10F6C7FF3F7BFFB56347 + B2ADE982D45A6B32A83523480619D4A9A8929A4CB53257EA4C9249ADB548B1C4 + 16853A55289C0D144E5DF469F445018D73D70DF40334D04F663DBE61F6FB1CD3 + C2674490C95C3B57ED75F1C3CC8609F73923A6BBB97D3E86B97727C0D6430F03 + 682FE1619A811D722F4F1376A0A0435AB347884BD863EA01A18C2328EAA0D294 + ED19002320A1FA0A18D29FB1819A9B2E658889072CF0D6E1A2893ACAC051DA90 + C706DC2575786975B4954DFDB65FBFEF523BFA7AEA5EC9F7D28E8B8F4C56FF9D + 1DE31BEBF3DD6D87BFBE3FAF9D1B5094F3E41C0700D266CB503A7BEF926EBFF6 + 300D3868BEE5DA72F9BA1DDFC6BF6ADFC10FF76D0CD3A54769DB8D1769FBED9F + D2C0CD1769CB0F8FEC3C1ED8181CA78CF5838D6FFDB65CB6E359DF0C866D0CAB + 03E202D6B75DE13B01BCF23D64D89CCB7CB69C0E5CB373B2CFDC7C179CEB65BC + A61FA675DF5A3BFBDB6CBFF1386D063AF3F72BDF2D2F07F0591D7497BF33F01B + 084EF8F00C80EF7B1BFE4EEE61ACEFD4FE9E9BF8FB967E94DD666580F0FA6FED + FF8D71ECFF91BD911DAC9B08CB8D773879415D20AF402F8097947ACE4176D268 + 2355886F5EAE885EC39401BD0AE34E99DFA3C030795EFAE05AC04B1F8482CEB0 + 981734ECF78ABEB898D6FE91BD86BFB1DF70B7873FFDD6DA7502804B7F807096 + FDBEF1E42D2FACB00738C7D2B54511033CFDC4AE49EE497CC9AF61BCD0423403 + AE55DA5BB809315DAE6B883290B78F90CC84BF2F1EC0F2F0F56B6BC8FB35B8A4 + 7E4D36BB5FCF95D2BE5CB395E75ADE5CBFCF7CEE7BFFCADB1745F84BDEBD804F + B24F7056DF295E06E25EC23D8588111FB84730B0172DB77C94EC4B4B5DDFFB1F + DABD118FDF738DF7AF3C80B577F0B223567ECFFAFB4B51DDE0B70380F106260C + 3480D7EEBFE5252C01E10EDCA53EDFB375FFA66EE1A1F7BD4C9FD82F03E09369 + DEFEE32EF6F5A73CFFC0FB4D19284CFF261A4811F3104523613E92CBEF792A31 + 4701DEBA98BB380C46CC77C29CC7E646BEEFAFCD99F402DD5C9B37CDB779D162 + 1B9B2D2DE65B1B60AF6B97C93D7F330046CCA3A4D90060B3113E7AA6CD0967F2 + 426099F7017E057FE39C50A23C1CFEC6B9246D7288E81C26BA1359C6E7A502C0 + 26BDB0E800D884872F62EE0C0866CEAB79B4603073E7087A4935E746D4F502C0 + 2E006D00C082BA11FC46008CC730A1A3A3AD117DCD2EF81BA1AFF202BB313F92 + 047A2717502C68EC9EC208F00BB4E519C4DA91279DBAB2D3B681C878FB9AA6D2 + 9E3CB0D652C22F3B00B6140F60A4A84D0AFDECC0B77804AB4C9B494B3736F056 + 40B706C0B20BFAD676E5057E6B45F8DB0B00B7A906BAAFAAEEE7F36EF85B03E0 + 36E01BE5FBFE06C5B50D44B9E79AC8EC956952510D7F597371005C298EC59A89 + 7B115B7F95B386B77500DC026AA3047C0566556E6B3B9A34460D7EDBE46DA777 + 1C2FA2EA35B0588E8AEB63ADF6A9BDD70DBBDAB5A8AB8DC669196F544D5D9861 + EF64C2EE16001CD663F35AECF0B5DCD1345A7FEC35FC6D6BDF65B3B46D0D596A + B365756C792C29D60F57DB9A7F736C00B0AF1D778FD1D4BFB2861F2B9E4B1EDB + ECAFC046BAFA75A9F01829F09891F4A601F088E21885230D634A3D14DB75B517 + E80D22F474974D6DDFA8CA39B471B5DF8B04684752AF3EE34768238D562F0184 + D536B68F50F6D7EA1F00F835D5F24FAD1F5BD70FADD87BFEA08ADDDBB4FDE825 + 2E72BF73080CF81D3FC12E82FE59B81876AB7363B01B53F9BCBA70660DBF49B6 + 8D23C5F1FED68AE7D84B3ACFD8AEEE133F4BFD99740CF519E7A140AC4E90D654 + 83DDA858AFF64D5D84BB12D0B7CD03788C13A8AE095A8F89573D8963F2E840D8 + 265A93672E4FCBECC16CDDB6FD69F6627BF86062E9F5612CE54D8C1527A0715C + 097B6EB72203E0027EC74DB67ACB3BF40500E3355BC06F37F0ED56132ABA8C5B + ABEBD8C503B886B951B1AE17FC7591075297B0CED4C58790287F20B1361E9628 + 3EC898E2C386D456AFF2F00724ECC05DD3EC0C7F05801B591D619DD5476365E5 + FD7B057863BE97C6E1591CDA017179DBF87500B0F6FAF58759CB773FD866980B + BC1D93BC7F07F492172096BD691B45FBC51DC02B004C1A0170B44D5C94DB472F + 60F7105EBAD91EE2B734C29601F0B634757906BA8057BC74DD1BB800E25CCE80 + 78CAB20C702501DF36C5BA992B77660FDF028FEB7602C1DAF31709000BFE0A00 + 0B023B005EB73B09FC46003C1515003C15DBFA415F549AB4DA3EA7A50B77BC95 + 064E7C92FA4F7EE27075CA7A1B7FC3AE06D8B220A5052A169FB44045AAC52DCA + D3DC6B98852E6C19FE92CF50382F84A1D95B088B973D26F0FE2515FCD5A29A2B + 2CBC0180F33E6E7803E73DE0668745BBB8C8A7540B80B4F505BD02700578DB80 + AEA436BDA0B1FACD256C7459D014FCA5FF82A123D9039805D2A1ECFDDB2C849A + B0C774BE29026094175859745578E72CEDC58780C0DA2750E05702FEFABEBCD2 + 5BA71DE62E7D9785632D2867C85B2F1C7BFEBDB31E069A7D0E59D066311BCFA6 + C5B4215CE57B963F7CC12130A017A02B11B61930BCC4D2E5ECB5782C87695E71 + ECA39CE281852CEFEDADCD623C8C8F5B7D81B3EEFD6B65F29B3EFE2E6DF9C345 + 5FE477D06B12F84582B980807578DA063840DAD687B688BEC005445E65070A96 + 0226569DED005F79FC0A08FB9E981E1AD5CEAF81BC405CBCEF2E37EAC0E06E68 + 22099A007F37E0A1F787EF1B682B29746C0D68284BD4F79283A52FAF3818D278 + 94B701642D5DC7B8263C1B377C01A8B2BA6F6E79885D3C1FD99776EBB7B7ADEE + 9AB5B99E36E119FCEDCDB4F6ABAB698DB507DEE2A50B98C34B77CD57F6B9AD1F + 30D7BD71BFBFD594B34770F1CA041A370038C35D4459A182017B82B20E680173 + A50C14547DB7B207B0833DBC490B14DD06747400CD79E0299B0133653F26E397 + 31181B2FDD0873B3276CF6E015D0A56EA3EA2C2FC03B1060A7DA9232A60030F5 + 5BCCBE09586902A6F65F34FB77F7EDFBBFEDDF79FF77ECFF6B7D7811C2BFBB0C + 30F5F981C0847E66EFDE1DD71F7BEAB0FB3280D93EC375E0AC7DB64B5753FF75 + 3B8F2BF7D206FEAE97EC1C6FBC74F8BBFDF68F693B1EC0D79ED9E77B64E76463 + 58BBFE2BD606B87A99CF7FCDCF7FFBF587764C6CD9A3397F2F8C9FA1AF007006 + BF1C9BCF0D84CE0098947100C903D79EA41D37EC98971ED879D9E7BEF6306DE3 + FCAFDEF7EF74837B0FE763F079F53722259C34FF6FBE1FF3253B17B3EB7802C0 + 5BEDEFE850D8CAFEF7BE98C740FC9F01F501C41A13DB669343DB026EB7F03FFC + 8DD98B047BC90BF6526E03C0D81C169BF0E8F570D1450E824DD81DC0026E2DCF + 4B184060C23FE7BD7E2FA6CDEC054C3B6F0BE0FDDE527ED7171DFEAEF9C3D7F6 + 9BE6258F1F1A782C808BD770DE47B80380D13A20B05D9780CC8492E63AC17521 + 5E6F5CD6C75F50B16B550D7AC9EBC516BF7ED9B5CB5F5EB13221A4579CB5EBEC + F9ECD90BEC45C0E0B51FF0E24C06C39297AD1E8F5EAEC19E62A70F7DCBB53EE6 + B98E7BB95CCF890C81E40D8C1CFCBE5FC2419FB6FBC0E90C7DD1AA338CC17D81 + 3D8209077DC13D803DE47391C24377C9DB58DBA378FC9E359D3315CFDF22AF3F + 6CF9F73EF23DF073740C3C80D952A10383971F3E6329F7E03369C5111BC7B4FC + 3021A3F33D19090E0BFA66C04BFA7EC977B751BF45848E2E7B0D2FC0F3F7C0FB + 2EF2EC4D9CF722CE10390260A0AF0030F3114160526D2D419D472EB1390B1018 + 00CCDEC2A8D94B98FEA6057BF28B727A594E732FD9C8037481BE78F932CF620E + A639D79C419B87150F6041613C8467EECC115B22E0AD853DC35D1BBB6A13FBCD + 7159D9C34233FFB47929D28B89C5DB579AC373B7D914FE59516D08F72CF02B75 + CF97F39CBA96E6D702C06D2F5ECA0B581200EEB215C04B5E603802E0AEB6A6E8 + F52B6FDF2801E1DA0EACAD6D5309D55CECF21ED698D8DD2BB8D43B10364D5901 + 342E796CD686FD841D1403880B9C75D08B7D69F6FC456EB37ADFC2C79EE70480 + 057F2300CE5BFE74406EEED701BD6DC0576D6ABB20729B26910282793EA65C00 + 70036B17E6E7EA290B36A429F6ACEACFD8FE1CDC0D745F5511005306FA6A0FE1 + 5E0058E5A8BA9DE7CB7AC4C416002CBBDA60F32DB1CA7A4BADF1A606E696F6B1 + 6F5B19A94F674F61ECBCD8DF81BC40DA08776BD81BF36DB6B6FAA85705C05A6F + ABC5DA57CC8FA47A6D2EDA5C5AEB2BEAD9AE52579BB04E58B71B51B4F72DF2CA + 3AAD8735EEACC1D6EBB7AFA27A3DB7AD3E82DF0882DBDA23D5D56BC7525C5FEE + 1EA76A5FD687BBEBDB55AFFBEB18E370466A19A3A97F65D5C7A9794169F3EF08 + 80879DCB6F283F8E9D5754D739B7C8DB85B4A9138F2AB0577B0F7741E0D83EF6 + F9552AE7DCC2D4C64FB2F15BEC6315FD7FED18AFAD0269278C6F01B695C68F52 + DF25B58DED2394FDB5FA07007E4D757901E77FEAF8C3449D7AD3683FAA961F7C + 977EE700783CF2CF6C226D6E029D9B50976813FBFB4574F84DB2B5AF496D758C + 57553CC66F2581F0687B9D6376BE8B6E981B15C1AF03DE9636A8A98FE01715F8 + EB00B808BB3C73BB27499D7C9C9C35932FD5C9D337DA90B56B268A8466E6F8F6 + 3D4D9BD39736DB43ED1EF66B5C650F4D84CDB176EA4B1FBC869BB1CA3871D2A9 + BA2EF944D0DA4CCB8057D0573607BBC061C0701165D5A99FB77300CCB1867F76 + A463929F5040AD835DF626619C0278D91F86870707B65616EC557B951B512EED + 86D50501871586480F1D515D0F1EA6F870A2FA68AB1F90F2BEBDF6F08508FF1C + C0AF836020ADABF300D58C630F5E02B9B5046E23BC5539B611D0150056E8A949 + D8433F8D21E82B3908B6D41F6003FCCD003803DC18F2394A7037D609FEF652EC + DFB42F7017F502C0D865F336A5EC0FFD45E465F3B4282F1C64000C8825F4B280 + 6F07047700307BFEB6016001DC08740578C9CF58BDD301B0DA49B46DDA53260D + C0578A219F252F07CF5F09003C65CDCE34D93465ADB5DD9017A2E405EC9EB89B + 867C91693A0B4F9BACCF261BA7783068B12A4A8B59D4D11FAF065FB41200DEB4 + 27CDDCBC2FCD32C90398B63318AF005F0F9517527900370B6CC0DF0600EF4DB3 + 071079407001C05AAC8B8B77254F7D0C01C89EBECD22A2493017B501E07965F1 + 516DBD8D895476B7E1C93274B859C05C6879FA2FDC73382D2E00588B9F0DEC65 + 2194C552133624E8ABC556F2799135435E5780BEB572F8E7E0014C1AE02F79F6 + F2F5F08FEF08009FF4C5662D164BBE685C20EF2204247EEF6C5A5AF63DCC7B1F + 9E4F2B8E02843F4C8BDE3BEF624F45A02EE992231F381C06ECCAEE7502BFA51D + A19E49175BFB45472EF8623E0BFE2CE813D699F0CF80DB5DDFDC4CC7EEFD9476 + 7F7BCB212E1EC0B4252FA84B4A3F0030296DB009EA220105810352C0814499BE + 0D78283062F5591BF33C614DBFF1F0A77D67BF4C7D67001A00E0EFD2868F2FB9 + 171DE077D585AF5D78D6E13147D8D4CD7FBC92367DF68397B38770F6F4AD01B0 + 3C7F7D0FCFE2E92B2023D02B4F5D011A79FAA96D5D4602BF402152CAF44594FB + 01B256B7DEDAAEE5B8006000D297665708E8AF2DFF2D5EA8771CFEAEFFF25ADA + 647F0F0FE5FCF5B5B4C6FA0074F14445ECCF9A3D79B3F7AD8709BE7CD761ECA6 + EF8096B4E978000BF8D672180BC8BB98E11DA9C2250386F17875586BED0885EC + 80CFDB716E1DC9031929EFB0D0DA025005911D36D35F635DB67A40E5553C6033 + BC759068F9EDD780A3050C4BE5DC68BFE3C683B4DDD46FED68EF30D252E0EF8E + EBF773BDC3D00098E97BC5EA7F7894062EDECF9EBFF6BD0F7C7F270D5CB2EF16 + 80ED9F2D034B7D5E426173CE40DF1CF6F9A19DBB1DD7FA0080B71700BCE9E2D5 + B4EDAAFD2DAE724EF7D2F69B4F1DFA6EB9FA2C6DBBFE2CED0002DF7861E30080 + EDDC6E3E4A3B6F3DB531F0F6E573017F81D7F6B9AE028081C88C6B7F3F13E017 + D02BCFDF816B9DEF0E0F62F2C05F523E67FF0F0FADFD23ABCB61A737D9F90281 + 07AE3F7208BC0EEF5C3C9DED381CDF53FEEEF659F97BF13FC2DF6ED0FAF0BD60 + C3E399CFA8F67C2F0E7901C8FC9DCD4E3FFFBF298AE321BC85D92FD83D78EDB7 + 409869F6F1752F5EB32BECB3C3611321A301BC82C2DBEC7F1B780C34C6B6EE8F + F63BB6DF2521A00931EDFDAD0F7B053BD0E5058A2F826730C7A49DB5E7F7BCD9 + EAF88D6EE57746BD95373BB0C583170F5DBB3E589EB69B00C51CA7F4E577CE6F + 9EF0D18485C61B387BFDFEE0290078ED27764D29DEC3B4F76B49C9FB18DE07E1 + 51DC1DB5A006C05CC3048001BF0E7F0915CDB5DDAEA968ED075FA7751FDAF592 + 6BACD93D6C74A907E4AEB37C037FB1D975986BB3E4FBB39B74BD5EE3A1A673B9 + EFB45EFEB1EB7BF1FCD57EC080E0357824D3A6847F06FC028581C1848046AB4E + D8FDA3847D76CF5FEC96AF45D8E7E547CFD9BDF0ACA7847C76F06B6996E5CBFE + F77D476C4C4B97F3C294DD4301C02BF00E76D88BD7EF6987BE51F5FD9932CA70 + 17AF5FE0AF942130F544F020CDE1A1E9CBBDFEACE701BF883C00387B09038473 + E8674980B7331FC961A12300A60E00ECA1A02D9FE739CC796C1E35F46E33DFC1 + EE6098748FB5297325248F609F6F95B9956CE4E3BC8B7AE573D9E67126E678DD + 219F87035ECD13E38B83D1EEA20ED97C94F9262F1DB22FB0E7998F560018E111 + 2C003C6BAB64753637D63C3A4A90B79600AFF22A6BCEDDD415B8DB28C05C07BA + 668BE56873080CC805C816514611D4D66AAB03D44AB5DD416E29AB2F12DC15E8 + 150C769B89B2DB4857640F60F7FE2D10967C0E29BDC5E1AFE0ED545EE0B567B4 + A94B4DC103B84B4BB6A4BCFD4F37FC8D5057F6589662BB5A6AD3068327D6F017 + 50EB7BFAE67D803B00B8533F920477DB14E12FFB030BFE46002CC92B3882DE36 + 356DB43651D6252614AFE0B85E11CB6E0302CF1E0E807DBF5F53BD96225BB357 + 30F9A0CE3A0CF95CCE7B017783DE1AE88E54AEE5FB0887BD84057CA3A27D18F0 + 2DE334F9B2DE568BB5BA4976EE4D44BFB026A67AD9B53E56AB59AF0B1A4B1B34 + 5AFD686AD638A72EB2FC70E8DAB65E1CD7A2C7228D518F13EB958ED636AA6D8D + 59FD748EA87BACEABCB546DC55DFAEB86E9F55BE874965DDB91AC3EBACAC7663 + 577D1C71828EDCFEA6B808E34CE8709CD63695BACEE36FA456BE542BB226E5DB + F89329C2DFDF2D007E15683A9A22546D535B9F31683C92D76E5527017F476BD3 + 259D535484B2BF951CFE5670B886BCBDECAE56F02BD98768FBB07F6F0AFFD8FA + 5146C57A57AF1F96C9DBA3961F7C037F5B2E74F5C567D48BD12B5E2C875DE07A + 9C4773D16E3E4729178DE506D0B9717414EB625BD9C6AABA4FDBCDF555C6545B + 8D53BFA9551F6F24B59D4BBB6C3CF6FFD5CDBA874685C0DCA823FC6D9360B0E5 + 870160EA6DA2C49E1CAF3CF1F27E05DC4EEF4C0C79EB6EE2B4A569B13D2CAD05 + C62CDDD03D61441C4B6394F1747CC9C72A758D688FE76F81B982BD848176EF5F + CAF2082EE0B706C092DBDD03D8BE97F0F975AE5184801E0FCC0D63715C87C105 + F2468F5F07B8A300DE36E1BD8CC8FBDEBFE5C1A2094DD4F6D051F2D85A1F5A7A + D823009E307BB51D23ABF10206D6CE5E65E35BFB106A69D2BCFC00D6067C7B29 + 42E0BA0CC8F5D46C0E830BF0ADDB5076E88BA833E9A152FB1B49005AE0EE386F + 83AD037AC92BEC73AC73300BC0AD24005CABA9AF24780BEC8D5018791E45F85B + 3C79BDAC34D815FE394AC0370A501B53E5057A23F0450AEBAC726D9F6179DA6B + 8C295616E8AD433D93A20888C94F5965E510025A7208BC565EC039E41CA1A0C9 + 036F5950221434F6A966C3031800EC8B4E5AACDA6863993CB41D5EC165218BFE + 84749EED9E0D783FB010C602D9FE347BEB01972F82D107CF882DD6AF405FA43C + DEBF4D18E8B2E0C6C25B86BEDD009885392DDAB9776FC937B6ED6FA57948F0B7 + 7890C485C40880631DD2A2247BCCB9B0ED7E372D90D78AD7E530845AF0F4C5CD + 7DC7D2422B13D270E1DEC356C60BE65817E815F86D1644B1990480A5ECFD6B7D + 0EE08973D25543DFE805BCF8D029D369CB77F6016E3C7F0B005E709085DFBC48 + DC5928665138EF3D887CB1B800DFC52C167BB867B3BF8327F03987C0C05F16C7 + 379EFB2A6D38FF952FAA038001BE0E758F7EE8009854B097361EE2B9D44751EF + 90F8C427BEE00F70C5F357E1A0490F5E79983EF9D37F4D6F5F7FE2E0569EC2F2 + EA05DC9222FA3B04B0768203A4CA0B30D3977E11F4622775A050DA22BC7D81BE + 2B2F7C9D569EFB3AF59DC9B6D5F61DACFBF0BBB41E0F3AF7EECD1EC04861A037 + 7D8AD7DD0FEE6D07005EF7F145B37FEF802442144113BCE7802F02BC82B59405 + 7305781DEAB83760F1224456264F1B4F4D40248747A60C8F7E704F43F7360CF5 + 0E9BF062FCF646F2FD72BFBBE522DCF3FACFAFA5CD5F01B46EA58D5FDF4C1B08 + 01FD1D302DEFA7EBFBFA5EBCED9EA728EF1B5BC22F339695FBB15B9B8DDF668F + 5C0026E0D7F7FF05B67D9781AF04D4C55B53700EC5D0BE0E5B1DF0E53AF6D705 + D8D23E87FC05E8599F4B789ADEB77677FC1C3D643110F8FB0CFEE441EB7D19E3 + 4A96C025DEB978F2CA6B17588B9417F41D06790BE0A58D83CBD2061B75B4E118 + 9449D566E0EAFDB4CBFED7775D7D9C765CB676ECC56BDF73FF65F617BE9FB65F + B5F48A95F1FA75CFDF7C5C3C71F11EDE7E95B0CF0F8B077006F2DBAF035FEDEF + 7AC9BEE34B7C3ECEEF71DA75EB857DB66769D395C769CBD52769DB8DE75EDEFA + C3036B67C7B8FED001F056424CFF70CBFA58F9A68DCFE7E29CD9CF9790CF766C + E02FD01700DC6FDFDFF6EB77D3E02DBE036033DF413EAEEF216C722FE52B0FEC + FB33116EFA87476903FB11F3F7BAF6D0C6BEEB0058FBF9EAEF9E41322F14F077 + BCE5DFC76E3BAF7E6BC3DF70E70D3CABEF795BFDADF21EC4373A7FD3027D9BEF + D04499FF3B426BF3BFB8C18EBD11506BFFA3DBACFD00FF27D6863DA3B7EAFFD3 + F2848C664F60C47EC1EE3D5CDA50E761D1BF286017386CFFD384967698FCF535 + 07C282C2BE4730BF634B7D2F624BF94DF3FBDC463BFF0DDB6FD6DA6CFD1CEFE0 + EFD2DA4FBEB6DF7B7E9943BF715D1B1C025B9E3179A9648D5D4708E7BCFE9302 + 802DF5E8047F3015D88B048F79F1846B0FD106048649E3F52B7A02D7A9C030F9 + 35A4446830ADFDC0DAB06F30D77CAEC1B4F56B6DDE17D8AFC35616FC75005C84 + 7D2DE3D0AF5CABD7B1CF7029D3572FF9F02211DEC040E0D55CDF1B8F60F68DFF + D00130D27EC0CB4F7E94569E62DF60BBAF9CE4BE65654B579FFEB4292B1C34F9 + 15C7F1F43D9F9603718F5D70AFE10EFCB57BE211B645B063D83D114FE0E5EF5A + 5FEE95DC4BDDF317000C0C3ED3405FC23F4B02C00E7587D9735DF6F2CDF7F5D8 + 9694FD7FF1FC150006F8CA1B5830782E91460EDA5CA50060C02E1EC002BE11FA + C6F909790162E642CC6798D7007D7D7E64A9E63ECC85166A8E14C06E0D806563 + AE8578B18E3A9F93591D6DDAC47C6F017BFF5A5ACF077D1E6879C1DF1A00477B + 239B9376CF41997BEECB61A10578836ACF60BDCC389B6837C1F337025D49F658 + 17CB35389E61F3F499EB77B966996A181C3D7EA3D72F72E80BA40D12A48DB0B6 + 06B700626C51B1AD1481B0F2A86E2BE0DB26858446005F3C7C81AB8DB72E1EBF + C5066005C0027801BF51BD00B0C6AA0170B4CB16A5FA588EF9A6BCD8B4287BFD + 46CF5FC1D906D0EA256A87BF391FA53E7AD61E8BE231047C2300964D60977424 + 001CFBC470D05E0E50782250B805004B11024F9EB32A4D9A6BE3CFE3A5F7EE76 + CD7A4B78295FD21A4BAD0915001E09EED6AADB7AFF392BECFCFA1C04636B83BC + D1A6B2A4B154D67A5D2D01604160AD81A92EF68B75B5AD4D712DAFCD5EB71955 + AC07863544F2D95905E1F8B220C5FD7E91BC713B6BB1614DD655DB87AB5EE7AD + CB92D6927BD5A391EC515A876E5F8B0E36AD0F37F691356CDD9FF1BBC6E828B6 + 7935E9181DB59EC72B328D9E12DB7853E3FD461A913145B5F0A611B9540F36F5 + 66558EDFC6D3DA54C3CF5EB6A2317901D7FD6BB5F5318D3AF618C67865C53125 + 81D7DF5211000B0247C05BB78F75AE56F02BD98768FBB07F6FAAFEB9E30F53AA + DB8CFA236B2E42D6576AB908FCADD475A11D55F1E2DDADD7B9118CD627DE6446 + 53DD4737C8BA4D2C8F45F1661BC71CEB39B6F5953DB66BC67B05003C924605C0 + A84060CF6BD284541F2755A348933400AE4FF8F0FCE55CCCCEA4111BC7020A4F + 6332CEA453FD755CE5C33EC0717CD43699CCFD382663DA4414E00B889DC4584B + D2E49936F906CE16484CBDA43EB1FF383BF70C80BB27B1B500BD00E006D032A6 + EF21CC04D98E03B02DC037825F1E2C0477C7A20880F12C16DC1D6793EE7184A1 + 36FBB0878FF010523FB4447B5DCE9EBD3CEC007A57DBD8AB1C00037F1D06FBFE + C079CF60DAF32005FC9DCC1E3E3C2415388B7A81E0DAE3B796ECA4932C15E0AD + EB6BFBC4F230396E9E1DC3D2F8B0E90FAFC0D7851BACAE1BF2B641E0088023D8 + 6D006FB14F01E2567508C82B6F5F81DE0880A3AD01C0E479D02F90570038A6DA + 0BB8B3FFEF365704BB11FAAA2CC91E2500DC405E1365EC12E077E6EAC1AE3A87 + B92545D1CB376A2AE96AEBB7C6EA2CAD0130798583EE84800606DBB825ACDCC4 + 55FD2E07B95BF7A5496BB7E73D8001BE9BAC6F25856C76E1F1C0C296090F88D9 + 5BF7A79926E0AF036016D958486301CCEA3B1E129DC5318D050026FCF4FC1D2C + D81DC8DA71A0847DCE0B6F79310E6500AC853D2DEED536C49E7078866851312E + 22BA2709F9414B092B5814F799936208678574CEB6EE054ED50BF0C6BABC00DA + B1B9CCA6C5542DA8C605D605FB32E475D0BBBF1BFC3676B365484C0A00CE21A0 + 01BF4BDE3AEB7208FC169E3E19FC027AF1FAC9FB0EA2EC2DC42231619F7DD1FA + C807963FD7847B5ECEDEBD2C621FC53BEA0FA9FF934B69FB673FF8623AF077A1 + B55D429F007F057E256C02BF0E7D4B1B16E957B0E07EFA3307BEECD30B8C05E8 + 2EB6E36DFEE47BF7022614347679EA922ACF623F5097BCC0AD8040ECA3B23C80 + 29C7B6C00324DB7A0F539A433E13FA597942427BF867423FBB572F4024836049 + 7045E19F9577C83B82002C022F82BD02C0823BC05BE0109E828025008FBC07EB + B28793FD9AB0CD57DC0B31D651C6F377EB57D7523FE0F79BEB69237016487AF1 + 4EDA06B0FDEE66DAF01521706FA56D80BA8BF7D32640DDF7771C007781D5CB77 + 1BC90E08A60C90C4F39610D01BF0A22C108D30BEBEF76E01A978787A9DA5EE89 + 7B397B71920AA20AAA3ACCA3BEC03ED5D18EF3E61C38AE422253F6F3B1D4F7CB + B5711D0017500820248D7952A06704BAC04FBC7B1D829663D30E09FE0AF26A3C + CE8D7AD5697CC6A41DF568E00A80F7411ABCFAD8357039EFC59BE12E219EED5C + AEDBB91410EC9EC83E1600DE8E6DF579EF5F6B63296D07AE716CFBEEEC788055 + C22B0FDE7A6E752F2CFF2C1D78FA2FE9F85FFF2D1D7CFE9FD2F69B2FACED136B + FBD064C7069E3BC0CD219CF53DE4B1ECBBBCC2E7BF95FAF1F6356DBB6A7F03D3 + F61BF6BDDD0276F39DDD4D9BCB71F13ECE1018E88B87F4B374E8F9BFA6137FFD + DFD3DE7B3FA58DFC1FF931EDBBB5CFC47180BE0EB80B6076CF6207B7B7D38E6B + F7D3D0AD47F67D58D9DA0FDE7A9C76DDB6EFCDBE17C1F608DFF9CE2300E63B14 + 4CD6CB0712EDD446D09D7612FD6817C398EB4506CF2B0528AB8DD9084B4DE86A + 80F196AFB317B18791FEF287B48EDFA4FD3EF12A96A7B07B207F63EDCBEF3D7B + 0D732D405C178A87307DCC2E90EBD70B13BFF3B59F7E97D6707DF9CC7EFFBC90 + 622921A0DD1BF88FF9BAC14B200D38E63CCAB568F54776BD2BD7245700BF02C1 + 0890EBDEBE1F74EF11DCB4679F74BBA602811BF85B80B00070B32F70A977CF5E + 93ECEAE7A20E9BA55CBF91B601E0FACE3D846BBDAEF35CDB29AF00E69610D082 + BFF2045E59E0AFC0EFBA739FBBC8F79DE085A50F1C00935F71F223BB875D482B + 8E5F309BDD1F01C347B317F00A80B0DD0FB977BA7C0F60131EC002C0251CF48A + 2367DD13588037C260015D8585960D5126CD1EC1279BB6482F75017DB9D7FBFD + BE847CCEF9331E0A7ACE5E9B6B007FF1022E7392E8E51BA5790A5EBF1EFA19CF + DFD25E732079FD525E6C6368EEA3F911732AE65ECCC534378B705736C0AF4070 + 1B048E2FF011F1450038CE01BB22C114D83B1AFCF53636CF14008E020667006C + F35E41DE6A3E5BCF71897C03BC6DF3EA6DA06E280BF62AAFED536407FA0AFEC6 + 3C2990579EBE82BF02C0B267086B737DB681B1BCE02C02F4A26893046F558E6D + B10BFC463B3607B9A5AF24BBE4DEBE01FC2A3F0D784CDD921C8A99D0CFC0DF99 + 3606766CD3FAB6A669F6FC363578FFB60260537E49B8037A5104B92A470FDE58 + A7FA2EE01B14FBE1F92B002CA05B3F3FB7D5F5B28F451100B741DCA8367B2CAB + 5E36C15F01E3A60C101E150077F604EE00E0DC3EAE97A0BC0ED30D80EB361DB1 + 7EB3EC8D48F018F03B796E5F038781B83500967D24A98DD6B6EA35366C82C035 + 00AE15FB8D455AE38BEB7CB5FD9554AD1DA2265A212A21A0E3BAECA4A90B5DD1 + 86C6FBFAECBCAEB5DA5EA27D7414EAF4EFD4B729B6194D755FAD1BB7AF45079B + D68F1BFBE8D29A7FFB181DF53E7EB7D4AEA39A2F747844F739F4D6ABB6FF7BD0 + 887C29AA05EAC6BE631AE38DAB1CAF8DA7A1083ADBEA7FCFD2398F76EEAFF2D9 + F45D44D5F0F53711D0F71F007864BDE1B7267AFE285B2E02BF4FC58B776FB55D + FC515BBBDA26D57D4753DD4F37C8BA4D2C8FA4BAEF68D2F1DA8EDB66EBA9BF05 + 0016F8ED0580A3C2246A984A7D3359B33213409F20DA848BCFC2F83E71B40972 + F6EE353B70381EA38CD38C196DA12E4E0E75CC262DF0B681B878FF4E5EECDEBF + 73166D4C330085336C823ED526A915FCAD05001E37D5FE0ED333CC96F81C5DB2 + C9BEC3DC027FBDFF7426C97D69EABC3569A63D504DE30167863D1C14208C0482 + C72A01E08994ED41A201C01EAEDAC4F8E501440F1BCDC34878B0E97E18190E80 + B3685B00F0AC55768C95364E06C0F200D6FEBF4800586A60AC49B09734AA06C0 + 02B911E8AA2ED6AB1CD57807037C175A3E3C60763F9C92F2809B41AFF202B952 + AC6FAB2305D6CA0600AEE56D83A2B76F037C435E65DFF39787F60080057DEB72 + 1B0016C875381BE0AE449976234160298E31C5347DD5CE346BCD2E87C0D46113 + FC95676F037C2DAFB243DFA266AF5F60F05A1680767B2ACD5C3FE4F017C97337 + 7B0177F60166010A3B61A081BF02C0DACF4C79002E8B5ABE5FEFB6BC872F7579 + C1AB037F1B2F6016D3CA1E690E894B5F2D8CC5C53200F0829D6F990EA5F93B0E + A6793B0FA639DB3304CEB2B11C08E3299C3D7CD19CB2B097F7FA2D9EBFC5AEFD + E3B498D82C22EE04FCBE93E696C5485F90EC0181290BEA6A0153628FBB85FB3A + 5EBD8DE70B8B967B8FA6F90510FB42E8C1E369D1816E101C1755B380BF41FBC3 + DEBF84832EDEC0BEEF2F40F7D0291BF3A48D1141B0D5997DC95BECFB7BD6742E + 2D7E9BC5DFCEBE82D90B8845636B63220F085EE6FBF9E2E59B17AD971DFEC0FA + B188FD51EA3BF6495A71F463ABFF202D3BFA515A7FFEABB4F1836F12213501C0 + 4BAC8FA06E2D87BC2504679B7DD5E9ECAD25F84B1E482BAFDEBC609FF302B9A4 + D891F2F40726532F4FB135C001F2A84000526F631230400204D1B6CEC1C470AD + BD80D76EF6E6CD5EBFC090ECF9BBEE63C160E5F1EEFDDE433B6B3F4F97EFC559 + C9EC02BE0E7D83D71DA9BC7D3D9CEC5719EC123ED641AEC9E110F0A6401F52DA + 6CF9EA5A02FAAE637CFA5A1B441B6C5BBFBE91B67D773BADB731D7595B79CFE2 + C1BBFE9B1BEEF1BBF15B201930F241DA62A9EBE25D07AB841BDE7CB1037C3378 + CD7511BC926EFC0E8FE16BD636035F3C33F1D014C8A58C3A1E9F198C4A82ADCA + 4B11A89246F80B08C50B1810DD0069FA3844CC7D258DE7F55606200280056ABD + CEEC0060790647A9BD40A92024619F635EEDBD8DA59DE313BAD9EA7E00F23E4A + DB2CBFC5617BDEE3377F9607EEDDDB6F9F2F8793EE8C3D78F371DA79FDB1B57B + 64A9E926E1943344E59CB7DFB03AABDF71F3B9D99FA51DB77E4A9FFCDBFF233D + FC7FFF7FD317FFE3FF9906EFFC94FAAF3D4DBBEFBCF0B6795F5E20329F87CF97 + A1F2B6027BFBD10DFB9E6ED939DCB473BF6ADFAFA9FFC69DB4E3F6FDB4F316C0 + 1BAF61EB67E7B6FDC623CF6FBBFA306DBC687FABEBCFD2D9FFFC3FD237FFB7FF + 2B1DFBD37F4D1BECEFB38E70CBF6D938F781EB77D3E6E25DCCB1048007EC330D + 5C0100DF4D8337ECFBC22B98CFC8776BE2EF449EEF54FF4FD8F89F02D00AE03A + D4B594970CE4151E01F1F66B365E81C4D80586D59F7CB367B5D545793B4276DB + B10943BDEE9B2BCD9EC3EE696CFD36E33D4FFF6FAEBA17F1BA2F2EFB9EC10061 + C0309EC2ECC34D2876C2B3AFFF32BFBC01D8C52B9810EEFDFCC61D065FCEFB04 + 5BCAF5C2AF1D7E6DB89CD67E46FA43DAF005A1A7ED776E796CA49B79C1836B09 + 80996B0CD78E20AE45DA73DCF55107EAAEB7947DCBD799D654D0576DFCC5166F + 9321B1EF256CD7DBB5767D95E41D1CA12F79D91C0A179BF26805F703ECE5DACD + 4B3B1BD86BDDF2BA5FE87ABEF2342FFFF0A210D122A8E3FEC17D85FB46DE0B98 + 3C5EC184885E7DE63387BF6BD9C7F824F7AFBC4770F600E67E63F7C69376CF32 + ADC093F8B8DD038F7DE07BE6AF640CBB2772DF5C61F74C3C811D0817EF604071 + 9FDDEF561EFF20AD3C6679F612B67B34EA4384972EC057769511799501BFB5A7 + F052DFF39FBCDDD3D9C2C1F28BDF41D6D65FFE2232C8099F67F8FEBF6F9DB039 + 06730D9BA3600B1EC092BC7E2300660E435B6F5FE642805F89B90E732349F3A5 + E8E52BC81BD5560FEC6D8BDE32AF80DE5A00E0088105823D35D500587078AECD + 1D7909B11B0293B7F96DFF1E87C0B3B7E6F9ACCF87C37C362ACF916D3E6D73EC + 28C15D415DE56B1BE09717349120F0CC0D5657815F6CB3645F67E3037D0BF8ED + 5201C2D34CFE32E81A7B8EC156A06DBD17B004B4555E6D750CF282BAD433068A + F6087E23FC952D425F52F6FFF536F6EC3661D17A87BD2BECEFB5C1E6BA8BED7B + 9FBE82ED7AECB9CDEA01C0D3FBEC1805FAF2DC27E0AB67C2468BA9331598DB06 + 7505766B005CB751BB28F5E19C517C5E8ED2B374F410AE9FAF25D58D453500AE + 21EEEB4AC057F0B7ADCCBA85D630DA94C34117F94BF179ADA45957096B267539 + DA1BCD245D9EC617805B4B4037AAAD3EDA64F730D0A54E4037C2DF5A6AD3A691 + 006E5C0B8B76D6DF580F1BA9EF48D2FA9ED6F3DAEC51B14D545BDB2E695D3100 + 6079FE4E9AB6C835B1D83B6BB68875E391D78E355677DFEEFA587E5DC5F191D6 + BDDBD7B6834DEBC78D7D7469CDBF7D8CD7533CDFE17C61AEB5693B8776D56D47 + 6BFF5A2A0E7B0DB7696BF386D5CA88DA3402008EB6466F9065F55639761B4F93 + 043ADBEA7A684C9EBFBFB574CEBDCE5D9F2BAAAD5D545B9F1ABEFE26EA018087 + B5ABF41F0A000FFBE77E33D2A6DD4D997CCB85E0F7A778F1EEADB60B3F6A6B13 + 6D51B1DF5854F7D10D32DA5E5763194B6DDADAB6D97A6A0C00783C8077B2D236 + 05B03B92EA7601B6B6AAAD3EDA4A7E021EB4536D526822AFC92175BEE7703C56 + 49DB2671B1ACFA9EAA212E1EC05396A469B357A5A5AB76A405CBED216CCE6AB7 + 8F047F9103E069F6FD8E0080791372B23D14F09628FBF27A3F8E396DA9DB80BF + 0BEC416F8E3DC00189DBA0EF583D818703E0FC3001F4F563037F2B00AC071052 + 3DD4C43AD987E7696B0F5280DAD9D903387A010B0E6BCCF820453EC2D9087D05 + 74494702C0EC2FA4761A27D6CB56D7F140E90FA0D5036927B45537F895A7AE40 + 6E2CD7F0577D7A8161FA350058E3B4480FFB4AB10DF306B6FF9718EAB9562F0F + 602490EBD0B6405ECA82BBB28F0680D5C7EB2C658F61423DCFC003D804DCC516 + A16F84BF0E86ED37E7A0178FDE35D9DB17C04B5A03E00888D99B1700EC8077FD + 2E07BF78FEB298C4C21221A03D24340B2B9B76A7A91B6C6C4BE5B52BF8CB8296 + 16BC80B6DD7519FA0AFC76F26591CC52E5B5DFAF16C92200CEF0170FE0838D17 + 7084C07306AC7E3BEDF2025F04C0D81DFEE21DE27BC3599B5D399CA0F6E96511 + D1213060973C30774F07044BD8490584B570190130FBDC2DD89BEB90C65AB0E7 + 986B3E0B9726C2432F3978222D26A4B3E517ED3BEEE982FD78D864CDB7F2BCBD + 2CB076349F0555EB83BAF7EC0B1E3D6F9D4E0B0E64EFE0C5564FD9BD7C2CCD90 + 372FF4B2089CF71BBCE0B077F1DBE7ACDDF9028981BF17B287D261BC7C3F7200 + BCF4BD0FD39277297F92561DFF2C2DB774D17B179AFD7D81B840DF2E6FDE027A + B1C9DB973A4031A21EA93DB655A733D005E0027959A867F19EB2406D84BF2CF4 + 23E5694B1E084C984FDA2DB5F6CB0B1C76A0FBC1D73EAE4287BAC71870C1526C + 4D48D160A3BD00C49A0BDF98BE2D295E6CC00D206F061C00911CEA347BBE3521 + 5201B9656FCD1AAA38A0F9B44061C9EC0E792D25242BDEB91EAAB994DD465F6B + 0300DA4CC866A0D0E705007FF943DA84CDEAB051661F5242CD6273CF5FE05209 + 372BDB966F6EA4ADDFDD4E1BBFC623F786835C040806003B0466CFDF8BC05E42 + 3C3FF0FCE68B78A60279EF274206136E79D3F7B9AF002CE055E3EDB89EF7A775 + 187C39035E79670ABACA26F88B2D825F8153B517448D40D5EBCBB1913C6201BF + 9C07A9C356A0611927C3D7CEB1106339FCBD11BC792D655F5F6C82BCD4E9DCB0 + 3900966877334B6D49DDEECAF016312E7BEB12C2D943395F618F5CCE3B7FC76E + 2F305B9F8FFD78D59FF1066F3D72088C27F0CE722E8C8B172FE7B3FBEE53B33D + 4DFD37D8F7F779DA7DFFCFE9B3FFFE3FD393FFCFFFCF01F0D6EBCFD2864BF47B + E6009870CC02C03B6F3C74EFDAC15B766E37EC3BBC6EDF3FB23CF09774DB35FB + 7E0B04A6BCC3C3403306DF6306C0ECEFBBD53ECBD6AB56BEF532ED7FF2CFE9DD + 9FFE73DAFBF04F5EB7C53EDB36FB6CFE37B3EF9C30D3F958FC8D00C184A3C6FB + D96C781B5FBBE53687D17C8745FA9BD52F139027B43761C3DD8BD7CAFE7F5880 + 3036C161FFBF00125FB23605EC92AF1501B1DAF81897E96B76132F3AA0E67F9E + F309FDDC53F89BAB1E129D30D21E2E9A30E81E6ADA7E875F5D756F637EBFEC11 + ECF01748FCA5F5E1BA60D70BBC78E5C1EB2F7C7CFE83FDE6AFA60DA6F5BC2C62 + 297B7AAFB736EBA8FB0AD06CE70CFCE57A50AE35F49707B1BF7462B68E17F0B7 + F9E594F2D20AFB03739D5B6B76F6085E4BC868BB06EA3AD84437B072BE5EDA75 + D4AE97EBC8038CB93E7F687D3EB2F12DE5BAAB6BEFDA0287057F1161A2F108C6 + A65421A2B9D66F601CAEDBF4319B223AAC3AC37D038F60C24C77B4E61CF71222 + 4AF0C291DDBF4A68E8BEE2050CFCC5F317F8BBEC18B0977BDE4769C5890FEDFE + 45D86880319EC1D8AD1FA1A34F7C9CFAEC3EB71CF8EB5EC0D69EBE0510F759DB + 5540661B63B51D6B152018287CF4BCA5A663DCAF33F4AD61AF406F2F001CCB4B + D9D2C16C6CE9B090FD7EA5B74EA6F936AFD0D61078106B2FE04500E1E0115C03 + 604160CA805F4246D3760E7321B3C9F3572FC6E5F9529E436177D91CA70DFEEA + 853DA502C1E4A307B000B0B7B3D4612FD0B7CC0D6BA9CE41B020B0A58D1AF8CB + FCD2340804E645C40C817358684B076CDEBACDE6AD652E8C349795347746D337 + F3F265F6E89522E88DB6E8EDAB7284BFDE7683094F62402B30D9E6F4DE1F005C + CAD44539E48D656BA71741F10A16CC9D5980AEE0ADA02F8AC057924D6DB0BD0A + 00565D9B7D721FCF645B7CDBA15936F67E9B277ECC7EF1874EA5F1F66C0AB89D + 617D81C43CBF4DE519AE80DF5A5DCF8B01FCD6EA05764782BE520D7F1B086CD2 + 3373E7D9393E477720AFF27AD696543F9ADA00B04240D750B70D0ACB56D745D0 + EBB0D76CF200EE02C165DDA297EAB59108745517DBC43EA8695FD6664602C063 + D1AB00E006E89636B5BD96FABE0EC4657D4E6B646DF52349EB7B6DEB7C5D755A + 2FACDA44D5ED5A453D1106598F3539B435B12FB0F6068EF235D9314A3059E5D7 + 19632C8AE737F29A79B0356BC7B28F2EADF9B78FF17A8AE73B9C2F0C87BA6E2F + 219B3BE7D4DE76C2148ED13EEE6BAB66366D6DDEB04684B8AFA3BF09F895CA79 + B7F13449A0B3ADEEF72C9D73DBB9EB3399C68FCF8AB661EDA5D8466A03AF6F5A + BD4240ABBE01BD45C3ECADE057B20FD1F661FFDED4F286C51B518FB736748189 + E536D5178CBF8DB8C8F6BA90772BB6EB25DD18639F571D6324FD1637DE91146F + CAA31D7BC4FA02801B38DB2601DB5FAB7ABCB14C9E42D9275D9A78055BF6F465 + 62B5344D9A6113D53231F4368462E10D3C2071E8EBFD5AC6AAEB513DD9F4F3B2 + 6309E00AF0E2013C6DCEEAB464F5CEB470853DBC595EED68A376C3E4E3E2CDDC + FDD66304C02E9BD40BE2B20FF038EB0B00A68C07F0ACC51BD3741E76A80FE19F + A3B08D260160F279DFDF0EF075F1B0111E4290EAF450523F9C445BAC233F618E + 3D50016D67E305DC513700B607281EA6BAFA62EB00DF36EF5FD9DB00B0F23195 + 629B2E6133350F957801570FA43CB8026FD9FB9754D0B686B8230160FA0900C7 + BE94D51EBB43607B886F80AED9E4058CE2833EF5B587B000700D823BE0376B2A + FFCF80D9028123F0A53C6929EDB24D76B56F05C092B593A6A3027B81BAEED11B + 00B06CECFD8BC8639BB2D28E65BF3941DD1AF8C6BA5876150F6042404F5DB7A3 + D503C1B579C8BD7F01C0A482BB110047702B109CCBA419FEB29086370521A1F1 + 008EED480581B57836C7F200E0793B0E65C8BBC3EA1DFE22AB3751F6C5B88183 + 79812E7875F8E29D7B6CBC95E6954541857666E170310076FF714F170E1D490B + CAE2A22F1C968549162823008E5097B216301DF296FCFC3DA41DEF605FDC04F0 + EE3D9E169A1C020F1DF5FCA27DEF3736B4B0847706DC2279FA76A9805FF6EBD3 + 9E7D02C008B04BCA422E75EEE9636AECDE87FDFF58303EE30BC1CBDE3B9F96BE + 73CE5480EFBB17D232F2EC53580070DF313C963E4E8BDFF9C0FAB25F210BD77F + 48CBB01D01EC7E94165BBB2578331DFFB881BAD10358DEBE82C094D98711017D + 2923FA79F964F6F065B11EEF2DD2B84F6F5EBCEF78EDC685FD7AA11F888B5718 + 9000299CA83CC61C2C00154C020A88BA081C481B1B1EBE78FA5E004E7C955699 + 6DB5D5033704390436043E5C00DB22F7E8FDAC1B04034DE4DD8BDC83EFCB1C86 + 957A87BD05C290670FCFB5D61E50B3F18B2B691320D784C720620F5FCAA42AE3 + 45E82166DDC3EF4A6E5352BC0C7D8CAF4CDF5C778F5FA0AF802DA076C3B78453 + 06FE027BD9B7F551DAFAC3C3B4F1FBBBAECD1781BFF7D3962B395CB087EF75D0 + 7AA7404B3C86EF38AC3CF0F0E774F0D12FD906842B504EF094943280AC816405 + C65227D029B08728CB43173563D9F180A478CA0A969212FE9974E01A2188F3F1 + 750E8CA7BC8E87372930D1C7262D40570058F5FDD7AC8F897680D69DB71E79BA + FD06F09336B91DDEC3E4A91BBCFDC4CEFD918F457BC602F00EDD799176DF7EEE + C097EF108F5FF228E7EDF8D60EA08EF82C02CB7C174060CE6DA79D27C096B11D + 885AFD20E1916F3E4E00E01DB75FA65DF77E49EFFCF85FD2C97FF9EFE9D0F3FF + 94B65E7BEAFB01036881B603363EA19BF99BEEBCF128EDBEFB240DDE016EDB31 + 6FD8778DAEDB777ACDFE56849A2EF9AD576F3914C603B89FB1EC9C190BF9B8D7 + 9ED8393EB3737991365F799C36F262C1B5C7D6077B06C5EC19EC7B165FBD9D76 + DC0230F337672FDF9BF619F110E6EF0410BE69E3DDF273CAB03DC377FE865B01 + BAE5EF9AF3F6772E7F5F04DCDD6C75FC2F62278CB63C7E377F67BF1D79FB06D0 + EBE1A02D559970DCDBCD465900987A858D6E3C8B0B6826E5D8F20AF7F3A3DE8E + E59EC4176F38181678C6639850E9EBBEBE92D67F73D581F080FD4EF100DECC5E + C05C57CA4B21BC10925FEEB0EB806913DEBDF61BDE6A0200AFFDE3654F01C29B + BFC1F3DF3E03BF71EB1B43C393478A1AC031B856391836BB424573CDE2DAD6C0 + E172ADEBBA0E06C56BA5F2AB3EC8DEC01B8898500030D76DAEE59491AEDDA47D + 01002B2C34D769A52EAEDDF4A19E7B83E5F3FD0261E785A2EC092C00ACB0D040 + 60E5A3E72F1098FD83F3BD8CED0B3E7200BCE294DD0BDFCF9EC192EF115C5E84 + E25EC9BD75393A4A7868EEB31F3A005E75026F616CE71DFE46018001BA11F602 + 76877BFB76A0F08A0A0A2F7E17D99CC024083CFF90CD394CF3CA1EC10A210D00 + 9637B0ABEC058C00BC005F24201CBD7F1D00333F621E840D605CE64B8868291E + F984FE367FE2E53881DD0C7B33F08DE59C66082C7BF6FA0DF0977A4B170CBE9B + 1616F9FC50A0D7C45C5121A2B1C7970855F6C832C53E9F9711070581F33C14EF + 5F4240372A1EC19ADF46E85BCF97BDBCA90376A334FF26DF057A8B4DF658A739 + 3C60B9C917F1322770575BBD2040EF9450A60FF5BC0CCABEC01E2EDAE421A20B + CC8D76BC86057605811DFA963E9423201610F6710AFC8DA0575EBEA4AAA32D75 + CA4FB3739BCECBAA4B36DAF3D596B4F5ED33E9BD4F2EA635BB8F3868C5037826 + 7D2CE5790E00DCE6058C9A67456F67B6007BDB00702F09F43AD8B534DA230456 + 1BB5ABF7059622B4D57375FDAC8DD44ECFE4BDA4B11010370260950578658BA0 + B75603768B62DDEB0060696279C9BD596F3169CD23AEB320D9635D6C4F186820 + F0C402661B706B2215B0AD15ED6A17DB2BDF067647B24BCD1861BD4BD23A5B54 + BD4ED74B23D5C731A49EF561ED31B689EA6ADF4BBE56682A5EC01240589277F0 + 68EBB8B5048047EA37D6BAB8165EAF89EBDC505B7D47C1F61A0038AB73BC7F37 + 00DC4375DBB1B66FABEB29E0338CA6709AD6366F580DE7098CE88DE8F7E0011C + 41675BFDBF977A9C5797D7718F3675DDF8906F54B797DADA09B6FE968A0058B6 + 087C47552BF89546F8C07F2FE28F21002C0DFB671F9B86FD985BC6D48F3EB68D + E5AE7A142F1821EFAAEA7BEE35DCCBDE53BA7077ABBEB8C70B7EADE66652CA71 + 9C5AB1DF9B543C7E2FC5F31CED9CD4B6565BDB5155C2268FA808717B69A4B6B1 + CE520FD70C982D93ACD60955A88F6DE23EBE8D8DBE9419B780DEAE3AD2D04FE3 + 69EC6897EA3186B7B7896A01B80DDC351BE074D6C20D1E027AD2CCBEA6CE816A + C947B9DDA1356F35B6EF7122E57D7EAD8F207009F52CD88B27307594A307F06B + 01603B776F6F1AF66041DAF59091DBF040E2FBF2560F26516AC3438E7B169B6D + E2DC1C5A7992A513E61439FCB53440DC098CCD43D51C1B0BF1A054EA6B45E85B + AB86BAADA0B7D89B3A1E425B1E3201C0CA771E4A33C0D5FEBE82B6AA13C0AD81 + 6E6D93BD173C6E007078A08FE0B7DEEB4900D8BD80178F0D002B2F008C04751D + DAAE0C9EBEAA2705CC96FCE450E76038C8E12F9EBD410AF51C5349501800EC70 + 182FE0D559C0DDE8E1AB7CAC934DF9B807300B3F0A29A7C5201696583872F0BB + C9DA9630D082C0110023D90473F342170B6107F362DBF6B73CEF40D8C3E4E576 + 5A0C439481C079CFDF12EEB940DFBCC09643EE45F88B7C6F361BDB17E9CA421F + 5EBFC0DF1812D0F356C7C2219E26C05FD266D111886BE9025F881C0E7A9B30CD + 65B1322E64AA8DA0AFDAE2D5B264FF093BD6FB0D04AEE1EFDC3D475D00DE18B6 + 39EEE94B3A9F7C057DC9F712F5CB0F9F4F0060F70C7E87D0CED4E5855C5F3076 + F07B3E2D79FB5C5A71F8C3B4FAD82769E5D18FADCF85B4E45DB3BF67FD0F037C + F156FA382D3DFC916BD931A02E80F793B4F4A8C9CA406079F74645B02B4F5F81 + E106F65A7D77DB4E286707B82CF097320BF784F404F2027FE5D18B682B00ECDE + BA2CF45367F9B505DE0A026BFF48C102EA510407B4218F179AEA907BA639B8C8 + DE6AAB2F7CE9FB5C22410CEAF1E0DDFCC7CB69D367973CBF1E08526088248022 + 98D2D80AC041BE0F6781C08017F7EC05EC143093BDFAEC385F5F4D9BBFBD9E36 + 027CBFBE92360173D963D4D26CCB7987BF1E6236D7AB4EB0D8ED966E30ADB7B6 + ECD1EB21A0BFBF95367E4F0A1C0394012233FC05DEEDB9F7731ABCF522017F37 + 39007EE810180F4EBC56BDDF4542E8022E33B0DC73F745DAFFE027EBF7C4A117 + 308C147829004C59C08E149BEA1DC8AA8DA5B4C18EA7AB034F80ACDA021C4D78 + FB36E193818F768EEE396BE7030C040A0A0CFA7EB340BB6B567F038808ACE433 + 016E6D5C00EB2D20E5C3D47FDD8E79EB91E7070889EC6D38D74E5F04C89417AD + 526C3BED3BD8790B6F5C806786C57E0C6BB3EFFE8FF65DBDB4737CD40580C923 + A0EFCE1B78F99694E395EF00F80BF445437711C779ECE7A8CF05641DB8F92CED + BCF3D2F463DA6E7F4742316FB9FAD4F3D8FB3D4CF4933478FB997F56423213FA + 79C72D3B16E19E4DC05FF6FD05F66E03FC9A08070DF8DD719BB676AEB7696FC7 + B3CF38E06017D967BE6EC77200FCDCDAE4636EB3636EE173DA77B48DEFDDFE8E + FE9DDEB0BFF16D8E67FF13576FDADFF176DA7507986DB6EBB74CB73D3F78B79C + 5BF1B81EB87ECFFE9E19F03BECB7FCC00DFAF03DF3B72A10B8FCBFF513561A88 + 6FC7657FE18D0500F7DBFF84C3DC26B5CFCAFF0DD0F8B2F5C306E8E5C5858B39 + B4B97B0F9BDDF7AE06E81600DC84A3B6E3E6F0DDF9EFC6793820664C3B0F9D97 + FAE039BCFE5BFB9D7E77356DFDEE46EAB7DF28619BB966785877BB3EE0C50BFC + DDC0CB215FDAEF9A70EEEEE17B336DB5DFE3C6AFAFA7358483A6CF9757D3C025 + FB3C846FB7DF3CFB0EFB7585FE96EA5AC34B27442268C24A9BDDBD8C39966C26 + 5DCBA4E6DAE6D742B305110A5A309890D05C4BB9F6AEE3DA0B00E6FA6AD7EFD5 + 6EB3EBAE5D8F3DCC73B976FB359B3E56A61DC226E9DA4D3DF785955CFF4DDC1F + FC8521B3AFC4B3B7847E5E73F68F699595D91BB8EF1436E0305B0BD83DCBB4FC + 7DBBDF1DCB2F3C710F63EFFB358CC77DEA14C0F803EB6BF7436BD7F114A67DBE + 7FFA8B506CAB70C4EA88A0C11EC047CE3B0826543410B88F7D844BE867F706B6 + 32796C0E7A0BD845EE118CDDD2A5C5B6ECB0D9B597B0E597BC67F38677F1F63D + 693A6D65EB6FC72405060382179497C71C14570058A0193BDEC0405E815FF2C0 + 610162790AE3190C0076304CDB324F5AB8EF705A7C20CF9134AF621EB660E848 + F104361B2FE7B90DD89BEB48BB95017186C11DCDB5B9E03C9BF72DB0FCC20284 + 9917CEB2B9E2EC019B5FDAFC30CF13DFCE2F095A4AF418E68DB211469A3925E9 + DC9D66DF993D80997FB22509CA7B03774242FB3C15D9DCB64D9AEFFA1C999724 + F1E635CD2C40B781BAAEDDBE5F30758DCDE6E544EE710F62D3749B93139D07BB + E0B0B7A1CEE6F69AD347004CEA00186FDF528F2803805BA12F20B787D4865412 + 0876610BB0575037DAF0F255C8E70EF0CDFD943A485EB3DD9EADEC99CF9EDB66 + 98DD3D8B69637D7D0F60AB9B66634C2304349EC30DDC2D5A62CF90A649AEBC8F + 2F003842DD36001C6DB247C8DB805D2BAB4FAC8BC2E66D2BB8CBF3F4B8796B5C + 82BB02C3B19DDABE0E00AEE1AF5403E15A82BD0DD82DE5A808801B084C5AC02D + EB1F02BDB554D7B67E8222E08D6B33511ACBD7496665E01B35AE2AD780568AF5 + BD60EE68F6BAAE0B2C576B5C383AE4B5AEDEEB6F6D7528AECF8D56DFD6AEAD1E + C536516D6D5BC5DA62F1026E34797E5629FB5A6C50AF75DD6803FE7A18694B63 + BDF21110C77EB14D545C778E6BCF757FB555BEA360FB1500581A37D1BE1B53B4 + BD8E3A9FA9660A73ADBEFBF8DDFCA15B755BA9ADEDEB6B8E7DE6C0625ADBBC59 + 75B19EC282DE887E05C31ABBCA39B731B5B14810F4D72802D536F56A1BED450D + 001EA55DA3D1EAA3E298B15F04B5BF9500C00E81834604BCB1AEADBE4BAFF025 + FC5EC51F2482DAA8F80F1FECE3835ADBF6907EEC13EC22E83FFAD2AFB9009472 + 531FFA20D9A5BADEC7A86CD13E36085C2ED2B495386EEB85BDA3581FD5D52E8E + 19C6EED5BFD70DF3DF43F1661CD5D6367EE6688F7DC64D663252006D541BBC1D + 49636967F5846376F8AB8991A5CD846A7A9834D19E7AB5A51DA9DB72BF5EF2F1 + AA72D36794BE88F67152EAFD8BBD5100BF51B2933A482DC2460A14AE3511A83B + 93D0D52D5EBF41DA7F3782DC1AF646C5BA088163FFB6BAE6DC00CA849C0E0F1F + F9A1036FE06EAF60EC3CB408EE3620983A931E76D46EA23D108DE3E1880795B9 + F6B0E5C015AD774D986B0F6A736AEFDD35FE20D5F56015C0EE5815E1AE006F07 + 16039D01C09D7AD43C448607CA6E751E480571E5C52BE57A1E603BC037E6DB24 + D0AB760DF0354510ACB7BA4991E02FA9F25D9EBF964713EDE11F45F88BA6AE60 + C1204360F2B50770570868E02C7596E2A92B6F5DE527591F842D8671261FA53E + 2AAB3FF9D85E101809F6B6A9F6F8258F8DD0D0A4481EC0F214D04291CAB23900 + A62E00605F6C2A00586A83C278FE0AFE7A983DCBE750D02C82ED1906814901C0 + F33CC45EDE6B0D003C77075E15FB5CD8E702800909BD33036007C5FDF9380B76 + 66AF0F16EC625840A43DE2582854E8E7B92C0AEEEE08902B882BC8DB59B0CC50 + 578B956D10587DD5CEC31A16D82BF0BB78FF099703E0BDC7D27C4B81BF40DEBC + 57EFE9B2976F86C14B0E9D4E8B0E9D74CF5F857D06E6CAAB576A4240177BDEC7 + 17AF1F168959300604B3788B878F1DEBED53695981BF4BDE229CE40769F5FB7F + 482B8F7FE20BD28B81C3EF5D309DB73C298BD51F3BEC05FCA2E5C73E4DCB8F7F + 9A96599F6565517BC991EC090CE4F570CE273F73F505105C435FD22E288C57F0 + 6916DCB3872F79BC81D91B98C57A3C8205781D005BEAE097057E16FE4D02C7F4 + 172400F6C650A15D600008608A76DA91AE2FDE66F4671C8030B082B0A6F25C8B + D27E9780DFAD5F5CC97B71FEF152034122180194004DA8571E200CBC69423217 + D8EBF902624865DB4C08D7AFF0DEBBE67B8FAEFBDADA7D63E5EFD9A737DB3C8C + ACA5B251C6B310A045EA79B5D39EA4974C17AD8EFC7737D3C68BB75C9B09617B + E55EF250C43F3C4A1BBFBF97F63DF853FAC3FFFE7F4FA7FEE9DFCCFEC8DA3C48 + DB6E3C4D5BAF3D4A5B81AE57EEDB5877ECF8B71DBCBA97AAA502AF789202BD1C + E49A00B6025E02BEA4C83D6AADADC02EB60C6A018FB90D60D6415AF1FCA40FC7 + C9E186F14AC67337435752CE85FD711D0217EF60F7102E6DD9A33603DAD2DEF7 + AC059EDA18779FA5FE5B8FEDF3DAF8779E660185AD0ECF553EAB7BBB5EE37CF0 + BE7DE2296338FCD5D866F33E96E6BD71F93EEC3CED9C86EE3C4FBB6E3DB3B11E + DB79599FAB805E3B17FB7E11DEC18863F9E704CCDE7C9076DF799CF6DC7B96F6 + DE7F6E227D6263313E103A7BDF72AC411B7BE7AD9769C79D9769F0EE8F69D7BD + 9F1C04F7DF789EB601666FD9D8D66617C7B9FBC2FBE4CF9FE16AFF4DFBCE9095 + 81B49B01C0C060B3916EBFFD200DDEB373F776F7D32E1B63FB2DC24EDBF770D3 + 3E871DA3FF5A86D0C0DFC1BB7822FF68FF3F0FD316FBEEF86ED75FBA99365DB9 + 6DFD6DAC3B0F4DF69DDEB0F2753C7DEFD898D8EDFBBA79D3EC77AD6CDF911D77 + FB4DFB7FB861FF530544936EE3FC8A773236520F294D08E91246BA9FBD85494D + 849ACEE1A63BFF8FFEF281A582B2C064D5B9ED87E2D16E76F523052CCBA3DDF7 + B776C0CBB9E5FF533CB687EE3EB5BFA77D373616D2FF7B06D71D31FE1660F237 + D73D04342096EB4623F7ECB7DF7401BF082FDF2DECBB6DED57DB75648DB55B67 + 6DD6DB1878C7F38204A1A59BEB0ED104AC1DF0D7F56986BE8A48B0AEA47E6D2A + E09770D07AB1C5CB964A7EBD0B521BC070B33730219B81BFE57AAA6B2BD75504 + 085EC13EBC17ECBA5F5EB8A11E3BD11D1A2FE0F3F95ACE359D6B3779AEE55CDB + 9B6810E5DA8ECD85DD5294EF41D963D8F3D499569CCA512AFAEC5EC6FD6CF529 + AB4396C70B78E5E90F4DD96378D9F10C80971C396FF7B90FFD0527EE7B0E814D + D8A85B020436F559FBD5276C8CE2054CBADAC644E41D0C1FB5FBF361BBBF9BBC + 4DF11AC60EE85D6A5A76C4EEEBA51D36CAE417BD03E8B5B90051400A7826CD10 + F8B4836087BC6F31E7B0B98BCF174EDAFC217B10CB3EFFE0314F81BB08F8BB18 + 0FE2028681BFA4F212D6FC6891D980BF02C068B109E8BB708FE599279597E6E6 + EF060203833BC2263B2FEE65407C24CD1D3C6C02FEBE6773BD3C07C42338D763 + 7B3BCDDA6EF3D032176CF60D661E3898F712A6DE6D56665B908E1D689CE16F04 + C03EFFF4A834F96545E6ADF94545A2D89898D39A6AAF60CAB2C539B0F29A57C7 + 393562FE1DE7E34D749E0280357F47D3D659DDDA8E97AF0400669B170160CDFF + 295307D48D4097B2F60C16F4A57E0A00B7A806BF2A5307C015E48D219D058191 + 40706DAFEBC94F432BB3CDBD8697D9B31F9EC30E803BCF6FD34AC86841E05751 + 0D7E5516D86DCBAB2C09FEFA0BD305FEC6BA0881799E06D246F8DB79B6CE6DA2 + E273782C47E01BC74311F8026C6359B69154B79914F2C3D62880BFA84059D63F + 581FD15A89ECB19E34AEB920D593F7759799CBBBEAB516A3F598DC9E3AD66932 + F41DA6994B5D11D646B5D54798DB4BB17D2DB571105CD6B6E27A9BD6DCA2E23A + 5CBD4617EDC3D6E92A8DA57E2C6AEB2BB5B667DD714A81BF410A05EDE5B0462B + 785B035CCA82BECA13523AB619CF7AAFA9D967B8B4454D9B70ACB16AF47E765C + E5C700801542B95BB9BFAF5BFF6A009CC78C4CA1ADBE4DDD2CA24DB94DCFF665 + 2FDF4675FDEF48B099C87A86F1A0DFB5CA39B631B5A8083C47B2458D56FF2B35 + E23EC31CBB3EFEAF3D1F8D598F1DA1EC6FA5FF3501307FBC3168C218DB0AE2D6 + AAFFE9CD16E16F1704AEDB4AA15E4036FEC0B9003417815237D12E92134A9BD8 + E76F038011175613EDBD0F17DBCE05BCA90F8AF56D6D7AF58BAAC778DD1BE69B + 966EDE6D6A6B3FD267CA62D251C06D94C35A262CA4C156D478F0065B5BBB618A + 6D047281BE336CD2842200F6095391A0ADD23ADFA6D1EA4750DBA44ED2A47342 + 80BE5284BFE3A6E636E425B51310563E4F8281BFA300E016D0DB0BF28E54A77A + A9AECB50BAD8CB4307CA1EBB41E181430F1D3CBC4CB6079B29F3ED01C9F2E366 + F090B13C4D9ADBB165EF5D7BD0E1C1694186B2E367F3D0630F5FF3EDA16E1E50 + D61EE02A003CC1C6880F56FE7055A06E2F0177DB6C11EE4E2A6DDCD3B71C5B20 + 7A621100176FDE710E8373B94B00DF02777B29BFC16C0FBC5DB60EEC957AD905 + 7B2300F63AD2228160C1E0986F2B67F08B581CE85E34100CCEE50C8005819100 + 30F017C02BD82B70ABB204E015FC15D01514563EB691AD81BD66537BD5778573 + 2E9017F5CA0B000B0267F8DB592CD2C25004C2BE28A405A64D19FC4A71614A0B + 5A5100DEEC019CC3ED213C80676FCB5EC033F178C0EBD8DACC25847311619FF1 + FEF5BDD42C25ECDE3CF7BC60E1ED509AE721F8CCB6EBAD34C752BC32E6EDC063 + 03AF5FF27801BFE39E1FD1F3570B7A5A0C44E4D91378F6A0D5EFB276883643EF + F99E75D1A3374A0058A25DAD85D4EDB1B678B2007A3DCCF371EBFB7E5A72E0A4 + CBC130DEBDC5C357DEBFC0DF85C503D8CBBE776FF6EA55F867415F6CA4805E41 + DE651ED619180CFCCD0058F925EFD0377B00535E8EA7EF3B1F98ED5C5AFC0E9E + 4578FAE6D0CFC05F4254BA8E7C94961FFDD8C4A2F5C769C9910C8157BCFF595A + 799285EFCF7D217CF9FB65AF5FD332F63E2C70D701AF973F4E2B4F7D9AC33E5B + DA7732C360FAB220BE8210CF966791DD437A96C576016016DDF1D8D2FEBCDAE3 + D117EA2DA54E50B8B19958FCF730A2964618806719FB4E3A143051BF066F326C + 96C7C38CB6D80414DCCB8C3678FE7E9453010849C0028F5FC0AE0360E04881BE + 02BF92876A35016FB67E75357BD20588035871B8421BABF3BD39CD4E99FAADDF + 5E37DDC800F89BBC27A800B0E02E52285AD5293C6D571BBC112FDF4C849BDD8A + 07E3E51CA27623FDBECF1078D3A5DB69CB0F782712EAF95E5AFFEDAD74E0E19F + D397FFFD7FA673FFE9FF4878036FBAFCC0615EFFCDA7EE05BCE9F25D1BF7AE7B + AB0E14B83A70E581C3D66D36CE0030F1BAD90B248BA920980420F330C6C0DD1B + 78CF16180C902B508EF0C6AAF310C766C79B16C03A78FBA903CCEC810B287DE2 + 1EC81E66F8DAC37C6E5701B3F2D6CDEDC83BA005D6DE7E92B6DB18BBEE3C4B43 + 0F7F4C3B80C0371FA501B30FDC02743E4A3BEF3C4D7BEEBF4CBBEE3EB773CAB0 + 977E43F75FA441ABEB0722BB572B20F3A1C9C6B731197F877B15E7F2A09D1FF0 + 57C017CF5FF27BEFBDF4D0D0BB6E3F73EF60F24064CE9DCF0A00C6E377CFBDA7 + EEF5BBE71E7DB2077006CC1C0301B69FD9F782F7AF9DAF9DF3EE073FA7DDF781 + C0805AA0EC736BF7D4CED1CECFBE4F00321EC4FC0D7652BEF3D0212FDEBD03B7 + 00B619FE621BC03318CF5F6BB3F5FA6DABBB9706ED3B190424DBB90380F5BF32 + 80F8AC765CEA01C47C973BEE3C495BAED9FF1C7F5B1B1FB0BBC33D80EF589F3B + F67DD99877EC6F740B909B434D03891DF8DA31B1017C1D02DBB964306CFF7B66 + 931C02537F1D4F5C4BF1FCB573DD6122F531F176D6FF5B91A02E79EAF0B8E67F + D9FF7F4DB1ADFEAFF95D6D04FEDA6F8B1717F8FFE4FF94BEC0DF7D0F5EF8DECC + F158EAEB22AFB1F81DF372875D0B7C8F5E52BB16B8B0DBB5610BDEC1DF9A2CED + 27E4B9C9C3BB9BFC6507FB5D6E242434DEFEF63BDFECED4BA8785E1629D72140 + EF7ABB9EC91B58E017291201E9DA027DFD7A879D7EE5DAA750D17EAD635CC62A + D74A8F9860A92226E0191CAFAF02C45C63FBCED9B5FEFC174D3D363C877DEF75 + BF8E77A23A007995722D17F8D5CB3E1104372F0971CDA71F29F566EB40E1B0F5 + C0692BD387FBD8898F7D1FE0BE532803E0E556CE7B04E3119C3D86B9372EC1FB + D7A3667CE4F54B0581F1FAC5DBF7F807A9EFFD0B69D509BC8229E321DC117057 + A23D36C15E52802F92E72F36DA6253F867BC7FE501AC3CF64505004B0060E02F + 30D8E1EF81027F9507E0E2058C6730369B2721C161F70436F91CCACA8BACCFA2 + FDE165396D9511003012F4C52ECD03F6EE3AEC9A63F3B8799646018067DB9C70 + 0E9EBC15006EA0AFCDF5948F7342D569EE08109ED17F20CDC4D397170E77DA5C + 96D47520877D66BE0AE4A54D11F35834CFFA23E6B7736C9C0C794BB41BFA0410 + 1C41AFE6D6F5CB96711E1EC5BC7D1800664E5FBC7E35A7F7797D81BD1100CB2B + 9832A037425C415D815F49F5B18DEC5D7098B400DC08726BB82B30DC4B4D3D7D + 5665DB248032F0D7D2FCD26E7E8937EF03CCB39C9EF5B204841B8F616C268581 + EEA55EC017C5760E77A30A0496E7AFC6680030298097360BF3CBD6931666E81B + C1EF481038D6C9264518DC067ADBBC81A5D8AED604526B432AD8EB6B1321F53C + F096B594B9D9C317696D4592AD17D4ADED6D36A9332E75E5A5FD220F0DED6B38 + C5C6FA52A5D81E65FBD22ED02B6FDE2EAFDE4A350096A88BEB5BCDDA5A81BAF5 + BA1BA9D6E79446FB58D6EFC6523F16A97DDBB9A261ED598F9CDA01BFA30160A0 + 6E0D6DA50874C74FC9B078F2F4C50E7BBDFF24536957F795749CD751EFFEF33A + F95FE901ECEBD66F08002378427B9B76C5B5F336A9CDF8C98C8B2D720B78C59C + E1B6DFA96033AF0D8047624E7F1395736CE573456DD053F6586ED18890F64DC9 + F9602C8F70BE6338E79E8AE3C6712294FD2DD506803D456D60F7F70E80E33FE0 + 486A01AF23FEB802AC6DCA6D753DDA8DE9872B591FE2CD3BECE5E26679CA02C2 + 4D9B32A6CE3D5E306453598AF6AE3AF22D17A2E1EA5C70A55E17EADA1EEB466A + F3F7246EBCBAA9B7D58F24DEB68AFD72F891006C05678BB87977D9D4CE342200 + EED567B2A98C397E4AE95F206DAF4993CBDA34C78B5037E65F4371B21635523D + 93BD66B25A006E2F09FA8E9BBAD45543E0588E93E05EF289F40C94A1AEC02E6A + 03BDAA1B8BD4A7D624521E467A3C90485EE7793B077BE0993AD71EA64C13E7D8 + 438E8DA1BE93AD3C8D87B9056BFD818807ABC93CE801784D8482C6FB76DCECD5 + 2E007017BCD58394E02F65AFEB405EC162A5BD007084C0849F16FC9D34370360 + 4160E0AE4239037F4923F805D67A1BCAE48B7CAFDD1635D0761409F6D61A5667 + 658575F6D0CE05ECB6C15E95A332E04539DC730D82550F00F670CD00D8150366 + EB7791476DF057B689EC114CDB086E0BDCA58DC06F84BB5171CFDF668CD53BD3 + A43E163F3A1058801709F846A92E4A1EC00A19A785A1087F5D5A5C62E169B3A5 + C5D3B7CD33418B55330863676516BB80BE5182C17802CFD84C1FBC7E0F357BB1 + B177EFCCAD077CBF60DFD717CF61EC3B5106B812B6B898A73250576DB4C88728 + 2FD87D382D6411D284C72FD05700D8BD800B08D642650C4F28B82BF0AB854B95 + BB548E0108EE0060164233D47500BC3F035F802EF0B72BFC73F1F655C867815E + C15E2942E09C07F09E4C0A0BA9908D52DC2370B9877726FCE487EEDD0B0046C0 + 5F20F08AA31F793DE90A41DCA39FA425873F36017809E9CCBE872C8E7F91569E + C95E51ECD3BBEC7D6B5BB404EF26D352CB2B8CB3A48574F2C060F7A82A6DDCC3 + 8AFA7A519E05F7B260AFB23CB7B4A0AF3EA41EAE397892399CFDF8DBB4EE930C + 17647368E075B99D8774361BFDC8031AA823456A8F800FF25E430009FAB10726 + C0034FB85E0292B0872F20C5C18D00EF17DDFB6EAEFFC2D22F0BDCB17A3C7EE3 + 3EBFC01EC2C06EF93EEF0F0ACC659F51415F015F6C78FAFA3EA584A32D7B8C4A + 84B7A56E1B5EBEEE6508ACBA9D43CEBA6C4CAB73006C229F8130D0144FCEE769 + E0D6F3B4F9CAC3B41E8FDF2BF7DD83733350D23D5309197D2FEF010CF475E06A + 75781417D82BCF4AE581B8B2910A92EDB254FBDB3A88B4B4F1F62DC2BEFBCE13 + EBCBFEBE803DF6AC7DEE00D6F7B1C57633835CE02EF0D4E1AF9D977BC6DE7EEA + DEB978EB2207C078F816D00BF81DBCF7DC6D5EB654C233983AD2A67DA9A3DC6F + E74BEA7980AAD5EFBAFFC2A5F6A483D61FAF5B344888E81B00EBA76908007C0F + 988CF72E501BB00BA4E6DCF37784D7EF2E60E7F5BBF65DDC4B071E3E4DFB1EBE + F0CFC4E7F350CC4060FADF06BC3E4FBB1FFC98763FFAC941B097EFE311FCD2CE + E749DA4AD86800EC9D0C2BFD6FE0E769DFFF9D87EE75DBC0604B77E2856B0200 + 03853300B673B1731BB2E3ECB4BF05C796320CB673B373417E5EF67D0DDEB3B2 + 1D13EF6141E5AD005B00AFE5B1F55BDA781C5B4AC86952A02E02FE0E5899B6EE + 1D6CEDB14500ECFDCC0E2076C84CDE8E8900C13B6EE4FFABE67FB3E481B38832 + D096BC3C7449550FB0CDBFAB5C273B79FAF16203F0F7C023FB6EECBB8DE3205E + C6D098B203910907EDE1DD117B7BE3C5EFBF69BB26B07FB75D1748D9EFDAF7F1 + FEE67A7AE7E95FD2E7FFF67FA673FFF26F69F0FA93B4E95BECF61BFFCEAE01EC + B36CA95F5300C9E5BAE4D71EC2C3DBF5285E9FFC1A65658447B05FD3CA758D3A + A0B0878E369BC3DF0080F5728CBC8111FB0663F3680905FC22AEB34456C0A66B + 30D75B5D73B191D296EBBC5FA30BE4D57600DC1710B6B515248EF71AF228BE58 + E4F719CB037EB5D580224D70FF5A8EE7EEE94FAC0CFCFDC4CBBE8730FB07B307 + B0DD1FFD1E7922BF14B5D4EE8D78006B9F60C24A2F3D66F7E1E359CB4D80DFE5 + 4701BA005C7436AD380610CED0579EBE82BD110093E20DBC983980A5191C5F70 + D02BE84B8AB237309EC0A7D2C2B7142924835FE60EE405805517CB0858ECB602 + 842500B03C829BBA32BFF27992A5847E0602B36D062FC831679A3F74D441AFA2 + A82007BF78F916017CF100060C0380B1F502C09A2F6A7E88541F6D88B600E099 + 03071D02CB7B98549A49EA2F38E6971C5D948B98C7CE19409D1721F3DC581038 + A700E0087A29D3A633AF6E97D717F05B0360E6F002BC35041600565868F27805 + 539EB97E974B2018F05B83E01A06475B04C0DECF5443E01AFE2285808E3624AF + E15A75BB0E04CE0038425FD249C0D722B7070180EB50D011EECA7317C57E51B4 + 77E0DB4300618DE1F0B72A7B5FBC84176DB0E75C9E79AD8EB583A01AF20276B1 + D5756D40B881B7654D817C1BF0CD6B14190CCB2635EB11969764D398B13C0E10 + CBCBEF0D98ED5684BFC3D6578ADA6C759D22B369DCB636D1E676FA84886E6D1A + CFFACE2CC06D37041E4D11F8D60018F9FAD68C0C7EA5B8E616D33A8F9A75BDB2 + 2618EBA2EA7E6D6AC61A456D7DA5B6F6E3A6716E0B53037B8BBACACDBAACD94D + ACD7B6697C59C3253F69FAA234D9C69E62C79DCA774AFF89F3D2241B37B68D7D + 6ADB9BD3BC4EFE5702608DF1264240A3DF8601306637B3D0E7A8EDBF57F9F9D6 + 8C2670A42E6E1315B9D3EF41EE44D9C2E8500D3C6B7BA91B15F4B68DD3666B93 + DAD5ED27D871C757B6D8BEB6C5F2183426781DA1EC6FA9FFB000B828FED046FC + 71BDAAAA1FE358C6ADCFC3C346D08FB1C6D93F4E68E3EDAB7E02C06DEA750CD9 + DF1800EE652F8AFD7AB5F97B922600F54D7C2C37F5BADF6800789822CC7D0D00 + DCD5A682B76D13A768AB0170EF49D6D8A4B1DB34521B2690CD64B580DCA836C0 + DBADF6BA8933BA616F2F4D08F0B74D35CC950DB5B595629FA8B10260F6F1D543 + C4042B4FD21BAEB357A629F3EC01CAE40F2373AD6C0F45D431667E905A93C637 + 501618BCC6C6647FE0D101B04360AF1B0E79A55E0018F01B21B08EDFA8F2FC8D + 8A005802004708DC06800570C7A22EC81BFAD5F60880E501DC4B00DF18125A00 + 783401823300DED100E05AEE091C5443601401EF480058A21D9EBFDAEF57FD14 + FA79F24AEB5300700D7923F8ADEBA204805924D27EBF5A20EA5A4002FE6E1E4A + B3B6ED756931CAF724B37C1B00F6BDCC0A0006F4BA0AFC254C339A6B65B76F65 + F1AB03806937D36C5A3CA3CD4CC6D9617DC2C21CD2421CA916F6C8AB2D8B7971 + 418FFC42F694DB7B24CDDF7BD83D7D058115FE19C0AB70CE2C4A46002CBB20B0 + 00B04458C346D6AF13DAF084D5BF6FCA619E01BD8B0F64E82BE02B002C5B04C0 + 35F8559867A5518475665156A09754611BB588BBFCF0B9B4E2C8F9BC3FB069C5 + D18F531F619D0F7F9016BF4BB848BC8BB2ADF10806067B88663C7AD9C797D097 + 7F4CAB4FE3FDC4E277F68E42005C16B8DD63D724108C2D42DFA806F80608ECDE + BF65919E85792DC86BF19EBCC0AF445B806FB3E05F16F8D701630BD8050E448F + 3207B52695A913D8551DC017084C9D3CCC54A7BE0214510060853F05F6023894 + 2A0FE45863ED00261ECE19C0F265F6B6F370AB450060BCF968E3E15781C1ECD1 + 6BF6759FDB9896DF7EF94EDA7EE59E7BEFE21598F71A650FD29B0E7601C0B2B1 + 1FE976F6A8B514AFDF4D00A4EF6FF85EA5DEAEB4CDC08A3D47AD1EEF6020329E + BC780D5E7D90B6E0C16BE9C68B77D2D24F2EA6B5DFDC4ABBEEFF94FA6F3C49EB + BEB3F3B03AC2F802821DECDD7C9CE1F115BC2B81704F2CC51BF77E06C0405B87 + 8A591EC2B9A4F2F0655F5F878FB709A78C472FFBDBB2376D2E37FD4C2AE3AD4A + A865BC7C156A19EF5B79F6725E7E2E76AE0E810BEC05004BD933D77427C35EC2 + 120BDC2A8DD0577687B80104C7B6BB1FE06DFBD2EB01BF7B1EFD943D8AED18B4 + 5358E9216BB3F7C14F690F30F68E8D75EBA9036120B0CA9C2320DBC349DBF884 + 7EDEF7E079DA7DC7BE839B80F3FB69FFC3276EF3CF443B60F41DFBDC80D6DB76 + EE80663BCED0A39FED9C0A000608DB7169B30D8FD4DB0FADDD531F1FC8BCE336 + 9FA71BF892F73D7F81C1765C602D6076E0161EBCF6370400DBE7E473F35DE8F8 + FE3FE21EC7CFEC7FA8F3BDE201BC13CFE53B794CA0EF16A0EDCD008081BD0E78 + EDFFC4CE4776E4E751FAD126EF45DC81BC0E874DE4F11C06109346000C44D71E + C102C0405B4160C1DD1AF492A7BE5663B7FEDEB694F9BFC6FB977C1C8354A1A3 + 0584B38DDFEF8DB4D5B4ED7BFBFDFA8B1FFCE679E1C3EA2F723DB8E37900B0EF + E9FDEDF574E69FFE5BBAF93FFF5FE9A3FFFC3FECBAF1D001F0C66F6EA60D5FF3 + 2289F5B1365C575CC0DAAFAFE56803DFD975E09B6B7E9D427A59C53D7F01B75C + DFCCE6910DCAB50C3B0018B0EB1EC005FE22CA5C07753DE49A29F935B45CAFB9 + A60A0093628FD772F2BA1EABCCB51A715FE03E10F7738F1038BE3854DF6BE4FD + EBF72AF2267905379EC1C061EE5BBE5FF01FCCFE495A759AED0CD843386BB97B + 03E7FB21F739FA0081971CC9FB03F7595D16D1322EA4A5C7CED9BDD7EED795B7 + 2F65040016F895623B01E025653EB00C707C14609C433EC7F0CFCA3700B8440A + 89F309E61002BCB2AB4C2A390CC61BB8EC098C0801DDB51FB0D57BCA3CC9F24B + D83BD8E64E405F6D9B01FC95E28B74F200766FDFDD780267F82BAF6080B0D7F1 + 329FCDFB6AB85BDB480586A334A774D03B60735393C06F33EFF497146DCE09E4 + 75B8DB01C0C05F7FE191F9AEEA8AE4012C017C23D4CD73E876008C2DAA0D000B + FAD6E057360160D5470FE0591BECB90015081C433F23D94603C09301B3968E15 + 00F702BB11FAA25EA0788AD9D8FB973D80E5E90B0CD6DEC01100CB4357905700 + B806BA311F15DB21C16301E0C603B828C25E01DFDA26BBD7B9CCF6770080C717 + 5B9BC68D1100A3B8D6A2B597D1D4B526638A638DA6BA6F9B3200CED07634D55E + C13500EE6AEB6B5B19FC4A6DEB6FA8D7DA9CEC6DEB77A38D19A5FEA3A9ADAFD4 + D6FE9500306BB456D69A2D92C7AF1C8048F1F89D6C63BBCDFA4C9EBA284DA4CE + 04141ECF3895E21AF09BD7BC4EFE3F2800FE7B937FE76D8CA6A8CDF6BBD41B00 + C0AFA5B1F61FED58B5BDAD6DAFBEBF5611CAFE96FA8F0C80E38FAC4D63695FB7 + 69D3ABB6ADBD7C9BBE56E6A68267B0EC6A2F45BBC6533E966BFBD80070F7C5BA + ED02DE1616BA96FA8DD6EEEF41714230527D9B4D138758D7058023A06D536837 + 26001CEB6A8D01006BC2E665DA4BA57D6C2B5B9B5D529DDAF5526C5F4B13493F + B70AE28E4D36896EB1C77147923C80C74D5FD6A508751DDCCE5EE98A90576D62 + 5BA906BF520D809B078192D7C3833FBCCCB3871F8E03E4B60799C9F381ADF901 + 6412B2F2147B909A3C97F6368E3F08599E07297B201B07F89D6DF9E205DC5180 + B7F610151FBC5C5E57B52BCA50B7A3689FBCC01E14E767F8DB0060802F6301A2 + 0BE81D0BFC4511FE2A3F529867A9ADCD30C86B52DB5E6D4683BF522F003CDCEB + B793E637C9F1F6ED7800D7F0777A81B682B802BA1100D7B057805876415F5249 + 0038DAB1017401BF5213E679DD6ED734CFF706BF9200705C1422D5E251B380B4 + D1EA37ED4E3309E9DCBFCF532420DC0580A9DB9261316D67E331B1757F5680C0 + 5A1443BE50267BC96BE18C947E006016DC0479A3047C558E8B772CEA0D135EBE + 05F622F7FA0DF0D7E17081C0A4F3299BF008563E03E4E2E9CB8265912F649605 + CDC5078E375A52006F0CF31CA1EFB07CF0FA555E00B8CD13587BFC027473A867 + 202F7BFA12FA1900DC01C12CE2AE387236F51D05005BFD7B67D3CAF73F496B09 + C9FCFEC765119870921FF95EC00E7F0901ED612AF16002FCE2BDFB795A73E6CB + B4EEDC5769EDD92FD36A16C7CF66AF2816C3F3427816E0578AF61A066B01BD81 + C000601B536251BE6DCFC6E5F4612C2DD697857DF5F3C57A85050D5057F040F9 + 06DA5A99B60208F401FAAA9DE0AFFA230187065000790BCC4002C092E02FF2F0 + A7005E6C5F64002C08ECB0D7ECEBFE68FDBFBA92B6E0A1FBF5B5BC2727D0445E + C15F5F75F003001EC09B507B8FFE903D7733B8ED780E920A3E450FC3062CD1DF + 841DF8BBE532635CB3FA1B6EEF675FDAAB8F3C1C312018008CB6E2057CFD711A + BC03607C96F04805EC090003850937AC90CAD1C316D82AC0EB4017E05740AE20 + 2E9EC084761634939D10C483B40700DFCE4078F08E8D71D7C6B6FC0090EE067B + C63EB3F29344C8E5AD57018136BE1D9F50CC1C7FB79DF72E3BDF7C2EC1E31758 + 6BFD80B21250D2C1A5D9A3A8AB3D7E05770581A3E4F10B04564A3BFA31060078 + 1B309B50D3F77F4CFB1EFC649FEB859D1300180FE01F1D0A03820937ED21A7AD + 2DD01C9048B8E7BDF7F104267D6CC2239AEFDD8E6DEDF02EDEE110D7CE9BCF7F + EF451A02343FFE25EDB4F3D951CE9173CA9F2903D541424BDF7F66B2CF74CFBE + 8F3BF63D014DED38BB1E006B3304DE8EAC3DE07500E07ACFBE13EAEE3D49BB1F + BDB0F233FB7C4072FB9EF85CFC8D8A5730DEC619B267EDB463FA98653CF7F4B5 + BCCAF20ECEC7E6EF93C16F84D2C05F8163DA3A0836E1F92BEF5FC02F6AF610C6 + F3973A97FD5F94DF0DBFB51A0263A7ACF0E43B4A7D532EF2FD9DAD2DFFCFFABF + DE0CCCE5376ABFCD2DE53729D88CF8CD0A04A306345F06F2DA6FD7E1AFFD5EF1 + C8BF6476E461D7EDBCF8FDF1128609FB76FBBDBEFDE42FE9C88B7F4D43B79E5B + DF3B69F377B71DFEAE07005B7EAB5D6F3CACBCEF237CC5AF334D5868C1E1729D + 8A1098D46131FB129B2200263434D73F0F555D52AE835C27656BBC80AD2DF61A + 02B3BFB0AED578F936D76A2B13BE1F3BD76D5DF311F57E7D36ADA7ED85AF9A7B + 09D24B43BAAFC43AEE21BA5FC57B5C7C61099BC3E0B3768F3A63F7BAD3760FF5 + 947B192F3F6518BCF274E7FEE70098FBEEF10FDD4378D5291BEF7DA26B7C9096 + BF7F3E78005FF0FD7DB5FF6F5404BE487058D078A983E133A9EF58AEE31E2FCF + 5F3C81FBF03CB634C35FEAF108B6F9C73B36F7B014E8EB1143CA7CA201BC26EA + 00C1D126B957B0CD7F0480057CBB3C808B8DFAA5EC1D6C75F38678E1CEEC26F2 + C05FF704DE93EDD8BC8DDB8EA5797B4D563717008C07F0102AF578F606EF5E89 + 720D85E3BC52D2FC52003887823ED8CC397D9E49641A207099B32A8D621EEB73 + E020857EEE9A435760B7B647002C3BE5E965BEAE883E02C19ADF6B6E4F2A8F5F + 52E5A923950730F077F6C6A134DB523C8123FCADD506800581A5E9D80A046E13 + F056692FD510B80D00BBAD8F67B60C80F1F275B05B00B05279050BDE026E0580 + 23E48D8AB0372AB601E07679FB622B8A6D2495A35D005821A3FF960058C0B7CD + 564BF0779C8D839AB589D104F02D6B24518060A4F515ADB1D48AF548EB3352AF + 766DAAFBB6D5BD0A00AE25F01B01B0D2B100E05EB6D89E94B1EA76D2486B7B92 + DA8CA6B6BE527BFB452E20B04241037F6B003CBE48EBB65D9A9CC55AEED4194B + DCCB773CDEBED316A5C936F68449F31DFE4EB13A0070BDEEFDDBAF8187F17F67 + 00F8B711DF6F601655D8E7F8B9A2FDF7A65646F3F7A67F0F00FC2AFD5FF5386D + ED5FA5FFAB2842D9DF52FF510170FC818DA4B1B4ADC7965437ACED08AEFA4DDB + E2F50BF09D643710F5AD61AFFAC4B26C71BC688F92FD5501707D236BEC3ECE70 + 7B94FA8ED6EEEF41713230527D5D8E8A75E3DF2400EE21B58D6A9B20D56222A5 + 895D631F0100370AA0384A93B391A4E3295F1FBFABCDB44E78672498DB66EBC8 + 26E5C543386AFCF4CE7147D2386B57C3DF0880057605706BD88B4DF9DADEA6B1 + 00601E3078589934CF1EA6ACFD78F634A6CC438C3DE4E0A58B6730E30180272D + 64AF9DFC20C53EC2E3ADDDE4F9F6D03667ADB5E358005A80AC3DA85560570F5C + DD0F51847BEEB469DA06F0DBA6087F05834782BD02B528DA511DFEB951A957BF + 08711B705B81DD5887E27EBF6A1FDB795D8B1AC05BD9A62DDFE66A407081BEDA + 332A2F187497B3584C1818A606FE16A00BA08D90B74D35F845F493C7AF14CBE4 + F1F645D3B0ADDDE5D037CAA1F0BA8EDCB3D7A4FD7EDB2400CCA28F1683581CC2 + 1B387A04FBFEBF4060169EB666CDDC666DFAF7A6D903FB3C45D8666E1BB2FC3E + B3EF77CD62B16C9B893D7F0B0466314CDE102C9469B14CDE1178012B14B4DACC + 3145C82BC545BB36E0AB853DF2F3B1EDCEDEBEBD3C7EBD5CA02E8B94F25889A1 + 0BA5E8CD226961538B9B0BCAE2E7423C7903E015F005084728EC6D02F04502C1 + 35F8CDFBFA763C81F3DEBFD9432743DE730E794901BE2B081D79F89409387CBA + 94CFA46587CFA555273E4EEBCE7C66E9470E7F9730D61116904B1868F700CE00 + B8EF44DEB377F5D93FA675E7BF48EBCF7F95D69DFB32AD39FB450EAF5916C7B5 + B88D58EC06FECAEB49A28EB65297979509A8AB509C520CCDC9623DA25C2FE0C7 + C57B2F9FB37280C082090E0D0A20A00CBC254F3BF2D82803148007EAA3FEEA83 + 475AA33F66C01B25D82B451B70247AD291024F80C04A0579D9E7B7BFECCD0990 + C14E1EF0BB03F87B0910C45EBDC0DA9B0E89E43108048E001809020B2EA93E82 + A62D97E97FDD05080608FB7EBDD732008E5EC078A192E2D18BB72A9EA9FD847C + BEF6D0B5CDF23954F1E30E5C2D107817A0F47601BAA6C6E397F3B3144826AF5E + 6019F08C3CB017D8E7C0CFFA2BBF9DB1EE016BCD7E0B6868C7296015A8B8E59A + 8D0DD8BD9B3D75D90B97D0D01E1E1A2F5FCEB1806079A1024023BC15D0654CC9 + F7002EF0973A3C7AF79A7617880AE0C5F315A84A7BC6C41EC78AC7F1316D2CBC + 6101B47B1EFEECA0D43D66EFBC4C3B6EBF4C0077602E22243410D83FCB9D270E + 7BD90778DF03F695651F604037801DD80BBC35018CEF66503DF4F0A59DF3CF69 + CF633B8E9D23DEC70340743B07BE4B8154D2DDA62140EE431BE3FE53FFBE7759 + 7EE8B18DFD90FCD3B4E7C98BB4F7A97D66B3EDBC6F7F03E0F07DFB7EEFD958D6 + 0710CCF89CC7AE07F6BD00C0CB71DDDBD8BF77BEFFFB7E4CA02DE0D7BD7801B5 + 00654BB3C72FA09CBF33101E4F6BFB5F2C40589098BE883E02C2F2FC150806FA + 0A02631BB899434A53C63397FF39FE2FFD6584EA7F13D0EB1EBCF6DD7779A45B + 1B4162FD4FFB1EBE26FA5316FC655C0030BFBFF87B8C121826E577EBBF73423C + 5FCCD05712FCC5AB9DF0E71EDADCFEAF076FD8F7FEC30307BE78FD02801B5DBC + 9B367F67D707220214D02B00ACBD85655BFF65BE7E6D639F615DAFCA354D36BD + C8021846EC5F8E773079BF16A2CF2E767904631708D68B34EBB9067F963D8481 + C2AB3EB4EBF9C7DFF81EC37816635B5DAEDF446DD0751D710D8F79C2FBEB5E11 + EF19A4F57D46F56B2CBF9E90D1D86863F736C15CEE5F6BED5EB8EADC1FD3F2D3 + 1FA765A73EF074C519BBDF9900C140E05576AFD5BD8EBECBD8FBFEE42769F519 + F6BEB7F2B10FD212602C80F7E48776CFFDD0C1EFEA931FB95695721BF04502C3 + E4A9CF5EC16753DFFBB90EC0BBB002C0F20226CD7B009FB4B2CD2D08251D5E1E + 13F015E04594258160D509F2320F5AF216739C0C7C91E6529A2FD1863C4077BE + CD8BE6EF236CB4CDB52CA5ECA0771F7B091FCD76DA5167223F67E8888BBAF9FB + 73A8E8057B6C3E870A04AE41709C37C6F9A4C0AF54DB557659597359040C1610 + D65C57F239B02BEFF91BC33E0374654302BC1102B779003B00DE64F37A9BB3D7 + 1EC051CD9CBECCF535F7274F3F5200305EC040DFE8012CA85B2B425FE5A34D9A + 818DFCEA1C0E3A4A1038E62581DD98AF55436187C078082FCB219F81B482C078 + 060B0293F77201BFB56AD0DBA636A8EB00973C76EA6917DAD346FDEABEEA0FFC + 1DBFD09EC55B20B0C0AEE0AE00F030D8DB662B6B09516381BD516AC74BEC82BF + F202AE3D81639F3886D7CDB53605D66A2DC5D753AC1CD75C621B842D4A6DD5BE + 573DF93876ADBA1F72FB2CD671B2776F84BB6DAA81AF60AFD2D8466B6D4830B8 + 5EFB8A6D7A89BE9380D4A51CFB8F751D50ED46D3ABF61B0680111EC194A764F8 + EB2A6BD55AAF05F6A2F1ACDB4E9A9F21EFE40569D2940C7FB14DB7CF8D1C105B + 1FF6036E73F8411AB756DDEEF564E7A3FC180030DB120EB797FE658C7F00E0DF + 5E5D6CA69746E048BF0BBD2E000EF53DC32557FDBBF605AEEAC6A2AEFE6DC7D4 + 98F5B8AF781CD758FA4428FB5BEA1F1EC06357DB182E7E88E5C7D8D62FAA691F + FB0735EDAC0D219E81BF0D002E366E32B48D30580038AA6B3CCBC7726D7F6300 + B887BDD658DBFDBDABEDA63FD20D3EBF79D60D775BE12F0A6D1A901B6C6D8AD0 + 37AA6D82344C536D8C92678235814916B660575D2CF7529CACF5529C34D693C8 + 7A0C41DE5E12F4ED02BDD350B7DDCB61DC113503E0DBED011C41AE80AF14DB38 + D09D6D0F0F405D93FA8C04805DD6278680AE553F8C4CB2879AC9D86CDC497357 + A7A9ECE3C3C3C614FB5EA6D8DFC2267DE3662DF576BC013BCE1F68D63A049E38 + 676D81B980DF2C015B87BAE1A1A991D9C702801B2FDF928F1EC0CAB7C15D015C + 41D7D8A60EFB3C0C049776F4E90579A3ADAE8BAA417014DEBF5182BF6D1A0B00 + 46D87208B1AC6CEB77017D25E02FE99415ED7B000BEE4A1100C73AC15EBC7D67 + AEDDDD847DAE0130619F15F259D0576904C01DCFDEA19E00183BF53336EEB1B4 + B350C422D0EC2D7B5DCD4252596C9ABEC5FA6CB1720580A50C81B1ED4F73B61F + F0140F602D7AB9F7435804130CAE3D2708058D1A00CC62DA60C7ABB75E84938D + 540B7C5AE4D3021D7594BD2E00DF611EBF26F7E62D0B94110093977D24001C21 + 702EB3E0D90D7A2304966DF1A153D95600B0BC7B230426EFE0F7BD738D169776 + 78F2B240ABC55ABC7556B247E0D1F3263C7E3304CEE097C55C60B0958F018059 + 58FEC417955904660198D417874BD8E71CFE190FE04FD20A2B0370579DFE2CAD + 398327B08905F23319F852276F5FCA02BD5A2CA7CEFB9B4D0BE1BE787EFE4B97 + EFA968298BF078F70AF092C6457980AFBCB8220C1638A6BC96C57A85F70CA19B + 8100005D0160EA4823E4D53E93E481BC887AEC82C02A6FF9FC07D7E63F5E4E9B + BEC8D0178811618680AFCA12700468222F3A4160B419E0D2025F106066DB7737 + 52FFC55BA9FFD26D87BFE409E5BCEE1BEB7BE9A6C3A1C64B30802480117B86AA + 0D20496D50A71F0018707CDDEC05005FC103D8EAF128FCE17EDA66EA072ADD24 + 8CF343AB7FE07011008C07F0962BF7D216F6DABDF6200DDCC8B0575EBFF2B225 + 3F047CBCF7CCC119104D700B6DB7BCBC2881690033E460ED4E07F866D00BECCB + F90C2CF1CACD294055E0B509C30C002DA03583E8676967393FC1585294212870 + 31C35F4499B100968CB523D4B947EFC31F1DF80E018101BF0FB2A7AFDAEA9CB0 + 63E31888FEEA4B7E1BB0FCF633F7CEDD7DFF651ABC8B7E4C3BEF90DA717C2F60 + 40F0F3C6131808BCEB0E7B01038009FD8C1730E5275667EDAD3E7E16CE81F304 + FE920E06EF5E606A86AB7C078FECB80F6D6CFED68FD29E472FAC3D50DBCEE581 + 8DFBC03E5781BD805F003020585098BAED77ED6F65E745DBC107F6DDDBB1763F + CCC7F7EFC2CE8BE3ED620F63E032C0D9DA01720927BDDD8E3FF4D0FE5F18EF5E + FE3F70E8CFDFB901FF0060FEF61920CB6398CF40DF5DF701F5393CB5BC7FA307 + B07BFF163B793C82A367BA035FCB47D0BBBDD8D446FFABFE328329426314CBE4 + BD6F01CCFA5DEAC50C297A1DF3FB206CFB66FBED6FBE485BD3256C28C360D26D + 3FE4DFACF6B7DE76D97E5F97ED3BB964C728DA8CD7EFC5BB69ABE76FA52D847A + 0600037CBFB9EA1A0680B9DE597EBB5D7FB65FB46BCCB737FCDA25081C5F64A1 + ACEB1DF5F19A472484CDD82C8D111304815596E4194C1ADBB9D7308018306CD7 + 6F5DB7B9EE23AEF3BAE6C7FB86EE2DCD4B4325AF178BC8AFB53C0058FBFE22EE + 5D4A11F7C1E5ECFF6BEA3BFB07137BD967088CA88FF73E42462F3FF1A1DD1379 + 41CA74222B8781CE1EC0ABDEFFD0C1AFE02F2037867CAE017084BF9D3644F6C0 + 63F87C5ACC7DFE086D280385F37D5F0098BD82BD2FE1A3DFC95B47C80B388260 + 802F29E51A0ECB1318B0BBF8D08906006B8E54CF9B322CEECC9548357FD25C49 + 7329D90581058401C0A4DE873D84F7DAB815008E12F8D53C32AA6D6EA971B031 + 07F570D03657F5F96B01BFF3D9CEA4780563AF95A3DDF0C263F6FE15C805FACA + 2BB8AE13F895646FEA999F6F2A00B8A4D3797113D0BB217BF822F7FA5DBFD3DB + 500F00C64EFB6900E075F65C626D6A000CD08D61A005766339A6B54602C023A9 + 0D06D7EA0580DDE3D79FE7B6A519A6087F239405F61222BA562F201CE16D2F09 + FE4608DC068023F48D1A09004709F08E15002BAC739B62C8E7086FA362FB0926 + 7901474FE00881D587B1C937EB1745DED6B7B4CA801740CB7A4ADC9B97B220B0 + EAA3C60271EBFA68ABEBEA36AFE2012CB8DB40DE11DA68ADAD565CFBA20CE08D + 8A6DDBECB1FF58D703EB76BDF4AA7D5E170037EBB7C0D202805D1359879FEF5E + C013275ABF713089B9B95CFA683D384A40B9B68F0FEBC2AFAF799DFC1801B0D4 + B197FE658C5705C07FDB757EFE566DECE2EF4B5D7C26F0A52E8DC091FE7D55CE + BB8DCF496D3055F698D68AF6AACD84C97332C4C5DEABBFA90BF68672630F758D + 34663DEE08C7E9A938562F4528FB5BEA3707C0FA40F597F05B29FE038E411186 + 363FB896FADADEA5EA071AC78B7D471D2748FBFDE670CFB94C5EE19FBD4DA96F + 03C0CD383DCAB57D6C0018E59BA1D476111EEB057FACEDFE9E54DFC47BBDF585 + 7A7D87E32633F92890B787FC463C3943DD087623EC1DD6AF6ADBF469991CB50A + 303D75914FE826CF5EE1D2A42EB68B13B1D13496F6A38DDB4C2C0BC88D8AE057 + EA06C0B277B7D3847554D983001EB6AF0A807B41DEB10060F7EAAD1E08241E42 + E2C30AA078120F3896725C40F0747B689B3C6F759A3C67759ABF6C6B5ABC6620 + CD5A620F76F4E52D57D3B8593CD0D803129EC02641E0EC099CA1ADCB1E9CA4E6 + E1C9816FB7A7700D80057A1DF25A59F580DF0604DB4321F055B056EA0580057F + 7B0160DF9797D4A4BEB58681DC1E3604FC9DBAD41ED8B199048405856BF82BC0 + EB90977EB4A37DA973086C12008E209894F0CFE43BF0370360BC7E81BE02BFD1 + 1358DEBCBD206F9B00BBB514F25972085C006FF4F61D9636DEBCDD12F0A54D4C + 1D0C6FDC93666EDA6B29F9BC3884E7EF9CADFB3C6541C8B5257B200077675A3A + 63EB1E176579FB927A7D01C0D9963D7E81B82C7A29451108FB8258F19C40F29E + F0C53340EF4EB397053A2DC8690FB668535EDE1C5AB08B5A1080AFE0AFEF3917 + 6C7151B206BC2A47BB162F5107FA66E5366531D241EF2907BD924030F9256F9D + 76F8DBCB03983C5EBE846E9416BD6BF5EF9CCE0BB740DEE319F82E77AFDFEC01 + EC62E1B700E0BE63EC0178D621B07B051D6731F9C3B482FD072D5DF9FE479E67 + 21D8BD808FB308CDBEBF1F370206B36FE1B20284F1085E59E0AEE0AF3C7CA3B4 + E84D9EB694F3027806C55A480702B3E02E90CBC2BB16E0B5282F1B8A8BF5A482 + C09459BCF7BD1FD917F8C34E4850E02D70004528206020681021AFF2D4D14E79 + A06F973ECF8037026000456D73BB4950046F38A0481718A9C0AF3C7FB1B10F27 + E077CBF737D22642435FBC99B695BD7F23DC052009A40294804600257904AB4E + 7049E029DBF01406FC5EB771180F38CC7880641BFBF2BDB4F512FD815B0F8BF7 + 2F9E8FCFACFCC88E051C7B9AD837773B50B300560F3D8CFDE6636B0F2C7BDA00 + 60076CC0B51BC5ABD252F6FB450E804D0037F7C42C7057CA90B7037CB56F6C47 + 4F9B10CB1ECE18B85B00A803473B1F0F476D7679E1E28DEA1EBBD60FD8EBA014 + 605B84AD1F306C6D01B6405C07B7056462575D86AAD9BBD58F6B7907D026F22A + 2BDF8C63793C7EB307F02F69E8C19F4CBF585EE9CF69DFE33FA7BD8FACEE1EE0 + D7CECDBF67BED7A769DFA3E769FFA36769AF7B003F7400ECED380EC707145B9E + 63ED7BFA27D32FF9B3DC07CE9A1E02ABF3770834258CF3AE078FD29EFB59438F + F2B9E6F37E69FDACBDB501F292EE00D09A00BF0060C030E5ED00551B1BE109CC + B1861EDB793DE4BCF08CB6F3052E3FCCB0961498BBEDE65DEF0F54DE6D9F0B80 + 2C6FE246F6FDF2F7CEFF077808DF6FE02FF2CF6163C93318A08DD8177810686E + F500DF08857348E80C7601B5FC1FF2FF8A0481058623FC8D36A4BE48F097FF67 + 8D2318ECED2D1F7F93C87F1B267E0BFC9EB7F1DBE55A80D72F7B80FF907F97BE + EF6F913C82150A7A337BFC7E6FBFE3008081BFEBBFBA9E367CCD1EC2B7D2866F + AEA68DDF5EF3EBCB66BBCEA02D78F516AF605D9BB81611829E175000C0847D06 + F2724D23A51C213079D24D5F66EF60ED1D0C0406E20275057D95C69766744D15 + F4C5D656E6FAACEBB8AEF1285EF30919EDF710BB6FE87EA1178B740F598DEC5E + 23E81B5F5642E4B1733FF317A1CEDBFDCDB4F29CDD17CF66359EC067ACCC3DD3 + EE517EAF3B6BF7AB93769F05F89AD82B788DD9D658BB1527D81BF89C43DF087F + 057881BA2B8EC5720700D780B8EF38F7F3F369899581BF4B08FB6CF77A0160EE + F7E4731410E615598483C61318F8CBBC421018282CE81B01B0BC80058781BF1E + 092578FFA20C7C3BB6DCAEF3629C606F67FED4A9635E3577A8E3098C00C15119 + FEDABC2CC05FF22A0BFE46E0AB39A4EA7BE569A71711A3F27EC0650E1BE6B59A + FB76E6BA40E0BCF7AF806FF4FE8D62FE2DE81BA1702DF7020E909832D17BB48D + 0B9ABA3147F451BD22FC3830B6F294F5F6ECB17E30CD04FE16018381BF4A2308 + 8E7058F936085C0360F25111FA46B501E02ED01B407077BD3DD739C8DD66CF6C + FD6986290260A0ACF6E505CCB6016001DF0881056E55A772632735BBC6971C02 + 9B047BBD2DEDA82B36C15EC943499BCDED05E68E06806339B691A62C5AE712C4 + 05CC46C53586B8EEE07693FAA10880057FA5669DC2445F8D4D64B47AFCA6ADD5 + 698DA50DC84609062381DAD1FAB429F6EDA5F1335F05002F49E3662CF134DB96 + BA286791CF6A5DEB0A8A6B6F63B1C7BA5A23D521ADFF8DA657EDD30580058163 + 7E4A81C05AA70DEBBA5AD32542A78780364D99BA284DB163037BA7DAF8F316AC + 4EB3E6ADCC61A16967F636D08BA8FF6D64E7ADFCBF0300EEB5C6FDDBA9F359BA + B8059EC09537F0EF595D7CE63F1A006EA98BC0B6579BB102E0D1D475ACA8B671 + 7FC571465484B2BFA57E6B003C7EFC4CD7DFCC2338FE038E51C360287A951F97 + 7EA0A18FC68CE38E7A9C308EDE2672304B28684B67CDE94B4B966D4C8B97AC4F + 33B8817361357B04C08C33D23155AEEDBF1600BFCE8DEC6F7B63F86D14BF8778 + 436F53AFBEF5F7300C00B740DD370980BD5F3539EA12932A523B1E6D09E932D5 + 26C4680A7BC696099CDAC789D8488AC768AB97466A13279811EC4A11EAF6D6D2 + 3C7E80C08C17DF60EC2541DE36F8ABBABA2C457BAD36F02B8D05004B0060BDC5 + 4AFDE4B9F6C08307B11D7F9E3D781EBBF04DBAF2F0CFE9EDD39FA5694B36A471 + 73ADCD82B50E7FC7CFB2C9EB5C7BA0D23EBCAED1017006BA19EA8E15004FF4E3 + 64CF5F293F2866D82A09E04688AB7204C011FC4AC0D80872A3E231284790ABFA + BAADDA90072E5307109622E08DA057005876846DFA8A7E7BB8CF7057AA01B0CA + 79B120036079FD46004C7E4681B511EE4620DC0BF646A91FC057799505710570 + 91F6FE1500C69B576A03C08D8A2D0260167E3200CE8B40B358686A6C2687C019 + F6CE1AC8A0B786C082BF2A671B619F33E475B01B16C222148E6058A21D5EC073 + 77666F5F2DCEB52DD4216C82C2B4C7A685BE8525F53C4016785B7900D70018C0 + 2BC82B984B798EF589690D81E3A2A616365988D4E2E5A243671CF4A236000CE8 + 8D10380ACF5FDFE3B778EA2087C002C04780B9E753DFFB1FE4C5DFA3E75CE457 + 1C273DEBEAB33C22BFD2F71624CC73F6025A5D4241AF611F42B32F6561F8D887 + 19F8CA0318B80B047600FC910360C242F715E82B985BC35E44D917CA2D65CF60 + D5D32FF6D57EC2B5B7AF20B0002FA916E8D1DAE2ED8BD49694328BFA2CFA0380 + E5052C084CAABCA06EEDFD8B1D58B0E193BCF7E4A64F2F75415FDAB3B7A4FA0A + 58002A0423F06E03FE6E058898B650070CB6BC40307941123C7F012CEEE9FB7D + 0EFBECA0C5440A8C01CC34F0F707F6FF25DCEB5D07BB0060802E790408062201 + 8E80B71DD88B17A1E052AEDF71FD81973300BE66E9752BDFB2B68CA1D0B3F772 + 7859F6FDC5B3D0BD8033FC75006CE5816B80ADEC91BA0BAF5AAF7FEA7941E0AC + 0C2B05C50061003180B0845D806DC8EAF6E2695A3C3D817CE4B3B768D6EE0740 + 4D04080402D2CEC6B9F722C35F3C6D81BCF732FCA4CEA1A99D4B04C0405C3C62 + 1D8852069402664DFB9EFCE26984B5E477D9B9E1119BA168F1F40D6057724F57 + B3538FB031868EA7761E36FAC14F76AC9FD3DE477F4ABBEF2340F02F69D73D3B + E6DD1F1D06A35D0E8081E984827E91F63D42CFD35EC231DF7F9CF6A10779EFDE + A1F21DF19D70BCBDF679F63FFB8B8BF3607F5EE0EC9E272FD320F0D5BE63A0EC + 6EFB4EF73F79910E3D7DE150D9BF631B0331CE9EC776AE8FED6F5EE06E0EFB8C + B7AFFD9D685FBC8081B8BBEDDCF63CF9D1BD8101B7EC0B3C441F20AD698F1D57 + 00180FE0DDC06340ADF5C5BB98FEF918760E0102FBDFF501DED3FC8F3CB4BFDD + A33CA68DA3D0CFFD37EC7F19B85BEC0E87ED3C76D9B9EDB563EEC13399BE0062 + 3C80EF3CF2FFCD41FBDF89107797FDAFE8FF93F0CF946B18ECE51B9DB2C0B1EC + 1A8BF276FBFDA90F80571EBFFCEEF442477CB1A39FDF362F64F0BBBE7ADFD2EC + 99BFED4AF98D06CF5FC2B693070A6FC2CBF7FB3B69EB456B53BC81377D732B6D + 04005B9DE0EFD64B76FDB87C3BB1D730FB0C3B00E64514BB560D5CBCE521E8B9 + 56F1728A5FA3EC7A04F4E5BAB6F9CB0E00CED0375EE7AEA50D9F5F496BEDBAE9 + FB03176F5E5D3BFDFA69928D544038426052A0B1F60C965DD77001DF78CD8F10 + 18C0CB3D64C347760FF8D8EAED9EC1FDC3F78FE75E72C174BEE3E51B41B04360 + 4B1B007CD6EE6117ECFE76E13307C00DF83D6BF7B77366B3FA0880733F5E88E2 + 3E49E48C4FD2EA736CB160639CE105AB0FEDDEDCF1FE150416145E6DF5A47EAF + 77D0DB1D023ADBF27D7FF9B17369B1CD1516339700F612FED9A16F86BF6CFB80 + 8730E19F01BF1265C02F12F8550AE8553902E025050C375EC13E1FEACC931699 + CDA12FF39DE221CC7C2882DF05FB6D1E05F405FEAACECACBDEB139CFBBE7BC3D + 61A11DF8963EE41D10EF0E9EBFBB8A2CDFD82AB0EB73C43D47BA3C86A9D37C93 + 79669C8F4661D71C148FE0ECE5DB3DAFD5DC560078DEF6838D0482057C11F968 + 8B4018E02BD0DB06805506EE027955478A544F1D52BF06181311687D96E06E17 + 0006DA9AD4467502C0516301C0516301C093ECD94C00B867FD0A7B1604E65A19 + A04B3E026009681B216F2FD82BBBCA92DA795B52B3B501605781BD1A37C2DFC6 + DB97F6A55F84BFF9593D43DD087727D973776C23C5362A4F5DBCDE25E02B203C + 655107004B02BD2A037A63DD300FE0B92B8701E0380EE57173FA12105820987A + 52B7B1CE15C02E6B2BCAB3AED200D9B2062375C1DAB046A37E51B11F8A7D5B85 + F3C19801F0526BBBB8D1845940E0AC8E9D36595DEB6A2D6A5B83231FD7EADADA + D675C8D7D1AA76511A7334BD6A9F3103E02080AF3C76E39AEDC4290B7D8F5F60 + 30EBE96BED3A75D69EE576EC7AC7D77E59E355BF91D6857BAD09BFBEC238AF08 + 803B10B8F42F63BC0E007E539F278ED74BFA2C5DDC02F8FB4A9CE3DF575D7C26 + 32A2A85EF67F7795F36EE373522F48DBC3DE78E7C63651667F65005CF5EFAE2B + F55DB696B66D7DDF842294FD2DF5FA00B8A5AE4BE58B199F01F0F8DFEA8BAA15 + FF015F415D30742C8A3FBE026DA34D3FE0386E5D1E49E327DA0DC36E2A78F8CA + D6B77A5B3AFFE157E9EC852FD2CAD5FDCD5813B8E095B1C7DBC5C3613017123C + 87AB6336ED2AFB3F00F0D8956F8C59F97B90BDFBC69EBF8F9227559EBD222C9F + C51859CD98666F03B78DCC3626003CD5548DA176B5DA26484893A9F1D36C62C5 + 78D696C91AE017B57901D71331526FC318CAFB1EBB8057DA98388F525FEFBFAB + 369DFA90F75413CD65398F872D80D565C76342EB935ADA723E452A7BCA583AAF + 3C2175EFDE997D69E28C222B4FB43127B2A76ED10453F60006020376CD667D1A + 906BF27D7B01B79617FC8DF636201C816FADF1480F0EE181410F1182BFDAB366 + BC3D2C4CB43AF6004613A6DB79D8B9CE59BE35ED3FFA51FAECF283B4EFF84769 + C6B24DFEB03369BE3D14CD5B6BE359DBD9368603DC0C7515DA793C80D83481F1 + 507950223F9E7A6F9BFB3AE0758F5E4032753C7065104C0AF89DBE744B9AB5C2 + 1E9217D9836569CF83610370E95BA06E04C0319C7394DAC6F6931775835D81DC + 988F36206E0378AD7F5DEF63D137F4C7364D9097E3144D5A6C6D0B009EB23403 + 60F2AAC706FC4519EC6645189CF7FCED78FFCAD397BD7EA7ADDC915500307920 + 7084B602BA02C0930B04765BD1D4953B93EFE74BB867BC7E3DD433E5C13495FE + 8061C23D53BF0EED4ED300BD96027CA7ADB5F69E16AD377B0B0006F4CE3279BA + 614F9A5D447EC646BC7F493BC077E6A63D6966D903585EC03336E785A3D902BB + 5B8703E0592C4A2116AB060EB8D8FF575EBE2C78B1AF2F5E11B3C342186953AF + 90796581CCCB83EFA4593BF102EE5E70D3A2DD1CCBCF19CC752CC069718E3A2D + E6B1FF5B5ED43BEC1EC07381B7BBDF6DDF03D8F22C509202795104BC35F08DE5 + 6651F3600E79D8F1786141923CDE2B80DE0C8159CCC423380360164ECFA4C50E + 812D6F5A62E525EF9CF510CFC8F7F83DCC02EF0769F991BC60CBFEBD00E1ECBD + 933D7D14B6D1176CAD9C178A09217921AD3A71212D3F76362D3D72CAD233A9CF + 6CCB8F5DB0725EFC5D75F293B4F6CCA71900BFFF515E20C61BB8C05F79F6F6E1 + 116CF9E5ECE90BFC252C7401B8F2EA8D10B8515930278CE61A4B29AB6D5F91F7 + 2FED057023CC45406142432BCCB32FD803780BFC95C72F79816107C8C1FB3702 + 005287BF05F4027081BA0205BEA7AFD9367CFABDDB37520FCC8D619F010EF4B7 + 36B40556448881DCF3D7B4E5ABAB69DBD7D71C0247E00B20899E72F2FA05A838 + F025FFFDCD3478ED41DA79F55EDAFCFD7507340E7F2B2FDF0C8133FC05040B06 + E77ADA6590BBE3067009AF604070AE73E06476DA12027ADB15E071F6FE55DF7E + DAD979B8E7EF553B9FEB402C005606BAEE817AFB99EF394A0A8C24AC329EC078 + D8BAAC8C06EF3CF3F20EC228DF24F4EEE30C2781B8779FA63DC054202A300C28 + 76DBEACDB6E721618209CB6CC706A6599B0C7E01882F3DC5BB95FC907B94E20D + 6AC7023C0BFA16284B1E503B046C05369B9DB181B240DAA1873F39D8CC0039F7 + 01CA0280F7FA5EB92F1D36EEB6760E1DAD8E7EF421A57D06BCD93B588057754A + 7DDC523FF428EFC19B0175D997F7911DEFC12F769EBF3800CEF0D7BE873BF699 + 1F583DFB03D3DE8EB5C7C6DCF7E8C7B417C0FA80EFF149DAFBF049DAFFF899E9 + A58D0354CEDF0FFB14EF7FF6E774E0F95FD2BEA77F4EFB4D7E8ED60620BBE7B1 + 9DABA50060603A7D0E3DFF533AF4CCCED33ED3A07DEF847EF6EFEA11E19E7F4C + FB9F5B1DDEBCF47F82C8DBF70A80BD97E12FF9A1C7EC11CC77C618F6BF0300B6 + 76BB01B096EEB57A0160041CF6BFADB5C5F378C8CE8DE3080037025E5BBB0C85 + ED5808080CE80546730E78289BC8E3D52C108C77F36E6BB30B180CA4266F75BB + 4D4336DE6EFB1E5CF63FE200D7A474909717AC8E94FF55A5FAFF758FE05BFC9F + D3D68E6B75FDF69BEBB7DF20F69DD4DD7E9476591D1078A0FC3EF9AD3A00B6DF + 71FE3DE7973AF29EDC77D3A0B5F5973BAE953D82AFE4973BD8AF3BEFD96D6313 + 469CB0EB0E88F1F805FE5ADBCBD90398BD7F09FF8C67FF363C862FDD4A441400 + FCF6FF4018696BCFB1BFB76B92695B093FEFA1E7EDFAB40100EC751912EB5A46 + BAE52BCB7F995F76D9F84506C1006041E07576DDC41398EBA6A22490C6FCE62F + ECBA6CD7D80D9FD975B65C6381BD02BE848406029317008EDA68D768AED77AA1 + 27DE1378B9673DF712BB1FACB5FB87EE372EEE23D6CEEF23DC8BB877957B1AF7 + 3201616CDCC3D65CB0BA0B766F3B67F73580AF89FCAAF3DCDFB41DC21FEC3EF7 + 47BB27F262145099FB252F40711FFC2CADF1BCB5396DF7655E8EE23E6BCF10AB + ED5ECB3D7AF5C98FB3AC7ED5C98FF2CB5F805FD372E0EF31802F2F805DB03A9B + 0B98961F3F9F961EB77982DDD779C98BFB3C2F8165F80B38CE2F903177F03D80 + 0B08265D7684AD2632F45D52BC827338E80C7A979A4DE9E2B74D665F8407B0D9 + 4817D87C687E80C08B0F9E702D21328A4B5EBED6C753BC808FA579FB8FA779D6 + 77DE81F7D34C9BD72D7AFB743A7EEB59FAE3CFFF5BDAFBD5751BF7FD94F7FD2D + D0785F0E093DA700DF7936D79B1F80EE3C4B99132EDA6B7DCC4E9914DBE27D76 + 5E26F23EC7B4F9A4E694CC33F592619C87CE1B2C7536579D39505E6A2CF3DA36 + 31C7F5393061A0D932C504048E5EC00E816D7E1DE16F84C182BE92E02D29E508 + 89057DC9ABAC5400586D1A6DB0B100BBA405EC924E03E49AA6AEB1E79AD5DB1B + 2F6100AF20AF802FF097366834B85BDB23D8ADED6305C093F00206FE9AA61629 + 34F424C06D512FB08B1AB06B797906D7ED95AF0130E56100B8D835E6E425D6A6 + 02C082C0B23700D7CA9317F30C9CC1AE4BB6629F18DB8776B24D599C45C4B2A8 + 890B80B4AC09007C0BF8454D796D1AEFF90C80F1061E3FAFC8CB563F6F4D6323 + C259B7F2DAC5B80280294700EC628D85BAB0BED2AC7DB0C682CA9A8CB72D02D4 + B6D991ECC3449F0278156ABA977208E8A54582BD6DA27E491010B8038227CE5E + 9ABA3C8159FF6A117697D6E14C4D1D65D6D228DB18D941A2D39636E37CED2DE7 + 651F4D5A571C4DAFDEA71D003721A02BC90398D455D66BA36D1CDB339A56AEDB + 91DEB167D74D5BF7A549761CD6896B709CD7823BE3684D5D92FDD7298C33C98E + 350A009E68DFC170081CF40F00FC3751179FD1B9A336EE54DB47109164DBEC6F + 563AEF163E273960AD6CB2B7EC1D3C0C00AB4E50D6E46DA24D6D4652DDAE8CE1 + 4EAB755DDBB8633D4E50F339465284B2BD346EDCB42E619B3061C6B03AB5A7AE + 4BD88AD4467D9B7C9BB7AF7F112D27FD7B50FC07FC3B5484B393672C4E93B961 + 60B78BC2F2D55BD307F61079E6C32FD38A35DBCC962F72F9229DF3BAF8B55E04 + 4795B5F71B18C7B7FE1372BEBEC0BA2DD8DB2ED2B52DAA57BFD791C66A535BFB + A8CE0D8EB6F4E9FEFE625DB4E926996D9672537520DB2281D7D8A6D85C55FB57 + 69E700D8A16C516C6F131CBC76C777D9B2FDD72A4EB45A275C9C0BEDA6993D84 + 608E79F7B20D1EBAE3A674DACA4EB9577FE59B369C0793CD9976FC19F67D7B3E + 9735C9555D969D5F684B681CF2B9AD1D630613EF3E934DD467DAA47F864DDAA7 + F7A589D36C226FE984E96633FBF859F6F06075E3675A3ACB1E4066DBC301F619 + 36919FBE224D344D021617F8EBE19FEDF34D9AD99726E3616BC7886038426029 + C25FBC7F7DF23FDBFA94871552CF570F183CA43820B63EEC353C65CEAA34D9CE + 8F3C7B004F99BB3A4D9BB7364D035C02490B9495BA41AF3D6005DB381B07A92E + 6A9CB57395FCF8F9EBEC218C7D7ED67B1E8D0B6540EBFC75BBD3A28D7BD3F4E5 + DBBCCEA1AD9D97BC79B12179F67A386793C68970B6D6E405F6206A9AB2B0037B + 0585256F6775C8CB26F7E22DED01BD94659B5CDA0BF2C670CF82BA932D4F19F8 + 3B7111651EF001BCFDF620BECD35C96C48A19C91EA90DA2387BB805BDA981A90 + 1BF2B562C86779F54EB6FCA4BE01D7142B4F593568796C2C8AEC4AD3D70EA519 + EBF6783A6DCD6E97F6F0D57EBE53D60EBA54AEEB6B7B2DC0EFECF519F8CEB6BF + FB9C0D59B337ED4B3337ED4F3336EE73CDDCBC2FCD42669FBD657F9ABB6DBF87 + 82F670D080DE2D43965A3BAB9BB985346BD65616A432E4950475DBC23C93B609 + D8BB68D7E1B46077DE5B4D8B6E6D0206CF1EB4FCAE7772BAF32D5FD8435A946B + E06F59DC03E8CACB57E057E506FE169B4242037F67038A0B1496F78AF2BE3069 + ED1079C15FD2A8EC9572222D3E74C6EACFA4A56F9FF3FC8203A7121EC10BCDB6 + F8ADB36989D99758AAFCD277CEBB96BCDBD1D2F72EA4154708F3FC817B03A38E + E74E0EDBEC0BB5657FDF3E5FD03559BAF2048017986BE771F8445A62E9F2F72F + A4A5476D6C80320BC22C069FFAC4F71F5CCEA2705920F6F2894FD2B222C15E14 + 3D77057D91EA59145F7FA100DF225F44B734EEB928C0EBA1388B8DF2DA0FB237 + 2FE139579FFBB201BBEE196C6DC86363CC35E73BC7C1B6FEA36F1D0E93AE325B + DFB9CF1DF60201220060AF48A041F41E032CC8A32C825C20442C37F962A72F80 + 01DB462084D904770121F2F4ADCB822411006F059A00514CC0DF0D5F5F7110C3 + 1EB9780A2A6433A16281B5F2F605FCECB8FAD0010F103803E01B560FC425E433 + BA9976DCB8E3DA7EFD76DA7E03B88BFD76DA76CD74FD4EEAF77D51EF9AEEA781 + EB781FE650B5792FD3070EAE06805256E67CE4D9B8F3161E90CF1A182CAF5A40 + 2F7BCEEE00AC120239883D74B731D6AD870EE0F0F4CCE1792903FFF2DEB8B423 + 5C315EB7C049802C76BC751DE2DE030E66A0491E3BF0F6C0B33F7B1F87B7A50D + E0963ED2EE472FD32EBC491FE530C478953A2036B9F7AD89BC835C4BA97358EB + C7221434E3FE98F63FFD8B835AF2006152DF1B97633CCCC7C5D316C00A2C1628 + A62D6322E02F2036B7C97079D7839FED3328E4F39FD3BEC77F716F600FFB6CF5 + C0E8FD846E76AFDEA70E4FB3C76CCEEF7BF2D2C67C66C77E6A793C9839B7723E + A6432FFE9A0E3EFF8BDBF84C92E037F2F3E3FB37FB81E77FB5F1FE943F17A0F6 + A994CB4060BE4379F5EE031617509BBD7DEDFFC3F28470DEF384F61914E790CE + 8061B3D958D9CB175B5606E4394435C7E6BB720F65D5DBB190BC8D773FE2384F + 3C1CF5C01DFBBFBDFF388F5D8EA7FD8881C4F24E8EFB14AB3DE7B0EBE1633BAE + 951F3DB673E0FF30EF53CCDEC3A4DA779AFFD9FCC201FF97CF3C9568B3F39E9D + 8769FB9DFC3FCFFF3EFB1313825AA1A649093DCD1EC4082FE4C15BFC0E09BD6E + BFDFEB77BD9C5FCCB893765B9F9DF6F9FCF76BBFF11DF6FBDD696DB6537FC57E + CB57EFA71DFC86AFDA7857F01ABE6776CB5BD9BD872FDF4D9BBFBF63D70ABC88 + CDC68B20766DE1BAC23504A88CC86F29D79E6D57092D7DCBBD843D5C74092BEF + 5EC25CCBCA0B2C9BBEBE9E367D71D5AE8B19F86EB4BC4479F35756EF30B81321 + C15FACB1EB2779AEBB39E51A0B00CED76AC161DA216C40600161AEF5BADE93EA + 851DF25CFF5104C4BA2748EEF95BF68ED74B43ABB09FFF22ADB4FB49BC77E9FE + 45D9EF4726F2CD3D8D7B1629F7BDD3DC33D9079FC81819F6AE39FF795A070CF6 + F0CF762F3DF587B412C07BCAEE7DA7BF4ACB8F7D6AF7E43F589DDD434D2B4FD0 + C6EEB336CE726BA710D24BEC7EBFF8A8CD338E9E4D4B8FDB1C817D844F9E4B2B + 4EDB3CE1A4E581C1CC0BECFEAE6D1FF0FA05FAEA25324160F219FE126504E87B + 2A2D79EFA4D94EA565E8081018F08B17F089B4F4DD93563EE95EBF0BDEB2B98F + 69D13B567EF7545AF84E8E72B2E0600EE3BC64FFC9B4E4808DB5FF745ABCD7DA + 981698CDE7460060D37CE0AECDA3E6D9DC6AFEA11369A6CDC7961C3E97FEF8A7 + FF921EFF9FFF573A7EFB799A6FE3CD2614B4B5F3882B7B2D1DB2B91A73371373 + 45E677CCFD80B4A48B989B31CFB33ACAB45B6473BBC57B8F3A1866EE18E7A2E4 + DBE6A60D0436519EBD1DB09BE7BB0A03AD971ABD6CCA79EC363F1EB07974BFCD + B52D2FE5083AF9454B5EB804E40A00D75EC235E825C5461BA43AA4BCBC7C7BC9 + C751042093EF23BC7ED0C5FEC04A01BD0A111DE12EF0776681C6C0DF29AB061A + 402C481C616E84B86DF6088FB1AB4EE948FD2717A91D76E0F0C4E55B5C9380B9 + 805DDA5B5E50384AD056E057005736416101E128B56BB3BBE7F152CB07A80BA4 + A52C5B04C38D67B0C070B0E938D822006EC6297B00AB4EE591347EFEDA569BA4 + 750AD62CBA228D551ACF1A89D64F047B4D82C90D542E76B545E3E6F4754163BC + 84DB806E5C8FF1359996365D6057E07714F89BB53C8D1F067BDBB4344D9C0DE8 + 657D8BB52FD6BD16155BAECB36D6C4580BEB486B698CD356875D6D548EC216C7 + 917C8CB06E58AF29BE09C5F16B65E717FBCC53178E496D507898580FB674FC34 + FB6EA72F4E93EC334EB094BAF1BEE69C53AD4F0B024710FC6B54AF81D76AEB83 + 627D3C27792CD76D63792469DCB148ED7B8DD1666B9778C670D5EBF9A8ADDDEF + 562D4E7D5D0A5038B2242F9B7D426C1BE5FD4895EF8CF3BB510B18FEBB5704C8 + CA9346203B92DA406F5DAF7C0D80271608DCD5BE7800C7F23F00F0288A3F9891 + D4D6B787F0E29D32DD6E225CF0AC0C749C3263719A3B7F659A3D6FA5E71B0869 + 1786FAA226B55E447AAA5C40FDA261FD7B01E04AF1C2AC8B736D8BEAD5EF75A4 + B1DAD4D63E8AEFAF0372E933FC7BACBF23F5E9F4B354C0B697A6D824A387BDD6 + F8C98411B1943C9EB955FDB0FE3681E90580F1AAED948BED0D68D40996CEC7F3 + 4B1B681B857DC2F4E58D648F65DAF4EADF51A76F6762CAE4B2D7A45376EB6352 + 1BF5CB764B1DC8F6D904DB26FAC5D3B7038001C2F6408066DAA47E56D6A4D9F6 + B060ED81BDEE1D6CE73471FAB20C8081B766579868CA8058D97B49E037CA2130 + 0F0D73EDE1410F24F680111F28F4D0E1795304C013CB713308E68124C35CC483 + 130F467A6012D46DB3457B94835D815BAB77108CE85B60ADE02FB0550078E186 + 3DEE3D4B9D43D8C5F6D0485BCB6BBC087F91DBCB319B7E668F02FCA2689377B0 + 8F57FA08DC52F6B1427B602FE7C9F93590D8EC0E80AD2C00DC2EDAD8437C0574 + 5104BD005E441E101CDB35654B6B003C12EC9587AF6C5301BDD66EE28A7E07C0 + 53F1F05DB3DB213002003BF02DE01701827B81DC5A6A377DA3FD2D2B7B04C233 + 3764EF5F07C11BF7369AB5F9800B083C7DE3BE347DD39E94BD81F1FC05025BBB + A2088001BE0DF40DAA616F04C0B247004C1E61472C8A2DD8F96E9A5F16D2B4D0 + A64537240FDF5968A78DC5C2DD109E1BDDFBBAA1589ECB7805FA2216FA047A05 + 7B25CA8B80B905F6C630D002C011080BFE46AF5F4940387BF89E4AC05DB4F4ED + 9C2E7A2B83DFC556F6D4CA8BF0042EF925D8DD03D8DA037F0F5F48CB0E7FE000 + 18B947303A92BD823D94B3A55AC8654FE06641171DB7F289BCF8DB11E017089C + 05F05D799290CE1F7774F2E3B4C26CCB4F74F6F68DC03742DFBA8CE4FD0BC075 + 687B3E7BF3F60AE3AC7A9417D559682F3098C5FA8FBECDC017AF5E9300B0EB23 + F4ADB76B6C26FA68319F457C01012DE8CB630C7880C80B265027200C68003850 + 26157C109C882082363E1E7D6967F59E9A6259FBFD3AF0FDE67A2320307B6912 + FA19E155B7F11B6B73E956DA7DFBB1878ECD1EBA39AC33F926CC331E7F57EEA7 + FE1FEE793983613C066DBC2B37AD7D86BFBB6EDD4B832687C037ADDF8DDB0E7C + 77602BE029E7D96F14EFC1CE5EBC805EA54061DF9BB7947702696F0084B347F0 + AE5B4FD39EBB840AFE31EDBE9721AD035479D3DE23E4F2A334C071EE3C746009 + 0456D85E80A6BC79630866815BEC40645207B90F5EBA688B5D90B31B6A16806C + F5CD98D81FBF74012F33440410DAF1ADBD8777B63E0E3C2DA50F633136901740 + BCEFF19FD281A77FF1146F5C79ED12BAD9DB3CB4364F7E71700A04F6316D7CC0 + AA835EF724B6F37D62E33CFFAB7BE5023B29EF7994E12FDAFBE8CF7E1CB4FF09 + 7BFF6688BBF7F14F761CBE9F0C7C01BF404ABC68F73F25C4F3731BEFA995F16A + E67C32C0E69C80E4FB9E640F623EDF1EBC870B1C1EB27109EB0CC0C6AB963E78 + 0BF3191C843BE0B5F37E2AB0CA7768EDCD8E072EDFE9FE67406FA0B0FDDDF0E2 + B53AEC80D55A19DCE6BC80ACDAFBDFC9BF37FBFB716CFE968F320066DCFC37CC + 201975E06D06C1716CD9EB3A3E03FD28674FE5C76E1FB2EF6EE851F110E69CEC + 983B81D978F2DE256C382F28D83805FE463B79C267F37F3D783F0360BC8FD983 + 58FB10B3FF30BF3D60AF7E83849EF6F0D3781FDBF9EC74E87BCB7E67F7D3901D + 9B1400BC03506CBF5700F0762B0F5AFF5DD6364360FB5D037E4D40E0C1F27B56 + 7869E0302F8C0081D94778CB15BBA6008BAD8EEB87F60B575E2098BDC6375ECC + 61E837D9356A0B7098EBD0E5DB390CB4D90855BFF19B6B696301BF9BBFBA9136 + 99367E913D7F51B6E76B5FBF49111274DDD53575F317886B6CF6FA151C8ED75C + 5DB3DD0B5829D7E2E2018CB8FE5376CFDF92E75E20F02B38ACFB0629F55D8018 + 7BB85F75EE59D9B6F26C07FE3A00369B3C85D7DA7D6DCDB9ECF5DB086FE0027F + 570388FD5EFC51DE63FFE4E776BFFED4EEC19FA655A7ECFE79E2332BDBBDD8EE + C72BEC1EBDECFD0F1DFEF20217F965DCDB5D362738613A69F3845317AC9E17BD + D8F221CF1B245E22F3A82205F80AFE22F20061DF6FF8487EC10CD0BBE49DF71D + FA0A002F7E3B97D1A2B78EA7F9878E3B00561869DFAE8288266F5BFAD6E9B4F4 + A08D83E72F20789F8D615AB0EF449A4B48E703EF3B2846805FFAA2056F9DB4F3 + 3B9776FEF1727AFB87FB6993FD0DE6EEB763597B40B0475CD973CCE67F369763 + 1C9BAF31FFEB7BF74CDA04507FEF6C33DFA38E3921F3BEC5CCD76C2EA7971099 + 87327F14D8557867815F49F0577356C15E094F60CD732933D79DBFE39D349FBE + 3B6D2EBC03E8BBDFC1EFDC1DD6C694C1B0CDB5076C2E1EBC824702C082BBA46D + 75B568DB78FBF610A017F82B002CF8DB00605706BD31DCB37B009B5D9EC30820 + 8C0480058191C0B124885BD75116D88D6D047725415EA52369CA8AEC213C1600 + 2C781BA12BE5E8018C048AD53EF655D9FB9BA62CB13A9340AD003036BC7A23E4 + 9504800581A3C62FEC805B41608D2B88DB0B023760B7B4A9EB91DA78BBB03E11 + D72E6AC90318D5B0B756B3D6D2435A73898A6B3292E0ED487563D7D800F0C4D9 + CBD2A439A4005AD6BC80C0D939220360ECB9DC592BCBEB692A334E5B5DB4B749 + E750DBBD7F584BFC2D346C6D3228AFAF0277178C1DF08EA6C9F3B32CDF80638D + 5DD69DD1486BD4BF1606F75A03AFCBBD8E23F81B15EB7B9D779BE2B9448DD426 + D6C5FA365BBBBAD7ECA3EAF57DD4D6EE77ABE20DDC1300576ADAB5F1A85A55DF + 768DEDB8BF89FE0180DBF5A600B0877516F0EDA57F00E0AC57FAD17414DFCA68 + 93F6F39D686D3DE582479D5DA8D87F80633AF09DC245D03E67B92878C8E75FAD + 7201F573B18B63F106EEBEB876142FC851A3D5BD8EDAC6426D6DA5B6F651A301 + E0E1DF4FF93B843E3E569BC76E2DEA273109286D7BB49F3065719A689AD0067F + A5D81FD02AC5364C72344635F179131A71B2A5F3291EC0A81BDA8E0E80DB6CED + F9BEC616F7E145311FD5D586892CFDD807587626AD336D4CC2EBCC2C1EBC36FE + C4692BD2E4E92BD3E4692BD3C4A97D69C6DC0D69E9DADD6931106D813DE0CCEC + CB63585B3C7CE99795C16D3C7617D0ADA06FD444C63175B567E23FDB54DE3E45 + 00E1F820111F4000C0ECFB3B19006CEA02CF963AE4E6DC5BDE98AD21AF1E9C24 + 6C71DF5EE430B680D5C6E3D7DA0281A9039CCA26780B58C5FB57756E07B22ECE + 1018F99825AFB243E1A236F83B4C365E1C53E3C472B44BE34B0A20C6AB977ACF + 7B486779FAE6B105923BDA9AA62EEDB707EF81C6CBB786BBB50483DDEBB7D868 + 3BD93489947281BEB5048191DAC4B26B959557B330923D7E01BF5135108E1EBF + 0E784B58678576564A3D79F6F005DA46BBFA6A8FDF1CEE39E73BCADEBF0DFCDD + 6CE314E1D9EB0B4E650169B687A1431DF0CBA296A4C52D4900B8B639F0D5229A + 16C74A5B525F181BEC2CA669114E792DB0C9FB575E1BBE9857BC7D510EF79CCB + BE80C7829EB58DED057F6BB100E80B81788714E01B258FDF586E03C0D9F3575E + C18439C4A305B88B17F0C9BC00AA85504B91F6FAC526B1E0C93EBF798FDFE2ED + FB5EF6FE8D0098C55B79F364CF9EBC4F208BB6CB8E9D4B4B5D364E81C0AB4E5B + BB93360E0BC1D607EFDF0880A3DC23F86486BF35009697AF441D7641DF0604D3 + 36C0DEBC38DED9E397F25A806E01C04A115E562B592CFF8830CDDF66C06B7949 + 0BF5E459CCD7A2BEEA64573B16FBDDB38C90CE65E15FC0B781062D00180821D8 + 4B0A7810F4AD01304017B00BFC15DC6DBC7A6917CAF200067C38F035C9EBD7F7 + D274F87BD3C33C0359002BC0D6BC8F6F0EFB2CC88BBCEEFA43DF97973D7A0138 + 798F5FDADE766FDF9D376E3BFCDD7D1B8FC27B56BEEB1EC040E09DB7EFBAE761 + F43EF490B480DF725C07BC9617F8C5D680A41BF76D3CCEE191E581C54FACDF93 + 3474E779DA7B1F00FCBC01B51D588BED69DAE9B0EC91D90168C0B29CA73E8771 + CED056F055FD4977007B4D02B5829B0AD1EC90B0D88097E48187DE0F2F644BB3 + 276B868C00604160BC82F73EF9B9C058C622CCB33C7AF3F1B0016AD93B377BEE + 023B33B875786B79F7542D360030E2188CCF313906C078E811809870CCD9C396 + 63D267FFD3BF26F6F8CDA19EADCD63B33DC11338A77B9BCF0DBC7DD1006079FF + 0280F73C7E667A62E3BDB0EF85EF227F1F780EF35DD1178F554266EF7B821D4F + E49FD25E131EBC9C2F7015B89BC345FFE2D095FA032F00E819A4D286F6D9EBB7 + 7CF7E57B477BECB352B7D7C68FB016F0BAF729DF7F06B08059E0AB877BE63BE2 + 5C0A3857C8EDBDFEDD5AB981CA1DE09BA12DDF6F968E23E81B8F493DC7EAC0DE + 7C7C0070C73B983AFB9FB4740F6D1EF2124306BED1D3570018EF5E09084CDB9D + F73A0098FF710F3F6DFFF78061816079FD0E5A99DFA203606BA33D89F1CEDF71 + F37EDA63E3EDBA6DE3B33737A0977EF67BDE61DA451FAB53E8E89D848EC773BF + 0810CCF5836B037B07B34F30DA060CE62512F7FABF97B65EC9D7169421305106 + EEB8D7FF907D4EEC0A45EF21A305804D5B2FDEC870D8B4E5DB9B0E7901BFEB3F + BF6AD7485E82C9E077CBD7D72D2D1EC0BC00535E82D1F596EB2AD7E5780D7685 + BCDAC4EB3536BF26732DFE4306BFA806C1BA6FACF9A873CF90E27D44F715B41A + 5D10ECEDBE8FE9FE562B47B2F8C2F70D0602E3FDABFD7F1B9D06F2DA7DB6BC80 + E5F7D7137F4C2B4ED83D149DCCF077195B30D83D5A00D8F70EB6F1F23DFBC346 + CB4F0084B327305078B9CD11143544E0B793C74E394B2F913500F8A8CD45047C + DF396E7A3F69EF5F943D80ADFEDD1369E1DB36173265F88B47709EFBF8BC876D + 2E08F54C7414D3523C816D6EB488B0CF3667D25C887421E1A46D1E24084C3902 + 6142433B2CB6BECCAFE60DE1C17BD4C6651FE0777DBE38687FB7F3F77E4C6F7D + 73232DB33ECC23990F32DF5BC29C8DF9DC102F0C76E69E7AA190B2B6178973D1 + 58569BA69DCD679BF96D990733E725DA0D00180F6000B060AFBC7F256CB301C3 + A4A3006081DED100706D1F090213167ACA5A7B760910380260D9DDB66647037C + A32208AE3D805184BBB2C92EF81BDB45E81BCBB500BF516DE017E1193CD53416 + 00EC105836F24500DD1A0047D81BED510AD92C6FDFA82E285CA0AFF76901C082 + C0CA0B000BE4C663080447B5C15DF26DF5B18DB70BF057AA6DBE9E5142476B3F + 61C15ED40577C702808B872F1A090023D58FD406B543DFA8E563F40006FE2E6F + 0070BB80C20061D6BE3ACE11BE1E56FAC7F164AFF32AC7BE6D7657CB3AE29B54 + 5CB3AC25001C61ED486A20EF689A3CBFA3CAEEEBC5A3A80DBABEAA7AAD7F47A8 + 1B35529B5887C6B2B6DE4BB1AFC6D67ABDA47AA9CD1EDB0FD7F0757BA9B36EDF + 515BBBDFAD6238E8310876E479F1AA91D4F4A34FBB9C0935EDFEC6FA0700EE96 + 40AE00F048201845F88B2200A63CDEE16E0BF41D266BA793FEBDC9FF59F867AD + FE797E434580DBA6B676BE4F6F2847BB43DFF2A32404F4246E40F6E3073C4E99 + B6284DE2C2AC0B171704A5314FFACAD205D464E722F8DBEBA21B2FC85123D5BD + 69D5E714D5D63EAABEC1D57DEAF1EAFA7C03B17601C8E6B0221D3540566D42DB + 2E159B00B0876E06A2C636521C8336526CC32447F66AE2F3A6C4042BEEE531BC + 8DD9A765D82B982B8D669B50A06ECCB76B85A7DE3F80DDA8085487D5DB24B401 + C082C06663B2EA6F46CE5C9626CF5C6EEA4B93ED3853A6F7A5A933EC6160F2D2 + B478C58E74E4F497E9D0B13FA465EB876C32BFDAC6B07E33F1B65D93A6E251CB + F94FCBC097E3D70058E755971B1580EC40596D98F817001CD5EB01A206C0F200 + F6F14901B3417A588A6529DA5527380CFC2575805B80ACA02C80B601BB05D80A + DA62535BF2F214F63102908D6AC06C186718EC2DF2B1498B046AA55887046E23 + CCC5CEF1B029CC33E934F6E25D56F6FC2D2078B88603E0A8E8F91BA17084C008 + 5BED01AC7C0D85057A55475E00D861F06A1647F0FEED06C00AFB0CFC9DBC3287 + 87A63C79CD4E5784C0BDD40B00ABCEA1EFE67D4D1B6F57A0AFC3DF0D7BB300C2 + 259C73C7C3B7406057070047F02B09F2466F5F9549590473003C200F5E16BD32 + F0F5F078D45B796E594C8B1E155A546341ADB117902BB9274781BD805F85EFA3 + 8C37076DB4EF2F79C15E7981A8DC687F0EEF3C1629CC33D077F121C23C13DA30 + 867F569BEC994248C305FBADFD5B78CF64E81B01B02F8C9632CAF017E19553F6 + FA7DF78243605201602DE02285745C0DC0C52BE8FD0B69C509423FE790907803 + AD3E6375A73E72FB0ADA581F857B66D119F0CB7EC07921B9DD0358C057605875 + F2FA258DDEC000603C7E591867819C8572C160CA11FE52871C009FFFC217DD7D + 5F5D13F9DA434BE5B870AF857AD2088559F4170016001030001408FA02120825 + BAE6E36F1A204C1B00EE164B8110087BCC3B6C602C2B2B15F08D9297AFEA2200 + D69EBF121E741280C5A12FA1577F20046B27F4332078D7EDC7897D3E7DCFCFEB + F70B9CCD8076C78D7B69E74DC2C69A6EE365082CCA21A0070803ED00F8BE8326 + 85A0053CB11F29B0070FDF6DD7E803F8B56330E6CD875E479E50D4A4BEEF69B3 + BF6F96878106B2A2028181B70296EC450B40DBCDFEAAC50378E881005B866B82 + BF82BEF4258F97EE763B0602040A1092BEF5E33F3BB8CD619433B405FC2A7598 + 7BEFA5F795272B72D068CADEA42F1D5666E0F9271B5B6199ED5C02D804EE026E + 81B511F60A00C7B2E0A88EB3FFF92F367EEEBBE73170186F5CC6E98CFDD6CB7F + 4D079FD9E729D03702E083CFFE9A0E589FFCFDF0DD0078F176E63BEE78006710 + 2CEFE00C8A735B3B869581EDD96E9FD96C079EFDE2E716CF1708ECDFC70BFB2C + 94EDFB39F0E2E774E8C73F3938CD00D58E67FD0EBEB4F3B331F45DE5F42707C7 + 795CFB5EEC5C046601B1FB9FFF940EBEE0BBCA619C3380CD63FAF715A03E709C + B104EB33BCB7BFCDC30CA2057251F64CA64D06BB883CC7DCF7EC474FB129ECB3 + DA64F0ABFC231BD3CE8530D0D87859C1812FFF9FFA1FCE79FE6FF1FA9500C284 + E6DEF520F7C78B78176397171E72C8F3EC05EFA989B0CEEC3B2C2F60F2FC2E07 + 80BC37EFA521EAEF58BFEBB94C08683C84F9AD131E7AE74D206E06BDFEF2C60D + FB6C5C0FAEB24F780EE9CC3EDF5B7FB0EB4880BFFD657F6FAE2DF4E5BAC27564 + D345C2C973ADB99BDE7DF19774F65FFF5B3AF0E0C7B48150CF176F6500CC9EE4 + 97F39EC1EEFDFBEDB5B4C9AF6556F7F50DBB26E209CC75D0DAD9F5AEFFBB9C2A + 2202D74444087C3C81B9EE727DE53AEDD758AEAB7EDDCD5114B8CE6EE4BA4B1D + FD4B7BBF16973E9E966BBEEE252A37F70DBBDEFB1EC2961714D6BD4336DD473C + 35ADD18B4BC51B785D05800586C973FFF37BA0E5D79E63DF60EE93BC4CC58B53 + DDEA00612B73BF3D69639DA2BD8D6BE57C3FE6DE0C20CEF76952FAF69D221CB4 + DDDB1B006CF77F9B07B0F7EF52A06E982B285D056C2E655E1ECB2F926518BC1C + E88BE72FE19E4D4B08F1FCEE89B4E4B0CD5F1C063377C9FB00AF38722EF57914 + 12E62F19FCA2856F0384F34B6E2E5E84DB7F222DB279D19283A7D2F2B7CEA415 + 6F132125BF18B7F45DCB9B340F12EC45780493CEDE7F34CD612E76C8ECC5FB97 + 7117EC25BCF3F1B4C4E65DF36D8E37C7E67E1B4FFD21BDFDEDCD34687FB3A5D6 + 1F28AC1709A3047D35DF9464D7BC140902D7F26D4A78D1B199FB9ACA4B8FF9C5 + 4764736387BD36DFC6D317D05BC9EBC917F83B77C0CA1500D63C3DC2DDA80880 + 25D9DBC2410B004F59CF734806C00A054D1EE02B101C3D8309032D09FE365EC1 + 3D24B01B55D7D5ED81BE6305C0D4AB5FB4A34E7B6B679AB6A21B0093971AF06B + 62EF606030E19B6BD0DB0BF6B6A906C082BE5DF01760BBD89E870BF88D92C76F + 2D07C105D66AFC382EB61A04D770977C2F008C6A00ACB589B876D165AF00B004 + F095546E83BE5111EAD66A5B9B194DC3616F9B963B00AEE16C9B3200265F835F + 69718A0058EB604A3B6364C956B75119452FE1686FEACADAE19B96D62587AF49 + 7694D76859B31D0E7BDBD405734793AF0BCF1FD677BCD97A815569B4FAD795C6 + 1D496DFD6A0D5F071F5D238D3DF21A7BA7BECDD6AEC834BA55C35FD4D6EE77AB + D1007080B9912B75D52BDFA2D8A797DAFAFD4DF40F00DC2D81DC370E804705C1 + FF00C0516D3F9257553D9E4360BB38017D27868DD93D2CB45D4CD5AEEED7A411 + 088F59F585B45DBD2ECA522F7BD4586E3663B929E93CDAD4D63EAA1EBFEE3396 + 734411E8F604C05284B72D7AA300F80DA9D7442A4EB8DAEC02C0B5C64DB5C95F + D184026FA33DB645B4513BE5A34D125C153C15E4251F016BB493B601E0894C84 + 6D423CD1F293662C4B934D53ACEDE469CBD3143C72ADDD9C451BD3E6EDEFA60D + 5BDF4EF3966D4B13EDE1623C21A067AF4E534C40E389765E9C270097E3313E65 + 3C7BFD9C3837202F80171BDEC6951C0097F3F7CFC0C47F140FE0F800417EA23D + 8410EED9433E970712C0F0446CD54392F6FD5599343E40496A53CBBD7D19AF40 + 590F910C5C05D89A9D7AF2D863386587BE564FEAED8B047B5596CDC33F8F0100 + AB7FD3BE8C15016F2DEA4967AEDC9EE6AC194C33D82F776907FC2A8DE0577D91 + E0B2B7B33678EED6507724002C081CEBA7AFDCE1A9A06F0D7965531A3D7EE501 + DCD8DC0378675186BF0AF78C6A002C0FE0E8095C8360C15DD25E523BBC7BD1B4 + 8D2CF458B900607900FBFEBF96AF01307B90A1BCB0C48211B23240B72842DF99 + D60745002CF81B17BE0480DD0B18005C04FC65E12C2EAA69E1ADF1A8D8F54E57 + 086707BF050213E699B6C3007001BEDE86C5BDD057A27E016D2A45C8DBCBBB17 + E0BB04981B842D026187C287084F68FDAD7EFEC16396B2575EF69A21559E0554 + F2CB1CFAB2D09A45484669F97B190023F6044602C00E7F8F998E7F94569DFC38 + AD7AFFE3B49210922C08BFFF415A72F4AC03DFD5A70B00C66B884564F6000C00 + D8F71E3C25F8CBC27207FA22F7F23D9341AF3C811DFE6207F0027659FCB6B203 + 60B369F15B5E52846616F8A58C3AE56FD25AAB77B06B62A17EE0EBEB69DB9757 + 7DE11DBB00AF206F2CC7C57A5216F9551600701B61418B0004805EE06F860B19 + 38ACB33A80834385AFAEA66DDF5CCF10D824E8100130ED0430047F495D5F5BDE + 844DD0772B9EBE88BC20305EBFDFE574CBF7371CA83800160CF63D38330896F7 + ADBC81B3876EF6CA551D020401841C0ADDBE9776989AB0CFB7AC9FEB9ED50186 + 81BE9D7D4807095D7BE789D53D4EFD568727E3AEBB4FAC3F754FD2D0FD67D6F7 + A1971DFE3A0C0326BEB4FCF3B40BC9D316D07AF7791ABCFB22EDBEFFD20167F6 + 4225FFC261246175819682933BEF3D7598A63125F7E605723EF931116698BD66 + 29E3494BA862F2075F642F5A60A124CF5D60A4BC750528190B097892CF9EA766 + B371808F8476A62D796CF232CE9EBA80CB0C7B6B0174F7DBF110E0534035434F + 8068F6F6A56D06BB799F5B60329EC5875EFEABE99FB367301099311FDB393DFC + 251D7CF64FE9A0D9F1E61D2274B39DF781671C8FF321FCF28F56FFB3D9D80F19 + D99840D8A7760E805ECBE3F18B0EBDF8B3EBA09DCFC1177F72D02B081CBF1FD9 + F86EF633E60BF20073BC7FB3C7F05E3F7EFE1C39C5739773E4EFC667CF63B267 + 30B01640CB38006020F56E5E007860FF7BF781B0405C203675BFE431F87B1728 + 0D00D6F93116C056E05729E7570BF82BD146B057FB270B4C23BC7F09010DC0D5 + 8B0A3914342F3464F13F0B08DE0EF8B5DF49F60ACE8017B8BBE3AEFD36EFDEB7 + FFD7FBF6BF98EDAAF330D0B429020CC79731C8038533DC0500DB18F69BC5CB77 + F75DFBCDE2514CB8E9701DC08B57DEFA1EB65D36BB2EECE0FA4099A801D71E74 + 01E0816B66BB723F6DBD0C2C2EDEC18489360D5C7DE061DD0FD86F61D7CD2776 + 8DBAEDF55B2EDD499BBEBFE5E9D6CB77D3A6EF6EA60DC05D4B377F63D7AEAFAE + A7FEEFEC9A73F16EDAC9B8176FA56DC061AE7B26BD18C3F514008C367FC98B37 + D77C0F61C2482B6C74DE37D86C76CD25DA82475C40761DE65A1EAFEDBABE23F2 + 5CAB112F00B9D72FE0D7AEFD4879C1E09114EF3BDCABD86B5E2F360184E3BD0E + 9BC0305EC0DC275713EEB9C05EBC775D96EF8486CEF7504240AF3A93C346EB9E + CB7D99BD843B5EBFDA07388780F6D0CF9578E98B7BBD5E1493565B5F81DF55D6 + 3FA726F60F3E9AE12FFBFE2200F0529B977888E8C3677DEEA2B90CF077E5B10C + 8F975A1D2FB0E53D8073341379002F7EABA3A5EF9C497DEF9C4B7DEF9E4FCB0A + F4150046F4031E238583E6A5B8856F59F980CDC90480F5A29DA5127335456A59 + 66FD97338EE523F48DDB831005C6434717D8AB79A922CC30CFA44CAAB9AA845D + 7D98E3028179C9D1A5B92EF3E07EAB03E6EE50D8673C7E51B727B0C241038181 + BF230160A4BA582FE82B6F5F7909479BEC310F0C0602037981BF3337F1DCC14B + A61900CB2B581018E8DB4060F2C5EB17002B102B09EEB6A96E13FBC93E16004C + 4A7B9D476CDF0D81BB01709B4603C075B8E706F22EB1E7DB50561BB5F3FA1E00 + 58A0566308FCB66918082EF0F6B702C0E87500709B2200563902DF5A6DE057D2 + 9A4C5D8E6B34B554379AC69B6A20DB2E40EC70E8DBADA569D29C1596E67530F5 + D5F8A4D4B70160B557DEA3EB052FE2568535C4DF42715DB2560D80BB00EEAF95 + AF0B9BCAD82EB3B35EAC75E75EEBCB23D5BDAE184FFBF98EA4B6BEB5465A5B1F + EB185171CDBE6DEC367B6C3F5C6D6C23AB86BFA8ADDDEF56A301E0203850546D + 1FA9ED043B5654AC8BFDFEA6FA0700EE96435B4B4702C0B11CE12F7AAD10D0AE + 7F00E05AF107F2BA8AE3B9E7EF34BB29F1E3338D9B6A173D2E58D64E61A2755C + F5E91AE3B5BC80E345B45DBD2ECABA81D4F6DF5A3A5E9BDADA47D537BDBA8FEA + A362BDF44A0018B501E0628B7B000F6B23FD8D01306A9B4CF59A6875EC4B3398 + 2D5097D435C5DAA360EFAA0FC23E2100DF9114C16E14D034863DEE06C0368906 + 00BBB09BCD26A602C093ED334CB1BA495397A429763E5300C0D357A4693357A6 + C9EC113CD9FA4DB1F324CC334094078CD9965AFD446B37A900DC0C6E6D625F8E + AB7391748ED1D62A26FC7A4090E72F0F20E1A102C5870C956B4D245CD13C3B57 + 806C5104BB78F4CAD64B755F24C82BCF5E20AC2030B09650D0AA537DCCAB5C43 + DB5AD8D52FF6AD158131605AE3358036005D849D36D880BF0B36EC49B3F1985D + 6E0FE2A54D14ED6A0900230F131D802E8AA05780578AF56D0038C25E443E4AC0 + 577B006B3FE0E9842A5FB9334D668C3E6BBB328360202F920770F408161816F0 + 4511023740D7E49EBD9BF0DEEDEC194C3E877606EAE67D81A76ED8DD805FE5DD + FB17E05BF6004694DDB6C5EA4C0E720B009EB9CDC6DC66C7332904B4A06F9B22 + F88DF09790CF7380BF3B330476ED7C3BCD231C5E08FBEC809885334BB1358B6C + 2CAC95053AC4625D84B8847966C10EF88B160E65AF60EA7C5F37DA943EF2F455 + DF087C958FD0B706C088B6A400DFA56F9F6A54C360B717EF18A02F8057298BA5 + A4C05E24004C1E2F1AEDC5A77DF8C8038397B30FF0E10F7D3FE0A8E5473E4C2B + 8E7E94FA8E7C94561EFD38F51D2B8BBCC5A317B8BBE408E11F3FCC80B7D800BE + D808034D993AF61F5CE59E44D90B98456679F24A791FC32FD3DA0B5FF9623636 + C15F8497135EBF5A088F1E5102BD19F6967D7D4D2CA2478F5EF65E64817DCBE7 + 3FA4EDDFDC48FD5F5D73902BC88B58C0DFF4E9A5B4A1D8116DB0BB776F00BE1B + 3EC9368100094810CBC0073CCF00BF0AE32C6F5EE084802EDE6AC88185297AFC + 928FDE6C1BBFBAE26A20F037D6E7DBEB9D3D7EDD432E435FA048FFA5EC4D87B6 + E1E9FBC31DF7A853885594C16E863CEE157C056F5EC23A9710CD96C70EEC0118 + 110E160178D9EB17000CF8DD7517B004F47D6065D3CD7B1E5E36C35F2B5B1F87 + 5945D91B377B382ADFA9C30608C3B337EFD30BAC250506EFB84798DC0C80D9EF + D60127DEA70DB803C0654F5C79DF52667FD721F74CCD618E01B6BB1E02E53268 + 043AEE7A88B767A70FA9434807C2367E17A0CDA19CE5910B64CDF0D5CEC7CE43 + E3EA9C188B7AA0A383674BD933F7E0F3BFA4FD010223079480D947662B02D81E + 78F6176FEF7D9E77C02AC7C82038C35B8E21580C603E8077AF8783CE61A191CE + 85FD85F73CC88018F9672074F3F39FD35B3FFDD5B5FF059EB83F9BF03226AC33 + 30F81787BC78F8028B15EA99F339F4E35F5C78EF66306DFDEC5CFD7BB6B69CEF + 3EB3EDC303D8D23D8CE1DF978D63E3D38EE3EFB5F368CED3523CB1F93CCDF758 + EC9CAFFFFDEDBB77D00E8C0D8016289B455DF144E66F58CE45F2BF91E7F97B75 + 80B2CAD1DB97F304340BFAA258470A04DE71FF711AB45476F6386E00F0A30C8A + 75AC0CB1B30739FF7FBC90C08B09C0EB0CB11FBB77F10EF6FE35EDBC6FBF3BC6 + BFC76F277BFFCA033842613C81FDB768E55D96B2EF2F5EBFFC5E77DEE1450E5E + DEB89F86F05A063617C8EBE2A50CC2B7DFB63A5ECEB0FCF6027F09EDBEEBD613 + DFA39BFDBA09D90E089676DC789C765E7F64D79C0C7501BA805F89B2C35DABA3 + BCFDDAC3B4F5877B69F3C5DB69F3A5DBD6EF6EDAFCDDEDB4C9CB77D296EFEC3A + 67EDB75FBE9306AFDE4F3B2CE55AC7B5100DD8383BAC1D1EC1BA6E666F61C242 + 67788CD770B6517FDDAEB1F9DACC757A1DD77240AF5D77D75B5E911C04800584 + B13500B8B443D409FC22A23FE8E5222982DF5ADCB75672EFB37B9CE02FF7B666 + DF7AEE95E55E88FC65A8739FA7B5673EF37D7F6BB11FF0DA73DC57B9E7DA7DF3 + 2C5EC3367E01C0FE22167B065B1B003200587B013721A04F7C90FA4ABEEF142F + 8395281FDCEB79D9CB943D80B380C01104AF06061302FAC8199B633057C92FAC + 314F597EF8AC0B18AC390DE5151E36DAE62547CEDBBCE59C035C802D22BF9488 + 26EFD217E06B731D2299588AF72FF017C02B50ACB2DBF0042E632D2E6D3CAACA + 2100B4950F9DB2F95B8EB8B2F0E009CF0B00CFC2E39739A095F552A0C35EE67D + E5C540EC8484466C2BD2C05E9BA3361EC002C72554742FCDB6B96EDC0F380260 + A2E110F5A6037A03EC6D6C07CB1EC1D93ED7346FBBE54701C0B15EA057B0578A + F636D117100C049E6129F0B70D00CB0BD8CBC0DEA259EB77B980AF82B6A4BD54 + D75396A2BDAEAB0130E56803FC8E05003B04A61C606F1B001E563669BF5F2942 + 6001DA1AFE4AC3DA14482BEF5F016004D88D697D8C36008C9A718A629D14A1EE + 580130623D42907734002CD02B45F85BAB0BF8D6E596359636B11EE3E0B6ACCD + 3420B72A475B5B5DD3C694B72CEB00D9766500DCD9F3B71D000BEE0AD292C726 + F83B1A008E6541E05AAAD35A211EBB6355677D7174C575C95A35001E4DADA0B7 + 977C5DD864F909D316B9DC5ED698475A4F1EA9EE75A53147535BDF5A63595B7F + 1569BD5EEA55DF666B571BDBC8FA5F11008BFB9046B5B51B2EFB0E6ADB24B619 + EDAD56EE56AB3AF61BD13F0070BB7AC15FD5293F1A00F6F613869787EB1F00F8 + D768D80FAE12707712C2F3D72E500E7BA79826E7CFD58489B6B66D637B9EB4ED + 0232A27401EDAD5E17E5780389EDEB7651F1C63316B58DD1762CA9AD7D543D6E + DD47F5BC39A5B7A7DADAD540B71500B7415FD983D84FB8150057ED5CD86D02F3 + 260070EB84893147A897ADAEEBD8963AC095C645B81B2070AB02209E5081DE9E + 6A03A626E0AA34BCDE26E4405F13F92C6BCB84DA26A813A72D4993A62D4D93A7 + 5A7ECA52CB2F4F93A7F7A549D357A629B3D6A4C9B3D7A449B3ED81C134DE1F30 + D63A04C63669863D3458DB8902C0B3ED58365997F76F3C0FCAAF04807990988B + 372F0F2125657CC6880F1BA4DA97C6FB99EC6165A23DBC4C02FECECDDEBEB578 + 3852486787BA66D3C353EB8354B1933A0046251F3D744965234511D64E01A02E + DEE4F50E6D4D02BE510E7139BF32A68E21C53125790F47EFDF984A02C6C05D3C + 7F67ADB207FC15F6701EDA2BF433F91A0877B4D51EA62D5DC6037C06BAB500BC + 80DDA836082C103C35005F01E008826359DEBE82C0006034C5CA82BFEC035C03 + E019EBF6788AE4150CB895A6AECB9AB61E7B06BF5206C07BAC3E03606CB336EF + 73A00B04F63136B2D0B32F4DDFCC624EF60476EF5FDA04355EC194B7E1C57B30 + CDEACF1018003CAB3F6B763FDE071DEFDE1AF4CEB314799B087F03D495A2B784 + 40AF602F6DDD4318406C3616DD00BCF2D02065018EBC60EE2216EF8AF7AF0360 + 64762DEA39E82DED59F0533F79FEFAA2208B7E962A0FE095372FE50882D586FA + 1AFA462D7B272F96B2381AA16F4C2507C06F9FCA0BA865EF3DC0AFF6E0230F00 + 5E76F882C3DEA83610EC3A96BD7DB287EFC769D93160EFC7A9EFC427BE082C51 + 2F180CFC65F159103887A3CC5E4878F2222D60B3A0AD85ED6641FBFC97698D09 + D8EB9E4E944BBD00B03CA148F19A62411D68AB4574497BFFB278BFF58B2B0E82 + 01B96A8F2813D25976C2780AFC4A00600FFB6CE32001815A0202A478F8021280 + 0A0E14AC5E8041505820D8BD764D40E006F87E9901B0E0F096007CB77C676DBF + BBDE01BE9732F075F08B002305F8BA0AC405EA22F2C05E52EA1AF87BADC05F00 + 10ED0200A6DF2E0FE5FCD06151DE6394FD7EEFA7DDF71E9AF0EAB5BE0E8173BD + 877FBEF3306DBFF5C0F73005F0027DF170748F5DF7D67DEE1EBAD4E510B7846C + 7E96816F81BE5D10D8B4F35E4E0571F39EB219829246002C9896CB3F666068A2 + 1FA19F0177027619F465009C61DECFCD98EA07800494021081BE5206C1EC67FB + 631AB463D1277BB8FE92763BA00472021D3BDEAC7817EF7FFE975C5734148EB3 + 1715C0CC3169EF1EC9CF32583DF83243608E913D617FB2FE1C0B80FA17AB3379 + FA4F361EE7FCA7F4D6CB7F496FFDF82F3E3E409871739AC7DEFFEC4FD63F9FFB + DB3FFF938BE31CFC11B0CBB1F89EF1080660FFD200E0DDBE0F6FFEDC0EA60BF0 + E5FBCBE7FA6787BEBBF92ECD7EE0C7BFA4FDD85E64ED01F8DADF01F00B48CE1E + CE5667E2F3F0DD90CF9FC7CED9CE93EFC7DB789AC16E3E2EDF4100B52FCD6EE7 + CFF10EFE6863D8E7D1B939882E79CE9DCF86763FB6FF41007029B7415E07C096 + AA0E61E77B0296EF7AF23C0DDC7F94B63F789C864ADDDE67FC8F115ABA3BBC74 + 06C0B9CCB9E7FFC33C66B6E534B7C1AB987D920959CE0B1478B8F36284FD8604 + 7AF17836EDBCF7D8A1B0DB8B7611061A386CF6EDE5B7CA6F963DB4F138DE517E + FF0E7DF94DE2A57FD78E69C29EAF09BC3CF228EDBE6DC737B1573710582078FB + CD2769F0E65307C0FD3FD875A678FDA2E8053C70C58E7F8DEB1221A2B3173160 + 581EC05B2E99AE105E9A31EC5A75E976965DE7F4D28BCBAE8D805F2030299EC0 + 9B7959E6CB6B69D3575C43B9BE729DBD6DD7D71B6E5FFFC7CE0B3A5C6B11D75E + FA725DF63AB3E9BA2E002C081C55DB0483DD6EF713EE35F1BEC3BD8A978D2200 + 5E6B65EE5BBCB8C47D8D28166B8B07708C78D175FF638B83E2EDBBBA80DC9595 + B8FFEA852BEEC32B0AFC5D63630080E539EC7DFD7ECD7DFB63EB6BF777806F81 + BF8DEC3E0F00EE936C8ED0E4DFB7F9C2F11CFAB90F106CCAFBFF229B7B1CEBCC + 451603830F9FF13ABC7DB314AD246F5921691B0B52C030F077052FA459EAF677 + CED89CC9C6B4743180370060F71C76F86BE5028F1BAF5F1321A597BF9701F2BC + BDBCC877DC95F3CCD58EA5B936FF9B6DF3469FBB31CF2BF341CD0D0581953673 + 44CBD376BECD3B3507459A870A06FB8B8A65CE8A3204AE4241DB7C983D801700 + 9469BFDDE6DCECF3BBB3050007F8DBD880C2C10BB886BFD1431809120BEC0AFE + CA3E92680B04F6F69B79EEE884818E12109E066405D2025D8B27B0424123206C + 2FB5015E60ADF2526C53C35E95A34DFDA2AD1BFEA20C80D1A4BE2D69D2F20268 + 81B94535008EAA01700D811DECD2D7A4BAD85610D7DB555EBAAE16C02B5BACD3 + 1EC028C2DDB12A825D34529DA4758928D626A4C6563C8023E495C76F94EA1AD8 + 5B95DD16D65D465217C89DB93C4BE54A11FE4AC3DA983A6B44CB1B08DB4BDD00 + 78491A3F7371D1222B7700308A5057E0B786BF526CABF248101869ADB00DF48E + A4CE1A63475A7B8C6B905A936CD554D64A5947CDD0B60DFA46A95D4FF95AB0A9 + B237FDA9EBB12EADB5E6A8B676AFAAB67151AF36D11E15EBC6B2B6FE2A623C8D + D93676ACAF6DED6A631B59FFAB02E036F56AD76DB3EF20F47965D0DB4B13AC7F + 38CE1BD13F00F070F582BF6DF61A00037FA5D837AA81BE84856E4243FF030077 + 4971D7C7A2B67EC1C68FD03D7C2D0F0406F4E63D7FEDA2673FFEF1E369677D5E + CBC37734D517D2E16ABB28D73790D83EB6FB2DD4762CA9AD7D543C67D4D687FA + 3600ACB6D8BAC06B2F09D88EA2E8013C0C2407F80B281E67EDDE0400D664A96B + C2A431AB36B1DCABAE23B34FCBF0771800AED4D666C2749B1887BA5135C326C8 + 411DA08BF7AF4DD843B9B1D7656B873C1CB34DA62796C9E6C4E93631B6F3C1AB + 77E24CEAEDC160F65A9BD00347D7A47168DEBA346EEE062B9B9D3633AC8DA58C + 377E565F67C26E93781E04BA26F1952D4EF8F11C6E00F62C1E205007D60AC84E + 201F349E3D884B3D507A8295B1A9CF241E862C1D176C517A50223F527D7CB8F2 + F6F600E69EB6260F076D72A05B94016D0EFD4C59D0174D059C025A17D9836701 + AE0E6C8B22FC458DBD19B7D3B6961F83630535E35B1AF311E4C63AD5038491EF + FF5BDAD57B014FC5637805DEBF19000BE446B88BE4E12BF05BB751791AA9B589 + 5EBE520D7D11EDF0168E0078C69A5D699AA5DA0378FA5A2BAFD9DDE5ED2BF81B + 356D3D50170FDE8EB457AF42362B6C33A9DA90CF1EBD073DF5F69BF10EC89EB9 + 8060F7F0A5DF9603AEE99BF7BBA66CD89326039937EDEB40DDED07D3CC7E6BEF + 9EC07B5DBE675959C81A067F77BC9DE623802F00D88EE9756663512CC2DF0600 + 037C77BDE321A1B1B160369FC5334BB59886E72F0B6F7863F8429C490B6FF386 + B216EC3D9C16EF3FE67200BCCF049C05DA6A016FDF91B4707FC7BBA3DEE7D701 + 2F60D752D94603C0751BA5CA23420E46EFDF087DE5F1ABB2D7BF7BCA612F0BAA + F2FC55B9B11500DC405E5304C1CB8F7C94FA8E7D9CFADEFF435AF1FE2769D9B1 + 8F5C19FC661BF91584873E993D81FB08FF788AB0D09F743C80F112360181DDCB + E8EC170E7F81B8319CB316B4A3772F8BDE71915BF51100BB0AE0D5023A8BE6EE + C1FBF1770E725797D09C1B3EBDD82CC2CB83B76B813E943758BAD1CA005DD901 + 001E9AB9887254ACA30F9EBF405B07BCD8DD132D7BFC2ADDE25022035F856E76 + C86B366006B097BCE030027A0C5CBAEDFB6312D6D9850770D92F13293F70E5AE + EFD389F76FBFE5B317EF23F7E293C7AF0030FB74A2C6FB3740606CB4C70E10CA + B23A8091035EEB431859F659BD03E4CD92276216DE89D467EF5F79FD0280B5CF + 2969D44E423E97B0CFE41B10FC100F5794E1AEE430AF80BCDD56E7DEA34F3A30 + 983C00186F5EF722C5ABB7840D16D443D8006F075FE63D698197805BBC4F01B3 + 789D0ACA024EF102CE7BD26610B9CB8EB10B806CFD32E4FC6BDA03AC647F5E13 + 79C621BFFFC77FF274B79DCB10A18E2D4F4A798F1D6F2F7098E37A1E50FA970C + 4B81AE36FE7E40A6D9A843D887F81E5EFC291DFAE55FD2C19FFFC5F27F4D7B1D + 980283FF29BDFF4FFF2D1DFBCB7FC920F5F95F1D0613161AF89B53803000F997 + C60318D0D900CFF23D11DA190FE0B7F0F62D9EBE3538C60B58601628CC39EB7B + 3964F51900F31DF1B9721DCAA19DFF64ED6893BF3FBE177D8FFBCC96BF8BFCFD + EE25F435C7B163F877FEA37D1700E517F65DD8DF116500FC977490CF636D74AC + 7C0EB9FFD033FB3F796EFF27CF9EA7C1C74FD2AE27CFAC6CFF1BD67F8F8DE5F2 + FA979E52DEFBF2272F0F593BF27E3C3B16E990B5D9F9C8FE7FF1F47DF2DCFB30 + 26F0760FE5279D90D2FC1F6ACFE1087B9187930EF95C9721700E1F8DF87D584A + C8674468E912825AFB03EBF7489EDFA7878BBE9D8131A95ED2F0DF2AD78ABBD9 + 437FD73D3BA7FBF6BBB094EBC0C00DAE09763DB865F5B7ADDED2C19B4FD2CE1B + F63B26BD65620FEF9B4F1DEE3AFC2DA077C7753C8873686897D5017EE9A330D2 + EC1DECA1A02FDE4E9BEC5AB6E57A0E2DCDF58AEB1AD7BD4DDF5DF7BD8109FFCC + 35B181BEC51B1828CC3594B0D15BBEBB6575D4B35770671FE1759FE59772B826 + C76B316351DE60D76EC1617F91A7DC1710F705EE2DEBED9E219B5FFBCB3D4165 + 522FF38251B9FF0806B74975DCCF80C1DC0F57977BA2E7CFE7BD82B90FA2D576 + 0F5C7DC1D2A095E73E4B7DE73E4D7D673FB5BCDD8BCFFFD1FBD36FF9A9CFD2B2 + 13769FB67B2940987B33D05721A301C139B57E67F3560F02C040614FB9DFB3AF + 7F89FC41BAF4E88526BFEC98A9D42D45A16E397B091F3B97161F3993161DB6B9 + 8BCD4F80C9FEB299B51308CEF3175E645328E80C80971DCE36EA10F625EF9E33 + 59FBF778D98DF94D06C4805E6C4060C23FB3EFAF3C7F81BE11007BE8E8B74EDB + 3C2D7BFE027FE7D97C702E2F06DAFC6BD1A1F7DD03180183994332FF643EA979 + A2002F796C7A59D0EBF66400ECF3C9A22E185C3C82E5292C08ECF3589BDFBA07 + 30116F00C056377FD0E6C503361F67FF5F42406FCF21A0253C7E81BE2A0B00CF + B5B9B9406F0D7B23F4955D12008E5EC06A2B6103FA0AFCAA8DFA601708EE0584 + 11E0774681C06D30B881C2B433018EA7026A5B3405785BC0AE207004C06D12E8 + 8DD0775201BF2302E015F61C6A9AEC70D69E33EDB91235C017B05B249BCA6E33 + 09EC46E01BF335181E0E80798ECEF0D7C34397B602BD11FC4AD8C72F5CE70200 + 47801C01AECAA389B66DB65A713D422FB2B7AD558C14025A12EC8DC0B72EBF36 + 002E8A75A3A9EEDB0D80D1C81038435CF24B92E06FCE038081C24B1DDCD256A9 + C06F2FF8DB260160A90BFCCA3EFDD5E12FD29A63545C7B6CB30DD354D64A0B98 + 45BE96FB2BD4364E6563CD384A6BD023A9EEF32AA27F1DFAB957BBB1D68D656D + 5D8A6BE56DE5686BAB8BF56DB676B5B18DACFF1500F070889BCB0DC42D8A6DDA + EAB3EC3BB0B469D7C6D46A0128C7A2F1D636B2AF16DEF54AFA0700CE02DE9246 + C82BD0DB5617FB0D1375A1AD8BFE8DB76F510380CB1F5727FD7B93FFB30CFFE7 + FF556AFBA77D9D7FE457F8014CE0C7499E948B54F9C1028709095D5FC46AB55E + 3C4650E7C22A9B2EA61DC50B70143707DD6462DBD1FABD09E9186DEA75439346 + AB678C5E768DEF9FB982B38D2290ED616F006F99900080274EB609D9549B184D + EBA881C0F4B1311A008C2DF4EF9AE08C41C326508C516B94C954EB185E67E502 + 73DB806DAC9B50BC786B7B2DD50DAFB789B069DC744B2B085CC36029835E9BC8 + 53C65BD7E4B6D936F1F7BD7C97A7C936A1660FE04936E924F4F3E4E92BCDB626 + 4D99630F1573D73A181D676DC74DEFB3CFCBF157DBC3803D84CCB6078C99F6A0 + 616D81C1DD00D88ED7F5209027F67A08E82D3B4740AC89E3220F395D80ACDBE6 + 16504BBE005FB551993ACA93EC21C8DB591F3D1021FF4C2D50189BECAA6B1EA4 + CA43D504D3B879D6BE3C88E5D04DDD3018502BE89B3D80330816D0151816148E + 8AF56A1FA53648ED384633B609882C902B102C682B450FDFD85610D801AF60AF + D981C0C8C742CB2C5F3415505C20AE24B00BF495D7AF806F04BF110ED30E5B2F + 001CCB0AFF8C3D7AFFBADDA43D80A7AF05F8EEB1FC2EF7021600C60B386AEABA + A134C5DA924E5D6FE522806E0D84DBA0F0AC4D872CBF3FE7B7B23874C045DEF7 + E9C5CB772B7BF71EF2140181A76EDCEB2078C6E67D69360B5203D6B6DFDA926E + B7B12D8DA19EB5DF2F120C766F5FC161F6382BA017918F5EC0D8E4F13B73C0CE + C7EA582C5BB8FBBDB4A02C9EE13D41C86816E9586463214E8B717377BF637ADB + 356FC8FA997DF181635EE7F9FDDD0098853CC15FE0305EBFDA174E7057793C46 + A257B0EA22E45D68E3D7B6286C847F5E46B8C2B74EFA1EC002C078F8FA1EBF96 + 275DFAEE49B3E1D972322D7AE77DAF2704B43C6900BF0ACBE810B878FA027E3D + D4F3910E009600C06B4E7E96569DFCD481EFD2A3663FCEDEBE9FA5956613105E + 7D1AB09B178DF11E925850D69E822B4F7FE28BCCF2EC25B433F017980BC46551 + 5A509894456EBC9DB4C84D1B520160CAB459C3827601BEA4520CE11C012F3018 + 69919E54F5A40ADBA9457B2DE0AB3D619D01BCD1AE3A4161EAD69A6DB3B50524 + 001180BA02BA0ADB0C6800E83AB0B0BCB73311E29936DB4A186752242F376C84 + 3FC55B77D3A59B69D3C51B0E42002240DE7EF6F10DF077A00060E0AE80AF24A8 + 2B18DC8FFDFAFDBCD727F0E7F6E306020B083700F8CE23F7F895B72F9E84024C + 11FCBAE7A1D90153405DC15F3C7D296BAF53F21960E5FAA1872FADFCD2DA59DB + 7BCF3DDD752F7B01FBFEB44F018480DE8E77AFE02329DE93E4018FD4EF02129B + 8D765EE7DEA57F72D00B54DBFBEC653AF0F2A7B4FF0550D88E6F3AF42300F8CF + 3E06F0F7AD1FFFD9016F06D03F3B003EF4E29FAC2E7BD67AF864E0AEB5072C02 + 1ADFFAE59F3D55A863879BA51E7099E16586BE40DEFD2FFFC9A12636EAC8E731 + 33F8D4F840DEA1A71A87B61D8889009C6FFFF93FA5B74C07ECBC1120F8D08FFF + 924EFDA77F4B47FFFA5F0BB0FE733A64C7CC9EBF7F76CF5AF2075EFCD93D7681 + BF40DC031CE7790E018DDEFE89BA1CE619D0FB8E7D4EF27CE7805E6CEFFEF95F + DD8E8DF3E49C3847CE15F89B01B0F529369D3BF076EF4BFBBC6ECFDF8FBE03D4 + 05821B009CFB3B00B6F33AF8B3D5BDB4EF07406B72406B021477BEF37C3CFAF8 + 77E76017009C0504DEF3FC999DCF4F36A6B501EAFE94C1AEC35FD3EEE719FC02 + 82A93FF467FBEE7EB1B15F58BDFD3FEDB5FFA13D96DFFDD4FECF8A00C1BB1EE7 + 50D2F22E160026C4B3BC8E81BC5204C07826F3FF0A24A63C08E8BDF7380D95FD + 84DDEBD77E7308F8CB5ED882C082BFE481BF03B7EC1A50403175FD66DB7ED77E + DF7779E922FF5EFD650C7E9FF7EDFBE0050C20F01D42489BEDEE0B6B4BD9DA14 + 0F6087BF26BC80B715C82BCF5FE0B087792EA1A00584D5D7E16FD1961FEEA6CD + 5CEB2CBFCDAE495BAEB1B7305EC09D175DB8EE6DE3DA47A87BAE91EC6FFEAD5D + 47BFBE92AFB35C77092F7DC9AE83DFE2196C7D0AFCDDFC55DE0B783DD7F6AFF3 + F538BE8C031456F40687C005E472ADDF52EE03F19EA1FB0876DD0B9447AA7760 + 1CA24B70AF92B867116D0251D6BD0D8F60BFBFE1197C3E4361BD04955F7AA29E + 7C476BCD469AA1F017E59EFA555A7EEAF3B4CCEEE1191E733FFECCEFD7780FEB + 3E2E08EC113CEC3E0EF45D7586FBB8B53B935FF04278023BE43D7A212D3E7CCE + 01AFA281A0A5C7CC7EF47C5A72A4038197B1B7EFB17369C9D1B39E2E3B7E3E6F + 3511203065D2060A9797D900BE7D476DEE82E7AFD981BFEC0FBCC4E6395E67FD + FA8E5B3D2FB91DB6B90E5EBDEF668F5F202F5EBFA400616C8060F6009E1FF6FD + 5D4448682B23E6629AEB692E0700C613780E10B8C0DB7905E46AAE18C1AF00AF + 249BDA93763C8143A8689BCF466F6052D9F39CD8E6E0CCAB01BD3B0FB980C00E + 7AB71F4CF3AD9E7C8EBEB3CFF2FB93878AA66D00C01902037033ACAD21B0DA08 + E402713DB47381C02802DEA91BEC1906D05BD5CB262924B44241AB2C288C57B0 + 00EFAC0D662FA0374A2018D03B19180BC0B5BC80B0E06F94D78F00801BE0BBC2 + 9E2BBB406FC7FBB75BF67C59016084DDEB4C40E0087A87A9405AF2EE155C8363 + 536C0BB4ADC12E7980F024CB4F5CC47379DEBBB76E27E82B61130076F86BB6B8 + EF6F5738695F03C86B060E702BC82BB5D5C57E7E2CDA94F508D62600C082C0B2 + AB6ED2024B17AC71B541DF1AF8F692B7B3746289B686DAD6703A6B401DA0DB59 + 07EAB68FD6366A38042E32BB43DB0ADC0AE466109B21F0380F01BDA494B16709 + FC4A6D70372A1EA7AD3E026060EEC499D636C0DD5E8AEDB4DE18F36D6B92D136 + 4C53598B2DA016B1B6AB54B2B27BEF9AC65B1975B5C716CAE409F7DC78FC4A66 + 9727705C778ECE4551AA8FF9D7511C333A31F56A473EAE7B47BBCABDD6C5633B + 496BE56355DD3F8E11F323AB9B6D448D8595F4B2FF2D34EAB1E5F8173C8123D0 + F568B1C53E76D978A46D3CAD45E3C7CF74F5E4806284BF1700FC2ACC72ACED46 + D368E3E89C7A49D07634D590F7D76A18F895FEA301E0EA9F553FB0AE36A1AE2D + 3F4CAFF02368C621E5075F6CBFD585A973B1958DE3744B6D46D3ABB47D15C571 + DB6E36FF1EF21B6201B3C3A06F5404C052AC9F9AD3095316A789A609050C3780 + 58ED4A5F0160AF6F9BDCBC86C63669B27368B1B74FC09606403B36F506BCDDF5 + C3EB6C12DC0280A51AFE4A75BD3C80F1BA2524333078B29527CFB2BC7BF5AEB5 + BC3DB8CCDD9826CC5D9FC6CFB10790791BD3DC354369F6AA5D69F2BC4D66C30B + 789D4DA8ED8164B63D90CC5A63F95579A2EEF017388B80C02883D976516FE758 + B5138C652C691C0A758069DA366DE60EF7E415CC8D0F45A4AA8FF9087E55171F + B0D0442B4700DC3C98557BF8E8A14F7649D09654D037025F815C790923002F6D + 1CC05A7B6CEA2B9BF72DE0B7F122E618360E92972F5017F82B001CA130525BF5 + 430EB395573BB49C076D1EB8DBF7F9758F5E602D325B84BFB19D40F00CE0AEA5 + F51EC011FC0AFA2AAD017003818B07F0B43539EC33E0B709F76CA93C8229CF5C + DF0D7609CB3CCDF228425F54C360016007BF9B0FA4D95B0E2687BE2504B4F6F6 + 05F8CA0398FCDCEDEFA4F5873F4C9B8E7D92E6ED78374D619F611692B6037F0F + A4E9DBF6650F85028081BB12B077DEF6CE1E670DFCB5148F0716BAA41A042BC4 + 736CA30532F79EA0CFA0B53769D10DB12887F2A25CC70B382FC6B12867EDF61C + 732DDC7BBCC93765530C0FC822A1160EB56828002C904B2A184C19A01BA12F76 + BC4BD8EB172D793BD77B5B2B470F603C7D01BC489EBFB27948C5774FF94229A0 + 57619F6B002CC80B0006F4AE3CFE89A78D2730DEBF27F0EC6581F70F0E7AB3C7 + EF278D8DFC0ADA9CEEEC21A8C563EDFB9BF7FE2DFB009FFED4C1AFBC96047E25 + CA82C0AA73C81B3C9E24B71500EC5EBF1FE6548BE894655B87EDD31CB293C577 + 01DFB8604F9E857A52D5370BFD05FA46C02BC9CE5EBF883CE14201065BADEC1E + 6905DC0AE84AC009B703812DEFA995B13BB8F8DEF278F702370A08F6F602C03F + DC4E1B2F5E4F1BBFB7B697AC5DD9D397BC835ED30E0FD1FAD0816EF4F2051E0B + FE524F5ED017B84BBAFBCE13977B00973AC2C0226CECF38B9721E163B7DDB063 + DD7998F000063E0197FA6FDE737B8650D80AF005C49A0623F47DF02CED7E98BD + 81DD2BD8EA09018C972C619EF10266CF5FF6C465EF5BDF8FD7C3E376802ED0D7 + A1F0B39F3D1DB232700FF04838E61D760C52C20F2307922FF04EFDC9F4A343BD + 033F036DFF5480DC8BC4BEB70A150C2C3E5020A98771F610CD7F4E0055BC6633 + FC95972EA0D2CEC58E21384B1E689B41286018CFDC0C7E8198B58094077FB271 + 2DA50DE3E20D4B9ABD83ED3B285EC38055F77A2DC7C9C7C5C3F65FEC38FF6A75 + 40E87F711DFAF95FD3BB7FF9DFD2DB96EEB3EF9276F2B2D531D13E1BEB80E910 + 105B1018B86ADF095ED1EFFCF21707C0D8A803F4664F61FBBEECF8F43F68F6B7 + 80BC668BDF85AB9C2FDF1580FC90F5CF9ECDF6BD9A8D3ABE277D66802FE777C0 + A47284C039CDDEBFC05FBC7DA3F76F93E7FCAC3DDF5BFE7BE4EF8CE30274735B + FBFB17ED7B69FF073FFED48C8780C187ECF363A30F36F21C17519687B08E4D5E + 103A9E530D86773C7CE2DEC22A2B1D241C75293BC82EC7D8FDD8EAEC3707FCDD + CBFFB7A50E7D4DF200DE8347FB437E6FF6DB363920B6DF2AB077E0B65D07F87D + 9A06EED8758297373C0C3B2F63BCF0DFDDF63BC063C6B0DFD4233BE7072FBDEC + E1D82DDD7EDBAE09B4B963C7B86DA909F8EBA19C7FB897FAAFD8F5E3868D79CB + CEEDF63307BE406107BF371EE7B2B5F53D8381C09612F29971DE79F14FE990FD + CEF000DEC40B2ED68EFD8777DEB4F3B4EBD2D6CBB7CB0B30768DB43CFB9B73ED + C446BA99EBEF25BBDEFD703F6DBF64D7A902823304BE92C3F07F7DD5AFADF220 + D68B3BD4090053F6C80D94EDDABF9517788ACDC3FA630F12F8D57D4536E4F723 + BBCF685B012030E097547BCD731F931770978A17AFABEC6FBFEE63EE81760FBD + F0795A71F6D3B4E2CC1F1CFE6EF8C4EA3EFACABD7FFBECDE8CA770BCCFE6FBF0 + E7EE254C9F95B4A90070A7FCA995F108CE513DF0FE25C2070202CB13585EBEC0 + 5E69C991F30DFC750F60F4BE958FDB5CA4A42B8E132AFA239BAF64E8CBBEC2A4 + D8961F27020973970C8301BD7802E7B2CD71FEFFECFDD7AF2DC9B6A787D5F6DE + 7BEFBDF7DEFB32BBBCF7DED7AEDADE7B5FBBFC39E7765F76B3BBEF6D2F92683D + 882024507A90040212D042F7935E4888A41A14A83F408FA1F18D885FCE3173E5 + 5C6BED32E79CEE7B36F043448C309973ED393323E3CB3102D93800651FD3CAC0 + 5D415EDAAA0CFC15009667300058619F3587D3BC8E3918F3B3F8429F3C80B1A1 + 5136AF1C4944191373CC087F95D625182C2F622408CC76253E870DF0B73EC725 + 0F001E21F0BB82972037B9C86BEF5F5EC61CB6649DCB41B09501C151A36C6E3E + 7269CBD33703E156190908036E81BFC32C3F7625F3767B1E2840D7EB1759DE14 + DB037EEB40B882C6C1031810DCA83956377BA53DEF64E80BC095A2AD0DF09A4D + 7D627BD5296C741D0447D84B997A54B7470181074ECF1018E01B21308ADEC075 + 985B015EF2A1ACB66AD3D6BEB48D1EC10E6D0BE8952D2A825EB525EFE19E8BBD + AD1ECFE120D6009417D0D5DA80CA51F575857A9D00B0C3DD9AB4C6D1A6007DBB + 1390B7570A9EC002B6E4EB40B80E70F3DA506EDFA41EDB585D04BF7D4CAD3EA4 + 935D7D00B81D406E0B004FF23A015FEDFBDB09EA46CF5E14FBAA8FFAD5FBD781 + 6E77EA6D3BD4090C47F519C49AEA584B598B6D4F95AFEC88355F6C83299BDDF2 + ACFF3A581D6C6361ABF2B97D1D02FB788C13A4B56C52A9DEA65EDF5B35F58FB6 + 58C7E7205F5F67AFF7E9B4BEAFB6514DED9AC4989DC68D75CA7752EE43DA5591 + 77A0A63651F5F67F0CF578EC1A00762654931852EF55FA34F1B406750B7FA3FA + F6CCB91E4AFF2901E09ED404677F4BE10DDCA782BC7F01C0BD01C0F147D7A4D8 + D6D5010037F5ABFA93D62F02BF81AA8B2D171F2E2E7EDC7C516DBFB876D6C3B4 + 7D58D5C77E989B4B5DBD3DC7DE1C23DFD49948F4A0087EA558EF939166005CB5 + 69E8DF04807B9AF87492FA75DB9FF344A5DCD4AE1AA378007785B55D45BB4A61 + FFDFDECB26BADD00601421AFF2B4571FD9DC5EE069BFE1653F5F447EB83D6C8C + 586893F805E91140EFE84569DAB2BDE9D56337D317D7FE2A6D3EFC461A3876A9 + 8DC9C3C5627BC8B0B680E0E100DF087633A455DE012D8035D87A92FA577D6B12 + 085659E0568A6D251E8AEA0F4D6A4BAA72F500658A00B83F60B5F6608622FC95 + 28038B058C631D40D73D762D05DEC650D200DDC1C0D9891902D30609FE4AC3A6 + 2D4B0B361C484BB73F9EC62FD8D0066E01C0C0601F67D2A2CAB3571017F82B55 + 50B7F4152C56DB5857953B7800D7C1AEC3DA628B9EBFF5B6A402C0FD262FE908 + 81558E120026FC7305811DFE4AED5EBF94F1061E309D858E350E80A3372FE19A + 81BFF200AEEC80E102809B20B087825EB439C3DF05EBD2A0F96B1D026780BBD5 + C1AF346EF5EEB4E5850FD2FEB7BF4AB3773C91862CDE98062DB2B196027FED58 + 8480F685A64D19EC06017DC712D26EC5B60A00BB57F0B2CD69048B4B40DC027E + 057BEB8B625A108B0B6619FEB616D5E469819AF3EDE0571AB3764F571B0A0B85 + F2188900786CF1EE15D8C57B770CDEC59BADBCF54005741175D8E5CD9B3D7A73 + 7867EA057815EE99FACAC3D7EC82C2805E79FCB2003A6D1F5E3478C9E4FD7F81 + BF941D08EF69857F06FECED8FFACA794DD33781FE11D9F73B5C23D5BBDC9E1EF + 41A0AED5B3888CD7EEA10C7BB5585C171078CA21EB47DBE2C1AB456CF2A8090C + 037A6797B097CA23E02F7BFBE211C50238DE5194593067211D1B750E8159442F + 5E5A02BF71415EA9BCB4B4684F0AD005004BF53A418065AF7F9496020DACAC70 + A1400541DFC59612AA549EBA8BDEFE38C38912A694761265AF03005B3B8006A2 + 9FA0C6226CEF7EEAB01709EA0A02E3DDABB0CD78F1027A29D3469EC0D4693F60 + E581BBECF1492A102CEF5F79FE92C74B78A5B5D9C07EB35F9DF570B14B3EFAC2 + A12F90297A1B923A28FECCC6FAE2A47BE366AFDBECB99BCB19E622CA2B8FD939 + 7C79C63D6D57B9C72F30F852DA7C267BDBAE3E7E3EADB476005D44B865815DF6 + DACD700F407ADE211F219997E3594CF8E73319FC3978749898419C87073E7F31 + 6D3011B6778DC3BAF3DE0E480A7CCC70360344C1473C76914235BBEDC2E5ECFD + 69C772207981E35CCEC0D604D404CC023C579FB8E065C15EC6AD0360FA61DB66 + 7D365B5BDA64E09BC1279EB4DB2E0177690F08CDE745FF16DCC5E3F6961DF7B6 + D96F7A3983E11B0E8A4911E795A17106B419C42A6C32E30057F1B2BD9CB65CB8 + EAE19FF114060223A0AFFA0AC8CADB77B3035DAB93872F70DDF29B2F5D4B5BAF + D8B9F2773A7BC901329F8DCFC3F9EBEFC179A24D769ED832BCCD02566F2E9EBF + 88F3139475EFDD73176D0CD402C69C4B1E1F989C4344E7B680DBF35606F62ACF + F72303DF4D17EDB35FB6EF02DF9502645B7DED7B6736815A4900989473DA7AD9 + FE7F1D165B7BB3AB8F7B0717D0AB72D35802C0AB4E9E4EABBE3A65BF8F3369A3 + 7DDF81BDE437B077F057A7BDBCE1A48D6FBF4960EFF2CFED3759C24C93AEF8C2 + AE0D6643781A13E21B0F7B80EE0AFB8D2FFFD4D2CF19D37E2726527E97ECBD5D + E5BFB073E4778A67FC1776CE9F9D76900BD0452B3F04FE9E7608BCEA23BB3E58 + 1EE84B48684FDFFF32EFEFFB01D1044EA6C5847BFEE82BF74C7FE2C1EFD3C11B + DFDAB99F4C0BF1FEB5362BFD7A65FD3E3866D733BBA6BDCD0B3176CDB37AAE95 + BACE92F7F0F7D686976696133ABF5C6397701DE5459B027FAB6BB5E5FD259C02 + 80FD5A6ED77EA0B0602F928D76B461AF6042FD734FD0BD42A903DFF05211F790 + F905F60A00AB8C3C724580BFE425DDDBB8E791CF2F3C713F7CC13D8081C0847F + 0600A30C7F7328E869DC9FD98681FBED63B4B3FB2DF5D69EFEE481C0DCB3BB02 + E00C87B9CF13D5C3F700AE41603C7E057881BE4865C1E0C9A6297B0B0436B18D + 0480778AA5932D9DB0DBDAF8961378163F9566968823DE97F1CA1878FEE2E9EB + 2FB51D301DB463EEB539111EC578175BBDF6FDF5B0D105F64A0E864D78007B18 + E832976B9FC76143F9C53D5ED6C31318F8CBFC4E9EC19E371B40973442DFF8C2 + 617DEE29F8EBD0B7F4950487350756D41BE6B963D75ABDCD73298FA6FFAAEC09 + 2C01800585F1FCD5162C11FEE2093C7AE5E6EC115CE4737400322F6B7ABFAC61 + 78079774282F775A7ED286DD6916F3CE8D7BDC3678E19AB6F6D822F88DDEBF11 + 000393F1187695BD8081BEF20446BE3F7000BA51F2008EB60880555787C3C3F0 + 2CA69E3E35088C047E555F07C0AA272FE82B8FDF3A009606980478EB0058619F + 81C511FAF607DE06795D91FA08F846BB6C02BF82BEA492CADAF737DA057DA322 + 0C8E6A5F0FC8629D20AA5EF7B000B8EEF9DB4915E0ED49050047382B001C85BD + 13D415248EB67ADB58AF3A57D94FB88BDD6D53AC5F4E1D08036F2B4D345B86B7 + 02B511002B4F5DF4E0952200A61CE16FBD6DD4C30060D4658D31486345618F6B + 93510060C15EA90DFA963C40D7BD7A079776D46B1DDACA0E7C43BFD8D7C53A71 + B5566C6DCADA715D119C4A3DD5A34E5EC4A8A97FB445BBEAE21A7BB44BF5B573 + D5C7F651B16D2775B71EAFF5FADE2A42DCA826EE213D6CFBDF52BD3E76033BAA + B8D143A9F469E2694DEA2D03AC0030E3FF9CF3AAE92F00F8B7D35F00704D3558 + DBE9C755B7776913616F18AF2E8D5357E30FFF3750EB626B6507C0A6868B6B6F + 152FE0BF86EA63C71B4BD34DA6BB1BCFAF798E159CAD2B02DE3AB895627B9B8C + 90F6D603B8EA4FBF86C9CDCF517D42D5D4A60E809B548DD10D00C6DE1B35F5ED + 2C9BE8B601609BB056F9AE0038D6A17E43A79A1D4DB709F13493B5B3B6F20AEE + 370C086C0F0E23E6D9247A5E7A64040F1D8BD3AA5D2FA43FFC8BFF36FD9FFEDD + FF923E3EFF5D1A316985FDDFCC4803C62CB0C9FE7C6B6BED4658BF918057815E + 424767E89ACB2DB0DB5BD10FC530D051BD05C04DF6269B1E9AEA75F1810A0FE0 + ECC9DBFCD046590F7782BF48F626017715469ABC422E472F60041C561B3472E6 + CAB47ACFD369C7D137D2ACD5BB1DDAE2F50BA0ED3FD11E52AD2C602BA8DB067C + 4B5A07C15E07EC9D9C4345A3B6BA007F91F6008E1A6002E23AC83509FA46D501 + B04245037E23FCED0E0493AF03607900672F60B3CD5EEB02FE3AEC2D10D83D80 + 0B0016CC05E4CA03380260A436D156177D81BF83E7AFF5F0CE847926EC330058 + E19F49C7AEDA99563FF1BA43E0E95B8EA461661B0AC45DCEA254F64A18BA7463 + 5944CA107854903C80B1E3FD0BFC1D16423DCBDB17B118A6053024E82B18ECA0 + 7895A97854C8CB02C585372DB091B238377E23DA9FC66FD8DFE6F53BCE34C16C + D8298F5E8BC707FBF81E6C941610C76FCE5E232C0C8EDDBCAF02C0A4805C445E + 3038C25D415DD509F8CAAE3222EF00B8405FBC7B2560AF7BFB16BB00B0B72D21 + 9EE5012C003C65EF5349A19E597425DC33C057E19E0580A7B3288BA72E1EBD96 + 4E3960F5A6B850CCE2B12F20B3A8FCB8B567B139C05E8573569E14E80BE04582 + BE12E12B91D7D516C0C94FB3F101BEEE05FC74F6049E0B0CC63BCA5407C0BE08 + 5F602E1E5D78FE525E4C0850130BF5D8F1ECA20DA0173B792DE45396F72FF097 + F0A1F21A9BFBD23BEED90B5400DACE7DF5BD34E79577D3DCD7DF4FF3DEF8C06D + 82C282BFF2F875C86BA9802D909750CF78FB4AD8A8973C84339EBF05FEE2B5AB + FD7CDD93D7CA31F4B3D79B627867815E87C02687BEECCD6BFD577E9AEB57005E + 3E04EC9EF63D60379FB96CF95369C947C7D2A20F6DFC4F8EB98761F43A5CF199 + 1DEB33EB7FEC9409F074CA0114FBF4AE035A016A0B00060CD36E151E885F5AF9 + ABF369FDC94B395CF1C92BD6E6625AF9D5B9B4CAFA0A00E7FC791740559EB151 + AB80C8847D2EB00F389BF3D93BD33D32CF5F486BCFD918A76DCC5380B773DE66 + F30560EAF504B8CC5EA9654F5AFBECEBCF5ADEE12FA18933D4240CF0E6CB197C + 6E270CB209D8C89EB4ABEDB312DA78838DC139AC3995013060B30293762C0793 + D606AD2D5078FB953B19CE5A3D1EC48CCFBEB9C0D91D576FA7ED576F799E63E5 + 738E00183BB09736373D2F090223C6DF821CBE0A78672F5DC259734C202AE78E + 77F08ECB37D3B64B76FE78039B9D3E5B2EB6425F233EFF0E3B376C40DE4D56BF + D1DAAF77107C296DBC78D9C3264B80D12D8C69ED37DAE7E06FB0C9CE67339FF9 + B29D9B89F2FA73F6373D0B440582DB312FE3496CC737F958805ACB331EB03643 + 60007686D43A1F87CE5697A1B17D07CE9CB171CF5AF9A21D8B9703CED918F679 + 6D2C423A23424113FE79B57D4F08FFEC7DCD26301BE5309750D1252C34E7C279 + 700EC074F5E17B18E1AE20B06C486D10752BED77B4FAF8A9B4FEC45907BDEB4F + E23D6FDF5BCBF31B941D008CD72F21A6F9ED69EF61F2C05FBC7F81BFBBAFDF4E + 5BECEF8DD7FDD24FECB7FB857D3EFBFD6DC37BFC9CFDDF13A6DCBEA3BE87B600 + 3061A1CB6FD5BD828B27701512FA63AE3F27DCEB9790CF6B3E3BE3E09730D0CB + 01C0780B7F68D727AE2D764CC23D736C8EB3FFC63769C785DB0E8097946B8FC2 + 49037D95F7327B039728087A29C6AF87256CB4BF5C5342F02F7BA7057C75FD45 + 6AC30B3CC0DD855CF75FB3EBAD5DDB01BD7E6DE7DE61D779AF2FD77C7909572F + 03BDDC7A6948F71CDD373C7DC1EE236613FC45805FEE5BF206AE4360EA48B9CF + 0181659BFDD4AB0E7181BF739EE61E983D7FB9F7E63D80F1FE05F4722FCE2F4E + CD3C9A3D86B193E241EC61A4AD8DDFC7CBBDBC8AE06122EFE1A00F64002C088C + 047FE5E52B6F60B59BB23FDB26EE79DCC342B33F30E097F9049A72D074E09934 + D9E61813ED5960FC2EA0315B4CD8BCC1E61BCC4B9887E410CF96DFCD9CE64987 + BFEEF56BE984028035EFC92FC599CD01F0A369E2B6BCF7EFA41DBC68D7F20826 + 2C749EB7D95C6C8BCDBD2C6D41E0FC229F22B2C8EB57915B7C8E87CC26E0AB10 + D0F5F9A65E46EC0485A3D49E90D01100EB8547CD7B95BA3730C0376898CDBD87 + 038409F76C793C82F10C4663566D75A92C8FE1EC352C009CC12F12184663576F + 4BD36D7E3ADBE69F0060EA0580A977F8BBA81DFA0A06D71501B0C23E930AFE02 + 83B10D9CBDC23D8109032DB04B3EEE0FAC3A81E0087F090FED7BF85AEA754582 + B811EAD6A5FA260FE001D36CDCA9ED00182924740C0B1D01705D0E6D4D02C392 + C06FBF89F32B45D81B416F1BC02D36C15FB59397EF603BE731F3D6A4B1F67C37 + D43E531708FC3301705C4388AAB7F9390038AA13F4ED0D04A64D7FFA16C02B50 + 2B001CF351B2D7A5319A616EBBADA94D9B2A304CB90E7F5BAAC06C00C05919EA + 52D704805184BDCA47301C55B56B80B6DDA96D7DB1976A5C9B4405DC0ADA7601 + B7A50EC8DB0F083CA480E0FE6352BF0119BC7A3BD681590F2EED2BE0EBEBC3AD + 7AE55937AE4BB014750774A54E7DEBAAB789FDA2627D5C1B6FEAD769EDBCBBF1 + A5A6365A8F473DD5F74675902BB558477B39DAA262FD1F53BD3E76076E54674B + 5D556F53CA4D3CAD49BD65806D1EC0BD39AF1EF41700DC3BFD9C90D07F01C00D + 0AB0B6F73FAE9FAFF823F66335FDE87F13E50BA7CB8F6BE743DA2FD863BDB769 + A833D52FDE7F8EF273ED67379AFE76D3EF46BD6A5307B99D14A16D511BE01D54 + 6C610FE0B6FAD0AF4DF40B139C5FAAB60992A9A94D6F94FB4FAA01DAAC0AF016 + 4FDF366193ACDCD4BF3B3D32C45207BFCD6A0B93E3360160AB631FE208808766 + 4F6007C70E8D67A63EC367A7BE23EC41C301B069CCE2347BCD91F4E9C59FD277 + FFF8BF4E4FBE73260D9DB8DCFAD20EF06B0F27A3EC6166943D6C8CB6870C7BE8 + 10F09507AFEF215C24A81B416F93047DA32290258D00587575A94E0F44B2D5ED + B15E8AE356F526806E0CEB1C151FDAA25D0F82F587BF5CB607CE0275C92BBCB3 + 3C7E49058381C0DE8E7AB38D9DB72E4D5AB4C961B0D75BDD8049D676F2227B30 + 9D6FFF7FF6C0683600AF602EFDE8DF0F486C6D73186720AE3D809B0476B10DB6 + 0775C270D5E1AFDAE6BE19F00E986C639800BE78F1569EBC006093806F5484C1 + 43B015C0DB047A2500AF43DE90AF3C7F8B9D3D80510C011D21B0403036F2F2EA + 15C46D02C01290581EBFCA7BBFD2C74347D316BBFDDF007C057F072F30BBD946 + 2CDB9626ACDD9BC6AFD9ED65403140172F5CA02D9EC08317DB38780E2CD9E8A0 + D7A1AFA5E4EB1A010C06E41645084C3E42602D84550B64ABB6A551C5A3428B6D + 5A7CD3C2DBF8B250C79E6DBE7F9BE5C76DD897266E3A98266C3CD00E802D8F0D + 088CE7EFA875190067CF900C7B516BB130DB1D0617CF5E815EC15F815CD91DE0 + 161BA24D6C2FE02B383C75672B8F08F95CF7FA9DB6A70582B1136671DAC127D3 + D48347F30229E116F75999055AD2FD2CC4E630CF128BB2B38EBCE87BFC4EDEFF + 749A8497CE011682F1EA659118EFA0172D7D314D672FC0432D0F60168A491D02 + B3C87CF4454B4BE8E6E2F51B2130C226D8DBE8FD8B37EF93AFA5B9D5A27786BF + 71A15C65E4DE542F847D1749CBE2BDA4F26CAB9BF1F46B5ED61E90F2F69A4588 + E817DFA9BCBD58C0475AE00702FB42BF89BE0003F6F145C00420033017D83BFF + 35D39B3636FBF616A01BBDD5E4E9EB00F89D4F1CE80231001A821A7805CFB7BE + 94A907F2AA9DF6F05D42B908C80BD095072F02FAD256ED1DEC3ADC2504EB57D6 + 8690B26793F6FF5CF1D989B4CCEA15EA9574B98DB3EEF8B9B4FE04D0C7DA1DB3 + 765FE0514838D9D3EEC92B2F43A053B69DF5D43D7CAD1EF09BC32B5F74F84BD9 + F78B3D75C1F217DCF317A884D61EBFE8DEBF1B4F018181BCD9BB9750CF78FD02 + 7D057ED79F06EC660F5D097BF6D005D40283EDB867816F6607CA5DC820929432 + B00E39EC3B7FC58125E012080994043C5600D8F26B4E5FF2326DB65EBDEE0078 + 9389FCD66BD71D48FA7EB0004F1B2B6A33B0F372069AEBF174BD7CCBEC372AF0 + 29102CF8C9396CBD72D38F01B466ECEDD76F9800A4766C1B2FEB86F71744DDE0 + 5EB4D90B3883603C86336C16FCCD9EC0E4F10AA6EEAA835E01605479F75EB892 + B65DB996765CBB69A9B575808AD77306AF0059ECDBAFDE48DBCADF44B097BCFF + 8D2C8F4D76E0B9FFCDAEDA67B731F437E2EF8AF83B6D0326DBE7DF68E7B4C1CE + 01A0BBD93E77EE97C558FC5FEA588C4F4A7FFE361A93FEFC3FE7B6F6DD396FDF + A733A7ECFFE98CB5BF6863D9F7EEC239CB67B0AFEF88BE27FC9F720C8EC9F894 + 057ABB7C8FAC8FD79F39E7DFDFFC4242F62A9697AF40F406FA9EE62584D3F61D + C3C3D8FADB77A70582F9AE5F48AB7869A278FA027B1DEC9EB47178A1C26CEC17 + 8C67AFA0EFC633FCB6EC585697BD85ED77FE5586C380E48D760C5E705873F252 + 5AF1E5B9B4D27E7BFC6E765CBD63BA9B765CB99B765ABAC5BEF78467273C34E2 + BAC03EDD7E7D10F8FDC4EC1F15686B222CB44419AF5FF2BE67701943D7982576 + 1DCAA1A7CFA6A536D682F7ED5AF5B15DA3AC3D2019F8EB61A43FB0B12D05262F + 7CEB53B3731D0C7B9D733DB46BA33C83B90E2F7CF383B4E49D8FD3B277ACAD5D + 9F09BF0F14E67ADD0680CD26284C996B3CB097EB3C7DBC1FD77BA02EF703CB23 + F27A5108E94523DD2BB87F60230CB4EE59D1F3579EC18A66310B952816AE0281 + F33DEF4D6BF7867BFF0ACE424FC70000FFF44944415402FB5EC04FBEEC30983C + 7B00BB3770A873D84B5DA9A7EC7B06D3D6EEDB731E7F31CD79CCDA07084CE867 + F6FDC5E357E057709817C026EF7FD2E609473D95F472185EC339FFB4E5F1222E + C0D734C9E61713F63D93C6EE792A4DD8FF4C1ABFF7E9346E17FD6D5E71F08534 + 656F86C40E8ACBBC25EFF7FB84CF6D08FD8CF7EF84DD477218E8DD251ACA4E9B + 57EDB479150A73AB895B6D1EB6ED509ABCDDE65096CABB1779BECCDD7CFEB6A9 + 25EADCDBD7E690EA132130F34B4030F07702769B5B6A1E4A59D017BBE6A682C1 + AA63AEAAF9AA976D8E3B6A2580B7058025BDF828691EEC73E69539F57E44D859 + 66A9CDAF999BFBFCDC6C48E591CB36579038C35E60AE5422F814080C389EB0CE + CED152A02FB097173CA9A72C2F6107C6257CB43C7E057DA5088281BD0A052DF8 + 2B28EC00B8805FC15D01E03AF855BD6C02C0DA23D8EB695703BC887204C34D6D + 50F4086E83BC05000F9A0128CEAAD76B4FE0A690D092ECAA93F76FACAF7BFA56 + F0D6CAD1AE32F93E63ED397EDC9C34CC3EC354FB7F9FB9DA9EA5E6AFF536311C + B49EEF7B2B81DD4ED23A81CABF140047080CD4ED49B1ADE7475B3EC0DB087863 + BEBFB5439DEA0574354E1BCC8D0A1EBFBD6B4779721680B6805887B5211F0170 + 270F60DA44514FAA3611F6C654791FA301F276A7FAFA642735AD4922EAAAB106 + B386DA02BDF55479A0AD83502B3B0C360DB2731FC8E7603D997A1BABF2042EB6 + B67C11ED7DAC22CA759B00705DB18DFAD5FBD6556FA372D39A7DACEF8DEAFD7F + AEE2B9349D43ACEF8DEA20576A661F590FDBFE4FAE5F1402BADE26949B98DACF + 551700DC9B73EB467F01C0BF9DFE02803BABEDC755F3E0EDF283EBC6CBB793E2 + F87535FEF87F75850BA81FD3CE8BF4210170A78B76B4FF3908B0DB1BB8DB9DFA + B2FFAEA58D50B6070174FB0E9E98FA0DB18988A5C8C33863EFE001DC110263AF + 4D727E0DF53491AAABB9ADD9EA80576A82BFB2FF110070539DD4D7BD7FD18C06 + CDB2C93370758E87807E64ECE23478EADA3463D5FEB474FB3369CAB23DF640B2 + C826F1F6F032C21E6246CD4F03C7583ADA1E4000A54511F20AFE921700AE4070 + 51DD1EC741B4A97BFC7A3F6BA3B449FE10141E909ADAA89E7C7D2CF56FED013C + 3B3D32CACE6574CBBB571200D6839B1EF0948F0F7C02C3B92D75C0E12CC1E028 + 3C82874FB7076A4BB5BF2F219D01B978050386B5576FDF09364681BBE46903BC + 25553FBC7B099B15412E60973E5286BB190C0BFC4A82C30E880BC405F2027BE5 + C15B79F25217D4048195772FE0027505733B959517FC15004675008CB72F1058 + 0078F8BC0D15044683E6AC7500EC1EBEF33B03E0087D23FC1500F6F6D8695744 + 586841DE8176DCC10BCCBE78ABD9B658797DEA3F87C51C00705E58C213D8BD7F + 97D9F1083FB78C3D83DB179F22F8D5229752A4C52D5FCCA26E55F6F0CDDE1165 + E10B158F5F2D9C49B4A38E3C8B709358E8B314F0AB707E63D6B10067F6E2014C + E867857F1EBFDE6C661FBD21EFF9DB4980E1311EFAD9DAB338B8B5252D3C026C + E5CD1B8130628152EDA98B0058A25DCC037FD9F717C9D3D7D3DD395F41E0FD4F + A4E9879E4A330E3FED1E332C9CE2113CA578E9080047B1B83B0B2FA0C3CFFB5E + BF2CD6FAA2AD0360BC7C5F4CB380B6AEBC502CF85B790B617B020F23006F2BBC + B3A0AE3C82A3E4053CF7A9D7D33C3C792DF5B6EC5F08442E0BDFAD05F0EC3145 + 3AC7A4B2836016D59F7F33CD29F01790AB709D75100C0426D562BEBCBA90E0AE + DBACBD00300BFA883A2DFE030B1C1ABC99E12FE19FABD0CD6FE5BD7A01B9F2E4 + 05043BEC7D377BF2CA834DC0D7E145F06603F62EB6BCCAEEED5BA0AFDB0BF805 + EE52265DF9E9892ABCF3DA2F4E57FBF8CA0B18008C4D5AFBF9E9B4EA7340CC89 + B4D25220B0C31D135066298087765F0189CF583D20E9A2435BDFDF17C85BF615 + 052E65F89401AFEA15FA390260D24D67AFF838ABF1F23D86CE3A005E77F2625A + 7FF2725A7FE292E77DCF56139EA8405D815E850BAE8BBAECA59BC1D95A8EEB60 + 378342290249CA9EBAA72A1E9E19D60ACA026C335CBD6E79C6CAE09531007DEB + 19F3927DA62B196E6A5CDA0022112053A920E7D62B40DE0C7A059CE5010B1816 + 08DD4C18E1CB57D2D66B57D336933C8F09A99C8F913D66350602F46EBF7CA778 + 13130E3A7BFF5206F0B907B0C3E00C7BB7D979E159BC8DB12EDA7914EFD9CD76 + 0E1C8763EEB87ECB2130D017008C872B79A0EFB66B368629C25352CAD8054EF9 + FB6CBA84FD8AD9ED3CAD7EC305FB7FB5BF7BF579CBDF0D008C365B7E13C0DB44 + 7BC6DA768D71F3DF61A38D895DFF1F8CC758FC3FE6D0D3ADFF2BE02F9017E8BB + E9D2051BF7A28D73256DBF61635CBD646387BF6F91FF5F5A1AA1B3A0B03E6BFC + 5EA9CFDA33F6FDB6DF835E4420D4B800307017CFE34D1700BD78059FB5733D6F + 65FBBB9D070CDBF7EA2CDF79FB1DD097EF32A1CDED7705F85D77CAC63DC16F2B + 435FC46FD07F8727EC778847F1C90C7DF97D92DF0414B6632F3B763C2D3F76C2 + 7F1B6B4E5F4AABEC378DE73CE265806D576EDBFFF33DFF9E6C387D25ADB4DF3D + E1A1B92E20CA7E9DF89430F2766CABE37AB2FC635E3AE1FA63F576ED58F2FEB1 + 0C84CD0E00C65B58FB09D31F6F6240F032BB6E2DB3F60BDF3BE6FBFF2EF6F0CF + 36167B0617082CF8EB220CF40739A4BD4739B0EB22D74E5D4FFD1A6CE9A27708 + B16FD7EAB73F49CB080BFD7A06BC0AC94FCA0B3CEE295CC246CF070017AF60DA + 2F7FEBD3B4EC4DBB96BF66D7F172CFE03E40CA3D25469AD0FD4251242853C74B + 49F39FCDF0B7BE17B08BFBDAB36F38F4E52526D482C0D93378B6DD1FE752C6E3 + F728F7C8BC0F30E9FCE7B81FD286AD12B2047FA947110213027AE6E32FA6B976 + 9F9D7BF4E534E78997D26C2B3B043EF2ACCD1F9EB339C1F36996CD0980C1D57D + BEDCF381BC7545104C592F90E5C82182BF4FA5896C37F12810FB9534F9F08B69 + E27E1BF3887D9EC32FA5498483B676110057731B9BEF007E91435F13F3A2F1DB + 6DCEE5DA6F73AB3C07F397ECB61E74788B80BF483607BA65EE857C3E66E958A2 + B7D89C91FAE8012C184CAAB28787369B802F7352D2693B8EA429762C815E246F + DF28ECF421EF6DD7DA1CB6CC81EB8AC057F365956503148F011633BF5EBA3943 + E0A09166F3C83B3657CFA1A1B317B04245B7D402C0CA037E07CEB7E7154B6507 + 04E30D4C8ACD4170D84718101C2130F90A0CCFCD5EC09D00B0E06EB58F6FCDEB + 3786788E00185B5BF867BC86E947B9A813E025AF36B16DAC07F2D63D7F230056 + 5DD5C6D41B001CDBC85697DB6B52686804D89517B080B1A0F070FB2CA3F95BD8 + 67C01601B09EF7E333BF9EFB95D6A53583DEE86100B0DBC7B620B0606E84BA52 + 932DCAEBC6F062BE950BC48DC21EEB3A95057225D5475B06B9AC4F4DC92A65D5 + 35AAB46BA90581056395AF435D49B60ADED6DAC5FA087A633BF59522DCADC06C + 597BACABBBBA26755D8F6CAD6BBA0AB015EC6D83BE353B1EC000DF014358A3B5 + 7AADDD5BDEBD82435BF577B1DE8C4A9EF69DA06E546CD353DBEED4A7AC7937E9 + E7ACCFF734665DF5F369EADF132B88F5BD5113CC45CDEC23EB61DBFFC9F5A704 + C0BD61806DE3FF4AFA0B00FEEDD5087FD1DF61008CAA1F564F00F821157FB84D + 6AFCF1FFEA0A17508EE9E19F6BF6A0FA85395EAC75818FED9BDAFD71553B760D + E63EAC80BF12B0B64F1398AD2B7800D701B0C35D2627D81F0200BBAD36C1F9B5 + 14274A4DF552F7ED32C4ED4E5D002EF69F097F514F00B877CAE1A209FBDCA780 + DFBE43B3DC2B7818B075B6ABCF283C59E7A547460284EDE163F4BCD477943D6C + 0C9F95060E9F9D068D9C6BB232E19F03948DF0B6932AC85BD226A9ADE06FA500 + 9423B4ED636A83B22681DC4ED20394C668AA6B01E0BCAFAF006E140F667AA8E3 + 614F1EBFF1618F765D617157002C4F5F092F60F6F305007B9B220FD58C476FC9 + E3FDFB0821A92C05D2E2F94B3B0F310D24B6FE0E83AD5E3037026029D6AB2C9B + C0B000F0E0A919F2C650CDD17317AF5EC06E27002CB107F0900270A53AD8653C + 89F1A98BA19FD587BD7F730868F239DC7327003CC43470360B299D01B0520160 + 41DFBABC2EA80D022F6869C4A26D69F8922CF712B6FAE14BF282D288A5849523 + BC5CD96B6CB9E50BFC8D0058DEBDF5452CF22C74B9B7C3EAED6964F1EC959784 + 423D47F81B17D3B03928365106FC4ED8BCDF17E758B8CB10184F603C82F778C8 + E7F1EBF7B9DC1318180CD82D5EBF88BCA0AF3C82F1FA9DB005C89BC309B2A79C + 40EED8CD79DF5FF22C40026F057B05802BA06B658936D862B867F6B19BBC2BEF + E93B85508ABEE099BD5DB4188AF70BDE317991F468DE3B6FEF130E7915AAD1F7 + CC3301869143E1834F569A7EB07D7F3FF7F8298BBC5AF48D10984562ED17A8D4 + 178D4BF849C2504EB5F6DAE3D73D7A4BA86780AFBC8267309ED509FE4A0E809F + 60D11AAF2516B15F754F2916CB25CA33CD4EB8675F587F218778268433705780 + D717E70BD8ED495AD897E663B33158C06721BF0AFFCCC2FE6BD9830C58E0D0E0 + 9D4FDBBCCE1C023B7C680160E0AF40AF3CD6047C25CA48E19DF16803DA0AF0CA + CBD7433C9736AB0ADC1508A64FF6ECCD205802FC52272F60E0B0EBF35369E567 + 846ECE00387B029F7638B312E0F3694E01350E6D0A005E8F07EE89B369CD7100 + 54F63C147C5A7F2A7BFBAE3D49A8E6B3D51EBEF2E415D0659FDE955FB117B0D9 + 09316B63AE76E07B396D3A73356D387339AD73808B67A5A567ACEEACD900C7A6 + EC450BE0CB5EB319D0E63CF6B5845D3E9DFB09D439AC3501EF04285DC5FB54C0 + 1001101D1C120A19CFD80269B33DA7712C07B4573328D5B1D69DBDE4EDE4C91A + 213063A36CBFED6346004C4A5F4224031DF1FCDD79D3C6B90A942C9EB5010033 + 8620B0B4EDEA9DB4FDDA5DAF633CEA29EFB871CFEAEE5A39F701446FBF763BED + B84E7B6CF9D83ABF6D576D7CA0EB152B973D6D3DECB27D56BC91513EC79C8F00 + 15F8BBE3868D5FECC05F202C9F63FB75802E609EFF17FE86B9BDFA0BB86665C0 + 9CF3D44980E13A00064E03EAF3FF2729E351CFB168BFFD3AC7E77896BF7635ED + BC952130E7C4988C97C76E07C03A86BC7D2953DF0480F379D8F70B4F6A3B0F20 + B77F97DD73F89CF5C72319909CD34D975A40397FB72CB53EC84374DB777AA3FD + 2636B067B509A80BF815085E0B08B6DFE0CAAFEC377FE294836680B3F6175E6F + DF55BC90571E3F9596B37FF7890B6915BFE55339AC390018EFF1EA3B78E1465A + 63BF4D7EA7ABEC774AA8765781B8AB1CFC0280ED588472FFE29C5D4FF27563F9 + 27A7D2D28F4EA4651F9FB476761D301BF56B8ED9F14B1BECD4A315EC176C2921 + A1977DF45506C078017FC0F5EDCBB4FC03AE61BCC8923D8B81C34060AE835C33 + 81BD59F9BABBE43D52AEBDEFD9F5F9A30AF0CE7BEDFDE21D9C43F313B21FE0BB + C4AEE98060803060D821F1EBD6EFCD4FD2D237EC7A6ED77EEE137A09A87A5188 + 7B05F0D752EE17511E029ABCDD53A85F60ED80C002BFDCCFFC4526CADCC3ECDE + A548165140E07C3F241CF4ABD6EF7587BE02BD94B15761A12D8DF017CD2C4018 + 00CCBD1A083C0B205CEEE9F17EEEA1A1E501EC7380B0E543A9D74B6092C070F6 + FEC55338471141BC5036159BA52BDEFA3CBDFDE33F49CFDEFAC9FE261FA48907 + 9E4F5309FF7CF8251BE77907C0936DEEE22FAC99DCFBD7E74179CE336997CDAD + 76DB5CCA347EA7CDB776D91C6C57899062F32824002CF08BE4050CC075881BA2 + B654F33140B0EA0BF48D79527902FB0B85AA07E05A79A2E567D8BC0D08CCDC53 + D167A807F2C639ABE0307684077027AFDF38576EB291573FF702B6F9765D7A01 + 337A007702C0EED12BA85BF2805E79FF4AB249B41BB9B4E50DDC1412BA02C205 + FEC610D0A8BE0F30690CFF2CBBEA62B96E77704C1E7B07B83B8070D126F29DDA + B4007086C082BF75D501700581FD79B578F21645005CB77592A06F84C0B24500 + 3CC43E0F36CA8F144FE07E13E63AF8CDF9DCAE27002CC575829E6C757507801B + 35B619FEF62481DFBAEA6037C2DDBA5D12BCAD97EB7D648F02E67AEAF5381244 + C81BD40900D724501B816ECCA39EA06E14F5EA4FDAA5BE40DF7EC3AC3E48F6BA + E27A636F545F938CB63E8358471D6BE93857CCB72940CBFE83C7A72123A7A6A1 + F659060D9DE860963600625481DF0EC2839831A43AD8D57150B437497D3BB58F + 63499DD6E17FCB35FAFA39C63AA9BBE33FFCB9D1B6ABBA728F961EB6FD9F853A + F0A33A63EA59F4E97DBF3E72727C4847C75FACBF00E05F5F7D1B6CDD02E07EFC + 31ED04FF5C21B07F597ECE8FA05DF51F52A71F5677F65F438D3FFCDF40BA783E + 0C00EE4E3FB7DFC3AABBB15B371B5DD44B5D03D4AD4B1EC2756FE1087FA50ACA + 760A072DF8DBDFC60A10B80DEAFA64C4C6EB060037294E6EDA34A4A8A9AE17EA + 6E1225C5364DED7A0C01DD090087BAA636DDE9610130619F1BEDC36C025EF6FD + 05FCF62B72383CD426DBC3673A187E84D0D00E23E7DB247C964D6AAD6C75FD46 + CC4A03ADEF00537FB3F563CF61A0AC03D916B8ED4EFD0B64157845FD11E0157B + 68CBB87500EC75A3DB816FA531261ED6C6B4606E1DEA46E93C623DC05775B1DE + C7ECF080865DF0B70900D386FEB98D3D6406C0AB90CF82C0B18EF4113B36219D + 01B8805D79027B68E6E2FD0BA4C5EBD701EDD4A519FEF2391883F1516927E10D + AC312581621FBB06802507CCA6B8DF6F7700B809FA22D50D05DE06C81B01B000 + 6F84BF00676C0060A43E0E8467037C59E0C8E01700EC7BFE9690CF521D000371 + EB0078D882CE005876D5A95E21A0057F872FDA5A69C4E26D69D4D21D69C492ED + AEE14BB6953D8281BF9B33E85DC93E6378FE6E4AA3D18A9607B0207017D85B16 + B2648BA05730579E1202C202BD11046B11CD17D5E84379E35E5F7863014E0204 + E305EC1EBF1BF6A789847D36E1F92BF8ABFDE114F2592098FCC4AD87D3E41D78 + F63EEA10783C1E25DB5A61A0058027B0E858162223E48D5EBDD106F8C59B577B + D94DD9C3BEBE396CF3F4FD4F7919E570884FE6F2AEC7D3442B6B0F3EF213D803 + 8F8553FAD0F7E0D134FD10D0B72BFC45D1231810CCA2AF03DDC3CFFA622E8BBA + ED5EC0ADC56260309E4384909C6165E0EF94C3CFA469D65E1EBED10BB82A9B66 + 17288C649FFD849503F055B86716CAF33E8A65F1BCD4E1EDCB02FBA297DE7560 + 1BE1EF1C3C825FC4BBF77D530BF06AC15ED057AACA2CD417CF2D1FDBCA805FF6 + 7A440E7F0300C6EBD7BD7D8BAA70CFC5DB372B7BA6094C5480B7808BA80881E5 + B14B0A0046E4D9EB57DEC00E700BDCA5AE2DD4F3C779CF5F0160EA0491A387F0 + DA2F81BAE7D31A4B81C078FC0277D67E75C161CD8ACFD83B94D0CF67ABBD7AD9 + D3B7057CD963142F442B9BD69C389BF0BE255CB300305A4DE8E7D3171D64ADB5 + 943AF6F1C5939730C5842776086C363C79F14E059CAD3963E7E1FBAA5EA800B0 + E0AF60B06CC0DF0C4EE5B97BD9011C300DB08636B14F2CA1770BB0430042FA67 + 000B44C64B3587111608059492A76D066480E10CEA1837824E0020F00EF84C5B + A0AA003063903236C7210F9405B809643BCCBD64C7201CB3F5074632EECE9BB7 + 2B40EA21A02F029133D8A58F20B0CA8C0B04CEE79EA19E03E06BF7D2B62B77D3 + D6CB369ED50380E571CBB9C6F3DD8AED2ADEC119006FB1CFB6E5520E85CCE745 + DB0ADCD539522771BEDBF9BB98B658390352FE5EB4CF0078D3C50C4E69CF18A4 + 8CC7DF51E3E6BF2D0038C25F79E866E81AFBF079F83F72E86AFF0FF9FF1A000B + 6CBDE8C776007C8530D617ED335C4C5BAF5D723BC78FE3E9BB439954DF1BC15F + D9F45DA08F40B04275EBBBE320F83C61A501C8E7AC3DE0F782B5CD9EC9C0F18D + F6F75807003E7BB17C87333CDE48386EFB2C847746848266CF5F0FF34CB8E713 + FC96EDB72B2F63BC886D1C87C9F6FB611FE3E5274EA515A635EE8D7CC97F73EB + CFD867244438FBFF5EBAED2921CFD7DAF156DB6F7995FD9657F2B28600300202 + 735DF8D48E6FD79375565EFBE54587BB8BDE3FE6F057409874DD57F67F70E272 + BEA67C7A3AADF88410D2A7D2E20F8F5B7BAE5D5F99CDAE55BC7C6263E2390CFC + 5DF6915D9380C7360EC742CB6D6C2030F5AD9767B8B6B6B4E89D0F4D1FD8B5F7 + 7D07C0BCA803F49DF3DA7BBE37BB476678FD0307C240608586E6BA8E28038149 + 510E0D0DFCCDF7004160F6845FF012DB04D871EC7EE1DB04703F010ADB7D8417 + 88FCDE61A97B0B73DFC213F8C577F37DCDEE6F8061BD74E41EC3CFB7F6B09737 + F09C675F4F739F251206F7C437D262BBAF2D7C01889CE12F901720BC80BE6623 + 5FC1DFA376EF9667B0E501C0D3CB9EC0D31E7BCE5FE08A809779C07445091100 + E6BE6F7D669739006D058205833304CE51441C005B1EA88BC80380F79CBA91FE + F07FFB7FA68BFFE6FF9856BEF5451ABFEF99EC057C9079C54BDE7F022FBD9539 + 0F0078EADE66003C61F7A134719FCDA7D81378D7911C25452FD4E189CB1EC0DB + ADAD49797900335FCB73B53C87F37E366F9B6879E671630BE8A5BD0070F40E76 + B05BEA79E1709CCD2D81BE78FF0282290B0033278DD057F3D6B6F96D01B875E1 + D5CB16289A2B932A2F008CB0B5B533C5F9776B1ECECB9A82BFD91B38C25FEC82 + BF82BA6A2BBBDA693F616C0A07EDD07889F52DA1A0EB105800B80E81872DB0E7 + 1C93DBE6B4423E930E9FB7C61541B0D4048163B909FED6C3420BF2C6362DE01B + 451B4B4D036734035FF6FFED3FCD343D977B02C04DF230D026ED0B2C350160DA + 21EC02C182C102BD94692BE84B5A07C002BF7AFED7DA4093625BB547715D416A + 02C071CDA2AE7E63DAE1AF8070D48071D6DFD2D84E6A83BF56660FE0FE01DA0A + E24AB247017049EBEDBAEB83DA4030F24872A42D8FE0AABE1BF8DB07306BF27C + 81B64D02E0D66130EA02756BF56AA3B4AD6D01BB11FEA2087DA3EA6B8E75695D + 32B66FAD47B6DA7879106BAE632D654DB66BAABCC35D4BFB0E189B0658BF89F6 + 5BDB65F7AA3DF60C3B69EA425F53A66DDFA1ACDB8EF33E92C650BE3B00CC383E + 56C39AB6DAD481318A6DA847B1AFD4DD5A79777575D58F2F35B5ED8D74EC5FE3 + DCB268DB5575E611F5B0EDFF1CD4851515EE5467513D8B3EBDEF0700AE20705B + DDC38DF3D0FA0B00FEF55507C0C0DE98EF0280BD4CBEE1A4FF1CE45F965E7E09 + 3BBCC550FDA04A7DFC8135B5ED64FFA56AFAD1FF56CA17BD7031E6F87E0ED9DE + 56176C9D14DBF6A6FD2F51CFC7A02ED41798DB93FA36D8DC5EC0AFF215C4AD29 + B6437598DC05185B196FE208813559694D5AB2BC7DA88F939BFA44E7E7AAA771 + 541FD5DEC66C40D91AD06D52A73A81DD4EEA3BC826BB250FFC75E83D344C006B + 13CE46D5E12F7B019BFA0F9B9EE1ED50A536191F62136F8487F0C8D969E8C485 + 69E8D4A539B4302076D8D43470D4EC3478D4AC3468F0D43464F0B434B078E33E + 32DC1E1A5001B682B7D99B37E67B567F1E6A0A748DCA80B98C2F9BA90DFC1601 + 5AFB843104721B1F944ABBA67AD555C70EDEBBE401CD7ECCD1335B6F08D71EE2 + EA651EF40612A69974DC7C9720B003DF0282BD5CE02EB62A84F3247B282EEDBC + CEFA5307DC1D35774D9AB56E6F9ABE6A671A3A63790584511F3B5E5F1E340300 + 16D09527705D11FA36B51D30A5ECF55BBC80A30602654DDD01603C88874C5BD6 + 1100CBC33742E081D356DAF88C6D6D66591B162E4A9B61B3D7B880BF005F2408 + 4C5E5EC0C8EBADAC10D080DC610B08E54C9937E9ADFDFC02861D066F74A92C00 + 8C0DF83B62D16687BE75EFDF610BB7541208561E183C72D9F60281E5050CFC2D + 1E072C0EB12885E7C18A96B7AF16B07C616BCDCE346EDD6EDFE3777481BA805F + 423F2BFCB3202FA924002C08CC221A7BB1A10A02D3C714E1AF2FC46D0016973D + 7F37EC4F93361DF43D81C76F3CE0A0377BF8E23D72D84130B60A0CB377B0D927 + 6EC7B3F7489AB0CDDA6CB5B605F2B26898F366DB8127AF89C54716234B3868A9 + 0E85D9EFD7172C0B0816009EBEEF1907C0E481BF48750E814D005FF6ED9DB8F7 + C93471379EC04F9AED295F409D7A8090D047D38CC34F7A9A61700B00CF3894F7 + F773F8CB226F5904F605DB12023A025FC245926AD117203CDBC3493E97A63F66 + 36F22C383FDE02C0EEF14BB978F47A38CBA75F4BF3F07C7AFA8D0A020389E7E0 + 11FC6C0E8D09E4A5BDF64E448B5E7CD7E59E5405D0225F2C379BE0AE806E2C2B + 0C347660B03CB9C8AB8EFCE2027C33F84579017F4959F41728408B0A0016D805 + 44905728D2C5EC53F9DE176E030047C0EB10F8FD2F3D75AFDE0284BD0DF51F66 + 6F5E80ADFAACF828EFDDBBD8DA2EFEE80BF7D805DA2EFDF84BD7F2007D57937E + 7AC241EF3A135EC1AA5BFFE5D9B4EED899B406006CE9DAE3674B98E7D3EE0D8C + 1730C006AD3E06F0C1FBEF4286BF7802D2B6849D053C0181D79E3A9D5623F61A + 3D71C1CAC0A50B0E7E51DE0BF5A243DD284236039F8050AB8EDBD82780C4D712 + FBD36EBA70CD3D6C73B85DBC215BF077FDD9AB699DF58940D8431F5FCE5EB019 + DC66800B0003C86D670F5B420A072097A15CCB8B3803DE021005F21C0067A048 + 9D8F6D7580C20AF45DC9A18EE5A58A4DFDB65DBF9DB6DFB863B67C3EF208665F + DA0C8001B7D9EB97F3A71EE9983E5611634B828BB4639C0C736DACABB7D3D66B + 77ECB8773DDD42A8E98B2DAF604161DA2A9FBD9CAFFBB9E0098C1813DBD62B37 + 3CF4F30E60EE75C06F0B76EB5CE267F773336D06A09AB652475B4BB7E0257DCD + 3EF7F52B69DB0DFB3CD7ED735DB5BFA1692B50B60065FAB1B7B2F7BF6EFD6FD8 + 319003DAECB98BF0BADE7AD53E1763733CFF3BD9F1CBB9034FD7F15D03D65EBA + ECDF2520AB3C80B75CB964C7B860E775DEC6B96CC7B2F3B3F1365EB2EF03FFB7 + 9C47F83FD6E7A38C1DE0EB80B5688395396FB4E532DF157DA772686EFFCE3930 + 06165FB431F15E061AE7B0D878480393D79E05DA660F603E83BFB460E3AF3FCD + FED9E7D2A6B3F69B3865BF2BFB0D926E30017FF93D6E3C67E7610202E3F14B3F + F6205E73F25C5A71DC7EDFF63BF57D86CF64CFDFB5A76DFC73F67F77F9AE03E0 + 0DF67B5C7D3287865ECDCB1AF63B5E61BFCDE55F9E4B2BED5AC0CB216BF1DCB7 + 3CE097EB0BD7130FE3EE80989470D5F639ECBA010006FE6E3C79C5CB0E85F102 + FEF8A4835CF61106ECAEFC0CF86B020E7FC08B2E761DFBD43EC3318E63D7864F + CFD875F1785A4C586820F30739F433E2859815764D04FEE2F93BFFCD77D3C277 + EC7EF1AE5DD3AD0C00F69774DEF8B00AD58F6279C95B9FA415EF7E9E96BF63D7 + F1E2110CECD51EC08B5FFB382D7DC3C6E75EC0BD02B8FBAAF57BC5EE03AFD8BD + C1CAD8A803FE2E7AE9BDB4E4E5F7ADBE848C2E50D7C1B08DB7C4FA2CB636EC61 + 0F3096C7B043E0E722047ED3ECEFA465762F02F00AF402804981C2405E5295A3 + 07307979083B00B6FBF474AFB3B4C0DFBC1F302F74BD946673DFC7FB97B9C191 + A7D3B4479F2DD13DCC063C3691E7852FE4799B2F20E60D0E7D8F0089092BFDA2 + E7271F7C364DB6BAD5EF1F4F6FFDE16FD333777E6FF7E5F7D3B8FDCFA60907AC + EE20A1A29FF5B9CC843D791E93E73A36E7D995E7503E37F2BCCDC376DA1C6A8F + CD9FF6DBBC681F73A347ABB9186984BECA3BE0DD6AF526E66BD1FB57F3318D31 + 66D35E87C0F21A16FC1500E66542F23E1EF68D7BD2789B5B22CD5901BFA31065 + E69F419ACFAAAC39ED186CC11B98F931F36487BB6BF3BC79EC4AAB63BFE01A04 + 6E02BF6382BA02E016F815D425820FA057E01709060B029317F0A51E1BE1A1B1 + 5127EF5F20B042412301E14A01FC460D07FAD63462DE1A1779C23A4BF2F08DF2 + 10D005E246A82BC5B694EBEDDAA16F4B8497F610D3088F5F20F0F4027E3B00E0 + FED80AF495046B3D5F423F7B18684B079734B6151446E49B003079EF67690577 + 4DB2ABCEC7B3F6D4E111ECE1A0ED39BF0E74EB6B0031A58D5E16AFB7274502BF + 525C9F18D08DFA8F662D25C35C2982DFDEC0DFB63C7BFBFA5A4E86B482B8DAF3 + 37DAD4A6DE36D635B59332CC05F0C6ADC4A6B9BD0E80EB1038DB32F815FC7D64 + C444CB4F4CFD464C72F51D3EB14D4DF03742DD18F6596DA33D4A7D5013D86D5F + 476C29B6E9D44F75AA972D8EF3C860D64F5B9037425AD994F6B7F3F53D7E4B1E + 00BCFFD117D221BB774D9DB934F561FDB9DFA80ABE4608ACF15CC54618E90143 + C6A7FE76FC7E66AFC3D3A6F5EC3A6CAD8E15FACA5EB791F6BC46FE7062DCBA64 + AFB7ED24B5D5B9753ABF873B77DAB5D47760CBD6CE3A5A6589B66DED7184AB9C + E17E5BB5711F18522F8E1BFB44D5595447718CDED86AEA0C80EBE25CC2F9B4F5 + 515D519F62F3F3E773D8DFBFAA2FFD7F6D00DC8DAD4FFF8663751AA749BD6DD7 + 4911CAFE96FA3B0D803BA8FE43EAEE87A5BABABAABEBAD9A7EF4BFB67421CC17 + BDF68B71D385B73B5B6F14FBFD1AEADDD8D485FA026EA30465A3EAF6EEFA545E + BEBD5181BD19E292B71BA1E501BB82BF0306D9A4C352EA1526BA2E4D6634F9E9 + 3AC9F9658A63566373BEA1AE7B4D6A83B57539E0AD29EE01ECB6867E1D152795 + 4C4E0300AE4F565BF63CB98D753E791C3225F51B3A35F51F36CD45BEDF50529B + 9413067AF0943472D2E2B478C3BE34CF1EA887DA83571F1BA3DFF02969883D10 + 0C1939230DB5CF3FC2FA0C2DD017CF5C9FA817294473776A82BFA83F20B648E0 + 5612D015B0AD877D961CD49A629F384E93E2839524BBDA7401C0F563860735A4 + 87BCF840471900EC9EC263A9E3E1AF05817D3FDF9217F01D3275691A3C654905 + 82F1D4C5461D36C1E0B10BD6A7455B8FB8C6CE5F5F015BF7D69D640FBA137399 + 07E93A088E60B7B71A5000B05487C0831AA02F40B80E859B00B020B0F202C003 + A6E2616C6314E02B484C7EF89CB56998290360ABC3CBB740E0087FC90F9DC3DB + F0D9D337025EF20A092DE0DB4982BF02C000DF118BB7B832FC05046FF1D0CF11 + 00CB1378CCF25D69E4D21C0A7AE8A20D0E80872DB6F1D85F6CD996346615DEC0 + 2C36B516A6047FA5316B76A471F63B65818B452DF640D3C29840B020AFD2E8E9 + 5B2D9831C6FA9617B03C2B587063910E2F0DF7E020F5362CD265CF5FE400B878 + FB0280E5014C2A79D944C867423F037F81C002B87911717FE5DD2B612765F132 + 0A1B0B8DEC59377927E1A205831F4B53773F91A6EC7ED2356DEFD32EF213773C + 9E26EF3A9AA6EE79CAC1B0EC53F63D9DA6EE27E4F333967FD6CB8805D4A9FB1F + 4F78FF66F8FB84E7A519879F4AB31F7BCE35F3C83369E6A3CFA659E41D00B388 + 4B39835FC1DFF94FBDEA5EBFF2FE217CE44CCA2C321F7D29CD26CCE4D196B7AF + 00F07454423B6B3F5F3C79173CFB569AFFCC9B0E82E73CF99A7B00E7FD0E0963 + 9961306D550602B358AE857216DB1165F7BA2A70578A1058901751A6DEBD7D8B + 5416005EF6FA479E2E7DC5CAA625AF58F9D58FD392D709094A38D14FD3B2B73F + 4FCBDEFB3C2D7FBFB5472F5AF6617B8867015F950583977FD0EA831C045B5FBC + 752579F7AE2C3098F2E20F3E4F8BADDDB24FCCF6E9F10C7F2D5D53BC7A01BFA4 + 94371C3B9B361D3F9F367E752E6DF8F2AC6B3DD0172FE0CF4FFABEC12BBFC8FB + FCAE3F71DE21300018C0E3F0D7B4F638F0370360F2EC450A685A77D2C6F1F0B3 + 80E05369CDC976009C3D81CFBBAA7D502D05FCB2D72C29A1A0D79CA46DCB1B71 + FD996B1E0A7AE3F90CDE1CB439006B87B5A480DF98B6003030B58055EB0F2004 + 6A46B04A8A80B20ED94C003AC1DC2D5738F6258779C0CB0AFC01F72C2F50898D + BA6D376EA51D376FBB80A302BDC0DF5D77EE5BDB7C2C802590110187B75EB373 + C60BF7AAE02D1054763BD72BD7D3C670DC0C52ED7371EE40606B43DBED37EE5A + BFBB76EC7BF659EF2579002B8DF097BF910070F61CCEE72BF8AB73473BADBCCB + 3E83A0EFAE5B563651E66F25305AE5F95BF9396608AC73E6EFE5BA616D0200DE + 728DBF1F50B715465AE3397805F296F6DB6FE67CD6652B5F4F3BED5C10C701C0 + 6EB808EC05E066F137CA7F6FFB3BDA7781E3E56393BFEC622C522073D5CE52FF + 7FB473D7E710C8A64C1B015FF2A4947D4F6887BC971CE2F2BDD2772E7FC7E863 + C7047ABB17335EC4BCA470C9BD7F1D28F3D24309014D9FFCF283D91DF69E4F9B + ADCEBD80ADBCE9EC452F038101C4C05FF7F83D6DBF45131EF494D79D259CFA19 + 1B1F2FE82B0E7E579FBC9C569DB86C29E5AB69CDA9CBAE95F63B07F83A04B6DF + E74ABB86ACB06BC76A4BD99F7B83D93690DA35816BCA9ACF4F7B28E7ADE76EA4 + A77FF8CFD2815BDFD9B5C2CEC3DA08FA12F219182C017AD7986D032F9B1CB373 + FB821753CEA52D763EDBCF5CB7EB969DCF67E7D3CA8FECBAF2C9D9B4EAD3D376 + 0D3C5101604242736D6D5D4389A460D7E8F788C0F04186BFEFBDEF65016024F8 + EB2FEFBC6DD7E252B7CC520030E1A2F5920FFBBBFB1EEFA50C0CD6FD2243E1FC + 6210F71A4161BD9804DCE58525EE5548F729CF171B8AF057658583561DE32EC5 + 0BB900DEB9CFE470CF94E5F11B434053768FDF12FA7986DD9F1D08037F4B08E8 + FCB256D7881EB38F98EDC8F369F6A3D6E631E06F86BC02C08AF631F9D0D32EAF + 3BFCBC0360D269D67FAAF5D556105E36FB542B4FB239C1F8475F4A134C931F7B + 394D3CF4A2E98534F9F00B699CCD6780BF8481F6F90B2FC1ED27F2C961CB3FE6 + DEC093771FF1F2841D3617DB69F3B73DD903D83D84CBFC4B2FD5017E057D4905 + 84C947F01B25BB7B076FB1F999B5270500CB7B38C260F2449BC92F2132FFCC73 + D8518879689042416B9E1AE7AECA3B445E6FF5BC1829080CF42DF3E3B12687C1 + 612EAD172C2B106CAA5EC6348D0E7900B0802FF017E01B01303680AECA02C048 + ED00BD6DC017AF5FEB23500C0096E4090C001610AED4007FDD23B878F746D5BD + 7FA3E7AFF2B16E102078C6B236B02BD5E1AFDA6153B9DE77D0F41C2EBAFF344B + 67D873E3F4C519F23A040EDEC0D801BF8060690ACFAAF61C0B8405C6963D7F07 + 13AAB9E4EBB62600AC31047323FC15F8456EB363465BBF0285A9A37F0589ED19 + 5FCFFB8A02166DBD91DAB34EC07A4254B57E505BA3E8A4BA07702709020B0ECB + 2E002C39E8AD01DC3AD88DF94E12A4251F21AE6C2DBB9C0BE8D33B00AC721E87 + 720B04A34E9EBE9D00706C1FD7D69A806F1DFEA24E20B749F57675756AD3B53F + E5712942DA2EC0B6D82A7BFF319E1F32725A1A35614E1A61DF83817CFEC1E37D + 5DF911D69663FB0EE3008823C0ED044D1F06A676A7A6F5711DB77EFC3A40AEF7 + 6DEA17F33F473A46FD1CEBF54D755D45BB66D5D9475463FB3F2200EE025E9BDA + 34A8911BC5717E03F51E00F7523D8DC3F148FB59DABF039CAD8936F5764DB64E + 6A6CF76B01E0DE9C4384B2BFA57A0D80DBF49F2E006EFA2135FEB0C29736F689 + EA6B172DA9A9BE3B35FDD87F0BF985AE4AF345B43717E3DEAADE2F8EF56BA8E9 + 5852EBA6445D7B3D00B75F501DE8BA685BF211FCD6D5D627A80BF48DE037D804 + 7FFBD8044500387B00B7F609766F643B96B7FFA300601B6FF0A4B6B1E3F8757B + B3AC7F13A835017F2BD84BBE067FA5A6BE510E91496B134D4D4E95973D2AB7A1 + 8F7D9E36593DA1A4074FAD4070BF215353FF21D34CD33DAC735FCB8F9DBA3CAD + DBF5445AB1F5701A3D7399BFF939C826F60346D06F521A346C6A1A3A6C9AA536 + D916F41DD90AD5AC3737EB2038DAEB752DD943881D6F803DC0E4375967572910 + 36025DD22E7BFFEAA149F53666F41A8E8A80B7FE00A5BD7F07126AD9E4FB0087 + 87393DA87527DAC570D0D834462EB71441B03C7C15EA19B80BF425A52E7A0593 + 07069352263F62A63DB85B7B423F13E699FD8381C5D800C91EEAD9EC754F60C9 + E16EC80390AB7D7F83064D5BE62199EBAA2030654BEBF0370260F275001C4170 + 4EF322832F364CCF36C06F5700BCA67A9B7DD8DC0C7833EC5D67E50C7F058049 + 47CC6F015E0160F282C2B9CEFA07E1252C0D5F48F8E7AC118B363AF41DB964AB + 4B105802FA8E5CB2DD455E1A6675C05F07BE2BB7FBE2923C815924920730FBFE + 0E67512A00E00C84B7587E6B1ABD7A5B1AB37A7B1ABB6E471AB78145303C23AC + DD1A00F036CB67CF60602F8B6C84D9D38259F4A0A82FAC8D6161AD2CDA69010F + CF8DB11B6C8C0A00E3B9415D5684C0E3B7B008C802218B8A8F2679FC4A93F0D8 + DD9961AE3C495A6037035DD95ADE2BAD7D7EBDEDCE83BE98E9DE2B3B0F7BF867 + 206FD65369EAAE275D53761E4D93B63FEE29E5197B9F714DDB5D60F001BC759E + AB44194F9AC9FBEC98FB1F4B530F64109C8537F05369C69167D2ECC79F4F738F + 0278F1F4C5A3B715CE9154F0D717868FBEEC9A6D795F30C663E8E8AB69CE93AF + A7D9D89F34FB513C7FB3F76F5BE867D2E8010CD035CD7BEA7507C080E0B97803 + 973A04FC65019D3099E4DD3398D0CE00DCE7DFF205F7A5AF7F9496B33FE32BEF + 7B5912CC150496ADA90D72CFDE22CA2CF2AF78F363F7FE5DF44A5ED4F77D8081 + C2763CF685C44B6CE5FBC7D2AA8FBE4A2B3FCC801611A279D5C7C7DDBB77D1DB + 9F99B277DA4A6BB7F4BD63EE114C5868F6F1F590CC5607F8A52FE0164F5DBC7C + A923ECB3423A332EED80C28B0A001604467802E3CDABFEDEC7F278F9E29DB7E1 + AB736923E1958F67D0B2F2D31C1A7A95F523FC33F077C3C90B9E27BCEBBAE317 + 3CD4EBF2CF4EA5159F9F76AFBED536C69AAFCE7A9867F6FAC50B1108E520F8F8 + C9B4FAC4A9B4FAE43907BF1900DB783666CE67C00BF0C5F390F0C61E0EDADA12 + 067A039EBC6627FC33E50C80816480DAEC75BBF10290B60581057D81BD59D753 + F6FACDDE960E5F81A9D708C37CB382BD78163B88B57AF6B705345266EC8D1733 + CC05FCB91D107805800C14CE02DA6D613FDC0239690F0CDD75FB6EDA7DE79EC3 + 4840A1435A1B77DBB5DB66FFDA6CB72B582B60BBE3E67DB3032F699F01B0C0F0 + 8E9B771D1E9352DEC23EB80E24699B217006C4D8737BF6F7DD79FD7EDA75F3EB + B4F3C67D07C1A408100CEC05FEA2E805BCE3FA5D6B93952179AEA76E979DE3EE + DB40E5FCB7F4CF7A933EB92CF08B542F708A6DF3950C7981ABDB6FDAFFC54D60 + 7986B0005DBC7AF3DF36FF2D057F73DE3E1B76F71ACE6364017001B736861D8F + BF49F5FF51FEBEADBF4FFE7B0ADCBA97AF9DCFA62B175D82C09BAEE410D09C0B + FBF192EEBC79D3FE26369E8D2BD80F90260FF08D2F0320EC1500360172E9A3EF + 1350180F605E2ED07709F8DBDA0FB8C05F4B69EBED2FE59721D69FB6DFD149FB + DD59BAD5BEAB5BECF701FCDD62F9AD17AE3A04DE04143E63BF31FB6D0282D79F + C9E1D8D79EB2DFE599B326F61EB671EDB783F7EFBAD357D23AF6FFB56BC30AFB + BD2FB3DF3EF0D73D802D05000384C9AF04F47E0500B671ADFD3AAE2576BD20CA + C02ABB4E2CFBF474DA73FD41FAE09FFF9BF4DCEFFE737F69843DC3D71DBF9896 + 7E743C2DC6AB17E80BEC2D9EC16B81C1C51B78D5A767D2CEF3B7D2F33FFCC3F4 + FADFFFA7E9E0CD1FD28A4FCEA6856FDB75F37DA2179C71608CC7B08787FEF02B + BF26726DE4051BF6FD5DF6012FD7D8751A10FCFEFB0E803314FEB4DAAF5D2098 + 3C2FF0A0A5EF7E56D5037C150A1A182C00ACBD81D927781E7B05DB3D2102E0F9 + 2F13452243DC0883559622E09580BC6A4BBDBC7F5BD0D8FABD68B6E7DE488B80 + C205FC120E1AE83BFBA916F4CDDEBE2D108C77305EBFD380B745150436F93DDE + DACCE225ADC778C10B086C36BC811D1ADB9C20F441F20826950DE03BF508E353 + 9FA12FF017FB742B639B64F9F1879E4BE30F3C9726D85C65CAA32F59FB97D224 + 2B8FDF63731E9FCBD8FC65CFD1BC8DC50140F0A30E826778DEE6335666EE347E + BBCDDF76D95CABC061BC8599674DDB93D34905DE028001B94061455B61FE2531 + 5F93473002FE22F25E4799796711E0573058DEC0E33632FFCCF35722CDF80B87 + 45B4F1B92790983929E394B96A9CD32A3F9EBA0080D91F98B69ADB562F4D16D1 + A67D6E6DAA416054790697D0CD19FEB6C35E6CAA17D48D4038C260CA005F441E + 9B43E045EBAA7D8023048E60B8B2037C8B27F0F085050A07D05B97EA047DEBF0 + 77680D12F34C2509FC4A0E766710F5C99E0BF59C6669DCF7576DC93B009E9AE1 + 2E7BFE7ADFB227301EC1831C10DBF3278036BC70CC7368CC67D9B3AA8DD36F8A + 3D139B80C482BE805E4970B782BF4062932030221FA130A9407004C4E481BEAA + 73086CCFF37AB68F00D83D8301C4BD95AF0B6400AC34E649517DBDA2A3C6CEEE + 02777B2379FEB68160CA05D256B0B7B4E9336ABAEB9191D31AD507016BCB9EBD + DDC1DB9CEF0C80750EEA13A5BA96B06700ACB5B3A89EE06FFF9153DBDAB086A6 + F5B8D8BE7DEDADACCF156FDD7E21FC33EB874D60B83BB5AF5FD634687C627DB5 + D51EFBB84A02B482B4B11C6D78027B79405E7BA68C57703F1B4FED5CD447C9CE + 38A69EA029751110D7DBF66EDDBBBD6DAC6F52ACEBD4B73BA96D5453BBBA7AD3 + BEB763B5D6FA5BAA330FA95E1FFB44FB1F4D82C04D75DDA80B3B12A3EAA0BE0D + E5BAAD5105FAF60480BDAEC15E5775AE8C17CE9B7C5F3E0BF9CA6663F61F6E22 + 3535B1BF4E7A18705BD45B50DC510F79BC2E8A50F6B7D45F00704BFA0175525B + 7BFD001ABEEC6ADBA56FC38FF74FA9F60B5EEB221A2FC6F50BB3CABD91FA348D + F34B148F11C7AC97EB37B2CADEBFAB5417A59BA18F53A0AB83D781BD03C051B1 + 9DF2405DE0737F40EF401B1B5BF1F6F563581E00ECFDFA112EA48485061EDB84 + A671A263EAD584A8A3AC5F01B211B222F788B509265034DA23888D7687B80D76 + 95DDE6619B830697BE0D6DBB93FAD7279871D25987C4A87D624B981B4B874D35 + E1E93B23F51F36D33560B83D1C9474E088D99E0E193D2F8D9AB8248D9C680F57 + 80D4E1D67EC4CCD4C7529F8433CE7026D5367967A23ED2C6B27A87C13659AF43 + 5E79F70AF2468FDFAE36F27AB3B4BD6D3F87C3C0DA0C67FD0167346FBBB61E8C + FA958723A06E3FEA6AEA6BFD25CA8C556F93C7B7073C4B25EC7D6C5CC1DA0ADA + 967ADA2BA47394BC79DDA3D7DA0BF42A2F693C17F505F0CABB57D05765DFD3D7 + 441EA00BE4F57183A807FC0AFE028711791FD3EC82BCA809000BFE12527AC8F4 + 65D583F7C0295983A614085C815B20700ED59C53DEFACEA9F252DDBBB7AEAE00 + 7865CB2BB8ECF34B59D202858735ABBDE98EC76F0EEDBC3E0D99BB310D9EB3D1 + D24D5666BFAC2C015ED2BA5D12F0AD836007C08B3304CEC0377B020B08475570 + 7889D52FCB626149792D308D5A61F9952D8D42ABAC9DA5235828B2540078CC1A + BC81B7A7B16B7738041EBDAE057FC75076186C6DF0005EB73B8D5B9BBD20B420 + A645342408EC79130B6E5AB4AB16E336EE4963D8D397BD8009096D7511127B1F + DA5A7ECC26F68803E41E4EA3375A3B4B81C182C213B73D9A266CCD02D04EDAFE + 98D7E1C98BB49FEF641623F1FC35F93E752687C000DFDD79BF3BF6F045B9FDE3 + 691A0BA0BB58C87CA2CA6B6173FADE275C33F61DF545D169611F5FD229079E4A + 93AC8E0554A4BD7E3DA4E3E1673D44E30CBC7C8AE74F4E01BB59DAF37786A51E + C2B9282FE4B66C1EB6F9C9D7D2ACA3AF3ADC05F24EB77190802F2972E8FBF4EB + 098F5EDFC3B728867896C7EFDC674DCF67E0EBFBFE92BE9417C801C0B3A8B714 + F0BB14706B298BF1C0DB0878A53AF0450AEDA93ED4CBB69485FFB748ADFCD687 + 69F19B768C37AC8FA5793FC98FD3D2773F49CB3FF834ADFCF08BB4EAA32FD39A + 4F4E38C05DF16186BF4BDEFDC20130F9E51F007FBF341D772D7BFFABB4E6D333 + 1EFE14C8AB10A8A4CF1D7F3FBD7DFACDF4C6F157D3EB5FBD92DE3CF99A955F4F + 6F9D7835BD71E2B5F4F457EF38F0C5EB572098B0CD78F0026781BAF2185E6976 + C0F0AA2F4E39ACD97CE66ADA7AEE7ADA78FA8A839CE59F11F23903E08DA7092F + 7B21ADFEEA4C5AF56506BEEE054CB867BC834FE16D7821AD61EF5E6BB3F6C4B9 + B4C1DA6B3F520F417B8A7D45F33EBE08E00BC8657F5FA02E36423E6FBE70DD6C + 57D2968BA4D71C0E13261A18BCF9C20D17FDD6D9B8EE5D5B3C51057073D8E6EC + B1BBE922301665580B5CD33EAC8288A4AADF7CD96C652C3C325B2018E899F7CA + 25CD5EB2D7F3B12FDD70E1B5BCFDAAE501D2172EA72D97AE7A19B807F444EC81 + AB7C0EED6CED6D0CC02A30577012E88BB7EECE5B5FA7DD26BC6E69ABF6F4DD75 + F35E25EA1963F72DE06E2E6B7CCE9332763FCE35BC73EFA7DD77BE49BB6E3F30 + 7B86C182BF5B2FD9B95DB1E35F4676CED76CDC1B761EA65DD7EFDB67CAA038CB + 8E6365C6DF7DEB5EDA7DDBCEE3261EC2D4F177B3F375500B3805D6B6CAFABBB0 + 776FDE7337878F16642575C06ADA758B762DF0ABD4C7B0BF29505DDED59EBF05 + F0E5FF9517043268E77CF477D2DF2796F9BF613C3F3E9EBED7083F7DC9FE1FF8 + 5ED877CFE4A1A5AFDA77F6D205DF1B78C7B5EBF6B9ED98F619B6E0A17BDEBE93 + 005CFBBF8FDF31C615601614D6778E947A3EB3423D0BFEF2DD650F637900D33E + 7F4FC3771A00CCCB0F672FA70D672EA52DE7AEA4ADF6BBD94C686B2B936EB5DF + C2D67336367B0597DFE466CF5FB2DF29FB065FF2DF2CBFE54D67EDF88458B774 + 938DB9C5FAD296DF37BF6D7EE3EAB3DE523CF7D778DF33B9DE5F1239ED29D79D + B5D8BFB4FAE397D3D68B77D363DFFD553A70F787B41EEFE26376BDA0CEC4F506 + E1098C80C0A4CB3E3E95967F6CD79C2F2EA57DD7BE4DEFFFEDFF367DF65FFC37 + E9C8D77F488B3F3D99E6BFF7857B17B31FB9EF3D0C6C06FEDAF5D223237C9843 + 41AFB0EBE1AA8F3FB76BEF8779FFDF77DE7500BCFC43BB4E1326FFFD1C852187 + E7CF7B0757E1F84D8BDFC9FB04CB2B9854C038868B9E07047EEDDDB4F075BBA7 + 142DB0325AF85ADE379EB0CF0B5FB17360CFE057EDDEC1FD87ED0B6A9EBEFE32 + 1340171BF72ACB0B0A938F6DBD8C3730E3D83D51F752D2EADE59F2BA9F52CFFD + 97E81B51314A07F2FB3BF775BFF7B3D5032F7B11D1C36C00DF0280EBA2CE5FFC + 2ADEC53379A10CAF639B53685FE0FC5219FB02DBDCA478094F39F46C9AB8FF69 + 0F0DDDDA3F38472F99B8FB681ACFCB753617E285355E6223CCF394FD05F29AF2 + 7EC078033FEADEBFDA2398947912628E252F5FBD8047AABC5EC69BB823BFC497 + E1EF418FF492B5CF6C190613127A347347E06C994B4A798E69751B77A5D11B76 + 7A3ACEDA8FB33963D776F94544A0F198DADC35E6F562A3035F6B275B8C7C83FC + 25C83237060C47085C79040B0A576A9F874B234CC397B7BC812300A62CC8EBF5 + CBB200BD2E6B2318EC7D0A00764F5F1379B555D93D83E7ADB6E71B7B0EB25410 + 5850587568E06C7B1633A9AD4479D01C7B662BF5553B9EA9F40C159FAF4A5960 + 5710382AEE09ACB6D8058007D973E5487B8E1B6ECF750E75ED7952CFA8BCA83C + 901795CDAEE7CF6133ED583C835A5963A33C5E86C0A40A211D4170056F81BCC0 + 5F3C840BEC8DC2E66098E7E089F6FC5CD3A049D62E941D0217E0DBF4C2370098 + 367555E0B8805F859156286989310481230CEED446757EEC9A54DFD2EC36E01B + F351D12ED05B6F239B207053FD23A8405AD67748EB00B80E7233086EB24FEBD2 + 577575F9B9D93879FDAB5DACFDC535B5EE14D7D4EA7058EB6E311FCBA44DEB74 + D8058ABB93D63E3BD95BEB9CE393878006FE0E19EFAA8030761336DFD7D7247B + 9FD286B44F6857D9E3380D8A4019301CD79B058BE3DA73AC975D79AD6937B5FB + 2DA573ABABA92DEAA98DD6EDA354D7DDB8BD51D7B1BBF28F68EBCEDE24B5ED6D + FB3F9A028F72C05A24D8DB496DFDA2C4BAC25895ADD6AEAA8B76856F8EF9A2E8 + 99DBA7EFB03460C0A83468D098D40F8FDF3EC3ECBCB23DF6E95675E01BCBF5BA + DF4ABFF41811CAFE96FA0B006E2902DB2675E9A32FBABEEC258D6DDBFA36FD50 + FF846ABF78A1CE17D07AB93BA96DBC7837D5FF1CC571EAE3D5CB75BBEA5AE037 + 786887BF454BA53F1A98BD6F5DC5AB5740B7BEB7AFE45EBB6617E825559E10CF + FD81BEA59E94311C0E6B7C3C7F399ED907DB046CCCB87969A84D081D10A30E90 + B7EB64E76164FD6AF0957C5F4B878C9E9D868F9B9F060123874CE9D24E6D2BD5 + 00705D8CA17D77DB20F02F50DB5EBE4C5E6B13CDBAD4A64D66639FDF7E4366A5 + 7EC3EC6160A8A5436757005810B89F951F1960C71D68C71B322DF51B6E9375E0 + 2E136DDEA8D4641E8DB4BA028869E3E0D7FE8E4D0058B67A5910382B3F18B414 + EA4758BF91B3D380D173D38002655BD0367BEDCA5B37BF01DB02B852EC93FB75 + B549F5BE405AF7CA0D525D1300960D301BA16FBD5C97C06F1DE646E8AB1439E4 + 25950AF08DE2817AE8B465AD87EB007F057933D8CD6552A9550F005E66F9ECBD + 3B708AD94CED0018D80BFC6D69D0F455AEBA6DF08CD545D4D7A16F06C319EE6A + A121805FD3E0E2FD8B17B0162786CF599546B00832CFEC73C3E2852902E02173 + 373B008EB057305721D45818A1BE0E7BA3EA0058FBFF6A0FE00883639E7D7F47 + 2DDF9E416F81BFEEF55B00B07B033B00DE9CC6AEDE9AC6017257E3FD9B0130AA + 3C7F0BFC954617F09B3D7FCD5600F068AB6371CBF7425B5D16BCDCE3220BAF89 + E845C19E6C134C71F14D0B70A40AC9C7421C5E1C7888C8A3436DC6E339B2BDB5 + DFEFA80D36C6663C7D33049EB2EB09F7C89DBCE349D7949D4F95FC1369F2AEC7 + D3A4DD78A53CE97BF8924A02BC8885CE09364E4C59F47400BCF7890AF6CE3AF0 + 54251636A7EF7D2CCDD8F7789A7DF0C934EBD0D3D933E6C0D116043ED45A4C95 + 0811ADFDF9940AF80283B55FDFCCC7F1DA7D35CD788C342F0C474D7F542920F8 + 15DFC3D7BD7D0BE8F5C5E5027C111018C5856AC40276DEDFF08DCA030AC9F309 + 1B756D8BE0563F873E26015B015C815DC15C79F35226E4731D02D3176F5ED2D8 + 87F262F67E7CF3830C7FDFB1F45D6B4B18514B292F7CDBC630B1B7242018F0B0 + FAE3ECA50BEC05FA666FDF63BE97E59277BF34DB89B4FCFDD3AE559F9C4D2F9E + FA389D3DFB5CFA0757F6A7FFCBBD8DE97FFD6151FAFFFD6E6EAF457BFAFD836B + FBD3AD4B4FA6E74EBDEF2018F88B07F01A423C7F7536B127A720CC865300A21B + 69E3D96B89B0CB2BBFBA90561C3B6575EC237AC1A10F900800B4EEE4F9B41178 + 7B364326EAB051A77A6C52064578F902732F38FC15008E5EBDA45B2EDE74F00B + 0046D9139810D1971C160380379CB992D69FC55BF29AC33D2027B0330260C2E3 + 02C604CC50CE67C846CA9EAA2D7BF6EE15A4CC76EB57C221A32D976EA76D5772 + C864CA9E5EBAE5E70400DE790D200A0CBC6AEDAEA75D376EA7DD37EFB8008C02 + C00E2EAF034AF1B2CD80B92A03686FDE77016977DFFE26ED64BF5E6B2708BCED + CAADB4CBDAED2A9EB902997B6E7F5D4161A4F1C9530720CEA1A0ADFE968D7DE7 + DBB4EBCE033BC6031BE39E7F961D57EFDAB8F72CBD9DB6DB712200DE8DB7F035 + 3BB74A5FE7BF4181AB2EBC924B7ED74D3BBF9B2DF89D012B50F776DA73E75EDA + 7BF7BE878BA60DA017009CE16786A2B4DB7DDB3E179ED3E56FA771F87F22AF90 + D3B93E43DD6D00D9CB7C2F00E682D5D6CF3E4B0E7F9D01B5FF2DED6FA46331BE + E0B3202C797D67B65FBD62FF17D7EC73DB77E5F22513366B8FD7EFC54BF6FF7E + D9FFDF790980EF00A99701BE7CA70A0026C5C6F8F1783A6686BF7C3F5BDF5F1F + C3FA911758CEFB055F706F78BCE6B7D8EF63CB393B4E056FAD2D1EC00060DA9C + 0308DB79DBEF621BBF1DA0B1FD36736ABFA793D6C77EFF5BEC37B989DFD7990B + 96028B6983672FD78C539E6E3CC535C07EE7D646BF7DF60B1700E6FA42D4800D + 78F51312FACC8DB4F62421DCEDF35FBC65BFEB6B7EDDC10B986B0FD7218F2670 + EC5CDE07F80BBBD61CB3718FD9F85F5D36D96739733B3DF1E0AFD3D1EFFFA1EF + 45BC8008075F9CB6EBD499B4EC5320F0A9EA6519DFD7DCAE750E71DFFBDC5FC6 + 59FD099EC01FF9B5D95500F02AC2E57F78CCA12FF0973E88B240702700ACB2BC + 84330CFEC0F4BEBF182408EC2F09997D99DD3F96BD46640A5E30B2730004731F + 2AF736F2D13B18F8CB3D48F72A5E6EE21EC79EC102C6A4BA172E32293286A02F + C057D23D963C75BA0F2B9DA1FBB4DDDF63A48E9CB77BF5D1D7D3ECC75E75AF5D + 202F5EBE0E8001BC65BB8739781AE371FC24D13F80CA2FD87CE0F92A62886F25 + 61739119879EF570D2330F1399C4E6223687F1B989A568F2FE3C77212A09B077 + F25EE6428FF93CC85F5C331BF5936CCE8380C0481EBF02BFC06024182CBBEADC + D60100CB0B18584C741720B0349EB9A1DB0FA4319BF3DC501EBD9A3F4EA09DD5 + 8DDBBC278DDD64F3CA4DBBAC5D06C582BDE425CD3FC9F3B2A1E6B192E02E6A82 + BF110093F7ED51D65B5F9B1FE30D0CE48D20B80E85872FB7F93DDEBA2640B060 + 30E9B0651B6D3E9FF7F6E5E5CD0AF85A5936CF17F83B72E9C69C2F0058C09854 + 0098342A7A030F617FE00273E5092C00AC671904E4451100ABBE09000B02CB0B + 58F0577B08EBB90B09F00265490585F3CBBB191247700BC81D6ECF7A432CED37 + C19E99272D4C43ECD974F8F4E569C40CB3979795498715003C1880ECA1A25B02 + 26037FFB4E5E504160A503A7F1FC9AE530B800602F4FB636805893E02F69E5F1 + 1B406F5D02C182BF5175000CE0ADC35F1D47B6CA03B88831DAC63145C88BEAF0 + 37D6D76DB2D7D55DA8E83AC48DEAD496B52124BBBC8F63DFBED4B13F7181C002 + B8515AFF69B2C9EE0078D894B6364D527B41DF2807C0E487B703DE2EEB69B5FA + 26CF60B5ABF7533E0260D9D55F40579EC27569EDB3C9D6BE26DA02B502BD7578 + 4BB9DF30FA75B5BBAD00E058D7AD80C583ACBFA9F208D6FA7293CA9A75DBDAB5 + F5A9C3D4265BAC8BF99E14FB3649E7D1A4FAF976D7B6494DFD744E0F3B9614C7 + 6CA99D7F4426F2306A67053F7F9C5FA28EC72EFC497CAA3B00DCAF4F2B5FF5AB + AB8C13C7AA6CB5366D00B8067BEB02FCF61B382AF51D60C7B7B6405FD4BFFF48 + 07C1CA5387E7EF2FF6CCFD8F4511CAFE96FA0B006E96C06D1BC0AD2B7ED183EA + 7DD5DFD3FA0FF54FA8F68B17EA7C01AD97BB53ECDFD4A75EFF30AA8F15C78BF9 + A8D8367AFDF60C8091F5470D003842E0871190575EBFF2067688DCD78E632965 + D561A33CCA26A153ECC1603493E0210056AB6B03B72D759DECF45619D866D9C4 + 6BB04D1A074FF632B076F8B879698C3D680CC573D4268411E44A6AEFEA0600AB + 4DBDDC450D90B7B772105C2695D504B23EA16C934D1C119365F6FB1D6213F6A1 + 36291F6A137034C426D5436CB2ECA1A1D91B180FE1AC81236C023FDCDA0FB349 + F4F0027781F54CF447028573E86707B4A37268655487C048650160D955C6E337 + 4EF2B35AFDFB0EB771399791F63033CA1E6A46729C2C857106FEF6160077B223 + C6579DF6EA6D02C088FAA838AE00B0FAA38701C011FC4A11FCAA0CF81D1C606F + 93047F058509031D21B004EC45ED00D81EEC81BD260160016179F76665003C60 + 2AFBF502750BFC350D986EF622CAC0DF413351F6EA75901BA16EC90F9965F52C + 205400784D1A5AF6FA1D6AE5D622C48A347C6E3B001E321BF89BBD7FB3F2FEBE + 43E76D4AC3E66F4EECDFABD0CF2D006CF5C5E33742DF3A0076E81B5281DFA184 + 930E00B82E00F0C865DB5CD1F3374260B72FCFA1E404804947AFB676AB4C0100 + D7E59018D50070F670C80B5CEC152CAF5F16C694EF12468F7281B9BED056CAB2 + B130470AF8F53DE10A001EBBC9EA36EFF5853F2D06E2853BEFC80B6936A10977 + 3CEAF6ECE9FB789ABC83F0CC1900139ED9BD75F73C51C442E4E30E81671E7CC6 + 457EEABE277D81337BFE1E6D137BDFF9E2E7DEA315409E75E8D934F7F0B376FC + A7B3D76FF1FCA50C006661B5E5E5FB9CC35D0FBF88B78D9DB3D409000BFE4EA3 + CE3EA7602F8BC140DE6947ACCDE3AFA6D9475F37DB6B9E9FFE2870382F18CF2C + D05762A1590BD3805FC160CABE388D77D38BD9A349003882DEFA823762317CB6 + B54302C011E0226CA4805C3C7951159EB3D40B00AB0DFD2BF0FB6A06CA0B5E65 + 51FF83B40800FCF64769D97B9FA6E5EF7FE629DEBFD825EA11FB007B28E78F4E + B8E409BCE243CAA7D28ECF4FA4F3E75E4CFFFAC69E8786BDBDD5FFFAC3C2F4AF + 6EEC4CE72F3C9F761E3F9ED61D3FE7A19C1D027F8197DE79DF939390AF847E05 + B01056B6F2FA2545801E8030D0082F40CB23C161074196C7B60E3BF5672EBB77 + 6F6B2FDF2B0E7E81BE94057E058301BD82BDD849E5294C1BCA1BCF5F75C82B01 + 7E05839160B0CB3D8105DD5A202DC3B4AC0CDE727DD5A61AE75A52C86320A2C2 + 273B04BE8CB2576E8688C58BF4DA35079A40E03DB732C414C874106AED2300A6 + ECBA712FEDBA0D9CCDC20B58E19EB7E1215CDA037CE5D5AB7C130026D4F1CE5B + 5677F741DA7DD7EA6DBCED57EF99EE5BFB7C8CBD77BF4B7BEF7C6BB60C7FF7DC + BCEFF0570018CF5F5780BFBB6E3CB0CFF5ADC9C6BDC5DFE38E035741D59DF639 + F6DCF9BAFADCDA1798CF0FD405FE22F2D8F99B6FBB0670CD61A225DA0BF222DA + 6B2C528D5DFD5D5D192413FA99FFA7DD371FD839717E19E6E305CDFF2BDF9B2D + 97F134D63118237B224B8069FD7F6EBB72D9FA9AD89FB70060A4EF0FC7DE61C7 + E4B3E41708B287AF424FF37FC19EC3D90B38035FC6D6F82DF89BA1AFE02F63C8 + 833ADA0480375CB898B6D87775FB253B86FD6E37E1490FF8B5636DE5F760DF7F + BCD2379EBFE4E936F63DBE74D57EE397ECF7441D1EF7D6FEC2F5F24287D9CFDA + B5E0CCF9B4E6C419FB2D9F7318BCE174DEDF3BEFED9DEDC05F7EEFD54B1FFCEE + EDDA42D8F88DA72E9AB83EF0B2C7151BCBAE27F67BDFE49106AE3A0066DF700F + 054D38E9AFCEE71752D85BFC4BEB771C286D9FE5B45D2BBEBA9C567DCEDEE317 + D36AB32FFDFC745A0A68B66BCA4A1B6709A1F53FB1EBE92727FD5AAB3DD1F1DE + F510FA1F13F900C8FB495AFAFEA7391CF4FB769D7FEF236FBFEC83DC5661F7DD + EBB7ECC92EB9777081BE0A130DF855A86800305A6CED16BE9921700580DF0408 + DB3D817B8FDF573EB47B8ED517D02B017E01C0EE155CEE47DC9BF4A292CA73EC + FE172130F74360F1A2B2B7B0006F04BEA4B2F19295BF68556C481098FB342F6D + CD3ECA7DFDD5027FF3FD5CF7F8198FBD98A6E3C98BA72FE0B7849726E4F4BC67 + 79A18BFB3A30F9C5349590D0266F5BB69190F72F305836E06FAC170C269D7AF0 + A92A8A09F3198F5852C030D0172F607FD1CDCAAE7D3687DA75A46C9F71A402C2 + F210269DBEFF680582BDAEC05F415F52207006C1F9653F6DF321004C3B01620F + 0F5D40307200BC15D99C738BCD3337DB3C15150F6001600163F2A3D6EDEA0280 + 99B7C6392D5017915759F0D7BD7E0B00969D39B2C2462B1C74543B00CE9EBEC0 + DEA180DA028265AB7BFCC67C04C2A415EC2D1204F634405F79FE027F058081C7 + EE291CBC7EEBF290D0E1C55679FC9276A70A18F37C55E0AF14E1AFCA82BE319F + 5FDE6DC161B723CBC7A852432D3FCC9E21874DCFF267547BC6E5B97630CFAE3C + 935ABB1C3E7A7100C019F84A94250030DEBEF2F88D21A02300AEECB4A10CA405 + F016482B5B27801B05C095376F9F71732A08EC60D7EAD55F63348E55007014C0 + B609ECF64675F02B0180FB8FCDA036425BE501B682BAE46543B41B306EB62B42 + 5EF5ADF7911D00ECE58704C04DF6BAF7B0BC82A568ABC35FD49F6DCC4ABE3BA0 + 4B1AEBC9C7328AEDEBFDEBB67A7BC03050B73700388693EEBA26CAFAE9381730 + 579EBE11F692EF1600932FEA2D0816006E84C0AC512BAF72106BD1A80E6DEBA1 + A2EBEAD4AF496ADB939ADAD6CF379E736FD45DBF871D2B2A8E1BA535FE2636D2 + 5B4556F047DD33B8A8E367108F12A30AACAA5FBF510F0F805119A30DFEA28636 + 551970DBC0E9047201BF00607901F72FD0F79147863AF81D38707406C07801D7 + C6F88F45FA6CFACCBD5284B2BFA57A0D80FB017D4D7F470030AA03DC2E0A5FF6 + D8B6531FB7D57FA87F42B55DBCFCA2D8F9E2592FFF1A8AC7E9AD7ECE38B16D84 + BE02C14D7DB2F2D80E7B3B00604970D7616E437D93D8FB37867CC6F3D73D8407 + 4E70C8DB9F3D7801C4563F74F4CC34C626BFC36D12C9A4E6B700C0DEAF40E03A + AC25C5F317F83B80BD4680B3036DA235A4EC915B54C15CE06F01C0F5F124D9D5 + A6B16DF0E8ED4E0F03809B55E0AFCBCA436DC20BF005FC0275290F252C749600 + F080E184882EE9709BAC17F51D619377FB7B45C0ABBD7F1DFE127A19981BEA05 + 7995B2A7EF404068694B8ABA7AFFA2DCDF8F3BA2057CA35A219D5BCA30D81E9C + 028C4511F2D66D1A47523DE33898B57306000B022BA54E1EBF713CF2D103D8C1 + AE49E54E8AF057B0576A03C1A6D8CEBD7A2DD55BD302BF7A908E8AA19EB50FB0 + 802F0FDAF9C19C320FCB3CA0037F3304D6DEBD8381BFA66ACFDFA900E0ECE10B + FC1D880AF0AD6B205EBF0E7F0B000ED0D7C16FE5E56B750E7E01C0E4B1017F33 + 0006F0E63A161996B71623900360C482C79A34689E69EE3A530B006B0F604930 + 58F9118B36A79178EF16502C51561DA9D70745D82BC9461868BC7F0580057DA3 + 2A00BC126F81CD69CCAA2DA6AD693460178F820E00189B03E0D50060AB5FBB3D + B19F9AEF0DBC76571ABF3E2F8CB1E825E82BCF0895A3C696053781DEBA64C7FB + 17002C2FE07140600FFD973D40F0FA9D73E8B9B4E5CDCFD2BA973F4833F73F5D + 16060FA51CEAB9B5376F86BF39F473F6F27D34877BC6EB64AFB5310186057FD1 + E4BD4FB9F0D09DB2EFD990CF8BA0BE10EA1018780C14CEDE2F00E099073358A6 + 4D6E6F6D0A0006E4CEC2ABA7005E79F992524F8A048109F948F8478578AEC242 + 0286694B9BB0503CF331AB2FA19E7D31B9C1CB973CB66857784B015EF2784551 + 66615C8BE3EE1D451B53E51155D2088081B802BF2CA02316D005800578B5D0AE + C57501608D21615FF446CB0B0C0F5FA02F8AE19FB3CDDABDF3415AFC169038EF + E78B0730F09714BD75E28DF45FDCDCDB086C7F6BFD3777B7A72FCEBF9E567E7E + C601302078FD894B69C3A92B69E3D9EB69DB95BB69FB95DB69E3B92B69E55767 + D28A2F4F57A920F09A93E7DD464A2858D9625900188F3F2010A19C1913EF5EDA + D07FFD994B69EBA59BEE7D0C1C06F60AF40258E5654B9DEA3751B671D601AA4C + 78FD026C05531D7E5EB6312F64D0B7E952F62A4502BCC8A1DD0D6B6B79813520 + ABC6C810D8DA5F06BE125699F1337095A72B00913132C4CC306FCB952B5E16A8 + 9428336E3EBF1CD259E3ECBA75DF812FA199057F3D0CF41D6CF71C026BDF5FA0 + 2EF2BD7D4B79D7EDFB6DE51C56DACECDCAEC3FBCFB4E09FD7CF3817BEFEEBDF3 + 5DDA8D27F0CD6FD23ECBEFB8666D0A008E1EC002C078077B6868FB6EECBCFE75 + DA73EBBBB4F7F637368EE56FE750D5FABB6DB7FEEC0D1C01307F6B20A6FEE612 + C0367BFD5EB73E19EAF2371598557BD9103652807056AB4E61A309470D000674 + F37F26000C30E76FCF77867D9EE9AF7E82B282BF2D004C686680EEA5B4F9E205 + FFFF6DC1DB02ADEDF3F3D9F93F657F61602FD03787F5B6BF8BEF356CF526CEBF + D53FC35FC6CCDF9BF69711F43743FEFD2C3642966FBC78C9B5C5BEDFFC86B69C + 37BBFD76BC5CC2916BCF5F0F4D4D48E9F397D2DAD3E71CF0BA87F295AB0E9137 + DAEF84173B36E1396CFD379CBF9856037C09E7CE7EC336460B02674F7FBDF8C1 + 0B1F849B063C6FB2DF3B7B11A38D27ADEFA94B25E4336D2F58BD8D7DEA725A69 + D71D802FE097F23A2204581B5E4C597FDCAE1F5FDAF5E4981DEFAB8BAE35C7EC + 7AF13921A3B9EE9C4B2B8F5BFEE4C5B4C2C6C00378251EC49F9ECA109730F79F + 1CF7D0FBEC75BEFA53BC7CF1ECC5C3F7D3B4E2A3CFAC9CF37E6D2EE0D7C3E39B + 220056586879F9C67D82058049755F40E409093DFF75BBBFBCFE615AC2BDE4CD + 96B8F7C47B13F056F7B6EA3EE7F7A3F7D33CAB9BCB4B51D64EF7ABB9DCCF80BE + DCD3AC9E14018EB53FBE202FF037E6EBE5FAFD58F7E43926F70A2EF77AF60166 + 5B87B94FD97DF9E937D25CDACADBD734F7E957D33C00B089BC7BFF3EFE42B51F + B0F60606F0027E09052DE02BC9EE5EC256CEF0F769CB3FE79EC2C8E16F030076 + EF5F2B63272F002CE0CBDC883998CACC93F29CEC489A12BC7F01BF0A0F1D01F0 + F8AD877DCE97E77E847FDEE7D037EE0D2C08CCCB84E3D926C4DA44004C8A47F0 + D85A086824EFDF6A6E6AD29C751C5B92B8F2BC561204D6FCB7AE36385CDB0FB8 + 130096E7AFBC7FD170CB038023F48DE017D88BC80379958F10586DEA1058DEC2 + 02C188BC0360AB8F9EBFCA0BFE4AD880BB78F75680D7CAF2081620561ABD80A3 + 047F81BB310FE055DEEBEC598CE7B30178FD168F60D2A8A1563F8C2D7EECB953 + 2F200B0CFB736CC90FB0E75387BF3696CB6C19066745F88B0805CD736C6F00B0 + 14E16C4F6A83B64102BE11FE4AD477EA5FB509E0174FDF7A58E79E006FBDBE49 + B493077084BE75016BE5D55B07C58326CC4D8327CE7308ACB6B14F233C66AC91 + A60280A508727BA308799BEC5111FA76521DE8A2269BEC4D21A09BD418F6B9F4 + 51DA1D00D6BA276AB2A1D67A6706B80E6587D4423D077B1D00B7C1DF9077F5E4 + 113C3003DF2E0018F93A73D7F5EDD65A74561DD8A29E00F0C3A87EBC8751FDDC + 9BCEBF3BA97D53BF871DAB2E9D8FD4AAA3DCCC477AA37686D2DCE6B754C7636B + 3FE1060180A508809BDA76541DFC76520700DC457D872500AF0361806F9F1CFA + 59001820EC6D1E16A4FE19E82F00F88F29FFE2D90FA0FE45FC99EA0473A3A877 + 2FD2D2B64B9F028ADD56FFA1FE09D57EF142ED174D54BF60AADCA3FCB3369735 + 8EC67C58556386F1B879C572A7F682BFFC5F3401E07813D44DD1E1ECA00915A8 + EDA80878EB65D982AAF14A1DE5BE9AACD8E465C0509B40399035BBA5FD6DD2D3 + 9F3D65DB26325DD575B2D3B3D4A70F7B0E07001B81ACF20E760758BF528E10B8 + 0F10B707F04B7B548D475BF6FE454D10F721E57D19AB164A0631118D9351A5B1 + 8D4F74876400DC0F0FDF11847ECEA057B057A1A0A31D68DC07383EC2E46906BE + 0E78AD1D721BA0362802609549FB8F9EED101865CF5F4160F276CCD1B4B1870C + CBC77D80EB80B6AB682301829B416F94EC5DC7CAFDD5D781AF9D6F27001CBD7F + E3D8DD86802E691450B70DF206E81B21307DB5B7B06CAA77C85B52D9629D7B00 + 0386EDE119E02B00EC0FDB05FEE687F0F200EE6F500384EDC17D2AE93207C083 + 8AD76F0EFBDCF2FE95064D5BD5C5F3B72BFCCD0078D04C202F0B052D659B840D + F8BBA6887C6C9F2130F237D5B507B089458CC1A4F3D7A6C1F3D69B36A6A1F336 + A761F3B7240F095D5407C140DD51C05A53F4144602C0920030809754D0B7BEF7 + AFC3DFA5D9FB9730D0D1E3372A7BFF865071252F083C6A555E941AB9927C06C1 + 82BF9E270474F1FE05008F5B87EA00D8CA1BF3C2996CE4276EDE9FC33FE3E5AB + 0536DA74500EDF973D80058009FDCCC2E0E49D84077CCC41EFF4BD4FA6352FBE + 97563CFB96435EBC7FA7ED3E6AF927D3941D47AD5D0E078DB76F5E80CC219EEB + 8B930060A02F210ED9EF176F5F0FCD7CE0F934E3D08B963E97A6EE236CE273EE + 1D236F183C8181BD82BF80E00C7F9FF2108BF2989976E0592B3F9F41EF6319EC + 460F5FDFD7AF40E12840B0FA4802C0A44DA1223D2DF0377AFDB2D03CFF992C2D + 3ACB1E01309E4D2ECB03827D51FC95EC15B5E4E5F75D2C7A037DD17C16C0AD8E + 5436415C16DA5930A74EEDA873AFDE202DACAB7ED99B9FB880C0CAD38EC57B2D + F8037B2579FB0A0267AF6020C1C7551851F6FEC5F3F7CCE967D27FFFEDB24630 + FBC7D67FFFDDF274FBF2D369C7F113693D9E782514F3E68B37D39E9B5F3B0804 + E2AE06F414E08B80B748769581B951F4C5EE1EC3451EAEB60060FA0A3A6D38CB + 5EA6D9CB5729F017E8B8A5848D1500DE6276F6E4ADBC7C4D5548DF6B787912E6 + 9736D71DF4512788C67EAC02C08034E49E9584ED750F5B4209E341AB50C18060 + 4B4B08683C80D93B77C7F5BBD617D89821B0802FAA60B2D9B663B37A443E0260 + 3FAF1B771CDEEEBDC7BEBCF713FBFF12023AC3DF6FDC7B5770975412008E6018 + C898812FD0D8FADCB8E721A477DDA22F109890CFDF5A9971730ACCDD7DEB4115 + FE79377DAED9673211F6792F6DA9BF6EE746086C135EC07B6E5A7FC242DFB0E3 + DDBA9FF6D9791EB8FF7DDA6BE7ECFB11DB31056E05BF55CE6986E2C0DB3D77EE + A4BD77F10C8EA1A181B21996AAAFFA632305DE028CE54DAC3E8CE9C7B7CFC3FF + 1500387B71DFCAF0BE806AF796B663F11D00420360B7E0E17BEDAA1D2BEF49CC + 7EC05BAE5EB6BF37FFBFB4B964C7B8617F4B207B6B2F67BE2FF212E7B87873EB + FF45FF47D5FF957F963C7E4F00384A1EC0FE3DB6327942997BB8F30B78CCF3C2 + 849DCBE55CB7FEDC0587BD405F01606030365200F0A68B97D3DA3300E10BF69B + B3F1F92D595BE0F1DAD3E7D3FAB317AD0D5EFB97D226DA146FE0F5ECEB5DBCFE + F1FEF570D3FCB6CF597FC2509FB66B807B02DBB1EDB78BB72FD09794B0D0805F + F26B8FCB8B183BA1A6EDBA72ECAC03DD559FDBF5E6B3B369ED9776EC53F6FB3F + 7EC5DAF3E2C8B9B4E48B53EE05BCF4F3533E2E2FB1008001B86BBF38ED0200A3 + 559FB0CF2F9EBF5C8BD9F7F7F3B4E6B3EC15EC2FE47400C05204BD82BCC85FEE + 79FF0BB76B2F60ED0B5CA56FE41788D83F7EE95B9F64088CCDEE3380DF7978ED + BEF2415ACABDC8EE41DCDB48175BFD7CBB1FCD7DE91D4FB93F716F420BB85759 + 3B5E84A2BDCBDA44008CF0062615F81518D63D1745E85BDDA7AD2DF93944E828 + F7F7D9475F4D739E7CDDEEDF6FA50536DF5908487ECEFA017B81C04FBEDC16FE + 597B043BF4254C7481BA338F3CE7E1A011E59697AFCD676C6E23AF60CA552868 + 207001BFD33C9A89A5075B21A09B0070E5D95BBC7DD93203C88B1DF99CC9E663 + 31F433B017F0AB7D7F3304B63959D9EE43DEBFE37821B0005F01E0FC7260DE1F + 788CE690C50378C2562B6FD9EBF9898C5DE6971E59C6DA0AFA463501E03AE8A5 + 9CEBAD4FB04B02C02357DBBCDA24E01BF7FF8D209839B8E6DD82BF485EC002C0 + CCD97DDE6E750A072DC08B0487C9D7EB048005784947DBB301A22C4F60C160BC + 80EBC057D0379681BB0AF11C3D8123041600960730D097E72BBC779BD2088729 + 037AC9F31C463A10D88BADD40D9A5960B0527BE664DF5F9E39D9F317E979D49F + 49795E35E1F52B704C7EE074CAC065EB6F79C15F79043B049EC20BCEF63C0CDC + 557E923D47035E81B105FE5640B808101C43432BAD83DBBA22F06D92DAC84B58 + 9EC292A06F1DFC528EDEC0AC3FC81EDBD7616F27F5ED0100CBB3570058DEBECA + 0F99343F0D9DBCC041706CEBA0B7E41F1939CD55D92D05D446F82B005C07B711 + ECF65655FF61532AB17EE690776886BD4D8A6B6852DDA67294EAB4DE565F7FAB + 03E028B5F336435A8037AAB58699EB63596AAD7966782B7501C0846BB6D4BD82 + 4BB9B297F62A577DEAA24EF275E4AC46008C068CC92A79AD4147C0AB7C27E84B + 9F286C0F0388EBFD9BD4D40F35B57D18C575FAA6BABAED61D4FDD8ED5CE461D5 + 11C23EA47ECE38DDF6098C2AAA0E807BE5FDDB8D1A9997D4130066AF5CA9D8FA + F52F619FFB0EF7BD8051DE07B8D6F73F654528FB5BAA1300CE90B783FC3FACE1 + A4FF1CE45FBCCE5FFE5F4DE10D88F803886A6BABF64D3FD43F9174F1685D40C8 + E78BA22EE84D17CDDE5CF0D5AF93D4A6DEAF376A1A2BDA63DB68AFD73B08E6B3 + 0FCCA9BC82BD0FF503EC465F2440EBD0B601E6B6A900DF366FE07E8CC984C226 + 2BC36C32639390CAFBB7BFDDF04D0E80834730793C81AB36EC114CE86887B471 + 32D3AEAE939DEE45DB3C61B2095680B675885B81DF02786577D1167B54BD4D4D + 75D81B5587BA3DA96972E900388C571F97C9ACF202D83EC11DCE7EBEEDEA3F62 + 7AEA4F3A2C6BE0F0E91EF679C0087B00C0E3B77809A3472CEF00B80E818B5417 + 6D82BF553F93203050B7D5D68EE7B017E8DB0E7E5B00382BC35553F1F615B0ED + 637AC4EA1F315B9FD119020BE44AB2457B533BD4068779D01A37AF82BE15142E + 76E0EFC031ED21A0A9AF206D51E5DD5B93EA057B2581DB7ABEDEA6F2EC6DA847 + F214A60D0FD23C5CB3A7120FD63C7047F8DBB2B5BC81874CB387F10280F1001E + CAFEBC65BFDED69EBDAB3CDCB3A43D7EE505DC0E7D33C415F8251D3863858B7C + 565E5C501B49619D917BF79A06CDC952A867857006D666B82BD0BBC1BD7FD907 + D853606F00B7846F96E4AD1BEB681FFB4894658BF017D85BC9F7FCDD11E06F06 + C0A3966F4963D8F7D73476E5764F1D04B3E8B46A9B7B1A646F03E0EE0E0F5137 + 66CDCE34CAF2BE87B0F5C13EDA6C5AC88A62E10BB1F0A5D078B4231FF7FD4580 + DFA9DB0FA729C05CCB13029A8537857D569E8538D9646791AE5209F997BD4232 + 00663150A19FD90B988542A0F0C49D8FBAB2776F6B1152C0579E28D32C3F73FF + 53694681B9EC7337712F1EC0E49F4ED30FB047DE4B69AAA513F73F1BC22BBFE2 + 5E392C8CCE3824C86B6394C551E02FF245D3C32CBC3E9F66D838338FBCEC9A7E + B8E5ED2B003CF7E8ABAE3936362936C2454F212CB4B511DC450E91AD8C47D06C + 6BEB763C7F9F78B9CDE3578BCB4D8BCD6EC303A9C05E2996DDFB9785EF97DECB + 5E4B2C6AB3186ED29E88805DC15B79FA92C7E68BE6AF7D9016067918674B17BD + FEA12BDA16BFF1515AF6F6A769C5BB9FA7E5EF7CE60BF6089BF2BEF8FF6E0EFD + 8CD8E79710D0F2FE05047B3E841B053CDC3CF7A887626E02B17F6A117AFAA79B + 47D3F65378F8026DAFB80730A0767581BC02C178000BF662539D6C005DD5AD3E + 8147305EC097D2463C712FE430B4002942D0B2DF2FD089BEA45B2F11EEB9150A + 9A10D0923C830586016D08E82648EBA0F632CA7015D53D83DD43D36CAA57B868 + EC191CE3299AF3C0DE9D029F00D29B5FA7BD80D99B662BFBDF0A2C2381CC5CBE + EE21817D1F60423BDF6C0FD92CAF5079EF0A00E7E3E6367BEF3E7000BC036859 + A0B0BC844981C508D818EB81BB7BEF7D57B5CB20F9DBB4FBAED9EE7E9F76DEF9 + DECA967A786420B1F5B5FE7C3EC4E7DEED9FF59BB4FFCE77AEBDB700BE36DEF5 + 075647F907EBF3B5034D9DFF8107DFA7FD5F7FE7E7BC8790D3B701CFD9035979 + B4F3167DB0DF4F7BEEDD4FFBBECEDA6D7F3B406EF6E08DDEB919220BF89247D1 + C3B8157EFB46D516E8AEBF7304B01C7B0FD0D8FA66906BDF0FEBBFEDC6F5B4FD + E68DB4E3D6CDB4EB8E1DF7F6ADB4CDF2DBCC465E7B1AEFB9933F837BF45EB663 + 5FE3EF677F1BFE76FC1FD8DF7B07FB37DFF9D6D3ADF6F7DC6AFF473935DDE4DC + 6CBCEBD9E318082C10CC9EC39B092F7DD5BEA3E5250252802E619F81C0D146D9 + F7C4B6DF8FF6A226656FE0F5E7EC37790EB07BDE7E77F61B3B8F37F0F9C49EC5 + 88F2DAD367D21AFBDD6F3C7FD101F1BAD3D96B7FD3D96B69137BF6F29B3B9F7F + A3FCB6B75CC8FB076F3C63E5E2114C3868ED31CC7EC14AB1038A01C4EBB84ED8 + F54321A3D7997D9DE57DAFE0A275C7ADFE2BBBDE7C993D81E5054C8A17F06ACB + AFF8FC4C5AF6C599B4E4F3D369F9B1B369D517EC3D6CD725B32FFFE8B8877F5E + CB3EE7A6151F117E9F30D000DE560A105EF745F10CB6F6129EC31E42FAC363FE + D20E2940981428ACB4AAFBD0DA16000CF00500D7F70A16085E62292F122D7FCB + EE1D763F590C30E6FE04B405ECBEA69795EC5EF772CECF7BC9EE67AFBCEBF730 + 45A2F017914AAA179F1C069B7CFB04BB4F027A01C14B5FB17E2FDBBDD1EE93DC + 5779A18A6D16AA7B6DB827C77BB4EEE1B24538ACFBFA3CB6687886B0D1DCF75F + ACF6004602C0D81CFE3EFE82C360BC8029B317309EBECC5F04781DF2164F6004 + 0CF697DD4A9E7A80F034BDD45600704C257905AB4C3E0261492FCE654F609B1F + 12327A47BBB031979BB003382C996DDB417F2150E19F01BF4AC76CDA9BC6FA1C + 32835F857F760F60D3D84D8482CE73CEBAE72F7942413B04B6FA31CC7311654B + 25415ECD79B1697E2C3BA23C724DDE2E654CF10216FC1500F6B9B60B5B06BE75 + F88B073022CF0B9BE36CCCB1D65E2F740A0E2BCD7900705486C1F20816E41500 + 9617F09005F6CC337F7506C0C55338C260E02F3097547914BD7F7B9220B07BF6 + 4AB3B2E7AFC23F2B343492972FF6E1F3EC987357A7013301B6D90B78C08CA569 + C02C7BA6B47A6031EDF0E8ED0B9805F05AFFA1F6FC27108CD82BD82171F12406 + 286B3FE1C1367604C082BFFD26DBF32F30B780DB36006CF90AF8525FCA02BD82 + BF750902F7060447D521B004FC7D64ECEC36081C81AEA0AE402FFB0C479BECB2 + 3581DE4E1200961CD07610F582BFC0DC01E3665500981420FC4881BDF5BEB497 + 1E4100E1006D7F4D008CDAC61991D3087BEB6AC1DC89D59A5B5D11F6AABD6C71 + 7DAE27690CF295334759DBD49A655398E72635AF7BB643E04688DBA05EB517F8 + AD97079ACAFA7284BD6D79D6D7078D73702BF51F3CBE0BC8AD9759B7FE63291E + F7D73C7ED3FABDECBD55BD6F3CBF56BBAE3CA449ED7CE43F2EB5312853645382 + BEFDFB8EAA3C80DBF6EE959A6CA638565D6AD3B71F7B0E0FAFD4258473F188AD + 7BC792AFF605360D1C32360D1C6CFF97B409ED3A8A63F519D6590136FF666A00 + DB0FA508657F4B3501E0CAE3377AFE56F0B7C0E13FC61FF1E7C8BF7CED5FFC46 + 75F862F75A1DFA36FD10625DD30FF54F255DDC5A17B8AE17CF4E17D59E14FB79 + DE3F7B6B9C58FF3052BFA87A5D6C1FEBEAB61E6F5AF590CE35C0DBC5D6541F14 + 433E0371878C9A9186DB8476904DC422F8750D6A0167173018F8AB760D131AA9 + BB494F9368CB242AEF013C39C3D9027FA5688B10B7B217E80B087618DCD0AE6F + F1FC95228C450F0B7F999056F93059942200EE696C9FDC36415F93CADEA68062 + 81E0FE236CC21F423F670FE00C6B3D05EAAA1CE06E547700B882BAA5BE25FAD0 + A6D51779C869572BECB340ADE781B496772F5D24705B5481DC9A2DDA5516C095 + BC4D01BD0AE3EC65D32378315B194FDF4163E77BEA5EBFF4333BD0D5C1EE787B + 6833F5B5873787B205FC7602C015280E36298260C15FF7EA2DA19E230456DB4A + 80E2298B1DEAFA437581BD31FCB380B000707E005FE1E0B70E8001BFBE4FAF6B + B5694D057E073BE45DE31EBFA8E5BDBBC61EF081BF19FC2AD4733300B6E3B6C1 + DFDC965410580018F03B64DE5A4FB57FAFBC76A3A7AFE0AF87810E2037425E52 + 41DD28D59357BFD85FF64E0058A19F5BCADEBF80DC28169B586402F49207FEB2 + F804FCC58B1739042E0B52E4D9D7570B5D4D62D14B0B632AE31121004C7ED296 + 030E8027E3C9DB007E635A79FB5A3E0260D54DC26B77E7A369CAAEC71D02E315 + 02041EB389853F3BCEEE27D2F47D4FA671C066DAED61AFDFBCE8C842635C7854 + 4A284200700ED7CC3EBF59006079004F3DF8429A72E88534F3282116DF4E0B9F + 7BD7E16BB5675EE92F102C08CCA229FBFA027A673DFA4A9A7EE8A534CDC68AA1 + 9E01C082BEA4F206461EFAB9C0DED9562F083CFDB1173D95ADF20C7EC26C4F67 + 8F5F2D2CE3014C8A8DBCE40BCA782695C5692D50C7328BDA0E7C2D95CDC35DB2 + D88DAD005F79F82E7CA5E5299517D3F3E23960370AF04B0AF8750F2BEB4719EF + 2CC0EFAAF78F390476F86B62D1DEF3C5EB4BF2FD7D0300AE64E5C5EF7EEC00F8 + B3532FFCD978FCF6A4FFE1BB15E9F38B6FA6D52780B9971DE212F6599EBAF2DA + C59E01EF394F01BC82BF802852ECC05FF6E974007C1E007CD93D0F094B4B1950 + 854730E3E205BC03CFD88B40E0EB6607FC52CE0078EB2580F42D1B038FCB0267 + 4D785B66189B433423BC7F057B81BC02C082BB02BC4810983684F1559971F070 + DD75EB41DA7E1D309B3D5DF7DE799001AEB5F73EC5333583C10C803390CCDEAB + EEA96AFDDC2BD6FA5563042F5E445936E57DEF5EF7E6CD2057A22C5BCC038111 + E57DF7BFB7BE40E51C52DACB77BF4BDB6E0127BF4BFBBFF95DDA7BFF07F70006 + 00E305CC5EC0C841F08D0C80F7DDFED69401F0EE1B76AC1B78047FEF62FFE07D + F7CD6EE7A8F3268FF6DE0302F319326805980AFCCA237AA77B269BEDCEADB4F3 + 762B6C7406B87806DBDFCBDA67289CBD82058155273B7F6BE02F7F773C82F7DD + E36FD3F2C2E5FC1C005FCFC7F17AEBBB8D7E45C05FE4B0D7A43C6078EBF56B69 + EBB59B3E16C0D83DC97D8F65FB5BDFFBC1C6FAC6CAF6FDC1FB178FE9DBF677BE + FB6DDA6E7FFFEDF67FB3CDFE5FB601CCAD0E00BCEDFAF5B4BDEC052CE1018C72 + 486C3BAE7B2667A84D2A00ACB2A0301EC0BC3C41D86E7E535B2E5CB7EF3030D9 + EC8C89977081BF8060C1DFF567F104B6DF7681C2EC69BCEEB45EDAC821D7D927 + 58BF67D24DF67BDD68E9E67397D3563B2EB017102C017D81BF5BF8BD5B0A2096 + 973061A20583D7A313765CBBCEE0314C7EED57672D256CB48D7FC63EDBC92B0E + 7F577D7E36ADF8F474D61767FD259535848C3E7E31ADFE22EF63BEE2B3530E80 + D9FB17688BE72FA9802FB017D561F0CA4F32F495F76FEE932130E9AA4F8EBBC8 + AB5D3DBFCCEE034BDECEE1A1151A5A0204134ADAA1B0DD5FE20B45DA17B81E89 + A2BA7F71EFB3FB57BE0F599FB22D418C4AE110987B62B86FF2A2D49297DE4BCB + 5EFDD021F0C217CB5EF9CFD8FDD804FCF57B2D61A00BE4553868EECD44EED03D + 5B5259F7F5ACECFD5B41E0A32F55B017D04B2A304CBD974D78FFCE3CF27C0578 + 15F259F31952D41412BAF2023EF4946BC6E19C8F0018208CB0934EDC03C03DE4 + 9157987369CB0CE66019FE1E6901E012F6198F60F70AF6BA477D3EC7161E9348 + 1D06E7BD7FE5012CAFE1CA1B98F9636D0F60A5633766B8DB2481600160F60296 + 97AFC43CB73EF7D5FC37E655F69726D759FBB556B679750BF8D60510CEDEBFA4 + 1265C02F0208038081BF78010BF6462FDF088047AD000867C536487017F04B18 + 682428DCE6055C03C002BE110247102C09F44609000F295195869BAA2D766AAA + A22E59DEE1AC00B0D900C6FD3C54F31287C3FDA62F497D2D0FF0EDCB73A6BFE8 + BB3CF52FF017B83C24C0DFEA59B500600430D65EC0D10358A1A001C07D81B4C0 + DA027401BFDADAA802BEC54E1E8F60C06E27F01B2508DC0483EB75AA8F792400 + 5CF7008E21A06318E85816FC75901BF28F8C9E55D9047A3BA9EFE8990E687B82 + BF08F80BE4252F6F5E3C7F81BF68E0783B5EB1AB6DF4186E03C0AC0DA2027EBB + 405B13B658FF73F57300B0C06E946C6AAB10D0A80E76956F525CCBAB54D636B5 + 661901704F20B8EBBAE7C3815FE91703E05E480018F03B60C8049780AFD6A705 + 367B5CC7FE95543F4EBDACF5F7EED6E17B52A77EF5B1BB5353FFAEA21DEDDB99 + 4854E423BD69FFA75077E7143994F25182C01DE16F2F15C76CB3F76D07C04D10 + 58F01775D90F78107F731BA7E4A9575D1CE3CF4611F8D6D3875584B2BFA5BA05 + C09D143FE89F9BFCCBD7FE457C28FDDC1F42E8137F10F147E1797E9C7FE48DC2 + 3BA9EB858D7CFB85B2E9A2DAD34DA7535DEF2FCE9DA5F389AAD7C5F6D1DEA5AE + 005EF6DD55B982B5E4FBD9CD18CF5B6C02B3A8B469B3D7DB149B8F2F0FE2C136 + 491968938FBE762C4B47D86476824DCA47DAE4B6FF509BF014D80B0C463A06B6 + 7E36B91930C426558300AD365E3710B8D384A793689B274F19DC46A05B950BD0 + 05DC22F2F5BA7E6647AAAF4B7DAB310280ED026A4B9FEEA06D9B6C7218278DCA + 3369A5BE1AD7F29ACC76B20BFEF6B50971DF30816D8D9BCF5F2038B7B7870341 + D8027E25F7020E6017656FDE963D425CA9CDABB7A1BEAF9D631F64F976B5EFFB + 2B60EB8076747CF0B18725E0ED28AB1B69ED4CE463D95560AFC688D037CAC7AF + 01E00A02973CEDDC0BB8847C765B04B896F7FEBD00C00E8A19D7D226081C81AE + 3C7F230016048E6D08F11C05EC15F8ADDBA33A01E0E8FD0BFC1D3A0BAD31AD6D + 41604BD9AB575EBF79DFDE2C79FF0A000BECB6E02F02FEB6EA63BBA1E4E7AC49 + 83E6007CD7B9C8B3C7EF406C78FD027F0BB06D179EBF5286B91265FA92C6FE02 + BBA4D8549600BEAA4702C0959665002CEF5F9581BD63F0FAC58B37C05FC0AE43 + DDB20025D00B001EB76E77E5055C2D46D1B6B6A85597206F7D514C363C7E01BF + 406016D2B4A8E60B6B050493C7EB82053B423EE3E93B6EEBFEBCA807FC2DED58 + DC9B5C03C0080F60160509EDCC3EBEA4E3B7B36898BD7C0581A3F7AFE0AF1621 + DD4365EF51DFE7B725163A9F4F330EBE94C1EDA32FA5D94FBD91163CFF6E9AFF + DC3B19003F061CC653E6396B0B08CEF245D24378CDE04D63631C7EC935FDD08B + 3E16E19D01BCC05F07C102B896FA5EBFC1361B306CC792B72FF669765C041C56 + 3BDF2F907D029F7EBD1542D2F2110057E5B208AD05EB6AE19A856CA06E29037F + 0995E9DE4D050E2300B087C2B4FA0881593497A794EA17E131F566D97BB1405E + A58060E02F298BF20800BCF2BD2F5A00B8D848B1E1F5B5E28363AE6584772E9E + BF4BDEFDD83D7DD96B9274EF276FA5FFFAD6E646D0FAE7AEFFEB832DE9D1B35F + 3A98ADC0CFF9AB0E77057BA943D8047D69A37694058581BC5B7D3F5DEB77EE9C + 8BBD4B1D625DBAE6FBF86EBC78356D07E4E2C54B7B87B9B7B3572FA1A02F591D + E1A02FDDB614789B43ED2279024BEEC10AB82D8037825FE47BEEDECC1EB9B411 + FCA51FFD0592D91717180A54C5D396F6F4537F07BBBEFF2C618A09E97BCDF3D8 + 7D6CC0F0CDDB0E4623DCCD8094FD731F783B856E76CFD9AFBF4DBBEF7F9376DE + CD2075CF5D6B770FAFDA6FAD6F86B9EECD6BA9C0EEFEAF7FF07CF404561BFAD0 + FFC0831FD32EB36DB96EE772F78774F4AFFE517AECC7BFB6F3CF9EBDBB6FB600 + B07B0317004CA8E7BD37ED1C6E7D9BF614ED36DBEE9BDFA77DF7BE4F07BFF9C1 + 3F0B9F4D9F0B28BCCF3E83606FF6F6BD67759C177F8BE2057C0BB80BC00594DF + 7228AB36126501E43896C69087B0F280E103F71FA4FD76FCDDB7BFF6FF27FD5F + E9FF3C03663B37FBFF91C7F0AE5BF4E57C00C840666CF67F79D78E7927C361FE + 8F00B978FE027FF97BE3490D0006FCAEB7EFE6E66BF7D35BFFD9DFA64FFFE65F + B93E437FFB2FD3E7FFF45FA52FFED9BFF6F4D3BFF96769BF8DBFF932BF01423A + E7FD8073F8E71CE299F313D8D60B06005F3C7FB387B97D97F9FD5C31DB65EBCB + 779BEFFAA51BF61B01260395F3B840E50D172EA44D972EB9369C3FEFBF41F202 + CF1B2F5EB4F101CC780EB3B73621A079A123FF9EDBC3B55B5A42456F2EA01701 + 80D79E38E31018000C20DEC46FFB94D5B327B0A578F9E2ED0BF4DD6A636F3B7F + CD2130F077F5B1535677D1ECF6B9CE5CAB00F08ACF4EA7E59F9C4A6F9F7937FD + EEC6D174F1EA2B69D7196BFFE5F9B4DCEA56E0CD7BECB48FB186D0CF9F9D4C6B + 8A17F0EACF4EA4755F9A8AC7AFC0AF4242E710D13954742B6474F624D65EC202 + BDA4D4910A103B00FEE08B1CEADF5F0C3A66F7842F2CC57E2C2D79FF33AB236A + 84DD230A089687F05260F19B1FB7ED372F10ECB2FB12F720DD7FFC3E66F72E01 + 60DA3B282EF74DE4F7474BB977027FD122BB37723F9D83D76EB9EFAA9D83E360 + E33EAD17B474BFE6DE1EA170F604CE00581018F82B005CEDF7FBD8F395473076 + 60B0E0305057223C34F3150160897204C095886C52E06F1D002B2F08CCDC0A00 + 8C08173DB50060CDC534379BB4EB705186BF1E16BAD4B9F76F01C0A4130915BD + 23CF1523001610567EE2B603696201C09203E0CD7B4DA50FEDC9038CCBCB869A + 9B4A3E6F655E1BE6B64A81BBA300BC56E6C5C7381F668EAC39B1A76C9752E6D9 + 758DF610D12DE88B00C263D6304F670E9F3D820584A3E72FCA5EBD59F2F445F2 + 08AEDB5C4B330406EE0A020B0A4B82C00E8417B4C23F0BF82A2F301CEB7E2900 + 9680BD39725306B9406087BB05D68E5BB2292DDA73342DD8FB843D1B6D487DD8 + BF77D6720F47DD77C6D2D4DFE421A44D78FA028287DBB3E0101B63A043DE0081 + 6DBC9618BF250F053D85E7DA0C6C1DE4DA397402C0C0DFBE13EC19DE6C11FA46 + F80BBC555EE5087423E4ADB7699220B0E4DEBFAC9B05F81B25F02B201CA1AFD6 + 3F943649A058620FE0FE63DB43400370A3B0518F280BE46207FA02811179F557 + FBA8D817E517FDB3BA40DB0080555F87C3B16DA73AAFC72BD9EC5A078BEB6172 + 80109C05000BEA7627D6CE90FA697DAE2ED5F5D44E6B9BD2C342DFA836901B61 + 6D0FFA5900D86D26D6C62DED1802BAE4A9671D5D009814F5A14D58CBA64D1D0C + FF2914D7D8A39ADAD6C5B9F774FE4D63775253FF26D57948546423514D6DFFDC + 14CFB3CEA2A2C4A57A625E1DFBD5DA783E72B00280FBF5E90A8105713D85253E + 32C4E12FDEBE7D8B0DE83B64F8843478987DF781C1669757F09F2D04AE4BAC14 + 35D5775284B2BFA51E0600F7E96F1FA23F0018359CF49F83FCCBD7FC25ED95F4 + 63E8499DFA5839FE5094AF6CE187FAA756FB850DE50B636F2EC8DDA9DE5717E5 + 87BD4037496344D5EB54D679643B9FB9FDD80E7E6BAA406E81AF2A0BCEA2AA3E + D8DD5697B589E37A48E7413631E997CB836DC236D226B278026B0CD22AEC7338 + 863C84A5A6098DD49BC94F146D2B003C1015F81B1500B083D94EF6528E8A36DA + 490E6E8B2A101BDA930ACE7627EFAB1031B5092393D6D856135AF5ABFA977C55 + 6F93D747988C5ACA845493D97E23D42E43E27E96F61D6AC760823CB21DFC0269 + 491D005BBB087FBD2E4060D23AEC5559EDEA76955BC7CCC057CA651E7CE6A53E + 80DF51762E68B43D04597F0F018D676E01BB511A837C1DF446D5EB2BE85B52CF + 8FCD21A0C9577B0017A02BA81B81AE00B0ABF44311000BFA56ED6963E54E00B8 + 2EC1608160C1DF3AD845D8B5FF2FA9F26ADF16027A3AFB3115005CA0AFF6011E + E45EC02B2B003C74E6DAE2FD0BAC4519040F9DB3CE3564361EC02C0A64C9A357 + 5268E79CCF75F2F655DDD0396BD3B0B9C1ABD704FC1D347783CBF7F95DB0D9D5 + 0E7CB3862D04D86EB17CBBB72FE5BA4D7985768E12FC95D4873CE057A19E5B1E + BF59C0DF312B773AF81DBB6A87A7235758DBB2DF9800300B505179F1A9E58540 + A83A2D5A69E1CA17B9D6585D58D0422C7669D10BBBEAD506000CFC05E2B288A6 + 507B2C9669C14D0B6C94DD03B82CEA49F2E820F545BDED8406CCD0B70A0D68A9 + E02FDE222C22B27028E8ABC54601DFCA0B65BFD9F73FE6210BB3F72E00F779D7 + D4FD40DDE7D3F40319DA4E3DF2729AF6380BADAFA5E9964E7DF4A534EDC80B1E + A619900BD46DA5D9C3D7BD7F0F67EFDFD98FBD9AC33F0380ADAFF6F8A53FE300 + 7E2507BA4FB4F6F605F20AF4BA8EBEE2B0D721F0E32CFCE6B200300BC4A49216 + 8DC9B3983CEBA9572B4F24498BD6F2F6956D311E5166274F1DE19F1DEC028659 + FC361BC2263B0BE32C902F65919C05F6B7728A3795C23F638B62815DED22F405 + 06CB2378C57BA60F8EE5D09F251C2810010114A44F4F3DFF671BEEB9B7FA5F7F + 5C94AEDC7CCD810F00577BF722F202BF942B2854E02F22AFBE3BAEDE4A3B6FDC + 74E0B4FEFC794FF14EF410B617D9ABF4B2EFD5CB9EBDECED9BBD76F1D80500DF + 4A1BCFDF481BCEDD489B2FDE4EDBAFDE33E53D5DA917F48D4018F8BB1B8F58BC + 3F0BAC45F20226DD19606EE5CD6B7D15061900EA60398459CE00317B8D02201D + 66DE0110223C46AFA55DB76EA5BD773278048A0A8C22F2F24675D87B3BDB9083 + 61C240DF7BE0F0776749F7DC03A67E9F80B84879C15DD268972730764419407C + E8DBDFD958567FE7BB74F887BF4EAFFDB3FF323DF70FFFA9D9BEB7CFF4C061B7 + BC8049F7DCB6716F0BF8E2F59B539481B19DD7DDEF3CE4B3C36C530C018DF72D + 7F273C6501B6FBBFFEC6C5DFCC616DD1EE3BFC1D2D25FC7301BB11FC7AFB027B + 2923F2F22276981BEC94DD8BB8F4119CCEA19F0B84B6630AA002E9751EFBBEB6 + BAFBF73C1CF59E7B19FCEEBD6FE3597EA79D23FF1F78F3E670CEF6F980C1BC34 + 60DAFBF5EFD2E11FFF61DAF3F51FD2BFFD7FFDCFA9A77F9FFCA37F14806F06B5 + 82B19C17C017000CA016E406EA46004C1B0F5D0D90B6DF0CA1CDF97E6FBB8A2D + 03E51C521AB0CB6F0FE87BDEEC976D9CCB7E3C1D73D3A58B69E3C50B96B7F321 + B4B58DB39EDFF2F9FCF2867BF197DF767EA9C37EBB17F8CDDB38F61B06F6B23F + B000309EC1D8B65FB4DF3FD78F33C0DF3369ED57A7D3C653E7D336FBBD6FB731 + B2FDACC35BC49EC19B4E5FF17D80571FB3F33D7535ED3E793AFD9FEF6D6CBF3E + FDB028BD75E66DDFFF77E5B153092F6379113BFCFDE2745AFFE5D9B4EED899B4 + EE4BABFF0C707BCC21F0DACFF10EC653F8984362DA038D11FD56E13D8CD72FE0 + 17085CC0AF04F405F84AEC138C97EFF20FADDDC7277DAF77F67CD7BEEF2B3FFE + 322DFBF08BB4F89D762F618F2811EE3BDCAF96BD91C12E2018AF5FEE43AAE7C5 + 25BF87BD9ABD84BD1D225FC4FDB27A618A6D13F0F225D5FD937BA5E5BD1EE06B + F759C030F75CEC44E5E07EED5B2E982208D67D3CDFCB89E461F30AF6FD7DDAEE + E905F24E7BCCE613C5FB57F0575EC0DA239832905821A1DD5BB8C0DE3AF08D50 + 585EC1EE391C00B080AF0030521868E4F3AB028327B167706D6EE67B01EFC936 + E66E780BE30DDC2AE7399EE67B13761C4EE3B7DB3C715B6B9E98E78687AAD0D1 + 79AB10F2CC273304AEBC81B7ECABDA038063D419E6A69AA37602C09AF7F252E3 + 089B1713327AB2CD4B67EC7ACCA3DD50C75CD83D7F6DCE4B7B9599478F662EDD + 067F7921B30579057A81BF63D752C73C7E73E5055C790307A01BF3F200AEDBD4 + AEAD4D09FB0CEC1DB5CCEAAD5C07BF68C88216E8C513388683965D75CA47105C + 07C28367F39C66CF7D05E85661A08B1DC0EBF5C5560160CBF30CE74097B269FE + B6C3E9851357D2F3272EA559F6DD7964C6E2D46FD6B23470EECAD46FE6B2D49F + 50D11E2EBAB5DFAF1F93E384B0CF48E53A009627F080027B51130016FCC5D6E4 + 01DC13C095EA00987E4DFD551FCB02BF526F013065F211F04630DC1B018023E0 + 25DFA42EF0B6405E79F82AC55E97FA3C52C243F7315B3F348AB59F7600AC725D + 0E729B202F65F6F96DAA2BF2635AEAA0B7930AA0ED2D0096EA60B7354E4B71DD + 0EC535BDAABEAC6DF6564DEB9D1210B7CBDEBFA69E006F77F53DF61DD4AE4E00 + 18B1462D2FE09F0B80B14B4DF53F47F198F5F1756EF11C7B52ECDF491AAB3E7E + 93EA7DEBCAC763ACAE4C242AF2911627696EFBE7A2A673AD33A8A8367E259E15 + D4B14F2785713A01E00C81AD5D8DDD29EC335EBF94070D1D97162C5E93162D5B + 9F468CB16B566907FCFD5303E0FAF1754EBFDA794528FB5BEAE7780013439B58 + DA8D27FEA7967FF97AF892FEDAD20FA87CF9E30FA59E8F3FD63FB55A178BF69B + 8AD4DB0B6A5DBA98D72FECE4B989FDD23796745E0F73BEF93387CFCA7EBA11FA + 223C7E5101AF511578EDA5DDA5714D1CA7DFC009A97F81BB7D06D8DF61F0C4CA + F3D7DB947ED578F5F3286D7CFFDF214C5A9AD59BC94F5DDE8771058081AF28E4 + 0571ABBA62EB1BE06F1D0A47F91EBB25EF630D6D81D90AC29671AAF10A98ED51 + 61E228F9A431B4899037CB26CCBE6F6FAECF6F3ADA44DCCEF311EB8F9A3C8073 + 997E3606B2B100C08F8CB0C93BB0B60DCAB67B000BE40AE046901BC16E6C2700 + 8CAA36413A4E84BFD90E049E931E1905000E6FC18EB2F6A3AD2F9EB905E23681 + 5FD7181E80E83BD761715DD801BFD1ABD7DB1760AB3C21A0290F320DA04F81B5 + 82BB11E87A5A00B0B76B5281BCEA87A28D34C2DE36D05B03C09557700DFCD63D + 7FEB75520BFE66AF5FE0EF4093F6FC251D68F67A18E856A867F23CFCCB86F76F + 01B800DEAA3E435EA511F64AC3E6029055CEDEBF11D492027601BF0E79176E4E + 43176E4983E70379B7587D57189CCB5D61AFCAF2EE553E825E491EBE4DF9A6B0 + CF31CF7EC0F2FE05F8027F87AFB4FE2B595CCA10387AFE0A00CB86D72F8B542C + 5609E6C67C5D405E16BB7C31CCF2512C7A018009FBCC625A148B6A31BCB316DA + 1C0A97C53C4910585E1EE3B1EF9087EFE3D5E260DEEFF7689AB2A7E5E52BD83B + 6D1FDE2447AC4CBBBCF0E83687BFD6F6C093D9ABE5E073A6172A09004FB1FCA4 + C32FA689879E4F13ACCDF8FDCFA471FB9E4E13E903F47DE29534EFC9D7AA7D7C + 057709E93CE771B33FF17A9AFFE49B0E81673D9AF7F725BC33E057F057601745 + F84B5DD5E6B197D3ECB23F200BC200DF088049057CEB0BC77805579EC1D43F97 + C350B2E8ACC5E9B6C5EB22CA0E8059B80606BF9A3D7B2579FDB2602E184C79C5 + DBC16B8AC5F5B73EA916D0E5F1CBE2BA7BF5168F5FB5AD03E0D51F7C9956BE8F + C76F0EFDBCE2438506C5338CBD21D913F2F3B4F1C30FD2BFBEB1B30D54FCC7AE + FFEADE81B4EFC2F90A0009F20276EBC05770483058E078DB951B69FBB51B0532 + 9D4B783E02A4F00276AFC6CB805F64ED8158EEB59B01AF7BE35EBA95365FB432 + FBAD5EBD6FE3DD71CFE0BCE76F3B0806E2E6F0D0A477DDF353619B230006FC3A + 4073D09C6DBB6EDE77F08980C0DBAFDF4D5BA8BF063CCCDEADEC5BEBC0D0F7AE + B5710B00C65374E7ED1B193ADAF1382EC720C52396BE796FDC0C76F1F2DDE563 + 65F8EBB0F7EB6F1D00EFC06E65F2D9F6ADC35B40AE406FF4F025A50E2F5F415F + DAED213CF3DD6FDC5377EFD7DFD939DB797CFD533AF2D33F482FFD937F959EFE + EB7F62E3E4BD80B703BF6D2CC625B4F37EEBB3CFEAF6DDF93EEDBF6BC7B574EF + EDEF3CC58EE7306D01BE287BFDB63E67F67ABEEB02E2EEBB8F2772CB9B170164 + 01AFFCEDF6DCBBE76DA897872E20973E0E6D6D0C24B00BFC25F570D0A18EBEEE + 39CB7EBD96E6FF33A0783E171D23EFFB9BDB90EEBE7B2F1DF8C6CEF1BE8D77CF + DA963D8081C0FCBF6EBF6DDF2FFB7E6C316D03DCF377BEF36DDA88C7F88DAFD3 + 537FFD4FD31BFF9BFF5D7AFCEFFD4DFAB7FFE3FFBB60DECEFF3EFB9BBFB1F3BC + E92F0CC8831C180BB4CDF0F6461BF0E5BB0A0C4602C048E1A0D5566D1803B08B + 672FBF357E7BEBCE9DB1DFDB453BDE351BAF1D102379093B40BE7ACBFA5EB7DF + 9DB5F3BDB95BBF75FFBD5FB0F279F6F2B6F12F58FF8BD8F8ED5FA8C2410381DD + 13F81C619EF10036FBC9739E6E3A7DC1E12FE5F5652FE0BC073020F8BC036052 + 4241FFE1E6938DD72620F0AE53A7D2EAAF3204467802BB377001C01BBE3A9701 + F01778F6E6BD7F375A7B52C02F5AF9B9E5ADBDC36293C3E04F4F541EBE11FECA + FB17E8EB5EBFEF7FEE79D2951F9F482B3F3A5101E0C5EFD8FDC2F2CB891A61E3 + B06500F0B782C0C8EE4B92DF77DEFAD4B71D501909FEEAFE45D48A056C5B4074 + 0BBBD7714F24BF94FB202F45D97D531EBD82BED89779BDDD3F5FE4BE9BEFBD6A + 5B7900F342D6B319002F7AD1EEB99672DF16F8E5FE3DFB295EE4CAA19FA3663C + C10B612F780AF89D8B877001BE883C3660B0C0AFEBD1E77C5FE0B947995F9808 + 11ED2FB165E01B2130F23D8303F08D1EBFE49100303641614160F706DE75244D + D99DE769D3F7DA1C6D2F2FE8E5399BC0AF3C80057F0580C76F3F9401B0694281 + BE005FC15F95A730C62EEA994B32B7B439A6EB4015427A5C997F0A00D75F5454 + 1900AC792D52441B01605E5E9C6AE736659B9D1B7565DEACF930E077E46A9B7F + D3BE2882E03A0016048EFB026B0F6087BFC05B1379C15C8467B0BC83058025ED + FDDB0489A9531868858216FC150C16E01DB168BD2BC2DE2881DF26001C35644E + DE03583016A8ABBCD4A53CA7B527307523E6AD4973B61E4A87ED77BEDFE68C13 + D7EF4C8F4C5D98FAE1F56BC71834BBC06493E02E79F73AB6FEB2D79581B03DD7 + 0639083609F83A90250D503802609523FC9580B5B12C5B04B97500AC36B14EF5 + CAD7E1AF5407BF9200AFF202B91100D7EBBA930030AA03E0588E2017A9BEDE27 + B68B0098F4910280C9F71B6DED0B0016A88DC0973AD5AB4D47C01BE06F5455FF + 50007852B52ED6B265D5A16F1D002B8D79D5AB2CF81B9D3ABC8E7259DFEC4E9D + D638DBCBE3DBE480B6566E5277F5DDF61F58C0EE206B5714816F1300669D9CF5 + 72095B5CE7D61A7B5D4D75D87E0D315627002C755A876F5253FFBADAD6EE7B50 + ECD7DDB8751E525784A95253BB3F27753A4F71A8C8A294EFA21A0C8E7D3BF669 + 501F535F41E06A3F60D2115E563868A069057EFB0E730F606C00E0252B37A5D5 + EB6D3E316166E17B566FEDFEDCBC80057F7FB5738A50F6B7548F7B00473900A6 + DD5053706D464D1FE24F21FFF2F5FE4B5A57FDCBDE49559F087FC30F476D625B + CFD77E983F5FED17BD6EC571FDD8EDF67C11CCA9F24D523DD2C5BA27A9AD6E14 + A483EC263ED4260D83ED46AE9BD91F4FE173007D01BD846226CF8400C06A65D9 + FA0EB67281AE0F25C15A13A19FFB0C34DBA05C76B0ABC90710B84875755B93BC + AE8CD149F5C9904F7C3807A95EA7BE7E5CDAB6E0AE524975F5FABE11EC16516E + DB2FD72408EC7D0BF495DA406D1923DA9B26A4555D98404A3E712CF551F403F4 + F61F6113F161ECF16B13683BAF7E43A7A501C36DC24E79B89DC7B009A9EF88D6 + 045693509FE03A2C6E298763B6C93BA0B60059490098FD8123E895FA8F9E5D29 + C25F247B6C1FE16FBBDAC16F5F539F51805B7B881A076CE561270B880A788D20 + 37E655969770AC97D40EC94317451B296D733DDEBFC062ABE3812E1E9FB645FD + 26DA835FF1B475683BD1F2656C1D47701709FC2A1F81B060AF40EFE029F6A03C + C51E86272F6903C011F846C0DB5EB687E769CB5D83A6017B33F81D1C1FAECD3E + 60EA729395AD1D295218E8C1D37938E7CD6FC238AF4E8300BE847D060ACFCEE1 + 9E07CEE06D714289AD4DC3E7E10D0C14EE1E00D376D8DC75A60D565E9B86CEB5 + 3E26F6FC15BC2505FCBA80BD0BB7A5618BB6A7210BB67A19511E8AC7EFC20C81 + F112CE1038035F41DFBA220C06EC0A08CBA6FD7D057D472FDBEE65C1DE0C7977 + 7A5E65EAF00ACEB6ECF1AB7D7EA5E1CBED5826F202BE5A7C42805F7928540B54 + 0506C7852FC0AFDA92C72608AC36DE9E852FDA632B8B6B02BD15EC45405DD3D8 + CDFBF2DE6D65712E4A8B7A1381BFD876B16F5C56F6FACD295EC01100E70545BC + 83F108C6A324CBCBFB1E4D93F61E31597DF1009EBAFF9934651FFBF93E9B72B8 + E61713A19BA71D79294D2E9AFAE8CB69F2E117D28403CFA629879F77500B9CC5 + 4317083CEFE8AB1EFA79B6D937BD7F3C6D7EFF545AF2C2FB69EE536FA6998FBF + 9A66990476E943FFD9D607298C335017C9AB5702F0C6F09011FCD2973A24084C + 1E3B65168F17BED05A708E8BD3110263571DEDD8BB9010CF73E8437B16B86947 + BF97F322B8C3DE373FF13A6C84D4D4BEBD8BDEF8282D60AFC5E275B518DB5B1F + 57FB3362CF7BF912BA9390CE9F791E1BF52AAFFAE82B07BFCB3FF8CA17F93DBC + E8A727D232F600FEE0B3B4EDD34FD2BF7BB0BA1154FCC7AE7FFFDDBAF4F88563 + 69DD990B2EA00FE15F0140EBCE9CF774EBE56B0E81A843D868837D93D981BD78 + 18E281984130600A2FC4BC6F2B0016CFC39CA2ECD58BD8A3D6F7FBBD76DF21F0 + 964B19F87A7D49058133FCC50397D4FA164F5F80165E9282B27807575E94D76F + 55A078A7EFCF6A7537723865528554CED0F56B87B3FBBEF9361DF8EE87B4FBBE + B5BB63E7ABBD62EDF330C62E1B8FCFC1B8FB0B20F5F49BEFD3DE07DF65007CEF + 81F5B373BB75D7A12FB017313EE516007EE07D0E7CF763552F600CD8F576C057 + CBD38FF1D457FD731B8EFB7DDAFDF54F69FF37BF4FFB080B7DEF7B87CB006320 + 32728F6207C0DF39FC3D70DFDADFFFD175C8FAA17D66DF6BE3EDFFD6FA98F85C + 7C26D2FDDF58D9CE77CFD7406C3EC77DABE37C39A7070ECFF75879F77D60B8FD + 7DEFDEB173FF3A1DB6BFE7013E5F81B884843EF80DE1ABF9DBDBFF6701BF805C + 07C505F8AA2C10EC50B81C43C7D679208EB903F06C76D2AD8C69F6BD0F3847DA + F0FF91FF4F77DCBA99B6DDBE6E7FD3DBBE77EF56BE3B77BFB1BFA1FDDDECEFB7 + ED16FBFC7EE7E0F7E57FFE6FD2913FFCE3F46FFFA79E01F097FFFC9F7BA869A4 + 30E2780103820987CDEF02A02B6F60816DA5AA674F60B5E1FB9C61309EC5D809 + 198D77AF7E7B171C006FB962BFCD2B19FAD216F15246FE8DE63D857919831723 + 102F65A08D17AEBAA73EE926DF6318EFFD0B69ED69F612CED784ADE5BA806DF5 + C93369EDC9B369E3E9B3698BB5DB7ADE7EF367ACAD95F112461BCFD8B580974B + 4C6B4F9C4FAB8E59DDC94BD6CEC6B674CD57E77D6FF2A6EB127AF7FC7B09EFDF + 555F9E76E1050C4C764FE02FF1383E9B361CCF6017CF5F09200CFCC5DB77D517 + A7D23AEB07805EF5D949F7FE75AFE0120E1A208C8DB0D0EE196C29C09708100A + 0F4D5B00F0B20FBFB2763905FE0A0673DF58F1E171BB9F705FF9DCD3C56FE311 + FC91D5DB3D863DE5CB0B4908F0BBE8CD8FD3C2373FF2FB954363CB2F7EFDC32A + 92050204EBBEE82F47716FB47B25F74F3C8281BE4B81C3AF7E989678FB77D2BC + 97EC9E6A9AFFB2DD8BC9BF8817B0DDB74D739FB37BF5F36FA4052FD818A6F9CF + E1F5CBFD1B29FC33F7FF960730A19F81BE0A038D048581BEBE07F0E32F64B86B + 0206B701609343E2121E1A208C7730F0178F60016007BFA629079E32B580AF00 + AF40B020B024F8DBA6B20F3073B529BB99A33D9AA6DB3C6CE6411BCBEA348723 + D55C0F65089C3D807D5FE19D47D2D412321AE8CB7CD15F14DC8A87EF41B31D4A + 5376D9DC71FB8112FED9E6A6EC0D0CFC25BACC169B776ECA409779A9A0AF5E56 + 04FE0278B149940589C9ABEC32DB682BEB0549CD99E37C1A69AE3D7A9DD9D65A + D9BD8099976F4F2357DB9C7D95CDD58B98CBFB0B9A00646BA3F9FD485EEE5CB1 + D9D22DBE1F70F5F26700BBF2FA1DBAD89E8F4CB24982C6006079018F5961C76C + 080F5D81E06E84A7F0A079F6CC661A3CDF9EC182A8439E0710873D7E5DB3573A + 98252C3460975400D8EBEAED4DDE7EFEDA347A399F79431A307745EA3F6BB98B + 30D0EE715C402FDEC28C2578ACB13B41E0A67D800798EA80B75E96287B5D1100 + 577969B0F5535D1D0237C15E04D075AF5ECBC776B2292F18CC7EC002B9D2007F + 69BC5546B491EA60D75F881F3DCBD3A87ABB4E1EC082B72A37497DEA6DBBEB5B + D5A100783BC9C16D49A3EA6D2487BF358FE0A63D80EBEB5C2D98DBF20016A06D + 5B1F2BF6DC36ACC7159BF2511ABBA90EF938432DAD29AE654ADDAD85A25C3FDE + 34CED51DB88D7551F576518DED7ABB07306BD496B246AEF573D6ADEB6BF148EB + ED6A1BFB44D5FBF5A47A7FA9DEAE890FC4726FD469ECA8C8237A52A7FE31DFCC + 54DA2598DA0454FF5CF430E718D995F2D156A9C6B250BD6DBDDCA40C80B3FA59 + BB9852D7D4074EE5E3DAE7E96FBF99212326A71163A6A741C3C6571EC282BF7F + 7200FC5BB2CF08657F4B3D0C00F6FA47069B7E6300FC4BC6F32F52E72F654F8A + 5FECEEE4EDF543A9FD58BA948BBC1F3FC45FE582922F769DD4BAD035D7D5D5D4 + 4E6A6A5F57FD02DED497BD0C86D84D1E104CFBDE8EFDCBC571F2B1382E9EB80E + 6781BF11AC5A8A5DAAA0AEF601AEC9FB475B19ABAD5CD20AEC0AB6A24156CF24 + A5D4455563055575718C06354E78389654AFABFAB6C3DF26C5FA0AE436B4A74E + F5CAD7D5046651B40900C7496954D5B64C10EB93C6D85793D8DC8FBC4DAC01C0 + 43A7DB67B0314C00E0412367A781A36CD23E8A892C107A42EA6FDFDB21E366A5 + 81A3ADED50FB4C36C6C0113352FFE100E416006E82C04D005880B72E8D11C170 + 84BFDEB7A67C1CF200607B801985D7EF5C579FD13C002D743DE2FBF9CE4E8327 + 2DF230CA84812634B4406D04B711DED6616F778A63699C7EE3E66788CB03989D + 2BE7E00F72805C3B0F07BBA60880FBD9C3A37BDE0270E94B9B329EC360FA1509 + F6C632E39007F00A14936F01607B082E1A683684E7AE5479F216BB970BC4CD1E + BD964EA56E89E579982E9A96C16FAB5D06C6191AF3400EFC0D00D83470066986 + BA02BC405DE02F605776C15EF2D847CC5FEF6DB2D6A7910B36996D631A3A775D + 1A328FBDABB2862D00E26E2C619F01C09B93C3DEC53BD390C53BD2A085DBD2C0 + 055BD300B30383111098F0CF43E60372B307704F0202037BA50880472ED9EE22 + EF10B8C05D857D06F8A20880DBF2ABF22291169424168C625D84BF02C05AA462 + C14A8B545AB072DB861C0A4F367937200160F2635808537BB3090047B9077080 + BD2CDAB12827EF0D2DE6B50160CFE3198257EF932E016041E0C9BBC9B7424003 + 7C27ED3AE4A97B01EFB3FEC5F317088CA6B0279D03E0A7D2141B73EA01C21D12 + 0A9170CDAFA4A9475E4C538EBC9416BDF861DAF6E9F9B4E6ED6369167B000366 + 1F7F294D3EF8AC2BF7B1B6079E494B5F7837BD74FBA7F4FAD77F3F6D7AFF649A + F1D82BD6F6D534EFD9B7D35C3C788A87AF24102C088C04754923EC150416E495 + A7AF52415F8160016016A6EB2127E5858494F7BD7F81B901F46A9F5F16B94929 + 635FF4DA0769E9DB19F66A515C1E558BF19CC2E3CAE41E5BEF7E9617D381C1EF + 7FEE613BE7BFF9615AF8F6C755384FBCBBD6B0F0FFE15715205E61B6D59F9E4C + AB3E3EE5F077F52727D3BA2F4E3B0406003CFBD55BFFD1877CEE4984847EE5EA + 670E9CB2D7EEB5CA0350B0575E80A49B814157096B7BC3DBCB13112095E11369 + CB63D1DBB2AF6A01C03BAEB7423ACBA3D73D7F2FE6FD790587E5F92B4FE06CCF + 63007E01B00E7C4BA8678030E1A1F7E0A10A3C748058C206DFBA97B4BF2B1EA3 + 78D30AFC66F89BBD6B1DCE16D8B9E76B3C76EF16A868C705D6D9D878C1E631B2 + E7B0EBC1772EC15F52C6DA7AF38E8360D90478C90BE2025901C0F4F7F0D0005D + B3EF3509BC22FA311EE3566DACCFFEEF7E48877EF89DB5F929ED796002F4E251 + 6CE57D05FC0A02E3497CF0EB1FD38160CF61A66D8C6F7E9F8E7CF757E9E0031B + EB6B6B5FC63DF8BD8D53CE21FF5D38AF0C7D81E5FBBEE53CF83C0FEC6F97416C + AEB7BFCF03F420EDFFC6CE93BF8DFD3DF108266434F057A1A301BDA47804E3C9 + BBCFFAEFB3B1E4592C108CF6526FC7E5781C47120026555D2EDBD8847E2E024A + 3BD4BF75D3F2B7AD1D76FEDE3F38FC05026FB7EFC38E3B3F58FA43DAF5CD1FD2 + DEEFFF3A6DBBF753FA7FFC4FFFA160DECEFFBEF8DBBF75E8BB2B78010B00B31F + B180AEA0AFC02F8A9EBF686700C4F9F7C66FF4B203E0566869602FBF3FBCEE11 + 65207186CC39BD66F6EC859FC3B0E7173248058201C3D9533FFF76B7DA6F78E3 + 05F6F6BEE030986B015A7FD6EE53A7EC1A79EA4CDA788690D017D2B68BF6BBB7 + 74D3596BC78B24A74933FCDD0008267AC0A9CB69E3E92B69FDA90C7F119EBE4D + D724F4CEF977D2BA93E75DEB4FE5F100C07801E3D90BDC95A7AFEB0BBB7E7F7E + DC459DD717504C9F654475F8E4B87B0EE3455C0F0B5DC160BB2770ED1724A67E + C5C7848DA6DDA9B4ECC3E30E8181BE2B3EC83078C9BBC7D2D2F7AC1FF7106BB7 + DCEE41DC63F402D272E06F81C04BCDAE7BD5E2B277302F32CD7FEDFD34FFD5F7 + D28257DFCF00D8D2F92FE77B24F746F7063639F4C543983D80695FC0EF9C17EC + 7E1C349BF479B663C802FE3A007E91FB332F6AB13FF01B9E0A06CF7D867B7DDE + FF37EE031C81B0445920789EF5C303388680060CCB4318F8CBDEC10A0D8D2730 + 9EC1F2088E5EC0D30F038333DC8DFB0003779B6C8817ED2495B52D072FEA0180 + 7D6B0EB3B5E66FA8F5A25FF60426EA0BF33A22C194B91EF93247CCD163B2E7EF + E49D39C50318F83B66D36E0F01AD8832A46336ED4DA3375267F3D73047150C46 + 7A7191BC606F84C45204C21E369A36966F9B47531FE6DB9E67BEBEDAE6E665CE + 4D8AC7F08855366FB7B49AA75B3B0160BDDCA9322F7F8E5E91D391CBB654B017 + 2FE1A14B37A4614BD6BB0DE82B002C8F61EC80DEB12BB7A6F11C6FC5962A24B4 + 24C82BCF607907038A116D48EB9EC32A7702C0510E75435E21A115FA5969CC13 + 4EBAEFAC65A9CFACA5A9EFEC65A9DFECE51E02BA82C0405E5384BB7A0EAD9709 + FDDCB293620FE1A003001E04B80D6A82C088763DA90E7FEB40586037C25DF2B1 + 3EDA559607701D002320301A38D18E675239C26081DD4EC0B7AEE8015C87B49D + 206E546C8BEA20596DEAED1F060037A9539B36F02B3500E0BA04690580EB6B6E + 942300962DE6EB6AAA93ADAEDE00E0A636526CE700B8ECD5DB05D886FD7B635D + 94EA9BD4A5ADC3DF312652D693433E0A3B6BD68372E8E70880B57E1D55B73501 + 60F57D18C5FE51F576F575FEA6363F571AFB170BEE5294C7CD638B0B34F395CC + 69BAABFF7390CEF161CEB3CEB2A48A5535312D53531F546F27350160E523008E + E35429F2FF3B4BEDB3F5E3FBC7B917F0FB2707C0917D36F1CA4EF6DE2A42D9DF + 52BD0D01DDC76C7DFB027E692700DCF5C47FD17F4C777FD0DECABF549DBF948D + AA7DD9F585EC4E55BFD0B78BBD61DCA61FE4CF133FF8CEEA7411951DE962ADB4 + 53BB871663F867ED7ABCFA5E06B1FE61D5FB9B0DC709C7EA1FE02D1EAFA63E83 + 6C8200BC04B252573C812507C280D80076BD5D13A0950DA83A28E44DBE972FF9 + 18C29936961282B9CB184DA28D4F60C2184171922375D7A665B77C00BC522C7B + 7DF012AED7913ADC55DB9AB0E305EC9EC036A1AC006E00B5D156AFEBA84E13C7 + DA382D083CDD3D80FB0CB1C9B10B0FE51969C0709B940FB532E7689358C2DCF4 + B789F18051D31CFEFAA477988D330C6F612BDB3895177101BDAE028105680578 + 2B886BF90A18973EE4D54EEADF008123FC6DD980BBF6A04218E7D173D323A3E7 + 5500F811C240E3116CE5016309C3BC2043D83173CA8351F6DA8DB037825C95A3 + A28DFD7D2507B826F7D29D600F7082B196FA0399E5F100F68737B36177100CA4 + 9DBCC8A1ED23F680F6080F7CC50358E3B4C96C11FC0AF2AA2C1BDEBDA8110093 + 06D8DBA401C0614B23D4ADE0EE74B3DBC371FF29A6B20F130058F5847C4603A7 + DB03BB69E00CF67FCADEBDAD70CFECF39B4130F641563F70B6F59DCB62C2DACA + 8BD73D790BD0C53674FEBA346CC1FA3464FEDA34D401F006D7B079EBADDE6CA5 + 7ED842DE76DF68E51CDA79041EBE0BB6A5017336A5C1960E5DB2D305041E8017 + F0A26D69D8E2ED69E4D20C6C1512BA67B58783AE7B018F58BCAD40E00C7DD1F0 + 255B0A00DE9246AFC8B017182CAF60C93D8303F48D12F48DFBFF2A04B400B016 + 9F58A472906B7916A9D04816A4288785ADBAEA8B6194B570569703605FA86B49 + B057E1FCA25D0B7B787C4CD9CB22E1938990CF19FA66018615069A85C1BC8F5C + CBEB77EADEC369DA7E802F1204B67EBE2069E3150F17F6EE05E4A2E9875F4C93 + 0FBD90663FF5463A70EA66FAEA9FFC9BF4FCAD9FD2D2573E7018CB3EBCECE3EB + 219DAD1F62C174FE336FA45D9F5F487B8E5D4A4B5EFE30CD7CE2B534FFB977D2 + 9CA7DF4C739E7CC3F45A47083CF349167133E815C88D1058805761211522526D + D52FD6017FE7BD90BD7F057F01BD84A6D4DEBFEE016C76F6F965115B5E4DD1EB + B7EE018C141E93D0CEA40A9B89F7AFEFBBF8CE270E794911A0D7437296509E31 + 9C277685F9545BCAAB58DCFFF078F1E4CA614159FC7FFEF8BBDD028AFF940404 + 7EFDC631073FF23EF47D7C2DC573903C8A700A58B5FD3AE5EC8D489A05708AE1 + 6AA9BF96B65F0302DF71E8ABBD6833C0CDC097BD6A81C2E4739BAF3DA55EC0CA + C33B174F5F8563F63D796F0095EF789EFD77E541EAA186EFDA5856DE0E44BBCE + FEB280C4BCBFAE422EEFBAFD6DDA6EE7C43EB03B0AA0DD75EF6B87B7E48198C0 + 44C24413DE78CFFDEC792B982BF8EB8037C05E80AD7B015B1EB02CC0BBFB6B93 + 8DB9F7DB6FD3BEEFBE7391DFF38DFD4DCCBE0B0F5B13F97DDF7FEF759477DCB3 + CF0AB0B4767B1C3A7F9B0EFEF0433AFCD3EFD3FEEF7E97F67DFBFB74E0DBDF79 + FE8069FFD73F569F11D80B003EF4CD4FE9E0839F1C000B80933F6C7D01C0871E + FC3E83E3EF7E488FFEEEAFD263BFFF7B0EA9F3DF84CF9C21EF1EFB0C8058C1DF + FDDFE2358C77307F1BEAF9DB642F60F2FBAC1D6198F7DB67C2FB17017B057E81 + C194058715B679F79D96277004C01C37C35FFE8F32E8157C466AC3F1058101D2 + 02C0A4EACB67433BEE3EF030D09B6FD8F7F0EE8F69D7833F38F8DD78F35B4FFF + FD7FF85F0AE6EDFCEFF3BFFD1BFB8E5D4F3B6FDEB0EF1D21AFB317B03C801D00 + 9BEA0098DF933C83F9EDF0DB423B6E646FF60C8EB1156FE25BD9337D0BFBFE5E + BB9CB65EBF62A9FD6EAF9AF0FCBD62BF4DFBCDB15F33DFFBADF6FB21E4F9D6AB + 845F07F0E69732B4C730F0172F60C2B6737C8E47CAEF1F08BCEEECF9B4E9022F + 85102DE06C5A6FDA6436C02FDA627607C0A6B5A7CEA53527CF5B7AA18498676F + F16B69DDE9CB69D5F1F369E5F1739EFFDFDFDFD5783D428F9EF9C2E12FE077D3 + D9CB9EE2C90BCC05EE028101B6005A00F0DA63272B017ED576C5672772E8E8E2 + 398CDDFB121EDAFAADFAE4443586C35EBB4F700F58F3D9494FF10E06F82E79FF + CBB4F8BD63692991223EE1A5A193D9F3179B89FC9ACF4EA575C067EBB7F4832F + 5A00F8BDCF7DCB8195EF7EEE307899DD8F969B7D99D57BF40ABB972D040A97FB + 1DF738E45EC12FBFEFF7460F05FDC6C7391C34F7477F71EAED34E7C537D2ECE7 + EDFE5C07C0669BFDDC6B0E79175A3B04EC8D0078BEB59B67EDE63FCB4B5F19FE + 0AF692C71625E8EBE0B7B48D00581EC15182BF930F3F93A698C8E3091CF7056E + 0F09CD3C2903DEBAB4377004C011FE4AEC078CA61400ECDEC04540DD16E0CD2F + F8F9761F3B1EF5F9E1945DB9BD20B1E67B82C0792EC90B85CC1F996B027C8932 + B3278DDBBCD7E79F13B7E53925916780BF7803FB4B895B6DDE69F59ABF562F2C + 9A280B0A37C161E0AFE6CABC08195F9294EA2F58FAFC7AB5CDD32DD5FC3BDA91 + CFCFCB4B9D11003BF8359B47F6F1E700D22C00AFC2467B5A4240CB439814CF61 + 4160806F04C078D5C67D810582E5191C25082CD8ABB2DA0A00CB13D8F70D9E97 + 41EF70BC7D8B9AE0AF0070D460EAE7E2019C41F280B92B537FBC7F67B7BC7FD9 + F777E00C7B6E9DD91E025AA0B71DF6B61401700CFF1C434023F7DE2D8A205802 + FEB20F302017C82B6F5F499057A0D7C12D6937A25D1DF2D65507C228025F415E + D904800581EB1EC0BD01BF1200B8FFD896376F1DDEF624015D4963D4EDA8EA53 + FAF5EB05004611F046C57A006FBDFE6100700BD2B603E098760780559628C730 + CF51B14D65EB01EA4AD8FB0DB3FE0D6D5BEB9FAC798E73B5C1DA0072EB7551F5 + 767535B5EF12F6591A30A62D9507306BDC4DEBE871ED5BE54E8A7D1F46BD192B + F2827A9B4EF9DE4A63FF2CC11FEA6A6B33CA8E8172BE49E2064D757F72F51BE9 + 698B6DB4CEB51367AADAA9BE8C518972E0554DA26F5D5DDAF54123DA00B0D4E8 + F95BE364598C9BC7EF6BFF5F7DFA8DB6D4C618D003008E0CB13BD5FB3DAC1AC6 + 6A3BA75F7A9C08657F4B0180BB42E076F82BEFDFB6FA5FF0011FEA3FAFDEA65B + 357D917E9EEA5FF2BAAAB6E5CB9BBFA4C5CE971FBB54DAB6F58F3FBC22FD80BB + FC301B45DB5ECACFCB643FA2C6FA0E6ABA28F756F53162B9B73783EEDAD5EB28 + 4B2D5BABBEB239C8B59BFFC0C99E3E32C02601454060BC83057D1DF6E2FDDBCF + C6ACC15EAF130C1E6402A02A55DEEA2AA02B7B2FD409006BAC9EC68B9320D45D + 9B36BB1F37C35D41DDBAA8EB0480EBAADA4AA5ADC242038A15FE39867A466E1B + 6CFD2D6D9C88D66513C53CF1B4CF1C278E61CCD6D82D0FE01CFE194FDE5969E0 + C83969C088D9D90B78D4ECD49F3D7D6D42CC047CE06860713E27FAF4B7367D86 + D8045A9EC405E63AD02DEA0480FB17A05B790AD3BE006087B9A16D1DF44A959D + B14CE4FB8F99630F15F3EDC106EFDE05A9CF2860EB823478A23DE88D078E2E4A + 43262FB5E35B1BFBBCFD81B5F610D567740EF58C47700CF9DC09FE6661CF8A6D + D51E482B70EB60D88427EF2093DA514FCA3181B943F0A6B58735423BF5196FF5 + ECBB3B2943615779B88B10388AB69280EF90A9005F1B179BEFE56BF9E2D58B04 + 7F49874C6D297A000381FBD720301EC003A6F1C06D0FD0F620DDDFF3F677B6E3 + 1102BA02C0D301C0D67E260FFC3CFCB3088027EF5ACB6739049EB5DA1EF057A6 + 01B36DFC39841A5B93069A1CFE5ADDC0D9AB1CFC660183D7A5A178DDCEDB9006 + CF31E129EC7078ADF559E7290078F862E0EBD6347CD1F63472E9CE346AD99E34 + 6CD1AE3468C18E346AC5FE3476EDA1347ACDC1347CF9BE3474E9AE347C9969F1 + 0E6BB3DDB42D11FE79987B01E7FD81872F62AC965A60779BE769D3B2D918D60F + 911FB574471ABD6CA7877F160816EC95C76FF40C1EBE7C9B6BC4CABC40D44980 + DE71EB76B7036016AA7A2187BA2C5E6DDC9B17C62C4F9867F7FC35611BB3416A + B5F3B6D4B340C6225B08F52C09F446D88B544FE867CA84FCC3FB23877AC64BA4 + 0E7FF10A3E6A690E1DE80B83659151DEBE1EFA797F2B0F049E76D0FA1E7C2A4D + 3BF48CA5CFA4E900DD23843E7CC1BD5D00BCB39F7E2BEDFAF24AFAE00FFF2C1D + BDFC202D7EE9FD34FBC9ECC92B004C48670FEB6C62DF5E402FE018CFDF394F01 + 62DF4E339E78D5FA58F9C91CAEB9EEDD4B397AF3CE7EE675F7D8259DF5B4B527 + F4A3E57DAF40137077C1736FB9E647E08BFD85772AE02BCFDFE8FD0BF88D1204 + 5E54C0AFE02F12EC45F3498B071412F84578FE2E7F3BEFA5B8F8AD8FD382373F + 72F8BB180F5F3CAE2C55E8CECA7BCB4499457DEA00C11108D36EC547D98B8B05 + FC551F9FF085FFBF4BF0570202BF7AFD0B07508028415F20B03C7A5BA16873FD + 962B80E00CA52200A65D0B5A019AF2DEA38482C6E39710C4871E1082F8DB0C6C + 3DB4F383B413201CBC6B3D2CF32DF6752574F31DAFD7FEB2A4DB6FDC361B76EA + F3BEAF0E7F4D31A4B083C37B5FA73D00DAB237AFEF678BA7E76D4BEF7EEB29FB + BF3AE82C0076C75DC0E183B4F71BC233D38FB1EFA77D0FB277EE76CB93CA0318 + C9DB17AF52F2DBDD63B7405BD3AEAFAD0F20F2FE3D2BDBE7FCF681DB05812953 + BFCB8E1BC170AE03187F9B0EFCF09DD9EC7CDD7B1520FA7DDAFBDD4F69EFF77F + B0BABF4A077FF88343604219EFBAF75DDA6D9F75AF8970D507BFFD311DF91608 + 4C4868EB77DFFE1FBEFD5D3AF8DDEFD3FE6F7E97F67EFDA3EF29CC98477EFA83 + 0B0F653ED34E1B630F5EBB7E4C426513F63A8358CAFBED7C3D04B4FDAD68B3EB + 5EF69E167C55BACFCE7DFFB77837DB7915BB60ACEA91EC005CAF03247B1FFEC6 + 82BE5902BD1A87F191EAB1319E0030FB01EFB7B1A8DB7EFB76DA66DF2187FF5F + E3F9FB6DDA6E7F9B5DDFFC3E6DB7BFC7963B3F380CFEF7FFE1FF53306FE77F5F + FD8B7F6E63DB77EF4E3EC6DEFB76FC3BF61BB879C3C34EB387B1BEAB7C6FF94E + E9FBCB6FAC0580F156CFFB14EFB865360F436EBFAF6B97D3B6EBD7CC7623EF61 + 7C933AFB2D5E03005B6ABFBF2DFC461987DFDE0DFB7C78D9F3A205A99537FBFE + DBB7D3F6AB8458C70BF846DA7299B0D078D8F39246F638CEE7925FE220ACBB00 + 30DA6CF9AD1749CFA54D67CF26BC81D91F789D69F549C24493BF9CD69FB9E2DA + E000F84A5A7BEAB26BFD99ABE9A54B5FF875A77E2DFADBBB8F9650D2174C171D + 000383D9139890CE0A0D2D88AB3D813758BA91142FDFAFCEA6E59F1E4F4B3FFE + D2C3476FBF607FBF7357DDBEE4A363EE11BCDAFA7A8867BB4FC47B06F700C6F5 + 1786ECFEB0F4A3136989DD1F16D97D6221F78A4F4EA683D71FA4A7EEFF3EED38 + 73BD00E22FAC2FF70F00B0F5B1FB0DDB082CFF30DF7796BEF7A9DD6778F18828 + 15F95EB5F4BDEC29ECD12C00C0A62525CA055A62F73DDD23DD03F835BB1F9AE2 + CB52735FC0FBD7EEDB2FDA7DF995775CF35F7EC7CA80E0D7DD3B78E1AB76FF35 + 51B71015182CAF603C84673DFD4A9AF994CD154C75003CAF848A16F405002B9D + F1C48B151006023B08B6F274E0AF2986866E82C302C1D103B80DF09AC8CBFBB7 + 1300D6BEC0E47D2F6000B0E5A70181D91318591E2F60443E035E3C7FB380C1D9 + 6B38CCF30A2C76605C5E209C4488E80280276EDB9FC66DB1B9A869C2B656E419 + E69C7801A338FF04048FDB98E12F405850B80900470900335FA65CBD0C69F9EA + 45496CB46950DB9CDBCA40E00A10DB1CBEF2F62D737A41617903B73C82B756F0 + 577B080F5DBED1530161416087BF968E5A8147704E09A73CD2DA8FB6B62396F0 + 7CB4CE9E89ACFD321BAF8487AE03DEE8E54B3E8261F2B1CEF37801CFCF30D8F7 + 0B9E6FCF70848E2E7B047BF8E620D978F117F9FEBED8ACCFC0B96647735A7583 + 799604FEE2FD0B48AE416084AD0A056D0218F39C2AF01BD506800BD01D0C44B6 + BCBC7D1B01B0E5232456DBBA68DBAF1710983602C14D121C96047D23E46DAD11 + B4AB09EA3E8C32006E179056696F1401AFC690AD2E6F5FFAF41BD55204BE75D5 + C1AE14EB057BEB6D6447ACA769BB36D6CBE23A9BE0AEA4B536ADB7457B04B728 + B66D92DAC4B6B1BF1481AEA4F54CA95EA735CFB6F54E1C9186740F7F51537DB4 + 45F554DF8763B22DE0C0007E512D1F0130A092357FF8006BDAA8DF20D6BA59D3 + 6ED9B2B40E1E6D5DD7C17F2DD5D7F9E3FAFBCF51537FD974AC8755A7F1F2DF85 + 3681BD04616BB2FF3949E718D5D4AE572A9C4A0C2BB22BE523C3AA5896DA9476 + 0E7A4B2ACFDFBAD7AF44BBBADADA50C64EFFFED6BE133BACABCE11EB6AEAF3B0 + EA69BC6EEA7AFC1C11B8FE5612FC6DF4020EF0576AD5FF3200DCA334F6438F5F + FBF2FC42D5BFE85155BBF285AD00B0BEB045AA7735FDE87EB6F2C5EDB71017C7 + 98EF49F986D37EE1AEF7F7327F9F5E8EF9B0D2392804467702A0E2ED8BFA0D99 + 92FAE3ED6969E501EC20B4805741E0087C658F00B8A882B3B2B54D387A5697E3 + 07A9AE6AD3D01FD527419DEABB4C88FCB82D505B41DC5256BE496ADFA641D6A7 + 0100A3CA4BB8C0DF8705C04D1353F7D635B54D4C43FB96B207F08091367907DE + E20D3CDC26E940D161332B004C9D9F874D8AFB8FB40938FDF8AEE0455C3C7F05 + 7D25C02DA9E06E05834B5D05704B3B6FDBC103B80DF4D624BBD78D9EEDF0D741 + F06800EC423BF65C1B634E9AB6647BDAF3D4BB69FB136FA6490B36A781E316A5 + 7ED459BBBEE3F0FA2DE05702040708DC15FCB6D47A40CA40378263EA81B4B95D + 2E933E626D1EB1CFE6D0B78CE39EC75637008F5F1E24A72E4A7DEDC111C9AB37 + C3E4F850670F7B05FC46002C8F5F09008CBA03C09D84E76F7F6BDBCFFA906A5F + DF4A7800F3001D1EBCEB7B000F9AC6C3B7B5F507763C7F81C0684D058087026C + E7AC4903CC3E90D05F961F0CFC9DBBC6C1EF00F70A6E0160A07006BEEB5D83E7 + 027C09FBBCDEA130E9204245CFE38DF54D79EFDDA53BD288253BD3B045847CDE + 9E462CDF9FE6EF7F3D3DFED9AD74F4F37B69F9E3EFA5316B0E7948E8214B7638 + 08CEEDB739C005F62A8D12D84502BE00E0114BED18A6E14BB6B9DA007080BD11 + 00D7ED02C0233B0060168910C03702E0B8D8C4A29416A7587062F14936EDE59B + 3D1DF6A70879E5FD80143A2FDA100B67E3D96F6DB395C3625B84BEA475182CB9 + 57F0AE96A787806F04C10A01ADB2A7C5BB37EBF10C7F7DDFDF2369E29EC32EEC + 2C524EAB00309E2D84737EBE02C2C0DCA9475E4AB39F7C33AD7EFB585AF6EAC7 + 69F6D1D7DC5B17900B309EFDF8CB69FED36FA43978F0E2395C80F0D447D9CF17 + 6F1DAB7BFACD34B3F4AB835ECAB211B6B9F2E825847301B782BE92EC0B9E7F3B + EFEFFB1CFB06BE9316BDF86EF290CE2F957D7C6BFDB029ECB300B1B7C30E042E + D01709040BFEFA42377AD5EA5EFFD025102CAF283C801121A0058081BF78F546 + B1884F1867EDE74B394260DAB0B82F38ECE50F4FA4359F9E4E2F9EFCF0EF1CFC + 9580316FDCFCD2E15384C080206C4021EC2DCFE02BEEE9B80B6FC46B782FE225 + 9CDB2AAC6DA51BD98377E78DBB69FFBD6FD2E16FBF4F871E7C9FF6127ED94341 + 676F5EF7E22DB012000C70458061E5F7B837EEBD0CD0D8F3F50E7BC866109C41 + AF8D77FB96D5DD4EBBEFDE49FBBEBE9F0E7E8B67E983DCCFDA728CDD761E15F8 + C553F6FEF7AE7DDF7C9776037001B940DD6FF00ED5B93C48FBBFF9C9DB138E19 + D0ABF0C878CA0AFC3A10361B2017582B4F5F602E9EBDC05B606F04C008B84B3D + DA1FBC7F6923107CF047EC1954034CF104DEF3ED8F69FF8F7F958EFCEEAFD361 + 4BF77D4B48684241FF980E7C93F7E0DD7F1FEFDBEFD3A3DFFDCEFEFED6FEEBEF + ADEEA774E8FB3F3800260CF2AEBBF679F118B6BFC1E11FFEE075794F614037D0 + D7FAD9FFDDC1EF7F48477EC2D398CF08C8E61C80C4F6F725CCB37D9683DFDB67 + 2A3097F3149CA57C00906DC22E80ABFA28A02DA28FA0AEC68A70B75EA68DC657 + 1DC70048ABDDDE32F60EFBFE6CBF7DD7FFAF81E77BBEFD5DDA76F707D3F70E80 + 773EF88303E0DE84803EF9AFFF550300BEE5F097EF24DF511401701B04E677E6 + E096B6371D1E0BF46EBD0EE8BD94B6DDB8567DBF1977EBF56B01FEDE70EF5FC2 + 9DB3E7F5B66B77ED7779D753ED7DBDC5C3ABDB312DCFEF8DDF65F6B0379BFDCE + 78A903E0BBF1E215FF3D2B8CF5A64B8486CF9EC0DB2E5F4E3B2E5F4A5B2E58B9 + 0060B4FE0C7B075F4CEBCE5E499B2FDCA83C7F01BE00E0F567AFD918D7D33A2B + AF35FB9173C7D37F79EF40FAEFBED992FEBB075BD2B51BAFA5CDE72E67988C17 + F189736D295098144F5E40305EBD15003609003B242E9EC200E06DE7ED6F63E7 + 24308CE44D4C68683C7DF100262CB4BC81B36C0CBB2FAC2802022FFFE4547AE3 + 1FFC8B74F2DFFC1FD2D1FBBF4F4B3FC03BF8330F15BD1C686CF713EE3500E0A5 + EF1F2B21A279F18888143932C5A277D9C220435FF603AE64655E70E27E87B8F7 + F97DB1DC270582F51215B6F980DFD7ECFEFABADD47DFE0FEC9BDF46D87C00060 + 07BF0502BB688F17F0F344F0C81078F63336BF00FE3E9DE1AF00F0BC027FC9CB + 0EE015FC45C06087C5D41778ECA1A2AD2EEE151CC12FE1A0F106C6EB17312742 + 0E808BDC13D8447EC691672AF82B30ACFD82057EF37CEB499F9F0908CF304D63 + BEB627C35D42412B1C34515D726417CDF7E4F90BF03D64B2B95C0C09BD531018 + 987B204D2A00180F603C813318CE9EC2CC39352F8D734E9FA79A9DB9EBA46D36 + CE569B9FD6C242EB0547815EC0AFE0AFE6C6E481C275002CC0ABF97704C0CA23 + C15F4241BB82D7AFE6F5E4B145003CCADA027AD1A855D8320446C05FC241E311 + 9CC16F96878B2EA1A21165B72FCDE1A111F958AE7B040377E5E54B39B61124F6 + B6941710812900E0793CAFB5C3DF58AE03E00C7AAD5D81BF559FE8295CC0AE7B + 1193DAF325CF9FF2FEA55CD967D8736C7941B90E7FFDF9B5C103380260015ED5 + 0BEACAA6FA087DA30477057A6553EA79D2027623F06D02C0CAD347F017B12E10 + 5F4C8F72885B52E59BEC9DF46B7A0037D5A3D806F8DB6FB429C05F14816F5D75 + A82BC57A41DE7A1BAF1B6176AB5394BC56B4BCA92E2F17C01BD7D91CCA0E9FE8 + A9EA23BCD55A5CCCAB8DEC75755B17E0AE14D734BBAB6B5BEFAC0160C1DBBADA + 006E0FEAB10FF5030BFC4503C67455A9EB3B689C87BE6D5AE7969A6C75C5FEBF + B6EAEBFCBFE5F174ACDEAA698C965AE0BC82A635C7BB5F0C54FF08D2394635B5 + EB8DDAB8168AEC2A2872B0C63E857DC9EBD781B0EA6B7246F6C870572333533B + 8EF5E70680517763351CAB37E7EE6D2290FD2DD409FEA2260FE076FD0A00F897 + F6EFA2F8A5A97D297F81EA5F76A96A53BEACBD02C0BDF2ECEDAD5A17BA5F4BCD + 17CA66C59B4BD38D46E3C91EC330C476BFA6385604BD0A379D852D9F6706AA13 + FD2DB76163E7A6D19316A511E3E7A7414039A0A4C026ED047A23F095280FB2D4 + A59B7AA9C3D636D9E89D2AB81B8F5314E1AFB769E88FE284A769E2D3C9DE1D00 + EE648BAAD7B7B52DE3765181BF92206DDDA649682731398D00B85298C822C1DF + 81A3663B00A68C47B07BFE9AFA0CB2F108073DC226E4C37388E73E2333C405CA + 0E005C026E1987D4EA04722BA05B1401B0974D15D8B514C57EB2A94D4F72006C + E7D37FF49CD47F9409083CC61E82C6E2FD3BD78E3F3B2D587720BDF5E5CDF4F6 + 5737D2FC957BEDDC69373F0D1C670F5280597B6872001BE06D2C0BD24A6AD312 + 0F505DDBD4FB036C07D9EF6CF8F4E59EEA18DADF17004CE8E73E3CE0D9C3641F + 7B3804000BFE66E9ADDF5C8EF057E1A001C6120058DEBFAA1B58007013F08D62 + BF5E002E20B711FEA2E9CBD2A05966F7905ACD00D8BD807DCFDFAC813300C23C + B8AF0E0078AD3DDCAF76F83B88306178EFCE5B9706CF5B9B06CE5D55BC825944 + 0000E3E5DB02BE02C082BFC31610EEB97804CF67516243F60006C42EDE9606B3 + D7AF69E4CA0369FD735FA46BFFF8BF4DDFFD57FFF7F4E827D7D3A44D47537FAB + 1BB48885967D69CCAABD0E71872CDC9C86D918BE2FB0A5486057A057521DF951 + CB77BA462EDB51897213001EBB62A7877AA60EE01B356A95D9CBA2505D39FCF3 + EE3466ED9E468D5DB7378DDFB03F8D5B6F9FC7DA511EB711988BD7434B63CD36 + C6DA8D5EDFF26E60018CC52E85CAD362996C9459449B801745596093A7058A8B + 7182C0F57A00308B838477CE9EBE5975003C915081E45930A41E15083C791FDE + C07500FC68C203588B92EC6B9721705EE874086C9A78F0B934C904D84540613C + 7D673FF1529AF3F84B69FE53AFA6B9475F36DB8B69EE93AFA639475FF3B0CE84 + 889E6EF5C05D3C80673DF58697F1F805F0C6F0CD02C2EED5FBFCDB5E1741AFC0 + 2DA96CD4037D17BFF45E057F3DF4242017EFA3D2DE3D7B599836B9A7EF8BEF78 + 48E8B9350180057AA3B05775C0DF027B7DAF5F3CA00200CE5EC09FA565EF7C96 + 96BCFBA9C3DC652CB617F9C23BE94725DCB3E02E9E5D9F664FAF1540DF52475B + F2CB3E040E7F990E1FFBF8EF2CFC95FEBF3F2E4EAF5D3FD6E6F92B65288CFDB2 + C3A02D57AE78385A003029902886AC9508734B18667904930746EEC35BF65600 + 5078F9DEBCED009770C0F2DE25F4B23C77779BF6025EDD6E6D1DB2595D80BD2A + 53B7E7DE1D2BDFF190C2FB1E583FDA5BFFBDEEA9FB6D057F819F19FEFE940EFD + F00707A8593FA49D0FBE4DDBED7C00A3BBEF7DEF9EB2A4C0633C63DD03F70121 + 907FF05436802E2037025E40A900EFBEEFF0E805EA92A7CD3769FFF7D90B18EF + 60E5777D6D9FD5CE5D691E0720FC753AF803A0D5CEFDBB1FD281DFFDBDF4E81F + FE7E3AFCD31FFC5CF63D601FDFDFA7477FFC7D3A64E773F0EB6FD2A16FBF4F47 + BEFFD1D3FD5F136AF98774D8EA0F7E8FF76FFE1BECF9DACED5FA1F301B7F03F6 + C6C576D0FAE1117CF0879F1C4E1FB0E33AF8B5CF77E0076BFFE38F96B773B6F3 + 3BFCBB9FEC5C7E970EFDC8DF3403E008732947384B1A41B1206D84BF005DF2BB + CDB6F3FE5D177F138174FE36127F1BFD4D29EFB0EFC1F6BBF61DB3D4CB0FEC7B + 67DF8B3D763C424713EE1B008CE7F45EFBDC3BEEFF90B6DF630FE0DFA55DDFFE + DE01F0BFFD1FFFE782793BFF3BFE2FFF7981B6005A1B9FCF61DF45CA7C97F96E + E7EF6EC917F98B0905023BFC2574F46DFB2DDDBA6E29C21318100CFCCDE36FBF + 79337B063B00CEF0770BFBFA5EBB93F0F8755DBB6FBFD3FB69EBD57BF6DB632F + EC3B0E8077F0C2C56DFB5BDFF9DA8E4768E85B794F60C690C7AF5D0300C0FCB6 + 79C163D3E5CB6903A19EF1FEB5FCD6CB972C7F3E6D3A77366D3E0F18B6F2F9AB + 69E305F613B6712EDF4A9B2FDD4A1BCF5F77E8BBF6F495B4060F604BD79DB992 + D6E21D6C759B2EDC489BCF5D499BCF5C4E1B4F5D4C1B4E67017A01B978FE027E + F104C60ED04584991604160846EB8E9D49AB8E9D4AEB01C52687C700E312025A + FB022B343490184F6152FA0281577A68E8AFD21AB3ADFE8CF2A9B4F2B3336919 + A19F3F3D9D9EFCEEEFA737FEE1BF4CFBAEDEF77BC792F7B9F7705F21EA447CE1 + 280360224F2C7EF7A3B4E89D8F33002EDB16F01293CBF2DA13B8EE0DBCE48D8F + 1D02B38D02F74900F092D7EDDE58EEA18BDEF8C064F9D7EDFEFC9ADD4F5FB57B + F0CB76FF7FC9EEEB2FBFED00387B06DB3DBBD8E65A3AEF05EEF16FD9BDDDEECF + C51358FB057BDA007FEB0098B2B7B336F2F62575105CE0AFC2406B1F6052C241 + 2B24B4A7363702F4CE7CF4D94A1106ABCC3C6AB2CDB92601782D1FBD81058B05 + 85B3C7B0CDBF98A7D501306DF653677D4C79CE47486822C2D85C71E78134A9C0 + 5FA0B087862E9A6CF609DB6DFE09F405020383AD0F00589017F88B34EF6CB359 + 7EE29676EF5F5407C048A057F057F35F80F088353BB2CD540F035D571D0E0B02 + 5700D8D41D001EBE1CD09B816F1300A62CD01BC3413BD8353B79E0EFD0C5EBBC + 4C88E891D6C6A1F0A2ECD11BE16F1310160C8E6DBB026013F9028123FC8DC25E + 07C31EDEB928C2E0810E7F33E8ADC24517F82B005C87C07804B7E0EF92D46FEA + E2D41F58DB0081DD33B80680057681B402BCAA8FB057752A47755727F02B300C + CC8D10B8B76AAD0958797CEBA57401DE8791FA36490078C0B8D9AE8785C00E75 + 8BEA754D63793B53FFD1561F407004BE4D6A02BB4875DD02E091965A1D91EF90 + D6CCB4BE86B4C6D67FE4644F5B90764286B356D6DA9BCA11E6C6FA68AFABDBBA + 1AE015E4ADDBEA21A0DBD73C599F1D67E51AA00D7A1800ACB6514D6DAA745056 + 058251B5869C8517B0D6D625D6B56339AE7D7792DAFC16AAB30377B02AF9DFFA + D83F470ED86138FD4D3DC0D326DB9F9BE2B94735B5ED496D5C0B457615A4765D + DA2331AF0E7D5DB1CEF282C0A4111A47F581EF15D6D76B08FCA7D62FE19B7528 + FB6B2AC2DF3A0076C01BE16F1300EEF70BC16F2DDFF41F2A5B535DB3AC8D7D51 + B21ABE983F537CC99B54B5295FE62EB628D91B7E743F5FF9C2DB939A2E82BFB5 + 74DC9E6E42BFF60D221EAFABA82FC71C6493814153D2E051B3D2D8294BD2E459 + ABD2C4192BD2704055097DE27BD4963D792B085B07C0926EFA3676BE919B0D45 + B8DA4B5570B7E138AAABDA34F4478D703748F55DDBB420AD606E84BAF5721DEA + B6D575636B83CC95576E561DEC46D5DBB6B52F1346A409A64B13D732A9D57EBD + C05FC23E7B79D8CC3468E49C3470C4EC3462FCA2347FD9CEB472E391346EC62A + 9B3832B166423EDB816E5FA0B1F5E9EB0038C3DA0871511BF4B5FAEE00B05441 + DD60EBEF70777655A7FA7EA57DF600B67AF6FDF5D41E62C62CF07D7F11A077D4 + F4D569F6AABD69EE1A7B689FB6320D1CB7300D9D680F7163AC0DFDEDA1297BD7 + F20035DF1E845A9057F036DAA262BDFA6B0C24504BBE8F7D8E490B37A6F5FB9F + 4BD3966D733BD0973A803070B6AF3DC43D620F5B7D80D2F6300800F6FD81AD2D + A058D2B811005780B78C256F60F700B6DF38C770203CB5E5F53B64BA3D3877D0 + 60530EF59CC3393769302097FD98FC41BA0580058809FF3C78A63DE0DBF545E0 + 17089C3D81D7B8F0FE1D01B89DB7CE3D7E07B168307F5D1A3A6FADC3E0FEF6B0 + 3FC0E461A109073D070F60C23CB740B0F21E12BA78000F5B60632E5CEF1079C8 + FC8D8910D02396EC4AC316EF4C03176E4B4396EE49F3F7BF915E3AF7FBF4EECD + 7F9C36BEF8651ABFFE8934D4EC78010F5B2A709B016D1DF42241E03A106E87BD + BB3C95283BE4B57115029AF2B895BBD2D855BBCA7EBF005FEB67A900B01686E2 + 0211CA7BFDEE4EA383047E81BEC0DF091B0FA409968EB7B2E7371F4C40DF311B + 58F462F1AB058147AFDF9B46AD6B5FF48A10B80E807DBFB5ED2D6FDF3AE0953D + D66157DD44F2BB08FF4C78C00C7F5910946748E525629ABCB7805F5305820B04 + CE22ECE063EE11AC30D0530F3C9EA61CCCF205CBC32C6E3E9F661C7A36E1113C + FDD197D3AC275E33DB8B568F8DFDF15EF2FD7EE73CF192B79FB4EFA8F799F7E4 + 6B0E80173C8B47EEDB0E7F09FD3CF799B7D2CC275F6F04C078FD0AFECA93D7A1 + EE8BEF2620AFC23E4708ACFC226BB7F8C5F72A4FE005A59E3A04F065AF5FB4A4 + E6D1EB2A0BD492ECD17B296AD16B1FB479FA925718680F8B59ECECA3983DAA3E + 77CFAB45ECF55B60B0002F2138B153EFE1380B180604AFFE2CEF1F4999B62CF6 + 6FFDEC8BF4EF1EAC6984A27FD7F43F7CBF22EDB97826291CADE0AFF603C69E43 + 3DE750B5950057C5BB91B2FA3AD40570594A19109CF747CD601808ECC21BD2FA + 03C4F0E495376F3B28CB402D2B0336006F4B405F1BF36E86BEFB018A96DF7337 + DBF6DEBD970E3E2014325093D0CE567FEF3BF772C563160078F8A7BF970E7CFF + 7BD77E60E037EC019BDB017EF73DF829EDB98FB76C0E05CD389E3EF8AED2FEEF + 320CC5D397B0CF005BC15252C024DEBE077EC8293078EFB7404BBC6933001610 + DEF75D0B700A0C53B7F7DBFBDEFFD04F3F7808683C800FFFEEAFD2A11F7EE700 + 181DF9E1A7F4D88FBF73E0CBBEBB87BF33DBF73F7A1E00BCFF9B1FD2C1EF7F4A + 07D933F81B64E31022DAC6386076BC98F93C00E1433FFEDEFE367F30FB8F76FC + 0CB4FFFFECFD77B45D5756A70D2B5A39CB5139E72C5D5DE59C252B589625674B + 0EB26CC951B272B472CEC939545140515451505480A6BBA90C54A00A9AAF81A2 + 49DD0DFD7EA369DE31BADFEF9FF9CD67AE35F75967DF7DEEBD922DDBF072C6F8 + 8D95F709F79EB3D75ECFFECD8580C1F32F5C9085972EC9DCF3E70D04CFD374FE + 8573DA37405B002E8E60943A7FCD89AB79EAE69D3D6329D017083C43C5DF10F7 + 36E19A1D1AA7B03705BD0E82A943E41DA2037E271DD6FF4FFE3FB46D9A7ED693 + 81C1F1B9ECB51CD77A9CE7A7CE69FB794D2FC874FD5CA6E0803E75517EF4377F + 1B316FE5C74B9F030087FF4FFF3F246F37271CE37F9A1B1FF4351FD3D7EA6E75 + FD9F47BE27F0F483FA1AF97F3527F1011D1B4030FBFC221CC038820320D6EF1D + 20783FEE611CBF474D93F7EB715FD7F77DE094E6F5FF77DF71FDCE1E36008C13 + 782AFB5DBF0E74060A1F94F17B70F8F21D0FDF6FBEA7A47ED307377C54EFE6FB + 8F3378A754EDD821D53B81C1DBA56AFB3699B06BA74CDCB547EB74CC2E3D1EEE + 5FCD57EF7A5DF3AF6B9F0306814971018FD9BA57C6EDD43E7B0EC9D8EDFB64DC + D6DD323EC2DD71AFEE32100CBC05FE0E7F61B3C15EFAD85EC09A0708DB1EC338 + 8223DC75006C0077D3AB593F5220306E5FEB1701B28360F609060067AE603D3F + 705EE05C01101EB5F13519B661B3397F81C0C3376ED5D7A4C7D394D0D0388087 + 3DCB0D46AF06C7B0A66C2DE0D125EC3C85E377ED33269CC1068755839E7ADEE0 + 7026F6048E101827B09DEF9ED0BA18FA9974C8631B0C005B548D879ED2F3E27A + 03C0067E71FEAE795C7AADD673BAA680DF010F3F6921A27DAF602030E5BE9A12 + 0E7AC01A9D133CC88D5D4F94ED11DC67251143020476F88BDBD71DBF94DD256C + E5087DDD090C0876F72F101801883D2434A90362E639656EDF2807C08876E650 + 387F81C094D390D0E4BB3346CBCCC7EE99C3CD770106E3F875F88BEE61BEA6F3 + 2AE670F7A898F7018883DB779EDC357DBE74011AEBBCD000700C237D373701DA + 9EC0CC271DFECE8DE5106DC65CBE2AF2E93CD4EB0903CD5CD6E1AFC3DE54D47B + 9BDF0C49DEE7BF1E12DA6E928CE037550A7E51511F20B0EF05EC8E6087C00E7F + 1DFC96149CBE005F07BF2E423EBBBB973C30185117C24197003100987EE6181E + 18E4503785BFF93687C00E8249710567EEE07E01FED606801DFE7A9BA7C0DE14 + 007BDEC23CBBBAEB75658108F78CBCDC02C5310E8151910B383880F51A3902DE + FA00606FAB4DDE2F95B719F48DA903E0B23D8353C89BD4392876A50E60E42038 + AF3CE025F219F2FABA40701E003B044EA16D6DAA0F004665FD5479008CF2D037 + 5511D87539FCCD03602F3768CB9A56582BF375B36CFD8C7551D25A00707EFDCD + 606D2CE7DB5C5EEF7D2BE5CBD204EA5612F03755DA16D63E59072D07B47965EB + BA39A57D1AB5D4F7AD4AEB8AC6646AA6B235683D4ECC1B9474C572A5F5EC4A00 + B8D4BFB4CE5DA95F7D956709BEAE5FD4C7CB7501E0A2BADA8EFF51941EB74CB0 + 9A26EDF4B584CFAA36705A545FA9EFA7217F2D955434A648D6BF886DE519567D + 74336354457B06BB52005C49F567851FAF6EC9F3A650F6E35611F87535C8BB7F + 53089C953F4600FCB17D78F97F38FE913F82E23FB07F29F2CA9EE706FFD16D2C + 5FBA0AE106EAFFA5A55FDD4A7F106FF644F07129FFFC1FE7EB293A76795DFC2C + 6C2FDF3BF424AB138316774BB336DDA479DBEE963661CF8B663A49D07A263C4D + 5AEAC4210F815D4DF544DC544FF8198C8D2773BBB32C85AA37A6EC78F9E75379 + 5B78BECA2A86BB35DBAD4F333D36B2B612D075709BC2DB1A757C8EAEA43D6B8B + E30ACB2AEB5B87CB974927A903DFBC33381B1727873E51CC2697DA964E689B02 + 4F5B77D3717ADC96DD0CFC366FA7137FCD376CDE457AF49F200FAFDB222F6E3F + 2923262FD1BF7F17FD5CEE91A6DAA789F6654CC3165DF57FA39B01E3C66D0394 + 4DE5D09734558D7E110A03739B44D8EB6D699DC35F83BE3E9EBC2900E0C61D70 + DDF6CDE06F00C13881FBEA6BE9A5937CC22EEBC54DA77EDAB7AFC1E2A69D025C + 75609B07B8F9722A6F2B521ED8E23426EC7397C11365EEAAA7A5F7E899560FB4 + 2524B4F5BF9B0BCB7071D9522F7EB98835006C1775E1780671553E3685BE0E7C + 71FC923AFC6DD96588B4520181ADDC7598B4EC36BC10FA529FB60181DDC5DB12 + C76EF71196A2E0EC1D66775A37EB3638A62E1DDF4D2FECCDE51BF7FC255551A6 + BE55CF31D2BAF73869A36AD777BCB435083C5A5AF6D3BEFD81B6A32C14F46DBD + 460A2EE0DB00C1EC110C4CEE1920B0C3DF9067C1A1E40A6E3BB05ADA0E1A670B + 13CDFB7297FA64693764A6B41D3A5B9A0F982E4DFA4E931643E6CA9D1357CA5D + 9357499B918BA4F9A059D272F04C69368030D1130CE6026D3B8E989E015C87BB + 45A2BDC3F0E926EF4BEA109834B4732C4071700003803BEB731401E0924A8B42 + 9E6F07008E6E02DCBD45EEDF4E51C05F1340B87AAE74A89A2D6D754CBB312C6C + B1A8E52E60A07029FC9D2F7E39F4F585B354EEF075B7451EF87ABD2FC6F95E6E + B76B39ECFF0B0426F45F08F19C8605BC0777C89CE5A6AE735758D921B02D20DA + 42E3BDB600D975DEBDA62EF374ECDC45A67BE62D962EF3B50ECD5B664E16606E + 0F4B1F90EE8B1F3275991BF607EE4DF8E7256B6C41D44323E28EE9BD64B5F45D + FEB0745B1C422E027383BBF731E9B362ADED25DC23EEFF0BEC75F8EB32000CC8 + 5DBD2E73EB0273DDED9B8A7A9CBCB87ECDF9BBFA494B1D0023F6FA05FC722C60 + 3065DCC1ECE3DB071770742B39FCF5B09568F0A3CF5AD9DB2D550D7EEC5919BA + F67919B66EA3A5805F00308BE0C39FDC64A983E0816B09A719E06FBF180ADA21 + 6F70F5BE90ED0FCC42BBE76DBFC7E75FCDC2428FDEB84526BCB25BBE71744A21 + 0CFD2CE8EBC7A6C993BB9F91B53BD6C9C5D7EF2DECF3D353A3E5D57D8FC9B3FB + 9E9457F63F6610B7A85F7DF59D53130C0239082205069177E06B0EDFFD40DD43 + E62204264D3D48A8595C88612CFD1C000378C9038A81C4B81E671E3926B38F9E + 9039C74F1A10CB9C91478F999BD735FB04CE5E8018C08B3D50F7CBF42384D905 + AE1D34F83BEBF8113DC6617D2E2D1F3924F34E030F8F6B1F5E1BD0F9903DCF42 + 9CAC672F082E5D03B7387F4F9C939927096F7C41E61242F9EC457382CE3E7751 + A69D3E235370F56A1F0B877CE4948D71B7AF0B480A30257500EC9012E8EB0018 + D8EBF077EEB95332EF7CC80374679D06EAEAE771963EC0612030601388AC9F83 + E6E9CFB899A7F5B33A7554FBE8787D9DB3CFB107306EDEF3320747B0BEEE0567 + F5BD46E00BFC0506BB0378CE295CCB2174350E600F053DE7B47E061100F35E1C + FE2EB8C0FEC2FAB9E97181BE7323E8253507B08AFA0085794F01F822E0EE82F3 + 3888F5B56819200CE805E87AFBFC73389603049E7912C7B6B669BF795A06E4D3 + DF40B2F601F83AE87550EE0A403DB43B18C6290CFC2DFD3D4E08EEDF59407493 + FEEF1DD33EFABF38EDE45903BF0E7F271C392D938F9F951FFD75DD00F8E5CF7F + 5EFF178FC6FF593DBEA68060D23927F475EAFF5F0A7FF97FE77BC1F7232F5CBE + 53CCFD1B1CC0FCDFB3DFAFA780601CC0530FEAF7E2D05173F44E3B78C2C0EFB4 + 83FAFE0F9FD1149D3559FD219EFBB43EF7291DC79ED884853E2C13F71F946A73 + FF86EFACBBF7F9BEE30406FCE2029E44A8E93DBB64DC8E6DA6AA9DDB2DADDEBD + 3380E29DFBCCF96BA077DB5E1300B89A3D87F71F937927F47BA5AF8BB0D413F7 + 1F91F17B0FCBE81DFA1CDBF6C804002F0EE057F5F8AA2CDC33003702DF315B76 + 6679DCC0F40F637648D5CBFA5A5EDA66D0D61CBEB87D5F2134F52E99B07D6F09 + 026BBB3B81DD3D3C62A39E13B4CDC24613FA79C32B3266D31619BDE9353D6768 + FD46EDFBD20EEDBB43FB6C13F605EEB76E9369E0332FC9600BFFCC38CE2D9BF5 + 1C94DF7F1EC0ABE72955D81B3880613B874527F080278203D8C3410F5085A817 + CFC9904703F4E5DC0900261DF020FBE43F2D831E7F46FA3FA2E7F3071F97DEB8 + 7BD768BA5ACFF7090006FA028071FFFA7EC0A923B8DF43EC0F9C83C084885EF9 + A8C9423C27D097BA3EF731FF0840184730C01727705FAD77B00BFCC5F5EB79EA + E947DEDDC2A4CC756CCE333FECF39B87C00E8791857E06F86A9B03DF30FF2A01 + 61E664616EB658CB4BA5FB5C9D7BCDD6313A6733073061A2757E776794DDD0A7 + BA6726F33F9DB7CD0CFB0593BF7BC6425580C15D748E788F3982E767BA6B6A09 + F2927A1E31D7C4354C1DF35007C39D99B78E0FA19F99DBFA5C967C1EF63A044E + 4130693BEA22E8650EEEA037EF0476E0EB79EFE30018F84BDE94DCDC999FEF97 + 9CC0D55A2EB9815B027A35F5326037C05EC24113F2B92ADB03B85417F6056EAB + 75BEFF6FEAF875B8EBEE5ECAB493FA7EBFDE4ED9F6F80502F7C9EDFF1BE16E1E + FE16953DD4B3CB1DC0290036676F57BD9E8D6AD56DA8B4D2FA96BD46482B9583 + E0B2FD7FF55ABA49F721D244C786ED8A4AF0976B574B55847D4E01705E29C0F5 + BC83DCFA2ADF9FB2BB800D12534F1FCA9A77512E82C07900EC1038750517815D + E0AF81DD04FCA64AFB5ABF0400DFD6B997E9462070067563D9816F3EEFBA1500 + B82EA5F0D781AFE7B372066F43B8E7D2BADBED656B6FA8C65A5C9443E47C3F57 + 385E49695B0A728B9487BD45001885F54FD6424BE036AF1AF01635EB2884714E + DBF3E3D2B6323553B146CCBAB4E62B01605FB3F635EC12D40D7BFFBAF2F01279 + 9FD2981234F6E3D65775F184B48F97D3E7A9EF73A6C74F8FF551943F66B90243 + F1CF8ACF8DB2A779A59F6F6D2A1A7BAB55F43A8A54AFB1456C2B555DF51F5175 + 02607861CA0F3F2372005C0304A7AFF7465F77119CBDD52A87BCB5E963F823D4 + F6C7F4B61BFDD06A40E08F2EFF52E495FD63EA172055E91F36F42B3AA6E91302 + C015C56B439A2FFA012D523819D54F3EC69F2F1DEF6DB742D9FB33957F4ED607 + 00DCF476954E340091E49BE8091B69DE0070739D30B4B85B9AB6D2094A0B9D2C + 5400B2006087C0597D0EB6DE8C2A3D5F5DE0D795C1DD5ADA8AFB9420AD835C87 + BAA9BC4FAD0038ED93A4358EC33EBF71AFDF14EA162985BFF9FE4C0E7DA2E913 + 4BF2D6960064E02FE19DB3BD7F5BE904BBB54EBE356DD4A2AB74EF3F411E79F2 + 357971C749193E69B18161535B9DF8B7EB2DCD222C66DC6D6DF5C2807D8401B1 + 0E665579F0EB7268EB8E607705A7B0D78F9101DF282FE78F8570F2360400B7EF + 230D08FDAC69966FDB579FA3B73E973EAFF669D43140624070D3CE7AF1D5BE97 + EDC79B3A7ADDE1DB44DB01BA5C10A570B736793FEF4BCA782EB8DA741B263D46 + 4CB3B411CF1305C435D07BCF20B963D824193075B1741B3BD3EE566EA863398E + C3DF1400A7F0B7120076514FDAAADB706905C88DA03705BE2900F6FD810905ED + 00D805083607707714C26C65F09710D75DF5180E807B8D9256BD4749EBDE234D + 6DFA8E91367DD03869DB77BCB4EB572DEDFBAB065449BBFE63A5ED80B1D2DAEE + 2A0F5097B0CF2DFA10FA79A4B00F304EE0E2FD7F710053175CC0ED064D90F683 + ABA5D5401633C66B3AC99CBD2D07CE9066AA26FD674AA37ED3A541DF29D2A0CF + 142B371B08009E25CD074EB3BE6D874DCB206EDED9EB4017751C31C3C06EA791 + 33E5F6D1B3ADEC63BC9FA7F473D00BF86D3F64B274183A25AB2B0FFBACC7CF54 + 5A14B27C5C300AE037387FDDFD8BF2657309EB713A6ABE535570FF027F51EA02 + EE5C4D9867D2F2852F5F106391EC0E9CBF2CA0A1087E5D805E5F844B17E2BC1E + 174797198BE5EEE961DF5F0FF177C7D4052A16FB96D842E01D9ADE0110C61912 + 217039FC4D0170587CEC3AFF5EE9B650D3053A66DE1283BE5D170475991F1D2B + 0B56488F452BA5A7CAF6035EB05AC7ACD1B655B6DF2F00B8FBC255DAA6ED0BC3 + FE780063C240F78A0BA7F750BF648DC15EDCBFEC21DC7B05A19D9F30B72EB037 + 1FFA1960DC6FD55A03BFC8DDBE0E6F49CB20B096FB027A759C03E37EAB18B7D6 + DAE96FAE5F6D236F6039C2DC14007B88670F5B89863EFE9C2D609701604D873C + BEC1E02FB09774D063610F44CA237159691DAE285B1C27446684BA99CB37825E + E02F72E72F40380DF96C656D03024FDAB2472E1C7FB410827E16F49393636CFF + 4C342AC28CA27EF76EDF24D5DB7648F58E5D3271CF1E79EFE882C27E37A22B47 + 970B8E5F402ECE4483B7FB5F8FCEC0E80E8ECE43F6E80D22FCACF6D13683BC11 + 6E017A11D0CBF73C75D80B109B73E254068009F93CED90BB7B75EC31001A6ECA + 8332E5E03E612F549C8F80DDA043D63EFB84F6D5FC8C23B41D9439278FCABCD3 + C70D1E4FC79179E488C14473C402688FEB73E26E3D4958670FF97CDEC0AF01E0 + D31765F6D9CB32FDF47999724CFBC610C9930F0637703EE4B3C151EA22040E4E + DF00247D3F5F423FE3060E7525000CD8C5FD3BFBCC715970F1B42CBA8C8316D0 + 8A8B1557AB7E2E27F473D03E8C41B3CE0085F53D9E3F2B0B2E5F937917AFCA9C + 73B877752CA0F73CE1ACCF190836187CFE822CBE70C9E037E19C6923655F5F5C + C38482263547B0A67390E6A95B78FE92B5CF3A0910D6E39D3D6BE0D7E16F084B + 0D800D7018F08A08513D5FDB81BBEEFE05E27A486707C4693EAD73E12246388A + D9039963A730D7C12FE2F320A5DE40AF035F4D5DF6DA789E53C7ED7F849B0580 + D1334EE871F5FDCD3A7B49FFFE9764DAC98BFAB73F2793F5EF5D9F10D02F7DF8 + 81FE0F1E96D90900260D205B3FB7F87FEEE2FF9F94EF808361BE037C47F81F07 + 00E3020E61A5F7CBE4D703000E21A129E310D6FF0BFD1F9C7154FFBF0EAB0E9D + 36F83BF388FEBF1EBE20330FEBDFEDA8FE6FEAFB987342FFB6FABF0E009E7CE0 + 987D77C3CD1B7C770FE9773CB8F2FDBB3E85BDBB2DAC3B6098EF3CBF078481DF + 25E377E9EFCDEE9D965AC8E89D0E805FB710D063B703800907CD6FC6515970E6 + AAACFDA5AFC8EA373F9419FA1A01C063B56D8CF6A9DAB65BAA5FD3636D01FE06 + 984B9ED0D013760487B0857C568D04E4BEBCD5C0307D091D6D7A75A701601CC0 + 216CB496B7E831752C0018A0ECA19FCDE9BB69B381607F4E803075DC20C431AA + F5358C7D715B70FE6E7C4D46BFBC5346BCA0E5E7365B18E800865F9361CF6F96 + 81EB5F92817A8E1ABE3EC0E3F49CC37969E8337A4E5B0FF8DD18DCC138879FD2 + F355120E9A3C5B1B00814DF1FC67373DA90809CD3971703C8F0E7E947AEDF398 + 8780D673F4C37A2E25147484BCC05E003079602FE5818F685F4D0D0A3FF0A8B9 + 8201C6068357716E0F32084C3EC25E4F510A84117BFE768B40D71CC0D42580D7 + C1AF3B83DD154CBD695198F318E88DF3246E96E34639E46552FA647329EAE697 + C23E3316284C1D50986D38C8F79CAFE3E7EA3883BA6CF7A173BB9901FCA610B8 + 0B0EE1D8C79CBF33880E1300B041E0E804060277D17A14207069CE19C244EB7C + 320784999F9603E0E0F2655ECB5626774D9E6F79E6BBB6BFAFCE791D08BBE397 + D421B0CF8FAD5E05DCCDEF07ECD097B4FD68E6ED3AD71F3BCD00701A021A10CC + BC3E44F461EE5F9AEB7B9D3B82DB8F9CA4F378AE07081B3DA1060076088CD3B7 + DC01ACD74383F41A69C068ABA7CE34B4B41F701EFC924740E20EC326581F07C0 + A903385384C02E87C0A952D85B0485B376DAF47A2F0BFB8CF47AD320B0AA65B7 + A1D2A6FB7069ADD7852D7AEA356C8F72358BD7A64D558DBB0D96262A00308E5F + 73FD46006C2A00C0945B68FF743F6007B8066B0B8070AA14EEA675A4E9F1CA8E + 493F2D9336D33272080CFC2DDB133881BBA9DBD79542E0BAE4C749550900A72A + 82B7457200EC10D8C7E5C7E6EB9AA05A0070111076A05B9B8ADCC08D756C8892 + 778FC901B04360CFE7D7D86C9D2D81B5D9DA5BAEECA2EC00D8FBA5A0D7CB691D + 7D8B406E25F9FA666D6DB5016077F696E06DC720CD7B9F7CBEA89C8D73B1FEDD + 2CC0DF0C02B36E8DB4CDD2B8569DAEA723406EAAA2B5F4FC1854EA1B8E5B9B42 + BF52395D3FCFD6D00BDABD9C1F5F5FD5F61C1FB7C2E7C2730696521B7B49DB5C + 8C2DAACF8FFD2454F43A8A54AFB145CC0AC09B4AEBE05CF9BABCD2E3D4CAC172 + E3F310B8B18E456500B8363E58A9FE16AB5E00B81EAF2B3B4611A02D52A3462D + 4D37DAE6CAFAA4219EEBD2AD06C0A89E1F5845E93FCFC721FF52F83FB1E733D0 + ABFFC0A9F2FFB8AEEC9880DF82BD80D32F62BEEDE6147E4CEBA3A21FC94A0A3F + 9EF513FDD3E7C8B7DD88EA7AADDE9E89CF3CF91CCAFAD97EBE11B45A5E4FD828 + 425607AD75858006FE66E56649DECBA80CB0DE807C7C728CD2C4E58EF2BE39D5 + D6273D4643C06BB3D0AF21EF350F6F73C0D695EFE32A6C77009CB497F54BE06F + 25A510B7A81DA513569FC086313AB94DC47EBF0060F6006ED8BA9B95A96FDABE + 97346DDB53DADC3E40068E9A29A3262F914E3D4769DF9ED2ACBD5E20689B41E0 + 8E9AC7F9ABC740C05803B511DC9649EBBDCDC16D91AC1F4E5F555A972AED9FF6 + 0979EDA3AF9F3D7571D436D0F7863398BA06ED80C100582E6402C835C88B80C6 + 7A8C06ED753C1739B90B1FF2E5174AB50360C02C69EA10A6ECE3C937D4D70570 + 26B4732AF6FF656FDE2E23A6CBA0A94BA5FB98D97AE1A917AD3A0698EB9017E8 + 5BE4FECD2B7500037F0D00EBF15A761B56068091877B76E1FA650F5FDFC73784 + 7BD68B75C6E8D80C005B59C7E805F66D7A316E755D69D78B7E14433CB3C76F8B + 5E7A91DF7794B41B1CEE386FDE7F94341B304A5A0D61F1A24A5A6BDA96C50A16 + 332C2CD93869D56FAC39820D06F71D2DCD00C19AB6EC4F7D80BE0E81495B12F6 + B96F95B4EC532D6DFA4F90B680DF012C6C4C94D68327490BAD6BDE17103C555A + 0C9A26CD71FA0E986C69ABC1D3F575CCD0D731DDC23F07E03B350BF1EC00380F + 744903D49D9101607700B70618D387B28AFE28F475101CEA0C06ABDA0CE34E7F + EEFC0F10D8C12D290AFBFD266017A03B7656261CBE9DABE64847CD3BE0456D75 + 4C9B51FA3C63679AFBB763F51CE9A4EA387EB6A6EC9116D4D9F6F30D0B659DA2 + 03C216C922EC257C5E08FB5C33B4B32FB8B1F8E6F20538F22CDC758D0E105B00 + F4453D16FB34BD03283C9310CE8BB33488F0CEF7666577FD92E61DC0DD172C33 + 08ECAE5F87C11E02DA01708F85712173C12AE9B668B5ED85D76DD12AE9BA30EC + 97D76DF1CA4C5D17AE94EE8B57658BA700E0BB70CB2C592D3D973DA229F50F8B + 87770602037F1D00037F1DF0BA83D79DBFD423EA08074D7D4F20B0967BDF0704 + 5E2B031EC4BDFB8CF45FA35AFDB4D5F5C31DBC469F2B025E002E8BD21EAA1279 + 1EC01B9C4B2C683F2D031F23AC33EE251D433E96073F11435E4617F0A0B8103E + F4A94D32FCE91742E8676DCFF6480400037F9F0A8E2A041076D72F0BF0BEFF6F + 10FB39BE22C336BC2043D66F9411CFBF2CCBB7BD6CFBDE1601D0CF822E1D5A21 + 635FDBA7DA23A337EF94C5DB5FAED1E717678649F53660896AC76E99B8679F7C + F7D4841AFD6E468F1C78C19C802900765730F9E0E6450E810F877A953B7F0DFC + C694BD7FC3FEBFC733286C21718161C0DF43470C7E990EC7B0CDD15109089E76 + 1808FCBAD5038883BB528F0DBCA38FE6AD4CA8DF134764F6A9105ED84308CF3E + 794A08FF3CF3040E545CA0A7B5EEACB95FE79CBEA8659CBBE70CFECE397755E6 + 5D7853669DBB22D32D3C32FBE49E93A9470092E7C37EB1DA77C68933E6F80596 + 92BA0BD681A8BB63A9F3FD8081C029CC056012EA3900E0B3B2E4EA454BF32E60 + 07C2A94378E1A5F3B2F0EA15997FF94A09009F275C7300C0F370EA6ADD82F397 + 65D145ED770E9730F017C7ED392D230030F03780DD7917B48CCE53CFD80BD677 + 967E7E73CE04C83A4B5FF374C0EA69FD2CCF04CDD2D705A075C0CDEB9B77FE8C + 8ACF05077409D8720C14DE7B00C626DA01B47A9C79E742BBC35C87BC0E74E9EF + C7F4367F7EAFA30FF9E91118733CEB1BFF2F66F3B93A60D6BFD36CFB3B86BF6F + D0459976ECA4FCD17FF9EB88792B3F5EFADC87FABF871B9DFFC1F07FEB61ACB9 + F98034EC51CD0D102A6E72E0FF92FF07ED672E64FDDF9EA2FFFB53D847F81080 + F7807E4F70BA3B04DE6FDF010B2DADFD082B3DE3C849CDEB677118B7EF69FDCE + EAE777E2B23CFCDE9764DD2F7D4D965FFA40BF63FA7F4DB8F363670C1413027A + 120E7E15F097BDBCA7EA77792A21A5F7F1FDDE6F7538E7A99BB877AF39814911 + 7B81038427EE254434A1A177CB84DDFBF4F7E7F50C02DB5EC0B87FF71D937927 + 2FCB436FFFB2ACB8FCAE4CDD7F52DB0ECA28FD6D1BB3ED808CDBAAC7D802CCDD + 699AA09AB84D8F4F48EA1D7BA54ACB63376F9331AF6E95D1AFBC26235FDA6CF9 + 71AF6E8BE078A7E5AB5EC1D5BB5946BCF8AA8CDB4208E8A0AAD776D838EA47BF + BC4DD32DB6E72FA1A0C7BFB64BC6BEBAC3F613AED2E34CDEBE4F266DD3E7040A + BFB84D866D7C4D86007D81BD1B19875318D730203940E321067B5F34B83B743D + E7173DFF3C1BA5E79A61CFE8396DFDF332F239A24E6CD6F4D570BEF27318372B + A942B8E80085FBC7F35CFF47F5FCFB989E5BF55CC83911086C5B23C4BA7E7AEE + ECFF989E6735EDF708E7D567A4DFC35A7E9810D14140E02C7D907337113BC25E + C0BD57312F783493C3DFE0065E6B9143EC06B298724399CF2F427411B6997858 + BADFCBBEC0E1C634FAF421FAC8F2876CCEE2F0D7CBC05F77063B20EEBEF801BB + D9AD07020EC71BE56CBE1465F3A145EC25ACF327953B87BBCE0F4E61E0AF4361 + 07C03880BB31D7D2795A3AB7630F60BFA92F6CE911E67377C73EB74FD739A401 + E0B80F709C2F92BF2B730087B67013E1FC104D2681BE3E474DE5372B76AA2E01 + 60BBA951451EB00BE44DDDBE0E7A91DF1099B6D99629AA4E9A07EE768875068C + C710A907F83BD914CADCC0A9F3FC9113A5DDA849D27E94D6AB3AEAF13B8DD163 + A83A8EE686CD9280C1EE0876481CA06FB5A6A5FD80D9DB17D80BF84580E01400 + B30FB001601D87DC01EC40D745D9C33E933A28CEF7F1BC4163AED712D89B7703 + 3BE8F57D7DCDB9DB7BA4897C0A7B5B7A3DE51E7A6D196580B77B08F5DC4ADB5A + ABB2D0CFDE4E5F2D876BD3E8FC256F0038847D76086CD26BE366F70081074B73 + BD6ECEF2B1BE08F8E6EBF26DE453B89B8260A0B2E7CD61ACED0E7B1D00BBCB17 + F0EBCE5EC06FA3CEBD33008C7CDDC2D72B52E8EB10D8E5FDF3FDF2C7F0BCAF85 + 34ECC04DF6DDA591AB630F6DE7C679D64E425B5D6AD0BE9B89F10E901DF41AEC + D5E3E481306AD8AEABC9017251B901FBF7B6E92279084C1E48ECA0D8EB0DFEB6 + BABB0C00DBD804007B3DF2B535133036CA012D6AD0EA4E3DDE5DDABF18ECBA8A + C6D626EF0B0066DD3203B9F1F879D504BD3541B0D797D64601B725805B087281 + 8649B971CBDBA57987AED2BCBD7E26C062ADAB31CEC7A2B44E9501E0DB0218CE + C43A356BE42A5B2B8FF58DB46F25089CA63722C65452B6969EA8521F2F171DB3 + D273A575958E7FA3AAEB18FE9CB7E9DFAB6DC76ED2AADD3DD284BF4559BFC00E + 6031EEA44EF98CABC45A4AFC26ADFB3464AFA371605445ED45F2D79E8D735695 + 0A508B34EFAC2BAB7325FD2B1E27AFDC580030CACAD96B4270BDC8042BF141AF + AF4BF9711F59FA99D4F5F9350AEFB3AC4FF2FE7DBCB517C1DABC1CDE6610B742 + 5BBEBE2C9F3A7B8B606F913E0E00FC5957FC03E5FFA0FE0772D09B17B0D7D354 + D91FB6E00B786B147E506F44E53F8225D5D59EAAD28FFD4755D16BF0E7296A0B + 75F9CF217E36B4234ECAF4D713697AE20DA9D637D113833986734A40AF01E266 + 9A47497D2A07CAF555F9C4A4549757D6EECF5FD6977AD56D01BEBACAE02BE566 + 9A07FEE2744EEA53F998A2F6B4ADA83D5561DF02989B5739D0CDBB827522DB9C + 3CE097C96BDD6AD88A49AD4EC65B6B19076E5B9D68B7D3893730953AED638057 + EB9A002B3505B69266F0358EF134555AE7E3DDF19B425D97C3DF2200ECE5CC31 + 9CCB5B18694DC378BD5801FCC663DA3138A6397FE3C54C27BD98B10B9A7051D3 + A0BD1E2386434A955E00791E39D8AD0B06BB0214F68BAFBE7ABCE0E84DC338E3 + 466E4438EA3B064ADB6E23A57D4FBDE0EE3E529ADDA5179F77E8C5E79D7A3169 + FDF52232CAE1AFB97DF502355CA412A6AA242E5E2D5456740187B05925A7AFCB + 433D17C15FAF6FA6FD9AE1F8EDA1FD6D0F5FBD58C7118C2357EBD8DBB719AEDF + 6E7AB1DF638C6AACB68FD58BF771D2B24F95B4607FDE8155D276C424693DAA5A + 9A8F1C2B6DC7574B87C95AAEAE96966368E36EF571D27AF078693DA8CA4070F3 + 01A3E4B67E236D2FDF662C20F4E3EE733D3E00D81CC2A4FA3CFA1C2DFB5649AB + BE13A4551F55DFF1D2AA7F9534E779FB0182F539FA699F7E00E66A693B6882B4 + 19305E552DED064D92B683B9BB7D6280C50327C4B2A65666AFDE12FC459481C2 + 0062F2D439086EABFDDAA880BFED71078F9C6902027B1DB018B97BD8DCC626C6 + F9D8E9362E3D464700F3E859993A8E992D9DC7CD35572F219C71F0DE397181DC + 3E619E957D9FDFF62AA06F87F1B3A5DDB899D279228B63F3AD6C3058CBED2768 + 3BE097C5B1E89430A83BB9E4A060012D5D6073B87BC7143D5E943B7BB30539CD + B3875B97998B83C3C342002EC916F46C7FB8394B03D49D13170113C8CB62204A + 616F9132F789CA5D2A1EBE90BDE8484308E8FBA2537885DCBDE03EE9628B9ADA + 6FE10AB967414859E464B1335BE8BCF77E5B3C65A1B4CBA2FBA52B211471CEE0 + A62184A28A45D87EF7AFCD1669912DD0260038157516BA393A807B695D4FC0EF + 034F481FF608BE7F9DA64F06F0FBF07AE9FF10619B9F96BEAB03D405FC5AFAA0 + EAA1276DE1D95DBB88FC20C238E3FED5FC105C4E8F3D6D0BD3A443D63E2BC39E + 7ACE449E5096883D0D87ACDB20835503353F9036CD0F7DF239D3E0752A4D873C + CDE2F906951EE3998D3262C34B32FC3916E237CAA0A7B56EC30B3272E3CB5AF7 + 928C78FE2519B5E9152B7B7ECC8B9BE5DBC7AA0AC1E76745AFEE7F46AAB61D50 + ED93EA1D07E495FDEB6AF4F9C6F1E9327EFB5E99B013ED9309BBF6D6E873B3FA + D999B106734BD0973D7CA35EC7FD8BD883F4758348A603076D8C4360A06EEA04 + 0600138A19A5E097D4F2F4D37A2030F9D9C74F58E85C34F3D8714B5FFEA55F92 + B7FFE3EF9B7EF4D77F6DFADA4FFF58DEF9CE77E4D86F7F43D65CBB2E334FE0AA + 05F605A86961998F9F921947F539D88FF504601017F0199973EA82EDED3BD3F6 + C005FEBE210B2EBD270B2EBE2733CFBC21334E5DD17E174D00C1B9672FCBCBBF + FC6BF2CEB7BFA7CFF93DF9F15FFF8DBE86BF912FFED18FE4FDEF7F5F767FE52B + B2F8C2053B7E1E02A3E9F6DAC2FEBD25601A4030758B2E038009AF4CA865F6D8 + 0DB034ED47FEBE6B9765FF6F7D55DEFBC1F7E5B7FFF44FE4C77FFB37F2FB7FF1 + 17F2DEF7BE2FC7BFFE7579E4EDB764E1C54BE6E8E5FDCF3F8793F782E00806E6 + E2825DA8AF73C179A0B01E5F3F2F03B697B4EFA50B3A56DBF4B35B4008E773A7 + 65DE9993329FE7BD7259F67CF52BF2EEF7BE2B5FFBF9CFE4477FA37F83BFD1CF + 40F563CDFFF69FFC5CDEFFC1F764CF6F7E451E79EF4D997BF19CCCD2F1C0E2E9 + B86D01C55A37EFD27999A3CF375BDB66E97181C8ECCF4B3A47DF337DE84B9994 + 63CCD43C20F7A137DF9013DFFC867EFEDF96AFFDEC8FF57979EEBFD1F7FE5DB9 + F4EF7F4F9EFEE0BDECB3A5FFF41338A6CB21327F8339FAF7987D0AD73161A843 + F8ECD9FCDD7002B357B2A6D3F47FE647FFE5BF44CC5BF9F1CA177E4970A28790 + E5217C39009870D60136EBFB3B7A442613DE19800B24A63E826CC2542F38BE4F + 369E7841AE9FBA4F3E7772B17CEFCC44F9FED94926F21F9C5824578FAF90E78F + 6C9425475FD7EF4A08E73C65FF31FD1E9DD6EFDC29FDAE1E93C5E7DE946DFFEE + 0FE4F00FFF5C1EFBF037F4BB754E261F20ECF309EDA73A8CDBF8984C39880B18 + 972FDFEF007471F93E7670939C3AF6803ED772BBA1E43BA7AA2D34FC774F4D94 + 2F9D9823978F2E95CD47D6C9DC033B64DCCEDD52B56B8F7DE7A7E86FC2647E17 + F61E0ADA73447554EBF5BDB327F1BEE352BDF3B08CDF7650C6AAAAB61F9409DB + F7CB84AD3BA47AEB76CDEF94497ABCC90065CD8FDBB2D5345EDBC66E7E4DC6BC + B25946BFCC1EBF5BA44AEB19334EEBC7C6F2D8575F9305AF6D90DDFB1E90770E + CE956F1FAF92EF9C18AFE978F9DA914972E1C0627962DBE332F2C52DE60A4623 + E25EC39376EE97B987F433DA735046BD04FCDD2223376D3307F0B0E7D92F1837 + 700C15BDF15519A6E796617ACE01F40EE7C6224D07AF7F5ECF3D1BB5FCBC9E7B + 366AFE7919B25ECF69CFACD773D42619C379E8B99733E0EB372BE116268205F0 + B7EF637ACE656B032D93028307B1ED810AE8DBF7113D0703811F7F4EDBD64B3F + 3D6FF67BF419E9F3889E9F1F7ACACEC9ECC1CFF9197123966DC1C0CD59DCB0A5 + 2282C7606ED25AA365E602CC13F4FCDF7F35DB3BE8F99C71DCE0A56D56BF4AE7 + 0A11029703E030CFE06633DF862203C3F1A6B5220730001865005853730D2FD6 + F2229DDF2C645B8C10018532B0D84349330762EEC4CD721CC3424A93C6F917F3 + 2E6EB863AF60E66D96DAFCAE74E31E73369FE33918BE73263701EAFCD1D24572 + D70CE0AECE31A72FC89CC361BB10E698CC37753EAAF34BBFA190F9A66D2BA273 + D33C00F6B2CF634DDCF0C8FCD76F74AC0E516E48293BE44D616F5ACE836020B0 + A55180E08E6371074F950E6380BF40E0495111081BF89D62EA306A8AC1DFDB75 + 8E4EEAA0D70500261474DBE1212C34E554ED87251A3A51DA0D99A0D7284060BD + E6D17C5B4DDBE8F5551BAD6B3B18F83BC100307207B0835F2FBB1398B23B85A9 + 0BD762C109EC20B865DCFBD781AF3B7CD3BA96B87C23E4C5E1EB009832AE5D04 + E825A43351A31CE61AE08DE32C1C742C97EDFD1BE16F0A8191B986630A0406FA + 02809BA81A0383E3353437493B00AE24AEAD9BDE05E42D5D6B3BCC05F4BA7BD8 + A1AF03E11406D3C7DDC10E8033F85B10CAB9B48610E02CF9B49FAF4F906F7667 + 587370B88BBC4FBABE91B6FBF8BCBC7F582B0900B7010016F8DAB147A622E09B + 573656F34D7200B8121026CD006F84BE0E80BD9FB5B709C0D601B0AB7184BF88 + 720A75F30EE014227B9DD5277D82EECE04F0CD04804DEA3320AB7907B979F89B + C1DD589F86867679DF32A8ABC76DD0F28E32F9F321770B67FDEBA5E2755614D6 + 593BE93181C95A6ED6599AE9E7D5B9DB20E9CC0D14FABAA9CB03E66C6C5DD2B1 + A5725C332E5847CECBDA7C4D5A535FB3F6E89416A192BAA6A5B5F954D4A7AA54 + 5F49BEB65ED4E64A8F99AEC7E79FEB469EF7A3088ED3BA5D17E9AFBFE5BD585B + D3FF2D0C63B4A5AFD3CBF9D795BE07EF57DEE60CE6D6286349FA3ED27C45151C + A34845C7293B7E84950E80EB82C037233B6641BD09177011ABFBB405502E7ADD + 05757C968DF89FE133A5BD818E47B1CDFA519F42DB9B95EDF51B816F09DE26C0 + 370F7DF36D45B2BE0500F89691F54F4766C5E68FA57F902239D82D6A43DEDE44 + FFD8FF920170519FA21FC54F4375BD0E6FCBBF870CF416C9DB481B93D684BE37 + ADE4245FEBE4A06C325052BD27132A9BCC98C3B75C7908EB2AEB97B876F375F9 + FEE41BB5D0495A94B7D74745B0370F78EBAFFA0160A0AFEFBD9BE51DA452D746 + FBB4E4EE4826BA4C9A03ECAD24EFE3C017A5ED5EB6E7A08E7294D7797D0A821D + E0A6F9BCD2F1F971A84907BDD0D172C3F65CA00082CB41AFD7E100F6BC8BF6F4 + 022894B9502A2985C179E54130B236F211FE928670CD830DFE36EEDCBF4C4D3A + 0FD07101F8A6F5F4BDED4EBD18B5B1C1E96BA19FF5E2D4EF58F67C80BEA4211F + EE6A2E07C12D80C08900BE0E811D045B7D772EBCB94B9B8BF5007FC35DDBE4F5 + E2BEE768158EDFA0D6BDC7E985FD5869DD8FF06193A5C38829D28A3BD6474F90 + 3BA7CF96C12B57CAC0FBEF93BBE7CD928E93580889E1C8868E979643C74ACBC1 + AA0163A4457F1CC03A5125BC9839828313B81950D842408790D000E0967D742C + EA3BDEA0737373065749ABFE5AA72975AD07544B9B81FA3A068CB73C90D780B0 + D6152B00E0340CB4975DEE0406FEE2FC4540E014DE926FEF203742E0CEA36659 + 4ABDB759BF14FA6A1FD429C2DFF6A3B45E53E06FA7B1730CFE12BED961AF8360 + 2030AE5E03BC9ADE3965A1815F00B0C3E0D6A3A705203C79BEB4AF9E2D6D58B0 + AA9E952D9439E425A5EC8B6794CDE51B416FE7C973A5D3246DD794B2BB321C06 + 93CF606F74007B3B60B81BE101E72ECD1607118B83BE40E86507BD16A6707ED8 + 6FCE0130E5B0B72F8E94B08F5D0F1638D182508F8BC5DB0D0E037717B3085A72 + FC92EFBE84854EC0B08E63517479583805FC22F6DD2304631FDB876F4D0CC148 + CA7E7C0FD922ADEDDB7BFF13E6EA75C7AF035FC4E2EE401684E342715F40B0D6 + B150DCFFC1F5067AFB3CA05A4558674DB5DCFF41DAA223983D08E91B179CFB3F + 1CDC47A4211F20300EDF418FE3F05D5F067AC90F7FEA7919F9CC2619F6E473DA + F76919A8F5067CA30668B93F212EE91F21F01080F133CF5B384D34FCD91764E4 + 861765047AEE452BB3208FCB17080CF01DFDC22B32F6C5CD32EAF99765A4955F + 95A37B971742CFBAF40F6707C8EF1FAB32A0F09DE3D565A2AE24FA04F8E000E2 + 3BE884F6557D9BFE2726C8774E4E54694AFE84E655DF3D39C9EA97EEDE2A1376 + 1E54BD2E13771D90BD071FD136DA55A782F61C782402E07DD667C5BED7621BB0 + A6A45F9CBDB97D81AF1E5B610018B833ED2040F77009F26A5A02C0217CACB978 + A3631839C89D6EFB94B247EF1103C100B51FFD15FAAB905236FDB5FCF8AFFF5A + 8EFED6D70C048750B9C7E5C12BD7E4EB7FFC33F9E7FFFD7F226EABFDF19FFFFB + 7F97635FFFBA2CBB7C590CC09E090ED699C74F08F037B859094D7CDE423AE3EE + 9D77F68ACC3D7B55E69E7F53E65D7847165C7A5F669D7D53A69DB824338E5F90 + 977EE9D7E4B77FFEA7F119EA7EFCFE9FFF6779F58BBF6AF0D6F7CB5DFFB90F0C + 58034A4B0AF0D2F327BEF54D03C0F32F106619076D00C00E8439C6B7F5D8F579 + FCC53FFEA39CF8C6B702002694B36A0E6E5EE078849E388401A0F3088BACCF33 + FB823ECF85B3B2E0C2795978FE9C2C56ADBA7A59AEFCFBDF933FFF877F8847AE + FFE3CFFFF11FE4BD1F7C4F1E7CE74D03B968F6F93319E445940D129FD5BF8DA6 + 94D14CABD3FEFA9A965EBD2417FFDDEFCA7FFD9FFF331EB9F6C73FFF9FFF2DEF + 7EF73BB2443F4B60AFC3F35FFDA33F0C7F03FB3BC4CFDECB7F1DF2338E1F97E9 + 405CFD3F41D4D7F578F997D803F8B039D17D8F630F61ED2EF5E9B1DEDA81C3FA + BA80D1074FAF93DF393BB3F03B589B7EE7F42CD97F74AD7E078FE8F7443FB7A3 + E7F53B7656165F7853367EF5DFCBABDFF8BEDC7FEDF3FADDC3197C42A61FD4CF + 5FFB018E671E3921EC213CC95CC5FB65E5A157E5DAF1E537FC5BF18D93D364CF + E147EC0690F0BDD7EFFFFE6332655FD0A43D4764E2EEA33261D7119301E01D87 + E5CA91FB6B1C0BD08B2610D23EE6F37D2EBDBED86070D596007D4DAF6D95DD7B + EFD7DFCFF135FA17E92F4F0F95E3AFDF6F21A26D7FE017F558AFED9229B897B5 + 6EF8A62D2AF608DE664EDFE11B03000E10F855D388E7C24D479C67DCF13BFCB9 + 4D326AA39E8B9ED7F3DA063D4F3DBB41CF457AEE5AFFAC9EA39EB7F354696FE0 + 8D19042675E00B00C609ECFD06AD7B3EB8815501003F6D3257305038BA85FB3D + B6DE5CC07E0EEE0FF48D1AF4F033067C07E9799B74F8E3CFC988279E97218493 + 5EA3C75A1D806F981B94E0AF6D15F1C01356664E0108F61BCC52017D81BFCC3F + 50CF65C1ED0BDCED06DC8D37AC65753A8F410E7FC99B2398BA1812BADB3CE64A + CC9DEE977BE6AF90BB5544400100A36E5A4F84942E3AAFBA47FB75991700B0EF + 0D6CC017D0CB1C4DCBA4B687B0CAE76EE99C2F4479090E6174B7815DC23C2F94 + AE33749E382BDC3C186E20D4F9ACCE3791CF2DDD097C678C409387C065D236A2 + D9E0FC75E8EB00D881B043E05429004E95825F83BFB12E8481060213FE39A804 + 82030046847446010287B0CFA9EB3795036187C0406103C3C840EF04BD262901 + 6083C083ABEDE6D75603B5CCCDB811003BD44D95868476B88B3C1FAEC1C2F598 + B765503881BD0E805D1EE2D9DCBDF1FA11A5101870DB12C536DBC7B7DB1003BE + C05EDFEBD7DCC0F48F7903C7AA3CFC75F05BA4922358AF99351FAE8F4BF26BE9 + 540E803D443400D721AFC1DC0880DDE50BE475514EC3423B18C6D1EBA19EDDF9 + EBEEDF3C1476A09BD6E10A46656EE14EE590D7416FBEECEB1BE9BA87D797EA48 + 03E875104CFE6600300EE0D4ED0BD475985BE40276E8EB6503BE1108231B4F9A + 80DFBC1CE4D625FAA5B0D8D3FCF8509703C0AA32301C616C06701339A82D6A73 + 799FBCF2B0B7484520B6A85CD42F6D4F53D4B4CD3DD2042772B3DBA5997E361D + BB0C944EAA667C1EB6EE5AF33835D7668BEBBD2D2B378BEBC6AA1AD017E5D6A6 + 1DFEA2C64D3B9B1A795D5C0FAF6BCDFCA3AAE8D8E973A6EBF0DE96F6F3FCAD14 + 90AD8D7E5F060EA996DE982980F7F135F11ADC59ED2A1B9B7BFD2551A6BE268B + C9EA1B47C8976BAF530511633F6E399FCA38957E460E25B3B281C90A0038F6BF + A5FAB80170255E79531CB3C267907C3680DF26FAFFD4A45950433EFB5C9F2C9F + 87B9372300B0E72B02DE1B05C0A67FFD00D8E47F9402655F80B42EF90239004E + E12F2AFAF2DD1A957EA8F2CAFF90A5F579D5A7CF27ADF4C739FF035D1F19DC45 + 3A364B8BF27E822D02C0F9FAB46F5EDE2739D1DBC9DD55A9BEA8BD0EF9E4224C + 2412809B00DB1A2016B89B2AE9978DAD0500A31B85BFA642905B5345A01895F7 + D309580539FC65DF5F07B6067B5B97036157511D4AC1AEABA8BE46FF04DABA1C + DCBA9B973A07B8805B87B779E58FE3F231E9382F07177039DC75A5E5B4DD2F94 + 4A1743A47D2C755582C135A06F2AEA54847DC6C16BFBF276D50BD8BB87D400BD + C1E9AB179B89A82BD50383FB4BB33B0308E6E2347F018BB8486DA917BB0E807D + 7F5F0F01EDE19C1DFEE6E56DCD09FF1C616F905EB0F7D6315CD46B99FD7A5D2D + FA8CB610CEADFBE1C01D23ED86564B07F6AC1A35513A4E98223D172E92094F3F + 2953363C2DFD572E953BA6CF90CE63A74AE71193C35E5384851E3C4E5AF99DE5 + 03B80B3DDC898E033884851E13006F5FED43F8679CBE7DAB230406F68EB576E0 + 2FA0D78130E522D0EB60385580C313CDE9EB72E7AFE71D08038181BE2900C6C9 + 9B85808E501798EBE0971439E04529F04DEB2C1D532E0070C7719AAFD2FCF839 + 4288E7B6A30951C75E6633A503FBFA4E9C67B097D42170FBAA5972C7E405D26D + D632B967FA12E93471AE742404DEA4007E1DF4A66E5FEA3AB237B08ABCB97F23 + E0C575C1429C3B80DDDDEB909714017EC3E25D0CE5A7B2BE5A7FE7ACC572078B + 7DB3091B18006F0A7953D84BDECB0E89C9DB98E84031009C842F4C01B0F72901 + 60422106F5B89745D1202BB368BA0CB01B402F297BECF549F6E3EB77FF633270 + F53A5B880502B318CB422DB2B0CE2B1E094E1E9C3E80DE981FC082EF9A27A58F + A6BDB53EECDDABDF89074B6EDF5EF7EB3154E60636F8BBDED46F4D08F38CCBB7 + DF434F99DCFD3B40EB6DFF42C23AAF7D565300F033067A113018083C74DD0619 + F1F4C6AC0ED7EFD0A79E370DD13A20302E60526F1BA21AF414CEDF0DE60206F8 + 0EB745F54D327CFD0B32323A7E598C1F4CE8CD0D2F04C7EF0B9B65CCA657AD6D + F20BCF1BC82D0203B58931ABB63C637B48123E94BD233D758D7D65AB8CC6A5F6 + AA3EE7AB5B2C14292148C76DD9268431C571C6DE9463B6EC96715BF79AB31787 + 2FF9715B71FA1E9449BB8FC8849D8764FCF603D63E71D74183BBD53BF669FD7E + CB4FDCF5BA96F75B78E82AF6CAD4FA49BB5F97C97BD10199B447FB119235D1F2 + 7D9BCDBD57F4DE2A8910D90B0EEE31772E8E5D1CBC405E87BBECE7EB7BFAFA7E + A6BE9729F50E80C93B200210D7F578F7F7BF6DA078D1A933F28EE66FF6012CDC + F595AF883971E37EB899F4D8B6EFED69F6B9BD2AF355B34E5DB27D5FA79EBC24 + AB3EFC923CFCCBBF294F7CF0ABF2A3FFF2B7F18837FE0030E2C69D73F68CBCFA + C55F89B5951FB858F3D017D72AE9977EFCA3D8EBC61EFFF59FFE495EFBD29765 + E1C52B7ADC0B325B5F0B001C001C20B0E6B58EE704B6E2CC9D7FFE9C3CFECEDB + F2CD3FFD9378948FFEC01DFCE0DB84D63E6D001817700A8053284C9E3AB4E737 + 7FC380EECD3C18C767CA67087005FED6F598116F140861B22FC84FF46F58D703 + 07B0857E3EA1DF83E8F8F5FD8C3DCCF3ACB4EECC293972EE29F9ABF3230ABF7B + 3722BEA76F9E59232BF4FF7CE6F1F3FA5C17E5DE4B1FC8A2736FCB9403A70CFE + CE3CC2DEC06764FA91D3FA9D246CF431FD3E1E94790777C8B5932B6A1CF346F5 + 8BB3C3E5F923CFEB731DD2DFA02332653FCEDFA33271CF21A9D6DF30F603AED6 + DFB509BBB4BCF3A05C3EB2B2C631C66EDB2EA3B7BCA6BF99DB6C5FF3893B76D5 + E873F9D0127305038171FF3EB473BD7CF764758D7EF5D14F4F8D9107B63F6B10 + 78CCABDB4D235FDA12C23D6FDA1AC33C87BD7E876C784586B29DC0A6CD32EAC5 + D764F48B5BEC6622CE2F1E6162F40B2FCB9817B5CFF340E10D32FC393D073DB7 + 49463CB75145A8E7007D81BB848176F0EB02FCBAFB77387BD66BBFC18061E0EE + A37ABE5DABED8F3D6B7900F0902737CA9075CFEBF951C746186CD137A2063FA2 + E923EB65C8A3CFCA50B662D0FCB0C736C8C8B51B65D43A7D5D8F3F27C31FD573 + 2B376C3DFCB4DD14866C8B873857E8B77AAD950D12EB9C228D3282282377023B + 0876C76F067FB9714DE72E5E8FB23ED1010C10EEB524EC0BDC7DC1FD0682C977 + 59709F016087BF0682A30B18706C21A517EA1C2ACEB10C02337F4B040436009C + CCE7FC463F9FC7A522624C70FDB285C842E9327DB14593F11B081D00FB3C9234 + 00E01099262F9FC7FADCF62E44841B9DFBBA8A00B0C36187C05E5F0900A775EC + 01DC699C03E07237B085838E0EE014022387BB0E801DFAA64A0130FB03B71A02 + E40DB017088C0B98BC2B83C12A07C06CB703D805FAA64A8130ED69DEC23DABB8 + 1EF33687C30E7F6B05C0DAA7551F552FAE298767B0D7602E101769BE9949AF43 + 01B93D4A50D8C1B0BB806D1C2A80BF29002EAACB94DD205D521EFEBA80C0A440 + DE2200EC7579F84B1D0098B4BDBEFF76BD475A5F202F2E60770217C160D76D38 + 7C3505F63A20F67CA664EDC29582DF7CBBAF81F8DA87D7853511EA7AE9981204 + AE0176A3F2E5B4DEDB00C029DC4D01B0978B447B0A8DCB01704929F84D9587B8 + 4572E8EBFD6B1B73A3003875F47A7D5ACEE06DAEEC7545F52E6FCBD25AC0AECB + FBD4D6D7D7543DB530D4ACB502595BDC29CDF5B36FC6DFA7396BA79DCBC6BAD2 + F5D94CCD3A87FE00DDA2F64429FCAD0D009377008CF3370F80D1CDAC9717A9D2 + 1AFC8DF282FCF84F4A4DF5F36FABDF1DE0AF3F3F69D316B74B93E6FA77D17CFA + 1E8AF2E96BF730D1452CA606008617E5FA14EA1300BFAE324655C0B4AC0CEF8A + DCAB0CFEA264CC2DD3671A004765AFB554E7FB0237BEAD9D346DAEFF7BA68E56 + 2EDB3338550A72EBAB1A219F1B3497062AF2C52037EADF00700525FF7C7955F8 + A74FBF4879157DF16E9DC28FD48D881FB2BCEAD3E756A9E8C4E03FBA45CAF7AD + D4BF0CF692A6796FB77272724DE5275E57515DAA742C27FEDA5430012852A5C9 + 45F9C4A3047E5121848D2AEAE3E5B42D05BDE4F34AEB8BFAF8583B6645985BBB + EA0B8081BE9E6672280B7CAD00802BC9C7E5CBF9FAB4DDF2A4F4C9415BAFF7B6 + 14F4BA2A8D29EA5B0902375635D00B199CBEEEF64D2F76B8182A5DECE8B858E7 + 77DB96FA96806F1E043BF8F5B43E009814080CFC751730C2DDEBB0977C2AEF13 + DAC3B190C1E01896CA2F5C3D3C95DFA99C3A8001C00E819B7998E704F8B2CF6F + 067EA3DC01ECF0973D9C9AF61A22B7F5D6F15C90C78BFBE67A514BC8E6560358 + 14A832608B9BB7FDA84972FBC469D269F214E9BE6081CCDBFCB2AC397C40463D + F690B49B30513A8E9922778DD576164086B38051256D6DD181C508162CC2C205 + C70DFB0013D2B94A5A037481B7FDABA575BF09264030C0D71DBF405CC02F6507 + C00676B58DB22B85C0B4B71BACAF6548C9E99B825F87BF80DF2CCD4239970408 + 4E61301098BD823D0C344ADDBEA9EB372DA7AE5FC23EDF5E35CFD461EC2C83BD + 00E06E3396CAC0C50F4A9F79F70B6E6083C1D1ED8B0BD8C33E0381C7AC795A96 + 6D392093D7BE209D27CD3307B0B928A62E2883BF2EEA583C7311F699C536178B + 7429EC75089CD6B370D7150787A6B4DD13E12FE0973DDFD09D33179541DE54BE + 5098B6E7EB80BB79D9826404C0AEAE5AF650CFEEF62505FCF65CFA80F45AC6A2 + 695838F505D414FE925206000F79E82919FBE48B9A3E638BAFC8166657050730 + EE5F77F738040E4E9F04006B3D0018E80BE01DF8F0B39A3E63E037847D8E6124 + 71FD3EF8B4F51DF0F0D332F8F10D99E3883CFBF886C5E7F5067871F90E590B0C + 5E6FA03794B5FDF110EE7998F6A11E872FD077D8D31B65F8339B4C0E831D08A3 + A14FB3EFEF733270DDB3068001BE88BD1601C1847666311E67168E6077018F7D + 698B8C7BE9356B3BBF6F61210CA84B3B773F20635EDE66D017911FFDD2564BC7 + 6FD925D5AFED96AACD4004C0AF438CB03FEFF8AD3BB58C76C958950360E02F10 + 97F2982D7B0CF84EDA7D5826EE3AA46DC0DE8332E3F56326C02F6E5F407000C0 + C063C6EC8BF057BF4F5179004C993D3AC7EFDA230FEDDF74437B1F7FE5D47C03 + C00E812D54334E5F95877E263FE3C8B1E8D83D560687CBF20743B8E7BA1EEFFC + C7DF9727DF7C5BFEF37FFB6FB1E6A33D7EFBE73F9365972F09A18E09F98BAB73 + 0661A54F9C9605E72EC9A20BD72CBCF3CC531764EAF1F332FBFC3579E177BE2B + BFF4977F2BFFF7FFF3FF8B47B9F90710F2F837BE7E4300387501A3AFFFC9CF63 + 8F9B7FFCFA4F7EAAC7D5CFE1FC39DB1F380060D2B3B210C7EFA50B32F7D27999 + A7CF7BE53FFC7B7DDDF5735CDFC883635EFA0FBF97C15E07BD4060CA40E1F997 + F575687ED9B5CBF2FB7FF1E771E4477BE0B05E7AE962BD0030FF2368E1858BB2 + F8D2E57A01E0CDBFFA2BB6CFAFEF390DF005F42287BEAE074FED911F9C9B52F8 + 7DFB28FAABF32365E3F1CD32E5F0699976E4BC4C3D744EBF736764D691B3E6FC + 9D7EE8A4964FE877F6987E1F8FC8E3875E32705B74AC9BD507C797E86F0D61A0 + 0FCBE47D47A45A7FABAA76EC3755EBEF144098C806970ED58CC2306EFB0E19F3 + 5A08FB4C08E862007CAFFDB6D28750CFFF70EEC66FE649C58D3D4060F6012614 + F408F6117E618B8C04F2BEC4EFFC0E7302E3022604343700F1BB6FD2BE40603B + BFBCFC9AA6947106E3067E5ECF372FCAA817883CB149863DABE7AE27895EB149 + 463DFFAA8CD8500A054DEA220C340018404C19B0DBFFD1F5068101BEC05EDB0B + 5FCFB321047410ED0680E34D58889BB43867DBCD5A0837B00A180C08261DFA48 + 80C3E610A66F0C159DCE135CFD22004E45A4913444B48360E6287EF31A224F9D + 435F52CF037ED33A2030E1A02D24B4B6650E61DCC22AF2C05FDB4623D6877E40 + E330E7224DE16FEA0066BE9697CFE590DFD4C7F6203EA744CC357D7E99CE2BBD + EC00F8EEA95A0F2C66FB91198B2D0500B3BD4936BF9DACD2B92F72009C3A81DD + 199CB6017F3D2D7202BBBC2D0060BDAE3108EC7237B0E6C74C0DED9A3A08C605 + EC80D7E12FFB00BB2BD8EB1D103B0436177074FF1206BA23A1A27102ABDC19EC + F23A6EB2CD036007BA0E77F3F236E410D8F3EE002E9283E056FDF4FAB08F9601 + BCC05895835C77F2027ED92B18358B603705C53E2E05C3768C38FE46658058C5 + 7572E966E95254AD12F4328505FC0000FFF4494441542D81E1DA0030C0D7A12F + A9836000702B7DBE2EFAB7B95BFF06AD01D68CC90160CF67E037D67B5D0A7CBD + 0F79DCC4AC5DF8FA85035DF22EAFF3FAB49CAAB446425A02C079E7AF815E806C + 2C97D5ABD2B2858FCEA912004E216F067BF37D6B81BEA91CE85682BADE96EF9F + 6F2FA9047A3F2A00CE8771F63179D5D58EACBD16A8EB4AE16F7DFB92CFD67B9B + F97AADA6B751CEAFBF96AFCD9A00B845EBC2E9DA70D138571D0018A5E19FD310 + D01FA7F26BEA69DB8DF082FCD84F4A0D1AEBEB6B18B98DBECEA27D95CBFA27EF + 297D5FA5CF80326D35594C2100AE0BEED2FE1906C09F28F875FD4B00C0B508D8 + 0BF40DD2CFD6E16FFA7C9ECF20EE0DA84E005C1BE4AD2F0036585CF00151778B + 3EB84F45F6C7E19F2EF74F68FF88057515947D810ABE74B756E53F58F551FA83 + E7AA4F9F5BA5A21FE2D20F6EEDA11AD2BEF97EE59057DF63AAB2F678624DE527 + 5D57A57A15275ECBA7E3ED24AEA92ABBA3CB4FFC759DFC55E904233FC9A8D9A6 + 6AA613A2085DEB520A6933C76F026FBD2DDF1F356E794F59DE95F671F9F81400 + D7A6740FE04A7B0117C15F17CE5FCFA7809630CF29004E616E066F0B9482E34A + FDD372BA676F1EE4A675697D5A9742DEA2BA14FCBAD2B6DA00B05FE050E7EDA5 + 8B9E7061E4FD01B8290476109C02E03C04CEC0AF8BFAE8DC251F40F08032C76F + 0A801DF67A7DB900C4F409C7CBEF4B84425D085715E0AF5EECE604004EF7FB4D + 01B043E1DBBAA900C0B8806D0FE0E1D2A4FB6069DC538FDF5B2F96FB685D1F2E + D8839AF5D58B7C1606068D93566868952D7874993EC700F03D7366CBF2DD3BE5 + 8537AFCBA40DCF48CB71B17DFC2CB983106823268A8583B60509E02F7B554D10 + 03C0FD03C405D69601DB019AF69F18352183BA805C202E7D0D186B4A5D1E00E7 + 8FE71098B11EF23905C00E7D1D005B39BA7D5D0E7FDD114C4A7D4780EF881916 + 0A1A79D867770993D2CFCBBEE76F0A8181BF77562F90CEE3E78A3B7E47AE5C2B + EB0E5F94FBB71D963E73574A4B168AE29EBFB87ED370D0A3573F258B5EDA2D13 + 1E7B5EEE9CBA503A4C9C2B5D662EB1707B2C8EB158861C007B39137511FEA60B + 742883BE7181AE26005E62F97B08F317F7FDF57DE0B2F07F5AE78B81ECE1DB75 + 5E697110A72F0E130F35D865AE1E1320ACF529F02D02BF3D17DF2FBD960477AF + BB7FBB2E2907C0C0DF3E2BE262695C4465D113910700037E3D3FF8C12765D4DA + 8DB6780BF005FEF65B15F6FDC5FD9BC1DEB8A08B6C8F3FE421A069B7329077BD + 0C7A648329C0E00DD942B2F7C7314CC867802FA9E787AEC5CDFB4C0D872FD017 + 91A7DE5DC0DE8FF0CE847F26DC336E5F4070190C7EFA7901F89AD39730CFCFE0 + A222DC66705EF99EBF2CC2B3200F00F6FACC05FCD21699FCE2C69B72FF5ED8BF + 3883BD559B779A80C00E02AAB6EC9409DBF7086E5FC291566F035EEC91493BF7 + 5A1A20F06E4DF7C8384D81BE2EE0EFE8CD806180F07EA9DEF1BA10F619C03B65 + EF115970E2BC2C3A7541A61F386A4E5F3479CF2199B43B38830DFEEE392879F7 + 2FFB012307C0ECE33B618FD6AB96EDDB7C431078E5C1AD16FA19F7AECB42401F + D0E7634FE083DA76F8B04C3F7244A5ED870F99A66B1DE9D443B41F9219478E9A + 4BB8AEC77FFCB33F937FFEDF37E7F8ACF4207CF1F24B9764D649F6623D265301 + D6274EC9BC33EC917B41669E3C27B3CF68FB99CB32FBEC5579E30F7F1A477E7C + 8FFA40DCF7BFFF3D03BFC058203030F8E380BFFEF8E69FFEA91EF7A23EC77959 + 70FE9C09F8BBF8D2455972F5B2DCFFCE9BF293BFBD79C7737D1F5FFFD33F31F8 + EBE19EC93B140602AF78E3AA858FFE381FFFF59FFEA7B9A1EB7A38A8058C2FD2 + CFAA3E0078CB177F55E69F396D009850CF847936D76F04C1F3705D6BBAF3F473 + F28FE76BFFEEFDECDC38F98DB30BE48D332B55F7CB9B675769BA4ABE726681B5 + 158D4975FDD41A997AF88C69CEC94B32FFE445FDEE9D32F83B557F47261D382C + 078E3D51E76FC03F9E1B20DF3B3D41BE79729ABC7F6C918597FFE333630BFBA6 + A2CFBCFDBB64F2BE433261F701A9DEA5BF6BAA897B5E0F7B03EF3E54088027EC + DA2DE377E8EFEBB6ED52BD7397FEA6EEACD1E7D2C125161A7ACFFED535DA10BF + EF5F3C3C4B2EEBF15FDDBF5676EF7B50C7DC2BDF383EB5222C3608BC6DBD8CE4 + 26A197B79A080B3DD27EF377E86FFC0E19F9C26BE6FE4540605CC023A2F3971B + 8F80C06C373072E38B0680476E7C41EB5E354730F077D053EBF53CA6F5CFE9B9 + 68938E7FFED5CC098CDB17204C1EF8EBCE6060308E60DCBE68D8D37A3E531908 + 5EFBBCEDFF4BC86807C083B4CFE0C754B87D63CA793B9CDB9FB2A81F9CFB0790 + 5799E3F7A1A702FCE51C4FBDB63347486F16F3B28BB24719710180997B940030 + FB023F94C96F4EEB79AF96973C68A9E7BB2F5E93D5F55AFA7056D77DD16ACB77 + 8DFB007BD868641078E14A93C361E6583EEF7200ECF0F74601B0A7774E0F5165 + 6CCEA8E24645B6122175D16E734D6E228C8E5FE6B22EBFB1D16E5ECCCD65A9CF + A4F360C0AF4361CFA790D85DC279004C3915759DC64D97CE55EE022E560A8083 + 02EC6DA7D742A494D91398D4C16F9AA6A2AE6C2FE0440E831DFEE20A6E35A01C + 003BE04DC16E5E29087637B087804EC340E7E500B875FF3106819BF5E2A662BD + E604F046009CB978B9A938BA866FC305ACD795CDB57F59BFA27C0EEC228E5749 + 693F203000B965372D27A0B7047CCB9DC1E1BA3A4060E4E57C9D03604F89C805 + FCBD43AF67B9393B05BE79D09BA69E77D08BF200D8CB69C8689487C2BECE9107 + C5457200EC20D701700A82CDDD5B50EF72189C3A801DE49216857E763908CE43 + 60EB477D84B229F0AD4DE520B734362DA770B818027F7C0EE0FA02E07A8B6324 + 50B72EC05B4985C768AE79CAAAB06EDB49EBC2FA6BBA1E5B51AC0333CED7847D + ED1725FDFC796B1C977E11EC96AD2F276BCF9F04002E02A69F262FA88FECB5EA + EB6AA4795CC04DD2D7CF6B3656530A054D7DFA3ED2F7459F46B7F11990D24E5B + 9EC3B42BD537BE0100FC49AA0E9E550302E7DA3F31FD0B07C0A886E3D7B9695E + 19C4BD01D50A80EB72F9D617009BF405A66F227D23F9FA7FC92AFA278CF22F45 + 2559BF4FF54B1E7EA86E44FE2397AA3E7D3E49851FDD62D5A70F2A87BC2A1D53 + 6F008C92136D615D5EDE273D8157527AA24FC4248009487E2253D42F5373ED9B + 00DB540DA2AB17D5D5E6E016A5FDD272DA4EFA7103E0C6AD81BA01FED6070057 + 82C0199405E2028013080CB4CDEFE1EBFDCBC6D50180F3E53C0076C89B87BD69 + 1DF0D6EB8BC06EAAB43DDFCFF2AADA0030629FE006EDF42205C576BFF8B1768E + 9383BF29D805F6DE068405F0264AFBD838FA6A3FC02D619B03C41D64B03705C0 + 38824953008CA82F89B21F43C7160060142E423DAC5570FD16016087BF0E7E1D + 0ADF1601B039801300DCB4C76069D25B9FA3AF8EEDA3637AE3081E2A4D35BDAD + AFF619305A5A12C679E8386939648CB41B355EEE993643EE9C3E5DEE99334BE6 + BEF2A23C79F6944CDFB441EE9A354BEEAC9E21F754CD94DB2300EEC0DDEC8327 + A8AA0DFC66EEDF7E218C7311B06D3D2002E0B8A72FEDEEE4250F0066ACC3DD74 + AC43611FE7C766AC43DE4A003873044797AF2B7502E755067873AA04807DFF5F + 07C0778C9F2FBEDF2F8EDF310F3C25EB4F5C9355DB8F48BF050F487BF6049EC0 + 82D70283C0C05F529CC0774F634FB565D275E652B973EA22B97DCA0283BAC05E + 16CBF21098BA4C2CA8018A1DF426D017E5C1B0F7F1C5BB4C2CF2CD2D0F07C882 + 5F0A7FF365FA017ED9EFD7D57D4179A84187C029F0453D34DF3D42DE1400A7B2 + F0CFE69629B96680BDB880D9FF973AA02F72474DFF558FDB822CC017004C9E45 + 5CF2C860EFEAB0A75FB6904B1AA12F0AEE5FEDB39A328E21424706088C063E4C + E8E7D29E82FD1F29397E09F78CEB77E8DAE765E8BAE70CEA0E7CEC4921F4F3B0 + 2771F852AF7DD76A1F4D7101930E7A428FA97D71F60E7F7AA310EAB9DFA34F65 + 619F87E8B1DC013C0CE0CB7EBFCFBD647BF986BD14594C0F219F1D00B3104F79 + 086DB18C80C2235517F62F2A0400B5E9570FCDCCE02F29E0D721B083600B01BD + 45F39B43C867A0EFE45DFBCA0030E0D71DBC010287BAA03DDAB6DF1CC1A4387F + 81C000E079C7CEC9821317CC051CE0EF4183C1A4137644200CFC65AFDE5A0030 + FBF60271D9D31718FCECC1670BDF6F91BE7C6A7E16BE39ECF37B48A61E3C6860 + 17017D671C3D22338F05008CC8CF623F54AD77281CEA8F4564F6C93F80C0B34F + 9D32003CFDD8715970FE92CC3B7751669D3A6F1078EEF9AB32EFC23539FCB56F + C5119FFCE3C31FFEC0C02F0018EDFFADAFC6968FEFF16B3FFEB1CC3B7FCE4033 + E2F9165FB9240FBCF3A6FCF93FFE63EC75EB1F5FFFD39F97415F07C2387F3F6E + F87B230F775D13A679817E3E84F1AEEBF1EAAFFC8ACC3985ABFCB8CC5091BA13 + D8C3411F39F354E1F70B110AFAEC994764D5899D618FE0643C624FE229878FC8 + D423C765C9892372E8F4D3F257E72A878FFECA99C532F5F0297D4D1765DEA90B + 32F3E8699976E8B84C79FD88C1DFA2310838FDE18945F2D8C14DFA5BB147B557 + A5BF21FBF65B78E7A9870ECBFC83BB64DF91476ADD2FF86767C6CAC2837BF577 + 093730CEDFA0497B291302BA26009EB87B8FA97AD76E83C19376EEAED1E7D2C1 + C585F0F7176786CA9E030FCA786EAC797597546DD923E336EBEFACE6ABF5F775 + A2FEEECEDAB6CD60707E2CFAE9A9D15988FE715B760408FC02B097BD800907BD + 2513F0D700F00BAFC8303DDF8C8C2018F8EB00987D8081BF8485663FE0C1CFE8 + 796EC34B32468F87466DDCACE5970DFE8E7A4EC7AB00C0412F1A0006EC0EE0DC + F934113136C930142130E5811110038A01C440E1A1ECED1BCFCD431F7F2E03C0 + A4035DF186AE7E7ACEEFCF7CE141F6F87F2ACE01C2CD62A9F337BD81CC217025 + F89BCAC341930284BDAEE7522DAB7ADEFB9081DE6E8B569B80BD0E7F29775DF8 + 80E51D00037983533840606E8A23FC336E6052E658CCB96C5EA6622E06004E21 + B0035F9FF7798AD2391F3702121DA6330078E62283BE2E87BE3EA7F4BC3B815D + B64D493297F5F9ACCBC16F568F2B58E7C829ECAD04805D79F0EB0A00788674AE + CA3B800B14DDBF290076E72FF0171914CE015FFAB8B2BA61930CF0027A6B03C0 + 16127A6065F76F2A80B04361FA78DF14FCB22FB0EF0D9CE513F80BCC05FA5206 + 00037871EE96ED078CD8139810D1711C635C1EEA390F805DB885DD45ECF0973D + 83511EFEA28F0300A702E8E6EBDC09CCB53879840B98361CC15EE790B7C8FD9B + C25D9487BF291CCEFA1600E014F87A9BAF755416EB2425B09B9743DEA23A9701 + 6000AEE68BF6FAF5BA14FC66903796F320D8548703B8B1B63769CF9A54717B5E + 29E42D82C0E86600702A07BD0E806B9383E3DA54635C3D0170ADEBA7DEA75967 + 31872F7DC86BEA6BB13E2E5FBE11953D57AE9CD667E235D40180BDEED37400A7 + 7528DFEFD312AFAD71D38ED24C3FD7A6FA79366EAE9F13AF8DD7AC229FC15F58 + 4EEEFD78DEDF7B106DF4CB739876A5FA08808D0FE5FAE4C5984AC7CBB3A81A82 + 3BA52A3A46AA746C81B3B78C69FD1B00AEB72A8677CEB3D2B4ECF90CE27E04D5 + D803B812E4AD0B0ED750F2E28BDEC4BF1615FD1316C8BF20A90ABF689FA8C28F + 5625F98F99FFA015D5E7DB8ADA3F0D95FFF0D64FE9F81AB037AD4BE527D64AAA + 74F2CDCBFBC4C9C3CDA8684250636290EF574F008CCAEA2BEC079C2A1DE76017 + 797B5A9757768C02D89B177017F89B07C03521704DE85BA40CCA3AC8CD01E0BC + BC7FAA14003B304EDBD3B1D9F3D0465D8180BC0E6C1DFA7AD99507BC06766B69 + 4FEBEBB30770DADE50CB45174119C4AD008053E89B96BDCE443EAA0482071BFC + 4DF7FE05FE7A68E814F496F2A1ECEE5FABCB815F803017A969F867DFFFD7C06F + CCE7013069536DA3CED30C001BFC25D4D608B9ADA7D6F7D563F4D3FABE7A61DF + 67B801E0DB7A6B998BFDFE7A013F68ACB41E5A6510B8D5B0B1D2B16AA2DC3165 + 9A749D3B57C6AF7D5C16BCFAB24C78729DF4597CAF7499345BEE1A3BDD0070C7 + E1388013D7EF001D8FFA038003C44D21ADE75BF58F1038425D6F07EC322605C0 + 2E07C4387D497D9CCBC0300E5E87BC9ACFCBEBD350CF0E80491DEA9252C6094C + 9BD711E2D9F7FB4DFB7A7D0A80112E60C24023DCBF0060D47BEE4A83C0FD17AE + 164242037AEF9ABAC8C02F29A2CE8130E1A07105DF31857D7CB56ED25C5B044B + 5DC02C8E799D3B29EE9E1E42F0E5C16F5EBE20E72098B22FE0DDC3A21D0B7D2C + F845B8CB821F8B80EEF025A5EC8B810E802DD473ECE38B8CA9DBC4DDC02C4202 + 7E09E9DCF3DE55067CDDE9EB7903BE510E801DF612DA90054E77013BF0ED131D + C00E88D903B84F84BFE600BEFF715BA0F5BD7F71F7B0800B20B6055C95035F07 + C0E4A943E500981417700C25F9C833067B87AC7D4E86ADDB9801606030A0D7F7 + F31DF4F8D351C1E1EB2E6003C0D1D94BD867402F4EDF91CFBE6829301807B0EF + F94B5D00C09B0CFA7AC867C23D2360AF857BD614C80BEC65219E3D802957BDB2 + 55C6BFBACDDCBF936EC2FDFBD393A365C696CD59D8E7D4053C71DB5E99B47D5F + 0CFDBCD380017B490210266CDF6DE09714F85BB56DA75445F88B08DD0CF82505 + 0A07857A0030F0971417F0E43D8765CADEA3E6F84D01F094BD8764FCCE3D52B5 + 73B7415D00AFC3DDD4ED4B9E7AE02F02E492D2FF7BA72716BEEFBC80424B8E1C + B0F0CD01021F9699067C8F1AD09D7DE2B88A3CF017B72F50F86084C0C70C0253 + 17EA0F4564F6E93CBEF1F39F0B7B00CF3E794AE69E3D2F0B2E5C9679E72ECBCC + 331764CEF92BB2FEF3BF1A7B7E3A8F0F7FF87D5970F1AC2CBC744E165D3E7F4B + 40A803600F334DBAEACDEBF2179F20FCF5071078E1D58BB2E00AA1B709057DCA + EA3ECDC73C7D1D68F6991332E7EC490B1F5DD7E3E55FFE82815EA02D296EDF74 + 1FE0174FBF5CF1BB05F89D79EA84C9E131FB10CFD0EFD68CE347CD513CF5887E + E70FEBF7E82840F9ACED5F3DFFDC4539767E831E6350E1B10F9E7C5A669D3867 + 0AFBFE1E9775C7B65674207FEBF47459707087FE3EECD3DF89FDAA032A6E1C21 + 0DBF1D530FEAEFC7C1D765B2B64D3EB05F4E1E5B6D4EE1A2E3FDEE999932FDE0 + 117302037F71039BF6EC97CB4797D5E80FF47508ECCAF701D4E6EBDE3D344FC6 + 6F23F202B077BF8C7F4D7F4BB7944070F556FD2DDCA675AFED91912F6E93B5DB + D7D53806DAB5778DF60921FBD9B37D949E3746016CF537DF7FFF53083C62D3AB + 3294AD0680C07ADEE19C03F01DF1FC8B7A0ED2F3DA46A24F706EE26624DA7009 + 6FB1F3145129386FA191DCD4A4E740225B10E182731D5B1DF4D77329FB060389 + 81C0439ED2F3A1A6C39FD1F39F6AC8939B64D0BAE70C00131A7AE893DAAEE764 + DBFF57CFC96800E77F9CBE9A722EC7156CCE602D3317604E603789691F9B0F00 + 8335F570D02E730EAB985F30CF4843407BF8E7BEC99EC07D54BD56043730E0D7 + F70BCE00F1729DB32C7B547A2C0102037D71040385710097C03090D8C030F31D + 15E0B727DB63686A21A193F0D0BD00C400E0187185F9593A2FB3B95905A500D8 + A3C0DC39739189BCEF099CCE2BFD6642F2D4A50038AB8BF35994DEC8483905C0 + 19204E802FB01779DE4344037ED37C0A7DB3BCA533E4F6F10E818300BEB6FF6F + 5E16FEB914023AB87F81C0DA5FEB8AA06F2ADA2C24740C058D8AE02FF23D8153 + F85B048029E795EF9B3A801DFC660058AF051DFE06276F0E00F71E29ADFB8C32 + 017E5BABDAF61D6D6AA57200EC633D7F5B0FBD8E05E2E600B02B0B0F1DE1AF03 + E014F8A6CAF6025651F66BE672E05B5E4E5DBEC8CB79017FB90EC7F94B1EE8EB + 6A747B1FEB9302E0540E7833A81BCBB501606B8B70D795026087C088B58D744D + A436E5D74F00C3010E535FDE46395D570937E077AF0180F37907BB949B76EA69 + F2FA066DBB98BC4F06830B40AEAB711D7B03D7A64208DC0AC00B044E0070AB3B + 2DBD51004CBE22C4BD49D97123902D5B0F2D50ADEBA7657D3A8772ECC35EC006 + 7C9B8575DDF2BEB5EB665F5B269E335D774EF36939AA1200AE6B9DFC56E8937C + AE5AD5B8BD346C149CC0654EE626FA1AD37E09F328E220A5F74399B69A2C260F + 80F3ED45FAD80070A5F1695B7E7C4E19D3A2FC6F00F823A922144E75330018A7 + AFCBEBEA04C05A2E6B4FDB8A941DA3E003FA5708803F1B20F76655FE435597D2 + 1F37FF514BEB5C45633F09153DBFBFCE7C395F9F2A7B2FE4B54F1E0297F5CD9D + 48EB54C1C937539C28149ED06F811AB6403AC988B035AF14F0A2AC3E96F3F579 + 795B0A7651DA96578D7E05C0372FE06E1EFCBAF2003805BD387E519336DDB33C + 72489B81D908721DDC7A3E5F269F6F470060E465EFE77DEB0B808B9402DEFAA8 + 08FEE601707A81525487F2E037888B25BDB0CA81DF22791F1CC1A90C04AB7001 + 37ECD8DB1440F0C00CFE36EAD44FEBF538B707300C046E7E3730582F2E230476 + F0EBC7224F7B1E00A717ACA50B57BDC0CD2983BC11027B3EAD33301C017033C2 + 4377D70B6C00701F3D465F6D270434FBFF7287B68A8BF696FDF4A27EE058B1BD + 7C87554BEBE1E3A5CD88F1D276CC44B973DA6C19BC62958C78E021E977EF52E9 + 3E67BEDC3D9EF0CFD3A4130B1C43ABA5DD10162782FBD7F7FD7500EC60D701AE + E7D33AAF77C0CB3EC13E8E72BE3D05C0AE6C6C84BFB87DDB0375A323385FEF21 + 9F81BB691EA0EB4AEB1D143BECF53D7F5D1D47CF36D88B52009CBA803B55CD31 + F7AF01DF092C5C2DB0B4DD683D6E957EA611F6A2D4F5EB30181102FA0EE40B64 + 71B1CC17C4A8630F355B489BAA6DD342483E16D8CA16DA1239EC7577067DA8B3 + 45BCB8C8E70B7DBEC8E7001870DB93C5C3052BCC31E280B8EBBCE5D26D1E619F + 7195007C4BF0B7DB829556EEA27DBACEA74D85236549D89BCE172AD9A38E3CB0 + 1708ECFBFD9206F7EF6A5BE874084C0AEC4DD5E7FE0079598405EA5A584715B0 + D7EB81BFA42CEEB2784B4A9BB97958E4555948C8877C5FDFB8F86BF5EC1DB8BE + 6CB1D8F6157C44EBA3DB37847B7EDEF2D40D591B20AF01E0B53A0601789FD063 + 689A825DA0EE88F52F18D41DF65408F54C1928ECE9F06735FFEC8B32DCFBC5D0 + CF2C8C3BF875D7157916D309C109F025A54C1BA987847E6DDB8AC245FF4A225C + E8AC6D5B220C606FDFAD3286E36BBE7A2BE14903E01D6F7BFBE21CDB6EF0B7EA + B510EA195501790DF46A1DF077E75EA9D231E308090D1CD6F2F81DA13C76DB4E + 19B77D9754EFD2BA9DFBB57E9F41E3711C67074EDE927B6ECA7EF6D73C2413F7 + 025302E405E81A04F674DF810C0C3BF87507AFEFE3FBFAB1C70BDF7B91CE9C78 + A82600C6A978ECA8CC3E01040E0078C651C23FE30C7E3D387EB5CF74ED33F5C8 + A1A8C311997D7A8F7DBFF99B32F7EC59997902A7E7259977F1AACC3A7B51165D + BC52AF10C1B7F201005E74F99C2CBE725E9EF8E09D58FBF13D3C04F4828B17F4 + 792ECAC24BE7357FEE1309FB5CE971FAF77EC700F09C0BA765EF6F7D25D67E7A + 8FD9E74E9A66A9669C392E3FAA0F00FE952FC81C42409F3A29F3CE9C31E7F0FC + F36765E1C5F3F2C8F90385D0F5E7E7ABE4C9D35B33F8EBC2A53EEBC42973FD4E + 3BAADF374B832C6CF9D9B07FF39CD3E7B4EF39597372BFFCEC5C558DE3F39CAB + 8EEDD6F17AAC232765C99183155DC3AF1F7F4CA6E97716F00B009E76F0807DCF + 6710DAFD10BF1DFA9D3EC0CD23DA27BAFE71F50382571CDC5C3134F4E9130FEA + 38220F7053CA5EA9DEADBF9B9A5EA9008027699BAB0800E7B5E7C01A19BF6D9B + 546F8F7BAE6FDD2F93761C9209DB0E18FC7527F0F8D7F4983B5E9789DB5FB770 + FCC75FBFBFC6B1B8E187DFF7F05BBE5DC6BEAAE92B3BB23DDE81C080DFE11B5F + 35F83B9C9B8D2200263FFA05224EBCAAE79D00817103038071028F7FF5353D3F + B14F700900DBF96BE34B32829B9AF4FCC6798E731C0206B3ED819DF3F45C38F8 + 293DDFEA79947A20F08867F47CB8FEA5E0047E829BA98898B149CFB51B65B09E + 9FDD05CCF9993DFAFDFCCD4D5C03F5DCEE8E5F5CC0B8827D1EE0A28EB982835F + C45C827906F30EE4379B31BFC049DCFF81E014A62EEB779FF65339287600DC6B + 794C973D62D01747706FCDA30CFAAAA80F21A5B54EE743611EC50D73CCA38200 + C30687D95E43E75538811D00FBCD792E6EDE43F93A20701E00FB4D7F068EE76A + BBCE271D00FB1C13F91CD3D3FC9C94B0D09520700A820D064F9C9B016087BC0E + 7F53F04B3B7200ECA1A05D5ED769FC74D5B4180ADA01B05EE78C9E5C0302BB13 + B8FDC8499652263C34F97623F47A84ED710AE02F4AF7026E3D44AF77860600EC + 8EDFCC154C395E5B85EBAC00738B206F0A7BF36DE4B9C673673069EA06B67CDF + 20606FB3DE7A6DA8D789693868F20684497B0747709B3EA34C7907B05D634617 + B14160C2422700D81DC1A9333805C4BE6F700A7B2B2A01C0C06057B88E0ED7DB + 45A0B748B4851BB1075B19D8EB21A10DFCD28F5485FB37550A8153391076089C + 964D09F8F57C5EDEE6EB2045EB21C8D7418AEAF2F5A97C8D05655B6C7508C037 + 85BE9E7739E04DDB52E76FDAAF2E005CA4D4E5EBAAD4AF0CFCBAACEEAE1AAA0D + 005782BC79009C2FD717167B9F1B01C0F55138D65DD2A4C59DD258F3C0DF26AD + F53568DED75BFDB9D235D84ACA1FBF4895C6593EDD07D8953A820BA0B0AF693B + E8AC24EFF771295B6F8F6BF7FE3C1970CDF5FFC404E86DDC5E1AF35A9A779626 + 2A5E8F397E1BB533F0EB60381D977F3F2551A63ECF61720038968BFA67A035C2 + D94AC7FBC82A82AB15E4AFC95E1775FF06806F4829F025EF2A2A973DAF83D9FA + AA04809B6510B81C00D704BA0D5300CC58D214F456D2FF5B1CC0455F9E9B50FE + CB6E5FA6A4FDD688E7CBFF485556FAC3E63FD2699DAB68ECAD567D5F83BF6EC4 + 0F376097FAFC783F461104F6FE96A627D85A64FBF816D4BBAC3D7732AFA8663A + 2655519F54B5F47307708302985B09002387B4AEB40D158DA9AFD2633AE0AD4D + 35416FB1F2E0D7950260CB035701AF099C7578EB2AAACBCB216F1340ABCACBA9 + E89787BF0E77C9A7A1A18B94F6CDD7A7E03705BEA43ECEEAC96BEA1721F98B13 + BF2841DEEE7D5DA5315CE80427705E2918AE0D00036C5123ED83821B18A05B0A + F3EC7227700A7D8B04183640AC17A5B87EDDF99BBF700DD20B62802E60D701F0 + DD830D0237892902FA3AF845B627700680F5E2BBDB5069AA17D24D7A6A7F5CBF + 5C8CAB80BF5C9CB7208457FFD1F10EF2B1D26A480805DD6A5895B41939413A8C + 9B22B74F9A219D274C9576E3ABA553F514E9CC5DEEC326097BFEB61EC842037B + 00B3485125AD067097390B0C25F80B9C75A56587B9B6776F5226A54CBEC3D029 + 59BB8F4DFBB828D3D79DBE40DE0E005B550A8229B3972F50D7E16FABC19333A7 + AFBB8129FB3EC0C0E0B6C36668390061E4F5C0DF007B675504C0B87F6FAF9EAF + 9FE52C13D09770D05EBE6BD20273F7765039EC2505020385116560F0DDD3174B + E7C900DFF9726704BEEEF6F5053300300200DF3E659E815FC23C93A60B71B640 + 477EEE52E93667A901E07B66E858A4FD6CDF5F16F854779257F9229F43615716 + 3270DE3283C13D17AE921E0BEECF606FD7F92C3006A7498F45AB34AF6D5617FA + 109230BF50D9E3DE35D27BF943A65ECB1ED0748DEDF74B0A0866913375F992F6 + 5D895BE6A1CCF1DB7FF51332E491F5B6679FEDE31BE1AF2FC492BA13B8C622ED + 834FD982AF03E0FCDEBE2918A68D7D04873EF69C39891CF6FA9EBF0E83D97390 + BD7F09ED6C7BFCAA80BA805CDCBB0E7F491175B4E1FA45E4B3FAA737656E5F17 + 8BE22C84B30FF0B0F51BCDDDEB7BFC027FDD01ECA19E1D0623FAD83EC0DAFE93 + 13A36A2CF857124EE1D53BD6CBB8D7020818FDCA1603C094ABB705872FD0973A + 9C62808710EE79AFB6EDCD007008EF8CBB77BF8CDFB557AA76EC92B15B7748D5 + 76EDBF67BF4CD9F7BA10A299F2D81D3B64FC9EDD3271DF3EA936F7EE7E4BC702 + 88F5D840DF69AF1F35079D01616D37479E8A10AD93F7E3D20B90D745D99DBF94 + 671E3926B38E1E0F00F8E01179FCF02B85EFBF487F756EB88D9F79E4A8EA88ED + EF3BF530B0F79041E039278FAB4E98DCF53BE3E8510B678B80C041371F02FABF + FECF7F926FFFC55FC807DFFF81BCF39DEFC8DBDFFEB6FCFE9FFFB9FCF3FFF93F + B147FD1E40DE1557AECBBCB31765E629C0E315997FE99ABCFBBD1FC41EF57F10 + 32F9577FF42379EF07DF93F77FF87DF9E28FFFE823B9763FF7073F907BAFB21F + EF4539FDBB7587A2FE6FFFEB9FE44D7DEE1DBFF55539A9FDBFF3977F195B6A3E + BEF59FFE931EF7AA2CBA7C5975C9F6FC5D7CE5A25CFBFDFF107BDCD883F7FE9D + 5FFC423EFCC31FCA073FFC817CE34FFF447EF277370692F9DB9DFABD6FC9BC4B + 6765F9F54B37FCB7FCE7FFF3BFF57FE2CF0D9C7FA09F03FACE5FFE85D5DFEC63 + E6D913268034FAF1DFD613009FC5357CCAC247E3AA4640F61F5E985AE3FBF427 + 17C6CB8AF3C76CCCACD327CB34EFDC3983BC334F10FAF988A6388BCFC8AC93EC + 4D8CA338E4A99F7592FC19B9F7D431F9F9F9F165CFF13B6766C982A3AFEBF7EF + 944C57BD71FA81B276D7E1D36BF53B7B4466E1E03F141CBFEEFA9D76F0A00160 + 6030BF19388001C0C1FD7F3473032F3AB25B7E76B62604669FE19507B7E8F188 + 44B04726EE2562C13EB97A6C798DBEE3770607B00360F601CEF74975FCF5FBF4 + F776A74CDCB9C300B0EDC1BE65AF4CDE79D800300E6034FAE51D3261EB7E5978 + ECBCACBAF4B6CC3D7CD2A235FCE5E9A1358EB9F0B5E7ED371D8D613FE0175F93 + B1D1013CE6A5ADD93EC0A35ED473027BCEEB7986BD8087E87966B89E67C6BD1C + 6E441ACFF9E265402F2E5F2252D006040E3729F9B98A73D470CE517A9E7300EC + 30D8B73C00FA0E5CA7E75F959D0BB5CCB608DC2085F377C0E3619FE0C1EBC2DE + C10684FDFC1C9DC0C0603F8F0F7A54FB731ED7F33CE7F8E16B37DA8D5EE93C00 + B1D77F6F9D3FF8CD63EE04CECF391C107B7DEFFB1F0B6904C5992BF8BEC74208 + 680B0D4D4413B6B388A1A323FC2D02C0A1FF1A8B8AE2915132E8CB0D768061AD + EBAA73318BB2A2CA476731D0AB798BDC12EB5D56D6F6BC0BD8413070D88EC77C + 30DE5488BA70536184C22EE02FF352BF39117173637A43A38BF92D7580DF4E13 + 67EBB54809EE027D81BB0E7D53A72F79770AD3E6B0B7FD58BD3E48207087AA19 + D271BC5E375495C23D07D81BE0AFD7A57200DC2901C0A40E80F34E602F970160 + D2B81F70110026BA12EA308CB41C0293E7265C17EDD4A761A01DF8E2FC254D61 + 705ADF6A602CB3251040B84F70033BD04D21B0D5EB75A48582E61AB24FA93FA9 + E711D79B4D7BE8B567E204CE435F07BE65F0D7D56D88DCD655AF79BBE835B6A6 + 7900DC5C55F3FAB90480C335B647D80214577600236F07F87A7F52EA00B60E7F + 9BD337CACB29047650EC6597EF09EC72C09BEE05CC3A86435B6F478D3B9640AE + B7A7A22FA9AF8FE4FB85F5919AF23514DB82AB5DD8660B00EC00D7216E0A75BD + AE41DB2E02DCCDD79366AE60AF8B2E5F5409E4E6CB79A56D65D0B7201FC644D0 + 9BE8E300C02E07BFAE7C7BAA1A7D0DDA6A3E075653799F54F9F62CDF5CDB9B05 + D0EBEE5FEF831A68BE418B9A6BB095E4C7B572B3640D37CDD7A26CCD99BEA4F5 + 00C0E9FA785E45B0F366941E93727ECDDDFBF05CA4699B8FB995F2E730F0AB4A + 9F93D7618EE0A635DB1065AF4B5F7748838A584C565F0080D33119640500BB43 + D7D38F516540B70ED5E8FB6F00F8A3C93969540680F36507B3372BE0AF893D81 + 0DD8D604C029D4F5710D1A961CC4595DD2AF4163C4F10ADEDCBF20F121376ACA + 17B2F89FAA5123526DD32F0CF67E7E18EC9F50BF006CFA8DF86250DFC87E0CCA + BF649FBEC20F94892F70524E7FD45C0643E997EB9B57D15894FE387E92AACF6B + AB4DF6BE91BE7687C0599A9E488B943FD94671727679B9E8245EA87A4E0032E5 + FAFBC4C2544B08E88F438D0AEAF272E0DB38CACBAEBCDBB748458EDFB47F68D7 + 896B0C035D140ADA21B083D922D1E6ED0E8129E75DBE695F07CA693F2FA7CA60 + 33658EA5690A746F5429F8ADD46EC76ECBC546F1454DFE22857C517BA98E8BA0 + 921CF2D608F31CE5F569BB03600F011DCA21EC33D03700DFA000824BA01715C1 + 60AB8BE0B7C99D5AA7A9DF9DCC45A68360D234FCF36D11F8BAD3D7F39E027D5B + F6185906830905DDB4ABF6D7D4F25C487301DE5D2FBA2D34B45EA0AB5AF62474 + D758BDA80F619B5B0D54B1B830749CB41D36DED446D592FD81876BFD886A6933 + 24DC910EEC45E4D9031840DBAA7F95B4D063B5D4B468FFDFD4D90BC8CD60EE90 + A8C1134D6D54ED866AFB3060AE2AE6DBC53EB4D3DFEA55F46DA7C772976F1EFE + 3A1006FE923AF075D80BD475E7AFD7391076B5195A82C6D63652FB8F9A69A9E7 + BD4C8A2BB8B3B97FE74BE7AA79D2711C8B52210CB40360DCC0774E5E68D01700 + ECB097D45DBFEE08EEBBE001E9397FA539806F37301CC23D779D19C02D7BFDDE + 0DFC4D17D7583C9B1620318B70BE5067A2CFEC25D205A8AB32B8AB02085B39C2 + 5DEB4B3FADF7F07F387D81C2B7CFD0D7A2A2EC8B865DE7133E10A708E09785C5 + 95E602463D17137A70B5893CCEDF6E8B824B85D08496AA58A844BD08E54C48E7 + A5405F164143B9F78A8732E89B572FAD47BD573E624E1B5F88F5455677FC3AEC + 45BE20CB02AD415F15219E7BD3AE7907BDA4FD5713F6319471FEA6C22DE4D037 + 388908F9FC9C2D28130ADA433C037007AFC58954EEF4A59E50CF991398BE71F1 + 1A0547D3F32587AFA6D4B3F83D7AE32B61CFDFE708A9B9C9602EC0D75377F902 + 7F3DFC3330D842716A9985799CC20B373F5763A1BF36116694BD22D1C5D797C8 + 85038B355DACE52572F9D0BDAAA5967A9F2B8797CAB5A3CBE5EA916572597549 + CBE8B2EA227DB4FFA5C3A441F4B97A6C855C3BBEC29C70978F2C952BC7965919 + 3072F9A896F578578EAED0715A3EB25CDB569AA8633CED57B5FFB5E3F7D9B82D + 879F8A6EDFE8D27B3D801B42B80287A71C3C283300B7470E1BB099B87F9F4C3A + 70A0F0FD57D293C7B7C89C132754C76358E7007F4308E810FA16CDB210B601FC + 66FBA09E3E25334E1E97A9C78E446456FFC77FFBA77F92DD5FF9B2CCD263185C + BB705E669F392D33715B9E3F6BA0EDC437BF7943EEDD2BFFF1F765CEB98B32E3 + D45973FFAEB8F6F60DC147C0E7BACF7F41165EB922F32E5D90F997CFC9822B17 + CCC94AFED10FDE966FFFE59FC7DEF57F7CF8073F9025D72FC9E26B172D5FDB03 + F87BFFDBD764D9DBD7556FE898ABB2E8EA55D9F8C52FCA5FFC8FFF117B85C7B7 + FEECCF64E99B6FCABDD7AF1B005E78E9A2EDFB7BFF1B576FE87DF39CD7BFF31F + 65E55B5765FED50BB2F0FA4599A7EF1980CBFB5E7CED922CD7639EFEDD6FD609 + C23FF8E1F7E5BE37AF649FD9FB3FF85E6CA9FBC16B3EF1ADAFCBBC8B6764FE25 + FDEC2FEB31F435CCBBA8AF43D3B917CEC889DFF9A6FDEFDCE88330D473F5B8BC + 2ED21FFF6D3DF600FEB55F96B9E74FCBFCF36764DEB95326F2BBCF6FACF13DC2 + 99BBFCDC610B2F3DFB0CD2EF876AD669FDAE9C42ECFB4B086984ABF8B496439A + C25FC2445B3E6AE9A923E604C6E5BBEDE406619F6BEFB3E6D4BE1AAF031D3CF9 + 847D97F95D9805D48DDF6B134E5FBEE3FABB8170034F35E0BB5FFB1FD4DF9403 + FA1BB257CBFB4C848FFEC5B9E1359EE34B27661BF89DB067970910CCEF5DBE1F + EE60A0EFF85DBBB4FF5E03C5F93EAEEF9CACB6D0FAF49FB07387E6B7CB582231 + 6CD5636C3F601AB795FDD655AF117EFF80CC3AA47F9363E764CA9E8332EA956D + B2634FCDBD8477EE5BAD6376D8B146BFB25546BEB84546BDB4D542FC9B5E7C4D + C6BC4458E8D764ACB6938EC2D1CB3908B0AB1AA36300C19C93381F79940ACA63 + 5F0A2EE114008FA40FE73294EC71EFE21C09F84DEB280F7A72830C7C5CCFD14F + 3C6BE758BFD9AAFF637A1E7F82F3EDF3B6973EE765C4B60B6CC540C48EC1EC1D + FCC8537A2E7F46866A79C0234F4B5F40EE437ABE376959E709CC1790B981992F + 30EF602E11EB996B30CF70F86B37A14560EC73119F9784B0D08F488FE50F4BF7 + A50F5A0AFCED7B9FB6AF78CC04140EC0F72113655200AFCBE6523A7F72F0EB29 + 11567C8B0D6ECCEBBA50E76851DD16DE677516DD45F30E853D34B4477A299B13 + C6F922A26C753A4724620CFB03DF4578682D77D1F92A73569BB7C679AA83606E + 54E4A64520B047B0B96B8A8A1B1EA98B7210DC19B7EF84D9D20955EBDC7A22E5 + 39D2B17A967418AF73F0F1B3ACEE6E9D33338E7EB4511F529DAF8FD56B034D3B + 55EBFC7D9C5E078C9BA2F3F469A6F663F59A63AC5E53680A04C6158C3BD81DC2 + D4993B383A840D0C27CEE0B6C3F55A65845EAB00834705A7B03B84DB0E9F602A + C1E250D75EC7B41FA66D43ABA3C64BBB61D55A576D693BAD6B33A4CA445BDB21 + DA4E1FF2AA3683ABA43537F3A2817A6D37708CA9C580519A8E36D0EB00B90806 + 67EAA78ACEDECCFDAB75C89CC200DE5E7A2D09F48DB0D76171E6FC55791E600C + F025C473BA977019008E220474B647B0A6B7751B224DF41A19009C8682F6B419 + F531CDC0F03DB878B9B60EAEDE708D1DE0AEBB7D1DEE3AE0A53EED93F6230F00 + 4EA16F5E0E7CE9E7C0370F8233E76F540682937CE3CEBDA5515451BBB7793B69 + 430BF19C5F0B2957517B5AD748D590686C1D7A4AE30C000373BB6650B7920B38 + 75FEA67DAD7F47D68F588FD2F604E0A665570A881DF29252D7545F1322EF6DD6 + 9EC15E3D664C4B10F8EE3A9582DD3CE475A8DBB49DBE86D846EAF5DE276DF33A + 4F2BC9C1ECCDA86C0D35AA6CCDB560ADF586D66DBD6F2DC76E8488E2D834AC45 + 37C27D4C4447D69869E37DFAF3A66BD0BE269D280D019DAD73E7D450D5A819C7 + C9B5C571D978576EBDBD68FD3F5D9B77A5FDD3BE1F876EE49865619E1319008E + EF257DCD006B5CC216123A721FEA612D40DC26CD01DAA10C276AA47DE86BE504 + E85682BF9F8AF4751ADC25CFEB73D0FA59D5C7098013085B5145E33E29E521EC + 8DAA0C0037E1CDD413002721A4B3BA0CFEFAD8E4C3F92C7C58372127ED4500D8 + EA0DFE260058BFCCA879CB3BA4D39DBDA563E79ED2841F0AFB81F894BFC8850A + 3F5E454A7FF05CF6A39E00E0A23EA8E887FED3545DEFAB2E6527347EF091E7AD + 9E13623CB116293DD1C6BA14FEA6CA9FE02BAABE9308974F2662399D94DC6A00 + 5C9B52C85B9BF2F0D7616F91BC0FAA0D00D780BEB16C7D12885B9B1C00A75037 + AD77B05B09F8A21A635579E0EB103885B99EF7B6B4BF2BED9F973B816DAC03E0 + CEE18ED8F40205A577B6A2F4AE586FF33A03B839009C07BC699D975339FC7500 + 1CC23887F0CFEEFC4D21B0C35E07BFBE07701A0EDAF27A410AFC759520B0DFA9 + CC856A00C0E6FABD8B0BCF120076C88B52008CC8037F5BF61CA517CAC3F48259 + 2F84D90B38B631C66171CB1E7A41DF432FF07BEA457F2F04041E2B2DFA69CAFE + BB03E2E200AEDE8155D27AF0786939A45A5AB2B84059E5FBFD926F3310D00B00 + 1E6F0018A500382F77ED3AFC05E09202765DD475183E553A8E9866EA3412A08B + C337F4F3713E3628B87C1DFE7AF8E7140A2377F102721DF49A9B370260E4E19D + 83D337C0E154B87B3B8C2977FB9601E031B3E576DBFF77BE741A3B573A8EC37D + 5012F017107CFBC4F9D279D23CDBFF17D8EBF0F7CE29387BC3BEC0D4F59E7FBF + F49C779FDC3155DB27CFD3F6E0F6ED3263B1EDF5EB2E0A735BCC0AFBAD190CD6 + 7616DD08E96CD076FA02E93C6D7E5894D3B22FDAB1A0C7629E2B5BC48B0B7AD4 + 79F83F5F04040C1B1CD6B2BB4882137879E6F405FC0281DDED6B79F6A15BCC22 + 64C9F5EB79162CDDB1923A5758C474672FF017F72FA9EFF98BCC117CFFA3D277 + 5509EEBABBD79507C02CBEB2871FFBFF92B270CBDE7EA57D7EC362AE2DE86ABD + 0361802F8E20844B2803BF8F68BF878370FE12FED9F7FCC5F96BF057F3039F58 + 2F031E7FC65216A38713BEF9E98D61717ADD0603BD43E8BF3E84744E17B44B8E + DF8DB6E00D001EB3E9D54C2C962316CE1DF49616D593B09AF463715DCB23697B + 65AB1CDB7F63E19FFFA5EA176787CB63075F94C9FB7107BF6EEEBD0080F7CB94 + D70F98736FF2C10332F100F057EB0EBE5E789C4AFAFCE9A5067A53A72F297533 + 8E95DCBE067C4F857D50339D396D0078CAB11B0B01FDF59FFF5CEEBB7645E69E + 0BEE4A1C93EC5B0B08F6FCC24B1764E1C50BB2FCCA25F9CFFFFDBFC791B53FFE + DBFFFA5F32FF921EF7C265997DEE52BDDDBF80C7E3DFFA5D5972FD4DB9F78DB7 + 64C1E54B320FF878F95C06413D9D7FF9AC9CFCDD6FC691F57B7CF8873F90C5D7 + 2FC9C2AB17EA04C07FF13FFED1FA2EBA76517549F357F4755D37DDFBC61B067D + 797CE33FFD27597CED9A2CD13ADA165DB9620E6000F037FFF44FAC4F7D1EDFFA + B33F9507DFBE2E8BD9B3F7EA395970ED822C7A43DFBFE6E75F396FAFF9DE372E + CB527D2D0B2E9CD1BFCB59B9FA9DFF1847971EDFD4E33CF1B9F7AC3F90D5D3FA + 8268C032E0D89F13F9E78E639794BA25FA5AE8F79FFFA17EFF13FE0000733CC6 + 73ACFA00E02DBFFEABC2DECDC8F6103EA7C7D0D7F257E747D4F81EBD74F66583 + BF68AEF6A3FF5C424E9F3E2E334F1E35003CF7B4D69DD1F713812FA2BCE0FC05 + 730703836D9FE02883BDAA7B4F1E9665A78FDA3887C4B47DE5CC821AAFE35BA7 + 6718E89D7E04E84BF8765CFBBEB7B77E9F8F95CAB38E46E7BFFE86F85EDE93F4 + 77841B4900C2E4D913F889C3C57B1D2FDBF79254EFDE692078F2FEBD72E5D8D2 + 1A7D80BE84810600037F7121E7FBB896EE7E51C6EFD825130829BD8B3DD61130 + F880D61FB07DD481BED53B5ECFF2446218F9CA76D53619B365A72CDCF6628DE3 + BE7B78818CDBB653C60081376F33A730F23DDEC7E8F871AF6E536959CF3B9C67 + 38C78CDFB2C3EAB9E9082730E7293F27911F0B34E69CA4E72EBF71C9CF6326CD + 8FE6DC45FB869732C89B397ECBCE91A10D47F0A0274A3757D97958F37EFE1DFA + 54D8579FF32F75D64FF3DCB4453AE0519D073CBA4EF594F47BE849E9FBE093D2 + 3FCE0B908786B6BCCE2510F3843E3A5FF09BC900C11E7904D90D67515E97AA8F + CE637CFEC2BC0500EC7B08F7D1BCC3DF6EF7027899FF04000CE4CDE6442AE64D + DD1C0067BA5FE755CCB9742EB6E83E7303BB98CBB1BD07F2799D0360E6773EEF + B31BFFB4CEE786CC137D4E695161E21C9179E75DB3752EAA2973527703FB1CD5 + 9CBF53754E8A746E9A4261B63931E9BCD76E644C22DE981B3802E0CE13E758D9 + EB3A027855D41B3056510FEC6DAFF27AFAB4AFD2F9FC78BD66A8C201ACD71AD5 + 33547A4DA079E02F2018B8EB0038DD1F18009C868806002300B041DD91684221 + 0026A59FF7451D35DF81FCF0087C81BFC0E208780DF246F8EB20D8E5FDEDE6DD + D8DE7AB05EA399C646954243BB9B3805C0290836E0DBB73C0CB4D7391C4E9582 + 5F973B812DA47474FF02805D067B01B911EC524E5DC20E889B026901B9B18FC3 + 5FA0702A83BFDDF458D101DCA2EB20DBD33785BC1ECED9E1AE035ECA0E8BD37E + 9E07F2B6A03FC789601751EF509832B0360F7FBD8E342F07BCF9BA1B9101E108 + 81D3B593BAE46B2A0888EC20B90480BB4AC3765D545D33B09B425E97D7E5DB8A + 0070066B631901759BB4275A5D49695FDA6FEBD8D3449EBA12E42D1D2BDF963A + 7E5D792730D0D6C16D93B6F7D400B7693D72009CCAC7BBBCBE41CB3B4C695F64 + C769155404785D656BA551697DE19A6A525FA6DC3A6CADAAA5AF1FBFA1B6376A + DE599AB4BC5D1A6BB951735CC8AA665A4E42505BFFB8EE9CAD43A7EBD25165EB + DDD9FA76817C1D3CE9E363D3F1691DCA73811B5993FFAC318554FE3EDC216DE0 + 57CBFE7A9BE9FFDB9D5DFACB1DF7E86F522BFDFB007601C48C49F705FE3700FC + D1F56F00B8FE2A770027D037551D00386D2FD3BF22005C6B9BB5F38F17FE019B + EA0F40C7CE3DA577FF51D2BDB74E94F40B9F7D99E217FCB3A3F0C355A4FC8F1C + AAADEDB3ACBADE575DAA71E2F3BCD5C7136A25A527D9589707BF881337697A82 + FFD8949B4CF824C5F42901E014F0D6A91CE4AD4D29004E15DA75929A9303E054 + 0E6F49F3F9BC52888BD27A6FF37C5EE938CA8CAB14EE19689B425F07B945C018 + D5067F5121002EB840417900EC755E5FDE8F104AF40D4A21700A791D00A7F559 + 39913B7871FF96C23D970BB89B02E01402FBDEC0BEFFAF877F76A500B824BDB0 + BD9B3B98B9681D22CD00B82A402E02FE220FFBEC50D8006F4FBD30EFA517EBBD + 80BC23AD4FB32EDAAFAB5E70771F25AD7B8C96D6DDF5225E9542E016BD348F7A + 8FD58B79ADEBABF97E2C067087F8386981344FB9650CF50C3076688CCCFD1BF3 + 00E01402E781B085742E00B90E8081BD0060171098D49CC065D0D705309E2E9D + 46CE34A500381F163A05C0EEE805EAA65018F8DB79CC1CE9347AB6C9F7FE358D + 996DA19D117BFC52F6D4C0B08A7C476DEF386E9EB4A7DE00F02C09A1E7B4DD21 + 308E60F627C3CD808381FDCC54005ED4919076DA76CFF4C506773B9B43629E41 + DF7BA6E1FC2D39273CE4330B6B84754616F29910CF71710EF00B04C699E1E0D7 + 21B0435F57BAB847E83F5F14744748EA0C491709D9D717E08B7001E3F6ED7DEF + 1ACBB32F30655F8874B7AFCBDDBEBD01BC510E7F1D00037AD3B404861FB1C552 + 164D53F09BC25E5B58D5328BAE9471FFE212760D400F05D89B8679F6B2D70180 + 6DDFDF18EED9C33C672123559487AE0BCEDFE0260A0BD02C3023169B7101037B + 47E0E67D6693B5BBF317F942B6B97D13104C3E5DECC6018CC690C60575C2687A + 084DAF2B537461F9E23C0E2DF676CC2FF4FF6B1510187032ED60297C2B1018E0 + 8B70FFBA7317B853748C4AC259C818C0EFDC53274D0E82A76B3D219E1D00CF3D + 735AE69D3D23F3CF9E95D984B6C5D5A82910B8BE8FAFFFFC67067A71F922F273 + CFEB312F9C9339E74ECBACB3A7648ED6CFBF0480BD60F965972ED61BF83DFF2B + 5FB4FD7F01C0F5750FEFFDADAFCB82CBD765F1B53764D1D56BFADC17649E3E3F + A010B728221FC0E6795974ED829CFA77F587C09FFBA31F1A7804EA5E2B80A7F9 + C71BDFFBB62CBA0E90BDA8E37003BF21CBDE7A43F3BCC66B72E6F77ECF603079 + D7A22B847EBE246BDE79ABDED0F5DCEFFDAE85A65E7CE5820160C0EFE237F575 + 6A0A085EACE9D2B7AE1A005E7459DFFBC5B3B244DF03EF853D7E79FCF1DFFF9D + 6CFFCD2F1BB4E6F3E13D3AC03EF0F5DFB23E753D1CFEE6C707E05E92B7F3B758 + 76EDD20D85E50600338EF180E4FA00E0AD5FFE3559E84EE40882375FDC5CE33B + F41BE7161AF475F03BFF02FFCFEC457D4A669F392E334F85B0D0F32FE8FFD479 + 9CEE6764FA8913FABD39A1FFF36765E105FD5B9FBB6000388486C60D7C2A03C1 + D4E1B647B3F4FB39E3F871DB3B38BF0731E50547F6CA349CBFDCB8A1DF5F87BD + 699AD67B1DFDC97343896BF2C1F01BC3EFCD574ECD2B7B2EF4C18945FABB44A8 + FABD965E3DBEAC461F73FDAAB861C595EF837EFDC41C99B47B8F858B9EBC779F + 4CDA139CC384D39FBCE7904CD8795026EE3A18A12F10D8B54F466FDE21A3D8DB + D7C2F3EF96BF3C3DACECD8DF3E516D009850FD211434A1FEE31EF084FC37F81B + E4EE5F523452CF476C3780380F8DD73E9CABFC4625C4392B85BFB6454154D8CE + 40F5DC4B19E8CD9F17FD7C69E7521CBE7A5E75F8EB7914A07000C3767EE6BC0C + 04560D89E9409CC28F3FADE7766EEE8A377B3D54BE0FB0835F573A6FE0C63157 + 1EFCA673149F9FB8BC9F95578632A1A01DF6028181BFE401C4947D6ED4E7BEB0 + 3D86CF93BAE97C8B1BEDC2CD763A3F53E1004EE12F222C34F3B9BC7C7E07F835 + 1770324F3458AC7DBC9CEE15ECBA1B10CCBC931B13A32C324D84BD7E13A39511 + D037077ED370D0EE04F63485C12E87C1E4E9E3C0977A8E433DE5F655005FC02F + 5BDEE0082624740902771817C23E3BF8CDCBDB50B617F0A8491920CE3452AF55 + 22FC75209C02E00E1100037D53A500D8E580D701307519042EEC1B00302E6187 + BF6998E81404B32FB041E0FE5C0B46C72F203782E014007BD9DDBF29082E07C0 + 9AE620700A7A81BAA42900260500E30206EEA6F03705C05ECE007184C0C05C87 + BA2875F73AD875D0EB7DBC9E94FEA81200A60EA500D8DDBEDE9E82DF14DA52F6 + BE2EEFE7F2BA745C25E1E0F5359274ADA47C7DA454A6DD23ABA1061D83C89742 + 40970070EAF04D550481BD2E6B4B0070911A6BBBAB0800236FCBD7A3F45894EB + 03805D29B84D01705A5F09F0A64037DFEEF017E5C7507600EC1098F5D04AD037 + 55110046D467C035A9BF51D57F3C6BC99DA4A9BE9F262DF5FD358BF0B745E975 + D8FBB3B6B0CE5C716DDA945BEBF67CB27E6ECAB7ABCAD6CBE33A795A873ECB10 + F76695BE27E70B65758DDB496BFDFE0E1C325EFA0D1A2B2DDBDE6D91612D2AEC + 6DB886E9C7E71A980C79EA3F4B003880D512E3FA4CEBE30E01FDB10A065AC058 + 6F560E616F560E80C917EDFF6B4AA06E3A36ADAFA17F4521A06B03C0161EBA29 + 36FE28FDB2E3F86DD7A19B74E93158EEEAA61393D6FA63AC5F68FB32FD1B00FE + 5454D7FBAA4BD9894EFFB6D989CFD304EC16AAA0DD4FC6452A3ED1DF982A4D20 + A8AFA15B0480DDB56B2EDC82F60CEED6A112BCAD9F1CF8A62AB5173B7EBD8EFD + 7F91C3D814E6E6CB5E479A825C87B9F5517E8CC3DE22016D1DE816ED0B9CC25D + EF5B0480BD1E008CB27A2E3A3A1543E03C00CE5FD878394061BD58AA008153A5 + 00D8A06F5A9741DF00818306689D5EF825E0D7C33F037CCB61B15EFCE5403002 + F6968B8B4D1441AF85AD2A893A0F07ED2E5EC0AFEF03ECE0D79CBF3D460638DC + 75A8741E3249BA8C9EA117D8A3E5B67B864ACBAE230DFAB6E931C644BE7577BD + D8EFA113C24C5AEE35CED4B2F738BD600F6AD9B74A2FF223DC3577EFD8E0F205 + FEF61D23CDFB8CCDE4CE5F9407C1AE0CFE0E2A8572CE0360AF73F86B61A01320 + EC10D861B1F727C433F097D4C33F037F1179EA7D0F6007BE594867CDA7E51400 + 037DCD0D1C9DBE08E0EB003815E0B7FDD8D9D2817CD55C4DE76999BCB68F0704 + EB6BA8621F327D9D55B3A4B3D6F922552A16B542D83A7DAEB85875FBE4B9067B + 7DD10CF84B9A02606FF7BD7E6D7FDFB9618F5E9C1880DFCCA5912CCE210BF7AC + 229F5FC823B590802CF251D634EF0E71200C000E7BFFE220B93F730377D714C0 + DB7BD983067F5307700A83ADCFF2E0F0F5C5CB22A500B81721127D2135595875 + B7AF3B7D7DD1D5CBE6FA8D0BB4847E7687AF2FDEBA6C11F7C1A07E0F3D6590D7 + 416FB6C7EFE31B6428219F7DFFC0C7D7CBD075E5D0D75DBF61D1F9B9087F37CA + C80DD1010C007EAA1C00E3F0CD40300BE0C06016BD73E19DCD31F57C70FB3AF4 + 6551DDE540981411FA9985787362E9D8C92F6D2A5BE0FF7F835E3BF264E60026 + 6C2B29EEBC14FE027150D1F8DAB4FAF84E73FCCE3B7DCA04049E0D40D2634E3B + 7AC494867D9E77EE8C81DFE9278E998317D5E741E85E9CBF8477C6E1EBAE5FC2 + 3FCFBF784E669D3B2D33CE9C949940E00B670DC29202811F7EEB0DA9CF1EB0BF + F6E39F1AFC7DF0ADF7624DED8F0FFFE08F64D1D5EBB2F0CA3513E19671DD2EBA + 76D98023D0100120533849DB977EFAE37894DA1F9FFFA33F907BDFBC6263377E + F10BB1B6F6C74FFFFEEF64EB57BFACE3AECAD2B74238E87BDFBC6E6E5FC23E2F + 7BEBAD0C022FD2CF74E1557D5D572ECA99DFFBDD7884DA1FBFF39FFE54565C0F + CEDEC597CFCB12C23CBFABCFA302FE2EB87E51EE7DFBAA2C7F07E7B11EFFD239 + 59A2C75FAAF9C56F5C3688BAF6F3EFDBFB020853E633E13D028DA9C3155CD703 + 58FDD807EFD85877F83A2C77F04BBD7FE6E9DF6495BEB6FAC26E20BE3B8939D6 + 4FFEAE6E00BCF9CBDC4C9038C055BF7971718DEFCF23E7F745C76F80C0EE04F6 + 30D0212434DF137D0DE7B8D1217C7F66121A5AD30CECE2BA3F19EAFCC68A69FA + 3DE47B36F314C709DF35FA6E3FB9A1C6EBF8FC9965E6D2E7BBEA377138E475D0 + 8B080D9D87C0DECE6F09F0D7C2451F3964BF2FFCDEAC3ABCADC6F37163CAD4D7 + 894CB0DF00F0B5E3CB6BF499B0674FE6FC25AD14027AF3A1750116EFD96B0078 + E2EEB05FF0845D5AB71BF0BBDF602F8EDFAA6DFB54ECC3BECFEAAAB6ED91B15B + 776A79B7398601BEE9B109FF5FB563B78CDDB653C6B1A73061A5B76C37183C6E + CB36A9D27CD516F61A0E4E606E34F2F30D1AAAE72ECE3BD45B88683D1F99AB97 + BAE4DCC5F96C4404C06C6F3024EE6F4F992D0F383FFA39D201B08B32DB248C78 + 6653B687BE0B10CC39D7CFCFA469840ED7606EDED2BAC17A2E1FF058B8B18B6D + 1DEC1C4FF48F87891A529A1FE00CCE036087C0691E59D868E02E73129D9BF84D + 6AE9DCC5C13062FE626D11F67A1868529CC101028728293E7FF27912CEE0D25C + 2B40E01AF03702E12E0B744EA773399FDFD59CE345F95C50E573C554CC25996B + FACD87C8E6A051DCC868F359ADB79B1A351FCA2AC02FED11FE3AF4F5B931A28D + 4838D4F93C1AD04B1B750E8149293B044EE17027C6103A5AE7E5848566AFE0CE + 5AE7FB05B71B4368E8A014F8E20476B933D8D511089CF425050003792DD4F3C8 + 4959B86887BF99B43D3879A362D995426107BF9E4F6170AA14001B044EE06F5D + 00D85DC00E730DE846F86BEA1B15DB1C02A3ACBF2A05C01E02DACBA93C34749A + 0700A3D4019C82602FA7B290D011E0A6303715F5F936CA067C359F87C2067A55 + E48B20316A02B405E8D29E941BE740AE035EE4E0380F7FEB02C0697FABCBAD93 + B8F2EB25AE1A00B8430F69C01A8DE64308E76ED2A87DD78A6037AFA27E595D02 + 7953A5E0D661AEB7A530D7DBEA921F278C2D81DE4CC05914CB0663DB949CC079 + D85B54579BBC6F1EF8E6CB7501E08A6BA6896A5B5BF5B67C9F7C7D5E75F5F1F6 + 862D0801DDD9D4903226226D6B948CC7586440D80D46188172EBD125E5D6B73D + 9FAC9F67B2B5EFD8E679571CEB20B436B1BE9EE6FF25CA9DBFE43D3AAABD27CA + 0DDB4A53FD6CEFEA3240EEEE3A409AB7D1FFB5E611FA3AEC85C54436F49980BE + A932B01A95C2D6CFA23EB300F86386BF2805B237A35A017011D88D0AFD13A563 + B263FCBF03007B78670FFF0C046EAC5FFC16FA4547DCE191DEE561A97E91325B + FDA72A5E53B1D21FB8B2365E77FC912B523AEEB3A28FFAFAF227B62C6FF5370E + 8051067C73F906A4F1E47D33F249467E1291D697B76B3E01B3F55183663A7953 + 15B5B952009C81E00238FB712A85C025E89B2AC05E206F110476005C09F6A665 + AF4BCB79A05B4969BFB47F1EE6E601AFC3DBB42ECDA7F2BE4572F8EBB2FA0A00 + D82F5ED28B166FABE9000E109714E89BAA12F44DC764F5851A1081AF5E204600 + 4C1DE1A26BC2E22087C319088E0EDF70677280BB35DCBE89686F01E00500D34F + 53E02F6A7CD7A0CC098CF3B755CF5101066BBEDBD8593260CA62E930A05A2F58 + 8719006ED56D5404BF25B5EAA117FBA6B1D2BA6795B4EE5525AD328D97567DC6 + 4BCB3ED5AA0931D572DF007753E89BCAE1AFB7E54170EAFC4D01AECBEB8A80AF + 83602FA7E3DC010CE005F67AD86707C10E8001C480DEBCE3379F07FAE69DBF0E + 7F5DE6F25539F825ED00FC1D17E06FC7F1F3A453D57CE9347EBE857B0600B3A8 + D4893DC89294B074B667198B4E512C60F9A2973B1F3A4F9E6BCE089C13A96382 + D4C2E7A9C8B38066C265313BB8352C64730278599C03F2BAFBD717EA3C4F9B43 + E00C08CF2F3940C8A71038CD93E2F2F53D7F3327B0E601BC77CF5F61F27D7F53 + A510B8675CBC2C02C1BEB0E9F52C88A68BA5E459287528CCC2290E5F52CF13F6 + 79C823EB659003DED5215C237977F16410F821EDA32A83C0A9F3570500B6FD7E + 55EC1B3878ED7A73FD02750DECAE23C4247BFF06283CF8C96765E833CFC9F0F5 + 01020F7D5ADB9FAC0B00ABCCFDF45200BF11F48E7EB1E49672E714FD4853008C + 705B55A946699D2FC6B310BF71DB9AB205FE5BA97F383740BE73A25ABE7B7282 + 7CF7D444D504F95E4CF3A26FD1313E0EB127B0BB800132330E1DCE4240BBDBCF + 4170D1F8DA74E4F49399FBD705109E097CD2E34D3B7A3438814FE8F39C3A6E10 + 0A20558255272232ABFD71ECEBBF6DAEDF25572FCBBD11048770CFC0BD0B067B + 81C0B3CEE3BA3D27F32E4727AEF60314BFF7BDEFC623557EFCF86FFF4EC75C93 + 53BFF71F624DE5070E61E02FCEDF0597AFEAF3B04F6F00AE804D948247E0A6D7 + 0105D933B73E001207B01F0FB849A8EAFA3E00C1DBBEFA1559FCC6551D7FCDE0 + 6FAA256FEAEBBF7E555F1310F6A27CF7177F1947567EFC777DDF00F5FBDEBC2A + CBDFD0D7A49FFD527D6DF7BDFF962C7DE79A01E085B87FDFBE2ACBDEE639439F + 65D7F53D5C0FB017F179F07E1C8A23E02F63A8279C755D0F3E1B87BA0ED9FDF3 + 75F735653E3B9E87E7A58D31D47FEE0F7F188F54FB83BE3C07109863FCA43E7B + 007FE95764AE8EE1B9784EC6E6C33FFFC985F1E60CC6F10B044EC1AFE7E79C8D + E09610D0E7CED9FF3CDF03FF1EF977C9BF4F0E7AA907FE7ABD3BE7E79E3D6DB0 + 377D1DE8A9D35BED260D340FB7FE693D0EDFE3087B01BF457218EC6583BF1100 + F3FB820398DF9C6F9D9A5EE339571EDC9CB9800B01F0DEBDD9DEBFA892039836 + 0B170D3026DDB53B03C0137707F80BE8653F60347E3B0058FBED04146B7E07FD + B55ED32F1E9D55E3F8553B77CBB8EDBB0CFE8EDFBAD3D2BC80C2559B0304C605 + EC4E60CE3F5E060CB325816F4B9045A988A29EF3998BF35F5E766E8C4E60979F + 3389B031F2D9170DF8BA03D8013079402FE765EA909FA3BDCEF6E0D7D4F6107E + 42CFD1F13CEF20B8AFCE077AAF5967738301C05F1CC26B74BE10412F72F8EB0A + F38D52BDC162FAEAFCC501703A87F1F90DF5D6C69CE73E6D8BEABFCADDC1A579 + 5151F41422ACF45CCA0D78610F60973B825197453A3F4BE674CCFBC2F61E610B + 10E67FB4593BE057DBB27922ED51D431E704FABA7C1E8AB2B9AA8AED4CBAC6B0 + D05E07042604B4035D441EE8EB5BA0905207D475D04BD9C7A43758A6309832F3 + 6EF611BE73A29623F0E5C6CC6C6EAEE5F663679813D8207074F9027C6F1F3FC3 + 9486854E61B003E054ED70008F08EE5F9CC2EE087639206E33AC5A5A036E2300 + 06F8E20C76512E02C22EAF73D1AFED50C070740D0FAA09811D04970160A03091 + A0FAD60CF98C9AF71921CD7272E86BE037ED0BF08DE0B72E009CE601C0599868 + C07004C029084ECB0E856F8BE0D6E5A01639D8F572BE8FE753009CCA61AF41DE + 5C5B067B01B300DC44595D84B6AE22D05BA93E55DA27BF2D164AD74DD2F513CF + D704C0E47B49934EBDA469A79E2A207009E6FABEBEA18D759B521B4AE1AF857F + 8E757900EC2E5F2F67A0B7D5DD02C4F57AF24500D88FE17D52E89BAA0CFCBA5A + 95E02F72C05B1BE84DDBD2B4B63A9402DFBC1CFC16C9D7488BDA52A5EBA9454A + D7603FAAD2639236D6F7D5048359CBCE5AD7A9AC2FAFADB1CAD79B0D00C7F1BE + DE5C2EFAE5D6B7B335EEA01AEBE1B549C73AD8FDB8E4CF5F49E93AFFCDAEF5D7 + 57E96B226D8C1B5BF3D9736B1DFB013769AA7D60425A6ED6EA0EDB07B8018640 + E3286DB56F64310E58CBF8CC674019588D4A61EB67519F690770ED5CD594F1D3 + 7A2885B937A31400233B68D18B4A657D9217E12AEB83FEF50260AF374520DAD0 + FEF934DF50FBABCCE21F61AF8161FD32D1D7EEF6D0BE962F50E197F096A9FC07 + D395FED015B5BBEAD3A748E9F13F097DD4E74E4F6A652748ABAF07004E55D427 + 557212BF197182F7C949BE3E55567F030E6007BFB54A8FF771C25F7701D74745 + D0B771EB2EA6500EA0D7E1AFEF07EC7B023B0436181B81AF2B0F70D3725A9F87 + BA45CAF7F3BE29CC05CAE6A1AE2B85B9697D3ABE0CEEAAD2BC97CBFA689ABF48 + F10B953445C1E95B92B773F1731B2E5C006C84BB29FCA52D6DF73657364E5377 + F3965402BFCDEE1A6CA28EBE00DE9AFD03FC75F72F21A04BF0172570370260F2 + 5E9FCAFA4439F40502BB2BD81DC12DBAE90578B761D2BACF68E930A83A40E12E + C3A465B791D2AA7B50EB1E25B5EC590A03DDAA975EFCF71C1B4240E3FC45387F + 7B8F2F530A805BA85A45B84B9D03DF140EA700D8C23F0FACCEA06FEB4113A415 + FB076BEAE0D7F329F075B7AFC35ECADEEE20B8CDE04919ECF57D7FDDF1EBF529 + 00F6FD7D1DFC92BA684BC16FE7D1B3831B786CC9E5EB4AEB80BFE6FE1DA775C0 + DFEAF972FB8445AA855A06008785251C06380E48293B00F6C529527731F86217 + 69A7895A1F212FB017F89B2EA0A5F2307AE962DB3D09DC75B7460A7D293BF845 + D49585FBA33D8A45415F04ECB9F87EE9B374B5F45AB22AABEBBBEC21E9872BF7 + DE351908EEBDF4415B7C04FC0280F30E60647BFEC6C54A4FF36E16D4278637EC + BBF251E91D9D320E7791436007C288FA01ABD78570CF6B9E2CEDFD1B612F8E1D + 5F88A54C9867EA2C4F68E747D7DBDEBEC05F5207BFA90204DE60619F87027F9F + 586F212583E3372C2EBB0369C853847F5E2FC39ED920C39FDDA8E9F3067FD9B3 + D017B081BFA39E7FB9E47252A50BE0EEF675C0EBB0D721B0B7919A4358EBC6BC + B2355B9027AD7E6DA7A5EFBC3EB7C602FFAD52D83F7287C1089C6953F6ED3797 + 1AA14A8115000DA02CEEDCA2F11F97AE9D5861CF018C61DF4E340D387324847B + 05FCB2FF2FE0A6687C6DFAEAB98532E7E471997DE2A8698E6AD609C23E1F93D9 + A74FC86CA0AF6ACE99007B67683D616D679E3E2E33CE1C97E9A78F456456F9F1 + CFFFFB7F1BC8026C11AA78C9B52BE6FA9D7701872760EF9241DFD9C0DF18FED9 + 7405D8A8ED972EC863EFBD138F56FB63C19537E5D7FFF8E7B154F9F1FE0F7F28 + 8BAE877D74175CB9A2CF4B78E0CBB2E4CD6BB20827EA5B574C0BAF5FB490CCF7 + 6A1E57ECA2EB976CAFDA05572FE8F3FC241EADF2C343403B00A67CA38F9FFCDD + DFCAB6DFFC0D59F65609FEAE78E71D59FECE5BFABAAEEB71712E0397EB0E7B7D + FE777F4756E8DFE03E1DB3E2CDABB24CC7AD78FBBA398017BD79C94240038297 + A90CE8AA00C58850D1EEC805C8A6A22F8E617700D7E7F1F0E7DEB3BE0E9419EB + 90DDC1BB7F76B493A7CD9FFF8177DF8847AAFD61CE65E0F145F6313E572F00BC + F9D77FD5FECEC05FB4FC42CD9B2BDEBEB0DAE0AFCBF70CCE03E139844ED7FF75 + 77BD3BE42534B48743274F1B29A2DDDDC05E67D2FA1F9C9B5CF63A08FF0CF8E5 + 460DC41EDD40E0D409ECA03785BDE4692B720B3B08E6C6137E7B5E3FFE78D973 + A2E78F3C5B72009FA809802D4C3DF097F0CF442C2800C0DF3B3DD1EA53085CBD + 7BB741607E6727EDD91F216F00BC55DB774BB5E61121A281C42900BE78E8DE1A + CF316EC7AECC013C1EE08BDB372A8485DE26637004EBB906E1F4F5F30EA28C03 + D86F44220F1C263A85C361CE5DA3B58DF399CBEAE2398DF32029FB099BE3372A + BB69CA5CC09B0204D6F2505CBFF13CCB4D56C39ED9248370F73EA1E77942416B + BDEDBF0F1C268D4E62EA71070F5AAB8A377F79041052E4F300F27DD6AC93DEF1 + 8632944615A16C6058FB0C78981BCCB4CF83EBA4CFEA2774CC63D27B15E09779 + 0D10F811E9A5F31D445D4F9DF7F4D03990CF7FFA180C7E44FADDAF633465CEE4 + F3275CBF29FCB579146DD1014C0A0C46F990D0DD16ADB4391DF3BB22084C1DA2 + 2E9D4332B77465F3486DB77928699C736675005F55D759F74AB7D93A3F9DA175 + 3EB7D5B9EFDDDCE8E8A19F8970A3755D672E313137C605EC7365C02E73E71402 + 33A7EE18E1AFCFB5BD5F07AD6F3776BACEDBA71BF0F51B335DA12E006097435E + 570A7B5367307973FDC650D0B4B7479AC721ECA1A21DF8A21400BB0C0C6B5A04 + 753DEF7B07E701B043E276C9BEC0C10D5C82C07918EC10D81DC10102EB7562DF + 10EA3985C1CD51DFA87E31A50D4770EC53438061158EE0E63DA3539870D1005C + 006F84BF9E277507B043E03C00761770067F71FFC6D0CF067273303705BD0E6D + 81B90E7DBD2D2DA7FD53009C570A7F293BFCAD21FA2402E8DA5EBE51067563BF + 3CF84DE5EDF975115F1BF1F511206F7E6D255543C23FAB682B01E0EED2B843B7 + 32A89B570A79F300386D77689B87BFC8206E04C080DBB4CD612E7D0AC7C57697 + 03E3008D395E4EADEF2A2B13F619A5E0B6480E778BFAA56D697B25006C75719D + B43E4A9DC279E5D7568B54691DF6A3A8117B1BE3FE6DA5C74571AF637B8FBC36 + ED83FB9710D016063A8EF3D762AF2781C00D6E638D3AAC6D135A3A5BEF4E44BD + 29ADCFAF8D473924FD3894AEC717295DE34F55D4F7562884742EBD56DE3F00B8 + A97EC68D9B689F26EDF433E3F5F0BA027FB13A2D9BBC2E964B8CE6D395312963 + 5BAAB4FC59D5BF64009C67AA752985B937A34A00D8F2F917972AF6ABA87F2500 + B84865F057C53F5D43A06F036D8BFF847C490805ED8EE0A22F8DD71729FF25BC + 550A3F34357F3CFD472D5FEFCA7EF42AFC38178DC92B3FE656EAA33E6F7652D3 + F75A76C2B3FADAA16EC3267AB254D500C12EFA25793B1137D3BAE4249D57FE24 + 9E2A4C0CC2C4245F9FAA01D2E761DF8822D89B5719E4BD8DB1B5947310380F6A + EBAB2C14740EEA16C9FAC7BC43DF9AD2496B0280F34A01B001D90A00386DCFE7 + 53A09B2A85BDA809D03529D3C7002EE5086853609B02DE7C7B0A815D29E07579 + 7F1F5FD6A6CA5FA0E4E517357E976B2ADAF300384D1DFE1601E0B49FE549E947 + FFE8E0CDC35F14EA02E46D7E7790ED09746780BE0E7E5BDCA317A2710FE01204 + 0EB0D71DC0A479F0DB923D7E35A59F835F80AF83600FFD8C1B1808DCAAC728BD + 081E2E8DF4B91AE136EE364CA5C7E8AE17DCDD90F68F6E6164FB05EBF816961F + 2D2D7A8590CE847926DC73CBDE00E00402F7A9D6B62ABDC81F17FAF5D13E7D83 + B3D7C16FB3DE634C35DCBF035403ABA5A5A60E798BCA291076E04B3E9543E09A + 6EE010EED9F7FC05F8FABEBF00E0CC219C405E20B0CBA1B083DF0E31E4B343DE + DBABE665F2F0CFBE173000D8E12FEE5F0070E7EAF972470680A90F8B4BC05F07 + C0A6087D4959B06261CA5D0A9459E0A2AED3241D17C33CBBD337754BB87C7F35 + C2E8B9E382C5B614EE3AEC4DDB7CD18E366F6771AF08FE3A00266591B0DFF207 + 2DA52FF50E807DFF5FCA03EF7F5C263FB759A63CBF45FAACD0B6A561BFDF54E6 + FC25BF3C3A7BE3A2655E2C7C22163D1DF47AF84487BDBEC72FA98361165FD9E7 + D7C33D3BF0657136950360DBEBF7B1E0EC05EEB2B8EBAE5F2F7B6A7A5CCB4F10 + FA59C7ACD5BE0060154E22771D0182D97370E8D31B64D8D3CFCAF0F51B64C486 + 8D21BCF37A4D59A88EC017F83BFAF9572C65E13B5DF07639E4C5D93BEE95AD19 + 044659B8675F2C67F19CBAB8083F71DB6E99BA6BBFA5DF393EBEC602FFADD2AE + FDAB0DFE566DDF21E377EC3020119C69BB335001BCD878E4F9C2F11F97DE38B9 + D2E0EFACA3470DFE5A1A618E031B873A45E36BD3CFCF8D33F03BEB387B81EAF1 + 8E1F961927343D7954E69C392E73CF9E94F9E74F19D422C4EDCC53DAF7F47199 + 7DEE84CC3C7BA25E00F8B77FFE33DB3318A8650E48F6F7BD88AB1287258ECF0B + 067F71009BFBF7D27903C1F459A06D8B2E5F94C5AABFF8C7BA5DA56B3FFF2BF2 + 93BFFBFB58AAFCD8F02BBF220BAF5D957BDF7A53165EBFA2AFE9A22CB87AC9F6 + C105FA22CF034581BF06806D8FDC4B3AF6B26CFFCDAFC4A3557EE052757809C8 + 44F571C7163DBEF5677F264F7DE10B16027AF9DB6FABDED4E301B1D195D8ABF6 + C723EFBE25F75DBD6C0EE0FBDEBA665AAE5AA2EF15F72FF077C5FB6FCAF2F702 + CCC5D14B1F40B1ED01ACAF9F7A07BD88FC8A77DF0821A3B5BD3EA1AE7FFAF77F + 9B01E3FCE79307BDFE3CBC16DABD8E14385ED783D7BE54C55EC6E8A7F518B3E5 + D7BF68E01717307AF5D2961ADF9D17CFBE6CDF09A02FC75D12F754264F9D8784 + 0E7B03873DAFB98922847F3E6E403705C379390476606CF9D3A76AECFFFBFDB3 + 932DFC3310983DBB2D647B0C058DA39FD4DCC009E075017F6973084C3A5D7F63 + 70FFCED0DF137EDF80C06B8FBC52F69CE8FA8915FABB146E80B97EF2BE1AEDDC + 94C2D889FBF7593869A216E4FB3800462900462124F47EFDBD05F672E34DB8F9 + 86D47E83B59EDFE3EA9DC0621DBF7B9F5C2A02C0DB779AAA08151DC33D1BF48D + FB00A71AA7F5F4417EFE19A3E722770067A1A093BA2CA53D913B83B3F31AF097 + 1BA354E94D5300E0D40D4C3D5B2838FC3500AC6D9407ACD573F53A3D4F6B9E3A + CEC3A4DE07683CE8293DD73FB12183BEDC0C46CA1C60D83AEDA7224FBB470CF1 + F905738AF446B31200D63E0F910FF0B7CFEAC7A5CF1A15796410F861E9759FCE + 7D0C06EBDC68C543D247F37D99FF1814D67992B6F7D27918C03795CFA35C566F + E07795F45EBE46E7620F5A4A9D857F5E487496FB0C083B00E6A63EE675CCFF3C + F579A14597893709DA9C50454868570A831DFC02851D0C33BE4BDCAE04007CCF + F4B0AD0973D92E892338BBA95145A86840317363E6C9C821717A13A5D721F2D4 + F91EC08CE9A82921A07D2E9ECECB3D6F4EE0F17AED902885BF1EE2D90170C7B1 + 53B42D40E03C000EED518C8921A081BCE6F41DA9D73649D9E06F04BB25981B94 + D6018001BE0E771D043BF0C5F9DB66C8D8520868EA92BD80F320D8C1AF43E136 + 6880E609091D61B0C15C406F84C5EE1C6ED15FA5ED7958ECE5CC1D8CBB57D54C + F3CD00BD11EEA6F0D7DCC029008ED03705BE362ECA01B0D5AB5ABABA0ED66BE9 + 00841DE23AE0450059602DEDF44B61AFF74129E875D500BB51B4313EEDE370D8 + 00B10AC0EBCA83DE1400A76D9E4F6171C34E01E0B20F70BA5692AE8FD4B6B682 + 88C0D6A873E803000E00B79BB00F701EEEA6F2B6548C05203B006E1CC16DAD00 + 38C25BF2F97A52AFF7BCB7E565F0B7157BFDD604C0387EF379E06FD3765D2A82 + DD7C5D5DAA0F4C36309CAC87FA9A695EF9F6748CABB675D65BA1EC796C7FE3CE + FA7EF4F5E1048EEF8BF7DE4AFFF6ADF57FA939FB4627AF9D347DBD762C83BFAC + 57B30EDDA104790BD4A8396E62D68D4B7535D6C4E33A3961919D1114C9432717 + B521D6DE8B54D496AEF1E795EFFB712AFF5AFDFD18E7695A0A059D825DCF2342 + 40FB9EBFE55CA626AF49E5E38BDA4C1F939BD87814EF2501C0F551CABA3E51FD + 5B08E8FACB01708306CDAD6C076D14DACA806E2D4A8F97AA41A37FBD00D8F6FE + 05FE6AB991FEB3375635D27FBE26FA63D358656963FD31D0F436FD0120A55FD1 + 1705E5DBF25FC25BA5F00352F3C733FD81CBB7E5DB8B543426AFA271B74A1FF5 + 7903E855F1C38E3C6FF511E256D08D00E0EC4EAC665A973B49A74A2702456D7E + C2CFD7A7BA9500D8FA7F02003885BBD9985C7DB974529B00E0B49CAA12F47550 + 9B425B44DF06AD990C873119D04D8EE3632AC9FA23CA11D0A670D7E16F0A771D + E67A1F2FBBBC5F6D6ADA512F36346DA4FD53009C5EB8B8F21734F97E4500D895 + C25F6FCFCBFB39FC75101C52DCBE25105CD2800079F5C2D1F607E282910BC708 + 8E81C00060D23C0076B8EBE19E53E0EB00D8EB3C04B4EFFB4BDE01302990D7F3 + 40DFE63DF4A2BABBE6552D7A70711DC2687161DDB2975E8C47D985B6B6B7E8C5 + 85B95EC0F71DA317F101E0B2F76FB6073061A0FB4DD07A9CBE63B5AFF6E93D56 + 5AF5AD96D6FDAB0DF2527F5BAFD1068233C76F4CDBE0488EC01779DE812F7200 + EC7579F09BC25F07C069886820AFCBC1AF435F80B049CB387E5300EC40D841B0 + 857E56B51BA9E551DA2F3A7D1DF6A672204C3B00B87D84BFAEDBAB174AE7EA05 + D2B1AA1C00DF31716E49934AA1ED7C910A79883BAFBB3371FD92A6795F18F345 + 33F6FFBD7BD662DBF7370F7F5DE9629C43DF54B668475E535FF443405F0BFDA7 + 29F5EE00EEB168655C045CA1E5070DFA027F71FFF658B84A86AC5E276B8E9C93 + 878F5F9441ABD766A19E1DFEB2EFAF41E064C112D8DBE7BE4732C72F8B9D0E7E + 49FBAFAAE9F2CD60AF3E9FB97D598405F652BF26B87108F5DCEBFEC76D0F3E16 + 6259A4F505DABCD8D3370DF78852204C9A41E0C79F31003CECC9E0F2C5FD9B07 + C006819FD2F2531B82FB774300C02359C0DE50BE806D0038713EA50E285216C1 + 01BDA4E60066F13C2E92535F85DB37CADA01C42CB25356A50BF2F9C5FD5BA9E5 + 7B5F310851BD73878CDDAAAF63C7F608827719ACA8DAB9D340C5A9A30F148EFF + B8F4C6A9FB0CFCCE397E5C661F3D2AB38F1D935909C801DC007150D1F8BA64CE + DF93C74C330DFEBA80C02764DEB953329F7D4ECFE248C40D7C42E69C3F2533CF + 9FAC1700267CF3BCB3672C7CEDDCF3ECC77A3603C016E619D84B1D902C0260DA + 676B7981E601C08B2E5D905FFF71DD8EDB6D5FFD6DF9C9DFFE7D2C557ECCD5E3 + 2DB87AC5C2289BEB97BD7F018D6F910279B53D8AB23B8281C284645EFAF61BF2 + F0E73E8847ABFC70000CB87470FAE4173EA8F7FEB5F9C7FFADE3DEF8DEF76C0F + E07BAFABDEB8AEC7BE5E2F18FDB3BFFF3BB95FFBAE7CE39ADCFFF67579E0BDB7 + E4FE7771145F314733EF75F97B6FC8CA0FDF9615EF69BDBE561CC00689F5F301 + 14DFA7F5C8612F5AF1EE1B56E78076EB577F3D3E63E5C7877FF88332B8EB4037 + 85BB2E8E499D7F7EE9980FFFE007F188951FEC756C10983D8FAF5CA81F00FEF2 + 1765EE65FD7FE4FFFEE219D97BF9C51ADF9B972FBC6A8E62802FC775F84B5DC9 + 0D1C4030001727F03CFDFF9FA7DF9D00874BCE5E80AFBB815D0E7D5D3309077D + F2648DD7F1C6995519007611C27D6E04C029044E61AFC3E0E94770FB1251E090 + 960903CD3EC007357F288B3C80F2CF7BFDC47D564F88E8374EADACD13EF5D0EB + 32E9C03E99B06FAF41608070BECF07271667E1A14927A40058CB53F71FD0DFE0 + DD3279EF1ED3143D16E9845DDC88B3537F8F771A089EB2EF75AD3F20978F2CAB + F11C84801EAFAADEB6CB2249980B38867D4E5DC088368F3601EC7599CB5753CE + 4101FE96EA5305E81BCE750E814953088C463D5782BFAEF4A62A07BB0E77BD8C + FBD71DC094A9A7DD9CBFAA817ABEB6F37972DE676E60699C0F64730155E6102E + 9853B80BB8BF858C0E00B8DF836B0DFE0282C9935A795580C0A4FD1E78DCC0AF + 9535DF7FB5CE5F1E205C7400C479F09BDE344739EC03CC361BABA4F7B2D5D267 + F9833500B0B97F6328E86E805FE67980DC38E7CBC360C4BC0FD1EEC017A520D8 + E79CE97C933A87C15D230066DFDF140033972575186CF35D5CC18487D6B9B103 + 6007BDCC951DF0D2EE7D3C4F7BC718FE99FEFD173D2043963E24774F5960C0D7 + E7E50E82CD095CADD7052A07C045E19F3B8E05FAE2002644342EE1E00C06FE3A + 00CE8B7D8011E017185CB42FB041E161C1ED9BA60E791D0453EFD0D7EBAC6C7D + 1C008FB576DADA0DD5F621DA1641700A8433F09B80E1B603B53E01C0A67EA332 + 50ECD0B8E500F60E56C57E0E7CF300D8051C6ECE9EC1F1FA3405C0AD22006EDA + 75B001606F2B03BD11FEA68EE0AC2E425FAECF9BA10872A94BE5B0D6FAD60180 + 1DEEBA1CEEBAD2B65A01307DA3D803D8F701B67D83B5DDDB1CFCD606800D02B3 + 8ED2A167B636E2EB23AC8BA0B4BE487E1CF206800DF07633356ADFDDC0AE435F + 77F7E6C1AFF7A90180A9AB007F91015D4D01B879D0EB759ECFABA82DEF004EC1 + 6F5A871C0093023101982E87B9452EDE4A624C1E02A77974A300B836555A63FD + 28AAEB353468A1CFD7EA0E7D5FDAA7B5D669DEDFDB1D3D87CADC7B1F92656B9E + 96C1A3A74953FD2C58EFCDC626EBC2F6BAE3BA7300C1618DDBC16E0A7A51BD01 + B0A679B8EBEBE95EAE0480BDCEFBA7F23E6999345DE32F92F7BF15F2F7D1509F + 27730237D1FADB4AEFC3416FCA5E5019FC05B2E6C0ADF7AB4BE91893C1D0F263 + DD8C8C4725C74A39556D2A83B29FA43E4100EC0CB07EFA0C026054E6004EA06E + 8346CD55CDEA54C30A0A2FB2E883F817A646ADEC0F5D24DA1BEB17A3A97EC181 + BE9E36D12F40E3866D0D0A37D31F860084B5CE531765FAC7FAECCB93FB12DE5A + 55FEA14CEB2AF5A9A4B47F25158DBB15CA3F677A0229FA312D978E4FA563B313 + 9D29825C87B979A5EDB529F6B793B09F980B4ED65E5F5B9B4F1CF2F5A9B2FA82 + 10D079985B545F067DD37CE2FE6D68D2C95B02756F468D5BEA64AE404D5AE9A4 + 51D548F3068A5B7629AB2F56B7A0D6DDA4696B9D0C936FA3A9CA01700A6F1DE0 + BA0CC27A3EB6037F1D009701DD383605BD4572801B04BC05EEEAC544AECDC16E + 51D9E170DA9E87BDA8694E56D7512F3A6CEF191D5B010053EF17335EB6BB5DE3 + 4551238ED189F672B8EBC0370F808BE47D1CDE3AFC659FDFA23D808313582F0C + 71FA7281C80524178EF162CE8F61BA53CB09006ED13540DF14FE5A5866002E4E + 5F15E0B71560973AED03F47539F4F5FD7F81BF1E0E1A576FCB5EA3F482598FDF + 438FD39B3BA9758C4ECCB9D06ED1878B6EEEBCD60B6BAD43B677137773730737 + FB3FB1EF53FF71516355E3B5BE5AC78DB7BD807103B32F70ABBE135513F4625F + D51F87707003A7AEDF4A901795015EFA0CA8D9DFC16F5BC23C0F65AFDFA99A4E + 917643264B7BF6FED57C80C0C0E0E0FC4DA1AF436044DE207184BEA4A9FB9732 + 6018A00BF84DF7FA75D1E672106C7D1903F49DC042D27C03C01DC7690A201E1F + 16921CFEDE09F49D5402C00E7F6DBFB3C9F32DBC5DFFC50FC88025AB0D02B380 + 454A3D21F01CF6DA82585C08B33A953926662D91BBD8736D56D8F3D7A1AF2FB6 + A5B2B6FC621CA097453C16F458BCA33D8A32D0D74303A2EE8B564A375C230B09 + 1BB83A825FDCBF84865E2D5DB5BEDFCA4764C9CE43A6BEF73D52068081BFA84F + 04BD2875AD504FA8C37E11FE9AF357EBFAAD0AA19E3D0434A037D580D5847BC6 + 71F394F4D57C9F079E94DEEC95F7000E605C3800614233B230BB5E863CF69C0C + 7A648395FB3FF8B43980873EFE9C0C5BBB5186AE7D3E5BF8258F6CC1F731157B + FE3EBEDEE02FCE5F0030619F53F81BF4AC857D0E22F4F3F3AAE72C1DF57C58A4 + 36B76F04C023C9B3B8AD72F85B72FD06772F0BE1B6884EAAF51E4E73CC4B0100 + 1BE8DDBC3DC05E4D0D046BDB581CC32A16DB27BDB8B1C6E2FEAD127BFA06F0E0 + C061A781E089BB010E3B0C540081D1974ECC293CC6C725E0CA4CC02F6E3DD5AC + 6301E0E0F8254C2B1007C0838AC6D7A5A74F6D9179A74FC87CC0EE491CBE847F + 3E6EA19E71309A32107C46E69E3F25B35533CE9F9019678F476456F971E25BDF + 940517CEABCEC9C218DE79D6F9007CC9E3027685FD6C2F853D80550BB56EC995 + 4BB2E8D225F9F00775874FDEFAD5AFD5CB01CCF3E2E205E62E790B27300EDF2B + B2ECDDEB26606F80BFC109EC5A749D31D70C00AF78F7AD78B4CA0F0F010D1C5D + 115DB240CC177FFD570DE6DEECE3BBBFF8853CF8C17BB2E29D005F777CEDABB1 + A5F2E3DFFD7FFE93AC7EEB4D59F5E67559F5D61BB2FABDB703007EE38A85B95E + A2EF1F006C0E607DADCBD80758DF33A1A2EFD3D7FFC0076FC9AA0FDF9195DA1E + 606F785FBC2707C2BCB71D5FFB8DF88C951F1FFCF0FB0677E9EF63F99CBC8E7C + 0A7EC9F35CE4E9433D0224D7F558CAFEC5FAB7265D7AED4ABD00F0E62FFF9ACC + BBC20D09A72D14F5BB971EAAF1BD59A8FF1F849676E7AF835F0F01ED69C9094C + 9868F6BDC6F58BCB17301C1CBE4060A02F7B63FBFED8D4B9FBD7403179FD9EE6 + 5FC71B67EE9719C743F867DCBF190C261F7F1700BF290436108C74CCB4430764 + CAA1FDFA7B12A0EFB4C3072C35387CF8B0451CA80C808FC88CC347E5FAC99A00 + D81CC0070F582868003069BECF9563CBB230D3A90B1811761F017BF92D06FE4E + D1DF62F2FC16531F1CC0BBCC15CCCD3A4500B83A8689AEDAB653C66FDD25135C + 006155BA2FF078A0703CDF18D0D5F3505AF69B9346BDA46DAFD00E187E5546AA + C857BDF29A8AC8165A7EE11519BD8973A39E2F4955233771BE04F812FE99ED13 + 5E94D1AA317ABE1C437B84BF290C7638EC20D8F30E8069A78C3378A09EBB7109 + 0F2004F43A3D97AFD3F33F738027E2CD5F8986302750D9D611110297228CACB7 + B90629F312606FAD5AF384F47FE07119B066ADA9DF2A9DEBE8FC67C0034FC8A0 + 8798F330077AD46E8E63AE04E8F59BE86C5E15E7520E80BB12EA79C94AE9B1F4 + 01ADD739DA72E660010A037DBB6B1BEE5FD2AE0B75EEA702FEF6000093F7391F + 753AEF23F5B9A1DF18E8F34D0420662EC9BCD2B62289F3CB0C00CF0D0E6040AF + C35E9FDB2287BF5E8F6CAE1B812ECAE6CF317FC7940884752ECD3C1BA72FE017 + C73037563A04664E3D46E780550F3D23F74C5D683769324777F72F73765304C0 + 38813D1474C7E80246B7E30A06FC0281C76A1A4170160A7A6C00BEED71030378 + 35A54C186887BE29004EDDBF28DDFBB7C388A076067B71FB027409FB5C6DF5A4 + 88FAF6D403830D00D3775C80BF0E87232C6E9B38821D02E765903781BAA4805E + 9CBFA970000386114EE014F4BAF275E46F03ECBAEB3782DE56BDB55DF365E19F + 73A017F09B2AD4EB75AFCAAEC723C84DC340A75017593BF037CAC7A4ED2885BB + AE14FA7A1FAF4BCB29004E01AF43DF1400A765FAE6E5EB1E652098F5900297AF + AFA5A4CAD65052E100461C43D5A4438F12B08D10B736E501B02BED5309001729 + 85BA29E40D6037D4356C57522394EB9387BF0DDA26D27AD448EBF3C016510E75 + 774A08737C8781DBB48FCB806E74BFE66172DACFCB769C04B2D6265F33B5720B + 54BE969AB657AAF7B64A7DF2F231E9D8540D62B8E726FA3986CF2794D1E0B1D3 + 65EFD14B72F1CD5F9625F7AF95569D7B96AD037B3EAF86CD0AC06E4E856DE9BA + 785236101A15D6DA83D2FADA948E49C7A580D8D3748DBF48E9713E6EF9EBB2AD + 40F5B91C085B08E816B707386C7DFDB5D454319BD1E315F42D52D938606D0644 + 43B4D9B2F64A629C835E3F663C96B3293F565A2E52F6FC37ADFC3128277544DD + 2D6B8FFA3707F08DA9320076085CBB1A36AC296BFBD710023A7131376C1A9CBF + A9A86FA45F100BF5ACFF7C26F6FFD52F4063FD6705EA02854981C1D4D726FBE2 + F897F11355F861CAFF50A6F5A9BCADAE7E75C9C77F124A9FCF4F1AF5959DD05C + FCD0A3ACAE1CE07E54A500B892B2494085369F34E4EB5365F5B7700F60003060 + 360F7453176FBEAD488D2AA8316E5F8E03004616CA39EFFA4D15C33CB7EE2E4D + DB04914F0170A3086D1DEE3AC42D5304ACDECF0130E514EC1601E06C6C419D03 + 5D83BFEDB54E27EAA676113CC77EE4510A80CBC7D71490D7DDBE2908CEF6FE4D + 00B05D80E42E62BCCE2E5E545CEC64173EA8037D68D70BA40200ECF2BA0CF626 + 6A16C1AF3B77CB006E0DF04B5867BD2805FEB2BF2F178B2ACBDFC5F8700C87C8 + 4D7200D8E1AF0160950360802F7584853600ACF50680A3EB97D0CF0E80CDEDAB + F59E377557F50CEEDFE63D86EB0534EE5E554FBD50EEA57DB5DC4CD5BC8F5E70 + 6BB9498FA1A6667AA1DD9C8BF5BEA34D2DFA472770BFB1AA71417D832318D72F + D0B775BF89AA0901044700DC7AC004693390FC7853D8F737825FE02E219F631D + 803783BB28025FAF470E8071F8761836453A0E07E846D7EF90C9010A533FA226 + EC4DDDC09D47CEB4FD7FC9B7D53A5CC0E6F81D1E5CBF006087C000E0CE6343C8 + 6703BC0E7C81BF1100771E4B7D0803EDF51D71044F5820774C5C6829A1A03B8E + D3638C9F2FB757EBF16258E7BB26CF97BB00C013E8AFF593E6660B587746A742 + CFB9CB65D87D8FCAD0158F48B759F75ABB2D88CD581CDC0F0900B645B158B6C5 + 31344BFB018055A90338EFFA75A706F914FE66E1FB58CCA32D2EE42116F9080D + D8FBDE076C01903A1609C39E720F484FE0EFE2353A7EA58EBF5F7A2D6521F261 + D58332F0812764C0AAC7B4EF03B618195C2A0F594868A0B08776EE134130A997 + 01C07DEF0F657700F7BEFF51DB1B0FF76F70F9E20276AD55699D81DEA74D805F + DCBFC05D1661D1A0479E95C18F6E30018091B5C5F0CF439F782E025F9C3EC0DF + 8D32E2A9174CC1F9F3AC0C59F7AC857D1EC6DEBFEEF27D528FF34408016DF097 + 10924FEA319F7A46863CFDAC0C5B0FF865B159FB3EBD41463EF74270FC6E7C25 + 5B9C66CF5FF6ED75E88B46008737BE24635E7C55C6BDCC8238F03784CB1CF552 + 704F8DD43C40B80AD88BE2E2BAB9ACB41D91070A53BF765BCD7D286F95BE73B2 + 5A261984C05D16529CBF0682776C37879A8747FDC5D96185C7F8B874F6D4C319 + B401FC027D7D0F4F9787722D1A5F975E3AF5A2E0FE9D0BF03DADE9B993067B0D + FCC614200CB45A78F19CB92267689FE9678FD50B006FFED21775EC595978E9BC + 813860E36C3DD6ECF30100037D17E3C08D290206FB7EC08BAE68FDE52BF2E10F + FF201EB1F263E7D7BE297FF20FB587582604B33D177BC3B27FEE9B40E02B59F8 + E3FB3E78CB2030C097B0CFD4932E7EF38A2CBC866B58CB6F5D97656FD7BD07ED + E7FFE80F0C5E026957BEFF560681D1839F7B57FEF8BFFE5DEC79E38FBFFCBFFE + 873CF6850FE5FE0FDE96DD5FFFCD585BF9F1057D2D0FBC79DDF6FF2504F42A9C + BBFA1EEEC521ABEF9FF7BEECBDEBFA5EAF05B0CB7BBF7E5996EBFB5DFDC13BB2 + 465FEF03AAFBDE7B4B9612165ADB791FBCA7008403A4DDF5DB75C3E80FFEE0FB + 16E2D901B0835DEA388E43608EE7E017D1463F577D1CC0CBDEB8268B2EE3FEBD + A4EFE76ABD00F0D6AFFE5A7082E38C56BD7D714D8DEFCD96AB5B656174FFA610 + 98EF8903DE007E430875E480D8DBE69C25A51C04F805000383BD2E085730694D + 07F0F5D3F7190006FAA6A1A03D1CF41C9487BFDC48A262FFEF59C70FCBF4A387 + B41E57F061997A68BFA533ED4613FD5D213D72A4E6F39E5C29D30EEAD843470C + 06E7DB01C08492E6B72AC0E09A0EE06BC757D83EEA0066520B19CD4D37FCB6EE + 3F602098C80B13F57777E2DEDD32710FE19E81BE00E0B0573021FA1120F8F2E1 + A5359E83FD81AB2200AE660FE16DC05F3D0EF9EDBB643CFB036FD573CC6BDB65 + EC16F6000EFBFB027F7103038B39FF8C7E31847FB6F393A6068055A35F7E55CF + 657AEED374DCAB6C73F09A9ED336CB583DDF8D8EC077E4C617F5BCF8B29E0F39 + 4F72CE04FE22CEA72FC8D84D2FCB38CE8F7ADE74A89BDE64E56E60F2DEC7FB79 + 3B007810FB04AFD3F339E7F5270919BD4106100E1A00AC1A92DE14A6C21D0C0C + C6053CE0E1A7A5FF434F5958E8810FB1BD841E4F0510CE9CBED1F98B1BD8853B + 78C023388535BF9A390E914E74EEA3F31EA0F040FAB36F30AEE038674A2170CF + 65E537D251D763E96AE9A6F337D47D6998B3754794A37A12D1C5DCC2A5BAEE8B + EED7B96269EB0FE6813D17AFCC6E0474206CF341C06E329F74D8EB00D8E7A35E + 6F752AC240779FABFD13109CCE755371E3E3DD53171AC4F5BD800DF04ED3BE94 + B50D00CC9C9A39B66FB1421FE6D80E84FB2D5A250316AFB69B3101C065E03701 + C0387F01BFEDC7E975C5B82996027871FA1A00D614F8DBC1F6F89D626D0E80DB + 695F806FBB18E2D9F601D67A07C0E6108E10D80130FBFFE2FE458478EE043456 + B5030CD306C41DCE3EC17A4D35B44AAF69B4AC79CAEDB4DE202F6017F0AB29B0 + 17086C63E3311C02B7216C7484BFED86E87895435F07C0E6EE05EA46680B0C6E + 65D7895C3772FD582CEFEF72F86BE266644030E1A0630A0076080C00269FC25F + EA1DF6B604184739FC4D9DC1CDBA9660AFBB7B5387AF81DB085D2D8A57543AC6 + CB25B01BC7E5646B001C57F3405A3F2EA981DC445EE74A816F2A3F4EBE2E5DFB + B0F0CF387681BBF570FAA2FC3A0B79D65FB29BF06D9DA6A734EDD0C3643097D4 + F31DBBD780BBF55125009CAF2F82BFD437CCD5376CAB7915F926B14FD9584070 + 02601BB4BE531AB4D1145959EBDB941CBF29B02D95EF54DD6E32705BA33D1EBF + E59DD2B875314CCECB61A9AF95A6F235544F5DB60F704B1D1FCB69BBAFAB7E5C + F2E3D67AFC96DACFDF8BED071CD675BBEB6FC6332FED92AD7B4EC894392BA479 + FB6EE251251BB660DD37E4CB15E02F6BDB35206FBAFEAD2A5A234FE5C7A86D9D + DDA169918AFAA3DAFAE4D9822BDFEF56287B4DFA7C0E7FBD2D5F2EBD26D29B97 + 3B875D653C2742DB4C695B6D8AF0B7461D5C2BE153F69CBC0F15EC2BDDEA345F + B631E96BA9A44A40F746F499DEFFF75F04000E076E486AD2175D49F6865A69DF + 56D2288A7CA9ADE843F82C887FB49B907E111ABAF812F0C5D06365FFE4F60F8C + C2972503BB7C19BC4EF3655F2CCFDF886E765CAD2AFED1CC976F46E971F3AAAD + BFE7F3ED1FAB9AF0A35D590D9A6AAAFDC2892F946D9F04DF2C1F1540DC1B56DE + 09CCC9DA55E3445D82B956CEF5A1DE270D697D4535D3E31480DCDA0070511BB2 + F63A00B0D527659402E12235A8249B5C868966C3363AE96CAD135483B83A6165 + F269692ADA4A2AF54FEA4881AD40D65876795DA5F632D01BFB94D5559043DA32 + E8CBDD9CF1AECAC6ED3896B6713CEDD7C4C545421C9B9783DE50C6E5DBC7D4A4 + BD5E6C685D435523E0AFAA815D787001C24509172341387A1BE918606FC34EDA + 47D5582F78D2BB5FB3BB60B988517101641741D42720986351F6D48070067755 + 493983BA11029307D2DE76D760BDD0E3623080DB006A6903E8868BCA66777361 + 39D8D4CC52EE262675F88BB3572F4C1DE6AA9A6ABBEFEF6B4EDF58DF9CB2CAA1 + AFA7D64F657528969BE316EEA917D9BDF4225D535CC02D7AEA85750FBD985691 + 060184F5429C36157B0013D2B9599FA89827A4B385828E6AD92FECEB4B58E736 + 032648DBA836310474067C5540604B0DFA56DB38DF17B80486B53D81BDEEF8F5 + 30CFA4D439E0ED00E41D817B778AB41DA6EDAA36F4D372A89F2EED00BB9A7600 + 006BBEF560FA4DB3B0CE9D47CDB23A87BF067D47CD94F6805F4D81B89657B90B + D8DCBDAA4E5573A533619D09F16CF9F956A6ADC3583D76EAFC55E1046E6FFB01 + CF97DB272E92CE380A260617028EDFDB0931C7FE63AACE93E7CA9DC0DF690BE4 + 0E5C0B53E7DBE257AFF9F7A956D8E217ED2C84B91CF6A2CC213173B1C916C866 + 2FB1C5369743DF20F2DA47D565EE92007CE3A21DF95E847406E4C6C53CE0AFBB + 3B904360DA59F0C3F9EB7BC6D9C2E19207A5E79287A4DBC207A4EB8255D27BE9 + C3D267F9A3B618D96DF16AB967BE8E210CE1521624599804F4027E71B604F55D + 09E07D4C06AE5E6BEABB12B72F009830D00F4B7FCDFBC2277BDFE18A1960C017 + 27B08E674155DBFBAD2194224E1AF6FBC56DB34E063EFC940C7AF4994C831F5B + 2F4380B8AA41B878A3C80F7A4CFB3CFE8C0C7CEC69D300CD0F7AE25919FAE446 + 19FAD42673000177873FF3BC0C7F7AA30C53017F3DC5FD3B0008BCEE5919FCE4 + 064DC9038071FF6E149C4AECFB8BCB08C7D2984DAF96016016A401BDA5FD7CB7 + C8E8175E91E12C706F7CC1DC50EE8A020457D97E8B5B6CDF44C0318E60730747 + E8CBE2FA6816E155E4DD05FCCADEC76A2CEEDF2A5D3E7CAFB9CF08413A85D0A4 + E636DB29EC4139993D2D23FC9DFFFACEC2F11FA71E3CB9CB000EF017A032F980 + 3EF7E143E6EE9B0F3C3A7DCA400FC0A7687C5D3A7FE6619975ECB0CC3A71D89C + BE0B8055E7711C9E90D951847FCEDC8CE7F5F9CE9D9019678EA9EA06C01FFEF0 + 7BB2E06270440290175E611FE010EA79E155DC9CC0BDEB967A99947D8109096D + 6EDDCB97E5737FF887F188951F7BBEF1BBF217FFF4BF62A9F28370CF16F619E0 + ACCF77EFDBD765C5FB6FCB7DAAE5EFBE698077A9D62D7FEF2DAB27A5CEFA5ADB + 1BDAAF6E07F0E77FFC87B2E4ADCBDA578FFF1E63424A8865203370F98D1F7C47 + FEEFFFE7E6DCC0BFF39FFFCC006A7DA0EB6FFCEC8F65259056DFFB8AB7AFCA03 + EFBF29F7EBD8656F5C91FBF4BDDEAFAF69B9A6CB0813ADEF6DE53BFA9EAF5D95 + E56FBE29ABDFFF50567FEE0359F5B9F765C5BBEFD8DF6BE5FBEFC8FD1FA077E5 + 3EFDCC96BDC37B7A4376D6E3B5E08C76008C80BC005FA0AE9781BDBC3707C429 + 185E7855FF2754F57100DFABEF6F01FBF2129EF94AFD42406FFDEA97E55EFE3F + F4FF70BEFE0FEEBEB0A9C6F766CBE5AD3144F90573172FB97A59FFCF09094D38 + E88BB29030E35A76572F37402087BB01FE06E76F0A7C4BED213474DE099C7F1D + BF716E817E57F5FBC96FC119ED7BEA64A639FADB3027E6F98D2034742660B0FE + 8678F8F7F97A8C99C78E986B9774D609EA4FC8BC93272D9C74FE79894C00B4B5 + 10D0050E605CBD530FBE2ED3895270E47021447EEBCC2AEB07F425E537D57F5B + 27E9EFAC398277EF56ED94F1BB7698AA63DE7E8BCD215C82C0970E2DA9F11C13 + 76EC9289EC13BC638FA67B65E22E3DA6A6E3B7EF92B15B77C898D7B607691E10 + CC5EC1E3366FB71B95C6E0E8050CB367B0E609191D6E488AAE603DA7A1919CEF + 70F06A0A4026A28547BB1885135853CE8D3884CD098C2B78237BFDEAB955CFB1 + 237103DBBEF9710B054DB9B12AB4AB9E09A1A01D003BF825050403807DCF60DB + 0758CFE3FDF59CDE4FE702FDF5FC3E68DD069D076CB0733D513F107304CADCFC + 3540E710FD1E7D5AFA12EAF9119D7B30A7500DD4B9C78047D943789DF47D48E7 + 2B0FAA70F4027D63586887C08061E634B881FBDAB617443DD139D2AAC7A4BFCE + 6F7AEB5C89F955EF1540E0476C4EC5DCAAE7B29047D433CFEA73DF6356B6F9D7 + BDCCD5D6E81CEE0153779D9FA11E5ACF8D7B883EDDB54F0FDAEDA63E5CBF2EF6 + 07A62E447F21650B90AEF3C24D857931C74C6F3A7410ECEAA6EA4E28E8A834FC + 33626ECB9C9739F01D16067A41D80F38D6131DA7CBCC30170610134A9A3EE60A + 8E209814F09B96998F7333A6035FDBF777DC0CCB0720ACD704557ACD306EBAA5 + EC1B4C5D272D7718A3D71EA371F9EA358A96DB8F29398471FEB61D3D49DAA83C + ECB3EF09ECFB009BEB373A845308EC3217F0B0897ACD3259AF4DA6481BCDB71E + 325EAF5F183B555A0F9F282D87EAB5544C5B685BFB5153F4FA65BAD6E935D560 + 424007D8EBFBFF02801D02B71E324EAFB3020076F84B1EF88BA3370BF13C708C + 5EBB8D96D6FDB4DC7F4CD8135865AE5FC0705E7DA372E0B719E19EA39AF71C16 + 6F505669EAE017017509016D70378A726B5CC111F63A0876E501B0893C7580DF + AE7A3DDE0505C00B04F69BB753E0DBBC0B8EE0017AADAE7548F32507B0A72501 + 75F310D7CB75D5A5E238A94B98750C87C0A45EE7D0B741C79E4111001321CD5D + BE76B37C02781BB4EF91C9E1AFF771F86BA24D95DEA8EFEB39E1467E52CAACE1 + 740B6B452A80B0877D76017DBD1D358E0A79D693A21C12C7B52773F51AE0650D + AA7C8CC1E036A84BB64647DE22ECC5B626ACB7B5F676D6F8A258F3D334556320 + 66540A691DF00651BEB3467DD61738AB6A1201B03B8091C35E03BE0575350470 + D514972D6AC4B1E3B86C6C6C236FF571CD352F5FABCDD66B555EAEAD6F7DD4A0 + B9A6ACB76AEAC7E538AD6FEF25BD065549DFA113A4E33D03B2CF268CE33574D2 + 9475E35401FAD6067EBD9E35F1DA606CC34AF589D2F175F52D9273825445FD3E + 69F9FBA9F4FA4AE5220E535221D84D545B9B43DB4C457DEAAB780C635C5AF6D7 + E521ADAD9CF4CFDA79BF990223CB2B7B7DB0B3BCBCCDA02EE582BAB4CDF209E7 + 7316F9692A7D3D26EA6E4411F456520A723FAACCF1EB4F5CF8E2736A5452C386 + 2ACB3336B67F26C53F4BFC2732F14F584F35D4FE48F30D1BEB978FBAF845B37F + 6603C251C91722ADE38B51D6F69911AFAB58F91FB854F5F9F12E3AA6AB3EFD8B + FA7C6465803701AF15E420D8CA65D036D6A775372B7F3E2F37CBC9616D6E0211 + 2601F56F2BD4A70080B3F658E793C842E924D2F713A95DE530D6807059B9A6DC + B9EBE19B1DEA3ABCADAFCA206FAC4B8F530900A7C096D0CF0DB97BD226DA405F + A4136E4D9BB467BCF6638C8A0B82DBF442E0B67851901E07F98542930E7AF1D1 + 9E3EDAB7535F53D30E7AC1A1ED0DB45F43EDD3406530585387BEAE006E557A91 + D2A8B3BE66933EC7ED7AECE462C8C5DDB05E6FBA5D2F9AA21AE9B11AE9314D9A + A70ED8EBED21CC73C8A70E6052DBC317300BA8BDCB816E1065DCBCCD81BA00DA + 087CC95BBDAA048CA9A76F09E0A226DAE600D8EB909709EB9CC2DF92FB77A834 + EDA6C732B72F17CE615F5F572BF6F4EDA917E03D493D1FD4B2D7E8A831A6E69A + 6FA6FD01C1ECE1DBBCF758BD68D78BFB7E55995A38C455B5D23C70170184F3F0 + B7EDA08996F7B1B4DBB8D8D7D53AD907D85CC2D1F10BFC75288CE3B7D3C8E9D2 + 5E53606F1BC02F001810AC6A3742EB23006E3374AAB64577AFAAD56020B18E1F + 3D5B3A8D9A95C15F002FE19D11F0D7423D8F9D6369BB513A16384C1D7B018F9D + 2D1DC6CD31B06B619DC7CFD5FC3C83BDE6F25599EB5705F875190006144F5828 + 9D01BE1381C0E5B205A829F36C41CB00F0D4F96161CB212F0B5940DF1925F0CB + 42180B63F7CC442C8A113A2FEC9FD6352E94B19F1AB03785C0EEBCE83A0F5706 + D2BE739748B77961518E05395CBF0E8009F9E7A1FD7078500FF04DEB80BFD97E + 714B564A4FC62D7948BA2F7E50DB56ABD668DDC3A65ECB1E91DE2A16177B2C65 + 5192908508572FE077ADEA09CDA3008207AE7ED20070BF558FC8C0358FC9A007 + 9F9001AB1F93016B1EB7F0872C80E28C19F4E03AD38035B87BB53F0BA9B678EA + 6241352CAC0E7AEC69190CCC8D8BAC036D5156EBE2FEBDB8791179171078D0E3 + 4FC9C0279EB1455D5BE4550D78BCE4F075E88B863FB34946AC7FC1EA01BF2C20 + B3701C00B08E7FEA39190A347E36849A1CFE6C84BD9B5E350181D1D84D9B65EC + 0B617FDFF1AF6E93EACDDB34BFD99C4EA336BD64F9EACDC061FA6C96AACD805D + 9CC084CDD4BC8AC574202F6E2B5C572CB88F7A798B09103C4ECB570ED57477DD + 2A6D7EFD097394115674DA81D765CABEFD96074E38A8004E6C38F46CE1F88F4B + 078E3D56E6A473A72FE15E01BE001F04E899AD2A3A465D7AF3EC2A9979E2B0EA + 4816EA19E1FA65CF5F0030F0963ADB03D821F0D913B63F6A5D8F5FFFE98F64E1 + 25C0968EC35D0C60BE1CDCBD69E867445DEA02A68E50CD0BAE5C91EFFCE22FE3 + 112B3F767CED1BF267FFD7FF37962A3F809FE6FE55E1E8055EDEF7FE3BB2E2BD + B7E5DEB7DE9045D703E805F22E7FF76D59A15AAAF5B42D7BE72DEBBFFE4BBF12 + 8F56F9F1859FFC912C79FBB22C7DE7AA2C7B579F47B5FC3DC0F21B99B31868FA + D0E7DF35987B338F9D5FFBAABCFC955F8BA5CA8F9FFDD7BF9795EF5C97156F5F + 93551FBC290FEA73AEFEF06D598913F8AD6BB252EB0909BDEABD77F5F35191BE + FBBEA61F6AFA394D3F90FBDF7F5F5F77F88C567EF06E99F8EC96EAE7529F70D4 + BCD765FAFE81BB2BF4F9491DF09222406FA82B17A1A717117EF9FA25F9F21FD7 + BD2F34C70036CFE7A683CB67EB05805FFBF29764C965F60EBE66AEE16D5777D4 + F8DEBC7BE12183BCEC510D285F7CE5B2C15BEA96B0BFF4D5CB26DADDD91BA030 + 219E030846B4791E39304EDDC0BE0F3010F807E7A794BD8E1F6AD9F6D78EBF03 + C87F17D2BA99274FC88C78A388BB832D4CB48A7D82179C3D6337994CD1DF1AFF + 9D997B52BFAFA74EC9CBA75F2E7B4E74E8F813827317B0FBE6E9FB6BB49BAB57 + 7F2B81C4F461FFF27C1F00B0FF96D28F31FCBE924701088776DF1BB87ACF6E99 + A0F210D1FC2E0381D90FB808004FD9BD47A6EDD17E40DF1DDA6FD71ED3B81DBB + 64CCD6007F01C1E3700213267A9BF6D9BAD3CE3B2309F5FCEA5619A77D0CFE6A + 7DB58A3A03C27A5E62BF609CC116DD22DEC8345ACF69DCE0341C90BB11E0FBB2 + DD20853C52866F8DC0CD5284891E0EDCDD1460F1C84D2F6BF94519B641CFBBCF + A93405040389FDBCCB39D821700A806D3F603D77130ABABFCE05486DEF609D0B + 78B40F9B1B38FCE5E6B198F6D739C6A0C7759EA1659B4B3CA6731BD5E0B53A17 + 79749DF47BF80995CE551ED536C07004BFEE0A26A58E0826BD746ED403D0BB52 + E73F0FEA3C66F53A03BB61FEF4B8F45E11E655D4917751A6DDEA75EEC57CACD7 + BD3A27D3395B8F250F67B2F9DA523D86AAA7B53F2CDD753ED77D11515FD0AA4C + 0E8449BB72739F95EF979E8BD04A9B47925A4499087F9143DFD28D89415D91CE + 655308EC20D801B0CF957D4E6C73E4282FDFAEF36A64374F46C7AFCFBBB350D1 + 1102037471007BE86700B0BB81534770C7AAB08D8B6DE1A2FD3B699FF663A74B + DB317A5D3196317A4D312EB885710DE3126E3B56AF51549DB57CE7043D4E7405 + 3B1076018571089BE3777400C3E47102B71AA2D745C32649DB9193A5F5B089D2 + 663840799AB41D35455A0E9B20ED34DF43AF03BACF5822AD464C3208DC4EDB50 + DB11DC04CBB5144EDF2A03C108F88B3207B0B63BFC75D7AF3B7F7D8FDFD60828 + DC6F74D80F1807707FBDA62C03BF238372E0B7ACDC8B9B8F874B8B1ED1C10BE8 + 5539F07565E5987A3983BB2A07BF08F07B5B57BD364F0130F559DB2069DA45AF + ED710677098EE0BCE33740DF01A616DDF4FA5F459E7607BEC0E054561F21AE03 + 5EA07211EC4DDBBC2E750797E98E72F8EB4A1DC02EBBF93D81BFA980BC0E7F33 + 976F84C388B2AB61476DD73EB69613E5EB3A41BEB6C3BA51F7006623C0050067 + 30B70EE51DC47E9CB006D5CD20B0E7B33AD4B6AB34C9A2EA95D4A47500BF8832 + EB7735FAB04D5B52A68FC156045C8D02AAA6A017B9B317398CB57E1170BA9AE4 + FA18B08D4AC7D5251F937F4EEA1C0E5B1F1CCA9A1A7C56A5AF250F787DDD35AD + 77795BBA869B5768477A3CD65A5973B52DF8425B637D1D4D590F6DD659EB3B85 + 54C7F96B71005CA432F88B9A76A8A9B8DECFBA3FCED63C03F07C5D5CE0A32ACF + 096E192BA845F9F79E7F1DE96BBB5139482DE63575A8718E4915F5A9A71CD45A + 4A39BEAE86CDF43D222F47E59DC959BDBEA7BC33D88F5D9B1A9681DD02D05BA4 + 8A00F61396BF8E5BA522907BB30AEEDFE485A76FA448F91793AAA8FF67422900 + AEFB9FAF48E5FFB8A4B97FE4FC17AEE04BC817222DE765C7ABF0C5F52F5451DB + CDABF4C39397FF987D9CF2E3169D243E91E777E05A0F356C92946B83B61F93B2 + 10D09CB85D71629057A509C20D2907800BA16EACAFD4C765ED290006EE2600D8 + 267B31EFE5B4CECBA91A9ABB37A818FCBA4A303785BA45A03755517D0A716B93 + 035957D13807C04DE2243E550A82491B70C7657B9D747720244F08CBD3B87D18 + 9BCAEF0AF53B43F3C70D00582F3C1C00B7EF2DB7750C0098B489B665803782D9 + 72E85BCA87D0CD5CF4E8B16ED7E7329503604B63DECB7661E5EEDE087ACD01AC + 229FD67BEA4A0170A6087B6B3A804BB0D7CB79009C82E0BC8BD7212F40977D7C + 492917F543B4B3EF6FE6E6C5C91BE16F8B5E2A4D099BD5BACF2803C025C83B36 + A67AC1DE53EB7A6ADABBA4007E350FF48D90D705E045EC03DC52DBBD0F103815 + 619D01BF6908E812E88DB03851D6165DC0A4C81DC0D4A58EE07640E104FC66F0 + D7F2530DEC027801BEBEB72F65F200601CBF99FB37825F97C1DE18DE997C067F + 356D3746152130B0D7536030020403800D0853A72930B83361A0C72FD0328B4F + 5A972C4091F7B0CFA6E9010097857706F602786795F63E63012C00E0A5D275D6 + 32CD2F358532CE870880E3E29A3B7FCD5D610B6F847D5E2ADD17007BA3B4CE17 + E770F75A883F52EA541EDA0F004CB83FCAEC0987EBB7ABED19A775F7AE52DD2F + 3DC82F06FCE2245913858BE491CC099C2E46A6F9FEAB70FA86C5C9D01616346D + CF5F03C08FCAE087D61A0406FEA2016B347D04A0CB3E7B4F9ADC39D3FFE1A858 + EEA7E3584405F8D2CF422F52A77D06C68559F6EF2594B38BBD7C512807878F3B + 7D4859D445941DFEA6009814304C3A5CCB43738BC9A40060771FF9E2F3B817B7 + 98C6028381C22FB207E256A9DEBCD51CBF84BB74F76FF5ABC101CC7E891E3A73 + CC2BDAA67D01BF2CA0E3AC9AB86D8FEDCDC8023B8BEF0E8071015F7CBDE6E2FE + ADD28203ECF91BF69704FEE20406FE3A98200F8C600FCBA2F11F45FF787EA07C + FFEC24D97AFCE90CFC02668032809AF9A74F19B801023B08C6E107EC293A5E5D + 0200FBBEBF73CF9EB07D46DDFD8BE317E7AFBB827102B30F30219DE75C386D50 + ADAEC79FFFE33FC8C2CB802D60174E604DAF9C3307A7857AC6157C95F0BF0102 + 2FBECEFEAE5765090E4C2B137AF9AAFC733DF6CC5DF2C63BF2BDBFFAEB58AAFC + 78FD9B5F13C23EDBFEBF84187E87BD7F81B2D7AD4CB867647B03AB96597D4871 + EFB247EEF96FFF8778B4CA0F007039F8D5F1EFBF61CFE5E1A509BD4C1DDAF0A5 + 5F96DFBD4110FCC780DDF7EB7623E332661F5F04FC7DF80BEF5BFA808E5DF9F6 + 7559F9D67559FDFEBBF2D0E73E276B3EFC501E78EF3D79F0739F97277EF54BB2 + EED77E431EFAC22FCBF277DED1D7CDFB7FDB802F201C480E3C27059A3FF8E1FB + F1192B3FFEFB3FFF2F59CA67A29F27E1B13DD4F30AFE06FA79BB0B78317B13BF + 7149F39481C340E1A0E5FAD911CEBBAE87BB89F97F0304FFF4EFEB0EBBBDF5CB + BF26F75EB9242BDED4D7F8D63579F0FAD91ADF9B1F5E986A4EDF258810D038D5 + F57F9CFF73F24B2C34F4255978E9620DB80B0876184CBDBB7F81BE0063C03121 + AB11FDDC098CBE7A6E518DD7B2E2ECD11A0098D45DC094C9131EDA9CC0AA6C9F + 60FDFDC0E94B1F9CC1387681BF59D8684DCF9EAAB907F2BAA3AF98FBD700F0A9 + 9A0098BD8369F7F0CEE4F37D08010DE4A50FA20FBFAD7EA30D6980C0EC0F8C23 + 789F4CDAA78AE017E10406008710D0F7D6788E29DCC4B3577FAF71FCBEB6CDC0 + 6FD5CEDD52A5E9B8ED01FA56695BA6FF3F7BEF1965C795DDF71219207220891C + 1A391039E79C736EE446260092600488CC9C891C1940104406080260184D1EC9 + 924723C9D268663CB6645B922D8F666C3F7B2DDBEF83DF97F3F66F9FB3EB9E5B + 5DB7BB01021C52EFF55AFF7562D5BD0DF4BD75EAFCEABFF7F69D495E6005BD00 + E0677768DB72066B840AE9E741A5818C91B73E5CC714000791E640A35C883CEC + 7D3A81BCF6C0141AC0F9A4CFD46BCB138900C6940061C249C7D760EA5C9701C0 + 405FAED776CDA64D540F00B0868796B58089B500C0B7232057D614DA27D2BA3E + 488680C16B55DD57136144D62DCB656DA290D8CF31009C0E0FDD61F11A05C045 + 0BA5AF78ADEBB64CE62F957588F4775E246B1D5947D91A8AF554027CC35A2B59 + 73017F654D56345BDAB3655C25E7953286BF068079982F71084F5F246B41AF18 + 06037E29E92F9A8116AA583BB2C6CC72FDB23E6D16E0AFD665CDDA02E9C38DC1 + D91BAD7F6317B0B5E3F5B3C16122EB341E3641EB406005BD2117707AFDAD2038 + C05F03C12683BF2603C088C83E1E0C8F9552D6F7011EFB9CC1848C96FB92015E + 4D86905F18D7F02805BEEA000E52F8DB07C03B5465A19F0D00D7EB09F41DAA40 + B7415F8EF14EE35A0F0F51351B31C5F591FFF73EB25E064057C5951B2030F301 + C0B5BB007A07BB063DE47EABBB07C10680D5190C1C0EF017E06B8E606B27397E + 3BCABD65070F8171041B00B671CDFF2BF79FE408B67CC179009812901B49C12E + 8AC06F5AB1C3973271FD8A3444B408F05B2D006085BE617E4E3206FCB55264AE + DFC401DCCAC35FAF4E49BD468BCE09FC35606C52101C60AF01DE18F2DA7E05F5 + 5263291964D6D70A73F2F63532A4E7674E0AFCB27762AE6003BC59D0D7E65357 + 40CC5E0E7B434169C300F27DA214CC05D45A3D7603A765734CF17131F4B5FECA + A2FB68130ABA6E00BA755B3A1F6D4FDAF55ABA6AF55A25001855933A7D2AFAD9 + 23635F2C1C4304BFFBEA344B00B009086CE035560C684D317035D998415C9477 + 4CC6B9D3B2E3E363AC4EBF4160045CCD7A1F2886BCB64F9B6EC7CA9A9B1B67DF + B78948FA6CEF55F764D97BA5BF49323F86C0761EBFEF2B7DA846A37C552F43BA + 6FEEC51EBAC1CFAC3DFCF478D69C427BFC15DDF7AFE8BCAF4BF67ECA145C27AB + 3F43779FF1DC9912B615F75797F706FC9552E16E70035791BF21648CCA542EFC + 8D1DBF511BF85BA9AA5736CFCBD0378941C64CF45E280BE4DEA9F2007045DE7C + 3C273EC6EADF487D75009C48ADEAD44398E7F41FB77D58320070B9B2D7C818B3 + 0F55D6D89D2BF7C593967DC115FA222F4FF145C08E4F9F3B56FCDA85E67C5525 + F0B6022A08805156DF579402602EDCB1D2D03628B728C8296B5E992A07009BCA + 1A8F8FAF480EE034EC4DF755B93FF7E4E05706C07564D19AEE0B4AFAA947FD15 + 9181DD04EE86BA2ED603082E35A70CB198AF841ACAFC461EF4026FBD72F01719 + FCD5FE0802F3D4A88A1B8AE000C6595CA53EF37D08684A00B0C15D85AE11F44D + AB6A13B9C1911B9818FEDACD507C53654FCBE6C6E4B8007901B869E09B6E1BFC + A51E03604A55047781B9E4E735A86BB0D7CFE16690F9DC5412EED9CB207016FC + A504EA028029AD1F99DB57A1AFD55B79F8ABD0D7407068537A07302E5F207069 + F8AB6AD7575DBFD55100C080E034FC45068015D8C6C037D54616FAD95CBF4928 + E80087EB771BE6D5959CBF1E1627803712619FE957201C1CC108009C83BE1108 + EE3952012FB0B74EF7110A8115F412D2D91CBF61DCDCBD3104A66E219F11F546 + 0326B806FDC77B491DF01BC35F03BF067F1BE206A67FC814F7D0F0E9EEC161D3 + 5CE3C18C4DD1B0CFE63640D42D1F19E503C3A56FA4B4C74C732DC7CE50696E33 + 146D72351D3D45EAB88081BEB3A58E33C2036005BF40601C13934A87DF4B20EF + A499520702CF72ADD0D439EAD2D070CE5212F2990D3940B082DF196CE879F7AF + 2A405F73FEB69949CEB8452A5FC7F58BA364890A088CA3449DBFD28F3305B109 + 6975DBA88CFBD8B40400775E44EEBB551A02BA23B9EFA4AEB9EF8214E88A3CE8 + F5259BA7EAF00D0098CD533651918E2D5FEB3AD21F1C37E6F48D01B0C1DF1802 + B3B99B6CEA4A9DD287807E2C81BD067C01C008204C9F8574365988490B39698E + 2336A015003FEEDDBF7DA34D6D20304E5F802F1058EBA23E5BE59C4F3CA5E0B7 + 3F40F869F22CCA18F3D90467439D4D7436DC693FEB5D57B6097FE4A5E9A536F7 + EF85FEFE604F37FAA5974B39CC86ECDBA7300298019C004418A83030AC40E40D + 7269FA5CBDB170F20E7BF105D1F30A76CD65C75C206F7A3EB97D199F7C60BF9B + 04D8312013802FF09750D0A3641E4067E6DB2F67FE3EE5E9FD838B22E72FF97E + DF7263DF79C38DDDFFA6C25FF29A0280D1B4A3381C0FB9C952028051457E4A3E + FED04D23042F797D39DF89C36E06397881BB277DD867A4EEDF5327DDFCB31FA9 + A61C3FE6A69D3CE15EFADE77C3990AFFFCF67FFE4F37FBF45977EE2FFE32F414 + FEF9DEBFFDB523C7EFF4F78F2988352994FC0840EB81AF87C42714D4A23967DF + D31CC1CCF9E97FFCFB70B6C23F177FFE1732FFFD3CF8CBF19A6F575E8B72FEB9 + D33A87FA02199B27E70604FF6905CE6F3F2B2E7EECFEEEFFFA6FA155F8E7E81F + FF445DBF4B2F7EA40098FAC2B31FB88567DED77CC0F34F7FE0969CFBD82DBD70 + CECDFFF043697FE836DCFCDC3DFB477FECD67FFE59027C175DF8585DBFD6A60E + 14F661B3DFAF5048EB47E57704CC1AFC35006C509812E88BEB17D88BCC054C7D + E3B58BE14C65FF582E617FBE531502C03B09017D4AFE3644C0DD19278FB87F38 + D2BBD46767CDC95775CE74CD052C7FD332775AF8BC4C3D7650FFE609033DE930 + 0E7800AF7C8EE4B343BF1FF300D8E02EA017E06BF03776109B5E3FB4BED4FB78 + FDD00605B86832F9820F1EC883C1161EDAFA10DF21C05D0B073D4EDA3884F93E + 52200C3C0EDF39BF3C3820EFF5784805F00BE42547709603987EC60D04670160 + 1EA0E1FB93B9F6BD3A64EF5E75FCF25DABB0377CCFD236E90339E1BB9912088C + 4EBC563A4AC430E91FB607F7EF6ED7FFB99D6EC0AEDDDE091C448EE021848516 + D1EE07D4DDB6D30DD9B147AF3F405EA0AF893100300F27F19012A0D7AE613100 + A6AEFD01F492EB1E088C0B98D2EA8C9B68038881BEE614A6DD6BCB13EA06E6E1 + AC9E22AEC35C83ED3ACD35DA1ED4B23A20D847F4F0D77FD60226DAAC0BD4F1AB + EB039F2642D70C0102FBF506A587BF38801100D8D6229D58A384750B10D84070 + A725EB5CFB45B2762996E396AE775D445DA5AF23293016FA282A31003657306D + 5B5B217300B767ADA5F5155A02857DD4165BC311D10515FB36EBB8198B5DDB19 + A409F1B0B71DED08066B7868C2428735A7815ECAF4BA340F048F9F1160B094AC + 7B81BE11F8459AEA44D6B9311036E9C39348D6D5B87F81C0941A5947D6DCA634 + 00A66D6E5F00B041E0D8FD9B03BD39084C697D067F3574F4E031EE81A1327F10 + EEE0E12AC242A306C1FDDB58EA4D068ECE73045B786805C4C105ECC3407B11F2 + B9310E635CC7BD876BC8E7FB7B0E75CD474E75DD672D755DA62D92FB9631AE66 + 37B9FF7A788842E37ABDE49C3D89A824F7615D077B972F703838802D0FB0F645 + C0D71CC0E6FE5577AFCABB7F09FFAC60B8B307BFCC490404EE2073CDF11B0160 + ED23EC7308F96C52285C00FED20FECF52E5EB9076FDD23D71740AF0160533600 + 061E7B290416F9D0D0DE09EC9573000380CD018C0CFEE6E67A70ACF7FD01D67E + 1500CC83EC69006CC7DAF1A6784C1F7E0F103796016083C0805F2B63086C6D54 + 11006CED6A8DDBB9EA4DDA25A19F63608BE8632C1D1ADA14035F9B1FF7C5E7D2 + 3ECA1006DAA0AFE5FCD53CC044AD0B63A45DA31DCBA733630F2DDED76B260AF0 + 17B00A6005B606109B86B2997D01B6E2C035486BA0360D80D3C79B2C74B4B5E3 + E3A9DB98F5F35E0917CD3E2A703586BE883E53BC3F9BD51FABACB14C005C53DE + 93EEB7B2A7EB61AFBD7E25DDE3F57BC8F4FB7DDFC65ED503DC8D01B0EE8F07D9 + B829F4B387CE5EBE398069A795DEF34F2BBDBF6FF32ABAEF9F35AFD06BDD6D65 + FD5EF67EEE9EB238CDD7A3982F25A0361A0700037F751CC05BBDA1AB2A7F4FD5 + E4EFB22C009CE66409E72A209F6E15DD0100FE2628EBFDC5CA3AE676940572EF + 54B70D8063C5C7C4E7F8462A86C0B7AFFC3FDEF0C7CC8702C81BCDC9FBC0DCAE + EC35B2C6EE89E22F9E7CC55F727722FBB28CBF34CB3A77FCDA85E67C15F98B58 + 00BA1550B900B8D0D8DD90B981EFA52AE000CEAAAB6A485F4DE98BCEA1F3EE10 + 005B3F0058CB0080B3816F5AA501EF9D0A709BD59756DE023DEAAB2A0BF358C9 + 9C48F1625E17F42272BF9057466F08E8AFEFFB2D1C10CA3B2FB2B186B9A74939 + 8F9E03305C5FEAF5C29C009435F7AFDE6C7093C28D89DCC0A4C0AF8D996BD783 + 5F246329001C2B197BC03B7963981B83DFB462104C69C7592E6003BD5E1EE602 + 81CDED9B93B97F4D7E2E32E80BC435C86B4EDF18F2C600983AFDA664BC154F3E + E7E0AF0160E473FF027FBD0BB856DB14FC15D514D510017F0D00E3EECD02C079 + 9017181CB978F3E07000BE9416FAD9E69502C0067D0BC05FC23E3790BA425F29 + 35D7AFB97F2B008071FA1AFC353770A2007C6399FBD700B0E5F4D590CF91DBD7 + C498C1DF0743EEDF86CC654CFA0C0093FFF78121E41BCBDF80B2CD274A424037 + 19364137AAD8D8D250CEB81F02FCD5CD2E11F0F7A15193B5BFD5F8D9EA008E01 + B087BF72DC440F8075E32DDA60F31B6E388167B9D653657C2AF097FA5C05BC45 + 33D9C85BA8C7B009A7619F717010D6791A61FDE6B9D694B87C0300A66C377BB1 + 2B9A53AC254EE0A2D94BC386A2DF586C0BF4456C2C6A9E3A0F797D0E6042417B + D8AB1B94618C391D1694B84E8B56BB2EC56B7593B303B980C9115CBCC6755EB2 + 4E9D301DA4DE7129A0779DAAF3121FDAB9EB8AF5AE3BB97C5792E37783B61512 + 2FF721A0D98435B70DD017D9C66BD26633372583BEF1462F25A0B7F7235B83D3 + F731D777F393AEFF96A75C9F4D4FB85EDA9703BD6C2AB3C16CD03706C20680FB + 3DFAB4EB0700CED8D02627B039800704C72FF017F57D52FA9F7EC60D7C76BB1B + 805BF8D99D6E5008FB0CE8A524CC26391B09DB89EB0A97D5D1AF0900FFE09D31 + 6ECCCBAFA8F3175908E821014C188C30C75A1EFC7DFD3505BB635ECF87BAC05C + 04041EF2FC3E05C0E6EAB532062E369F7EFA62318FBCBF387F47E20E06DE48FF + D68C30AD15D1070717BB4907F72B9842B87E2DF72FC0CA83DB43EA6E5401B844 + 938E79085C919FEFFD9B7FEDA69F38E2A68980BF53714CBE7B3271FDCE78EF94 + C2DE89470EBB19EFBEEB567DF2895BFBD92D37FBF469997BD2FDFC3F970FEDBE + FB6FFEAD9B75FA8CDBF1C597A1A7EC9F928B67DDB4F772009830CD405A60ECFC + 731F68E9C3347B000CF405D202709FFCEC7A384BD93F3880175D3CA3E733F8BB + E0FC870A7D391FF545173FD23673165D90F6B90F5DB1F42DB974D6BDFEA3EF57 + 08A6EEFDEE97EEB35FFF2AB40AFF008971FD0280D5FD2BEF67D1D9D3AAB984BC + 7EFF5DB7F0A30FDD421CBD1FC8EFFAC1876EDDA79FB9677FF2476EDDE737F43D + CFFF58DEDFA5F36EC1B98FD4F96B21A0E77D2CE75037F3FB1582E33FFD87BF53 + 283BF78CFCBB06F06B20983A2E6003BF73C99D2CA20E14A6ACA8531A00CCF916 + C8FB032C571400CF7AD7C3DF99A259A78EB94F8E2F2CF5D9B97A6CBE02603E13 + FC7D934F99920727F8EC18B405025B0E60CB114C19BB80919FEBC13032A770EE + 3C87DDF2C32F967A1F848536D03B553E430681D194C3729CC8FA0C141B103647 + 30D2D0D022BE4F78E884EF9AF5EF6C2FF57AB70E4CD1EF29FD3E7AEBAD4C000C + 2036483CF6D5D7F4FB323DE7E49B73F4BB73C21B6FE8F848F9AEE5FB953EDAC0 + 5F83C1C8BE6B47CBF7F3484070F87E36189C0580076BFE76B447EA7BBD76ED4D + 1CBF8377EE490030FDE402E69AC3F507D86B3058AF435237086CD72BFFF0D273 + 6E6000C0FD00B901085327AF3D398115084B1B186CA0D7AE9B805EBB8E1232BA + 5770FFDA1C4A1CC03D363EEA1EDE90FFA016D7671CC0F6A01632088C0BD83B83 + 7D5E60D601F6F097AD0D0C00DBBA41DB91586F100E3A0B00F3609A3EA4B62417 + 069AB54D9765EB5DD1C2D5EA00D63510CE5F590FB156C2050C08662D455E60D6 + 503E8A4A0E02B3B642D43B04B50FEB2E9FF3D7CB1EDEB307F9DACC94BA8807FC + DACC5C2C7DE40CF6C01700AC3982A7CABA50F300CBDA73B2AC3DA504EC6A1A12 + 5D9BCA3A54FAE2483331203648AC6D60B0AC692979A89175B13DE448486873FD + 36C50D2C4A5CC294A3A729F44DC240CBBAD942411BF0D5B5373098872F2987E4 + 5CBF2683C2A61806334E6900D8E02F21A171FB361D3151217003F2020F18195C + C1213CF48051729F9103C0569A3398BAAADF4885C0F57BC97D12397F71FE86BC + BFA8AEF4533694FE8708492DAF0FF025EC737DC245F790FBB1EE83E51E488E7F + 1808EC5DBEB930D0F9E1A0EB530F20D8C02F32E72FA21D03E204FA06D58AE61A + 044ECB7200D7020CB7E3DE351F06C7E057416F048071F802762D2FB039804D31 + 00060AA705F835089C80E0963E1CB4858456081C81E00402A7C06F1A00C78A41 + 2F7B12066D63C50058A1AF9541F1B1E9E3F5D8B0E741086803C0405C03C0C8E0 + 6FAC18FEA2B83F01C0A278AF277ED8DF442EE01800C7E03686BA69E09BEECB84 + BDA9B68A79F5D913F37B730A7F09152D6515E957302C624FAE523D3F9E887922 + 3B16F9BDBBA6A504008E21701ACE9A0CEA2205AF515F1AFCEA9C705CD6B962C5 + C7C4C7D9989D5B552B076DD3CA815B83AF651B77A8671D1BAB32A1A7A95B0868 + F6621508B39FDB24F33C7AFE30562E00B6FEB89DEC9DFB7DF4F49E7E5A85FACB + 5379FBFEF179EF051FB81DD9EF6FEFF9EE2A8BD37C3D32709BF4A50D8CD5FC1C + EA0A785300389E5B5EFEDFAC31948CDD2E0046692EF9FB92BD8F7BA52C907BA7 + F200584EAACAF8656E47FC87DDEE7FDAD7A6BB098045E143A0A5F5873FFE7BA2 + 3B7114972BFBD229ADAC2FBEDB917D499AACBFD0B9EFF6EBA795BBB845A03543 + 95AA05551400678D7F05E1044EC241DF05D9E2C1943FCE6222888544ACB82F3D + 0EFC957355B6316DE7F2FC2AD095760C784D69F01B8B630C0EE741DEDA848AE1 + 69C1FCFEDC829205A807B169C53037AD783C7D9CA92A0BEE34C4A54CCD33D8CB + 7C0DD12C657C4C5A36BF528336EE3E59F8DF270B794A05BFA2F866209EAF3702 + 805C6E20AC44DC58D84D84A86A7D99574FFA1BB4D35CBF9AFB57E7CB9C90F337 + 06C006840DFE56923E0D111DC06F2CBBF9B1BAB50DE226EEDD20A0AE8D99D2E0 + 97323E1E00EC5DBDDEFDEB816F2ECFAF8584A6AD9057E16FCE214C890CFC2283 + BB067F0DF4DAB841DE749F01E06A2DE598D6321641DF5A6DB971E6263AE7FE25 + FCB3E5004E423F0727F0FD52AF25A5E6FB8DC23A9727CD011C407029181C402F + 4A03601D3337AF2982BFE6F08DFB923183BC01FCD60DA21DCB00AF857DA66E50 + 1847B0857FCE0AFBACD0B77F80BF2203BC805D802F25CE5E4488674A1BB731FA + CD01ECE1EF542901C1A2E13E04341B4D6C40E150A07C904D28DC0908B81B3919 + 9A8E0E1B5AE3A67B67C4786953D74DB159AED9583453CB163880C7870D343469 + B66B8EA28DB5E613640EB97F71FF4E9BED5A4F0F0A00B8FDAC6285BD805F8E07 + F8D236004C4868C23E5BC8E7560104B79DBDD8B59FB344C4C6E022D76EE63257 + 3407A0BBC26F30CE2F49361C75D3316C441AFC8DC7D8B46C3B57FAE4583632D9 + DCEC84D365F946D771E906DDE8EC22F5AEA20E4BD6AADA2B045EA76E182030A0 + 17E8DBA3E49144C06083BE0679AD4CDCBD6CC6B2F9CAE62C1BB6D1A62D4E1F44 + 3E5F73F452B2B18B14FC8A1EDEF8B86E10B379DC6FCB53AE379BC99B7D6E5FC0 + AE6D2027F0F7B1A75CFF007709F5DC47E62800967A7F5CC0C1B9641BD87DB7FA + 50CFFD9FF2F97E3DE8258CB31CFF2430D803E041CFEE7043B7EF56F86B1BED6C + B2FBCDF65D5A0EDFFDBCCFDDF8EC4E77FC95D2E13DEF850EBCB9C48D7FED7575 + 990117C6012F5EF6F928BDBCEBD75C6D0625B4FDC6EB0A6E47BF06F8257C3322 + A4AA77FB0285E3B0CE63DF78356FCEC47700BC3E2FA7398463F89BB4DF793B81 + 36E6E6BB706076E6EF539E4E1F2A76530E13B6D63B188159406064600B017F71 + 09130A7A2A1037E455ADE8CF7337AFBBE9720CB95B8172534E1C73938062278F + 290C9E72FCB81B7FE8B0CC39A50078C3975FBA05E7CEB9937FF227E10C65FFBC + F5C31FBAE9EFBDABAA48B86840A0B97B71FC9AF31750BBF0C2872A8030609839 + 067F81B5FFA1026E5B7E00C04BAE7CEC169C078E7AE0BBF0C2192D5FFBF1F7B5 + 5E7CF9632D175E3CE396CADCA5D25E76E5BC5B7EF5825B74FE8C7BED47DF0B67 + 2BFC73F9AFFFCA3D75F3466895FD73F5973F774B2E7EE416CB7B2896DFA51820 + FDD1076EF60727DDDC0FDF97BAFCDE52522F3E7FC1ADB8F2895B7EF98A5B74E1 + A3E477F8FCDFFCDA3DFDD92D85C0DE0D0CC03EADBF27FF8EEFFD69C5FECFDEFC + C90F14CC22C258036A0DFEC600D8641078D797B7C219CAFF01FA726ECAB967DE + AB1000DE01007E5F5E1F182D9AF5FE09B7E654E91CB668F5A9D7E46FF7909B76 + F2889BF9DE09FD6C4C24D4F9319F0F9810D0E6E645334FF9FCC0E6F2C5E11B3B + 7E918161C60C00279058F4E3C3E34BBD8F270E3CA98077DA513936405F03BF26 + 83C2B1CC110CF40500135160B4947CCF00817F766878E9D77AE7091D231CFDC4 + 37DFCC0C013D5A1F8891EF381E6609DF7BE939EFBE354F434E8F92B994235E7A + D18D7C59BE5B5FF3C098EF55BE6B4DF421BE6B698F08DFD13CA08303F8780600 + B6FCC0006020EFD03DCF6B09FC2504F44073030385A52424B4CEDBB9D70D08E1 + A0A9736DB23CC0E60E465CB70690079848159617987EE9EB2BD747C240931318 + 5730309808197A2D8DA02FD75873FD92E7D7EBC95290987E83BF767DB6EB750C + 810D0A0381AD54102C620D60A91F80C0BA3E907E5D43B0BE606DC17A8331D616 + BA0621E4B3AC5BC203692600300FAC017E79802DD1F20DB22E92B50F6B237503 + 0388652E2098B5120FC72D2C71ED17ACD2B2E302591F05004C786873085BDBFA + 9287F26691FFD7AB2D2038805F85BE228DF0226A2DF556D317CA5AD2877D6E3D + 85FCBFA40CC9A50E41805E5D8706B1B64C00B0CC43E604669D6ACE605BB7EAFC + 8908282CE3212FB0857A7E688CACB3D168690728AC63360E24A65FEA310056E8 + 8BF377E804F7100F650E93B5390F6A46EE5F4A6B5BDE5F83C131208EC1B007C2 + 63652EC77A904CD954D6FABC0EF98601B54DFA8F768DFB017A3DFCF5A23E46CB + 46FD47C9FD89A82F21A2C913CCB8DCBF481B772F0E60CD032CA577FB0E4B8070 + 3D806F2F0F8091B67B70CF35C835EC31D83550E0EB41701C0EBA9ED41B74274C + B4D4BBC9FD5B17EFF235B89B76FAC66388F19A1D7A7B75947BD100814DC05FDC + C0C05F5CC0715D61B02806C1067E81BC09C0A5B4BE0CC53098F91C1FAB66EB1C + 0036F85B13F8DBA2AB5773EEE5B9BFE7C1EF6C00CC1880D6C3DFCE52F7A03786 + B394353907E311BC4DC35C83BFD6D639D66F73034C8EA132FDB6FFE1F73D3AB8 + 6A01E2C68AE16E5C4FF66C44D68F68B3B763C057F79E8292BE200070DADD6BF0 + 360BE096D54F199F234BCC03F8267B6E01F2565177AF9C3B40E0AC3D3A2D657E + 726CE82B047F4D40D74200384FE4DF95D2A06D16FCB5D254682CEE4FF7D979AD + D4B1DAEC777AE05A1EC0CD52BC4F5BE8F85C5F2839A65A138751887A5EF4C8D4 + B9727BBDA8B197ED859705804DF4A3AA003FBF979F9503D894D5A7FD554BF7DD + AE0AF18142AF79AF64BF7BFABDA4DF575A36BF6C65719AAF47E50260513C6E00 + 1851D77EE354C6C9D28AC23EC74C2D57F7E51D016094054CFFB9290BE4DEA9F4 + 841581BFE9379135E79B2EFDC3F37F70B7ABF88F55153E1CD6AF63E10352EA83 + F40D53FE7BA35E5A595F60774B59E7BF77AFEFCFE52F68E5C3DA4AD51A7BC500 + 3805794B8D65CCA98812D81B2BEF829D2F2EEC5AB78BBD5C884D95E44BD88708 + 91FE9A5E7ED1905B44D842C2CE95259B6773B394355EF97E5908C5D03782C109 + E00D7037EE8B95078063E85B00007B79F7AF01D25880D918EE1AECCDEA8BEB76 + 6C7C9EAC76252BA3F3D91CC4B8CEE198025238DB10082C731B7AF07B5F23EF0A + B69B0183C008E7AFDE28841B894ACC6DEC1DC4898B989B03CD035C94077FAB00 + 7F1BB757000C088EC1AFCFF9EB212CAADCB8833ED99A77A3A3B0D74A91DCF854 + 09AACC4D8C1C6780D75CC026EB37D9DCEA28350EF8AD2937839471EE5F9CBD06 + 7D0D04C7B0D7E06F9E02D035F06B8A0170027D6305F06BF057CB82CEDFB4FAC8 + 386372D35E2437EA91EA140D70B503D4B53286BC9932582C75DCC0067CB36030 + FD71BB7617190F397EC9ED9B86BF31F045B880553D738017E7AF2F47A81AF41E + 95531F0F77CDF56B8A0130A58E67847D7E20C05F0BFD0CD405026B58E7A004F6 + 8A0C02037C0DFA7A083CD5BB7E8752E2049EA1320730AE03E06F73E02EE19FC3 + E6940F77979FEF4C25FD96C7B7F9840082D91403008F9BE51E1A33C3351DCBC6 + 593EECD54DB468934D37D8D0E4590A805B4D9DED21B0A8D5143FA7CDB405EAF8 + 35B7AFB982C9E1461FCE8E1621E4739B598B140253C7F96B00B8DDEC25EAFCED + 3077956B3FD73619D768694E94F6F37CDD361EB5BEC06F44B261D96EDE0A05C1 + 1D17CABCC56B5CD1E2B5AEEDA2B5AEDD22DC2EEB5C4775F98A966F709D4445B8 + 5E8AD7EB66A88674C6E51BF202037ECD096CCE5F73DF5007FA027F69F7405207 + ECDAE66D028043499FB97C14F84A1DF5DE18C23D4B9DF0906C14FBCDE3C77463 + 79D093DB35ACB3397D150A874D6A9C4BB6594D39E009DF06FEFA90CF5286397D + B67AA72FAE5F03BFC8D711FDCFBA81DB70FAEE5407B06DAC0FDDB147B457EBB6 + D13EE4B93D3AE7C4AB7706386F578FBDB6C58D7DE55575998D054CBCF24A027F + BDA3ED350510F4E78189D780B95EA35E7D4935E6B59715F29A80C3847036B7EF + B837710ABFEC46BCFC62080BFDBA42E03CD81BEA006185C21C1FA00DC2C937E3 + AD97342C6BD6EF539E4E1F5EEC261FF2A19E7D2ED3C31AC25673FF06C7AF86AB + A5EFD03B6EEA519977EAA8E653C5CD5BD11FA0EC96AB17F5380BFD3C11A87CEC + B042DB99EFBFAFB97EA79E3CE1E69D052C5E74077EF247E1E8B27F38F7820F4F + BB19EFBFA7FAC1DFFE4D1829FBE7FB7FFB6F13B04B090006F6023381B2719FCD + FBE1BFABD8B9F9B9F28BBF52003C3F388ACDF17BE44FFCEFF5A37FFFB76EF1A5 + B3DA87965E3E97688948C7CE9F2D17385FFEF9CFDDBC3367DCEFFEE7FF0C3D65 + FF1CFBE9BF700BE5775C245A7CFE8C0252E0EAC27367DC4279BD791F7DE8169C + 3DEB965EBAE4169FBBA0FF3773CFBCEF565E3EEFFEF43FE5722C5FF9C5CF75BE + 07E7FEF7E3DF68E9858FC28CB27F70373F7DEB530F81C9451C402DE542E9E33D + C5619F17C86B3CF3F9A7157245DB0FE7B690D2732B0880777FE7B30418E348E6 + 8105FE6EFFECE8C8529F9F3F3F364AC7A69E3AA20F37906B980723784062A6B4 + D5F97EFC889B792A1FFE4E3BEEA12F7DB3DF3BA5E5AC774F4A7952C12FFAF2D8 + 4C77E5E80237EFC89B491EE029470EBB678E3C5BEA7DFCFDE15E6EEEA1371400 + E3024E20F0A1431EF61E88DDBF39006C70988748CC058C00C2AF1D585BEA757E + 7578A0BA7E01C093F6CB7793CC7DEF9DF9A5E68D95EF320B6F4FE403949EF3FE + FE856ECC1B3E3202911286BF24DFB3612EDFAB7CDF26B017C76F549A3B78F8F3 + 2F88A4BE371B00E30ED610FEBBBCF3D72030757503530708CB1C05C2B882A53D + 1887F0B61D52CA3529B8812D1CB4C1607B708990D0BD42CE7ADAE4B11FF4CC0E + D74FAE93EA02966BA40260AE9152EF03CC7DEC29D717B08BA48EF357FB34A582 + 7FD0CAC2401B04A6CFC36299BFD98360BB8E9BF4A1ADD06F40F861606FC80FAC + 0F800185A387C4CC0D1C03E0DCDA62A37B782D6B105CBF161A3AF7B01A10D8C0 + AFBA7F79B86D851CB77C93EB42EE5F1E8493F50F1098BCC040E02E00605CC0D2 + 56C97A2B71FCCA3A0C59BBA3ACBB10751EC8B3A82C8487EE30875CC04BA5AF58 + 05F0A53400AC1078C622D74AD6892DA72E48802F6BC5B6284E1BA2EB47FF60A1 + 01E018045367DDAA6B5B20B1AE6559EB4E97B1E01A0E73F40147E68E9DE61E1A + 3B55DDC50F4AF9C068597F8770CF1A065A1FA29432ACA5D5051CE0B0BA824786 + 083C3C94C9BA7C84ACC54358E726833CD805F8D267F0D780B0D50DFE1A08B6BE + 4603C7B9860300C1F17980CB93D4255CBFEF48D7A8DF68B9DF00F612D2D9BB7E + 9B0C1C231A9B387DC9E1DB504A9D93C061395EFA99431D085CB7176E60B9AFEA + 3DDCD5256F702F1140F8610F8329EBF4182AF754C35C23196B447EE19EB9F0CF + 9A03B83B79800728040614533797AFC98070DA115C0AFE06D542911B18594868 + 83BFAAD08E01300E6073F82AD80D65DAF96BE33696B45300D84246D76A431F63 + 39001CE7028EC5BD7F8DE006F6A587C4940A7603AC35806B80D6002F00D81483 + 5B1B8F8F4F2463892B98F2C19C7B383E47B2FF1154ED419913DCBE067F63A94B + D8F65100BDF4055551077198AB7B3752863D9FB4F221703EFC4DC35B03BB31F8 + 8DEB59ED747F5AE6F855886BEEDE1802DB58866CAEC9F7B37FD754CA66993200 + 9B06C0068563384C69A03681B3A9765AF1B8C9DCBD85FA4D1C5FAD7E0B7F9E68 + CFB42CC5FBA5A6786FB7AC7979FD35E5B8EA721CD27D63F693D907B67DE1261E + 12277BC5619FD800B0B50DFE9625F6CE83D84737F819CBF6DAAD5DD61E7E7C5C + 794A1F7B6FF8C05753A1F7937EFFF17B2F5B2956636CC9C06AE04E775B31B7CA + E743D9F3AC0EF8C5FDAB7D65815FA4E95383A4ADBF577A8EF563D8CC6278E529 + CD29FF392A0BE4DEA9F48459FF9059B2379035F66D90FE8165FFD195A7E48318 + F7F32188FBA20F887D48BEF9E27D9656FCE5F5555591F3DDBBD7F7E7D28B580C + 600B41DB18E8DECE1C949E172B8C1BF48D658B80B4B8D8A7DB499FC2ECF09A5C + F06B9163828502E3D217CD8F953E572C5B8094B51041D9E3D21701E0D80DAC70 + 3780DD1800276EE1D04F08E8642C0D7ACB01C069F86A40369601DE2CD938E74B + 1F13B70B9DDB64D0B710F88DFB755E83E0009605BE07BA01E64A8903D816FCDC + 00F85CC17233C0CD82DE30008AA5AF311098BA1CC7B8DC4454C5BDDBA8BD825F + 85BF01F6E2EE55676F681BF855F81BC6D302EE527A68EB65A0985CC194B9F11C + DC4D2B392688BE1A01109B5358FB348FAF07C0E6F0BD1F080B9805DA36F720D8 + 003063269B13436183BF067DA91BF4D590CE729CB6658C7A2C83C0B514E8CACD + 750A009BEB5761B0DC38D76CE7E7D1471EE0DAED03B4ED38C8AB03F55C68E73C + F84B3BA5A40FF80BEC15D50D70D7E6C4209870CF887ADDCEDE156CF0D700B041 + DE18FA12FA39A751AE612F4238E3E81DE5EA891AF4C6D9EBD5B037805704CC95 + 12C04BAEDFC67DC72BFCB5364A8030F508002BFC1D38C93D3460929600E08603 + 3D004696F717991338090D1D09F80BF06D3C18F8EBC33F7B27F0740F81474C91 + D23B1012001C36A670FCE26A68858B37388071F36A386729BDFB77866E74351B + 2FE332D73B8067BAA6A2E684839E08E4F5A1F44C0A75A72F70ED446CC8B139A7 + B077AACC9F4208681F06BA25A1FAE45836D9E2CDBBB63317AA5A4BBD35E19EC9 + F93B6381425FCBF90BFC6D479D30D07397B8F6F3C9E7BBC27524FCE03CBFD9D8 + 11274A00C01DA4BF682E7356BACE8B56BB2E8BBD3A17AF715D08ED8CEB776189 + 2B62FEC255EA666957BCCEB59063DBB0C9B978BD6B0B1496FECE2B37B9CEAB36 + B92219EFB8644300C2E4D35B9783BD25DEE9DB0D08BC6ABD07BD6B82DB57C691 + 016085BB6B7D184775EE249BB4413880D76D763DD66F49DC3F6CFE027D35C433 + 4E21A0B0F4AB03988D6629515FCA477D9D8D66E06FB2F1CC06F59627B4AEF90C + 9FDE9E03BF526A5D84C30900DC7BAB9CEF49393E847EA60E003620ACF56765BE + 963B9C825ED1A0B0798EEB17D1A79BECDB77B913AF7D3D0018579986177D29E7 + F2A54E6852EF40F3F0D7C67C5F1A007B572FEEB7B16FBCA67063E42B2FE9D838 + C02E0058440E4E5CC323E4F8E14090D75ED33EE6682856CA0065C8F9CB5CEB07 + D01804FEE1A17199BF4B4574FAD06205BB067B112E5F1CC016CAD6C2430382CD + 114C18E889470F046456B11F40EDA93FF917CE72FE4E397EC44D3C22E73A29ED + F7DF77D3DE7DD74D3A71C24D3E71B2C2CE5F7EBEFF377FE3C8413BFBC30FE43C + EFBA276E7E1A46CAFFF9C1BFFBB76ED9C58F14D0E2F82537AF77FAFA5CBFDE05 + 7CCA2DBF74F6B6F2F2F283DB76F1A58FD419CBF9288FFDF48FC3A8FFF9D5EF7E + EBB6DCFA44C7165F38EB965FBDE8965EFAD82D38F7A15B7409107CC1FDEE7FFD + AF303BFBE7CA5FFFB55B74FE9C3B7A1BFF669FFDFA976EC5E573EAEC5D842B59 + 5E6FF1C5B36E89BCE6E28BE7DD6239DFE2F31FBB85E7CEBAF967CFB87DDFFFAE + FBBBFFFEDFC3D1B91FE0F4D69BD7F4DF0A700EC8060403B72BF2F3BFFFCFFF71 + AFFFE4077A2CEF0190BCF0FC193D0F8EE3991F1C5717F65CF97F78E327150B89 + 1DFFCC262FB1C25F0F747FF14FBF0923857F00C0E64406FE5202821F7DF7F9CC + CFD097C767EA3C00B039DCC96F3C0727F34960AF7D6E70FBE2EAF5E1A0A97B30 + 0C14CE8D1332FAF323B990F7E41F7EEAF0D3FA799C2E9F991932E7CF8E9486D1 + 3F3E32C14D27CCB41C3FE5887C6645534593C9417C483ED307E5FBE2C0DB6EA2 + 48DBA2C90114DBC324947CDFAC797B5BE64325B87F27BEED1F6099F0B67C3FC9 + 77D27BFB4B03601E72E1811503BB94E9391F1C58A8C7EB83317CD785BA86BF8F + F207037D81BD9613180DD9BBD70DDABDDB0DD9B73781C15921A089E4A00EE1BD + 1EF4520EDB27E7DBFB821B261A2EF5E1CFBFE886ECD9E706EECCE5071EF8DC2E + 75000FD9B957AF435CA700C05C8F2C44747C8D02021312DAC671006BD867AE8B + 40E0487DF43AC9B5D1BB82510C8879E0CAF2FFA2741D506C00B8F726EF04B6E8 + 1D563777B002E0E002D63581AC01D4112C6B0783C006804DB6AEF04078A3EBB9 + 8E75070FA5E5A7A348D627ABA4BD7283C25FD53259EB100D8588273CF456ECDD + C0ED45444B01029B039807E9DAC97A4BA3AD0076E7AED0B518652EF5C62AAD1B + 00D650D07356E8037C6D672F117900ACF057DA3CE48700C038823D04F6EB44D6 + 9B5AB24E24420C0F0CB2964CD69FDEF18B0CFC2AEC15017F35FF2F6DEAB2D66D + CA7A57D39678009C3C0C39D183623FCFAF8D71013F300AB0EB23E958AE60EAAC + B32D6474F2D0256B6F141EC6040203839B0C19EF1A8B9A0C99206B79DCC1D23F + 4CEE1370F3E2EA05124B69F370F42A0006FE6ABF87C00686CD196C754244D7EF + 07EC1D9BC8C3DFD1EE8141F90018A0DB001730C7F491FB25C0B1D481BED6660E + 7500304E60EB270434F03756835EC3E59E6A9868A86BD07348028081BF75BAF5 + 572908060A77F5B017017F71045B8E6003BEB1233886C2C8BB8003FC15D5EE28 + 7345E6043637B082DFD0AE55C43D6BCA019C5232D6260780ADD471E6495DDDC2 + 9AF3371F049B62005C2DC05D05BC2114B48583F64EE09C62A0ABC016281B6400 + D860ADCDB36300B7B10CF4C6220FB0D59903B005DE528FE1AF015D85BF8CB127 + 920180CDD99B97233840E03CF09B3E06081CA06F36FC6DE7340558C39C7337CB + 0D1C03DDB89D567A3C86BE2675FF06009CA52A29009C1ECF929FDBACA00CD4C6 + B017186BFD56A78C01B08DA7E7C5EDB4E2FE34EC8DE7591FF3F53D71CE54DEDF + 42CADF2FCDA9AC3DD94C01803504743827C747E0376EAB6A360A6A9CAF3400B6 + 7DF22C4540B310A4CDEA8BC70AF5E3284E2B3DFFDEF181DB97FD9EE9F7142B7D + 4CD69C6C65309A9839550000DF097FB263CA3B366B3CE993F797C9C9EE446900 + 6CECB12CFE58513E199FEBDBA82C907BA7D21366FD23A5652F1EDAB13D9BFA1D + D9B5BF6EE91FD79DFD711602C0794F3D441F86F487A45CDDA3273BCA17EF335F + E92FAF3B51D67951D65C54D179B72F7F2E7F11938B61AC2C681B035D5345E6A0 + F4BC5861FC6E0060BBE87B457D1A222417F683BEF42224F35C419A4B23633193 + 9E97DD2F7D1900380D796DCCE6597FB90018A521B0B67D0EE0CAF564F15A3F1F + FEC670376EC7FD711DC5E7B27E3B6FFAFCE97E1D0B02D65ADD16F1067DE385FD + 7DD2775F030400E6A9501F7A5961AEF4552277AF967E5EE2FED59B07196B0C00 + 3679373110D8DF70C8CD0AE704084BDD406F1CF6D9EAC85EDB9440607503E700 + AF815A03B93676BBE21C0A7CA350D1310026BC130EE018001BE035D166AC76EB + 5E2AEADA1F04D44D0360A48E5F995BA3755068C7AE5F958CD5A4DDC6435D05BD + 411EF2E2F225AC334F58F77135DAF576D5DBCA39DAF57575DA07B81B41DA18FA + 2690973941C9BC0C599867AFE0F0957E85BDD217E7FA55100CF00DE0D7C35F2B + 01BF234309FC1DE51AF51AED1AF71EA365C39EA35DFD9E1EE4AA1ECE85753635 + E8E51DBF8C13E23906BEF435E93741DDBD40614071A310F2991219007E70E064 + F780D41BE1021E840B604A027A81BF3880693F346C9AB3D0CF805F9CC0C81CBF + 0060E02FE0D75CC1D49B8FF21B504DD96862038A52DABA5115729A21DBB022DF + 59AB8961732C6C72D9E6578B09B315FC7AF7AF1C3771AE6B35491436C9D4ED2B + C719D04D34954DB8B9AEF534E99F0E1C9EE3DACEA064636E9EE66B6B013426AC + F3CC451ADEB985CC6F11C23E037C0DFE5ADE5FD47AD64255BB79C5AEC302E0EE + 32D77E1E9B8BE49A63D37199025F4070C705384F3C24EEB4107730AE9512D7A9 + B8C47559B2C67505044BD9715189EB5CBC5A3738DB2C1615AF733D373EE5FA6E + D9EEBAAE7ED4B52327F0F28DAED3CA4DAEE3F2475CA7155217755EBE3E174AB1 + C4877806F0B2C18A12D81BC6D0C3C1A5C3862CA19F2DB4337DE6E051370F9BBC + 1BBDC3B707F0376CFC52DAE630AE20C426F1409CBC8F3FE3FAD0CF66F19627D4 + 8DA4AE5F36A4530098CD670BF96C00987136AFD5C9C431724E423D1BE8B5DCBF + 0680FB3E25F37007131AFAD9E774B33CDE4827FFEFB09D7B35FC339BECF40FDD + BDCFBDFDEAA2529BFB775BBF38D05F412FC021CE45491BC860EE33FA011396E7 + 9290A5C08E3C8001A80D22C4E97005BCAF260E3BA4619C81C0CC9163C8E93B4A + CEA16158A5641CD04B495B21B0CCB5315C7BB70E4DCDFC5D2AAA0F8F14ABBB17 + D00B603201815501FE5A3868CDE32B6DE60081EFE40717E6C13FFC895B7CE643 + 37F5C40937E5E42937F5D4BB6EE5B9F3EEAD1FFDC8FDB6824E567E80CAEBAE5E + 0CF0F0031FD6F9F429F72FFFE1EFC28CF27F7EF7BFFEA7FBE0CFFEA57BE4D3CB + 09A80508CF3DFBAEDB78FD92BBF4F3BFB86DF0C8CFD55FFDDC91039870D20BCE + 7FE08EFECB6C4733109430CEABAF5D766B3FFBD4ADFCE4B2FC3E675C89B47FF2 + 77E5FF1ECFFFE007AEF8D245D125F7CBDFFE2EF496FFC3EBE2527EE6CB1BEA78 + 5E72E59C5B7AF5BC5B7CF16375F6965CB9E88EFDF44FDCAF7EF74FE188C23F84 + BB8E01F0A61B57C248C57E7E29AFF1E61FFED0C377F9F7E71C006080FC6B3FFE + 9EFBC56F7F1366DEDE0FE0D6002E50F7971500C0BBBEFC4C012E02E672ECDC90 + A7F8A3E3CB323F4740609CC00A7E033C9E09087E97B0CE387A730F4F5047334E + 1E49C6CC693FEDC4E13CF81B6BE5D117E5781FAA7DF5F197DD7F3BD2B5D41C8E + 255CBB7F70E3809B26E7B5FCDDFA39E7F3CDC31DA20907DF52573161A527E22E + 0E21A3D71680BF3F3C3856BFCF80BB00E0B16F90EFFCB5CC1CC0CC310730F097 + E3D273388EE399973BE7EB5A4E7AE71DFD6EE5BB96EF62006F9C0FD82030D271 + 69670160CDE3FED2CB6E98CC19BC1750CCB9C8E98E6BD803E011FB5ECAB98245 + 00602D4543B9360174E5DAC4B588EB9546AB0800D8AE51F43127090B4D648B67 + 9ED3F0CFC05EC02E7981AD4431F4D56B68682B108E806F7CFDE5DACBB556DDC1 + CCA1946BB901606400D84243DB3AC01CC1DD647DD0B944D61FAC3D0204B63585 + 4160D61A26DCBFDE01EC1F5043067F59A71099A4BB94406005C1CBBC00C0C05F + 03C1DA070C26ECF3A2D58900C1A4D5683B47D662B20ED3482BF3F3F3019B23D8 + C0B03981DBCD010803819784FA5259F379C51098D41F9A1A2452ABE9F3356548 + ABC8196C2E6183C1EAFC658D1BC99CBEA5F206877ECB1B6CEB62D6BAC8407053 + 80AF94ACA5913E6819ADB5516B424ACB1A9B3ED6E10FE21C0E117A080B4D5E60 + 0D0F3D4CEE1FB4EDC74D805E85BF80E20086751EC7581B802C22243402005B9E + 60D464D038F7006058FA7CE867B95FD13A8E600F790DF8E21406F42283BFB87C + 29AD1FF8AB00588E51306CD0B7B7DC5BF519A9AADFD3835F0DF7DCC3E7FFA51D + BB81ADBF6E08EF9C05800D0C97A5181227B03882C026C06FD26ECFBD6CEF4469 + 180CE4A56EF981CDED9B847C0E70D7035E0F800D025B7F4E8061B91F0F001817 + 708D96DDE4DEBD7BA242101825D037D58E41702C83B731C805F6C6C037167099 + 79E6F0A55E0800D35FE321190F10380D73E92B940B58F77178D03F887E9CC4D5 + 1BCB9C14F0ADD6A82825E96F9483BF69001C0BA89B6EA765FD85C0AFA94A00BD + 5932F88BB2C6D3C2019C057E4D96073881AD01F2A6E1AC95C8E6C7B2B1B214CF + 8BCF6F8AC7EE2317AFF5853D5394BF1F5A5AB6F71AEFAD5A9F29EBB852520730 + 10D8B7EF431C2B4AF666395F02801B672B00E04A35A40ED0B47DF22C05206BF0 + 330D684DF178D69CF41E3F7300BE556B3649F4CF0100DFB94A339A4A212CB232 + A60A02E02424730515732B54680EE745E93995E5BD1B232B4F092F435148E8BC + 760C808D3DA655DEF83F576581DC3B959ED0FE216F4731F0A5FEFF03606D97F7 + 21FA26C9BFCF7BF3E59A3EA7C9C60B7DC1171ABF7385DF512517BA8A80DAB42A + 3207A5E7C50AE3B70380D3E2629F5CE053F558858E41717F5AE9854CD6B9ADDF + C6AC4E08E8CA110056887B070038995387C5610B0F7A63E09B9201DC18E22A64 + 05B6A6FBA3B9E97E4A05B8A1CFEA067663C5734C5503D08D65003886C0E9F1EA + 0F7474755ACBCD1590F2C12EAE72E30E8E10CD9A0F5816F9951A4ABD81416239 + 97DC0C68F820B909A9DC44C6F30070BB0400FBA74C2B068015F832CFEA910C02 + C7D016501B83DC3400664EDC973E87F551DAF912A84C3825C23F35EF2A376372 + D3D5D4E7FD4DA06E04800D089BFB5701701807FC1AFC4D036073FFC600B83A6D + 512DE953006CFDADBC4B983AD017D86BAEDFDAA23A1DE4865B54AB7D5F05C035 + E5E6B986F4D72A929BF00080EFEF3428E7D60D003886BE48C7AD645E80BEC0DD + 52EA32D4D515017963F0DB00A01BA06F9CD717F06BB097B25E77CA11AE6E375F + 6FF0F02885BE8D7A8D710D14F8FA5CBE40E006BD22176F1E00CEC15F4A03C036 + 060006F23609D017F80BF0B570CF0A80A90FF4B250CF0680A9038181BEC0DFA6 + C3A7AB1E1279E02BE301005BDE5F531C12BAE928EFF6CD7221D8A6943915086D + 87FB57216E80B939E1F69DA3AE5F424003805B48BBB5B92AC23CDB2453B7059B + 678C895A4FF5F0B7ED8C790A7F8B66CD4F423C03809BCBB8C15E36F09A2B0096 + E300BC73162B18D6B0CF738A5DFB794B546DE917B59B2B7DF3976A99B76138D7 + 80F032D771011B90388257681DC770FB052B1402772E5EEDBA2D5DE3BA2F5BA7 + EA2AEAB46CBDEBB06293EBBDF95937F3B5236EEE5B27DCC0ADBB5CC7959B5CA7 + 922D5A765EB5D9755AB9D175582E73657E477301AFF28E9A1E6B10703797FFF7 + E1359B5CCF755B920DDAEE52B219ABF97FD76E76DD44F431A639FDA2CD5E85BC + A1342790BA7CC32631FDD4FB3DEAF3FE32C7720EC6619D35F473107D0A7B1F97 + 63C246B43A9550D8B8F6EE2536B7B7BB41DB9E5301827102930B18F593360098 + 10D04377FA9CBF16F299CDF5215212F6997E83BF38B636BEB0B1D4E6FEDDD68D + 7726297040061A800BE63A03065B585283C0E3DFF0B97A2D4C330003C5619B81 + 1C0A7703D8A51C85BB4D8E05F002720DF422FA01BDF49BFCF85B52FAF6B203CF + BB9F1D1A91F97BDC8E5E38B655C33E0374D5F52B52B7AF81A300A6283544B448 + 6192CCC789F8FBFE39FF977FAE0E5D1CA8004373F1AEBDF271857201A77F80A2 + BFFCED3FA953F44EA06FFC830318F80B0406B272EEF27EFEF67FFC0FF7A7F2FA + FFFABFFC970ACDE767EDF54F5DF1A5CBAA67BFFC4E858F4BFFFCFDFFF8EFEE57 + FFE5B7FAFBDFC939FEEEBFFF5F6EF3CDAB0A9301B8977FF15761E4F67E7015F3 + 1EFEEEBF572CD772593F0B00CA84F23E7DD21152FA971500C9388001B8B16220 + FC17C747677E967E7D74B07BECBD17140033979CD7B3DF23C4B31C7BEA989B29 + 9F1D3E43334E1CD13A3220CC6710A8FBAF8F0CCA3CF71747A727B018718EB78F + 3F9239F7CF8E8C702B8EBCE83FBBE1F36B9FE118024F38F8B602E049470E6A88 + 6900F0DEC38F65C25F424C4F7FE305FD7EF2E19FDF49BEE370F2A6E733A60FC3 + 10F580EF3269A7E7BCB77F81C26183C094B40D08C70018F17DCC77AE7DFFDAF7 + B37D579F7C634EA9D72004F4E897FC3906EFDDEB06EFDBE706ED21F4F31ED15E + 75040FDDF3828686B610D13E4C347985656CF7F3EA021EBC63B71B217D405F20 + 2F1A04180ED72E03C0B884752CB4FB017AB97E6EDBA1290E066F93EBDCB33BD5 + 21AC2E60AEA972FDEC2DD754646D0B0F6D0F6121AEB95C97918E732D060273ED + 16D91AC0AEF93C08A6B97F35F4B38FFA8180C15D8816226B8C64FD4098685953 + C4F0B7B3AC4F50D7D5B22E59C5DA240780CD016CEB156DAF94F155522EDFE87A + C89AA7BBAC7D0C061B0026658602602981C3B12358D36A2C5AED3A2F5EEB3A86 + DCBF405F750287741D0686CD11EC1FE0C311ECD772F19A0E01815BF360200F01 + CE5C90A4054106815BEBC387F3BC322070ABC9B9D0D048D7AB01F60280753DCB + 3CE9A39FB52E32D72FF3E275B23A89E5384D9FA2F0779A3E8069E0B7CDA439AE + 88872179C052FA588B3719EE73025B48E82C3166E309E895D2C66300AC603838 + 7E81BD3E2FB07702D3AE07A495B2B1F4379212A80BE8354730759302E181721F + D44FEE91FA0184090D3DDC9117983A65DD5E435C1D1CBDBD81C2C35DDD9E724F + 469EDF5EC35DA3BEE41196FB23A9D703F606F80BEC0504E3063647700C86EB76 + C3191CF2037717D10EF097D29CC02806C488BA8D27F01798DCC92B0D81632910 + 2E02F20600DCD6C35E730003796B4B990E111DC35F93BA7F3300B09F9B730053 + 2280702D9102E09639006C10D8EAD5E3BEA0388CB302D90C006CA2CD3C03C0F1 + B13100D6E3A46EC75B69101851A7BF067988AD3F71F8B697BA9F479F815F03C2 + 0A85D95F612F471FE46FE7AA35F1FDD5651E1018A5C16FAE3F0580A5AC620A30 + 380D85AD9D86BF80DEB85E26006EC0DE9707B8552218CC5E5D2CC64C36A7F4DC + 66D22E1F02C700D6206C1694A51E835F80714500B0CDCF1A43F67AF63A26EDE3 + 75A3FDD0F4DE6B2CEBCFED99960D80ED9CA5640038388093E3A59E7DDE26A2C6 + A51501E05210987ADC16B18F6EF033BD3F6FED42E3A6340B609EB97ED30038EB + 1C694EF04D50FC3B5554858FCB663548F953467F5A308F7B0D80EF630EEC4BC4 + FBAA8CE4FD1B272B24E6E531B42C00AC7DC6EC029F8C01E8DD52CC06EFD56BDC + 0B6581DC3BD57D95EFCF3F79FA1FC514CF89E7A6DBDF64E91F59EA0FB01CA5FF + 80F3C6EDC3413F8A3E2CDF74D98739FD6576AF655FECB1CA1A8FC762D91766D6 + 58BEFC1C7F11938B5C5A69501BAB22E319E74AC06E3CB72CE55DAC0B2B5900B0 + F808F5828B0451A9E3CAE937D938B20545211934AE525B165111C035A5616FD6 + 98C2DF3A2D5CD5DAB2709492B601E058A5403073EB79806BA1A0EF03EE4A5F55 + 202BF035C05A83BE85C41C9B1FF7C56326DAA5447F4A1AE2398C017CED69CECA + 0D83A357DE73BD560FBB76FDC7BB963D47B93AAD7BCB8D4347775F031FCA5973 + C4C8621F384B1B004CCE5F0F7F790AD597EAFC05FE526A3E61399E63F4389E2E + 2D030067280B009B62886B0057C16DA81BC8B53E64C75A3FB27351270C3402FE + 56E1495A6EA834FF2FA5CFF58BAA8B14E44A5FDC6F4058E1AF8879067B13591F + E321ECB32A847336085C0300DC06A76F1F57A34D6F99DB4B5543FE6F6A4AFFFD + EDE4261AB8DB0ED7AFDC4C03793B0E909BE77EAE264E6029C9D9AB797B996730 + 9752440867443D0D810DFA724CDC07F4BDBF23FD08D72FE07758A2FA80DCAEB8 + 7E87CBCDBF8C01797B8C50E00BDCC5EDDBE061342A9187C1CC1999887E72F622 + C02E20B7519FF179CE5EA4619C09E7DC7BACAB1FC0307380BE3AB7F718D7B8DF + 780DF51CE7FB7D70D0E424DC33F97E519CEBD704FC35180CF435E72F65D39133 + 1502C70098761A023F306CBA689A7B70248E5F20308E03F291F97AF3B1C0DF99 + AED584D9AE85425D9C0F33B46C8643613C0E6036C3829270CF735D8B09735CF3 + 716C7CCD76387B7149D866189B6DB6C1467FB3C9726E514B00EF743437940061 + 993B7DBEBA7D01C0B836BC8B63BE6B3573A16881AACD6C0F7FD9ECA35E04009E + BFD47558B02C077EE72C9631EF18691F9CBEA848EAF4775AB052433E77C0092C + FDC0DF0E8B56B94E4B70FBAE765D96AD71DDC9DDBB72A3EB28EDAE2B1F71839E + DCEDA6BD76C8AD3A75DE151F3EED063DB1DB755AB5D9755BB7D5752E01DCA24D + 0A81BDFCC6AA87BFC1F1BB6ABDEBB262B5EBBC725D02797B88CCE59B846494F9 + EAFA5D1F2475366FD9FCC5F5CBE6AE02DD2003BC06802981BDE419D4FEE03232 + 47119BCB8479A6B40D6AC02E1BD61AE2994D6A369ED9949639FD83AB69D0B33B + 557DB702839F55F80BF0C5F5ABF0370061D576C23EEF72C37158EDD823E3DE35 + A50018E81B5C558383138B8DF8652F3C5E6A73FF6EEBE53757295C20F4A8397F + 0D30D0B6B0A4D35FDDE71E7D6D8B7BECCDC7DC13EF6C153DE11E7FEB710F3C02 + CC40C56FED725BDF79DC6D7DFB71197FCC3DFAF6636EE19B3BDC98375F73E3DE + 7953357EFF5B6EEB8127DC13079E744F1E7CD23D25A2BD75FFD6D02FF5A02764 + EC83438BDC8F0F8FCF7CFF77A2678F6D530885631001770D50193842CC518825 + 5288257D33A5EFF7F9032824FFECCCD327D5F9EBC337FB50CEB33E3CE10EFED1 + 0FC3CCDFCF0F00B8F8F259F7DC773FBF63285BDE0FA078E995AB0A7F975DBDE6 + 965CBEE28EFEC94FC3E8D7FBC3EFB8ED0F6EE9EF0CF8C6110C14FE7DFE2CBE70 + C6155FFC48731EA38A00601CC031FCC5016CF09772E9E9C3EED7C706677E9ED0 + 774ECC726BDF7BC3CD7EFFA49B47086A2967BD7BDCCD16290496CFCE2CF91C21 + 3E6BAB4FBCE2BE3C3A23F35C0828BCE8C4DBFE010C996FE27359C82DFCDF8E74 + 5577FFBCA36F249F5F00B039FCBDABFFB0945E4F1E7ADAFDEC70F6032500E175 + FBB72BFC4584AF07D002810919FD7E0600F621A2C973EEE12F657A0E00D8BE2F + EDBB13C7306D8E19FFA6BC9E7CDF8E7AC53F9483F8FE35304C3406BE97A9539E + 7A6B5EA9D718F5827C77BFE873062BF405024B3944CAA17BE43A030C2617F06E + AE35D22FF5618489160DD290D0E404666CB71B2A63A8FFB61DAE2FE1A0C9132C + EA27F57ECFCA3513E02BD7377301733D43D4913DD444940BAE9B7DC81B2CD754 + AEA33104A66D6E61AEB95C8B87707D953E83C15CB3B97673DD56C935BDE7E6AD + 9AE601F8DB4BAEED405F83BFB616E0FACF387D36B7F7235B4B01602B59A77459 + 232A5927EB940DAEA7CCE9B5FE51FF701A8EE10081511759C374656DB354CAE5 + 1E049B00C08850D008F8DB51DA88341A3A2EFD3884918260293B2C289135DB72 + 59BBB11EE3813C22B594A8D40D3C3BF7001FA5858256083C6F99ACF316BB56B3 + 581BFAF5210F0AA601702B5CC02A0F83A9EB0389AC51C378CB29B2EE9475AAC1 + 5F0B0D4DA9FD01FA329E80DEE0F85517B0AC7BE9C32D6C00182770D3B13EC58A + 3D6809F46D3B79AE6B37455E5BEAEA00B6883C23A7E481DC34D88DE1AF8D718C + F5C7AE60D5E071721FE101AF397FAD240CB4B5B55E0E006ED05FEE85FA0F770D + FB7BE8DBB0DF8844F5FBC83D57EFA1AAFA94806040AFD41BF6C1FD2B737A9107 + 78B0EF8F606F7DE0AE94B481C0D435FCB3E6040EF057FA1AF69473F590E3BBC9 + FD232037406073FA523266E0371EB390D0D411305873035BD8E72003C035A5AE + A1A1C90DDC16C97DB2D40D00A3D811AC3057EA006185C2F445C09771DCC1D68F + D431DC4AEEC55B7615017EBD1B38114058C6927CBF11FCA5DFF2069BFBD7806E + 2C85B26508A7703CDFDCC3D40D022BEC658C32B4AD6EA2CDF9A81B148EA5FD08 + 281C544DFA81C50A8C6340CC7CF63E10B098FD99C645897C7E60A0AFB445A4FF + AA0CB08DD5A88D0A006C7D5545805FE0AE9565C9C06FBA6D5298DC40C65270D7 + DA598AE12F4AF6F17000D717950381EF0BF0350BCEA218D0C6F0D70070ACACE3 + 0BF5C7E2DC067D13F88BC25EACED8796B5AF7ABB8AF759F344A4C79AF29E2C04 + B428DEB38DDB365EA916FBCC8D4BC9E06F028023E541E168AFBDD0DE7C797BF6 + E9319B9FA542E3F1F1DF64E5806E3EA788EB59F3B258CD3752B1F1F13654A54A + 7D5739A33F01BF0108572A2B0474AC78CEDD52D6EB7C93940572EF547A42FBA5 + 2BF2CBC773D2FF60D6FEA64AFFD8E48F37FEC32B471502C07741859EBAB8974A + E068F8F2A11E7FC9C6FDB7AB425FD8D61FABACF1782C96BDB7ACF7977F5CF81D + 5572411355AAEACB18DA26B23E1B2FA4F431515FB900383537BE7827D2D01DBE + 9EBB880769D8E7DCC5BF900A1D5FF0BC41E9F1CC454810F0D7D76521950180CB + 52990058012F8BC4165A9682C088D70AB0D642411B08AE0A70C5650B840D7362 + C5734D36D74A83BD31FC35593F651AFC9AD200B84A43BF88C7D9ABA0B66E6B57 + A74537D7FCE161EEC12E43DCFD2D1E76951B7770848356002C0B7D1350585DC0 + 010C7B080CA0953185BF223D2FF303344669002C6521B88BF2FBE526849B1451 + 3C0725372B0FE6A0AF39794D067B0D04A7C7B4DE949B32B9A113F990CFD2171C + C05EF9F0B76AB36EAE1A003838836B18086E9173FC66B97D6BE0E20D6E5E73F4 + 22E0AF857626CC73ADB6387C01BC72032DF5B4740CC05B04F035D80B00F6C097 + 12272FF51A4062A927EE5E806E80C0E4F14D43E01800D7E47C9C8BF9CCED8486 + 24F2F077B80AF08B2BB836CE60A91BCC45387BE3BA8936F31A3226ED7A40DF00 + 7E4DC05E83BB0060EDA7AFEFB82494B3D6654E6399D3B8DF0405C2757BB1A9E2 + C33CABF3574A75FC0E9CE4017000BDC05F05B984741619FC5507707002037D99 + 63737D98670F7B2981BF4D47CCF0FD43FDD843D26E3A6AB67B68E44CF7E0089C + BF4060A0EF2CD76C0C8E5F42D011F679966B0DD49D38CF350BCE5E406F4E387E + E728F055E81BFA01C1ADA7CCD7CDB1C43D21757559B0C1C66619C2C5AB70D7BB + 795BCD98E35ACF98E75A4DA72DC7685FD8AC1369D83E29D9C4C3E9AB5037405F + 73FB268EDF0081730098BEE5EAEED590CF0B719DAC52D0DB6EDE32D7797189EB + B6749D9680DF0E8B654C04ECEDB474B5EBB06495CFE32BA2BFE7BAC7DDD4970E + B8A5C7CEB8692F1F74439FDEA7A1A001BF9D576D71DD56B3B1FAB8E851D77535 + 1BAC8472F66116359CB394BA81BA92FC79AB7D58E8B59BD59D631BB1B4633D4C + 586711B0D73678D9C8EDF18807C06CF41AF825CF2F22C76F3FE9EBF7B8771699 + 7BC83694D94C46EA327ACA87A4ECC9C6B2CC53676FE8C3CDC4063560D85CBFD4 + D9B00600D3070036C76FFF10FA594B03C1DB77B8C1CFED74C377ED732377BF20 + E5F36E28EE2B36C79FF32136715C919711F8ABAE2CA9A737F7EFB64A5E7D5241 + C204C043800C08284C7B5C70FFEE7FAB38F3F81864A05B0726979A73F0C05237 + 7EFF1B6EE2C1B75540DFF49CAF530B8EBDA980082044CE50EAEA4E0CA0D76053 + 5C8FDD8CBFAF1FC2362F39F7A13A7F0D006BAED8E002C6150C1026C7EFEFEBE7 + CA2FFF4A61E8CFFEF13F869EBBFFF3D28F7FE2965FFB442130A5AF5F73577FF5 + 8B30E3EBF901FE3EF7BDCFDD922B1F2761AF8B2F7FECB6DCBC76CFE077457E16 + 06F08B7003573404B4815F0BFF4C6900987A791018317EEEC472F7DCC9E7DCF6 + 13DBDD92770FB8C527DF71CF1E7BD66D3FBEDD9D3DB6D4FDBA80E3D7F4EBA383 + F41840B17D0EEDF33A357C56BF3C56181EA31F1F19EF8E1D2D714F1F79C62D3F + FCBC02E0678F3DE35E38FE84BB7264BEE618CE3A0E017FD7EEDFAE11092C6CBD + E61F07001FD8AF7983DF3F583A3CBF8671DEFF4E5E68E7F41C0030FD067D2D5C + B481604A8BA08093D8BE87F98E36F86B75BEA7DFCD00C0C05F20B08590360184 + 71070FDBB7D70DDE9B03C243F7ECD1B0D1C3710AEFDCE506EED8A9FD8377CB75 + 69D72ED580E776B87E5CC3141CEF93FA4ED777DB0EE9DFE5FACB750D57B0015F + 1CC3005FF2035B0E610424EEF78CF4493FD74EAE9B0680B9CED23627B001607D + 284BAED17AED16C500D8AEDF3CD8450EFFFE5CF359036CCA450031F86B914180 + C6BDB748495F0480758D1187850600AF5E2F63EBF5A1B45E3C7416007097953E + A585B980A9775CB2D6035E60EEB2F50A842D1C34A0D7DAA6A42F806003C00061 + CAB6B25E6BA70FE3F99CC11D88D0B2A044D67ECB5DDB59FE613E5CC006805BCF + 94F5DFEC6259EF2DD135610B5947B69A296B4E5CC0C8D6923C44385DEA005E5D + 63FA3565CBE9B2CEE58144A0AF8CE9DA943A9077AAB40D0007F89B00601E6894 + BA867E0EC097B68261994B9F398681C840E087C64E93F5B4ACB5C778084CD867 + 0070E2FE1D3DC5E70CC625CC839901023F342CE70606F89ADB9712E91C193768 + 6CF0B711D057A40E60E6D2179CBFEAF40DD0377603930F18008C80BD883A619C + 0D0CE3FEADD78FF0CF1EFA1A046ED01727301A26F74032DE6798DCFB0C11017F + ADEDA1B0770407400CE8C5E51B01E0D8010C0046B581B9DD07C87DDA60D7B087 + 1C17016003BE0904EE5C7618E80400A390173876FEC6AAD9BE97AB5ED4D3556F + 2BF7C9EDE43E5961F0C3AEA6C820B0815D85BBD2AE43C868FA03DC4D036003BF + D501BF912C4CB4B9804DD55BC97D7F0BDCBE1EF26A3EDF963E5CB407C78C7595 + 7EF60DD82FC82906C33A1620ADC2DB48067CAD6EED3C004C3DC0DC2C19082E34 + C639E2F3F887F63DEC450685ADED61B1F41BF0A564AF47745FA3B62A750BEB1E + 127B441802880CD74A5509602B6DA46D80ADF465B97ED332E86BA5B986E3F104 + 00CBEB56615E0477AB20691B1C8E816F6CE2D03D3CC06F3DA9D797F10652AFDF + 5CE63593B12CB1D7C75EE043AE12203682B2066E13182BA27D3BF0377D6CA1B1 + 3CE82B4AE6514FED89A2783FB522CA3A267DCE44E500E058B9F3D16E9CAF721C + C0E9FEF4DE7CFE3E7BE13DFF422A749E58F19CB2E67DD314F30994F5FEB37F9F + 06E52A8BE57C6D0AAEDF3C0E664A415C53A548C0DF2AA11ECF491F9B0980E3F6 + BD94F1CC6FAAB240EE9D4A4F98F58F7007FAC6E702D63F36F9238EFFF0CA5121 + 00ACF5AC0FC8B748FECBA4F417957D19595FFE17D4BD51FA75CAFBB2B7F756FE + FBCB8D1BA82D17005744E9E3522ACF016C90B820000ECA5DC0455CF0555CFCC3 + 022043840651D59085018AC6743CF4E79D3B922D34B2C650DE6224D1ED036053 + 41009C387D3DF02DB590CC00C09502D455302B4A435E53A17E3B0EB85B15681B + 95D66F4A03DFB42CCCB3AA812CD69182DDB088973A4F7F56939B8FAA0F7652F8 + 5AA911F2D017972E00177957B03F2EB93150F76F80BF22F206730E60AFB97B15 + E8C67DFA1ADC54007773EE5C9BABF3992B635E7203F3A0DC1C897275F2F5E672 + F6E601DD480680E331AB6BCEDF664172C3565D6EE6F2C12F4FFDCACD60047563 + B0ABF5A85D48C05F0DE91C397D0DFC5A3B97CFD783DF58E4F145B58BFA2B0056 + 017F03A005F4E640B087BF068301C0C062EBB37173F51AEC355988E7B89F3AF9 + 7E09F91CBB7E81BEF5BB8D50D513D5E93ACCD526EF6F70F51AE08D4B0BF36C00 + 58212F020007017A4D89E397637A79B76FFDDE52EFEB01B03A7981C0B87F01BD + 0327A93B1899E3572170DFDC5C75FE027B830CF45AC86775FF4ADB1CBEF15CDA + E6F805FA26E588E9EE81E1D3DC83B447CE70CD46CD72CDC7CC71CD46CF760F8D + 923E0DF98CE377569E5A027303DC35004C5D9DBE29B59A342FA9B79EB2C0B59D + BEC8B599B6206C96CD96F602CDE14BDEB59601FC2236DFD8786B256A3313B119 + 37DFB59E2163388009031DDCBD1AC60FD83BDB43DEA2007929695B9FD5933EED + 5F9A387F8B543EC433650760707189EBBC74ADEBB26CADEBB0B8C4152D5AE98A + 16AF749DA58D3A14AF72ED970283D7B82ECBD7B96E259BDDB067F6B989FBDE72 + DDD73DE1DA2F7FC4755FBFD5755BF798EBB002F70C9BA839F8DB4DDB9BFCC62A + 9BA606804B36B8EEA21E6B43C8E7B0198B340CF48690CB2FD4D9C4B5908FEAE2 + C1F1236D20B06E083FEAF306F6DFFA8C1BF8E476D78F4DE30078918594B4CD64 + CD3B287D1EE07AC0DB8B0D66C6A46D790B073EEBC357D2676098B97D9FDAE606 + 6DDB29DAE5CBEDDEED0BF0D5FCBFCFCAFC6DF23E7005D327E3C376ED7123F7BC + 207AD18DD8FB82867B1EC826FAB61D5A6A7EC6E75FD27E40F0DF1DEC596A83FF + 6E0AA72F30C15C66C005EA1AE659343680DD1BFB4B83DD9F1D1AAEAE350519CC + 93F2978706949AF7F83B8FB971EFC8F9F6E3FE7DD31D3EB8BCD49CAF4BFFED48 + 571F1656044C9A725CEAA2A927C8457AC411E219E064D0C9E7153D9840E0DF17 + 0026B4F3D69BD7DCAC0FDF7573CFE2F8F5B96211F017084CBFB9817FF0EFFE4D + 38F2EBFD21BF2E20B4E4DA7905D677FBE747FFFEDFABEB77DDE75FB8921B3715 + 02E30006002FBD7AC95DFBD5CFC3CC7BFBA3F0F7BB9F2BFC35F72FE58AEB9734 + AFF0335FDEFCBD41E085E74EBB79F2F7812A0A80F7FCC1671AC619CD3DC3B1EF + 271018306CFDCB3E3CA26EDFACCFD6DDD0D5E30B3487F074F9AC4D3B7944C3AF + F3B9E3B338493E8B94B88A71171F3F5A92798EAFA25F1D1EE8D61FDC21DF573E + 4FB9C25FA98F07EC1ED8AFEE5F7500970180E370D1E939E400A61FD80BE805FE + 8E7CE565FD8EA56EE1A093EF55423FD31F1ECC19F79A8FD4409406BEB7DF7DBB + 34001E4978E81772911C284D16DA3F0F08EFDD1700300F1EE108DE13C0704E83 + 01C13883F7EC530DB49CC152F6DF2ED7BE10BD42434787BE3E727DE41A69EEE0 + 44B4E57A6AD7D2F8DA4B499A058BC8A1D769E9B36B37257D068069334FC7A933 + 07200CE895F5813E24C6C363C1116C30D8D6111A5924C05F8D2AC29A43EADDD6 + C91A65ADAC5190AC5B98C3C36B067D918F6422EB1AA9775EBE41C12FE931A813 + FAD95CC0805ECD051CC0AF2986C184872E5A58E2DA132A3A848606FE5AA868CB + 1B6C00B8CDEC62D79675DDDC65AE680E1098282F8B74CDD776CE22671162D4FD + 2BFDBE2D6B4B515B22C7CC607DC99A73816B27E702101BF4B5F5A9AE51A77947 + 30EBD6E6B29E45805C20AF45B2619D6B2E5FEA6DA7CD7745E41F96F5AE398299 + 4F3F201800DC5C23E9C8D8189F03B8D504D6DC3315083F386AB242DF66A3A6BA + 1644E421428FB49B8EF07DD463F08BCC010C008EDDBFC82030A51D6750D84030 + 223C340200ABFACBFD0D103872FEC610B8C100B97742FD3DF45510ACC057EEBF + FA0C937B9DE1AEB1F435060CD3879B37430D08114D78E8008401C0C0605580C1 + 94260F82FBCBBD9B8CF5F0EEE038DCB3397EEB74ED2FF77D729F2AA539804D69 + 109C006194150ABA631F57B3436F57A3BDDC17170501804500E11A6D3CF03517 + B0C2DD360F9772006BD86885BFCCC9C1E22C005CAD0560D7435D1F0ADA8783AE + 2EF56A2DBBC83DBC8CB13F9006C0CC6FC9FE8077079B5BD8A43018A80BA405C2 + 4670D7DAA634004E424333DFE06C39D2D789E65337F84B499F7F683F27F66E90 + D581C19511F5B0C7A392F1648F87F9BA7FC418650E00C71058CB08E4A6816E5A + 067E0BC900B0C9C3DB50D6B7D2AB52BD96DEB801BCA5AC17897638D64BE6D707 + 06332EF323F91CC1D2AFFB7FEC037A550AAA2CFD956BB37F9803C19469F84B5F + 16008E81AE293D96057EAD5451CFDC0F2DADACBDD4B294750ED51D0060DF6E9C + AF008013E91E7924FA6AC8BCD03688599E2AC9DCBBA57B79EE7BA9984FA0ACF7 + 1FB7EDDF3806BD8594C572BE56191F8B416F0AFAA218FA2AF8ADE2DDBF280B00 + 5732004CBD4A364BAC8462189A1ABF2B8ACFFF4D5416C8BD53714203B71585B7 + 59736FE7F8DF9BF48F2DFA03AE80D2003856E687E31BA0AC2F8D424A7F51A1E4 + 4B29D5FEAA8ABF04AD8C5FB722AF75BBF3FDEFE9EB066FCB04C0A80280B73C95 + 02C0E19CD69F09802D69BF8824FE76F14EA400970B7FBEEEAB91EA93F67D3500 + BDF98AE7DFA72EE26CD9422357CF98277DC982445536002E0B0C1704C001FC16 + 02C095981B806D1AE05A1BD09BEE4B2B1EB73A8AA16F7ABC3C006C219F2935DC + 73100018506B0B79423AAB1B58E611BA3981B02A16F9BECE82DFA030D2104172 + B350C96E0E9AC8B1A274BE1903BF26CE711FE78920AFD50D02E7C670EF026DBB + E4A9FA437233F620F2B98063C86BF55809F0B571DACDB8B1F3A03717EE390580 + 19CB80BA1511E057F3FA6A48E71C00CE1200D8E7F7F5D09750CF08D80BF855B5 + 0B0098FE007C01C03502E805F85A986703C00A7C03C84D0070681BDC8D61AF01 + 6014F791E33709F1DCC5C23E8F700DBA8F74F545847BB69CBE96D7D740705CC7 + ED6B797E13A76F0AFA2AF8B532CCD330CF7D7C386703BC16DA19A84BBB7170FB + 1A20A60E0056E84B5BC61BF41BE7EAF71FAF6E5E75F406A86BF0D742401BE8A5 + 641E40D85CBF56B7B0CF5A0280450F027F47037F674B39C7351D354B840301F7 + EF4C0F7C8300C0CD5100BFE4F445B481BD31F0D536E077320EDF05AED51429A7 + 7A08DC4ADBDE25D15CDD1572FEE9005E42392F50B00B0426ACB387BFE4F95DE8 + 37E7D4ED0BF0CD397CDBCCF1F5A2794B34C473C785CB1397AFC25EE92F92765B + 3601E90B6DE6B69BCBBCA5D25E2E9236FD0B57A8EB17E08BB317C0DB4EEA6D64 + 9C36B017D1DF11F84B1E60F2DDAD7C44C33D775CB9C5B5653373D516D765CD63 + AEC3AA0DAACEAB37B9AE6BB72439800905ADE0D700B094405F365F7BB1391B36 + 5F2D2F1F6DCBF1DB6323CE5FE94301FCA23E9BFD062F00B8BBCCD150905B9E50 + F5DDFAB4B3CD63368AFB3FC1E6B0CFE13B884D67C02FCEA1ADB970CF08F7116D + DB84C615CC46F5D0EDBBDDE067776A1FF370FDB2A9DDF7695CBEB87BC9E58BC8 + 83E89DBE0A81B78BA4AFFF73CF8976A8B38A30D01A1673E75E85BC4308BBB96B + AF86D5C4F53BF285973D00DEF3BC1B2EE5F7DF19536A83FF6EE94F0F0C538800 + 24C0696632F83B91FCBB6FFA9CBEBFCA00BBB70E4C494085E5EF4DCF41E3F6BF + E1C6BEFDBA0A107CEBD094CC795F87FEFCE84885BF138F1E749381BF270FBB49 + 270EF9FA091F3616017D2D44B402E0E03C044695F7F3D7BFF947F7FDBFB97B00 + F6DFFFB7FFAAF97D01BB067BE77DFC4128DF4F4A72D0E2449D7F8EB153EED6AF + EF9E23F677FFFB7FB90FFFD5CF42ABF0CFE55FFCA5BC8FD36EF1A58F3407F0DD + 0C87FCF7FFE37F68EEDF159F5C5700BCFAE62DAD230070F1E50B6EF927E7DDA9 + BFF8E93D85AFE40DDEF1FD2F14FE2EB97C565DBFC0DF65D778FD8BF27FF0915B + 76F5BC7BEB5FFDA9FBCDFFFDBFC3515FFDE747FFFE6F2A14CE19E84BEE5FF200 + 2F92BF09720B97F703009E77E63D85BC1C37FFA3F7B5EDA1F029156DC638F78B + A79E72FF70B4B08BF676C5B99E7B77A7CF23FCEE31F95C1E7193EDA10CF9DCF1 + 99E4C10DC2B5038169536E3ABAAB600EE1DBD5A54373DDDCC36FB849870FB809 + 073CF09D74F080B40FAA264A1DF8CBF75CA110D016265A5DC0190098DCC1CCB3 + 3CC1EAF415D9F728B2F350070EAB5B3802C4F69D0D087EEF9DF9A55E6304E361 + 8ECD35106CF0377105EF15EDD9ABF077B85C8B0CFE6ABF48FB15124BDF1EA252 + 3CEF5314C8B58A8795A80F90EB1F2098EB17A5B982F5A1A6F08094398007E01A + DE0E04967E11F981358D825C7B01C094D6CF359C6BB59526602F0F7B01806D9C + 6BBDBA81A5544770E4FCB575037DD48920A2FD28AC37740D225220BC6EB3EBB1 + 8174139BA4EED34F207D888DF58B0810ECE561306BA2AEAB646C95AC71A8AFD8 + A870571DC101F452EFB0789DAA6371CE1DECFB030006F48A80C0E403C6196C61 + A281C0EDE6AD9475DE7229659D376F99A6EDE8B060A5ACED96F8B5E1DCC5B226 + E4214180702E2A0C10B8CDEC85B29E5CE0DACD218A8C68965F6FB6937979E1A1 + 832CEA8CC261A9E30E661DAB0018103C798ED681BF0A8071F94A9FA53AC13D8C + 33D852A0E00AB630D1EA0C9E405A150F8081BFAD48AD32669A867F06F4E2FE05 + 002B0426554BE8535770047963F01B036004F04DC3620D050D1436301C00B086 + 861E98DF0604130E1AA7F0034062E6180C1E241A28F7400372CE5FEFF01DEEEA + 877AE3BE23DD030346BBC6FD467AD01BC3DFDE3E2474122E1AB7AFF4AB335854 + 07D81B423E1B086ED80B57304078A0963884755CE601840DEE2AE8C519DC4DEE + 4FBBF44B9CC126C6B300F0FDC0DFF6721F1B42406BD8E7F6A20E520F00D89736 + D6DB3B82DB78B81B878156202C3208ECEB1E00E7EEB163004C290A6D0F7FBB89 + 2C5FB01710D81CC1D5021C8E65C098521DC229F8AB0058EA31E4CDAA5BDBFA12 + 004C1F631932D86B4AF71904B67EEAE6F635F84BDD00B0C15F2F991BC3DFA0C4 + F94B5D4ADF4F5F6B85BDC800B0416042415B3EE04210D8DAC0D8B83FAD1CB0F5 + CA03C0A15445E3F7C5E057A1AE9461BE9DB3126EE1CCF9F97B7DBA07687B82A1 + 649FD01403DE18FE66C1E0B462B89BD51FCBFA9379B5A51DED8B5A4ABCB8EF4E + 95BFBF2AAF9F8C495DF772737DC91E724AF9E38DF3551E004EC9F6EF2D476F25 + E943D69F1E8BE7DC89E2F39AB2E6FD3E94E60FE9F717338A0A099E5401A6E499 + 4DF658AC8ACEBB236570B22C001C835F53590058FB0B01E0003F2B554AC15014 + 33BEF254DE31F179BFA9CA02B9A6CA415963A545FEDFDC2FFF8D07B85F49A5FF + 40B355E00F5C9483BFF2C19732F3C3F17B9441DDF2E473FFFA7AD69751FA4BCC + DA155145BEA8B35EA3A2AF5591395EA5E725D037560C695360B62CA58FAB8832 + CF135FB06BC8BCA02C00EC5DBD652B0779A55D5D8E11595F3CC6E2A1722D59C0 + 4432E77025423387B6CDA38C9DC400643B8EB9956BC9E24A6440B73CC5F01719 + 0046B40DFA96A9BA849D91052CD03580D92CD99C34F045D6677362C5C7C7C7A8 + E84B49C33CC702F636C8815F2B0DFC26301897AF9480D94A11EC05C612B21901 + 7C09219D2CFE437866FFF4283715E1068292F92A40B0D5E5C64264B981D34E5F + 83B5D6CEC943E02C10EC55FA981804538F65A01777AF09D84BBF42DFD0C793BD + D5B8812BC3E5ABA1A1433DED0E56E72F397E790AB98D77FCA6DDBF067E736D0F + 7D81BF3EE72F7DC1F50BE445322776FC9AD3977A0C80937680BC3646DD423E1B + E48D616F1A00D7967ADD6EC3E4663E077FD5018CFBB7472E7F6F21005C97FE20 + 423C9BE337560C7E01BEEA1A0EF0B72EF5DE32DED7435F03C03104B6B2F5A8D9 + AED7EC12D77BCE6AD762F80C85BF40E06643A6BA07A404E62AE01D34C9351425 + E19E030036080CE435C06BF5E6A36669193B8111AEDF8746CE50351D3553E16F + 8BB1DEFDFB20EE60E9B770CFAD26CC5191F357C1AF9532F6D018729779006CE0 + 17B598C486962F5B4E9EE79A4BBDD90436C6E6BB36D300C0F3D5F58B8BC287D7 + 9BAD1B6A6CB6B59DE5E12ECEDE5633E74939CFB561434EFB7CBDDD9C45A25C78 + 67535100C0887A0C802DF4737BC6172EF7907881174E92CE6C262E2AD18D43C2 + 407759BACEB55BB8C2B59571402F30987A87E2126DE30006FE765AB646431D76 + D5D0879B5CC7151B5C2B395F87151B5D8FF55B5DA7924DAEBDC2DE475C6751B7 + B59B5DE75500E075B9F0CFD287D84C7D787D0EF412DA990D5A4238F67FEC19D7 + 778B0FE5DC73D3E3EADE491C3B8F3CE67A6E7E5C73FDF9508E4FFABC7F32CF1C + 41B13348DBA23E8F037B83ABF7A9E038020E070175CD91C49886AE2444A5B4D9 + B41EB27DB71B640EE0679E7396D7903A0EA7FE3236081714905785236AA7025F + 0DA9B953EA529253714880BD38A414FA927B71DF0B6ED85E2F0DFD2C1A2AF591 + 2FBEE2F6BFB9A4D406FFDDD27B6FCF57888070FFA23194AFFB1C95E37000BFF9 + 869BF5D64B99C7FFF0E058375AE6E1FE2544EAE9C3D961A2671F7C459DBFC0DF + 75FBB76988D5AC795F87CE1E5FA61017E08BA69E3AA21018F83B35380E150047 + F017F004000644E13C2CEFE7AF7FF39FDDEC0FDF75EFFEF44FD4B9FB557E6EFD + FA976EC9F93391CBD73B7D81AC0B2F9CD112F88B008F4BAF9E53083CEFE3F7DC + FC73EFBBB7FFC58FBE320CFDD97FFA07B7E6FA45F7C28FBE1B7A0AFF0080797D + 00305AFDC905F7ABDF950F20CBFB21EFEFC65BB75CC98D5B6EEB8F7FE29EF9E3 + 3F711BBEFC8E5B79FD537502AFBC7EC32DFFE48A5B711D087BC13DF90737E598 + DF86A3EFCE0FFF8ED77EF5D77A7E64EE5F1CBF4BAF9E57004CC9FF43C9CDAB6E + FB9FFE917BF517FFCAFDE557FCFD79DD637FFAC7FAEF5911008CF317480B085E + 7CE143F7AB8A00E0EF7EEEE6CBFCF9F27744B9E09CAFCF3197B9681E7F77D28F + E6E240FFF0947BE7D49672C34297A53F3F36CABD7D6293869A364D3B75543F8B + FA79943A9F3B0BC13E8D87370EEFD7CFA6F5F3797DEBD823770C823F3B32DD2D + 3BFCA2C2DEC947E47BC1602F0058EAE40BA6DF00F0B8FD6FBBF70F9606C009F8 + 158D97EFC3715900F8E02275158F02EACA77AB7E774A5BE74B1BE939A40DF81D + F1F24BFA5D3CFE6D0F880D00E306E6FBFAFD7716947A0DE02FE3067D510C800D + 020F2104F45EA02EA577FAC6F017E10C46DA8F4378CF3E3768D71E99EF1F5022 + 1C74FF1D5CDB38EE05BDA66968E8F040D330D219008901C2722D45E40DC6050C + E4E5DAAAB057AEB716069A6BB13E64C5F537C8C02F2E5FD44FAEE7E60E66CC5F + FB7D7EFF3EB20650F88B36FB12F84B78684B21C17AC200714F5D833C26EB982D + AEEBDA4DB276D9E41EDE28EB1400B0AE597200581F600BF057C12F3980C909BC + 4AE696C85C11F52ECB3728DC6D2FEBAD8E4BD6CB5A6A83D449A181C8078C0B38 + 1736DA2030A54260D656220B0B6D4EE00EA4F0E0813DD67EF39769BD13293BF4 + 213F9FFEA3E342FF7020915FE8C3118C80C2EDE65297F5279A4D749905B2DE0C + E2E143D6A5010603802D1CB44AEA00E0168964BD1C1CC0805D8D7813A4397F71 + 040385A5AD9038C0E01613293D040606E304265FB0BA80C7F9D0D09A27981CC1 + C0DF51528A0CFEB6A00C4EE034F84DCB40B087C180DF71AEC9D09C1A0D1E2BF7 + 0C5E0D070274476BFD8121E3E5FE02100C041EA5FD0F0C1E27FD8061B92FC2D5 + 3B90FEA00172FF9482C0A8519F11AE49FF51AA467DE5BE2B005E0F7B01C0C334 + 47705DA91B0036508C1BB83650576421A11B91433840601F1E3A00E03007106C + 8E5FEA00E0FBBBF473B5E9EBE6FB62F89B0E0D5DBB93F4931F18E09BB87DE5DE + 3802C0963FB856072FCB0DACA09790CF01021B1036004C38E81800FBFB6BA26C + 79D56CDB43C3491B14C605ECF300FB5CC15EF9003806BEE93E750547F057DDC0 + D403D4B5FE5865016013C7DB7962A52130F5AC7E73011B0C4E2B077CBDE8D363 + 1F946374DFC62B81C0BA271420B0EE0151B2879403BE89F357CA04FE8AC8FD8B + 62F89B06C1B10C14A7E718B88DDBD667F5743B07769B6B69E3362701C7417E6E + 8BA49E0781635388D42B49A92610F60F23B85B16F8CDEA8BE16E3C66FD69D971 + AA14EC2D0D6BBF9A0AC35F931FCBDB47CE14FBCA8DA46CEC455D9417FA3994BA + 571EEDC71B00A68ECADADFF76CA122E2DCFEFCB1FCB98D51344C807245B8C2DD + 52211661FDB1D2734C59733DE02DD06F8A998E855B8E15B19EBCB9F75839F625 + 6D785806F08DFB62E7AFA9C20E60C60B006055DC67F50C9532A546E7283866F5 + 8C63BE312A0D72732A0B0097EECF07C0FFBCE5FFB0BCC21F71A65263F11F75F2 + 21900F3E1F848C0FCAEF53B92FD5B29503C0A5BFA8B2BEC4E2BEF254912FEAF8 + 9CF61A1579AD8ACCC9A9F4BC6F23008E2FFE0900AE1ECAF2140160DAB668D07A + 0D59BC04686B526770E426CE9DCBDAF118A53FCE4AAD970180CDDD8BEE0A000E + 619C33016DA418E8C60038EEA35E15689B94B9E369A36A0DDBA980BB1501C084 + 78CE73FE062900B631FA64810F943517B007C2BEAF6AA3F6AEBAA85A43A93790 + 453FAFAFC057F40020D7CF03E6A29C7BD8BB7DD52DCC98948CD9790D00A334AC + 251C7515B911411E007B085C160036986CE730C7AF9556C7F90BE0ADD5B27B22 + 83C0067F114F0657959BBB3400A66D7D5900589DBF52E2FCBDBFB587BE71CEDF + 1800E76E50AD4DBD8FBA803DF805104B9B7A91DC3477F0B97DB3003065A950CF + 40DD4E83F2743B0098B975BA0C919BFAA1AE1E10B81000C6050CE4EDE6F3FF22 + 20AE867996FE3AA8878C4BBF3A7A03F42D058003F48D2171BD5E52028003F0CD + 02C08D70F98A7ACE5AE5166E7BCDCD7FE615D771E22257B7E72805C02D47CCD4 + 12D89B005F1125397E1F00E88631FA01BF16E219C84BD98C7CBED2AF00987329 + FC9D96405E84FB17F88B9A8F99A5CE60F200B7189B0BF9DC4A0400CECBF71B1C + C01A025ADA790078B287BF1E00CF75CDC9612675EFFC5DA06D754C044785BA7E + 0DFE8A7C786742F3CD73AD66CDD34DB97673C3E61C62B32EC05D7372988A70F7 + 06E0ABF5D067CEE04E8B70F7AED412A7AFC2DE256B35D76F37293B2D2A71EDC9 + 095CBCDAB52517B0CC49DCBE4B562BF8EDB4648D82E0F64B4A5CE715EB75E313 + 084C3EE02E387D576D749D7004E3E85D878BE631D77DCD66D749E674D6F10D9A + D3D742289A93C63B7E73F9F6D89005F8EAC6EEE3B9F08D0A7D031C46EA060600 + 6F91B1CD5BF3A02F1BC13881906E0207E1F4ED25E3BD1F9773CB98E61864C358 + E61B00A6DFC23DF765B3F919A9B3591DCA4161935A5D4BDB77B9A1B877458C01 + 8B29076EC7D9F49CEB1F5CBFC0DF5880E0C16C9C0707152E29DC52EAF4DDF762 + 027FE9533795F48F7CE915B7EA95274B6DF0DF2D3DF7C67A75FC5AF86705C0AF + FB5C94E636033A6C7D676BE6F1DF463D7B6C9B425C9C850A98DE3DA6A501E019 + D256A804F0953996F717306CC0A9BC1F05C0674EB919A74FBAD517CFB91FFCED + DF86918AFFFCE2B7BF717BBFFFA5C3D53BFFDC699577FDFAF0CFF42FBEF4B194 + 39000C205C76EDBC96F3CF7D90842506DE7EF6EB5FBAFFFD7F6E0F46FF97FFFD + BF1420730E5CAE2FFDF87B61A4F0CFD55FFE5CE71B002EBEFC9182D2133FFB13 + 3DDFEDFE003FCFFEE5BF728B2F5E708B2E9C77AB6FDE742FFDF297EE8DBFF95B + B7E5FB3F08E19FAFB8559F0282AFB9959F5E702BE4F75DF929BAE45EFBE31FBB + BFF8CD3F86B3DDD90FEFE1EA2FFFCA3DF6F9A70A7E39AF0160FEBD711D03DE0D + 0413067ACD679FB8CDDFFFC26DF9C1976ED58D2B0ACFEF0448FFE83FFCAD7BF4 + D6B5043657C4CD3BFFACFCDFCBDF4BF1C5B36ED1F933153A86BF35FE5EF87B42 + F3CF7FE0E69D93BFB5B33EBF34257F63FA7715F21DCFFDF83DCD413DF3F409F7 + E87BCFBB13A7D6BA9F1C9FE47E7DB4301006F87E797CA682E3B5EFBFA10E6342 + 4CE3FC05FEF2794449EE6119E3F38AF8EC018081BF94FA79B4CFA77C5E7104AF + 3AFAA23B7A6495E600FEB323234ABD3EB97F7F26FD978FCE77FB4E3CE9169CDC + 2F9FFDA36EE2D1436EC2E1036E9294538E1C76130FEE776309FD8C13F8D00115 + 4018F72F0078C9817DEEC9834FB9270E3C9994E407D6D0CFCC01E84AF9F83B4F + C8F7A768FF9352DFEA96BCB357C747037BA5B4FCC24450B01CC0C05F0B13CD77 + 3050D8FAF83EB6480D7C5F2F7E7DA77BFCCDC7DCA3AF6D516D7E7553122E3A06 + C00681CD111CC36040B06A8F77FF0EDF4BFA0169EFDE93388007131A5AFA35FC + B35CC7F4C1A5E75F748376EFF50058FAD5112CD74503C05CDB00C5765D030E0F + 22DF3D926B29A097EB2AA26D5138EC3A6DD7651EC6B21CFE1AE659A4513D98A3 + 75D23AE0FE0DEB011E0C93F502EB82DE8F3D296B8527743D616B0A5302805967 + 046770B7759B45B29E59BFD1755FC7436B7EED62F017F540AC5BA40402AB13B8 + 44D637ABB72410B8EB4A1F12BA83ACB53AE0F25DBA5ED651EB446B5CD1E2D51E + 0A8B00BE16121A006CF210789542DF1800775C0810967E1EF293F55EA745CB65 + 9D870B380060E9B7F51F25D0B7F5ACF9BA9E6C3F9FB523E1A0E7B9963C64381B + E83BDFB520E508A1A0595BCE295608CC7AD5C0AF85854E807080C284753600AC + 21A145C05E75F9027701BE00E030479DBF41405F646EE096320F008C9A8FE381 + 4B00F074D73208089CB880454060EA1612BA3C18DC74A4DC1728001E9BC05FDA + 566F3C648CDC5FE0EA1D25E518B987182B927EA91BE46D3408382CF73BC0DEFE + 23B48F3AF057FB28FB0D778D22080CCC05FC9AD4F91BD4A0D7B004F6E6A0B01F + A3AE0018608BBB57DDBFC05F197F18D83B48217029002C25F301BDB884D501DC + 354060A9038181BDE6F88D01B0C9E070027A8300C09476ACCDA1FFFEF644C9F2 + CA72019B0CE4EAFD75B8C7BEBF5D38AEA897D403200ED037EF1885C140E11C00 + 8EEB405F83C1C05F157D21043400B856A8DF0D009C86C01505C0564F835F1B8F + C1AFD5F51C1C437F1A0007E5EF1FE55CBF38810D021B00A604E2DE3E00CE29D7 + 9F83BBF1B1BEAF34F835A5C1AECD8B8F673C3D37292300AC10987D3F40300A7D + EC1DC640977A7900D8DA2806BBF15816F0B5B68D55BA8BB0374B9900580D39A6 + DC58DE5E7229B1AF1C03E0860EE05BA93A7BE0D26FB2BDF268AFDDE030F5B2C5 + 7C7F4CF9CA9EEBF941AE6D009832ED3CBE572AC419AC3F968DA5B947D6BC3201 + 70D28E984E16004681F5E4CDBDC7CAB12F69A7B858A2A82F06BFA63B06C06900 + 6A4C2FAE67A810E4D530D2717FACF46BC47DD61FB77F1FCA87B8159701E01C08 + FEFF0A00F67F5439853FE2DB54EE43201F683E08191F946F93FC1755FE97957D + 81C55F6271DFDD5045BFC46D9EBD077B3F157B4FA9DF4515A06F190058059845 + 71DBE6DA58A1632BA05210B89687BD7AA1AE11C671D846177F3F266500B25E06 + 682BA61CBCF5EDCA01DA9A728B8AFC79F1B169953A471900D844A8E84AAADC5C + 5DC405D1CE5BF015942C1A8383B72C19ECBD1DD931551B78F05BBD5191ABF540 + 475703D029EDB2C0AFC25FE92F15FA392CDE4B03E090C737990B0096C53FE037 + A86AC3502ADCF532F8AB6181B4EDC51CE02FE751002C8A0170DE5CF99D4A01E0 + 870C00E7DCBF8500B087C3DCB478F81B83DF9AC05E9182DF20ED2B0700AB2358 + 6EE2546500607200A7C701BF1AF6B9B5DC780280E50614006C1038077B730038 + 0781A9E7C23E6B1EE0B6FD3CFC6D2F37CE9D062510B8A200D8CABA9DE5463F80 + 5DE02FE1A30DFC9A4BB86E9721F90EE04E3CE9ED1DC0F570F6768D04008EC06F + 0C802DDFAFE5FA8DC1AFD718D7B0F7382DEB3DECC1AF4260299997406205BF08 + F04B0868C23D23DAE3B5DDB8FFC4240C749BD173DCE0E24D6EE8D22DAEED98B9 + DAC7D84383A7B826832629EC35B72FA501E058387E15EE0E9BA64A9CBEC10DAC + 21A087FAD0CF067E01BD0807B0858046B49B8EF6EEDF24E7AFA81400A62FE402 + 36F0AB21A027CDF70EDF290BD5F14BA8E75653A47FF23C05C3ADA72F7245B397 + B8B63317259B670680291500CF20B7AF07C06CC415CDC3DDEB43F851A69DBF59 + 10D89CC00682A9775ABCD27501DA16AFD24DC0F62272FD762E5EEDBA2E59E3BA + 48D961D14A57B470853A7EC9F1DB65F91AD703588BCB77C53A55473947110079 + 69897B78ED23AE37D076ED266D775EB146FA36B9CE2BD7B9F6CBD6E80629E19B + D92C0500DB662932F86B1BA90682A90389BBAFDBAC9BB3805F3663D988654336 + BD516BB97E9101E038F4231BC1039FDAAE9BC2360650F600D8835EC02F9BCBE6 + F6B53675DD787E52C69E218FEF4E1FB632D4073FB75BA12F25EE5F4A05BF8CED + DCED06EED8A1F017A5C1AF494367B2291E2070E2F695B6D501C1DA6643FD0536 + EB5F76FFF5D0BD71CC16BFB14BC33C4F78F34D0D498A0C3AE03CA30E74C0B596 + 75FCB751E61C9C014C0AB009F89B84803EE55DBEB3647CCE7B27DC5CD16C11F0 + E9B600F0E977DD8C0F4EB919EFE39E3CE3D65DB9E22EFCE55FB95FFCA6308CFB + E53FFDCEDDFAD5BF715B6F5C77733E02B6051817E06F0C80E9C3691A03609CB7 + 80C818006B986211632BAF9C5327E9CFFEF11F146A66FD00697FFC77FFCEEDFF + 939FE8B93CFCF55A77E3B27BE147DF117DE95EFAF177DD4B3FF9817BF90F7F20 + E5F7DC8BD2468F7D7E3D794DDE07255014E1903DF8D37FE1FEE83FFE9DBC4EE1 + D0C8FFF7FFF37FDC9FFDE33FBA937FF633F7C8679FB995D73F71CBAF5D75C597 + 2EBA559F7EE29EFBD33F753B7EF6676ECDADCF341FF0CAEBD7DD2AD1CAEB3880 + 01B240DACB6EF5ADAB6ECD67D75CC9CD2B6EC3AD6B72BE9FBA3FFC87FFE0FEF5 + 7FFD5D78A5EC1F5E1F58FBE5DFFE5AFE1D7E2CBF032E5FFF7B78B87C494BE02F + F555F2EF421D000CFC5DFBD9270A80790F1C433FE2F7DFF983EFB82FFEE6D71A + 46BAD0CFAFE4B5CFFEFC2FDCE35F7C1AFD1BFAFFD767BEB8E1F6FDE04BF7FCF7 + E5FFE047F27FF0A3EFB9177EF0076EDFF7BE4CB4F09CFCBF5F3AEB96C96B2EBE + 806BB87C00BC4FDE178E7283C0FCFDD8DF1532F73975C6ECEF0E003CE383E30A + 8115148B7006933B18806B30D77209A3743D86BFB87F9101609DC779E4B38713 + 1FC8EBC1AF87C138F529352CF4F1230A820913ADA1A28F1D56E11046C0E2A9D2 + 1E7FF8807CDE79F883D73D29AF775CDB138E1EF200F8E86105BEC0DF0907F7E7 + 00B0D4E9030013227AF2C183494868CB0D3C31942603BCCC41EA323EE48FA34E + C9F8A8D75F4BBE732D04B48588D6680C00E30082C745101811C161D4CB2F27CE + 606B037A15FA86923E9B970E0DAD21A10300C6F90BFC25628585811EBC9B6815 + B8865F48F2000FDDE7EB03C80DCCBCE75FD2EB16F0577302CB35CE1E681ABADB + 5FF78651CA759407A9E230D0FA805588B2C1B5DAC2412B00966B35113B3C0096 + BE2778D8CB835FE443443FE35DC1E1DADF9B54105CF3A58D23D800B03D4CC6FA + 22166B0E844358C33FAF635D226B1785C03900ACEB17D63352027E29B51EC02F + 1058C340AFDAE8BAACD8A839813B2D0300AF0B3078AD6B5FEC1DC01D167BD76F + 0C8001BFD42D37706799DA32D6970000FFF449444154D7558EED2275EF06F600 + 98D41EA4F8B0482FED17F8D41F9AF68375201098F59DAC2301BD80DF0E0B0813 + BDC8B59C21EB5111FD80E1D6449E91B5A5AD217102F3D0226B564030B23A6B57 + 44541BD4728A87BE847CD6B0CF80E090F6847AEC025667F07859978F9D960783 + 63006C79809B8E9EAA65AB713355C06020B0C15E73031B0036D01B3B7ED300D8 + 3B7EC7241018006CA25F3568B4DC47C878501A002303BF69E99CFE2335DCB301 + 5F00AF77020F97FB2A0F7CAD8F1CC1F501C051F86703C2B87B09030D00260CB4 + 01E0063D3D00B610D0F4650160EAD6266730109812005CB78B7700C7CA82C069 + 017CE33AAAD9B1B7ABD1A1571E004609008E04C0C5D55BA3150E5F7FBF7D7F3B + 1EAA46D473E098791C63E03877AC87BD2603C0E6F84D4BC7980BB06DC13E8297 + C1DE34DC45715F21006C5017593B0D750DF6A695058C0DF89A7888DF4AAB238D + EE560000DBFE91EE15D5CFE5FD4DBB804D55A41D03E0B2C06F5932A04B3D7D8E + 78CCEA280BE85A3D9E6F6D9B9F3737EC01DA7EA0B68313D8DA9555EC257A386B + 90171908AE5C8731AF783C86BB65F5C7ED04FE2A00CEC1DA7BA17CF89B93EECB + 46F057FBF2806F961A476AE8EEAB6E02FC7AF86BAAC41E3B7BEDD657A13D7B39 + 26B3DFC478E139392EE0616A951A1E06FBB274E8E9DF876206117308DE57FCDE + 0ACD2B6F2CF76F5458592CE75E2BC7BEA40D0FCB02C04185DCBF7118E852C7DD + 03001C2B86C100E0B8DFCE93E417662CEBB5D263BF2FC550F74EF4FF03E03B17 + 1F80CA55E503AF1FDED21F946F93FC9749EE0B29FF8BC8ABACB13B5545BEC4E3 + 2F547B0FB1D2F34B2B7F8EBF90C9852FAD34A0CD9A5348D17195AACB05366AC7 + CA83BDA936D2A4FE7601AF5111005C01E1FE8DDA31ACB53E6B9BD2703756A139 + F1F13A9E82BA568F0500B67CC1DA8EE0AF2959ECE5015FBFE8AB5C4F168C2A0F + 78B364EE601403E0AA00DA502F0486E9B73173FDD668DCDED57EA8B3ABF980DC + 0004009C057E4D31008EC12FB2857C29004C481F9D238B7D59FC576BD2C1BB7F + A56D2058217023B94910556ED8DE0BB7AF4A6E18787F721CE70700C7B057C783 + 531759DB00B0BA794555F42687B273047A73F5580060C216716C0C7DB3E0AF8D + 03780BC9F2001B008EE12EEE5E80AFB97C63184C9FC15F0DFF1C003037A0B103 + D8C06FEEA6B44F0480A9CB4D6F3BB90136085C9473FC02731345A01729F8A50E + D005DE863EE6190036B06B0E6075F84A5F0C80C9F75BB70BB07890BB5F54A75B + 08019D02C075BB4A5FF71CF8A504FE52E60160913A7923000CFC6DD87BBCD4C7 + 06003C56FB1AF4F26DCA467D70F54E50D06B6AD817B7EFC4A89CE89A0C98ECDD + BD032729ECC5154CBDD9D0690A7EB54FA4619B71F8AA83D70B006CCE5F640058 + DDC1B423086C7D388309078D23D800B041604A83C0A6E6A3676702E018F622F2 + FB5ADE5FC65A4D9CA7F97EDB4C5BA8A19E35E7AFD4DBCE58EC5A49BDE5D4F9AE + FBD2756EF8E667DC80B55B14F6E2A288F3AC190CD63CC0E4FF0D9B7406818BE6 + E1FAF5CE5F03BF456CF8CDF7397DA9D3C7C69FDF005C96A823EEDFE2552AC654 + 384200C2A24E4BA4BFD8835F206FF755EB5DBF8D8FBADEB85F5692F7778DEBBC + 7CB5EBB8ACC4755CBACA7559B9D6F5DEB0C5F507BA6E7854DB0A80D76C700FAF + DDE0BA9688E41CDDC88727A2CEA6291BA6C060EAD6EEC21C5C350060FAD63CA2 + 1BAFB631CB066C17E9C38963CE9C24FFAFBCBEC15D83C10680710523DB10A6DF + 3680CDE96B8A433E038063F86B00980D6A84C3B74FE803FA027C6340CC9C21BA + C9BD2B81BE1AEA19C7544A6CAE0FDDC3E6BA07BE06830D000FDBF382CF01FCC2 + 4BBAB98EC38AF2FA5B133361E65711619827BEFD969BBC7FBF968422252C2900 + C204780044FCF8F0F8CC737CDBF49323131422CD3C71D4CD7CEFB8026073FF9A + A6497BD6A9E30A7DE77E70CACD7DFFA482E0D9A7BC3318385CDECF2FFEE9376E + F66942E4BEEFE67E74DACD3EF3A1D4CFB8391F9E75F3CF5E700B3EBEE88A2F5E + 73C7FEFA57EEC27FFC8DDBF1B3BF724BAFDC70B34E5F74F3CE8A3E3EA3801779 + E8E6C1AF07C21F6ADDE7FBFD48844398715CC13EF72EB01690E7434203207340 + 1610CC98CD0348BEFA473F748F7E763D0F183366E011B8B9F4AA07A0C59739EE + B48C9D8DC6009C7EAE1D1FD79903FCB41CB9C053CAE5D72EC96BFFA1EAE59FFC + D8BDF4E31FBBCD5F7CE1367CF9855BF7F9670E57EF8A4F3ED172CDAD5B5AAEFA + F4BA8C7FA93980577E7243FA3F776B3FFB5CF3002F93F3ADB87ECEADBC7E41E6 + 5D762537AEB8D537AF4A7955C62FCABFB1BC5FF91DFC98EFDBFAC50DF7E28FBE + ABDA70EBB28C5D90B18B6E9568D935DE3FFF8638AB3F96737BE06B009812B80C + 00B6DF6995BC26F077F5AD6BD277498EF500180735BF3F50D8FE2DE8E3FF0890 + FED28FBF1FE6F06FE5213625FF0FFC5B22FECFF83FD6FFBF0B67E46F48FE2F2E + 9D51D88BC8F7BBE882FC0DC8DF04FD3A26FF5FCF7DF7967BE187DF71CF038E45 + FBBEFF85DBFBBDCFA5FCDC3DAFFAC2AD92F3735EC46B20FEAE4CB40D041B0406 + F6027E01C0806005C0673D00F6B983653C405CEA067EAD648C3AB0B72C318FCF + E9C423FBDD94E387DC0CF90C4E97CFE2E46307B56F32B057C679784341311058 + 84CB7F4AA873DC64F9EC4F92B9B87D01BE5381C8F25937004C3F9A74C40B106C + E19F35047400C1AA102E9A3CC1805D4A14C35F83BB068001BE538FC8FB1151B7 + 63630730DFB9B88827733E1953C72FDFCDAFBE92E40AE6819DB1910BD880AF85 + F3A79EC0DF975FF27984C3988DDB1C03C116161AD84BC867AE615CCB0C000FDA + 85CB77AFCC7B29B92ED9350A3730751E58A2247AC5905DDEF54BAA03A25D0086 + 87854817F46B18E8673C008E65D764441D00CCB559DB22AEED3CE435806B72B8 + D60F628E8C2920963E4DF910EA8C2B187E54FA44E6028EE1AF858546FEA133D6 + 1C9B5CCF8D5BA4CC3DBC86923A40B864630282C9FB0BF0459D09FFBC6C4368CB + 1A48EA3880293B2F070A4B5B01B077FBE20236D1EE22E3DDE4B8CECC5FB2CE75 + 5DB6DE75218730731797B84E3CBC170030A5D5DB0600DC26AC153B92CA43D687 + B6A6F4B98165CD19C0AFAD37DBCFF70F0EB2B6644D0910B670D06D66CAFA36AC + 5963009CF401817103A3007EDB4C9BEFDA4E97F1280730EE60CB01DC74DC7405 + BFD4759C3A1AE7D57CAC8C8F9996078009054D6EE087460273030096BE6640E0 + D153A55FC664FCC1119312313786C06900EC9DBF63550680C9EBDB44CA340446 + E60036A541700C802DEC73E3BE235C932020B0C1DF86B884FB8DD05CC140E018 + 00EB1CA9D70F2EE03AA2380CB4C15D73041BE455C0DBBDB463D8A03025AADF7D + B0230F700C7AD3E0D700AFD56DCC4A85BF2124B486852E0A797E450A81A9036F + 03FC35886B397DD3F7DC0A7D81BCCC6925F7EDC05EA99706C01EF89ACCF59B06 + C0E60436D592D745850030F97D9141DF04F686793647FB02BC8DC1AFC9402D2A + 0F00C7C76701E058EA04967E9FDEAB300446DE2CD04A21AF41DF4C00DCB8ADAB + DEA45DB910B83C306C503756A13183B856BF2F05804DF15896742CB50798DE17 + B476A5BAB882CB03C00F8632372706BBD6A604EEA6FB4D31008EF768BF2E917E + 4F0D401576FF9A1A7B85F0CFEA0236556F206AE43424B4A81275510C80D39033 + ADF2C6731033BFDF8E4B8EAF4E5F6EBE825F798F85CE5DA8FF6E2B8B43A08ACC + B5FEF8F734C5C7E5FE8D0A2B8BE5DC6B9502C00594057FD1ED01E032E02F32AE + 17D7CB90C1DFBC7346FD765EE06F0287439FCD2DD5F77B551EC4FD6AD213DA2F + F8CF56FC2767FCD1DD8170FF1A00AE54E04312F77D93655F48F95F40F197906F + 17FA92BB5315FEF22BFD05495FFACBB462EF277F8EBF9071410BF03603E2AAA4 + AF52D5D49C2C45C7007F356433650482AD9D06BE269D2325003801BD35E458CE + 25F57C00CC39E59808EA96A9140046956BC942A65661F7B01FCF2901B5A97E04 + FCCD1B97322BAC33B2762C03C055641167F3AACA424EE16F58DC6589C51EF0B7 + 4A7D596C4A19435F64003706BF5581B2DA1FCB435EEA5550EA38AD8B1402CBF1 + 38806B36E9903880B3A06F2C9D03E46D581A00AB6397B1061E00E7DD0484053F + 73AA027EE53C7900B8B101E08EAE72C30EAE52035F5735F62016704CCE1805C0 + 525705001C3B7EA95B186893F611FA596F5A3ACB3C0F7AAB3F243760A21A4DE5 + 664B443D0782E52647E62283BEB11BD8FA6B35979B332981BC2603BFB1FB5741 + B0DE448A5200B8BA2806C0D66FD0D700B0C2E0E806D49CBE84A402FAD62E02F4 + E600B0AFCB4DAF0160511D2030A01668DBDEBB7DA9AB1338405EEAEAEE0D6097 + 7E05BD9DFC3800D8FABC424EE0007CBDEB17102C37EE5DE546BEDB5057B7DB10 + 571B082CA25EAF3BC03782BF84810E00380EF76C00D860B00160E02F2E607302 + 37E8353E0F0037E8E58130A2DEA88F07BB0F0C04E04ECA03C0F437EE0FEC9DA8 + 257300BD06812D27F0030326BA87A4EF41FAA404F80279CDF96BF0D79CBF405E + ED973EC23CD306F2B61C3347A12FFDD60700D610D1526F86DB979CBF5226D037 + D4295B8E9DEB5A8C9BE3017080C069000CFC6D3B75A16B336541E404C6EDB0C0 + B59DBE4837BA5A4F9DAFEDD63316B996D364DED479AED78AF56EE233BBDD882D + 4FB9F6738B5DABE9F35C0772B2CD5E2C73E63B0BB907DCD5CDB6D9728EB03147 + DD6FCE9576FB12D619F9CD3ADC1E7E0310C51018E80B08C60DAC4E60A9172D5A + AE6ABF7885EBB86495EB849377D96AD76BED263710778C94F41515AF70ED97AC + 749D96AF56D8DB63CD4605C07DC9C1BB0E77CB3A55CF758FB85EEB37691F0EE1 + 6E25EB45EBB46EC01711F21951EFBC7AA3025E365B6D03D6365EFB3DE6376AAD + DF9CBF9AFB57EA067C01BDDDE4FDE002F61BBC21DFAF8C5357002CA53A7F8393 + 084751777205CBF11672924D65FAFB3DB1CDE7FC7D7ABBCFFBAB397D772561A0 + 01C0C81CBF94C376ED73A3F6BEA8E5A0E764EE0E392671FA7AE7545A1E00E3FC + F5EE5F36C387EF7B4943679AF3979099439FC795E537DAD95C7FE695B59940F3 + ABE84707C7258E3280033297199021769CFDFDE15E99E7F8B6E9EDE39BBCFB17 + 00FCAE874BE6389CF9BE6F039666487B968CCF51F87BD2CD0E8EE0DB01C033DF + 3FE5669D2687EA6937FDFDF7DCF4F73E70B33E38EB165FB8E2169DBFE2E67D74 + C16DFCF2076ECF5FFCCA3DF9873F73C5976FB8F9E764EC22E17E2F2AF05B7801 + 784C0E569C95EF49FB8C0243CAC59708B3EC432D03ED0C1403787D2862E473D2 + 021629018A06813D5034287945CBC597CF06E8F871F25AB45724C096F3C979AF + C8F1110046067AE933F0EB21B33F07C7DB7BE07C40D3D537AF893E712BAF5F95 + 735F76CBAE5C96FA75B7F97BDF758FFDF0076ECDAD9B1ADE79D91560ED75B7F6 + D62D85BFF4AFFAF486B43F77EB3EFFD2AD559730B038005C20ADBCDE4A7EB74F + 2EB955D72F4BDF15695F72CBE5DF84F6EA1B575D09FDA29538863F21BCF42539 + 37EFEB9242E01C003E2BEFFF9C4261002E7096DF01F88BFCBF8FBCEECDCB6EED + E79F447DC0EE4B6EA9FEBB018F81C58CC97B923A25FFD6FC3FF26FED9DC23134 + F7FF9E560283FDFFAFCC913A1078F1C5D36ED9D58FDD0A5E43CEB5E432D097FE + 0FDCC2F3EFEBFFD572F93D288B2F7D18FADF730BCEBD2BE529B7E802A507BA28 + 0BFCF2F7650F0EC475FEE62C043475E61316DA00F0DC33EF25203806C0D4D5D9 + 1B649037EEA36DCE7C73114F3979C44D3E714861F05460AF3986A59C1EC6758E + 1C332938FA2787073BE89B7874BF9B845BF8D47185BF938F1DF6C03794AA5067 + 6CEA31791D03C0870F2AF89D24A20D18667CDA519973E4B096E6EC350179271D + F02E5F9CC000DF2987E55811E097FCE963DE7CDD8D7BE74D37E19D7792EF5DC0 + AFE501A60F017F0D0013B561FC1BF2FDFCFAEB5A6AE8FE0083C7BCF2AA025D60 + 2EE0D7003279DD47CB9C51C1016CD0D7C2435B4E60FA14006B548B9D3E07B08C + 0FDC2DD7B6DD7BE438E0F1AB7A7D42A429B06B9541E1E1521F012836D80B0496 + 12B730ED7EDB77B8BE725DE5DACA7517D0DB5FAED9C05E1ECC1AF8F48EA46E00 + 986BF840A90FE6DA4C5F00BE5CEB074B1BD86BD0973183BFBA16907E5CC0AC2D + 883262EB0A7305C750D803E0475D8FF59B150093AA82B50BEB19DCBF5692DA02 + 0182D5095C22FD387F23104CBDEBCA4DAEF37259F72CA7CD1CC243D3B741D65A + EB5CA7E2750A8371FB0280BD0378BD42E00E84810E6DC0709185834E01602B59 + F751B695B63D340800EEB08075E2225D57224241FB3E03C3FEC1428E69337B51 + 588FCADA56D6AA6D672C54D0DB52D6AF1E009B7250584343CB9AB79588F5701B + D6C493831B38120E604A05BF2203C00A83C72159D38FF7B98091F68D9DAE02F6 + 2AE005004BA9003802C526E6201CC40FE1221671CC83C3278A26E4B97E63006C + 4058017110F09730D026002FA2FF4139BEF1100F850D046B58E87EA2BE233C04 + EE2773A57CA0FF281575EFFC1DE67305879CC10680E310D0EA040E7DA80E1017 + 80FBB077F51AE06DC43CA91BE035388C0C161B0036D5EB0E04F602045B496EE0 + 3A848A0E10D840AF81E05831FCD5D0D045721F1D0070028145C05BCA3AEDE51E + 5AEA405D0B0DCDBDB781E01AAD89C49503BF068201BF069029BD2C1434503807 + 7963A501704D510C800D02C78017C55058616F1E000E7D4D3DB88D01AEC15D83 + C094D66F7D261BB37E0BF31C4360EA797B3F8C45F3B45D0000FBBDA4B6526F2B + 7330151061CE87848EC3425769DCC6556FE2E16F7900384BF19C34E82D2403B8 + E93EABDF17416064F3E3E32875CC606F70FDC67B80365631009C03B869253037 + D54EF7233B6F322FECD15666BF364FF47919A4BD5BF230977AEEDCF9A0B7901A + 7B017FAB4702000316D96FAFD158958CD9DEB9D4D37BF305A5FBEC723E556E0F + BE72543779576F6EDF5F433D5797F72063F755AAE3C1A09C8F3E9B933E47A1FE + BBA52CFE100BD650B97A2E6F71D631D66FEF35968DE971C9BF1BC765319B5C7F + 5CBFD7AA28004E83DF42CA0A01ED05AFF3397A95DD95829F11AF8CEB6548CF95 + 3A5F027FE3B976FE2C9537FEB5AA9207C0770302EB09E37F04157D0584633856 + A963BFA98AFED800C2068593A70EA27151FC07EF251FE23C491F73E5BC80E0D2 + F34B7F88EEB9A218F115917D21255F3EC10DECE1A929F7E574B754E8CB2F3D66 + E3E92F53FF1E73FDF1F185A4E7E20B5ACF99212E3A08F85B850B5880BC69E85B + 00FE728155084CDDE0AEB5B3C49C1A720EA9E701E0306E17F6DC055EE6EA31D2 + E6E2AF1017C89B53A51A720E73E7C6F520E0AFAFCBA2A946CE095C0A0093D357 + 54A5962C9E44BE4F8E57F9399CA7CAFD48EAB423009C80E01400AE84AC4D59A7 + A58CCB82AFB6944821AE2CFC1402FB322DC671FE56AE0FA065BED401B6005C11 + C0D660AED551A5FA3297F948E131F3398FEFD3E381BEBC07CE479D3E8E07E806 + 51B7BE58C9EB4B1DF8AB92457A15202C73A44C2B81C111FCF50B7EB9316824ED + 204030B980B5AD3700D471F9CABC86BE8EAA36EEE0AA07B8AB40977901ECC60E + E018005B198F5779801B14C672A19F0D009BAA05557D48C61EF4D0D7545D6E9E + 80C794A61ACDBAB89A805E03BC01001BF82514F4FDADE44630848536005C1398 + 0BDC25AC335037C0DE1A3AA73400BEBF756FB969941B5651FC04329017E85BAB + A88FAB09F4D552E6E2FE95B21640383876D5DD0BEC95D2802FA50160E0ADB978 + 81BA16BAD9C0AE82DE4EA28E387E81C5805FE678D5ED82CB77A8ABD77598B487 + CA311C2B63F4ABDB57FABB0E5569F86703C0C0DD6E4111E835C72F65BA9FB24E + 80BF0680CDF59B73001312DABB8051A3BE1334B4B30FEF9C13C017001CAB717F + 0F7E9B0C98E8C33C0370A58EEBB761FFF1EEC1C1535CD3E1D3F31CBF067D5538 + 7CA5AF89D42DFF2FE19D01BC06802DFFAFF5532285C10A79E7B81680E0480900 + 1E37D7B59C30D7351F2F6D9CC0C05FE0EE6436B016B85693E6E7016083C2847E + 56E02B6A33CD00F042D77A7AB16B356D916B3A699E6B3BABD8F559B9C9F55CB6 + 41EA8BD549D161DE32D77EEE52DD3C6B3B7B9187B8F396B8766CD6017F710193 + FB770E70D887E44B0360CDE9BB80FC6F68B9F4FB4D3FD461610C80BD13B8E362 + 0F813B4909F835000CE83500FCF0EA8DEAFEED518243658DEBB07495C25F20AF + 87BAEB5DF7D51B127559B956C70801DD63CD235AFAFA46F7304098308A21C433 + 1BA6BD363E9E8487EE2AF370F7F6D8B0D93DBCD1875BA4CDA6EC80ADCFBAFE8F + 3F936CD222DD9CDDBCD5F5DC921FF2D942401B003669AE4011A0B7A78C2BE095 + 39B47BC8318C017FD9586653D96B9BEB0BFC7D5ACEF58C94DB76B8FECFEE50D0 + EB41B077FD0EC6C9B4638F86B01CB9F74537E6C557DDB0BDCFBB810A8077BA41 + 84C2DCB53B01C1F9CE5F2F03C14060DCC0C3F7E1FA7DC98D7CF165374CEA6CB2 + E3B4CA6DCCB349FFE25D0F03FDFEFE0509EC35E060D0973A409872C9DBBB338F + FF366AC9BB0714E0028167BDE7819381A7391F7A38E5A1D29100818FC93CF28F + 027F81C6C7DC6C6997F74308E8E99CF7F4BB6EEE471FB8190A80DF77B34F9F75 + 0BCF5D740B44B881175DB8E2565EBFE5965DBDE98A2F5D774BAF7DEA965C2684 + B2078A404360DBFC731F2ADC35286B40370781CF3ACB15EC4122F010B7ED2585 + 87C5328EFB7505E7040E8BE8E738EF3AF58ED4651C27733C9404447A68597293 + DCBA004BE02720F863B7FCEAB9F01E7D69C092BA0950690ED6B81F880A240502 + 2FBFC6EF4A18E5ABD22FBA7ED5ADFBE2A6DBF43D72E75E5330BC42433B5FD1FE + CDDFFB8E5B7D8B70CFD71404AFBE79C3957CEA21326EDFF5845F9672D5F50081 + 45067C19C7FD0B9C5D09F815D15EA5AEDE0B0A7FD77D2EBFEF0DC02DA0DAFF4E + CBAE017FBDDBD700B0B97F29FDEF7D5EDAE4FEC5F97B517E77FF3BFB71FFEFC3 + EF4B9B92631173FCBFB7776B1BE88D2130A23F96C2E00B38B9CFB81532CEEF09 + 00A60DB42EBEFC915B0CE8BF725601318E6DC678A820D179FEA64452CE0FC017 + 77AF0F31CE9807BE067D4DB401C3733E7A571F50305730259A73C6C35F04F435 + 19DCA53F06C3F6398C41307540AF858306F02AEC259C73F82CEA6753E6E3E637 + F72F4EE0A9270EAB70FD4E214C34A5883E1D3F89F3FF98BA7E09073DEED07E2D + 01C1068309053DF1C841475EE089870FAA239836FD938FE22E96EF89E3F2DE8E + 1F5319003627B0857A9E24E79E44E8E8FD6F49FF3BB9F63B6FB9516FBCEAC6BE + 0D0096EF61199FB43F077EF91E06DEF2FD6C6D402EE504FADE2214B4CC01FCAA + 5E736300C0AF79C83BEAA597B5CE397016E3201E2D620C00CCB5C500B0016300 + 30ED512F026F7DD867DAB88699C37508E00B000604A3D1AFBC9640E0211C23D7 + 3700F0C8E7FD834D848126F7EFC09D7BDCA05D7BD40DDC4FAEAB7DE43ADBF759 + B9065302809387AF9ED3D0D003B9F65206086C407810ED0075115078C836B9FE + 3EF59CEBFDD8D32A1EE8EA2B6B89BE5BE5BA2FED9E5B640D21625DD1EF31591F + 3C2A6B08A99B2B987ABE3318002C6B1ADCBFC0DFC8016C02FEB2BED12826AB1F + 71DD56CBDCD55BBCD65097B155D2BF4AE6953CE2BA97C85A48CA6EAB36B8AE2B + 37BACE2B7C6868D45904F0ED0CE8D5F0D0E408F6C017006C60B868A1D417910B + 58D672C91A6FA5B6913DF0C7C37FBAAED4752379812917E9C385160ABAE342EF + 0CA68FB0D031FCE5E14485C021624D6B406F00BCD4DBCCF41098871A5B4E9335 + F1B439529FEBDAC8585B590FB35ED6A839A442C1151C9CBF887A5B1E809452F3 + 048F9BAE397F3DF8F56AAD0AE1A0C7CADA7ECC54D7148D0E92BE440A7EA7BAE6 + D447037EBDE8333D387252027D63086C003801C152A64130E017D84B1FEE6075 + F932C6FC41635DA341635CA381635C8301723FD54FEEAB80BCA2469170023F30 + 60B442610070FD08009B08075D2FC0DF040007304C9B1237304A00B0F401800D + F4526F8CBB98738479B133D84030E1A0EB761BE81AF418EC1A741F9C00600F81 + 4B87810600C7B9802DEFAF416005C0EDBD6A8750D0D4EF0F1018F85BB703E993 + A4BF6DCF0410C70F59D7040EB795FBF9040EFBBA02639139810D021B18CEC160 + AF1800E7A955575743CA180023CB091CC3E004F83603F2863EF62DA45DAD5947 + 19431D54D5A56EF0379602DE07A94B19406F0C7EADCFE06E1ED80D10B8B2D463 + 009C968E9702BFED1277B0C2DF266D658E4F2B56A9416B5515A9B3FF54AD515B + 110E60BF1F5595FEA02A0DD8AFF2D2BE461503C0B70B8451D6FC04F246F017C5 + 63067F09F31C03DF2A9CA31E7B84BE7D5F1D007133574964905641ADA812B036 + 520C744DC05C3B2686BED66FEDE4BC364E59BBB4F41C419A27D84071394AEFFB + 662BEC2187323D6E7BC836A675DD6B6E1CD4C8AB7AA46ABE8C01B0E607AED650 + E5F7C7E9F34AEFD3C7AA94388A45E1783D470D7F2CAF55B99ACFE95BA5262628 + FA1BB8CA35EBBBCA35EA2B4FA97E7F63D7E8A1D6AE71D32257AB6E7357853D7A + F6FA393FE78A545106505125BF4768C7E7CFD563166100D8D7B3E421774EBE3F + 7DCEFA39865306C789CF9B357E2F9430AD14273325DC4BEA59C037561EFCD5DF + 3530B84AA15E250300A7EBD6B67AACF4BCA80F876FE56A303B7F7E05C194A15E + EA586B7FA354E9CE00F07D95734ADA55EE17C94913D12E439553CA3BD614FDC3 + 7FA3C47F7AF8C3B27606004EFE9813C9872DA5BCF999C778657D98EE892AF0C5 + 91ADEC2F91F84B26FEA24ACBC66E47E92FD7ACF3C56DFB428E9535C78ECD52FA + D842E369009C76EC96EA8F2EB6B7A3F4853A567C312F2D1BB7C5026D93B415F0 + 660BE7AFC25F04B40508E308668C39A5DA1EE4C67D36EF3E162BB6B8D1058F48 + E6DEC7390C008712F0AB0B39B990EBD37A805CC02FF016D596859F889CBEF701 + 60150C4B19FA4D95A50F55A92B0B5091D5ABD59745AEA86A7D59B4D6F3D271CE + A5F20B4BBFB8F40BCCCAB2488E17A2DA96E381BF80DF18EA2AD80DB239C9DCA0 + 645E0C804D0DF2DBD51A16F93A6097853D3700BAB86791CFA25E6E0E22251057 + C6737372F3D4351CC0B1B61F889CBDF449DB94F407710303B44D43603F976372 + 0098D2F201037EAB35F500B886940A7603D4ADAE2E5F6EBABAB96A48EA88BE9A + 2D00BC72432765BEE406B0D5C32AC601BCA8564BFA7ABADAC05D299142E0F0E4 + B087C072432865CD36DC40869BCDC8F5EBC34FC9F1457D5DCDF67D5D8D767253 + 2B37A5A8BA8C6B1D380CFCC5B50BDC957ACDF63EF473E2FA0DFDE930CE09F44D + 3444E60F75753A86B273BED4C11B647DB5E598DA8C751B2EF2619DF3D47D78E2 + F4F5C2ED9B73FC5A3FC0B73E7D52AF17815F0BFFEC0130256DDCBF3E0770121E + BACF5875F0D6EF2DFD9A03789C82E0260380BC387F71014F740F0C9CACF586B4 + 01C083646CD04475EF361A385181AF95067BA9A3874258675CBDA8A1CCB33A60 + B7C5E8D909F43519FC35006C80384FA366FA90CF00E1A056E3E7B99613E6B966 + E3E7B8E6E370FF4ADFA405AECDD445A2C5AECD9445BA91D51AE03B6581AA0D30 + 58433F2F5410EC6130E19F97BA363356B8565397BB169397B836D397B9A2592B + 5DDB99CB45CB5C9B99D237B358EAA2598B5DD15C0F743BCC07E416BB0E73177B + 5167C34E6400B8681E1B76CB03FC5DA67DEDE72F731D17AC94F60A5744A8BFB0 + 09D85EEA6C0A762E26077089F615C931847D06F8E2FC45805F423DA3AE2BD66A + E8E76EABD6BB1EAB096B9813A0B74B08F94C0900060813E6B9CB4A6030CEDF47 + B4DE7DCD23AECFA6ADBA416AF017F70CF09752FBD4F90BF8DDECBAAFDBE4BAAC + 91E3A48F0D58DB8845068015066F91BE47654C04FC8D812FC2E94B8EC07E4F3C + EB5DBD52B770CF1A4E12C8FB24DAE6FA3EB54DDBFDA41C4408E76DBBB4AFF713 + CFB8BE00E06D3B74639A0DEA01DB77B9C1E432DC2E7A6EAF1BB263AFC25F83C0 + 4377ED738377EFD54D6E9FDFD73B7D81BFE4021EF0DC8EC40D0C08A6EEE1AFAF + 0FD9EDC7705899236BF09E3D6EC8DE3D79EE2C36EC6FBC7D77C3406F7DFB719F + AFF29DB7D59D865B0C01198010530F1D74530E1E70BBF76FCE3CFEDBA6EF1C9F + E9E69D7E575DBD84779EF38185A93DE5E62AB0C2A108882217E951375D21F011 + 37F3BD6389667F70C2CD95F9E5FD008067CAB908D50CB45D70FE2337FFE3B36E + DEC71FB97967A57DEE63B7F0FC79E93FA7F505E7CEB9A597AFB8E500E04B57DD + A20B1E00021B0D30027681BC9C0F60086004E0D2F68E5F3F07011C8184CBE538 + E026EE50DCB094CB651EA077A988F3208E05ECAEFCE4B2BA68975E91E381C738 + 563FFBD4ADFEFCA6962B6E7EE256481FB013980CBCE475787F2BA44F4BE0EAA7 + 3E84F28AEB8CF31A388E09A1CC18E0F4B202558E47311045B4577F76D5AD04BA + DEBAA2AEDAD59F5DD39CBEEAB09512784B1867C0EE2A79AF2BE477587BEB9A7B + E4CB9B5A96009945AB3937101BDD885F83DFC3E7F1CDBD07CE7949CECDFBF2C0 + 16F19E09E90CDCA5CE7C4AABFBE37104D3C739F9FFE0FFCFBB8499A7FF1F32CF + 7E3F5E37F7EFE5FF8F6D0EFFF7F6EF8A62904E9BBA39B9A913D69A3180302076 + D1A53332CFFFDF70AC07FDDE3D8C38D68EF740D78BBF2F8E9FF7316E731F269C + 727EE404A60EE4F5CEF4533A9F31FA668450D0F308577EF60337EFA3F7F53306 + FC35D86BE2F3369F50E6843797FA2CF96CF1D903FE329F3A25E7613E809750CE + 334F1E77F33E784F3EC7A7DCF41347F5610EF2FF4E397A501FEE509DE2410D39 + 9F8C9113D8F203933798DCDF40DE2972AC416005BED28733987E6B8F3F723001 + C3F423EA36D71CC2387A71F95A9867EFF205064BDFFE37DDC4836F6BAE6234E9 + D03BDAF67ACB4D38F0961BFBF66B0A82271DE07B57E6C87900BFC05E03C00672 + D5CD2B756B8F906B03A0D760AF82E3D75FD730D1E40A26AFF0B017E53AF2D28B + FEFB5DE6E1041E1940B0C262915D6FA86B78E9575ED53E84BB18B7F1D8D75E57 + D86BF0D700B0398209FDACB9ED2977EFF3E057A47DD2A66F08F97FC99FBF7D87 + 5C6B77BA7ECFF910D10379D0EA191F718307AFECE12BAED1C06034E0699F23D8 + C3DD67B44EFF90EDF23ACFEC72BD1E7BD63DFCA8F43F25E715F517F5DEBACDF5 + 7CEC69F7B0AE239E72FD1FDFEEFA4B5F1FC0AFA857586BE43B8045B246F1EB14 + 1F1EFA612959C7D85AC640306B1AD25C7492F50F10F8E1350060EF0AEE22FDAC + 9378480EB16E621DD551D36B7875D27595CC5BB1C1755D2EEBA9E5406199B304 + E7EF5A75065BA8E8A245A205ACE978A04FD673A22E4BD6C83A0F304C94171FED + C52030EB47D68E4060BF86E4014259372E9435A7AC2F81C1C0DF36B317EA4388 + 45B2AEE4C144CBFFAB0F2ACE5C24EBD8C50A6D79D0B19DF415317F96AC856790 + 07788E6B3D7DAECC9B276BE0F9B23E9EEF8AA62D76ED67C8F91504CF55E76F0C + 808B66C8FA79AAACBB27CC740F8D99E65A8C9BEE5A8E0702CBFA7E9CB4C74E75 + 2DC7D23753C3453F3866AA426085C5324FC1B194E412D67CC26300BE80DFC949 + DDDA4D474F720F8CC887BEE6E805EC1ADC35A8ABFDC32728FCD53E6933AEC097 + 7199DF48CA7AFDE5BEAABFDC130D18231A2BF73391B44FA4E3A3E51E098D728D + FA8E94FBA791AE4150234244CB3825A1A3150A8B80C148DDC2005E607004861B + 00841F1E2CF764010E4B1D080C0046067E01BD96FBD7A0318A8170BDAE72FF1A + 802FC2058C0CFC9A0CF626C0B7639F040C1B00AEDDB1AFABDB59CED391D44832 + 8FB0D01DFAA8C8111CAB16701857B0A8663B396FBB9EAA9AA204005B99C05E5F + 47768F1F43DF82F03792815F03BEB502F44D2070B32EAE9A94C0DFAA849396B2 + 7A735347697750556DDA3E91C2E087F2417035DCC23C40CFC3F20F018ABD63D8 + 5CC3C800701AF06A886714F66E00BDC998C1618E7B201C9B0180ABF1A03EC702 + 7FA50F5381ED351135AE7AE3F62AEA556DCF29929917727B56A521B0C1DE58E9 + 39362FAE9B6C8F2DAEC7EDFB22009C1EA34FF70D455589FA8739A42EC6911652 + F725FB890682FD1E237DCD5DE520EA0687736AAAE0D660B001DD34DC3595375E + 48BA3F1A290BF8A6E5F774CB5779FBC579FDCC53009C528DC6A2462A85BE2871 + 093796E3429FD5AB7B489C077523D95EBCE6174E20B3CDE5351E7095795F94D5 + 1F50F85BB916EF43C66BD47755EFAF23FDB5E43C355DB396ADDDB459B3DDC469 + 735DB356F259E53C55EAEB1E3DE038FDBAF6DA7743694E109F3B57379E40BB7C + 9943381F007B25DC055E94B0A4C21C27F3D87BAC846745DC2B563CAE3CAC0CE9 + 31C0DE5206D2349F1319F44CD7AD6DF558E979511FF0B74A0DF9770BE355AAF3 + BE3DFCA55ED92070C6B1DF1C552A1B00678DC5F0374F5990B74C718C296B5CDE + 60FC8FF78D13EF2F7A8F5F050047C7953EC62BEBC3744F74DBE0375FE92F93F8 + 4B26FEF24BCBC66E4759E72924E6DB1772ACF83C36C7CE9FA5D25FBE56A65EA3 + 6A63554501707C21BE1DD9453A4BC9853B538C9BD27D9C5384A3B766535F9A08 + 095D9D3E59108912B82B02DA96527A8CBA8ABA9CAFB6BCAE889C1A7E81E3C12F + 73EDBC95ED35020056108BB3B7760B2F75F97AB89BD7160180ABD4960521E037 + 48FBB45FEA4155A55DAD5E9B52AA5A5716A432A6D09845625850DA22D38BB65F + 7CAAE4B81CC8CDD54D55C362B91000CE84BF19F2219DBD80C0387B0BC15D05BA + 405991CDD12742A31B086E0490CDA9A4733CCC8D816E2CEB37073075EBCB07C0 + DCE46403E0EACDBA2525B0D7C06F7572F34A9F96A2AA48DAF4D568E6A16EA216 + 72C3D702202C378052D776736907650160FA6AE01C961BC36A2DE53DC8CD1CF9 + 82F5C962B9D104FAE2FCF54F1D7BF88B00BF3571FE16F575358AFA49BB9FABDE + B6AFABDECEAB667B9FC71751678EF5C5AE5F83BE69990BB84EA721AE6EA7A1AE + 6EC7A152F79057816F80BD26FA08E95C1FE08B1B586440D764003876FA36EC59 + 1A00DB38F2903717F619B00BE88DD510B88B22004CB8E8467DC7B946FD08F93C + 3611ED260326B826FDC9FD3BDE35A61DEA9ADF17B03BD803DFD8E54B5D73F78A + 80BE1AE239006003BE96DF971219DCA504FCDA9C1802037B2D0F70F3007F69B7 + 10351F9383BFEAFE1D3FCFB598200A65CB89F35DEB49805D0F80DB4E59EC5A4F + F1A0B735397E35E4B39702601CC0BA29B6C8B59BB94CDACB5CCB29CB5D97051B + DC90F5DB5C9F958FBB56D396CAB14B5DFB39AB1404B79A26E79DB9449DC08484 + 468488EE4028BE0081DB87D07C16E219F86BA19F81C00A8317AE10AD92F62A9F + 174ED441DA7E73B0C4755DBA4EB4D675268C20709890CEC1F56B10983AF0D700 + B0397E81BC847106007BD8EB213075C466268E96AE2B36C9715BA47CC4755905 + F0F54E9938F433EEDFC4019C0060AF1E1B71036F703D09CD1CC02F32674EDF2D + A17C4CF4B877FC589867423C5AB8479CBE805E73F5524FDCBD5B9FF570F7A96D + BAA9CC06337564397CE923FC337909D98CEEBF8DCDE91D5A5757EF0EA0EF3E15 + 2078D0F6DD7E737A270E5F1FCAD987737E511DBD405E002F213481BDE4503408 + 1C0360EA8C0D051AEF96D7D923E3FBF6290C36F84B9D8DFA92579FCC049B77AA + D96FBF9C842A1DFBB6CF016C6001E8400E4A00F0070717651EFF6DD373277728 + 14320180014F31003619089E01F8552875C2CD14D18FBBB1BC9F5FFCD37FD6B0 + CDC031C09AC2B60B67DDFC8FCFC8F1A7DDBCB3B42FB885E7CF29185E709E10BD + 97DCF26B9FB8E5573F714B2E990B1480970B0B0CAC05F01AB0332D05EA5E031C + DA7C7F0C0070FDE7D7DDE33FF8D26DFAE253B70AE029E75D7AF99C5B7E05680B + ACF4EE54C0E3EA1BD75CC90D790F9F5C71AB6E5E774BAF5F75C58455FEE2A65B + FB075FB8959F7DEA56E2A2BDE1A1A6863796E3567C8AA3D672FBF25E809F9C13 + 10CAEFC17B248C3250F5925BFBF955B7FA164ED8100A1AA01BCE475B21ED2DE0 + F31505C1405FE0ABC153B492E3E4BDAC23DF2EB0F7DA45B7E6D32B6ECDAD6BF2 + 7BC83C75FD5ED171040CC625CB39367CE7865BFFA5FC7BC839F8DDE95BC7BF8F + 94FC3BF07B207D1F416BE47DD87BF0BFA707BFFE3D03882FCB38EF8F3EFFBB93 + 8FD83B873D4446FCAE1C6FFFA79C83F761AF49DBFE3FACEEFFEF3D00A6CEDF13 + 2563367799FCDBF2B7C1DF1C4E5FFA98039C451CE3FFA6FCB9ACEDEB5EE64436 + 480CF0E57CC05DEACC650C083CE723FFF76D10993AF057FB653E701778EB4B40 + EF69B7F09CFCED7F843398CF19AE60FF39E3F3C5E710313E5F3447CE05009E87 + 9318F7F0A9136EDAF1636EF6A9936E817C86E67EF0BE9B7EE2984260002FB9BD + 0D00F3F9A604006BF8F6778FB919278FC85C1EEAC0E14F4E614A9FFF77EA493E + EBB893DFD5B6E5079E24E5041CBF524E7B575E5FC680C31381BE80E0C307351C + F4A410FA39CEF3AB50F8C05B0A80271C78330F007BBDED261FDEEF261E7CD38D + 790B27F06B3A7F0A6098D0CF387DA5CC7304CB7734DFCD7C47E3E83531166BCC + EB1E142B207EF92515F0D7F208EBD8ABAFBAD12FFBDCC0005EC0AFD52DA434D7 + 1DBBFE508E7A19F8FB8A23DCB3858006FE8EC4112CF5A1720D1CBC53AE7D3C14 + 25E2FA68D748CB753F7C9794CCD9B1C70DD821D751E6EE91EB2A0F51D1A730D8 + 5F97150ACB359B6BB35DA37109F3F016F0B7DF9372CD7E0A77B05CAB9FD9ED7A + 6FDDA11AB4ED79D5E06DF21EA4BFFF533B5C9F27B6B9871F7DDAF57B4C8E7B74 + 9BAE2378B00CC50F9E51D787CE22286CEA19D631AC6B7AACF30FB421D63A5D4A + 3668D4931EAC7F70F98A00BBDD576F0C6B29FFE01CEB28A2AB745A5EE23AAF20 + C5C61A5D5F7596F557E7E5ACC5641D2602047759BEDE755EEAC33F9B03B8BDA8 + F362392E805F4AD67BED17ACD487FF2CDA0B6B44E473FB2ED3320B002BFC251A + 0D0F15CA7A93BA8577C601DC7EB6AC4D454533658D2BEBDC0E32DE710EA51C37 + 73BE6B3B639E6B337D8E966DA7CF73EDA6CB313364FEF4C5AECD641F35A7D5A4 + 39B28E26754A0E026B486851739CBFE3A7CB7A9C90CE387971ED4E91B5BA8810 + CF1380C0331404B7903A30D980B207C132679C1C23C7018F5B8E07284B7BF464 + F7E048B9E7183E5E0170D3919354C05D03BF4D474C94FB06DFB670CF0F8AE8A3 + 0E0046068081BF0FC931D41B100E3A805EA06FE341322788B68E518A9A302EED + 2622CA4680E17E805FAF86FDE45CB11B18080C0C0E10D840B001604A7502F71C + A2AE6185C101F01AFCADD949EE6545B481C306840D046B38E810F6D99CBE280D + 814D310406FAC6FDF707C05BA7533F57AF8B1CD751E60280031866DC1CC25667 + BC968CD7042A1BFC95BE1AF4E3F4C5F11BC24927EDA07C077036004E83606BA7 + 01704D910160FAAB871CC20A7F299B7772D59A7754F8EB01707B9507BFD43D10 + AEDEB483AB012C8EE0AF8FB89603C02673FF22F66A80B8B133B89AF45563EF86 + BD16F67BA4AD0FF1876312000CFC659CBD9908025BDDEF0D4909146E9C331718 + 0026659807C0A5F7A17200987698D32807810DE2A6158FC573B38E8BF7DBE2BE + 782C3DD7CEA1FB7580DFFAECFBB570184A80BE55D9470C7B8CD564AC1A7381C3 + BAC7E8A1B009209C184FA81B28AE23C7D7A938D4B57915999B06BFA62CE09B56 + 6E4FB77C95B5579C1E8BF7971301856B007881B55296A7EAA501701E04663F9E + FDF55A32C724C755AAE9F7CB2D24761511AFAD6E6305C5F5E5FFB281AB5AABB6 + 9CAF86AB52A396EBD8B5B35BB96E8D9BB764A56BDEBA9BABCA9E7BD586AEAA9C + 0BE96BC96BFA10D265EFFDDFAE8C05583BE611C61BCA02C05980376EA7957017 + 78112C29F0A4A43FA5CC63EFB1F29856605E69953596C89819A19E8199796C2E + 43063DD3756B5B3D567A9E28CFF11B202FB057A16F35793F324619BB83F364E7 + FC462803F0C6AA3000CE02B85533FA6E4BF206D3FF78DF28F1FEA2F798FE0315 + C57FEC39C9072E0F00CB1F7B0A1E332F7D7CD687E99BA8B2BE4CFC974DE92F42 + 937D51565459E748CBE6C5C7657D31C76359F55C5F4EF6FBD897A81F0F5FE805 + 00B0C2DF4869005CF022FB15155FC4BDE833E5F7D9022201BEA50030F5A62A1C + C100DAC4A59B210DDB9CD19F73FCCA05BD367D39E89B9BD3BC1400F6619C5B89 + 02E80D0E5D5CBFD667FD3878D5251C20AE3980ADDF84CB973E55BD48D6E63508 + F12C0BCB4200D8DA3E4CB487B9B1582413C6B96AB26096B6F5F31AD4458CEB22 + 9AD2EA4131F48DC5980FED1C035E16F5A5E5C1B0D4114F7FEA13A05ED5C9DBFB + 404785BFF7C93963089C03BABE3419FC652EB23E2F5FF7B97CE586AA99DC8435 + ED26372F7233256DA02FC205EC01B08C8BACAEF9794309FC455564AE81E11A00 + 5F60AE883A219CAD3F06C8D6A7AEDFA8ACDE5C5EA7650F0F80E5A6AE5A7000D7 + 6A2B379871E8679CBF210C55CD226E4AB971F5CE5E83BCC05F4AFA6200CC3C03 + C0E6FA35D06BB057730187B0D026DCC20A813B0F73B5BBE45CBE3EF47370FF06 + 000CFC45F5BAF910CF06716319E035F97ECA1CF4B57E406E3D14EAD9F077AC6B + 8CB357D448EA068101C2B87F01BB9922BFAF940DFA8D1379F8DB68004FD27BD0 + 6B0058C1EFA049EA06A6A40D10060CC7B01701740DEE5A1F75136326DAC05EC2 + 429B4B18E84B9D3E4241C7A582E0F184809EAFE017E72FA2DE92FA9485AEF594 + 45AEED340F7B510C7F55E4019E811B02A80BE85D26F5D56EF0866D6EFE8B87DC + F8A75E70ADA62F75CD262F749D16AC711DE7AF766D672D5500DC6ECE127501E3 + BA50F705D077AE8870D052E672B8F9B0CF067FF340B0B64B5C9762BF19E821B0 + 01E0B5AEEBB2B5AEF312420A96B88E21DC3302FE76285E29FD2BD5018CF3B77B + 496E8312974A0C834D368EBBA5E7BAC7DDC36B1F773DD63CE6BAAD2657DE56DD + 14C50DC306291BA526C228E2A0215C22219F7B3E82BB668BEBB559E66DDAE2FA + 844D58365D01BE039FD8A6EAF7F833AEB7F4F77BDCE7F6B39C7E68A088BC811A + EEF9B1A7348F202122D918D69C826C144B1D186C00984D64CBE14B3D1120F8B9 + DDBA793D70E76EDD98F6DAA5612BD9B01EB6D3BB9700C06C540FC2191CE0AF81 + DF9CD8EC4E815E1C56CFBFE046BE487E44C23EFB524342A37DFB14F8B2D9CE66 + 3C8A37E0D14F0F0C2D0536EF44E4F41DF7D69B6E0CB00068006850E0BB5FC390 + 8E7F0BF8F0A6D67F766878E639BE4DFAD74706290C3238A4D250B35E31043640 + 6563402A9342AA33E53B807FF1DBDF68185DC0D9BC8F4FBB05E770579E51F03B + F7A30FD5050CFC35008C165FBCE034DFED55A0E42507C405EEE2CA04EC017981 + 8DD66F21A1CDC16B1018D1C73CFA36FDC12DB7EB677FEC9EFCF1F7DC9A1BD7DC + B2CBE71530AF90D75875FD9AD3FCBB9F90631727ED755772E353B7E6B35B6EE3 + 77FFC02DFFF4865B2CEF69D5679FB935DFF9D2ADBCF5995B49DEDE5B57540A47 + 01B500D94F0CFEE6F2E5527A10EA8128CE58EAC05244A8E5D537A97B5731E3B8 + 6681AD6B3F97D7211CF36757DC1AA9D367C7015515244BDF7ADCC10058F9BD57 + 5D2364F54575F1AE929273AFFFE213B7E14B7E2FEFECE53C1BBFF3A95B4F9F82 + D9DCEB51E77DE05CA6CEEF85F3B7E426D0D6C3697523DFBA2AEFD5C3571FCEDA + CB43E2ABC979398F87C3FC7BF1FEBD0CCE0274A91B1C36190CB63AAFC35C2B01 + B1948C73BCCDE7FF1DD86BE3405CDA066FE9430691E933C5409936509763FDDF + 3050D943E7788CF31A3036503CFFDC07F2F7FCBE7E8E14E69ECDD5EDB365B097 + CF176E7B4A1D273434AEE2306F4608EFACB9B8C9A97DEA849B2BE582331F687B + 068E5E11B9B909D14EDF9CF7DF95CFF74905C368EE07F2993EFD9E8C1F77D38E + 1F71D339463EFF849A9E29E745B4A7038CA53E4DCE358530D3D24693A53E1917 + B1D4199B70E4809B780C67F07E374E21EE0137EDC8611FEE19F87BF89002600D + 071D397D3DF07DC74D3922DFB322EA08E83BEE9DD7DDF8FDF29D0B3096EF5D22 + 32F0FD0C504EA234F03D2D7DD42947E1FA7DE375D57880B1883A798547027E89 + E81080EFD83702F47DCD436140321059C12F60F8955714FE027E11F51808730D + 1AB47B97A62330001C43609CC094E4AFB7681831F4A5AE6DAE97BB9F5751F76E + 61198BE6283CDE29D74DB91E73AD4EC06F90B983BD2378BBEBCBB5FD09E97F7A + B7EBF714021AEF767D9E2477BF5CDB150ECB799E26E7BFAC171E7B56D61AB226 + 78D44714E9FB98AC2D649D610F9D59DD80B045223118CC5A0657306B1D8B7062 + 0FBD0180A97701FE8A3AAD58EFBA966C743DC2DA29173D8587ED48A3E1536974 + 01FEAE58E73A2E9375DCF2B51A26BA3B21A60915AD00D887806EBF688DEB54CC + BA4EE62F9575DCE255C9BAAF3D9160E6017D7D1868D686B8802D720C65517003 + 934284BA77FF2E9235A77F20913529D169DACE5AA420B8688E87C21D58ABCE96 + B529916B28199B0500C6F54BE8E7391E024F9FE78A66C8B888B5718B89735CF3 + 09848026820EEB6A59734F9AA5F0B735F982835A4F9AED9A2BC89DA66A3E1EA0 + 3BC535C7192CF33906E06B3984358FB0D4CD09DC3CB88601C9ADD5452CF70EA3 + 3C0006FE3E346A926B818358FA00B8005E95D43524F4D0718943D880307D067E + D5352C7D163E3AE91FCC03AE1EF8C6F0D70070A341D22F7500307A40C6910161 + C02F4EE0FA7D80BEC312F84B88680D131DDCBE310006FC920FD84243C78E6083 + BD0FF41BE99A0F96D7E92BF78E0A7C657E0F1CBF0365DCBB85B5BF870FFF0CEC + 8DC16F961837C72FE09776028051C7BE0A8091C2DE0EBDB5AF4C001CCAEA6DE5 + BE3D006064D057616F00BE31043607B0B98009031DCB5CC11616DADA0A820300 + 5605086CF0D7007075D25051A76CDED9556BE1E16F8D161DA404F8B6D7B681E1 + 1AA8192A0D80AB027B23E05B48898318004C1BF1F07E1314F675A2F909000E0F + FA2B08A60C2E6003C1F49900BEB1DBB77AA3DC3E53966C3F0B089CD41B66435E + 647B65F118A5F559DBE6DB587C8CB56D3FCEE695DE9F13495F15C25AB377787F + 3357A55E4B57B5BE9CA32E2E60EAAD5C35DAECF1C9581EFC65CF91DCC111004E + FA5300D8C06E21C06B7314E44ADBA06E7CACCDB339F13C1DABED9586BEB1727B + BAB7AFB2CE91B5B7EC01B081D808F41690CE13252038A54AD51ACAF9A45E4BEA + F7CBDC5A48CE1FF6AAEDF7AFCA7E31FBE8ECF557976300C035EBB9CA35EACAF9 + 6BBBAA356BBB064D1E701DBBF570ED3A3FEC6AD76BE1F7E4AB7AE08BF4D82AC0 + BBDCDE3DAF7F2FA4AF1544DBBF1E63B44B2B3D966328D9CAE32D4924D7A82F52 + B9C7DF23E531AD9461D2A46319FD794A8E8D995C39027AC6F5B4A2B90A78A376 + DC6F80976368ABFB97F9067CC3B9F2FAA2E3E376A2F87D7C6DCA00BCE5A914FC + FDFF2C004E29FD072A8AFFD873920FDB370500DB9744AAFF2B7F219479DEFC2F + C258F19765459500D73254E898747F5A651D5BE8F8644C2E341A063A00E072C5 + 45542EA6052FB2155059C7DA05BC22AA7CBF08B72E905781AF2886C0912A0A80 + 0B42E07254458EAB5ADBE700CE016016622CD458B0E5C029F5D84DAB60584A05 + C21AF605F9BE643CC8CEA5D285A0D5730017676FD6E2327F714AE9CFEF8FCB41 + DEB49205346D9997078063450B6D9ECCCC89B687C23E17B007BF0662D3003876 + 03276AECC578B507E4C602682B256DCE11C35E83BA69104C9FB5794DEBF3D0B7 + B39472E3D4546EB270EDE2C8050237CBC1DE2CE5C1601100D820300058213063 + 01FAD66CD5536EDAE4E610000CD80DB239F1DCB8F47539969BC4D6729E007ACD + FD6B0E60424293FBD703600F7F91857AA65E03F7AF949ADF17C80BD46D2F37C3 + 40E00800C7A0D7DAA65CF86772024BA9798109EDEC416FECF845E4F4D53CBF61 + 8C12A7AF015DE4DDBE3EB7AFC940AF8D35EA3546A5ED007263E89B55671EE0B7 + 7100C18D4416FA19C5CEDFC6405FFAFB8906485D54BFFF785717082C651EDC1D + 3449C7E9478476A68FF1D8F51B2B86BCB40D000386F334DC3B7F81BDADC6CE4D + 0030CEDF960060DCBE63E7E858AB71F3B4DE7C5C00C093D898225FD9C2D20058 + E1EF2297CBF72B7D525AE86736B9DACC9039B316BBD6338A456CA2AD727D566D + 75139E7DD10DDBFC9C6B3D73B96B3EADD8B59FB74AB452C697C9FC62D7760E61 + 9EFD861CC0D7423D239CBF1AE67901EE5F1C1B393707250058F3FB2E5AEEBA14 + AF725D97ACF14E9045AB1400A3CEC5AB5DD7A5D227EAB08470CFC1E5BB626DE2 + 02EEBC0407F03AD76DF506AF00784D6C586A5EDFE006EECEE6A5F4772DC1DD0B + F07DCC75594328C4475DEF479ED00D50364271FCF67E64AB6E8EE290E9BC5A8E + 5DBF45D56BD363AECF1636541FD512B1E11A03E0414F6E778300B8C05BDCBD00 + DE279E75839E7ECE0D260FA0949A0B10C76F80BC067D71FDC60098D0CF7D08FF + 2C6D368ED559B46D6700C16C30CB7970F3EEF26E25A0EF809DBBDD40DAC0DF5D + 7BD5B5345C37AEFD06B66D6C230F8003C8DDE3F3FCC6F2A19EBDB3D75CBDE458 + 1CF3F22B0A8A07EFDCA5F36C039ECD76C26EDA06BCC160B4F9B5BB138EF9D6C1 + 290EC830EAF5D71416008101C0D38F1C72D30E1F50F86B00F8BF1EBEBBB9877F + 1F7AFBF826EF067CEF849B8BF3971CA3EF1202F6B00AA7AFC15E03C1D463004C + 9F41ACF27E7EF9DBDF28A0CDE98CBA7F4D0BCF939F9590BDE7350434009890D0 + 0BCF5F903640CEBB78CDED6B909790C594B8356D8C798C231FF6D78776A64E7E + DDB59F5D775B7FF45DF7C8776EB955D7AFB8658477BE7CDEAD04007F724DFA3E + 71AB3EFD547443E6DF7025376EBA47BEFB3DB7F9FB3F702B3FFD4CCEF3A92BB9 + F5855BF5390058844BF8262EDE6B6EDD17D71D4EDA921B3E2F2E618F71FF027F + 01B5E4D505F8D2A6AE00F8130F8001A5EA0096F1929B7EDCE6727E2D6F5D726B + BEB8EAD67FE713792DC07300C7619C9CBDEB3FBFEAD630973CBD9C4BCF0D7005 + 105F71EB395E8E5D23EDD5FABEAF68BF9D83D7A58EA8A35C3FCE5FF9F7B97145 + FF5D01AE3885F9BDA97B00ECC13025C2250CE4F5F0D8035FCE6325FDFEDFCAC3 + 5FDA763C109739762E9B4F7F0C7A0DE21A1046F4C760973E2B63708C9648DD60 + 2EF30DEEDA1CE02E02EC02790D20DB7CEB37086CA0D800F0C2F3D2C7DF7D00B9 + C9C313A9072D6C1C319FF119EFCB67F1B47C4EC371805A00309F5B75EE4B9D50 + EED4D5D12B9F6B00B02FCDFDCB1850F8849BFFE107AA192701C03E87F0E4E387 + 1CB9BE7DA869DCC03ED7B087C2805F20B1CF47CCD8E4E307757CCA09F2031F70 + 93441344E381B887F62BF4D5B0CF8764DE11F94E090018872F02F802817D1D27 + B0EF47B8813DFCF5A1A27112F3400EF0575DC5B4A51E3FAC3346BEBBA9338736 + DFE5B447BDF1BAD6474B7D04DFEBF2FD8E6398077ABC33D88360EA16627AD46B + AFB8912FC9F545C4F506D94347C0DFF16FC81CB9F6909600006CF97E8791E317 + E8BB8FFCC12FC8F80B0A6F8790164164D7466470374B068C6370EC1FB4F20038 + 77ADF6E057E1AFF591AFFFA967821B1897EF0EB9DE734DDFE97A3EFE8C7BF8F1 + A75D2F22801032FA89ED0900EEFBE833B9541232A7DF634F276B109301605302 + 8081BF227BC88DB58EAE7108E31C20306D4070E7551B545DC3C375AC9FE207EA + 724058E60500DC89751AD098BCC12B37BAAECB37BA4E4BD66B1E60C2417701FE + 2E97B900E0E21259F779173021A0DBCFCFE502EE206B439F2738AC17A5644D99 + 5B67B206250434F565C9BA54D394CC0EE057A3D3C8B8AC55DBCD5EAC8E60EF0E + 96F530CEDF1973BDF357D44EC51A59D6C5D3582FE752A4F88726594713467A6E + 9823FD53710CCB315202789B4F9035BBA8C5C419524E9712008CEB37E7FC3599 + 339810D04D8363B8A5D45B58486873124B0900C6FD0BF055901B006F0C75D519 + 2C629EB665AED501BE068BCD09ACFD110036373061A173A1A1E55E0A495B1DC0 + 322F96B97FEB03780B00E004F206C5E0D7CAC67D4768EEDF46D26E2265FB31D3 + DDB0E275AEE7F4850A7CEFEF82137880D4733098B251AF218E3CC03EF7AF07BD + D4E39CC0B10CFA1A2CA60E10AED5C98783BEBF23F7CE1EF052E20436C5FD568F + 0170B53672DF1E40B00160A276DD8FA45E1B37B0E60DEEE5F305B796F9727F0F + 1CBE5F8E4331004EC3DFC411DCCA835E72FF02800D04531A0CAE41CAAA44B43B + 07006C2E6040B0D45B74923183BF11000EFB24BA87F29048FA62781B035F0B09 + 1D8F25E19E01C0CC35881BCD31F89B2799632EE02C008C34429C8CF97DA7DC1E + 94296F0F4B95BFBFC5BE1850B65AE3B62AAB53DA7E99B54D764C56DD8E492B0B + 005722EF6F080B6D73F47CF5E598BA2D140057AA437867EF0CD6FCC0EC37323F + 0E056D0200A3D0F6009876D304001BB84DC3DCB462986BF5F898F81C367EDFFD + 0FAA0C7E9AB2F66F6F77BFD7A47BC5A15E16448EF79573020037526541DF58E5 + 01E044090006FE7AE1FCAD26FF0F55E4FD01802BD790F753B5A1AB042CAD0E47 + A827651D57A5665D57FDFEBAAE52B59AEEBEFBAA88AABBFB2AC35960110D720E + 60DBB7177D150E519EB278816705D479CDD2CA3193FC7A2195662DD2178D97A7 + F8F87BA53CA6550000DF9E32585C21013DE37A5AD15C85B7513B9602DFC8ED6B + E19F3947027DC3F9ACAD7DA16DE7C953FC3EBE3655BA8700F82B4BDE60D63FD4 + 3755A93F4CB3DF97967CD8CA00C0D9C7E47F88EE9ABE46004C6272FBE2B32FDA + B4E22FC78AC8606B792A745CBABF222AEBBCF1F86D036054439479712D5FE55D + A0E3F172551305C89B02C0B93CC0BE5DB9962C5A500ADCA65551006CF32881BE + 55EE8F00B03A7F5980F1049E2CE402A44D03602D65DC5CC0B46357B08DC7E788 + 65E7B27A72FE28DF2FB24569DEE2B43EC7F805B081DF42001868AB4F615246C2 + 255C5916D32A9B8758A847F34C1AFE99C53CA037C05F85B34165016083C60680 + 4D3A4E19C40D8AC9FA6257707A0E37351E0077C903C0359BE714035F4237C76D + 750707A7AF397F0B01E01CC8F500180184515CB7B6F559BD566B423B07D0DBCE + 83DE520038F4DF5F44CE5FF2FF7AD81BE7FA8D0130EE5D05C141CCD1FE08F0A6 + E1AFBA7E15FC0E71F5BA0E55E59CBEA18CA400B8DB70575B017098DB55145CC0 + E990CE067CE37A1A002383BC6501E0FA944140DF467D7D486740AF09F84BFE5F + ED27CF6FDF1C0056C83B70A242601CBF16EED90030A25F2575A02EE3AA007A0D + F61AF035E95C1933F76F220030B077F42CEFF04D6BECDC24DC732B9CBE387E2D + F4F3A4F9AAE65A2E70AD262F70AD272FD4DCBF6D08D53CBD38D9C832004CD8E7 + E693E7BA1693C97FE601706B20F07420F012D76AC65285BEEDE7AF761D17AC76 + EDE6AC5079F8BBDCB59DBD5437DA705B109AAF08E78508373039DB80BC45F37C + 7E609C1B067DA99BB48D70032F6113708DEB5C4C9EB855AEFD2236FBBC3304F7 + 2F0018115250F3FC6AA841E923FFDCB2129FF7370AF54C694A00F0BA4DAA6E6B + 36B82E25723CF9EED66C765DD76E761D57F93A8E1836450DFE223648353CE29A + 4DEAFCEDB5E971D7EFB12755BD373F1EE0EF131AEA994D574A366371F7E2F265 + 83968D5A75EA3CFEB477FE027245B87D11A01700EC3781BD00C38C51AAC3579D + 43DBDD200D2DE935F8B95DAA81DBE43CCF49DFAEDD6E8094FDB691BB97F65E0D + 4FC966F4B05DFB14008FD8F3821B1E36B54D1E04CB78E4FAA5A43D42E41DBE7B + 1500037687487DF8DEE7D5013C4CFA066BE8E7DD0A860DFE2236E0D9782757E3 + 689C5CAFBDAA613CEF860BF8D081150A0F0C02E31AC36966797F2DF4E8C6FDCF + 651EFF6DD23F1CE9ED66BC7BCC4D0712BD7BDCCD02F2BC775C73FC4E39714801 + F03472FD6ACED11CA832F01B432B0B675BDECF2F7FF74F018C9133F5B4C3053C + EF2C92F6C767DC82731FB9E24BE745171502E304F630F8BC7707CBB1067573F2 + D01759C85E6400987A0C8081C5B856B544D72E255AF909BAEC565C25E7EF15B7 + F2FA27AA15D76EB83537BF708F7CF7FBEEF11FFFA17BE20FFFD86DF8CEF71D20 + 78F5675FBA355FFC812BB9755D012B0018D8698E5820296E5720AE415473FE1A + 64252C34B0779D425DA0AC7701733E530C8E29574B1F0098B0D19CC300305A27 + 631BBFB8E6D6DFBCEC565FBFE0D67E86E357DE93F4AF93F956E735D6CA71F4AD + 41F4C9F10A99398FCC43D4EDBD220F6D3DB8F51097DFD7FFDE0677A91BD405C2 + 5A3FF3FDBF8D1F8FFB0DF8E6E678604C9F07BE1E0053678E815F9B632E5DA0AD + 815CFA9843198BF908B81BC36193415D83C70673290D0ADB98CD050023602FF3 + 28CD294C1E629476FE027817CA67611179ACA54496FF9771FA29ED73C818E700 + D2F220867E6EE5733CEBBD133E97B794EAFCC5D97F8A70CF32266D05C03277C6 + BB94F2D9FDF07D39EEA49B7CF4B09BCE43201F9C52C08B00BC805D202FB987C9 + 35CCEB017FE98FA12FD0184D3A7A501DC093E5BB639294005D85BD22C0AFD711 + 5FCA5CCD412CF37000037B3DF4F5CE600B070D00463A2EE7E1611CBE9F4D0A7D + DF7C43BFA74D7C77DB780C870D20F3600F2E5FBEC727BC9D9F539812A90B1828 + 8C1398EB4C04800D029B23D85F97885041EE5F5CBF00E017E57AB64F3598879D + E4DA37444A9F0B9F6B9FBF360E91EBA89548D32AEC2234742E4CB4E50BE6BA6B + 0058D32C04A521B045EF1810D4EF995DAEEF133EF7EF806777B93E4FCABA40D4 + FB09F2FB6F5300DCEF4999B775BBEB8F4244913E8FCBFA2206BE9B72C09707D2 + D200D8646B1CD63BEAFA05DAAEDB923882710A6B0910961208AC2078B5ACB544 + ACAB6CDDD55DDA4060E0AFCF1B2CF300C7B2CEEABA62A3EBB26C833A8129BB2C + 5B2FEBB9B5AE437189AACBB2B5AEDBB2750904260D08B98001BF1DC2C383B66E + 642DD98E070C45848056A91B78A9AC4B97C8BA55D6B022D6A81DE6C9B1B23EC5 + 196C516A5ACF94F5EF8C05AE259A3ECFB526E4F32C9F1318B59D296BE8A9B22E + D60723710CCBEBCC9173CD9135ADD45B038EA7CD53F8AB61A02779476F12125A + DA2D26CA5A7EC20C2D4DB40D0E7B403C4B9DBF06801F1A3BC5351559DE5F4A42 + 4A9353B8951C4B5B9DBFE6F805F006886BE0D7E02F2E61EA9456679E1D9BE40A + 96BEC68373AE5F03BFF571F506006C60B881867826DFAF0F014DDE5F2FDF6ED8 + 8F7CC023A4EE73011B084E4240F7F665A3BE23548D992F250018359136101827 + 3020B8C3D8196EF0C2D5AE9BDCA3F89CBFFD5D4319430061DCBF160A3A0B00D3 + 174360EAB153D800B039829107C0F9F0D71CC1068063C50018E81B036000AFE6 + F70DF0D700709DF6DCAF07082C639AD68939EDBC6AB5EDA1AA19416083BF2881 + C252AFD502E540B0E6E74D00B0C9036042447BF08B3AE400B094359A6600E087 + C25EC9839DB55E8D3E913A7C23F19A1A825AEA31F8450A7ACD0DCC7E4C00BFB1 + F2E6224DF5E5F78DB2E02F2227307B4E160E3A2DF6B0D27D71543AE06EAC18EA + 5222EB37A5C7A9277B6B91B28C18761C75DCBAF7A520B09E0FB76F9D162AF619 + EFABDDDC697868FAD873947A4500B09707C09523606B4A835CEB2B04806D0C65 + 1D930060C62B00806F7BCF57A4C7D5C8ED4367CD4176FED26A149483BD593200 + 5C2E04E65C06806B7A11F6B9AAFCDEC05F7500133D93BCC0558363B6460357A5 + 467D57A5667D57EDFEFAAE6A4DE05B2D57A9CAFDAEB2CCA912DCBFD56A3DA0A5 + EDD95B08E83BE1101551C20532C662081BCB7393D2F542CAE72DD9735016748E + 8FBD97CA635A112F4B74DB503883C51512D0B3BC7A2C03A5A9FECA0A74A52E4A + C23D3356E97E57AD664357ABCE03AEAAFC1DC6F0B720F835D96B7DADAAF43503 + E0F818CE65E7CB9AFB6D06C0F2475CA9E01F327FF8914862FDCF0D0067E89B06 + 80D37D155559E78DC7F300F06DBA80EF44E55DA0E37153C1A7BC62C84B19015F + 1B4BC06F10B980933CBF41BAC08AEA15511A0057BE5F4A83C1A2DCE22B07744D + DA0EFDF1B8C1DC78AEC9C60A291F00E72F384B2F50E51885BDF98B6403C03957 + 70908CDB1398B12A3592B90D83A27AD65C935FC87BD05B39C05D530276A9A7C6 + 74BE2CD6E3367962B42DC7C44A036073FCC64EE01CF4F50058256D00707515A1 + 92BA8B0A3B802D247455247593815F83C1067EA95B9BD2002F603756590018C8 + 8B80BC5900B8563BB919957E72FFC60018F09B06C03E74B307C05637D50D8037 + 06C1E604CE02C07532C0AFA94E80BD566A18E8EEB910CF06810B01E118003704 + E48A00BC96F7D7C23FD367633A8F70CFC1DD8B2CDC33D0B7C9808909005617B0 + 2801C05212EAD9423C1B04A6D476E833188CDB1725B097B0CF32469F857C4EC6 + 22084CDB1CBFD493FE11D35DD351335DB3D1B314023723ECF39839AED538DCBE + 387F3D00D61CBFC1E16BF97E71FDB6983CDF359F384F459D9CBF6DA7152BFC45 + 381712003C7DA142DF665364BEA8D58C850A7301C03880DBCE5AE6DACE5EEE5A + CF5CEA5ACA395ACD20D433D01787C572D761FE4A573497F612D77EDE7255D15C + 20F0321963938ED0CEDEE96BA0171707254EDF6E4B57BBAE4B4AB4AD730C0013 + 065084D3974D4142027692BAC15FD4791910788D3A8009F16C6203D23622AD6E + CE146BF7580FC0DDECBAAF7BC4752597DD6A9FDFB7FB3ADCBF3216421FF6D9F4 + 841BB4759B966C8E523EBCE131D7F391C71CC0B7EFA35BDD80AD845C24DCE213 + 0E104C5D1D3722423EF7DFEA73FBDA062D252E1D40301058A1307D32669097BA + B97ED1A06777BAC1DB76BA41C0DDA041DB9F73839FDBA11AB49D12088C93E839 + 3790BE9DBBDC0029FB6DDB2EE573BA49AD1BD7BB08FF2CC201BC2786BE3E6CA5 + 6D64037C0D025302784785D0CDC05D03BC84821EC6B8D447BDCC663A0E2A36D0 + F7A80CFEDA263CF91BE3DC8D8FBEF96826E8BC1D6DDDFFA442068305C004F24E + 020B2649DFE4FDFBDDA40307DC6B07D6651EFF6DD2F3279E50D8634047F5EE51 + 37E3BDA3D2EFA58029B87D0D001B8832806510987A793FBFFADD3F25900C1908 + 36F88B0318004CDE5F2030E19F975CF6E5C2F3671DCE5E802EC04DE1AFD40D28 + 7BF971E0AF415FEAC05EE4FB7DC8E865D70CFE5E76259F7EE2D6DCBCEED6DCF8 + D4ADFEF4BA5BF9C975B7E2131CC037DDCAEB375CC9F55B6EF58DCFDD9ACFBEE3 + 76FCD95FBA43FFE9B76EE79FFF95BCB71B6ED5F52FDC862FBFEF367CE773911C + 7FCB8349C02FF5755F78486BD0D7202A4035075501BA17DDDA2F2EABD6DCBAE8 + D67D7EC5ADFF9263AF491D5D55300CE8354730E73538ACA058CAB5F29A8FC86B + 6E96F7B2518EDBF0D955B749EA1BBE23BF9F8C6F90736EFAEE0D05C02BE53C94 + F451F27ACCB3F351B776FC9A16CA99F7F6C8776FEAEF4D8E5FC244036DC9258C + A8FB50D0BC47EF18466B3F27CCB40F216D6E69FE9DF8F7D210D6FCBED2C60D4D + 9F95848EE635786D0B256DE1A469E3A406CA029DF97FB6BA87C71E16DB9801E0 + 180E1BD48DEBFCAD19D445D419B773D99CACB9406044DF12C6E46FD41EA2E0F3 + 02CC5D2C7FD34BE56F19515F287D8CD99C621CEDF219B1CF18750F883D00E6F3 + 68227C3B20770E0E5E42378BC8E93DEFC377B50F984BF8E859B8864FBF279F75 + 9CC4A7D4FDBEE18B1BFA0004B017D00BFCE5FB8136C751374730250018D0AB6E + 5F20F0B1430A84A7F000C9F1C36ECA91836ECA61E0AF9CE7E851A91F76530F7B + 003CFDD811378370D280E304FEFAB0CFC80030EE5F003050182731A1A40DFAF2 + DD4CDDDAF6C00EFD0A81C3F737B23CC4D41500CB7CBED30DF6027F359FB0D409 + 05ADE1A0E9977120308A434027D7209102E010C522EE1FB2776F9EB896E9034F + 0A8271F5027B7D1A04C403525C6BFD75D33B80D30098940B4335EFBECCDDEEDD + BE31088EC3420383E91FB283BCBF7BA5CD7C1EEE92EBF06E79DFBB5E90F66E0D + 014D2EE0BE4FB0A6C8A59250004C3868597FF421DA88D63DF04DCBC2416BA492 + CD324FD6383CE486F4E137996300986827F6401C250FC29980C5B975174E60CA + 8D09FCC50DDC49D66A9D59AB2D97F63242417B08DC798907C0ED79D84FA40FF7 + 59AA0F59F70181550B791010104CFEDFA50904663D59347FA982DF4E8B5867CA + 1A74BEAC5BE72C5537306B53BF1E65BD4AAE60AFA2794B345C74EBD9B2069EB5 + 506170EB99B2269EB540D6B63E64340EE156AC91C9FF3B13D7B0AC73E55C1D65 + 6D4BBDE5F405B26E9EE35A4EE101CA391EE44E98A9B98081C0941EEC4EF7A077 + D26C2D9B8DF7D0D75CC2E93A6EE1A6E4FD0D8E5FD4720CB980BDB46FF41405BA + 405E5500BAB16C2C5D37504C19E705CE7200C7B2B0D00A84FB01770DFA1A00F6 + 6DEF081E9DC84070A37E23540DFB7AF80BF8454DFA8FD2D25CC18D42BD0E61A0 + 7B0E710F0D1CE39A0D92F7D44BEE377B0CD250D084856E845B18A02CFDE40026 + 2F703DCD051CE06F570F7C0D001B1436181C03600BFF9C058081BE753BCBBCDB + 00C0C05F03C0DA56889B13A0575DC0C061A9E3FED5085F805DA06F500DAB07D8 + 1B8786CE7308735C4B0F810D006B992100700C7F2DFF2F797F630180ED5CD589 + 9226AA4AFA2D406E0AFEA224ECB3C8202E7D56EA38F530168BB1B2007016FC45 + 09000E403706BD85940580D9234B435DABA7153B76D390D7FAB2C610E7E5F5E2 + B9F7D56BA1E7A58FFA7DB846D94BC4F94B44C03ACD1DE1A16BF07BD6678FD0EF + 3152A64341DB584EA50170D57ACD13706B7D2683B931F0CD02C0F1585C477703 + 009719DE993DE8B00F9D3507D9F94BCB03DEF2541100AC63351ACA7CA9037FE9 + ABDE581DBFBC7F83BFA83210B88ACCAD52DF55ADD5C455278D21AE4C9CC035EA + B9AAC0E05A0DA5BF89AB26EFB38AFC7E7E8FBE811E037B286B4FFF4E5471A6C1 + 9C6C65711494351755640ECA9A131F7B2F95C7B44AB131D1B70000DF57A9B6AB + 2CAF0B080602C763F7D77DD0357AA0B542E00AC35F64AFF5B5AAD25D04C0E93E + E69AD263A252E7FBB60160FE01F3DAF2C718402EF0B7C2005825FD617EFCE1C8 + FBB0647C98BE0D8ABF64EC4BCFBE1CD3CAFF622C5F7C61679D27ADAC636F47F1 + 85C12E14852E16C9581A006741E0745FA90BEADD577C21CF5A08C490B72C5536 + F01BDA0A8923F81B2B06BCF902F69A4A8F03811504CBB801E1DC228CC5992CFC + EAE5C06E59E09752216ED4AE88F201B05FA4B298A4B485AD2D680DEC7E55008C + 62E8AB105845BDB49863003801B2B102B0CD73F74652E81BD7C339D2B2F3C4B0 + 97BA8DD1AED94C6E9244067F81C1356837EB96805DABC7ED3807B0B645B1D317 + C56D83B869000C1CA6445900385616008E453868003025E0D7CB435E530C8069 + 53AFD785BCBD83F300705D5300BC8500B0416005C15AE69CBD1ADE59E4433F7B + E81B2B0D786310DC80FED036252098B2672EDF2FF03706C009F425B7AFBA7D3D + F04D24ED38F43352E80BECC5ED0B140E6E5E83BD06802DBF6F5C4F9CBEA21802 + 1BE405EC12B2D9202F219C15F00E9F2E92F923A6EB38C2F96B90F8A19172DC98 + D9891406531F37C73527CF6FC801DC8C36AE5F5173423F0380272D742DA72CF0 + 2E600030A1A001C0805FDCBFD3166B18E8D6E4FB258CDDCCC50A7DFD269768C6 + 22DD30236F9A0F9D879362A92B9AB3DCF9907AB82A8A5D4B8E953A90D7A06F52 + 12B66FFE4AD769C14AD779514992E397307E45F3E4DCF3706A2C55F0FB30E19A + 97AED68D3C1C1E0A808BE51872002BE425BCB301DFB5EAFA45E495EB02F05DBE + 46CB2EAB083318397C438867AF47341C745719EFBA5AE60400FCF086CD2A5F27 + E4B377BFB0116A9B9CFDB63CE506136A71D313BA29DA9BCD5275D078B72FA539 + 8001C0039F7846A59BAE22E0AE815F44A8E781B878A54EAEDF7E80E1A708FBB8 + 5DC1EFE06DBBB444405F9D2B63E4071CAC4E5FE0AF8C499986BE369600E05DBB + 12083C7087CCDBB5DB0DDBBBD78DD8BB4FF4821B2AB25C8584B8A4F410D83B9D + C8E70BF8C5F1ABEE5F00F0CB3E74331BE5B6716E7D6CB2B3E16E9BED2323E84B + 5DDB2F49FB65A993A7F1D557924DFBEFEF1F93093B2B2A8305406003C1E3DF7E + DB4D78875CC04008C2941E74970ECDC93CFEDBA23F3F364A818EB9FD1075C08F + 415F03BF28EE33006C4E461BA72CEFE797BFF500D8035C734D8A2E503FA7F077 + C9E50B6EE9958B0A7E81C0E4FF5D7AE5B2D47DE867002FE740E6BAA4CFCBBB31 + 81BF5E405740AB0F270CD4F3B96C817797DCEA1BD7DCCA4FAEBA924FBD5693E7 + 5744B8E7D5376FB935B7BE706B6E7EEE565FBFE956894A6E7EE1B6FDF4CFDDA1 + FFF41BB7EFE7BF72CBAFDD92D7B8E5D67DF15DE70130B0D43B5B7DEE5B00B087 + B40054044C35700BB8049C96DC94FECFA4FCFCA28260A4219ABFFC44DE870FCD + 6C6DC4B19C07772EE7A204E2E2EC5DFFD9D504006FFAF2BA965B14D25ED7F33C + F2079FBA477FF09996067F374ADDBF5780EE8DE4354CDEF5EBDFB739822D6435 + C76EFC83EBFA7B00B881B5C05F73F0027B090F6D21A381BE1BBE734BC6704C03 + 9C6FCAEBE55ED3DCC784A45EF39907DEBC2E32F7342502A0DB7C7D4FFA6FECC3 + 45F37FE0FFEFBD633986BDFC1D50DA18B2BF0FFB1BA28E14DE06B86BB2E36D0E + E326E6CF0FF0973A0F28704E00EF928B6715DEF2B931D73C5018C8ABF037C05D + FB6C1920A6DF9CF6763C6DFB7CA2991F78080C60661C68CBE75A01EE69F97CBF + C7C31D7C7E3DFC9D7BE6B4CCE3B53F768FC8DFEED37FF443B7F1CB9BF21A1FE8 + F700C2F58B00BDC8FA01C008F03B81DCBD270E278E6000F0B49347DDD46387DC + 34D18C1347DDF4E347E57BD387839E06103E2673640C27304E612BCDFDEBC341 + 1F90BAE50A7E5B1DC07C1FF31D0DECA504EA1ADC35286CA017301CBB82117368 + 33060036A7AF0F072D73989F02C05C8746CBF568DCEB729C284E47C0B58BD2D2 + 18C4D7319B93372F02C07979F0C3F5913EAE9FF6309501604A8030F98179F06A + E80EE9DFE60130301830ECA1B05CA7830BD8C3E1DDD2BF57EABBA56F87CE1BF3 + C2EB6EDA5BF27FF1C601693FEF7ACBBAA4D7634FBBDE8F3FEDD344C89AC21E2E + E3C132640F9E5137D09B05807104F70518E30C96B50FE22138D6413D651DD473 + BD0F0D6D60180185090D9D0060D26210198570D1D2070046B970D0250A813BE1 + F85DB6C1755C222A967E4241470EE08E4B56AB92360F03D25E8C1B78A503EEAA + E3571F2AC40DEC1F2A04026BBE60750BCB5A733E2098C8350063E02F0F280282 + 697BE97A748EAC75672D74AD13C9FA58D46AE60259EB0282658EAC795B4C5DE0 + 9A4D9EEB9A4F9DAFEB66D452EA0A80A7CDD3BA02DE89B364CDEDC3429317D887 + 769EAE63F4038ACD25AC9058FAF3050CB690D1335CAB8933D5F94B4868423F23 + 7204B71E3F23C9036C2018A09B25CBF99BE7180E25EE5F00B0CE1D3A51EE5F64 + 3EA077602E0FB041614A2031F53814742C9F1758CE3190362EE1007F450DFA11 + 1ADAAB519F11792E607307C76D7203DF0FCC0DF98001BCC05EE0AF415F5CBFD4 + 29CD095CA7BB1C2332A76F0C7C150C4B5DC16FE77E0A7E0DFEC6A541E19A2900 + 1CC35E44DBA02FA5D54D36AF3AEDE0F025DC33A5B98101C0F421606E8D36DD5D + F5D6DD54D45121F88BF4985632BF65140A1A518FDAC05FCAEACD3B0500DC3E07 + 801F2A0C80F55885C03EF433CA7201C7B097B61D63631E00B32F23AF19E6C5AA + 0800B6765A559B882A008009136DB98263006C10983D329341DBB80FC58017D9 + 3C545EBFEDC551BF2FE5FEA55EE38122D748FE0E1AF04080BC27F612D5FDDBA0 + B5868636C86BD231E93799333837877A3305C080DF5859E037563CA660379A1F + F7A7555964F097FDDB58E9BDDC422A6F6ED65E7196E27D65AFC615D2ED00E04A + 00609B57ADA1AB52CD8781AE5C535EBF7A1357A5C683FF2F7BFFD965C791ACE9 + 820490D05A1120544227B4D600A1B5D65A230532135A11D45A6BADC9A2964501 + 4182BAC8AA3AA2BB6F77DFE9EE3B77E6C3CCDCBBD64CCF879EDF60638FB95B6C + DF913B0150549DAA739A6BBDCBDDCD3D622713B977F88E275E336946C64AE265 + 5DA4AC25A99D757D590755DB00805BE30AC6F91B9CBE4D9AEBB99C0BFC067C20 + AF3C7FC85EAB844A7392A094A3E475A5F5A5E65DA5D6A4C7FE2555C4B4322616 + F957A3CCEC724A18DC9594323B87A0F97EAAC6E24DDA48D366EDF56F91FFA7E2 + 75ADDA76934E5DFB588B2BF8AAE02FF273FC55D5E4B701C0D8EB1BC4AE744C7E + EEDF3A004EE46F8EA2374B8937D3DF83D20F99C2875EF850CC2BFD60BC1A953A + 4729953AF6E72885BDF43D5D44BA269D6F1400E7816F7E9C3C7DF59752A90B79 + 2620EE550260D4B4B56E54543606F6F2545684B79707C0005DDD40B5D18D9BAA + 69DB9082C54170EA022E0D80D99C15002DAD01603D670A7FD339948E695D1EBF + DC38C422E8D50D256D5997829A75D679D6B03601C0887100C139E99C035F36DA + 9E8E39A464A68DF3F42FA7B8BE49D7A46E6FA20CE276E78B4000B6A91AB88269 + 759D2B3F46006083BB2A77057BAC01042E018053A76F0A85897BDAE73C007639 + E47580EBC037EDFB18B8EBEBF200D8D51A45E08BE893F2D9D56A004F1F3B002E + 805F6F5DC05F77F9D266699F8945286C603707808302FC45BEAE6D851E337CA6 + 7EC90E60B7FDC802E835D83B32C05D9B8B7103BD805B5507206E1480D7C55C51 + 9FB551005F5A00B0C15FEAFB1AD45D686E5E73FD6A6BE0D7A56B52006C31DAE8 + F6CD83DF1400E3FA2DEA47C84BCC9DC280E09E11E202791DFA02770D06CF5D23 + 3D676B4CDB6B67AF94EED7AF90EBE6ACD6F85A73F9D2673DF3BDE7AF0B699E55 + D7CD5F2B3D553D6817AC955EF3D6496F8D037DDDF94BDF2070ACF58BFA2C2DF4 + 01BF7D55BD89037F498B67F5D1C28D2CFA76A34B5BEBAFA3F6EF76A1167039B5 + D2D66A9CDA6AEB71FE86F47AF4876CDA9341E02200ACF1A1A46DDE0C00DE176F + CAE9FC263D370078D34EA9C001BCF3800C07000388D13600F05EAB033754E3B8 + 7C81BD0E7F87EFAE9611BB3DF5A0B67BABACB62FF017E83B86D4CE11FEFA78A4 + B6B87C4D957A5C6504C03908CCCD4D07C0805E6E828EAD3D96F5C758AAC4C3E2 + EE5FE4757F43DAE7E332F9F82999740C776F48F1EC00D86BFC02741DF0027669 + 3DED33FDE9676F95C927032406064FA1A66F4CF73CE56C84BBDAB7710282B3F1 + 8D37C9B49B6E0E8EA45BC34D6ABF611D00F01D32FBCEBB63BAE6BB34867B891B + D5C11D4CEDDFEB751EB9FB77861EE337CA673BC84D6EA4DB8D7695C76901C17E + D3DD6FACD327ED33E0D7E12F37F181C19BEE3FFB8B6BF3FEF393B38AD285BA16 + 3EFE48062670AA2D7FEE49F9F7CF5C5FF21C7F2F3AFCD21D067200449EE275FD + 1BAF58EB40C91DBF06975E05FC008BA3D350632984220604BED27F0060076C88 + 7E70F402CF006901FEEEFCF80303C1DB3EC0014CFF03D9FA3E10AE508F15B846 + 0B7403C63990F3312D63A09F0360806050807F07CE7F22FB3E03027F60A216F0 + 9E4F3E925DBF0FE99FA9016C30F8B3F3BA0E07F097AA4B72E8BB1FE5F88FFF20 + 07BFFC56CF7B5100C3004D6025A98F83A335C05962C049079608D009E0B4D8E7 + EF18FC3D70517FA6F3EF6AFB81547D1953355F04B60234DFB7B4CE405C802BE7 + 36E81B1DBA08707B505B0030AE5F20B003E07A9DC305CC3CE0170770FD37E74C + F489DB39F535FD35F8B91DB0D2F7D7F0FFA7BDE7F87F217535B03680E1AA2FF8 + FF2A00552070DD3717F4D873FAFF1CD23ED77E7D414EFDF9077DED4B3AFF99FE + AE3827BFB3423D637F4DE4703785D1C8D7F9EB33C7CF8B23987F03C4BF3DE2E7 + 010AFBBF3F7F1F291C668E3E71FFDBF4BF25FE46FD38FFDB73F9DF32EBBCE5EF + D2E1AFAFB1FEFB01021B08E621867743CA6780AE8B31C0371531C02FEF35C47B + 9296F53EDEF816AD03DF008001B7BC9F37BCF9AAC679B8E3395DCFEBBC211BDF + FC9D6C78E377B2FE77AF9BB6BDA77F7BFA77CFCFC5E7020F83F8F19CD301309F + 11C4DD1DEC20986370062F79FE29596E5904F4F3E2456D5F78C6D24FAF7C81B4 + CF4FE8E72750F8691B2F7B36D40276008CF88C35D81BE34B71174777703E3303 + A0D763B4FEB94D9F3907C0C062179FEFFEB9BE40CF610208038F1F0A9924905F + 530C08DF1BAE4B5C77D082071E2CBA46A5A097B15FA710D73507C35CFB48036D + 6E61D6733DE4BAA8E22129145243D3BF47AF9B771B0CF6FABF85CC1A7A0CF584 + 6F02EE06F84B2906944F0FED02064F3E43A60FBD76DF72A72CBAEF61136EE0B1 + 474FCB98C327642C994400BFF12133AB039C7BF00C8DA72630EB81BC75BAB749 + E02FF25AC1CC93127A44257BA4E006C609CC43705E0FD85343FB18081C5ADD6F + 696BE057E5201827F030CBD4121DC02A83C03BBC167080BCFEB09F03607303EB + BE8F76A0EE0F07DA1EB20077EDA1C1CDBA6F2446DD5F629BF6CAE02DFB748FB9 + 570631670F1FB21E5730D03700E1412A52450F58BF55FAE1045EA3FBE2B505F5 + 5DBDD1C0F08075BA4F5EB3C5B2E4745BB85ABA2FD27D3C2553566C3218DC87FA + C05E4BD840F07ADD6FEBB1D40F26B5F432DD9B2F5D6B2D0018F5D7796A05D332 + B654D111008714D111FE2E5DA3FB78FD3EB070A5B981AF9DBDD80400261574AF + 79FA9DE3320018F0EBEE5E4FF3ECA9A35D806073FF1A24D6EF3268FA22FD9E13 + 6A02A7EAAE715ADCC00684230876F8DB6D72A8051CC605F04B0A684F039D69FC + 2CE9386EA6815E84E3D701B08F49070DFC451DA915AC6A3F6AAAB59D18138F62 + 9CBA815D29004E5DBF067B87A9B4A5EFF0B7F590F1D6CF00F0E071FA3D77A274 + 1CCE77E46200ECEEDF3C0046E91A5A03C21A6F430CE72F29A007EAB9630A686A + 045B5CDBC600709BFE00E4CB03609CC02DB545068355B46D626B40B7CFD090F2 + B9D720153017189B07C0A480E6BE4841805CDCBFEE004E21300017686B7058FB + 29FC4D01B0AFCB036087BF285D67B581BB5E0D00C658F02B007097689448406F + E11E5A62A04880AECFA7EB7D2E9D4F63F9B1C7AE8930B8BBFE7D4C9CB34AC64C + 5B229D7A0DB7FB95CD3B979B00BBD4062ED37E1999FC3AE86B945000BFAE8600 + 18789B07C0A9F210381D1BE04DE245E03759F76B017029A5F786AFEA5EB1CAD7 + 15D4F5AA744500DCBCB3B5067F7300B829A0B7457703C0E6FED57193665D0C0C + 376FA5FF9E16D3B5C61E3AEA5AEEFFC32268C33D7A6A06738F9E1AC0DCCBF754 + D0BF052B40297B70A5F3CE08F8394A7392A074CEFB692CAF2BCDA3C6D6A4F1BF + A48A9856CABD223BFBF9103861705752CAEC8A406854BA365D938B37D158B3B2 + 90FAD95E3F59D74C632D5AEBDF630BFEFF0AC7662038595B248FFF55D5E4D703 + E0264DDB4853558375D7507C3BA8D471D7348BB27E1EFEFE9D0060A47DFBC7F5 + 3FC8F847FCB35340276BD23749F66629F166FA5B51FA41927E98906BBE58FEC1 + C79A864A3F24AF46A5CE514A8D1D978F5F8DF8F97F33008C62AC097D07C0BF11 + 04E622CDC6208BC5F3165FC87B042560170182AF69A1F3B978AAA6AD7523D246 + 373600DA086F5339C02DC8E1AF6EE60CFA16DAD027CD73617D761E3F577CFAAE + 69075DDBA1007633E72F00382A05BEBE36EDFBB80077C39CC7CA3AF56F70BC39 + 78E346D436A69D80BD6C5E6981BA0E80FB271B63EAFD06085CD65937D4B18F9A + E83C0A00970D78B19A7609E9997D9C42DA54B87E11F017D958E3066E63DB98CA + AED52F10893C4EDF012E7D03BC2A5AEAD5A4A0973E713FCEE30E8101C0D4F875 + E04BCB385FF7D7C67D464B736D53E0EB00D70500F634D01E03E4A66B4A4B5FB3 + AF7E89348514D1AD70F8AAF2009834D0ED074F9276AA368381BE7C190DEE5F07 + BBEEFC2D06C0859ABFBECE5CBF005DA070E6F40D757F01BF0100EB1770C0F030 + EDC7B6ED30FDA23E4ABFE4036B81B34059953B79CDC11BFBD93842DDF611E2B6 + 1B797DE6E63571AEB1F3A4CBF805067753E04BCB1C224EDB2DA67506EA0277B3 + B4CF2A83BCAAAC06704CFD6CEBB575F84B9AE7CE0900C6E9EB50D89CBEB87B35 + EE00D8DDBEC4CCF10BE855017101BEEEE80DAEDF15D273AEC6E7AC36B88BCBB7 + 3B2E606DB374CFEEFA8D0018C8DB3B42DEEBB4EDB1709D5CB768BDF4A1E6EF62 + 15EEDE58E3B74F7402F759B6D96AFDF65B4E9AE7CD59DB4B05FC7500ECA097FA + 680060A0AEC1DD08810DF272E36BED16E9BF5ED7ADDB2A7DD7685FE3C05D202F + 7DDAA1DC6CD3766084C00336E0C6E0E65C74006FA176F01E19BA75B754ECD0F5 + 76430E697F9BC6B6EF9561C05E40F00E523D53EF77AF0CA1BF4B63E61A212D60 + 04C07B6B64E4815A03BC80DF617B48FB5C6563EAFAE2F0050C8FDA5F13C6D5B5 + 96EA39D50815D097FE286240E3AA4376B313D0EB3745B9113AFC409DB5D4FB05 + F88E39782883BE63EB8E587FBCF6C7113BA2B1C3DA46F7CDC463E126ECF4D337 + CBB43337CBC4539EDEB9008291DFC0C5F53BF1E40DE6F805064F3B7B8B4C2685 + 7304BC79003CFD2652430667302DB0D71D49005F5A6E543B00F654CEDCB44EE3 + 68C66DB715CDDB396EBED56E7C3BECE586B8DF1CF71BE7DC64F71BE5C41CF61A + F08D37E0173D1252327BEA4E573A7EEAB19D25A1E79584ABD7DD63EE2063BCF0 + F1873330411F2051EAF8BF17BDF5C22E833A0669220406F4E0FC0BE028B87953 + E094425E07502E07C1C4FFCB7FFF7F9BFEEB7FFFFFC87FFDFFBAFEBBD5FEA5BD + F8BFFFB7087D0BF552C338406003BC1FBD670078E7C7B88081C05E0FF84DC1D5 + EB203785BEA91CDAB9F33280BE502716F9BA2CF6D907AA0F65CFA71F9876C5D7 + 0502EFFDECD36227F0F92FF57CE7F467FC4C6A2E7E2D87BEFD4970FFEEF9F482 + 9EEB6383A2A42AAEFE92B4C6C1AD0BD4044E022C03A07C57C780D6DFCBBEF3FA + B37DFE8E415F20F0EECFDEB65800C0D4E60DB57A0FE8392BF518EBC77371DE83 + C9EB3800AEE2F55500E0435F7D2E47BE3E27872E7D6E20F8F037E7748DFE4C17 + 3F36107CE4FB8BD6F23AB8877103BB23D8A12AAF939D9FF817B8713F94CA2F3F + D49F39806DD6D6E9B95913402CF0363881EBBFBDA8F3A47CFE488F3F2767FFF9 + 8F72CF7FFBCF72EA4F3FEADAE00CE6FFC7C1B34360CE8300BCFCDE1C0233E710 + D841B1AFE5778B0B99D776000CD8054433A69FCAFF8EFC610107C32EFF7BA2CF + B918A7F037FD1BF6BF2BE60CF86AEB7F9FE95A4030A03505C10E80DD0DEC4A01 + 30F3BCD7D6C5872F58BBF12D1DBFF1A26C7A47DF876FF11E05DC86F72E0F76AC + 7F8394D1D4100620D3D79F43DF476B5F7F5556BFF28AAC79F55559F5F2CBB2F2 + 25EAFFBEA89F0705A8EBEE5F3E1F3C05347D5A602F73FEF9E17D8EC3016C716A + 8BBFF08CB5067C71F43EFD84F000CD4A8D038097E867E922FD6CA55D1A01309F + B18BF4B376319FB92A87C1A47FCE0BD8EBE2B3DAE1AEF71D04037E49FD4C9FE3 + 9873C730D78E007DEFD556D7C46B8903E0F90F6A9F8C14A67B65CE7DC1DD3B3B + 5EB31CF466D7B17B43AC1400C601ECC7DAC35311025B660C03C064C9000073ED + 0C1018201C549C6503CDBC598F8FEE5F04E0E55A3F15E07B43A8090C000E75FD + 6F35103CFEC4196DC9F271B34C387956C61F3B9DC91CC01A9BC45E42F7110E80 + DDFDEB7DE2949E187B88FDCA7103C2130E9F0CEE5FE06F04C2B4A37109EB9E87 + BD8FB9855523AB82EB17F8EB5952D837A56E60DA61FB7005931E3A80E0617B75 + 9F460DE0BD0775FFA66BF6EADA3DB552B1FBA03982876C0BE0D71FF21B660FFD + 51DEA35A46EA710680753F39909AC0B882490DAD63F698A487B63DE6E67DD29F + FDE7FADD3248FB83B61CD0B507CC418C23B87CFD76E9A37BDD7E6BB7D9FE14F5 + 5FAFFB5CDDDF96E302D6BD2E758107ACD7D614FAC4FB5AADE02D61BFCCDE59F7 + C9F401C0BD753FCD1CB584FBAED43DF88A0DDAEADE1C00EC601810AC71DCBF96 + 1E7A79A81B5C4E7D611DF78A0098F4CF388001C0E8BA852B4C3D69632A68040C + 46D407C6116C2EE0B985BAC079B89B425ED6E21CF694D08C591B00F1E2008167 + E877249CC011FCBA0B987E9729210574A80BACDFB126D3CE952EA47F9EA2DFD9 + 2C0DF41CFD7EA54A5240A740D8DCC0EE04CEB97F01BFA47DC6FDEB5018104C1F + 10DC7AB87EDF654E635D3846E33883DD1D8CDC098C3AA23C00E61CDAB6D51608 + EC0018F80BF0A5B514D051C06000302E6060AE43DE140067B141619C87BFA483 + CED6E9D821B0C35F5718EBDA240DB4A58206024710ECC0B751018E55ADFB8E94 + 3639B50212F70910B8656F20B08A1AC0D70D9156B1F66FAB5E387F630AE91C00 + B6FAC139018181C2C05F64803802DCBC1CEEE6953A7F3D46DF0030EEDE040037 + 0682CDA4A04A81AF0BF04B9D6083BE69BDE02EE57A4CB99475ED5F10E328E070 + 8B6E034CF4B9A7E630D8E75D8CB3FB6EAAB45F4AE9F180600070E7F2513276E6 + B200807B5608A695E65DF4DC11F8365735CBB97DF36A520480AF33954A019D8E + F3F23987BDAEC680AFAFE3759A1AF845C5F03755F1FDDDCBCBD767F78813F97C + 63E76C784CD7AB52A3E0D705C0D516F8DBB4755CDBBCB309A72FF27BE70060E0 + 6FAAA62DF4355A247C0086D21C1770F13DFB667A5EBB97CFB9758DDFDB4FD7FC + 1265AF9B289D7746703900EC719F4BC769FC72C7343697EA4AF3BFB58A9856C2 + BC32F8EB4AE72EAB126CAE31455E57D4778E97CEA56B7271585F5393BE76B226 + EFF4655C12FAA6FD541EFFABAAC9AF04C03A06FE5E0900171DE32A097D53E90F + 98FF25FDAD895FA2B65610DAFF20AFF8C7CB1F7E5E85F9F40D92AAD49BE96F45 + E907CDE5E410B8D407244A3F24FF92F2D732509B8CD398F7F32AB52E55B6EE67 + 0060E06F93F4427A2500EC90F832EBFC225D74818FEBC358E74B405DD7351100 + 676AE00CE6BC61730000A64E6F01F496926E72DAF596B2F67D55FDA4491B606F + A18F13987EB3767DC27A5CBEDA5E09006770D68503387101BB52D04BEBF31E47 + 6580DB087EBDF5E3EDDCACEFCCC613E931ACB175B155B1F1CDE2991A02E03200 + B0CE390076D09B6F0DE06A7B39000C7875089CEFFB38F403C045BEC6C7806093 + 8D030C2E02C0D4A7D1B6790FA471DD40D39A0BF8BA61F178BE5CF08526C490D5 + FF8D00D82170067B01B8F455C44BB9783D96AA45EF91E61206000372DB94EB17 + C1BEA3A5AC5738470B5DD32AC25DE20E7B5BF7D32FA0FDF48B65B97EF92C1F2F + 6DFBEB97C8FEBA2E825F87BF0E80DB0E06F622E06E00BC01F286B4CE08C06B2E + DF218CA759DB160DD6F1101D5B6C9AC5DA4405D83B43DAEA1C22D67628D05735 + 5CE746CC88AD7E6107E08E98256D753D7D6030B097988161553BED3306FC026E + 81BF0E7501C0F4893BECED18533A03811D045B8A67E22A625D01BA1303CCCDEA + F9262EDF8E1374FD443D9E391CBF005F64699F03F835803B7D99015FABF51B01 + AF016080B0F67B44000CCCB51AC02A07BC9EE2B927E0D7A4FD5901005B6AE759 + BA2ED6F535003C6FAD015F1FD337000CF80500CF5F67E99E49F18C3200BC408F + 0300E3EC05FA6A0BF0EDB764B3F4C5F94B6C39E99DB74A5F6D7B0387B5050833 + 668EB6EFCA2D06793DED3337B282E337387B0D0E73836B8D1EBB6E8B0CDABC43 + 066EDC2E7DD7E86BACD92C0348A1B779B7F45FAF6B55382900BAC4E8339F4160 + DC189B00C4DA275DDFF65D52B10DF08B3343635B77D878D88E7D52B113C76F74 + FE6EDB2B4319EF39600E5F6AFB72731037092906C71CA89731805CD23AEFAD94 + E1FB2B6554E54173F792E619003C7C7F4D00BC11F4027DDDFD4BFDDFD1070FC9 + 48ED07187CC86E70027F47D61CB61BA07E43949ABF63EBA8EFAB22B5F3A163DA + 3F2193B9F17A18E70CC0F7848CD7F97187F5386D275017F83837614FCBE41367 + 64F2C91B4C134E70C3F6944C3E75D6523A4F3903002605B4CE6B4B8D5F6A044E + 3797D0AD11EEE6523EC7BE6B3A378901C137DF62D0967ABC389602D40D2E2583 + BDCCDD196E58B30E97B0DFC4666CC7DE7E4711009E7EDB6D56DB971BDF7EB39C + 1BE1C86F9073939D9BE63E97D65D0400938E73E9138F5B2D5EBF41EFEE2DABDF + A82D6BE8FFBBA77EBE43F7C1A7AA83334C8F77006C10F8F1470C4020E044D5E3 + 37943CFEEF41FFD767A719CC01D8788A5877000389004D380953C8EBE9681DF6 + 6E8A63D620FA1B626CA3AF0358BDF39A41D800BE5ED77E704502C31C88855800 + B5C86AFCBEFF9641E0BD9FFD5E0E5EBAA8FA4276FDFE639D0BAE4C409C83355A + C60EF23CCEB937C67AAD7E0CB017E85738EE033DEFBBAAB7F478D668CCEACA86 + F4D0682F69A1CF01813F33EDFBFC7339709E7AC0E7A4EAC245A9FBFA1BA9BAF4 + A5D507AEBC808B16181900651E6A06672C103AA437AEBEA4E30BFA739D0BAE5F + FA0E8081C3353A4F1D6097A544BE18CE8F6A00B6BAEE409CABF9EA13A9FD4A5F + EFCBDF4B9DBE2EF0F790BEF691AF3F97FA2F3F95BA2F490BFDA9D402792F69FF + 1B9DFFF69CD4E8FA03E73FB0D7A01F20B0B676FE784E3D0EA86D305BD7557FC5 + EBFD5EAABE04B406F76F00C4A4752606F8663DEE64DCC4C1110C0CBEED3FFDB3 + DCF57FF98F72EC87EFEC770B98E577C3F108D8CCB832FECE1CEE225E27FD5D7A + 8C7E48FF1CD24373CEF09AB8AC81C8B40063FE5D18871AC63B3E06FCE2182735 + 7880BCC8E13035A469D3BF2F8070FA77B645FFC681BD8C910362FF3BA34D0130 + EF890C02ABB612A356F03BFA5E7857DF2F39208C36BFADEF1FDCC2AA0D6FC5F7 + D81BBC5F79EFE97BF0AD175538F35FC8DEA7C1CDEFEF6BD23DF31EE5FDFD8AAC + 78E90559F1A2BEF75F7D55D6BEF69A69F52B2FDBE781435E87BB0E786901BFA4 + 7DA60DE70F9F23DEAE7AF57959FA624807BDE2F9A765A5CA9CC03125345ACA67 + 29EE5F03C28FCB02FD4C5DF8C42336361730503802608FF1D94BED606469A49F + 7DC65242037E3D35B47D5627103885C32EE02FEB59977EC687EBC97D7A3DD16B + 8A5E4302104E5CC00F863E9078F67D7A1DBBF71E03C2F335C6756A3640370261 + 83C2F7140360E668A7F37053BCDE5DAF2DD7CD0C00EBF59236158E60ABB77F1B + A0F80EBBF632B68C1BF6D05650A8077C935DC7490F3DE3C6DB0C007B2D60770A + 03813D862819E15943D24C22B43C48E62E60340187B0B6EE0E060023FA06868F + 9DD135A7C27EA75EF72E3885B5EF0FC1A5507894EE8D2897416614F64A232AEB + 0D0A3B081EC5B852F75507A8115C2F157B0F9A86EEA9310DDBA37BB03D75BA3F + 0302038375FFA5AAD8A5EB7655CBB09DBA7723C38B6A988E2B78E84FDBA1DB2B + 75AF7840F78DB88155B885B7EDD73D645085CE0FDD7640F79CBB75FFA97B515D + 3B786BA590627A90CE0FE08145B2D390B166F35EDDD7EE907EEC753506041EB0 + 0110BCDD1CC1A48C661FEB596BC87C534E29143D6684BECECCFAD332BDF6A43D + D4882BB84F2C9DC23E9A1AC27D576D506D8C103880E03EDAEFBD6283F402F652 + EFB7C8F1AB7BFFA83E0680898514D0C0DF1EF3F5FB870A089CD6004E21702603 + C14BB3FAC00E7711EE5F40703E8E1C10779F495AE8007FBB4CD3EF4D5349FB3C + 5F5BFDBEA5EA3C65AE7E67C2FD3B4F3A017EE9DB1CFD391A9B2D9D01BF3A4EC1 + AF2B7503D327D619E83BB6D8090C00C6E98BAB17D0DB41E56EE0B623F5BB2CEE + 5E9CBFC4890D9F64A20F00B6E3A23AA3D1DA1F352D83C00686756D87E8123618 + 5C5170003BF87527B0A58ED63675003BE4CDCBE681C8DACF80AFCA1DC22D068C + 96E6E5A3A4657FFDCE5F1EE4E99F2D05B48E0D06B3DEC16FFF91D1091C5DBF31 + 557429B504F2DA7C5CD37744A80DACF1D63A6EDD47633881FB0CD3984ADB56BD + 2B54B451B9B4CFB416D39634D2A9FBD7E1AFF733201C616E5E29F42D358F1C06 + BB23388B4508EC6ADE5DD7E7607019EA8206483303BC110A5B4CD7C4B8AB6997 + F20602CCBA80B32900F6780A715379CCE7911B325CE95C7A4EE6683BF6192E9D + F5DFAB79A77ED2A46D2F69D691FB7AC1DD6B358131A9E4406F29101CD453E703 + 9805D0E6016F1A4319C88D5017D0EBEB7CEC73289DB3791481ACBB807FAD1B38 + 7F4CE1FEF0950130B2FBCAAE965D8A05EC6DDDCD64F037C68B0070F3CEC57D1B + 875829009CDD432FEB2A4D55CD5A749732EA0273AFBD4927B9A629F7E1716002 + 8AF5B866B005EECBFBBDF9709F3EBDE76FFCA15900C08DDDD74F75B93576AE78 + 6EEFFBB8941C02E75949C6579A7528DD4FD4E09824968FE775B56B4AC5DCD817 + 607BF17C632A625ACEBDF2F0F78A0C2D558EC95D4E91D75D4E0E6E1BC05BC61E + 372573AAA2631A3B4F9C2BA9E45C7F3D017073E0F64ACA006E0A7855A5D6BAB2 + 63529584BEA9F4072CF58BFA5B12BF446BF51FB9E41F6729F1871FD4443F9C0A + 2A7E73E455EACDF42F2DFFF0684CB62EF9D02AC44B2BFD60BCDA0FE3C6E4C7A7 + E7495FE372F1FC9A543E9797CF67EEE05200D8A12F4A6245F037A7D2175A3DDE + 95C655E9C53995CDB78E6B5AF5D063753391C1DC8632004CDB0000FB6680D742 + BA4168AD9B1B83BCA51552410371FB48593BDD98B5D78D9CB600DF329CBFE620 + EEAD1B19DDC0E91A8E69861B58657D730D6B1B534037D18D9A83D914D23626E0 + AD435E573ACF98A718CB3AE9861160DBC85A1BF3B46047FAAC0B72E7AFBB7F8B + A59BE3087C01C02620B06E98DD01EC8017390C2E05801B136EDF3CF0F554D029 + 00CE60AF2A5D9FC603F4D52F1CC0DE6B2B0CE89A7AF2E564987E09C1C11B5406 + 048EB1E6C05E53986BD90BD01BD53B825E606E84BEC8A06F54594F3D5F84BA79 + 00ECCA00702F1CC28C01BBFA85B07CBC7EB9D32F85405F6D5BE00626F5731C03 + 76DB0E9820ED06E817CD81FA8556DBB603F40B270058E75B47F09BA67D6E636E + 5FBE90EA17D741BA3E825E5302800B9AAE9AA1F3D3A5CD200030AEDF19D24EC7 + 6D87007991F6754DC711FAC59CF4CD2366E9976040B01E5301FC05F8CE928EA3 + 48F10CD49DA93160F04C9B07029BCB57D56AC85429D3FF8796FADA6D394F74FA + BA00BD41F449E70CF45D60ADB9845521BDF3C2087B174AE7088371F3769FBC58 + BA4D5A6CF12E89FBD7216FC7893A06F64E0D0EDF4E93165AACA3B6F471F7027F + B374CE3A76009CCA63ACB3B5493F83C011E822EAFB027F0D0827A017C84B4A67 + ABE54B1A676DA9EF7BDDBC42DC533B7B5A67EBAB7AE2005EBCC11CBD264FF10C + 08F67E4CF50CECC5F50BF0F5DABFB4D402EEB752FBAB58A7C7A880BD19FC8DA2 + BE6FF96ADCBF00E0ED2A5CC07ABED59B35B6D552EA0174FBAFD773AA185B8DDF + 8D0116936EAF0080F7DA0D356EA60DD9BA43B553866C0306EFCEDAA1DBF7C890 + ED40DF008201C2B84046ECAD8E2EDF50F377E47E523BD7CBE8FDB532FA40ADB9 + 7D47A846566BBC26C0DC11076A3218EC0098F870628C6BEA05E7EE384BE11C20 + 300E176E7CD2E27841DCF4B49BA1874F98261D3B691A7F04A7EF31EBE3F81D77 + F8A84CD43E02044F3E715A269C3C251349077D4275F2B44C3A753A38774FDF90 + F5B9E96B4E5ED5B4B3371AD89D7ED3CD32F396DB642629996FE2067174F6AA66 + 68CC55E4FC65DDCD21ED33D09694CDC05FC6DC8C9E77EF7D32FBEE7B0C0CCFB8 + F34E73F4028BA90BCCFA0637AF91AE23DDE5ECBBE34D6F1537C369FD66B9C7B3 + 9BE7F7863AC0EEFEE5263B2E2C6EC453A7911BF45EA711000CF0A59E23FDB9BA + 96F99D8FDDFEB35341573F7E63060E96A8163D811BED5159A82D00829A9538D4 + 9E7E7257C9E3FFD6F53F9E196EA99F1DE838B4713106EAAC7F3300DCF5C05CED + 6F7CFB55D56B2660170AF361ED3AD6D99A007D6D7D84BF0EBE36BDAB732A4030 + 310099435FFADB3F62CCBADF651078FFE79FC8899F7E90537FFA492A2F9CD779 + AF011CDCBD081807AC032EA6F0CE419C3B3B7DCE415E58A7E34FDF965DBF7F53 + 767EF2A6C1D7DD9FE10C06F805D8B7EB138E09E7247DF181F3B865A90FFCA9F5 + ABBFBC2095972E4AD517C1E50A38F5D4CE0144E2068E2E60A0A6CEEDA7AEED85 + 0FA4F20BDA50EF17372DAE5F4FFF5CF7CD6752AFAAD17E256B91CE55EB388057 + CEFB911C8C7DCE55F3E5C736AEBDF4891CF9E67339F6ED3939F4D5A752AF3FD3 + 218DD5697B505FBF9E799D3BF2EDE752F7D5277290DAC21AAFFA0200ACE7B804 + 780FAAD5F9BAAF81C0C1658CFBD7E0F3D73AAF71007015B01848FC95FEBC7A6E + 40F1C1AF00B9011A1FF9E14B39F6E3D752FFED1772E8BB2FE5F43FFC20C77FFA + 565F2BD401C69D0B282715352D7097DF57068255F42D05B5BE868166ED1BFCD5 + 39E207CE7FA8FF8E6FCBBEE8460642871ACC211D3569A181FC0EE6F9B70116EF + FCFD5BD6064731CE7262FCBDF0775278508036FC3D14DCC1C8612F73FCADA1F4 + EFCEE3BC07F8BBF579A0EF16FD3B47D63708AC7FD3C05F7B0FBD662D635B17E3 + A496DEF4DEEBB2FEAD908A79C35B2F6B9CF7E98BAAE765E35B2F85F7DF5BA487 + 0EF5B959477FFD1BAFCABAD789017A01BAB88475FCDA6B1A7F5DDB57B3F59EDE + D9DDBEA9FBD7E38CF9DC483F4F0C00BFF0B42C7DEE4983BD387D71009B1BF845 + 3D8F8E1DFC028111F017084CDC4131F3C8A0F1B3A482D6F54F07E00BBC75908B + 80C27C6E134F212F72F0EBEB6859E7EE5F978D1FD66BC7230F856B4CBCCEA0EC + 6123EB3F2073EEBF4766AB2843600F28F1E0925EAF18D39F7D9F5EDFB8C651A7 + 3E5EEFFCBA36E3CE3B64FA1D7A6DBDE3F6E8082E388079608A6BA541E1DB6FB7 + 87AFB8AE4EBB255C8FD154AEC73AE69A6BD75D1ECED2EBF8B41BD903E89EE0F4 + 199971D3AD32FBF63B0D0C4F3C794663BA2FB891753884434A68C65633F8061E + 1EBBC93286202B17114B49E006360180DDF94BD6115A80702C4D3145D722C030 + B1713CDC16F73AEC7DC6D585B4D0E6108E5018080C240E40980C29EC97D8471D + D57DD611D3E86AEA071F9111FB0F49C5DE5AA9C0E9ABEDB07D75327C5FBDC1DF + 0A5242B3B7DBAF7B33641018A76F95EEFF6A74AF7750F77F07A5627BB554ECA8 + 96E11A1BB95BCFA1EDC0CDFB55D40AD6B5DB6B6C3DFD81BACF04FE0ED279C0EF + D0782CEBFB6FD82DA376D7C9823377CAA2B377DBF9FAEA3E17603C68532A3D3E + B6388B81C50E9581C5A37656CBE23377C88293B70A254E7000F3C0A49552D1B6 + DFEA6DBA47D6FDF8AA8DD25B65ED4ADDBFAFD0BDBCAAF732D592B519F005FE3A + 0CCE641078B50910EC10B8E7C2E596FAD994C0E0DEF35718F4ED19EB03038271 + F83A0476C8DB53C518D7AF43DF345D34E31ED717D70546F4BB4EA70DA0B72B31 + 9F5375D379C41C00D8D6700C5038BA7DBB3AF805F6BAF33702604B050DC81D3F + 53FB33F5BB5D1C8F09299F0D068F9E66AE5FFAEE0ECE04C05539D47517700680 + B5DF8571067C27DB5AE6586F00784470030767B07E8F8EAE6087BF960E1A280C + 1C1E12DAD61104D35227B8D520FD7E0EF81DAA7140B2C61CFA3A1C0E0038B881 + 49078D13D8EB023B0C76B52EE7FEC04869618EDE91191C0E738563BC6F635D93 + 07C0CD7B0F933295BB8381C1AD7A0D37017F5B330608A7EAC53D92007F1D007B + 3A69E2EEF0CD402E6D84BF006287C0F9D4CFD9FA04F636ED3EC8E4B03785BE19 + F88D6D33602FF0B76B0100979243E1A65DB96FC53DA6701C0EE1269DFB5B5B50 + B9A949E77EA66B00B43A3657702277EAFA380F7653A89B8AB934DDB38FD3E368 + FDBC4D4805DDEE3A550EE4A6B03701C09EF61965F345C7F6D4583100CE606DE2 + E47528EC73F9F95429F44DFBC8017033740500ECF1FC7DE054F96350A975AEC6 + EE338718F7A3BB14EB4A00B879E720FA45E3C804620DE06200ACFD786FBD296E + 5F95F5FD7E7B339D573529D3D78CE326CDC29CDFAB77A5F7F439777EBE31F931 + A5E42CE1726B1A8A638AE5CC047E928D7F0600FEAD64FC49CFDD54FFDD705007 + 80AEAFA7FF9F4DB525D6EC1700E022C8FB4BE1AFADCD31B9CBC979DD655412DC + 22C61E2BA1A263AE325E14F3D7F9ABAAC95F0700A7FA570180F9E5158D0B757B + 0B7F98C91F6A9158C79B40DF44FFCA01302AB5DE3EDC13E89AF65DE987F32F91 + 1F8F1CCAFAEB967ABDAB55633F53FA7AB6265E90ECA27405009C5E58F32A75F1 + CD2B9DF7F50DC58538AE01E21ACCBDB20C0247F8DBB48D9E07C573366DC3EBF5 + C86AFDE6857BD7FA9CA3356E5EDD44B5EE2D656DFB4AAB0EFDA5453BDDC869AC + 79BBBED25C634D5AE9064AC70E7D51596C53004C0D6073E42602D022AF01EC69 + A13D9EAD8BA01637EF35C4B405ECBA73D7206E4E651A4706743B9252468F4FE4 + E774B12EA4C189C0B78B6EAA55213D8E6E9A5579D0EB6DBE7F35CAC3DF008043 + EB7301F08635AEE638765500DF821C00EB178D1E805D14206D29889B578BEB42 + EB8EDFBC5AF7D12F543874E398B59C9F5ABF2900A64DFB0E812DDE7BB4940179 + CBC74B599F31D2A4A7FE6CDA6F8FBB76F0942CDEA29F7E31EC3FC1C6AD01BE03 + 27491BD23553BB774088B704020F18678018F0DB3ACA6BFEB61EC493C98574CE + A46D6ECD9814CE31DD336A3D786AA6562A806FDB8A007769DBC53EAD03606AFB + 9AC337B6218DB37ED98F9017572F00981ABE9DC7CCD32FCCB34C5DC6CE976B27 + 2C921E9397CAB59396986317B08B3AE8F1ED47017E71FB86BE4161ED771CBB40 + 3A8D5BA0C707A7AF01E0F101FE7699B4503A03752704976F574BE3AC3160AEF7 + 75BE33009894CE067F03D4F5B1CBDDBD405C77F5FA38EFF64DC130601731DF6D + 5A00C4DD71035FBFD204E825B5337D4BF11C45BCCFBC02E045405DEAFA5A6D5F + 1DF759B2314BEFDC0BB7AFF503E0ED4DCAE7659B0DE2BAAB3775FB9A1B58C75E + EF17F533E0BB4DFAC4FE80353BA5FFEA1D59FAE7BEABB61401606E62853ABF88 + 1B64DBA4FF862D52BE7E93B5C0DD419B76085078E0263D0ED7C4C61D0677A9EB + 3B6083C654F4876EDD2715DB7067E0D2D86BAEDF613BF748C54EEDEF08ADF5E3 + D800B03B8177ED9761BB0E18001EB9BF462AA20B18180C041EB1BFDAEAFC1A00 + AEAA95B1070FCBB8BA23593DDF51352A6D01C0005EA02F2DAE5FD238038091A5 + 74567113939B9BDCC8E4E6A6DFF89C686E99E332EEC8B10879B9497AC4A02F20 + 78FC11DA233249E3934F9C126A004F3D754626A926E8D805E405EACEE2A6EFD9 + 9BC2FC4940F1699974FA0603BC08A08B26E97AE2A479E64631C73AF89DA19A79 + F36D32EB96DB65A6B98C6E37D80BF4C5C18BA8FFCB0DE759B7DF2973EE8EF50E + EFBA53CCD9E48EE0E8109EADB139DCF0D61671337B96AE9D7B1F37C083B3979B + DE7E43DCD26A6AEB22EE73DC585FF8D04326BF09EF8E2C1F23FADCB047F3B54F + CD4707C5B73C5657128436A6D79FDA2AAF3FBD4D7EF74C10FDD79EDE1AF4D416 + 1D07FDBDD6FF7DF4C5FA0CDA18E87DC35D82C1DD07D4C1F567D0F7DDD764C3DB + AFC8DA375E9275805E1CBE6FBF6A7076F3FBC15DCB18D1074C3994F5BE035E5C + BEC0DFCDEFE1C8050087F4B80848B6E3E3F7324866F0F783B7AC25DD73ED5717 + A5FE9BAF0CB8EED475A1567048F1CC7AFA005A87BB2885C11E4B011E71D61804 + FCEC6DD9F3F9DBB2F7DC3BB2EFFCBBD6DFF5E9DBB2F393B7B4C54D1A1CBB0100 + 9382F9B34CD40FDE0F10FEE2BCD47C755E82939554CA015C0231018B56CBD75C + AA387543EAE4AA4B1F19B805F8026F2B23D02506E0ADFBF63339F4CDE7526B2E + 603DC7179C0700FCA1CEE38EC531FC9EF5EBBE03B8FEDE9CBFD55FE8792E7C20 + F5977E2F47BE06FEEAFC171FCBA1AF4907FD89D4EABCCF1D8EB1C3DA3FA4AAFB + 06C00AF4D5D7D7B93A8EFF467F0E40B2FE3C75DF7C62AF177E06A0AFBE9EFE7C + 07BFD66374BE567F5E1CC0005903B9FABAA485BEF1DFFD516EFFCFFF2C27FFF4 + 83CEEBEFEA120057DB2F81C49F6B0B388E2E63FD3D91E6DAC02EE05C5B1F330F + 2476F99C1F03FC2586D2F4D0010887D4D10E7F3D15B73B868913DBF1317F936F + C5870000B8C0E000801DE8FADF13EE6062FE37C6DFA2035E5A9F63AD8F391FF3 + E16F3DBC5FF85BF698BDAF54F4D375BCA7D2F71B02023B00DEF8360F6D007FA9 + 071C9CBEBCBF01BABCDFE98758700023FA6B0D08BF6A4E6080F0EA570300E6F3 + A094F88C486130ADC35FFAC062DCBF805C87BEDE225CC10E77CD21FCC2338554 + CF3C64A33107C428730A3FC983398FCAB2A79E9255CFE9B99E7FCE9CC0C05E73 + 036B0BE8258652B7700A8381BF5C2310D037DFCE03FCF240D1E38F59A609CF2C + 51B8CEE00EBECF2030C0D7B256DCAFD7321E52D273D0BFFE5EBD4E46713D03FC + BAECFA7927F057AFB9767D0CD75174FD1D7A7DBC5DAFA7B7DD26D301C0DE0708 + 47186C10383E7445CCAEC95C87F59A4ED60FAEF53CD0354B8F9B798BEE03F4BA + 4FEDDF293704E81BF6054060FAB714D24447F88BDC0DECE5240CFAEA98B4D0E6 + 108EB0770A8AF017283C41F738067EB52535F4684A5DD4F390DB299978F4B4EA + 5404BEC109ECEEE0F18774FEC81919A7ED98DAE3BABF227B0A40F898EEC90ECB + F003F5A661FB8346541E969195472C457485EEEB2AF6D6C8B07D07A502EDA626 + 70B509F84B7AE8917B75FFB6BB5646A846EFAD9731FB0EC9F05D07A57CC31E19 + B049F7873B9923562B43B6555A6CD0D62AEDE318D6F399AA0C160FD8B45726D7 + 9D92AD8F3C2F5B1F7D4126541F3757F0B06D07748FBA2FDBABD292569A0C369E + C5060DD8B05BF7BA3BA542E7C7EF3F2463F5E71BB87E97F463FFCC5E1A07B0AA + CF4ADD8BAF086E5FC06F0A80FB587A68F6F064F109A9A011635242239FA3F6AF + CB21702FEA006B9B694100C27D00C2739709EEDFEE73161B04CEE0EFEC25FA3D + 64B141DFDE1A030CA710D8C1AF2B05C3388201B99E3ABA7B14B0D721700A803D + 9EF6DD116CCE5F60F0C48203D85CC03A7620DC69E22C03C4006007BDA97006BB + 18037F0D0C037655005E07BE0E775D9DC6D0028983E317798AE82C0D34F0D752 + 490700DC2EA685F65AC0EE0E4E65407868923A3A3A87B3B511FAA6003895A786 + 6E9D4B07EDA0D7806E84B9A94BD8D522C25E87BF2DFA007947661018E04B5D60 + E47D80700BDCBD387A7B47F8DB8B87E1E3C3F1B411E27AEA6780B083E23C1846 + 067A5D8C55599B00DF7C1F35067F7D3E8D930EDA84CBD700F01069A14AE16F0B + EE172510D85CC1DAF77131FC2D0D803DE6103705C03E76809B82DC522AB58E36 + 85C0E95CB308721DE83AC8A5F62F638F79BF1400F63541C0E49E1900CE406D84 + BB0E78F340D781B0F753409C1EEBC717A904EC4DE5F7665330ECB1BCF2C7B81A + BB8F6CF79813D351C37857B906D8EBC0B794B80FDD52DB1611EAD2BA18C7FBE9 + 769F1ED8D8B210B3B8AA6919D037C05FC43D766BE3D863C50AF7EDF3F7EFF3E3 + 7F19756AA03C3729D54F63A5E2BF859C3D9519E8856B84D29ECDF8F769DA419A + EA7CB3F8FF903FB694329E95323207BF97E566AA745D563A5595B2B8DF520E4A + 4BCDFD0C5D1100674036BE563AFE8BAAC92F07C0A5E65C979BFF9F009875BC09 + F44DFB770880FDC3C6DFF0E9B8944A1DDB1880FDB570B631655056FB7EFE5F7A + 21F073A52AB52E3C857419009C2A5E507FA98A2EC4B958417A5134C7AEF67F06 + 003601735BB16168F8D4187379F0DB4006707523156520B8956EB45AE3F2ED63 + 40B845FB726D7503D78635AC07FC0617B00360D242874D5880BC7965B5801359 + 3CC25E07BE0E7FB35804B729144629D07535ED4CDAE6FE724DE7F2A04E7A9CAA + 29F3406493F703EC75506B4F57027D6DB35C90CD27638FE5533D37A602D4C5D9 + 0B04A6A5266F41A4757679CC20AFAA450FFD5252420E80DDA5EB60D761AFF7D3 + 583A574A29006EA96AA16B1DF63600BD71CC3A73FEF6D138F37DC7EA97BBF1A6 + E6387EFBA9E8F70F6DCB01138ADB8113AC5F4AD4F46D3568BCB41D04202E80DF + 50EF37B87F1D005BAD5F5AC6068301BEC0DF297A8E2000B041DF44405FD47E98 + 7EC11E3ECB00302D80D752371BE00DE0D7EBF91604C0D5F8E8F9D265EC42FD82 + CD9A79327AF91E597DF84E99B7F7945C3775A5AE992B5DC62F928E63A8EB3B5F + BFC8BBE659DB693C0077B17499B0C81CBE99262E0C757B55A47C06FE1203F4E2 + E435A01B012F634BED0CB455E1F0A586AFD5EF8D31AFDB9B825E406E9AD23983 + BC1A37C81BD75F0B0006F6321FC70E7B1DF87A6DDF1E897ACE5B237D166E0850 + 57751DF037D6F545BD16EB5C74F3F6020047B76F3F5CBC005C80AFAAFFAAEDE6 + E6B594CE1AA7B5DABEB105F496EB9AB465CE01F080B5BB6410A9EFD6863A66A5 + 00F0A0F5BB2C1D9E41DE8D5BA57CC31619B0699B397671FB0EDCAC719D1B84F3 + 77EB1E19BC65B7F5076FD5767368A9EB3B74FB3E83BAA69DB87AF7AA688300C0 + C390C62B1813DF7D40DB033244D70FDF17406F48F54C0AE86A19ADE35195D1FD + BB3FA47906FC3AFCA5A54E2F2D73387F01C00E7B1DFCA63018C72FE90D496D48 + 3D3C6E7C4EE0C6E8A1A33256050476972FFDF1474FC8B8D84E38AE73274EC8A4 + 932765CAA9D332F5F499E8F40D1078DC315D132130377981BACCB166226D8C4F + 3F1B1CC168829E0371E317D88B2B18F00B009E0E10BEF576B9FEF63B64164E24 + 80EF2DB7DA4D666E2E1B00BEED36136E5E6E560370A76B9C3496B3EFA246A1C6 + 3536FFBEFB64210EA87BEED273DD26B3EEBC43E6687FF63D77678EDEC20DF0E0 + 0CF69BE3565B31A99BC8DA050F3C280B1F7C48163C540C7BFD463C8E5F83BE0F + 3E60AE5F6EE02F20A6B2B49E3AC74DF95F5A0FF85F9BDE7A7157E6F25DFFC62B + E610DCF4F6EB9942FDDF008D2C7DF3BB00A69764CDEFF49837F498B780C02F5B + 7CE33B80285DA7A20FE005EA22FAC402040EE99D43CB1A601729A15D01726DFF + A861DDD41023D5EE7BB2E7D38F541FCA9ECF8174218D33629D0339E480CEE75D + 299CA34F3A6060AEB93ECFBF27FB2FBCAB2D10F82D7D0D9CC06F181006B0EE3B + FFBE1C886997F79F0F5011805CF54570C4721E5CA6555F0082499DFC490099B4 + 5F51DF36004AEAEE86D4CA3856835317781A5246E35AFD20B87001C01AAF05C8 + 5EFABDD4015BBFF84063B87371F97E2807BFF9588FC531FC81D47EA36BBEA53E + 6F70ECD6E8F9AA2FBE2F755F7D2487BFF944EAF57C751A3BACF347BEFD54EABF + 0206FF5E0EE971B4AC39FADD6772F4FBCFE430F31CA3AA03EAEACF58FFCDA706 + 7F11F15A80F3D7FC7C3A8F33587F4E5AFE7F48476DCE6140B3B98181CAE7E4C4 + 4F5FCBA93F7F2787BFFF427F2F9F4AD545FDDD7D01DC3EAFAFFF85FE2CA10E31 + BF3787BEE17716C6F41D009B03580500669C87C5BEDE812F20977F6777113B00 + 0EFF8EFCAD50B3398061C6DB3FA2CEB4FE2D4400EC7F33E1EF2BA48BC61D8CE8 + E3180EEEE2E01CA6258EA33DA4960E71EFFBDF297FE7FE8084C361621B23D875 + 280CFC25E6F1B56FE0B87F398BD97BF0ED9765330F58A836F23EFD1DF5B8C383 + 1D9ECE3D7DE0C3FBAEE0080EAE609F4B3F1342FF759BA30638B5C05100C0CF6B + 3CD40867BCFCC5A765C54BCF587CED2B2F18E045C05FDA3C0026EE4EE0007B49 + 014D8AE8902E7AF9734FAB9E91A5805CD5F677DE9283E7CFC9BE8F3F9415CF3D + 6B9FF9CB9F25D5F473B2EC9962F8EB8E5F2031F0D75CC24F14524333F614D0EE + 083608CC7546630E7F1DEE72BD09D7A0FB751C1E44427E6DA275003CFB3EBD96 + 69DF1E6E8AD73DAE6D0063BF0672FDF3EB22993580C0567241AFB756075F456B + 90D8E030B1506681EB3331E0AF3D8845CCD242DF6C993C66E978E6AD85EC1FC0 + 5E4A4104B730B19032DA6B044F0708E3FED5B1016053027D4FDF94A5869E1235 + 55C7883E0E6173FEEA5E07189CD507D698398855C4BC16301018003C46F74980 + 5E03BF55EC9D02F81D5BC75C80C1C4103078D8810080710693229ADAC000605A + 9CC1D40206EE7A4AE8917B75EFB64FF773FB741FB73F88D4D1C370096FAB948A + 1D35327A9FEEDB5443B7D5E8DEB35246ED3D2C23F71C8A60B8DE40F0804DFBA4 + FFC6BD960E7A5CE531B9FEB8FE6E0FDF68CE605CBD43B7C47AC2DA0EDDAAFB55 + 6D076F225DB4EE872D357400C1E5EB80C0C0E0DD963ABA0FE99ED7EC9072EDF7 + 35F0ABFB6F158EE09E4B753FBF0C004CFA67B2E6044730EAA7316AFFF6A50670 + 84BE8CFBAFDC646D3F3DDE00F092820BD89DC0A484A6F57E562378FE72FD6EB1 + 54AE9DB344AED596B4D0BD700053E777EED20C08F7D13100D8DDC140E014FEE6 + 61306307C10E80AF651CE1AEBB811913470E88519A16DA9CC1DAEF1AD3420384 + 5327B0CB1CC2E3660607302037025FFAF914D1AEF6B40E7BB5EFCEDF14000705 + 000CF405007BDA6807C005081C459FB4CF09002EA522777012E7188BEBF7F614 + 04B7C5418C6B38026087C1AD556D981F382E83C06D5CA48406F2AA1CFC36EF33 + 2203C0AE1400078DD063C21A9CC41C53D66B9841E09640E1DEBADE329FE9B94C + 00E2E1D24205100E25B282AB17F89B4162E2365F0200037955068A7300381571 + E65337710A7B4BC53200ACB235D11C60F0D7EE330557B003DF86B0B79402ECBD + 120076F9B824BC2DA174DE8F67DC2800EED857A8F7EB357F1DE8A690F7970060 + 9CB90E68F320D76329EC6D0C00A7E0373D3E150E60EEE1E6EFC3A2F4DEEFD500 + 60577ABFD8CF9157367705005CE4F82D219B6FA9FD162500708CD9FDF34C9DB2 + 9849D750E31791FE19354D20B0AB007EA3E2F18DDDAF7735766FDFD9C45F824F + A4BC2455CA4E5286522AD6D8FA5FAD786E806F5919E037BC5E8BD6FABBD79F1D + A6658C2A7F5C23CA7856CAC81CEA668CACB8146A8375A6186BA6FD94C5FD9672 + 505A6AEE97287103FFDD03E0A64D4AD4FD75B87B1915D69682BEA9F407F45FD0 + DF9AF8E5158DFF6D0060FFA07135164F951E5F585FEA43B0F0215B6AEEB710E7 + E643BDAC55375396AE59E7D20FFBCBC92F0CA94AAD73009CC1DFCB01E096850B + 6DA678A1BDDC45D9557421CEC50AFA3500B8B089C86F3AD8905CD306D01BDDBE + 2A9CBFC5D2CD5404BFD4FD6DD1B1BFB4EC34C05A00709396C0DEBED2BC9D6EE2 + DAEA862DA67E0EED9501B03B7C1B05C0510E761DF4361AF763E33803C4280260 + 57E98DAF6EA4BBB2492E06BA0D44DCD7A2B8D6D237EBB814EC2D25E06F80BB15 + DA2FA8F9B5FA854455D65DBF580073AFD52F20D686B88B5841FA6525F69BF7D0 + 6301C13DB51F9507BDC801B14362571EFE2287BF0E7A5DA1B66FC398A9AFCE65 + 1A63AEDD007CC74BB7317364F09CB5D263FC7C69A673A8056037C25A5A206F69 + F01B81EEA089DA4E5069ABFD5688799CC20900CED60F0EF09794CE21ADF3748D + 05F72F7D402F6A0C0023C69ED619F7AFBB7EBDE6AF3B8271F5B61F15D479DC42 + 83BBED46CE91FE7336C8EC5DC765EAE63AB976CA729BEF322E0260EAF98E5F58 + A46E93974AF7C9CBA40B1038A67576A500B8CB045CBE01F4160160A47D00AFD7 + F605FEF600D402711DE6EAD89DBC8C01C0005E8FA1140467E3D9AB33B89BA676 + 4E752DF3DA5A7A6720EFFCB59970FD027F7101F75C403DB0007971F702816989 + B99803FEE2FAB5FABD805C1DF78D50D79DBEEEF675D0DB0000EB7CF96A402F60 + 778F0CC245B16E87015F5AA0AF836003C0DCECDA40ADDF1D429DDE415B77CA40 + B445E74909ADFDC1D4F28D7077F0B63D32783BAE5E6AF8E2E4DD6B8EDE41BA86 + 36D4F70DC0D7A0EFAE427FB8F687EFD92FC3F71E90617B692B2DE5F3D05DFB65 + E4BE6A19535527A349DD0CF8558DA9AC953111EE66357D559EE619B03BAE1E27 + 2F3720EB2D1ED23C173B7F1D0053C3177193733CE90E8F007A4F19F81D5DAFEB + B505FC4E22A573144077ECD1E332FEB8AE3D7552DBE31A3B21934E93EEF9944C + 3C793A83C0E37101333E7336C05F8D33C70D5E03BF405DDCBF717E2AFDE816E2 + A630378881BEC8FAB7863493DC4C4EC12FB50681BFB4D4EEC5B18493971BD133 + 4961793B37A86FB79ABE73EF099A7FDFBD328FBA88005FD6EABAEBEF46D4F32D + B87AB9D9EDEEA639F7E0A27A40E63DF8A0CCD1E3434DC5070DEA2ED09694CEDC + 54F79BEF38B200BF7E639E750683A3B3CBC5989BFF2B9E7ECA40F0F9A7969784 + A2FF5674E98535066F02100A80C7C0CE5BAFCBE6B77F275BDE7943B6BEF7A66C + 7BFF2D13F54837BE03E07D390AD05B18AF7BF345137D5CBDDB3EA4CE2E2017A0 + 05F025FD332EC630871CFE7A9F796032D0D7DDBCB405A04B7DD677753D30AD18 + EC06576E70F93A9CA3EF6B42BDD7B0863171076CACDD772EA4050612E2BEDD77 + FE1D83BFBB3EFD9DEA0DD9F3F95B1A7F4FAABE0C299A81B2A43BC6AD1BA0E09B + 76ACB95C2F5067F67D3DE787067C01900E2571001760E6EF0547AD41DA986699 + B60AB87CFE7D3D0687EF87BA565F4BE3E6C2BD845B57CFAB0A4096E33E92C3DF + 7F2E87BEFB54EABFFD4485FB37C4696B3946DBC3DF7C2AC7BEFB5C8E6A5BAFFF + 1F8788E97AA0EF116D8FEAF1B4010087FE21ED1FD2F3215E8FD7E5BCBC6E00BF + 00E1022066FD617D0DFEBFF8D971281FBCC4BA505FF8F077E7E5C8F717341640 + 2DE9B1891DFEF682D47E794EFF3FCFEB6B7F2947BEFB428F09AE5E03C7FA3BE4 + 98BAAFCFE918877080BA3E47BA67C6F4F9FD123FF2FD453DF7053B87AD57017D + 01BCC8FEAD75ECCEE0B475589C4261FAFE6F0DE0B574E13A46CC214031E3DD9F + 0685E370FC0274799021CC87F7068EF66247B08BBF4D441FE08BCB97F703F2B4 + CF3C5CC178E33BBC37438C96B59BEC218C0200E63D6ED0F7F5007F37BC5178CF + BB7009037843CDEF5764CD6BC05D3E1F821CFE722E7F5884CF8F55AFE0002625 + F4B3067901C2C05FFACC053D6B10D8A12F5AAD7D44DF5DBDB44060770207F72F + 3582491D8D6BF8F9AC452B9E7F56B6BEFDA6549FFB4C767FF8BE8D173D094C26 + DDF433D62E66FCD4132AFAD46C7FCC20B13D1CF4F8A3B244CFCD71CBF4FA90BF + 6E20C0AF836007BC29005EF808B017F85B7820C9E7E73F485902DCC1F7691BEB + 07ABB8F6017DFD1A08F805003B10F6F99980DC3BC243570680E38352B3EEBE2B + 5C7BF5BA7A7D7CE8CA3374B82B18D998EBB75ED779B0CBAEF57ADD676F00F89D + 459CEB7E8C91361A10ECE9A0B39AC1A76FD43E8038EA8C2A02609CC0E38FE9DE + 04E72F00383A873D3EFE7848158D43384D090D0C46388271FD86AC28BAF7A9C3 + 11ACFB9D237A7CFD6983BFE3EA759F7398391CC2A48C0E7D77040707B0B6070E + C9B07DB5E6FE1D595967316A068FDE7F58F7764764B4F603FC55697FD4FE4336 + C6190C281EB597395DB3F7900CDF5D2F153B34BE53F787FB8EE85EB2CEC02F2D + 0EE0FE1B01C0BA3FDD4A4A68D2471F90F20D7B2D5DB46903654BF6641A646DA8 + 170C00C6FD3B48D70180CBC98AA3EBFBACDE26BD57E97E7A2D607867ACFD1BD4 + 7BC52683BF5E1B9894D0FDD6E83E7DF5E600805704E06BE03707805100C0A482 + 5EADFBFF007B1D00237704BB00C0405F53AC156CAEE0852B4D5E2F18E0EBF037 + 05C000DE54067A73B190023A388153C80BFC6D0C0093FED90131AD49FBDDA7E9 + 5A15F5817100A7A9A003009EA5DF0B03F4ED32FEFA205CC111FC3A0CC6F99B01 + 60ED9B80BE11FEA62A0060524417523F67699F63EB7D07C0368E00384B031D1D + C1AEBC43B80100260EEC8D0018F89BAF23EC222534317301978F31080CF8456D + 35668A6018F08B80C129002E86BF614DAB729D8BC71800EE3D3C40DE3ECC17EE + 6950AEAA0519C9FAD00286754D2F15C0B76780B9067A81C0D1459C415E8D9B2B + B84780BF0E800D183B144EC02F2A058F9BE93CF275B4805E3F3685C0C57155E2 + F6F5BEDFEBBAFCFDB062008C3C9682DFBC1CF03AD0A5CDC77D2E9D77A571EFDB + B863D05F020003701DF61AA88DF0D6412FCA036087BA693F0F7DF320F8720098 + B8DFFBCDC72FA7C6EE21A72A35D7F09808781B01C03E575403B884ECFE39FDDC + 3D7554B8EFDED9EAFCA2A6CD81BCDA66AEE00406E31666BEC4B94A29BDAF8F3C + EE6CE22FC328385F69356427A563A5E2BF8548F50C7F6AD9A6BBF41F304AFAF4 + 1D6EAEDF32FDDDB474080CEF223D752329AA53653CEBB28CAC11005CA4184F39 + DC6F2D07A5A5E67EAE384F02801BCCA5AF958EFFA26AF22B1CC04D0B003805BC + 0D406E3AA72A1C935BD740FA03E67F517F2BE2975734FED70F80FD4326D595E6 + 5DE9BAB0B6D48760E143B6D4DCAF517A5EA06F73FDE04240E0F4433E2F3F2E3D + 3E7F8148E54019F9934757058051726135F87B1900EC17DEBC2EBFE61702E008 + 7FD327CA8AD44EE76D6302E825CDB36E66A283D7658E5F1569A09B7728975E83 + 26C9C8498BA4F7A0C97A6EDD5CB5222D34B5817593474DE00C0007E50170D30E + BAA6039BB8827E2D0076B7AFC5FCF8DC3C4EDF06CA6D80A9E9EBC03705BA0E75 + 4BE91A3DC6D7B83C75F3D52AA4780E10D8D538E40D2A35DFB2E70869451A676D + 01C0405F87BB290076984BBF31009CCA01B0B979550D206F54F8C2A4C768BF35 + 357CFBF1C54C5B577F9EEE9D20653A07001E3073A5CCD95627C3176E32585B56 + 3E2EB882750E584BBD5E03C140E044AD32981B2131F037C2DDA0D4E99BA47EB6 + 3EE0775A067801BB8D415E87C12E9BD3B68D1ED77AA87E59B6BABF0100BBEB37 + 83BFC42CADF37CFD220E0426ADF33C693B62B6A9D3D805D275C262E9300EE84B + FDDE8521CDF38485FA851F37EF228B23C06FD7494BB55D54047F1D009B80BDC0 + DF087A01BE805F9CBDA46A36F76F84BF0E80CDB9EB6D94835D77F9BAC397D661 + 30D0F7BA087E1DF8F6A67E6FACDD6BF138D74BC7C401BDD725699E11C0F73A80 + AFB699748CDBD7E0EEB2CD454E6040B0B98135EED017772F72D7AFC35E07BDFD + 57EF2882BEDE2F27BE1A772F7077B7C1DFC1DCECDAB02B00DF753B35AEFDF5DA + 027D37EE9621560B8D3ABF80DE5D3264BB1EB775A794E30656D1F734CE80E041 + 5BC39A14EE0ED9BEC74030F077F86E523A179CBFC318EFA60DF07758141078E4 + 7EEAFED698E397F4CFA3A3AB17D72FF57D71FEBABB1701791DF4BADB17C86B50 + 3701BD0061620E875983C3779CC6261E3D61D077F209EAE49DB25ABE805F87BF + D4F59D72F28C692AF5F94E9D9671C78ECBC4933A77FAB48C3F76CC2030F01710 + 4C0D60E02F0E5F4FF3ECB097D66B000378B9E10BF425EDF3D41B6FCA6E0A1BE0 + D53EE99CA7236E304731A6C6E0E41B6F14EAFFCEBC3D387FC371DC88BE4D66DF + 835B891ABEC05CD23807283CF7DEBB659E6AF65DD42F4CA46B70FFCEB9371CC3 + 0D6F733969CB8D6E6E6633E666B843DEACA6EFC38594CEC86FB2D307FCBA1873 + 837DD163A11EA4DFBC47DCD85FFFCACBB2F9F5D76C0EA7D7C5A7579484A3FFDA + 05FC5D8FD3EFCD58D357FB06830D12BD6C71EA8D6E7EF7770681B7BE1F523603 + 7C3D6DB38B31AE60E02F2D6B88A700983E6E5F77FC1247DB3E0C70D8C1D8F638 + F674D0EE86048401C800BAA4DE0502BB43388060601C20B80080C3DAD077F09B + 02E0FCBABD9F070730D02F3880DF910317DF8FE99F7100BFA9F177A5F28B0088 + 1100B8F69B4F350E507EDBDAEA2FA9EBCB6B5043F67D21FD7135357223A0ACBE + 185CAC55387B55405F03BC11065B5D5D5EC3E0AF9E4F5FA706C7AE01D4006B01 + B4B8750F7FE750F62339FAC33939F2DDE719AC05FA66905605EC05FC9EF8FE9C + E928D016880C08FE5AE7BEFB4C8EFFF0B91CFBFE3303BF0E82112E6103CC9C57 + CF63005845CBEB1FD663F9D97C1C6031601BF7ED87E167B09F9F9F0D087C4EE3 + A468FED08E3BF2FD7903B5B5A481FE32B8796BF5F78680C6755FE304E6FCB888 + 392FAE69FEBF43CC5DBE277EBA24A7FFFC8D815F52541FFBC31726A0B1FDEE23 + 0476B86B305E65FFE6E7421D60E65C8C5DCC037381C10E7C893B4CF6350E845D + AC23EEC7D122FEE6F95BC7150C04F6071252F1F78D1CFCF25E4861707011E364 + E7A1886267F0C677438AF58DE6067E25D6E7A6E57D4F6A68E2BA4EC51CE3100B + EE5E3E03D6E12C568594D12F180476B06B1059CF47BC00799FB379E6580310F6 + E3985BF6C253B2F2C5A7B2F4CF6BB45DFBF2F3D6AE7C1E572FB57D9FB4BAEACC + 3307205EF66C70FCAE7EE905137DE43098795A2031C0D760AEC6E82F78FC5199 + FF985E039E78D4C0F0E2A79E30D1C7F9CB3C63CE8753D8EB06D3BA4B98710680 + B9E6301FAF37E1DAC33529D404E6BA450BF445A4869EFF10E037388411F373EE + D76B1F0F47912A5AAF713C38C58354E1BA788F3D5C65A99E6F479E1E3AB87FC3 + 38B880EDDA79E75DD9359D1618CC75DECB30201BABAC963FFB829B28FDA0D76C + 1CC2BA3F088EE0F0D0180F8AE104B674D0672200D616F01B5245DF1220F0E9E0 + FE251DF4B8A3A71A40609FB3B4D12A9CC1D3350E0C76F8EB3583271FBF41A57B + 9F63670CEECE3A7BA7AC79E8595979FF5332EDE42DBA9F02F806F04B6A685A40 + 30101827304E613482D4D0FBD9CBE9BEAEFAB08C610EF0AB736370131F38ACFB + 3FDDDFEDAF37183CFA80C6F61FD67DA27E7F52857EBD0CDE5E6D1ABE17385CA7 + 7BD0AACCFD0B1406047B3A680030F0977AC0D40806EA0EDE4CBAE7A014020701 + 8675FFBB79AF0CDD7AC0C06FBFB5388235CEC393BA8FEEBF6E97F45F1B1E9CE4 + 414ADABEAB4801BDC964D97534CE5CDF553CB4B949F7EE01F81AFCF574CF3105 + 7400C3C4F47BC452FD9EA1EAA302FAA64E60A0AF3B808B9CC0DE328F22FC3547 + F05CD59CA596FEB9410AE84429007617303107C0693C75FBD277A5EE605FE310 + B8DB345D8BA6CE33E89B3A801D08239CC05D48114D1BE500D821304A9DC080E3 + CE2AABF7AB6314D2418771E7B1C4428DE062303CCD5CC1B4C4439A6840B06A78 + 80BF1D46682C42E05262CE2170EB980EDA41B08F01BC0E805317B0C15F6A01BB + 1BB8FF68A12E702B5CBFC0608E03FE0E2CB8830DFCEA1AFA3883531177206C90 + B8AFB6C0E17295B6CD35566662ACAFD94F5F9F07D281BFDAE7E175C030801850 + 5C76DD3029EB59A16D50F35E2AEA0AD3027089017F11D036CAE1B0C5A380BAE9 + 380F80FD589402603FCEC72E07C05647B89BA785D6587400974EF95C5000C500 + 61EE919506C0F9DABFA5E4B0D7FBDEA690D7E3F963D275A608729B752CF4C3FD + C462D09B8ED37843018BAFB314D00E7553988B3C564A067423DC4DDB22E09B1B + 97BC079BC8EFF7968A5D8D1ABF7FFCF30030FAA500D885DBB7E89E7ABC2FEF7D + 207010F3DC6FD7B6795769D65C8FB336A8693CEE4AF2D749E573A558C06F27CE + 575A0DD949E958A9F86F21634F4DDA4B872E7D65FAEC653269EA7C29E377704D + 3B83C0EE02B6F53F070037E062C5F2758D4B7F06585ACAE17E6B39282D35F773 + 75B9F3F8EBF89A74FC1755935F0E804BAA14C8CDAD299CA7C4DA22E90F98FF45 + FDAD885F5ED1F8DF16002E358FD2358D297C8096FA10FCCB01E0BCF8602F7561 + C9CB7F9E54F9357E0ECE97AA697C02A924EC2DA5E4C26AE324D5862BBD1897D2 + E5D7FD72004CEAE73C00CEC66C5280C06DB42D017F03CCD54D54740097B5EF27 + 0346CC94394BB7CBD031F3F4FF53D7B4EE232DDAEB46B09D6ED870FCB62B3E47 + 560358375D6143C6064ED72602F4A62A8AD14FE460D7E16EA166AF6E5E01BDB9 + F50E87A9077C4D473D3E55A790063AA8BFC921701E00A7315357DD5C9700C01E + 6FC646DB5400BD8DA9008C0B103805BC29E4CDC35FD6A6F0B74D6FFDC2D373A4 + 7E3108F0D7216EDA4FDDBC3E4EE71A8C13C8CB97A2ACAF6A81581F5B0030EB0D + 00F7A77ECF7869D3DF354E63E30D000372FB4C592263976D9781B356994BD7E1 + 2F9018206C757B81B9405EC06F54EAECB5395511F01D3AD5044076110FFD007E + DDDD5B047713958A595C456D5F4FFFECF017F0EB222534757A03009E6700B8ED + 486AFB120B2EE03623674B7B9D37C83B7E61102038C25FA0AF3B803BAB188739 + D6040180CDFD0BFC8D2E5F07BDB4A8BBF60D02270018B06BFD0877DDD5EBCEDE + 14082362760CE098310038C25D07C07D176C50AD0FB1781E5CC1B87F0D0EE3DE + 05FA02731310EC7D5A5CC07D97F0C4FF960C007B8D5F8BE1D805F4EAB8172038 + 025F07C09EF2D9006F547F6AFBAAACCEEF9A1DD28FB8B6B87EC38D2A406F00C0 + 4336ED0D9077C32E4B7947DDB341DA0EDABC5B866ED96BB5CE866CD1D8965D32 + 70EB2E03BC80601CC00348FFAC623C74070EDFA050C7D7217000BE23F61CC8E0 + 2F2DE3117B35B60F551AF01D11DB007F2B65D4811A195D191CBF067CAB428A67 + 5CBCC3F65707C88BE357DB61CC03880F1EB2963940B0435F77033BF875B7EFF8 + C3C7838EE06E3962AD43DE49387C8F52EBF798B5067D4FE18C392D93B53FEDCC + 8D32F5861B32F83BE5CC19EBA3AC7F2A405FE0EFD86327CC05ECA99E1135FEB8 + 893BF9EC4DE6F445936FD4FECD3A7F2B90B7503B10B87BFD9DDC3806FAEABC6A + C66DDC382E1ECFBA83F492AADB03E875F83BEBAE3B64CEBDA4B2BCDB202F6E5F + 802F757F177073FBDE7B0CFC5EAFEB3866C18338797138017F75ED3DD1098CBB + F7E1870CF8922A939BE4A9984B41AFB9AB540E7DE92F03ECEA3CA99FE768EC7A + 7D7D87BFDCC45FF9DCB306813D0528ED85A7FE6D41E0AF5E5A2BEBDE7C45D602 + 7E71FC997BF075EB1347A1B62F2049E72C6D7348D90C50CA036087C0A5C600AE + D4E90B0C7630B6E363D2DCBE21DB54C0308F07A724D0D753E5027B832BD261AE + 3B811D0407000C800B2E5FD60179691D1E3366CEE5299FA9D1EB4098B801BDCF + DF896E5F9DBFF8818D771A047EDBDCBFC05F5AEADD1EFEE1BC545FD2D73D47CA + 6800316998F5DC17DE2B40E2AF00A2804F77A316435F5A446D5DC635FABACC03 + 7E0DA0025671E37EF7A9A56406D6D2025E01AEACC9435883C5BAA69E3E905675 + 2C02E093FA33D312C3510CE43DFE83C67FBC20C7BEFFDC802FA2EF3AA2E235EC + 9CBA1EA5AF43BFFA0B80F507FAFF1B9CC12883D071CCFF27901B01838FEACF02 + AC0D299C393FC037A4C5F6DF0F63FA1E4BE7BC7FF69FBE93C7FE1FFF459EF87F + FD37B9FD3FFD497FD673D99A00DC3F16DCBE38B481C0805FFAC881AFC75228EC + 730E711DFC22E20E8099678E358C69F95BA6EF719F43CC8587240A6E5FFF7BA7 + CFDFBB3F04E17FFFC801302D637F0FB00E073029A101C0B884790FB31651BFDB + C0B08A35A48CF6394B23FDE64B496DEF90561AD967846ABD3986030C76D00B00 + 76289CF653E72F633F66E54BCFC81AE03135855F7921E8E5170C00AFC2158CEB + 57453FC0E13007D005F0027F57BDF8BC39751DF43206FC027101BA1E7720ECB0 + D7E12F609879802FF33E47DFD343E75344730DF134D080E085F11AE40F222D78 + 38F4B906F918E04B4D6004007679AD6034F701AE733ABEEF1EBD3EF22015D748 + AEA577C9F43B6E95A9B7DE24D36FBF4566DC795BBCD6DEA96314B265E0040600 + 03837930CBEBF4530398D66BF73B0C461653CD642D73370173A9091C3384E89E + 813EFB0B20B081DE46E4758253D06B75811331677D6D0D0CABA80F0CF40504D3 + 9FCA1C6BF51CC4261EBD4116DFF588EC7F4D3F935F7957E6DE72AFEEB54ECAE8 + 6ADDEBD49F1600B0A7837617B06B54356E60DDCFA900C0A3AB01C0D1211C6B07 + 5790165A35623F2EE0A3325A35624F70FC8ED87BC880EF901D35D62283C1DBAA + 64E49E7A19B3FFA8E00006FA0282490BED4098D4D1C3742D299F2BB6E9BE7347 + B501DE215BA8FF1B5A87C243B61CB018C22D0CF065FF3C9894D2B888B5CF8393 + 64CC31D8BB36945401025B6D6032EAA8428DE0AD2ADDDBAFDA24E59EEE39A905 + EC1098B6EF328D2D5B237D97AF35F55BA66D84C019EC8D0208A729A27DBEE782 + E5E60606FE9206DAC0AFB73908DC33AA4792FED9E23145B4C1DE289FA70F1076 + 008CDC0DECFD1400930A1A7599169DC1A4839EACB1580BD8C1AFC15F1D3367AE + E00402BB0B387503BB2CA60200037853751CED3038D402A69FCA21B083625CC2 + 0681474D35080CDCBD1CFC453E0F0476E04BEB02045B0C87EF601D47A52E6087 + C0D404363126AE6A13E16F9A1EBA31006CF038026007BECDCB47D9F980CA2D54 + 65E56334A6E7EF3F4ED78C15B296D11604041E2DCD230476913E1AF05BD66B68 + 6C2B0C08A710380F738BE211E41A28BE36BA7C55EEFC35F76F8C31E710D88FF3 + 712A8F1928D6961AC0A5EA00BB885D0D006EA6BA1AF8EB4A812EE33CFC4569CC + FB0D14416E1EEAFA383F9FC64AAB0080F360D79507B8285D97F6F36B7D9CA95D + 0F69C631F1DE6B5E29F4CDABE1FDE020E6D2638995BA87ECF79653359CCB81DE + 12B2352DBA08EE5E13FD54160F6D762F5DE3E9BD7AA06FB8FFDE291B17E68092 + C56ADA086FC82B7D1D97CF5D8903FC3A71BECB2BE52B8D71977CFC5729322752 + 3BA3D6ED7B4AC5C8293278F82429D37FC366CCC3B39AB4373EC5BF81A5EBF6E3 + 1B51C6B31A70B162F9BAC6A5FFCFDAFEE214D07986574A0E4A4BCD219FBF9CD2 + B5E9B1A9F2EBD3F15F544D7E63008C4AC1DC64BE709E12EB8AA43F60FE17F5B7 + 227E7945E37F9D0038FD50B99A0F195F737995FA00FC6D95FF10F70B0973FEE1 + 9D7EB8FF1AA5AF91BE56D3AB01C0A92BD82FA2805F8FE52170E20A2EA5A2B50D + D6710126D6430A00F8BAD836A2CBA47E763569ABF36D54B649D18D44026E8302 + 0006FCD25ED3A6B774BE6EA40C1D3D577AF69FA8AFA3F3D4026EA31B34ABFFAB + 6DDE018CDAE91ADD74854D199B393D5F1460363F4E6329C475C09BB629004EEB + FD22E6CB927131F02DC8DDBFAE14F49684BFC840EF609D1F948931F579695D5E + B3F7724AD753FFB71400CE2B05C50060E06FEB5EA382AED32F36B880931AC028 + EF0246806087C1A5C66DFAEA9729FDF2D34A5B570A807104A7F57F89F1F42CC7 + B4D62F5200E0B6FD83E8B7D6BF1B002FF0B67DC53453BBA1530DE4A6299E19A7 + 4AE12F4A01B0C35F80319097F32187BF36D6D7693BB4E0FC4D013060B7D3C8D9 + 85FABE31C638156B3A44A7AFA77D7611030A23C60E8001BEB87D3BE87BC65DBF + D4FBED34AE00753B8CD379552714612F699FBB4E5A627D5AC6388033F8AB0ACE + 5F9D5739F0F57ABEDE07F8E601B0C5220006D43606807DEE3A523C139F156373 + 428A6760AFA778EE3D77ADF499B7CEDA5ECCC5164770EF05A1AE6F8F056BA5A7 + CA5DBF805FABFDBB6493F4D5B67CD916E9BF7CAB815E00B03B7DA9ED3B00880B + BC050C47D86B699E5917D7E0EE650CE8EDBF66A7A577A63F00C8BB6EB70CC2A1 + A07DE02FC0979B55F42D05B4F601C043370382F7C8D0ADDC0CE3061875D0F6C9 + B0ED0764F8F64A19B62D40E0815B020006F0027101BC96127ACB0E03BE0E7769 + 876C2FAC1BB9AF5246EFAFD2F85E19B2637788EDAD34017DD1A803D4F82D6874 + 658D8CA93A28A3AB0FCAA8AA90E219006C357D23E4A51D0BDCADA9CF523E4F38 + 4C7AC2E3067D71003B00460E7DC7E2F875084CFD5E159017972FEDA498DE19C7 + 2FE09779C6C05F20B041E1E32765D2E933067A81BF2E9CBFC8FB961AFAE469F1 + 1BB796F639DEC0F59BB8B41300C967CF6637822DF5F3ADB7CA14DA9B6EB61BC3 + C05A6AF4CEBCFD368DE1F8BDC9FAD793AE59451FCD32501C637768FFEE3B6506 + E05835FF81FB02D4BDF76E03BDB388DD778F2C20F5A5C6E66A9F9663163EF480 + 69DEFDB87DEF97E54F3C2E2B9F7ECA523973031DF84BBDC4C2CDF47063DDC7DE + 77E8EB3018F0EB0078919E939BF9EE00F6969BF97E73DFC5DCF927FF6DA48306 + FE6E7A1780F45AAC1B0A2C02ECE21C7C5D36209DF3B1C5E298FABC29004E21AF + C35D84CB375DE7AE5FE6805EC0304B89FB89AEFFF877B2E3F76FA80AA971031C + 0E0E6187B2805C6018702C005CC05B6881C1382149C9BBF773205C80B90E831D + EE324E63FBCFFF5E2A2FE20605008754D1013003F880BC1F18C045A1D62FE778 + 4FA8FF0BFC05F8565FFA58EABFFB4C6ABE629D9ED3A0F0C772E8FB7372F0EB8F + AD0F0006FC1EFFF1921CFBC397063B3DAD33C037B85FCF19A8048A7A0BBC2CB8 + 6D3F96BA6F80AD9F668E5C522D3BF075000C88B5F5FC0CDA27062806E09EFAF1 + 829CFAC30571F87BECDBCFE4A8EAD80FE7E4C41FCE1BFC450697F5BCC1117CCE + 44DF5CC0CCD1D7F3E5417080BBD43626E5F347D9CF657DFD7902282ED411E6FF + 9354D1E890FE1CB5FAFFCBFFDFD11F80C29FC7F3F21AB8810B00D8E131F1A33F + 5CD06383A3F8D6FFE52783BF8FFF3FFFABDCF21F7EB47987ECACC7210C0006EE + D202707D9CC25F5276A775811D023B00A6E538E4003805BBC88F73E88BF8BBF6 + 750E84F99B77F7AFFFFDF1B7EA0F2E04C0CB7BA21027E66E60D73673CE17DCC1 + 806014006F80C11EDB980060FA2805C1C41D04732C9F019B559BE8F3904874F7 + BAB3170186B7F019A102F67ABD5F9F67AD1DA373ACB5D82B2FA89E97552F3D2B + 2B5E7C4696E30006FEBEF49CAC7E99F873B292F4D026C67A8E975FB076F9F3A4 + 8C0EAEDF75AFBE2C6B5E7ED140304AE1AE03DF14F61233D81B63406303C07A0D + C201ECF582B94EA4CE5FFA886B080EE0F4C1A31406D3E71A444A6807BD051730 + AE60EA0017C0B0C5F45AE80F51CD2355B45E2F67DE759B5E5F6F95EBEFB95DE3 + 7A8D8ED7D05977856C19592A68ED7B6A68523D734DB7EC1EF17AEF0F7AF9435F + E18130BDC6DF7A8B5CCF759DEC1FD1010C0C9EA19AA67D1E229B4E9988980A7A + AAB60E7D5D8CA9134CBA67AB171CD342037C1D0613F394D0068A350EE805F84E + 0308038881BE1108A30947CEC89C9BEF95F58FBDA07A51AEBFF12E195F7F838C + A93929E3EA4EEB5EECB4B97F01C00E7EE90705080C0046D405B6DAC0D547743F + 77488669AC623F009834D175BA4FD47D5F74FD56ECAA95B155C765C18DF7CAEA + 7B9E9039A7EFD2F821DD5FEA7E71DF111911533F0FDB79D05AC02F8EE0611A67 + 6EC48E5AA9D85A2D2376D6C8A8DDB519004615685B65186FD1FD2C80787B9541 + 611CC3962EDAA06F612F1DF6D6BB0CFCBA0BD85A15F0D70170B9CDE308D6FDFB + EACD96EED9534107E8BBDE5CC1E60E5EA1E3E5FA1D2342E014003BEC4D65E017 + C76F02805DE6009EBB2C405F7702C774D0B4C471069B3B78F692008223FC7540 + 0C18F63685BFE60A4E60AF036087BE2E806F068063BF0B8037A907EC20D8DDC0 + F978271CC0E30BB0B79452473020D7017001F24E89A017972F9037887AC0C8C7 + 450018676F04BBA814F44DE52EE0BC3277F0A07106791D04D37ADF41B0C3E016 + 03C748F381A3AD6FE9A1FB8F09E9A113006C1038519A129A316D59F928698EAB + 7800AF8D1399D7E2B56309AA0113F5388DF71D1F445F8513B8B9A5831E692EE2 + 96A48CEE3D524801EDF0D70070EF61190446A14EB0CE0166550D80304A00AEC1 + 5B95B97871F6C671590EF8DA71B93150D85DC4653DE2F9BA0DBA22002E8E1503 + 6077FF122F05775D3ED7D81A07BAE998BABF5EFBD7FB69CC14616E1EECD26F16 + D343FB5CAA02F02D56A91AC00E6EBDBDA6CDB5993C8E80BEA55242A7C7E6D737 + D3D7B37E04BEA554EA1E2D2ADC070E4AE7D2E3F2EB5CE97D6597AF2FC402006E + 0C0267F32D1CF4AAE8FB386BC39CDD4BB77ABF857BF9E1BE7BA706C2890AE82D + 805FFA85F12F85C01EFF9706C0A91AE32EF9F8AF5274F372DE00DCA9FBDB4D5A + EADF71D3D65DA5452BFDF7846735696FBF67FB19F2E728A122A6D5808D1594AE + 2B2D7D3DDA9856B9646AE5CB290F432FB7A6D41C4A8F6F4CA5D6A6E72835978E + FFA26AF22B00704968DB88FE27004EC43AFF034E95FE713754A937D3AF957F70 + 5C8DEC98ABB1F8C7F54D5B343C4750FAC1F71B2BF7E19DCAC12CEBECDF812757 + B41FE6C345A6E85C3E47EB638F5D463F0B002387C07E11BD0A00ECCA1CBD8D5C + A4D3B97011D698C15DC06FAFA8CB40E0E478DF20E4D5A48DBE962A6C58F498AC + EEAF6E56DA92FE1905E7AF41E0087B5B7518202D55A47E2E6BA3F1B6BAD96B17 + D4AC9D6EBC80BFED7473A6C75F0900E7E500D821700A7A1DEAA6F21ABF3EDF98 + 9AA22E414D5089B4CFC8FB45B037CAD7344B606D4105F72E4E5E8F51CFD7E38D + A9B01E20AC1BFC1CECCDAB45D26F19EBFEA6E99F695B51BBA65731FCF594D0A5 + 00F0E5D400FA46F93C7D07C0AC35605C5E00C0067ECB794A977184B883279BDB + B7891E83F3D7E0AD0AC8EB0EE062C5B4CED4F0CD1420B0035F87C9A9DAAB3A68 + BC03AEDD613375CD8C060018B8DB79D41C03BC2910EE3C724E0300DC18FCC5F9 + 0BFCA5ED345A8F1DA371A0EF988501008F9A6B69A00D028F5F6C357DBB4E5E6A + AEDF0E63431A68648EDF09A4755E62F300E02E5374ED94E5067A3DE57337D23F + E3F89DDA10FEBAEB17E04B8D5F73043306FE027D01BCB105E8DAD8A16F8C390C + 36008C22F42D2849F13C6F9DF49AA33115D017C76FDF851BA4AFBB7C49E3EC69 + A05529FCEDB7B4D8DDDB6F7980BEC821AFA56E5E15002FB0B7EF8A50E7D7D239 + AFD4966381C396D279BBF48FE99BCBD7EE0835C922F4C5A510523DEF93212ABF + 713568C35E9B0300E3FC1DB46157067D81C0C800F08E4AED1F90215B49F14C2A + 67523707072FA017089C3A7E81BB068177D327ADF30119B9EF808CDA8FBB37A4 + 7A1EB117205CADB1AA4CA323F44563AB0ECAD8EA5A19AD1A050056018087E308 + AEA99731B54702FC25BD33299C6B0F47207C58261D3D29D34EDE602DF301FC06 + 283CF1F0099341E243C764E211ED1F3D6170D705EC4D630E8101BF33CEDC24D3 + 4EDD20133496D5FD3D7952A6DE70C660AFBB7F1163E0F08413F4F5D89B6E91EB + 630D3FAFE367691CA3269DBD3103BD1900564DC20DAC71C601F672C3F866997C + F6ACB5387D01C3C05EFA068023F4C5C58BEB9796B4CF3897963CF6B02C55CD7F + E05E997B2FF3A488BE43E63D80BB17007CB7CC51017D01C50E85173FF290AC79 + F66959F3DC33B218A75584B9DC48A7AE2F37D35D79108CCB2ACC3D228B1F7DD4 + 003237DEE7E93C4EDF6D6FBD29AB5F78DE6EE0E306E6E6BD3BBAB8C9BFF88962 + 08FCEA135B4A42D37F2DFAF4E56D0106BD0BC8FD9DC1DF756F028BC2D852C1BE + 838330AC71F86B00E83D52D07A8DDE5765CB7BC45E972D1FBC6E60D7012FD016 + E04B1A684F05CDDCB60FC31C0AA037A455DEF9C91BD6EEFA34C05FE60AEB0066 + EECA0D357B0BE320206E4809FDB69EEB6DD9F339800D01718174C03E001FB08E + 5AAFB83A8300BF068049FB8C7B535F638F9E8B5AC0761C69982F7E2435A419FE + EA33D5E752F5454C1F7C41D7018949E10CF88CB01747702D50F7870B72188849 + 8A6640E9B7E7E4D0B7D4A3BD28477FBC28C77FFCC2C02F9032B8602F04D0F975 + 48914CDF533EE3DE05A2024E0DA26A6BA999F5DC56EB37CE1DFBC3793D0F4EDA + E0B835584B3A675D77EAA70B72E33F7C2D67FF742983BF47F4FCC774CDC99F2E + CAC91F0300060453FB17017E4FFCA1007F1D001F4211FE16606EF8D998A74DE1 + 2FE9A79967CED2541B3CE6D870BC01E258D3B8F6EB8FF4F7F791FE7FE9F9F435 + F85D0447B0BBA403F805F81EFBC34539FDE7AFE5D49FBEB2DF1FBFD333FFF08D + 9CFCE3255BC3FAF478AB157C29B87FCD01ACFFC6EEE2E5DFDBC130ADA7897690 + CBBF336BC3DF5671ED603F07ADF789BB58EF0018E501300F2EF0779D0AD0CBDF + 3BEEDE140CFB7B81711E06038A5180C2BC5F710487F7F6167BFF06391806F6D2 + F738F214D2CCD36E7A9798CEE939B66ABB99CF04FDDC7027B0C35FC03073C8DD + BE405F77FF867E00C6EE143697F0CBCFC8AA979E91E50060D5B2179E96952F3F + A7732F58EBF155AF708E9735FE92ACD43E6340F0AA97F59CAF6A5CFBC10DEC29 + A29FCD80EFB26743AA684030B0D75DC2C4891914562D79F249BB36702D701730 + 7DAE1FC4973FFDB4AC78F65983C3B656AF390E7BB956D9F5C9AE4BA1FEAFBB7B + 173C0CFC0D00387501675098985E13B936FAF511088C0B78B66AEEFDE1FAC9F5 + D63366CC8EA51352F88B13184DBFFD0EBBD64FB77AFE3CBC156A027B2A68E6C2 + 9EE016997DC7AD32FBF6DB6556DC3BCCBCF956B95EFBEC2BE8531F78FACD7A0C + 1018D0EBA99F559E0A1A383C5DE767DCA86D84C1EEFAA505084F3BA36BCFDC2C + 134F04E72FF09754D0883E69A0C31E89B91B65E2515CC037CAE4E3BA6739799B + 4C39A1C71EB949C6D5E3FC3D2D63EB4FCAB8A406B03981899970061F955135B8 + 7FC9D272D4E65907041E8523585B7303EFAF9761FB4819AD7D9CBFBBEA64CAA1 + B372E08577E4868F2FC9FAFB9E9151FB8EE9FE52CFB3FF980CDF552F43B71F34 + F04BDFDCBF3A1EBCA54AF7B50765D8961ADDFB56CA886D353272A78EB75719EC + 3507F016DD036F3E204337E97893EE71753C6CABEE8337EB58F7CC0323F41DB0 + 6EB765D6A1AC8A8360DB8BAFDDAE7B6CF6DF3C8819E02FE01737303200BC8EFD + FB6619B052A56D0682978594D00100531B38B87F1D00A71018E84B9F18758271 + FB1AEC5D5C4809EDA20670067BE735AC036C30380A08DC734E00BF3DAFF776B1 + 81610400CEA78336017C13F8EB00D8A1B0397E633F95D5039EAAE78A2098D6FB + 79289CA686465D27CE3665CEE004FE5A3A6855977133A5AB8AB6D358D23C0379 + 0BA03795436087BFA691FABDDA6A02171CC0EE0676911A3A85BE79208C32785C + C1C3DC01FAB62145B4F6533908F63ED0B7C580D1068071EE027BBD26B043DE36 + 40DE44006087C16DFA85D660F220A0B29EAF5C8F1D304EDA0E9E28ED874C36F8 + DBAAFF043D16083CC9D4AA7CBC092730003868B465400BF743466A1F47F0B04C + EE0406FEB6EE3D425A53433882609703601FBB33B895AE03E2E661B1F5594B1B + 616F2A62457584756C60B87B410E7B8B1DBFA9889767BA5A004CCC9DC1D6766D + B8CE816EA9184AC16FD17C87007ACB3AEA3801BB1E47DE0FF7191B02609FA79F + 07C0A5DCBC0E7153984BBF3100DC98FCDC40E03CF44D55EA1E2D4AEFE536763F + 37BFC6D7D1E6EF2DA3745D881500F035ADBA44A5B1A81609F03505E09B8D5B86 + D6EEA5036FB5EF0AF7E603947430D9A47947695AA6B1326F0B7210FC730070A9 + B8EB4AF3BF4CFC3F5CBD1A632EF9F8AF92FE4EAD853F696B0C0AAE754D3BED77 + B0DF2D31FF1DFFDC1AC076BE066CAC71A5C78563C3EB3729D3B904025F15086E + 0042733C2FBFAED41C4A8F6F4CE9BAFCF1AEC6D6FFC5D5E47F02E05F247E7945 + 63FDC3E38F5F5BFB836D52FCC75B2CFE78FD0F993F6257E18FBB944ABD997EAD + D20F952B297F5C3ACE2B1CD3F0830D28EC1FE48D3D51937EC05EFEC356CF630A + 7D2E204DE3EB1AD0F50B875E4438BF3D1DA4FDE66DBA4B2BBD00B7D68B2A7DEA + 001BB08DAF55F49AF45DD9B86B4EC99A285EB349F36EA60CE6A64A216F04C04D + FCC2DA2800E622DB504D1235984B1CBC99CCFD0BF44DA59B92BCECD8E2E34905 + 1D946C1A78BA0D17B06D76F4B81C000ED24D533BDD6021EA01B70EC2F1DBAC2D + 00B8BFB648377A6D58471CF0DB3BAA00809B659BB2440061DBD4B9D8CCE9B900 + BB8D00E0D4154CDFD341A76BB3360AD8EB1BDE26B6C175E5E12E7552066771FA + E99CA94B00C0CD32F81B9CBEA18EAFC3DCE258E30A00D8D33F1BE08DA99E81B9 + A99A6BCCC06F9C3798ABB2BECED352FBB775AF91D246BF94B4E229D55E7CE948 + 953A8001B840E0C641705AF31739FC25CDB3A57AA6051027F2DABF0E8069BD0E + 706B83BBD4F10DA0B7D5C082E317000CE86D35D081AFB6882F65AAB64349EF3C + 3948C79EF23903BEE6F20D31CE650018F83B6C867404CE46772FE0B76D45A8FF + EB00B89303605DD301200CDC2506DCC50D3CFC7A8B77D4350E7D4D09007638DC + 794C5C43CA67EAFA9A03789E7E099F67CE5F9CBE9D71014F5045E0EBCAD23E6B + BFA3AA03639CC206849748B72968A9749DB458BA008481BE11F83AF40D0EDFD8 + 9FB15CC75ECF17C84B8DDF55D25305D87518DC7D26D2FEAC95E6F845C0DF6B75 + 1DB11E00601CBFF3D6C975733DF5F31A83BFA47FB6FABFD1F14B1DDFDE0B0A90 + 9734CEA47F664C2AE82CD5F3D290EE19F7AE397B976F95EB74EE3A5D0FFC7500 + 1CD23CBB48F3CC0DA31D412BB61900EEA7FD7EAB756CE99D01BB7BA4FF5A6E42 + 798C3ABE7B64886AE8967D3264F33E19B481D4757BA4C26E74ED97C19BA873B6 + 4B066ED8A5F31ADF860B789F0CB516F7038EE0BD32780B757E49E55C69EDE098 + EE19D80B0C0EB57E437AE88A5D7B02F4558DA09EEF9EBD327CEF5E19BDFF808C + ADAC96315501F45A8A671330B8E0FC05008FA9C6FD0BF0D5FEC15A19A1C70CDB + 5F2523993B586F2018017F81BC23343EB2A64E261C3E269301B347B8917848E7 + 0E597FD2D1133241DBD03F29938F9D9229C74FCB9413A765DAA9B332FD34373A + 496F88C3E5A4B5D33466E99E8F9F926967CECAAC9B6E9519676F36172FAE5E73 + F69E01EE0271F5D83337584A6880300A719C39376B5F8F554DBBF1169971B3F6 + 5506816FD435B877B8117C6BB811ECC097182920AD6F6374934CB9595F4B359D + 74CF80DFE8F49D7907F0F70E03BDDC847627D2DC07EED1F19DE6FE5DF5CC93B2 + F2E9C765E143F7C9BCFB81BDA47ABE57963EF6902C7AF87E8D879BDBF3EEBBD7 + 5CC1F31E0837BA5DF3A8F57BFFFD76237DC9138F85F6D1C774EE41AB07BCF011 + 6EACE3F27D58963CFEA82C7DE2715558B790FA8B3ABFE8B14764B1CE71F31D98 + BCE685E70DF8920E9A9BF8F497E0F4C215A63FEF425DBF44FB2B9F7F56963DAD + 633DF6E1676AE47F3C33BC2440FD7BD5FFEFD911F2DCDBA765DB876F1BCCA505 + 140538843B973AA3A4967DC3E649F91ADC82111EBDEF4E4360D0EB067FB7BEFF + 46D0876F08354C81C3805FC02D696D1DFCBAEB77D727402C6016357B0304DE6D + D037C0DF006E435A5CD604B764805EEEE0057839FC0D60EC3DC1D1CBFC1E9D07 + E2E2D4C55DBC5B5F6BDFE7C1B909940BCE6000DAFBB6A6EA8B00FA0CFAD1FF12 + D10FB098DAB400428061DD379F1BC0B53AB25F921E381C6B50505F9363EBBF65 + CD67560F989ABF805C20F0913F5C9043DFC594CF7ABEC3DF9C971BFEE97BB9FB + BFFC93DCF2EF7F0C0E563DFF515CC07A0EFA47F4B5009CE68EFD3E3878EBBF01 + 6606C0EA301697EC119D07B802564FFE917AB7E70DAA1273207C588F03E4DEF0 + A74B72E6275D83EBF79BCFE4B8B62738EEC7F321CDB39ECF6B00037F4FFFF1A2 + 81630030B234D0DAF2F304F0EBA99DF5E7E2E7615EC5CFC83C0E655EFFA8FE1C + C4ADAFF2352E87C0B55FE372461F19383FA4EB4CFC5EBFC2FD8BB3997AC1C0DF + 0B067A6FF8C76F0C00F3FB62CE41310E6B5A203B00D8E6F4775B8BBED6D7D416 + 191036C01FFE5D0BC037B87B1DE8867FF74F22D0654D01FA3A304E45DC41AF1F + E3EF0177A907D779F83BA7EFCEF502FCE58108A071A136700A83113187B7C060 + 3F8EF77490BF77431F173D6B37269017E84B8C795A7709FBBC3D34A27200BCE9 + 6DD55BAF19C475A0EBAE5E6F51E6FA35F81B5243E7A1F02AE65500DF55AF3C2F + 2B5E7A3603C088310018F8BBEE77AFEA1C6098B51102E31436E7EF73B2F28590 + F6991677EFEA97A80FFCACAC783E0060624B9EC6FD4B8A683DA79E8735CB9F7B + DAAE0340E0055C4B545C0F963FC343428FC922AE373ABF428F5FA95A465CD772 + FD58F8D8C3E1BA43DF1E46C2011CCA18CCD3EB1CFDC5B13E308E60D7C247C2D8 + 40B00160AE9BF744007C9F5D532991C09887AAB8DE06C81B21306515AC4E3000 + 38B88019CFD36BE81C3268E878B68AEBB6D703E67AEF10987AFFD36ED6FDC3AD + B7C82C6AFA0380E3FE01F08B239807CA66DD72BBEE236E952967758F70A3EE1B + 6ED2E3A223D86571D514D2419B2318D8ABFB89D337CA64F63AD40F564D3C41F9 + 8A333245FB68AACE4FB3D4CF3718009E70E484ED8B70054F3E7EA38C3B744646 + 91F6B9F6944C3A76B301E1F187CFC898BA931A3F662D7580331D3E6D2D00785C + 3D59598EAB4ECA84433A265DB41E33AAFA88D5040EE9A34FC8C8EAA332E2006E + 616A041FB3F4CFE36A4EC88ABB1E938D0F3F2FB34FDD61E077E4DE23326C1780 + F890F52B76D6EA9EB4DA6A020FDF51AB7BDE2ADDCF56C970C0EFF683328A5AC1 + 3B6A74AF5B2543CC015CA96B54A47FDEB45706E95ED91F9404000F5EBFDB32EC + F01025FBE8016B49FB4CCA671EC40C7BEE81EB99A70F180608F330A7EEEB576E + 923E2AFAB87FFBAE04F26E920138818931B77CBDF45BA6DF2F56688B0378C53A + 8DAF37F527060C5EAADF4170FD2E2A00E0DE4BF4BB0DE057DB5ED40D8E69A233 + 00BC70857E5F097580AF9BBF4CFACC5F91C9A16F01008774D140E01EB31717D2 + 4347100C0006FE3A00F674D0D40EEE4E2C0A00EC90378D593C195FABEA315DE3 + 53E749F729730D065F3B4DE7B5EF00D853423B08060E779D3447BA4F9E6BA2DF + 59D798FB77EC74550104771E3F53BAE00EB6B4D0EE020EB57E01C4A91C04771C + 13D69803D85CC153F4BBEE14FD6EACF1913AAF02FA7A4DE00EC3F5BB77857E9F + 1F1A2030C0B73DF3712D6B1C00B7D775D4FFA56D3B74927E779FA01A1F14C16F + 5A231808EC8E6073FDAACC054CEB90378EDD198CA8174CAC6D7958678EE24113 + A4C5807152D69FF38D93F6C3A64957FDCEDF95EFFE23E7E86BCED0D79B262D07 + 4C955603264BABF209E6026E6E69A003FC2D03045BAA681E7E1F112170B15A6A + BC559F2020706B8DB5221E613050973E31C47C9BB8CEE345E038C25E20AF8362 + 8F650216235CBF31FD33318BE308C655DC6DA08A76908E5367305098FB65414D + 3A037FE9975B3C9F02BA79D720EB772BCC37EF322003C04D3BF5C960AE835D3F + DED7F87CBA268B75D4E33BE9D814C64D3AE400306B626BE25EA3AE31250098F6 + 9A76BD54D74913A06C875ED2B43D8A20B76DCF0CDA3AE0450D806E32EF733E9F + 8E5DA4816E0A4CD6F39BDA24E5F892FBB279A5F771C3BDDCD271BFDFDC583C15 + 713F4788B9C9A86B80B811E416815D03C089ECDE3DF371DC32A849CB6E761F9D + FBF8DC53E71EBDDF970F46AC20B882F1033842B362004C0C65638ECF81E0CB33 + 8686FAB9EBAF4EFC6C57AFC059BC2D701C1FFF268A00D8D8535320ABFE0E791D + 7E8FD1E0C81C317EB757037F5111D32AE262519CBBC8405958971E1BD410FEBA + 8AF85CAA061034C7F2525DEDFCE594AECB1FEF6A6C7D033177B9F9C6D4A61135 + F9D70B802FF78770D93F905F2B83C0E91F70E36AF8075D5AA5DE4857A5E458FF + C0F471FA817235F263FCF820C68DABF4875D41E95A83B5894AAD0FD2F3F2FFC2 + 85A1995E14CAF4C2D3422F5280D426FA3335E5BC7AF128EB2A657AD1680EB0E4 + E2C10749B34ED2A9C740993977B92C5DB555FA0FD50D5C3B9D8FBF1B5ED79E3A + 52D9054B5FAFE802664A00AD299D8B1731E2FC3CAA262DF9F992F50E7569515C + D33481AD45F3F1627B4DCB1E3F5B4D0DEEEA794B01DEAB5506820BE2676DDA86 + 4D079057D764F017017C0B3298DB4E374CB10E70D338C60DDCAC3DB017E906AD + 4D5F3DB7CEB7E92365A48C6EDB5B9AB7D7B65D6F95B71AE378DD7C35E5893CDD + 80219CC2CD3AF0645F50E8EB46B2936E3081BC29CC651C9541E0087E5365EB23 + F84DDDBE0D523C2763FA0E79191741DF184BE36538760D04EB38D6F30DD2B5AC + 8F2256D61D18DC50CDAF0DE0D7E1EFE5003090B7ACE730957EB140A47826ADB3 + 01E0E1520600D679FB12D177A47ED900F6F2E5812F16FA05C4A06F10E0B64DDF + B1D2B69F7E210200FB9C435DD6F4D52F51405D1D3BF435C0ABB2DABFC49807F8 + 2690D7E16FE6008E7DD6B4D42F5AA45AE2895BDAB64378029814CFA45E9A68C0 + 376BF932685F225DBA36E97B8AE7364352C534D015D3F58B272EDF087A81BFC3 + 674A3B55DB61334CF48903751DE83AC805EC761815EBFA8E40C1DDEBEBB23563 + E6666383BF63E7491755E7D1F3F5F87999BBD7C06E84BCEEFAA5EFE99DBB92EA + 19D09B08008C1C0077D1759D272DB5B6ABB9829749F7A92BA4DB54802F2E5F8D + 6BDB2D02DFAED3A8DBABF3AAEE3348ED8CAB77B5B6A4740EA99D83D3775504BA + 1A9FBD567ACE5EA3736B746EB574471ABF76EE1A6DD768ABF373D7494F5CBFF3 + 01BF1BA5CFC24DAA0D3A5E27D799BB77A3F498AFEB74DC87F4CE4B368738E077 + D916E9B7748B942FDB6A2DE3007671F76E93EB742DF57D89017E2DADF30AB4DD + E651BF95D4F5DD65EAB76AA7F45595AF01F692762EF4FBAF25DDB3A7A60B37A2 + 70F60EDEB85B866CD91BB4798F0CDDBC57866F0D2E5F6AFD0E8C357F076FD175 + 5B4905BD47866CDB23153BF6CBF09D9552B11D00BC5BC7C1E18BE317003C74E7 + 9EE8E825CD7300BD4376ECD4D86E197D609F8CDAAFD276E481BD3272BFC62AF7 + C9D8AA0332A6AA52FB5596E2790C90B7AA5A5525636BAA65DCC11A6B475556EA + 5CB58CAFAB957175F532A626B88101C2636B03F8CD20F0A1233246D78C568D3B + 7C58C61F3E621ACD3A8DD19F70F4988C3B72547544755C261C3F69757CA79C3A + 63ED6460EEA91B642A3742754C4B8D5F20B0F5CFDE94DD704D53377B7CD6CDB7 + 593B13907BD34DBA8E38B01757CE6D990B67DAD95B65C62DB7CBACDBEEB09BB9 + 0E781DFEBAFB9738377F67504350E3B4336ED7F60E8D4799ABF79E3B4DC17514 + FAB3AC3E21A928EF9639F7DF25731FB85B70F82E7AE40159F1D463B2EC894764 + FE83F71AFCA505FEAE78E25159F2C883B2F081FBAC1E305AF2F043B214E7D443 + 0F1A4C062CCFBA535FEBEEBB65D1C30F07A88BBBF7C187EC86F682871E90458F + 861BE6B44B1E27D5F3E3B25C5F73A99E7F11CE63E6F5F5163FF998D5675C149D + 5956B3F151EDE30E363D2ACB9E795C963CA5AFF1F8C3B2F4E9C7ADE6E48AE79E + D4D863B2FCD927E5D073B7CAFFF6F4949230F5EF4DFFFBF3D3E5CC3B0F0BB014 + C003243AF8D539A9FDFA82C50238022EBD6B9008186490D78010B0B650879498 + C56D0EC81BC0116B70E0027BF30A354E0BE99D03F4C5C90BEC22CD6D48810BFC + 02AA01CC02282E402E03BC110207001CDCC0ACDB7FFE2373F0565DF824E8FC27 + B29FF4D09FBC27FBF57C40605ECB8FDB7BEE7D3970D1A19DEA2BC0EF47527929 + A8FAEBDF4B95B6072E7C60D096DABD218570D0C14B40CFD027BD70A5AE0336A6 + A98A6BBEFCD80478040203756B757C58D79CFCC31772D33F7E2F77FEE73FCBED + FFFE2739F9E35772F4BB8B72FCBB0BAAF37244D71EFE1AC8AAEDF7E7E5E88F00 + 54CE83ABF83339FE87737A0E5CBA387B037045C05E07C0B8733D06943DFC0D29 + A0CFC9A91F2F5A0AE8637ADC093DFFA9EFCEC9E93F680C47B2AE3BF1C379EB9F + 04AEAA4EEBF9187B0D60DCC18061FA765E7D1D5CC8069FA37B98D736F8ABED71 + 1D9FD2739CFCD397729CD4D2BA0EE0CB5CD1CF18A1303AFC6D80C236D6D65DC6 + A10D4E623FEE38E0DA5E33006203D1FAFFC438B8A7F9F9CED9BF0BFF6E07F51C + B52A1CDBB4D6D77FDF1A1DE3DCAED635EEDE05E07A9A67C06FCDA5CFF5EFE533 + 1D0377491FED0F0114524133F6980360C4DF287F7F80DB008FC338FCBDE3E60D + 29A20BA0D8DDBDBC6FDEB2187FC3BC4F78DFF17E74080CEC751730EF47E63C46 + 9F1605081CDEFF692CBCDF49E11EDEDFF41D0023EF6F566D652DF1B75E910D6F + 3AF07DC5E030103805BB0E8111B175BF7B59E340DBE74DC438C6C57902247EC9 + E681BF1C431F200CF45DF1D2F3B20CB8FBE273D612A786F0AA1781BC4FCB6AEA + 08E3187EEE291B33B7F285300604038901BFCC114FD72FD16BC262FDFC5FF484 + 5E2BF43AC67500714D58FCE4A3769D40F459C3B5837561FC882C78F421D3C278 + 1D5AA87DAE4121534580C38B22F4C5F5BBC8D6E21866CC0351F7840C190F007C + EF8FD2B907F418DCC4F7DD1B20EF3DB19C82398201C2EEFC0D0F633944C63D6C + 0F70DD7E6B2CE3706778F02BEE031C08339E816338EE116CBFA0FB8D69375123 + 98F4D03AA7EB6CEDADB76BABC747183C83BD89EE39A6C707D07CFFC27E254D23 + 8D26C787DB269FD63DD199E44137DD1B017D79386EE2D19332E978C892420D60 + 80EFC4A3674D534EDC2A938E9D0D90F7F029DD971DB776CA71DD571DD3B54780 + BF01FC8EADD3FD58FD51CBC862595AEA4FC8781CC035476478559D8CACE6013E + 5CC3A48E3E6A4EE051AAD1B8822B0F9B2B987AC1B8822BF6695F5B346C4FBDA5 + 831EB9EF900CDD552503B690A1E680EE5D6BA46227F58029475225A3F6D4C848 + 623B2A65A8CE0FF56C37B657D663543CF8687BE34DBB6488EE93076EC0D9BB5D + F7D4E181495AC6B87A11FD81EB71FD4697EF5ADDF747F55BB359FAAE0600EBF7 + 8355FA1D62857E9750D1EF4B6C25FDF5D27FD5065DB341E7F5BB84AA1FF057C7 + FD75AEDF8AB552BE3CA685CE9CC021253431D47BE96AFD3EB1CA20B08160C070 + 74055FB77099F45CB04CBFBBACCC447A68AB0F0C1C36ADB0BAC13DB54FBC07AE + 5FC0AFCA40EFF511F8AA32F7AFCADCC0510E7C71FDD25ADA687709C7B8CFF931 + 006083BF5334467FDA7CAB115C0481B5DF6DF21CE93E69AE2A006007C59D26EA + F75A20B049BFE78E9B61FD2E2AEA02038749FB8C4B1820DC8554D2DA6FAF3164 + 0078F43469A722EDB33B8271FE1A001E33D50072E7712ACE030C5601790DF6AA + 3C5574BB615334A6E75175A005100FC33D1CD43E7313EBF7F90A1EF28E69A287 + 8C5301832718FCA56D4DCDE0E8146E3528F45B0E1C23AD078C95963879FB231E + 341F276D078D57E9B1AA76AA363A6ECDFD075DDB8A54CF83A748F34193A56CC8 + 1469314CFF9FA72C96D19B0FCAAAD3F7CB9223774ABF059BA5E5B0EB75CD0C5D + 3F5DCF39452861D5BA5C7F8EFEFAFAFA3A65E5A3A559BF51D2C2D2410380116E + E0A0967D5D238274BE55EF6145C0D7E16E3EE64A63CD7B5664E99E5B683F73FA + 027A7B1003FEAA22F4CD9CBFDA37B7B11E475D604B2BADF273D137181C5DC01C + 93DEF30AEAAFB101D23CAA4CC7A879545967402E4057D7D176A2345B0100E7E1 + 6E0A7F7D0EE1004E63DE77777013ED5FE37D95AFCB2BAC0D6BAE3110AC6BA39A + 980BB8008669A90B6C3581DB71EFB3007E4BC9A16E7E9CC6D379C4BD564F27ED + F2F5D938C2DD547E6F19958AB9B2FBCD894ACD79CCCF13E231C67CCB6EA6C23D + 71EE737B4CD7660AF7C0FDFE7813D492E399EB92DD83377395F6CDF0A5FD3C23 + 28C51C1AC622C44CD6FC6D889FE9EA449DDDCC911BF94E3657C4667EBDB27346 + B85B789D304EE336BE0A15B8160CCC59594E3E07E0A51F65C0D715F95E2995E4 + 72AE148CA6B174CD95949EE34A2A757C29A5EBD3E3D35884E34D9AB4353598F7 + BE2B03C605FE5AAC26BFB20630A036ED37A6E498C2794AAC2B52EE7FE62FA952 + BFBC5F2BFB63E60FB97115DE0C41F9374A3AFE45E27578F368DF21A7BF590B6F + E686C73536E7F182F21F647F09957A2D1D97F101AF1789E67A512CD30B5A8B1E + D2A26D5F69DDBE5C9AB6D48B52F3EED25C2F36E6F0E5A924BB8874923E83464B + CDE11BE4AEFB9F94D98B5749DB2EBDF43CEDF4F84EBA3602603D26BDF8142982 + DD02D0D57EA6182BA1ECA2965C444D5C1CB5CD5F941DFE16C60D01EF9504C0F5 + F672BAD29AA6AD7523A26AD2B2106B466AE6087BAD36455B9D4FDCBF286C8274 + B3E4F0579539811D02FBD86A00AB6C4D6F5319005805F42D5204C3EE103638CC + BA0E7DA3FA498B8EFD4DCDF5EF2105BFA47B76C8EBAE5F9F33E89BF41BC2DFD2 + 00F81A5B1762F9B9861BE010735730ADCB206F6C535D0D000E2A00E014021709 + 088CEB37BA782DBDB3C1E1300EE20B4680BEA6EBF872C117898600B86DBFB1D2 + 8674CDBD81BBEEEE8D4E5E6DDB94EB972F556B80AE0A904B7D5F806F8BBEA34D + 067F13E05B4A5EFF1765357754D4DB693D78A27E99C301CC173297BEA67EF16B + CD17C0A1B4BA6E88AE499401E00A20F074FD7238CD94A6766E5F3153C7B13F62 + 967E790D357A5D067713A80BC0753166CED7A56BAC654C7FCC5CFDA21CDA4E80 + DF71FA855EDB4E63E74B87312AE0EF844559BDDFD4E56BB57E3DD5336D84BC2E + A0AF83DF224D5926DDA62E37F8DB7D5A1090B7EBB4E5D265AA43DF95452DE0B7 + E7F56B22000ED017C86B6EDFB9D4F35D27BDE6AE93EBE6AC956BAF5F2DBDE6D0 + 5F67FD1EF3D749EF459BE4BA051B126D949EF337488FB9A4772695F356E9B570 + A3F4D0F3F45EBC59FAA868D1751AEFA9C7F4D273F45BB65DB54DFA2ED9A26B54 + DA027D7B2FDB9A415EFABD960628DC77C50EE9B7927ABE689794AFDA19E0EF1A + DC0701F20E58B7C7443F85C0C4702CE04E00FCA2419B80BB1100470D550DDE4A + ADDEFDA6C1DB428AE721DB755E357407F57C2B65C4DE6A19B6EB80C6A2E37757 + 48F13C62EF0119495AE73DA479DE2BA32B0FC818D5C8FD7B0CF68E3AB04B4655 + EE9131557B656CD4982AE201048FA9A992F1B53532EE60B58CA9AE94B135953A + AE9689F5073556A5C7EE37103CB1BE5E261E3A2CE3EB0F4B00BF7532AEEE908E + 8FC8B8DA2332B6EEB04C3C7AD400EFF8234765A26A82035F4030B163C765826A + ECB1633246D78E01041F3F21134E52A7F78C4C3C75DAD2395B5D5F1D7323949B + A2536E0810D86F8EFA8DD22967347E6370F67ADC5231DE5248EFCCD86EAEDE98 + DC88BD49E76FBD5366DE7EB7CCBAE32E997E5B48F9E8C2F13313DD7E4700BE31 + 65A46B16CEDE7B81BC77C8ACBB75DD9DA47ABEC340AEA570BEFF6E9B9B7117F5 + 08EFC8D6D2027A173E7CBF4160C47AE2B87F173D74BF2CC4CDC44DE97BEFCEE0 + 2F5A8C4B376AC1FDF7CBFCFBEED7F60159F8C083B2E881870CFECED7396E842F + 34C754B1B8810E085EFA24699D03CCB51BEF1A5BFC386018B76F48EFBCF8F1C7 + 75EE51990F18D6B5CB9E79C26EE62FE1A6BEF6973DFB6416634C1C18F0C1D3EB + 4A42D5BF177DF2D256D9F9E673121CBCC0DD37847AB987BEBB24477EF84AA89F + 1B206F00C0B880C3DAE0064C4191815F6D814A010817DC851CEB10CBD33823C0 + 2F2E60AF01CC78F7A7EE782C40DD02E4059A7DA863A018202C005F07660670B5 + F53E620E285769E0F7633970EEF7527DF153A9BD744EEABE3AAFE38F65AF9E33 + 4BF77C21C0B9FDE7437D5FD23603810F7CA96B54558C2F7D6CF57CAB751E555D + D4B517DE13521417606480C13511F6027EDD811AD20DC754C5DF7E2E87BEFE44 + 8E7CF3A9C1D6337FBC24277F00C47EA1FDAFE4D48F5FC9B1EF80B217E4C4F717 + CCFD5B1F61F291EFCECBD13F9C93633F9E93E33F9DCF00F0993F7D612016509A + 3A6A81A1DE3780AA02D43ABC05E69E06F07E7F5E4E7C1701F00F17E4F44F010C + E30E3EFB277EBEF3E6123EAE6BE8039CAD6E308A75820D0647310616B38ED4D4 + FC6EFCE7014A3B98E6672D82BDF1E767CE45CC01710A7F3DEEB58D398E18F3A4 + 9AA675A89C9EC3FB9666FA6BEA3193A6FB2353E8EBBFB1FEBB56E93980C0E604 + BE585C1318557D5190FDBDE99A0087C3DF934363640EF238EF31E61DF2E6D73B + 280EF039FCFDF3B7EDEF251E8A601DC7F31EE23D597008E39C0F29A1FDBDE9F3 + E1218942AA6897CFD12F7E4F1743E0140C07D73FEEFFD764E3DB2FCBBA375E90 + 0D6FBE186A0BBFFDBAA5804E21307DC030C215BCFE8D62C8BB21A68D0EF037CC + 6F7893F1AB991B1800CC5A402FC01700BCF675D6BF667D73064700CC67B8D50F + D63E02EEAE7999B4CF4FDA67BA435F03C2CF53FFF72959F69CCE3DAB9FF9AA65 + CF85EB80035FAE07883E31FAB4C05E03BEF6B051E15AB2F071AE3D0F597CFEA3 + 7A4D7BEC41BD6E4527F083A146B0C161BD96A1C5944CE05AF528AE618DE97512 + B06BD7BB87F41C7A1D0CAD5ED71E7C58E6DC7B9FC15E1EBE3277B05E9301BE80 + 5E8E9B7DCF3D76DD463CB4C5DA69B79201E4662BE530E7EEBBEDFA6FF57FD963 + 007B752F30F30EBD96EB9EC15CC171AF40CBFC4CC6BAC620B1C6CD49AC7D202F + C22D6C29A2753F6231DDB3CCD073FB1E8587DAD2BD0D10D841F0841394B60008 + 6BEC24757F4F092018006CA9A00F9F91C9C774AF742268F2F19B748EF4D15E2B + 588F3DA17B2A6A091F0F0038C05FF66E4164704194F0187350C7AA5135876464 + 75BDB523AA680FCBC82AED1FA88BA24F4AE83AA9D85B6BB234D1AA91FB0FC928 + 8D0FDF572D4375BF3A843225BBF649C5EE03326CCF01DDAF565A9C3D2E1AB29D + 7D6F286FC2838F8336EFD4FDF22E19AAFB60CA9D0CD5396268C896A08AADBB75 + 0FBDCBDAC19B76C8C00DDB4C83366E9741F4D76D9301EBB71904A67519145EB3 + C54070B1364ADF95EBA52FA077CD26DDF787B141E0E8002E5F891B5805145E11 + D242F75942FDDF954500182770CF452BCC090C08365730EB220CBE6ED1729BCF + 6A04C77E8F05CB0DFC7607086BDF524A3B140610CF592A3D7005931E9AFEEC25 + 45001801790DF4AA00C20E787BE8D821308EE04E93F5FBA40A086C5018691CE8 + DB7D4A700203813D15B4A7860ECEDF90F2D9D23F4FD6EFA013F5FB2A2EDF2973 + B2F37700064FD235F11CA48BC60DDC9EDABFE3664AB709B3A5DBF8EB0D00037D + CD2D1C1DC1C0608BA10881693B8D996600B8EBF819219DB4AE2B4A156D6BA74A + BBE1685A221CC01A030AC77E8711C141EC717707B7AD98206D629F87BE434CBF + F7E31A1E868B38F4B93F00D0C5CDCB7D05AFE5CB83E53C606E1ACABA89D272D0 + F850EF77F064695D31435A54CC94326D9B8C9C253DF43BE78E27DE9057FED7FF + 531EFCEE3FCAD49A1BA5C59805D274D02C3DDF4C693B609AB4EDAFAF3B40CF47 + D6B281E3A4F980D1D2AC7C647000AB0AC0B7A0008503006E8E4338D6062EC05D + EEEB1462C8C741E11E4E7355D9B543A559774A85B97900C5FB49067343BC85A9 + 008053D0DBB4DB409303E066D78698C35F9CC0764C028483C23DAF965DF59C5D + 745D67002F0AFD66067C83B86F97C9806F00BDAEBCEBB714CCF5B1AF49E3D774 + E81DA06EEE9852CACE9D03C00D154D296E82C11DDCBE18E2BA4A81DE22901BE7 + 4BC9D7F8BAAC9F3881CB62BA681F5F497EAF39BBE79CDEA74EEE455F494D01C1 + F17EF7D528BB4F9EBFB7CEFDF7DC3D78EEE3132BB080AB931BCB9AB52C369A95 + 52A9E3FFB22AE62C79C13B8AFAB021E74891EF382749FB3EFEADD4E07CC96B17 + C7937E4E817505801B3859646F097FF3B914EA5E4E4D9B77C8E4313F57039578 + BDA2FED5283DC79554EAF8524AD7A7C7A7B19F0B808DA1A2527C55F5AB0170AA + 522FE04AD615CE53625D91F2FF33BF814AFE9292B952F15F237F9336A2C6E0EF + 6F21FB0048CE9DFF40F07129E56171E3C7E43FC87E9EAEFE0337FF7A3CC9A3C7 + 36D78B56CB5EFABBEE2665AD7BC9A8890B64CE924DD2B9E760FD10E86405E459 + DFB45557694A0D5CFDF0EFD16F98ACDCB053F654EB97A5E973A44D971E7A41D1 + 37947E7894E93ABB40B4E42254B8E8142B5E9C5A06A89BBF4866CE5D14D799F2 + EB5C71CE2F9EF978B830E7E06E0B5D87D2D815E4E016E5C7A5E28D1DE76ADA5A + 3717FA3B3708DB4E373E39F88BC226A8B74A374C09FC4DC7DE2F88F5BA89B2CD + 93F6DB692C11AEE1B20EBAF923CD330E621BF789F239546E6AD65E378811ECA6 + 29A153976FAAB2A46D087F5100C0A59407BD0E7B1D02A7AD81DE38E7001835F7 + 8D7894C3DD163D2AB23E4AD7381C7600EC6EE052009874CF405EE0ADBB745BF6 + CED7F3D5582F5D0FF4E58B434F3D6F4FBE4014C0B149D7037FA9D7EB0E5F83BF + 7D1807B5EE1701707F977EB92AD72F59068183D337B4805E00AF7EE92A210070 + DB81DA5ABA67FDF21505F86D03CCD52F6A01024FD42F6601F0F285CD5441CA67 + 9CBF8C49031DBEF8590CB7EF30FD52A95FE01CFA92AED9346C967E4924D57370 + FA3AC44D012F6D67C06D8CD137F76E12CB8BD4D006831947E8EB32F83B7E81B5 + A609317DB3CAE06E84C096E239026073FB9600BDA562D40176E08BFBB7CB14D2 + 3D93B63940DD2E53974B679CC13A8FCB17759FBECAE680BFD7E1EE8D1018D998 + DABD73D706003C6FBDB97B7BCC21D5F37A73F8F624B680B4CD40DD006D7B2FDC + 2C3DE7037B37C8B573D75B0B0006125FCB79B4EDBF72A70C50D10700972FDB66 + E0B77CF976E9C35AA0AFAA6F74FE2200B00B081C5CC18C49EFBC53FAE1EC5DB3 + 5B06AC0DC0B73199FB57D795B396F4749B42CA677329E4344865AEDF2D7BA502 + 87AF6AC8766E8405F05BB173BF415F0030E22618F57E83DB979ABE9532A6AA5A + C6D51CB4FABD23F7ED93D107F6CB984A5595F62BF7A876697F8F8CA9D92B636B + F64569BF7A9F1E572913EA0F1AEC05028FADA99231D5070200AE3BA8C754CAA8 + AAFD32AEB64A261DAE378D3F7458C7F5960E3AB880EB645CFD119978F8A84C8C + B017017E19738392FEE4132765EAA9D332F9E42999A8B1893A46C05FC48D4D6E + 7A027E81C0B47E4394B8CBD7A1A92A87BC052770B8A13AFD668D735336C25CD2 + 3202807102E3FC9D7E1B0E1F8DDF7A874C8D009875DCB8E506EFF57785DA815E + 27D06B0772131800CC4D64AB3378EF1D01EEDE13A030101865D0F7BE103377F0 + BD7709E929173C749F09100C100602130FF580EF96B9F7DC650078D1830F18FC + 0504CFBBF71E59FCC003B2D460F083B2E8A14764F1C30F6BAB63D5A2871E9685 + A47E2645F4432A3DF7626EBAAB00CDA4D05C48DF5C58E1A63C0200E3F40DE99E + 2300D6FE82C71F95F92AC0AE01DF1272E7581AAB7DE616F9DF9E995A12B0FEAD + 0AD7EF6D6FDE291BDE06E4E01804F490FA15D8F3B6A4B573433AE8E0B8052839 + 087258E4E33C28E23807491CCFB140604096D73645B818710203AE1803B502D8 + 0AF57CBD4E2F2EC7D40DECE97001BC382803F00D6972835B384061B4EF73ED03 + CD3EF9D040F0E16FBF94337FFE839CF8F15BA9F9E2339BE31CEEE234D076FE3D + 3970F103038007BFF938387FBFFA586ABEF9C4E060D517EF4BD5C5F754EF4BB5 + F6AB0D087F602011D8886A81885697362883BEE6420D757C0DC27EF7990155A0 + 2B1018D07BECDB73724AFB38828F7E734E8E7D775E4ED0C731ACC792FE9954C7 + 4748C91C01F0E1EF3F95633F7C2637FCE90BB1B4CD09500574A672D8CA1AD23C + 9FFA09F8FCA5DCA03A0388FEFEBC9C569DD49F15F84B1CF84B4B8DE0A3DF7C6A + 2D5098983985710647F88BE8BB383F2DAF67FFCFF1F5ED67D0B5B4B8804FE83A + 570A85BDCFCF0EF07539D4450E84D398AFF7D74C63A9ECDFEB9B2007C1DE0700 + 070184718007272FA99C03FC05E602850B32E7EF17FA77F2256EF1F040C1818B + A14F4B3A716A4793269C31F37BCF017901C461FD7E5A153018B9FB98BF75FEAE + FD810ADE5340601FEF8EA9D179CF21D6D67ECD030297F4F53EB1F728EF2F7F8F + B016081CDEABE11887BFFE6087BFBF1D00170BF0FBAA01E0ED1FEAF8DD570D02 + 6F5205477070003BD0A505FCD2AE7F23A48506ECB26EFD1BCCBF2E9B4835AF71 + 603169A20D0A47008C5318084C8CF5010803955F97FD9FF3B0C7677AFE3764B9 + 81DE007B81BEC0DD35805F15EE5F73076BCCE0AF8A3688CF7A80EF13DA86FE72 + 8DAD7CF1E9CCF5EB0078E9D38FD9D81DC00E7CFD7AE3EBC828119CC140E00764 + DEA32AC0EEC30FCA626AD43FA6EBF57A65D72DD242EB78D9138F5ADD7C9CC15C + 3B43FA673DE62180B05EE3F43A38EF7EB2643C10AFD9E1A12B326F0080C3B599 + EBB1F6F55A0CFC9D7EFBADB60E003C43FB6836E0F81EBDEEDF7957F6501830F7 + 7A629C57FB067A636610AF190CF8753928BE9E0C2337E3F8D5BD8CCAE12F7B15 + F630EC6FD8E79482BFEC81ECC138DD1FB90C0053E2827DD029C47E4A754CD71F + BFD15CBF805F6A0393261AF88B269F08CEE14938878F8434CFB87DC7D507F76F + 00BF8764544DBD8CA822CB4BADEDEBC61FD23D9DCE7B1C383CAAFAB00CDB77D0 + 34B2B25EF788010287DAC0BAEEC021EDD7C988FD1AD776F8DE1A95EE615580DF + 617BAAE2B84A86EED23DEF0E20B02BC0601E821CB865976AA78EF7D8DE97F226 + 03B786F22643B7ED96A1DAAF30EDB676C8E61D0681876CD66300C436DE298336 + EE300DDCB0BD20C0707407A750B8DF9A4DD267D57A53F99A8D06817101236070 + 396D04C07D0DFEAE31F531A7AF4ADB0600D8E2ABB31660DCDBE6F5FBD4FCA5FA + BD65A95C377FB97E8759297D17AFD6EF387A9C01E0A5D20308ACC21D9C398517 + E83989E30A9EB344AE9DBDD8DCC096229AF4D0EE143697F062E91E5DC2DD80C1 + D3E71B9C2585B4435AAF09CC9CB98759431A6834659E01DE6ED1E18B0C0EEB1C + 10B8CBC4EBADDF43639D265D2F1D54A4AF1EBA62A3F49DBFC2E072673DBFB994 + F5DC388971000380694907DD75CC0C03BE0061625633585B44CC9CC2F40DF44E + 33009C8A5848113DD9D4C1DCC0D3557AAE913C803D5DBF87A3008503F8D535AA + 8EBAAEE3C8D00F4EE05027B8FDF0505F38A497668E361CEB0AC038DC13B007C5 + 074FB05ABE2683BFB41120333728C800F190E9D276D86C291B36539A8E9869DF + 35F73CF9863CFFEFFFEF72D7853FCBD4AA33D266EC42291B38435A0F98216DCB + A7A9264B9BFE7AAE0113CC49DCBCFF282953B58CE9A7F31018F04B1AE8B25E21 + 1D74736070EF00745B7966364A7371AF867B36F1C17D87BE08D7AF397FCDFD0B + D8E5FE50B8A794DE4B72D93DA7F45E5384BF5E0FD841B0C71C0A037C5DA500B0 + C75B761B2C2DBA6ABFCBC04C0E82D371766F2E077CF34A216DEA08CEC0ADF61B + 5B4FDF95CE7B8BB2B55704C0BD85D4D076FF3203C0A1E62FF29ABF79A08B1A03 + C07E6CFE785F8723D85DC1B6BE6DD0D500E014DCFE560200BBF2B0D7EE99B728 + 18A2D25803356F08804D1E5715984041E9BC498F4901EF9554EA9C7F5939DFB8 + 3A5D0E00E795F29BBFB8F859103F57A9799531A9086953507BB9FECF951F5B24 + 677FA9D2B974ED95943FCFE554EAF8524AD7A7C7A7B19F0D804B71D544BF0600 + E7C7255F00F97C54E1B8126B8B94FF9FF90D54F297F41794BF494BC8E1AC03DA + DF52FEE6B7BCF79C3F7E4898AEF081912A3B26AAE19A521F6657AFABFFD04D5F + 2FF481BBCD5AE8850E20D9AC93B468D757D66FAD969BEE7A4206E9C6EF9A6BF4 + EFAC1560B8BBC9A06ECB2ED2AA732FB9B65F85F42C1F2A6D3A7171D4FFCF16FA + EFD1BCA3C15FAF079C5D68BC9F295E9C1CD45E4E2D4BAF4B2F96F9988D392E1E + 6B17E90CBE1643D85429A87515E61BCE5D49573A77D3E80A6E9A4BF95C0C7F51 + 0100372607C1068871E5B6EB1D53AB683F51EAD875988B9A750CA20E7029D85B + 5817E229FCCD3F6D581663CD54851ABF05A5B0B749540A7BD37E6372D8EB1BE2 + 14009BE226DC37E2B619D7CD7AAA861B77D6E866BF877E5148D24133B654CF51 + 80DB02E80D0038AF007871FB4625757FFD384BE7DC778CC1DF36C0DB7EE3745C + 2C0070AB3E3A6700989447FA8547FBA9DA0CD02F56513C156BED409E8E2DA8DD + 40FD423770A2B4A3C6AF0AC8EB0A4FE8EA97B721534D6D064FD12F70D4F6D52F + 761580DD206AFE921A8A7EFB61A474C6F51B6A00B71B5A00C056AB977ABEA474 + 56B9C317176F0A7FF3D0D79CBC8C1300ECEA1A1DBD3E660D8EDFF64060C609F8 + ED3A41BFCCABE8A36E1317E917F4A5067F2DD5736C01BFA6087A1DF0E2EA7515 + A06F70FB027FBB4D5D21D74E5F2D3D67AC32C8DB794A48F7DC6316E99AD75ACC + 1CC0DA12CB743DF36BCCDD0BF4BD8E14CEDA773106FE027D71F0F69C1FA06F2F + 523B2FDA6C0EDF9EF33618F8EDB56093B97E7B2ED8287D966C95DE8BB7581FD0 + 8B70FCF65DBA55FAAFD8210357EDB218E3C1AB77CB008D0186FBAE2C4EE78C7C + 8CC3B75C8F2BC7F58BE8AB0C0027E99D01BD03D7EF3519F055D11FBC71BF69E0 + FAFD52BE768F94AF27A5F33E1942DD5FDCBE5BF799E8E3041EBC75AF0C8AC2FD + 3B74C701A9D889C28D30DA0C02EFA9B4BABF23F756CAA8033873AB64E4814A4B + DB3CFE609DD5EB1D75E0808CDABFDFDAB15501E68EA98EB0B7F6808CAFAB4C54 + 2513EB6B65E2A1837A3CE7A891B1DAE2FA9D505B631AABFDD1B55532AEBE3AAC + 3B542B130E1F8A4EE07A4B098D0B7822F57E8F1DCF00B0BB7D27A104004F3B45 + ADDF9326FA68928A1B9BDCE8F41BA0F9B1DD0CE5C6A7DDFC3C1B416FB871EA6E + DFEC26ABC1DF7013D65D38E6C4B9F936ABC737F3D63B344E8D3FDC3DB7CA143D + 76EAAD01FCB28E9BB9760C3501EF094E216EFACEE466F0EDB7EBDA5BEDC67148 + 1B195D45F7DD2973EF2FA499742770269D03FE5A3D5FBF61FDD07D421D4303C2 + DA7A6DDFF97A5E60EFFCFBEF93C5B87F1F79C8A0EEBCFBF43C2A4B0BCDCD6E9D + 23FD732A80F062EAFE7213FDB18764D993D4FE7D44960082A3E30AF8CBCD776E + D2235C59D467A4A6E3D2A79E94254F3F258BB4BF48638B9E7C4CF5A809108CD3 + 17A5C0D7218139C4741CD2873E238F3C572BFFC733E34A02D7BF15FD8F6787CB + CBAF1E149CBE29F0F53E620EE1FA45F459031002C03A1CA275C0EBF0288546F4 + 81C6CC158EA1AE6F80C0802A07BA015A711E07589C37006360AEBB201D7601C1 + 005EF499038C310E4EC8B03E405CE0D987067877EBCFB2F7D30FA5EAC2A772E4 + BB4B06808FFDF0B51CFCF2BCA588AED4F5755F7F2E07BFFA3C00BD2F007E1FA8 + 3E124B07FCEDA7AACFA4EEBBCFA4F66B802EC09834D0D49205F4E2060E635CA8 + 40C510FFBD9ED7D344032C0193A4250E756781C140D1637A5E1CB5405F0030D0 + 97F6E40FA10F003EFE234ED98B76BC834C00F0F19FCEC9F13F5E90C33F7C2647 + BFFF544EFF1480AB8158A0ABB6BE9E3EA0D5816A70FE0687EED93F7F6590F746 + 60AFBED60D40DD1FCE9B2B9838B017A72FE35BFEE95BB9E33FFC416EFDE7EF0C + 001BFCD53984FBD801700A8289E33876A0EB3F4B0A7D5DB88269D39F95FF07E0 + 2EBF5BFB7F8FFF4F7EBEF4FFD1CFE3C7D3B28E79CEE172584F1FE05BF3B5FEBB + A2E8003E18FBC1051C802E2098F4E0EEFE35D86B02E852E3F7338B53FB99B596 + 425CFF9EBCDD7F41FF4E55F45D8C7119B39E070F18D39A2E8454D001EC0627BD + BF1F9003628FB18E071A00BCBC2FF77EFEA19CF8E3B772DB7FFA27FD5BF9C662 + BC2799777770AAF4BD0CF44DE5EF6FC02F2EE0A0D764F3BB2FCBB60F5E93ED1F + 0627301018F8BB851AE1D40A8F6EDFCDEF14DCC028005EAF01EC29A3F59CAA82 + FB37387D01BF08E72FEB680D1ABFF9AAAC7E0DD8FCA6545F3C27B5972ECAB6F7 + DF961580DF979E9575AFBE281B5ED3E35F7E5E56BFF86C80C0DACF40B0D5097E + D620B0CB523C3FF764E86B8B0378C50BFA996F3037005F5BA3637BC0E871BD16 + D9B525A47C7667B0AF0B503800E0B0EE1159A8E325BA76F953B15C010F2791FA + D920302E60BDE6E998879BB8AE52537F9EC1DD70BD0DD7D007F41A4CFAE7702D + E6BA3CEF41D6DD6B10988C1DB3EEBADDAED15CBF434AE850BAC1FBC061AED573 + EED66BBF5EF711E5167C2F30FD0EC031A520C25E81795AF6100E8067008D755F + E1627F92397F5596B58436F65D068723004E5BEFFBD841F1C49327833BF8C40D + 32DEC06F70FC4E3C16E0EFA4E3B8855DA764FCD1E332EE90D7FB0D7017390046 + 23ABEB644475AD8CA9D37D9EAD25F573888FADA52630A99FEB330180434B6AE8 + 9016DAB45FB557D7ECABD53DE94119B1AFDAA0EFC8FD35D6070457ECAE92A1EC + 7975EF3B5CFB3CE8E8FBDFC15B75FF8CB33766C41914A1F0102F8FB279B70CDD + BC4B866EE141CB9D06766919A3213AE77210CC1A52430F5C5F70073BFC3557B0 + A588DE682A5FBB2903C0C05F7704E3020ECEDF35D6A63218BC0C28BCD660AF03 + 5F8E7130DC0F37F1D2008871FA22A02E701700DC77F12A7302E3FC05F4E2FC35 + 00CC9A052BA5F7FC6200ECC219DC8BDAC12A03C140E028E600C1D4FC05FC5E17 + E70DCA4E2BD40766CCBC416003C1FAFDD21CBE73CC0D4CDB635AA815ECF58169 + 7BCD5CA46D701A0F58BC4646537F79C95AE93445BFDBAA386FF7197A2EC611F4 + 768A3581BB8CA526F00CE91CD3437BCD6007C1068119472770A176B0AB00802D + 4534F3A3F4B8D17CEF267B162018205CAC007F830210C6091C524277183159E3 + 5385F4D2289D73018A33B7F0D009D6922EBAED10CA4BF19039E9A2C936165242 + B7D3587B9D6FC303E803F4F88A59D272F82C693E527FCE192B64C4961A59704C + 3F470E9EB5EFBD2D465C2F2D06CF9036FDA7499B7E53A46DB9BE56B91ECF83F0 + E5630CFE96E1002E1FD500003BFC0D007898C9627D864B0B03BD23C23D9AEBB4 + 6F0FE93704C0067E7B0CCDD45CD5B287AE637D0FEE15A5F7910AF79C8AEE31E1 + 04A6F66F2287BF28A47F0E699F5BA852F8CB7D336B2320B6355D076500B8F81E + 9CF64BB9803BF7D3B5E5A634ED334A812FF2DAC20E6EAF892E5F5F9FAE45692C + DFA775F87B39001CEA0117E40098EC883880CB3A5CBE1670BE9F8E1DFABA3CEE + F0D741B0D507E6B808805D79E8EB4AEF3FA7F7A05DF9B557AB2B01E0522A02BF + AA10EB22D93DF8F45E3CFD9C0AACA0E15C7EFE5F06F25E4E9D7E961C00A7ECA8 + C19A84DBFCD5E4F017959C8777155CBAA9DCBD5B8AA3A5EB4ACD5F56CEFC4A29 + 5D931E7325E5CF7339953ABE94D2F5E9F169EC6F110067B1522F80E2FAFC71FF + 9601700A7FD337F16FA1F403C0CEEDAF9B42E05FA8F40326A8D487D95F42E9EB + 857ED3161488D70B4C73BD6034E928CDDBF496794BB7C881EA3352DE7FB43469 + DA5ECAA859AB1FF6D40DB08B40CBAE7A41A6DEAE5E44E2DF17FD262DC37901C0 + A48CB634137EA1F17EA678916A19955C40AF46F98B651A4FD779CC2EBEAD1200 + DCA20066511ECA5A5AE60867A9FF1BE06F2116E2617D63E748D7E4E3CDA2421F + 0770D8E8B84A0360DD4C01771B11A9A0CBE86B6BD0565BD4B44301E2027FF300 + 3803BD9711EB81C72900F63903BE71A39957067CBB16FA4101F03AFCCD03E03C + E445693CCC15C46618D7AF397F7D7DA2529BF2BC5AEA26BEA56EF25B02787BEA + 9783E8024E01B0A57A5639004E616FDAB6D0B6B9AEA36F757DFB8C94367DA9F5 + 1B6AF922C06F31FC1D9B41DF14FE9A03D800F004693D6062500281E9B703F00E + 9AAC5FA6269514C0B7ADCEB71F3CC5D6B503EE02790D04037F01B85333316EA3 + 6B420DDF02002E48BF240E07F4027CA7E93A6233F48BE0ACE0FE05F4EA17365C + BA805A802DE0374DE1EC20D8602EEB5400DD2C8D7314D017018011B57D81BDAC + 71F09B87BF005F83BE1104779BA45FEE71E40272A3A8F7DB0DA88BA615DCBCC8 + DDBD2E4FF37CEDF49551ABA4C78CD5D2333A7EBDB66FEAEE25DE7DE64AE971BD + AE23C5339017E04B7AE77928387B117DE2BDE6AF97DE56CF5763FAA597F4CE0E + 731DFAF659B445FAAAB2F1E22DBA46FB2A40309097B4CEE6F255018071FBE2FE + 3508ACB1BE3A1E40DAE6D5BB0C02F75BA56B568794CE8C517F523703788BD4D0 + E50BEC1DB4619F89310098FED02D553244357853A50CDCB05F06E1FEDDB25F86 + 6ED339D37EA9D8AEDA419D33406FA554ECAAB4FE60E6AD3E5A95815EA02F37C1 + CC0DBC73BF0CDF07F8AD96D195D4EA5555D7C8F0FD0764C4814A1977F0A08CAD + A9B17ABDA3A9EBAB1A534D2D5FD5419DAF3D20E3EA2B65C2A12A55B569E2A11A + 997408F76F8D8CAF250DB4CED55587986A02ED61D5915A197FB8468F0704D7C8 + 84237532F9D811997AE2984C01F01EA13DA1E393067C1DFE027C271F3F91C5E8 + 4F3D79CA5A0030FD2906804F65CE16BB19AA02FEFA3873C4701394BE8A9BA4E6 + FCBD995ABFE1462BE91767D2DE7E87CC246573A63BADA51E1F9A71FB5DBA861B + B6771B049E7E8BC6B40DAE5FE6F418742737830BEE5F5CBF3885A8F74B1B6E1E + 130FEEA100830B6300B1A59EBCF71E9B43DCA05E9C005D5CC4BE6EE1430FD81C + F0963EA2BFE4D1874DA475E60636E7E33C0679499389EB57B5E8E14764C923C0 + DE87ED46F992C71F96654F3D6610D89C532A6EB207156EC66732A716E93F8396 + 3C5D48ED9CC25F5B9708E09BC25F738FC5D4A1C084175FA8FC9B03C1FFE3D911 + F2CE2B7B65D79BCF19B0091027C05F202F4067D3BBC1CDE750C7E10F7D770106 + A85B482D1B9CBD21C5AC3B0E1D14D12FC0240030316A93BAC317204CCAE700B3 + 10E99FF77C863331B81E01BABE96BE83369FA32DC0B7904E9716808BF647C724 + 757EF77D86FB57E3173E95835F7E2E87BEB92807BFF84CEA2E9D97DAAFCEC9C1 + 4BC0C0CFA55605BC03C491FEF9E0D79F48DDB79F49ED373AAF6DFD771C0BBCFD + BDD4E9BCBB47A9290BF8ADF912900CA0C4910AF80DD0D721B001DF3F5CB49618 + 30F7D81F2E64A9984FFEF8859CFCE94BED5F90E300E03F7C116AFF7E7B4E8EEB + 71C774FEE80F40E098E2F90F7ACC9F2ECAA97FBC24C77E02947E26A77E3C2F67 + FEF4A54157ABD3AB7DE0274A532E0345A9C17B06E91A00F08DE84F970C009B80 + C11A03F23AE005FA3EF05FFFC974FBBFFF2103C0269D3718FC5380BE2E40B0D5 + 068EE096D7E6E7E0674329F0457980EB80D7C12D7D07BA79E04BCB3969FD58E2 + BE3E3DD61DC0C4F8773C08F8B57F77FDB73485710D305FFFCDBD16708D8A74D0 + 07BFFA2CFB7B43355F7E6631DCBDFB2F04804BBAF090421A208CE317B81B002F + 715A1CE79C1B39F80502A49881F00000FFF4494441541B08D6F3EC3BF7BEECD6 + F74648910ED40D0F4020FEEE11B154C11D1FC074DD37FAFFFF87AFCC09ECEF57 + 87BFE97BDADDC0BCD7FD7DEC730E854B01E04DEFBC62E077DB07BF93EDD4217E + 1FF0AB9F1B3C6CC24325EFE8678AB6C060AFFB0BE4C5EDEBCE5EC69BDFD1CF21 + 1560373887439CD621F07A730907B7307103C0AFEADCEBAFE86BBF2B3B3F7A5F + E75E931540DD979F9375AFBC281B7FF78A81604FFD4C6B9FD98060157D7FB8C7 + 3FCF7DCC67FDD267C2B560B15E13784088EBC98AF8F00F735EF3D74A0D3CAED7 + 23AE371CA3EB590704A6BC00A09838D70E83C24FEA3550AF51942A58F244B896 + D9F52C5ED396028A1FD56B21F037D6CFE7012C00B09556D0EB22F57F01B6ACA1 + 7EB08BF5D4E59F714778808B6B2C6DA8DB8FF498785DE601ACF9F7EAF5FB1EB2 + 72F0B0579001E0E8FA4D1F189B7D577006E30406040380FDE134DB9FA40FA9A9 + D8BF38F075201C4A55DC9C015FF63FDEDA9CB60E80BD3F5EF753644C9970FC8C + 8C3FA6E363A7654204BF534E9E95A9A76ED4BD18EEE033E60A1E7FE464D4A958 + F33740605240D33AF00D29A18F18F8050E87BEB635BAF6E051DD53EA71D404AE + 06F602820F19FC75006CFD03DA1EA8D73D6B00C023F757CB887D0100D3C7055C + B15BF7C73BF7C930F6B896E52664BC613C9434D00904060093127A0880D700F0 + 2EDD63EF94C1B11D189DBE29F4453E76000CF0A51D84D6EBBE3FC25F87C10337 + 6C350D58BF45F7FF9B03F44D95A482A61EF080D51B64E09A8DD606080C0CC625 + 1CD60DD06306E97988D9DCF275962ABAF7A2552165347DA0AFD5065EA1DF5956 + DA98783F5DD37BC10A83C0386B81BBBDE70630DC6B5E716A689BD35806805538 + 6F53673002FEF69AB3D40030C03605C09E223A005BD5349D8F699FBBE1FA8DC0 + D701B0D705669EB59C839F1308DC4B5FA7FDC459D26EC24C730277C569ACE702 + F4520B98F4D1DD80CBF4C7CD6C008071093B046E376AAAB4A10E704C03ED0AA9 + 9F5DC05F52444FD7EFD6FA9AA3F5FBF818BE73D39F6920382F83C1D10D1CDCBE + 0501805D388281BDED86B91CFE1662ED190F4DC1EF5853AB812AAB193C56A82D + 6C7581074E91F643664ACBA1D3A559856AC42C69396E9EB49BB244DAEAF7F8B2 + E1D74BD9D019D25AD7B4EB3F4DDAF79B24EDFA8E9776FDF478EEA350FBB77C94 + 34EF17806F5E0E8069511AB37884BD006083C071DC18006E461D5F15E0370F80 + 0B99E4C27DA57CDC9DBF290076794AE8E6CCC7B4D18D01605397E8F425F57306 + 79FB4BD34EE5D6A6A2145BB32EE552D6B5AF2AF44D11D6E681701E00BB7C1E31 + 97AA542CD55503E0F605006CD91175ECC0D6E5803785BC693C3F4EC16F0A7D99 + F336EB27C0F74A0018A5F7A27F2B5D09FE5E2D20CEEEC1A7F2FBF0F978223841 + DAFFDB57A74C8DB396643E729D941D15CDFF4B2902E046999671A8021F4BC1EE + E500EF95E64BCA59DFE594AE4D8FBD92F2E7B99C4A1D5F4AE9FAF4F834F677E5 + 00F6784E85E372EB1B28FF3FF31BA8E42F29992B15FF3572F89AC8A16FAA926F + 965FA8061F04BC295DC9BA467599F57EEE824A7D98FD25947F3DFDFFA32DEB22 + 4DCBBAEACFAAAD5E78BAE846A7B76E8ADAE845AFB95EDC5A0051F9BDEB5A6AF4 + 0618DC499AB7EAAAEA22CD54A4866E6ACEDFE8FE55F9C523BBD0F845C7C6498D + 0203C17AA1423978DB98D20B653E9EB6DE37B50A103605BFA61685B914D2BA1A + 9B4BE3A9D235AE66C0DCACAF9B909C52F88B1A03C00679A31CFAA600B839731D + CA0DFC366917E16FC7FED2849801E0008289A7F037AF0CFE72BC1E03F875F85B + 16379AD91A551E02E3FC6DCCFD1B5400BEE86A00B041DD642ECC8735B6518E9B + ECC6946EC4D3189B74DBC8B3F9E7C950402F4E5FA0AF6EF65BE8061F206CF104 + FEA6CE5F03C31A279D730B5573ED9711B3795D0704EE1B5CBFADF50B4B21D5B3 + CE4508DC9A1ABE7D1D028F171CBFADFA21FD0285CB17E83B70A2B41C30515AF5 + 1F2F2D81BFDA4F01AF03DDBC80B90674758DAD2565B3CAE70AF017101CE64C11 + 007BDBC15CBF8814CF01003376C76F0A79A9CDEB80D7216F067FBD4F1B659038 + 81BADE07F8BA0C0AD326EA4CAAE7F1FAE57BC202E93A3100DFAE0900EEC63871 + F7668E5E003060775A01EC9AEB97F10C1D27EAAE733D66AC921E33579B7AE2E6 + A57F7D48DF1CEAF4AED57100BD7D81B8A4649EADEB80BF73D749EF087C71F422 + D2348774CE9BAD4FDD5ED467C126E9BB50E30BB718E0B53ABE4BB70418ACF17E + 4BB767757FFB2DDD66E99DAF5BB8C9C6FD97EF34676FBFE5DBA51CF0AB32E0AB + 1AB87AB70C5AB347FA585AE7E0F0C5F1DB7B85CE038001BEA46BA6BEAFB97B01 + BC7B0BB077FD1E03BBEEEC1DB2E980B51E634C7F2063ED038083AA4D155B2B65 + E856EAF686FABEEEF21DB60BC85B23C3F71C94E1FB54DAAFD018ED481D8FE206 + D9BE6A5D5395B97F471DA891315507654CF541198D6A0ECA884A3D8F6A4C4DAD + C6AA5507828B37BA7749DD3CA1BE5AC61FAA96B1872A654CED01D3D8FA2A8B39 + 0C9EA063E0F0A4C33532F968AD4C8A9A7C0CD85B27138E1E0C0018707CB44EA6 + 9C3A2AD34F9D9069274E64F077FA69DCBDC1ED8BEB770A801728AC7D8030E99F + A79F2165E16999441C9D3A2593F538802F373581BDEEF8A5F59B9E79F90D5307 + C15E7B0FD06B6E1C03B6A478BC334BDB3C8D34CFAA997770A396F48EF768AB6B + 74DE5D3C9EE2D135DB1CC0C1E9EBB0D6653799EFC511146F1E1B10E6F5F467B9 + F33673FCCED5750E7FD13C40EFC30FEAF1F7E96B311F9C497E337A11001840AC + 6B2C8DB38A3100989BE00B893D789F81E2658F3D224B55068D0D00AB6C5D104E + AAC54F017D03FC5DFA64C1F1EB8E2C6EB87B4ACE70533F405DEF3BF07508E069 + 431DFA12F7BE43025F87000CC004D6E108FE0FCFCE2E0964FF5AFABFBD30439E + 7EE378066C9083DDB44D812F6B117DC431B4EEE0C5991BA0103581C31A622944 + 72857170F2BA73D1C12F6D31C87A47767FF6B6EC3DFF9E503715800650735845 + DDD4EA2F83DB9263005E06DDBEC4755980C0695A5E5CC00685F5D8AA0B1AA746 + EB053DC7799D3BF7B1397FEBBE221DF34531F8FBD56752F7CDE726802F224D73 + 01007F2A87BE67BD8E49E7FC15F5678188B84A03E4CD5CBD0679CFEB5C80BC1C + 0318A67FE227206E9803141BE4FC418FF9FE7339F123F0F22B5DF3A590F2F9D4 + 4F97AC3D6AA018C0195DC0BA1E6849FAE7D3FFF0A5DCF0CF5FCBC93F7F61A9A0 + 4FFDF1A29CFD87AFE4863F5F32D13F1961A8C3565E93FE99B8C604F88D2EE01B + F4756ED09FE5C63F5E929B22183EAD6360EF6DFFEE7BB9E73FFF49EEFC5F7E94 + 9BFEE16B730623E62D2534C727AF0D84C6894CAA69DAE3C05AC6B42A87BFE9D8 + 7F4640AD435CFE7F5D1EA7EFD017D17731072C66AD9FCBA0793CD6C7FC1B00E1 + 1DDADBBF29FF9EFCDB1B202EFE37AEF952FF6EB4655CAF7F2BFCADE208AEE6EF + 1667B0FE0D56EB1A77FD567D492AF1E024070257923EDCDCBECC07F7EFDE73EF + 1AFC052E3BF8050A23FA9CCB41316099D4D1FBED0187007FED418728C6BCA778 + 9F547DC1FBE853ED7F18DFC3EF581F304C9B87BD290046F45DCCFB7BDE4170D0 + EFACF6AF01601D6FFF40D7BCFFA6015F5A40B03B805180C26F668077C787EF58 + 6A76449FF50E81D7474730635AC6EE00F6E337BCC9BAD72C0DF4EA575FD27960 + F12BB2E6F517AD56F00AFD8C5E8773F8B59764AD7E46AF2565F44BCF859AC02F + 3D6BEDB2F899CE5AC62B5ED0CFFAE7A9035CD08AE79EB6EB8397090022AFD5D7 + 5CF9C2B3B254C7E1DA1080310F14858C128F596CE5F3BA86878C9EE21A13AE37 + 00615CC00877B065ABD06B15303800E14764D9531A7B52AF858FEAB5F1B10765 + FEC3A18C02F0976BE1E24749037D9FCC01E43E708FCCD16B2CF0D79DC436A7D7 + 62AEADACE7E1AB397A1D9FADD7FA39F7DEABE307F4DA7E9FAE231BC7FD599F16 + B12F98757B7C604CF70C067B5580605A00B03986754FE1E0D75DC0F4D9AFB80C + F6EA1E06D1B7CC25C4E23EC8F63ADAB296E3C978E200983EED04DD474DD4BDD6 + A4936765E2891BCD090C0CA605FE4E4900F0C4A3A7747FA66BB58F261FD33E4E + E1A3BA6F3BA2FB3415B129C7754FA6EBC61F3A2123AB0F5BDDDF71F5C7656CDD + 31DD770607B00360FAEE001E5919007001041F91D1DA8EA176B0EE5F81BEA986 + EF63CF0BF0DD5FF4D0A3ED7955153BD833931D878720F7596CC44EDD3F6FD735 + 5BF748C536B2EA0437B0016177FD4647B0A77E7608ECE381B880551657D127EE + A038388A75DDFAAD1900F634D0A87F22A0EFA0B59B4C406080AFC3DFB066BDC6 + 990F00D81CC0CBA819BC5AFA2E5D23E5CBD7656377FDD2028089F75FA26B16AE + 963EF356482FAF011C41AF398217AE3440EC7018A76F5E4060443F03C4388323 + E875006CEE5FD6CE8CF58319E3020602EB3CA9A2BB4C9B6B2017D89BA5819E14 + 1DC2BAD66A10EB718060D6037EAD0EB0CAD6C5BAC1D40236F7AF8A34D0369E78 + BDB98253084C1F018381C06D55ED33E09BD714FDBE4C7D60DCBF11F28E0CE037 + 05C040DFF6A4755615A78406FCE2FE2D40E082DB37387D5DE938730553179854 + D0C05E95815F95F5078C959603C6D8B84DFFF1D279D074E95A315B5A0F9926CD + 064D926B064F966B864C912615D3A5C9D0E93A9E2A2D074F9776AAF603F4E7EB + A7AFD7679C6A8C8A7B2B23ADF66F0B6D49EDECB03705C029FC450E7F6DAD2A85 + BEA91C003B044E4130F7879AF72864934B816F7E1C62DA26C0173904C6FD6B46 + 09006FBA2602E054C1F0A0C768BF4CD5A29B8EE9DBFD33EEA3E5EFB79171AFBF + B6E5D2B46BBFD04639CC05F8221FA7403885B83E9F8FA773F978AA9F0D80B947 + 6A0EE0821CD6BA8839E0CDCFF97C3A36F89B00E054C49A44A80BDCFD9702C0A5 + 606EAA14005F0E023700BDE97DF8FC5C232A3083A0BF3DF72FEA74D50A8C2672 + 9A126C268DFDD515B95149A6C51C2C26E16329D82D02BC098B2B397F3572D677 + 39A56BD363AFA4FC792EA752C79752BA3E3D3E8DFD2D01E006CA4E1AFA4DB4BD + ECF1BEBE51E5FF67FE0E7505F09BAAC11BE6B7547C63D2F7FABE0DD65C85D20F + A2A0521F66BFB54ABD968ECBF4835CD54C2F38404ABB6034D50FC06BF4F75ED6 + 599AB5EA6E357D710A37D1FFDF666D422A686A0337D7B6657BBD98EAB9AE69D6 + 5E9AB5D4F9967ABEC62E3045F104FEA66A992802DCBCF2174A8B736C32E717E5 + A2E35A15206D067FA3D278BA0E609B8E4BE99AD685BE43DE4CFA3B75C0DB10EA + 5EBD780A2E03C011F8A672285C0C8071FA46F76E07EFEBA64FD5AC836EF4AC5F + 00BB2106D8D556E5ADC5E2791CF69604C00904CE6F3ED3CD6B8815C3DEA64550 + 978D6D68A99F821A07C06C847503CD9CCACE13FB79659B71ED67D27180BC15D2 + D2523507C88B9AE302D6788BEB0AE017A510D8444C05F475017E8B417080BDC4 + BCF57E0100037FF54B51B9F663AAE7D60342EB299D01B62D074E94161A6F3160 + 7C067053A5D037EDA772C0EBFDF615FA05D0A45F00550E848939000EA037405F + 07BFED874D938E236648E791FAC5755470F19A22FC3510ECB138EEA06AAF7D77 + FBBAF2603785C1692C1D77C1153C69917E5906FA027A1749B7C9A4758E7DD312 + 73F576998C4A00E0A92B54A476C609BC5CBA017D67867ABDA9A8D58BB3D70518 + 26D67BCE3AE9337F83A56EEE11D33AF75D08C4DD14D239CF596B2D6BFA2CDC68 + B0D7E1AF3975976E3590DB77F1D6A86DD26FC936295FB4D520308E5E1CBC7D75 + 0D699E69FBAFD8692A5FBE4306ACDCADED4E8D6F977ECB77491FA0EFCA1D42ED + 5DDAEBF43548DD3CA5F294A5D31AB6A546CA57EB318062555F5AD23B931A7ACD + 1E3D6E8F0C5CBB5706AEDB2783D6ED9741EB83BBD7216FC596AA4C43753C44E3 + B4159B2BC3585B64F0D7FABA766BB50CDFAEED368D037EB7EBFA58E7D701F088 + BDA4C5C319512BA32B714A70A32CB825704D8CA9AA0BAA0CEDE8CA8332AAAA46 + 46D5E85CACC18BC61CACD3588D8C3E582D63EB6A54B4D542EAE60987AB65E2E1 + 9A0081EB2A4DE3EAAA345E633210ACEDC423B87D0F66D0D721F0C4A30765FC91 + 1A197744D7E978CAC92332F5D4119972E2B0CEA1A332F5E4499979C30DD63AEC + 9D761A07CA29C1090C049EAEE319379CB538D03780DFD345E097BE8FB9F9E9B5 + F1F2B29BA537C5748ADA9F7AF32D06792D2D6304BE08B89B8D6F2345E39D32EB + F60200A6CE6F56D72FA67CCE6EF8DECB8D61006EC1A9EBCEDB450F3F20F31FC2 + 8574BFCC51CD7D508F79E87E997DFF3D32F31E7D0D524F52D35763C179749F8E + 59F3A02C78F8219973FF7D325B5F673EEE5D4B6F49FC411BCF7B1085634875C9 + 5C80BA38A11EB4B498CB9F784C563CF5B8397BCDF1FB48B8B9CD4D6E83BF4FBA + D33708E0BBE4E950A7D15C563A4629E4E526FD32527BD2C675CC7113DF5D6208 + B0EB4E3007BD691FF95A00B06BC3EB2F9B23B8F2A507E5F72F6C96FFF3D9F125 + 21ED6F2D5EE7D397B7CAE9B71F2C8234F4013A0E6603C879DBE680BC0E794807 + ED30B8F818DA00813D96D5EFB5B60090A821BCEB13AFD9FB7606A568DDD10BA0 + 0AC00AE7E27B067FD1FE0BEF09A97381B91C432ADB9056F75353067523EC4D01 + B0C35F5AC6ACB5B8C66ABEFCDCEAFD565F440102577F410A5F9CBBC0BDF30681 + 81BC4101FC3AFC0D00F81339FCC3E772E8BB007A1D0ABA1C1402800BB57A0370 + A43DA4C7D307541EFBC3395D1B52190760A9C768ECF81FCECBC93FE25E0D3AF9 + C74B191006FE06001C60268093F39C8A00F8F43F5E92137FBA2867FEFCA5DCF8 + 8F5F1B7CA545C05EC0EB698DB93BF6A4B6A781C051F481B7C05E20B03982F5F5 + 6FD6D7BFF94F7A9E3877F33F7E6329A03D2D3450D8A47D3BE69FBE969BFE49D7 + FCBBEFE4A67FD67540607D3DD2529FFEB3FEFFA8786D03B611C4F23B00FAF2B3 + B1069D881018390466ED516D3DEEC7E6D739FC75B7B0C7BC9FAEE5F70954E7DF + 0C40CFBF1F7F0FF5DFD0F26F57903BB9FDDF997F639CC1C45CD55F7C1420B1FE + EDE018C745EE4E7277F80286E913F714D0F4F93BF354D10E7D190386691917E6 + 782F84F783BFBF78A88236FCFD13E3618950EBD7DFB73883794F018179C082F7 + 2D73B47C06F86784BF9F1D0AA7EBBC6FE2B3E483E0FE75E81B206FEC935DE09D + 90D299B4D03BA80FFED1BBB2D91CC0AFC9EEDFBF2FB597CEDB7B13000C1C0602 + 03773DCD73EA0C4E0170E8E3127E43D6BCF68A2C7F513F8B5FA3A6F0EF64FD5B + 38835F90152F3D6BEEE1B5E63C7E298B2D7B413FD701C02F3E637DDCC21E5FFA + FC53B244AF0FB4C4380678CCE7FDD267C800A1D780179ED36BC50BB2F2F9E76C + BC9C98BE3E5AFEDC33B2F82960310F14E975E2058DE93CC7222032F5E8B9EED8 + B547C7D4130E35851FB554D1D40A460B9FD06BE2930FE9F58954D3A4850EE9A1 + B91622AEB9731FBCD7AEC173EEBFDBAEA57E6D5CC0B55BC764D4A096B0D5DDBF + 8F87B034FE809E5FAFBF805FFA0106335700C073EFBEC7DCBEA487F614D19E35 + C461B0B98699E381B3E802A69F8E71034FBD45A57B17D32DA1656F8378F8CDF7 + 381900D6964C28BEFF612F34957D17FBA733DA3F73AB4C3E7593C1603419F8AB + 0A70F80699A47257F0B4D337CAF453ECC7741D25378EEB3EEDE809D5C9088675 + 8F76EC8C81DF31B5388471051F93F13A76E72FF0D735BA1A0730CE5F7454C788 + 75AA2A9D3307B0EE5D0FA0D0B754D0A484DE5B2DA3F7D598F377E8CEFD324C5B + 62FE00A4BB8247E99A317BF5B89D0764C48E7D068387EFD81FB47D9F0938ECEE + 6007BF43B7040DD91C1CC1A14E7000BDD40F76F05BE42CDEA8DF27D66DD1EF06 + 210D74EA00C6D10BD40D0AEE5E6205281CD6642EE1D50102F75DB1AEE00C5EAE + 6D54EA02C6FDEB2E6062FD17ADD6EF45ABF5BB958E23EC35F72F29A14919BD68 + 95F4252DF4BCE5C52EDF398574D00E818933C6116CEE5E406F04BE066E55699D + 60643581630C65C038C25F9CBF3D00BE1112730C6DC749D707D76F04C79D01C5 + 3A269DB401DFE8F805062303C3B88927CCCA524477A436303057C79DC6CE2CB8 + 8249F15C42387F3B238EC5194C6D5F5547D23C1B0C0EE99E1DFCB6A586AF8A5A + BE0102170360A0AE397C2B423DE0D4F55B2483C1AAA113A4CD60EE5104009C81 + 60DA0111080F1A2B6D068C972E03A64AD741D3757EA2341F34419A574C96A615 + 53E59A2153A5C9A0A9D254D56AC834CB60D61EB770390EE071AAD10680DBF41D + 652EE056FD464A8BBE23A48CFB3809ECCD8B3987C4D6CF524117445DE014FEA6 + 7208DC2CCA523B47E0DBBC7B6C13D97D271CC33D11EB079B80BF8C53088C3277 + 704C09EDF7CEDCF95B0480E3D8E7EC98EC3E5B5E110297502900EC30D79DBFA5 + 80702A83BBB1EFE749E7D1E500B0CB00B0CA017059DB007E53176F0A6D7D2E1F + 4FE5357E11EB9A77EC9D1DE332F81BE5D0F75F0200DBFDEA1CC82DA52BC3DF6E + 52FAFE7B4EBEA6D4BA64CED9C1BF0A00DC189729D3F9BF064FFA25BA0A001C98 + 5929907919E5D7FBF86A941EE3FDAB51FE3C9753A9E34B295D9F1E9FC6FEA601 + 704E45C725E36C4DA91FA848F9FF999FA7A23FAA1273DE36B6E63711FF607F6D + 001C416FA9381F1021CD31603480E0521F2A8D7DD8641F3499F21F643F4F7C18 + FFFC0F647D5D7EF6E61CA7C7EB05A7592BBD489675D5DF63E7ACE62FFFAF19F8 + 6DD94D9A238D376BD54DCCF5ABBF033E2C9BB6A0F66FFC5D00815B4607B05F40 + 685D8C7100A74A217084B9572BBB60C663F2175297CDB50A70D7A16F0A7BF37D + 13CE60EF27F3D9BAD60DE590370F7BE937B6C6C49A92628E4D4D6F956EA080BA + 6D7593D456374E89880718AC1B35606FFB7E724DBBE0F8BDA67D7951DBA4437F + DD840D50E10C0E02F68618F54274F3D8A9D09A22102E0060E673B1CB387F7D03 + 5B5000B87998EB2D02ECDA9395DABA4A1D9302605778DAB278136EE78A4F6CBA + CAE2139D6CEA0B9BFF007801BFF645A077027B01B7D7018AB5DF67B4B406E212 + 4BE4CE5EE440D841AFC5B56DA15F5A107D9CBF9602DA6AD9007F434DDFD603A3 + 22FC05D65297B7D5A0494522962981BD0E78D371B1A6861ABF4371F6CE30B5F7 + B642BF1892E659FBA47B0EF057BF885A6A6714FB23F50BE928FDF20AE81D159C + BFD4F96D4FCDDF087FCDBD1B9DBC005C6AF6A20EDAB774CF63E79A2C5534299E + D1F8D85A3F387C431BE4B57D0DF24E59A25F961799BA4E596CE3AE9319EB1775 + 1D779FBA2C00DE69CBAD2E6F1760F094A5BA766900BE515D75CE3423A4736E00 + 8113000CF8ED3E73A56A9539802D65734CE38C7A2FD8287D176DB6D6523A6BDB + 07288C9B77C996CCF54BDFD2342FDB6ED0D7B494D4CD61DC9794CE3A3F60E52E + 295FB1537A2DDE2283D7EE97D975B7C89CFA5BA46243A5F45EB25DE7F7C8C0D5 + FBA5CFF2DD068081B9FD54BDF53CBD7009AFDD2BD36BCECAEC43B7CAA8EDF532 + 70DD7E03BC83D7871ABD03B43F60ED3EEDAB88EB79890FDE7840860270375706 + E01B5B17A017B7AF01DFADD1F1BBB53A93A580B6B96A19B653B5A35A2A76EAB1 + BB2A65D8EE2A55B5397D87EFA50E5A70008F3C502763AA0FC71B65B53A3E28A3 + 2A6B65DCC1433281D47CDA321E7EA05A465456CBE883B532B64E8FA1ADAD95F1 + F5A4E9ABD7FE4119531740F01800F0A16A4BDF3CE1C8C1A0087D81BA938ED519 + F09D70C4C7C0DE007C1D06679098353A3FE5E461997EE6B84C3D7954261CADD7 + 3890F8B04C3B755266DC70C60030C0D760EF191C28A7650A2E5F8D4F3B1D1CC0 + 53756EDAD9B332FD46A06F48F36C60575B77F8A67238CC1A6E8A226EA0CE2275 + B3B67EF374DAEDDABFED3699E6D2F18C3BEF3417B0B981CDED1B6400F8AEE0F8 + 991DEBFAB91C00CFD116F81BDCBFAAE8D6E5E6B2D5207C50D7E0267AE00199C7 + 4DE5471ED456630FDE2BF31E02DE869BD2B86F973CFE882C245DF3C32A6DE751 + C3F711E61E97654F3EA1F38FCB8218072A235FB3E8B187EDE6F5C2471FD4BE1E + F3E4A3B20CF8FBD463C1D5ABE75FFC7870FCDA6B3DF988DD30F79BE7AE65CF3E + 91A5DEF414D0067A81BF2ADC5DD47404E6E2DE5AA231C4BC41E0179F3510ECA9 + 429183E1B445EB5F7BC9A02F2DA946B7BDF386ECFAE01DD9F2D6EB96EA14012A + AA5F7F445E7EB95A7E7A7E89FC1FCFFE3669A2A9EBFBC71797CA2BAF544BFDDB + 4F1870D9FC5E48CD8A336FDB876FCAD60F020006C8E2C6C5955B1CC715480DD1 + 006D03D80D0AEE5DA010C70284DE915D3155330AF0967306572FA008A8149CBB + 8514B50E6E193BA40AB10F64EFB9F7CC0189005BB822F77E1ED2DDB28694BA21 + AD6E80BB9E6ED7FB0E80E9BBFBD7C7AC39F8F5E772F0D2E7067E81C0755F9D37 + E76FFD37E7755ED7E9DADAAF3E95BAAF717C6AFBDDA706EE5C00E1FA6F3FD1FE + A7110007D00B2C746898826054FF354E52523E8794C32E079680C700283F31F0 + 7BE24F5F581DDF633F0280BF30407AF40F17E438CEDA7FF85A4EFF19081C00F0 + C93FBADB951627EF1772E61F2FC9D97FFA4AD77D2967FF21C05B77E09ED516A7 + 2F3AFB4FDFC81920B0BE86B9705538745D1C07DC25CDF32DAA5BFFE11BB9E54F + 5FC94D7FBC2437FEA4E78E8E609CBF06895360FCC72F2D7EF33FEB31FFFCADDC + ACAF751687B0BEEE0DFAFF639059FBAEF467E0FF8731F11BF4E7E3185BA7C710 + 47E614F6B51A7757731EFEF23BF63E6BD398AF0DBFBFD0C735EDEB51010207C8 + CBD863FCBB3BFC0FE2618050DFD9DDC336A6AEF0F77A0E953B7B01C00E7C8985 + BFAD10636C8EF35C8CE35C1C870318087CE08BE00206F286F757786002D72FEF + 19FEEE69C398B9007B81B9BCD781C00830EC0F7FA0F0000729D63FB47E0A8001 + C3C8D7FA788781E03765BB7EA6F0B9B20D07300F91007F7100BFF786096770C1 + 1D1CD659FBBEC63F7CDBC69B70FAAA826B380062149CBE418C99A30F985DA39F + BBC0615CC0ABA30378D3DB3AD67987C50E9359EFA017773022AD34719F7320EC + 8078350FF5700E3DFFDA575F96952F92E50107310F033D27CB9E7B46963CADD7 + 194A0A3CCBC342CFC9AA175FD0EBCBF3B666795CBBECB9A775DD53B6CED63EA3 + AF014CE6DAA3D7B8709D0AA50B02007ED05AD2462F3127F0C3B21818FCC4437A + FD0BE9A12DF34504BDA6470A9AAF631ED442CC2DD4EBAAAD7908917583720A64 + DAD0B86AFE030FEAF53F82DFB82F98771FD23E311E108B201828ECB2F19DBA9F + B85DF71FEC536EBED5E0EF1CDD7BA0D91A07E8028B53784CEDE0E9BA9FB1FD0D + 8E61D534DDEF4CBB49F73E1A4B81F04CCE89DB9854D3DA9F71A3CE9FD53DD099 + 5B64CA69D24307109C42608423184D3D7D56A69D092078CA491CC12765C2B113 + 32CED2421F97898701C137E8FEEEB4398181C068B28E271D3915A0AF0A183CBE + 2E4061524307188CE803818FE8DEB55E46C77D2DA28F80C1D4021EB95FF7AD8C + F7D7EA1830ACE3CA3A19ADE391EC93F7F0C06400C5A318EB5E7A24A266F0CE4A + D501D33003C207A4621BA557F648C5D6BDD6B756355463E61206F4C6795A770B + 5B0A6903C4C1156CA9A1D76ED6EF075B55211D347247EFC0D847805F4FF73C78 + FD5619043CD63ED0774084C2B887696DFDEA0DD21FB77074FF027F71043B0C0E + 69A05706F00BE48DCA1CBF2AC6FD1602805704576F74FCF69A135345CF5B5E70 + FC2660D800308077D622E9317BB1893EB0D71441AF8B39D667D2589769F3A4D3 + C4EB2D25344EE2AED3751C812FA28FCC350C004653F4BBF1A4D9D2717C80BC38 + 7EBB4D9E2BD74E9967209954D29D26CC0A109835E36706180C0036081C803000 + D8D23C0387F32D6BB48F23D89CC2A34907AD71939E237100B71B3E2553FB1100 + E3020076E01B6AFB4E2C8AA520B8F590F1D21AF0ABA2DFC635B8A0B62AE06F4B + D2410F1E276D078C958EFD2698ABB775FF711A9B286D2AA6498B6153A579C574 + 6931443578AA1E33D9EE83B41B30493A944F94F60E80FB525E6BA4B4EE374A5A + 978F929606810BFA3900983AC018015C7928EC2A40E021211D34201723418F0A + 331398A9201997D93DA8007B5B5CC77C00BC8C5BEAD821705E0E8581BAC8C16F + 0A8051067F55CD90C6B8EF86EB37387F1D08D32F0D80490B9D0260E4C0D7FB3E + 9F025E570A7853109C97CD75D4FE6500B0A903F73C7B4B595B55BB5E52461AE8 + 0E05589BC2DD7CACD41A87BFC0DD169DFA48AB2EFDACCDBB869927057419E7C0 + 797C150038AFFCBDE7CBDD9B2E250C504D7EA6AE69C17DF4AE61DC2289DBFDF6 + DC3D78BF0F9F1FA7311F2731CBFEA9B22C9F8C75EE6F479DAE5A7926937299A2 + D4D019702D5EFBD752937C2CFB79226F6B002BA3346E70B3D45C5E0E3D5DA562 + AE52C796EA5F8DD2F35E49A58E2FA5747D7A7C1AFBBB01C076F2CB8C3D7659E5 + FF677E7B3500C00D7E81BF527F6DF89B53D1870600585FAB598BCED2BC753783 + C0E67A2DF1A142AC141CF6F31594FF20FBEBEA1AFB19F5FFA1A9FE6C08B8DDA6 + BBB4D08B25D03700EFAE56D7F71A80B01EC3D88E3540ACEB01C63AEF30DA5240 + 6BDBB8B8609510172CE460375E3C7F898A2EA83E6EA517F8CBA869EBEBACBDC6 + A1AFCAD334BBB2350E7EA9110C5C6E04003703DE6A9BAD8FEB1C0CA36B98F7F4 + 2725C5A648374FEDFA58FF1ADA58CFB7A9397C7503A62AD4F71DA07D555B4070 + B95CD34137831D066AAC7F90F66DDC9196395DABFDA04121DE29B4D70086DD3D + DC31C0DF326A8EA8D2BEBB7A5DF9DABEA9F21037550A7151BE9FCAE3D95396C9 + D836DF89D8B0A3521BFA620500EC2EDFAC662F70D7C0305F0CB48DB0370F7ECD + C51BE57177F892FEB995B62DF48B0BC2ED4B0AA476A472D62F366DFA037CF5CB + 13E017B80BE4D57EAB413A1E5C0C7F1DFAA631DCC1C4DA007F73C2C59BAA9D7E + B16A3B789AB41FAA5F022BF48BE5B050BF37A4772EF449EDDC69A47E5125C573 + 14E32EA3F5CBEC28FDA28A2B1717EFC8EBA55D1480D7DDBA40DCE0D80DADB97D + 75AEE3B879058D9F1F2030C077827EA18E0E5E77F77A2CC4179ACB17D0DB6D1A + 1077A97E995EA45FB075EDD4C5D275EA126D97682CF4AFA53E2FE99DB5050223 + ABD91BEBF6A6EAAAB16E80DD59AB8A74ADCAD339F720D573EC3367699FE7AFCF + 52385B8A67C68B3699C31759AAE7259B85BABCC1C9BB3928426077F7A250C777 + 9BF459BA557A2FDB2A7D9653A37797B63BA4C7C22D32624B9DACB9E571597DF3 + E3326ADB218BF55DB15B06AEDD2FFD57ED95FE6BF699FAACD825BD97EF94F2D5 + 2156B1F9A08CD8562F433654CBC0753877AB65E8A61A19B2E5A00CDA58250381 + BE1B80B9A46C3E2803D61F10EAF78ED87150866FAB31F88B0398D6EBFB0EDA5A + 2983B6A8B6EA785BB50CD95E2315ACDF5527C376D64AC5CE833274FB416DB5BF + AB4686EDD1FEEE6A5595F6A97BC60D2E6E7ED5593B42DB5423ABEB6554759D8C + AEA9B7FA6B636B0F6B1B6223AB0FCA28A06F7DBD8C3B744846D7D6CA983A40EE + 11197FE8B08CADABD5588DCE0372EB557506781DE04E3A5E679A7CA2DE5AE268 + C2B1833626D5F3F823D516F375769C8AFE945387544774EEB0C68E6AEC984C23 + 15F4E9131A57C5D4CFC0DF5967B9C918DCBEA47A9E7623AE5EDCBD3718FC9D75 + CB2D72FDADB7DACD4B87BDC0DD59B863B8E949ECC69B8BE658CB8DD1D9B8696E + 0F75F748FD3C1DE7EF5D77CACC7BEE92E977DE61627C3DA91D0DE2DE2BB392F4 + CE00E190BA3942606ED8C6FE5C6EF23E0884BDCF1C44D41644F4ADEE20EDFD7A + 0C29A071073DF880E0062EB87D69430C572F5076D9938FCBE2C71E35F8BBE8D1 + 008319037F973DF1B82CD2FE028DCFD738ADF7E7D37FECE170D3FB09ED3F4E9F + F4982185F3E298D6D9002F75149F7DC2E4E315CF3D9969B989B49DDC600F2E5F + 73EEBEF08CAC8A0EDED52F72A3FE5959F6E2D3B214D7976AB19E0F57B03B7B01 + BCEE00A68FABD7DDBE8C5907F475084C7FF787EF4AE5671FCBF6F7DE908D6FBC + 2CD4BC4438E04881BAF1AD5765BDC677BDF18CDCFEE69DF2DAEB75F2F64BBBE5 + 8F2F2C95FFF8C2BC46C5FCBBAFEC91575EAB969B5EBD4976FCEE09D9F4EE2BB2 + F1ED9765FD9B2FCAF60F7F273B3E0200BFA6F157659B8E81B2C099ADD4E6FCE8 + 0D21FDB28FB7BCFFBACD0365691170D6EBF4A671C63B7EFFA6ECFAF44D03B596 + 8AF6E2FB26C67B3EC7C10B200E699D1DFABAF69F0B2E60E6382F500AA7AFA7BA + 056AB9CB31A4BCFD400E926E37425FDA86101867709873E86BC0D7D605685CFF + ED7939F4DD85E0EE559196F9C8F717E5D0B717048727E00EB7262DEE4E005E31 + CC0D2ED254D48A65CE5332676EDF04085B8D605D67298623F87500692053C5F8 + F0F7A481FE4C8EE3FAFDF39772ECA70B06834FFDC32539F2A3FEAC7F08757B81 + A1805B87A7D6070CEBDC293DE6063DF6A67FFCEAFF4FDE5F36CB756C6DBBA025 + D9225B6090655BB698999999999999596B2D2D3133B364B16C59B665B62C336C + C3DEFB79DEF79C3E101D71E274447FE8FED0BF61F47D8D9C596BAE5249F6A6E7 + DDA7FBC31D993932E7AC5A5255CD9C75D53D86F44580B3DF07E8EAF0F7BB90A2 + 19209BFFCB437704731CB57971E5460730EB3916F84B9A6720EEE63F3DB4821F + 1F38FC4DABE08707B6E54F5F5A811E2F3A80A33B98E3698B9CC49FF9E3E20A06 + 4AFB63A9F554D1AE908ADA61754AF139F9DAE438C473674C1BFF4D01BA887F2B + 62E97F6BFA11BC47F01BD7C731F3A4E5E6FF2FFE1FF25AA08D31FE8FE36B2400 + 61C6C0DF500B3A027EFE3FF9C1002017A81B012E7017201C41EFF26F744E8DD3 + C077999E4384C669E76F6CA30338C61893123ABEFE79AFC536FD1EE47D17DE7B + 21153B1018C88B033F82E1087F81C53145741A02C7F9D8CFE5080E9905826226 + 81E8148ECEE2A2D84DAD21CD3C590882C6DEBCE202E84EBC75B508045FD3E766 + 0AFEBAD4C7D10BDC8D69A61D104BB4283A875917812F9097F1E8AB17BD8D8A10 + 38AE09709839D249B3F6928DBA7CC5464A232E5DB6A1E72FD86040EFF97336F8 + EC19EB7FF2A46BD0191D7FFEBC6BC8B97336F0F4691B70EA940D387DCAE7FB1C + 3B6EFD4E9056FA94AE5F9A030AE316D6753366A9E87B4CD7BDE307FD074C5CC3 + B8FEF53EB4CF61B05F0B8F07583CE0C4211BC8B147C275957AC25C3FF99156CF + FDFB32D7D8DE0780BF64D738E8D7643273F0832BDA70AD3EA86BFD7EEBB17BAF + F61001FE46873035F7BDBC031943766A3F20B117F1D4CFA99638FDAEDAAFF8BE + 457B965EDB75CCB61DDE07FC924A1A604C198A8E891B9838E30885A3DCF52BF1 + 8338F6433DB4EFE9C90FDA0AB407DA44FAE822B5DF986FEDD6E759DB759B5C11 + 0447381CA1308E605244B759450DE175D682FAC0CBD658DB656BDD018CDA2C5F + EB4E600410260614664CEB4A9CC1C0E00C107618ACFD6B4AFCD0B105ED5CED5F + 933D307B61622DE68578F3394B326A367BB1359DB5C81A4DD75E7ADABC54061D + EDED67148FA146FCD872D26C6B387196359EACF194B93EAEAF712ED51D3FC3C1 + 6FAD31935DD1351CA130358169EB8E9DE4103882E0D8D6551BD33F3B0406088F + E207A813DD395C7D187313AD2EC70C4FDCC3DE86F4D0358704373020180181A3 + 1C04F71FA67BA461567DC008ABDE7FB803E0B403B846127FADCF6087C2AFF70E + 8AE0D7417022FAA4908EA018D107005749E0AE3B821379ED6029F6994F3B8201 + BDB400E09781C209EC8D6EE18CFBB77D8F4CEB7D09C72FF2DAC2126E62E0AF03 + E036DD3230B8522BDD9BB7D4FDBA145DC3D121EC5038913B86A58C4BD8C75D35 + D67D7D73DDD72729A181BF3115342EE10A5E53B85DD226E36CD8DB283880D371 + E06F84BE691173D52D72023F5B270060E02FE30A355B58A51A2DAD72CD365691 + 725675C3F71DCFE012AEDB5EE7EEE8DF6B50F6EAB95AADAD82D656A88E5A48CD + ECD9EA4DED39E9D91ACDAC9C84139874D01965C15F9F4F20708C6503E1387E9A + 926099EF851A7A89305A20AE835CDCBC55EB698DC6280574697D3DEB5893C438 + A6246037E5008E0038B346FD620058AD3B8653F017D8FB388305DFB1017E9FAA + 5C2301C0D18C5143F3C5217009E06D12CF06BE69110300C77E761B9581BCC5C0 + 6F71409C76003F49A52A50FFB7A806707400E772FB6643DFECF9E8ECA58FFB17 + F81B8F8973E935A814CEE31C00387B9CFEAE395B45DF453F795DD4536501B7E1 + FB73DADF535C57ECFB75FF5E3FD54F8F1F17CB15CF5A93F97E3F2538C03FAECA + 29E59AFF23CA0D767F4FF171D3AC26C3941CB646E82A651D976EFF152AF974D6 + B9E373F1E7938395A5F547385C0EE0F9BB66CEF431B19F758EBF49E973E452AE + 637229D7B1D9629DC35F4CA539E651F6797372D594FEE900D84F9A23F6A4F863 + 95EB0FFAE7EA5FEB00D6B99337E3EFA9D81BE59FA8F801E1E3E443800F0B0030 + F0977E662E257EBDF17F05001C1DBB254BF081A3BFA77C152B594E2205B4E2A5 + E83FF6839FBE8EF5B6C88D8CD2178E8C744CB8A8A4A0AFE4E92A12F05B74C1FC + E300F871EB632C33FF18376F541A00C7B1FF022DA50C044E01DD0875B305E47D + BA82361A00DCAC7804C30E827342DFB412009C12C017A56BF716A569AE6D65AA + 34B4F2AF6883ABB6D4F375B5561BC0087C2BE0FC0DE0B7043107BDDA38B2AE72 + 9D3066EE796D3213008C7B381B00A3675C6C468B43DEC7291BFA46776F1A0067 + A77206E8C6787AAE98BB37D32F82BF3186BC66CB2BDA94576B6CE55E07EEE2E4 + 2DAE22F81BDCBED4EC8D0098BEA77A7E553707090046DE8FE32C081CFB387C51 + D9EA8A11A7ADA135BAF901F8529B975ABEB87D8BC15FA47119CD799F78027E23 + D88D1038C2DF38970D7C1DFA26299D435AE74E6A8BE02F02FC66EAF9A246BAF9 + 6CAA1B55D23BA720700600A3C4E1EBE99D01C1B87AB3E02F631CBF959218EE5D + A06EE5569AA77E2F70B76D3FDDE8D216A5727EA9DD006F5F6C2B252DB12AB877 + 3B0CD28D35699B0705E0CBBC43E141BAD90EB1973AE9861E376FE7610E785F4A + B97BA39BF7D5C4D18B00BD2F017CBB8F48497340DF1E5A4F3AE79EA33CCD7348 + EF4C5FB13E6332299D695FA5C66FDFB1EEEE45387F3DDD337DA99AFAE835C551 + B13E1018F83B60A2BDA63600E0A9F6C690C9F64ABF895667E41CEB306F83B59F + BBDEEA8E9E6BD5064ED1FC0C87BA753406F6D61A89C3778EC35FFAB46F0C0D90 + 18F0CBBABA6370F42EB40613165ABDF10B1C02D71D877377914361803090B7D1 + 9445D66092E6C7E9FCE3665BDDF173B466BE03DFFA09F4AD37597D5A8D1B6A7D + E3194BACC98CA5D668E6526B307D89354433165923172038C05F5CBF7CE1E5E9 + 9ED1BCA5D678EE626BA8386DB3054BAD25AE5FA9F98225D67CE152578B45CBAD + F5B215D67A7950AB65CB1CFE0283DBAE5861ED56AE54BB5C718D572EB7CEEB56 + 5BC7B52B1CE4BAAB3781B8518C89E3EAEDBC7EB96200E005AEF6EA775CBFCC3A + 6D586E9D34D771DD326BB76EA9A476ED526B4B1AE8F5ABADF3A675D671E35A6B + BF6E95B55EB5C2DAAC5AE90018A76F974D79D619089C405F07BE7C89B9699375 + 528C315F600275DDD5B229DFBF248D69123BE76DCE386170D0B83386F92D5BBD + BE1ED0D6BF38A5DEEF8E1D0E6371CE46E81BC7BD249CBAC47B104BDC3C0E7E77 + 268098384E1FAD0B40373884FA1C4C5234AB1FDC43010A93F239BA7449DB5C04 + 7CF91239B87C433A675CBC45D0373D3FE0F0211B70E470F8F29939B5FD8F1EB1 + 81C78ED9806347AD9FE6FA1C0404037F0F584CDD0C000ECEA8902A73E049DCBD + 01FE0EC5C52B45003CEC2C8097FABDC76DF099008123BC8D757B1DFCBA73F78C + 8DB878C6865F000A0447D890F327DDFD35FCC229AF19391A37D9A57336F24202 + 7D2F9ECDC4628A67E2E3811300028D894FBD73C366BFFB964D04665CBF64636F + 5CB2D1D748650A90006E289E80E009B742DA5480707A8CE87B3DCC1BA43EBD64 + 63AE5FB471372FDAF85B9732EDE4B7AFBA48BF3A95DABAD2248D03F0BDE6E016 + 804B2C5BAC8960883E31FA0022202D7D07BFEF007EAFDBE477DEB429F7AEDA8C + FB37A50081D174CD017F677D80E3F74EC6DD0B8C4500A990EE39A47C064CE15E + 9CF749005A005F805684659C1308B6FEE787B6E4E1073A5748F9BCE801B5798B + 6AAED22E7C00E8E5B8A2D82277F2DECFC0DF655F7DEC5AF1350EDACF6CE5379F + 1A003800E1E0F08CEE4E20DF723DF6D287D1E119D23423601F6380DF52CDAD02 + 00933AF82BEAC77E602BBFFEC8D67CF799F4A9C6F7B526A41F060AE25C75876B + 023B81B89E96F95B1E1B001CA0EF2ADCBD3F68DD9F3EB7556A391E40C9DA0D7F + 7A601B7E9648EBFCCB17B651FD00443F3600F0261D53F0EB43DB4CEA6587AF00 + 543DD6B71F7B0AE68D3A2E035613A8EA6EE1449B3406DA0281A3C397F390027A + 138017872F31B5C4B6E8FFA740E75CAF730398F3746EDCC30E8FD53A04663D71 + 3D27C4E3ACC7B1ECCFA1E879E0FA054C03896999F794D1FC9B49F1EF2416E3EE + 0AD6E3027991BB82D546E08B220C8EA9A5998FD0370D82599B86BDF4E338A6DE + A6CFFF333F22206D34AF135CDEE10701A1D6308FC5FF1963AF239CC0DD451A2F + 632E81BE1106A71DBFBC07E8138B7037A47A0E3F8E604D04C5511CE38E62BDEE + A99D1D7F6411D34033E6FD4CCB0F258831CFFB3B3A7D8B406F80BBA470279686 + BDCCC5F5310ED0CD86BB711CA1708CD3A290818096CF162032B5C463FAE9309E + 806B18F87BFB9AC3DB087D3DC6E7216EDF6BFCF0E5B23E13F97C0506074730E3 + D1572FDAB04B671DEA721CC747B88BDB1727708CB326C2DE381F15C0F045C559 + 7346D7065D27CE9DB361172ED8F08B97D55ED218D07BC161EF0060EEA95336E8 + CC59D7E0B341006120302D6BFA1E3FE11AA8FEB0B33AFECC7987C18874D143CE + 9CF41F2151A2C07FB4A46B167584A9274CEA68AE839ECD82EBA1E4A9A575BE7E + BA9E0ED0B592EB679F43BA26EB3ACB3516F90FAC70FC726D8ED7605D9F7BE93A + DE576BFB1C38E00018F5243DF4BE7DD6675FE20E569FD4D0DD77EEB05EEC1BC8 + 22A23D4974F3525A0287701F1D874B98B9AE85616FD373DB7607C0EE0E662FC3 + 8FCFA4CEEA7748402FFDAEDBB6793DE1288FB32F4AF64BA8DBE6A2B4D21D36E2 + 0E268534A018B7B08E01046FC8CF0060D24477DAA8C7C025AC3169A1A3A243D8 + FBD40756DB7E95FA00E0156B1D02B75EB6C6451F080C0C66EC203801BFAD97AC + 763910D638E30A5EB02C2881C064BB21F34D6300B0F6C1CD3546C49B3306FEA6 + 1420B0F6DA405EB5944F692235D2BEBA011038893701144F996B0D27CDB12653 + E7B92200A675383C99FD3C2EE199BA0F98A1FDFD3417E99F1B792A69AD99C45C + E20C1E3359FD29566F9CDAB193752F3151F712935CF5106307C1E3752F3241F7 + 1E6A4786B6C6085CC3CC4F74E76F0D20B1141CC3A3AC0EC078D858AB39644C06 + 00D346300C0426E6E9A2078EB4EAB880530EE09A8A0181AB01849398BB867B07 + 57B03B8201C0B881B586E35E55FB0AF58401C96A7106037511A03780DFA274D2 + 1100D35601F6D2E20CE618B5EE18A69F82C29E3E5A2DC01727306D7407BF44FA + 6840703B1CC189EBB74D37DD07875AC2B49E0E5A3100B02B81BF316534D09771 + 04C21102C75AC1159B13D3F12DBA3AFC7DAE6927ABD0A49301801D022769A381 + BE80E0D87FAE7152CF9736AA49FBA2386A580487511AFC1603C3090046B87F51 + F9DA2DAD52EDD656B9562BAB50B3A53D2B95AFD35A73ADECE99ACDACACFA7C9F + 51A616D9D19ADBB36A2BD66C65156BA016F65C8D6656BE3AF0B7A995AF89748C + FAEEFEADDE240582E9073D0900A72130F03700E0007D8B09C89BC05C14E3110C + 33EF6BB2806E04BD25ABD47139D44DC1DEB8C6D74911FC66E67011E338764730 + DF9705B72F6DEC0705381CE16F11182E02BFD920D86309C005EAC694D0D9D037 + 2AC6D3F3F17854D47F3DD5E20EAEE670D7016F25AD4BB981B355AA020E5D0070 + 00B5280D80A332C036E9A7E7A2E29AA8087A4B5548C75E76957CAEAA9570B8FB + B295D23A443FA808FC322E518EEF9AF9EE3A8871B6D26B8B14BE932E1231CEF1 + 78D0FB8794F98E3D6B9CAD3FBA3E15C7D8E54C404A7FD78F323CE16F56E5C728 + D7DA27A988B13C4EF1DC8F8B67E6329055621CDB7F07A50070646FD97A94A33D + 468F859E8F5136284D8F63EC6F55F639B295EB982C3DF237A78FCB3ED7BF2D00 + 2E76528D4B947DFCFAF4DA272AD71FF4CFD5DFF4824B29FD822D768E62FF218C + 93177C4AFF1500F8910F85543CC04E7DE824639F4BC1DFF4F1E963A3E25C50F6 + 07D97FADF8602F51BAB295C2E90BC8E502C4077E29FECE173CDD73FCC07FE4C3 + FF198D9FD1858B5AC23A96F3C5B978EE9C8AD017001CC1EFDF00808B5F388B94 + 6B6D31E580BF69D80BDC4DC7A2EB370D7EA3D240F74902F4A6616F84BF69954C + A73EC92952A51407BF69A501F053A482AE5CDBAAD6EB680DDAEB66A771372BFD + 52031D0BBCD546F1F9BA5AA3B5388113F01B1DBFA55ED066540214A3A75FA8EB + C0D7DDBF15F418151F7500234F139D03F6A2E8F48DE3087D8B00705400C169D0 + 1B61AF035C8E610D7D5718FB79AA44E8AB8DF82B8D5CF4D133AFB0318F8A0E5F + 6DF2BDB66F3206FE5603FC4AAFEA06E1D566EA8734CF9EEAF9F51656B65A73DD + 2034D54D01699DB5E6F504F226A0D7E7B4867ABF0E8081BC29001C523C27F0B7 + A6CE57BB95DFD894D50D5059E06F027BA3CA28EEBF7CADD35EEAA01BA1007BB3 + 1DBD69081CE7D26BD2D037D4ED45F483C3373A7E2B36D20D22203801BCAEA645 + A017085CA989D648CC39186E1E1CC0AE04FE7A7AE7140476F08B5AF672D05BC5 + 216E91B31791B2B94A4760EE407BA17D3FDD0893C29954CE03AD8A5EC3C86BF4 + 52BB977E87C15EBF373A7ADDD5EBB122976F95CE412F771EA29B6F496DD56E21 + 8D33ADA76EEE35DADEE83DD6A1AEC701BDBD46D9CB3D4766C418472FC2E94BAA + E7D748EBEC757EC7DBEBEEE20D8E5E5CBFB880DDF12B45209CA9F93B70A2835C + A06ED0A44493EDB54113AD1AC017483C648AC72300A62E6FCDE1D3D59F66D587 + 50AB775680BAC3A63BFCA5AD3122C05E0070EDD173ACCE9879493BD76A8ED47A + C5890180EB8D9BEFB0B7C1C445EEF645405F40703DC5EAA8ADCD9A890BACE1D4 + C5EEE4AD3B719E0BC8CB38E3F29DA6734C091038CC2F70C72FF0B7E1ACA5D660 + 865AEF2F516CB17F59D5685670FB36E1CB2CBEF84273975893F94BADF102699E + E20B9659F385CBACE592E5D20A77FF027FE9B75DBEC6DAAF9256AFB176AB5659 + EB15CBADD992C5D67C093077857558BBC63AAE5F6BEDD7ACB4F66B5759A7F58C + 570660BB7AB1ABC3BA65D671DD72AD0931FAFDB617D8C05D9B1DF2B659B9D05D + BE5D37E9F84D3ACF8665D6B560B5ABC3A615D691789ECE9BB7565AA3D86A6BB7 + 61A5B55DBBD2DAAC5E651DD6AFB3CE1B375A67E06F5E9E7529A076DD66EBBC39 + DFBA6C0E8EDFCEF9387BB5265F31CD778AAEDEAD455F8CF2456797CD8501F42A + EE5F7CF2056AE2AC01FCF2C56B77BE7845BBA55D3B13181B446DDE509F77AFE6 + 8303B8FB9E5077B7DF8103D68F2F7CF9223701C480E2706C70F2F63EB8CFFA1C + DE6F7D0E05084CBF27299EF7EB1C87F9E2387C81CCF901BD5EDF57E774A8EB0E + A2007E711321FA0E8269B5C6DDBFC78FD980A347AC2FEB01C2C78FDA40C5F882 + 9A3538A0FA3BE80D4EDF7E7C992DF1657848C37CC2869CE58B719D4B22853390 + 37BA7F87036F49D9497ACFF3275DC32F9CB411174FAB259608772F693D13672E + F075C46500018E2F20C0191BA516080CD01DF3E645D7D8AB97D45EB0D18A8D53 + 7FF4151D2B4DBCF9A64DC2810680D0FC841B5734BE6663AFE93840C5CDCB2EAF + C77B9B14A73874AFD8446AF3E27ABB71D946E939E060F394A9B7DE0CD0F87A38 + 1EA8C11CAEDE087F27E2DE7DE7BA4DC7AD7BEF864360C6F471F902736923CCA5 + 8DEE5F0414661CDDBE6960CCDA18A73FF55DAD97A8D11BE0EF4D9BF941E8C7DA + BD110AC7C78B1018E014DD8911FC02B348F34C1A5BE016026445A0C51810B6E2 + DB8F1D78C594B90033142118C09875B451C499A7BFE44BADFF1208FCA12DFF1A + E847DDD9CFBCC505BCECCB00FB22E00BC02FC04340AF3B7813D7AFBB3A15A3EF + 50F7E107B65AE75CA973D3AEFDE693D0EADC88FEAAAF74FE2FEFDB4AAD5FFDCD + 870E3A819B0810CB78ED8F00491D839B5771C02F0206AFD11C40D101F08F9F7A + 9A67D6AFFBD3A7B6E167C06970F8927239EF67CEA9F3ABDDACF1669D3F9FC7FA + 518FF3FD27465D60202BAEDF35DF7EE88F1F9E07CF496B39876240634030C7FA + B9152B20FEDDC7B65ECF255FB12DBF3CF418FDBC64FD463D068F131DC000E028 + 7F1E5ACFF305FE02720304C6C94BCAEAE0F88DFF36FCBB38E0E531937508A773 + 5A0E91F5DC3DA575027F23D84DC35DE6F27EFDD2F27FFBCA5360B32EC2DA0884 + F93F05EEA60170AC0B1C526D039979DD04F01B8031C784D705621C01708CC5D7 + 25AFD38C34A66EF0A2078AF32303BD4680C511F02E4AE4E3CFDFB5F07E09EF1B + C46B3CF633739FD3BFE7EF39DE7FBCD778DFF13EE43D199DF1CC138B103800DF + 90FE9D1F67444730A016114745E036C402080E1038C0DB74EAF7008D63AAE898 + 2E3AAE0126037BA7DEBBADCF1CA07100C4CC038F7102875AE4D76D0C90D73FFB + F4B9C98F6314633DF57D519CCF3883B586F188ABE76DB8A7DFE7333E005F34FA + EA4517B0983602E0087FD3303802606A0A0F397FDA069D3DE52017083CF2F29B + 2E40F09073171C06470D3D8F2E7A9C96317D60F0C0D3676C10C0571A72EEA2AE + 4B5774EDBA6883CF700E5CC5171D1E0F3DA7EBD1F9008649253DEAF225AD3D67 + 31E5F4E053276DD089007E079F3C2D9DD175F3A4FF506A00A038759DED23F502 + F6BA00C3075D3DF7EFB76E7BF65837ED05BA530378FF411B78F8A8F60A87AD8F + FA7D5D5AAF3952435337983D449FBDFBB41FD1DE6027AEE0F023B3982E9A7137 + F635EC5D70FB6EDD613DB7EF0C29A2939213B49EC564EB562F5911CB56B8EB57 + FBA0E804A6C54DCCDE887D536787C2E187729DF349178DF39794D1EC9FB67AAC + C3C600813B481D379059050771BEB55BB7C9DAACDDA8BD5B913B38AD8C33985A + C06A3BAE516C95C6CBD73A10EEB856FBB875799A0F0E61407080BE2B43BDE025 + 2B332EE1660B97FB3E17354DA9097BDE79EA0383D90B4BC0E1960B57042D586E + 2D1C0807B7B0BB8167B2CF0E2554BC5CCA6CEDC5A7CD75916587F4D14D28AD32 + 6381350614538665F21C6B3079B6BB839B4C9BEFF2186078122EE0695677C274 + 6BA673B6D0F99A4CC5353C43F70853A55023B881C36169E274AB4F0DE1B10106 + A3FA6326496A15AF337AB2BB866B93427AE444ABE14EE189D224DDC3E0081E63 + D58707B7302E601CC1B5864943C724358147BA00C0806000701A080381DFC009 + 0CE8555B03A770BFA119A05B0DF80B20A6DF6B707003037A192B8E6BF855B555 + 7B0FB2AA7D92B8D6E10C7E3981BCAFF4189038850738F805EE12F7390071027E + 89E308261DB43B831318EC403835CE80DFA48F1B38A688AEDC4EF7D6C05FEA09 + 77000EF7F4B4D280E0E80476204CEDE0D60100BF98F423F88DC2FD0BFC7DB649 + 7B07C1805F40301038AA7273B5B881351FE02FE9A4434AE908801DF6728E26ED + 745F9F7206374DC45C0A084747304ABB831D04274E6077FFD66B65CFD56E6515 + EBB4B68AB580BFCDAD7CED16EE16A64EF033B59BD933B59A5B69DCC25239F549 + 17FD5C2D1D072CAED1DC32299F6B0495A9A5714DB5F4532A5DBD71460E8213E8 + 9B5604C0D1F95BEAD5FA6A1B68DCD04A23D23E27803702DA088123F875689B88 + 7E31A01BFB719DC6C4A2D2A037AE8DF1187B4409048E8A4EDF007B433F3D0E29 + A1AB675CBF19F0FB6280C14F02C011EAD2CF561AFC46C558894AAF6554B27235 + 496D02814B550A2A49BA67A94405CD25FD9240DE1CB0377B9C4B4F82C445A0B7 + 8AE255332AF95C8821FA2501C198A124C02CA2FFB4E2A854321FA16D545C9B56 + FA1CE958541AFE3E5526C0DEBF19FCA2CCF7EB493F1D4BEB71C764AF4FC792EC + 9F69A5BFEB47ACC9B5EEF162AD8E295DA49C31588BAB68FE71CAC55BD0EFADC9 + 9EFF234C297D4C3CEE5FAD0CEF4AB1B4BF49C5F85AA25CEB50F65CF671B9945E + FF4794EB1C69E53AE631CAC922D3E78127FE4D0098580EFE5A4C25FE01000CA4 + 4DF7A31897286B4F3D15F4C8317F58D97FD0BF8F1E0B80B39586BFFE1F17FAF1 + 8D907943E478B3FC2B54ECCDFE985F86FCDE87427A3EF707E37FA1923403FE5C + FCC3BBE839C50FF8DCD205EA69DCBFBAA0513B98BEE21110737CBC90641E2BC6 + 52EEDF4CDAE7B472C1DB947E0FFE668F33F1A40E70B600BA11F6A6635111FE66 + 1F930BF8662B0D7ED3FA7B007084BDB41108A308809F4AE2A52AD5B2E76BB4B6 + 379AF6B0176BB70BD03701C03881E9E3F47DE6A506F65C356DAC5F6BE6EE5F77 + 036B9E75A55FD406F4857A0E7FD1D38A3FED35825300D8EBFD926EE6F70170D4 + A300386C64B301306D1AFAFA3CEEDE2495B33B7AAB36B0523EAF5855A02F5057 + 1B7A5CBAAF68330F08561FB08B8337A670C6C55B4A73E869C6D1C15BAD996E06 + A8D1DBC2CA48A51DE806318EB13252D9377413535D372B52E9EABA61D171A8B4 + E265342E5BA3A5A77906F852DBA69C6E5CCAAA2DA39B18E06FB93A6DD40702EB + C608676F16002E5D2B3A7F81BC1D2520705807DC2D02BACC05089C86C269001C + C16FC546BA097405E80BEC8DF0B762A3EE56D11DBE09FC6DD2C32A037E9BE3F0 + ED95F449DF8C74D30AF44DF40270578A299E01BEEEFE952A2A468BC337387A83 + AB3700E00402B7D39C0360E0EE40DD040F90B42602DF7683ECF9B603748E811E + A38E2FE99A5174F456ED323CE3F48DE3101BAA9BF390D6F915806E8F911977EF + ABBD46798D5E8F018701C1BD473BF48D72F0DB77AC435C4FF19C1A57EB1FD23C + 57C7ADAB7155CDD1E2F425B57384C2AFF72715F44477F6027681C00E8287847E + F5A153BC766F91E3779A43E0D7700B6B4C0D5F6AF3027D01BEAEA1D288995603 + B82B5547C9B8D6A8D99EC219176F4CEF4C1B5DBE0E7A25DCBEC05EA06FFD498B + AD2E309834D1B880894D06F206D08BC397F4CEC05FA06FA319005D9CBD8BADC1 + 7449B17A8060CD37D4B8E1EC65D660D612AB8F0B78D6526B345BEB5D8BDCDD9B + F9F22B7EE145BB6099355DB8DC9A4B2D16ADB0168BD5A604FC6DB574A5B559B1 + C6DAAE58656D56AEB2B6AB57BBDBB6C5F265D67CD9526BBD7285B557ACE3BA35 + 0E80DBAD5A611DD4765C07185E66EDA58E1B565897BCD5AE8E1B575AFBF5CBAD + 93DA3EDBF3ADFFAECDD62D7F8DC62BACABE67B6C596F5D0BD658BB8DCBAC1300 + B870AD752E58659DF391FAF9EBAC4BC17A6FBB6E5E6FDD0A49E99CAF715000BF + 05D66D4BA1F5D8B6D5D57D2B5F5E6E7675A1DDC2979B05FEC56617402FA911FD + CBD2ED991488F4637A44C02FD0B7B7D7E0DBE38E1C77F33AC00D8EDF58631777 + 4F8F7D21E5235FF276D71A406D74E046309BA9BBBB2FB879FB9002925ABE87D4 + 3FB2CFFA1EDEEF69263DF5F2D1838AEB31A92708183EACB1CE17D24A1E74076F + 74F1C63E4EDE41A4B69406AA9F01BB6A01BD1E578B53A9DFD1C35E3771E0C9E3 + DE920273E0C91336F814757B432D5E773BA98DE998434AE65336F4DC09175017 + C03BF4DCC990DA3971F786B509F49546B0EE928EBD0CE02DFA721F1799035A40 + 82DA0884010101089CF5B143D8641DB038C6223CC6D10BC805EC128F6E5EE6DD + D50BF805F6BE75CDC1EF981B97BC0576101B7F1BB001980D4EB8B86E6C028A99 + 0FED6507C0A47C9EA03EB01700EC699F1527E62018B89B02BC1100D38F299D1D + EC266BE25C545C8F804580DD191FDCCC381271ECCEF9F8B6DAE0000E109754CE + 012C21005414D00918155A9C8C0160712E144156EC03B822D88D407709A04D5A + F4C5BB0EBBE8E3A60C29737153064725F57A59138E093197FA450ECF00F65070 + 007F64110203F022C47357278FA37E9403598EFBE2BEADFAEA635BF3CD27B6EE + BBCF6CFDF79F3BF045C418A3D5D4F1FDFA0357B174CB40D39F3E7337EFBA1F1F + D89A1F3FB7753F3FB082BF7E6385FFF19D6DF8E50B5B03F4D4DAE85E0500AFFB + 298060A0EDC61F83EB77CB5FBEB6C23F7F1520AE83DCCFAC50E7DD82EB963ABC + FE78A136302015004BDF1F3F3907C76ED27981B880E0C2E4581CBE9BB354A838 + 1098360381751C8FE3EB491D0D24D67CEC3B5C76001C20308F1B9F93FF2DC973 + 09CF8FBAC401FEC675710EF09B9EA3E671FCF789FF56EE949680BD1BFE146A1E + 6F0444A70030FF9740DC55FCFF7C1BEA070376790D00E4630B04F6FFF36F433D + 618E89F01845101CCF8780C2BC76828A528A074779504C254D1BFB2EDCEB498C + B4E40B3FBB9779CFA1F89E49FF7082F70C6D880371EFF8FB0DE0CBFB8FF72300 + 9818FDF87EE44719F4E3FB9471FCA106EFFBA8088BD39F0F518CA9295E049281 + BB45C037C6A77B1DF1009171F94EBE8B439831F1008223F4C5111CE3E3F5B919 + 81302D73C0E3F019F966266534FD98463A42E091FCB0479FC100606A0DE31CA6 + 9F162038F42F39ECC5E91B523FF303213EF781C697753D20853440F8B28D7EF3 + 6A80BF97AE68FD251B8C8BF7FC451BA1317210AC317078D495AB2EE6079F3B27 + 9D573CACA31D7EE1AA0D39AB63CE73CE6B9ABF647D4F9CB601A7CFDAA0B3E7AC + 3F7057E71F75856378ACB3361888ECC057EB8E9F525FB153E76DC0C953FE03AA + 702DD575F814358775BDD5986B2ED7E8DE07B8561FB1BE870EAB3DAC6B397018 + 18ACEBFB7E7EB8A539C57AEF3BE010B83F3058FDDE7B7102F32332ED0D12384C + 0CF5DAC3DE44FB0AA98783615CC27BACFB8EDDAE1E51BB7639280600037F6951 + 97A49F71056F2EF4D4D0B4C4D82FB92B381165303A176A6D3EEBA402807100C2 + 1D3791365AEB361668CF97E73098D4D0C05F07C0D4004E52423BF855BF83E6BD + 86F0AA0DDA4B6ED01E7293755A9FEF2DEE60A0308018F978C53A87C240E01640 + 60B4747580C240E0C5ABBC7EB0836129D613061647E7B0BB84E7058730AEE198 + 42DA5DC1DA2B7B1D61F6CCFC70926C39DA533307200602037FEB4D9B6B8DA92F + CC5AAD73D770E210F6B4D133175A5320B244FA681CC3F5A7CEB5BA93A65B9D49 + 33BCE6700B1DDF700A8EE1E9D2346BE8358481C1D3DD15DC70F20C7703D71E33 + D1456AE80082D58E0B29A4A9230C34AE3D2601C1A348078D2378AC8BD4D05E43 + 1847F0D0D10106ABADADD63554E321A3EC8D81C375FF540480A9195CC3E345B5 + 823D4D74BFA1BA3F1BE22D6038D60CA63E708D7E3A5EFD08805FD3BA57FA0C2E + 02C00914061ABFDA4BB11E03744F38D0FBB42F931EBA5BA8010C0C7E155770E2 + 008E3583DDFD9BC8817007DD1FB7EFE12E604F21AD36ED160618FB9A745A68C0 + 6F04C5EE10D6B86D0FABA2F665C6594098D65344E31E6E09DC25E533C057F7FC + 8902000614E318D6B1AD68BB38EC2D720003758BDCC011FC3AFCD55C70077778 + A4CFFC738D490D1DD24347F01B5DC1CF356C571C04272EE10AF5A43A80E096EE + 0606FC22DCC1A56B37B7A76B36B5D2B59A393476385CAB853DCB5AB5E5F8317D + 8D2656566BCA684DE9DACDAC8C54B656D3104BE06F1A00A7FB65DD119CB46F00 + 811B250AF0370D8011FD52AF112B82B819909B0576A380B4110023DCC0B9D248 + 47D81B953E261DCF86C43E4E397FB3816FF638170006FE66A47884BC69F81B61 + 6E9C4B83DFA86CF01BDAD7A56A06FC45A59E7F3D48B1520E815FCBA84485575D + E971C904DE3E49BF07848BA02F90F7F10038C25F07C0C49EC3B5CB77C97C1F1D + C02ED0F719C59FC1150C308ED0F631E0375BE975C50170729E320970E5FBF36C + 001CBF3B4FF4BB70D8BF674F5ABEE34F1DFBC8BA748C7E5C9F1D4B29FD9D7ED4 + 93E61E15EBB43E057A73AA8CCE9B513826D7B1C5E259BC25EA49F3E9E35DC47F + C7F99B7D4CAE35FF0AFD4D003817E4CC56AEE3507A3EBDFE8FC41F37F7F72A9E + F331CAC912B38FCF0580E3DA5CC7B852EC35A74AFC135340036D633FDB01FC08 + DCFD23CAF107FE9B28FD1F56EC3F2D5B69009CD27F1900FE3BCEFD7B1F0AE9F9 + DC1F8EFF95D2734C3DCF3FFA9C1CF2E2FC4D290260C49A7871491F172E2ABAE0 + 456583DA2C10EC4EE1EC357FAFCA8414D0280D737309C0FB7B02F03E2EFD7354 + 2EF81BF54F03C0319E08073010D76BFB52039894CFB87A632C19BFDAB08BF519 + 33DFDAF79FE8EE5F8F037A9FAFEBAE615CC0A47E06FA7AAAE717EA14531AF452 + EF371BF64645D85B047CB57965FC62F1588C038033E0375104BFFE4B4CEFD306 + C85BE6D5007673C9A1AFE46EDE246533D03782DFA8D23880DDD5AB9B9044C0DE + 675E6BE602FE122BF3466801BC80DEA8B2355AB9CA7B3DDF3656AE662B87BF08 + F05B1EA76F1DDCBE01F896954AD76AEDA2EF2EDEBA218EEBB75C5D802F3017A8 + DB496D27CDB3869B29DDA035EA62951A036FBBF8B83CE0D70170471F3FD7B093 + AB02C0B731C057377B5A8FD22EDF4AAE9E8A91E6B9A78B71E566BDECF966BDD5 + 06D17FC1EBF8AA6DA91B5BD23823EFF70D6ADD5F6D3FC5FB85B675547F07B82E + C6C0DD3603A4FE0E7A51A5B65A9300DF17DA037913D0DB218C2BFB3CA99EB5A6 + D3107B9994CDDD245234771F61AF48D1E14B6A66C6C1ED2B3197B87971F556E9 + 31C25ED271D1E1EB63A96AEF510E715F06FE0282256AFABAB35722B5338AE350 + F717103CDE5E955EE937D65E1F38C1AA0F9EEC6EDF57B58639FAD513F05B6DC8 + A4627A7DE864AB317C9A34DD2130A2EFE3A1531D06D71A35D36A8E9C99C467B8 + A3D75DBDC0DED173ACD698B956436DF5D1B3ED8D1180616A79CDB53AE302F08D + F097585D6AFC4E5C6875D4D69DB0C0EA48F5272DB27A891A4C5EECE30693975A + 8329B87B17BADCF5EBA99C9738EC05FAE2F06D347B599003E1A5D678CE726B32 + 778535961AA9DF70F6726F9BCC5DA6D83207BDB87B116E88E60B5738F06DC617 + 61EE9628FAD2ACD5B255D65A02FAB65A06F85DEDF0B795DA56CB575AEB158AAD + 59636DD7AEB136AB575BCB952BACF5AA95D676CD6A6BB7566B57335EAEF10AEB + B861A5754EA06F97BC35EAAFB14EEAA32E9BD759D782B5D689359B5659CFADEB + ADE796F5D6A3709DF5DCB6C1BA6BDC79F31AEB98BFCABA6E5967DDA4CE05ABAD + E3268E5F6B5D376FD0BA7CEBB363B3F5D9492D3A9C29F9D6293F4FED6687BFDD + B76DB59EDBB7590F897E57C5BA6DDD623D766CB79ED4E4550CD8EB5F7E26B017 + D04B8B00C211F8E2B4E97F902F5A430A465CBC7D13A01B52302B7648F3090046 + B87D00AE805A80EF40402B7056730E7F15F7B58770EF867A825E6BF77048BB9C + A9BBABD6EB102610B8B70360C50F85F303711DE4267017983BF0C4311B822BE9 + E409F549CB9CA4763E76C40624F0D701B0E2FD5D476DE0A91336F8CC291B7AF6 + B4B7EE6C3A8DCB9734CEF449E57CDC8691B219F8EB3AED50170175A35337C0E1 + 007F49C58C865F3C6D437102E300666D0AE68EBA7ADE46BC79CE5B4F239A82BB + D13516C7B4281ECBDA506F328CE903818923FA3EBE71C94105D083BA97805C77 + F52A4E3FA64B653EC29300488A6A6B469802401E7FFB8ACEF7A6835E523ECF78 + EF96CD7C5FF3EF5C7747309AAC78843401CE1401DFB4988F6D843C8C6923F801 + 0E915679CEC78FD61E8D629C864F1C03440238A56BF3CEFD84B501EE46076384 + BB883EE7C900DF87EF4BA4D1C5C9FB9E2DFD0AF7246BDFB145D252C59603DC92 + 3AB9CBBEBAAFD8FB1E5FECCE4B1CC3EFDB3280DC571F64A01AA999017D887E50 + 707E06D0F751104050E70300E39A05EC112385330EDF150F3FB4955F7C646BBF + D1F89B4F6C4DE202DEF03D8EDBCF6DD38FD4C0FDC2009E80DF586F37A4330EF0 + 12F0897377DD8F9FDBAAEF70F63EB0CD7FFDC6B6FEE7F756F0E7AF6D3D6054F2 + 7ABF3ACFC69F75DE5F48FFFC2080DE04DC0280B7EAB86DFFF1AD6DF9F35796F7 + E367EE00DEA6736CF9ED2B77E8027671E1F27811B84611430E8AB52E1FC8FBEB + 9701E2AA0FECDDF9D76F6DD77F7C675B1507FC6ED7B977FC45CF5563E02F10D8 + C130F38900C09C2B82E0008103F08D8F990D7FD36D7C7EE93551FE7C155BFDDD + 47B6FAFB8F0D27307017A76F81FE3DF27E0B8E5FC6EBB50EE80B240618B33E40 + DF0F6CA583DDA42EB3FE7FD692321B20FF3DC70082C3EB02B186717416A7E722 + FC8D001845301C5E6F00E6A8A231D07781D7A026BD78787DA2509B5AEF2DBDA7 + E64BF3F4FE8AEFBF087F517C1FC5F7226BE67E72D7D3A22F7A703F0374791FC6 + 74ECB19E368A69A17389637281E2F8D9118170E847776F91F397FEAC0F388EE7 + 40EAE9BB5AC3F19C8774ED1FE839DDF7316077F25D3EB700D677757C48113DED + DE1DCDF17918C07148171D3F4F43CDE034FC4D036087C3FC90E63A9FCF8A5D03 + F286CFF6E8001E7723491D7D8DCFF32B0E8101BEF4C75E7F53F3D7D40271A9FD + 7BD926DEBAA5D8AD0070351EF126F585AFDA88CB6F4A571CF60EBF14D2438F7E + F39A8DD7DA3157AF3B2C1E76E9A2AE453C8770CCD00BB88701C1D7B5F6969ED7 + 0D1B72FEB20D3A7BD1069EBD6003CF505B1887326EE2731A9FD1DC59EF0F517C + D0A9B336F0E4195D27710A9F77583C80EB2FD7D5D3FC18EA9C0D3FC77C700C0F + 387A5CD76C60F05187BF7D0F9381E3A4AEC9A7749D3EA9FDC111EBBEF780C360 + E02F3078C0115DABD5E20A060AC734D10E7F13301CC12F20380D8A336078E71E + 07C0DD77EDB29E090076D7AFF6459D49FDBC658B97B520D6BEA0C03A683FD5B1 + B0D0E3B451D12DCCFEA983D6B5CFE747755BA4ADD63EAFC0DAAEDFE8A9A2299D + D1297F8BB5C309BC29A489060447B9333891A788DEB0D9DAADCF0FEE609CC0EB + F21C1E0388A915DC66E53AED43D706D19788C578EC534BB8D532ED5B7D5FABFD + 6A0289D9EB02823D75F412ED5997AEB1D65A0304E6C791B440606070DA2DCC8F + 28FDC79449F61CD245B7E487935A0FF8AD4F9915CD7B69950418038A1BE1084E + A031001960CCB8E1CCF95E9AA5C1741CC4B3ADF14C6A0B6B3C8554D133DD05DC + 68AAFA93675A6DC06E9226BAEE386A064F70D51937D9EA4F98A218B58227A99D + 1CD2494F98AE76B262A18EB00360EA048F1867B17630C2158CA80B4C2DE1BA52 + 1DADAB4D8D6060AF543D81C4510E8171072780386884622374CF35D2DB1A8346 + 594D5792221AC0DB7BB0EEE5061707C07D43DA682030EE61807004C00E817101 + F71CE010B86AD7FE9E12DAD3416B1C53425755CCEB09771BA0FBD490F6390D80 + DD351CA1B1E6A35ED4F8F90EBAF74EDCC01104BFD0B1A7EE81831BB84AC75EF6 + 7247ADD79874D1E934D11920DC5A2DEE60C60061E4A9A103F8ADEC8038A488AE + D8BCA3556ADE29038103EC0D2EDF0083DB6B0E200C0C0E20380D83D3F0370A00 + 0CEC7DB6416B7BAE611B1F0386D3CE60E02FED73F5358F13B86E2B77FEC6D4D0 + E819A06EADE601FE6A0E1770195CC2B5481DDDC2CA02866B37B3F27558D3DCCA + 2622564E7340E0A80882639F967565595B437D4F0BDD382340B03B7F93362DDC + C00E7013909BAD087611C037AD6CF81B1521702E6503E00CF84555EA5A84BF19 + BD5494123A1B0E87BAC06F64007031F89B02BE69F89B86C0287B3EC2DE34F80D + EBE807E01B9CBFD5326300701AFE46E09BAD086CB3816E7AFC38A5D7C5F34445 + 00EC4EDF94320098310EDFC4991B150170480B5D0480D36B1E177B521C390066 + 9EEFCE131503B069F1DD7B7A3E97E277EDB480D9ACE38BAD7B9C526B32DFDF3F + 06F046D3D71F979E530AE03E5669009C6B3E51114F29529AB9FC9EFEDEE3FFD6 + F5FF0C39F34AB1B3C729374F4BC1CD5CB13FAAC79D337B3ED7DCDFA3F4E33D46 + 39FFE6F4B1FFF600F8714A806E89A79F751587BC4F126BB3FEC07F133DF1459A + 96FF67FD0F00C0319D338F17FB596B7EEF8DFFB8B9785C50AE0FC7FF42950E1F + AEEEFE4D7DB8C75FF344153BE63102FC3E5DF625AF1590B9B8A42E2299F1339A + 47B900B0542C3D742E65AF7F9CE3373BFE18009C8E1703BC29109C0D8573C1DE + 5CCA057EA3FE5E001CC12FE3188B601800FCCC0B751DF68654CF12CE5E8D71F9 + 967AA1BEC69AAB50D31AB41B684BF20ED9D8D9EB74BCD6686D9997EA393CF6B4 + D05274FB027C4BBF583723C06E1A00BB6B370BF6A2B493D7A1AEABBE3DFD627D + 87CE456B431C95A2FFB236C7491AE7D29EC619376F48D91CFA459017B01B6AF7 + 06118BB57A8B435E5CBE0102A7C7A8CCEBBA5978A385BB7A11C0B7180066AE46 + 6B2B57B37502785B1703C031FE6C6DDDE848E571F252BB06F9AF5B752CF05771 + 602F22C573ACDD0BE80D4EDE087E71F67674F84BADDE671B76B6F20D3B59F906 + 1A37EA621580BF4DBA79CB987839E0AFD6A5E76811357A49D38C32299C9BF6B4 + CA4D7BB99EA76D56A44ACD7B07B5086DE51600DF7E0E795F6CA39B5B87B801F8 + A2178993B619A7AEE211FE92C619681B416FC5567DAD12A0583107BC1260B812 + F36A5F6A3F38405FE9C58EC1E18B5EEC345837CA219D33F09754CCD4E475F8AB + D695AECF8B70F4B206876FAFA254CE2FF718E1F0D7616EDFB1DEBAB3576D5594 + C0DF281CBF69017F81BAC0DD6AFDC61BCEDE6A832759B5416A255CBEB4AF0D9C + E0ADBB7C71FDE2F41D1A20306DF5E153ADC688695673E474AB356A86C3DD3786 + 2A367CBA91F6B9FAC8195673D44CAB351AD8AB79A9F69859D26C8F0381DDDD9B + A46CAE31668EBDA1F3BC317AA6D51E3FD7EA4E242DF37CEFD79930CF450A67D2 + 3467C69317BAEA2A561FE0AB3E6A3825006077F54A40DFC63303F0AD3F73B1D5 + 9FC11750CB32B017C72F6DB3F9ABACE9BC95DEF7F182D5D67CE11A6BBE68B535 + 5B181C0FD1FD00ECF52FC47046F0E5D8F235C5BE346BBB8A947B41EDD7ACB70E + 6B3778DB566ABD7A8DC3DFF6EBD75BA74D1BADE346CD6DD03AB471BDA4E3D6AF + B1B6EB49CDBCDA3AE4AFB62E85EBACF3E67506F46DBF6185D6ACB4AE1AF7DA91 + 673DB76DF278C7BC55D663FB06EBB97DA3F5D8B6C17AEFCCF316F76F97C235D6 + 5DFD1E5B3758379CC1387EB76C72D72FEABE25CFD5A5B040CAD7631558671CBE + 5BB75877E0EF8EEDD65D0AB09734CE5BADD7CE9DD67B0FA912778634CE3B76F8 + 97A1DDA9DDAB712685226918F7ED73E83BE0D0211B74842F5EF93276BFC360A0 + 2E2997638A655A00B0035EC64743BD3F77E36A1D623E825F0071FFA3A45A567B + 4C6B8E873E757681BDA45B8E2DCE5B5AE02F319C45C05CDC45834E1DF7B6AF1E + 0B8711750BC75DBD62A32F5F74D81B1D48CC0D501BDD48A1BE2169344FDA80D3 + 273CA5E6D00BA45EC6717BDE869C3F6383CE9D4A80ED59CD85DABCB43135E750 + 94E99FB591D4E3BD7CDE53358F568BBC7F05276F70F146784B1B612D3579875F + D1FC9BE732509616203CFA6A80BC6945D88BB2EBF4C63EE79D74E79A4DC3D976 + F7A64D7EEB7A06E2022CC6E210CE82BF119CD0027AA3D269556959CB3926DFBD + EEF017D7EFACFB776CF6076F39088E6E60FA1EFF30D4FE8C00185013E06ED0EC + 0F03E4A5A5C66E1887E3A2E67E5C048D02D82D02C040A7A80C94D279E67C14E0 + 6F1A30B970333E78D79DBDB48FA4C5CDC4DF0F6EDF8700DB7B19154BBF9C4036 + 62B48C89334F2C1E9381709AA7566B5C1FE3B4D1A91963405EE01E6992577DAD + B18E05DA92C219804BAAE2BC3F7D69793F7D65EBBEA5FEED034FF70C10DEF0C3 + 032BFCED5B2BF8E56B5FB3E9A787B6E1C750731745989956DECF0F6CC34F3856 + 1F3AC004F4AEA36EEE2F3856750EE67F2C02B51C03F475B7AFC608F72F00D8F5 + 97AF6DB3CE03942DFCD3170E810B752E4FEDCC79FFF4C0F2754C3C07DAACC7A1 + 76B0A772D63A402DE09696736DFBEDAB0CF0050223FAC401C0688B8EA7F5C753 + CBF1A19E70909F4FE34DC9DF917EFCF877C57F8F18CB9EE7DF208ABF21EF17FD + DBFCAC7FDF3F29F6ABC6BF3DB482BF7C65F97FD6BFDBAFFA379098C735EDA9B3 + 9336F6D7FCA0FF6729B8AA3F77F730B01D45201CE12EE3E0287EE0EB22FCA58D + FDF83A8CAFAD2285D7577C8DA55B5E7BBC56177E76D7155F8BC4167CFAB6D79B + 5EA6F7060E62DE33F1C712F13DC8FB26D6118EEFA1F83E5AF079F8014684BF00 + E1F89E8C6D04BCF13D1F3E13E2FB3FBC97E39A28D6F0D992FEC1089F53C0DE39 + 1FE1F4673D9F1B1CCB63A0F7350636F3D8A465FFD4B5E8C1470E7CA7BE0330E6 + 71EF2540F8A63ECF74DE77DFB189776EEA3393CFBEE018060207D07B25D40B96 + 22F845110ABB9B589AE09FCFD7F4D91DDCBE315D3463FAB4E36FF2597E5DF1D0 + 8EBF79CDFBE36F28761D40FBA643E071D76F68FD0D5D3F48BBAF7F8F8F3ED667 + EFBBFA5CBF6123DFBCAA6BC8751BA335A3AEEA79DD065EBF6D636FDCD4F8BAE6 + DFB411C05FAD637ED8A52B36F81C4EE29B3AE71D5D776EE97AA5735FD5FA376F + E83AF7A6AE7380E637755DBC6003CE9CB521172EEA9AA7F35CBA6A43CE05588C + 6B1891567AF0D950AB78A0AEB383CF04F8CB3578D0A993BA0E070189FB1C396A + 7D0EEBFAEC0EE2C451AC7E9F43BAEE1F3921E93A7EF4A80D38A6EBB8D4FFE831 + 7706F7DAB7DF7AEED9E7101828DCF7806240E024169DC3C8813110D841F01E2F + 5B41F909DACEC05FED973A697FD4517BA72E1A93061AE88BE8B34FA20F104EC7 + 80C0C4DAE5E75BC702F65F216574BB4D79D63E4F7BB2C2ADD649C2150C1C46C1 + 211C6A05BB36EA9C52E74DB8850BB5872CB036EB36595B9CC0DE6EF2B4D16DD7 + 6ED43E74BDB5D49EB515CEDFA47E706BC0B0E43038F601BEBE2EB4ED88EB18A0 + 30FB60D244B75CAAFDB0C6ADB50F667FCC0F24231CE6079241CB3D4E9914D248 + B39F6EB260A9359CABFDB962CCC579D468EEE2B0761E3FCE4CA0F1ECD0A20633 + B5EF9F31DFEA4F9F670DD5070C379EBDC01ACE98E74E624F293D6596AB11B169 + 73ACF6C4E9BA77986E0D26534B7886D59D30D5EA490DA7901A5AF18953353FC5 + 15524A238DC74D0D2E6020F0E809AEDA529D3113BD76303018D51935CEEA8E02 + 0CAB3F729CD51E31D6534447D7701429A36BA26163747F1500B10BF02B110302 + 571F4C4BCAE811567DC088E0FCED0BFC1D92710BC7B65AFF61991601865FE93D + C85EEBADB5BDD4F61AEC4E60F44A8F810E7CD32E6117CE61C5DD059C005E806F + DA059C892773C062DCC015DAE87EBE751707C1D98EE0780C02163FCF9A940338 + 42E038A65E7065AF15DC5DD2B912D76F7407931A9A16000CE8259D33299F23E0 + 7DBE452797CFA5606F84C0D9F097182DF0B77CFD5619081CC5B8080027629D54 + AE5E4B2B5BB785CFA1B27542EA67E0AFC361D241D7C4E5DBDCCAD49100B800DF + 7A2DC2B1EAE3022EA7F9F2C0E14469101CC7B4E5B58EB5E598F3DAC0C1099C86 + C0E97E9483E0D71B5A19F5CB567B546918ECC0F7D52200EC0E61A0EEEF29057D + D37A04004BA5A8079CC0DE922FD5F671515935BE570B1038ED0006026703E0A7 + 5FAA65A5A534DC8D4A8FE33C8AD0375B69F01BDDBFC554F155CB40DE1CF0F7A9 + E75E719548806D36D8CDD61F994B2B02E06C08FC28000E70363A7581BE31F573 + DABD9B4B8F03C3BF27C06E496A09A7016FFABBF327A9D877ECA9365BBF371FC5 + 7CA25CDFF7A7F52F03C0D97A0C102ECE548272B197C7E971C7A7FBAEC882A458 + 02D4CB81A6D7FC8BF50F01E05C7A047A66297B5DFAD8ECB9B472ADCB56AEE3D2 + CA75CC1F55F6F1FF7629A0FF885240F7FF970070B672BD804B94D27F14FF5951 + 8F81BF28D71BE59FA2E431730160F4C80744D463D647C5E3727F38FE0F9403DF + A2F1A3F037F7F3E602101DC0397F9DA43545635D0C13005CCCE1AB7E06FE6603 + E034C8CDD2DF0A8023F84DCBE74AEBE29E8C4B26D0372D62518FC0DE671F337E + 561B9CC7A82400B8C21F07C069E89BEE473908AEAC8D59655CBE387F1993B619 + 98AB0DA0DA67AA3470F84ABF6ADD0ED665C0646BD679A83D5DB1A69E4FF50077 + 9FAF136A026B5D48051D1CBFCC3D2339D84D806F5A45303759435B2581BF6C56 + 35060097AEA24DF44BE179443126EE732F037E83BB37A6718E8A8037F6110038 + 2ACE673B7CB3812FAEE0007E755340CD5EAFE59BB87BB31CC0C4CBA5DCBD0E78 + D546D72F0200137FAE8E6E8AA467A598DAB95CDDD6BA69916A05855880C001FE + 92AEB9A3C35E87BEF53A793FC25FAFD5DBA88B83DC087C69631F017E11FD087C + 2BB03EE95702FE26A99B2B038149E99C387B83BBB7AF6EF6FAEA0631518B3EBA + 81ECA31BC97EBA714CDCBD6D02E4F55ABC6A23EC654DF6DC0BC05F6211F23A04 + C6050C10D69878A7A1BA011E662F7618E2F117DB0DB62A1D15935EA0A6AFE65F + E936D25EEB31CA6BF6BA935702EC92BA19B88B0B18C0EBE0B777A8CFEB357AFB + 486A8957951C00037A01BC090C8EB5798BDCBC63336BD2CA387E13017E49F3EC + CE5EB535874EB1EAC302DC25AD7304C031BD7394A77D06FC6A6D04BF41B87C67 + 584D5238E3F09548FB0CF88D00D881AFDABAB878C7CD7557700DD690D679C242 + 6991FAF3ADD6B839AEDA13E63AE8C5BD5B17E83B7981C3E008787D2C359C468D + DF453E0600D7515B0F104CED5FE6A6D10F699F01C0387C71FE3620FDB35A0070 + 93792B5DC0DEA6F35739F875D7AF622D16ADB3564BD65BCB256BADF9A2C4EDB0 + 745500BECBD658DBE5EB2CD45253BB72BDA7D1A3CE1A29F500BE1D12F0DB61DD + 46EBB86E93B738393A6CDC68ED36686ED306EB98BF49523C4F6BA44E0579D679 + 739EDA0DD6A530CFBA6E51BF709D752C58E310B8DB56C5B7ACB3AE5BD75BB76D + 1B7DDC7DEB46EB81340EF077A3F5DD5D60FDF7165A0F8D71003BFC55BF2BEBB7 + 6FB21E3BF2ADE7CE429D9FBABE9BBCEDB6355FEB14DB5AA8C7A0E5CBC96DD663 + E70EEB25F5DEBDCB7AEDA66EEF4EEBB18BBA773BADF7DEDD2E40AF035F52317B + 3AE7DD2EFA51CCC5D4CDBD0FECF774CE387EBD9E6E02812308669D43E00400E3 + CAF57E9CCFC05F1D73ECA80D3C8123971ABB87ACFFF104FAA601F0B103D6FF44 + 00C0B4FD34A60FC005ECF6D3397007D3C7C94B7CD8F93336E1C6551B73E5923B + 7B51741D0D3C7DC245ED42DC48C3CE9F75D0CB97D428A6DBC47905041E7631D4 + 62C4A915533147C57874718D24BDE76509089CD4E51DFBE645AFD1EB73D23880 + EBCD2BA1AEEEF5E0DCF53E3000D87BFD828DBE71D1C6DEBA6CE36E5F71481BD7 + A7452C0AD80BE88DE017C53540DFE9F76EBB80C053DCB176D3E16D4C031D9D71 + 805DFA33EFBF9D01BF45295371E406279DD7CB4CD6027249DB8CEB7786E66751 + A3F35D1DA738FDD9F703C001CA84D4AD212573003BB8F18200B528401DA00D10 + A8281ED6E10204E204E1068ED09734CE1136218014F3F33F0DC0B70836BDAF7E + 519DDE989639822BFA5E03550A5018601BDCB700DB087401610EC3148FD00DE0 + 063003A6E5826FC4E2DA3817D7D14600175B1E13A76EA83F9BD49AD51800BBE1 + C7C4B5FB13EE589CB63FD8D6BFFC687BFF6F7FB16D7FFDC9D67DF7C0533E6FC4 + F58B03F8A7870104030D757C4C7D8C22C405BA026F37FFFA8D15FCF6ADE5FDF6 + 95AFE5F137FDE98183D9003F8B5CB311A046288A380F6B37038213309BAFC7DB + ACE78A731778EBE99BF55CDC9DCBDA04FA728C3B87FFAA351A7BDA67294FC702 + 84B7EA5C9C23825EFABBFFF37BEF530738BA8377E040D65A04700EE0F7815113 + 389C2FC0DFF877672BC6FDEF489E5B1C33170170BACD67CD5FBE72F00BECDD88 + 3B3A6923F8050E3B204EC5D2F1B4D6F2FF0300D6BF95A79476B77070FCAEE7DF + 83BF4B7F377311F8F2DA723730B098FAD15F110F73F175185F6351F1359A8EC7 + D738AF77E2B48C89C7D72B0E77DCF011F6C6F71069D0517C1FC5F7174EE0057A + 3F2EFE42EFB187A11EF6E22FEEEB3D1ADCF9110247C89B06C0FCE02382633E23 + D25098367EA6841F7DDC7588CBE7180037C0DF007BD19C8FDED55AC071700123 + EA004FD5E71A2D9AF1DE3BFA0CBBABCFCCB76CF2DBB76DCA5D7DE6BDF396B793 + DEBAE51078D25B643448A78B266D7EA8211C534423807074144F7EEB863EABAF + BB80BE00DF09B7F8BCD6F52A01C0B88327DED6F96EDF509CB537BD1D7BED9AC3 + DFB1D7AEDB98AB577D3CF6FA0D07BA93EE007F71A0FFA0F613C56ED968AD03EC + 8EBD7ED3C648E36FDD96EE04607CF586AE47D7326B70008FBD7E4BF3EF6AED3D + E96D3DC66DC57102A39B36F4C25507BC43A8217CFEB20DA69EF0455DDB742C80 + 783810F8FC9B360427B1E681C343A913CC0FA9CE9ED675F8A4F53D7EDCFAA163 + 945B38A9EBF019D7A053A7ADDF515DD38F9DD4BE40F193676CF8D90BAE61D428 + 3E71CAA12F1AA0E3071CD3390E1FB5DE870E59AFFD071DFC0ED0187730FD34F4 + ED09104E14A170AFBDFBBC6C45DCEFD0070477D57E09F88BBAEDD4787B00C1DD + D547F4B36B05C73EAE6052450380A9314CE90C4F1F5DB8D93A71CC96ED1A17A5 + 8A263D3469A23B20F5A91FDC611369A30BADED86CDD6667D81B5DB5868ED7104 + 4BCC67524893FE99FD2AA9A1DD2D1CC0702BED655BE1FE454060EA080389D9DF + 92629AE31238DC72F99AF003C8041AB74E6A09C7D4D12DB467762D093F9C8C3F + A4642FDD74D1726B343F006100B1A7955E2CA925D34E635CC329E77028B9A2FD + FB6CEDEB6769DF3F738183E08638837105CF657FAFB84361E0F06C6B20359E85 + 4B78AEEE0766E99E61B6359C3E47F70F73ACF1D439D664DA5C6BAAB946D366EA + FE62BAA792AE3B79A6A793AE33517D5CC012CE6140B06B3C69A271094B632659 + 9D51135D40E1DAA301C1E3AD16F03701C0C0615CC331757414E9A36BE106262D + B41401705AAF0F2A4A155D8D54D10386791B629AEB1FD24767C360FA2EC6A487 + 96DC090CE4ED3548F7960375BF39D85EEFA363192BFE52D7BEEE0A0602073770 + 7F87BC08F76F7400A705FC05EA566EDF23B880A3033891D70B46C938C2E1CA29 + 009C16F0B762CBCE56A96517DD8B07004C8B233842E04794A47E06FE027D5FD0 + F108081C5DC011F4660360FA71CDB38D22EC8D2EE0981E3AC0E18C1A0650EC00 + B75E0B2BAF5885865AD740EBEAF13D490B2B5757AAD3D205107630AC181018F8 + CBF1883100D89DC159D037CAA16F6A8C3C65748DE2F0372A82E108878B398101 + C429089C06BF19E1F07D2571F24A99D4D011F43E46BE2E81BDD9003842DFC729 + AEC9C8E1B08E492030E037DB014C1F6503E0D8A6959ECF06C0D1FD9B86BF4F55 + 7CF5515548002F30F60922057436C80DFD30CE35F7E8DA22F05B14AF6A11F63E + 518903380D7C1F077D330037C75C541AF46644DA67948CF9EE3CAAD877E7C9F7 + E78FCC652BB5B6D83897B2D7A7E7D2F144F13BFEF89DFFDF620ECBA534C07D44 + 39206F31F89B9ECF308922E5622E4F52F6F1F11CE97E1AFEA69950B135FF0572 + E61519D913948BB3E5541A7EA661683AFE4794EBD87F54B91E27ADC7AD53AC44 + 76FCDF1E00E784B839A4B525FED0FAEC3FE8DF578F7D0147009B5236FC45B9DE + 28FF0C659F3FBED9B3953EC6153F28B2E3298563737F38FE57A9A42B7CC09790 + 4AF121AF71B135A90FFDECBF3BFB6F606DFAA29191E668C3C54B17C3B41CF256 + 79349E86B78F1190370D7A639FB664F9AAC5D73D09E86A0C004ECFC57E84C6C0 + E192E55E0B7329D8FB1440F739DA949ED3E6C7FB6AE9E71070B76445E06E36F0 + CD5611002EE6F64DA06F6C9FAEACCD9CDA121500C49A9700C0EEE2555B522AC1 + 9AE7B5D1D3A6B0FCCB0DEDD9971B58B917EB59F92A0DACCC0BDA28029081B9EA + 87B4D1B5B5A1433ACF1F4CF31C05F08D10D837A8EA977EB9BE9579B95106023B + 084EE02FF132492DDF32AF68C39D0300BBB3577A3A89A72130757E2304F635AC + A52FA56BFD027DA300BFE56AB4B4F23581BE2DB4B1D7CD4504C0B88013285C3E + 71F81673F92A56D6A59B18E2C0DFBAED7DAE1CAAAB3575DBE9A605B76F5B77FB + 8614CF0100C73ABD41010047E8EBC2CDDB30D4EB75276F02759FCB82C011043F + CB1CD09775127D445DDE589BD7D5A29794387CDDE5DB473785B87B63CAE67EBA + 89A43E6F11D875A89B5226A5B3E4F57A89B703F60EB6976835060657C1C52BE1 + E8653EF407BB00BC2F7709B57B5FEA30C45ED21C7D6AF8BE442D5F9CBE3D4659 + B55E63AC5ACF22472F0EDFD7FB8CF531001837AF03DD3E63ECF57E01EABEDE77 + ACD68CB15713176F5A0E76D54637AFC7526377FAF62BD26BFDC7D96B03C65BB5 + A88113EC8DC193CCD33D0F9C6835874DB51AD21BC3A6B8627AE71A23A73BE88D + 71C498B8835F5CBF1AD71A35DD6AE3DA1D3DCB6A037CA5624E5F1CC06371F5CE + B17AE349E74CDAE7D941E3E63A00AE475AE70940DDF9C504BCC5C15B7FDA2289 + 74CE6AA5007E177AEA666AF632475AE7BAD3740EF5EB313F132D71B72F02F802 + 80512320B004E805FA46A72F7DA0302DE3568BD75B4B0907708BC5AB3DB57374 + FBB659B9CEDA017DD5B675F8AB962FC9560508DCD1A16F91EBB7CD5A5CBD8A6D + CAB78E7979EEF46DB749739B36585BF5517B0030EEDB2DF9D669CB46EBBA2DDF + BAED28B0CE405F1CBDBB0BACCFDE42EBB9A7C07AAA0FC8ED5CB8CEBA6ED960BD + B6E7594FA9FBB68DD67B57810D3AB0DD06EC2375739EF5DA916F7D766FB65EBB + F2ADD79ECD3A76B3F540BB36EBBCF97A9C02EBB173ABE6B65BDF7DBBACFB8E6D + D605F8ABB6FBCEED01F4EED9ADB9BDD6EFE03E1B70F880B7BDF7EDB63EFBF758 + DF03219573BF04EE026E7B2BD66BFF3EAFD78B7AE958C69974CD127577BDF62E + C71C4ED230E3BE05F66A9DC362C030299701BF00E004143B08D6DA81C78FDBE0 + 93A4603E1A00F009C58F07C72F699FA3EB77C049AD3D7DC4069D3E2691A6F988 + 0D397342FD008081BB034F1DB381678EDBA073B87883DC917B01E7911EE7DC49 + 1B7AFEB40D05F29E3DA5F1298D81BFACA1062F60F75C58A36300BA63AE5DB6D1 + 57937A8C09E01D753580DC91005D5CBD5771E05EB271D7AFF897F963D4BA834B + C74D507FCAEDEB36E9D6351BE7E7A236EF6507B20E6BA9C39B80DA89B781BA57 + 34C641469D5D1DFFD6159BF4CE359B8CCB56C7006E01B811E2128B71C4393957 + 8C45283C05E7DA3DA06CD0548DA3A337BA7A8343EE3D9B759F74A80017E04851 + AA54E6638C791C71C05F62733F7AC766027DEFDDB699EFBD25692E79BC59A456 + BD7FD78F03C6C473A69D78D1A137AF98432FB8F33C9E8CE373604D84BAA18DB0 + 57FA2CC0E0087781B78B1E7CE06009E88400C1B811635D5EC05484557E4C02AD + 02D00A70CBD3383F088EDD34AC25061C631C53F0D2674D5C175D99B428AE8BC0 + 8E36A4E00D8EE1281E6B99DA958AE3F60504E3025EA3E301B21B00B8DFD17E66 + 793FE1EEFDD2B6FFC79FECD8FFF1BFD8C1FFFDBF59E16FDF3BFC253D343580E9 + E7FFFC956DFC8954C59F271039A42F2615335013B809002EF8F56BDBF4CB5796 + FFEB57B60507ED6F5FB95317D099F733D01478FA89CE87F3358C03300DD03403 + 55753E00F08E3F4B3AE7AEBF7E9781B500E01DA471FE6F3FF863E4FF1CDCB5B8 + 8677FC37AD514BFAE8ADBF7D1D5247EBB96CFEF9A1C3E3E8F8A50FE005FA4631 + 269E9E033407F0AB353ADF56CD6D561C1730CFD3A1770AF2A6F5B83802FA3AFC + D6797000F3F792EE7993E6B2816F14E0370D868945081CE3EB752C4EE0D5DF7F + E87017A72F8F45AA6D5FCFFF1DE099FF1369AD5E0F38C5FDF5A5D70963D245AF + D6FFBBD78156BBFADBE01E26CD34AFBBB4E26B31FD9A8D40D8CF99BC66D3AFF9 + 304F5D6BC4FB24BCDF80BDA10E7680BEF1FD454BBDEDC57AFF2DFBEA635BF210 + D7ED476A3FCABC9FE38F3418A79DBEB411FEA20078E38F4802088EFDB91F8794 + EFE1F3E29EE27C86857E5404BF80603E67E847272FE99C67BCAFCFACFBEF3900 + 06F6028081BF40DF29776FDBA2CF3FB1059F7E62D3EEBDED1018D80BDC0D3F9A + 093F9089E9A123F865EC31AD9D709BCFFFEB363E01BF7C46131F778314CFBA1E + 688EC702FA1601609CBE405FD6DDB4F137F4197FEB96BB7A27DC7A4BD78DBB36 + ED3D52CB7FA9E7F5BEAE45B76DDCCDB7B4F68EF7C75C0F299D475C01F8E2E845 + 648150FCEA0DAFF73BE96DFEAD1EE81AF391E277752DBBA3EB1C60F79AAE7B00 + 63F52FBF69432F5EB2619702E01D72E1BCF74778AA69CD5D609EB90B36F8FC79 + E99CFAFCC8EA9CAEE3A76DC0A95336406DBF1327D4AAAFF1C093D28900807101 + F7573BF8D4591B79E18A4DBC76D3C65DB9E62078D0899301029F38AE3D03B0F8 + 98F5252B89D4FFB0F61A87D4A2C361DC9734D1070E598FFDFBAD074EE1FDDABF + B80EBA7338C2DFDEFE8336ED73F6F103B8008501C20E886929971133A56481E0 + 4ED404DEC67E8B187583B5FFDAB65D7B369D5BB1E018266534E9A3B7AAAF7141 + A1B573E82BA5EA063B14F6FAC1DBACF3E61DD62951C782EDD67E938E953ABAB6 + 380C26A5B443E18DF9EE10A6A6703BED5FDBAE01FA6A6FBA7683F6AEA1B6705B + D2497B7D611D93006420B13B89B5F7650FCCFE977D308E61F6CB2D24DA6C0185 + 9B2F5D19E0B0C6CD97ACB4A6C05F1CC5DA63B3A6E9E2E5D664E1326B2A355980 + 28C3125CC23884A34B18380C0026F53470B8217583E7E012C60D3C477BFE79D6 + 88FE8CB91918DC683AE0779E359FB5C09A008DA7CCB4BA93A659BDC933FD987A + D3E6EAFE638E62338214AFAFB6FE24D64DD77DCBB4E01406048F991440F06820 + F07857CD91638B346ABCEE9DC659F51163BD8D29A4BD8670CA0D8C0BF8759CC0 + 43759F98388201C0D580C08347281E9CC2EE1A8E9098F901C3ECD5FE43ED957E + 43BC0512BFAAFE6BA483EE176A0A93221AB72FE097D4D1557A0DF454D2C0E157 + 35C6195CA55B5F4F070DFC7DBDB78EC729AC31CE5FDA742DE0285CBD2F74EA65 + A4824669B72F020047404C8AE84A6DBB05084C0A68891AC1A48246D9303800E0 + EEF67C2BB56D624AE82E9936AAC80D1C9CBFC0DF175B75F136ED02A68DB03742 + E008807D8DE4B1466DAD1229A57115270AE9A0DB044770A33656AE616B2BDBA0 + 9595939E55AC62439D47AA585F6BEAB6B467EBB408225574E21CC6211CEA08E3 + 326E63E5D52F8B3338A675563FA6884EC3DE6C001C21310038DB011C01B0A788 + 967CFEF5C6EEFE7D5A8AE9A01F0B8013D76F1AFED23E92FA39354EC7630DE1BF + 15021783BFC93A3760E0047E894C7D01F6BA927EAC0B1CE16E5404C211F6A2EC + 317A14023FD9011C5DBFB9D23FA7C170AC01FC28E42DAAE71BE7D2B580D3FDF4 + BAA2F5B4458EDFB462FCA9F22FD95309B405DEA6E16F06DEFE13E4DF87977DBE + F838F33D79F1EFCE633C3A8473AE45A9F5B940EE13E7633F47FC8F00E0E2ACE0 + F7A4E3D38037AD34E0CD8EA5440653AFD7FB3B4AF38A5CF3D98AEB8A1D135950 + 6451A9B57FF4BCFF1495FC7DB89BE167BFA322CE96B0C26C201AE37F54B98EFF + 4794EB311EA7D47AE06F49806FB179E96F06C0E5A52C4E5B4C250200FE5B4070 + EE13692E71F79600DE3E93E5F64DAFD3394AEA89FD3E04CEFE83FEC7EAF75E94 + 11FCA69506BF6945309B0D68FFD58A6FF66C3DB236F32B91ACF823E258CE113E + 14B33F64FFD58A8F575CC4998F7FDFA3C7A5FFF6743C9EA3D8C5244BE1E2A58B + 6C1AF4C6BEC3E024964311F866EB71EB8A1D533637FC2D555E9B10C532A03789 + C7B5215ED54A95D586A59C3635AC77F01BE46EDE6240988D8CD6309700E26CF8 + EBF339616F2E3D1E00A312491BE7220CF67595B4617BBE96957EB18ED7F12DF1 + 6C381731F48CD63DA335CFA85F06909BC481C60E83D57A8DDFE7F9A5602D8B35 + 451E510AF866CB6BFB267A0400039E01BEC05FF48A36DF92C3DE2CC8CBB8544A + 71BEECABDAA8BF1600B04360D6723C6D96320018C72FF5796BB4B0F2B5742321 + 95ADDE521BFCE6569A3ABFEEECD5CD492AED7374FE6620B0DA50CFB78D6E3C92 + D4CEF500BEB1966F74FA92DAB97D31E04B1F3D47BD5EA97C7D2949F51C9DBC11 + F25602E236ED6ECFAA5F5E319406BB28425F14C7E936C2DFCA2D7423D9B2B755 + 6AD9CB2AB6E8EDAA94005F606FB600B955DA0F72981BD33867C02F7579D592CE + D92131F0B7C3106F89BDD461B0BD0CE4957001C731699C5FEC38D848ED5CB5EB + 70D7CB5D87F918F8EB5097DABE89B3D75DBC89FB17000C14C6E5FB4AAF50BFD7 + 1DBD3875FB8EB537A8B93B6082BB7B637AE7087D630A671CBC1947AFC6CC55C5 + 39DC3FC0E36A3A9EF4CD190D9206EB9C83437A6777F946D03B686270F74AD1D5 + EB905702EA46976F9CF3186E5FA9BA626F0C9FEAEEDFBA63E7589D31B35CF4EB + 8DD378EC6CAB85E84F9C1BA0EEC405569BF4CDA9D4CE387D5D387AA9CD2BE1D8 + 75F03B756188CD0CAE5D202F6A327399354CA02E20B7BEE6427A67AD4109ECF5 + 14CF6A59175DBD31CD734CFF4CACE9FCD5D66CC19AA0856BACE5E275D67AE906 + 6BBB3CCF5AA97500BC34A4C06B85BB412D0E88E8F6A50500B759155C127C51D6 + 71439E75DCB8C9A16F9B35EBACD5EAB5D676DD06EBB429DFBA141458A7827C6B + 9FA7F94D3A7E938E913AE46FB2CE5B0A1CC83AFCDD9E675DB66DB26E3BF2ACCF + BE2D36E8C84EEB7F709BF5D895A7D826EB4E8AE7DD05D60BB8BB233F3881B76F + B27E7BB7D8C003DB1D04F7DA996F830EEEB0FEFBB65A5FC5FB1DD8E6E7EAB9B7 + D07AED29D4F1EAEFD966BDF7E1E6DD657DA49E7B77641CBE3D77ABDDB3C37AED + DFE3F575FB1F3E608380AA386B0F31DE6F038E84D4CCC05BD22B86FAB987DC9D + 4B3F02DCA8745AE790BE992F5FC3DA41D4E1E598E47C83742C62ECAD62ACA356 + 2FB577079382F9F4711BE46EDE63DEA7C66E70F31E76C83B9CF4CBE74E260057 + 3A87BBF7A4D7D205F6F63BA9C7030C3BF03DED8E5CC02FEB1D0813BF141CBDC3 + AFA022F7EEF0CBD29BE76D4484BB52518AE68BEEC2420E7601C18A8DBE71C9C6 + 52CB11E7D68DAB36EEE635AD916E91A2932FE6C397F87CA93FF5EE2D9B91C050 + D27D8EBF851BF79AC71DD8AA8F638C3ABAE3A9A1FB36B579437AE489C0DFB7AF + 7AFA649CB4A470C6C10B5405B246984B3CF623188E6EDF3837EBFDBB0E656967 + 7FF04E589B408AE0F00DC0D781C9FD006AE9CFFF1410037805B8148158002C63 + D6CDFFF83D5BFAC527B640F1791F6ACD47EFD99C0F388FCEA7769E8E658EF5C0 + 5F14002E4E3F9C7FE17CC05FD6C5C78C7368E1E71FF898C707ECCCFF84F4B4C1 + 2D48BB08D7A0C3A2008FBDD6E81780DDFBEE300434018111AEC3E848A48DB078 + F9371F796C21E0F8E1FB5A277D05D0BAE7027251C717F805204BC3DE08CE1050 + 0CC5386B585BB41E00F791F7577D9B06C140E0E4F8AF70642602B2E97123045E + F5CD070E80E96FF8FE535BF71D0EDE4F3DD5F3DA6F3FB34D3F7D65BBFEFBAF2E + 1CC1A47FC6CD0BFCC5018C1398B4D1405F2030D0B50058FB4B009A00585CB279 + 7F02987EA9F897B6F5CFDF3830CD27FE23C0F773CDF3981F79BBF937C02DAE5F + 523F1701533FD72F0F1CEEEEFE8FEF6D0FFACF1F02F00500EB31B7FFF55BDB09 + 10FEED6B2BD0636E561CF7EFF6FFFC4EE702D252BBF72B07C0DBFFA2E7F19BFA + 5AB343EB77FF35A47E26D533CE5FA032E3E80E8E2098F500E058F7771BE7513C + BA8079AED1D51B15016F54AE981FF36B80E69CD75B09F72F6999D3C09716208C + 00BC51710ED84B1A68C61C5F04803FCAB88BD32DFF77B8B0795E8C634DE7D57A + 6DADFD56AF09BD363C06F8FD5AE791D6688E35ABBE0B3F48E0F5198070D10F14 + 780DD2925E9A96D7631CF31AE61862AC03022F268D790600D3EAF5AAF7517C7F + F1DE8A3FB2883160F0F2AF3FD1FBF1231DCFFBF3A3CCFB1D773E0AFDF8238F00 + 7DA3622C825F14EB01D3223208F0B912145CBF517C9E4510CCE7058F1DE3B4B3 + F4B985FB77DA3DF476C6091C626FDBCCF7EFE9DFE31BD7DC8FDF77A81B016FCC + 8EE06E627DC6120F6098F86DADB9E95917A22338CAD76A6EBC5F27B83670CEDB + 0E8111007AC2ED5BBAE6E8FA729374D33A8FC693DED2B5E5CE1DC56FDB6880EF + ADBB7A8CF76CECCDDB36EA1AE72375F53D6FC7DD7ACB465EBDA16BE0359F438C + C7E8D831D7700B0717F1F47B1FEA1A76D7465E210D340EE0EB36E4C2251D77D5 + C6E8BC0E8CAF5DD3B157756DBDE410380261C6B411000F3C7B56D7E0B3BA6E9F + B721D25080B0C6A176F019B5A71D06F73FA1EBF9C933DA039C0BE99F81C1C749 + 197DC1C65CD6639DBFA8FDC2191B70FCA4F606EC114E681FC10FC028FD10C6A8 + EF61ED47BC4E307B99630E86A9314C7DE1DE070FA93DE46DAF0307A503D67DEF + 1E57F8C1DB3EEBADBD121098EC26211B0A29A27759D79DDBDD198C62798C1EBB + 14DF46FDDF2DD645F30E89710D3B240E020C07B7F00EB5DB1DFE76DEBACDBAAA + DFA5905AC3FC600FA7B0E60BD14E07BFA8CB1669EB6EED2B775927C53B16ECB0 + 4EF9DBBDED98BFCD6171BBBC026BB331CF5AAFDFE4E9A2BD9EF0466A0AABBF5E + FBD20D1B25E2DACBAED51E1708BC3ECFD7018C491BCDDED8D347B317664F8C93 + 98780287DD25CC9A15FC7852ADE299B9E4D8E6CBB40F5FB232A492568C1F5A02 + 839B2F010A0730DC6C714827ED40387107BB53781E2058F706F39704203C4FF3 + 12A9A2EB93FE19003C6781F6FC0BADC96CB50904A66D3A67A1359DA57B8D69B3 + 1DF4E21266CED34B4FD37D0A6E60A99EBB87675AFD29B31C12E30EAE457DE0F1 + A16E709DB1135D7513D51933C16A673431933A1AD54A846318105C9B3AC26A71 + 0857272D34303881C0D52487C1C346EB3E0C177100C59E267A08107884BD3E68 + B8551B384CA20D7AADFFD000811347704C119D4E1DED758451AF81EE0A7E456D + D51E031DFCBED67390CB9DC0A48BA60E70D25601FC4ADE027EBBF4B197BAF7D3 + FD6F00C469877000C452C75E0E83A3DC1D2CBDD43E28D6042E26EA027B2AE8A4 + 4670563F0D8119E3067E5E7A41E3A0DC00388ED3009875B411FC56C6519C0302 + 0710DCD69E555B56C7976DD8C6CA37A026701BAB58BFB58BFAC015EAB60C7582 + 1BA8EFE01817B1D66A3EA4926E6BE5EBB5B632498D601CC3E5EBB5744770765D + E0087DD31038531F1825E037A67B2E0E80C3DC33D59234D0890BD8A57E00BF3A + 86F264EA03721D02BF06D4C505FC28E88D72509C52C6059C281BFE46B09B06BE + D963D667C79E7EB9B6957CA966319578B1863DF54200C0D92EDF088023E42D51 + 09B05BB4EE490038C2DF3404A68F4A552A5E03380D80D38A003882DC6CA81BC7 + 69017FD30038AAC4B35513712C003900DFC7E9A97209904DDCBB1102FF61009C + 72F53E517FD4015CECBBF3D4DC6362996368737D0F9F9ECF9ECB56728EF8DD7E + FCAE3F1700FEE3E2381D9F06BCB914616FF638ADD2B9B84A918AB30A9D23C79A + C729730C1C27C59EBC9FBD2675DCBF4AFED839D8D93FAC14042D5122C0D31259 + 7157F671D9CA5EFF8F2AD763A4F5B8353C7FFD1DC5E61300FC5409FD1F9648CD + A7953E871F43BC88BD3E2A606C4AD9B03797E2C1D9B10C00CE80E0D016537CE0 + EC7E4EE5F883FECD940BFAA615DF74691503BFC938D79BE55FA1F866CF56CEB5 + 7FE879712CE7C8F521F9AF57FC502FFEE19E3DCEA547FF0D50113CD607735A3A + 5754B860E9229B06C028C2DF2700E0A80CD44D946B1E07709CF37509D005EE46 + C57111E80D6BA2D2B1926558F7AA36071A47B09BC0DE088333CA82BDB9F428E8 + CD2D5CC20E7A2B689395C0DE6C45E88B1CDC3AFC4DDCC01535F77CA8E35BAA52 + 2D2B45BAE74A92E2655EAC6BE5AAD4F77300813D05B4A78D0E7D523E07000C08 + 4EEAFED20710733EA07192169A3ABED4EF2DC939923E7A9A9ABF89324EDF0402 + D396ADDA38A334008E7200AC16E08B93372A7B4DD9040097A956BCCE6F36FC75 + D76F75206F0B2B57B365060097ABDED24AE3FEAD11D23D3F5B3B2A387E23F48D + 1038D6F3C5E51B9CBE6D1D08FB38037F839EABAF1BA6469DA54E56B1214EDF00 + 8181BF15347EAE61477B4EF369F04B8BF3B772D3EE56A9690F87BEE51A767600 + 0CD48D309716B84B2CAD6CE85B5CD4F4ED6B9549EF1C812F10374B0E77DBE3DE + 1D9801BFCFB793DA6B2C798A67EAF5FA9AA8E0FCAD8A9B370260AD2106E075E0 + AB36025FC6AF761BA19BE21156B54BE8037923FCF53ABEDD47245038A480C6B1 + 8B80BF31A53330D7016E0AFEBEDA676C18F71B9FC0DF71F6FA80099EBE398A31 + AE5E4FDB8C064FF674CDAEC1935C80DFEA43A604B72F3077D474AFE1FB3A699E + D522406EFDF1735D75C7CE76980BF0AD83BB5773DE4F62B5501203F6D6D331B5 + 750CA25F6FE23C77FCD69D30CF1A4C09299871F1D623A57392AEB9DE64C59294 + CED4EBC5D5DB64D6326B3A7BB9B7B87B11FD667356B8A283B7D9DC95993E6037 + C2DD281CBCEEECC5C99BA4768EA0B7F9C2B50E785B2C5A1B5CBFC4E705C76F8B + C5EBADD5B20DD66E459E755CB5D9DA4B6D576CB2D6CBA979B6DEDDBE387F497D + D701C88BD6E5794A3C9C0FE18B2FE06F8175DE24E51538F06DBF5EF1751B3CFD + 7397FC42EBB6658B75C361AB963ABB9D36E7BBF397D4CBDDB76FB51E3BB758CF + 9D5BADF7EE6DD673B7623BF3ADD7DE42EB7768BBF5DEAF6349E52CE1E6EDB37F + ABF53FB8DDA12FF017E88BDB173106000F3DBADB06B2661F0078AB1FD37BDF36 + EBBB6F87F5DFBFCBFA1FD8ED2DEEDF5E7B776A7EB7F5D9B7C7FAEEDFEB409858 + CF7DBBADD781BD7A0E01F83A043E76D8069F00D206583BF018A9984F4800D9E3 + 36F0F85187B4F469879E39E52DEE5D4FDB7CFC98035DDA0874879C3C618375DC + 50B56818ADE283B57EF869523AF2A5ED311BAC76C8A9933AA7E62540EFB0B327 + 6DE4F9D336421AA23E2078C485D336FACA391B79E98CC3DDE197246FCF18A994 + 6901BC43CE9F9274BE0BA76CA4D6E3B405E2321E7691D8595F8F46272D757349 + BD3C8E14CC491AE65873975ABC405E202EE01617AD8360522D274E2F444D484F + 03EA5FD28794A01348D97927AC99460DC97B40DADBC1E175273875432D5EBEE4 + 0FCEB00004A8C17B3D0365A7BC734D0A2995114037B86BDF72576DBA8D8A2038 + C6E9CFF90010FB4E660DD003703B9DF3BE9FA475C6BD7B3FD6DF0D0018CDFD04 + 2012D6CFFA00B882CBF67D9BF729203840E0F99FDDB7255F7CECF0762E40E563 + CD7DF4AE8360620B3FFDC0167EF681CE1352AEA6A14B04BB69D09B167368F117 + 1FD9A2071F26E3F71DF8E2225C421A595CBDEABBAB5771DAE02C043631C7B998 + 0700E342D4FC57014201A40054B4110007C722809835B4A4880EC02B425DFA19 + 582BC57874EDD2C6F54033E059046A212E7DC3DC478A03813FF431F31C43BBEA + 5BCD2B46BDDF157A8C98067A9DD603F700BFD4AF05046EF43AB60F15FBC2367C + FFD02170DE9FBE56FC4BCBFFF91BDBFCEBB71A7FE99018B97BD861E203170E56 + 079FBF7EE17DE670CAEEF8F3F7B6FDD7AFADE0C70719C08A7B77FB7FE0D6FDCC + E12FED963F3FB4021CC0EA17FEF68515FE99C7D5F9248025699C01C03B7F0B8E + DD5DF481B4BF15015E0484C6890BFCC5FD0B64C559CB73611D909558A1CEBB15 + 27B08E01046FFD2DA4824ED7038E0E6057E2028EA9A4398FD7FF05A2FE88A3B9 + A8C66FEC0372B35DC1C4F97BFCDF4AF2F86FE13993C69AB5A48006EC46F72F7D + EAFF0276D32038170046716E83E21CBFF627526F037C01BABC06F4FFEF69BB3F + F5E7CA73E03911F7D7C8777A6D7CFB59C6F5BDFA6B1CC07AED7DF9B1AD7427F0 + 47FE5A8BAFE1F8DA8DAFBD087719D3E7F51C5FD3E9D7B5CF7F15D23F2F551BDE + 2B40DEF0E30ADE5F28FD230BC62B7021EBB58B0338BCC7F57EFB82F738EF533E + 038AC06F1CF3838E180BF1F07EA78F2B38005F320304A77070FF86CC057CDECC + F51F96B0FE036F81BCC4F93C2A82BE211DF4BC4FEEEB33EC0387BCD3DED1E7E4 + BB776DCE87EFFB380D81E77FF2A1AF9BF15E700EF3B9CD6779CCA81052E317C5 + 823318A8CB8F7B48131D6AAD176561083FCA7120FC5600BE5374DD0829A7DFF2 + C7040A8FBFC53506307C27E9070730C077CC8DE0029E70E71D87BAA3AEDD72E8 + 3BF1AD776CFC6DE277BD1D7BF38EE6D16D07C023DEBCEEEB981F73F32D1B75FD + B60DBB0428BEE9F1D15A37F4F29B5A0700BE215D0FBA4EDA68A0EF451B7EF992 + ABA87FC541F0E0F317746D06FE5EB0C16701C01A9F3B6703CF9CD5F89C0DF2F6 + BC0D3C75567B0F00B0AEEBA4983E7D2E930E7A88E6869C3AEDE9A2FB1FE7475F + A7B43E19037ED5E2221E24017DFB1CE6C76A0100871FAB85B4D1D410EE7BB808 + 06F73E78D0B39874C3059C94B488A52D507007F3A3BA54598CDD94CDD0DE6A9F + D63804DE61DD1D1287D21ADD92CC2BF4BB6CA37630995AB42FDCB5CBC1306EE1 + 6E3BB427DBA9F36D0F2EE2EE6ABB6DDD695D0A833A156CB7CE688B9400E12E52 + D72D9A0706AB0F380624E32C6E9B9767AD376EB2361BF2DD51DC05C85C50681D + B5876DAF7D6B48339D6FED36E459DBF579DACB16489BAD0329A5D7E5FBDE37C0 + DF4DC11DAC583BF5D90FB75BAB63566F0CEEE0559446D11AC57C8DE65BAD0414 + 4BDA57B7F66C3AFCA07283B55AB126038301C64064770D2F219574A8330CFCC5 + 19DC74C11257F345BA5F486A09375FA8FB86F98060DD5FCC9EAFFDFF625F0304 + 6E367791354D607053F59B4BCD66CDB3C633E6E8BE439A39CFEACF58600D80C0 + 53754F433DE16973742F335BF72BB375DF122070BD8953A529567FC25449ED78 + 343968DCA420F5EB8D0B4E61524767D2478F4D346692D5538B701007C89BAA19 + 3C2CB883E90389A91F5C2305808B2BC43D6D74E20C2645744C215D63F028DD37 + 2A9E006120306E60DA0C24A6BE30B5817B14970360E9552030A9A013F8FB5297 + 3EDEC7414CFA686A02037823008E0E62D6328EAEE108815F626D875E0E80D37A + A96DA236A480EE662F6601E0088163CCFB49EDDF179A0717704C030DCC8DF017 + D01B616F1A0AFBBA04160386A92B9C76FE06889B38801BB4B6E71AB5B3F23ABE + ACE6CB0175EBB6B2E7EAB5B24A6A2BD6696995D556AEAFE3EA03815B7B6AE8E8 + 002E4B5A68D243D76EE93582CBD66C6ECFE21AD65A40F0930070F9DACD338EE0 + B2359A5A99EAC1059C5684BFE56A30060017C1DFA006AEB413B85C7404038589 + BF5E5F02040728FC58BD92A4804EA06F36FCF5F9A45F0474738B3511FE46001C + 62B51CFAC63617004E83DFB4033802E00879D310F87100389DF639C2DF0C00AE + A0580E089C8E959222CC8DC0F771F037DB018CE25A54048081BFE8E50CEC4500 + E14CAC7C027CCB55B1524F48FB8C8AC1DCDF5384BD4F80C3D930977E49C5519C + FB232AF6DD7BD6F7EFC5F4A4B928AD29FEDDFF3F4381171483BDB99481BC3962 + E938E7CAC9571E6536B9D63C4EC58E2995C59E52F3C5D6FD8B958B93E552366B + 7BA25210F41F02C028FB987F44B9CE9FD61F5913F5F700E0344F8DBCB5984AFC + 7300B0C74B112FAB7FF800814B3E0320E549A59E40AE2713638F28D71FF4EFA1 + 5C2FD89C4ABDE1326FBC1C4038D71BE55FA5F41BFD496FFC3FF6BC383ED707E4 + 7F957491795A178DB49ED105471FFC69E53E36AAE803B0B87338C821B7D6155D + 5474E14CBB7DA3B220EE1FD51F01C01E2BAB710AEC46C5187037CE15C5524AE2 + 19000CC48D10582A01D8FD1B207004BCD9E36CA5017084C0D1E91B01708CE1DE + 75F0EB31CD570810D8816FA5E0EC05048731EEE0DA56FA85BA56FAF93A0E831D + 12037FD59606DE027C1DFA6A03F9423D6F4B54AEEB358251C9E7EBBA621D5FAF + 312C51C717A5FB5EDB3781BEB469005CEE156DB25F6D12E06F92023A3A7F637D + DF08725184C069977004BCB469D88B4A57D3FCEB3A4F027FCBD7D08D43CDE2CA + D4F2ADA51B0EAFE54B4D5F443F51EDB6BA7108A037C2DF6771F6E2E84DF501BE + 151A90D6B98395AF17FA951B7771556A446AE74E4562AC7848E7DC45374C49CA + E704FEBED04C376FCD7B39C80D2EDF6E8F80DD0880D32038BDE6C5D67DED4560 + AF44FB62EBFEF6629B0119F8FBBC841338E3E805EC26E0372ACE91CE19076F14 + E99AAB007913F88BD3F7952EC3EDB56E23AD2A8EDEC4F50BF07D355DD3B7EB70 + 9F7FA5AB6EA6BBEB86BBD71807BFCC93F29936387E47867ABEA4694EA02E2D0E + 5E802FFDE8008EF097B65ABFF19A274DF324DDB84FD48D39E3F101FC0E9A68D5 + 074FD68DFE6477EF5297F78DA1A15F63F8B4445333F57B49F15C6BC474AB9538 + 7A1DE28E9AE1FDD8126B00B09D383F3878338EDE0083A3EA8D9D634062D6349A + 34DF1A03712752BB7796C42FE8A9D5BB406D80BFCD662DB766B3978754CD5349 + D5BCC81DBC4D67ADB046D3973A0C6E3075B1C6CBADF9DC95D672FE2A6B019055 + 9F36AAF5C2006C01B7AD16AFB3966A23F08D299CDB2CDD60AD16694E7DC02F62 + CCFA0082577B5DDFB6CB363AE4A56DBD78BD8B6371FC7658B5D93AAD29B48E52 + 875505AE8E6BD4AED964ED57ADB3F6ABD7A9BFC13AAECDB34EEBF35D0E8181BF + 527BF53B6DDC6C5DF20AAD6B41A175DBBCC5BAE46B8C34EEBE659B75F33A6EA1 + C66EB7ED5BACFB8EADDE7673F8BBDD7AEEDA66BD766F0B60F620A903F3ADDBAE + 3CEB7360AB03E01EBBF38D14D07DF6157ADB7B6FA1F5C5E1BB47ADD47BF76617 + FD3EC412483CF0F00E1B7068BBFA3AF7C19D7EFEC107F6DA40D23A1FDC63030E + EFB37E07F77A4AE7BE8ABBF6EFB1DE07F6582FC57B1FD2FCD1839E6A79D0C923 + D2511B7C2A49BDECF0F798798DDC33276D90E23870079E3C6E43CE9CB21117A8 + 677BC946AA05F4027101B811E4468D387DCA86EA9891674EDBA833676C04B578 + B57ED8C96336FEE2791B718E2F72431DDE6167A9BF8B70F89EB2E1C0DF0B67BC + 762E2D31C0EF18806DE2CA1D29017D71EE0271A35B37C260203063EAEA8EBD76 + D14130A017D11FFDA6C6C412E03BF1D69B36499A70F38A8DBB49DA65D22F53B3 + F1CD8C70E5F2253EF57281BF7C49EFEE2EA0EE5DC0EE1DFFB23EA6089D762FD4 + 8F44D3B4760A35227102BFFB96CD486030F234CCB87A39978381EB36E51DA040 + 48CD3C1DE7EF7B80819B36F3FD34E00D4E5EC680DE986279B662386EE77EF8AE + F7A35B18288BA22B38A43E0500DFB2902EF51D9BFD01AEB63B6ADFB279D4DEFD + F0AE51A7178842CAD65057339D56351C472D4F20CE7C1CBEC05D52AAEAB118CF + FF38C0DF0552513AE7E0B2A32506900902127D600B1F7C688BBEF8C8963CFCD8 + A12F701930ECE03681C028CC0373890770B40818FCE5470E96D0D22F3F564CC7 + 931EFA536A970668BC98D4CF38171F724C484F0BA4420E87DDA1F8812DFFFABE + 04C80A706BC537C5D3E0463086223443B10F248B202D1E87F397F88A6FEE2BF6 + A1ADFA4EF3DFDE7747262D71FA0E8D49F58C74FC9AAF83DB17A7E73AE01F699F + BFFBD4F280803F7E6E21B5F1D70E7DF37EFC46F35FD8861F70FB92FEF973AF05 + 0C105CF71D0EE187B609D8F92BB57C718F064769DE9F422A636022501820BBF3 + CF3FD8B69FBFB2BCE4B1B6FFE56B3BF4BFFE6AFBFF973FD9B6BF5227F8810B00 + 9CFF0BE7F8CCB6FC8594CF5FBBAB9894C80104EB7C7A8EDB7EF9D276FCF96BDB + F1976F6D1BF037037683CBB81017F06F5AF3DFBE7717B003573DBF0889835BF9 + A1BB87B7FE8C1E06697EBB8E052AEFD4B901C131F573C605AC16709C81BE6A81 + C1FE1C23CC95D2E037B6110247F76D84C4717D9C8F7207F0CFD4EDD55AF5A909 + 9C86BD314E8C7E4CFFBC36D5E2000E6B3FD163E2D826EDF7870E7F63DA6E9E0B + 2D6098D70A607FE38F0FF4DAF83C0380D77CF399F4A9ADD27B825AC02BBF092E + 5E606EFAB51A01708CD346F09B7EADC7D7F44A3D5E84BEB4CBBFC1F5CBFB49EF + 35BDAF00C1C480BE0B755CFA87162B00C05FF37EE6FDFDAEBF7FF9D1465480BB + 405B806FF85107EBF8CC09BAE71098F73E10387C2E15D5070E5038A48A7708EC + 9F39454EE0087F51ECFBE7359FD1EFF1F9A7CF3369F6075AFB81CE21017EA9FD + 1BEBFFF2431F3EF3DD9DABEB43F81C0FA097CF571E976C07405E606F04C301FA + 168DC38F7D02008E1038A3A4DEF094BB80E87B6A4947CDF50618FCB68DBF458A + 679ECBDB0E7D27DCB9A7EBD4BB5AF7BEDA7B9ABF6B636F699DE67006B366F23B + 9ABBFB9EC63882DF76D7F0B0CB5C7BDED7F3FDC421F2B0376F4AB76DE4F53B36 + 0E60ACF38FA29EF0F59B36EAFA551B451AEA9BD41DBE6A23DE04F85216E1928D + B872D1865DBAA0F145F52F3B0876208C2BF8A2E6BC1EF0451B9CB88181C243CE + 2986BBF7EC791B80F3F7E43987C043A5411A038007530B58EB869DBD60034E9C + B6FEA7CED8C0D3676D80DA7EA4843EA93DCBE9D3DABF9C0E2E60D2421F3DEA2D + 7586A3FA1E39EEE9A5FB1C39E210D841F0A1030E7F7BECDBE72D6E604A5CF403 + 0EEFD5BE09D0BB7F9F97C1E8B37FBFF654FBB577230E04D65ACD53EEC2CB65EC + DBEF70B8FBCE007BBD36306E5F206F923EBAF3569CC180E33D1AEFB22EDB765A + F76DBBADDB963DD6B5709775DEBCD35DBE40DFAE5B776BAFB9DBDBEEDBF658CF + 1DFBB4DFDC1FDCC09B83AB9894D3A4956EA73D6A5B524AE7175AE7C26D2EFA6D + 37E67B4B5AE90E795BACFDA62DD6296FAB75CEDBA67DAED6A90F08469D366EB1 + 8E1B0AB50F2E7011EBB851C72AD641E38E6BB45F5EBFD93A6FD2F1126B5A6B6F + DD56FB6987C99A6BA735ED566F32D248F3634BE0310EE1E02426AD747013D3B6 + F01AC3ABBD9E70CB25BA4F48EA0ED3F7922D6AA937DC18E0BB60A9EE13A827BC + 48F701CB5CC0E0C67316588B054BACCDC265D60AA03C67A135F2BAC28BADD19C + 25D67826D9863456AC11AE61D73C6B22359E36CB9A4C9DA976A6B78D264FD73D + D454DD4B4D2992C600E20613A765C4B81EB0785C22E6278539770B273582A33B + 9854D19E265A3107C143C7584DAF1B3C4AEDA85067188D08B018080C24AE3628 + 805FA02FE2B81A83749F4A5A68C5A31338B346EDEB7D86E81E76B034C8155DC0 + EE0AEEA13EF581D38E60A92ACEDF040AC77ABF11F6B22ED60E06024787B043E2 + 4EBDFD982A9DFA5895F6C5217095763A472207C129E81B1DBFDEB6EC6A2FB6A1 + 4630FD007C01C0DE4608DC2C005F006F8C3F9FC0DE08812B6B4D060C376A5B0C + FE3AF005E2267220DCA4BD956FDCD6CA685CD61DBFADAD72BDD6F682DAE7EBB4 + F2F6853A5A5BBB8595AFD3DC01318A358101BFE56A69AEA6E66A522F38D40DF6 + 5AC119005C047B23F88D6357CD6629F01BD23D037D1DFE3AF86D9851E9D71B64 + F44CB5FAAE32EA0379A3CABCAE98E2715C4EC779DF534337B4675ED1B1191581 + DF08779FA91AEA0647114F03E0F4380D7DD3F1087ED320B8E44BB514ABED0018 + C53EA9A0A9059C0D7EA3B2416F80BC41D9203840E222D88B00C069377004BE69 + D89BAD087FA3D260371BF43E2E9E06C069851AC2C0DE978A2940E02AEEFE05EE + 3EFD6C557BA6FCCB5632057C81C1A5CA3D190A3F4E0E7873F4E3F8A9B22F5809 + 29C24DFA25F55C4A957FC9453FCED33E49018EAAE57BF7A8F8FDFB93FAD96BA5 + E2DFF507D3586CD37CE0F739415A293E9086BB6925FF0EE16FC9319F52B1F3E5 + E02CC5E6C9CE8A72AC2BA664CD1F396754AE75FF7C557001689FA45CDCEDB14A + 41D07F180047651FFBF728D779FF5E6500B0FEED5C391EEF91C74CF1D43473CD + A8C4DF067FA33838F63D9D33FD92655C0104F3A06A9F0EA9A0A31E7922E927F8 + 88B2FF987F1FE584BDB99402BF51FFA50038FD61F1473E34FE66F1A1C18747AE + 0FC9FF023DAD0FD752FAA0978A2070F10FF2F4077D6E05D72FFDF471F16F2BBA + B0709141FCC2497A12F4652E5BB9D6498F03C05199780AFA660B1730E99D9F2A + F39AF4AAC7D2F0D7C7713DB172DA4C90FA9994CA494A681FABFF14E3E774AE14 + EC2D12F12295AC08D005EC6AE3944321FD73510AE8270260D239BF50DB21307D + EAFE96AE5CC79EA95C5B1B2FA030BFE4C3210C246663A70DA0FA4FB943B89695 + 2165F3F3A47C61E3A78D2563AFEF8BCBB7BE5A49AD83606A043F9F0060F53300 + 38057B33D037A9EB9B5115C513009C81BD120018980BD44D835F14A16EE9D78A + 83E0A8CC7CA2B4D317957EA399954E523E07E00BEC4D84FB5731770247E04B2D + 5FF55105F51913C7D94BBA67170018E05BBF4331F0EB6AA01BA3469DAD52E32E + EA77D60D4F67DD0875D5B87BA8E94BBF490F77F6566CA258936E56A171371F57 + 02F26A4CBF7233D453375CBA216CDD57376BD4ECED65159BF74C002F0EE02040 + 2F2DF3AC8BE097E35E6AD34F1AA09B408916502BA5DDBE0E78510271AB90AAB9 + C3A08CF397B88361A0AFE65EEA1CA0AEA76DC6D1DB696870F9AA7DADFB487BBD + C76877F346C72F2D5017D0EB357D7B8CD48D2F6EDF91BA41A686EF584FED4C8D + 5F177D89F81B00DDFEE3ED35802F9017A94FBA661CBCEEE21D30DEEBF2C614CF + D1E9FBBA2BD4ECF5B58C074989CBD741EF88E0EC8DA99AA348D38CBB3702DE34 + EC8D636F2587BB805D1CC0E3E6585D1CBC0818EC9AED6DFD89F3ACC1A405D668 + CA426B326DB1359DBEC81A4E9E677527CEB17A93E65A43D2366B0E08DC48F32D + E6ACB4E6C05EF51B4E5D9471F4027823046E2C359B1D806FAB056BADF5A275D6 + 7AE13A6B23017E5B2BD60670BB74A3ABCD920DD67AC97A6BBE00201CD235B7D4 + B8EDB24D8A6FB056EAE3F20512B759A6F5CB3759ABA58A29DE1A87EFCA3CEBB0 + BA40DA6C1DD624D2B83DA0779DFA6BE9E7FBB8E3BA4269B3BB7A3B90D619E08B + 3406F476DAC4175C9B8D7478381F3AAC0FF12EF95BAD5BE136EB5AB8D5DD14B4 + 3DB76EB79EDB765877EABD156EB6CE5B365BB71D5BACC7CE6D09000E30B8E7AE + ADD66BF756EBB77FA7F53B40BAC0CDD67BEF6687B8FD49E1BCB7D0E12F6DEF3D + 9ADB53E0B09734CFB87C89F7D5BA41477659FF437ACCDD05D64FC70E39BEC786 + 9DD8EB1A726C8F0D38A4F9837BA490CA792020F7C841238521EAAB783FC5FB52 + 4B37037E8F1ACE5A4FB57C0AE07BDC6BECBA83F7D4091B7AF6B404003EE6F141 + 80DA7340D82B36912F7CD50E3D7BC6069F01D892D6F18C0DD378D899D3D2191B + AE7688CE3342B191E7CE2A76CA869ED6DA33276CFC958B36EAD2459DEFAC1F3B + F2E2798D2FA80DC017003C5CEDC88BCCAB2F0183C75CB9E0EDC84B67DD0D3CE2 + 5270033BFCD598B4CD005FEAF2466730E3F137AFB86B9754CDE35CA14F4DDF31 + D781BDD4DD0D357871FBE2EEA50663FCB21E971630388C93DA8D6F07C76EF872 + 1E98FB964DBB7BC7A6BE7D5B2D80F76D9B0540B87FCFE67E70CF412DB017F8BB + E093F76CC9830F6DE1A7F703B005124801C6723EDCBF376CDA7BC0849B0674A5 + C6E5CCFBB76D16694E49CDFC1E201808CC39832318000C7C9DF3C15D7F0C447F + 56028D99C391CBBA008049950AF878DBE67DACB98F439A66D237731C2DC0381E + 476A654FB7AC96FE5C407072ACA75FC6550758491DC3DF189FCB828FDFD5FCBD + 648C339835F7147B57C7E2DAC5091C1CBA211D2CF0F6135B9A40E0A50F3FF136 + 8A31F30E8171F5266077E943CD7DC9BCCE917105930AFA7D5BE87DE05470F892 + 9236D6FF8DA27E2990CADDBEDF047815A15600C000DDE0908C02862DFF92C70E + 403803D5B43E02B500D700BF01BEADFAF6535BFDDDA7B686FAACDF7FEC6B7152 + BA03F85BD606704C3DD735DF7DE4A99F5748A47DC6F5496C95CEB5F6DB8FDD05 + 8C1BD8EBC0FEF4856DFCE1816DFAF1610280BF70E0BB468FB9F69B4FDC19BA5E + 635FA3F5F9BF3CF474CB0E61A5E88E058C46A89CFFD343CBFBFE33DB24E10006 + A2EEF9CF1F6DCF7FFFC9D328037C0B7F7B68DBFEFAB5836020F2161CB97FFDC6 + 366B6D81CE8F23967881CE8963971AC09C6733313D8F2DBF7D297DA5F3710EAD + FFE5810364CE59E8AE5DD67CE5E99F6971DA3AD4FDE5AB22002C395CD663A3E8 + F8453115342DE78BCE5F5CC09CCFCF898337D1163D3602EA0279A3CBD6617402 + 8069633CC2DF08CF231CDEA076237D00B0CE0BF0CDD33A6AF80282F3221006F6 + 4A1B12304C1AE8D53FE8FF96D8CFD469E6FFFD236F114EE000E999D3EBE15BBD + AEF4FAC12DBEFEFB4FF4FF1F00F746699DFEDF8223387103F3FA03DE665E9721 + F533358369630D61E62218F6782207BFC9312BF45AF5B4E89EF619772F3F9EE0 + 3D742FF37EA26EF652FF6185DE7B7A6FD1E2CC5FE6EFA3007B8B9CBF005DC601 + 00CF27D3803E33685148214F268250E73782E334184E03E05043F8BE835F202F + 0A3F3E0963DAF8C394383F9BD4CFEFBFE3B077963EC3677F187EB032D3D3E2EB + B3FC1E3FFCD1E7BCD6F0D9EFAEE0C401EC35D1F9CCD5E73929A4C3B9DFB7191C + E77097B9F8E39B987A3FD409E67A0340E61CD312E730FDA9F780BD9AD73566DA + 3D3DCE3B6F6BADE2B4BADE907522B881A5B780C04153EFBEEB9A224DBE0B147E + 47D7B6BB5A437A6862388271FCBEADEBDE6D5D3BAFE9F13ED067FAA7BA2EDEB1 + 1157AFD9C81B376D9434FAE62D1BAD16E03BE6C6751B794DD7D0EB64C32005F4 + 251B7699120ABAB65FBDECE311BA5E038189A1916F521B38B88381C2433537F4 + 020A1078F845C5491D7DE1920DC2097CF69CF617E7B4AF386F43D5520778F069 + F604175D0313E83B50FB8AC1DA7F780A69ED53D060C506497DB59FE973ECA8B7 + FD53EAA758287371547BA3239E1EBAFFB150D6A2CF21ED938E50FA4252DB9FFE + 41ED9F34D757734060406FBFC35A7B60AFF5DCB7CB7F5CD7476B10F3FD298FA1 + 35B88401BEB43D7607F0DB653B20180180773BFCC509DC63D76EED19F7588F1D + 7BACFB76602F6EE05DDA4BE23ADE6F5D35D77D37005AE7DD7BC0BAEDDC635DB4 + 8674D28065524E9366BA43217585B59FDD4C569AA0F6F99BA48DEA6B5F4B0C57 + 30CAD7FE552D7BDB2E9B0BAD533E0E611D2BE112F69AC1EC97C984935F685DF3 + B768AFCCFE9AD228F95AAF73C4B552A7BC70AE10D3B1EBF8312529A843BFCD6A + EDF35751679874D1DAD76B1C63A82DB1243B0F6DABE5AB5D9E727AF95A6BBE78 + 99F6FF2B5C4D17E8BE64F172770B3799AF7B12A9A5E6DB2E5DA97B0BDD9B2C5C + 624DE62DB446F31659635CC3F39748EA2BD664CE7C6B3A778135D7B885E69B69 + DC6CD65C6B327B8E359D39DB9A4C9FA57BA319BA279AE64097B6217585274DB5 + 7AEAD79F323D48FDBA13A758ED7193749F35D99DC40D146FA863EB4E989C4911 + 5D6BCC04DD8385FAC2B5464DB0EA23C7EA9E6EACBB8001BD35490B3D7C8CD556 + BCD6A871BABF0B8018000C208E3584A9138C6AE3261E32DA816FF541D40F9606 + 8EF0FEEB9E367A9855EB37D4DEE83BCC050C7637B0444D60C6AFF51A948955EB + 3DD8AAF51A6CAFF4C0FD4BFDE0BEF662575242F7753770D5EEFD031CD6D89DC1 + 92BB865330D8C1B1FA553A688E54D0893C3574875EA18D105872D80BFC6DD5C5 + A16F65D4A6AB556CD5D92AB50CE0B7280D7400C011ECD2328EF581A30BF83989 + FABF9558D75CB1A61A038689376E6BCF356A230528ECF0577D3F4E73E501C452 + C5066DADB2F47C8336EEFE7DBE5E6B6F9FADD3D29EF5F4CE2900ECB0B7A59567 + 2E11C0B74C9D66C59471014B80DF67759C03608D710497AED138930ADA854B18 + A5E06FD9EA8DAC5C8D466A1F05C11100037D5DAFD5B3D2521C47085CEE759CC2 + 3ADED345231D9F720047C5FAC19E1A3A152F82BC51C5A1F0335503088E738F02 + E0E0FE7D3A81BEB87F4B018501BE52992AB51DFE6603E0088123E84D2B17008E + 3580A3B28170495242570800383B1D745A1907304AE0EE23FD44D9D097F974BF + 982AD256B5E20EE09733A2FE6F89F22FDBD3CF55B552CFF21D32DF2707E8EB00 + 583114C700DCD87F9CD2B0174567719C0B00F879CBC04EFAE55ED0F378D14A02 + A781D4EA138B734F5231088CE3B874D28FE2FBF8F438C672C4B3BFD72FFEDD7F + 91D26B7E5F308F2265035D571A00C77F1729FB98F47932733959CB3F57FFAAC7 + CC70AE1C73AEC8D172C1CBB452BCED77F5B8631F17FF23CA3EF6EF51AEF3FEDD + 4A320A97503F9E3BFBF1623CA3144F4D73D78C00B809C8FD7B9506C0254B9555 + 1B5CC038804B95A9E8ADF74B03457982FFFE80F7F704DC4DB7B19F56C9672A3E + 128B8ABF82C8F906F9672A0D8013FDA1377E721C05CA9FB82E42D294F8804CF7 + FF95CA7EDC5C4A7FB8677FE8A3EC0B86035E97FA00E6CC9C2E7469F76F0AD8E6 + 146BFEC0BA34EC7D9C32EB13B0EB2037239CBFDA3C50DF576D00C0C10D9C5E1B + C72580C50E7FB5BE7C71952CAF0DCFB3DAF83CA7CDD073AFDB53594A435C87B6 + 405C606E056DA672A854B22E1EE76EDE0ADAC8554C49C73F1D533F272A82C0B5 + 1D04D32F51B9863D2551CF9796DABDD4F58DF57D9FAE02F0A59EAFE212EDD3C9 + 1A6030B57CBD9EAFA77B0E703800628DDDE91BDCBFD90A1038B87BA3C3F7E9AA + 0D8348EB2C45276F04C069A80BCC659C71FD32F7BAE2D5B5D97F439BFCD79BDB + 33AC51AC346E5FC49CA7796EE96D69B5CF48A56BB6B232B55A17415CD2383BD4 + 6DAD563721F5426DDE50DBB7B8D31765E209FCF5B5C413C5B4CE419DAC7293EE + 56A161572B570FD72F8EDE5EBA09EA6EE51BE0F2ED69959BF5B6E79AF6D44D10 + D0B7876EA670F96A2C556ADEDB2A326ED6D32AAA5FA9651F4FD98CE8574AFACF + B7EE57CCC91B63005EEAF73ADC4DE65E54CC216F027433505702DED23AEC2565 + 73E7A27ABD2F741C642F45F02B315785BABD5204C019472FEEDF948B17B72FE9 + 9D11E99B71F3A2D748E19CA472CE16AE5E5CBC11EE027BABF61BEB7AA5FF387B + 75C0F84CFBC6A049567DF0447B63085077A2BD3690F8586FAB0D1AEF6BA8E5EB + 699C538EDE285CBD80DC087733622CD51835DD6A304E402FC017572F8AEB6AE3 + F41D17523907F83B37D4ED9D48BD5EB593A8D51B9CBDF5A6A285AE06D31659A3 + E90BADD194F90E819B68AEC9B4856A176A1C0070D319CBACF18C2501FECE5CEA + 4018473020B8F9DC55D66CCE2AC571092B3E6FB5B504FA2EC6C9BBDE5A2F5C6F + 6D176DB0366ADB2C586FED176F9436591BC558D31228BC3CCFDAAF2CB056CB36 + 5ACBE59BAC85DA562BF3ACF52A89F4CDAB484F87CB20DF5AAFCEB336ABC3B843 + E242E898B7C53A6C2CB4761B0AACEDC6CDD67653BED7370B5F5EF185D4166B07 + E0CDD3FA42D2E8EDB28E05DBADBD8EED80F3215FC76F0EEA98CFF9C217645DB6 + 6CB750936D9B75C269A1710F8D7B6DDD663DB76DB76EDB705F145AD79D5BACDB + 4E52FA6DB11EBB3487F66CB1DE7BB75A6F5A5CBCFBB658FF83DBACDFC1ADD67F + FF161B280D02F02A36E8E0761B7060AB0D700770A8F13BE0D00E235D74FFC33B + 6CE0D19D36E048D0C0A3BB6CF0F15D36FCD43E1B7A1CC7EF0EEB7F68B7F507EE + 9E386AFD4FF105E721EB9540DF81278E64202FADA75B3E7BD2D32D7B4C628C70 + FE0E3903743D6BC3CEE1A40930781835FBA49197AFDA38DC3F6F5E73C70E5FDC + E2E62195236B869FBF6023DCDD73D60671FC39DCB8D4E1A5AFF39CC7CD7BCE86 + 5FBCA0E32ED8A8CB176DCC9B976DEC556AEB5E76883BF28A1EE7D2799DE7ACA7 + 811E7DF9BC8D7D53EBAE2040F159B58AE1E0BD02183EEF6E5E9CBEB87971F802 + 8147320FD005EEDEB9E15F548F49BEB01E7BFBBA8DBE01F00D730E75EF9246F3 + 6AA89D8B3BF7EE2D17299A27DEB9EED0770A693CDF262D67A8D738ED9D3B36EB + FEBB36EBBD776CE6DDB7A5B7BC9D214DBB73C766BC7DC7E6BDFF8ECDBB7FCF66 + E1D87DEF6D079D6B7FF8C2567CFD8943D459F7EF783BFB23C0016EE0376DFA7B + D76DD647B715D3F93FBC6D333FB8A57E70DFCEB98FC356E7D27133DEC315AC75 + 1FE0DABD63733E7C5BC2BD7BD7C16CE8BF13CEFFC1DB01B87A6D5EC53E0C8017 + 984B2DDD006DDFB3851FBD6FF33F50EC03CD7DF8AE2DF808BD678B3E79DF967C + F6812DFC58E30FDFD1F85D5BFCA9E21FDF93DEF5F9851FEB7820F0FB6FDBFC0F + EFFA9A057ADEF3F53C167DF2962DFEECAE2DF9FCAE5AFD3B7CFAB68F973E78C7 + 963D78CF967EFE9E2DFE5CE7440FF4589F7FA8B98F6CC5979FDA869FBEB1FC5F + BEB735DF7D614BBFF8C8967DF1B1AD547CD5579FAAFD44C77F602B1E7EE42E47 + DCAE40AEE55FDC4FA4B9AF70D97EEA6967636A5A5A4015025E85FABE218D6D8C + C731502BADA529F80BEC8D02FE521B7889CEE70EC96F3F2856677519E9A8794E + 5F01801FD8EA6FBFB495DFD07EAED8270EAC577E0D14FE4CE30FF55AE13CA488 + 26552EAE4CEAB0520FF633E9135BF5BDFE0DBED7F847F57FF8C4D6FEA4BFFFA7 + CFF5FA020E7E619B7EFCC23650EB17F0ABBF7DE30F9F39D0A5B62FFD0D3A6F84 + BC857F7AE8DAAC6336FFF44548F72CE10EA68EED3A9D738394AFF3E3E2DDF2EB + D70E4D81C5804607B27FFE26C0639D13B01AE16F9E5A4F75AC6371E8EEA13EEF + 5F34F7D3A7B651FF3E0549CAE8E824268DF4E65FF5388A6DD571680B40F8E707 + B69354CEBF06705CA8E7B5EDE790023AAA18044E1CBF0E8A53EE5F9E23CF8FE7 + CCDF401D60EA136F4E9CBE11E846E76F84BAD1ED1B817084BC713D63C0707007 + E32AD6DA9FF577FE0248062C7FED72209E9C23425C14A1F2C6C4F5EB2E60FDDF + BA03F8878F5C1B7FD2983AC189DB17F8CB313882D7E8F5467CFD771F17FBFF06 + 066F206D383F16D06B719DE6D7AA7567B9D6AFD3FFDF7A07C5C0611CE73CD6A7 + 1E8FC037DDC6D7B3BFA6BF7EDF167F794FD26B5FE2FDE28E5FDE67C9FBC1DF13 + FCA80247F0437E2811C498F7033FD8582691023AB87DF9314892AA5DEF775ACF + 30F06900B55E2BFC93E2A03782DF0883F9ACA40528935D20BA7E01B231FD3350 + 37D6070E75C5838847677058031C0ED016403CEFE3F71D0AD39FF93E9FE10063 + 406DC80A310B88ACE366E87374A634E7E3FB8ABDAFCFF7776CAAAE03D3DEBBEB + 4098F33B507630ACCF79F5793CCE4B8CEBD2349D2FA69126557F700E07681CD7 + C472043CFEB47B77740DB9A371C84C31D5617511408E7D448AE989122DD7415C + CC5334CF78FC2D7EF874C3C6EBDCD4B51F7713B7AFAE9FB774ADD675158DD7F5 + 921F5051537FC49BE7FC9A3B922C1B9E69E392AEE508F8CB0FB380BFD1158C43 + 587B8A8BE76C88AEF3F44769FDE86B5714BB6003A80BCC0FCBD807F85E20A68C + 3EA396FD88F6156748FF7CCCEBFE93F163D0B913DECF8CCF9EB0DEC70E5A5FED + 7F062A36E8F451D7C05387B5273A14B29F9C38ACBD94FA127BA3811AF7D7DE69 + E8194A5D1CF73E1AA0BD55ECF73AB4D7FA686F15C607ADF7A13D1AEFD55E6B9F + F5D8BFCB7AEEDB697D0FE212A63EF00E6F7B539AE3002EE15DD67DF736EBBE6B + ABF550DB6397F6973BB66ABC5D73ACDFE5EAB967A775DFB9CDFB00E59E07F759 + B7436A0F1FB09E6ABB2ADE79E776EBB2639BF69D9C6F9BCE53689DB76DB62E5B + 0BBC1449E7ADF9DA9BE65BA72D1BB4AF5DAB76BDC61BD54A851BADF3964D5A9B + E76BBA6D2F7085F5DA8B176CB0B679EBAD1DCADFA0BD6F9E75DBB2D9BA6ED1B9 + 0B3659874D3A67FE46EBB8597D87CB9BB45FD65EBBA0407B6549EB3A6FD173D0 + 711DB5AEDD469D67E35AEDD1D75AFB0D6BB4375FADBDF92A6BBB6EB9B4C2DB36 + 6B5748ABB5C75FA356F70FABD758CB952BADC58A15D65A6DDBD500E2D5D676A5 + 8E935A2E5BAEFB85E5D672E93217FD56CB578476A9E2CB746FB274B1355EA4FB + 99C50BACD9D2855AB7C49A2F5E64CD162EB4E68B70102FB6E6EA379B37DFA170 + E33973ADF1EC39D678E66C6B346396EE7D661453DDE9BACF9A36CDEA4C9B6EF5 + A6CFB4863375CFA57ECD4993ADD6E4295677CA34ABAF750D756CBDA9D3AD3EC0 + 78E214AB336192D51C3BC16A8D9B6835C7E91E700C3584C727B0776C46D5878F + B61A23C6D81BD407A67630EE6040F0B031BA77A44EF04807C68063E070B54123 + 74FF38C69DC5B546E85E75C8487B7590EE7F070EB56A83876B0D507884EE5F87 + 257584875875F5DFE8AFF9BE83EDB53E835CAF2B1EC7AFF61AE03584A9255CC5 + 6B096BDC536DF70101F826A9A2E9D3027EA36B9858AC0D4C7AE8748A68AF15AC + FECB9DFA58D5CE7D5D2F116FD7DDF5BC54B14D17ABD0BA8B55C205DCA6ABBDD0 + A6B85B38D4060ECE5FE02FAADCAAB3556CD1D19E6BD6DE9E6DDACE9E555B41E3 + 8A2D3B5985E61D3C4EEB730061F52BEB1C15D57FAE49DBE01A6EDAC19EA75670 + E216AE9838863D5D74E21AAED0A84D88AB4F8DDF28D23D47312E57BF85D4DCCA + D66B564CE5EA4AF59A8736A532B59BD833351BB9CA48656B35B1D28A95A9D9D8 + CA12ABDED8CAD5686CE56B3671D18F5038AA346D02811DF6562BEAA334088E7D + 04248E2AF3EAE355FA95BAC5F44CD53A2EFA4FD34FE2D1D1CB313C0762EEF04D + D23D33EFC7BD5CC7533E97C0D1FB420D07C06512005CE6C55A39017084C069F0 + 9B8E1577071707BF8FC060406DA26C009CCB191CD3433F49E9B511FC96909EA2 + CD5A5BA2A262155FB6A72AA0AAAE1215833C06F4F55AC1C97992D4D14F95274E + FA68E2C5D348D38F2A9103F8027A81C6112AC7B6180006DE46089C86B9E55F74 + A56301183F410934F594D0C9F933E9A1D3DFD5A7F58C8E4159E3F8DD7F14DFF9 + 97D279D22296BDEEC982373CAA0875338AF037A3DCC7E5526EDEF2EFAF5C26C7 + 624A78D9EF292737CBB12E27047D92729D03E55AFB8F2AD7E3E452AE631E777C + AE75D97AD25C46E5ECA934C8FD7B050076085CB2AC4E58DAF54CD98A56F1F957 + ACD20BAF5AE9727A4167B980439B3CC97F13E57AC13D49BF777C2E005C047ED3 + 7AF44DF44F537CD3A562D1F19AF383260B188735451F7C69E0FA47943EF65FA1 + BFF571321709C6FAD07700CC98360D787F4F29809B01BD51E9B93FA05CC0375B + E9B5B87D3360B7B4E6CAE8225F569B87040247101C5CC128F44B94AB1ACE45EB + 8E5FAD75077035C574BC8E2B555E1B9D67B5117A2EC821708537B4112982C011 + FE66A4F9A7B53642DF672A6A73852B371903874B007FFD7CACD7A62B05809F26 + B5730AFE46001C20B036805284BC11F8A6C70E8213E8EBE0D77F2D185A207009 + CEA33538819FA902080E726770127B1A30EC2A82BE41F4B5F17DA56980BFAF16 + 09985BEA9546AE0876A3B2DDBCB4CF546B624F4B805EE06F48DBDC427D6AF7E2 + F20DFD98CAD945AD5EB56513E81BE16F80BE6D1CE6A232B880EBB471805BAE5E + 87104BCD97C3FD9B02BDB87E81BCCFD6471D741C2E605ADDF0786AE7C4F5DBB0 + 9B6E6600BEDDDCF1FB7CF33E0E78CB37521CE0DBBC7700BE4D48DDDC4B37587D + 02F8C5C9DBB28FB7B9FA11F22200EF4B11DCB60BA99A2B037C13B8CB98F4CE95 + 882573E974CFB87D5FEA10402F757D2BB70B003882DE087863DF6BF55293B7DB + F00C0406F2A641AFC35E8D330018B72FE99C7B8E0AEEDEBE63439D5E89F4CD80 + 6014DDBDA15E2F8EDE890E805FED3F2E037CA9D5FB9AC418551B04DC9DE00018 + 551B385E37E23ACFE0F15A8726F831EEF61D36D56A0E9F9611EEDF37249CBF11 + 00679CC08CC7CCB41A52F5D133ACC6D899562B01BF9EEA59F2F4CE52BD7173AD + 9EC67500BE1300BEF313F8AB3593E75BBD290BAC41027CEB4E5F6475A62D74D5 + 931A4C5B600DA74A53E65B63B58D157328EC2D807889A77F6E3863B1D7F7C5E5 + 0BFC6D3C73A9359BBFDAA16FE3D92BACC99C95D674EE2A6BB160AD43605A0404 + 6EBD609DB59E1FFA00E1568B3704A7EF52EAF5E65BBB5505D662F9466B2EB500 + FAAE2EB0B66B0BADCD9ACDAEB66B375BEB75F40B3497EFE30E1BB7589782EDD6 + 297F9BA7B46B070826C51D30377FB301713B6FD9E675D2488DD79EF1D65DD675 + DB5EEB54B8CB3A6EDE216DB78E85A4CFD33912084C7D36EAAAE1AEE8B24DE707 + 004B9D1527FD738FAD5BADC7F658E36DBBF5DCB3C3C12F5FC4F5DABBC3FA1ED8 + 29D14AFBB7495B1DE87AFAE6C3DB6CE0916D36E8D0561B280D3A2C1DDA668335 + 37446B061CD8E68EDF01477658BF23DBADDFD1ED36E8F82E1B72628F0D3AB6CB + 061EDD61838FEDB0612789050D3EB1377CA979FAB80D3873C2FA9F3A6AFD4E1C + F62F3CC39799A1BE6EACB14B5A651F2720383A6D81BDC3CF0557EE880BB86F88 + 9FB35197AED8884B576DE4E56B36E6EA4D1BA176E879DC3A576CD495ABAEB1D7 + 6E68EEBAF747BAFBE7BC74CE865E3C6B432E509B973ABC676CF8256AF29E55FF + 9CCE431DDEF065312DF097F911A47F3E7FC646E9D889D7AFD8B8AB01FE8E7310 + 7CC1C65C3EA7F865D7B837CFDBF86B176CFC75CDAB05068F42D72E06E9F83137 + A95978D546DF78D3C648A36FAABD7DCD26DE097516A9E1EBAE5E202FE99DA9DD + 7BEB9A4DBA75DD812FE99B69A7DE4DD2360388717BBDFBB6CD7EFF9ECD7DEF9E + CDBEF78ECD7A1B00FC96FA776D4ED4BB6FD97CAD990328B877C7E6DC0FAE35D2 + 977A4DDF776F1A299749B7EC4EDFF76FAA7FC7E67D7AD7E67EF2760600CFFCE0 + B6430DD23BBB73186770027FE70287B58E76EE4780E0A0791FBFE51011D08243 + 16A7EEE2CF3F70888A0B19F81BDCBFEFB83317D7EEDCF7DFB1259F7E681B7F20 + 85F057B6F8930F6CF1C7E8BE2D02100385F51C167FFC9EC6F7D4BFEBEDE24FF4 + 189A0718CF7E4FCFE9FE5BEAEBDC3C3F3DFF051FDF7108BCF0933BAE05D2A24F + DF4A00F0BB6AEFD9523D574030CF77F167F71D38037A0B7EF9CEB6FDF5274F67 + BCFC8B0F1DF652CB9416F84B8CFAA621CD71487B8BEB15F8E9EED76F43ED5D6A + ED46376F04BB5EE31768E57037C67009E30ED6DFA5363818F5BCBC0E307DADFB + 2A00DDE800C6B9CBF1D40A5E262D579F54CEF4C31A80D9C7B6F29BCF6CF5B75F + D8AA6F1EEA6F7EA0BFF7135BFEE0635BA5BF73CD370F6C9DFE86D55F7F6ACBF5 + B72DF9F2430765CBF5380ECC1E507BF5435BA9BF67F5F79FDA4AFD4D2BF537AD + 04CE01047FFCC4D6010B7FFA3CA4494EE0DF46DCC13F7CE6E037D6D17508AAF9 + CD40DF9F8AA06FFE0FB87403FCA5CF7936FEF2D0D6FF4C4DE0CF836B567D5231 + 6F96381F629D3B89FDDCC05552353F7407EA463DCEC65FBE701730CEDF7DFFED + 0787BA797ABE9BBEFFC8F2FFF4690600037FB7FCF6D0B6FC9974CF7A9C9F3FB7 + AD3A76CB9F3EB7C29F3E73084C3FFFFB8F350E6EE26DBF841AC011006FD1DFB4 + 4D7D1CC0D1F90B00266574DAF10BF88D2AFC2DB89401B3280D8023F48DFD087F + 23F88DF1988A7993CFF1FC75DE5FBF95BED1BF59485D1DD36DE3BE0EC038F423 + 0076252EE0284F0D9DD4FCDDA07F4F206FAC0B4D9CC7648E3EE7C1DD0CE85DFB + CD470E7D01C2EE06FE01F77784C0387E01C6C151CE7901C09C13084C9F38AEE0 + 007BF9E1022AFAF183FF00C27F24C1EB5DEF43DE437ACD93161A58CC3CF077D1 + E7FA4C7100AC79BD96790D23C6FCD803B73E3F8AC0099C06C0EEF48F2E7FF517 + 7E1E403029DD11B037D4FFE5734C9F457CBE7D012806A2BEADF83B19000CF48D + F57F390F3019E81BDDC5010007101CD2DB9321216449E0B33A3A8F01B49C0790 + 1C606D481FED003871ECCE01186B0EE03BED3D3EC3030406FC4ED1350031C771 + 40E098029A632380A60FE0052807DDD15AA03169F983C33802618E0DC7E31E0E + 6B638C3E9A928C03400EA50CC86A11C59879FAFC108AAC17C410E39049232991 + 208D95C66B0DED28AEBBD701B85CC7B906871F63918183720B23B9B62710987E + 70045F74208C53388C75ECD5CB0E8507EBDA4F8C1F688D9346696EE8057E4816 + F611432F008143C98891DA53900D843DCF20ED75866B4C66904167B51702FA9E + D53EE71C7B9DA336F8CC111B78EA90F64A076DD0C94336441A7AEAB074C4F749 + 40E1FEC71507220393D54F0B40DCF7083FAC0300934DE5A0FA7B12EDB5DE0777 + 5B9FC37BACFF5100F04EEBB357FBB703BBB427DCAE313078A7F77BEF636FB8CB + 638060F68F8CD93B0289FBECDFA5963DA4CE7B78BF83DF1E470F5A8F6307ACCB + 81DDD645EBBB695D8FBD3B1D1EF7D8A57DE94E32CF145A0FB55DB6E559D7ED79 + D66D479EF6B0EBADD3D675D675C7062F3BD26DC726CD6DF0B91EBB36EBF8CD6A + 0B3C434D0FA9EB4EE0F146EB50B8CE4171972DF9DA2F07A0DB7133E058E7D6B8 + DB76AD4DE200DFAEDB0AADCBD600A0BB6C052A87F94E8500E2F5D63E6FBD752E + D8601DD5EF98BFD63AE4ADB636EB974ACBADED8695D67EE32AEBB0699D5AAD5D + BF41FB7BDD2BAC5E65AD57ADF27EC70D1BACC33A9CC18AAD58E930B8FDEA35D6 + 6E7580C2AD97AFB0365ADB4E71D46A85EE5396E97E65D9226BB67CA1B55CB544 + E75AA6754BADC5D245D662C9226BBE78A1355D38DF9A2CD03DCFDC79D610F81B + 356BB6359A392BA3C6B3E6588359B3ACCE8C00821BCC9CEDC73498ADFBAFE9D3 + 3D565F020A3755BC918E6F387DA6EEAD66589D4953ACE6F889566BC224AB3379 + AAD59E38D9EAE0081E3B51F76ED2B8D0D61839CEAA8F18238D750182710A671C + C1C342FD6062D5868C74485C77EC246B3C49CF67BCEE21472A3E54F7BC4346B8 + DE50FF8DC1D4111E1E349054D2D4081EAAFBDCC1BAC70D7A4D02022320F02B7D + 8887BAC2557A0EB0AABD067A6DE01781BFDDFAD9CB3DFADB4B6AAB74EDE7CEE0 + 2A38833BF796FA781B9DC10E8481C01E0F63D248730C29A501C4110EA3E7DBF7 + 705596620AE9E7DB76B7E7DB74B3CAADBB5AA504F846F8EBAE600030B0B74547 + A943A28E5A1BE2CFE104061003819BB5F718C74438ECB5839BB4B7CA5204C028 + A6888EC235CCBC0360602FD03781C3C520708396AE008283CAD66B6E6580BD11 + 04A754B64E532B53BBB19546B56801C0A15F56FDF2B59BDAB3B582622DE0E802 + CE00E0D71B0427B0FA652577FB264A83E0A834044EABEC6B4111FAA621705A11 + 00037F4BBE5CCB4A55ADED718E89A037AEC3E58B62DC21709522017F4BBD50C3 + 95D3FDABF1D309E8450E79E92773110247005C3201C019B76F2E002C792AE84A + 493DE0C780DE187FDC7CB680BD19016C133D768D949ECB5E13C16FB622140E10 + F85100EC4A81DD6C00ECF0B74200C8EE36665D0AEEBA7B370D73FF2E3D6F387F + 39777C1E19B923F80F88EFF8A568F44AEB7F14002E21957CC2F16915E332FF17 + 51C6FD9B06BE49DAE2CC388B9B3D4ED9ECCCF9597A4D4EA8F96FA6F4F37D92B2 + 8F498FD3F1F45CAE63FFC85C5AB980EEDFA352A59EB592B87A4B3C634F3DF58C + 952957C95E7CF90D7BA96A752B5D5E2F7EAD8138FFBB3B80B35F7069889B0D74 + 7FEFD85CFA7700C04FFC9049C15F14D6A53FF8F47F9902BCBFA7EC63FFD97AD2 + E3647FA867E0AFE2B9A50B4D4E5579340690CD06BE719C8EFD01A541EFE3546C + 6DD9007C01BF1100A75DC0457AD54A957FCDA1AE43E05853D801B03624D9EEDF + C4F98B00B7805DC06D04BDD900D8A12EF3AC7FAEC8F5EB0018C8CB39D2EB1407 + FEA601B0F72B017D03ECA5A66F5AA59E0F9037037F5F2C82BEB877BDBE2F2DA0 + 97B904027B2C5170FAB2594CD2414BE9D4329E7E26E5FE8D699FA3E3B74CCADD + 8BBC5E6F0AF666435F806FB63C85734A319533B0372D606FF95A6DECD9DABA89 + A05E2F003701BED1C11B416E84BCAE5A3880758351AFA33D5BAF93E683B337B8 + 7C01BB1D332A57378C49ED5CA16117DDB074D54D8ADA06A47BD64D4F52D3F7B9 + 868A37E866159AE2E20DCE5E206F74F6A2CA2DFAB8885552FB7CE2EE05F40277 + 01BDF433AEDF04FE46B9D3B76D00C069476F0602ABCDF41F99633CD8AA741CEA + 0018F88B00BF69E01BC16E84BF0E757B8ECA406084DBD7D33AA7403069A0AB25 + E99C1DFCA6DCBDC05FAFD79BC0DE38CE387ED57F432D10D841B0FA35064FB65A + 43A75A4D5CBC43263BD4C5D5EBD07790D6008107AB3F58C70CD179864E72554F + B97D81BED4F275004C2B91E2D9D33C8F0A2E60443A6777F27AEA66EAF3CE76D5 + A646AFC611007B2D5FA9C184F946ED5F802F4E5F6AF7E2F6AD3F6581C3DF58BB + B7FE8CC5AE7A891A48C0DD46331759C31968A1C60BADC9ECA5D67CEE726B3267 + A9D7D16A306BA9359EA3F1DC15D66CC12AEF236AF7365BB8DA1A53CB773E5AA5 + F11A6BBE686DA6C5E9EB8EE04CADDE90D2B9B5D466659EB55D956F6D561558AB + D5F9D64AFDD66B365BEBF55BACDDFAAD455AB7C5636DD6165ADB7585D67EC356 + EB94B723D44A2BD861387A3B17EEB42E5B433D35EAA275D91E5A402E1098F478 + 1DD576DEBEC33A694DE7EDBBACEB8E3D9E4EAFEB0E9D67DB56C500B9A4E4D3DC + F6EDD675DB3623755FF7A4765B8F1DDBADA7E67BEDDE653DF7EEB25EFB77A71C + 1F3BADCF0120ED5E698FF53B9880E083DBAD9FC3DF205CBD030E017AB768AE50 + C714A8BFD5861CD965438FEEB6C1C7763BF01D786C97F53FA2B5D290E3BB6CD8 + C93D36F4D80EADD96A434F0081F70427F0C9FD36E8C4011B70EA880DC6CD7BFE + 940D397BDC869D3D61C3CF9DF4FABAD1E50BE81D79E18CA75BC6611B632842D7 + 31972ED8A80B4058E2676CF465BEC805ECBE69A3AE027CDFB4919781BF6FDAD8 + 6BB882AFD9E43BB76DFCCD1BEEFA71272F2EDC2BE76DC4E573FE85ECB04B3AFF + 95B38A037EA54BEA7B3AE7F301EA4AA3580F0066FEE2197F9EE3710691E6F9C2 + 291BA37634E99D75BEF17CC9ACC719ADFED82B67D43FA396FE398FE3101EADE7 + 402A681C49A476467C51EDB57FA9FB7BE79A4DBA7DD526027B6F5FB3696FDFB4 + 696FDDB0A96F5DB769D474BC7DDD262A8E0B98FABDD3EFDDF134CEA4519E7DFF + 1D9BAE3E357C67BDA398FAD3D507F8AE7AF8A915FCE97B5BFFED437700CF79F7 + 6D07AB7380013AC7349CC4D47B7C879AC077FC7CA1AEEF5BE6E997DDB51BD233 + 037E67BCAFB5EFDDB019F701BE7A9C7701D08ADD53ECBDEB0E8DA7BF1BDA99EF + 5F57EC9A6BD6FD1B3ACFDB0E54973E24B5677003931679C9838F1C0A03811110 + 660129563FB8676BBF796807FEB7FFC976FF4F7FB5E50F3EB1459FDC7710BCF4 + 53E9F30F1DCC2EFB4CEB3F78C7DDC0CB3EBBAFF87D5B0028B9FF96CD0568BF0F + F87DCB01F0FC8FDE72003CEFA3DB19CDF9F096B70B3F79CB167D7A577AC7167F + 7ACF167D868BEF3D5BFA80C7BAEF9097DAB5387BE9E33A06FA027C573EFC58CF + 457F9B446C99FE2E5CBFABBEFAC8E1EFC61F1ED89AAF3FF6F4CCCB2487585F03 + 63DF4FA0564871EB6EDD6F8A52DEE2E65D8C7B31E5607490F5E09D0085D55F82 + 4B59FFAE9C9754BAABBEE5F8049001C4780C00F0970108AFFD9EF4BE5FDAA69F + BFB18D7FFACED67EF795FE9DBFB2829FBFD7F354FFEBCF6CE3775FE835F3C056 + 7EF191FEB68F6CCDF7EAEBF9AFD0FFDDCA2F3FF4BFCDDDCCA4F1FDEE237762AE + FDE1137F6C9CC66B7086FE48ADD8CFDDADBBD1D3007F6E7900465CA0490CF88B + FBD5EBEDFE8A9B3681C06A0B00C1497FB38E7300AC16F8BBE1A7CF3C8D332994 + B7005B75FC76EAF8FEE56B3FA79F17E76F028001D00E9F790EBF6AFEB72FBD7E + 30357A0B71F56ABCF5B7005FB7FFC7D73ACF570E7E391F50D65DC43AA743DC5F + 49F1FCC0B6E879D0027A77FCF68DEDFCF3B7DEA2EDBFE85C3CF71FB5EEE7AF1C + 026FD539FCF804F492B21AA5E12FE239007123D4CD86BF51B9E0AFCF2916412C + 10B6E09700C3F3489FFD93FE1DF56FE1FF0FFC5BFE082806F606F81BEA2FE3E2 + 0DC7F37F8800BF19109C405A5ED3C059D27ED36EF881F3C4F3920E1A00CCFFC5 + 4387FEABBFFAC0B5EEDB4F6C3D29C7BFD66B45AFD935DF525FFA235BADF11A40 + B1CEB7F61BC5B49674E32B394E717E5C40DDEB007CC37B03A7BBF71D00871F37 + ACD479E2FBA7D8FB8834D00F921F75A8457C1691FA99CF1C4F1FAFCF21FA7C2E + 2DF84C9F47129F01388297686D1AF6A2853AC742BD0743DA76FA618E351100CF + 210B42E20806F8861AE341C14D0CC4254533403740E4E00606CAF2631C20EB1D + FF0C9EFD21E9A6E3981AE81C977605BF9301AF005D523E4707312097768ECE3D + 53EDBC044407F7B03EF7DF0BD0369E27732E7DC6D3CE0546EBF94400CC98E749 + 8CF928C02E2DCF7BBA9712209B0429A581D2B885438AE90886639F36ADA9D425 + 0608EB1A18CB1F8CA7263EBAFD66104E6020F0ADA071126B28A1C07577BCAEAF + D4D49FA8EBADD7E0E71AACEBFEE8ABB42163C7986B946CB868636E68FEBAF60B + 9A1BAEFDC1B0CBC05FED416EE81A0D6CD639C9EA31ECA2F634174F7AEBA2CEBF + DA713AFF181D3BE0F411C54EF88FC0866BAF31F89CF63E6A875D607F734273C7 + 6CE8B96336E40CE52F0EDB60C0AFDA613A6EF8191D7BF6980D3A79D8FA1D256B + 0A65328E38E00514A301C7B5BFD21EABFFB1FDD6FF7891FA68CFD6EBD00EEBC9 + 5EEFC81E5F33F8C47EEDD976DB00C5FA1FC6C5BBCD7AEFDBA6BDDE0EED09D9FF + 698FA8B5FD0F017D99D3FE50F1008977A9AF3DE5011DABB6DF917DD6E7F821EB + 73E288753FBCCF7A029FD5EFABE7C9DE93CC33FCF0B0F7DEEDD6775F80CE0E76 + 77E6299E675D776EB22E3B365AF7DD79D66B5FA1F5DC53603D761768AF5A2891 + A5664BD2DF2C2520785781F6CE1BB5FFCDB79E3A3F501990DBA160BDF6C3F95E + DAA4F79E6D1EEFBA6DB3973DA1040AA54FBA6D2F54CBE3173A000618772CDC18 + 60722100789DC3DF0E79AB32EA98BF4A7BF835D27AEB98A7351B376ABFBFCE1D + C16DD7ADD1788375C927EDB4EE2356E3065E691DD7AFB54E1B74AE756BADDD1A + AD53ACDD9A55D65EEBDBAD5963AD57E11E5E662D562ED57DC5726BBD56C7AD5B + A9FB8CE5D672F9126BB56C8943E0668B1638046EB640F73C73E739BC454DE684 + 94D011083BD4555B6FC6CC008067CDB626F3E65B23AD23567FE62CAB93C45BE8 + 5CAC6FA0384EE07A53A63904AE3B79AA8F6949110DF80502D753BFDE8429567B + F404DD038EB3DA8AD51DAFF19800851110B8263584478D77305C6DE8287B7DD8 + 18DD0B4EB64693F5B813F5183A0E27F11BC346E9FE7384BB80DF18240D1EE1EE + 614F214D7DE081BA5F2655F48061AE6AFD8739187E1D309C4061203000F865E0 + AF0408AE428AE89E456340F02BDD15276574973EF6925A07C4295730CA1EA30C + 204E1CC2D139ECC0586D95246D74E576DD5D95DA7593825BF885365DEDC5D645 + 0E6060AFBB86E33881BC087730635AA02F6B89D377308C43B87180BEB45E3338 + 336EE7E3087FA30338D6108E8A40380D86D3E372F55A5AD9BA2DAC0C35811D04 + 3755BFA903E072759BBB331840FC0C6EE0DA8DED993A8D35DFC49EADD7CC2A68 + FE391D1700B0E2D51B591952414B387F8BA58206F84AE5B5863617108E50F871 + 00B88C94CB111CC1705A4FBF52C74A54A9E91098B4D3AC01F066523D47E05B35 + 710BC7B9176B38FC2D43CA68B5B88171023BFC95A8079C1673514F557E23B33E + 7B0D73A8E4F3AFDBD32FBC514CD910D801303038A9099C0BF0A6E16FB6B2D7A2 + A7D0B3552D0378E9036DD36BE25CA274ACF89AAA561287AEDA22F01B14C6C105 + 1C1DC04535845FF63160D755EEC50079B35DC25A5FB4E68520A06D7400A781ED + 3FA0C701E05C4EE0742CBA85D3D037AD3417F8FB54F9B17A1204FEFF07009C61 + 4E8F53C2CAB2F959B6B2D9D923C7E4029AFF6E4A3FDF27297B7DFA1C4FD2EFAD + FDBDF95C30F78F2A5D3FB8944EF6F43340E0320E804BA9FF6CC52AF65CA5973D + F5F3534F950DC73CCD7FAC1EF8FF2229A0D32FBAF0222C7A617A4EEE62734F56 + 4EF8CB39BCFFE89BE99FA2F8A64BC59EF401939DBB3DAC4D7FF015571AF6E652 + AE63FE99FA5B1E270380B3A50B42902E3439F5728E98E4A0978B922EA06574D1 + 8CB1BF1102A7416FB632D036BDB6AC2EF09EE6591B044FEDAC0B7FE2002E2E5D + E4CBBD62A5CAE3EC95B8E873BEF2DA2C94D74604E09BA47C4EC3DFA80880D3CA + 86BF25D427CD7384BF11003F9DA4868EEB33CA018071003B004E407011FCD586 + 2F037FE96BB397B4317DB3436089387D4FED9C524CF79C56B1F92AF5AC74556D + 80AB364A1CBF01FE6600F06B4DB4316DF208FC4D03E0087E63FF490018E76F59 + 9400E0F2D1E99B12E037BB5E6F04BF28D6EB8D40383A81CBD7ED60CFD5EBE47A + B66E478DA9EF8BC3576309E0EBF57CD5CF00E0465DECB9C65D75B3D2D59EA32F + 51CF17F9B8B16E9E9A07C05B8E54D0D4FE55FFF996D4F3EDE3ED0BADFA790BFC + 8DAEDE0878230C4EBB7E3DD573B20EF76F155CBC090006F0E2E88D3038038471 + F5B22E99030247F85BB5F3706FA3EB9754CE00DCD846A01BC700E058A7D75DBD + 8A51D737D6F68DE99F3D46FDDE44EEF8ED33A618007E1DC09B8060806FF54193 + BC7D63806EBE49ED9C3880A9E55B7BD834AB3F6A96D51D31C36A0E9962B5A4BA + C3A75BAD61531DFCBE317892D51C36D9AA0F95D4D61C59047C6B8FD431ACD598 + 7EDDD13ACF98E0E605F666802F0E5FB5805D07BC8970F7D69E30C7C57C3D14D7 + 6AAED1A4055ED7B77E027FBD5EEF34DCBB8BADD18C2589965A83994BACE12CA0 + AEFA52A3594BACF1ECA5D664EE3257A33980DEA5D67CC14A6BB5788DB74DE72D + B7A6F35759F3856BAC055AB4D6C74D17ACB616D4E95DB2CE9A2F5E1BA0EF52F5 + 97ADB716CB3778DB526DBBD5055EA3B7ED0A523DE75B7B8D49EBDC466ABFAED0 + DAAD0D6ABB618BB559AF76E3366B97B7DDD57E53503BC5DAE7EDB00EF93BADD3 + E6DDD6B9708F75DEA21615E2EADD6D3D76EEB3EEBBF659D71D7BADEB4EEAAF01 + 76773908EEBE4BFD9D3BAC3369F7F6EEB0AE7B765A1720EEFE7DD6FBE00189FA + 6D7BACCFFEBDD68F5A7007F67BED36406F5F6AE92ADE779F5AA9DFA1FDD6FFF0 + 017767E0061970EC900D3C7AD0FA1DDC6BFD8FECB321270FDAA0E3D4DEDDE5EA + 7F78B70D3A1A1CBC838EA93DBEC7069F908EEF7647EF8023DBD56EB3C19A1F71 + 6A9F43DD610E763966A70D38BCDD5DBF234FEFB5112776D9D0235B6DE8B1ED5A + B747EBF7270058C2D172EEB87F393AE44CA8F18B4B06904B1B20EF691B75F18C + 8D4E5CB6388289015B81BFA32E9DB5B157CE873E1096B4CDEA0FBF4CCA46EAED + 5EB0D1572FDB986B6FDAD8EB6FAA7FD1812D2DB095FABB8CF93297B490455FEA + EA71AF691DE2FC97C3638D519F31757D47731EEAF94A2375CCC80BA7DDED3B1A + 507C09B87BD6C6294E8B465D3C65A32F9CB4B197D0096F999F78F59C4DBA76DE + C6EBF1C7E939F185F4841B9733E28B695ABE5C9EA0FEA49B6FDA1469EAADABEE + FE9D7BFFAEA7329EFECEAD4C1D61EA044FBD7B33A9BF1B20300077F25BD76D26 + 6E60BE58BF7DCD66DDBD652B1F7E6285BFFC609B7EF8DA5329CF7AF78EA78146 + 40E019A40A750771380F2D801977316019084B3A50BEB89FFE1EAE2DE91E5FE6 + DF365CBFD4099E72F75A223D7769D25B9793FE65CD5FD1FAAB3E9EFACE55C319 + 3CEF633DDF7BD7A4EB7A8CB73C8DB4BB7F3F01580458421A68DCC52BBF7E60DB + FFDB9FADF0AF3FDBA2CF3E34D24ECFFBE05D5BFEC5C79E7E39BA7371FB2EFA24 + 0059402C7099E7C7E301AA67DDBFE54EE4F99FF0F801FA06DD74383DFB839B36 + 0720FC316EE5E0649EF7919ED7A7776D3100E8D3F7FCBC2B700852175463524E + 135BFDD527B68AF4CFC0E74FDF7747F2629FBB672BBEB8AFF98F1C9C2EC20528 + 0173238CA5BE2EF03442DFE8668CE0CA9D8DA475D6314B22FCFD5CADB4E48162 + 9FEB5C9F039DEFDBF287F76D85D6537F7585CEBF9CF4CFD24A3D0E0E4BD2ECAE + C5E5FA332ED92FADE0976FA4EF2DEF4F80D35FECE4FFE3FF6EA7FECFFFDD76FF + E72F56F8F3B77ACD7C69EB71027FF3B96DFC51FDEF1FD85A1CCC40DFAF3FF676 + FD0F9FDB3AEABEFE10D203037E1D00FF18C60E8013C0E8A9887F7998018F3186 + 431548E85012409940DB425215036501C300D8C4D1EB2E5FADF514D080588DA9 + AD8BB316015901C6D408468C11F57D49FDBCF14F7A5C5CB43AC6DDC79ADBFE1F + DFD9AEFFFCC1DBED7FFDD6059C0DFDEFFCB9E0D4DDA131F234CE7A3EB87D77FD + F91BDBF597EF6CC79F15FF15F7AF9E83FE7D37EB6F415BF5FCB73BA0565CE728 + 3A6F780C62405F9E0F309CE7E3003881BAF48BB97213118B75825913523DEBEF + 56CB1C1018500EF4F634DB89FB9AFF03774CEB71A20B388AFF97007F81BC9FDA + DA9FF47F0B044E03609D07677B48E7AD310E77CEFFC3033D1670F94BFD3B3FD4 + DCA77A2E5FD9DEFFF967DBF33FFD49FD2F3DC6FB61ED3700E08FFD75C4B968F9 + 61013F98F0B85AC62BF5BEE1C706DED76B0F45E72E4E74202EFD655FEB7D89DC + CD1B7E1041AD5FD6B2CE5DEBFEE393D0271BC142BD3F17FB8F2712E09BFC0085 + 3E5AF8D9BB36FFB3775C01F0DEB3057ADFF19930E7E3B75C6446604CDCEB0A4B + ACA576B7F7D532E7E7D0E7C8A207C13D5C1C00934EBA287534D018D12746B605 + EA0CCF02FEEA315DA4E4570C28EC10F6C3B0867180C601F632876398C770E7B0 + 3E536727F1B53F7C65EB7EFA5ACFE3BEAE01FCA8E8B6E6C339784C20F3B477F9 + 414F48EFCF6734A9FD67E83A40DA7FCFD600E00648BF1F6201FAE2F6BDE5EB38 + 365C23F831508831F6FAF2F76E1453BC96C4791FBF73CD26BD4DDDFBD04EB8A3 + EBA534F12DAE33576DE21D00300EDD0B36FE96AEADB7751D95C6DFB8E89938B8 + B64EBEADB5B7AE687CD1C6712DBE7E4EF1F32EFA68E24D1D7F5BF3B72ED8981B + BAEE5FD51E21A591E8CD647C4DFB93CC9CFA974FDBF00B27B45F386B13F4188C + 47F343B06B6400D1DE401A734DEDE593DABF1C57CBDA93411E3BA63DD1111B7E + F6B00D3F73D801F0605CC42770061FB121F4D94F9D218BCA111BA83D56FFE37B + 6DC089BD36F8F441D7A05340E17DD6F7E86EEBA37D5A7FB2B468DF36E4243F00 + DC69838E48DAD3F53B04FCDD9694F008A2CF0F01FB1CD8EA73FC401040DCFF90 + E681C787357F48E363FB6C3070FAEC09EB73FCA003EF39DA7F8CD0BEACF7C13D + D66B1FAE61C0B2CE796897679AE9B37FABF5DA5BA878A1F5DC5B603DF6E45BCF + 7D9BB5BF2D74084C3912CA8C20B2D3007F7B3A1406020731EEBE1301730BACDB + 8E7C771577D9868338DF633D7616BAD317E8DB0310BD43E31D8AEF429B7D3D29 + A85197AD1BADF3E675DACBAFD63E7FB9DA95D66DEB7AEB4A6A6ADCC6056BB4C7 + C709BCC63A6E5A6F1D37AEB70EEAB7DFB05AE375D69954D479EB355E631DD66B + CD86B5D67ECD2A07BA0E84D769EDDA15EA2F4F622BACCD9A1501FEAA6DBB6E95 + B55BBFCA5AAD5AE6CE600070CBA58B1D02B758BC50F7348BADF9FC059E0E3AB6 + CDE62420582DE3C6F3742F367BB6035FDCC24DB40E000CFC45A483A66D0E00D6 + 7ADCC00D80C2D36658BDA9A4879E1EFAA48AA6BEB0546FD2144F15DD70CA74DD + CB4DB25A63C65BDD0953ACD1D499BAAFD3BDE3E8090E8081C175C64EF29658CD + 5148F7A46352B191E31317F1584F1F8D4B9836A6937E63F028AB3164B4D5501F + 91461AD550BC3A3584070CB76A09107EADDF507BA5F72087C0C0E00884894777 + 30B1D77A533B7840A813ACF6D59E8A771F9001BEB48C5FE9A6580E004CDDE008 + 7EBD8670A2973B6ADC41F1944338EAC576DDEDC5B6DD1C044731F6BAC2EA47C8 + 4B5DE14AD408561F081C9DC1D41C0608E306AED8B2A3D7147EBE99DAA4BE70B1 + B669102E6187C340E0943338A306C1319C86C280DF2800302A470B08A62EB0FA + 319534FD32B59BD933B59A5A690950FC6CFD1656B17E4B7BAE6E73770297F5B4 + D08DAD5CAD26AEB235347EA3C8094C9DE0A834144E8360E06F6C738160206EAE + B4D0B900306B80BF25A4D2AFD6CD006077FC5609315F97B8893D25740ADA4670 + 4B8B13D8DDC039E6D38A9017089CBD2ECE01809F79B17A4E001C1DC0510E821F + 0375631C9770AE54D151C5E02D7056E39200DBA40F188EEBE2DA743F5BC0DFA7 + DCD9FBB2AB08DA128B719DDBDB2A09C87DD1FB715D06EE4A45C71529AC0FC715 + 01E0447C979E02B68F405C1CBCA9F9DFD3EF398023F8CD09859FC9ED00CE6603 + 715DAE786E557EAC7202E0048C97D038D7318F539AC5A4F5A4B9FF91FAA300F8 + 8F2827434BAF4983CCBF47E973E552AE63FE56E53AEF1F51F639D2E3B49E3487 + 1E77CE5803381BEAFEADCA4060D240972A6B259FD649D546B72FB57FA9078C8A + 9CBF120FFEEF0A80D3FF50E998F703B82EF174457FB167BF28B35FA899F163F5 + E89BE89F2A7FC349A9D8EF7DB8A0A235B4E143EF711FA011C2FE2D30F67F848A + 01DFEC7146BAD8FC9ED290F749CA3E2E057DD34A03DF6C3D0E00473D950580D3 + 29A0431C08ACCD88BB815F09751BF8E557796D14589BE5FA4569001C5DBFC0DE + 6C984BED5EAFDF2B3D931CEBC2014C9C54CE52D13145E0370380692BD5D4FA5A + F60CF57E33F0B78E366252027633ADC3DED03AC4A54DE2A55EAA570CEE46C0EB + A29F527AFE9997B56975081C407084BF655FD52619BD9680DF0402875ABE1100 + 07F0EBF309082E5DED51F84B8DDF58D3B77C2277FBE2F2ADD5A698DCF9AB96B4 + CF387B63DAE70880631AE7E0EE0D303880DE22D81BDDBE11FC3E4B4AE746B87D + 71F906084C5BB131B57CBB1503C0F42B01801BE3FEEDE90E5F5AE06FF926DDAD + 42F35E0E741DE226F01708FC420AEE5646090476D0AB78ACE91BEBFAC6188017 + 45D89B06C02F76D05C478DD5E2EE7D2159875857A5D3507BB9F330AFFF1B9DBF + 40DE087ABDAE6F540AFE52C317B8EB7D8403B8D71807BDD5FB8C532BF50DAADE + 6FBC0BD80BF805F6A20880E993CAF9F581131D00A31A03275BCDC1535CD50786 + 58EDE1BA811F3DDBEA8D9AE54E60040CAEA3B8BB7B87017771F64EB5DA52FDD1 + D3ADCEC8190E806BA905FCD61935C3C16FFDB173AC019077E27C6B3C79A12BE3 + EA458A47E1EE050093D2B99E04E8E53862F535A6566FE3A98BACD19485D670CA + 22AFD7DB64E6326B3C6B993599BDDC9ACE5921E1E65D618DE62EB7C673575893 + B92BADF11C1CBB9A5FB8CC9A2D5A61CD17ADB6E60B5759F3C5ABADD5D2B5D66A + F93A6F5B2E59AFFE0669A3B55EA656E316405FC55AAE508C94CDA46E5EBADE5A + AED4983ABD6BF233EAB0618B75DCB8D5DAAF2FF416B55DB7D9DA69DC396FBB75 + DCB4CD3A102FD8611D48E95CB8D33A4A1D18E7AB953A16EC74E8DB75FB5EEBBE + 73BF8B7E976D8A017F77EDB3BEFB0F5BEF7D87ACFB6ECDEFDD673DA51EBBF7B8 + 7A1FD86FBD0FEDF7FA6DBD0F69EE80E2FB153F487DB72336F0D8511B7854EDD1 + C336E068A8FBD657EBD0006AE91E3E6003A441C70ED9B0537C41181C23A45EC6 + 3182A364F009CD4BB84A00B2038EEDB1C127F7DB909307420BD83D7DC0869F39 + 6023CF1FB231178ED8E80B87BD3FFCF43E69AF8D3A7BC0A1EE088D479D3B20ED + B7E1A776D9D0133B6DF8C93D36F2C46E1B716247D0C95D36EC04A038B880879D + D679CE1DB5E1E78FE9B112000CE0255522E0F7FC299D0F201CC6C3CE9EB261A4 + 8066FE4200AECC8D521FE84A6CF4F9E0C41D7AE98CA77276D8FBE625236DF318 + 09976FA8B977CC865ED06369DD48E02E10F7CA69D7882B3A4EF22F6EF9521737 + F0C5933AAF6217F5BCA4E8EC658EF8489D2BCCE1EEBD6013AE5DB2F157CFAB7F + CE26DD202DF4791B79F1948DBD74CAC601802F1E974ED8842B676DD2B50B3691 + B57A1E63DE3C6B1370135DBF686315C79934F9F6559B70FD928D4DE0305F564F + 95266B1DF139EFBFED4E579CBAB88F80C008083CED9D9BEE089EE43513838378 + 3A60F89D0080796ED3DEBAEED077F6BB6F297EC7A6BF7D4BFDB76D0EB51FEFDE + B699E81EF0F7AECD7CEF6D07CBC0641CC6D1658C738B34D3000240010E61C073 + F8323FC0DD22F8CB97F0E75D93DEBA68D3DFBDAA75D76CF2DB97923120802FFB + F55CBD0F14C03D7CCB8143708C053800005EF8E907B6F8F38F5CD4050EA0FAAE + CDFFF87DAF1B3C53CF6D0EEB34B7F4B30FBD06F05CE007AE398716A4A9C6C5A6 + BFFB83908ABA08F2023C6E689DFE8D1C02DF72F08B5399DAC5F33F0170DCB385 + 40990FEF7A1A697703BB233880669CC1A4485EF6C5870E9F97932A16F7DF67EF + 2A76DF567DFDB1E63F70F0BB40E7C559BCC4E1ED3D87BB80DFD5DF918E59C727 + B0779904FCCD0820FCF07D6F5D8AADF82A387101662E0019B104FE463126452F + A97A49CB1BDA00EE36FE802BF45B07C03BFFE3373BF7FFFC3FEDDAFFE7FF6597 + FFDFFF4F3BF17FFC6FB6FBBFFD6A5B7EFDC136FFFCADE5FFFC8D6DF8FE0B3FCE + C1DF7780C2CFBD262F699ED1869F81C19F3800C60D0CA40432021C1D3AFEF695 + E50338D53AF48DFA9994C5013E220071AC53BBE52FDF84BED679BAE4BF502337 + 8CDDE9ABE350BE8EA30504535B3702605AD22E5313D853410381DD99AAC7F825 + D40CE63C5B01B1FFF1AD9F7FF3AF0FDD89BB83FAC0FFFD07DBF53FFF68BBFEFB + 8FB647DA2DEDD0BAED5AEFE99CA9EFABF3BAF3F7CFDFDAB65F15D7789BE20E7B + 2580F14EC072029739EFB6BFE8B1B4C6A1F62F7AAE12CFC981347FC36F5F5A21 + 7FAB04DC8DD0373A7E63BBF5AFDFF89A90C239CC4768CCD85335EBFFDBD36FE3 + D09536FFCCBFA51E5BCF8DFF9700E681C689A3578A0078BDCEE1699FA300C4DF + 7F9EFC080088ABD7C3B78AF3BA903CDD7902825957F8DB7776E07FFD8B1DFCDF + FEAA7FD3EFF53CBED031387CF59AF90EE9181D0FF4E5750C085EFD35AFE9E032 + 0F3F6A485EEBDF50AF1A17BDDE678996691E0730CEDF989E7CE1E7EF3AE05D4C + AA678D972640D86BFC32D67B891F51007801BD387EA9798DFB7791A77DE73CD1 + 058CBB17000C08D6678204F405C046001C01F112DEBF7A1EA4725FCC8F36F4BC + 96B8745E77F1F3581FEBFCD418D6E71A90D95DC12125BE03E8C471BC2899C34D + 3C5F9F19310D3F6D14CF817AEC0E893F20634384B738786983AB18A72EE7F0B8 + 3E1BC9EC00705EF1CD67B6F2BB075AF39E83DA99F7F93C24F53F9FC980DFF0B9 + CCE726F1599A0F9FD7FC6886CFCAF0793AEBFE4DC56E788CC7E55CFCB867A6C6 + 9C6B26C7E85C33BD4E3C3F20D2E7BB3EE3A7AB4553DF01FAF203A2ABFEE3A1A9 + EF702DB9A26B4691A6E97A81A630D69A10D775529A78E7B28DBF49660D5D9B6F + 5DD47555D747FA3774ED557CF26D20303FB6BAE8F1C9B72FE9BAA8C7B9A3FEAD + F312315D8F757DE2FA35F1CE455DBF00CAE76DEC0D5DB7AF6BEF710DD87BCADB + 31D791AEF537CFE871CFEAFACFBEE484F607276D128F7793EB7900C363B4C718 + 7BFDACAEF700E133617C4DC7EA1CE375AE719CF3F2711BA1FDD7C88B471318AC + FDCBD9A38A253AAF7989F38FBAC40FE90ED9E0D3DAC369EF36F4EC6187C743CE + 1C920E3A60E69C232EE0303EA8BDD75EED137768CFB6DB7FC837E838D0778767 + 75197A92397E14C88FFE88F3A3C0DDFEA3C0415A0F3C663F37E4C45EED4177DA + 20ED0547694F334C7BA281DA5752577FD38F5FDAC4EB17ACDF11CEA5F31FDDE7 + AEE2BE87B63B4C468065C0739F035B02F43DB8C5D5577120F1804300E31D0E81 + 7BEF2D74E08BE803887106037281B8C0DEE816060477DEBA41F14D19281CD249 + 07E81B617198DBA4FDF906EDE7D759E7CD6BAD63FEEA8C1877DFB63103812300 + A63670E7BCF5D6A5608375CE5FA77B83D5BA370810B8535E50874D6BADDDBA55 + D6760D699F9759FBD52BACE35AAD03F2AE5966ED562F75B55DBDCC5AAF59EE10 + B8CDDA956A975A8B650BADD992F90E7F81C088B4D00E83172E2CA666F3E76780 + 30AEDEE68B17EB3E6991A78CA66DB16489D604084C3A689CC280E146EAD30280 + A9254C2A68D243E3080608D79F3ADD9ACE9C63CDE7CEB726B3E6E85E6DB63599 + A1FBCAC9D3ADF68429567F8AD6AA5F671CCEE0A9D67092EE3B274ED3BDDE3477 + 06A37A13A76BACFBC971BADF1C3DC1AA534778D4F80C24A6EFCEE1A40502239C + C45E3378F8D8A0613A6EE81887C000E23706254E61D245F71BAAFB6509283C60 + B8BDE64E61D2480F77080C0C7658DC7BB0BDDA8B7AC283AD9AE2AFE21CEE31C0 + AA74EF6FAFF41CA07B74CD01867B0CCCB88101C28061D24147301C5344FB1810 + 9CB4A48A8E7AB1634FABE229A2BBDB4B6AA923FC523BB549EC8576DDECF9B65D + BD0DEA6E95DB74F598BB845BAB9FD414468C634AE917931AC32800E020E03000 + 180180A32B38A3264138869FD3FCE39CC169201CC16F5A110C1701E2E6F65C83 + 96F65CBD160E8023F87DB64E337BB67633AF091C812FEE6014C711FCA20C0C8E + D03769D300388E116037CE0175B3E12F20B7B4DAB89614D0CFBC5A541FD89DBF + AFD4F1E381C0D105EC71A974559DA36A5D07B74F3DFF8683DFB422D0CD06BC5E + 3738D193E6A9031C01F0E3C06F9C8BE308784B552802BFC0D8D84F43E06C608B + 22CC65DEE12E8036351FCF13C769F89BDDF73100596D5AD1095CDC01FC524EC8 + 9B86BFD950387B8DBB84A562A03795BE3927C04D8F131503B88C1F775C04C0A9 + EFEFA3E337737C4A696E9156E402E9713AFE64C13972EBA9D27A4E40DF34004E + 9406C0D9A538B3958BCBFCBBAB38004EF7B3C629669656E465D922FB6ECCC09B + EBB8624AF3BBB49EB4EE4973FFA8B2CFFD38E53AF65FAA7F100047576F29EAFF + FAB8B4FE83CAE93F8A1ABF01F40281112E6087C025021C8E2035E73FC4BF91E2 + 0B30F6D3F1E22FEE2087C2A93741669CE3CDF25F229EC7DF08808B7F0805F1A1 + 173F28D322FE38F81BE7FF5D540CF6C67E3AE6D245E7F79482B7BFAB32BA00E2 + 0E4EFA8F48F134F0FD432AAB8B794AA1C66F84BEA15F8A5ABEA4754EE06F04C0 + 25CB0395398F360B49DAE7B4033802E0E8FECD06C011FA1641DDDC8AEBD27247 + 70256DB670FA46001CC78F387FEB58C9E7EB261038F6834ABD50CF4ABEA88DA3 + 5AF434FD9700BD0DD4D6F736887E0280A5341006FA16815F5CBFDAB026299F23 + E84DABCC6BDA141703BDCD8BA9EC6BCDB5316E61B18E6F2E45876FF95AADB401 + 0FB0373A7CD38AAEDE087FD300B818F84DEAF856400D83BB37A6788E699E23E8 + 25C5B3C35DB5151B75D58D4737DD8C74B50A4D7463D3B47B0602D3566ED6C35E + 68DED32A35EB69159B8654CF159A28D6BA9F55ED38C4A1EEB34D7B04000CE805 + 06270018A80BDC7D418A299C81BA40DF573A0D9186BAAAB41FAC1BAF040623F5 + 63BAE717BC0FE4D5BC1EEF65AD070053DBD72130357F752E041C8EE037825EFA + 283A7F3DAD730FD2398FC9387F23048E209814CF08D05B0B608B733771F746D0 + 9BD18090E639A67A2605F4AB80E081A1562F699DAB0F9E62B5864E73D51C82C3 + 77926E94A75ADD51331DE4027CAB0FD59AE189BB77A45A52378F2685F35477FF + D655BFEE68AD579CF4CEB5D5C7B5EB699B71F0AA6D3871BE359DB6C49A4D5F1A + A02E73D4ED95A8E58BEA119F3CDFEA4E9AE7299D71F3527F97D4CD9EDA79DAA2 + E0F04DDCBEC0DEA6A4649EBBC29DBAA46976C7AE6BB535D7B8C5C2B56AD75AD3 + 459A5BB2D29A2D5D65CD97AFB5D6AB36582BA9E5CAF5D6426AA97E9BB579D66E + 7D81B5955AAFC9D31A20EF466BB13ACF454DDE566A9BADDC60CDD76EB4561BF2 + ACCDA6026B9317446D5DEAEFB6CFDB6C1D376FB50E855BAD9DC6D4DBED4CAA66 + A913F5769394CDDD76907E79A775DE424AE73DAEAEDBF65AB7EDFBACDBEEFDD6 + 6DEF01EBBEEFA075DF7BD0BAEEDE27ED511FB07BD8FA1E3C6ABD0F1C72B0DBE7 + D041EBBD7FBFF5D97FC0FA1D0975DCFA9F3864FD8E2B7E649FF53ABCCFFA1E39 + E0F101C70F7BFD37C43CEBFA9F507B3CA4FDEBC7976C6A079F3C6423CE1EB311 + E7A8AD7BD4C1AF3B45CE1EB5616749257858FD23EE10E1CB40FFE2F0CC011B78 + 12F7C81EADDDEB0078C4D98336521A75FEB06BF4F94336F6C221B5076CD4D97D + 36E6DC019B70F9B04D7AF3A8E2076CE4E9DD36EA8C746A978D3ABDC3469FD9A9 + 76970D3F0E1CDEA3F3E9383FD7511B79EEA883DED1E74F273A63632E9C956881 + BAA7F5D8278B89F52813BB108E8BC078C4E5B3EEEE1D859BF7D24977DF8C4C5C + 37C32F905EF1888DB8785C633DAE6263AFEA71BCAF735D39AE638E065DE60B56 + ADD5731DA9E3DCB59B68CCA51336E632E057C79C074C8734D68060D23FE3F81D + A1380E659EDF50FD3F8C260D24C75F3CE11A7FE9B48DBF7256313D37CD8DD1F3 + 05167B0ACA2BE702F0C505AC18B078C2B50B36E5E6459B7EF3924DC1ADA4E73E + E9E61507B8B87B01BF38857101873EB510DFF41829A501C053EE5CB3E96FDFB0 + 19D409A6B6F0F5CB36499A76E7BA4DBD73C3EB09038167DCBD63D3A8277CFBBA + B733EEBDE590778A8E0502E3028EE9A601C2C418A76300823400E64B797751DD + 3CF7FF25EF3FDFAA4AB7AD6F74D5AAB42AE71CAC5C96659539E79C2392040191 + A080604024E71C054144909CA3A0828A3957B2D2DAFBD9CFFB3ED7F978BE9C3F + A29FD6FA3DC66448A1AB6AADDAEBDDD7391F9A77BEC798D3C99C63CCDF6CBDCB + B6E64AF16CC7E3E9AEB3BED0AFC57C037D7D7B1816BA59E8C8253460B868E620 + 360E35BA839B143AEC1EEA97A0D3BD0A7DE94426040E1D347D140131413441F0 + EEB3FD12710EE303BD1240D87DAA439827937083AE37754C9FA2F3B7037BD319 + DC6E1D9F50C384AC360E610BFA9E638858BA0609A00770DC537A3E61E74FAB18 + B6DA2EF73247AE05746C78C472EF25E62C1DD436E10EE112A130730B879DEB36 + EEDC4B0C134DC045D8D5A725C3E152CC81CA3102311B8C31442E73A5465F1F74 + C133BA2919B6F9E015BA2B4D58DD7D174F696987DDA5A2099B2FF7CBDEF3BD12 + 85F33E7475580E5FBF84F2A2C4DDBC2A19F7BE95D2FFF3BF1402D7FF7FFEDF52 + F45FBF4AFA0F7725ED07BA37AF61DE053978D93A9E05811518DE1C72014A4243 + 751A5F3FA36D85BF16E8653D9680D302BBA9F7AE29E4B581B0ED4665C93EBB5F + E1F137281D8054E1EF9D11004C11FC26730EC1AFD5B673EE321C341DC684A114 + D71328DB7BD2456BC3662AFDA71B92FDF7DBD01DC924F8FDF5B6E442193F5D77 + C168BA7AB37FBC21593F5C930C0D3F6DA0AE86A3BE775D32304665FD7C4B95F9 + D34D887DD71530F331A9BEC5F1BF33D0D7E54A469D7097E2B911E812D2B25F21 + B8257B9CE097CF3DE7B0DFEED310D07C7CFC3F50E7F4B04A9F4FFE7FE07922F8 + B5EB36FC353F1630FF8F84C81A0E9AFDB7E828C6FFD3AD4BC23CD80C874E3134 + 7AF435BC3EAE120213009BF1F85B7CEEAFEB8F08087CF9BAD1D7D015C25FACC3 + 7CEDC3EB9B619FA3190E9AE19EF17A8DE2EB18AF57930B98B986F1FAD5D2FC6D + 708C8EF7BD74D45317FA249C7F5B0A5AF9830A5B26CCB3C9F3CBBF6BFE2D5A4E + 7AB4098509889DAEE011977017D429BB2FD0E54BD8DBA125C5BE3DC30CC5DE2D + E1CCCF7DB94FA2AEE16F16CF1B43AE33C4BB9D833BE232CE8FE7CB1F84D0B18C + F3B18FABC2DF3EC3591B873FF6D473E37976EB39307241C810DE9F707C2A7888 + 0EE436BCBFB54B28CE2F146DFB872D14DFCB0867430671CE830CD76CDEF78251 + 27200F19EA56A0ACB9DC098B356F3B45E0CBF74ABE27F27DB845FB39CE1FC9EC + 1C68854CD4043B8A42C029F3231AD6F9239A1D3DF5E2D7572F3B4FA36F00EAC7 + 3EA71A51366BE9DF5387F7688CF735A876749D14DFCE1A954F4735CA5A887D23 + 0AC0670765CFDD8E79DE9DD5B2BD0B6BB8AEFD8478B51E576D47DD47E16E153E + 3F8FAB5827ECF56DAF12BFAE1312D05D2D3B3BAB2400F28776745662EFE3F85C + 3B8112F33A4F601F7CFEB61DD3D2A7037BB71FC3671B3EDB9BCAF0995B2A5E6D + 15E8AB14AFE60AF169C65A82E416AC23F0ADC7B5432DE635610DB4ADB15C3C5B + B81F3EF7F1D9A86AC035411DAE294E96AA58D775A8F3BAC40DD72A6C13106FAD + C5F508C71BCA5D7336E2BA85629DD776ACFBB4554B203E63BD9A4FA04D5731AE + D78EE7E3DA893FE0C3B51E7FC4C76BB71379B2B5DAD4D7F3DAED7836E6E5A19D + ABA07863659E6CE2B521B4B1325F36A0BDB18A80F9A886AF66E8EA607C76F287 + 479E0DC765D5114691C9977565B92E3731B5349FA945521532B324045E518C76 + 095DC819C6995C9225ABE9322EC49C7CACC93521A0592EC7FA6579C9B2288B60 + 375E9664272A14E6BC4559090A75E7A7C7E23A9D10988E60D64D4E611B023304 + B53D673EE6DBE097C0770101720ACB18999B7450352F395AE6249A39F3930EC9 + 42BA8613A365165DC187A354B3E3F6CB9CF8035A9F76284266C646C91C8CCF3E + 64A02F1DC06CCF427BEAFEDD32396A8FE6FEB5C3406BDD02C05322C3655AD45E + D5F47D910A81A74484C9E470ACB1F4F59E5099BCDB68CA9EDD18C79CBD7B1504 + 4F467D6A64A47CB57B8F8682660869CACE25CC50D0137605E15E2C48F3072B0C + 469F025F88F0775270A8BA8C27060619B7716030E606613C08F77281F2B197AF + 7CEEE32F13FD761947F076DC437AED5048FC29C33EFB06E29E10F79B0C17EDE1 + ADB0584131CA8F3DB61BB96FD790D2E3985798C077ABA70935BDC95DC5DCC236 + 1C7E7F839B6A1CC53E8260E87D868F86DE45DD0E1DFDD64A0386DF5BBD4941F0 + DBCB9943D802C2286DC7F05B84C2CBD629247E67C95ADCA3AFC6BDFC6A05C26C + 1B30BC4A5E674E610B04DB9058E1304ADB2D4CBD3CDB8486A63B585DC2740EA3 + 4E6730F307BF3C8B7984176AF9CAECC5F2EA1C8E597D105DC22F4F5DE092DD7E + 95F529F3E5A5C9F3E4854973E5F9AFE7C87304BF285F845E40FD79CB09AC6EE0 + 89237AE12BF45B7ADE82C44E37B0130813FEBAF2058F02C0B6E80636EEE0AF55 + 9A2BF8A32FE52948E1AF2D42E1711314043F4DD7305DC174098FBB1F003FFECE + 672A825F8A751B0ADB63BF81C49C0FB1FE9405800982D51D4C006C4162BA8959 + 27F0250456408CF2D1374CFB712734B664DA58F33A43427FA8E1A26D90EB7401 + 3B21EFE8BA2D1B06DB755B7F7DE53DF4BD2B7403DF077AADB6130E6BFB459416 + E47D986C906BC35C1BDEB2649F3DC706B9CEB92E71EC99375CCE60A7EC754ED8 + 3B1A04DB30F86100D8290260CA057C47CD77F58D82BD767B2C90FB9B311BEADA + 72C25EE75AE7DCD1DFE98FEEB3EAA39905F56FCB014C3D0000FF5E3D9CCFDC3F + F6FF88C8976C59DCCB08638EF6238FE29C2D1EE6646A7F54368373C901351F79 + 84E5FD7D2E3D64DD03FBFF0C39F77E98C65AFBDFAA7F11005326FCF3D35A3EF2 + D727F11FF437081BD3016C0360948FD209CCD0CF2EF8FB3F13008FF582FBAD7E + 0B7AEF4B72ED906BDE587F38FF06E9B11F0280C77A13B1C7CD98D1D86F82FFF3 + 20EF58B2CFFFBE0F0BBBEEEC53E103E91FC902B7BF4BCEB90EE8EBD498907794 + E804B635027E6DB04BD06B00B0090D8D8B01C2E0273187D0D79AA7754DEE0F3D + 8D39160026F0B5F5D7E77001F4BC15DED9A1D150976D867FFE0BC628D61FB120 + 2F1DC18F59EE5FEA51BA80AD311BF82AF465C91CBFD0631600BE1FFE7E228FBC + F8B1CA06BF0A7FADB1C75EC1C5E7AB23B97B6DD9A19C47C4FE11E8EB84BF4FBE + 898BDC3727C8E36F3A00EF28F86B03E0BFDD17DE79928AF0D7AEFFED3D037A9F + F8C0E4F2A5ECBCBE06FC8E387CED10CF0A7B1D72B97B3F6188E7999AD757C5B6 + 057E7F0380C7CF91E71500CF53E06B8BE0D706BE2F4E58A0AE5E025E025F42DE + 1728BB8E927AE9EBC5F2F224DCF44C590A2D77B97F5F9CBC5CC62D76932FD6ED + 5080FB14F79B8C1B2A025E88219CD5CDCB364A3B4FAF3A7A67AF5390FBE65CDC + F8416F326CF3ACF5F2FACC75F2FA2C8EAD37219C1506E3C66FEE46797DCE062D + DF5DE426EF2CDA2AAFCFDB881B32CCC5FA37E61BA84BD8FBEA5CACA5EB77216E + 2EAD1CBE360C5600CC10CF4BB628E8A5B3F75D0BFADA10581DBED0DB568EDF71 + 2B3D15007FB06A04006BE8672BE433F3F9329CB32D42E0B75619F8FBF61ACA84 + 721E6701E08F36F809C33D8F5B67F2FF7EBCC9387ADFDFE0839BE6ED0A820980 + 8DBB3740433B7F4418BC79870260F67DBCD5846AFED46397CBE56B877966E8E6 + 2F7DC36422A4619CBD423477EF27DE04BE04BFE8A3BB7707C629FF3097B39710 + 98F09720F84BF44D441F5DBECCD14B00AC4E5F866EDE73D088E19B43A3654AD8 + 2199167158A6861F96499187E4EB7DD1AA29070FCB8C9878990E4D8D3E2C530E + C42AFC9D1D97A29A159F2A336293309E20330EA384A6C624CA3468466CB2D6A7 + C6C5CB8CC4242BDF6EAACC4E4ED51CBC2C67A36F5E5ABACC670EDED434AD13F6 + 2ECA322EDD2539C6B1ABCEDD9C7C590C2DC92D425988FE42F417C9A2DC025942 + C05B502C4B0B8B4D5DCB0259565824CB8A4A647971B1AC28315A566460F0F2E2 + 225941576F59A186D25B72047B43CB8F16AAB342DDBC104300AE2EA393974E8E + 6205BECCFFB65201709EC25E860BA4D661DDDA8A22D506BA474E1C55D7084130 + DD211B4F1C11BA47F8C5E2FAE304C205B2BE325F36551907F0267EB97782623D + 5FB69CC8976D3585E25E5320EE270BC40372AFC997AD5539E2763C5BB6A124FC + DD5491269B5996A5CBBA9234D9509A299B2BF3C48DFBF0CBC38A4271ABAA40FB + 980BEADA8097DA6AF5B3E4D8A6CAA306FAAAF0188E95E9F836868A660E613CD6 + 4D9A4BAF4C3657E3F1E171F10BC9CD35E5E246976F0DFB8A65530D1F2FBF2835 + 6116596EE617AB36FC85365417CB96EA23B215DA56532A1EB547C5B3F698CA03 + 7BB9D7D2D58BF5380E413B41B0795E09822BF479DF5885361DC2905BCD5171AB + 2EC563C59E552550291E17D763DC7216D335EC567B425DCD0C29ED515FAD2EE1 + ED8D35A66CA812DFA66AF165084BCCA50BD8ABB54E98AB90E097209325DDC09E + AD0D0A86E906669E441F88AE628ACE5F42DFED2D7413D3554C4770837835358A + 6F7B9BECECE99280EE4EF16EC11AC8475DC4283BE9FC6A171F1CC71B6D868226 + 806569BB8D39474131C37412FE761A87169D599EAD744E558B7B33CE1D251D5D + EAEAC2BC200BF872EE4E0BB8B2A42B8C0EDD80812ED1DC930384193D1272B64F + 18869A8E5FBA7FC32F9C151B02B36478682AFCFC591D0F3973CA84B61E307931 + 83B05EC3AB9EE99540ECC130A904C084BE0602778C6A7729A875E502A5ABF7E2 + 1951D7DE0594C38310CFC1805FD6D917759961694D1EE330BA80AF0C4AE4658C + 5920388C2166A108DD0BE50542D83E55E4798670A6FA345C344158ECCD0BAA98 + 1B84694362E7108E4549E87BBF8CFB52611A81EC35BA2B1946972078C00583A3 + 864F691F215B248EBF0FE71E8DE78EDA7F91F9592FA812EEDC90C2FFFABB14FC + E7CF1A3A3CE9EE4D49FDEE0E8E7D598131F3021FBA721E73711C1C8B508EB05A + DDA2749BA23C64B98D356CF1DD6149F8EE8A247E7F45E20861BFBD24A93FDD90 + ACFFB823D9FF7957327FBD2DE93FDF94B41FAF2B78258824F475C2DF04C24F3A + 5A092071AC981B83389F2189C3638F439D8AC718DDBE543C2125FAE80AA62398 + 7D711AA2D8AC3F786500CF29E613BE5AC7218C4EFFF186A410E4E25CB27EB92D + 59BF1AF89B86FE0CC2DB9F6F094352C73337F1DD8B9279EF86E4FF7A57F27EC6 + DC1F6F493ADA84BF04C184BD36F4CDC65E0C81CC320B8FD5388031CF02C5E93F + 5D938C9FA19FD0F723C1AEC9F94B909BF6E33585BC36D4B5C12F654361BB4CF8 + C6B87FEDB9ECB761B586D4E6F3CAE703E74E283C3AFC733C6442400F2AC4A773 + DCE91E27E88FB965DCBDB1B7F8FF71D10581A3AFD1157C0E1AD23AFB0981636F + 600EE1B0F50385188CC5627E1CC6136E5FD1BA71049B1F3450B6239861A04DB8 + 688261F3A303969CC37EBA84195A9D3FA8D87B8161D04F4938FE7699133B127F + 8F9AB3FACA1905AE9198CFBA8684C6DF045DC054384BB6213A86775BE095FD76 + BEED70E6DA1EEE421F416F37E6D2BDDF8B3DE9D4378ABA82BFB1ABFDB2FF3A9E + B31B38B76B7C1F401FB48F30F8AA357E8D6E7F1B02D3BD6C42BE9B70EF84D3FC + F14997EC81C22E74AB08A009784387DA55BB09A10983593FDFA9E7B6E74227DA + 6D123288FEB3ED0A6E19DE9E40977D84B89AE79C809B607B88E19C4DA87B0262 + 02667B7ED0595B0C8BDF62416503870D246EC37BAC710113F8F2BD936D8E2B10 + 66DEF7DE7A09EC834E35A0845006F513061B00ECD77D52765A10D8AFBB5665C3 + 5D962375D3E67C8A7D04BC04A984C5F6DC1D84C06D27F0397742FC3A6AC4BF13 + 7DFC0112DA9402601DAB9680CE6AF16F3F8EF2B8EC5455C9CEAE2A09E8C278D7 + 71A852E5D35E8E7565E2DB5181FD8FE173AE02C7A62AF13958AE00D8B3B95C76 + E05C22CF76CA7E3CB7011D27C5BBA152BC1A8F895773258E5985B9F82C6C2128 + C6B505E4D14C0730D480EB857A5CDFE05A64730D812FAE411A791D43285C22DB + 30C6F6B67A038609843D9BB04753A5CEA16398A54723AEABB09E00D8BB058FA3 + BB0EC73AAEE39B4FF0FA2E17D77350751EAEED7271DD97A9E51602E0E339A8E7 + 614E9E6CC5F51E4B467FD9589923EB8F65633C57F7D8526DAE0FD755E4E31A94 + 3F3E2CC07510AE9B708DC4B0D52B4B98422457569764A9EB977097AEE2E574FB + 12FA1631BC7432EA49989B2AAB4B33644D693AE6A7C9EAE2745955942E2B308F + B0D7E4FF8D871264692E1DBF09B8F64E94E5F974049B71C25FDB25BC187542DE + 11001CA76DCEA13407B135BE20FDB0E5F48DB6CA432A1BFADA00989A0B8D8485 + 3E20B3E3F7E1BE639F9907CD49D8AFE077666CA48ECD89DB27B30F47CAAC98BD + 50A4CCE15874B84CD9172A93F7EFC63D4C8482DFA90723702F1325D30EEC9529 + 51611A265AE504C0917BD4093C95AE6086880EDF837B240AF3210260825F3A7F + 595274037F111C2C13517EBD7B8F3A820984150AA3AEE1A4834C2869E6059E1C + BA47A684EC5100ACF98677B1C4DC20BA8C718CDDD83F78B74CDC192CE377EC54 + 2730E1EF04D67D77CA27DB718FE9ED2F9F78F9E1DE30403EF3A6FC208694F653 + B1FEA997EF7DFAC472067F087DB415DAE2A9FA9061A337120EBBC9079BB6A93B + D876086BA8E80D5BE5038C8D63CEE1B59B707F8C7BF7D51BE54D0B003B8130E1 + AFC9274CB730C60986291B002F5DE782C14ED13DCCF0D104BE6F520B57CA5B0C + 276D3B842D08FC8A3A800D007E63D63279638E1141F02B3308800D04362098F0 + D78495B6E130D7BD3C6D81BC043941B0D6357CF40279718A15367A92D14BA8BF + C43A61B0CA848756D83B1604667BC248A8689676DDCE1FACE1A22DDD07821D7D + CEF0D07FFBF82BA34F26CAD30C0D4DF7AF3A824D5868DB15CC3E42E0273E9828 + 4FBEFFA53CF9DE04D513EF322434A130DB2CD91E71068F68BC2824B6C0AF0D82 + 5DA542E14FE589B70D0066DF53EF61ECDDCF2CD8FB89F6518FBFF5B13A8355AF + 7DA0B097E097A21BD8F47D6839830D04663E606A2C47B02DBB3D1AFADA6DBBEF + 2F2FBD0BBDA3BA0FFEBE3C1216DA09810980C7D2C3E0EF6870CBD21E77815C7B + 9C22F4B5356A5CE7587DDA8F39A301B0BA93ADBA197B1D1A01B90F920D80C71A + B3E504C0F781DD0768CCB904BB4ED97D8E75AEBE31BFBB77C8FEBE1F72B200CA + 06C08FFDED5597D81E3DEFE17A714C8D0980291B00739E3295DFB2967F564E76 + F3EF948B6D8D017FCD18CECF9AF39BB68BA5FD73B2599D8BDD5950D300E007C8 + 9E3B6ACD6FDAA3FB1FA6D16BC6D258EBC6D2586BFF5BF52701603B04F4638F3D + 2D7F7D9CF0F7496CFC24FEA39FC60B0507B25CBF0A805DF0F77F0600BEEF4564 + B57F97AC17B4F995C38846F7D92FF8B1FE80FE1DD2633F00003FEC0D6464ECB7 + 1AFB0DF1FF39D9BFEA196B8CB2CF79CC0F8CDFF4E1C3E81F6914C07DA86CE8EB + 14FAC782BC63C90E01CDD295C3D702C08F3EFDB6A5771500B37CF2F90F54AC13 + 00FFE549EC63C16213029A10197D5C67835F3A80391FF54746415FBB4EE84B98 + 4B11E412F62AFCA54398732DC0EB9C47F04BB9D6514E00CCBCBF04C0565EDF11 + E102CF017A6D19B7EF67AEB1C75E35B0F7C1E097CE5E8674667D04FA1AF0FB85 + 8676667EDFFB0130E6BF850B59270CB6EB2E006C5CBE74FB5204BFAA7153B49F + A50D7A473B7B097E9F51C04BB0CBBCBD06F83EF9D10C5CA4CF447DC4B9FB7B64 + 435E97B3D7A11726E086E44BDC984CC40DCD57B889F96A91D8E0F779D6272DC1 + CD09F4F562EDB79DBF2F4F5E2AAF4E5B21AF11E24E5EA6219F09763F5AE62113 + 3704C87B0BB79830D053961B77EFEC75F2EA2C68A609DDCCD0CCECB7DDBFA66D + E5EB9D4300BC415E9FB55ED7D8397C5F9FB3113768A8CFDB24EF2DDE26EF2E82 + 16E3C671B9A7BC8FE3BEBD682B6EEE366BF90EFA2985C00B3729E425C07D7729 + D6D0E54B10CC3EDBEDCBB0CD16C0D5F0CD569B32F97A3D316660AFE6F15D6DF2 + F712EE12F8BEBDD284787EDB72FFBEB776BBBCB76EBBBCBBCEDB946BBDE5EDB5 + 5EF2CE7AF413EC42EFAFF7C5980FC698D3D71737CA0CE3ECA74EDF712CB7FA9B + 728BBF7CB8354065E7F1FDC83D40F58907DADB76CA671E0CD3BC5B3EF70955B8 + 4BA84B87AF3A7A51FFCCCED9EBBBC7B87903C2E5F38030F90C1AAFF97923E5EB + E07DF215F4C5AEBDA68F0EDF90FD327E5724B417F57D32292C5A2647C4C8A488 + 43F255D841F952DDBDE88B3A2C9328E6E4C5F8B4A838611EDEE974EE46C7C994 + C3713235365EA6C725A95A8C23320000FFF4494441544B979A1E87F1D8049911 + 9F2C7392D2654E62BACC4DC990392969C6B99B9C2EF352336536C7923350CF42 + 897A4A8ACC4F4F97059946F333E81AC84099A65A88BEC5D9CCC99BA1758E2DC9 + C99165790CE19C2FCCBD6B6B6571A1AC2C2A911585C5B2BCA0509652F979B2A2 + A800C2587191CE59535A22AB4A0A657921F3F932271B5DBB85B202251DBECB8A + D05F942B8B0BB25066A32F579615E39874411CCD957574EB5614CAFA72082543 + 36AFB34237B3BDBE827586F2CB91B5A8AF3F5E241BAA8A657D6591AC45FF5A86 + 76AE2A72395B371E2F940D9585B2056DF7BA32713B79047582CF22FDC28F5FEC + 51EA0C39CE2F080985F3647355AEB8D518F0EB515B28DBAAF314F86E3C9AAA8E + DF6D5559B2A93C5DD61D4D930D473365FD913459539426EB4A30A73457B6E09C + 371DC5B994E5614D896C602EBBB22208E78AF6C68A52D978ACCC94D066825F48 + 812F21F1F132D48F60DD11EDDF4AD05A598AE315CADAB202597B94EE95129C67 + 99CA4D43301F53C731BF90DC7CBC44B612D4127CAB4A31872EDF32886EDD127D + 1EB65563ED09A36DD574EE1E73695B35C135CE8B201763667F4275C2E0727505 + D3914C98EB517B02FB1FC37357AAE7C973DFA82AC5712A655B7DB5B8D7D7C8B6 + 3A96D55A32F73161B07BFD49F16EAC836AC50B73BC1B4E2AB4F5696D94EDAD0D + E2D178D2720CD7CAF6F626F16CA91737F41100130813007B5AE1A429BA80FDE9 + 78ED6A959D9DAD12D0DEACF9847D5A1A65475B9BECEA65FEDAB3D069CCE914FF + CE76CCEF103F28A0B75381ABAAAF5B76F5F74A487F8F049DEA16FF1E8682B6C2 + 42336F704FB3EC64684F2D5BC4BF1BFB77D66BE9DB51877339A9A56F87E90B3D + D3097549507F1BD46E95840784B074EC7699B0A5677A2598B92B070CFCB5812F + 7300470C0F49F885213970EDB2A4DEFB5ED27FBE2707AE5E959033A731FF14D6 + 0E601E1EDBF92109193A239A6B7388E15DCF8AC9BDD92F211A1615E771BA5B8F + 15A46098619FE9FEED73397DF760AD963836E1F3DE4B431279E99C2A8AF0F9EA + B0445D3E2F9197CF49C4A5410963F86794FBAE610C629DE15E59EEBD3C241117 + 51A2CD1CC284B011E77A2562A857C2A13088006BDFA5330A72193ED72E35AFEA + ED8B1A3E97309880CCE8BC0259C2DFC3B7AE4A2C81DAEDCBA85F94989B17249A + A0ECEAA0300C35C5B0BDEC3B7895B0EE02C6087DCFCB7E9CFFFECB987FF512F6 + BF227137AFC9C1CBC30A86E36F5D53084CF84BC5326F28D613001BE0C73D8DDB + 370E224424A8A5FB34FE1B9C0781E1DD4B0A81E31982F9BBCB92F2E37549FFE5 + B664FCFD0ECA5B463FDD143A70E9084EA593F67BBA71AF5840D8B872D5A17A0D + CF0F8133A1201DD157065484DE87AE9D55D9009CF598EB848BA6CD7ACC753C0F + 0AC2B1C70D9E27C129DDB75771DC1BAAF41F6F6999F23DCE13F58C9FE8FCBD2D + 69F76E4AE23704C0175574FDE6A03F1373337F346E5F1B0013FE12F8DAF09732 + 4E60BA80E90E36CAFC196B7FB92ED9BFDE909CBFDF90AC5F31FECB353C1F84C1 + 57511A406C43E0A4EF087BE9D865AE5D025BF35CB33FF9FB8B38E78B0A91E926 + 2600A6D3D8E574A63B98105981B081C1DC870E60BA7EF5FF10753ECFC6656E64 + BB821512E3F93273B8DF6555EC6D13EE595FAB370987F17AE40F162C11D8527C + EDF2359C88D767D2ED4B927CE7325E5F7CDDE1FF87605FFF7F189E7CE4FFD1FE + 3FA4F8FFC59261ACCD387FE4700A7F4BBD1235DCA761D2E9048E38DF63DCF496 + 433812FDEAB4C7FC28FCDF475D41FF55E832E633DC3AF3655F66C8E61E05BC7B + D5AD0BA1BD1FFD07AEF6612DEAD0C16BCCA7DD87F2149E9B7E15EB07AE32F736 + E65CE9C37385D7DE753A947BA05E5D6F843574C713AEE31CEC70ED2CB92EEA62 + 8F445CC0F1A148D4F7E1D8FB089A87BBD0DF2161E7DA5176620CEDE14EBC2FB5 + CB1EF4475CC4D830C1700BDEBB5A65CF10616F93049F6EC2FB62B3EC1E6C35FD + E7DAB007D60D75A01F73CF731D73131330B74BF0600BDE831BA03AD5AE330DE8 + C31E581B74B65982061AB16723D672EF1609A4EBB7BF4942CF62FF411C137D04 + BCECDBD9578BCF856AE804DEFBABB41E78EA24DEEFEBF15941606B9CB8AC53FE + DDD5E2079935D56813C6724E95D677741E57718D2FD6D2914BD72EC702ECB984 + B9180FECAE510574A2BF1D6BDAAB6407E6FB42FE180FE0DAD672F16B2B17FFF6 + 0AD9D9516901E04AAC39267E04BEED1847E9DF5981CFE132F16A3922DE84C168 + FBF554E273F79878B5E19AA1A148B6371D95A8B3ED72E07C97F8B5E2B3FE6489 + 783694E2F3F618D6605E4B05548ECF6F5C5F34620D4470CC7EF7465C1FD4E27A + 0DD75A5B6A713D8275DBEA71CD73B250DCEA8B317E443CEA4BB15F19AEDF4AC4 + BB8961A68FC9D65A038D3D9B089A71ADD2806BA45A5CA74004C684C59B707DB3 + 15FB6E3D49C09B8D6B956CD95C9D83EB3CD4718D47004C104CC0CBF6D69A22AD + 13FE120CAF63BA8F6339DAE77692115D708DC9C831B8F664FA10CD155C946E85 + 89A603381BD7BFF787985E55928EEBE31494A9989B8CEBE604595192A2A1A8D7 + 9665C86AD457A27F456132AE9993644501216F922CC98D97C539712E2D2738C6 + 3ECBF2097FE9F68D55104C284CA730DDC18BB2097DCDD8C24CAE4BB060322130 + FA21964B0887330EE1BEE180C2DC79160C5E9816230BA079C90731B65F21F0FC + 54C2E18398B74F662746C9DCA403061A131827600EDA060447CA8CC37B15FACE + 4BD827B3E2F6CA8CE83D32ED40A84C8D0ED3B1E98723653A1DC2319132F330F3 + 00EF931907236556F43E750C1302CFD88F725FB84C8D0A5308CCD286C0D4B4C8 + 0815DDBF74FDDA25A561A0F7101C47C8D488BD32393CC2E413DEBD473519D2F0 + D121A13219E5348C4FC73CB6E916FE2A740F14261343306777984C0E8B908998 + F785BA8603712FB8CB6867904C080C41192C9FFBED924F7D775A0A441BF7983B + 02E4F3EDFEF2A9F70E750E7F82F213CD316CC030A130730B7FB8CD4B3E70F394 + 0F213A830985356434FBB77AB8E0B0ED106608E90F19467A13EED9D76D9277D6 + 6CC27DF31679672DEECF517F9BA1A2D967D50987D9AFF3D8B76A83BCB97C9DBC + B98CB983ADFCC20C2B3D0A00BFB97835B4C6EA23245E87FEB5DAF7C6A2D5F2DA + 8295104130347BB9BC3977A5BC6EE9B5392BE4955974072F9397662F41B9549D + C204C604C72FCD32B983A917097919F679C6620384A72F54BDC850D11021B082 + 60CCA1B48D9230988E603A839D0EE16727CE52B1AEE38E7CC163C906C2CF5336 + 142604868C53781AEA23E1A39D61A3FFE672064F96A7513EF311EA1F7FADB25D + C34FA2FEE4875FC9DF3E201CA64318E5FB5F6AA9F57174094F9427513EF1FE84 + 51FA529E644918FCCE4848680380599ADCC184C49CA3CEE1F73EC5BA4FE5B177 + 3E9627DEFE18E304C374007FA0EDE73FFA525EFAF82B795C21EF3879F22DCC7B + F323F9EBABEF6BFB89373F34F5573EB80FFEDA30D8EEB301B0B3ED04BE2CEDBE + D100F89197DF55D115CCF2B197DE93C7197ADA01801502DB60D80184C702BFCE + B6B3DF39361AF03AE1AE530F1A1F0BFEDA7A10001E0D7A9D6EDFDF0B806930B2 + E502BBA3F4A0FEFBC02FBF8B77B69DB2BFABB7BFD3777E87EFECB3C4EFFE9D7C + C0E6050F72003F8C258C884CE1B7FA0DF81D2575002B47199BC5FCB372B29B7F + 9B46015FAD5B46489B8FD9FCEB37EDB198DA1F9093D9A90834C7EA73EA1F8DFF + 7769F4711FA4B1D6FEB7EA4F04C0AC3FFA28EA9A03F8496CFE379416E875B881 + FFA701E0D11AEBC5F65B3D8FC7C91733FEF01C7F0463C97EC18FF907F46F901E + FB1F00E0B1DE4046FA7FABB1DF10FFE7CA3E67FD40C09BFB7DFA4D1F3E9CFE91 + 2C88FB7BE484B90FEAFF3D1A0D800DD03500F83102DCA70C047EF9AD2FE5DD8F + 67C88B049F84B74FBE8173C67ACC7FEC59CCE505013EF81F7D161728CFE1A2C7 + 76FE620F1B00BB1CBD2C097751FF0D00E638A47321DB19ACF3ACB93614B6D7D1 + E54BE8EB04C023AEDF070360D69D220476C902BF0F06C006F83A9DBF74FDFEED + 9D2FE5E9F770E1CB1CBD6F8D006075F95A6E5F53377DCCF36B877E7EFCDDAF15 + FCB23475EC61C1601B083FF3C108007ED282BF94E6EB85D4F9FB215DC0B354AC + 3FF5D14CAD3FF3096E0C3E1D1BF88ED668006CBB7C9D4E5F025E1BF2BE80F279 + CAAA1302DB62DBEE7B790A6E88A62E97E7272D13867C36219CD72BFCA58B9739 + 7E4DB866938F97219BED10CE84BEECB31DC06F603EC338DBB0972581300130DB + 6F2FC09EF3E9DC457DD156F9608597BCB7D45D212FCBF7977B689D00F82D1CDF + 86C1AF2FD868E0EF92AD1ABED976F452767EDFB796B929E865F9A6251BFED2E1 + AB0078B5C9DDAB2098AE5F0BF46A2E5F82E0B5460CEF4C11FA6A08E78D4606FA + 9A72DCA61DAA0F36F9E326D70737B3DE98839BEA6DB8D9DE16281FBB05CA07EE + 3BE5238F5D2A866866C9BE4FDC8370A31D829BEE20D567DB83E513AF20F9DC67 + B74CF08FD0F0CC74EB529FFB87C9A7D06756F939A1EFCE70F96217E16EA47C19 + 12251342508646C9D77BF6CBA4B0033269CF01F96AF73ED5D4C81899B62F56BE + DE1B2D5F451C9429FB0ECBCC4389322B2649A65B619A19BE79FAA104997998F9 + 7613657A7482B667C525CB9CF85499159B2233087D93936576729A025CBA7619 + A279764A9A09CF9C9E29F33332B55C98952D0BB373644166B62CC9C985F26411 + FA16A3BE2CBF5096E6E6A29E258B73B364491EFA512ECAC994A5F9A86BDBF42D + C9CBC15C3A7E99BB374396A1BDA220CFA8281FCA93D54708768B15EC6A5E5E94 + 3A66CD5B5988B230579617E4C8C68A23B2E57885AC25F42DCA915547F2542B4B + 9847CD94CB0A71DCFC34CCCF90954559989761B92632711CE660CB96B5477315 + F6F28B367EE1B6EA4896F66DAC2CD490CDCCE1BBB63C473654E62BC8A48377C3 + B13C0DBFAC797CADD2387A0BF4CB3DB793450A7B6DB9F14B46C8768250FC4270 + 1BFAB6D5321F70AE6C3E9E2D6E5539B215A51DEE99DA5C912E1BCBD3341FF03A + 7E99570A9564C9BA6294783C1B8AB3646329C342E31CCB8A305E246BF03C1200 + 13F812F46E4049187C1F1456185C6A80EF318E15684920BC9E8EE8123C9FC5B9 + 78DE723086E7E218DDB647C5ED44B96C467DFD51BA8E8BD47DEB5EC37CBC74E1 + 62CE891275F87A375488577DB9B89F2C834AC5434336635E35DA0A80ED36E16F + B98263C25FF7DA4A85CC7425D3914CA7324B8F0642DD132A3A7A379D38261BD0 + CF7CC7EB8F3154229D3A74F2D295532F5BEBAA654B2DE6D7D78A7B43BDB8D5D5 + A25D2D5BB1968098797BBD30D793B0B7B551BC5BEA31B706F3B03FE171C349F4 + 35C876685B63AD8682F664586886846EACD1FA76C260ACF3EB68965DDDED12D8 + D92A3B5A1AC5AFB559FCE8FEEDEA9490BE5E0986023A3BC4B7B54576B4B5CACE + EE2ED9D5DB23BBFA7A24F474BF849D6548E57E093ED58DFE2ED1DCC184C103C6 + 51BBEB9401B8817D463B7BB10714D4DF21FEDDCDE2DD5A2BBE1D0D5AA73867F7 + D91ECD1D183CD0A9F358063327E569F49F1B50F8CA3AC33BEFEAC771077AD037 + 20E1170615EC465C3C2FE1C3E724F222E1D53792FCFDF71271E182049F3E8DF3 + 1A90E033A72574D000E0DDE70625E80C43BBD2097851A2AE1A284BA86BC0AF71 + 192B703EDD2BA1A887E11CD4DD0B859F37B9877733B730FB14E49ED37DF65D1B + 9603372E6919796948A22E11B65E90288C475E441BE53ECE7568EFC541DD9779 + 82A39837F8FC8044529A53B8DFE4143ED7AF8058C72F9C967D97086E995FF582 + 30B4AE09B54B384C87B009BF1B73F392C4DEB92E7177AFCBE1DBD7E4F0ADCB12 + 73FBA244DFC4395E3F2FFBAFE1DC309F259DC9FBAF9E57E734E1F57E9C33B5F7 + D2799CFF65397C03EB6F32DCF3658986126EDF90A46F6E6988E8D89B572401C7 + 88BF73558FCBF3506726DDBEB7CF6BEE5C7596DE39AF0ED4E41FAEE29C087DAF + 4AD2BDEB927C0F7BFD80F3FCE6B22ADE5212C693BFBB86F5575DD095B0957DF1 + 772E61EF8BAAC3047FD7CF4A8C050747C25BF7BB60214BDBF56C8343D6090939 + 167BE31C1E930983CC90C40AD75DC7B884639AE3F33C4600F01DD7F9E8797E73 + 4541703AC633A0D46FAE4A3A1E57C64F3714FE3A1DC0940D80357F30E1EF4F58 + FBE36549A3EE5D822EA27D49327FBE22D9BF5E53109CFB1FB724FBEF37D5199C + FEE315CCB9AC7037F19BF338DF213C571754F177F078EE9ED3FEE4EF86755E26 + D6B0649B6B58728EC261ABDF5E6F83E0B85B43F8FF1D52D736012F9DC0B1160C + 66DB805FE31A669BD09CC09EA03E1EFDB137097F3117CF6DDC2D86DA36F98635 + F7F08DF3DA3E8CD7CAE1AB43128F36157B6D107D67F15A33509F40571DDEF83F + 36A1BA097C09804D9BCE6FBB4DF1FF3D6AB817FFBF7D0A82F95AE0FFF5DE733D + F8DB328A38D70D754924FA2287FBF0B7D98BBFD55E05ACE1E70943DB15AC869F + EF80DA2562B81D7F0B1DF85BEDC09C0EFC0D77C9812B5DB2FF72A71CBCD28DF3 + EBB3C4D7573FCEC9E8E055825ACEEFC6EB7440B5EF5217FE8EB9BE07AF5BCCBF + C1FE332A02E2835771CE57FAB4B4EB5117BBB1AEC7D5BFFF0ACFB75BF7DE7BA1 + 1DE39D78AC9883F3E2B95251A8475EEA441D732C855F68C37B57AB96E1E7DBF4 + F1A81426E3BD77A011EF772D78EC1658C6E3DF73AE05EF878D785F64B4867A09 + 463D74B049154C187CBA01EBEAF09E0CF5D3DD8B79D887C0997BB0BEB3AF0E6B + 319773FAAAF199725CFC3B8F49404F95849CAE95DD83DCA756FC7B8ECB8EAE0A + CCAFC23E2724A0F7B82AB00F75CCB5E5CFF5101DB92C47FA19B2B9420250EEC2 + 5E015D38061488FAAEDE2A1C17F3096B3BCA5D636CB37F27412FE16E7BB9F8B7 + 1D35427B27CE73178F4100DC5AA63098F37DDB8F8A57EB1195775B9978A3BDBD + BD4CBCDA8FE073B7583C1B8A25A00DE7D1C5F0CF65E2515F229EF598DF582ADB + 1A8E40B8F640BF67B3914753A9292142DE4D35B8FEAACE93ADF545E827ECE58F + F4F2710D5624DE58E7D3528ECF685CB73494A28D6B98265CA3D4615F1EA305D7 + 292809947DDA8F2B6466DBA3E9986CC6F51E1DBDDBEA0C04DE52938B6B145E2F + 12F6F29A90D788A6CE6B3F1E9375868736D785C6F5BBAD963F1EC463A1A3983F + 28B4AE4B6DF0BB09D7A41BB5CD9CC1B82EC335E1DA325CAF966528005E5D9A86 + 6B5796A9B2F248B2AC399A86EBD74CCCC5B523C6569724E37A2E4961301DC1AB + 8A53713D4DD89BA4C077798101C414DBD4D2BC0497088B97E426B8E69B76BCD6 + 152C171028C7C9A2EC385CE7C7C952948B330FCB82B468D5A28CC3DA5E9A8D75 + 59B1DA373F753F74106331E83B2C0BD30FCABCD47D3237799FF62F483928F393 + 4D392F89E1A0A364767CA4825FBB9C151721330E8743110634271F9259F1FB35 + 94B409276D20F09CC30764760C730647AA982FD886C02A05BE26473045A7F094 + A84899BA2F4A2613FEA29C71E000D6F05E0CF5FDD80B9A1615255322F62A04A6 + 268785E3DE0D25EAD35027FC9D1A1E2113E9120E0DC5DE91F27518EE0DD19E10 + 122A1377EFD1FAF8C02085C2D3F646C9D408CC41FF97C1BB717F182AE3E90E0E + 0892F1FE465FA03E216017EE2B19121AF7AAD0A7BEFE0A853FF5F1377D5EBEB8 + 57F5918FDCBD559F32D730FA9957D886C31FA39F4E61CD23BCD553C345B34E7D + B005F7E51BB6688868BA860989592708D6D0D1EBB7BADCC1ECB321319DC26FAD + 588FFB71E616DE8A7B72DCE3A3FEF60AE61166B86853BEB50C42F9A6D5E71A5F + 8AF54BD6C99B8B0983D7CA5B8BD6CADB0BA1056BE48D05CC29BC4A5E9BB752A5 + 7078FE4A7965DE0A79792E730AAF90D7A0976611082F5348FCE2B4C5F2D2F4C5 + F2FAECE5F2C69CE5F7858F7E91B24030A130E1AF5DBE603983E910B67308DB10 + D8290260A79E6348685B84C3941526DAE50CBE2F5C34EBA6EFFEFEA9EA105650 + 6C0BED67399FB09890F89349C2DCC184C07FFBF02B799AFAE86BD5331F4F92A7 + 1C60782C110ABB9CC3EF1BF7B0ADC7DF19AFB2EB2E00CCF2DD4FE4D1B73FD292 + 00F8B1373F543D336EBC4CC3FFF5C68030F970EA0205BD7F7D7D9C3CF9365DBF + 1FA8088309826DB8FB208D06C34E00EC84BF7F79F13D9513FA3E4AC06C49DB0E + E8EB74073B5DC163C15F5B36E8B5CBD1B2A1AE2DE7D87D4EE051F35C0098634F + 3364F3EB321604B601F05F9E7EED3EC0FB30006CCFB1EBA3E504C08F3EC3EFAB + F93DF72B2310D701731F08809DE277F18E35BF19A3ECEFF4C7FA5EDF16C66CE0 + 6B83DDD1E5E8FAEF1399C26F351AF8FE468F8FCD59FE5539F7FCB7690CDEA500 + 1832DC0BE766F1AFDFB45D2CED5F938BDD39C1A683E7DD27E71C7BDEE8BEFF0E + 8D3E8F0769AC75A3FBFE54FDC9009821A0FFF228C12F45103C027B3514F4FF1F + 0060BEB847B7B56FAC3FA07F83F4D87FB203D829BE19FED1374FCEFFE36FB8FF + 9CEC73D4BA2D1CFBBED2AEAB2CC8FB308D76F48E2507E8B55DBC7F445CE35CF7 + 3000AC0EDEA7DE91C79E794F5E797BA2BCF7C94C2D9F7C1E17379867CF27007E + 54F33E8C0060E6FA7DF4395CD85820D7057F29B6C7CAFFEBD0EF05C08FBE68AF + B91FFE3EF6322FC63EC6C5D76F01F068E86B971AF6996D47BE5F03776DE84BD7 + EF17567867F68DB87F15FEBE45C03BE13EC86B835E5B04C314EB06FC1A3DFE2E + 650030F5E4FBB8581E671CC1EA0A7E1F17D1E370716D0160825E0260678867BA + 7E6D57F0D39F18E04BD930D806C09ABB770C11FC3EFDF928004CD7AF057D6DD9 + AE5E1B02B37C1EFD84BFCFA1A49C10580130EB9397A2BD54433CDB619E29D65F + 9CBA5CEB2ED04BF04BD83BD3E4E77D6906E6A27C65D61A88AEDF75AEBCBC9AB3 + 77CE5ACDDBFBDA9CF5F2FADC0D1AD2F9752B772F45F72E4335BFBD688B02DE37 + 6CC00BBDB9708BBC01BD8931F613FEB2FF9D65C6DDABE19C577A681EDFF75779 + BA72F6AA8B17E36FD1FD6BB50978C7D19D4B37EF1A2FF960BD8FE6E8D536C33B + AF61CE5EDCF06E24CCDDA1C0579DBD0EE8ABB0778B9FEA4337E3E0FD785BA07C + B46D27DA81F2D9F63D3235345ABEDE19851BE510DC34076B3EDE4F7C427183BD + 1B37D8B859F70B17BA77998B976097219A3FF7DB2D9FFB87E2C61CED9D61F2E5 + AE48E3D80D8E723978BF088A9409E89BB87BBFE6E1FD922544D03B25225AA6EC + 3D245F871F94AF220EC8A4BDD132352A46A6EF8F554D8366C624CAACD8649971 + 385166C626C99CF83459909421F312D3355C33FB66C7A7C8BCA47459909229F3 + 9333644E62AACC4AA0A33755E6A764A03F4B73EECEA31B373B5BC32FCFCFC2DC + 4C8C6567CAE2BC1C59929F2B4BF272D579BBB400424977EEF2C2025975A45896 + A1346EDD62598172696E16E6E428C45D56C0B55928732CE56AFF0AC25B0BE22E + CDCD96E538C6AA22EC07AD3D4270C92FBC8E28A45CC9F945CC7186B218FB16E6 + C8F2826C1C234396E4A663CF4C85955BABCAB0AE40561FC991D525D9589789B9 + 1938473A24082FD9CED472797E1ACE9B2E8874280DFBA761FF345973244BBF5C + 5B57863D50670E3586D4DB509E8B63E4CBC663795A67C91C6F9B2AD10F69DE5E + BA798FE588E67383E8E2D8741CE355C6FDA15F0A9E2C820AC4AD066B20BA4336 + 3234E0F12C71433F0130E76F42DF964AF4B11F22FCA50B787D69B26C389AA6C7 + 59576EDC1F3C3F85C0D0FAE20C5957982A1B50DF5466E02EDDBCF70160B46DE8 + 4B006C8BFDEB8F16E33920ECCDD1722D9DD44539B212FF77AB0BF36515FECFD6 + 1417C8FA23F8FF292DC239612FECBDAE245FD661FE66ECE15E552E9ECC354C58 + 5B7944B5F578A96C3B7154B6D1395C518CC78379D5E5E2C110CE96F398DA76A2 + 422130EB04D25BB0D7D613C7C4FD64956A4BD531713B715CBC9BEA84EEDE2D0C + 514D17706D8D6C86D65757C9068C6FA9AD13F7A666F1686E11CF9656D95A5F0F + 3588576B9B6C6FEF405FB3B83535885B7DAD7836D58B4F7B8B78B7348A7B43AD + 3A830998DDEB4EAA63986E61C2600F3A85715CC260371C9BF097DA527B1CED13 + 183750DA136BFD14FC368A0FE6530604B78A7F5B0BDA0D2A6F8685C6791006EF + ECEED4D0D00C111DD4D723C1D02EBA827B3AD4051C363820E143A725F44C9F04 + 13D29E6A57B01BD043077093F877D10DDC2E7E7426B7E098ED8D4227B25F4713 + D42821FD9DB2FB34F63DD5817A978412FE9E2290E856F8CB5CBB817D9DB2ABAF + 4B424EF749C8D953C27CBF21670724941A3C8D7967D03E2311E72F48C4858B98 + 7F4A76F6F5611FCCED1FC05EA7D5111C3A784602FB0978E9E43D276184C817D8 + 8F790306FEEE1EEC97DD84C467FB256C6840F65E20783D239184C6C34372E0D2 + B044A2242C8DBE7E590EDDB82C07AF5D92A84B84A8C372E0DA453970C5844FA6 + F65D3C277BB196E5418C475F1D96FD740C5F1CC43ED813E5812BC675CBB682E6 + 7303B2078F9DDAADE7740AE782364AF6D9503AEAD210D631CF2F4AECB18FFB5C + BD2007AF5F9443B7AE480C14CDF3BB8E635E3B2F51578764EFE5B3D020DAC33A + 3F0C8F9F207AEF458C59A09A003BEC02DBD8EFCA25EC43C87B4B95F4CD1D49BF + F7ADA44104C189DFDE541DE6F16E0C4BCCED0B26CCB3154E39EE2E5DA2174C18 + E27BD70C00FEF69A82DFD49F6E49CABD9B0A7F632DE89A8071C25442D5A46FAE + 4A2A81EAF73724E5BBEBDA47C7285DA5876F107E0FC9C1ABA7F1BC3297319DBF + C6E54B07326121A16E3CCE47F326439AF316E21CB6098319F69950966E5D82CA + 5888E740472F5DABF1778C1B38EB97BB92FF5F3F48CEDFBF553730E17422E625 + E1DC592670DEDD4B9282321525730DA74284A9CC374C772F816FEEDF4DEEE09C + 5F6E4AD68FD724F3C72B92F113E6FFC0BCBF04B73CA74149F97E08CFF3051DCB + FAF90A8E7B1D6B6FAA3338CB82C004B884B684BD2984BB0A72CF63FD7949467F + CA371770AE84C957B03FCECD9A9F7CF71CCE6908F3CF639CE097D018C7BD83E3 + 626DDA77780CD82B5141B2F9BF3361A4E9C03621A619969A503F410130733B63 + DE7797705C3CC70CF57D93CEDC413CA784B684B90CB37D461298079AA1C1AFE1 + FFEA4ABF445F3E2571D7CE4822E7D2C98DFEB81BA751F66BDD56DC8DB3BA96FB + 1D429BFBB18C57506D8E7598C7C19E07E8A085A2E9BEBD724A62F01A89C558CC + 558CD1497BA14322873A2462A8534167D47097445EE8C4DF582BFEDE9AF177D6 + 8CBFC116B49BF13ED18CBF835689BCD886F78116CC47FB422BD6B4C97E0AFD07 + 2E75E07174E3F1F4E0983D5A1EBADC837E02DF4E9C4FB7445FEB5511FC12181F + E45C42633E56E810DDC3681FBC4AD0DB67D54FE17DA3DB82C0DD784C3DBA1FC1 + 6FC4F9369C739BEC1BEEC07974629CFDE81B6EC73C039979DCFDD0812BDDD8AB + 47C573B0C7B9867099C7083DD3A88F3D6CB0157BB75BC768C5E3C7F3608940D8 + 86BF21A71BF03C35E9BAA0FE3AA816EFFDF5123CD080BE668C37A1AF1EEFDB84 + BF4D3A3F64A0169F1FD5F81C310A457BF759CC3F5D87B535F84CA952481C08D9 + 50977DCEB60D7EFD08705172FEAEDE1312C8B50A7A8F2BA42564A663972E5EF6 + 0512E412FC123EA3F46B2F57572FDB819813D889B9ED47F179588AB25C82785C + 0B0CDB70D8AFED28D6411D580B11FA7AB710B65A6A2911AFE652F1692E13DFA6 + A3B2BDB154BC1A4AF0598A7A539902602F42DFA652F16EC57A0D196D0030FB3D + 9A0871710D5257A84E622F9C0B5DC604CAFC119E677D89EC68E5F9104A57E238 + E5EA2AF66D3D26DB5B2A140AFB60CFAD7504C747F1FC308F7295BA8EBD5B71DD + 537F4441AEBB05802905C04CE5515368606F753EE6301A8C01C284CE741A7B35 + 1E55E731E1AF07F6210076ABC1F9425B9977B8AA48DCAA1965A50475465C29C2 + 7532AE492BB21500AF22C83D92AAE0773D612FB4AE225DD61E4B93F59519B82E + 85D0C7E831EBCAD05F9AE62A0D04C6757011AF8B5350A708850D00B6FBE91C5E + 5E40E03BE2105E5198A4F5E50504C2F15AAE28A40C3426005E8EF9CB1404C76A + B93C375E56E627E1BA127B1222A36F71E621996FC1E16539090A8D17A61F9279 + 2974001F948569B1EA1A9E937850C341D311ACAE60D419269A00786EE23E68BF + CA84953EA4A1A25571FBC5CE274C184C003CEB10D6C51897F0F4FD615084CC38 + B05766303CF43E3A7D097E2364EA3ECC39B80FDA2FD32003820D149EBE7F9FCC + 3CE000C0CC13BC678FE6079E1416AEB985A9A9111132253CC2C0DCA010F992E1 + A1C3717F18B647E1EF5784C611E1B817DCAD6353F746CAA2F8445914978075BC + 6F0C9109BB42E42B86880EE2FD25EE5177042A049E68E510A61BF813BA813146 + 073121F0273E7EC611ECB5433EF6DCAEB097AEE0F13EFE32DE7607D335ECE123 + 9FB89BBCC15A6EF3B6DA06087FB0D9388209893F73F7910F0981995778B3A7D6 + 194AFA5DE60EDEC070D226BFF03B6B36CB3BABB7C8B8F598B7C103F7E4B8C75F + EB26EFA28FFDEFB1BE66ABBCB30AF30886576DC23D3E6401E07751BE4338BC84 + AEE0F5F2DECACD326E25D62DDD286F2F592F6F2E5A236F2C5C236F2D5E27AF2F + 5825AF41AF42AF681EE1553AFE1ACAD7E719BD3AC73888DF409D7A75CE320D2B + FD1241309DC3285F99B958F5F2F445F2CA8C455ABEA421A3ED50D10BE5C5A984 + C2F3E5B949738C2B78F23C85C22F4E9AEBCA193C5AECA76C30FCFC971610869E + 9F30539EF962BA3C4DF8FB85710B5336081E0D83D95627B135870E6175FF5A6E + 60DB25FC140130EA0A89D9FEF02B97460360BBEF0982603A842D20FCCC875FCB + CBE3713E9F4C5117B181C40C236D398115007FAC2E60D61F7FEB23F9EB9B1FC8 + D3E3C6CB8C356EB2352852C6CF5D2E8FBFF1A13CF2EA7BF2C49BC6054C194730 + D65860F789D7D186ECB62D1B003F6AB97F9DB221B03A7F15FE8EC35C8860D901 + 7F6D11F23EFE8A294703601B0CAB3866015D27C4FD47B281EE83C6C602C04EF7 + 2FC71EE400B6DBBF0700BBC0AE551F6B9C7281E27F04802D80EB02C0638CB9F4 + B031CAF9DDFD7DDFDFBF2C8F3C698D5B226BB05DBE4EF6F0AF31087285DF6A4C + E86BC9B87FC7E62CFF8A9CFB39F5B0B13F432EB6F5278680FEEBE3CFBB34D6F8 + 688DC5F01EA8D100F44163CEFE7F24E7BA0769AC75A3F5A035A3FBFF54FDE900 + 98A19E6D08EC00BD3C90B3740927E17C12FE876BE40587171F5EC4CE10CFA3E5 + 84BFDA1EE30FE8DFA1871DFB1FBD811871FC8F6BEC37CD11F8EB7C23FE57357A + 4F5BA3E7DDF78141618EEABE7E7C788CD65800D8E1E6FDBD72025E6AAC31BB6D + 035FCA1E639FFEB2EB29F4690E5FA3BF3C850B83A77111F22C2E4C9E1F274FBE + F8A13C4177AFE606C687BE2DEEC50F6D7C383FF2EC3BB87030F096B241AFCA02 + C084BE04C384BCB65C7078949CA19E293BFCB3530C01FD17E89197702167857E + 26FCFDAD46C23DBB602FDAAC3FF19A81BC23E0D7D46DE86BC0EF0415C33B13F4 + 32A4F3E36F7E6E397B3186D2847A667D04FCBA1CC01AE6D9D27BBC5835FADBFB + BC78C545B215E699797D097929867C66FBC90FA7BAC23D537688E7D1F97B9FFA + D480604A5DC19F99B0CF76F867DBD56B03DEE7BF98AB62BEDF67C7CFC685FE2C + 7906E5B35FE086E04B8C7F395FA12FE1AE2DDBD1AB619E2DC06BF7DB10F8C5C9 + 189BB61C372018875E46FD95E92BE4A5A9E89B02594E5FBA799F6338684260D4 + E9F0250056C83B77BD81BBB3D76A49B1CF06BFAC334FEFABF3D6CB8BB3B16ECE + 5A7973016ED298AF773EC6D1FF1A4B88219DE9E6654900FCFAE2CDF2E652DCE0 + AD7077E5E4B5EBEFAFF272856CFE78C30EF97493BF961FAEC3CDE53A5FF960ED + 76CDBFCBDCBB9A87579DB97E320E733ED8E8279FBA056A8EDD7174E8BAED948F + A00F1992D912C3357FB41537C084BA16DC65DE5DBBFD91FB2EF9186DBA783FF7 + 0E912F2C77EEC73E21B8818E9085B199E253D120ABD38BE58B5D91F2916FA871 + EDEE8C90CF03F76ADFC490FDF255C8019918BA4F9DBA5F86E0662718DA1D891B + FBFD9A63979A187150BE8426EE35FA2A325AA610E646C73B1427330EC5C9AC98 + 04991963DA930F61CEE1780DCB4C703B37394D662624C9F4F844998E52C331A7 + 66C8FCF44C613EDD79A89B7CBB26F72EFB16661AD72E5DBC74F552EC5B929D2F + 8B7273656141B62CD1F0C9F9B2B8204716D1B10B2D2DCE936525F91A46796569 + 91961C5F5A9C8FFE02595D7E44569615CB92A23C5981F67202DFEC34599E9729 + EA1A8596A2BE2CDF684561B6ACC6DA95381EA1E25AEC6DEAE82F2204CE95F538 + CEBAD202A1AB746D29617126F6CD9095C57436646AB9343F4516E72669C9B681 + B51998C72FB2D2F58B3013DE2E19FDA9B2A61473981F8D7B60CE8A1CF4E727C9 + 9AA25459CB2FBD0A92D19F2CEB8F64C8FAB24C594318AC63A9B2AE2445432D9B + 5CBBE9B2B12C43CB0DCCBF5B9A8AF34C938D1519464719868F5FC0A5A8D65760 + CD31CC85365666C8367E19782247361D33D09732E3FC722E4B4CEEB77C71ABCE + 976D2772C5ED38C16F12CE230ECF55A26CAA4897CD95D9B2F9581EF6A6CB19CF + 2BCE93CFCBFAD25CD94027309E9755787C6BF17FB9AEA45856F1FF2A8FCF71AE + C25B3A82097BB71E2F5708BC0ECF379DC2ABF5FF0FFFB77959FAFFB73C3743FF + 2F97E6E179C5FAD5789DACCDCF9735D0DAFC3C577B6371B1AC2FC46B213747D6 + E4E5A25D245B8E946AB9A1A850D6439CCF396CAFCB27402EC26328932DC78ECA + A6F2523C97250A93B72834AED0F3E30F015812F83244B3675D8DB89F3C21DB6A + AAD0AE45BB56B65657C9E69A13E2D6D028DB9A5A65535D936CA96F92ED1D3DE2 + DB7D4ABCDABAC5B7EB94CAA7B34FFC7A0664E7A9B312D87F56FC7B07647B678F + F8706E67AF78B6768A7B43B3B8D735407506D0B6B6A06C5017B13A841B098C8D + AB98AE61B6097D098CD9F641FF76F4319CB48F5532C434C341EFEA6A939D1D2D + E2D7DAA40A686F91A0AE76D9D5DD21011CEB6E175FCCF3EF6C95A0533D9A5797 + EEDFD0815E89BA30A82208DE7DBA4F82E904EEE53A42DE66CD354C31FFB00F43 + 545B6D86B2A62B3910737776B7E97C3A8B83FA3A75EE8E4EBA845B553B706E0A + 80713C1E3B00F3FD702E3C87A053BDB2FB0C8E7DE68C86B10EE93F2381782E03 + 7B4EE13CFA714E83127A66508206CE48E8D921D93D3824C1A7CFA08D39FD7DB2 + 6B00E27121425FC26C967BF078146E33ECF3D953FAD8A2CE0F49F4E54B72E0E2 + 7989BE7249126FDF92946FEE4AFCCD1B0A86F75D1CD230C954249E13825F4263 + C2DFFD97B0E6EA4589B94E27EDB001C090865EB65CC304B104BB04DF141DD084 + D32190DD47484DA732731CBB4252A3B4C7199A5A9DBCD72F4A340130C47AE495 + F31241B03B7C5643536B086AAB1D8EE3B2CE720FC6181A9B0038E2E205D93B3C + AC8ABC7451C36C1FBE7D4312BFBB2D49DFDF91B8BB37E4104340DFBCA265CCED + CB1ADE39FEDB2B28D1BE83C776F3AC44DF1E92D86F2EC861B4E910A683993961 + E3EF5E95846FAE99124AFA86B9706F4BCAB737341F6CE29D6B5A4FBC8371B4E3 + 6E5E92C3D78725FE1601ED2575EB1EBA312407AE3264F0592B2CB171A9D2B14A + 67AA5DA75335F5DE7575A9AA8315F3086DD9CEF8E5A6C24D4263F6A77C47607A + 4D12BFE13108372F4AF6DFEF48D1FF7D4F72FFF31B5D93AC70F7B224E2F1121C + C7602DF31027DE3C27297770AC6F862599F0FBE6205E27E724036BF27EBD2D85 + FF795BF2FF7E4B32EF5D91D46F8725F3C78B92F3CB2594C378AC987BE7B42AED + FB21C9FEF922FA2F48EAF7E725FDBB0B587349B27FBAA2623D0D7D29DF9C5370 + 9B7C77C825F6A77F3FACB2E7D8E2DCC4DB67554977CFE831A9E46F4C9BE3193F + E0F1FE4847F3059CC7B002E7AC5F4C886A03832F09F313AB6BF9DE4585C8A93F + 9C97B41FA17BD0F7E724F5BB216BDF41FC7FF5E3FFAD07FF7F7D38C669739CDB + 789CB70624FEC62949BE734632B147D63D1CEF5B3C06B4D99F70B31FCF67BFC4 + 5EED45BB1FCF29F6C47E713709757BF0184E4B0E01F93D3C7777CF42381ED6C6 + 5DEF93C3188FBFD1A7C7E09EDC3B03E7958CE736EE7AAF1CBADC89BFE376FC5D + 76E0EFB10B42FB52BB2AFA4A871CB8DC2651979AF1BA6FC2EBAB55A2AFB7CBBE + CB2DB20F7D07AFB4CBA1AB1DAA7DC3CD2AAE3B7819EBB01FC5C71B7F0BE779AD + 5BFB0FE1186C277F83F3C3F9EFE7B1A183388F4357BB31AF47C5BAB6718E763B + FA4A17CE87E764F6DE378C73BBD06A9D6BA7EE4F10CDBEA80B2DDA8EC1F10FDF + E895C3D8C37EEEE371DC58D4792E54348ECD73E07E91E75BF05C8CD4C3CF37E1 + EF1EE58566894049B12FEC1C1DBE0D782FADC37B69BD01B867583214340170AD + 4AA1F040BD84688868FED0A716AAC167C64905BF21FD27F15E7FC2A5E053D52A + CE619B1097A095B21DBCACD3D14BB14E072FD7B04DA8CBB5AC73AEF6A154D72E + 14DC8B634074F412E6FAB6941A572FEABBBA2A65CFA91AD98B730AEEAC942028 + 8473098BDB8FEA7A42609FA6127C6616EB5AFBDC76609C21A1B7B7968A775BA9 + BA8303DA8F897F6B393E638BC5B3BE507C9B8FC8766BAD0FD66EB7C2472BDCB5 + E0B14F9BD9C70BF3285FB409A8B7731C7BB8D7E6633D0130F6A723D93AAE426C + 86B06EC33151FA4184CA04CC0CABEDD34E558AED0476A71B19E7E55E57A43098 + 3FEC338EDE230A78195A7A7B7385968C1243D7AFDDEF76926932B88EFDB84EAA + 2AC2354FB178D4625FE628E6D88942D90A6DA9C235D5B16C598FEBC775E5B8BE + C5F5E89A325C8FE21A71D37146AAC175E1715C5BE23AD206C0EB316F3DAE539D + 5A7304D790C5C6114CB1BEAA3811D784ACD30D1C87FE046DAF3962C696E61DC6 + 35763CFAE90EA6F3D768695E9C2CC98DD5D21E5B9A138BEBED041541B0ADB5C5 + F6B5752AAE4F13718F7218F72C87700F13238BD2E36431F30AA33E37C980DC25 + 9909B220F5B002608260D6992FD8E40C8E56286C83E1052987741DF305CF38BC + 57E631CF7022DA71FB54C60D1C2573AC9221A2674447A80885E7100E47D3D96B + 85893EB80F730E60EE412DA71C88C43D5C984C56386CF2084F3F80B9510C031D + A661A1A9AFC3F7682E616A52D86EDC2352A1F2D5EEDD3211F509BB43E54B9493 + 222264DABE2805CB93F686AB3BF8ABB07099773856E6C7C669FDF35D410A8609 + 952785472834FE2C7097F6AB83383814F7A4C1F239C3470706A91404FB437EB8 + F7F50D908FB7E35E19FACC82C21FD1156C892E61961F7A6C47E98B36E7E01EDA + 13F3DC7D14107FBCCD473EF5C07A4F3FAD7FB8D51BF7D284C5D803E5B82D0C25 + ED2D1FBB633DFAC66DF694F736BAE3DEDD03F7EE04C6EEF2DEFA6DAAF7371012 + 7B68FDED355B54EFACA59318F5D50608D331CC92CEE0B7566C9471EBB6C987EB + B10701F2CA4DC631BC7CA3BCCBFAD275F23A4347631ECBD716AFD1D0D3EC2740 + 5627F1C2350A86E91EA66BF89539CB55EA1EB664F7D1356C3B875F9EB9545E81 + 58BE3C6BA9BC347389BC387DB13C3785207881BC3C8DB07889E6137EC986C196 + 1832DAAE13163FF3E54C79162214667E61CA098647A031DB33E4992FA6C9D3E3 + A7AA58D7B605826D17B12B443461AF0588097D6D186CBB84E9107639853FFA4A + 9EF870A28A75BBFFF10FBE94BFBE375E1E1BC7FCC25FC9AB1367CB84251BE4E3 + 79ABE4C5F1D3B5CFE414A62398CEE0CF5C7AEC6D533EFAD6A70A87358434FA14 + F8BEF191C2E1C7DEFC58F305338FB0E612C65CE608768A3981C7D223AF8C53FD + F5D50FB4CD9272F63FF6DA47F2248EF5C41B1FBA20B0ED04B6CBC7E940C6D863 + 68DB40D8960D82150EBF3036C41D4B2EF72E4B4BAE318A63961EB1C69C7D4E20 + EC84BED46338AE13009B3A01F1EBA8BFE1920D791F047BC7921300DB1098B2DB + 63E93EC03B9608799DF5D1FACDF7F60ED9DFEB5B1A8B0D3C4CA3B9C1D832EC84 + FAEB136410468FD8B97E097BC956FE0400EC9C3F7ACDE831A77ECF9C3F5B4EE6 + F5AF02E0B1C439CAE046C34C07A77BA046AF192D6B9E7D2EF7AD7D90C6DA67B4 + 9C739D6B9D7AD89AFF36FD77016085C006F6FED6F5EB14C71C4FC2FF00D9FFF9 + 0F93CE75FCDA614CDD170B7DEC3F96D1CEDCFF0E3DF0D8BF53E64D046F6A6340 + 5E5BF69BE2587D6369ACB8FBFF8A7EEF9BF7833E28EE1FC387CBBF01003F68CC + 6E3B1DBFCE39630160C25F5BF785747E9A60D8CC316B70E1C00F6D7EF03F4FD8 + FB3E64415F4B0A7D1DD27E960F90EDF2A54603DFC75EA2CBD7E8D197087E47E0 + AF0D801FB15CBF36FC7DEC55C25F94A3E02F4BCDF9FB8671F8DAE09725351600 + B69DBDCE10CF36FC3520D800DFA7DF31B0D7CEF1FBB7F7BE1E11A1AF02E0095A + 376E5F03819D00F8990F71618BF61304C096EB579DBF160C26E47DF6B359AA67 + 588E9F890BED19F2F467D38D3E270466FE5F532AE8FD9C30D89A8FBA0B027F81 + 31E8B909687F891B848984BFF371B13F5F9E9960F4DCC4050A79E9EC7D69326E + 386C57EFD7238E5F8E33CCF36B335729F87D692A6E56A6E32666066E6CA6AFD4 + 30CF04C02F4C5D2926C433340D7367D8797D4D69435E825FDBE9CB36212F21B0 + C260D409745F99BB4E41B00D7ADF5A42995CBD74F4AAABD7EA27F8D550CDAB1C + 619A516A9BE1990975D5C5EB2D9F6E0E90091EC1F205C3286FDD291F6FF453E8 + CBF2B32D3B75FC63B700F984B976B7B0DC29E33D70838BF904C1E33D4370631A + 645CBC84BBCCCBBBC55F61EF78AF1019EF8D9BE3EDBB55AC3327AFE6DCF535F9 + 7727FAEF95AF7645A95BF78BC0BDF2F5EE68599C902DDB8E54CBCAB442DCB01F + D07CBB1342F7E3C67B9F8A7977278646C9C4DDB8690F3F2853220F19C72ECA29 + FB0FCBA4FD3132ED503C9420930EC4CAE4837132ED7092CC8C4D965971293237 + 214DE624A4AA5B979A9798AA9A1B9F2273E8E08D23F425E04DB700AFD1FCB474 + ED9B93CA30CD668C5A9849D89BAD2581EFA26C8664CE9325B9F9B234AF40CB25 + 39502EC16CBE2C67D8E6028C17E5CA9262A890CA91C505D9B20C7DCB8FE4CB8A + D202590EAD28C37A8CD1D16BC0709EAC3E5A24ABCA8A6419DA0CD9BC126B96E5 + 66C88A3C42D43C59833984BED47202E0822C285B96E6A4CBB2BC0C9DB30A7BAE + C8473F44F04818CC3D96E5A6A39E212B1500A7637D2AD6A6617EBA966C2FCF4F + C11A938F6C4976822CCB31F9CC8C03017568696E3CFA8C53612581716E3294A4 + C0770DFA6C070265802F8E81F96BB076E39134D974245DD661DEAAFC04595D90 + A8FD6BB1766D71B2AC2E4EC0BEF1FA85D89A2349B2EE488AAC2B4D96F5E5A9B2 + A1824EDD74D9A4AE8B3459773445EB9BF9A55C591AE6F08BBA0CD5860A1CA322 + 03CA95F5950C016D877F4E970D4712718EB1B2E168B2789CCC976DD504B878BE + 4BF1BC1567C9CA922C7539AF2DC991F578AED617A4C85A3C37ABF2F09CE7F1FF + 3F5316675B10B7285FB5BEB458B656962B7C55280CADC8CFC1FF0BFF1F726439 + D7A4A7C8A22C3CC75C979F2BEBB0D7EAAC6C599593236B087FF1FA5993674030 + C12F01B00AE39CB7323B4BEBEB0B0A645D7E1EDAD807ED2DC525B2B582E0B74C + C12FB505EDADC72BC4ADF2181E73B96C41E976BC52B69D388EFE63B2B58A2E60 + E6E96D10AFFA3AF1AEAB13AF0642E01AA1FB775B23A16FB7025FCFD62EF1E9EE + 13FF53A765E70041DC6509BF7845024F0F6A5FE8B961D97BE99A840E0EEB3CAF + B62E5DE7DED2216E8DCDB20DFB7B403EB63BB7AB53027B3A516F951DAD8D0A82 + 3D982FD80A09EDD97852E8FAF56EAA155FE610665EE1FA1AD98E3E5FCCF5C71A + 037E1B6507C603DA088051C75A3F887308897D5AB1B6AD41FC3137B0A7430208 + 845B1B64477BA3025F82DBE0BE2ED94D407B8AE7D48673C2BECC354CD8DB82F5 + 965837E77512E75BAB7B720EE1F04E0B1ABBD7330FF209F16E65BE60C2620390 + FD78AED89775E636260426B8DD7DA65FC2060D0026080EED3F2321783E83A1DD + 789E43CF9E91B0A121891C2678BD2891172EC89EC1410939336041E56E091DE8 + 913D674E4918440730E16FD8D97EAD07E1B1EDC2E3DE83F951E7CFCBDE7343D0 + A0ECBF4437EF05893C3F24E143831271CE3885097E753D14394CA7EFA08265F6 + 1FBC725162D4257C4EF65D1C947D04BFC3386FE6253E87399C7F91B985ED7EE3 + 82A60B9821A983CFF42800260836E0F714EABD98D307315731CEE18A01C031B7 + AFCAE1BBD7E4D0ED2B72E00621F03905BF54F8C5B3C6FD8C3D4270ECDDE78D0C + 108686CF61AF41D973FE1C8E3DA8CF21436ED315CC1CC7FB2E9BB0D1FBB0275D + C7B177AE2AF43D7473580ED2990BEDBF765AA2AE0DC83EE63FBDD68FF6193C6E + 3C07CC177CC5CA59ACA1AB192298A1822F4912CE371E25F30AC7A28FF03711E7 + 9F70EBB2C4E131B08FE3F1B72E8A866ABEC37CC7E714F412F2263284B195EF96 + FD84C2B60339FDA71B0A6F392F9E10184A606EE23B7473D3357C5652BFBB2259 + 3F5D97CC7BD724F57B425E86393E2F09DF0E4BAABA602FA90B96B973D319DEF9 + FB2BA261AE71AC4428094AB97D4E52B16F2AD625DF1E52A5637DCEBDAB92FBD3 + 35C9F9F19A647C7751D2BE3D87E35CC0F1087A87B1DF791C7F50D2A08C7BE7D1 + 6FFA086753EEE0DCBE3987350606B324D025F055E72ECA54ECC7B18CEF870D2C + FEF1B264A14CA52B1873126E9E967828E1F68042E6248A1098FB7F7BD61CE7DB + 4185B1B9741DFF74097BE178DFE3F9C39E84BEB60399AE64D3A63399EEE6CBA8 + 630D4A3E1EC26B9E7F061E1F1F13C5C79481C79309E570CE8F78DCD83BF7E7CB + 52FA7FDD913228FFD7AB780C06D4A6DC3D2DA93CC7DB84BF67F058B816CF2F21 + 32FA08754BFEEBA614FCFDAAF625DE3AA5FDC9787CE9385E3A21341E5F1A1E13 + E766E33926044EC7634DC1BE0904A2377AF19C503D127BBD0BAFB16E8943FDF0 + F54EBC265AE5E055A3E8EB6D127DAD15635D78CE709CBBFDBA26FA0A216E1BD4 + 2E87AE18B06AC3DE541E074AC439B19D8032E9CE008ED9273116E8E51A825EF6 + 110CB33C7CBD17E702B9FA088309898D0E5D31D0976B630978751F8264AACB05 + 9F793C3E4682F0F89BD8CB02DF14D710942B4CB720B1EE6F9DD7C12B1DB21F8A + BAD4A6E50128F262AB42E1880BCDB267888EE106BCAF3649F8B96689203446C9 + 361DD4BBCF36412D78FFA44BB85E424FD72AAC0D3955A3A0967585C1A887D00D + 8CFEDDFD68A30C7280DA1D6D650A74D5BD0BD9A0987D3BD1262C26EC250C669D + B098E3BBB137CB5D3D55BA17E12F8F43672F812EC33C074081A887F5D74A129E + 8B8CBB786F3AD722411D385EAB1923080EA27B18E7E2D75AA6EBE804B65DC1BE + 383F036BD187F309247CB6F6F76D3982CF52AA54B63716E39AA04021B0378130 + DDBD56B9DD82C15A77F4FBB4983E1F85C045E25157A810D817737D5A4B15FEFA + B557AA2BD8A799EEE44A85C0DBDB9967F828F6A4F3F798E5323600784B4D81B8 + 5BA1A4E9E435C2319B089B2BD4E96BF20B1F11930B988E604262E3F8DD5A6380 + 3161F0D66AA6D128C5E3320098F09722082600669499CDFC5121AE19098129D6 + 371ECF927595E9B2B12ACB449A6159958D6BD06C5C3F32AA8D710113FE8EA555 + C50C0F6D802F412F2130DB6B4BE90C26148E85706D8DEB61425E425F9674FDDA + 4E604AE7A04DE0BB34FBB02CC98A91451926DC33EBAB704DCD6BEB65185F9875 + 18F73731B220FDB02C4A8F95C5992677B081BD31B2342BC1058697E35A9F73EC + 313B4730013061300130CBD909C61D4C00CCB119B15168EF573730352FFE80CC + 81582728A666130C3344F4A17DC60D0CCD8C660E61ACB7E0EFE4FDE8437B56CC + 41998E364550CC70D15F87ED96C911610A8335947404FAF684E05E31080AC6BD + 6488C260BA7D2786ED51B13E19732747710FDC7F2A4436625868C2DEF141C1AA + 0984C37BB0269CC7222CC63D2CC346638C2581F0A70181F279C02E85C19A3F18 + A23B98F0978EE0F13B704F8DF2636F5F959D3358DB9A43D85F3EF3D989FBE340 + 2DA9F1DB517AF9CBE7DE01AA4F3C31DFDD573E74DBAEFAC882C084C284BF9F58 + 631F6CF1920F377BCB475B184ADA4BC66D2214F650004CF84BE8FBD6EACD2310 + 98CEE0355B65DC3A77750DD331FCF64A0384DF5DE7A6109821A5097AE916A69B + 98F30984597EB891DF2F6C95379877789909236D8B21A4DF5CB85A41F06BF3AC + F0D1F357BA42493B41B02DE618A66B584B0B16BF3277B9C2E017A62D52110613 + 1433B7F04B531628EC65A8E897A78ED4A91726CD93E7BE32A1A35F4049F8FB32 + E1B0C3356C3B855FFC7A04023F3B61BA8A7585C213A6C933A35CC04E67B0DD76 + E611B6C3443B45E06B43619535CF86C34F61EC2D3C9E49ABDCE4F3C5EBE5E52F + 6769BEE047096F0977DFFFC2B8801500333434DB5F68FDF1773E97BFBEF681FC + 85F9765FFF489EB2F20A2BF4C57CBB4E186C43612704B645D0EBACDBD0D7960D + 875D7A8570D7005EDBF54BF0EB12DAAE31E6047ED9CAFD6B8160BBAEAE602B17 + B03324B4025A0710A638A6409780D66A3BC7ED311B0ADBFDDA6703606B9C7527 + 001E0BFE1AB1FE60F8FB30003C7AFCF70260C25BBBFCA76403617E777FDFF7F6 + 96ACEFF49DB2B98093133C4C9CFB8F45C661343600A65EC4F147E45C733F5F19 + 698FEE7F9846AF19AD3F32F79FD218FCCA05785538F683DA6370351BF03E48CE + F1D1DCEE776934001D6BCE1FD5E83DC7D258F3FED13E0FEAFF53F594FCE52F7F + F91BFE79909E5429E8FDEBD3BFD1A3A3DA0F7400533CD87F8703D87E30638DFD + 1372BEE0F48566E9BEFE470DDC35C20BFF21B25FF0A3FF5046D65B1A3DFE67C8 + 61CB77FDE1FDC1370533D77ED3C39BDB0300AF13FE8E35FE3F41F68784AB8D37 + 7BEAFE39F8707918FC851E797204DCFE11D9907734EC655D433BB3CF82CB63CD + 73E575781A1FE40CED3C0A0013FADA00D84060F6DBF097170014D6E383FF91E7 + DEC5058403E2A2D4F0CE6C5B7242613BF4F368006CC35E1B02539AEBF765025E + 825E8CA17CF415037A9D00F83E08FCCAC72ED101EC02C0D0FDA19E4700B013FC + 12F012F4DAB2C1EF68D9D097B2C33ADB6E5FE6F9A5140213F6BEFFA53C350E63 + EAFCB5F5B53C3EEE6B79E283292EB7AF33A4B3E6FBB59CBD36F865DB06B936E0 + 7D509B22ECB5E12FC7FF46B7F06733150A13F812FC3AE1EF8B5F31E72FFBE6C8 + B3683F833AF5FCD70B70D3B0585E845E988C9B0C88ED97A72E71F5BF327D99BC + 3E1337243396CB4B3396A95E99B5023727CB7153B20C5A292FCE58A5619E5F9D + B956433A530A80671BA9EB177DAF33ACF3FC8D0A78E9E67D73E1267973014AE8 + EDC5B8E15AB4455E5B80F1851BE58DC5B8695BEAE672F352CCD34BC72F4BDBE9 + 4BE0AB90977977513217EFDB6BBCE4CDD51EF2CE3A4F7977BD97BC8DFAFBEBB7 + CBC75B7043CA90CC1B98A3D7E8C34DB82145DF275BFD7163E9271F728E9B3F6E + 2E03E45377DCC4BAEF52F0FB394333D3D1BB2D1037A641F28957B0EA536FDC10 + FB58B0172543355313022270631C21E37786A926EC8A908921519A7FF78B20F4 + 87ECD5FCBA33A21364EA81389910B65F26841F944951B1F2D5DE189988B1AFF7 + 1F928991076442C43E85BD530FD1B17B58EC30CBD3E2126406EAB3125365467C + 32EA29288D6626A6B85CBAF3A0D9CCC39B9424739353647E6A9ACC4FC3585A32 + C693657E7AAA2CCC4C972539590AE99837777E669A2C64E866F4DBE30B32D2AC + 9CBB39AE3CBC14DB0CBFBC9CA1978B8C58A7D84F314FAF53CB0B1976395F56D1 + A55BC2D0CDD9B28C90964097F960ADFCBACE5CBB9A27B63053561764CA9A7CA3 + 550519B2A62447D615E7C85AECB1B6909091103743561767CBAAA22C59959F21 + 2BF3D0979B06A5C8D2AC64285196E724CBB23C4B74FC422B72120DC045DF8A9C + 24599E1DAF5A999B803E8CE527626E0C8E112BABE95A284CC43193654D41A2AC + C0BC55D09ADC78590DADC1DCF5C5A9B2F148BAD697651D9265D9D1B23A2F4E36 + 1425C996D23415EB6BF3391F2A88C7E348C6E3814A502F8EC331D05F1C2FEB8E + 24C986A3D8CF02BF0A7CE9E2457B5D9901C304C01B99CB177D9B8F619C0EE272 + CECF950D5545B2966585C9FBEB5E952DDB8E65C926CCD98C351E3505E2517F54 + 36561D911584EB470A65FDF132D95879047B95620D74B4483697E6CB96F26259 + 5F56220CA14DADA3A316ED5545F90A7C6DD72D61F05AE6596648EF827C2D57E6 + E27591635CC3D4DA8202D9585028EBB20DF8DD5C542C5B4B4A65437E81ACC3DC + 0DF9380FF4AFCD3190784D36FEBFD1DE5468E611FA723DD76D2B2DC3632E930D + A538F69112D95A7E543CAB8E8BE7C96A9577DD49D9D1D4247ECDCD689F946D27 + 4E88576DADF810FEA2F4A8A9114FC8B7B941B637D48B07E4DDD6213B7A4EA9C3 + 77474FBF82DD80FE33B2E702F3C5DE92BD97AF4BF0E005093C3D24A1E72E2A10 + 260CF66CE914F79636716F6D158F960E716F6C15CF7AEC57CF73689080D666D5 + AECE3675F0D2C9CB90CE9E75D5EAF455E08B3621EE8E967ACC6D94809646AC3D + 89F3AD12DF861A096C6B54ED683C29BE8D35E2DF528779F5DADE5E8FC756775C + DC6B2A3407B2575DA578D51FC7E33A81FD6BC40BA5374A1EC7B715C7807675B7 + 4A504FABECEC6C127F025AECEDDFD6AC73B637DA21A71BB44D0732EBFE6D4D12 + D0817368AB17FF76B4210FECED595F253ECD27B107FA798ED8DFA79930BA4EFC + 3B9A1514D3351CD2DF2DBB4FF72ABCDD7B76003A2D11674E4BF8E90109ED3FA5 + 798D4307FA64DFF09030043345372E612C9DBE5C1FD4D78EB24308B3437A3B15 + 6687122CD319DCDDAE909C623DB40FFD033DD8BB5B42147E1BC7339DCBECDFC3 + DCC116086778EBF0C15E5508DBA73A24FC6CAF449D1F907D50F8609F440C1136 + F70AF31F337C76E8996E091B42FFF97E617E643B9F7230C639C7A501EC771AC7 + 67EEE4810ECCED92B07338D6F080445D19D47CBFB177AF48DCB704B357E4D0AD + 8B9AF377DFD52189C478F845E631EE57850CE1F867F1785012FEEEBD4267F019 + D1BCC8677A709C1EECCF73622EE24161B86A02DC835707E5C0D5B31A8A99F961 + E982DD7FA55FA22EF559EA917D977B8DAE74EBD8812BA7F1FCA31CEE97833887 + E84B031273E50CF633E19B35CF2B4BEC4B312F6CFC0DBA6A2F4AC6F7D7248BA1 + A3BFB92C09B72E48E26D93B796219029D6EDBCB6A95002FA0EDFC21EB707B54E + E76A32C6EDB93A9FE56DE6A31D50384A684A2049A729C16906DAC9DF711F3A73 + 09498724094ABF77D138717FB888B984BC6725ED0EE1E27985B384B1048FA977 + CF4A2AD6A5730DFA52EFB2CFF4A77D735632BE1B92CC7B04C12CA9413CC6B392 + 8D7A36FBBF1F948C6FCF28B4A4B8A70A6B5321BB4DB049C899FBD3450BAE0E4B + DE2F975504AE74DC26DCE853504A289A78AB5FA5A0154AB54029A129E7737DEE + CF84C8C3BA2F01B5825D3CE6DC9FAFE21857240BCF0DFBE960CEFE99201BEB7E + BAA0F5DC5F2FA07F58EB3996F27EC57A3CAE9C1FCF4BD17F5C81AEE17C87A5F0 + EF57A4FCFFDC95D2FF7D03EBF11CFE705A327E1890B4EF08517BB5CCFEF10C8E + 3B089D959C7B6724E39B5EC9FCEE34D6F23CCFA17D4A526E75AB58CFB97716CF + 23F6403DE3DB01B4B1167BE7FE34247928F37EC4E3C4FA943B7DF8BFEB95A45B + 3D788E50DEA1B39820B80DAFC326397CBD59CBE82B0D72E82ADA84C0373AF1FA + EB41D985763BD401754AECF54E6D1FBA82BEABDD9270B30FE77E1ACFE95985EC + 49B7E978EE73E894FE1F18486B44281B83BF95E84B84B43D2E80CBFD62AF41D7 + 79CC4E89B9DA813A216EAF0B0227DCEC956485E3032ADDFB562F1E4F1F1ED729 + AD1FC61E5C43716FDDFF168E8B63C45EA3D3DA9C1381319DD0741813405384C2 + FB2E32FC750BDEB71A25EC6C3DCA26BC9FA1EF7C8BBA8FE9428E3CD7227B875A + 25EC4C13DEF3EAF1DE5883F7E77ABC4F1B17304BC2DF88C106DD43812DFA5857 + 0770AF09E74CA84BC0CAD206C00CF71CEC80C80A7931D71E271826540EE7F118 + 96BAFBB886726669D76D78CB926DBA7F632F77E1B5735A22CF34E2F39D219C8F + 2820DED5714C023B2A5574F6D2814BF9B5956BC9F353188BB9F69E741D13FA52 + 3BD1A673982E5E8FBA02F16C28143A8009B6B9CE38998FA18E767329F620CC65 + E8E8627CD617E33C4A653BCEC506C0EC679B73EC70D07E7A5E15EA02DED97542 + FC20AF667C6613FEB654C8B6C61100CC3CC01E0D5C8BCFF2C67205C204BC84BF + 0C276DE713A6D84F173061AF02E09347B474AF2320C69E7547715D80924098CE + DFAA3C5C2B146AB8EACD55B99A5EC4CD8A1A43E7EF065E2FA26F13AE3B195986 + 5166B69CC8C6DC2C285BB660CDA663390E00CC1CC049A8A7A22F5D18D186D097 + 7D36006649004CF8CB797403DB322E60BBCDFCBFFC4124AEC58B19369A6BE92C + 4E90C539876471D6415C57C6280866B86702E01579B87ECF8DD33AC3432FCF4B + D2EBFD25B8C65F9E97A23FF25C44F09B15AFD7FF4B71CD4F284C08CC72797692 + 2C4E8F937949D132972EDFE46885C10BD30E2B189E9D7C50C53AC767C619004C + 77309DC4264FB059339B6018221056C599DCC176C8E839F1D12809802354CC25 + 3C233A4AF307CF4439337A9F4C898C90A97BC3D5113C3BFA80CC3AB85FEB5322 + C3A13D32792FC16F28CA30DC4B86AB8B784278A87C19B15B359100796F848697 + 6678E82F4243D421CC36C5F0D0E343828D6B383C422130DDC25F0407CB04E8AB + 50868F0E91CF76E2DE18FA2C3010758683467B87BF7CB4C34F3EF5F397CFFD03 + B4EF73D43FB3F4A91FC630E7E3ED7EF29137EEB97D30C777978CDF11285FF861 + 8F1DBB5C30F8D3ED01B89F3650F823775F19B70DF7EA9EBEF291C70E038221C2 + 61961FBAA1DC3AA28FDD76E03E9EE998BC70FFCFEF01DC2182E06DF2D61A3779 + 73F55679177D1F6CF256BDBFC153C78CCCD83B6BDDE42DCC636903E437571120 + BBE97CCE7963E5467973C526E31EB6B56483BCB998798537C8EB8BD6C9EB8BD7 + C8EB4BD6C86B8B57CBAB8B56CAAB0B96CBEB0B56CA6BD0EBF357CA1B5AD215BC + 545E9DBBCC920581E72C9797662F859669FDB579D0ECE5EA047E691A43472FD6 + F2C5A90C19CDBCC2A64DC7F0F393E7CB0B930987290386E91436E57C79FE6B6A + AE3CF7F56C79EEAB99F2FC446A96E6117E66025DC104C233E479CD273C434510 + ACF0974078C274CC31AEE1A73E833E3520D806C2740BDB8E615BCE3E1B00B37C + F6F3A9F22ACEEDF9F1D3D51D4C67F0931F608CE57BE3E589F7C7CBE3EF9970D0 + B62BD886C28F2928FE4CFB088B55EF1A006CCB06C0B61E7FC388A1A1478B6E61 + 8ED92098A53D9F80987D7F79759CFCE56507F01D434E37B0966300606D135E3B + 20F0E32FBC238F4104BCA3E504BE1441AF0D7B9DD09775E79AFB20B00304FF55 + 21EFFDB221F0080C7E30FCA59CD0F741B21DC04E27B00D807F0370A17F0900DB + B2BEBBB7BFCF27B875F559FD2AD4EDEFF64783DE07C99EFF708D7014EA3E006C + 43DF3140B07101A37E1F5F199193BD8C35EED4E8B9A3F57BE7FD993290D729F6 + D972B41F1BE16A6371B8D15CEF3EFE66C9E508B6E669DD5EEF94BDD783E498F3 + 08EA0FDC672C39F7799046CFFDA3EBFF2C8D750E4EE06BBB7A9D7D9413F23A35 + 1A00FFF551AC57F84B3960AF0D7F7F03811D27F2CF6AF403FA1765BFB8C6D2C8 + 2F101C40F50FE8BE3F1627FCA59C637F96460160F6FD913710CACCB5DFF41EAC + DF0380FFD81BEC3FAFB1CEC5F58160CDD13E9C8BB36DE6E1C3E51F00601790FD + 8372425D27D81DABDFAEDFE70056072F44983B0A0013FE8E06C0CCF3AB739F61 + EE5F5C303CF7B63CF63C3EF49FE7C5042E649E1F81BF94338FAF0D835D00D802 + BDFF0C00A61E0480097F7F0B80E90436219F15025BF0D7E9F81D0D809F78EB0B + 85BB4E8D05806DF8EB04C074FAB2B40130E12FC12FF5F407CCF18BF2C349AAA7 + 3E98AC00F8F171B8C8FD1017C676986794360026B4B5DDBE4E17AF0D742985C4 + 28D9A760D711E2D916D732D433C5B0CFCF4F30C0D7847C3632EDF90A7F9FFF8A + BF18650EDF450A7F5F98B4D0805EB409805927F0A55E2204865E9D4107306E46 + 20C25F825F02E05766F117AA04C12BE525CDEFBB0A7DABE4B5B96B55AFCEC18D + CF3CE6F75DAFA5DD7E7321C3381BBDB57893BCBD64B3BC03BDBD740B6E962802 + 5EDC88AD30E19CDF5ABE4DF3F3D2D5AB797A97B9A9EB97E384C29A8FD776FFAE + 2308C68DDF065FDCF0792BFC1DB7C9474BC2E00F36333FEF76797B2DD66EF0C6 + 4D22433BD3C51B209F7904CA271E3B715319209F7A06CAE7DE41F2B9E72EED53 + 79ED928FD14F8DF7C50DF5CE709910C090CE04BEB821863EF50DC68DEF6EF962 + 57B84C0C899489A146134270638DF657BBF741FBE5AB3D50C401F9820ADB275F + 4712F61E9629D1F132233645A6C524CAD4E80499169B20530F1BE84BD83B2B29 + 45E624A7CAE2EC1C599C95A30EDD3904BCCCB3AB4A9739290CCF6CE02FDB0B32 + 087133514FC3FA2499959C2873D29255B3539354F332526551768642DDC5B999 + B2202B0D7D29B2301BEB1D756A514E86BA7417E532AF6F9A8AF5C50CC55C60C0 + 2E45C8AB6E5E940600D3DD6B01E3DC6C5941F85744409C83B12C595E908576AE + ACA2DB97501875E6E3B54BEEBD0CC7585D9021AB7253654576B2ACCC4991D5F9 + 69B2BE245B361CC9312ACE96D58504BEA95AAEC238E752CBB392B02615A569D3 + ADBB2A9F6EDB140D13B79AB9C2B2E2646966AC2CCF8EC578BCACCA4B407F22F6 + 8BC3DC381C3F5E21EE92EC831A36797B6DA1F8D615CBA62329B226375636608F + 8DD8735D6E826C2848118FF21CF13A962F9B718C555987644566B4C25E425F6A + 6371B28A20786B59BA6C390A956740E9E2762C55B656A4CA46C2DDD204595F96 + 289B08792B30E718E610F0326C3344A06BD7DD8E67883BBF94ABC890B545380F + 9C9B7B4D9178D497CBA6EA22FD228F21A07DEA8AF4DC3DAB73752E73FF6E3A51 + 226B2B8FC8B22345B2F4E811595355219B6A0811AB34F7AE4765996CAB2811CF + 1374D1968F84573E765445E0CB30D0CCFBBBB1BC545DC01C77A3D3F65885AC29 + 2ED4355E274F88E7C92AE3142E39229B0B4A545B4BCA64FB318C9557CAE6A223 + DADE524C185C24EBF30A656341B1B81D21F43563EE6515B2ADB45CE77B1FAB12 + 8FA315B2B98CC72C95CDE547C5BDF298789DA812AF9A6AF1AAAD91ED75B50A7F + FD5B5AC41B75CF93356837A1DD2CDBEB6B758E574D8DEC686894805682D04EF1 + EFEED390CFBBCE0C49F0E07994E714F4465CBAA600386CF88A3A8159069FBD20 + 3B4E9D168F8E2EF168ED14EFCE2ED94E75748B5F678FF8B7B58B5F0B0134416E + 83F834D4C98EE606976BD7A7A95E3CEB71AE84B4CD84B384BA2725A0AD4E02DB + EB6517F3EF12A43611E45661AF932ADFA66AEC53A3F302DA6AB5CD719F463CF6 + BA4A05C0EEB5F83FAB390AE1F9AA3D2E9E75503D9EEB3A1C8F429DB07657778B + 425CD603BB9A25B887009539869B2410654877A781AAED2D12D8D906B5EA1ACE + A582500FE07912FCB61AEDEA24E86E461F4348635FD443FADA65777FA7449CED + 95F0333D127EBA4FF69EED97A8A1332AC2E0B0813ED97DAA474B86A93EC8BCBC + 97CE4BC4B9D31AD239B8AF5302BB19E2BA45827A5B25B8B74DCF37B8B75D4176 + 08DABBBA789E781C5D6D1A163B18F3833197E33C6F9D8773D1B59642B03E14DA + 7DAA5DC207708EA7BB50E7DE38EF9E16ADEFEEC71C947B4E77CA9EB35D12D887 + C7DDDD2881BD38567F9B849E26E86508EC46A841769D6A919001AC81769D6AD5 + F92C77623EEB2198BF67B05BC2CFE379B83820FBAE9E91E81BE7E4D0ADF3AA03 + D787D07756222F9FC66B6F40C22FF54BC4653C47174F49E8F91E05C0C1833D0A + 840987779F3F25C167BA547BCEF6E0393B255117E9E01D94E8ABE724E63AF6BE + 3624D13787E4D08DB3E81F90A8618604674ED15E89BCD825FB2E51CC33DA25FB + AF3017699FC2DF7D175162EE0194D197FBE510D6528735072CCEDBD977E5B4C4 + 5F1FD4D0CAA9778625EB07BA67AF483A73EC32CCF2B7E714D0328F6D22E12CCA + 94EF2F48EA8F1725F9870B12FFCDA0C4DC3E2D87A1846F8720CCE31CACB14517 + 2DE12FA16FC1AF57147A128CD1ADA92198BFC3F8AD7E857869749512B85A0E5D + 863BA693D60E354CF7AC3A5B7F2008B660AD25025B02572AF51B0306D3BF374A + FBEE8CC2C28CEFCF2800CE84B250CFBD372879F7CE49CE0F43E827E885B84EA1 + F019AD131253599853F0EB25C9FFE5A2E4FE7441F27E1ED63A41721A8E47E5FC + 48503C24A977070CF4C539A5F3F1D8FB602C0B73086C5912DA1EF9DFB7547C5E + F8386CD89CF93D9E773C2F49B7099609934FE1710DE0F1E0BCBEC7E38332F038 + 72085E7FC6F3F5238EFB4D3F8E771AE77501E78ABE1F4E4B36E6E4613CF3FB01 + ECD781E7B313CF6F1F8ED52DC9B73B708EDD38BF5E6DA77FD7837A8FA4DDE990 + E45B6D9272BB5DD2BFE9929C7BA7B04F9F64604E2AC6D2B007957EB747B2B16F + FE4F672DE1FFF8E7412D33BFC7FF29C6136F754A028E938C7D52BEED91A4BB1D + 1277B34562AE35408D127D8522006E54081C73A54562AF63CE0D037C29B60DFC + 6D9183975AF01A6EC73C3E965EFDFFCDF801AF5F3CD73678E7EB8BA5FD9A20E4 + 25C88DB9D225071996F97C8B1C186ED3F6213A702FA27DB1157B53D81F254133 + C53E8ED3919C8CFF0782DEA4DBD81FFF2F743327E2FF2519FF2F84C0743B2BE0 + B58E17072510FADEC43A2D31D782C787AF75E9FA24BC56E826A6439840986E63 + 86CFDE77A145F65F68C3F9E2FC987B18759607863B70FEEDB277B045C24E37E2 + BDAE1EEF7375B2E74C9D058D1B15FE46D12D7CBA56824E9DD0311B2A1316DB30 + 98EE5EDBD5CB92A2D3970099D0D816C7D5150C85102833E4745F8D81BE9D260C + B43A7FDBCB5DB2017060C731D9DD7342F6F6D74970C771D9D95AAE2EE05D6D15 + F89C2D55081BD455A51098F51D2D652E086C40B081C1A6EF283EEB8EE2B3F788 + 3A86796C05C096FB57212F8E6BBB9AF9F8780E9A5F180AEC3AAE7B12FE7AD516 + E133F888AE234066C9B0D5145DC1BE380F3F9C636067151E0B731E1380E3FCBB + 4E884F5BA56C87E804DED674443C5ACA1504BB615FE60B0EE8ACC11E95B2A9AA + 40B65617AAEB97F097251DC1234EE012D97692B97F8D0881B71202D7332730AE + 916A717D565D60402FE48EFAB69A42D972224FDDBE2CDD6A0A647315DB06F452 + 1B79CD89722BAE1FD9CFF6C6CA2CD9742C5B65C23FA728D825FC656A1202601B + FE3A65A2DC1800BCA63451569524A85614C62924660A14EE43D8CBB99CC7BD58 + AE42DF929C43B2145A5980EBF402FE50D3B882591202130AAFC8C3B198D2A538 + 13F728380F946B4AB2705F9182F504C304C2A9FAE3D085E9B1B8CE67349E0CBD + 6F5892CE1F6666CAA6B23C5983FB8AC59909B8A73A8CFBAD43328F8EE2AC7859 + 94166B424127472B206658694261751B67C4C982941859901C23F3930E6969D7 + E72546AB66C71D54ADCEC3394273120EC9CC98FD323BF680CC8FC3BCB8689971 + 9039814D1EE199ACA39C8636EB7414B39CB62F5C0130E12F3571EF1E9940008C + 92ED497402D3054CE7B03A83F76A68683BEF30C1F0843D98CF5CC136FC0D0951 + 57F0E48808F93A2C4C3EDFB54B01F0A7813B15067F16807BE71D7EF20941AF3F + 7304EF942F7606DE2702E1CFFC9833981018F7DC3B709FEDB70B7DBBE4F31DBB + 702F8D3AFAA84F7DB1DE3F4826F8070B43457FE8615CC304C30C19CDF6479081 + C068BBD10DCCB61FEEDF03B4FC60CB7685C0E3366F5730FC014A02E1B7D76E93 + F736F0FB80ED2ACE615B6131C60878C7B10F6D96E33679638DBBBC8531C263AD + 13245B40F8ED955BE4CDE5CC2BBC45DE5AB649F5F672A6A6DA8892FD1B3554F4 + EB4BD66AD868966F2E367A63D11ACD2BFCCABC15F2F2DCE55ABE3A9F80789596 + 5AB7424AD35DFCC6FCD5EA16D650D1D04B334CA868CA6EAB6318B2E1304B535F + 80D2B415124FB200F0D7B3B464F8683A8719429A10F83902602B6C34A1AFE615 + 76B56DC7B0197BCA72033B5DC176DBD967036085C19633586130C4B0D014F30C + 3F43A04C403CEE0BF9DB071314026BDD82BCA34343DB00F86905C09FDF078009 + 75097709821F24DB296C978FBEF191FCF5F50FB5B4C131EB8FBCF681D12B06EC + FE5E8D7602DF278713D8097C6D80EBAC3BE504C0A3E584CA6CDF0781516788E8 + D1F0F79F01C0D458D0D7A9B100B0ADD1E0F641FD7F58D677F7F77D9F4FB1DFD6 + A87127E47D98ECB9F6BAB135C2516C391DC023C0D7EEB3DB907209275F317272 + 175B2EB0EC98F7B0F94EFDDE797FA60CEBC271FF0C00EC68DB0C6EB49C737E97 + 6C5E389A1B3ADA7F686FE75E0FD2E8B97F74FD9FA5B1CEC106BE2C1F7DF41915 + 61AE0D83A9FB20AF43FFBF08807FAF0881FF8A3FE47F24E71F80EB0FC501669D + 70F64FD77F03001E0BAE3EACFFFF098D751EF68781F303C1098047C6F1E1F230 + F84B5940F68FCA09799DE21841AF0D7BD9673B8247840FCE67505A00F87E8DED + 00260076C15FE8B1E7DE96C75F784B1E7B0117171A02DA84771E0D7F15FC42CE + BCBFBF1700EB7ACE7BD9387F7F2B03826DE7EF6800FC28434053160436E0F733 + ED7BEC355CBCBD8E0B42E8893798F7D7A1B770016985777E1804D63E07FCB543 + 3EDFD767E5FB5508FC012E62A1673E9A6C89797E097F7191FB112E623F260436 + 7AF2E329B8F0E5AF25193E87617570F16CC909816D10AC75CC7BEE8B99F2C284 + B9D048AE5FADA38FA19EED5CBF84BDA301B0AD67BF982D2F4E5A286FCCC20D05 + DDBC53184A0822FC45FFF384C1539728ECA5587F79DA5275FE12FA1AC7EF5295 + 0D825F9D831B9439ABE5E559146E6266AF76C15F9684BF74F7BEB1802098A191 + 7023B468A3825F5BEF2CDD22EF2EDF8A9BA6AD0A80DF5CE6E672F5DAD0F76D82 + DE75C6DDFB16EAEF40EFAFF6D639ECB7356E036EF236E26690797C37EDC04DDF + 0EF9C4DD5FC66DF1557DE68D9BCCEDC1B861F4C7CDA29FC25ED65912F8F2C6F1 + 234F7FF90C7326F8865A7DB819DD1E849BD1DDB8490D914FA0F13B77CBC4E008 + F932285CBE08DCA36D9613D09E4867EF1EDC4887EF974911FB654AE4412DBF0E + DF2793C20FC8CCFD7132372659E6C4A5C8F4C34932F960AC4C8F8E9759B1C932 + 332E596625A4CAAC784B0929323B31456625266BC9D0CC8BB3B26405C32B430B + B3B215F0528BD04FD03B3F9D8E5DA30519B60883B13E3D55EB735393646652BC + 6A4E5A9242DE45D969B2201363E9C9320F629D7D2E618E02DF1CEC9185399949 + 32273D41E666246A7D3EFA1666A7CAE29C34886B53707E29285385795EA925B9 + 38FFBC74599A9F21CCD9BBB420136DEC4D572E4AE6E2654EDE15E85F969F8EF9 + A95A528B735264B10571976724C8D2B4382D0983F9C50CC52F66087CF9450E7F + D5BF343759BFC4616E2F7EA943B1BD14EBB8CF2ACC5B5F92255B2BF2644B45AE + 6C28C1F17313654956AC82603A7E097F19A2D986C074E6AECC8BC59E0715ACEE + EE3A21917DB5E2599923EB0B12645351B26C2E4E938D8529A8A78AFBD14C1501 + F0E6D254D9569EA9A0773D9DC3F9D8AF205E361F4915EFAA3CF1AD29149F9385 + E2555D201E55B9E279225B3CA0AD74FA56A42AFCDDC42FD2CAD13E96295B98EB + 176D969E35F9E2763C5B369499395B2AB3645D29435AC7CB9A9214D95A5322EE + CDC7C5ADB1429833CE13C7F1AD2F11BFC652F1AE63BEB75C598FE760CDB122D9 + 5853296E4D0D50936CACAB43BB463657552AE0753F562ADB2077E6D345B9A9BC + 04E75682631F956D55C7C4ADAA42B61C2F173705C4150A61371D2D131FECE37D + B256D6951C51D7EDEEDE6E858974E46E395A211BF28B650B816EF931F1ABAE15 + AF8AE30A78097609780981B794948AC7D1630A7A39B6A9B0C4057FBDD0E75551 + 25DB2BABF1FC9DC079E07C2BF158517AD7D6286CF56DACC739D488572D1DB88D + B2B3AD451DB8368C653BB0BDD53537B8AB43C2064E49606FAF04F4F649E4A5CB + 127DF3A6445DB92AE1178625ECC205091F1E963DE7CECB9EF317246470507674 + F788675B9B78B5B68A4F47BBF87577CA0EECE3DBDE263BBBBA24B8A743761198 + 321473074134C334D7AA7B777B63B56C6F6249872EE1ED098C1929CC8576A06F + 07C67CEB8E894F6DB96C87BCEB2AF0185147E9D350293EF5788ED857572E5E27 + 8F8A474D194ACEAB441DFF87D565B2BD1EF3B09777FD71DD97E0D8BFF5249E8B + 1A9C1F9E8B0E82E43A09EE6E963D74B5F6129E366B193ED025BBFB3A15A40631 + 84731765002F4BAE09EE6EC2F347584D6772159EDB5A09EEA45BB94E7C9B09AE + 6B30B75E42FB9AF11CB7C99EFE56ECD98A63717F3CEFA7BB64CF40A7849EEAC0 + 9C76038B715C3A6EC3CEF66A3BB0B34908A977B49C147FEC1FD4DD803EF338FC + DB6A708C13387E8D9E03E17908CF0BE710D461E6EDEAAA93909E461CB345C24F + F31C5AF0FFD3807D30B717FD68879F6E95F0336D1271B65D2250EEE96FC618E7 + E0B9E9E1F963DE00C3A4B64A703F1E331462B5C3CE7548F8F94ED93384E7EF2C + 1EDB20F6B8D0A5DA8D76C8E91609C5FE9C1F8A7A18C6C3873A25FC1C1EE7F96E + D93BDC235104AD74DD5E1D90830CBF7CF5B482DAFD97E9C41DD0F63ED4398FF3 + 232FD2ADDB0F710C6B307E10F3A2A1982BA7550731B6FFD2293970097B63DD81 + CBA7D0C6DA7338FE59E3FCDB3FCC7CAA1D0AB208B158126431B46DCC5542DF1E + 3974A50FA22391EE43E3BE8D471977EDB4C45E1B90D8ABFDA81300631C259574 + E38C647E7751B2BFBF2C39F72E4BF60F0C214CD72D43209F11935FF6B47141A2 + 9DFCDDA0247E7356626F9D92C337FB1462311C6FE21D8640362E49CD457B13ED + 9BA72403FB14D2CDCA75D70906E93E659E5C864D362ECAE4BB46049D04B669DF + 9E9134C254EC9B8E3DB3BF3B2B793F9CC3F99D53204B655384B8D48F74C0D28D + 3A24F93F9F57576A36F6C9FC7640B2E854FDFE8CE47D7F5672BE1B90DCEFFBA5 + 08F38A7E392F793F9E91ACEF19DAF8B43A5A395FD7B00DA5A3CE521DAED6BE76 + A940993019C72EFCFB4529F87558CF81AE58733E433A46672CCF89EBA86C8C17 + FC7A418EFE9F1B52FEFFBA2DD93F9D37A0D77AFC2928E3AEB7436D9270B303FF + 7FED78CE3A15AA26DFE996A45B04B83DC6858BC7937AB70FCF7337D4A5AEDBB4 + 3BBD78DE3AA16ED431F766A7C45F6DC173DF8AC766C6584FBED9AE4AB8D6A24A + 62DF8D3649BCD62C71971B506FC5F3D62779F7FAF11CF648CACD36CCE1BC1649 + BDD52E1977BB25EB9B1E23EC9BF3FD293CB7A7241DFD49D8877BF2386968A711 + 00DFEEC0636A91C374FC428714FE3648DCCD5689BFD586B20DAF573C5E888EE0 + 189C33E16FCC55538FC57362DCC004CBBD0AFA19BA9AF09CAF39C25F85AF788D + 19D8CA76B78666E6DFCAFE0BF81BBA6000F0C18B6DB2EF4213FEAEEA5591E7EA + 25EA7CBDEC43B9FF7C03C61AD05F8BBFBD93F8BB6BC6B9B6E3EFAB4D8FCD70D6 + 1AD6FA7AB7C2611B30BB40F330CEF95217CE87109F21B1FB25F64A0FFEE6BA4C + 8EE44B7C4C5D58DB877D7AB06F07C4C7C5BFE31E3978C9CCA1F60DB76AEE603A + 80ED1CC56C479E6BD3F784F0B378AF3B538FF7E046F437AB087C779FAE13E604 + 0E3B8BC771AE59C5B5CC4DCC3AC1B0ED0E66698779261CB62170F86083CE0B62 + 88678C33FCF39E01ACEBAF85300762F8673B07B0425F485DC15DC7F19E5F8EF7 + 79037D03DB8FE1B3A74AC24ED54A68373F03CA759C00980A68AB50F9B51C951D + CD6528B1B6ED98AB9FDAC91CC06D18A70BB8159F974D84B876FEDE63262C35CE + 2590650FCF8961A9E94CC63E94B53FE1AF0AC7E11E04BE74081310B36DFA189A + 9AD0F8181E13F6ECAE9680CE6AF169C3E77E2BAE6B5A2AC4AD1ED7460DB8D669 + 3A26EEBC66C37E740EFBB71D178FBA12F16CE0F9958A27AEE9EC7CBE9E75A5E2 + 85756CBBE1FA8FD78026F473918681A603D893FB62AE3B5DC510DDBFAC33C4F4 + D61A868FCEB50070BE82DE2D2772B4A46CF8AB00B8D25C8B6E3E9685EB3FCC83 + 36566429045E7F345D012EA5C0B638D102BEC6FDBBA6146529CB244D6542AD2D + 4BD6F6CA625CBFA2649A13D77C940A8A5996E21A9E61A3E91426FC2D64541EA6 + 5C31009830980078596EACF6AF2D49C7F570A6AC2CC47E28D79566CB8AFC1475 + 052FCD677A17E30AA61B78553EEF2D70EF827B8995B857F06F3C2E613D4DB8BE + 2C95A5D9B8EF498BC13D571CEE71189E9AF716892ED8CB3584C074FED249BC14 + 5A948A31DCB72C4C39ECAAB3649BA23B787146BC04E15A26A4A35E56E6A6C9DC + 04C2E143323F097B25466B4E6186909E7D8879832364DA813099BE3F4CDBF362 + F7CB9C982805C493F7EE91AF2377CBA47DE150847C0D4D46FFD483915014D611 + F846CA94A8BD32696F84425F6A4A14C6354F30EE6FC3F62804663EE1AFF78469 + D86805C76161323E8879810D18664920FC4940807C0A110653A3213001B07105 + E39E9AA097C03780A1A32DF06BB5A94FFD195E3A58C607F287D50C2DEDAF25DD + C104C184C2B63EF0A043D88060DEC37FEE8DBDBCF8636E3F85C29F78F8CB782F + F4790428F025EC25D4B5A130EBE3B6A07F137F28EE8E71B437F968A99078AB8F + BCBBD11BE3DBE59D0D5EF23644584C084C984C08AC3078D556796BC566797B25 + F30A6F439DA59BBC8B31BA83355FB05D5A7A7DA981C0AF2F5CED82BEAF10082F + 8416AC46DBF4735C730C73AE2394B4BA849933187A79F632A15B98F9836D30EC + 0C21CD3E05C4EC9FBA505E60C8E829F3E4C5A9F3E4A5A97608E9B9F2ECC4592E + 110813FA2AF8FD9A2602B4BF9CA961A2193EDACE31CC713B6CB40D7D2967C868 + 825F8AFD2C097D097B6D506C8FB34FC73E9AA8F0774413353FF053E3986EED0B + CB096CC2419B9CC113B424C47DE2ADCFE5C9B7F99D1FC6D886088215065B9077 + 2CD9634EC7B01300130A2B1876847F1E4BCEDCC0360066C8670DFBFC32DA56DD + D53706FC1D2D27E0B5E1EF68086CD7B91FDDC514D7B2DF76FEEA1CE8310A739D + 00D8A9FB730013F632B4F3487867A7C602BF4EFD5E006CCB39E7411A6B9D8A61 + A0F9DDBDF5FDBDAB0E3DF224F6865C6390FD7DFF68D0FB7BE45CFBDB3AF9C2FD + FACB1338E62817F07D40788C35B67ECB5D1E2EE7FCB1F47BE7FD991AE15D38F6 + 3F00C063825607A874CE19ADFBD6FC51D9CCD0C90D47B77FAF9C7B3D48A3E7FE + D1F57F96C63A0717CC7DF41979E289E7E5C9275F90C71F7FCE05821FA6078680 + 56B16E80AF1326532320D87122FFAC463FA03F4BCE7DEDBAA36FAC17E53FD27D + FB5BB2FF20C6FA63FA5344004C104CA1FD47DE402833EF8FCB7EB37C9846BFC9 + FEB3FA3DC71D01BC63F78D081F1E0F03C00EA0FB7B753FC8357202DE87CDA374 + 2E3E64CD07F18301B00D8147C4F60800763A801902DA09805D10D802BB941300 + DB80D71EBBAFEFC571F217876CD7AF13FC8E8482FE581EA12CE8FB1BF7AF0D80 + 1D22F875C2DF870160E6F6758A7D04BF8F59B25DBE36FCB5C5B609FB3CE2FEA5 + F397F097CE5FC2DF673FC6852C4A825F026042DFA73F610868E6F9BD1F0033AF + AF9DE397A50D849FFB0217DB13E8E635B2FB5EF8728EBC3871AE962AD49F47F9 + 1CE63C8339ACBFF8152EE6D1FFDC44867A367AEE2BCCFB9A796170A13E65B101 + C016E0754160D419EEF9C569B88970857736A09772D66DD105FCFABCD5F2FA7C + E6BAC14D8A4B6BE40DF4BDB1903736EBEE13C322BDCD5FC542AC6B7BD9267967 + 056E9E966F91379631EFCE567977AD9771F2AEF192F7D698F0CD1F6EDEA13765 + 6FADE1AF6FCD4DDA07E8631EDE0F361BE0CBBA114339E3C60F3780BCF9D39B43 + 4F7F99B8738F4C098E942FFD77CBA7DB03B5EF43CCF9D80B37AD3EB881F50E90 + 8FBC78A3B94BBEF00B91F13B82B59CB80B37BFA191F255C85E037C77ED9109C1 + E1F24550989636F09D12710037C84684BE0A80A3A265EAFE18998C72CA3EDCC8 + 27A4CA82E474994DB89BC450CDC93233018A4F92E9B10932232E41C33B3374B3 + BA7819AE39DD847026E05D9A9B2B4B72724C09CD27E0C5B80D7FE7A6A66A49F0 + 3B2F2DD9057EE76710CE12C466C9A26C8CA527CB9CD444999D922073D392A044 + B4E35126C8C2AC64599A43A7B181AED422FE0A3E876035096BE3709C58999372 + 58CB79697138463C8E99A025DBCE3E7B0FAE57D04BA88B72696E2AFA38360267 + 1996D986B504B70CD94C372FDBFC7284E07709BF3CC1B11502E37C56600DE771 + 2DF75A9493280BF88B7C94FC65BF8260EE538863E7250BC3BAF158DC975FF06C + 3E9AA75A5F9C857D709EFCC2263B0E751C3F870EE06405C0740333A4339DC14B + 330FCA9AC20475D0EE6A3A2ADBCA32645D5EBC6C2848948D8549B23E3F41D661 + 1EFBD6E4C4CADADC38F1AACC969D0D47C4BD2253D6E4C5AA0881371F4911EFAA + 5CF1ADCE5700ECCD1C6FC773C4AD324BDC1856CF72F76E3C9A2AEB2DE7837154 + 6461CC7CD9B6955FD41DE7176ED8FB085D169C87F5CCE956532C5B98FBADB14A + 3CF9A562336160315424BE0D255A6ACEB71325B2ADE9A4049D3D23D1B7BE91A8 + 6B77C4A7F734D6B5CAFAE3D5B2EEE851D9AA50F5983A77B71C2B53F8BBB9027B + A3ED7EA2D2386DE9EEA56318A2EB7733D6D1554B00BCE96885B857D54A606B87 + F837B5604D8D6C282D93F50545EAF8DD567E4C7CAA6B647BD5093C4FC7C4B3B2 + 523C8E1D13AFE3C7513F2E1EE8F3AEAC9180BA663C5FF5784EB1EFF193E25971 + 42DC4A2B65474D83F83737A99377CB719CEB89E30A78437A3A6557471B8ED928 + 7E16FC0DEDE992B0FE1E09EA6C130DB3DCDC203BDB9B5DA198FD5A1A858ED140 + BA46FBBA25E6FA6589BF7D439DA8CC291B807EFFCE56611E5B5FACF16A666EDC + 5AF16E346B192699AE5ECA8B797D1BE9E4ADC779D4E3988D12DCD58CF3E0714F + 884F63A578371CC39C4A1C1B8FA3B90AE744976F85789E2CD5FFC76DC7A1AA42 + F1A82A16AF1347C4B3AA48DC2BF15AA9CCC3EB251FE3A873FC44B178E0FFD3B3 + 1AFFF795F9FAE306CF6AFC3F631F2D6BCAC4AF11CF4B53159EA772D981E3ECEE + 6DC239D5E13C18FA92CFD909ED273C0DEE6AC039D7E15C6B50D65A6D86A4A67B + DA4056FF1633DFBFF538CEFD18CA2AD985D79A7F13FE3FEBCA644743B9F873EF + FA32F1AAC339D4F38B717E01CF2FDBCD97DFBE785D52FEAD95E287F5B602DA71 + 9CB66AD4AB2500C70FE8AAD5B66F33436056E0793B2A3E58B793A1345BD0AE2F + 159F8632EC5F86E3633F28A0B95276B5574B50478D0443BB3AAB55415DD512DA + 5B8BD741832AA4E724FA9933B246424F113ED44A681FE10321449D4209BAD282 + BA31A7AB4A76A33F0C7D0C691ADC8FBA0B82B4C881CB0CBDDAA78A26AC419BB9 + 3AA9FDA8EFBFD4A110865095402696A16331167DB95B0E5CEC5608BBEF42A71C + BC843557E9FC3BE352DC0DE68245797D00EBE8183C23CC114B254274C432B46F + DEAF5725EB1E73D50E41CCBD3A8835A714021DB8D026510CF53AD804354AE439 + A3A8F38D38A6712E1EBED221B1573B25FE0A74BD531D89845F710A942C3875A3 + DB3816EF184724C7E9465427E36D86B7ED943842A8EB3D928AF1ECEF8624E77B + EABCE4DD3B2FB984ABDF9D91CC6F8C2396CED678EE7DCDB82F53BF25F4EDC531 + BA51F60843FD123E26D3B50AA5E1B8E9501AEB18CFFC066D9429373B258D6186 + 7F2074EDC73E74B99A10C1490A37BBB0B647B23046589B7917EBB17706F6CA21 + 04BE07FD785AF27F3A2385BF0C4AD1DFCF490954FA9F171C3A2FC5BF0E4901E6 + 14DC3B2D8550F14F67A5E4A74129FEF18CD6CB7E3D2F65FF715E0AD197F3C319 + 55F6F7A7F1F807F0D84F4BEE3DB60982FB25F3DB53AA1CF4E7FDC8E39FD571B6 + 5916FE724E8AFF7E414AFE6358CB22ECEDD4E8B1829FE9983D2345FF8173FF2F + 869426C824FCC5E3BF8DE7F76EA7A50EBC6EDAF1FFD52EC9770880BB8C6E75B8 + E06DDA9D2E05BAEC236C8DBDDC2871579AB44E084BA89B8279A91827B425C8CD + E01E37DBD4E96BC3DEF82B8D5A677FEA8D5649BCD428A9589B8D73CAA4EB97C7 + C27EE956997CB55992AE60FEB5569D9781FDB3B16F36FE0F533187FBC5E15C78 + FCCCBBD803FFE729F8BF4DBC4EE0DC2A713856ECB51689E739DEE9C0EBA81DAF + 5986816E95C357E9C46D54C5622E2178D26D3E76BA6CCD6B371EAFF1E4DBA724 + E50E212BFE0EF1F7CAD7671C43395FEDC0FEF81B800EE3EF248679842F317C34 + 5EF3E8639D4EE0C8C17AD97BB60EAA55450DD529FC3D70A151F6439183B51271 + A646E7702EFF068D6318BAD08CB94D289BB4DC7B967BD54BF869EED7A0B039E6 + 328FDFA3F079EFD94609235455588BF735FE5D5F6CD5DCBFE1ACA33C88BFC7E8 + AB788FC17B10E12F612D612F416FD439B487E958EEB2DE83E814EE30E316DC35 + A1A2B11FF6DF7BAE49E1B1131CEB8F47F01EC3357C4FA4186A9A25212F5DB304 + BD74FD121E132A5384C10A9379EE671A547C7F8A1C6A96087D5C262C74E829E6 + 1AE6FB32F30FE3BDB717EFE5DD06F086F4544B30DE9FA9D0DE1AF31E8DF7EE5D + 9DF83C6BAB5017308130E1EF765CFF10D012D612FADA90987303BB4C18683A77 + B737615EF31185C1063E63AF0E025B53D2894C00BC8B10D83A86869CC671188E + DABFB5029FE7E633C917D78BCCF7EB856B412F1C9F619C295F7C6E0676E1B3A9 + 079F6FF88CF2EFAA5379B754895B1DAEBD6AF1D989CF3B4FE6FCADE339D1A18C + 7D5BB016C7F16EA2C317D704B8B6738A7D04C06EF8ECDF866BC1ADBC26C4F501 + C1B0571DD6B07E12D714381F0FCCA7DB9721A00981595777F0495C5FD4A1AC21 + 0C3652187C2217D72610A3CA40EEA86F23303E91AFE1A43757E6A81B78437986 + 2B04F4DA23C90A6FE9E8A5D6F3BAB63C55212F4BBB4E104CF8BBB6CC82C42506 + FCDA61A44D38E8446DEB7531F7A2E3B818D7FA160026F4B5B5863FDE54A5CAF2 + 42ACE70F3B4B701F826BFB25B8B667C9FE55BCC6C7D82A8E17E29E85A95D700F + B0BDB654AF1D3CF0FCADC84BC47D522CEEC1989FD8E4235E8A6B7DF62DC6FC25 + BCF720F44D8F55186C424A27A816A49AF0D17618694262BA8619369AF724BB78 + CDD55E27AB0AD234C730C345CF4FB69CC209076551728C6A2E4348C744CACC43 + 7B51EE95B987F729009E154D5770B84CDE17AEA1A4A71E44FB50A44C8F61B869 + 869D3E00456B7EE11907F7E11E3542BE8ED8239323C3B18E60799F4C3F403770 + B84CDA6B1CC204C2EA18465BC3440705B95CC1149DC284C0D4F8409416EC25F8 + 9D8036357E27DACC1DEC8F7BEF1D3BE5138260FF5DF7C986BF1CFF18E39F60EE + 27A82B20F6457DBBBF2B7C348130DB06049B1F6F539F781312EF940F3DFC707F + BF4381F0045FDCCBFB04E9F70104BA04C304C604BFEF6FF6D63ED6DFDD4810CC + 3E037DDF41FD5DD4DF22F445F936FA281D439F02E1B51EF2F66A7715C1EFBB28 + DF5FEBA9E5BBABDDD0B7551DC2140131C5FA1B4B37C8EB8BD7ABDE60C86894AF + 2E5CEBD22B0BD6C8CBF357AB587F4DBF4759ABF9845F656E610B18BF3C67B9C2 + 5FC260426105C184C2360846E9D20CE3167E611ABFEF59282F4F5B20AFCC58A8 + 7A79FA4279710A4343CF718900582130EA1A429AA505849943F89529F3A0F916 + 041E71063FF3C53495DDA69EFA7C8A3CF5997106D301FCF8871315F48E760AB3 + EFB10FBE94BF71ECA3AF15FADAA52DC2608AF9821F7B9721A2BFD03ACB47DF62 + 68E8CF5D6098259DC02E67B00D821DA19FED30CFDA1E05889DE0D7D663AFDF9F + 0378B47E0B807F0B7DFFFAE23BAAD1A0F741B221AF13F6DA1ADDCFF2370EE0D1 + 0098732C8D86BF46230EE0FB01F06F21F068E03B5A63C1DF87425C4BA3E73B35 + D67CD51800D806BF4ED973ECEFFB6DDEF08FF428D6DAA55DB7F718D1084771CA + 057E6DD900D80981A931D68EA5073199078D39F57BE7FD9972015E158EFFA0F6 + 4398988BBB41F6BC7F38DFB1E6A1B2E73AE5EC1F3DFF1FC9B9CF83347AEE1F5D + FF6769AC73B0A1AC0D80FFF6B717B554A08B7E27E01D2D2700D67DFEFAA4CA06 + C0F6DE0FD27D27F2CF6AF403FA1FA687BE7021FE318CF587F4A7C9017F6DFDB1 + 3706CEFBE3FAED1BE66F65BFE18E35F6AFEABE7D5177425EBBDFD937227C70FC + C9EEDFB1A0AE53F61CDB056CCBEE7BF41913AE43E1EF1800D8E4FA1D4BB858C0 + 38E1AFC2E067F9A18FF5CF324C08C66DC00BD910987242DE07C986BF06001BE0 + EB9481BD1F59A19F59270846DB82BA2ED88BF231ABCF29025FBB7402DFB100F0 + 936F9A5F0212F83EF5EE979608800D107EF21D86771EC9F16B03603A7F8DFB17 + A5057E19F299C097B2DDBFB6D847086C435FA7088047C35FC25DA7087A7F0B80 + 4DFE153BFC0EF5022EC829BBAD7D130D007EE1EB79F2EC57FC15E708FC7D690A + 2EEC2733DF2F341517FE5397B8423EABCB77FA32797914E0B5433DB34E006CB7 + A95767AF54F86B43DF3716D0DDCBF0CEEBE4AD45EBE56D0BEEBEB998372C6B34 + D4D1DB4BD1EFD2067987E0975ABE5901F03B2BB7CA5B2BDDE42DDC40BDB3C643 + DEE50D14C43CBDCCCFCB7CBC1F6CDDA1A19C19D299E19C19BA99FD2C3FB64239 + 7FB4D50F63E666EE23DCFC7DEC89B63B6EF8BCFC0DCCDDB947C12E81EFA73EB8 + 19F56598A950F9322054C6FB9B5F23F397C75F05ED9149A111322D2C4AA646EC + 93697BF7CBA4B07D323194E19C235CFA8AE351D132E3C06199B6EF903A7ED99E + 1C790037CB0714FECE3C942033A2E3646674BCC25F6A466C82425F42DE5974F9 + 2A044E94394949B2282353165BEE5EE6E3A5E6A42429D45D847E867336903743 + C13073FBCE4BC53E0CF19C98280B333384F97C097E09801765611DDDBB5974E7 + 722C53DBF3D213B13E56E6A4C629FC9D939620F33393D471CB5CBB84B436F45D + 44289B972A8B73F9C547BCCCCF8895B969315A2EC88CD3BE0599F1380F03876D + 30BC303351D7B35C48884C572FF6B50130B598E0968E5D0BFC2E4A375F9610F8 + DA5098F58569B10A7F6D086CFF929E5FA870FEB29C24595190262B8AD2719E38 + E79C441C27455658E1DED697E5CA1A94AB0BF0F8F058F88B7D8ABFF45F5398AE + 8E6002E095F929B2AA80758C67C5C9223CCEC5E9D1A81F36F037E3902CC93828 + 4BB3A3716E31B2864EDEBC7885BC1481EFAAAC185999794856661D92E598BB32 + 2B5ADCCA52C5A73A5F81EFEADCC32A42E0F58509B2B93445B6607C4B394ABA7D + 8FA6AA9B57BFDC22F02D4B970DA5FCA22B4D365664CA1602E213F9E2595B2CDB + AA0B6443459642607E99A739D82AB2C5A7E998440D764AE45087F8B6D51800DC + 7E4276B413B6958A1F14687D41B9ED64916CAE2E911DDD6DB2FFE64D49FDF9BF + 24FEDE7F4AF0D015F16CED918D27EA655365A5B8575789C7C9133856853A806D + D90E60EFDA6A1561B0C7892A71AF3A8EB25A3CAB195AF9A44260AF9A3AF1AEAE + 13AFAA5A3CD66378DCE5E2565E215B517A1EC7F9D5D64B6063B3F8D535A05D25 + DBB17657738BF8A17F4BE951F13BD9289937EF4AFAF53B12D2DA25FEB54D0A84 + BDB11F01F08EC60661BE5AEFC65AF169AC9780B666D9D5D52E84B9411DED12D2 + 45176E9BF68774774860478B864076AF398EE7936EDB93788C7C9C74E132BC72 + ADCE891A1A90BD43FDC2FCB59EE8F782DCEB4EC8A61315B2A1B24C365595CBB6 + BA2AF1AEAF814EE0FFA21CCF179DB8781EAA8FCA96CA12D976A24CA1AB6F0321 + 6BB56CAF3F269E27CBC4A3E6088EC72F9B193AB202FDC6C1CB31F7EA6271ABCA + 17B763B9B2B93C07AF915C71ABC8936DC7F2C5E37881785A6304BD84C05E35A5 + 0A5DB79F3CA26D426085B0DCBBFA88C2635FBA85EB8E61EF230A98FD5B4F885F + 4BA57E214CA0AAEEE15A3A852BC4B7B152CF93E7EDD3704C412F9DC67419739C + 6DCEF1E117D90D65780E8FA0BF4CA16B404385F8D51F155FC807C7D97EB218C7 + 25FC2DC37A82E0127D2D6E6F2C112FD43DEB8AB5F4E67940DB09895B2B154AF3 + F5CCD2BFA35AFCF03AA602DAABD40515D05E29BB086515CC9E90DDDD3512DE57 + 2711A7EAB50CEFAB9788BE46891C6896FD67DB24EA6C8B840F34C8EEBE5AD9DD + 5B2B7B4ED54918C39CA20CED3DA95078CF29A310EED9791C7DD512DE8FBDA0B0 + 5327251C8AE8AFC53E74AA9D94B0D375F85BC331869A343C2AA10A4169E2ED01 + 617ECEC3D77A54EC633BE116411CC3F19ED770C359F7CE4BFAF70CB1CB90B2CC + 377A4612E86CBDC590C30C418CF1EF2EA858A752EE72FE10F618969C9F2E4397 + B00FEB17A5E8BFAE4BD9FF7D5B0AFE7E55B2EF5D80CE4BC67783C290BFF13C17 + 868225A8196E859A2586D0F752AB440FB748CCC55649B8D6A1202DE3EE29C9BC + 6BE0249DAB290C45FB0DCF9FA167BB159051890C357BAB57FB12AED3256ADA29 + 774E49CA4DF45B20390963A9B7FA24E39B7EC982085E33EFF64A3641EC0F6735 + FF6BC20D42B436ACEF9634F4A7E1D8749FA6100813D4624E26D666E37C72BE3B + 2D790C53FC3D5DBCCC214B48D9A53058F7FCF1B464FF7446F27E19C4F3C2DCBC + 684319DFE2B8DFF6E098FD927B8FCE5F9C138130F6CEC77EF93F620DFA097709 + 798FFCC73955D9FFBAA02AFD5FE7A5E43FD0FFF7219418C39CD25FCFC9D1FFB8 + 009D9732D4CB7E41FBEF17A4FC3F2F626C58E80426C42DF879488170E1CF6755 + 04CD3C0FBA66E96ACDF886618FF97CB0DFC8843E1ED2D206C14E19006CC47AE1 + 2F38061EB30D81F37FC63E3FE1F982B2EE9DC2734AB7AC09C99CFE1D9ED76F8C + 7B96F58CEFFB240BCF07DDB69938173A70A9B4DB9DAE3291AEDB6BCD0A77936F + 19772F5DBDE977F89AE952009CC93D19021A22F04DB8DA645CBD04C55012DA09 + 971A14F212EE265DC13894A990176BB11FC7E32F36A01FEBAE1B309C75A75B95 + 8CBD62866B5509DC03EB0CA06E93F8ABAD2E110613F827628C8EE098CB0D78FD + 374A2CD61CA6E397F3F09A23FC4DFD86AFB15E89639EDECB6D18C363BB617EC0 + C01F4350497CFDEA8F15F85AC5EB1C7FD3C9E8633F61311DE50CD79D70A3570E + E17D60FFF966D977AED1A583C34D387E1B8E4180DC21B1A8131E47E3EFCE06BE + 9C173188F79BD3D51236700225DE6FCEE03D89CED9BE1312D27B02F5930A8119 + 8A397290510B4E6A7F482FDEAFFAE9A42560AD91B0B3D8077BB1A47397CE5F02 + 5ABE476948E8ABDD0A6DF75FE0FB01DDC33D1273B557A22F756B5FD479826C9C + DBB001DAFCC148D4B9667DAF3B887587F1DC709F839709949977B84BEB04C0E1 + 67097E9BF44731E1834D128CF74D8AA03784E70BD975931BF8A4869B26EC3500 + 98C719114130FBED92E20F59F60DB528F025F8350EDE72AD870F10983729D865 + F8E7FB9CB910FBD4416C397F096E77761100E333079F4DEE27F3F13984CFAD26 + 7E561D110D47DD51A10E61D603DA2B201CABA70AC26745079EFBEE6AD983CF91 + 107C6EF8B7E07313C7F1AA2B52E04C27B10D5A2982579F26EE572541BD75128C + CFAA80EE5AD985CF2BCAB71DD754F8DCF3C4E7A9960DB8DEAAC1E73FCE8B8E5F + 3A7F0992DD6B711D80FE913E03740972DD6B704C3A80717DB89521A3F5878158 + 43308CBA270130D6D9B97FE9FA655DE1B1963857ECBFA5DA3880350CF4715E73 + A20D31FA8C3B4A8F1A5C93101EE37A9261A4798DAA1018D7AC4C33A22E619690 + C25C5CCF6E622EE1CA742843361C4B97F515B8DE45B9960098E94F0885713DBC + AE34D5058D09809D4E625E236FA4B0DF86D2744DE9B2A6C8A47751F84BB00B2D + 2F4852B8CBD2AEAF403F1DBC6C2BFCC51AD6097FD716A7E17E2009F725B15A9A + FD92B177AAAEA36B7831EF09F212646136EE7B709F301FF7070B71AF407730E1 + EF722BAF304BDE6FD8CE60C2613B77B009171DADF732BCDFE1FDC9A20CDC4BA1 + 8FE23D11EF7518327A61728C2C64E8E8F803EA069E111D81FBCC3DD06EDC8786 + CB9C43510A86A74747CA34683ADA3362F659E0D788A1A6E7C645CBAC4307705F + BA4F66C7A00F629DF077EA3E86851EAD480D014DE03B3174B771068785699E60 + 7502130A0705CB17BB8264BCE5FA25F8FD126D5B6C7FE68FFB745F7F05BC06F2 + 329C74907C46C72F6403601724866C97309DC004C084BF7AAF0EB1CE1F741302 + 7FCC9CC1FC21377FDC4D000CD93FF0E6BD3E61302130E1AF2B74F4161FCD1BFC + C9363FF9609BBFEAFDADFEF2EEE61D0A806D08CC3E8E310DD438373FED7F7F8B + AF81C1D6F712F6F716FCD13A4B03858D3398625DDB2BB7B8A02F4345336C344B + 1B08DB22F4A558D7F98BD61A97F0BC152ED7B01D3E9A6058E1B0E50C1E2D9753 + 78167FC0CFEF7C96C8CBD3994BD8C0DF97A6F1FBA1F9F2E2E4792A1B0213FCDA + 7DF703E0392E073061301DC1ECB7C342DB61A36D31AF301DC27403DBB0D72EED + FCC0AC3FFB39C63F45F9C96479E6E349F7EBC3AFEF83C13604FEDBFB5FCA53E3 + 68D0F8D2B880D511FCE5882B187AEABDF12A678868DB1D6CBB7E598EEEBFCFF9 + 8B3A6503605B7F75C25E070876EA318CD9619F15023F00FEDAE076ACB1D17282 + 5F5B36E8BDAFCDD0CF940300DBA0D70981EF87C2F787803630F755D41F0E8047 + B7A9DF0B80478F3F4CA3D7BAC4EFF36DF13B7BAB3E16FCA56CCE40D940F76172 + 0260BBCFC5125C320C65344BD1B60D7F6D3921F093F7AF73CA0EF7EC0CF97C1F + 8B1965E2BB6F6C0CFD917963F5FF331A81BD148EFFA0F63FE062943DE761735D + FDBF9701DABCF0417AD01ABB1C3DC7EE7B9846CFFDA3EBFF2C8D750E0A62AD10 + D08F3DF6ACC25F9676FE5F27B01DADBF8EEE23F47541E091B52EC7AFA5917E86 + 81B64EE49FD5E807F427EB412FBCDFABDFBC789DE7CAFE31FE88FE1DFAFD7FF4 + 9CF760D96F880FEA1FADDF33E75F91FD863D5A360476CEB53F3C5C75153E3C1E + 00801F79121F76D068C0FB8FE484BD0F930BFC5AEB6CF86B036017041E0580C7 + 86BF84BEB84070EA595C18F0C3FB59EC6139805D0E5F4B4EB8EB0CEF6CBB81A9 + DF8CBDF0BEFCC5017F6DF04B3DF62A012FA4EDFB1DBEEAEC45F9384BC8D5FF3A + 2ECCDE18D1936F7D2E7F63B817944FBCC95FF88DB4ED3A612FA1AF0D7F6D00FC + E4DBAC7F2D4FBE3B4941AFD3F9FBD4FB93547F7B6F922BECB39DEFD7E9F875CA + 09809FF974BA3CFB192E8421170CFE8CE1707EEBF455C8AB32FDA6C48534C3E7 + 7C6972B1B860EF57B3F5E29BB261B0D6F92B4C94CE5F6DF2A2FD95690B552F4D + 5DA8B9615E9EBE445EC1C5BFAD976710F012FA32C4B3091BC4FA6B73E806C64D + 03FA5E99BD4C5EC58D867D33C11B8DD7F82B54E84DDC94BCBD649DBC8DF2ADC5 + EBE49D6504BC84C0EB5CF0F7DDE51BB5EFED65EBE59D959BE4FD555BE4FD959B + 4DB966ABBC0B31FF0E4330BD8D9BA9F77033F5FE7A6FF970938FDE987DEA8E1B + 2FDCA4315C13C332B18F219D3EF7DCA9E5A7B8A963D8278680D21BBB6DBE9A2F + 68BC5F90B941B4C45F0E53E3995F6827DDBDB8910DC20D6DE06EF93288F97B43 + 555F06EF91497B70D31BB14FA6589A14811BE3C8033275EF7E991C4ED76F944C + 8B3A28B3A20FCBF4838764EA8168998672766CBCCC3A1CAFF59931B8C94F4CC3 + 4D7B862C48C24D7D2A0170AACE9943B76F7C226EC64D39273E49C716A6A5CB7C + 3A7FA1C59959B2089A939428F3529255ACCF4D4E52CD8C8F9319D0AC4402E504 + 8CF34B8B22D974F4A82CCFCB9105E929AA8519A9B22013F58C24999F9E6894C6 + 3201FD89EAD49D4BD72E5DB5D989B23A2F5573E42EE3AFD719D60C5A8EFA4A3A + 6E317F597ABCBA700962198A59C3325B90D6CE6FB588A03613FD84C0185B9469 + 802E1DBBCCDF4BF7EE32EE658961D29613FAA6C7E118095A2E49A5D317C74F89 + 51B1CE394B30C663CCD32F480E1A589C9F22AB8F64CA9AD22C595698224BF292 + F40B1A96AB4A326443459E8E9B106FC9A26EE1AC78D40D38E6972E2BADD0706B + 991FAC305D56332718CE6B616A34CA587504AFC8617868532EC98C9655398765 + 433173F9A6CAFAFC4471C16096F9F1B206759D5394285B4A531500531B4B9265 + 13C12FBF143B9A269BCB5365EBB15471AB641EDF2C1342AF9C79D3B26443798E + 6CAECCD32FEFE8DEA093635B4D89F8B59E90C0CE5A758D6E6FAE941D0479900F + 41597B8DEC3FDF2B872EF54B705FAB78B7D78B7747BDF8B6568B7F2BC3241ED7 + BC793BDB2A64DBC912D95C552E1E2DCDB263E08C045DB826A197EF4AC0E065D9 + D2D42DEB2AEB64EBF15AF1A8AE916D84C035C715FA1A005C265B2B8F6AC867BA + 7EDD4E98F0CB84C50CB7ECDBD0283E905F53B304B577CBCEE64ED97EB259BCAA + 1B30BF41B6D7358A6F7DA378D7E271D4D78B5F6393ECEEEE913DDDBDDABFA3A1 + 49423ABB24B0B50DCF4D95EC6A6D97237FFF4F29FDFBFF92A8D38312DCD12D61 + 3DFDD8BB4B76B575E2B1354B40678B04F674885F47B37837568B47039E93D67A + D949D76D4793F8349F148FBAE3E25E5B89E7B41C3A8AE793A1ABCBF03C97C896 + E34770EE0CA57C4C3C4F94E25CCB248021981B8E8BE749C2D16338573CBE93C7 + 14EC6E2E2F94AD15C5E2515506E139292F900D25D9B2E5689E78559588474581 + 6C3992259BF1DA24C075AF2A122F42DF1345E20ED92E1CBA5729AFDA12A15376 + 7B7DB1BAB429E66C666870F7CA6CF1AACC159FEA42F1C79C80FA23E27B92E1C3 + 8BC4BFB154029ACB25B0E99804361F9380C672F1238C6DAC909DE8F3AD2D5310 + EB574F676C051ECF51888E6094CC8DC81F06B4D2A57444BFACDEDE50AAB0767B + 03E16E39FA8F618E81BDDED8D7B87D8F4B60C70975CEF2CB758A5F8C87761E97 + 3DFC321CE52E7EC18EFD83DA8EE2FFB24242BAF0FA4319DA5D297BFA2AD12E97 + 5D1D65B2ABFDA8047594EB5830F608EEAE923DA78C838B30366CA05E22CE1076 + 1092B4CA41BACE869A65DF609344A37DF8629BC45E6A973828F16A97245DEB96 + 94ABDD9274A94BE22F7448E2C52E49BEDC2349E83B8C3907CF35C9FEC1063930 + D488F5CD1233DC2A872EB4A8622E344BF4B906D97FB656F69D39A9F51886683D + DF88F14689BD4C1764ABF6450FE3F817B11EEDE88B8D38B77A89419B1035F926 + 43E3764BE2F54E6D27DDC079DDC279DC615ED4010DEF9BFBD3A0E4FFC290BF43 + 1AF297CAF991217C4D186296769D62AE573B8F6C264A8EE5627E9EAEC31E50FE + AF17A4F83F2FEABE749DD2614A304A201677852173E9D624ACEB96CCDB5D927E + B343015BCAB53649C37966DCEA949C6F7A24FFFB53520015FDD42F85F7060CA4 + B48061CA9D4E618E578A795B290304B1C75D3A6CB13FF66069C0A00189543AD6 + D2CD9971BB4DB2EEB64BDE777DBA7FDE0FA730CEBDDA24E35BE686EDC5E3EA97 + BC1F4F49F6F7DD92FD5DB7E4DDEB937CA8E01ECE0D65DEF73D92FB03430313E8 + F64A16E665711ED6E4FF3C2005BF0C48F17F9C9592FF1C74A9F0D7D392CF718A + 8F0DE25E856817730DF6E63915DD3B2D253F9F91925F5042A57F3F039D9523BF + D0E18BFAAF4306FA12F44215FF310CB13C2F471502639C25DA04C685BF9E9502 + EC47B09CFF231EEF3D3C3628178FDB86A62C191239FFC7D3C65DEC10A1B40DA6 + 8BE84C8608910978A9029C57A1EE7F5AFBF558D678EE8FFD783E4F417D5A66FF + D0A37977B3F1FF4B114213FCB2CE63F0F814EB0CBB4C184C284C65F03CF17F4A + 10CBD70E5DBA9471E89A31D6096C09770973336EB4ABD2F13A4BBBD68ABFC746 + 49186E9014BC1E932F354BE2850649433D97F0FE265E3F57F13AC538FB532FB7 + A0DD86B5ED9285D76606FF8E2E374BEC853A891BAE97844B8D127FA949E2312F + 415FE3F8FBBC84127F93F184B8D7DA251EAFED589C231DC3742EA7124EE3FCE9 + 04E78F0718C29A4ABAD181F584C8FCC102DE2FA004BC9F50C937F0D8EFF2F9BD + 040DE339B1DCEB77F1B78132EBDBB32EA5315C39DE83F89EC4F714BEDF445BEF + 1D7C3FE0FB42CAAD1E55B2FE60A2CF720EB72904B6816FD840AD449C6D50E04A + F84AB14D1100D3EDAB619215FE1A30EC0C9BCCFA1EECC19273E9AA2540A5A2CE + E3BC2E76606F86BBE7FE0C5FDDA9E0977D61A79B65F7007FD4D2A24E6096E167 + 9B353434C5B9872EE33AE38A090FCF88040C09CFB6711077A80E1226A3CDB594 + 092BDD2C61671AD509BCABA75A424FD54950EF499C730DCE158F5DCF993098EE + 5F23E623A6B43DC03EBAA0DBF01EDCA19F13FC11901D8E391025812E43341308 + 9BCF34BA6E4DC91F1F311F30612E212EC1AE71F39A5CC374FE7AD61729B4F56A + 30F97FB92FC338339C33DB3EF85CE3E7E40EE6E56DC3B1DA8EE35AA44AE58FCF + 556F7C962B84850866E9FE25A4DD5A8D6B027C86335FEF8EE6E3F8DC65EE5F02 + F0663C1FCDB2ABAF49825006F636887F4F1D542F3B3A6A706D47177011AE0373 + 70DD9627EEF5B8F6708464A63B98E7CB73E3397BE21A82C7F56E3CA2D09873B6 + 9D342E5F85D02772C513E766436902601B02ABAAF315026F3E912F1BAD631034 + D3FDEB5E8DE3A3DCCA68352772B04FA1FE88CBB537B405E39B306E8781DE8C3A + 4568BBAE1CD7BDDC136B15005BF077D3715CF7A24E08CCBECD68338730A1F1EA + 92242DE9281E710227C8DA9244DCF7A4E3FA2ADB0581D50DCC6B755C8B2FCD8B + 53ADC4753745D7EEF202E6144E96E58589B21CD7E7AB717DCEB115C5B827401F + C75614D1E58BFB8FAC68E8A0E61A5E5D441773BA82628680A60B7841D6619997 + 71484500BC18F707CB1871C80180194A7AB433D886C19A27383546E6241E5019 + D730EFA9700F847205D69B7B113A80710C3A82E3F76B586842E06907C3702F1A + A1A1A0E7C713F4EE93E987F6AA5827F4A50398409810782E730C1FC25AE60F8E + D9AF9A7170AF3A82097C0982E908669DA07876CC4199C2FCC10C15BD375C2685 + E31E19FA3A6CB74C08099289A121F275E81EDC3B87C8040BF84E0C0A96AF8259 + 120407AA08873FDBB94BC6E33EFBF3C0102D27E07EFBCB9030F92A345C26E03E + 9CA19FBF40C97B72CEE18FB2BF0808D6FBF64F7C0990711FEF877DAC10D29FFB + 616C4730EEED030D0086087E6D373001B10D8159EA9887BF7E5FC0EF16F87D02 + 655CC3010A7AC7B9EF8458FACB475E741FE31C76EC96AF0223E433DF1085C01F + 505B4D743246217B9F0EE20D0C21EDABE5DB6B4C88681BFCBE87365DC184C104 + BA84BEFA037888751B0633873065E71436807893BCBE649D090F0D319730F5AA + E504D61FDB334FF002FE30DF7C37634361D65D3078EE0A798D9ABD5C5E9BC9EF + 8098237881C9113C75A1BC8C92A261C0180716AA5E98447730C1304130A1EF5C + 13896EC21C79EECBD9268FF0241B04CF9417685A601E6187EC70D0B6E3D759B7 + 61F0339FF1BBB229637EBFC6EFDE9E659DDFC1413464D09841304C3DCBC87C56 + A43E8ADFDB11FEEAF77F16007642601BF4DA621FC79C7D361C76EAF1378C0BF8 + 099476DD097B6D10EC84C4B61E7F157AE57D79FC6513A2D906BEB69C7DA361AF + 13FA3AFB6D11EEDA00987567DBD9AF25648781B601F0FD50D81882EE07C0BF85 + BFCEB1D1E3CE7E1BDCFEF5697E773DD21E4BBF81BAFF484ED7EF3F900D80EDD2 + C901C682BBA3C5B1B1C69DECE04190D6D53726FC35D2BCC0D63CA71E04809D75 + BB6DD71FA6D1EBFE1D7A20F01DDD1ECDC52CD97DF6F883E6D97AD8D898B279E1 + 68D9630F9AFFA0FAEFD1587B8DEEFB7768AC7350103B460EE07F15003FF2C8EF + 04C0CE93F96734FA01FD19B2F774EE3B46DFEF79F1DDF7021EBD27FB55CE3FA2 + 7FEF1FAC5363BD61983EF3A6473D08DC3AC1EE83E650A3E78DF5E6FAAFC8F986 + 3D5ABF998F3EFDA070F4FD2B009810F741FD0F920D7D5DA017F5C7D0FFA8635C + C7F0A16D436005C10F80C076F86757D86787980398BFFA7A847A1E173004BD0E + F8AB72C0DDC75EC2850FC43AC1B00D7FEDFEBFBEF481C25F8680763A7F5DD017 + 7AFC35025E23DBE9ABE017B2DDBC04C02C5DFD6FE0C2ED4DF4434EE06B435F96 + 4FF1820FD23EABFDF4BB13E4E977BE44897E5E0072CD3BE87B8FA0778ACBF14B + 11FC3E3D0E179C10DB7A21C95F148EFB527F79C88BD3E73F9D26CF7D32559EFB + 74E442552F5A3F9DEE920D8029EDD39CBEC6E96BCB00610380097F19E2D986BD + FCE5242FA89DAE5F962F4F9EE7127F8169FF4AD3F96B4DE674E185BC0D805F99 + BE505E9BB554DE9CBB52F53A6E005E99B9547F0D4AD8FB2ADA4E00CCB6F9A528 + EA0EE8CB5F98B2CE1B0DE6A4B19DBDBC01A1087AC7ADDA22EFADC08D0CFA597E + B01A373F2B7153B372136E82B6CAB875DB64DC5A37F900E547EBDD651CF4FE46 + 2F17E0E5AF7299A3474BB71D9ACF87376C2C79D3F6B9376E24B7EF72415FB627 + E0C66EC20EDC2CF297BE3BD0C64DE3C45D21F2256E20097AC7F3E6D102C0BC99 + 240026FC9D1C1A215376E3C6373C4A268545E246365C45F83B75AF81BF53A30E + C8F4FDB8A98E3E2C330FC6A01E2DD3F619F84B903B232656A61C88C60D367FA9 + 9D2E8B52D2B49F7077715A862CCFCAC18D7C9A0BE2CEA76B373E1EE3B1323B3E + 4EE624C4A32F59E1EF1C9433E2B13699219CB14F4A8ACC888B93990904BD989B + CC50CB29C2D0CDACCFA33B382D55C130CBF5474A644B050170968E5373531265 + 6E2ACA74ACCDA09B986B700E69589F41476F822CC0D8C2D43805BBCB53636525 + FA0861095E9740CBD0CF3EEDC7F8E2A443B224394656A4C7AB96632E012D6583 + DB955989B22A3B49961018A761AF8C3885CA84CC2CF98506FBF805C8A2541CC7 + 5AB70CED25A9876461D2015990B85FB530F1802C4A8C36FB630DBF08999F1C8D + E76EBFF942057BD2A1BCA22855D696E7C8EAB22CD4D3140033B797B64B988338 + 59DDC18BB21364496E922CC3D8B2825459599801A5CB0AD4A9D5A8AF53109CA1 + 21DF96E732BF588AFEFA7F6DA11D163A5E73046F2849C3DC54D95098A421A099 + 0B986248E875F99853102FEB0BE3656371B2B81DCD10B78A2CD95C9E2E5B2B32 + C4A32A47DC8F678AC7894CF13C91235E35F9E25D5F2C9E75CCCF7644DCAA097B + E9E6E0177A47C5BBE998783596A34D7764B504F5344A6057BDF8B79F14BF36BA + 84F8256DA704B4D74A485F93440D75CB9ED31D12D0D726BEDD4DE2D75E27BB3A + 6A24B4A74642BAAAC4AFE9A878D5978B677D8DB83536CAC6C656D9D2DA2B0183 + 572578F896B8B59E92D595F5B2B9AA51DC6BEA15D40612B232A76E4DA5789E3C + 2E5E274DE867966E9595FA03846DC78F8BF7C93AD9D9D226219D3D12DAD9079D + 925D6D7DE2D7D081B5CDE253DF2A012D1D0A6F83DADB715E505B87ECED3F2DFB + CE0CA2AF53FC1A5B748FD0AE1E09EEE842D925D1E7CEC9E1E161393038289103 + A725ACB757C2FB4EC9A10B1764FFB9413CDE5ED9D5DB213ECD75B2ADBA12CF63 + 95F8B4D6494067A3F8349D14F75AE6CE23FC25F06588E5723CCFE50A7EB72AFC + AD100F02E1CA62FC5F15E0FF2C4FB61DCD976DE574DC1E115F8CFB60DCFB4499 + 6A7B35CB522DB7A3BDADA250B694E5A0C4FFE5F162D95E8975C772C483E19AAB + 0A15FE6EAF3D0A99F0C87E4D0C11592E3E8D74D5D2415B243E0DC5B2A3B14402 + 5A09462B14A2FA3794887F7D8904351F95DDEDC714B052BBDB8F4B5857B544F4 + D5CA9EEE6ACCAD9010F487F5544B18DDB03D7512D5DF2491A71AB5BEBB0BFFFF + 54275E031D272458BFB03E2E415D26A7A2BAA0B03FBFD0A6C37627C3277732FC + 73BDEC4249E84B85586ED9C8B3CDEAC0A2DB2AF26CA344D3553AD42807E88A25 + 7CE8AD9288FE6A85A90787EAE500147381B0A64D12AEB4A2CEBE933AC6FEC3C3 + CDAAD88B1CEF94A4ABBD9274BD4F926F9C5231DF6BF29D7E052C2937FB54AC67 + 7D735AB2BF3D8D7AAF2AF3F629C965DF9D0149BDDA23C9973A257198B00A6278 + 571C3FEE52AB245EED90E4EB5D92C69CB110C30053A90434373AD0D7AD4ED58C + 3BBD9887B9E8CB62A8DEEF06B42F15E384ABA97410DE26406AC7F94298A7EE43 + ABA40C60629DB9518DCB9680D4066F747EE6D2817A6FE83EE56A5ED933FAF8E8 + 7ACDFE960ED87EC9FBE10CC6CFBA4AD58F8368630EF624B4237C2570B573AB32 + E76A06DA0A387F1890421EF36E8FE4DCE9963C940574C17ED70375E1F9EB92BC + 6FBBA5F8470347B3BFEB955C94EA2655986894F53D9EBBBB9D0A7F096F33BFEB + 960CAC637F26CA1CAC23D4D4F53F601FF499BD3BA5E0FB6E29F9B15F4A7F3923 + 477E3E2D45F77AA5002A445F31C12B812BC4E3530A6331CFCCC5B97EDB83C7D2 + A390BAF43F06A5F857023AAC4559FABF8684F0B7E8EF6714FC96FDEFF352FA5F + E7503F2BC57F3F8D390372E4970103767FC571B067091E5BC10FBD92FF438F14 + FFDC8FE361AF9F796C3C5F3FF1F8A6EFC8CF0352863547FF6E41DE5F862CE138 + BF0EBAFA4A7F1EC4FC4129425F211EA301BF786D125EFF40D0DBAF2AC07109A4 + 09B809B4EDC7C0C75F8863B980B5058E0D2026A41D50B89B87F584BF45BFE0F1 + 426CE7E3FC09BB8DF87F86D7F2779D0A7F09D739AEF0F8279C1764C3600399EF + DFDF29DD1BFF8FFC9180866BBE86D7D4F536C9C6DF005F4B36E8CDC46BDC2EF3 + BFC5E3FD06AF03CCA1D89F72A559416F2A4A4AF720E045C976D27083249CAF97 + E48B4D0602E3FD8220389960F742A3C49DC7F830DE4B503F3C5427B1E709825B + F1DED1E27A1F895798DB26872F61CEA5060D5FCD90D1EA68C6F967E09CF877CC + BFF354E637E6DF3894768B61C1F15E731DEF1D50CA0DCEE997F45B0378AF19C2 + 6BFA1CD60D683F7F7062FFE884629DEF5DF157DA15265384BF0771BE36042698 + B65DCA09780F52D07C83EEFC0E971B78DFB966D97FBE05ED763938DC863E867C + 36EE7D8E11026BB86788A098F0378C397451869E224835D0972250A5183E39A8 + 07EFFBBDD5F8ACE4FA46B46B24B0932ED63AAC656EF666D98DCF8B905EE63A6F + C03E4D98DB28BBF11E1F4A007BC66AF713C2F23C3B71DEFD127FFDB42AE6721F + CEBB4B0E5EEC96435798BF189FCF97BB70BE84CFCDE8EF9203C39D1275AE4D81 + 2EE12FFB5906E37365CF4003C47E9E338F5FEF3A4786FB6719C430FEDDE67168 + 28E81EF441815DF8DC6A2F17DF1646B428C2F54DA1AB74273485B6D5E48A476D + BE6B8C0E5FFEF88930D8AFF5A8B6D5F9DB7C44812B45E84BB86C877EDE8E3182 + 55F32329465529C7B50E3E7FF199B983512CAA8B709D03E1339D5250CC54077A + CD55A6797A296F5CCBB1BDA3A50A8FB7198FB74D8270ED4608AC39E2CFE0FAE7 + 74ABECEC69909D5D27341435CF9DAE5F2F9C3B9DB99B4E64C9B63AF4D51BB7AE + 27CE9DF2E2E3E2638238D7AC211426982EC0F380B927F1F82CB7F0D69A02D95A + 9DAFF077E3F11CD96C4160BBF4A093F9FFCBDE5FFFD77165EBBE704C493A4C8E + 63071D72C88E1D33B3CCB22DC99265CB20D96266666666B6CCCCEC304327DD69 + DA7BDF73EE7DDFF70F19EFF38C5973A9ACC8E9F4DEBDF73EE7DEFBC3F39958B3 + 6AD55AAB6ACEFAD61863089F87201DDBEC244CEEA4FB67CC5DFB30B7E1388EF5 + EF0EB45184CC360630C12DF39B5B2C00C6FC17C7B4BDB35421B04F5799E6BDDB + D00F220066BAADC300E28D4DF91E57D084C15E9857AFAFCD920D75D9B2B505DB + 639EE5DD847DD517383180B3319FCF9555D599B2B20A6B98C61CD5AA6AAC2350 + 5E5B9725AB6BB056A9C5FC1EE3D1AAD8AB1EDBD87AA4EB505E5793295ED84685 + 7D6E68C09A01737FEB029A56C4CC130813FEAE429E9E8468F5CBF58A05C0AB4A + E80A1A6B26BE60CB350BDA2C0C26ECD5B510D73FE8A36018E28BA8417D2DB2AB + A74941F0D2DC1459928DB551763294240B33E2655E6A8CCC8716672418388CB6 + 7904BFE9F19A5F929BA6E0972EA1098109830980E7A7C4CBC274AC6D1D184CCD + 4F2510362EA2098117A6637F59E958F7F225E81899C3F8C10E0426009E1E192E + 3363680D4CEB60ACBBC30ECAFB61A1323302F5D00CE4DF3B488B604260ACBD43 + 2314F812FC12F412FAB23C2332562130C5F5F79CB8248C15E579299BFD68313C + 9510D871134D8BE1697B0F627D1F266FEEE11A3F44C5E70084C216FA1202DBB0 + 4E2C53F61982C72AD87123FD5A20EA83C2E48DDDA10A7F5966FA5650B8BCB33F + 0AFB0993A901D87F002D8A83652A41F036C6140E52086C6308F385F617A0E737 + FACA8B1BFDE4E54DFEF2D2C61D32C56BBBBA8166CA32C1B0DB4A9890F8A5F57E + F2E23A5F05C104C44C9F5BE32D93566F9267576D92E7D67ACBE4359BE5D995EB + 65E20A68E54699B492CF608C65302D84ED3319EB2E9A9AE84061EAB9C5EB6412 + 9FFDCC5F2D4FA831C04A99883C9F0D31350602B4145E2184C184C084C14F7DB0 + 5C53C2604FB83102E0990BE589F7699CE08060070233663045AB60B504260876 + E51F7F77BECA3E077BF88D0FCCB335C83E5B73CB1A6158084C28FCC8ABCC4F77 + 2C818DC73E0381DF513DF4D2DBAAD12C81ADDC6098E0D70D826D9D6AD2AB72FF + 73C352F7D1A89BE080608F25F0C4975504C46C23307EF0D9A9F2C044E41D10AC + 56C14F0EC3DFD1A0F048D04B59003C12F4326FFBD8362BAD83EEE3335EF41DCD + DAD7E48701F04898EB06BCF7D2B8874D3A12005BF8FBF720F0A890F7D7E402C0 + 6E0B5F77DE63C4E5D4DBB6B12E983B12ECB23CB2CEB280B190BBCD53EFC9DF8B + 97A0CE65013CE641F487FE1E00FE7BFAC53E5CFB1D4DBFB59F5BFF9E6DDCBA27 + F01D59BE073BB3BCEC17ED23D919C43E63273C66FA8E68FBA7C9BDDFD1F2BF45 + A38D35B2EEBF42A31D83825807F6BADD39BBEB2CB0754BDB46D68F028047C25F + 2BD3F6BF300076E747CADDF7EFE8AE1FF3C83158AF72FF89F8E773FE80235C37 + FF3DFDDA1FF7B7FCA947FBF39BBAE18B9E3D3E7B31B41A3EEED1DB47CA5E54AD + 46EB6335B2EF48FDBDFEF6223EB2DF3D01F028F097B200783408EC86BAA3D5DD + 4B16F0327DF08917E5F5F716C9722F3F99316F35CA2F61BF38F6DF0DBFB96541 + 30A12FAD7A6D6A44F08B89898AC0D78060A6E31FC5A4E3314C281C003CD601C0 + D68AD70260D659E8EB16FB1002333FE1294C84E8CEF9A9A977C5FE25FCB5AE9F + 097FEF7F16932A9726406EF8EB71E3EC80E0F11399A2AF037FA991F0976D6E00 + 4CC87BBF070A5B004CAB5F038609801F791913C9576639D6BEE68D42F356A181 + C0A301604E3E097B15F8D2358D6B526A5D3EBBAD801F7DDD81C22E006C63FF1A + 77D07374E2FBF8BB0B4CACDFE9C6AAD700DFF9F22426D58CB7F2D44CBADC3113 + 6CB71E67DD68D017D2B7396763E23E7B2926F52BE4B9456B65F2622F99B4788D + 3C43CB5E025D949F5D84050226FE84BF6E004C3D83458202DF651B3C9AEC58FE + BEB8768B6A321620CFAD2000F636D6BD8EA66EF095D736EF50D8FB1216422F31 + 25F4453A758BBFBCB675A7BCB66D975AF8BEEA805E425DC25EBA68E2C2EC0DC7 + 8D13176FEF60A1F736167CD4B420BED91B2CEFEE0B9519C1589C21A535EF0C2C + 16A71FC4C2130B4DA32885C16FEF3F20EF04D3555598BC15C2856A84CC8C8AC1 + 62334E6663B1392B96B17CB1E8457E4172BACC4B4CC582375E662724CADCD414 + 99979C82057192CC4DC2E21AF97969690A79E76764C9AC9414999F9921CB0AF2 + 558B737364495EAE2C2F2CC022BE5096E6E7CA829C2C599C9723CB096B333365 + 7E5AAA2CC4364B905F96932DCBD17F41669ACCC1E27B4176862C42DD42A4F333 + 5364415606C624A4CD9515B4E8CDCF41390BE3E7C9AA922294F3745FEBAB2A54 + CB18DB3787DB64E238B05FA44BF3B3656571BEAC2E29C418B9B2946EC5F271CC + 85682B44DFFC34752DB6243B51D6161B10BB34275901F02AF45DE9C0DD65B929 + 5ACF54A12FDA56630C5A075B0B6196BDCA8CB5EF0A8267EC872ECC96E7A76BFB + 5AC70298AECD68C9BB986FBDE718A84CB8BC2C1BE3F301078F27331169A22CCE + 8C5728BC3C3F4596E5A32F8E657961BA6CAC2E942D0DA56AC1BB1129E39D6D6E + 2EF7C0DC3555F90A83097A69ED4BEB5F75D15691AD560074E1E6556D5CC4F141 + 0EDDBBADAF2504CE576B026F686363816C6A46BE8971838B646B53A16C6F2916 + BFB652B413BC3B31C86A7364535D2EEA784C0550BED39F2E9E8B65675795C6F9 + 556BCECE32F1EB2A17BFCE52F1EF2A959D3DE5B2ABAF52E11FE39AEE3EDCAA31 + 7B771F69933DC7F9E0910F3B7BE5C0A91E7519B8973ACA87915DB2FF64B7BAC7 + 4DB97D4EB23FBB823EB486E997846B2725E1C61989B9764A422F0E49D839BAB9 + ED9383DC8656A087A023EDB2F7789FEC3E39283B0EF7428724F4CA3549FCF863 + 093A7D4A7CFA7B65E750BFBA3DDE7BB84F424F1E92838CFD3AD0267B07DB65CF + 50BBBA07E6834CDFB61A7CD64AF16BAFF5C0D283C70624FC04B639D22707A090 + 218CD3DF2541FD9D1282F1C28F0F6A5B08DD2DA32DF2E461893D7B5CC28E0DA2 + 5F8704F5B649E8D17ED40F49F0A16E09EA6993E0C16E893A754462CE1C9328F4 + 4FBC408BB0EB927AF33C3EDF90BA26F6EF6A107F02D8FE2609C6B93A40F7C1F8 + ACBB069B65FFE1369CEF7AD9D98DF6BE7A8D99EBDF518963AEC6F7538F6D6B64 + 7B6B99F8B49628A8F7692DC267C277D45D89F62A4D77E17BDC3FD484E36C9308 + 7C3FE14843F15DD132774F6FB5ECEDAB91FD04B6430D385F8D6ADDAA30F7789B + C2F7F0D35D1275AE47DD09879DC6B9A0D5EB298C751A6341B4948D3CDB2131E7 + BBD1A74BA2CF764AECB92EB5484DBE362849B45EBD7A4892AF1E96245AC25E39 + 24B17CC87F8EC0B5472DCE526FD232F6A4E47D46487A11E959B590B5D6AF0957 + 0634A5E23126E350D2F28C1A060AC602CD5A9E11F4C65C1A94E45B18E7E63175 + 276C2DD72875958A3485E3627C5AD7AA45DD87C714B6D20AB6E04BBABB3D2B85 + DF9E939C2F4EAA0B56BA5ACDFFEA9CC67DA5E86E98B1640BBEBD24253F5C9792 + 1F6F4A11D282EFAE4297A510629F42B4D31AB6FC87AB52F17B5AC75E90C26F20 + 8C55F2ED4529FEE692421A053338CE3C28FF33BA90C5717C7E46087D592EFAF2 + DC5DA0B554DD0C5F50A85AFB87EB0A59098A0883DDE09520B8E27B63C969AD25 + DDF0B5D8EA6BC628352E75E9AE96108AA2EB5C133B95D0E984B65B204CAB4CA6 + 2CD3FAB6EC7BC241EC03326E93B16FBA35FE1E65BA0D8698D2E297309A2E7175 + DC2F4F60DFB4A03520D65A9E56637C5AF9BAEB18CBB65AC7C7FEBE3DA3C09660 + 91625CDAEA3F5D93DA9F874557C384873C56056ADF1232D205F3599C2302EDF3 + 06FA3AFBB6D6A9353F5E52005DFFE365173CA52BE56B528F7D58B7CB7504A7D8 + 374124B7617B13E3F1B21FC6A9451DAD87ABD9F6971BD2FCB7DB66DBBF5C9796 + 7FBBA36AF8EB0D8FEAFE8CEDD04637CEECEF11B721A8C5F1702C5ACF5A2B5B6B + 696B558772038EB58170F7C7ABD2F8078CFDD375E45987B1FE78139FE5A634FF + 7C4BF30D7F441BC6AFFFF30D1D4B2D6B19E7F7278E87E3F9F926F67F4B7F67F6 + 77558DDF721DB66B600C61ECD30DF92BBEA7EBEBAB38CEEBD055331654F313BE + 138CC17DD0F297DF0DA596C13F63BB1FF1DBF81EDF2B2D7FA16AED774DB7A57B + 6977CC613B262DC8A9E1B23976F631BF33FECE8DFB6CFE1728C2548AFF17C664 + E67F86C75CF53D3EC37748F52506FCD7F9DBFCFC84947F7DC67C87F83DD022BD + E0E3A3924F0BE08F86240F29630033A5A56FE6ED43927E6B48326E19F7D0391F + 0EA9326F0E4A3A757B48D2EF40B4CABF7148ADF4333EA46B735E8B7A55741B9D + 816D690D9CC558C6DC07FE87F9B80EE57D7A0CD70AE43FC57F87D7A4CFF03F82 + 2C18CEC6B52CE7237A19E04B2427702C472585D7535CEB784D4EC5F52ED52927 + E23A4A8F01B4BA4FC371A7DC1C90E41BFD9274BD4FC57CF28D0149A55BE83BB8 + 567F7442AFA184BF84C0F42440A9B701A484C7C6A2F984C2E484AB7C01A75712 + AFE15ECF58C2E73B25F24C9B449DEF90B8CB3DB81F74493C52E6A32F74A822CE + B6E15EE4785B38837B0DEE31614C71CF8938633C8344D29258C57B0FEE01B84F + 4540BC17D8FB02CBEA769F9E1A2E62FFD709AD714FB87D428CBBE8A31EB14C57 + CD16D626DEA0CBEBD3A83F89FBCA1107F80E2A000E3BC797D730473983F1D532 + 9829E62BB86F3276AF75DD4C6F131ACBB7BF56760FD4CA7E02DA632D0A48F71C + 461F5AC0F656A9B56C407FB5C25EE6091999FAA38D209530582D6A1D28CCBE01 + FD15E85F2541876A64675F19E636E5B277A84E428E3561FE63BC58EC3B5C8F79 + 4F8DBA86A6D78CBD50C8517AD5C09C822E9AF585BE4AF5E0B1971E3670DCFB18 + 1318DA8B79C37ECCEFE881C302E23D475B31C7EB9660CCE5824FE11C9CC73DF7 + 1A63309F92E82BC7F15D314E3DE65027185BBF069FBB06E78210169FA3BB42CF + 41605FB5F8B49728900DE03C05F3905D74337D8896CAC6B308433D3006B05F4F + AD30C4847A1F411F5A0A6BDC60C782776B47B978B797A9F5AF4F4F8DC2E1007C + B6BD38C7EA298496D43886005A15330630FA736E14D08D7EE8CBF3CFCFCC971A + 39F7B5736633A72AD179B12FE6CF018C29DC89797507CADDA59A7AB715C8D676 + CCA15BF230A7CF964DD096A65C281F79C6F4CDC11C3D5BD3CD8D795ABF19F370 + CECF29CEDBE9AED9BEB4B981B180D17F23E6E64CD754A4C9DA8A74F1AAC4DAA5 + 2C55D3755558B338E93A5A16132E576560FB6CDD86F0774D659AAC2C4FD57502 + D715B4F22508663C61AE1F087F098157D15310D614AB191318EBA855A548B1E6 + A0C52F412FE12FC1AF958D074CF0CB325F70E59A88E9E6DA62CC8B31471F6851 + 2F44CBF39215002FC6BA8856C08B321314022F4C8B5300BC046BB345D896F58B + A125B906002FC43A92F077B642E0385984B515D7718B32931402538B681D9C81 + F5661AD6B729890A831765A00FEAAC55302D822D1C9E1517AD3078767CAC0700 + BFAB16C1A1F27E44B88AF97768011C1222EF858661DDCD7ED1323D225ADEC7FA + 9BC0572D7E69051C813234271E6B60ACC10985B55ED358792F225666629D3E23 + 3A5EDE3A10216F3871838D3570A8D11E1BDA2942530B7DDD50D88AF5B41236F0 + 778F71191D142A6FED0DF740606AAA3FFA40AFF8EFF740E13702B16F42604FF8 + A920C71AD800607DA9DDDB5F5EDCECA72FB9BFEC1D20AF6C099097379AF8C004 + BF0A8037FB2B28566BE18DA8DBE4EF81C5D67AF8F975B424DE2E53BCBC65F25A + 6FE4B7C99475DE3269CD4699B86ABD0382691DBC56F5ECCA0DF2DCEACD3279D5 + 66F5C446206CA030F24B37C8B34BD6CB64A4D4A4C5EB3CCF7B98B24C190300C7 + 53DC1C860C5B214FCE36F927917F7CE60A7974C6127964C622798C0608B39640 + 4B516F603101B10D456684BC63D870B791030D1F088C4D1DF38FBC690C2C1E81 + 7EF71A9FABD1BBDE2C79F0D5F73D654DA7BE6F9EC5392EA3D53DB42B34DB0384 + C02FD3831FDD42BFA53098AEA2DDEEA129C60AD6B8C12ED0CBBC1B10DB36C610 + 7E700A53C60D260446FE3942E0A90A7A4702600B81A9079E9D2A0F4E443A71AA + DCFFCCAB32E1E95764DC93E80B8D7F8ACF577F1B0076CBC25F8A65DB9F90D742 + 616A1CDB187BD8691FF718AD7D6934E4805B0F04B620D83C4F1EFBC83008FE35 + 79B6FD0D007834D86BFB8CD6369A6C3F0B74A9B10FDE5DA63C1098CFF347D413 + 005BD04BB99FF7BBF980875B301D4BB0680CD4940B386DEE31DCCCC42D652713 + 20C2DE11F057EBFE41006C2D82EF6632C3E57BE9EFF673F325277FD7BE583752 + B6FF3D744FE03BB26C99D808595E366ABB8BBDB19DF05701F088B67F9A46F2BA + 7BB5FD3D8DDCE6DF3BCE7F54A31DC35D00F73F51BF80C5237417201E47D7D094 + EB40FFEF2C5C6C8C86FF2CB6CCD4531EE50FF7CF92BD58DC4BA36D63355AFF7B + C9B838B81BD05AE9C5D7B9D8DA0B2EEBED45773C2EFC942D0F5F884DDF7B69E4 + 7E468AE3D8FCC86DEEDA9723DE80EEA571BFC3CD0D7DF446646F5EA3CA05817F + 871BF54353E4774FBD228B56FB484462916CF60B93479E7E03DF3BC67CD8005C + 0380EDCDDBDEB8CD4DDCCA4E18468A93033B81300098130F4C4820E6C73CCAB7 + CD5E94FB9F7C59EEC7848513154E58C63EF1A28A654E64ACC6A13C161A833E63 + B0CD7D4FBCA210D8BA80A6C5AF07FE2AF4A56BE761D1B53361AEC25E97BB6796 + C743E326A37ECA2FE12FE586C2B6CDD619F86BDE08D4C91E267E741BF328268A + 0FA9DB184E144D3C111B6384B2E057638E4C45DD6B985CBEFEBE3C80F4815751 + EFE4CD24751800EB2476DA1C7984EE6E907FEC2D425E4C7ADF36D0D76836EA86 + DDE53C8909F0134EF991F7E6C9A3EF2FC0C47B894C9CB75C9E99BB4CF38FCFE4 + 1B97982CA34DDDEEA06CF3B62F61EFD3E84F3D8332D3275946FDC445AB65D212 + 5AF2424BBDE4B965EB542C4F5C8C85C0C2552A9627A1DDA62F6241F1D26ABA7B + 5E2F2FACDC282FADD9A27A8556BE74E70CD1C5F30B5EDBE4C5F5E68DD7A95B76 + CA546FB318320A9097B6FACB2BDB77CAABBEBBE435BFDDF2FA8E20793D602F16 + 62FB5CDA8F459689E9F33ADFE8DD7B000BC84899713046DE09316EA2A6051F94 + B70F60A119898524C4FC7B61E13223020B4CA4D3C323501F854528DD4B618119 + 1AAEF0F75DE4DF431F4D21C6369A111D27EF459A744E22AD7B33644E42BABC1F + 93803ABAC58A97F7E3E3E45D2C76672625C8DCF434999D9A227369C59B932DB3 + 519E9391AE795AF512D62E2F2408CE9445D919B23027058BF21459969D26CBD3 + 536429DD6A6141BD282D49966081BD048BF3258CCDC4053CDFEEC6C2DC42565D + C0E7A52AEC3496AFA9EAFA7801FBE4A7CBD2BC6C59CAFD15E4C88AC25C4FBAA6 + B4405615E7A09D16B6591EAD2ACE55D9F28A425A20A7AA35EDA2EC38EC3F56D6 + 14D1E5328170821E071F2A2CA6B212F5AD723E34589285CF81CFB3242B55B532 + 1F6317E038723364590EAD79B36575699EEE6B593ECE494E9A2C423FB67B95E4 + CB7A1CDF6AF6CFC1F6188FE9AADC74599E813CB42A1B63629BE5D8CF8AEC1459 + 9E9F28AB8A52B09F648C81761C9F576996EC68A994DD5D0DE2D3522D74BFEBDB + 5E2BEB190FB8822E96ABC5AFBD4ED6D5D2BA97F180CB902F954DF5B88E36F201 + 51A678559937FBD7D440D599B2AECEB899A38B39C61CA3D5C2F68E32D9D6562C + DB5AE9A2B9587C3B0BC5AFAD5076B6D39297B17969A1502EEB1B8A64635389BA + 6BDEDE5105D1455EB96C6B29951D9D55EA6E970FFFF85030B0AF46FC1843ADAD + 54456B603E0CA36B3D5F3E481BA897DD475B65DFF176093ED9A10F43F9609410 + F8E0E91E61ECD35D18CB3C38EC5225DCE003D853B257B7E99498CB8725FEEA11 + 49A4DBC50B031279AE5742511F3440CBD72AEC8BB1E99A1528879CC498481993 + EEE0E9436A3D1C76EE903E8C3C485784277BD5D574604F8DECEAAC90403EF0C3 + 3107F556425512D855263BDA8AC48FB014DAD698ABDAD95922BB7BCA2400E76D + 6787C9EFEA665A2EFBFBEB647F6F9D047560CCB65AD9D3592BC17D0D2AD6EFC2 + B90B682991A0CE4AD9D783636DAF54EDC571EFEB69907DDDF5B2BFA75122863A + 24EE043E1B1FB4E21CEEEAE4FE4AC4BF1DFBEC2CD5BAFDFD35B26F10DB0D201D + A8423DDB8AF578788C011D0550113E4321C43CBEDFCE7C7CC65CE4F939F22400 + F91D1D3912D89D2FFB0E9549D8895A893DD72C0917DA24FE7CAB449F6992D063 + 351232542E07862A24EC58B5449EAAD73E89973B24E17297C45DEC44DA2D29D7 + FB25F5C680A6F1974C3DCB041829480907526F0D7A4480904EB7A79F1C95CC4F + 8F41069A528C25695C101F96D40F8724EDA323683F2A395F1C97FCAF4F49C137 + 678CDBDEAF4F4BF6E78CB1CA3E83480F49D6674750774CB2301E635E323E25C5 + 589589B4582384C0D8A9778E683DF795F5E949C9A5EBD22FCF6A4A709BF3F969 + 6D675F2AE95A9F245EEB556041E06BDC9D9ED618AEECCFED0A1DD89BF335DABF + BB2045844A7FB822253F5D96C21F2EA2EE22D24B52F4E35529FEC30D55C1EFAF + 4ADEF797A5E07BD3C67E5401FB4245DFA3EE9B0B06267FCDF8B1D807D2C26FCE + A18DAE762F8AC6B5854ABFA1351F81D55929FAEABCBA502EFFE1B294E0988A51 + 5F86FD30366D25F659F603E132B6C5F8E5BFA7BB6508C75BFDE345E32A18F9FA + 1F69957A41AAF159AABF3B2F35D4F717A4E60794D1CF5A7FD295315D1D177D75 + 5C55F82521F051C9FFFC888A75A5DF3AB154098EBF421EDF6305B6A3FBE332C6 + 6EA51B658DB76ADCF25A4B5F05C82813F85A97C20A6309501D0B5142580B7C19 + 83D68AF170352E2ECE27CB162212F412FC56FEF19A54FDE98654FD7C1D65937A + F2F8CEB83DA516A5AEFD55FD8473F9C3398C795E1A7EBE2C4D7F2640BD22753F + D19DB2899F4BA06BE3E836FDE5A63442849F8CC16B5D201384126ED6FDE9BA81 + CF3FA3AC96B504B4D7A4E14FB7A4E56F1FA99AFE72475AFEE543E9F81F1F4BF3 + DF6E2954D6F8BD7FB9A1809879C262B635FEE536B6BD8D3E3715D2D6FDF9163E + 135D68DF942AFCE66A7FBE85E3B82535F8CE6B7EB82675BF27F0BD29753FA22F + D4F4C7DBD2F487DBD2F813C6FAF90EC6BA23757FC43E7F46DD9F3FC498A8C3F1 + 50CC5375E8538B7DD4FCE1A6D442EC5FF3D30D07EA423FE1333A6D5A8FDF5CD5 + F757A5E27B7C0FD83F55F5E34DF4C3B1E218ADAAB05D257EC7353876EE873183 + 2DBC25B43590F782797101BF6BCAB817A7DBF19BF85DDF920A7C36ABCA3FDC52 + 8D7449CEBE25DF5F97A26FE98E1CFF591C1BC597318AF01FE37F90FFBB22A48C + F9CC97314AB14F0264AA18FF47BA5F665AFDD3557C673827F84EF3717DD29722 + 183BF80BFC47BEC66F1DD7075AD267D38D33AF81B8C665E33A6863FCAA9C365E + 2BD3EF1CC675745092AEF7EA7594D7CD4C6C9371A34F52AF7449CAE54E49BAD8 + AE4ABED4A17519E89B75B3DFA34CF4659A43C87C07627A6BC069EF930C5CDFD2 + AE605F84BE97FB25F17CB7245DE896B4ABA887922E754ADCB97649B8D825C9E8 + CBEB7CF20DC2DA6E49B8D229F1BC1F5CED94945B04D2BC0FE0DA4F587DFB08F2 + 04D7B82E7F7454723FC1F9C0759556CEC9B87FA45FEF93CC9BE88BB178CC54FA + B51E49C69871E75BCCFDE8621B3E1BEE89679B55F1E75A24E674932A16F7A8A8 + 930D1275AA4162D88E6DD89F623EFA4CA3DEC7B4ED2CC63BD32CD1A7A033EDB8 + C7754BDC05C658EF9698F35D985330DF2591673AD4D34314F27197F0D9AF1D96 + 64C6E6D59792FA25DA81C67451CD78EAB1970E49D29563F87E8E4BD28D131271 + AE5F824F74AA8571F4A521093B63E2F9D25A39E27C9F1CC0FC27E4789BBE4015 + CC30035030EFF5BD35B81757C92ECCA5F6624EB567A04EC53E1198E7D01B0601 + 31E75A943FE656944259C263F4A515AC718D8C39576FB504F662EED095AFF7FB + A0DE62A844F6F597C9FE810ACC21AAE4C0E13A9CBB56093DCE900B04CF75B26F + A80673AA4AD9D9532A7E9847F87715613F980721CFB9C8DEBE6AD903EDC53EF6 + 21E58B61218770FC3C5E1C8B01C30C01C198BDE877AC4DDD5C27DE382609D78E + C981D3BDEA8D23E4489B7A00091EC49C09F3AE80F652D56E1C7F1014D8518A39 + 29E63D6D98CFB4E37360FEB4A38D2F1B627ED6CB58C09827B7D7604E4BD7CC98 + FFF5D0F29716C10D42F7D47405EDC73AF4F5E9463FCCB5B6615BB50AC6F874F3 + 4C77DA7BE9629A733E7C969D380E9F967CF169CAD3FDEFEEC29C10E7785F1FE6 + 5A747DDD562E7E8D38174DC5E28F79F0AE0ECCCFA01DCD25A82FC65CB158B6B7 + 16E05C15612CCC853B0B646B7336E6D5F9E2D7823C416F659A6CA84A13EF861C + EDEB8D79E5C63A0260C2E13CD5BA6A5ADDD21574896C692C35615C30E767BAA9 + A658B6324C4B458E6C28CD94F565466B8BD3646D61AAAC2BCD507995A4CB86F2 + 0C0DDFB2B92607DB67C9DAF234595D81F510F6BFB212EB9772AC471802A63257 + D656E5CBA65AAC2B6AF215CEAEC6D8AB4AD0AF3815F974F12AC39874135D82B5 + 5A412294242BB0B6A255B05A0697E50843E3308C0DC12F3D23A98AF9C26D2AD6 + 61E97A9C5BAAF1192B73B0BE4A96157949585F25622DC9355892AEC9B846E3CB + BB8BF8B2ACB661DD06693F882FF5DE058BB1AEA3072505CD584B2DC076D4428C + C1745E3AD6B86971EA3E9A9A9B1E2BB353A2655652947C901CA37185E740B312 + B01E8E8B9459715867474539EBEA705D5B4FC7DA7B3ACAE6856BACDF43C3746D + CE94EBF30FE2B0D68E4B909951D1CEBA9D634463BB485D93CF8A8B5729EC0D8D + 92D70F44617D8CDF49730FBE9F66793F364DDE088E401B74205C348E305D4B23 + 7D37245C3D7FD1DB17C518C20C0BA57242425957D2B424A655F12BBB82E4B55D + 7B641AF2B6EF5B7B8CB5B05A06EF449F1D7BD455F49B812126FC94139E8ADECA + E8C1CCC25FC6047E696B80BCB27587BCB4C5575F785710ECED8FBA00D411FCFA + C9F31B7D3CF9291BB6CBE4F5DB90272CDEA96098409870D858121B97D2CFADDB + 2E93BCB622DD82FEDE9A4E5ABB499E59BD5E9E5EBD4E26AEDE2093BD36CBF368 + 5348BC7AB33C4B2F6DABB742DBE499155BE4C9251BE419C2DFE59BE4D9651BE5 + E9C5EBE4C9050CF3E5251317D38D34C642FEC9F96BE48979AB35A51E9FB74A9E + 98BB4A9E9C8B3AE88939ABE4D159CBE5B10FE83D6EA58AF94767A2EEFD65F2C4 + AC15F2E4072B3D62DDA3D39778F418F4F87B48DF5DACB2F947DF99EF8925FCF0 + DBB3E5A1B73E50FD6EDA2C79E84DBA8CA6FB68E32E9A16C30FBF89FA37669A67 + 72CEF3B9875E9DE17966474D78D948A1309FF7D14A18A2710735FEF9B765DC94 + B7541390B71A8FF2B0A6416FC8FDCFBF29BF7B7E9A3CC8679193E8169A3098CF + 2E5F937113A7CAD8675ED15485FCF867A6CA84897CF6F9AA8C9F8832A130FB3C + FDAA8C7972AADC078D611EFDEE7B9A79477C96CAE7A84C9FE0F3D417A0E79137 + BAEFB1E7A049C83F27E31F9F24131E9D24E31F794E263C3249EE674AC0EBF431 + FD26ABC63CFE9C11FA8F25B07D68A282DB09D8E67E6C3B01F9F134407AC83C7F + 1EFB0852E739B2DB65F4B015B179FEEC1ECBCA425FD523CF9A78C02E38AC3182 + 7FF7B4A7DEBDDDE8CFC77F5D77C161A7ACA2A5B0DB5D34F316263BCFE5F5D9BC + C321EC337DD6912D3CF2D44BF2F21BB3E465BE6C40888E7A15FB4D7852C6DFFF + B48C433F0B711598BAAC7DFF237273925FD3680CC6CADDCFB016739CA36E37CE + B02595D33E9EE781E70C79E54EAE7E6AAC68999503600962C7DDFFB8276F59D5 + 68F0D7F02C9337E5C7E4BE3118D3615C1EB9F7E392A92713347C70CCF88755EE + 762BF776FF218D04A623EB476B1BA1BF7B3CEE7146E6DDFDAC6C1FB746EBF76B + B2DB8D0663FF2B346C057C0F8DA3FE1D1FEC7F47F18FA01AFE13D8B2FBCFE1F9 + E3FE27C95E2846D368FDAD46EB4FDDBB2F2F4AF796E7A27B0FD93E4CDDB0F85E + 1AB9FDAF69E436A301E0BB6E244EDED613027BDA5C37A85F6A1800F3E63BFEA1 + E765FCC3CFCB0BAFCF960F166F94D731699AF0C84BB881E106FCE84B1E00CC9B + 2BE57E7BEBAE9472A0AF5BF60D31ABB1B4067E9C56C02F226F00B0070ABBC02F + E57973CD259DB0381AFBD42B1EEB5F6B016CAD8029E6C752CFA07E22EA9E7DDD + 0373097DDD50588509172761F76362A64017A91B025BD87BAFFA075FE09B80E6 + EDBF07A08730F17B189341A61600AB1CF86BE59E4C5A00CC0927F53BD52C31C0 + 771ED2B99A3EF6D67C85BE9C9CB2CD5AFA32BEAF85C08C8B425908CCF4097D0B + 127DA6CF5700FCC4AC45F2F49CA52A025EEAC90F168F2AB611F85284C09316AE + 92C904BE8BD728F8B5F077CAB2753279F97A0FE0A5DC2098F56CB77553566C50 + 004C3D4F4BDF951B35FFF2DAAD3275BDAFBC8A05CA542E5CB8C841FE95CD010A + 7EA76EDB85050F1744FE2A026042DF693BF7C95B815840F9D3C5F36E7DFB96E0 + 978BAE69589CBDB5E7A04CDBC385DB41D51B4107B090C3C2F260B4BC1D12A5A9 + BA92C24273566C9CCC8A895508CCC5A405C0CCCFC102725E42A2CC461F5D6846 + 709B705D64BAF51E21720CDF688E970FE28DDBE7993149323D0AE3C61A006C21 + F0ECE4242C900D009EE7405F82E07910DD39D31278514E962CCBCF96C539C867 + A74129B22427459666A5C802EB7A2B0D0BF2742CE2D3B178CFA02B2FC2DF64E3 + E69896B604C6B9A9EA967939CA4BF353CDDBDD58CC2FC0027E0117F418777116 + 16F1D9E9CEBED265496EBAAC2ECE950D95C5B2BA24D7EC97D6B705990A625716 + E5A896E5130C736C6E972C0B32E231568C2CC98A86088213508E97F96958F063 + F1BF201D757CC8908E7DA7A22F8E9B407B49662AEA68AD6B4030DF245F8A3A82 + DEA5BABF5C595392A76EA097E6A669FD1AEC9F3185BD8A736415AD900978D502 + 18C783635989635E95675C4FAFC7F61B4BF3647D05E3EAE6C8BAD24C595D942E + 6B4B3215006F6F2C15BF964AD95257A60F7B365433A66FBE0260C66F0D39D223 + 3E1D75B2BDBD56B6B6D2EAA151F61DE950EB84ED6D25E2DD8CFED599B2866FFE + 3BF1BFBCEAF2644353915AFD6E42BAB9B958B63415CAA68602D94C284C6B8486 + 3CD9DA982B3E2D8518B754A1AF7F2FE3CBD132A54D2D2D0859BDF5815491F8B6 + 57A8B569605FAD5A66D042C39716166A595C20BE2D25E287716825B10DC7B5BD + 9371DEAAD4DD1EAD5EE81A7A271F66226599793E90A3358CC6B23BDA2A074E74 + 226D96807E5AAEF0E1648B0443FB071BA07A09A23BC3AE72B548DE50932D1B6B + 73F58159606F9DBA2E0C394A8BDE66B5980D3D6DAC8D0F22A52B48BA02F6EFA9 + C7672E906DF539E283F340ED682910FFD622D9D16AC069506FB9ECEA2E95AD0D + 99F84ED2C5A739079F0DFB69C890ADF5E9B2BD290BE56CF4C7F7D35EAC807747 + 6389F83795CA4E7CFEC0B632D949CB109C871D38E7BE0DB9E2D798873CF68573 + 158073B30BE7860F1677E07BF1C3F7B3ABB542823A704EDB8BC4BFBD4002BBD0 + 07C740C0BBA30DDB29D02DF2D4532CB33DB00BC7D05128019D45B2BBB74CF6F4 + 5548A002EA12D93750267B078A65574F0154A8DADD5724FB0F95C9C1A3551271 + B24E62CE354BDC8556893DD72251A71B25EC782DDAAA1504879FA893C8530DFA + 403DF172A724D3EACB810119B74DCCCACC3B8C2D4B00D02719C85BB7A02345F8 + 91F3D909C9F9F2942AEB8B93924BD7A5849D5F9F333016CAF9EA8C647D7E5232 + 3E457FF4CBFF067548D33F392A691F635F50C6A78725FB3302E26392C7D8ADD8 + 86009760D7025E0B8359661BC7A7B83F029E92EF0DE851C8839422D465DFDC2F + 30168E999678B456A335AE82A6EF2E7AA0B18E85B24784423F5E91D23F5C9392 + 9FAE7A54FCFB6B52F4C35529FC11E94FD74D4A188BBE6CB7FD0992793E72152E + E37C7C41E84BEBE1AB52F6FD350FCCE571D0CA97E09796BEB4FA25E02534A315 + A4B184C471A06F29FB42BA0D53C2B53FDC8008C66EA2EE0AC63D2F15DF9D53EB + 4563C568E2E5BA550D11B6D222565DEAFE301CF3D5425AC2DC42027B88F174E9 + 525963E9222DF8EAA4147D73CA58D67E4FE06B01F069A1C53165A02FC73CA316 + C3566CE3FE087DAD7B601BDF95B096A0D7BA00A62CF0755BF8120457FC01E7F1 + 27A47F7440F0CF38174E5AF3E79B488D25B0DD9EF097D6BB04AF04B5EA36F847 + 13639796B616F836FDE9864706041BAB572B0B8215FC629B863F194B5A0B8179 + 6C6A29FB27025782546E73479AFFE52369F957EA8E425FAAE55F0CF025FC6DFA + EB2DCD37FF0D7DFFF2A16ED3F857A37AE46BFE7447AA7FBE25D57FC4987FBA8D + 7DDF96DA9FF0397FC47E7FA295EF6D69FAF98EAAE5CF1F49F39F3006B621F06D + 44BEF68F06CC725B825842649609661BFFCAE3FA549AFEF6B1D613F212165B68 + 5C83CFC1BE16FE12EAD21538AD7E2B7F40FE471E87037F1D691F47FC2DDB7D73 + 7F2C1B190B5E63316E606C197ED70A77F1B9CAB0AF0A1C43258EC1AA0A9F8969 + 398E85ED562C739B22FC7F688D5F8C6363BEE05B5C139C173BCC0B18FC1F5EC0 + 6F97FBA5D530AE1DDF9E93225E3770DDA21531AD9B6BF8FBFAEEBC0260BECC50 + C2DFB8037F2D2CA66C9C5EE6E9429DE59C4FE9669DB17B8F28002608F65C5759 + 467DD68774154DB0DB2B69D77A24ED4AB7A45FE951655CED95F4AB3D9272A54B + D2AFF54AF6ED430A59D3AE12E83AC2366C4BF74057A457D1EFFA90645C1B9494 + 4BBD927CB14752AF20BDDC238917BB248196B8E7DA25F602D28B06FAC6394AB8 + DAA53038E5669FA87532A12E5D563BAEE9E9A69ED6D3B4A2CEC7B538E3FA808E + 9974A94312A1141C5316B6CBBAD5AFC7178F7B109584B193F139922EB44B2214 + 7F0EF550C2F9360FD0653EF912EE47B8272538E3255C44DF0B84C0AD2AE64DB9 + 45A24F3549D42942E436893D8BFEF89C29EAB2FF88245F1DC03DB043224EB74A + D4D9767CCE1E49BA82F3716D483D54C4328EAFE3E922EED28042E1842B87B1CF + 23E877147D8E49C4D93E09C1DC25EAC2A002CFB8CB43383F0C2D7058622EF461 + DC1EECA357E2CFF7E3D8FB25EE4C8F4470BE73A85181E881A12609C61C27F458 + ABC69D8FBF382019B74EE0188E48E4E92ED93FD42041B498E57C69B0CE580B63 + 9EB4EF48A35AF0D2A235E464BB1C3849AF280CD5605EFADA335022C143951282 + 3A02E03D3DA50A81234E344BCC99761561F0812398D7F5620ED886B90A5F1AEB + E18B657CA9AC50FC9A31F7682B16FF36CC5D3A387729D397E5827AAA65172D74 + 7B098F69354C77CAE53A4FDC3BD42291E7E8D67B10C7D5A12F03EE403F5AD9EE + EAC271382FDC71DEE58B79D14E8EDD56887281EC46FD6EF4D9A900B80CF3B332 + CC2BCB656717E68FDDF5EAA5C5BBB942B663BE44B7D41C9B56BF3656B13FE6A8 + B4102604DEDE5D2BDB3A6B30B765AC61CC6FFBEA64EF1186AC685258CE392C3D + D86CC57C783B5FF66B379F6F27BDA5F0E53DCCD17C9B30AF6DC6B1B4554A20C6 + 09C2B8BB09A3DB2BC5A7A918F3C142CC8FF33117C7FCACAF12639661AE5E8073 + 897387F91C5F22DC589D8E797DBA6C6DC9536F397E388FDED86623E7B07C6113 + F343CECB150633244B3D2D800B645D558142E08DB545987F623FD53906FE9653 + 591EF0BB0179D62B00AEC8904DD5D90A803947A65530AD85D7F24551BA84AE33 + 5E82E85568437D89EE8B7186097CD7725CAC53F445D5C264CDAF43DDAAA22459 + 539AAAEDABB13FAE5DD69565E318305679B6BA88E6B1AEABC4B828AFABC8C131 + 600D87E35A5344109C2CAB0B1365455E3CD64FF1581B324D54C8BB3893EB3203 + 7717610DB714EB43BE20BB22CFBC284B004C57D1B60F41305F2C5E4197D310D7 + 95F4C2C43520D78A1441B085C173D2E2645E5AACCCC7FA8F00787A5C98CC4C8C + 92B95803721D3B2729566627608D1C17A3D6C03363A818991D8F3533D6DD7CF9 + FA6DC6050E0DD317B2599E8E7538FBCC8C86B02E9F8574766C2CFAC76AFDBB11 + E1F20EFA7C9090803579A2BC79205C5E0B8EC05A18F3F1DE2398F3F7C8ECA42C + 795BAD83A3F439C0DB1A7F98C0F780BC1B7C50A607D363189F1B18CF5FD66BD8 + B4BDC1AAD7F7EE9737F605CB1BC121F2FAFE6099BA7B8F02E037828C35F02B3B + 76CB6B3BF72AFC25F82500A658B65ECA6CA82A8AE1AB5E71DC401300BFB06507 + 44C0EBAB90F75E221466FADC7A42DD6DD8364061329F974CDEE02B53087E1DF8 + 3B793D01F03668AB02E31736F940DBE57968CA06070A432FA2ED256CC374F25A + D4AFA1B67900F0D3CB36C9B38C19EC00606A22B584D6C14616025B3DB560EDB0 + E643F368356C5282600B802D047E1CB200F8296AF62A2D3F3173B9EAF1F7195B + 98AEA4972AF8BD5BC6139E0D7D464B61D5DBC64DB48AC6157CAE46318EF09BB3 + D418C33E9B634AC30D1A69DCFFCADD2208B600D87AFDA3A7BF092FBCE301C226 + 86308D45DE56384C594BE1075E98E600E0699E679304C016028F7FF6554F3A96 + 10988633CF981078E39E666AEB0884DF9071CF4013098F5FD367A156ECCB7EDA + 977A92CF5C5FF068EC13D41468B28C87C6F119F04313E5FEC726CB038F4F9109 + 4F4E41BF29C3C0175218FCC844B9EFD167B53C161A078D7F147A98F017C23813 + 1EC1980F3F6F00F0C3C3CF9147026093A7C19179FE6C61EF68109880977D6879 + 6CC5B207041302FFEE99E1FEBF78366EE481BAFF88EE05809D3A3EA3A791D6B8 + 87703C0FE21858C767FEA87F009F73F2D4F7640DFE9B6B3705C8247CF7E42F7C + F67F3F9FB763BBFBC63D21E3C63F29E3B09DC2556BD96B41EEC8F23FA091FCE4 + 5E1A8DAB588DD69F1AADAF8A7C09A97E1696C73C2A63C70F5B1D336FB67F4C59 + 1561E6D8FBD14FB91501ECA332FE019C134F9DE555D8CEE156B63C2A00F6C8D5 + 7724C4FD05E0B406A2771B8ADAEDEEDA76A42CF81CAD6D34DDABBFADFF0D63FD + EAF150EE7146E6DDFDDCED2335B2DFDF93DD6E2498FDAF90BA7EE617A7A0D7FD + 458ED4BFE383FDEFA851FE04B66CDBB4CEFDC7FD27C9B3BF51DAA8BF7B0181FE + 5EFBDD62DFBB65A1EB48F8EAB938FF8AEE35C63FA2D1F667CB2301B0E7C6823E + 36DEC0687107B43CCA4DED6EB920F0C3B8C11004E3667CFFA32FE2E66E80F0F8 + 875F54D7CE1AE717376D73739D04F1A6FA4BB9DD818C266B057C1F6EFC84BE23 + AD80998E1B017FDD00D896C7A0ED3E4716005BF0EB86C11E719283898F9D4859 + E0CBD44E8A26D8BAE73051520B60C7ADF30808EC01BD8ED40DB40394B50D7D09 + 80DD2E6098BF0BFE3A52D7310EFCB52E66D4E297D6BE986072C249E94453DDD2 + 18B7CF1E392E9ED9F7416C4FF1ED453B81B500F8614C5E09852DFC7DFAFD45F2 + 245D404323E3FAD2ADF33373961BCDA395EF724F1DF34FCD59A6629EEDCF2E58 + 251369CDBB788D825C825D8A40971077B2C68D41DDB2B53279C53A790E796ACA + CAF5F2FC2AF481A6ACD924CF7B6D514D469E9AB276B3BC804588C6F1A52B6782 + DD6D815808ED9457B633C582C967AFBCC6D83A74EFEC2C925EF30D92695854BD + B39B717B8DDE86E8CA99D2B773F704CB5BEAE6996E9BB110E45BBE2334233C4E + 6644C542D1C64D737C828AF9195151AAD97171323F110BD9E4145990942C73B0 + 98E45BC7BAF074897D59BF20254D16A767C882D4548DF9CB38BF74FF3C2F2559 + E62727A29D6EB2D2B0C866ECDF0C750B3D27CDB87EA6ACE5EFC26C5ABB66A816 + 6671314D17C7460B3293F4ED696A694E1A16E75884A3DFD29C5405AC5C9C9BB8 + BBE9EA1299509410983098EE90976241BE24030B7D2CC817D28D5746922C4E4B + 4619C27E08601767A4A07FAAAC2ACC54713C1DC31181F2CA820C599E375CCFB7 + C7B9EFE5797C7B3C56E62686E27347E8C383C57C631CFBE2FE794CCB32936549 + 3AF3E9B2B63047D6D0B5746E06C4CF8231B35364753EF68D7D2CC13E561567CB + DA325A0267CB52EE13FB5FC378C2A5B99A1276F318D7E058D7A36E6379BE6CAE + 2CD4743DB6DB5259245BAB8B64731563ECA2ADBA403656E6C9A6AA7CD9807473 + 5D916CAC299435D8966FD1F3618A576501526CDB50AE10D840DF2ED933D48994 + 314DFBE5E0A96E75BDB7ADBD42363414CB7A685D7D91ACA929905518DB0BE3AE + 6F28D1940F7BD6D515CBC63ABAAE2B5760BAABB7567C5BCBA072F1EBAA96DD83 + 0D1272A24DC24E63FC539DB2FF688B5A9D6C73DCDFD1ED1D5DE56DA7FB3BD419 + B77805E25D8FE32488ADCD93ED75B4A428320FEEDAF860AD40B5BDA508DBD0C5 + 1DCE01C4FCB6967CF16D2F929D3D15B2BBBF4A7633BE5B67A93E34F3EB3056C5 + 84A5FEAD85E2DB982D3E8D590A643756A7C8862AA38DD569A8CB12BFB662D977 + A84E0E1E6E51CB9AFDB44839D4A09632BB074CBC37BAF6F3413F9FE63CF17300 + 2EE5DF96273B3B0A54BBBA8A64DF4085041FAA923DBD7CC859A2754CF7F496CA + DEBE324D0351B7A33517E3E448402BB66FE3434C8EC5BA6CF16DCE427B36C6CC + 43DF5CF16F27344EC3E7C99640D4EDED2D96E0FE32A85C153250A90AEAAF504B + 1C6A8FA3BD87AA25F808AD82686D83E3820E1C6B9083C71B25E448BD041FAE85 + 6A90AF95B0138D12798631749BD1DE807CB3449E6D92B0530D1271BA59A2CEB6 + 48CCB93689BFD82971173B24FE52A7245DA54BE63E49BCD2237174B38976AB98 + F3ED9E947D087DD36FD1852944C8F0E161B5F2627D2A2DD5EED0C5E751C9FEF4 + B8BA45A6B56FFAC7473C52EB5FD45304BC999F9D94EC2F4E6B4A657D7ECA534E + FF84FD6CFB0949C57ED23F3986F229292080F98116B1E7A4F05B5A071BB86B2D + 8A0971299B27C029FEEE8A147C4D807B1EFD2F6A99809575CCD3FA8F79029FE2 + EF2E092DFE68F59B87ED9927F0AAFBD32DB508641F02E4E21F8C4A7E7FC59316 + 12227D7F51AD8109768B7EB82279D85F3E5D3D1332392218B6D0CA8228BA8BE6 + 3151B444E43155D05AF287EBC67252E1D90D0F34ABA425E377973D60AE9200F8 + F757150897FF68802FA53098F018F9EA9F6919897DAAC5E325A417A51CAAF881 + 56B49731C63567BB6B52FED36529435DC90F17A0F352FAFB0B1E15E3DC53B69E + 69D1776721D6E31C7ECF3AF6A370BE7EB8887E9730DE25DDAE9056BFDF715B1C + 0364FA711BA71F645DF912CCBA5D0213725BAB50E6E90698A2B568EDCF37A4F6 + 2FB7F05DDDD4BC825F7C060585FC6C7F3475E50E1066EA81C10E00AEC4BE2D78 + 56ABE79F686DCAD8B217A4FAF726EE6DCD1FAF483DC627B4A52CC0656AD5F8E7 + DBC69AF7AFB4A2453BD4406B5DA8FECF04B3065033DFFC2F9F20FD503F03DB09 + 809BFFE58E34FDEDB6E9C758C27F312EA21BFFCAB16821CB3CF5A14259AB06A8 + EE2F18EB4FB7D54A9700B701650255825DB5ECFDD3870A7D09809BFF8CED087E + A1E6BF7E8CE3FD48B7B1F0B7DE194B5F1EC06F8380D7B673DCB6FFE373E9FC3F + BFD2FF88673F04C33FDD546B60425D4DF19BAFFE71D82AD72DB7D56E397EE34C + 69C1CCDF222DDBED6F99797E97E6378214BFF39160772404B6623DDB29969996 + F0050DFCF7AC0AF1DFA715BEBB8ED787624262B6E15A409522CF1732CAF0BF61 + 4C6DBA90A6ABE8A2AF681D0FE19A44C84B004C8B7CF6A78AD18F40D88064BA78 + 37EEDEE96580EEE5F9C249EE67A7215C1371CD53F7F3B816E67C7C4C723F3C22 + F91F591D9542F4255CA5856DF6EDC392CE98C2E853846B26E301E7A04FD69DC3 + 9281EBB515CBECC3F10A3F3D2D459FE1FAF909FADEC17E6E1D914CC631BF3E20 + 29B402C6BD81F705EBE18169E2D52E49BADE23093728F4B9D92B69B709AD713D + FF90101BC7C2E3C2B593EEB5B3700C1C27E55ABFE4E15873711F48E6F65770EF + B9D62DA910A1AF5AFDF225235A382BB4EE31C771B95792B0DFA44BDD12837B5B + 04EE7184D22C275CE894D8B3F45ED1A1D09A623DCBAC37FD70EC48B92D1587FA + C48BDD181BE7029F97FB20988EC33D2E8EF7BA3314C177A782E2A44B84E1DD9A + 4FBD3A201937709EAF1FC6F119255F666C7AC6A4C73DF5028EF5EAA0245CEE17 + BAD5A6622FF6E2B3E13C5DEC976428F5F2A0A671A7BB24FE0C3EC3B91E893C8E + CF75B45952AFE0FBBB8DFBDECDA3F83E4EE27C9DD2FDC59FEF52501B79B24522 + A0D01374E5DC2491A7DBF139BB9076E09EDF22078F3660FE522EBBBB0B305729 + 9190A14ACC872A64EF409942E00343D5123258230787EA240A73BDB833387F67 + DA25F2142D92AB855E44767615CAEE1ECC79FA08810942097EF9625A21E63225 + 2A7A2621C00DECE54B81953A7F23FCDCD185395B0FE62C4338BE535D72E064BB + C622F6EBAD14FF3E7AB72995ED4D7918AF580E1E6BD49010B42EDED15288B911 + 2130C655485CA9F26FC37CB3A944E7A83B7B38676DC6F84D98AF3648402FDD69 + B74AE0A12675F9EC03EDE86F50F9F4D48B6F6F83F8F5356ADE0FF91D10C1F0EE + A1668D631C8879E1CE7EC2E172F592B305F3D46DADC5E2DB41805B86FA4AF1C5 + BCD8AFB316FBA4C530B6E96BD5F9F75ECCC5FD30AE776B15B62D978D8DC5B2A1 + 91F17EE902BA5CB662FEEACD9733F1793663AECB38BC1B1A73353E30DD74D38D + 37EB37F0454C7C767AE8D94118DD5DA92F6B6E8236623E4F40CBB0305B9A314F + C7985B30D7F62A4B536DA8E40BA5D9585764AB85ADB1084E9175DA96A956C01E + B7D0E5E99A678897CD1863631DD62738E62D2D65E2DD58AA96C76AF54B77D2E8 + BBBA3859D694602CE43926CBDCE7DA52ACBD0A99CF10EFBA02CCFBB1CEA0CBE8 + 8A2C595399235E58E36C44DD66B4ADC771D9EDBD4A302EC65B5D48B04BE81B85 + B56814D668E13237395416A4453920D858FD5A30CC942098627E5106D68D90F1 + ECC4178C53B0E6C33A13F2E4B11EA4D7297A995A988D756F06D6990E3866ACE1 + 0F1223654E7234EA68559C24F3D3E2644E12D6E1F151322B2E5266C6462A089E + 9B182FF3183718F91991E132333A12F9189915CD97B5CD1A7D566CACAED3097F + DF8FA267AE30D4B11EEB72C61526148E4F5408FC6E04D6EEB1C95887E7605D9E + 81BEF1F26EB8B13CE6CBDFF4F0F55630E30187C8DBFB0EC87BC1A10600337F20 + 4CA68784C9BBFB0FCABB21B4100E95B7428C073182E369070ECA6B7BF6ABDEDC + 13AC2FA21302BF1AB8D781C07BB4EECDDDB42436F2C40EF60992D7FDD0CF778F + 4A21F0969DF2FC665F79618B9FBCBC2D40E8F18C9ECF58A698673DD317BD7768 + FA8A3E3F31CF49E82D8DF09722087E714B80A62F6C3256C3D40B9BD006316FAD + 870986191F780A3DB1AD37EEA327ADF6966757515B5513A9155B64D20ABA8736 + 9A82768A790B843D6078E906A185F0D30C13B608A96319FC382D8191DAF2632E + 003C12023F4EE04BF0EB00613708566BE0F716CB63EF2D91479052741D6DB4C0 + A3470882DF41FAF6B075F0F0B3345A047FA0CFD73C56C1109FCBD16D34F5C0D4 + 1972FFAB334CCA78C1F4FCC7D06D8E08812D14B656C204C184C256060ABFADCF + 11F97CD11AAA50F61923659F39DA76FB1C7358347821FC7DDD84B87BF6EEE79D + 16008FA19E324635F4AA38EE1700D85802D3B5335D3F8F27F47DDC58E38E79E4 + 590FE41DF734FA41F73D86BAC726A26E92DCF784D198A726CB84A7098B1D80FC + F04499F0D824B99FB1801FC13E1CF84B59F06B6501309F3B5BB86BE12D35D29A + D763E54B4B650700ABE530B77340B079FEFDEBBA0BEADAFC6865B7D8E6C0DE91 + FDF86CDEEDA5932120F9ACFF3EF20F88F58F3DF38ABC3A6DB6BCF8FA4C79009F + 996C41BD7B725BF4574680BCD9C713C3B2E0F77F21003C5ABB5B74716DF992B5 + 16D6BCF20FE4C73DA616C10FE1FB7F98F1A2EFC739821E78F81999F020DB87E1 + 262D80C761BB617E65D261715FB61EC7A77D2CE3B272E0B105A6BF0A381F42BD + E58777C35FBBFD9831DCD6B58D7BBCD164FBB975AFB6BFB79D4B9ECF732FB9C7 + 199977F773B78FD4C87EBF26671B3D3FA301DAFF4C694CE1BB20EFAFE91FFC60 + FFBBCAF9139A3F87D1F09FC3B469DD287FE2FFA886F7E56884AFF77FF4A23352 + BFECCFBABB65A1EBBD345A3F7B31FEAD63FCA3B2E3BB65C1AE47E8E74EEF02C0 + ACB3379E516E6E46B851BBE2058FFD1D6E380FE0863D1E373FA413706326001E + F3BBE7E5BEDF3DE701C04CDDD0D7ADFBD0A662FE57750F004CD12AD8057F7F0D + 008F66014C37D0D615B45B23DD405B7132C4D44EA6C6613235961098654EB62C + E875265E0A799D3AABBBEA3171B3D0D76309EC0060BA7EF6C05FB6BD843E8EFB + 670260CA5AFF5AF8EB79D3D013A7846E69A0372D00667E965A08532C5BF8CB3C + 27AE6C27FC7D1293DCA7662C9467662E96A7672E92C75D10D882604F8C5F07F0 + 5204BE4C27CE5FE981C2B64C3D4930BC60A53CBB74AD4C647C98655E0A7EA7AC + DAA87A6E0563C3ACBB2BB56DCFAFDE242F7979CBCB1BB66141B15521F00BEB50 + DEB85DA66EF653F0FBBA77A0EAD56D3BB118DA2DAF6131F4BADF3E796347B04C + 0BC0026CD701790B9A46B7498CB1B3739F8AE077C63E2CCC82B170DB8F05D9BE + 83EAEA997A1B0BB57750FFEE0163EDFB5E688CB1FA75CA33A313354ED0AC58BE + 211C8FC56692CC4B4A5600FC7E34169C5C1422D505667CBCC25F6A6E62A29659 + 6F01302D80595E9082856F46A62C4C4DD5F1088119EB7721216F7222EA69E54A + F7C6995824E7C8929C6C599495294BF3726459419E2C41CA58BF8CB9BB308B56 + BFA90A7F99D2C532DFB0E65BD574854CD13533B58CAE98F3D3B160C7623B3D4E + 5D742D455FC2D6A5595888A763E19E1E2F8CEF44004C2DC2A27B410A16F2CC67 + 26CBDCA4381C2B17F75CF81B5760B426E662DE2EEE2DFC1D06C3E9684BF3A42B + 0BF890215556E527E03347E05CC48857519AAC2BCE90557929B2A6201DE52C59 + 86CFB028355EDD337B1566CB5A1CFFEA5CF4C94D9795D8072D77D9CF8BF1804B + B3657D55816CAE2B51CB5C96579564C91AA45EA539B286AECF0A33344FA8EB5D + 532881ED35B213DAD15C293E0DA5B2956FEFA39E00980F4CB6305FCD87278518 + 3B0FE3E68B57458E8EABEED31C976A6B0980EBCB856EA07DDA6B65E7408BF8F3 + 21547FABEC1E6A977DC7BAD5A5F186A672D9847D79639F1B91AE6F2C934DAD55 + B2A9A552D661FFEB1A71EC8DA5B2BEAE1822C4AD9303C7BB24E274AFECEE6FD0 + 58B18C7946AB9290632D48E9E6B951F6413BBBAB344630E3021302FBB597886F + 9B892BEB0BF9A0CE1B6D9BABE9722E5B3621DD529B2DDB1AB2712E32704E5264 + 732DAD68F3D0BF40B635A16F5DA6BAC35B5F99AADA847E7C28E85D9F25EB2B08 + 75D3654B5D36C6C9914D55CE185549D847926CA84C948D5589B2B52E0DFBCE82 + 0886B3C55F2D772B640F21322D4B18931865C66F0BE8AA5098BCADB9006981EC + EA26D02D425AACE2834EBA48A4827A5086682D1376B456428FD4A2BE5C1FA686 + 1EA99648025628E27883841CC2FE7AB96D89040F9649F0A112F4C3985DF9FA00 + 76DF40911C3C522EE127AA24EC5839F2A55A3E70B84C420F57629C5A8939DD28 + D1A71A24FC58AD1C18AA9483C7903FDDAAD6481A0B91D65717F9D09D80B64F2D + B10863F9509E229C8D3ADBEA7940CF36A614FBB2DE02DE3485B6C30097D66514 + AD78E9A633E7E3E31E8B5EB70877A9EC8F8FAAE51A2DD568CDCB07FB4C5947A9 + A5EFA704B42725F7CB53AAECCF4FA8BB67825F8FEBE7CF8E2BC825E0CDC0584C + 53B05F2AFD138CF3F9690F08CEF9F28CA3B3289B3CAD64F3BE3EA3CAFD8A7DB0 + 2FD411CAE67C7E5645C84B206C2C7ACF78206FFE5797B43DFF2B5A01D302D888 + 1684044ED60D2CC1122D0E096C68E947D0D3FA6F9FA9C5A38244B6FF81E0D602 + 28C25C6C4B00FCC325B1EE9D69E55B886D097F0BBEC3BEDC00F8FBEBAA921F30 + 0EDDD16A7A5DCA1D004677B5E504BF50F9F7C87F877DD285ED77B484A425E8E7 + D0A74257BA65DFD33212C271590B4ABA7926E8250826B022042740E371177F8F + E3F916E700298FB914FD4BD09F2AFBD140368AD0986EA3096A0966AD8A28025B + 967F7F09DB137E9BB44CA1B181B8A349C7F8917902EECB7A7C65BFBFA6DF0FCF + 3DCF81823CE7184603BD6E2B514A3FAF4B2361A2B11025F4C539F92321213EA3 + 4706062B142610C6B1D32A9AF18019B7585D4AFFDEB8BFA615B2713FCDFC65D1 + 78B510AD76ADDC10B8E9CFC622D7005F0380295B6741305D2B37FF8BB1A82540 + B510952F1CD00A982098009C22FC25F425E0A68C6B66B38D673BC259E4096D29 + 96DDED0D2837FDE52369F819C7F7071CEB1F09866F8ABA5DFED9B47B002FF3DC + CE512DEB59C7B1D19FA097F0BAFD7F7CA129E173BDE342DAE3FE19DF91C73D34 + AD7DB13FABD160B05BF67B1C59C7EF99FF3B0FFCFDD1C05D5B26ECA5F4B7E400 + 5F0B7D2977BBB5FEF5005FE7050DC261BE9CA19018FF055E2BCCEF94AEA8CD35 + 82FFBB12FCBF4B0881BF23E8C5FFE29B734277F184BABC8618CB61FCE621425F + 8A75561604F35A4318CCBC82616C9F876B18AF6384C3791F9F50E85B847C31AE + 93A51C1F629E317F0975F39012FE528C63CCF8BFBCBEF3DA6EBD36284CC6388C + 215EF4E53975214F8B5DC638A7B221825CD59D2392767350BD3EA4DCE897B43B + 8724EDA3C3B85E0F49D2ED0149BEDDAF6EFBB33EC175F8535C6F3F65EC6EC25F + E3369F717F632F76EBFDC5BA36CFC3FD82209916C584C104BE543A5D5C5FEBF1 + 80E0D4ABFD927CB957012DA16EE449C6ACAF97E8D32D0A7F6378AF3CDEA86097 + E03715F7C9942BBD0A7CD997FDB85DD4A966BDC7461CAF53459E689058C2E033 + 2D127D92F7748C790AE39C6ED2F6F063ECD3A81059C7C598748B9D8E73907E7D + 50D2AE0D22C567BE85FBD8F5C30A78E93E3AF64237EEB904E6F84CE867ADA1B3 + 6E1FC51803127FA64312D0CFB8DB469F4BBD9270B65DC20ED74BF8910649BDDC + 2759370EAB2BEEC4F374EB7D4832AEE19E7DD5B8EA4EC318A957FA154E479C68 + 91A893B46E6E91C8E3CD1276A45E4231673970A40A7311CE37AAD49307433D04 + 61FE42F81B83BE11C730CF1BACC11CA45EF77B60A8069F17FBC7F9E17C665717 + E6B09DF9981B15A0CC394D25B6AF40B954BDA31000FBD292D501C29C6711FE52 + 3AE7A22BEA1E96AB558CEF1BD05FA5317C69C14C37CB11C75BA4FE0FB7A5EFFF + FC4E522E0FA847165A1A871E6E9270860DE9AFC31CAA0AC781B95B6BA94259BA + 80DE3DD82A4187DA65D740AB040EB648D091560D21C2F8BF0C271284FCCE43CD + E2D35327FE034DB2FB4887CE9135F4C7509BF8F7354A405F03B66DD2FE8183F4 + 7253295B31A72504F666B81442EA0EEC177362FFFE7A1C7BB304607F3BFA9AC5 + 0FF3EF9D83ED2A1FBAA32668C6785B3BAB65731BE6E1984B6F69A34A64732BE6 + F92DF99887D31B4F1E52CC7F696D8CCFBFA9A550CB5BDB3187EE2AC7D8D5B2FB + 70233E4F93F8315E31C6A38BEBCD98C353DB3A08872B34848BC6F7B530B72607 + F3E45CCCC1F30CF02DA7CBE56129F885DC6EA13997679CE1AD6D152A1F1C37E1 + 37C75C534EC89BAA00796B7DBE6EB3BA284956142438503855561626694A0B5F + CAAB027D2AB3650DAD8E1B8BB06E68913DF83E6875BC9A10B90CF37C1C0BD766 + 2BB1365B9A1D83B560A42C480DC39AEFA0CC4F21000E57110A13FC2E4C8F91F9 + A984C43158032668D81C82619617A4452B04B620D80D81AD58C718C4F43645F8 + 4BEB61BE00BC08EB4BAE4F098359CFF5EA3C5A08A7C460BD4C57D011F27E4CB8 + CC880E53183C3B016B70BA888E8994B9F1710A8569293C338690972F6F0F5B0E + 13FEBE13765063096B1F6836D7EC58D7CF4D4C5157D0EF4644CB3BE1B1322D24 + 4ADE3A18256F1F0CC736C6B5F47B6111F2768801C09407F41208EF3FA0292D81 + 1506733B86830A0D97B74343E58D6063014CBDBE7BAFBCB66B9FC25F6AEA4E63 + 0D4C083CD53F4881B08D1B3CD537482130AD81D52218F957B6EF32C076932F52 + 3F9477A2DF2E7919A905C06E283C65938FE6DFDD8DE38508919FDFB443E12FC7 + A165312D82995A8F6AAF6CDDA9AEA4ADF530F745083CC9CB5B26AEDD22930980 + D7F9C814AFED6AF94BF04BEBDFC96B5987BE489F5FBDD5A329E8F33C6421302D + 83994E5EB94553631DBC5E2D830981E9169A0098D6C0AC231C7E9CAEA2E718B7 + D06C7BDCB10A7E8C0098D6BE33972BFCB57A1CF574094D0B60EB12FA1147A66C + 21B0D123EF2DF400E087DF32D6C00CA9F68BB06AEA89CFA484C0365E30F5008D + 38A61A084CB921F0434EDC606A2404568B60E41F64DD8B26949C3E4B9C3C4D26 + 3C47B99E47D2C804A9697B53C6D37065C4734D4A61EF3348A1B14F13FA9A679D + 6E08AC6EA19F72C2E9F119EB53C300D8C25F0380E9A1F10599F014D2A7E8D5F1 + 59512BDFC726C918BA7C7E7A8ADC078D79C680E031EC03DDF724DA908E85C6A1 + DF7D84C60F3D23E3B0BDC2DD11009872835FA69405C00A815D00D8CA3ED7F658 + F9A29F4D097E2D105678FD309F7DFFFAF371DBFE0BFD1A00B61AA58FFBF9BC42 + C8F14FA2FC948C7B10C7C767F6E369F9FA94DCFF10CE0B2131F2367EB085A54C + F5B3F2335AD0FBF7E0EFC8F6D1FA40A3F193D1F44BA672B77E4B3FDB47A12D3F + DB04BA71467E1C52F214F461F9A967A7CACC39CB65EEFCD532690A7EAF04BDF7 + 3F2EE31F242331405861E2F84765BC0390CD18662CABD140EFDD75A6DE3028CB + C50CA8BC8B95DDA5614668C1AF95D659C839DA38B66E64FD48DDABCF6FDD1EB2 + C7734FB9C719991FA51F3F93D55DFD7FA3EEDA7E3448FB9FA9FF17008F22FBC3 + C79FC0FC3186CB36AFF523FEC4FF0CDDBD3F47160243BC10FCD68BC96F9301AC + A3C982D691F523212FFB58AB5C777F77FE9F0186EDF15829D4A5981F217B73B1 + 3718D56837A1BB6E72C6FAD702602B82E0310FE286FB3BDCC41FC28DF961E4A1 + 310FF3E6CCB7A8CC4D7A348B600F001E0D02DF556700305D401300DFE50ADA01 + C016025BF86BC57A0260037F517E8AC0D765EDEBC8BA80A60B94FBE92EE5594E + 7A68098C3694DDA25B9507269BB7EB1803D800E0377472A5132F4716F25A8BE0 + 91D27A47D6FA57DDB97042F7F27B9800D2158C0B023B16C004C056B400B600D8 + C25FA636CEAF479888D2AAD7C8BCA568AC7CE76BEA06C0D60534AD7E9F9AB9C8 + A3C7DF5F208FBEBF1013E7610B60D5074B3D56BE8CEBFBA405C08CDDBB60A5EA + 19C7EA97623C5F02E06796AC9167967BC9B32BD6C91407004F5EBD519E733469 + D5067976E57A4DDD96BE84BDAF72D18285CA8B9BB6C9546F5F99CA3756B10899 + BA6DA7BCE683C5D1F62099CA541743FBA0FDF29A7FB04CDB8D05D6DE707933C8 + C4D279639779BBF6CDDDC1F256101669A87F876E9EF7A13D3854DE3E108E4519 + 1773519ABE1B6A80AF07FE86C6C8FB51F1323B2E4916A6A42BA0A5CB66A6D4CC + B838991113AD6E9B995AC83B8BAE9B13D12F29C963096CA130413145783C87EE + A2E369511CAF4079515A1A948245682CC6C7C237138BE55CC6BACDC5C23E1F0B + 7BBA53CE97552505B2BC305796A1BC249796BFD8D601BF142D5F1500E7301654 + BA2CC94F9305D9F80C28538C0F4C60EBB1C445995A9289453B16D80B53636569 + 7A82ACC84C9295189BE952C6CAC53EF48D6D6CC37E0B20C6D3A55BE5358559AA + D505995A5E5B942DEB4BF3644359BEAC2BC1F1A36D15416D69AEAC2BCF938D15 + B9B2A9220B7DD2646359A66CADCA97AD35F9E25D9127EB8B32655D71A66C423F + BA655E9C1EAFB0776D61A696097DA9350519E8C731B2D4C5D93A5AED3696CA8E + 8E5AD9D1496BD872D94C4BDA9A42D9585D205E7481867E9B6B0A646B7DA1C6E9 + DAA5AE7C2BD575DD8EE6321C43816CAEA44B6B02D25C75AB46176A6BB08F95C5 + D877498630F6D66A94099BE94A6D7911EA4A306E7D995A016FA82F952D84BA4D + E50A713722DDD6D5205B3AEB65654D8978D557C8C6965A59DB5029EB9AAB24E0 + 50876CEF6E920D8D8C81D6203BFA5A645B7BAD6C69AE94EDED75EA423AE2CC80 + 04230DE8AE5177D2C1879B24A8AF46FCDA69855B2ABBF9F0AE1DFBAFCDC167CD + 946D0DB4642D167F27366D00E3CEB615CB76D46FA9A1BBE42CF126B8AD4EC7E7 + 4D16EFAA24F1AD4B951D4D59E2DF92877391233E0D99B2B92A59BC4A62655D69 + 9CCAAB244EB654A560BB142DAF2F4B10EF1A8C51952A1B4A13A058D9521927DB + EA92A014F181FC9B3224A0255BFC9A325501AD39B2BB8B16BC05C6321722DCDD + 4B8BDA9E72D9D9411786051284BAE0C1723970A4520E1EA9D607A461C76A24EA + 64BD3E0426E425D8659F8387AB24EC688D82598A0F4835F62094E0B8A30C3F5E + ABAE92E92639E62C630F364A04C6A26BE5E8330D127BAE49E22E3449FC45A66C + 6FD0B6B0E3661BEBAA922E96D5DDF2E96689A5A5D5E56E49BCDA2B4974B94CCB + 5A5ADB12C6F2E1FB2D3E8847DBB51E89BBD4AE0FD0B3E916F9CEA089CB78A553 + 1FAE334E2FE12F21AFC6DFBD33A410980FF309802D0C201C202418290B110A14 + 64D022ED845AA2D1A532DD2B334E2E5D2C1B97C967D4EDB2C6D6FDDCB847CEFB + FA9C82596BD14BA89B8371D4FDB3BA783EA1F5160233655F6E6745F09B85F1A8 + EC2F4C99209863B92D864DFF0BC8D35DF439C9A7C5ED37E775FF2CE7D1B2F79B + 2BAABCAF2E49FED797A5E8BB6B0A609912C0122059204B6048E8AA00D8813E84 + 55353FDF92B21F8C352DE113E1A901A8060453C53F629BDFD302F8A20260C25E + 5A051762BCA21FB03F05C04C2D00BEA9FBB7C750F1D36D611C53C627A5CABEBB + 2EA5DF605FDF5CC531DD900AF4AFFE8900EE13A9FBF963D4611C1C23616FFD5F + 3F52B7BF45DF5EC2311352DFC258D8D737C6A5AD85A0C6C291F017C7FBC3353D + 36AB224270D4137A1184D13D2EA198A9339F97D6CE760C9BA71B6CB619184E20 + 4C99FE23B7E171987EC316D00ADD20E6CBF51C98EF81A0CDC236AA0232315F87 + 412FF3C64AD340BAC26FB0AF6F09F371EE9C73CB71AD0C38C4F6B4067680B05A + 033BA25BE932C705B58D0B5CF1E30585BF6A118C720DB677835FCA006182545A + 641B6B646B89EC2E13AE6A3CDDBFE237F567E84FB7A5F6CFB4DAFD18759F22FD + 48AD5F59DFF5FFF94ABAFEAF2F753BC26F0B8E755C5A11D3E533A1ABCBEA97F1 + 7F6B307E0DF63312009B7D7FA8B0B9E5AF9F481B440B60C60826A8E5F12BBC75 + E02E21B0A684BECE7E1AFEF609C4E3FC18BFB73BF89DF158B92FEE1F421DC7A9 + FD038EE32782608CA165F4FB035D537F847EDCF6236C7B079FF54315F35616D0 + 5AD1953355FD67F4651EFF476D635F6EC3ED21E6F9FBB1A9FD5FB1CD966D1DCB + 4CF99FB7D702A6F63A60FBDA76FEA7EC4B23FCAD9998C2147EB70A88F9A209AF + 19E6850BF6E1EFD8BED0617EA3DC9E56C4EC67C6B29E08F8620AC5F2F06F1979 + F6C5F52DFF335CCB3EA5752F7506D76603590987095E99B26CF384B016C4DA3E + B68D69F62727D5C3012D8E733E41FA29C6FCFC3CFAF3651AB30DAD793378AD67 + 0C75DC2FF8128FBED883FB01AFDBE97495FFD1715CFB09AA2F785EBEC9C276DC + 07B74DBE3188633FADD6E5B432A76B687DB1E8437A8C38ACF73482DFD49B7D7A + 6FE3FDCB00E03E49BB86365A235FEAF658F25A0B60B5DC459E629E7DD56AF84A + AF026282D2B8731D0A7419D33E1CF7FB08863660CA17BC86AA2478A05C4206CD + 4B59A1B8E71F18AC9083B8DF732E108AFB7F24B68D3D857BF89916493CD7A669 + D409DCBBCFB649DA953EC9B842CBDE2E3D2E826602E794AB3D92796B4072F0D9 + 72E811E3D610E60C3C0E82D67ACC219A24F17CA7245FE892E8138D1276B816FB + 68D1328170E4B1468939D92C0938F6F833ED928EFD64DFA40BF04392C398F638 + 1F04D8311827867392E3F5481B25DE7191CD394D083ECFC1639CE3600E73189F + 03FBE131249EC3B19E443FEC8FFBDDDF572A070F55E2BCD4EAB93988F314DC5F + 2AFBF8625C5F898694B0DA3740D7CD25B2BD8961310C04A668214C40CC3E2674 + 45A9CE1135ED2A16C625DE37548B39553DC6AEC2B9EB90065C17BAFEF699A45D + EA351E5E1AB225640073B163AD3886467C2F8CC15BAD96C63B3A4A25B0A75A82 + 061AA046CC596BC5BFA75676F345C5A32DB2871E5F90069F6C97A023CDEA0A7A + D7A166D97FBC4BF6427B8E7549D0D14ED979A855812EE1F1EEA156D939D02C6A + E5DB5D8DB44ABC5B696D5B8CB44C7CB08F1D83CDE23FD40E75CA0ECCA9771CEA + 9480C3DDE237D425DB51F63B8CFAC31DE2D3DF245B7B1B645B6FA36CEBAE934D + 98FBAF6FA2856FBE6C6C26042EC4FCBD48B6E09C6C6A2B920D2D5813202510DE + 8ECFB7BDAB5C5D59338E71407FBDC631F6C538DE6D84C026DEF156BA8DC67C5B + 417573816C69C45A486170866CAACD362F62EA4B93991E00BCB1026B9ECA2C75 + 1D4DA04BEB5F8A2F7D6EEFAC94CDAD18B3D58C4908CC36C60DDE88F1B6371561 + 3D938739791AD68A89B2B63445C13045A8ECC598C36519B28196BEE8E7D580F5 + 565BB9449CEB9383273B646519D6804509B2AE02C742B05C922C2B0BE265457E + 9C2CCB899165D951B2342B0A69B46A4966A42CCE8890A599582B66C51B6BDFF4 + 68599ACDFE7CA9380E750CEF138B3563A2B6537413BDAA301DEBC10CAC13D365 + 05D675CB731936886B52AC5133E2755DB9288D63D2CA9863F12564AC9193A365 + 7E4A8CBEA0BC10E5F92974074D2BE030285CE1EFCCD808ACAD23B19E8E953989 + 04BB11321BF9F92989A88F9619D1E1586F4743B132238AAEA1A31414535C9F33 + C4D3CC9838793F261663D1F3579C4C0B8984222063C13B23D258014F0FE73303 + 868A425D6884CC088B145AF9BEB92F44DEDCB37F180CA30F8131FB13201300BF + 1AB4574517D0AF2B04A605709051C06E85BF04C104C22A945FF60B7220F06E6D + 7BC937505EDEBE4B18FF9796BD04BDAFFAEEF200606BF93BD52750EB59A6A530 + C362BD1B142A6FED0C51D84B97CFB4F8250CA6088269156CDC4C1B103C9516C3 + DB762A04B6F18409818D6B685F7991718737A07E9DAF825FEAF9751877FD0E08 + 6DEB7DE525A6687FDE6BBB3CBF06DBADF6514D59B94DF5FC2A8C87F4B9E5DEF2 + ECD2CD3269D9164D9F59B8419E5EB04126A3FEC5B5DB15183FE5B210B62EA369 + 19AC2EA2193378CE2A797ACE6A7505CD9456C0D632584518EC9275116D44484C + 08BC581E7977A14260BA867E9286113492709EAB3DF6CE02159FB7B90D2DD432 + 98B1831D20ECB60AB62078A41E7C99CF03A72BF4654A404CB16C8D442CECB5CF + 141F703D63B410F82EF199A5038415F24E44FAEC1B9E74C224D36EFBA825303D + 243EE5B881763D6B1D690D6C44203C59AD7E15FC12F23EF19CDCF7E47338B657 + E58559F3E5D5793897AF4C53F8AB009896BF4F4ED1ED68317CDFC3CFA8A5F098 + C730C62318EF1E00981A86C1E6F9F34808EC06B604A3EE3E1606B38EF0F7C1A7 + 5E90DF3DFDA28C7F64D25DCFC4DD63D9FA7BEAB70060B7D81FF23C9BE7337C72 + 0F3EE7A7752F741F9FDDA3AC4C8165F6511EF3843036F004BA8D76FA9867FB48 + EFA5917077B43ED4887EBF6425F7D6681CC6EAEFF5B1ED04BC13F09908810DEF + 41FB98473DAC89F5CFBFFC8E786DDC21DEDB82E4B5693315002B8424087EE009 + B50456E88A6D741C8721B16C65EA6CEA16EBDC72EAC7DD1B9A5AC0EBD668F59E + 6D2C20FD0721A947F7DAF61F18F7AEE3194DEE7146E6DD75901BDE7A20AEEDF7 + 1B3476EC7F3300A63C2EA0FFAE7EFB07FBDF5ACE0FFF6ED378FB07B16DC88FF8 + 23FFB374F73EEFD63F1B001B5703C3A0D79DBF9746F661DE02E07BF5F94F01C0 + 14EA3D80D725DE5CD45504E4B9D18CBC0921B537CBBBF4206EAC0F4D42DE00E0 + F10F4F418A9BFA830CB88F1B376FD004B60EF0755B008FA1EB0DDC4C7993A6C6 + A0CE425EB7DB67776AF27C83EC9700789CBA807EC90380DD931133213116C006 + FCA2FC34F54BF83B12004F9848004C9728F706C00F4E7903C2E4089328C6001E + 8FD40D7F290B79DD2EA1DD7516007392666379683C614CDC38A12300B6105835 + 75BABA7DA66C2C60EBFE997A00F9FB1D97D00A80198784134C4E3431E9349350 + E3E6991351963D009850F85DA6734CCCDFF7E66122BB40C1EF93EF1B10FC042D + 7F097E672F55D84B11FCB2ACE9DC659858239D873607FC3E357F858A0098B17E + 9FA3CB67A413097F9779C9B3AB0875BD5593D7D0ADF3562C0430F95FC7B743BD + 65D26A4CF2A1E7D046B17E2A5D3CF38D538D57E327AFFAECC4E2058B0F9F5DF2 + BAFF1E9916182C6F0686C8EB748B1414226FEC3E28AF055207E4CDA030798B00 + 5863F986CA34C6E1D977405D3CBFB5875038188BADFDF2DA5E2CD018AB078B39 + C6F4793F926EA262657A448CBC174EC5A22D5ADE0D8B910F6213656E522A169D + 595874A6CB2CC61AA2F56F5292CC888B9519B13106F6A624CB82D414999B9CA4 + 656A4E12B64D4C54104C4B5F6A1EEAB8F8B4E0774E429C5AFC2E4E4F97655919 + 5858BBE2F6A633A6523A16C819B2222F5396B8AC79195F9732AE9EB13D5D6C61 + 414DF8BB025A4AD88B05F5FCDC64D5DC4C8C4965E05869E18BFE2BF2B108C742 + 9C8B715D886727ABC5EDF28C44599E96202B701CAB521365258E635566B2ACCA + 4A9635F969E2559C216B0AD214CA12C8AE2B26ECCD5531CE2E4597CA5BAA8A64 + 534581AC2BC991B54506107B21BFB192F1B10A65435906FA65C88EA652D9D556 + 253E75C5B2A91CE394A05F11E16EA6AC65EC281C17F743E0BB82C70D11001308 + 6F28CD968D65C6A277737DB1ECE8A896A0FE26D90DF9B657C9C6FA225947976B + 5526DE15AD7737D5D212B640012F41B0718F9627DEB5F938A64C8D8345F7698C + A7B5BA244D9617A4C8D2FC1459CC1857C8130013FE3206B05765BEC6FD627E4B + 03017099AC45795353955A006F6A2C9755E8B3A6BE4256D75722AD92754DB550 + 9DAC6DAC96ED3DED1272E6B8041CEA95ADEDCDB26BA847F69F18949DFD1DE2D7 + DD22BE9D4D18A749F61EEE94E063BD127CA45B428FB54BF0A15AD9C5F864748B + DC5E24FB076BD5A276471BDDFFD11D2063BF95683C5A8A509571DB7C1A73646B + 5DA66C837C1A72C4AF214BFCEA3324B02953825AB365575396ECA84D95EDD5C9 + 8E92646B65826CAB4A149F9A14285902D0774F7BBE04D2757243BA04B660BBD6 + 1CD98534B02503F934D9DB95257BBA7325A8234B823AB36437D2C036F46D4D47 + 5D8EECE9CC95FDDD0512D25F222183A51272A84C82074A656F6F91ECEECA97BD + 3D4542B7CBFB064AE4C09172893C5E2751A7EA25F66CBD249CE783DB168939DB + A43096107718EC362BA8658C41C6C9657B3CCB0A6E1B2582A0F722CA573A24E9 + 9A51E2D57649BC86BAAB18F36283445FA857C55E6A42DF66893EDF2851E71A24 + EE528BF64DB8D2A6F5B197185FB14BE2AF7463DB1E8CD1AB00D8025CC68664BC + C5A4EBDDEA8633F9462FEA0725FB9323FA30DDC664A492AEF76A5CC6D45B436A + 85C587F0497C388EBCBA15FDF8B83EDCA7D4D2EB43C7DD282D815DED162A309F + 466B3007FCDA946E96097ED33F441DCA16E612CE52CCDBB289716BA030EB2CC4 + B5796BE16BC16DE667A7311E2131412FEBCF2BE865CA768261F6C922F8459D4D + E97299623E17FDF3BEB92CF9DF5E9182EFAEAA9827EC649AFB35AD73093FAFA3 + 8CF66FAE2808B2908756A9CCE77F45A87351212581A88D1B6A41A9CD9BD8BF97 + 55160013F67A00ABEECBE62D00BEE58856C837A4E81BB47F8DED90967E8FBAEF + 6E48F1B704BDB47CBDAD109840B818C75B4A5885B1087B1BFFF533057284493C + 666329690070E1B717A4E4471C1B44185CF81DCA385E7EEE3C9E077CD65C8C97 + 071512741150FF1EC7626135F2462CF3FC19F1F3D1BA39EF9B4B285F965282DD + 9F6E603F382FE85BF2238E15B263E9E7E6F9C2BE8B08727FC2E7866C7BB103DE + 08DD68C54C0B675A415B4BE8D2BB201CCE378EBF147D9912FA52FC0E0B915AC0 + 7E17CCC37E1532FFC158811AF8CBEF90DFE7652DD31A9896CA74494DABE532A6 + DF9D437A5EC53CAD926901EB16012AAD93DD6E8B097229137B19EDECF7F32D05 + B3B4A6A54BEE2A1C07BF37C25303813FF1E43BFEAF6FA0AF15BCDAB1B8BD4D39 + 8601B31F6B7F6E570D55FD1929853CEBEAFFFCB13440F57FA2852EDD527F2A5D + FFF31BE9FBFFFEA069CBBF7C86F68F1C61AC9FB17F887D6B99C7D816F852CC13 + 02D7430602F3E505FEDEF01991AF51E8EBE88F7754757FE4981FE398F1F9A0EA + 3F7DEC01B716DE5A706BAD79DDD6BAB4E0E567AAF9CB479AAA55AFB36DD5CF38 + 3F901B005304BCFA628593B79097BF09DBC6DF851BFCBA5FCAD0DF0CEA6CBBFE + 7FF53F82DF20AD779DDF13FFFF74E1CE974F98327637ADF9794EF48512E78506 + CA5E538CCB792382DF5CBEE4E27831609E75F6BF4C70CC6B50EE1704AC90730D + E4F52FEDE313920E657D86EB27B6CD466AC7A1D89FD72E2A1FDBB19DE23ED8C6 + 3E599FD26DBEE9CB7A96333EE64B3DA725F3135CA3313E5FFCE1F5DA5EA7791D + A74B7E7A6DC8FB92E3135AE31A8231B86DEEE7746D7D01F7135CC3EF1C9342E4 + 69E59CABF0F904FA9DC6BDE594DEDBF8B212EF5F8C2DAC2F2E5DEF177AAED097 + 9520C698A7D70BBED8440F174CD3F87213944C408C6D6C6C7A2A5DAD960983FB + F4652DBEE4456F1B312AE4F9D2D7B15A09E38B5E47AA301FA85530CC3C416818 + F207E8DDA3A71069B1841F2A9728B4850F95A3AE484207CA25F6589DC411BE62 + 9C48072C73FC24CC0D5219E7F80A8EF342BB449DA857B01C42D80C11B812D8C6 + 9DC65CE004016E9382DF948BDD127FA64D0170DCA916D4B56A9FE48B1D1AF399 + CAC09C80690AC64F63CC67C6423ED726C9675A2485FB655C646CA796CE2731C7 + 38C738CAED9244408E73917619738A0B9DD857A7C2EC68BED876B406C2B11F67 + 8ACF7FB8423FF35ECCA7827A4B851E5168411C76B44EF6F69763FE97A7A133F8 + B2DDF6E60CF16DC9919D9D741D5D22BB19DE6210E7089F9321335817E884D6E0 + 8B74FB31073B78A80AE7BF59A1F4BEDE32F16FC915DFA66CCC270B644F0FB6EB + A9907D7DEC5BA9317977B462DED9512EF44813D059E5B10AF6EFADC6BCB656FC + FB6B30CF6D907DC7DBD52278E740A3EC3AD4227B8EB44BD061D41DE9929093FD + 127CA24FF663CEBBEF580FEA3A15066FEFAC13BFDE26D939D8223E3DB5B2B9B5 + 4236B7578B4F5FB3041CED96C013FD12786A48769D392CC1974E4BE8F58BB217 + A9FFC943B2E3E8A0EC383E207E18D3E750B76C1BEC94AD7D2DB2A9BD46D6B796 + 21C5FC1DC7EF81BE98476FEB2E576DED2AD3743B2D7B3B2A643B5D5EF7D7A9D5 + B16F77ADF874D5CA96D64AD9D45C2EDE6D38265A144384C0DB3B4AC5A7BD44B6 + 1030F3A5CB06868BC957EB5EC6FF5D5F91A100785325E6E6B5059897639BC662 + 5D9BD00BD15675FF5C823545816C25FC85363717AA0B6C0F04568F3E269630A1 + 325D4B5B6BE24D95D8475996C6F8E59A671DB659538B755A3DD60EBD38F6CE0A + 595E92242B4B92657D65A66EBFB192203845BC4A519F1783F56134D67BD1B21A + F9B505F15807C6634D46616D98C7756ABCC25E825F8AF9C599315ABF228F0038 + 1AC23879584316D1D534D6904857625DB51C75CBE8361AEBAB25D9B400E6587C + 013ACE23C2E3056931FAE231AD8397651B8F5504C17312A3E483F80885C1B3E2 + C2E583B808999F44ABE438ACC769311C8F3CD7E4311A4398EB6A8A79BA929E9B + C830445C8FC7A9772E15D6F67C71FBFD683E0B88D4670326F66FA83014D44C02 + E2A81805C14CAD14F21E0C93B7420EA8DE3918AA718869356CC3451102BFA9ED + 0755D3820F687CE0D7F6EC53204C786C44901CA216C2AF071A5917D1AF041840 + 4C18ACD0D787CF480CE8A568E54B31FF86FF1E797D07FAA34C204C00FCFA0EBA + 8F36D6C316F612FED2BD34AD7E5FF2B6DAE9D1CBDE01F2F2961D0A80AD4BE817 + 3733E6302D836985BC439EDF60A1EF0E796923FA6FDA292F6EF09717A0A99B02 + E5158860F8B9D5DB6532F48217FA4353D6F868F9B955DBE4D915DE32099ABC62 + AB07065384C553D707C894955BE59925EBD55DB4CA89214C110A6BBA709DC2E0 + 27098321631D6CDC42BBE57111ED485D473B80F8515A0B438FCC58248FBEBF18 + 754BE4B119C66534E3063FC1D8C1EF12082FBC0B06DBE76F8FBD35D7631D6C41 + 300130536B25FCF0AB460A8247006042611A86A871886328E2967A0E74BC0A5A + 2313B71E7CDE4061F56468A1AF5A101310D350C53CBBA4F84CD382628D15AC71 + 83F9FCD4C2608A46387C166B34F6A9E73D609796BEEAF6996E9E9F9E224FBD39 + 5D1678EF90D581FB65F2F479E883F6275F90318F4F91314FD01A187D1F270446 + FFC727C9D8C7F83C18758FA0ED11E71931341A001E8FBE6EB84B8D84B60A72D5 + 48C9C8D3CF01C00F60FF84C0D60298DB5AF84BD971FE53E43CA75717D084BCF7 + 9BE7FCCA0A94AF3CE169A375F0B8094F19DDFFB481C00F3CA379CF73FFD1A02E + E506BBA3B55BB9FB416E4EF25B341A8BF92D728FA1D6BEFCEC232C806DDFC7F1 + 7B7B7BFA42797FF65299F8DC54859904BF8CFB4B18AC40D881C6BA1D9991C393 + 98DAFCAF0ADBAB6C79DC3034B540773471DF4C47EBE7616BD43F00487F210B5F + EF55FF1BC6FEC5F18C947B9CD1F28EDCE076A4EE1AEF1E22FCFD5F020053EABF + FBFF8D016CE4FCF0FFBB00B05B23FFB0FF9516C05623C1ABD5687DEE55F79F06 + 80ADD06E61306F2A7F1700BBE486BF2C8F7DD0C40026FCBDEF2163E14BCB5FBA + 7D36C1F921D4190D5B001BE84BF86BDBB09D0378A9B1B869DF4B6AE9FB185D8A + 1817D004C01E186C21AF63014C11FE4EC0848452100C19006C262AC340D89447 + 6A3CDF70C3E466EC3328BB00F084498C0BFC1A2642B4007E43F5E09469F20027 + 5304B99C24392098F2B87976E476FB3C52BA2DE406C0D60AD88AE0F7D13766A9 + 1E7A15934197EBE7DFB90030D39100D842605AF89AF824C356C1EAB666FA02F3 + E6E2F4E1B7182DF865FAE42C037E1F9FBD5C9E9A6FE0AEDBC533A1AFC25E5AF6 + 2E5AEDB1F27DD2A96399B58333020000FFF449444154F51317AF91494BD7C9A4 + 651B3091DFA4B16108759912F8BEB07E3B1604B4EEE5DBA23E5A7E1EA22BA167 + D76ED1B74A5FDDBA53DED886850C5D3C6F0F9437B0C879DD3F48DED8B94F21EE + 7BFBC2E5BDFD91F26E4894BC472BDD03D1F2F6BE28991644CBDF3099B697AE99 + AC256F141658586C39EE9EDFDE17226F217D93B17B189327344CDE8BC0E28C8B + B818BA774EC4823049353D2A56DE8BA45BA864999F922E8BB2B2646166A67C90 + 9C2CB392D12729516625621B88F179176567CA92DC4C599891864567827C9080 + 45241694D615B401C1B40A4E90F9C9F11AE377412A17B08C7B8BC52F63E96662 + 519B81457026DBE2F40D67E69764B00FDF8E36F0761117C0B4C4A5952FF28C9D + B48C40B6385396DB37ABF352B57E0E16D2737312D50278712E8FD36C434B61C6 + E6A55BE4D545995888A7E97684AC6BF2D28CD52F16D74B711CCB902E4F4F9415 + 5989B2926F706341CF740DF6B116FB5D9D9F2A2B7218A737097982D92CD95096 + A7F097507835C65C89312905C6A823B05D5B8CED215ADDFA3494C866C6D92D43 + 1B4400CCB1B82D8FCBF3B6784E8AACA125318132F6CD7E0A81B1EDA68622F169 + AB9480AE3AF16BAFD6D859ABCAF0D94AF1992A18F32A473654D3AD71816C812C + 04B6DA887642DF5545748546376C39B21EF2AAA4EBB41C59539625EB68195C4B + F7C865B219C74CF8EBDD582E5B9B2AC4AFA34682FA5A3506B07F6FAB449D3FA2 + B1803734942BF8F56AAE95ED7DEDE23FD82B7EFD9DB273A847222E9D96F85B17 + 25EAF22905BFFB4F0C48C8A9431284ED761D42DFBE66F1E9AC13DFAE5A09EC6F + 91BD43ED123C54AFD61B81749DDC5922BB7BE842B041428F344A3063EB220D3B + D628FB07AB65375D01A25F5077990476148B5F738E6C6B4887B2C4AF255702DA + F264777BBE0431FE2DAD74EBD3641BA12FE45B9FAA8077476386F83765CACEE6 + 2C05BDC13DC51231542561872A653FF2FBBAF9D0B54C228F98079351C72A25E2 + 088E69A04442FA8B3C0AEE2B94BDDDD85F57AEEC433EF448953E80E583DEB0A3 + 551272A81CC75C2AFB0E959A7878183FA8BF44F61FA954B84B981B7DB65162CE + 35492C01F08556B5A2A5A2CF354BDCC53649BCD229C9D7BAD5D296796B9D9478 + A94362D11E7BA9DD3CB4BE4EE8DAE302B03D9270AD53E2AEB44BCCA55689BDDC + 867A4259A78D635DE971B6EB53D86B816FEAAD4115F35684B96AC57B9B309720 + 97609880B75F12315E3C8E810FD053EFA00D4A427DF22DF4BD754412097F6F1E + 9294DB8725E3A3E3C3D096C0D681B954EA9D23BF10DB6D5F6ECFD45A012B1040 + 1BB7E5F854028E3B910FEC3F3C22291F1D95E40F0F434714F21218101E58006C + C16FDAC7181B7DD238E6A7A7D41534A106F3C6FA97B0E1AC020F03812F4ACE17 + 1724F3D3B3E84B00724AD2088A1D189C8D7E54D61704C09714F45A086CA16FEE + D797D17E01DBB02F8131EBD10769F1F73714F2585844A098FDF9793D06821F4A + E3F5BA00A896D9E6D4310670FE77973CFB654A15605C02672B37042E5140755D + F2BF425F88F9B21F69A5781BF99B92FFF53529F9F69A14A28DD676842DB41826 + 082DC0B113849611A27E7B590114416C298F8516898ECB675AEAD2629A16D2B4 + F62DC0E7CBC3F679C8E77E6700B005E516FE2AB84559C1173E4BC17706A62BB0 + 75C02DCF010130A1B79181BC23A5E7CB82325A373AD06D18AC4184C4C81BB87B + 4DC16F19EA09E4D98716DC56DC8E90AE8CB08EF04EB731C097E7CD0DD7099A09 + A3999611303A909116CA3C3704E3CC13FAD2ED75D1F704FE17354F97D004BF06 + 04D32D342D8509790DDC1D86BC849537152453953FD3FD302126E3DAA28C3E6C + AFFC13AD5A0DDC24C8ACFA9920F32355F59F3F52C859FBD78FA5E15F3F93FABF + 7DA256B5D66532F769F66BC0B1B51EA66AFF44107AC78C49584A391058FB41F5 + 3F7F240D7FF944DAFEE7D7D2FDFFFB413AFFAF6FA5F55FBF90E6BF7E2A8D503D + DA14D4A21F616DFD5F3F93A67FFD529AFFED2BA9FDCBA7FA9BABF8035D38634C + 94EB205AF46A3D54F5C73B520DD5E2B3D440ECA7C7893AF6ABF9F3A70A7F2903 + 6E09823F32E0175228EBFAAEDCF9728C518173C5F3A680D8D9C6825F0B6DDD75 + 16F4DADF99F9CDB19FF95D18E80BE93E3006C4BC969DF1ECEF94B2657DD100BF + 4DAB02FCF7F3F15FE36F9BD7027D3184D703C8589E1BA04D111C5B97D3F68512 + EBC580D73D825D5EEFAC78CDCBC5752A8BD72B5AE9E2B7CFFDEB35F0635CF73E + C7B591D731C7C3413EAE711C8BD74F025D6B554CA09CFD29FA7F7A46FBE67D85 + 6B29AF977A7DC5F51263D86B6BC62767349FFBE5054F1FD3CF1E9785C0662CFE + 37794C1C231BC7A3C7F8F515DD57EA1D7A8F3825D9B8567B5E2EA26532C4178F + F86212EF53BCB7F14525863CE0CB50BC37A6DCC43D8571EA9D7BA47D494ADD52 + 3B222CA69857AF17D8264D613063F6A20FB649388FFB30EEF971679B354FC5B1 + 0C25221F8F7A03881B14E6EA0B643D9863F416CAFEFE6239C0F9475F31E62898 + 7BF4164B08E62A7BBAF2655F6F915A1853070F572A3C0EE70B64273077423908 + F399A0AE42CC412AD4B2582D8CAD0E1336D749C4B106C7357437F6DF2C51C71B + 14D0A65FED916402EFB3AD9274BE5DE132E16FFAD56EC9BCD12B9948D32E7648 + 128E3D197398349433F879AFF4AAEBEA0C9C8B0C9C4F5A21E7E27E9E751D7381 + F3987B9CC738183703E725F91CE696982F451E2EC33CAC142A9383FDF8CC3DF9 + 7ABCD1A778CE5A241EFDE83E9BF18483072B644F7791EC68CD56101CD85928BB + 087AFB4BF5E53A5A4D871FA5156F19E68CC5FAF25D0863111380F757C87ECC23 + FD5B72C40F73417A71F16DCC52D1530C3DC670AC3DBDA5B2AB932F2516626E59 + 2C01ED652AFF366CDB817C77A5C6CDF5EDC2DC75A04EF61E69762C8231AF3DDA + 86396F8BEC1A6C42B94D0E9EEC93F0D38724ECF420F2FDFA0224E308FB622EBC + 1373E2A0A10E0DB3E2D7D320018318E3788FEC3B33207BCF62EE7CF6B0EC3D77 + 4CC26F5E90F84F6F49D8CD8B1278FAB0EC3E73540E5C3E2DA1574FCB9ED343B2 + 63B04BFC304FDFDEDD20DBBA6BC4A7B74AFCFAAAC4BBBD44AD81BDDB8BC5BFBF + 4A02066AC5B7BB427C18FBB797AEA66BD40DF4CEC17A754D4D104C00BC156B0E + EFD64AD9D65E215BDA8AD59A98EEA4BDDBB02EE92AC33C1E6A2FC538A5E2DD5C + 682C7CEBD08750B6126B137A26C29AC2971E83B0BEF0467E4B3DC6419FF5F538 + 9E96227C7E13B3582D8B09801BF3D5CD3421F0861A5A161728045E5BCAF5608A + AE6568F94B4F466C234C5ED75020ABD0774565862C2BC75A0FE9AAAA0CF1AACE + 34EEAA395605DD40A7606D98A0F077794E942CCF8EC41A304256E74443B1B23A + 370E6BC178B427A0DD58FDBA2D8009812D082600B64078554132D67D69FAA2ED + CAFC24A1DB68D633BFBA90809856CC69A84B10BA995E901689B52F01323D4E19 + 8F55762D486BE0B94951EA12DAA604C25C3B2FCB6628A274998FB52B61EF8234 + C622C6767958CF633D3D372546E6A1DFBCE438F5BA353F056B73ACCFE7C4C7CA + FB31D11A43780E5FF88E61DCE0048DFFFB6E7898A6B3E8561A62CCE1D9742DED + A4AC630CE277C3692DCC97CC0FC87B61E1E63903B6B361A0088159F76EA8E31A + 9A96C3FBF9627A88BC1D7240A1309F51D08D349F5DF0D907352D881ECD0883F7 + C86B3B83E435FF5DF2EA8E4099EAB7535EF1091C960BFEBE19108434C8038719 + 4798ED2F7907680C602D6F33AE9F2D04661DDB6CBD15AD8D8D2BE8002305C306 + 0EBFB819DA843C347533C6DA8AE3DBB647EBA6ACF3F300E1C95EBEF2ECEA6DF2 + DC1A1A0318604C57D12C13005BB981F033CB366BFEF9353E3269E566796AC97A + 79C6026088AEA1AD2639B184AD55B0BA909E63E2046BACE0D9C3AEA19F9ABD0A + E96A8F9E98B5CA0382AD4530DD46337D62A6B10C26FC7D72C6127962FA62CD3F + F6AE7117CD94CFDCACF1C593EF2E90C7DF9EA7CFE2F84C8E61D91E7C7DA6A626 + 849B891FFCC86BC8332EB0037D7F21C72084EDB6DF032F1BF7D06A19EC02C26E + 3DF8C2DBFAEC91C0773CAD7D1DF0CBE7980F4C9936FC7C12D2679B936CFCE057 + 653C9F8D6AC83C82603E577D45C6B901F0D32FC9B8675E92314FBF68602EF5D4 + 0B72DF1353640CDA1F7FF55D99B17AB32CD8E22F93DE99ADFDC638ED2AF41F03 + 8D657F5A103F8EBA1100981AABCF9647138D8F46B702B6CFB6DD6E9FDDB2753A + 86B38DDDDE3D8E5BEEE7E6FFB0465A0A3BCFEEED737A3EBFD778B7E30D2FE033 + 7FD631A5EEFFDD449940A32CE779FF98B18FCBD8B1E8C7E7FDCEF8631E44DBAF + 81DD916D6EB9FB41A3F3927B6B3416F35BA56390918C319C878679E3786E50A7 + A23B67F61B4777CFCFC8FDFC6EC6199879FF434F9B18C09611A10FB771B323E6 + AD6CDD3D6501B085C0CE7E2C1FB35077A4DC0078643F5BF71F9605B0F7AA1FAD + 6D84FEEEF1B8C7192DEFC8425BABB1F70DE7EF1AEF1EFA6F05C06347A9F340E0 + 91FA7F1A00767EF8FF2B0060CA7D0CFF0C007C777F96EF96BDE0FE9A2C841DAD + CDCADDE71F199B1A6D6C3B9E9507FCDABC233700F6C05FCA7DF371E91737388E + C1378FE8FE1937DFE19BEC2405C0639CF8BFB68D37E25F03C0A3015FCADD6601 + B005BF16FE4E78F265190FA99BE7C7F9E618FA8C84C048C7EBE4E46E006CF323 + C5B7DAA8B1C8FF1A00BE7FD2EBF2004400FC202753CE448A90575DAB8C10EB3D + 623F4776826545F86B276CD6F25701F0AB33EE02C08CFFEB89FBEB00E0075F7F + 5FE12FF3EAC619134BCA58FE1A97CF4626FFC47B8CF14BF7CE8B5DC2C475D612 + 85C014EB3445DD337357CAD3F357CB330BD678522B969F5A40F0BB0613EC7510 + DDEFACD53253AB898BBDB4FDB9E51BE57907F8BAF5FC86EDFAF628171014012F + 6563CA983AC6F50DD23756A93703F6CA9B8158F410E01E0897770E446281C4B7 + 6CA3D552777A443C165071582C45CB1BFBE9A6098BB4B038B4C563E115ADDB4C + DB1F8A8554A8BC17122A330E46A8D5EF7BBA08E3428D0B33C60EA2C56EAACC49 + 49935989C9323D16DB63E137273955E1EFB2827C595E58208B72B2640EE3F466 + A6CB62E4176565C8C24C5AE666CAA2EC749997460BE03899158F456142AC5AF8 + 9A38442606D16CD473B169C1AFC6DC4D8D8750C602955A8E852BEB2D005E8C05 + 2CCB7CE3D9B86D4EC242364553BACD620CA5B525D9B2AE2C57561618F8BBAA20 + 4396E6A7CAE25C6C93470BD654595194A1EE8A9715A20FA16F6186E99F9F8645 + 3816E30EA025005E858532AD819732163001308E6B55562216D531323F39140B + E26059981C2EAB7313558B52E9762B4C16225D969D80853CDFF22658C6383989 + FAE637F3ABF33314006FAACA474A57607431CC37E0F9967AB66C28CF55EBE18D + 657926B62F5D4997E4CADAC26C752FBDB9B248FC5B6AC4B7B11CFD726463698E + 78571B17CF6B2B73D4DA777343B16C692C91F535F92A960986FD3A2AC5A7A54C + 1F826C6F29951D6D15B20DF96D0D45E25D5F289B6801AC0F63B214FE9A072625 + B2BDB9DC51A9F8B6968B7F0763D7D661BC6A8C53A5F0776B331F70D549F8C901 + 893C77580E208DB9780C698F6C6BAF97CD6DF5E23BD0297B4E1E9203E78E49C8 + 99C3127AE19824DCBEA84ABC755E22500E3ED92FFB8FF7693C34FFFE66F1EDAE + 976D6DD5B209FB56EB0268474BA1F8B7E5AB02DA0B24B0B348F6F6954BF020E3 + C9554BC8508D1C3C0C1DA9455DA5EC43DBDE5EF360CFB7990F89D2646B7DBAC6 + E5F56FCD935D1DF912D89E87F39A2D3BF4E15E86F835A42BF465EA5B9F263B69 + E5DB962BBBD16F7F4FB1C2DF88C3D8575FA9ECE1C35202E063B56AA91375A246 + C28EF0784A64FF40B1841C2AD158BA8CA9CB58BBBB7BF22404E5C853E80BF101 + 241FC0469CC4F6B4EA3DDF2251671A25EC44AD8460AC88F34D9270B943016F14 + AD712F3449DCA53685B249D7BA557197187FD7D4C5234F381C7709ED57195397 + 0F9C0724F9C690245F1F92846B87A1A3927C0BBA7954526E1F93A49B04AF8735 + 8DBF764865DB4CBF639272EBB8A4DE3EA129CB09D707D13624999F18489BF111 + C7C1B657FBD036806D87248D60F5F62149B8D92789377BD1DE2BB157BA24FA72 + A7C45DED96841B3DA847FF1B7D128BEDE26F607B28F1168EE53663ED1E7320EB + 49CDA77E745CC16B2A1FC273EC0F718CACB3A275D927A790E258B0FF943B87D1 + 177D90DAD8BD0937079D7D0D28FCA5087D933F1A561A3E53D617A725EBAB3392 + F1F94949FFEC84275550CCF190A6E2B8D2088091A67F7A46323F3F079D45F9B4 + 8A75199F9D55657D7E5E95A120F8A4C71A9832DB9D7360EF25C926C4A0059D03 + 7F097C73BE62BD81BFF9DF5C979C2F098109360D0026D420D05070CB3ED84E21 + E5F7C67AD86D51CC71EDF81634B34C717B7B1CDC17C12FAD8DCD7E095509A568 + 5D4B4BD99B189F4095808856873C966B381642610340097B0980793C0AB53816 + 3F07948FFA02C63876A074FE37E8F70DA1B981BEB4DC63BEE0DBF3D807DA317E + C10F37A1EB92FF038EEB3B038473706C542E8F9542DE0DB32D0036A00CC7F8BD + 03BFBE33EEA60BE90AD7015B23454096FDE5599C1B1C97EB5CF13BCAE5B9D273 + 63002FC19511AD7E79AE8CF87D5898A7E7CE2502E0F2DF1B70AE60DD03D86F7A + A0B5817DE8EB586F12C80DA73C76E3AA9A10972EAD8BBF3F8771CE4BD9EF2F49 + 055D48A39FC2697C76B544FD0DAAF8232D8E0DF4B52E8D2D103400D8B829765B + B412925A8869AD64156E5AA1AED2E9A770D581AAECEBAE23C855CB5B5759C1EE + BF7DA96AFC972FA4E16F9F4B1DEA689D4B686B60AE81BC4DFFF695B4FC8F6FA4 + E15FBEC4F668C73EF45820EE4B8FCDE94FD7CE7AEC4C59CF3E10F3DA17FBAFFC + 138E51D38FA502FDCA515F863E657F208035DF95FD9E6C9E629BE9C3170FCC4B + 066C77035EFE3F6D5FF7384CD9A6BF5BFB9BC036766CF6B1DB50EC677E2FCCDF + DDA718F5FC0DBAAF053A0E7E137ACD70AE15457C6901BF79FBDBB2DF3F7F772C + 5BF1BF625F8CB080D57A3BB07036FBF30BB8A69D57F19878BEF8120BCBF60516 + A6BC7E188F06CE8B27B498C7719AF3735D72BFBCA4D7CD6C6C9B83FF1C534265 + 037B0D6876C3641D83C2F58BB057AFB3B826731B0266DE4F088B2D344EFFF88C + 647D6660328175DA47B836A38D203A1BD7EF2CF4A7B5B1DB5299F94C5EF3711F + A2BB697AAC48A0652FEE7BF462A13017F7262A8D50D7F6B9C27008FDDA8FFDE3 + 518EBFDC833C5FA6E24B538C6B0FE13E9A46987AA943BD7924139C5E631CDE0E + 0F50654A18CC39875A011FA992D0C3E572E050A90463DEC197CC423007893A56 + 2DB1C76B2494F175BB317F415BD8B12A9D8F841C2E95FDE8BF1F6D1A8777A852 + 5F440B3D5AAD2F9DD12A370CDBEBCB69FDA50A534331AF22008E3F83F90501F0 + E91689385EAF2E9D3308C031EF883FDB2A89E70CB026A84EC49C2681DE494ED6 + A99230BF49C57C85D6C184BC39B8D7E7E29E6A7444F270DE183FBA90E7F70AE6 + 08E7D0F77297E4D355F5F51EDD3EE658A5441FADC0677360303E3BC35D64DEE8 + 972CBE80C617D46871CC73C6F9068E25EC5085EC67AC60CCD9F67416A8D574C4 + 914A892748C73CEC20CA07FA8A256AA85C620E574934CE4714B689C5F960F900 + 630D631E18D49A2301F59827622E19D09825FB3A0B25B41FE7B3AF4C76614E1A + 8079E5CE36CC4D3BA0F642D985396A606FA9ECEC2ED51716F71FAA91E021CCEF + 8E344808E31C1F6B96E0234DB277A8116A9690A31D72F078B7849DEC837A65FF + E176F5EAB3B3AB5E76F736CBFEA14ED475A15FB7449C3D2491178FCAC1738764 + EFA9010938DA233B4FF44AF0E5131271EB82845C39257BCF1F95B0EBE724F9D3 + 9B92F6D92D89BA7A4EF69F1C925D435D12D0DFA216C8BB0E37CA8E811AB5F6F5 + EE28159FDE0AF1EBAF12DF9E0AD94E78DB592E017D35128463A41B6B03B05B15 + 60EFEC6F10FF5ECCF1BB6A643BD619DB3A4B1400AFA9CD920D0D79580360EDC0 + F8CABDD5B2ABAF4E767456693CDF6D588F70ADC235CAA646EC97318431DFDF42 + 6B62BE64DA542E1B1A8B645D63816CC1FE19EB787B77A56CEDC05AA0B554BC9B + 8A64635D9E78A30FD7275CC3AC2DCF92359017D643AB4AB1062BC13AAB3C4363 + 09FB629F1B9B8BC4AB3E4FD6D6E5CAD2B254595E912E1B9BB086C2388C114CF7 + D1CBF268791B254BB3097F63B1368C9715D9B1B22C234A96D3F5735A982C490D + 93A5E8B3CC81BE04B9B4F6758B0098F0D7C2614260D619D89BA27D2C30669D57 + 313D4C61FD5990ACF5767BBA975E988EFD6427CB2AAC57B99EE4BA9296C1F352 + B0F64C8EC47A196B53478BB16E5D5988CF8E35E4FCCC44ACCBB10FAC9997635D + BC186BDDF904C7EC8736AEAB17618D4DAF5BF3680DCC97B73D709796BE58BBC7 + 603D4FB81B152AD32343656674B86A765C9447F393E2747BE6199B783AFA32D6 + 30353D325CE30D33FE30F3B42426049E76E080BC7D3054DE820C000E96770E1C + 94690E0C7E2704FDF6A1CFDE10796B4FB0A6D3F6060F4360FF5D0A7FAD5EF10D + 90977DFC65AA6FA0BCC6672801BB55AFD1A5B44FA0BA8766E8AC97B71B50FC46 + 00C6F1DF272F6EDD292F780768DB9B3B83E58D1DA8F7DBAB40D80262B7088729 + 85C2B410DE026D461E9AEA8D7D6EDB23AF6FDF2BAFFBEC9397B7EC92E7D6F9C9 + F3EB77C88B1B0364CA7A7F99ECE5E771134D79003064EB59C73CE3094F5CBEC5 + D1667966D946797AE9068FD41A78F17A799620186DAAA51B65E2A2F57743E0B9 + AB346630DD4313FC6A7E2EE308A3DDD1E3B3572B247E74D6B0BB68BA88B616C2 + B408A61530AD81AD4530E3053FFCAE01C0F4B4670D2F9E9C6E80B005C056365E + B0C25F27EFB6101ED64C68441CE1D7666ABFFB5F996E620543D672D896DDBAFF + 055A07BF2313F8FCD17986E9365AE1F34B1AB4D8D8C1E326D215348D641C6319 + E7D9A986D9739EBF52639FE473D697652C01F0532FC898671C204CC84BB08BFC + 432F4D93C75E7907FBE2B82F1B40ACB0788A8C612C61C795B4C2DFC7680D8CF6 + 47A86100ACCF8AF5F9F248F179B3793EAD30F761230B6FF579F6EF201704B6B0 + D8BA835697D04E7FBBED68F28CE706B923A1EEBD64FB31B5B2CFE5F90C9F6C00 + 793EB367BC5FC601D6329FC53B7D0C074019E38C85582693514EC33E7CD63F1A + D8A52CD81DADCDCA057FA9D1D8C9AFE96EAEF2EF93425A9C0B05BE74033D8660 + F5090F0CB6FB5126E4F02AC6FE1D3BC1D4D1EDF3789E33B63B30D9F22A2BADB3 + 70D79DC7581E386A41A7B38F7B41530B78DDB2DB313F120ADB7A4FD9BD1F9BB7 + E5D174AFF6DFB2ADA391C7F30BB9C7192DEFC8425B8AF0F71F01C016FEFE2F61 + 01EC81BC23C1AF5BE3A8BFFFC1FEB796E78B76FF69F087B37F9491E21F74C41F + F8BF5BA35E8C7EED38D5D7BCFD2C14B71F86AE6E59887B1F038A6B9DD9172F56 + B6CF4848FBCB6DCDF823FBDD4BF71A97D21B07C5BC2D3BA905C0BF05025BF86B + A5F5BC413D64A0AF825FDC6CE976C3962DE01D29DBCFBAEAB02EA0AD15B0CDDB + 1BBB8903CCB61714005BDD0D8039D13000F8BEC79E575977D0D60298E2C4640C + 5215F38EECE4C50D85B5ED298893977B4060A3D7E5FEE70CD4D5581A2EB9E1EF + 687576626521F04398803D84C91881AF1BFE6A996FF659F7CF98E031B5F17E29 + 825F42E08769F5FBD66C05BC4FD182D7995C5AF8CB09A7BA77D6D4B8779E387B + A93CF3C112797AD61279EA03E4E72C3700187A9CF17E672D94A7D907F5931CD8 + FBD47C4C8E1D003C71E15A99B4789D3CBBC84BF30A84993AB077D2D2F53265C5 + 2699BC7CA33C47AB5F88F917D778CB4BEB31895FBFCDE3F6F9155AFD6E86B6EC + C062618782DFB777EE9777A0A97C1B75ABBF037CF729F49D468BDFC0FDF2D6AE + 607D03765A4898C2DCB70E44C89BC1E12ABA692604B62E9BADDBE6B70F466341 + 15853412FDD92F42DE83A61F0893F7C32364767C827C101F2F33A26334D60FCB + F392B1284C4D979989C9F25E6CBCBC1B8D456022169859D90A7E571617C99AB2 + 52595D5A2C8B724CDDDAF212595D52282B188F373F5B1666A5C99CD444B5009E + 9B92280BD2683DCC38C0C6CDF3C2D464B5FCE562938B4EB5EE25E04D4B906599 + C9B23C8710365DD615E768CA3E2B72D3B40F2D8259E65BCD16FA527C0B9A56B2 + 1B2AF255CBF3B0A02700A66BE7922C1C6FB6825F5A0853CCAF29A3356B8EB6AD + 2ACAD4B7AA393EDD6BE91BD6D80F613021F24A8CA520180B665A022FE3423C39 + 4C01F09CB8105986C53901F0F22CC66D8AC2678B92A59958CCE726E3B8702CB9 + 7CC39B8BFD3405C2EBB0CFCD9579B2B5B6483655E5895729EB8CCBE7F538A6CD + 95851A3398A0D7BBBA443655146A9E75B4260E6CAB93C8A3FD12DCDF2EDE356C + CF97ED0DA5B2BDA5C2F3208529E3FFD205B47F57ADECECA957D12AD8B7B542B6 + 35A11DF26BABD08726DE0D381607FE32F56E64DCE03CD98832DBD8671B4468CC + 6D023AAB35162FE30C13FA1204FBB657EABE02FACC83205AEB06F6B7AAB5826F + 5793F8F7B5CB9E1383B2F7E4A0041EE9929D873BA12EF13FD4268187E8F6AE5B + 5DDFD1EA97DA39D0AA16059B5BCAD4A5DC16A41BF1B936D112A02E57B6D5672A + C025D0F56DA6356F8E047616C8EEEE22A484C339C833CE6E9142E29D1D05EA06 + 705B43866CA94D5500CCD8BB740FB8BD3E43B6D5A52BF80D68CD969D6DB90A83 + 29E6690D632C42CA35A5E81691F1F698EEEB2D519789E1476A24FC58B5841EAB + 94D0A395FA9035FC789584414CA34E139057CB8163E5127EAA5662CF353B10B8 + 41122ED1AD32A16D9B245DA3C510DD2B77AAC5AEB596653E12DB44D312E832EB + D9A75DE2D0976D71973BC45AE0D2EA28EE325D54F221739F24D2EA88D6B5D78F + 62BBA39274E398A4DD3925E91F9E814E4BCAAD13927493B0F7B8A4DE39019DD4 + BAE49BC735659F8C8FCEA8526F9F44FD0949B94D4B5C63599B4E287B87C09810 + D958D7C6D3328AD6C11F1D46CAFC80E6D33E392C291F0F4A0ADD406B3D01F1A0 + C4380038E1E610EA0E6B1A87B205C184BB04C02CC75D1F54B13FCBC9DCB70388 + 8D0C2026FCA5086C9320C25D82614AAD881D289CF609B6FDF498247D8863BD33 + 84E33B2AE99F11EAE23C209F8A76C25F2AED539C13D411FC1220D0956926C182 + 05BD5F9C972C02902F2F082DDD32A034F43350D8A4B4FEA5D52F4149D617E883 + ED088B152A7E7D4961B01D8BE0D702D8DCAF907E7D4DD3CCCFD8CFF457EBE04F + CFA28E96C41784AE89096509422CF4B1C085751E80896D2D1062AAF96F68B56A + 528E61DD4D9B6D0981312EDAB39DE3B2C049F7877D10CC10FEE47E89BE3A8E81 + 4B164A1721A5FBD57C6C9F4B90E358E7657E7E4A45AB6B0B80295A011372D262 + 97C0558F81FB82085FB33166E6971755591C13EDE6B869B5ECC0597C2E964D1D + 81D70555E1B7B49EBDA800D5C8405F4AADA4BF25D825781AB62824B04FFF94EE + BEF17DE173E6F11CF273135239AE6AF3716ED5C52C41D6EF0DF82D258CC3B9B2 + E78129CF31CF8D05C3045E4C0D541FFE4E78DC047906F859CB4C63554A186760 + 9DB1E0A58B687E165A0397FCFE9294A34C17CFE5E84B6B57859D7F36B1692DEC + B56097B0CFDD66EB3C0018FB27BC3416AB04C074556CE035CBB48C25A864B9FA + 4F749B6CE02BE1AA8DA14B29A825E4FD8BB1AE651F8A305781AE53766F6B616D + DDDF3E97867FF942EA91120A731C0B74757CF6C398D66D33538262E63D409AC7 + C07151E711CABF00D6F84C06F4A21EE35216000F43E03B9EEF66F83B1A06B9A6 + 3C6C9D6EEB99B7DF31BF5F05B210F36CB3BF571D830095D018ED761CA35FFE56 + EC78EE71E8F29CF584D12CB3BFDD9F19836EC4CD4B05FC3D5911FC52B6AC56C2 + 905AD03BA0D8D3A6BF73ECFF1B5E572EEAB580D71CAA0ADF41C3BF7D85ED782D + C218F82CFAA206F2396867CAE3D0EB083E4F21DA8A9DFF02FF13FC9FF0F8EDE7 + B4903797FF77C25F8CC117505867AF6B4CE97121F5C393CE75FA9C82DDB48F4E + A90884B96DE6A7B8663A96CAEC47F1BF4C4BE0F48F0879E98102D7275A04E3FF + AD6EA7F93208AFDD8ECB6975494DCF13778EA868314CD15A98621DAD86150EB3 + 0FEF3DBC57E21E49110E6BF803DCCF52A1B46BB857E27E9876AD57D26EF649D6 + ED41059BA9B424C6BD9D7098292D63131D57CAE9573A2505F387B8330D127DB2 + 56624ED74B22E60B6917DB241DF38364CC2F62CE308C4483C49F6F9248CC45C2 + 4F10F4D6CBC1A355123C542E078E54A9A791B80BE87BB609739646898668594B + 57C9A1476B25F162B764F245B1F35D12711C7D4E346B9CE0F0E30D0A82197398 + D6C031271B8DF5F061CC958E9B97E3624E1000630C1C4702F6434BE1E40BDD1A + 2738FD5ABFE4E1DC14E2FE9D47208C394021EEE979B48EBE88CF76BE5DD22FB4 + 4BEAF91605C07138F68453359274BA56E28F974BFC892A3D0F79B87FE7F27C5D + EF55E57D3824259F1D977CA41974257DA25EC230970BED2B9530C653EE2F9208 + 7CF6A823959AC6E25CC49FC0F93B5C2E61BD4512D6572C49F85C39183B19E72A + 9C613B30C7DC8DB9E76E7A84C19C337288DB34480CE6847C1970574721E68EF9 + 98AF3BC2FC73676711E6A145B2B7BF5242866A65FF40B5ECEDAB927D876A64EF + 609DEC3FD40035CBBE81668D1F1C88797410F2C187DB65FFE10ED93788B9716F + 93047635608C5609C11C3AE224E77A2724F6EA0939707650761FEF959DC7BA65 + F7A93E09BE7C5442AE9E9203574E4BEC875725EDABDB92FAD94DCC9B2E4AC4C5 + 531271FE84849E1C92BD9877EF3DD62941475BC5AFAF567C7A30B71FA853CBDF + AD9DA5B291D6C02D450A8069814BEB658D617CBC4D424EB4AB82869A65F72186 + 6BA997AD6D98B3B7624DD19C2B6B6BB3641DE6EB5B9A8CC52EAD80F7F437E273 + B64810E31BF734C88EAE7AB56CF643EAA3AA93ED1DD877478D78A37E0BD6161B + 086D1B0A64536B19CAB4022E976D483735623FF5051A27982FB96E2148AE2FD2 + F58F777399BE10BBAA3C4B5F84F56E2A95ADFA2269A9AC6FC21AA7B554BCB0ED + CAEA1C595B97276B995671DD982C4BF3B0DECC8D9515F97C993749BC68999B97 + 202B3263644556B4AC44BA320BEDB9711EF0BB3A3F05EB45AC599127C025E025 + D4B5D6BD14612E2D7A097C57A14CF7D1CB7312748C95C8AFCEE71A97E18FA254 + DC969A9F1A21F3522290C7B194304C0F5F74C61A3A8B3183A3D146AB5FE3229A + F097718597E7A7CBC26CACC1B31265493ED6BF2C632D3B373D4E1664122E63DD + 9A8BF52DFA2CCE489479C931322F315AE64273E2A2646E6C945A031300CF8EA1 + 35305F140F971991A1F27E142D81B10E463FD6CF4D88D1D04D8BD2B956A75532 + B64988D297BEADE624D22318630CD3929800F8A0BC75F020F27C113D5CF37419 + 4DD1029800F83D3EB3088DD4F4EDFD073C2214A63530AD80A7FAEF92D714F2EE + 92577C0DD825E82500A68B688D09EC632C841946EB8D80BDE8B753EB1412A38E + B185194FF8151FF6A785B001BD2F6F732C7F097969E98BBCD6D33A98621BDD3F + 6FF497E737F8230D9097361106A3CF96DDF2B2F72EB5007E81759B76693DDB5F + DCBC53FB3FE7E58061E4097D15FEB28C3EB416A6D5B0B6A1EE39B43DBBCA5B26 + AEDC2CCFAE3072C3606B11EC81C010DD43DB58C1EE38C12A949F5A80FA795E77 + 41E027E6D065F42A85C084C116FE0E03606311FCD8CC25F2F82C6315FCF0BB0B + 541A6E4D8D328C073EC25F3700D6E7756FCDBD2704B680D7ED267AB4F284971D + E0FBD27B2AE62710F43A22F425007EF0E5E9C6BD34C56790A8A7681D4C17D00F + 4CA685B0F3FC92CF32271A039809CFBD26E3293E1765D83C75094D286CBC2FDE + AFC63406005BF06B65AC8169E53B59EE735C3E6BBC60B6B10FE1EF1384BF7C9E + 3B59C6A82190310672C35FF38CD83C37B6CF970DFCE53367C782D701C06E29E8 + B5CFB37FF7B4298F90ED6BC1B11B208F14EB473E2BBF97DCCFD7EF25F7F379FB + 9C7F18E442CEF37CCFF37B96097CC73F216351379EDE39755BB63D310C7247CA + 0D7551BECB4A98F5A3F4D3BE0EDFF8AD1A95AFFC3BE41E8B796BFD4B8D7B80E1 + 36597ED423F6611DDB348FF344587C17AB828C41A3534FE86BE5C04CB22F0F1C + B5A0D361637F179ADE43DCCEBDEDC8B2CAEECB2D77FB7FB5463B0E779DAD47EA + 86B79482E0B1CE36903BEF16CF811B026BBF310FFD3702600FE8B5D6BEA3C975 + 42FE6FA4BB7E94AE2FD8FDD684E7CD09DB9F65E74FF9CF14FFC0EE0BC03F4B76 + DC911AADAFD5BDFAF282349A78012798F55CCC7F452361AEBBDEDDCFAD91DBE8 + 0D81627E44596F2A2E79FA8C722372EBAE1B98BE35859BAFE3069AB25098799B + DE7D53765B039BB7B63C37720ADBE8DB5CE86FAD80B5EE11960DF8755B00539C + 248CC564816F88D994E25B631E5724C87362C137CEC63C6DC0AF5B84BF561E20 + 4C39F077240036EE9F097F999A4991BE1DE7C86DD9CB49939D3CB15E27519075 + 056D275B0F63E24500EC76D3E201C1538D05B0BA7D46FE77AFA2EC02C0B4FA25 + 04A6E52F01F0E3EFCE9367662E96A7DF5FACB097E2449322F82518A665AF425F + C6F69DB1D0B87C46F9E9D9CB3C00D89497CAA4F9ABE4B905ABE5590B7D1779C9 + B32EE8CB54A596BFEB64D292F59EFCF39888BFB06A8B425FA61481F0F36B90A7 + C52FE500E0173620BFC9170B087F7985B164BCFDE5359F5DF286B300792360CF + 30F075D27783B0D08168FD4B00CC383CEF47C6C9BB61B10A80DFD88F45516894 + CC8849C4C22C190BAB44792F224EDE098D81B0C8E2C2293C5A66603BC6E6792F + 2C426645D3023709FDE3E483F80459929985856881A6F352526576728ACC884F + D418BF4B730976F364495EAE2CCECD91E585F9B2AAA448E1EFEAD22205C05ED0 + CA22F4C9CD10C6E75D909522F3D293B1C04CC502335D166712E0A6CBCADC6C15 + E3FC2ECB4ED305A705BACB9012F432462E61E796AA621C13E301E318D0467151 + EB717FC537A0ADC56E0116EC1063ECAE2DCE528BDE354599582467AB7BE75545 + 19B282F0170BE22558FC7261CCFA8D5585B2B9BA48BC4A73F42D68BB785E48D0 + 4B104DEB60B512CE9465D80FAD8D17A5C5C8920C2CCAB1885F9C1A298BB0205F + 8185FCC6B22CD95A9DAFF173B9E827F8A55BE855F9293826825D425E1E53A66C + 28CF5157CF5B6A1937B750D656E4C9FA8A7C59579683F349E85A2CEB2A0A705E + F365734D896C6DA890ED4D55B2A9BA443654168A4F73B5ECEA6E966DA8A7D5F3 + DA12C6B52A10EF46BE0D5F261BEB8A65636D916C45DEAFA34E02BAEB1504F3A1 + 885A03D7336E56916C449EB18037D614C8BACA5C4F7CE0CDD8DEAFAD4AB6B714 + CB863A5A1467C986DA1CD958CF8738D84F5321C62E946D2DA518976E9F2B657B + 6BB9426002E06DED95E28DF2D63623BA83F3EDAE93C043ADB2FB7887041C6915 + DF8106F1EBAB57F161934F7715542D3BFA6AC4AFB746B67756CA96567C5EBA88 + 6BCAD7F861DBDB4AC4BBB940B634E68A777D966C6FCC54E84B777E84C03E1001 + 2F41EF8ED63CD9DE9C293BDA7235D6DB0ED4F975A2BE3D4FB6613B1FC6EE45DF + 80EE22955F47BEEC407B4057A104F597C9DEC10A091AA8907D43D51272B44E42 + 4F3449F8C966A48D72F07883841E6F44B949224E374B18EA428ED4C98163E877 + B2410E9EAC836A25FC7483449D6B95E80B6D263DDF26D117DB25E242AB849EC5 + 58675A24E21CCAE7DB25FA5297245CEB97B86B7D124B97919AD222B617EA9118 + C76236F25207D429D157BAA11E47CC53E88B31122D3CBD7158E2AF0F49D22DBA + 3B3E82BA2149BE7D1C3A85F229B41F477A5A52EF9C93945B6724194AB97D46D2 + 3E3C079D57B12EE1FA4949BC714ACB191F5DD0BE4928A7A22F5D79A6DC39AEFB + 4B722C76296BBD9B4CEB61B413C81A807B521FBCD3FA956E93197F37F5230369 + 694D1B4FE07B7D109F63C00377290B772DE0659970D8B673BB04ECD36D35ACC7 + E1E4EDFE3575B6679E966084CA6C634A2B5E42DD54C26CA7CCD48AD096A097F9 + 646C43A5E233A5731CEDCBED8DE52FE1AD85B874DB9C4EF0F009CE1952030FCD + 764C3350661FC25E6E6365A0B051D617B4CE25D430D2FC1797743B5AD4B12EEB + 8BCB9E7DD1527824E4556082BC05A2B66CDB098698D2E28E2E5969255BC07E5F + 5F165ADEDD656587F66CC75A38FFBBABC61216D2F8BC682798D163A0951EA420 + 98DB710CD505852C39F89CD938A799B420FFD4C45EB600D80D81D5029816BBDF + 9A63E6E7E2F80A9ABEC1585F193196B28229022975C18CCFA7568974C78DE350 + 9D97BC2FCE4801F653FA1D63E49E97A26FCE49C13767D1FF9CD0857221F697FF + F5594767F0D94F4B0EBE7F5ABAD38D2CE38812FC5BCB428E6F2D031500633F74 + 5FABD6C00E2C331A86BF16FCD20AD44ACB3F1848C894C08B72C33C85778E85A6 + 856FCC13FC12023355D7D03F5E46FE8AD695FE48D7D3371470AA55AB5AF81AD8 + 49112E5AC858FE87E13AB72C38A515AF716B6CE2D65AB9B7B1FBB12901ABBA5F + 2694FDD9057369C18BB64AF4639930972E9A2D18B6B260D8E699BAE130F75185 + 316AFE64A02EDBEC718DDC8662991099FBB7C7698F49C74359853A63C18BCF86 + B205BF23E12F5D3DB39F951B06B36C5C8A0F5BE552F6FF66BF5B8AF5F6BBA7DC + FD2977BD95DDDE6EC73129FBDBD1FFB4F33F675FFE67A9E1EDB04FC257FCFEF8 + DBA48C35B0F93DBA7FA3B6DEBCBC80FF98BB9DE3D936DDBF01BC7C6183EEE209 + CEAD0BF702BE08813CDBF9C20AAF67ECAFC70259184CF85BF88D330E5FDC8072 + 700DC8C1FF8A2963816BCA1735B04F5EF3EEBA8EE27A68A1AE15EF416AE1EB5C + CF28E3461AD7605E87794D453F8263C25FDEE7D8DF40605A04F3451BF3BFE77F + DD5A05E7E0DA604314F0A5287D310A659BDA3CC310D8B006F49A61FBF265AA14 + DCB3088653714F33509831F08F48EEA78C757F54D2694D8CFB1C5D47C75DE890 + B8F31DEA3E390BFDB2EF0C49F6ED41B5144EC67C21F55AB764DCE8934C471937 + FB24FD66AF64DEEA870624E96A977A0BE18B669C9F449E6B91988B1D92883907 + 5D57C75F66BB895F1C77B15B622F74A20EFBC27929C43D22FDD671893AD38579 + 10E630E7BA24F24CBB1C3C8639D0B146893AD922919833D113CB81C3D5128A79 + 54E4C946893ED528B1981B31AE31E31CD34573E48966ED1B75AA45E2CFB64B22 + F69380B951EC99568D311C7FBA555D34F385BBD0C172B5DA3D38502A6143A512 + 79B45C228F94A13E5F42070A353E70C2E9266C672C8F9331FFCAC2E729B87D48 + 0A6E0D4A21CE4336E65109C76B2592EE9F078A25C211F351832512D95724E13D + 0512D19D27E19D3912DA9527F187CA25F36C83A49CAA41BF22D9DF96217B5A32 + E44077BE440D9549FCB16A8C592D31472A249CEEB7198BB9BB5042BA0A646F7B + 8EEC6ECD92C0D66CD9D59E2BC1BD25124E304E6BE49E12D9D35D2C415D4C4B25 + B8BF4A0E0CD6CA9EDE0AF16FC5DCB4A548767531BE70BD0463DEBCAFB75EF6F4 + 3448705F23FA622E3AD482F3D92BD167FAD46A78FFB10ECC3187301F3C266117 + 0625F8DC80445E3929395FF3E5914F314F3B27FB8EF64AD06087841EEB939833 + 431276AC5BF61E6EC11CBD5E760ED4A2AD51F61C6AC05CB84EFC3BCBB1FE302E + AD033B2B309F36DADD532D7B06EA201C172D810F35CAAE5EAC37BA6A14BE6E6D + 29D1B8C75BB1DD46869AA9C3BAAE11EB993602E426D97FA44B8218E2A5B751C3 + BC041EEA901D036DE2D3D7223E03ADE287BC774F936CED6F95CDDDB5B20EE3AD + 6BC29AA4196B1C825EAC2B36632CBE24BA9DA0B8B942B638DAC61752E93908F5 + 9B515E83358E17D63A9B5BD086FE5BBBEAC49B2EABD1BE0D29EBD7A27D2DD644 + 5EB504C658FB95A4CA9AB2740D89E355CAF52543042560FD1983356B8CAC5417 + D074FF1C27CBF313B1CEA44BE70C59C5F55F11B62D4C97555C0716F24562A6A8 + 87087A1765C6603D19A379B50046BA1463AD2C48C27EE8BD2A594131A12F539B + 2710D697899D18C21C774956BCB1008616A4C740760D8B357376927C8075ED02 + E45797E6CAAAE26C998B35EE0729D158AF33F630BD5399F537D7D60B53E3644E + 7CA4CC8EC3780931323F8110385A664546CA0791113227D6C4187E3FEAA0CC41 + 1FEBB16B7E329F2944EB4BD4EAD10BA2F72ED6CD4D322F7FD3BA98A2F72FC260 + 5A03CF888A50104C97D0945A05D31AD8891F4C6B60BA889EC598C3E111F20E5D + 43EFDB2F6F07D332F880A6D3F6184BE03777ED95B776EF933776221F602C7FA7 + EE0854F7D02ABF40AD7B937DA157FD08710D14260436B098F57B84F0F745BA77 + A6BBE76DD0561323D85AFEBEE6BB07DA8BF26E85BF2F6EF6178D17BC05637AEF + 9657986E41DB662A5061EE8B9B90B28CF697B7EE9617D17FF2FA1D3269AD8F4C + 5EE7A7E0780ACA36FF22B67B99722030A1F1F3EBFD658A978F3CB77AAB4C5AB9 + 4501F0C4E59B3C72C3600B82691DFCF4621317982208A69E59605202E027E779 + C9E373182F78AD02E0C73E58A57A94EEA051A6CB680B833D16C14E4A3D327D89 + C70A9816C084C016FE2AF87D7B8E01C3682300B65098F18129F6B1EEA1550EF0 + F5C8D653AF39D6BFD0830ED825E4B510D82DD6D132D893F2D9A3F3EC52BD1A2A + 043696C0F659A795A9636A60F0040704D3832261B002603E47759EAB0E3F731D + 76093DE629C7CD339FD1D2D2D7026005BFCFCBB8C78DF8FC97F077587C3E6C9E + 1BB37D02C6548D00C0A3015D2BADE7336DA78DA9DBF2D7D68F84BEF639B895AD + B7CFC8DDCFCC7FABDCE369F981612F9DEEE7FE773DCFB779B653A85337D1DC86 + C7C4F1903756C5ECFFC4DD1A09776DD95DEFCEBB34927FFC56B9198A47FF80B1 + A07B8C91631ACEF2CB7D5806C3FC5DBCEA2EB9EAC8BA5C00D8CDBDEEE2604EDD + 6896BCF7D4886DEFAA73F7FB8FEA3F63CC7F4074C13D12E25A10CCE3723345E6 + D5525BADB59DF37FDF43A6AF936A7E3440FB9FAD5F03C063C6F3E07992FF9F02 + 80EDB9309F55BF38E7C73F52EEB730EEADBBFFA87F4FF60F3FDA9FFC1FB9888C + 947BDC7F54EE8B0B65CB6E0DF73717F1BF275EF0C739177EF7C5FFD764FB5A8D + 84BDEE9B86ADF3D45B8DB8298D94FB0675F78DF26EE8CBD40D803DC01765779D + 5A00A39F47DC966DCE4DDF02605B4F286C260326D5BC037C75B2C049C4281058 + EB1D004CB7251EC8FBD4B035F04800ACEDE86B6521B075017D570C604C84AC95 + 2FC5F2EFEC24CA81BE4CAD14FC522EF8FB08265F0FD90918645D4113005BF7CF + 1401F083AFA14CD7CF0E04B6AE9F295A02F3CD425A0053D6DAD7026096098009 + 7F2982DFC768153C7D813C4EB7CF16FECE5EAAB17D9F9DBF4A262D5C2DCF11F0 + 12F662A2FC1C26CCD6EAD70D7F3DEE9D1D2B5F5AFF5A00CC3AA62FAEF6D6BA29 + ABB70863FE32FE2F63FD32E6EF0B8CF9BBD90F0B082C46E87E088B0B5A033316 + 0DAD7DDFDE8DC54CE07E0FFC7D1B7A6FCF4199BE3F1C8B9D7079372C4A66C526 + C8DC84549915938C72B4BC752052DE09679CDE542CBEB2644E629ACC8C4DC4E2 + 2A5EA687472B009E1E1E25EF4746CBF4082EB8A264763C176189263E6F5292AC + CCCB977525A5B23C275716A6A563F19821F3986632166C91AC282850006C9423 + 4BF3736559419EAC282AC002B950D69417CAB2C22C59949D2A4BF3E876395316 + E51006239F9785056E3616C6F9AA95F9395A5E9A95A66F0973A148B0BB28030B + 56BAB6CA4B97F5A5792A5A032F4C8B57691F2C6A09680980D716656B9C5D4262 + 96D9BE342B096363410BA9F56F01F68F3A2E6C57E761714E6B5E426388B05863 + F45616C8FA722CF8D1A68B6142628C4737594B0BE8263A4BD670E15C82E32ECE + 94A5D909C6E2372741D6E424C9AA9C44F12A4C956DD5F9B2A3A144B6D514280C + 5E5F920965E1BC66C8C672EC0B5A574AD7CF19D8678E6A4365AEACAF2E102F1C + 835705CF659EAC2CCD96755545A82F96D528AFAB2A16BFF67AD9D9DD229BEB2B + 6465098EA53447D656E4E39C67E0FB48518BE655D8D74A684D39DD3717C8E6DA + 5275CBBCBDA55A7C5AAB654B4319C6CA5717D11B6A0B3C30D8AB2A57D656E560 + 5FC6527A5D65BE7837948B7F67ADF875968B774BA16C6AC8958DF539B2AE2653 + 36D4656B9EDAA06FF517AA5BB52D74BFD64AE05C29DBDB0DF4F56E2F956D9D38 + 86AE0A8D1116748C71CA5A24F048B3C60D0B3CD480B441828E3422AD97807EC6 + 16AB16BF9E0AD9DA8E319BF3C4BB39577C3B8A25A0A75C763086583B631CE78B + 4F4BAE825FC2DE0002DFB63C15E3BAEDEE614CE01205BF5410E3B2F59648601F + 5288C077CF6085041FA991E0A33572E044831C38D92421A79A249830177996A9 + B0B36DAAD033AD1206698A72C4F90E89BCD0A90A3FD7EE6927CC0D3F87FE50F8 + A50E89BADCED9181B5BD1279B957422F74C981331D12761EF5570724E6DA2189 + BD4E6BD7C39A8FBE3AA8E5982B7D28F749E4956E89B8DCA529CB71D7FB25E66A + 3FC643BB93465F1E1E878ABF794412087FEF10569E94A45BC7A0E30A75936E9E + F5805D96936F9DD33AA629B7CF4BEA9D8B92F6E125AD8BBF760AFD1813F102DA + CE49DCD513A83B21E91F5D908C4FD01FE373DC14ECC3E4B99F639AE783733E44 + 4FA40BE9DB74D14C608C7EC813BA12C42A287680AC05B784BA16E6DA7685DA7C + 283EA2AFAD270426FC665D0AFA1008B32E0979F65511FE222518664AF84A98CB + 3E2CAB2B690B661DE86BCB1600531600731C6ECB32B74DBA4328CC3696F1393F + C2F9459A8AF340F8ABF17B3FE7793B638033440B63BA8C26B420F0259CC85097 + A4065E70BB149C370B8855C85BF7A7195F5C94D44F2F48D657D724EFFBDB92F5 + F53549FBEC3CF67B46D239C69797D43296D6BAB48EA5B52CAD6609542C9CB196 + C104280443D9DF5C52B0924F78FAB575157D01655AC5621B281BFD085EB2B10D + 95897E59ACFBEEAA8AFBC9F8027538966C1EAFC25E63214BCB3A02135AD05139 + 5FE0B37D7E1A9FF7A4C25502601B7BD95DA63B68B52EFC0AE7E073D6D135B4B1 + DAB5EE6835562FF2054CD12FFF2BEE9B567A2714BA304FF89BF7F929C9FDEC84 + 147E794A8AA0DCCF8E2B6429407DD15767A488E0F78B535280B6C2AF4E43D886 + C782FDF298396E1EDD42633C85C210E383B28E6D8538CE82AFCF9BF4DB611102 + 5B606BE3AC32EF01C468A32CF85340E70036023B0BF4ECE7E436C36331BDEA91 + 01C22655CB4D8CE38923CBF17FB8E181BE562CFF5A9D82D63F7DA45093C767A1 + 30EB2996B5CFCF26462EB7B1109660D75AEC12B212FC2AEC75CA6A11EC9285B1 + 84B504B514C1F04898CB3E36B5795A22D37A57C12ECADCC66E47711C5A102B80 + E6F8CEB1B1CC312C00E6F606EA1AD8EB06C046A69D40DC0D81DDF0D79679BEEC + F7CAF3C9EF83DF29CBF6BBD636F4F5F473B6B3ED6ED86BEB99673FBB7FDBCFF3 + 7B21F0FD9AFF67F37FB72F7EF07FAD2F70A09E0098AEDBF9F207812821AE425D + 8F30E6B7C61297A28B77F3A2085F642038C6B8CE7684C4947A04404A0B5FBDC6 + B00CA9BB76B6E3F3B30FE12F8F47C1347FDB6833D723E73AC5FDF1DAF43DAE2B + B8E664E1FA4431AF3098E2B5079FC702E061EF09C340D8BC8C63DCF35B77FDF6 + 0519D307D7275C9F79CDE2BD8BE2B59AF7300B816D1DC56B30638CD39301AF69 + 197C01E8430372D33FE23D12F70CD4592B61E66D99D7412B0B90B3706FD1D000 + FA6210EE4BBCBFE03E968AFB4A3AEF4DBC6739209990585D4E634EC118C2E937 + 0725FD96512ABD7D609EC0FACCDB836A3D9C7193B1867B258DF0F7CEA02AE3F6 + 21940724859E39B05DCA2DC6DD1F820E3B1AD2FDA80BEBDB47D11FFBC6BD8DD7 + F35CDEC770AF4ABA86FB2EE62B368DBFDC27B1177B24FE12A1718F42E368CC87 + A2CEB64A2CD2388AF933CD1275BA4922F872DD71CCB58E356A1A893962F4C956 + 898122517770A846422186DB08651CDF9E22D9CD38C65D0512D25F240707A1FE + 02D9DF9D8DBA5C09E92D46B94CC2D037F27095C41CAD9584E3F59280795EE2C9 + 46493DDD2819675B24F118C6C51807B14D5877BEA60714F6E648704BA6043524 + CBFEA61409EBC890F0AE6C89407D646F8E44615F917D0512D6932BA1DD3912DE + 9B27D1874A24F178B5241CAB92081E13C1715FA12AB8235B829AD3645753AAEC + 8476B765E238F32474A044AD8D0FF415C9BECE7CD9D596A36146F6741649704F + 99047514895F638EF860AEEDDF8C796D4799ECE9AE907D3D35B2AFB70E9FB34E + 82BBAA5511833887875B652FEA76F5E2739DEE94984B87E4E0E91E3970BA1BF3 + A313B817DD90E26FEEE07B3A2521473A25B0AB4EF6F6E09C1FE9C2396EC79CB8 + 517613FE0E60ECA166394037D487301F1E6894FD98BFEFEDAB950398B3871D66 + B906DF43950475574960478504E19882B0DFC0CE4ACCC92B3127AF524F413BBB + 6A54DB9AE865A8447CDBABB1966954CF407B0E754A10A16F6F93C620F6459DEF + 409B6C1FEC109F23DDB2E368AF6C21041EEA902DFD4DB2099F734BB701B7DB7B + 1B647B4FBD6CC17EB6A36E476FA3C25E5F8CE387CFB49D56C3AD55B2A9B94236 + 34D1E57339D232D9D45A213ED86EC7A136F1E96F96ADE8E78BFE7ED86E23FAAD + AF2F9675145F8AC57A6903D6671B9132348F02D7FC14AC51B99E34617F5665C7 + CB728856BFB4C8E50BBEF4E8B41629212DFBAFE4360558773A29B514EBC7C599 + 741B9DA856C1D6CA775561B2039A69D91BABD097A2C5B081C131E8673C4A2DA7 + A531C65C863CC1EFDC944859843197A07D29D6D24BB0565D989920F3B0B65E98 + 938C7579BACB0238560130D7C97C219A6B64DD06EBE8B98991F2415CB8CC8D8F + 94B9483F880E935911613233FCA0CC8A0E9559B1A1F27EF401999B10A55EBB96 + A427CABC245AFB46AA68454CF83B27394A662745A99BE9F9A93896347AD94A90 + 398931F241BCB10C9E4DEBE218C6058E12BA85A645F03BA107E4AD03C11A3FF8 + DDD083321DFB9E151921332322E53D5A07871C50BD7BE0A0EAEDFDC1F26610E1 + AFD1DBBBF6CA3BBBF7C934C73D34A1EF1B3B8354D3D0461100D32534AD83AD85 + 306303BF863C9FC5D00BDB2BDB0315104FF5091CD676038019A66B2A81F056F4 + 71AC82A76E67CCE03D2AC6FE7D157A05A2FB67EAA52D81F222D2170986B7EE52 + 8B60C25E02E0E7BC7C1512D322987504C1144130EB6CD9B4F9C9F35EDB65F29A + 6D3269B5B73CBB6A8B491D184CF8CBF8C04FD32D34F204C0EA229A40986EA117 + A3BCC800E1A71779C9D304C10BD61B003C6F9D3CB310DB3B40989AB808DB2C5C + AFAEA23D31841DD7D01606D32AF891198B1402537CE64611FA12FE3EF4D66C03 + 859D3A5B3696C206085B00FCD09B1F18D0EB92C60C463DD3070880A74E5777D0 + 16105BF7D0160C53B410B6E0D7C2E0FB9D678F6E1108EBF34AE7F925DD44EB73 + CE496FCA830A811D2398675F97F1135FF340607D9E0A69283EC87A63649D7DE6 + 7AD7F3569651AFEEA39F30863E6ECF8F631E7D5165CA7C0E6C3C478E7FE20579 + E0E997E57E489F097B9E31FF12008FB4081E59CFB23ED776AC82B59F0378ADDC + B096B2F5F619B9E79939C1ABCDBBE5AE77F2762C5BEF7E467FD7737A3EC3779E + E30FCB79AE4FD9FECE180622A3CF281057351AEC75D7DD43C34CE3B7C9721037 + 77F1E83FC86E46AB1F294FBFBBA0EF08B9B89707A0BA355A1BCA1E003CB29FED + EBCE8FD4C83EFFA8DCE38CACFB8F8CFB1FD46800981A37CEC513D9E7FEC73546 + B3825FA79EEDEC67B7193FFE3119CFDFC86880F63F5BC3D0131A0180EFD67FDF + C9FECF9407E86A199FF39F0A80A95FFE59FFAB652F52FF1EFD12F20ED78DD636 + 1ABC756B24C8A546EB375223B719FD46E1D423BDEBA662E5DC7CEE257B93F2DC + AC3C00D8B951BA6401B0951B0ADB1BF4BD00B0C7FA97ED4E9DAD1F290B7B097E + 273C6D5D8618590BE061008CFC339888A86B12E3AE642400F6B47102330A0076 + 4360EA41BE01C749112741569C1C59D04B572A48474A27544E9EEE9F2D00765B + 00530FBEFCAE070033EE2F45E87BAFF8BF4CAD1B688AD6C046C61258279428AB + D5EFCCC52A825F8AF9BBE0EF8255F2DCA2350A809F5DBC46262F5B2753966D54 + 006CDD3D13FE4EC1A47932DDE92CC1E478D15A85BDB4F225002608669E751606 + BF4408BCC61B93F2CD0A81097F5FDF168845C14EB5F89D86C5C534FFBDF2BA2F + 1612CEA243E3FC72E141B7CF84BFBB431400D30278467038163E91F26E78B4BC + 1F138F855392CC8A3190774654BC5AFE12FE2E4CCB56104CD19DF3ACD87885BE + 84BF33A368398C45570C1666F1090A7F3F482408A6556B9A2CCBCA961539B958 + CCE660E1998D8563B62CC8C8500BE0E5F9F90A8257979628F45DAA16C1D9B2AC + 201775850A2517E6A4CAFC8C44B500260426FC5D0211F612FAAE2E2464CD55B1 + BC243315FB8E312EA792A3D5FAD6B872CE444A975458D842B412B610987D16A5 + 1B00EC559C23EB08657309BE63750CC25E42DCA558EC2ECBC3C21B8B5B42652E + 6E097F194F57D3421B53374B21312D803794E78B5719DF90CE9295C558EC57E4 + CB6AC2D0C60AF169AF950D8D65B2AEA6087DB2651D16E89B906E66AC5E6E5B9A + A5D07773458E6CA9CC453953CBDE55D8BE3A5F3656609B5263056C2C81F9A020 + 1D69A67811CA62DCB595181B5A5355285ED5B4E02D536DAA2B179FD63AF16DAB + D7F625F974659D292B4AB2657929CE2D8E610DC65883635D5E9429CB8AD2653D + C6A0D5300130C1AF7763B9304EAF4F6B95C6E9E55BF814C1EDE68642D9505768 + 2C83EB8AC4B7B55276F534A8DBB9809E2AD94178DB5D21BE1DA5B2B9314F0130 + 538A16C19B1BF36553439E5A066F6B2D566D6929922D4837B796A865EF8EBE5A + 093CD4287B09808F3619E87BB859F630DE19CAC1275A65DFB14694EB65D7A11A + D9395065D4572EBBFACB65CF50B5EC1DAA955DACEBAD90C03E1C635F8582DDBD + 6CEF2B93DD7DA5AA3D0315B27FA846452BDEDDCEF6D4BEC304BE7572E044A344 + 9E638CDC4E89BDD82531977A24FA729F44200DBBD02DA1E7BB541117096E7B25 + 12F5CC47A0AF51B7D611E4EA36E73B55E117D08672F8C51E39C87190465E1EEE + 17897D445EEE97C82BFD127AA94F82CF76C941D65F1B9488AB039A465E1D94F0 + 2BC85F1F94A86B871CA0CBB45FA2097A3D72C6E2F1398AC27651EC7BF3B02AF6 + D61155DCADA346379087126E9C90F8EBC725114ABA75522D82A9C49B2725FE1A + 741D75B70883CF4AECB563AA44F44BFBE82CFA9F92982BB42E3E2E599F5D94F4 + 8FCFE9B6A97423FD311F9473AC132A825E42DFC49B471508B32E892EA5B5DD01 + C0B73136DA29ADBBC37DE1D8AF0D28D4B580D79D12005BE918480961554E3BEB + 09672DA075E76D99A91B08BBEB14E8A29E32D6CA27153A33B56EA853097AF9F9 + E8DAD3338EFD1CE8F3E1496C7F1263B23F5D8E1228101C10223896BF9F9D1613 + 039896C104CBA73016CFB5B15023E825A820B0B0B0C2C20CC2074ADD96B2FCD5 + 6529FAE1A614FE70435D20136AD042D658CA1AD15A9556B00487795F5F947C82 + 5DF4A1AB62C6ACA5C52A2165BEC24AC254BA3A3596B8AC2FFA017DBE2338C258 + 7489FCB581270A93E826D9B1CACBD17D9B63B5D67474A54A304237E14C0941AC + F59C02125AC37D72027902125A4513B8F31CA1FCB9B1BAB596C104C1364FF7D0 + 541E8E3507E75145288B31E8923587637C7214FB3F22591F1F55657F7C4CADE9 + D26E11820C48F687436A11472BBADC0F8F4A01B62BC4FEF23F3B25855F9E96A2 + AF0881098369357C16F97352F4F57929409A8B7E799F9F94BC2F4E092D856941 + EC16AD898BBEC7B9834A7E7F59D412D781B384BF26B6EA55CFE7285457D784C0 + 5715ECD11A92A05663BF3A209822F8552B600722D34DB3B1F4BD1B005304C416 + 009BB8AAC3969C160213D6BA2D786D9900D4E62DD825ECB44052E12EEA586FC0 + ABE9A3FD9C6DD53534CAEED8BE16D65A084CF8AA009680977097E338D27EDCCE + 81BF366F65C12EB753788B6DB85F0B9FDDC764C53A8ADB1104B7FE8F6FA4ED7F + 7E2B8DFFFAA5B6EB67E679C0F6B45C75CB0D7FADEE057A29379CD5B203D3EDF7 + 69CB4CED7761F3B6CD9E6F6E43D936F31B2144BEE1D9BFA933FB674A10CC98D8 + 04B58504B9D82781ACBD2EA835B0F39B20C0B556C06AC9ABDB19A0CB3A425E8A + D71EFEB7D50AF64B5C031C6B5AFBBB2AFCCE405BC25EBE0CE289CF8D63B76EDB + 0D8C461F05C004B874696F40ACB50A566F02104170213E3F21B0B5FAE58B2A94 + 05BFFC2C6EE0EB2E53F6F3B2CD5C570981597F11D71902E273BA5FD659CF0B7C + 8987D7705E9779EDA695B04DF9B9F959F9B909840D34E6B5DE580D5B0DB7993C + AFED3675BFCCC31764D4C2D839AF6918CBBE5095887B7712EE2FBC27A5E2FA69 + 00B381C18C33ACEEA31937F8F661CD5B97D219B83F65DE392C1984BB370754A9 + 04BEEA8E9A70F988679C548EE3DCE7F4FEE4B8B626BCCE24B8C6F1F0B829DECF + 52799FC7B1A523CDC0394A439A4A480C65DCC138B817A6610EC2E34AA4771205 + D67D9288795122C35340B18C217CBE5BE2CFA1EE42AF245D842E1925A22DFE6C + 87C49D6D93D8D3AD128DB95BF8B15A093D5225E187AB24922E9B69713B542E61 + 43C57270101AAAD058C88C891C7D94AA9508F40DEB2F93030CD5D1432BDF0A55 + 08437BD022B72D47F6B664C99EE64CD987741FD2A0C654D9DF9C2207DBD324A4 + 2D5D825B52519F2C21ED1912D55F280947CA25E128C7299288FE02893F5A2989 + 276A24EA50A9825F5A1487F616C86E82DFFA24D9DD9C267B6835DC910DD16D74 + BE84F6174B704F8102E15DD8AF7F43BA043667239F233B1A32C4AF315DFC1BB3 + 91CF12BF7AB43731AE70B104626E1DD486F92ED23DCD85B2BFA31CE3554900E6 + DE5B3147F76F2BC1DCB84EAD780F1C6F95842BF85D3094C735CC532E61FE736E + 402D87F77456497057AD04F7D64BF060A3EC43FF7D8375127C88EEA9EB5484BE + 21744FDD57856DEA24E6448B441D69941094833A2B64575BB9F8B794883F8E65 + 474BB1A6BBDA31F7C6D8413D186BA049537A15DADDD7847D74226DC51CBD4B82 + 8FF548E040AB6CEFAE17EFAE7AD9AA16BF2DE277B457769E18906D87BB64D360 + 9B6CC37AC4A7BF497C217FF4DF75A443E5DBDBA8EEA20991193226E4643FD613 + 7D0A80BDEA8A656D6D91825D425E9FEE06AC49B006E2CBADFDCDD85F9D96BDDB + 6B645B47AD02E375F525BACDBADA12ACC3B016C35A8A2FD4D24BD31AACB95617 + D2BB14D66FC519583FA6C9AA3C86F949431BD78D8CB79B81356486AC469EDE99 + E8858A5EA956D39314FAAD2CA0D56D32D6A849B22C970037D503715715A6CAFA + F21C85C706104743316AF5BB9056C0C8534B73129D3112315EAA8AB057E12F5F + 50261C8608979761ECE538A645B9291A077811D7C6388E85D989B22033C178D2 + C29A99EBEA85E9183F235616A4F025F170F9202E4CE6C44231613237265CE644 + 85CA6CD4CD8E0F53084C48BC006B72AEBD3F40FF99A8A3662744C807D0FB8918 + 2331D200E0B438599096A820786E729CCC4E8CD194D6C084C016043356F08CC8 + 30792FFCA04C775C4CCF880C97F7C20ECA8C3013BE6A4658B84704C26F335670 + D05E7963372D7B0979F7C83B7BF6CBDB41FB3C5098B26DEFEE0B91771943D801 + C48C1B4C0B61E6F9FC85CF5EDE093A20AF070469DB54DF00E14BFAD4CBDB03E4 + 256FBEB81F202FA8D52FDA09847D087F83D4B29716C053B7605CE8155AFB7AD3 + 721829AD7EB704CAF39B028C361AD83BC9CB4726ADF395299BFC65CA861D0A83 + 09859F5DBB5D9E5BEFE781C493D6FAA2CD479EDFE0272FAEF79517909FE2C598 + C15B65F2DA6D32698DB73CBB728B5A01BB01B085C0D622D8535610EC05AD53C8 + FBD86C02E0F5F2FC8A6D327999B73C31D7B8887E7EE5567971E5367976D1FA51 + 01F048084C77D08FF1F91B0D34A61B97D00FBE355B7EF7F61C7D3647204C1106 + 3FEA40601A6FD00A9890973098A9D56800D8025F8F8530645D435B10AC2EA25F + 3210584130AD845FA665B0037EDDCF2151A625309F6F12FEDA78C0D61A7802CA + 9496271937D1E39E7E556304D31298B220D8C2E0097CDECABC5347304CF83BDE + F1EE4859083CEEF19790120013FCFE1200DFFFD44BAAF1340C621DDA6C0C6037 + F41D098047ABB71098A93EDB76C15F6AE473F091F5A33D3BBF4BA380E15F6CEB + 3CA3F7005DFB8CDEFD4CDFE6399EADB3FAFFB3F7D7FF791CD9DA373AA1C94C98 + 9999133B8EED9899D9922D962CCB962CC9922C666666065B3233333B0E334E66 + 66EFE7BCE7BCFFC775AE6BF5DDD26D454E32B3F7CC7E9EF79C1FAE4F55575557 + 57F70D5DD5DF5E6B699FC172B51B82B7BFAA7F11007635127FF977CAB5001EB4 + 02362B5F2F003CC8FBBC20EA4820D5AB7CD06A7558F960DE5B6EBBFF2E79F739 + FC58AE7EABFE1F91DBD76FE89700D8B9CE37F07ADBF5D735BBF51EDC7CDB7D06 + 805D307CB3CAF8BD959BEE9BA95B6EB9DB91BE772301DA7FB5FE70E370D07B3D + FDFE8BF37F920681AE77B9E78BE0D6B9F5D76CFF6EFDF247FAEFD6487F54BF47 + 2303DE5F02E06BDB28FFEB7221EE4875BF47068025E60753EFBCFA1E7E6391BC + 6E4823C9FB0665792F00ECADA11BEB10F875DD435FE326DA0378BDE5C2DE5F83 + BED2A005B007F60AF2DE723F2700AEDB672FF86B521C8A0739C110CCD5646304 + 09FCEAEDB53F3EE4B83371DD3F7B5BFF7AC35F27EF4C7ABC2D803549D264C9E2 + 69C88533758D6B67815FD50B027B5901CB05F4AD6C770B2760AEFEF814DB3F33 + 64057CDB0B8EDB674916C002C0C325002C2B6027C6C87B16F7F75E4E3465096C + 6F130A0EEB0D444E401D37CF532CFEAFA5631C3D386EBAC1DFC7A6CEC3A393E7 + E2D1A973F1C4F4059CFCCE3710FCF0245903CFC22393E758B9A4B62A57D92353 + E4069A6D26CDE1BE8A032C28AC18C09A402FC4D39C983F3977B9B9819605B0E0 + EFCB2B83F0BAFF1ABC1DBA1EAF07841B007E9E7AD11393466EA065FD6BF0D763 + 0D2C97D0AF8744E0D535EB9D58BEEBB5488A755C414773211597887189A99898 + 9A89F793D36DFBBD4D490675C7246CE2A22AD6F4768CDC407361B661034631FF + 66743446C5C915531C17731BD9360193D3330C024B93B3E4163903E3D3D33031 + 2B0BD30BF231A350B17EF330313B13133233CC1A786A01DBE66718FC7D970BBD + F7E40E2A3D910BD6642E2653D84FFAA0E40E5AE994EC0CF69D6C0BC0F16C3F5E + 96BDE9F198919FC6C56F3617B28EBBA889E93C8F342736AFDE287600B06329EC + BA7C160C56995C412F28CBB345B716B53358B7A4B6144BAB4B0C14CF2FCAC22C + 590B672663767E261696E61B449EC9B2B925B9585C5D6CEE96B5D89F5B5588A5 + ADCEDBE46BF76CC6FA7D5BB194F9D9B545985B598085150558CC36721FBDD0DE + 0E97EB698E879A6730381B8B2B73B1AC96FDD4E41B009664F53BBB28D571FBC5 + 05FECCE254733936AFBE0C0BEA2B78CC5AACE868C072A6CBDBEAB08CE9A2C64A + CCAF2BC33C9ECBECCA42D3BC1A8E839A5F578225CDB2B8AD868FDC9DB5969B75 + AF60EE825A8E93C79F5FC3B136145B9CDEA0CDCDF0EFA9B758BDB2D6F5EDA8C4 + 8AD63283C08B6AF3B1B2A50C41BDF5660DB0766B2B0237D7C2BFB79AAA42405F + 357C3BCBB0AC85E7D45C6052DEA7A3D4A0AFDC33BB79016059F02EA1567495C3 + 4FFDF4D7C17FA01A01033596066DAB47C8F67A84EDAC378BDBB01D3508D95A45 + 55225496B93B6B11BEB30E6B77D561DD9E46ACDBDDC0367508DD5ECBD4B1D88D + 64B92437CC51FB9A114945B03C9C7DAA5DC8B61AEB4BEDD7EC6CE23ECD58B7AF + 1591073A1073A4071B653D7BB4C7AC67375082B21147FAB0EE689F415801D818 + 59E19EDC8A981303883CCAEF82EA0FB32DB55EDB6C1B71A8076B0F7663DD21B7 + 8F5E841FEE43F891CD88609BB54CA508E9583FD651AA5BC33ED61EDD62DBAA5F + 777CC052EDBBEE783FD6737BC389ADD48081DDE853CC733C1ADB3A1D9BFB451E + 57EA28F2F816AB57BB98D3DBB0816994F63FB9CDD228D6C79E92AB64C1E07E6C + 3CA118BA5B87C4F68E6481CB36DC4F96C42A334BE2B33B2D2FEB6459D69AC5EB + 995D068305926505EC2A81FB4BF1EC63D3693D5876C070FC49F62BA0AC8791E7 + 3C00D503804DDC47F057805D2EA0BD81AE0B630560552EA94CE0D5C47606833D + 30574AE076C2399E0FC7AB733637D12AF7A46E7B6D0F9679FA130C567D0AF306 + 7CBD640FE13DA93BBED48BCEB6CE4BF0DB3D67B38EF66C3B10D803022EEF3100 + 2C97D2B23096A5713AF372252C509B2138CC6DD54BD9725BECC6A2FD8079C111 + 410359D10AC01A44380201C3CA1F2EA2F6E7CB101CCCFA40A0826D3E11ECDD6F + 56A9C59F2B0EEF21039972555CFCD91194508572BBFCD15E6A37F23FD987A2CF + 1D8829D7C826E6055DCD3A5716B99FB06FDBDE8FAC8F58F6D17E03C38E45B3DC + 50B34EE7C8CF2CE92CAF313FD37881007E0F0581650D2790210BB754C5613E2F + 50EBB844CDB8BCCB2C825DAB60415FC725B4C6206B608E9369FEC702B2DCFE80 + 6D05782F6C47063FF3D4D35B907EAE9FDB5B99327F66B3415E57B2884B3ED689 + 144AAE51E51655EE53B3CFF723FFD276147CB013F95777A2E8E33D9420B0DC9E + CA6298A901E1BDC8FF70AF59091B00A65C00ACEB2409FE167DAE78CC8A617C18 + AE4BE6A22F046F2559027B6D335FFCF57193A09E805FF1170E0076819F0B820D + 020AFE7AC5EE75DD3FBBF0B7FCDB538300D862FCBA1092FDC85D6F11657DB31F + 6F0928BB7DBA92CB6857E5DF0DC5BCADF8FE02AA7F1CB2AC7500AB62ED0AC27A + E0AB07B62A3508CCFA1AE66B389E5AB66FF8F1321A7FBACCF4126A7F10EC553B + 815FC1E573B6ADB8C415CC4BCA57FFC83E7EE2F7FC2FDC87AAE1B60B945D28AC + 63BA20D8B97E434058A90B88D5B6E96F1F19046EF8F9AA9D83CA150358E05AF1 + 6B2BD8AF341CFEBAE0D785AF2E74D56767C0D60362E502596EA42DF57C96DE30 + D71DA33E0F03BE5E9FB9F7F85DB965859FCBD2562F7F382F0394C8A29CA99BD7 + F7C82C813F75E1EE31FE6E8E9A55B0A0B059DD52F62287C782DEF99F712CF3F5 + C2882BF7BFC9E260F33F49FF614A65916F3171B99F5E2CC915901568651F69FC + 5F4A13D8D47F95602F95E381BEAE045D330567AF1E46D61581651D5BFF57FA8F + 1370E6180D02CB2AD819B3BD74E281BE2EE89554EE026017FA0E6DABAD80F021 + 83BD2E6896047B5D2F0DDE92F706577A8147E057FFE54EDE19A780ADA0AF2B6F + D0EBC25EB74C6D25B7CE05C0FA4FD776AAC7DAD8CA6C5CCEBE2A4F55CA6D4169 + 8DC7F99FD538E55182C76579BADA5CD0CB571E4F1CBCC7A6E8A5246E2B5C43F2 + F95DD6975E504AE27F70A2CAD85E72BD57E84524C90D65A0D8F282DE0EF8569C + 64C1E9033C9E0704F3FE96CA6BE2B64993C70ACFB57260B1BC83ECE4989CFB6D + B2FE9F15FA81FFF9499A4B705E914A2573AE93C8F9C526CE6736716E95C4394A + 1AEF15699CFF241FEBF380E12E6C920BE7832D883FD0808D8A694CC5EEAD46B4 + 002CE78C719C13C6EF6F463CD3B8BD8DD8C83241E088DE124474CBDD73193672 + 6E18476DE82BC3FAEE62ACEDC843586B36958388F67CB30E8EEACCC186AE6CAC + EFC8C4FAB60CAC15086E4BC7BAAE2CEE97878D5B8B11D35F88B59D9958ABB2FE + 02C46E2BB178C4B1AC5BD79D83E0965404362621B839C5DA4574F3381DD958D3 + 9E89F0CE2C84B1DFD0D60CD6A79995B0590B37A4C0A77A1356D72523A8390B7E + F569585A9E80655549585D9F8E55B5A9F0AD4EC1AAAA64ACA6FC6AD3B09A654B + 2B366171693C5656A570DF74F8D6B1CF963CACDF528DA8815AACDB5C89B89D8D + 48E61C78C356CEA739470F6ECE47486B21C27ACB11AE9027FD5508DB52615E74 + FCDB0AA87CF835E5C0A72E03FE4DD9086779646F25C2DA4B112810DD5202DFDA + 3CACA8C9C1B2EA6CAEB9B2E0DB5888A02ECEE3B73461FDB636F6A9B8C59CFF77 + D723B0AF0581BDCD58BBBD9B73F67ECED53BB14A80975ADADB88E55B5AB16A77 + 0F56EFE9C30AA6F3FA1AB0940A1868E15AA211ABB98E09D9DE65B057A9FFE676 + 040D74717EDF8F0D0777B2BC1F3EDD4DB6AE9ADF5881153D4D58D5DF6E16C64B + 3AEAB0B04D718DEBB0B4BD0E8B5B6B4C4BB8DE5ADACAEDE65A2C68A8C2BCBA72 + AA946B2DAE1FB9CE13009E59926DA183B4C694D728AD2B156A68AEC7EB923C33 + 4D67DD8C124ADE98B80E9CC7F279155C9796725D5DC8F567411AD7D069068B67 + B28DA472952DA82EC08A66AEE1B8A69C9C9BC475760226E56CC284AC784CC88E + C7E4BC44DB9E9CC7F56F713AFBD7FE5CB3E7251B0C16F815F49565B1A4F8C0B3 + B81E9DC1314DE67A592F084FA326B17C7C6602C6A66DB4B5F6D45CEECFEDF792 + A2B8D68EC138A6A33646E0EDE8B518B5211C633746624202CB63233136210A63 + 121C002C482C8D11F0653A2A7E3D466F8AC2BB891BF00EF36F090ACB9A382906 + 635264CD9C708D04841D6B60B989DE68F95171D178372E06EFC4705F4A00F875 + 8F55F09BB2065EB78E5A6F7A2B82DB6BD7E2F5356BF06AE81A73056D103828C4 + B65F090933A9DCAD53FA467884E98580203CE3BB9AF2C78BFEC1782598FD0487 + E3658FF5B02C8705869F5EB1CAF4CCCAD57876A53F9E5AB61A4F0A02CB2DB407 + FE4A720F2D202CD7CE72DFFCE422A79DAC835D3DB9D80F8F2DF0A57CF0C44259 + F8323F7F251E99B7028F3295947F78CE323C24A83BD781BE0FCD5E6A52F913DC + F729EEF7F85CEEC3369240F0A382C133D96EDA22271EF094057878EAC241E8FB + 0B003C5910780E1E9C30CFAC7EEF18351D778F998D27A62FC7E3D397E1CE77A7 + E19EB133F1F8B4C5786AC610003657D05E2EA00701F0DB130D00DFF5960C2F26 + 9B94775C438F33DDF9DA784A21DBC6E3EED7DFC71DAF8E33A9FCB697DFC3EDD4 + 9DDC562ADDF6D2BB235B003FF796C905C1DE305879C15FE9E667983EE3580BFF + F959D6CB3AF8C9571DF7CF5E005840F896C765FDEB3170913CC62EF6CCD32395 + 5978BB47D9F661963DF43C6E7DF0B9415D0F061B10BE97A917FC1D2EC70AD871 + 01ED3E077601B0DC3F0B00DB736196DFC2D485C037DF2583A35FC26057EE736B + F779F670B970F77A00D8D5F067E4BF4723F561E59EE7F3233EA797BC9FE9FF4A + 9978C04800F7BAFA170360692406F3EF901DFFB700B0713CF13C2FDEE5615BC3 + CBBC39D85099A7AFEBE9A65FF63352DF231EEF7F5AFF30009615EF5D26F7FC6F + BCC989C36CDF037EC76EF8133F139DEB1FFE6C9FC52DFCEEDFACEFAE0714FFF1 + D6FB1C387C038F3D12A0FD57EAC61B6FE320BC212FE55A020FBE29A06DE9F75F + 9CFF93A40FE7DA2F38CFF33A00D8956DFF6EFDF287FAEF96F79FD3AF6938CC75 + B7BDFBF9EDFE1C48FB6BBA1EFC1D5E7E3D503C08804712EB958E7853F1DC7C46 + D2F01B95C90BF8BAF96BF5F0A0047E5DB916C08A01EC0D7987835FDDCC757337 + D0EB69E3CA05C08A012CC03B1CF80EDF1EB20076DD8EC802F879DCFC80F40227 + 2A12272B0FBF601208BE1E00762400FCA2BD05E7BEFDE6C25F93264A9A300D9B + 484902BEB2FA95DCBC6B0DACFA9100B0EBFAD985BFDE00F8F697F556E0B5D057 + 56BF6E5EF057EEA0959A15B02C82957F7B8281DF07C64CC583EF4DC30363A799 + E5EFBD63A7E2BE71D30CF00AF83E3973119E9CB1D081C153E6309D6BE9239305 + 7B671B1CF62E130436402C4D667B4F1F8F4F9795B02C84E7E2E9394BF1CC8215 + 786AFE323C396F099E59B8122F70C1F0DAAA508C0A598F3702C3F0FC0A3F3CBF + D20F2FAE920570105EF60BC66B2C7F33642DDE0809C74B015CCCB0EC5596BD1C + 148617B84079293402AF84AFC76B115116DBF79D98782EC21231765322DE8B4B + C03BD11B317A631C46C7C93A3806AF4746E2552E9C5E090FC36B6BC3F1962DA4 + D6E135961B188E8EB636720D2D083C3E998BC10C2E12D33331212D9D0BC4740F + 04CEF42803EFCB7D73460AD334BC9FCDBADC342E52933036350EEFC91A37399E + 8B3B2E6033520DF60AFA6A7B6C3217B3E98A3994C1C5AA60B062236561666126 + B79331354F71773330315316CD89CE1BC39EB8414E5C212795A5AF80AF00F08C + BC3483C14BAA8AE0D354892535C598CBC5B6E2E7FA28566E7B037CEB2BB1BCAA + C4A0AFE0EFDCC26C2C28CB37ABDFF9E5F9068A57365763A51E0434556011B5A2 + BD1EAB7BE5D6779B3D54F0E96EC19CBA32CCA928C29C925C1BF30C8E7596C0B2 + DEF096CB692EE2DD18BFF3CB73B0A022170B980A10CB05B4AC7FE5EE6B72569C + 41E039DC5E545F82151D75F0EB6D45C8B66E8452728FB6BAB709CB593EAFAE04 + 332B0B319763D49BEC4BF5B67A87E25AC93D73055675D723786B2BC7D987C87D + 3D08ECAB37CBDE05753CAFFA422C6928C2D2E612F875D72074A01921FD4D58DD + 5583551D95F057DACE6BC6760B6BF2B0BCA9C4B6F5B67E504F8D0160815FBF1E + B6EDAD1AB40416E8955677570CD6CB755CE0965A73DF2CF96FA9A678FDFBAB11 + BAA311613B1B112428BBB30E6BF608C4B621626F0BD6EE6942F89E7A9E7715FB + 2841F040052276D763DD9E864145EE6B32F7CA6BB99FDAAFDBDF826859EECAB5 + 32D398235DCC775B1A79B00DE17B9B10B65B96C54DECAB99C7910BE70EACDFDF + 6969D4C16E6C38D483E8233D883AE458EC461D1540EDF7005307B46E38B50D91 + 02A727B7198C7541AE0B77D7729FF0A33D58C3FDD71CEC4238FB5A2BF8AB361E + AD39BC85A9806FBFE5D71EED37E8EBECBF05EB4F6CC33A1E23E2D800F35BB156 + 70585098F9F53C6E248F2BD0EB825C415E01E6B5871D58ED825F1702479DE877 + C64E4509221F653FDCDF81C50388E1B909866E3CB9C55C47471FEB1D54EC89CD + AC73DC462B15ECD5C3613D10161C557DB4971572D4117E06C7B660E3F1ADA658 + 1E43D057C057F9689EA3F28E4BCD43064263799ED1470610C7F37601B19420EB + 26B374E27105BB391E8D7110E47AE0ACB95E3E2738ED40E86B2C7A3D92A5B001 + DF33D439EECBB2E10058DB92EB3EDAB53656DF0685BDE529775D367B435F6BCB + 7AE51D384DE9E1BC1ED67B52B3043EE7B8C376008000CA4118C8A55CC82BA892 + 23AB528F2B6307AAB0CD55C70DB2033F55E65ABF3AF50235B2CC732184A0B000 + 57C3DFAEA2ECDB33DC669B0F047E0FA3F0534782BD451E002C57C745ECBBE893 + 7DE60E39F7EA0EE47EB40B059FEE45C9E7FB51FC29CB3F9275EB5E83C0029C79 + 4CE5E65863D1D85C509B7E85D7E8F24E53C6D53DC8921B658DF503598FF19A9D + DE82C4537D669996C66B96CEEB6996671E209076711BCB3C00D80381DD98C08E + FB672716B0C0AF40B541608D99FD675D72AC7AB3A8CCF3ECEB8CDC9CF623FBD2 + 5696F523F3ECE66B24E09B76B2DBE06FCE8501E45EDC8ABC4BDB907F79070A3F + D865C05712FC2D160CE7319DED3DB65DC872816741E07C9E6BBE5C44733C2E04 + 16F82DF9F2C82F24103C68ED4B09004B02B502B865DF9D3209C43A80F0D4200C + 2CFDCAB10CD5B60B6E056B87E458FC4A15DF9D46D50F029802C42751F1FD3954 + FD78C1524163C141773F5925BB10DA1D8BE0F1903C305850F81B07083BA0F114 + 2AD95F35FB955B68C7EDF2106015401590951C97CAE7516DD0573A8F9AEFCE52 + 6750F7FD79D4FF707150753F523F5DA22EA296DBD53FA87FF6C7E30A4C0B420B + 021B08A66A048DD987A47CFDCF8E2B67D792D781A74330D5C0AE3B4EE6A54138 + FDC3250762735B40B9EADB8B8356BF4EBC5F07F40AF8BA105816D13A5F6FD83C + F819798E3B0882BDCADDBCF691947781B0E496BB7D2A35CB648F4ABF661F7A51 + E073B9413F32F85D2AFC5C2F15C8ADF371FB3F910A3F1BB2EED5CB23AEA5AEC0 + B0C0AF0B71F5BFE4C05DFE9E29F74594F42B8ED702FD07DACB2BE6A6D8A977E0 + AFF6D7FF980386F55F9D72690F922EEF45925C2EF3FFC96282F3D8E69940E0F5 + CA41EE7F80ED1D8B5A178ECA1236E7EA61FE771CE5EFEA187F63C76C5BFB645B + 1F8EBC41AF1BA35C124CCD64B90B87552F90E9025F6FF8ABFD25C14E41508160 + B7DE2C83AD5E2EA11DA029E9851ED7A343CA790157C14F9EB7975CB8EB0D8025 + 775BA92B1B2FDBAADCEE17BCE76ACC82DDB270161CD7F9E9DC332983D5068087 + CE41A0D83D478DDFC0EC79DD83F6D87805690565D3750EAC571F9B78AC38DE77 + 1364612C48CCED142F106CDE280621B03E1B7E2E768D8F72CC1CD715C16F5E1B + 815FD6677F78C4A4EBA2E33B00581E2F3416E7BC922F50FC6FD74B4FE9FA3EE9 + E51FCE395239E748E4FC25FE681FE238C7D978B80709C7375BB95C4F5BCA3949 + FA29969DE84112E71F82C071FB9BB0717F2362F73931853772FE97C87960D2A1 + F641A5283DD08AF85D0DE6127AE38E5A24B35D1AE7929B76D6238173D3B8AD55 + 8894FBE8AE42A6A588E92F47FC4009E2371722B6370FD15D72159D8590E66404 + 36269A829A9211DC9A8A0096F93524C09F65A19D99D8D05F88A82DF958D79B8D + D0F674ACAE4F806F4D1C425BD3CCCD74686BBAE5A560F521353A0A6B615D733A + 64811CD29486F0B61C04376762756D1256D52623A021037EB5A95859BE09CB8A + 376239B5B22C012BA8A5A51BB1A48465CCAFAC4CC4F2CA4D068557D6A461557D + 067C1BD210D2968B75F29ED35100FF862CF6CBF1D5B1CFE63C8B431CC273979B + 6DBF962CACE0719657A760455532565426C187A97FBD621FE721B021077EF5B9 + 3CE77CACA8CEC4E2F2342CA948C3D2AA4C2CADCEC12AAE3B82BBEB10C875866F + 4B1956341663654BB9859711000EDFD689F5BBFA10B6A38BEB8456AE0FBA10B4 + BB07A1FBB720F4403FFCF7F662E59E2ECCEFABC38A2D4D5C477460556FBDAD7F + 56F7695B10B897FBF7209CFD44EEDF86883D03068357F5B46079A7AC7E1BE1BF + AD033E9B5BB0ACB711CB7A9ACCCA7825CB03FB3BE1CB35DECAEE26B6A33A9B11 + B4B517015BBAE0D3C5F2CE262C6BADE35A4AEBBE7CCC2ACBC56C81DFF23C0B95 + 335FA9D69AE52CE73A725E55319636566119D79D7A51789AD685ACD77E06854B + 1D38AC50428AC53BA734CFD6A906964BB3B1B4A18CEBAF5AAEC74AADDD948254 + 4CE29AF8FDEC446A13D7D9899898B30953F2933183EBCC99724B5D46717D392D + 3799EB6DAE8FD3375A4C608B13CCFDF542F57479B52A56682179EBCAC2FB5909 + E6025A007862E626AE63F5527222C6A5441B041E9BE881BCB11118151D8EB1F1 + 519898B811E313A2F17E528CD50BFCCA55B4344E65524A8CC51F1E9BBA11A313 + A3312A31C6E0AF5E0C7F3739CE51D2468C49D13815E338C9B1008E8BC6D84D31 + 18B3291AEF26308D7700B0E0EF9B51117823620DE500E0D7D786E355015FEA8D + F07003C0DE20F895D050BC1A166675AF317D2948D03718AF843810D885C22F05 + 85E28580403CE71F6020F8797F89DB7E94C0AFDC467B20F0B33EABF19CAF1FF3 + 8E3B694160C50D7E466EA37D82F0DCCAE04197D082BC82C08E9CF8C1B21C7E6A + 99AC85579BD5F0E302BF8B7D2DBEB02C89B56DD077EE7203C002C28F7AB61F9A + B3CC03831DF8AB768F2FF0C1E36AE7057F0D0633F57605ADF451C509F6405FC1 + 60C940B0B63DEEA09D38C0B371E768C5FB9D8D47A7709F490B70C7E8292C9B6A + B182057FEF1BABD8C00E0096B5AF3700BE7BD464DCF5CE24C702F8ED21007CF7 + DB930C02BBBAFB8D098E5E7FDF20F0EDAF8C3509020B001B04E6F69F5F1A833F + BDF8AE015F0160D71AD8DD362B60CADBE2D74D0DFCCAD0441058C0D75377DB73 + EFE0CF4C6F7D6A08FABABAE58957A85707DD41BBCF2EF5ACD3AC813DCF40DDF0 + 77F63C5316C39E67A96654C3D4F21E08EC82601700DF3C0200161476C1F0904B + 6827CCDF1FEE7AD4A4E7C06E0C603D1BD6F363E799F1A3B8E1AE21F07B3D08AC + 67D523815F57EEF3EEEB0160F7F9B877FA7B35BC9FC1722FF07B0D04F63CBFBF + DE73FC6BF6A314137824807B5DC8FB3BE0AF3432DBF87D7279C97FC5F5F33FAB + 5F0060C9B89627FD27E4F22F6361837D3BBAE1269EB3D731ADCE0B02BBFCEC7A + 72DB796BA476C325ABE4E1DE7947EAEB5FA51BCC85B3037FBD01B07D06FA0EE8 + 3BF66731A7FB7013D3DBF83BBDF5CFFC7DDDEC5CAF1B759DD85EB0F8267D6FD4 + EF4890F65FA96B00B037F01D516AF3FF1008EC751EFFCC97C7DAFF2EF143FEDF + 40C3FFA05C8DD4D65BC3DB79EF3BB2AE05B5D7D3F500B02B17FCBA1AA9DEFBA6 + 708D583F62B9E47503F2966E4EEECDCF5B2EE81D0E80876EAE43EE9F1DF8CB1B + DDED72B7A137B37E097BDD9BB76B092CF87B3D002C59B99705B0815EE6BDE3FF + 9A0601F053B8F17EB97B56BC89E7201725725532A4E73D2098139A079E67FBE7 + 4C37B1DC05BD16E7C24B2A1B7CF3CD9DF47820F0CD2CBFE95147F6F69C265094 + 264FB2F89564F53B1C00CB0D8BA0AF0B7E5DEB5F03BF82BE1E09FC4A2EF81D72 + F73CD6AC7E5D10EC0D8065057CCF5BE371EFDBEF0FC6FC95E5AFC0AF00B0C0AF + AB07DE9F814726CDC1E3D317529C144F9C858727CCC4632C931E11E06599957B + F20FB25EA95901530F0F03C0DEDB4FA86CF6223C396F299E592818BCDC40F02B + 5C38BCEC1380E7977361413DBB82E94A2E307C03F08A5F305E17F0F50FB14587 + DE40B5B835815CB0E8CDD490355CD844E0B5355C18AD8DE4A228CA89ED1B154D + 39E99B91517875ED3ABC121E8197B8287A2E988B94003F2E9A42F0F67A6721F5 + 96DC41C76CB47DDE88643F1B98464599A5F07B9B12313E3905E353B900CD1490 + CDE0622D8DDB295CE42573812738CBC51BD3318AEB93A9B795B9A0CB48C0FB69 + F19EB77B9330392BCDDC3D4FCFCBC5946C2E7C7373B828CDA77231AD80DB05D9 + 98599487592579169B686296F671AC86058FC7A5241A3456ECA0491943EEA027 + 6570719B9F610058F0575AC0C5F88A860A2CE5627D41551117E5051607D7B7A5 + 067EB2AC954B642ED065BD3BBF2C1F7379CC0595456C5F81954D3558D1CC366C + BBA4B91A0B1B2BB190E5D252B95F6E6FC4D2E63A2CA8ADC4BC8A62CCE4A25A6E + 982779DEAAD6A25A8B7ABDC9AD38BAB3A8E945E9E6166C4179AE5909CF2F77AC + 7F6715A5606E5906B733B1A832178BEA8B798C2A046D69C7DA9DBD1613CBAFA7 + 09FE944F5703C7C3B1345618EC5DDA5A6596BE2BBB6A982FC34A41DC2D0D08D9 + DE86F57BBBB1617F3742071AE0DBC6BAD612F8305DD65488C5F5794C8BB0AAA3 + 1CAB3B2B595EEAA92F854F6B29CFBDC8DA29F569E3356B2F65BB728EA5C6DC32 + 4B81FDD558DD5B61AE9943B6D5236C7B03CB786DFB34862AB3E6553B7FB60B1C + A835E0BB765713D568F056C0377C2FD303ED432E940F7561FD7E96EF912BE81A + 046CD1312BB166A727262FF79722F6B420621FAFCFDE7684B39F75FB3B117DA4 + CF142980EB71D7BC815A7F48EE9BDBCD9573E4E11EAB5F47AD97C5EEE13EB3D4 + 75F272F9ECC8B68F6D1984ACAEA24E0C98A5ED3A83AB5B1071B4CFF292E53DDB + 82BE6B792C81D97572EFAC3682B85484279595AFF266052CF8ABB1C8D2D7531E + 7EA8D791A7DCF6639D81DE433CCFA3FDD8707C2BA2048A3D56C8EB8FC8F5F316 + 440B5C737CB25056EAAD48D6ABBDEAA2793EB1A6CD0650371EE7BEC77A0DE6AA + 4CAEA665F12BB7D3E64E9A72AD7F379ED4B617943DA7763D88E5B1E378DD361E + 1F300D07C07AF0ECC48E3C6E0FC9E305A0796D057BD5CE85BF922C80075D406B + 7C9480AD40ADC627695B20577903B81C8BCA0CEC520E0416D4653B41ECB31CCF + B99D6CB79D6582C37AD0CDE332EFC26EB37AE6B8747C07425F2BA7DC01BEB2FA + 7521B19B7701B00B88D314EB5110C003484C8AFDC8ED8C0F64D9B51F3902168A + FF7869AFB9D7B458901F2836EE6EC815B2E2C9E67E34146336FBEA6E838B0618 + AF3AC073C8FA54D6A8FB51C4B6B93CBE54CC7CD9674750F6F911940ADAB27D31 + DB977F7200A51FEF4331FB2B9275EB15B93BDE8ABCF3FDC83BD78B9CB3BDC83E + DF8BDC4B5B4CCA679DED41F6B9CDC8BBB295C7D889E28F77A1F0C31D289485EC + 955DC8B9B41D9982AC97B7599A7E7100E9E707B8BD1DB957391E2A8FCAB9B203 + 1917069026AB5C59EA5259977722977D6432CDBCB293D740F125E5025AAE471D + 57D06E7CDDECAB8E0B6841A6EC0F7723876DF378EE066029B3C2E5BE79966A5C + 3B39667EFF0CD0721C1C679E4782BCB917B7F1FC062C5FC2EB6AD7E5C33D2865 + FFCAAB4CA0B784D7AFECB34328FDF410CF7D3FCB0EA084DBC59F1E341572BB80 + 6D64ED5BF089235958970AA4CAE2972AFBEAB84965255F2806B003EC1C682760 + 7BD201ACB2D8FDD6B1EABD1E007621B0C140B613AC1DB2DC7520B2FA11FCADFE + F11C2ABF17BC3D85F2EFD93FF3659E6308320B0EDB31B99F0B0F653DECF6A1FA + 41A90F8D932A537BB6537BA79D0385CBBF95BBE7B3065005570559AB65214C09 + A8567ECD317D7306753F9C43C38F1750FFFD59D472FF3AEEAFED869F1C352AFD + 8B5C330B065F40CD8FE73DE7224B64EF3151BC069512F335ECB786C7AA1A84C5 + 8EE572E5771A872C90057D057F594E556ABC729DFCA5C70A97D7B9FC4B9E0FAF + 6DF5D7B250E67908FA725B56D3DED6D8BAFE82BE3A57593EEB7C5D00EC7C2EBA + AE8EDCCF471A8AD7EC943B9FBF9377A1AD5B26A92F775FDBF6C0DF128DFB2B7E + DE54E917FC1EF1BB54228B7259F77F7A98DFC3C3C8FFE4107F37FABDE8C58D23 + 28FA8CF59F1DE7F74E2EC715375B96B487072D7F25BD94927159169C0286FC3F + 92FB5FFD97F1B7E8FEC7C93570862C44F91FA8FF35416303C89E974D0CEC5DDC + 696D53D887AC490528052A053B5DE0E902504144792A70E2E8F23F91CABAC8FF + 4996E77DC07151396C2F57F97AE1E51AD0AB72CACD7BF76DC71A068005750DE8 + F258829B2EE01C02C0FC1F667B17561B0CF6804C679C870C70BA30D801C17BED + 98EA47FBBB30D67B5B125C752D7C9DF376C63B6801CCF6291C87AE970160594A + 7F7CD46341CDF3B0B13BB05772C6E99C9FB61D0B6641716DEBDAEA5A3880D6C6 + CC34E312F767AAB8F2726D9D202F1C9480AF5918CBFA979FAF5D1B6EEB5E66E3 + E41873A87C5E135965AB3F9D87C62E19C4673BBB0E1EC06DE7C454DB6AEB4A73 + 01A5D68E69C6797E9FCEB1CC5C48732CBCDFEA9EABFB6F12A5B2148E3555D6CA + 6739CF38C9B9C8D12EC41E69430C157DA80591071A11B9AF01317B9B117FA00D + 099C074A8987E5E5A10729C7BB9174B80309075A4C498759A778C49C7F26701F + A51BB65662DDE63244F49622AABF021BB79563637F1136F4E622B23B17EB9886 + B665C2AF3E11BE359BB0B276137C6A93B0BA311581AD9908E9CC41586F21D6F7 + 9722BC2F1FC1ED19AC4B866F5D12FCD5860A6A4EB33458A0B7351D212DA9086B + 63BE25CD01CA7235CD36A1CDA908694A7552B60D69C9E0BEE9086C4A837F7D32 + 56D726C24FAA494460BDF6611BD5715CAB2AE210C463ACEBE218B8AFC6EAC77D + 02756CB6F1E57E2BEB36715C3A8F449E433256D4256379431A7C1AD3E1C3FE96 + 55C56371792C16976DC4CAAA04ACAA66DB2A9E6F25D3EA14A6C95849ADA84EC5 + 92F264CC2FD9C4F515FBA8CBC292AA6C2CA9CEE1BA4B9E92B22DECCDC2CA2CAC + 6C2E414027D7153DF508ECAA857F07F39B9BB16E4F0F3F4FCD077773EEA81705 + F9BD3CBD1F9147B661E516AD85B8D6D0BA87FB3B1E8EF2CD93917F6F03FCBA1B + B95EE21A624B1B82FA5ACDBDB45F6F337CBB1AE1CB7594AFB98D6EC20AB6950B + 69DF2DACEB6DE47AA315415B3BB092FBCB0DB44070E056AEA1766E46D0B63EF8 + 0FF470DF762CE1FA714E6D89E31A5A5EA06AE526BA0073AAF3B1A0A6088BB93E + 5BC8B5E7FCFA4A2C6AE4FAB1A1D2C2F8CC28CB659B42CCE7BEF2D834BBAA0033 + B81E9CCCB5E2C4FC344C11982DCDB1303E3359BE80ED963496639127DCD0F4D2 + 6C4C2A4CC3240F089E90C3F5B15C3E1767386E9DB31331295730381353F3536D + FD3D2123DE5EA49ECD63CC29CAE2FA9A6B6401668E45305AEB5559214F669B49 + EC4FEEA3159358D6C2B21E7E3F35D65C404BEF6D8AC4988D11181B1F890949B1 + 9890188389B2104ED1CBE24300582198F472F6A42C5921739D9E9D8471E9B230 + 8EBF4602C0A33D00787C7A22D7F889E6D94BB18425B9947E2776BDE9AD0D1118 + B581F9A875783B32625082C1AF8587DAB30CE5F53C43B18995BEB1360CAFB34E + E99BAC7B352C182F04FAE1C5207FBC14146420F8D55041E05003C04A058925D7 + 42D895C5140E0EC34B812178DE2FC0E0B0BD90EF1F8CE77C031D004C0900BFE0 + 1B82177CD86E6590C51256ECE0A1B8C1B2125E85A796FAE0C92592AF4960587A + 6AE92A3CC1EDC717AEC4E30BA415786CFE723CA16DEA917902C04BF0C8DCA578 + 94E52A7B6CBEDC422FBF06FEBAD6C00F0BF84E5BE4C0DF994BF1D88C258356C0 + 8FB06C10004F7200B0E0EFBDE31C57CFF78C653A6E36CBE65AD9DDEFCDC05D63 + 1C2BE07BC6CCC0DDEF4EB7D45C408F9A3CA87BDF9D6A7221B0E4C25F6F0900CB + 3DB42C8265F17B97C702F87A00F8D61746E34F2F2A049C8C40DE357943609340 + B007FCBA32D7CFEEB6802F25EBDFDB3D00F88F4F7B8C559E78CD9E5BDEFCD8CB + 26D798E54F1EF8ABE795DE00D8DB0AD8959E8FDA7353C9131FD85B4E28BEE770 + E37DCFE2C67B99BFE719DCEC2D96492ABFE91EC745F48DF70C3DE71500BEF1CE + 218320F7F9B19E0F3BCF971FC11F140BF85740B09B5EFBEC7A080EFFE2B9F708 + D076A4B2DFABC17DBC5C435F0372BDF2BFB0F0F5D4498AF77BD39F1F3439B17F + DD7DEE61FA1BF286BEC3F32300E16BB9C63F2E6F8EF26FD58D77196034102B20 + 3902BFFAA5C400472A1F01C60EF6AD94E73A0880DD94BAF9976153AF1B4AF5F7 + 1CF3DFAC91C62479D70900DFC473753578EEFAECF97DBAC1BE5377E3963B1EC6 + DB63A7614D542216AD0CC6C38FBDC47EEEC11FF9DDBD4930F8863B710BBFF3B7 + C8D0722448FBAFD6A00B6817F40ECF0F6E2B1DB28EFD3F56EE3978CEE3B73EF4 + 9164FBC8C7B7F6FB5579FD30FF0735FCCFC9D5486DBCCB462A77B747D6B5A0F6 + BFAAEB0160C96E14C36537034F3A3C2F796E3E76031A76731B7E13B41BA117F4 + F5966E9C2EE41D7203EDD6EBC6EB01C0AC7361AF7BC33657D06ACFD4FBA67E5D + 693F4F0C607752E06A10027B01608BED7B9F67C2413916C0D7CA2624F72B06B0 + 62FE3A90D705BDEE0447A95B66F0D7E3D27910E672FB1696BB005813284DA61C + 00FCBAC5FC956E979E7CDDF651DC60BD7D371CFCBAB17F075D3E0BFE7A5C3D0B + 000BF4BAB05772B725D7FDB300B00381DFC7FDA326E2BE779CB8BF16FB97DB16 + F777DC7403B802BFF78F9B69B238BF93E65273583E130F518F8C9FE9C40566BB + 07957AC0EF7DE3A79B5C002C28AC7217FACA15B4AC7F5D97D18F4C9DCB09F93C + 3C3E67B159022B959E59B402CF7231203DBD78259E5CB4124F2BBFD4D780F00B + 5A5C2CE3C260E94A733BF4925F10171F215C8C84E2F550C5045E8737A9D7987F + 358C0A8FC0CB6B98AE5DE701C01B3CDB8ECBA4974343F0627010DEE222697434 + 17591B22A158C0EF44C7182C36701C1D8DD7D6AF37C93258B181DF4B16E84DC1 + FB69A9968E4B491E84B2635312B890E3222F55E9468C4B8BB305E8C4CC447BB3 + F7FD34A5C95C582AFE6D31E697956249751596D654335F62F0776ABEA31945B9 + 1677776A7EBA59046B9FC95999069DDF4F4BC1E44C2E96B3B8C0659F82C0D373 + D3304F56B8F9191627586EACA6E4A4604E490EE655146241550917D4F998519A + 87F972D5CC85B5DEB49ECD7AA572D52500AD37B657B6D4597C5DC5DA9D5BC971 + D696624E0DF72FD3023C1F938B7331AD241FF36ACAB1A0BA1C73CA8B30AB9C7D + 9517F0584598CBE3CD569FC53A0F2DB6B9E8D65BDE5CA0CF28C9C4422EFA97D5 + 9560717501AF83037E9737146165533196315DD85082054D65F0EF6B46F0403B + 5674D462797B0D7C3AEBCCB5B3C0AF20F0B2D62ADB5ED929559AFCFA6A1038D0 + E0B858A614EF2BA8B70641DDD508EEAD4540AFAC9A8BB1B83E174B1B0B06A1AF + 2FB5AAA3CC206F406F0582375723D02C79CBE0DBC96BD25EC4B404FE5B2A11B4 + 55EE9A759C2A1EA71CC1CCAF336BDC2604F457C1B7A794EDD8C7361E9F756A1F + B2BD166B763518F88DD8DD6016BD72F31C2E8BDFFD1D8838D08E88FD0E149615 + B0DA4ADA5F0ADD5167EDD7EC6941D89E5684EF6BC7BA835D587BA00B61FB3A10 + 2E4B5E01D0833D58BBAF93FDB6B3AEC3DA441CEA861B5737F6D456447900E806 + 0F2475C0699FA3A39BB19EEDD61AC4A5581F71A2DFDC2FBB56BAE187E5B659D6 + B9DB4CCA471CA5980AD00AEE0EB5EDB3BC37E83548ACE308F4EA186CE3C2DEB5 + 12C7A1726DAFE1F938FB38FB2B75DA69AC5B790EDB790E1C87EA781E922C9737 + 7801DFE112F89574FEB1A7B621E6543FD583B8D3BD883DE9805F17007B83DF98 + E312AF21CB94578C61B533287BB61F896707107F8AFBF0BCE2795D0473054A05 + 74BD81B0AC8FF4B0580F82F53037EE8403899517F48DE3758CB7B602C98E45AE + 0B785D8B5D49C7D5B680AF526D7B0360D79AD785B302BE1B4F3B319013CF3B0F + B713E512D31E76FF12006FF4806EB9A476E5B872A6CC8DB360B0037D057B5D8B + 5FEFED940BAC171CB9B2178EDB66A6571D17A9AE1B677397FAE101E4CAB5EAA5 + BDC8BEB807D94C73AFEC47EE07FB2D66AD2C5FB32EED82136BD6B19E1D02BEBB + 98DF89BC0F769835AA5C14675FD98E02A6E59FEE47B9A0E5955D2814E8BCB015 + 455776A294758597B6A1E8F236CB97C8CAF55C1FF2CEF6505DC83AD981B4232D + 483BD0889483F5483FD68CACD39DC838D581AC539DC83CDD859CF37D28FC80FB + 7FC4FD3F647F57B6A1E0F256E45FDC6696B33954AE802FCB73789C2C6E67F338 + 392CCB643E8B794900D8DD563E9BF5795777239BE797234B5E8E5FA057C0D705 + C0AE2CAEAF950B12EFE0B176DA35912BE642AAE8A3FD4CA90FF7A1E8AAA3D28F + 0FA2FCB343289115EF87BB51C2CFA384C729FB781FCA980A84977DB21F555F1C + 42F5178751F9E9015E4747B6CF277B51F2E93E94B3BE82F5659F1E864070E9E7 + 4C4D0751F2D94114CBBDB32C7CBF388692CF8FA1F44B01DF13B6ADBCE4E655EE + 82D641A8F7CD90BB65171E0A324AAE85A8F2655FCB6DF11000567BC163ABE37E + 66412C6BDFEF1CEBDFEA1FCF30152C1554E671BFE631990A00977F7FD2E0B0DA + 6A1F6FB7D12EFC1570752457CC02A602AE4E7DD9371ABFE3D2BA8C7DBA20D8DA + 7AB96F36EB5C59F1CAE2F75B6E7F7716F53F9C430355CFBE25E59BFF7209CD3F + 5F421325005C4FD50D53ED4FECE3A7733CAFB3362E41698D47E392AA5956FDBD + 0378DDEBAAF1C83D74AD5C467B2C85352E83BF5FF3DCBF12D8E6F597BEE43E9F + F3FA7C761C95BCEE55BCC6952C176C755C2F3BD7DD9540AC6BF16C50D653EF7C + 168EECB3F4805E572A73CB25B553EA0D80BDFBD0B6639DCD6BCEFD6479AD38D1 + 459F1F4119CBCBF83D2C95C5B92CCAF9FD2CFAC489512D37EF7A8944D6FA66F9 + AFEFE0171C038F212B61D7DB401EEBF2150758EEA1053EE5E6F9F23E93BDA8A2 + 6D2A53FF6D9E1756141B56EEDBB5AD58DE8A279ECBFF7AB994B7D8DE6A43194C + E67EFAEF93E4E9C0FAF3F42165F1FF4FEDF55F98C1FF6AC59055FC58C5919554 + 96A163093673DFDC0F750C8DF3E0A0B2AE0C415F0149175A5E6B29ECB8801620 + 753C5B6C37E8EB825A1706ABADDA9875EDA015ABCE45C0F6906D3B9E2D8600B4 + 814FAF7E4692EB665979673C3A06E5E699BA20359BFDE7F2F3108416D84DBB72 + C8DC69A7F01829AC171496D22596A7B22C99E34BE17E292C533EFDEA51647D7C + 82ED8E20E532DBC875F385FDBC5FEDE37D712F12787F4EB8B017491739361EC3 + 5BA91C87ACB633D997AE63D60787798D9D6BAE311A34E6FDD101DD07AC8FC4F3 + B2F2D57D512F3FE99EE7C068D51B4857CA6B2737D6291E0B65C75D34AF8F6D0B + 90EF3148EE7AD0D0BD58F057EEAD058D152E60D369279440DCC91ECE437A39EF + E0FCE46807361C6E43EC8136C41DEE30C51E6CC3C643ED483CDE8364CE772C95 + 1BE9C39D483AD265B18563763723617F3BE21567784F23A277D621A2BF92AA40 + CCF62A24EEAA44CADE6A24EDAE46ECF60AACDB5C8CB0CE5C04B5E722B8231FC1 + 5D8508ED29C6FAAD15D8B0B30631BBEAD9673DD60F942184ED5635A66255430A + 82DAB210D2968990964C0436A51B0816F85DD39E81B55DD958D39A8E80FA4D58 + 5D1D8755551B2DF5A988C1CAF20D58551307B9881608F6AF4F845F5D82C162B3 + 226E91957006A2BAF310D5958B70C51AAEDD84B5CD1988DF52828D9B0BAD7E6D + 6716E2064A10DB5F8490E61404342621A83585634941706B06FCD9BFC6B9BA21 + 0DBEF5A906AD058A57D526C1AF3619AB6B92E15BC1B28A44F856266165C526B3 + 345E5ECE94E50B4A13B0A432193E75D95856C335476D0ED73EB958549589B9A5 + 2958589909BFB63284728DA238C1DA5E2028DC548A35032DFCDCB620EDCC3E24 + 9DDEC5CF770FD22F1CE2FC7027C27674704D528E250DF2D694CB7557166695A6 + 635E650E96D4731DD5584E55C0A7A5C63C39F975704DD4D58C156DB5F672ADBC + 26ADDEDC02BFFE56ACE23A4BF18097719DB5A2AB0EAB7A9B2CD48F6200CB2A78 + D59636F80D74C37F6B1F02B76F61DA8BE55D4D98D75081B9F56558D85489053C + DE9C3AAEFDA8A5F216D5E6B88E5ED4A417886BA93A2C68A8C47CBD74DC5ACBB5 + 5A23C7516390784E753166706D2AF03B43F057AEA52BF331BFA698E7C7F63C8F + C5EC7F417DA901E3A965D906870570057DF5B2F16CAE3FB5C6542C5F815CB993 + 9E5DE6B144565A918745D55C63F2F8CB79CCC55AD3729D3A292F0DE3B3120D00 + 4F97EB69A68A673C4B2F2B33952B693706F004B9874E89C5F84D91541426266F + C4A494384CCEE03A9F7582C3EF26ACC79844D6A7C65A08A7C9D949D6B7C62538 + 3D9EEBF2F733B9665798261E776CDA269342440900EB456EBDD02DEBDF77E3A2 + 302A56317FE57A5A2FA8CBFDF4FA41D9738B28B97F0EF75804870DC25FE94D6E + BF1E1E8CD7D604E18DB57AD17D8DE55F0C5A8D978203F07248A0A5AF84B816C1 + 216609FC6AA82C82651D1C82D7C2F472BD60B0D3E6556EBFCCFA17831C28FC72 + A0134BF8055909FB04E0B9950178C18775ABD866B5148A17562BAE7010CBE516 + DADFE0EFD3CB64EDEB4329D5B6E0EFAA6B80B0CA9F9145F0221F83C04F2F71B6 + 9517FC55FA84EA0485057F29C1DF27E6ADC493F37DF0846208CF1A02C00FCF58 + 82C7662EC5132C9315B00B7F4D93E5A9CE81C02E00B638C013E5127AAE015FC1 + DF0764F53B7EB6D5DFFBDE4CD3FD969F8EFBC64C1B04BFCA3BDB9371CF6807FE + 7A5BFF0E87BFB7BD36CEE40D8325815F0160170AFFE9C531F8B3C7F257718125 + 6F086C20F8F9777E0180B5EDBA837625007C8700B0DA3CF59A2379217CE215DC + 2CE3153D9F14FC7DCA096B27083CF81C9452AA679E2EF4D5B6D53DA467A72FD9 + 735433A4519DC47233B451ACE0075EC00DF73D873FDCEB40E09BEF7674D33D82 + BFCF999477B69FC18DF73C891BF45CD8F39CD7DB28C87D8EECA6BF05805D0DC2 + 5E2FF83B5836FCB9F70890D72DB3D40BE4FE9AD4D65BD7D4DD3A243D9B1F82B9 + C39ED97BEA5C0D87C07FB8456DB8CFADF73075E09BA5C3E5825E57C3CBBDDB52 + 23F38D7F5EDE4CE55FA94110495DC3AD6E76589ECBBB862C686FA7C4FABCDA7A + 745D36E65ABB7AEB0627B5630F03C06E5FDE7D8CD8AF97AED9CF23E5FF95728F + 317C2C23C91B00BBD75BD6BDE6315796BFD22D77E3DE479E4368D4269CB8F409 + 3AFAF760DCE479B899DFE39BCDF5333FA33FDC61A9E201FF8F00605783D0D75B + 2E0036087C2D38FD3F56C3CEE39A2FC0F0B6C3E5B58FFBA5FC6DB93FCE7FDF9F + C070FD57FE9086B7F7EEE397FA25A8FD355D0FEEBA725C50FF7A9B41F82BB937 + 0DEFBC2BEF9BD6B01B98F74D6AF026380874878B6DECE6E971F32CC0CBF21BEF + 7898A9A36B0030B70DFAAA1DCBDCBCBB3D58A67D3DDBDED21B60831381DF00C0 + 373DF014F4C69937007625E82BB7D0CE36F30FC8CDF3B580D785BCAEDC098F3B + 117226419EB6AA7FC2DB858AE3D2D900B0AC7F3DE0D7AC803DD05800D8DEB27B + 9A93ACE7DE348B5F016057836E9FA93BE4F659D6BFAFBC7B0D00BE4700F88DB1 + B8E3B56BDD3F1BFC7DDBD103EF4CC4FDA326E13EA677B15EB180EFE3E454717F + 057F4DE367E1BEB13338B99D81FB3C30F8A1F767E151015DD64B0F8D9F6EE9A3 + 1367E311EAFEF1332807003F3491ED27CDE62499FBAAAD5C3F4F9E8DC7A7CDC3 + 933316981E9B3E8F93EFF96609FC3427F14FCE5E8CA7E62CC1739CC83FB75871 + 5C96E1E985CB1D18CC05814160EA192E149EE1A2418B87E77D03CC05F46B415C + AC0484E2CD9070BC15CE85D0DA75785DF037742D173AB2065E8FD725C5C6F1B2 + 081EAD18C05C3C69B13426360A633672911515C10555B4E96D0F001E15136B56 + C17207AD98C0631313B9B8DB8451F171960AFC8E17101600563E4D9057F03509 + E352E3A8182E00132DFEEED41CC5FB4DA652B8B0CCC3E2AA4A03BF0B992EACE4 + 22B8B404338BF331BB54D6B44AF331B7B2C859D872513C25371B937372D84F36 + 9589E9DC9EA618C23CA600F0ECC22C2C1278652A002C4D92F530179A82C8334B + 146F4990970BEC62BDF59C8589B91C17D3D9655C7CB37E4A41A6C5D33557CF3D + 2D5C78D770015D3CA859658598555AC8F114305F84C575D5540D6696956006B7 + E7D796615143955905CFAEE0A2BE249FED791E068695E7C2BD340BF32AF3B04C + 6E9E1B4AB188F98515B9BC16055821CB5F6A615D21FB29C6EAEE3A04F4366245 + 6B1556B657C1A7BD06CB65F5DB548A156D1516AF7755A7E2F0B2BEAD1CABBA2A + CDFD72405F0DC75F81553DE558DD55860059EEF65423A8B71A7EDDE558A1F8BC + CD4566D5ABED559D4A4B11D85B8EC0BE72840E5423626703D588D0AD02C1E5F0 + 67BD5F0FDB0CC8E297FDF69579206F8D81D9887DCDE6CA5965068515BB777BAD + 81DB30B978DE598F353B6A11B1BB89FDD6997B67ABDBD38290BDAD08DBDB86D0 + 5D3C1E8F19A6744703C277359BD5B0AC8843389E70B68BD8D781B5FB3B11A1F8 + BA471DB7CB6B0EF651DD083FD88535FB3BB0C60384230E74619DC5E1ED1904BE + D12707CC3DB2C5F4E5B620AC59D7B29FB547047907B096E5E68E79503CCEE15E + F6DF83D003DD0839D08330015BD51DDD8235DC3FEC10C72090AB32415F6A0DDB + 84721F6BCB3184733C268E692DB723D8E75AD6AF61D99AFD4352FC608D57A9DA + 589EE96039F78FD4F9C8A2D62301DF48B5651BA5D286A302B6AADFC2BC5C3B3B + 2057DB6E5ED6BF16D3D7D48FD8E32A972B67EDAF7DB620E6D800A28FCA225616 + B33BD9662BEB797CD60B08AB9F0D47E52E5A40760031BC261BB98F00B00B7865 + F92BD7D066D52B00CB7259FECA627B038F15CFFDCC42F7A4DC503BD6C87247AD + 58CCCA9B4EF59B06DD345382BD0677B5CD72D735B3BBADD401B10E94DD244BE0 + B3DBED81BB01E0730E0896048125956D3AA37315387680AF3D84BEB067F041B6 + E559AEF3505D9A59BEED8663D5E4E4A5B4F33B91C6637BC3CA2CA59E78990EFC + D0B6E2D5EE45B61E90EBC1F6B96DCCEF461ECB057D73B85FEEE55DC8632A6B56 + 59CEE65C91152DB7AFEE32682A082CB7CCF91FF25882A817B7A28CEDCA595E74 + 79000517FB0DF0E69EEE45F6C94E641C6D45CEC90EE409E632CD3CD6826C2AEB + 5833320E3522796F0D36ED2A43D29E72A41EA841DA9166649DE844E1B95E145F + DC82924B03EC7F1BCAAE6E47892C81D97F01CBF3596E209869FEE56D66155CC0 + 71E4714C72AB9C4DA59FDD8CAC8B033C87ED4ECA76CA6732EF02609D9F59F49A + 45B3AC79797D0CF60A100938ED459E411F41A85DBCAE6CFBE11E735F2D0BDC7C + 59DE2AFFD13E14F21A175002C282B502C002B9C51FEF4519DB947F7C00159FB0 + FCE3830E20FEFC08AABF3A8EBAAF8F1B08AEF89C759F1F468959FAB20DB72BBE + F05853AB5CFD492C77C16FF1D71E10FAD50954786908FC9EA41CF0E7404107FC + 0A42CA22B5F23B2746AE230145B94FA63C6E85CD4DB107345A19FB10FC1D748B + FC9540A8631DEBC2DB8AEFE49E5996C01E00FCED71C7BAD8E02FCB0D0E3B20B5 + FC5B8D4D20F7B8B38F97AAB97F0DDBD6FE7816353FF098ECAB9A69CD4FEA57E0 + 571A82CE95DF9FE57E1EF8FBDD05D47D7FD9E2FCCACD731D53B97F16006EFCF1 + 3C25187C062D3F5D40FB5F3F40EB5FAFA0E92F97D0F8978B66015CCB36B582BF + 3F735B60F86F57A8CBA867DEAC823DC0B78A69155359006BDBD21F3C318379CD + AA787DE5CED9B5007601759959FBF2FC585FCBB29A6FF879B0ACFC739E93F485 + 2C9E757D74DD3D6EAD990E7E0EFC5C1C18ECC0DF41EB607EB62EE0F506BF067C + BFD0F742FD0EB5B3CF53D05A96C8AC3728ED792140F52E1896DC3ECD8297DF51 + B96E2FE47F815C955B5EBF034A2EDE4B3F3FCAEFAAC02FBFBF667D7E0C45DCAF + F873590A1F37105CCCE3190C96C5B05C457F7ACC40B080AE606EFEC7D21103C4 + 72239FAB78E2B2C2BD7A80BFCB7D9047833CB7CDC7B23E3E8A22EBC369AB175E + ECA517E5E5D5C05C457BCA3FE0FF1D7FE3D9FCBF53AA1761F4524C26FF4F33F8 + 7F9ACEFFE634DE3B92796F4BE17D433038EBD21EFEBFEA7F60BF016AED9377D5 + DB22D881B282B502C172776DAE939997DC98C3BA0F24F01E20182BF86AF0F6E2 + 3EFB5F1748D6FFBEF24E5FAADB6B7DBA40D860F065C722D8E0268F9DCA71A5F0 + 9A24F35C92384E4979EF6D49C7F28E4DEC2A5DFBB3AD2C81CD85B5C0AB80B5A7 + 7FB9A34E611BC1DE148E2989E353EAC2E0545E87B40F8F18044EE2F852AE706C + 578F20EDF261A45E74E2F6A65C3A684ABE7480FBEF47E220FCDD8B445EF7449E + 7792EE7BBA0E3C8E9D3FA573D6F5D17591D22E3AD740D745F74ADD3F65312CB0 + AB17AD12CFCADDB3D34F0AA5544A661B03CE6A4B19DC56FE1C8F6D2F5AEDF0BC + 80C5FBEFF93D760F766230B32F2A45F772BDF8657380011E5B2F7E71AE70AA0F + 1B8F7723814A3CD98B4D02C447BBA92ECB279FEE47D2A92D48E29C23F9C46624 + 72CE117FA40B29C77B91C6ED64B64D3EDA89242A765F13362896F0EE5A149FE9 + 453BFFDF1B3EE37538DA8E0DDB2A11BDA31AF1FB1A0D184771AEABB6F1075A6C + FFEC33BCC7715C71FB1B10B9AD0211FDA508EB2B42584F11A2B694607D5F0942 + DBF310DC92690A69CD40587B86B9815E55B3112B2BA2B1A26C0356946EC092E2 + 755850188E656551F0ABDB84A02607DC0634C8525896C129542AC215A3B82B17 + EB3AB311CEFED634A650C958D7928EE84EC775F5FA9E1C24EDAE42CABE1AC40C + C845751E22A8353DD908EBCE41405B3AFCD897497DB765628DB9ABD6D8D2E057 + 93049FF204F855A7C0BF4E56C854433A825AB211D29E8FA0D63CF837E772DF3C + AC6A29C0EAD622AA18FEED6558D95880E5F53C06D71FC1DD15585CADD03F9B30 + A738856BA24C2CAF2BE035D275ECE0B5EF41F47ECE45F76F46E4CE4EACE9E7BA + A8A988EBC74C2CAEC931CD2BCFC0EC92542C28675935D798D585585A5B8CC5B5 + 2558DEC8B512D77582A9F36A8BB0A8B90CBEBDF5F01F68816F5F0316B7CAAB52 + 1996B45461596B35D74E4D5CCBB4C2A747F1855B98B6607937DB6EEEC4EA814E + 2C13006EAAC67C79866AAEC482A672CCE39A4E2179F4C2AEDC4DAFEAEDE07E6D + F0A5E43E7A797B1396B53530DF04DFEE666E3718149E5D558AB935A5985FE758 + F9CEE49A704E0DC7DEC2751CDBC96A79715385D5CFAE122CCE632A6B63B62DCB + B135B25C39CFABCC37E03B57AEA859BF88E7B984FB08F62EA92F834F7315FCDB + EACC03D60296CFD0BAB7886BD2E22C03C4F3B88F20F2C4AC04AE9D13B8C64EB2 + 18C213D3E3F07E4A0C26319D9AC9BA94584C4C8AC594D4044CCD48C48C9C144C + CA48C0D8A428BC27F8CBB613D2147338C100B0E0AF343127C9A0AF5279F79A92 + 9781C9B9E998A89051D9ACA72651068013D897DC3FC76FC0A88DB23A8EC4A898 + 08BC1BBB0EEF6C588BB7A3D66274CC3A8C8AE276A463ED2B8D5ECF36912C5F27 + 37D0720B1D82D7D604E3AD7561B6DF9B11A1783534D02C815F63DD2BA1417859 + 32001C6A65AA53B9C0B0AC830580E54ADA892B1C8A978243862C83F512BEBCB1 + F9B36C75305E58158C9756CB5B5B185EF20B655908CB42F0BC6FA0796ED3CBFB + 4F2F1F82BF72052DABE1A7990A021BFC5DBA0A4F53CF2EF3C3F32B02F0D4625F + B3FE15FC959E58B0128F2D588E27F4BC486EA30581E7727BBE8F815F83BFF356 + 0E02E04764F13B7D311E9EB1D800F093331D003C087FBD20B024F02BC82B18FC + C814A7DC85BE2E287EE0FD39CEF3B1F1B3ECB999194E8C9D81FBDF555835A6EF + 4DC7837A86F69E42AC4DC33DA3A7E0EE511371978C3004826529FCF62483BD06 + 7765F9CBFCDDEF4CB2721704AB4E2058EEA11507D860F02B630CF80AFC0E02E0 + 1747E1B61746E18E97DEC5ED0681DFC2ADCFBD69F20E23A7E78896AA4C0627D4 + 9F9F516CE0D7718B0700DF2CD7CF02C1D41F5D002C0900EBF9A79E753EA6E7A1 + AFE1B6C75FC79F1E65BB875F36DDFAB08C602819BF3CE240606FAF894A5D302C + B91E176F34031C8F3C6537E8B9ECBD72172D0B602FF82BC31F2FF0EBCADDFEC3 + 1D4356BED783BE37DDE1E8C6DB3D20D6DBFDB39E5D0F83BFAEBC9F917B3F271F + 5EFE6B1AB1FDAD435057CFE6BDF3BF0700BB1ADC47D0CD85B9C340EE2FCA8703 + 5FE66FE0FEE217069107EB9DB2FF2E7933957FA564893B086587F1AB1B6EBE96 + 7739120016D7F2627D83EDBDB898879929EEADAC564D7F10F865BD2B01E06BFA + 76E4F6E7ADEB954BC3F7FF2D8DE40EFABFAA91C6F50B0DB382BEF1667D7778FD + 5D00FCA77B71F723CF625960045AFB77A3A0B209EF8C9F811BFFCCEFB23E2F0F + 30B778C0760D4700B3FF2E5D037E7F0D008F7421FE4F917B0E5EE7F2BB3EF811 + F61919F60E97F78FF39FFC13901F795723D5FF0E8DF487248DD4D65B23B5F52E + FBA586C0ECAFC9B916431AA98D24007C136F16AE25B02BB77EB00FE525BB1950 + DE756E99E47513726F4CEE4D4DF2BE11DACDD0037DDD1BE9B5D20DD67101EDDC + 80DDBC038625E5870360E7A67DFD6DDDD85DCB60E9462FE0ABC980EB065A1AEE + 02DA05C0377B62FF7A03606F0BE05B1E7C11B77859FEBA00D875DDACD485B6AE + D4C69DD0C82D8A4D8C0473E54A8572DDA768F2646E9F87F5633059F2B2001604 + 9615B06B09EC825F73FB2CF0EB89F16B7AC371FB2CE06BEE9DDF1CB2FCBDFFED + 09067D65F56B10D85C4073F2C9BA3BE42A5A56C07A3371CC148BFF7B8F47F78E + 9345EF4C83C0F77A5C42CB05F423D4A3B2F2F5C060A58F4C907BE8596621FC90 + CA27CDC2A353E672923CC763053CDBE2043F3D6B119E11E89DB9104F31FFE4EC + 45786ACE623C3DCF01C002C12F2E5985E739F97F86137B81E09756FAE3454A10 + 58F05756C02F7211A185C6EB416BF056D83ABC1DBA0E6F068763D41A2E78D646 + E21DCA4030B7DF8E88C2E8A8182E8E36E0ADF591782F360E1314133836166362 + A331367603C6C644617C6C14C651A33670E1141DC5F2588CDE10CD05D37ABC1B + 1383B171F19065F0BBD4F8844D189F94C4C5D9268B2FFC3EF3EF272762424A12 + 1787290679A7E765324DC3E49C644C2F483217C78A833B3B3F13337233312D27 + 13738B0AB0ACBA1A0B2BCA31A3201F537373302D9F8BD8322EC06B6A30BF9C0B + DD122D688BCD92765A4106178659068067B2DD8CBC5C4C170CCEC8C484A4143B + FE4CF6AF18BE738B73599F895972259D9F85A95C5C4EC9CDC054E6A7B36C16DB + CCAC28C0E422EE5B98861915F9E69E6B916234D59498A5EFD256B931E6A29D0B + F60555E5985B5E8CF9955CA053F32AB8C8AF924BE97283D78B6AAA31A7B21CB3 + B8885FD4A438BC35985D5D8A69A50598AEF170C1BEA8AE0C8BEB4A6C81ADB851 + 5A982F6B2CC54ABD29AE37BCAB8BB0A032078BAB7279FEF95CB4F3FA3495C0B7 + AD0AABDAAB4D7E9D35F0EFE2E29D652B5B2A2C2EAF5F5795C15E9F8E72AC682B + B178BC7E3D15F0EB2E3358EBD7278BDE62AC6E2BC4EA0EA65D45F0EDC8874F1B + D55E88559D856C5B84D5DD056C5F8480DE12F8F7942090FB850E5462ED8E5AD3 + 9A6D3508DB5A8535DBAB11B2B306815B2BE1DF5F86A06D5508DBC536FBEA11B6 + A71601DB78FC8152A66508DE5189202A7437F7655DD8CE6A046D2D47B820F0CE + 3A84A9AFEDB508DED988A05D2D08DEDD8AC01DCC7B14BC5D31825B10BAB31941 + DB1BD8AEC9AC7EC30FF460CD7EC1DE1E841FD982B0C3FD083DB019A107056965 + 49CB7281E0839D4CA9435D587B5816C0BD8E752F157E44C056717A95F6727FE5 + 55DE8F30EB53FDB11F17E40AD26A9B6D95864907BA11BABFCB52576E1BB79DEA + 4339D6305929EFEF40D8DE0EACD9DB6E3288CD72A56E59B8C03525882B782BC8 + 2A0962ABAD0B77A3D8BF80AAB481E38BF648E5913CEE7A8E45529D60ACD3CED9 + 27F6D866C41CEB41F4D16E441DEEE471981720E6B9C71CDDC6F26D2CEF47E421 + 950DB0ED366A27CB54A738BBBB4DD1C776B06CABD51B10E635531F7127779895 + AFC0AE2C7813CC95B20396E3990AC8BA70D7E20D0B1C3375DD346F3CD16B4A38 + C37D4F6F61BECFD24D671CB86BFBCBEA97A90B7B95C60B0C53DE656AA3BCE0AF + 94745E7057F057F9DD66ADA438877AE8AF07C9AE25961E2E1BF43D27B0EB7910 + 7D5E658ADB2BB0EB6D99244B5459AE1D40C625C755A9AC95D2D8975C9766E9C1 + FD25C765AA6BB5EABA2F56B9134F73175327BEAD6046EAA91D483E3980349E43 + 16C79DC376D99776592CDBF4F35B9171619B63317B51D6B43BA8EDAC1338DD66 + 6E8DF3AE6E47D6A5CDC8BFD88762AAE8422F724F7720E37833328E3422E35003 + 52F6572171773992F75622757F35520F54239D651907AB91C17CE6FE0AA4EF2D + 43DADE1264EC2B4326CB738E36A3F054174ACFF5523D283AD38DA2B34CA9E2F3 + BD286459E1791EEFE2161EB71F8517B620EFEC66E49E637A7E007917FA2D9F7D + 663332CEB15C56CA1FEE320B61592DCB7A39F703B98866FE63B968D6B607720B + 7A2BEEAF20B049167F070C3E09520922291EB05C2D177E7A88A9A3426ECBF2B1 + E4D38328911BE68F99675AF6D96194322DE57E156C5FF5D951547F7E8CF9C328 + A72ABF3C86EAAF4F1804160C962ACCAA9229EB2A585E2660FBE571147DA6D8A9 + 87904FC97DB3C5CDFDF2A859D51A78FDE6342ABF3A8DAAAF1D09CA9A65A952CA + 017D0EE095E43A58725D262B9EAC52C7ADF02553E5776CC36D3796AEB5D7BEDF + 39316B0D227F738ADB0E009635AEA3E3067E0DE8FE781215DF3B56BF66F9CBED + EA9F58FE8380B160B40B7F4F5A99F67141F02000FEE9ACA5DAAEFBCB3934FE9D + 63FC4100D3B10016FCADF98963FF5E60DA81AC4EBCDF0BA8575CDE1FCFA38192 + DBE706B66D621FCD3F9F637A06ADDCAFFD2F57D0FEF315B4FEE5325A7EBE6CD6 + C0B202960483057DEBFF76190D02C07F659EDB8A11EC5A060B16CBB5B48ED36C + A0F82AC7A36BE858FB567CE358FCEA33712D8005802BBF3A055926D7F23A2B2E + 71353F1F59FE567C71D2AC81CBBE64FE6B07FABA9F89F3B95C303940DEE3FEF9 + 6B7DC68A0D7C6630CE72D197274C02B782B906825DC8EB198F5C54EB33166C76 + 61BEC627B7D982FF251C4B89C1657E7EECB78CED8A39B67CFD16F87BC9BEB4DD + FE1B5C0F0172819EFFE13E147BDC94977EC1EF3ABFCBA5FCCE9A8530BFC77215 + AD17165C4BF542C505E7EF42C73010CDFE05894B0C14CB6AD819B7CAE44ADA7E + 8F94F24E39CFF333E9A8B99B163CD6EF56D6C7EE6FD455FEC7079127CB64BD0C + C3FFC79C2BBB91FFC13E14C89298691EFF4BF32EF2BCF81F997946B15FFB917A + 722BFF270586E52581FFD7B208E67F7836FFB7F3AE3AEE89052B05261DB7CCB2 + 5C16683E0CC5A51D02C072ED7CD80323E596FF80C15859EEEABEE0BAAB3630CC + 3AD79A58C059705920D495B6B3AF1E36009CC6B1BB00D85BA92AA70C0CAB4C00 + 974A13CCD571D446DB9E3602C4694CD33D6350DF43B098E27864F16B0058F72B + 2F009CCEF34CFFE8A841E0648E2DE5B2CA65F17C0419AE05B0070A4BCA1B08E6 + B532F0CBFE04955D382B2B5E4170E76527E73E28693B97C7115C57DEDC5A737F + B79DEEA9F18AF17F5ADE38761A0836302CE8CE7B7202531DCB2C832F3B6EB493 + CEEC42E229597FCAC297F745B9831600567FDC5FFBEA852D7B69CBE3C5432F86 + E93EAF97BD369ED24B6ADD9C73F4724E30C0F9C0568E61B3CD31E24EF6B1CF2D + 36BF483CDD8F64CE31124E6EE631B6F2B7CDEF2C7F13A92C4B617DAAA0B2DA1D + EB3537D1D51FEDC3AEFFFB33F4FDC715DEF7789CBDCDE65A3AF5780F12CD8AB8 + 1D099C7B26736E93C63E0AAFEC4511EF654947597EB8996DE48ADAB12E8EDFD3 + 8004A631DBAAB17E7319D6703E2ED01ADAE6B8820E6A4A322B60BF9A78C7FAB7 + 221ACBCA23B1BC2C0A3E35B1086A49C69ACE2CB6CF4288C77DB4038033B0AE3B + DBD2D0A66484D425507108A98DC79AC644AC6D4D41447726A236E762C3965C44 + 6FCD47CCCE22446E2F40685F2682BAD2B0BA69135635246075E326AC6E4E4468 + 473AD6501A93BF2C936B5957B7098A471CDC24309C63303B667B0DE2F736217E + 5F0B22F5A2675F39D66CAE44C4B63A846FE17AA0AF1AC15D5C2750E19BB9B6E8 + E57AA6310FBE7539585D9F8F95D55958569106BF865C0434733CEDE588E8AD45 + 787715CF976B8CE662B6CDC5D24AB6ABCAC6F2DA3C2C10002E4AC63C416085D4 + 2953B89D1C2CAACAC7F2FA32AE9DAAB1BC99EBB6BA422C6E2EC1B2CE0A2CEFAA + C472A68B5A4BB876E31AAEB994EB3F8E6573330207DACC55B42FF32BFB9AB0B8 + B38E6AC08ABE162CED6DC682CE7ACC6DABC13CAEC7E670BD36AFA51C0BB83E5B + D6538FD5035D08DEB5956B9901E67BE1D3D3099FEE76ACEC6AB3B8C2CB3A1A78 + AC1ACCAD2DE77AB104F3B9469CCF31CEAA2AE49AB4C0420EF96F6E47D8B61E7B + 1959DBB3AB8B592FB7D18598555D60EEA70581675772EDCCBCDC452FACE17A53 + 6B58AE2B573457722D598EC535A5F0E1FAD6AFADCED279E55C731728FE30AF19 + D7AACBEA791DD8FFB2A64A03C7E3D2F5827724DE4F8B3517D07205AD78C0D3B2 + 13B9864FC6B4CC4D067FA729EC52760A66E7A6719D2DCBDD184C488FC174B699 + 9E976AAE9FA7E4A6984B6A016079C092C5F29482748B85AC97AFF542F694BC2C + 937900E3DA7DBCBC88C56F7062003395C633FF5E9C0382057EDF8E0AC7687B66 + B1CE342ACAB1FC7D376A3DC64647610C53E99D8870BC1511666DC7C4CA725810 + 38CC80F05BEBC2CD3DF4ABD4EBE1B21676CA04925F0F7760AFA4D8C16FAFE331 + A95743432C86B0E0AFE204BFE01F8CE7FD82F0A25FB01396CB7F0D5EF1A3FCC3 + 2C9525B000B0C0F0F3AB02F0AC00F032B97B76B50A4F795C3FCB3DF4D3CB141B + 78359E913CC05716C002C082BDEEF6A058FEF4421F3C35DF8729FB5AE03BE8FA + D9DC41CF5E6E105832F7CFD4E3946B01ECAD47A7329DB210B2FA15E47D6CDA62 + 03BEB20816145699EA1E98A0F8BF73F0E0FBB30725C309D7EAD785BF0E00762C + 831D003C19F7BC3B65D04A584618B7BDF61E35CEAC816521ACF06CD75807B34E + 10583182EF7AED7DB302BEF335EA1527049CE47A05340F819E67850A1B371C02 + BB20D852CF334649CF1D6580E2EA8F4A9FF2B884769F63EA79A54702C17F7EE2 + 75FCE9B1D7F0A7475EC52D0FBD8C9B1F9495EFCBF8F3A32C7BD2D9FF8F8FBFE2 + 40E08755E700606F08EC3E6B7521B10B8507E1B00C74EE7363063F35F49C57CF + 803D00D8D535CF873D71806FBCFB11DC7C979E3D5F0B7FA59BEED4F369C7E2D7 + C0EF9FEF37A94E65D73CF3F63C0BF77E56EEEAD7EA4692DBFE9A7D3CC6572ECC + 75C1EE35CFEEBDF323C9AD1F6CAFE7FC9437D8F5960B75BDE555F7DF0180C52A + 462A77E5CD55FE957261E470003C9C730DC90B001BF3BB0E131B64602C1B0496 + C3340200F61EC33FA3DFD38FF7F1FEBB34D271AE9573BEDE3208ACEF8E07FE2A + BD85BFCDC75E7813EF4E9A8D57474FC41D0F3E69DF33B981D63EB21A1EFCBC46 + 02B3FF2EFD02FEDA976138001EE942FC1FA86130F7373F74AFF6D2E03EFCE0BC + 35F423BC76FB9F9637FCFD17006057BFB5CFF5CA7F29FD09FEB606E1AC4723B5 + 71E4F47B2D0076C0B0EA7FF5A63042B97BB3F196F71B45E656C273131CBC217A + 6EA2C3B715F3F75A08FC886D8F5466A9E4025EDDC8991F04C54ADD1BFB3D4FE0 + 16EA8F9ED485C02EF0FDE3FD4FE316A6DE31806DB2602EA09967FDCD72EDFC80 + E3EAD995B6E592E44F8F70E24229EF4E4C04775D08EC025FB3F27D8C6DB5FD18 + B71F7D65704223D90487F5B772E2631325015FB97D7EFACD4188ECF4E1F46979 + A59C708DE4FEF9CE174739F2805FA577BEAC54133FB97A1E7F8DB5AFA0AFE49D + 17F81518961407F84EB69734F9942BE8BB46311DCD89E6A809B89DBA6BCC64DC + 376E3AEE1DCB49EB7B9CAC520F8C9B3608806501FC20B725953D21F7CE82BE93 + 1C0B6001E0C7A6CEB3542E9F057E5F5CB002CFCF5B86A7997F8EE9730BA885CB + 4D4FCD5D6C40F8B9452B0CFC0A02BFB87CF520007E7E851F9EE5F6F33E7EE6F6 + F9D5202E4864F11B168151E1EB317A6D24DE8DD880D111511815B11E6FAFA50C + FEC6626C6C02DE91D5EFFA48CCCACAC192E2124CDCB4898B272D9A0485B9C8DA + 188D71F1B118CDFC286D27C47391156756C002BFE39212F1DEA604B3007E2F41 + F180D23029231313D3D331214D6FEBA6606286806F06A61766633A1774330A73 + 31BB3417F3F5667231179A79695C14A6634E611E96545660456D2D9656D76066 + 7E8141DDC9D95CF815C8E2970BF19A1ACC2A2E32283C9DEDA772513839370313 + B3B2B998CCB37DA6E5E77371C9F2CC4C2A9D63D11BC85C98E6A499BB685901CF + 2F2DC0BC927C83BED30AF230ABB418F32ACAB0B0BE1AF36B2B31AD8C7D97719C + 5C542FE3C2D87F4B87BDB5BDB8B91A0BB8785EC685FD522E961770F12C2D69A8 + E4A2BA1A0BABB9C8AF2AC35C59FD1616B24F2EB01BEBB0BCB309BE3DAD58C9C5 + FDFC862A2EE20B31939A5F5FCA7EB810E7E27E414D1116D59562456B2556B470 + D1DD54061FE67D5B2BB8F8E6381AF4A0221F3E2DA5F0EFAA85E25BAD6AAF7040 + 6F678D49E05765FE2C0BEAAB45F0963AACEA2C635931827A2A11ACF8BB5DDA2E + C26A41DEB602F8310DE82C46405711558CC06EF55FCCFA7C53407711F72BA3CA + 11DCABB40CA15B2A0C02876FAF36ADD9C674470D4276C8FD732502B75620787B + 9529706B3902284161FF7EF63DC0D4A3C06D55DC47AEA0ABE1B799FD6EAD72FA + D95ECB3A9EE3401D566F6D84FFF626F8330DDCDE88E05DCD08DDD982F03DED58 + B3BB0D413B9A0C0E87EE6935F81BBAAFCB1472A017C1073623787F1FF37D066E + 238E6C8162E4861DE844C8BE36AA1DA182AC077B0CE6CA7DB3F60BDAD78D40F6 + 11B0B7D3147290FB1CDA6C7D047BEA0586057643F6755AEA5A0AAFA3E4AAD91B + FE8619EC7580B08EE50260675F8FE5EF3E01E03603D9111C9F64509B921B6CB9 + AC8EE058056B652D3B18D3F8A0806E17A20486057B797CC905BE4A9D7C37D6F1 + 98AED44F8CAC8029E5DDEDE8A3ECE74837220F75586A6EB3F72B36B2037DA38F + 6C33C51CDB81D8E33B2CDD70643BEB07B82DEBD87D505C36C16201E08D27B65B + 1A736C2BEBB71BF8D5B8752CD785B36B8D2B309B70BADFC06EDC49C7D2577037 + F9BC80ADCAF560B687ED36735B96BF6AA787B48E25AFEDEF01BB2EDC55EAF6EF + 96BB0058722D800580A5E40BB242DA3DF8D038897927CEA15C55B2FEFC2E0305 + 6E9C42C97DC82D2BA741D0CBBCDC94E67F7808791F1C4026B7D30488B9BFE264 + 0A52665F95A5D45E2826A61BC3364BD6C09E34EDA2E2046FE3311CB89BCA6B93 + 7E763B32398EF433DBB83DC07400593C9FCCB3FD483BDDC7EDCD483FE758D04A + 1917FA9179AE8FFBF421EB7C2FF33D483FDD8EDCB35D283AD785BC536D483958 + 87B89D6548DE53858C0375C83854833483C0B2F02D43CABE7283BCD9876B9179 + A80A39872A917BB81A79476A907FB41645279B507EAE1B1517FA507CA613B9C7 + 9B9079A41E59479B9077A20D852C2B1614E6F10BCEF522EF8C6B55DC81F4E39D + C83CD5835C8E39E72CC7C9CF37839FAF5C563B00D801C1B2C62DF868370A3FD9 + 83E24F65B1A838BE72732DC9B2791FF23FDA87BC8FF61B2812E8523C53B9B115 + A82AFC5400EB9859301ACCA2CABE3866EE702BBF3A665057D6BF82BF152CAFFC + 9CE56C53C3F6F55F9F44DD37A750CDF22AB6AFFAEA386ABE39691058C0B7F2CB + E396571F95B20A964B65B637D7CD8AAFAAE3EB98CC97B0ADE2B02A166DD5778A + 6FEBC8018AB22ABDE0C4A41570F448E0B6E60759B75E42ED4F975027F7C802BB + DF3910D885BDB53F5E460D55FDC325B6971CF7C58AA7ABFED4BFF695F5AAE0AF + 59C20A02CB45F337C71D7DCBF310C4FDD181BA02C265B2DAFDFE382A7EE0B805 + 84CD82D76927F8EB0260EF54AEA45D77D2B200AEF9E90C1AFE768EFDF19A7CC9 + 6BCF63A94EEE99CD35B3AC81753D381E59E31AC4FDE93CA5D8BF1E00CCBC0070 + E34FA7D1C4ED565E0F01E08E9F3F40FBDF1C6B60B9856EFACB4534CAFAF7EF57 + 062508DCF8578F54472976B05916336DFB8FAB68FD8F0FCD6A5816C4353FE81A + 09A83AEEA10585E592BAEAEB3306EC65F55BF125CBB96D9F1DAF71F5B73C8F2F + 4FF03B258BDD330678F5F9547FAF3EAFD8E727D7D2FA3C2CAFCF829F912C9365 + 8D5CC9732FFB5A6EBC4F9A94AFE4180CE07A415E7D5F0C3CF3B375A0AF3E477D + 4F3C62BDC6E70063590E9FE498D9F71727EDA507BD38212F01F95481C73DBC79 + 09B8E278099064192C77E5659F1D42A95E88308BE0A3A8E06FA59CDFF1F26FF8 + F97B5E60702D97CD3299C7940C080B0C7BB625B364A674CD64D5AD71967D710A + C5E64AFA30F23C16C0B9FC1DE77FAC17380EA0E0D30328FA5C009ABF65B9A856 + B97EDF5491B63F641B59147FC0FF03FE97165ED98782CB4CF97F9A23E02B20CC + FF5C59085B7A6E17B2F95F9D73791F32054B65256A2FF0ECB2FF6C815B03B894 + E0A90B8425D7EA772400AC72D5E77D7ACC64ED0D021F643BB968762C8DD5B760 + 7286FEFB3F928B660702BB40D77B5B32F8AB324A30D8B50E3650CC3A03C41E00 + AD31B82F2C698C66A1CC72B3CAE578D2A941CB5F9609044BA982C36C9FC8EB91 + ACB1B25DF647C7907555B1840FF3581C2753C15FD3E5033C8E5E8A1278765C4B + BBD6BA72F5ECDE175D2B68492F42E57D7C04059FB05FB6D77D5265BAE6EEFD53 + 21212CE483C7CDB640AE620EC7733B5E6115D8D6203C53590B3B5E44789F662A + 8F219B4EC9BB07EFEFBA7753092C4F623B5D93C4733B6C6E20F0EBBCECC5B9C9 + 69CE7F4EF420562F95F1FEB9E99C3C9E080AB38C120CD6BC23E53CE720E7FA91 + C4F6BA3737F33FA492BFA914F691C47B700AEFC5E6829CFDA773FF12FEBE9A78 + 3FA8E16F268F739734CE656435BCE970B729EDA4EED503D63E45A9B6792C5912 + A79EECB27CF231B99DEE40D2A136A41DE9443A95C27CC29E7A6CE0DC39727311 + A2FA0AB1A1B71051BD05086FCF4240832C7D13CDD5B37F5D0256D5C621A43515 + 91BDF958CF76EB7AF2CCF2576561ED6958DB998150E6036AE2B0BA2C1AFE9531 + 08A9730070446B0AC2A9A0C678F61587351D693C6E3E22B7E523AC3703411D29 + F06B4E444013DB34A79A6BE8B0F67404B7A418F895DBE99016B9ACCEC49AB65C + 84B566738CB988DE5C8A845D7548E3B9651CEF46FCFE66446DAF41ECEE46C41F + 6C478CE6F19BB92EE82D47D4CE06C4ED6D47F4F666440E3460E3CE0EC4EFE15C + B9AF0E3EF5795C2BE6C2A72E0F812D2558D35981C0E612F854E762553DD7354D + 45ACE7BAB63A134B65055C99C5F5668A491078566132D782C958549983950D25 + 5C73956125B5ACB9148B1A0AB180FD2E68CCE79A8D6B97CD72CB5C89C5AC5FCA + F5984F178FDF5987651DD5161B78755F2B9676D463514B3596743560796F1396 + F5346189CA5AB92EE43E0BDBABB0A8A306CBBAB91E641B9FDE76AE81B896D8BA + 19019B7BB1BAA7033EDDAD58D1D58CA5EDB23AAEB374495B3D163655636E9D5E + 242EE1FA936BAA7ECEF70F6C47F8AE2D1C1BF7EB6AC1C2E61ACC619B39B5A598 + 5D5BC2F6A5068DE7D59570CD59CAB56BA5B9945E50C77368E239B4D59A5672CC + AB5A98E7DA5670787A61A6697E25D7A795455CA717605155B1854692BBE82979 + 4EEC60B9709E929384D9C5E9982D97D0054C0B33B89E4FC5D4CC244CC94CC48C + AC24CCCA49C14C59FAB2FDD49C4D985B92C9F5718EC5219EAE97B7F3520CFCCE + A994D57291BD803D9D6B6F79E212F4D5FA7E12D7EED30A32312D2F0D8A1D3C7E + 532CC6290E709C3C94451A101EB37103DE8BD5738B4883BE067563D6DBB6D277 + 95DFB00EE36222313E361AE336C6604CB4DA46625C5C34DE677FEA4BF0F8EDC8 + 70BC4319085EBF968A30F82BAB61C50A7E736D38252BE28841F82B10FC527010 + 5E080AB4F0592F0606E1D9D5FEA697151F38640D5E0E14FC0DC5AB01219420B0 + DC40CB02986D7DFCF1DC4A81DE557876F92AF3E82608FCCC0A95397ADEDA508A + 13BCD4170A05E60260A5CF2CE13E8B56E0A9C502C214EB9F59C43E2803C0F37D + F1C41C07007BC7027E4C4058E0D7238B092CAB5F81DFE98B4D02BE2EF415087E + 92ED1E9932DF80EF235316E0D1A9B20E9E8D0727CEC1C313E75E0381E502FAEE + D10EDCD573B3FBB87DBFA0B0070CDF3B7A1AEE19A5743AEE7BD729BBF3ED09B8 + ED8D71D47883C30FBCA77281E18966FD7BFBEBE30D0EDFF1DA78DCF3C684412B + 603DFB73AD7F057FAF81C09442C62916F0ADCFBDED89F9FBC620083618FCEC9B + CEF346D649B20016F895118A4900D8037F5D006CA1ECF4AC9272AC805FC51F1F + 7905B73CF492B9737601B02C82F5DC5396C302C0F67C7424B9CF4B3DF206C183 + 40D8F3BCF696079EC51FA95BF41CD740F0937062024BCA3F899BF56C98E537DF + CD323D4BBEFB916BE4026057DA76E1B0B69DE7D70F586AC0F876B6A1069F715F + 07F2FE02E6FE86DCF6D7EC2300EC0581BDE5FDACFE9A67F8C378815B2E066006 + 617FE4B60B6FBDC1AF17E4FD85BCEA860360C56FBDD1FAF7661BFF357933947F + AD783C974979B1ADEBE9C65BEE303910F8974C4C72CB5CFEE502CF6BE401C006 + 46DD76BF575E6CEDBA7219DC481AA9FDBF43BF180BCFFF167E06B7527F649EE9 + 0D7F64FE4FF7E166BD8471077F77FACEF1733277D19ECFEA46EE67EEA04702B3 + FF2EFD2600BEAE7EEF07E0F982DCF84F7C41FE9BE4FD85FE2FCBCEC5A1FEBFA6 + 5FFE40FF67F56B7F4E2396FF067CBE761FE74FF91FD5B580D791CA7F790C07FE + 4A837FE09E3FF1DF94FA53DF1ED8FB8B9BCF2DBA913840D8FB06E67D53F4DEBE + F136DE4CA99B6EE78DF53A72E1EFCDBC494B2EF075B76FB9FB7193BBED5D768B + 675B70F80F77B28F7B9FC0ED0F3F6FFAE3039A1C0C83C0F73F891B04813569B8 + FF194A6E9F256FCBDFE7712B271F7F7A989310C5A6E0F62D0FBF805B1F150416 + D87D81131467DBCDDBE485ED0580072D793DC0589319F72DB9DB9E7A1DB77332 + 75872C803989FAD3A39AECF0588FBDC4FD3C2059ED3861528C0D016001DFBB39 + B9BBFBC5D1963718CC09DDDD02BF2CBBFDF9519CB0BD65EE5EEE7E752CEE7E6D + 2CEEE12451F97B3999BCEF4D4D163D56C1941B23D85C45B3FC1ED57B74AFC0F1 + A809D444DC2D38FCC658DCF536EB464DE244559AC8C9EA244E4C27E3A171D34C + 0F7072FBE0D8A96605FCF8E439787AFA023CA938BF13E40E673A27C64E2CE0C7 + A7CDC7D3B316E3F979CBF11227EF2FCC5F6EF0F7454ED85F58BC825A8EE7172D + C733F3971A047E6ADE523CB3D081C02F2C5B8517B90078C927002FF972B1413D + BF3A00CFFB07E2255B6884E1B5502E52C2B9388988C49B61EBF07230171F612C + 5B1785D1311B31CAA3D723E4063A022BABAA11D1DD8BA9E9197863DD7ABCB13E + 126F456FC03BB1317837211EA3A9519B12F06E6222466F4AC23BF1094C13312E + 35156353D2F06E720AC666A463424E36A61515624659092615E4617C6E262615 + E6626A4921A614E5614A713E669697604E7919661615734198CB055D06C6B38F + 85E59588D8B215313B767341DA8129B9DC27AF880BC1524C2F29E5E2B08A8BD7 + 7ACCA9A8C48CD2524CE5FE9372F33031371FEF671733ADE0B1AB31BBBC8E8BC8 + 6AAA02F36BABB8982CC2045B44A673C19985B9E58558525B8E0595A59855C205 + 6719F3EC7771533317C92D985355C505693E17BB4558505B86956D7508DADC8E + 80DE162CE7627EA95C2E6B21DF568E15EDD55CBCB7C0AFB7998BF046962B7653 + 1D163508107391DFDE8655DD1DF0ED6D83DFE64E046CE9865F5F07967351BFA0 + B1C2DC8A2D6E2AC772C5EC6D2AC322B977AE2FC0B266C70DF3D206E565915B84 + 95AD854EBEB31C81FD8D081E68847F5F0DFC7AAAE0DF5D8300A9B71A8154587F + 3DD6EF68C19A813A047696522508EBAD40484FA5E557B7E561556B0ED35C0475 + 1721BCBF026B04607B4A1CF59620A8B3906D0B102200DC5584A0AE62846D2E47 + C4D66AACA5C2B7552384FB056F2937056E2E45507F2902FA8AA86204B32EA0AF + 143E1D85F0ED2E46E0962A965573DC8ADB5B8FC0815A5E136EF7D721744713C2 + 763623647B034276C8CD33C7BEA70DA1BB5B11B483DADE84A06D4D08DDD986F0 + BD9D88D8D785B57BBBD8AE1DC1DB5B10CC730D653E947552C89E0E53A840EDDE + 6E04B36DB0D2FD7D083EE000E1907D3D0EE855F9BE5E841DDC82D08302C64E79 + 10EB55E7BFBB03017B3AAD5C203868BFEAB48F40B3037F5DA82B0D5A027B0160 + C1DEA03DEDA6E0BD1C1BF70B621AB89B63665E6E9B058015BF58E077FDA16E83 + BEDA566AB18A3D5078BD2C7EF73B718C233C755187E5D6B9C7AC80D7F1F88E7A + CC2DF4BAC3DDA608A5EC5720587241B20B93B56DA058A95C3CB32CF6F8162BD3 + F5164C96C570DC896DDC476E9FB7705B50762B124EEDB07CF4912D66351B7772 + 1B1CF7D3ECDF1EE8EAE1EB0EE6FBAD5FB9878E3D2E97CD0EFC9575AEEBBE5940 + 57F17A0D087BE0ADAC73AE95AC891D4BE138F62977CC0EB0951B4907F20AD67A + C35F775B69EAC55DCC6F33CB1E3DC44DBB24CB5EEE7B6107922FED44DAE55D48 + A55298572A4BDC14D6E9D8C96CEF5AE7A6B2CC5506FB10782C90BBD22B7B91CE + E3645EDC896CB923665D06FBCF609959F2322F089C7EC9715129B7D06609CCBA + 4C59F3F298D93C76C6B9ADC83CD76FB17265D19BCEBC6BD99B717E00A967FA90 + 71663372653D7B7E0BB2CEF420EB74170ACEF7A2E4723F8A2F6F46F1855E149F + EF46FEE956641EAD43F2FE4A6CDA5381D47D95C83A28D05B89849DC588DF5982 + 9C634D283AD38EFC536D543BF24EB6A1F074070A4EB7A1E45C27CA2E74A2F86C + 1BDB34A2E854230A4E34A29069C9B916545EE944C5A54EE49F6C40CED12A641E + AA40FA817203C6993C4EFE8966EEDB8902F69D79AC192972234DA51D6B47D6D9 + 5EE45DEC47FEA56DC83BE7B881CEF5C42C2EFA58C0772FB5CFA094C15FE60B99 + 170476DD5ACB9D6D09EB546FED3ED98F92CF0E980B66474750FED509547E7DCA + 1EDE4B5582BADF9E627A120672BF388A6AA675DCAE63FDA0B89FD27AB6AF655A + CB7D0480BD5349105870B88AEDABBE62FF3C9654F3CD69EE771A355FB1CD9727 + 999E44C3B7A7D1F8FD59B36EADFF5E56AEE7D0F0DD59969F31D5711FA9E1BB33 + 689685EA8F17D164BA8026B66F605AFF83F63BCF7E1C35B0BEFEA7F3A8FB8BAC + 5B0516CF9AEBE55A8FEA7F388B469637703FB92FAE63BFB5727DCC635471FC02 + 7A028E029FAEAB67D745B4E3B25956BE82BC82BE82BB6AE7C05E6FE85BF3D359 + D4FEE59CA52A174436D8FB17F5A5B8C1BCD62C935570DDCFE76D1F6BF33DAF95 + C6CAFDACCE4DFFC2FE782E1AAFC0B0B96CE635D2B510F4EDF8FB87E8F88F0FD1 + F6B70F0CCE98FE76056DFFC9328F5AFF7E156D7219FD97CB68FDE9125A7FBE82 + 16B66BFAE9A2B9966E126CE67513746EF4A4B502E4FC0C047C057905CFEBA9BA + EF2FF0FB739ADFA9A3F6BDAAE2E7AEF64D3F9FE3F5E475FAEA18CABE3C8E727E + D695945C59CB3A59C7A9E335ADE5356DE4B56BE1715A7FBE609FAF7D17BEE579 + 7ECDCFEC0B5ECFCFF5F9F39882C5FA8CF8FD70DD551BBC37382D202C48CCEF99 + 3E13AA867D4BB5DFF038FCEEE9FB5CF1E941947D44F1BF49B1AC4BF99B2AFF6C + 2FBFEF074C159FF377A2D8D5FC0D952AD6B5625ACB325EBF2189DB155F1EE6B5 + 38866A817B7E8635DF9D403D3F93DA1F2EF0333DE3C41996A53255CAEFB72C95 + 65755CC13A03D7945C673BF057DFD54B762DAB79BE729B5DF6F94927D6F0A787 + F91F2AD7F6FC5DCB35FBC78758CEEB2C37D49F1CE558941EE16FDF79A9A39079 + 731DCDF3CBFBF000FF070E7974D0C0700EFF53F3F87FEF5859EE43C1953D0687 + 73599E75613BFF73F9DF6C5E189CFFF4CC0F1C97FB6E0CF66CB99D96FB692A9B + CA649F592AE33132AF1EB036699765993A04800B3E3F81BC4F8E0D0263C9B5C8 + 959417FCCDE2D85D2B5EC15C9549AE55AF406FFA876CFFD16193A0B15C3E4B69 + EC3395F5E622FABC5C29F33E2280CB7BCC26DE03155B3E8DE799CEF34A659B74 + 8E33CBC62F30CC31733B85D72245F72ADEB752B99D7891F7385E97545E83749E + 63BA62035F96CB671E837DA7EA1C35769E83EB1143E3900465651DAD7108EABA + 0038436EA0CFB3BF8B7BF959C9F2FB143FA72306DDF532942C81057FE5C2D9E6 + 0127055DB77900B020B320F01E6A2FCF6B0FE7110E101618564C7DBD98A517C0 + E44524F698E6159B9DB9C429CE253837917B6879E61028B610139C2B38611B04 + 90B7F1780A2FE1CC1136F2D8D1C738CF9195EE893E8E691BDBE905318A7307C5 + 12CEE075D0F8D32FF2BAB0CFA4B3EA7B0FFBDA83F89302CEBCEFEB7A6BFC3C46 + 32F74939CFB905C7ACB98A8E97CCB98ADA19AC56CA71B84AD2FC85DA74927323 + CD83743D385F8A3FDA8BF863BD4838DA8DF8239D883FD886B8BD4D88DD5D87A8 + ED9C93F72B7E700142BBF2B16E7309D6F61421B43D87EBBA7C440F9421C65482 + D86DC5DC2EC4BADE1CC81D747073B2A5010D9BB0AA3A16814CC3B81DC6F2A0DA + 8D5855118555951B10DA188FF59D2958DF958A888E64AC6D4FC59A8E0C847766 + F3187958DF9B8F88AE1C0437C92A5916C6D958C7F2F0CE5CA68588E49A605D77 + 01223AF310D5CBF9C6F65A24ECA8454C7F25C7548DC47DCDD874B8031B0F718E + AB35407F2D2239478E93F799FD9BB17E0F3FDB43FC0C8FED45D4FE6D5C5BB460 + 456B0D5628862FD768C1DD0D5CB3342080F980B61AAC6C28C4C28A54CC2F4FC2 + C24AAD2F5330B76413E694C6636E59026617C76146C146CC2D4DE47A3003CB1B + 72E0D398CFB5671156082ED7E7C3AFBD04EBB635228673F390817A2C6B2DC642 + D62DE41A6D611D55CFB5647385B9855ED05086B935ACE79A6E29D7732BDB6AB1 + AABD0E3E6DD558E5914FABD67A25E6C16909D77BCBB996F4EB69E0FAAD09ABE4 + FA996B48DF9E66ACE27AD1B7A71DAB36776105D3F95C53CEA829C7CCDA0A2CEF + 6945CCF103FCAE1E42E00EAE6776F563FDE1BD08DCD98F459D2D58D0DE88F96D + 0D98D75A47719DA93CC73287639CC7EBB588F925ED544B151659C8A04A07060B + 5473FC338A32796D146B398F6906A6E7A770AD9E81C98AD39B93846905E966B1 + 3B8979C513560CDFA9DC9E5598813945EE0BDDA90680A7666CC2F44C59032760 + 8ADC3EA7C55BDD2CB65368A599655CE317E7728D5E8859556598C935F974AECF + F502F62C85452A53682679E94AC494BC0403C8533C6EA6C72744616CFC7ABC17 + B70EEFC608F83235E8BBCE342A2ADCD271711BF0FEA6188C4DD860ED0479C7C4 + 3A96C2A3058BE363302E310EE393E45A3A16A3A3D79BB5B080AF60AF00B06209 + 4BAFAF0DC36BE12C5B17C1FEA3581689B723D7B3CD3A2B7F2934C4E0EFCB4C5F + 5DA358C04178CECF1FAF866A9F48BCAEF05B410ACF158AD70242F18ABFE0AFE2 + FEFAE2A9653E963EB76295E9696E0B06BFC0FA177C030CFEBEB49A7DCBA2582E + A397AFB6F05F0EEC5518B05578566EA30DFC0A02AFE036F75FEA87E796AC36EB + DFA7A967173AF927E739AEA0CD22788EAC8197E1F1D9CB4D7209FDE00800F8F1 + E94B0CFECADA57C0F7B1690B99CE67B9A0B062040B06B36ECA0203C0E6FE79BC + E2034FC3DDB2EC7D4FC613D3718FA0AFAC7FC7CEC0DDA31DF07BEF68B67B673A + C5B277D89EBA73D434DCF6D664DCFEB6E031F71B35157772FBB6D727E0764A20 + F8AE3726E09E37271A04BE4B71815F1A833FBFF82E6E632ADDFEB280B02BC73D + F49F5F7420B013EF7728FEAFE2FEDEE6913D4714007EF64DC703E1D38EE4F2D9 + 1BFEBA120416D4750C5C5E3309062BDEEF1F1F91B18CC750867220B123794794 + 15F04D82BA1EFD2E002C2BE007F4BCF619DC6CCF6F6509FC346EBCF729FCC162 + 023B00F8266EEBB9AF6B217CC3DD0A13F8B059017B4B7181DDD8C0D290C1D1B5 + 16C2DEA078B0CCEB79B7E9B6FB4D372AFCE1F03A6A10EE7A9E997B6FFF96ECD9 + BB7799E0F01F1D793382EBC958C17F0500532E5F18EA437D0F95FF23F26623FF + 33E2385CFEE4E15DD793EB3AD981C0BFCF85F210FFF2C89B87FD33F057123C1D + BEFD8FC87BDF7FA5DC630D3FBE87950E5E1F5D2B5D4FCAF104CC547971347E47 + F47281CB0F0739A259518F0066FF1DBAF1460EDE1BFCBAFA05EC1D499E8BF29B + E2090E9EF03FF945F9DF49DE1F9E17F01D4923FF50FF67F46B7F5ABF2877E1EF + BF18004BC301B0FDB15FD3F7B5BAE6CFDDEBCF7CA472671FCFCD42371CA68337 + 196EBB654A0D00AB8E3722DDCCDC1B9EDDAC3CDBBF06806FBE833759A6DEF537 + DFE9C05FD72AD885C083A057FB78CA9C9BB4471ECB61B9879625F06D0F3D67FA + E3FD9C04D81B611EABE0079EC62D0F3E6D2EA06F64DD8DF771E240993B688F6E + 795056C04AA5E7D9F6599B70DCACFE1E7160AFF2D22D0FCB2D89EA9FC61FD4D7 + FDCFE1D6475E31CBDE3B9E7AC3AC7B5D00AC54131F0704BFCC3A4D8E142B5875 + 9AF808284BDC7E9C759A54C93A58C0F8D9B770E7F3EF98EEF0C401BEEBC5D1B8 + EF95B1A63B9FE7C4EE694ED8389953BC0F5904DFC1C9E0ED02C6AF8C712C853D + 725D451B10F6C8AC8329E5955A6CE0B739C154BDAC84DF1C8FFBB8EDBA8EBE7F + D4043C3466121E7C6F0AEE9775F0BB932C9505B02C821F9B347BD032D8AC83E5 + 1EDA03809F9AB908CFCD5D86E738F97E66CE123C2DB7CFD4537316E2C939AC9F + BB10CF2E586A96C0CF504F2F70F42C27FB2FAF0CC0ABAB83F1AA5F085EA15EE4 + A2E0F9002E0C4242B9F80833BD1A166E717E5F0D5B8B1742C2F0D2DAB5787343 + 3446C5C6E16D8BDF1B83B722377001B301D3D3333137AF10EF2724723B1AAFAD + 5B8F37A36330263111EFA5A46042661626E7E66152562EC6A765635C5A16C673 + 9FF7593E3E231B6333B2F05E4626C6656519EC5DD8508739B55578BF30D734BD + A214B3AA2B30BBA612332ACBB8E82BC1A4DC424CC8607F2999189BA437878BB1 + B2BE05AB9ADA31BBA4820B4CC5D3ADC6DCAA062E066B30AFB61E4B5BDAB8B86E + C5B2E6362C61DB7915B50EF02D69C08C122E7E6B7BB9A8DE82F98D5C34D73772 + 71DEC405792DE673E1BCA84E2E992BB8C82DC7626ECB5A572E9A170AFE363663 + 514B2B1636B5B26D1D1654576379631DDBA94D297CDBEA10B2B91561031D58B3 + A313A1DB5AB940AF86AFE2F0F66BC1DE089F9E26ACEE653917EF01FD3D08D936 + C0369D5C68731C2DF558C685F94A2ED07DBA9AB1A2A301CB6545CC45F892C672 + 2C6B52CCDE2AF874C80258F17D2BB0BA4B0F0BF2B1BC258FF962ACEA2A816F67 + 298F216BD97AF86F9165722D82B7D663ED4E8E6D5B1382B91DD457C371D663FD + B64684F55619F00DEE2AC59ABE4A44F457237C7305427B4B1122B7CE1D7908EC + 2A40D8E61284F615B32DB73B7211D2998F60D649A15D850868CDC1AAC62C04B4 + 17B09F7284F757F27A949B5B68B9895ED55D08DFAE7CF8F5E4F33C72A85CF8F7 + F03C3A0B39FE5CF870BFA0CD9508DBCAEBC8B185706C81FD3C87CD1CFF400322 + F6749855EA9ADD1D08DBD566523E9CE582BCB2F875E496770E4AF5212C37B14E + 00386C9FE2E57663ADC72574D0AE0E04EC6C87FFEE6E04ECEB43D0FECD08DAD7 + 8BC0BD3D086059E09E5E84ECDF628038706F9703783D205800D88FC750B99549 + 3C46A007E80AE61AD0F56C7B435EC7CDB363E9ABB2C0DD6D26773B48905A30F7 + 6017C2F6B2AF5D2D83C057D6C0C13B9B2D952B68C705B4E30ADA7BDB7517EDB8 + 81EEC6FAC382BE3D0EF03DE2A46BE5EADAA375073AADFF48B6158C7562F43A50 + 562039CA9357FCDED86372FDBC05B22C16208E39AC87ACFDE6C6D949A9634EEC + 5EE50D280BF2727F378EB0E2FF5A5F2C8F3ED64BF5595C61C71DE37683C002BE + 9200B059E950AEA5AE1ECE3A0F681D4BE0B44B72D9AC87A75BADADC5E1F53C8C + 1600B687DF17761AE4D503DB646D0BBC0AB6AA9CF9743D1CD783EEF3DB917681 + BAC87617793CF69F74611B522FEF40EA959D48A332AFEE461695CEB2B44BDB91 + 2197CA5794CA32779BE5B304642FC8DA762BB22FED40CEE55DC8627F82B592DC + 330BE8E65CA6AEC875F15E645F51CCDF7D83CAF9609F953BDA63D678F9ECB780 + FD157FB01B259FEC856068DE5559ECC97DEB0072A922E64B3ED88AA2CB5B5078 + A10F05E7BB5174AE13A517BA517EB187EA42E1A916641CAC40DAFE32641C2847 + EA9E12A4EE2C42FA9E52641DA840CED11A6BD3F0C956D47FBA15E59737A3EC62 + 1F2A2E6D41ED875B5179A50FA5E7BB0C00979EEF605D1BD336149D6E46C1C97A + 149D6940F1B93AA635C83F5985FC6355C83922C85C8294BDC55409F28ED5A354 + 1099FD149FEB46FE991EE49FED43F1A5AD1CFF0E947DB40B151FEF45F947FB51 + FCD15E8B516A71783D5057328BC42F0E992ABE12789305AFE2F5EE43C9A78E4A + 992FFD742FCA3EDF8FCAAF0EB18D2C7CA92F05664F1A8CADFBEE2CEABE9745EC + 59D47E7B06D502B55F9D40AD40DE37A74C02BE4ACDFAF7CB13A8FFE624EABF65 + 4AD5322FF05BF7DD69D47F7FC6AC53957A0361D5357C7F164D3F9C4313D3C6EF + CEA0F15B96F1580D6CD3C063357D7F1A2D3F9EB778B60679D9B6996D5B7E3863 + 6AFDF12CDAFF721E1D3F5F421BDBB478EABDA5B256EEABB62D3F9D45C34F6750 + FF33C7F317C153C1548ECF2359D00A744A8D82C9063F1D90AC6B52E3B112AEFE + 5E16C74C6505CBEB64D6A9549500248F552D38FA970B90FBE66A41E61FD9C680 + B003810575EBFF7AC10381B58F5C3D0B0A0B480B549E32C8DBC036F56CABBC5C + 43D7FCE8B88A5699EAD44FDD5FD9D7CFCCF378F57FB984869FAF707CB2B67560 + B60070FBDFAF1A0056AA6DA9CDC0B053DEF59F1FA35BFAFB87E894B5F04F97CD + 7D74E7DFAE5A1CE1165E8B067E1675DFF333FD919FB9CE81E75323D0CACF4C96 + BD82952EB4AC93B535AF5195DC787F791895FC8ED571FC8D3F73BF9F4E70BFE3 + 064AABF59DFB9C75FCCE34F29CEBBF3B6969A35296B5F238ED3CDF167E579A78 + 9C169E57EB7797D0F22D8FF305AFC317FCFCBE3C6DDF51B9AA96CB6A6908049F + 62CAF1F1D8D53FF07BC7EB57CFEB5BC732B398D67790BF91DA2F0EA3FAB343A8 + FBFC30EAF87BA9F97C1F6ABE3CC0EF3BB725FE3E6A781ED59FB38E6D6D5B2F4D + F0F758FCD16E94F13755C5DF9B5D1F1EABF63B9EE3B7C7EC38B59EEF89B9B0E6 + 775B92C5B26B992CF86B16C05F9E34D05BF9A55E94D06F85DF0D4A40B8EA2B9E + 8327952B75BDB8A117380A3FD44B1FFACDEB777E1CA59F392AF9F4980160B992 + 36F7D254815C4C0B1E7FEAB88C961B69790528E47F6931FF5315C7BB92F55219 + EB8AF8FF5B78650F0A3E1498DCCF7D156B98FFC154CE4702BFFB0DF8E67ECCBE + 3CD6C9528E5EF061B9621FE77D224B59070A3B627BC53DE618F3989AA5ABAC6F + 592EAB5F83C0DC762D880580057C057F058233D9BFBB2DF8AB32171CAB2FC9F6 + 9765ED25C7BD72922C5D791F15D4D40B457A99C9F154A1FB9500A92C60058477 + 225DB097E79CAA179E54AE7B29B7D3AECAA2783712750FA492F50294C0B6E034 + C79224D07C99EDAE7840B659FCCAE5F36EB33076DC5FAB5F07FEBAD6D406812F + ED47C645B9E43FCCCFEC18EF7F3C0F79D038BF17A967D98EF7ECA4339A3F708E + 71A41331C7BA0D04CB6257F774591A279EDDEB40D63382BDBB78BEDBB0C95EE6 + 72A47942FC29C72388BC84C87A3796730D0B2D212F239C9F249CDC6AF98493DB + D897E6059C2BC8E5F40927EC84C25EAC3BC0B9D6BE16A6AD0681E34F6C661BCE + 4B049465616CF05896CA1CC3E93DBCF6FC9C2E1E66D9218EED00CBF6D9186575 + 2C57D6B25ADEA4EB4125F2B3D24B701B15F682759AAB28D484A43A016DCB7BA5 + B262B6F911E756310ACFC16BB4916312184E38DE8B8D87DAB1614F13D580A83D + 7588DC518368C516DEDB8898EDD55827E8DA5B840D03E588DE5689986DE5D8B8 + BDCCBC7D440F1421BC2B0B6BA9C8BE3CB3DE5D5DB3D1E4571B07BFAA18AC2E8F + 825F4534026A6210541783D0BA58842A5FED94ADAE8BC7EA8644042B86704736 + 825BD8475D22FCEA9310DA9689F0CE1C4BC3DAB3B09EE388E23A2382EB84356D + 2C6FC946405D2A02EB5311C5BAC4BD0D883BD082189E53ECC14E6CE0FC763DE7 + CED187F9D99ED84BED43EC91FD9C8F1EA0F621F2E02EACDDB58DEB9E2D5C57F4 + 614D3FE7DB9BB95EE8E3BC5916B68D45585499827965099857BA09F3CB12A904 + 2CA8D8E4A85CF184379A1655266145432656D46761456D3602DB4AB06E0BC7B3 + A70B719CAB476E6FB235D5AA0E59CF16702D998B79D5D9985D916DA17B16D696 + 988BE579D55CFB7AB95A567CDDA5DC5E5A5B84E54C57D415F31845585657C835 + 661EE6D7687D57061F85EE69AFC10A6A797BADC5FF5DCC35E382E67A2C6C6B30 + 883BA7A916B31AABB1624B17369E3B86B8B347B9CED9CEF9FD1EAC3F76906B8A + DD5C93EC44D8C17D4C776139D7A04BFA3AB092D766D5AE7EACD8D68DE55CBFFA + 6EED84CF96562CE9A831CBE005F5251C03FB6DADC142E66797F2BC8A33785DD2 + 31B320C5AC7BE795E7581CE1D9E5B9985628ABDD5473D53CBD3893699AB9719E + CE6D41E09905DCB73013B3F2B97F5E1AA6656DC254590BA7C563424A3C2667A6 + 6046410EE654941AF09D54528809250598585A888945F9542EA696E66056A5BC + 7EE945EC0CCC284CC0A49C0D989C158B2999710680C7C6ADC37B1B234CEF44AD + 31E03B7AC35A83BEEF460BD03A00F8BD8D911897B001EFC54799BBE8B189B178 + 9BF56FAABD5C46C747E3BD4D1B313629CED2311BA3313A3AD280AFDC3DBFB92E + 1C6F44ACC16BE1A178654D085E5FCB636D88621B85CF5AC7FA084BDF58A7B611 + 6CBBD6E0AFEA276C4AC05B11EBF05290620687E2F5302756F06BCCBF12A878C0 + 0166F5FBCC321F3CBD74059E5DEE8B177DFDF1D22AA75C2EA1BD01F08B2C97A7 + 3701E0173C1EE06409AC5060B20A7E4156C20A0BB684FD2D5A6130F8B9A5ABF0 + BC4160D62F0BC48BCB82F0CCA2D5787CDE4A3C366F051E574CE0058E8B68950B + 0C3F3A6BA9C504762D821F9BB164508F4E9515B060EFDC41F8FBD834C503D6F6 + 3CCBAB5C96C0F7BF3F0B0A9726007C8FAC7EB9FDE084D91646EDBE7194E0F098 + 19B8EFBDD978E0BDB9B8EFDD39B86FF45CDCFDF60CDCF90EEB46CF1ED43DEFCE + C43DA30488A7190496640D2C392EA0C7E38E57C6E2F697C6E236EA8E9799B798 + C04E2A09080B0E0F02E0E7DFC6ADCFBE657201F0EDCFBD633210FCDC5B4E1C60 + 0FFC955CF0EB42603D9774A1B0953FEE405E6F99210BA574B0CC4B2E0476E56D + 09ECC25FD7F5B3370076AC8015B2CF797E2BF87B830700FFC10B00CB1A582101 + FF7097E0EF30DDE9789ABCC1D2A1D08296EA39B23D5B1E02C0DE10D8DD7621F0 + A0047E153FD8E4580F9B05B19E8BFFD9794EEE025CEFFC3FABC1E7F11E36E05A + FB5E4F83CFFB5DB90CC09577DD08F5834CC1ADF76CFF331A8991FC7B353486E1 + F0F6B7F46BB1744764605EFA3D6D4C2E34FDADEDDF92777B37FF5FD5F0635C57 + B75123B052D5B19FE1D7EEA63FDEED5C5B6F38EFB241D7725A1A09CEFEB3FAC3 + 1FFEF4BBA4B6FF3F0380DD0F70A43ACAFDC0AEB77D8D06CFE57F3F00FC6B7F46 + DE7523E5BDE5BDDF708DD47EA43FE77F44FF1400FE1DB2F6FAF1E9385EC057F2 + B608D6B6DC405B5E37220FF01D7E8373CAE43263640B6017FE7AB7F17607AD9B + B059FC2ABD6348B7DCE958FDAADE85C5CECDDC711D2DD71F37DFEDC485B8E11E + 2FB7D00F3CED05819FB64983646F91790160C9B10E7E06374A0F30FFA0038125 + E5BDA5FA1BEEE744E40141E2976CD22308AC498E3B997121F09F1E7360AF037C + 5F645B590CBFCC360E4C567A2BDBDCFAF84BF8E3632FE166B98FD624EA296722 + 769B24AB604EE4EE7DE53DDCFFFA38B304FE3327758AE5A1499EDEF69335B060 + B020B041E16110D82C7F0DF03A56C182BF6E997437A578C202BE2EFC95BB688B + 293CEA7D3C3066A259FEBA00F89E511370AF2C83C74EC5FD2C572A202CAB60B3 + 00D64499922B688160B9827E62C60227F6EFEC05787C26EBA82766CF3700FC22 + 27F32F2EF1C5F38B7CF0ECFCE57876E10AC70DB4C702D8DE0A0D08C6ABC16178 + 63CD5A2E30D6E2B535E14C234C02C0AF8647E0B5F5915CF46CC01B1B28A60678 + E337617C623246C7C4E14DB9875EBF810B978D78372E0163366DC2E8C424B3EC + 9D55528C0595D598535A8EA979B2CC2DC0D4FC422ECAF23025BF0013F2F2F15E + 4616C666669945EDF2B6162C6969C2B4F2124C2AE2C2B1BA024B5A1BB1909A51 + 5581C92C9B9ACF855E4E36A60A1ACB8573662626A5A7E3FD94644C484BE302B4 + 180BABAAB1A8B6960BD35A2C6B6D864F673B17C9AD58D1DC089F9616F8B7B562 + 556B1B17DB4D5C78B722A86F27C277EC856F4F375674B462595B235674D66375 + 4F3302BB5BE0DB5A8765F5E558525BC64578395634D560715D3517EE9598535B + 89050D75542D16B06C5183E07391B9A65AD65001FF9E26846EED40F0403B7CBB + EA787E15ECBF8AC7A9C1D2362EA0DBB8786FADE5C2BDC65C79F97301BEB2AB1D + 4B5B1BCC8DB4DEF0F667F96A590AB7D5DA827B656B3596349659DCA915ADA53C + 46254264BDBBAB1D113B5A10D053C1F202F8761422B0AF1C215B64E55B8355DD + 9594E2F9AA7D1DD6ED6A43F8B626846CAE46506F25CFB502019DA5F06B2B846F + 731E7C9A73B1BA35DF4070684F1942BA4B10DC59C4EB970FBF8E02B667BE23DF + AC82573666C2A721132B1B32E0D792834095376563795D067C98FAB7732C7213 + CD3EFC7A1CF952AB048337973965B220DEC231F45660656731EB4B11BCB5166B + 76362358F0776B030206EA798DA406B3E20DE53908F0866C6F639B0E046D6F45 + F00E9609009B9CB220B695542600BC665F1742980AF206EDEE44C85EB95CEEC5 + DA839B1171680BC264C96B0058C0B5C714BC97525EF0775717D56DE5CAFBEF6C + 4780FADAD385803D82BF6D54ABE5033DE03760771BFCF5F9EC6866BFD40E7EB6 + 949FD29D4DDCBF0581BB5B781C9E0B15B4A795FB512C9342F6B6738C02C41D96 + 0FDDD781D03D3C77EE17C654317F4377F3FC6515ED299354A63A17F84A2E0C56 + B959032B9EB10B7D0F4B8A89EC580F9B2B694A79B515308E3CD263FBB8C0D6DC + 400B0C0B040BFA9AD5EFD64110AC7A0160B96A9462F4A0D56379B3F1B81EA4CA + 7A58B17A651DAC079702CA3DF6F032E1CC803D7C4DD4C34F3D80D543D293CE03 + D8F8D3FD2CD7035DE7E1B5B79BE64DA7B81FEB93CE0E98B5AD01DB0BB2CED9EE + 3CCCBEA4387F8A134C9DDB6A905716B926C1DE73DB4D66792BAB2FEE2B105BF0 + D17EE45CDD8D8CCB3B0CF0CAD572EA25F67F6517D39D96CFF860173229AB9725 + 2EF3D902C317B72393ED95CFBEACF8BC034839A5987E5BCCB22C97658A4D9BC7 + E3E4B02FA58557F7A3E043B92F3EC8631F44FEC78790FFD121B3602BFAE490A9 + F0EA5E147EB017454C8B3FDC8392AB7B50FED13E547C7600659FAABFADC8BB3C + 8082CB5BD96E3B4A3EDC81226EE75FECB578BE454C8B2FC8DD72170ACC7AB71D + F9275B9075A40E59876B9077A201F954C1F146149D6C46F9852E545DEE45ED95 + 2D68FF6A3F9ABFDC8BAA0F065071790BCBFB517B752B2A2FF7990570E9794795 + 577A5073B50F15B2083EDD8AC2530DC83BCEBEA992538D283DDBCC63D623FD60 + 2555CDE3B5A0EC7C2FAAD5DF47DBD1F0E92E347EBA170D9F1F443D55F3D97E54 + 7F7E00359F0B521D41D5974750FEE52103BE92AC0F05D9049E04A16ABF3DEE88 + DBD5DF1E43E55787D9E610CAD847B9F4C5412B13A4AA55FBEF04F14E9BC5AF54 + FB9D036D4DDF9F31CB5EB97896556E93EABE15EC3D8926D61944FBE6249ABF67 + 9DE0DDF782C02751F72DF33F9C46F34F674D4D3F9E619D2C3BD98E799509C836 + FF28F8CB72B6571F266D7FE71CAFF1C7736C7B9EFB78E02CD3D69F2EA0FDE78B + E8F8EB25B43115247620EFF941E8EB0D7F0587DBB86FF34F1EAB5903A8ECEBAF + 174CCDD2CF17D0C2BE9AFFE2EA92476CC37D0D02B33F0734CA9DB4132BB7EE47 + 8AA9DC22D7B9717529ABF3943B2E93CFA18663576CDD061E4B00B88EA940B1DA + D5FF4520D7512DDBC93AB9EE6759F76A1FC16A07FCBA6AFCDB45477FBF88A6BF + CBA2F732DAFEE303B4FFE7559ED3255EE7F3066EDBFE7299D7E90374FCED0303 + BA1D1EA86B56BE3C6EC7DFAEA0FB3F3E44CF7F7E447D886EB5FB59FB5C66FB0F + D0C57AE5DB78BD2CBEF0CFFC4EF0F373617ECD370EACACFF9EE7216B5C59029B + 05ABAC81F502C129547FA3EFA163155BF33DBF73B28EE5E7AC1700F43DA9E5F7 + A5E61B59961FE7F781DF93EF644D7ECCF2ADFCCCF4924103D5FED345F4FCFD23 + 8EF1437E5FCEA1FAB393A8F8F4382ABF14543E63D7D8B9963C363F775D6BB38E + E6F5931A996FE475745F48D04B07FACE369B0498059A059F8FF2984312F0ADFF + FA98A95163E4775379E73F67BFA5355F1DE179E91C65057CD454C77EEBECB354 + 6CE7F3A8B497051C205CFD83DC8E9F4515BFE3153C37B38896FB6759C153B28C + 56CC64DB36EBF833A81620FEFC307FFF87A8C328FDEC208A3EDE8712A6723F5D + 268BEB4F0588159758B1841D37EEA554F1172CE3FF4621DB4A0673F9FF59C8FF + CE52FEE796F33FB78AEDA50AFECF6A5B16D1E572FBAED8DCECBB90C72EE47F50 + C1678AD97DD811DB38AEA7296EE77D22002C77C63C06FB555E10D855DEC74779 + ECA3BCAFC86DB4DA1E42EE27874D720B6D1097121096047D65E52BD8AAD45BAA + 13FC35F7CE7299EC81BF6EF80183AD7AF189F754BD3025AF12991FC80DB3CA65 + 79EA4060C1DF940BB27A95D70A415A8533904BE55D8ED5AEDAB38D407132EF91 + 66797B49D6C1ECEBEA21677C82D41C871DDBC6C0F15EDA6F16BF8A8BEF5AFCAA + 7FDDE3757F575E0038F703DDE3784D741EE778EF6479B6CE87E3493E2DC02750 + ABB94607E71E1D0671F5C2579220B0CD137620E114C7E789B1AFF370A0AFBC7F + A89D27B403E70002C10A6311C379C846CE67A458E6A538CE4FE2392771B48575 + 9CB7B0DC81BFAD58BBAF1911FB1D00BCE110C772B8D314C57CEC916E6CE4BC46 + D038E9F42E1B4BF2B97DBC66FB991EE0DC663F12CEEDC7A6B30EE895AE81BCE7 + 3C5058E7CB541E4B0C0CB31F816EBDC426A8EEBECCA6CF4012DC8F3FE5ECA317 + EDE2059035F7E2FC4CA0349AF3B9E8FD6D542BE20EB45B8CE1248E39764F23D6 + 6FADC45ACEC9D7F557583E726B05A26409BCBD02B13BAA982F45E49612ACDF52 + 8C90CE2CF8D66DC28AEA382CAF88C1F2B20DF0A98E859F206F6D1C7CAA62E053 + 110D9FF228AC285B8F6525EBB0A8682D1615AFC3B2CA68F8566D844F652C9695 + 4661657934FCD957A05C52D72522A03E11C18D72279D6C0A6D609EE5ABCA63B1 + B2740302AA12B0B6230B1BB69562E39E5A24EC6FA09A11B3AF051B0F7623F9F8 + 56A49CE1B53AB907F1C7A913BB90707237F3D491ED883DDC8FB8437D6CCBEF10 + D70291F27EB4B90A7E5CF32CAD4935C0BBB86213965426624945021697C533E5 + 76793C16966CC4F2EA24F837F3FCEBD3B0B42211AB1BB211DE53CEEB53C77552 + 0996D5A66325D7512B5BF2B1BC31876B46B948CEA0B2B8F6CCE77A52EE927330 + A73C0B0BB9BDB0966BDE8A1CCCE6F69CCA4CE6055133B0B082F555DCAF8AFB57 + E5B06D1E9636166159532996B65462717385B96E9E5D5D8A9955C5985E598C59 + 5CABCE6DAC322BE0C5ED4DF0DDC239FDC1DD587B6007D7045BB1E1D87ECEF7F7 + 719DB30791470F21EECC49C49C3CCAF5C52E04EDDD8EB083BB10766017D72F03 + 58BDB38F6B945EACDECA75744F0316710DBBA0B114CB78EC957A21B9996310C0 + AEC83357CD72EF3CA734CB62FFCA6DB42C9DE7D5704CAC9F2D38CBF219E55CC7 + 1765623A35AB389BEBF66CCCA56615E8A5EE34D34C59046725E1FD8C444CC9CD + E0752AB1D0488B5B1AECC5F029653C5701E1AA727B417C666511FB65FFEC5BF0 + 7976712A26E7C4E1FDB418D3B8A468BC1B13817736ACC1DB518E047F47B34CD0 + 5779416059FE0AFE8ED9284BE1484C49DF64718947C7AE3308FC5E02FB898BA2 + A2CD0AF8BD8458CBBFB7319AFD6CC0A80DEBF1766484416001E057D784180C7E + 47F183A957D7AC31D8FBFADA7093AC8005806501FCF6FA75989020001C81E7FC + 56E3C5C040BC12168A974342F04A50105EF60F34A06B6E9D57ACC6B31E8B5F17 + 002B150456782F4160A5CFAD5CE594AD64D94AFF41082C00AC5060F20C27BD20 + 17D28B57E089F94B0D04BFB06C355E58C13E5704B22E004F2F59852717F9E289 + 853E7872B12F9E563CE125D4E255787CA1C0F0723C413D397F251E9FB30C8FCE + 9425F0223C3663111E376BE0857864CABC61D077287D78F21C3C30899A484D98 + 6DE0F7BE09B3ACECC1C9732DD5F6FD13553707F78C998DBBDE9969F057BAE7DD + 39B873D474DCF6F654DC316A06EE7E7716EE1A3DDD248B61B98B56DC60C505B6 + 58C0AF8FB758C082BD77BE3ADE74D7EBEF3B65D46D82BFD49F0701B034DACB0A + F86DFCF9F9774CB73D27E39251B84DDB2CD733C63FCB12F8E96BAD7FBDE1AF3D + 8F74DB3CE158FEBA32E8EB01BFDED0577243DEB95E120582057E072D823DD057 + D23355EF6DA7EC79DCA0E7B2F73900D87D8E6B1AB4FE1D02C023491058A99E17 + BBCF8EBD35F46CF95AE03B5CD703C02EFC35E999B8C53B759E8D7B4BCFC9BD35 + 521B93974BE86BCABCB880EBF2F97A205816BB83F07604C07B4DDD08F5233105 + B7EC1FD548ACE4DFABA131B8FCEA1FD5C8BCEB5A76F6BBF71B2EB71FB72FEFED + 9134D27EC3CBDDFCAF69F87EC3E5D65FAFCDF0F2E110F8265900AB8D98A8D3C6 + BD26EEF5B9E9A67BCCD5B33141971FBA00F8BFD302F897A0F7D611CA1C0DEEE3 + 7D325E27F5DBBACE051B2E998B0FD2EEDFF965F9EFD6AF7D01A8E16F3FB8DB23 + B51DFC003D1A0E7D5D5DFBE3FCF7E8F7FE2179B7F1DEC7BB7C248DD4D6D1C87F + D0C3E5C0DD91CB87A46D958F741C47DE7FD8BF90D71FBEB5F502C02EFC95B4ED + 0D8007F39E9BD1F01B98F2FF2C0036A03B0C020BFA1AF8F54060B7DCD550B0FF + A11BBAC504BE97E51E5720AE1BE85BEE7F0A37DFAF89C3B51078B86C82616077 + 0800BBC0D73B75CA9DB7D4047B35A991DCB7D894D7C4C789F92B8B60C78A58A9 + E3F6790800BB657F9285F0E38E35F0AD4F72B2E49970B9932E6FD7D07751B7BF + 24F0EBE8764F5C60815F9BDC09067BE2820C59018FB5F41A57D002BFCCDFF9FA + 7B8312F015FCBD7FD44483C182C0F7BC43BDFB3EEE1BE358FECA2DB400F0DDD4 + 3DCC0F82E071D3F0E00459FFCE360BE0872672F2FBFE0CD3C3936673E23C0F4F + CD5E8467E62EC2D3F31619FC7D62F63CE6179B05F00B8B7DF02227EA2F2EF271 + 403027E9CF2DF5B5C9BF20F06BFEA1782B642D46ADE5C22422925A8FB7C2D7E3 + B5B0082E4AF4766A14DE584F4546E1D5F5EBF14614173D099B30212D1D93D232 + 306E53121749F17837369E8BA0444C4ACFE0422B0DA39292303E2BD3E2EF2EA9 + ABC39CB23283BFD30A0A31BDB808930B0A30B3940BB6F27273C93CAD586F41D7 + 7221DB8445D4BCFA5AB3FA9D2BA0DAAC05673366D7D670815782D98AB15B9C89 + 792539985F9A8F795C344ECD4AC684D4784CE3226D3117814B6A4AB0880BD2E5 + AE9BAAB63A2E962BB1B0BA084B6B4B11D05E0BBFB67AB6A9C482FA1A040E6C46 + F8EE1DF0EFEF82DF9656F8F6359A7C7AF4F6760D17F095ECB3108BABF2B9A867 + 1F5C6CCFAB2CC02C8D850BDA793C96E22CC90D95D2595C84CFA92EC4A286722C + 69AEC2A2A6729E4B316654E4622617C18BB9289625EFC2C6728BDFBBB89E8BF5 + 6A2E90AB8A585783553D6DF0E96CC28AD65AACEE6C40504F0B02BA9AE0D3528D + A50D655852AFB7C50B30AF2A8FE7900FDFF632ACD9DA84A8DD1D58B7B30D41BD + 553CF73CB6CDC6EAB6627349A6ED95CD0516D7774533CFA32117ABDA04708BE1 + DF5688D5AD05F069CEC30AEEB3BC3E0B8BAB53B1A032110B2B93B0BC2E1DBE4D + 5958A5871D4C7DDB72B0AA2B9F63637F6D791C570E963764729F34B64FE635CE + 806F732E7C9A72D957365634E5709F228EB3087E3DE508D8528580816A046F57 + CCDE3AA68D08DED68010A672E31CBAB319815BEB4DCA0B522A66AF5F7F03D508 + FF6D2D26BF6DCD540BFB68661F1D542782B6B7C37F7B2B0277F03AEC6C47E0CE + 4EAA1D019E32E5430CF87621784F175CF8EBA887EA45E83EA6ACF357FB5D6AD7 + CD7602BEEC8B1214F6DFA1E3B461F58E56ACDADA8CD5941FB70378FD0376B5C2 + 7F27C728F17CFCB5BD836D149398F2E3B94AFE3C5F6F05EABC3D1218F6DFD1C8 + FE9A06B79586EE6B47D83E41EB16047B20AF006F088FE5825F6D5B9927AFEB27 + E02B29EFCAB106EEC05A2AE220EBA935073A1046AD615E00F81AEBDFC33D8332 + 77CF5E16BBB2F28D3EECC404565C612756AF638512CDBCAC7C9597CCEDE271C1 + E26E4B659DE2A4B2C6911B464702BC52CA05C73AD71EEC5266C5EBB1F04D546C + 3E7B90BBCD2457CB492C13F895158ED2646ECB6573CA79D56FE5B6F6DBC2F6FD + A6E4F303EC7F1BD22FCA427707322E4A82BF0348635B415B59E9A69DDB623130 + E54238EFC3DDC8BAB21D991FEC306B5E07F4EAE1B58EB315A917B7214D62DF52 + E6A51DC8BAB49362CAFE65DD9BC1F1649E17F4DD65566BF9B2E065BEE0EA5E14 + 7DB49FE93E7347AA7895851F511F1F1C04BE52F1A7471CA8C1F2820FF6A298FB + 992B56E92359AFED43C9D5DDC8BFDC8FDC0B7DC8BF24E02BB7CF9B4DF91736A3 + 40D0F7629FA9E492A3DC53EDC83EDE6C6E9D65C12B159D6945C1A966545CE846 + ED8703A8FF6807EAAE6E43ED075BD1FCE96ED47FB21355CC57AB8C75CA0B0657 + 5CDECC7CBFA9F6A3AD68FC6C3B6A3ED88CB2B31D9E3E9B5074BA091517BB5073 + A51765E73A9077A21105A75BADEF868F77B1FF3D68FD621F5ABFDC8F96AF0EA2 + E59B2368FCFA081ABE3A8C7A59187E7994F9E3A8FDEA28AA65E12BF0FBC521D4 + 7E730CF5DF9D40D30FA74C75DCAE62B954CDFD05A65CD5493F782C150DFE3A00 + B8E61BC54E3D63AAFD56969267CC3A5756B70265B2F69585AE00B02C739B7F3C + 8D8E9F2F0CAAF3AF828D6A7FDA01BD026A3F9D45DBCFE74DADACD3B652DBA65A + 7E3CEB4063C1E4EF4EA155EDDD7A59E3720C02751A8B40B064969B3F69FF8B68 + FFF9128F77DEC62708AC6D41CDB6BF682C43529940B0F6AF11BCE4319A38DE96 + BFB1FDDF2F5B6AFAF9B207FA3AB172A5269E9BA478B8B22E75C0AE00EF25038D + 72876CE2789A341E73B1AC3A17EC5EB4B8B9F53F5D1CCC37B2DECAB8EDF44731 + DF2400AD58BCCCCB557503FB7025A86B751C8B8ED7CCF14BADFF2157CE02BF1F + A0FBFFFA103DFFEF8FD0F1BFAEA09DE7D32988FBD72B960AE476FFFD2A7AFEE3 + 2ABAFEFE013F33B97A1648BF64DB82BF068059DFC9ED4EB6D73EA6BF53FF7999 + FD52FFC96B26F0CC6B29F7DC0D3FF01CBFBB60DF1F49F0576EB71DEB716D9F65 + FD1983A102BFFACE3956E1FA3C782E26F525CBF35366696C80F8DB138EC5B7A0 + AEACCC29817C8DB1EBEF1FF27BC3CFE26B1EF32BF6CD6308CCD7F33B2337D5BA + 7EAE1A784D05CC65313DF8590AAEF33BAF171B6471DCC2EFAC2CC49BF93D93B5 + B1B6ED850683C18E35B220B12C92AD8EDFFD66FE7E1A04AA5927705DFBF5518E + E5A85902CBAA5E56CE02E51A4F1D3F4BC1FF6A8E51AEAAE5AADB752F2E37DA35 + 127F5BB26496AB734996F8B2BC37F7E8B270FEFA34AA3EE7EF5A56CA5F0B96CB + 25FB6103D0A59F1F40A580B460EFA74E4C62415B4965E5B256FEFCA059639730 + 95E431A080FF9DFAFF150036D8FB89037F65012C957F7C88DBEC8BFD1553859F + 09FECABA573A847CD6B9F0572EA5CD3D35F7CFFB44F1C505800599F71B24CEFF + F830F2F4320F8FA154F0D7DDB632F6E5942BCEB1038325B30C361D32F7CA92B6 + 0D0EB30F8BE5EB81BE2E3C762D810D020BD69EDBC57BA1DC14EBDEB50719B2F2 + BDB8CBA418F3726B9D7E791FEF637B1D80CBFD0417051C057595179094CCEA96 + FD29AFF274C52EE698323ED86FED532E281C028F715930FA80C158B9344E6359 + FA45074A0BDA6A4C763C1E2B8BE7902D4F17BC6FEAC5AB1C8E51E1113278EF4C + 3DABFB7ABFDDB3379DEEE3FCA203F127FBECFE2E8F1D291A935C229F61DBF3DC + CFCE7B2FC72830EA0060C16003C04C3507D14B690EDCED438C5E6EE33C47739A + 58BDBCA6F98B3C9550567688F547BA1075B09D739E369677726ED385D8231DD8 + 70A01551FB1DABD4680161CEA164456C71854D3AD73D1CC32EC49CD8898D67F6 + 22FEEC6E03BFF1942C7D056E379EDA81D893CA3B56CD02BF82C26A972037D21E + 48ACEB6DD7DFF2BAD6FB90C0FE2CFEB1DAC895B5F639B50D719C7FC519E0EE43 + 1CCF318EE72857D1299C7BA5700EB64910786F33A277376283B44BAA43D4CE1A + 6CD8598B98DD754C6BB06EA01C6BFA8A112C0F3EAD5C1BB470CEDF9806DFFA54 + AC6E4AC3AAC614ACAC89C7CACA8D26DF2AC160C1DE18AC288FC4F2D275A6C585 + E1589C1F8AA54CE5365A16C4BE4C7DCB22B1AA3C0A2B4B22B09C753EA5EB1158 + 1D8BA09A38F857466365710496E685B17C1DC25A13B1A13F07B15BE5B6BA001B + B696207A6725CFA3011BF9D96CE2E79AC8F34B3CB919498ACFCCBC2CA1E3F999 + 259FEC463ABF3F2927BAB1E9501BE2F7731DB5A31A61DD45086CCB855F532656 + 3770FD5393CCF348842FD743ABAB92E1579B8A60AE85D6761522AC3DCFDA2CAD + D884C5E509A6B9251BA9587325BD88ED05931756245B6C619F46ADB7CACCDA78 + 7E6526E69472AD5595C5B59CE06F3A6695A771FD9881B955695C67A662716D26 + 966A4D569BC5B55F019635169B96D473EDC835E442AE09E7C80D734521D7C885 + 98CDB5E46CAE816773AD3BA79EEBDED6462CED6AC18ADE762CED68C2B2AE66AE + 1BB671CEBF876BA11D08DBBB1391470E20FAF821441D3B80F547F763FDF17D08 + 3BB8137E3BFBB07C732B9676376249671D16B75563B1D6C3AD9516CAC805C04B + B88E5DC2F5F542AE71057F67728D3E9BF9F975255C03B36D472D56707FB98F9E + C335F4ACAA025B0B0B0ACFA9C8E775A2CAF230A3201D5315FFB730CBC0B05C48 + 4F2C48C3B452B6AB29C7B2B626AE8F3BEC45EDC5CD5CC337D563692BCF89E50B + B976D63A7AB6DA1673AD5D9881C9D949189FB211639262307AD3068C8E8BC4DB + 31117873433846C5AEC7184A318165F1FB4E5438DEE5B680EFA494788C8D6739 + EB666426616E7E06DEDDB80EA3A223308665A3B56FFC06BC9F9C6010784C7C0C + EB3760544C14DE8A92A57004DE8E5A672EA1058125B985161816EC7D731DEBD6 + AF33002C182C2916F08B810166F9FBBCBFBF4996BF02C68A0DFC4A40205EF60B + C48BAB03A840B3F055CC5F59025F037DA9677D57E3396E0B0E3FB574259EA69E + 5BBE0A2FAE0C1884BF7205FDF4E215789E7D0800CB0AF8E945CB0D003FB57039 + 9ED7B3A2E5B2165E8DA716FB9A1C082C17D2B22076EAB4FDF88215A62717FA58 + ECE0A7E6AFC0A3B316E391998BF0F8CCC57872F6524B1F99B6809A8F87A7CEF7 + E42541E1F97868CA3C83BC4A552E77D1CADF3F71EEA01C20CCFAA90B71FFF879 + 66F17BCFE839B86F0CCB262EC1431316E0CED13370FBA869B86FEC1CDC357A1A + 6E7F6732EE78670ADB4EC15DA326E1EE7726E18EB71C57D077BEA6906EEFE3EE + 37C7E3AE37C6D9B6370036F8EB710BED486EA21D0311F7D9A0775EA981612F2B + E0E100D885C0DE00F85659017BE0AF3700F686BE2A17EC95318CB75CF07BE32F + 40EF7326EF322BA76EB8EF197B6E3B68054CD9B69EE57AE4C4031E7AFEEBCA71 + 09ED80E0EB0160F739F43F0A8015BFD405C0AA772D80873F1FF77E4EFE6BF5D7 + E83A00D805BEFF2D00F84FD7A9A386F30477FB9FD148BCE4DF2BEF310CB1AC7F + 4423F2AE1B6EA36E1F6468BF7BBFE17221ABCBE2BCF323B5B99EBCDB7AEF7B3D + 0DDF6FB8DCFA91DA8C546E657F72E4B2D041663AD4CEFBFA08FEDEE872416FCB + 5F57DE10F7DFAD5FC0DFC193F92D8D70C146D22000FE9D5F947F93DC0FC71BFE + 9AB9B647DA76FC780F1BF7F00F8F1A0E7FED83BEE6C7F9EFD175FF9086B973F6 + 6EE3BD8F77F9F53452FB91FE9C7F4B23C1606F00FC6B20D8FB4FFCD7E4B477FA + 1D84BFDC7601B0ABC13A69F84DC9A3A19B1A6FA01EC03B92BCEBBD01B0E44260 + C980AF0702DF7297130FD83B0EF04D9405FA1F146FFC9C00B8377DF7862FD924 + E05EC50276260E163B8213865FC060D50B003FF84B00EC0D7F056F25D74D892B + A75C6E9E5F31AB60B985BEFD494D8634F171EA87E20A6B7F070A9B5BE8C75FC2 + 9F9F7C19B73DC509D4D3AF71A2F5FAE064CB05C0F6961E276972096DB1815F1E + 6DA0572E9F1503F8AE57DE732675AA1B0E8059E76E0BFA0EB70496F5AFC15ECA + 05C0F7CB35B440F0E809B8EF3D96BD37D9AC8065017CF73BEFE32E4A10F83E8F + 3BE807DE9F81872639F17F1FD65B901367E1DEF1D371AFC0B05C454F9F872767 + 2DC4537316E1A9B90BF0E49C794CE733953BE8457866DE523CCF89F88B9C90BF + C049FA330B96E3B925CCEB8D4F9F007305FD667038DE59C3858D2C79057DC322 + F06AE85A2E52B8508A8EC13B311BF1766C2CDE8C8EC65B31B11897946CF05716 + C0EFC56FC2FB4929989C9A8E49D4C4342A33131373B97033C85B8A791515985E + 5084895939989A5F84198AE15BC83AC5D0ADAD33D7C9F3AAABB958ACC4AC4A59 + D4D618045ED2DA8CF98DF5985B4735346216CB675539E07541450E16EBED622E + 7A97561519049E9E938CA9594998C505E4022E2A17D6146131179F8B6A0BB1B0 + B698C72AC49CF25C2CE44273452DCBB5502E2DE622BB1C4BB8885CD1DE82E51D + 72B72C17C3CD58DDD7E4BC5D5D5F62D282766A5E0A661767614E590E66946462 + 1A35AB9A0B5EF63DAFB60C0BEBABB91896CBE95A2CE0C27C5E9D13DFC9DE78E6 + 98A695729CDC570B764160BD153DBBBA100BCCA556A92D5EE7D75560494B0DAF + 4B196671213BB72C174BD86EB96032533B7671066696A473FCCEDBE4CB5B8A2C + 86AF62F9AE6A2F33D0AB8706F3CA53782D529D585695A958CA3201D9C5D5E95C + 6C27634165321655A772E19E6EF0565A569B616FAE2FA94E31E94D780160B3F0 + 6D7400F0AAB65CF8741460656B1E7C256EAF6CCAC5B23A81E30C1E27CB40B22F + CB54BE5C60B9B500CBDB8BE0D35D0E3F59246FAB47F04E49F17B9B10BC5DB17A + 5BB0766F874979415F73D1BC4756AD4D08D8DA04FF816618E0DDD56170366867 + 17020CECCA9AB79BDBDDB62D38AC7602B582BC9280AF5C3AAF163CF6EAC3E0AF + EA7675B2AED5EA04721D002CE8EB7105CD6DFF1DAC53CA3682CF82BFBE034D06 + 8025FFEDB2EC6D61EAC05E83BB3B1AB17A7B03DBD6B34D3DFC287F8F02B6350C + 4AE72B886BB07BA0CE6475EC6335F392A0AFAC7E0583D5364CB09765413B1BB9 + 5FA35DA7D0DD02BF2D08D9A5073D02C06D66C12BD7CFCAABCC71032D0B60C772 + D8EA0F3800D8E0EFA12E4408FEBAF17F0FF70C02DF1859EC1EDBE2493DAEA03D + F0D7AC80290160C9C0EF890107FC7A1EA01A1C3EA9F2CD26C5B873AD76ADECE4 + E66B00B0E3CA596DA8B34EBBA107B802C02A53FCDFCD489605902C8665F9CBF6 + C94C53B89DCAFDE4BA5912A04D3ABB05C9E7B620F5C200E49259AE99D32E6E45 + 3A25D89B69A9E0EF3664CB7D33F36967FB907E6E33CB069071B19FF5FD486779 + 2ACB52CE52E7D9A7D2337D3C8EEAB7228BFD644B977798C56FCE955D06797304 + 81396E59F9967F7E04B208145090F5AFE043C92707982A36A52082DC151F42D1 + A782BE873D3A8412C5E814D850B9F63160B10FE51FEF4785D20FF7A2582EA12F + F5A3E0F2000AAF6CB77CCEB95E1470FC651F6C43E9957E945CDA82E20B7D28A5 + CA2E6D46D1B91E149CEE44E1A90E94325FCD7D2B592E8BE0E233ED2867BEE6C3 + 6DA8FF7827B50BCD5FEC43FD677B50C7ED864F77A3917981604160C1E0BA8F77 + 709B7996357CB40D351FC80AB81B6554C5A56E547FD08BDA0FB7B0AF6DDCEEE5 + 38BAD87E004DECABF5F3FD68FFFA203ABE3D8CD6AF0EA0F10BEACB8368FEE630 + 5ABE3B8E96EF4FA05990F79B13669158FBE51173475BFDF921C73AF19BE36864 + 7DC3B72750F5C541B34AAC903BDB6F8FA1E1C75368FCE93453474D7F396BAA63 + 9FB2BE14406B90D5E677E7B8FF39D40BDAC9FA57B0973200EC81BFB2AA6DF9F1 + 0CDA7E3E879EBF5F42F7DF2FA2F3AF0E00963B6673B5FC832C37CFA2FDE7F356 + D741B5FFF5BC599176FCED02BAFEE312B7E5D6D981C9668529B8C6FA56D6B7FD + EDE2203416041620742D71956F62BEF5AFECE36707F8CA85B440A160AFCA64B1 + 2AB9F5AA531B7343CDFE64012C1028802AF0EB82C1C69F2EF2FA0810EA581779 + 1C0F44A404696BFF72C99159FF3A10D7018DECCFC677D162D90A046B1F1718BB + B0D79503812F7BCA1D985CA7E3B1ACE56F8E8B66F523E82C092AABDCE95BA0D8 + 19BBE075C7FFBA8AAEFFD787E8FCBF3E44EFFFE7136CFEBF3F43CFFFF511BA59 + DE2BFDA7A0EF15A61F60F3FFFA107D94CABA0579D9AF24E82B30AC3217FA76FE + 4D75BC963CA6ACADF5D9B5FD9D9FA33EBBFFB8CCCFE80AF50175152D3F7F6010 + B8F65B7E871407F8FB0BFCDEF15A7FC3EFD3F79E1708BE73BF7BFCFEB04FC1F5 + 067E2E0E0C1648763E1F816001607D2FDA7E9205F759C83A5C2F1FE8BBD7C66B + 25CB6601E0C66FD9CF0F4E5FCEB5713E475DB37A7D8E4CF55934F17B2A99D5B7 + DAF07BA46339EEC88F1BC8D5F164D9DEC832D7C5B85E4A1008765C519FE77511 + 303F6F00B895BF1F7D87DB586EDFE31F9CFDD59FE06FBD5EC6D0EF8CBF937A9E + B3ACBA156FBA86FDD5E9BBC8F372E229F3FBC0715A9C659E57F3CFFCAC29D7FD + B8A46BD8A01733BEE26FF5ABE31CEB0983D27A11A4F6AB63F61BD775ABE6D82B + F81F5BFDC531969FB0F8C655DC2EFF8CFF9FAEA780AF0EA3EAEB2396977BF812 + FEA73AF0F7102AF87FAB54F0579E154A3FD40B3607ACBEE8D303C8FF741FF23E + DD8BFC4FF6A280FFC9059F08FEEEE7FFF741FE2F539F38005892F5B1C0B0E0AF + 53E6805EB9A52EFCF4E8D0F6C7470C0E4BB91F0A1A3B00385B56C057E55EFA28 + 8F770C8A199CF3D11107103395B258EF02DF6CB328F602C7ACB37AD725F4855D + A6ACAB4E0CE3F4CB4A65A1EC895FCC63675D3DC07DB5CF4183C08A072C97CEA9 + 9765C9EAB86D16B47520A4A354B96B16749575AF590AABCC01C0992AE33E4E4C + 5DD7BAD8916070A620B1AC5ACFEDE0BD555E3774DF74C06FF2E9CD483CD9CDFB + 7C9FDDB3A5845372DDDC69F77401E1748569505B8F65B2333E591CEB2532BD2C + A6F8BC2C974710B6D15C4296BF3147D8CF5141DF2E73E5AC7CCCD16EE6BBCD92 + 37EE58AF2986F51B39378AD50B6C473BEDD8F1277B380E59FBB621726F23D6CB + BDF2FE56441E6833AB60CD676471EDC4FFDDC57417E73DDB117D6207624EED42 + C2B93D4838BB13B1A7B721FAD4564473BEB4E1F800EB07107B92E778DE01EBB2 + 08967B6769D359F627E8CE6BA87D1DCB61B5D98BB833BB1127AB62B99E3EEF89 + 837C7A27B5836351880C81F31D909BEB24E693153FF9D400924F700EC339580A + 9578AC07714738F643ED883FD08A8DFB5B10C7542E97A376D562EDD60A846FAB + C0DA01AABF1C6BB69422B4B708A13D85501898D54DE958DD988280964C04B5E5 + 20B03513014DA9086C4A46507312FCEA366259F97A2C295E8B95E551F0AFDD88 + 80BA58F85545C3AF7203562B8E7079A46D07D6C621B03ACE5C4BFB9646625559 + 147C4AD663694118969684B1CF5884772523AC3D1541AD2908EAC844587F21D6 + EE28C7BA1D3558BFBB01D1079A1073A815D107DBA91644EEAFE767558F0D071A + 10436D3C48B12C7A97007719D60F94607D7F1922FA8AD86F36425B3210DAEC68 + 6D470E22BB8B10BDB914710395169FD8AF3E150B4AA331B73092690CD799495C + 4BA5711D958C85150998579A802555A958D55408BFB652F8305D5C93CD755A2A + E6727D36BF9AEBC932AE31CB9331BF260DF3AB52B0A82603ABB96E92F7A4D56D + 255C7B556075471556B4E8E5E122AA044BF52231D7870BEA25590397611EF373 + 1BAB31B3A60C3364195CCBF566632DE63554713D5D81A50A2DD4D389A51D5CF7 + 76722DB46D0BD61EDCC379FE3E441ED98BF547F620E2F06EF8EFEAC3828E5ACC + E47166724D3DB7816BE7568F172BF344558E395CDFCE2AC9E2F951E5395CB7F2 + 3C989F53C5B5714DBEC539F6EDE11AA6AF194BDAAA39B6128EA314F3956A6DCE + 75AD5C602FE2FA771ED7BB02BFB34AB9CE657E1AA5B5F39C1AC7C259A1997CBA + 3BE0BFA517AB7BBAB896E65A5B9EBEDA9AB0ACB58ED783E3292DC28CFC6C4CCE + 4EC6C4CC4D783F7513DE4B8DC398D48D1897168F3189B178276E03DE4B8CC1D8 + 4D1BA818B3E87D33728D01DEC9A98A171C6FF057B17F27A7C4615A4622DE8D5D + 87B722C3AC8DE0F1D84DD198989A6872AD80DF89653FD16C17BD1EA363A2307A + C37A2806B0AC800580E5267A4C2C8FBD51EEA837E2DD185925471B147E6DCD1A + BC1C1C8C6757AF36F8FB4240806DBF121A8A578343F01AF3AF0A02FB07E295C0 + 109320B02C81057B5F90BB67C161BF403CE7EB876757AE1A04C04F2E59816797 + F99AF5AFE0EF938B56E08985CBF1E4C265067D9F5DE263C60172FFACEDA717AD + B4BC5C453FC9F4D1852BF038CB04791F9BBFDC52C16059013FB9D8877DAD343D + 493D2D0BE1F96C377B894160A54F289DB1C8C0AEE0AFC0EE200416E89D34D7AC + 7C65DDABD4AC86672E669B8583F057E0F7FE8973701FF38F28CEF094257860DC + 02DC3B661E1E7C7F319E9FE38FE7E6AC32087CF7BB33F1F004EE3B6EAEC50436 + 003C6A2AEE1E351977BE3DD12437D0F7BE49BD3589924B6807084BB204567AFB + 2B6307AD841DE9B99F5C44CB6864B47908D43342A5D2ADCFBF8D3F3EFB9601E0 + 3B9E7BDBC2CF19E07DF2DAD8BF2E041E84C3067A1D77CFDE3217D0AC73AD7C5D + 4BDFE1F28E03EC1ACE0C07BFAE1492EF0F02BCF73DFD0B831D3DB3554CE0E100 + D81B047B0360C15ED7FDB32B6D0F3D871E02C037DDFE906D7B97B9F057CFBA05 + 80A56BAC7FBD00F0F59F915F5FD7EC63CFDBBDB6252F2B606F00EC9679EBFF0F + 80BDE53D8621F8E8CDB386EB1AAEE59177B9B5BBF176DC2000EC5D364C6EDDAF + CA85A9DE40D53B3FBC8DB7466AEF9DFF350DEF63B8DC7AEF76C3CBAED16DD448 + 9C94D769846B61D7483CD01BFC9AD52FF507D65B7E0430FBAFD08DC3B6AF81BE + DEBAE6C47E4DBC20C34EF897F23AF1FF2108EC7E51AFF76370CB87D78FD4D7E0 + 0738784EFF7B02E07FF44FE9B7F6F3AE1F59BFFC83FE67F55B20D8FEB0BDFFF8 + 7F4DECEB26DE646E949857FF7643D1CDC79567DB60B06E40925BE77D73A29C1B + D9C81058657FF8936E9E4EBD49755E376041601704BBD0D71BFE9A05306FDADE + 6F72C912D8DC407BA41BBD77FC076732E0ED2AE449731D62D2B6A02F2597D036 + B1F0E4DD6D4B0DFE3A16BBB73EEAC4AD702728CAEBCD3681DD3F3DA678BF7277 + E26C2B2F29EF426017222B559FB73C4C3DF21C27552FE0CF4FBE84DB9EE23E4F + 4B9C4C7132A649D91DCFBFC5F40DFC497098E99D2FBE8D3B5F7AC720B000B059 + F6BEF69E5906FFF945967B00B063FDEBC05F83C5CC0BF82AE6EF83EF4EC603A3 + 2799047A950AFE0EC5FE9DC836D4984978E8BDC978C8E3EE5916C0DE005856C0 + 2A7FC863FD2BD7CF729B233D3469361E507CE02973F0D8F405787CC6023C3A63 + 1ED3B9786ACE023C396F31B5044FC92A78EE623C376F199EE7E4FD39C57661FA + FCD25578898B80977D03F1EAAA20BCEE1F8AD703C3F066F01A83C1AF042816F0 + 5ABCB561231735495CE4A4606C620AC6303F2A368192B56F12C66D4AC63B31F1 + 5636292D1393D3B3303E35DD62FE4E2D2EC2A4C2424C2DE2424C16BFB9B99894 + 9D83E985B2022EC0A47CB629127CAD36083CABBC12938B4A30A5980BD5CA6A2E + F20480DB30BFA919731AFEBFECFD75941D4796F50D8FDBED6EB7DD66B6253393 + 6C59CCCC2A1508AAC45052958A99999999B94ACCCC8C1658922DB39B7B7AA667 + E6199EF9DE77ED6FEFC89BA52B596E989E1E78D6FBC75E1119111909376F6644 + FEF29C538689A5A51893978BD1D9A99C282761162795EE259C4073D2EB5EC049 + 697A1C46278661643CF7252E8493BF484CE284545F224FCA8EC594DC444C2F48 + C5D4EC044CCF4CC0B48C788C4908C798A4684E5C39B9CC56DCA0744C37D6B839 + F06A288367432966701BD3D8FFD43C1E4B52248671F23836298A13D4788C4E8B + C1B0D428EE530C2616A66246459E89E5BBB4AB192BD6B4626E632527E239964B + AA7C1EAB26E139EC27330963B3523149935D4E7E2717A59B09B026D2E66BEAB2 + 5C8CCF4EE4798AE5FE45F29842313699C7931E8BC919B1989012C5F230EE4F30 + 27B8813CD650B81426625E55163CCAD3303D8FC79B1DC9897804D3084CCD8934 + C0D7A3240E734B15AB2A06B3F452829A5920401C665E4C28CE95BE5477298CC2 + ACE268B897C5B1CF6478D5A6C2B32605732A1230BB2C16EEE571263EEFCC8A14 + 1E7332DCAB559F85797539985D9D098FCA74B857A451A9985B9B85F98D79F0A8 + CB861BEBE63431DF948B791D4558B84616ADA558B8B602F3BB2A0DDC5DD465C5 + E35D442DECAEC5026AD15AB95DAEC7F24D2D58BDB31B2B36B761C1BA06CC9745 + AE2C73D734606E570D66775433AD85574F3D3C7BEA98D77215E6C952976D17AE + 570CDE4678B2CFD98ABFDC2E2B62415D0B020BF40AFC9ABA0E59F03AA0AF4072 + 3BAF0727CD6799AC8E956ABF3DDB4B31AF4DC0B6040BBA545F062FB9AB7682BB + 5E94674731DB1539546864835E6901DB2C94257017DBB65B6DBCB88E34B7B5C0 + 486D64D92BD82B09F20AFA6A1D41759509FE0AF22E5D67A716F0950B676797CE + 36105ECEBC81BC5BE4DEB9EE26F96C66BAA99679AB4E6E99158FD77FA72C6664 + C52BD7CD8DF0DD5A6F5C284AB2E695566F6B306D64D52BEB5E0377E5F2795733 + 82F708F25AF1F78276B7206C6F8789E917A2B65C47EE1D83D82E707723EBE586 + B1C98A61C7FA50876B6763D97BB01D11B2E4DDD78A30F615BE47D0B715115C8E + 3A20F02B70DB8618B68911903DDA8584A39D883BD281D8A31D883B26C0CBE5C3 + 6C77A005D17AB14CC51F6C4302D789DBD76214CFBA98034DECBF1E11BB6B11B3 + BF11F1875A117BB015316C2FC51D68A5B80E9574A40BA9C77BA86EA49D5863DC + 322B7664FAF1751658A0146352CB79E7B6194BBEFCF33B9179721332051ACEB0 + 9E6D32CF6C45E6D96DC83AB71DB91FEE44EEF9EDC833DACAFC1663F926D8997B + C682BFB9673720EF0C756A1D724FAC45EEC9B5C83FD383EC131DC83AD68E8C23 + AD06F8169CEE41F159D69DEC42E6A116641E6E41EE5159EA2A766FBB89E59BBE + B71EA93CD6AC83CD06E8E609141F6840C6C12603762BCE6F42C585CDA8FC682B + CA2F6E46F9879B507E61134A152BF4D41AA3A233D4A94ECB1A98DB2838D28602 + 879BE8BC63DCDE29C50FCDB3D07C0000FFF4494441545E63547CAACBA8ECFC5A + D45C16F4DD82FA2BDB507F6D3B6A2E711BECBFF8CC7A947EB8D100E0EAAB3B51 + FBC96ED45CDB85AA2B3B502ECB60AAF4D236945CDC8AD2CBDB99DF8EF28FE49E + 7627E4C2B654EDAEED3620B6F6D303A8616A2C846DD8AAE58F65A9AB6559E71E + 42E5C78AA37B10729F2C0B5E3BA66FE915594AEE45D53559E81EA40E18ABDF86 + CF0508057DAD78BC0276B66CAB604BB2123E885A0700AE1334A6049105D22403 + 18A9CACFB82FD2A7FBA90390DB61B966AE968B634A79013CA5B2021608EE05C3 + 12F382C5BDC0589051D69E94E2D696B28D65412B606849104E2E836D17CFCA97 + F07CC84A53205120572A74A880658554C975813AAECF3259B4963235D08EEB1A + 78EB807BEACBF44F95085E7E720C659F1D47E9A727A8630E1DE77E1CE7F12A4E + EF49EE3FDB08025E3F82CA2F8EA1EA73D55916CA02A8553CF73A1FF55F9D40D3 + 37A78D5A7E7A166D3F3B87969F9CA14EA3F5A7A72CFDE414DA98B6FDF434DA25 + D6B5B1ACE51B016141E2E368E0361A784EA47AF6DDF0850030CF3BCFA17E5BF3 + 1B0B7AF277ACFFE28801C68D5F9E46C397A7F8BB9EE0B57584D789057F2581E0 + B28F797EE50AFAF23E145FDC63206595A0F357A7A078C4AA970571B189AD6C59 + BFEA5C1ACBE2ABFBB8AEC0B0959ADF5620F9B22C77F55182A0BEE0FE21EE17AF + 0D9E972AFDE6FC6DCB786D2BD6AF00B380AF65E12D706E2D57F0BA29E3356A2C + DF1DAED28D35EFE59DD42E037ECBB95CCABC2C802B79DDEB5CE89CE8DC182B75 + AE5FC3BE04816B048605831DD773C5277BB9BF94E24A73B98C690955CC735824 + 0B7D590FEBFFC77D2DE7B5615DBF962AB85CC3DFB956B198795D54096E1BE06D + C5B8B6AC95F531C66E23F35FE3FF46FB220BE6924B3BCDFE0B52EB980A3EDCC6 + 7BE816145CE0FD535E04149B58C7CC7B4411F3F9977621FFC24E038DA5C20FB7 + 23FF9CEEB3B200966B68A6CC2BEE70E6B98DC8A0D279EF4DE77D586559E67E6C + 294BF77ADECF65819CC37E94A6737D491FF648969BEAADA69D293BBD05A95C4F + B038551E208E6F42D2B1F526E6BB4200A4F37990C1FD529A2A2B61B64B314059 + 16C3CC6B1DD387C2057099525E7D2A9F7C72A351D2890D26B5CA3799BC092F70 + 6A2BFBE0BA542A9793655D2CA87C8CFBE100CD06220B0833156C8E55FC61133F + 58E1167A107B782DEBD7B27E1DE28E2A6482635996BC3C16B5893C608558883B + B286CFE6B594DC3FAF43F441B94CB63EAC32CFE6FD1C13ECA845C0D64A046EAB + 44E8EE7A44EE6D44E8AE3A5316B4AD1A91FB9A10C3B6FA282BFA6087E943A11B + 620E76B26D3BC277B7B38D9EF5EBAC98F927D69BED46EA8332792011D0DD5A0B + FF2DD508DA5E8FD09D8D08D9D180C0AD35C6923484DB0FDD5967B615B8B58A6D + B43F55062A066EAF4130EB0C00DE5006EFB525F05E538255EBCA0C7054DF81E6 + 83368E5FE496795F3782F674C27F37C5E5803DD4EE0EF8B1DE9FA924F82BADD6 + C7738AE9CBF6816C6BD707EDE57869FF1A633D1CB44FAEA2E51DA587F975EC73 + 1D563BE4A7E57D6B11C834683F75407058EDD6184BE13041E383964B7081E1B0 + FD72A12DF7D9DCC6CE561E17C75DBB2CCBE110EE4F98F685E3B8D59BEBA93AF8 + CBE5F5E66AF8AEAFC0EAF595F0DB5085D5EB2AB0BCB3004BDB72B1A2B3082BBB + 4BB0AC3D1F4B9B33B1BC291DAB9AD3E0DD9884853531F02A8FC482CA682C667E + 696D0C96D5C662455D1C56362662556302BC157398658BCAC2E1591808AFE260 + 93F72A0C32D6C153E3E76266AA273C8B7CB1B82C946D02E096BF1A734A43B180 + 7D7A560BA2C6637E4312E637A560614B1A16B76560517B1AE6B7A6C0B3299E8A + C3FC96042C6C4AC0226E7B597D0256B6A5C0BF23038154504B2A425BD310D894 + 047FD6053526334D4220FB0AEBCC4620FBF36D49C1126ECB4B2EAC2BA2B090F9 + 056531985B1001D7EC604CCF08306EA4DDF4616D7E14A66573EEA518C3396198 + 51C8B29268CC60DB9985119C83716E961F8AE9F961985DC5B95B5D123CEBB8DF + 8D0558D450843995D99CABA6C1AD3C0B5EF5C558D85CC1395899F17435B528D3 + 82AC65D9C61A5896C1B2089E56AA7045B99C736619A03A212713E3B3D23837CD + 8647ADC2EC346149770B9672FEBA746D2B96AC69E6DCA7C658FD4E94D56E21DB + 966470EE9763AC7E059FA7E4A5604C461C46244760744A04C667703E9C138F29 + 052998CAFD9B5C9C8AE9E59C1B360A38976106E7B8D32AF331A3AA00B36A8AE0 + 5EAB8FAFF5D1B462090B26733B9C6F6BEE2C17D6E38B7331A12417532B8A3049 + 70BB341F6EB59C733573BED6588B19A52C2FC885C22FCD2C2BC2D4C23C8CCD48 + C1F084180C89D69C3D0AC312E2302429D668784A0206C546E1BD88507C101986 + 0F2282F1415810DE0F5A8DB70D005E81A161014682BFB208961B68B9841E12EA + 67CA14037850985C4A07626864308644066140A83F060A2487F8B12FB6617FB2 + 0C1E12168CFE01BE7877F54ABC1FB01A8342023028341043C283581F6C40F0C0 + 003FBCB7CA1BEFAFF4C67B2B57E0CDC58BF0C6C24578C5D30B2F536F2C14FCA5 + 162F36E502C1EF2E5D8E7E0ADDB570295EF35A84D7E7B37EE112D62FC1AB5E0B + 2D003CDB8A07FC9CEB6C03815F749F6BAC84B5DC67869B81C0CF1AE0EB8EE794 + BACC3656C0B2087E81CB7DA7BB9976CFB05E00B88F40EF8CD9786A9A1B9E1204 + 1610A69EB6E130D779867941E03EB2089EEC8AA727CDA25CA819C61A58B0F789 + 917A8F25B7CE96B5AF40B02C7F8D6B67C500A61E9395EF1801E31906024B82C5 + 6AF7E090095C772A9E1C39030F0F9E6C62FD3E3C70325E98C8FD9E34178F0E99 + 6200B02C841F1C34010F0C543C616AC058968FC68FDF1B81FBDF97E7BDD178F0 + FD51B89FCB0FBC3BDCB885BEEF6D5B43713F5313275830F8ADC1D420DC67DEFD + D9107800EE35DE032DABE01FBDFC1E7EF8A2151F580058EF1965057CF70B6FE3 + 87CFBD851FF4BD1900DB50F8AE675EB780705F876B68A502C24E205880581058 + 56C0360836D6C00E0902EB9DA9ED1ADA58063B0C696ED59DB20CD63BDA872DF5 + BE93754060CB0AB8AF79CFEB0C807BA1AFF3FB60A7F8BF36FCB56503605B8A09 + 6CABB7DC017FCDBBEE7B2CFD85D10D00DC2B27B0FB2DB8EB9073FD4D6DEC77EC + 7AE77E6BD9ADEFE7ED32CA9911DC61BFE317E4959CB9809DB701B05D766B1B7B + D99177E60C7F8C6EC74B6E2DFBAF9233DBFA2EDD966DDD46A6FD9D7FDC3ABF53 + B785AA7FA26EB79DFFA81C7D0A7A1BC980D594DD43FD2E567A4B3F3749BCD069 + D9DE6F817593DE0266FF1429AEEF77E9CE5B96EFD08EFFCE83BA8D6E6AC39DBF + E9406F276758FA9F7411FD91BA15ECDAB2EB6F2D77AEFB96741C36C1FFBF0500 + DFA97373637DBBDCB9AFDFAF1B37E73F55B7C25F67007CD30DFC0FD01D7C5008 + 001B08ECF420E97DC8382DF7BA80BE0500DFFA70BB43F1829D21AF9304806D69 + F97BF75A0F5A67F86B03605901CBD2D758033B52EB21CDB654EF835B750FF0C1 + AF94FA9E1EEC121FF87AF85B5F827180F000070692236E84F972CCFE928CE9AD + 79E92E0E34142358A0F6FB8F73F9090B005B56BCD6976BDF7FE2250E602CCBDE + 1FC992576E9C0DF055F90DB7CFDFB604D60087DB7C940398C79FC3F79F7C013F + 78DA02C13F789A6DE412BAEFEBC61AF8C72FBE6324087CDF4B7207DD0F3F7EE9 + 1D0EE258F6EAFB0602DF67BEF2B300B05C430BF8DA2E9F6D082CF8FBD87BC3F1 + E480D1468F0D1889C7078CC2E3FD2D186CC7FEB52D811FED2F30AC7498B1FC7D + C811FF5700D886BFB605F013C3C7E3E9D1934DBCDF6727CCC473135DD0572E75 + C64DC3D3D433E364FDCBF229D4E4E9785EE934573CC781BB5C40BFC481FBCBD4 + 8B1CB09B728705B0DCFDBC367B3E5E97E62CC0EB9EFAB27411DEF25A8CB7172E + C7BBABFC3030381CC32262303C2A0EA3E39238918AE3C42614EFAC0E403F3F4E + 7AC2A3312C329613A1280724E6442B261E63D2D23122350D439293315CB03799 + EB26C41B083C263D0D235353308CF9A1895CCEC8E4243417E3B2B20DFC15041E + 9B9D8B2945259C34561A003CAD92694505C6E4E6B14D3226642770C29C80999C + 24BA099CEA2BE1047DE5EB8F9171C106020F8B09A4020C149E901E8D69398954 + 9281A7E313C2312E3E94FBEF8741D4B0B8108C4A8EE47EC7625C6682D1D48234 + 2A1DE3991F9B1667245752C363433136251A93B21238714EC068AE333A3B1AA3 + 73A2313E3FC9C4F45DD85E0B9F0D9D58D2D58419E5859CCCA673829AC5096A3E + A614E660547A3246A62572A29A85599CAC0B324FCD4F73B8ACCE356EBD64E13B + 2A210C63B46FB141181DCF3C8F715C128F27251C93D264E91C86D10981189318 + 84A959310602CF30F0370A33F263E151966C24AB5FD7E238CC2BD717F849985B + 1E67AC79DD4A638D75EFB49C504CC90A362ECA661546622635C3BC8088C65CB6 + F7AA4BC3BC6AB9868EE5362230B3380A2EEC6B46451A8F370373EB73B0B0A508 + 8BDB4BA912CC6F2E8067432EE6510BDB4AB0ACA70A8B58E7C536B2F295D5EB02 + 872BE345EBAAA058B60BD6D618AB59C5EE554C5E815701D9456B1AB0784333D5 + 0AFFBD1B9074E108224FECC6B26D9D98BFBEC9C88B6DE674D751B598DB5307CF + B50D543DE671FDB93DB52CAFC15C93AFC33C6A4E27973BAA318FE93CA673DAAB + 309B9ADB596DA0B0963DBB6AE0C93E3D59665C3BB795625E4B31E652F35AADF8 + C3060A9BF212CC6D2D645DC14D40D706BD5ECC9B63EEE23A060017624E4B7EAF + 6C106CC35E41DCF926653F1D5CDF5126202C00AC65015FC9B6F8B5F3CECB36F4 + 956C086C835FC91906AFDC2457860E00BCB996AA31A9CFD67A96C9FDB3DAB36C + 4B2D7CB7351899F8BCB286A1047D576F17ECB5966D00ECB7C36E63D529469FA0 + AF6DCD2B086C01604A5078574BAFF56EC86EC5CD6B46F801B5671D974376B7B0 + ACCDAAE7BA6182BC87DA1179B0CD80DE48F6A954EE21E5BED9B2C0ED44C2110B + F8C65389473B2995CB2D33D3631DAC6F47CCC11644EC6940F8AE7A4450D1BB1B + 8CC2B7D7227C472DE2F63521E16033A2F6B28E4A3ADC8AB4935D483E4EB1CFD4 + 63DD483FBE0669C7D630DF838C136B91796A1DD25476628DB11CCB3CBE9EE906 + E34EB4F0C31D26AEA44070CEA94DC6A568DED91D4CB721FBB465D52B20907D6E + 3B72CECB8DE94EE45FDC8DFC0BBB50705179AE7F719B71AF5AF8E136149CDBC2 + FED4CF3AE49C5C839CE33D54370A4FAF45E1991E641D6B45C6E166640BF272BF + 05660B4F7523CB61FD9BCDB490E5B2E2B5AD770B786EB2599779B0C9D4C9D2B7 + 80EBE4315F767E232A2E6EB17469AB492B99DA2A3EBF81DB586720B3D5A72C80 + 3B50C67CF999B528A3E4365AFB567A7EBDE9BBF4F41A54326DBEBE0B2D9FEE46 + D3F59D68F96C0FEA3EDA8EF2B31B512257D354B95C415FDE82DAABDB51FBF10E + 545FDD66962B3E627A758743BB50FDF11EA3DAEBFB50FFD9014B9F0BD41D627A + D8A8F6FA0154C97D2D655C330B625D9795ED7EC815F38D58B972976C81260124 + 0B10EF33ED6DA02B6B481BF0DA56BF025176BDB3D4CE569D5203CA0E5AC04CE0 + 4A7098FB5743C9FAD7C499D53EF118ACFC0D005CF3855C33CB2AD272076D602F + EB9CCB6EB626BD615D6AC47DB4AD448D95EE270E40CB63972B5EC986C096BBDE + FD96AEEC33693ED37CF699CF3AC5742D32F0977D38DCF6DAF0D882C10E08ECE8 + 5F754557985ED5BECAFAF7384AAE1F3310D9B20496EB670B00CBAAB68AA901BE + 3CEEAACF8F71D986BF96F56DED9714CF47E3D727D1FA93B34682BFED3F3FC7F4 + B481BD1D3F3F8BCE5F9C3352BEE31767D1F58BF3E8FEF97993DAE56A2B08DCF8 + C55123E525C1CE1A5D27FCFD950AE4ABCCB6286EFC4231834FB2FC047F5FEE37 + 8FA9FC131E870D7F992FE73196F298E556BCF8EA5E9E77592E1F4715D7A9FBEA + 0C9A7F7681E969B61734B600BA00B03C059838C394F92DF9FB295EB071DB7C45 + AEA1791E99CA3572D915EBFA951B70A95CBF3365AE09A6E68300FEE6CA5BD78C + 055D65852B4B5DC5CD96643DAF6B5E56BF02BFA597771A08AC655DE7F6F56C7F + E46064FF17F46103A50F1DCC39E335ACFF4C2565E2607F2AE99AB6F282D1155C + D782BFD67E19F138AB794DD5F2BCD5F29AA8E2356AC572962B6F9639FAD73E55 + B35FFD1606E4F3F7A8D3B9655FDA2FB5A9FC4496D7D6471CE53CBE32E91A8F8B + F70DA9EC1AEB786E8D3B6CFE3E3AB78AE75D22AB62DD6BCFF3FEAD0F6F2E6C43 + 1EEFC1B21ACEF9700BB22F6C46F687BC079FDF8CBC738A1DCCF42C971D9E1A72 + 3FDC8E02E3927ABB81C2BAC7AB5C9E1F5496717A93090590A50F8028C1E04C03 + 83999EDCC8678B151220E3FC3664F2F991717E3BD2B8BE6D4D9CCE7E32D50FCB + F5E150169F2F997C86182F12A76EC87893B001B4FA36DBD9CA3CDB6ABB6A7F7A + 1B5205804F6C46DA991DEC7F8765817C624B2F004E643EF1B824382CCB62CB5A + D8721DDD03C5D217D44DE4334FA03941E5877B0C0C4E3AB591CFDEB5C6C2379A + 8A3BDCCDE730DB5382B672D51BB5B705B107F53C6F34CFE6A06D55F0DB5486E0 + EDD508DB25105B85802D1508D85CCEB22A84EFAE635B3EA38D178E563ED32D2F + 1ED15C0EE758226237C705FBBAF8EC5F83C46316044E14943ED4CDF6ED08DDD5 + 88C06D02CA35ECAF0E613B1B10BAA31EC1DB6ACCB64276D418001CCC54FBA065 + 7F6E5B56A452E0D64A532738ECBBA1142BBB0BE0DD536C2C81FD39765ABDDDFA + 48CE7C0CB7A70301BB05775B8DC5AF806EC0AE76A692E06E9781BD017B3A7B41 + B09F03FC06D8C0D800E16E037203F7F620980AA202F6AE81DFDE75F0DDB39E52 + BAD62C1B57D307362078FF7A04EC5B0BFF3D3D5CCF02C872171DC4BE02D9BF24 + 38AD7D0C623E84DB920277B63395D7155930735D2E5B65ED66EC16B8AD1121DB + 388EDA2DE0DEC1DF88E33B594F735C1968D23AF86FE2EFB5BE14416B8BA842F8 + F5E46375678E915F779E8933ECDF998D80EE1C84ACCD47F8FA7C84ADCD435067 + 265636246049551496D7C660455D3C1696856376B60F5C53176066CA3CB8652D + 8567A11F3C8BFCE09ABB123358E75E1804CFCA28CCAB8C817B5904E757A19859 + 12060FF633AF210E9ECD0998DB180DF79A30B85547606E5D24BCAAC330BF3202 + 0B6B22B1BC2ED640E8A096648477A423A22D0D81DC0F9FF2282C2908C672F6B9 + 9ACBFECDC9086CCF807F5B3A7C5B53AD63E8CAC1B2EA04EE5338DCB203312B3B + 08EE02BA25DC5E51245C9877E17C4B9E9634EF72D707B69447690C6697714E57 + 14C1B95C085C4B23E05E19098F4A7D949B8E39E5199CD725732E968439D57958 + D1C971F95ACE935AAAE0EE00B3D335AF2CC9342E98A7C97B5465016656147039 + 1B131D90757C560AC6A7A760627606A614C8135621CF4319CF07E7438DF28855 + 62E6B4AE555A97FD6A7E5A99875915B98E796B2A262BAC913C5BA5C75251C62B + 954B6906CF65AE01C0130BF5A1702EDCEB4BE0D1588E59752598A6FE6A8A30A7 + A992F3E57A236D736679BE034E73BFCBD5A614532A4B30B9A21853AB4A30B1AC + 00934AB91F9545ECB308B35836BB96C75C5986E9C5859C531760B20070561A86 + 25C763587C3C46250950A7719E9E6E34262703C3529330302E0683E2A331343E + 0A43E3A8E8307C10224B601F0C09F7C790303F0C0AB22C80058565FD2B2BE051 + D1211816198411B17AE7108C41E1015C6F35FA07FB9AFCE0086B5940F98360FF + 5ECB607BD95809FBFBA05F00FBF5F735317F057E07333F34D00FFD5779E3EDA5 + 8BF1D692457867E952BCEF2DCF6C2BF0FA8285786DBE2C7F17E2ADC54BD06FD9 + 32BC2B00BC6819DE5CB4146F2F596EA4BC00F04B73BD7A2508FC82FBDCDE98C0 + 02C0B2089675EF0BAEAC632AF8ABF4550F4FBCA638C1AE73F1EC0C773C33DD02 + C082BF2F7ACC671F0BB9FE3CB32CE8FBE454573C31659605875DE6E069816059 + 0233ED3395EB39E2020B023F31CE82B84F8C9A82C746CAD2D7B2F6B5250B5F63 + E53BCCB2F87D7C94DC435B10D858FC8E9DC17653F0F0B0497894E9E3C3A7E3E1 + C1934CECDF07064C30CB4F8C988107074EC40303C7E3E1211371DF076370FF00 + 79CD9B8007078DC78303C6E0BEFEA34C2CE0873E188D87DE1F8507DE1B69AC81 + 0D00A64CFE5D475EF0F78D21F8F19B0371EF9B0370EF1B1FE0DED765FCE180BF + 0E1900FCCAFBB8FB2541E07771F70BEF18EF82773FFF76AF04816DAB5F19A228 + 7506C0A68C3200F86996DD463604B6F3CEFA2165BB8C565E10B8F79D2AA5BC59 + 96A1CDC337E0AF641BE8DC0E00DF0A7F6F5D16F8753620B2DF239BF7CD4ED0F7 + 563903602327006C5B08DF2AE7F637DE8FDF00BEB7CA6E731300569EEAF5C669 + BF9B7796FD8EDE49BD16C07F08007600DEDFA79B99C61FAE9B78CA7FB36C86F5 + BBF42DAEF51D726667A64CC0D2AE77CEFFA1B2E1E77FA66EB79DFFA81C7D5AF0 + D7D1B7BD9DEF39F1CF9B38A8744B3FBF4BBDFD39FABF1DC8FD73E83F64017CBB + 7AE94EC97100BF533740E97FB705F0EDEAFE68FD0F06C0BF4BB7BD59D9AEA105 + 806F53EF5CF6FB75F3CDF94F93D5E7ED4070EF0DFED69BFA772C0BFADE047F8D + AB044A79A7078B79B8E801643FA0EC078FE38176D383CBE46F7DB83D6ECA7ADB + 512ABF01801F370FCCBFD003530F4E2EDF799FF550B6A1AF01BF4E80D819022B + 2FF06B3FD46DE9417F97C335F45F380600DF7F58F1819FC50F047735987858D2 + 40A20FBEF7485F0E38987F84838A479FC55D82BE0EF87B27CB05827FF8940570 + 057D6D4BDE3B1D56BD02BC02BDCED0D7CE0B0E6B59BAA78F520B009B7E9FE076 + 9E621F9440F09D5CBE5396C1DCD60FFAB28FE7DFC0BD2FBE85FB5E7E1BF7BFF2 + 2E7EFC8ADC426BA0F62607706FE19E57DEB3C0AFD297FBE16E4A1058E0D786C0 + B6E5EF13FD47E2A981630CF4D5F2A3EF0DC763EF5B96C0160056ECDFA1C60AF8 + 0610661FEF0EC643EFB1DCB8851E6EE2012BF6EFE343C6E2A9E113F0D4C849E8 + 33762A9E9F340B2F72102DBD30C515CF4DE6807BC20CF4193F032F4C73C32B6E + 73F0B28B1B9E9FEA826765FD3B75165E98EE8E97677AE0C569EE2C637B0EC0FB + 32D5D79E72F5F3B2DB3CBCC281FEAB1CD0BFE1B5D802C0F397E0DDA52BD1DF27 + 100383E4D6280283C3A2303A3E09432363F1B68F3F5E5BB1CAA4A63C2EC900E2 + FE81A118C0B6C362E331223119C353520DFC1D96C27C62022758712C4FC0282E + 8F484AC490C4440C8A4BC0F0E4148C4C4DC398CC2C8CCFCDC7E8CC6C8C48D317 + CBF9985A5A862965E54C2B3881ADC4848262B6C9C664B96ECE4CC2C4F4644CC9 + 4CC5A4D4444EC842CDD7B623A2C3314A93B9C8600C08E5042E2290CB11189F12 + 8F09298918131F83919121181CCC8916276103980E890AC1F0B8482A8A13C618 + EE671C8627C461547222278FDC67E6F515F1A0B0504A718E39C9E5A47956511E + 6673623AA3340BA3336230342902A339091E9F9D64AC885D39D99E5698C3FDCD + 66AA4979215C4A8A31BD289FF585985353010F4E606770C23B532E9ECB38012F + 4EC3B4FC444CCD4BC0F4AC78B86472329D1567F2935323312E31181392423125 + 3DC2687C7288D1B4CC68CC2A4C846BB1DC30276176592AE65566188B60978258 + 4CCB89305F9CCF2B8DC7DCF204CC2A565938266706636A761866E4C9D5738C01 + C58ADF3BBD209AFB1C8D9965F1DCC724EE6B0A3C2AE5CA3901AE5C76AD4EE384 + 3E175EAD25BD717B97AFAD365AD2598E05ADC59CD4971AF8BB625D2DCB6BB164 + 8DAC7B057D1DDA50E7901563779162ED52F3057115C7B7A7160B7A14A3B7118B + D63761F5EEB5883EB91BC107361B4B5E815DCF3575C6BAD793EDE676D7513598 + 67E06DAD492D55634E47253CDACA31BBBD1C7399F7ECAC8289DFABF29652B82B + E633EBE7AABEBD029E1D55F06CAFC4BC3659FC0AF896C0B3B508739B0B1D2AC0 + 9CA67CCC967573432E95038FC65C2E534D7998D39C6FAC9DE7329DD75A08CFB6 + 22CC97CB6759F5B6685DB6513DF3AA33909875F39D52C53F562A2DEC2A75405E + B989164C669913EC5D2CA02ED7D0D4E26EE67BF87BACAF8137CFAFDC692FE7F9 + 5EC6DF46EEA24DFD9A2AACDAD400DFAD4DF0D9DC88951BEBA93A4AF180058A59 + BFA50EBEB2ECA5941704161016FC3560D861F16B015ECA017D25E3D65996BE3B + B8BCA3117EDBEA0C0416E815F80D3F2097CEAD08DED38490BDCD94AC821BB86E + 3D4C6CDEFD6D08E172C4BE260371C3F736216C4F2322F7B5208AF5918AFFC775 + F4A237E69040AE5C327720CE017CE5BE39FE600712E57AF988058013586789ED + 0FB4227A6F0322F7D623E66013DB53879A10B397DBD85987B06D3588DC518768 + EE4F2CB71BB7AF19C987DB9076AC1349475A91C47CFA892E649D598B8CD3D4A9 + 1E649F5E07C5DCCD3AB5DEC8B2FE520CDE7546262EAFACC34EB2ECE446C8AD68 + A12CCCCE6F67DD66649FDA6200B0E259E69EDB66A0400EEB64F12BCBE0C28BBB + 507889E9052D6F45C1875A5FDA86222E179DDB82A2B39B90776A2DB2B89F9947 + 3A907BA21B05DCB7BCE31DC83ADC6C2C7C0B65657B760D0A4FF72097E5D98715 + A3B7C340DA22D615392070E5879B50CDFECBCEAE47C919CBCD73E5A5CDDCC63A + E4F3B84B2F6C42E5E5EDD40EA3EA8F76A2F6EA2ED45CDD8D6AB96696F5EDF94D + 2839B701151736A2EAE246D47DB40D2D1FEF42D3D59DA867DEB875BEB099DA82 + 5AB62F3FCB6DB0DFE6EBBBD1706D07EAAF6E43C3156EE3DC46149FE436B9DD8A + F3EB51CDFDA8BBBA1DF56C537B651BB7C77E3E1288665FECB3FA0ACB3FD98DC6 + CF0EA0F1D38368FA8CFAFC10F30750FFA9AC160FA28EE58246B615AEEDAED986 + B2025B82491557F739C09140B0E57AD7E83ACB04FE1C96A13600B321B0DDDE06 + 83761B037BAFB35E40AA174A59EBC82AD9406796F5C260EEB700B0DC401BAB5F + A6BD12D8E2F6954AB675AFA0AEF2CE653608366E76AF297FD0005F03FD1CB2E2 + F6DE80BF06CEB2BD0D7F9517ECCDFB682F722FEFB9A12B5CA6F278AEF2AE0A12 + 0B22CA62D55A4FB201B0019ACA731B966BE923ECF7108AAE69BB47517AFD388A + 995A6EA50FA398A92C81CB057FBFA4BE38C9F3A1F4B8A5CF8F416EA0E576B94E + 7170BFB6246BDFF69F9F47FBCFCEA14350F797CAB3EC67A7D1F98BB3E8FEE539 + ACF9F587BD5AFB9B8B584F6DF8CB4B26D572F7AFCE1B0BE1D66F4E18C93A5856 + C3B20E6EF842BFBBF5118125CB3AD8580C733FEB1557F8ABD3DC874B68FBC565 + 0375057DCB748C3CE68A4F8FF3F739CE32C541B6CE6D21CF9F0071F34F2F62E3 + DF7F89EEBFFA18355F9CE2EF7794EB1DE279DC67AC73F52148DE851DBC1FEC36 + EE9A0B2EED3296DC553CB7B28E2DE77997AB637915B0DD801B08CC3E2C8B765E + 4B94EACA9CAE0D59139BBC80EBF5FDDCCFBDD41E9629BEAFE5BE5CD7BEFE1737 + DC403BFE0F94AE65FB7A566A59C0EBFFB39F650E50CEFF98CE97DD46A9AE73B9 + 42B7DCA13BAC85B51DAE57AD3A5DE7DC6FB932578CE61A0700AEE27911FC956B + 6EFD7F6CE97F2C10AFDF4AA95C763B4B96C1B24AD6F664712FEBFB6A0168DE33 + CA3FDEC9740FCFB93E8CD8CB6BD8F1C1C747ACBBCCF3AC38C317AD98E302E38A + 315EC8FB9FDC47E75DDC6C94CF7B982071F1F9CD2891BB7973BFE67DFAA25CD2 + CB05F5360385F3592615F09E5EC03EB365517C6A8371379DC7E7839E1179E7D5 + 6687794EA85C7036EB43B6E573C282BB5B9066A0AE80AF552EE91922CF12EAC3 + C06627E903A36CF6957D9A755C37CB3CAB98F21994A9D8C754B6FA13303E2910 + 2D90BC1D8A399FC6F234AE9BCA7565259C7A722B9205858FCB7A5856C69B2801 + DFF506B2261E57BCFC8DC6225996CC0947D61AA5B04C717D659D6B62E11F5298 + 052B2E7E943E02DBC9E7F19E163E6B5927CF1C7C5E47EDA1E4FE79571D427754 + 9B0FB3F4B15684522A92CFEBE8DD8D6CDB44C92B07C7077C7647729DC8DD7CC6 + EFEF60FFDD8861AAD8FE3154FC814E23D545EF91FBE306846EAB4508C70021B2 + FA651ABABD1661DB6BB8BD6A6EA7CE6C277C179705A03794625577015676E59B + 7CD8B66A337E08DC586ECA57AD2BB12C8A65252B8B61C59EDDDB8EA03DED08DC + 2D37CFAD16F095F52D252B5FC15EDBDA57E5B6D5AF0D832DF06B0160FFDD4AD5 + C6B2E8552A00ECBF6F3D56EFDB00DFBDEB295901AF47E0818D08DEBFD15802AF + 669BD5BB7BB07A8F253FAE17B87F0DD7EDC66A039B3B0D680EDADF8D6059156B + 1F58BE5A613C0489B96DE5FD76F258F675F158E446BB19413BF4F15E27C2F675 + 20746F1B82390EB42C87E5B9A583E3B876F3219FB188DE5E8710595A33AF542E + A7C354BEA91A7EEBCBE1C7F319B8B10C219B2A11BCA11CFE3D85F0E9CC858F5C + 4FB764C2B3321E738AC3B1B83C1CCBAB23B1C8B8950E81576918BC2AA231A72C + 86692C96376762456B2E1635A6637E7D2A3C6B534CBAA029035E2D1998DF9C8E + 05C62A381B8B5A92E059138E39A5C1701748CEF7C3EC7C7F781507625959187C + 2AA3B0AC2404F332566156C212B8A5796351712896564461714524165444603E + DB78B3BFA075FCFDD9B757491466E504C2353714734AA2E1559980C575A958DC + 908E654D9958C4BC574D22E6D726637E5522E60A041747C0253F18337303E156 + 10CCE30C856B01E76A3991989AA1F95F3866E60B0867F1D8CAE1C3B9CEE28662 + 785464732E9909F7F22CCEFF724C3AAB3413AE9CA7BA29F45061BAF980595EAC + A66427637A4E06A6E566604A0EE7D2D9299892976EBC50D9B17D35BF9D55C1F9 + 6C652EDCAB386FA92DE09C300F338BD3B95E12A6E5A5983E1576695C5A242666 + C7617E03E785ED5570A9E47C9D735AC50C9E555308F78672783455C2A5AE0CB3 + A8D9CDD5982FABE39E562C643AB7A98AF3A26ACEA16A30ADA614D31492A9AD81 + 73B2067834D7606A793E261565602A8F633AF7D195FB385B56CF0AB9A458C7A5 + F99855C939755911C6E7695E9E86B159595C8773F1AA324CAB2CC584D2028CC9 + CFC618D68FCBCD34E05B9ACAFCD8E4380C8E0836AE9DE5127A6088B41AEFFB2D + C707FECB31262604A36382313A2E0CA3E2155B38C8C0DE77FDBDF15EE02ACBA5 + B40308BF17E8635C41F7F35F85FE81AB312024000343030D00EEE7B78AF5BE18 + C0FC80D53E18448D0C0DC6107F3FBCB36431DE5E2CF8BB18EF79AFC0203F3FF4 + 5FB90AAF7BCDC74B73E7E155A66F2E5C84B7962CC51B4B96E075B9855EBC84CB + 16007E6BF132E31A5A96C0D2EBF317E1B5790BF02AF5CA5CF631DB13CFBBCF35 + 10F805B73978C97D9E1513D86D9E090D661B06BCCAFC8BAE73F1BCC436CFBB29 + 9EB09781C0CA0B02F7759963C0EFB3B3D886654A9F110856998B003253874B68 + 416001E0C7C74CC5E36329A68F8D9E8247474DC1E38AFF3B66061E1F3D1D8F8E + 9CCAB269784CE2B2CA9F183B93E94C53F608EB953E316A061E1D3ECD58003F34 + 688A057D074C603ADE02BD4326F402E01FCBD297650FB1EE012EDBF0F7E10163 + F048FF310602DFDF6F04EE33E0D7720D6D03E11FBF35183F7E7310EE7D6300EE + 79FD03A3DEFC6BFDAD78C04EB220B062005B00D85982C082BCCEB2AD806D30DC + 2B87C5AF645BFC0AFADA96C1CAABCCB87D666AB7B52D8005806F077F8D54760B + F875B6003661FC8CACF7BCB605B0B354F6FD87FB5A92AB6827006CCB06C0DFBF + 5FDE27F5EEF9BB01B052DB05B46200DBEFAC6DD9ED8D1B6967687C1B39BF13B7 + 61AF91DEAF3BBD73D7FBF75E087C3BE91DBD936EEB02FA7679E7325BB72BA36E + 661A7FB87A99CAFF00D9BCEB8FD16D59177553BD33B4745E76965DF75DBADD3A + 7FAA6EB79DFFA87E67DF5A7662A3CE7CD42C3BB7FD23E40C65FF9CFAA301B053 + D96DD7FB5F0280BFF5A33AFDB0DFBAF86FD756EAADE771FC010058BADD9FF3BF + 43DFBA594902BFF6EFF23F06007FBBDFEF04C0B7DCB8BF4B02BF06FE3A1E20BD + 0098CB377D71A4EDEB01643FB0EC2F92F460722AEB95FDD0EA7DC8DD78D0D9ED + 95DEE97858DA0F4CB9CEB8F961EA7890DE2700CC07B503009B87B51EE2940D81 + 0D08767AA0F73ED8D9E67B0F3C833B1EB4BE06B30702773D222BE067993E67F2 + 773ECCC1C3237D0DF8FD9E2315003640F85B651A907020F29865216CAC841D30 + 5870586E9FEFE560E9C78AE9FB8C2C7F55FE12073BB21296AB680D72B8AE4302 + C102BF1604B6F2DF7FF27963117C77DF5770AF01C0ECEFA5B78CEE61FE479400 + F00F9EE3365E78DBB86FB9F7A577718F067402C0AFBC877B39E8B35D410B003F + FCEE50037D25E56D30ACBC20F0630E77D092E0AF52C1E007DE19480D3250D8B8 + 8D96CBE7C1630CFC7D62E8383C337212FA8E9B86E726CE30E0F7E5691E7861AA + DB0DF84B3D3B79269E9FE1869738987FD9D51D2F2876CB7457F435D6BEAE789E + 7A768A2BFA4C76C1339366A2EFD459A6EED9E96E06020B00BF3E6F11DE5EB014 + 6F2F5C8AB7162CC37BCB3931F10DC60701C19CB8846068440C26A77272149F8C + B756F9E1B5E5AB38710933CB23A2E238A1B140B1DC3F0F8B4F3496BD23E4023A + 3B1343539231322911C3E3133024260683A2A23038261A83E36231248E6571F1 + 18C2F6C35252304296C36919C60A78429EE2049562526129276985189B5360D2 + 89052CE7646D3C2776A3929230323111236264851C8EA1E11118C5FC88A86893 + 17AC1D141A8691D13118131F8F91AC1B191D8B61A1E11814188421212118440D + 09D797BC11181117C309600A2665666362463626A5E7625C7226D74BC2E0B068 + 1E6704CF07D7657E5C721A5C0B8BE0595905B7F2624CCDCFC2D80C1E7732B72D + 0BE3CC044CC94FC384AC744CC9E5E4B9AC14EE159CFC5656C0ABBE0E4B5A5BB0 + B8A519F3147FA9348F93FD6CCCA9C8653E0D330A12E052988479CCCF2F4AC59C + BC44CCCA89C5B40C4EAE9342A8604C4A09A3423121390453D2C2312B370EB34B + 5331B7220373CAD2E0AEAFC3F3E2303933129332C230313D18933382D92E82E5 + 1106FA4A33F3A34C9C5ECF2A01E314CCAFCDE4E43D1BB36BD2E1AA98BE2C9B5B + 9F8985AD0558D2514C1561415B81019B9EAD2558B1BE1EAB3635C27B43BD9196 + 97AEA9C6A2AE4AA6828EB558CC74C9BA7A2CDBD884058AC3BBB1110B3736603E + DB2FD9D2CC3C97D7523D56FC5E2F59DE76D51A79495C16145EBCBE094B36341B + CBE0D99D5570EFA8C46CB978EEA935E0774E578D299F4309EC0AE4F6AAA302B3 + 5B4B31BBAD0C732547CC5EA52A576AE2F932F5682981475309663715634EB324 + 682BE82BA87B03FABAD7E7C04DAEAD29F7FA6C038115DF784E63AE696BB57780 + 5E73BE0AE1C97EE6340B10B31D35AF351F5EED8558D0598C85D4FC8E422E1730 + E57936CB82C3853C9FA53CBFB2C0ADC3D27515AC2BC2C2AE622C92D56F5719EB + 2D08BF6C6D151675B2ACB3D4E42DF85B6DF2565ADDDBCE00E02D8D4CF5DBD51A + 09007B6FAC86F7A66A037865E5EBB3B5C1C902B8CEB875B65D3D0BFCDA793FE6 + 057F15B33778779B03003722D8B86E6EB140AF802F15CABCA432A561FB5A10BC + 57963175083FD082A803AD08DFD388C463DDC69DA6AC781457573177E31477F7 + A0E5D2597179138E7420E95897B1EC35A0572058757B1DAE9BD9D6687F2BE2E4 + 4E522F85D977F88E2A846CAF40F8CE6A44EDADE33A0D48905BE7FD4D88E7B612 + D936F9602BD28F76215B50F5E41A641CEF42DAD10EA6DDC86159CEE9B5C83EC3 + 94CA3DBB9E526CC8F54672C96CACBF4E6F302E43F3CF6F41DE875B8C6BD08C93 + 2C3BB3A5D7BDA85ED44BF9E7B69B6581E13C59F90A1053451714DF76270A2FA9 + 5E96C2B22CDBECB0FADD8C42E68BCE6E44D1990DC83FD983CCC3969BE7A2D33D + 06DE161EEF302A65BEFCDC5A94512A2F3ADD6DE06FA12C824F74A150317A792C + 02B2F55776A2EEA39DA8B92450BB1DB55776A0D4C08C4D2866BD5C2D577F6401 + 5FA9FEDA5E345EDF8FFA4FF6A3EEE3BD68B87E00B5AA63BBBA8F76A0F1DA2EB4 + 7DBA1F1D5F1C44EBF57D466D9FED47F3277BCD761AB8BD5A1E5FF5C5CDBD1224 + 1600AEE0B92DE26F5CCA63AB38B30E35AA6379C3554AA901C15B517B798B5576 + 7517EA3FDE8D864FB54F0E7DB20F7557F7A0E69ACA6F0060C128C5EC6DFCFC88 + 81B93698B2009605B59CCB9CF316D0B52C7AB52C28A618A936885299EA2CB0CC + EDE99C7CC2F5AFED47D595BDA8BC6AC550AD35F5B23076C40956CA73D9EBEA59 + B0D721C1E0AACFD9C71716DC15EC15E015089635B0CA0CCC13E8FBF446CC542D + 575C97EB64E988018F65DCF772AE6795DDB0CCB5752B0096B5AFE0AF947369B7 + A58F6E06C0B9C61A783F8A6499EA58CFC06007F415FC158036CB067E1E45E135 + B9933E6CAC7D2DEBDFE3289125302577D0659F9F40C5172750F9E529D47C751A + 355F9E44F597277A556BC0EF29347C731A8D3FB1DC3E0BFC76FEF282057F7F75 + 1EDDBFFED058F50A020B00773900F0FABFBA848DBFFD089BFFE60AB6487FFD11 + 36511B58B68E753D6CA3B692D6EBF8C5196349DCF2534166C50D3ED60B830580 + 1BBF3C8EFA2F8EF1373D8226EED39ADF5CC3DADF7E622C7A65DD6B4360C1DFEA + CF4FF1773C09595D175D3D80C2CBFB517A95D7C21767D0FD9BEB68FFD5476C73 + 92BF3BD7E36F59C2DF59D6B94557F83BC865FCA55D06FE0A02EB37AFB4A13FCF + AF6253975E96CB66C5ECB56254CB8ADDC06089E75F3188A50A015FF5CDDFAFF7 + E3015E6B9665AE257DF0605DC7923E66B0F235BC76749D3B03607B59D6F2966B + 66FD27ACFF9AD167FA0842FF254BAAABE1367AA5FF98A9B3E0B02CE1157BB956 + 32DB56FC65E61DB19AB51FDA37DBEAD75861DBD6D88E8F2FACDFE698B1E46EF8 + 92CBFCDD9ABE919BF01368FCFA18EA14C7F9F3FD46B5BC47E97F56CEFB95E0B7 + 622D97F31C572895D530A57C05EF27721F5FC27B5731EF438597784FBFB80545 + 1F6E46E985AD28972ECA1DFD0E4B97780FBDCCB617ADFB78B162945FE2B24345 + 1714BB78BB2993E476BAE0FC36F3D18FC97FC8E7843E0ABAE0B02216DCA5F4B1 + 50369F1F8A256FCAF86CB163174BE60323AAB7CC91E618C82B17D57C5EF13922 + C96B8520B4DC63EB43A32C138A601332D5079F4F59DC7E3AD74F3BBD151967B6 + 1BA59D1288DE0601E274C5AEA732F84C521CE454F69FCA5456C8C6B298CB2982 + C2B2C03DBA86CFEEB5CCAF35B1F0E30FF2F97D803AD8C56775BB795E27F2199F + C8E759FCC146241C121056880541DE06C472ECA036F11C2F281FB5B31E913BEA + 1123084C09D6866EA946C8964A3EEFEB4D9FD1B2F41568DCD988C83D1A0BB4B2 + 6D33D564D651F80741E6A0CD15F05B5F62D2087D0826CF1FBB6B10B9B396691D + D7AB43D4F62A84B13E48A0727D31823796216C5B15F7C182C35A376053B97119 + 2DF7D11ADFE8A3B7B0FD1D08E5BE04ED6D851FC74672EF1C7A40EE987B8CF5ED + EAED2DF0DFD966206BF0DE2E23E59DAD812DF8DB019FEDAD945C44DFB004F6DF + 2DA0BB163ED4AA3D4CA9D5FB3720E000B57F3DFCF6A9AC1BBE4CA595BB3BB14A + AEA8F7AF31F2DDD3057F41DF83EB104829EFC7329F5D1D58C17DF3E1F6FDB9BC + 7A57A781C5FE7B3BE1CBFDF5D9D60C5F4AFBEFBB43215B14264463C10E8E033B + 0C18968B691D8BEFCE5603BF43B86EA800F22E850969E7D8B103013A268E4F57 + 6DE4989392ABE9C0AD8D08E258D47F53AD7135BD5C1F4136E762714326FCDA72 + 11DA5DC03407CB6A93B1BC360DAB38B65ED95E4415C2AFAB9CBF0FCFFF863AAA + 1E7E1C077B738CBC84F39A85ADB958CA71F6AA75E558BDA112DEDD3958D0108D + B99521F02809806BBE0FA6A72FC1F4E485704D5E8C7959DEF0CA5A0997F88598 + 1C3E1BD362BC58E663DC51CFCEF3874BF62A4C4DF7E67A01585A1F8FE5F509F0 + 2A8BC49C9230130F59719017D526627963067CDBF3E0C37DF76EC9A232B0B431 + 155E95B1702FE4DC2C2700D3B3566346B63FDC7203E091A77D09C1F49C504CCD + 609A11CAFD63DF95E958509B635C43CFAFCD85674D2E16D617623ED3D9651970 + 2B4AC18CBC784CCF4DE09C3515B3F293313533065332E3303337052EF9E99891 + 9B8A69D92CCF4E62BB54B8146772CE9A8559D48C920CCC284A33C0D7A32A175E + 759C0FC92A58E579C96C9FCC341593B3E2303A29D400E0854D6558CE79DB3CCE + A3662956704329E741E598DB52034F01DDD67AB835541900ECD5D984C5EB3AB1 + A8A79DF39D662C5AD381A5EBBBE1D65C0B9726CE1D37F570EED805CFF606B8D6 + 16C355DBA604A76755CAA5B5006F3AA695E5C0BDAE94732C6EB7B90E2E95E598 + 54A818C80598CABC476B13DC5A1A30A5A612132B4A58568A195565F0A8AB8647 + 4D39E7E72CCBC9C488D8080C08F1C7FB013E4C57634844000685F86050D04A0C + 0B5B8D61F21A16158421918106F60AFACAE5B3A465491F9F4BEF07F91AD02BEB + DFFEC12AB3AC81FBF9FBA03FD3C1A181181CE887A1017E1811148441BE3EBD00 + B8DFF2A578CF7BB981C0EFAD586EA0EFCBF33CF19AD77C130FD8C0DFC58BF1DA + A2852646F03B2B56E0DD15DE787BD972F6A1755619BDBD7819DE5AC0B65E5C7F + 8E97B1047EDE6D8E49A5973CE6E115C50E16F4953CBCF0E2ACB9E6FDD04B3212 + 98331FAFCC5B68C0AF00AF24102C09023FE7E68997E62CC44BB32DCB601B063F + EF4EB97A9A38C1CF0A0A4F75352EA01F1F3B0D4F4E9889A726BAE0F171D3F1D8 + 98A97842D6BDE36719D9C0F7C971AC67AABCCA9F1CE78A4747CFC0A3A3B8CEE8 + 99787CE40C3C326CAA03004FC6838327E1814113F1C0E0F17878E8440380EF1F + 340EF70D188B7BFB8F3656C00F0F9E808759662C7F1D7A64E058933ED07F9489 + 0F2C38FCA052B9897E7728EE7D7B30EE7D6BA0B100960B686309DC0B83FBE39E + D7DE37862136FC95D188DE15FEE8E57771CF8B96EE7DA95F6FDEC06027102CD8 + 6BBB83B661B05295FFE099D78DBEFFD46BD4AB06FEDED3F7AD9B00F0F714D397 + B25D40DB12E8B5E2FDBE68521B029B7AA5F63BD65B00B00D81AD107F7DCC7B5C + F32ED701816DAB5F1B009B7AB57DE0996F41607999B4E1AF0D806FBC877648E0 + D79603007FEFC737DE57DBEFB2ED756D002CDD0E06DBEFC47BDF9BDBEFE26D39 + DEB19BF7E5F6FB785B8EF7F2CECBCEBCE026006CBFF3775EB6F3CE654EBA43BA + EB96B25BF8C31F2A67DEF2DF2D1BDAFE29FA4E1E6697FFBEBAEFD2ADEBFC67E8 + 76DBF98FEAF7F6AD32071FBD85919ABADFB9EE77C819AEFE39F59D00F87607E3 + 24E775CC7AD2FF220BE09B7E8CDFF7E3D8F5B7AAB79EC7F1BF0C004B37DDACA4 + FF7100F8767D5AFABD00D879D9AE7794DD16002BE5B201BEF64347FBA065FBA1 + E580BCDF7A90396497DFAEDE79D97A805A0F4FCB0298ED6FF99A4AB21EAA82C0 + D6B2F590B6BEDABA436098F546F75BFADE036C2BB19DB5CC073C1FFC77C9F297 + FA3E0704370D0A542E10FCA8AC7FFB7E1B0073F90E016296497770BDBF90DB91 + 47E516FA25FCF0490D563468514C605901BF6CAC7CEFEDABAFDC2C4B5F4920F8 + EEA7D5C602BF9635B196057D9FC30F9E961B68D699F425DCDDE715DCF3EC6BF8 + F1F36F7020F6BAD1DDCFB14FCA2CBFC001D6F36F72B02628FC36076FEFE03E07 + FC35E2604F0058A0F7A1772CF82BD82B3DF4CE103CC001A35537048FF61B6EAC + 829F18301A4F0C1C6D62043FF6818030D7E937180FBD3B048FBC379CF5A3F0E4 + A03106003F31642C9E1E31117D464DC6B3E3A6E1F9498AE3EB861726BBE2D989 + 33D167E20CF4652A10FC8220EF4C770EB2DDF0DC2C570ED8E7E0350ED8E5D659 + B15CE4F2F9590EBC9F15389ECA76D367E3F919D44C0F3CEF321B2F7280FF9AE2 + 00CFB7E0EF9B0B38E958C689CCEA100C090EC7D0B0488C8E4DC48CAC3C4C4CC9 + 307181DF5CB91AC32363312129CD80E0771CD6C0432363302A2119231DD6BFC3 + 33D2312C99CB890918111F8FC1915118181E8E0F22A4080C8D955BE8148C4ECB + C0F094140C8C8BC7B0A454E30A7A627E21261514614C763E4665665339189595 + 4B65626C560646A5A56208D71F1019C94995266CE118C27D18191DC77D8BC690 + F048EE8FF6291AA3E31330213905C3A2A33198DB1F121A86A1A1E1181515C509 + 5D180607731216168C91B1D19C0067C3A3AC0C73CA2A39392EC1D824EE5B14F7 + 2B52318E13312E310D53D2B3313D3B97CAE1243A035372D3E152928B9925D926 + 1E925C39CFAB2DC19CAA124C2FC8C38CC23CCCAE2A37E0776E4D954917363562 + 41631D3CEB2A0C009E55C8093727D8AE259C7C1726627A6E2CDCF312303B371E + 2EE911989A1C86A9296198921A86C9C92198AC34251453057F7362E15198048F + 62C5454E31207856410226A787635C4A3026A4C9C5B380AFD210EE7308661644 + C3AB26032B5A0BB1AAB3142B3A8AB1ACAD906909967795628962D9B26E415B11 + 967495C17B5D357C3736C067A300AFE2F5B2BEA3022B3634C2674B0B566E6886 + F7FA262E3761D99A7A2CEEAEA6AA0CFC5D2017D06BEB38C9579CDD5A2C5CDF00 + AF7575460B373460FE3A415F9677D56151773DE62B662FB5403178BBE4AEB902 + F3DAABE0A918BF4C159F5796BD82BE1E1D95706FAF605A8E799DAAB32C7D3DDA + 8A31BB95BF415B2927FEC537A52A776F298207359B652A9F2B57CE9480AF5B43 + 3EDCEB0AE0D1204BDD42CC6ECC3770D7AD2ECB805E815F57E565F54B69594058 + D6BFCED0D706BF92AC7E0580E70AFEB6E4606E4BAE058199F76CCBA3F20DFCED + 05C006025BF979AD79060E0BCCCA2DF3D2B582BEB228B6C171A9512F00560C61 + FE3E4B7A2A0CF455BAB8BBDC01846B8C546EE7A5951B14E7B71E3EECDF76FF6C + 5C3C6F6F82CF5659FF569BEDAFDC546362FDAABCD7F2D7A1004A71E18277B53A + D46CE2F686EE6935F179C3057915CF77571D4276D51B8BDE7003841B0D048E90 + 0BE67D4D883ED06A60AF206F9EE2B77E7A002927D79A18808AB72BC81B6FAC7D + 2D4BDE7896E905B0C0B02C7EF5C258B037760FDBB3FF38F6AD97BA71FB1A11BB + D75A1600967BE7A89D7588E4BEC8F257EE9D538EB421FD6827320578E546F984 + D463606FF689B5C839EE48F5C25C60D780DEF5C83BA717E682BC2C3B4319E82B + 57CF1B8D143B522FFC156F522FD6CDCB7587F59701BF4C0B05782F08FAEE4081 + B1F6A52EEE306040566382078203B2F895455991FA533F6737A2C0B85A5E8F32 + E60B05A38FB7A3F06427CACFAD43E5F9F52817F815D8655ECB15E7D7A1E24359 + F6F618F85B70BC13F947DB5172BA07B597B7A2E1CA2ED47DB40BF54CEBAFEC46 + E3B53DA8FF788F01BD55D77643B1730570EAA87A81CD4FF61BE0DBFCF961636D + 2B4BDB962F0EA3996933EB04799BB8BE5241E01696B57E7200ADCCAB5C80B85E + 90F8EA4ED429B6EF854DA8BDB8192DD776B1DD6ED45FDA8C8A533D2839D98D32 + A63A861AB669BCBA8D7D72DD2B02C29BD996EDAFEF669FDC67F6DB785DFBBD1B + 75ECA7E1E3BD4CAD63A8E7B6CD7E53B24A14C033717BAF5B6E9B65D928106BE0 + ECA7872C10E59033DC551BC9065ECE00586D953756C4729FAB3A594C5EE53A82 + BFB2D8BC6641B35BDB0B9A557CC23CCF8F5C3E0BF8DAAE9F2B1C60AE92EBD800 + D8C0BF4F2D2B60E52D9027082C0B5B0BE60AF25AB17D950AB61E36B1772D286B + C15E036505621D925BE6E2AB967B66A9807967009C7DD902C0B72AEFCA3EE31E + BAF0AAAC5A05830F18A97F1333987953CEED16723BF9EC57F1838B3E9505F031 + 4B9F1E47D96702BFA750F9E54963FD5BFDD549D47E7D1AB55F9D42ADC0EF97C7 + 51C7BAC6AFCFA2F92767D0FCD3D34C4F19EBDF8E9F9F47F7AF2EA0EB571FA2E7 + 2F2F60ED6F2E61CDAF2EA2E7971F621DCBD6FDFA0236B06CCB6FAF62FBDF7D8C + 1DB6FEF6636CFB9B6BD8F6B7D70C10DEF8D797B19EED048AD7FDE6A281C60606 + FF526EA40594B9CDAFE51AFA9881BFC63AF8F363C66A5596A9ED3F67FB5F5FE6 + 7E9D3356C0B55F9D45B58E49C7C66394CA3EE371F3B72AB9C6737E85E7FA23FE + 96D7D98675B2FE55EC631B14DBC0BE50E797D794ED125AEE8ACDB5C0EB499057 + 2AE3EF24F86B5FCF72835CC16BC358B3B27D09D332F621B85B4195F2BA2BFB58 + D79DF581411555CEEB4D30D858A3F37AABF94456B496D5ADE57AD982C135AA73 + FC47ECFF8364015EC171ADA736AAE37FC6D45B16C2B793B11A663F06049BFFA9 + 20BB33F015D4BD61FDABED0AF40A004B9635B66591ADDFA589D74EF3D74E7058 + 79B98816C4FFE638EABF64FF5FB1CFAF2DD57ECDDF90657255AD0F35F4D146B5 + FEBF3C5FB5DCBF3AEE9B8EA592F79572DEB7CAF481CC95EDBC5FCBEA7713CA3E + DC82CA0B5B5075712B2A2EEFE03957DC64B6BBCC36BC874B6597159B7C4F6F9D + CE89624DCB4B80DCD2AB5EED8AA4F34AB92E9F09B21856FCF73C3E034CEAC81B + 37D4E736234B30F70CCBCEEA6321815E0710BEA52CFB943C52581F30155ED8CC + 67945C4DAF41FAF12E642B8EFCF9CD4CF5C1D27AE8C3254167C530966B6A015D + DBE238EBACC3F298CBC6A5F4D94DC8E1B32E8BEBA7F33998C6E76596F691C791 + CB676126CB52151A81CFD34C3E43D34FAD430A97938F7423F95017528FF42083 + 75A947BB91C87140C2A126241D6E46D2A1563EDF5B107F4831FA5BF85CD7075D + 2C679BC483ED06FC0A02C7ECE2F37D4783B11C0D5C578AD0CD5588DC2960DC82 + 28B609D9566BDC3BC7EC6D45B4C24CC8DDF3B61AAEAB361C83684CB2B5027EEB + 8A8C556FD4AE5AF6CFB1C31E8E177654713BB5881710DEC97EB75732E5BA2C0F + DF5A8E304A6D225926C92258EEA383B7562360672DC7400D08E57E871E684330 + C72282C041FB3B11BABF8763A14EE306DA6F470BFC0C046EED75C72CD9D05731 + 81ED74D5B626ACDCDACC7C8765FDBBA71BAB7777C14F56BD7BD6DCD0DEB5F0DF + A7B4DBC897F2DFAFB21EACDAD98E953BDAD8AE0B016CA33259030BFE06080AB3 + 6D800302AB9DAF635B81BBD9C74E0B02AFDADE0C1FEEB3F65B10D8676B9349FD + D83E90ED83E42E9AED747C02C42BF5F1215363C92C00CDF6ABB634B17D2BFB64 + 1BF5C136BE5B1AB09A75013CC6606EC35FCB9B381FD950059F0D95F059570EFF + 35A508EA29865F77115677152250CBEB2AE1D7C37A8E7D7D990AFE066FE235B1 + B91901EBEBB19AE3616FCE75049257724CEDBB4156C71CE7AE11104EC4D2B644 + 2C6B4DC0A2DA687896066156D672CC485A088FF415F0CA590DF794E59816ED85 + E9710BE19EBE1273F30330AF2494F3AF30CEE782389F0B81674D2C16D6256041 + 6D3CD3242C6FC9C0B2A60C2CAC4DA652B0A42E150B6A9231BF26095ED589F0AA + 8CC39CD268B8158462667620A6A5AFE67CD30733327CE096C56DE687C1AD380A + EE45D198572A2BE2742CE43CCEB322957D646011E7232B38675BD9C6F9597516 + 5CF2623135239C7D84617A56345C389F9C9E194D45C195F34BF78254B8E6A7B1 + 5D325CF253308B722D4CE3BE53255429E7B6CC4F67F90CCAAD340B9ED59C97D4 + 705E54966300F0D42CCE35B3E230213D0A6338479D98CDB96B45363C9B4A30B7 + 91F3AC3ACE811ACB30BFAD9AF3C95A2C68E79C4F16BD8D96B5EFFC8E462C5AD3 + C2F95E33E761B514E7C9ED759C9F55C3B3B3014BD6B763415713E74BD5F0A82F + 65BFE598DF54068F1AB9A5CEC6D492744CE1BECEACE4BCAA81739FF65ACEB7EA + 30B3A602934A8B30B98C73BAA63A2C58DB89791D2D98515F8569FAF0BAA186DB + AFC3FC9606CCA9E3727931E7EAB998989E84E13161508CDE1131A11815178A11 + 9181181ABADA40E021A1BE18C0BCE200CBD2777044A09140B052CB0D74A0B10E + 9635F180D040F40FF1C77B41ABF16E800FDEF1F5C6DB3E2BF08E9FB77105DD7F + F52A0CF05D850F567A1BBDBF6239DE5DBA04EF7B2F37EEA0FBAD586A5C41BFB5 + 7889E5027AE142BCB96C19DE64D96B8B1619082CF83BD0DFDFE8BD5502BF2B31 + D0D70F037DFCD16FD94ABCB980EB7A0AD2CA8A77AE8905FCA2DC423B8160BD0B + 12F015F87D564604D3DDF0EC0C0FB661BD8767AF85AF20EF8BB36551BC18AF50 + 2F53AF7A2EB1F27316F6C261038167CD3300F839AEA7FE140FF8A909334D2A8B + E0A726CE3456C102BCCF4E998DE7A6CE419F49EE787A22DB4E7035E5CF30AFBA + 3E93661B08FCF81817A3471D16C08AF36B20F090C97878A8E51E5A1208BE6FE0 + 58930A063F3464221E956BE9A1130D04962B6849005896C037A0F0283CD07F04 + 1EE8371C0FBE378CA95C42CB6063889120702FFCA5641022E9BDA081BF0E1963 + 9197FB19F87BAB8C35B0933BE85B21F0AD6572117DD75396EB67C15F41E07B9F + 7DDB2CCB02D806C0C612D809FEDABA09FE3A016041DF5E639B5B2C80ADB07E37 + AC7C6D00ECACDE7A7984FC3D00D819E2DE241BFE4AB701C0B7EA56007C2B0476 + 7E2FFEFB00B064BF93EF7D2FEFAC5E562016F030BE67DEE37FC7FBFFDBB0809B + CB1EB400B098825DCEFCADECE10F95336BF9EF9633C8FD7DFA7DE151BF9383D9 + 75CECB7F886EEDEF3F43B7DBCE7F44B7EB5BFA565B95DD9E99DEE0AAAABF753D + 27997AF543DD0A58FFB3F4177F71F74DCBB705C0F68138CBE9806CDDA1B8C177 + DC63AD27FD6F02C0FF99D27138C1DF5BF5BF0102F79669FF7EC73E3ADFE0FEA7 + E85B37765BDF51FE2D006CCB2E73AE53EAFC30723CC46E921E6E8E079E73F9AD + EBF52E2B6F82E6336F02E7B3FE5E3E2C6FF3F0B51FCAB7D3F71CE0F726F8EBA4 + 3B1F78DA7CF925F8AB543240F811B9827ECE00E01F3EF29CC90BF00AFA0A06DF + 25F7CC0E0BE03B0481058799978B6841E03BF4B5D9E3CF733023A0CB81C9E31C + 983C26282CB86B59FD0AFADAC0D7CE2BB5A558C082C4773FF322EEEE2309FC2A + 7D053FEAFBAA01C0F73E7703FEFEF0D9577B01F03D2FBC69E202FFF8257DB9F7 + 0E7EFCF2BBB8FF75C504FE003F667AEFEBEFE1FEB73EC043EF0EC2A3EF0FC5E3 + 1F0C377A62C0083CCA01E3C372EBFCCE203C483DDC6F081EEB3F1C4F0E1C8527 + 060A048F647EA469FF98E2003BDC453FC981A720B12C8105819F1D3315CF7160 + FCF428CB0AF8F9892E2606709FF18AF9EB62DC3ABF34DD0372F1FC2A07E8CFBB + 2A1E8BAB19D8BFB36819DE5FEA8DB7BC961837CFC6DD3307E22FCDE4807F2607 + FA2E2CA334B857DC97978D1BE8057883EDDF59BC02EF2DE30467152731012118 + 141C66AC5F4746C77342146D00B0AC806DABDF77577182B192CB7EC11818186A + DC418F884B34D6BDC364D99B9088A1B1B1181419896151EC273E0183A363F05E + 0827583171989657008FF24A4C613A2229052393524D3A362B07E373F20CFC1D + 9B958F09794518975388D159D9189D9D85E1A9691810158F7EC1E1E84F0D0A89 + E6BE466158782CF73501C3B5BF11B118111D87B1EC6F727A1646C52762606804 + 868445627818D36041EE600C0B0DC2C8A8308C898FC5948C74B81515615E4535 + 66E41461644C328645C4B33E11535238F92DA9C492EA06A6E598969189714909 + 989499889985999859906E26C9F3AAF2B1A8B11CB315A3A8300FB38A3959AEA8 + 80676D0D27D0A5985954C84935EB2897E27C4CCDE5E4352F0553B3E339898EC0 + C48C088C4F0DE1F642318B79D7AC28CCD2849DE90CB9FE4A0D3512FC9DC972B7 + BC78CC2E4C865B41A271032D00EC5E9C8C69D9D198A275F262E15210CF7D8BC1 + F4BC68B8952462415D2E567555C07F5D1DFCD6D762658F5E865460690727D0AD + C52666AFDC39CB82D4DBB806968BE026786F6AC4D20DB558B8A61A0B7B2CCBDE + 159B58BEBE09CBD635182DE9E1A4BAA3125EED9CA89B98BE82B98AF1CBC9BED2 + 359CA0F7541BCDEDAEA2AAE1D55585855DD558DC2510ECE89BED167455624E4B + 3166372BF62E27FF94574705E67756C25375DC4FB7E622037105766733EFD690 + 0B37B9626E2EC0DC169EFB267D819E6B34C751E621A8DB98D75BE7569B0DF7BA + 1C78D473BD863CCC6D2CC0BCA642CC637B59F5BAD765C1B53613AE3519F06850 + DB2CCC6DCAC33CC5F115F8555EA057C09779DB1258D0778171E35C04AF566D9B + 6D5B730DD4ED85C094F22AB3AC7F2D10BCA0D386BCB206B6B4A8BBA4575A5ED8 + C5BCDC437797F17C9563E95AB977964B68590497F3B7102C2EE172E94DF54A97 + ADB3B47C5DA5B1FC5DB5B9CEC4FBF5DD5A0BBFEDF5F0DF210BDF46B3BC6AB3E0 + AF00740D7CB7C90AB81E7E942C7C15BB5712F40DD9DD8A20590153218AD7BBD7 + 52C85EDBF2B7C940DE482A82F930BD0CA5C276371AF02BB01B7BB00DA1BBEA8D + D56FC1E51D28BABA0B49C7BA8D5BC7D8FDAD483C2C37CCB2126A430CCBE40652 + 163EB2DA917BE764D6271F629B831D4890D5AF003015CF6D261E6C315279E281 + 36D326F1403BD376A41FE942CEF135C83DBE0ED9477B9077621DF2943F228BDA + 6EE41C5B83829302AC9B907F7A23F24EAE472E557076130A3FDC8C02BD40BFC0 + 3AA679260E24DB186DEA85BFF91F0AFC2A2FB79FB2E6B2ACAC0AF462FFA35D28 + BA228BBE1D06FC2AB6AF5EF8CB124CF1368DEB51AE2BB7A2A55CAF8CFD95723B + 25E7D6A1E4EC1A943B5C23CBDD72F9F975A8625A4D959EEE41E9A92E949FE941 + 15CBAB3FDC885AB6ABB9B409C5A7BB5174B2DB58D80ABAD67DB4D5613D2B70BA + 170DD7F6A3F1E30368BE7E084D54F3A7878CB5AC6057D397477BD560C0EF61B4 + 7E7904AD5F30FFE901A3864FF6A1E5BA402F973FDE6780B26072E3D5BD46DA96 + AC8BEB2E3B2C79AF6C43E3951D460DDC971696B55CDD8E66B669E43ED75FDC40 + F1D8CE76F3983AD0F2F136ACFD09F7EF63EDF7666E47F077A7E95360B9E5B3FD + DC873D0600CB3A59FB54CBEDD75EDB6BA0750DCB04811B3F3F6A24F072C37AD1 + 8AEF2A592E6CBF0D80057CF5DBD8E057EBA95CD2B2409B6DCD270BC0D20B3B8D + 25A1AC7F057FD55E70CE76A7DB1B5F554088FB69C35F1B00975DDF87529E53C9 + B870FE84E59F5A6EA06D182C10288BD0A2ABFB2CA0EB80BFB64A3E3982E28F0F + 1B8B5B0158A36B16E815FC9575AA0D22159BD78ECF5BC8FAFC2B077A01B05C3F + 0BF6DAD6BF36FC55B9DC44DB71829DF306085F738A214CE5CB9A556D585ECCFD + 93BB675902177D7A04259F1D43855C3E7F258BDFE3A8F98A626AF29400B0DC3D + 377D73C6405FB9FC6DFDC969B42ACEEFCFCE18D8BBE6D717B0EE3797B1E1AFAF + 60F36FAF610BD3ADD26FAF1AD8BBFBFF5CC7DEBFFFB4577B2496A97C176503E1 + AD7F7B95EB5FC186DF5E36EEA165196C5B07CB4D74CB37270C08B6D2130632CA + 6AB5F14BEEDFD7A751FFD529D429E5BED6F0782A3E3BCEDF42109EE75910DEFE + 9DAE1D41D9151EFB473CEF1FF19CF0FC977F72CC580F6B9DAACF8F5BBF237F5B + 5903DFF49BF3FC0BFECBB5B3AC7C057F057A6D002C95F31A134494CAAEF05AD4 + B5CEEBBB9CFF8972C74707C6E2D7589ACB529CDBE7FFC4C4E5E5B62A796DD45C + 9705AEACE599675ACFDFC9B2B03D6AE0AB6227EBF8EDFF8A258161ADAF7DD983 + AA4F76A382FF4BC5E02DE77ED8FF01A502A22AB3AD870546D58700AFED4A5D32 + 2097D784AC7FEDFF9EEA9BBED2F93F6EE06FC3E7CC7F2157D0A7D1FCD559EEDF + 19FE3E5A160C3E89E66F98E76F665B0437FF94EDBF39C6DF8BEB52B6C57293A0 + 33CF6B35CFB3E5E1603FEAF87FACE03D4C50B782F7ACCA6B3B4D1CF252DEF74B + CFAC4795B9D76E450DFFD3D5BCF7E9E313E31A9FF7EEB28BDB50290F0BFA5DF8 + 9B54F03E22AF042DBC86748F557C697D38625B1C97F07960DC4EB39D3E02528C + 61E3FE9FCF085BF232215971E12DEB6129FF1C9F3DBD1204B6E06F069F6F9927 + F86C3BD3CD67943E6612746DE733B1112987DB90C56783E070D6A9B55C67A379 + 8EA97F632D2C17D67C76E5B3FF5C852B1078E673314BCF453E23E5ED22FB9CE0 + F15A64E843AA337C16B28D59FFF406B65B6B3C65E843A9CC93EB1CEDD6219DCF + D9543E8B334F70F9F85A0377930FC913472752F581973EF6D207627B1ACDB33D + 51CF722AE5500792F91C4FE478237E7713E2773622826396F08D9588DBC5E361 + 5D329FF75A276A5703D7D7C7602D88665DC4F65A4452B1B200767C1816BDB316 + 615BCA11B8A11891DBCA91B8AF1E097BD86E6B19A2B757227667154237152270 + 5D1EB7538268B609DB548C900D4508DB5C8A904D725D5C86D0AD5508DB618160 + C5070E61DF662CC47D16080EE37E8571BC12BAAFD300602968573BFC054FA900 + 0344DB8C8B65C1605903CB15B3AF40AB43ABB6C9E25656C05DF0DBD561AC71FD + 77B31F59F152FEFBBACDB262081BF7CEEA5F30997DF93155992FB59AF5261EF1 + 5ED559F5ABB92CF02B20AC74D5F65623DF1D7207CDBEB65B56C8ABB4BFDA4FF5 + CF7D160016C0F5D9AA3022CD1C4BF238B40DC1E66DAD58B6B919CBB6346325D7 + F5E6FE2FDFDC6864F5DDD6BBBE40B015AAA4013E9BE47148E3D45AF86ABCCABC + 3CE378AFA9C02A8E6B57ADAF848F402EEB02D8D6776D0D567657B2AE8AA925BF + B5AC5B5F8FA08D0DFC6DD93FE740BE9CA3F8F4701E24CF45722FDD93C1F23CAC + 5E9B03EF8E3478B724637E05E75B19DE9895BE12F3F202A840CCC90980478E3F + 66E787605E4904BC6A12B0B435134BDBA88E1CACE8CEC78ACE3C2C637E415326 + 16B76663690BF3F5E96C9B82D965719C8FC560B6541A83392551F02C8F835759 + 2CFB0C874B7A00A624AEC494F8E570495A09B78C40B8B37C6E51341655246059 + 4D2AE7A32958C0FCA2AA24AC68C8807753161657A5B04D0C5CF5D16FF26A4C88 + F7C1F4D460CE21234D3A2DC9DFCC2DF591B15B7E123C0A9231A788F346E65DF3 + 1239974CE2FEA4614E4526DC99BA16A5607A6E3CE7A8B19C63B26D79263C4AD3 + 4DD9A48C284CC98EC3F4FC048A6D381F9DC6F5A717A76206E7A23304912B3527 + 2AE1FCA882AAC6ECC60AB8D571EE565F665C42CF6DADC29CD66A7834734EC7FC + EC16C1E172CE5DEA3877698417CBE773D9A33A0F1E553998DFC0395223E767F5 + 9CBF5567615639E7D0946BAD42F014C3B5A90C536B8B31BEB200936AB9DDEE56 + 2CD9B49673C656CC6CA8C6CCC66A78CADAB8BB8D73CE367835D5C24DF18F4B0B + 39FFCFC3C4D478CEEDA331393D9EF968CEFF838DF5EFD0D055261D1CB21A1F04 + FA98B8BF02BF82BDB606CA0A382200C36242313C3AD440E081E1410602BFE3BF + 0A6FAE5A8E37572EC31BAB96E12DE6FBAD5A61A0EF3B8B1759AE9F972C367A77 + D9520380DF5FB9C2C4047E9375AF2F5A88572959FFBEB58C7D38D2FEBEBE181A + 1C8CC1818178DFC707EF79AFC4001FBDC7F145FF152BD9EF5263012C17D002BF + B664092C57D07D5C14EBD715CF390C039E65DA67BA1BE58E675D3CF0DC2CB6A3 + 9E77552CE1797859EF8BE62F61BF2BB8AFDE789B7A63C152F62F002C503C077D + 4D3FB3CD3B26C51A56E8B1678C27BA59E833D9157DA7B871D9154F4F74E1B21B + 5E9C310FAFCC5A6020B0A0AF00B00D7F25E59F1A6F41E127C6CEB4DC438FA246 + 4EC363C3A7E0E1A1930CE47D68A825415FC1DF471CB184E566FAB11102C5E3F1 + D0A0B1D43823C1600B028FC683FD47E2FEF786E3FE7E432DE82BE30E96490FA9 + 5CD6C0C602D802C002BF3F7E5DEF051507D882C0B6EEA57E2C087C0BFC956409 + 6CE202DFE20ADA19FE4AB206B6F5C367DE308057BAEFF977F1C8AB030C04B6DD + 3F9B3A27C82BE82BF8EB5CA6BC40B1AC82EDF8BFCE1058657FF1D0B3B8C3C05F + 7973B4C0EF77C9B60816F8B5753B006CDE27DFF2EEB9578E77DB460E006CBB80 + BE9D9CDBDF0A809DDF89F74A60D72103799DEAB4CEF7EEE6B6ECBA5BDFD15377 + 50F63B7D81E01BF0F61636706BDEA13BEEE6BA3FD2F6D49F1353E0F21D3F1454 + B6CA6CDD8E8BDCAA6FB593719D2DE7F2FF22DD0A737F976E8AEFFB2DD6750BDF + 735AFECE757E9F9CC1EA7F966EB79D3F46B7EBD359B75BC748F53718E9B76403 + DE5B756B2C616728FBE7D49F0280A59BD7937440B73B31CEFABF10003BEB77C0 + 6023E79BC177E9367FE2FF0972BE11FE4F51EFCDDCBEB9DF7A93BFA55CF17FA5 + 9B1E24B74060F3B0B1CB6E792039CBF961673FE46C39AF7793546700F00DE941 + 7BBB87B033F0B5A507F65D0F585F6DA9CD1DF751F73F81BF7002C2060ADFAF78 + C0CF38BEFEEA83EF537771E07017070D82C0C612587058F9479817FC65DE2C1B + E06B5905DBB261B00D84EF7C5C717C15239879C16307E4B563FCDE0A809DC1B0 + D21FF579997AC9E89EBE2F5BEA63C15F59FF4AF7BDF026EE7F49B0F72DDCF3C2 + 1B0E08FC1AEE79FE2DDCF7D2BBB8FFD5F7F0C06BEFE3410EF81E7853033D5900 + F733E90382C0EF0CC2A3EF0DC113FD87E329815D0E1C1F795B2EA0079AF60FBD + 33108FBF3FCCC0611BFA3A0363C16101E0C7DF1F8947DF1F6162032B0EB0DC3F + 0B02CB125810F8E93153D077FC0C3C2FAB5F0E9A5F9045F01457BC3A7D365EE5 + A05C00B8CF4C96C902D87321DEF45A6C00F0DB1AA4337D432E7A5C38789F31C7 + 4060A57DA77BA0AFBEF0E4005D5F7EBE3A7701DE5EB402FD97AFC6FBCB7DF18E + 20F2F2557877A52FDEF6F6C51B2B7D3871F1C59BDE3E5C96D86605EBD476C56A + F4F3F1375078444C0286C527190BDD21B171181A1D8D81A1E106208F8C4DE472 + 1C0684451A50EC5A5402AFEA3ACC623A2E2D0363D3D2313A391563989F989D8F + 89B9059890C3495C5E11F38598902777D039189C9882F723E2F07E68042760D1 + 1816C96D86C76304352A3AC940DBC1A1D1181DA71847F998C6BEC6C9BA3836C9 + 58348F8A8CC590C0107CB07A35D709C58CF414B8E466617A762666E6E6C1ABB2 + 1AAEF9256C9F6CAC7F4745C76372521A27E5C5F02C2A854B6616262626626C42 + 2C26A52738DC68A5614A9626C9099892998009695C272319D372D86F5E2E66E4 + E573B29C898919196C9785499919189F9A84B1C9319C64C6606C521846C4FA63 + 746220C62405614A6A08DCB2A331B7201E730B3951CF8BC5F4F4304C480EC4A4 + 9460F355F70C01604EBCDD8B92308B936F17CA8D794956C0B38A12E15E960AB7 + B2147894A76041430E5676969B171E82BE2BBA2AB0A2BBD268895C0777946241 + 7B091632AF38B27219BC6C5D0D96AF97EBE1062C5B5F8F25CC2F612A8BDE256B + ACF2C5DDB558D059C509750D27EC565E905656BCB2FE95E5AF81C09417E50C80 + 952EE892DBE86A6EBF0A5EED1594AC58CB319FA9B1C2A5E6CA6257D6B90D7998 + DD5800F7264BB3784C82BEAEF53998559765E4DE9883D9CD02BC79C652D7B536 + CBB86A9ECD7672E12CAB5D63B94B1937CE355CA7361B1E7539986300B0BE5ACF + 377977B978569F0ED990D706BE82BDB31BB25996633487DB9E4BCD6BCA85674B + 3EE6B715C2AB359FF99B21AF6DDD6BA0B043CEF516E02D36F056C0D72E575E50 + D7AE5FD0E17007DD53C2DF4320B8CC017C6FE46D6979D9BA72FE9EFCCD15EBD7 + 8EF7ABF8BE5BEA1CA9AC7C6BB17A9B62D65940580058E5FE8ACF26302C40EC00 + C072F12C4BDFE05D2D06FACAF23778179777DBAE9E65E1DB84F0FDAD883ED86A + B96FA614BF3752E5B2B6D9558FF0DD0D96552F15B1A7C158FCA69CE8318ADA27 + 882B78DB6EC06FFC01897DEDE1FA8AD5B7B9128AD91BBFAFC5C0DC64BD0C3E24 + 10DCC1E55613BF37F9483B528E7620856932FBD00BE3D4C35D8E17C71D48633E + FBD81AE41C5B8B9CA36B917B741D728FAD43E1A94D2839BB0DC567B6A0E0E47A + E4534567E47279330A4E6FA436A1E8C216147DB4154597B7A2E022CB1D2ABC44 + 5DB4E2F42A2D603B4160C1600B08B3EE22FB16F4BBBA1BC5D728B91315ECBD68 + B90D9515988088B10056FB7382BE9B4C7CDD32599AC9C297AABAB8C9C4D4AD66 + 5AAD78BB97B7182BDA9253DD283ADE8E8AB33DA8FE703DEA2E6D32AE95EBD9B6 + F2FC06549C6319F7BBE593DDC622B7E5FABE5E582AE86B4980F7089A3F652AE8 + FBD53134538215465F1E432BD5FED551B47DC9769F1DA4045F0579F718C992B8 + FAE276D4F278645DAC3241DA461E73D3B55DD40E03811BAFEE4033971B2E6F43 + 23F7AB896ABCA4740BDA3FD9466D45CD795903B7A2E3B39DD8F5DB3358F75341 + E6ED68F9547D6F376EAB9BAEED45FBE707CDF134C9DDF49787AD7D33C7A5B8C0 + 870DB836405B56829F1F453D8FCF887596EB66015F0B020BFE3A03DE5BE1ADA4 + 3249A0ED06CCB2E096609BE06FE5477B2C2B60471B0BEEF337E76FAD18A1E6B7 + 37D7C31E0BBC71FB92F26536FCFD64BF017D92E5EED972DF2B0058C0ED493600 + 3650F1934328EED51123CBDDF2510708B6AC4F057FCB3E66BDAC7D2FEF372AFC + 48D6BA0EF87BF58001BA920D7EF3AFED37B2E2FF5AF1816FC05DABADF236F8D5 + B2B10076C86EAB7D95856BD9E7C751FCD931CB1A9879B97EAEFEFA046A789D19 + F8CBEBCED251D4510D5F9D30D69C06FAFE5CEE9DE5AAF91CBA7E7E0E3DBFBA80 + F5BFB98C8D02BE7FFB3176FCBD057577FD9D057EF7FFC36738F88F5FE0E03F7D + 8143FFF4A5D1C17FFC1CFBA9032C53BA976DD476C7FFF904DBB8DEE6BFBB6ADC + 43AFFF6B0B042B4EB0DC42B7FE84FB409998C1DC17E38A582098AAFF92FBCF6B + CCDAFFE3A8E27FA8E2339EEBEB8A6F2CD07D88A9DC2ECBCDF149D45E3FC16BE6 + 20AF079ECB0BFC2D2FE9F716243ECCEB4056DC9614B3D98EEF2C085C7C790FAF + 1F7D4420D7CFBA2E0580EDBC758DDAD79B54C56B50D7B5CA4B041A3F661BDE03 + CCB5266B615E3BEABF58F5EA8BBF975C472B1EAF89C9CB7D901B6659E15AD6CF + C7793E3EE439FAD8585C0BCEAA7F039ECD7EE8BFC0FB00AF617D7C51F909F7EB + 9A5D6E7D4461DC2A7359A901C58EFF93FE7382BE961B6A0B005B16BEFA9F5ACB + 82BF82D0B6C56F23AF9D065E4B8D3CA74D82C05F9EE1FD4A005856DB8AE54CF1 + 37B3AC804F9854BF5D2B7FC3C6AF64B57C082DBAA7514DFA5084F714C1DC2AB9 + B5E77DBA9AF7AAB24B5B507A61332A78DFA9E47225EFAD25BCA7CADDBE3EC0A9 + D7072E577719EF03FAA8461FA054C9DA57F0571FC0A84FFE7695FC5D047D8D6B + 6CFEBF2DAF01BC5FE83CE89CF0F7B1BD089429E633EF15FAFD04C1F5C1818EBF + 94BF69E1793E6B3EB4C0B00DFA6D40ACF282F35608811C036BD721E384AC7DBB + 90CBE743C6C94E241D6A42FCFE7A3E3B9B91754AE57C0E9E596F2C828D770B59 + 02F3F9937FDE82B9E6E32681612AF7EC26649F66DBD36C7776A391F219272C6B + DE54134E61BD0580E549C391576A421970DFE431239D6DD3D4FE883EE46A412A + C70459C7BA9171A40B49FBF9ECE7B8226E8F057F057DF50CCF38D6639EE129FB + DB91B497E5FBD86E672362385649E53A995C374DFD71EC607F1816C3314ACC4E + 8E3B380649E018239E6395688584D85185D8DDB5260DDA508C908DC588DB2590 + 5C89882D2546915B4BE0DF9305EFD664F8756520647D3E82D6E521702DB5BE10 + AB7BF2B1AA3317FEEB4ACC18254A639CBD0D08631ACC6D0A0247700C137E4820 + B81541BB356E6AE358A9CB2868671B02144663572B5359D1B619301CB2CF8AB7 + 6BAC6C0580592EF02ACB5A63252CC86BA0712B02F77620605F27FCF6B46135FB + 1680154CB62D8855E76FEAB90EFBF4E1B67CB84D5FA6B67CD88F2FD715205EB5 + A305DEDCAE24AB636D5FF077E556592137C1D786BC82D25B1AE1BDB90EDE9BE4 + 4146D0D6017377B461E5CE0E2CDDD6862582C09B1B2D6D69C60AF6B9926D56B0 + EDF28DB51C9B6AECD988959BEAB154DE6D3A3916E6D8571F2EAE585F8525DDE5 + 58D4CEB1AFBC16F57ED8C879CB3A8E6937700CBBB11E3EEB38A65D5B036FCE35 + 247DF41AB4A109619B79EEB7F07C6EE479E2B26F4F1D96C972B625134BBB33B1 + 726D2E96B7A762694B12963727615175345CB37D312D7539DC73566341690496 + D5C463457D325636A66159431A963667C0674D91711DEE4379AF2DE4DCAA00CB + BBF3B1A0390B5E0602E76009B5B03113732A353F8BC3BC8A442CAC4EB2406E75 + 329630EF55140DAFBC48CCC90CC5B4B895981CBD0CD3137DE022089C13827985 + 915850128BF9A5B1F02C89C6BCE2482A0AEEB9A19C27FA617ADA6ACE5303302D + CD17931256626A921F5CD243303DD90F9362576072BC0FA6A606637A66245CB2 + A2CCC7C433B3788CB9F1F028E07E1525731E2959F3C9E939B198981EC1396D24 + E7D3319CE746738E2B2F53A198AC79686932E6556562567132A6A86D560CC6B1 + 7C42761C26E7731E5E9AC1795891B1E275AD29A20A4D5CE0592C73E1F2CCAA42 + B8369461D11A5E57DBD660F95A5E1BDDBC86D6B560614B153CEB4B30B338CDC0 + 65F7F274CCE57CCD93F3B3799A0F323F8373DC299CEB4EAFC9C1F4FA224C6B28 + C1146A7A4B05E6F6B4C26B6D3BE6B435C1A5A916AE8D9C83B634709EDB8AA53D + 1D58D05A0FF70A01E05C1E6B0EE7E371542CA665256132D3B1B1A118151D80D1 + D4A8483F0C17E40DF5C7FB81AB281F0C8908E4DC5DF3F7300C890EC6E0A8200C + 8FE3725C388646876010EB3F08093016BFEFF8ACC05BDECBF086F712BCB17C09 + DE59BED4C05E1B004B6F2E5C8037162DC0BBAAA3DE59B6046F2F55ACDF857885 + 75AF2E5C88B7972F47BF952B0DF0D57B0C41602DABFC1DEADD6596FA7B7BE3BD + 15DEEC738901C0C6EDF36C4FE30EDAB6087E769607FACC7033EF91641020C922 + 58E056F181152AEC25190ACCD5BBA5257873BE5C4A2F3650F9ED45DCCE126FB3 + FCD26CAE276B61D73978D6C582BF2F081CBB78A0EF74573C3345B0D7057DA7CC + 329EE994DA30F8451927CC9C6760B0DC3F0BF4CA1AB8EF640F3C33D1720FAD72 + E31A5AF182150778F4743C356E269E1E3393CBB2FE9D8047864F32D05756C002 + C18F8C9884C7474CC163235536010F0D1E8787E4F69912F87D64D0383C3E6402 + 1E1B242BE09178F0FDE106FC4A0FBE371C0FBD3F020F3055D8360380DF18D00B + 7D2579039494EF85BFC62AD802C03F7ED9D27DAF70D991976E40E06F5B00DB92 + E5EFF79F7ECD00E0FB5F7C0FF7BFD0CF58000B003FFC0AB7D7F7AD5E007CE713 + 96D5AF20AFE46CFD6BC35F2DDFF1E88BF88B879EC35F3CD8F726082CF55AFF52 + 77B1DC7E8F6BEB76F0F7BB00B0F27F6E006CEBA677E20E4F9A7FA8BE0BFCDABA + E131D4B2D635EFFC6F95DEFDDF2E4F09001BA9FC2E07EB509DC0F06D00F01F2B + D3DF7F33D3B91DE0FD7DFA36E712A8BC85EF392D9B75BE6BDDDF25BBDFFF5BF4 + 3B78E9B7B8AAA3ED4D1C55B20BFEDCFA5301F04DFAFF00B0A5DF0780FF10DDE6 + 4FFC3F41B7BBC17D976EB7BEB36EB7CE7F54BD37766739DDE49DCBBFC707CA4D + 00980F8E9B1E28ACFB2E006C1E488E879974BB879CADEF5C4F6D6F7D68F2416B + 3F886F4800D859CE00F829232DDFD48FDAA81F4A60F82F54AFE507D996BAEBA1 + A771D7C3CF38A481825C85285EC433F83E759764E2027300E1A8FFBEDD8EEB7C + 4FE5D49D8FF6E1C085838E273800A1EE7C9C0392272C77CF02BC82C036E8FDE1 + D32F1BF82B8B5F1B021B4BE1A75F309215B001C17DA9672C0B60C15F59003FF0 + E25B78E895773938535C8ED7F183BEAF7030F6BAB100BEFFE57E788883BA47DF + 1C8847E4F29903BA7B15D7E3A577708FB1107E07F7BDDACFC06141DF2714DFF7 + AD8178F0F5FEA6ED8F5F13241E8047FB0D3196C2B21896E4FA59CB16141E65E2 + 040B02CB85F483EF0CE60074189E183A164F0D9F80A7874FC493232670003B11 + 7DC64DC74B53DDF1BCBEA0645E7A61922B5E9C2E17D01E78DE4D20770E5EE420 + FE8559B34D2C9777172C43BF05CBF19AFB7CBC3883037F9779786996279E9B31 + 07CF4C73A7DCD0679A6206BB9901FB4B73167080BF8C03FC6578C573115E99BF + 18AF2D588217E62DC0F3F3E6E3252D2FE2E465F172BCBE70292729CBF0D6526F + F45BBE0AFD7DFC3124280C1312533021250DC3626339318AC290F0280C94956E + 60188686459B98C2160C4EE0242B1353327238A9CCE684320F330A8A3035371F + E333323139271FD38B4A313E3B1B63D3333899CCC4C4FC5C8CC9CE40FFE868BC + 17168E01919C70454658717E15FF3722124342C230203008EF2BAE4EB426AB19 + 9CBC6662466E16E69415634E09B7919A8C11E1C1DC5F5F4CE444CD3527192EB9 + C998941E87496909702FCE836B411EF33A96448C4FE06436360A5312591F1B89 + 116101DC5600C6C486605C5218C6A784636C5228C62484721218C8E3E6243126 + 98CB51181D1FC97C048673BDD15C7F72662A8F359DEB24B06D38462744606232 + 27D389611897188249A961DC8F084C4B0F3700784E7E3CDCF3E3303D239CFB11 + 80B1897E989C168C69991198C149B6BB26EB854998C976D2ACC204CCCA4F802B + 27ED732B3330AF261B73ABB3B0A8A9003E6BAB11B8B909011B1BB1724D159674 + 946169570527DA9558DCC9C97767391629DF5DC5C97105972BB0B887EDD6D552 + 0D58B4AE8EAAC16241E075F59CACD762413727D38AA5DB52C2B402F3BBAA0D00 + 5EE8B0FEB5C47694AC80156B5796BBB33B4A3187DB9CDB550E2F2ECF6FD517E2 + 459823B0DB60C5D89D232B5D47CC5D8F3A96D5E66066653AA655A46246553A66 + D664304DA352E1529D0E172ECB52D7CDB86ACE669A8159B56998559D06D79A74 + 78D40BE26662560DCB2837E6B53C9BEBCCD5F6EA7329A602C175BC5E280FD6BB + B15EF0D7837D4A16F41540B6E0B05B6D06F36A936134A7311B9E2D82C4B20EB6 + C0B02457CF02B992F2926D053CBB89BF930302DB205890D79696ED7A1B20DBA0 + 5812187686BE4BD75AA0572FCBEC722DDBAE9C057B95DAB296555765E4B3B906 + AB8DA5458D95DF56672C7D4328815F5B01DB1B0C000E71C05FB97C96C264E5B2 + BBC9B87A96B56FC4DE160372E5DE59525E90578AD827085C678070B4C0F0EE06 + A3E8BD8D88D9D764E5D98F2C7E65E91BB9B31ED1B2E271B87C8CD8566300B096 + F57257E037E57027528F7420F56807D28E7522FD18F34764EDDB6A5E2AA71D6A + 47C611961F66DB436D48639A7DCCB2F4CD39B206B954C5F96DE8FAFA2436FCF2 + 43347E7210F927D623F7D85A037F8BCF6E41C1890DA62CEFEC06E47DB8B157F9 + 173699789002C22557B6A3E4A39D508C482D175E92B6A1E812972FED60DD0E94 + C985E8C7BB51727597692BD85B7E6917AA3EDA8D4AC1918F76A1546E442F6E43 + F9C52D281770E0762A2E6CB0AC7DCF6F40CDA5CDA8BCB811951F6E30E0A196DB + 1700AE38BBD6C0DFDA8BEBA80D2C57DD660388055B055D9BAEED44DBA7FB0C30 + 6DFBEC9071DFDCF2B92C7EE5C6D9A12F8EA14510F82B59DD59F0B7F59B1396BE + 3C8636D6777C2D082CD07A08AD5F1C44D3278AEFBBA317F82AADE1B129154C91 + 6570FBE7FBD1FA9960F04E2381E8169E0BB97F6EE17969E5BEB57FBC0B9D9FEC + 42C7F59D68BEBA158D1F6D46E395CDE8F96A3FB6FDE549ACFBE921747CB1C7F4 + 53C7756AAFEC42F375F6FDC521EED741EED321B47F6D819CD62F8EA2E3CBE3DC + C7E368FEF2A8399E660123590BCAADAC64AC1905B5ACD8AE824E025936F8BD55 + 0254D24D659F28B5CB05CE6415CCB2AB6A2F77D1568CD6924BB2C8548C678139 + FDFE9604804B3F661B874AAE59CB2566795FAFD5A7A438B0B6F22F33E5755328 + 2078CD72BB2CCB5B81590BC61E34C05710B8F013CBFDB289C17BED08B761C5E3 + CDBBCCF617D9FE9200AE20ED41145CE3F54F296FFA73005FC1DF021E5B21FF1F + 85DC965C3CDB6E9E6DB8EB5C56C875E47ADA8069412B9E57A99CE7B8EAB363A8 + A4CAAF1F8562DE56C9BD2F7F87065E578DFC9D1AE5BEF76BCB5A5365025FADFC + 7F767C7D0A3D3F3B8B0DBFBE804D7F79099B7E73199B996EFDCB8FB0EDAFAF62 + E7DF7C8CBD7FFF19F6FFE31738F47F3EC7E1BFFD1447981EFFC7AF70F29FBFC1 + A97FFE094EFD8BA593FFF4B5293FC1543AF68F5FE2C83F7C81837FFF39F6B10F + 0383FFF6636CFFDB6BC69278B3AC8B7F75016B7F7ECED22FCEA39BFBD2F9D333 + E862DA2E8BE46F04144FA2E1CB13A8FFE2386A795C359FE9F88EA2E2F3239440 + 2BAF2DFE1E0D06001F478501C0FC6D3F94C5A7DC3CEB37DF67C0BEAC8505904B + 1DE0D75C07BC7E14CF57D6BF8A592BE027C8AB0F10EC8F10047F8D586E3E46E0 + BA15BA0E79DDA85C56C08A3D5DAC8F51D847057FA7AAEBDC3FFEEE02C2A5BCAE + 4BB9FD326EA3926555FC0D0D08662A57D0FA9F28C6F2BABFBA8CAE5F9E37FFA1 + 9BFE3BECDF02BB9665AF64802FAF7759260B44F75AFDF2FF6359DF5BD6F4CE10 + 58FD49B64B69AD632CF9B93DE38A9BFFDF669E6BFDAFF56147F39727799D9C46 + CB5702F302BF72076DA5260EF04F04EA2D37DEB21E16C86FFE46609BF7C36F8E + A38B6AFD4CEEECE5CA7EB78939DEC87B523DEF495557B6A292F7D2AA2BDBF8DF + DEC67B34EFFDA7BACD873755BC171BAF061FEF3116C326463AFFBF26363AEF51 + BD298FAD86C72EF7D236F4D579D1F9B225382C9973A8363C5F7243DDF38B73D8 + C8F3DDF6CD4973DE742EF5D190B116A64A2EEC40F1877CD6C81A98CFACBCD39B + 9075621D32F89C4B3BD2C5E7640BD28FB720EB64079F917A7636B1AC99CB9DC8 + 3FCF679D9E6967D71B2BE0CC53B2E455C8033EE33E1404DE64BC5CC875B43C60 + 6839876D73CF6D60D9265EBFB210DE848C933DE6232C59F30AC22A9E7ECEC9B5 + C83FBD1E0567F411D546149F97478B6D28E472D6F16E641CED42E6D14E3E97F9 + ACD63E1EE6BE71AC90C0677C22C7012907DA582F28DC8DAC633D263C43FAC14E + A4EC6B43EAFE0E641EEE41DA810E24ED6E46FA013EEBF97CCF38DC8E54F5C3E7 + BD3C82C4EEAA37FDA9DFE4FD1C7B6CAB42D4D67296D520657F03E299866F29A3 + 4A10BFAB12497B6B58578DD81D1588DE5E86D00DF958DD990EFFEE4C84ACCF43 + E8C60204ADCD855F570EBC5B33B1AC290DBEDD0508DDC27E393E09DFD3603E74 + 0BDE51CFB15483191B8570EC13C4F2A05DCC730C15B2A7DD580107EF92CBE416 + 04ED6E45F0EE36A66D263EAE2C6C058265192CD02B981BB04B16C2AC3356C296 + 5416C4BE8214DB9779BFED16483616C53B54C76D0826CBE2578097E572E32C4B + 5E1FB973DED10C5FC1604ACB2BE56ADAD4717967AB81CE82C992AC9015FF57FB + 635C406FE1FC62631D566CA8ED953C09192F335B1BB09AFDF96E6DC4AA4D02BC + 373E3E54A891951B39665D53CCB9471E96AF2982CFC60AF8ACE758B6B3100B9B + B3B0B0250BCBBB0A387F296219C7C86DD9F0EAC8C6FCCE5C78329DD3C2F17C73 + 26E6B7C992571096627F2B99066CAE42C856C585AEB7B45996C0D5F0E71879B5 + BCE6B41660765D22E7222958DACD7E1B1230A72A161E6591985F1D8F05957170 + 2B0831317A175527C0BB290301DC46C8FA0AF876156315C7E0BEEBABB09AE3E5 + 551B2AB1A4A79863F3022CEA2AE2F8BB048BBB3846EF2EE37CAB040B388EF76C + CCC202CE0B16B7CA6B148FA126158B396759CC39CB82B2042CAF4CC54A6A6E56 + 286624FB6246AA1F66652AD67038E61747C18BF2C80BC5CC4C7FCCC8109C5E89 + 4909CB31216E1926277A9BB25959FE7049F3C18C242A7135A6C67B6362F4128C + 8F5A8A71312B3039C90F5353023089F3CA294C5D32C3E09E2777D19170C98B84 + 47511CE793B19899138DE9D991989816CA79AC3FE7B3BED46ACE7B83387F8F30 + F34D8FF2544CCB8D3560787C7A24C665446292E20D6771DE9B9388E9C59C7B95 + E7C2AD321FEE55854C0BE0562557CE7998529AC5791D7FC3B66ACE393A0DF85D + DE598F555D9C673694C3BD3C1B5372E2D837E7C6B9DCA79264CCE1FC7631E78C + 8B15CAA7360BD3CB380FACC9C1ACC622B83597C1AD95EBB555C2B5BD06AEC685 + 7435E785D59C1756C2A3A68273BE6ACC6FA9C7BCFA0AB814B37F01DFF478CEDB + 2338CFE6B16A5ECE39FCB838CECD1342302686C71DEE8B915181181E198201C1 + AB295FE32A7A5C4A0CC6B0FDB0D8500C8D09310058D2F2E0C840F467DBF7037C + F1BE9F0FDE9555AFF732A682B44B0DF47D63C17C93CAFDB3D2B7162F34E0D700 + E0A59655B096DF64BDDC3EDB96BF82BF02BF5A965BE8B799BEBD749971192DF5 + 5BB61CEF2D5FC1FC5213CF5796BFAFCE5BD06B116CC7059625B0E201CB10E025 + C1610F4FBCECEE89D767B3ED9C0526B4D81B9E8B8C87B9B717AA2F6BBD57D85E + D6C0AFB14FF5F5A2BBC0B1AC7FDD8D14A24CEA3BD30D7DA7CD42DFA9B34CF8B2 + E7A728349905819F9DE28E17A7CFC10BD366A38F60EFD8197866820BFA4EE23A + 135D59E682A7C7CFC453E3ACF8C0264EF0E8E97892E9D3E35CD067DC2CE6A71A + C82B102C6BDF4747C815F4043C3A7C221E193A010F0F1D8F87875083C71BF8FB + B003003F3A681C1E63D9A38365056C59FB4A06FEF61B66246BE0FB14DA4D2EA0 + DF1C84FB5E1FE080BC375B00F7BA7F76B200BEEFE5F77BE1AFB315B0DC40FFF0 + B9B7F08367DFC40FFADEB0F435EE9E9D24007C779F37F1F02B037AA1AFF4E3E7 + B8BEC305B4B1EA652A57D0B6B5AF0D7D6D09044BB2FEBDE3E1E70DE835B0F761 + 0B023BEBFB06FE5A00B8D768C709003BC3DF5B01B02DB984BE8BFAF67BE79B61 + B0B305AF3300569DF3BBE7DE77D0BFE39DB82973BC0BBFF5FDF84DEFC89DEB6D + 37D18EF7F4F6FB79F38E5ECB946DFD7B130FB8F5FDBF73FE76CB9213001614BE + 93DBBFF30737C24D4ACEDCE1BB746BBB5BD9C77FB59CC1EE77B978BE5536DFEA + CDF7024E67F6759B32A775FF20D97DFC17EA8E3B7EB76EB7CE1F2C735CDFC14C + BF83AB7E8BC13A17FC39F59F0A807F9F8FEB5ED980F48FBC50FE3BE4F8517551 + 7CEF7BDF2EB7E4046E0DFC6599D2EF02C1775A6EA1ED65E7FCEF73C1FCDF2DFB + E6F65D37C2DBADF387E8767DFDA1FAD64D5C7294F5B671945B5FFA3C82EFDFCD + 078C1E2A4E727EA8DC5ADEFB20723CA4AC8713EBEE79E4B60FBA1B6DBEAD3BEE + 79B4F721DBFBD064DEF9417CBB87B2B3546FDAE821AD07B423ED2D671B0B005B + D6C106FE3E2CF8FB34BEEF80C04A6D306CC4E5EFAB5E75D49DCCDFF9F0531C6C + 58CBDF63FD1D2CFB1E970D007EAC2FEE12F8A5941710BEEB89E738B079013F90 + C5EF932FE26E07F8ED95960D04162496F5B02C85058E058B6D602C18FCAA6509 + FCDCEBB8EFC5B770AF62FFF67D1D3F7C96290765B2FE7D9003BA87DF1C80C7DE + 1A840735A07B5EAE5ADE34BA9B03B61F3D4BBD60C509BEEF957E78E88D0F8CC5 + B0F2721B2D587CFF6BEFE3A1B7071A4B61590CDFF7467FDC2FCBE07707E1B10F + E41A7A349E7040E087FA0DC503B21E7E6F1807A5A3F0E8406AF0183C3E848354 + A64F2926F0F8197892E9A3C3C6E3899193F0D4E8291C084FC213E3A7E299A933 + ADD82D1C64F799EA82E7987FC5CD939A8FE7A77BE0D9691EC6F5B32C80FB4E75 + C73353DC4C1C16B9E4797AD24C3CCDF55E70E5805EF15B983ECB41FC4B9C20BC + CA89C2739C18F499E5811738C07F85CB2FB3FC150EF8DF5A687DFDD96FE94ABC + B782939B95AB31427183A36331308413A4A0500C0A89C4E0C0300CF40BC6E0A0 + 700C0B8FC13007041E1412C10914CBA2E23025231BB30A8B305D0038350393B8 + 3C3DBFC8C0DF5149C918979A8209391918921287B7C382D02F340883A3B8AE80 + 6C74384646846150A01FFAF9729F56AF30EE9986458570822A57C8497029C8E0 + 04BE180B2B4B302D3309C32338890BF1C18498204E9CA330353D0A130CCCE504 + 332781DB8FC398A4088CE344505F008F0CF7C7A8703F0C0B5E8941014B313AD2 + 8793EC608C8DF7C3D0086F0C0A5D8E21612B797CAB99177C16D08DB6BE0A8E0C + C630EEE3F894244CCB4EA752CC7E0D8BE6C431291293D314AB291633385976C9 + 8E316E9D65F1EB961B035759FE6645603C27DD236356617C520097C33123270A + 3339D9762B4C34D6BE33F238292E4A845B318F5516C39C20CFAFCDC1E2C6022C + 6A2AC4B28E522CEFAEC08A1EB944ABC1CA358A77558D6594AC7D65F52B8B5B81 + 5F697E7B3916755642AE9817AFAFC7D2F50D58B2B6010BD7289EAF6070BD01BB + 0BD7D6C3B34BF179CB9956C0ABBB129E5CDF8BF9F9CA739B5E5D9598D7596EDC + 34BB19ABDD3CB8D4E71AB9322F77C9B6AB65C1561741DD8A14CCAA4E372E977B + DD2FD7659BBAA9A589AC4FC6CCCA14D36E7A79B269EB569B6E40AF01BCD582BC + A96CC37655C970615E40586D5CAAD4770ADCB9EC41CDADCBC43C037C333187DB + B29461963DEA32CCB6B55F4A6756B24F6E53A9B6E15EA7ED0A306B9BA9269DDD + 2017D102D802C6EC83DB98D328EB61CBDDB364BB7EB6E1AF0D829DEB6DD8EB0C + 8AED7A1B101B2B6027006C5BFACABA576057A971F5BCBEB217F4DAF05730D88E + 2D6CBD84535E16C1BC466C4B6007045ECDBC0D7B9506524A6DD91058F037626F + 9B01BE962C2BDFB0DDF5C6D23774679DB18251B960AFE2FA461F6836903742E5 + AC975B674979015F49F9D06DD508914BC5ADB2D2A933563CB2FA55AC5FB96F54 + 5E2F7765E5AB78BE29475A9172B4DD02C106F4B623715F33520EB42253C0F768 + 37B28E749A97C3C6EDF371CBED73CE911EE41CEE46DD477BB0EDAF3EC29EBFFB + 042D9F1E46C189F5C8677DE1A98DC64574CE11C5DBB52CA472CFAF47DE85F528 + B8B8C958FF0A02DB30D8B206DED20B858B3FDA86922B3B986E3780B8F4EA0EA6 + DB502C2946E4C5ED28BF2C2BB1DDA8BCBC8379B90BDD8E2A59B7B28DE2F40AE2 + 4AD51F6E400DFBAFE176CBCEAE45D99935D45A943BF202C035973618F82B105C + 4E1930F1D156747CB61F1D9FEF47E717FBD1F6E95EB47C62BB6C3E08B97B9695 + ACD4FCD911B47C71CC58FF1A60FAF571B47D7D02ED9280C91747D1FAF961E68F + 18F8DB217D7108ED9F1D44EBF57D68FE782FDA3E3BC06D1C30E057D6C07209DD + 727D3F9AAF0BA8F0B83EDA6260B4E06F2BD5F1E93EB45DDFC3742FBAB87F3D5F + 1D40FBA7BBB8DF8AF5BB83657BB1E6EB0358F39383E8FC720FBABEDECBED1F44 + 8BAC7E056EBE3C828EAF8FA2F39B63466DDCB7E6CF05B7B98F5F1DE73E0BFC52 + 3C9E162EB77C79A217020B1A0902DBF0D7B63A7406C0368CAAFDEC9089A15A25 + E0AB7AA682BF95262F6BC983069059F157597EF500CA05F13EDA6BAC35254160 + 03EF046CAFEC45C9E5DDA64DD935B9F8655B9EABA2AB02BA7B8C957809F385BC + 36F32FEE44C1873B4C6CCE5CB962BDC094D78D20B0E2C316C85533FB36B17A2F + CB6DB3DC37EF67BA0F391FC98A57D6BB0750704DF17D0F21FFA303ACDB8FEC8B + 7B907389EDD9D640DF8F592F57C5D78FA0982AFC84CB02C25C5F505740D7C4F8 + 653F8293B234B6DA1C305058B0B7FC33CBDA588059F187CB3F3F862A9EE3CACF + 8F18C92A5616B2B294ADE6F93716B302BEDF9C306E956DEBDA8E9F9E4627D5F1 + 3359FC9E45B703BC6EFCF545ECF8ED55ECFA9B8FB1FB6F3FC11E6A37F34AF7FF + 9F4F71F81FBFC4D17F16D4FD06A7FFE12B9C657AFE9F7F8A0FFFF5E7B8F8EFBF + B4F46FBFC0F97FF999D1878EF42CDB9CFEE79FE0C43F7F83A3FFF4350EB19F03 + FFF039F6B1CF3D7F779DDBFAD86C77EB6F2E63EB5F5EC216591C735F0485D7FD + FA43ACFDE579F4FCE23CBA7E2E4BE17368FD9965816AAC8279BC963530CF07AF + 0F730DF09C565215BC4E74BD08E80A0697F37C97B04C603F8FF78522FE8F14EF + 5796B939FCEDB34F6F36C0581F1754F27A91E5A7893D7E7EAB01C102BC96E5E8 + 4E930A004B0614B2DEC0E2AB2CE33556C87CE1851DE6038572FE7E9260B05C40 + 17F39A2A1234D676786DCB22B8FC2AEBAF582EA7150BB88EFF9B6AFED7ABF8FF + AEE57F4EC05E00BF9EFF3DFD5F14D75780D7004B81696D9BD7ADEDF6D986BF82 + BC02BB1628B6E0B06459C2EE432D8F5F50547DA8AD1DF35796C752B3C473DCF2 + 950D804F997C33532B26B0C0FC0934F037D0BED92EA61B79AF68FAFA10DBF15E + F2A5EE2756BC727913301E053EDE89C68F77A0E5B33DBC9FC863C26ED47FBA1B + 35BC8F95F1DE5A70A213C5C73A51797A2DEA79AF6EBEBE178DBC17D6F3FE6AE2 + 90CB1DB4F4D14E630D5C73D5724B2FD7D2CAD7B08D2C816D6B6049EEA18D1B6D + 1EAF60B1AC86AB29DD83BB794D35F05C547E6459119BF3C57362E031DB5608AE + 7359E7B84856B6A73720FBE43A641DEF41CA613E230F3723F3643BB21CCA39DD + 85C20FD7F19AD8C4EB85CFB2737CB6F1999277761D14E7DE8A172C002CB7CE1B + 2C2BE1B302C37CFEB1ACE4433EE7A8DC731BCD075809FB9B91C8677CDAD14E6E + 770DFB12F8DD80E2B31B5144E5CB02D8A1DCE3F2C2C167F1A93528E4F9CB533C + 623ECF33F82C4F3BA40FB7DAF8ACEE728468588FCC43DD465947D620FD401752 + F7B2EDC12EE3BD23E7E81AA4EDE7F37F3FC7021C6BA471ACA17ED23936483DC8 + B1C27E81648D073876D85D8DC8ADA588DD5E8174D6651CE23EEFA946CCF67244 + 6E2946DCCE0AF6516794B0BB0A713BE406BA0C515B8BA812531FB3AD0CE11B8B + 11D45300FFEE7C0408FE6EAE301FA8C9CD74C48E7A8E73AC0FE24276D771CC54 + 8BC05DB508DED360BCA468FC1422D82BEBDD1D1C67E903BB1D0AB1D18A101E57 + C8DE0E9317100EDED38E00A6C6D2D7017CE57AD902BF1D08DED781502A847959 + 12FB6D6F823FFBF2DF6EB9950ED9D3C5FEBA58D6025F96C96AD7779B057B7D54 + 26F06B248B4CC5E76DB420AF80EF0E87D53165C7FC954C1F5BD9767303BC37D4 + C35B2164A40D7558B15E63518E3B3770EEB181E3CB75B2922DC5EA0D15F015E0 + 5D5384C5EDB958D89A05AFC6547835A460695B16E72A0558C5F3B8A22D1B8B9B + D2B0B42503ABBAF2E0B7A6103E3CC74B3BB338EFC886570FC7CA3DB998D7C5B1 + 7C4726E730D99CDB703D6A89DC3177E562F5FA62F8AD2DC6CACE7C7877585AD9 + 9EC734977DB12DF3B3AB1338E64FE47E64C0AB3E19EE159C639547C3B38665F5 + 29F0AC625A978A65CD5958DE9205DF9E1204C9EDF45A8E9BD7561A77D43E8EE3 + 5ECAB9D562962D5DCF71F5D646786FE239D9D460DC5B6BCEB58873AEE56B2A39 + 272BC7E256CDD372B1B4290F4BEBB3B19CF30D5FCE8B02B8BCA22A1DF38BE3E1 + 59188D79F9E19857100ECFA208A66170CF09824BA61F6666F86246DA4A4C4D5E + 81E94C67A6ADC22C96B967FBC123D31FB3527C312DC11B93A397604AD4224C8D + 5B86A949ABB86E10666506635A8A1FD767BBAC20CE3B43302B27046E05119853 + 128F99D9119C9B86C1252782F3747DA41C882969219C7F727EAA0F8E0BE2E092 + 1FC7797334C6A58461544230E7CEC19CB3876222E7D59333623029330E537339 + 372DCEE039CDC7DC9A22B855E4617A6106A6E4716E9FCB393EEB2715A6C1ADBA + 00736B8BE1C5369E55F9F02849C7CC82446369AC7EC7B3DF19DCDEECF2342CE4 + 7C7239E7960B5A0AF9BB717EC439A687E699CD523166319D29EBE1DA12CEFB4A + 30AB4A16C8AC63DFB32A0BE15A5680994582CB2918CDB9FE60CEF38784ADC6C8 + E8208C8E09C1A8E8608C8C0D603E082322579B39FED0701FCA1F8339CF1F1216 + 8831F1913C2796DBE8E1B1A118C6F546C4876314E7F6A329B9831E10E4870101 + AB31C0DF17EFAC5C61E0EF07BEABD0CF7BB9B1F67DD5739EB1FC7D6FF932E312 + BA3FDB7CE0B3D2B88116147E6BD1220380FB792FC33BAC7F6BD97213FBD758FD + CAC257718297A96E859140F05B4BACD8C1AF2F588857BDE6E3E5B99E7875DEFC + 5EF82B0B60A536CC35F2B02C82A5175CE7E015B7B9D43CBC22D8EBC1F5B58E01 + BD738D5E762CBFAA7E6451EC26F7CF6E7866FA2CF411F0A59E9375B18C14A6BB + E1B9C9AE786EA28BE5CD6EAA2B532E4F76C74B5367E3E56973F0DC2457F4193F + 13CF4EE07A13D5D6D5E49F6199F494AC83C7CE34F05752FE99B12ECCCB22782A + 97A71B09040B023F3E72321E193E118F0D9D44311D32018F08F83A5C3FDB10F8 + 9181638CB5AF5C3D1B08CCFCC3B20276C4FFBD9FD23BBA070581DFB801806D09 + FAFEE8A57771F7CBFD0C0416FC550C60DBE2D776FD6C036059FF1AF86B00B023 + CEEF7748D6C0F73DCFF59E7DDBC05E415F59FDDA7959012B2FFDE89937700FA5 + F487720DED047F0D2CB6F30E8B5F01DFEF49CCDBFA3E6501E0E74D083F1BFEDA + B17F6F67FD6BC700B625EBDF9B01B0E33DB2936E75E17CA762FF7E87F5AF5DAE + B677FE88A9DE813BBD33BF9DBEF56EFCD63A037FF53EFF61037AED77F4E69DBC + DECF0BFA3255ECDF5EF7CF360FB0F3CE727000C3026CDDAD7DD4761C6D1CBC40 + 6EA18D0482B91F364BF853753BFEF1E7D6AD70F70FD14D1CEC7749BCEB76E5DF + A16FF56D73343B7F6BD91FB3FC5FA5DFB74DE3D6F9163EFAC770556728FBE7D4 + 7F0C00DFEDD02DEBFDD10058FA232EB4FF0E39FDA877981FF6E6324BF6B1F0CF + 66C02FDBD800F81608DC1B03D8A9EC26FD0F76FF2CDDEE86E6ACDBADF387E876 + 7DFDA1BAF5C66E64DFCC6D39D5E9E1210BE0DE878843DF7AB83895DFFA90B2F2 + AC737A60DD2AE7759C25006C3F607B1F9ECCDF7810FF11FAB143CC6B7D67CB60 + 5903DF213DC0FA079F32C0D786BEDF82BF9201C016ECBD212720AC759CE0AF0D + 8095AACC2AB7EB9EC50F1E7F1E773F7E03069B94BAFB092BAF38C3C655B4AC85 + 1D16C3B6D5B060F18FFABC6A2C82EFEDFB1AEEE96B01E07B9EB3E2FFDEC7819B + A0EF031CC4C912582EA17FD8E735A3BB953EFB06EE7696D67BF16DCA8A19ACD8 + C1060C332F0BE187DEFC000FBCCEFE4C2CE1FE785096C1EF0FC5534E00F86101 + E0B7069941E6C31F7020DA7F381E622A10AC2F131F1F320E4F0EE7E075D0183C + 3C780C1E1B31118F0E1B8707068EC40383D9C7C8097872FC54F499CCC1F36417 + F49930037DA96727CEE2B21BFA1AB99A2F2B9F9EC04133A565B9DCE9A3AF3067 + B81BABE157DC3D4D2C18F3F52607F8E66B504D1038B817F8D517A49A1CBC32DB + 0BEF2CE084633E271A5E4BF09ABE1C65D95B8B97E33DE31EDA07EFFB0470D213 + 8A81BEC1787FA53FFAFB041A183C24241C030343F0FEEA00BCE7CB364121181E + 1583B18949462363E3313A3E0163935338994CC488D8388C4948C0A8C4380C88 + 0EC13BC17E783F8493AF8850C8CA77645404FB0C64FF2BF1AEF752F4F75B85D1 + 71E1989812CB3402E393633086E9586A4262342775216CEF8BA141AB3026DC0F + 9393D836399C6D8238990BE636433034DA1F03437D3024D4976D990672E217B0 + 0283FD966148C0524CE4A4D0252D12E3E339110C59C66358C2B6DE181EE96726 + 876362C330298513E2A4281E0FF733261C1392E331393DD1B897D217C6A33949 + 9CC83693D3A239A18EE2243B06EEB9F19CECA7626E490ADC05740B12308D13F0 + B149011893E887A999117063B96B71225C0A2DABDFA99C786B422C8B5F415FA5 + 732AD2B1A8211FCBDBCAB0BCBD0C8BDB4BE1C549B257730116B79562992C7F7B + AAB0B8BB02F33B4A31AFB508739B8B30AF85136FB69FDFA158B2D5C6EDF3D20D + 0D58BAAECEC0E0052C9BDF5565A0EEFC6EAECF7A6319BCA6064BD6CAF2B7029E + 5C7F1EB731AFAD04B3DB8A21F7CD8ABF2BCB5E97DA2CCCACCEB42C78AB3330AB + 468034FB065C75485057A9A0B0EA24416259DB5AF055B097EB2BADB5AC74AD65 + 07FC65B925CBFAD758E93608C85A16BB6AEF61AC7AB330BB3ED3C8C0DC9A34B8 + D7A663B6E06F03FB6599B5BED5BF2C8EA73BA0F3CC8A649625B32E8D4A37D6BF + 92057D2D5965B23ED6F6B91D07EC7506BDCEB22D806D6B61DBDA576D6DCB6067 + D7D0C602D8E11ADA2E532AF0EBBD516E9E2B7B5D3EDBD6BE3618B6DAD8005870 + 587981E20AF86CADB6DC401B08CCFC965AE3F6B917FA326FDC4053413B1A11B4 + 53B17C9B11BEA78552DA6C62FB9A38BFBBEA11B2ABCEC4BFB301B081C1942C62 + C25817B1A31661DB6B0CDC95E43631DCBC34AD35D6BDA12C93254D38DB6859D0 + 57717C2D37CFED483A60B97E96E56FF221C5026E45E2A116241D527CC03696CB + FAA703A9C6DAB70D99C73A90A3F887D271BD50EE41E1A975283821EBDF4EE4B0 + 6DD9D94D68FFFC08BABE3C864AB9CF3CBE062527D6A3F0F85A64733BB9C77A50 + 706A03F2CF521FCAEAD7B2EE2DBCB0D55846E59E9335D47A53577C493066A351 + F1C5AD28BDBC0D2594F2C57203CDFA92CB9BA9AD28BBB41D159776A0EAB2B495 + DA82DA2BDB517F7507EAAF58EE9B1BAE6C655E56BE9645AF4070C949CBF24CF0 + B7E464178A4E74A2EAFC7A345FDB86BACB1B5176A6DBC4041600561F2D1FEF32 + 16B7021AEA57D6BAF5D7F6A04960561098C72EB508F0F21C1857CF5F5996BF6D + 94E043EB1772F97CC8B87D96D5AF24D8DBF9C561ACF9E608D608C21ACBE203C6 + 1AD7B860BE26D87CC011A377178F6B1B6A6491CCFD68FD740FDBEEE3393F88EE + 2F0F98B4F30B2BEDF842318977A0E5FA2EFE2E7BD1FDF541ACFDD951747F7308 + 9D5F1F4007B7D7F993E3E8E4BE757C7D1CEDDF1C43C7CF4EA15D167D5F0AEC1E + E231284EF151EBD8744CB264E671B450BD6EA0AFCB15F411D43A2C0D6D09FADA + E0D706C3825DB54CAB79FCD58255D70F58D6BE1F0BFECA42D2B292D472C5350B + E809F44925B200A6CA3EDAC7BA03A8F8F890016BC6CA92CB92964B28015D0BEA + EE46C165CBEAD700602AEFC20EE45CD88E6C5EA3391777208F7506FEB2EFAC0B + 3B917969B7057BAF1C30E037EBD21E645EDC6DD23CB97BBE76843A8CBC2B0791 + 734900789F81C41620B62C7F2503803F3D6A52590F1BCB601E8FE06F31D352B3 + FF0751C2E335BA7EC8B82AAE36B0F3242A15BFF693A3A8E4FA359F1F43DD5796 + EAF93BC8BA57AE9C9B8DEBE4536856CCD66F4EA1ED67A7D1F5F3D358F3F33358 + FBCBB358FFABF3D8F89717B0E12F3FC4C6DF5CC096DF5CC2B6DF5CC6AEBFBA8A + 7D7FF3090EFCDDA738F4F79FE3F03F7C81234C8F52B2E89575EF997FFB19CEFF + DBCF71E95F7E8E2BFFF20B5CF9D75FE2DABFFF1A9FFC3FBFC175873EE6B2A4F2 + 2BFFFE2B5CFE370718E67A67FFF56738FD2F3FC5C97FFA0647D9E7E17FF81287 + A803FFF019F6FDDD75B3FD3DBFFD183BFFFA2AB6FFD5156CFDAB8F8C25F2A65F + 5FE43E5F36EEA8D770B9EB17E7D1F693D368E031D7F15AD4B9D04702829AB2CA + ADE1EF5DCB7367AE45D629C66E9D2C86D94656BF8557F7A088D782AE0B01E002 + FEC6B967B7A288D781ED26585054F057B2DD3E17B35E2E8265156A5B002BD53A + C60258EBF2BF29F05B784196C7FAC0807DEBDE272B616EAB986D8AAEECE6B6F7 + A282D77AF927BA5EF7F01EB6DBB42FBD2C38CC7DBB6259B2CBAAB886F7877A41 + D6AF045A2DD8AAFF90AC7A0D00E6B60DA0667B6D5F0053B241B0CA6CF7D18A91 + AD6329E7FFA04630F41341E77DA8E37DA5FED37D68E07DA6E9CB4368E23DA7D9 + 7CEC718CE9312EEB830FFECFBF92E40EFA840582BFB0FEF70D02BFFA0084ED1A + 79EF6AE6BDA695F79FD6CFE5429EF748DE279BA96AC1CFA3ED283CD9C9E55DE8 + FE893E7AE1BDEC4BDECB785FAAE67D4CEE9FCB4EF6A0E2F45AD45CD8C47B1ED7 + E7FEC8FAD786BF8A096CDC403335EEA1791CB212AEF9780F6AD94E718725C15F + 415D593B1B8B672E37F0FF56CFFB9040B052A982E7446EA49B797EE5BA5EF057 + E74E6EEE25F525A06C5B1097B17D09EF19B2EECD39BD0605E7F8EC3B4F29FD70 + 3DAFA58D28D533E9E26663055C28F0CBB4C0C882BF3600CE39B3CE4810389769 + DE492E1FEF41C6513D6F5B0C644DE73337F7540F8AD84F31FB10F8B555C0E76E + 1E9FBB02BBFA204B1F66E59F5C8BE2B3EB507C8AE57C96A7EE6B40DAFE26641E + 6943FE313DDBD6209FDBD0733887EBE41DE373FB28B7C97180649ECFFAB84BCF + FE7D4D48DED788D483CDC83862C1DDF403EC8B69C6418E23F6D61AABDE843DD5 + 2C6F40FAFE0624EDAA46DCF652C46C2942F8863C84ADCF659A8FC84D45542122 + 3614703997F97CC46C2B418200F0D632961522786D21C237715D8E5FE2390652 + 0C627DA026B7D50A5B11BDB709917B3936924BE83DF55403C74BFA50AE85F916 + 8E939A8C9B687D5427101CB4B31941BB650DDC8C407961D9DB8EB0FD1DC6C257 + 0058D0D702C02D060207EE962CEB612980EBFB0B28330D601BFFED2D08DC6959 + 112BCEB02C72AD7529B97CA66401BC4A32F0B7C1B870160096A5B0893DCCBC1D + A777E5E606234162A506746E507C5EA9162BD6D762B9092543ADA9C092CE7C2C + 69CFC2B2CE5C63CDBBA49DE3E1FA14CCAA8C832B35B73601F31B52B0B82503CB + 58B7427179992E6BCDC60AA6B2AA5EDD5D84551D79EC230B0BD7E461D1FA422C + DB540CEFCDC558C5DFC787BF8F2F7F0B9FB59C03756462494B2A16D627725E14 + C5795330DC4A23304F56BD0DC9AC4BE736B2B1B4358BDB4E826B95F621090BEA + 53599F8AC54D19F0AA91EBE118CCAF4B857747017C7A4AB86F7958CA63F15DCF + F1F2BA2A2CEDE63CABB318CB788CCB0581375AB0D786E33A378A892C0BEED55B + 9ACDB9F1DE500DEFB51558D953C67DADE07E1463456B2156B5156225E703DE9C + A3ACE4BC69795D0E9656717F4A63E19E170AD7DC60CCC90B336E9FE7954A9198 + 5D1802D7BC0078E8F872033033C307AE99BEF0C856CC5F5F4C4D588E89D18B30 + 257A31DC527DE1551881F925D198CDFE5C32FCE19A1568F28A69EC961D04B79C + 50F613811919219896118CA9E9819CABB2DFEC307895A7C0BB391F2B9AF231AF + 24C97C943CD98428D247CD21189B20301AC865955B10785A563C66E426635631 + E772159C0B16A5637C7A0C46A546626C7A34462BCF79F0B8CC584C95DBE8FC44 + 4CCD8AC3942C5911539C1F8F4BB1DC4E4FCD89E1EFC8F971592A3C39E7F5ACE7 + 7CAA2E170B35EF6C2A824B7536A696A76372591A26713B130BD3315690393B09 + 530AD231BD240B93725330362596F37D6E3F399A73F410BCEFBF1CFDFD966358 + 982F4645F91BD7CF3A8ED1B1B2FA5DCDB9FD4A0C095B65E2000F09F5C7B0F020 + 8C880AC5A8D8700C8F0EC1E08840A3A1B12118CDB9FF84F4388C48D087EE7E18 + 18E88B0F027CF0D68A25E63DC500DF95786FD50ABCBB7431DE5EB400EF2C5988 + F7962F457FEFE5F860D54A0C5CEDC336ABD06FC572BCA9D8BF6CF39EEA562B34 + 97B771F7FCEECA95E8B78ACBDEDE466F0B0E2FB740703F2EBFB968315E9EE769 + F40AF5BAD74213264CEF71047C65B52B782B18ACF73D7AB7F3F43417F499E18A + E75CDCF1FC0C37BC30C31D2F3A00AE0D745F7297B5B0058F6D08AC3EE5465A86 + 087DD4C7546ACA4C93F695A7B9292E7866E20C3C3B69269E63F90BB2069E348B + CB2E060CBF388DDB9BE2866727B2FD849926B5F336009664F56B43E0A7C7721B + E366E1A931D3F1E4A8A9789AA9F4D4E8697882CB4F8D9966CA9F1CC574C4143C + 3E6C121E17087640E08707C8F5F3283CD47F64AFEB6781E04704821D9215B040 + B0DECD3D2418FCE6C05EABDF5EF8FBCA7BB8FBA577F1C317DFB12030F306F8BE + D80FF768F9E577F123D6A94C92F5EF5D7DDF301204BEABCF1BF87E9F1BD0D7B6 + 0836EAFBA6B102BEEBA9D78C0B6749B0D706BF762AABDF7BD8EEDEBE6F19006C + 5B01DBD0D796297BECC55E8BDF3B1E7E0E7FF1B06511ACF8BF72012D086C81E0 + 1BD6BFB703C0DF6501FC6D00FC44EF7B695B36FCED55AF05F0B701B0AD5E002C + 23AB3F0100F7D60BECDEF530EE607FA64FFBDDBC297FE8E618C0324C737AD7FF + BBD4EBFA59F0F736F582BF96C1D81F6EF9FB87E876FCE3CFAD5BE1EE1FA29B38 + 98CDBD6E5776BBF23F46BD1CCDA92FE7FC1FB3FC3F45DF62A60EDD96ABDE8D3B + 1C3CB557CE50F6CFA93F16005BED2C00FCADF5CCBA3A78C78FF29D7286A2B7B9 + D0FEB7C91C07FF6846CC3BC35FA9F7582D006C20B040AF53B991CAFE1702E0DB + B5FB5374BB6DFC2EDD0A788D6E05C04E6D7E9F0BE89BCAF580B101B0E361F61F + D14D0F363D481D0FD8DE8726F3BF0B009B87B153DE592A13F4FDC1834F1BD900 + 58327D4A0F3CC981C093F89E43720B6DE7251B0A5BAEA12DF5425F67302C6BE0 + 4759FF581FA31F08022BFF685F5367590D4B6ADBC71A9850DFD72045AE4A1E7D + 163F7CF439AE67590EDBF05740D89620B0E532FA150E965EC77DCFBD89FB5E78 + 1BF73A5223815CA682BB3F78868328B6FD21D31F38D27B586E2C801DFAA12C82 + 1DF058AEA10583EF79E12DFC98FDDCCF81E183AF59305956C5B2147EF0CD0130 + B18339D07CEC3D0E36E5FEF9EDC16660F990E2006B00DA7F381E1B30128F0D1C + 6D00F06383C7E2C921E3B93C068F33FFD4F0897862A85CDA8CC6A3C3C771203C + 057D264CE38099836B0D9CC7CF409FB153F10C07C0CF8ED73207DF1A1C2B6E0A + 07C74FB3BCAFDA4DE6805B83EFE91E78CDD5132FCD9CCDC1FE6CBCA6B82F6E73 + F1A2EB1CE332C84C1A3841E8CBC1FF8BB366E3750F4FBC2117410EBD2C17D26C + FB3ADBBEB96009DE59BC02EFCB2AD8DB1FFD96ADC6DB8BBDF11625383CC03700 + 1FF8F8E1FD55AB399909C4808060F4F70F3220787098E2054760506838068484 + 6270B896A988300C8D0AC3E0A810F4E7C4AB5F90157F67586830468787626800 + FBE2646A10274C234302312D290ED393E33132329813353FF45BBD1CEF782F46 + 3F9F25DCD6320C0EF6C6F0E01B00786C3CB71FC2F54339290B5A86770338410B + 5AC1899E378650C343566114359C6543FD976034278182C0A3227C3038700986 + 042EC5C8301F8C8EF0C388705F8C8FE38439251AE313A3302C22004323FC313E + 29D2C05F7D293C21455F0DCBFD33B71D1B8CF19C604EE6E4D92DC702C0C6AD73 + 413C5C1DB077465E2C979D206F75163C38C19DC949F0B4BC18787022BC9C935F + C5F75DDE518E854D8558DC5204EF8E0A787796637E4BB101BC0BDACBB0A8A314 + 0BE576B9AD049EAD4596AB656389ABF8B605F06C29C4FC8E122CEAA9C4D2B5D5 + 58B24660B71C0BBACAE0D5598A396D45F06829C0DCB662037C17B3DE12B7D359 + 82B95C7FAE8032D339CDF998D32457CE72EB2CD02B17CC96AB66496582BC2E36 + F4AD4A33E56A63202DEB6D57CBCEA960B02C879557FF1E5CD63A331C70D8BD36 + ABB7AD737BC5EFBD616DCC36F5391618A6DCEAB31CFB96C172D653CA0BFC6A5F + DCB40E65DC4D1B715BB582D4E9EC338BFD735F1C56BF3610565E750604CB12B8 + D982BF36E8B541B07399E0AFEDDE59C0D706C1769900AF6459FFB2AC231F8BBA + 05858B99AA5E71D2CAF8DBC91A5896C125CCCB05B4DC42571828BC6203AF8B8D + 72096D81E055C6EDB35469E4BBADC6C400F6A7FCB633BFADD6E4FD77D4238052 + 3E800AA24277342074B7DC380BF83699347807EBB756C17F4B2502B755B35D0D + CB6A0D000EDD598BA06DACDB5C6E14B8B9C258F60673BB06F20AFE528AED2B09 + 0887325599C0AF2C7DE5EA590058523E61BF5C4C2AF6AF0584130EB09EE5723B + 997AB8DDB881CE382E0BA46E649DEC42F6F10EE49EEC44EE894EE41C6B471E97 + 0B599ECF34E7700B728EB49AB292D3EB507A7A3DF28F76A1F07837CA4E6D4081 + 5C4E1EEE40D1A935283EBB1E0554DED94DC83BB7C5A8C0C446B4ACA0141FB8E4 + D23694C81DA65C459F598FE20F3741B123CB2F6D35F1234BB85C7A512FDC37A0 + FCF2161347B2FAF276D45ED961DC8DCA42B6516E479517A8BDBC19B59737A2FA + E27A63D55B756E2D2A28C15F015EB98016F8AD34F077073A3EDB8DC62B5B4C6C + 60C15FB98956FCDFB273EB50C1BC49B94E8DE2555E63DB4F0F1817D036F8B5D5 + F6C571B47F7DD258FF1A10FCE551347D7690FB272BDE3D68FA741F5A3EDB6760 + 49CB277B2C70FBF561E3E6592E531B58D6F4E97E345D17589195AE5C45EF376A + E6BA1D5F1D44D74F8EA0875AF7B3E358F7936358F3CD51F47C7D84E931ACFD29 + F7C3B4DD63D22ED6ADF9F909ACF9D90974B24D1B973B7F760A5D3F3B6D00B5E0 + 6EFB4F4F19CBD126C5F1FCEA305AE5CE95E52D5F59F0B74D96A502C4CC377E7E + 040D9F1E46DDA787507F5DC044E0D7B2FC950481057C2DEB600B62D55E572A1D + 4635DB56B2ACE2938314F3AC93CA99579989D72AA82B58EA8077B2A894D5AFE0 + 9ACAEC98BE72B75B728DF52C97F5AF1DDBD79660B062FECA12B890CBB2FA1504 + 967205852FEF419EAC7C8DE5AF03E65E3DC8D4B2FECD66B9606FDE4707597E04 + F9D78E20EFCA61B63DC0750F9A7213F3F7DA8D54D0B717FE1AAB61B986B62C80 + E56A5A2AA58CC5AF80E6A7475123D7C73F398B46E9EB333C7FC70DD46C9265A6 + 01EF27D1FA13CBB2B7EBA7678CFBE49E9F9DE36F7A9EBFED39ACFBE5396CFAE5 + 796CF9D5396CFDF5796CFFAB0BD8F1DBCBD8F9DB4BD8FD375770E0B7D770E8B7 + 9FE0C8DF5EC7B1BFFB14C7FFCF1738FD8FDFE0DC3FFD141FFEE34F8C2EFCD3CF + 70F95F7F818FFEDD02BE9FFDFB5FE2CB7FFD0DBEFCB7BFC257FFFED7F8C9FFF3 + 37F8D9FFFBB7D4DFE0270E7DFDFFFE165FFCFFFECAB4FDE45F7F8DABD447FFF6 + 2B5CFAD75FE2C2BFFC82FDFF0C67B98D33FFFC13E346FAC43F7C85E37FF7058E + FEED6738F4B79FE2C0DF5CC75EEED7CEBFBA8A1DBFF908DBFFFA0AB6FDF62AB6 + 30DDF897978CABE80E016E9E83F66F4EF37F758AFF27B9263F667D58F195E5C2 + 58E7C97247CEF3C6FF9F6D1D5DFDD951CB629CBFA5DC3157EBFAD235C1DFBFF0 + 1CEF2BB2EC75405D8154B9142EBA28E0B719F98A61EE00C16A57C16BC806C1BA + CE2B78EDE9030559A7E79EDE86F4E3EB917596F732039EF571C14E5E733BB9BC + DB2C175E9615FA56E49EDF6A5C512B667091AE53F65D2470ACFE65C9CCEB5B16 + CA15BCA6253B5FCA6DDB56CA968B68D67D6CC98A5D6CB94496E4CE5A2EABCB99 + 1A30CA7B55DD67FB51FBF10E54E91E26F7F657B7F3FEB919F572212F10FCD901 + 13D35CF17C5B78CFD2796EE575D8CC73D8C4FFAEE5E5C0F274D0CCF3DBFA25EF + 135FEC33EAFC46DE0C788FBA6EC529CFE5B3216A4321320F341800DCF535EF61 + 82C49FEDE57D730F6AB96DC563AFE63DBDFAC38DA8BDB885F7C35DECFB86FB67 + DD63EB788EEBF8BBA8ACE1132BEEBAEEBBD59FC8025AC7C57B8DC3E2D71900CB + ED7313EF11DAE746DDA7B8ACBE94CAB3C1E6BFFE08DD3F3FC3F3B11F35D7F6F2 + 3E76000D6C2B006C5B110B0CDBD6D315DC9F0A9EA7AAAB8A31CCEBC17CC0B419 + 85DC7F3DB3CA2E6F332AE1711409FC9EE3338CCAE7B34520D886C182BFD967D6 + 21ED289FBFFB9B902A587BB80D057C469608FA9EE3738F6D4AD956CB857225CD + BAFC93960AE412FAB8206E37728F74591F611D6943D6FE06246E2F47F4A662C4 + 6C2A45FCB60A24EDA842FADE7A641F6AE1EFD186BC231D7C2677F3F9ACE7723B + 32F6351965F3B7CA96E5F08146A4ECAB47F2BE3A4B7BA9DD354895ABE75DD588 + DD56469522656F2D320F36206E5B09C2D7E72172431EA236E623627D2E02BBD2 + E1D79602FF8E3404756520A02315BEAD4908EACE60DB7CA3D0B5B95CCE464057 + 1642D717219A639AB8EDF548DC2317D31CA370DCA2D0155194425FC4735F630E + EA83B97AE32125C240E046038323F736237C6F2BC2B86E30C753013B35EE92AB + 688EAFF6B723FC602742F676226097057CE5225A96BE56DCE0965EE0ABBC2DC1 + E0C09D82BE2D06FC0A04DBF23356C03740B0E5D6B9C9004B9F2D0D06620A2ECB + F257AE9E556EAC82058165F5EB0C7F37D641717B4D2A57D0EB6AB18CF388A50A + 27D3A98F51F3B1B82D0B4BDA32B1A4230BF39B53E151150797D2487854C66251 + 432A96B6661A202B2B60C1DF959D79F0ED566C654B82BF2B5AB3B18CF58BBAB3 + B0B087EDD76463257F07DF75B9F05993059FEE4CAE93099FCE742C6B4AC4BC8A + 704CCFF1C1F894259896B90A73CAC231AF2E160BEAE2B1A431198B1A5330BB32 + 1EB34A634C7CDEF9F5E92CCBE07EE4607E03C7F8354958C46DAE5A570EB979F6 + 66BA546EA6375463E5863A2CEAAAC0929E0A2C13E8DE50CFF3C1F3B2AD0D3E3B + DAE1B3BD1DDE5B5BB82CC86E594EAFDC5CCFB61C7F1BB7D175462B38875ADE55 + 8AE59D9C5B714E32AF22190B38D758C479C722CE43E673D9BD200A6E0591985B + 1C03CFB238CC298D86477104DC0A43E056140CCFF248CC2B09877B6E2066E707 + C2B3301873F383E09AE183E949CB31356E09F3ABB1B49AE7BA42718303E09AED + CFB6C158501685F9A5112C0BC28CB4D59892C47552FD303DC39FF3531F4C900B + E9245F6E3F9AFB938685559C03E54561524A00CB833039250C53D3C29986625C + 4220C62784301F8E299CEB4E498DC4E4D4289E7BCE657313312523166339F71E + C3FA89199C13A747321F8671A9E1C675F42496190B62E6A5E9B9F19896136B2C + 81A764A92EC2B89B96E5B115FE28119E559CF395A5734E1C87F1AC97C672DB23 + 12C230302AD0B8631EC5FC58EEC788F8100C08F5C5A0105FF351F8E8D8100C09 + F5C10781CB38EF5F89D131FE98941C8669FA409BEDC72504B18D3F46B28F61A1 + 0118AE5050914118C2F9FD202E0F0CF3677F7E46CA0F539F89B2000EC107DCC6 + C0605FF6BD0A6FAD588C7E2B9618B82B2BE0813E2B3170D50ABCEFBD14FD962F + C63B4B17E1AD250B4CDCDF7E2B9619E86BC5015E8CF76519CC75FAFB78E3031F + 1F0CF6E3B6FDFD7B6300CB0AF8F5855C7F29FBF2F666CAE5050B8D5E9BBF00AF + CF673A6FA101BE02B6B61B68B97FB6D3675DDC0D087E8E7A7EBA2B5E986E4160 + 63C13B8DCBCCBFE23ECFB885965EE13A7A4F242BDFE766BA1900AC7672F72C83 + 843E93A6E3A94953F1D4C469787AC254F499381D7D27CDC0B3AA9B301D4F8F9D + 8AA7C74F63D94CE31E5AA9969F619DA4FCD3E398EF05C0330DF095FA30DF4740 + 78B4057AAD775BD63B2E49F5D6F2543C3E7C72AF15B089FB3B783C1E11007658 + FCCAFAD748109812FC7D947592DEC93DF8EE103CE0784F67C7FD7506C03F78F1 + 1DA31FDA96C0B2FA95BB67E6EF7E99650E007C2F759305B000B003060B040BFA + DE140F583182FBB09D93A5AF1DFBD7795916BF02BF773FFDBAD5F6F11BF17F6D + 59B181AD98BFC6FAF761CBEAD77609ADD8C04AEF7448EF586F07809D25EB5F67 + 0B60C500BEF33600F85B725800F75A02F70260E9DBF0D7E8476CA3F7DC7A6FFE + C7006059D9DED2D694CB02D8F11E5E00D8F4EB78376FCA291B001B507BBB77FF + B69C18C1EF82BF92A993B8DEADCCC1D67F845B38AFF35FA5DB01DEDFA76F732E + 1B6EFE9EB23F56761FFF11FD67F4F1E7901333BD49BD4C55CB0E8EFABD1F3A64 + 8360CA19AEFE39F5C700E01B6DFE3F007C93CC71F08F66EB36E0F75BFA1F0680 + 6F77A3FA4375BBFEFE14DD6E1BBF4BB7BBB9F79639CB51A707452F00767A9018 + A9EC16F502E0DBC8F96175D343EB475CF7BBDAF502603D30B9AC872AF37AE0DE + 0A77FF50DD00C04CEF7F0ADFFFF113F83EFBFBFEFD964C3BB983965BE8FBA9FB + 1E63FA442F08FEC30130DB3CFA0C7E60A02FDB997C5FFCF0310D441CED6535CC + 7606083B6252688072D743967EF0F0B3F8C1A382BFCFDD047E6D2BE1BB6421FC + A4E205BF827B38E0BA8F03B1FB057C9F7F0BF7BD6049305816C1773DF31AEE7C + F225DCF514DB3B00B0242B606331FCAC6209BF6960F1FD1CF4091C0B206B5956 + C34A1F60F9431C1C3EC041E083D4C382C06F0DC423FD06E3A1B7ADAF09ED01A5 + 628B08043FC601E7E30346193DDA7F8401C14F0E1E87A7874E30E91383C6E299 + 21E3F1CC88497866D4643CC7C1F18B1C383F37C9057DC64C65D914F4E5E0F799 + D153F014F34F8DE4C0782407C6A366501C0CEB8B48D63D3D9603F109B3F0EC04 + 17038D5F9CE68697A673703F6B2EDE9CC3C9C26C4E0E38D0975B1FC588E9A338 + 2E33DC8C5BA03759F7BADB3CBC3A6B0E5E132456DC61C7A4C08E31F3F622B988 + 5E8577167362B27005DE5CB002EF2CF1C6FBCB56E1BDE5ABF0EE32E6BD15C726 + 101F7022D3DF9F9399E0100C0B8FC0C0A060F4F3E3442A341883C342D03FD81F + 43A34231322E9A93AB20BCE3B70A6FAE5C86FEAB576178700086FAAFC6075C1E + EAE78351A19CE45123B8CE403F6FBCB37C21DE5FB50C03FC56A29FEF12BCB77A + 318604ADC28890D546A338891B1AEE87FE812B31287C35DE0D5886B7FD166340 + F00A0C8FF4C5F8F860CC4C8BC4B484504C88E03AAC1FCA3EA4217E4B31226805 + C686FB62524C10264607636CE46A4C880DC2F4F4384C4989C188087F8C88F2C3 + 644E68A7E72471E29B80A9199CD4B24F4D2A4746ADC698A8004C62FF2E2C77CF + 95D56F0CC66745C0A58893DAEA2C78D66463415D1E9628E6516321E6D7E7C18B + CB02C18A85B4B4A518C19B9A11B6AD0DAB37D46359473916B51661497B119676 + 94C28BEBCD6D2E84575B091674946021CB047A3DEA73319BF26A2AC082962278 + 321510F66C2960DB22CC6F2FC6FCD6620386055AE709FC2A56165381E0059DA5 + 060ECF67EAD5510C4F6E4FF55AD7B4E53A36A0B5E2F25AF179B52C5956BD99BD + 56BF02B0769D11DB9A3E1C6EA26D90AB7D96CB2F81DCB9CDF998DB926FFA3571 + 80056C59EE51CFF63C16B537EB303FB729DF1C9F5B9D5C4B5B005870DA8E373C + 5BF18729D5BBD5B0BEC6B22E36FBEC90B134766CC35D56BD8D5906F20AFECAF5 + B360AF00B0E0AF89FDAB7325302CD7D04EB057A0D7CE3B970BF8DA96BDB605B0 + CA95B75D3E4B0BBB04840BB05071C6BA1567CC82C09201BF0E20AC7656BD0586 + 6DF86BC7FBF5DE20105C8955B2FCDD5C01DFAD95F017C0D54B47F3E25120B816 + 7EDB6A4C3E705783290FDA516FE2D90900DBE03744962B82C35BABE1B7B912AB + 37951B090407C985F38E5AAA0681DAC6E632037F83B75421886910DB19F7CEEC + 5BB03798FB63E2E7715956BF7A792AE09B72A8D3405EE56D17D072076DC7FFB5 + EB0CFC95FBC8635DBDCA3CDE85F4A3EDC83CDA869CE36DC83ED68AAC232DC83D + DA8A3C95319F75A811D9879A917BA40D05C73A5174A20B05C73B51C875F38F76 + 22F3402BDBB71B97CBA5E7D6A150564CE73623F7F44664CB8DE5E90D28382FCB + A92D28BEB00585E73721FFCC7A149DDB80920B9B0CE4ADBEB2F5FFCFDE5F80D7 + 71A45DBBF04C26CC6008931387632766666626C9922D8B2DB06449962D5BB624 + 5BCCCC686666666666877992C9406692AC7FADEABDE56DC5197A67BE73BEF3BF + 57AE95EAAEAE86DD6A7757F5DDEB79A895C83AB60CE9879720E3F02293D357F3 + 26A7E4B1E5C83FB1DC38738B0D0016F865BD80EDFE79D46C641E90E3773632F6 + CD41D6C1F9D4422385832E38BE028527561A6851C2F5F359A750D15A5F0E6181 + E2B47DF39177628571DE9A9C96CA6F797603E4E655C86481A8AA6B7B8C2C68B2 + 1355575867C23E6F3790B8F8DC461EE31A149D5A6B9C72655401B7A9E39033B9 + E4EC1AA3C253ABB87D0B84284473E9A5CDDCE656940B105F517E5ECBBD2BA0BB + C0405D4E0B00B36EE18DBD58FCD17ECCBBB11B1597B7F0B8B661EE87BB30876D + 2AAEF138AFEFC09C1B3C36850856FE55AAE2FA5E94EBD86FEC43C58D3D267C75 + B1E08F09256C415F85162EBB2E08B90BC50A012B5062735E1A6822E71C65402F + 552097E0155B385B6E2BEF9CF29B6E471EDBAB34F9522F6C37E0D3E8C24EE380 + 1510556864B937EDE0D72EE570950473550A00679CDD62E0AF9C9E49272DB86B + 72FB5202BF82C21664B34160D6259E588B8463AB90707435E2A9B863EB4CB867 + 415ABBF337964A38E5E0EA3DB9D500DFB8E35B6EEAD826C40BFE72995CC10AFD + ACB6260CF4D96D06FE1A71BB0AE76CF2F9F2B709F866B03EE39C421AEF42DE85 + DDC8BF24F8BB1FA51F1E46F9078779DE0FF19CEFE7DF651FFF6607F8B7B1F2E4 + CEFFE820167E72C8E4F055F864B966977F7E02CBBF3C8E955F1EC5EA2F8E61ED + 1747B1EEAB63D8F0CD096CFAF624B67C771ADBFF700EBBFE70017BFE20E87B19 + 07BE5768E7AB38F2E70F70F2878F71FA874F70E62FD40F9FE2DCDF3E37BAF8D3 + 57B8F6D3D7F8E06FD48FDFE043EA939FBEC5A7F80E9FFE4CA9A43EFAF95BB3FC + DADFBEC2E5BF7E890B7FA37EFC92DBF80267FEFA394EFEF5531CE7F6153AFA08 + 75E82F1F70FFD7B1F7BB2BD8CDE3D9F9DD656CFBEE1236FDFEBC7105AFA34C58 + 6A4A2078E9E7C7B0E8D34358F8B115267AE1272C3F3A84791FEEE7F950686B9D + 1F7DC8A0EBD4929CD18557F7A090FFFE0A280161E515565E645DA782A8B17B17 + 235679C9F72D8542C90B9A0A00CB459B7A6CB5C9031BB76731E2789FAA6E7778 + 5575BED8144E271FE1BC71A66FE07D8CD7D2BE6548503DAFB1C413BABE5620EE + C80A960A3B6E4D9BF9C32B2D57BACD9D2E677A324BB9830593A594A3BC860FAF + 669D60AFA030AF77EE47FB4B3FB10E1972FACAF16B97CDF96B07C302C20656B3 + ADDCACC6317C72ADC97F9EB2A712C9BBAB90BA772EEFD75548DFBFD0E41E572E + F29233EB517989F79A4B3B51C1FB58F9C51DBC5FF1DFF9D9CD9422116C3279DE + 8BCF09185B79C98BCFEB7EB5165517D763F6850D28E23D79C6DA5C842D4931F7 + B7F9D7B6715BDCB6A218F01EAB7B9C42D90B3E171C5986E263CA57BECA7CC463 + BFB71AD04B955C50887DDE3FE55C3EBB91CB369AE982F3727FDF0CF95C1D025A + 0057E2F1090097F2BEA2BCEB25BA5FD9E0EFE2CF8E62FD1F2F62D917C751C27B + 92720B170A34733B8E720C2F6D0033F79DCF7DCB4DAD739A714261C15723E3B8 + 1CE53CB73C872AD3F9F716184E38B894D7C512FE6D97F16FAABA15ACE3B36FDF + 42F36C8D96B3761B9FA37CF626EE9967A06F169F83993C279A4EE1DF2589CF9E + 38FE8D6277549930CFA97BF501931CBF0BCC8756717C8647AECD43D8D204842E + 8AC7E4A5A998BA320B5356646012A7272F4FC7D455D9885C938F19EB0A316B53 + A901C1F1DB2A395D86999B4ACC875CB17C9E4FDF5880A9EBF310BE2AD384749E + BA360711ABB3307985807206A6B06E32CB49CBD310B62C05410BE3E03F2F0613 + E6CF320078F28A544C5A966C80B014CCE5017362E037DB82BFA14B534C19B430 + 814A44E0FC78042E4C46D0E2744C64FF2A6C653EF75B8010F6A1D4AF09595B88 + D08D2598C27EC4641EEBC40D7200DBA2A1500A111DCADF132A606C960B0C9762 + 8260F0BA32046DAC44E8963908DE34D7961FB80C01EB2DB7AF9F0D08076EAC42 + D0E6396C5B55ED0256F8E8F1AB2BD89753A9B69683D80E7FBD56CAAD5A02EF95 + 0AE52CD9EACCF23293EF773CD71120962BD87D79B101C376792EB7C23FBB7399 + 421EAB8DC7B262F62F8B3076413E46CFC9A2324C6EDEB1739330664E22CB648E + 4F12E0541A635CBFCEC5511853118B31B3E5DC4D306199DD587AB22FEBCBBEAB + CF827478CD4BC1D88A7803665DD8D6A92C0AC38B233092722E8BE418691A5C8A + A7C0B524021E65D3E05E3A152EF993302C3D007DE33CD13D7A2CFAC47B61507A + 2006A44F3020D8393F0263CAE4F48DC490ACA9185910C5F1551486E44EC33096 + 4E653CA63949FC1D19F05C9203CFA5B9EC2F2B9A8E52A614B0FF5C88D173D9A7 + 5E5264CE9DFBD212F3BBC72D2B83E78A4A4B1C9B79C8F9CB736201740173C1F2 + 026E8FDB5894C3ED2B32532646EB634F8E074614C56238C713A3387618CDB1CF + A89C680CCF988E115954F6540C4E556EDE00748BF6468F99BEE89F34012373C2 + 313A6F0A5C72C3E19A1701D7DCC970C90EC3A88C891892C071E93437F499E10E + A7AC608CC80840FF382F0C49F28353E644B69D845199219C0F34AE60E512EE3E + D50D3D6728C4B40F7A44729C1BE9811ED1720207A2F74C8584F6E538D61DED27 + 7BA0CB54A5540A468FE941E8328DCBA6069AE96E9141E83A6D027ACC08412F85 + 84669B7693FDD0614A20FF1E21E81DAB344821E83C3D109D674C40F799966BB8 + 7BEC14F4E2785800B8774204FA2547A24FE25403803B4472AC3D2DC0E40556E8 + E98EDC7EAFD8C966ECDC79469059D62EC28FFB198F96937CD1540EDE505FB453 + 94AFC902B7DE68AA70CFACEBCCB177D76913793C0168A20FC103C7B18D1F8F2D + 0CFDE2A652D3D08DC7AE5CC0ED42B97E7000DA4CF447AB89E3D12A44F237B980 + AD944FFE06001B081C6AB9829516AA65F078340BF44103CFB16830CE158DDCDD + 4C88E7F73DC65202BD2E78678C33DE1C3D02AF0C1F420D35D0570058E05711CE + 549A50D06E72107BA2C5783F34F1F2C6BB6E96FB57A1A05F7372427D41DFD1A3 + F1868B0BDE700803FDFA486E7F942BDE1839DABCA7B143DF97070F371FF9BFA5 + 7A457A9323B8FF60037BF54EE8953E32080CC56B0A07DD7F18EAF51B8C9759F7 + 2AEBF43EE825CECB2D2CF0FBBCD290C9EDDB55B97C7BE2D92E321BF4A038DF49 + B0D79240F0531DBBA376DBCEA8D3A60B9E6ADF1DCF76EA45F5E4740F3CD3A127 + 9EE948B17C5AA54C0D9DB50D0BEA3ED381F31DFB9852F0B74EEB6ED5105865DD + 36DD6FBA81395DAB6517D46ADED9C80E806B37E9885A8DDAE30941E086ADAA1D + BF7600AC7A01E0C7DF6D8947DEB2E0AF22F53DA2F7763520B07100DBE1AF5DAF + 36C4FD9481C05C2620FCA0C0B02D0770751EE01ABAF7F9B770DFF34A3BF736EE + A5EE7BEE2DDCABB0CE4FBF6EE408807F4769DA0E7E1D5DBF82C28EF52607F013 + 37C33D57835FBB0358E097F57607B01DFEFE3D002CD901B0E0AF5D7201DB01B0 + DE11DF027FED758E10B83A04B45403FC52BFB9DFF68EDBFE1EFC36EFC06BBE0F + AFD9C671DE4810D9F61EBE1A00EBDDBCED9DBD95FBD70E816D6EDDDBBDFF971C + 19815D3597D598AFC91BA4DB718BEA7DDFA6BDA36EB7EE7F5BB703BCFF48BFE4 + 5C16DCBCEDB29A3210F4D6BA5F5DCF0E4DFF1DFD27B6F13FD1AFEDDFC64C7FA1 + 6AA62A866A07BFBFD4FF1539807FB19EF4FFA700D872F6F21FB7DD05FCEFE87F + 0170B56EB79F5FD3AFDEC06F272EB76ED47CA0D81E1CB70060C9FE7051A976BF + 0A80B95C6122EEBF29334F1900EC202B57702D233BF0B503E0DF685D5B9DF560 + B5EBA653F876B23F74EFA0AC87731DDCF5D093B8EBC127AB01F0EF1E66FD43D2 + CD6DFF4625F72508FCDB47B82D13229AEB3C664160BB7EF7D893EC3850ACFF9D + 4D773EFE14EE7AE269DC2DF0FB84A57B6A3D877BE5E8358EDF676C20D90E939F + B5C46506FC52F7B07363C29638867FAEFD52B5EEACF3323B42F5280BEADA41EE + FDCFBD8907D8E95279DFB36FE05EDB977882C076E07B2FDB0B1C1B78FCAC6D3D + 4A0059A057E5C32F5940D9EE267E94F502BFAA37A0F8D506C601FCE8DBEA3CBE + 8FFBEA37341D48D3917CA3111E79A799057D1B591DD2C7DE6D81279BB4C75336 + F0FB2455BB91C0701B3CD5BC239E6BDD152FB6EB8197D4216ED5959DD9B6ECB4 + B666BB4E78BA4557038DEB36ED82BA8D3BB3AE1B9E69C1CE77ABEE78929DE1A7 + D9617EAE431F3CCF0EF50B9D2DE7B020707D76E85F1B38DC74EE5F6227FF050E + 025EE83310CF29C7700FB6E180A07E5F0E507A0CC04BEC90BFA850D3ECB8BFD8 + BD1F070A967BF8ED11A3D160B41BDE75F13410D802C11E68E0EA4971DE558EE0 + B1460DDC3CD0D0C30BEFBA7BA289AFBEAE9D8066010168EAEF873621411C6429 + 9FB00F076B81681516862601811C4479E3EDB1E3D0806AECEE85A66ED4987168 + E6E689A6DCCE7B6EEE68E4C1ED79B8E36D97D168EE3B1E6DB8DD461C34E9EBD9 + 96DAF68420B41CEF8FC63E0A55CDC1D98440B40E0B45A3C0F178C7771C1A0578 + A27D38079531533022791606C54C430FE5F60DF4452BEF7168CE415B2B1F7774 + E1806F30DB3827C760440207A89113D19383C9811C380E4A988A6ED3B80E079C + DDA2F4E5F634A3DE0A6D3523D4C0E57661E3D179722007DAC166BD015CD63F69 + 2A7A2647605066145C4A5230A63CCD48A077647E3C86E5CCC2A822E5B1CA84E7 + 9C6CF82E2C40E08A32F82D2B85D7C27C8C9B9B0D97AA54385724C3C540DC140C + 299263360EC34A13E15C990627B9740BE331B4200E4E6529185D918691A54918 + C2F9C105B330440093EDD56670412C06E7C760985E4254A41A37F0C8B99936E7 + 6F36E76F42E061950293A9185E9184C1C57118C8750C44CD8B31A1958772DBC3 + B9ED619472EDF6CD89B2941B6DA0AA80ACA0AE80EC201EF31085192B4BE2710B + 42271A303C30DF02B4FD7262ACDC4FAC97047425CBADCBDFCB7334B8F8A60649 + 450918C0DFA4E31AC47D0DD2364BE556A6781E069624A12FB7D12FDF72100FE6 + B4710DF337D8434BEB78E5201EC2DF27B86B87BE8EA5057F1330A25CC72EF86C + 8579B6BB8025FBBC63BDC0B000AF3DC4B3DD056C07C3AAB724C02B289C6CA0EF + 9885E906F0DA41AF1CBF7201DBA1B01D0C6B99E5FEBD0981AD175996FBD77379 + 36BC57E51B086C6483BFDE941FE74D78C2D58506F44A01AB2C102CF9AD64DB65 + 72FEE69A693FAEE3BBDC720207ACCA330EE0E0758526ECB3A627AE2944E8BA62 + F36234482F476DA0D7847AE67E27719920AF4227866F2C45E4B6D998BEDD02C0 + 76D06B77036BDE0A016D3981676CAF42B4802F15C57522B93C82DB98B2A108D3 + 3615226A5B11A2B715237A6B09666D2B31D077E69662C46C2EC2ACCDC588E37C + E2F60A24EFAA32CEE059DBB8FF757946093B2B91756821D2F72F40A2425EEE5E + 88593BE751F311BF47B07719D28FAC44C6D195663AE5809CB74B917B6C057295 + C3574EDE93AB8C632DEBD062A41F58689CB89972E71E5D8A4CD5299428EB738E + 2C3212B44DDC518EF84D4548DD5E86DC03F391436553794794E77729B2B90DE5 + FE95C357C057D058E19E0BB81F956A23002C09D0CA9D3BEFEA0E545E5218E7CD + 06FE0AEC565EDE69C0EF6CB933AFEEB172005FD88ECA2BBBB86C17CA2F6C4599 + DCBC67D6A3F8D41A949C5E8BF2F372FEAE35FB4DDD33C71C87F6597A8ECB942F + F3E246AEBF15B3AF6D43F9E52D28A14AAFCA95BB139537F6608EE0D727078DE6 + 7EB417733F94F619476F95C02D8FC7B824AFED42D547FB30FBE37D28FB600F4A + A972B6ADFC60BF01C052E58D7DC659AA523058F940E5A62CBF61B5337557779B + D0C382BF068CC84D777E2B8A385FA2D0B15777192987A9095D2B27A660F1E5DD + 9CDE85828B3B917F7117722FEC44CEF91D962EC8A1B9DB2887CB0580057F4D48 + 649B43364D2E6087692955605830ECB4559F726A13B5114927D623F1B85CBFEB + 9124806603C5C936C896CC6572011BE8765CE19F57516B6C0098EB9F929B7793 + 09F72CC51E9713782B92CE6C67B90DB1C73661E6918DD5D27CFCC9AD48E032B5 + 31ED94FF97BF2DF91CE7ED00F8CC3693D33783BF2F87BF3FEFC22E9E0BFEEE0B + 7B90777E0F0A2FEC43C9D5833CDF475071E330FF0E87319BD3733F3C8A791F1E + C1FC8F8E62D12747B1F4B36358F1F909936B7BF517A7B0F6CBD358FFD5696CFA + FD19A32DDF9EC6766A27B5EB0F67B0E7FB73D8F7C7F3D8FFE78B38F4E7CB38F2 + 97AB38F6C3759CFCE1439CFEE1239CFECB8738C3F2FC5F3FC5C51FBFC0E59FBE + C2951F057DBFC28D9FBEC6873FFD1E9FFCF82DBEF8F13B7CF913C5F2EB9FBFC7 + 37D4D7F8A3A59FFF88AFF03D3EFFF90FF894CB3FFAE95B7CC0F56EFCF40DAE53 + 577FFC1A97B8CDF33F7E8933DCC7999FBEC0A9BF7D86E372072BAFF09FAFE1D0 + 5F6EE000B5F74FD779DC97B0E3DB0BD8F11DF5C7CBD8FCFD25ACE76F5BF7F529 + ACF9EAA471074BCB3EE3393161AE0F60E1A78730EF53FE3BF8F820667F741055 + 1FEA5CCA49BD0F45D7F69AB2E2035BDD555E87BC6EE5F615F415D49DA5DCE4FB + 162385F7A0B463AB0D4055E8DFF423ABCDC72A09FB97980F56E2F72E315058AE + 614D476EAEE23DB2DC80640365791D49695C3FF1D45A249DE4FA2794675A2078 + A55102A755A76BD0BA1ED98ED760C211B61304667D2AAF5139DFD3745D2B8FB0 + A0AFF261F35AB75CF11BADFCD7A795777823B26C12E03590979273D5C06CB956 + 4F2A67B0F2140B5E2FE73D53F7B40AA4EC12082CA18A90BCBB92F75C85C8E7FD + 8FF75C738F122C3DA7DCE40A45BF09CA47AE0F58AC8F58ACE9FC13BA372F47C1 + 296D7721D75D82B2932B90B5672E66ACC9413C9F0B73AF6CC1B24F782F915398 + F73D85A62FD4473DBA07F31E5EC07B60D989D5283DB59ADB5B813C1EAB3E9651 + 1E723977E54816F0350098F7D182739B50C47B6FC965856CB6E0AF5D02BFCAA5 + 5C78699BB957959EE3FA5419FFED15F237E4F3BCC911AC5CC082C0BA670B0ADB + 6507BE8E12F835009D7F57736ECF080073D9D92D663A97DBCDE1DF44CA939B98 + 7539FCDBA4CBF9AD67DA31A52D5865AE2DB9CE15F5226EDF02C4ECA840FCAE4A + 24EC9D83B85D56748D443D0B784ED2F94C4A501DDB284FB2CA59DBCAF96C9D8D + 543EDB52F7CD37CF417D9894B083CFEE8D05885A9781B84D3948E1733A657B29 + 6236E62262653A9585A92BA46C331DB92617317C2E0AFAC66F2F67596A3EE68A + E1F37CF2DA0C4C56BEDE95CAD79B8C88D56998CAF988D599366561CACA4C04CD + 8F334E5F01DEE045F1C6F91BBE3C05D3D76563E6E602A3A8F539885891865085 + 835E9468DCC29396A7226449B205A65764725EA1A33311A2FCBFEC57052DC9C2 + 84C59908A0CC076DEB8A10B6A118612AD7172344D05761A0592A4A8A4995B19E + 7D232D633B53F2DFA414B8BE14814AB5B1B112133654995CC00A0BADF0D01358 + E7BFBE02FE1B2A11BA632126ED5C82895B179850D182B6CAF7ABBCBD0A413C61 + 839503586E543BF8AD06C0AB048567B3DF574555B0DFA736E5F05E5D4955C07D + 7909C62E2982DBB2624E0BFC0A725AD0D8DB566ADE7385C2440B8696C24D6964 + 16E4C195E305B7455918BB90FDD0F9E9705BC8BEE7A25C8C5B980D57A52FA94A + 8113C70F4E951C8F702CE1323BCD68CCDC0CB829B4B281A31C7F705C63C43EF3 + F09C69189C3A1143D38231222B1C2332C2303C6D229C3243E04A8D4A0DC4E038 + 1F0C9CE5850171DEE817CB32613C0624FAA16F829F71CEBA14469AD0D29EB393 + E1CEFEFA9892788C641F7E24C727A34A66710C950477F59B17AB8FAC70D659EC + 476761DCD25C037015E67A0C7F9FE7329E83E53C478B4BE0BE84BF7F59393C96 + 94F1F75BF36E8B8B316671213C04D7796EC7F21CBA2ECEE7F9CC87EBA21C4A39 + 831579C9EABBBB709FA359BAB1548868F72AF6EB4BD9972FE258A07016C781D3 + 31287D32158E21995330346B3286E7712C5A340D6EA551702B89866BFE54B8E6 + 0A084760446A28FA45FB509E189AE48341499EE837CB1D03E3BD313C3500C352 + FCD19FE747F9841542BA779427BA468CE5D8741CFACD1C8FC1890A3B1D86A1C9 + 1CCBC607A1EFCC00F498E1CD31AE27BA4478A173B83E7EF64197C9D494F1E81E + 11881E9141E826101C398163E2C926CA55C7297E683EC10DAD823DD12942CEE1 + 09E81AE96FD42D6A027ACC0A43F758E50D8E44EF84199CE678388EE3E6C419E8 + C53176B798C9E8343D149D3986EECEE9CE1C372B4CB31CC79D23020C78963A86 + 8FA77CB96C3CDA4E1E8F0EDC5767D6CBDDDB32D803AD427D792C6CCF317AF7E9 + C11C8F07A2D9047723A583EA1A1D86DE3327A337F7DF95E3F70E3600DC2658F0 + D7CF487982DB4F9988AE3326B3CD147430B980838C03D8AE566113D0367402DB + FBA3F1780F3494DB778C0BDE7171C6DBAE4E06FCBE3B76B451034160857C564E + 60B73168E2E981A65E9E68423518EB8AD7468DC01BA34799D0D00DDDC6711BAE + 784380D7C909AF393BE3B5D1A3F12AD77F65D448AB8ECB5E1D3EC2848116087E + C7451FE6BB55436013C6997A93F3EF388F311FEFCB212C07F0CB72FBF61E84FA + 7D07E3CD018AFA36C27CF42F57F0F30AE12C47709F4146720B1BE76FF7BE78AA + 4B2F3CD9B13B9EECD0CD40DF7A3DFAE3955E03F15C67D6B7EF8ABAEDBAE2A9F6 + DD58763111EC9E68D9017594C6AC6D373CD58EEBB134E0D70682E512160C7EAE + 536F9682BADD0DD8AD09786BB7EA6A40B04ACDAB5EF3921D002B07B071FF2AFF + 6F639BDE6F6B5CBEF670CF26E4F3BB2D0DF8D53BB847DF6A8E075FB78C1A7600 + FCD0EB8DF090DEDDD57FCFE4FC95BBF73ECA4CDB80AF81C07208ABA41E78A521 + 1E7A95EDE50CFE15087CCF8B16F8BDFF857770FF8BEFE03EEA5E39819FB2E0AF + 1D00DB4B41604D3B825E49D37600AC52F382C2BFAB530FBFABF57235043620D8 + 010ADB01B0DD016C07C08E12F4AD0E016D0B032DE8FB9B079F34B203603BE8D5 + BBE47F06004BF6F7D38E12FC7504C0B785B90EFA67DA487AFF7E3B55BF9FBFCB + 7A8F6FE9510BFEFE1A07B03302BB6EB7CCB18ED236FF59B8FBCFA826EBF83F21 + 47B0FBCFEA979CCB829BBFBADC51D520F466DDAFAE636FFBCFEA76EB3AD639D6 + FFB775BBFDDD2EFFAF5DD54CD50E80EFAEA1FF8B00F02F245BF3BF0480FF3F00 + 7FA9DFF2F7D8433B9BF0CE35C1EEBFA2FF2F00E0FFC06FB8DD7E7E4DB7BB613B + D6D79CBF7933BFA95F406047E941739B07D36FEEE3B21AA0D7E8FE5BE7AB01F1 + 0356A92FA8CC9754B6876EF583D361DA3E6F977910DFE6E1AC7CC28E0F51937F + A1FA61AD756ECA82CEB56CFB67DDC3AC7B54EEDF5B5DC006FE3EF29401C3BF7D + F456A9ED5D8F3D6500AF057F9F35F0F7BE3A2F18107C8B8398DBB95BE0D72E1B + FC5529007C572D76626ABD689315F244BAABF6CBB89B1D20B980E5ECB5C23A0B + F20AF0B293F5F41BB8DBD6D132A1589E7FC3B8832541DF7B9FAE6F6407C07200 + DBF5103B7482BD46ECE0A97C8C9D3FB97F4D586981E1571BE09137DEC7436FB2 + 53585FA1611A1AF8FB303B950FAA73F976533CC14EE8130DD9117DA739CBD678 + B9436FBCC84EAEC0AF2467B020EF534DDBE3F9565DF0429B6E78BE7557038AAD + 10354D518BEBD561A7B6F67B6CFF5E7BAA03EA36EA88E79AB3634DD569D21175 + 9B77C433EC203FAF2F2DDB77C7D3EC542B24CF8BECC4D76387BE7EFFA1A8C78E + FF8B1C04E8AB4EC1DFE7D9B97F5EE17B94CB45617BD8517FBA5567D469C1EDB1 + 03FF5CE79E7885EDDF1EEA8486CE6E1C508CC3DB4E37659CC04E633860D1A0C5 + 15AF73D0F186B3C21B29CF8D2B0746EE68ECE38D4654B3003FB40EE680CCDF17 + EFBABBE16D7777BCE3ED8B773C7DD1D8CB0F8DDDC7A3A18B07DE1BED8E46CE1C + B48DE620C9C5138D5D3DD0C2CB1F6D0382D1CA2F00CDBC7DD07162183A854E42 + 0B5F3FB4F0F1471B2E6B131082768193D0D27F229AAB6D70285A4F0C45E38000 + 341CEF8366811C504E0EE120782A86C6CF42DFC808749DC8C1ECC46074E0B1B5 + F4F2405B1F2FCE4FC0E0191118933213EE99B11895381DFD668461E0CC49189A + 1081FE33C3D0D37CF9CCBAC4A9189C32CD940339B8ED1F37D580E0AE111C8046 + 04A3CFF43038A5CDE2E03D1DA34A5330A22801AE15E9709B9D654A97B2540C2F + 88C3909C9970294F85DFD26204AC28C578961E0AD75C9966346E7EB6D1983999 + 70954BB72A0D23E5E8956B9772AE48C568B6934B785469B201BF02C206000BAE + 66476150EE0C0C2D9889E14571185E1C8711945369827921A370CFCE94EBA25C + B82ECC85D3BC4C8CE03E8696256190A068991CBC02A633D0379BE7232B92DB9C + 8EC19C375FB417CE8493B69D178DDE595C2E386C5CBA960CF0958358EE5E1E93 + 4255AB141036AE5D9E17E3D6B539750581E50A1E6C03BE030BE5F24DE0B1681B + 7223A76158691AD74FE1321D63328F35D502BEFCFD96ACE90102C0DC7EDFA258 + 0C2C1144B601609BFBD70A1F6D3983870822F3B7F4CF53DE61859F9E6100B01C + C102BE92DD1D3CA42496C7C663A51C81EF109E534DDB5DC1F610D0C6ED4BD95D + C0060C5729C4B3C23B6718083CB24AB0388975E9A64E2F8D24C15F397DADB0CF + 960480AD10D182C5160436E057F97F5758F97FE502364EE0E53906F87AAFC883 + 17A735AF723CE70DD8E53A82BC9AAFAEA354E7BD34DB4060FB32FF95055C660B + 032D086C0B051DB4A61013D71519001CBEBED4CA91B7B61821AB0B11B6BA0811 + EBCB30756339A6507A512AB7CCB4EDB331756BA581C282BE76D86B0FFF3C63FB + 1CDBBC72025760FA0ED66FAFC054AE1BAE17AD2B7310B43C1DA1ABD3B9FD2C4C + DF9C6F4070ECF652F3D258EEDFA88DACDB9087984D8526D4A7C0B0607034E7A7 + ADCDC68C0DF948DA5D699CB8029DF13BE72076C73CC4EF5E88E4FD4B91767005 + 328FAC46D6D1D5C83EB68A5A8D9C632B91737839B2E48612E43DB2C484099523 + 37EBC022A4293F22B795796021F21506FAE852A4EC9D6BDCC7965B772E927696 + 237E4B3112794C69DBCA91B3770E72F6CF45CEC1F9283ABE0CA5A757717BDAD6 + 5C0380EDF0B7883220F6F46A942ADFEF899590B358A14D155E79CE6585695638 + E66D28BB60E5F32DBB2857EE2E545E5798DA3D28B9A45CA402B73BB9DE0E949F + DB8432B985951BF3CC5ACEAF47E5F9F506001BE06CDB77C999D5987D6533E65D + DF668577FE6007B50B55DC6FC9952D28A64A14BEF9C65EE3DC9DF7C961A32A81 + DF8F0F508750F9C13EB6DD65806EC58DFD2813BCFD602FAAB8ACECC38328625D + 2155726DAF155698CBD556B0B742B0F7C6BE9BA186D95E75C65129F82BA86BDC + BF0A91BAD9942557B52FB5DF63735ECA6DB913053C27CAC56A7471278A2EEF41 + E1E5BDC815EC35E1797753BBAA01B0A633CF5900D884453EBFBDDA116C977106 + DB64C2281B18BCD9068095FB57107883291315D6F9E85AC41F5E8DB8432B8DE2 + 0F5BAEDFC4636BD84EE179D7228152FE5F857F4E3CB9D90060857C16008E3BB6 + D9387C057F057A638F6FC6ACA316F895E24E70F9A9AD883F4DB18C3B2937B015 + FED988C79F42291770FAF99DC8E2EF567E5F9D8B92ABFC3B5D3E80A20BFB507C + 510EEC43A8FAE028AFA1C3D4210380177E741C4B3E3D81A59F9EC4CACF4E62F5 + 97A7B0D6E690DDF8D5596CF9E61CB67E770E3BFE701E3BBE3F875D7F3A8FFD7F + 12ECBD88233F5CC2B1BF5EC1F1BF5DC389BF5DC7C9BF7D80D33F7D84B33F7F82 + 0B3F7F8E4B3F7D8ECB3FB2FCF1335CF9F14BE3F4BD86DFE306F5E1CFBF37CEDE + CF7EB6A0EFB73F7E8FEFA49FBEC71FF067FCE1E73FE13B0956F92DCB6F04827F + FE1E5F0804733DE9939FBFC547DCD68D9F7E8FAB3F7DC37D7E8D8B3F7FC5FD7F + 6572099FFEDBA738F5B78F71EAC74F70F2A74F70ECC78F71F8871BD8FFC72BFC + 1DD45FAE61CF0FD7B1F34F97B1ED8F17B1E5FB0BD8F4DD796CF8F62CD67CCD73 + F2C551ACF8EA28567E7D0C4BBF3A86859F1FC1FC4F0F63EEC78731FBA3C306FA + 567CC87F139CAEA2CAAEEFE7B957586D2B24B9C294E79CDC80545E1372FCA61D + 6749659E926376A371CE2AFF6BEAE19548671B132A9A52186881E0F0B5250858 + 9285C82D7390C1BACC130AD12CA7F93AC4F35E96746A8D81B4A9DC5ED209815F + 6E9FD3A9A7396F9CEA0A0BCDF62C2D37B080B0C0EE66F36F21F32CAF9BD35B8C + 8B3D85D765FA19850BE7BF89B3AC535E6C857F3EBB0939BCBF649F6169648160 + 414B9502D91600965359BF6309EF990B517C741E32F79422797B0132F69421E7 + C05CE41E5A80C263CBACD0F9C7784FE2B92839B59EF7C2B52816FC3DAEFBA142 + EBAF4131EB0B4FAE46DEB1A5D4226A09B20F2F44F6C179C8DE3B1BA982957C0E + E8239BAA8B1BB180F733DDDBAA2E6F32AA38BF81DB586D42F0EB439B621E9BEE + B5CA6F9EC763351FD6E8C312DE6314FAB940B97ECF6F32C0B7E022EF3D940070 + F1255B98E6F35B8CB35B1FA408FE162984B5EE4D722FF37C09FE1A00CC735624 + E7B22214F01AD0BCC2429770BA90F5C6412C90ACED71DF0A75AF0F5DAC30D037 + 41AFA0AF40AF2304D6724D179C53B87BE51656D86E39B36DE1B8F93737F9A58F + AE42E2017D50C0E7D881F948D77388E72989CF2801E0B4030B90C2E798C232C7 + 6C2EC6CC2D25E6632B41DA543EDB3294C280CFD334AEAB326977151277942173 + 5F058A8ECC43F191F9C8DD5F89A4ED8588DD52683ECA4ADB3D9BCFC44ACE9720 + 9ACFE4E84DF9E6F91DB7431F7415723F7C5EAFCB42E8F2444C5E9D86F055CADF + 1B8FD065F198BC2A1953D70A02A71B876FE8922404CC8D81FFEC2844AC4C43F4 + C63C037C67715FDADF2CF3515811A2D6E52272553AA62C4F31EB4D5D2DE730D7 + 5F9A6220F2D4D5D9AC573E606A553E26B3EF13C63ED02496A1AB0A11BAA6C87C + D4A6A82506FEEA8338B6D30771416BF5115D3EFB49B998B0D6FA582E700DFB55 + EC338D675F4C1FDBF9717D3FF69B02D6F1DFA99CBF94DF9A1203808337CFC104 + 397ED75760C2A63908DA3C17811B15265AF981CB8DDBD76F5DA551C0FAD908D8 + 30DBD4C9FDABD272F852EBB8CE9A3918BFDA82C0DEAB2AE1B5BA0A5E6BAAE0B9 + AA02E39697620CC71B6E4B9503B8CC404F0F395D0DFC2C673FB1DC029F6C679C + C46CE3C1B66317739D4505EC970A7666C36501C7289CF6585A64A43CC14A3923 + 48EC3A2F9BFD544BCE4A3FA32846B6943543CB6D2966588E2C4980730EC72989 + 13D12F663CFA44F9A2CF346F0C88F2C1C88420B8A64EC4F0383FF48F74C78019 + 1E1812EB8B2171E38D13765872208670F988EC708C2D8B3661A63DD8BFF6E2D8 + C893DB77AFE4F41CB98ED99F5DC43EF062F6974DB41C8D79B24CFFDB75816076 + 1EFBD37906728F5D54C2E32FE4F11761DC22C1DF72FE66FEF685C55CAF94ED8B + 30722EC76982C02B2B306659319C387E1AB3AC10EE2B8BE1B28C6329F6D59D17 + 65B19E7DF6C5197063DFDC9DFD6DAF25BC161615C17701D79D938B71959918C3 + 733096E302B7AA248CA988C7C84285B10EC7E0AC49189E3B19C3590E4909C6D0 + 6481DB100C889F80FEB1FAED13E0941544F11C24FAA0EF4C01DF71E81535169D + A738A1C3A4119C1E87413C4F7DA27DD06BBA40B12F06270561786A18CF1FC7AA + D1E33936F6C580D8000C8E0FC6809820749DCCB176A807BA4DF145AFC840F499 + 31113D2383D0298CF561DEE8392D10DD2327A03DA75B06BAA1E50437B40E7147 + FB702F748AF0E5583800BD664E44EFF829E81117812E3323D0332E12BD3856EE + 21081B1361F2F5769A3E09DD62381F3D053D8DC28D8BB783806E088F777220FA + 72BED7243F740DF743E7C954843FBA080C4704A07D28F73F611CDA8478A143F8 + 78E34EEE46B50BF34553FF716811EC69DCBE9DA60519577087A9DCF6544E4F0D + 409769216C3B091D274F34A1A3E5FE6D15168036E113D0764A30DA184DB4DCC0 + A10168AE90D1A1816C1BC4ED4FE07E7DD1C44B1FAE8F45A37163D0D04D8E5E17 + 0BFEB2D4B2663E9E68E1E38D3601FE684D35D33B110F372B44F41817CB15EC36 + 0E6F8D76B5423C8F1C65415EAABEB333DE901378F468E30656BDF2FFD6D732B9 + 804739E34D2757BC6593527609FA0A00BF6183BF26976F9F01E65D90C0AD81C0 + 54BD9E03F012EB5E5658681B0416F035A19E290160AD23776FDDF65D51A75D17 + E3F6ADDF7728DEE83F0C2F76EB6B81DFB65DAA55BB75273CD1AAA3296BB7EA6C + 54B74DD76A106CC1DEEE06043FDB51397EE5EEED6AE0AE1D022BDFAFA06FEDD6 + DCB66030A79F68D1D980DFBAADB99D36B610D02DD9462EE0261DF0F8FBEDF0D8 + 7BCAFFDB0E4FBC67335C3468894794E7F7ED66C6F16BB97E9BE2A137ACBCBFD5 + 92EBF7D58678A0FE7B566903BA0FBCF42E1E7EF53D33AF90CF76086C4A4AF50F + D77FDF40603B00BE05FE72FE3E3B007ED102C006023FC77682B90AF1ACFCBE9A + 7EF64D332D006C87BD76E06B97EAEDB243E17B9F7E0D77D77DC5BC0BAD0E054D + D943420BFCDA650F015D53D500F8E1A76E09FD6CE02F75C74372FEDE74FFEA3D + B3FDDDF22DAEE05F05C0B79A96646432662647D5344439BE3FA71CDF5D1BA8EB + B0ACBADEE6FAB5435FFBB47D5EEFE8CD3BFC3B1FB5F8C1DDD46D40AE911DF2D6 + 80BD2677B04D56BDB66971033B2FA829FB7247FD330CC3B1CDFF29D9A1EDBFA2 + 5FB02E1BF0FCD5E5524D28EAB0CCBECE2FD6AD06A70ED335E71DDB3BEAD796DB + EBFF9BFAB57D993A31D3DBB0D36AA66A01E0DFDC06009B7A4728FBDFD42F01B0 + 0ED8F1C7D8653F7047DD6E99EDA4FC43FD5F027F1DFFA8BF22F30FC201000B08 + FF02ECFE0B3210D9EE06FE55FDF21FF9BFA3DBDD941C6F56359739EA57DBE8F8 + 6A2CFF35DDB29E836ED7D6AE5B6EE83554B39DE3FCDFD3ADDBB13D004CE809AB + 34F90004736D0F348157130A430FBD7BD9DE0E7AF5B0B33FBC1CE0AFFD01689F + 3710960F5401DBEA2FA11CE0AF9CC48E10F85701B096731BE6E1CB7DDEA1E4FB + 0A312D393CD06FAA8E7106DFC507FC9D36E8EB283B00BEE3517D35C68EC363B7 + CAEE02364EE0C79FBE15003F21B7AF0D003FF60CEE6407C49A97035870F86618 + 6875500C04366E60AAD68B96CC34555B79815F6627AA9ED13D0AF1FCF42BA663 + 24DD23C8FB8C5CBF6FE04176C81E7AF14D3CF8FC1BB8EF9957B95CEDEA73FA75 + 3CF01CF5FCEB6CC775B8ECC117DFC023F5DFC563AF35C4A3F51BE089D7DFC3E3 + 6F36C2636FBC87475E53C7AF0197BF878759FF50FD86B85F1DC2571A9A5032F6 + 90320A015D8B1DD15A0A43C38E68DD466DF14A87DE78595F39366E67F45493F6 + A8D3A80D9E6CDA0ECFB4E884675B76367AAA69073CC175D581D5578B8F376885 + 27A83A8DB41ED7A1EA369593B823EAB0032C00FC24D77BDA964FB8B6206EDBAE + 2604CF0B5DFB988E7FBD9EFA6A535F5DB2F36DC2F3F4C233EC803F2DF0CB4EB8 + D6556EE25A4DDAA156B3F678AA75173CC78E7ABDEE03F05ABF617875E070BC36 + 6414DE18CA41C930672B44F49011786DB8061C2EA6AC3F5CF969588E18C5018D + 0642630DF06DE4EB85F739106AE0390EAFBB3A532E78435FBC7A78A355A0E0EE + 040E9EDC8D9A7AF8E07DE3065668E740748B988CDE332239E0E400726A04FA72 + BA6B78189AFB78A185B72FDAF807A05D1007A693A6509339200C336A13128C96 + 41CA05C4415D50005A4FF043FB891C344E0A418720D68DF7425B3F1FB4F5E7E0 + CCCF0B9D2706A05B2807BE1CD8F58A08C190D8486A3A07CC61E8393D9483E449 + E84DF599198EFEF1111894CCE56933302C2306C3D26762704A347AC7461808DC + 3B8603F7F8E9189B970C8FD9B9185D958551656970ADCCC29839D9183337076E + 7A41322713CE5569F0589C0FFFD51518BFB20CEE9C963357B96F0717C471DD4C + 8C5B908F71F30B3066BEF26F65C355C07681F25465C37976069C04804D3EE034 + 0C9783B628C1B881058087E4CDC4306E6B78513C4614C76314978F2A4BC2A8CA + 64AE9B8AD1F3D38D5CE6733B73D33082F5C6B52B585A3013430A6719C76FFFEC + 280CC89A8E813933302837CA0060397F4716C762A4096F360B7DB96C80DCC0A5 + F1185C22C7B00069BCCDF19B806115C9185E9E6CA62D406C85A21E625CC256F8 + 66BBBBD984B4B6BB7D0589590E2BB3425F0F2F4FE336922DF85B9ACA79E5454E + E63ED9B6240983392D203CA434C5D40D10F4E53EFB0BF82AFC33F73590C7ACE3 + 1004B660B040B05CBF31D5EE60D529E4B45CBF72080FE2B9508EE121FC6D834A + 785E8AF9772F8BE5FE58C7DF39A49CE7BE22012314F6CDE6F8B543E0919C9786 + CB015CA5D0CF693CDF19705DA81768599C16D4E5DF626E96F9DB8E5998697433 + D7AF64E5F995AB40F981ED00D872FD0AFCE6C25DF97F970B0067C2734926BC58 + FA70DE77450EBC592A34B4E7920C787199F7D22C537A2ECEA0D28D34AF90CF72 + FBDADBF8AECC35F25F53003F857D9638EFB78A5A9D6FE6E576095E5B84F04D65 + 98BCB91C611B4A11BAB61893379619A83B757325C259A7794D4B53D4967502C0 + 02BE72FD5ACB04842B11B945C0B7D4E4228CDC528AC8CD6CBBB10853D6E5236C + 753626ADCEC48C4DB988DB5E88995BF311BDB9C000E0F81DCAFD5B8218CEEBE5 + F1CCADC598A95096DBCA112BB7D2F6724EB3CDCE4A24ED9D8BE47DF390B26F3E + 920F2C46F2C1A526E473C6B155C83AB10659C75723FDF032A41D94F3690952F7 + 2F44FCF64ACC124CDE5E8658E3382E46E2CE72A4EF9F8BAC430B90B15F00781E + 720ECB0D3CDF0A3BBDB3CA005D01E0D4BD9CDE53854CEE376BFF7C641D90D377 + 21728F2D3521488BCFAC43FEC9559C977B6DB515D6F9D41AE41F5F81A253AB50 + 26487B71332ACE6FB472EE5EDD862AE5C515FCBDB80D6597B7A3440E3539D104 + 83AFEC40E9959D28313948A51D565E60B591F397FB2B39BF8EDBDC8839CADF4B + 555CD880F2F3EBCDF4ECCB9B517E61232A2F6F35397A2BAF6EA57660F63581E5 + 9D28BDB6DDA61DC6012C876FE575B9781526788F29E77E7418B305BCAE1F4099 + 091D4CDDB0DA0818975FDD8BE24B962B57B97D2BB5ED6BCAABBA0B55D7779B30 + BA02C1DA9E40B072020B12972AC72ADB0B02979ADFC9DF7E99BF598EE4EB566E + 600160B9844BAEEE31FB28BCB013F9E7E4B6DC89C28BBBB9CFBD28A20A2FED31 + E18EF3780CB917771A17B072B5E65CD88E2C81DEB3825CDB90C5FA2CAE9B796E + 07EB04BD2CA59DD98654396A4F6F35A195934E6D46F2698558A6146A99F34927 + 371947AFC06EDCD1B5883DB21A7147D660D6E1559849C51D933B7303B7B11989 + 2CE5008E33C0573980371B889B70423058A19E15F65961A1A52DD5219F2541E1 + 78B655BDDCC2B3E4083E2E60CC63E331A6E978A9CC0BBB4CA8E79CF3CA4DBB87 + E76E3FCFC77E9EC783A62CBEBC0F65FC9B557E78987FAB2314FF8E1F1FC6C24F + 8F63D917A7B0E2ABD358F5E529ACF9FAB471C36EFCFD596CFDF63C767E7F01BB + FE78DE80DFDD7F3E8FFD3F5CC2D1BF5EC5C9BF5EC34941DF9F6EE0CCCF1FE2FC + CF1FE3C2CF9FE2223EC7657C892BF80AD7A91BF81A1F521FE11B7C826FF131A5 + F253FC015F1887EFF7F8BDA0EF4F7FC2F73F53F813FFFB0BFE4CFD89FFB7EB8F + D4F794E0B060B09CC15F515FB0F633D67EFAF31FB8DDEFB8AFEFF0015B5CC7EF + 71F5E76F70F9A72F0D88BE48094A9FFDE9339C1208FEEB8738F2C3751CFEEB0D + 1C6479E0876BD8FBC355ECFAF365ECF8E3056CF9EE1CCFC329ACFDFA18D67E73 + 1CEBBE3D8935DF9CC08AAF4F98BCAECA1BBCF0D36398C77338E7E3234655FC77 + 51AE10DBFC3750C2EBB94821A14D7E6A7DA4B01DF954CE055E7B673721E3F446 + 4AF0CECAC39B79722372CE28C7EC16D609AC6E4686AEAD7D4B317DC77CC41D58 + 6E815F397A6DCEDE8423AB90785C007803D2B93D93E7F7049709F8721BC6A54E + 251F97435DF30A636E5DC7BAB653CFEC304AE6B594C86B2FF9D436645FDA87A2 + 1B875174FD20722EEE42C6B9ADC838633B5E1EA38E53C72707B3A0A33D14B409 + 5D2C182C37B0F2A59F5881E2934B917390F7CADD65C83E301B854717A1F4C452 + 549D598DAA736B50CCFB64D151CE9F5E8DCAD32B39BD0CC54797A38CF7C69293 + 02C36B507E6E1DA588066C7B7C8951C111DE9BF7942399F7ECE46DA5C8E6BDB7 + 84EB545EDC500D80679BFBDD16DEEF364039DB0BF4D18D8EEB9805800B58A77B + A1F29ECB055C724E797B371AF85BCA7B7011EFBD05BCEF160BF472B91D002B74 + BDC94FCEB28832F3FCBBCA01AC90D582BEBA27CB012CC9F19B2790CDBFB9EEE3 + 4506FCEAC31689F37211F33EA70F064CC87B4A79CF058A0BCE68DD8DC8E3F591 + 2F09FEF25C4B0A3D2D086C07C059BCC7189D50C8E875D45A5E2F6B4CD868E5B6 + 57B8E7CC6396B2542AF5019F831907162179F71C3EFFF81CDC516E3E72CA3CB4 + C800E094BD730C0C362098CFB84C3EDF0A8E2DE1DF6639FF964BF8BCB33E824A + D95569A24C14F2B926D89E7590EBF36F92B2BB0AB3B614237A5301666CC83170 + 366449228216CC44D8B2044C5AAE30CDB1085A148B1085965EAADCBD71C6E9AB + D0CF9357A663EA9A2CF36C4EDE339BCF763EE3F7CE46FCCE0AD30F502E61933F + 787D3EA6519A9ECA72D2CA4C842C4FC7E4D5398858578070F66BC2D7E6234269 + 29D69762CADA12EB63B64D15ECB754206C6DA9896822776F90F9108E7D206975 + 11825615B2CC372EE11045465953087FF6C1BC16B19FB520D5F4AB7CD8171BCF + BA00B6F7A77C96E7C167553E02D79763C206CB11ECC3FAF16B4AE0BB467057F9 + 7F2B8C5B38C02C2FC77893179863071312DA7207FB0A12AFB2A430D03E72FDB2 + F4544868E5AD5D5B052FB6F7647B0F9BCBD7876DBD34FE58568C714B8BD84F2C + 31F2D007A9CB0476592EE5FC9222B82F11042D82DBD2428CA1C62E51E8E47CF6 + 4DF3D99FCDE3B82497FD52E50D665BB559C4BA0539709E97C5BE70BAD58F2FB7 + A5A5A9649FB83C0923CB12E05A3C0B23B32238BE9AC07114C76493DDD065CA38 + 8EBF3CD13F6E3C0627046260AC1FE56BC0E7F0D4208CC80885934225174CC598 + E248B896CCC0989218B894CCA462E1569184711CD3B8B3CFED3137055E0BD5CF + CDE0F88BFDED39FA1093FDF3CA04F6C71339AF7E7B129CE7AB6F9ECE63558A1D + 8DB37231766E2EC6CDCB83E7A242FEDD4A384EE36F12389E97CEF395CD7E7A3A + 7F672A3C96B37FBD42EE5F8EAD16B27FBE34032E8BD8575F900637F6CDC7B18F + EEB93817E379CEFC786E7C78BEBC38261CC7E31B5795C4E91478EB18F2A7A32F + 7F6FF7186FF49AE9831ED15EE83E5D70D70BFDF8FBFBCEF2C5A004E5FA9D0897 + BC491893AF90CF411898E08B01F13E3C5FDEE8193D165DA7B9A0DF2C6F9E278E + 695382D16FA62FC7B03EE84DF58AF246B7A9EEE8143E96E77A2CCFB317FACEF4 + 438F483F7498E46DD42DC20F3D232718759DAC31B427DA06B9A34DD038B40818 + 83968163D126D81DED42591FE64E79A1A35CC3D3B99D9820F48E9B849EB3C239 + 1F068582EEA188591C4777880846F3105FB49EA4D0CF93D1615A283A4E0D314E + 6001DA36A11C9B87F8A0EB9400F49014796B6A00DB04A05364103ACBE52B172F + 8FA9DD147F6E4F70D7DF2C973BB8759842427BA33D977761FB8E5CDE3AD483F5 + 5E683F653CDB8E37A1A07B4547A0CBB4496837896DA6041B10DCC4DF0BEFFB8E + 33B97E5B062BAD941F9A06F898FAC67E9E68EAEF8D96418A70E68FE67E3E06F2 + 36F672C7FB54635F0F34F2B1CAA67E5E6846B59AE0C7733C91C71F8EF69382D1 + CC9FEBB0BE4D7000DA4D9C8016E37DF0BEBBD2698DC3BBAEFAA8DE09F5860EB3 + 9CBE4E72103B9BBCBF02BF6F72BAC118455CB3C241BFE92CF7B11B1AB88E3312 + 0816FC55F8E77A0386E2E50143F0527F2BCCF32BBDA5C178A97B7F3CDFA50F9E + A35EEAD1DF84867E91E5735DFBE0A98E3DF17467E5F6ED83E7BBF635EF8B9E6C + 27D0DBD54CBFDC43EF9206E059B982DB7646DDD61D51A75507D46ED509B55AB6 + 67D9D1E889161DF044F30E5C2EF7AE05819FE276F4BE49A9CDA4A75ACBE5CBE5 + AD55DFD344B17B8A659D965D4D9D80AF40AFDCBE9AAEDB8A923BB87927D469C6 + FD34E17E1BB737D1F2E4FC55FE5F3BFC95F35751F81E7AAB091E7AB3A981BF76 + 00AC487D923DACB3A30479E500566867532ACFAFA47A2E3765BD770DF87DF4F5 + C678E4B54616247E51219E2D17B0A4F9FB94F3D79EFFD726C15EE5F7BDDF560A + F4DAF3FD0AEC2AB7EF9D752CC0ABFA9AB980B55CE19F15F9F0EE275FC53D2CEF + 9511C606818D39A696DCBF02BF72FD5ABA05FC3EF66CB5AA73FE3E24438F4240 + 3F65A0EF1DBF78276C49EF93EDB2CF9B77CF76E99DB259AE79BB91E8E6B479CF + 7D9B77DDB7C8F6FEDC2C3725A5F7EC7AC7EE00766F9163D86747D9EB6C46ADDF + 0A00EB5DFE3D920DE4FE53B2D6BB83DB306057726003FFAAFE11CBA8B9FCFF84 + ECD0F69FD12D7CCB917DD5646135E67F6DDD9ADBBFA5DDAF6DD35EE7B8EC7672 + 6C7BBBFAFFA67E6D5F8EC761E7A306003B3253CD5BA974AD74BA37A78D1CA1EC + 7F533701F0FD46FFF8C7D8E5586F5F76F38FFDCBE5FF17EB17E7E15659BFD916 + 065AA5E68D2B58E7C1A6DB80DE5B72053B4EFF33FA0F00E05FBB2939D6D75CE6 + A8BFD7C671D9DF53CDF5ECBA5D5BE956507BAB6ED7FE1FC9F14B9EEA0782B6C7 + F20E3DACEEE583C926FB43ED770FD6C29D0FD5C15D0FF2C1795F6DB6D572C157 + 3EA8F4C5921E4C7AA8695A105770574058320F403D2CF9D0B4C1DF9BCB582FA0 + 4BDD0283B50D3D808D93F7970F6BFB835A20D892B57D2BD7B0DAF2A16FFBEA4B + 52F27FC1598505F91DA56947FDEE31EA712B8CC89DB6D29A563D3B140A0D4D59 + 30F81998F0CE36F7AFE6B58E7DBDBBCC7A16ECBDA5836287C206023F8F7B6ABF + 68A4DCC0A61DCB7BEABE6483B9363D558F52F90AEE7BFA55DCFF4C7D3CF0DC6B + 78F88537F0D0F3AFB3E3A5D0CFEA40BDC40E94058DAB81F0337212B3EEF9FAEC + F8B1FDABEFE091D71BA0D6DB8D51FBDD26A8D5A0299E78A7311E7BF33D3CF63A + 3B800AFBFCD2DBEC2CBE8D87D9317C881DC4075F6D80875F7B0F4FBCDD14751A + B4E07ACD51BB610B3CD9A80D9E6EDA0E4F376B8F271BB7415DAA8E4244BFDF0A + 7534DFA49D71023FCD8EAD00B0F2070B22CB09AC103675D8C97DB2193BD49420 + EDE35CE789C66D519BEB08FA3ED9926259879DEEDAA6E3DD991D6E76BCD971B7 + 77BA9FB575BC9F69D3CD74BEEBB664C79AEBD45558EA26ECBC376A6772A5C871 + FC243BD7CFB6E882E7D9F17EA1033BE7DD7BE1E5BEFD517FE050BC3698839301 + 8339C0188CD738D0786BD468BC3E7C345E193212AF0E933850711E8D77389869 + E8E186773D3860D117B25E6E468DBD39B0F20F44BBA089E81C1686361C20BDE7 + 3DCE0CAE34B07ADF9783A9F11E1C94059AAF74BBCF0843DF98480C8A8DC2A059 + 33D0393488831D67347471460B1F6B80D53E8C83C729E1E8181E868E9CEE141A + 826E9CEF3A6512B71F88C63C8E263C0EE5166EEEED8EA63C8EA65E63D1D2CF03 + 5D39F81B30730A07B913D18E83BD36411338209E82EED3A6A16BE41474080F45 + 1B0EDC3A4F9F845E71533120250A433263312C279E651CFAA7CC40EFC469E895 + 30153D4CEEA2C918C436CEB989185D9E09A7D939709DAB970745F05C5A62C2AB + E9258DF26BE9C589D78A1213DA4D2F5046CE4EC33041D252E5DD4DC1F0B24C38 + CFCEB37D89CEF68B0B3076518EF9EA5E5FD93BCFCF32D05670756885E5AE1D22 + 009C178BE185092624B434A23809C358AF5261A4F57263D49C348C9E9B0697B9 + A91855A5FCB4CA7D1B8B21050A1B3D0B234BE231AA94EB70BD610571DC5E3C9C + B8FD91C502CCDC3ECB1102CE82B3DCEE1001D11201E05918581483FE05331C5C + B2F11600D63196276104A7955F786899DCBD6C53C2ED97C753096CAB1CC372DF + F21C972572991CC13C366E47CB87711D6D4FA05A2F93AA5DC625AAB7D7E9DCA5 + 609880B06033B73380FBE857340B8314C699C7A0F51562BA7FFECCEAD272055B + D382E0DAA6426D5BB98B05A5E5F48DC740FEC601A5311854AE92EB717E4825B7 + CBF338720ECF39CFABC0AF80AF1DFECA056C492F9832E03C2F13AE0B73307671 + 2EC62CD6DF331B2EF32D695ACBC6289FD822BD60D2B455BAA93D4B5701E22599 + 06FABA2FCBE576048C330CE835397B9764C0675926C6AFB0F2F6FAAD12D4CD80 + FBC2548C5B90028F45A9F05C9CC632CDCC8F9B9FCC6B34CDBC9854A8676DC387 + EB8E5F9387F16BF3E1B7AE00FE94995E9D077F395DD617B32C42A05E726E2C41 + C8E672AAC2286C4B25A66C9B8388ED733179EB6C84B16EEA16CBE52B37B0DCBF + 1194423A47EF986B24E81BC1ED4CDB5C4C59219EA75333383D6313B5B90033B6 + 14206A5301927697A3ECCC32CCBDB80A59FB2A307343AE79711CBFBDCCE6022E + 41EC7685B29C83C4BD0B90B067211294D777FF524B079622E1C062134635E5D0 + 72A42967E2A9554839B60CC9479722F5F872A41E5B82C47D734D9EE1B89DE598 + B9A508D3D7285465066236E5602615B9360D51EB3390BAA71479C71620F7C81C + A4703A617B21E2B715B3540EE22AE3764ADD3B07E9CAF37B7431724FAC40CEA9 + D5C8E63E73CFAC43E1F90DC83FAB30A9EB4D784F810485EDCC3DB5DE28472E37 + 9665E7B718785B26C07B41E14737A1F422EB2E6F372A1688B8A410C872C16E37 + D27401DB0850C87D5676719BC961597096FB3DB79EEB6F321079EEB51DD43603 + 42AA2E6FC6EC6BCA51C9FD9CDF68C29D6A7F021F821572162B0473C507D21E54 + 7DB81B551F08C45AB059B056AAFCE000E67C7804733F3E8E391F1C43D9B50328 + BBB20F95D7F663EE0D2EBBBA07B3D97EDE95DD98CFF60BAEEFC1C21B2CA979D7 + 76611EE7E7CA05CCED1AE71CE7055094DF57A5950398BFE9FA4E1ECF4E1EC30E + 1E0B8FEFBA9C76560868858D16602BBBB217251777A3E8A26030A72FCB95BCCF + 804E4920B8C00060E5FFDD86AC735B9075963AB795B28160397ECFECA0047E77 + 20F3EC4E23CD2B9FAEC2352709022BE4B2C22F735E0EDE449609A70471A9D3AC + A394CB37FEC446C41DDF8058815E96092715DE590098CB8EB1FEE87ACC3AAAE5 + 9BB86C1B924E5A619F057F4DA86781619B1B38D1847BDEC1FD6F67DDD6EA365A + AEBA64AEAF63CFE674D6A9EDC83DBB8BD7DE4E149CDB8DD9D78F60CE8D63FC1B + F39C5CE0B91110E6F912B09763D5FA1B1EC6DC4F8F60C16747B1ECEB5358F9CD + 69ACFCFA24567F73CA80CF4DDF9EC1F6EFCF61CF9F2F62EF9F2E50E7B0F7CF17 + 70F02F9771FCAFD770FAAF1FE0F4DF3EC4E91F3FC0B99F3FC2A59F3FC5357C69 + 41DF9F2D7D487D826FF0397ECFDA6FF1F5CF0AEFFC077C4529CCF3B73FFFC900 + 5D815DEB3F41DFBF98FFFE861FF0B79FA5BF1AFDF5E7BFE0079BD446EB58CEE0 + 3FE3F72CBFA90E19FD3DBEE0563FFB5990F9F7F8F8A7AFF0C18F5FE0DA5F3FC7 + D5BF7D66DCC817A9F33F7EC263FF90BFE5068E2844F49FAFF0B75DC1BE3F5FC2 + EEEF151EFA2C3609FC7E71186BA5AF8E62CD57C7B0E6EB13D429ACFAF2049629 + 6F30CFA3C267CFFDE808667F7088D7AAF5618481C0FA18C20682E550CFD38708 + BC7E33794D2A57B3428B67F29AD47528B7B99CE669BCD68CF39CD79396CBB59E + 7B658FC9659D747C3D620FAD32A59627B39413DDE4B236F31B91C06B2FF1F826 + A4F21AD2F5912AD7B8AEB9A31BCD470329E776F17ADE61FBC8601B5278DD48C9 + A7F8EFE0DC1EE4F3DA29FFEC0CAAA8E2EB8790797EA7E57E976BF8C42664F05A + CEE671C90D9CC17B98F202CBE52C07AB60A47206A753268CF16996C75720F3F0 + 42641E9C87FC230B50727C214A8ECE47D1814A646E2F40D68E02541C9B83D9A7 + 16A0F4C87C941D5F848A5382C0CB517E7A15AA2EACC3EC0BAB51715A219C17A3 + F4D822141F5B881CAE9FB4AD00719BF291C1FB71F99935987D71232A2EAC37E1 + ED2B2F6D321200D6473145BC2F2B2FBAD1E9B5264FB011EF99CA815E76699B75 + 3FB4415D859C2FD2BD95D372FDDA5DBB5AA6FBA54AC15AE3E23DAB7BF7365B94 + 86DD2657BBEEAB52B1D6E17DBD98F75D859C2EB9C069DE878B789FCEE77481A6 + 2F2B9CB4CEE146DE0B37F118B6A294F3857C86E49E5C4BAD433ECFB50075C199 + 8D0628E7F378E406CE34AE5FCB319EAB8F0804E8392D277026DBE5F2F9937D92 + D7CBD115483BB612CA1D9C77663DF2B8CD3C85D9E6332AF7C46A641C5C84AC43 + 8B9073788991225F280A46E6419E6BCEE71E598A7C6E23FFC45AF3FC4A3BB80C + 690796206D9F5221A8CD52136E5BB2521DAC3430387E5B19A2D7E763FADA1C4C + 5D998EB0C58998B8300E939626227C590AA530CD56BEDF898B9310BC5060381D + 333616F1793F1B29071620692F9FF77B16206ECF3CC4EE9E8F59D4CCDDF310B5 + 93FD8C6D552605C50C491F9E6D2AC7E48DA5085B5788F00DEC7B28CD84FA226C + A7B611ECD7846F500A8B524CDE5C85F02DB331717D1942D69520786D3126AC2E + 44089787AE2D41D08A4204AF2C46D8EA5284AFE33A6B4B11B2B200014BB2E133 + 3F0DDEF352E0B3301DDE0BD2307E718689A6E2BF52E38334B82F4AC7F895EC57 + AD29651FAB18E3571571AC2071DA38840581AD3CC1CA076CEAB99FF106FC96C2 + 47A19F399EF05A2E602B905B04AF65C5F0549E5AE5FB5DC5656BCBE1CDF69E6A + 67F203971967B1F203BBB3FDB8A58536E82BD85B482934B2151E594E5FD5B92D + 2E647F331F6397724CB2BC146ECB4A309A6DE48A1D392F1FA3161461F4C222B8 + 2C2A34F5CE1CAB8C9A9BCD3E7186197B8C66FF7734FBC1CEEC173BB1DF3F8AE3 + 05A7A2288C2A988EE1B911189CC9315F7210BACEF241BB6963D07EBA3BFA264D + C0D0F4300CE7B25139E118993D0923732663744124C616CFA022E19237152E05 + D330AA6406FBFE542955340D83F23826CB9FCCB1D1540C2F9DCE65911CB74CC3 + E0C2A918513603A32A676264450CFBF63330A43C0A2366C7524AE3C2B14B09C7 + 421C0F8DE5B8C96B3EFBD3FC3BFAF2EF366E5E3C5C6747C3B9721AB73185DB88 + E06F8AA466F07771FC3737162E0B13E1B62415EE4BB3AC083D9427FBEA3E2B72 + E1BB82750B93E13E3709EE1C37B8703F633886F22C8B837376047A47FBA0C734 + 77F489F242CF19EEE819350E03E2BD0DE41D98E48BA1698118961E8421694118 + 9A1182619954462846654F31B990876785A27F923F06240660B0E06F823F7AC4 + 705BD19EE8C1ED758F1C47B9A35B0435651CBA4E7643A74963D1216C1CC7D35E + E812E1CF3130C7E0E1E3D179922F3A85FBA2A3A06A08C7EF13385EF71D8DA67E + AE683D51CE5F1F749CA230CCE3D181EA38D5175DA68F47D719011C434F4097A9 + 41E81219CCF13CC7D15383B9FD00B40CF645CB89E3D17E7230DA8407A3A55CB6 + A1FE6811E68F9693380E9F1C80B653FCD136DCCF8477EEA2DCBD549799E1E836 + 33021DA787A153D424749F15811EB3A6723A04ED2303D172B22FDA4DE5EF4E99 + 867E4953D17D6608BA46F118783C9D227CD075BA0F3A4DF362198CEE33A6B02E + CC387A7B4586A3FB945034F7F5C0BB639CD0C47D2C5A787BA0A9C73834F77247 + 534F0F341833060DC68E41232F2F34F6F14513EA7DE5EEF5F4C2BBFAD03DC00B + 8D267899BCC18D02BDF08EF758BCEBE386A69C17E0EE1A359965287F4F08BA45 + 4E42E7C9A168A30FD203FCD0CCD7170DC6B9E32DA7D17875C850BC3A78105E1B + 3A0CF5870CC72B2C0586DFE7724542535AAC77C78C3591D61AB97BE1BD313CE6 + D1563868E5007E6DD828F391FEEB4347A2FEA0E1A8D77F30EAF5198C977A0DC2 + 0B3DFA1BBDD8DDD20BDDFA1918FC8C4C019222C975B472F6DE74EFF6C0B31DB8 + AC7D0F3CD9A60BEAB46C873ACDDBA06EB3D6A8D3B40D9E68D206B59BB545DD16 + 1D58DF1EB59AB63332EFA55A75C673DCCE736D7BE099D65D4D8AB3A75B7631EF + ABEA6A79CBAE78AA4D0F3CD7BE8F01C1755A68FB5D519BA5423CD7A29E6CD58D + 6DBB729BEDCDBBAF5A8D28BDAB6AC47D36E2B10802376C6D1926ECE19EED00F8 + ADA6C6096CA2F3BDDEB83AD7AF01C02CEDEE5F3B003612F415C47DE91DDC57CF + 82BF32793CC065D2C3AF35C2636F36351058F572FBDEAFD473F650CF82BFCFBE + 813B9F7EAD5A7729F2E0B36FE27E07A86B77F4DA6501E0970DDCB51B59EC6616 + A5BCBBA3D68B569867397D397D77ED17716FED97708FA6EDB2BF47A56EBE9775 + 08016D40EF2F75E7C3D4234F563B7B7F550FD8DE25DB64DE2DDB74870C4BF6F7 + CDF73B44B27400C0C60DCCD20E816F59761B99F7DE7A572E807BB705816F81BC + 3564DAD880EF2DB22F33DBE3FCBF20858C766402FF53D56419D23F5AFEDF969D + CBFD33AA665B3559A05D3597DBE71DE4B89D9ADBAF5EF6F7B6E95867D32DEBDA + F52B6D6FD9CE7F43BFB62FC77A07597CF501FC966D24536F7709DB6567AB5A5E + 13D4FE3724F87BAB03F8360058AAF163FE5737E5785157C3DF9A17A911EBAA01 + 2EFF513AC2DC7F473697EDBF22C79BD0BFA37FB49D7F675F8EEB38EA96360E90 + D70E676FABFFC1973BBFD807550D7EEDDBB73B7DF550E383EF4E3D2CF505931E + 3C7C70D913D4DB651E5A76B84BD99DBE8E725C2E59CE5D3D602D905B0D80D9F6 + E6175896ACB01C371FD68E529EDFDF084EDF5787D3755957D72A4DE88FA7D819 + 5087807A949D06E911C15AB9739FB79CBA0AD3AC2FCDD4B9B0B779EC19DCF1E8 + 53F8ED23DCD6C33C463988E518B641E03BA56A006C7552B40D3BDC15E8552745 + A149E430B620F04D007C6F1D76766CEDEEAEF582997FE0E95770BF01BE76002C + B07B6BA965F7DB40EFDD759543981DA8DA2F9852F985EF7EF265DCF38CF20973 + 3975DFF3AFE1817A6FB1A3F72E9E78BB116ABDD318B5DE6D6200F0A36F34C4A3 + AF35C023AFBC6B4246DF4F3D5CEF5D2301E107593EFAFAFB78829DCF5A6F37B5 + 01E0D678B2B1C44E6BE356A8F51E3BAA0D9BA1F6FB2D51979DE7279BB4C553EC + 303FD5849D59766CE51A7E586E624A1D59E5137EB2597BD415E0653B75821FE7 + 364D6798EBD4E63223C1DF161D4D28E75AB6AF306BCB21CCCEF6D3EC7C3FDDBA + 0B9E6AD9D9B4D57A75584AB51AEB2BCAD678E2BD56A8FD5E6BE3507EA67927BC + C08EF70BEDBAE1D9AEDDF062CFDE78B9DF00BCCAC1C42BFD06E2E53E030C087E + 63D848E3027E550078F070F305EA5B4E2E26B451230F0F13EAE85D37170E9AC6 + A1A98FA70987D43D7C0A7A4D9D8A76CA13ECE785A6FE5E681EE86D29C8070D38 + 706AE83386F5EEACF340AB607F740809E4403418ADFDBDD160F4283474764663 + 0ECEDEF71967B6D1364421A942D06E6210DA054F408FC808F4899E8EB69C7EDF + 9303252F37341268B6B56F19E0CD657E1C04876160FC340EB227A1ED447FB499 + 10C0C16E1807C991E83A2D02AD4383D0222400ED3970EB97148DA1D90918999F + 82E17949E89716859E0911E89514817E290A713515DD62C2D13B3E022372E331 + 760E07FD8BCA38D8AF42E0DAF9085C371F016BE7C06F4D053C57149BBC531ECB + 0AE1B6241FA3157EB93215A366A763CC825C8C5B548831F30BE0343B1FCED4D8 + 054518B7B8D0BC84112CD68B19852CD397F40AA336A42C11C38CC337C584991E + 53990937CAB532C3D409D60AFE3A5565C0794E269CE6A46194C2125708AA2A74 + 713C86CBC99A1F83C179D106F4BA28C4B4F2151773BB25C9DC563A46972BC770 + 329CCA53E1C4FD0CE7F2116AA3B0C706D0C61A103CA87826860A201BE76F127F + 9B5CB10A852CE0AC7D0B76CBE56B855916F81DCAE318581883FEF95118C45240 + 7A98C22E17AA8DB5DCCABBCBE3E5EF95A358C05721E5E42AD0B49CBD82BBCA33 + AC70D28338AF9CC6C3791C83CBB88CA560B4A0B142512B34B4DD95ACF0D083A9 + 2185DC8FEA057C25C1656D4F20580E60416E1D6B05D7E7714B72FF0EAB4CC488 + D9C9183597BF4FD0D701FCCA55A090DBA3E767D96485CF13E895346D5F66D55B + A057E057A1EA5C944F8CA5DB925C2ACF40E3318B33E02AF06B83BF7AD1E4BB32 + 0FBECBE4EECD3020D7B875E5E2E5B440EF98B9897013EC358E5FB981A574037C + D566BC71A6B05C9103DF55B906F85603E0F50508D85884C0F5C598B041F9EACA + 8C82057DB75622744B95057F375762B2E0EF8E7988D83E0FE15BAB10BAA91C53 + 365558EEDF8DE5265C74F8BA62E3029EC6758C0B98DB90EB77CA8602446CC847 + E4A60244C90DB4B504510ADDB8A5103172F4529987E663D18DCD58F9F176E41F + 5E80D8CDF926D4A3E0AF4254DA5DBE097BE72171EF4224EE11EC5D8AA483CB91 + 7C688529130E2CA19622F1E032241D598994E3AC3FBA1471FBE72376EF5CC4EF + 9B87843DB391B0AB02F13BB9CD2D0588D9946BC0AF006FCAEE52C46FCB45CCC6 + 4C24EE2840C6FE72D615F35872CCF1246D2F33E19F5376CFAE06C09972FB9E58 + 81FCD3AB917F760D72CEAC41DEF98DC6DD957B7603324FAD352153054814EA35 + F3989CC86BB9CE5A149CDE80E273568E5FA98452FE490321AE6C374EB47CE5A2 + E4B68A2F6D45F1D56D1610BEBCC5C08242D5293F259717B05DC94581036EEB92 + B6C979C18673EB38BDC14010E5072E3AC77D0A447079E9C56DD5B0A252E0F6FA + 7ECCBEB11755377662F6073B50714D505661A12D00AC50CFE51F1C60A99CBE87 + D8FE30DB70FEEA3ECCA6E65EDF87855CBEF4A38358462DFFE800967DBC1FCB3E + D987A51FEFC1A20FF760F147FBD96E37CA79FCA5022EFC9DC5DAC765CBD92C00 + 5C2A5DE73EAFEF42E507BBB93FC1E21D36271ECFC1C59D5C976D2E0BFC0A20EF + E3BA72BACA79B9DF40600BFEEE413E7F5FDE25E503DE8E2C2AFBDC364E6FB3A0 + F0859DC832C057E07707B2CEC90D4C5DD885744EA79CD98AD4D35B9172761B52 + CF6D47EA590B0ADBE16FBC1CC082C0A7B7983CBC4982C027379B524058CBE34E + 2894B3DCBFD471CA0062857CDE84A45302CCDCDEC96D06FCDAE16FD229E5F3DD + 81641E53328F4FD3263CB40D0CA7B24E4A3FCDE33FBF1BB9E776218B6D0C043E + B5837FCB2358F5FB2B58FAC579038005844B2EEDE579DD8732F33754FEDAC398 + F38972DA1EC6BCCF0E63D197C7B1FCAB9358F9F571ACFCE604D6FDFE24367E7B + 0A5BFE70063BFF781EBB0582BF3B833D7F3867C23F1FF9E19A01A727FEF6014E + 1A08FC31CEFDF8292EFEFC39AEFCFC05AEFCF439AEFEF839AEFFF4A505817FFE + 1A9FFFFC2DBEFCF93B037FBFC21F2CE72F658578FEB371FFFE91A524C7AF20B0 + 01BE2A7FFA0BFEAC7A9BD4F6BB9FFFC8F5ADFCC05FFFF4BDC91FFC05B72F7D4E + 7DAA10D33F7D838F7EFA1A377EFA02D7FEC663A31492FA228FF5DCDF3EC2293B + 00FE8B72045FC5C11FAE62DF5FAE60F71F2F61E71F2E60EBEF4F61DD1747B086 + E768D5A787B0E233EAD32358F1F9112CA3967E7E0C8B3F3986051F1FC7DC8F8E + 1A5775C5F583FCF76241600160A9E8F24EE4F33ACCE73559C06B34877F8FCC0B + 3B9061739E0B06279CD880598757235EF97DE5BAB58526CFE6F59E7D6997994F + 649B042E1790B5C2936FB1C295EB5AD5B5776C23E28F282CF946A49C54BD00B0 + F24A5B21C5134F735FBC1E92CFECE475A8BCD2D67CDAF93DDCA7A58CF37B917B + F5108A3E388A7C9619BCD6524E5AFBD2871339541EFF0DE5EBA38A331B2DC828 + 972BEF71D99C560E5A85B9CE3EBD1E796736F0DEB80E79BC475AA19BE722677F + 25D2771421717D3A625624207A591C9236A4A1F04039AA4E2E42F9C9C5283EB6 + 0065A796A1F2FC5A949E598582A38B517868AE093D5C7274210A8F2C40F6BE4A + A46C2F46F2F612E41E58804A5BC87B85D42FE1BD58510F2A784F2CBF68BB1FF2 + BEA77B62A9EE95E7793FE43D515117546F72ADF3BE6BDD7B6DF722B97C957B9C + F7260381057A158E9ED3655C6E07C026A72FEF0385C6FD2BE8CB752F711B17B7 + 57AB5CF737DDC7CF591FF8C8612C08AC10D3452C8B2FEB231F9ED30B7A76F078 + 5857C2653A77D93A977C5E08FA16F17951CCF32C37B17116F3D9A2732E090617 + F01814263AF3C47A13565C70D884EB3EB501E9FCBBA41F59858CA3AB4D348C9C + 137C56F1F95478663D9F45EB8C333AF7E80AE41CB17226E71C596203B956FA00 + 0BEEB234DBE1F3F5C032641EE53A27B82D963947961B082C091E4B590717F2D9 + 5ACEE7279FA35B4A4CC8E6E80DB9885A9F8B59EC7BCCDACC3AD56F51186AF60B + 36147279A179E627F0599BB46F0192F72F66B9C880DF989DF310B34BF07701CB + 0588E2FC8C1D73317DA7A528F651A6B3AF3243E576D651D16C379DEDA44893AE + A20A53D84799A4A8255B665B1FB46DAA44D8C60A846E2847F0DA524EB3FFB396 + FDA1E585060287AC2A41E89A124C54EA8C4599F09A9B6200B04070C052F6CBD8 + 77F3677F4B61A39556C3837D3077D6F9AE283010D847E5EA62F8AD2D311208F6 + 5A916F20A21797F9AC2A36F25E5964E4B392F32B05814B0D04F6E6F84220D89B + 52E9BE2C1FE396713F6CEBB9C2067715B2D88C41B4ED52037DC7715C619699B1 + 468191DBA2BC6A69DCA131885CBD2E1CA7B8699D65C518C375461BE05B08E745 + 4518353F1FA3E6E5B23F9B07673980E753733339F6C8800BC73A8A40E4C4FEF2 + 28F6A34715C760B4207071341505E7C2191895AF30C893D13725187D93279ABC + B8A30A22D96E8681BE23B86C38EB46644F81537E8481BF4ED9111899C7B157E1 + 340C2D98864105533030271C7D3326A2776A107A26FB1BF5499B80FE19C15408 + 86715DE772EE9B1A5A14C9714704C70F33389E88667F3F12BDD32761406A3846 + E6CEC0D8D2387854725C571683910593313427188332FCD13FD31FBDD37CD023 + D913FDB30230B4300CC38BA76044E974B8CC8983E7C20CF6ABF3E0230730FBE8 + DECB32E029303C3F115EF314298AE7212F124352C3303223DCE4F81D18EB8F41 + 71FE189E1C8C612941D4048C480B629B000C4EF5C7D04CD6650663686A3086A4 + 4D34F99387A4856024CF8B2BCF914BC1740CCBE0B1274D40AF59BEE81C39161D + A7B9A1678C0F7A533DA3BC8C0BB8F70C1F9317B87F8C3F7A457A9B1CC05DA78D + 47DF99C1E8372B04BD6604B14D30C7D2A1E83A95E3E3107DA8EDCE31B9C6E51E + 665EB9783B9BB0CA963A47FAA1D3343F7498EC8B8E9315B69963EAC9FE681736 + 1EED27F9A36338C7D39302D1263400EDC203D12922C400E02641DE0600B766FB + 36D2A4F16813E6834E5302D03D6A127ACC9C8C5EB111E8A930D2D1E1E8C2B1B6 + 0070B79953D069C644748E0A465B1D474C28AF89445E4749FCFD1C9FCF0C419F + D810F488E2B14DF542E769FC5B45855253D03E7C227FCF78B4E318BFCD84F1E6 + E3F0F7F4BE629C2B9A7A8C4523857376773320F8BD31AE5C36064D7CBCD0C8DB + 1BEF7BFBE07D5F5F34F2F343A3097E6810E089B77DB93ECB26C1BEA67CC77B8C + 51237F4F03BBBB739F5D4DE8E9609383B8A5BF0F5AF973BFDCE6DB0AF93C42D1 + D586E1F5C1438C34FD0AA57A01E016BEE3D1C4D30BEF8DF340634F1FA3775DDD + F1DAC8D17865E808F31EE68D614E7873A8135E1B3C02F5FA0FC18BBD07991460 + 2FF71E8C57FA0CC6AB7D87E2D53E43F072CF81C6E9FB64FBEEA82B87AE1CC0B6 + 7CBDF630CDC6BDAB687106FE76459D96321A5800B84EB3D6A82D00DCB8356A35 + 696BDE25E91D954A9913EAC8B4D0A2239EE57ACFC97860C06F67AA0BD7EFC4B6 + D674DD56DDF04CBB5EA694E3D79EE3F7D146EDF018555BA60845C6B3A54D539E + 5F49EFAC6496A8F51E8FA1412B3CF68E0D00BFD3DC4AA3F6765303801F7CB309 + 1E7843F0B75135FCAD06C00E40580E5FA397DFC17DD403F5940BB88191090FFD + B265F850EEDF475E7D9F6543DC6373FACAF17BFFF36FE301EA3E4A75773DF3BA + D13DCFBC6180F07DCFBC89FB9F7E13F73EC9FA3AAF52AFE0CEDA0AE35C8FD3F5 + 382D73CACBB8E7A957719F22143E551FF7725A4EDFBBEABC6CDE595A1058E19E + 6D9110F52ED4F63E54B2BF2FAD0981AD77AA36C7EF6D24F7AF23E8757C8FFCF7 + E4B88EA3EC60F71700D80E766D0058D335973B2E33D336006C7F8F5EFDEE5CB2 + D53BCA0E8055DAA78DB49EE3FBFA9AAAF98EDF56FF9F02C0B7E318FF4FA9E6F1 + DC71D7C346B76753B7D13FCB00FFD17247898DF118549AF99ADBFF47F3FF8C1C + D7F96FEBD7F6E9783CBF68A369C77947D997DDFFBF00F8FF8BBA19229A378CEA + 69FE03BDC3E61E7698FF87FA170070CD1BD57F53FFEA3E1D8FD351B7B4BBBBC6 + 8DBCA6046659FE56725CEF5FD02F1E0E92E33E241B0036D2C34BB97E55A7878F + 1E4CB687574D3902DE9A00B8A6ECB916ECEEE09A52BD639BEA76D5AEDF9BD0F8 + 8EFBD9C60E7D6DFADD434FE1AE479EC1DD8F3E6B4921440C00B660B03D4FAF24 + 787BA7C28C70993A1102BED26F1EAE8BDF3C24007CAB03D8B8806D10589D1275 + 50D469B98F9D1E49D3AA575E8ADF282F851CC7B68E8C96590EE09B9D1CCD6B3D + 038605716DB2C35F037629415ECBF56BB97FED00D84060CA2C7B9AED057F9FAB + 8F075E789D1DBD3770FF2B6FE1E1FAEF52EF98F0CF72043F54FF6D3CFCEA3B78 + E86575F85E33E1A31F7E89EDD8397CF04576F05E7A931DC3B7F148FD06266CF4 + E36F35429D06CDF0A4602F5547F0F7DD26ECA43646AD86AC6FD4CAC0E1BA8DB4 + AC156A3754C8E79678E2DDE66CD3941DD92678BC4173E316162CAE2DF72FA79F + E0B6A4C739FD18B721D5E2723BD4ADDDAC1D9E68D20EB51AAB032DB7B03AEDFA + 42B383F93A53F376E98BCADA8D6C1D6A1396BA3D9E6EDE11CFB7EA8A17144EBA + 73173CD7A53B5EE8DE1BF57AF747FD3E03F10A4B333D8003150E3A34D8787DF0 + 48BC357C34DE1E311AEFBB70F0A4B046A39DF18EF328BCC3F2BD316338907247 + 9BF11C10064D440B0DA0C68D45F3F15E68A1704AE33DD1DCDF0B0D3C5CF1F638 + 670EB0C6E27D1FB9889557D885831D373476734523576734777747734F0EB8DC + 46E30DF7516830DE0DCD26FA1958DB2C9883CFA961E81419CE41E57834F0F3C6 + 7B81BE68E0EF8DC6417E683D3904ADC343D03C3410ADA704A173740415897653 + 581FE2C74161207A464FE5207806BACD988CF65326A06D44207AC74F35CEDF11 + 1C5C0ECA8C41BFD4486A1AFAA64E471FAA77D274F4E080B42707A943B2E3E03A + 3B17EE8BCA38E0AF84DFEAB908A0C62F2F83D79242782D2E8097BEB417C89DAF + D0D059A674A5DC17E69B76E3575462DCA212B8CCCD87F3DC6C132278CC821CF3 + 12C6BCD461A93AC154C1D011A5492677B03BDBBACFCEC2B88A0C8C294B875369 + 0A469525C3A52A9DDBCA86C2472B9FF0B072CB393BB494BFA922094E95720AC7 + 6270DE4C8C2892833915CE82C7450906026BDEA5220DA32BD3E15A9501973959 + 5C47E1A7D3AD173A552918591E8F61655439D75178EA3919E6F804AA2DF09B6E + 53AA71010F976B97FB1F5C1C0BE51B561E5E69506134864805311862DCB9315C + 6EB985D55EC7ACE3B7425D27D4650000FFF4494441544C0B065B005890564E68 + 8598367986B94CAE6ABB5358D343CB12AD7D737A38EB46B21C29177111B74909 + 962B6CB672031B27B07116CBE5ACE3B420F4B00AD6E97CA914F82E67E910F259 + F057F9C524C176279E87D1F314E239C7FC8D5D38ED2C27849CC00AE92DC0ABF0 + CF72FA2ADCF3923C8C5B4A2D91BB579058EBF2EF29E7AFC90BACB6CA179C560D + 7F1542703C2517AFC0AEE5E6B5F2F82A9FAFDB7C01E0245E5F02BE59460AF5AC + 36CA4D17B0BA107ECA53A79CBFDA9672D9AD2B84BF9CBEEB8B30614309420578 + 057A379563E28632961598B4552F45E7207C6325C2369463D2860A4CD9548588 + 2D733075EB5C4CD9C2361B4B11C175A76DAD329AC2F595472F744D21CB1213FA + 79DA96724CDB5C86291B0A31959AB1B904D1DBCA112347D0D6124CDFCCBA2D45 + 46F17B2A917F62098A4E2E41F22EB6D9948798AD8598B9BD04B314EA79D76CC4 + EEA6F6CCE1F45CC4EE920B680166ED5D84B87D8B116F97E6A959BB1722FEC012 + 241E5A8A597BE621664715F7310FC9FBADBC86C97BAA10B7BD18B1CA35CCFDC4 + 6F2B440215B7258FCA47D2CE6224EDE0B2CD3998B52917C93BCB917D7081096D + A9FCBFCA339CBE7F3E7294EB5200F8D42AE49F5E855C9B0338FF9C42B82A0CE7 + 6A649D5A6B5ED02BBF67C69155C864695EBE9F14045E8FA2B37286C9212617D9 + 66145102BA79E73618A796A60580E512B33BC5AA1D6317ADB68E005865018F21 + F7C42AEE6705F7B312853CAE121E4FC959B9E136A25460E49200F02EA3F2CB96 + 2AAEEC40955CC337B6A1FC8A20ED56963B4C08E73982BF72365E62BBCBBB31FB + EA5E545ED98BAAAB7B30EFFA5E2CFEF020567E7614ABA9E51FECC392EBBB39BF + 0FCB3FDB8D251FEEC0A20F7761FE7585ABDE84D2F31B0C04AEBCAAB0D0DCC6F5 + 7D506EDF5213FE791714125AE1A7154EBAEC8A60EF4E145FDC81A28B022F7B39 + BFCF404D336D83BF9A2EBEAAFCAB7B5160CBF99BC7DF6474711772389F73C90A + 059DCB52E16DB3CF4B3B29B9842D282CE096A17CBA67B61BA59DE334DBA83410 + F8CC36CB096C77079FDD869473DB0D281620363AA3F0CD0AEBBCD182BF3600AC + BAC4935C2EC8CB767647B1C0AF71FE72598AA0DDB95D2C771AC9ED6B1CC136F8 + 9B7E7EB7557259C6D95DC8E26FC83CC7691E6B0EE7E77D720A6BFE78038BBF38 + 8FC2F37B782DCAFDBB1FC50292FC1B96F2EF544E557E7410733EA63E3D84799F + 1FC5A22F8E63D997C7B1E2ABE358FD8D15EE78DDD7278D1378CB37A7B095D3DB + 39AD90D07BFE741907FE7C1507054EFFFA010EFFF0018E52C77FFC08A77FFE04 + 677FFA14E7A98B3F7D86CB3F7D61C2305FFBE92BDC1090C5B7461FCB9D4B297C + F3173F7F6F72FB5A0EDE3FE21B4A619E1DF50DA590CF56E867CBE5AB1CC21FFF + CC6DFDF42D3EF8F11B7CC0ED7FC06D2AC7F075AEA19CC357B9DF4B3C860B7FFB + 1CE77FFC1CE77EFE1CA7796CC7FFF6118EFEF5068FFDBA0901BD5F21A0FF760D + BBFF72153B940F98BF73CB1FCE1A10BE86E764A5A02FCFD5629EB7851FEDC7C2 + 4F0E61FE27474C08EDF99F9CC0DC0F8F41A1B52B6F1C46055576FD209483B9F8 + DA5E14F27A2EE0755CC8F982AB07907F793FAFB3DDD57FBB547D3070743D661E + 5C85B8C36B91C26B229DD75ACD9CD49A77ACB3CF1B88AC0F0A8E6D44C2D10D48 + 64A9F9B4D3BC66547F6233B585D7D3365EC73B79ED6DE7B5A8EB4F0E60EB8382 + 34DB3597705CF5DB9175712FFF8DEC3700388DED33B90FFD1BD1C71482BF0517 + B659D057E19F0541399DC77B59D689B5C8944BF4F406142832C1990D0612661F + 59CCFBE77C641FB02070D6AE52A4EB9EBB2E1D512B9290B2351FA5C717A1E8D8 + 22038A0B4F2C43F979DE234FAD34E1F7D3F75420F7D03C1472B9F208A7F31E2E + E51C5C8862DEE3E4FC2D3BC77BDC99D596B3F7FC7ADED336F3FEA528070AA96F + 015EA9FC12EB2FF31EC8FBA822289408EC5EE07D551FE298B0F3BA07592EDF6A + 18AC880C97B69B792B543DEF49BC871673FD42DE1B8A2F58A19F05800582EDF0 + D7D4F1FE55A630D13C6766FEAAEE63D6F60497CDB6589AC80EDC66298FA194C7 + 26976FF649E5575E6760AF222514F15805A38BCE6F3561A0159ADAC806E17358 + 9A9CC0FAE0C8FEB750BE699659C75621E3F00AA42B35C29195AC5BCD67979E45 + CAB3BC12D9ACB772E22FB200F0B165D472E41D5DCA79FEFD0E6BD932B37EFAA1 + 65C896CB97CF1995D947B82E9FB9E9FB1722716715127656F2393B07F13BCA0D + 04566E60E51156AE7E3D7FD3F91C4ED93317497A2EEF9D6F5238980FB4284D27 + EE9B8FB85DF3F8AC9F87847D0B9170C07AB6DB154B09041B18BC6701A2D82788 + 368098EBEC59C8E7BEDA2C367D841906145B0038723BFB312CA76CAB6239CF82 + C0D414F679A4499B2A11BEB90A611BAB3071752926AC2840D0CA4284AC2E46D0 + 8A7CF82DCE84F7FC548C673F4DB98227B2CF13B82A17816BF210C23E55D03A01 + DC1C78998FEE8AE0CB75BD97B31FC7ED04C8F9BBB684CB0B4CEE5A37F6FFDC97 + E4C09BCBBC970BDE0A0AE7C393D31A13782DB75CC07627B00580158E399FFDC9 + 5CABDD8A225327F06B3E283560576EDF42E3F495C61AE06B9B5EC83EA84D7610 + AC3EACFAB26317E7B16D3EFBA26CC7F5058215165AE1A09DD9C669AE3E36B5FA + B94A77A27433AEEAF7B29FEC5C9908E7AA048E2512318E7D69D7F2388E5566C1 + B93806A304810532CB66720C1347C5C2A574A69113978DC89D8AE1D911189416 + 86FE2972C28660A0C029E78751235242312295D3C9A118963411036303D177BA + 277A4D1D87BE33BCD03FDA07FDA27C30242E102E191170CB9F01E7AC080C4D0E + C160AE3B2C2D1C4359F69F350103A96149A118951A8E51E993383D81F5DCC62C + 4FF48BF3C4E0A4F1E817EB812E91A3D17BD638036847658560745E04C6144E87 + 3B7F9317C7065EFCCDEE55F1FC9D3331BA2C1A63F85BDCF99B5D8BA231307922 + 7AC5F8626082F2FA4E865306F7933A1123D242393F09A3B2C38C4664F1D8B227 + 1AF83C222714CE395C96158641A9C1E89F18C0ED4CC048AE3F3A2F92EDA799F3 + D237DEDF02BFB3C6A37F4210FAC54F40AFE8F1E835C3077D63FC30987543798E + FAC6F8A36BA40FBACFF0E76F0BC280B810F48F53AEE0C9FCCDD3D03B3A046DC3 + BCD12ECC07CAB72BB0ABDCBB1D268D47C7297EE82C001C19C8D21F1D23FC398E + B600B072F6769CC2F94976203C81CB27A05DB83FDA4D96BB3714EDA74EE0B87C + 3CDA47B29EDB6935C907AD423CD189DBED13158201B1E1E8376B127ACF0C458F + E889E8161D8CAE3314992B081DE55466D95DCB662AAF7038FA26F0B76744F39C + 4CE56F0A3430BAD5C47168ECE78C96C16E264F709B90F168E2EF81F7C7CBD1AC + 8FC4C7A0A9F75834E774536F3734A194B3B789F73834F5F5B43E201FEF8116C1 + FE68161480A6C1816813CE638F0CE7314F42B3107FBCEB3B0E0DFD3CD05CEF22 + 42D96EA22FA705B2798C53C3D07D7A387F13CFA3B6C16D295770F3F1DE68ECE9 + 6E423CD71F390AAF0F1B81D7060DC1AB0306E1B5C12C070E453DCEBF35DAC5C0 + 5F8160A5CCB203E0B75DDDF0CAB0917869D030BC3268B8790FF3DAE0517865C0 + 50BCD4679071FEBEDC471FEE0FC3EBFDB9DC264160B980057E0580EBB6EF863A + 6DAC90CDB73880952A4CD3541D856E6ED9916A8FDA2DDAE189E66DF1785396CD + DA71BE8301C44A4356CB980F3AB06C8FBAAD3A19A9CE52673CD1946D9AB12DA7 + E5F8950B58AEDFC7592F3DD6A4031E7EBF2D1E794FEFB6DAE07119236C619E2D + 69BA65F5BC5CBF02C092D2A709FCDAE1EFFDAF37C27D0EA0D711002B0FB0DD05 + 2CF87B8F72FE9A10D0EFE0C157F4EEAFA19115EAF92DDCFBC21BB8FF85B78CD9 + 43397EEF7EE60D0379EF7ECE0AF3AC3CBF06002B17B0ADEE5ED63D40DDFF0C97 + 3FF53AEEA95B1F77D57E0577D6AA679315C259F97CEFAE2BD3CAAB966C619E05 + 7FF59EF277B59EA75EC01D66FA05DC29038EED3DAAFD9DA87DDAFEDED40E7FF5 + 8EF6B77A47FBD053B7D51D0F3BC0DB5F01BD8EF3321139CEDB55DDCE06756BAA + FAFDB883ECE0D711FEDE223B00A6AADF99DB81AFADFE9669B5ABA19BED6ABCC3 + FF7BB2BDD7BF1D0FF877549361FCBF49BF0A7F7F8DEFFD0A03FCC5366A2CFF7B + BAAD39D2717DFBF4AFCDFF33725CE7BF2DC77D3AEEDB5EEFA8DBADF3ABBAEF7F + 01F0FF67E4700E6F425EDE301C806F4DDD16F8D6D4FFD001FCF712994BFF68F9 + 7F4A358FADE6714A3A8E5B40ECAFE88E1AEBFDABBAED4342B26DDF1E06BA5A36 + F06C3D78386F83C072023B4A80D600605B58E89A20D80E72ED70B71AEAD66823 + B86B6F73E7037C10DBD691ECDB35DB366D6DED0CF4B574E7234F57C3DF7B1E7B + AE5A773FC24E860D00DFA579763CEE16007E8C1D8D479F33A1A27FFBF093545D + 0B005377D8DCBF76F87B4F6D015B7DA9761302AB836287B892C95F6107C0CA47 + 61FF6ACD0682ED1D1BB591EC9D1E237588E4E6ADFB9201C006023FA90ED54B56 + E7C926E3FA659D1D046BB9997F527A19F73F57DFE401BEFF0576D49EAF8F7B9F + 7F8D9D3B76C45E78CD848616187EF06576E85E7C9D1D3DB665FDC30A195DEF0D + 760A5F63BB5771DF8BF5F180EA5E79939DC8B70C387EFCED4678422E62C15F4E + 3FFAD6FBA6ACDDB0196A3568C6E92678947AA2610B13225A6E6139841F7D472E + 60B6D5BA6CFB78C3E6464FBCD702B50C00668797F5D2639C7EA2714B03820DD8 + 6DCCCEB9803125C7B04A037FED90586D9AB4456DB653186A49F0F7A9661D0C00 + 960BF8E9561DD9F1EF4075E2A0A02B5EEAD607AFF51E88577B0F40BD9EFDF052 + 8FBE78A17B5FBCD473005EEB3F0CF5FB71B0D26F10DE1A360A4DC68EA3DCF0CE + 88917873F870BCEBE48477468DC29B1CE0BCE7E28AF75DC71840DCC4731CDA04 + FA71E0C5C18DE758BCE735160D3DC7984159F3401F0EBE38A0F2F140331F2E1F + 3B86EB8D46730F8567F2C05B639D506FCC50D4771F898601ACD3A02BCC1F6DA6 + 4E44530EBADE09E43A1C98359F128AA6E113D13E7A3A7A25C7A3736C149A71BE + D9A489681319818EB3A2D171C614B4E660B675882FBA45866248723486A5CDE4 + A092033CE51FE2807340DA740CCEE2C092E5C0AC180CCD8B47FF8C18F448988A + 9E71D3D06D966072387A25CDC080EC780C294CC3C8F22C8C9D5F08EFA5E5F058 + 5488B173B3E0BE2097D379183D3B15C34BE2E1549E04E7CA542A056EF304EE4A + A17C53AE7373E054998111E56C57968C516CE33237B3FA858CF39C745327A039 + B234C940DAB15519185B96863165A9702DE5364B920DC815B41D33CF828F2304 + 420DD814044D86F27139C99D2B205A22C76F9291C0AFC23F0B022B9CF4E8CA34 + B8703BAEB3330D001ECDD299F3F6906E23E4042E951358F9B552E134FB260076 + 9A9D6EE605860D805528681EC708FEF621C5B11858380B030A047E6761A81CC4 + 853116046639203F0A030BA2D82E8EED79DC36682D886B9CBB0A4DCDDF23695F + F67CC39A97CB57523867815C7B1B4170E38AE6EF9774FE04CAF53B4D3E65AEAF + 75748C2379BC82CD72352BB7EFA82AAE27772F7FB3A0AF72FE4AF6792DD3CB2E + 977919E677EB1CA8B41C105970A55C781DC80DE162CBC33C66612694E7D77D69 + 5EF58B3DE5F875372FEEF4922F17E396281FB0951B78DC12951946E396667179 + 0EBC9665C373B195DB5770D78B6D047F258F456CB74079D094DF37DB2C930488 + 03D71461C2DA424BAB0B58162350E10D3794227893F2D2B154E8E66D733075E7 + 7C4CDA5C8989EB4B2DB7AF01C05508DFA05089154653362A9FAFF2FA5AB0375C + F97F4D6845BBD846A115D797206C6D91D1944DC598B65510B8C4E4FE8DDE5A86 + 98ED154633B672D9C642446EA236161847B0606C1C15B349CE5C2B07F0AC1D65 + 88DD25783B1FB1BBE762E6AE3998B57B9E09FF2827905D7AE16BE0EFBEC5E6C5 + B05E04EBE56FF2911588DFBF18715C3FF5E062930731E3D022F3225A7905E3A8 + 841DA588DB5A84E87539885E9F8B24EE336D4F25E2E410DE908DC4EDC5C8DA3F + 0FF90AF57C74093204919527F1E022E41D5F66F249CAE1967B7225B24FAC44EE + A935C83DB31659275721FDE84A649A17E46B8CF32AF3E81A4E2BA4A7A5FC53EB + 0C042E3CB3010AF7295892735A2FFCB50D0B24CB096C00304B037CE51AB3C1E0 + 820B16FC3590F802DB9EE776CEAD473EF79F7B6AB5392E016041EA426EB3E4CC + 7A03814BE58433AE631B90B8B493D3021482BE1B517659DB93437913DB6D45F9 + E59D98A3DCBD977799761517397F6D1FE6DD3880051F1DC0FCEB0AF3BC1B8B6F + ECC5A2ABBB30FFE2562CE0F1CEB9C0DF766209720ECD471ECF9F2042CED1A5DC + EE5A938378F6B53D567E605B4EE0121D871C7457F61AD85C7E759F9996DB5765 + F9D50398FDA19C964750C66981DFF26B070DE02CB8B4C7A83A0730D7510EE07C + 4A30388765368F3F87DB37AE4AC15E1BF8CDD13495C3FA9C4BCAA56B85839643 + 53F037E3C26E6A9791230C4E131CE37AE946BB2C406C07BB9471040B14DB40AF + 81BFA7B71BC89B7486EDD8566EE1C4936A63C15F03E2B85D815F47F8AB693B00 + 369058EE4D4A905A103B97C75878711FE67C781C0B3F3BC3F37298D792C2D7CA + 85BA1F8557F718155FDF8762FE2DCB3ED887CA0FF7A3EAA38398FBC961CCFFF4 + 08D73B8A259F1F33CED6659F1DC10A6AF5E7C7B1E68B6358F7F9516CF8EA3836 + FEFE14B67E7B163BBE536EE04BD8FDA7CBD8FDC7CBD8471DFCF3351CFDDB0738 + FEB78F714AFAEB4738F3C32738FBC36738FFC3A7B8F4839CB85F195DFDF16B96 + 5FE3C39F7E8F4F7FFE0E9FFD24E7EE1F8CBE140C7690E0B0EA3EFF49A19D95EF + F73B7CF2D3B7F8E8E7DF1BE87B9DDBBAFAD72F71E5AF721F0BF87E850B3F7E69 + A0EFB9BF7E86D37FF91427FFFC098EFFE5631CFBE1631C6179F0CF1FE2C09FAE + F3B8AF61CF9F95FBF70AB6FFF932B6FDE912B67C7F019B14025AFAC319E3885E + 2538CEF3B1E4934358F831AFFB8F0F61DE478778BE0F63B6C9A96CC1DF4AB980 + A992EB075164CEFB5E037F751DE6F2BACCB9B887E55E5E6FBC1E8DF61AC89A2C + 487B7C939120AECAF43316DCCD9483DD848FE636284DDF0282752DDA60AF4AFB + C70BE60306CE1B09F62AAF35EB0478057F55CA61AE6B4BF5BAAEAC0F14B8EE59 + 5EEF6778BDB15EF0D7844DE7FA39DC6F21FF9D2807B9F21B0B042BFCB3E0A480 + A4C0AFA0A33E7A51D8E47C852CE6BD27FF04EF93C716A3E0E8421306BAECD822 + 94CAC9ABC80B1BB210BF31D7B87A0579730E29FCFE22E31AB6F2D1CE43269715 + 6ADD134B91CFFAAC0356BBE2932B50A1BCE78A74705E61F5D7A3FCE206945DDC + 84CA2BDB50756DA7F9B04521F515F5C0B87C2F6D41C5E52D5CCE7B19EB15FEB9 + 44F758CAEEFAB54360E51E97AC8F530483B93DD697735A1FC7A88DF297EB6315 + 454328E7BFB152DE676ABA80151E5AF759455D506E74BBB358EEF0626EBB90D3 + 0A355DACF504994D14886D268580093FCDFBB0E6E542B6D20AF0FECFF32EF0AE + 30CF72002BDA44B61CBFA76D3AB5C1720FEB592347B6F2331FD347482BCDC748 + D9C75721E7F84A641D5DCEE7E25293175F39F453F6CD43DAFEF9483F607D30A5 + 8F9D320E2EB496EF5F84F4436C7F7839D20E2EE1BCEA16229DD30A059DC467B4 + 1CBC71DBCB91A867F78E0A038493389DB4AB12297B6673BBF391757831D2F62D + E0BEB8EE21AE7B88CF6B3E5B93B8EF4445FAD8B300F1BB283EDB13F90C4F3CE0 + F0A117A567BDA52598C572A60310561F40E03796F5312C0580A72B2DC5F6D998 + C672DAAE7998BA4320789E81BD02C09A8EDCB9A01A02AB6ED2864A040BFCAE2E + 42A84240B38F14B2867DAA15B9085A998F10F67342D6152188FD2C49007822E5 + CF36E3D714C08F7D2E857EF662FFCF7B591EFC5616C28FF382C11E4B72306E11 + FB79EC0F0A107B71B9A75CBD9CF7582288CBFEE262ABF4B0C973A9DA15DAFA92 + 6CA775382DD7B040F038015C81DC852C297D60AA08449AB6DCBF02C05A668D37 + ACE56A9F6B40B04AD7F9D6478D6316299D09EB0C14CE33E94C4CDF97FD7DA7D9 + 0AFD9C6ED2CE3857B1AF5C3A0BC38B6330B26C164657C49B9CBD72028F30E1A0 + 0580A3319A6DC6CD4982E7DC54B8CF4EC2D88A382A16AE253331BA281A23F322 + D13F2504DD66FAA2FB4C3F8EBDFCD13B2E000366FA63C88CF11816ED8FE13303 + 31821A1AE587FE53C6A14FD818F40E7545BF7037F49F3C0E03233CE0141B04CF + CC69F0CE9E8ED1C993302C760286C6046050943F064EF3C5A0E93E1812ED478D + C790185FCA0783A33D3134D61BC3E27D312A2900C3137CD13FC61D83E3BC3132 + 3910CEA941189D1A02A7F4506A1246A44CC2D024E5E20DC5A0E410F44F969B37 + 1423B84CC0B92F8FBB0F7FC7D09489F02889817739CF4BEE148CCA9E04978208 + 8C2E980297C2A9702D9E86D1C55331B2700A46E6875BCB72266368EA4413E659 + B07750F244AE27F81C05D782E970CE9D06A79CA9462332271BA7F18084200C88 + 0DC4101ED388B4708C4C9B8441F1C1E8C5DFDC6306CF637420A703D02B7A0206 + C5F1F85323D9762A8F310CBD63B87E7C04FA29E551548801C1ED26F9A05DB88F + 15027A8A1F3A450802B39CEA6FC0B080B0DA759D1A88CE930587C7A3DD64E5E3 + 0D4497E889E8387D023A4D0F641988B6111C83735B5AAFFFAC700C49988201B3 + 42D193CBBA4FF743CFE820E3E8ED131B66DCBDDDA38279CCC1E8C6B27B7428DA + 4F0E44EB308EE1796CDDA326A279901B1A788D4443AF5178C76338DEF51C8906 + 9ECE78D7C3C57C5CFE9ECF1834F11B87262C9BF98E454B451E1314F6198B46BE + 6E266A58D3F19E68E24F057AA38DDE234C9BCCE38E44BF94780CC84844EF8468 + 74993E19AD2705A179881F5A86FAA3CD24C1701E77CC549EEF68F49E15C97310 + 820E6113D07E62A0F9F0BD89C24CFB7AE3BD71634D98E7379D46E3ED51CE787D + D050D41F30186F0F1F85B7868D44BD81434C2E60455653F8E7B75D5CD160EC38 + 3474F3C0DBAE63517FB813EA0D1A8E9795FBB7EF10BCDC4FB2F2FFD6E3FCABFD + B8BD7EC3F09A4DF56D2EE0977A0C302EE0E73AF736322E601306BA9701BECAFF + 6B2070871E9653B88DF2F3DECCFF5BCB40E0F626E29CF2FFD669D319B55B7742 + 1DAA6E9B2EA825C7706B951DF158B37678BC5947E3EE950482057D550A02D76A + DEC580DFC79A74C4A38D3BE0E1466DF0E8FBADA8D678B4614B3CFC6E0B23E5F8 + 7DAC81CAE6966CE9D21E7D5BD3CDA190CF16F815F47D1FF751F7DAA0AFDDE56B + F2FD72DEC05FCAC0DF97DEC63D2FBE45BD6D60EF03F5DE31F0F7A157D9F6E577 + 58FF26EE7EEE75DCFBDC1BB8EF85378DEEE1FC5DCFBE86BB15E299F5F73C4FB1 + BCDB56778FEA9F791DF73DF31AEEB5C1DFBBEBBC6A03BFBF04C00602D7A967C0 + AFA4E9BB6AEB1DA6E0AFA53B6CD332DF58E9F2AC77A28E65B5D8C68AE0F8CC6D + C1AF5D0AFDFCF7C23FDB970BEE3A4692B483DF9A7576A02BE86B9F966E793F5E + 43BFBA5CEFC5ED80D7F61EBD5A76B05B438EF057AA5E769BF7F6468EF535743B + 16F0EFC8CE32FEDF261DDB3F02C07638FB7701EDAFE91F2DB7E9EFEDBF7ADA71 + 7F35E7FF1539AEFBDF92E3BE1CF7FDCFA8E6B66ED1FF02E0FFB16E7BB1FD3F21 + 87737813F2F286E1007C1D755BD87B3BFD1B00D851BF76D3B2D73B02E0DB2DFF + 3FA13BEEBC395D7DC3FE3B3775C775FF5DD5DCA611F7650F07FD0B08ACE3B8E5 + 01F4B881BECA0D2C5543607DD5A4079D5D0E20D811E4D604BF766999DDF97BE7 + 8375719780AE721073BEE67AF66DD9DB1BF1A1AD75EE526780E5DD8F3C4D0906 + 3F83BB1E560741CE60D63D66D5DD2D002B30CBBA3BD84150FEE13BD4817854E0 + 96ED057F1F67BB279EC53D0E00F85E416001608586D6B69E90BB97CB6AC951AC + CE8A2D4C09F7AD8E8BBDD432BBFBD7DED1A9D9E91104BEA7CE4D37B0E0AEE930 + 711FBF7DFC59234160B983ED0E61E312E6B420B07D9920F07DCFB2A3A69CC0CF + D872033FF72A141ADA84877EF10DE3FC7D80D30FBD2807F09B4682C2F7B29D24 + 38AC790391590A043FFC5A033CFC46033CF24643A347954FF8ADF74DF9C8EB56 + DDE3EF34469DF79AA3EEFB2DF07883A67884CB1F798BED58AFF9C7DE69621CC4 + B5D8A6B681C0CDF1D87B4DD9416E8AC739FDF8FB2DF19820F17B2D51BB495B3C + D9BC039E66E7FC297D89292770E336A823402CB7B081BF72FF2A3435DBDAE0AF + 4AC1E0DA8DB8BC496BD469D11A4FB56C87E7DA7646BD4EBDF05AF77E78957AB9 + 4B2F3CD7BE2B97B7C7332C0586EBF5EC8FE7BBF5C22B7D06E0FD91A3D1CC750C + DE5248A34183F186BE62E5F4DB4387E19DE123F0B640B0F368B41DCF017C1807 + 4681FE68E6E381167EDE1C8879A255802F3A8505A173180777E1A1E8141C8C26 + E3DCF18E9313DE757236E5DBAEA3D0406E610EE69AF87AA00D07561D264F44D7 + E953D02A6C229A864C40BBC8C9E814351D1D2223D13B310943B3B339704B45E7 + A8680E3A63D07DE62CF48C4FE480713ADA85F8A355B02F3A8607A37F7C2486A6 + CD429F84E9E81215CE011F07BF89D3D027793A7A254DC780CC380C2D4C47FFCC + 44748D8D449759D3D029C652EF9438F4E3C0B06FB642112BDC592EC62D2886DB + BC7CB8546660ECEC4C8CA948C590DC280CCCE0E03A7B0606534372A3E15C9A68 + 5CBC0ADF2C47EA7081C9CA340BF496DB5CAB1582ADCAFB6B85423690B238D1C0 + 5E97D214B814B3E4BC6B6932460B0CB3CEB53C15A367A719D03BDCC04D6D87DB + 65FD281E8B33F76101DE748C66FD08E5F9E5369CB80DC160A7F2148CAEE2F2D9 + 196CCB36FC0D1600E674550AEB780C0A452D002C702A286D606FAA29157A5AD2 + B41CBB063CAB0D25E03A8CFB130C1F2587AEC22A1745B32E06C34A62780EA38C + 86B37E94606B79024BAE4F09004B02BE3A1782AC9271FCF2382C57B0057D759E + 54AF7D3AF1B8EC6EDF91FC7D82E482BF7203EB1CDBC1EF28F33B6C8E65D5719F + 4E731C1DBE160896342D8D9E970ED7053C3F7240F07C592FC22CE8AC3AB9BDDD + 94037881F2FE0A0AA7C1757E3AE733316E5136DC0582F5C26F71B679F1377E75 + 917182E8859FFB12D62DCD31C0D77D4926DC16A563CCC2346E2F8DF576E02BD7 + 6F9E01BC82BD7E2BF38D7C9629BF6FB6AD4D0E7C972B44749E71FF4EE0F683D7 + 97DA5486A075A508A482365650E50816D8DD3E1791BB1660D2962AD34E20385C + E111379662F2A672446CAA44C4E62A037EA5884D02C27A115A6240F0E44D6508 + 376DCB2C00CC69B98043561760D2BA22AE5B8AA95BA8CDC588E4F48CAD82BFE5 + 9C2EC6D48D2588D85884695414E7676D2F45AC60ECF67213EE3976772566B29C + 29F7EF9E0526EF5FCCEE7998B557D30B393D1FD1BBAC70907AE16B5E0CEF572E + E0A56CBF0889FB9721FDE85AA41E5E899483CB90796425B5DCBCE08EDB5E8199 + DC67CCC642C46C2A34A12963B71619F89BBE770ED2F6CC46DC9662AA10A93C0E + 01E0CCFD73CDB2641E4FDA9E39C83EB408B9C796514B4D997D6C39B28EAD30E0 + 55CA3CBE0269479723F3985EB0EBA5BB5EB82B97A340EF46E3EECA131CB129FF + AC5527709279620D943FB8408E60FB4B7F2ECB551BD6155EDC6A24402C297764 + B1423F5382C0CA072C085C7066350ACF28CF23CB936B507C7A1D4ACF6C409900 + F039E50116DCDD6E73010B026F46C1B9B5283CB7861234E034B755C9360ADD3C + E7DA2ECCB9BA13559777601EE7177DB81F0B3ED88DD997B7602EB5F0CA36CCBF + B00965FADD3B2A10BD3A035396C661FA9A14E3AA4EDBCB7379681E4ACFAEE176 + B61B17B0C07519B7AFFDCB4557726127A7F7A1EADA41A3CAABFB5179ED002AAF + 1FC2EC0F95B3F614E67F7AD2805F3B0056997F41795677A3E8CA3E145ED96B85 + DB6529695A7954154AD7C05F9B7204D5CE6F37B9594D7E56EE3BD7B4B1398339 + 9F4965B06D06B79D69970D04A79FDF6501604DB3CE80E1535B917C52F95C95CB + D572109BFAD3DB90221027787BD6E618967BD8E61C169C4B3DABB69623D30E7F + 4D38689BEC6E4D03800D9CB3013A6E3FEFE25EFEF60328BE7AD094B9E777238B + DB2BE0B92CE439C9E3EF52386CCB89BA074502EE37F6A0F483BDA8FCE800AA3E + 3C4459AEE0792CE7DFD88F851F1EC0E28F0F63E9C787B082A515FEF8A8C97FBB + F6EBD3D8F0ED396CFCEE3C36B2DC42EDF8C345ECF9F335ECFFCB0D1CA0F6FFE9 + 1A0EFDE93A8EFEE9439CF8CB4738F9C32738FDC36738FDD7CF71D6C0D9CF7149 + EEE09FBFC175EA839FBFC507F80E1F1A67AFA58F38FD91CDE52B587CE3E7DFE3 + FA4FDFE02A75F9E7AFB9FED738FFE317DCEEA738F583A0F36738F1B7CF7094FB + 3AC27D1EFAF30738F0271ECB1FAF63EFF7D7B0E7FBABD845EDF8EE32B6FFE112 + B6519B79DC1BFF701EEBF91BD67E7B166BBE3983550A89FDD509EA38967F7914 + CBBEA0F8DB977C76140B153A9BE764B62D247A39AFCD0AAAFC86A5D21BFC1B5C + DB8F826BFB90CFEB2E8FD7B6C0ADE06FF60541E07D06FCE6F06F967B693FB2CE + EF317F4B8160FD3D157259D791F2529B1CD50E8E5FC1605DBB2A534F5BA1A02D + 50BCC3B889ED6059EE626BFDEDC61DAE10CE4927372149E1C74F6EE6BA82BC3B + CDF5A8EBD23E9F714EAEE4DD6CCFEBEFD846084C67F01ACDD18712BC6EF3CE29 + 0FB91CB0FCB7C6DF25085CC87FB7453C1EE348E57D2A87F7B0AC93EB91AD3CC0 + 9C57EE5AE5E0959B37FFF862E41D59809CFD7390B9BB0CA93B8A912E37F0AE72 + A4EE2A43E681B9C83FBA84F7898548E27D583032F3F022E41F598492532B507E + 76358A4F2E473EEFBB852757F05EB616559794FB77032A2F5AF9CF2BAF6C3560 + B782F7AA4A01DC6BBB794FB180ACEE79A517375B1058D10D04730DC4B5C2CCDB + 5DB976E7AF3E4E911495C0D499FBA572FD723FD776F0FEB3CB442928E77DCAFA + 6045110BAC880A76086C7D642308CDE3E176CA795C721417715F855C6ECEA1FE + 6DF23C6A5E1208D6720176859AD673C011001B08CC6B22FFACF2CB0B000BFA6E + 30EE5F81602B2C37EB057E05E0792F5764883C963927F83CE2B32A83F7E9F4C3 + 4B8C920F2E421C9F6D31FA506B2B9FC3BC7F27F13997B85B6913AA58CEE57399 + D32CD30E2E45FA213E6B391DB3AD02B376542279DF02A4CAA9CB6DC4EFB4D64D + D96F397B93E4F4E5B333614F9529D30F2D40C69125483DB008A9FB1773FF7C56 + 1F598A64415E3EDFF5C156229FE7CAED6FF2FB735A1F79E9397F13FC4A4B10BB + DF02C0317B171AE86B89FD03F61962B84E34FB07D37709EE4AEAEFB0A4A629A5 + C5B6B906000BFA4EDB319FED161A102C00AC3E91A4A828A15B2A11B655EDD83F + 625F2784FD9F50455011F0550A8D35059820104CE9A3BB80B5ECDFAD2B361200 + F659960FDF65B918BF340F7E9CF65B51C0BE9AA4E84005F0512868F601BD9553 + 56FD3CB6539FD16D6196E9431AA7B060F1E25C6EA7103EECFB79ADD0878482C0 + ACE77A5ECB0BD95FCC677F5300399F7DC902B809DE1AB8AB9CBF5608684162BB + F3F75608CCFD2DA2386D8F6AE3A234260AF1ACBEF11CF675258E379C2AF481AB + A20CA96F3F0B8315E2396F2A8629EFAF3EF2CC9B8E41B91C07E5CF605F7C16C7 + 1CB1EC8FC7730C9488B1551CC35424983AA7A2991855100527AEA37248D614F4 + 4D0E419FA430F44B0B47FFD4508EDB2660408C3F06CEF2C7E0B8400C8A0DC0A0 + 5901E837C3CB3880BB4E7241F7C9633068E6780C8CF145FF193E189E30112E69 + 93313C692206CCF243DF685FF49AE18DDED3BCD06F9A3B06507DA7B8A1DF5437 + 0C9AE18961315E1819371E2313FD303229004362BDB92D0F4EFB634C460846A7 + 066144821F86C4F96278BC3F9C1283B92CD8388EFB996D7BA14FB437FA69FF71 + 01182EA76FF664B8E44E816FE52C04CC4B845BC974B8144D857BC90CB8154FC7 + 985296E5D1702BE5EF2F8AE0EF9F02B7A219F0288EC2D8824838E74CC1B0F430 + 0CE63918C9F33286E3A7B11C3FC981EC5D9900AF8A788CE5F973C9E7FAD9D330 + 34751286248762687218861A401D8681B1C1E81315809E33941E693C7A44FAF1 + 3883313881E7269D7FAB748EC952A38C86A7456310C7C3DD67041BF8DB3AD40B + 6D58B613049EE68FCED3E5EE0D44A7C80074E2BCDAF58C9AC8ED4E30D30393A6 + 6278560C06A6722C9D348D65247AC54DE2583B08DD63823030710AF71BCEE309 + 448F29DEE81C360EDDA67AF16F330183E2F93B13276140FC24F49D29381C0CE5 + 206E1DE28D16419E6811EC810E5302D07EB21F9AF88FC57BDEA3D164BC1B1AF9 + BAE25D0F27CA190DBDC6A0919F1B9A068E439B101FB4E27ACDFD0481C7A009F5 + 3EDB360DD047E85E68A6DCBE2C1B0778A1D5E420748C8E40CFC4180CC84C42DF + F43874E7D8BF5BD41413E2B94BE424EE37081D26F3F7B25DFFF828F48B9D6E42 + 5DB79AE08B76C17EE81432012DFD7CD0C44BA19C9577D805AF8F1C89B74639A3 + 81B32B1A8C7042C391A3D1D293C7E5E58B862E63F02E2500FC96F368D41F3112 + AF8D74C29B6CFBA6F318BC3662345E193C1C2FF51F82177A0F347AA9AFE0EF60 + BCDC7B105EEE3510F57A715E61A0294D2B04B41CC0CF77EB8717BAF6C54BDDFA + E385EEFDF01CA79FE9D2074F75EA85BA1D7A181900DCB1279EECD01D4FB6EF8A + BAEDBAA06EDBCEA8D3A6930DF87636B0D8B886B9FCA9F6DDF0B4DAB6ED6654BB + 5567D46AD1D1005FC15E49219FABC33E3753D8E70E78B4493B3CDAB81D1E69DC + 168F346A6DC15FEAE106CDF1E0DBCDF090C23B1B59219E8DDE6C824794EBF74D + 4B0F2ADCB35CBFF5057C6DAAD700F7D47BD748B0D79EF7F70195AA7BF92DDCF3 + C25BB85B219D9FB740EF7D2F2A9ADF3B26ECB39CBF773DF71AEE7AA6BE81BBF7 + 6AF9F36FB07C0DF73C5B1FF7A8DEB6CC88D3BF7BFA15DCF594540F773E59CFC0 + DCBB4DC8E75729959200AF057E05817FF7C48BB7C0600BFEB24E0616BDCBA47E + 5BEBB99B005870F7319BCBD7A1AC7E27FA28DB3C6207C04FE3370F3EF90BF8AB + A88F7F0F00DB97DD650B136D87BE356500B07DDA067C6B02E09A727C576E9FAF + 595F0D806F27C777EB9423F4BD9D9406B29A0FDC4EB779BF7F3B0EF0EFC89193 + FC9AFED976FF69DD16EE8A49D9EA1D4344FFA29D9D01FE1A0B74605CFF50FF68 + 1B8EDBAA397FBBF635EBED725CF7BF25C7FDD4DCFF3F92E3767EA1FF05C0FFB6 + EC17F02F2EE2FF96FE85F37313F4F286E1007D6BEA17B0F776FA5700B0DAFE0F + 81F1ED54F306F89FD66D01F0DF91E3BAFF13FD62DB7C70D8E1EF1DF7F1C16607 + C172006BB91E3C76F1A125D85B1300DF710F1F9482C00E4EDD5F03C0AA77ACAB + 293B0096FE69005C4372101B80ECD009B8CBD611B0CBBECCFEE59771123FCA4E + 822300A6EE1104A6EE7E82D3ECBC5403605B5B4BCA4361C1DFDF3D6CB98D8DE4 + 3E160096F358AE6307007C879673DA7474582F40AC70D0F72A2CB4424AB3E324 + F8FB1BB6FD0D3B469220AF1DFCDAE1AF7203DB21B07DFEBEA75F35E5FDECDCDD + FBCCAB06F60AFA6AFE4195ECF869FA2176021F66E7F0E117D859E4B4BDBD00B2 + DADFF3ECAB14B7FF9C0582EFAFF7261E7A555F16BE6B8595AEAF69858C7E8752 + FE60858EB660F083AC7BE0552E635DED77954FB8997112CB39AC9CC4C63DACBA + 864DF0580341E16678A2A1F29F34C2436F353261A39F6BD9092FB5ED82E7D941 + 7F5A1058E1A6A5260AF5DC16B5153E9A9DEB47DFE5BA82C6EC744B8F37B4BEB0 + 7CAC01F7FB7E73AEDB86DBE98C57391078A5630FBCC06D3EDBAA23EB058A5BA1 + 6ED33678919DFE973AF53450F8F98EDD50BF677FBCDD6F30DEE83300AFF5E98F + 577BF7C5DB0387A0F1480E72860CE720A40FDE1D3A1C1D7CC6A3734000DAFAFA + A0CD786F749C104071E0181C84CE94A6BB4C0C4607FF0034721D8B7739487A9F + 83A146AE1CA48D1E85561EAE68EB3D0EADBCDDD9D61F3D264F42EFA8480EC034 + 109B8C1E3367A2CBF428B40D9BC68169347ACE4A448F9804F4884A40BFB80C0E + 34B33948CBE4606D263A4E0A46DB1085AB0A42B7E993D8761A7AC446A227D523 + 761ABACE9C8CCED11CE4B1BE6F7A0286166563407606BAC4CF42875951E83033 + 8AD3711890C5EDE665A26F6E32FA6527A03F35302791F5711898390BA3F21330 + 226796C95134386D1A46E5C6C0297F1686E770804D3917C5C3A93811238B938C + 7377DC825CB8EB0BFDB9991851968CE1A5968357D0537052F0D6A92419A3D9DE + 59D0B6301E2ED498D224B850AA13581E55168F91657114D72B4F659962395FB9 + 3DBBB3D72E0152674A50D859F097A5EB6CB97D332C90C969B9919DE56CAD4866 + 1B3968F575BFC22DF3D82B5278AC566866415F85AB36F987B9DE08CE1BB8CA63 + 3760B45C7985B90DB39D448C289103380AC38AA231BC38DAB88187513A76E72A + FE0EBD1092D3B64ADBE7DF81FB139C95D3D60E80056CEDD231EA589D0472D9CE + 005E1DA7423D17C49A90D7CE3ACE92240C2D8E37E0D80E6C0DA016F8D5F172DE + 6936CFCB6C6E53A0572FB8E6CAD96BBDF0B2C4E50A73679469BD1893F8DB5DE6 + C80D2E27479EE5D258CCBFEB62015F072DCA32A5C0AF641C1F7284C8C5619667 + B32E075ECB39BD44C038DD4060C97DB15CBFD9E645A137D793E357CE5FBF950A + F15CC47A2B17994AFF5585085C536C4A394D146E30787D395581A07565F0E77C + C0DA524C605DD0860A4CDC5089F0ADB34D6EDFB04D9C5F578AC91BF582B31C61 + EB8A0CE89DBC9E755438974D5A5B8C49DCBEDCBD93A8C91BCB6E3A7F379662E2 + 9A0204ADCA43D0CA5C84703A74752126AD2FC0E44D82C5D4C662446C5478E852 + 4CDB526672054FDDA4B2CC40E1D85D9588DF5D8538857ADE3D07D1BBAA306347 + 05351B33F72E348ADE3D0F51BBE619F0AB5292035840B8FAE5F05E6ACF62241F + D0CBEDD5486199BC7F2952951B58F0785B39666D95FBB7CC00E0C835B9507E42 + 85A4CC3EB410E9FBE62291FB55DEC2846DA50600A7EF9D8DA49D6588DB5A8C78 + AE270770CEE1C5C83DB6C400E0ECA34B917964A905804FAF46CE49BD605798CD + 6594157253AE2B85E1CC3D2997EF06F3C25ECA314E2DE570DC68C1132ED37CB6 + 008AADDEBCDC579DE6D9C6EE0A1324966338EFEC3A287764F1C54DD500D882C0 + 6B0C0096E4FE15F82D3BBB01151736A1FCBC2DDFF0255B18D4F3DB4C8EE0C2B3 + 6BB88DB5283ECFF54FAF4211D79D7B752B165CDF89251FEDC5D28FF79950CEF3 + AF6EC7A2EBBBB0E0CA362CB8BC194BD866F1A54D98736A257279CEA296256362 + E5544C5E3003693BF350757E292ACF2D47F1C9C5283DB3025517D61BC75EF9F9 + 8DA8BCB41DE5172D1772F9C55DA8BCBC1F73AE1EC6ECAB4750A5F23ACB6B4730 + FBC651CCFFE414E67E7C02720097B05DD9550B00175DDC832285855618E82BFB + 2C47F095BD06062B1F70DED5BDC81684BB6473FA0AFA9EDF817C96059C97F2A9 + 5CA35D0604ABCCBAB8C3E46B150816FCCDE276338D762383CBEDEEDF8C732CCF + EE408680DD19CB7D2920A73A49798605E20C8C9304860D4416D41510DE6A4A4B + 0EC0F7DC2EB6539ED6DDD6B44D1917F620FBE23EB30F393CB3790C26BC30CF85 + 1D02CA699A47E5F258F5BB0480EDCABFCCF6721E5EDB8DE2EB3C6F72065FDB8B + 529615D2B53D3CE7FB30E7C67ECCFFF020167C78088B3E3C8C451F1FC1A24F8F + 61C91727B0E2EBD358F10DF5F529ACF8F214D67C7D061BFF70115BFF78D912A7 + 77FCE132F6FCE91A0EFEE5060EFEF9031CFCD30738F4970F71F4878F70EC6F9F + E0E48F9FE1CC8F5FE2DC4F5FE282846F7089BAF2D3D746977EB640EF951FBFC6 + E51FBFC2C59FBE326DCF50A77EFC82EB7F8EE37FFDD4C05EB97AF773DB7B7FF8 + 10BBB8AF1DDCEFF63F5EA12E63CBF797B085C7B3E9BB8BD8F8ED05ACFD46A0F7 + 2C56FFFE0C56FDFE2C96F3B72CFBF224167F79028BBF388E455F1CC3824F0FF3 + 5A3B88B91F1DC0BC8F29E54DA604C9AB3E3880329E27E5F8D5B558510D7F0FF1 + 7C1E40E1751B00D675C7F39DCD6B26F7D23EE45D3E6024F06B97E50216B0B500 + AE40B1058B798DF15A50CE5FE5FE8DE5FD4CA51D042B54B4A4BFBFDA09FAD65C + D74839824F6D44E2D1754838B216292736DA423AEF34B9AFB53C8BD33982D336 + E93A4D3CB681EDD721F58442D2EBDF09AFA3B3BCD64E6F46CE19DE83CEE85EB4 + 85DA6A6064F1F9AD28BDBCC3846B577D0EEF5179E7ACFB95F2D6169CB29CC00A + 9D9FC1FB6DD2CE72F3D18DA22B14B13EF3C07C93573DE7A8C20BAF30F7D1BCD3 + 82976B50CCFB50E9B975563EF373EB793F5B87629665BA9709E8F2FE5775652B + 665FDB858AAB3BA03CBD02B8E5BCBE8D4B97C755C2E3321F995C52E40385C0DF + 89F2ABD67285611670958AAF58619915EA59EE5F6DA384FF560460AD7BE63694 + 5FDEC27F230A956F6D53B9C94B04C6F9B72EB9AC8F581452DFFAB0E62608D634 + B72BB86B9CD3DB794EE5FCD5BEF5018BCD51ED207BB86DE3F895CEDF0CFBAC67 + 81423F0BF69A6789ED792237B09615B03E9FF77D8569D6B349E7531F27295285 + 806BCAC1457C4E2E441AA7530FF1B9B96B36A2B6949A5CFD717CFE5ACB172061 + CF5CB36CE6763EA377CD31E036ED20DBEFB4DACFD85A8A993BAB90B84779F6F9 + FCDC3B8FDB5BC47D2C411ACB546E47DB4AE4B6121541837FFF343E33530E2D45 + B25D3C86C4434BAC881D7C8E5B1F732D331F74695E60376E3F9773DED23253C6 + 510600EF5E644A3B0C16F8350098D351D4F4DDF3ABE1AF99DEB9C00060397D55 + DA01B0CAF0CD9508659F48D0376CDB6C4CDA390F9376CC657F495098F5ECF348 + A1EAF76C2831F0D71E7125907D1F3980057FFDD84FF25D5560A2B204ACC84780 + 0030FB7BFE2C03571460C20AB663BD2FFB7F3EEAE7D9FA72DE26C24B26C7081C + 2B2C4C37FD3E4D7BB22FE9CD75058CBDD95FF45CA2E5A95C9E66F52BB9CC6B89 + D62F80A772029B50D082C272005BE1A11DE1AF1D009B2836F3330C70765B98CD + BE7506FBCBEC6B730C60FAE3A571EC7773ECC1BEB00BFBE9A3CA399E619FDD99 + 1AC9BEFD30F6ED47144DC7A8A2288E3F666118EB0616446330FBF82314DD87E3 + 9A2105B34C0A98E18AFA63EAE231343F068372220D301EAEB61A13E4476364E1 + 2C38716C33AA682646E446C2297B2A466545604486DCB16118AAD0D0A9211892 + A4DCB6BE06BE8EE0FCB064CECFF4C3C0F8400C4D0CC600967D048063FDD13BC6 + 82C083667860D07437F40E7746F79091E8193A0ABD2639A1FFD4B11814E581E1 + 337D291FB6F1C080C8711818E9663460DA58F48B70C580A9AE1816E509A7387F + 8C8CF7C790685F0C98E1857E515EE813E98E01B1E331263F023E6531F0289E01 + CFF2687857CEC498E269702D9906CFCA18B85744C3B56C065C4A66C0B9709AC9 + 413C3C7F0A5CF223318EE7D29D6324F7E218B8E65B8EDF310533E05E160B8F8A + 78785526524970E73974E339F4E058C99DE39BD1B91C57A54EC1C084508E81C3 + 30306E22FA4607A0D7F4F1467201F78E9E80EE9101E831239063E5500C489882 + 212933303C9D7F97F4680C4A8A44AF98508EAB03D05E619FA7529CEE30CD1F5D + A283D02B2E8C63E750748F09E1987B22BACF0842F7693CC75C6754D64CB81670 + 0C9B3603233363385EE5182E330A7DE2C3D07B5630FACE0A42D7082FB49FE882 + 2E21AE94C0FD58F49AEA859E119EE836C51D9D278F43B7081F7499321E6D26BA + A345C058B40EF6424BAA45904A6FCEFBA265902F9A057A9B90CF8DFD3CD02CC0 + D32C6B15E24579A0F3D44013D6BA2DE75B048E635B37340E1C8B96213E6833C9 + 0F2D27F9A229A71B05F9A0F9E409E8143715BDD267A1777A2C3AC54F43DB8889 + E8302518DDA687F3374EE6F6420DF0EDC9E99E5153D02E34108DBCDCD0D0DD15 + CDF5A17A802F1A7BB8A1C11817CA15EFB88CC6EB2347E0CD5123F1AED368BCEF + E482E6633DD02920085D2684A0A517F73DCE03EF8D71C31B2347E1E52143518F + AA3F7C04EA8F188557878DAC06C072FED6331A8257FA0E36D1D904785FECD6DF + 52774B2F08FCDAF42225006CAF7FAE477F3CD7BD1F9EE9D607CF74ED8367A967 + BAF5C5535D7AE2C98EDD51975269A63B7433AA760F77EA8567048DED21A4DBF5 + B03987BBE2C9D6DD4DB867815F4DDB01B0E0EFE3CDDAE1D1266DF150A35678A8 + 614B13FED900609682BF0F28A4334B397C0DF81504669DE0AF5D0F995CBF56B8 + 67E3EAADF7AE71FBDEFBF2BBB8C7615EE0F7018160CEDFAFDCBE2FBF85FB14E2 + D9846E7EBD1AE456BB7C9FA98F3B9F7A15773C590F773CFD2AEEA2EEA1EED6B4 + 81BCAF70B9252D93CC3CDBFFAEEE4BB84320B79624D0FBB205836B48CBEF78FC + 05238160A35A37E16F35007EE2392E7BCE9862EEB4415F454DB4CB4060CA00E2 + 47B8FC61BD57BD0980EDFAED834FE10E03802D67AFFD7DAE31F318D9A6EDCB6C + 61A2EDC0F776929148EF8FED80D71100DF0275FF15FD1B00F857EBFF9968A18E + EFF56D26B7FF946EC749A47FA6CD7F53B7E361764E26F0FBBBBB1FF9F701B0F4 + CF72B05F6BA77A47D5ACFBB5F635EBED725CF7FF846E770C7F4FB7DB46B5FE17 + 00FFDBB25FC0B7BD90FFD372383FFFD4BEEEB0E7FAE58DA006F4B5EB17A0F7D7 + F4EF00608775FE133723C79BDA7F5BB7BD81D7D0EDD6FB7755BD5DDB3E6F0B80 + EDC77227657F18F1A16507C0D5F097B203E1DFD9E0AC1DDADAA7EDC0D671DE51 + 6A6B87C69A7784B976F06B977D1DC736D57A90F52671BF2513D2839D803B6D9D + 037DE1F55B2EB7435FB3DC167A5AED1406FA770AFF6C83C076107CD7E38ED3EA + A0B0F3C176266CB44D773CCCCE88F206B3F372D7E382BEEAC4A8AD6031EBCC57 + 6DEAF0A893F3143B3B6CCF6DFE4EED582F27F1BD751452FA25DCFFA47203BF88 + 7BEA28CFAF5CC0CFB2E3C4F5EB3C8FBB9E7C1177D57D01773FF512EE7BE65503 + 741F7AE10D3CF8DC6B46D6B4057B1FE0BCDADCCF0EA0DA3D6007C09CD7F20795 + 33F8D957D9CE0A077DBF1CC04FBFCCCEA1C2490B045B20F96E7D0DF8743DDCAB + B62FBE8187EBBD85875F79DB940F2A7C34F5A0E0F02B9CA7EE7FF975DCFB22F7 + FD32EBEBBF83C7DF7ADFC0DF47DE7C0F0F0B10B37CF4AD4678F4EDF7D9196E68 + 64858A6E8407B9FCFED7DE3561A39F6ADA06CFB063FD34CBBA02B5EFB7C4534D + 5A1B70AB6572123FCC6D3FC4ED091C3FFA6E63AED798DB6B8487B8BD4738FDC4 + 7BCDCCFA2FB5E9847AEDBAE27985FA69D4D284A456986A01E55AB690D44FB5E8 + 80275BB447DD16DC3EDB3DD7AE0B5EEBD1076FF6E98FD77AF5C5EBBDFBE3ADFE + 1C9CF4E88D3A6DDAE3A5EEBDD060F848BCC7414C430E7E5ABABBA15360207A86 + 4D42F789A168E3C381D2D8B168E6360E8D5DDCF0F67067BC3BD299ED3878F3F0 + C6FB1C343577E67A635DD08C83A9363E5EE83C31181DC342D17CC204B40E9D84 + AE51D1E838753A5A4D9C82A613C2D02C381CCD8226A16D582407A3F1E8119582 + AE3392D06D5AB4C9D3D33E7C0215848E1CD0758A9CC481EC54748B998ACE5111 + 68C3815D8BF010B48F9A86FE99E918565C80BE1959E83033166D6362D02E2616 + EDA215623A195D9312D12599FB8E8B44DBE8296815198AD653B9CDA830F44B8C + 34F989FACC0AE3803A12AE1CFC7A96A7636C690A07F8891823176F598A71E28E + 9D970D8F8579F05E52084F85649B936D42392B8CB0BEBC775B988F71F37331A6 + 321D2E25C91859108BE1393170CA9B85D1450906268F2A9C855145B1E665CBA8 + F259C6AD2B17EFB0E2440C2D8C33A18FC7CCC93272AE486799692437B020B0A4 + E931029902A936D06AF2160B0257241BE7AEB62388AABCBA163C55386681531E + 9B395EBD3CA2B4CE6C8584967B380DA32B05982D39CBDD5B3C13238B62AA35A2 + 3806A34A63B93C012EB3935926C289FB1400B6C4E31188E63159B986ADB0D376 + 206C855CCE34505700D8E43E960B5AAE634A005CC76E9CC8721297C9756D03C0 + 5A47DBD2F6B90DBDEC32E077B6C271A773DB02E1B6729E15CE79F47CCBFDABDF + AC9766D52FD4F4B7E2DF506E0BBD70F332B9DBF2A93C28A49FD7B25CF362CE73 + 699E4D16041EA7DCBF3677B027E7BD14CE6FA915067ADCE22C782CCB31AE8EB1 + 0B780DCD4B83C7224166FD6DD2CCCBC0F12B941FB8D8BCE8F35A9A0F9FE585F0 + 5F5D82C0B5E5F05F538AF12B8BE1B7BA88F32598B0B6D4841D941345F0376473 + 9551E826A9C272B96C60BD20AF423E6F2C330078E2AA7C4CE03104F098021665 + C29FD274F08A5C847059C8DA42037E0581276DE0BE57E4C07F5996A9973B780A + B713CEFA291B05808B31697D11C2D70B02972272730522B75462DAE672969A2E + C3F4AD2588DA568AE8ED9588DE21F85B89E9DBAB307DE75CCCDAB708B19480AF + C2384E97583F83F382C13377291FB0F2FE2D44CCCE7998B5730112F62E46825E + 0AEFE0FC8EB98865FB99DBCA11B5A90871DBCB90BC5B6EDE2A037B95D7578EDE + AC830B0DDC55DD2CB613044EDD3D1B190A4BC975E414D6B28C030B4C08CB2CE5 + 4534F0572EAAC5C894235800F8C44AE3AECA38B21C69879621E5805E84DB60F0 + 919548561EC663AB9179721D324EAC35D32A35AF7C8F2A538FAE42F2A1E54865 + FB8CE36BAAC3AA0A10DB21B155BFDA849BCE3E6995590A397D6AB57100E79D5A + 832C1E938EAF90EDE4002EE2B2E2D36B0D082E39B709256737A14861A84F6DA0 + D6A2F8DC1A949E5FCB656B917B6239F24FAE34D065F6952D58706307165EDF81 + 45377662D1F59D5870791B165DD986151FECC2CA0F7660C1D935283BB20805FB + AA90B5A318C99B3291B3A7080B2FAFC09ACFB662C98D0DA838B71C25A7971927 + DFECCB9B318FDB9C7375072A2E5980A4F2F21ECCBEBA9F3A84AA2B075149555D + 3984729665570EA04A6176AF1FE2F43E28EF6F19DB4AA59A665DF9B5FD26F76A + 11B7537869B709075D707917F2AECA056C0B09ADF90BF65CA672DC09C208BE58 + 4E60493058A054B9824D88E84BBB90CD6D66733F59DCBE947985D382C217AC5C + AE82BC826FB954960DC0C9C9997E7A1BB2CE0AB6A98D72BB6E372E62E581CD64 + DBF4F33B907E6E2BD2CECA052C6DAF06BFE9827117F79A52F382BF0A039DC9BA + EC2B728F723F67B69BFD165E3BC8DF66416101C03C2ED7BC40B6C9896CA0367F + 137F770E7F772E7F9F1D08E75E66C97353CCDF542610AC50B657F7A292D3CAD1 + 3CF7C6412CF8E030167C7404F33E3D82059F1FC5E2CF4F1A2DFCEC38167E720C + 4B59AEFAFA14D6FEFE2CD67E730EEBA94DDF5EC0B63F5CC6AE3F5EC55E855BFE + DE2AF7FFF9060EFDF543EA631CFDEBA73841C9BD2BA87B863AFB379B346D9B3F + F3D7CF7192ED8ECBDDAB50CED461EAD09F3EC4813FDEC0EEEFAF6127B7BDFD8F + D7B195E516EE77E37797B0E1BB8B5827E8FBFBF3466BBE3E87E55F9CC6321EFB + 12C1DECF1536FB1816F0F8E77D76B43A24F6DC4F0E61F6470750F9C17E6A1F2A + A41B7B295E733C2FC557748D293F2CCFD7B50328E6B527E7AF81BF3C67F9D7F6 + 990F0FB278BDE9C3819CCBFB91CFEBB880D7B64A8160E5D9B53B80E5E4569868 + FDCD6E01C0273723E1E83AC41C58612070EAE9CD06021B97F9295B8867CAFED1 + 8140B0AE8B4C5D67D279B9D13723F1D87AC41F5E63007016D7573EDFECB35B91 + 75660BAF797DF8C06B99EBE770BFF93C8EEC33DB907C742D928FAC41F6E92DFC + 37C2EB87EDD38EAD439AC2D99F586FDCA7F9540125002CF059C6EB4810544ED6 + 7C8529A60ACF71F939E53B57187ADE67CEAC4601EF9345BABFF05E53CAFB9572 + 95179F5EC36DAD351FB2545CDDC67FEB72CF6EE63D482EDF4D2C3719D85B7661 + 33CA8C93D7FA884539CD2BAFC8616B83BD0E2E5E0378793C02D302B426CFF815 + 0B129BFCE236F06BE0AFD6D33AD776DF04C0941CB972DF2A4C7E992030F72D17 + B0F2082B1F6F912030D737F97FB90D398E0D60D67D4D1FD870B942EE174B0AA3 + CD7FEFF972F06A9ACB8A78FE8B741F32F723C982BEF90EC057397EEDF3D9BC67 + 2BBF7C8E722CF35EAE7CF1F99C9672F5A1D0A975149F01BCD7671DE37344219C + 6D5036E3E8124E2F44D2FEF906CCA61DE6F389129C8DE3F34F4AD2B390CFAFD4 + C3CB90784029160477ADF0CCC97BADD0CD02BE8AD211B9A5D4A47A88E1333B4E + B058219CB94ECAA1C566FDD4237C0652890715CA7991994E3EBC14298797737A + 05EB97562BE9D00AF37C14FC350EDF3D8B6DF97CA9FD4B10C767699C01BF2AD9 + 66FF322E5FC27EC0222B0C34657D1C361F517B16209AFD0649D077EA8ED998B6 + 6B2EA6B3BF600FF72CC9F52BF06B97D26084B25F14B68D2535917D958982C1EC + AB846FAB42F8D64A846E2E43F0866284B07F132A5730FB3AC17201CB01BCC672 + 005B00381FFE2BF311BCA6D8E4131EBF280BE3176723807DC4092BF2D86712FC + CD84CFD22CF82ED3C77E4AE791C13E9F95DEC353FD4193EEC3EAFB79B0EFE8C5 + 79AFC59A4E85DBBC24D6A7703E13DEEC23FA2C667FD33879D997645FD5735101 + DC17A92F69397DEDAA76F9DAFAD2AEECEB8E613F577D5E45E331695C0A6230A8 + 70A6991E599164FAC8A3ABD8972EE71884320098639091ECCF3B1547C3B92406 + CE654ACF126FD2B4D853BC8C607F7C48411C06E5C56018EB352E70A94AE5BAEA + AFCFC488126E83E301C98C6B28E7B204B8962B977002C6719C3096FB1BAD70D2 + F933E0941F0597A2688CE13E9D72A71AA7ECF08C491891156EE647E74F377973 + 87654EC6202E1B92118EC169A118983411A392046E7DD17FBA1B7A4D7646F730 + 277413049EE28A01D33D4C58E8918913304C619C23DCD065E248740E1A866E21 + 23D0357818BA4EB4A67B853B63C00C0F0C9B391E4363FC30404EE0696EE813E5 + 8E51E9C1189B371963A8B14553E15A348DC71C6E1CC01E553170ABE4B92A9E8E + A1D9E118981682C11961189CC9E34C0DC1F0D43038654D81737604CB088CCE9D + 06379E5FCF8A7878CF498287C653D9D33128651286A44DE66F952B38096339EE + 1B91311D03E4B28D0D459FE8093C1E1D93C0B71FFAC44C40EFA8407499E2CBF1 + B037BA4606A067F444F44F88C0E094E9E8AF90D0B1E1AC0B41D71885E2A65876 + 89E2F83B32105DA383D02F29020353A7A1775CB809D5DC39C20FED43BDD029DC + 97E3EE60F462FBEE51411C0B87F36F321DFD12C2D02552B9823DD12E640C9AFB + 0E471B7F9EEFB071E812EA4AB9A053882B5AF98D4063AF4168E23314AD035DD1 + 36D80DAD046C03DDD03248E19ADDD1C8772C9A057AA155F078340F9433D81BED + 2605725C3F111D262B2C75004B3F13BEBA37C7E103391E974359A1AA15B2BA8D + C25AB3945A2B8770F878B49CC4F96913D12B251A83F352D02F331EDD12A6A373 + E4241E73B0F970BC5DD804A3F65447CEB70D0D44636F37BC397A045E771A8677 + 5C9DF0DE5817BCE93402F5870FC15B4EA3F0AE8B0BDE767636D36F8D1C850623 + 9CD1C485BFC583C74DBD377A0CDE1CE184D7878FC4AB4387E1E541438C5E1932 + 0CF5060FC54B0307E3E501ACEB6FE50C7E6DC070BCD27F28EA2904B4DCBF3D07 + E1E5EE03F062D77E78BE6B1F3CD7A58F09FD2CE02B37B0C241D7EF6385857EA5 + 2FA7B9FE6B034798F0D12FF51E841705910584BBF6C6539D7BE16996CF76E376 + BAF735754F77EE89673BF736F057DB1608AEDBA62B6AB7EAC252A1A3BB57C3DF + A7DBF6344E60BB1BB8768B4E963BB86547134AFAE146ADF150831678B8614B3C + 66C23FB7C243EFB4C0438EF09752E86713FEF9AD9B2EE0075F6F740B0016EC95 + EEB795F7BEF22EEEAFD7000FD46B88075F66F9D2DBB8FFA5B7F0C0CB6F5BD32F + 2A87EFEBB8F759397B2DA7AFCA3B6D40F77702BA06F4D6C39D755FA6ACF27752 + 1D4B775277B19DA4FADFD67E11BF79E205DC41D91DBE02BEF630CFF650CF8E00 + F88EC7F5CE52D0D701FE725E1200BE43A61701E0476D00F8E1A7F19B879EC46F + 1E7ED27A2FAA77A68F3D833B386DA5E7B324E07BC783AC7B50D0B72E7E47DD02 + 711F906CEF7EF54ED7366D4960D7AABF9DCC3BE5FBEB9877D876E8FB8FE0EF3F + 5A6E24006C7B777E0BF4AD01806F077E7FA17FC101FC5BC9FEBEFF3FC8341C19 + C9EDB65973F9FF11D56051763E6677FEFE4700F0DF5B2EFDBD36F6658E6D6ACE + D76CEB5857538EEBFE9FD0ED8EE1D774BBF56FD1FF02E07F5BF60BD851B76BF7 + 1F91EDDCFCD3FBAC06B8FC07E90873FF1DFD2B00F85F50CD9B955DB76B6BD7ED + DAFF27F50F7300DB6FE8B759F77FA2DBEEEB76FA1500ACD22E41E03BF9F094AA + 21B020B17D9A12A0759C77941DEEDA21B07DFE76AA5ECFF660FF9DA31EE47249 + 5F6ED96487C2060C1BF8CB7AB6B12F575BA387B8EC610B029B1CC0C6D96B8160 + FBB45D76F8FB1B1B34765CE7CEC79EAA06C5F6756F2EFBFFB1F797E1791CE9B6 + 3F3C99CC046C2786D84E0CB11D3333333383CC2033C8205B92654BB62C666666 + C9B2CCCCCC8C491C270E0C666636CCDEFBEC73AEFFFB61BD6B553F2D3FD62893 + 993D33FB7DFFE7DA1FD655D5D555D5D5AD47DD55FDEBFBBE2DF86BDA53CA0B02 + 2BA6B0620BBF535F10B829DEFD5020B819AA7DC4F4234EB83EE444AA3E274D1F + 35C1DB0DB9DDA019DE69D41CD59BB446AD161D50A74547D46CD6CE48DB4A0576 + AB356A89771A7C8277D8470DE6AB71E2575D96C14CB55DBDA1AC8DAD6368FBDD + 462DF0D6474DF1738D83EDAA715B4058B059E0F92DD56FCC763CAEDC474BD59B + B646B52602C3AD39296D8B1A9FC8CD741BBCF7495B135FF8FD961DF05E8BF626 + 7DBF5547D468D5C1E83DE6DF6F63E93D5B6D3B71B2DB9E93DC7678AF7547D46E + DF15B5E542DA917ED0A5273EEAD9170D7A0FC0873DE566BA27DEEFD0C5A8A640 + 7287AE78AF7D174BDCD6FE0FBBF741C31EFDD0B4CF60B41838024DFB0F45FD6E + 8A5BDCDDC434AEDF7D00EAC97A986AD477281A1B97D3438CC571A3FEC3D072C4 + 38B4E542A1E5C871683674149A0E1D898603D947BF41683468383E193916AD27 + 4C41FBA9D3D173FE020C5FB71E13BDBC31D6DD030397AF46CF854BD07DFE6274 + E082A4251734523B2E6E3A7091D261C254749B3C0D3D66B8A0DBAC39E8C38552 + FF156CB36C25BA2E5D891E6B3762A0D7762EE80230C22700FD3D7CD163E356F4 + 58EFC545A237FA52FDD67BA3B7DB560CDCC8451C8F3BD8D313833C786C6AD0B6 + AD18B6630717B47E18BE2B00037C77A2A7C736F4F7F5E7223009931333302234 + 1E7D76861A0D088841DF9D51E8B12D14BD77066240B01F868450417E18E4B70D + 83B9F81CB56B1BA684EEC494101F2EB2B7616AC876CC8ADA89C5A9615C98C761 + 756122D6142763496E8C157B373F068B0BE3B0AC34192BF6A46155B95CF96663 + EDA11CAC3C90855507B2B16A5F26962B26AF2C5893033033C10F739202302F2D + 04F33342302F4371B582AD972F7916005E50106780A7BEAC9775EFB2DDC90600 + 0BF42A4EB1B4B830DEB2CE159C2D8E835C162F294BC4FC2259C1B24D790A5CCB + 920CFCD597FA82A9D2C2028EB954E055D6B889E605926D4920102A206A5E1E95 + C46349710C5C4BE3E05A1C8B25456C971F89C57961468B723856B984CB0BE5B8 + 22594716D1D1D64BA53C5E9B82682CE6381616C56111FB524CDD850E97CB96FB + 6507642EB1AC960575058A4D3C606A6E9E0356B35CA9E0B001BF6AC736EA4300 + 7981FA2DE7B9CB6DB35203783966A64BF7261B777872A5A77D4BE58ACFE4259E + EB3E0BF6AE3A9865FE5E9695859481158E58BF26C62FF3564C3789E54C4DDC36 + A6CBD9E7F27DCA0B160B1A6798786E8A172CAD7294ADD038CA93B06ABF62BFF1 + B806F66618D8BBE1781EDC8EE618AD3F966BB6379EC877520125CBDF5C038BDD + D8C6FD74213C2F94C2EB7C293CCE14C2FD540E369D9255702E36531EA7F28C4B + C3CD2732B06E7F02D694C76275590CD696C562DD9E38ACDF178F4D0793E07E38 + 199B0E25C3E358BA81C002C002BF06FE9ECBC7AE4B250890CBC58BC506F2FAB0 + CEF6D3B94CF3B1E36C01769C2B60B9E20A2B4670017C996E3FABB8C159D875B1 + 88ED8AB053F0F76A29FCE5EAF96639026EEEC1AE6BBB0DF895FCF422F772A939 + 8E81C037E4167A0F022E9720E852B181BE81170AE1CFFE032F1421E8622102CF + E722E87C3622AF1620EEE66E24DC2A43E2ED3DC6F237F14E9981BFB1D74B4D1A + 230B282A89FB13592FFC02DB9EC944F49542C4DEB45C6346DDD88D38BD589765 + 93DADEDD6BACAB8CFB67C1DFFB870DFC0DE73944717CD1B7F621E2BA05AA23EF + 1E42FCA3E34682BD31F78F18C53D3C86389645717FC4ADFD88BCAD17E5163016 + 1C16F8552AF86B03E0C44747D8C741D6DB6FACA9E21F1E42CA530B0847DF2E33 + 634B797018290F39B67BB2583E64ACF1B29F9D42D69313C8647FD9CF4E23FB53 + 8199A3C8F9EC08329E1C46D2DD7D068C673E3982DCCF4E20FFF393C87D760CA5 + 5F9E4531F3A9BC2ED9F7F7A2ECF971947F711C7B3E3D82DDCF0EA2E4C95E943E + DD8B9267E5D8FBE5011CFA96FB5E1C44FE933DC879BC07B94FF7A190C7D8FDF2 + 2CCA7F7189E90594C88DF43757B0FBDBEB4C6FA0E4E52D147D751DF95F5E45EE + 175721D7D0B92FAE20FFA52CFCAEC3C4D27CC9F46BC5DED4F675039E148F53CA + FEF20A32BF94D5A2DCA85E42DA579791FE9265AC23CBD70CED7358D9653AE08F + EACA22B8423C9EAC670D3C952B5F1E4B4A61DE96AC6CED98AE069C7D7593FDDF + 629B1B4812AC35D6991791C4F10B2627B35FB9F035A0EF25FBE2788C85F17381 + 612BAEAB8937EC80BE02BD52050016F07594277D79D3724DCDF125B3AFB46F6E + 1B4BD3784140596B721C295F5916C0E9DACFEB69C538BE84241ECFB26CBE64E0 + 733C8F9DC4F1657C7D13D9DFDE42E637374DFCDA1CA679DF5916AEB2C02EFED5 + 4314FE86E9F70F51F2DB4728FECD2314FEFA3ECBEF6137CBF7FEEE310EFCD353 + 1CFCA72738F4872738FACFCF70E25FE52AFA0B9CFFB72F71E1DF5EE0E2FFFA12 + 97FEF7D7B84C5DF93FDFE2FAFFF9056E5177FECF2F71C70175A5FBFF8FA507FF + 9FDFE2DEFFF31BEEFB156EFDEF5FE0C6FF629BFFFC16D7FEB7A5ABFFF90D2EFF + C74B0BF8FEFB4B9CFC8FAF7182E9F13F7E85A3FFFA050EFDF3731CFCE7CF39AE + CF8CF6FDE119F67CFF18651CFFEEDF3E40C96FEEA288E32FFCF53D14FCF22E75 + 0785BF6219B70B7F7D17F9BFBC6D2CA4B3F8BBCAFC4AF15AF91BE2B54CD7EF82 + 7FCB345EB774FEBDD378AD8D78ADD35FDE44EAD7B7A89B48D60703E6E3010179 + 01E03B06005BD6BFFC1B7E761571CFF8B7E0EF45BFA514FD4DF5B1804382C072 + E11CFDE40CA21E9F3600D8FC4E9E5FA6ACBA82BF360856FD44EE33B18399A6F0 + B79DCCBF73DC63B6BF779CE929A41817C1E78DBBE094CF0416E50EFDB289F39B + FA5CFF3BFC6D7FCE368F4EF1DEC2FB0C8F9DC6630A0027720C892C97FB610155 + 59C00A00A73F3B834C96096E66F1BA64E97FD0C04CFE7F51395F59D6B7C68AF6 + C579E302BEE86B6E2B76B93E4A79718EDB978D9B6553CF40DACBC895C5ED8B8B + 06A42A26B9DC38E77CC56DD613C4950B673B96789E5C9BF3FF5BCA95BB65DE4F + 0ABED58722BA2F701FFF769665B0F63BC030EF0D72C3ACB8C692DD5679816049 + E75861014CE50A4473BCB92FCE225BAEFA9F0BEE5A6353AC610389797EB95FB0 + 8E43D98AD5FEFC3CB23E677D739DCE1B2B5D2BE535E375CB643F72F92CD86C00 + 30EBCAF57FEAA782BC6760622AEB23A027FAD04816BDC791F65431DFF5C1CE71 + EA18924DD881C348E433411F00C5DEDB8BA85BBB1179B394CF83DD88B9B3C748 + 1058E0572EA063F95CB0A0EC7EC773643FA2EE588AB8BD17E1B7048CF7F279B6 + C7806359062B1E70049FCD81578ACD475CFA702BEC4699253EAF236EF33927A8 + 2BEF1B7765B16B59F846DD635EC057CF3C3EC322F87C0BE7FE887B8710A50F9F + F8AC535918EB04DFDC874059F25EDF8BA01BCAEF43C035C7F6F5FD2C3B80C06B + FBE07F957304964B3B2F698E50C6F9822C7F59C6BA7EDCF665F90EC500BEBCC7 + 02BD4ED6BF3B289F8B2556CA7ADE9CC34882BFEE67F2B0459E51384F11FCDD7A + 5E213464F59BCEF95216B69ECD83F7397BEE9409F7E3DC77521E57B2B1F15806 + DC8F66C0F37836B6705EB8615FB2D1A683299C43A560E38164B8ED4B80DBFE44 + B809021F10E0D5477F715823307C3099EB0101E018CEE163B86EE09C8C73B175 + FBE3B0F6402C56ED89C6CADD14E769AB76B38DE68B9AC372AEBE8CF3DC957B52 + 8D5B67415E1BF8CAADB34959663E6CA4EC0F1F1772DE3E87737181DF69E98106 + FE0A08EB234963015CACB5468401C0F3388F9F6D3CFA04C0257D175CD2E4F639 + 0053D28331352BCC78F299C7B585F116C479BCBC1429FC8D3E34D5C7A70A5F23 + B0AC74BE3E3AA5E6710D332B8BC764BA9873FC053CDF79BB6330A794F37BAE19 + 6617705D42CD2BE4188A39867C8E213700D3337C313BC79FEB1AAE334AC2B906 + 08814B0ED74E59FE70C90DE23905626E6E2016A76DC7A2644FCC8ED9849972F7 + 1CED8E1911EE4C3DE112EB85D9521C15BF15F313B6619EDC30476EC0E4A01518 + BFCB1513FD97625AC82A131F7856D85ACC0E5B87D921B2025E89093B5C312960 + 39EBBBB16F37CC8AD980B9891E989BCCFEA8F969DBB020CD87DBDB302B61AB89 + 713CC67F35C607AE3596CA332237630E8F3F274EC7DC8269611C5B94075CE2BC + E1124F256E874B822F26846DC1889D6B30D26F2D26867A6066EC0ECC8CD981A9 + 11DB4D9C5CC5EE1DBDD30D5303DD3125709371013D7ED7068CD9E186A1DEAB30 + 74DB6A036C27877A6372980F2686CBB39617C6047A622C352E64ABD158964982 + C08AE93B966593227C0C181EEDE78E9102AA9E82BBAB30CC7B2D866F5B87312C + 9F18E26534DC47A07609FA6F5A8CFE1B97A0CF9A7918E8B61023B72CC5F0CD8B + 30DAD315A3BC9663E0FA05DCE7827E6EF3D17F03D7FC1B9618EBDFBECCF75ABF + 14BD37ACC0802D6B3178EB460CF070439FCDAB317C8707A6460652019810B20D + E335E6200F133B786A942F6644EFC4249E9F80F67881E9400FD6531D4F5EB74D + 6CBF91DA84613B36637CB81FA6278462724C20C687F96172F02E8CF7DB8EA19E + 9B38F6B5E8B781E7B16135FAB8AD42CFB52BD075F962B45F38076DE6BAA0CD1C + 17B49B371B6D996F3D7B263ACC9F832E8B1619755C301FED67CF46C75973D165 + 2EB7E7CC479B192E683175069A4D9A8A6653A6A1E5F499D42CB49EE982B62E73 + 4CBED9E46946B2FA6D358D7D4F67B9E2008F9F8E96E367A0F5C45926356E9E47 + 29E6EF2463E92BF8AB78C0EDA7CF43479785E8306B21DACF5A800ECC779CBD08 + 6D67CC3310B8E9F869F878CC64341C33090DA8C613A6596EA627CD4053E63F1E + 37054DC64EB12C8547713FFB6F347C6205FCAD376034EAF61F5561F9AB18C075 + 7A5B2EA1EBF51F81FA0346A281DC450F1A8D0F7A0FB1AC7FBB0DC4073D06E383 + EE835087F93A5DFB1BAF7472FF2CD90058F05796BFB6F5AF0D8025C5F895B56F + 8D56DD28CBE5737541E0E69D2A80AFA47C75E59BB6477587AA35698777E5EA59 + 10B89142C25996BD4682C002C082BEF5143ECEB2F2B5F32A37FBA89FD66D869F + D4698237EABCB2EC957BE7CA00F8CDBA7201DD043F93D5EF6BE0F763232B8C5D + 23F6D5086F98F7A296641023E8AB107C6FD4B4DEA39AF7A5B5F5AED57ACF6ABD + 5F7DE5CED906BFCE863CCEEF97AB7A6F2CD9EF977F481575AB80BF76DE79DF0F + A902FE4A7F0100AEB0F0FD31FD3500D87ECFFFDFCC332A1FEFBF45955894CDA9 + 9CA16F65556EF367F597B0C23F57C7DEE75CA7F276E5BACE6595E5DCF6BF4355 + 8DE1875455FBD7F43F00F8BFACBFF987FCD7C8716D748C1FFA477ABDBE0D70F9 + 0FE90C73DF603DE7EDBF447F2300AEEAC6F497E8C7FAAAAAEC6F966ED855DDCC + 6D55BAA1FFBD54D1BF5C3DDB723A9E91D9E643EFB5879005812B3FCC04810D10 + B61FA66F295FD780E0378CAC87ABFD903656C4553CACCDC3D8D96D06B78DA5B1 + 637FC543BE1AB7F9B095D5B2B15C961C0F601B00FF4479C776557206C06FBCC7 + FA9500B033FC55DE19E8AADE4F287BBF2DED53BD9F7F20CB5E87B53065CA1C50 + D8EEC706BF6FD5951BE8D7D38AB8C3F52D20FBB3FA1F1BF02B002CF85BEDE396 + 78AF591BD46ED91EB53E91D56F4B546FDC8293BF564C5BA29A0031F5AE60F187 + 82CA4DD92FFBAED7D8E4ABB31F4166FB78D51AB06E434EFCEA7DCC496063FCFC + 23C514E6F18C3E31C77D9BFD99E3F338D53E96781C876A38A0AF52A966F376A8 + D3B28319DF7BCD5A9B71D568DE8693D736A8DEBC2DDE63F9FBAD3AA066AB8E26 + AF32ED7B87F5A477D946DBD55AB29CAADEAA3D6AB6EF8C0F3AF740DDAE8A29DC + DD405FA976E7EEF880654A0583DF63BD9A9DBAA26EF75EF8A8475F7CD4B5171A + 74EB83667D07A3C58011682CEBE1CEBD4C0CE39A1D7BB3BFBE68D0739051C35E + 830D046ECA7A4D070D47B301C38D1BEAC6FD87A0415FD6E93310F57BF5C747FD + 06E3436E371E381C2D468D43DB0993D071FA0CF45DE48A01CB57A0CFA265E831 + 6F117ACE5B8CAE7317A21517338D868F43FD4123F141DF61F8A8FF4834E93F0C + 1DC68C43F729D3D165EA4C74E5C2A9D3ECF968377B01BAB872E1B5621DBAAC11 + 04DE8111BE8118ECED87FE9E3B3170AB3F067AFA7171B6055D96B8A1FDA255E8 + B864357AAE598F7E9BDCD1CF7D0BFA6C70478FF5DCF6F0C2E8A0104C888CC6C8 + E0080CF20BC1D0C0488C8948C4C8D0780CD81585DE3B22D06B47247AFBC5A2F7 + CE78F4F289465FBF300C0D0FC1B8F8484C8C8FC698F0208CF2DFC1C5A51F2605 + EFC4E4001F2EFEB761EC0E778CE6C2784AD836CC890F30EEB0E6A78662764A30 + 66A7E9CBF6682CC88D31525CE0E5BB53B05A50716F061697CAFD5A325CA9A545 + 09589815899989BB30236E8701C0F3D343B0202B140BB22559FA5A56C073B3E5 + 524D2F5014F337D680DFE5724D4C09FC0A06CB0DF4E24259FE5AAE8EF5C267C5 + BE54B8EE4DC67C87E5ADAC526509303B3B0433D28330333D1873D8B7A0ACEA4A + CB0CB8B400A8A0A7F2AB0F6662F95E1D2B8E638FC1B232016E8EA550318CD99E + E35C926F01E005D9C11C77081615456251613816E4E93CC2303B2B04F373AD97 + 3EB21A985F146D5E12C95241F055B059EEA665C16BC522B65C410B461B0B616E + DB56C202BEC695764982D96FBBBA3379F6B3F4403A5C0F666019C7BD92A9CE4B + 1058407CC57EB973B620AF00AD80ECEA43593C47C5ECCD6699C02FCF5FB0BE02 + 02B32F8D51D7C80171570A001BB0CB3E0E5A50D780DDC3EC57E587D807F3DAB6 + CBD71DCB81DB71EA582ED61EC9C19AC3FA2040EEA2330DE47D0576F32BE0AFF2 + EEA70AB1E96481D1E6D3C5F03C570ACFB3BB992F32656AB3FE442E369CCC83FB + 99426C395B8C4DDC5E77340D6B8EA460E3C94C6C399D67E200CB85A1DB11C516 + 8EC7BA7DF1C6AA77CB91746C556C5F59051F4AC1A683092C4B63599681BF3EE7 + F3B153D096F23D5760AC7F779C2F84DF8522037CB5ED6B620A1760E779C51316 + 04B6630B17B2AC10BE1772D94F8E01BF3B2E70FB62A1B1DA09B851865DD7CBE0 + 77ADD44810586E1C65CDB3FD7C01DBF138CCFB5F2D45C0D512F85F2E4210FB08 + BE546C00B0E06F28F719179617F34CACE108B91EBD5A8458C529BC568CF85B25 + 48BA5BC6ED4284C93D34C722EB60C5FA95057004DBF99F4CC3AE136908E5BED0 + 8B39F03F9D8E008E3782FBC3A960F62BD01AFFE020E21EC88AC9B27C12F80DE1 + 58C2AE94229CE711244BE46B7B0CE09555AF14F7E098B17AAA00C094F291772C + 1798B20456990D80ED7606063F159C398284278791F058A09879C5237E729475 + 8E9817F672C1A958BEE9AC6BB9A53E60DC82663D3D818CC7B2C43B819C4FCF20 + F7B9ACF38E327FD4B86A15FC4DBEBB0F998F0E23EFB31306FE66F118B9EC3781 + D7D9777F14FC0F4523E4681CC28EC523F34621F63D3F8AF2E702C1FB51FA5939 + F6BD3C80BD2F0E22F77E11326E1720E7C16EE43CDA83ECC7E528F8FC284ABF3A + 87922FCF1908BCF717D7A89B28FBF6268A5F5E47E1975790FFE2B2B198131C91 + EB54B9552DFAE63A8A6599FA35EBBCBC8682AFAE1A502C2B3BE3E2F52B59D359 + D687A906A0505F9C47FA8B8B3096BEB2E6FBD2B2F693A5A081408E32C162DB2D + B42058E6578E38C21C8F89E74A09A21A17D194E2B8CA6253D0572E98950A021B + D7CB2FACBA7201AC18B0922C89D5473AC72F17C1CA275500E00B16B4E371535E + DE44A200AF602F535B02C3095F58505800587031F59BDB48E218E592DAB8A976 + 582ACB62394963E47105AE1507398563D131E2784DE34C7A09317211FCD97903 + 84E5B2D8C4AF55BF3A6F6E677F771BB926CEED7DE47D771F39BFBC87DCDFDC47 + FEAF1F20EF57CCFFF22EF27F71C740D4D2DF3E44F91F9E508F50FEFB87D8FF87 + C71608FEA76738F1CF9FE1D4BF3E3730F8EC7FBCC0D9FFF502E7FFF3252EFEA7 + 65B97BED3FBFC5F5FFFD1DAEFFE777B8F99F82C2BFC4CDFF6DC1E16BFFE73B5C + E1FE2B8EF8BDB22A3EFFBF5E525FE19CFAFAB71738FDC71738C17D47FEFD250E + FFF14B1CFA972F70F09F9F63EF1F3E43F9EF9E99D8D1D2EEDF3E42E96F047E1F + A05890F79777CCF873BEBB856CFEF68CBEBB891CB979E6B9A95C2EB353F9DB31 + 96D302F8BCD69655B505FE65396BF45CEEB7799DBF12FCBD4DDD4132AF67127F + 13FA7BA57E2917D036FCE5DFD058FFDA00586EBB151FF8AA01BA06E4DA7DF278 + E6B7C454BF9704C7EF2489C749D1C706CFAD36B625B03E3A4810AC659AEAF8B0 + 21E9E959C43E38C17BC529FE5F9C45DA7387AB609E932C990D04FE8CE7F7F965 + 63FD1B2FD82C97D34CD5D672FD7C01C94F14E7F72C14EBB654A05C709CBFDDB4 + A7A791CABED39E9D31EE8DE5DAD8765F2C8B56B93D16D8CC923E3F8F1C59F0BE + 14103D85ACCF4E1BB82AD8ABFF4B59D39A364CF3F83FEFEC4AD94879C15F037E + 6FA2E817B78C5BEE7CE6F3057C798F506A596FDF327959B467F37F41805740D8 + B8861618363058DB163CB661B089C5ABB150169875C06749309763CEFCF42432 + 9E9D36CAE43D344BF18E3F3F63F60B12E73C679EA900B0CA729ECB129AE7CEEB + 952DE02BEBE96767914EA53D3D8354CA0A11A0F8C96790C6BED2789CD44F2D77 + FFB665AF71E7CCFB701AA5F8F389F70F9AB8BC09F7F61B8F0F09BC6FEBC3A4E8 + 5B6588BA2D60CB546EB729C1DFB87BE5C61238E1C1010B00B3BD14CD7BBEB1F8 + BD598E303EBB226FEF37903784CFDDF09BEC4FE0D7C063B975569C5EB96BDECB + BA7B10C154C058AE9C230C38DE8F70A672DD1C71E700426FED338ABC77C858F0 + 86CA9297F9D0BB0711C2B1871A1D44289F9D216C17CCFE83D87F20DB04DD3C60 + 1478633FFCAFED3552DE2E9302048729DBA25756BECACBB5B3F2B685AF01BE2C + 17003675AF9461FBC51213FBD7E762A901C05B2F721EC57989C0AF07E72B5E9C + 47789D639EF31DF713199C27712E2431BFE514E746A72CEB5FC503DECC79D196 + 1339DCCE86FB31961F631DCE9DB61C4EC3E643A97017FC3DA079940580371C48 + A2942663BDA0EFBE38ACA1D671EEE576987331590497C762E9EE68CEAD63B06A + 4F2CD69647338DC49222B94EDE8939E93B4D1CDDC5F9E106122F2D8DE7FC3F09 + 2B39E73500D8805FCEA71D73EAC5C602D8515E6AC15F496B03015F416059F22A + AFB5823E0C1500762D89C29212791C8A8442B8CC910BE8EC20B82886AF0030D3 + E9391198911783D96C3B9773F1C55C872C294B351F8F5ADE83B8C6E17C59A93E + E6341F65B2DE126E6BADA2F9FDFCFC58B366995F128F99253198C563BA704D31 + BB2C162EBC0EB3B87E9855140697A250CCA15C0A8330BB28D8A4330B02312B3F + 0833A86979811C0BC757148279C5E15892E7874559DB3137C513F352BCB198D7 + 4E96C44BB926599213844572679DB20373927DE19A15809505A1582EB09CEE8D + 85A95BB1247D1B9667F96259E6762C4DDB0AD7244F2C88DC8099412B31337835 + 1626786071AA3766C76EC4B488B59811B3012EF19BE192E881994CA70826476C + C4D4C8CD181FEC86D17E2B31CE7FB571512D97D52E319E9819E9814941EB316E + D71A8C0B588331FE6B307CC70A8CF05B855101EB31DCDF0D437C5761F0F69518 + B1D30DE302B7188BDC7102B8019E18BD8B7D07BA635AF0164C0FD98C89811B31 + CE6F0346EF588F613E6B3172C7064C0CF6321EAB0C140DDD8AD1811E18CD3EE4 + DE59F991FEEE18B18B6B59E6058365153C4AE53CD6501F378C601FA30450B7AE + 3156C023B7BB6114354EC70EF2A23C307CFB1AF4D9B808BDD72DC0C04DAE18BC + 713106AD5F88214C4779BA62A22FCFDB671D4679ACC4B02DAE18EA21CBDF65E8 + E3B6D8C0DFFE9B9663C096D518E1C3730852FCDD5D1CB72FC7B995E3F4C1D4A8 + 40CC8C0B814B02FFC6F1BB3035DA1793227D3039D2171304B4FD371BAB64816E + 01E159B1BB3099EBF251BB6439BCC1C414EEE3BE0A036541ECEF89213B796EBE + 5B30DA672B467A6EC1C00DEBD077ED4AF45ABD0C3D56BAA2FB8A25469D972E40 + C725F3D161A162F7BA18EBDF4E8BE6A3A35269FE5C749C3B07ED058767CC42A7 + 5973D075EE62B4993E1B4D274C46B38953D16CD2347C32791A5A4D9B899654EB + 19ACEB32D7091073FFA4E906FCCAFAB7F92401DF2968368A1A6DB9787656B371 + D32C403C71165A4F99CD7EE7A0C51417B67741ABA9B38D15B0E06FB309D30DDC + 35B181E5127AFC5434556C615918EB383C9E60F0C72CD7FE8F464E607DC51C76 + 41935153D160E8780380EB184BDF91A82F18DC6FB481C072FF5CA7EF30D4ED3F + 020D878C35AAD777386AF71864E06FBD5E4351BFA7429551DD5F87C082BFB565 + 01DCBE5785E5AF62FF1A315FC3A1F75B51ADA56E46EFB5EC6C5C3B2BAEAF0D7C + 15CFD786BD02BF359A7530B2B7951A6BE04656BC5F0160C15D0BF82AB560AFF4 + F30F154EAE45C5BE373E6886376A3779DDB5F307964B6813E7D7B8856E6659F8 + D66D6242D1293C9D5C3F1B97CF755EC1DF37EA34C04F2801600BF6EAFDA980AF + 95B7E1AFB32ADEC7528AE32BD921FC8C47C7EA32ECA96F00B02DFB1DB1F3FB65 + E7FC0FA902023BDE233B835C7BDBDE67DE193BF2556DDBAA00C08EF7E495F336 + FCAD78C76EBFF7AE2CB3DFF14EDE59AFBDABB7F2AFBDE77762197F777651852A + 1FEF1FAA0AEFAF4E1CEABFA00A9E55996939EB8758E15FC2121DFBDF7883A9E4 + 54F627EDAA2AAB2CE7B6FF1DAA6A0C3FA4AADABFA6FF7F00C0550DFCFF05D28F + D4366BAF12C2FE03641FC7595501E10A78FB86C432815F3BADD8C76D872BE82A + DD42DB75ABFA67972AFEE11DAAAACE5F2159DF4A55DDC87E4CCE6DEDFEECF237 + 79D3FF4BFB353775FB265E951C37F6AADAFE4DD2F8A80AF0FB03B21E4EAFDABD + B65FED1D7A430F3AAAC285B44306085701678DEB65E51DFB9CEBA8ACE218EA43 + FB5466EAE8416BB5B3C76E1FCFC05EEE7BD30187DFA8FE0AFE3A5B024BF6B1EC + FDAAFB13A63FD13EB913E1C4E3679C74384B712454AEF812AFEA7042E2B45FE9 + DB9CECBC23B02AF7CE661F272E02C0726DC2BC81BFDAB6A1AF33F87592CA6421 + FC469D867853F0F6434159B97E6E8EEA1FB7C4FB9FB445ADE6ED50AB691B546F + D4C258F2BEF3915C470B1C37B12C8899BE5D4F31881D16C9EC4BDB82C2EFD46B + 62621A4B6F6BFBC366784B656AAFB63A96FA9495B0F294E9BF01EB51DA166CAE + D1442EA405795B5758FE0A001B38DD42E2B624182CF8CB3A35592EBDC77AB218 + AE264B62EA1D9ED7DB8D792E4D5AE1ED8F5BE0ED262D39E16D85F7D8BE4EDBCE + A8236B60AA4EA76EA8DBA5073EECD6DB40DE0F7BF645DD6EBD508765465D1547 + B8371ACA6298753EECDA138D7BF4C3C77221DD7300EA77E9833A1D7AA1563BC5 + 25564CE27EECB3B7713D5D9FFB1BF5198AC6B208EE33040D7A0F44DDEE7D2C75 + E3E4BD530F7CD0B5373EE076FDDEAC3B60089A0E198596A3C6A3EDC469267E70 + AB71721B3DDDA8D5F869683A7C02EA73A150BBF760D4EE3908B5BB0FC087BD06 + A3F9C8F168C7854F5B2E44DACC988D1654B359F3D0CE75053AAE5C8BD6CB57A3 + CB0677F4DCB2157DBD7D302C20081323633026380CFDBCB6A1ABDB26B45FB91A + 1D96AD40CF35AB31C8C31D43BC3CD0DF7D137A6D70433F0F2EF00202B9088FC4 + 8488288C8F88C1A498444C884AC28890380C0F89C7D0E0380C0C88C59090640C + 8FC8C4B0F0348C8949C594D454CC4C4FC5D4C4048C090B613FFE5C68FB7361BD + 1D63B8681CEFEB8DE11E5C586E71E33617D43BB670A1B90923776EE6F8B6625A + BC3F666744604E6604E6325D98136D62022F2A8A370077667A38A6A785C22533 + 1C0B72A230273504D3E3FD302BD11F73995F90118E458A019CA718B791585CCC + 7C710CE628FE6DB680AA2C70D95FA1DC305B317E8DDB670153631D1B8145AC6F + 0160C598D58B1959C7B24CC0B64CF034CAC4E87531F1BA42D84F1496950916A7 + 60EDE14CB81DCB3670D2B83C1624DD9F06C50E133C76DD1D87A5EC6FB1AC804B + A278EC082C601F8AE5B5242FDCB8735B942378CD7E15BF98E533D37619AB011D + 731ECB16154663615134166B8CB23ADE9384950732B0E64026C79C682C0AE616 + 09F64673CCF190B5EE62591F4BB26260AA175CAE7B2CAB5D81E395FB057AB38C + 561DC9C6AA63B9587194DB87B3B056AE940FA5B38E00B72C735398CAAD33CB1C + 107735EBD95A7548603813CB057D951EC8629A81A5FBD20C0836E0587057F5A4 + 8382C0EA83D7ED680ED61DCB33E9EAC33958C3EBA8BCAEA7AD754773798DF3E1 + 765C525E5038179B14CB5756BBA70BB0E184AC7FB38D369D14006699EA30EF79 + AE045B2FEE86E7F9126C395364B4F97421369ECCC586D379703F5B802DE70A59 + 9F633894CA7126B3FF0C6C399DCB7A39E605A65E3A6E389202F7A369F03A9D8D + 9D170BB0EBB2606D1EB69EC880C7D1546C9345EFD93C6CA7046CFD65957BA918 + 5E27B2E07E24158A09AC38C1B2FAB5E1AFF7C91CA3ED8A2FEC900D857D2F1460 + DBB95C6365E373B1C8C0DF5DD7CB8DC58EB1DAB9AA187E65D87965377C789C6D + 723DCDF16C73C067FF2B250602070AB05E2E46C8A542845D2942C4F532E3BA32 + F47201822EE418501B7A391F6197F2114E09E82A1E70E4957C049E49C3F6C371 + F03D9A80A0B319083E93895D2752B1EB642A761E4F86FFA97484B07EE0B96CEC + 3A9361D260F625D79741EC3F8CC789BAB3CF58E1CADDA5496FED4508C714C6B1 + 8772EC41974A0C00D64BF0B847C710FFF8B891F282BCB206B625282C502C182C + 4B61E31EFAF109CBFA571098DBB20E4BFFE22C750669CF056A4EB0AEEA1F62BD + 2388B9BB1FC90F8FA2F8DBAB287C7909E9DC2FEB305989653E398EDCCFE416FA + 04D25927F3C951E47C7E1C599F0B2E1C623DCBCD75FAA383C8797A0C594F0E23 + FDC17EA4DE2E43F08914ECD81F61E0AF47C14EAC4ADE8CC04351287EB80FE59F + 1D41C99303287E5A8E92CFF6A2E4D37DC87DB01BB90FF7A0F0D901E43E2A47FA + DD1264DC2F43EED343C8FFECB889095C2297D0DF5EA2AE70AC1751F8B5E274CA + 65EA598EEB1C0A5E5C44F1CB2B28FDE61A8ABF161C3E8F9CCF4E23EBD949E43E + 3F6B2C0AE51A56C045E02593FB929F9C4032AF95812A9FF13AC91DED0BCB2A51 + 12FC9504AA4C4C4D1E278D752CD7D082C05791F1D535647C790D5902B7DC4E15 + 2895D5A720E00BB998BEC17D8A3D7CCBC41E4E7FA1FA370C3C36F18899CA0AD7 + 8058415841BCCF65956959030BC8CAA233FEB37306D01ACB60C1BD1737B85F30 + F126B76F225550914AE6B6AC49935FDE46FAB7778D54962010A9BA5FDF3652DE + 02C202CD828F82CD722D7DD9B8998E57FC57B689E77812388E248E5116AB69DF + DC42DAD7B74C1F4A33BFBD83ACEFEE23FBDB7BCCDF45C6777790FD0B815F967D + 771799DFDC461695FB8BBBC61AB85496B5DF3F42D9F78FB1D7B8597E82837F78 + 8A437F7886A3FFF2194EFCF1394EFEF1054EFEDB0B9CFA8F2F71F6DF159FF74B + 5CFC8FAF1C317BBFC615DBB2F77F49DFB2EC1B5CF8F7AF71FE8F5FE1EC1FBFC4 + 997F552C5FA6FFF6A571EF2CB7CE47FFE5731C61FE10F71DF8E7E7D8F7874F51 + FE7B59FB3EC3EEDF3E45C96F1EA3E8D78F50F8CB0728E0D8F37F790F793A0F9E + 9FE2F61AF7CDDFF0EFF6B5240B68FE3D795EBA7629BC36B2E2D6DFCCC4549678 + DDA4E4E757AD18CF94E57A59D75830FEBA81C069BC5E49FC6D580058717F1503 + D88A032C082C37D072072DD9D6E4EACB8A0B2C77CED6070482F1E95FB34FE68D + 4B72F6AFDF883E42B0DBC98DB4A06FFC33FE9E9E9E43E2B3734896A52F7FD789 + 4FCFF0BEC07BC8D353FC8D9FE76FDBFACDEB3E922CF8280BDFCF2F2245E0F8E9 + 69ABAE62967F2AF87B86F71659A2F2372A48F9F4AC71A99C2F68AA8F2A147756 + F057AEE43F9705B0402A7FEFFC6DCBC2581F612846ADF53F78DE580FCBF2358B + FFAF96F87F499978C1FC5FD4071C02AFC68A97FFEF72D56CB96B161C9665B0ED + A659E0F69A895B2DD06BAC7D9DE06FD12F6FA3F0BB5BEC4360F89681C0DAAFB6 + 8AF36BAC7DBF65FDEF6E2297E5593C860D80754F9095B481D8BA6F707C16BC56 + 4CE353263671C633DE3B9F1E77E804B2E44581F7D41CA353C6AB82E215CBCAD9 + C42BFEE23CF279FEB9EC2387D73253B05C00597F13FE1D14273E496EFD1F1FE5 + 353E6E5C38A752DA4EE43D3D99F7F4D4274778ACA36CC33A02B877F618E09B24 + 8B5FA60A51107F6B0FA2AF97F099C3E7E0B52244DD2AADB0FC15C48DBA5D4E31 + 7FA79CCF2959E5EEA7F8BC62DE8ECB1B21B87B7B2F426F962398CFD1B05B565D + 590B4B7AD609F846B3ADAC8765D92BD01BF5E01022F98C91556F28B7C365F1CB + E75DC86D015B3E0F591672E720F3FB11200B5FA6AF6B1F026EEC85BFC4394080 + 293B60EDBBA5546D0F22F0C60104C8FDB3AC806F709B79C15E59F1EE548CDF4B + BBB1ED42B101BA3B2EEBA331CE1D2E965458FE6E970B686D535B3927DACAB988 + F2BEACB79DCF692F9679727E63BB7E3696BF9CDF6C3C9C824D47D2B04560F758 + 3A36531E9C2B296C86E7F12C789DCC86C7896C96676133E7999B8F6460E3FE64 + 037B3771CEB559209872675E10D8FD70AA912C82371C4CC6FA0389587F3089E2 + DCEC4012D6C81DF49E58ACA45697C761456904E7D20E37C2A9DB31256E0BA6C4 + 7A6066E236CCCB08E0BE702C2D8EC532CE9F9739BCFFE8634A5759FAB2CC58E0 + EEA1CA048365152C4BE0042C944A147A4596C05A0BC8130FE7D4C6625896C2DC + CFB9BD4291AC39A858C3E958C0F5C1BC02CB4278414194F531298FB5B02C050B + 990AE0EAC3CD85BB93D9BF05806575AC8F31AD79FA2BEB64135A8573760B5427 + F338ECA72491F3FD38CCD3479D1CCB7CCEF9E7F2DCE6F2B87364852C6BE402AE + 7D4A2231373F1433B302302B9B6B97DC30CCC90BC34CC523CEF4C7AC1CAE2F0A + C2B0204F56CA5E5CC36DC1AC546FCCE7355C24F85BC8F509FBD11A45DE8AB45E + 599C1BC2BCD62E81589AE38F65B97E5891E78F15F98AC9BB0BAB7377C18DE5AB + 327CB138D90BAEC95BB132D317CBB37660518A1766C56DC4CC5877B8C46D864B + EC664C8FDA80C961EB3029743DA686CB32772DC6EC588D09FEEB303180E5C16E + 981EB10953433762CCAE5518E6B30CC37D9763E8F6E518E4BD0403B72FC3609F + 5518E2BB1623FD376074A03BC6057B509E5C8B7A182BD849E13E261CD1D4F06D + 9816B20533C23C8DFBE7F101EE9810B819A3FC3660D44E1E5BA0D79FDB019B31 + 2E749BD198A0AD1815E88991BB3C3062D7160C532CE01D1B3082F5C6047B73BF + 37CBB89EF65E8F913EEE18B97D13867AADC310CF3518E1ED86E15BDDB8E655DC + 656F0830CBD5745F7757F4DFE88AC1EECB317CCB0A0CD9B48C5A8E71BEEB313D + 847F078E63ACB1245E89415B5661B0C75A0CF65CC775FB1A0CDDBA01E3FCB762 + 6E3CFF9689FC5BA7F0EF9C1E83B9C9E19810E6C3B17B629A6218A78462469C3F + CBB6196BE609B284E6351917E861AC8005BAE5167A228F359EE735723B8FB779 + 35FAB92D45CF558BD0638D2B7AB92D47AF752B58B60A7DD6AE40B7658BD165E9 + 424BAE0B0DE0EDBC64013A2E9C672C7E3B31EDBC502E9EE732BF00DD162F4657 + A69D14FB77B60BDACC9C89363366A0F5F419683BDD051D67CE43EB2902B053D0 + 7CE234B49A3A93FB6619F0DB7CCA7403835B4E557D59FDCE44F3C9D3D162D234 + 6A3A9A8E9F8CC6A327A0D1F0B168386C1C1A0F9F8026B2FA1D35D9A446CC1B30 + 3C6E9A91DC41CB7D7353A62D27CD643FB3D074C274341EC336E3A6720C0E6B5F + AAE9A41968316D36C73297C79F8B96935D4C5DC5096E3472229A8F9F89B693E7 + A2F99819683474023E1C380EF5078C41BDFEA32B00B015FB5796C0434D0CE00F + FB8FC0870346A21EF31FF41C8C3ADDF5CE681035101F741B803A5DFAA276973E + 46753A33DFA9376A7590F56F77546BD315EFB5E986F7DB76377AAFB5AC7EE5FA + B933AA37EF8C1A2DBAE0FD965DA92EA8D1BC9381BB06FE366B871A4D057EDBE0 + 9D46AD29856DD3BB38960BFC325F4DB18099DA529DB71BCA02F813BC59BF9905 + 6C8D3B6801DF66AFE9A7759B5A00B7B6E5B6F9CD3A4D9C646DFFFC83A6F8992C + 7F3FF8986A8AB7D8EE2D19ADC802B84E23FCB476432709FE520E4319BD07956C + D06BC0AEAC7CF54E55EF48B5DF947D68BD5B75A80202EB1DAC31067A057C2BAB + 32E0B5E55CFE27FBAB00BE3FA4CA80B8B2CCBB6BBD1737EFC75F7F5FEEAC8A77 + E9F67BEFCA326DAA2A777002275660C991777A77EFACAAD885ADAACA7E4876DD + AADA54DEF78F52651655957E8C6939EF37AA8A15DA657FAB9CFB73EEFF07F483 + 63FC337AA38A32A3CA63F93155D5C7DF24F6E90C6AFF51FA1F00FCF7917D9C1F + D32B884BFD84ED0473EDD409F0DAE0F735006CD7B1D32AFEC98D047DB5FF1F08 + 80AB2AAB4A763DE7FEECF29FF12122085CB9CD0FE9D54DBB0A396EEA55B5FB5B + 54719E3A07E7078AB3AA38EE4FDE769655EF8D77249EAF202C1F789645AEE321 + 58CD52C543D309CA56C0573D3C9DEB68DBE9215A950CF4AD38BE75DC37F910B7 + A4E3BF7EACCA00D83EAEE49CFF49354E044C5BD6E104E367EF71D241D9EEA315 + 4B42130F6BBFB5EF67EFB18C521D4D50DEE6C4E51D4E7EDED244C5D1DE765F22 + D95FB5D9F0D576192DE82B285B4DC09593280161B98AD684490058D6B982C0B6 + 7B6641D79ACDDAA2E6C7ADF15EC316A82658EB68674364BB7FDBF25879B39FC7 + D13124038255D7B99C63507F4AB55FFD4ADAFF96C6E128ABDEA0396A34B6DC47 + BFDBB885B14A36AE9F3F696BF2668CDCAED3AAA39180B0B695AA8EF6CB82B87A + 9356C67258B187CD36CBD59FA46DC1E27AEDBAA25E876EA8DB9E69A71E68D8A3 + 2F3EEED51F4D7A0F44835EFDF0618F3EA84FC94DB49D36EF3FD8E863EEFFA84B + 2F7C2097D21DBAA3767BA682BF9C70D7E4E45B13F19AED7BE0FDF6DCD7A99781 + C175641DAC897BE79E16F4554CE14E3D5153718CA93ADCAED3957D76ED8D7ADD + 071817D28DF43568BF11683C68349A0E1D8746034751A3D180E5F57A0F41DD9E + 54AFC1A8DDBD3FEA7311D1641417485C707CC2054B732E469ACE9C8B46B3B8F8 + 58BC0CAD57ACC5272B57A2E5BAB5E8BECD1BFD0376614464186665A561417E0E + A626256010CB3AAE5F8F8EAB57A2DFC60D5CA872E1BA7307866DF342FF2D9B30 + C8CB830B603F8C0F0EE5C2390C134223313E320613221331313683CAC4C8D024 + 8C0C4BC18484024C492BC5B4CCDD98935F86F9F98598959681F111D15C7C0663 + 6C20E51F80A11E9E5CE06EC6882D1E18B2613D86B9F3B89EEE18E8BE0EBD37AC + 42EF4DAB31C86723C673D13A352E00D31202312B49AE9C23313F2706B3322330 + 252988C7F6C7F8989D9810EF8749B17E5CC86EC394981D06042F609D85995158 + 9C1F8345729596176180AE5ED2CCC98FC68CF41028FEEFECAC70CCCA08C1CCF4 + 5093BA6486727F045C72C230335B6ED1E4324D2F4EC2B164771C16EF8EC5C2D2 + 18B89627C0756F2216717B514124161746C1B52406CBF6C463B9F695C541316D + 155B56EE88054C57CBD2757F121697466349592C96B19EEA2E2E8EC4929230B6 + 0F372F5716F2D8CB8AD8575134F3A1989F116C2C99E7A407627AF24EB86404F2 + EF274B639E9FDC4217C7F2F8315820C8BB3B1EB2C45DB32F0D4BCB12B190DB8B + 4C5C5EA5F158B247D097E3535C5DB9BEA396C98AF75026561CC8C0328159A62B + 8F70DC4773B0E6441ED69E2AC09A534C4FE4602DCF67CDE10CAC3B2AC09BCEB6 + C958CEF35ACE3E2CEB5F815C87F52EFB58C37A6B8E6463C5E12C2C3F92831594 + D2E5DCB76C7FBAB16C9085F1CA4359587538876D72D92617EB8EE763FDC9426C + 38558475C7F2B1F6681EDC8E1752058E6D8AE9FA132AA34E1460C3E9026C3A43 + 31753F5B08F77385D84C6D3A936F40F0469E83FBE97CEE67BBE339D878320F5B + CE1519F8AB7A9BD97633DB29553FEEE78BB099525F6E3AF7A3993C5626369ECE + 66BD1CD6A314CBEE54B6896727F786DE02AC970A8C7CCFE7C2EB54063C8EA71A + D7CF157280E0AD02C84753B1F150B281C0C62534C72A791DCF322F3D8D35F189 + 6C6C3BC57694E0AFDFA512F85ED48B5AC5D72B82EFE512F8DF2887DFF5BDE6E5 + ADEF9532ECB8BAC7BC98D53E1387EF7C81B100F6B958C8B1151B006D00F0D512 + 045F2E4288E0EE953C845D9535703E02CE67535908BCE0D0F94C049C4987DFC9 + 14EC3C916CB4FD481CB61E88C2F6A3F10838CDF2E3492C4BE0BE14049ECD44C8 + F91C04537EA733E07726134117F310A4BE796D82AE94182BA9D01B7B8CF554C4 + 2D594FED43A42CA1E4CAF9E63E8EA59C63927BCC7D88B87BD0406029FAC111C4 + 3C3C8A1807F4B52D81954A8A032C17D0B675B02D416141958C17E70C044EF9EC + 14E21F1F45ECC3C34C058199BF7F08898F8E19EB3AB95095855AFAB353C65DA8 + E085C044FAA32388E6B58BB9568AC4078A65BC0FF1F7F652E546C9548AAC81EF + ED41D29D3224DD2842DCE55CA4DF2A42D6ED127814FA616EE8526CC9F745FAF5 + 7C143E2847FEFD3214C8CA97CA7FB407F94FF6A1F48BA328FFEA048A9E1D44E6 + BD52A4DF2D3696C0394FF723E3A162309722E14E0992789CD4C70790FDFC18F2 + 5F9EE6B8054FCE1837D165DF5C46C9CB8B28F8F20C144734EDE141A4DCDF8FAC + A74791F7F949E47C7A1CD94F8F9954F18E1379FE09BCC6022B8A8D99CEF33720 + E785AC7D65116C816001AAA4A716EC3280EAF905A47D6EB9BC15149345709660 + B00020B7D39E5F641D59563A2C85A98C17D7D8E68AB1A04C931B5D6E67BFB8C1 + 76378CA568EA57ACF3F50D243FBF8CB8A7E710FFECBC71FB2CC57D7A1EF19F9D + 651DB67D69C57F953568BAA032258B62591ACBBA58D6C106000B087F7DC728E5 + A580AF058553B4ED90B11C3690F0BAB1544DE0B8623EBF540180E53E3A5196C2 + 2F39AE6FD8F7B76CFF8DD587FA4DFBE62E750FE9DFDD471A95FEADF2F7904965 + FDE23EB20485BFB983F4AF6F23936D737E7117F9BFBA8FA25F3F44E96F1EA3EC + FBA7D8F3FBA728FFFD13EC1508FEA76738FCAF9FE3C8BF3EC7E17F798EA37FFC + C2C0E053D4D97F130CB62D7BBFC6F9FFF81AE7FEED25CB5FE2DCBF7F6DD253FF + F6154EB0EEF17FF90227A8E3FFFA058EFEF3E738FC874F71F00FCFB0EF9F3FC3 + BE7FFA9CC7FA147B7EF714A5BF7DC27108FE3E45D16F9EA0E0D78F90FBCB07C8 + D6F8796ED9DFDE356916CF2193E790FEB5AC7D6F98EBAC6B9FF20D65AEB72C78 + F977D107005FE96F690160C587B6A1AFA0ADDC37EB5AEBDAC6516A9BF1CB7BE6 + 9AA61AC0CBFE5EA8EF576EA0151338F7970F792DEF725B1F08F0EF2180CC3E25 + E332FCA5F5F140DA4BEBF88AD79CCABFAB6D71ACDF866575CE6D7D50F0E41C12 + 9E9C41F2A7E790CCDF55127FF782BA494F4E21F9D969F3A1832C739537F7997B + 478CA56F327F87296C93C87AF6FF83A0ADF22A4B7926F8CB7E1EB39F27FAC082 + 7D190BD63316FCE5FF915CB567F377A58F276CA02C089CFC39EB192B7CEB7F4E + 71738DBB63FD2F3ACA94DA00D6C0DD6FAE2387A9299375AE03D01A30FCAD2C7A + AD6D815C6D4B16E0BD8E82EF14B3F9A6A99BC36B27009CC7BFA755C702C0DA27 + 009CF71DFBFA46EEBDE521C031168ECD8E616C5B319B7BED6772C77C028AB39E + 49657D761C99BCD7285EBA5CE7CB65BEBC2664F0DE24C96B42F633EB9E94F7E9 + 2914F06F91FFFC0CF2982A067BB62C879FB03FDEAFE5C23985EDE5BA594A7EA4 + 0F710E2096F7DD98DBBB91C87B7112EFCD92EE7B897714BE600FD2584FC74EB8 + A970057908554CDEB3590838976979A7B8A910058A032CF8AB58BF72D5BC1B11 + 82C10E001C7E47CF25C1DC83E619127DFFA0D996B5AE5C3647F13E1AFB401F24 + F19975570078BF691B7D9F79D635B1796FED4338F785B10F299C75231F1E617A + 18C1EC2380CFC600D609BEA3ED4308BA75D082BA9440AFB3FC6FECC32ECE01EC + D4EFDA3ECE09F6715BE0F700765DE5F615B977DEEBC8EFB1E60B82BD0EAB5E81 + 5D6FCE3194170CF63A5B80AD9C0B29CEAF5D26D9F5EC6DBBCC8BF31B69EB59CD + 7D382F924B6759F152B2E8F5E4B6BCA3781D4D8737E7383E9C93499E2CDB7428 + 0D1BF6A7C04D6E998BA3B19273DB35BB63B89D804D9C67CA12D858031F4C66BD + 44ACDB97F04A7B158283736ECEAB579446617949A44957325D5A108245D9BBB8 + 26F0C3FC743FAE0F7C31376D07E7D4015892178A65FA68B388F3F1E278B84A45 + 719C8BC71BEF3F0A172358AB78BEF2FA63855A9145708A056E8B058305856521 + ACF02876BC60AA3419F3F44129DBC8AB8C3BE7B296C79D04CEBB39F7DD936AE6 + BFAE7B5339EFD65C3A9DFD0AE272BEBE2785FBD21DFDBD8A436C00308F2B294E + B109D3A231F3786A27603C4FC79095B0C382586EA48DDB68AE1116723D64C2EA + F07CB4269AA378C53911989F67B993960723AD83E6668761614114E6E4EEC2A4 + 640F8C8F77C75459EA66FB735D176A2C9A15EA46E16F96F2FAACD307A0FCBB2D + D187AC05DC9F1D8079E9BE5890BE1DF3337DB030CB074B99AECDF1C3C6A210B8 + 1507636D813F96656CC782C42D98CBFE67C66CA0366266F4464C0B77C3746A66 + E4464C0FDD88A9C11B3061C71A8CF55E89B1DB5661BCEF6A53E612ED89E9119B + 3176D76A0CF55D86113B5662E4CE3518BE733546F8AFC3F05D6E18B47D0D46F9 + B39FD89D9811B70B13C2043CBD3032600BD7988A771C801951BE981AEA8929A1 + 1E1823EB5FBF8D9810E4C1F5EA668C09A0023D30CC6F1386EEDC88D141DE1817 + EAC3742BB7B98EDDE585F121DB0D4C9505F0703F77F6BF9D6B665F63213B74EB + 460CF7DA80215B3896CD6BB9CE5D83218AC9EBB116C3B9CE9D10B895C7DC8201 + 9EAB30C06325EB73FC5E9427E5B11AC3A8D1AC37957D4E09DE8611DB37B0DE2A + 0396476CE7F1798C31FE1C53D076B8C487C16D773696E5A7605E1AFF46195C6B + 66C7F16F11868991DB312EDC1B13C2B7616CE82B97D5638279CED44496C9E257 + E73C64EB6A0CE658867B728C9B57A3FF1A57F4709D8B6E8BE6A0CBC2D9E8347F + 363ACE9B8DAE0BE7A1E38239683D77265ACD9E8E364CDBCC99655C3DB79F3F07 + 6D5C583E631ADA3195856F5B1789EDE7CC4387D973D176A6E2F6CE40AB69B2EE + 9515AFDC290BC44E366A3266129A8D9B8C5693A61930DC6AFA2CD6998A26C632 + 788A01C1CDB9DD7C223581DBE327E3E3D1E3D168E4584BC3C71B002C351C3ACE + A8C190B1F868F018341C32CEB883563CE04F465956C2AD2672EC5366A3E5C499 + C64AD8C0DFC9966B6981DF061C53C3F172473D0B2DA7F3BC0581A7CEE1186656 + B88AFE64EC74F635038D864D34C0B75EBF3126DEAF64C06FEFE1A82B0BE03EC3 + 51AFF750D4ED35041FF41C54217DDCFF7E97BEA82577CF9DFB42317FDF6BDF03 + 353BF4429DCE7DF00155A7532FBCDF4E2E9FBB529DF13ED39AAD2DBD27F8DBBC + A3B1F29575AF606F0D8160230B00576FD29EE5B2F07580DF862DF10EF52EF3D5 + 3F6E836A82BD8E3253EED03B0D5AE06DEAAD0F3FC1CFEB3733E9DB1F35C7CF65 + B12B57CD72D9EC70DF6CBB707EE383C64C658CF2A7FA599D8F8D7EFE411328AE + AF8C569C3D1F9AF7974E32B057A9DE690A0AD76C6062FF2A9CDECFF43E957901 + DEB76A3534B2DF99DAEF5795CAFAD786BF36E4754EEDBCB3E7C90AB8EB905D5E + 5966BFF37B66FBBDB053DEB9ECC7A47AE67DB8D3FB6AFB9DB5B39CF755D69FAD + 57052BA8C8EB1DBDDEF5FFDC7AD76FDEF73BDEE3DB79677661ABAAB21F925DB7 + AA3695F755A5BFB4DE9F53553CAAB29CD99F5D6673B4CACCCBA82A566897FDAD + 72EECFB9FF1F50C53954B1EF87F4A3E7F597CA6EF3637D38EFFF313983DA7F94 + FE6F05C0955555BDBFA7AA3A66553260D6405CFE5309F43A5401751D7A0DFC9A + FA3F50A78A7F7423D5F93BC05FE71BD57F457FAECFCA37DB1FD36B37F1CA72DC + D4AB6AF7E7F4D39F555D6EEB4DA78742C5C3A4B27EE0B8AF8D8D727E4049CE00 + F8B507A66030CB6CE06AC3D9CADB951FA015EDED72039C5F3FAE7D4CE7FACEB2 + FBFE4100ACB21AACA389452569C26103E09FBDCF890A2724D6B6DAA88EB5CF00 + 62E60581DFE204471319E77E2A26330E002C8B601BCE6AC2A454D05600561058 + 796B1F274202B975250BBECA4AF7AD06CD50AD610BD41084A55E03C0755E815F + F3A59D626A306F83664960D706C0362CB640B005842BCA29BB8DEAFCACB6EDBA + BA31EB70BC1AEB873CEE474D2DEB64C5096EDC8213D3E64682BBC65AD9017D0D + 24665A19F20A061B4B62C1600718D6BEEA4D5A1A2BE27AEDBAE0C38EDDF161A7 + EEA8DFA5271AF6E86300B0D4A8675F63EDDBB0573F34EAD31F0D7A5B69EBC1C3 + D166C8087CD27710EA77E224BC55471363B85ACB769C747741CDB6DDCC17978A + AD52BD1527D8FA1A53650E6992FE7E7BA654AD0EDD5147E0B86D57D461BE7E97 + DEA8A72F3A05853BF4446D4EF86B77EE675CFDD4E7A2E0A33EC350AFD7607CD8 + 7B887105A42F428D3B207D21DA63203EEA371ACD46CAA591DC14E9CBD459F878 + 8A0B1A4C9D85A6F316A3E5921568BE7439DAAD75C340BF9D18111A8C11214198 + 1C1F8305D91958989389E909715C206F45DF8D6E18E1E58171BE5C20FAF960DC + 8E6D18BE75B389EB33CAC71B138302302924840B447D4D1C8E69718958945388 + B969F9181912839161099898908D6929855854B81F4B4BF763515E91A93764A7 + 3F066EDD8E113E1C03D3FE6E1BD18F631AC474909B1B866EDA80A1EE1BD1D76D + 35BAAD59866EEB96630017CC63B9A09D10E68BB15C5C4F08DF8119F1C170498D + C0F4E4304C89E779C4046042B41F46857963D02E2ECEB9689FCC85FCBCB4302C + CA8AC2C24C2E80B3A3388E6813276B728A3F66329D9B138979B218CE8EE47883 + A860CC480BC194A4000386CDD7FC45B19895138EE919DC97198CD9B9A158BC3B + 0ECBF6253BE06992C9CB0276EDFE34ACDE9B82157B92B07C4F229695F1DA94C4 + 1AAB61B97A567CDC15FBB99F5AB64F5FEFCB7D743CB713B1A23C0E4BB9ED2A2B + 85E270B81645582FA3A825F951989F1582793CFE42B9B1CE09E3B8434DBCE015 + 3CCE0A5912CB0D5D618C7107AD9731724BA763AEDCCB71F2F88BF708542719D7 + D5B20E5E22CB63B593E5EE4187D5EEE174AC3B966DB4E64826D61CCDC4DAE3CC + 9F60D9C95CB89D2E80DB9902AC3F9DC73A59587B2403EBB58FF5561DE2B91F96 + 8B6659EEAA2FA6D42A590B1FCBC13AC5DE3D9E8B7527F2B0E66401D64AA70AD9 + 77018F9F695C4CAF64AABA6EACBBFE84A06E3E36CA4DF399226C3A5D6460B02C + 7C379E2AC6869345CC1798D4E3FC1E785DDC0BF733A5DCB6EACB6277D3A93C03 + 79DDCFE663CBF922AAD8805D950902DBE946D5918BE773C516301600E6B97A9C + 63DF178AD98EFB14D38EFDB8F3DC15FF770BFBF7BCC03AE7F3E17136179EE714 + CF2E1F9EE6C5678EB1B2DD7E3E0FDBCE317F361B1E27D3B1E5842C5D32E17522 + 13DB4ECBA237175B4F66559479CA0DE2D134B81F4EC1D61359F038CAEBCBDFC9 + 86FD49262FCB5FCB32B800BB2E5AF18203AFEF41E04D59FCC872A7183E178B8D + 858EE2F6E9C5AEB1E6B95864AC7F7D2F5BB078C7A512ECBC520AC50A0EB8BA1B + FE57D59780AFAC7F0B107EB5C848D6BF91D78B117DB304219773B1E34C0A7C4E + 24C2F75822B61F8B87CFF104F89D4E81FFD974EC3A938E80B31908399F65AC80 + 779D4AE37616422EE623E87C2E76F2FCBD8E24C2FB680A76F1DA04F258FE170B + 11C0F18672ECE13C075B51771453F1900575EF32BDB99FE5FB107EC782BF82C0 + F6CB75E5A31C16BF36DCB501B0645B032BB52D84E31E5AF183653D9CF0446D8E + 21E6810032EB3C12D0396A1473FF2062EF1D342038E3D333C816D090359E6083 + F6DFDCC3F32D4030157AB510E1B74A0D2488515CE4BB7B107F7BB7898B1C7F6B + 3712EFEC462AB7B31EECC3EEE7C751F4EC10FC0E446169C27A7814F921FE5226 + 526FE423FD66017264E1FB702F32EFEE46FA9D1264DE2B43CEC372E43C2A47D6 + C33DC87CB40739CFF621F34939E2583FE86C2A3CF68562CBBE30FEDDF238B6FD + 1CE7610382B33E3B66ACEA0A044E3E3F85822FCE9898C41906B41C32C025FDE1 + 41A43D3880F4070791F5442EAE8F2195E58A799CFAE438D23F3D65ACF7B25F5C + 805CCB3A431EE3269AD2B60D952C007C1EA94C057E25415F59350A0C2B9FF5D5 + 35E47C2DC87BCDC0E0B4CFB5EF22923F154066DF9F5F45C60B8125B9939665F0 + 75A47D61590027B08E248BCD84E7178D25700A8F6B5CFD3EBF6A015F01608149 + 4160C7B660AB80AF00B0525B82BFC902B7DFDCAD90CA8CBB6803816F20F18B6B + 8865DF718A1BEBB052958570B2819DB791FAED2B786C2030FBB053490038E3BB + FB46E9DFDD45D677F70C4C3563FCFA16B2BEB52C810B0C047E8092DF3EC6EEEF + 9F186B60A9FCFBA7C63257B178F7333DF42FCF714C40F75F5FE0E4BF7E89537F + 7C8933FFFEB5A57FFB1AA75876F25FBEC0E93F72DFBF7D89137FB480EF1127C9 + B2F8E0EFD8EF6F9F18E8BBFB770EAB5FAA84C72EF9CD1314FEFA11F27FFD10F9 + BFB264815F59FEDA10580058F0F736D2BE147097C5B5AE27AF0D95C27D294A65 + 9D4DD9005830DF8AEDACBFE915C81257D742103741805D40F89B5BBC76FA1B0A + 00CB0AF826EBF078BC9E69FC1BA6533ABEDA09000B405BB1802D182CD7DD96F5 + AF8E6B0161CB0D39EB7CC1FDAC6303E074FECE5205A59F9D45A2602D7FEFA994 + AC7EE31F9D40FCE393060227715B1E058CB48FF792B8872790CC7602C002BE36 + 0096CCF693D348E57EC1DFB8FBC78C0480E51E39DB48F16F15B3F72A1403581F + 4DA4BDE0FF80FE77589ECEFFB9AC97FCDFD2FF17B71523D8805F636D6FC5E135 + A097FB057425FB7FD10063C73EA506DC52CE0058D0D782BD563B7BFB559F561B + ED57FC5FC50796B23526870480D3BE10B0B6EE05BA4FE88310E319C048405C20 + FC04D29EF19EF28CF7542A55A0F6FE3EC49B78F2E5BC575A31E5E36F962189DB + 2977AD78EAC977F621F5DE01DEA70E2183F75E81DF8CC7ECE7E151A4F23E9FC2 + FB76925CECDF976BE67DC69D733CDB47DD2C753CC3786F661A76F9551803C5B3 + 8F535C5EE6057DFD4EA422F04CA6F95849610F4215AF5EE31100BEA7E792E5E6 + D978A9E0716279DF8CE5F8E5BA3994CF0259FA46316F9E251C57249F278A596F + 9E2B2A93A5B040AF43917CB6C8EA378275C2D82EF8F63EE3CE39E2019F5B7CC6 + 44B28FB0FB8710C4679D0070E02DD6B97B18C17C9649817C1E06DE3E88805B96 + 02A9A05B875897DB370FC0FFC67EECBC5ACE677E19B65D28C5F68BB2E4554C5F + C5EEDD63E47B694F05B815D81508562AA8EB2C01602FCE8B6CD8AB3ACEF5B4DF + 531FAC19A9AEC06F0EB6706EA38FD714FA42D0D7A447D34D788B1D9C4F79739E + E3758CF31FCA83F564EDBB8EF3EA359CB3AEE13C7BCDEE58AC2A8DC28AE2482C + E79C585A551A6D24B8BBA420840A669EFBB8BDAC98F366A37087C2B89F73F6BC + 60B8E687604551389617866125EB18B1CDCA9268F61F6D3EB45C5A4871CEAD0F + 29F5C1A8E6D40B38FF9EC739B5DC3A5B6E9E9597F71FB9814EC64263096CC15F + C1591BD45AB0D6D2A212D549301F656A9E2B482B2B5DD559B6379DEB87742CE1 + FAC095F3E065FB33CC7CD8ECDBA7F97436F7B3AC3CDD78E611F0352EA8D54751 + 9C91E0EEC282588E371E0B98CEE55A674E1ED703DC5E549C8005DA2F2B62793A + E2F92DE4B92DC813A48DC142A68BF2A3B144E7C4D45A4370CDA17504CB9696C4 + 636E6E8001BF93123D3035C51BB33277717F288F1769AECFDCDC0803CBD7E9FC + F8F75BCC6BBB202710B3D3B85E4BF4C2F4D8CD9811E78E19B19B30236623E6B1 + 9FA5993BB0346B87710F3D2F81FBA3D6615AC41A4C8B5C0797B84D981DAB38C3 + 1B4C2C619728E6C33619083C2D90F2DF8899819B3133D8C3B87E7689F6C20CA6 + 1383D71BD7CFE302D76362C8264C0ADF82E9F1BE98162BCBD74D1817E281C55C + 132ECD8DC5ECA4608E6B17460AEE0679726CBB78FCED981AE289F1EC7F84EF3A + 8CDCB9C1C4FD9D28EB5CB941667EE88E8D182237CE7ECCEF74C7605FAE5F7D36 + 6254A037A646EDC2AC84204C8DD989F1A1DB3021D417E3827D31CCD70343BC36 + 62F0E67518B46935066C5C857EEB5762E0865518B8793506C97A57F272C3309F + 0D3CAE3BD7C91B314C96C25BD660A4D77A8CDEE68E51DB3763DC2E6F8CF5DB6A + E2FB0E60FD3101DE06FE0EF1DEC4B5B2E5E2796E0A7FCF79C998971A8DA9D1FE + 0E8BDF30CCCF88C0148E6D549007C7CEE3F8F3FA70DF14AE97E5BA7AB0CF3AF6 + 254BE50D1CCF4AE356BAE7CA79E8B5723EFA50DD96CC41E7F933D1699E0B3ACF + 754107971968336D0ADACF9A812E0BE71B0B5FC1DE9633A6E29369934DDA76F6 + 2CB4E1FE56D3A7A1C5F4A9683195FB264F3690B7952C78274FC1271326A1C984 + 09684A35A33E1E23783B0E0D475852FEE351DC3F6EA20578274E41B3F1935897 + FD705B69D3B113D16CEC24341F37D9A819B75526182C37D0B2F815F4FD70D0E8 + 0A00FC41BFE1A8DB6F84C9379295F0B009683E761ADA4E9E6D20F027CCCB2AB8 + F5D4D9E8347B11DACF98CF71CF347180A58FC773FFC419C632B805CBE54E5AF5 + 05931B8F98880F078F459DBEC351ABA73CB60DC507BD8719E05BA7D730D4E176 + 5DAA7E9F6114CB1CD0B756B7FEA8D9B59F81BEEF09FACAC2B77D2FE3D6B95AEB + EE78AF6D4FD46CDF1BB5595EBB03F38E774FEFE93D948C105A7731F0B746ABCE + A8F64947BCD3A49D915C3E2BCEAF01BF4D3B1877CEEF3696D56FAB0AB8AB5460 + D702BE56B9B6DF6ED8026F297C5B83E616F8FDA8B981BE06007FD8CC82C0F514 + B757B0B7317E5AB731DEACFB71857E2619EBDE8FF17359FC523FABD5B8426FD6 + 12EC6D88376B3395C743C1DFDA8AED6B59F6DAFA1310AC7792B52C8BDF37DEFB + D00060FB5DABDE9B4AC67BA2DE99BE277D889F56D7BB5B0BFE5676F5AC7DB62A + 3C44EA9DB0FD0E98797987B43D481A0F948E7AAFEA70FB9DD7DF3F3BABA29E43 + 95F7396F3B975706C0CEEFAD2BDE5D3BEDAF2CE7BA15E54EEFDF2BDEC75751A6 + 10936F38B100674E5115BB702E772EFB2139F7F167651BED5562377F551F3FA0 + D73854150CCBA812ACACB28D331B74AAFB27657FAB9CFB73EEFF07F4A3E75685 + 7EB07EE5B1FCBD54D5B17E48CEA0F61FA5FF01C07F1F5575CCAA5401707F0400 + 1B5506C04CDF90EC768E6DEB6621FDE93FFDDFAACA37B2BF567FCF3E5FBB813B + CBF946AE7A551CB3B2EC3E05806DD965CEFA5B00B02DF38591C6E77830FDC943 + 4A7995E901A70762C53653E707A403C6DA90B6F203D49662FA9AF22A00B039AE + F63920B3737FCE79C15FBB3FE7F8C0D69765D604E415A815E8B5CAECFD3FE7A4 + C406C21630B6E0AFC66641607DA5F6CA4D89B66555EC0C807FFE3E273F4A5947 + EEA2058265152C09D60AD2DAF055794DA80C74AD637D5DA7BC996409E872D256 + 0167256EAB8DF9824E6D048F35661E4F32EE554C7F0E6B60D3DED25BEABB0EF7 + 51F63E2B153476D471B892FE39EB98F27A2CAFCF89A1436F7FD4CCB88936AEA2 + 8DBBE84F8CC5B2EDB65A96C2EF346C6EB6DF311353CBA5B4F2D5047B65FDAB18 + C602C3AC63B56B81F73F698D3AAD3AA07EFBAE68D0B9071A76EB858672F14C7D + C4FC475D7BA201F38D7BF54393BE03D0B84F7F34EEDD1F4DFB0E44D33E03D048 + D6C19DBA71A2CD4974F3B6A8DEA22DDE6FD3C9B2E615F4E5C4BB7A8B4E78B745 + 470383E58AA756C79EC63D74ED8E3D4CBDF7DA74462D4ED2EB70E25EB7430F7C + D8B9373EECD20F7515D3A51DEB70A2FF7EBB9E789F93FE5A9DFAE283AEFD2D20 + CC3A7504873BEAEBCF7EF8A05B7FD4E93A00F57B0E47930113D064F044341C3C + 010D874D46839153F0E1A84968A42F5267CE438B398BD075E55AF477D742742B + 866DDB6A59F9FA72C11BCC05636C04A605EFC0143F6FCCE40273FCD60D18E3E1 + 86C93E5B30C1C71D43DD5761D0FAE518B975132607FA607AB83F5C62C3B0382D + 11CB7332B1203D15E3424330C86F2786F8FB634C5804A6C627617A423266C6A5 + 627C50247A6DDC828ECB57A33BC7D17B951BFAAE5C87FEABDDD08F699F65ABD0 + 7BF94AF45AB1023D962D43F715D4EA65E8B7712D867A7361CAC5ED008FF5261D + EDEFCD853D17EB71A19899188E19F12198CC45EEC8606F0C328BF30D181FB28D + 0BF160CC4F8BC49CE450B82485604E3ACF312D0413141F383D148BF213B0765F + 0ED61FCAC79CEC68CC480D3330786646185C7222CD8B16BD9CB1637099985FFA + E25E20D76125EB5AAEF8B8A9C60D9B9B62DEB24C31C456282EAE7117271770F1 + ECCB72C92C30BAFC402ADBC8E5721C9696C7F318942C16985F521A8185852170 + 2D8EC232591914E8A58CE3054D0117F5257158C13E97533AD6EA7DE946AB78FC + E53C866B29FBE4F19695256099E0F05E8E855A220B636AA960B52034C7B0EA50 + BAB1CC550CDDB547047433B1F178AEB1921528F5382FF8598C8D67F2B1EE24EB + 9CC8C19AE3AC7F221B9B4EE761E3C96C6CA0DC8E6562ED51C163B98696A5AF40 + 2EAF0565FA577C5EB65D77DC02C9EB4EB1BF5385703B530CB7D345DC978F0D4E + B077E3E9426C907B665B27B98F6312DCDD7042F1798B39C6526C3E2397CD7BB0 + F5E23E785DD80B8F7365703F53E280BC1CDF29C5EFCDE1761ECFA5089E17AC7D + 2A17B856AA7AB6048D65012C57CF9E3CEF6D9777C39BF2BC5004AF8BC5F0BA54 + 82AD94B6057FBDA8AD976425A397A08E979E67728CD5CB76EEB394CFFDB2B8B5 + AC6EE5EA597057F057F2E6F5DB46E985A8A4FD7A11BAF970AA81BF6BF7C441EE + 0E65F5ABB8BF82BF36000EBC5A86A0EBE508A476725CDBCFE79B787CDE673926 + DB5DE37959EBB0DDE512F85E29C68ECB92E070A9B1FE0D3016C025CC172294FB + 63042BE5C258710EEFED45DAD323487F7614E1D70BB1ED4422BC8EC462DBF104 + F89C4CC2CE332908BC9489D0ABB9C66A38E4721EC2A8D04B3908BE908B209E7B + 20AFC12E5E93EDC7D2B0F5480AB61FCF809F00B0E20F73CC52F82D594F1D36B1 + 7B236F1FB460ADC3BA2E968AB973C858FF4A02C051F70FFF89A229015D815D03 + 7CF5E29DFD28AF7E8D4B68590CEBE53AFBAE28633BB531C058FB1F1D354038F6 + E151F6C13A770E208EFB93E40EF9C929C4B35ED4CDBD88BEB5D7B8C98EBCB687 + F97284DF2E33522CC8983BE50602C7725B402381DB49F7CA91F5E820B21F1D46 + CE934328FCEC18D25816C06BEA7F2C0E3197339124007CB7189977F720F56629 + E2782D834FA6C0EF701C424E2521FE5A3E32EF97410038834ABD5F8A98EBBC96 + 6792B1B93C08EE7B43107DB310199F1EE458F722F97139C75082884B8588BB51 + 86F4878790F3F428321F1F4422C79678AB14A977CB2B94767F1FB2F9F7CE7F7E + 0279CF2D17D119CF8E238BF9DC17E74D9CD12C59F37D6159F7D9126412209285 + 600538125CFAEA2AD3ABC814C87A7E8979B98DBE8ADC97D791F735EBBEBC6600 + 9600B1DC46ABAEAC858D0B6959082B651B81300162C55A95E5A8C0AF2C814D9C + 56D657DC5DC5194E617D0160813D413E67102C2B5129FD1BCB42D7B6D2B521B0 + 2973C05A236EAB3CFD25DBBC64FE2BCB15B1AC85959A36AFD5B780AF25ABCC19 + 061B00FCED7DA47F7D0F696C2BEBD5ECEF6409ACFE35C65BC8FDC57DE4FFEA01 + 0AA8A2DF3C3210B654D6B8DF53BF110C9645B02C759FE3C0BF7C8143FFFC1C87 + FFF90B1CFED72F71EC5FBFC209E98F2F8D8EFDCB0B1C65BDA3DC7F4475FFE973 + 1C104036FACC8064E5F7CBE257B0F7FBC728D2F1BEFF1425BF7B66F282BF391C + 53CE77F7CCD8F27EF9C0A4DAD6B835662B6E332577CC2F6E9B540038E925AF95 + E0AF2C810572956799B1D475807CFDED045EB52D0B60F5A5BF9BE2F7C6C9D25A + 96C26C932688CF766A9BC16D0167D38F60BFA32C431F0AF077A0BC40B0CA653D + 6EC79F36EE9FF5FBE0EFC7763F9DFCFC8A39B6B6E5A2DCC4F3E56F3BF5F939F6 + 7DCEB86FD6C722D1BC07E99EA198E372252F0900A70AF0328D331E052C37E9F6 + 071172812C993C25D7CD82BEF10F8E23E1E109B39DC3DF76AEFE270CFC65DE01 + 600D58FD9AE55F5F473695F552D6B5973936FD0FE8E30B2BAF0F2A24FD7F5860 + F8958CD5ED1792F5C1865D6E015DC7711CB057FFB72AB33FECB061AF057C2D58 + 6CF6B14F815DCB05B45526CB6459FE4AA9CF2D97D9695FE8E30F5EBF4FCFF0BA + 58923574F2A7B286D6F91F46C2838326EEAEE2EDC6F01E19CBE74E1C2530AB38 + F251578A104D45EA23248509E0F324E45C0EC2CEE7725F31EF6772D7BC17097C + 2624F19991706F9FB9EF0AD82A7EBBB613A9389645DFDC8D48F61B76B5102117 + F310C1675DA4F2ECD3FF4C26769E48C38EE329083E9F8D04DD0B1F1D32F17DA3 + EFF03E7F977DA86F4AAE9B057F057C6314B39EF7F284C776DCF903C68D73F4FD + 4388E5F323EE317F377C9E44389E33E6F9260B5F3EAB83F8CC08BDB197DB7CD6 + F17913CDFAE10F0E1B17CFB2FE8D7C780C512A63BF216C2BF06BE9A0057DF90C + 0C64B93FB777DD3C00BF1B9676DDE0B6D2EBB202B6D29D57F762EB39CE5938AF + F23C236B5DC5EA2DC38E2BE59C13C8F2B70C5B3957F0E6DC68DB45B9707EA56D + 9C3F689F80AF20AF71F75C49060C57C05FCB4A781B8FB3FD1C53CEA9F4719BB1 + F6E5BCC647739E5339F0E53C67D785220380BDB86FD38124ACE71C781DE73A6B + 39275EC7B9EBFABDC99CF7586E9F35FF59C7FDCB8B3937CE0DC2FCAC002CC80E + C4A2DC602A088BF3A88260B816866269519881C12B4A23B172771456ECE63CBE + 28DC82BF6CBFA62C06AB4BA3B07677348F178B75E5712C8BC7CAA2582C2D8832 + D23C7B417E04E6CBE34FAEAC622D77C90B4B623997977B67492160128C8BE5F9 + 02B1B2C09545AE0304CB1DB3A4BC2C7CB58E506815B98E76E5BA4079B3CD3586 + 60AFF6CB0AD8DED63E016081E11507322DF0CB32AD339631D5F11709FEE6C760 + 3ED707FA5075B1CA0A0581E3386659F246338D651D2A37C668515E0C96B08D09 + 8993158E45591158CC35CFF28258AC2A49C2F2C278B832BF8CEB1D69797182D1 + E2FC302CC809C29C8C5D7049DB89D9E9FE9897C9BF457608CBE5065A9E8AD886 + C75F901D6AD54BF1E5DACF0B53A336635AE426CC8A76C74C6A4AC47A4CA6A644 + AEC78490D55CABADC4C4D095981AB61A5328C5009E97B8058B52B66161B237E6 + 2778624ED416CC0CDD84E961EE9815EE8945B13BB1262D1CAB14FA27C69BEDDC + 31357C0BA6846FA69846CACDF156B824EEE4798660516618662A2C5042005615 + F1F7B6876B31A68B32A33029C2C758C08E93ABE3404F4C0DF5C6D85D9B306AE7 + 468CF1DF62DC3E4F89F0C5E4C81DC6C5F3605F770CDFE581A13BDCD1C7738DD1 + 10E6C70409F8FA706DBA83F2356BD03181DB307CA7170671DD3C70CB7A0C7277 + C3C04D6B3160C32AF4735B81FE4C076C5A8D7EEEABD193EBEB011EEB3036D0B2 + E21DC1B5F710CF0D18CC7623BCB7704C3E1CCF0E8C0BDE8551013BD0DFCB1D03 + B76DC1E8C01D18C4FD3DD9EFC06D9BB9EDCBE35A56C703BC3760E0F6F518B653 + 6EAB05AFB5BD16C3FD366198EF068CE2F9CC8C09C4B4A81DACBF816BF4A5E8BD + DE15BDD62E44B7E573D165F12CB49B3D19ED5D26A3F39C69E8385B9A811EF3E6 + A1CF8285E83D672EBACD9C851E73E660C02A9ECFAA95E8327F1E5A4E9B822613 + C6A1D9A489CC4F45EB19D3D16ABA5C364F4153C1DE718A916B015D81DA8F478F + 4363A3B1CC8F31E0B6B92C6CC74E41E311E3D16838F78DA0468E4393D113F189 + 40EFF8C96839612A5A4C98E200BE93F009D3D693A61BA9ACE9E8095639FB511C + E046C3C61BD8FBF1F009265F6FC048D4A56C6BE0FA034719B7D02DC64D37AEA2 + A54FC64E45A7190BD06D8E2B3A4D5F80369367A3F984191CFB5463E9DB7814C7 + 3F7222C7C674045387A5B1FA1360AED56B306A751F88DA3D061BB7CE52EDEE96 + EA741F84FABD86E2A3DEC350A75B7FCBD2B7531FBCD7A9B7C3E2B7276AB4EB6E + 2417CFD5646440BDDFA63B6AE9DD529B6EA8D98ADBAD98B6EECE7C37D468D1D9 + C4F8552A002CF06BB97A16F865BE89057E8D3B6787D5AF496D0B6007E015D87D + 4BC6200DAC776C32CCF8F9875233879A1A37CD6FD593AB6659ED524AEB5A4626 + 3236F999526DEB1DE0077AF7F831DEAAD3C4A80200D76C8C376B0AE4BE92DE17 + 4A158627B6F40E927565F16B00B0DE7552E63DEB7B8E907A15EF5A2D09FA1AC9 + 32B83ACB1C317E9D65AC7D0572ABEBFDAB438EF7BB95DF1FDBEF7A2BCBB98EFA + 32EF91FF02D96DEC7CE5FD76F9DF0A809D65EA49361F901C8CA0AA320B0057C1 + 0AF4FEDE91FF93F7FA3F505E95ECBA3F2807F47DC3A1CA00F8EFA22AB8D50FCA + 89FF196EF503FBAA648576D9DF2AE7FE9CFBFF015530B62AF6FDD5AA3C96FF8A + AAEAF7AF9133A8FD47E9FF46002CD93F86BFDB0FE247E47CBC3FA75740D702B9 + 1596BD92BDAFA2CE9F96FD946515D0B8D23E038CABFAC7FF3BA8AA9BDA5FA2AA + FA92AAAAFB637AED06EE2CE71BB9EA5571BCCAD2CDFEA7ACEB0C80B55DF9986F + 3AE0AF54656C014A0F8F8A7EABE8437A6DAC951F624EFB9C1F78B654DF58ED52 + 82B16FF2616CE2F7F28168F699FD7C3852567C5FE5ADBA7A68FE5479477B736C + A50E9907AFD343DEF45DE375EB5FB3ED00BBB2EC35130F0724355FA1BDDFC07C + 7DA6AFCEE4E659F0F72D4AF99F4A06027302E24825F5A17A6AAF098D7129ED04 + 809D5D9BA8CE3B751AE36D7D45C76DDB35B4264C36A495CCD7749A44699B932B + A56612C5639971307DAD2DFBB561B391CEAB421C93436FD6E279D5D6A4CD02BA + CADB7ACB01782B402F2783CE10F8AD0F1A3AAC7F2D6B644D2A0DF075E82D037F + 05769B9B6D1B10CB8DB5DC59DB2EAD8D586EBBB67E5770987A476D1D7A979358 + 03819B5910F8C30EDDD0B0730F34E8D2D340DDBAEDBBA04EBB4EA8DF91E5DD7A + 1B77CF82BE8D7BF635B17FEB7796B56E77D4ED28D8CBC9748BB6A86620B02C81 + DB73C2ADB28E9C7C77E1C4BC3BFBEA69E2034BB53991FFA0532FAA0727EE5D38 + 59EFCCFD0E2B604EEEEB71B25FAF631FD4669B9A6D048AF565674F03826BB1BC + 56FBDE2CEBE5B028E6F1D99F2C863FE0BEBA9D06A15ED7E1A8DF732417175CD0 + F419850FFA8F469D01A3506F38173C5AA44C9C8E962EF3D069C952745FBE1C7D + D6AE32EE9627FA6EC3586F4FE6B9D0DCB41263B7BA61EAF68D18BC6611FA2C9D + 83216B1663E02A2E0617CD40C77953D063F93C0C715F85A11E6B31DECF0B33C3 + FD3131682717BB7E18EDEF831E9BDDD079FD1AA69BD065E37A7459BF0183BCB8 + B8F4DE896E6BD6A3F57C57B49DBF189D162E438F25ABD073E94A7465BEF38225 + 683F7B1E5ACF7441C7F90BD073F90AF45CB91CDD57B8A2C7EAE5E8218BE0554B + D1732DC7BE910B6EEFCD9818BA8B8BFA704C8D09C2D8102E78B9D01EE6E78191 + BB3CB9D8DDCA05FB2EB82486613A17B89323FD30353E00539242302D230CF3F3 + E3E05A980CB7BDB25228C582BC78CC4A8F601A8BD98A0D2C005C9E8A55E6EBFB + 4CAC399C67E0E3DA237970DD93862565A958BC5B5FE2EB854D0696536B0F641A + 10BB8AEDD6EC4FC7EA031926DEAF2C7F97518A6FBBE67036561C48679B24CB82 + 788F5CC9C5C075772C56EC4BC0D2DD51585414CA3416CBF412A930D6B8A05BCE + FC6AF6B1663FFBA456EC49A552B0B23C85C7CD30F079ED21C5FC651DC1DD7DC9 + E64BFC55FB92B0F620EB1E4CC1F2032C63BAFA701AD61C49C7DA6372659C8575 + 4739F6438A4D960AB7C31926EEADA72C3DAE94C1EB52A9B17C5D77321B6B4F64 + 61CDB10CAC631BF73382A702C05946EBB94FB0D586ABEA773DB7DD6439CC54D0 + 58F0D840E1131604763B5B840D674B0CF0951BE68DA7E4B6591058D6B645D86C + 2C7205840B2C9DB200F0C693C5D872B6CC80DF6D970F18F8BBE5EC6E789EDBC3 + 719761F3D902B8CBAD32C7B8F98CDC3BE7C3E37CA101B7B2D8D5BE8DA7AC73D0 + 3EA51BE5A6F05C1E3C2F151BC8BBEDCA6EECB8510EDFEB65F056EC5C96F9B06C + DB95525E13B98BB62C7FBD2F1662C76559CE145BD6BF672D00BCAD02005BE057 + E075E7D552EC385F605E806E3B95032F59FE527A39EACD6D0161DF3356EC5F95 + BB1F4AC1C683C9F0E2B513F415009605B0DC3F075DDD8320FE7D765D2C66B9F5 + B2753BDBEDB8500C9FF3C5CC5B105816C18A0DBCFD4221C7946F5C404B3E94DA + EDE4F8FC2E17C29F0ABB5A82D8DBE548797400C98F0F5A56ACF72DF797FE1704 + A663A9186C3B99C871A660E7B974F85FCC4220E57F2E93929BE86C045DCA41D0 + C57C04F3B8013A5F8E7F3BCFC78FC70BE075DAC5F2205E4BBD040FBDB9D77A71 + EE00C082C1F60B738197987B470D140EB9568E10F3D2DC0172E5A6F9D131CBD2 + EADE21CB1AF81EDBCB3298D2CB750387B94F6D94EA85BCCA22EE5850D8806607 + FC15E0D17EF519FBF098D99665B1DC4BC7DC978B696E0B44730C8A4B1CCD71C6 + F29809EC2F5131831F1CE4380408F622EA96DC85961988204BB494878790FAF0 + 20723E3D865C59BFC9F532AF6FFE67471177BD10A1BC8E09D7F39071BFD428F1 + 4631222FE4214C16D4A7D3117C3A0D9117B31177AD0009370A9170AB0029F74B + 90F67037F38588BA9683C8ABD988B99187E48765487B7600E9CF78EC07BCC657 + 7210C8BF4BC4E502C4DFDC8D986B45083E9F099FC371D876201AA167329072B3 + 14A977F620F94E1952F9F7CE7A7A18055F9C45DEF3B3C8FAEC147299CFFFEA02 + 725E5C30313D65D167835F5B364CB274CDC438CD7F79DDC409560CD21CD6C9FF + EA2A8AE55E96FBE40E3796D7551692B272949561DE373750F0DD2D1353349B75 + 05AFE40AD7C861399CF2FC0AE23FBD8098C76790F8D92503FC64399B2D4BD1AF + AEC3B87C9685A703020BAECAD5B280A552CB5DB10564D324815A070436B0D601 + 6C0DB4FD466E9AEF19009CCAFDB6E5B081C76C6BDC3B33553B6345ECE8C386BE + 961BE857C7D27165252BF7C5E95FC9925520F53EC7648D4F80D5B6B42DF8CD23 + 14FDEE198ABFFF14C5BF7D86A25FCB1DB32C743FC7DE7FFAD24060CB22F87313 + B75716C1870584FFE50BA343DC7FF0F7B2F2654AEDFFFD67D8FBBB4F8DF6B17C + EFEFB8CDB272A6BBBF7F8AC2EF79CCDFF29826FF94C77F6CC6635BFA4A1510F8 + 3B5E175E4701F654C5E27D7E83E94DA4BFB8CDBF8760F81D24717F12AF773253 + CB35B60581AD365791FC39FF76FC7BE5709FACBB8D4BE8CF2F1B286B00F0B34B + C61574EA4B59E7CA45B3602DB799A61B2B62ABAEAC88F501806211DBF1880DF4 + 95EB70EE33EEA6957EA9547DC8F5F465247E7ADE48E348114865B93E38B0A029 + 7FE3CFCF23E9D969243C3E693C0BD8EEE4533F3DC3FF81F348FBEC2C1215839C + F70C7D306259C15B1F44587D58FDD8AE9AD33F3FEF88017CC65803A77F7ACEB8 + 7236EE9C79FC1C0160FEE6331DF05656B539FC3FC8E1FF92AC6B6DF8AB311A97 + EA4C53B96DDCAA53193C2767E06B8F4556F502D1CA3BFFCFAAAE80B0FE5F15F3 + 57A9CA64BD6FDC49B31F7DAC912700CC7AD6B958FFFB76CC6FF567AC7E994F12 + E85519C79EF2F959243C3D69242B69B9CB1604D6FF7A02AF99DCEBC72A06EECD + 3D06CE0A000BBC26DEB35C345B96C02CBF5E82083E4742CE66C3FF249F3554D0 + 992C84B12CEA4A0922A9283E57E5863FFC6A21822FE599D8F282BD02C0290F0E + 98FBAFF2F177F7F138BC2F5F639BEBA588961BE72B4508E17D36980AE7FD5110 + 3AE5F121A43D3D8624DED3E3384E798190E5AEE06FC46D3EAB6E333516C007F9 + EC90D789A39615F8239E177F077A8698E78C9E4B7CEE44DE565C7B7DD8647DC4 + 14C46747C0E5DD08BEAE8F86B88F75231E1E31E057717FC3F89C0AE333259469 + C8DDC308E2B34F2E9F950F667FFEAC1320087CE730FC6F1D86DFF503D8716D1F + 765EDF6F52DFAB7B0DD815E0957C2EED36A0D65356B9670B0DD8F59567106ABB + 3E70BB5802AFF39CEF5CD2F61E6E738E73A9CCF4A3324FCEC1BC38DFF0E6BC62 + 2BE74F565FF9D8C2799E8700AFC0AF0D804FE5C18BF338EF939C67708EB39573 + 1A0F1B0033D5876EDB38D711F4DD7238151BF727C2AD5CD037066E7B62B17E4F + 023670CEBA715F0ADC398735D6C07B2DB7CED2AADDB2D00DC792FC102C2B0AE7 + 760CD696C76335DB2F2D0E37E057D077F59E18AC298F35E9AA3DD1ACC7B2D228 + 037BD7F238AB64F52B182C086CCA12B82DE0198315C571585E2AD7CF310EEB56 + 4B8B8BA3E15AA60F3915D6251E4B7627980F42175026F62EE7E24BE4CAD9A494 + C31AD8B60A5E5A9E66C0AEEDB6596057D07731EB4A82C4361496ACB649AFB675 + 8CDD896C9382659CF32FE6381717CB3A39926B9908C8D390EB6E8EAB9829CF65 + 51411CE6E74663416E0CE6E74461416624E6697DC374494E349670DF926C9E5B + 7A28E6534BB9BDA228C100E015855C371473AD509C8495451C6B410296E44562 + 19AFC7F2C2282C55AC644916C2D4A23C595CF3BA71ADB244D6C3194126A6F282 + 0C7FCC4FDA813909DB312F711B1625FB60718A2FF3DE7049F0C4D4E84D1817B8 + 0263FD976142F07203802705AFC0985D8B3199E99C18772C48F0C2FC782FCC8D + F4C0AC105901BB6356841716C6EEC4CAB4702C4B0DC1CC686F4C0A75C7D4080F + CC8CF1C6AC781FAE0B7D8DE5AF4B921FE6A705626106CF9375672705625E6A28 + 5C73B8E6A2662705614AF40E8C0DF2C4B0ED6E18EDE78E2921DB305216BEDBD7 + 1B4BDC51BB361BB03B319CEBD89D5B30C867138673ED3B8C527E90AF3B86EDF4 + A0B660A8EF268C106C657E18CB876C77C7C0AD1BD1D77D2D7AAE5B81DE6BA8D5 + CB982E35EAC7B241EE6B3060CB3AF49655B0871B86EE605FD4881D5EBC163E18 + E3B70D237CBC30D2CF87EBEC204C8A8AC4A4C8088C0EF2E7187C58DF1BFDB896 + EFB1D10D3DDDD7A3AFC746F4DEBC1EDD37AC62D94A745BEF8A2EEB16A3EBFAC5 + E8EC369F5A80DE1ECBD1DF7315FA6E5E81FE8AEBCBB4D7FAA5E8B67A11BAAF92 + D5EF4274759D8B76B3A7A2E5D409683B7D123ACE9C8ACEB3A7A3EBDCD9E83D7F + 21062E74C5A0F94BD06FEE7CF49E371F7D962C466FD725E832770E5A4D9155EF + 44347758FAB69C3A1D2DA64C43D3F1932C8BDC7193D074CC2B90ABB216131577 + 772A5A4D9E82D693A7A3A52C6B0559474E7000E0F16832526DD8DFF8E9683D61 + 1ADA4E9A813636EC557F54EB89D3A8E9AC37996D2D70DC74D424341B35194DE4 + 025AD6BE43593E6CBC498D9857B92C83057D5557FB6535FCC9688E67C24CB4A2 + 04865B8E9F8116942C7D15EBB7C1D0B1F870C818D41B380A1F0E1A858F068D36 + 56C675FB8F40ADDE4350B3FB40D4ECD61FB57B289EEF60D4EE3A00B5BAF437AA + DDB5BF89EF5BB7DB00D47400DF57526CDF1E5437A31A6DBB5AD6BD0E0BDFF7A5 + 96548BCEA8D9A22B6A2ABEAFE0EF271D4D2AD560DE06BF4626A6AF65D92BF7CE + 82BECADBE0D7922C7B9B99776A7AD766BD3F93AB67EBBD9BF59E8D62DE321661 + 9903D81A0312199D502AB740AEDEF559005846273FABC5FAB2FA35E0D7364CB1 + DF195AEF2AAB02C0F63EBBAE794FA9779702BE0EF05B592A37E0D721DBD2B732 + FCB500B005629DE5FCEED796DE0B57555EB9ADF33B64E7EDCAD23EE73E9CFB74 + 2EAF78DF5DC57B70FB5D79556555C9EC979CDFBB4BCE792729C4639500D886B1 + 95DEE79B77FA8E3A95CBABD26B7D562581DF9F0AFEB2BF7F14C7A9825BFD97E4 + CC06AB628576D9DF2AE7FE9CFBFFEF50E5B1FC98AAEAE36F9533A8FD47E9FF76 + 005CD5BE7F849C21EF9F5305AC550C60415C5B76B9B3AA2AFF736DFE115F8D54 + A1AA6E707F8DFEAB7D54DCCCAB926EE44CAB6A57A57893FDE99B566AEBA74A2B + 9FAB539F06106BBBB22AB531EDECE338E94FC66BCBA9CC7EB8D95F3B55D64F29 + 13BF57B05470D7516EF7F553C56D501DEEFF99AC6CE5EE83F525E3BAC380634B + B6AB0FFB21FD660DCBC2D702B494525366B96B36C0D68052CB3AF7AD9A0D4DFC + DEB798FFB9CA6A58E057B17D95FE5C5FAE09A09A7E5EF5278B60BB3FEB78F5AD + 7151DA16F87D9B1329C986B3F6B681C2EC5F9323630DCCFA36D415FCB5DDA6D8 + 00F827AA23EB629EAF81CC1C83F555DD2BF0FB0AFA6ABC2AD3A48B6D78DE6FBC + CFF1D5E4BE5A2CE7A4CC4E6DE06BABAA325B160016F8A538D17CBBE12B00FC76 + 430BE65613CCD576FDA6F8B926A69C68EACBC237D9DE7C69A80926CBDFE1C4F5 + 5DF651AD4173BC6BFAB0FA7586C0D51A35C77B4D5BA14ECB0EA8D7B6B3519DD6 + 1D38896E8BF73F6963590877EC8A065D7BE2E31E7DD1A85B6F03850589A50FDA + E90BCBB6A8D6AC15DE6DE250D33678E7E3D678A7491B4EBCBBA041E77E68C809 + 7EDD0EBD0D0CD6D7988A135CB793A07077037E2581E0F75A7644ADD6DD0C30AE + D9BA076A6822AF2F3ADBF4C0FB6D2D8BE05A6DB82068C94580EAB7E9845AEDD9 + 17FBA9D7A937EAB5EB877AED07A06EC781F8A0E300D4EF3218F5BA0D46FD1E43 + F091E2087371D2C8C4BA198D56E3A7A0EBAC39E8BB701146AC5C8591AB57A3EF + 82F9E8386D12DA4E1E8B5E5CE00D983F0BED268EC2272306A3F5D8E168493519 + 3E001F0FEB8F561347A2E3ACC9463D16CFC6C0D5AEE8B77A29066D5C8DA11E1B + D06BDD4A7459B1149D96BBA2E5827968317B0EBA2C5A8EBEABDCD07DF92AB49F + B710ED5CB8B09C39978BCAD9683BCD05ED66CC41A7D90BB868D457B513D17EA6 + 0B7A2E76450F2E28BB2C9C87AE1C5FD7C50BD065B116A5D4D2C5E8B57A394608 + 0207EDE002D91B833CB9C8DEB41683BD3662F44E2F4C3056CA0198131B069798 + 50CC880EC68CF850CCCE88C5A2A234B816A763516E1216E52462517E3266A645 + C3259DCA88C1E4A4504C4B8FC0D23D9958B12F174BCA32B1FA5021DC4FEF85DB + D1122C2E4DC7BCFC14CCCD4BC6FC42BD9CC9822BEB2CDD9D6AB4722FEBEFCFC6 + EA03395877381FAB0FE66005B757319556EC67DD725917B3FE1EC5088BC7B2F2 + 64AC3A906E62012FDD1D8795FBD8CFBE4C2C2991556F125695A763EDFE0CAC61 + DB95E56958529C605CBEAD2C4FC58623B9544E05005E7B300DEB64DDABFCFE64 + EE4BC7FAE3195877340D6B0EA7602DE5762495E7C27A4C571F48E27813B1665F + 22DC98DF7024CDC4B3DD7AAE005BCEB0EFE399587F2C1D1B4E64C29DE59B4E52 + CC6F3C9E0EF79359F0389D0B0FD6F3940B64B6D9743287C763DDD382AF82B902 + B816185E7722CB00E10DA7F3B14990F77C293C987A725B2F14B798BEF2E075BE + 10DB2E161B795F288217FB753F958B2D27F3392EB91F2C83EFE572E3C2D0472F + 2FB9BDFD4219B6B13FF5E529082BF07949ED0BB1F57C01FB50DB7CA679DCCE37 + 56BBDB2FE9A5A6E5BAD9FB42017CAFEC86EFD5DDD87EB9043B0481AF96F218B2 + A62D828FC6C43A268E2EC7E37D360FDBCEE69BF21D3C8600ACB7A0B08E2D17D0 + EC57E936A7ED9D6C274B18BD0C95FB43C9BC187500E09DECCB9FE7AC7A2AB3AD + 83E5FE79EB891CD6CF7100600BFEAA8EAC8805907DCE14701C45F0651FBEE798 + 5E2836EE9E7D59E6C3F109FAFA9AD4CAEF60EAE700C081578B117AA504E13C5F + B9368EBD538EB06BC52656EFAE73D9D87E2A95C74FA49278CC141E2B153BCEA5 + C3F76C067C4EA661FBF134EC3899895D67B31178318F2A4430FB0BD0DF8063D6 + 3907EAC5F7B53DF0BF5C8A90EBE506AEEA65B981B7945E8887DD12BC15003E62 + E06CEC836388D40BF0EB7B0D00569D88DB072DDDA1EE1E44E8AD7D08B9B9D7A4 + C137CA4D6A5C43DF3F8C70C1612A4A2FE6F552FE01CBB84F6D23EFC8EDF311A3 + C87B8711C1FAD1F7793CBD94673E8CC70DE73874ACB05B1A9B35C6282A41D680 + 0F8F21E1A1C6292B64D5D57E8E5F80E001EBC82D29F727B1BF44EE4B7D700869 + 0F0F23596E4A65A576670FA2AF9520E68660EE7EEE67BBAB05083B9F8DA8CB05 + 48621D130BD3C4C73C8CB4FB7B9178AB04F1B78AD9C76EA43FDC8BB4077B4DBB + F42707914125DEB7E20F27B13FA5D1B77623FA4629222F175396655DE8F91CF8 + 1E4980FBEE506C3F1083A88B3988BBC67D97F31176219B6328443AC79AF3E969 + 647E7A0A39CFCF1937B5E90E4B3E0B76590049A90D976C0B43B989CD9305A100 + D3CB2B16F4E5761EB74BBFBDC9FE2E2288BF5D5FFE2604D60CF0525BD6557BC5 + 194E671DCB75AC809AA09565192C77D1C99F596EA065BD2917C0B9BFB867E09F + 2C402D97C472476C59002B0EB07159ECB05A1504961BE18C6FEE19A509D87E75 + 0BC972312C37C6669FE0AF2C861F18489B2697C62F6E22893256C10EF86B01E0 + FB90C56FF24B0B02DBF0D79681C002C0AAEB00C9EADB1C9FE5992CCFFEC503E4 + FEFA31F27EF31839BF7A443D443EF306FE7EFF198A7EFB0C05BF7E82C25FC92D + F3A728FBDD73ECF9FD73036FF70AE65282BA4682BDFFF405F3CFB1EF77DC66DD + FDBFB7B6CBBFFFDC0064A565BF653FBF7986DD02CBEAFB370F51F8DB4726D66F + BE1DEF97D74BD74C525ED6C082C0CACBAADAC06C5D932F78ED5EF0FABF944B68 + 5E17D649619D645E6B81E01459507F7B8BE76BB98ECE9085EE73FE9DBF92AB61 + FE1E5EDE40A620EF73415D41DE6B966B68FECDD3BF1100BE8A14FEBE529E9F47 + EA17FCADE97727CB70FD1EBEE46F80ED73E5425B10F89B5B1C87D55F3AFB915B + E7545985CBD257A9A0F1F34B26666F327FC7A9EC57AECA159BDAB6D84DE7B681 + 9BC68A9775042F9F9C44F2D35306FE66CBDA95658AE5AA0F3EE21F1F631F760C + ECF3162CD5C71226B5AC6FD54FF253CB1574AA5C453F71C4037E7A06699F9D37 + A0581F57983E5857D6B6FA7FC8F946F17B65992C382D2BDEAB2C57EC6C9D8BE3 + 0389CF34560B3A5BFF8B8E71F35C64C56CC5237E6599AC3A765E1F6814F07F52 + 1F5D68BC1A832C9BF5818681DDAC2729067806CF31F9995C621F47C263B9BDE6 + 39B04E1CF331BC17C6735FCA17E791C47AF14F4F9A6D01F4445E3B2B36F22993 + 4F78C4FB26EFED71FA08E7EE5E5EC372DE53F5A1D17EDE5315779CF7C847870C + BC8DBD5986083E9F046AC32F15987B590CEBC6DC2C47E435EE733CB3822E1562 + D7F95C3E3B790FBB5E8A78DE631315D3FDFE41C451F1BC3FC7F19E2E25E87ECC + 67410C9F0391B7F6F2D8FB4D3E96F768C5E8951482401F224599E78420EE2B45 + B1AE3C4E44EBBECF7A3177F5DCE073E29E159AC088F5F44C331F1F71BF9E35E1 + B70E21F4069F517CF685CAA305FB0EE7BE103E6782F96C9194B7E2FAEE33A900 + 70289F4152188F114C05F1591574F72802EF1C813FFBDC75F320FC2859FFEEBC + B6AFC2BA576E9FBD380FD0DCC99373306FDE73B7734E236DE333782BB73D390F + F03C5F02EF4BFAD0ADCC017C39BF72C0E02D670A1D1E54141243F341E6391791 + 6CEB5FE5DD39B733F3CC8329587F20994A327213E4DD1B6FC0EBEADD72B51C01 + B9605E2CCBDD9C4063992B17CFEBF7F379743815EE070586934DBCDF55653158 + C67DCBD96E6539E7BA7B62B0825ABD37CECC53DD0E2661DD01CE5BD9FF1AF6AF + D8BFEBF6C5F398094682C002C0AB2981DEF5FB12CC38960922E7851877D2DA5E + B72791634BC44ACEC3D79425B13FCEC5CB93B08CF3F0C525319C83C79A39F9B2 + 3D9C9B0BFE96C6527106C80AFACA0390BC01AD90B5AE3C00B1FD72B567BF4BD8 + A7E2062F2E615FACBFD01156664999DC3F0B1C3BE2FF723D602C831D52DFAABF + 6C0FD709668D10874594FA945CD9DFD2D218AE01C2313F8FD753AEAB8B395601 + 5A41601E6381C39DF3DCEC70CCC90CA542B02037028BF323E0CA7AAE72699D1D + 0297B400AE8B82313F27CC583DCBF5F59262C541E6F996F278D47296AD2C8AC1 + 4A1E634511AF29AFC73A8E7F4D5902AF63345C0B22B194FD2ECB0F876B6E28F3 + A158591086E5D9415892EE4FEDC2D2343F2C4BDB85C54C17A6EFC09CC46D981E + B101D3C2D63375C3B4F0B518EFBF0CC3BCE66298E71C8CF5598C09BB56604AD0 + 5A4C0FD980A94194E2008779606AB817A6456CC564A613C3B6580ADF826931DE + 06FE4E8BDD8E49515B312586C788F3C1AC443FB824F9634AB40FEB7AB3DE4E4C + 8DE6DA31700BC604B863B4FF260C9745ACE2F6FA7B98B8BA033CD76200D3C1DB + 3762849FA781BE837C36633035CC6F2B46728D3926D81763837D30C2DF0BFD59 + B79FD75A0CDBBE81DA88816C3FD0C3CDA8F786D5E8B6C2155D972D4437AAC70A + 4B7DD72CC5C04D6BD07FF35AF4D9B41ADD37AC4017B7E5E8C1FC701E63724410 + 26850560C4CEAD3CB6174606FB635C742426C64661624C2446870462A89F2F06 + 7A7BA19F8707FA6DD982FE5BDCD9D706F4DDC03ED7AF46E7E58BD0C1753EBAAC + 5A8CCE2B17A2FDD239E8B86C1EBA71BBC3E239683E7B2AD7E9D3D06ED16C745C + 32179D16CD45B7250BD075D13CB4993515ADA74D46FB1953D1C96526BACC7541 + 973973D175CE3C74E79ABDF7EC85E8397B1E3ACF9A8D76D3A6733D3F1DEDA6CF + 40EB296C37651ADA4C9B89E613A7A0C5A4E96831999A380DAD26CD40CB09D3D0 + 62DC54B41CCF743CD389D3D166CA2CF6E182B653671ACBDE26A32619E0FBF188 + 096834748C81C04D474F326D5A8E9B864F464D468BB193CD3B8D66A35967D818 + 341931162DC64C62D954341F3BA90200CBE2F7E361130CD415A095047C0DE41D + C163C86A5750D8D4130C1E878683C6A031D3A6B2EAE5BE060347A391AC87B95F + E5DADF407D09FCEA63FBBEC35157B17C7B0FC307BD86A0568F41A8D9DDB2EAAD + 29EF6DDD2CD85B5BE0B7733FD4EED417751C79797BABD146B17CBBA3465B415F + B9791604EE8E9A6D2DBD2F6BDFD6DDF07EABAEC6C840717CDF6BDE11EF7FD201 + EF337D8FA9E0AF5C3CBFCF7D92E5EEB91DAA3BAC7EAB356A6D80AFEDE6D98AE3 + AB18BE0ED56F6ADEB199506CF53EB6F2B2F2ADDFC4BC5F13DCB501AFED2DD058 + F7D6D1BB3C8572B3F6397B1EFC796DBDF7B30C3EB4FDA6EDEEB9A624831467B8 + 6BC15FA582BEE6BD654DEBFDA50D828DCB674965360016CC757A4F6900B003F0 + 5686BF550260BD07AE02BC56A5CA00D77E3F6CC35B3B5F79DF8FC96EEB7CACD7 + EA38BDC7B6DF77DB32E56FD5324658AF95FD989CDEAD576C3BEF7748FDCA08AC + F27BFCFF36004C198EF0A64395F6FD3D54158FFA73AA8A73FD89AA628576D9DF + 2AE7FE9CFBFF7BA8F2312ACBDEFF97AAAA3EFE26B14F6750FB8FD2FFAD00F8BF + 5B55FD0355A50A582B00EC0C6FFF5A3900B0F95AC4B9FC1F74F370565537B8FF + 0E55DCCCAB926EE44CAB6A57A51CD7CD1900DBFA6BCFD5B97E65E9A66EC716F8 + 93F1DA726C3B3FF07E0800DBD27E415E6309CC6DE7FE2CABDF0F0C00FE192700 + AF80EF9F4E0C546E5905B32F4D3434B910B4AD486591CB09062723C6DAF73D0B + F60AFABE2D002CEB5F4E3E0C0016A8E504E61D4E6E940AFE560060F3C0D7839E + E749E9E1FF6A1FD36A1453D3B70300CBEAD7D90AD82AE73135B912D815A4ADC1 + F1B31F33813280D621D65399B1E215F8D5F5D4F570F4A5B19AD4211B040B625B + 56CE02CBAFE06F6500ACC99E6DE5AB326B12F70A14DB1214160056AC6263B5DB + C8E1B2D961C1ABBCE06F0D4E54AB37685E21C52956CC607B02AA54718BDFADCF + 7ED417F3EF38A0F23B0D586E40B2DCD9BC02C1354C4CE13646359AB632318295 + D66CD10E1FB4ED84FAEDBBA041C7EEF8A86337D46BD711F55856B74D47D46AD9 + 1EEF35B3620BDB3187DF6FA6B2765407D46ED5856DFAA00127F8F5DAF5426D4D + EA3519D7979A6DBAA096B1FAB5AC7FDF6FD509D59B73E2DE4256C19CDCB7EC8A + 779B7542B54F38B967DE4CF4CDD79C5D50BD1927F72D3B98F6F53AF5C4875D7A + A251B73E68D479001A751C808FDAF545FD36BDF151FBBE963AF443A3AE03F031 + 17238DB91869C80548B33E43D0810BA26EE326A3D3E8F168DE7F303EEEDD17CD + FAF541CBC103D076E860B41CD40F1F76EFCC854907D4EDD609B5BBF19CBAB6C7 + 873DBBA0F1C03E683CB82F9A0CE9C785D650749C3E1E3DE6CF408FC52EE8E13A + 173D96CE47F765D4722D74B9C09C3B970BC2D9E8CC856297B90BD166FA2CB320 + D457BF4DC670B1C5055A0B2EE0DA99459F62F74C64FD996681D9956DBBCEE362 + 9452BCA14E73D80FCB3A697BE13CF45EB50C0337AE45FFF56BD073B5652DDC53 + 659BDC3076BB172671613C35C80F33234330273E127312A2B020231E8BF35231 + 3B3D16D313238DA62684635A5214A62773511D17825191FE989A168D65E57958 + BA2717F30BD3E05A968DB5477663D5C1122CD9CDB2820CCCCC4CC48C8C04CCCE + 4EC1ECDC64CCCA8AC29CDC582C2C4AC292D254B8EE4EC3AAFDB96C93875507F2 + B192F9E5FB72B06C6F266441ACD48EE9B5727F26561FCC82E2F62E150CDE9FC1 + B21CF69162B48CF5979659A980B0DCBC2D2E8AC73AB6F1385904F7E3F9587738 + 136B0FA563BD5C391FCB86DB61E60FA56193DCEC9DC8C4FAA369DC9F84758704 + 8553B1E1682AD60B081F7080DF83C9D8C4EDCD47D2E0713C035B4E66B19F34F6 + 93CC3E53788C0C789CCE311220DECC3E3D98CAF5F1D6B37906A80AA6AA9D5E08 + 7A08885E2C86D785226C615ED6B846CC0B0C6FB950024FC592BB546AAC68652D + AB7E2459ACEE549CDA6BBBA93293DFCA769EA7B84F96AD6CEB236B576AE765D6 + B95A6E5C1ECB05B297002BC7B2F3AAD55600D758C0B26CBBE028B7ADE3151AEB + 58C5EED531B55FE58A95AB7311E015E8F53192554BAE81BCBEDCB6406B812933 + E5928E2B68CB54D056FBD5CE87FD1817CFBA1E27B28C358C80A89F002C257788 + 82BE4AED32492E12554FA06C1BAFB3B1A439A6B6567FB21CDE7C28058AAB67DC + 4A9FE439B05C63B4C6CC6BC07EFD2E951859796B7B1725F7CB0172C57CB51801 + 574B10C47D011C6F10FF0E82BFC157AC97EABE67E4BE31D3C0DE6DA7D3E07D2A + 153BCF67B17D1E769CCB86F7F1546C3B9E063F8E3390D73498FD045F2E45A05C + 4C333596C694C0AFDC562BEEB020ADA081C06CD8DD8306D24650569CDFC3887D + 741CF18F4F20FED129C43C386E5E9CCBEA590039F08A654515C8ED909BFBD8D7 + 7E23B50DBDBD1F217A01EFE85331154399575C45C5548C7C7884F9C326EEA2C0 + AEE072E4DDC3A67E28CBC2EFCA1A8BDB54C86DBD98579F074D9FEA3F5A16C182 + 171CBB5C4EC7B1BF9887870D3456FFB2FC9255B2B15066998043F49D0388B9BD + 0F713A674A966F61578A8DE2EEEC43FAD3A3262666E2FD03C67A2DE6E66EA4DC + DF8F8CC702BF4790A5FD8A9579A7DCC4C794FB66415FC5EDCD6079E6E3234863 + 2A97AAD137CB1CDA6B004B22CF354140E37A99B1A84BBEB7CFC4FF4DBEBB0F11 + 97E45A35C780DF48C1DFF339C6C22EFC621E12EFEC47FA93134895ABDBC72791 + FCE824124DACD333481364FA42D67F96AB571B2AD9D68502BA8264395F5C44C1 + CB2B28FBE52D147D7D1539CFCFA3E8E55564B10F7FFEBFECE06F5AF19555D7B8 + ACE57EC1A754EE37C71118637F161876B886A63204068DCBDFDBC61DB124B7C0 + 92959725EAAD0AF86B5C18FFF2810198360816A814DC952B6659E41AABDC97AF + AC72B3BF7B80BC5F3D669B87A63CE5C54D24ABCEB73CDEAF1F239BCAFCD54364 + 707FDAB70F90CA7606F83AE0B080AFB6058455A6D8BF82CBCA67FEF211B71F18 + 38AC72F597F7FD33E4FEE629B20580B99DFFDB6728FCFE5314FDFE7314FEEE33 + E4FDFA29727FF504F9BF79866296EDFEDD17D8F3FD73EC71A465D26F05789F63 + EF1FBEC4DEDFBFE03E955B65CA4BBB7FFB9951E96F3E45E9AF9F514F51CA7E8B + 7E2DF8CB73FEA5AEA7057C751D8DD5B443360C16FC95745DE4CE5A605BD6CDBA + 76D9BC163AA7545E671B00CB6D76AAFA300098FDC865F38B6B0668EAEF2957C7 + 85BFB88B5C87357086B1FABDCEEB7D957D53FC0D58BF314A6055BFAD97724D6C + B90C97725ECA6A5C6537F97B927BF15BCC2BC6F40D1EE73A7FAFB6F8DB912B71 + F62737CFB26095B5AFFAB781A95267002C6B5FC1505B29CF64BD7E84F7A43DBC + 77EC47D2A72790FEE5398ECB017D29C1580B00CB02586056BF6DF5651D23F6E1 + 71E3DD40805480566549CF4E219DC7CAE031D587E56EF91A6409AC0F20EC38DA + 465F5C42F2E78A99CDF37000608DD7B2B4E5FF2AFF6FAD98C5A74DDFB60C0CD6 + F19EB09C4AE778ECFF3FB9772FF8F63AB278EC64B68FE77D2EE1FE51DE438EF2 + FF9FE963DDF3EC38EA56EC74B93AD6FD2E8AD7436E8BE304883F3D6D14EB00C0 + 8A9F6C6228733B89E502E982C109BC0682A98A9F1B7CAD146137CB117DEF00EF + 0987914409062751BA2FC6C915F39DBD96EB6859DE0AD4F29E1FC3FB729CEEEF + B2AEBD5282605903DFDA6B2C7465BDABBF4F38EFBDB6CB7FE3CADBDCBBE5DA9B + CF01F30C50DD23E6A32379A0109C551A71FB80495F93E2D1F398E6F9C1E31AD8 + CB7BBA3E400AE45C23E83ACF455E224C9D03060A9BBA77E479E2A849C3F95C09 + E773274C50F7EED18AE78C5229E0C67EF85FDF0B7F3EF304816D05EB39642C81 + 0F2390F5026F1F46D09D232635BA7990EDF6C34F96C0C6B533E757E78A0DE895 + 047D256F3EF7B5AD799981BAE7E419A598F9626C3E2DC8CB3994A3ADB6379FE6 + DC4D1FEC716E23D996BF76EC5F59036FE4BC641DE7226B0E2460CDBE78AC2AB7 + 816C02D69573BB34CAC0DE0559FE58981D802579C1585A108A658561060CAF15 + A0D51C9492B5EFEAD268AC94BB66B6D7C789AB98AEDCCB7EF6C69963B8710E2B + ADD99F60E6AF1BE52D85B2E1EF7ACD69B58FF5D7511BD887426A2876B0E0EFC2 + 6CC1E7501E3B066B0D00E671CA12599FF3E17DA91C3FE7DAB2A82D89E19C3B1E + 2BCA13B17C4F02F371464BB94F80767159329618689BC472C159D589C7D2D258 + 6335BC5056C4D4C2C2282AC6E13E3A0E0B8A63D98E6DF65A717D65352CC02B4B + DEC50E57D392E9B34CB17E151A2602AEEC7389AC914BA2B1945A981F86B9D941 + 3C9F109E573816E786F1BC22B09875651D3C8F65737242E0C2F39DCD7AF378CE + 0BD866514138161684311F0A970C7F4C4FDBC5F54C2066660561765E28E61546 + 1B2D289277225ECB525E9B628E2737128B792CD7FC70ACE2F1753D97B1AFC539 + C158422D63DFD24AC175C56BCE0DC15201E0D49D704DDE89A5297E589CB213F3 + 537CE192E08DE98AED4BCD8ADA8459E11B30257015C66D77C528AF8518E9B900 + 23BD16608C8F2BC6F9ADC4B85DAB4D8CDF09A19B313ED403E3C33C3196E9386E + 8F0971C7E8E04D18CB747CD816966F36DBE3048623BD3035D607D31376626A8C + 0FA6446DC7F458E6A37C3026601346EE74C318FF8DCC6FC628BFCD182E8B5E1F + 59EF6EC6105F59F56EC1A06DEEE8EBB91EFDBD376210B7870778B37F5F8C0A52 + 6CE1ED18B1CB1303B771BFC71A0CF2D487C8EBD07FF32AF4735F85BE9B56A3F7 + 86955CA32E45CF954BD06FED52F4777345DFB58B992E333181FB6C5C81DEACDB + 63D30A74E4FEAEAC2F8F56E342FD302A703B066EDF82FE3BBC312C3410A36322 + 30322A0C23C28231263C1413A3A33986400CDCEA8D41DEDB3078EB56F4DFB411 + BDD7AC41F795CBD17131D7DECB783C37855F5A8AF60BE7A2FD8239E8C03574BB + 79B3D166CE2C6A26DA2A75998196D3A7A2E554AEC5A74CC2279327A215F36D59 + D67EC674749C350B1D67BA98F57ADB4933D171EA1C749C361BEDB9DD66CA74B4 + 992A083CC3A83DD7FA6D58FE09D7F7CD274CE37EB59B8DD68A953B6E9AB1EE95 + 9A8D91ABE649266D366612F3E3F1F1C8F11600A61ACB8B9940EBE0D1264E6FB3 + 9113D184651F0D1A85FA0346A0C1E0916838441A852623C6A1E53859EC4E33D6 + C08D868F41C361A3D1608803D43A24E82BCB5EA5B6257083C1638C64C5FB61FF + 91F8B0DF08A38682BBBD87A14E8F41A8A778BD7D871BC05BA7E760960F35DB52 + 5D96D5EE3E10B5BA0D40ADAE031C96BEFD505BF0B7ABC275F537E1BC047DEBD8 + 0098A93CBC09FA0AFE56578CDF760AFF2500AC8FFFBBE3FDD67A4F44C9DAB725 + F7B7B0E06F0DBD0F6AD6CEA1B6A8DEB435AAC9C2F7636E0BFAB2BC5A13B97AB6 + AC7C2B80AF71EFACD8BD9FE0ADFACD0CD895273DCBB0C2F12EAD5E53F33E4D72 + 7EC766C0AF43DA36E23E03831DA9E0AF0D806D90EB0C776D55BC8BAC29D06BD7 + B1CA2CF0EB306279DF7AD768E59DB6F55E53062B7A2FEB78376BCB86C206EE3A + 64BFE37DE35D3BCF3A9431FEA9FE3A78AD2C67231DC1580377DFA9FD27905775 + EDD459F6FEBF56AFB5777E8FED78CF6DCB94E9FDBEBDFF2F91F3FB7667699FCE + CDA9AE01C0B20276BCD337211FF50EDFB15DF13EDFB1FD43653F24BBEE8FE92F + 6FF77E15657F5E55F1A83FA7AA38D79FA82A566897FDAD72EECFB97FE7727B5F + 95F93F23BB8EDDC659CEFBFE1A55EEC7565575A5AAC665A476EFFE0F00FE7F93 + AAFA07AA4A15A0F6EF0480CDD729CEE59500E63F42956F527F4EA68D9DFE48DB + 1FDD5FD5CDDC966EE44CAB6A57A5749D78BD2AC35F5B7FC9786DD975AB52E5BE + 9C1F3A15728CFFB5875D25A9DC190A570580ED87A36D255C150016AC759E38A8 + FC27EFE881CFFD952619B60C1C153075C05EB537D0B706CB9C624E581098E554 + 850B68C7C3DD86BF3F7997E7EAD04FAB739C9A74A83FF6632C88654D5CABA189 + CB6BDC3E1BAB5FC15A81E1060E002C48CBE36AB22200CCBCAC7ED5469285B0C6 + 6B5C3E533A2F1BFEBE5D9B133D63B9DCD0F4FF4E1D4EFAD4C60180CDF971DC36 + 007EA326CFC109EEDAB201AF0D7FED499B5DF70D47FA33EE57CC6001DB7765B5 + DBA805AA51351AB744F5468A4DD2DC58F30A00BFC7EDF73999ADF9716B931704 + AE3C39D584D54C503521D5178C0E0B604BAFE0AF0D98DF61FFEF34FA8493630B + 360B0AD7FCA4ADB10E366AD501B55BB647AD16ADA936A8D9BC0DDE6BD606D559 + 4FF05793EE1A4DDAE2FDA6EDD9AE036A35EF8CBA9CC87FD8BE373EECD01B7539 + B1AFADC9BBE38BCC5AAD3AA39620B04302C052AD565D509B13FD5AAC5BA37917 + BCF789D4C9B8EF51FC167DC9A9C9FD7BCD3BA06EBB6E68D8A5B7B14C6ED2B31F + 9A722122D0ABE3D5E1B13F68DDC352AB6E1C8B2C96996FCF72AA5EC79EF8886D + 3FEAD213F53A75439DF69D50BB7D077CD8A52B9AF6E987667DFBA17E478EA965 + 2BD468D91A355BB7C57BAD5A7311D20675595EAF6B570386EB75EB8C46FDFBA0 + D5E811E83C55D6C313F0C9D8D16831712CDACF9E8EBE2B9662E87A37745BB000 + AD274E41072E043B7141D8723C1778A3C6A2F9D8895CECE98BDCD1683A722C5A + 8C9B88961326A2ED94A9E66BE2AE73E6A0FBBC79E8BD68117A2D5AC8B2995C3C + 4E355F19B79BCE45E64C2E3067CF46C705F3D1C57531BA2FD7D7D54BD06989AC + 8497A0EFAA5518B0760D066E5887513E5E981C1280A9E1C1981C118889D4F000 + 1F0C0BF0C5C4A8104C4F8CC694F8488C0E0FC088603F8C8E0EC6ACAC242C2F2F + 345A529A0BD7DDF9702D2BC0C262C1DF2CB864A762625234C6C7476072722CA6 + A6C661527228A6A747C0252786FB63302B3BB6C24278E99E6C2C2A4967FB746E + 67302FF09B49656059793A561DC8C2F27D19C632582F965CCBB8BF2CC380E4C5 + A5C958549C84854509DC165C4E862B256BE00D8773E17E2C1F1EA78AE071462A + 84875EDA9DCCC3866399D87434135B040A4F6660D3F1746C3C9AC6B234B64987 + 3BD38D72C57730D9B8DBDBB83F099B04810FA558D61847D2D87F8A058659D7F3 + 5416BC4E6563ABA0E7B97C789DCEA990E7C96CEECF86C7C92C1E2FD3406201DB + ADE78BE029B781CCCB1245EE04BD2E385C0932557E3B250B5BDF8B05D82177C9 + D4CE4B45F0BB5C0C5F59DC9ECDE571790EB242563CB87DB22E49C6E6A319D876 + 46B0B3D0C0CEEDB272D1CB49D6D5F8765DD90DFF6BBBB1D3F4536880EC8E4BC5 + D87575378FC131D8E317C466BACD06BEE7154797FDF05C4CCC5EA63E02566C2F + EB5B49D6B3BE3C860D7DBDD5C771873B675E0BE3CED9C05F4A5096DB4A8DB52F + 2537D036E0151C0DB85462DC220B02DBFB24E56DA9AEA4387A82BE8A13ECE148 + BD782DB69F661B8163CA1C97B2DD4DFA0B925F2C615A8C001E47105516B94157 + 4B0D00DE75A5088102C0BC86FE3CF740FE2D02CEE79978BD3B959E97EB69C1EE + 742A83FD1420E85A09FBE4F53A9D8D1D677210C8EB127CA9142197CB107489D7 + 9EC7130016B40DE2350FB8CA63321F7C639FB1A60DBD2D0BA77D08BE7300E10F + 8E20FCFE210BDCEA45BD01A92710F3E004226E1FE6F5E1DF91E7B5EB828E2978 + CD7EAF3B2C82CDCB7859F91E35921B4D415FC55454DF21820C7A597FFF308F71 + D84060010BC15F5967093C871A179C7A216FEB28DB1F41E04D8E532FEDEF5BB1 + 1AE39F9EB200AFA007FB897EC83E280397B93FE689DC521F43A48E250B300760 + 88E5B6DC450B5684F337197AB504E1D7CB10774FD6C4870CEC487D7A0C294F8E + 22E9E14113BF325E6E51EFEE45CA838348BABBDF58F1069ECE44F8C57C24DDD9 + 87D4FB0790C676993C668AA0C8EDFD6C7798ED0E2186E38EBF730C89BC7E093C + D764D64BBAB79FE941643D3B86DCCF4EA2E8CBB328A4D447DC4DC5E02C43825C + 803F3880CC67A790F9E919B63F8EE85BFC7BF06F1077FF38929FB1FE73CBED6B + E2A79605A0B665E5A898A18256592F2E20E78B4BC66A38F3B3B3C8FDE23CB23F + 3F8DB427C7999E41D667A779BCBD1CE741647E7E16D95F59E0D8B67E34A04D20 + EC0BB9C2BD8CCCAF2CD7BCB60B5C03FA64F1E9B048B5A16FF637961BE59C6F1D + 16AB2C37F0D7017E3305356D1029F0FA8B8706F41A106CAC5755FF01F27EF1C8 + 28F7978F0C0C9605B000B14067D62F1FBE0E80B99DF18B071516C1CA67AACCB1 + ADD8C0C6629892F5B0A06F26FB4E57BFCCAB4E9603FAAA9D24109CFFFDA706FC + 161A00FC390ABEFF0C79BF7966A43267005CFABBE72815D4FDF5A7060297FFE1 + 05CAFFE94BECF93DF7B1BCF8379F1A957EFFB9D996157189524AF54BD967C1AF + 79DC5F0A94EBDAF13C354E0744377240DF0C9E839DD7F5D2F59125B3CE4F56CD + C6629A6D93BFB90DC5004EF8CA8A9B6C2CA74D5F3790F5D50D63C12AB7C5699F + 09965E46E12FEE20FF3BFE0DB8CF06C0492C4F797E118A2DABDF86F948400098 + BF15FDD6ACD8B4B242B764B9429605F975E4CA35388F99CBBF59CE573C1FFEFD + B2388E2C59857F7115C99F9E3530527D5BB096BFAFCFF9BB9685EE67E7B82D2B + D9B306A8EAF7A8DFA524F82B57D0C6525456A64F8F23ED8BB3FCFDF1B7FE92BF + 4FFDFE8DD4E7AB71EBFF439265AF206CFC237DDC72A202D02A9F40A97FE33299 + 6582C5C61298BF771B00CB5A39E533C16BF6A36B23F0CB733731813F3F6F2C8B + 1364A1CB310A02EBFF55C74B627F96152EFB571DDE17148FD8C4331690655D81 + E0B2EFEF1B8BFD2C1E2349AE8D792F89E37D4C0036EAB65CF02BFEAD5CE3CBEA + 551FC4E80398A356CC5BCABA0FF25E2957CF4F4F208EC78C933530FBB7DD4227 + 309FC863C63D3A693E00125055280081DA683E03E2D93EFEA1EE3796F5AE2D59 + E8C6F13E17CFFBAEB1E6D5BD8EF7B644539FC7E4FD4431DB8DD52EFF3EA11C6F + E00D3D73CA1C5097653779AC1B82BBD6BD5FF768EBA32379A2386C9E4B11BCDF + 19EF12DA67C0EF01E3BE595E28F41152209F65FE947996DD2C37D0D782BFE5E6 + 83A4308163B6B53D53045E2BE773B09CEDF8ACBBB69FCFADFD1CD77E04DD3A68 + C06F10EFD5DA56BE629BE30CA0FCD8D697CF3E1F3EF376F099AD6D5BBBAEEDB3 + E2FD5E3F6062FE4A82BF8AFBABD8BEB2E0DDCE74FB953DF0E1B3D7B6FAF51210 + 7640604F3E53379F7D25F75382BD05C62B8B40B0B100E6B6CA3772DE226D3EA9 + 0F00730D0436E29C4BDBEE9CF76DE03CD14D1E66F491A13E263C9A0E0F6A9380 + AD2C75CB045C6339DF4C32212E04668D1B684AC058F058E9AAB268037BD71D4A + C53AB677A3D61FCBE09C561F382A54890580CD71349FE57ECD53D7CADA98B241 + B081C1FB12CDBC763DE78DAB4A63E09A1F0601E8E545E158B327DECC27D7EC49 + A492D8565E73D2398E74ACE0B66B692CD7008958B937C958012FDD2D201CC7F5 + 800568172AFEAF5C2ECBFAD6488056B0371CF3F242302F3718F305BE79CC0505 + 919CD727180BDE8545316C2F77D29CDB97241A203CDFC0E270037A1716B10F6A + 4949B471453D37271073B203309FFD2916EFA2FC502CD13158EE92E687D9297E + 9897E18F05598158981DC43A21984FCD330AC4EC4C7FCCC9B234372B80DBBB30 + 3B7D97E9673EFB73C90860FF21989B1BCA314418683D27378CEB188EBD200A6E + 5C77ACE5B92E97DBE79C502CCE15700E31319817B0BF7902FBB2E8E67575CD0F + C6D2C210AC2808C5528E7961EA0E2C48DA8E05093E5810BF1DF3E3BDE112BF15 + D3623C302D6A33E6C4796261BC2716C579617EF416CC0A72C3D41D2B3165E72A + 4CDDB51A5302D76052D03A037F2787B96362B827C6477A6362EC0E4C89F33556 + BE9363BC312152C074B39100B0E0EFF8700F0380A7C5F96066E22ECC48F0C38C + 783F130F787A0CD77F211E1817B40513984E0861BF415B31C67F2B46FB7B6384 + AC7C77799B78BAA3B85E1CECEB8101DB37A39FF7460CDEE16900F070D61BE22B + F8BB0903BCD6628007D79C5B5663C0E655E8BB71397AAE77450FB7A5E8B16E29 + 7AAE5A823ED4900DCB31C47D250671FF50F7D518B97583B13896A5F1209F4DE8 + BB753D06F86CC688401FA3813E5BD067EB260CE17A754202D79C8931181E118C + 01BB7C31269C7FE7CC4C4C898EC640EF6DE8B7C503FDA9DEEBD6A3FBB295E8BC + C4151D16C86BD652F45ABD866BE52568377F8E057E67BBA0C3BCB9E8C2F575FB + B92E266E6FABE9D3CCDABAC594C968CCF57C93F1E3D092F9B62A9785EFF41968 + 336D8671D92C17C872D32C8BDE5613A7A3B50300B733F077263A4C77E1BE6926 + 066F3341D9C9330DFC15E8FD78E404341935D14879415E49C0B6C1D0914CC7B2 + DE14E38EB9E1D0B1A83F6024EAF71F818F0609F4CA9BD9187C347814EA0D18E1 + 80C0A30C006E3C6C0C9A8D9A80E663643D3CCE82BF43E59A79243EE83B941A66 + 20AF5C3AB79F32C7406001E17AECBB6EBFE1A8D367286A3BA0AE60AFA06F5DC5 + ECED36D048F17B55569BAAD34379C5F21D845ADCF77E97FE46EF196B5FD61504 + 3656BEFD2C8BDFCE7D515B61BEA89A1D7A1BAB5FC15EDBE2B75AEBEEA821D8DB + AE07254BE06EC6D573F5E69D505DAE9CF53EA8B9EDD25970B7AD01BCD59AB645 + F58F5BA39A402FF596606FE3960EF06B01DFB7047B95523FABDFCCC8580137B0 + 42ABD916BFE65D9A40AEFD2E4DEFD11C65CEB0D7405EEE576A97293570D751DF + B87C7606BDB56C00FCBA95AF0D769DA1AF9D777E77E8FC0ED1E46D00EC78BFAA + 779892B3F5AF01BC4EEF72ADF40323BD7B35DE222953E6045EAD77B2167C35F5 + 1CF0B72A005CB98D5DE6DCDEDE762E776EE3BCEDACD7F639DE6DEB1DB6F33B71 + B35D852AEFFB93F64E5CE035A9AC32007EEB4F01B02DE777FC5595D96DFE9CEC + BA3FA6AADADA7AADAECD7E9CCB7E547F39AF7AC5B62A71C0CA6CB02A566897FD + AD72EECFB97FE7F23FA337DEF8F3AAA8EBDCD75FD0EF5FAC1FE9ABAA3159AA46 + FD3701E0D7E5047F25FB42FF8F7E5455FD13493FFDB925B3FD1AACE53FA4D2AA + 5C39FF25AA0A00FFD53784FF9AAABA3955A58A368E71555547B26FAA3FD6F79F + DCC8ED9BB9C47C556D2C0B5CC7B89D24F7CF6F2AEEEF9BCAB3AC92ECB157D967 + 2599BA7FC1B537759D1E3ACE6357FA270F30A73A2AB72D7B6D09FE2AB563FFDA + 0F7E49FB047FE502FA0DA709820D6B0DF435B200B0EAD871262A03605B82A495 + 01F24FDF75A46AEF389E2611B675EFEB0F7A9E47050456DE9A48E8B8C67AF8FD + 06788BA9F2722D2DD86BEB15FC6D60E0AF01C0E6181C0B5359285BFB1BF2D8DC + 4F59EEAA39D93213A67A0622BFC3899920B38EF36E9DC6A8F68100B0DD46609B + 75D5B72669EF73FC9A847142F6A660AE13F075967399EA1999F6563CE49F73CC + 8A176226A28EF8BDEF71F25A430098DB9A64CA4258D057F057AAC189AD6D016C + E28F70C269EA7DC4FA9C98560061F6F9AE40B1B1066E66C1E58FD937A5BC892D + 2CB7D33AAE034057D77E63116C41DE6A1FABEC13BCD7B4396A34B62C94AD36EC + 9BF51547453154DE6D244BE08EA8D3B22BEAB5E9817A6D7BA2B6AC7859F63E27 + F0B55B74C6079CE0D7692BF0CBB2969D50AB4D376E0BD876479D563D50B76D2F + D46D47B5E9895AB2FE75805F7BC2FFDE27EDF161FB6E68DCA5371A287E71B75E + F8A87B1FD465BE567BF6DB5680D952CDD682CB5C30B46887F75B7440CD961D8D + AA37E7C2E193365C5CB4479D0E9D51BB3DC7D6B6BD8971DCA80BFB6AC131376C + CFBF413BD46ECA3A1C432D1EBF76B3F63C870E54478EB70B3EEAD40B2DFA0F47 + E7B193D16E0C17748386A1D1802168397602FA2E70C590E5ABD1898BC19623C7 + A0DDB8F1683F7E02DA8C19875663C6B2CE78341FCD45DE88D1462DC78C479B09 + 93D0890BCC1EB3E75273D8C7420C725D6AD24E53B9001D3F096D274E41DB4953 + D166B26205E94BE56968E7321B9DE6CF47D7450BD045EEACE7CE41A73973D065 + DE3C7459B800BD57ADC090CDEE18BAD50B7D376F42AFCD5C6CFB72811FEC8FF1 + 51E19892108B91A14118E4E78B6141FE1817138969A949985F908325A5855854 + 5280B9CC4F4D4FC5B8F8582EC2E34C3A3A3A12E3E36231292901139262313131 + 8C75A2303D231A1393423131391CD333E3D836D568667602156FF273F253A864 + 038497960BFE66C1750FF37B646D9C8A25269F8595FBF2B0FA500156ECCF31B1 + 88E5567AD5FE6CACD82B8BDE7CB81DC965992C7EF3E075AE842A86FB095964C8 + 4237071E26F65B8181991E02B32733E17E34156EFBE3B0765F0CDC0E2418D8AB + 986C9BE4BA6F4F3CDCCA622DED8EC5DAD218AC656AC5A2CD3440D44AAD18B502 + 915B8E6618C94275F391742B7F321BDE02C567F3E175260F9EA76525AC98B88A + 97BB1BDB986E95F5EFE5DDC645B12063C0C5420408FC5E60BDD3B2784D81DBDE + 18ACDE1D4145624D59145696469A174CCB0B43B17E6F1C3C8EA699178C1A8762 + CBC9BAD693DA76CA02B0B676F0D8C6DA5616B0178BB15D633A9E61CE43705656 + AA06D89ECE8362F02A6F41DD0C13B7CEB86696D5EC79B6675FC6C297C7D87E82 + E7792C135B79CE46C7E5D6592E9373E1734AC0576E9B057D65C99B6700AF89D9 + 2B4B1AC7B855C7EF7C9181B5FE1A1F8FA56BB04B56D01AD319B987961B65A6BC + AE3A4F8F23C91C572676F0183E2CDB71261FFE974B8C6B68815D41523F1E4BA0 + 57A054D6B20182BE577623E85AB9290B661A729D65D74BE16FACA7CA1076650F + 420586798DFCD8E72E0161F62577D37E17F577B2AE93FA16DC3565ACB38BD745 + D037F8723982A8E02B0EF11882B47AC91E728BC7BCAE17F1871176FB10826E3A + DC5CDE3984D0FBB2BC3D8A50035F65A17B1491778EB1EE21FECDCAA098C63BF8 + DB0EBCBCC7BC88D74B79596C99D8BCF70E1BAB61415DC1D78887470DF40DBABD + BF02002B2FB79A4A05884D7D1EC7B8E7643E42D6588EE387F2D8E1F78FB3DD11 + B63960CA221E1C43D4E3E388FBF49401C872232D97CF11F7796E0F0F21FCF151 + 843D62BB473CFE7D0B42AB8E6D5D16C7F6496C9F682CCF787C8E4BF1218D3BD1 + BB878C559DDCABA6C872EEEE7E440BC60AF03E3A8C74F69B7C97F50D00CE42E8 + B95CC4DD2863D941A4B3DF9CA76791F1F02452EE1D47EEA79790FDEC3292EE9F + 45D2BD7314D3072790F1F414329F5A10368DFD653E396AE0EFEE6F2F21FFF969 + 643C3E822C96E53F3F858217E791FBC282B7898F4E738C1AB38095AC08CF1BEB + 49815F29997552BF386F4055D68BCBC812B4A5B2BFB8840CEE539CE4A81B7B2C + 17D80F0F730C82CFA790F7F929147D7591C761DBCFCF229D4AFD4CD04B3A6B2C + 200DF03320EDA2817272756BB9C1BD86CC6F04202DD02BAB5463F56BA0E41D6E + DF33C0D7C4AB151016FC6579DA57720B2DC0789BED64EDFA08F9BF7A825C81E0 + 6FEE538A6FFB1079BF78CC368FD9D703A4BDBC8B94AF042F752CF6F52B96FFEA + 9101C106F6DAC0571098C7D576D6AF1E1A65FCE23E320C70B62C7D53995AD6C0 + 0E60ACFDAC2F88AC3E2C80CCB1FCFA29F27EFBCC40E002C1DFDF3F47E11FBE40 + 3EF3F9DF7F66CA8AA9DDDF0B043F376EA20B7FF5D4B8882E16D465DDB27F7A81 + D23FBC4089637F9180EFF7CFA9CF4DDE6C53A5BF655FBF79C6EB749FE7760779 + BFE139FE5AB0FC0ED215AFF74BC5DFBD6E622ACBA576BAAC74B58FCA10FCD579 + F09C047D4D6C642A89D737FECB9B06FE26BEBC55E11A3BED25AFBDFE76543655 + F0ED2D93263EE1DF5CF05690937FDFCC97D779ED6F22F9C515243FBF687E63B2 + C43596B44C2DB7C996E5B9CA053F33BFBC8234C5C17D7E957DDE4436FF66D95F + DD619EBF8717CC33CD7979C75819CB62583034EEE17113E7D7FC7605679F5F30 + 6E9AF53F28006CAC76F5FBA66C00ACDFA681B44F05574F716CA790CAFF9FB417 + FA3FE06FF40B8E95693AD394CF1D1F48B09DAC6B0586AD0F19D837C7AEDFB465 + B12B38EB00BF82A6BC1FC5E8BEC1FB842C55ED73B5DC3BEBE38B0BC6FD730AFB + 32AEAB4DCAB13F3B635C330B002731951B675D2B416DB99B16609655703215CF + FB460CEF7971BC37C5F238D1BC2FCADABFF8DBEB2856DC618E2B857D240BB6F2 + 7EA418BAD1B775DF2DE7FD80F727DE9B62F4D189C30A38FEC909C45002C0729B + 6F7D30A3B2D3887E7C1231CC0B0AC73C568C75A52710F588F7529647F19E1DC5 + F1980F6C1C70586EB575FE111C972DDD33A378AF94EBE558DEC314ABDD76DF6F + E2C2F31EAA78F0C6B532D3A05B82A3E5C69A3658E097CF1C1BB0CAD2369CF7FB + 60A621FAC0E7C17104F13E1F7C8763BF778CCF2059E9F298BA872BCF7BB54210 + 0800EB39BD8BCF3E7D48256F16FEF274C167A73E6C523D853108BAB607BB58EE + CF3ABEE74BE07DBA907321CE7DCEED86CFF952F85C28C5CECB65F0E3F356DE4B + 94FA5FDB8B003E1F353EC982BFA566CE246DBB20CF2445E6A33ACD9FB65F645F + 7C362A9EAF1DF3D7977D2ACEEF36B6DBC671FA70BC3EDC2FB7CFDE0E002C0BE0 + 6DDCB681B007C7B799CF5777CE1537535B04838DEBE742E68BB0E9649E25CE87 + 8C1530E7389240B0E4C1B98D07E73B9B390F1104DE7422C3788ED9C879A63BE7 + 2A5BE44DE6443A3C8EA773CE956EE654FA484E1FCB79720E24AF3382C502C7F2 + 4EB3CE5811C71AEBDE359C97AE15F855DF9C73C97BCC461E631DEBCA9B8D60B0 + 00F07A59FF1AABDF446375BCA63CC6C4045E561C6EB998669900F0EAB238AC2C + 893120786D79023628D6F0C174ACDF9F06B77D69DCE6BCFA5016DC0E66720CA9 + 5821EBE0BD2958BD3FC500600159416059F85A20381E7213BDB838024B4BA2B0 + 9CC75A412DE3B117170463415E2016E4066021E7AC8B0444D95EAEA5E5467A51 + 49BC01BFF38A62315FF17C05800BC20D2C9E2F106B5C3B4762517E38E6E704B2 + 2C907D84C0B5300C2B4A224D1CE4852C9B9BEE8759C93E9893B6D358362FCA09 + C2FCAC00CCCF74284B605896D7961664ED625D5FCC4EF1E536C7462D60FF8B78 + DCC5056158521C6920B65C4BCFCA08C4BCEC50E302DA3597E5B9E1585A204BE4 + 082CE4F9CDC9F6834BE60ECCCCDA89D9397EDCDE815999DB3027D3077333A8D4 + 6D9895E88599B19E9845CD8876C78C08774C8BD8623427762B5C5376C235713B + 96C47A6169DC36B846796141C8262C8AF4C0C2184FCC89DA8C994C67B0EE8C84 + 6D9814BF1DE3E2B6637CD24E4C4EF3874B3AC79EC5EB45CD4909C0ECE45D989F + AE38C4C13C477FCC623D97647FE6039906C025D19F7DEDC0D4C8ED9814E64579 + 608A14EA85C961DBA99D1817EC8751FEB2EEF5C184707F4C8B0DC5E498104C8A + 0EC6386E8F0ED989B1AC373674A771073D60FB46F4DFBA1683BDD76198F77A0C + F1588B019B561A08DC8F69BF8D2BD17BCD62F4A586AC5F86A19B5660D0C6A518 + E2BE0263776E36E079A4BF0746047A61789017D7A15A9FEEC088901D18B0D30B + BDB76FC6506E8F4F8CC4B8E4280C8D08401F590407F9617A623CC68586A0AFA7 + 27BABBAD478FB5EBD075D92A74E01ABCDDBC85E83077113ACD9337AE45683B6B + 1EDA516D67CC466BE3B27986F1B8D572D224341B3FDEC4EEED38CB05ED67CD32 + F17B9B4D9C88B633679AED363366A0F50C17B49832138DC74D42A33113D164FC + 5434993095F949F898EBFAA663C7A125D7F1ADB9466F35719A89C3DB6CAC0572 + 65E5FBF1C889F868C8187CE8B0E6957BE78F1D2E989B98FC38341CCA7D43C7A0 + F1F07168C0BAF5078D34D6BF02C10D07737B8040F0282B2EF070B9709E80A6EC + B79162F10E1C6ED468C868EE631F23D8D7F0D13CDE2803783FA0EA0F1C6562FC + 361F331572035DB7EF30D4E93D04B57B0DC6FB3D06E0BDAEFD8D15AFC06ECD2E + FDA90106F0D6E931041FF4188A3A2CFF80AAD77D303E10183696BEFDF17EA7BE + A8D5B91F6A32B55D3C0BF4DAB0D706BE36F4ADD6BA1BDE69D9C5A48AF55BAD75 + 57546BD30535DA74458DD65D50BD853CC175C0BB4DDB1B23807765DDDBD482BE + 02BD6FCBA257EF9D2A62F9B6C25B0D5AE0E702BD0D5B40805756BE06F8D66B86 + 9FD7B700B0B1FEFD48EFAF2C2388EA7A2766DEA739AC7D1DEFD304756D4B5F03 + 6C1DD056791B029B3A0E55405DBD2F94918AC35045062BD2CF6A3630D6BEAF80 + B053A83A27FDA4667DBC61DE1952DA7EBFDE6B12FCFD89A4778F35EA39DE9DB2 + 5CA0D421BBCC84F833E2B68C818CF4BEF5958150C57B62C7BBD857EF637F58A6 + 5E25EB5F03859D6497DBB2DB55EECB96739DAA64C6A8F7DF95DFEDFF802ADE95 + 3B60AEFDFEBCA2DCEE43C0567CC16C57EED73A96DECDDB1CA2B22ADEF1535595 + 29FF63B2EBFE35FA737DBC61F3A31FE20EE2188E36156D7F6A71A80A6EA5BC43 + CECCCA9469BF0352FE445052F93FC7052B959B7676FDCA72AA57A1CAFBECBC5D + EEBCCF59CEFB9DEB317D1DAA56AD3F695B95AA389E753DFEB4FC35D9E5CEA943 + E6F86C5FA1D7C625005CEDFF5700B83204E6F66BA6C9FFA32AF533FE11ED7F22 + FE53DAFA29FFF17EFA73FE139A6DFD13F21FD6C891FF3300B83294AC2CBB4E45 + 9B1FBA19FC1DE57C43FA3155B4738CCB9439CBB1DFF9A6FAE754F16070967D23 + 67BEA29E7D5C49D74772CE53F2F72F00FC26F392331C7E4D4EC7FF21597DFFF8 + B537F59DC76C3FA89CB7255E0B733E6FDB0F345E1F3E1C2BF48ED23A7CE033D5 + C35365F683D52E574A59130007A47D97130C3D6429EBD8CA5B60B802E4D6A8FF + 27E057321055FB1C005975D54E3200D9917F350179F5D07FB5AD3CCFED5D9E1B + CB0488552E902B502AB7D1CE16C436FC15B4B5F315E0D7211B061B78EBC86BD2 + A4F4E79C64A95F0162590AABCCB8A6662A40FB566D0B2AABDE9B9C70993E2881 + 5BF3259E00B624B8ABBEEAF0F894E56ADA82BECED6C0D25B8E710A541BF84B99 + 38C99C28BE295734F53991E4445456BF72F5FC76FD26A64F7D5128B86BAC8039 + D99545B00D7A3531552A2BE21A6C6343604163E32EDA0179057BDF6BDA06359B + C962B70DAA376E896AFA5292E5EF08E87EC449F247EC53D264D8A1B73FE224B8 + 0127B90D9A70126DB9CB79B32EC72B773966BFBEAEB426D4D53F6E8B1A02B672 + C3D3A43DDE635ED6C1B564FD4BD56945B5EE88F75AB4478D161D51BB4D0FD46D + D79B653D50AB45577C20EBE10EBDF021170BB5B938A8AEBE28B9F9A9DE4CEEAA + DB1900FC51876EA8DBB6333E68DF05B53A74C6FB6D3B70E1D09E29FB6FD785E5 + EC8BAACDED9AADDBB3FF4E3C4E17C7B12D005CA3653BD46EDF19753B2AFE30C7 + 48D56EC53135E98C3A8DBAA2D6C79D51B3118FDDA80DAF7B6BD46CDA0E753E61 + FF9FF098CD58A7456734EED61F1DB8006BCFC5DD278347A251BF21683A7804DA + 72D1D79E0BC0165C80351D3C9452F950B41C31921A85C68386A2E180C1F878E8 + 70341F3D066DC64D409BF113D16EE2647498320D9DA85EB3E6A0FFFC85E83973 + 36DA739FEAB49B30196DB9E06C3D510BCC296841B564DD56FAF2980BD74E73E7 + A2BD8B8B63D1EAC2B2D95CF8CE43D7254BD16DF92AB459C845F0D2C518BED317 + D3E2633135361663B878EEEFB30D83FC766054582886870463183581FB6765A6 + 634A6A12464486616070100606056278443846474763425C3CA6A5A460725212 + C6C644512198901081F109A1181D13887109219892160397DC44B8E42418183C + 2B3B0173F35330AF308D65892696F0B2F26C2CDF978DA5E59958C1FCCA7D3970 + 3596C139587FB4145BCEECC3A653E5D878A2141B8E9760F5816C18B7D07BD2B0 + 5CB1C376EB25952C2E32B1F9780EDC8F65C3FDA8E0A6AC700BB1551058E0F678 + 16B61C4937F1D216E70561516E2056974661F361CB85B0975EC6ED4DC0BA9268 + ACCC0F856B56201667FAC33537186B76C760F3C11478B0FD46B9D22B8EC28AC2 + 70B895C7C39DE59B0F592076F321C571533DEB25DFF6B379F0FEFFB2F7975172 + 5CE9B636DA665BB658B298999999C1922C6666A6AA5231A91824958A99410562 + 666666B06594A1BBDDB07BEF73EEFD37EF9C2B33CAE9EA92EDFECEEE6F9C73C6 + FD31C76288C8CCC858F1C4FB2EC1DF93B2AECD816D5FDA5DF0B9580C9FCBC5F0 + BB5A0CFF4BF908389B09FF3319F03E95C679C4635D712456E607739C20ACDE15 + 66D21B766F375A57B40DEB0A992ED9818D7BA2A91803AF0D90E6F83A1EA78309 + 70399C687BD8C8E3F63AAEFE73E07F36D758D67A1C4F336057EE959517A807BA + 829C2CF315E014103E21B89A6E40AF3FD301ACE7CFE3F016E0555BCA9DC7E9B4 + 3B0E2EFB12E07E38C5ECD1EB294B5CEDC7CBD0EB7816FBC884CFC92CF8C942F6 + 628181BBAE4792E07C200EEE4752E07B269B73135C15F0B581DF409D93F39CEB + 0941E8441E0B8FE720CFF3BE38381D8887C7B1646C3D9385409ED300B60BB95C + 88605922F138822E69AFDD4203ACD5A7EFB97CFBC36A8E2D0B681EA7FF39B967 + E618CC93FB67FF8B793C07F908BD5484C82BBB4DB895730A601F418CFBC9D536 + D302CB3E1C53121816B85699EA84E841FBA5DD08BEBC0791370E609BC0E98D7D + A50058565872D52C3798A12C97A56DF8DDA306B6EA017CD81D3D78D7437CB947 + 3DCAF6878DA596EF855DF03EC331D87FE4CDFDD87EE750A904806D7BF4CA15E7 + 1E0300C2EFEC4788C0EF8D3D26D47E8C41D74AE07785E784DF37590687B14DF8 + 754A2115A9B9DC16B8950E9B39697E72B919CEB9C95D6708FB0EBB7B10813777 + 23E05A31FBDD8D88BB6CFB80F3787C0C118F8E21FC0125D7A606721CC18E7B36 + 97A93A1772691DF754C0E7B8812F82D80236823B823F665FCD47DA27F418E21F + 1D41DCC3C34878701889F70F234ED0E37A0922F879C99DB3AC8293EEB3ADE0EE + FDE3487D7812298F4E23E3F905643CBB82B4A75799BE82C47BE7107FFF0492D8 + 6FDAD3B3487F7E16A9B27CBC7710B177F6B1CE1164BF3885CCE727D8EE04B23F + 631F72ED6C6094409BDCCC9E43CCE3B388797406310F4F23F6B14DB244944562 + 02E71F6FC0D849CEFF38C363487EC6E37C78044117F8BD399E8A1D374A38D651 + A43F3E8AACA72790FFF959147C790169CFD986C71D2DF8C373A536090270CFCF + 18906C409C817102C2B2EE94EB60DB9EAEF19FD9F6F995A56AE2CBEB887D7E09 + 319C6BEC53ED0D7BCD58049B7D6BEDFBD9A67D7D1BE946CCFBFA8101BD5994AC + 7DD3BF1604663D4AEEA193BFBC4DDD42D297B63D8165299CFDDD4364FF2838FB + 10E9DFDB20B05C364B02BF82A129B236FEEE0E52BF17D0BDC5B682CD6C5F0A8A + 6D4AA1525FA9ED8352299DAAF07B41E687A596C002C0597F7A81F41F9E32EF29 + F39E731E4F91236BE01F59FEFD6364F038325EF178BE7F84BC3F3F45C14FCF91 + F7A7E7C8675BC9C4D9CEC0E0EF9F20876D725837FB3B8EC17602C019DFDD46D6 + 0FF7797C3C0FAF6EF19CDE40DC8B2BB6F3FAECA2D9573995C797C2F329B86DF6 + 36A6047E65E16B81DE1801607E36D19FCBF5B3E030C5FC24ED5D6BB7E0CEFCF2 + 1A8AFEF400791C335A6EDEF97B13088E7FC2EFE817578D3570D2CBABFC9C2F18 + 6B5D0B00C73F3E8E28FEEEE53E58D6AC82B529FCAE64BCBCC87672037DC5BC20 + 90F6D9357E7F2E23854A7ECE715F5C451AE793F5D575E3525CAE8975ED502828 + 2B6B627DD76CFBD51E33A057C057DFC39FA1EF719367F6EB6528781CCDEF74CC + E3C30C0F61277F4B3B1F31FE98C7C2EFB981A2F7F8BDE6F75BBF0FCB5DBAAC96 + E5DA59D0D9E6765A2EA76D803889C723D7EDE1BC96875C2E322E8705816DD6BB + 7A0183F5B57F311523AB5ED69725B3A0A9AE3572716C5E26E17109001BE0CB50 + F0577BF7AA9EF2A25877FB6D5E53F4D28C19AF18DB79FD8C679D78BDE8C2BC68 + 96C7EB77C9EB44EC8383EC53D7E5FDC6CA56005896BABA66ED547D037779DD63 + 7D815541D4C87B82BCBC2E322EEF0ABA7E6A0F765DB723757DE5B52BE21E3F7B + 7EEEDB1F289475F0716325AC63D635528057C7A3977C8C55AEF92FD9CBF67AE1 + E780CDD382AEE12ABBB50F4182AF82BDB7E44EF900D3B650B2F6D2B5E9104238 + 8FE09B87112CE07BEF1802F9FF13C0FF9F209605F2DA2C682C57CDB20C56A8FF + 13BDC464FEC338A6B625F0E37FA6CF39FE079A97AFE4CA9AF734FC5FF43CC5FB + 07FEF7FAF1BFCB5740F6B4B69AC867A8AD2CF2E04E7919082CF17EE87CA101C2 + 82C15BF93FE7C7CFDE8BFFBD1EFCAFF6D63D83D202B6BC3FD03D9D1BFF0F5D25 + FEF7BA6B3C59FBB23F0FC6DD9867BCACB0AD2C7DDD383757FEEF6FE13C95AF74 + A92B68CAE5EC2E389D1204B6435FCE7523EF5D36F1180485D71FCDC03ADE536E + E07D8F01C0BCEF913650EB0F276303EF35B5FFEF1ADE776DE0FD8CD349D6E3FD + C97ADEC769AB11C1E0CDC752386FDD8FDA3DAE582F24B2FEE6A3C9667B11038F + 594FDB95AC3F9080B56CBB86F749AB0F256225FB5ECDFBCBB5ACBF86E915FB63 + B142AEA05967DD21D6130CDE1F870D02C20CB5F7EF822C59CB6EC5A2EC1073CF + BBB684F7C9453B59B613AB185FBB271E1BF671FE7B93B16E8F2030C5F89ABDB2 + 004E60BD582C2B54DD38ACDE93605C406BDFDD25EC6B69491496ED8E365A62DF + AB7879E10E637D2C17D6AB049A0BB661695E04C70F355A28F1BE7A614E3816B3 + EEA25D72F7CC79CA2AD9EC2D2C97D161982B17CC198198A77B70B65F92B70D8B + 73C3B07C5724FBDE693CF4E83E7E3DEFC3E5C25A7B1A2FE03DBAF6DD955BE6E5 + 02B84CCFE7B12F300AE03D7C2016F19E7F31CFC992EC20C60330274556C3DE98 + 95E48379E9FE5C1304D9C4792EC98FC4A2DC70CC490FC2EC349EC7D420CC4CF0 + C3F4381FB6D98A79EC7F36FB9E91EA8369C99E9892EC8EC949AEF834DE19E3A2 + 37E2D3A88D981CBD19D3635C302D7A0BA6ED949C3165C7264CD9B619D3225D30 + 6B871B16C4796351822FA6876DC624FF559815B2110B58B620C2058B76B8637E + 941B66EFD882593BDD312BC61B3313FD3135C11F93A8A9E9219891118659293C + 4FE911589E1385A539DBB134773BD7255A43C4F35C44F15823B1202D1C739343 + 318F9A9FC236F14198BA4356C0DEF834DC15E3435D303E640B3E0976C3D8604F + 7C12E287716101F824D41F63C3FC31318A63C5F35CE7A660755136CF693C2647 + 85E2D348AEF342B86E0C70C588AD5B30D2D705C33C3761A8EB7A0C77DF486DC0 + 488F0D18E1BE1E439C5662C8A61518E9BC1AC337AFC4C0F58BD17FC3120C755B + 8361DE1B31D0633D86FABB6054A8178604B861A09F2B060778A1BF9F3BFAFA32 + BDD50B7DFD3D3020C4078339AF3EBE1EE8E9E98ADE1E6EE8E9E28CCEEBD7A3E3 + CA55E8BC6C253A2E5A86B67316A2CDCCF968CFB0FDF479683E69065A4D9B8D8E + B317A1CD9459661BA646A33E316ACAF5B5B5055313AEB39B8E1B4F4D4013862D + 264F46ABA9D34CD87CD25434F9748A0DFA8E1E8FFA5CE7D71B3501B5868F45DD + 1123D170C428341AA1BD7A3F41FD61635167E868037DEB0FFD0475068D36326E + 98B507EF6059FDCAFA971A3C060D868C3110B7CE40B96B1E6EAC766BF415B01D + CE3A63D178C478D41B34C658E956EB3D18B5078D3400B9C9C87168C67934E478 + B5070C43CDBE2C63586FC808B69335F1088E3BC2805F01E02A3D07A24A8F81A8 + DA7D202AC95DB3206ED701904BE7EA3DD977B701A82C80DBB91FAA77B1C5B587 + AFB102EE2C69DF5EB96FEE655C3857EFD417359857A3AB5C3CB3BF76BD505156 + BD54A5363F5BF9DA64DBE3D7A85557546821E0DB05150DF4ED880AD48752D30E + F8A0513BBCDFA035DEA33EA0DEAF27F0DB92A11DF4CAAA9712E8B5B976B6E5BD + 53AB29DEA923C86BB3F47DAB66238A71E65B56C07AC6F64E2D9B818479D656BB + 89797EF64E75DBF334EB999A00B060B0D202BB06EEEA99A0CA996F59FF960260 + 3BD415E8D5D67182C0EFD925C312B3359D797E68015FDBB344B3159D9E432A2E + 005C59E0976145FB33C632325BDB7D4855A8FE0B393E5B2D7DBE6A77EFACD0F1 + 79ABB6DD2B05C0F67CC7F656BAACAC72EBF9AC55F7D700B063BA6C99A3D49763 + 1F964C999E4D9BE7DF0ECFC87F8F7E0500EB99BBC5170C63706C67AFF3A6C3B3 + 7C4716E118FFC5337CBBCACB7B9DACBAFFAA5ED75E6CC2C40D77B0B307538F52 + 9E9886BDADADBDF2C48B1C99951DF496E157A5796FDA80E41F0424AD782917B4 + D771A8EB98FE051C2D23CDE19F54B65F2B6EE53B969527C73A8E6D7E45E51E8F + 63BC3C596DEDFACDFA96CAD433633BA4CB55F990F6BF5FBF74014D19082C7DC0 + 894882C0967EE701FF5F251DF3EB64AF238AFF96057FF9E313647C8B3F3E87D0 + 518E80B714E03AC8B1EEEB64D52B6DF73B20E4FFAA1C2F2ABFA5D2768EF33217 + A75FCEB5BCB6E5C9FC31FC8604581D01EE1FDEB1C4F2D250757961D705FE1D9B + 142F57F6B17F4BA5C76757D9FC5FD455C83F9F370578195A32C7A0F03D96DBA1 + AFFECC1CC1AFA0AD01C46C6BDC3C0BC0EA98D9EE2DB679E3BD8ACCE7F782E11B + EF7DC87815FB0D80FEC4F5E76BFF23D71FA56E0EEC6F90296EDE24E39FFE2FE1 + 2F6F5298FFF30D88BD9EBD3F136A6EF67E4AA12E430BA8FE7C23A1F63C1EFB0D + 8B556E60AD01A5B2FEB5016023E5F186CB02AA65AD7FADB8EA9A37E524E61B55 + 619DAAB5F17675DDD0D980ADDC31CB52F82DEA0D85BA993320976986CAB795B1 + 3FFB18C6829A3769AA23B8FBAE60AD6E10ED7D3A02609509565BC0DA1C0F65C0 + B67DAC3758EF4DF5C11B4AF386614DB972964B9A06E60DC50ABCB115D0FD90AA + 20EB5E0158B97AB6D737AEA46BF1A69665A60E655C3EB3DD87759BA1628316A8 + 2417D2F55B98B4A4B2F7644D2CA0AC1B5EB37731C7ACC650AA5E0FEF7CCC5070 + 9A73B1A4F4FBB51B19BDC79B69238169DE505B40B842BDE6A8D8B0152A366A6D + C2CA4D5AA34AB3B6A8D4B40D3E6AD2C6B885AEC64540E566EDF051A356F8A871 + 2B5469DEDE8062B5B1B9A6E63C1B72CE6C5B99EDAA3697156E1B5461DCB8A86E + D58EEDDBB09CFDB3EF1A5C407CDCA6336A511FB7E9849A6D3A9A7D8CEB75EA86 + 3A1DBBA246FB8EA8D29A6D5AB36E87CEA8D7BD17EA76EF891A9D3AA392FA6AD2 + 16551AB7E37C5B73FE2DF0417DED736CEBBB0AE759A5597B13566658BD751734 + E8DE0F4DFA0E41632ECA1A74EF8F7A5C08D5E9D217757BF447C3DE03D1A00FF3 + 7AF6415DAA619F0168D0AB1F6A76E9C9C5550FD4EED5178D060F41732E1E9B0F + 1F8926438619B5E042B2D51BBC730000FFF4494441540317A45D274D41A70913 + D14696C3CC6BC9C56A4B2EF69A8FFAD42CFA9A8F951BAAC968FAC964B4F8749A + 59D8B69EAA379C67A115D5928BDE1693B9F09D3E076D67CE43AB9973D16EFE22 + F4DFE88431BE5CBCFB066280932B7AACDD887E2E6E18ECE58B3E6E9EE8B98571 + FFAD98B4331A6323B7A39FB70FE58B91A111F864DB0E8CDBBE13E3B645E393C8 + 288C0DDF81D1A1915CB847605840108605B2DFF030968763FCF670F6B18D8AC0 + F4F8ED66FFE1A5F9A9D8B87F1756166561516E3256946462CDDE5C2C2BCEC092 + 22C1DD14CC48DF89E9693B194FC5CA3DD958B5370B2B8A53B13027163393C330 + 253680FD05604E721016A60563797638D614EC80C7A114845FD88540B930DE1D + 05A7C26D702ADE01973D31703D90884D25B15891158605495BB120D91F2B3243 + B06E5714D6154561BD60EEDE58ACDEB50D73D2FC3123C99BE156F38049D6132E + 0753E0B43FC958042F61BB15B911D8BC371ECEFB13B1A12406EB0AD94711FBE0 + 184EFB6C6E895D8FFCBC6FADEFD93CB81D4F875C251B77BE17F2E07B5E803203 + 3EA75447203A0E1BF76EC3AABC406A2B9CF644C2FD50345CF66D830BF3DD0F46 + C3FD400C9CF7ECC0E6DD3CAE7DB1D8C2E37216F8E598CE3AC67DD1585F12C963 + 89C29683713CEE789E9704F81C4B42E0E90C04CBDAF8641A7C8F26C1F75832FC + 8EA720E054BA2953BED23E2CF33A9C04CF030970E739F1DE178720CE3DE0781A + BC0FC6C36D7F2CFB8C8727FB75DB1F07378EE1753419BE27D8FE14FBE1184167 + B311723E1B41E732D9771AFC4F2623F04C06FCCFA4C3E3481C8F2B167E275211 + 60C6E539389602AF23B2E6CD44C0857CF89DC9B641659E2FE3FAF9685AE9B9DC + 2AE87A2E1721E7B211CCF3177A41E3E4B28D2CA9B3107249FBF0327D3A13DE82 + DCAC17785156BC9970E798EEC752E1A573C0BA5B2FE62290F503F9790850875E + 2E323250F9F22E4A56C582BD72812D8BE05C930E94E593DA5C2D82F63B0CBDB1 + DB5844055FDF6DB7D0129C95CB4D591A1723D4584869DFC47D665FDDF0BBACF3 + F018B6DD3F62A47D7465BD25181B75EFA80131B2D20BBE566CE62277A47177B4 + 1FE511EC94D5D7F53DD87143FBEBCACAB4046102261C2BF44A896D3C3DA8677F + 82CF8A9BFD800512ECD04070C5DA0358E064DB1DCD81E298B2603320E3EE6193 + B65907EF4710C79225B1428166B981DECEF9443D3C610719B278B3C5773C3A61 + 24B8211812F5E814629FC97AF60CA21FCA0DEC71C8F2511689094F64C127D7B3 + 1791F9E525C88DB26097F6DD94D5B05CBAEED031DFDCCBF821243D3D8964C123 + 019E07C790F898E9E7B6FD46539E9F47DA0BF62100F6FC0275CE5837260B3E29 + 7C7E86ED8E1AF0B393E742D029F5F969A4C97533DB1B4B48B9C2E53C93ED6E67 + A31F9EE6E7711AB154DC431EC3FD33887F7896F390A5EF592408C63DE43CEFEE + 659F7B107B6F2FA26EF373BB9883C0B359C68A30F1D131A43C3A8934F66DF484 + 633CB2ED052A0BE91D76B0A73D80CD9C05975947568C493C4F82779902B2CF2F + 22FE892C1F654D7A8DC77C0D09CFAF21EEE90503D7A31F1C611B016A9E8BCFAF + 22EDAB9B06FA1A80FBFD6364BD7A849CEF9F208BF1B46FEF21FD95DC1FDF45C6 + B772697C0D495F5C42CA975718BF8A946FA46BAC7713D93FDC45DE9F1E22F7A7 + C7C8FEA3F6E4B5C15FB9834EFBDE1E97F52FFB4A7925EB58B978BEC9F602C872 + 432D97D3F7912E0BE3570FD9FE11E7609B47C6F7DAFFF7A101C069DF3F44DAAB + 0766DFDFEC1F9E20FBC767C8FCEE0952BFD19EC4CCFBFE19727E7CCA76EC43F5 + D84EE05769C505A973047AED1200B60161B6FB81C7AFFAE65C28BC6F2CA5334D + 789FE74056D18ADF33105C7BFC26EB9C532902E1B25016BC96A5B380F73702BC + 3711FFA5DDCAD702C22F6F20C940F43B48F8FC06E29E5F463CBFDBB2F24DFFF2 + 2A32BFBA867C9ECF9C57B78C1BE3E8FB72B5CCCFF4D945680F5F7DCE49CFAFF0 + 33E467FAF8ACB1F04DA5F4F9CA05B33EE3F807879070FF8071619EFAF810D29F + 1F47EAD3A3487C7810C9FC2EA6F2F791C136195F5C45FA4B7E0F5EDAF60916F0 + 95CB6501608155BD5C90FC82DFF3A7A7CC35472F6308FA5A7BF15A10D8924070 + 94AE5D540C7F477102BE0F3927C6A5D8C7B6FD6EB5EFED367E9F65FD1AABDF2A + BFCFB1FCADC4CB52D95842DBDD603FB3B9364FA564AD1BC56B8D5E74312FA85C + DBCF791EE67550631EE3B1F39AC06B8B2C86A3EE31CD6B48347F1FD2365E6322 + EE08BCCA229779EC6BA7E02F7F4F3B3817CD234AD7D40772AD7FC858CD6A3F5C + FDEECC8B35BC0EEEE0B53792E3865CD235B718DB6EEC3756C202EDB288D66F51 + D7646371AB6B26AF49C69302C308B60D633CECA6ED651A41DE08861172AD2F00 + AB176FA8889BAAABEBAB80B1AE91A7CD3553B058D74ADB7F01DBCB4A59E750D7 + 4FB98BE6B8C6BB02C7083192E708BD48C4BE38B6B60F90456F90802FF3826F1D + E6F59ED7EE3B474D2885F19C85F11C06B12CE0FA41EA10021997F5EF56B633BA + BE1FFED72CB7CA7B8C02D5AFA032A5AD04C279EC2137F74056BEDA4E42DB4CC8 + DAD778C5389B6BBC88289425B02C876521EC7BB1103EB2083E27F84B9DCF8737 + DB79F3BFD4FB7C01F3F34DB9A0B0E7B91CB8CB138A74DAB62D85CF79D6655B4F + 037DF3E0C27CC96CAB71467BF66ABB8D5C9B05EFA96C6C66992C789DCEE4319E + 8B4DF67CA7D33970661F4E6CE74CB99C2D605E9E81BD02BFD286E399266DF24E + 66D92030EF4DD6F11EA4544752B186F7236B0E26534958252829487C221DEB79 + 7FB1FE680A361D4B3340D899F7572EBCAF71E63D94D331BB785FE2C4FB9ECD82 + CAB2F0E5BDD526A63771FCF51C6B2DFB5E7F9C631C4AC4D23DD1462B79EFB5FA + 7002561C88C5F2FD315875200EAB790FB692E995FBEC69DE132E2B96956E0496 + 52AB78FFBB9AF7C2AB76C7F03E3B1E6B789FB89A61A94AE2782F1E839572F55C + B8134BE4F699F7C44605DB19DAD24B788FBC8CF7C8CB388F657B634CB8740FEB + 0B08F3DE77C59E58F6CFB914C5F11E9F732D92079B242CCBDF89A579AC43CD4D + 0D36D6B9C63A395F3038108B7202B80E08C2626A41A63FE667F8613EC3453941 + AC178AE5ACAB70C5AE08ACE5716CE2FDA65EB6FCD985F64E6ABBF188B389735A + 57BC0D2BF242B08C7D2FCF0D641880C5D95BB1887D2E64DF4B99B79C632FC8F4 + 3356BAB393BD382F2FCC49F1E17AC21BF3D2FDB0243784F5424C5C90782ECB66 + 27C992D6035363DD30235E16C75E9899E485E9091E9812E38249511B317187B4 + 099F466EC0E4A8CD06FCCE8C75C7DC042FCC607CEA0E27CC8876C1FC444F2C49 + F3C3C264F6B9D319134256637CD04A4C8BD88099919B3023623366EF70C59C28 + B68DF6C4C2045F2C4AE49A2521100B1283B1282D1CCBF3F8596447615E720866 + 27681D14C134CF738E3C17F1B3D8C5EF414E141667F1BB90B9DD940B002F4C8B + C4BC9408CC880BE67184606A4C203EDDE6834F22BC3126CC93EB380F7C12EE83 + 89518198B06D2BC686F9627CE4564C89E6E795118B55055CA365C4617A741826 + 47066262B8BFB11A1E1FE283D1721DEDBE09C3DC3662B49713C6FA729DE9BD11 + 233DD662A8F34A0CDDBC02A35DD76184F36A0CDEB4020337AE44DFF5CBD163CD + 52F4DBB406C37DB6606CA02F3E09D98A91413E18EAEF85413EEE18BCD50B83FD + BCD1C3D519BD3C5D31D0DF0703FC7CD0C7CB03DD9D9CD075C306745CB30E1D57 + ADE55A7603BAAF5C6B207027AADB9215E8B26089B1066E3D6B1E3ACC5968D6C6 + 8DC64C443D59E58E908B66AEA3C74E425381D4919F5063D178F438D6198F4663 + C7A3C9B84FD198EBF1865493F153D18C6BED465C73D71DF1296A0C1987EA43C7 + A2CE88B1A8377C34FB1BCBF6E30CFCAD3564246A0F1E858F070E2FB5FAD59EBB + 02BA8ACB025756BD75FB8F40BD0123517FF068D4D35EBC02C4DA8BB7DF50D4EA + 3B14B5E5E6B9FF3013AFD17B103EEE3BC494D767DF0D398E547FF048B6639D7E + 4350BBFF50F63B8C52389CED87A3A6F6E9ED391855BA0D40A5CEFD50A9631F7C + D4A117D5DBC0DE1ADD06E2E31E8351B3FB20B34F6FF54EFDF071978126ACDA41 + A0B79FC917FCADD25E16BD02BCDD50B95D0F03836B76E9831A9DFA98F4877A79 + 9FAADC5AFBF8B25EABAEF8A845677CD0AC3D2AC8B2B75947C8DD7345EAC3168C + 33BF62B376CC6FC7783B7CD8B8AD81BD02BDD27B7A0E53CF6EE95BA7B91DE236 + C17B1F37C1FB0C25C5DFA9D5D8AE8678BBB60C161AE16DBB047DF59CCB3CEBAA + C9748D06A59057CFCF2CA0AB3C415F237B1D9327C05BF59700D81104BFC37C6D + 35275916BEB204B659FEDA2D7A2B7E0C199B943E872C959E4D5206FAAA8E0DF2 + EAD9A279CE28314FA1798E699EA7DAC1AB832C886AC911AC5A71C73C038125D3 + 27EB50E5D57594CA4D5B7B1BC98C5F06D85A2A2D738C3BCACA372A536697CD73 + A50DDEFE02E29623C73A8EF57E4EDB9FB5EBB938430BE4968A75F5ECDDE8DD6A + 784BCFD24D9C7DD89FD14B56BE639EA5B2757F8F1CF9C0EF956584565E9991C5 + 7EDEAE640C0DDF144330B0572057DE67D55E751917DC3530F723BCA5FA26FDA1 + 91F2947ED3B473645FE5E8F742CFD7C88C5546165035E058FD5B6358712BFD7B + 545EDBD7A96CDBDF52D9B6BFB78FB260FDF7B4FB0594FD37AA1400FFE17DFC81 + 3221D306001B08EC08802D953361475927C951567ED9BAFF5B4BF32DEFF82973 + 6EECC7264B5EBB35AF0568DF7C9B17097BFC9F55060097B1042ECDFF0DFDA29E + 2368FD37E97FF9A2F6DF0D80ED177F237B9EB9F0DBF58B32873A56BE057A4BEB + 955179F37094F5F6D02F8ED1E198AC3F1D2B6DF21CF21DA53203A9DFE3E76A00 + B0ED0F4DE0F70DC1DF7725FEC10958BF530D6FBF5F03EF55F818EFCA7A97C724 + 57E37F78E703BCF3C147A85CFD6354AE5913EF7EC431791CC622D8FE67EEF847 + 6FFDC92B34B0963724B6D0266305AB1B00EB8FDADED6B10FABBDA3D487057215 + 2F3BAE545A87374E06E66A4CAB0FC555CE3247F05BAED43FDB487FE0FC0DC0B5 + C35F814C01D4F73FB681D6776AD8DCBFE88DBF377943F7872AB56D62DD378DE5 + 2BC57A6F57AF87F798FF8EB17CB61D8FA35B6A590FFF1300667EA9B532E7FCAE + 7D6EEF31D471CACAD8B88A366DD807C730F05773A36489AB3D7CDFAF2B082CA0 + AB90E98F7503AB9B50DD8CB27FB6795F758D25AF0DCC7E20205CA7093EAADBD4 + 48F10ABC21163036EDCD4DB0AD8F77B457B2DE58E41CCDDB8BBAC9B5CFE37DF6 + 257DC0BE2DE9DC9939EABCA80F8DA97CB3DFB020B07D9F15E6C9ED74A526ADCD + 3EC2EFD7E78D7DDD66F8A8614BE3725AAEA68DBBE986CD8D147F8FC7F7417DCE + B7610BD3466D2B09F4325EAD591BD468D10ED55B0AD8B632EEA92B3668CE786B + 5417086EDE0E355B090677429D0E5DD0B04B0F34EED11B4D7AF541831EBD50AF + 674F34E9D71FAD870E438BA1C3D1A0FF00D4EED60335DB754655F6FB51E3965C + 88B43050BA62538D2B17D49C03E3559AB73110B82A173302D8B5DA75E3183D51 + 8B0BA21A5CE8546BDED1C0E1DA9D7AA04E57E677EA8E8F3B7543AD8EDA7BB82B + AACBEA9873AAD9B91B6A77E75C7AF74383BEFD519F61BDDE7DD1A8FF2034E79C + DA8C1C83D6DA3378F01033BF865CE03518340AF5B870ACC78564A3915C908E9A + 88C6A3B8781D3305CDC7CD40F30933B9489D81C6E3B4489D8AC6E3A7A3D9C499 + E6EDE776B317A01317C35DB830EEB1740D7A2E5F67C22E8B57A313D565F97A74 + 5DE3841E9BB8C8F6F0C5D890488C09D986015E5BD1DF53AEBF2230821A1E18CE + 057908F30330C83310833D8330726B0486F90463D4D6704C088B6218C27C2F8C + F0F5C398AD7E98181E8819D111989F1C8DF5C539589997860569715892938CE5 + BBD2B1243F15F3721231233D16E363C329ED23168779B9899893118D6909A118 + BF83730A73C7786A5A940F16240562657A08158C65C9FED852B81D11A732117C + 28011BD27CB03CDA19ABE2DDB13623109B0B776043DE76AC480BC692A4AD589C + E067DEBE5F9CE48779895E9897EA67DCE52D2F8CC4AC8C00CCCE0AC4D2E2ED58 + B34FFBEDA6C2F5480636CB5D9EDC44176CC7FAC228037B3795C460752EDB6586 + 62754E0436E4EFC0C6C29DD8C0F275BB76C069772CBC8FC98A3617CEFBE3B16A + 572436ECD9096781DCFDD1D8723806AEC7E2E072241A1BF745626D511056E5F9 + 634DAE1F361785C0654F3836EE0AC03A01E1E208B8EE8BA27652B1703B9808B7 + 43C970DA9FC8B9317E38055B0EC560E3EE306C280AC36681E0E230389784C27D + DF366C3D9680E05329F0651D67951704C3695708B6B05FB73DDBE1BE77075C19 + 5A718F92EDD8521006B75D11083A9C88C063C9F03D180BCFFD3BE17530065B4F + 2423E8741A7C4F24C1EF640A02CFA423E0742A422F6421FC720EC22E66304C47 + C4A534049D4D40C0A9649E876478F178BD8FC6C3EF7822FC8E2562EBF114F81F + E3BC8EA7C3E794F6DBCD316EAB3D8E671A8B2259166D3D5F007FB959BE5C627B + 407F3E0FA1E705B405B69310CC79049C49C3562AF83CBF039C83E6E471241EBE + 82CF4C075C906592F634965BEB74F89DCF42E065415C8D21A82BB82B109C8F10 + C1DF4B05083650B8D08472D12D281C7EA30461377753C508BFBD07E177F721E0 + 4631BC2EE6C09F7D85DD9195962C6D8B117CB50821574B6CEE3B6F1F34FBF5CA + 2A375CF0F4A1ACC4F4A05FAE410FD9F694BC73D058AB19D8F3E008226EEE41E4 + F512C4DCDC87F85B07107FE79089EF609FB137F620E1CE01A6F79A74C4A542E3 + C23AE84281B1D032FBFCDE3E806D7217FAE818760894A84F032C982F10A2322A + F2AEDC91522C0FBB270B35B9933E6214CA31436E0B2A1C30F91102139C7B04DB + 09D06CBB739C7D1D67FB13D8769F633C3CC5F14E63C79333887A7296F153887A + 760E3B9F9D47B4AC6A5F5C44E26782405791F8FC12129E5E60FC0A32042BBF94 + A5A4AC5FE52ED6E62A56D68E022F3B3876F4C363D0BE9E09768B42D5D31EBA29 + DA67F4E5459B184FFBE212B4576AFA979791F692633CD3FEA102A3EAEF94B14C + B4AC0BD597DAA94DF26772A7AB3D79B5AFE879C4F09CEDE471C53D149065DEE3 + 7388BF770671774E23E9D119A43E615BCE2F99E723FEDE5E68FFE2E83B25D879 + BB0851B78A8DFBD8983BFB90F0F008921F9F324A7C7802F10F8E1BD7B4DA9354 + 61ECFDE3C6EA3899F34CD3DEAE3C5F2A8BBA73983A8A94E71791F9C535E60B98 + F3583FBB86F4CF6F20F9F935CEF39AB1F64C7D7112894F0E21EE916D6FD3F8C7 + 67782C9791F4D90DA47C7117492FEF22F5EB07C8FDE367C8FBF367C6D572FA77 + DAEF57FB09DF44DA5782BF6791F1F579A47EC339BCE267F4DD55A4BEE267F3DD + 75E4FCE91EF2FFFA08F97FD2BEB90F4CDB4C59EAB29F8C1F1E1A082C17D1B2F8 + 4D15047E751BC902CB5FDDE6B9BD43DD43C637B2BA7D82EC574F91F3FD33F6F3 + C4B8964EFEE61E92BF663BED3F6C87BAB972D1FCE30B647FF7145982BFDF692F + DF17C6B573A66031DBA8AE2C8173E4365A5097F3110016F0952C009CFBC72706 + F4DAF63FBE8BCCEF39F7EFEF30EF367238F7EC578F8C2574D62B1B284F9765F4 + B74C1BB02CF82C807DB75466FF652AF9DB5B48FCC6E6DE3A85FD9AF22F6FF3BB + 7C8FDFA95B887B7619D18FCF23F6D905247DCECFE8CBAB3C0F5758C6EF25437D + DF047F53F9FDD7FEBD92DC38273CB984C4A7DAEBF7260A784CB93C9F89FA0E3F + 398984C74791747F1FE26EECC2F64B19D87E311D515732107D2D0B7137F39178 + BB98E50791F2F404BF4BB63D83059065692B57C9FA1DC9125516C0D66F4856C6 + EA5BBF3963812E37E876006C5C3853DA0F5896B361D7769BBD6677CA3A9FDF7B + E55BED65712B881CC3EFF90EFDEECD8B1F3CFEE71710CD6B410C7FFF8AEB0586 + E41757381ECF8BF9ADF1FAC0EBC60E41D17BC7B183D71469FB6DC15FED3D7CD2 + B8649776DEE7B5EC8E5E9C38C9EB81AC81CFF2BA769AD7A31388E0EF28F2C149 + 44F2B7B45DC7CA79C80DB320AAAE59BAE69A3DD679CD8DD60B13FCFD9B3D8859 + 4796C2DB799DD33EB8C157F720E49ADC2E1FE475422FE4F077AA6361BFD17AC9 + 85BF4B796808BBBECF28F4DA7E5E73E5E94169EDA3AB7DD60FB2FD21037CC379 + ED0C637DEDE91E714B2FD61CE7B5553A66AC7043783CE19C631815228F100C43 + 792CC1BC2687DC3DC6B21306E0AA2C90730CE4E717C4CF3198E960CE4516C6C1 + FC6C02395620FB0BE2352388ED82057C19060A04F3FC04F33AED7BFD00BC3957 + DF6B82BE07B195F317F455E8C7F9FB69EB01D6F155FAC63E6CBD69EF937D07F0 + 18F412903C4AF808005FC8870FE5CFFF9E406DA970515B30E4190F1BC17A918A + FF4B41578A0C041630F6557DFEBF5A6D252FCAE35C2E3C29856EDA8242D28B7C + 47528DA5ACF6F4F73ECBF2B33970399901276D93C1FB0437DE17B852CE67F30D + E8DD70320B6B8FA55399D8703C071B4EE4613DC3F5C7B299B601DE8D277E8E3B + 9FC92BB5FA15F85DCF7B358556DACA5B7B341DEBCA68AD81C202BEF63627D9EF + 890CE6A572CC74039C5D385F671ECBA61399D8CC396DE65C9C4E30EF541ECBF2 + CC7EC3B63D86D54F9A99E7DAA33C06F6BB81C7B8EE683256CAE2F7402C561F49 + 607E9281C046BCCF5D2B2B61C697ED8FC1D2BD3BB17C6F349697ECC48ADD3B8D + 4B695905AFE5BDE26AED037C505BA7A461F5FE24ACDA97C07BDB44ACDCC3BE4B + A2B192F7B1CB78CFBA74D7762C2B8AC20AC15E864B64F5CBBCE54CAFDA17CFB9 + C46305FB5B610167CAF4218BDC3D0958B93B09CB8B12B1AA24056B4BD2B03C3F + 0E4B0BB4C7701C166687F33E5B2F5506615EBA2766A7B8625E9A3BD7011E5898 + E68905A91E589CE1852599BC2FCFF2C7D2EC002C131C66FD05995BB1308BF7ED + 8C2F2FD0B62891585DB8CDDC4FAFE5BDEC9AC250ACCA0FC2B26C7F2CCBF2C5F2 + 1C5BB838C39BFD7B624E8A1B66256DC1FC4C2F2CE6FDF55C8E3923C18579AE2C + 73C7AC4481DD2D9895C039A578525E9893C47CA6E7A7A90F5FCC49F6C2F43857 + 4C8F75C5EC040FCC11048E7333A077D2B6CDD4264CD9EE844F23366242F8068C + 0BDD8089919B316DC7164C8E74E19AC515B3D8766EA22B8FD703F339EEEC4467 + CC49D88279C93C0F1C735E02E71AE781B972511DE78DF9B13E5894E8C7354B00 + 16C569DD12C8354C1856654460596A38E6270563460CD72BF1C13C7F11EC231C + 8B98BF90E1DCC410CC4EE2B94E0963FFE19811178499F1B2660EC6B4B810CC48 + DA869949919818CDF55B843746867B617C943F26C504616A7CA80927ED0CC414 + 964F890EC464C63F8DF4C384085F4C0A6728B7CCFE6E18EBE786D13E5B30C263 + 3386BA6EC060E7B518ECB40AC3DDD660B8EB2A0CDABC187DD62C40CF55F33060 + DD620CDDBC12439DD760E0A6D5E8B59AEBD1A50B597F3DC66FF5C1D48830AE13 + A378DE423082E9C13E9E18CAF5E388C000AE397DD1C7D31D3DDDB6A0AFA70706 + F9F8A2BFBB077A6D7646D7F59BD16B930B0638BBA2D7BA4DE8B47425BA2D5F85 + DEABD7A3D7CA35E8B674193ACE5F848EB3E7A3E514AD8FB9361E3B1D8D474FA1 + B87E1EABBD7CA7A3C527D3D094E94623C6A3FEF0716838723C1A8D9A80FAA33E + 31C0B8F158968FD71A7B161A8E9D85BAA367A0D6C829A8C3FAB5868E42035367 + 22EACB0A78F8270604D7E4BA5D16BBF58709005396A5AFA0306501E0465AD3CB + 3278C0286A38350C7505817B0F42CD9E03F071EF81A8D36F08DB0C475D950D18 + 6A0FA9FE4351BBEF10D4EC3308B5FA0E66BDA1A8C33C853565D96B77DD2CD02B + C86B406F97FEA8DCB12FAA75EE879A02C02A539DCEFD51A3633FD4E8D017353B + B08CAA41C9E2B76AFB9EA8DC5616BE5D50A1852C763B993D7BABB6EF816A1D58 + C6B8DC374B9504795B7432FA88E9F71AB5C1BB0D5B1BBDDFB82D2A346D8FF78D + 7BE756A8D0A0A5093F6CD0CAECDF2B63026BBF5E015F1916BC2F18AC78ADA678 + 4FE05700B866639B1897C73C79A97B5B9EF34CFC67BD6D9E53E979958C269867 + 7FCE2709E41A09E6DA01B0B1F655DA2E9597825D9529CDD0CA932C005CAACAB5 + 6D96BEB2E8A50476DFB63F2F7C47B23FFB535A727CA65AFADC54F90ACBC4ADE7 + A496CA3E3F75CC73CC2F05ABCA571DCAEACF02C265B70C74948C7A6CB2C58D2B + 6807D06A015B5B79755B9955C78A3BAA4C9E01BDBF10C7659D9F3D5EDA543A5E + 1939D6F9A77A7AB6EFF0ACBDF4997B9967ED16DC2D2BEB39BC631D2BAF6C7F65 + F32CBD8E059457569E4A8DD7C4482CAEE3C04B7E51579E6459666D35FAE6BB95 + F196F40EF30456DFB20157856FC940F10F158CDE7A9375055FDFB0015895498A + FF3303FBEF55B9005810DAAE7FE2858EE97F458E6D5FA7F2DA95A7DFDBCE2AD7 + 7130FD263F0759552BFCA73A96ACFCB22A0B6AFF5D12003610D80E808D982E05 + C0E542E032132F7B50BF26C776FFDBABEC713BA8F4FCF0980C00D60F963F483B + 98B50160FEE8DFE48FDF58014BFCD19691F991FF1600B600F3AFD57BCD45E2BF + 538E17B3DF7341B3EA5917CC520B5C7BFEBFA25278EBA8B217FFD7955BF1F2F2 + FF55997D046C32D6BB9A9BC3F13AFE39285E5EFAB512007E570098A1B104D61F + 1CFF208DE52FE3D41B76ABE5B7187FF7FD1A46EFBC5F1D6FABCD9BEFA352951A + 183C6224868D1D831AF51ADAE7CC3F33FB5B65E646C0BAE1A0AC9B07EB26C5BA + 51918C4B1296596F7039DE7038DE74488EF9EAD7B1BFB2E5D678E6E688B26E8A + 4AEB30AEF12D00ACB8EA58F32A0B814D7BC15F41563BFC355055405320B38E1D + 660A5E0A86526FF1A6CF40E06A75F0464DDE10D6E28D21EBBC5B4730B3213E10 + 6CAD54BBF4582CB7D4A5D6C80E1058605756BE2A7F9FB2DC595BC768603065AC + 8CED6D8C2B698EFD760DDEA4727CB966360098B200B0E66CAC8439170B34BFAD + E3533B1EE35B12DB0BD256603BB53192F530F3DE9765316F8E159A3EE4C25A73 + E07C65112DF82B302CC06BC982C18ED27933009DE3E906DC02C082BF1FD66F66 + A0B08E41F91F9A7D859B9B7329D900AFF672695A0A813FA8DFC48402C3151B0B + 102B5FE54D996E816ACDDBA266AB76A821F7CECD5BA372C31636A8CD312B71BC + CA8D5ADA2C849BB741F596EDF071DB8EA8D7B12B1A77EF8D56FD07A1F5A02168 + 3374383A8C1C854E63C6A2DD88D1683668301AF5EE83FA9D7BE2E3D69D515596 + CADA2B58EEB2D99F20B464CD4940D8EC092CF7D6CDEC56C15CD454E222A712C3 + 8A4D996EC5B2361D8DAA52724F5DB535C3B69D29E6B7EB88CADA87B8436754EF + D4958BAF1EA8DDAD27EAF5E88386BDFBA371BF8168DC7720EAF5EA8D8FBBF740 + AD1E7D51BBE7207CCC055DEDBEC3D160D0182E1A3F4143AAC9B0F1683A42AEA0 + C6A10E1792F598D788E9E69F4C45EB4933D176FA5CB49F390F1DE72C307B22B5 + 9D3617ADA7CC669EF6475A805653E7A0E5F47968B76039BAAFD988FECEEE18EA + E58761DEFEE8EFEA89DE9BDDD0C7858B6F2737F476E1A2DBC91D7D9D3D31D8D5 + 0783B6506E7E18EAB11563B686615C401806B0BCC79A75E8B3690386B83A6198 + 9B33867BB860B4BF0726866DC594C8204CD9168CA951A1981A1D8EE9F1DB3025 + 761B26531376EAE14328E665C763717E22662547B22C103393423127290C7313 + 42CD438EB57951702A8CC5AAF430CCDAEE665CA62D8FF7C1D2684FCC09DD80D9 + 816B303F7C33E6EDF4C0B294202C4F0BC1C2047FCC8BF1C69C1D1E98C3360BE3 + BCB030D50FF3D3B6625156309616688FAF306341A17DD8361C4CC2C6FD4958BF + 271E6B8B63B08112D815F05D9A1E44B14D4688B12C5E95158E95E9A1589E1A84 + C5495B318F73599E110CD77D89F096D5C71EB98F96554330561786625D61089C + F66F87C789040380D714EA01960F9665786249B22BE6C76DC68204272C4B75C5 + 9A2C6F6C2A0882DBDE6DF03E140B9F2389F0399A0CAFA329703B940897FD72C3 + CC71D897EBA11D9C673836EC0AC5C6C260039037B36F97E2506C290EC3FA5C7F + 2C4DDA8225492E5891E681D5597E589713808D9CD726B6712E8E80FBDEEDF0D9 + BF133E7BA3E0B587F17D51261D782C11412753E077341121673210723613C10C + 65891B7426CD28F47C3AF353E17F3C0EBE47A2587727B61E8F86DFF118781CDE + 01F743ECEF680C8F211AFE476311723AC500DC90339908BB20D7CB3908389703 + BFB382B6B9C67AD7EF4C16FC990EBA988BE00B791C93F15329F0E7F1069C4C62 + 3A0D81D2B974049FE75C381F01604F9DA7E3C90838CB3C59FB4AEC53EE790DE8 + BDB20B61D70A117AB580E91C96E5306419EB689CD04B0508BF5A886D377623FC + 5A3122AE1763FB6DB901DD8FF05B728B598400B6F5BD940BEF8BD9F0BB928FA0 + 9BC506100BDE46DCD47EBD072077A1318F8F63074363E125F0FBD0065CB5FFA3 + 2CD3542E7027C530BDE39E2CCCF6628760CBADFD88BECEF0C67EECB8B61BDB2E + 1721EA6A11765E2F61BC1011177719855F2E40288F2BFC6A31B6B16DD4DD03D8 + F9506E598F313C8EED1AEBFE110340059A8D251B1579E7B0B1D8D3FCB447A52C + D1648116C15096C3219C83B15A1634797A0A3B9F9C307D6D63BB1D77655547DD + 3F66AC81639E9CB6ED5FFAE21C629E9FC5CEA7A711A3B8242B6086492F2F21ED + CBAB364BC01702B65790F9B5AC5CAF1A6095F1E565647D2D6B49B9B5BD8078F6 + 6973057B0272B72C8B45B949CE6279EE37D7D84E7BAB5E40FACB8B6C73896DE5 + 6A571695726F7BC158EB263D3D8B44B96E7E22C9A251FB9A6A7FD373AC77896D + 2FC372B32CAB4801379BCB59ED5BCA7E9E5F422A95FC98C7768FF3B8AF7D858F + 2399734A7974CC585F263CD8CFF243761D461295C878CAB313487F7196EDD917 + 8F45B037E1F129CE49C0EDACB1FA95047F533F3B6FF21258678780D4B57D88B9 + 7F9CE5368BC9A4673C5F9F5FE37911FC1544BB8E8C2F78EE3E3F8BE42747D9AF + E0AF409CDC535FE4B9BA8AF8E73710F7EC1A92BFB883DCEF9F62975C22CBF2F6 + AB9B4895FB6001F417B21A3D89ECAFCE20F39BF348FFEE0A32BEBF86F46FAF20 + FD1B7E1EDFDD44CE0F6A7F0F39DFC95AF61EB2054F7F9045F003A4C92DF42B81 + D3BB068E1A97C9DF689FE2DBFCACEF20FDABFBC8FC5600F8B181B9399C87AC7C + 65C99BFCD51D63359BCE7896DDBDB3406FC19F3F43DE0FCF399EDC363F36FBF8 + 4A82B299AC2BABDE7C815ED6CD615FDADFB7E0CFB67D80A55D3F49CF99AFBD7E + EF712E9C8F5E34F8F63632BFBF85CC1F6E2397F3CF66FF19DFF098BEB5016059 + 09CBA2384780FBFB87AC7FAF540600CB729AE74016D4C62298CAE0B11B97DADA + 4F592EB5BFB88DA4CF6F987D84B59F70D21736A5C812F7B38BD09EBF92F677D6 + DEC01600CE906B6F017D7EBEDADFB8E42F9F239FF3907BE7547E87E43A3CFDC9 + 31C4F23A13712193D7DA145E0BE3117E210DF1B78B0C1C4EB8B797DFD1FD887F + 28C8CBEFF973DB9EC17A7942BF23414FFD966401ACDF9779894216F4E6E50B7D + DF6D205840D8EC494DC90278A75E18E1F540D7B428BD7C72EF20BFCF27CD6F51 + 0058FD1A2B627E971364ADCEEF6C1C434160037F19C6294F6ECCF99BD26F4BD6 + FF727F1DFDF82CA2F89DDD298BFBFBA778CD3C69ACA363A8D88782AFCC13E0BD + 25EF0A36C8BAFD9EDC4D9FE4B554A097218F691B8F2F82D7A9485EA3B6F15A11 + C974A4AE6B7A0986BF8DED8F789DD34B12B268E69C751E744E04810597057865 + 5DAC6B5AF8ADC308BBA1F1E4BEF988299315B22C908D752F7F9B16F455187485 + FF0957F7DA2030DB688E52C44DFE8E4D48DDD24B384711C27EE59E3990F58359 + DFB8DFBF7BC4B861B6E9B091DC45DBE0EF5193BF95C71FC8B904332DF8ABFDDA + 436EED43F06D59EA1E347582EFB0ECF631863605DD66BF6C1378F3307CAFEE37 + F2BF7E005B391FED43AFBDE283798D577CEBAD43F0E771FB0BF8B22F8D2505F0 + 187CAFEEB1EDB52B6B5C3BB4F5D2DEFD176C7BFE6A9B05B39503F3B51FBFB647 + 08B854C832FE47B28DAC857DF99FEA7759FB1317C397FF5DDE4C7BB29DFBD91C + 037825F733D936002C77C94752E0CAFB2777A54FA463D3B114383374D758FC1F + F4663FEE1CC7E55CBE71DDBC5EE0F6580E369D90656F1E369E14E065FAA4CD02 + 5860567BEA6E3C9151EAF6D93194369CB083DFC369C60278E3F12CD3BE542724 + 1B44565AF07933FBDEC87E371C4F37DA74328B7959EC97E39ECC81CBE97C6C39 + 5B68E0AFC0AF8BE033E7B6E998DDEA573A9A8575C7395FF6E97C3A13CE27D339 + B62C8A93399744D6496479328F31C5841B4FA472AEA90602AFDA1F8735B21216 + 9895379BBD71C60DF5C64349587730D9B8B25E77381DAB0E2463F57EB9AE4EC5 + DA032AD39EC5723D9D84D5BCCF5DBD5B7B116B4F629BC5F01ADE5BAE3DC0B1D5 + 8E75571F60DE81786C90FB6A7E16AA27EBE095BBE5F63A837D6563DD019EBBBD + A95855188F5545B158CB32B96C5E9419642C7167277B6056921B16A47B615186 + 0F16A4F29E9DE98569BC7FCF92B5AE2FCB7C794FBE150B33792F9DEE8739A93E + 989DE2CD7C3F2CCE0AC4B2DC109BB2034CBBD909AE9811E38439F15B303FC91D + 73989E15E78C59F12E981EE784A9D4EC5477CCCDF0C6F4842D98C2BAD359772E + FB9C9BE48D99B16E5C8FC855B39371D9ACF4EC3841600FD673C5B458B68976C1 + B41857F6ED8559B11EACEF82A902BCDB9D0DEC9DB2C3059F466CC6D8E0B5181D + B8DA84E342D6636238C78EE5BA23DE937374C18C58671EB33756E587606D6124 + 56F03816C965776A0096A471FD901A8CB9B1FE981AE181C9113CAE288E17E583 + 993B79EE62FC30338AF3DDE96BDC390BECCE60382B3E98C71BC4B90571DE0198 + CEF249DB3D3125CA17D363023065E7564C8DB1694A6C10A62544605A7C04C6EF + 0CC4B0100F8C08F5C4E498600385672486636A6CB001BF5305807706604C30D7 + 843E9B31D27F0BC6F8BB6288FB46F4DBBC1AFD37AEC2804D6B3070F31AF463BC + D7EAA5E8BE6201FAAC5D847EEB17A0E7CAD9E8BA74163A2F9E81AECB66A1F79A + 85E8BF6139B58275B43FF0120CDABC0E1383FCB1203E86738FC6F8B0408C0CF4 + C5507F1F0CDDBA156343C3303E3C1C83BDBCD07DE346AE45B7706DEA83113EBE + 18E8E28A3E1B9D31886BD6C1F268B5721D3A2E5CCA31D9FFCAB5D41AF45EBE0A + 3D972C4797790BD07AEA0CB4FC74065A4F9C8716E367738D3C1D2D274C47AB4F + 67A2C5B8A9683A7AA271E15C77885C388F45C3119FA0FE88B1A83F5C7BFD4E42 + BD5193D1643CD7D43397A2C3DC956836793EEA8C1C672C7E1B8D1E8F669F4C66 + 38010DA906CCAF3B7434EA08FA0E1A85DA0347A056FFE1B6BD7C078E447DAEE1 + 057F05811B0CE278B20AEE3714B5FB689BA8A14675FA0E3110587982BCB5FA0C + 42CDDE030DEC15E0AD3760B801C0357B0D44D51EFD50BDD7007CDC77303E66F9 + C7CCD3F3825AB2EEEDC6FCEE03515BCF0F28A5AB75EE6B2C7B6B74EE875A5D59 + B7140033BF5D2FAA37AA5355DBF444E5D63D8C35EF872D3AE183E61DF07EB3F6 + 46B2E2ADD4BA8B81BF82BEB6FD7BDBDBD5CEB87456DE078DDBE17D3B047EA741 + 2BBC4BBD57BF15DEAF6BB3EA7DB78EB62B6B6677E9DCF467002CD7CE0C2D19F0 + 6BD77B351BDB54A311D5C058ED964ACFB5047BED69CB9AD711F25A328097A1A0 + EF5B16EC55DA2EE3A54FCF12B5E59D9ED7A9BC726DC6F5BCAC4E69DA512A7BC3 + 5EDFD463DC82BEE6B91FEB48659F815ACF4C2DBD2EDF51561D41D4B2798E6D4B + 61ABEAAA8C797A86A932798D946C4645D56CCFAF1DC5BC528F93769505AD56DA + 6C4568F5F35B7ACF4165F2AD31ACBEAD715E2BF33CBB9C7C8965D6B3F6B22AFB + ACDD11FC4AE5D5B1F21CFBF92D39F20F472EF0BAB2B212FCD516956F38729DD7 + 016031240B30DAC3B7DEAB82B7650D6C2C7A3F3292C5AF642C4F055AFF50017F + A0045FDF66FE3BAC6F0031D3BFE05F6599DD6BF8DD2F5C47FF867E0B00974260 + D5B7C67FCDB8BF2AC7B6AF5379EDCA5379ED1CE30EF50C747FC396369FC7BBFC + 2C18964260AB9DA3ACF68E2A0FD6FE3B540A801DF40BF86BE91700D461D2BF75 + 40AFCBFF3F428EC75C466F53C655B68E8D1FAE81BF920DCC5AD057793F0360C9 + B18E2D5E1ED8FDA7F2FF0D00B0F45B173B4B8EF5FED58B68792A05B0BFA672FE + 10CAD56F95FF9A1CFBA764695CF60F43B2DE1EFAD7C4637D8FE7CF2E59020BDE + CAEAD711001B31FE96427EF6EF701EEF7DC8F237DE4395EAB5307DCE7C2C58BA + 0C0D9AB5B10177F62D57D0E66D2BFB0D816E4A149AB7B21C6E201CCB0CFC95AC + 9B097B3DC71B8FF2A4B6160055DCB18D358ECA4C3965C66199E3CD8BF22CC0AB + 3A82BC9694B6F2B4EF860979A36560A6B1F86D880AB51B197D20A82A106C9705 + 80ADB7FEE492F9CD9AF58DE45A46D052E0B802FB7ABF0A6F0CEDC761DEE8E34D + A0B9A96368F604B6436001E0B7790368AC6A793329006C59019B9B41B63196CE + 542934AE6A9300B0B154D6DCEC32D05AF3F85837B236F86BAC87ED7AA3B24037 + F3AAD9F4AE81C00D8DCB680BF8EA5C28B45902B35FA60570CDB81C5F105950F7 + FD5A3C5F02DE1CCF9C1F8EA97C0B042B4FE0D572056D2C7FEBD95C4F0BFE4A8A + 0BFEDADC39CBADB2DCFAB00E430155016059FB4A1604B600B081B00DE50A5AED + 1B31DDC258FD7EDCBABD0DFE366D894AAA5BAF092AD66F8A4A6C53B5696B546D + DE06959BB534AAD6A20D3E6ED301F53A7445A3EE3DD1AC4F3FB41E3418ED860D + 47FB1123D166F01034E9D70F0DBAF5409DB65D50838B1BED935C51D6C90D649D + 2C686D0B3F6468C160CD4DFB12AB9EF654962A36E27C0482B9D8A9AA3D8F5B77 + 4295D65C20B568878ACDDBA222C32AAD3A18105CA5AD164EED51B1553B130A06 + 57EFD805353B7645EDCEDD8DF570DD6EBD50BB6B0FD4ECD2CDB88EAED1A93717 + 6CBDB9A01B88865C34361E380A0DFB8D40A3FEA3D088F17A8CD7EECDC5659F61 + 6838700C5A8D998C4E53E6A0FBAC45E83A7321DA33DE72FC5434192577577ABB + F953F3C67253ED59FCE9346329DC76EE422EE0F586B60BFA6ED88CEEABD7A2E7 + EA0DE841F55ABB09BDD76D627C3D17F74E18EEE289115BBC18DFC285BD13DBB8 + 63B8B337FAAD75420FF6D16DD972745BBE145D962C647A29EBADC6808D6B30C2 + DD19E3B67A638C3FDBFBBA636CA00F46323D3AC81F9F6E0BC6B4D8502CC88CC1 + D2DC78CC4A0CC5CCF810AC294882F39E4C6CD8958C35D931589B138DF5393158 + 141F8871FEEB31DA7D0546382FC6904D8B30D2790926303DD9671D26873A636E + 8CDCA805625EAC1F666EF3C054E64D0F7761DA1B4BD2028D5B67ED23B62C2F1C + 8B73428DF4C04AE0575A21D778B9E15895276BDF102C48F4C5EC580FCC8A71C7 + DC786F2C4DB559232FD11E61315E9813ED69F6EE9A17E785D5D9E1D85CB2136B + F3C3B12C27082BF283B0BA30046B8B42E0B46F3BDC8EC46233C35579015898EA + C136CE98B3732366EF588705319BB02ADD034E0501702B0985E7BE48F81CDC09 + DFC331F03B1A07FF1389F03D9E088F43B170611FEE07A3E0797827DC0E44C165 + 4F045C7687627361209C8A64ED1B804DB9BE589BE18115C92E589DE18E8D797E + CC0F820BE7B2A5381C6E7BB6C155DA1D01AF03DB1170340681C762E07B703B7C + 0EEC40F0E964849E4B873FC70C3E9D6A206CF8F90C445ECC46D0C944780B68EF + 0987CFBE6D70E77C3765FB605396373C8B43D85F089C77FBC37D5F20FC8F44C0 + F75038FC0F4722EC642C824FC621F46412765CCAC6F6ABF9086718CC7102CE09 + EAA619A01B74369563A71AB81CC0F1BD0FC7C15330FC6802B6B2ADFFA9646305 + 1C7A3E0BE1177218E61A57D4FEA7D88F60F5C53C845F2E347B4C865DDA85D08B + BB107955FB3FEE46E4B522845ECA679D5C845ECE47D89502445C2D44E4F5226C + BF5962F682DC717337B6DF60FD5B0C6FEF4518DBF85FC886F7D974F89ECF44D0 + D50284CBFAF7EE3E0388055765B92AB010FFEC14125F9C41ACACD2EE1FC58E87 + 4711FDE404763E3E6173517ACF06666D969BB20A659DDBFB112197D2370F205A + 567BD7F7DBDC3D5FE4315CE039BAC0E331309CE7FE7436222EE623EAFA2E44DD + 2842F4ED3D88B9BB9FDAC7B60710734F7D1C32125C8ED5F81CCFEC8379C7E6D2 + D540E187C78C659C42ED99A9BD2DE59E5A96C4729DAA3CEDE919C7B9CB1A57D6 + AB8299065C0B3A3E3B8944590F6A4FCF2F2E20F1E539C47F7E16098A7F71DEC4 + E39F9F41F2CBF366EF518127CB8A37E3CB4BC8FCF20A25807B05B9AFAE23E7DB + 6BA51058B04A402AE9B9F6443DC3F6E70CE8CDF956A05800977D7E7ECE84191C + 4BF12441DE276791FA8CE9E717906A87AC199F5F428620B4C0D7F3F3487D7191 + 71F561DB4BD502C00274C665ED67DA4B55FBAB32CE32412EB9A0D67EC1C9FC6C + 93793E121E1D42F2D323487F7102199F9D64781CE99F9FE458A7983EC390F392 + 8532DB1BB7D4026B3C2E9D870C592BCB0A592056208E650270C60DF4438DC163 + 1030E4FC6DC0FC9A91DC40CBD234F3ABDBC81458E4672357BA66EEB2AE14487C + 791DE9DA1398CAFDEE3E0A7E7880FC577791FBCD4D335E3AE796F2E428E2EEF2 + 7B7E3D8FDFA17CF67118A95F5D44E6ABABC87E750DD9DFEAB3B889EC6F6E20C3 + EC257B1DA9D2973718DE4412C7487A79C3E62AF99BDBC6A5B4CDB25850576E96 + 1FD8DC50BFB2B96036E91F1ED9F6F11540960B664A96BCF97F7E86829F5EA0F0 + AF9F1909F0AA6DE6B7B63EF27E7882FC1F9F224FB0F74FCF51F497CF51F4D3E7 + 28F8A3F6FA7D864201E09FD856FAB3F482F94F91A33E389774CD8F73336EADFF + F880FD0848730EDF08E03E30F3533AF7FBC7C8E5588E00D8CC93733796C66A67 + 773F6DDA58D6C2DF708CAFEF427B2ACBFDB6C6B3E926CFE9759E237E9FED1058 + 2F40282EA5F3F39432BED477FE2A3FAFEBC61DB58E3983FDA4BDE4F79CDF915C + FE3E76F1FB9EAA3D77AF1562E7357E66D7721173739781BF290F0FB1EC08521E + 1FE3E7AA970C4E9BEF84ED7B21106CDBB75AD6F53617D0B2B6D7F7E5ACADCC0E + 89F51B3756BDFC8EEB9A64AE4BBA2EF03A615E68B9C7EB09AF73F1BC9E253ED1 + BEC036882A2B7BEDF59BA29729F8DB91E5B35C600BFC2A94629E5DC04EFE2EA3 + 1ECA1AFF38AF2BA70D208E79760EB14F78CD787C86E36A6CCE9B6572BF1CCBB8 + E625B7CBB63DD54B8C3703BD3CA3EB68F433F6C16B43F4535E53998EE4DCB7F1 + 38B731DCA117609E9E64D94944719E518F8EF1F8F8DBE218C6FAD7C05F59FAF2 + B835A6399ED39CDB695E1B6DB057D6BC7AD94592E703015E41DF50C15E964B61 + CC53DA067A0F1977D0DA07D80AE5A63A8CF9A1D70F1888BDE3AE0D2AEFB877CC + D409E1B53EF8C63EB30FAF00B100F4365900B39EDC3DCBC57300FB0F643D2988 + 0AB929782CF7CC078DB706B9EE0FBB738C79C71072EB8871071D24D02BD82CE8 + 7B4DE14104A8AF5B76D7CEA63DFBE2B53E88D7F980DB47E0C772BF6B366B60C1 + 62C9EFEA1EF85C2E86CFA522785F288017FF7FBCF83F6B40F0D91CB3FFAFE7C9 + 4C786B9B0BB3D77E21FF230BE177BE80FF9372F79C6FDC416FBDB21B01ECCBEF + 8AFA2A8437FF8B3D650DCC7ADA17586EA1DDD987EBA94C6C394E1D4D87DB890C + 6C39910EE713A97039996E40B1CD7A38DFB88276366E9F25C1D65D703B5B0857 + 9669AF5FB31FF0B93C78B0BE5C48BBF2BEC0E574A6A96B5C451B209B6FA0AC42 + 67015D81DEE35976782BB7D33F4B0057DACC32C9723D2DA86C5C400B0033BE89 + F71EB20056BF1EE78B788C257065FF9B65F57B24DD2601E66359D87C22171B4E + 08600B2A67993D8F05C55D29679E07B989DEC8E3DE744AE09A6D4EA6C189E743 + EEA53769CCC3C9D87C34159B19D78B8B6BF72560D38144B8F0DC6D3AC6791D4E + 3700788D2C81A90D87D38C36B1DC89E7D8E928FB39CC391F4AC7C6232A639F87 + 520C24B6492EAF6545CCB60793A944AC63FF2BF7C461C5EE58ACDA9348F1DE7C + 5F1AD61FE49887D8DF01F6BF3F994AC286BDB1BCF78D30DE7666CA8236D90B4B + B2B66255413096E704604E821BE626BA1B00BC849A9FEA652C7117A4FB1BCBDF + A5B9C158941560F6F05D92FD33005E9C1EC0765E9811B305D377BA6046F41613 + 97BB65B9589E19EB6A5302EFD5537D293FCC48F0C474C15DB6D3CB9F0B53B662 + 36EFD50573276D73C2D46DEC67A71B6646BB6372940BC66FDB884951CE98BCD3 + D5B8809E97C23EE23C4CD934E609FC4ED9E18CD9F15E9815E789C9DB9D4C3F93 + 64111CAEBD7FB7709DE0870549FE66CD309575E726F860457618966785624EBC + 0FF3D84FA41BA66E77379077DA0E1F8C09D8CC75D1267C12EA8A89115E181FEE + 894FC2DC312ECC0D9F467A617A4C20A6C706626AEC56CECD9FF3F1E331FB63CA + 766F7C12EC82117E1B3162EB668C0D71C58448CE6BA79F1D02071AEBDE31E1BE + 181DE183B1DBFD31617B00A6468770ED1566AC7D2744FA715C2FAEC9B8A6F377 + 413F97D5E8EBB216433C9D30D86D23FA6E5A859E6B57706DB88C6BBFE5E8C5B0 + FB8A25E8BC78013A2E9A8B9E2B17A1D7CA05E8B4683AF366712DC875E8CA79E8 + BA7C0EBA2E9B8B1E2CEBB57A31D784CBD167DD0A0CE79A704A7830C687046098 + AF078FDB17C3A981DE9E18191088B141C11CD71DBD366C443F27670CF360BE97 + 37063B6D419FB59C0FD573C55A745AB014EDE72C429785CBD07331E7477597D7 + AB454BD16DFE02B49B22D84B4D9A8D5613E7A0F5A733D17AA2A0F034036F9B8C + 9E60F6F16D38620CEA0D1D893A8347A0DEB0D168384A0078026A0DFD040DC64C + 41C739CBD06BD906B49FB518F5478F679D51683A562F5DB38F315C578F9980C6 + 82C0C33F416D037F471AA82C102C002C105C977975FA0D47EDBEB2E21D81BA8C + D7EA3D981A68E0AD2C7FA51A3DFADBAC8005747B0F32105816C102C3F5FA0F33 + 90B83AEB54EED20B55BBF53130B87AF77EA8DAB52F6AF51C82FA7D869582DF3A + BD86A0762F3B00D61EBFED7A1A693F5F4979D53AF44615E6691FDFCAADBA1955 + 6AD10D1F35EF64B3DC6DDA0EEF37B149E98FE4D69912F035A0B7611B7C405590 + 85AFC4BC0A942DDE16EFB1EC1DB975D61EBFF55AC0B67F6F13037E0D0466589A + 47BD55A3919180B0230036D6BFD51BE3BDEA8D6C60B79ACDAAF767EBDE9F2D7D + CBCA02BC26549E3D54DA66D56B87B9950469F59CD1F62CB1F419E34736A94CCF + EF7EB93D9E55DFFE1CD2D4B3198A481604961C9F81EAF9A6F50CD47A26EA28EB + 19687965D2AF9539CA7A66EB38D6EBA06EE9B36CE659755E57D74AFF2B721CE3 + D79EA5FFA67EAD9D9E73BFF34BEE5056E53F73D7F3F4F2EB5879E5F18DB2FAB5 + BABFB70F4B06002B5E1ED3511E65CADFD0B6A3765665E76B0640BECDB41D081B + D921A4CD1574251BECB5C35601E077794E95F70B46C778298C2D0B661D64ED1D + FCBF0281ADB9582A1D47A1A5327DFCA61CDBBE4EE5B57B9DCA6B2F95A9576A75 + 6D3FFF8E00B8F41CFD461F468E40F6DFA9B2F0572A85BE65D3A500D461D2BF75 + 30AFCBFF3F429AB775CC3F4B3F30F323531D737C82B3FC81D901AD0DDEF2475C + AE54F6B30CBC2D07ECFEA2FC35FA457979178B7F837EEF45D1AA67A9EC85F675 + 79AFD32FFE382C39FE01944D3BCA2A2BDBCE4AFF5E59EDCAC8F14FC3527900D8 + 3A96D7E5CB5FFF1BBC60C80AF89F00B01D029702E077AAE19DF7AAE36DC6DFD2 + 1B3C6AF3D60778BF6255B4EED005EDBB7447A59AF5D88675A9B7DEE74D88DEFE + D28DC04736D0EB786362DD2828CF11DC5A00D8BAB1B0EABF4E567B0B9C4A8E6D + 1CC730F9769507807563E508804B6F6A3437BBAC32C15781CE0A02A196047B1D + 81A6C028F38C6B644160FB9B82726DFC9640ABDE0E14741518655AD6BCD61B7C + 1A47905796B382BF02AA9665AE01B39554CE1B3E815F0B005B71F66349E5E6ED + 4181D86A8CDBAD798D7B6ABBACF92A2E4B61D3BFE6601FC766ED5C0B6F68AF63 + 4A7B12BFA7F94A9C9701E194CE81AC816D16C13C17EA93DF096B1C9D1B815F01 + 5EED3F2C58EE08822D00AC3C63595CBF9971C32C77CC15047719AF28AB59858D + 5A1AABDAAA82A08D65556BB3A4ADDCB47529007E5796D8F56D60F8FD7ABCB9AF + D7D46671DBA8193E6CD0C484559BB5428D56B200665F02BCCCABDCB0192A3568 + 6AC2CA8D5B983A55A8CA4D5AA052D316A8D2BC35DBB443CD36ED51A34D1BD46A + D71E0DBA7445939EBDD0BC6F5F34EED50B75BB7745AD0E1D51A3457B546D2A80 + DB8AD2D8B2FEE53109FE0A60734E3A360383756C82C482BF2CD39ECB02C6951B + B741B566EDCD5EC41FB7EF8AEA0C2BF1B83F34EEA459D6BA233EEED00D1F77EC + 866AEDB4574E7B546AC93139C72A6D3AA00AD355DB7644F5F66CDF89F53A7747 + 4DA9630F546BDF0D55DA7645CD4EBDD0900BC5467D86A23E1775757B0CE1C26E + 28EAF6E2A2B2D730D4EC3E181F530D078E46DB4FA6A1CBD4B9E8F8E96C341D31 + 0175068C406D2D1A078DC4C703B8E01C34820BDE7168366E0A9A8E9F82C61326 + A3F58C39E8B174857993BADD82C5E8B264053A2D5E8EAE4B57A10BC30EF3179B + 857657C6BB31AFD3C25568377729F3B8D85EBAD6B896EEB1743517E16C3F878B + EC19D3D07EEE2C745E389F6D16A0CF9A1518EABC1143B66CC600A7F518E4A2B7 + C999F6D88209E1FE9815178A7949915890BA1D5377FAE1D3702F2C4C8CC0FADC + 04ACCC88C60296CF88F4C5D4502F8CF773C2E04D8BD077D52CF45C3A8D9A8181 + 6BE663D8FAC518BA6131467BAEC1F4080FCCDDE98B99DBBC3029C8059FF8ACC5 + 38FF759814B21953225C303DDA13739365051C8A051941462B0AB661EDEE38AC + 2E8EC1929C08CC4F0D347B072F48F0C19C680F4CDFE6CCB69B313DD219F363BD + B038D11FF3633C38C6168EE5C9B41FE6C7796341BC2796A6F8607EBC3BE625B8 + 63556E203614866153490436ED89C47AB9B02B08C1CA9CAD989FE88A6991EB30 + 6BFB06AC4EF7825B5108BCF745C26B6F043C0556F747C2E7C036B8ED09C5E65D + 01702E0A8627D39E0777608B806B41A0B1F2F5DC2F901BC6BC10B8EF0E87EFA1 + 28F81FDE01EFFD11F0625BF7E22078ED0B87DFA1EDF0655B2FD6F7DABF1D5E07 + 76C07D6FA401C62E94F78108041C8BA67622F0A842591FCB05F40EF6C9F4D178 + 849E4C46C4D95433C6AA3477AC4C71856B61303C04B8B37CE192E3075FCDFD60 + 08DCF605C0E75030028F4570DC501E4B187CF686C2634F087C799C814763117C + 3A095B4FCAB2370141E752107E25135137F21071211D61E75211CEBC00D693AB + EA2D7B39F7C3B1F03F1E8FC05329083D9789C84BB9D87E6597B190DD7EA51021 + E77310782693612E2299B7ED5AB13D2C41D4CDBD46DBAE1519EBD9086AC7F522 + ECBCB91B31B7F7205A2E7C0553A9ED82C1D48E5B7B1075876D6E9620E8721EFC + CE6521E0622E226EB1BFFB07107D5FFB741E45CC8363B0B90E3D61C04AD20BB9 + 70B5410C816059E5099618D07AF7B0011B3BEEB0FDBD4306DE6A5E915738CFEB + 1CEFF6018409FE5ECA47D8C57C33CF28CE79E7F562ECB85A88ED540CC797E55F + D2830348A4E23897B8BBFB10CBB946DF96F621EEDE01C4DC3FC8391E42EC8323 + 06340BFC6AFF4FC19ED8A7D413B970B540EF49333FC111590CDBEAD9AD679F9D + 36D6AA8E2057C798FCF979A4BCBC80E42F2E2249FAF22212A964852FCF1B109E + CCFA36F864035002A2B6BE2E209DF532BFBA6C20B0428161038715677F19EC23 + F34BEA8BF3487F79CEA4B3599EFDD545934E7E7192F339C53E058E04D499E618 + E96AFB05EB092E7F7B9DF5AF20FD739B95B0A4FD8833BEBC6A248025D7B4A92F + E59AFA86816F29DA5BF8338D210B619B45A45CE56670CC549EB7C4478791F6E2 + 18325F9E32D037FDB3939CE31964719E99064A5F304AFF9221A5F958D6CE82DC + E638CDF15D40968E9F12DCCEFFEE26F25FDD34D6CEC6BD35E792FDED6DE408AC + 7E7DCB00C76CC673BEBEC9B9C98259C77AC958550B66E7BCBA8DFC1FEEA180DA + FDD36394FCF410BBBEBB8D8257ECFB9B4BD8F5F545643FE377F56A1E7FDF31FC + CD6F43D8957C24F01833BEBE86FCEF6F61D71FD9FEC7FB8CDFE65C6F701E5739 + CE259E279DAB6BFCCCAF18A57E791D19DFDE42F6777790FBC35DA31C8E9BF3E3 + 03E47CAFFD87B58FAFCDB236FFCF4F51F093DC346BBF5E1B4C5599AC7505818B + FEF6194AFEE3258AFEFA826959F7DA806CC19F9EA1F0A717D8F5E7E7261400DE + F5E717C8FB91750487FFF8CC8061E559120C56FDFC3F3EE1180F90F53D8FE54F + 4FD8F6390A583FF73B8E6F87D3B22A563F02CCB976406D5C41B39D00AFE2CACF + FD93E6CD90120C566883D90F780EE4EE9A9F8F3E971FEE73CC7B8C0B02F3BBC4 + 73AAEF98F60296B2BFBDC9EFD83564B12C539F21CF61CE37B798E6B99675F157 + 7790F2C52D7E376EF0BBCBCFE3DB1B28E66797F9EC04A26F1421EE6611D21F1F + 42C6B3C348D51EC0FCED6B1FE014FE76D3F819A63C3B6B5C33EB776C5EA0F8EC + 9CB9F608003B4ABF6B5DAB2C97C8DAFB56F0577BE56ADF5F815FFD9E24ED559D + 2828AC6B9DB9661D36D70663FDFB54FB06AB7FBDAC719EDFA39FC1AF0582A339 + A7A827A78D5B7A790DD09EBD719C6B3CBFBBDA2B5CD74559F90BD26A5E06CA72 + 4C4973DC21F7F372997F4F7BFEF2BBCB716374ED629B18CE5B9E0A2299B79DE7 + 6007F3A274DDE531C73C635C00586D781D962CF02B5930587159D0273EE375EC + 39AF5B4FCF725C9BE5EFB65B876CAE9F05856F1F3696C2DA435D329E10048959 + CFE4DFB7B99F36FBAB4B9C6F38DBC81DB45EBC91FBF974FEAEE5A27E3BCBB5BF + AFB5CF6FB8B60CB04BFB0B6BDF5F63A5CBFF03B961F6E7FF4100C320FE4F84F0 + DA2E002CF81BCE7313CEB1C3A8108E11228B6059060B2E33942B7F93E67F4AD0 + CDFDC6BB43A8692F17FF2CA702782CDA13D8F7CA6E785F2E3196BF7EC64DF45E + 8EBF97E352CC0BE07FE8D62B25F0BB5C04DF8B02B7DAAB3717BE67F5BFA86D1B + 987F6E177CCEE4C3FB6C01CBE50ABAB81400FB5F953B69F67FB9185E177619F9 + 5E2A665F45F03C970F8FF3F9F03C4D9DC885F7E95C78CA32F87426DC4E65C0DD + EE2A5AD6C0DA6F7793E030D35BCEE6C2EDFC2EB81BE51B799C2F301230B6AC8D + DDD85759B02B694F614B82B5E5698BEA0A020BFE9EC8C666CAE994C0AD2C8753 + 8D04809DCC7C38D6B93C8E2DD05D047759FECA4A59F0F588F603CEB68D757697 + 01D11BE54EFA7806361FCB800BCB5C99E7762A87636ABE82D1B228962572165C + 28532677D66CE3CAF9B8B1FE16B6733E92812D87D3E17A9C71A6371D159CE6B8 + 1C73232597D4CADB7454FB13B36FBBB69CE0183CDF9BE48A7A7F0A56ED4D34A1 + 00F27AD6DDC8FCCD0C371C4CC1DA7D8958BD2FC9566F4F321617C46071DE4EAC + DA9D00A723FC9C8E71FEACBF85E3B91E4DC586BDDAF625148BB3757F1D865585 + E1585F128975BC075E9CEE8325193E26BE9EF7C62BF202B13C27102B0B228CBB + E7B5DACF98A1D2CB7243B134472F6D0661511AEF9B93797F2DEBE0541FAE1BBC + 291F2C48E1FD77AA1F166504607146201667069AB1A5054CCF4BF1C75C4A3077 + 5E922FE624781B783B23DABD14FECE8EF5C46CE527B27F8D2358CCB6CB72C34C + 382F95EB8134C16BAE09A8455C23CC4B128475C74CB69DC575C1D42837F6E9C9 + 7B7BB64D0EC02CC62771CD3125720BA66D77C3A761CE18EDBF1E237CB50FEE6A + 4A56C35B302E740B46F86FC008BF4D1815E88A4FC23C313AD80D23025C3026C4 + D3C0E04F233CB95EF2C4B8700F8C0D73C327A1EE66AD349EF586796DC0802D2B + D1CF650506720D34D25F20D91D13B7FB614A5420E3DE18E2E384E1416C131384 + C93B83312ECC0FA383BC38860725F0ABED8036A1AFD31AF4DEB012BDD7AF44DF + 8DABD1475ABFCABCD0DB77DD2AF45DBBC20060BDE4DB7DE922A33EAB96714DC6 + F4B205E8B57231FAAD5B8E011B5618F0DB6DF93CF45EB3187DD62EB6E72D41B7 + 154BD837D78D6B56A2EB6AF6EDEC8C3ECE9BD165ED6A74DFB00EDDD750ABD6A0 + DBCAD5463DB4E72FD35D97AEE47A7411DACC9E8F665367A2D9A41968397536DA + CD9887F633E7A3FDF4B968336D16DACF988D2EB3E6A2FDD4E9683E7E329A8D9F + 86B693E7A0FD3496B34DD3B193D068D478341EC5B5F0D84F8DEA0E1D85BA8347 + D8F2C64D42E3D1135167E858D41D3E1EAD26CF4597392BD8FF2253D6EC930968 + CDB573ABF18C8FD1F64CE3D164F478B3B6AE3B789401BF661FE0C163B8FEB65B + 010BF8720D5F9BB24070CD5E366BDF5A94057F05736B32AC2DE0DB976B7EB565 + BD1ACCFFB8C7005356AD4B1F54EED813553AF542F5AE7D51AD6B1F93F771F701 + C602B856F74146B5193716C05D6D00B84AFB5E14DBB5EB61F6F0952AB5ED8E8A + ADBBA262CB2EA84429D43EBEB2E6B559F6B635A1C0AE80EE074C0BFABED7A035 + DE6F6003BF15595ED10E83DFADAFFC56546B1B0456DDFA2DF10155817AAFAE0D + F80AF6BEAB7D7AA9B76B3532D25EBE6FD56860F4760D3D979364F12B35C47BD5 + 6D7AB75A7D036EFFC9157319A98E65D92BC9E0E167E8ABE76F2C97CC334A815D + FBF3CE8A3F3F57749429AF647BCEE828DB33CA9FF51665415FF3CC506D1DA4E7 + 9DBFFAFCD47AFE4939E65BCF3DCB2B33529FAFE9B7F419ABE265C06ED9E7D816 + B0FDB57A569DDF2BC7B646BFE3797AA91CEB9627877AE6F97939DCA1ACCA3E7B + FFE5F3F7F2551EDF284FFF4ADD5FD3AFF6630160191B1A6E64676B562837CF8C + 975AF3FEE1039B0457997E877D945AFC2A8FF504810D00567D4BEAC301CA1A30 + EB586ED7BF0B00DB40AA2D5E7A6CBF478E6D7E4BE5B5FF35FD9EF6269FF3B703 + 6047FDE21CFD563F65A1ECBF5B6FBEA92F8E20EFCFB0D7B1FC67082C394CFAB7 + 0EE4FF7839825F7DB03F7FE94D79E9F1336EDC405B0057A1C48BC86B00F0CFF0 + D6216E6FEF987E9D7ED90755F662F16FD0EFB9285A752CBDEE42FB7B2EBE96FE + E9A25F5656B9F567A0368EF965E365DA949B5756569D32B28ECF5166FC5F391F + 65F34CFE3B6C6B20303F4BB99966BDD2D002C00E10F82DE61B086CFAE3F7EFDD + 0FF1D6FBBCF0BCC7EFE93BFC4DBFC73C3B447E93F5E4B243E9372A54FBA79B12 + 2B6EE53B9695BDA9B064955BEDAC50B2E06F29E8756867952BDFDCD02864BEA3 + AC9B2CCB0AD8BAE12A5BC78CC17281568159035415EAA6CFDCF8492CAB6EDB77 + 57F0B7426DBD5DD8C8486963156CBD5D28CB60D613ACD5BEBFE60D3E8E6100B0 + 60B0E9AB9EB1AE95D5ADC63456B9BC419435F23B82C40AF516A05DA520D81E0A + 22DB2C716D105737A8262EC02BA8AB320B106BFE1ABBA2EDA6D23A2FE65CE8C6 + 9337AD2AB3F55F17EF73FE1504B92959FF5A16C0A5529EAC75057E05766B37C6 + 47F6FD83E566D972B5ACF29FE1B0CDBDF447F59AA2A2C0AF5D95658D2B57C94D + DB983D796BB7E98CDA727FDCA29DB19EFDA89120AAE06E53BC5F4F16C03CE7D4 + 7B7578335FBB8109E50EFAC3864D51A97173F6D30AD59BB741CD16AD51B3A5F6 + FA6D892A0D9BA05203BBEA37E6F89CAF11D3ECBB0ADBC8025816C1559B0B3837 + 479566CD50BD652BD46ACB39B56F6F8070CDF66D996E83BAAD3BA04E4BA6D9A6 + 86AC8765E5DB4816C6B6F361CE89CE8360B0806F1DCEAD6E3354549AF91599AE + C6854E752E88E486BA7273862DDA730E54CB0EF8B84D17D4EDD403F5BAF43270 + B84A2BE6B7E9801AED3AA17ADB8EA82AF8CB39C845748D8E5D51AB7377D4EDD2 + 13F53AF744ED0E3D50B36D57546FD50535DB7445C3AEFDD1A8DB20D4EF3200F5 + B998ABDB7520EA305E4BFBF7B4EB83EAED7BA336D3F57A0D45032E30EB72F1F7 + 71172E24F556708F7EA8DB6720EAF61B8C7AFD87A0E1202E72878D412369F818 + 341D331EAD274D41EB29D3B9909E8DB65C4037FDE45334A11A8F1EC7F20968FE + C924A3269F4C4453ED354CB5FC743ADA4E9A89F69367A3F3F479E8CE4579B7D9 + 5C28CF9C898ED3A6A32317DF5D66CE46F7B973D1670917FFAB56A0F7F2A5E8BE + 7821BA2E588C7EAB566384DB268CF1D98CD15E1B31CEDF05A37D366190CB2A8C + F1DE8439DBB6627AB03746BB6DC0C0754BD17FED620C5EBF1483D62C41BF65F3 + D9E71CF4593A1B83562EC4E0554BD16FE9020C64D968B7F518E3B101C3D9CFC0 + 8D4BD07FFD420CDDB28CE3ACC5F8808D98182EAB011FCC8A9775820FE6A66CC5 + D29C08ACCC8FC2B29C482C4A0F350F7066EFF4C00C590744386326357B9B2BE6 + EFF4C492585FCC8BF2C094A00D98E0BF065343366376A42BA6856DC2C4A0D598 + 16B10ED323D663419C2B361684C2A5641B9CCC1EBDE158B72B041B8AC2B1895A + 96E68D993B36607ECC666CCCF587FF81ED0816702D0981EBAE00F8ED8B605E24 + 9CF37D39E63A2C8C5E8D7519AE702B0E80C7EE106C290CC2C61C3FCA075B7605 + C277FF36841E8F45C8D1686C3DB41D01477620F86814821806310C64E8CD3E5D + 8A82E0521808578DC37E14DFC2F13C4A82E1BD270CBEACE3B73F82FD076253BE + 3FDCF786C1BD3814EE8521F0D91369E6E9CAB157A7BA615D9A3BBC5816C8B1BD + 8B82B125C71BEE797EACBF151E7B02E1B33F98FD0533ED8F2D05DE589FB6056B + 535DE194ED03A75C3F6C625D1701EA03DBE07F3C1AC1671211A23D84790C4147 + 631076320E0187A3E0B63B121EFBE55E3A1961E733B1FD722E765EDD85E8EB85 + 88B95E84981BC588BDB91B51570B117A2E9BCA314038EA460976DE14D4DD8F84 + 7B07112F0BD95BBB117DAB986131E2EEEC36DAC97EB65F29A0F2117131876364 + 631BD33B6F9618CBDA58815486911C27E25AA109C3AFDB5C2FC7C925B0A08AC0 + C6A393262E28A2B86473B17AD6008D04D68D9745AEAC86AFB1BD5C4E5FDBC5F8 + 2E8E47311DA13D8965D57C318BE97C33D7C4FBFB91F2F020521E1C40DAA383C8 + 7872C8802041A084BB7BB1F34611626F9520FECE1EC4DDE2F1F218158F67FDF8 + 474790F4F8385238BF4403744F214596BB060ED9E08B2C6D939EC98256E0E538 + C313487AC236CF4E22D358B49E45F6571790FDB52C772F1817C4192FCF22FD73 + B9543E6FFA4A111CFD92F12F986F079FA9AC27253FE7980CE5AA5910359D7D0A + E0A60B924A2F65C52BC87B1E59AC932B70FBE565E4B02C5B005863BD386DAC6B + 73398F3C2AF3E519A4BE388E342AF5C5299EDBE34682C4B95F5D410E95FBF555 + E47D730D39B236FEFC12B2BEB88C4C2AE38B2B3C961BC8FAFA3AC7B759690AFE + E6BCBA872C41CFCF38A7CF39F637575947E0562EA8D9FEABCB48D1B13C39C6B9 + 9C42368F55E742C793F9C539737EB2BFD13912E066FE576CC7B4AD9FAB0CAF19 + 09782B2DABE7FCEFB5F7EB2D14FDF10E8AFF740FBB7EB8836CE6E77C7B1D79DF + DD42E19F1EA288CAFB9EF9AFEE32EF2E0AA8DC6FEF18A898F32DE7FDED4D732C + 056CBBEBC73B28FCE12E76FFF91EF6FEF92E8ABFBB8E925797B0E7D505ECF9EA + 0C0A5F1C45D6BD62C45FC940CCA534C4DFDB035941677F739363DF667BCEE18F + F751F823C7F9FE1EF58071CEE1C7C79CE36313DFF5E3038E751F791C2B8F6D04 + 8BF339BF5CCE2BE73BB98D7E80FC1F9F60D79F9EA3F8A7CFB0FBEF2F51F21F9F + A3F0AFCF91F7A7C7C892FBE86F387F9EEF9C1F1FA1F02FCF5847F53E43D15F9F + B1DD5314FCF1090A7F7ACE34F3FEF2C284C57F631F3FD900B0A06DFE1F9F1B00 + 2CE85BFCD79746257FFB827A69AC81059F058155A7F82F6CABBAB21CFEE30B8E + 21D9E0B15C4917081A1B3D33D0577058AEA83596F2A43C8160CA4A0B540B120B + FA0AFEE6F1BC098267BEBA65E06F1A3FE38C6FAE239B9F63CE7782F33C3F2CCB + FE56D0F716BF5702C0B799BE6BB3FE7D65B32ACEE2E7AC3A39DFDCE0F7F7AA71 + 05BD83D79BE89BC5487B7204599F9F40DAD323FCECE4ADA00451BC9EE8B72BF7 + CC72372EA899A8972EF81DD64B1A7A59C3BC98C23CEB050E0B062BAE970DF4A2 + 83A0AE2C7F65016CB3A8671B8669FC7EA73D3B8184FB8711779FE39A175D4E21 + 997D1A782B602B4B5A5EF762B5A7B55C3F3FB701E098676711CDB2E8C7B2D865 + F9735E1BD95F1C7FCFB2D2952C17CD364B60F565DBFB3B49409B6901ED5895B3 + AE00B03C17E8C5157926D8416D7B74CAEC011C45ED1000E6BCA239EF1DBCDECA + CDBDDC3D5BEE9FAD3194B6AC81E5763AF1E945FEEEAFD8AE073C6772FFBC5D96 + BC370F18E02B57D1664F60C15EE60B10EF609E0580057DC3EE1D46E8AD834661 + B70F2192DA7E5B16C507CCCB3782DCF2FE202F0B11770E1A8F0BE1776CF130FE + 478508F05261B7F69974F08D3DD87AA508FE5420FF2F940EE6FF99CD925716C0 + ECE38E2030C736A12C820FD9CA4CC8B930AE5016C746EC5BFB081B4BE07B878C + BB681B34DE8F005924DFDC8720FEAF291DA8F4359BDB6AABBD7165CD3CB975F6 + BB54089FF305F03E976F42DFB30CCF16C0E7DC2E03760578FD2FCB0574092590 + BDDB00604FD69514B70160590317C2FF3CEB9E2D4200F3E44ADAE7422EFC2EE4 + C19F924B698F33D9D8722AD3005D0F59235FCC87C78502035DDDCFE61A1997D5 + E729D53FC73CD6356EA6393737CECD9D737384BB96DC59E6AAF8A95C13FE52B9 + 3FE7AB1DC73110F88405A3D9BFAC8E794C36B86D3B3E592CBB9DC93500D9ED8C + C07421DC29578E65A03225CBE3758752B1FE402A360BE49E10F8D6FEC89A4B8E + 1957F534B63BC71654B681629D0F8E7786E39D66A8F429DB71096E1B6B67F6E5 + 7252E72CCF805F278E25D0EC7C8C733F9CCEF132D9178FFBD42EE6E5302DB8AC + F9E6F3B872B1F16836361F1354669EACA3E5025B6EAC0FA661F55E591E276123 + E7EE743403CE87D20CFC753B960697C3C9D8B82F0E9B0F24602B3FA3E0ABC52C + 4FC09A5DE1585F18C9FBE0306CD917056FDECF791C4B862BCBDC8F24C3E55032 + D695ECC49AA21D58BF3BDAC4571644F2FE3CD4B8945E9219886559BCF7CC0FC5 + DA5D1158551081B5853BB061770CD61547637D4934D652AB766DC72AF6B18E7D + AC29DE8915F9DBB0382BCC40DBF949FE5828F7CBDAD2252D180B9303312FDE1F + F378CFBF382B02CBF3D9B6702796E6722DC036CBF3B66309F397644552E158A0 + 3E9283B1382D0C731202313DDA1733E3FC313B3100B362B76246B40F66333D37 + 2E10B3760660EA361F8C0FDA8211BE1B30CC6B1D867B6FC050EFF5184C0DF1DD + 84617ECE18EAEBCCF866862E18E6EF8AE101AE4C3BA1BFE7260CF076C660D619 + C4B683DDD7601035D0630D86303DC26F3346786FC460B735E8E7B41CBD362C45 + 9FCDCB3070CB6A0CF5DA84515BDD313EC417A3033C30C893FDFB6DC1A8506F8E + E98A7E2EEBD16BD36AF410ECDDBC06FD5D36A0DFE67518E4BC01233C5C3070D3 + 3A745DB6085DA81E2B96A00FD76FFD56AF445F867D562EA798A6FAAE588E9E5C + DFF55AC6B5D6BAD518BC712D066D584DAD44BF354B31CA7523E66D0FC2CC303F + 8C74DDC0F67A917801DA2F98851633A7A1F98C5968BF68295ACF5F8066B366A3 + 05D57CCA4CB49E36076D66CC45CB29B3D06CD27434FD742A9A4EE4DA73E23434 + 18FB296A73ED5A77F82706D40AE83695772B59E38E1D8FE6E326A21DDBB49D38 + 19CD99D772FC34AE5117A02BD586EBD686C3C7A1FED091683CEA130373555FEB + 6001DD7693A6B11DE73576B2F19C556F28C71835092DC6CD44AB0933B9EE9D8C + D6EC577DB79E30112DC64E306D9B32140C6ECC36F5878C3616BFF5078D346E9F + B5FF6FBD81A38CE56F7D8672FF2C085C53DB3CF51A8C3A72F92CD7CF52AF41A8 + D353EE9B0772ED2E57CEFD5183EBF8AA9D7AA14AC79EA82AF06B851D6CF16A2C + 531DA97A67B979EE8B9A5CFF7FDCB5BF09AB77EA83AA1D7A1959FBF7566AD5C5 + A862ABCEA868DFC3B792F6F16D2E0BDF4EF8A869077CD4A41DC3B626FCB0715B + 5468D41AEF376C6980AE00EFFB0C3F627E95661D51A9513B0379DF919B67B976 + AEA397FD6D7AC76EEDFB7EDD664CDBC0AF60EF9BD5EBD95483AA591F6FD5B019 + 6148C620A39A2C7B1BE05D4ACFDAF4DC4E326E9CABFC0C7ADF30CFCD24A62BD7 + C61B94424990F767D536CFEE6CAE9AF53C4FCFD26CA19EADC9BB9FF6EED57335 + F3AC4FA13D6DF21496936FC02A659E8156F8F9D9A6A9C33CC7B8A31C9F859AE7 + B392E0AE15DA21AFE9F303D551992DDF9409CC9686AAA7F15FF39CD65EC74A3B + 825D4748EB982E0F009757EF57F59E2DB49E91FF2EFDCEE7ED65EB94F72CFD75 + 2AFB9CFE75CFE21DE5C8367E971C784CD9F6C672F737F4AB63AA5F8BF9C8F050 + 1CCA2E414681DC3705770557997E97E7E73D7EAF9467E0F01B150CF0950C6C35 + 7B02DB20B0DC4697722F3BD7B2C164B13D3BE72AE562BF227BBD5F70B232FA05 + 047680BF96AC7C6B1EE5F551AEACFABF47E5B5FF3539B435E7EEF7F4F19AE377 + ECABDC7E1CE1EBBF4B82BEC605B442CA11761AE0F9E6CF754B01B0E2E50160C7 + C9FF5F2587F36100B0ED4BFD8B2F76699A2AFD71DAE40881CB9659B280B023D0 + 75CC2B5BE668696C42BD0DF2DF0D80D5B783CC58E5D52B47E6C2A9B88EFD6D81 + 4DFB054D73FC17E6695D80CD05B1BC3F04C73F020795B671AC5336FE7BF4BA76 + 0E7F6CB2AE7DDD9F4979C76149E7C45269BEDAAB4FB9803696C012FB61990D00 + 5382BF0CB507B000B0645C3CBCCBEFE03BFC7EBECB8BA711BFAF060ADBE76DDD + 68302DCB62EB06E57537278E65569EA3946FDDE058B2806FD9FCD7B55528905B + 7A23A5BA65E20A0554756366E5590058710B000BB40AC2FEA1524DFCA132C38A + 0C8DA52CDB5566B9DD6A5796BFA5F09737A41528939645B081BF36CBDE77F4F6 + A06E24D9B7C63020B8AA0300FED8E652FA5DDDB832CF8C23D9E7592A9D0F49C7 + 61EFE76D815E87393A4AFD944A305837AF6CA76334B2F7679D0B330643F5AF73 + 20ABE50A351AE003EA7DCADA435812B416F0AD58B7A991C0AF05804BD3540559 + 0C1BFD0C8015B7AC8195160CAED2B005AA376D83EACDDA9602E0BA6DBBA246AB + F6A86CF6D795252DFBABCFF67617CF1FD46DC845016FEC6B714EB5F97930ADF2 + CA8D9BA35A931646D59BB640AD16ADF1315595F982C052E50602D1DAEFB83EE7 + 51CF405B5908CB52D8580EB76885DAADDBA256EBD6A829B56A856A2D5B703E82 + C16D51AF5D5B349075709BF6A8D5BC156A36E1988D9AB35FC16DF65B5B2EB365 + 35ADF3D2C400F30A3C673A3795048095CFEF4B652E68AA356C652C838D25348F + B54A33B9A56E6783C06DBBA0763B9E07C62BCB2258C0B9752754E379A9D2A22D + AAB664BDD61D51B37D177C2C08DCB11B3E66BC7AAB0EA8D6AC1DAA346DC7E369 + 8F8F5B77439D76BD50AB4D0FF62771C1D7A63BFBEB8ACA5CAC556ADE095518AF + DAA61BAA6A8F9E96CCA3AAB4EE8C6AEDBBA276F75E68D877201AF61B887ABDFB + A35E1FC6070C45E3C123D048FB197131DC68F828341A319A0BD5A1A8D57B00EA + B3BC3EE3F5FB0F458301C35977149A0C1D83262326A0D5D8A968F7E94CB41D37 + 0D2D477D8AB65C2C77E6A2B9DB9419E8F0E944E64F40C78953D163C61C7499CA + C5F7B4E9E831771EBACCE1A27EF264B498C08536EBF658340FBD56CE41E725D3 + D07DF96C0C58BF04FDD62EC230A7959819EA8DA9FE1E18B1790DFAAF5C84DECB + E6A1AFB474117A2D5C807E4B1661E0AA2518B6662506AF5C899EF3E7A3CBBC99 + E8BE780EBA2D9E8D2E0B66A0F3C2A9E8BE7426066F5A8AD19E6B31D677232685 + 6CC18C9DEC7BBB3B266E73C5B4182FCCD75EBE19A1663FAEA5E9215894188069 + E12E18E7B70613B7AEC3BCED6E58991480B5DAF737C11F939937D4793E866F59 + 84F13EAB31C1773546BA2FC250D7B918E3BB18D323D66155BA2F5C4BB6C3A928 + 1C2B33FCB034D50B2BB3FCB0B1380CCEB206CE0FC2AA4C5FACCDF4C6BA0C6F38 + E7FAC1AB28089B323DB03AD109CED9DE70CDF3C39AC4CD9819B41053FC6763FE + B6E5589DBC199BB23DE1511C0437D6DF52E00FEF9210041EDC81F0E331083C10 + 012F96791605C0AB2400BE7B43B09579BE7BC3E05618800D999E1CCF1D9B737D + E05A1C088FDDC1F0D9130A5FCA9BFDF9EC0EA142B131CB13EB33DDE17B20D280 + 61F75D01265F16BEAEF9FE70CBF3876F492802F64620705F248218FA1506C225 + DD1D1BD35C3877CE7117C760E894E58A0DA94E58B6733556C66EC466CE617396 + 07E7E20EB71201E00878EEE718FBC2E0521208AFBDA13C9EED083DB2138147A2 + E073300AFEC71210723A03DB2FE5235AD0D7A8D0B846553CE1B6E0A7F6CC9505 + 710E76323FE1F65EC4DFDE877801122AC1C4F71A682AF02B08BCF34621C2CE67 + 21F8743A42CF6422E8649A896FBF9C8FD8DBBB91707F3F529F1C43FA3341D163 + 887B6083C13B58B6E3CE3EC40A003F398D84C73609FE6ABF57B94B96359EE282 + AB090F8F19109BFCE438E2EF1FC48E6B4588E4189157F2CC9C232FE51910BC9D + C7147955F099F3BFB707A98F0E22F3E951643C3D62E06FFA63E900D21FEE43F2 + BDBD3CFE5DD8765140BC00F1B20CA61239B72496A53C3962AC53D39F9F46C6F3 + B3487D7E06A9823976706BE0ED0B9B55AEE698C2E348792A97C66790F5F20C72 + BE388B3C2AFB25F5A54D992F4F23F3F353C83216AF67EC00F434D23FE71802A2 + 5F9E43EEB797B0EBFB2BC8FBF622B2BF9205EF5964313F9FE9BCAFCFDBDA295F + A0D86EDD2BC9D237FB8B0BC8FD92EDBEB868FA4E7BA1F99C64FC34DBB11F8E9F + C5F133795C0A73BE505A80EA24750AF95F5F46E1AB1B06204B795F5D659D2BC8 + 79C9F4D7D76C12587B2537C9B72157C7DA9338E7D51DE4CBE29361A601B45790 + FBFD75E47D2F2BDC4B14F3BEB96283E71C27DB80E64BC8E15CB304AABFBE84BC + EF38D6779791F1358F87E74A61F62BE6BDBA6EC0AFA0AECDDDB5C21B06B816FE + F18E4D3FDE46C17737392F9B0AFF7817C53F3D40F15F1EA3E8CF0F6D56B93F3C + E018F759EF1EF2BF63FCFB3B28A0F2791CA6BF1FEFB0CF5B3CF78C7F7315855F + 5F44D1D717B0FBDB73D8FDF529E43DDE879C07252878B41B85D4EE170790FDD9 + 71C8B23BFFD56D94FCF13E4AFEF4D0580FEFF9F303ECF9E909F6FDF5390EFEE3 + 250EFEC7E7B6903AF0F7CF4DFE6E33B7FBA5732DFAA3CEDF3D9EB70728FCD333 + ECFEEBE7D8F3B797D8FB8F2FB1F73FBF40F1DF045A65E12BCBDA87C6325790B7 + F8EF9F61EF7FBCC43E6ACFDF3E639FDABBF8318A35C63F641DFC19C39794FAF8 + 1C457F11F0FD1C257FFB12BBFF437D7F837DFFF50DF6FCC757D8FDF7AF0C0496 + B5AF7127FD4AA0FAA901C2253F7D863D7FE538ACB3F76FACF7171B342EE21CA5 + 62B6930C14FE93CDCDB401CC7F79815D7FFD0C050CF36549FCD33314FE55D6C6 + 02D04F791CDA9FF821F2A85C9EC36C594FF333CCF8F6A681C14A6B4FE5FC3FCA + CDF32D6419086CB3B016E89575B72CA2B37F7C824C03941FF07B7487DF41C1FD + 6BFCEE9F47D2A3A3487C7C98BFCFA3C6E578EAB3A388E1752CE26A91711F1FFB + E09801C029FC2D1BCBFDE702BD3600ECE87EDD588CDB2DF925E396FC4B5B9E5C + 91EB05115D0BCC35EBB1DC32CB2DF9095E3F4E204D2F85081A9B6B9CCDF2DEB8 + 2F1708A662586620AFF6FFE5D8719C439C80F093B3887D7A1EB102C2CFB55FB8 + 6D6FF1A847C78D0B67B9728EB383648160C1DF04F6AD9766046DE5A940697914 + 1004960B7A4996C83B39BF28F6BFF3E939EC90A531DB6B2F72ED5F6E2C8029C1 + 63E3E580FD19B8CC72CB125A16C7B10F4FF33A2ED82C302E9D31B037EA9E00EF + 09EC643CFAA15C489F30FB035B40D8E45382C0DBEE1DB5C158B97D160416FC95 + 870701EADBDA13783F22A9F01BFB4C5A507A273F336D09206DBB7B886DF723EC + 26CB1997E5B3DC5E875CDF83A0AB2508BDB187FD6AFFDEBDC64258EEA5C3EFC8 + DDB4C2C30638CBB5B491CA04A165C16C9F8F2C8D8DD5B14033FB3570986384DC + 3D8A50CD5B16CB468A0B2E1F4210EB0771BE16000EBEB11F81D7F661EB953DF0 + BFBC1B7E94CFC56263EDEB63F6062E44C0A512045ED98DADA516BF45AC63B388 + 35C097F266DAB200565B4F81E3F3BB8C15B1DF396DB7C07E2EEEA2F211787917 + FB2B645868F61DF63C9B63F622F6D15EC302D0EA8B6D3D65414C790B1C5F92E4 + 663A0F5EE76C6EAB6D63163114802E34C057723B23486B9360AB42ABEC67E599 + 7C8F731AA708EE3C56C995FDBA9E63991997F393BB6C8616FCD5317ADBAD817D + 78AC9207E720106DB304661F94CBC95C38CB8296A1A0AE37CF81A4B13DCEEEE2 + 7114D9A53D99771910ACBAEEA779DE584FE7CCF72CC7A13C399EC61068D6389E + E6DC30AE7D8905922959FFCA05B4D3D16C8E27005FC2FC42B89CE0B19C66FD73 + C5C6ADB693B10ECE662848CE314FE7190B635914BB9EC8E167C1F3274BE5E3E9 + 703E9A8A2D94AC7F5D8FA41A98EB76340D21FCEEEA250AF763E9D850BC039BA8 + CD45DBE0B63F061E8713E0B23F0E2EFBE2E0CE36EEC7D2B0716F8C81BF9BF7C7 + C3E94002D3B15857128555BBB619AD2BDE8ECD7BA2E1CC7C95BB1D4983872CA8 + 0FA560F3BE446CD8138BD5455106FC6EDC97C03AA9C62DF5DA62E6E7EDC08AAC + 08ACC8DE86357951589BBF13ABB27760494A28162406617E5A1896B06C657E34 + 16674532CD3501C345E9915898168979C921989D1080F9C96158901AC1782866 + 6A5FDEB840CC4C08C2CC98404C8C70C78430174C8EF0C68CED81981CEE87913E + CE18E8B60E433C366084AF33067B6DC200F70D18ECED6487BE6E18E6E78621BE + 6E18EAEF8E41CC1BE4ED8C3E1E9BD1C5652DBA39AF460FA7E5E8B969097A6E58 + 8C9E1B97A0F7E665E8C3BC7ECE2B0DF0EDEFBC0A7D36AF403FAE9D066C598341 + 6EEB31C87503C774C2302FF6C7BE0632DE8F79DDD7AF40F7B5CBD063ED727459 + B9189DB8BEEAC6B554AFD5CB31C4792326F87B6388D346B49FCFF5DA9C59683F + 67363ACD9B8B2EF3E7A1DBC285E8B198F358BA14BD962C45F7858BD091EBBA8E + 2CEFB97C29FAAC90BBE4C5E8B77A19866C5A8D59A15C2B1464605D4A0C3E7177 + C2008ED97BC522F63B15CDA64D46B3A953D17CDA2C349E3C1D0DB9666C3C693A + 1ACBB276DC2434A11A8CFE1475477C823AC3C7A29EB6321A3D01F58DAB65B96C + 1E85868C0BB8D61FC2F8B031464D478D474B59F6B25D03E6371D3981EBD51968 + 3F61BA29ABCF356FE39163D066E264AE43B9969D3C159DA6CD448F39F3D175E6 + 6CB49B34152DC7719D2A17CFDA4269B8AC7C27313D959A488D47B3B1E3D07CEC + 78B4E25A5640B80DDBB49AA03613B9961E6BC6683474141A73CDDC78E8583494 + 868C657C1C9A0E1F8F8683C718F85BAD5B3FD4ECDECFC0DE3A3D07A14EAF4126 + 5E5D56BEEDBBA362BB6E26ACD2BE072AB6E94A717DDFA107AA76EAC990E56DBB + 9AF21A9DFBA07AE75E060C6BEB28B5171856DD4A6DBB99B695DB754755AA4A6B + EDE16BB7F86DD199A11D003717F8ED880F9B74B4C1DFC66DF051A336A8680F3F + 6CD8DA58F1BE5FBF052A346C850F1AB4C4878D5AA352A3B636EB5E037E9B1AF7 + CE96E4CAF9CD1A0DF1D6C7B66DD58C6AC9D2B73EFE50B54EA904826D46173FCB + 00603D7FAB260B5E79D9B3C902C032E83016BD26A40C10AE5D0A871DDD3A1B31 + 6E7B0627D9F2141A105CC9FE4C4ECFD828C7B8F58CD23C5FB33F63B3D2D6F337 + F30C4EA1C3B34D2B6DC51DA16E59993207F0FB8BBA6540EDEBF4A664015BBB1C + 21AE911D129B3A65DA979579AE5D4EBA6CFEAFC93C737654D967E8E5C9AAF35B + 75CBD4B39E9FEB59FBEF01BABF57D6F3FB7F49E224169B61DAB11F63A1FC2F70 + 9472257E627120A6DF9065A9DDBAF4EDF7AA98ED202D2E55AD664374EED61F1D + BBF64325FE5E8C55F01FDE3770D5B2FA1564351270B57896A55FE37AE5713F2B + 8F323C4C5C8C73FBC33B0EACCC2ECDC58C6F07BD16F8B5F46F07C0E5B5FD3DB2 + B72FB54E7E4DF92FD23A0E1D178FE99FEA95AD6FA4FA76F0FAEF90B5EFEF5B6F + 8AFEDBE0AFF2FFE008811D2C7ECBB637F55E3BF9FFDBE4704E280BFE5A521D85 + 7ADBC2D4D70FF3352E9D1DF3CA9659E58E617932EDCAE9FF9F2E14FF4F6541DA + 32FDFFDE31CCC55371B5653FA69DD5D6EADBA1FEEF91B98896FD4328F327F0BB + E5D887A3CA2B774C3BC6AD3F6DC60DB4FD8578FC76694F029BEC7D3BC8B1CD2F + CA4AC7633B0B00975A03735CBB05B0AC85B507F0DBAC6B732FC00BEABB36BD5D + 817F061FF0022A4B628D6DF569FAB5A5CD4D82FD46C5BAE1B0D265E3AF4BEB86 + C851D68D9375F364C9B19D635B85A500D36A4799B154660FCDCD9BBD4F53DF6A + 6B2F3380981280356E92EDB240AA058005402DC86B8160CB0258EEA0ADFD4174 + A369DE30AC6C1BD70066A60DFC653F82BEEF5BAEA50D2CD64D686D63C16B00B4 + 60AE357FC539DFD2E3B68E45F362F886CA047EED3210D95E668EC75EDFF1DC5A + FB8C389E2FC99471AEA516C7E65874136D9BBBE62AA8692C7EEDF05716C21FF2 + E6DC9200E807B26ED6DEC0D4074AB38EE592FA67D7D1EC4710B8510BAA252A37 + 6E856A02C12D650DDB1A959B31DFEEA6F9C3068DF17E5DB6A9DF081F356C828A + 82C27579FE6BD747857A36005C5116BDAC5799E502C082BF359BB762BC25AA31 + 2D6B5D5904576BD21C951BA98F066CD3C080DB8F1A344495264D51AB552B34E8 + D01EF5DBB747EDB69C4B8B16A8DEBC396AB66C89DA6DDAA00EA53A356425DCB0 + 312AD56FC83E1ADA42CEE3A33A94390F3A2F7A21C006CE6D90DC660D2C082C20 + 5C59EEA1EB3787F63DB6B98B6E665C5B7FC0F8473C17959BB541A526AD51B191 + F611E66249F1C62DB9C0D2BEC56D505516C302BEADDAA32A55A939175B0D9BF3 + 1CB4E09C6CAEA72B376E8BAA4DDBF1F8DBF1383AA046F38E3CB79D5085E1875C + 7CBD5FBF15FB635F2D3BA146EB2E26FCA8593B54D0582DD8467B0D6B8F61AA56 + 971EA8D3BD371AF41D8846FD06A361FF2168D86F08EAF419806A5DF55631EBF6 + E88BE65C38371B3C12B5E592AA736F34EC33046DE40E6BE4447418370DDD2671 + D1CF0575BB3113D17EEC44B41B3D0EAD65513C78389A0F1D81765C1C779D3CCD + C0E0D65C2C779C3805ED3F9DCC85F9783467DD665CCCB7FEF453749E3D111DE7 + 4D44F745333174ED720C59B31443D62EC3444F678CDBB211C3D60AF02E419FC5 + 5C9CCF99858ED3A7A2EDE449E83C6306FA2C9C8F814B58B660113A4C9B8EB653 + 27A2CDB489683D85634E9EC0F804749E3315FD972FC09075EC77E3728CF5DA88 + E9115E9812EE8109C1CE662FAEB90901589A1E8E6549A15896188CB93BBC31C1 + 771D466E5986719EAB3127CC15CB62FCB1949A11B81983D6CD418F2593306CD3 + 7C4CF15F8F49BE6B3074E31CF45D3D198336CDC4E4A0D55895E207E7FC70ACCF + 0AC4C25857CCDABE11F3A29DB022C3CFC0DFCD05C1702B8A846B412856C66FC1 + A21DEBB036710B56C76DC6C2C89558B27D0D9645ADC5C2F01598B97501A6F9CF + C39CE025460B2356604B8E2FFCF68463EBEE70F89584C1B320101E795BE1914F + 156C856B9E0F3667B9C329DB13EEBBB6C2B32810AEF97E70CAF2C6864C0F38E5 + FAC0AD58B0350C5EBB43E0B1CB1F6EB9DEACE30BB73C3F6C4873635B2F041DD9 + 89D023D1089285F2C12804C885747130B6EE8B44C4B158037F3D77051A101CCC + 3A5BB27DB036C9191B39F6E61C4F6CCC70C3C6F42D5893E88C253CC635092E70 + CDDDCA3905C1BD301801ECDB97F2645B23F61F742C0E11C71311792219612712 + 10703C1601271311742209810C434E33FF4C2A9582F0B3A9D871210BB157F310 + 7D3517DB2F6699FD85E36F1622F5DE3E24DFDD8BD89BC5D87621171167ED6EA3 + AF15603BEB6FBBC2BCCB39083A9DC67E5310762E0BDB2FB19F6B85C6A256A058 + 6E56D39E1C46C6F3E3C812E87C7102A9823054EAB3E3C6A237E1E171241B3042 + 3D3B65206A22F3E31E1C3656BF498F8F21F9E161243F3AC2FC8388BFBF8FFD17 + 23F606C7B95180986B1CF34236C30224DCDB8DC4BBC54879B01B198FF723FBC9 + 01E43C3D88F4479CCB3DCEE94E21E26EE521F1F62E24B15EF4B55C6C33C79C8B + D8EBBB90C879A7F3B8B39F1C41F6E7B2523D479D478E40AB40E96702B63677CA + 0AD35E68AFDC7366DEA9942C6DB3BF3C87826F2E621795C778F6E7670D6C95E5 + 61FA731DF73103A194CE1108B603E19CAFCE19E82BF85BF2C7AB28F9F12A76BD + BA84BC6F2E209FDAF58A65DF32FEF539E47E7D0139B298FDFA22DB5D60FA12EB + 5CE678CCE7DCB25F728E9C8BF6AC4DE7B9CFB5C3E82C01E1674791C9F1735F9E + 46FE5767D8E60CE7781A393CBE42F651FCFD358E7111F95F5E44C1D75790F715 + FBFE4A73B969D377D4F7B750F0FD6D63659B4F157C779BF9F78DE40A39EF7B59 + D4DA94FB4AFB145F452EFB366E9BBFE079F9EA0A8FE53A0A5F5D679FD4F73750 + F8C34DB6557DB974BE44B19EDC2FB35CE037EFBB1B1C53E3DE32A0B6E8C7DB28 + 16FC15B4E59C0AD40FE752F4E31DECFEE90176FFE5118AFEFC08857FBC67E06A + 9180E91F1FA3E8870728F9D323EC6659D10F7750F02DFB65DBE23FAA2DE7F1CD + 25147E751145DF9E4711CF75C9D76751FCF238D2EE1421F5563E72EF15A3E4D1 + 3E1CF8FC18F2F9FD90D573F18F0FB1FFAFCF0CDC3D4C1DF8DB33ECFFDB73E63D + C7C1BF7FC6F40B967D6640B0C2FDD43ED6D9F3D727D8FBD74738F08FE7D8F7F7 + 172894452FB5FB2FCFB0976D76FFED33EC1130FEC717D8F31F2F51C276BB99AF + BCBD7F7FC9F00BECFFCF2F71F0BFBEC2C1FF649C75F6FEF5058AFFFC147B38FE + DE7F7CC67A1C8BE31A406CDA7D8103FFF935DBBCC201EAD0FFFC0E87FFA7E2DF + 729C6FD8FF97669FE01CEDEFFBDD63634D5CF4678EF9D79738F01F5FE310EBED + FFFB57D8F39797D8FDD72F8C0483F7B29DE0F01E86253FBD649BCFCC9EC3257F + F9DC00679B95B1E0F37394688ECC2F615A2AFA49D6C4CF50F897A728FCE9B1D1 + AE3F736C7E4EF93F3E40FE1F6589AC3D87EF2047D6D27219FDED6D7EA76E1B6B + 60E392FAC767C8FAE129E4223BEFFBFBC63A5870BF50DFCF6FF5F2825E8A38C3 + EBCF095E478E20EEDE41C82D73ECBDC3E68513C15859AFCAA2576ECE53CCDEC3 + B6BDA76D2EC56DAEC8ADB471C9FEE2ACED8510B9427F667B59C502C1B2068E96 + DBE47B878CABF9145EBFF49245F2E393B6175A9E9C408AC02CDB1837F76C1FAF + 9749D8B7A5448E91F8C2EE1EDA00E1B38811A47D24F7CCC711CD79C70AC6AABD + 424A16CBC68DBE2C751F9F30703999D7AD64792D101C16D0A5CC3EC58F4E18B0 + 2CE06CAC8DD93E8675E238AF58CE5573B25940338F7D194F0C3C36CB334332E7 + 942838FDF80C769A7D8A057AE5265A6EB139D633963F3FC75080F814C7139416 + 80E6F9615FB2EA8D796493A07014B5FDBECD3A582EAB059D65652C37D0966C56 + C73637D4DAD7D8DA8B7DC7FD2306065BFBB32B6D83C27B4D182E8B617E1611F7 + 8E2052C05996BFB70F21E4D6410367436F1CB0ED51AC3D85AFDB20B1406F9880 + B480F01DE6B30FB9900E51FE5D815EB9903ECC3E298E67A91408DBE1B219E7E6 + 01037F7D2F161BF81B746D1F42AE1F4030658B734CCE411238DE7A8D752F97D8 + A1A880A8AC7E8B8C4B6BED092C80ABFD85B50FB0DC46CB725816C4DE67F2E0CF + BCAD9401C1AC176000B0DC4B0BE61670FC42D38F712D2DB82AC8CA71FCAE1461 + EB55591D0BBA0AC0DAAC937D2E9518F90A5CEB18184A02C28EB2F25557694163 + 4910D50064416BED67CC31242F1E8FB7C6521E432F8E29E86A00F765415F1B14 + F63262BED23C275E574AE0C9397B098C731C1FF6ED73C1165A69AFB3EC8BE7C3 + 8BE7C5EB1CCF952030E31EA7F38CFB678F333C5F6CE37B9E733ACDF8391DF36E + CE89FD5FA42E1432ADCF4A71B63B27A05C0077B6B7B990966535DB5ED80DB7D3 + 850644BB9ECA8727FB91DCCF1460CB49590FE71857D04EC7328CE482DA4D7B40 + F3B370399686CD4792984E87FB8934B81C4986FBF134037337EF4F84B34030DB + 6C39920AE70389D8240BDFFC306C28DA6640EEA6DDD1D8C83CA7BDB1C662D8E9 + 60A281C75B8EA41880ACD0F9509201C2EB596783DAEC89C1E6BD31703998048F + E359F03C91C576C9585F128735453BB162D736ACDC250BE038381F4CC716C16E + B9B62E8EC5AAFC28ACC8D9815579515853B0132B73B763516A2866C5FA615294 + 1766C407605E5A38E6326F8EC070460416A447624E7228A646FB637A6C00E60B + FE26B24D7C30F3B69A3D7AC78579606CB02B86F96DC410EFB518E5EF84B181EE + 18EEBD0503B6AC477FD77518E6E58C613E2E18E0BE117D5CD662801BD737BEAE + 1813E28D51815E6CCBFA010CFDE592D90383BC5CD1DD7903BA3BAD411FE7D5E8 + EBB402BD372E43AFF54B8CB56FF7B50BD173DD620C74598D415BD630E43AC47D + 0386B1FF21AEEBD16FD32AE3CEB9DFE6B5E8EFB40EFDA89EEB56A0F3D285E8B4 + 643EBAAF5A8A9E6B96A1CBF2C54C2F40B7250BD177D54A0CD9B401BD962F4387 + 7936B0DB6EE62CB4993A8D6B2AAEBDA6CD409739F3D06DDE42AEC7E6A3E36C96 + CF9889565A7F4D9FC1F5D65C74D70BBA2B966100FBFF84C73837CC1F537CDD30 + 90E9DECBE6A3DB823968CD755AF32993D062CA54B49C3A0BCD26CF44A3F153D0 + E4D3A968368E65636D1EA81A8E1C87465C2B36183E0EF5868DF9594347A3F188 + 71683A7A3CEA739D2A351E36D678B76A3C42E128D4E1DAB6765FAE71078F4673 + B9761E2920ACBAC3D18C7DB7E39AB423C7EF30690ABACD9C8D5E73E7A3D3D419 + 68C5F15B714DDB661CE73666329A09FE326CFD89D6B35CC38EE1BCB8C66D3C7C + 94B10496B7AB0E93A6A2F578CE9BF3693AE213A316A3B8D6A504A29B0CFF048D + 067FC2B98C45D361E3D064C8275C730F43F56EFD8C1B6759EFD664BCD4E2B763 + 4F037725C1DFAA1D98E6DAFEA3569DED20B787497FD0BC033E6AD91955DA6ACB + A8CEA8D4BAA3D93AAA6AFB6EACD70595DA7466BD4EA8D48AF9AD55AF8B092BB7 + EA6453CB8EA8D8A283D1475485A672F5DC061F366A850A0D5BA282202FC30F19 + 5610F8A5DED7F38E062D4C1D85B2EC7DB7B6F6F16D8CF76B35C1FB759AE27D59 + FD32EFDD8F1BE3ADEA72EB5C1F6F7FDC006FD76A807718BE59AD2EFE50B5B681 + BF6FC8C39D9E9FD9F5B642C15F87675692057A552697CE82C3EF54F9596FEBD9 + 9C25E3DAB9362CD8FBB398FE90AA50136F7F580B6F1B106C7BB6679E1F2A4EBD + 0E0097A7B2CF35AD677C65CB1CC1AEF5ACB56C7979F54C7E3970552A0B722DD0 + FB963D2CABB72AD430B2D28E6D1D653D272E9B769463FDB22A3534B23F1F2F95 + E3336EFB33E85F55D9FA8E2AA7DC7007AA3C232CC7747972640DFFCB12EB288F + C930FD2F19EA59ACC45EBF749E26ED98AFB16C0C4AFBCDBE2D66F0460563CDDB + B44547CC9CB31453672E42ED7ACD4DB9789DEABF2500CCB602AD02AEB2043690 + D2CEB42C954D1BBD8EFD39E46B0C63FD6A07C0A61F8160FBD6A91600B680EF6F + C978D8751CEBB7E4309772555E9BDFA37FB1FD3F1D4739758C1CE7660C6DCB40 + D7FF4E19F86B857600FCD65B1FF24BF34BD8592A4EC8B1FDFF1F00FF3300B6C5 + 29814FE30ADA1E7704A8F6B4A3AC32ABDC312C4F8E6D4B65BF18FCB7C8BAE8D8 + FB7E432AAFDE6B642EAA8A973727ABEFB2F9BF47652EF8FF8F55F68FC65279E5 + BFD6A6EC1F5C39B259F3DAD3E6CFD1412A2BFDC3B4E599B455BF8C6C2E35F8C7 + FD1EA53F399E137D1FDE7EB79A1D02EB0FA022DEAD50C5B8817EE35DFE4E75E1 + D51F87635FF6E311003696B41F304F371A656E481CF3744363C1DCDFBAF1911C + 6FA0249597D79F5557F15FDC7439D4B1DEBC53996ECE2C39F66FA4F6CC979B65 + 0159B959366E5F2AF3E68E71CB02D8BA89140896DB67E3FAD9FED6A1797B9065 + 7A93D0ECB16B87B1A63D6F4E0DEC653F0602DBE38E37AAE666D502C19C9381BB + 16CCB5CFD19AE72F6E3C1DD28E5299F5A6A1E3B97D8FC765DC493374BC61B5E2 + E61C993A9CA7E6689FA7E66CED0D2C186E85EF7F2CEB6095A99ECE17E759957D + 56AFCD36750C107EEF6381731E3BEBBD5B93FDD6ACC39B7D59E336B4B94EAEDB + 18151B3633C0B74AD3E6A8D4B419AA08023715CC6CCC0544437CD4A809AA31AF + 7A1396D76F848A54B5C6ACD7B8393EAA67B3EE150016F8ADDDAA6D2904AED9AC + 95894BB55AB6C1C72D0583E582BA113E6A509F63D765DBBAA8C274758E5BB549 + 13F623B85BDF84551B3736AADEB4296A685E0D3976BD7A469554A7810D02DB40 + 30FBAC2B2B609D1F597AD76558DF58FE5A56D296A5F47B02E3B294AED7940B25 + 2D92B840AA6F03C292F23FB0EF9DFC514381F0E6B6BD91E576BA696B546AD61A + 159B0A10B7B2EDA5CCFE65555D858BAECAECAF0A175A551BB6E6396ACB79B763 + D886C7DCDA80E18F1AB44605DED499BD979BB6459526EDD8472BB360D31ECC55 + 9857AD7507D468D311D5DBD85C4ECBDAB86EB79EA8D7BD37EA75EB8DFA3DFAA0 + 4EB75E5C347642B5769DD1B8D700B41C381C0D7B0E40752E26AB73C1D8A06B1F + B41A3002CD068E44CB2163D0860BF2D65C844BED878F41CB41C3D1A8577F34E8 + D197613FB4183004AD868C401386CD060D439B116C33722C3A7061DF612C17D1 + 5C04371838988BEBA1E83CFD53F45B300F43972F45BF8572273D033DE6CC64FE + 54749C3C998B6BB9DC9A88D6E32670F1CDC5F5C8D15CCC8FE5827C3CDA72D1DD + 860B76B9E26A3541EEBEB8001F3D1A8D4772EC519CEB2763D069CA242EF0A7A2 + C7BC9918B67E1926F96DC1E4ADAE98E0B719E3FD37636A9807E6EFDC8AB9E1DE + 98B6D5199FB8AFC2B00D0B8DC638AFC0A7EE6B31DE7535466D5A8AC12BE7A0DB + DC89E834732CFA2E9D8A4F5C9663DC96E5E8BF7C1ABACE67DE8A2918EFB1140B + B6B96075822F5627FA6055A22F9625786271DC162C4970C7AA3459FDFA617366 + 20D6A7FA604EE81A4CF25A80B9412BB0286C1516842CC1C2D0E5581AB90A8B18 + CE0D5888597EF38DA679CDC64CDF795813BB194EE99E7049F38453AA07D627BA + 606DDC26C65DE19EE70DCF023FB8C8CA366D0BC771C3965C2FB8647B32F4864B + 9EAF9173AE0F36E57863932C82D35DB12985738EDF80953102B59BE1C63A02BF + E14762107230CAB87ADEBA3F12FE7B231072241A11C7E34CDC2D7F6BA936A67B + 6075F2161E1FE795EB0FE79CAD667FE00D295E5893E806E7ACADF02DD9868083 + 71083E9684B0D369083A9902BFA309083E958688F359D876361BDB4F6522EA54 + 16224FA522EC6C2295C47AB1F03914692C867D0F6E33D6C97E87B621F4441CB6 + 9F4BC1B6334988A022CF2663FBF954C45EC941CCE51C849F4941E0D138041D89 + 45C8A964845EC840C4A52C6C67F9362AEC7C2695811D57F29078772FD21E1D32 + E0572E97D31F1D66FC20921E326E07BF292C4F51FE93A3487ACCF41381D153C6 + 0A557026EDC931D326EECE5EC40B22DF3F60FA4DB823805B64C06DCADD22A4DE + 2E44F2CD7CA3F45B05C8605EE6A312643D2846CEA3DDC8795882ECFB45C8A252 + EF1620F196206F269581A4DB7948BD5F88B89BB9D879250B71D7F3596737321E + EC47EED3C328FCFC0C0ABE38675C26177C7BC94856B872A39CC1B2ACCFCF53E7 + 8C75A1B1B67D6983C4058287AF2EA3E8D525E47F79D640569B4E2183FD0A500B + 5EC7DDE6313CDC87CC674790FDD909E47E71DA80DD826FD8C7D7EAE71CFBB988 + C26F29F6B54B10F8ABB3286479B1F2398EC6CA1368E678C5DF5D33D23EB5799F + 9F45DECB73064E6AECBC9767B0EBABF32854BF8CE77E769C754E20FFF393AC73 + 12055FB20EE790F3EC280A39E73DDF5F45C9779750CCFE4B5E5DC1AE2F05832F + 33EF16C7B865809A6460F0F7B75024D7C7DFDEE4FC6F72AE02B2B751F0FD3514 + FC7015BB7EB886FCEFAEA2E0BBEB3C87578D7BE95DDFCAC2F6068FED86E96FCF + 1FEF52F76CFDFC7813457FE4387F92752FDBB09F42037C6F1B37CF45AC57F8FD + 6D14332D95FC7807258ADBE756F2A3FABA8FFD3F3DC6BEBF3C46F19F1FA2E44F + 0F0CEC2DD65EB63F3EC49E1F1FB0FC8951B160E21757782EAF62CF9F6FE1E0DF + EFB0FC320EFD740347FEC26378790C05CF0F61D7B303D8F5947AB21F458FF661 + F7E37DD8CF7358C276C5DF3DC2C1BFBCC0917FBCC4E17F7C8923FFF1050EFDC7 + E7382418FCD7CF7094E923FFF19279B20066BEAC80592E407CE0EFCF8C0EFDE3 + 33862F38CFC714E72660FC1F36502CA87BF01F5FE1D0FFF8DAE8E07F7EC9B22F + B0EFEFB2C6FDDCC40F31EFF07F720C59190BF6B26F8D75F8BF0485D5CF0B3B74 + 569CF5FEF31B96BD625FDFB01EFB35E96F197E6B20AFE06EC99F391F86FBFFC6 + 3EFEAE31BFC611B639F23F5E71DC2FB1E7A7CFB1F72F9CC75F191700FEABEA7D + C5FEBE611B3B20FECBE71038DECB3E14CAAA792FE72C4B65CB9AF800C7DF276B + 64D5D5FCFEF18501DA964573C95FE40AFB19F5DC5836171817DAF790FF3DF5ED + 7D64BFBA870C853F3C458E00303F8F3C638D7E9FDF9BFB3C9F0F512C77DB5FCB + 95F9795E67CEF09A63B3784F7F7196BF69B955B7B92DCFFCEA92ED658357B721 + D7E0D972414DC9EA5C56E899FCFE6A2F6AC972FD9CAA174164116CE2B6B4B1EC + E518666FED8747F8BBDF675CD127F37A272F06294F8F2395D7BC645EF312984E + D04B2F06D49E432AAF27529A2C8BA974CE29ED8BCB487C710109CFE5225AEEA2 + 4F18CBDC84A767390759245F3456CB09CFCE218975525E703E0C0DA87D2137D5 + 362B6603802DE0CCEB6EACAC78656DCCFA466C132FE04BC9AA5896BF9A977155 + FDD0B64F7802DBC8557FF2338E4D99FD8B396E3CE76293F6041670BE60F6D7CE + F8F20AD2397F9BD5B4CE99A0B0FA3F6DA436B18F65452C10ACBDDE0587999675 + F3839FF71A8EBA77C44871E5EDB87B183B98967B68C15EC160C15F85920584B7 + 194BDF03D8769FF51F1F37FB29EF7878C2EC3BAC7D8665A11BACBD8A6F1C40C4 + CD8308633AF4DA3E0384057FB50F7128F323EED8F6260E9565F1EDFDC60575A4 + 2C7E1986DD615D03986D9059EEA4C3595FEEA523EE1D35C0593058A0597BFA06 + 6A3CF61DC9BC48D531EEB265E92C007D8063734E9C438060ABB17E158C2C8405 + 80B59FB000B02457D2DA3FD8EF7C21B65E2882DFD90286050864BE147A6D3742 + AEEF46C0E5624AEEB04B204BE8E09BFB1164AC92F79ABD86FD2F97705EF632ED + 9F2C37D51777612BFBF0BFCA3A9A0FCF4D20E7E7A820CED951AA1370FD80A9AF + 763660AC7D8CF7309F21E5C771FC6E94C0FFC66E13F7B95A04AFCBBBE04D997D + 921DE0AF37C71728967C04AD6FEC85FFCD7DF0E57179B38EED1CF0B8799EE412 + DBDB005F8171016059FBCA3A38C758072BED7E5AD6BF7936107F69373C595FFB + F97AB25C40D75801334F7B2BCBFA58E7C65827B33FB9DAD618EADFE38C00303F + 0F1E9B8E4FD0DBB28E3656D10C4D9AFDBA1CCFE21852265C4EA4536970399682 + 0D0762B17E5F345C8E2460CBD1786C3A188BCDBCB79336ECB3415D0BE26EDC13 + 8D350511589913828D2551703F9C0CCF63E9703BCCF2FD89D8B83FDED4F5E018 + 92FBF10C23B7E36906246FDC1B8F7525ECA36807D616EFC406A65D0EAB2C156B + 4B62B17257145614466159C1362CCBDF8E757B049405A253D92E012B0AA2B030 + 33DC58F62E4C0FC3B29C6DC6ADB3DC377FBACD1D23829C313ACC0D9FEEF0C1A4 + 9D7E981A1B885949615428A6C5056202F3953723210413B7FB62667C2826476D + C5B0AD4EE8EFB1167DDDD7A08FEB2AF463282BDF819E9BD1C779A391AC6F87F9 + B81AE03BD8DBC5A44D9EAF1B46057861C4560F0CF5F5C0103F4F8CDCEA83117E + 5E18E8EE8ADE2E4EEC731306B96DC630B70D18ECB216839C57A3FFA695E8B672 + 01BAAF5A68F25436D4750386BB6FC660C1DEF52BD17BCD72F45CBD0C7DD6AE44 + 4FB95E5EBE045D972E46C7F9F3D072C634B49935139D162C4067AAABB1EC5D8C + 6E0B16A2FDCC99D42CF45CBC047D962C45D739F3D076D254B41837092D264C41 + BB296C377D0E3A4E9B830ED367A32DD3CD274E432B86BD162E43BFA52BD09D63 + 749C3115DDE6CF44BFE50BD077D97CF3326EC75993D17EFA24B499CCF5DAE449 + 68FEE924B49E3C136DA6CC46B3F153D14CEE9C470AD65202A92329AE351B694F + 5DAE31E5565956B502AA02ACADC67C8A868347A211D56CD818D6938BE561A83F + 7098F16455B7DF10D61FCD3EC6A2C9B05126BF2ED7A40D070F637A24F3D96EC4 + 28B49B30111D274D411BAE295B8D198F366327A295DC4B739DDA6CC4A7ACCBB5 + E627D33172C51A0C5FB69C75C6B2AF4168397AAC79C9B9F928CE89FD3596C5F1 + B0B1683E82ED384779CD52D87CE404341830DA58FDD6EA3D0475FB0E471DC62D + E05BA34B1FCA161A77CF1D7A18CB5EC1DE6AED7B185592E56EAB2EA8DC9AB2EF + D3FB81DC33CB05B35EFA6ED10E159AB5C6472DDBB25E3BAA3DEB764065AEFBAB + 30D49653555B7744E596CC6FD98E7DD854B1591B237930FBA0510BBCDFB039DE + AFAFEDBA9A429ECD3EB0C7E5BE59E1FB759B9A671AD6B30D3DFB78F7E38646C6 + 9B9EBCA631942C8F7AE6399B3196A06AD4335BB4BD616D6F56A3AE312878DB6C + DBA6676FB56DCFEDF42CCD9E67647FB6F6AEAC80F54C4BE0B7529D9FA5ADDAE4 + EAD958FCDAE1EF871FDB20AF8394F766053D53B49755643D3D97A36C7BF89679 + 0EE9F0DCAD3C59CF331DF5BAFCB2CF54CBAA6CBFA5F51D9F0BBF5BA5345D1EC4 + B540EFDB1FF2981C60AF25AB8E649E0197692F59CF891DE36555B68DA3CAAB5F + 2AEB79FAEB9EA99727C736BFD696F9C6F0AA1CA85B36FD6F95C568CA728ED7E5 + 97278B9338E89F8FC1D68FB1B0B5B32A3128E3C6F94DB1BC8AA853BF0506F33A + 3660F01854ADD1C00060C3AA048D25C52D4B5BE31EFA439357DA17EBA88DCDC0 + CC965F1EF333FD58E58EF9D61812E336106C2FD3B876F8FC7BF4BF0D00FE97A5 + B11C555E1DEA17F3FB7F03000BFADAE1EFCF0098279B2A0B3D4BAD814BA53CFB + 64CB3B98FFABE4783E747EEC5FE8F2540A68191A2B5DA51DA532073958F23A42 + DE5FD32FDA4BD645E2171787FF55713E9A13E75C3A6EB9F5FE59A517E0DF9A97 + FAFC57E6EDF807F0BFA2F2FE3CA4F2CA7FA3CD6BFFF0EC6F40D9F66978BDAC3F + E57FFA0335E32AB4C7996700B07D0FE0371997F5EF5B6F536F55357B00DB2EEE + 1FE1ADF72AE2ADF72BD9F6FF2D0F005BD258663C967FC0FE7523C23959371EA5 + 3720F69B93D21BA5D7DCE0488E37318E37502AB3FAB2EA59FD39DE84596DCAD6 + 51BC146CDAF58B9B347B7D534750D441665F5D4A379036786B7F8B90A17583AA + 78A9FB98B2005837A4BC09B5AC412DE8EB0881ADB8E0AA42956BCC52F8CCFE04 + 74AD395916BA8EB057C76459F65AE5CA33C74859105869EB5C287494756E2401 + E2F779C3FC9E8E93B2E6ED2873732EF05BBD8ED1BBD578F35C59D09A9F012510 + 2C002C382C186AEAD4E039AACE1B780162C60D0C1624AE55DF40DC8ADACFB751 + 137CD440EE999BA272134960B899B1E0B559F5B680406F5596D7B643DE2A8C7F + 54AFA171F5ACB401BD0C65F92B8BDD1A720D2D97CD8A376FCEBE9A32DE04551A + CB7AB83E3EAC53171FD5A9878FEAD645855AB5F959D4E0BC6B32AF0E2A0BF252 + D558B746D3A6EC476337E2580D4D58ADB15C4C3742255914D7E571D46D60B308 + 96BBE9DA75796C3CFE5A8ACB6DB8DC87DB1634EFC8029CE90AB20EE6CD564559 + EF521F09F4D6B3ED935C810B254160A982B116B62DAC2A18697FE4E6A6BEE079 + 25A6AB302E5556282B60F65BB5412BCEB135E7DA9AE7896193B6669C0FEAC892 + 5A6337377DBFAF34C38A6C57B5491B54E7224FFB316B7FE4CAADDADAF722EE8C + 1AED3BA36ADB4EA86EE25D0C1CAEDDA91B9AF61E8826BDFAE3E30EDD6C2EAAB5 + A773FBAEA8DBA9273ED61EC55D7AA376D7DEA8C5749D2EBD0C44AEDDA5076A74 + E8CCBE3AA1A601CCBD50A76B4FB3B771BD9EFDD0B8DF60341B38146DB8A86F3F + 7A029A0D1AC5C52FEBF4EDCD85F628749830011DC68F47BB71E3981ECD45F770 + 2E9E8772F13E8C0BFAA1683050FB17CB1DF560936E34640417DA5A706B5F25A6 + F556F7482EF4B9986FC4C57DC3414318524306A3F9F0E1683172045A8C1E89CE + 332763D09AA518EDB206239D5662F086C518BC7109863B2FC7E0350BD067C974 + F45A3815BD174DC7C01573316CCD220C583E17DDE74D66DB09E83AEB53749A36 + 0E6D268E40AB09C3D065E638F49A3F095D668D45A7596358773AC63A2DC464AF + E59813BC014BB6BB62635A205C0B22B1392708AB923DB12CCE158B766CC6DCD0 + 759819B012E3B7CCC5E88DD331C57D3E667A2FC10CEF0598EDB7040B83566261 + C00A4C779F8B099BA662A2F30C4C759D8369EEF330CD6B1E66FA2DC0C2909558 + B9630356C76CC2AAE88D704E75874F7E00BCF2FCB139D515EB929CB1267E13D6 + 263A61438A8B0DF6667A1950BB26D99573D9881531EBB121613336253A636DEC + 06ACDCB916EB59DF25D313EEB97E08D81D0EFF92307817061977D101FB238D82 + 0FED3056C82EB9BED894E505E71C1F135F972E37D34C1B4BDF50781686C3B380 + ED8BD426163BCEA623E6723EE2B4F7EEA55C84321D7C3A0DE1E7B3B0FD729E01 + C35BF7C762FB890CC4331D772D1B519752117E260141C7A2E17F643B7C0F45C0 + 675F28BCA98043DB107E2A0EDBCE269A7D90FD3837CFDD21661FE5E0C33B117C + 341A61C76211713219DB2E6420F27216A2AEE721FE8E406621626E1620EA5A1E + 626EEC42F2FD7DB67D761F1F46F6F363C87D7102194F8E20F9C17EA4303FE5D1 + 01B6DB8DE89B2588BDB317C9AC270BD54CD69332D846FB74AABE71C57C670FB5 + 1B71EC3BF1D62EA4DD2B468674BF0869770A907A3B9FE95DC87D5882FCC7BB91 + FFA418BB1E17A380CA7B58889C7B05C8BEA73AF948B99383E4DBB948BB9B87AC + 8745C879B487FD15997E53EF9620E7E951147C7602852F4FA3E4AB4BD8F5A5AC + 6D2FA04000962AF8EE32F2BFB988AC97B2E83DC7F01CB25F5E34D68566AFD81F + 6E62F78FD719BF825DB2E6B5C3DF822FCF22EFF3533C27078C05F2CEAB7988BA + 928D788DFB602F329F1C420E8F5B505670566EAA531FEE47E6D323C8F9EC240A + BF3EC7FECE2297E50AF77C7F0945AF3817CE4D56C61AAFE4FBABD8FD9DCD6D71 + FEE76751C0B9EDFEF60AF6316F0FE75EF2F5791ED37914735EC55F9D41F197A7 + 799C2750F4E529147D7112D99C5BE6FD3DC87F7ED494977C730E7B5F5DC41EB9 + A33630F9224A5EF118BFBB89E257B7502478FBEAA681AF529160F0374C7F7F07 + BB8D4BE6ABC865FB7CCEB340F3D2F9F99E6D056FA922590CBF621F6CBBEFA707 + 46B2E69505EE9E9FEE62FFDF65C17B1B853FB0DD8F77B1F7CF0FB1EF2F92DC2B + 3F349077CF8FF7B0E70796A95C32F1FBD847EDFFD323EA31DB3DC681BF3EC5FE + BF3C65DF8F90FFED1D1EC31DECFFE3031CF8D343147F7B8B9F91CEE325ECFFE9 + 368EFFE7031CF9DB2D1CA5767D790C51973390782D07BB9E1FC2D1EF2FE2F80F + 977088E767CFD3C3D8CDCF6C0FCFC3C19F3EC3F17F7C8513FFE30B4AE157382E + FD27E3CC3FF93FBEC189FFFA1AC7FFEB2B1C63FE91FFFC02870D2CFE9CF19746 + 47FF4BD0F8058FEF290EFCEDB9C93BC6FA4759FFE8FFFC1AC7FEE7B70CA9FFCF + 2B133FCCFE0E491CE3F03FBEC091FFFAD2F4AD7105A20FFEFD73A6399FFFEF97 + 38F23F05863F33505AF9A6FE7F0AE60AF67E85FD7F7B6900AF20B001BCCC3FCC + F8A17F7CCD90E3FDD777EC8B62D97186476501CC36FBFE2AA8CD76EAE3AF5F70 + EE4AEBF8BEE1185FF338BE34FDDAE031EB30DC278B6503AE35A6AC96398EA033 + E3FBD407F30F0876F3D80EF2D80483F7FF837DFCC71736182C10CCCFB4F8A7C7 + 28F98BACBB1F23F707DBDEBF99B202FEE131B25F3D44C10FFCACBF7F6003FE3F + 3D4111BF2FDAD33AE3B30BC8FE42BFE36B28E2F7B190DFE7FC6FF572C20D633D + BE8BDF3F9B2BF0FBB0EDE17C97FDDC411EBFAFB9AC6381606B2FEA0CFE2EA474 + 7E876CFB70CB2DFB451317044E7EA6BDB74F218BD7B874B9A17F76DCB861972B + F73CFE7EB51777CAF313487A7612C92F4E53B2423E8734EDEDFDE525647C7D99 + C7730D399C67C6373790C2EB4DC28BB34860DF4902BB2F59E7ABABAC7B15299F + 5D42D2F30B06C826DB5D540B04271B206C87C202CC9A23AF6FE99C6712E7695C + 4DB34E1CEBC89258F0D700605909CB8258C0F7C969243D3E8D946772E17ED6B8 + B1562817FDC9CF04BCE5125FE7E232CF812CFD2F238DD23EDB595F0B9C5F3569 + 816059570B029B79BEB8C0795D30C05812D016408EE57866AFE187B67D87AD3D + 878D4535F3240160590347DD3F6273FB6C87C0C61DB4802F430161B98A1604DE + 2620CCE3D9C1FE2C00BCEDEE5144DE3A8C885B2CBFC372A6236E1C3010585056 + 792AB3B98C16D4D53EC1B202D6DEC33645DE3B884841E0DBFB107A732F42656D + AC7D89E532FAE601CEE59001C5DB39D74881E1DBAACFB9EB983897488162591D + 1BD9C60EBB266B60592FEFB7B99FE6F104DE104C2D360ABCB6075B2F9750C5D8 + 7A89E94BBB117C650F42046A2F6A3FE07C63FD1B74B9887DEEE5B1A82F416DCE + CFB8C1D6DC34E79FAD938D1534C7D1DCC3591ECEFA02C7C1574B38B65C5FB31E + 2597D792F63E961CF3C2EE1E29CD539BADDAEFD8C0E3BD4CEF33B241E0DD08B8 + B1070137F7C0EF5AB101C082BF3E970B11704DE3EDB541571ECBD6EB7B11C0F9 + 58D05710D897EDFDD8DE8F71592A6B4F659D13C9EC99ACF322582C902CEBDDB3 + 727F9D4B2914B8DD8580AB3657DBEEE7F2E07422CBB87936D0F89CDC356763CB + A92CB89FC9850FCFA32FE72137DB3EE705800B59270FEEA7734C9E207D203FAF + 80EB9C1F3F139B05B3E65ECCFE0AE121187C3207EE27B38DBBE74D8712B1F160 + 02361D8CC3BA7D51585D1C89B5BBB763C3DE1DD8B82F1A9B0EC4328CB1C1DFC3 + C9469BF6C462457E98D9C777697630D617EF80F37E95A71897D196C5AF37E7B4 + 95E3FBF11804815D0EA7C285654E0793B0BE24166B0A77625D31FBDEC739EC4B + E0980958BF271EAB8BA2D9FF76ACD81565E0EF92DC48ACDC158DB5C52C2B8CC5 + D29CED9897128C69D13E981AED8D394981589C196140F0B4181F8C0D71312077 + 58803346066FA15C3126CC039FEEF0C3C4283F4CD8EE8371919E4CFBE293082F + 8C62F9F4D8604CDA118061FE2E18E0B9017DDD0480D730BEDEC0DF7EAE1BD1D7 + 45EE9E9D31C4DB0D437DDD30C447EE9EDD4D7CA0A7B3D1204F170CF6DEC23AAE + 8C33F472C5405727F4D9B401BD9D3660A0BB1346B26C14EB0E71B141DE111E4E + E8BD6E39BAAF5A8CA15B36B0CC05C35D3763D0A6F5E8BE62313ACB63D2E205E8 + BE6C317AAD5C866E4B16A1C3FC39E8BC603E7A08EA2E5888763366A1D5E4E926 + EC3C771EBACE5F8076D367A0E1E8B168F1E944F498BF083DE72D42C769B38D4B + E426A3E40AF953B4FC64123A4C99892ED3E71AB59FC2F28933D066CA1CF49CBF + 0CDDE72E440BAEE75A4C18878E33A7A2E7C2D9E8BB743E3ACF9E8AD653B8769A + C275DED48968ABED80C6E9C5DD295C4FCD44F34FA69A3D741B0E1D8D46549361 + 630C049655AFC0ADD6790D078DE47A6E1C9A0B0E0FE73C65D56BF287A2E9D091 + 26ACD79F6B44E6D56558A7EF00D4ED3B10F5196FC83E1A2ABFDF406A00E383B9 + 7E1C8666C346B0BF516826183C7C145A8FFA046DE5425AE3F61FCE35E748D4EE + 3D84E38EC7E44D2E98B2D909ED39EFDABDFB72AC012C1FC471FAA3769F7EA8D7 + 6F10E7308279C319E79CFB6BAB25F63DEC13C647A07AD7FEA8DCA9372A77969B + E6BEF8B85B7F037DAB75D4BEBE3D8CB5AFA0AFDC365792DB66EAFFC7DE5FC657 + 952D5BE3F069A77188BB1B492082050B1ADC1A777777D7404842D0E04EDCDDDD + DD710B125CBA71E93EE7196FD5DC7BA53739A14F1FB9F7B9CFFDBF1FC66F5ACD + 9AB55676F65E738E55552DD8BBD7CC0ECD396C33E7E9651859A3B181159A1858 + A31913C08426C6ADD0C8D01C8D095C36E26862D4C7246F5305A297D194FA9B18 + 525D4424A339FA6668A46B225E5C6FA063801FB499EC2568E80BFCC050D74303 + 756A0B48752EF5D0905F80E7146A229A1E3B14C89C0A7EE0B3353E0F23887331 + 79F4382680BF6EA14E75762290459593CECB44E8E6E6043E9B62270D3E57E3B3 + 3A3E0BA33E41F436959DA18933B326327C2B87208085E72F81495E698CDA8CAF + 3F2380E5E787F2B34371B6263F5F538434269D5D7E097F69D85296964E018AF3 + 6AD790EB94FA14D790C063B5E7AF75CF86E550EC97C0E7C992A7EF970860098A + BAFE0E7F70E6CD50D4A388FA64FFE3F8D299BCBCBF2E515ACB4728F4FD697CC3 + BCCC3FC9AD481C4D7DF3B8EFCFE893E414A1302E9CBF2430B1FAEDEFA46D6DD8 + 656A7FF74373346EA28AC6F4BF524BFE4AE3721906877E66325691CF63B92F12 + C0721945D9DA7109DC66592E3912A98846FABB1E4502F8CF10C1FFEF12C07F12 + 9FD9F76F12C01CE7BBBE7E4508D2F79F25806BDB6C643D17F1BF167C4F18BF7F + D8EB452D414BA5F002E6B622B84F017508E0AF395F2E41225DEBC367F3B92DFF + 52F84FA376BD7AC6FE08B55FB8F57C71D582FB85FD5F18AF03A14FFAE2FF4FA0 + EE8FC71FE10FE6D4FEE8D5959113C0826095FF90D712AE0AF8E20FA8A4AFAE5E + 39292C0860CE01FC1DFDE8CB732C731E60D9973FE17BFAF2A62FDCAFA4F0D3D2 + FCBA9013CC9C13583C74B0CD72A2B6D66E795BF1E1446AD72D1551F7C1469A27 + C9F2B82279C96D698EE218D715E7486392AC20D3699C1FBEA4F0D092A72DE32B + 01EAE7363F38D6217FEB12C022FF88BCCD0FA3C27B564946F04A84AF44F68A07 + 5D82F0A2E53A3FE8CA65F8A1562287250298C9DD06A49B4333F3832DF7295E13 + 8F3371FB23C930B82EAE95507B6D0AF275C172820096EBE1751A34D7AC5D4F78 + 273755C55F1AD37DA392EFC50FF400FE233D8C3754D112F8514E047FD782ECA5 + B2018DB167F0775467EF60E121CC44319702B426B5D94BB6096D1C9AEA188A50 + CE4CE672D942DF084AC6A6223FAF44FEB6602297FBA9546182D788FAF4691E87 + 88E6795472BEDF66F292C9592ED943B79196161ACB3D7B5B1AC8BC7A99C0652F + 5E0693BACDB549465D1D4D08CD485E494F0FAA4646B4BE3194F4F5699E01AD69 + 48301284B08A11E71F967B0D6B698B759AE932B1AC87461ADA68A8C121AF6521 + B1F96D56F686161061B279E3A32F3C829B6AC9485B26729B68719868633461C2 + 5743F2906662983657EABAA2CD04715396D1334653BA17CDF54D682E13E17CFF + 0C687DD243684650A2CD9ABAA91534CC5A438D3675ACBB11EBA54D1887E266EF + 63E161ACC39EC62668A647F7DA98C3715BA285492BA85AB4815A2B5B592E62CE + 4D6C4C3A082DA8DE92755AB2B76F07E8D876803A7BFF9AB616398C55CDDAD05C + 1BB430B396C38A36A4047ED3D8843794A654B7400B732BE169AC6CD9866438C7 + B1B5C86DACD6A62DD46DD8EBB80B74DAD306BC6D671999CC1EC81D68E3DCBE33 + 743B7685216D98D96B58AF9323F46933CDA5AE433711B25ACB8136D9ECADDCDE + 013A5DBAC1B07B2FE875EB016DDACCEBB1A7718FDE306032B8136DEC093C57A7 + 23E9EDD0057A1DBB409B37E8DDBAC2A89F13ECC68D42C7C9E3D076C228D88DFF + 09AD470F81F9202798F5EF09AB217D613F6A283A4D1883CE24633F7A38F5F547 + EB618360FBD310B41E321016FD7BC3A87737983839C272701F580E7142EBE1FD + E1306904BACD1A835EF3C661E032F6DC5D88F987B6608D973BD6F8B862F1C92D + 98BE7705C66C9B8D111B6760E8EAC9E83167181C670CC1B06513316AD574F49F + 37063DA70E419FE9C33170EE58F49E361C3DA60C41DF59233188F4F69F33125D + A7F643F799FD316ACD24CC765D82B97B966196FB222C3DB41A9B7C7662DDB92D + 987B602966EE5980191EF3319BC32F1F5E81F984D9079761EEA11558747C1DB5 + 57620E8DCDF55880F984453467E1C1255870600966D3BC19EE73B0ECF86A2C3F + B50E8B8E3181BC011B025DB035DC03CE31FBB121D40D8BCE6FC4629FCDD810EE + 8EAD31FBB02ECC1D2BFC7760B99F33D606BA616BC47E6C8F3C08D784633890E5 + 8513C5C1385D118E6365A1D89FEF038F9CF3F0C8F61204B06BFA596C893948F3 + 5DB123E6084E143049EA8BFDD94CEE1E807BB22776A71EC0AEC4DDD81AE98C0D + C15BB039741B9CA377C135DE033B63DCB131640756F96EA2FBBD19DBC9268FE4 + 43F0CC3C8923EC295BE48B0304CF327F9CB81086D3972270EA22DB122CBC734F + 548509529373EC9E2630E9C938C9FD172370E652248E5784E070499020908F57 + 85D6EA39778589E3089C280BC149D275FE4228BC2E84E074297BF97AE36C853F + FC2E06C1AF2A10BE95FEF0AEF081FFC500045C0C8457A917BC8BCFC0BBE004CE + 641FC2896C4F9C2B3C8E80CAF308BC407255BEF0655C08A03200DE55C1C2AB33 + F05A1CBC2E46D1FD8C80D7950404DE9188D342E101CB44AE3713B9D5A9087994 + 2FC223FBDEC986CF6D0EB39C0EFF7BB9087B548CB0878508BD9F87C0BB59F0B9 + 9902DFEB4908B89D2E43751AFC6F24C2EF7A0C02AE47C39BEEC1D9CA10BABE30 + 51F7B91C294AC679BA4F274B037194EEF189D2609C23DB026F2523F8760A026E + 2622E8762AC26A32107437137EA497ED0BAEC916B96A831977321142B685DECD + 46644D2E22A82F8C6C08259B426E2423E4661242AB931041FA226F2723BA260D + 9177121170391C7E97C2E07F25027E644FF0CD789A9B86B03B6908A535C26B72 + 1042F723F84E0EAD5F808887C5887C5C8A70C6A312443D2A45F4932AC4BEB888 + 989F2B10FC380F5E34DF8FEC647235E47E01629F5721EEE7CB88785229885A46 + E4D30B887E7E5120EAF90544BDA8A4F9AC83C9DE32843D25DD4F492FCD8BFBF9 + 0A125E5E43E2EB1B88FFE52A6268BDC84715644739A10251A437E619E9229D11 + 0FCBE9EF588288C74CE85E215CA3752F22F401C93E205B1F9523866C0E65CFDF + 07F9887A5C483AF2695E9E2883EF65627F810F36D3FFD261FA1F8AA47B9CFAA2 + 1CE9BF5420EDE772C4D3E720FA6E2E629F5C42F29BFB487DF70029EFEE22F5FD + 5DA47FAC213CA0FA7DEABB47A841F25B2ADFD720EDE37D410CC7BDBC8584D7D5 + 82184EA6794C0EC7BDBA41D7788DF4DDA2B97784AEB40F34E7C37D24D3DCC4B7 + A487757E7C28CAE4F70F9044BAE35FDD269DB74937ADFBE11EDD9F9B4824FD49 + A427F1DD0D92A1B598647E7593407D249BF8E61EF5C988DAD897B711F79AE6BD + BD4FB2AC5386C477F749A60609AFEE0A3B53F81ADE90ADECE54CFAE368DD24EA + 4B625DAFEED13DBE43FA797DB295CA042A13C8E604929110F7FA0E62695E1CAD + 1B4FF72D9ED6E432E6E55DC4D07C09D1B4267B12CB48E81A2AEF2182F321B397 + F0F3EB8280662FEA700E2DFDEC2A7C1E70A8E8CB08E09CC38FAF22F8E91501F6 + A60EA77B1AF6E232429F5408AF75F6280F7B5A8108CED34C7DFC12027BB4F38B + 0CA1CFCA10FCA41C81F4F9087A5449608FF30AF8DF2F83EFBD22F84944317DFE + 6561C7A98F7366D3E7C8EB760EBCEE70AEEC3C016FAA731F8756F7BD9E2C5E02 + F1BE992CBE1FFC6FF34B1D99F027B067FF79FADF3C77230567394C3E4719A0EF + 96B3D51938C321C6EFE6C09FFEC7FCC9BE73A493BD794FB3B7F2ED5C78DF2D80 + F7BD4291EFF85C7521CEDCCCC7995B7972E4925C0E4E5DCF1621AACFDCCC2619 + B2E94E1E5D0B87C4CEA3368DDF22901C7BE5B2B7EF29923DCBE1AE799DEBFCB2 + 0B473BA0BE1BFCD24B167DEF65C9FB68FC9ACC9B9AF39F9FB9CA61E733E93A68 + 9C749E27DDE7693DCE91CC7633E1CCB61F67629CF4CA722873686D19248F6ACE + 2FEC795146F272486B99B7327B23A7D692D252686896E1B608FB2C277B39FFEF + 5EB977B008714D60598FCA58B893CC1E6E93BE0397681DC6C514598E6202E725 + 66D2774F2987924EAE1DDB539120BC799900E610CF1CFE99C9542657F796478B + 35DD8BC3B1332F18AE8561D41723E0C1443487ADAE8A233D1CE29A7320C7D0BA + 892257BA377D2FB2CDEC71BC9774333C4A649EC05C8AEB20DBF7D27CF6387629 + 0AC32E5A87096E372673692DD78230411CF31C97DC5041006E49E730D07ED899 + 1B8C5DF9A17023390E83CD9ED03C57E45926B8B177347B20139808E63CCC4C78 + BB17C7600FADCDC431E751E690D71CD2DA8DB09BECD9CD36D35C979228819D4C + 544B324C725329237BC30538D4B4546E2F082179BA5704E7A2106C2F0CC2D6A2 + 506C290CC1A6FC206A87C095AE75477104B6D1F5EE28A735E8FEEEA4FBB99D49 + 602680590FCD712E0C15E1AA9934DE551221CBD14CE54E9E47630CAEF39A5BF3 + D8833AB0567E3B93C8A46B434E20D613844EBA975BB23907B13736E5F88B399B + B3FDB029C3179BE99E6E141EC5E744D8E635C967B1352B40E45CDECA9ED774AF + 395C3497ECB52DF346F6C786541F6C600FE4E4F358117B1C8B384F2F3DCF2D89 + 3A48F57D9817E4863901BB3097B028C4038BC2B8CF030B83F76359C4212C0939 + 80F97E1E98E1ED8AA967776286972BE6FA7B609EFF5E2C0AF514E4EF9A843358 + 97E84DEBF05A3E589FE48D65912730CB6F9FF0DC9D17E489593E7B31C7771F96 + 861D9579FD861FC19CA083981F7A180B823D31DB6F2FA6D373F6E4B3BB30FEA4 + 33C69DD88E71C79D31E1C44E4C3CE982D1F41C3E78F76A0CF15883B147B66122 + F533113C6CDF7AF4DDB91CFD76514970725907A79D32707B80FB260C70DB84FE + 6E1B04B8AF9FEB460CDDB30DFD5C36A2EF8E0D18E8B605BDB6AF417791D3770D + 1C37AE4497B5EC05BC123DB7AE15219D199CEB5710C11C1A7AFD0A38AC5C8CF6 + CBE653B908DD489E4335B75FC8DEBAD3D16ECE4CE1BDDB73E5520CD8B4064EEB + 56A0CBE2F9E8B16231066E5A87DEAB487EE13CF45BB70A43366F408FA53477F6 + 4CD84E9D0CCBF1ECC53B066D268C87EDE489B09F32197654B69B36195D67CF46 + 9719B3D07AD458980E1E0A73F6C225980E1906C3FE03A1E9D813BABD9C441E5D + E3BE4344386383DE03A0D78B73D9F687B1D360D8FC340E1DC74D45DBD193613D + 6C1C4C078D82C9C051301B345A90B85A3D7B43CF8974D05EC97AD430D8D27ECB + 9A3D7F870F86D9D041B0183298300466033845D07098F41B4E6B0C814E8FFED0 + 77EC47FBBA3EB4C7EB0D7D0EDFCCA19D1DFB40CFB13774BA91DEEE4E606F5E3D + 0EC3DCD389F674DDA1EDC82F06F7A88536F569D0BE90495A7ED1589DF68CDA9D + BAC2805F1EEE4AFB43DA379A39F583D58041B02418F6E825F6943C6E423A4D7B + F5A3FD25D9D0A907343B3842C5AE13ED3BBBC1AADF2058D13DD2255DAAF6EDA1 + D6F677A8B7EB000DDECBD27E55A3435742375A93D32CD19A742DDA9D7A911D8E + 22476FE356F62287AF8CFC6D8766B4B76E4A7BF2C6B4CF6E62CE751B34E390CD + 6636B487B6A3FDB9AD48F9D4C4D00A8DF52DD1C480F6ED06B4FF660298DB7A9C + A7D70C8DF44CD190895CDAFB37D03116606257F4F3CBE27C3EC051C65896433B + 533F93BFECCD2BBC7B093F6819C83C7CF9857539012C4A267BC5B9873CED991C + 3F101AC8A3C889F331651944A4386ECB09DE6F9434F02DE1776F5F4DB9ACECBC + 4C22806B9D269A7DEE2821394E283A4B7C76EEA57026F61D9F6BC9FBEA8E7FDD + 8820086039E424E9373F2A0B0892547E4629C860E97C517EC6F887909F9532F8 + 5C528234CE75457BA471453BEBCE11F314085C867476AB786E2CEC562815FB15 + A138C6F8EC2C5811F59D3D2BA0AE1E09F5C9FE235DFF34EAD323E997B80205DE + A02EA4B13F05E66518F58DFD11FE88D710BC483DFD8A603B1551675C70335FCB + 4A41B03298A792D7250258F0747F613251CED949FC158F4B73E47D5F73F45A1E + 530013BF12A43EC5798AF3C5589D7E8E4AFA354718FD81FF169FCFE7F5BE2530 + F9CBA8CD074CF81F4F00FFBBF3EBC367F6FD0B043093BE8AA84FA62ED8135822 + 80195F7D89F455247F05FE0B6EC0FF0628847DAE9BA7F78F2091AD7F86006688 + 795CD6F962F87F02E20B508EFAC6E5F8BB2F6EE94BFE3F81BA3F208AA82B535F + 5BAA2B8ED7ED97E30F7F1C25A2B8BEB1FA2097E510D09CEBE0EF12DEF35B3EDF + 49A0FBCB60BBA4FBA7A84BC16661233D20D4258015CBBA0F27525DB194F0FB03 + D7EF7D75A1F8E053DF43912214D794C6A57AAD9D0C6A73D865892C55244D45C9 + EBF0832493BCECFDAACC616964043093A1821C96F7735F2D81CB0FAC726257F1 + 41573CC0F2833095B2B71C650FB48AF318221435ADCF0FAF828C656297EAD2B5 + 490F8D92E7EE670430CBF0B5722997E7792C2B85809620EE25D7A9FF7B7AA866 + B2F947BA9E1FE97A1A88EBD414C4AF20C4E9419BAF91C33B3756D34613755D34 + D5D0A3BA4E2D11CC60AF5F2678BF6E4E0FA8CDE81A9818A63E2964B4546F44F3 + 99A86CCE61A0750D0489CB047073267A0D65397C397F2FF70962588FC31DCBE4 + 9A69EBA0A9A62E1AAAB23E55B2551D3FAA69D2E64283F4926E0D4D8186EAB496 + AA1A6D48D4D158534B10C32DF4386F3093C63A82FC65825799495EAAB7D0D111 + 50313080A61913D0EC594B72067A503631828AA909544C3847B00CCAC64668C9 + 61A2396FB0B1CCDEE67A744D646B531D0341CC36A47BD44893BD7E69E343F74A + F637976D841AD366A9A90813CDE190A84DF24DB599C8658F63269F991CA60D16 + E71A261DAC93D18CD66846F789E5788CF32B73D9449B09749A67600235E356D0 + 696523A0616A45F7D39CC64C683EDB4673694327720CEB99082F60F60A6E6160 + 0E25634BB424A8D1A652D3D29ECA3668C9F9874D2CD1DCA8951C16A264A25793 + 36A71A9676747FACD1D2C892EE4B6BA898B58612879132A50D27BF696CC09B47 + 5A87EE0BA329FD6D19CD4DCCD1D2AC95BC6E219B63466B99D3FC567668C94432 + D9C15069D50ECAB4196E69660B55AB76D0B2A30DB47D2768D2865BBB5D674227 + E8B007316DAAB5DBF31BD3ED69D3DC9636DCBCD976805AFB8E50EBD0516CEEF5 + BBF62039DADCD3469D6140D0215DDA36346ED75184B96E69439B695B5BE876EF + 06B37E7D60D6BF0F8CFBF5846E0F5AD3A12D6DECDBC1B44757580DE883368307 + 1006A2557F27580EEC87762386A3C3C891683388FA786E1F3E44E849F5DEB0E8 + DB0BAD06F486CDF001B01B3D08F6A307A2C38421E831670C86AD998DC9AECB31 + E7E01ACCDABF0A935D1661DCD6B998E0BC00A337CE41CFD923E0306100FACC1C + 89BEB3C6A0FD482798F56A07B39EEDD1A6BF23A11BEC06F54487E17DD17E581F + D80EEE0EBB9FBAA2EB64270C5E3406A3D74CC590E563D077C1500C5F351ED35D + E763A6FB024C729E8529BB6663BAFB7CCCDAB308B3F62EC654B77998E8320733 + A8BEE0E85ACCDCBF14535DE760CAF6E918BF7122266E998C49DBA6622CD5072D + 1B0AA705FDF1D3FA3118EB3C19E35CA661B2C71C2C38B1161B42DDB13ECC1D0B + BD3763DAF155987D663D5687EFC656CED79B720C5BE20E6153F4016C8DF1844B + FC31EC8C3B8A5D09C7E0917E1687F3FD70B4D01FFBF3BC4518E603857E8400EC + CBF5816BDA196C4F3C8275E1FBB02E64375CE20EE270CE491CC838821DD1AED8 + 16B1133B62766157BC3BB6453A63B5DF5AACF65D83357EEBB02E6003B6846D23 + 991DD818BC0D6B7C366243C076EC8CF68067C6319C29F5C3A98A401C290FC4D1 + 8AA05AE2964332B337EB310ECF2C8767810F3CB2CE084FD7C3C5FE384AE32C77 + FA42B8C8CFCBF38F9405E050890F3C8BBC7188E059E08D7D59A7B13BED180EE5 + 9CC1B9725F7855F8E274F1399C2C3C83D345A771A6E824BC4ACEC0AFC21B0155 + 844A6FF8959FC7E97CB22FDB13C713DDB02B60357604AEC291A45DF0293E8AC0 + 8A3384F3F0293D2BE6F95F0880776520FC2E8523F85A3CFCAEC6E3FCA558F8DE + 4846F0BD6C84D4E421F87EAE2073FDAB398C73327CAB5311C0A42BF5053CC845 + 90F06ACD47C8E322843E2880CF8D34785D4B82F7F514828CECF1BFCD61A45345 + 58E590DB2908AD4E44F4BD14C4D4A423EA4E2A22EFA420FC561242AEC7931DB1 + 08BC120D9FAA30BAEE6078D1BD0DB81647739211752F0391F7988C4D213DC902 + 4177D20409CC84354396AB361361645F444D0EA2EFE723BA265710BFC1D71210 + 72954065F8CD4444DC4C4024D912732F15B16CCF9D786A4723EC7A04C26E4422 + E2761CA2EE26919D6988B99F8ED8C7B9887D9487702672497F287BCB3E2C44F4 + 5326586588795A8E849F2F21F197CB886502F8512E7CC95E7FB68BEE65CCB332 + 24BFBA8CA4575749EE32E29E5F225C148879764120F1E5151ABF86845FAE20E6 + F905C4BEAC42C26BD2F9F22AF55F47D2CB6BA4E30652DFDEA2F21A629F5521FA + 4905A112518F2B48D70592211D4C0E3F9591C34C0827503B99E627BFBA896426 + 909F5622FA4131426FE7D2FDCA45FC73B2ED25ADF7BC04A1F7731046F6B2D7B4 + 0FDD2BAF2BF1887A988FF4575548FDA50A6924974136A5FC5C8914BA86B4D777 + 91F5F129723E3DA5F221323FDCA7F201E111D2A9CEA470327B02BFB98DD477F7 + 90F191FADEDE41FC2F3790F0AA5A46AA523F13C0F1746D49D49F41F5AC0F35C8 + 787F0F991F6B90F9E9A18C047E5B83B4F70F49C763D2CDFA1F8A35925E5723F1 + F52D647C20FD1FEE22850964267A5FDE402CDDEFC4B774DDEF6F13C88EB74CFE + B23DF764A434E94C797B5FE8659DA9EFB8FE80EA8C87624D267CF91A52DFDE45 + 1AD996423A92A88FFB93689C89EE34BAF614268ED94BF80DCD794376519D896A + 1E63723A85EC13DED13C5F90E224F3E601E9B92F88E3F8574C66D720F697BB88 + 6622F82593D34C1A539FF032BE85A89FAB11F9823D9C699CE67019CEA1A55F70 + 48E99B08FF45460A87BD60D2F71A227EBE46F257850778147D3E235F5C40247D + E62299FCE570E18FCA64A1CDEFD3E7E16109C29F9622E25925C29E5421843E3F + C10FCA11FCB01C210FA9FEB00C81ECE95E53243C8783685E1097ECD9FE88FB0A + 1040FF778CC0FB05847CF8D37789F78D5478731E6EFA7E397B391E272FC4E0CC + A5389C2578D1770593C14C0A7BDF4A871FE7F32678DFC914DEC1A7E8FBE4CCCD + 0C7833A95C932F2398EFE5C3B7A65090D1BE4CFEDE664FE47C9CBB5D08EF7B9C + 8FBB027EF7CB49AE84E40BA9BF809027E4CEDF61629A7411BC0441CBB9B639C7 + 7691086FCD32AC33F0015DCF7D5A83FE3F38C281DF3D9A272222C8738DDFA179 + D599228CF6B10B09385A958013175370EA3213C2F27CEE975344C961A7392CF6 + A91B9938CE44EE85449CBC96A640FECA3CA79900E630DAC7AFC9F2148B5CC3DC + A6B9C708276E660AB0E733E735665298C962595E63595D80D7B8C4E1B799FC4D + ACCD17BCF7423CF670BE6092676FE7A322143587AE665D1CE69A733773EE641A + A76BE19CC5872E32319C84FDA46B2F83EA1C427A0F93A02591F0288EC49E9228 + 01AE7B50DFFEF2185A2F1E072A63B1BF229A20F754AE8813398CF794460B52DA + 9BEE2B93FA6CEFEE22D243FDFB682ECB31B82E85B966125884B5661DDC2F74D1 + 1893C6A531626D26825D98F0CD0BC18E6C29FC73988C242E0C95D95B162D4860 + F6681664AF409C0087C3DE53CADEC851244FB2A497C96AD7E20859486B02E73F + 1675BA262680B92D4190E2F2924972175A6F277B11D37AECD1CB70267B761491 + 3DA53CC6ED50EC280EC3CEB2486C2F0DC7E642595868D7AA18ECAA8CC18EF248 + EC288BC0CEF268EC12FA63217236B3C77249840013E22EA483B193B083E633F1 + CB24AF737E30B6E7C9F21A3309CCEB31692CBC83692D269E852731DBC6C47169 + 04B6E67118EA00785E4BC629FABCEFBB10872D597ED8987A1E9BD37D0491BB32 + F604D6259DC1A6346F2ACF61438A97F006DE961D4CB28122C7F0BA246F9AE38B + CD697ED894EA43752FAC4938851531C7B0929EE956C61DC132268143F7607EA0 + 1BE6FAEFC41C3F17CCF4D989A967B761F2E9AD98E9457D4CF87ABB63AEDF1ECC + F3DF273C781787780A02787EF00111DE7959D431CC0B3A84695E1E9874C60D53 + CF7B60C2A95D1879680B461FD98AF12776082277D229174C3F4BE3E7DC30E10C + B5CFB96226E961AFDF49675D30FAE8168C38B401233C37D0DC8D18B67FAD2078 + 471FDA8AD187B7E2A7031B05C61EDD86919E1CEA792306ED5E8DFEAE2B31C463 + 1B959BD1D7650306537DD0EEADA2CE25B7FBBB6E12ED016E5B31E200877FDE85 + 7E3B3761A0EB560CDFE382FE2E5BE0E4BC51C071E32A745AC3A19FD9FB77B568 + 77DBB04290BE9D99E85DC3588A0E4BE7C366EE74B4993315F60B66C17EEE0CB4 + 993A096DA64C44BBE953D161C63438CC9E896E8B16A0EBE205E8306F16DACF9E + 018779B3D165C13CF45CB604C3376FC2A0756B603765322CC78D41EB89E36125 + 27805B8F1B0B9BB163D186FAAD468F84E5A811B0A63D8CC5D06130E83B00FA04 + C37E543AF5834EAF3ED0EEDE0B3ADD9964ED035DC75ED0EA22830EE7D975EC03 + ADAEEC6D4B7B9F7E4360397804CCFB0F13E18DF5FA0C816EAF4134BF3F54495E + B37B4FD2D79BF43BC164607F98D23ECA84C0E4AFE9A0FE301B4875DA47990D18 + 0CD3FE4344A865CED5ABD3C509FA04DDCEA4A36377E85069D08DDBA4AF4B0F68 + 30B9EAD055448412646FF71ED0EAE608CDAE8E5073E80CF5CE5D04F1CB7515DA + 1B6A52A9D9B1B3D837AAD2FE8F5F24D6EDD20D463D7A09E2D79A3D90E9DAB94F + 8B64753BD3DE915F40668FE14E8ED0A6F5D4690FAA6ADF114A36EDD0B2B52D5A + 58DB8852D9C61E2A76EDA04E7B51CDF60ED0A275343B30C806DAB36A50A9D5B1 + AB8C106EDF95EC203B3B741764B208EF6C654FBADAA2B9A52D9A59B44653336B + 01CECDDB9CF7E7B44F6E497B6425735ACBD406CD98FCD53317B97799BC6D42F5 + A64CFCEA988173F3CABC778DF083B6E167E0BE1F758D6560197EF99C895E8E46 + 262788B99FDBDF6BE8E33B753D591431067BF6F2CBEDD4C760F297239E7DABAC + 55EBC9CB10F978F99C8BCA6FA894C0242FCB31192CC9D67736C6E34C000BCF5F + 41FECA485EE91C4B91F85544ED7997FCEC50407EFE25394CD4F64B60F297C95E + F6909513BE821895439124AD4B9A4A6D4528E6DBE5BA44024B6793D2D99E7446 + C936D43DEB64D4ED6759098AEB49906CAA6B1BD715CF78EB8E2B421AAB8BDAF9 + 0AE7C3FF16243DFFAEAEFAF4705DE1FCBB3EDE403A2797A0C831485094AFED67 + 7E42E2679893A93BFEAFA296BF61D4199343D19E7AD794DB25E350EA838C6895 + 885919F15AD77991AF4B828CBF92C929CAD40F49B66EDFEF3C187B1FB7800A7D + C7B4A4EF9B6F1AD03530B1C972DF11A8CE84EF77DF35C3B7B4BE044512B82E19 + FC3941FADF88CFAEEB0BFDF5419263DB15F1A5F952BFC07F1301CCF88C009648 + 5FAAD712C012FEFF04F09F832081EB838CE895FE716B09DD3F41F8D687BFFB52 + F81F862F7E59D6E9FF929CE217E097F0D98FC23F40BD73A41F1245288EF30FC6 + 676DC21FCDA96FEC9FC4673FC8FCE0A0F0A32CF5CB3C803FFF6193C0D7F835D9 + 22486085FB28423D4BEB48F6CA6D163AE9A1407825CB1F566A1F40E40F298A7D + 527FDDB604F1B055E721A7EE585DF098A2BEBA90E6D67D5012C4AF0224B25482 + D0CF73E4F89643C83009CB042B13774AFCF6A28C04169013C0F53EACF2C3AE00 + 3DF83239CBE19DA92E42D5087D7230394CA5049655B441D8C67612A40744AEF3 + 432D93BE0D680D0613BC2CCFD7C460829BAF51DC0BEA17442F4178FB1204212C + 07E7FF65F2B7A1AA1E1A0A2FD5DFDFD494857FE6BCBF3A6844634CFA32012C41 + F20666B007F077ECF9DB9CEE1D81DB4CF636618F56F662654254DE6602B8196D + 3CD85B974328B3072D93BD32EF5DCEADCB6194B504B9CB75F6AEE57A23550DFC + A8CC76D343725325FCD0528D6C231B55A8A43141FAAA32294C726AEA82086602 + B8197BEA72DE5EAA37D6D014657339E9DB9CBD84B548862091C04C10FFA8A14E + 1B276DB4309011BD4A460C2351E7DCC20C0D0B0B685AB4829A9905D44CCDA162 + 220B45DDD28043571B09D295C9D9069C179936363FD026A6216D901AD3A6A989 + 265DA7862E5D1B5FFFEF04AE923E418F3D846504397B18339A735E6403269979 + E345F7439DEE0BE7201604B13E548C4DC80E73A8B7B2820641CDDC92DAADA04E + A52A954A1C3E9BD6604FE2C6A4BF11E96F447F8346F4E0D58C36782D8D2C48A6 + 156D2499046E4D73ACA16C6625C0C46F33033382399A1B5AA0A5B195F01066AF + 5F19016C25F32236B684327BF8D29ACD8C68D3A9CB9B4BFADB93DD4D0C694D7D + 7E7B986CD6A3358D4DA99F73409BA0B9B139C9935E5A5BC98236B7264C0E5BD0 + 9C5634D6062D084D0D5A890D6E4BF1C6336D7E797D2B3BE139AC61DB0E5AB4C9 + 66A8535DC9DA162DAD689E656B34237B5A585A41D5C68EC61DA06DE7001D9B0E + D026B017B3766BDAA05B921EDA64ABF23CDABC37A3792D6D6CA1D69EC63AB687 + 6A877650B6B7818A6D1B68B7B58749972E68D5BB375A39F585799F3E30EADE1D + 167D9CD0FEA711E8F0D34858F7EB0FB39EBD60DCBD078CBA39C2C891D10DC63D + 1D61EED40BADFAF58245DF1E30EBDB1DAD06F684DDC801E8317334062C1B8F81 + 8421CB2663C4EA1918BD6E0E862F9F891E5347C06E8413DAFDD40FF6439D68AD + AED0EFDC16FA0EF630E8D41E265D3BC2CCB1332C7A7415A551D7F6B0E8E780B6 + A3BAA3CB84FEE83CA93FEC467783C5A0F668F353177499D20FBD660FC1A0A563 + 306AFD348C5C37153F11C66C998589CEF33171FB028CDB3C172337CCC4C01513 + 496E2C862C1A85DE3306C071AA137A4CEB0B4742FB718EB01DD5011D2639A2F7 + 82C118BA761CC66C9F8E590797638DEF4E2C38B309130F2CC1A8DD0B30FEC052 + 2CF2DD814DB187B023E53876241F8573C21138C71D12B9775DB83FC6133B633D + E19A74046EA9C7B02BED183C72CF605FFE7982170E16F8606FCE59EC4C398AED + 0987B036C415ABFCB6C139CA1DBB930E606BB88BC867BCECEC6AAC0FD8828D41 + 5BB1CA7B2D56F9ACC53AFF0DD810B419DB2376C0397297207E979D598795E736 + 624BA82B76271EC4F1BCF338511A000E637CB8C41F8798F42D0DC4FE5C2FEC61 + B237E71C0EB29770BE37F6A69FC4AEC443229FF021EA3B56E48B63C57E384EF3 + 19474B9914F68567DE591CC83A81BD1947B127F530DCC8CE5D097BB12FE5208E + 651FC7D9C23338CFE46FEE711C4F3F08CF24771CCBD80FBFD2D3F02B39853359 + 9E3891BE1F27B30EE0549A073CC337C3F9DC42B8FAAFC08964179C4EF7C0D114 + 371C4A76C78178379CC83A02FF0BFE08BA120ADF8B1C323A92102D421F07DC4A + 81EFCD549CBD924888873FD599540D7F90858807D9349E0A3F920979908BB0C7 + 05087D988FF0C7ECF99B2342389FBF12477392117C374D8456F6BB9500EFEBB1 + C27B37AA260D51B7E311551D83A85BB188BE9D80F89A54C4DE4E44F8B518845D + 8944F0E508045E0A43C8D54844DF4942E2C30CA43CCE41F2A35C243CC840744D + 0AC2AA131072331EA1B79311519381D0BB1CAE3A0531645F1CC971195D938998 + BB5988B99349EB2523FA6622226FC423F2661C626EC523FA7A14953188BB134F + 6534A2AE87D2FA7E08B9E483B8DBE1487B924C6BA620E17E32E209D1F768ADBB + C988A4F5A21FE4D0BAD988A26B8E7B5A8C841765487C518EC4E784675C5620EE + 4529221FE721EC7E9608231DF3B808292FAB90F2CB05A4FC5C85CCB75791FEFA + 32929E5722FE6939E9A1792FAA90FAEA1252B9FFE70B48203DC9AF2F22F3C30D + 64BEBB858C373791F6FA1A525E5D9595BF5CA2B5AA90C0739F5552BD92FA2E92 + DE2B487F750D69BF50F9F20AD2A89ECAE5CBABC87A578DBC0FD5C87C7901F18F + 8A1079370FF10F8B91F1EA02B2DED2DA2F2B90F0AC08893F9720FD6D15B2DE5F + 46FEA7EB28FAEB6DE4FF4AF3DE5C4506E9CF7843BAC9CECCD73790FDAE06B9BF + 3E46FE5F09BF3D42EEA707C823E4FFFA08D99FEE0B4236FDFD1DA4BDBB2D4A6E + A7BDAD164476F29B6AEABF8BD4B74CAEDE462A5D633AF567537F36F56711723E + B2FE87C8FEF80019EFEF23EB03E92564BEA536AD2DFA697EF2CB1B74AD37904E + 3A325E1398ECFEE5AA20BF9930CFFC780F599F9854E6F565C8FC40F3DF3FA475 + 1E8832E7C36351667F78406B3E421E5D5716D5B3DED7C80869BA864CBA866CD2 + 95F9EE0E525E5723E5CD6DBA261AFFF490AEEF01E9AD21DCA7FEFB487CC9E4F7 + 5D319EF1E91ED23ED01CFA1BA4D0F5A7D2B509F29B09E837EC597C8FECBC4F78 + 8024F61CA6B9713FDF46FCCBDB82AC660FE48497D588FFE526F5DF443CAD1DFF + FA0E125F5523F625B76FCABC9C39EFF1ABEB88FEF92A227FE670DAF23AE77C66 + EFF417324FF218AA473EE570E1E58432FABC96238A3E4B313F9723963EA7D1CF + 2ED2FF7605FD8F33495C41B255887A7681FACA05591C5C53240B835EC361CF8B + E9FBA004A18F0A11C451006AF2A8CC9787662F10910482AAD3E17B2D19A7AB62 + 71AA320A672FC6E0DCC558785D4E10DF330C3F7E6944EED5CF9EC13EB7D270F6 + 7A0ACEDF4817A4B01FE90924FD21F4D9E570F3C10F99A46572361F7E770BE07B + 9743599720906C0E7C50868007A5D42EA6F12231C6C4AE1F83659908AECE85F7 + ED5CF8DF2F44E0A3129227BDE25A645EFDA1A43FE06E2E214778F173B8F9A01A + B281E04FDF2FDE6CDFD5449CBC188F13847357D3E0752313E7AEA7E3D4E56441 + 0C1F279CB842D77D3D15E76E64E03493C157E99AD883F8367B1A67086F67E16D + 4CE3676EA6099CABCEC0B9DBD9328F670EA94D3849321CBEFA0C7B2DD33AEC51 + CCE079A7AEC9749CE33E2A4F922DC79844BE9884237212D8F352120ED1FAC738 + FC3493D1D733442E6326608F114E53FFD99B349FC64E923E59886B92BF9282A3 + 4C4AB3773213C604CE9DEC59150FCFCA581C288FC2DE9270ECA7F2E8A544D295 + 848315D1D85B4A7D659138447FF3432C571685FDA5115446C293C64F5C49A2B5 + 5271A42A0EFB4B22484704F614870BEC2D8924B968D2C9A470340E54C4D03AA4 + 9374ECA5F9FB453B86D6E1F5A3C51C57F63E26EC2E0C2384C2BD20046EECFD9B + 1F42F550414EEF219D4C04BB17470982777709877B2614C908DF039589D8C764 + 70A18C9016A435ADE34EEBBA9546C2956C7493406DEEE7DCCA07E8BAF756C689 + 7106F7EF2A0AC74EB283BD965DA8E47E41D6529BC365F37CA92DE6511F878466 + 0FE09D2CCB3A082E24E7C275B2C7B5441656DBB598D6666F66EADFCD7DA463A7 + 207C83B03D37103BA874A1B64B7E30B665F9631BE732CE632F5D9917F0562685 + 9928A6BF9B33D920F20A739BD667AF5FD671EC461AFCE97FE1087D5EB766FA63 + 638A379CB383B03327049BD9AB37C90B6B13CE627914875F3E8FAD199C833918 + 9BD203B03AC15BE6919BE2834DA9BE589F789E64CE601DC973C8E6B51CB239EE + 0456451DC592D0835814B40F8B02F7628E9F3B66FBB862C6F99D98747C33A69E + DC8A19675C30EB2CC1CB5578F02E0A3A4C738E627EC041CCF2DE47730E60BAD7 + 1E8C3CB415FDDD56A1DF2E82EB6AF4D9B9027D9C976190DB1A0CDBB3013FEDDF + 88611EEBF193FB3A0CDFB30EC3F6AEC54F073760CCD12D9870DC19230F6E246C + C028CF8D187B680B261EDD86510736D01C92A3F9630F6FC3C4E33B05269D70C1 + 18CF2DF869DF060CA5F1C1BBD761B0FB26386D5F4BA0F5766FC360D7CDE8BD95 + 6CD9B10E035C3837EF5AF4D9B60603766EC2F0DDCE18E6B61D3D37AE82E3BAE5 + E8B5690DD557A3278770DEB00A9D572E46DB4573D17EC97C7458B600ED97CE47 + DBC57360BF680EDA2D9E2BFA3A2D5F0487C5F3613B6B1AACA74E84CDB4C9B09D + 364578ECB69D34091D264F41FB8913613B7E3CDA30264E40EB49E36139760CCC + 478D40ABD1A3D076EA1438CEA735A64D83E9B061301E3A14E62347C072F46858 + 115A938CDDE8B1B01C3E1C46FDFB43AF6F5F68F7E8098DAE8ED0E4E8493D7A09 + 6F5FA37E0361DC6F008CFAF483F9C0A130ED3B08FADDD9EBB60FB4BBF6826697 + 1E507770A4BD5267A8B6EF0C4D8EAED4B537B499ECEDDC0B5A5D991CEE43F53E + 50EB447A1D7BC180F64E260368AF3468104C070EA4FA4098729BECE01CBAE6D4 + B6183088F649EC81CB246C2F683BF4847647474198AAB793D0056A6DD993B723 + D4DA390868B4EF24BC7A353B7781167BFA32E1DBAE035ADAB7450BBBB6506A4B + FB3A92D7EED44578EBAAD9B5838A8D3D3469AE660707E8D21CE39EBD0511CC32 + 6AED696FC91EBCED3A924C2768917E4D91DAA83DED0B696E1B7B28B766D889FD + 670BDA7FB6B4A23D23F56BD23ABA1C698AA34C8968563497D6E1E8561A34A641 + A52AED45D5686FAA6E4FF7CFD681E691AD56F6B47FA57DAC7831BA0D815F8A6E + 8DE6A6D6022D4C699F6AC2A03AEDC19BEA5B80BD7839CA1713B69C42EA472DCE + C34BD03412B97999DCFD41539FF6E1FAB2711D439A6384461C294C10C086F85E + 4D17DFA86AE35BC2F71A7A348F64691ED7BF55D5A1311D7CA7AA2BA2DA49E745 + ECF92BBC7FA9CE5EBD4CEC7EA3A489AF9BABC9C0842D9F83356302F777885CBE + 7288F332014D6ACBCECD2408F9A6A487095D39F86C8B51B72DCEF3B85F01B5E7 + 8704E1FC21979352A6D5422134B262786471BED8487EC628EFAB05F749F20252 + 9F0C42179FC531785C713D39A4B34BC5BA747E29F529CA28F609625A7EE65B7B + F6FB85BEDAB3DD3A50945184E27C45D4A7E3DF429DB3E67F0B8ABAE4F84A7EBE + 2D50B72D87E2D938E3CFF00C625C90B484BAFDFF0E98BB9108DC5A12B78ECC3F + 8264D717E772BF8CDCADAD7FC344ADAC4F40914BA23119EA72597242B26E3FE1 + 0F09E0BF344483862A30B16E0B3DFA2E6B42FFF38DF95C9BFEDFBF666F6092FF + 86416BD6455D025822813F2748FF0740F17AEB835C4E22B0FF8EC8FE82BC0C3F + FEF711C08CBA04B0E2D8DF13C0F518FFDF892FDDC0FF4BF8BB0FFF97F035FFA3 + F0875FFE4FCAE179B9FD9FF852F91F08E90BB33ED427A7D8A7D8FF5F81DA1F90 + 3FF891F92CA1BC24CFA86FCE3FE3C5FB27C03FC4FC80217B0890FF28CBD7173F + D23CFE03817ED4BE6132585E57FC9113EDEF08DF4A762BD82E5D8BE8575893C1 + 0F07F2B5EB7B28F9525D118AF3EA427A60AB0F8A7275F54932F5BD29A7F8969D + 98CFE375F0FB032593A64C9832C9AA8D062D15C85F02E70096D5B994BDC9C884 + A908E94C0FAB02AC47FE00CA6D41C232694BF39874656F5BC5D0D00D5897FC41 + 966DF9BB8753027BFAB23ED62511C04CEA4AF314A1783DEC4D2CBC7C795D5A9F + ED90EC64625AF24E966C119ECA72F2B7A11A13B8FA68A4FA3BE1DB8836070C26 + 85198D544837939CECF94B609926B461684A1B8766EC792A27811BB2ACBAAE0C + 726297C95E094CE4FEA04236C921237635F0BDB29A207F7F6849D7D38CFECE4D + 95A94EE34A4C52AB8A394CFEB2ACD029F7066EA2A585E65ADA024D35C826754D + 34A3B285968EAC4F53138DD5D5E85A54096A2224742342032680B5B5D0545717 + CDF4E83AF4746BEB2D392C347B049B9840D58CC9602A1966EC29CCA1AA25AF60 + 2657F5E99AE9BED26688EF177B4F3713F7843D9CA9AE6B203C9F554DCCA1CAF3 + F48D84D7B32CECB531E9A2BA9191209CD5CC395C33E701A6797A348F436273DE + 643D7D994732D9A2D2CA0CCAE6A487ED31378786A5A500D7958C4D0409DC98D6 + 6844F31B31394BB634D73381B2712BA89859920E92B5B0861A41D5DC4AA08591 + 395D3393C85674DD32B25799C0A5849686966866688E167CFDE6165032334773 + 5E8F20B51BEB1BA08136FDCD759804D6A392EEABBE09CD334553038619ADC564 + B309D5B9DF82D6A54DAF411B59082B5D0B34A14D6E137D731AB7400B636BD2DD + 1AAAB43156A38DB73A419536DDAA166413879B36314333BA47CD3874B7A91954 + 68D3AC6C6E43D7682342572BD1B5A8F0F518B6A2BF97850865AD6E6D0FF536ED + 68E36D8DC6A6ADD08443589BB71224B2868D3D0C69E36ED1A507AC7B3AC1AA77 + 3F98F7728261D7EE30EADA03567DFAC3DAA93FCC38DF7097EED0EFD815DA6D1D + A069D701DA344FAF53671874EE0A23827EE74ED0EBEC00FD2E9D60D8BD2BCC9D + BAC17C4047580CEC04FBE1BDE03066003A8CEC0F9B214C163BC2B4775758F6EE + 41BA69AE434718922E23872E3068DF09A69DBBC3B453771877EC06530747EAE3 + B0D96D60D4A32DACFB77459BC18E30EBD711BADD5AC3B0A73DCCFB3BC07A4837 + 388CEF871ED387A2E3847E683BD6097DE68DC5B84D0B3161E302F45F301E9D27 + 0E428771FDD1716C3FB41FDE1D76833BC3764867B41BDA15F643BBC07A704758 + 0FED80F6631CE1347FB8C8513C71C73C2C38B21E6B7C5D31F7F8464CDCB71C23 + DD1762F49EC5987FDE19AB43F76155A82B95AE58C70871C1FAA01DD818B413EB + 029CB196B09EFAB6467B6047B2275C338FC13DFB0476651C854BDA61B8A61EC6 + AE94C3D81CE38135C13BB0CA6733567B6DC26AEFAD587C6A2D661E5888E97BE7 + 60CEE10558746219969C5A81555E6BB129680BB687EFC4AE18373887EDC45A9F + 2D587A6A1D955BB1236A2F7646EEC1AEE87DD897710A07F2BCB03BF314DCD24E + 08B8A61C85739C27B6D3B86BA227F6A61DC7EE24B223EE00DC930EE120C91ECE + 3D8B43B9677034FF3C8E17F9E064A93F4E16FAE060EA31EC4F3A8883699EF04C + F7C4BED4FDD897B407070987A97E2AEB9020794FA5EDC7C9640FEC8FDA86C309 + 3BE19D7310A7D33C7020620B3C4236E040E466EC0B5A871D27E760F391A9D8ED + B304C7A237E140F87AB806AC826BF05AB8846E8067D26EE141EC7BC11F5E9501 + 38571184B36541220CB4CF95389CBD1083E365113853198DE05B69887A9883B8 + E7F9887DCADEA2E9843484D56422F441A62038A31FE722FA49AE2062036F2520 + A83A91CA78F85D8B861787B5BE1022424D47DD66F235129157831176310051D7 + C290782F1E09B763A82F14A11782104C88B8168EB8DBB188BB1387F8BB092493 + 820442DCED4444DE8A45F8CD18841122AB1310C7043221FA5E1A529FE623E345 + 1192C99EF8FB1988BD9D8A98EA24C4DF4E42D29D04C4DF8A4362752C126F4523 + 9A6C60C45E0B41F8455F04949E824FC12104961E45C2CD00A4DE8B40C29D7024 + DC8D221D9108207B03AF8421EE413A92E85AE3E99EC43FC846F2937CA4BD2845 + C62FE5B47619D9508AD46754FE528A24B2258E64639FE421E95931929F9520E9 + 6909D29E9721EB6515A112294FCB484729F55520F3D54564BCBC80F45FAA64A0 + 7AE6EB4BC879770DD96F9814BD818C579791F6CB0592BB446B5E44FAF34AA43D + 2B47FA8B4A812C1ACBFA9974BCA8227B2E20FB15CD7F738DFA2F21FD5915325F + 5E45FEDBABC87D4D75B2399BD6C97F7B19851FAF22EFDD2564BDAAA47EB2EF4D + 25723F5C44EEFB2BC8FB709DEA3791F7E90EF23FDD43DEC7BBC879CF046D35C9 + 5523E7ED5D92B98FFC5FEF23F7530DCDE1F65D147CAC41CEC77B32D0BCCC0F77 + 90F591711719EF6F0B64515FCE7BEA13A4EA5DC21DE4BEB98582B7D5648F6C2C + FFC33D14FEFA00F91F99A4255DEFEFD33DB98F2CF63C7EC3363D106BA6BEB88A + E4E75790FEF35564BFBC81DC5737E9FA6FD03DBC855CD2C57AF2482EF71DD9F7 + 8EEBF7A8EF3ED9F990EA0FA9FE88EA8FA8E4B5EED39A8F50FCD7C728FC443272 + 88EB7E7F93EEC76D5A93EC7B47D740C8E66BA0FB90F19AAE8BECCA26FB325FD7 + 20F94535925F5623FD2D13C7B26B4E63229F904A76A5926CDAEB7B487F7397C6 + 99DC66A29B49E74724FB90C66B90FEAE069974ED99641713DE69EC89FCBA5A90 + E88254A7760AE94AA7BF4706D991C6DEE1AF6F20F1E535C4FD72151CF65A84D6 + 7E791D71746F62E91EC58B50E25791C8DED1029791F0337B8A57D1BC4AC4D3E7 + 28EEC5252117FBFC12629F5D44CC731EBF82A4D7D7C061CEC31F70F8F062443C + 2C43147BA1D36739F20987822FA845C49342443D2D42D423AADFC942E0F554FA + 8E4982DFD52404F18B223753092908B9958A90EA34FABE49A1EF0E4235E1367B + F86720F87E36826B7210742F8B74E6097DE1E2E5933CD10E7F50888847BC3613 + D9E5D45706CEC31DFC80CB7204DDE7FCDC2502C164B36CAC04813545F0BFC364 + 703E026AF211F8B090FA48EF7DD2CF21ED990CAEC9233BC8EEDB64C7DD2C6AB3 + 2DD4BE9321C2DAFB91DD1C35813D9B7D6FB0CD34CE6431CDF1B999062FBA5EF6 + 7EF6B9952EBC9B4528ECEA0CF8D1B8FFDD6CF83089CCA1F46FA6E31C4196573D + 1D5EB49E2F8DFB925DBE0F8B70FE5EBEC8B72C0B139D89B37738C77036CEDF66 + 6FE802B9373393D9ECA59C4BBA48EE6A9AF03E3E493875254D942798D0BD9E81 + E3F250D7472F33F99B2C3C954FD1F8191A6388B0D6A24D7631A14D6B9DADE69C + C939640313D159222CF5B14B89B4463C8E5F8EC3A1CA481CAE8AA276028E54C5 + C2A330186E797EF02808C28192301CAE88C4A1F2081C2A0BC391F2701C2A0DC1 + C1E26051721FE7C3F7A4F1BDD4B73B3F007B8B42A81D8543154CFE46E2208D1D + 280DC39EE210EC2909A53E6A53DF3EAAEF2B215D349F49E43D8521343F107B8A + 8245DD2D3710AED90170CF0D824771043824B57B61047617470AEF6126793D4A + 38E474A4F04E664FE5BDD4B7BB887433C12C3C99E3E05E122E425C7B9445620F + D92581DB1C02FBC08558ECAF8CC1EE52CE372C834B41309C73FD05B8BEBB94EC + A3394CD6323C9840269D3BF382449E6137AA8B3CCA040EE3BC8BC6DDCA489EC9 + 615A9FC1E1A85DF243495FA8D021C2600B2FE750ECA23ED625E53E76A33687E8 + DE45BA77D07DD896ED8F4D199C07D81B1BB37CB189DA1C0E9ABD819D498E3D96 + 79CD1D4C246707D23D89C0FEF25861DB26CE059CEE27C8DF5DB9A1D896EE8F8D + 495E5819731A8B428E6079E429AC17A4AF0FD6C49FC76AC2FA24592866268997 + 851FC3FCC003581878100B03F6619EF76ECCF7657860D67957CC662F5F6ACF23 + 2CE1D0CFA19E98756E17A69F72C694E3DB30FEF0068CF1241CDA8271477662F2 + 49774C3AE18E5107B763F8BE2D18B26713FABBACC6805D6B089C2777197A6F5B + 8E01D43770E76AF4775E897EDB97A3DFB61518BC6D2586BBAE1524F000D715E8 + BF6B19867BACC588BDEB31EED0564C38BA1D138E10D8E397C9DD9D2B3168E72A + 8CDCB709138FB960E2511792DB8E5107B6D0BCF518E2BE0E03DDD690FED5E8BD + 79159CB6AC41FFEDEBD167F36AF458BF1CDDD62C4597558BD075F56238AE5D8A + EEEB96C191FA3A2F5F8436B3A7A1F5AC2968BB7036DA2F9A838E4BE611E6A3DD + C239328FDEF9B3C458DB05B36133671AAC4996E56DE74C473BF6E69D371BF633 + A709F2D76EDA14D84F9904FB09E361379653CF8C45DB51A3613B62145A0D1F0E + C34103A13FA0BF288D870C86C9E021301E3444846E361E3C188603A99FDA2683 + 87C26CC870180F2099FE8360C5F5BEFDA1DFDB89D0077ADD392F2DC1B107F408 + 5CB720399B612304EC478E8679BFC1D0EDCA29767A419BF61F6AED3B7F06F50E + ECD1DA5D860EF2B223874BEE01B5763446730C7AF58649DF7E82EC3571EA27F2 + EC9AF6EA0313B2C3844A33DA4F59F41900D39EFD60D4AD0F741D585717A8DA75 + 843213AFF61DA044654BDAAF29D95069D3162A76D44F50B66B07D5B6ED69CDCE + 2274B31693B8ED58DE1E2DDAD841D99E43323B0832568B49589AC325EFD3D46C + 490F4110BEED1D44C9F35AB6B18512CFB5B6830A93BD56D4A6BD674B0297B27A + 1BB4A0BD7333DA3772A942FB517E41599B74EBB4EB082DB259836CE6B4472A56 + A487A06C692BBC7995ACECA1DABA3D54ADC9766BB281C694687FCB2F5BABD038 + 972D68BFDA94F3F24AD037A3D28CF6D61CDA591EA659DD00DFABE9E17B555D19 + 51ABCEF97925E8E1071AE3FCBC3F6A18E0474D4381861CD259CB4878F67EA3AC + 8DAF5A6A0A7CCD64AE8A8CF0FD8ECA6FA8FD357BF6CABD7625E7054588F3B016 + 1AE01CBDBF876BFE9DB8FD12C45954730D793867EAE3B32B39A4B3BC6F9AC8CE + B4185F718A32C2D7F23EE90C4F72EEA8255DA5B97248E3025C6730315B87FC94 + A078BEC8E0F9529F623F43ACA5A857D4C9162A657628D665F275EBAC5372F660 + 48EB4867977521FA7FF89D9CFD23D49EEDD68134FED9752BCCAB8BFAE6D685A2 + CC7F02925EB6ADBEF1CF50376A669DB36C293AA6740EFEEFA03E2E4291A3F897 + A04800FF2BE4EF3F0505BB692D11365A1E3A5A10BF8A4434E12BC15329706B7F + C0B52992BF7FC7817DD508DFD3DF4255D308AADAC630306B037BFAAD3034B7C1 + 0FF4FFC8EB7CF3EDEF9EBFF591BE8AF8EA5FE1FB24DBBF84FAE628A2BE398AA8 + 6F4EBD509CF307F31475FF7713C01224B2F74BFD7FF986F10717F15F8DCF6E92 + 1CF5C9FD8F05FDD331E89FF33FF265F23F18D217CF3FBACE2FC928CEFF4F43F1 + 07E3EF201FE31F927F282FFD18FD2302F84B6B7D014CFACA1E6AE8C141FE0023 + C6E47AF8C7B22EF15BB75D0B3901FCD50F8CBF5F4B11D28FB0F400C20F29D2C3 + 8922C1AA382ED5198A0F328A75E98147023F0C490F7F522941F1A1509255EC97 + 208DF33A75D7529413B25CF27C017A206D420FA44D35F043337AD06DCE1EAED2 + 9B89F4704B0FAB02FC76A204F9836EAD0E059D82FC5578C06502F607268205E1 + AA0B1146874957269159073D104B7AF89A04F841595E973C7A8537AFFC015ACC + A1B9EC492CBC896BDB32E2594600EB8850CFBCB6588BE404694D0FEDFCF02E11 + BF12B1CDED1F55F5D0484D170D9559872CE47343DA004844B008074DEDBF2380 + 250F606D0341F87208680E052D03D9ADA48AEF94D56A095F51529BFB1A30A1AB + 42FA94A98FE55AAAA001CD61B0FE6F9BA98892D7FBA105DD0F2681956568A042 + EBAB92ADAAEA68ACAE29237E353485F72FA3A92084399C34D94EED1FD5783DD6 + AF4CF395D1504D1D4D7548465F1F4D7575D0488B3D9165A432E7156ECE24307B + 0AEBEBA185812C57B0929121544548680687B2261818A30587AFD6D04113352D + 2A75D14C784033981C97E531E6BCC62A24AF66622E424AAB12D44CCC44C924B0 + 9AA9A9F034D6B4B0A0758C69C347EB999840BD157BEC7248673D8116C646B449 + 34A53E5A9B096392D7B2B686769BD67212D80CCA66E650A2B1E686C6742DB2D0 + D24C586B585841C3AA0DC991AC359556ADA16E694D6B5882F332AB9B931EDAF0 + 6A5ADA409336B92AECAD6B4CF698B087B1B50815DDCCD00C4D0D8DA06446B6B3 + 6DE6E6A2D4B4E6DCC1B421A6EB6946EB3631A0CDA6AE9E404B530B1961CB7982 + 990C26883CC7862682786E616085667A9668A2638AC6127469D32BC25B998B50 + D6ECA1CC44B012DBC176999A53C964B4198DB13E43BA3F74ADA49B3D989B1B5A + D2DF95F46871A86FEA239D2D74E8DE185B41D7A6230C3A76136F5AFFA0471B6A + 067B2A9BD175D0A6DDA85D2741B2320C3B7681A14357E872FE28DAD873DBAC6B + 0F5838F6A6B2178C4846CB8636FBB4B1D7A24DBF6EBB8E32593E58B0B383A67D + 5B68B56B07ED0EEDA1DDDE16AA76F4F726E838B48141371AEB40F7D9C6126A76 + 6DA0D7B11D4CBA7621DB1CA065D70E7AED1CA0DFD601BAF60E30EE48B6B4EF0A + 1D5BD26FD749E468D66C6B07FD4E6D61D6A313CC7B75817ED7B6506F6F09DD2E + 7630EED501267D3AC2A27F17580FE90EAB218EB018D815ED46F7439F39633168 + C164384EFD09F6239DD066584F58F6EB447AECD19ACAF63FF544C711BD6133A8 + 0B2C077480FD886EE83E6D1046AE998A293B166196C70A2C3FE58C35DE6E5874 + 6A1BE69ED88CC9075761C2BEE59845F519C736628A27D58FADC592F35BB08CB0 + F8E43ACCA5BEE97B1761EADE8598777C35D6F8EFC0E6480F6C8EDA83CDD17BB0 + 35663FB644EFC3C630776C0EF3C032AFAD9877620D161C5B8DD9FB9660A2CB5C + 8C769E8131DBA7513915A34448EAE998BA670EE61D5E8215E7D66373F04E3847 + BAC339DC0DDB49C7E640376C0972C3D6E0DD58EDB51D2BCF6DC3F6887D704B3A + 0A9784C3D8167380D6F3C0D6C87D708EDE8F6D117BE01CE181EDE13238CBDB4C + 1CEF8A65ECC781F4E33851E88D53C57E38967B1607920EE360D2411C66023879 + 3FF627EEC1A1A47D3896BC0F47933C70287E17F6476EC341C2A1E86D700B5C03 + F7A0B53814B50D1E41EBE07C7E09369F9C874D27E763D5BE2998BB6520E66F1D + 8C551E63B1E9F074389F9D87DD416B7038CE19871377E170F26E1C4DDF8FA3B9 + 4771AAE82C4E177388693F9CAB0885F7C528785F88C1F90BD1F0BF168FD05B29 + 88BE9F8E982799887C902EBC78A3EFA721A43A1EC1B793445F24B5C3EEA6505F + 02FCAFD3FC4BA138531188B384F35581F0B9148CA0AB6188BA1985B89BE188BD + 1684A84BFE88B9168CC4EA4824104449E3F13722A81D8DC4BBB188B91981A86B + E188BA1E89482A8339F771B91F02AA0210CAFD4C12DF4F46C28354C4D7A420F5 + 7116D29E6621E9411AB593117F279174C422FE562CE98F41C2AD28A4DE8941EA + ED08C45CF1271B7C1075D91BA1E5A7E0977F10BEB97B115CE289846BE79178CB + 0F31D77D117B33189137821170C10761645BDAB32C643CCF43F2935C243ECA12 + 4473FC831C6A1720ED7931325E9422FB974AE4BCAE44C6CB12A43CCD45CA7326 + 890B9144F24C4E67FC5C4C25E97844638F68EC493172684EDE6B266F2B484739 + F2DE5C46C1C71BC8FF700D3954CF78755110C4E92497C6322F2B91FDAA0AD93F + 33895B4125F53D2FA5F9E5C87C564AF7A208E94F4A68EC82207CF35E5F41D68B + 4A64BEA842CECB0B8200CE7F5385C2371751FCF12A8A3F5D45D1C76B545E1384 + 70F6AB0BC8609BDE5F1576643369FCF63A8A7FBD8FE2DF1E20873D6A5F5D47D6 + EB9BC87D771B054CF83229CAA4B0F0E0BD81DCF7B790F7EE96E8CFFF744790A6 + F91F99CCAD116D26930B7FBB8F22D2994F7D0584C25FEFA188E6158979D5843B + 28FA5423D62C644F602685DF12DEDCA6B25AAC5D443A0A3FDE45FACF5790F2EC + 02B25F5EA1FB771D85EF6EA288E617D2FC820FF7084CFC928D34BFE03DCDF9F0 + 90E691DE4F0F688CC9DF8784FB3262F8FD5D5AF701ADFB48942C235BFF165DF3 + 55717D79EF6F8B3E06DB24BC9605D97C97E449177B0EBFBE83F497D5C8784DF7 + E52D93C554670298EE5DEA2FD769EC3632DFB0A73313E5B4B6DCEB38EFD727A2 + CC219B7209D94C7A3301CE1ECB023487EE2713E6395C277B65043AADF1BE1AE9 + 6F6E2185FE0E292F09749F04D8F398EC4BFCE51A925ED2FAF437CC201D192CFF + FA3A927FB98C44FA6C243209FCCB4511623CE535EBB8263CA9393C790A7D06D2 + E9EF93FCEA2A62D923F871397D3754209E3E5789F4594BA2CF65227D16D92B3E + F669098D15239A734BDFCF43F89D2C84DCE250F0E988B89749DF25D988E4E802 + B7D3107187BE63EEE720F6017BF0B3873DE7DCCE40D4931C24FCCC7A0A117C97 + 739367219AFED7A2385735C9C9C295E7D37829E29E5782736C3398AC8E7E7E09 + 514F2F827360473CAE90A31C114FCA84876FF0832204DC2B40604D21421F92FE + 0754BF9787600213BFA19CDB5C204B847E8F7CC079D173A93F0B81D5ECB19C2C + CA200E6F7F97918130BAA690BBD908A8660FE634F8511948D7ED4F2587C63F7F + 2509BE37D21048328C003958CE5784DA27B0A7F4ED0CF8716E6479B869CEA5EC + 733717BE77F3E0753B1B3ED4F6AEA692E04F7D320FE83CF870BEE55B59C2D398 + BD90D9CBD88BE04D387F9DC965CE819C8D53D7D93B3889908853EC857B351967 + AFA5509984D3D43E4775C65979BFCC033B876CE090D9ECA99C8613D79271F26A + 22CEDC48C4E9EB8938713916C72EC5106271B832127B0B83E09AE5839D69E7B0 + 2BF33CF615060AA2F77059088E9653591A8A834581F0241CAF08C749FACD395E + 1585C3E5E1384CEDA3559138467D872B22B09F640F103CA97E9074EC2F098167 + 59B8207D3D0A02B1A720009EACAF8441E345C1F02C2694723D44D8B2AF3894E6 + 91BC2085991C0EC701267C09FB38F474097B3147D278185C7383B02B87F416F2 + 9C481A8BC46ED2E95E48F3587F79A48C94AEA439A5A49374BB911DEEB4CEEEA2 + 20415033B8BD2BCF5F6037DB40F6B067B407ADBD8B6C60EF64D7BC10ECCC61AF + DA40913F791713C3E5A4B7924353478BB6338D6D978FBB168490DE705A4F46EC + BA90AD2ED9D49F1B423607C339D31FDB33FCB0332B00BB0B9830A76B2A8A1024 + F14E5A8B3D783767FA6163BA2FD6A7F9603D959B3203B09DF4718E67F62CDE41 + EBB8E4876107E96592774DDC69E1BDBB36FE2CD6C432CE616DDC39515F167E1C + F3023CB120F0081612E6F81DC40CEFBD98ED77000B828F6269D8712C09398219 + E77763ECE1CD18777813461F588F61BB5762E4DEB518736003B53760C2912D98 + 746C0B26D0F8B4933B319F74CC3AB30B334FEDC4F413DB30FED07AFCB4671586 + B9AFC250F7F518B97F1BC61EDA81611E1BD1CF7915FA6C5B0EA7ED2B3072DF56 + 1ADB8E012E6B31F6C00ECC3AB50F23776F429F0D8BD17BE362F4DFBC1CC3B6AF + C108D7F518B1670306BAACA0B98B3074D72A8C3BB805938FECC0F8835B316AEF + 460C735D83FE5B9760C096A5E8BB792906EF588511B4DE30D77518E0BC0203D9 + D37807CDDFBA0CBD362D458FB54BD073DD72F4D9B88AEACB447DC0B60DE8B97E + 253A2E9D2FC8DD4E9CAF97CAF60BE7A0EDBC996833732A6C674F873DD56D664D + 85CDEC69A2DF61D15C745DB6508E05E8BC781EDAB1FC8CC9B09C3A019653C6C3 + 7A8ACCEBD766EA24D84C9A803613C6C16AB48CEC6D3564185A0D1A8A5603060B + 2F59F6DEE590CA3A3DD963B737F4383251FF8182E435E83710FAFD0688FCBD26 + 0387C2B0DF20E83BF58796634F687138E31EBDA1D585F62A5D1D65E4AF630F18 + 1038EC317BC2EA74E690C8BD614DBA6C060F431B8249AFBED0E610C89D699E43 + 37A8D1BE48AD7D2768D01E88431BABB5ED2CA04EFB12F6D6E59CB7B210C7DDA0 + 42FDAAED3A8B35F57BF686612F275A936CE9447B29D269D0AD270CA9CDA57ED7 + EED0A63D9616874B666299F63C2D45A4263B41F432E9DB9CEA4AEC816BDB5684 + 7156B69591BFDA9D3AD37CDA1B3189CD79801DC826FBF6B4A76A27BC7655693E + 93B3EA6DECA86C2B485A26805549570BCB3622943313C1EC1DCC6B36B5B046F3 + 56ADD182495E91B6883D71AD04B8DED2AC359A9BD17E95F6A04D4D5BD19E91F6 + BC24CF2430BFA02C4189FA9A71942BE356A214A03AEB5112619D49BF48A72423 + 7F95E5C42FA75F62C2B731E7ECE5D0CC7234646F5F79C8E6EFD5F50559FB2D87 + 6196E307355D41F832F1CB63ECE1CB643013C09CBF5702B759E65B650EE1CC24 + B006BE6212B7257BF332E1AB856F95B4F1B592A608F12C393CD482E499F4FDAC + 4F7E2E26391B284291FC959D6D911C3B5E70F43DD1FEFDEC4B9CA5F11927978D + 6410442C9FEF49E7768DE524B0FCCCAF16F5F529409C07CA49CFBA90CE0B7F3F + 3794E1AB867C9628E1F7B16F1AF3B9E2EFBAA5F6DFE3779BA4F3C7DAEBA43EC5 + B34A45F098544A7576F4E1B3658920AD05E910D7C1F78D0935D629EFAF8DE2A8 + D0C7A8BD6E49C77F01C439729D33F1BAE39FC9CA21F5D5B54F51A6160ABAEB83 + 440033C4D97D1D5EE1DFE516EAD3F7CF42D1FB5610B1F5C808FCA197EF3F0013 + BE75EDAD257BB94E6B0B50FBAB26F88AF1757D9CD53FC6DF11C0D4FE9ACA6F08 + 0D9AA8C0DCBA3D7AF419022B1B0734A0FF03CE49FC35C97CFF7D7301C9F3F733 + 0F609ACBF67C9194FEEF84B4B6E2FA8A7DF5E14B72FFA85FA0E13F2680EB92BC + 8AE46F7D638A6D092207709DBECF08E0BFC8200CFA5F1602BAF6432BFD31B8AD + F84156ACFF8BF8BBB7236ADFB4F8FD9FF23FF9C5F2FF1AF8BAA52FEAFAEE8362 + FF7F12824865D4F763228D11FE4E9EA128FB67F04FCE937E08BFA507A1EFF8C1 + 891E7EB8CE7D3CFE750359BE896F99F0FDFE0BA42FE15B96A331E91A842CF5F3 + 0F64BDD723AF8B07068507118960AD4BB27EF6A0A220531FA4798AF3A50722C5 + 52F161A96E5BF1214A82A44F711D49B6EEBC5AD03D1561A09BF1DB88F420CB0F + A6CDE9C1974A092C2372E51224B296211E38E590085C266A15C95A7E00662298 + BD71D913984956CE0FCBE42B3F387FC3C42C3D3C735E60C58765D983EEEFB60B + CF5F0287C611B634A3719EA7F8102EDED4D44403D2DD80F3FC2A78F82A42F2FE + 956C600862981EF61BD046A21193BEA48789DE5A30294C604F5CF6C895C07D52 + BE60267F7FE4704224C3B98119DFB5A4B2255D879C046630F9CB60F2B791AA26 + 1AAB31C9AC81EF5B904C73267CD5E97ED17DE3B9CDE99E32F9DB9C41F7A2197D + D69BB5A47B4568AE44632A82186EA024F31C6630B1DB880961F6FCD5D641136D + 6D34A17A230D2D341261A3696D9269CCE1A075F5059A686A8B790D95556584B2 + 9C40E67E0E25CD750E2FDD524F1F2A0646503334819A91095499B8650F5E3D03 + B4A071269CB96CC184AF961E9A69B0E7B46C5DF6546E4EF3D5CDCCA1DDCA92D0 + 0A9AE61682FC6DA94F7AA9D46052D5D84478223722B43431165EBD2D8D8DD1CC + 886C3531848A8529D42D99746502D60C9AAD5B41DB4646006BB6B68496B52574 + ADADA1CBE42E6D4E9B73A8693D43A8999A41C7CA8AC6DA40AF4D1BE810B45BD3 + 3C8226D92285BBD66F6343736DA043726AADACA042F62A9B929DE696C2E396C9 + D6664606543711B6B4E49CC916E6A4AF35E9A2352D2C05D4482793D04C146BDB + D8C0B06D3BE8DBD943DBD21A6A74DDCA26A6820867CF68CE5BDC9836B732D0DF + 83DACDF44DD14CCF148D69E3DB80C36833A1AE4FD762C01ED1665432C1CD642F + C9B167B19E11CD334413CED34C7DCD0DCDE86F6F4C7F3F23FAFB71B86D73B4D0 + A36B31B1869E5D679876EA01654B3BFC68688E86841F496753DA80ABB6B2A37B + D91E7AB61DA1D5DA9EDAD650A77BA9C6F793EE8BBE5D47983938A255975EB020 + 987470848E4D7B685AD943C7B61DE96E0F2D5B7BA8D1FD53B6E483019ADBBA0D + 34E91EE810D4ADE85E5BD3FD6A6B03838EEDA0417D4AEC894DBAB56D69DDB69D + A0DFAE3374EC1CA0C16F89933D5CEAB7ED22885F354B7BBADFB6D068DD167AED + 3B41BB7D3B4130EB39742674827A5B3B68925EBD6E1DA1E7D801BADD3A40BF87 + 034CFB768371EF2E84CEB01ED80B6D470C82EDF001B01AC461AEBBC1BC472758 + 74EF046B2747B41BDA170E3F0DA29243523BA1EB8421709A390663D6CCC374E7 + 1598E3BE168B0E6CC6A2835B3077FF26CCF7DC8AD9073661DA9EB598B2670D46 + 6E5F80619B6661CC8E8598ECBE0293DC9661DCB67918B96E0686AC9A8441AB26 + 62D4961998B67719E61F5D87B987D760FABE65587ED619DBC33DB1F2FC4E2C38 + B20153DD9760EC36D2B3753646AC9F89212B2663D8CA29A46726466E9A8161EB + A760C8DA89F869D3544C729D87E5A7B7607BC83E6C09DA834D817BB023FC3076 + 8478629DB73B161DDF8239073760D9A95DD81ABC1F6E7187B12FE534DC134ED0 + 9A07E012E98903C9277130E5245C22F661ADD716AC3EBB119B7D9DB12D6007B6 + 066CC746DFCD58777E03E9748167F2611C4E3D8243C99E389646F5C47DD81DBA + 1DBBFC37616FE80E1C8DDF83D3C9FB7032CE0D07C336C3CD6715769E5D8AEDA7 + 1661ADE70C6C3C321BEEDE2BE0E6B51CDB4E2DC0BAC333B16CF704CCDC341853 + D7F4C5C2EDC3B168E7482C751D8D2DC7E792CE0D3896B80BA7D3F60802784FCC + 4E78A6ECC1B9A2D3F0ADF0836F65202118FE17C3E07B210CDE5C5E0A875F5508 + 42AE8421AA3A1AD1B7E31077370171F7121176231C41D7C210723D12FE9743E0 + 7729188157A9BC1C80B3A5E770B2E80C9567E177D11BC1D702117A231811D783 + 107DD51F5117CF23B4ECA44044E539C45DF147FA9D0864D7C420F9660812AE05 + 21F15628E26E8420F67A30626F8422F26A2002CBCFC3BBF02402CACE228249E4 + 5B6188A6797177A3107B8B89E21044D13A9134167D8BF4544720FE5A2892687E + 5A7538D2ABC39071271429377D117DE124A2AB4E22F6E229C45D3C89F0E2FDF0 + C9D881801C57C4561D46CAF5B33497ECAC3C4DEB9D44F085F388BF138ED44709 + 48799084B447E9487D928184072948AC491761AAD39EE521E3693EB208D92F0A + 91F533D5A92F87CADC5F72692C83DA99C8A37AEAC31424D52420F57E0A321F67 + 23E719C9BC28A0B979C87E5684E2F71751FEEB7514BF63F296C9D862A4BF2842 + E6F3124231B25F9622FF4D050A5F95A1F075190AA89DFBAC00D94F19B43E21E7 + 5921F2A9BFE4FD05C26514BDBD486B9723F7651972A83FEF97529A578542EA2F + 7A7709A51FAEA1FC23ADF9F61AD97B0199CF2A05795CFAEB2D947EBA89E20F37 + C8AE9BD447E3AFAE8832FF0D7B145F41D1FBEBA4FF3A0A5E53DFCB2B28A07925 + 1F6FD0DC6A94FFF50EE1364A3EDDA23E6EDF2650F95B0DE1014A05097C97C6EF + A28CCAD28F37C9169A4B6B967FBA8DCA5F6B50F1D71AD245321F6E93BDB751F0 + A69AECA926D93B28FBED2E2AFF7A8FFAAE21E7C505BA67D750463A2A3E55A3E2 + B77BA493D6F8F480D6AE213BEFA2F0DD3DBA1F0FE95A1FA3ECD323D2F190F412 + 3E3E209D3564FB1D02D94BF23CAF984AEE2FF94073E99AF35E5D421E5D77E1FB + 1B247387C0A4F51DBA8777A9E4F51EA2F26F4F50F137D24F6B17BEBB83DCD7EC + D17C0B057CFFDEDD4016DDBF2CBA4FF97C2D3CF7E35D14D375167F62329B6CF8 + C89EC9642FD7C9E61C9ACF64377B1DE733D9CE04FA4726D4ABA94E7A7F253D74 + BD7974DF72E96F91FDE616325FDF44D6EB5BC87A5B2DBC8FD9039BBD84194C46 + 67B3473621F50D93D197904C7FEF040E63FEA212A92F2F21F3ED2D118E3BEDE7 + 6B487F4936D33C0E45CEE1C793599EF1F32524FD5C85A45F08342F99E6273D2F + 43D2B332243C2941DCE362C43D2A42ECC342C43EC847DC8302243D29422A7D8E + 939F1622FE7E2E62EFA523FE01BF1C918F440EF5FE309BCA6C243CCA42E253FA + 5FA1CF7DD2937C44DD4B43F8ED6444DFCF14FD094FB210793F03910FB211FFA4 + 0089CF4A104F88152847CCD34A91073BE609E7C9AE442CB5639F96239A6C8B7A + 5A82C8C7450827BB221E16218AEC8C20DB426A72085908B99789B03B9988AEC9 + 46EC7DD24F76253E294402D91843ED30CE437E2B11116413879C8F2544D5709E + F20CEACB42E8DD4C30211C7227036177731074271D3E3792E075355E94ECD92C + 21986483695E1083DA017752E12F07E75DF7BDC979D4A9FF36E9ACC915605239 + 80ECF3A73E51BF9B053FF61ABE9526E47D6EA60AF9D0FB79087D90477A699CC6 + 5896435873E8EA33D7E271E64A0CCE5C8EC6B94BD1384F387B310AE72E46C387 + ECF4BB9E00DFEB642FC971787FB6DBE716E7694E85F7CD147851FD1CF59DA5B1 + 335758573C4E5D8AC3C9CBB13875390E272E44627FBE1F5CE8377157FA191C2C + F4C791E2401C2EF21369138E978550198C9365C1385DC579F123708CBD834BC3 + 70B43C1CC72A22709CBD8AA98FC9E3FD05013858128C4334EF307B0D33095C1C + 8C7DF9FEB5F0C8F1C181A220D2114AFA437194CA43C541342F88F450BB3C0C9E + 4534272F80E44927D5F73281CCFA0B43A81E0CB76C5FB8A67BC13DCB177B7349 + 6F9E3FF632587FAE1F7613F6E6932D4C30930DACC33DC7173B52CF607BF22951 + BA66D27CD2B39BFA5D33BDB12BC30B6E59E749A7177667F9C13D3380FAFCE19A + 114072B4664E305CA8BD3DDD87E08B5D3941D8531421085C97EC40D1E74CB22E + D4CF61ADF750BF474E28DC3203B133D58FD6F5C1B6242F6C8A3B8D7591C7B12E + EA0436C79FA335E8DAF2C2C9EE70B81376E747D1B5C6C0A32816EEF9D170CE0C + C3FAE400AC49F2C5FA94406CCD0AC1E6347F6C647D1941D890E88B25A1C730CF + EF00E60842D603534F7A60DAC9BD98E3E5894501C7B030E028D50F61F6794F4C + 3FB317138EB962EC911D040E95EC8A29275C31F5980B46EFDB8C41BB566230A1 + FF8E65E8B3653106EE588DA16E1BF093FB468CD9B705A376AFC3C0AD4B3160EB + 328C705B8F09073763DAD1ED9873C615B34FEFC2C4435B3062F77A8CDCBD91EA + 2E98756A2F669CDC83F107776294C7560C77DD80B17BB763DCBE9D18BE6B1326 + EE77C182D39E18E5B211BD57CD47EF95F3D177C5028CDABE0E933DB663E29E6D + 18E1B21683B6D07A3BD660D2DE6D98E8416BEC588B215B56A2EFBA45625EBF75 + 8BE1B46A019CD62CC0808D4B457F9FF58B3094E60EE79CBE5B57C269DD32745F + 3A1FDD16CF458FE50BD07DD97CF459BD94746DC1A08D6BE1B864013A2D980387 + F9B3D189D079DE6C745D30179DE7CE82C3EC19683B6DB2C8BD6B3B65123ACC9E + 8EAEF3E6C091C61D17CC43F785F3D06D2ECD9D391DF65326C27AC258588C1D8D + 5604EB7163604DA5E5C89F603674188C063079DB17864EFD61D4BB3F0C7AF485 + 41CF3ED0EBD10B3ADDBAD3733D7BEFF6822E939E3DFAC0A0773FE8F7EC4BE80F + C3DE0361D46720F4A8ADE5D80B6A9DBA4295F6051A9DBB084F59F50E9DA0C564 + AB03B7A9BF8303B4A95FAB631759386592D5EBD20D065D65E4AC2068BBC8085A + 265E55ECDA4199F6384A366DD18249DA369C03973D6D3B8A90CCAC8773DE3289 + CB39739934D6EAE808AD4E8ED0A07554D82BB73DED55BAF480115D8301E7F1A5 + 7E35D2A16ADB161AB44F1464AC257BDADA41B54D5B28B5B64373CB36324298EA + EAECF9CBDEBBA44BBB7D479AEF20422FEBD23569D3F531C9CB6982D88B973D78 + D5AC680FD5DA16EA64AB3AD9AD268732D9CFA19C95DBD84189F65FCDCDADD0D4 + 94F6CC12E14BFB4101637EB199C004ADB1958CC835B140337EA199495E420B02 + BF98DCC2C45294CD49A6B1BE09EDE3690F4A7BD9A68426BA26B43FE5FDAA39B5 + F90567599D5F5866D29773F132C9FBA3A681DC5B57566708CF5E353DE1DDCB9E + BFDFA9EAC8BC7699FCA5FA8FFCF23EC9301AD0780392E37CBD0D99F495D71B30 + 29CC219C09DFD33C069F1B714EDEAF5BA88B92DB22AFAFBCFF1BEE678297C05E + BE5F3597413A7FE273248EA02711BE92F383D4969D67F119990ABE15E797D46E + 44656335E17CC1A5846F1AAAE26BCECD4BE35C2A429A23E478BCB14A6D9A3786 + 48FB2695DCFF27514B86D6F6B5C4D78DF8FCB039FEF263732A5B08701FE39BC6 + 7CAEF8E7A0782EC9F5DAB33CB2FDF7F57E07CB486793529DCBDAF353B2ADB62E + 8708592D08EBCFCF603FD32DC86C2E65F74AD4F99A158856E9EC97EB12A47BA3 + 08C5F17F84BA67DDF5C930FE8C0C4351EECFE08F0860E92C9CA128F38F20C92A + EAFA2F8744FE7E8100AECF4E467DB28CCF0867898792F055134102D74B4CFE59 + 28109882B4657CD510DF7EDF0C4A2ABAD033B4848A8621BEA3CFA1C4AD31E15B + 5FEE5F59B86A396FA6A0B7BEB5FE9FC03F6D37C9D692B07F127F44007F09FF88 + 00FECB57323D32EFDFFF5D04705DF087B6F683FB191408DC7F17F27FBCCFFE31 + FFC13FEEFF76287E79D5BD0F75C7FE53A8253FBFF42691345E1FEA93FF23FC93 + F3F8478F7F74D9F3577A5B8EBD81C50F31FFC88B92C649AE3EAF5FBE3EEE6302 + 586A4B7D8210FEEC5A14EC52B4B3CEC344ED8387BCADF8E051DFB8D4FE523FD7 + 3F7BC891F7490F42F541DC072AA5B515D797F4312459C5370D15F5C8C85C1A53 + 207C99089648602EF941F74B04B0A27E462DF92B8D53594B04CB3D6D25225696 + 438549605A57E16159D146C966A19FC70952B89DAF691EEBF8813700F2877901 + 5E834A89E81521A815C07D9CD345B2450AE5C3798B1B2869D792BF0D98882588 + BA9CF8954A2665194C004B398339EC337BFF327E60B25894B4119013BE3F70D8 + 6706D57F54D1406355268FB50538EC34EBFEAE96F055A1EBA5CF3CB57F20DB64 + 4434DD9B66F4B76DD202DF34A5CF2F81DBDF3567390E152D23821BA97088670D + E1F9DB943D80B575D04C4757066ACBC860F612D642330D5D34E53CBBEC494C73 + 1B28A9A221D9D798F312ABAAA3A18A2A5DA3AAD0C961A59B934E26789574F5A1 + A26700650ECBACCB842F879A96C85FCE39CC755A57933DA43564E4B2BA8C7456 + 36348296B905B42C5A41DDD40C4A061C2A5A47946A26265031E21CC07A686AC8 + 84AF116D3C8D0511ACD2CA149A6D2CA06B6F0D1D5B6BDAD05A40DDDA9CFA5A11 + 2CA16E45B06E258858035B5B41F26A5AD066943D94F549B7A909B45BC9D6D5B1 + 221D5656D0226813D4CDCD45DE63656323A8999941DDCC021A34579564954D99 + 00E690D3E682CC6D4E324A64978A992995649B9121D4684CDFCE0EC6ED9938B5 + 957923935EF64E66025B976C31E9D001660493B66DA16FD3061AD4CF9ED51C3E + 5B9647D95010C14D74381732AD61C8A1B269C3ACC5F995E9F3449F11914B9943 + 6BEBD23D2234D6D2A33EFA9B6A920E319F40FD4DF4698C37DEB4A16EC69EBF7A + 7C9F69836ED40A2AA636D06EED40F676414B735B34A10D7D73CE4D62628D6646 + D66869CCB9916DE89A6CE8FA383FB109FD0D18A68204D76DDD1646F69D6168D7 + 89EE71076AB7A77BD8169A9676A4B72DB4086A16AD490FE73C36135ECAAA9674 + AFDBD0BA745F34ADADE8EF6703D34E9D60D1B51B0CDA75140714FC16B85A2B5B + 6858DB938EF654B6A7B9ADE973C0070A4C1CB7A3F1B6F499688DE664AB1AADC9 + 1EC7FC56BBBA5D7BE874E80A3D3E7C69DF51BC116FDEB70F4CFBF05BFE5D60D4 + B31B4C7A77876E570768393039DC49F41974EF2260D4BD2BAC49DE6E503F583B + F5449BFE7DD069E430F49A3C067D678C47DF99E3D16BEA28F49B350E83174DC6 + 90A5533174D9348C5A3D0763372CC264E75598E9B11153DDD662F88679E8BD78 + 227A2E1A8F9E8B27A0C7C2F1E83E6F341C678F40CFD9A3D06B2E61FE28F45E30 + 12FD978F27F9E9184618B26E2A26BB2DC582239B3063EF2ACCDEB312A337CE44 + 1F92EBB7783C062D9B8A21B4E6A0C5933160D144FCB4663A26382F1418B76D2E + A6BA2EC6B2E35BB1CECB1D4B8E6FC79C031B48D766CCF7DC88C9AE2B30D67911 + A6EF59878D7E07E116730AFB924EE168960F3C53CFC335EA083C628FE170DA19 + 1C4C3A812D7E2E58E4B91C0B0F2EC58A23ABB0FCC84AAC3CBE12AB4FADC2B223 + 4BB0FAE44A38076D837B840B7687EF846BD0566C3EBD12CBF6CEC262F7695877 + 78215CFD36E050C40E1C0EDF86BD016BB1EDD4222CF79884D95B8661CED66158 + 77601A0E04AEC3C9C8EDD8E7B7069B8ECCC682ED23306D6D7FCCDB321CCBDD26 + 60C18E5158E83C12EB0FCF829BFF6AEC0BDF8C63496E3810EB0297904D708F74 + C6E99CA3F029F34200E704BE1882A0CB21F0A90AC6D9523F1CCF3F87D3F967E0 + 5D7C16A19783107D2302A19782107421008117FDE17FC91FDEE53E3857720EDE + 95DED4F683CF051F9C2E392570BEFC34BCCA4EE27CE92978979D864FC9099CCD + DE8B9329BB703C71074EA7B9C127F700828A8F21ACFC14A22F9C4348E909518F + BFEA87F81B0148B81988845B4C020722F28A2FC22F7923FA7A0012EF8621B63A + 1891D7FD117B2B0831241772F13C82AB4847D579845FF645C28D20245D0F420A + 9599D5A1C8BA1382B45B7E88BB7802A1C5FB105CE081F0927D88AD3884B00277 + 9C4B5C0FAF948D08CA714148C15E849578C2BF603FBC73F72380EC8A257BE2AA + 79DD08A43F4A41CECFD9C87C9685CC27D9C87A968BF4275948AA4915A1AA531E + A489B1AC1734FE2C1D994FD3904D65D6E314643E4AA23219E90F12A84C42F693 + 54A4DDA7F6FD54643CC840CE935C14BDAE40F1EB2AE4B147F1CFC5C87C5144BA + 0A91F5BC80F41422FF97121A2F21B91294BE2D43D1AB12E432F9FC380779CFF2 + 5022C68AA92C45D9FB0A94BCAF44D9BB4A14BF2947DECB62A127EB39EBA175DE + 5C42F1DBCB28A2B2E0972A14BDBC8492B7D748FF15E452BDE0CD1554FC7A1B17 + FEC6C4E90D9A5745D75E85D20FD751F2E61A0A5E5DA0B997C98EEB34E7AA9857 + 4E635534A7F2372670AB51FEE93ACA3E5C45F9C71BA8FADB2D54FDB51A5582B8 + AD11046EF1A75B28FE7013A54C107FBC8EF2F74C46DFC485DFEEE0E25FEF90DC + 6D5410AA7EBB47FD35B4F65D547CA2F9BFDE23DBEEE222D956F6FE260A5F5DA1 + F2062A3F558B7955BF92FE8FF7C98E87B4E663D2F3886C79403A1E900CF5FD46 + 7DBF3DA1F209E97A4436DE47F13BB287C0F32A685EF9A71AB16639AD574AF697 + 7CB8861226C4A95ECC24F0FB5B247B8FE693BEBF919EBF32782D194A4967E1DB + 3B247B4B46A6130A484701DF3FA1936CFC44F6134A3EDC431113BC6F6F23FF6D + 35950C9267A298FBDEDF119ECC4C0033E19B4FF72CFFD30DE4D33D631470C921 + BBDFDD9279243398007EF77B086A0EB7CDA1B5396C75067B05BFBA8AA4171582 + B44D7A5E8E949F2B04419D4DEBB28777C62F4C58DF94E9A176DACB2B82304EFB + E532325E5E255C46CA8B4AC43F2E41CC830201267CE39F1493CE52A492EE7442 + 1AE9E6F0E8E92F386439874A2F1221D499FC8DBE9D82C85B4908BF9E80983B29 + 487B4AFF4FCFF2914C9FE724027BDC273FCD11C43023F9490EE21F6620F2AE2C + 377714877C7F94839847F9887C908FA847C5887A5C8AC8478C32AA5720FA4905 + 629F11C88ED8E7EC314C20FB629FB297721122396CF5835C44908E88873908AF + C944F4FD2CC4DECF441C13D4646B2AFDEF7108F8B80799D49F41B6E42095FE8F + 529E16229189E89A2CC43DCC4534E748BF938ED05BA98860E2F65E3A023927FA + CD44F8D37506DE4EA5BE0CD12FF28493FE304208AD15742F0D81F75211783715 + 017799B495859F1661A83904F5BD2C117E3AE076BA2075395F72D05D26823348 + 771AFC6E240B70EE760EBDCDDED3E1F773687E1AFC194C46DF4AC6F9ABB13875 + 210CC7CB8371ACD81FC78B7D71A2C41F2709A74A0370A63C08E768FC3C87F1BF + 18815355245B158E5317A3686E3CBCAF27E1DCD5049CB9148B735738FF713CCE + 30F9CB44F295389CBF168FA365C1F0C8398F03059CFB3E18478AFC7020579627 + FF28AD79AC84F3E15349F02CF48547B617DC33CF0BECCDF1A53EF6100E12E4EF + 9E3C1FECC9F5C681A2001C290BC5E1122677834937CD65729809DA6C6FECCEF2 + C61E2AF791AEFD8C3C6F413E1F2D09C189F2702AC3684E200E12846E5AE34061 + 80D0B387E477249F8073F249786479914EEE6722D88BDADE027BB27D846D7B73 + FD44DD8D6CDD957E0E2EA9A7E14CBFFF5BE38F897267F229516E893D82F59107 + B18EB0366C3FD6841DC0FA88C3D8167F1AAE69A4232B086EE97ED89E788EE69E + 85738A3776A6F96207C139853DA8FDB02B2300BB7342E81E8409B8A507627BFC + 796C8A3E89756147B03C703F16FBEDC1FCF36E987D6A27669D74C1BCB3BBB1CC + DF93D63C49EB9D11D818E38D6D09FEA43B143B33C2B03929002B23CF6161F049 + 2C083E8EC56127B02CE21456469D16C4EF1C9F83987E660FA69C70C7C4233B31 + DED319E30EEEC0D8FDB272EA31774C3FB907938EEC26B861DC21178C3EB003A3 + 3D776004C98CD8B715A3F66D16E4EF4FEEEB31C8653506BBACC1C01DABD077CB + 4A0C745E87A1BB3662D08EF518BC731D86EC5883015B57C069D3120CD8B60CC3 + 5C566094C75A4C3BB60373CFBA63C609178CDD4BFA3C366302AD33E3B807E69E + 3E80D927C8CE237B30D6631B863AD33A5BD7C069DD72F458B910FDD72D438FC5 + 73E0306B12DACF98807653C6A1D7C23918B1790D466C5B83411B96A22F13C384 + 41EB96A01F95FD562D40FF550BD16BC96C749937051D67D1BCE963D16DC174F4 + 5E3E0F3D97CE41AF15F330CC792DC6BA6FC318B76D18B163339C962F4697B933 + D16DFE2C7A7E9D4FF31752B9105DE7CD46C799D3D076EA2441F476983E159D66 + 4E47B739B3E0306D2A6CC78D85F5A891683572045A8F1D0BBB4913445FEB51A3 + 60C33978C78E83ED186A53DD8A73F7FE341CE6043382E9F061301BCA619C07C1 + 68C040E8F576820E13BCDD7B43B75B2FE83BF6A6E775277A96EF29485C0D2639 + 3B75857EB71E30E2F4344CFEF6A0B2D74098F4190CE3DE8360D0B31F74699E56 + 97EED0ECDC0D3A5D1C690E7BEE32E9DB19DA0E4CD4768616B5F53B3B8A1CB94A + 36F6686A457B176B5BA8DAB6834EC7AE30ECD693C6BA8950CA4CFA2AD9B413A4 + 6F332B5B820D9A4B24307BD9B2776DC74ED07190E5C055A3B68A5D07A8DA3B50 + D9515E729E5D07E1A96B403631F4989016A46D5BA8B46E0325F6CC6DD506CA4C + 005BD9A3A5451B346342D69C6CE397655B533F41A5B52D54F8455BB259C59AF6 + 6C64077BFB6A3121CDC431875DA6399C3E885FDE55E31CBB4C24B3C72E5F23E9 + 50A6FE96D4D7CCDC0A8D82AEEEE90000FFF4494441548D2D683FC85EBB4CE0CA + D082F68CCD042C449BEB1C8EB909BF74CC2F198BD0CC26224A9584260C7DD3DA + 7CBB8D685FDB885F64161EB9FA68C4E42E13BBA24E63D466EF5C0EE7FC3D93B3 + 72EF5D868CEC9585799679F64A2430B765642E13BB12D92B11BE8A751E97C85F + 3E1B924A267AB9FC5699BD7F7F277F0501DC521D5FB7501367430CE90CACF62C + 4C910096A71A93C85F890096C8DF5A478946344F4EEE2A12BAB5A4AEBC5FAA2B + 42515E8CF3191F9FF7F1D91F41227EFF6902F8B3369F3DB610A88FFCFDE708E0 + CFCF25B92E9DE5F1D99E7446599F8CE21920F7299E8B4AA5E21C451D529F54AF + BB963426F4D443AE2AF62912BF12245945B92F41D2599F6E45D49591D6F992CC + 9F455D0298511F9F5057E6CF42919FF82FC77F900066D4258005E4DEBF9F11C0 + 7F4748D6415D5EAB9E39ECE1FBCDF7CDF0ED0FEC7CD60C5F7FD75440D43907F1 + D78DF1CDD74DF0DD772453270C3413C07FF9B61EBD5F58EB7F3CEAB35BEAFB12 + 2412F6CFE28F08E02F11C25F2280EBC5FF460298FE107FF7A601D5FFCE6BF73F + 4502D307BDBE7FCCFF2FE38FBEC0EA8EFDA7F019D9591FA47149A66EFBBF18D2 + 0F60ED8FBAFC875B3C1470C963724257227A198A64B0AC4EF3E4D72CF533449F + B89E3A6B2B5EA3B41EA3CE5B689297F0677DF2870F09F58D7DA92DF5D57D6851 + 7C90911E8EA4B9DC27D515E7B28C44CE2A3E5849E33292961F52655024812522 + 98C32C7F45F3EB12C0E2618E21D72FC063926D72FD52C92172F88159845D66F2 + 573C644B0430AFFDBB1EC956AE4B5EC53FB05D640BBF9129F4F083B7FCE1BF81 + BA6CE350FB66A7AA361AF01883BD7D15C85E8904962011C30D5574D148993612 + EC0D4CF6301AD03ADCE670D08AC4AF5467D25631577043391A933D9C1BB8911A + 7B3D6BD03AEA640B97A44FEEF92B91BF8D4986E732992C74D39A4C3C7F47F7FB + FBE66A64930E9AD0C6A731DBDB92D7672F61FABB3657162513BF5229BC805509 + 728F5B41F46A69A1A98E0E816CD2A6B538CCB33A8DB30D647B13BA5F8DE96FD1 + 4899FAE87A1A931D4DC99E266CAB12E96DAE44F74099D65713A470134253D2DF + 8C74309AB0C73187A356E61CC3EA743D1C929AE4349884E6EB9491BF0D54D9BB + 5855D8D4823D898D8C04E9DB9C730E4B04B0A909D419EC1DCB619E39CFAF9931 + 542DCDA163670D83766D60D8CE06BAB6ECB9DB4A40D3CA0CEAAD4CA062660235 + 92D7B4A47E6B6B6858980BAFE2167A9CFF5787EA7AA4D708AA4686541A439DE4 + 554D493783FA5B1AEAA399C881CCE1AF75A164CCF9868D05D1CBB6A8B63287B2 + 05139AC65013734D44486C268DB52C2D61D4B61DCC1D1C60D8D69E6C9285A2E6 + B9CA1CC69A09621B1B18D9D9C1C4BE2D8CEDEDA0D5CA022D0CF4D05C5F172D0D + 0C05988456323210750E97AD646084A6DA1C3A9D09601909CC39A69BD386BA25 + 87C6E2CF3E7D769BAA1BA099A6319AD0669A439137D0A6CFB5960E1A0A3299F5 + 30C1DD4AE4365631E5D0D6F6686162874606D6F8D1804379B521D850BB156DE0 + 5BA1B98115C15A849F6EA64FBAE91E36D135400B4326C82DC9765BFA5B5943D9 + 88497CBAD7666D445BDDACB5285B1A71A86B634142373732839AA515B46D6C45 + F86D35F3D6D06DDD0E661DBBC0AC4357E8DA74A0FB64030E59DDC2983DAEDB88 + FCC54CFAB610A4341F44B4460BD3D66846E3ECA5DCD2A23534D8EBD8B613B4EC + BB40B75D571838F48061E71E3072EC03B3DEFDD16EC468741E3B010E63C6A1DB + C489541F07EB810361E0E808DD2E3487A0DDB913743BF3414C57583939C176C8 + 2058F5ED03EBBE4EE836760CFACF9886BED326C271DC08B41BD61F76837AA1ED + 7027D80FEF03DBA1BDD071F420F4606278FE648C5AB710A3D9DB62CE04749C30 + 14766387C166F41058FF3400AD692E7B1C771A379CE447A3F7ACB1E8357D0CBA + 4F1B8D1E54EF317B1C1C678D418FB9E3D16BEE04F45D3805A356CD13B2F623FA + A3D3C411E83E7D1C7ACF9D8CEE33C7C361CA4FE83D6F22466F5884B15B1663F4 + A6F998E8BC18D3DD5761AAEB4A8CDA340F03564C85D3F2C9705A3605FD964DC7 + A0D573317AEB2ACCF574C18AD32E5877CE19DBFD776347D01EEC08DC038F084F + B887EFC3BA331B31DD6516266C9A8825FB9760EDB13598EF3E178BF6CEC7BA93 + ABB0FAD8322C3FBC10EB4EADC036BF0DD8746E35E6B94DC5E40D23307ECD108C + 5B311053370CC7B2BDD3B1EBDC4AECF35B0797D38BB1FEC00C2C741E85714B7B + 60CAEABE58B77F1A0EF8ADC1D1D0CDD8E7B312DB8ECCC1B29D633075553F4C5A + E184591B87639EF3182C759D84CDC717C0D57715DC02D7E140E476B8D39C6DFE + 6BB02B6C330EA7ECC1E9DCA3F02D3B87802A3F045EF0877FA53FBC4ABD713CFB + 188EA61DC0B1947D389B7D185E79C77134791F3CA27709EFE153794771B6E004 + CE141CC7B19C43385B7402E78A8FE37CC909F8559E41D0C573F0293946329EF0 + 2A3E0AAF8203389EBC0D9E916BB13F7C0D3CA3D6933D6BB13B683976F92DC1FE + 8875381AB7055E59EE0821F9B0B21388AC3C8D98CB5E88BBE187C45B0148AA0E + 42D29D6024DF0D41DC2D7F445E3B8FE81B3E88ABF643F8D5F308BD7806219527 + 115A45F3AE9C47C2751FC45E3A27904CB23195471194EF0EBFCCED389FBA115E + 291BE097BE09BEA9EB1190BE11C1D95B712E712D8E44ACC0B9D46D082BDE8FD0 + 124F04951E43D4156FC4DF0A45E2DD6841E6E6BFCC45D67326803391F5240319 + 8F52692C9E6C8941724D22F5A721EB692AD5E34498EBD4BB3188BD1182902A2F + BA8E30643F8A47C18B24E43F4F415A4D3C321F24535F26F29FE6A2E8E7521470 + 48E947B9C87C5A80DC9F19B4DED36C643DCE42DEB36C14FE928B02B2A1F86501 + E9C841E6C35464DC4FA1F959A8785B84F23785287B554872F928F8391F8572E4 + BD600FE502E4BC2846FEAB3214BDA942C9BB8B287A7B09B92F2A48E745547EBC + 858A0F3751F00BF553BBE2FD755CF9DB5D5CFEEB1D14BDBA88ECA7C534568692 + 571750F2FA224ADF5C44397B11BFB980D2B71768EE159A7315C5A43BF77911B2 + 1F17D0DAE5A8A4BE4B7F6562F706AA7EBD85AADF6EA3E2D32D947DB881B277D7 + 514A28235B4ADE5492BE8BB8F0EB7554FE7A13659FAEA39CE42EFEF51E2E102A + 685E3913CB1FAB51F9E936E9A2FA876B32329BED797B11173F5D43D5C73B287D + 5F43E38F70F9FF3CC3653CA3B59FA0EAD363EA7B4CFA9EE0D2DF9EE2CAFF7941 + E3CF855CD9871A9A2323742FFE8D647E7B407610A8ACA0752A7E6302FB8EB0A7 + 98EC2D794F7DBFDE27D927B8486B54FE2691CC4C1ADF27DC43D9C7BB28FDC0D7 + 7A1B4C7A57D03554D2FDACFCEB5DE1A55CF189F47DBC4DEBDE128472D1DB1B32 + 6F6A42D1DB9B28A13126900BDFDD109EC7451FE96FF3F19A08D19DF7EE0A9557 + 90FBEE320AA85DF8E13AF2DFDF442ECDE350D54C0267BEE5D0D3B790212F33A9 + 4F780633B8CD442E7B7DD3DF368BEE5FF6EBCB348F74BCBF233C8F05F1FBCB55 + A4BFBC46E31CFAFB1A329900A6CF0723F57925929F9621F169A9C8739DFC9C89 + DF7292AFA4B9174418F31CD29DF17325D299147E524C28122F36B0F77CF8F558 + F85584C0BF8243AEC722ED5106D29E6423EE5E2A226F2522FA4E0AE21FA623E1 + 6126E2EEA721B62619E1F47F16703514FE5742117C335610C11177D310519385 + 98C745887B562E23811F1623FA492962C8B6680E4B4DEBC73CE3B2985044FD45 + 887E548828268E1FE6D3BC62C43ECE472413C08428D6793B1531F73290FC280F + 294F65C474E2C32CB23D97AEA3004954263DCE4602F73FCB47CCA31C7958FC44 + 84D1DCB09A3484D7A423EC5E9A40F01DB29FDA11C2833913110FB2104EF5601E + 238430683C5C8C912EB283F322FBDF4A86FF8D44F85C8D83EFF504AA272190FA + 42EF66905E9A7F8B64AEC6C397C639C77204F547DECBA2321341B792684E1C7C + 245C8B1604EFC9B2401C2BF0C1D13C2F1CCFF7C249C2F1BC733896771E270A7D + 70B22400A74AFD71AADC1FC7CB0370A23C0827E96F75A2321C27AA2271E24214 + 4E5F8E11DEC4272FB2D76E184E51798EDA27A87EA83800C7CB649EBE87F2BDB1 + 27FD04F6669CC4C15C2F1CA6B50ED33A9E3967B13BED2476241CC5CEA413704D + 3B833D595E3890E78B03F97ED89BEB833D64DF1E9AB32FDF07874A027184BD80 + 8B0371B08889E4209CE01CC28501C2E37863E47E6C0ADF839DB107B137FD380E + 92FE4334F76036CDCF3C47BACFC193F49E280FC3690E355D1642EBF8C035E504 + 3646ECC1269AEF9A781CFB49F660F67978904EE7584EFB40B6A79EC7FE0C6FEC + 4E3D8B9D71C7B12DEA3076C49D805BF219C26921B3339EFAA38F6043E87EACF4 + 73C592F33BB0989E19169ED92A30EFD4562CF376C7D6E85370656FDB98535813 + 72081B228E614792179C93CE637DE431810D51C7B12EE228D6861E259923581D + 7408CBBDF763E1A95D987BDC19D33D376302E7AEDDB30EE33CD663B4DB5A8CD8 + B91A3FED5889112EAB30CA752DC6B087ADC72692D98289FBB761EA11174C3FE9 + 8EA927DC31F9B81B261E73C394537B30F3BC27E6FA1EC26CEF039870D405C3F7 + 6CC2C8BD5B8587ED84C33B31F1B00BC6D333D0C8BDDB30D46D237EDABD85B015 + 835D366290CB06F4735E87BEDBD76320B507ECDC44EDB522E76EFF6DCBD17FEB + 72F463B067EDA695224C72BF2DEBD09FC0F53E1B486ED31A0CD8BC067D37527D + F30A0CDDB15A78028F705B8711AE6B3092CA516E1B30C47935C92C212CC5A02D + AB30DC79035DEF660CDDB21E3D572C86C3FC99683B632A5A4FA4E7D671A3D066 + CC28588D1C4E180A6B82E5F021B0A7FE8E53C7A21DC16EC248B41E330CB6E347 + C07EC22874A6E7D61E73A68BD26EEC089A371856A386A0DDE471E83063922091 + ED095D17CD42AF150BD167D552F45DBD0CDDE7CD42BB49E3D176C258749E3105 + ED274F80F9D0C1B0184EEB8E1A015B7A2EB62130D9CB64AECD18AA8FF8091643 + 86C07CF060B41A3294EA43613E70108CFBF58531E7BA65F4E92760D8A72F0CFB + F6850141BF5F3FE8F5EB0FED3E7DA04330A0BA415F6A77EF03CD6E3D09BDA0D9 + B527F4BA3BC1B817CDE176872E844EC223970960CEDDABD3B93B74BAB05C3F92 + 1B0473A76130EF3304C63D0740AF5B6F7AD62739B917AF26877026E83874811E + 3DF7EB75EA02C32EB427A031A536B668626129D0A2B58D089FACD6B6035AB6B1 + 43F3D6B622E72E93B79C33578461A6712685799C4329ABDADA43AB6D5B68B6E3 + 17523B43DDDE01CA6D3A40A9757B34B76C8796AD3B923C13C91D48DE9EFAED45 + 5E5E6DB6870963DA233635334323235334E7FD9519AD4968497B1ECE89DB589F + D30F99D198255A9A5B43A9158753A6FD9039B72DA16CD906AA56B650B7B415F9 + 74395C337BED2A71D865D356B487A2BD14EDDF9A9858086F5F268299F8E550CE + DCD7C8C81C8D0C691D2681696F2720F7CE6DAA67467B414E496426C23137D032 + A0FDA5117E64681B8A36E37B4D3D7C4F7B54D166CF5D0D7D34927BF18AB31982 + 38B3E1F31AAAB3E7EE8FEA4CCEF21813B25AF88EC16DC237CA9CA3574346D032 + 392BCE77982426D0B838E3E197EF4529237A2570CA3111718ECF80781EED95F9 + DC482ABFE5B328D259DB660258DECF1EC15F355313E19ED9D980CFC0D8F1E02F + 0A04308F7DC79047CFFB1C7CA6267384F8BA319F8B11EA90BB750960452892BF + 8CBF1B939FE1D512BE54FF578860498FACCD678F2DC0E4AF843F4B002BCAC8E4 + 14F5CAD691CE06A53525D427A3282B8D2BCAD557AFDB27E9AB4F46A0015D2F9F + ED2A9CFB4A7D8CBAE46F7DC4ECBF0A455D5F5ABFBEF13F8DEFF95C5BE18CFB1F + E0B3F3F03F09457EE2FF366A6DE2EB96D7BFFE07367EC5A84B02CB8960E1792A + F1607F84FAB82D857176A494085F8900166322F4F3E7797F6B3D80BF6A8CAFBE + A2B98200FE827EC5FAFF4BF857EC562465FF0CFE11015CB78F519700FEFAEB86 + 74F3E5246FBDC4AF043250BAA8FF2DE09B4EE567E42F13BFD23FC8BF40027FAE + 8B3EF0FCF603936E75FE29FFBF02E94BAA3E485FC47F56FEDFC16744E79750DF + B834AFEEFCBA6D86DCBBF8DFFA4153F4D065483A094C004B619E15895F19B88F + 7E6CBF93DDD3CFC73EFFC1ABD5AD785D8AD7C26BF20F373D3C08B296DBA28FEA + 7508DC3F82A2DC971E60EAE20F1F6408F58DF11C894C95203D58D54290BFFCA0 + CA63F252E14196F3957C45727FE13719793EEB9113B59F81FB45DE12B9CD3447 + 405EFF46E1AD497EC0E6103BFC90FD75730EED2CD74B50247DA5B6D4C70FDB12 + 89CCE46DED5B9E04F176286F2A7823206D06C4C642473CE47FDD5216E6E7BB96 + EC99FB3909CCF9821BABE8A111E72926DD4CBCF2032CAFDB90BD82B9AF19DD9B + 261C829989571909CCF851594390B70C890C968585D6A752E65DCB219E7FE4DC + BE722F5D6E37566759D93C0693C922FC7373CE334CF6B590954D54F5D154CD80 + 4A3D025DB30ACBCBC23DB32731974CFE7E2711C34A741F09DF73AE60265D3534 + D0484B0B4D74E81AA96C48ED1F45B8677534A2759BD2F537A5FBD198FE1E8DE8 + 7A1AD37D6A4CB634614F68D2D78009E016CA68443A1BB33730ADD54849856418 + A45F89EE47730E4BDD02DF366F417670686A25345056412335353452A7EBA5F2 + 0715B68DC6B85F934960BD5AF2974B25037D281BE90B1258AB552B6858B682AA + A505D4AC5A41D7DE06C61DED0501AC63CB797CCDA0656D069D36E62467081513 + 3D2819EB43D594094A13919F987317B3F73387AF56D2D581AAA12E540C681D3D + 6DA818128C9978A531AAAB9AE80B1D2D49A6A9AE96400BAA2B9B1AD2669AD66A + 6D012D1B4B681234AC694D0B5AC384495B0328191989D0D1BAAD5BC3D0D606BA + D656A4CF98AE85ED3226FD9C7FD8085A24A36369090396E370C87C7DA45F99EC + 56313110FAD4CC8CA062AA8F16647B4B7DD22DBCA3D9EB973E471A5AF4B9A6BF + 8D067B601B439536E3CD358DD04CC3084A3A66024D340CE9DEEAD3A69B36BB84 + 86DA4C00B307B4295A1898A3A591155D97159AE859A1A19E351A1AB64603AAFF + A86F8D26266DD040DF020D746893AF6B497A5AA101E96E4CEB3767125D5F4690 + 3767BB0C693DF65036608F6D73A81AB7A2B6B9AC9FFAD84399DFFAE6905EFCA6 + B892397B68B7A6FB6905655A53DDC4165AE6F65037B5A3EBB625D8A3B9411B34 + 3360CF613B3437B2410BE33650124430E7866A0D0E49D68CF34D59584389C351 + B769076DDBEE3072E807B32E0360DA99CA6E0360D16B184C7A0C42DBE1E3D167 + FA3C38CD98877EB3E65339170EA3C6A395D32098F6EC0773A7FEB0EE3F08ADFB + 0F8155EF013071EC257271B1F7B045AF3E701831025D478F81FDC081B0ECDD0B + AD7A39C2AC7B1798F5EC4AE35D61D4A503ADE380D6037AC27E787F749930023D + A78D45A771C3D07A701FB4EA3740AC61DCD30966BDFAA24DBF21683F8C748E19 + 8B1E13C6A1DBD8B168FFD34FB01D3204B6C387C27220D9E0D41BA67D7AC36A60 + 7FD853BF656F2798932D560306A235B5ED46FE8476E346A3DDF8D1E83C653C7A + CC9C8CEEB326A2FB9C89E8B37032FA2D9E8ADEF327A2DB8C5170983602EDA78D + 42C719E3E1386F1A7A2E9885EEF3E6A2D7E2F918BC6626C66D998669DB6763A6 + CB7C2CDCBB1CCB3DD7609EFB028C5835128E53BBA1F78C1E58E0361B9B4EAEC5 + D27D0BB0F2E022AC3FB6024BF6CCC21C970958B67F16369C5C86B5471661E696 + 3118B1A03706CDEA82A1F31C3171F5402C74198FCD9EB3B1F3E83C6CDA370DAB + 5CC761C1A621183BB733A62DEF8D0DFBA660ABE774AC721B83B51EE30813B07C + E7688C5DD00DDD475962D08C4E98B1E1272CF7988E0D47E663EBE925D8E5B706 + 1E211BB1D36F35B6FBAE844BE87AB8476EC2BED8ED3892EA8163E97B09FB7026 + F728BC8B4EE254D6411C8C73C1DEB0CDD813B611FB22366357C01AAC3B310FEB + CF2E805BF8061C4971C5E1643778446FC1D134379ABB0FDEC587105C750A1157 + CE22A0FC30BC0AF7C1A7F800FC38CC728E2BCEA66DC389C44D381CBD06AE7EF3 + B1F1F8146C39390D1E818BB13768290E86AFC4D1980D381EBF19E7335D10527A + 0851974E22EAF22984561E4520E90CAC3C82900BC748BF277C4B0F50FB30FCCA + 0EE07C8107CEE7BAE36CE62E9CCF724348F141840AEC47248DFB6739E344CC72 + 9C8A598A53D18B702A6A21BC1297C33F6D35C2733721A6C819BEA96B71227A29 + 02B29D117FE928E104222A8E21BCEA0C62AF0720A5260639728FDED4874948BB + 9F88AC47C9C8A47AEABD38A43F4844CEF374E43C4B41D6E344928F16E1AC136F + 8520A4F20C4E647820A8E224C947A2E45512F25F2420FB491C0A7F4E43F1CF59 + 28F9250F65AF8A50C45EBAF77391FDA800052F7291FF733A321F2723F57E3CD2 + 1FC693EE14B2210DB9EC594CEBA7DC8E42F2CD08643F8847C9CF9928255D45CF + 3290C35EC7F79390C1763E48A679E9C87E9E87FC9765224474C1AB32943069FB + F11ACADE5E45D99BEBB8F0E936CADFDC40F10BEA7F7D05173F5CC7955F6FE13A + 7BE132D1F8AC08798F73C9DE4294BFAE245C1465E99B7294BFAD44D5870BA87C + 5741761720B5261DC9D564EBC35C94BEAC205D5770E9D7ABA8FC78051768CD8B + 1FAFE3127BFB7EB849EB5F43F1AB0A14FEC2DECB65A8785F457D1751C4A4EEBB + 6BB8F45B352EFDB51A177EBB8972268DDFDFC0855F9904BE4E7526A2AB90FFBC + 0805CF0BC9CE2AD27F1B15EF1E52F90857FEFA1C577E7B8ECBBF3EC5E5DF9E52 + FB19AEFE8DF11CD7FECF0B5CFBDB0BB2EB31CAD9FBF7CD6DBA0735B8FCB787B8 + 44B8F0DB435CFC2B93C04C365F171ECEBC3E7B1A5FF874976CA8C145268C7F7B + 44ED07A8FA741F551FEF1168EC37C2DFEED1F5D23D7D7713151F6E93CEFBB886 + 07742DECD55CFD3B094E2867F2FD63355D4F358AE96FC05ED725D4CF9ED879AF + 2E20EF75150ADE5D14A1BB0BDF7138EA4BD46682B58A7041D40BDE5F431E87EA + E65CCCAFAB91F39649DC6A64D1757199CD6012587808DF41FEFBDBC827DB7239 + D7F3EBCB0426946F21EFFD1DE4BDE3F937680E8796BE417A6FC9644926E7E525 + 643171FCF202B245686C9AF7F612C95621F3970AA4FD5C86F49FCB91F5FA0272 + 5F5D14E1B3737EA942CAC34224F367FB79317239C4F3DD14FABE8842D4B568C4 + DF89A7B114243D4841D4ED38045F8E40D0A50884DC88462821E45A14226E4522 + 8CFEA7FC2FFAC2EF5280188BB81D8FB0EA38918B3CFC5E1A221FE620EA7121A2 + 9F140A929709DF682AA30891D417C9B9899FE423823E9711777308D988269B12 + 683CEE511E22EFA421A23A19A1371310703516418448FA1CC7DFCF427C4D86F0 + 5A8EA2F1889B49D49F8C9807E9887B9283B8C7B4EEFD0C84DD4941E86D0E139D + 8AC89A34D117FD209390216FA70B7018E990BBC908BC45EBDC8CA7321E21B793 + 10467D6135A908A5F9C134E677351A5E17C3E1733912FE5763E077251A8172BB + 426ED0355727095B83AEC52280C6FC2F47D1580C82A91D4C327E741FBD2E8492 + 8E30F8523DE07A345D57047C2F86C0A73C105EC53E385B701EE70ACFE17CD179 + 8237CE167AE374A1174ED3D8E9525F9C29F7C7A9327F1C2FF1C3D1D2001CAD08 + 115EC1472B65B97D8F9487E2C485709CA1BFD7192A4F5584E27869104E70C867 + 9AE399730EFBD24F124EE040C6491C909707334E616FEA09B8251D8747FA1978 + E6F9E24861003C0BFCB09F3D7F73CE6377F639813D395EC22B777F3E7B0C9FC7 + 3E1A3F5C1488A3C541D84BED0D917BB1DC773BD606EEC0B670573847B8096C8B + 7027EC8673D41E38C7ED874BC221EC4EA3F5592F959CD37F479C273687BB632B + 6167CC3E019EB7D66F3B969CDC82E5E7B663BDBF1BB686EEC39690BDD810E08E + 353EBB082E58EBEB8A8D811ED81CBC87FA7763C539672C3EB959A4A898757015 + 667BAEC6CC032BA9BE1253F72CC3F47D6BB1E48C1BD6F81FC40A9FBD987F7227 + E69DDA89255EBBB1F0AC1BA61FDD82E9C7B662CAE14D18B76F0D46BB2EC718C6 + AEE518BB6329466F5F84115BE97964C35C0C58C75888C19B9660E8E615184618 + B271A9E8EBBD6A0EBA2F9D0947C6F2D9E8BE720E7AAF5D80015B966390F36A0C + DBB51E233C3663F4FE9D987064B7F0E21D73C099DADB318473DDEE588921BBD6 + E1A7DD9B3182095FF72D18E6BA05FDB7AD45BFAD6BD077CB6A386D5A8DDE846E + 6B96A0F3CA45542E85E3DA65E8B67A313A2D9F858E4BA6C261C90C745A321B0E + 8BE6A0133D4739AE588A5E6B56A1C7EA95545F8E1EAB56A0F7EA5564E362382C + 9C8DAECBE6A2FFE6A51841F6F5DDB8189D974E47CF9573D177FD62745F3107F6 + B3C7C366FA682A27A0C3EC29E838672ABA2F9A8F4E7366C266D278588D1925F3 + 941D3614A68306C17CF020D88E188E762347D033E20018F5EA0923A71E30EED7 + 0B864E3DA1D7AB3BD5FBC062F040D8D0BC36C387C192EAE603FAC374403F580E + 1D8456C387C0944AA341FDA03FC009A623A83D7218CC478F40EBF1636043CF98 + A6FD9C485F2F98D11C13AAEBF67084619F5E30E9DF97F40D412B7A1635E9C764 + 2ECB39C19860D2BB0F8CE959D5A497138C7AF48161D71EC2BBD5B0B30C7A9DBA + 09E874EA2A3C72D5BB76832A3D6B2B938C72976E50A3676FCD1EBDA0E9D81B6A + 5D7A42A35B6FE8F4E807EDEE7DA1DDCD09FADD692DC73ED0EDDC9DC0797B4927 + CDD1E9D41D9C7B57A7734FE874ED039D2E7D60E0D84F40B70BE9EBD84D90C61C + 9659A39D83C88FAB6ED7011A6DDB53BB3DB40459EB203C70556C6CD1CCD20A8D + 5B59A0A58D1D946CEDD1CCDA064D2D5B13ACA162DF4E90CF5AED1DC0219F0519 + 4C50B1E570D0EC896B05152B92A3391AB6EDA1D2BA2D5AB66A8BA66676686462 + 8326E676686261878654FFD188F65FEC516BC6FB191B92A53D0FED771B9918E1 + 477EF1D7D0022D4846F662ACA52080997865AF5A46534E03447BAF16C6B40FE4 + 484B261682E86D417B31DEEBB17C53DA2336A37D19CB36E56852B417638FDD1F + F4696F46731A9B904E2A1B1933E96C46EB9AD25E9040320DD9B3578FBD784D84 + 976E637EB998F69B4D684FC8A4EE771A7A023F30D1CB043043D300DFA9CB72F3 + 32512BA0AA2DBC70194CC28A17F7997455E2731C75518A48694AEA3228ABE35B + 2ABF555213F56F5AAA7D06A95F060DD2A94960729820085C1989FBBD12BFD8AF + 23CA6F5B6AE1DB161CE98DC6A8CE7D5CCADA5C2719AA734A320179FD6B39BE6A + C691E798106622581D7F692A4B47F6B582F3C467F5C6B2369FA589F334AA7FD7 + 98AE494EF44AA4AE8CFCFDBD9F21AB53BF3C54B424CBF8EA478632FED2E0F373 + 3E8900E673B63F4B044BE774D23920D73F23801B341350F402AE8FF865D4257F + 65B272DDBC867C2D696DC5B34F0975FB3FB7EBF73E457945487D8AE7AF92ACE2 + 1CC5F3DAFACEA2A5336AC67F9AF8553CC3FEBDEFF3F515F18FC6BF08F9B9B5E2 + D9F69750975F50E41CEA8ED5457DB2FF69FC33EB085985737BE606BE24A7780D + 9F79034B1EC08C7FD5D95191BCFC5616C2F9733416E42E93BEECF5CBA4AF207C + 097FF94B435949639FE9FAA335FE5FC33F7B2D8A44ED9F8122F95B97ECFD52FF + BF4F0073F9AFA2CE0DF9BF007E43A1BE7EE9435B4BFE7E4602CB213EAC52FBEF + 75D485D0C9FFA0F22FA9CFFE4185ABBF42FB1FE0EFE6FF3740F1CBE3CF4092FF + 23D4252319FFAAAE7F06B55F98757F4414F1A57169AE34AED8A6EBF95D56F643 + F615959FFFA8D5F303C7F3F85EB0AC1CB23E994EC57E86E4FD2B885F9A2FDE7A + E2B69C106602B8D6FB97742886C5A8BDE7A25FD1F63A1076C94A613F831F22EA + 2180A5878F3F427DF28A0F2AD2C38FE2030FA3EEFCBAF5BA90742912C00C49B7 + A813A40747D93A34263DC82A8C7D250F6523E6CAF530248296C1394EA4353F03 + CBF2B89CC415619799046E410FF8FC96A57C1D06CB0902984AE9BABFA57EA19F + FA45CE60D221BC7639D40FE77CE1CD85AAECAD50CEFFC26F7E8A4D07BF01AAC6 + 6F806A0B025896BB85F4082298360FB459E09CC13F121AD266A1A1B2267E5052 + 11A18CF52CACA1654C1B32EAFBBE39DF2BFA1C35A6BF7FE3E6640793AD321298 + 89DB06AC87F4317EA4CDCD8F641B93C94C0833F1CB246D8316AA228FEFF7ECA9 + 4B7D8D68E3D290362D3CE707B2A79650A68D4943D2C168447AD8FBB7095D4353 + BA360ED7DC8473EAD286A7912A4110CBEC152C0B032D720453FD5BAA7FC76433 + 7BE6B287AEBAA6C8E5CB397D39F473439ACB21A51BD27AECFDCBE42FD77FA46B + 12203B1BF23887842634E2B0D00CF6021675553491D7D93B98095F410237E790 + D45C57A27BA24236AA0912984349FFA0C43629D1DF4D856CD040336DCE51AC25 + 086026835BE8E9A285BE2E948D39A4B219346813AED6CA5C40C7A635F4ED6DA0 + D5BA15542D4C0431AB46257BFFAA9A33094B7399B0353280B2A1015AE8EAA131 + 7B3AB337B29A069448B79A919E208095F59900D6829AB12E5404F9AA0F757323 + 6898730E62636A1B092F5CD6A54A6D4D6B0B11725ACFBE8D00E71BD6B430A5F9 + 8650D2D717799195F449D6C85884B056E7BCBF060622AFB1B2A1A128B9AD6A64 + 04F67AD620194D7373681134CC4DC9062692F5646B923D4A6467735DD93DE1B9 + 4C8E37A7EB61325BE47126B4D035243D16D0346D050DDAF06B18594285DFD0D6 + D647535D0334D6D54743926F422593B4CAB4B957267965134B34D737A7715399 + 67AD596BDAE89BA321CD6D4A631CFEAB09B55BD058631D3334A00DBE2080F91A + E85A98546FCC39A5E9FFA3A59EA1F0AE6EA6C37F3B26AB8DE8EF678866645B53 + 1DB2418BC061A9F58DC121A4954CCCE85A2DA06A680D65036BBA260BBA26339A + CB61A9ED68DC16CDA8BF99516B34376E836602D426B4306D236C6D61CEE1CB48 + 8EEAEC09ACDEBA235A3B0D43FB21E360D96B305AF51A042BA721B0E833407800 + F79A36035D274C44FB1123D166E06098F7718285533FB41D3A029D478F43F7B1 + 93D06DF404741C3A12A6DD7A41D986DFCAB7856E87CEB0E8D50B56BDFBC0AC9B + 234CBA7685450F479839768571E74E30E8D81E5AF479D46E670B936E9D60DEAB + 1BCC7B3BC27A401FB4261877EF0C931EBD44F8380E23A7DF89C6BBF781759FFE + B0EB3F10F6030751BD0F8CBA74815EC74E30E9DE8364390C1D7B23778651B72E + 30E535A9DFA2474F614B9B017DD176F860749D38065D268E43C7B123D061CC70 + B41B3D0CF6A306C36ED420C24081F66306A3D3A491E8327D1CBACD9E84EEB3A7 + A0CBB489709832115D674E46EF85933078D9780C5C381A7D670FC7C0F92309A3 + E0346728BA4CE801AB0136B01B6A8FE18B86618EF36C2C709987C5EE0B3077C7 + 4C8C5C3A044316F4C5A4F5A3B0C0752696ED9D87D95B49DFAC5EE83ADA1E4E53 + 3A61CCD20198B66E04666F1889F9EB4760C1C69198BD762846CF75C4C0096D31 + 664E772CDD3E01CBB68DC7C4457D094E98B8B81FC62E7082D3848EB0ED670187 + E16D31689613C6AE188E09AB4660E69609587F74195CFC3763BBD75A6C3ABB02 + 1BCEAFC29AD34BB1EAF822AC3DBB8CDA2BB1F1FC6AE199BC3B7C07DC429DB1CD + 6B1D369E5C46731761E3092A8F2FC1C2DDD3316BD724AC3C3C1FCE3EEBE04CF2 + 5BBD56634FE4761C4FDE83E3A91E3895B117A733A8CEE19E535C7022D505A752 + 77E26CAA33CEA66CC3C984CD3812BD168723D6E078EC069C4DDA86E3311BB027 + 70295C7D16C2CD67113C029693CC269CCB708757D66E9C48DE21BC843DC25663 + 5FCC7A9C48DB81A3C95BB13F7A1D0E256CC2A1F84DD81BB10A1E21CBE1EABB08 + EEA4E358D40678A7BB2020DB0D3EE9241FC17993A7C3DD7B3AF607CCC6F18825 + 08C8D882F0BC9D08CD754644BE0BC2F25D1159B217F1554710597E10C185FB10 + 50B01F0145871156790A093703905A1381949A4824DE89405275A48C7CAD8E42 + CA9D5819D97A2F0189D7C3107F3510F157FC1155750EA14547712ED515476236 + C12FCB1D6937BD50F8381CF90F098FA250FC2C01858FE290F7201E798F9290F3 + 3005193529C87D9C85E21779287C9689EC07AC3B16E9F7A2692C16590FE2482E + 16D9F7A3914476255CF1415A75A8D0914F3AF21F25904C2CD945F6DD0AA73971 + C87A9C8AAC6739287C5986629147B81CC5AF2A51F9E11A2E7088E6F7D77091EA + 659C1FF859312A68ECCAC7CBB8F4FE222A7E2945C9F322423E4A9E66A384F494 + FD9C87CAD785249F8FA2E7B9287D5184AA37A5A87A5B828AD7452822DB731EA5 + 21FB612A4A7F29C485F7170897689D4BB8F4E91A2EB3A72EE92E6302996C2A79 + 5140F78274FF9C838A3785A8785B8AC25F4A50F07339C95DA739D5C223B9ECDD + 15410E97BFADA0B925287B5582A29F8B90F3200B054F7271F16D39C932995D4D + D7750B173EDC21DCC5C58F35B8F4EB035CF9EB235CFDEB632A9FE0EADFB87C24 + 88DC120E65FDEA2ACADF5DC7C54F37C57A577E257CBA41F7E5025D6B09D95688 + D25765741D745F3EB2FDD7857CD5C7DBB8F4DB7DD2738F6C243BA9CD1ECB17FF + 4F0D2A394FEFCB8B74BF2F81435B5FF91BD9F3E90A2A3F5D46D9FBAA5ACFE9CB + 7FABC6D5FF7317559F6EA194730DBFB9827C9A93FDA21239740F725E30714FF7 + 89FE7625748D456F64A1C0735F1422F739D5E9EFC5399F4BE8FE14BEBD8AFC37 + D791FBF60672DFDD401ED994CD9EC1842CF6F06572F7C32DE47EB88E820F3790 + FF9EE45F5FA639D790C3630234FEFE16F23FDE41015D4F1ED7DFDF44015D7321 + 5D33E781CE7D7B19796F681E95B9AF2E2095BD801F1520F94901529E1621836C + CE7D558502F62E7E5E86C49A6C24DCCB44E633B2F9658908499EF92C8FAE311F + 99CF7390F23803290FD390449FF7985BD108BD1C86E0CBA108BE1482F0AB6188 + A3FFB7989B6188BA1E8A98DBD188B9138FE83B8908BB15077F263FAFC620F44E + 0AA21E6623E6491E95B9887C902348E198C7B9D4CE4134B5E31FE70944DDCD44 + 78751AA2EF65218164E3EE6722E62ECDBF9588808BA13857E20BEFB200045E08 + 41C8A530845E09A77A30FCCAFDE157EA8F20AA875F0D1739D2A36B921155930A + F6480E257B42183763453DB43A1E91F79210FB208D6492117E270161B7E3117C + 33866C8E80CFA550785506C197D60CA5BE90EA38045C8B82CF8530F85C245C0A + 27B928045F8F411095FE9723E07B2154E46E0FBC12892042F0D54804908DBE55 + C18410F85404C387C3399706E06C891FCE97078A30FFBC065F5B20C9FA5F0822 + 195F9C2B3E8FD305A745E94DEDF3653E385D741E27F24EE370D6711C4C3F8243 + 99C77034FB348EE69EC5B1221F1C2BF58367BE17F6669D162513BDDC7FB4D00B + 278A7D71BCD0074748F668DE591C91E370EE191CC8380E8FE443D8937A0407B3 + 4EE160F659ECCB20649D8767AE0FC11B0772BD04F6669E856BF2716C8FF38473 + C211B8507D5BDC216C8B39889D7147B093C32EC71EC6D6C88358EDEF8A2567B6 + 60A5F776ACF66112763DE61E5A81B9475661F1A90D587E7E1B169FD98C85545F + 747A2396796DC572EF6D346F2736857910F66053B03BD6FBBB60098DCFF25C81 + 291E4B30DE653126B92FC5B4BD4CE2AEC5BC231B05A6ED5B49FD34EEBA10933D + 9662EADEE524BF0C13DD1689BE91DB6763C4B65918B3639EC0B89DF3317EE702 + 4CDCB50493DD48F7EE35347F15C6BA2C23D9C524BB08433671BED919E8B5721A + 7A2E9F8ADEABA6A3DF9A5918BC611E866C988F416BE6A0EF8AE9E8B36C0A612A + FA926CBFB5B3D177F56C0C5CBB103F6D5E81E19B9763F0FA25E8BF66217A2D9D + 832E0B66A0CB427AA65B340BDD96CC41EF950BD16FFD320CDCBC120308FDB7AC + C680AD6BD06BFD52F4A5F923DCB760D08EB570A4F98EAB17A2E7DAA5E8B17AB1 + 28FB6E5A837E9BD7C069C32AF459BF0A4EEBB85C8D6E2B97A2E392F9E8B47401 + BA2C5B445848F559E8B0782ADA2F9886F6F3A6C176E624C254745E340F3D572E + 438F15CBD07931C92D5C80AE8B17A3F38279B0A771DB1913D1713E5DFF8AF9E8 + 46F63BCC9F4E633304D1DB8EC65A4F1A8D3693C7C07EDA78B4993006ADC78E82 + C34C1A9F311D36E3C7A1D54F3FC178E040980D1902032659FB0F10C4AECDF09F + 60F7D34898F7EB0F5D4747683B76835E8F1E306042B84F5F18134C9D38776E6F + 912B974BCE9FCBD0A6BAB6C89BDB17DABDFA40879E8775FBF687E1A0C1B0649D + 030642B7AB23D41C3A41B58303D4A9D4EED24D40BD63176870E8E4CEDDA0D9B9 + 0BB549A67D07701E5D0ECDCC3974553994319522AF2E875B6EDB5EE4AA65E255 + 83C798846DDF112A84166D3BA019F52B911E59AEDE2E50A135D4E899599B9EC7 + 0D7BF717B97DB5BBF6823EB54D7B3AC1B06B77115E5993F4E875728451D7DED4 + D74B44FE31767482AE43775AA733ADC7C46C077AA66F0B55BB76D4D7016A766D + A1D286C327DB43D9DA062DAD682FD2BA35F5D910DA40DDCE0E6AB67668497DEA + F66D452E5ECEB5AB66DB0E4A344FC5460665926D293C7E592F13C734C7922313 + 59D05E85F661E69668D18AF62E1C5699F6550D8DADF0A3512B34A2FA8F4616F8 + 51BF15814A033334625296D3E59898D39E86F6656626B4FF9191AF8D75699CF3 + E672FE5C7DDA8F714E5CB9B76D432A8507AE96BE0093C28D495690C49A34CE20 + 79917797D058C758CC6192F65B0D5D34E09774F5495E8FFAA9FC91CA1F48DF0F + 345F40CB48C88A1CBC6AF2B0CA6A0C7EB15E0F3F30F92B2F7F247B0434384FAF + 2EBE579679D38A9CBA2D3585A76F03155D71662381CF5FA47318457CABC473D8 + BB564D94DF51FB7B653E2F92856466707866869015F21AA22DF2F8CACF8798E0 + 65FCFFD87BEFF8B88A6C5D7488CE969573CE6AB5D42DB5BAD549522BE79C7396 + 6539E79C73C6C638601B6C828D01631B6C8C8D2338670326C330C099C8100766 + E6DCFBBEB756756F79BB9119269D73CF7BF78FEF57B5AB5655ADAA9D6AD7B7D7 + 2A41E83AF03A13E7912CE10126836D713904A9CBEB4C146719061FF79627F0F1 + 3DBC5E35E836D92AD6CE6C44AF9500E63536EBDA1A1F0B430A0E2502980D2504 + 01CC6B689C4E69369297E3A22C83D7E2842CB53180D7FF386412D36ECD4FA687 + 5517AB6E7710C09C47A1B4A6C69064459CEBBE8B05F04F11C0F6C4AF04EBDEBC + D6353DA90D69AD52D2FD0E5D5847D6B58FBCBF05495EAA438ADF3146B6F45E1D + 9844176BD112A8DF3670BAC01D04B0045BDECF01D56B8D932EDCC683B7DB90D6 + B9EF804C074986494C41640A19DB7AB42D5D2E773B8FCE19AF69CBC16902544E + C0764C79F6BC829C6B9043E4DF6FC34FC8FDABF1F7B4D5DB07EA9B344E3F25D7 + 2B2FE531F724E3B804317C3F853F813B78B05ED8715D1287663B16AE9F49EE9E + 7B07E1DE7BE998E2F73121CCF9F6E4A704599D027DC9FC4FC1DFDB173951FB73 + D017C1FBB7604F00337A89DFBBE1BE817442D9753229694F0CCB64FE36B8BCDD + A0FC578206595CA4B6632683E517ACF544DC7973DC0161324F657A6F983B2F7A + A90D01A94E267985FCED1BF38E1B509E7E17FCE806FE2FC2DDDA95D2FF16ECCB + 4965FB4AB7C7ED7AE841FE77C1BE8C4D97DE872541FE52E97DB9C8F2EE262307 + C9887AA98D5F3CC82FBD3B714F3F7A19F611EF059713E0B80429CD2E8FEB2008 + 9297AD7CE9656BAD87262703697245E0BEFE825F5AAC1B93B5148A97AAEC65D8 + FB6796D4D7BBF5534AE717CBCFB4FC952629F2090AA3AF727DC9F65596D15BDE + 8E8496F2B98C9CECED0B12892BD5CFE11DE91497268972D29721C931312B59EC + 729CD3259D7B612B2FED93C2AE9C7992CE136AE1FE99EB23489356495E4C1C6D + 71E958E8CE93742ADFBB872FFFF5E94A1F1AE26F53AB4B21DEE777801BEFF7CB + 6E9EE943803E0EEE7160F73DA48F03D5C1FBECD2C78444E23271FBA013A70F85 + 9F220A85B5B548A68FE17E4E34D60307D1247E18B53B0CD63D7899E8E4B2EC06 + DA0B039C7DD19F3E3618DC3FDE13461CB38ED4E6836C354C6DF6A3324C1CF39E + BE3FDA6B987460EBDE21D407B6101EC8AEA25D9828F6C420774AF76457BFEC72 + 99895F770C76B362A00B13CAAE5407D74FA0B6184CF0F23EC312D1CCE0388309 + E87E8E6E82941EE8CC6EA6ADEDB055308349E581DC06B53554EC15CC7BF9920E + 74CCE06306E70F20B901AE548F1BEB6B25960589CCE431E9D69F2D916D243187 + 4C06B39BE8416C994C60A276085B28132432988955B7A060B83281CA96C14CA0 + D231876C752BDC2533A11A120A674A6712D6C1C7BAC7B11385C3BC5837EA07B5 + C524B4838FB7B0FC7509604BE000788404C02B3C04DE51E1F05544521841C74C + C852181529E016C6EE9B430401ED1DA3ECDDBF968F99C875614B586A977F1670 + F0F281932FEB6D258299EC655D194CFE325CA84FBC5FB233F5CF9DFA2588E070 + B6A40DB2F59BEA2230D92BC6C056EE36916CB5066622984961269A791F639FC8 + 28788545C03D98F4E53162423994EA0CF2132EA6D91AD99BAE67FF58EA83B04E + 0E8313D5E712CEEEB5791F271A3B82135B4E4786C33D92EA2279173A7664DD48 + 97A13E3E56229AE24C00B315F9503A5FECCEBB1F9F7BBA2E7B896A5FEBBEC6C3 + 28646298CF9338574C900753FD4191342E11C23299DD550FF10DC6D000EB7E52 + 830242041C421570088BC1E02026A5557089D412F4708ED0939E460C0B316070 + 483C5C55F148282B81B1A61AD1B9D95017E5425356086D4531529AAA91DBDD8A + CC8E0618AA8A11966A82B72E1E611633128A0A602CA98036BF04EA8C3C2853B3 + 106248160B43BC68E3AC50C1275E8FC0443342129311AA4F41B83185C224046A + 8DF02239D7A858B847AB29AE816F9C0EBE9A4404917C98D922C2009D11BEF13A + F8A81210A0D1235867422085FE6A2D0278CF62D2DD335A094F452C82B506AA9B + CA1ACC50245B106134937C221D1BA14C4947426E2E0CD437737901321A2A6028 + 2D409425098AD414A83299584E47745A1AE2B2B391909F0F5D6121128B8B61AE + AE80A5A11A96FA5A24D554C25C538E94FA2A4225F495C5882BCE417C4136F434 + 6649B565486FAE24B942C4E452DD1989544F32B29A8A503ABC1A65DD349EAD45 + B054A721AD2603C59D25689ADC88CE991DA8EC2947728519BAFC04180BB5482A + 49447A95093935C928AA4F476973260AEA2D48294C80294785E43C0D0A6AD351 + 4D751435E621A32C15E60213F4794624641BA1CA4E427C5E2AF4151974EE32A0 + 2B4F455A533E9AE77663D2C69918FFF0148C583506A3D68C238C41E7D24E742E + E9C088E5DD18BEAC0B2DF39BD13AB709EDF39AD132BD0E0D93AA5037A102B5E3 + AA5035BA12A523CA5132BC0235E3EAD1316B38BAE78E40C7CC368C5F3E0A7337 + 4FC5BC2D13316DDD084C59D389399B98889D8CE54F4CC4C2ED23B1647B17563F + D983154F7463D1D656ACDAD98DC70ECEC413AFCCC58A1DC3B160532B966DEFC6 + D26D23B0646B0F963D3606CB768CC5FC2DC331755D1326AFADC3F40D0D98FD28 + C93D3D0A4B090B767461E59EB158BF6F3256EE1A49ED3763DA9A0A4C5F5D8E25 + 24B7E6A991584B58F17817166C6CA2BC2A4C5C5186590F3762CDD313B0FDE505 + 78FAC472EC3EB102BB087BCFADC3E19BDBF1E2D58DD87A78361E7A6112B61E9D + 87A7CFAFC2DE9B1BF0CA078FE1C8473BF1D23BDB71E0CDEDC255F593A71FC2CE + 130FE1D90BDBF0FCC5C7B0F3D83A6CD9B7089B9E9D830DBBA663EDE363B07443 + 33A1019B9FEEC17387A7E3E5D717E3E8C5D53872F9211CBEFC305EBEBA01FB2F + AFC30B97D763FFB54DD8F7C6561CFA7007CEFC7A1F2EFFEE242EFFC7695CFD8F + 13845771F9B3C3B8F8D97E9CFDE51EC26E5CF8F4199CFBE4299CFE60075EFBE8 + 69BCFEF11E9CF9D55E9CF9741F4EFF6A3F4EFE6A1F8EBEBF87C203B8F0C79360 + B7D057BEB98E6B3FDC14EE962F7F7D551C5F6132F1EBF3143F8F4B5F9EC6A5DF + 1FC78D6F4FE3ED1FCEE38D6F5EC3A5DF1CC1C5CF8FE0F2AF5FC5A5CF8EE0FC2F + 0F52BB7B70E577CFE0CAEFF7E2DCA70770FEB39771F577C770ED772770FDABD3 + B8F11D5B35BF8A0B5FBC829B7F7A0DB7FE72156FFCE92DBCF5C3BB78FB7F7F88 + 1B3FBC8533BC97F2AF5FC185DF1FC3CD3F509BFF41757FB217177FB39FCA9FC2 + D56F2EE0DCEF2EE2F21FDFC6B5AF3F26DDDEC6D56FAFE1FA376771E177AFE0E2 + EF0EE3EA172749FE38CEFEEA156AFF24DEFDF6226EFD70096FFCE522DEF8F325 + DCF8D355C24DDCFCFE16E9F12EDEFCFE23DCFA2BBBB5FE95B0EC7DE37FB35BE9 + F77091C6E3CA77D771F3CF24FBC335EAF725BCFBFD557CFCD7EBF8F02F67F1D6 + 574770F5B70771E5B78771EDF72770E50FA771E58B33D4C74BB84EFD7AE32FEF + E3F277B770EE2BDE1B99F4FDF3FBB8FE978F70E5870FF0DA1FAEE1CC976F80F7 + 45BEF197B770E37BD2ED4F34DE5FBD86AB5F9EC51BDF5DC57B7F7D1B1FFCAFF7 + 71EBCFEFE01A13C67FBC81735FDCC079DE77F9DBB7681CAFE0FCEF5EA7B1780D + 97BF388EEB7F7C1937BF3C2AC6FBEAEFCEE0ADAFAFE0D69FAEE34DEAEF1BDFF3 + 397E03E769ACCE519F2EFCF92DBCF6A73771F2EB1B38F90DD5F9FDDBB848FA9E + FF815D47BF838BDF13BEBD850BDFBD4B721FE0B4C0FB84F7F0DAF71FE02CEF39 + 4C38FB3D97790FE7681CCF7DF73681CEE1D737F1DA57D770F20F17F18AB0923D + 81A3BF7E4DB8493EFEBBF378FDABAB38F5C7CB38FCF96B3844F9AFFEFE1C4E7D + 7519AF7D7315A7BF655C215CA6B40B38FE87D7A9CC099CF8ED711CA36B9D7F60 + 38F8EE73D8F7E6D338F8CEB338C2AED8DF7F0EC73F3E80639F1CC2C10F29FFC3 + 8338F8F151ECFFE055BCF0C151BCF8CB6338F8D909B157F04BBF3C82BDEFBF84 + 031F1EC2A14F5EC1A18F0FE3F02F0FE3F8AF5EA5FA8FE1E54F5EC64B1FBE4469 + 07055EF9E4259CFC35E57F7E50EC29FEC48547F0FC8DC7F002E1E9F31BB0FBE2 + 23C22D3CEF5FFEDCE5CDD87B790B5EB8B01107DFD889173F780E2F7D7690FAF8 + 32F67FFC229E7B7F2FF6BCBD077BDE7A06BBDEDC85E7DEDD43BAEDC34B9FEC27 + 3D9F2790FC877BB18FD2D915FFE3E7B760DBEB8FE0F10B5BF0CC1B4FE2999B4F + 63D78DA7F1CC5B7BA80FFBF0DC3BCF5BEB233CFDC6D378F2FA1378F2DA13D879 + E571ECB8F218611B76F01EEC97B7E189CBF42CBAB01D3B2F3D8E272EEDC49397 + 49F6CA93E0BDDB9FBCB10BBBDFDC4DF5EEC613D777E2312AFBD8E5EDD8727613 + 1E651D2E6DC596F39BB1E1D47AAC7B652596EC9D8FD94F4EC5923D73B0FED04A + 6C38BC1A1BE939B7F124E7AFC1EA432BB0F6E5E558F5D2322C7961016121561E + 5C86E52F2DC1D2FD0BB1EAD072AC3FFE30369FDD8A472F6EC786339BB0E6C43A + ACA1F28F9CDB0A2686D71CDF8255C7360B6BE015471EC1F2C3EBB1FC9587B182 + B0F4D03ACC7D6125663EBF14339E5D82C94FCFC7E4A71660FCE3F330FAD199C2 + DA76F2138B31E5A965E8DE3803AD6B26A1C586C61513D0B87C025A564D46DDD2 + B1289BD381AA05C3297D145A56D33B70FD448CD83815231E99222C75DB564F44 + FDE23128A7F75BD9CC11148E46F9ECD1A85E301E350BC7A16AFE18D42F9984DA + C513903BB90559E3EB9137A509A5B33B5032AB034533DA503CB35D20675203A1 + 1EF9D35A5138BD1D45D3DAA8BE4ED4CE1B819AB9C35131B31345939A913F81F2 + 27537CCA70E48EEF44EA882698DBEB60686577C23548EE6A42DAA84E648DED46 + DAC84EA40C6F259936648FA33233BA513E7F244A48D7C2A9DD28620BDF89DD24 + DB49F9DD48A772C68E1618DBDB90D445F1B656243637515A1BCCC33B61A050DF + DE2AC284D66668294C1AD503D3886E2490ACB6BD0386E1C3A1A7B2095487A1BB + 0B69E3C7217BCA54644C9888D4316390326A348C234652DE885E987A7A903276 + 246104924677C3D8D389F8B626A89B695E4975654E188FAC8913A89D1EC437B7 + 20AEA9199A9616A81B1B11535707654D3D62AAEBA1AEA7F4C636686A9BA1AEAC + 83B6BA11F115945E5E0775451DA28B2A1043D0D5B7404B320A8A2B0A4A096550 + 523C382D17FEC959629FDBB0CC22C41456212ABB047EC67478E92C08346722D0 + 94057F5336029272E0ABCFA0F96E9A08FD283D28390F3E864CB8255828CC4648 + 6A1102097EC905F032E6C02789EACD2A47A8A580CAA4C33DDE4CF36B03DCD526 + 78C499E1AE32C24599086765025C984015888353B48AD2D4622ECE708C510BEB + 59B696E51F2AD93532EFA9CBFBEBBA30994A736226645D29EE4469EC5AD999E2 + 6E1A2DDC695EEC164FE07842A2209579AF5EB61A6617CE41C624F86BF5F0A6F2 + 5E54C68764FDB58908A0B420BD1121346F0E4A34C18FE6E25E2A0D3C48470FD2 + D13396E6E9D496676C1C3C04014C3A464593EED1D407251C150A8A2BE0A58987 + 9F4E07AF782A27DC392710B4F06257D074EC16CB7D8D11C4317B21E27D74A5FD + 731DD91573A402C3C2C23194BEC786845AAD6A07D3771913BC4343E99B273C0A + 43284DEC8FEBCFAE93F947DD000CA6B843602886B107252ACB16BD4CF4F6F30A + 240410FCE938581CF35EB90C41CC72BEED580EC9331A83C960DE6357C83261EB + EE2730D02B88BEE5ACFBEF8A7C2E4BF9BC4F6F3F7796B3CACA5D2A4BE09FEEB9 + 6E76AF6C75E56C839B8DE0756577CB561258EC914B69728F6B0C8908B65AE392 + 1C13B6242BDC323398A4B5ADF130382E41E4F781BE64981066D7CD0CE1B699D7 + 8E8658D7A2A47D7A85473BDBDA94B47E233CD0D9D6B3241971CCEB5076EB5B92 + B5AB581BB3C9F0165EC2684102E711C4FA151B53D8CADD2BCACAEBBA73FD4C22 + 6EC5BA18A7C9641912B96BBF6626CAD941C8DAE4A550921784B68C009613BF0C + 7BC257CA93E4ACE072D6B262ABBC81D426B72BB56707793F24883C265D399FCA + F7A6F501CE93C68B8F79FD534A93CB4810E91432F9FB23F412C13250BA20DE65 + F851B99F02AF4FF7231DFA5359FBB5EE1FAD6913E4F9045EBBBE6DC044E32B83 + 48B3C97159FB7C01698DDB7E1DDB0639112CC78FB90602AFA1335FC3BC8D8C8F + F839B8A31E425F3277C3DF236FDFCEDF5356E001AA83C27B6DFDFC3990F8AA3B + D26424B0C481C9C33BF8B57BAD1084B02D4DE2C97AE332EEEC8EF4BE64FE2740 + DE8F9F033929FBEFC23F4500DB13BA7DC94AB097BD037D74FEBF00BD7EC66D27 + 47DAA8BAF72215E08B9B6EA87BF9A227C84DE5EFB1BB09FA228165275FA44944 + AFFDE6DE774BEF03FFD04DFE2FC0DDDA95D2EDF12357C376E57AF137FA2BC15A + 0FD7F9CFC0A60B939AF6905E127DE531642F1109827CE5B85D3BD20B4AFE6263 + F00BF18E9722CBD8E18E7AE490CAF04B9AEB62D85ED8BFE03A297EEF83F4E2E5 + 36A4972FFF39C7131C264919B6BFB47AFB20EFD7DDFA29A533FA208019D2A4C3 + 7EF221A5D9435E562E2BAF4F2EC7793C91918E05EC749164244875CAE372198E + 731B52DA1D93558298C8D23187525CCAEB4DE70935859C26D527AF9327A777D4 + D10778226A3FA1ED6B522BEAE1F668B27E1FBB661E46753BD304DE9526EE6EDE + F48141A00F1011DAC0AE86EEE7BF4749F6010A9988B5B7E215AE949D8609722E + 392F1F09160BA5D3C477C850FA2070C4FD84FB86D2F80EA109F150B6BA65F2D5 + 6A3D2C91C0FCF1D0CF91DAB411C0FC2170FF1076276D25801972025810A6C2DA + D646F64A2EA3D9DDB2AB95881DC0FBE87ACAF7DB6572D5D5062BD16A2581DDD0 + 9FAD936D0430D7C9613F4A137B0AD3479020A105294D72C39CA8CFCED496AB68 + AF371484AE1D014C713E662298094FC690DE7D867D30D4DB0F0E1EEC4A9BD25C + 48D6CDAA2F13BEFD9D5DA81D2B44BF781F61AE93CA0DF6A2F1F3A0FE52DD1221 + CCE4A213D53FCC93DAF1F0C6506E9309706A83F732E67D7187FAB0A56C80B086 + 75F0F627DDAC843493E42CCFFB150F16FB2DBB93CE1E24E309475F860F5CFCFD + E0161208CF88507847F23EB661700D62CBDF60B887878B3D7B9D4382E1C8242C + 5D0B6E911170A334C7D0100CF5F7A7BE934EA4FF306F5F0CB38D0513C14C02BB + 30E949328EECDADAC74790BE12F9CB690C268119AE543F5B420FF1F602EF3B2C + 0860B6B86559CA67D297495DD6CF8DF707B611C24C1873C8696C752CC2300E99 + 00A63221342E81A45320F5332C047E310A04C5C52050AD84B72282FA170A8FA8 + 709B3575005C4249970876771D069FD848F8C72985AB6D2F9265F2D6C187F4F6 + A3BE31D94E3A3952DB4C76B3BE1229CDA144628B7E08FDADE43593BF4C8CBBB0 + D5B00FF5C19FEA09A4B1F4E13FCB7DD0CFCB1703E93C0E66D767C1A1D6FDAC78 + 61292A0ECE9109F088310977CF5EAA54B84713A23228CD026F9D168A9C544465 + A709ABDBF8A26CA4345621ADA50699EDB52819D38ED2311D48AE2D4668920EEE + B10A78B035B94E8710AD4990B75ED1F1D45F3D023446B1AFB0D8132B5421F6C7 + F28A49809FCA80E0842484E892840CC39BD2D92DB5235B278728681C557057A8 + 45BA7F9C81C62E115E4A355D334CF0C6C037364E10BE6E912C1F01A7D028CAE3 + FD8E1502FE2A1DC2F5662892D204E1CBC4AF5F3CE9A5D120362D03A6E262188B + 7290906321A4203AC58450BD969048302084F75336507A72BA281F65B620DC64 + 419425153119A9822056A45BA04C4F416C461A62B232119E9E8ED0548B75EFE3 + BC5CE88A8B60AC2C83B6381FCAAC6444A4E8106A54233A5587C47C0B0C451668 + F2CC88CFD6933E29C8692840DDA87A348F6F41466D16D499718823687334D011 + 0C790930E569909CAF434A41228CD971D058A211971485587324D449D1D06769 + 61A0FA34A95AC42469106DD620C662803A3B03318C5CEA437632C23328AD2085 + CE6919AAA6B4A27C52038AC7D5A07A7A0B5AE777A1794E0BEAA7D7A36E5A1DCA + C797935C26CC352618ABF4B0549B0849305798602A4B86A9D40273693ACC6599 + 482ACB80A5321BE93539C8AECB41717B21EAC755A07912B533221B15DD996818 + 5F80D6A925689A948FFAF199E89A9987196BEB317D6D1D462D28C4E8790598BE + AA0673D73561DCA2120C9F9183894BAA307D653346CD2D177B1AD78E4D43C548 + 332A4799D034251DDDF3F3317A29EF935C830954CFC487EA307F7B1756ED1E87 + 85DBDA3179552546CDCBC1E8B9D998BAA29C5081710B0BE8380F2366E6509D69 + A81B67C1F0D94598BEA6158BB78DC59A5DD3B1E6E9E958BE7322D63D3B033B5F + 5D81EDAF2CC6B2A7C660C18E6E6C7A79369EBDFC109EBFFA105EB8B10EFB6F6E + C0D3E7560AABE4875F9A8D453BC762D6C66E2C7C6C02963E398DF4188B39EBBB + 306531F5737A01BA27A6A167423266CCCFC3A6C7DBF1CC8109D8BD7F3CB6EEEE + C6FA273AB1FEE91E6CD93789DA9C85274E2EC433E757E299CB6BB0E7FA5A1C7E + F7519C78F7799CFBF0086EFEE604DEF8CD515CFDFC455CFEF479BCFEC1E338F5 + EE569CFFE54E5CF97C172EFEEA699CFB64174E7FF4044E7EF8144E7FFC0C4E7D + B217677FFD124EFD72AF208279CFE18B5F9FC3D5EFAEE3C60FD7719DC24B5F5F + C2D93F9CC1E9DF1CC789FF388CD77F7704677F7718E7A8DC953FBE8CEB5F1FC1 + F52F98D83D842BBF3E884B9FBC88D7DF7F0E47AEEFC0E1EBEB49BFF538FFC9E3 + D4F6B338FBCB1748E6155CFAF430CE929E977E7F0857BF7C0557BEA23ABE3D81 + B77EB888B77F780BEFFDE73B78EF7FBD8337FF740317FE7012A7FF633FCEFFFA + 00DEF8ED2B78937438FDCE4EBC726B03CEFEC7B3B8F9DD055CFBFA062EFEFE2D + AAEF3D5CFEE216AE7D731937BE3D856B7F3C8C1B5F1FC5AD6F4EE18D2F4EE2CA + 6F5EC5D5DFBE8A37BF3C8D37BE7B0DD77F3845FDA4F04FE770ED5BB64ABE4AE5 + DEC05BDFBF8FB7FFF313BCF59FBFC4CDBF7C801B7FE5FD7CDFC4F9AF2EE3DA9F + AEE0CDBF5C25994B78E3EBB378F3EB33F8F087F3F8E4AFA770EB8BFD38F7F153 + 384B637BE5D78770EDF74771F977C704817BE3DBABD4C6355CFCEA2AD5730397 + BE794B58F5BEF9BF98F0FD90D26EE2D277B770F5CF6C317C1D37FF7486F4A7F3 + F1DBC3B8FE47AAFBBB8BB8F5ED45BCF3A76B78F7CF6FE28D6F49FE8F57085771 + F59B9BB8FAED0D9CFFFD591A133A3F9FBD84739FBE809BBFDF8F9B7F7891AE89 + 83E2E7805B5F9DC7FB3F5CC1DBA4FBADEFAFE1C69F795FE70B38FBCD455CF8E1 + 265EA7764F918EC7FE781927BEB86C257EFFFC0ECEFDE9162E7C7B0B970867BF + 791BAFD1F89CA6713949C7C7BEB881E35FDEC06BDFBD8DF37F666B62267DDFC6 + EB6C994C7D3A4BE37996CECFF1DF9EC72B9F9E241CC7D1CF4FE1D5FF207C7E12 + 473E3D8123BF3A8E573E3B89577F7B0627BFB8406D5FC4E9AFAEE0ECF7AC13E9 + F3E5791CF9E20CE9751627FF7806A7FE700A277F7B14277FF30ACED0189FF8F4 + 00F6BDB1032F5CDB8A836FD27577EB09BCF2F62E1C7EE7391C7C672F0E7F7410 + C73E3B416D9CC2818F8F62DF7B87B0EFFD83D8FFFE8BD8FBF60B78FEEDE729ED + 05ECBDF51CF65C7B1ACFDFD8837D379FC7DE379EC35EAAE305C28177A9AE0F5E + C02B1FEFC7316AEFE82724FFC6E378EEC656BCFCDE5338486DEE3EBF017B2E6D + C08B6F6CC3A1B71EC7A1371FC7816B5BF0EC99B578EEC22382307EEEFD67B19F + EA7889EE9303141EF8F005D2632F9E7D7317765EDA8E27AE6CC3F3B79EA6E327 + B0E7E64EECA3F89EEB8F63E7B94DD87A6A1DD61E5C8415FBE761F38987F0E4C5 + ED78E6E693D87D7D277611765EDE8EC7CE3F8A27AE3E8EC72E6C15D8C1C4ED99 + 4D58776C15561F5E8AB5479663C3F135620FF7478EADC59653EBF1D899CDD87E + 762BB6BEFE28B69FDB8E27AF3E2908E9A7DE780A8F5F7D0C5B2F3E8ACDE737E3 + 91338F6013E9C1E4EFA6D71EC17A2ABFFAE052CCDC3919A3D60FC7948DA3B1F4 + E99958FDFC7CACDEBF10AB0F2CC2DA971663D5FE0558F4CC74CC796232E690EC + CCC727621A3D0FA76E1F8F69149FBB7B2696BEB80C6B8E52FF8EADC78A23ABB1 + F4F00A2C7F7515D69E7A04EB4E3D8AD5C73663F92B8F60F9A1F558B47F35E63C + BF0473F62EC1A2975663F9CB0F63D1813598F9CC224C7D7A1E665038FDE9C582 + F86D5BCB56B8A3289C809E8D33D0B67A126A168D42D5C231A85F3689C0C4EF04 + D42C1E8F92595DC89FD2829299ADA898D781F2B9ADA85AD081CAF9ED289AD988 + FC69F5289CD28CBC892DC8A5F773E1E4E1289E3A12051347503A13AC743C7D04 + 951D8BAAF9E3C4961099E31A5038BD0DD50B47A376D11894CFE946F10CAE7B04 + 8A28CC9AD080EC094DC89BDC46E5A99DC9F41E9ED186FA7923503FB71B6553DA + 9037B619F9E35A5134B10B05E387237B6407D2BA9A61E920B4B720A9AD05260E + 3B99C46D15616A779720840BA6501D7346A162EE18144D63F2773832C77622AD + A70DC95D2D30B43620BEB69A500B7D7333744D8D622F5A7575752FE2183514AF + A9455C7D23A109B1D5B55054542286C2F8A6662434B74055D7801892D1B6B6C2 + 3C7C04F41D9D22AE6D69A17C0E5BA16B6DA3781B85ED30918EA6EE4E18BA3A90 + D8D98AB8A67A286BAAA0211D2C237B903E66348C9DC311D7D000456525A22A2A + 0422CB2B11565886C0EC2204E6142134BF0C61796508CF291304AE32B702E199 + A508C92842404A0E82527310994B3259746CA1784E31E2CB6A115B5C8DC0D43C + 781AD2E0939405FFE45CF825E50862D74B6B818726193EBA54423ABC13D3E06B + CC80A72E051EDA647853195F6326422C850834E7C15B9F093F0A43524B109256 + 0AFF9422782666C14D9B011F03D5ABCF8677423ADC546641FEBAC71AE1166380 + 6BB40E2E51097052B05BE3383845C6D1B11ACEC22B8FD52296BDE70CA3F92E83 + 8F1DA363312492E6D36C0D6B4B638B5C2682990066C2D85A265680C962B6B465 + E2D82596618DB3F5B0972611DE54D69364DCD81D7324CD9DA314F43DA0848732 + 96E6EA2AF8A8789EADA16F987831FFF6A46F084F0ABD62E2E04D690C9F58AE83 + EAA4329E2AFED1331E1EB1B170532A45DC4F9B407D8EA13EC7C0338E64D5241B + ABA6EF91183853BB4CFC3281EC4EBAB9B39ED4066F4BC3DF0DC3E8BB8531343C + 5210BE4343C3E1101C4EDF0A1160AB5EA7B04811770C8EC010FA161A48DFB003 + E8DB75207D130DF6E31F61D9B5335BFC86082BDF073D783F5D3F3C48DF475622 + 38D04ACCDA885A89B4E5B407C516587E5612D79D7F82A77A6D2430C7792B2C89 + A8EDEF4AEDBADDF69026D09BEF233CA64910E508BDC75C37951BC86D0902977F + 9C97818E2572B7BF0BB54590C7B98C389611C1BC97AE64B9CB7139812B11C0E2 + D8D9065B9E5C5E90BB8E8C3BD3EE207F1D08B6352541F432E16B237A197C2C7E + E277F4126B56D25A1AAF37B1173A69CD4B8248E7B5282E2F8B8BEDD0A4B82DCF + BE0CD72B877DBD5C07C7E56B5F525C2EDB9BC6F5525B7DAD9749E0B53509627D + CEAECE7BD96A5710BBB7C95D39F9CBEB58B7D31D4528C85E99C5F02F06501A8D + 9B58B3B4837D9F19F27542795AAFFB6561996CD39720AD714AC7F232F2B54F3E + E690C79F5D504B79425646E4DE41EC4AC4AF0459BE1C52D95ED8641977C8C8EB + B0AD75DF75DD5B5AC7A6F8CF415FA4B03D84A192FD1A761F9048E03BBC62DAE1 + DEFBADE82539992CED43EEE7A22F5EE3BF1BBDFA897EDAC3DA6F7B487CD51DE9 + BDDC171B53CA38301BF725414ABB037282D49E2C9538347B99FF6990F7E9E740 + 4ECAFE2B21B712FE3904F08FF3065A61272B9793A357A64FF29707E61F189CBB + E1670E385F88BC39B544F84A6430C79908BE8FD2ADF9F470B89F1E0E1209CCA4 + 6F2FF9CB37827433701D7D10C07660991F91BDB21BEA7F22012CCFBB1BFA2AF3 + B3201B8BBE1EF6FF18481F39A9F973217F814816A81CB7BDD07AEBE76339284D + 8A4B2F42F95F4CBD729C4F90FE7EBAE36F278E7339E9852BBD506D2F5A76B372 + 2FBF807F41D71941B85561B72B3C71107FB8911C4F1C6C130C795F04292CFAD1 + 473FED6147BA4A131051B76DA2D117A4F6E590D7612F27C525199ED4F00451CA + 97E74990EA90C33AC1FB719A3C5D6A4F4A67705B9285AF9898DA26B40C29DF1E + 529D527D724865A489B834F99626AED264559A984A69F23C51079561125810C0 + 84FB1C094E5417C391F29C486726765DBDE963C7070328ECC72E9E0956C2973E + 006CE42F87D6387D04383B0BB7C56E4181700D081484255BAFF21EB7920530BB + 81FE054D80D915743FFA1061127880337DD4F0DEBD4CFC0A509C2D81E943E436 + E96A051F4B96BF82B4253051DBDF89EB6332D74AFC0E72650295D29904EEB5F8 + 75A57C975E4824707F47B632B6E281A1CE22648B600E6FFFBD691D3B26A385DC + 3027FA687316EE99D965B31CC282970954267E3DBC095E024305E1E92B483F41 + FC09CBD00038FA06C2D1CB5FB8AA1EE2CED6CA0C26623D493FD29F8965AED746 + 003371CA642793C75602D84360A007B5C9A0BE0E7275136EA7B99EA19E3E1844 + 6506787A09F9811432E9C856A75C87284BE76D08E531612DCA1098B81EEA4969 + DEAC3BF7C54A063BF9F980F71D666B5DB638662B57B66E15442C1FFBB2AB633A + A6B85348107DB407D147BB2F067A5B8966891C77B491C0A22DA1A73786507B7C + 0D59DB22191BF1CB243093C21239CC79EC0E7B880FE913E80B765FCDC43093C2 + C3A87D479267FD7CD82A372404EC269BD3878ABE78D9F283E011160A4F4100D3 + 354B700E0E20BDA90D76A94DF9DE91E1F08F5108F846470A1298C95D2F8A7B2A + C2E1111E02F78810410AFBA9A211A28B4728C15BC97BFB06090B6BB7A0106121 + ED49BA7884870BE23C345E0385DE80C0589595F4A573E11CC856DB81745D503F + 9814A6F17325DDDD42C2E014C004B0BF701FCD04F0601ABB7E74BE06535997C8 + 4878A9621190A081AF4603EF8404F8698DF08E37C28710C004ACD602FF843404 + 6AB31169CA4354AA0521661D3CD52AB8C746233A3505796D8D281ED98EECF67A + E4B4D521BDB112CACC2478A814181A4CF733EB184663106D2568DD2362693CE2 + E1139300AF580D5C78312A5C6125A129DF955DC1C7C6C3579D001F15E9A64A10 + 56BC5E54DE95177D82C3C45FFBAEE14CEA32A11B856141FC37BF15ECFADA2D22 + 92D223E140C783F987054EA7321E0AB62C8FA731A7FE2AD5220CD626D2F82722 + 50A3857F5C3CC20D26282D298834EB093A28920C50262743939989F8AC2CCA37 + 23204E4FE54C08A2F1F225DD3C987C26FDDC95BC00160377B656888EA6E36878 + D2B9F249A0BAE9BC05994C88B058109D66B52056A459109E6C4288518B10433C + 81CE6F8A1E719966A8D20D884ED620D2A88232291EBA6C33D2CAB2905E950B4D + 8E19CAF4040A0D301625C354980C73A119A9254948C9374197AE415C720C4189 + F89458E8D2E2A1CF48B0A65BE2A134A9104DF5C624C72326D508852595F44841 + 58921961EC76DB4C634288C9A27E17A743554061491AD25A4AE95CD7A3A8A71A + 391DA5C86C2E44724D16128A4C884E8F434852242252A31195164BFD5442911E + 0F35E918979D84D82C3362D2682C53F588CF219D4B3260284E464A652AB21B32 + 905163414A8511E6522DA5C7C15CA6456E93050DE34B316A5E2BBA6636A27952 + 399A2694A1A6271725AD69A8ECCE45716B06AA4614A2716C0D72EBB3A0CD8E87 + 2A8DFA9D190B7309D5D19886B2EE3C548D2944DDA452344C2D43EBCC4A8C58D8 + 80D18B9BD13DB70A23E75663F4EC2A0C9F5A8A2E1BDA2615A27D62219AC6E6A1 + B0350959F57A1477A4A16A643E1A275562CCE26E8C5FD68D86C9A56824F9A9EB + 7A30F7D1F198FC5007663F3A12DB8EAEC0AED7D761FDFE9958FDDC646C38301B + CB9E1A8FA9EBDB30655D1B26AC6C46CF821AD2A30E63963461CCFC4AF4CC2C40 + E7E40CB48D37A3637C22C64C4BC2BC1585D8F0582B763C3B1ADB76F760E5D606 + CC79A8024BB6B562D3FEC9D8F9EA023C7B6E35F65FDB8867CEAFC5E3279762F7 + D955D87B661D0E5FDD8A53EF3C8EC3D71FC6BE0B2BF0F2F575387E6B035E7D73 + BD088FBFF5085EB9B91E2FDFD880433737E2F09B9BF1EA3B5B71ECC31D38C684 + F0C74FE2B5CFF7E0ECEF5FC2C53F1EC7852F4EE1FC1F28FCC3099CFBCD519CFA + F4251CFB642F8E7CB40B273EDD8D33BFDE83339F3D8DF39F3D894BBF7E02D77E + FF34DEFEF259BCF1DBA771E6BD4D387C6505F69F5988831716E3F88D55384D3A + 1CB9B6012F5D588F136F3E8E63371FC32BD48FF31F3E81377FFF02612F6EFEEE + 79AAE3103EF8E624E1383EFCFA287EF9ED2BF8F0DB1729EF495CFC680BDEF8E4 + 497CFC87FDB8456D9E7E77054E7FB01E6F7D7B18BFFC7FAEE3FD3FBF819B5FB1 + 55EE65BCF5A7D3141EC49BDFEEC7BB7FA63AFF7C04EF7D7B04377EFF22CE7FFA + 0CE9BB0FB7BE3B8A37BE7B1537A99D1B5F1DA7B2AFE38DAFCF51FA157CF097B7 + F1C17FBE8777FFFA1EDEF94F76457D0B6FFC701DD7BE6517D4C771FEF3435417 + D77912EF7E75026FD3B85DFFEC71EADB4A1CBAB010AFBFBB11EF7EB91F1F7CF7 + 326EFDF120DEF9E618DEFFE175BCFBC359BCF3C305D2EF02DEA476DEFFCF5BF8 + 041FE2C3FFFD1EB5F126DEFAF39BD4C675DCF8FA22DEF9FE2C6E7E710C97A8AD + ABBF398CB7BE3E8E5B5F1FC3BBDF519BDF9F227D5FC5953F1C1638FFBB9770F4 + C35D78E98D6D38FEEEE338F3F14E9CFBF871BCFEC156BA16D6E2E0953538FEF6 + 365CF8FC791AAF6378FBCF6770F3BB73B8F8E56B38FFC5595CF9EE1AAE531FAF + FCE52D5CF9EBBB38F7ED9B38FEDB4B38F9874B78FDEBCB78FDAB4BB8F4ED555C + F9D3759CFDE60A4E7F7315677EB88933DFDCC0C9DF5FC1A92F2EE3FC9FDEA0F2 + EFE03C13BEEC8EFA0F97F1DA1F2FE302953947751CFEE551EC7B7B1F0E7FCCFB + 541F17FB63BFFAE9613A3E847D6FEEC5BE5BFB70FCF39338F7E5059CF9E3799C + A136CF7E7B05AF7D7511A7BE3C87935F32F9FB3A4E7F711A67BE388133BF3B82 + 33BF6537E62FE2E0AD1D78ECF8326C3B3C1F7B2FACC6A1EB1BB0EFF27A3C7771 + 3DF65ED9827D3776E0C09BBBF0E2BBCFE3E00707F0D2FBFBB0EFAD67F0DCCDA7 + F0C2AD3D78F1FD17F03C858F9EDE8885CFCCC59C2766E1A197D662E7B927F1FC + 9BFBB0F7AD7DD8736D0FF6938E873F3848FA3E8FDD979FC4E34C98BEB6098F51 + B9478FAEC586979663DBF187F0ECA5ADD877ED713C7F693B76BDB611DB8FACC2 + 8EE3EBB0E3CC166C3FF728765CDC8EA7AE3269FB049E273DF6BDF33CF6BEF90C + 9EB8B01D9B486E135BCFBEBA069B8EAEC1C32FAFC49A034BB0F6C5A558F9C202 + 2CDA3D0B0B092B5E58848D47D663CBF147B0EED04A2CDFB748601961E54BCBB0 + 6CFF620A9762DD9135587B780D96BCB018F376CDC2AC27A661D6CEE998F9F834 + EAE74CCC7B8AEB9B8725CF2DC1F2FDABB0EE950DD874EA516C3ABD19EB4F509C + 75A67E6CBBB8155B2F3C8A8DAF3F824DAF6FA4FC47B0E535C2A947B070CF5C8C + DD3012A3567760CCAA4E8C7B6838C66F188E198F8FC59A9716501F9692DED4E6 + 6313307BC724CC7F7A3A963EBF004B9F5B44BACCC4B4C76660D6930B30F38985 + 98F2D81C4CD8360B931E9F8B694F2DC2CCDD4BADD8B50C339F5A8ED9BB5662CE + EEE5226FEAD30B30E7B9E598B77715A63CB910A3B74EC7A427E661F18BEBB1FC + D066CCD9B316631F9D8FA6E54CBCB6A36601BB581E85CA79235032CB4A8C56CE + 1B8B8AF963503ABB072533BB514E61C59C11289BD545C76D289DD58AE269CDC8 + 9B548FDC09B5C89FD084FCF1CDC819DB849C312DC81AD90C734B2D4CADD548EA + AC45D6E816944C1D81A6C553D0BD762E9A964C42E5EC91685E3A09C31F9A85C6 + 2513513D771486AF9D25503663383247D7217F622B6AE78E41DD9CD1A89AD985 + EAE99DA89CDA81E2F1ADC81DD580AC11F5C8EC6E40467733D2BB9A60696F44C6 + 709A8F8DE8A2E30E24B7B5C0DCDA80A48E4658BA5A9136BC1319233B9135A615 + D96399B0EE40C6A836A48D68417A4F3B923B9BA06FAA23BD9BA1AB6F404C5905 + E2AB6B443CAEAA1A2A2677CB2B44BA8AC2D8B27228291E535185680AC38B8A10 + 565828DC29ABA95C5C4D9DC80F2F2C466471A9707BCC21CB4510228B8BA12829 + 21993221A7A4FA95D59588AE2E475439C9969520B4200F01D99914E623BEA606 + 894D4D30B650BFDA3AA06B684234D5199A5788E09C0204A4E7C1D79205BFD40C + 0465E62230230F3EA62C0426E708F818D2E0AE4B862BCDDDDC134D746C81973E + 19BE260B42D37310955388D08C5CF89AD3E14579BEE64CF82767C3233105CE6A + 031C63D96AD6005FBD05814999F0A174EFC4647869799FDC5484A6E622C0CC96 + C1EC1E3903BEC67404A5E422229DFA99457DCAA940A8A5107EA66C78EAD2E0A6 + 32C135560F97D8440AD9E2570BE7680D9C22D5342F8EC5D030151C0462E85849 + 61341CC209513182E81D2A0850858D10556208C50753DA609A1773BA951C56C3 + C9663DECC8E5287D485894A887F3993866CB5A86A3825D35733C06CE6C654B73 + F7A16C554B73688790700C63725510ACFC6365249CF9C74AD2C38DE40568EEEB + 19A3A6B97D1CBC555612D83B86AD81D5941E43E92A012691F987CB207D22DC63 + 62C55EBE2EDC6654B48003B74BEDB095AF1BC9BAC7A844E8C2FA31014CFA0FA5 + F6F9DBC131228AF464440A9DF8A74F87A03001B6EE1D16C8EE9D8304F9DBDFCB + 0F03BC02E8BB32108304111C84C1F42DCD96BC0FB8F9E201FA967EC093420FB6 + DCF5473F5789B0B55AE2B27B664EB3E236512B91B756F297EAE01FDC5D6E93B3 + D6BD706F43A4DB13AD9CC79ED1081CBF7DCC75F0B197B0D8B507A7B39C44F20A + 02575E274110C1B63CDED64B6C05C624304172BFCCDB853D48F5DD86B5AC3C5F + 6C09662B2FB970B6BA6DB6E2DEA16E02C2F2D7065E6BE2752CE18DCEC143AC8B + F05A15AF4B71BA586BB2AD5D49EB5DF2B52C698D8B4386E4FD4E3A96F2398DC1 + 71A92CC7A563292ECF97EA13D6C8BCE6C6E5294DBE1EC6E85D3FB3413A16B0C9 + 48EB6512B87EFB786F7D1CDAD2ADEB45B7C3FB87705F39E43496B99DCF712B29 + 6C5DBF14E3641B2BA92D396EB7616D4F5AB394CA8AF282F8B5E23E3B2B607B59 + 39A43C499E431E1BAEE78E7254BF7CCDB897B0ED0352BE1C77C8DC852C96CBF0 + B16405CCA13C2E5FD3BE8FD3382E5F43FF277007012CADD3CB215BE396F889BB + 4122807B89E03E64FE1EDCC16DFC1F07D2F14704B03D1C0424EEAB6F02983DEB + DA38AF7BD95054E65DF72EBCD81DBC9D04FBBCBBC9FD4F81D49F9F8BBEC8D47F + 35FE1E0B60B98C405F69F6E0B2F731EE46FEF6D1F17F05FEC6A0DF7BFF503C40 + 90AC7E99F0E550C2FD9CFF8003EEE39BA297E825D8DC3E5BE31CDA6033659743 + DEDEED34A93C97B1413A96A7F77983FEF7E26E0F31F903EEA7605F4E2ADB57BA + 3DAC75FCF861FFB7D157399B3E12E1F9F7407AB1F0CB832D69F985C2C754EF1D + B0BDE0EE0A3B397E118A9723C57F4400DBF2A497A7148A388724F3C040370C72 + F4C1A06134C9ED4F139B4134416257D0F492E63FD324E257B200EE0BF2BE49FD + FB910CD5F523F044C336C9904F72EC8FA53486BCBC24274F97CB71C875C8EBB5 + CF9797B91B245DEE5617A78989934D8E27B1FC77A434A1E53C0EA589AFBC2E09 + 529DF675F3B134E914134F69D2CAEDD171EF44512623A1777C299FE57BAD8087 + 910EC3A80E391C494F1B11CCA4EF001BF12B8512E9CB90AC811F74A4BE0E6362 + 771885C3D0DFC95980DD3FB3F52F13A6720298AD80B95C6FBD36F2B7D70A9842 + C9CD3313BF4CBC32A4C9B4354ED7AC8DB07D80AD8A6DF1FEBC87AF33D5CDE42E + 93B26C3D6B0B25BDFAB15B65016B39095C5EAA97C1719EAC4B7F748AB687B1EE + 56C25910DFBDED59897026A4D955F440772F0CA68F54B6B86538B08B5F02EFF3 + CA215BE03A058408AB4E674A1B461FB543E98355C260FA806522BBD79A99FAC0 + 84B59558F6C220DE9B982D886D8432BB826602B8BF8DE8661258B89E66591B51 + DC9F8E07B2A53013AA81BCEF2D7D447B521AE5F33EC76C8D2B086609542F13AE + 4C9A72DD4CD0F23193AD6C9DCB842C5B1E0F7077EFB542E6787F373711678B5C + 26338778931CE9C716BF6CFDCBED3058374170DBCE15FF4430C0D595FAE721DA + 64A257227CE5C71CB7EAE205B7207F7886068B1F0F78CF62677F5F38FBF90897 + D53E11A1843091CFC4F5600F1A134F77117709F013321E2141024C023B0551FF + 03A83D3F6F3806F8C22598F2793FE348AA83EAE29049605F65247CA2D9E535A5 + 731EC12F261AC1F12A04C7A928DD6A01CCD6BB1E61E1741C45E5145457043C09 + 91BA442466662142AB13E7508CBFED5C72C8632659557B4729E0AF8C11F05144 + 0B2B6BE1629AFAEB111D85E0C40428D35210979581284B32020C7A049B921061 + E17D78D311624A45A82903AACC72184B9B602CAE434C5A060212E2E0A954C029 + 8CC6282606898539B0D496425F940B55960591297AF8C42BE1181E8C81BEA423 + 8DBD73681802551A04C46A1118A343B0DA007F8A7B30611A43FD66AB8168155C + C2A3E018CAAEB123E14AFD768F8E861BC57D482648A3833FEF2946FD700A0981 + 2BF55110BD5437F78B2DC587F2B54563E718CC6430EFD11520FEFAE7B87B5434 + 02E31310966810F549326E1151E298AD863DD80A393A06016A35C2F409884935 + 1192A04C49863A2D0DAAD43484B3BBE93803FC5409F050C492CE56AB6487A048 + 0C0BE6C5270EC3AD602B84F04892A37A79314CA34580DE8860BD89C65F8F403A + 8FFEF171F051A9E017174B69F1884E6137D446841BB4084E605D220582E21408 + A3E3F044BA56E81C841912A04A3721313795608121D78CD47C0B4CD926A84C2A + 28F5D1509963A1CFD021B7321B55AD15286DAE4046653E3419264102C7985588 + 36B335339DEBC414046BCD08D299E81C27C29BC6CA272101BE891A786BD50830 + 6A119D6D813A3F03CAEC144431419DA6A7F3AD4388310E015A257CE2C2E1A909 + 2504535F431090A8A06B494348A43E6B11941887506302A22D26C46624212A25 + 9EAE410DE27312E95883C82425A29295884E8F85A1C48C92AE0AB44CEA40FDA8 + 665476D6A2BCA312C5ADA5482B4F833A550D6D065B349B90529441C8453C93E9 + 348E812A1A27D2594DBA19F32C3017B3F5712652ABD390D1908EE2E145A81957 + 85F29E6214B7E7A0BC330F951D79286ACA407E5D2A8A9B3251D696832A922B6D + CB87A5DC8CC4BC04E8F375043D92CB2C286A2F41E9F012245719612AD7A27078 + 0E6A2757A0715A25BA17D663FEB60958BD7B16E66C1D85C90FB560DCCA3A74CE + 2B41FBCC228C5E508BF18BEAD13D8BAD9F8B503F261B753D49681963C4C819A9 + 98BC381773561560E9C3A558B3A506EBB63662D3CE0E3CB2A3030BD65762CAB2 + 22CCDFD880879E1D8B0D07A662CBA179D8FECA126CD83F0B2B9E998CB5CF4DC1 + BA67C760E3BE1E6C3CD083C53B6A316D7D2E166D2FC7B683A3F0DC6B33B1FBC4 + 746C7A611496ED68C6D29D6DD8B06F021E7F750E9E3EBD18BBCE2DC6F66333B1 + E7D2121CFF742B5EFFF53378EDD3FD8403847D38F3D97E9CFEE439BCFADE1378 + F9D6A378E1EA6A1CB8B112273FDA848B9F6FC585CF37E1C267EBF0FA47CB71F9 + D3B5B8FAABB538FDD67C1C38330EBB8F8EC08BAFCFC0EB6FACC511AA7FFBDE51 + 58B7A3130F3FD183955B5AB06C5323761E988493D7D7E0D27B9B70FDA3AD78EB + 93EDB8F1C1169CBDB106A72E2DA570092EDD5A820B6FCDC5C98B53F1FAB54578 + F73FB6E1E33F6CC3E58F17E3E4DBF371998EDFFBFE283EF8F379BCFDDD39BCF3 + FD69BCF1F58B38FBF96338F7EBADB8F9F51378E7BB3D78F3CB677186EA7FF5ED + F5B8FA9B27F1E1F72FE3CD3F1EC0855FEDC6B98FF7E0FA6F5EC6DB5F9DC6FBDF + 5FC4073F5CA33AAF0AF7CEEFFEF93ADEFBEB35BCFFD72B78FB873338FFF97EBC + 7283EAB9F128AE7CBC1BB7E8F8DA873BF0F2851978F2502BF69E18832B1F3D84 + 8FBFD98D4FFEB4171F7DB317EF7FF502DEFBEA003EFAFE55FCF2AFA7F1D69747 + 71E9D7EC2AFA18AEFEFE24AEFFF175BCF3E7AB846BB8FED5055CFBFD2961A5FC + F6D7A749AFC338FDEE2E9C7C7787B02EBEFAF96EDCFCDD3384DDB8F2BB5DB8F8 + EB2770E2834D78FAB53978F8851E3CFAD218ECBF301F072F2FC4B67DEC32BD1C + 1B5FE8C0812B4B71FCFD8D38F6FEA338F1D19338F5AB67F0CAC7CFE3D8AF0EE1 + D297EC0AFB6D5CFBE116AEFFE55D81D7BFB886A39FBD8693BF7B1DAFFFF10CCE + 7D7D16E7BF3D830B5F539CF774FEF60AC5AFE03CEFC14CE1A5EFAE12AE897DA3 + 2F7E7B1D17BF614B67CAFBE612CE7C7906C73F3F82573F3E88139F1EC689CF0E + E1E82FF7E1D4E787E8F8100EDC7A167B6FECC2A1F7F6E3F8674784DBEF0B5F5F + A076CFE1D4EF5FA73ACEE1F2F79770EECBD770EA374770EAD78770E6B72FE3F5 + CF0FE0F0AD9D78E2C472AC7D7612D63F3F11CF9E5E80972F2FC3BEF34BB19BE2 + DB0ECFC043CF4FC672BA5F1E7E7101769C5A87DDE737E3990B5BB0E7F256BCF8 + F62EEC7B73171E39BA0633774C41FB9266544F2F43F78A2E2CDABD141B0E6FC1 + 9657B762C3CB1BB1EDC463D879E649AC3BB81EF39E5C80E9DB6662EAE62918BF + 7E0C462C6FC3F0A5AD18BFB60773B64DC1C227A611A660C16393316FEB242C79 + 7216963E33178BF6CCC6E23DF3B0F0D9F958F8FC7CAC39B41C5B4E6CC0B6D39B + F1E88947B0EAC0722CD83D07CB9F5B88A5BBE762C6A313316DF358CCDE3E4984 + B3B64D224CC1D44DE3443873FB544CD93C0E131E198D298F8EC734921BBF690C + C66D1C45E1284CD83C0653A8CC8C1D4CFCCEC08CC7A661FC8671E85AD6899685 + 2D685BD88EAEE5DD18B56E2CC66D9E8CE93BE760CEAEF998B66326266E9F4C3A + 2EC6C6D39BF0E8591A87D73761C3F17578E4F87AAC3FBA166B8FACC643875763 + 16D5396245075AE7D7A1755E239AE7D6A16E6601DA169560EA964E2CDE35192B + 9F9B8D55D4DFE57BE6507C3E1E7A710596EE5982891BA760E49A71E859351E9D + 4BC7A279C148342E1C899625E3D1BE62AA40F3D231685A3202ADCBC762C4BAE9 + 98F0E83C4CD8361F131E9B8FE9BB9660F2138BD0F5F06434AD1C85111BA751DA + 72CC7D760D66ED5A81693B1763C4C3D35033AF0B65333B50CA6E95A775207F4A + 3B7226B62177523B8AA677236F72078A678C40E5BC312899D18DE2E924476057 + CF55B3BB5131B30BA5D3DA5138B91559A3EB90D25905737B250CF4AE8DAF2C12 + 48AC2D83B9B912699D0D289FD2838EA533D1387F124A2777A16ECE5874AD988E + FAB9A351337B24266D598A394FAE43DBD229C819DD809C510DA89A41FD5E3001 + 75B346A2604C23B2BA6B903DA21EE99D35486E2E87BEB604DA9A52E8A81D4D35 + CDC5EA2B91DAD1446881B1A90EFA866A181A6B616CAC83B9A91EE65626786BA0 + A7B2C6960AE89B48DFA66A985AEA91505309557919B4B53548A8AE416C6919F5 + A19AEAAF431C93BC452588CC2F44786E1EC272721192998D604248162307A1D9 + B994978FA8822221CB21A7F9A7A6C33725153EC916789993E06134C3DD6012A1 + 973959E405A6672288EA0ACACE817F56267C69CEE56749855752125CF57A7898 + E8FD9E9923EA655D8C0D4DA4572D22F20AE097920E4F438A8097D102BFE43404 + A565C3CF9C01977813DC1392E1A9B350DC004795164EBCCF2CCD719CE2B47061 + 17C68624F818A9AC3E4920C8C296BBA4AFC1828024D2C5940E6F7D0AD563806B + 9C0E5E5A9AC31A93E0AB37C34767A63992098134678ECCC84330C93319CCAEA2 + 7DA88CBF290D01E64C44A417222AB30461A905083467C3536B819382DA5724C0 + 35C60AA7E8780C8BA4F9698495F41D4C73CB41BC9F2D6170088711184873CBC1 + A19182C81D1C4A692111E298495D0EA5B8B08CA53935EF91CBB283683EDA3F30 + 5494B7E6DF268FE5A14018D5111C86C1812118E01788FEF4ED3990AD68EDC0F9 + 4C0C3B32F94A6D319CA93D864B64742F5C69FEED12C9794C1A3322E0A38E13F3 + 602FB6F825995E7299C08433D7CD71A95E6EC3312C9ACA5A65AD84B4D5D2D799 + 0960EA3783E7DE9265AF007D2B0F629297BEABFBD137F2404F3F81415EFC3333 + 5BEDFAE17E571FDCE7E24DA1AFC07D2E1CFA08DCB6C6F51516B90F32C1CBB24C + C8DAC0C70FF04FEF9CCF71A7DBEE940589CA64ADCD525690B6B26341C4DAE292 + B5AD1C2CCFFBFBF29EBB72709AB0C0A550B87FA67645DB14972C73A57D810579 + 4B21AF934864672FF16983751DE5C790CB30781D4810A5BC972EAF1DF1FA10C5 + 25F254829C00968853695D494A136B7BFD1C30C089BEF33D0245C8EB59D2FA15 + C7598E4369AD4D5A0F93EA9464E4C7B7F5BA738D4C9E2E647F26C4BA5B1FE90C + 69BD4C4EB84A6D487972C875B2EAC17DB9DD1F6EEBCE75BE3BFBC690B723415A + CB93D0A70C8D1F8FA31C62AC653252FDD23990C7ED212F234F93F4E463B156CD + EDC808DB9FC24F91BB12385DDAC7592E27C5A5F5EA3B604B13EBDE0C8A0B12F8 + 01697DFE9F439F04B06C5DFB366E7313FFD5E88BE3F82F451F1CD36D329774EC + 83F495E3479C15C19EF39243E2BFEE2A23E7EBA4F8FF5760DFD79F8B3E09D57F + 31FE1E02B8177212B7AF7C822087397E57F2578E3E3AFFCFE26F0CFE3D4CD8DE + 63CD7F801E102EEE01F00DA0098C9B3FEEBB9F2EE85F0CC4BDECA39C60257DFB + BAE8AD69BD3706BB809611C1F2F6ACC4B2EDE611C4AF144A90E711EC6ECEFF13 + 70B78797FCC1660F29FFA7E4E5793F85BE1EF67F1BDC465F69D46E5F04EFDF42 + 9F2F1202D5CBE0FA7BC95D0613B80CFB74799E0DFC2264F7CD7710C076724286 + 5EA0F70DA0173B81E37CED70B908951179E54D5069533160980FC97219D289AE + 6F7B22F7E7404E1A4B61EF44852714B2C98B0469E2214D56A449889426A5F755 + 4E9E2E1D4B69F6E5FF16A47AE4F5499321491F494E2AC369523E4FF278622E27 + 8019521A87D24450AE9BBC6E09BD6D70BA0DF2492887529A7D3ED7D51701FC20 + 7FAC38D06475184D486DA42F93BFBD44B003F581C2071CA91F847E4CF4D2B11C + 0F30394BE0BD7DEF1D62257B194CAE3E388CDB1B8A0728ECEF4C6D38B00C5D47 + FC77A4CD0DB4D57A98C6823E6A18D2870A13C06CED2B277D19D63F2BADB877D0 + 300A1D7B211D0BF2D646D272DB0F3838912E4E549FB300EB75FF502EE3D05B4E + 4E264B71B62E664B60D641FA5B93757890D36CF50BE297495FB117B0977043CD + FB0EB35BE5419EECA298095076EB1B04C780600AD9C23310C3088E9C461FC6CE + F461EBE21F02479F003830614C1FB6433CA8AC80B720490589CB96BE6CD5EC42 + 63EF4A6DB3A52C13A2368B51260F9948147B07B3C530C90A8B5B9BBB6941007B + 909E4CA6FAF90937CDBC4F2D5BEB5A5D5133D94BF5C8485A71ECCD16CC560B63 + 613DEB6B75C5CC04B070D9CC842CD5C904345BE5F623DD1E7072C2001B093C84 + C03A30C1CCAE9E194C04B36E03B93F4CCE4B7071C1402AC7E42EB72511BD0C89 + 146602580293BD9EA141F00E0F15A17B30EF791C00D7406AC7460207C5442342 + 13278860B6601EEAE5417D6042D94310C2921C9771A4FA98FC65CB6201927720 + 594E77A27C57268BA92D9FA808F812780F646FDEFB98AD70A3A2E0AB54C04711 + 05CFF070788685C33B22121EA1618470B157B24B601078CF65CE0B51C7531821 + ACD41FE09F14D8729BCE118F0DEF93CC32BE4C76C6AA10A9D3416130209C5D30 + 535FFC62A3859531938DA18909884D4B41426E2654596C8D9A4C6116B4054550 + E7E441919A0185858EF32A9051DB094B6513228C49F08E263D4967C72076A1E6 + 0F8FE848F8AA95F05246C23B564175C7C057A58427C93987D2B54BE79B4969DF + A818F82BD4088E259D74C908D71A484E4D65D5084C48800FC5DD150AB84684C3 + 9DF784A63172A3D02934046EE161F027397F950A6E743EB85E371E4F1AB710AD + 0631C966846A134837053CA88CBBB00066B7E26C111C062F453482E235084FD4 + 2324410B0F5E88620298C795EE2D0E05711C44F715950950C520CAA445A44943 + F553BB4CCE936E9E51D4BFD838D2430BD73085585812FB29078453DC1A0E0B8E + 00EF2DE6104CD70DB711120617D6274A0957652C3C626DD611228C85A792C72A + 1A9E54BF37C503E3E2101047E3C2E72ADAFAD3805F4C14F53592AE913091C63F + 05B06C008D7B945E07558A09712989884BD2D2D8C62244158E88F82828F52AE8 + 330C28AA2D467D5723AA3BEA51D4548594A26CA82D098834C4D2982422429F8A + 7042608289CE9F1E3EEA0478AAE2086AF8C493AE1AB5802F8D85B72616AE3191 + 7089A6EB93420F259D2F8ABB73A80C83671C9D37358DBB32086ECA5078ABA3E1 + AB5151487DA5EBC3373E16FE09DC77BA8E62C3E0174F7DD484C12DDA8FEAF141 + A8211A9A9C24A494E7A0A0B91225ADD5C8AACC4356450E528A53A1CB36203E4D + 8B6813F5333E12A1EA48446AA91FA457508C12FE342E416A155D5F1A28E91CC6 + A51A119FC9AEB4CD88CB4A14647352791AB21BF29156990E7DAE0E09EC4E3B35 + 16B1490AA853944848574397150F637E22B5A7A3EB4B0D25219AA030ABE9BEA1 + 7AB28C5065EA1097A783A6408778DE8FB93209992D99C86CB6A07844367A1634 + 63D29AE118BDB81ECD530B513B3613CD930A307E411366AEECC2E8D9D5A868B7 + 20B72601652D3AB48DB360D282222C78A81E2B37B560DDA31D58BBB90D0B5757 + 61CE920A4C5B548AEE6999689D988E11734A307E793D26AE6AC6D4B55D98BEAE + 1B1356B5A37B5E1D3A6797A17B6E3E462FCAC398C5F9E89C958E9A3109A819AD + C5A879B998F77023E6AC6BC2C83985A8EC36A2624412DAA6935E2B1A31EB91E1 + 98BEA1136356D461EEF611D8766C119EBDB4012FDD781CC7DE7D06AFFD722F4E + 7DF41C5E7E73079E39FB10761C5B828D2F4EC5D697A7E1F9F34B71ECD67A9C7C + EF611C7B6B35761F9F82DDAF4EC4E1F3F371E8CC1CECDCD78DB5DB6AB165770F + 0E1C5F82DD2FCDC4D2871B306B5925C64CCF456D47028A1B14E81C9F82D5D4EF + A7F64DC1BE2373F1D2D1B9D87B68329E7961349E7A7E38B63F598B8D8F1662DB + CE223CB9AB1ABB9F1F81574ECDC5D1B3B3B0FBE54E3CFE52279E3E3613CFBDB6 + 16FBCE6FC7D1B7F6E2C47BCFE3C0958DD87D7A110E5C5D860B9F6DC33B5FBF20 + 2C8C8FDC7C047BCFAEC4990F77E0AD2FF6E2CC7BDB71E0FC4A1CBAB81E67DED9 + 855BBF3B828FBE3D8B0FBEB9800FBEBD88F7BEBB88F7BFBF8C8FFE7A0DEF7E77 + 16D77F7F18E77EB91B2F9E5D8B270ECCC4EE43B370E2FC5A5CBDF5288EBC360F + 3B9EEDC47387C6E1F495E578FDE66A9C79632DCEBEB901A76EAC236CC0CD5FED + C6FB7F3C888B1F3D837D34A65B0ECCC1CAA72760EBE1C538F5F15EBCC56EA9BF + 388EB39FECC7998F5FC0854FF7E3D2AF9EC72BD71EC1AEE373F1ECA939387861 + 115EBDB112AFBDBB1E173ED98CCB9F6DC7E9F71EC18E572663FA43C518BD201D + 4B363760CDE3ED98BC300F63E7A6E1913DED387A73198EBEB90ACF9E5D80FD97 + 56E3B58F9EC4917776E0C9536BB0EBCC661C79F7004E7C7C042FBFF3225EBCB5 + 0FCFDF7816BBAFB0D5EC7EBCFCD101BC706B379E7BF329BCF2D17EBCFE9B6338 + FD39E13F4EE2E21F794FE8F338F2D1613C77ED39BCF4DE615CFAE61ADEFCCFB7 + 7199C293BF790DA77F43725FF2DEC4AFE3F5FFA036DE7D1EFB6F3E85C3EF3D8B + 139FBC8857DEDF8BE7AF3D8967AFECC4BE379EC1416AF3C82F0FE3D8E72770F4 + D36378E59357F0EA674771ECD35770E46392FF80CEE507CFE0D09B3BA93F1BF0 + D89125D8F6F23C3C796C215E786D095E3AB7142F5F7E08FBCEACC6E6BDD3306F + 5327C62EADA67BA496EEA5264C59DB8E654F4EC5E643CBF0F871B6DE5F8E498F + 8C411BDDAFB5D3AA5138BA08793DF9A898548BE6055DE85A3E5A60D4EA8918B9 + 7A029AD93DF0A41A544DAD43C3CC66D44D6B40D9B832148C2840E1C8429453BC + 6A5239EAA757A2654E0D5A67D5A26D5E233AA8FEE1CB5A3172D570F4D07361E4 + EA2E8C7D7814266D1C87299BC663CAC60998F830C51F198FC54FCEC1DCC76660 + DCDA5118B36A04C6AD1E81D1CBA9DCB24E34527D35932BD036B79E8E3B309AEA + 1AB76E2426AC1F8D31148EA0FA47AEE946C7D25634CCAD45E3FC3AB42FA67657 + 8FC4C4472662CC9AB1689CDD84A23125D4D752EA473D6A6634A27656139A1776 + A0650961693B5AA9AD711B2762FE9E4558B27F1996BEB014CBF72DC70AC2DCA7 + E662C2A609184DEDF1DEF0F5D36BD0447A752D6C433795AF9F598CD2F1A928A1 + 675DC3CC224CDD381A2BF72CC412EAD7ACCD93317EF568B4CF6B470DB55D31B1 + 0EE513EA694C1BA94C238AC637A064621B2AA775134650BC15F9636B50403295 + B33AD0BA742CBA564F42CFC33330E9B14598B06D013AD74E46FDD251685846E7 + 71D544B4537ED7DA2918B97E06BA564D43FDFC51A89CD989D2A974EED8D27766 + 0F0A26772267423B0A2675A1645A0FCA668C46F19411C818D984D4117548EDA9 + 45D6D846144F6AA372ED28243DB246D623A9AD1CFAC612E8EA4BA0AD2D454255 + 09E22B8AA1AD2A859E8E75554548A8CC87B9A11C199DB5C81DD98C92F1D4C6F8 + 0EE48D6A42C1B856742C9B8E091B97A26DC954E48D6E46726B2552DB6B90DBD3 + 40A8476667151D57C0D25609534311E2CB72A02CC8848210959F89F09C340AB3 + A8DD126ABF0CB125855014E622323F075179B9505018539487B88A22D29174AD + B312C7BADA0A24549753190AABAAA02A2B25D97C4221628B4B115B548268267E + B37311CC3F02A6A4C237C9021F53327CCDC9F0A338A705515E0813B96999F067 + 193313B266B8EB0C7049D0C151AD21C4C3292E01C354F1B6B8064EF10970D3E9 + E1A137C0DBCCA4B0191E4623BC8D2678529A8BC64AD87A241AAD6D59D210989A + 4E6DA6C1D79404779A4F3AC7E9E0CA64AECE041F7D323C7566B8A812318C2D6A + 69DEE94A731B47950643A363AD7BE7D23C6C08CD4B8729D5949700179AF738C6 + C4C19964FC4DD406C15363806782193E891684A5E52024391D6EA48B4B8C0AEE + A48F2FE9E3CB96C43AD28BDA0C34A5C097DAF74D4C86B72E89CA9A48CE0057D2 + C3576B819F8EC68442AFF824B8C5E8E114150F578586E6861A9A53C5C399E6C8 + AE8A389A0F519A82C62622064343A2041C4268CE191C89C1ECD23884F7BD8D14 + FBDE0E605237280C8368DEC91842F9429E2D8599E80D21394AEFE7178C077D02 + 31202094CA33C11B61AD8B43CAE738CB717C085BCDD2F7E540267FE91B534E00 + 0FA0381F0BB05B65DF40B1C72E63307D970EA26F56DE6286CB32387D88D86E86 + C9626A9BBE1D06F39C9BBE55DC682ECB73E421A4FF5001CE0FA532D6B6382E59 + 200FA17E0EA37EB1A72061994CF2229DDA64C2B7D7E297096E4AE3BD7ED9CDB3 + 03C7591FDE02C78B5D35FB092F65FD79FB2AC2032E9EB8D7D9CB0A176F2B11EC + E243F0166982DC250872D506265C05012BC3FD04916F237D85D5ADB38FB0A695 + 2C6825DC3F8CCA38B025ADD56D328756EB5A6B1ECBB305AEDCF256B2B2BD8DDB + 65ECEBEEB5D41DEA7EDB2D33817FE0EF8BD0B587BD8C5C4E4EECCAD790E46428 + C7253989D4E435274996D76A782DEB1EF676F9A0031CBDE81B312C5690C0F235 + 2E8E7359B11E655B53E37439196A7F2CAD8931ECF324483A7128E92C07EB7F47 + 1F58873EE418D2DA991C9C2ED5218754463AE6F6B96E49170977E8CAB04BEB0B + 3FA50F431A13716CB746683F6642CE262341CA97E21C4A6B937C5D08D7CFBCE6 + 4BE992BEA2AC6883E46DE4EC7F09FA51DF64EBD77740B60E6EBFE67D37FCAD7C + C65D2D8065EBF5ECF1B2AF7D80FFABD017BFF1EFC64FB6CF9C532F974532FF00 + 012CE7BB7A61E3E0980F630EECFF12C07F07E4A4ECBF0B7F3701DC2781FB0FA2 + B7CE7F6290EE869F31F862136ABA2807D343333A361149965C44D024B01F3F24 + 7E4400FFF882BFDB8D2188E0BE086061622FDD7477DE84FF5D0F85BF07F28747 + 5FF1BB41F2AF6F5F8F3CEFE7C1FA80FFE761ADEFAEE4AE7D7A5F90642579BB36 + 7A5F6E7288736E8394462FB4DE905F6E843B5E8604294FE4D30B552281A5BFAB + B8FC0383DC91915F8505AB36A2A665145C3C43ADF5927E12912B879CE895D097 + 1CA357C66E92222615046942284192914269222387BCDC4FD523D26D93190952 + F9BBC1BE1EA92E7B1DA47AA47CB98C7C12288734D1E2B8FD044D2A2B877DFDE2 + D8262B4D3CEDE519F6C7529AD081277A4CB032896B2362FB39D3870183C96007 + 9A880EA5FA09F7B33B64267C6596B87230197BFF10265DADC42B5BC40E74A1FA + 1CA97F439DACE4A81B7D24084296EA639295C9651B241D184C4AF7A30F1FDE73 + D74AF2DEDE47C5DA0E5BEC5ADBB55AE75A09DC5F0CA0E769FF21D44707716C25 + 8299B0A56B52C41DA94E1701267745199297CA48A42F13BE0CC9B5F4001E171B + 11CDBA48FA3091CD7DEBEDAFAB2706D307A883275BE332A9EA8FC15EBE18245C + 3EFBF712C00E016CE1C82430851CA73417FAA8752538C9AC8087B8F35EB9BE70 + F066B005AC0F8678790BABDF81EEA4931B8D1581095DDE0B580213BDFDDDDC29 + DF5DC8F1F1202F2F61F52B2C7D296477CD437C492FB6CEF5F717E03813C8A20E + 9261529849652698AD2EA6DD8505B070CB4C79EC9259DA8B5702BB836617CCEC + C25910B96CC9CB3A70592683DD3D05B9C9643693BDEC1E9CC1F181AE2C639565 + 025894B181895F094C000B1D487FD681895BAFB010B0852F93BF120499CBC42D + 8189DF10558C20899D98C8F563FDD98AD84A0073E8E0C5C4F06DD2995D5F73DB + 6C8D2C5924B33533EFCDCBFD74A1FEBA113CC3C2E0131969257A85B56E141D47 + C18BE25E4CFA125CE9DCBBD2B9F60C0913A13406628F688A8BFD906D70F6F587 + 4750087D40472090C9DFE818042A6311A422A855088A8B45882606C1094AF8A9 + A9ADD808F8AA2211941883684B22E2B293A12FC9416A75194CA545C22A38263D + 1521068320038374664499D3109EA0873FFFDDEF1780219ED67D888705D075CB + 6EB209BC2FB21F137071710854A904A1CDFB283381ED19120EFFC8180444A910 + AA4A4098468BC0F8380468E2E04B3A7A28A2E01E1921ACA0BD154C8CB3FB6B2A + 4FE7852DAADD79EFE4207623EE0DB7101F01EFC840A8921290535D8CACAA32E8 + B2D211AED3C057497D642BE7383522F53A44191211AE4D40687C3C0278AF31B6 + 1E0EA1F31A1008695F6C76ABCE6EB29D828285EB6DB6D2760EA66B2534509C1F + 774A730F8FA0FEA9E0437D700E8AA0FB31C40A5F0EC3692C22A97C94C0B0C070 + BA6F4309618210760AB15A29388746C1253C9AEA530A12D98DE0191145D78102 + BE0A15E9174FFAABE045D703B7C796DCBED1B1744CB2A4B32B5D0FEE61D1244F + FDA03A583E3C5E87C804AD40308DBF6F385DBB310A28B4718833EB60CC4C8139 + 3B9542334C79A930E4A40AEBD88844354213686C12D31061C844503C13FC89F0 + 5468E1199D20F6580E8837F4BABAF6612B66825B64B4AD2FD45FD2C7393482FA + 12212C3CDCE99A738E614B92500CA3EB9CDD61FBC65159BA8ED8D283DD0332F9 + CD643F13FCAE91747D4785C23982EEC5E80804EBB488CFCA41727119928A4BA0 + CFCD42629605FA6C0BE2D30C884D4A8426CD8C384B1222B45A04D1F9F455440B + 17E93E91B1F0A37974B08AFA463A479B92A14A498526330389797948CCCD417C + 463A54A92988493151BFE311ACA67BC0A083263509B1663D147A2D624C3A1A9F + 442AAFA5FB4645D020DA98842863322218546FA8C18C1076E94DE5141994966A + 8432D384B87C1354390930969850D45E88DA5115A81B5386BA9125A8EF294463 + 4F01464D6FC0FC95633061463DF2CBB4C8288845758B091D633331714E05662D + 6DC4AC258D98B7B419B317D563D4A442D4B52721BF2A1E59155A1435A6A17C78 + 11AA7A4AA9EE2AB44E6A44FB9466348CAB45715B01322A4DC824D9A2561D6A46 + A4A27E5436AABB3351DA9A8E3242457B0E4A9B72905D9106638E9E608031CF84 + EC9A5C94759621BF351FD9945F3EAA14EDB39B30655D0F56EF9E8AC78F2CC6B3 + AFAFC7AE136BB071EF2C2CDE3612731FE9C0EC875BB06C7B37D63D3D060F3F35 + 0A8FEE99889DFBA662D5A6162C7DA81E8F6CEBC69A0DAD98362B07A3C6A762D2 + 8C522C5CDE85F9CBDA3179561526CDACC2C80985A86AD42133271C8545B1E81E + 9985692437775E2916CC2FC48AA5F9787247238EBE321ABB9E28C7DC19519831 + 31002B17E8B0ED916A3CFBCC186CD9568F69F31231618E0153971660E242AA7B + 612B166E988A255BA662CEBAE158BCB9035B5E988023D7D6E1FAA7BBF0DAADC7 + F0D4CB0BB171CF34EC7A75090EBCB6144FBC300E9B9E1A8EA7F64FC1C1132B70 + FAEA165C7EEF19BCF1AB43F8E8CBD7F0ABEFCFE1978C1FCEE1E66F0EE2D59B5B + 70F0FC2A3C75600A1EDEDA820D5BEAB0FBD9E13879622E0EBD3C1B9B36B563D5 + CA5AAC79A806ABD69463E5DA722C5B5D86A5AB4BF1E88E76BCFADA425C7F7733 + CEDDA4B13D38113356E4A0715C0CC62E4EC753AF4EC3D55F3F818B9F3D2EDC36 + EF3BBF1CBB4F2DC4CB9757E2E50B0BB1E7D858EC7975049E7BB50BBB5F69A378 + 274E5C9B819BBF5C8F2BEF3F8C670F4FC6E4F9996818AE46F7380BC64ECEC3E8 + F1C598B7B819DB77531F0FB6E2E9439DA4FF2C5CFB642B3EFEEA00CEBDB70D1B + 768FC6FC879BB099CEF3AE5757E1E1DD33C5CF0153D7B563F61626ED6663D5B3 + B33167FB38CCDE3616AB9F9F852D2F2FC6E6838BF1CCD9CD38F1E17E1C7D6F2F + 1E3EB014931EEEC1E25DF3F0DCB56771FCD35771F4C397B1FF8D67F1C2B5A770 + E8CDDD38F4C653D87361339E3A4DFD3FF9101E67F7E9147FF6DC663CF6EA6A6C + 7E6919D5BB021B0FAEC4C697D762C7E9ED78ECE436AC7E612556ED5D8EAD4737 + E0E9D7B762F7EB9BF1F4C97578F2C46AC22AEC39B30A2F5E5A87972FADC7FE53 + CBB0E7D05CEC3FBE1CFB8FADC2D6DD33B0F4E16E4C985B898EF17968A2EBADAE + 2703C3A79761C6AAE198BD6E0CA6AD1987F14B47A3756A13AAC7D4A26C640D8A + BAAB91D75589ACCE52647494C0D25C80A4DA5C24D7E723ADB90819AD4528ECA9 + 41E5F856948E6A407E5705F23A19E5C8E9284626DD5B395D85281D5D898A31D5 + 844A548FAF40E3D45A344FAF43ED447A5E4CAA40EDE42A548E2B43093D2F8A47 + 14A06274299AA735A067E1700C9FD781A6A98D689AD284660AEB26D4925E45B0 + D4A620B3311D35E32BD135B70D23170FC7B815A33072C970B4CF6D45F7A22E8C + 5E3E122DB31A5132B604055477414F314A465790BEB5A899D888F2B1B5A46B29 + 325B8A4857EE4F2132DAF2A9BF45C81B598A8AC975689E4F3A2C1F85B10F4FC2 + 946D333171CB348CDB381963D68D47DBE24E54513FAAA93F0DD3E9B934A70D9D + 73DBD131BB8D9E25AD68985681E29E74A4352622ADC180929134F62C3BB90675 + 136B503BBE0E35E3EA5139B60E85DD9528A0F12BEAE16D071A68DC6B69EC1A50 + 30A24520ABA316A9AD65486D2F430E8D79D9E436544DEB44EDAC11685932016D + 4B27A271E15854CC1C8E9269EDA89D33126D4B26A163E914EAC338544E198EB2 + 09EDA4EB70D4CF1A89F6C59349FF69A89E350E459346207FFC0854CD9C84D229 + E390D6D5027D7D1534D5258422E86A4B606E2C85A5B502292D1530D695C25053 + 4261198CF515142F87A6B4189A922224945199D242C414642322D382487A17EB + AB4A91DC500D735D25C996C2585B86B4D63A64B43720BBAB5984E6BA72E82AA8 + 7C4901E28B73A88E1C2A57005D653ED474ACCC4D8582DEE58A1C7A7F67A5212C + 3D05C1163342D32C88C8C8A0B632E83805FE4926F8188DF031E8090604A7A420 + B63097DAAD84B1BA8AF42B436C01E9975F40D771138A468EA173DE4969C5249B + 8E0073AA084353B30899084C4A85AF2149C04F82DE2CE0AF3789D02BC100377A + E7BBC4F2BEB371020E8A580C664BD3289B5B629AE7703EEF4DEB42F9620F5A9A + 1BB8C5F13EB41A0AE3E0A149802713AE94EE18AD1265AD6E8AD570E6AD376234 + F05069E141F3130F9A577AC6E9E0A331C0574B3A30F14A7318C728B5B0AA7556 + C65BEB8854C285CABB28E33084E65F43693EE618152BE20382C23090E6761C77 + 8BA5BAA95E479AEF3151EB4DF57A6B12E1417A79AAD5626B8F1013F55D6B80A7 + 4A036F9A8779D15CC7BA5F2DE9CF5B9C501BAED437779AA7F9903E9EA4ABA792 + E64204F728EA2FCD89DD685CDCA87D578ABB52FF3C49376FEA9717E9EB417DE5 + F9A26B448CCDDB0CCD2DD9D30C81DD30B3952C13A54CBA3261CB3F220E0B8C80 + 53B082E6660A928BC2503A1EEC178A81344F650C22F03187037D823188C0A194 + 3FD02B88BEFB0205FA7BD9DC26FB0460107D7F4AE063467FFAF6E470207D874A + E8EFED87073D7CACA0EF847E54473FFA4EE9CFDF8A3CE7A6EF0126811903E97B + F041FA9EE927DA0814754BF530812B91BA564297E6D94191623C78FEC9A4AFB5 + 3DEBDEBE5C56B2FA9508DF81942EAC7C6571B6FC1DE8612581190FBA58AD78EF + B3A1D7A297895E82DC8AF7BE619EBD16B972088B5C265C6DC42B13B156EBD91F + 5BD04A64AA9C5C142420AFAFD8C03272EBDBBEE4EDCBF499C6EB3C43DD70CF10 + 5E7321F0FA911DB80C13C5F234791DA21E5BDDF6605989D0BCDBB168D706898C + E4F52C8620C40843DDE99B3C385A10C072325422804559DB5A14C7A57C86BC7E + 09D23A565F790C693D8D43495F894095207497C13EDF1EF632F6E5E5F9F6E97D + F543EA038F977D9EBCFF12A43CB90ED2784B106D731E1FCBD6F7A4347B483272 + 5986D4967C1C590796E1754796B186A42B5B8ADBAC75EDAD767F0EE496BD3F17 + 62DD5ABED62D3FB6A589F56C0A19923B68B1F64DF9FF087EB4466F4700DFF320 + 13C92C6BCF2FDC1D127721C7CFC9FB2999FF0EDC559F7F8600165BA2FE04B14B + A16414F97F096019FA9295434ECAFEBB703702D8FEF83659DB0791FB0FE09E7B + 09BD75735A1F03F4CF401AC4BEF218B63C266607D2033334420DAD3E15412131 + C2FD3313C04C10337A2F70DB05FFE39B41C2ED3CE1EA59D69E208009D20DF737 + 1F0A36ABE03EF3FE9B70B78787947E37FC9F470033481FE94561FFC2B04FFF39 + E8A38D3B5E747703BDE8E460C2975F8212D8B25790BC523EA7D9C8DF5E0B60AA + 87E36CF95BDF361AC959A518E2EA4FF2A4477FAA6F20C9F26481C171DE5F82C9 + 5D9E48D8C38EFC654804B04464DA4F4A385D805FB2B6B894279797435E569ED6 + 5B971CB27A19F2F252DCBE4E39A47439A43619928C14729A34B192C77F34C1A3 + 3CFB721CCAEB90A7312459A93E0EA53CFBFA24588F99BCB496E1891E93ACC2B2 + 96095547B6C4B4EEEFCBAE999998BD77085BCAB2C52B13B33C39E47256125894 + B34190A28200267D991475E47D71E9A3C0CE4A968FADB092BEBD04B38C0CE6B6 + 998C7E80F79EE983F47D90D2EFD483FBCCE7969E0B4CE80EA0E7E7407A4EB255 + EF60EEAF95207E80E23F22806DA4B1C8B759FCCAF7151EE8EC41217DBC51DF59 + 0776032DB982960860B66AE63E32C9CDA4ADB0E0F50DC0600293BF033DD9BAD6 + 0F43FC02C096BF4CFA0EF6A70F67027F400FE23D8029DD993E721DE9A396495F + 26CB0712067359260B6DC41683E38C5E8B5E371A5BB61CA563296FB0275BE2B2 + C52D5BB2529AAFAF0013BF12F93BC4CF8F74F415E4EFB0C040380458DD41B375 + 30836598F064527810D567ADDFDA3E5BFB4AAE9899841596C004272611B97E4F + 4F41FC0E613071EBCA3F04B8F6BAA41EE84AD792239D57B614777211C4AFB437 + 3013AD1209CC90485FC9E297895F09DC3613BD92B52FBB8366C2D725C0EA027A + 98375B0EBB537976F9EC0D677F2ECF75116CE1506F2B093CC0D949B8CD665D79 + EF64B16734C5FBF31ECF2EACA3BBE833F791E148E3C2FA70BF3D789FDEE06038 + 0505893D7B3D4343E14EC74CF6329CFC988C665D83E0164872BE7C9E692C3DBC + 44DC3D90CA1358D62B24147E515156E2571103BF488595488E0883775418BCA2 + 42E01BC3FB0D47C257150EEF584A8B0D854F5C18420DB18849D743939B064371 + 2E4C2505D0E667232AD904CF182586B22B656ADF2198758C84BB5F0886BAD1F9 + A4EBD699AEC7A1746E1FA4FEB205B7135D175E119108888945803246F489F7F5 + F6080A26908EE1D1F00E8E827708C9442B1118AF4680460D4F25BB7E0E866330 + 9D97D04002F5398CCA4484C025C41F2EC1BE3456DE74CDBB539BEE347E1E348E + CE743E5C10A80C85313B05196545306467203C218E74A0BE29C21199A841BC25 + 09AA2423A212D9F5B40ABE344E5EEC7E2E4A21ACACD9127E108D295FFB8ED41F + 57D2D595CE8973105D0F413E347EA18234F663A291C6D73D2C8AF48944943E05 + 0A825360041C7C43E11C180997A028B88528E87C2A281E49F74918DD2FC174FF + 040932D831380C4E04C7E0702A1746F7365BF78788B82BD5E91B150B7F7687CD + EEF35896CEAF47288D57442CBCC2A2A9EE086A87D3B92D05E9A2A4F458F845A8 + 1014A346A84A0DFF281A631A773F3AF741CA4804C74421541D8DF0B86844C42B + 49E738441B34084BA07314ABA43EA910A04A44A83609016A3D3C15F1708F54C3 + 8DC08B8FBC10E913CB8B954CD2AB28AE11A15348143D8F423188FAC664B7332F + 4A868461285D778382299DCEB703F5D395FAC47B3D7B2AA95E2AC70B9DEE748D + 0A6BEFC808E14ADC8D897F653481C69864230C16C4A7E7419F5B00537E0E92F2 + B3A04D4FA6318F4730F52132310E0A831681BC87752CF539864979257C48679F + C8383AD62054A3279924C4985310979E017D5EBE405C7AA62085A34D6641EC86 + 12E22C1698F27291909E86189311EA946424A4928CD188A038B6023740614A45 + 9491D2CC1950246720D4988220BD19C104268243A95C48920E41496A049B9488 + 4DD7C0549C84F4AA546454D3F559918492C62C348E2846E7D82A8C9A5887A6B6 + 1C64E4C4223D2706A5B57A3474A561C2EC2ACC5ED682E90B1A307106C94D2A41 + 436706328BD58835F9934EA148CCD6C25C924CF76B128CC5C9B094A723B33A1B + 39B539C8ACC882314787848C48584A6391DF60445E9D09B9B566E45425D17D62 + 842997F7834E80D6A283CAA881CAAC454C5202E2530D30E4A520B12005C6A214 + 98CB2D3092DE697546948F4C42E7AC024C5AD18CF14BEAD131BD087563D25133 + 321575A32DE8989C8BB173CB306E3613DC399832B7182327A4A36B640A468FCB + 407BA701A52561282A8E44599516653549D46713AA9A93D03C3C130D1D6928AD + D121232B0A99D94A54559BD1D4908C8E16337A3AF5983C36119BD615E3E8A1D1 + 78664735664C08C7D84E772C9DADC5EEED1DD8B76712D6AE29C3E8F171681B19 + 4BE3AC4379AB01652D69A8EE2A40F5F05C348CCEC1A8D94598BEB20A2B1FEBC2 + A6E7C661E5F66E4C5C5C85D1734B306D753D66AEA8C0C49916CC589883359B9A + F1F0B6E158B3A50B0FEF1C87A70E2DC5A99B4FE3E6E78770E55707F0DA7BCFE0 + 956B8FE299134BB16DEF44AC7EB4090B97176105EFDBBCA114EB1F2A13E4F5E8 + 5129686952A3A12E0A8D8D91686A8AA0FE05A2AE2E04132619F1D0C39578E1E0 + 641C3BB3187B5E9A88450F15A37B8A0E236799B1F2F166BC787109F65F5A82AD + 2F4DC6EAA77B306F73131EDE3D12CFBE3A1D2F9F9B892317A6E285E323F0D8F3 + 55D8F1420D0EBE3616676F2EC2B99BABB0EFE80CCC5B514263A2475D8B16754D + 46D2A7148B177761F99A524C9C138D594B8DD87378344E5D5F82B36FAEC3F1CB + ABB1EDB93158B0BE160FEDE8C1A6DD93B064631746CC2A42C3F80CB44C29C4F0 + B955689D518C9A89B9A89B5280D65925689D5980AE39A598BB79241E7D69191E + D9BF146357B6A1784C169AE7D662DAA35330FFA97998F7C41C4C7F7432A66E1A + 83D95BC761EEB63198BB753456EC9E8A0D7BE762E9E3E3A90EEA27A54DDB301C + E357B561DCAA768C5DD58991CBBA306AC5080C5FDC899AA935689C518731ABBA + 316B338DDBE393B172D7746C3EB0004F9F5889E75E5F855DC71763C7C1D9D8B2 + 6732D66DEFC1C69D63281C8D994B6BE95E2BC5B89965183FB30213665561F804 + EADF08BA56C71762FCDC26CC5C350693168F44F38406B44C6E45F79C1EB4CFEC + 41F9A846A43515C15495037D7916E20A2DD09565525A29325B2B0915486F28A5 + 7B271789946EAECE83B9260FC6CA4C24D03DABAFB020BDB910D96D25C86D2F44 + D9C852D44EA844497701526BE99951CF446E2A851624551AA12FD1C2509288A4 + 0A33321B329145CF919CE61C147595A2A0A310C95529D016244293AF83A99CEE + F5960C140DCFA37A8B503DAE5CA0724C19EA2733D15A8BBC8E6C182AF5482CD7 + 23A9C6828CE65C64931E4C5067B51623ADB190FA960B6D8905DAD264E8E81990 + 58950C535D1A32DA0B5036A11EB5335AD0B270387AD64E40C7CA91A89BD382AA + 690D289D5885426AAB745C256AA734A079462BEA26D5A16C54058ABB8B51D049 + CFBCC624245527C250AA85B6281EBAA204184B0D48AD49453EFF34339EEA1FDF + 82A2CE2A1A9F7214F734A07C742B4A4636A1B0BB11B91DB5B0D497C25091075D + 650E0C75F948692D43E6F01A640DAF46665715727AEA5030AE194513D83AB716 + C9ED65C8EEA947D5D46E34CC1C85AA299DC81B598F023A979553BAA83F3D689E + 371E8D7327A264E208A4753622B98D5D300F177BE926545722B6B41831258588 + 292D80B22817B14559509764092BDCF852D2A5A24090B9A6BA0AE8ABCBA0292E + 842A3F17CA9C6C44D0BB252C35192129F4BE4831213A3B1DE14CD8261B119969 + 21B96C41F446513C38C548B266013E8ECA4AA5F2C908CF3023223359202C3D89 + CA27439145EFACDC2CAA83C9DE640426D33B89C2207A87059893E047EF266F76 + 9FACE5BD6B35C27AD597DE6B4C0EC71717435D500805BB6DA6F762585A065D13 + 34769DDD48AA6BA276F304D9EB49EF3F9F4413E99A46EFB75441F87A26E8E1C6 + 6467BC0E9E1ADE32C206B5165E3632D655190727266C23941816118D21349761 + AB558770051C239594170B179A7FB8B2056E34CD3904A1AB863BCD633C547170 + 8BA5F90213C274ECACB4EE4BCBF5707D2E341F71656BD928B5352E958FA6724A + 0DDC9509F08C4D84478C164E912A38B0D5A86CAF5B2667DD62E3E148F38661A4 + CBB0C868E13A995D243F40DF550FD2BC8C5D21BB285570A63A87D05C6B205BCB + 868593EE91A433CD9D345A44D13C21506F10FBD60E63AF33147AAAE3E145F306 + EE8B0BB5E54CFD74A3B98F3BE9E8C4E465B892E63FD447AAD79DFACF3FC9B929 + 94D4179AE3913E9E24EB4D65BDA86DDE3EC495E6830CB1450ABB3D0EA23924CD + 05196C1D6BB5BC0D16F331F642C33F203AD33CDB91E6890E74CC04EF20EF20FA + 7EA37C267EE97880678095E4F562AB5806C703D19FD21EF4F0C303EEBE7890DD + 197BF8A01F812D67C59EB9BEBC676E1006DA48D54102D6FD74199CDEDFD34F94 + 6108F298D2FAD137E98334C71E48DF2EFC2D3B944960423FFA7E78C0C39BC0F9 + D49EA7BF209099541616BC36CB5E2BE9CC043193C06CE5CB16CAC182387E80BE + 4518DC96B0F265509C75BF9FBE89D9C2B71F5BFBBA533F3CE8FB9AFA36883090 + BE6318BC65D5030256D25710BF3622985D3FF7A76306BB751696B58E3F066F8B + 75AF839BC0FD436464AACD7DAF20CA243297C9B29F0013AE12012C91C052DE1D + A4A1AC8C3C4F4EDA4A69724290D77BC49A8F0DA22E86AD9E5EFD6CE9521DD231 + E74B3252DD225F762C4F93D69738E4B5245E5762083D6C6B7DBCB634C8C55758 + 06DBCB4B908E7BF592E5496952BA7D597BB05C2FB9CCE56CA11CF27EFC2D88B6 + A90EA9FD3BF2ECC6E56E75CAF325C8895B39FA2A7F1FAF7FD9E2F6F202548F34 + 467C2CADF349E9AC37C7A5B1E3502E23A54BB08E1DD5C3E7B33FAF71596518DC + 67EBFA21C507D3353090CA0CA236081C975C39FF14FA2276E5B09793886591CE + EBD6F6EBDCB2F56F5ECF6648C4EFBF8200BE636DFF2708E0BF877B90F31DFF9F + 8544E8DEEB807F8800FE09BEEDFF12C07DA02F5939E424ECBF0B7F37012CA10F + 52F7EFC1FF11043081495976013D845EEE4EF4E21B441305E11AFA17AC17CB48 + 17B8F5C6E063E9C61071711350BA2DFF8E1B44D61E5FF856029841F97F0BB63F + 32FE4F2281EFF630943F28FF5EFC3B0860E905F2B75E227710B8FF0CFAA85B0E + F90BAF4F909ED28B9109E0FBE925783F13BCFC3224482FC95EB0E52FF74D02A5 + DD3FC81D0E2E0170F70E87836B8078C15B37E9B74EF0A4C902C7ED2713724879 + 72481345EB44E276B99FCC23FC82214BEBCDA33439E4E9F6F5C98F25C8E5EDEB + EB4BBE2F48D6CCF665F8583E560CE9D87E7226B52D95938EA5B8742C8F4BC7F7 + 0DB2D6C9B88726DE7C9E384FE841CF2349F6179C679BC4B0CE2CCF046B2FC9CA + 44AECD125710BD6CF1CA84AC8D78650254829C0096AC61ADA4286108B53188EE + 65C2BD83E9BEA46389F465B2F4DE214C985AAD66E5A4AF95046639AA9BAD8D1D + DD7AAD6E2548A42FBBA1965C514B3A71BE550F7A9630114CB86F20B535C8190F + D038DD4F7A3E48F94CF24A24B0643D6CB514B692BF4CFADE49FE5ADBE4BE4A64 + 34834971267EB91F82E06679B600A60FD721F4B1CA84EF201FFA80B559000FA6 + F860FA58E60FE6817EF491ED4F1FC44CFE1206FAF90BC2D8813E748752D94154 + 0793C903A8BE01F431CDE4AB6495CB242C13B28E549EC92EC9E53383E38C61D4 + 06E73B32B12BC85D8A0706F6BA7B1696BE140E66703C80EA0A62E2C72AC36E7E + 1DB83CC59D99D00C0E81337DDC739DBD7BF90A329A89B6DB56B9C26533EF634B + 2113B84CFEF2F130D2D9C19DD25DDD3194C2A1ECCA9AE2BC1F3393C0BCF72F13 + BF929B6BAE4F72F9CC60D255B23476237D24B299E11C60257E9DFCD82A988963 + 172AEF02471F77025BF5BA529D6C89EC486D38639837E945C7035C799F612BF8 + 78A827EF99EC28F6861EE44CFAB8B80B1D79BF65DE777990AB95BC66527818F5 + DD99C6C185C6DF89C69B49680693E2BC0732BBBBE6F190C685F564DD992CE663 + 26AE87D1F5C17B2BBB304949633B8C49753ACF1CB2EB67AFF070F84646C13B3C + 129EBC472E95F78E0883BF3292108E000EA323443C501581407514026223C4DE + B2BCAF6CB89EA1418CC504557A12427471C2DDF2603F5F3CE8411FB62EAE18EC + 4CE786E62AC35CE97AF2F4832BFF9C403A88BDA3DDDC68ECBC49BF00F8900EFE + 910A7833091D122A0860B65276F30F2630691D0CAFB070B1FF31BB3AF65444C0 + 3934104E817E74DDD0F804D07805FAC083D23C82FDE11EEC4B7D61AB6D7FAA8F + DAF5A731F574C4503707D2C10DA1CA5084A9A311A88CA0B69944F78167680042 + 62AD56B00AEA4BAC4187586322C2E35424A72044C33B2C4C5C27E2FAA2F3E44C + E7C4834979EAB393BFA7B0300E898B46B03A96C64E013F85428CB54B4010D28A + 4A0965A45B049C7C83A91C8F7B14BC421522740BA2745E44E41F39E87E6697EE + 4EFE4174FD31A80D8A739E8337DD6F3E741FF9D2751A140AB710B69A0E11F7F6 + 102F7FD289D202C3E97C86C3852D3BFCC3E0E8174EF75684209CDD4398F08D41 + 40B44A90BF5E54DE3B94CE7B249D6B8277288D7548107CE95C729F859B644594 + 7047EEC36EC823787FE928F8462B85AB6C768F2D5C68B37B3EDEBF988ED99DB6 + 13DDD36E14B21B6C0EF978085DD30368EC86D1F3C9359849F050B138C88B744E + A4976B780C5D4371708D50C3254C4965A86FA49F138DB163083D2BC24837BE26 + F93AA0F1F58C0A874F740C42350641AEAB939285B5724A411AE22D6C8D1B41D7 + 4C207C9501F0570513C2E01BC356D951745D3301AC847B189D07D23950AD166E + C14313E21196180F853911512603220C06C4A5A52121231D61090988D069299E + 2A88E6F8B46444EAD4825C562599292F51B8E40ED42420305E23F67D0E8A3722 + 446B869F5A0F9F583D7C5546F8A8F4C25A9A2D9CBDE2E85AD128116650233A29 + 1E31492A284D0AA8CC5148C93720BF3A130584C28A54E4159B91919B88B46C1D + D2F37528AA4E41CF847ACC58D083C9B33BD035AA02D54D39C8294E8636390E61 + F17CBF4620CA980015E9AA4C312182FA1469D423DAAC475CAA093A4A579BB588 + D245429BA1425A8919C9057A98F27430515BC6AC446852F87E502386C6254CA5 + 14AEEE95746F284D7AA82D6624E6A6C35098057D613A12F2939190AB83BE408D + 94B24464542721A944075D8E9AE4E26028D0C0529288BC9A6434F414A36D7429 + AA9BD351599F8282522D72F3E3505CA4437E4E1C92F5C1302686C24C7D49488E + 87C6A2A231D121A3D880CC421392D2E3919D6F41CFA80E8C1D3702C5C519C8CF + D5A3A9DE82512352B1705E011EDDDC84752B4B30655C02C68D88C1D23939D8F6 + C868AC5FDD83E9538AD133320D2D5D1614D718905AA84366491A324AB2E81A4A + 464E652A2ADA3251DD61215D2DE89A9487AE89F968ECC9444D67261A46E6A1B1 + CB8CBAA6588C1A6FC182653598B3B8063D9372D0393107539635E1A19D33B1F3 + A55578ECE072AC7E6A1A56EE9888D53B2760DEDA464C9A938BD11392306D4A32 + 962CC8C6ACA9D446BB0EE5A591C8CDF645615E00AA2BA3505F1D8592027F5496 + 04A3A74B8F195372B06A4503366F1D85351B3AE9FC57A17B622EEA875BD03621 + 1FB3D67462D1E6B198B4A2132D53AA50393A0FED53CBB070C308EC3EB40847CE + AFC18BC7E761CB536D786447031E7FB61B4FEF1B879DCF4DC0E69DA330775915 + 464FCAC5F05139E819538C71E36A3071420DA64CCBC6A8B1719834CD84F59BEA + B0657B33B6EE68C5D3CF8CC0A6C71AB160791E96AC2EC68A759558B6B606F368 + 3C26CE2DC5C8A9856861ABF2CE3494B6A7A2BC33073523735145E7A8AE27152D + E30B316A6E13462D6C47E3D46A94529B5513AB5135B916E5E3AB5032AE12793D + 45281C5D88EAC9E568985A8156B6025FD28C29AB3B317E59333A6655A199FA58 + 37A9049563D802B61095E3CB513CB20419CDD948AA4E859990D99283B2315CBE + 05A3970CC7F4F563B0ECB16958FED8642CD83C0AD357B760FCC20A4C58588E19 + 2B6AB0E4E1562C5CD786D1330AD1312E1DE3679560E1AA16EA5F07A6CC294353 + B7114D3D49183FBB1C9317D5A1677A39BAA7D661C2A2E118B7A80B1D339A5036 + A29ADACF87A92217499545D015E520A1200BC6B27C2416E543959D01455A4A2F + 94191644588C0835EB106CD2D0B109A6CA7C586A8B90529D8DAC06BAC71BB390 + 549A84F8AC04241618612E494272393DFBCAD3A0CB334249F78AD2A2863E3F89 + EECD642457A423AF85DE4135395067E9A04CD740936BA27C1395D742C3F767A1 + 16C6123D2C5574ED376709E238BD2E03865223D4B9F4AECFA2E7409606F17906 + E88B53612CCF869EEE9584C20CC4E565503F2C50E51811576042023D4734F4BC + D2905EFA8A34A436E6A3B0A71A15939A5136B11185A36A9037BC02E96C115D93 + 094B7D0E725B8B90DF46E7AB8EC689F43596584827333D530CD0E6D3F32A478B + C894588418E83D6D89833A5B0F03C964371623ABBE18A6D20C4226B29B4A51DC + 5587124271572D729BCB68AC33A0CE4B829ADE0D9AD25CE82A0BA0AF2C84B1AA + 10866A8A57E523A5B902B93D4DB0B45622A12247A4A7515A4E472DB2DBAB6169 + 2A477A0BC90CA7E3CE1A6474D420BDBD0EC6BA0AC416E723A680CE656111D445 + 2550E615222ABB0031F945944EF19C6C4466A5515ABAB0C055E46620B6201BF1 + 25F9C2DA37AEA890F42B40645A0682CD49083498E8DC9B1140EF1F5F9D0EFEFA + 44F868B5F0D2681040F11033FF4064829F2E81F2E95D43A1673C6FB910075F7A + 87F96935944E21E505D1B33A949EFD91A929084F49426812D56F32C2CFA0879F + 5E8F00A309FED49E5FA28142A388FBD0BBCC235E4B4880B756476D1A11966C11 + 16AC8124136C34232C2905D11999D0501F9599B954AF45FCE4E4A321F94413C2 + 93D31091924E6552C4315BBC7AA834F05227C047AD8537C53D9571F08C61AF2E + 5A01FEE98BF76C65E2D3312C0AC342D97235128EA1BCAF2BA58759C196B02CEB + C93FC0C5F04F812A3A8E11651D692EE9C49E672874E177BC908B8387424DF38B + 589A5BC4D05C4329E2AE04273A76A239917318E54550FB34F770A079D930265F + A34886DAF088D1087DDD634846A1A4BC480C09A1F92ECD631C42233094F41C4C + F395C1C1611846FA39842AD1CF37040FF0371ACDBFD99DB12BE9E24773082F9A + 6B38D19C88D31C69EEE512C13F34F27EB5EC2185B74BA1B91AD5C156BB0EFCE3 + 1FCDE5D83AD72D82E65CD4BE17CD5DDCA268CEC89E66081EFC7322F555DACAC4 + 85EAE4ED4986F18F9834771C4AF3E86134D71AC6BAF6BA40A6396300C9D11C91 + 7FC6E31F1087D15C71085BF67AB0DB63F6301524BCD570DA20AF004A63585D22 + 0FB411C1FD298DF7C96508F29642419A920C5BCF0E6622D866412BEDA1CBE120 + AA6710C9B02C83C956013EA6392E5BDF0EE26F4F1ABF01341FEEEFC9A432E5D3 + F7433FFE66A5F92E93B7B70967FE1EB691CBBE8154879F955876E7F2FCB3B435 + 9DF57C80495E376F3C48ED71D907E97B9809DF07E87BFC7EC2034E1E78907F52 + 77F512642F43FCB8CE5B49F18FE31284E52F81895E76A5CC2E95E978800DFC93 + 39FF6CFE401F105B62D9D62524024E5ABB11642985F69088547B3031C9DB7D31 + 84AB641B01DC4BF4C92048473B70BA44FADD11B741BE1E24D68428ED0192B3FE + E06F8524CB90641877F481DA92B7236F5B2E2B95E5BEF1B158931AE0240C3FD8 + BDF1036C214A69D27A96587F92AD45C9F594D7D957BA0429DD1E77C8F1B16DCC + 24B0FE77837DBEBC2EF9F993A7CB71373D441A85D258DA8FDF1D7212F8588EA1 + 549E5D7D53FBD63A6ED727F26DE524FD2462592297398FC7431A6F3EE6507E0E + A43A6EEF49CCC42E95A7F376FF209267EB5E927B7028DD4BFC13C460DB39B511 + B43F4500F712B776693F057BB9BB11C0BC4EDDBBD66D972E277E7FEEFAFDDD21 + 5B9BB791BE72FC5F02F82E10DC16F357D4E73BC0697742705312E404B08D73B3 + 277AFF7F4900DBF7EDEF859C84FD77E16E04F0DF8D3E48DEBF89DEB2D45969B0 + FE559006B1AF3C1BA40B928959716132E97BCF40DCCBFBF8DACADE236E0C2B6E + DF00D61BE38E8B5F8EDE9B83DA61C8DA6202F8673F506CF5881BAEAFFCFF62DC + ED6128A5FF23F87710C03F17BD04ECBF087DB5C1B8438E5E6AF62F403998F065 + F2574E00F7E6CBEBB0C579E37CB6FEBD6F20BDD05956B8972630F9CBC7360258 + 024F2204C9289BE4C9F3E4E8AB0C1FDB4F4A04065989536992244D7AC4C44686 + 3BCAD820CF13EDF0DF5AB6F6EC8FEDCBF696B1C34FCA707DB63AFB2A2FD79721 + F5493E71962661D2B1BC2D79D9BEC016D8F7D32489277A628CD8BA9A262692B5 + B598A43021CC71F98F063619268FEF17AE84580FBA5E9804668B5F11DEB6B865 + C889562B19CAFDA3FA6DC4AFD53296EE2DE17E99E3744FB36B651B097CCF601A + 274EA33CC96A566EFDCBFB074BEE9425F46372D546FADEA1077DB859DD557B58 + 3FE6E843AFBF237FE8F0E499C786CF07B53390EAA3BE3D487DEC47136A869C00 + B6BA7A666B5F76F3EC8E412E1E82F4650278207D5072FC41D24D22B9B9DFBDFA + 385A7564F297DD3F0F2059C96297F7FC650258805D41FBD2072E93BE4CFEF282 + 03854C00F31FD3BC77D220CA1304306108E7531DFDDDA94F04DE4398DD480FF6 + F6C5206F1FCAB35AF93219CB2E6FA5BD7F25709E839F9F206E9D43420439C3A1 + 4B68285C2874A474267C87F8935E4CE806061282302C28184E94EF4C724E040E + B98C7B78383C22ADAE8D5DA82E6E8349666165ECE505DE1378A0CD45B3E4B659 + 8A0FA6B803E93B8C894D0F2B09EC4821137483DDE8DC39F3D8B9080258B20016 + 6E91A95E267D250B63264C9900652B5BC9B2568ABB31B9CD16BFFEDE70F2F514 + 442EC3C1CB4D90BD529C433E6630113CC06518FA3B39E2412707F47776106943 + DC5D096E18E24ABA30014C7A3101CC443013BF4C003339CA6EAD99D876A43160 + C296F594085F417C7BD2B543613FB6262639268099B8E6BE703ECB729C8962C9 + 9299D3D9CA58B89AF6F4A071F615E5186C3DCC24265B397B878520203A1C4131 + 51F08F62D7D341F057862230269CE281F022F84687C097D23C29CF5B118A00B5 + 023E4A5E480AC0401A8B07DD9CD08FFAD59FEEBD41F42C1AEAEC0327262799C0 + A4F11FC2A4BD97B7E81F13DC9EC1218284F40C0E15A13B5D2F8ED467DE1FDAC5 + 375090C1AE741E3CC343E01141E7278C5D1B932EA4AB6F2495090B827B1093B1 + FE74CEFCE0131A007FD2CD8F740D5404C32FCC17EEBEAE70F371455078005409 + 6CF94CE736C09BCEAF375C03BCE019E287E098885ECBD7689D5AB843569B12E9 + 3816018A709209820B8DD3302FBA8E683CDDE8FEF061F7DBC23ADC039E617E08 + 898B42884A496347E345BA7AD2B5C456CD6AA399EA4B1696D8CE3E81F00C0A87 + 5770B8B06EF62278048651DD4CD4D33DC63F34B0DB6E1A1B76D9CD16DCDC960B + 5BF1FB04D19805D2F54172EE349E9E01A40FDD63140E71F7A778302184EE09B6 + 020FC33082834F281C99180E8EA2FE46C2D59FFB426DB2C52DB5C9EEC39978F7 + 098B207DE83EA4EB8175F665CBE730D291EF51465818C9D1796277E8742EDCE8 + 1CB806F30F1CF40CE09F27E8FC7990BC2B9D1F9710EA37C1258874A63C4986EF + 697683CE3F34B05782A1AC5F60343D0B62E116A6A2500DC7E06861253C2C2084 + 9E3524E34FD72AB5E51D1D8A309D0A0AA31621744E42E262855BE7C8040342D5 + 3A84C5C543698843AC99F7F4E51F1702E11FED07EF083ABF119E74DD06C0273A + 48EC8DECAF8CB65A75539F183ED1D1F08B61529B8EE9DAF1890EA36B9CAEEB68 + 25D5A5A53A93844B70B60A8FD26B851BE8F0781502E95E0954462180EAF38CA0 + 3162829C9E672EA1FCAC8BB0BA69646BA2305E60D6C02D520FB78844B886C7C3 + 252C5690DEDE8A18E17E3C54A342406C38FC1401740D8520C6A0842649858464 + 354C691A58B2F448C932C268D188E3DC620BEA5BCBD1DED380C68E2A145664C2 + 9CAE834AAF42688C82EE6305781FEC90F844842626234467827F9C0EBE2A2D82 + D40934661ABADE13E8FA56537B5108D74443658A437C4A02B4697A6852B474FD + 6B11931887481AEBF0D858BA6FA211AAA2638D1E913A03148906A82D16E8B2B3 + 919095867876579ACF7B251BA0498F872A85DA4FA0F18E0DA2718C82D2A44262 + A61E19C5A9286B28425D6B05AA29ACA92B4045550E8A8A52919D6140AA510323 + DD8B3A6A375EAB8382A167DDE2A04DD540979A0805E5979557E3D12D4F60E323 + 8F213FAF18F1AA18E46619D0D69C8E6993F3B17A650D562C2EC5CC49E9183D3C + 11A3BB9231AEA7081D4DD9A82E33A2BCCC80FC123D923235D052BF0D69164226 + 3474BE7516239268BCCD193148CB57A2B229159DA38BD132220F6575E928AAB0 + A0AC2A11D5553168EB3060EAF452CC985B8DEEF1B968199D89E1534BD133B312 + A36651DAAC4A34507AD3B86C744C6217C2C9681F61C0A851668CEA4EC084911A + CC989882F1232DA8AB52A3B8209CF48B414BAD065D4D8918DEA4C7D8AE144C19 + 9D858923333196307244065ADB52505967404E493C0CE9513064AB905AAC475E + 5D26721A729152910653591252CB9350D999837173EAB164C370ACDF36024B1E + AA1196CBB317E763D6A2024C989E85C9330B84DB6726C57B461761DACC167475 + 97A382CAD754C5A2AB4385C91393B1707E1E962FCAA7B1CDC5DA150558B13C07 + 73669A3071BC0AE3C7C761C68C14AC58598D35EB5A694CCAD1323C0DD5AD2928 + 6F4A43FDF052744DA846D7C462B48DC94505A597B4B09BF23C14138ABA0B5034 + A204795DC5C86ACB47466B1EB23A0A51D85346E985C86BCF426157366AC615A1 + 754A9940F3A472D48D2B41594F014ABAF250D091873C2A9BD9940B73793A7405 + 493096A522A93A03E9F539C86B29447167116AC755A26B463DDAA654A07E5C01 + 6A4765A1A2D384B2762D1AC75930617E19E6AE6EC1D4C5D5E81C97868E31164C + 9C518819F3CA699C32D0D8AE46EB083D464FCD463BC9378FB260F2A266CC5E3D + 122367D7A16E64310A9A8A612EC94752691152CA4B91989F07557A1AA2929210 + 44F7117B4CE07DDEFDE27508D21A109890081F753C210EFE9A38849B8DD0E466 + 43CB3F9D6427213E438738BAFFE35213A0CB3221B9281D29C569482EC940765D + 2132AAF2285F0F65720254749FC4D07DCCFB9027E4A4203E2B193194A74A3721 + 21DB42F7AC89EA8BA7B816A622338C0574CD1727511D19309524419D9580B86C + 7AA664D1FD67894398899E557A25819E31F46C884832124C88A4FB2532D98C28 + 4B22A2F919946D406CAE19D1990644929E4A7A766949475D293D1B2A33915C9B + 2F2CA2D5B9264450BD51167A56906C7C8E196ABAFFD5197AEB3EEC9949D0649A + 119BAAA7F1A267AE41237E98094CA0E70A3D97C2E839A148A167519206A1FA18 + 4498D4A27C5C26F58BEE5DF64E602E4DA3B13341994E6392918CE80C0B14E956 + A872D30579ADC86237C799C22D735C490EC2A9DDA8EC14C4E4A6425D900E4D51 + 06E2288CCB4F83A63083CA59A0A4F154E666904C2614D959084BCD4080295958 + BD86A6B0156F2EA232F310919183A0E4540427270B77CBC1296604D0B885A452 + 1BF4CC8C1616B9990867B2D46C41289737A70A32D537414FD0C19BF78FA5D047 + A31508D4D375A3D1805D0BFBEB74F0A3779407BD9B784B05377A97792863E89A + 52C197DE8901095A04904C203D43FD3409F0A6673ED7E1A74D14F0D7E945C8F5 + 7318A4372240AB176DB2652A6FC7E01F47EF0C7ADE33FC28EEAFD1D1394F4154 + B285CE4112C20CD497240B22688E136AA0B14EA531CBC9175067E6D0F59729F2 + 82E97D11447507F375AEA136551A78C7C4C34F4DFA503BEE51ECBA98DECB6C75 + 1BC92E8C23E87B2152C091E62C0E411118C63FD18546D1FB348ADEADD1424E02 + 9795CA886D276C32EE4C067319FED12C8CE442A3459C439750EBF1509A173904 + 44D0FC230A4302C208FCAD427326AA97F5123AD19CC189E6A80EF43DC356A94C + AABA501BECA565887FB0B026EDCFC427CDB706FA84088BD3C1F41DC4DF60BCBF + ED502AC7DE71788F5BE1DE98BD34511E1393EC867828CD77198EBCEF2ECDF386 + 505D1C3AD25CC899E68CAE340F73A6F99523CD4519CE34C772A6B9157B06E21F + 6BC58FB854AF037D17F2B640D277A2F8FEA33687B0FB67D243B84B662F3334EF + 74668299B71E6162987FC8B391B64CD672DB622B214A1FECED2F085E41B852C8 + 24AF5C9665AC7236929730804958372BC1CA04AF208FFB227E6D18C096B636B2 + 98BF55F9FB94BF57F947E57E6E6C99CB242B7D2B31E1EBE98F7E4CD2BA52DD5C + 3FB743C7525CB4C104308165998C96B7C93A31C9FBA00BD549DFE3F733396B23 + 77990066E29743267FA5350189087E80495C9B3C87F7D1B73E83E52502585E8E + C164B01CE26774FED677B0126D1291CBA1B45E23D661065BF3A5751D694D47BE + B623E573597B6B5E7BC8C94B69FD8B0D20E424DF3DB6BAB9FDBBAD15C975E063 + B99C9426976148F5739B52FB9CCE7D90FAC6E5EC8F1952FDC245B0ED584A93EA + 97CB49F5CAF3A4B854BF7C5C25F4CADBCA4890EBDD17EE958109D6DE31E6633B + 88B66CE7C1FE5CF40579593E37224D06795FE490F74B0E510FE5F782C9594102 + DBC0C42B85F709EB746BDE3D83F8FC71DB523AA5D98854B6DA95C85A091281CB + 790C89781DEA4ADFEE814A0C710DB85D7620AFA379E05E2664C59AB0D31D7549 + 65E590085D7B62B72FDC4D561C737BD2DA35412280E54430C785374B0A25FC2B + 0860C61D7B01F39A2A83D224F2F7EFE31E7E1E7E8A1391F2FE5B213CCB12FAC8 + B3E7B76E439E771BBD3C97C47BD9716B22DECBA359F9AFDE747BC8F93A29FE3F + 0ECC694AA063FBBEFD0872F93E202765FF5DB02780EFA586EF91C8597BA2F60E + D894BC1B04A9FB53601949CE3650FF4A4883DC571E812F4489F8E550C46DE9F7 + 3D4817B7885B5D37DF73877F745B68B3061617FE2FA84EE92690E2F21BC3A687 + B869187DDC7C127A1F149205B084BF51EEDF01FB07D7DD1E6452FACF85FC01FC + F73D8459F65F072650EF38B6EDCF6B9F76C7F1CF805CBEB78CEC8527C1FE98F1 + A3971F85BD75DBF493EA136558565E7E204D1EE8C5DEFBF265229189459A08F4 + 860461596A233C457A1FC7BDA0899FC8B3D525266DBDF549323C99BB73F2C444 + A59864D8269472F44EFE6471E998DD1D33247DB85DD6D79A676B9BDBEB8DDF96 + 95EA97EA951F4BB0D6496368272FE57128D789FB224DFCE4695228C5EDEBB24F + BB8FC7CE0E9CF623D2D77ED222812655F7523EF7DB3AE1E4B6A90E01D6C58A3B + 8960C91296655977D685EA10642B8F19D52B308CCE17D5CF69FDE91AA37C267D + 19F730D825F350264F69424C9048602B11EC7C0798AC9513D1827CB57DA8F57E + ECD9202C82593F2693A91FACD7FDA40713C04CFCF677E071E789B8D5EAF7016A + 8F49E3C1AE3E18E2E68B41F441C8A42F93C15652983FF6AC65ACE42F13C7AE24 + C37204574F8101CEEEF431E98101ECB2993EBCF9639EF7FE9530883EEC85BB67 + D9628264FDCB04B0B0006632D6CF1FC304E142E95C4EB21CE6FA789180E25CFF + 000FB60AA6B63D795F5FAB7B6741FEB2052A85228DC9C39010B8B2452413B961 + E104DE6BC9BAC7E7B0A010B01B6A5E70E0B875DF4F92098D8033C93A4784C33D + 2A52FCB9EECE1684919170E3BA828385F530BB90E67D84852EBCBFAFA727E968 + 256C99DC640294C950264D19EC029A09608644000FE1BD7ED9C296C955B9F52F + C7A93CD72311C14C96B2652793A80CC912D82338046E016C55C924A18F208199 + E0652258025B0173BA044E63194E9708E2C1EE8E70F072A13C2F38515BC33C68 + 0C990466529A30D4D61789001EC02EAB29CE16CEBC27B2E8AB0093BF4C32535F + C57858895E677F76FDCC44AED53DB6B048A5B8344E4CB80DF164F298EA776582 + D98574A2FA29CD0ADE379949594F38FB7AC33324103EE14C8033B1EC232C6ADD + 827DE018E04ED7812B5C8349268CAD38FDE0124479618170E505A900BA9E3CA9 + 0D776A83C679205DEB03E9593484AEFD619E3ED6F342E90E34EEC2CD35E9E844 + D7923393D44CF812D8F2998F9DB91F3E1427F078717F5D02D8E297C0D6DA4101 + 62DFE52026DF1411F00E0F865728EB1D08BF70DE43D80F9EA477406410022328 + 9DF4F72544A922A0356B11A18E865F44107C49DE273C007E51C108534741A18D + 45245B64AAA24418A38F43447C0C02A3990C0D862B8DB503EFE74CD7A64F28B5 + AF881656C4EEC15E700FF1826F54208263A3040295E1F05784C38BCAF930991A + 1E21C86D2681DDE9DE60EB67DE1FD9C59F89E5400A691C097C0EAD7B60FB529C + 5DBE5BC78B495976EFCD16D10E9E749FD0B80E75A7B1F3F217C78210F608A0B1 + 0EA2310B869337C979D1FD4EF7B5A37700DC834205D9EBE8C9E3CDE3CC6EC6E9 + 7AE4FA29740B20195E40B411F36CBDEC4AFA0937E37C1F93BE2E54C69DCE815B + 20DF0F54CE8FEE01BE2F7CA90C8D8D7B28F58D09623A376EA156D297F7747663 + 0B18BAA778FF6EFE01C0D193CF375F5774FF87A9849B6A27DE17D93F9C9E3321 + 6211933D03B88506095254A18F458C490575723C12D212A149D521DEAC414292 + 1E4AAD16E12A152116A162DC4310A20A474074083C42F8BE74277D3DE97EF681 + 4F54A8B064F68E88A273AF846F8492E2ECDE9BADA3E9FC84D01805F3B51D68ED + 0B5B4487535D314A04C5D235C33F4544F0B51620E0CBD7165D471E74BDB115BA + 135DA3C3F81AF1A7310E0EA2E71EBBF06677DEFCFC8B8547B8165E115A1A1F35 + DD37D174DF4450D930BA4E14543FEFDF1C02EF483F4102072803A83FC188D644 + 20461B09B55E292C7BB5C92A18533548CF3521333F09A9792698D3B5D0186384 + 6C989AADFB83E0C53F4790DEBEB1BC47A01A9ED1BC80CDCFDB28F8454723285A + 89602513BA0A84B01BF6A858F8537EA83A164A9D8610872826DAA323E14FCF03 + FFB05004525985464BF78901A1710934CEF188D026229AF73A4E6482460F9525 + 15EA1403A212E3114AF7908F82CE03DF978A3084D03DA634C6C19865445AA105 + 594529C82B4D455D633E468CAEC388913528AF4C456A4A2C524CD1484D564367 + 8C8722414DF764249DE320A80C54878ECE73A43FD2B2323163E6024C9D3A0FA9 + A95988A0EB4E131F85DCDC7874742463DAB47C4C9F9A8B2913B331AA2B05D525 + B1C8CD8C465EB60A59192A245B14484C8EA26B49094DB2167166036212F5D45E + 82F8094495A8409C3E1029D991A86E49C588718568EB4E47414902D2A87C568E + 1AB9794AE4E42B50569D88E6E1B9E81C578A9E6935183EAD1AB53DF928EDC842 + 4E235B612620B95483E412152C055128AD8A436B9B098DB56AD457466064971E + A3BA33D1DC9082FA1A036A2B75A82C51A3B5DE88B123723071543EBA9A93A90F + 71282950A1203F4EB8BF4E4AA76B2331889E530188D48511C2119E188DC8141A + B7F404A868ACB5D97AE87312602E5023BB4A836A6AB76D642A5ABACD68E832A1 + B4210ED9A591A8684A445DBB0579E51A145518D0D85E80CABA0C6464C6C0A4F7 + 447E76089A1A12D1DD9D84C913B330636A0E66CFCC11D6D633A7A4A2B34D818E + F6181AFB788C1E9B8AC9D30AD1DE9389FC0A2D72CB4DC8A9B4A0B2AD0CCD749E + EB68ACAA696C0AEB92905565425A8519FAE24424161B60284F414A6DAED82FD7 + 54958D8CE61264779421A9260BBA12334CE5C9C86FCD43D5A852D48C2E47FDF8 + 6AD48CAB46F1F012E4B51520AB310FA6B23424E4A740939B8A04469E05F1B9C9 + 88CD324091AA454C5ABCB02C4D259DB21B3390DBC256B229C8AA3520B94C8394 + 0A1DF21BD3503B229F90873C3ABF96FC686417C7A2BC361115355A14942A51DD + A845D7E82CB48FCA4033F5B56B7C293A2754A26164110AEB339042D7B9369BDA + CF247D32D3299E097D5E0E54A9690850EBE0A560EF19D1F4AC88821BBB7B65EB + 4676EDAA8C854F2CDD97F1F1084AD422589F8060BA264313E89E8DE76D056210 + 9FAA8721271906EA9781FA975E9187ACEA223AA67BD042F768622CFCD491540F + 3D0B941108D2D07D644840A439118A641314493AC4A4A890986380A52C1D96D2 + 7498E99E3415264343D74C64520C22CCB188485221949E2FC189F42E4C50C28B + EE637725CDE1F8471AF67A909088209D96F4245DE9591D62522322251151163D + C2CCF41C30C711E2114261580AB56FD1513EF589E4FC740AF86BE91924085C7A + DFD2F3212A4903558601F119664232E9998848934E20305E059F9868EA4F1485 + F41CE7AD2A62690E101346A0E7BB2A8C9E75F4FE88F0A37808C28DD150501FC3 + E97D11C2D6B06623C293CDA403D59B964C7103FC12E3E04BEFFA90643D024C09 + F0D6A9E0A78F478091CAD07C218CFA129EAC438881D2F46AF8EA62285F852013 + BD7B528C884C4B41489205FE7A33D5C556B269084DCAA0B434041A5328CD4879 + 4604994D5467221D6BA93C8D579201619624842627C3979EA96C29CB16B3A166 + 0BC50DF0A5710DA4726C55CB84AC07EFAB1FAF15A42F93BF9EEA3878C5C58BB8 + 9786F79DD5C0235625BC4BF8AAE3E11BA7B1EDA1AF147BEEB3DB60B69AF56142 + 99EAF4A1BAFC28F4A7D09791A0A3736C4408E9E14FCF751F35978F833F7BB320 + 5D4274343EF4DCF7A7F2912633A2786B04920B8CA767BFDE0025EF179C4AD77A + 7636F4F905D0E5E689787C66A64857989311654EA2F369A13005C15AFE01228E + BE05E89AB711B84CD87AB1AB63BA075CE99809584E633257B834A677A93564D7 + C64C8846523E7D4B90BCA84310BFFC2D62757B6CBDAFA2E89B857FCC22B9F018 + 8A2BC4961B6C05EB1414295C200F6597C8043E1EEA1722AC461D02F8A756DE8A + 2354FCC4CA3FB70EA2F9E9409AC7F0F63643699E3288E64403F9075A9AABB217 + 26B63E1DC8F32E763F1C104AE07D73FDADD6AB5EF48D45726C2DCB96ACFD288D + CB0A52D583094A0E79FB1D3FB1DFEC209AD30DF5A6F91CD7E5134CF5D33C8FE7 + F604E15989E657568F4AECE5C9EAA1C991E66CBC3D097B8519486DF08FBEFC63 + B0F82693407350EE1713CD629B90201A531E578A731A6F37220868FED9D03748 + 844CCAB29EF7BB5B5D28F763C2D6E336112CC90F668B5B3A6630992B91AD1239 + CB84AD88DB88D8FEEC5699D3091CEF2FF2E95B997FE2E59F94A90F8C016E9446 + DF4DF73BD237377D13F7A3EFE47EFC4D4DF36041D6D237FB034CACF2B73BD74F + DFDE03DC781CAD632941227ED9D5F37D4EEEB8CF56467827E3EF7CDBB1B002B6 + 7DFFCBD7042432F73E921724B06DAD41C04E460A855E723941FC1278DD41AC7D + 909C0D124927ADCB486B37F2351C39384D2A2340DF5C12A928A12F3291E312A9 + C92193C072883C591B0CF93A9104299F43E958CA93CA49F548EDC8DB95F21872 + 7979FDB78F498689459B9C0489EC94CA4AE9525A5FE90C7B92543A9674937460 + C8F5BE034C8CB23C9DC7DB70B7C10DBF60B7C79C6F0739617C471EB72B6B4792 + BD9FAE8D7B09BF6079AAF35E81DB75F0BECC2C2387D8ABB9172C6385D8E759E8 + 270313BDB6BAEEB1412A7BAF8D08B65AF3DAE29CCE6BA90369DC86507B361258 + 2282E50430874CB672DC234041F378A308F95810B2FD782D8DF4E535611941FB + 73F1B7CA08A2D70EBDE97604705F106BD692B1131DDBC37E2D9DF1B7F225BEA0 + 9700A6D09EF8FD29EE4132B8B947421F32FF2CE4FCC97F1924F257825DBE44EC + FEDD04702F4725E3D71836B2974339E472BD90F37552FC7F1498F7B4E3477BFB + C6797D412EDF07E444EDBF0B720298C9DF5E02D81EA490BC9CE8B4D4C17F085C + FE9FADE327209D98BEF2087C2132D1DB4BFED2B13C6E85D545B428431778EFC5 + 7B2F13BBD6340141F4DAE2BD69F238E553D87BD3DCED06B47B38FCE826E36399 + FCBF1B3FF78125979197B187BDBC7DFADF42DF0FFB1FA39728B5236FED21276B + E565EE5A8E5E38BD2F2F3A9688D8DEB80C5219F192EAAB9C0429CD962F6F4FE8 + C2A12CBF578E5E98E2E549B88F5EA01C172F5C9AC809B294E24C18DABF9CEDFF + FCB2CFFF4970FD14DEF18297E54B7532EE68C736D193269C72C8278277CDEBA3 + 5E863421B2CA5AF1A3B236C8895DE9588224232F2BAF83E3D204579E669F2F4F + 9370473B3682B72FF492BD76714112F3E4DC068938166E737A09606EFFF6645B + D25510F0A4D38FD26D10FADA74968F831847CEE73D7A09B7895E76F16CFDE092 + DC2C5965981C76A28F2396A5B8206B6F93C0FD87593FFE24481F6B92BB264158 + 737DC2BDB3156CFD2B59005BC95C1A0FB60EA67AEF2480E903D6B6D7AF04C9DA + 97E3925BE821F4813A943E8A8779F981DDBD8AFD7A99A82508D7CF4CFCFA5A2D + 79C5FEBFBCD8E04B1FDB01F4A14E1FF64C040BF2971726FCF9AF73FAF8150B14 + 5456FCD16DB506960864AE8F4960F197357D6CF773A38F08171A53271A1B6767 + 3CE8467A7979095296895FB1772FD5E91C62257E2572D7359CDD8F458AB823E5 + 71BA95F860B908B846F01FF91118164AF24C8429A2055C23D8DD999548E6BFD8 + 792183DB19E04963E5EE86FEEEEE60B7D4BC57B044CE4A56AD0E4C467B30B9EB + 41E3E52D482526E604D1EBC6FB03B3C52E9387244FE3C556959CC7D6C36C092B + 591333112CD5C96052924326C89C7C7D04F9EB1648C77E24E7E3214289F0E538 + 5B9E32F9C821CB3131E61A709B14E66326CCDC83FCE1CAA41FE9E248BAB0F532 + 5BFA4A2113BE82D026DD1DB93FA40313D4AC2313B543BD08DE94E7ED45ED923E + 4C32F93281CD441CE919C8EE8F99BCB65A2D3B783301CC56D3AE22640C7463F7 + CBECB6DA8D40634B63CCF91C77F4F1862B5B0253BB2E54DED9D79374F5245DDC + 695CD982D9C94A667BBA908C273C827CA9CF7ED4AFDB63C32EB187725B6ED41F + 673A6FAE349E9EBE56D29BD2F8BC707FF97CF4BAB5A6F607915E035D9D31D0C5 + 59C487D03992C663305D8F436DFA0988FE3361E90FCF60B65E0D867768087C43 + 830801F0E673E0ED0E174F5778F879C03BC00B3E81DE22F40FF14560B83FFC23 + 0210A0084220C13F32007E91FEE23834360C61AA70042B430921088909B54219 + 8E204518BCD915B12F9D7F1A9F80C8480446B11B6726A3FDE01BE94B21D51F1D + 82C80405A275B1884A5022223E9AEA5408AB60DFF0506A3352ECB92B91ADECC2 + 9CAF5D37BA367CD91A95EE056FEA970F938F743E99946772DE85CE0B13C86EFE + 8170A26B5AFAE981AF7D895C1FE246B29EFE74CED82A98C6D79DF3F8FEA0EB86 + AE13577FEBF5ECC47B54DB487F07BAB6784CD9BAD9890967FE2180F41942CF21 + 0777BA06A91E2690C5DEE3A22E67AACB89D29C28CD91AE4B82A7B338F77CBDBB + D335E149F7035B68B325BD07DDB76CC1CCF7135BEBB3A5BE333D6FDCE939E4C7 + 16B0A1B1D4B730EA53900D743F51FF0369BC62126361C94F46415516B28A93A1 + 4F5541971203439A1A1A5334E2F4D1888E8B42B43A0A91314C540653B93044A8 + 6384D5B60B9DF3619E6EA2BF4CDEBB932EEE41D6FDB03D83A2698C5574FE9474 + 9D87533FD96A9AF7A10EA17E04897B88CBB37539F749FC0811C8F73E5D5B7E6E + 74BF79D0F5EF0D8F501F78D335E51BC5D6D0F4FCA67B6328EFFF1D40D7389D63 + 27BA4ED95DB75738EFBFAC816F441CBCC3A28515B67BA0F53AF009B3EEC3CC3F + 13F830A94CD72487018A10712DFA53DD41D1A150D07824A4E89048D01862A14C + 0847842A88AE4F7F044707202C360411341E7E24EB49E3E0A95488BDB9DDA315 + 7089A0F1080FA3E7345B7187921E6C41CFD6F2C10856242020C2447A68E99C68 + E81A4D40B82A0E614C20D335E845CF1A9F603A5F542E2A3E9ECE8B11E1EA0404 + 29E310CC16C5B17AF82854F08B894768BC1961F12604C4505A9416EE34C66E21 + 31700D628BEB28B1E777948EEAD0ABA1D48643A909464AA61A4DED791835AE12 + AD1D59282D4D4061811AC545F1B064C423962D0E9581740F7AD3BDE48B80284F + 3A7FAE829456AA0D88D3A62296DA54C4C42146150DAD2112695991C8C88B407E + 6114EAEA8D18DE9583CA5203924C1148CB88437AB60EA69438E893E3A1A5F1D4 + F2FEDF062322353A1AC378EA9F1251AA10C4250620AB5089C6F6148C18938DE1 + 3DE9A8A8D020D51209B359099D311AB19A40BA0E03A0A3E3BC8A74D47557A3AA + AB02058DF9C86F2A44465D8ED87F599B6B803A4D0595391C869448A4A6462227 + 3D0CA5F90AD4946B50596E42658505956549C84E5723C518813C1A9BDAF21454 + 979A91658946A2DA1F710473928AFA6180293991DA57D3B98F45983A1E0AAD11 + 610946042424C25F4B7D3153BF522D509A99A857213E592DEEA38C42230A2A53 + 91519C84048B12B1C650183263915E6C10AEB675A9B13066A89159A84746B606 + 26EA675A8A16596909C84C57A1B8508BFA6A3D3ADB923086C664789B110D35B1 + E86835A0A6260EF9F9D4B73C0552B395483046429FA2A1768D3065A6C2929B0A + 13D56DCA8C41524E021233786F6935A20C2A4498E2119D6A402C5B4766A42282 + CE8B92E2CABC0C446524232AD584D80C766D9C8EF4EA3CE4341609F7C6D98D25 + 486557CBA559482CCC40B4C52C2C6803345A61A11866302154FFFFB2F79FDFB2 + 25F775205855CF5E7FD3FBCC93DE7B73BD7FD73CEFAAEA95070A6581020A050F + 822448800468448A942851A2D89C6E49ECD68C967A9656CFACFE30ABA7D79A0F + D3FFD69EBDE364E43D372BDFAB57008DA49E0F7B459CF011274E9C88D8F1FBC5 + 263243915A6DE4477DD40C11BA81F68D4DF4F96E44080F6E6DA1714304E12EEA + 4722AB24EDDE436DD4E698DE427FBD89AD5DB6CD6E1DBB8775BCF2DA3E3EFBDE + EBF8C94FDFC737BFF71AEEBDBA8BBD53BE1BB6CFCED9907DB789E6C680714768 + 6CF0DB3D39C2F1E347D8BD739FCFFB28F6B6F8BD0EF88D703E34964834A414C7 + 87B00EA7D4C6F3A47219B1BAD4D7D7F87D56F8CF2A99C34F258E758DF5215A3B + EC5B7B3BE81FECA37F7848F3009575F601D635C17F4E428795F8EDA53B5D249A + 1DA20BA9BECFF5AA280E6B68ED4A629F65DCEE115DD445E26EB4501C355060BD + 9D410B297E5BF1560D518E2761DDCB6EA42B99569BE3466F0D19F6BF4CB7838C + AE4E603B17445CB39D1DA927E658926C338D6EDB8C4BC9761359BE9F4CAF8D24 + D3CDF53B288D060605BA575927DDAFDE3B3864597650DFD966FFD8449EE34FAA + D932AAFBA31CD362552FF8EF6C707CABE98022C76C91C00DFE4B7A65E6D340BA + DD4361B481C6DE3E5A87EC5FBB7B2CE31A522C6F8AE54EF7BA2C4B1B51962FAE + B14FD739F4A5D69FE8D3CE3121A13668D710EBF0DF4FC474704724F8681B39C2 + 19EC309D6DC6DB19DB3768976437FD2545CB6F5284796630347D3127A9DBD126 + 128375C47B23A63B32EA92937C96CAE418DDE263F5C991767F6C76116E75A0FB + 63434DA9296E330CE3B1AF871BAED609DD0D2BE870818850DD8D2A335461DB35 + E84748AA55646B82E3669263B7247D8B6B1CDB3776901B72FCE63F2041FF34FB + 4ABA4D936D93EE74CCA184F2DA1AA1AB16061CF75BEC672DBEBB21AA1BFCA6B6 + 36F9AEF8DDD06E0E06315C95A6AE49A8B00D2A5B3B06F9E11ACBD83407457560 + D4C77583D60BE18A0E2AF1DF453F4176AD2FECBA42E105D9B5E650F8B0D4207B + C2AD64B9FE609DC345F69332FBA9C8DF12BF2791CC6A8FB42BAD6A494E3DFB32 + 2244CB584DE5B12CF704E7519CA72C6BBDC5B9AAD6253A1C2BD3B5C7B8468912 + B273CEA4351BD7582B8CEFB7E42AA135D675CEC50423993A9644BDA683BD3283 + 96B074A1BB661739F75A8E698E28AD2E39CEBD2459CB3C341FD49C8273A408E7 + 5711CE17A56125CCB96120CD3500DB31CA76F1B3DCE610B1E685CCDFEFE41987 + FF7F6979A11910B1CD32FA457A3B7A963BC7194958B3CC6A132BC92B98721BC2 + D625708D8A646B57F955769659E55F90142FB128C95BC210AF0C2382F412D7BB + 224EDD3A270CC9EA4AD1729D6D4CF9736D1C90D62269E112A94AFBF830B4B173 + AD7C7DACA16B5E12C063A25684B0D213396C085CBF8861971C36A0DB65C67D69 + 358417B9967F49EB7AADF32D514BBB31F97C85A67533EB7FC2FB7C690CB95978 + 09611BE7B20E860BE37D06BB7720B8FB0F1E0253A4D9D4DE8777BF46F6697F99 + D37B27D3D2A5965414F42C77ED9D5832D884E1B3217FED5E10C39A304A4F7114 + 6EFC2C4CE76DFDE56EF7692E84276691A8766FC75B3F6BF7A62BF38A5117EC92 + E4D6CDA66F61D3B09815C662DA4FCFC2A45C1E4CBB299F171705D68BEFD04B00 + 8BA47D2A4CF8B139B65B12D6EBEE757B7145DF45CCC0753FCFCB422A9D2FADEA + FB1251CCF7FB85700965C1E6350DEB7F89B8CCB617F9EBB63FDD683704F098E4 + B598EC777AF63FE52EBB4C7F9C6B0FAE09644A9A5B57055EBA4EBF0537BE37AE + 85DD53F5EEAD9A6F659C97DCBD7BBC5F844978ED0D0B33F6AC2D2EEC632BCC53 + F6DDBD84EF2C7C3E8E875BF010B84F237F2FD8155FA4F118B21BB7B1FFD37091 + A7F86221376FF8FF5220BEE925BE83972EB3EE63ADB72E3C1CD4142C196CA0EB + 4D0D3F76CEBD595E6DC29D3D0DD37C9D7DFEAF062E477A011EEEF457829770FD + 87C0B3C85F0B1BD654D8FB02FF2B844BF27E1EB3C278DD0CCC4B1F7F0886E09D + FA58C6A4AF85FD48266EB33E40EFE0F08C931A7FDFF00E545ECC0A3B0BB306BD + E9305EBFE787D27C369E49E0CE80C8D589DD13573F16F3E31A8799FCC8BC76E2 + 73E16681FEF687679F2FC0134E3052BD6373A6FFF8A7E9C5F48F7816BC3F79D9 + 9FF673B77E9330F667FEB930B48FDD0D013B0E6FD3F79AB3E09D64CC0A67DDBD + F0FA4F4F880CC613483389E464D74EBABD93606F180BEFA4D54CA0E966E37821 + B769C8DD9BAEB55BBF0B61C704AE8571939FCA288C0961E3273BDDCCFD1EE3B4 + A7D3F3427EB6FC6632ED71F742616C382D504C7DA760162F63E2D792BF9305DA + 98F89554B085D4451B92D6A88E76257805B320E3E24C6E8AAB85A116A45A306A + 3138B9B3878BD32B0227DCE6E42E716D4512BD52F37C2ED52BC83E91E8F54064 + B020BB48E1391F17B25C944A25F41217D3867889BBF7801A92968B779952E965 + 9F0DF91BD349719D3C972430DDD3920676D58D2DA4B9E8A69BEEFF9D673A3A71 + 7E5D6145222BBE05FD1695CF38CDAB3A611D609BFAD99E01B64F2864363A5C02 + 58484C4EBE2F67F35812692BE9406DDC48CA579B0B527D5AAE9A8D1CB361532C + C15F2AC3478800962912D85F2EC35F54DC224D91C539A342DA972F8C89E0A4C9 + 773E913052C8BACB565284BA7B7842D84A4DAEC8529D7C67D8E5983604B439C0 + 72D35C88883C95A4AF0863970016F92B75C8130298F593D4A585A414454C0A22 + 81C2B98C21818CC4A3C8BF8CDCF83C960A8EE4A53A39672467C359857389E180 + 217F5DE9614322651D44D86E92F29484A5CA6DA0FAD15C89B39C2CABAD8F2B89 + 9C3544AF485E95C5DC47CC76D1B3A48145EA8A1876C92A91E492DA9534B0545B + BBE17D0C2FBB219025BDAAB432921C96A4B0D29244B40842A6C1BAFB55969888 + DA3056A2212C8703EC97012CD25C0CFBB144AC468308880C9754734212D821AC + 4482ECBB2270192EC87285992FFB9B21BB232C6FD895CA366AACA3AE14B62B99 + 2C323E64C869DD997C2DE0C3359FAB427B2EE03790EA6C4B548AB4F49A7E9197 + AC6344EF8A7509B0DCBE50102B411F7C2C6B301640241926220889A48CF23913 + 413C1F473417433813E63BA13FEDC992C82507B9469E901AE98C2189A54A3A5B + 2B205DCE326E12A174CC9094897C11BA67385D4B21DB64BC5601799170FD1ADA + 9B3DF4B607686F7451D4DDB3A5AC21A8B3D58221373315A981CE1A32D99F8821 + 9A655EB50A72F52A5131D2AC8566957D8BFD807D28C47725723EC4BE1C609B06 + 24A94B538705568DC97EC2316195DFB9C8DAD5984BB8AE8AC067BDFDA908EB98 + 44BC9831E47D447D977D4688B26F86330A2F923E3C564FCE6F272C02557D95E3 + 03ED8BFC7656237EB6B10FCB511FC32EF3DD11E1153E8B1876BF0FA3925B642B + FB7088DFABC86B91CBAEAA76BE3396557742479D12EB54625D724486EE2C0FBF + 8F3CDBA0B739C0D1DD43BCFACE7D7CE5A357F0EA576EE1C69D11368F1A58DB97 + D46311F58E837AB7805A278F42D5419C7549E41DE41B35BE3FA9D77610D4810D + 7D37CCDFCF360B70BCF3A7B4B1CAB1275966FE15B6678175E5B819E2581B4DB3 + AFA9CEEC5FBAEB3B11A219E477CA3E9F12991CE077B162CC4836CE7713653E49 + 43D226CA9204D6F7C5EF5F6301DF698065D25DCED15C0DC96293E34485ED5364 + BB1798B7EEBA96B47A0631B6593C9F3612E9F1720AA94A1A996A967D8B60DD74 + B0A0D4ABA1BED6429BA8774AC8B37FA64B51F6DB08128518528CA7B0F1928304 + FB9824E562CD06426C4FA98F94D606A9E88E14B38815758043FD38896CA58F4C + 790331A7C3F6AFB33F54E1542AECAB65A454A65C02F95A1E956ECDBC978E5460 + B7BBFC2E1A70EA2DD69BF994AAFC06FA2EF9DBD946AAB1814445047017A17C8B + ED51E3F85D4594FF894499DF00BF29A7C97A9683A87462B871AB8DD7DFDEC79B + EF1CE2E55736F0F0E110F71F8E707A6713DB3736D019E9BB88B04C41640A0124 + B22124D9B79C5C03B5FA1A3ADD4D74FAEBE80F3BE80F0AE80DE218ACC5B0B3E3 + E0ECAC86D75EDDC2FDBB6BD8DC2A63FBA089AD9D36BAC32AEAC33A6AC3165A22 + E9D647A80C86288B0066DB95F8ED57DB518677F0E48D3EBEFDED33FCC68F1FE2 + 83F7F6707C50C1DAB08CF690DF6A83EFA71447AE9240B557C0F0A08BAD5B1BD8 + BCBD8DCDBB7B58BFBD87CD7B07C66CEE74501DE4D11A382C670607DB39DC3FAB + E3DECD16CE4EBB383AE86067B3827E378D563D8A7E2B811DF6F583ED2ACD02FA + 9D241A8D3836361B383ED9C1DEE136FA2CBB24E00BCD0E0AAD21D20DDD775947 + A2D3319299A58D212A52E5BDD64473BD811ECBB076D0C7D609DB6B575A0F4A6C + 8322EB9E477DC43A6DEA004B09ADB51C06DB456CEE34D85E1DAC732C5B1B5459 + EF0291C7E65A06877B05DC3C2DE2F424875BA7253CB8DFC6299F47EB51740711 + BE37876D54427BBDC971B18942A7C1BE5341A59D615BA5996F9EED5D408EE366 + AE5B457ED446696B88FC8654D70E911CF690D17B39D843797FC748C396B7D78C + 4AE0DACE00CD03BEFB936D620BEDA34D629B10E1BB6EFA7EACD1322A6633066B + 4875FA88B74444760DB997EEB690193690DB60D936F8CE375B70D85EC9F53593 + BFC84BA7DD446DA38FD1C12636F747D860DFD9DE6BE2E4560FEF7F740FBFFCE3 + 6FE3CFFEC58FF19B3F7B1F4FDE3EC6EE890EA9B09DF6EAACB7BED936DA5B52AB + BE83EDD3631CDCB98B8DD39BE8EE1DA0B37DC476DF40B2DA3624553057812F9D + 87B4B548524F73AA80E6589C0B848D7605FE73386FD2C12721467BB45230DF7B + AA5E47A6A97BD107862C15012C822ED7EBF17B6C235E6FBA8472592A71459856 + 182F8F443D8B2CC793429FDFB0C615BE6391BFE5758E33DBFC7FADB7916A5711 + AB9710613E61FEBBC2D2DC52D13DF0D22CD045947D2ECE3C92CDA6210A336CB7 + 4CAFCB786DE6C96F9FE38F0EFBE9004A90F5501A518E4BE10A9FCB45A6C37181 + FFBD64A3CE7ED04683EF58F7ACF78FD8466CB7E6CE96B983DDE9B410AF496B4C + 1121D6DD22C234E2B512D2FC6FEAAEF738EB15E35818AD6468E7FCCC10C47524 + EB6D64BB7D14866B467A353764BF603FC8B08D44FE46EA35735846F58BF23F12 + 6BD6582F8EDDFC9FA45A75F69F73223C54E3DCB5946778B66367C03436916CAF + B35E03835863C834986E7BC8BEB74E7F7E9BCC5B52AFE9EEC010C70949ECB67A + E66EDC685BD2BC52EDEC92BDA146073E49B5EA8E5B3E0BE166174111BBAD0EA2 + 9D1EC3B488B68913625AAB2CB7AB7142073225052BAD3C927675A53E03FCFF84 + 458212018691C46C84EF51EF5212B409A6956A7569F6F94E9887C8E42ADB806D + A7430429BE5F1D2A308707FA7D63A6F89D25D86E49D34E42DDBC2311F411BE6F + 415A41E29CDFC445DC57D97EECA70942EB84954C0ECBD250C435852576759FAD + B4045D247E5DD5CBEEB532E704B00E974A42D8AE39B426914AE6651DE472B8B6 + 10D199653F1B139C7A5E4C64B986D2E157AD895C225852B6BE7481737F49B1BA + 076FCD95349C63CD73CE3AAF43899CA3EBD9BDA686EB213EBB26E7D8D274C3B9 + C372DC611A2C67D2558B2C15C452876CEFBE9DA84116591A491922749EA6517F + 2C93EE228057940EE348BBCBA20E126BCEAC750AFF7BD2AE62AFAB89485B0ADB + 44D777E8B09DB4B6E8E0ABD63FBA366895638908601FDBC3688232DAA05CC95E + 2F09BE9A294C2077DB3E2280ADFD9CBC76C95F49D25ED6DA976B6549E3BA04AC + 0BD5CB48F47AD7D16318D295EEB29BF5F664DD1DC155AE8775F591175E12788E + 795A35CB56E2D64AE87A9F751FEE9515979435E42DA175BDF231E42F31216FC7 + 764BE05AC8DDC2BA4DEF397861DDECFEC345B2D735BDD07E8E0860BBDF619F27 + A4F0D8DDEE894CBBCB6EA1674B565A625776B9197257FB2C1E3FEBEFC584E054 + 38C29BB6356D59F4EC2DBB30BDCFE38DEF4DDB9B9FDDF3B17164DAB46D7D651A + 15D7CA6F9C9E8537DE2CBBCDDFEB6EDD44864B6DF6554FFA5EF2FCA9F010AF2F + B1CF7E9E608DCC24536741C4EE655FDC98E7CF2E2CA96BFDBCE97AD3B0F1CEC3 + 3D1DD3F1EDB3172FC8B4719699B695FC9D8251093D26639F05BB372A02792190 + C655C63504E675F695F13EA9FCBC613FB76F3AC6B49F0D3F0BDEFD60B3073CED + 3E45004FEF5F7B9F2D912BD3DABDB0EEB3FCA6FD6711AE82DC2FB14C82D7ED42 + F8B1F4AFC17392C09F4BE33930CD877C0ECFC9FB3C575ACF0D96CD23F9FB344E + CBCB634D4B08BF70C9C395BDB440B78B3CDB050ECDC24BA4F2F9A9E1046FD87F + 6C5C28D3394F7ACE91FE9AB009FD43E1FF6C04F0F3E0D91DF7E2877101BF2201 + 3C79FE47227F056F39EC80F565069A2F13C71BF68BA141F6D9F8B204B017D304 + B0F76773F1077311DE9FDB84AC15BCCF4FF31FC3FE14CDCF90B0F7FF7ADDBD61 + BD710DE4667FBA6352761666FDD8A7C378FD9EE56FA58D65EA0E0BF33CB67B61 + F29469ED849DBCD8E767613A3D0B4D900CBCE1C793504193E5C9A94B91ABE3C9 + B3C1AC492B3199408FE34FC28F6127E1D3909F9D08DB09F0741CFB6CFC8CD4EE + 79180B6F9AD3712CF4ECADD774FD34F1B78B129B8E3D216A170976B26D27E49A + A87BEFB8B9CC09B85D7879176B76F1657169D94777998491046659097B87AF91 + DEE5E2CCAA90D6A2D43DD5CCC52BCD2B01E6A9452227E4EEA252642F179B9CB4 + EB8EE0F980085D427124E9CBB0920076EDAE29C2D74A042F7341EF2EEA93268E + 0860430273612BA265299A3427C92DE97B4EFC9E2FC4EDC27C918BF705865DE0 + 225F84AE08DF79DA85059D72CFE427CF8B261CFD690AE6DE29B3D9483BD33FBF + 6B89F58BB00E9128E6B5B9914C6145049193A1A93498779A798F89606DC6F873 + 45048870417761EAEE2D77E366B5509C90C041B9955D4960B91977490E572A08 + 988D4186A1BFEE07964AE815470475CACD9FF9491258EAA0A5165AEA918D6A67 + 43244ADA55770347A0BB71AFFA83B8160AF3FDB14D255D2B494BA5914841F7E7 + 8A049669D41F33BD55E6610960A52FE8BED2D531B11A724496BA92B622586345 + A9FD75A56DA3859CD98495C4A3DC8CAAE28C2BA1EBCF2418C72584FD4C4B92BB + 526D2CC94FAB6AD7DC872AE948E6BD622430992FFD42BAF7B628B5D4AC775224 + B64BF48AAC3564A98859919DE332BAE5903A5AC5718960D9D322822A25633775 + C9EAAE5C491A168DEA64496B2A0F23556C483211D131ACB27D5645A486024490 + F600E133C4EA4AD08F55964130FE41F905E18B880876EF3A9614A92123F98E96 + 234C4FE49F794F7C2763296CD56331CAF0AC874C61211280EE4C3608D34D2AAB + 05DA57F88E553E43E61122805798DE2ADD7DCC53E55956D9589E65BF0F8BBE65 + 2C0756E08F0411D4BDCD213FDD96B01458C24A78C5603542FFB81F916C14C962 + 92882363C8B714D2952472F51C52BA47D889222CC9CF4418C16484F6A491D4CD + 94891AC3D7D3C835F328768A28F5CBA80D1BA80F9B2877ABC8EA5E5991AEC918 + E34710C9C49114712CA28E66349B405C2AAB2BB9090ACDA29148CE35443C3B88 + 4BE2DA4920C67CA3EC5311D63F20295E53FF984B08AB0FCB8DED1C64DF8AB00F + 065321BAFB10CE844CBD8A6D96AF5D46B9554259A688E65A85E573E08B496B01 + DF81DE4D30CCF71883952E9E0B84B97067DBB20D45ACAFC88C85983EF363DF5C + D6F7A40317FCE6FCFC7EB41969547AB3BF8BDC0D8AE0657FF0714CF5738C0B44 + 587E8E73BE10DF9DE230BE8FFD22C5314612BD3B4743DC797480C7AF1DE29537 + 0EF0EA5BFB78F98D6DDC7B3CC4F1ED26760ECB186DE6B07DD0C0EE510BED5E11 + 4E2183743E836CA9881CFBBC53CA21216295DF6F44870438C6A91C3E8E9D4B61 + 8EABC44A88CFBA3F39C231570770426AD3844BB6AB9FB1DF0811B67D2ACFF7A0 + 832051B6672AC87748B7421C69F69B2CDF51A694E17B4A2296653F62598C443A + BFAD28DF4394F9C7F8DEA2C9A8494352E9916408E14490CF613EB3FF656388D3 + 1472B50C4AAD3CB222677408A15940A69187D3C8A1D429A1AAF72709E1AA03A7 + 984282694ACA3D21B5ED198D5359C438AE6AF35EE48BD4ECEB8EE608C7DC5821 + 8FA8C2B14F477598215360D9CA44D61CA4D09DD931C787647E99755864FF58C1 + 68278B5B8FD7F0E035A99D1EA0BDAE830CFC56AA71F6FB2C0AED024A5DA9796E + F05BE8235E96E46F13A19C88DF1AC2F93AF3AD31DF02C7534989A790AAF21B2A + 84F8CDF9D0E827B17FDCC0D9ED3E4E6EB671745CC3C15E0DC7871D9CDDE8637B + B382662D8C7A4588A35E4EA15C70502956D1AA77D1263ACD1E3686036C0C1A18 + B41DAC0FB3D8DD28627BAD801B3B759C1DF6B0BB5DC3DA66192D736840DF39FB + 8CDAB05E641BBBF73917DA6D54DA4D546A79D4EB11EC6C27F0D61B5DFCE68F6F + E3F77FF7657CE3831D9CEEE4D0E7B75E69D7DD6F96DFB2A0C300098E1DC54105 + D58D2ECAA32E0AFD0E6A5B1BE8EC6D736C68A3C0BCAA7C97AD5696E5CD617F23 + 8BCD4106EB52F5CD775BE777DAE098522DC751CA87D02847D06FA7B0D6CFD19F + EE6CF7C1B08C037E237B47030CB7DAA8762AEE21952AFF4D1C53FCF918220DA9 + C1E537D0CC20D3CE20DFCD727CCAA2BE5633C47773ADC37756E7FB2B23CFF8E9 + 7A9E7D8CFD8DFD4EF750378625B4060574882EC7B346A78E5AAB822EDBB7A97B + A36B0E6ACD9CF9F65ABD1C3AFD02FAA3323AA392914CCF3532A65FD419B7C238 + 5201EE48529FF914395E9659AE92C2B0AFAB8FE7E957E8B590EB8A1CAA23DE68 + 22DE6C22D1EA20BFB68182D4E4F606C8F7070CD383EEEED65DDD2205733D8619 + F4505A1BA1BEB9C5F65F47C290923D645A6B48B7464835864836FB4C9B719B22 + B95A88D5990FDF79BC2D09D93AD2AC63BCDB469469258D846A07B961079DFD0D + 1CDE3FC6D1BD3DEC9E0D70F7956DBCF5B5137CF8CDDBF8AD9F7F05BFFC938FF1 + ED1FDCC7AB6F6EE0C1AB7DFAB771E36EDD1C58E9F2FDB4D73BE86E7631DC1B62 + B0BB8EDEF61ACD0D8CF677D1DA1CB19D98BF8832CE25C2FCAFACF27FECAAD6E7 + B8C9399751B12F6D170C132FF07B65181DB2D1551552551F35647091F310FAF3 + 7BD71DE722EAF2FD1EDBB4EF12C0D526626355B9617E373113CE1D570ADD322A + 7A4FEC1795B526AAEBFC77D1ACC8A45B56AAE51B4CBFCAF295F2868C8E70AE16 + 137958960ADF16DD2A2C0BE715B52A926C579911D62720E94411549CD385381F + 0CD34D7611599A57FA392709737E6254EE57CAC875DA68EDAC993B8ED74E37D0 + 3B1818E9F5425FD29C0E82ECDF417EFB7E8EC722C6FD4C2BA87439BE89E44DD4 + F83EAB0D4434DE710E1A619B88308EB0BE1651CE45E3EA3F6CA33491A0DD5C47 + C231334288B48EEA99ED2D82395AE61C4F732911C24DD6ABAAF92AF32D335DD6 + 33AEBE2412BFDCE6FCB5CEF2885815A1DA21DAEC5B525DDD22D4A7D9662237AB + AE647788F365A9E88FB7BA466DB351BDDCE923DAEC98F97150756AD3BDA3FBA2 + FB88B57447B47B4F74ACE5C64B747A88B21E41965984AF213E69AE66A586D825 + 415D294F49C5D2E45CDDE7E4CDE1024985FAE9BECA67118521DA8DCA6442D2B2 + 61B6ADFE1B51BE6FD376A6FD44B2B32D387710F91EE6BF5677F507F8FF0D4873 + 09DF8B8F735877CDC0BEACF930DFB3DC74C073957DD7C73EEB1BCFF3A5596851 + F37BFAE920A80E2B85994F807D7D956999F084C20AF6B0A88FE5B7D7CDC8349A + 903259B89A92CED74F46C2572438A1C31556AAD5ACA144F8725EB092603E2996 + 87E197394758E57CC19F62793887D10152C16AAFD1552482AEF208A675A88C75 + 92661603861F4B104B027621EA4AD18A18D6B359BF4544D04ACA97E58CB2DE22 + 7A638C2353D2B352FF6CED9CA7E8709AA479E7B5EEE1FC4A07556D39DC358408 + E3F161409641DA8FB43E1221BDAAB524DBC3DCA92B75CA84AEFCB12A9A05B5C5 + 644DC9F6B044AF75B36B4EEB6E6124810943F69AF5AF2B4D2B58227642C88EDD + BDB06BF6AB5A73CB8DE144F09A6B90641296FCF512C157B9AED69ADFA6A3F4BD + 04F0D555A5EDEE15D870C69DB842BB60DD2CA96BCB22D8FD04BB5730ED37C1D8 + 6D5638F33C268067C13D2CCE728EF762647AD5415BBF69FFE9385ED8FD13BB77 + 6249D65904B0F59B8E63A0709EF4277118CEEEF54CFCA6ECDEF4CCDECD94BB2D + 834C0BF97B61C398BD9EB19B378FE9FDA6A7416D75A11DC7F16597FB359FFA6C + C2ECE74CDA7D0C53F6312E9447CF5EB2977DF19CA8F5C22564BF08227FAFB00C + 9604769F5D68CFE99CA83D87753B87C2CD267F9F46F01A92770A933853715F12 + A6C85F4B007F8E049624EFD84DFB9D32AD74B0F664CD7EA9F602AFFB2771E47F + 759975A769F76E6D588B59EE5E37AFBBC5F47EB0C585BDE26BCF86DDBFF6EEA7 + DB3DF72F036F7C2F193B6D9F266B276EB4BF207809E031096C08E019713F9706 + 2172D986791AC7F145FCC7B49F37FC34BEC8FFCB81F9FD5D12C084E5CFA6E10D + F3A5304DC2FE63E24299CE79D2738EF4D7844DE81F122EB13B054FA12E86F3BC + 98FF4631ABC34EDCA63E8C2FC2E4C399F9F14DE11F89FC15ECC0338D596167E1 + CB84F7A6FFC5D0C0FA6CFC4313C0226A8DA4AEE2CBB476EFB317D6CFE3AF1FA1 + EE41B03F45A974BE4C8800B6FEDE9FE5F4F3049E1FEFD330FDB39EFE617F6198 + 1984EF2CD2771A7632E37DD624C71BC6BA7BC34DC3FAD9705EB8615416176662 + 2E72D4637AFD2761C6937761E2A7B072A7DD4E78ED04DDEBE7850D6727D2368E + D7B4FE9349F3D8CDC29B9E179F8B43375B0E6F794C1A84B78E8299645BD87464 + 72F2A909B9C8574BC0BA1373FA79165C5ED8C5D895559DEC15B8789284AE81D2 + A4DB62D0DCDF6B55321B772D0AB590E54257F71CD93B832E7361A949FA950017 + BC9CA05FA37DCE2F02970B792DCE694A1D954E26BB0B58D72E55555A64CABE30 + 3ED9AD45BD16F7B2BB2AAC18866534AAAD8251B33960EE52321B133A89ADCD0A + 77F13DBD5017CC49EE940B11C186F04D6B23441B22BA935727E78BAE9D6E227E + 05F794BCBB2162258B8D74715C0B7F177A96CA696DE4F8B3DA44613C6D268CA5 + 8E97F8BCAC3BA79886361974CA3C2015633A8D5F2C4FC8DF50B56636BD824460 + 4C040BEEDD64DA0CA3498824D6E97FA985D6A6CF4A96E93ADA48723793969352 + 35C6F69234A94EE327122C2717FF9130DF1517D522804361E3A793F9E6D4BC08 + 6011C8998C1B3ECAF711517C91C30993A68591004E9D13C1225F5D023606DDCB + 6A8955992253658AF8F5BA5977FBAC7B7A45D40A227725E52B55D341D6C9DC3D + ACCD51D64D52C3CA4376371D57D258E4F36A9A654DB2CC8930169361AC646208 + 6413460D73A29C43AA5644BA5A3012A3D142C6205ECA21D7AAA1DC6F19335397 + DAC832ED55E4F99CAE164D3893AF516D9C84D42B4BEDB088D600DBC71F8B1804 + 05B6B1A46B45B0CA3CB707E0A75F40618C3471CCD4F15CCDB5C85A6D4CF1392E + F25E2470C04044EF3C713DB88AB9908FDF86A42D75D7B12457C3586298B9808F + DF991FCB91902997A426A502DAA82D16212CC23ACCB09216665891D232E75797 + 0DF4ACF22DB3AC732B4B749BC35270D110C0FE981F3127824C394924902C4489 + 1821B7380A0D4959C6114A320D860DC4FDB44B9A3886703261C8BCB824291926 + 53730C61E734B2B467912AA7912EA60C740F7128C9F2C77CC64CE6E248159288 + 4B829448D32EF5D4C6CD10812C03E33874CB57B3C855453231DD828394246C53 + 51F8A322E7595FB6918F6D1534EF90E54A278C246CAAC4BE9A0DF37995E98590 + AFA751EB97D118D6D0E89551ED96506997997E1E09F6B37022EABE3FB6E7B2D4 + 78F31B139675677590EF234C7F91CD6CCB603CC232E78CB46EA2903724F4BC3F + 84399F087B4990EBFDB3EF72EC0A270A88260BEC430EFC1CEF5638CE2D733C5E + 659A01BE9B10CB1F56FFA13DCD3E5D6F17B1B5D3C48DD33E8E6F767072BB895B + F7DAB8FFB887BB0FF87CB38ABD8302D6371D1C8934BC39C0DA461D15A9332E64 + 9165799C22DBCAC061391DB637BF13278B30C7283FC74F7FD4C10AC7EC455F94 + E58821C2715404688CFE719A497E9B29229D75902F17D0EC36D15F1FA0D96B22 + 5F611EE5148AAD1C2AAD3C8A7CEF79BEA76C316DD48CE7F89E4AFC1ECBFCBE8A + E522727A670EDF692A88486C15896410F9621CD97C14894C807D6815997C0885 + 4A12855A1AC5BAC3F75342A3CFB87C4EB26F4ABD789C6682F94A6D79B6CC3E51 + 513D3386F895847BC249301D8E47FC9EF5ADE96089B90F90E39EC63FBFA48372 + 3ABC9263BFE0589609B11FEBBB65FF8EA75977B651460434CBE8CC2391BB8C6C + F52AEABD25EC9F66F1DA5737F1D67BFBB8F9A887CE4612E9F222FBD8324AED24 + EA431D7E70CB17CE9611702ACCB3CC7193E378AA8860A68248B662546CEBB089 + EE1797DAF44C35865431C07A06D11EA43158CFA127A9D15E12ED561C1BFD0C0E + 37CAD861FA83461443BA8DDA0EFAFCC6EA7CB725FE6FCA4E1125FEE3AAB922BA + F51AFD4AE8F19DACF7738C9FC3A099C2462F8793BD368E8FDAD8D82EA32BF5D3 + FD1ABF818AF956C36A4327C5BECC36AF56516DD4D1E418B9B951C1E3FB2D7CFC + C13A7EF8FD43FCE6F78FF0D1DB7DDCDCC9A2CF6F27A77B35D947A42A3B27B5DA + 7CF7F1A224C2F9DCD47DCC15C42B1C7B1B6DE43B63A9E932BF3DFA0D074C7F50 + C6A8C5BA56A3E8347268D58B68B11F779B22821D141C7EB7D9006AE5089AF504 + AA558E498530EAAC537F54445F92BA8322C729F6D50AC71B8E39EA2BE16204E1 + 721071B659BA9D80C377E3F03D65EA71A4D9A7D21C4F8A86FCAD20C172EB7EEC + 04E1B4D81E8D22FF0F2CC75A0383511DAD5E0165FE372ACD16FB7B9B681A545B + 7534BA2DFA5590E5BFA6C4740AEC93C55A89E529C3E1FF4892F1F9561959A669 + EE45279C661E258E4555F6F36A9DDF50B3C8F42A28B1BD8BED06C33759863AD1 + 429AEDE6127A4D248854A3437491ACB710938A64CE17444AE980438CF614E364 + 258DD9EAB3DD19B6D643B6BDCE7A8D90ACF61846446887697610AB328D9A0839 + BA35A53297F656036122D46921D1EB20DD6F23376AA17BB881A3872738BABF47 + 6CE1F5F76FE11BDF7D19DFF8EC1E3EFAE619BEF6E13E1E3FE9E0C1C3065E7EAD + 8FC7AF77F090CF775F5EE3B7B38DC146179D510D6DA2D6ADF29BAAA1BBD131A8 + 0CF83F6E1490AFE9DA810ABF23DD655E316ADA633969894899EF39AC8313791D + DEE037CC6F5957022439B772D83E428648E8A047A1803091607B64DB1D645BAC + 2FDB2A5EAAF33DB33DD98E91BCC85AA981E7BCA1CE71937DB9CA7AB6B7CE3557 + 9407BA9B9C7306BEAF34FBA6EEF38E5744E4722CE1DC26C871362482B5C8B4CB + 4DCE45F48D330CE77C09BE1311D2AE6A5A87E03C83A608331D0251D9352FD25C + 47A4B7C247392E85F92DA90E850EFBDD36C7DC83167A7B2D74B6D9EFF8DF4AF2 + BB96546F9463B03FCF76E13CC9CFF6F0335F1D400CE638D7D4419352D3485387 + 38168559AE68B18218E79551118BFCC6C32CA325334510875856C1F4237EFF89 + B114ABC206F90E421CC3A325B67D39CFB82C7B8EF30F7EF392A216D92D423428 + C95B43B2AA0CAC3FDB3CC3EF4552B3224B5D2959A936E67C57F363CE937D225B + 45ACD22DD6909A6C4904B3EF295EB7CB3931FB37C70C490F17753FEFDA96B93B + B7B0BE01A73F40BAD7372ACD8DB43B2175E1AB6C0B49746A5EEF12A0FC07A4DD + 360A49BA96ED2509521D2C9029AC6A5DC035C032FF03AEDA6349C472BECF7582 + 9FFF9080FE1D79FEC36D5CFE1BDDB8FCB770EEE0E35C556DE2A7E9A719E27F38 + C2B60AF11F2C37699D71FDB846605C91B8E7F37ADA45FEB29FCBD4E14FBD87A0 + C8DFB1BFAE8809B0BFC9D4B34F79EB10014DB3FED03A84E596D4EE4443124DD5 + C910C05CAF987A114B09BA738DB3C0398AB95E4761688AB85D5558B5019F45EA + 4A9A369C958A658279EB1086246F2DD4C763FA06CCC126F5731721AEAB8C243F + E71596F8D5BACF4AF84AFA57CF2B2286391F71A1BC199EE60530DC9289C7F5A1 + 0E4F123A44296D3A9A4BCBD4B30EED5D80E66D11D6916B581D2C9EE3F375CEEB + 74DD8F544F4B65B35DDFBA442ED7A22CD7C44D6B59AE3725B92CB2D8AB02DADC + E54BBB218F05D5C9AC65B946F790ABCF82596F8FD7EB5E12779EE92F1A8D335C + 331356F5B30860BB96D73ADEC6B5F10CF9CB740C29BB34DE43D0DE01F3511C1B + 66BA7CF2D77E811B6EBC3FC034E4767E887C4CD8D2CFC6136C786FDC0B6EC2CA + B3E0928EDAEB3084A4F638C66E5EE2D2426ED64FFB24D3FE5EA272029175537B + 2A0674337B4AE3BD978B71A6C28E31BD5FE3858DEB0D6F9FDDBB728390BA64AB + 325977D19A7B69C7619586CAA4B4ED3E984D5BEEAAB36D13531F611C7752EE29 + D87D234BEA1A2CBBA6DC45FECE05D8E745BA1ABFF378326DFAB6CEC6CE327B55 + 2F5B02D84AED9E4304EE454C93BF2259A7DDBC69C96E48D965865D66FC958431 + A7217783E5CFFBBFB8C4B84BB413B20B2F30BD69BC48CC8AFBC2627402372D96 + CB8065F790C09377C536F2EE771A829761AD84AF15B4D1FBB36A9FB57F6B8960 + EF9EAE97CC9DE5EE75F3BA9B7C683E6DBF78B2572CBFA97D6ABB773DBD873DBD + AF6EF7DD9F1717E3CBCDCB1BB8F012B55E376BF792BE06CF4100EBD94A150B97 + 59671346FCC698C39154B18DF73CDCC72C7F6FBC2FF2FBD5C1F43C04F039097C + CE615DE0B18C5DE1589EE72080BDD7AB5EE0D13CE1ADFBB4DB04D324EC3F262E + 94E99C273DE7487F4DD884FE21F1390278AA5017C3795ECC7FA3785A279D401F + C27312C013CCFCF8A6F05F00013CCBEFD7C20CA966EF40F6C5D020FAF7872F22 + 809F7651BD7081F095DDFBEC85F5F3F8DB9FA148607321BE87009EFE513E13DE + 1FEFD44FF959F0C67B5ADC899F87ECFD3204F0343481F112C0E7139D8BF0C699 + C6ECB02A8B0B4BEE5AC8CD4EA2ECC4D3EBE78571232CA96A6109592FBCEE368E + 35A7FD6D3A1726DA53B061A7C34FC31BC7BAD949B63B511CFBC98D9369638EED + 66123E36ED84FD9C00669C15E6EF91F4B50B3549F3CADF2EC62485EB828B5AA5 + 4137B3E8E4424404B0A47545029B45DD78D12B955D57B8787691C4E550920BE2 + 1417C65C2C84525C0873316F4F66D37E9D6E7352ED4C7F2DF275A27B918B66B3 + D8E7C25844AF218509F9DB8D00A336CBCF3AAE06592E9629A845BE484A6D62E8 + D4B54E639F9FD4360B6E2DCAE3EE0976FBACB04B2284E30C4FFB5C82F9EBE4F7 + 98E85D7274425ED200AE7A344B0E1B0237ED9E963724B010773751CC89716DA4 + 1092C25D4EA5B1325621B690CE40F70FCF2799575CE58A43D2C33A616E369218 + C6DCEB25325844AF885D4BF68A08A65D44B060C9614B001B15D1A53251E4B3AB + 8ECF551FC8E702CB9D66199349B3F9E3CFE78C84C16232C6B27341178DD24CB8 + A4876763C96E1A4985B4885E4BF04AA59A97009694B1A45256D2DA18625DCC06 + 8BA45423589544A0D4C912228445E8BA6A945D925890E4EFE709E0944BFED2DF + A864661EC1AC247C8B4856B4015F722588B569567237CE2205C790BF818CD4C9 + 2611CC6B938DF1256158D5866C09C95ACE401BF6596DB213B2C72B0E22C514D3 + 482151CDC269968C7BBA5130F66CBB0CA755327E816C0C2BC9307487AF7B7F71 + D22098964A68E62DA9D5541C11D63342B710EB2D923048334053E4F02ADB6739 + 1A342AA357622197C0950ADD441C2B7117AE0AEAC858925952A32EC12B53A4EF + F5C00ABFBF25CC0757B11C91746910ABC4B22481833E2C86FC46CDB42F26A253 + 65929426CB294958BD17BEF765A9F6F6FBB114F4635104B06F0573ABCB58F433 + 4DBA89145EA07D25BC3A267283AC53182991715549E42610CB8610750248E4C3 + 702A09E4EA294300C7B26144A52A9AE1236CAB88DE3FCB2395C8A17408114302 + F3DDE4A20865F82C02B7903044AFC8DC682662F253BE51A621A24D12A03E9625 + 14677E4E1C29BEE3649E6570A20C17601B0791607CA9DF2DF3DD9644CC54F286 + 048EA52208C4FCCC7F95E5F0D1CE3C8D6AEA1432EC4322A30C692C523BC73A12 + 856A0AB56E118D7E0555A9ABAE39869C8EA545F00758A730EDAC6B46EF99FD59 + 2A9FC321B62DFDF91CE7B793CE6591605F902472B65C62996A2C779EEF86DF89 + 9FED1FE0B7120CB38D6346A256646B3495639A79843966F94274F787B1E4730F + 0F8458F638EB994E476986987610B95204CD561683B512F68F1A38BBD3C5C171 + 056B5B22BD62180CA2E8F5A2683482E875E3180E32A8D593C8B08DD344966D98 + 2B240D295B6AE450AEB31DD4AE29BDB738CB93613E39843856ADB2CC7E9639C1 + 71CC613DF24581E17329647369DA3328F37B6B77EB18AD77305C6FA3DD2BA3DE + C9A33328A137A8A0497BAD9141A39563B8323ABD1A51418BDF59BB5345BD5940 + B11247AE1864D902C88BC06BA45067998BC5301162DC0406C302F328636DB36A + D01B165166B834E325F37EA44ABAF736C677C977A1F62262EAC3D15544F9FED2 + EC53797EF38E3958E0AA74D7FDD292C05FE4FB9129B5E181448A7EFC8EF97D05 + F9ED06E91EE13F24914822C53EE464234C47E55C41A5B6CA7711C0DA5A122727 + 22DB3BD83B50FDD26C633FD2591FDB39C67666BE6549C687F83DEA108924A738 + 06335D495CFBF80FF3EB0000F3F7716CF0732CD03791CE4791CA06D9F64154AA + 31D46A092285662D8D4E338BB5AE839D7E169B1D076BCD0C36DA19F44590B39E + 65F655271246261C462E164591F9561CF6854C12F55C0CBD6A12DD461223C6D9 + 1B9570FBA08B07673D1CEF56B0B55EC53ADF656FD84491636834C5BECF714E87 + 074AFCD734F81F1A753B38D9EFE2CD473D7CF4D521BEFDF13ABEF7F11A3E7AD2 + C1A3832A36FB351445F0727C8F734C707209D3F6E9420CD96282ED92E5372732 + B4C1F185DF49B1C0FAF2FBE57754ADB04EA32AF637AAD8EAA7B1D18963936DBA + 35CC616794C57A3F8166651545679E984325B7886A7105F5B21FB572108D1AFB + 4C2D62FA50B3E5F03D3928B0BFABAF56A5C697ED17679BA62B11943A1954BA39 + 0349DDEA804B826393C37A3BE6A08AD48CD3AC65F87F900AFC34D34861B09EC5 + D65681EF3ECDFEC9763592E71C372A6954F85DA9AFB7FA259425C1CB344BECE3 + 65B67989E36689E366917D22CB779AE1F7EC94D937E99735EE09A617E7B79162 + 1A197E4305BE07A6D52B32FD3C2A4DD6A15DE138D540B55F47B6C97F5A3E03DD + B39FE01C40775847252D2A7BB980682987709EFFDB6C9AE3B0D49BD38DEF31CA + B64E97ABCCB3896CADC1EFA78258BEC0B4184F128D449C738D5443AA81A5E696 + FF629A524F1CA8308CC63622D7667FDF1A62EB78079B876BB8717B0BAF7DE516 + BEFAFE2DA3B2FCC1A3010E8E0A6CAB140E8F8AB8795B92EC75DCBDDFC183C71B + 38B9B38DDDA321F68F07186DD75947BE8F661ACD7E1E75426D94ABF27D0FCA18 + 6CB7D1DFEA1A34863A2CC0FAB09F8734DEEB1FADFF319F7575800E42E9FEFB4C + ADC2F7584186F38804DB459A40226C0FD53FC5F985B480A83D74AD844CDD852E + 324B87BFA4723EADF7D764FF1854D1D9E618B7DFC760B78BD63ACBDA2F23CFB1 + 38C36F529A2E741825CC6F4BAAFEA5514492C922074D7A448CF9AA0C69B65F8A + ED982A974D19E392F0D5A1367E2B32A5F65E57673875F6D556DDA8EDD741B608 + C7E94896DF930ED3B09F5438B6373826B6B79A341B28B00F663B4564D8273457 + 0AE9E05DC125355D48825A9A0E2A0888ACE4DC56F7A106381F75E7642C834875 + 42E4A55F7510C128429AE53752CAEC23928E16592CC2CFA8F257DF627C5DF911 + 62F974682EC4B146F7B08A6C979AFF40A6403F575236CEF1235171350AE47BEC + 7F9D3AD24DA94066BFAA960D691CAF35CD61475D83228957491E279B2D643A1D + 148643D4B6B6E048D2BDA5FB87FB28AEAD5F40A6ABBB79DB48349B88B0EF4AA5 + B6E6C8E690A63403710E3F1FD54148CE773916EB309008DCB081DB07C2AC6F90 + E62AC76BA90B5E661C5D8120E27029CA78745BE27F526A8F5739FFF7B3CEE61A + 160BB6DF2AFBA3DFE13F85EF5E6D1A94D68B529EF3CB32DB51043ADB97F38580 + C2A8DD999F2B1DCC39AE4865B69F88DD15CDD539864BDB8FDE8799BF73EEAEF9 + BBEED3D7DC5FEFD4C7FEA6F021C60BB2ED44FC0B3A7C2A325712AE46CA556B17 + B38E617DD806E640682CCE36E13A45DA8DF45FE4F392FCC612B38BC442388A45 + 42D2B441B699EED48D733D22E97A7BB041872C046317A9CE3697D61391C64240 + C4798AEB0D4328B30E5AB3697D1462BE9C0389905D3457A3708D41D340EDACF6 + 262C09ED9A228715DFD55EA42B6C846BFEC03956FD4480EB5BBFC135AE27AFFB + C234751D918BAB9C77894C5D603E461575846D25C2576B5011A32250658A1415 + B949BBFC75207942F07AC24E5457CB5FD0FA56698CC95143C88EED5E372F116B + 89D2C95ADD4F3F5D8D34D68A25987A686DCC3AB9EB6437AC2569DD784C7F55C4 + A90EA20770C95CED34DE6BA0DBA565FF240F9B9F2D8F4C9B86F7D912CC664F41 + F978E2A8FEE6AE6081616D7CAF6961F6163C10F17B7E5594CACDB4584E03ED75 + A8CCE3BD13BB6762EDB3FC2771E52E73BC5FE28571675EC63EC615E56BF75326 + FB2D6E5811B3763FC6EE45D9FD280B4BF8D9FD2A6F5EB61C36BEB92B570430EB + FC02EBFF12DB612229EB0967F37861E1E27E9860EB6AEB6EC22B9EDCC679CFC4 + B89E76FF68029F0EF2B33FFBC66DA0743C5018B92B1FE56FCB6249EC0991CD76 + F292B692DC75EB6609DDD9B012B696043644EF18B66DCCB387989D10BD337079 + 3569E0759B8E679F5F5C793626E196CE09604B1E1B1258FE6302F89C043EEF0F + D3FB9E82DD0F75E38CDFB5F666AF6B5FD86FF670A7495D4BE83ECDDDEBE6759F + E5E7C585BDE4A9BD6BBB9F3DBDAF6DB55E5A684FDDEEBD3F0F2EEEC7CBCDE50C + BC3C83DCBD7EF6F973D2BF170860867D4E0258E4AF4CB99B7C2D013C0E67E37A + CBF434CCF27F5A3CEBFEEB83E93D07017C112CCFAF4000CFC274786F3A134C93 + B0FF98B850A673AEF49C23FD356113FA8784C9F8FF4F004FF0CCCE68A10F4124 + F0F310C1333FBCFFBAF0773FF03C2F94AF3BE0FF7DE08B08E067E10B495F6B7A + 4137EFCFF06904F08574C6F166C2F3E3353FDF193FE7A7E1B9E36902309E704C + C33B21F1E269612E9E707B3ABCF1BD9815CE4BE85ABB77B2AB89919D849A09A7 + C7DF3B213676627AA12078095A1B46A6485FAB8E59B820952BFF316C1E4F830D + E70D6F2775B37061A1C249AD9D580BEE3D3DE7271EBD93E9C9A4DA777151A505 + 9908DFF3FB7D5DFB4B8BE7EDA8F6B3797D0EF4BBC609FC35532EB59FBB90348B + 5A2E8E756FEEE50827D59124AEC632B8CAC5F2152E72AFEA2474CCC17C3C8B45 + 623EA2D3D152F34537D909D997E23983737777B1ADF4BD8B6AB3F8E6A2FC4A80 + E590F46A50AA8BB90866DE96D815196CC95F73F7D2183AA57D450B5142E92EB0 + 6C825DAC9BBBA458F6056D065922D80311C0BA674E77FA4A9A5764EE7C223526 + 81959688E831C69B29AED413ED3A0000FFF4494441544401D36498EB719629A6 + B2A80C7CC77E2ED275CA5C27CC1947A7EB5775EF945441178B46F5B3217CC724 + B0248025212CC2570470B4D174A5842B2E112C7BACD540BCD342B45E372AFA42 + 35A98B2E2228957EB46BD32B5829C25770E0CBBBEAE5B4392409024974988D25 + 6D54655D8901B38994CFD1DD55516736A2E82FC2779930E4EF182284172261B6 + A35415870DE92B2CD12E52D8BAF974176E3A61885E6D3086B5D1A94D433E5B82 + 7845A487246C99AE36D12421A1FBFC92BAF76E8232120DDD9556A45F9E61D208 + E61386FC0D97D2ACBF83B8EECEEC56511C3491EF5791EB558C591CD50D0A831A + 32ED2274AF5FAC9A2118AF9C625B2426089792C62D5C729F8305E541F7420A51 + 41F965E308A4A308A6636643369A633A466A50F7D1329E0862D63BC0FAAF4A85 + 7344F7FFFA083FDB4552BC61C6770972D3660CB31C0FF339EE12DB34034C3BE4 + F03913C3728CF1432B98F72F612120A95D1FFC4C47AA9B43714916336E4C6AA6 + 0346F2D5CFB6D77DBF82F25F89458CAAE839BF0FF301DD21BC826BAB4BB8BAB2 + 68CCEBFE652C187259C4AAC8D2884BE612B16CC49022F19CD43307093F52C5A8 + 91984D975C774908171B79A28014DF4BD2A81556DDFD588D07D807027CB721DA + 9947680921919A63695E91BE4230212962D62BE26295F515011C4D4590924424 + 9114C1EFC40CF92B89E368266A540B672B596326F89EE2BA7B361536612C42C9 + 10E26CCB4CD941BE9A43B6E420C3F798E3FB4D67C348A4FC46E2B4DAC8A2D12A + A252CF21574923C1BA87543EA93566BBC4985F222B55D322082521CAB64A460D + 292C09E44AA38252B5C2B40B6C831CC3B12FF33BF7710C10FCFCF6FD51BE5BA9 + 770ED3E4F8104EB2FFA4D97FE2FC462292208E211C8BF23980643A8842296EA4 + 01CB22E1B36CB79C1FC572049D5E06374EDBB8F76884BDA3129A1D3FEA8D15B4 + 5B7EB4DB21D46B41944A2B28169691CDF990482C2395F12157081929C962296A + 88AB669B6D52E43B661B24D221386CE3742E61F2F7F35D05F81EA27C372927CC + 360A9BF8E94C18291D0E60F814DFABC33E50AE24995616AD6E16DD9E83C1208F + E1B030B6E770B0DFC6ED9B9B38391E627BBB86F551191B6B750CFB7974DA3143 + A4EDEF5719AE86FDBD2AB6360AE875E21876E3D8DEC86167AB60B0B753C1EE6E + 956615DBB40F4659B4BB09837A23C67AC50CD19BCB2691E5FB72922C33BFD522 + FB4595E384A4481B8D322A1C5FB2D91CE21CAB0391280261B639DF4B94637984 + ED1F8AB0BF872288F15F92E4FF28C5300EBFD5722E8E3ADBAB5589A15B8BA255 + 0EA1590A62D04C1A49DA8D7E096B9D329AEC932596A1C8F62C8A70CFC74D9B45 + 444A679836FBB1BE111D1A918A6F3FFF0D2A83340504A22184F95DC753EC6F2C + 7FC661F98B6994F219D62383729E75AE96B0DEC963B395C2A896C47A2D8F75D5 + 8DE3478EDFB1C33122135C366681634229114125994085636D856937D9361D7E + D75B2DBE9B6E1E477C0FB7D9BE87FDA449AF53CBA259651D381E3ACC5FED59CC + 39283A0E9A05BEBB561B0783326E6F67F0E466165F7BA58C0F5EA9E2CDD3226E + 0F73586B1650A995CC2101F59344D287749ADF99134291FDA85A4AA141FF76B3 + 8566A36588E52CFFB34E3C8E0ABFB1B54E0EC75B559CED5670BCE1E0703D8EA3 + CDA8C1F66015BDFA55B4AA57D12C5F43A334874E7D196BDD28DB3F8DB55E8AE5 + 8F1149F69F3C069D12FD0BE8F0FFD1EF54D161D9AA52F9CEBE52ABA5D1EB96B0 + BDD5619FEAA1437B91E37FCA8C7B7138E534521CDFE2C51092651FC73D3F5AFD + 2036588E9BA759DCBD5DC4E1910E24B0BDDA09F4690ED70B585BCFA3C572549B + 71349AEA9F29B4F95C6B465117C12BF28EE1F3253FC78C00C3C4199FEDCEFE3E + 1A110321C1EF244D64F89DA4F99D248814FAFC9ED6D62AE8B1FD6BFC7E55DE9C + EE68671F9196041D8CD11DEE518E870EFF95F94A817EAE760469338873BC4AF1 + 7F95CE734CE1789D613C49CB6B8CD5F816E77F4C635A92FD4C6465B29C67FD73 + 86AC9424AAFED352A31ECF1538EE5650EBF550EF77506D4B85781FA7B7767074 + 32C2C64619837E0ADD2EDF452782D1308D83831A4E4EDA383D6EF37BAF638DDF + F9DE41030F1E6DE1E4ACCB764CA3CDEFBE3F64BC8108738E6B2DF6D3DD3A760E + 9A186D548D4683BEEE97E63B9466813CC7F584C67DF6F120C763BFFEAF860C16 + 11A9FFA8ECAE1966FD0C41AB833AAC4BB294E7BFDB4180F38F90D4C373FE912E + 978C847A81FDA4DCE3B7ACFC7645FE0A6D8CF65AD838EC62E77880F583367A5B + 1523E55F6AB33C55FE533877D0218218DB3F94E53C41879C684F70FC4957A409 + 236FA4994B5D91F86D54BB2D145B7538FCA653255DAB904596F39C0AFD5BBA23 + 99A84BF534E730E57ECDDCA59FD558C771B3CA7E51E3F756E96599A66354CD4B + 5D75B1DB649806129C3F8A848B1544C69558A6B2814839917152DDBB1C8DD2B4 + 07D05C6D2C13B09D7C86D8E45C5007004538720C1061EE1EE2535FD0BCC8D5AC + 122322EC576A4BCDE934BFD3F50B217EDB926CD6613EF5275DB121E9EA12E767 + B5510339CECB34EF921A6BA7DD40B6DBE173D91C5234F9716EAA7B9C739D0EEA + EBEBE8ECEEA2D8EB23CD3AC6CB3573584152CC52EF9DAEB3DE9C0FA7EAD22E40 + 3BE7C249FE9B25B52CA96C49F4EA0A195D65B2CC3151E4AA8857A9E88E334F1D + 3C14529C6B2BCD20D700AB9AAF73FE2F15C22B5C77AC12CBFC5F88AC14092C3F + A91BB6D79648EDB039D068E6BFEEDCD6B6A90E0748D384DAD0A82766FBE81A96 + 80EAC972182962B6955482EB2086487CB5BF7BB7AE0E62BA5A2BEC5DBBB28BF8 + 551869B210912DB5D422FAC344A42469EDF28404B68758B506D1DA45EA93E7F8 + EFB9CEFF9DAE87993312B211D62D6A20BBC5F540C86091F319D559FDC8BD6B97 + 791222CEA5BDC7C2AF83B146E34DC25C9761AEA6F160916E92DE5D18A77F9E27 + CBC17C64EA599A8D444E8BA4361A8C989E34184D486BC69D0BAA6C227F095F60 + 02119FD330A42FCD2BCB01E80AA4CBAB8AA77A8B80D67B75095D733F2FD7BE96 + C8142CD129487B950E2F9B3821AD2DE3EE1DBF633F0B1BDE426978C95DAFDD62 + DACD90A72CF795253FD7E47E5376D541F64B8B3E638AE0B512B45ABBCB14B17A + 45A425FDDCB0AA37D3E25ADF3D28EE337B0496D0B576E5A9B2DAFCBDB065B350 + D94CF908B597EE13B6505A36DCACB8138CF71AEC1DC02AB3F623B457E1DD2FB1 + FB2CB25BBFE9676BF7EE6B5C1EEF9FCCC2642F65AC5EF86A203181DC1566A2D6 + 58E18909A93C86D9271A43EEDEB2287F4B9A0A56BDB2DC44265BA25412B3C265 + BF5B1693B7CA4E78D52B9F4B09339E279F499E63F2D5C25BD759B075B7EDE0AD + B32DF3841C268CA63742EEDE7A7A2580BD04B0CDE33CDD8B982679BDEED3D2C1 + F6D986797125764ECEAE2666E2922F7901B3C23C0BB308E069F2D7BA4F13C016 + DEBDD0E97D4FAFDBA525B7FF78F757ADD4AE772FF76964EE2CF7A785FD4278F6 + B0ADDD0BBB77ED2580B59F6EF7DDA7E1DD737F9ABF6BBAB0DC81E52C2C8C9BA0 + 3096F09DC6AF40005B7F93EF7FA504B04BEA9EDB67F258134CF94F71635E4277 + 16440A4F4B063F13D324EC3F262E94E9BF6202F8A5A9E7998519E33CCC7F4304 + F0F48BF5E0B93BE63309607D24E30F65E687F75F289E4362D7EBF7F78DE9C1FE + EF1ABF1601FC2C28ED59F631ECCFD0AA81B63F4399937053F1BC3FD017AFD2EE + 2170A749DCE7FD793F0F013C49CB90AD2259E93E9E807861271E76F2310DA923 + 119EC7DD9B9630338FF164C74BF6CA7C1A016C269B1E7F6F18136E0C4BFE5A58 + 32D712BB5E784960995E0278E2AFB8B2CB4DE93D050A67A16795D93B51F6BA79 + 6127D86641A1C9B316039E49B97061911274172966F1355E5C794FE66A31E7C2 + 2DB75B7EE5AD49FDF982C42E0865BF46CCB17C575576BB1863BAE614B4C8D568 + D210C057E3195C8B3BB81A23C6F6F944CE6051A6C86042F6C558764CF812D12C + E17051EF92C2861816196BE012B59604BE1E964A2D9629C4452617F92251AFD1 + CD48F72A2CF39D97C46FCC2582AF04D946BA9798B027B545245BC96273127BBC + 68376AC7122C4F2A6BEE03B6C4AF4BFE16B1922B62299BC342C6C1423A4BFFB1 + 4A35A3064ED2B22EE12B35D04B29D72E095FB3A912754FDA1BC29A6576CB1D73 + C9E231A4726D25C7FCF2052CE9F4BED441D75C0258F69542112285A3CD16E2AD + 0E62ED36C28D86F197196D3711EDB41069F05977CA3519B65537AA1A93744F75 + 75079AC8E3B27B2F5BB18870591B613524086DB049C2C0AAF78B54186E0CDDEF + 162CE68D14B12F97C56A36831547D2CB292C89103604B04EDD07C664AF5441EB + A47EC440EA8CB501A68D2E49231994245D54309BC752E7ECDEEDABCD2F6D9449 + 5A42E4B2361C538604B677CB25EA25A4588794D486B6CA4877649610ABE70CF1 + 1BAD397C2EC0E95690ED55511CD6CD1D7DA551836603B5CD366ADB1D54699646 + BAB3AF84742B8F4CBB60CC64837955C6A46F29C97AC7112CC5D806494314279B + 39A4DA8A5344AAC6B025496BC410CA49CD74CA6C7A6A03D448084B22694C0207 + 5271F8926C8B58907DCD4FE86EE22056E24104D2118415DE61B874CC90BE1111 + C9442015852F11A29B248F255144309D40C4BD6BD81712291944381E36646424 + 252954919D018498B688C92021927839E2C362C40F91CF0B411FE6FC2BC4B231 + AFFB960DAE0556E8EFC32AF314011C65BE2257258528925677B94A125844B0C8 + DF5C2D8562936D4EBBD442975A39F436BA440FC546D1A8164EE5320824A46E8F + F90597D8067EF8E2AE54AE246A45EC06E3AC4F74C548FF2AAFB0EEE48D9E93C0 + 61C6D77DB099427A424E889C9604B00860D557F7CD5AE961B907E8164C86D826 + 61D643698AC88E98B869490017F8BE54378691446D3CE94328B284586215996C + 04B97C12F9A22445E36C5BB5A54BFE46924C8F654E383142F7C3AAED09B511E1 + 1493A8364B6875EA28548A0C9B843F2CB5E07A17519687EF3AA177A5FB6DF98E + 632283D506EC67F108423191F931C4F95D64D9F725992BA9D94EAF847EBF4ED4 + 50AB4B9A3189463389D17A0187379A383CAE63FFB082ED9D1C3637D2180DE30C + 9BC4A0EFA02DF2A91645B39E444DF11A69542A224803AC67806925506BF25DD2 + AD20E2B49860DE92A00D21C4F7100CAD20CCF7108DF9108BAF2299F2C1C9FA91 + 4E079066BBE7726194445057532C531ABD7E0E6B2CD73A311AE60CD646591CEC + 55F1E8E10EDE7AF394E62E0EF79BD8DA28637BA3CA3297B0B599C3CD9B6DBCFA + 640BAFBEB285C70FD7F1E0FEC8B81DED97B0BB9DC5CE660607BB45DC38ACE3F8 + A889DB37FB78F8600377EE0E71E3B88983830AD64739B4DB197445C2AE379977 + 157D4923371C749A190C7B056CADD759BE26868326BAED1ACB5F40428457326E + A47C931CC713C904FB450C49BE97548CFD9DDF71C989A35ECAA0C77E3F6C278C + 6AE2754982366268EB1EDA7A029BDD3C7606556C701CEAD4B36856986F238F2E + DF63B39E4136AB7EA7830B41E6C9F433CC87FD22CABE1089C668B26F31AF5426 + 8E34FB5946F71DB34F963926D52B199438DEA4F97D27F83D67F90DD5B201B48B + AB68F15DF6CB29F458BE22FD93BE25A402CBC8C55650E67BAAB01F57B341D499 + 6E35C9EF95E34839BA8A5ADA8F61358A4139805E7E19BB0D1FF69A016CB14F6C + 726C9434715312ABB928F2ECE3298E5FC97008E564166DFEAF14F76818C69D9D + 20EEEEADE2F6E6120E5B4B58CFAFA096D141830CFB9DA486E3C83A7E14722C4B + 218C6A3E821AC7927A298D26FF33BD5613AD06C7ED1CFB37D12E67B0D649E36C + B78A07ECDF279B296CB57CD86CAD60BBEBC76E2F88BD6108FB9B51EC8CA2586B + 07B1D18BE2C64E1E6707359A656CF698463783ED51892863BD5FE47BCBA2CB6F + A0D7C860407BBB91E2BBC9607FAB89077776F1E8FE218E6FAC6130ACA2C2BA97 + F8CE721AFF59FF38DB3A63C8DA10FADD386E1C14F1E62B43BCFBE6169EBCBCCE + 78559CDE68E021FBEEDD0743DCBDD735AAD8FBC308B6B6D23839ADE1F4AC8ABD + 7D077B7B799CB05E07FB65F4BA31743B616C0CD338DA62FFDED5410711BF110C + FB110C7A61F6DB30467DD67310C3C628CD6F216FEE6EEEB65368361268775897 + 4E0E258ECB79DDDDCCFE12E6371A65B98B22BBA5C2B85E40B19C3552D84291FF + 3BDD532C15ED25F6D522FFA319FED374E7769C7D5307668C6A788EE571FDDFB2 + 69C4F2592444F465F95FCFA6E89F419EEFAFCAF946B15A84C3B1B3C47EDAE94A + 6A99DF5D97634E3DC46F32828DF51C76F95EF67577F511BFEBBD26C707F62FF9 + 6D1670FFE106EE3F5AC7E14903BB071C1BF68BD8D895A603B6C5760E87A72DEC + F0DB1F6DF03D72EC1872ECE88D2ACCA7863EFFF3B5661139F6FF14CB17D1C129 + FE2FA529C3FC73387EC9F49B7F83A4855D4D19929C4E4B1A532A8C756849E438 + DD0A9C4735864D34473574365A18EE77B17E34C0F68D01764E0638385BC3E9BD + 2DDC79B487DB8FF77174671DA3DD1A5AA33C1AFD3C6A520BDE114A28B58B2870 + DE50E69CA5DCAB187B9EFF48A9F4AE0EDA68AEF7D0265A4463C8394ABF8962AB + 62C2E88EFCF6461B6BFB236C9F6E63E7E61ECBB181F66613358EEF75F6EF5A87 + DF0CC73C5DC7E054D2E67EFC2AC7B6DAB0C7BC3B485724D99D27720867A51A9B + F317919DB2735E1BE6FF26680E9CB970A597E966200966F7509EEE5BF665DC39 + 9A2483E5270238CA7F555C7DA4C8EFA4C4FF3EFB58B29433F321A5E7E738AAEB + 28828AC7799E0EBEE96A8E6435675467E73A6C0BFEE3B24D91C269CE13F9AFD7 + 61BF6A95794BD532E763CC27C03861BEA730F34BF2FF9AA9578DD4A9C8479190 + 967015E9282966B9A7AA65388D1AD2F50AD3E41C926694735B3FE7DCAB69CEBD + 19C7AD0FE3B07DA29C072744FC8A3C1E23A1BB9B4596275D02D8CF7FB74FDA3A + 0891C092445DD51DFE9CCBBB64A7A482350F966689A8991B2FF21FB21C975B14 + 52472C77438A4A1B0FCD65C611A91CCC48CABB8020D71C22728D6A698EB321BE + 3FD92555ECE73B0BF2FDC9CD25E559963121BC329E431B02986B8770BE48B36C + D4ECC7CA55C629312CE7F284C8E055AD63582723F16B085497FCF54264AF0855 + D96D98EBC402D733AAA7E6EE82EAA7FAC8B458E2FF6CA282D993FEBC343989AC + 2524B52B2C88E86558B5914C23C5EBF31BBF793E6BFD775D52BE635CA7DB55A6 + 7B6D0C5BCEAB227C7DAE54AFA463BDB0D2BEDEE72B86FC15E1A935AE2B457B4D + 92B52234B9F6B6EB6943628A58E51A7B42764A6295980BF0DD865306F35C1F6B + 7DA9B8967C351AB618DE4B062B9D49BA9E35BB75F3FA29EC646DBFACF5B8CFC0 + 92C0B2BFB4B03A86DC5D95CC960816BCC4AA21809798E6CA791904EFC17199B6 + 3C760FC1EE2978E3A84C57C66DA2F5B6257D5F523C629A009E85497A2CA3F0D2 + 02F327CCBECB92F6872EEEB7D867BB6762DDEC9E8A0D27FB647F83B004F02C62 + D4EEABD83D162F012C77138FEFF2C571BC5952B526BFE5B1C9E76BAB71B38F23 + 18D5D5E3BD1D49D64AB5B2AEC8322A96C7D7675D615E97695A5C09D25D5019E4 + 3626856D99CCB3C27988589337CBEAFABB612618D7ED73CF63288EC525FAABBE + B68D6CD94DF9999FF7D9E6ED6DCF2FC639A1EB8597FC15E4E6257CA79F751FAF + 97009E90B45304EE3401FC345CF6A70CBC6EDE74DCF465F27999A6511D6D895F + D74DF61797587EB6C9D30860BBFF69FD4C1F62DF71F74BD597F52ED5EE6C6BE6 + A93D55EF1EB0DDC79DC6B3FC041BFF8BF68927F0EC537BF7ADBD90FB9721806D + 98A7873B275CCFB983A7F016578869E2D78BAB0C4F7809DC69583FEB3F497B8C + 6785B16978F12CF7597ED6FDD707D39B2280DD67CB63CD02E3799FC77CD82CCE + CC72694FC374F8CF419C9CD7FE8F8D0B65F96F88007E16CE0BF8DF28013CE5F7 + ECCEE909FF1402D8158D773F2A83991FDE97C797F9F0A7078C6761126F06F92B + 3C35FC0C3C2DECB4FBF3C30EF05F1EF687F1EC1F0707FC5F4702582631EB07E7 + C52C3FAFDB3401ECF533FE6349E10BCFB37EBE827EDA9E4983256DBDB0619F46 + 00CF8A330BDE7C34397916EC0465DACD0BAF9F17D379D8B4DC38727721B2D24B + EEBAE4E50C3F4277EA1A3F1B8EB0C4ED3401FC3428CEB3E0CDDF2E32CC424318 + 4FBE2D6C194D9E33FC9F86C9A49AD044F9C2C45C76EFC240E4AF51BB9C34EAA5 + CCA2958B57BB5814CCA2CD2EE0C624B09B8F26F1EE294EE5778D934C73C78EEC + 8408E06B2CBF168F735C0CCC332F49D21AC9DA4802D7E2695C4B66713521F237 + 8B2B2280699F4BE630372681AFC71C2ED45DC2774112C12284699F8BCA9DF1B9 + 60BE1E712579A596CDDCD7CB340CA94B37A36EDA10A95C304558AEF018229F25 + F1CB32489DF37C927164328E54448B04D61D4D466597DA85E596CA6943F886B9 + F0E2F332F3F7B1AC4B6911BC792C8ED53F8B08160C014C1855D03A396FDC0AAE + D4AEDD4419633523E95A827623593026810DD13B3E696F2484E9AE78130239ED + 60559B5142BEE04AFC56AA08546B58A954B05C2EC327B2D790BF3D443A1D445A + 2D841B4D849A34DBEE9D7CE1660DA1BA88E22AE29D2692BD3612BD16925DA243 + F41A8C5F3352C282EE594B0822916B3583B08866A541487A385C2D2358910472 + 11FE92CA96C76A2187E52CEB270960426AA0E74241B3C12342581B6A92AA9064 + 82BBA9968754004AB2C2AA0D342A0133ACBB5121AD13FD31136731C178C90496 + 5271B64B02E6AEB4620ED10AE3D60A46EA375ECFB33E6564FA3503D9939D129C + 410585611D7949920C6BC80F6A7C260622836BA8AC3751D96CA1BCDE30CF9600 + 4E3573C6B410212C3FA7533C77EB1490E931CF6115D9510D4EBF8238DD23D534 + CB9646A2ECAA8A8C973288E4A4166F4CD84AEA48D246B90402B4FB335122CEBA + 4B222444BF30242114639C482E6E2493B4311AC98A4466FC54D46C522BAD88E3 + 4A65C5D92EF1440C511186912082611F82D10044A88A041581EA4AEEBAC4A748 + E1D5B8DF4810AF309CB0C8388BE15523952CF5D2C2722C04A99C96AAEB88882F + 49871533C8944426A491AB3AE61E4D91BEDA682EB773A8F74B06A516DD5B5954 + 3B15D43A553892C6629D53225A599755A63B1FF019F5D451D585F50A13A16418 + AB227AC792BC52F16CA46B194677F88A701544BE0A22A5E3F277A4725B69D0CF + C02580DDFAB34DE92F296191BC3186B3C4AD54390B5267AB768930CD8488613E + 47637C8EF9106659226C2BA95B8E2B3DE3E6439CED1A57DA6CDB648EF1724196 + 552AA8C32CB7DC02CCCF870CDF69B19A42B6A27A841010F110594120A1BB9459 + 968CEE1C667DD222EBFDC63DC4F247F91C4E322F22E58451A8A4D0EAE5D01938 + 44DE48FA1935B01D91C2224D1CBA153018E6E996C0D6661987074DEC6C97311A + 66D0EFA5D1EF16882286FD32CD125A22415B3974DB7934F80E9BCD0C5A6DBE37 + A9272E8490C94590D3FDAC39B69DDA2C1E34ED90665F74B25164458E334C3E1F + 41A51441AD1246B311675E0ED644F60E32D890A4E661110707456C6F67589E0C + F6F71CDC3CABE0B557D7F0C6EB9BB87BA785DDAD0C364771ECACA7B0B7E3E0F8 + 4689EE0DDCBE2D72288F5B2715BCF2A887375F5DC7EB8FFB78F96E1DF7CFCAB8 + 7B52C2EDA322EEDC28E3E1CD065EB9DBC1C3DB2DDC39A9E2A6F265BA5B6B696C + EF167042FFB3D32A0E76D2D81A45B0BD16C5D12ECBC2BC8E778BCC3B8F51C741 + B51445DA594532B10C2715402E1341D689A0C06FB2C2BA56F3ABE8D6FCD8EA27 + 7063BB885B87924A2DE0703D8DC3511A078314369B096C3692D8EB6671382862 + 8B6DDCAFC6306CF0DD0CB2D81E6530EA45512BF99149B13FC57D48F0BB4CB39F + 3B990432E924F38F20C57EAABCD5D61996C9C92CA1945F41B711C51ADF69AF19 + 43AB14422DEF47C559413E3D879C73996196D0AC24D1D277CABE940C2F231599 + 4739BB8421FBC7C6304933846E3D8A463E8872721995D432DAC5656C74FD46A2 + F6C6D08F577623787D3F89B78F1B78E3B48FDB5B65D62B827A7A11F9D81CB1C8 + 7841B43329F49C1406051F8EFA3EBC7327836FBD51C2070FD2B8330A603BBF8C + 7A62093996A5514BA0538FA15309A2C7B446F530D66A618C2A11740A51748B0E + 06D522CB9246351D47AF98C55A3D8D9D760C671B29DCDD4DE2B81FC05671056B + B9556C319D1B9D146EAD177067A788D3CD2CF6FA3183B3ED2CEEEC9771BA95C7 + DE208EED4E10FB743F1A310DBDAF21DBA2E1C7B0B2CC7A05B05EF71B9C6C64F0 + E8ACC93ED6661FA9619DDFD5FA5A195B3B6D0CF84FA94BBB4129897249870012 + E8D7B338DD68E3F5B36DBC796B1B6FDC6EE32B2F37F08DAF0EF1C9D746F8F09D + 0E3EFD7080AFBE5EC0C35B41BCF3C4C10F3FDBC04FBEB783EF7EA3CF302D7CFD + AD16BEF6B88A578ED3B8B919C6C30307EFDDEBE15DF6FBFBB772383B49E1783F + C67EEAC37A779EFD6F11871B019CECC470BA97C4C15A0C6B2D3F46ED20FB760A + DB9B390C0769A319A0D149F39B0E209D0FA0C4F1A85A735067DFA8735CA994E3 + A855D9CEDD8C3930B1C976DCDE28719CC8A0520CB2DF0750A948EB40DA681DC8 + 1553E6F04D9CE369927D335F0CA1580E1A942AEC8F1C437A1C6B6AD5248AC508 + DB89DF4C358C763BCAB1288DCDAD1C0EF68A38396AE0C6410D3B1B796CB17D37 + F85D6CAE39D8D9C970BC28E0EE3D7ECB2F0FF080DFFBBD875DDC7DD8221AB87B + BF869B77ABB8C1EF7F87E1B6F60BD8DC65BFDCAD6267BF8D1D49E6F2BFDCE03F + 59247049D73854B2E68E7811D9FA17E83090FE8DDE7FA44CDD2FAFAB081446CF + F65FA2AB06F275FDE71C3438CE364705B4D74B58DF6BE0E0E600B71F6CE0C12B + BBB8F76813677787D83DAAA3CB3ED61E2430DC2C60B4594197FDA72769E5ED26 + D6763AD8D8EF63B82FB2B78A2AC76511C455CE2B2A3D3E1335CE671AEC6B8D51 + 1DA54E1E591D3E603FABD0DE58637A4C63C0F892462EB53328D613281B29761D + 5812A99F627DA429C3E13F9BF324496C17681A89DB1CE71A9CAFE81FCBB0C912 + FFEF65F61122C334929AC7F07F9F283948EB0A8D5AC990AC71A62395D951CEFB + 0449EFBA2AC4DD6749F2661836CDEF5773204938A7380FCA300DDD712D35D652 + 67AD398DC860CD2F029A9F6463884BCA9E754C73DC4CB1EFA80C49CEA54400C7 + 4B59CE87729CFB64398FD2952122A31308B3FC61CE4D8C5DA6B4CA88A04E6B2E + 2F029BCF59C6E1FC52F1E39CB3EAAEE8A4E68E0511D459CE435DED378131C96A + EE7ECF72AECA79AD25B5A5323DC4F9A7B90F9EF3D5B024590D01ACF90CE7B95C + 970822807D5CAFF85392A6E6BA20C1F9AF245A2D511AF0109C2259455CD24D50 + 188515896A5419331DA38A9BE592647080EB09CD93BD10B9ADC393F2F742EE22 + C0DD839492365658FA114A4F12E0BA8757EDE9AA6066FDC67611D62A8795F495 + 24AE91C615596B49557F80F5611DC6D2B952012D0960138773FFF9B05B47D77E + 0E91B8B6FE4ADB48F59A7609726D1630503843FC125E02D88DCF7C19F71AD3B9 + 4AFB35D9B9DE10AE84B89E0DFA719990FD5A80EB55B3F615612A82976DED8B62 + 8E6B68619E6BE1B915964590DDAA4F16214CBBD4288B0C76714E9E8A48B592A9 + 82DCAEAD30BCC8DFB15DF9CC735DBE10E01A99E6D531F96B48584161B936F74A + FF7A0960B35E272E8D9FBD44AB9728152ED35DA4AF25B20D893D9600B604B048 + 5E5BDE69A95A4B006B5FE28AF625C679D8F2285FED155812D8DABDCFB67CB68C + 86F455F905D99F029B8F777FC20B531696F7BC9CEE1E8AA03D149768D43E91F6 + 8DDC3D18ED6578F7572E10A122D53C982680EDFE8A778FC53E5BB76B4169224B + 5E2083B50F3309A3FD121FC3D1DDEC9D58936E738AAB3488EB8C2BA25769992B + B1085D91351F48D1CFDDC3D17E88F673BCFB3A72F3BADBF8B64CD654592CB96A + CB6DE3584C13CBB28BE8B5EE5E7FEB37D98362FB590101B33F3565F7B6A517B6 + 4C5EB87E6AEB7378895D0B4BF85ABB859E958697007E61397EF1CEDE1924B017 + D384AFC59540DAC0EB76C96721325848B9582196F93CC6253E0BB21B89E0CFA9 + 8126B4EFB9E0EE814EF63F89AB0CAB7B800D51CBEF73E267E32BCE1CDD3D64EE + 05211B8FFB346C98CB363CDD6C5ADE7DE299B876BEC73D6B2FDBC292BACF2280 + BDFBEDCF0E2353CF17F9093D7BDD5FD45EBFA47C8DA4AF87F4BD00378C37CD5F + 17DE323D0BCF13E7CBA4F7C5605A639ECA4B004FD43B4F715B2EA6DC2FF0634F + 8797F87D3ABF4678B9392F3F37EDFE8F810B65F93F2501EC69847F14CCCAFFD7 + 2F93B7635AF1F459E15C35D9AEFDC51719DE12C0B28F3F1AFB01FD5D12C05F34 + 284CC3867F1E5C883B8304B603A1E0759F85A7A53DEDFEFC381FE8BF2C9EE7E7 + 21FC2A04F0242EEDB37E764F238767B909B3EE00B6A4AF70698E3F65C2EB36F3 + E72BE8273D264C85A791B6E6798E6512A6C2584CC79FF56C6127204F8326255F + 8600F63E4FE7213F4B006B326FC36AE26F495E2F093C715FA01B274B9A405D66 + 1C9DB49469D2D6A24164A7160E5A0C183FA6AD85C30C583F1BEF02F1CBBC8C5D + EE0A6FCA48304D73C78A8116219EBC6837E95A7F4E54EDA459909B4DC7DC2D33 + 76D7DD2B8255FB6C171E5ED809BC019FAF8DA55ACF4F186BD1C1B85C885EE1E2 + 53B8AC05961657CA8F79DB45CAE4342727D1D304F01CC32D328FD55806BE44D6 + 90A85602782EE9E07A2A6788DFCBD10C2E4539598ED13FEE1059CC11D763595C + 8D64703D2C7256C4AE08DE9C3145006B8122A95D11BECB4C4B10016CC9DF79E6 + 3B1F13792A425564B0D42AD34C304D11D0B40BD7F9AC34AC34B00923F298695F + 0FB3AC3417E9B6CC741725094CB75596C79F2E60399DC77C3A4BE48C24B0EA25 + 1889E031296C8861FA2F32DC92A476C7306430B192917BC6F513094C5324B008 + DF95A44EE6EBA47E167E86F5670BF03979AC12BA0758E4AF2F5F84BFA87B7DCB + 46F2D75FA962B52212B86AE0AF37106AB51014D92B029888B4DB88749A86040E + B5EA08D6AB0856CB88346A88346B0837AA08D52B88B46A8875A42E9AE65855B4 + 257D753F9A101F13C1114BFE1221A625880436281711B044702E075F81F576F4 + 9EA2EC132E01BC9C14C99D641BA4CD065BB85834AAE874E75B8A7510A4524F2A + EE0C791C899A53FD3ACD3F1F8F19F277299D804F6A05B5F9582D21A63BFA6A05 + 963BCFB2E5106DE40D2275DA9B05A4A5E659C4EE5A13B9511DB9B53A0A92F2ED + 970D992BE4FB1543FC8A10CEF524499C43BC963192BF86E86D17E07414CE258C + 6D58C5CB319D74B78854BF08675433E9A7E996EC14906831FF86903792C1B112 + CB9D4F42C46F58D2C1C534A2E50C222287E91729D224741F6EB298405C9BCFB9 + 3874476E384B3852DD9942A290414AD234A5ACB1C7253143A469CFE61DA49DA4 + 218103611FFCA115F823EEBDBDDECD6D9768241C491AEBEEDD18FC225D13227B + 5D2964BF48E68C2BF5E397CAE684A45953C896A402996DA2FB2F250D5B494352 + BFDA1497EA674152BF6DB6854860A792442A2FB5A2EE667A28A9CDF6109259D6 + 53523B19A92565F9F31924442EB31E4294EF5BD2CB221872527B29D5CC6C3399 + 39E69D65BB8978D07DBE92688E3909C667DDD38A774E02BBD2CA2288634C57AA + 4D4526C7C6A4AF4BDC86A50A5A44391111D9C634B279D695488B748E058DBA63 + BF7F09A1D02A22CC33145B4528BA8C48DC8768C28778DA87642EC8F2AA6E21D6 + 39826C39C6B2C6999FFC57902D8491AF2650641BE52A929253B9C2C89412C8F2 + 39918BF03DACF07DADF2FD048C9F544D4753CC23B98CA4E3675B44506DC65069 + 4650EFA4D061DF937AE59A2478F91E4AD518AA8D0421695E3F9A4DA98ECDA0DD + 8A632015B8A322BAEC9B2D7E23DD46116DF6CFBA08C28643BB83463D85563383 + 26D32BB2FC49D62B9DD1BDB671E4F27164D8CE59BE9322DF51B59A458BDFC770 + 58C5685046A79545BF93C1A09BC6A8EF6067AB8483BD9AABA2793B8B1B4725A2 + 80C3FD0C4E6F88CC2DE0F659110FEED670E76609BB1B110CDA4B187657B03EF0 + 637B3D821B87399C1CE7B14DBF6177153B1B31DC3E29E1D1DD265EBED3C42BB7 + 6B44150F4F8BB873E8E021FD5EB955C7A3D30AEEDD28E10EF3BBBD97C5AD832C + 6E32CFD3B3126EDF6FE0D6AD220E7645A285B0B711C28DDD046E1F17707A90C7 + CE5A12DD7A1085EC2212F1AB48C6AF219B5945291F46AD123777EC4A6274D00A + 617B18C2D99E83C7CCF3F5076DBC7C56657EACDB2EF3DA601BB4E3D86DA7703C + 2CE06CBD82A361193B5D075B6CA7AD7EC648A78E3A7EBE0B49C04691E5F7998A + F9E0D0CCF19BC9B34FA7F91D3899300A92DA64FFCA669691CF2CA0515EC5D630 + 89C3ED3C0E367338246E6CE5B1BFCEEFAF1940A5BAC077197609FF4E054D7EAB + AA4331BB8C6ED38FE38312CE8ECB6CE718DB3D8C418DED5F8F62AD99C0AE48ED + 9D18EE9DA6F0EEE30ABEFD5A1DDF7C54C2C70F9AF8F0C11ADEBCD9C14DD66F50 + 5E4125710D0D96A95F8C61AB92C71EFBD45E23847BDB117CE79D0EFEF4370EF1 + 07DFD9C6FB774B3861BEBDEC0A6AC530DACC6BC87EBCD98D617798C2C1208EA3 + 410247ECA7879D1C8E7A55ECB44A6867D8D739660C39DE6CD593D8EFC4703A8A + E06CCD875BA3206E31FC11BFA91BECB7B7D9CEB7D758AF519E6D9EC6F188E90D + E3B8C1777AB691C6E97A1A07BD08B61BCBD86BFB5887041E1EE6F1E8887D6433 + C23CFD38E8F8B05E9E433F7B199B95051C0E42381845F9AE6246C27B83DFD0E6 + 660D1B1B0DF6FD1ACAE514FB0AFB06BF8F01BF89ED26FB7DBB8613FE33EE6EA7 + F1EEA302BEF36EC7E0DBEF36F1838F1BF8E137EAF8D98F47F8EBBFB887FFF96F + DFC5DFFEF513FCCB7F728A9FFF6084EFBCD7C0676F55F0E99332BE7296C687F7 + EAF8C9570FF1A30F0EF1F1D7D6F0E1FB237CF0951EFB5B9EFD3B82BB4761BC7E + 2F8FF75F6BE1BDD73A78ED7619770FB3B8CD3E7F8BFDF9D68D324E8E6A46C5F2 + E636FB4157E34484FD236EBEF74ED341AF25AD0009DA25619CC31EBFD9E3BD32 + 6E1ED571B85362FF8873DC886173BD88DDED1AD6D8979BFCF625D59BE5BF2A9B + 0BA35CF1A1DD09A0DF8F6230CC60538704187ED8CBA0C7FEDE6E27890406BD24 + 36D61C1C1E5470F75617F76EF57072A07B9DD96F187E6FB384E37DD6E14E03F7 + F89DDF67FDEFDDAFE1C183065E7EA587B7DFD9C0FB1FECE2C38FF6F0D6DB6BB8 + 7DA782BD037E537B390CF97EDB4C5F076086A312EAEC1722424B7C2F658E7715 + BE9B6AAB8C4ABDC8B159AAAD45044BB259F7BDF31FC1779830E36E98FF558ECF + FC471A3F3E4752D2A2212D15CBFCA7AC720C8FA0C4F6AA7693E8AD65B1B15BC1 + E1710B37EF0E7072AB8D83E306D6F93DB6FB31AC6D65717C93F5BC39C4CE4187 + 68E3E86448AC31DC1AB60E06E8AED750EFE551EBBAD0C12AFD17F48F2D35F3A8 + B45D377BD0AACA6FA4D0A47F3D8E7C2B892CCBE254C2FC47443956D39DFFAE0C + FFB9698ED9D26260EEDC960615FE5B750F725AFF71CEA3D2FA9FF27F50E07FA1 + C0B1CD1CDCE2B7946FA490E2BF41F3906481FFAC06E741ED3A329C3F26CA224F + 2549CB394E4D52BA75387417392C641B9C57751A2C17DD987E94ED6AD48ED7F2 + 2872BE996F56987FD6A8BBD7FC232E75E37C4749BEAB08C7A85891FFE7520851 + FE33C3FC2F46F9FF89105133472A109CFF71BE29C9EC30E72922B135170AE920 + 5D92731ACE1523925416E999949D730A91A171CE2109D53FCEF992E65011695B + C889E0969A68CE7F39CF966AE5B0AE4BC9E710E25CD6C7340C525CF3D00C31BE + 24A725311D76748F2DD70C3AC419A37F3C65A48057B476E1BAC25D5BE870A94B + FE5A72779961BD38275B45A672BECB35CDB20EA76A8D121119ACFC456EBB2A94 + 0DB12BE29970554B33AFB174AD09C730227C653712C563880C96961D4925FB24 + B59C90EA6AF72E5F95C310D97A667944E85A53B0E4EFF52955CA462533FD96C6 + E165BF66C85CF7DE5D2F096C88DB4060E26E8863CEF745249B7B79E93717625A + AC87EA63A5A3E7236C3BDA27CF84C923E8E73A83F1C6B81CF4E125FF0A2E0556 + B98EF473CD7B4EB00A3AB46C0E2E730D2D2C700DBCC035B06049E0EB3E97449D + F7AB5C11E6E312C1226D45447AE125540D9886EEBDB544B3C1981416492B281D + A37E997988E01431ED12AE82EC2E812A42D448158F8951C5B5E4EAE70853BA4D + A498FDEC6B86BC96BBCA27E257AA93CFCB7B4EFECAF4427B19C109A1EB962764 + EC5E72D7BA7B31290B61C85DBA59895FAFE4AF20BB5509ADF0B63E17EAE485A7 + ECDAABD05E8F3DB46EF72AB4B7E2DD8BB1FB19E7F7F8BA7B2EDEFD16E33E8388 + 3484AEF654647A11605E666F457B0829CC85757D948B6B6302D695CC75495843 + B806C784AC4C91BC0C7B7D8C39B30F423BDD0D011C49634187E38D3FDD685E23 + B4277235EC12BFDA23B1B024B08D6FF219E72D7342B6AA4E2ABBD2210C992BD0 + CD4BF00A46CA97FDF612DD849744F80AE36763274C3BB1FD4C1B7BDBD36B6718 + C1A8C71EDB056F7B5BB87E9F27809F0543F6F29DBAC4BD4DDB92C0CF4700CF92 + 0EBEEC3B277A5D88FCCD3C15977CE973AC12227C5792062E012CB7345CE960EF + FEA8DB5F45E4BE344FC8347B9CE3BD5011B38B11E8DE5FF55DED87CADDEECB9A + 342CA13BC684D03569BA6E368C81E2D1BCBCC0F416F99E581E99C225BAD97033 + F79D05E66D0493AE052EEC754F9B667F9BF6E93DF56937EB3E0BD65F04F22599 + 721367E0E123BCDC837996F91402F845DDE54B3C8F04B0E04DDFCB91D838D67F + 8219C275D39895B6D7FF696EBF3A98D698A77A2601EC15747C71FC6CDD2EF063 + 4F8797637B2ABF6689562F9EE5F70F8D0B657916012C3F0BAFFB17C026F4F705 + 11BF162FBE70DDB8CD2CC8B3E0BD2FF873F034D2AF827123BF687165DCC1D469 + 64AABC72570732612DBE5C67F462BA630ACF2681A7C07CCFC9DF316C5AF62392 + A98F6DFCE179C35EFC20BF1CA6078C597E4F831DAC26717E4D09600DBC2F1932 + 759CBEC7FEBC3083EF05B7F301FFCBC0FB03B1B03F24FB3C1DDEEB3FCB5DB03F + 2F43F05A78DC8D9FC27AFCA6E3DA30DEE709A9ABE76791BBCF89C90F7DC6CF7A + 42DE321F23013C8300B6930C3BA178166CB8F389CB6CB864AD8B596E5E77EB67 + EDDE3CDCB09AC47352A23084393DA7BB3538B1316566B80918D66B3793428597 + 0A164E065DF52B9C1C9A74C7694F418B1F43CECE709B86F1F340F7AE3CEB9E13 + 2FCC495343BE72C2CA49AA5D9498450CFD55767752ABB09A189F4F88AF482DD0 + 1857839CFC13976917AE047472D1B55F5258CF22C40B43167B163352273D2913 + 27D076B163CB2A53C4B3CA7795769D5C95DAE4E5B88325A977D6622494300B12 + 43F026B2EE1DC0D134AE78702D9A31909F9E4D783E8B049629D8458D4C4B0E2F + 25F386203692C263B70B9074B0A48445C6A6B246F25744B095023692C063774B + 125B485DB4A03B83A55E5AF55949321DA6773DC5F08488E4AB312EAE14877611 + C122852D31AC745D223867F2F142F9CD25D22C974B048B0036F70513563DB425 + 8857C7C4AF2012580470400470B9620860DD0BBC92A79F41113EFA05AB2274A5 + E6B98E68B341C8AC9BE750AD8A408561CA226D2B08D2EE2B32FD420E7E9A2270 + 45EA8ADC3504AF88629A92F68D35849A710B49E2B75430A6BF98377611BF7AB6 + FE5E88045E4873519A88B14D9258CEA4B0E264CC3DC2DA58D39D6B897A0DE966 + D3DCD5A63BDC24092CC960A9AA5B4C24F81EE26C97A4C1B293C26A3E8390EE1C + AC97916CD591EAD4916C57C72823D6CCC35F629FCCC759AF0C52DD12327DFA75 + 8AF4CB19E95C43D8B699462363543BC7EB8E21788574ABC03239889493C64FCF + 52ED9C6E958D0A691B46905DF708E7BA6564DACC4769D02EF76483E9577248D5 + 985F5D1BA4DA28CD19291AA985D64668BC98325231926C49B0AC71DA132CBBA4 + 64A45E339AD126651CF6AE5F6D6C46B38C53D44629CB5CCF8D374CDD34321597 + 1815499A701246B249C4AF3FB64AF8680F209214511A75EF8094CA59A9F514D1 + ACFB8D7351E615615E6183B0FCD98E118691BB20A9E4B436FCA51294F9EA8EDF + 783E4CC82F8A8436CC09A98476A46E54ED980E22105B31082545B2FA1166B932 + B984B9733757C9225FC92357CC1872569BF225B65B417E637BB35B43BD5D4189 + 6DA9CDFB56AF8E56A78C6C29897082758BAE1A75CB69492FA95D59CE04CB6F36 + F1594691A892C84D1762704AAC43218A24DDD2793E2B6C5A52D30184A37EC493 + 116444BC95E3285462C815182E134022C53C922B484A9A3717402AE7433CB38C + 58DA2566D339DDC11B645A014415CE0920CFF25474A76335CE76A15BD1877A2F + 8B4DDD1179344447870E589E4239817A2BC7F07C0F7C5F11B6553A1B62DE31A4 + B241A6B7C4BC9759D6108A9528F2C500B2F96594AB41D49A4943D4E60B61730F + AF930D18F5CB79963BCF77E16442484497904A2CA3528A1992B75288A3C87E20 + D5C525BEDF12DBA8528CA0CCF87947C4670805BA89744C240248274270521114 + 18BE51CEA1D328A2D72AA1CFEF6863AD82E31B3D9C9DF67020C9BBCD3C36D71D + BAA771B45FC1BD3B3DDCBDDD36E4EFD16101A7C725DC3C29E3F6690527874586 + C9E1E44611BB3B19745A7EA6EDC3FA284E24987E18A35E0C1BC3387ACD005AE5 + 550C9A61ECE8BED0CD1CF6370407071B19EC8D62D81B4670CA74EE1CE671B69B + C0C9569466CA90C26F3F6CE3CD975BB877B788DB37F338BD91C1C14E0C473B09 + 1C6CC5B0390CE278AF809B371AD8651DBA8D302AB96514D28B286757D1288558 + 8604B646796CAD17B0D9CF627B10C3F1761C770F1DBC72BB84D7EE96F1CA6901 + 6F3F68E0937736F195472D9C6C44706318C6ADCD0C6E6D886464D9764A2C7316 + FB9B5923E93CEA44997618BD46C2DC1F5CCD8550CAF85163FFADF3DB2AA5FD7C + 5E4195EFBC515C41ABB8887E7D19FBEB51DCDCCFE221DB52751401FE88E6E39B + 35DCD84A62AB13C05E3F8AFD5106DBBD3486F528BAEC33FD5A8071D37870D6C4 + C3B3BA69B3C321DB6A2385D76F36F1B5073DBC7E56C283C3085EBF1DC7375FAF + E3876FB5F0ADC705BC7B2B87B7F8FE5EBB51C59DF51CB6AB7E8C0A8B34C3D8AF + 2570DC4AE2DE80698FC2786D378CCF5EADE117DFDCC66F7D6D84AF1CA5F1886D + 716F3D867B7B0CB71FC79DBD185EBE51C0CBEC1BB7D662B8D90FE236DFE3A36D + 07AF1D95F168278B33D6E1D65A1CF7B624DD1BC1D9D087DB6B7E3CDE8DE1EDD3 + 3CBE7292C76B3B693C5E4BE0D5CD245EDF2FE0F176060F361278C838F7371897 + 6DF5846DF5FA518EE9FBB15BBD8CE3CE75BCBC1FC1FB0F73F8E8B18377EFC5F1 + D64DE6BD13C5616319EBD9390C32D7D0CF5DC5A8BC8C7E6515EDD232DF911F8D + 9A1FDD36FBE9A068247F0BA900CA1C73DB1C3BDA1C7B3A1C5F4625B6496719AF + DE601E8FF3F8FA932ABEF556159F7E258F3FF9D916FE1FFFB7B7F1FFFDDFBE89 + FFE3FFFD4DFC2FFFF175FC9B7F7684DFFD410BDF7EBF88EFBF5FC74FBFB98EDF + FC6884DFFDE626FEF437CFF0E7BF7B137FF23B37F0677F70863FFDF9197EFE83 + 5DFCF893013E7BAF831F7C3CC2EFFFF8187FFCD37BF8E9770EF1D9FB7D7CF26E + 171FBECDF7F9660BAF3EACE1CE6911C707ECBB6B0E465D96BD9F62F973D814E1 + BB51C0CE5A1EC35E0AC36EC2F8EDF0FD9E1CD47076D4A4BD40F724361856873A + F6766B581B15D06BEBBB4DA325F2B8CD6F70B7825B3707663CB871D4C221E34B + 05FB81EEF066BCADAD024623F6CDAD0CC7830ABFC50E1EDCEBE3D1BD2E6E9F35 + 3826547083EFEFF8C0C1037EAB6FBDD6C157DF1CE0B5C70D3CBA5BC29BAFB6F0 + C1BB1BF8F41B07F8E1774FF19D4F6FE0AB6F0D70E7AC88FD6D8E19AD55B4EA7E + 734778AF9B42AB1147A5C2B1AB1444B5164787E36FBBCB31B0CEB19DE37126B7 + C27195282C71BCF7A3580D101C67AB1C0F396E97AB0986E57F88FF33DD671E8D + AF22A0FBCFC34BFC17285C9263B00EE030CD6ACA1CC419AE95B0BD57C7EE5ECB + 10EEA3CD22F68E1AB8797784B33B431C1C37B17B58C3FE490D47676CA3931EC3 + F78CCAEA463BC7F47246C578A62875DB3AA4C47F502E69EE62D65DCEFA2F5638 + 2F69704E52E67F235B89F3BFA77BA983FCF772EC2EA7595FC6975AF86CDC1CBE + 8A73FE6054CB331D69E0D0FFBBA07F1391AF655114C12C69ED1EFFB1DD2CD375 + 50625DF235FE279A31D43AB457D96635FEA3E99EE6FFD1A971CEC17859CE7772 + 52B3DDE45C48731DCE7D3235A9B3E67C88FFEF38E73C1196232AED1F9CBF6575 + FFAE0EF015D2E6605B84E5736A39A3D6DA69E490AC48EA378904F310191CE43F + C9C7EFCBAF436C2CBFB923B856E69C29CF67CE438A592351ACB9953DACA6836B + 920476554DEB5E67A9684E60391AC14A2C6AECBADB39988A1B53646E4677F48B + 04E6FC34219299F3DC68CE3124F26A9CE9723E2A695F219062D933590349CC06 + 397FF7C73947D59DBF71571AD82B3D2BD5C846EA572429DDDD300C1B4B1A495B + 11AEBA375864AFC85F4300D3BE1C663AC1041682313ED3CD48058BF41569ABF0 + 71938F88DB7315D3514352BB770EBBE4AFC854C1A89E8E445C72754CFAAA8C46 + FD32CBA9B46CB925ED6B89DF6902D812BE3227F7E91222868DDB58E5B2DC45F4 + BAF7F54AD2D7854855A97B561E227EE745D2FAC23077F032AEC2B8C4700CD224 + A434DC74E4EEFAD9B69514B02487958E7095F1AF4CC0B2306D49D91AAD4F9664 + 3412C10AEB92A6BA3F7702B9ABEC8C7F4DA434F3B8CAB25EF689F8946A651757 + 57DC7B7645161BA9617FD8E4F9D2B20F9708A98FBECA38A65E5221AD3B85CD5D + BC7E93A760EEE5A579856175DFAE2B652C925379882876894FA32E7A4C225BA2 + D9EB7679496E9242663E6395D5465DF5B8CE17085A8677D3A5FB12A1BD07D9C7 + D0C1F51799BE25806711BC93761CC390D41E78FD145E715F645A82258145FE7A + 09600B6F5E36BE85D96759D41E11D321B407A3B29B7690BFF258D45E15FD194E + 7E869CF4BBFB1C76DFC3EEC35832D2250DDDBD0ED94D58ED9584532E51AAF866 + BFC5254F0DE13ADEBBB8AAFD0A91B834AF8FF73014EF5A24453FC627AE69EF83 + E6759A733187A683AB63CCA5F266BFE47220899794474C7B0A79A341ED723C83 + CB0C7F3592653ADA3319636CD721FAAB11876933CDF039AE06D3665FE8CA786F + E812A1BD212F44FEBE247277028613F93B767FD11F3378C11735B0CFC2E43E62 + 4FFB599876649B6BFFC980E105A561DCC7D03563D66E6108DFCFC1DDCB125E5A + 7521FBF93E97085BD74D7B624AC390B82275975856A962967DD9B56B1FF0D20A + E31955CDEEB3D7CD8B2B3EB6E104CF2680852B01C740F60B843061A5838D9A68 + 11D12281F5BD99BD48DA59567B77F00B0B917368BFD3807D537B85DA13D4FEE7 + 1CBF85F9C878DF94DFC2BC1FD7E738DE5C27E6F80D695F98DFC04B1C1BCCBDCF + 345F34DF0ED3E477F322717995FD4044B53FC9F6671917682E258D7091390462 + BEB3009FFD2C8B8FA60B1D14310230DA57BE7ABECFFDC255BA696FDBE23AE35D + 73F7D7B5CF6EF6C8BDFBE984FCBC7BF1D3B0FE067ABECCFC2F717C252ED13E2B + CE3426794EE12207710E4BEECE82E5536C98697EE469FCCAE7F03C61FE0EE1E5 + A29E05C3633D0FC4C5195EEC221738E1C43C98F833EC8B2F32BC21563DF19EF6 + FC2B40E99B3C2C4769FDBE4CDACF1BD686FB5CD8292E751AD384EDAF8B175EB8 + 7E01227D2704B00D63247BA730AB7016D3612F60AAF25F16B6F1C6A63A897B69 + B43A20DD548FB1BB2184E9663BD2850EF56BC0A4FDACB4CC8B3C2F9F81FD50EC + F3C4DD7E18F2E740E1F9A0BC98F5613E0FECE033CBEF59B0F1BE28EEF38613BC + 04B04C0B6F1A5F88F1C91BFB6CA57305EF80EF1DC49F176670675C613A0DEB6E + 7E3E4F71FF5250DCE974EDCF4F24EC58F5F22C15CCD3CF7FEFF090B8E6073F26 + 5A276E1E4C93BF5ED83036AECCBF3F70F2C20992EC6622C4499297049E4C9866 + 40E1267649249B4917279A069A745D24702DEC42C89C5E341334B71CE7F55699 + DC70E69423CDA791BF963CF5627AC2EC92BCEEC2C3850DFBF9C9B1C178E22B4C + 13BE16E7FE5C508C172CF6D4E7B4DD2C7038E137273A69EAD9BAD989BE99BC8F + FD2DEC22E70AD3D0624271E4661638E3458E4C4BF86AA164164BF69990DD2C9C + E4AEF408FB6C61096211BDE6CEE0312C196C9E53052CA65D695C2B956B895F91 + B4968C15796B8961AFB4B02497AF45743FAFAB767A212E42D9258B8D44334D11 + C05E58225850DA96FCB5F9DA3CDDF869BA31ED244DDAAFC7B98024E674CF1621 + 32587E8B6987E5575A22946977B246BDB491042E940C31BCECE40C8C9430E1D7 + 5D67E532C2958A41B05442A05834A6DC0549F75A3F8597A4AE91D6D51D62BA7B + 774CE25A5257A685BFC43C8A7943EA8AF85DCD670DE4E60D2FD392C80AB79273 + 0CA41A7A35EF98F0BA4358C46FB251276AB45711AF96102D971016A9AC3C1CC6 + CBA4B13A268C83259543E4739E6917116F5691EA34E00CDAC88D3AC8AF758936 + D2FD9A91FC8D88806D4B1D749570D532876B19D62381603589589DE916636CBF + 10EB1F45B49A41BCE618B5CD2270754770B65BA65B16B18A43E49010815B9124 + 6F0611A95414E9CA7C1CE627F748296524796365A653D47DBDCCA72052D7BD03 + CF9AB1BCABD6598815928608B6881524A9124358122CDAC43452B89200A63BD3 + 73D34CBA846F4D9BAC2E9C7ADE90C169965584704269E525A52295C601F8A23E + 03DD012C495891BEF1628269C5112DF099ED22B58B8972947617894A0CA96A02 + A95A0249DAF5AC4D5987EDE8D42519A4B892D0398721818B71A62F82398C4826 + C87A30EFF012FCA14584A32B88B21C7111C09908726C476D5497CA5982662583 + 32DB5CA8D45DB3C377BAB53BC4C6560FDD4113BD2131A8A3A10D67953FB16AA4 + 70B3AC4FB52D359F39E4AA29E4583E49E01A9424891B4381E52E3532F44B18B7 + 22DF779179A625119C946A6791BD49A69B44B11A45A51541B941D4C22C4FD8D8 + 6B746B76E306D57604D5560C954602854A04D9421069C78F687C193111A759DD + 7B99A7BF36CC57F86E96D018A4B07DA363EE8A1C6EE84E6011CD09345B59D629 + 8B02DB302B02B61845997E4523CD1530F7EB4AC56AB916A37B98EE3E9A01A36E + 359D5D412ABD8A0CF34E49CD6F3208C7D11DB14924747FB07F0591C02AE29100 + 12513F9211B6BFEE6C0DAE20115E4136154041E933BEC372A762F4677B64D9FF + 12912062A100E147816D336856B0DEAD60B35FC67A2F8F9DF5226EECD57063BF + 84831D07BB9B49734FE9E19E837BB71B78E3C93A5E7D3CC0C9511E7BDB0963DE + 3AADE0682F8BF541146BC338B637B334D368D48268D623D858CB62D84FA39C5F + 412E358F0ACD0AEB584C2FA1EA04D0E5373C6A3AE894F81EF2419AC20AFAD515 + EC7423B8B59BC1C3932CEE1F25F0F03885771ED5F1C9DB03BCFB5A030FEFA671 + FB248EE3BD308E76A2B87994A13D8D0D964577A88AE05AEBA6D16DC4D0A986D1 + A7B9D64D11718C7427692F82F54E08A3BA0FBB7D1F6E318D574EF378EB5E096F + DC76F0FAED0CBEFE761BBFF9AD7D7CF7FD119E9C31BFED00CED682381EFA713C + 0AE36C2F8713E2ECA08893FD2276D652E8D5A50639CCB64DB30E596CEAAEDD66 + 121BEC5B03A9482E2DD35CC47A7D113B9D451CADADE2D1711CAFDECCE0ADBB85 + 713E3EDCDF8FE0B5B3241EEE89340DE11EDFC583DD344E86616C5416B0569EC3 + 56C3871B8304EEED16F0F25105AF1C15F1702785C77B717CF4A88AEFBDD5C387 + F7B378E766081F3C88E1FB6F56F0D3AFB5F0DDD7F2F8EA5902AFEEC5F0683381 + DBBD380EAA011CB1EC777A49DC61DBDFEDAEE2C95A00AFAFADE08D8D25BC7F18 + C5A7B70AF8603F8D37599E4FCE0AF8DE932ABEF36601DF7DBB8C4F5F2FE2475F + EDE3FB6F0DF00EDFD5E3CD00DEDC4FE0DDE334DEB911C35B877E7C7C3F81DF7A + AF89DF7EAF85CF5ECDE3833B49BC7F278B4F5F69E2BB4F3AF8F461111F1C87F1 + E6D622BE7610C6A7F7ABF880EFE4F5ED28DE62BDDE66FDDFDE4BE143E6FDE14D + 07AFAC2DE276EB45BCBC790D9F3C8CE317DF6CE34FBED7C22F3E2DE237DFCBE1 + EBF77278B2EDE0F17A16B73A316C1597B12675DBD979349D4BA8392FA05EBCC4 + 77B68241238E462E867CCC8702BFAD6A3C8406BFBF11FBE63EDFE54DBEF7D76E + 44F0DE03071F3CCEE35B6F14F1E38FABF8F77F7913FF9FFFF56DFCBFFEF303FC + A77F7784FFE12F37F04F7E976DF1491C3FFAB8845FFC600DFFFCE7A7F8F3DF3D + C69FFFEC007FF367B7F03FFDE51DFC4FFFEA14FFE12F4FF0377F72037FF98B7D + FCD3DFDEC2EF7D36C0CF3E1BE20F7FBC853FFB9D43FCE94FF7F1F3EFF7F1BBDF + EBE0F77FBC819F7C7B808FDFA9E1B507053C3A93D4701E27ACDFC13AFBDA2889 + DDB5248EB6B338DC72B0D1673FE718B7D68EF1FB4E627BC86F7A5DAA99D3FCF6 + 63E67B58EF67B0BD9E3307180EA4667BC3C126FD37FA0E4EF75A78747F13F7EE + F09B3F6EE0F4B48E9B379BB875AB85BB777BB871A38AF575F69BC322EEDF1BE0 + EEED2EC3B68D5AF787F71A383B66D96E38B87992C2A3DB49BCFB460DDFFA7084 + 6FBE3FC4C75FEDE293F77AF8C6D7BAF8E68703FCE8DBBBF8F1677BF8FABB033C + B95FC2E94E045BFD156CF11B3BD84CE3703B67EE09D7FDE07B3B05ECF27983DF + 5ABF17E658BB8C5A6D01DD8E0FC34118DDAE0FBDAE9FCF4437847E3F862EC7A0 + EE2043E4CDF85CE63FDEE1FF3936D608118E0590E1185B2CF39FC37F7D52D709 + F0FD67F9BFAE37F368730EA17BDA87EB15EC1E34717CD6C3F14D8EFD27151C9E + 9671E326CB7CB78AA3B32636776A469343B11CE738EF22A37F35D38CC6C38827 + A21CDBF93FE6189CC9A64C9ED57A11C5BA346388280EF27FBB8A843404F0FFAF + F2A86C296910C8735EC3B987EEB817726591B9FCF7711E24696269ED28F01F55 + E1B8D318E609A6AD7B847B7CCF0755BCFAD60E1EBFB6864657AAC3E71897FF09 + FE7B72B528F21CA7B275CE0B3887CA6A3EC27988B91B9AF3BD68363DB9B2C25C + 55C1722438BFD3BDC392CE9594AEEE8C8EB2BD52FCFF67398F71F8CF771A0ED2 + 2C5BA24CFF02E7404E18AB495D57E13324B0AB4185F139C711C91B2B708E4688 + F4952A69579DB47B6FB125800523C11B8F4364AE2F113744B0207B98734D57A2 + 5777213BE65952BE921EF6293CE18F2710E05C7D351AC7522882E570D46025C2 + B92421B7C560C410BCCB2262C32EA16A2568458C8A209DF787B018089BB026BC + 247EF9BCE0E7738061B9CE5AF047B1C435D632D7564BA114E3BBA4F0423886F9 + 10317E9E0FBAE4ED84048EB912C1227F2D01BC2AC95F967D59770F8B3C9574ED + 38BC9704B6E4AF81C855A62B825675B175B02AA02D39BC3026818D44B00796EC + B6709F45DCB2CC2CFF6288ED432CA8FC6C0F23B56A31958E51092D3BA1BC0411 + C622C595D61CDB6ADEC7728D21B5CE961C152CC928B82464002FAEF80D5E5AA5 + 5DE4AB2FC8F56F88EB58AEE999FE9540F0022EFB035C57FB9996485A175757FD + EEFB1CB791DA525A8B5E5A59C5A56582FE970973EFB0219AFD17E2C9F492C186 + FC1E9B86C865592F99FB6E5DC9DD69E257B084B0081957C297E594DA67D6CDA8 + BB96D433FB93C85143AC324D11C09248367711AFCAEF1CF67AA94B6A370F116B + C9584BEE4EBB7925AC05AF9BEC0A6B2580F50E0CE94B77BD1BB9C9DFE6A7B22A + 5D0B2FA12C7FB34F23624BA4D792DAC96D17E327537B3316E33D9A6B01BE1B8F + 84ACDD6BB107DD05AF9B086049EADA7D09B327C2EF51FB25DE3D0AED675CD6FE + C71893FD0D1D788FD1DFEE738CB59F192472E66AACCB74BFC4B097937C4E1570 + 3D53C242BE8EF97C159753795C4A66F152D2C14B095DA395679A0C475C89326E + C4C1E5B0832B11A647B7EBB1737F1386EEF2BF14CAE0A5609A605EC1149134B8 + CCBA7931EDA66711C2C28B6C8F17D82ED6FE22DB41CF82DD33BA80F1DED324BC + 07EEFE14FD45EAB2AF7909584BC28ABCB56A98AD2A66EFB3D7FD02C644F1448A + 5712BD36CDA97C66C14BFC0A861CBD0095C32573A7895FC112BF5E12D8C212C0 + 26BECC1965F2EE577A61F63BE96F9E97B49769C9618E6734CD9E24BF85ABD77D + 98BFB68AA5ABAB58B81EC0556927E498F0C26A102FF8C32E64E777F1C232D362 + 1BBDC07EF142C8C10B3187EF957D6689E612DD1586E3C40B1C4B5E5858255688 + 65BC38BF62E0DEC51D1C0B338520E27702BBDF3DC7B882246FA7F7CBC77BE65E + 02D80BBB2F3FFDFCE298FCBDE281486091C2B33089F754AC4E602586BD6EB360 + 798BA712C06378E34C6356788BCF85F312CA34BF28FED3308B8F9A85CF11BD4F + 81E5BE2E70665EAECC83E93096579B09CBBBFDBA9895B6C5ACF0B3302BEEF360 + 565A5E78C9DBBF0B4C13C082DC6712C0B3C8DE59F81CE9EB052B31ABE2CF0B75 + 0C99E3067991B8CCC1EBD2D51533702DAEF0877F9D038D29874BFE4EC27BD379 + 1AC6E9CEF47B5E8CD350DE5652F86970C3DB8F63F6876531EBE3FC225C1810BE + 046CBC5F75D0781A540F2FF9FB6509E0CF0FB6E783F2AC1FC197C134213BFDD3 + B198849DE167E0757B5AB8596EFAF949CD84FD115A48EDC4B4DB0CCC246F7F45 + 4CA73921707F45027856D88B84EDAF0F4BF84EC34E862E4C94BCA7E43CEEDE70 + 32A7275A1309604E84AC9A65033DCF801BFE22E46E09E04B63B2579845006B61 + 21D33E5B58095F57CAD75D8048558E4E4A7A318BDC9D05EB2F62D84A074F08DF + F12245A67533EE63F2D662DA4D937AABFAC71BCF86B327487572548B1F11C0D7 + C25CF884B4783A97F8BD161519ECC23DC9EA2EA02CC12BF369D0C95AC1841B63 + 9E8B286141D2C16302D810B052DB4CD34AF85AA95D2F896BEE29F6C0EB6EE319 + 82784CF2DAF85E7825800D092CD279FC2C73A22A7A8C05966B2E25E2988B486D + 1688FC4D67B0E828AC63B090627E7437C4702AEDFA4B8DB448DFAC4BFA5A2C39 + 0E963319AC8830CD6627A4AEA0671FCD09115C2A1BBB257F9715975862FC6527 + 63C85DC192BB5EBB179608B67611BD5E58D258769BA660C96291C3D18A45D155 + E54CC46BE5893D52665A228C73226AB388D6E9D790FA6949171710E173A25387 + 33EC20BFDE436143E8F2B985CCA049B7B641762492B84254CD5DBDE16A9AE54A + D04C31DD18DB224AC4D8364984A57AB09C42677F845B6F3CC4C6E93EA225E69F + 4B1835CDB1324133988D632519C26A2A6CEC822F1DC1723C60DCA5E259C4AFD4 + 1E26C712BF96C00D33ACAB723962A0FBEEA22282F309C40B52B59830EA9125AD + 1B48041148499225C2780CC73096FC15E19B6F950C247923335DCD1A1259C472 + C8E411667CDD27ABBB0B59AE44C8D883E9B091FC4D18C297E1F352AF1845926D + 22C2375D4BC269A4916BA791EF382874B3281205DACB920E1A9450EEE6916D66 + 182786542D864C236190AE315E33873C9191047025854C318104CBA27B731344 + 34B68A7078D198894C0829270287F593F4ABA4AC8A4496EF24930B23C978B556 + 01DB7B236CED0D315C6FA12BF257AA47595E87654F39219A51D45B7934FB5594 + 1A2C5B396188DF02EB526639852ACBDFD49D8DBD9C2164CB22836B2914594627 + 174582ED924C2BDFA421930B15A62915CBC31CFAEB050CD673B4A7D11E6430D0 + 5D95FB55EC1D36B17FD4C6FA5605ED7E16ED5E16AD4E16E56A1269D62BC93E92 + E37B2BD6E2C8B7836CC7085AEB19B4D772688FF268B04CA5AAEED44C997B777B + 6CDF5EAF88569B656D65D0E9E6305A2BA3CB742BD5288AA520EACD180623071B + 1B050C070E2AB510B2B9554312972BBAAB576DE2679DC270588634FB66261D45 + 8AEF3E165E413CB28A0CED79F689547819C9D022B249C64FFB50CA8550700270 + 927E9A52151C83C37E938D8790679C2AFB6797EFA7CFF2AEE99ED07E069B439A + 83047A2D1FB18C7E7B193B9B51DCB959C6CB0F3B78E5511FF76ED5CD7DBBFBDB + 291C1FE6B0BF9342BFE343A3328F6ECB8FB561CAA885ADD79847D987668DEFBD + 14402A7E15D1C00BC8C42FA190BAC6325F4725EB4387FDB45D8923175F8013B9 + 8A56D1878D8E2483A3461DF35B8F3AF8E6D7B6F18D778678F7D5063E7ABB8B4F + BED2C39B0F0BB87F96C4DDA338F6D77CD819F971BC97C4D96101873B39749B21 + B6C33CD3BD8EB2B3CC3CC258EB8824CB6347EFBD1A40CDB94E5C45AF28227619 + 37374378F52C8F77EE97F0FE2B657CE3AD063E7EB3467B09EFBD5CC41BB753B8 + B71BC0C9FA2A6E8C5671BC16C2F1660A37B652D8DF48626F3DC1F413586BBBA4 + DBEE20875B7B0D3C3A1DE0D17117B7B6CB381C65B02BB2B51FC1C95A1CB776E2 + 787094C01B77F3CC3B83C7ACCF9DED008E878B381E2DE1F6A60F77B78278B89F + C4EB67453C392DE2E67A1CDBF5558C4A0BC43CD6CB4BD86D06703A4AE2FE6E96 + 71E238EE2DE16CB080473B01BCB61FC087F752F8C13B4DFCF25B5BF8A3CF76F0 + FDB7AAF8F07E0E5FBB9DC7DB27793CD9CBE32EE3DFEA45F1683D8D5736127879 + DD8F77F64278FF4600EF1EADE0BD233F3EBD93C60F1E97F1FDC715FCECDD21FE + 90E9FDC17736F1FB9FAEE1B73EECE1F7BFB9835F7EFB003F7CBB8D6F32DC4FBE + 3AC46FE9CEDC870EDE3B0BE3BBAFE7F17B9FF4F18B4F86C43AFEF0D35DFC92F8 + FD4F76F08B8FF7F0B30FD6F1DD578AF8E0248AEF3EAEE2E71FEEE2476FF6F0E1 + AD0C91C2B71F97F0DD572BF8EE9322BEFD72061FDD89E2FDB310BE7E3F8CDF78 + B78CBFFCE92EFEE35FDCC1DFFED31BF8A73F1AE1C72CC7C7F7EBF8E441175F95 + 2A71B6F921FBF75AF13A8695AB586FCCB13FF870B8C136EDC7D193B4686A05A5 + C40A6AE98051857DBC56C1939B437C95E97CF8721E9FBC51C2675F29E3773FEB + E19FFF621F7FFBAFCFF09FFEED29FEEDBF5AC39FFFA28C5FFC24839F7C3B801F + 7D33849F7FBF8A7FF98B5DFC777F728A3FFC8D019F1BF8D39F8DF0577FB28B7F + F7CF8FE8BE83BFF8F906FEECB7D9163F649B7C7F1DBFF8C1267EF4510D3FFE7A + 1D3FFB7E1F3FF9B48AEF7EBD80DFFC6E073FFC5607DFFB64806F7D30C2BBAF75 + F1D6CB3DBC7CAB8133BEF77DBEB3BDF524BF8724B67520A317C62EC7A81B3B65 + 1C6C17F98D27D16B84D06F868D7D5B77F3AE89F4CDF2DB29E1D6610DA70715EC + 6E381876E2FC5E92581719ACFBB6B7B2E6DEEF93D3324E4EAA383B6BE0E8A86C + A47F4F4E6A78F2EA061E3EE8E3E8208FC3BD0CC7862CC78D0A5E7FD23752BD6F + F39DBDF3A48A4FDE1FE237BE7388DFF9F14DFCF48767F8FE3777F1D9C71BF821 + FBC0B73E5A67983A5EBD5FC6DDE32C4EF7533839607F66FA8FEE0CF0CAFD75BC + F1EA2EDEFDCA295E7D790BFBBBBA83DC31770B1FEA30CAED1AEEDD6F1BF3F4AC + 829B37EB86A4DED92962B49E475F77AD77A51E9B63728DFF0E49E1666288E83F + C6B134CE71516A968504C7D4880E3825FC1CFF43C8182D0E11343BFC7E0FEAB8 + 737F84C7AF6DE2953736F0E4ED2DBCFE954D9AEBB8FB68C47F5CC3DCB9AE0344 + 1ABFB3D2E0C0FF90FE8D09DDBB1EE3F84D33C5BC8DF60C49F056F88FE5BF5B77 + CC4B2575AA184496FF2D876E19FED3DCFF18FF31F51C8A92F6AD39E6DFE894E8 + C77025E657E17FA6DCCEA0D24DA339CAA1B755C270B782DE7601FDAD3C1E3ED9 + C18F7FFA0EBEF5DDC7D8E67BCA1417902E2E235D527E7E24F3412459D6A4B455 + 70FE61349A1452883A09F8F9EF580EFBB012F59BF988A480A5B239944E21A843 + 6E8618E67C85FF27AB5D4452C5CE84FC8D19ED24BA0623C0B9C26A224004E1E7 + BFDAC7E765B6F3523448B788C14A9CEE4C5744B0BDBA2298617ECA33939E10BB + 8154C290BE9608B612C2E659442FDD64AE4439AF8B304DDA5D22386108DF7911 + 9022FE88C560184B61970C5E14B14A5364F0B2246DF92CD2D52715CD84EC8630 + F5B924B0859EAFAF06706DC58F397FD825317D612C04A24C4FE46BC210BDD745 + 1233AF25A5C37C44A8CACD10A2226645CA2AEF18F3939432E7F601AE1F24AD1C + 1C9B2A87248FFD49BA497299F073CD2189622F216CC95E4BF45A7809602301CC + 363052BE348D14B1C2B10C8654A62942D42581553EBA87993EB11C491AD32581 + 6386D09514AD2B491BC415439C325D11BE263ED396C96791C4224A4D78134FD2 + C3227D5D22D512A3162ED1A8F578C8908F2F2CFB0C2C017C89ED7F99EF4152BE + D7C331CCB10ED7F9EEAE8D21BBB98E46189761023EEB5D2C475927B6EB1CDF8D + 258DAF2A1E4D91D7AA8B24935537D356E3F8AADF559641E4ADCA2E0257507B88 + 04BEB4B8CA6711C222362F129E93FA4DEC6EFCCBCB7E5C3652C0741781CAB699 + 1B93B146125AED357ED67550567DF239F97B0E2FD16BC9582F216BDD94967BC5 + 944BF85A5C1F9B56DAD780EF4DEFE42AC3EA59EFC410BF4A6B5C2E9BCE749EB6 + 3C6A07915022C74DBBA8EEF25B6138D6C3D4477B33F6D91F77095DC22580B54F + E24A8C5ED55E4940E6398C7A674BF286532EC67B1EE6A0BADC892BE173025852 + B957B5F761895E4244AF20D25752BDD709D9AFEB395DC0659A2FB03C73993272 + 9B87689FDC43B4BB8E1719E7C558065792395C4B1619A7700157E3795C89E598 + 1FFD634C374EF71924B02582276698694E302E3FEB718975F212C05E22D8E225 + D6F125BAC97C91ED26425724AFF67DAC2AE90BE1E967895FD92D2C616B48DC55 + 861D13AEDA9B33A62570C798267EA7E14D4F98C41BA76BD3FE32F0C63D87D276 + A5785D32F7E9B86C895EEB6655421BF297E59C91FEAC727861F634B5BF2955D6 + 4B34053E9BFDD1058E0BD70398BBE6C7E2B580C1B5797E23DA375D894C08FB17 + 2541BDA8FAF1DDFBF27423C265BC1029E1257F89DF4B957E057E9369576A5AF1 + 17020656FAF78539C16F6009E009097C9DE678CF794200131288BAB0473E8569 + 22D8EEE57F8E07B87C91009614B0174F238327F13F87732EE25938E7293E8F69 + 6EC46256582F66C5119E27CCAF82691EEA69F092BCCFC205EE6B0A5E6EEC7361 + 9EC6CD59F7BF0B8CD37C5AF966C6998559719F855969CC822565FF2E318BFCBD + 4800BBB0CF4FC33F08013C8679416C10A3067AFC9C2F77B077741B41FE945E78 + E11A3FEA450E02EAC4EA4C0AF31C79AB9167B93F27BC1D47762FBCE12E60F271 + CCFEB0BC98F5813E0B4F1D10BE408DC0DFC740223C6F9D6CFE5EE8E4CCE707D7 + 5983F397877E169FFBC1D07D2651FB45F0C6795A7CAFBB7E7A4F2379E5EEF57B + 0619EC25709F05AFFAE7592AA085E9342D916BEF80982673BD64AF174F0B679F + 2D49EB2548ADDBDF15BCE4AEB54F13C01617264DE367EF44CBAA57BEC4C5C144 + B296D0B39794D5B375B36A99BD71BD619E16CE9BBE9EBD71A69F8D4A6895C383 + 6992D7ABC6C7E273A4EF185AF4D845CB8505CC0CC8CF4BFACA3471386917ECB3 + BB8862F871BEDE725D0F7111C48590160FEEE28069EA0E1B230D9C35B004B016 + 1E57942F4D490A4B75B420C2781A0A33395D3BB6DB6741F1E7B9989A97DAE684 + 4854B9B19C5384EF34016CC95CFB6CDCC6F012C016227645F40A2278054B045B + 77EBB79C2D1AACE44A06CBB982217B45EACE4BCD31ED4BD91C560B4583E55C1E + 0B19E661FC33C6AE30322D14DE55FF2C02388345A533864BE6BAC4AE8595F2B5 + A4B0217D99B6C2CF2725859C609B2798569A65651A34A5BA79319332D0B395E2 + 9569C9DC69E2779604B1C2D9B07E23C5EB22542C8C2175D079430427EA152475 + 871C2132582470A8983561E42FC9DF6059E96511A8E411699491EC36E00CBBC8 + AD091D43FE8A04CEADB50CF99BEC48FAB760A48073831A52B4472A6984CA0944 + AA29241A59E69BA39943B299372A9D376F1DE0D1BB6F60EFEE0992D59C91F64D + 49CA56770AD759CF7C12CBC920E623AB980BAF187321EAC322B114F523A0CDCC + 421AA932D32C6410761270D5384B2A256CC8594B02CBB410192CF236ECC4CC9D + B561C210C5F2A79F71CF2720B5D1227B4502671B05034901C70B4943EEAEC459 + 8EF01296828B580E2E23180F2299D5A66F061942E162BAE7B02CC2373356B598 + 3004B0085C4104705ED248DD3C4ABD022AFD22AA83223AEB75F4369B68ADD78C + 5BAEE5204F141856C84982B5A3F06554BA42099576D16C3C4B2DA5538A23E504 + 114FAE229AF2234ABBEEBA8DD01ECF0691C886104DB30D634B580DCFC3175930 + EA9A1BBD12DAC32AEADD228A468237C9B474A76F784CF46A233B877293EF4984 + 33D3D27DBC4E398A7C2D81423D814A2783D62087064D11AFCD6E0EB556C6A8F0 + 14819C48071189FB118949E5B21F4E2164FCFB6B55AC6DB7B0B6553744B0EE7A + 5C173972DCC6ADBB6BB8FF681B37CE7AD839A863D710C21D6CEDD6D194CACC52 + 08B962D04812F77633D8BBD9C0F1DD2E764F1A186EE6D0EE27516B445015E149 + B33F74B0BE5532C4C3F66E9569B5B07B50436F9046AB13C56094C2CE7E0567B7 + FAB87D771DFBCCAFD5491862B8D14C98B8BD618E6D1D84549BA6B3AB466AB85A + 8BA15E8DA1520ED38CA0DB94DA6111366974EA51548B7E14B2CBA85742685405 + 96A593C5B097479BEDD7E2F3A89DC680FDA22112BAE443B71132F7C8F6DA7E86 + 1356689FC7A8B7841B7B71DCBD59C4EDD31C6E1E3B34F3B87356C0BD9B15DACB + D8D98CA1D35C42BBB184B57E88CF696CAEA799469C6E01D48A2B2865B90EC85C + 47C1B98C52EE32EAE57973E7EEE62045A459FE389A651F6A8545AC7542B8B193 + C69DD302DE7CA589F7DFEEE2E3AF0EF0F577FBF8E82B6D7CEB831E3E78AB8A7B + 47111C6F0670B219C5766F95755AC1D6308CC3AD0C91C5A0CEFE14BF8E78E045 + E4E2F368F0DDF5D8761BDD0CF607796CB6D9166CD76E71197BDD10CE364378B0 + 1FC2ABC771BC71338D8F9E94F19D775BF8D69B55BCFFC8C1BBF73378EB348697 + 0FC278B81FC6838338EEEF2771733B8ECDCE32363A2C7B6B01A3E60276FA41EC + 491ABA9760197378F9AC83D76E7518BE8C9B2CDBADED2CED79BC7C2C49E32ABE + F6A881F75F6EE0F5D30C1EECB01C3B01DCDF0EE2D6681537873EDCDD8CE0E15E + 128F0F92B8BD15C17E7B15DB0DD69758AF2C63905FC0A0308FCDDA2A8E86311C + 0FA3D8A92D60BD70095B959770DCBE8CB76E84F1DD371AF89D8F47F8F1BBCCEF + 5E021FB15E9FBEDEC0C72FD7F0B57B55BC7D2B87570EA278EB3881F76EA7F0FE + 9D083E7B12C76FBDEFE0475F89E17B6F86F17BDF28E19FFFC6007FFCDD167EEF + 930AFEF8FB23FCE94FF6F1CBEFAFE1773FE9E1B73FEAE2675FEFE1671FB5F1D3 + F71AF8A34F47F8673FDAC59F7C67033FFFA8851FBC99C307B7FD78F76405DF7F + 3D6BFCFFE0DB23A6DBC33FFDFE01FEE2C7870CD7A15F117FF88D1DFCBB5FBE86 + BFF9BDBBF893EF8DF0273F68E2AF7EBF8FFFFE8F87F89B3FEAE1AF7FD9C15FFE + 6E877E1DFCEC1B15FCD64715FCFEB75BF89B3F3CC6DFFEF92DFCD94FD6F1DB1F + B6F0BDB78AF8EE9B257CE3E52C5E3F16A17E1D0FF656F1F84614AFF25D3FB9ED + E00D497DDFAFE1DE0D07076B21F4AB7368E6AE60ADBE8A63BEB3472735BC75A7 + 80F71EA5F1E93B39FCDEF75BF81FFEE200FFF3BF3FC3DFFED516FEFA9F75F0EF + FF6A0DFFE35F6FE25FFC511D7FF43B19837FF14735FCC7FFCB0EFEF3BF3FC52F + 7FD3C1D7DFB98A4FDF5DC46F7D96C21FFC6685A8E1973F6AE2CF7F671DFFF297 + 47C44DFCD39F1EE2071F94F1D9D772F8AD6F37F0FD8FB2F8CACB4B78EBE122DE + 7B3D86EF7CD4C0A71F74F095572B78EBE53ADE7CD4C293074DBC7CBF81DB2765 + EC6F490A3E840D7E0F37F6B3FC5EAB4655FBCE6612BDD62A3A0DF6D751947E39 + 9CE81EEF3D7E13DB0973B0E3C68183A37D07DB1B52DBEEE7F8318F7A9DFDAB1F + C62EFD0FE8B7B39BC4E6661C6BEB71ACAF27B1CD6FEEF846D910C443861B7683 + D81CC5707AA384B7DFD8C247EFEFE32B6FB4F1C6AB75BCCFEFF8FB9F1DE1273F + 38C36F7CE7043FFAEC06BEFFAD037CFBEB3BF8E0AB43BCF56A1BAF3F6EE3F1BD + 06EEDFAAE1D6498568E1C19D753C7AB085D79F1CE22B6F9FE2E183751CEE9770 + 93E3F0A387433CBCDFC12B8F8778F3CD6DBCF6FA069E3C59C757DF3DC4EBAFEF + E0E6591B7B1C8347C32C9A8DB8193F9B55692948A35C8E735C0F21995E412CB1 + 684C2727B5FC1AEFC366CCCF16F84CE438B6569B618CF84D1FDF6CE1D19335BC + F5DE3EDEFBFA093EF8E418EF7EB88F276FEDE0EC761FBD91AE3DD0F502CBFC8F + AD9AB4324C3391E17F89FFCC88AE21A03DAE433AA508FF77FC7F7493E60EE26A + 3781C6308506FF113AB8D4EEE7986785FFAD2AFF5945869394AFB460B82A9D2B + ED381A1C47DB6B197437B2E86E66D1DBCE63FDA08CED932A36F91ED60FF23865 + 1BBDF9EE115E7E7D0F1BBBFC9FB7A2C8D558C6D20A12791F62FC7F87533E4432 + 61A35A3AC73953B69235573144397FF1C702841FC184FEEB7EFED3834488FF77 + 573389EEE9D701B5448EFFE95609F5618375A8A3CCB94396F3334903272B3AD8 + 16E5DC268065A6E74B46E04BC5B09290E4AB3BEF92B91861FA22961DCE85C604 + 705812C679CE7B386F74553B73CE29D5D0A9A4217EAD29F277311CC24228C8F9 + 53088B3405F31CE1BC2A2602348CEB7E919D01CC4B6D312189DA9598A489E386 + E0B57603CEAB5DB2D5551B6DC8D758C22588C3921816A12A62D395A01521E825 + 5925756BD5491BC297617527AEEEF05D62FA861C1DC7B19024AAD449DB7C45FC + 0A61AE232C425C47C88C703D122644028B0036C42FF33404AE4CA667248525DD + 4A78A580AD29A8EC4665B2C84EB6DD75B6938851439E8E096BA3E69A501ED3E9 + 79C95F4184F234012CD38079592959631A75CD4F8754215B5310317A79D98F4B + 639254900A6691CA229325616DC86B96794EF71213F322DCD9DEC29248F271D9 + 4DFD597EDB5E8AA3B08A63C2F31D2C28BEDA685C7E43FA7A4860D551EA9F2F2D + F9F0D2A2CF10B82A8B25C50DD94D5CF1A9FC2EC969C9CEAB22BA7DE724AC247E + 6D7DAFF945DABA61757FF002D7EEF361ED0F48BA3586EB3445D81AD2967E961C + 958A63B3CED79A5FEE81B89B36E388B4B5D0B317226CE743CC8398266B6517D1 + 3B89CFBAD9F48C6436CB7899A62913CBA8ABA4ACBF258D6D3D2D542F579A794C + FC12D29CE695E8D535556E3D9217085DB3D761F64668B7D033719D764107D7E7 + 8C6A65773FC3257A5D5254CFDEFD0C43FA1286D44DE65DA2774CF25A58F7F954 + C13593DA1BA863C1A9E245A639E794513FBC858D87AF2131DCC60BD10C5ED29E + 8853C17CBA6C48DFEB8CE385258245FC5A58F277FAD94B084FC03CBC902AE973 + 7808E271BDB55F7341AA986D6BF778CE554827DD67B6B7218AD9872C6CF89702 + DA1B3A276D5D1258C428E37B085CC17B37AF2578A7C378DDBD7E76BFCFEEF93D + 0D768FD0EE134EE379D31126E595DA694F7ADEF8DEF4BC980E6BE12D9BB417DA + 67C190C1DAFBD45EE352C810C1D7E638CE5CE7B831C76F6B61BC4FBAC47416F9 + 7D2EB04FCD65B1B058C282AFC5EFAFC3FE3724066C3B6279C43C3AB8BC58637A + 05E699A39961FC14D361FEDA8B651E2FCE4FED2B1BE2D743FE0A5F44004FEFA9 + D36E8960EF7EFE0502586E3308DE67C11BF7F338E7219E0A23C17CCE5F4CC3CB + 7778312BECD3302BBE17B3C27D99F8C22CCE6616BC24EF4C8CEF04FE223EEC0B + F9326266182F51FAEBC09BE6346685F7E279C3FDAA9845BCFE5DC312C02FBD34 + 6F20B7FFD208600BD3095E98C3259A7B4777F08D4F7FC80555DD90D957AFB163 + 5E61C7331D450DA8FC67A7F37705DB313FD7399F85F10732EBA39AC6AC0FF459 + F8321FFA2C7C51FC2F9BFEF3D6C9A6EBC584001E5FCAEE0EAEB306E72F07FBB3 + B8F09311E8F7B99FCDB4FBB4DF17612ABCF9C1797F7E4F8197949D05FB439D26 + 7767C1867D1E98F0C405D2566E1EF256764BEC4E633A9C17D65DF8FB2280DDFB + 7D2F92BC666233869E3541B27E763265FDBD132B136E4CCE5EB2C4AB885611AE + E3672F316BED4F83C24CEE1EF1B879FD2CF42C78F3B3760B1BD64BAC5A4C885D + 0FAC9B775173E1D943F05A02580BA18974AD776144FF8B04F038AE590C8CD360 + BA2A8BEEAC91A9C5A3B78C93D39FE389BF5D3459F2D72C2EECC283690A2271CD + DDC15A4CD1FE3492D7EB67E37AD310093C97E0624C64AE885EC292B986D01D93 + BBD6CDAA6A9E679C09E1EBB527CF895D2FD1EB4AF5168DB920C9E331E9BB9829 + 4C600960859169EFEF15896B895C11BF7E49E7962B13125838BFE7D70DBFE828 + 0ED377B2C67F55717374CB6458FED44C02D83E4F88DF71588BEB712E90A351B6 + 518CE54F1ACC271374E7829E984BC4CDF352C625862724705E92C505E80E6163 + 1F13C056FDB3086041EEBA6BD8202FF2D6950C16F9AB7B8043C53C913364AF24 + 7FBD04B048DFA8544A1BC238675443CB2D5215192C55D325C4A40ABAD744764C + 028B004EF6EA48742A74AF20DD95BFD440D7901FD491EF578D6A67A979CEF6CA + E659D0734A92A3222DD73A686CF451EE37916994E0344BC87718AE5D81D32822 + 5E761074E2588A070CF93B1F5E319B8FDA981456132143024BEAD79F92644A08 + BE64182B8920E1873F1D82A47AA76188DE5CDC90BCBA7B5777F0BA77F20A6143 + EE7A2581A305DD8FE7AA9116712C7F1FD35F892C6339B4841596CB175E4588F9 + 261927CFFA95BB15943A65E468CF5433C8D433AC57DE90B6AE9937C4AF88DD42 + 2787622F8BF2208FDAB088FAA884E6B082D65A0DCDB52A2AC312CABDBC218205 + 490797BA7A2EA036AA30BC0BC5A9F5E82F7258F9959346DA375F491909E1582E + 8240721581C42222696D70070C440447523EC49CA021810D794C33EA0410CF06 + 9029460CF99BAF268CA4AFABF2396EC2A50B11FA878D9453AE2A69A7088A8DB8 + B9C7B02969AE7E0E3DD6A9C5F2D724215C4FB35C09238D1C8E05118EAF22930F + A2C8B4CB8D24EA6DC529A0372AA0BF9EC568AB6036C3758FE38DB32E0E4EDAD8 + BBD1C0C1691BC7B7FB38BE35C0FE51131B3B55862F626DA7801B77DA78F4C60E + 5E7DEB10771FAF63EFB86E48E0CE2089AEEEE2DCCC6377BF869DFD3AD6B78AD8 + 965AE5B30E0E8E1B58DBCA617D3B87C3E32AEE3EE8E3C91B8778F2FA0D9C9CF5 + D11F66588728DABD24B6F62A58DB2CF299F5AD06506D84E99EC06094C1FA5A0E + A3410623A96F5D2BE068B78A5B275D9C1E36B1B996C7A09BC2B097C1DAC0C1C6 + 288FFD9D06D1C4FA90F198F6FE7609C77B756CD25F927E1B8318061D3FFADD15 + 868B997B754F6E2471E3208E3B67793CBA57C3833B153CBC5BC193975B78F309 + CBFDA8833BA7651CEEA47040DCD8CBB11C59EC6F39D85A4B629D698A546EEAAE + D56AD0DC8DBBB51EC3F64614877B59DCBFDD21DAD8DFCC183278D48960775D12 + 7F39D62383BB67397CEDAD2EDE7D43128119BCFD6A015F7BB38C0FDEA9E0F547 + 299C6EAFE064DD8FBBFB29DCD9E3F34E12B70EB2B8B99FC7C15A1A839A1FD5CC + 024AA945340B01ACB592D8EE3BD8D3BDA76C87CD561403966D9FEFECB5B3063E + 78B9898F9F54F1DEC302DE7B50C0375EABF0398FAFBF5EC0C7AF64F1C1A30C3E + 7A94238A78E74E166FDC74F0C66DB6CDA1831BC300361A73E8942EA157B982ED + AECFDC61BCD109309F00B6DB216C367CD8A579673B8B978F59879B35BC7DB781 + F71EB7F0EEA31A5E3D4DE355B6F9BBF74AF8D69B1DE6DFC67BF76B789F78EF7E + 136FDE62FB1FA471BA16603A8BB83108E2742381C341027B6CBFFD4E0847FD28 + EEEC38B8BF9FC529FD0F5A0B386ACFE146F3121E6FADE06BB7D2F8F01EDBF2C4 + 8F87EBD7F0647F156F9E44F1703780970FA278E736DBFC7E01DF7AA386DFFE7A + 1FBFFCCE087FF1DB9BF89B5F6CE3F7BF95C3676FACE0675F77F0CF7EDCC11F7C + BB821F7D35861F7C35831F7CAD8C1F7D50C66F7FA385DFFEA8899F7DA3833FFE + 6C843FF85617BFF8A48B3FFEDE08FFEC37B6F1E73FDAC44F3FA8E0833B2B7863 + FF0ADEB9711DDF7A1CC377DFC8E2BBAF67E8D7C0CF3F6AE3BBAF65F0C1AD107E + FC5A0DFFEA37CEF037BF770B7FF9D32DFCF52F06F80F7F31C47FFA379BF8CF7F + B34D731B7FFB679B74DFC63FFFAD35FCE2B3367EE3C322FEE07B6DFCE5CFD7F1 + 4F7ED4C34F3F6EB865FAA4839F7C50C7379E24F1B587717CF266059FBED3C1D7 + DF68E1AD7B39BCC6BABFF5A082FB27292355BE3D5842ABF022DAC52B18D696B1 + 515FC1517709B746737C4F4BF88D6F14F11FFEEA18FFCFFF781BFFD7FF7E1B7F + F5676DFC8F7FBD83FFED7F798CFFFBDF9EE1DFFEAB4DFCEB3F6DE3DFFEEB1EFE + F7FFF501FE8FFFFD2DFC9B3FEFE3879FF8F1E3AF87F1D3EF64F1934FD3F8FE47 + 717CF7FD247EFBDB35FCF16F6DE28F7FB2839F7DA78F6FBC19C7D71E07F0AD77 + 12F8E46DBE9B478B78E3FE3CDE7EECC31B0F437870EAC7E9AE1F677B51DC66BF + B87F56C4BD5B251CEDA5B1B3C6BED7679F1C04F87D25E8C67EBF2D69DE10BA2D + 1D0E9943ABBE609E0F76197E33867E67997E8B1C2F8286103EDC952A68BAB757 + D06D2F73AC09608BDFEEEE6EC260733D8AB551D4DCEFDDED04D1EB8630D437DF + F61B0D02FD36FBFE9A48E022EE73ECB87FCBE1379DE25892C5EBAF54F0EAC322 + BFEB3CBFEF16DE7BAB83B75EAEE2C9C312DDAB7870BB825B37F238E55870B09D + 613A698E2952F99CE318A2BBCA331CABC2CC2FC271AA8C07F7BBB873AB8E7BB7 + 5B78F9F190CF1DDCBBDBC293574778FC6880DB1C67CE34FE1E541927634860DD + 9FDEE57FB2D17438B6F29F53949A7FDDCDBECAFF471025FE6BAA8D848B7A8C66 + CC5C1F50AE07F90F09A33BE2B8B99BE17FA288933B559CDEE1987A2B4754F90F + E1FF6023C77031F7BA01C62B942348E575406A85FFC35573A84948E4FCC89442 + A87692E8E95F74A38EED9326F6CE5A341BD8DCAB628FFF9FE35B7D1CF29FB4B1 + 53429BFF807A3F89D6308D1EFF35FD4DFECBB6F358DB2F1AD2776DBF64ECDBC7 + 25A65335E6E621DB6F9FEF74AF600E1E6DEC163062DC36FF39E5B60E5625E1E8 + 7A08FE6F1DFECB2BED126ADD1AFD4A283538A7A864CD9DC5997C0AC96C1C2169 + 4909AE60C9BF8A45FF8A99A3849241F7FEFE620A55CEBF7A5B1D0C76FBE86CB6 + 51ED57506C17A06B277468CDCFB0AB31CDA75C29DF405AF7FD86CCBC6B3EB482 + EB8165331793DB6A4287E1C2660E2612582AA21325694971385F4A1BD257D2BF + 5615B4257FE78301CC05A4A2D767EC16D36E0B21E6CBB9F3CA98E415BC24B0D4 + 2EFB39DFF6A753F0A524912BA2D9BD3B5804AC485949E92A9E486411BE2E812B + C255E5718965577AD895CE555E262DCEDD6517B1BA14911A6B11C51ED2977928 + 1F9778963D6524A025FD1CE63A40F94B3258C46F88EB8A40DA61795D02D8854B + FA5AF25784B28597F895DD3C8745F686B82E0C1A5CF2F9F0D2EA2AD7AE7EBA4B + 7D771221AE4124912C02DBDE3F2CE254E4A897FC3510512A529CA651051DD05D + BA7E5852D88B2BC674C95E919ED703225B2F42F7EF5AC2D8E621778517F97A8D + FEF2BB2E896CD58D7516812B69DEEB7C1732F5BCA836667BFA08B585DACA4B6C + 4BE27981F55B66BBFBD8C6ABACEF0AED6A43D356F2F7B49F88EECB2B3E96C5AD + ABEA3029F398B4D61DC25E77497CDBBB7D27D2B6228AC7F5F1B681E21982DBE7 + 92AB822160194F04AB91B695BB48D7687A72A5D2B9E62D3ECBD4DDB50A332667 + AF318E894FD30B91B71302771CC6E28A9E89CBB45F66794502CB6E49618551FA + 26AF6710C0A6BE1EA80D24D57C35E092BDDAB31024E52B427B2E7CBECF61EB66 + F63BC604AF20BBDD37D1F37C94EB7C62CEEE5718B844B0257EE7E2DA6770495D + 2FB96B09DE59307E064543EA4AEA773E57C5B574095712392C176A08D43B58AE + B43097AFE15AB682ABF4BBC6F073A9D2055802F8DA58F257901AE869483DB4EE + 0F3677081B0960EDBFB804B0755718AB467A76BCF3B816974352299D9E10BEEE + DDC1AE5DFB3D922816BC24F07958C125810D69EB213BBDC4AEF034F2F769C4B0 + F537EEDE74C7FB81BF2EECFEE2F3C0EE4B7A312BCD6978CB2D78D392BFF7D9E2 + 85E5085E5895D0491857E73916CE716C9B0BE1D21CDDE6E9BFC036984F607ECE + C1C25C010BF335CCAFB4F88D0D7039B68985FC0D2C664E71C9BFCBB4FBB8345F + C78B0B45DAFF7FD4FD578F2449BAE78D757597CE4A9DA1B5D65ACBCC8C48ADB5 + 5695A5B5EC525D5DADB598E9D1FACC9973E6ECBB6777B90B10E4020401F2869F + 805F81EF052F4910BC22F0E7F398876579464576F7CCF4CCBB7BF1876973738F + 707733FBF963E6C26BED0E722D540FB5AF31EFDC3C5FFD6D0098F70116F3E38D + 7972E91752CFA3ABFCCD73FA322CE7EA5B015E759C3AFEDBA5308903D8DB4AFF + 5B01E0EF69DC7764F926B56236AD74007ABF437F311F6B929AB11DAAA71530FD + 6BA43AD62B6A955FADEFCAA7AEEBBBF2B6522BF0FAF7921A007F1FFD4301305F + 0C72C51FE0356AEB1BED3408CA63727A19067AD132003E71AA931E02F4E754FF + 497E08C91FA3455ACB3FE67749DE182D6EAA66B5BA41BF4D7FC98D2EC40F0FD5 + 0344FDB0F8361DAAE35BF47DCFA9D53104006E3C54A55A3F9C0F4BFD32E03D83 + A59AD39BE1AC7C697C6B7CB3D4F95AA9398FFAC5D74A0D8B5FF9C2FC3649C0FB + 5DE2BCCDC739AAAE37DAF86BB0D670558A61EE51F0B719F436EB28E0DB0A06CB + B85669AD24F3A93B3C071D1FEA0835778EA4BF39ACEE504900AC9684B36A3190 + 65B7555E99C6AE00B6E436D723D3647A2B294B10BD94C84F2E035635005643DF + 832F5539CC7E1AC4C8818C84BF420C5F55909425A12A2FA9CC03220965E5579F + F28B4FF62B031E65D023002EE7A37AB96D0C7925DC955F8B72E7FEE5D240D421 + 27C93C07F097F2F157B342A22D3410A301D5591E5091CB61591FE7575B074B08 + DC0A08ABE3643CBBB2AC2CAF0E33306EA7C1D9391EA0919FC54B4AB3CE5A4856 + 86B91EA136F24BB5DBBDE870F884D82FF370B8D3E91752A77738BCE87291DC0C + 753D425D6E1FBA3D5EA12E76BD3EE176BA299D242D83457E6945EC701F00E22E + 720FA0AE8D2D8A6D94E6A03486C56C3D6C13FE2E37436506C787C5E967ADF45F + 3233F85524A1B004C31CE6FA254C96AEDAA298FDBCCCB42E101092FB0E739CDA + FA98DD83E5A0A94D520C83753E5EFED92FF607360603E025A239CCAE04C61CCF + 7B07EBC9D5B1953043E0B01FE658089664189604A545293EE4A6B8001CE928EC + 241B2F171D0FC099F0C39309C19B0DC3417E4B4481BECE4400D68857C8E87708 + 2B5F83C70E2BD5EF8804600B7961A67823EFE1EB666B12DE73CE06B6EC65B8CB + 938CD2CF93919D465E9650232623591C1600D8DC8B1E9B96EA370AF539F4E8B5 + E98465701FC35E86BE4E83D8F3AEC7CA4B1DF689A50F39AC71E8A075EA05F8E5 + 6511D9AFE4D350FDBC146217BA8CDDE831F5404BED31084B626A97B90FBCFCA2 + C96B862BE2863F1910E289554FC2035FCA87402680603648AE9FC21E8AA7B4A4 + 13DE9413FE8C5BC0DF708EF2A53D08667C08E5BC08E57D08921B2EF8859FF305 + B2944E712CF687A80C970BA5E8B831BBD86FD013B42A4B3D47D87298F72FE63D + 8BA9BD8E5E985D5AD8BD26656F425E4EDA6F164B56BA0216E1B265AFC6DC4EE7 + D7010B5B5BF132C07E03956140AC8393812D2FF71C53967C0E25ED08A7ECC2FA + C91735C21BD3535BAC88A4292DC6CB4D1BE0A2F281A80D6106D8D43E335D5783 + A50F36FA2D9C1E231C1E3DDC3E230211051E47A9CE78C62E2C8153793B32253B + 721527CA350F2A43CAE4F9E85C1CD38B69CC2C1730B590C3C84C0413F351CCAF + E530BB92C3E8740CFD753F8A5506BB7694FA5D181A0D637C328991B138FA07FD + 28945CE8AFF9303619C7D85494D203E40F6161258B8D9D61ACAC0F61782C85F2 + 4040C0E814EF6D99B7209935229AD2093757B662A0EEC1F04800033CD19F3523 + 9D32A098B561B0E2C6D45814D3E3718C0E47315C0FA152A2F3E03D3D2B2E0C52 + FEDAA00765B60C64085B3462A8C6CBB4DA51EB37517E33858D94B7172343462C + 2FF9B1B4E8C1DCAC138B735EAC2C86B0B11CC3C66A145BEB316CAD25B038CB90 + C686C91117F92358A6EB3445E735C016C069034AD43E5E8296979B2DA69D1819 + F063762A80D5A518D657D2D8A0EBB7BE9CC568CD8D74AC07D9780FC6EA4ECC4F + FB305237D079E8B0BF17C1950B51EC6C387161DF8F8BE73DD8DDB263795187F9 + 891EAC4D18B03BE7C6859520AEEEA47075378B5DAA7FBA6E4329D9857CAC8BDC + 3E54521AB1C4EF64CD89D9610FC6AB1694E2ED2845CF60B666C2B58D049E5D2D + E2C58D3CEEEE847065C9899B6B2EDC58B3E1C6A60DB737EC78B0E7C3DB579278 + 743E8A9B1B41CA97C0EDAD3876A6DD581CB260A2ACC550AE1B83E92E0C153498 + 183461A45F8F5AAE17B9F019243C6FA01CEDC464D98259DE53B962C1D29003BB + D35EEC4C3A30DFAFC1E69815375642B8BB11C7EDD5186EADC471678DB419C7CD + CD10F6179C581ED262BADC8D859A018B2356CCD7AD581CB6637DDC8BDDD920AE + AC257161298CC5BA1E73956EAC0F6BB135AAC5AD652FEE6D847071C68CCD3A95 + 2FB56375B017AB351DA68B9DA46EAC0E5B7071C18F87FB597CF2B08E9FBD3B81 + 3F7E3A8F3F7F318B0F6F06716DA11B0FE9FA3FDB0FE0F68A037B631AEC8CF5E0 + FAB2054F2F86F0C1AD243EBEC3C037891F3D4EE3EB3713F8D1A3347EFAAC20F4 + E337B3F8F85A046FEF79F0E2BC178FD62CB8B5A0C3ED653DEEADE8F178C78AC7 + 7B76DC58D260B9720AB3E9B362EFE0DB8B0E3CDEF550FD3EFCE85908BFFF348F + FFF0931A7EFB59019F3F8CE183EB09BC773D89C7E7E9FCE78DF47BE9E9B7B4E1 + F9150FEE6F79F1F442189FDCA763DF4F527C04CFAF46F1FC4606CF2E17E8778E + 6285AEE174498705BA0F16E8FF3F4BD76484AE47217C1A09D74924BDED88B9DA + 91F19C45D17716D5C819BADE7ABCF720879F7C5AC3CF3E1DC0276FA54959FCEA + CB51FCFAAB31FCEAAB61FCFAEB1AFEFCDB71FC1FFEF306FEEBBFADE2175F0DE2 + 471F94F1CD4783F8E24515EFDE4BE2C6960D7BF3FC3F36E1FC820D9796DDB8B2 + E2C6E6B8164BF52E6C4E68B03BA3C3E5152BDEBC4CFFBD9D2056C68D182D75A3 + 3FD58972AC1BC5481F72511D12E11E84FD9D4804BB49BD4884B4745FF112EF56 + A4E999998CE811A7B890A71B7E570742DE1E0AEB100B6B100E75211AEE453A69 + 443167462547CF892C43E42E5237B274AC54BC0D99543B0AD91E940B3AF497CC + A8D2FD9E4BEBA99C8E5C33DDF7167A269950A6E7D7203D4BEBFD4E0C0D5A3146 + FFADB1613326E93F3E356EC560B90779FA7DEBF4BF9F1CB5607C889E43833A7A + 1E1950C8F42113EFA63A7BE9981A44A85D3E6F27DCCE76D8AD6760319F20F738 + 82DE3671EC2AEF575EB161889E8FE36341E1D6F84392117AE6D233696A2A8EF9 + F99C80C0FCF14C3868249985BCFCE1915B2B5678F0FA75F0878CF053BA87E23D + F44EF2D17B8897730E452DE472582B96EA0F46F508D2B3CD17ECA47C67E93DD8 + 067FB49D9ED506648A2E24B2F47E495B10495AE08F98E0F46A6175F428AB59D0 + 3B8E3F7AB2B878650B0DBD230DF42EB32159F6A0C0CFEED12806E95D3130C1AB + A7917F2486FA68426C5350ACFAC5474BB9B21779FE488894AD78C9F5A14465AB + F4FC2F0FB11B427D9CDE351351D4C642144765E81A65F8A3A722BD3FE8589982 + 0BE9A20FB96A148942107E7ACFFAE8DD1E4EFB912C4491CC47E91C78B50EEA2F + C4A90F10772112A37E40CC43D7CB02BD45831EEA1375EBBA44DFC4487D13B34B + 0FAB97DEE54106CBD4E7A0725EEA9BB8C24E7ABF3B28CD2A9688D6727FC6427D + 24AB81FC06B17434F79DBAA99E73BA4E9CEA3987D3BDEDD417EA39E883B1F883 + 3C86BE07DB7038181EBF84BFED7A2D89FB51ECF6E1ACA60767B5BD3827E27989 + 5F8EA3305B2EF3D2D0A28C8EFA7D66EA83D9A98F6655A0AC8901E14B48DB47FD + 6B4556E18A259985943097ED237F1743513DC36CFE68D0426238DD589E9AFD66 + 72AD26EA035AA99F69A5B6F392D66CDDAB58327358B824097AC5FEC51466D02D + 258037D5C5C7E17D821940F758B90D0EAAC32EFCDD569B00B612D47692CB60B9 + C3A084DB743ABA36BCD4332F03CD16D8742D389E7486C70C3A0506B3CE705E8A + 6368AA5E665A82E603002C2C7C5FC25F0161A92CC72B90B71BC71928931802CB + FD81E5BEBC6AF07946EC89CCD6D2BC24B1E257C361DE83F89CCE245C5E925A01 + AA0C5915A0CCC794ED9356C00C7639CC6DE77360E0AB0079BA860CF2E5B9D1F5 + ECE2DF99C63C7D3436EBA1314F8F8DAE2903E30628EE10B058B122E6F394E722 + AC831BE7FB06EF13DCC7405D23CE91F724E6A5A9A54E69E83AAB7482F72DEE51 + CE91CF97CF93FD2749BCB4F57176BB398F0254DFA0EB2CC1EA09BE3E5A138DF7 + CC34DEB31E8C8F791CCC636239366EE3384A537F282E75005D299ED3056CE5FC + 6A191A730D94476E17D5FCB1398BE364FD1C2F2541B1D401706EC0E9335486CB + C9361CB483DA2CC02FB54111834E652E4340DC465899E77819C77311678D2E1A + FF529A4A6A8BDE66E02BC347492EFFAC40602F4E9ADD3869F50800FC06B5F175 + 6A7BAF3F8AD4E43C4A0B1B30258B4A1E6E13E56FB3F80EE0AF908964A47A5510 + F8000637E20E59FE36E0AD04C0277494AEA5FC3ACACB1098DCD37AAA93C47E4E + 933AA1A17AB83CBBA4E37D0EFA0D79BEE7E57C919CEB11CB49D3F56F06C08AF5 + AF5A0A00967BEC4AB02B01EE01C86D82BB12FE7E1F002CE7017F28B105EE517A + 9D24E71BC59C23E9BBF21D0589A5CB92739C62C9E786C5EF2BE53A29AE4B47F9 + E9FE68D3E1E459BAE7CFD23D7E568FD7DB4C94DF8AD7DA6C38D9EEC5D9AE284E + 7726F07A770AAF192B389B5A8277E519FC6BEFA3337B11AF9947E838696A6B8C + EA0BE15847808EEDA63AEC78ED0CB5E7341D8B977C3EFDEAFCF381D4D6BF677B + C5768807F0579D8FE7C5D5F3E8ACC61CFC2B73FE32AE09F4B612E7FB7E3ACC25 + 8E35AD4EFA924F1CAD66E621D52AEF517AA5BC9ADF48BF0CB7D277A5935A319B + BF46DF0680FF125E26CB1F2A2399DCDF2AD5715AAA5599566A55F6BBD4AA9E66 + B502AF7F2F7D1FF8ABCEF38FB6006EFE039CA507462FBD20CE307C3AA1FC4178 + 0F5E7599BF59EA1FA345BAFACF79A87DDFA603004C37758B1B47AAD5CDF95D3A + F24171949A1E08EA87CD513A54FE3BF47DCFABD571FE16002CFD4701E037D45F + 1749717E2955BCA8E72871BAACAB11FE56A95F6AADF47700C02CF531BEADAE56 + 00B819BEFEA500F8FBC0DB569240F7FB893A328D3D786567477690A46467481D + 2F3B4FEAB004C022AD016A25D06DB6086E2535B495509725A12DC7B72A23F3A9 + CB4BA9EB90525BFE4AFF21A84B925FAC4AA901B05ABC0FAF84A92CEE98F3D7AC + 3CE0914056BA52322FC71F0C86A4F43408A2E3701BA4B5AF1C0088FAC9E5CEBD + 3ACC521F87E395BD721458CB834E095F7990C671624F1D1ED089B6BE84B89C57 + 4AA6B1D410985D09BAD5F9651DF2582C097BA5782FE1761A9475D8BC02DCAAE1 + AE84BF32CC69CDB0B7CB1540B73B28C47E0980455E07C35C723D7E7435D4E9A6 + 721C476E8F8FCA7903AFC4332816F9DD741C11E7255701C18A75B0036DA473E4 + E77886C7ED94A60E334896B0992D8E59EC6790DCE1542C89C552D216DE7FD844 + D7C644D7C37C007F799F6186BD9C875D097FA5E531EF2D2CF61A56C15FB9E7B0 + 5003040BC93D851D7674937A5D4E017919F832E465978130C7312096E90C8159 + 6C7D2C2C89FD6C79CC16C1ECBA618C78A10F79A00B2A00D8960CC3160FC21A0D + C01CF1C0C4CB25C77C70C40330055DD07AAC3030D80D38856BF0D9C53EBF6CE5 + 6BF2DB61F43A6062EB645E8ADACE136D46319168A5633A190C073D0204EBDD16 + 7279728E27153524DE7F57876EB3069D62C942C5EA97A1AFC16B81D96FA3FAB9 + 1C4FEA31F0D509F0AB752962D8CB7097F7BE337A7989660BB916E8793F3CB751 + 406016436006C0BC375E87A11B9DE66EF458FAA0B3E96174D0B11A00B8CFA211 + 1098F7FEB5D0713D31AF0AFA06844239564858EF06B20C7415882BA12E43E080 + 80BF01017D05F8CD291098E12F5B0BB3181A0B702C6130D79174C11BE5FDFD8C + 02D27A8266384216B1D4342F1DED8B53D9B0054EB15C332FE76C4780E2037127 + 4209DEDBD72300AD276C86D5A381C5DD079B00BE067279E9499E14D7C147C788 + 663C48E43D4815BDC80FF0247614C55A08898213C1A491642659A82E03D5D54B + E5FBE08B58104EB90434E67ACC760DAC0E139C1E333C7E2B7C416A4BC40E5FC8 + 0CB75F8760CC78007F5305133215334A232EF48F7B519B0A6062318ED9F53496 + 764A42B3EB29CC6FA4B0BC55C4EC6A1A23534154EB2E14FB6DC895CCC8E44DC8 + 162CA88F043035CB1098DACDD6B1233ECC2DA631BF9CC6D854085373612CAE66 + 29AE84D189AC58867A64228191F128B2250B92791DB946E42A26946B56D447DD + 989E8B606E3E8AE161174A9496496B914D6951C8E8D15FB608CBDAA1BA1763A3 + D4A68A1595B25571A98EFE7E33EA751B95B563B066C0D0B001B30C78179CD858 + 0F606F378AE525176667AC5859F16079D985A545279617DC5862CBBCC500F93D + E47793EBC3FC8C1333134E2CCD05B1B192C0EA620C73934161C1CB567BA3358F + 803FE53C5B05FB3036E4C7C26C00BBDB699CDFCD61772B87ED8D2CA6463DC8A7 + BB50CCF66076D2838DD51016E76D585D76E0C6F5141EDECFE3D9E3023EF96454 + E8F9F332AE5D0F607FC78E1BBB3EDCDE0DE3DEC504DEBA53C1F3BB83B8BD9FC1 + DAB403A3D53E4CD68C981FB361AA6EC478BF1673C3662C8C983133A8C358A913 + B3B53E9CA773BCB515C29B7B51BC7D95EAB91CC3831D1F1E6C7BF068CF8DA797 + 7C78FF56023F7ADA8F6FDEAAE3BD1B193CBF9CC6F3AB39DCD98E887D81178674 + E4DAB1B7E0C5F6AC0B5B748D36E95AADCC3AB0306AC6645583A9AA0E3303160C + E7FAD01F6BC740ECACD8DB97E1EF3E5DCBAD3113F6A9DD7BE3166CD4F5B830E9 + 147BD7EE8E387071CE8EBBBB1EDC3DEFC3957507B6A6F5D89C316073D68A9D79 + 072EAF047163238AFBFB593CBD51C5CDED18D646B4D818D6E0F2BC45C0ECF76F + E5F1C9FD32EEEF78B03BDE87B5C12E6C8D3074366195AECFC2800E73552D56E9 + FA5C5EF6D3F9E5F1D9C31A7EFC6C043F7B3E821757A3B8BD6AC7FDCD006E2C7B + A8BC1633B90ECC15DBB13BA1C59D0D179E5EF0E1BD9B217CFD34815FBE97C1AF + 3FC8E157EF65F18B17297CFD208CCFEF84F0E59D383EBF9524378D4F6FC4F0F6 + 2E95DBB4E2F98E0DCFCF932E5AF1E6793D3686CEA2EA790D65C76B180D1DC36A + E51C2ECFF4E0FE562F3EBCEDA1BA4BF8FC71140F779CB8BBE2C5CD253776C7A8 + FD03EDB834DB85C7178CF4FBD97165CE82CBB326DC5E33E3E1AE058FF7EDF41B + 33140EE1EE5A90AEAF8DCE438789AC062B750B2E2CFAB13661C13CFDA6FC7100 + EFD39C8FF421E6EE44DAD38D52408BBCBF1B0349BA7E330EDCDC0BE3CE8518AE + 6F077069DD2B7461D58D6BDB7E3CBC1AC6074FF2F8F2FD41BC47EEDB0F32F8E2 + BD617CF5FE24DEB957C11DFACF6D4CD17F72402300FC64BE1B0B551336E8375F + 1A34525C0F664A1437A8C155BA2F3E7D328947572B5819736282EEFF7A5A8F52 + B80F094727FCE676B8CCE760379E85D3C46A83CBD281805B8B78C08C5488F7FB + B62241CFEB98CF8890879E7D1E2D022E0DFCF4CC8D040DF42C71A2520AA29F9E + B7D59C01D56C274AE90EBA3F3B91499E452CFC3AC2FE6388864E50B8E3000297 + F24664937AE45226548B74BF975D18A905303F9DC6C24C52EC053C33E5C1DC8C + 0FAB4B61F121C90CDD2F153AE72A9DDF08DDA7A34316F4D3FF2F956C47C07F86 + 7416E17007522903920C517D3D703ADA61B7B5C1616F83C775061EC709F85CA7 + 100AB6239DD26070C029E0EF003D07EB35B780BF23C3018C8E8430391E476D30 + 48F9D802D880704847C7E883D3D90D9BBD130E670FBCF4BE08864C0834C030EF + F9EBF2E8E1F1E9E1A738258DAE5DD88448D44C7E3D95EB80D97A0A16075D736F + 3B7CF47B841316443236C4E9BD92287A1049D37B94DE850EBAE636F18113BDD3 + 79F50B7BB7D8D280F7B28FE75C88E61C62D9E768D68A78C9814CBF1BC5019FF8 + 28285B76235354C4F0B7341014E22D0C18E466299EE130EF675FECF7A33C18C0 + C070180374EEC5012A57B2224DEF25017DF36EC4D356D1CE34BDFBFB4733A8D4 + D3F4CE0922417D82643E8C74312A94C805114DFBC43B5B5939C34FE50388F3D6 + 1061DEA6813FB462CB61EA8F501FC7E2D253580BA3B34F4867EB858EDEC17DD6 + 5ED17FE165A2795B0CB538AE8FFB36D4C761711FA84DDB81B39A76E157B6DCA0 + FE11F5C3D865F1077A0C8B394E2E11CD96C2BC7F305B15F3D2D20C813B8D3A74 + 90DB61D0A2DDA051FCA42ECAD36BA33E1E97331BA99FC67D3827F5C31802334C + A53E22B91AEAD332A8956106C09C47E4E3788AE3F883B8465986C13A17C7311C + A67A9C2C13F9B9AFC72BBF503D0EDED398578AA13EA1C72E96B7D6D879296B83 + E89FCAFD8FD9DF6335A087E17103FE32B85664A0BE22436AAB0230B9FFCE309A + FAE6BD2486963D9C46797AA87FCF509AE13683DE737AFDAB3218D141E9E7E838 + 6D741C763B182293DA39DE48F19447BD94B2B0B0D5E921F6DB6D58C04A087A86 + E131E9749F027915F5D038B997C6C77D34AE65C8ACA5F1679F905C6A5AEAA456 + 4BE35E86A34A3E097F5967796965CD4B29FB282B4B31B3D8129BE1ACB48066CB + 5D762500967098FD1DBCA4B7B87E76E176323CA7EBC80098E16F278F9B2420A6 + EBC980B883DC4E86C88DBACE507B8F7576E2B533678535F0190DB5BF8FE1B95E + E43F4D2E9FE3696AC7191D5BEBBE04C152BC7731EF63CC1058C2DF13DD0C90C9 + CF80B8574BE91A48CBDB932C8D91AE137FEC6B12AB5DF14A58CA164734C63F18 + E7D238B3211E17F378B979EC7D428EBB797C4F6288CAE3E84E1B8D5F493C9616 + 636A8A93E5796CCEE37B594E8CD5FB4C072058825C01730FFC26F2F36A632FAD + 8D192A4B302DC51FB5378FF75FFA15B8CB6D54C35E11476D932057C25CE997D0 + B715FC156097E218EC4A31E055A7B3D4F59C3153D8E439D8DBF70CB9274C2E01 + 827DD511EC3E7A1B97DFFE08A181499C347BC532CF9C8781AFB0FA55A979EF5F + 35006E06C3D2A257C9EB1600584060F2AB75CAE039947E5CEB7C456F681C78BD + CF7E080033EC9596BF528796816E2CFDDCBC04B41ADCB682BAEA3D7E659EA3E0 + EF2151BA7A1E50FAFF16B502BADF57C755E519020B902BB7B0536D65F7ED3A9C + E7607E930170B7728E27DA0C8ADAE9FE3E67C68973BCFF30FD6E9DF4DB7607D1 + D697C0596D16AFE98B381E984560FF435CFCF3FF0D77FEDBFF1DD5B7FF133A6B + 77F086610867BA4B38AD29D0FD96A567488CAEA99FEA74E00DAE4F5803D3B3EB + 4C9F027A4F772B925B1BAAE12F8BE7A279BF60D57CF5811A10F8D07CBB6A1E5E + 2D31CFDF02F8364B3281EF96C2245E3BC5E2F6F331152671984F1CAD56DC83D5 + 2AEF517AA5BCE437CD6ACE27F55DE9A456CCE6FB8BDA79A046DC710502B76461 + DF4347F235359BFB6BA5AEAF955A9569A5A6722DDBDBAC56F5344B0D5F7F48C9 + FD7FA5FE52EB5FD63F1A00F305E1FD7FDF38D9A980DED7A81DA49374539EA087 + 0AC7BDDCFB972E3E83D6E63ABE452D7F30F58FA18A973FF05152E77D45FF1B02 + E0571E264D0F0475FAB749E6FF2E7DDFF36A758C0300AC7AD8B67E387FB72400 + 965F0AFDA50058D4D1147F90DEFC426A4A3F14DFEAA5265F860D4928ABF6FF2D + 7A8DD5741C75BA1A147F9705300356E99720F828F02BA52EF37DF412EA1E1603 + DE139DD46121B15FAD131D4A3AE75377E264C787253B421CAFF6ABF3B15F0D80 + DF68005A0980A564BC941ADC4A58DB0C6C59325EE695F98F77BF2CA34E3B3806 + FB79B9A3DE97F9DE60713E2E23FC14CFC750E900F036C1603508964B1C49A02B + 2561AC5A12FEAAC5F16280D4180831006E33380504E63D72B8D3FF060DC4E400 + 407D0C592FFB459A1830BE0A80E5E08C0787EA019B1C601E1EC02970588A0790 + 2CD10E1A604860CC712FE1311F83CB292E9F8F380F1A84B1CE5918EC7AC4208D + DD763170F5A2D3EE4327C35B27C9C196B7BE5720B004BECDEAF184841802331C + EE62484CF11D2EAAD3ED1390574A8260F63300EEF587847AFC41743310F629F1 + 329DCBB7BBBCD42E86C9CA12D28A95305B16BB0F00AF02765D07E057025F162F + 37DDE70F0857C2605E9E59C2DA97A096979A76092BDE1EAE878FC761918FF373 + 1C0DF6292FBB0C7CB57EBF00C0EC672920D80B2DBB8DBA053C76D820F617B659 + D1C3136694A66B58FD6A7D9C978EE35420710FB90C81D9ED72DAE978766149AC + 117B03F37EC30C841DD006A98E2003608F00C17A0AEBFD2C2F4C611643665E72 + DA0E9DD726A0AFC66D41A755872E9E24B41BA1735B0504566417F057EFA67C6C + 7DE1B4C2E873C11EF6C3110DC212F40A486CF0D9C8E5A50579A93BA398A0632B + 13B63691162A0C79756E13D5698139608739E8A0BAACD07BCCD0539C29608335 + C856C776AA5771EDBC2F312F8148AE99D2F55E33B40C7E5D060502331066BF8B + 5CDE3FD8A987D85398CE47CB4B469BFAA023D7C84B47539AB426B604ACF0A7FD + 0815C2F033EC4DB3D52E03DD00228510A2A520B90184F27E8A6F4060B6F06500 + 4CF9830C87D3BCECB34BC05F06BE0C7FDD0C81C9EF66089C762900992D88536E + F8E276B8A31678636C8D6B872766833B66872FE14290D283090702A408D59D2C + 06912A8511A56345E838B19C0F496A0FC35DCEE78B5AE18D98E10A1A60F7EB60 + F769E10E1911A6F6A4A86CA61C14F077703C85F1F992D0C05802E90AD765473C + EF44B2E4A13A39ECA1E3701B1844DBE00999E0F0F352D266381A16C0BC64B52F + A8580C3BFDBDF0477448B0C56DD142751891EDB7A23AEEC1F06C18630B314C2E + 2731B992C4D46A1A536B69CC6F66B179A982F3D7EAD8BA3C80858D2C86A782A8 + D49D28F5DB90CAEA104BF6A13AE8C4C4741423E341D446BCC2BFB092C1DC7202 + 937321CC2E4531B310479D97F9ACB205B117FD751FAA6C115B34A03468164B8B + 8ECD844801CC2E46B0B496C2D26A02B3547E9CDA58ABD991CB6A118FF0F2AD1D + 48257B502C9851AF7951ADDA50A95830C0FB7B32781C7160613184F5CD1896D7 + FC58587661752380F3E7E3B873A782274F86857BE56A0657AF65B1BD17C4E6A6 + 1F5BEB01AC2DB3E56E002B8B5E72593E2C2D78B1B614C2F6460A9BEB492CCC86 + 303DE1C7385BD80E7B313341ED1EF66168C083D17A10C30C6486CDA28EBDAD14 + 2EED97B1BF53A07C1E540A7D94CF44F586B0BF9BC0CDEB79BC787B0C9F7D3A83 + 8FDE1FC2A71F0FE3173F5FC21F7EBF895FFC62014F9F6670F3A2170F794FD9AB + 493CBD95C35B774A78EB7605F72FE5B0BBE8C1DAB40BDBD4E6F36BBC47AA0B1B + B3BC8FAA075B730E6CCD5A7171C58DFB1793787C238BDB74AE3737DC6229E377 + 6FC6F0FEED249E5F0BE3C58D203E7F94C1AF3F1AC69FBE9EC32F3F18C57BB792 + B8BBED137BC16E4EE8B034DC8B95310D76171CB8BAE1C78D9D106EED4771F342 + 1CD7F763B8B016C0161D7F67D187B50907460B5D184C9FC170E634968734B8B4 + E8C6FE9C0DBB934692098B95764CE74E6367D880CB531EAC56F458ABF5E2E282 + 0537367DB84CBFDBE6841EEB1306AC312CA6F3DC99F36387F2F21EB5BBF3F43B + 8D5A3155EAC2525523F6F3DD1E35E326FD860FB6A3B83267C77A7F17A6D32731 + 913C85997C0796FA3558A86A31926AC360E43426B21DD81D37E3D68A0F0F3643 + 78B617C79B5B61DCA0DFFECAAC0F9B750B95EDC470B803F5309D4BEC04E64BE7 + B035DA83EB2B06BCB8EAC3376FE5F0F3778AF8FA71123F7A94C01777E3F8E84A + 9894C0BB745D9E6D05F116E9E9BA1F8FD7DD78B46EC7CD390DEEACE8F1E68E15 + 97A70D58CC75623AD186F90C2F19DD8BFD894E5C9C398DBBEB3D78EF06FD5E7B + 665C9AD1E1C294191B437A4C65CF60BE708EDA68C05B97FCC22AF9C2B491DA76 + 1A93D9D730997B0D1B23677179A18FE235748DF5582E69B05836606580AECBB0 + 9BAE6F0CFBF43F3C4FBABE4DF7C16C1CA5981E41CB59A45CED1888EAD01FD360 + 30D54DBFBB0D1796FD9487CA4E9AB139CDE0B81BC3F913A8658EA39E7D1D33B5 + 36EC2E9B7071C386AB5B4EDC3AEFA3FF895BFC7ED3E54EFA9D3A3141BFC148BA + 0DE374BE73653D96068C581F31D3FFC380B9FE1ECAD7813D3AC6B3EB655CDF8C + 60BABF0FE3450DC928DA9274B421E6E840D4DB87A0BB1B3E86A4C653B0E88EC3 + 6D3B8B88AF0F89A01EF1A00E51BF46514023AC7F455C508B64DC846A999E53A3 + 29CC4DE5303B1AC6C4800D43D49EC19216F5AA1EB58A0EE57C0F0A992E520FCA + 393DAA0593F8002519ED432AA6A57BD9866A51D1F8701093A321D407A99E9A09 + 33932E7A9684E99991A0E740908E65C5C8900953134ECC4D7A3042FFAB3CD59D + 604BDA581752290D0A790B32190B22612D02FE1EF87DBD88923F1AEA81C77112 + 0ED3EB748E0A04CE537B8AD49E72899E77F40C1A1DF1A336E842A56C17CB46E7 + 32760180B319A770C374DE2E670FEC8E5E58AC5DB0DABAC8DF03A74B2BE4761B + E0A1F786D3A58387FC7E3FBDF3BC46211FBD57BC3E3DA5F592BA292F03E43E7A + BFF489E59E031193589D2291F721CE1F62C5E9FDEF37C32A00A90E7A6B2F4C76 + 8DD8F6201077201C73C04BEF3D3BBD931CF4DBB8E81C7D310322292BA20CAD93 + 6684138AE2195EB1C2836C89DE1B15DE66208034BDFB3279AF7053F40E4CE55C + C814D9CAD74DE5CD024CFB236CB96C8037C096CBE48F18114B3B91EB8FD0FB26 + 895C2942EF423FBD7B3D745C6E779014107BF647535E3AB653C06ADE6E2141FD + 8C78268860DC0F97DF0E9BCF24B67760F86BB2F7C168EF1100586BE942B7A11D + 1DDA36A11E83B254B4D6A2A1FE0C5D0706C0661D3446B6EED5820130BBBC124A + 1789C3DC1FE27E18836006BE1DFA1E018859BC5A0A5B087718FAC8CF2BB4F047 + 7A5AF2B3952F2F21CD40594F7E86CA3D944F2360718F85977C562C85D992B64F + C05E06BC26F43070A53C0C6E19E2EAF8C3468B598062B60A66C0ABA538BD4BE9 + 4F4AF8CB6290CCFD4B2DF52F0D1E86CA36F2733DDC3F6497577C91FD493A573E + 7F0A73BA86FA99DD163E7765E5994E13BBFC51205F136A8F4DB10A666B6106C1 + BDA43E9B0D7DD497D6509F9DA12F03E04E4AEFE2BE780304F392CD7DDC2F6797 + E2180477731D0C84391F8BCBD0397658C8A578EECB77501CAB93A171430C82CF + F147A50D4BE1362389C34623CE7058A7C7592D5B162B3ADD80BFA7B45A9CD669 + 8565B184C75C07FB7969E6533A9D9000CABC5211D727E20D38A925B70148D556 + B16A29505823AC8E4FF7F409F8CC4B344BD0CB16CA627F5FF233B095F1C2FAD7 + 44E7CD56BFFC012C9D6FBBD98273749EBC8A127F442BC47E8AE7B476BE0E9C97 + E3198A739BE93CBAE9FADAA331D8426101E4B9EEB30DE82BAC8F8D0C44F93A28 + 009875564F7918DC368030835EB6F4658B5F3EA793C2EAB70180299DAD7C8FF7 + 6A857B5A476D34DAE8FADBA96D0E9CE3ED8C1CBCF5108D71851A635E5287833F + 6C26D198588E7D0520269D219D3633ECE4AD8F3C540F5BA8D2B8983F9EA6B173 + B7334863371AF372591A634BF1D89BC7FE623E80C6F31C16730C061BFDA65672 + 1B30D7A8B80C7805E4E53693CEF2D64D740E6728AF4833291F729F6397C23C47 + C061658CDF80CEA6C3B0572DCEC7127056CE731C805B655E40C6B12B2D81E59C + 885AEA3459874C93F52B56C0EC36E0AFC58BB356BF62E94BC733260A18DABA80 + E90B37E1CCD6289F0F2719E636F2B3C4BEBF0DBF7A0968B524003EA48374AAAB + 01795B015FB59AA16F3300E6BD80E5FC0FC3DED7792E46C05FF32B0098F34AAB + 6109809B217033D89579D400582D994FAD83B46FB10056CF11B20E19877C8BD4 + 40B79518F2AA25E35F3FA77F45C79AE16FB3284F731C97E1B2228D2D8259ECEF + A47435003E47EA30E1748715A73AE87EEBA1FBAED74BCFBA30CE6AE274EF1471 + DC59C7B9D21EA6BFFC77FCF3FFFAFFC1FFF1FF0BBCF57FF97F227DE3D7E8F22C + A2BDAF9F9E71557A5615E99994C4A9BE204E75D17F82EA7C83F703E6B69CD528 + 73D13CFF7C4A35B72DC1AF0A001FE46B560300B79C5757FB4972CEBE15EC55C7 + A9E3BF5D0A7F1052338926102CD9452B35330FA956798FD2A17292A70896F4D2 + 7F20755E59F61F0C8095B61DC1C49A98DAB7E95079E680C7141DE2737FAD54C7 + 7945ADF2B752ABB2DF47ADEA52AB1578FDA1C5E0F78D06009610F800EE4AC0DB + 02F41E93F99BE20F8B4EA2D589FF9562C8CB0058FC218EF1B1299E2F5423FD7B + 01D8BF44AABAD592C7F94B25CA376ED01F0200BFF25068F1A050EB503A3F08E4 + C382D594F7BB24EBFA2EBD7EFCB05AE56925F583951FB40C845B59F7CAB084BB + 32ACCEA3E453C4F51CE76529389DD578691C84D571CDF1CD69DF4707E548AD5E + 68470060B5BE2DEDBBA4AEBBB91E35FC554B42DDA3C0AE3ABD599CCE20B6B94C + B314A8CBF918F02A9D1FD911524BDD41FA3E716A98CB92E15671EAB0F41FEAE4 + 353A8FB2B378D0696CEE44925FDD09FD4B75A2973AC84D71EAE335A7B512D7A1 + D6C93E9BD02996C6DE52A7B53410D1D1A043479D763DD5A37BA91334783A4903 + 2E290EB3384D1D7E9987EAA481CDC117AC34709250550D8B65580D5D5B0161B6 + 1A56C7715E967A40260768324DC6C9322C0993C5E083013AF95F1E5701C00220 + D30050581B3370E6C1200F5C493C909530575AE94A4B5E2106B76E06B76C79AB + E86070DC08777968A04BE27C2CF6777B43420771EC7AA9AC97CA7A7CE8F451B8 + A12E3F9567D81BA0320DF504C3E80D4584D8CFE9327F07D5A3B80C9215982B41 + AE04BC12EAB25F02E266F8AB16C7698321E84261E1B2348120B424914E75C9E3 + A8CB8B3C9497E37AD83297AD76037E11AFCEA788EBE0B6715B185433B076422C + 21ED700AF02C80B1CF03DE5BB8C3E510809825602FA791BA3D94979790F6F3F2 + CF411823DC5E6E17A5531C97D7047CE4E7FD8639AF83E25C3084BC30311CF63A + D0EBB4A0CF6985DEE784D66347A7D520C461839F2D831DD0F1DEC45E9790B6E1 + 370678AFE000ACBC4F311D4BE47159A1630B60AF0D7AB62226B165719FCB2CE0 + 729FD3841EBB01DD365EB2D9406123B45E33743E0BC94C652C30851CB0C53C70 + A602702549091FB9BC47B11F8EB807664AD77B4D54A71EBD6EAA83D4EB648B62 + 038C548F25E88035E484C56F87C963815E4C16F23E790AF0D5B9B430780CE4EA + 84AC212BFC193FFCF9107C793F3C59AF70836CDD5B0C215A8E0810AC58032B00 + D8CF164929B6F2B5C315B7C1C94B28271D62F9E76889AD827D8814FD543E8010 + 5B0793026C1D9C760B482C968866184CF9239427C2E5325E44526E84290F8733 + D5082AC369F48FE550AA27912C05114ABB84756E8CDA10A7FAD88D65DD88E528 + 3E6D432061419027BCB30EA42B7E148722288FF05E8851D42653189B2F607CB1 + 8891D91C062692E81F8F6390975AA6F0D84211F5E92C72F52862656A4B81DA92 + 732098B6C31F75C215300BEB628B4723F610F6478D62823D9432229637235E30 + 2051322037684379CC85DA4C00230B110CCF87C90D931B2205E9F8512C6CA4B0 + B6972715B0BA93C71C03E28528E69612189F0EA136E2C1D8641033B3110C92BF + 3060456DC28399A5286697C3985C0E60622980A129174A830EA40A1624727A64 + CA2614FA2D28D7AD189FF361693341F5A7B1713E8BDDCB45EC5D2961732F83E5 + D5281697C20204D7B87CBC07B1703B620C81D31A54FAEDA8D55DA80FDB313AEE + C0EC8217AB1B115CB89CC1B5DB055CB81AC3C6AE0F3BFB7EDCBC93C5C3C7553C + 7B7B18CFDF19C3BBEF4FE0DD0F26F0F8D9006E51DECB97D3387F3E86FDFD386E + 5E2FE016696F2782CDB58080B5572EE4289CC4FCAC0FE3A30E8C0CD930527760 + 6CD883A141B604F408083436E426BF16B393466CAD867071378BBDADB4D8F373 + 76C289E9311BE6671CD8DB0EE1F9B321FCEED77BF8D31FF7F1E31FCFE0BDF72B + 78F7DD023EF8A088B75FE470F3961757F64C7870D183E7B79278EF61016FD179 + 3CB99526E570E7520C37F7C3B8B51FC1F95586BF665CDEF2E2D1CD0C9EDC6025 + F1F6DD343E7954C1478FF37874D98FA7570278E75610EFDDF193EBC5DB373D94 + 1EC64FDECFE1F75FD5F0EB4F07F0C1FD30EEEF59B136D68699FEE3589FE8C2AD + 5D0F1E5D8DE2DA8E0357B6EC744C0F1EDF89E2C317FDF8F8BD3A9EDC4BE1D685 + 202E6F7BB1356FC4EA641FB666F5D89AD26297DC0B14B735DE8D9D895E5C9C37 + 606F528BED510D2E4E187171CC80CD816E2C97BBB13AA0C3F688051BC3062C94 + 7B3057D262BAA0C5545E8F89821EFDB10E94026750F49E42DAF10652B6D751F1 + 9CC548B81323912ECCA4B498CFEA319BE925B71D53B193188B9EC154A6032B55 + 1DD56FC010853396D790B3BF81A1481BA652E7309BEDC07ABF015B353B968A46 + 4CA7F4188B1850F3695175695076F4A2DFDD8BE1481FA6B33D58EEEFC1CE9806 + D797CDB8B36EC7D3F37EFCF8613F7EF6E610DEDA88E1FA9807D74703B832E2C3 + 15FA8F5C19F6E2122F855DD761BD4A6507B5B83261C3B5191F6ECE47F0E65A1A + 0F5612B8BB1CC29D659BB002DE1FEFC085A94EACD54E6326770A53E9B3180E9D + A1769CC66888CEA7A8C755BA7FEF6FC67075C186D5C1364CE7DFC078EA0436EA + 9DB8B6A0C71E5DF3ED9A061B151DF6469C383FE2C762D18C8512FD1E635E5C5B + CFE0EEFE0076E7B328D0B3226C3B8BACB70D83896E0CA50D182B38B14ECF9FF3 + 0B396C4C86B13AEEC2F2981563255E06FC0C6A99D318489D41354EFE7427C5F7 + 60AAAAC564B58BD2DE4031720C03C95358602BF5BA09E325BA7EFD7AAC8FDAB0 + 45F7C38579DE833A844B8B5EAC8C683037D08ED5311D56C6E81EAAF5D1B12C58 + 9D7063869E2DC3591D06330694D24614920614E837CAC47A11F3B723EAEB40D4 + DF89B0B7BDA10E04DDE710F4B48B25A373091DF2492D8A191D06CA76BA57C358 + 9E4E62653A8AE5891016C602981FF561652684EDD5043657E2989BF06274C086 + C1123DAB7274CC0C5BFF1A5114CB3F9B91496811A663C7A9FE74B40F497A2EE5 + E97F541FA4FF2A9DDFD4B81DD3130E721D98A0FB9E2D83971642F40CA167C194 + 8FE2BD181A7289A59D73628F611DA211AA33AC21578F448C97B3D6221AECA2E3 + 74211AA0738DF488BD88A3916E24127DA8F0FEBC034E7A3EBAA91E071DDF8674 + D2A200607A2730F00EF87AE1B07791FA6032F742A7EF82DED04D7E0D2C56B6EE + B5C04D7D0287DD04A7C30C17851D7633AC163DCC262DB9061117A2BE48384CEF + 45EA2FB8E9DD6EE7F7B283658083DEEF4E8F895C336CF42E67D9A93F61B51B61 + 21D95D54A7DB0A2B5BD15AFAA03775C268EDA2780D5C5E9DD80EC146EF2C2111 + A676F98C622B8500BDD712E920D2B92852D93062291FC2517A2F479C88C6DD48 + A67D4824E99D1FB6525DBC0D821636570FCCD60ED89D3DF005F4F086F508501F + 2096A1B2290FBC311B5C4113C92CB67AE08FB4F89DCDF2519A2F42D72D624688 + CAF03B3FC4CB3BFBE9FAF814CB66DECA8197B7B6BA790B0806C27DD09A3BD16D + 382740B0C6D2039D55039D85FA32663DF4565EDDC404238957356169790B0D1B + A553BF4709EBA1695807F3DEC1E7B4ED68D39CC399BE73C24A98DD339A368AEF + 4007FD862CDE47F84C1F03E21E0184D93A58B116A6BAE8BAB315312F29CD96C2 + 02F892D81A98AD87190A2BD0B561852B2C7A19BA9A85C52D5BE0328435BA1D30 + 51BFD4487D4CDE97983F32D4537F51C31F0F72DFD06582C6C5D6C57C0E0C7919 + FE3656857168C5477CCAD6202C2DBA7905188B069DA61EB41BBAD049E7DACBFD + 4C2AAB75501FD745FD5292D66111D2F35EC83E37F527DDA24FABE1FEB49301B0 + 19ED1623BAECD466B640A676B27AC9DFC37D708A67BF9ECA18A86FCD12E5294F + 0FF5D53BA93F2DD541C769A7FFBC548783FAF894A7C3CEC7B1D2316C62E51F16 + FB25386638CA126058AF83B02466884EE9DD344E6071790151E9BA32446697CB + F27EBB1D2406AFBC7AD11993B9E19AD0C6C054A7005396B09C650B616121ABA5 + 308367DE7FB9017719F6929F61B004C012FE32B46580CBD05769F3CBE330FC65 + 97B7CD61B15FC6B378352501BEE9FFC0E0FA8446037B3C8E0BF71FE0FAE327F0 + 677334DEEFA5B1BDB2FCF309DE7B9AF24ACBEA336C35CDE0B7210181A94D0C87 + 19F4B2D5332F077D929775EE3350FD061CD79A687C4CA2F33EC1CB3C1BAD34E6 + 75D2EF42632C3B8D0D9DCA98B5C3ABA8DD13C039B75FA8CDE9451BE5E171F139 + 077F74EC13AEDACFAE1C03CB31B11CF7721A973FA883C490F8F0F85B7E80CDD0 + 54093338E57139E791AB72719BD94AB9CD42E377138DEBCDCA385E8EE1A5387C + C6AAE8B4858EC3612AAF58DE3681598BB2B72EC3D5668925976D740D48676D3E + 2A43F99A2C705B9663EB5EAEFB40743C2AC716BF07F9B82C898FC13A25AC817D + 3865F5E3B495AE9D278E2E6F1227CD019C307219CA6374E3A4C1851306E7814E + 8925AA15B8DB0C80A55AA58B301DFF24D57DCAC416C61EA11306378EEB5D42D2 + FF86CE791027C3ACD7B50E1CD3D8F106EF054C12D6BD7D36BCD6631161017BC9 + 7DAD8FE3AD07F097F329795520B83147A69E6B93F370CDE1566ACE733077D702 + E8CAF941F5BC602BA9F31DE427A9E724D592F1CD739D9C2681AF4C57E79161B5 + 18F24AD0CB92F9647C2BBD768EDB4CE7DC66C6C933F44C3863C2A9B3745DDAE9 + 3A77D375EEA1DFA497FE379A204EEA6338632BE264600C5DC39730F393FF82DF + FC3FFE5FF8F7FFF7FF0F0FFFAFFF2B32F77E4FF7EF02CEF494D0A62BD0B33203 + B6003ED6E3A36B6A57AE05CFF3B6F132D35ABCDED62720AF301A6A9A7B96619E + 3F3E5851B231477D68EE5ACE951F0583E5FC3CB9C7546C80D58A0F7C5FBD76A2 + C1210E2C98C92FAD8125A338D129748CF2B6D22176C26E03C4B6CA2B96996E8A + 3B54BE29FCBD45C7146CA905EF3938561387FAEBD5A84BCDBEA4D45C4DC2CE23 + 589B4C3B047D9BD2FE26A98FD5AC56F95BA955DEE6BABE4DCD6565F96658FBF7 + 9284BF4702E0163A46694ADED6E98A1A27F20389FF488AA5AFE29EE4E503F8CF + 45174CC689BCF202FE9DC4C792C7939271DF25095C7F0800DCAC560F0BB50EA5 + AB0170E36174200EABE39AC2B29EEFA31F02001F6B00E0430F65AA4B0D8059F2 + 212F1FF46A00FC0685595CCF775A00B39AE3D5799BD3BE4F9EA300308BDBD388 + 6FF9E26B92CCF3F7927C098B17B10AECB2D4E1A3FCEA7073BCB4E8550030775E + 38FE65C746767A8E5273674A1DCFEEB775D29AD39ABFE863BFDC6B44C47307B1 + 450752BAB213A996CCDF9C2ECBB118D0B6923A5DFAD5F925D495F132DC9C7E08 + F252475C405EEA8CABE3653E11CF16C07A86BA8725F6DF6D88978596F1F22B56 + 299987FD3CA091F102CE927B14B07D0962A96E15EC95B0965D096FD96598CB65 + B80E759D5C86E365585DBFFA989C4F869563517D262AC703401A28B25FC25FB5 + 9F078D6AE87B0082ED0A00560F685BA9AB017B5B0D80D9CF83E04E8EA3F4731E + 1A587BE918A40E2FE5F151B90600564B0D82D965E0DBCECB42B3257023BE87DC + 5ECADBEB0F081DDA3B98C16F5398F7176671DE3E06BB2A31B0D587238AA86E29 + 1D49C26006CA0C75D5609781319751D215082DA130BB2CCEA79463F8AC8821B1 + 36A84061B626E6BD8C7B5C8A1531C35E3159E4B009F1D2D33D4EA7B020360402 + D42E3FB5CB0F7D280053342CC46186BF3A8AD337A0B03916117B031B23148E06 + 61265929DE18A436BA1DE8B69BD1E3302B90979779A6F2B66808A6102F3FCD56 + C3CA2417E76519021E58A86E2BD717F041CB10DA652759A0F358610A38618BF9 + 858C8DE5A5D9D2982D8E190AB3C5B19E2D8FFD76E80336722D0A08F65B616000 + 9CF0C2910A08106C8FBAE088F172D56E98C30E91AF8F977B76E9C96F82D66716 + 2EC35F335B0B879CC2AAD8E4E5093FC51A985D8D93272A7BD06BEF25F540EF31 + 505E1B3C292F02F910FC85107C053FDC592F3CBCC76F3E007FCE2FAC82BD290F + DC0917A9017C49F68819D6B089CE554FC7D3C311B588659E23A520C2A5008279 + AF00C0F16A04F1FEA880C05E86BF59B79256F0215E89203D1847BC1C4224A340 + DD74398CDC400C798ACF52B90443E4B40BDEB85528486DE0FD7C83293B957121 + 59F42253F59302C8920A8361948662A88EA5509BCEA0369546FF7842EC8B5819 + 8B527C4C81BE73394C2C9731B942227762A984D1F93C2A943757E7760591EAF7 + 23552215436269CB08B5239271225EF020517021C64B3F575D743C1FD5EF43FF + 841F8353410C4CF905001E9CF693DF4B7E8A9FA6F4493706265CA8933B3CEDC1 + D87C00736B092C6D66B0BC99C3EA561EB34B710C4FF8303AE9C7CC5C04F5315E + C25A8B6C4587B1191FA61783189DF56068C685DA8403E5BA9DAE811189AC16A5 + 9A0D538B51AC6C67B1B697C5CA4E029B17D2B878B382AB7706718D74E14A19AB + EB092C2E45B0BC92C4DC6C1C23750FAA2533B2190D12C91E148A060C8F383132 + EEC4EC820FDBFB195CBD51C6BD47753C7C3A881BF732B8722B8947CFFAF1DE47 + E378F27C106F3EADE2F93B23149E1200F8DD0F26F1F68B093C7C58C7D5AB5901 + 821F3D1AC2B3676314CEE3C27E02B76E5470EF16B5E97C1A0B733E4C8C393136 + E210007878D08EC18A15C3032E4C8E28007862D488E54527F6B662B8B497C3F9 + AD14B6D7A2585DF06394F75DADF56267C38F278FFAF1F107E3F8F4E3517CF451 + 1DEF7F50C13BEF66E9F811DCBAEDC5F59B2EDCBAE6C69BD74278EB269DC7D538 + AE6CBA717EC58AABDB5EDCD80BE1D2A607BB4B36CC0D7761B47C1A0B633DB8B8 + E1C4AD3D3F1E5C0A92027842EEB39B213CBEEAC7DB3703F8E86114EFDF0BE0F1 + 652B9E5EB7E3E347217CFD2285CF9E46F0EE5D1F9E5E75E2DE79332E2CF7E1C2 + 8A968E1BC467CF07F0F9DB83787A2789B7EEA5F0F1DB557CF9C120BEF97C045F + 7F46D7F3690ECFEFA7F1E6CD186E9CF750DBDC787095CE63D78DCB2B265C58D4 + E1E282167736ED787135862F1F55F1ABF726F1C5FD0A6E2ED8B15DEFC32683CA + BA5E58F3AE91BB50EEC342D588A9BC0E93590D26B23A0C27FB3096D26038DE8B + 92F72C52E63790341C43DE7A1C43C10ECC674D582A58309FD16131D78335AA63 + 6FC48E7DFA2F9F9FF4607BD4898974070602273093D7E0FC8413BB6316AC54B5 + 58EB37623EAFC558B41323E11E4C268D188F5B5071695075693119B7633663C3 + 7486DA55ECC17AAD1B6B03E7B0583C81CDC136DC5EB0E1E18A1F57E8789B7913 + D6B366CCC7A85CA013B3D16ECC273BA83DBDB839EFC7CD592F2E8E5971998E7F + 7F358A772F95F1F6F902DE5C8FE2EE92139727FBB03E709ADA7502F3C5373057 + 388BB96C3726139D188F766128D0867EF7496A63172ECE38F1F4520C6F5D8AE0 + CD5D2F9E9C0FE3BD6B19BC73258AFBEB4EEC8FEAB141E7B73E60C13AEFCB4CD7 + 7230781A23547691FEC32B637E8C971C284474487BBB900FB4A31AEBC450468F + 517A768C175D18CFBB315566F8EBC6CA045D87BA016B933692150BC3368C150C + 54A603A5E83954E31D1864289C7A1DF9D031F4C74F6175CC8E8BCB515CA0FBF9 + CA6A1C8FE87C9F5CAEE0DA4A04D756C3B8BA1200EF373C3FD8496DEAC2C2500F + 168775D8987660772E88CDE920C5D1BDCEFBF1E68C2433EA153B86AB0E0CD0EF + 9D4F6811A3DF3FE26F4332DC8D54A41731BAEE61EF3944E97C78FFED5CA20FB9 + 541FF2195E52BE0F597A860CE40D9819F26061DC8779D2D274189BCB496CADA4 + 304FCFB71A1D2F9FD42045BF5F2EA558FE1632660165BDCE9370DB8F23E0398B + 78A41325FA9F8ED16F3F4DFFB549097DE919B83017C0D4B417B3B33EACD1F99E + DFCDE3EAE501520DDBDB254C4D8685456F32AEA5761AE938748CAC1325527FDE + 897AD58BD1215E692084817EB7B0164E25B588D33D90CBEAC512F8F53AEF03CC + E93E64D2F4FE09F421E0EF15D6C45E77175C8E1E58E9BD6A36F7917A61B168C8 + D5C264D250BC1E769B49C841FD0B86C12EA71576BB090683163A5DAF80C03EAF + 137E1FBDE71D16D8E97D6DA777B5C1DC038DBE0B7DFA4E680DDDD0197AA137F6 + C1C8755319298BCD080B4350AB065A6337B4FA0EE88CED30D9BA6111CB28F7C2 + 40EF7B9DA50B7A6B37F41612B5574BF5EB4966AB160EB7056E9F0D2E0F1DDF6E + 107BDE7BFD66018283240FEFBFEFD6C146F7AB952D9E5DBD14D6C0EDD5C11B36 + 0A00CCDB3430F475F80C62D50C879FEAE1153948AE9011DEA8095E017F0D08C7 + 4CF053586CF5D000BEAE004369258ECB5BDCBC9D039D2BB55F4FEDD772DB6D3D + 14C740DB048BCB0CB383FA1F745DCD4E0BC5D9610F3BC56A29BC5506AF8AC2DB + 655878B514F26BEC3AE8D912D8C65B72F0D2CFBC2F708F80BD0AF8ED04AFCEC2 + E265A3D982982D6919F2F6D9E85A7BA9FE904BC8162051BFCE427D39A39BFA6F + F49B3250EDA37E242F0BCD30B8977E7316FB19028BE59CED36F499CDD0B005B0 + 8DFA7B7605C4F272CE66EA0F9AA97F68A4BEA18ECE4DCB1F0D92F823C13E8741 + 2CFFAC779B292F9F1BFB95F333519F91B702E9B56B45BF50E7322AA27C46BF0D + 166AA789DA6BA2BEAD84CDDC5EB62C36FB5D74BDBC02E472BF56F47D03D477E6 + 5574A8BD2C06BF0C8625006628DCC596C4A21FEC8585FAC0DC0F36515F9AFBC9 + 0222BBA99F4EC7E18F2EF9834B5E91A78BE2BAA84C8FD74DA23E3E1F93FCBC8D + 0B6FF122C5E13E37F5A785683CE0A0FE3F435F27B5A711AFF1F07630540F5B2C + 533A2FB5CCAB03715E1E3BF471BAC743E92E2ACBDBD5F0EA41D4CF7738D169B5 + A3D3F2521D661BDA4D56B419CCC29A982D6DCF91DB6534D36F6FA1FF8059805F + DEA397C1308360454A5E06D4ED56AB00CE0CA319EA8AAD72A84DFC412BFB0580 + 26576E9173CE4A79C9CFC09AA12E2F537DBCAF8FC61B612C5DB880CD6BD7E0CB + 64C016CCED9487978EE6E330FC6548CED6C3E72C6C494DF51A2C2433DA8C746C + 12FBCFE88C0202B3CE30EC6E2C977CBC4F8F3734743C2A739AC1299DFF59AB83 + C6BDBCED8F1B62FB221AABB6B97C42679D5E9C717870DAEEC6291B8D9DADAE97 + 1097C6B2727CCBE35DE9CAF12F87D56361CEDF2E56BD7A995F3DC66EA731362F + 33CDD0F71C2F174D61290E731A8FDF2500666B655EA69A21F0596E178DDD0558 + 660B648702AB25F855E0AF9BF251DBB91E06CB0D7158119D33035E6BD3DEBA0C + 721B00F89C3D20C479D4E9521CCF92B058406515FC65F02B25CB1CC4515E86BB + 02C09AA88D8E30CED882784DEBC0EB3A274E0AEB5F06B33CEFA22CF32CE1AFB0 + 026E00E083FA1BB057025FB92CB414EFFBCBEE692343E797E0578AA1AF5A6AE0 + DBAC9700D871007C19ECAAE1EFEB5A450C818F6914082CD3643E06C0AFF39C57 + 639E4B3DC7D62C395FC6AE0C378BE3794E4E5812ABE603A5D47386ADE60DD579 + 84652D8B012B83568A6B9E9B6C56F33CE7C1DCA59C036D3B3C17AA9EEF648399 + 5673AC726EF5A52B61B09C7BA53ADB78F5436A7F9B19274ED3FD7ECA205C11DF + 41E743D7E3B52EFA8DFA3C78A32F48D73E8ED7AD559C482CC073F13D2CFEFABF + 63E1D7FF27046EFC1CE72A5771423F8C93DD059CECCDD06F93C2B1AE085EEBF4 + D2F5A13A447B7538D1AEC77136F2E1952505EC6597DA47E2F314E72AC22FCF45 + 0D810FCD7D1F05809B75E2302BF8DBD545C764752B52B3094A53F30A193E886F + F0920396D2A4E6FC6AB5CAFF7DD5B22EC97B047F3A226F1387FAEBD5A8EBF84B + 66D78AA5BD023D8F4A6B5673DEEFCA7F945AD5F37DD55CFEAFADF7A8726A48FB + F7D4EB4DE14300F808BD92F71F008059F28FC4EE71BA2945982EDC31FA93BDDE + F03797F91F454A5BE50DF8ED379BBC39FF913A785830ECE57636B5E3205D15A7 + 8E6F4EE32F5D7E0800FCF2C1AA3C909BC1AF5A0C7719004BF02BBE04A26B7D82 + E21900F3B971BE96C056C6FF256AAEE328C99797FA6576285D95D6A40330ABF2 + FF50925F61099DE117F44BF82B017073F865A742E968C88E87EC7CA8C3AC97F9 + D579940E8DFC3A4D74505492E952CDE9EA3CEACE18BBDC4153C35DD9495377E0 + D479A418FC9EE8A64E6213007EA5D3D88897AE5AAD3A9BB20C8745E7B4D1C13D + CE9D5CF637243ABADC416EF88FF5505912FB39AFCCAFCE7742CB00F5A5E41E2E + ECE7FC324E9D47EA202FE90D0A8B3691CBE238293930907E96FA6B5119565C1E + 80D1A0A3F135ADFCEA9661AAB4B0558031C35C06B7522F813083DE66F0DB0CA0 + 65FDEC1E159671F28B5EF597BD0AA4A6E352BB7800A94E538BE3C4B2CF0C7BE5 + 7257524E1AAC92CED100B9CD452EA9DD1D3C248E6375784242EC6FE3FC2DD44E + 835FF1757543ED5E1A10B37C5496A4C06006BE6CF51B15EA0DC544B893E2595D + 8128BA838A55B01443E09E26EB60E9676B6316FBA518308B72A108FAC251A15E + 0A6B4351210DD52F2596A2F671FD41F4525B9BA5A5B6E9A89DECF605A83D322F + 95E3B026C82E875F5A0133FCD5851838B3E52EC36109965F02629E30EAF6B884 + E492D4EC72592EA70BD3B1C301E822544F34044DC82F648C47604A44856B4EC6 + 604DC661E1704C01C5D6444C80615E36BAC7E3442F5BF0067D0218733AA73158 + D6F8BD94C727C0322F33AD0B7A61A6E3D8A83CD7618AF279FBD0E7630B631BB5 + D74AF9EC3006DD308678596A979031E2A3FC542EC9C70EC212E7B09FDAE48531 + EAA23AEC740D48613B0C51274C315EC2DA065DC042AE95D2AC30441C14EFA2E3 + BB845F17B2926C22CE9EF4C21AF3C01C76D2B1A90E9E18F4596008D844982D85 + BBECBDE4EAA00F18A00F9A856C0C760B4104AA31F82B61F8CA21F247101A8821 + 500AC195F1501E071C49275C29179C49F227ECB0C5ED30472C54B7918E698633 + E580BFE083BFE487AFE813627FA81A42B83F8C60258870358C30B9014A0BB295 + 702584787F049122A5E5BC88978248D1B1230CA2E356D88206987D5A58FC3A38 + C24638A346720D708675F0C44DC23A3759F4203B1040BE1644612884EA680283 + D3390CB095EF5C06FDB31994A712284EC6519E4EA2381143653A85F1F50AA6B7 + 0730B951C5D84A19430B790CCDE7502777602E8BD2249519A7BC1349F44F6451 + 9F29A236934795C393490CCEA4E838490C2FA4A97C1AE3AB69CC6C1584C65653 + 185D49626299DCC518E5F7A130EC4065DC8381092FB5D185A1291F4617829858 + 8A606A258E999584D0D85C0803632EA1D1691F6A132E9446ACA88CD931B618C4 + D46A84EA0D617C2588C9951085A3A8515D6506A7F3012CEF65B079B988A5BD24 + E6372258DE8961F3420A5BFB296CEF67B1BD9BC5F26A1C8BCB71ACAC66B1309F + C1085DB772C98E42DE847CC18872C582FE9A1DF5513BE696C3387FA580ABB72B + B87EAF821B0F4BB8FE2087FBCF07F0D19773F8E29B65BCFBF1049EBE3384671F + 8CE2C9BB2378F0AC8E379F8FE0F97B5378F6F624EE3C1CC08D3B653C783C8447 + CF4671EB5E3FAEDD2AE2C6ED2AAE5E2F616B9BDAB216C6C2129D1F9DCBD828EF + C369419EA176CE809141B602F66162DC8CD9693336D742B8B097C585DD0CF6B7 + 5358A77243FD7DA816CF616BD583A78FAA78F6B884B71E17F0A32F26F14FBF5F + C7AF7FBD80AFBFE0E5A007F025B9DF7C3E85CFDE1EC1BD4B492C8DE951CF9DC2 + 20696CA013F3E346ACCEDAB13869C1C4602FC6AA5D181FECC2D45017E5D5E1C2 + AA13D7B77CB8B9E5C7CD6D2FAEAE5B70E782059FBE1DC38F3EC8E28327217CF8 + 348CAFDE4FE19B8F73F8EC450C9FBE15C38F3FCCE38BF7B378E751142FDE8CE3 + B3774BF8C917C3F8F167757CF0A2800F29FCCD5723F8DDAFA6F1FBDFCEE0673F + 19C6179F96F0F1FB79BCF32481E76F46F1EED324DE7D9CC2D3DB01DCDEB7E2CE + 451BDE7D10C3576F15F09317FDF8CDC7A3F8C3E7D3F8ECCD02AE2D9970794E87 + DBAB6EDC5CF1E2CA8213FBD356EC4E58B03566C5F2A001AB7533B6C65DD81C77 + 638DFEA3AB75DE3FD686899C1655EF2914ECC7500F9CC642568FED4137C989F5 + B2011B033A9C9FB0E3C28C0B17673DB8344769A33A2C0DB4E3E69A1B5FBC59C1 + 978FAB78FF6606CF2E45719FAED5B545272ED175DD9BB462B1DA8BC1E8695402 + 6D98C898B05673636BC4466D33E2FC742F7626DAB13972161BC32729ED04D6FB + 4F61B7DE87FD216A7BBF094BF95ECCA6CE51BB3AB045F1B7565CF8F0065DAB9B + 453CDD6540EBC5832D0FDEA1637F76378FAFDEACE2F37B253CDB8BE2FABC0D57 + E7797F5F2B2ECD98B13F69A063F761BDDE8DC5723B9DEF310C785FA7702F9E5C + F6E0933793F8F8410A5F3DA9E0E7EF8CE09BB706F1C1CD14D515C4CD458BD813 + F9FCA4095BA3068CA6DB30186BC3485E83C9AA1943053D4AC96E54325D18CC77 + A0966DC340AE0B35BABEF9700FC2F63388BBCFA246F917276CD85CF063673980 + 9569076687E877A83A305C3463306B4035A5116587A88D8374DEFD74FE73750B + 2EADA7706D2B8B1BDB793CBD5EC7936B03B840F784D84F7AC686C5612DE66A3D + 5818EEC5FC88865C1D16478CF45F7660859E496C053C90E94331D587FE9C11B5 + A205F59215F5B255F8FBE9B93050300B283C51F7616A388031BA2707284F99DA + 55CE1A51A53C25FA8FC4826D08BA4E201BE9453D6FC368C581F17E3A872A3DBF + 6A4E4C0E7B44BDA5B40E85145D03523143E756B4A19835211EEE4622D2857C9A + EF7DA3D89B7C6CC489B9292F16667C989D7262793180F37B695CD8CF61733D8E + F9390FE6E93FB8B591C4A5FD12F6770BD8DEC8637E2682A1210FCA059380CCC5 + 3C9D4BC58D1ABD2F46EA614C8ED173772A4DCF9C14868743E8EFF7A25276A350 + B02197B308B75C76A15E0F6178348252D58360480B97A7130E571BA91D1EB602 + E695216C0C53BB49BD305B7A6134B325701FF935C2B558E91DC670D761A4B00E + 3A7D0F7AFB3AA0D5F5C060D490FA603030E0ED86C1DC2E202EAB4F7716DD7D67 + D1A7E9804ED745F9BBA0D17689325CA7CDAEA73A0D241D490BA7534BED223935 + E42A56BF36AF11367AEF9B1C6C3DDB0D9DA5472C21ADB3F4A14F4FF5E929CEA4 + 81CEAC81C6D80BADA997F2B2B530BDD7A91F21B640F09961F71960F1F5C11E50 + F6DC77F8F570D2FBDF45FD0007C9CA71D417F0527FC1C31F89D13BDB1AD4924B + F978C58C941991AC0DB19C0D81A4098E9001267ABFB31BE06D2362544FD8043B + 852D146FF268A0B577A1CFD609A387FB017A58FD944EFD227BD041796C42B680 + 43C8EAB7C1EAB3C2CC1FC4517F875D19C7AED16DA23ACD30934C0C511DCA3619 + 0C868D2EFA5DC8D5F116192423035492B01E26993CD41FA16338422E2177D42B + 64F353DF8E2D6B1DD49F7232CCA5FE9897FA6F6CD5EBA23E1B8B2D6D3DD49FB3 + 53FFD064409F858EC3CB43DB1902533BC41EBE0C92CDD0D80CE8B5F0C77A24B6 + 58B669D1C796CCDC1EEA5B9AF83CE8FC79B5185E15263190812F477DD6461C8B + FB7BD6A81BF6B81796A887FAA1D45F64B8CCFDD000F54B1B3287A9CF18F58B2D + 5134741E7AEA0BDBA9DFEBA4FE32835D06BA3ADE3625E011B0D7C01F4C5298E1 + EEC16A3A612A1FE50F2D03D45FE63E3DF565A9DF6B8807A9FF4DED22D710A1BE + 3AF57FF531CA43FD6BD947D771BF9DFAD8F2634D5EC587FBDABDD4E7671D6C21 + 437E5ECD87D3F8E34E5EE187E3C447A224B15D0D83631A53B0782CA18C27688C + C31FBD9278C523B112928B2D5DDDE874B8D061E75587149DB3DA71D66CC569A3 + 62A9CBD6C3C20AD96416FB16331C664BDDD30C83F5BC97B14540E3334633CE5A + 6D686B80DD3686B5363BCED91D38E770A2DDE9423BB5516CC3D3D89A476EC9C3 + 7944FE8655700FC5DB534938D26968A9ED0C90950F61A90EAA8BF3773010A778 + 86C967CD0C97AD24B674A630435D238D9B0D161AA39B857BDAC82B61D1F85A67 + A2F13E5BFE521C7FECCC56BF2E2F89C6854EC57A9741ADB0F66D00E033147FDA + E1C1A90600663114E63C3C7E9516C2ECB2C5B01CB7B22BFD2C69517C289DC7C0 + 0C855934C6669DA3B137C3D84E07A593DA6D9487D461A7F294AE580833C855A0 + 3503E073D426AE43969570FA250466EB5A45A7A9FC19CA775680DE9710987586 + 2D7B1D019C6171B851460D8E59ECE77A4ED9E89A50DDBC5433EFDF2B606EC38A + 57826006BAA70FAC80C995B05795F7A514007CDAE4A5FAFCD49EB0B0003ECE30 + 96E24E5BFC02C4CA259FD995121098AD801BF33412FACAB91AF53C8E1256E689 + 78F9E693BC04B491EAD67B854E1AC865E8DB08B38EEB3C785DE3C2B13EA770DF + D0BA0F5C2986C0CAB2D0CABCD221B04B92E0F7403CB7C5F35A3AA50CE7157360 + AAF9B5E6B936F6F3FC9A9C8F53C7C9BC6A713CCFC9B502C062DEAF294ECE2F36 + EB00FEAAC4F392C7CF19FE6249187A582FE751E5BCAB7A2EB5799E55CEBDCAF8 + C3F9A9BE7374DEED74CDDAE83970DA80D3A774387E8AF29DD152DB35CA39B41B + 71BCD38E531DF45F69F7E1444F046F98B3688B4DC058BB0043F9324E7BE7F09A + A61F6F746571BC3B81135D61BCDE13C26B5D2EBCD66111F5BCD14EEDE9E8C3F1 + 7616F9A97E658B41BA460C7CCF1884947D829538A5BDAA3966018C95796A3117 + CE73E652AAB9F3836D186598A43604FB21F4EABCBF64122FA58E3BC441D4795A + C4A9E3D5E9CDE1A3F236A7B792C877AC5D51834135D7C16AC5A25AE9A08E23A5 + E461E62599DD81FEAE8CAEC95AF8BBD4B28EBF417F4DBD47955143D9BFA7FE1A + 00DC5247016079827FE9853942077F28557DAFFCC9FE0793BC11E4CDC350F200 + 8A36DD5C224F8B9BF31F26098059C749AD1E262A1D95F643036009759B01B0FA + 41AFB6FE65F8CBCB3DBF71A247B1FEA53A5EA773138098F34871584A152FEA69 + 963A6F2B7D5B5E15D43D1200CB171CF9C58B8DDDC64BF087D621F8DBD031921A + F8AAA1EFC18B99BFE8A20E905ACD50B695641EB5C4D2242CF2ABF5AD9DAD46FE + A3F2C88E9CECCCC9329C47DDB19379A45A0160752753825C86B8D28A56FDB521 + 8BFD1C27C561D1116535A0EDB74982DE6F03C0AF7553478EE2D56524CC95B057 + A649D02BA1AF0C37E7177537D40C80D5E2818274D50309160F300EE285453003 + 5605C42AC0F52598957112FC365BEE320866F8CB125098EA9300585A1BBF5257 + C3558BE3D42057425EA59C62E1CB005819442AE952EA419E1C48B22B07A6AC36 + 077F11FD12E2B224F455C35E75DC592EA3D2411A0F7EBD241A08AB81AF84BB0A + E08DA02714436F382EC47E8E6375331466204C2EE76510CCF09661AEB418565B + 0F4B082C256130C773192EAB89C40EEA6068CBD09761AE00BAD4BE1E1AE4CBBD + 8A250C967B11AB412F836306C15C46C261F64B2B606109DC2486C352C29238A8 + 406186C33C91C3B0B7D7CFA0F6E5BEC1BC6CF4CBC91E654289A5C0E0108CF128 + 0C0C71A36118480C76799289F332CC65C06BA13C0C7939CC2E4B026096282300 + 355B0E47608A456149C4E04827E1CAA6E1CE65E0C8A4614DC6608C3188F6A02F + E8A2F350A0AF2EE88681F71B8EF9614D8561CF44E1C8C6E1CCC5E022B90B71B8 + F2318A0FC19CF0C314F751BBBDD46E2FF411171DD741E5ED30451C747C271DDB + 43C70BC0990DC296F5C392F2D0B1BD5427C565827024FDB051793B5B10277C42 + 4E8A73A70222CE9E70C3930BC05F0A511BA87CD249EDA2B85218FEFE187CD508 + 82837144875308D5E30204BB0B7E3A572FBC850082542E500C0A4B616F8EEA4E + B9A84E87707D4505F606CA0145E40F0D46101E8C22528B2146F5A5A8DED45002 + 490EF7535A258448398478358C04F913A50062547720CDD635BCDC620774CE6E + 31A91C483BE04D5AE188E8493AB8132684F24E61A59B1F0AA3381245653C8EEA + 640AD5D90C4A336914679228CDA64914B790C5E07241848B33090CAF1731B933 + 80F1CD0AC6D64A185D29607CAD8CA94D8A5BAB6268A980D1D53226D6FA31B254 + C2C44A1593EBA48D32E6F76A58B93882A5FD3A6990D48FB99D1266B78BC29DDE + CC53DE2CA63672020CF74F07519D0A08203CB6C8CB4E7B313C1BC2C4524C2C09 + 3DB918C314F927795FDF9504E65633985FCB923F8EE1191F86E67C985A8D6161 + 3B8DA5DD1CB929CC6EC5B1B897C6F2851C6636E3D4CE3096CE67B075B588ED6B + 05F2C7B1B417C7C6C534D6CF27B0BA1DC3EA461CCB540FC3DFC5A52466E83AD4 + EB01B1B7653C6A409A97802D5B5119B4A33460C1D0981D8B6B11EC5DCEE1D2CD + 022EDFCAE1DAFD3CEE3C2DE3F17B4378F7F369BCF7F90CDEFA781CCF3F19C75B + 9F4CE0E13BC3B8F3AC867BCF87F0E0F9306E3DAC52D99C287BF34115771FD771 + FBC1002E5DE77D82E358DB0C637ED987C5D510B52B8CB9B900A6277D18AED951 + 291A8565322F033D31E2C1F0A006F5FE4E2CCC38B1BF9D11CB405F3A4F75EFE7 + B0B112C4C4B00E9BCB6E3C7BB38A17CFAA78F779153FFBD12CFEFCCF3BF8D31F + 37F0DB5FCEE377BF5E202DE2573F5EC257EF4CE3CE7E1AF3437AD4326D18CC9E + C5C4400F56661C62FFDFCDC500B69783D85B8D6073C18BA5490B96272CD85970 + E3C64E0C6F5ECCE2E17E0277F77C7876DB879F7C9EC76F7E32885FFEA88ADFFF + 6C08FFFABB49FCDB1F26F14FBF1CC11F7E31827FFECD147EF3D311FCF48B01FC + FCEB3A7EF3B349FCFA1753F8C98F86F1F92715FCE8CB3A7EFFDB39FCE7FFB48E + FFF65FB7F01FFE6D11BFFBED18BEF9BA1F5F7C52C4375F52DD3F19C1CFBE1AC2 + 571F96F1FED3283E7E9EC6AFBE1CC2EFBF1AC52F3EA8E2EBB772F8E06E148F2F + B8706BDD88079B0E3CDE0DE0CE861FD7975CB8C37BE6EEC771772B8C8B736E5C + 5EF4E3FE5E16B7B6D2D89BF2E23CFD4FF7E9BFB931ECC25C5E8FE9541F66923D + 984FF662BD68C276D586F592117B752BEED0EFF580FE7B77D64378B01DC2FD2D + 1F95EFC195051DDEBF19C18F9E15F0D5932C3EB917C7477713787E3988BBD49E + EBAB666C8E7560B270024309BAE6193D36869CB838E3C2F51527EE6E5B70674B + 879BEB7DB8B9D6834BB3E7B03B7A16172774D81F3563A39F7EE7013DD6AB7D58 + E4BD83477A716D89CAAC39F060C38587A4FB1B56DC5DD3E3E6622F9EECD9F1E5 + C30C7EFAB48E8F6F54F1642789C7DB493CBF98C1D3FD305D1F2F9E5E0C52FB42 + 7846EE850903C6626CC9FC3AAE2C9FC3E34B263CB9E4C4A3F35EBCB81AC547B7 + D378712982F7C8FFEED5009E5FF1E39D1B093CB910C3E6880963D92E4C960D58 + A76BB945F7F7FCB813536C793D69C4CC880695F45914E3E7900DB523E23C8980 + F518127EBA16A51E2C4E5929BF1993351D868A5AD4F3268C94EC182E31A435A0 + 9CEA447FF61C6AB92E0C907FAC64C0EAB81F2B631EA1EDD920B6A6BD58A37B77 + 75DC8A8D693B36A62C5899E0E5C34D581CD561A6DE87C9815E9206333513A6FB + 8DF4DFEF4129D945EA413ED28174F00C72D14E3A8E511C7BB46AA736B931371A + A436C6303F11C1E4900FA3836E0186191CE7E23D4886CF22113883B8A703495F + 1F4A0923EA050B6AA4A18A036394BF56B2A194D60B78DC5F5496812E65493933 + CA79BADFEB749CE928666762F48C8A60613E24967A9F9FF1606692CE879E7537 + AF56F0E0DE106E5CAF629DC2F3F4DF595AF063792188F9691FC98FE9712FC687 + DCA8168CC8C4BB05A01EACB8303410C420BD37866A118C8D25313E9EC010BD43 + 060703181C6010EC47B1E4443E6F472A65422EE7408DF256FA0308D373D2E5E9 + 82D3DD098FAF07A18801C188091EBF81E2E8BDE43550BC51F85D1ECAEB36C2E1 + D4C36633C06AD3C36CD1426FE8834EDF8B3E0D03DD6E0183B5DA6EE8298EAD88 + 4DE64E58AC3D544E078BA5171AED395227F4C65EE80C3D3098FAA82E031C2E33 + D56F86D7674528EC4432EE418AFA08D19813A190153EBFB287BD2B6C833FEE86 + 376AA7B005762FC57BAD70FAECB0B92DC292D648F519AC3AE8CCBCB4B2568439 + CDE1B509B9FCBCB4B319763F9D0B7F90C5AB7EF03907E9DCA356781276B8794F + FFA4437CCC15CE79E8FDEC125B29448BA4921BC9AA17B91ABD6F06FD4896A99D + 141FCC3A11CEBBE9FD4FFD0BF27B9316F8D376F1AE77452D3079A92D1E8D00CC + 8E8899FA05743D437C2C0F5C11379C6106B2D437E21550780F61971E165EDD84 + CED1C6F299E1A67E5480FA3FBE985BF8DD54DEC1D6C14E3E6FCA4FAE95AEA399 + 5C86C0163A6F67C029402F0366839B575251969216B098F2325016C7F5DB61E0 + A599294DC716C96E9B80C2F6801B561FF5AF4816AF0B66FEC0D06115D6C01A96 + 9DF7DF3588E5A4B5548EDD6E93165DFA3E74D1EFDC63D108F0CCABB7F06A2EB6 + A043581F3BE89C1DBC1548C40957DA8F543D4B7DA804F5F3B8BFE705AF1263A4 + F3B3531EDE468421B0F83030CA65283D427DD1580096A8E23A926121FE4091FB + AAF65484FA7B71EA1B3390A5FC94873F5CE4B0855C23F56575DCCFA5EBC3FD5C + 43D447FD56EA37934C09EA27936B494660CBC561CF25604DC744D8928E529F35 + 4EFDCD84F2616642F94093FBE602FE525F9BC5DBB574B99D07FD7B5D40E99FCB + 8F30455F9F21316F0FE35524C7013C5E901F956AA94E1E5BF4F894D588E4AA45 + 0202F38A480D08CB205802E1761BEF83EB409B8501AB1D1DE4B69B6D38C716C2 + 0DEB5A86BE0C7F655A9B85E12FF91D4E019A25E0E5D5963A3C742E7E3A26B5AD + 8FC634FCF12BAFBC24DBD0C5DBD8B8BDE8A6FCE2E356178D4F6D569CB559A80E + A73837B12A929B3F8055B6F0E9A0B672BBF9782C01B01D3C2EA274278DE9689C + C9615ED6598AAD65DB2C34FE654B59B69065E84AE3391E73F247C507509645FE + 0E5F98C6A7219CF304D14663D5B32EFF8138AE399DC5618EEFF4F347CA478BF3 + C8E5A5A595318F89950FA9C3E40FD179D1D8D24D635B17E57586D066A7F1AB33 + 804EFE089BF71FB6D36FE6F1D33561804CE7C8C097EA3823C6D9DE837138BB67 + EC5E9538FC52A76DBE03719801B080C04D6952A7AC5E05F8927B28DC00C0ADF4 + 2AE83D5ABCC7EF19D2694B402CF97CCAC2FB01FB0400E6E59F25009656BF6A00 + AC86C0ADAC7E392CE3C41C0FCF0569A9AC8EEA15905701BDAC560058425F2935 + FC65C9FD80E59C929C5F126097E7B83456BCA1B31F5801BFD663163AC8D798C7 + 3AA9A1F2BDE46F31CFC67E39CF26E6DA1A798E12E7510360F5BC60F39CA08C57 + CF331E2531F7486A05788F522BE8AB8052EDCB39D9369555AC987F7D15F61E0D + 80B94C1F8529AE8DCEE91C5DB7B3669C3AADC399531A9C38A5CC05BF768ED44E + 65DAF53875C684B3676C38DB46FF9173F43BF684715C9FA2FF5C919E730368EB + 29E3644706C7BAE278AD2B88D7BBFDE4F7D075B22AFB0DF37EBEE77AF07A7BB7 + D01BE77A0F01E037CED0F53AAD27D1B5A563BD7E86AEEF99BF1000B34E2AF057 + 2D1147624330969A0B1C0584D55CE1281DE46D485D4EA6A9D5AAAC3AFE10EB50 + 3113A9E6F896F999D3B09AD295ED325FEAA08C643A076CE765FD52AD58542BBD + 52D72B52F2F01EC0DF6A01FC83EB7F2200FC5DED5043D9BFA77E3000CCFA0700 + E0FF5974007DD56ADC3CFF230260F900790500539A7C98349751A71D3C681A12 + 0FA3C6391E9C6BE318DFA5EF03805B81DFE6B07C11707BCED18BDFE94DC0ED4F + E30CBFB0B92ECA7308D6CA3089CBABC3DF2A59BEB91E751CBFC4A4542FB103BF + 2AFEE08526E31B3A781936FC3F84644743DDE1504B76305E8A3B18F422A76B28 + C1EEB759F2CA254CD86D5EEE8425C1ADD4519DABA3D2D41D34994776E0649C8C + 6FD5D153C73100668934EA2C72C752DD69643F2FA9DC469D6186C0325DE66DA5 + 838E2A77667B6D24EAD8B2FA14A02BE38EF751075C431D711287794F8E377A94 + 7CF28B49EE343300E64EF2B1469CE85C374902630178499C474A74F45592E545 + 9916E90779C8950306B98C50ABAF4A8568D0C190F5845E81C00C5D2594950057 + 5802EBB88D3601799B0130C7AB2D7F9BE1AFAC8FC569EA304B81BCCAB1250416 + 6057E597E0B719004BF82B25BFF25503E1B334783C4D034F9684B9CDC0571DDF + 1C2725E315001CA401B1027B25D06DA5DE0600E63CAD00B004C59A68E2609F60 + 097EA5752FC360690D2C759405B004C002E486630796C08A15AF620D2CA1B284 + CCEC4A3FD725F37119AE8B273E7A03018AE37A19123374E6E3B2E57248F865BC + 2E1C81211216C09521B004C13CC9A3EC15EC121320AC2EB79BEAF589C91E4D88 + 27591800B32530D513A163919F5DE9D7521EB9E4345B229862113A4E58997C8A + 92E2146E580C5B5271588592B0A693B06552B067D3B092EBC8A5E12A64859C24 + 3B85AD99044C296A375B3D24C3B0A4C3B065A3B0E7E370141270169370969270 + 9452E4A6E0AD64E0EFCF91B2F09429AD40F91806E7A25417B5211D803D13862B + 1F863B1F81A710A3E386A81D01921FB65C80EA0C53D9287CD504BCE518E58D88 + FCBE520CC15202C17202A14A0AC10AB90349C487B3484D14919A2C21496E6C34 + 87C8700691912CA2EC27373E5E406C3C8BE05002811AD5538B233C9446941463 + B79E42A49E44B83F864039046FC10F4F29003F5BF70E460F1465D03B9E417A32 + 87EC540199891CD2A3696429AE40FED4488AEA0821D21F4686F2A6B80CEF1B9C + F5883D80798F617340076BC88060CE8D44358848C98360D189084F1E57BC880F + F890AE07911F8DA27F3A8DA1F93C6A8B05541772C8CFA6909B89A3309344793E + 83DA4A11B5D512F9D3C84FC7D1BF9AC3F8DE00C6B6AB18DE2861727700F39746 + 3173BE8EF1CD2A26B707317F710C0B97C630BB5BC3E46605E3946FEEFC20B66E + 4F63EFDE1C36AE8D61E5D210D6AF8D60E5721DD39B454C6F519EDD0A66B68A98 + 58CF6364298DE1C52426D67258D8EBC7EC461143B311D4674202064FAFA6B1B8 + 5DC4F25E190B5B79EC503D57EF4EE3E2CD092CED9430321F427DD687998D1436 + 2EF763FB7A8DDC0A562E14B0C6CB395F2961613F83850B596CDEA862F7D600A9 + 8A8D2B05EC5C2FE1F2BD1A2EDDAE62E75201EB9B742C06CCF3494C4ED135AB79 + 904C1A100E6A100DEB91C99851AADA501B736374DA8FF995A82877E5CE20AEDF + 1FC0CD8755DC7FAB86072FEAB8FBF600EE3D1F14BAFB7C000FDF1FC69B1F8EE1 + EE3B23B8FD7C88DC31DC7C3284DDEB59ACED533D57D3B87ABF8A3B4F29FDCD1A + B62FA730B3E4C2F88C1D93F32EF2FB30BDE0C7CCAC1FD374EC91213B86EA768C + 8E7A303519C2CCA40FE3C37AB1CCF3D4A8094BD31E6CAC4470F9421EB76F0CE0 + CAC50236D783D8D90CE0FEDD025E3CAFE1BD776AF8F2B309FCF49B597CF5E518 + 3EF9A8461AC4071F54F101A57DF46C148F6F167175278ACB5B515CD98DE1EEB5 + 3C9EDE1FC4AD2B795CDC8E515C9294C6DE4608DBCB5E6C2FBAB1BFE6C5ADF331 + 3CBC9CC1D3AB39BCB853C0272F8AF8E2A3023EFB2087AF3F2BE30FBF1AC3FFF2 + 2F0BF8B77F9EC31F7F3B8DDFFF7A02BFF8D9287EF4450D5F7D31889FFD741CBF + F9D51C7EFE8B497CF3138AFFD1307EFDEB59FCF9DFD6F1EFFF651DFFE5BF6EE2 + BFFCB74DFCE95FE7F0CB5F8EE237BF9EC29FFE6909FFF6A735FCEB1F57F1C7DF + CCE3375CC73723F803B9BFA3F23FFBA4828F9F26F0F8AA1377B68DB8B763C6C3 + F34EDCDBB5E3E6A61937B76C787C25884F9E94F0C9D30A9EDFCEE0AD5B39BC7D + B788BB17E3B8B2E6A37C215C59F5637BD28AFD1927AE2F057175DE8BCB930E5C + 9972E2DA8C9B5C076E2FFBF0EEF5223EBA5BC5E34B513CB91CC28B9B09DCDA30 + 6377BA03D75774787CC18127179D784C6D787E2584B7AE86F1E6051FB5CD89FD + 452D9647BBB0326CC0D684878EE7C2857907B5D18D67D7FD7872C5817B7B46BC + 7DDD83F76E07F0E27A004FF643B843D7FE26FD0E3796D8EAD886EDB13E6C8E75 + 6373BC1D4B0327B03E741AD71635787AD14EC7B4E2D66A17CE4F9FC285990EDC + 5874E1C146128F76B3B8BB1EC793F3F4DB5DCBE0E3FB69FCFAE361FCDBCF66F1 + C7AF26289CC7B5251BF666BA716DAD1B77E93ADEDEB26267AA0FDBE37DB83C6F + A2FAB4B8B14AD777CF8DE7D7A278E74E1A7777C358E68F09926CE9ABC1FA6C18 + B72E0FE0DA7E1ECBB34E2CD0351D1DD0201B3D8D4CF42C4AA96E14135D4806CF + 22EC3A8198F734C575A2926118DB8D429CDC9406FD6CFD9B315058874CA80B99 + 7027F2512A1BEBC2605A87918211C3050386F33A8C16B51829F6627AD0808511 + 3336D8427B238C4B1B11ECD26FB6426D9818ECC370B51BA3FDBD181FD46162C0 + 847A4987121D371FEF12E037153E8758A00DA96007B551832A5BFA521B2A5923 + FAF36654722694486C05DC5FB0D0F9F420EA3B8B58E81C929176049D67E0B39D + 41CCD729CAD78B74EE836E018D47FA9D54975E2C1D5D486B493AE4D37AA10243 + DA7E1B2626FC989B8B6179859E7B1B19ECD0F36F652984F9392F5696FD387F3E + 836B37FA71F55A157BE7D3D8DC8E62639357358860612180397E664E7B3139EE + 419DF745A76B53CC6B51299A3150F5A052F1A04CEF914ABF0FD59A17A57E178A + 15274A145F2CBB90CE9991CC981089E98452190B62F4ACF4F8BB8505B02FD84B + F10624B356640A4EA4F36E2AE322BF07297263293BA2491BA2710742119B00C4 + 36672F2CF61E72FB60776A617368E074EBE1F61AE1F418E1F29AE08F58E00FE9 + 29BF968E41696C71EBEAA5741D3C01A39037684620624588EA8E90122937B2C5 + 004AF43E2E909BCABA49D486B4036E86D3091B22391FA2591F429437488A647C + 88E58288537F2194F2C31BA1BC410BEC7E132C5E03B9D49E9009EEB0855CB390 + 878EC990D71D35C31BA376D2B183697A5F177C624B87608EFCF40EE777757220 + 44EFF50872C351E447224285B118CA1309B18A4669344E71318A23FF5806D9E1 + 18C22537C245E55D9FAE45E87DEF878BDAEE4EDA11A23E4180FA06BC0D8527E9 + 4280FA412C77DC0D5BD80E6BC00227B5DF11300BD713A17C2477D8065FCC8960 + D243E7E9A5763BE08D3A280F5B069B6074EA6074186060D86AD30919D81AD8C3 + 16C0A68365A4D9357A2C627965DE8357E7A2321E65296686C36C392C20B0DD08 + 83D30CB397EAF7DB61F159A91E2B4C2EF27B1928BB60723B28AF49EC23CCE057 + C05FB1CF302F4DAD411FB785AD94FD56EAFF38608FB8E064E01DF3C215F7C1C5 + 6097577C49D0F5C806E1A3DFD09B0DC14FFD3F375D134BCC0D07FDA6EE4C4840 + 60673A28FA920EEA5732E0B58815688262351A473A2AC46133893F526499394F + 92CA53FFD5C94098FAB6DCA795E23E2AF76F9DD4B77550DFD54E7D57EEE7DA19 + FCE6A9BF9C4F93523067623051FDE66C92E2D3545F86F2A460A1B039ADC0605E + B947ACE0D3E8C7739F9EC5FD790EB318FC0A501CA3FE3A49F6E93502F6D23881 + 57E2A171838657016AA837CC1FB8F2D884C73A7E1A9F286304AE9B41AC9404C3 + C25A989729767AD1E1E0A5885D029EAAC590F8400C5FD9BA97CA897A7865A340 + 10BD8D314D2FB5A78FDAA58DD3382A4663B44884C63C3C36A376515E16AF8624 + B6C92171F92E9F175D0C7ED9CA99AF01B7597C28CBE9CA87B75D0C6F3DD4769F + 8FC29C2E576D8A1F922618A3B42895A371A18FC6855E3A7603F276B618477691 + DB49615E5D4AC05AD239CA2B25016F07E56BA7FC6D54D7591AC74A7198F34957 + 8AC36A290099B74152E0B10295A342ED5EAADF131130B82790A0EB1113A095C7 + E19A007F945A47A83E4ABF37A579D87299C6F62E4A6FD425C6DF0C9305A096F0 + 9A5D6A63436DCE9792D0F710FCB5FA057C6D165BE44A9DB20570D2E6C7498E27 + 9D219DA5F833161AEB5379562BC8AB960288593E017FD500F8942578007FD9E5 + 659A59BC043483D483A59F4DEE976A9AA351FB794E8761AB04AE277594AE7353 + BC8BC2EE03F8CB4B3D33003EA9A7E31E21018D5512E51B00585801378EA39E57 + 62C939ACB3663A7F9347E439B004166D7288B936F57C9B7A5E4DCCAD35E259DF + 0582D500F8182F7DDC981754CF094A713CA77FA7788EB131FFC94B1E7F971886 + 1E15DFACC3F3B27DAFCCC9AAE769050096E538AFD877B7B791AE1716B7274F1B + 71E6E44B00CCF95E6BD7E0B54E2D5EEBA0F26728FEB40127DA4CD4262B8E7579 + F15A6F08278C099CD626D1D695C6E97351CAEFA7FC6E4A7792DF4AC7E679603A + 1E6FCDD900C06F9CEBA6FA7B957338A3C7EBA71500CC2E036045E4174B43531E + 86D5F27C1BF3D387E6BF794E5C35A7CEE1D74F5319923A8DC36F703CFB55927C + A0596AB6D0AC56F959DF96E7A8B497712FA5662652CDF147E66F00E0E63C2CC9 + 65249B115C49729D03BEA3AA8BEB68E25047E9957A5E91924772AF567CECEFA3 + 7F2C007EE5DCFE92BABF25AFA8570D65FF9EFA8701E0A693FC9F5DF2CFFD17E9 + E026526EC8A3D47C63FE23C40F09B1E1798B36A81F2CEAF86F4BFB7B2C01DDEA + A1AA16A7F1835E58FED20B41C6F343504F1DA681A159D44616D0DD67A71B8C6E + 32AE8FCB511921F6AB24EAFC36C972ADA44E67BF1AE61EE59761598E5F6AEA34 + 95D410F76FD1A1176E4B8BDF979D0D45470360B5641E097E4F50874AFA398D25 + C1EC517057EADBD225C05577D6D471EA78A9561D3CF64B00CC56C0B213293B94 + 2CEE6832003E4B1D62D92195F9D41D51759CB2DF2E75B025F865B84BFFBFD77B + 6D0AE46DC05E164360291927E1308BFDC7BAAD144F1D2EEA204B2B5F35F4554B + 6DE52B3BD36AC97232ADB9732EC51D743140680C209A01B012AFC0DF2E671896 + 5801FA207514299DCBCB410787655D2C513FD72B20B013A7391FD72544F12A58 + 2CA1AE04B86AA8CC92A0570D7D25046E2ECFB097F70F6A16C737C35FD6692B9D + 6703F6B6127F492C41AF5A325E42DEE67419DFE1A601B0F85ABA61FD4B03641E + 804BD02BC57172802E07E9CDC098F3C97459464262098AD5E5A4D4C76ECE2BFC + 0C68A33168627121F6B37489A4506F84F232BC0D4784D82FC5614E67C974F6F3 + 44882616A5F2F1837A58DA784248178B411BA574720D8984903E1E17713C9922 + 277278F2A4D34B037FB71BEDBCCF17C573BD3CD9D2130E1D4CB868F87807932F + 6CE14C61928ECE439FA06324151953711848EC9AB329587269520AD6421AF662 + 168E525EC85ECC515C16A67C1A669EDC2AA6612D66842C2CCA6FCC2561E2C9AD + 1295AD66E11A2CC05D2BC23B54868FE4AD9744D84DF1DE811CFCF5A29077300F + 0F85DDFD54A69286B39C16AE9FE28283598448C1C11C3C9514B523065B210C5B + 29024725467525E1AF6510A867E11B4C23400A0FE584A2230524C7CB484E9491 + 9AAA203B3780ECE220F2CB7514578785D89FA6B8CC524DB8ACD4423F6900E9F9 + 7E5215E9D92A52331564A62B484F537D5325018F434309041912D7E3080F2711 + 194908454793484DE4919F2DA3B85845656510C5052A3BC1003A8BEC7481CA53 + BBFB83880C46901949214BE5535457B41C44A8E843B8E847A41C40ACCA80388E + FC781A99D138B2A35154E70AA82F975099CF223F1947712E8DC1F50A4636FA31 + B23580A1ED01F46F945158A536ACE4505E2FA27FAB22DCD25A0165D2D0DE2066 + AE8D636C7F10F5AD3266AE8C62E5F60CA62E0E6178BB22DC851B9358BD3D8DB5 + DBE398BB3C80A9FD32E6AF0C62E5E608D6296EF5FA2896AE0C63EDC61836AE8F + 61EE7C3FA6B7AB98DFEDC7C2F941051C533B26D74A98DEA0F89D1A16E8B8531B + 058CAD24292E87E5FD019CBF39411AC7DAC5015CB833891B8F17B17F7B02F3DB + 258C2E2730BA12C3FC5E01DBB74670E1E1444363B8F8E6242E3E1EC7EE831A36 + 6EF563EBCE00366E96B179A38C9DDBFDB8F27014B71E4FE1DAFD11EC5DA96075 + 2B8B859534A6666318A87B91CCE81108F5221635A098F7A07F20801A2FB53C1F + C6E266161BFB45EC5F1FC0953B355CBA5BC5A5FB15DC7E36845B6FD571F57115 + 97DFACE0CAE3015C7D5AC3B5E775DC7C31825BEF8CE1C68B51DC783E8A4B8F6A + D8A332E7EF53D9C7355C7E5CA732355CBC57C1C6D50C663702985CF4606AD98B + D9B520E6D722985B896272C687DA901543C30E4CF05EC8B3112C2FC7B0B515C5 + CE56044BF35E8C8F5830356EC7FA4A04DB1B09AC2D87B0B2E0C1FAAA1717CE87 + F1F05E016F3DADE09DE7FD78EF453F9EBF55C4D32779520E4F9E64C512D1CF1E + 5471FF460EF76EE4F1D69BBC0CF3243EF9701EEFBF338DDB374BB8B49FC43E83 + 256ADFF282035B6B1E5CD80CE2CA6E1837E818D777C825FFA3EB293CB917C38D + AB565CBE68C0A387017CF649113FFE7A005F7C56C0A71F17F0D9E7657CFC4909 + EF7F58C03BEFE5C92DE2BD0F8AF890E2BEFC710D3FFBD5247EFB4F0BF8ED9F16 + F0AB3F4EE0F7FF3A857FFDF725FCFE5F66F09B3F4CE14F7F5EC19FFF970DFCE9 + 5F56F12FFFBA8AFFF0E70DFC0772FFF4FB19FCFE1763F8DDCF47F14BAAE7AB8F + 8A78E75108772F9A707BD788E777FCF8E049109FBE1BC1971F26F0F97B097CF5 + 610E3FFF6A10BFF8AA866F3EEDC787CF537870C3859BFB66920DD7776DB8B3EF + C48BDB717C703F8B17D71378FF660A9FDCCBE1A3BB39BC7F2B8D7729FCC1833C + A567F0F8AA170F2F3BF1F6DD20DEBE13C0DBB77C141FC1A76FC6F0E13D06BF6E + 92179F3ECAE0EBB7ABF8E061060FAFF87175DB49D7D08FDB7B11DCDA0BE0C195 + 301EDFA2FC77BD787ACB8127371DF8E871103F7E378DDF7C59238DE2775F4CE0 + 5FBE59C2EFBE9CC6D7EF54F1DE83381DDF83FB172C787CC58EF7E97CBF7C12C7 + AF3FCDE10F5F65F1D5331FEEEE74637BE22CD66B065C994DE0D14E3FAECDA7B0 + 371EC4839D247EFCA28C3FFF7202FFF99FE837F8BA889F7F5AC46FBF1CC66FBF + 18C117CF73787A2382FBFB615C58B06365A4174BF52E2C0F77E3FC820157D7AD + D85BD46065A213F314379C3B876CE02462EEE3182E9B7071378F1B57F25898B1 + 63B0AC4536D18D6CBC0B03453D46076DA8574CC827FA10F5F4C06FEB42C0DE89 + B0AB0F11B70689402F32916EE4E37DC8457548068D8879CD8838492E2D12DE6E + 14227D28C4FA504DEB3154644B5B3D6A452DA6EA56CC8CD83037E6C0EE6A1457 + F772B8B095C4FA7C00D323568C0F59303962C7D4A81353C34E4C0C3930346043 + ADCC7B005BD15F342113A76783FF1C92A14EE4A80DD9A886DAAE453EA927578F + 544443D22213D309A5A35AA164B807B1403702AE0E78ED6710F69C137B09332C + 1E1E7062ACEEC150BF13456A33EF219C4D685060C04DCFA374A207C964170A05 + 1D6ABCA7F98C1F2BAB74EFD33DBEB599C0C67A8C14C1C626C56D27B0B39BC22E + 2F077DA5806B37FB71F57A153B7B192CAF2A4BCAAFD0B393C52B0B0C0D5B51AA + 18C412F7FCA14BB1EA40A6684634A54130D68D0443E9921DE9BC15D1A40EB194 + 01A19816FE702F3CC14E58DD67E0F0B5239234205771A138E046A9E64665849E + 5793010CCF86850626FC280EB990AF3990EDB72359B2209A332244E7CB8A64CD + 88E5AC88E729ADE040B2E84422EFA4B0030586D1351712943F426D88A4748853 + FE6C89AE579541B517E5013F2988CA6000D55A10FDF510FA8743A8D402289419 + 063B28AF1BFD2321148683C8D649724FFB7E1FB2B510F243514AA3F72ABD5B93 + BC4D43DE8D70CE8950CE015FDA0A6FCA0C7F8665A130BB5604732E444B3EC4CA + 7E24FB43480DF05EF931A4EB31246A6124EA11E4C692284CA55098E69536D2E4 + 4FA030112757598DA34CAACC6614CD6430B858122A4EA5A97C48283914467A24 + 466E4C6C1911A1F671DF2045C70AE67DF067BD08E4FCF065BC02063BE30EB893 + 4E04331EB1FCB43B66838FC281B41B015E9D24E98627E68023CCCB54F312D266 + B8A276382336D8C86FF1B12C307994A5A319FCF6D9B4E8E27D981D3AB1A7B02D + 44E523BC67AEB2C73003604ED393CBB09841AFC16DA6723A05E2324C76F1F2CE + 8ACC3E079C612F5C619FB010664B613DDDC76CFDCB96C00206F3F2D30C9E7D36 + 98193AD3B158BCA4332FFB6C0A5198DA618ABA60492AABBFD8D3CA8A30BC328C + 2313843DC516B83E58D37ED848560ADBD20AC87515937090CBF056425C7B2E06 + 732A24740074F9A3459287FAA1FC91A2AB44FDDA0C8DF57251EA0B2745DF94FB + AF3EEAB7FA6B0578A8DFEAAE66841CE50CEC246B292BFAC9862CF5E333D4C726 + D7CC7D6A8A6759F22998382D1D853611A63EBA225D82C60071EEB7F378A11147 + 7E5DC315FDF6469F9DA54F521D24633A45611A4FA4A8BF4FD224694C10A7714A + 94C63C611A3B90BA2321314668567788D28381830F5959FC812BAF9274C641E3 + 4B1B2FBFCC4B0D7B14CB532F8D415C3E74345651EAE48F5F69EC23C63D3C56A2 + F10CAB97C64E2C0D8D73A438DC4379443E92A631E66140DC4BE3939E10B589C7 + 518DFAC4D82B4263B010D5D518EBF138AD27C4692CAA3F9AA2F252E98652144F + C76CA82F42ED0927A81C8F13A91E1E2F521CAB8BFC0C7E3B0251B4FBD98D0BB1 + 9F758EC16CC3DF194C08713AC7B779238724E3D469673D61A1833C34EE64807C + CEAFC0E04EAAB78BEA634910CCEAA6E370DC3937E5F1D2EF1FA5E7C7D21A4676 + F6614A65689C6E479BCB49E5957A18244B0B65053287D046E36D35FC659D71BE + 04C20C81D9EA5881C121B1EF6E2BF8CB3A048055F16728CCF057AA8DEA607D1B + 049616C2A74CAC97005881C0740CB600362956C12C35003EAE678B5FB606E6B2 + 4ABC10EFDFAB6778ABE8142FEF4C623F83593117453A4EFE533A274EEB19E032 + 006640CCD6C087FD6AA9EB55E25EC25F65EFDF97F35672DE8BEB9265780E8857 + B3E3392857A61F9EDCA000C172E5BB933C4FD46BC5EB0DE30B35E8657FF39C1B + 4BEE672BF349C974D6510098E7FD0E56006CCC0DAA0D48D4FE66713CCF69B602 + BB526F90E43CEAF10ECA4B525BEE72BA1AFE1EC0DCC61CAD1A901ECCD936E670 + 5FC92BD319008B3C54F719034E9ED6E1F4C93E9C39D1A702C0BD78AD93F27769 + 714C40DB3E5117B7EF448785AE3FCF4DBA70B6CB8B8E362FDA4ED37FA29D7ECF + 2E4AA3EBC4E0F8B50E3ACEB94E3A5E2795EF69C0DF2E0AF7D0F1A9DE53BA437A + 4DADD387DBCA7A65BEFB942239372EC3470260DED250E6E1382E73E2302FF85B + 24B6C8149CE4552621E38E8A3FC43A54CCE4809D34C57F577E56733AFBBF1302 + ABCB4BEEC3E0B6E13F4A87EA68A5630DB7C1C9D4DCECEF2AC91ABFAF5AD5F1B7 + E82FA95B9DB7557E3594FD7BEA2F06C087206F8B72DF07001F75D27FA124586D + 95F6BFB50EA0AF4AF206F99F630F60955AE49792E50ECAABE27F6800FC7DA47E + D84B0B607E00B6D10BDFEA8CC0E68AE134BFC8F93A539E9630B71127EB3C32DF + F7912CC72FA8A697DA913AA5BCE05AA51DF58254A749BDD21968A1E674F5F2CF + 72D991C3E20E8C027F9B01B004BB872130776E5E5A028B32A4EFEA4C35A7B7CA + CB9D0FB5643E9957825E964C6B2EC76119273B80A213A8EA3CCA0E23771E9B3B + 9E2CF64BA9E324003EC91DE01E5EDA590581292CFD322CE3A45F8625003EE84C + 8B0EFBABC0572D0975D57A0957A9434FE90C9039AF48A7382935FC55EB00DA92 + 645E09778F6B95386BBC88DCF82282E511D199975F7DF2D79D6779D042799B8F + A16E1743E003191BA0D744690D9D66B8DBB0D265BF8C97716A1DA43720B11A00 + F3BEBEAC574030C5B195AFB0FA3D5826EAA518F8F23E3F12EE32C895D6BC02EC + 36FCEA320C79D57B034BF1BEC05DBEC8819425B12408967BFABE9484B332CC7E + 096E65BC84BF725240E69392716AF8CBE2AFAFA5649D2C99DEC55FA347A20770 + 962721D835A4D2423CB9C19318729283FD521C9665E4048600BC8924F4499EA4 + 494347D2A73307E2384E6338CCE0D7984C1E480D8185A530F919E8F686A98DA1 + 9008EB191627F9188ACB13413C2164CA6460CE65C94D539894A2702A0B4B260B + 6B360B33A59B320AF4B5B1256F3177007B9DE4BAAA2538FB4B70548B42F66A01 + D64A5EC856CDC33140F10394DE908DD26DFD45B8EA657887ABF08DF4C33FDA8F + E0F82042133521F607282E30544190F2F829AFAF563A90BF5E20B728D22323A4 + A12242B5BC00C03CD9C6936E9EC10CDC8329B8FA1302023BAB71F8EA190447F2 + 088F1511192F213A51467CA282DC421DE5D531A1E2CA280A6B23286D5078735C + A8B849F19B13C8AF8F21B7368CECEA90881F383F87C1BD39E167489C5BAA51F9 + 61145686843F3545F58FE5111F67659198C81D283555406EAE82E2E2004A4BFD + A8AED6D0BF42E517AAC8CD149199CAD379C5E0ADF810A804101B8C20359CA067 + 4906D9B1B458269AC325AAA746E547D60631B25E476DA58A81A51246B787B078 + 751A73972630B653C3D8F9214C5E1AC3E485514C5D1CC3D4D5494C5C1BC7C8E5 + 610C5DAC63E8D2106AE76B286F9450DD2C6368BF8EF12B94EFC624462F0E6398 + 61EDF5712CDE9AC2E4C5218CECF663E6EA28D6EECD62F3E13C361F4C61F5D630 + 966F0E6389DCD92B0358BC3684E5EB2358B85CC7CCFE00E6F66B98D9A1B62EE7 + 4859CC6C57B170BE8EB9DD1A66B70731B34579C85DA2FA572E0F62FE7C592C1D + BD796D0417EFCDE0C2DD49AC5D1AA0F030B6A8DED54B94F742152B1707B07AB5 + 86CD9B43D8BD3786CB4F6770E3C5126EBDB32C74E7FD45DC78670A179F8CE2FC + 9B43D8BED38F9DDB03B8FC6814779ECFE036E5BF7A8FD2AED1B1CE97B0B655C0 + EC6212FD35B610D32296D4A15CF662742481B1C904C66723985E8E617E3D85B9 + F5181636495B14DE0961793F82BDDB795C7C5011F0F7DA5B756AC3386EBD3B81 + EB6F8F52BBC671FBBD69A1ABCF46B1F790DAF16C18773E98C5DD0FE770EDF918 + 76EE56B0722585E52B493AD78258B29AF72B5EDA8963633F8BF5DD2C16962302 + FC4C92C6263C189DF061612982FDFD34EEDFAFE3D19B93B8B05FC0F48403A3C3 + BC2FB00B33534ED46B1AD4EBDD589A37E3E27E00972F07B0B7EBC2A50B1EDCBE + 15C1E3C779BCFB62002FDEAEE2FEDD24AEECFBB1BFEDC3E50B21DCBA91C6BD3B + 45DCB955C4954B692A17C1850B715C384FED5AF361673B88BB370BB8CF16C3E7 + A3B8B8E1C5E5CD006E9D8FE0C18D046E5EF26263A5136B4B6DB87ED586E7CF12 + 78F13C81371FF8F0E09E0F8F9F46F08CC24FDF4AE1F1B324F9D3422FDE2FE08B + 1F0DE157BF9BC33FFF791DBFFFB745FCF28F93F8D53F4FE1777F9AC16FFE89DC + 3FCEE34FFFBA86DFFFF3127EF1EB29FCFA3733F8E77F5AC21F7E3F8F9FFCA886 + 6FBEA8E0D73F19C6CFBE1EC04FBE1CC08F3EEDC73B6F46F1F8A6075F7F58C11F + 7F3981FFF82F73F84FFFB6807FFAD518BEFC38874FDF4F935BC467EFE7F1FE5B + 493CBD17C09DCB0E5CDBB50800FCF846189F3E2DE193C705BC7D2386B7AF44F1 + 2E9DE77BB753F8F4CD027EF6DE087EFEF1187EFAD110BE7AA784771FD0F5A5E3 + BDB81FC0CF3EAAE2CF3F9DC5BFFF62117FFC6A0C5F3CCDE0DD9B117C782F898F + 1F15A8DE0ABE7CB78E4F5FD4F1E45606D7B6FCB87B218CF71F95F1D1B322DEBA + 1FC28B07417CF82486771F86F0C9D3247EF3D508FEE3EFD6F0BFFFD78BF83FFF + 973BF8EFFFF106FEE9A7CBF8FCED0ADEB917C5C74FD3F80FBF59C67FFFF30571 + ECDF7C56C11FBEA6767E94C4BB77DD78E74E181FDCA9E1E9853AAECC25B05836 + 6332DD876B4B3E7CFDA28C5F7E5EA636317C36E1AB7753F8F7DF2EE37FF7C76D + FCEEEB29BCF730874797E9FFB197C0A51527D6A674D89A35E2C6792F1EDD8C62 + 675587C9C1E3581CD3627EC4825AB60751D7EBC884DBB034E5C525FA6F2F2FB8 + 313C60128075BCEEC0DC8497E4C3E4B013834533725123A21E0D22EE1EC47C1A + C4FD5A24830C80BBC4D2CA0C89B35103927E3B823633BCE66E04ED6DC845BA51 + 4AF2DEC0068C54CD18AEE8313E68C2FCB8038B932EC5527EC98FF3741FEF6F25 + B1BF93C2F9AD04F63613D8DD486087B4BF93C3C59D2276D6B3585B4C60613A8C + F121272A391D72B16E1412BD2867F5A8E64C182CD93132E8C550D58352C62C40 + 70316D447FD18A7AD585C1B283E28D28664CC824F508F9DB11F29E458221323D + 6F866B74FD96E8FEA2E7F0E24C1C83153BCA7913AA05332AA4529ECA1675242D + 8A252D4A152A336CC1F252081BD4E6ADEDE401F4DD6E68633B81CD9D042E5E29 + E2DAAD019CBF90C3DA7A18B3736ECC2DF8B0BC1AC5DA062F7B1FC6E8B80BD5BA + 1515FA0DAA431EE4AB364492BD7007CE2010ED40A66842A1E21420B858752295 + 33231CD7C11FE9832F4ABF079DDFF014FD7FD6AB58DAA860869EF593CB49CCAE + 67B0B45BC2225DC719F28F2FD1F39434BA18C7D06C14C3B3318CCC917F268AA1 + E9386A93510C8C8751190DA03CE2439A8E992C39303016C4D85C0CB5F120F9FD + 42E53A5DEB410F2A437ED4C7A3189B49637C268309D2F834F9A7D2D426AA7B2C + 8AC1E130068683189E8C6176B980F9AD7E4CAE5450A332556AB7D8BB9ECAF54F + A6501C8D21371C41A61E4676282CFC392ACFAB6AA46A7E8A0F903FA0F887028A + 252F43E311B6DAE5F2091446C93F914179268BD26C16FD8B450C2E9731B052A6 + 777E119585ACF8506B60A584FA06BDBFD7CAA82CE6293E87FA6A1975CA579DCF + 6370A98CF1ED61D2100AD4D6F0800FB15A98FA0131A1643D8E587F1881BC0FBE + 8C1BEE9403CEB84DC89D74C09FF5205CF0C34F699E9413DEB413BEB44BE46597 + E39C512BCC3EDE77D820FC0C8A1510EC8427CE16B6BCC4B2038EA813F6B01D16 + 86AE3E8BB0F435926B0E5829CE2EE0AC9EE2B44EBD48630B617B98C64394C616 + C20C80191EF7DA34C242D8E4B3C21A70C11E72C31674511B9C30FB9D30F91DD0 + B92DE87318A0F39A610C3A60A67A580C7A0D549FDE6F85968EADA1747675242D + C5F1F620968457405E76F5740E3A8AD353DBF5741EEC1A626EB1B5883919A4FE + 6E54C05EB518F632DC65F0CB309857A961B8EB2AA5E02EA7455F33502F0AB06B + 652BDF22F53729BFB39C82A3219780BEE417ABDB2460A53AC40791C52C8C79EA + D767A95F9E8941CF2BE4E4A9AFCD1F4D16D202FE1A3371456906B88AE44798A6 + 0CF7E195B010854D6C31CC71E457E2A93FDF9085FAF9DCD73764F943CC8C10FB + 756986C2D40672597D0986C37152021A8AEFA331C3019465801B79F9E16B4F38 + 8A761F5B96D2B8CEE31360B857AC8AD4B0B00D373ED8E5ED73C4B884E2E2748C + 784AA83796A47A79ACC44038054D220D6D3243C74E533E1AEFC4E9FA509CAE31 + 3E62C963ABA58CC314A0CB2097C580579FA0F153222BA48B53BD51CEF3520C82 + D5E1BE485240E05E2ADF43757593BAC8DF49715DA1A40276430AE05583DE66C9 + 78098759EC6FCE27D3D479D86DF3470FAC88599CB7339812F58A7C9E185DEB24 + 5D774AF746A9ED196A37B595C6C8E64C01F642053D411AE3FAFD620CC8305C19 + 4FF31859C25F2E1B39048019FC0AF8DB64F5DBE6A03CF6B080BF2C35E86D86BD + AD242D805F958FE47D450AFC7583E1EF49A35780DC66D02BE12EBB6AD0DB0AFC + CA3CBC9FAF04AE6AF8CB62687B006A4927B52E9CD4A9976E3E2C097E6558946B + F2CBFA18F6AAE7B684CB719426EBE0F06B9D06311795AA4F2337B6803E7714C7 + 18D292E43C9A7A4E4DED976135D46D058065BA04C007E1C6FC9F9C0B947385EA + 7941B5D4738BADD28E09C8AB6DCC79B69604C0DF9626D404738F927A0EF7904E + 1F9EE7153AA5C1F193BD3875A217A78FF7284B40F39C6E8756C0DFD7BA75740E + BD029E0A68DDA1C7C90EBE1E741DBBAC38DDEE44FB593BCE9CB5519C85AE355D + 8F762A2744759DEBA1FA3AA8ED0C81BBF0FA996E1C3BCD73D9740C017E15B719 + FE2AEA3D98F71673DF4D865132BE955E2729F3FC549EA486C252AFCCC793E4FC + FEF791BA9CD071661D8A5EC97B44DACBF8C3DC443213353B698EFB4BC575A801 + B084C022BDC1A10EE597EC49F0A997E1563A287FA41A798E6061325E48B2C01F + 80070AA9EBFB2EB52AFFB7E82FA957DD8E56E5D450F6EFA91F0A008BB22C3500 + 7EBD71A2EA13936A75D24D6AF98751A7B1BFA99EA3F2FF0FA1C646DCFF530060 + 568B7CADA0B02C7BF080E138D20F01808F919487D777EB38BDD058CD5FFBBC41 + 2F13AE835DB60E6E0968594DF1EA3A0EE56B257559B5643ABF84542F34F152A3 + 97B2F037BDEC0EA48A6F7E991FCA77449EE64E41737A2B3503E0667187415A01 + 2B80973A502AFF01E415618E7FD9E139D83F58D5C1921D2AD9B9FA2EC9726AA9 + E3453E06D0E4E7CE9CFAAB3E994FDDE16BD5F913A24EA284B8ECCA8EA3BA63C9 + 6AEE88CA32524AD8D6B00066804B9D642105E6BEDEF3328EFDAC63DD6CE9ABA4 + A97550AE4F825E06B80AC46D562B00CC92A095FD27B49C57F92A5376EE9BC569 + 72B0D02A1FA79DA6018B32B0E025B1FC308468102A2C80299D97836E58024B68 + CC3098E324085683DF330C7D1BE23C02DE3620EF49F2B318EC4AB8AB8EE330BB + 0C7FD516BC7279676919CC7E097CDBAC3EB4D1A08CC56DE7BD8658E7EC3C0894 + 03C2C396BB07B097A458F1F297C28707902FC55F13BF04BE6AF117CCDD34F03D + 5063992DFE4A992507AEEA812CBB0A187EB92C9792B719022B96C0BDE42ACB78 + 29EA0A5339920CB33A68E0CCE2AFAF596DBEA090F81ABB91D61E08A327121713 + 167DC934BA63872731D86D0EB378C2432993444F3C4EE5A84D098A27713D9A54 + 06DA745600602901803359181906275302E06AA93CA7191A7098C12E5BF23220 + 1660374779B3949EE27C94964EC298CFC15428929B177E4BA9007BB50A477F3F + ACE5324CF9022C946E2D9684CC1436513E7351C96B63C0DBCF2E95EB67BF2206 + BBF6C1B202796B65D8EB8A9CC355B84707E119AB1D4886FD93C3084E8F203035 + 0CDF441DDE718A233734398410C585283D34417E8A6340EC1BA9C23B5C8187AD + 8487CBF091FC141616C20DEB60B6120E507C68BC1FE1890104C6CB700F65E018 + 4CC1594BC13F564470A24475971199AE0A373C5541666908A5CD092186BC0572 + 8BDBE342F94D0AEF4CA1B83B8DC2F624F2A4ECE6382ABB3318BCBC84FAA52594 + 7768B0BE3A82DCDAA800C5850D2A47F5A4E76B48CD4A2BE1974ACD5515FF523F + 32CB03C82C56505C1940FFFA100648A5C57E6427F3888F25111CE425AEDD70E6 + 5CF095FD880DC7519C2DA14A658B0B651448F5ED61CC5E9BC3DC8D054C5E9DC6 + C8C551D21826AE8C63F6D60CE6EECE63E6EE1CA6EECC608AC2B3149E7BB48C99 + C74B987C731E130FE730F1600E23B7A750B93C84CA851A6A978731726D8CF24F + 29BA3189392ABF7097CADF223FE55DA33AF6DFD9C5D50FCEE3D23BEBD87D328F + DDA78BD87C7306D357063171611093170650DBC8A2301741793E818125F24F44 + 50180F63782987295E4A7AA342EAC7E47A3FC657CB98DE2E63E9D22096856AA4 + 01AC5DAD0B283CBD9DC7CC4E01535B39E1AE5DAFE1E2A3395C79B6840B8FA6B1 + F7E6042EBD35831BEFAFE0D6871BB8F6CE32AEBE98C5951713B8F2F6142E3D9B + C4FE9BA3D87F30824B0FC771EDF1042EDE1FC5CEB5016C5DAC606DA788A58D3C + A61612A8D69D48170CC8154D181E0961663A8BE9D91426E7C398598990A2185F + 0C6274C18F91051F46165D185D73636E3F8ACD5B056A4F3F1D7384DA32856BEF + 4D61FFD908F69E8DE2E2DB93B844DA7934447113B8F3C92AEE7FB1815B1F2DE3 + FAFB0BB8F8D61436EF0C62EBEE20AE93FF3AB579FB763FB6AE95B075A584B5FD + 1C96B75358DE4C61693D81B1292FEAA36ECC2C84B1B917C7CDBB03B8F76802D7 + 6ED5B177B188CDDD145636A3985DF66378C284D1293DE5F363FF6A183B173CD8 + DA766267C7859D5D72771DD8DD73E3F2D5101EBE99C55B6F5571F3661CEB1B26 + ACAE1B45DEAD6D3736B63D58DB70627DCB85953527D6D69DB8742D817B0F4AB8 + 7A39869D4D372EECF970FB4A1CF7AE25F1F0561637AFC4B0BFE3C0CEB619572E + 7971E7560CB76F8571F9920BFB171C749C101EBC99C6EDFB495CBB1DC5AD7B09 + 3C7C94C1D317657CF8D908BEF866123FFDED027EF687057CF3BB197CF9CB717C + F2A3217CFECD10BEFED914BEFCF1043EF878102FDE2FE3C57B65BCF36E19EF7F + 50C6F3E7493C7914C6F367713CBCEFC3F327317CF9E9203EA23C6F3D8CE3D30F + FAF1CB9F4CE3E7A46F7E348ECFA88E7BB7FDD8DB33E0C2BE9DCE87AE07B5F1C6 + 753FDEBC9FC67DBA1E97773DB8B0E5C4D55D2FAED2F5B8B4EAC4DEB40517E6AD + B8B5E3C5F35B29FCF49351FCFCAB71FCF8933A7EFAF930BEF9B48E0FDEA26BFA + 2088F79F26F1E3F76BF8C587E3F8D907A3F8EA791DEFDE29E201FD776E6C05F0 + E47A093FFB7C9DCA6FE1CDEB65AC8C5BB1304CEDA1DFF0F6F914EE5D4AE39D07 + 83F8ECED093CBF53C2931B597CFA7C04BFFDF1267EFFCD0E7EF1D90A7E4EFFAB + 0F9F8EE0E1F52CEE5E49E311D5FFF97B33F8D1FB8BF8F0D108952F52798A7BB7 + 88170FA3F8E0490A5FBC18C19B577298AFF561B470166B133ADCA0FFC55BF702 + F8F49D343E7E91C4BB8FE3F8F0AD323E7A3A8447D733B87DD187473792788BDB + 71338F9BE7C3D85B71E002FD07AE5F08E2C6E520FD17DCD8DDB2E3FCB60F97F6 + 92D8DD8C6376C281FE522F4606E9FFB84A79CE27B0B11AC2DA7210DB9B61ACF2 + F2E7A35A8C0E6B3045E73F3162A7FC06E4D33D286575A81474E82FF762B0D28D + A1412DC6E9FA8C0FDB04782DA6DC4845FA90899F43ADDA83919A062375AA67C2 + 8095451BF6B6BDD8DFF5D131FDB87C31844BD4CE7DBA272EEC057083CEE5015D + 9B7BF78AB87E3D8D6BD75278F468006FBF3D8EB75E8CE329DDC3F7EEF563FF62 + 12CB2B6E4CF27EC8A37A7A3ED8B0BC1CC0EA1A83550FEA4346E40B9D28577A30 + 31E9C4D25298F2F31EDE7E4C4CB831CE707B2A40CF1627AA652372995E64B3DD + 181BA37BE96215F7EF4CE2E27E3FD51B466D4081C0F9B40EC59C01D5AA090383 + 66F40F9A50E9D761A066C4D8840B13D31E4C2F04B0B01AC5F24602AB5B74EF93 + 3B4FC71D9F736272C18D85B51016795FF1B50046A6AC18A0EB36386AC2F88C93 + CAC4B0BE93C1CA4E1A0B9B69CCACC53132EB477EC0006FF40CECBE1308A7BA51 + 1D7663629E7EC3D52C86A622C222B7507763608A9E93EB392C6C95B1BA378875 + 7A07ACEC57B07A51598E7FFD2AF92F97B0703E87593AC6CC362B8389F524C656 + E342234B318C2E2530BC18437D3E8AA185180667C328B1D5F0980FE5712F6AB3 + 018C51BEE9F534265692A84EF910A76B12291991E66B35EA2705501E269754AC + 7B511AF6A1C6EF9DA904EAD309D42623E44630349742753281FC481085B11006 + 6693A8CD2B30B8381643693C8AEA4C0AB5853CEA8B790CCC33B04D63603183FA + 0AC52DE7847F70912D77F923AD04B263FC815614895A10E99128FAE70B18DEE0 + 776155ACC451DFECC7C86E1D237B75D4E81DC862FF04BDBBC7F68731BC5BC3F0 + 4E0DE3E41F217788CA8DEF0D63F3FE1ACE3FD9C1C4DE2852E371448722423152 + B83F8000F511A28374DCA104E2F53842D5105C59176C491BC90A67CA0177C605 + 6FCE0D77DA016BCC0C73C404072F234DF1AEB413B6B815F6B8FD20CC651C4927 + 5CBC3C74D6476117A5BBE0C9F8291C802DEA82DE6F419F4B8F5EB71E5A9F09FA + 0043583334E4D7F9CC3006ACC242975D8DC7882EBB16ED961E7235229D612EC3 + 5D43C026002E8BAD7A395E4F710C77D9B526FC7088A59AC3B0A783B0A5FCB026 + 7D02F05A28CD96F4C39E0928D6BDBCFC75310A673E044B3A0013A59928AF39EB + A73EB28FFACB6E18A93C6F2962C94660CC4460CA923F1F83AD9880B392A13E65 + 41F43119E2DAD9B297812E0360B10A8DE2972BD338298F73200B5B85FADF8518 + F5BFC3D06542D4D78E8AB0A518A77E74829484B542F9FAA9DFCC7D67EA535B2B + 39214BE3C349E12F539E72AEA13C85A92F5ECA525D94B744FE4246596D875C86 + C907CA2971A63CAFC6A388412F4B9F49C190E3714081FAF54598CB25984A8A6B + A994C92D503AE5A73CC622F5FF29CD50C8439BCD4143E3121E9FE8D2393AB7BC + 108F5578EC22212E8F6F74091A9324E9B8691A67A472D025694CC35097C63D72 + BCD44B6257F8B97C9CC7434911E63A855274BCC6D889CB0B51DD0C87657E5946 + 88F2F6C6B82D0C951932334CE6B114B581A4A1B23D94CE7938AD57250EF7C5A8 + DE6806DD51A51E11476522395ABF0000FFF4494441547AE35C4E5177348BEE08 + 294C7E524FC3DF154A0B40DB11481E52BB9F812D836006C24A9C027295BC1CAF + D641B92095A171AA5C225AB12E56A0F2391EE3FA191E2BE2FC7DD4EE73BEB058 + E299C7C03CEED544E3B0D3EFECE9AFD26F91A3B8048D89E394CED0BE6185EC61 + AB602EC7CB412B16C12C1EA3CBE59F95FD7E0F03DFEF02C10C7CC572CF0D294B + 3F2BD0575A011F05815F5AFEF247EB0AC865782BF34B102CA1AE4C97B057A6CB + BC6A00AC2CDDFC12FE360360F59CCE492DA5352DDD2CAC76A5F14103EE4A719A + 84BAC2EDB5E3582F2FE1AC182F28ABD529EE099EFF92F9C895696CE9CBEAA66B + DBED0CE38CC12556D73BC1735CBD9487E7D47855BEC63C9B9C53534BCEBD7D5F + 49587C30D7D7909C4394F388CDE1E638B5449A98177D0971BF15F036491AD8C8 + 39D6638DF95A39AFDB3C97DB2CF51CAE04A06A082CC2A77A05F43D75A24788E7 + CAC5BC6E0795EF62E9C0CB418BFCE7C8EDA0B675D279F71AF19A96AE4F8FB282 + 23EB359E073ED34BE55F8A81EF6B673A485D0AF815F097012C9DDF49BA464DE0 + 972D8325FC3D80B4EAB974D59CB904B912F6B652330096F5C8B207F5AA24F27F + 870EE6FFC92F447E351B68D60F01808F4A1761157F6995479D26DDEF0B80BF8F + 0ECA1FA9469E06F7921C4FEA101393FC4ECD021BE143CB39ABCB7C1FC93ABF4D + ADCAFD2DFA4BEA55B7A355393558FD7BEADB00F02BF187E0AFD4ABF9143FA7A9 + 4E48ADA34EFAAFD50F59D70F21757BD4ED6ABA41D43793F41FDC64AA9BF31F2E + 7AC01CE3B6A8E2D87F4CDED88DBD810FD21A0F212986BD6F34F4D7406035003E + 00C122EDE5C394F5FAF1977EF1B01560BD8B5E6CBD3879568BE3FCF0A6F6B2CB + 5F3EF1759779D52F01F583FD40CDE16FD351F5483FBB8D17D9C10B8D5EB2CD71 + AFE814B59F5CF912574B9D47E46B8E271DD54168A543791B9D11D92139E894B4 + 71474527A406BED22F3B0772B9678E57E767579661715876ACBEB71A655EE97C + A9FC328F1A004B082CF33503DFE6B0FC8A903B9C6CC5CBAEEC40CA7829D90965 + 57C609CB5FEEC452E7F5F51E3A36758AE517912C06C2DC29562F0D7DBC87F237 + 242C868F10D7C58097D50AFEB2BE0B002B1098EB50BEC8FC36A9070CD2CF520F + 2A589CCE9D7F86BA0C6F8FF5D235E1E59DA9532FAD7D05F06D58F70AC0CB1DFE + 86387C962D85490C89252856835E9604C2CD926058026069C9CB56BDC2B2D74A + 83241A1471BD12F84AB5D380EF00FE3A7940A82C01D54661F1B5F0C180511944 + F20052E808F8CBE579CF2216FBD5E14E6FF440DD34B015A281AA84BACAD7CA2F + 21B004C06AA90170331866907C68392FF145370D8449EC3F146E8897FB124B7E + A907E1013A165B1273FE08B9B1C4C1E403FB39AE8B06DD3C29C1931A020A37F9 + 39AF84BCECE73429599794C897CC404F837743260F9D08A7A82C4F6CA4040066 + B15F4FAE2995823195A4FC14E6AFFCF359E1F257FC5A8A3332E02D55612A9560 + 28E6C5848F7DA01FF6FE2A6C958A80C0164A63BFAD4C61F273D8CC6E85E2FB39 + 7F85F297611D28C33C4069FD8A2CFD4558064935CA37548173B41FEEF13A7C53 + C3F04F8F08F9A687E121F96647119C9F406461128199517827EBF04CD4847C93 + 43084C8D20343D8A309509729821F018A58D0CC03B4CF5D62B2465B9684F7F1E + CE6A0E8E81828863CBE1D0D400C2338A825355A1E85C0DF18521C4E607C93F20 + 149AA950DE126273FD482CD6905CAA23B53622006F6E6B02D9ED09E4762651D8 + 9B46E5E23CAA9716845BBE3087D2FEAC70190CB34ADB53021C33FC95CAAE0C0B + B89C5F1D6D6818D9E5BA584A3AB534A8C0DF86724B0328AC0CA04CCACF95C4BE + C0ACFF3F7B7FFD1D4792ED7BC333D3DC3D33CDEDB6BBCD0C922C662E41A9545C + A512333333335B645B60D932DB6DBB9999B987E9CC9C73EE7DEF7AFF8FEFBB77 + 648594AA2EBB7BFA9C39EB59EF737FF8AE88D80199059919119FDC113E097ED8 + 1F7210CFF2046ED05E1C8C3E0A7F43306232E211951127C07154661C74656698 + AAED30545B905C65425C6912E249195DD9A89CAB43DE70114C8D36589A6CB0B7 + 3960EB4C83B5331DA6F654183A5361EC4A4372470A121A4C88AD4C82A62C5100 + 606DA50EFA5A230C7526217D8D818E6180B5DE82B49654E4F7E6A264A800395D + A948A94F465A931E8E8664249544212E2F1CB1D96108B1FBC027F9307D9E2308 + 311E4748B217A24C3ED0A685419711096D6A2892D242040866281C4D79896901 + 30E747C05E1C4361380C39C148CA0C4462BA1F9232029090E62B425B7104B2EB + B428684A464EA31619F5F1C86CD622B7C380BC4E23325BE83B6860D0AA45C580 + 155503361477EAA98E06E91591482D0B87A3280C8EFC50D8338360B01D473C9D + 6B64DC6E04453C23BCDD4222772136F11074662F18784F62BB270CA95ED0A77A + 2229E528921CC76048F78681A156AE176CA5BEC86D8911D0B9B0532B606F7177 + 3272DBB4C86A4D404E870EB99DC9C8A27471BF05755359281F7120AF4B8FC21E + 139DA71D253D66AA634031C3E0AA28D88A82905A128AF4E250A4E4052095945E + 100447A61FE2D9632F663BB4C643B0657A22B3888E5F124C0A4311D52DABD520 + A784C1B607EC19C7905F1A80CAC630543705A3A2D61F95B57EA8AAF74359357B + 081E4556D63E6467ED41474F0416CFA4637482BEBB9A632828DD4F653C505CE9 + 85C2724FE453D99CC24348CDDE0B47D66EE4141D425ED161A4A43C03AB650B8A + 0AD8B3D70FF5553E282F3A86C21C866DC75055EE8BBA1A7FD4D707A0A6C61765 + 65479197B71F8554B7AE2E10750DC1741C86C0DEA86F0E46676F2C7A4713D13D + 1A8BFEF1680C8E476160220A5D63D1E81C8EC1E89C1E13F346B4F785A3B2CE0B + B58D3E68ED0841776F04FA87A3D1371889EE9E60747507A1B9C50BCD8DC7D0D3 + 198881BE300CF545627C2816E3A3F1E8ED8F446F5F1446C612043C6E6B0F4047 + 6F385ADA83E9DCF6D139EE416353101A6B039197B51F69D6679162781A76DD16 + 3874DB608A780CC6B047916BDA816A06DCBCCF70D91EB4D77B60A82B08A38361 + 181F89C0D84028063B8331D01C846EFAFCB57947506CDF8B02EB6EA4253D034B + DC3614A579A2AD36160D15E1C8B41D4542D853083BFE00E2431E43BAE120F2ED + 5E28C9F04549BA0F72AD4751E0388ACA1C3FB45444A1995453188C7AFAAF54E4 + FAA230C31345D9BEC84E3B0A87690F32CC875098E683924C0F54171CA5E37893 + 8EA1BE741FAA0AF6A138730F726CDB9197BA1BC5F43FC8497D927EBBA7D054B5 + 0F8D957B515F7104F5E5F4BFA1B6330C3B614D7C1CA55987D0591F8EBE9658B4 + D684A18AFE8315459EF43BFBA1B1817E8BAE588C8D1A31326C407F9F1E5DEDC9 + A8AF8943615E20B233BC504EFFD526BA761BEA35A8AD09477D6D186AAB029097 + 4BFFAF14FAEE73BC5041D76A49217D1FA93E70F072E7EC999B7B1C85F91EC8CF + 3B8AECCC2348B51F8629F928B4B1C7101FB5177AED4E0192D352F7C16EDD81D4 + D45D282D398666FAEEEBEB7DD0D21A80CEEE30B4B606A1A92990D2C1F43F88C3 + C8B81E03234968EB8A12FFC1E6F670F40CC463705487FEA12474D0E7A96F0E45 + 757D108A2A8E233D7B3F52D2F6212DF33052330EC368D90D4DE253745D3E8AC4 + E467E0483F2A3C7033B23D919DE7839C7C7FA467F953F940A46706C0EEA07B87 + F930B4BABD30519891E98BCCEC0058AC1E4848DC8FE898DD080BDF81D0D06711 + 1EB61D91113B1111F92CA262B7232691F724DF89D8841D749FDA8904C37E684D + BC4C3DDDB32C87906C3D4CF7B5C348B21E4082711712E93F604A3D8C343A0F7B + 96274C29879068D883B8E45D48C93C4EF78A50E4D07F28B3344CDC6FD28B4261 + CAF44164D22EF8456C4550CC2EC4998E912D08B69C30E81DFE88377922C1E209 + 637A10ECB9E1228F95921781B422E77DB694EE5D741FB21606D2FD9CEEFF743F + 4A4C3B46F280867EB718EB210A8F8830D6CAE1110AE9774CF142028361FB717A + 3E7821DA7C049174AF4B48A1E3F1779447CF86346F84EA0EC0277A078E476C17 + 20D833642BBCE9FBF2A1EFCD9BBE330E43E20E203AD91331FC1252EC01F846ED + 21ED8527E51F0EDA0ACFF09D084A388C08BD27C2E83915CEB0D81A80247A3625 + 67472191616D4A00A253E8BECBFBD9E747C15018056D6E1834F45B46D9795967 + 7F04EAFCE19BE0052FF6144EA4E3D1B32EB930014905F102F42652985CA6134A + 2C4A80262F16B1A4F8A27824156BA12FD5C1509E0C4BA511B66A13AC9506E88B + 136128D1C2526E80AE4047C7D320D21189504B08424CC1F0D5FAC13BC10741D4 + 3F08358709F925F9E368F4311C083B887D21FB9D80D803DE9AE338167914FB83 + F7616FE05E1C22FB514A73FE81E003141EC19188A338187200FB83F691F6E360 + F011A1DD7E7BB1D37BB7537BB1E3F81E3CEBB51BCF7AEEC4331E3B29DC85ED9C + 266DF3D8856DC7760918FCF8FE6D78E2C0861EDBF7B4D096C3DB4599AD477762 + EB11D62E6C610FDEC3D49EE75E6CF3DA87ADECADEB41C7397E50ECD5BB27D013 + 7B83BCB02FF8B808F7042BDA4BE9FDA1A4F0E3381CE58B4311BCA4B307F6861C + C7BE701FEC09F7C6BE301FEC0AF4C0B37E87B193EC7BC279D9651F017D7786F9 + 52FF56D99A643FF52FB93F7924310A0735A1CEE59B15AF5E2ECF1EBF0C8B19EC + EEA0367747FA535D7FEA2BFB501FFB38B6857851E845FD663FEC8909C4DE58EA + AB460790281E1786FD89D1E2A547EEDF1EE6FE2DBF1C19C77DDC28ECD744AE6B + 5F6C04D509132F5E32046600CCE29732A57651FF7C77449810C77790ED59EAE3 + F31880612E8F19B6F8D3582288C612C14102ECEE88A0BE3CF5FF77464709ED8A + 89167A86C605ACED342ED81E1945F1086CA3F100EB9910EAF7872AE1D6A09075 + 310CDE1A104CA234695B60281D9B43B2D1D8E569FF606CF10BA2B14A208D4718 + 266FD6130C7B29DC426D3C4DE2B24FF8305C56002D83DCA7A89E0264FD04C865 + EF5C0ED9F62403666AFB09EFC07558CBE2F453BEC1424FFA04515D1A77918DE3 + 4F705A9C4F909226BB109579F23885DC1E9DC7937EC164E7BA41D466208DE5F8 + 188AD8F6B817D96F03825D01AF84BFBF3AC290D707BF3CECBD49D2CE757F79D8 + 0B0F1DA631358F3F29BE5E8EC1EF015F3C7490CA1EF653DA39A48C53C5B2D107 + 796C7E4400608F8438F819F4F47F8D509D1B9F0BB54D63F7070F7A503B9E34B6 + 6610ACEC33CCCB4DFF729F271E6400C94B3EEFDA00BD02F032C0DD49E37C2700 + 56C3600969D5CB3C2B4B3D531DA7041056D925007E70C7413C48F1079F65D8AB + C05FE9052CC1AE529ECE816CCAFEBE4A1E2F117DDF36B66F0062A97500BC7503 + F6BACED348F02B6DA2DC537B71CF937B36AD38F763A49ED762F11C1503DE9F3D + A688E3627E4B5556E60BDB13147F782B7EF6ABA7C53C1ACF53DDFD14CF753D2B + 20F07D4FECC27D5496E7E7E4DC9A9C73BB9DD4C0574A82DF7500AC9AF35B9F3B + 74CE25AAE714EF2CAEBB4D29AF02C0AEF0D71D0056CFB37E4F3CEFAA9ADB759D + C7DD345FEBB4AD8B9752762EA7FC8BFBE95824DE9B97C1ACF400BEEF6E059A0A + 78CBD097E1EF23744C86C1BC2CF42F394E790F53F838E53D45DFCD63EC294C71 + DE3398EB3100E673E47D7F45FA11FCFC7E453FBB4F99F7DEF0FE55B4C9E3D7A9 + EFC159397FEE9C3B57035C096CD5E0576EF9B809DA92D6E7DE9D5A6F8342D7F6 + 364B7ACD3ADB91529DD37A9C2419849A45081EE14C8B76D47627DFD9C43C98A5 + 3077B95DDE6DC4F9F27C6F57569659CF7771E09392ECE987241D1925BBBAADEE + FA6F76C8FC67389F2C7B3BB9ABF35F95B35D764CFDC52F28FCF97FE1986AA8FA + AFD47F0900DFA6DC7A5C9473F3E17ECA17723BA9BFE0FFAE36FF3B24CF457D6E + EB170D5D44146EBAA89C17CD7ADACD05FA3F23E779302C55DB9DE7E80A86597C + 6311BA976E347CC3A39BDCEDB45EF60E720F7FA5366EB4521B375645FC30B887 + 97B1E01B2F9D2F8762B90B2ACB6F10ADDFC039548B6D6ABB4CFF90D46DB8CA99 + 2F1E38FCC0E307A6EAE1F743FADEC3DDA9F532AA4E826B3E8777EC283825F78F + 107B4808C8CB9D00AAF3BD4E8A028019E64AE8AB8EB3240456E749082CEBCA72 + 1C979DAEDBE9E7BF7A86427EE38C3B65DCD152C4F60D295EBDAE9D3335D89562 + 3BB72BD312F8AAD3A20C2F25439DC64DFB02ABCADE4B1DD2FB1EDF294299CFE2 + 38DBA4EE798C3BAB0AD0156F47F21B90240973D7DF9874768CD5126F58729EB3 + AC14E709CF5D37E0572D097D252C560360254F01C0A2E34F8301B5C40081ED14 + CA41847A30A14ECB3A1CE77AECD97BDF36DE074681C112B8B218F4CAA5A42504 + 967A90CA6D12835C922BE4954B3C4BB187AF5A5C47C6D9CB9761B0F0F6E5B76D + E9387C3EBC3CB51AFEFE9A067D6E0130BF15CC6F07EF6320AB2C212501B0DC2F + E8415E468A41B0B39C04C5EBE59D105802605708FCC8412F92E201EC0A7615B8 + 7B0C8FB8405F097BDD01E0C718021FDD80BAAE90D7D5CE92B0580063675969E7 + B7C31FE5B7CFBD7DF184AFBF10C71FF1A2B224CE935097A12FC35F5700CC92A0 + 570D7F651EBF212F00B01F43DC200500FB0728933801FC863CBF29AF4CEA081B + E53D131028966CE6E5DC9EF46508CC652500F613133FF28D7F3909C469A9ED24 + E911BC274291982022ED888E208563BB4ACF0A082C45F56342B143138E9D0991 + D8AB8BC50143BC1003DFC3662D0E910E9A12858E5875384A3A447186C2FB9263 + B13F5983C3EC156C48C011D25106C7BA381CD46A0400DE1F4FEDB26771342F27 + 1D42E71682BD91BC3F70180E687839E9681CA1726229E9E4681CD3C7C0C3180B + 6F6B1CFC5212E1EF48C0F1140DBCACD102F872E8698AC4714BB400C3C7ED1AF8 + 6732F44D46489E0141F98A428BCC082FB509459022CBEC220CCCD323205729CB + 30382CCF283C84191EB3D7701083E43465F9680985D94B98E59F12075F5B14FC + EC91084A8D45B023168156DE3F3808DE5A3FF824FA21C81082604328BC34DE38 + 18791447E38FC35B1F88205B04A2B212A0C9D721A1480F4D613234C5C948AA34 + 23B9D6025DBD05DA5A8A3758503A538DAE2B832899AB83B13D15968E5438FAB3 + 9032980B3BC9DA9F0D4B5F16CCBD9922B45268684D415203D737435F6F86B1C1 + 0A4B931D066EB73219896549482C4D42724532EC8D36A4B63AA0AFD22236370C + DA925898AA93A02B4B4062512CA23342116AF34580E9187C930E2330D9031166 + 86B8E130E6C622395359123A2185158468932F42938F219A416B6E281CA51AD8 + 4B6260CA0F437276100CB921B01645C0521841612452CAA29156A5417A6D3C52 + EB344821A536C423AD2941C8D1108BF4C63814761B503D9A8A9A915494769B90 + 51150B733EB59715005356201CF9E1B06786406BF244B8660FFC42B70A05466C + 4378CC0E68B4FB916CF1842DDD1F69740EA9F9C1B066FBC1987E1C962C3F6414 + 8721B72A1A85742C5E6EBA7ECC8EBA511B8A7BF5C8EF4A42618F01457D2614F7 + 9B513A6843E9901D2514968DA6A16A3A17E513592818B0A1B0DF86F261072AE9 + 3C4BFAAD48A98E40B4E31062538FC0921F007B01431505003B72FC614CF1A073 + DB8D98F85D30D83CE060A893E78934DE9BB73C1CE5F5F1A86B4D466563020A2A + 23505A1B89A66E2DDAFB497D71E82075F6C7A36B80E2BDB168E988445D6328AA + EA02D0DE138DC9591386C613D1D21586E6CE30740DC6A38DEAD4B686A1A6259C + DA8E424D7334CA6A435054E18FCCBCA3305B77C068D88A8CB4BD282EF04441CE + 11A43BF620C5B20B79D9C75053198CBA3A3A46B53FAA6BFC515EE18DBC8223C8 + C93B8CEABA60B477C7A1BE391CD5F5C168ED88C5E0703206C77474BE1168EDA6 + F3EA0D40CF4804FAE8BC7AC69230386540F770224AAABC9092B91379458750D7 + 1484AEBE580C4FE83031958CF1092D26A793289E80BEBE50B434FBA2B52D1003 + FD31181E8AA363D071C6E330336FC2D26A3A96CEA6E3DC851C5CBA5E8293CBA9 + 6868094071E961343607A391CEABBCF438AACABC5156700CD98EDDC84DD98B6C + C32EA4C43D8DD4E46DC84ADD81F4942DC8CBDC85BA2A4FB4B6F86168200AB3B3 + 742E23B1E8A263F7B404A2AB9E3E7FF61ED8E21F81859492FC2452E9BBCB4BDD + 83FCECFDC8C9DC8D9CD4BD70E876203EF457880BF925AC89CF527A3712C31E45 + C4F17B1017FC4BA4E89E45966D0FD2CC3B9061DF89F2224F34D377DC521B8EC6 + 5AFAAECB0390957E1066FD33D43E9FEF215136C5F8048A7376A3B6F2182A8BF7 + A32C772FAA4B8EA2B9D60F8DD53EC8CBD88E4CC7165496EE456BE31174B47AA0 + B7D30FFD3D5118E8D2A0AD2610A5D9FB50917B10EDF57E18E80C4717C3D47A1F + 3435FA60685883A913062C2C39B0B2968B53CB69189FD4A3A74F831EFABF75F7 + D077D11583DE1E2DFD16C9F45B24A383FE832DAD11181BB56072C28ED6A61854 + 5785A1A34D8BA17E0BBA3AB4E2FFD3D41486EEAE38F474C6A1B9291CA5C57EC8 + 4C3F029BE5204CC90760351D12CB3717E4FAA3A4D01FB9D91E484FDB8702FAAF + D5D6F9A3A1917EFFC1448C8DEBD1DD1B23CEA78FD27D433AF40FEBE93F67C4C0 + 28FDB7C8D646E7D944E7D5D81E81FAB630543704A18AFE0775CD11A86A08A5FF + DC71A4D0F76B731C8423FD18D232BD60654FE09423B067D0F598E38BD42C6FA4 + 66D375591A86F29A389454C622873DF473039091C3CB347BC1EE380A8BF5100C + A6FDD025EF45926E1F0C86A3301A3D919C7C043ADD61214DFC5E44C66C47345D + F7F1BABDE2E58F44D3612450A8B31E45B28D45E56D87614CF5803DDB17D65C1F + 18E8DC740EB2651C456669100AAB2250561F879CD25058D33C60E6E353985118 + 88BCAA4814D76B50DE9484D2C6442A1F4EF79E10BABF84C142F7627DBA2F0CFC + 120EDDF7AD39A1CABD343390ECFED0A67A439FE90F6B7E286C0521301704C394 + E74FF7DC00BAEF074097C92FF9780969E91E9A90EA890407A5A9DE3AF04DF1A6 + B42F74740C634E08F459C13064D33D3AC35FBC00C42B40580B22905AAE81A32C + 16FAEC30C4D1B3269C9E313ED17BE011FA2CBC790FE31806BDFBE013B90F7ED1 + 0711C84B35C71D56D9F7E25830EFE3FB340E056CC3714A876ABD10630EA07308 + A5E347D1B1E859951583785EA2D91688089B3F34999457100D7D510C12E93B88 + CD0844742A3DC3ECA188B48420D4E80F7FFD717AF6F922869E77490571D01626 + 20A1301E49A55AE82BF530561B61A8324057AE437C7102628BE2E8991E0F2DE5 + 27D17396CB98AB4C02046BA95E786A30C21D2188A573D116521B25F4DC2F4844 + 424E1CA2D3A249B188A3BE414CBA061129D4B73087D03904C2978174329D0F85 + 0C88434CA122F44F625B00828D21227D3CCE07477819E988A322641D8D620F63 + 6FF8240450BEAF481F0C3B8C3D81FB498728CE65BCE111C379DE54E738D5F712 + 3A18E6817DC1ECA97B00CF78EDC5B3DE7B459CF7E3DDE97B707D9F5E9658AE99 + 3D72FD0EE3D9E3FBC512CDDB8EEFC34EDEBF37E81876F81D141EBC0C6FF732E4 + 0DF3C181705FEC23ED09F3C6AE102FEC263B6B6FF8711C88A23C06C0A1DE3814 + 1388C31ADE1A24406C0FC22078778437F6527A2FAF20C3DB8AC4852AABC750DF + 95FB908713A3142544891567F645076277B83F769276450490FCC11EBDECDDFB + 4CC871EA43FBE0594A3F1BEA8B9D110C7C437030210C4775D4175D6F3312FBC9 + B65F1B85FD49FC8224BFF018275E743C44FDDC03DA580A19062B6220BC278641 + 2DF5AD19008773BF5DD1F6F0103A177E6153D19EA808FA2CD46FA770574498B0 + B1972F43DF2DBCCD8BCAD397FBF57B62A2E91C635C00702CB64745537F3E0ACF + 725E6CACD0F6E8680183593B23A2B1232C8A3E3383601A0784D278202C824252 + 4824B60586AF8360B59E0E0C118057ADA7685CC3E2BCAD41A12264B18DA1F063 + 0C80494F3A0130C3E3C79D9EBCC29B979775F65100EE16BF502A138A27BC19D8 + 2AD0760300875299907571FEE30C7519F2FAF2B136F438D591A0586943694F80 + 5ECF6092028219FE3EA182BF1CBA8260779260D81D20DE10035F8FEF03E043BE + 0A00E6D0A95F1F65A04C63545E76FA88271ED8CFE3EF8362DFE6DD614134460A + C316FA2E1F3D4663D4A33E42EAE5A7C5D2D41C97E37486C27B9DF0572CFDBC21 + 06BD1C0A784B72F50466B8AB06BFBF74969571961A06AB01B0AB363C8115CF5E + 0996EF7F9621AF2CC3E09745F667A418042B7558D2F3577AFFB2D47336EBF33B + CE391B91FF34E5AB00B098637202DE3BC9B5BC98B7E279AD4715F0CB2BD9DDFD + 28957B6C032CAFAF76E7F406BE57CC3551FC891DC21B98C37B9FDC857B1FDFB1 + 3EAFC6E2793776A860C0BB09E43EF2CC1DC1AF5A9BEAB9CEF9ADCF396E9E4394 + F9EE24EBFCEC575B28BD01777F0C00666D9E5F55CDD5AAE66BEF3487EB6A5F9F + 337ED0E90074BFB32C03DA871E155EBA3C1F7E2F89E7C99536947959769CB9FB + C16DB8FFBEADB8E73E3A3F86C7DC067B013FCA73B64FE31E3AE7BBF95C9DC792 + 5AF7F8150058CE7D53DEBDF41919FC52B82E06C00C8F1F78C429650E7BD39CF9 + 6DE6D65D81AD84C01200731D31D7CE92F59D6D49F02BE5AE9D0DF8CB528EB77E + 7C673B9BDA54499467BBE4112EF68DBCCDDC649DA5309875C25935F358CF7723 + 75B9DB9567DB7F2B00FE85C2887E5077FD370260354773972FA52EF7437257FF + A74AD5EECF49FFAF02C0B795DC07F85F29F517FCCF7EC9FF8D72EBDECE529F9B + CB5B13EE2E2E297717E8FF889C3721711E6ABBFABC59AA3CF5CD47DC6CE826E7 + 4EAEE5EEA41F0B80D5375B7123BF67E326CC9FE12E8ADF4337F67BE9A123DF14 + 5AAFC737F2DB89CAC863DC4E9B1E08CE3ADFB339251E4AFCB0BC1D00960F4027 + D0755B86241EE62E0F5FB5643E4B760EA4D479B79328E7D22951EB670F6E86C0 + AE70972561B04C4B9B3A6F03006F405DF770F799DBDA796F0A1972478EB5A953 + 46921D35096ED569D78E9DBA0D5EA6FA2EEAC0DEFD887BDDF328C3DF5D229472 + CDBFEFF1DDB8EF893DB88F3AB9ECE92B80EE8F10778AE57239AE36B578DF16E9 + 412C3BD7AE52BFD1C971EEE4DFB7853AFC248ECBB48C731957A9F364FC7B7A92 + 06157C0C1A54088F5EE772463C68116FBAB26810C300562E052DBD7EDD036065 + B96616435F35FC95DEBD1CBA03C02C35045E07C0EC01CC032B150096F0F7D77B + 8EAE2FFDFCCB3D143A01307BF3B217B01AEA6E02C04EE02BC5F9FC46B1941C68 + AA01B2BA0CE7FFFA20C715B02B41AE04BD12FE3E4603625ED6594260017A5569 + 35001636120F8E5912E6AAE1AFDAC66524E8154B85F16483A78FB0C9A5C518D0 + 32F46589E5CC18E83AC1ADDAC621036129992725DB6071795E66ED29FFC04DDA + 4252965C6318FC7D89E5DA18FE06060908BC25C09FEAF96F5AEA6D1DF646848B + 37FB39E4B7FCB7918DB59DD2BBA3A3C484104F0CEDE1780CC535B1D819178BED + 9A683C1BCB934291A408EC888B12DAAE8914E2F8AE8418ECD6C662AF5E8303C6 + 042186BE12FC1E3025601FD9F69BC8C6DEBF9624C54E360E4539CE37C4833D84 + 8F910EEAE2C4B2D17BE3A3C492D2BB19324706614754109D6F08F645F19EC011 + 3842C73F92C8936C1A78246BE0A98F83A751032F531C8E9BE371DC16071F47BC + 90776A1C7CD313E093A6C1F19418E1F5EB93160FBFEC2484149A105460448880 + BE764494A520ACC486E042B310C7D91E5A6CA5725CD684D0420BC28B6D8828B2 + 22ACC08CD07C13C2C916924BEDE418109AA517DEC1CA1EC20A005620702C7CAC + 1108488946685A1C826CD1F04D0E129EBF21A670443962114D6522ECD10877C4 + 202A331E317949D014EA91506A82AECA0663AD03FADA1424555BA1AD3123B1D6 + 0C6DA30DDA161B92DBECC81C2F42D9A95A644D94C0D89D014B5F2652C70A903A + 5904C74431A904A924DB4821EC43F9481DCE87B92703C9ED29B053F994AE0CD8 + DAD3E0E84883BD3D15C606B3F00C8ECC8F4674410CB4155A18EAF4882FD1203C + 330471853130D5E861E465A3CBB588CF8B46547A08221D8108367B23C4EC8368 + 7B20B45991D067472181E23C61CF8A321E47B4C91B51166F6852FDA0CF0B85A9 + 3002969228180B43A1CB0D84213F18F6F228A4576B9051CB4B62C623BD2E0E8E + 3A0DEC75D1B0D4448A30AD89F29A29AF251E99AD89C2BB36BFDB80ECD62451C7 + C2EDB1F76C8A17E2AC9ED0A707C0484AB478224CB30B7E214F21307C2BA2139E + 85CE7410E6140F58D38FC391ED878C8260A41604C194711CFA540F58B27C915D + 1E8992262D2ABB4C68184A45D378066A475250366845C9804540DF8AD154D44E + 65A171BE10F57305A899CE41CD4C01EA4E96A1FE54396AE68B503E9983EAC96C + 34517ED5583ACC152108B7ED47528E0F32ABA291432AA8D1A0883E7F6641A858 + 0ADAC6103A4D39B794421F18720EC15EE48DB2D67834D3F15BFAEDA8EB32A2AA + 558B8A963834F425A163C4808E612DBA4775E819D7A3632801CD9D51A86B0D43 + 7D7B049ABB63D0D41989E68E0827FC8A46437B345A7AE2D135948CF6816434F5 + 24A1B58FDA19B0A1ADDF82868E64145786233387F721F5426EB617B2D28FC161 + 3D009B713FD26C47909FC5FB0AB307AD176C963DC8C93A8A92127F1416FAA230 + DF079595A168A6F3AEAC0E47495900EA1AA2D0DD9B80AE810434B40593FCD1DE + 1786C1A9248CCE58D1336A407357022A1BC29155E001A37D072CECB95AEE8BD6 + AE3801F1C6A7CD989E31616EC182D979B380C1BDBD91023476B447A1933E77D7 + 4034864F24E1E48A03CB17B3B17229CFA9029C3C9789FE492D5A7BE95CE83BEB + 1DD0A2A33316DD5DF1686B8E4245A92F6ACAFCD0501280F2ECE3C8A4DF2BC5BA + 8B3ECF31D45587A0A93114DDDD519898D0637C4C8BFA3AFA6D4A0EA2BD290063 + BD3118E53D971B03515F7614D52587D050E581C65A2FB4B5FA60702814E3231A + 74B784A3307B3FECFAAD4835EE4286791FAC89CF2029F23118629F408E6D0FCA + F28EA0308BC2827DE8ED0AC7D4A40ED3937A8C8D68D1DE168EAA0AFA7EE97BA9 + 2EF143798127F232F622D5BA15B919BB5057E38DEE8E700C0FC4636A4287D1D1 + 7801C82B2B8FA0A1E138FAFB22E87BD3E0E4E9649CBFE0C0F98B19585D4DC5FC + AC11C35D9168ABF344272FB3DD760CCD4DFBE937DC8F9E015F4CCCC46166D180 + D9150B66964C189F4BC4D0540446A643307D32169333B1F47D46A0A7371AFDFD + 1A3A672D4646233030188D1333295898CDC5C890051D6DF1F47D27606888EA0F + 47A3BDDB0FADBC5F7467205A5B4350DF108C8A8A00141579218FBE07F662CFCA + 3A88EC4C5F6465F82337CB1FD919DE48B1EF476AEA1EE4E51F4269A927FD8E31 + F4DB68E8FFE687B24A3FD4D485A1A63E0235F4BF6B6C4F40E78001FD6366740F + 25D1751189B23A3A46B50F8AF9C5055E22BA2E1CC555C1C82FF3471EFD8F0BCA + 82E81A884439DD9B4AE91A2DA07B554E711832F282E0C8F2436A0EFDF7E97E56 + D9A0A5EB4987DA160DCA6AC3C9E647F9876173EC87C5BE0F66EB5E58ACFB914E + D7766E6E087D16AA9F42651CFE484D0F82D9EE039DF19890D142D7527A20AC59 + 21484E390E1DDDAB7875027E39C544D79B35C71F8EA260D8D8F336DB1B49E947 + 852C749DA69704A3A82E16C5A4EC92205838CF4AFFAD1C0FE456D2E76B89416D + 47122ADBB428AA8F417E752CF2E9FE935E120663B62F0C993E482B8D4466790C + 528AC2E8FE130A734E304C39D4167DE694C210D80B42E87E4BE5F38261A67B16 + AFDAC0B2168409D98BE99E4DE2177AF8451F3DD537F2BD3F378CDA88404A710C + 524B35482989152F04A594C522B592EEED5589142620A386F79FD7C25C18275E + 2A62601B6D0E4060FC3104C41D45508227859EF08DF1807F8C27FC633DE0177B + 0C41F19E08D51E4770BC0702638F228014A8E1F21E88D0FBD2B3210CDAF46824 + A6C720312D06F16951F45CA2E7B23D1861B64044A58588BDF0B5F9944FCFC384 + FC48E8F2629194A58126251C91D620512E322D14097931944F762A1B57A44162 + 6922B4E53AE86B8C30355885926B0C48AC48162B77684AB4882D4E404CA10631 + B9D188CD89115EC2E1E961084E0941585A38A2194817242081149F1B074DB606 + 49855A18CB0C14EA1097134F36B6C7D133381A118E488459C3116A0913DB4788 + 2D24CCA108255B24F541C26C1102147BC479E198C6733DE4AD28020D21542F02 + A1E608041A4310A00F866F12AF52128C2063043DD7A3116C8A428081FA33FA70 + B287527E08BC1302E1A9F18307C92B2E00C7297D3C3E98C2107851E8191784A3 + B1FE3814E583C391BE38C05036D45301BE0187C552CDDB038F604F9897D0EEF0 + E302E01E8E0B56A409C281D840EC25DBAE483FEC8EF013CB311F8862F9E36074 + 208E6842E1991829E4413A96108183B12138A009C1A1F8301C890F878796F2F5 + B1F0A6FEA70FF54539E43EE431EA4B32BC3DC62F1552DD83B10C5A8384F6C584 + 084FE1DD918122BD97D27B48FBA88F7A80FAAA021E27F18B88BCAD492C8EE8F8 + C544DEEA241AFBB531D8E7D44192B2CA0D03608DF00C5EDF3A252E9AFAB911EB + ABEF30DC656F60DE7A6547981A0087D3B9509D68EA8347529F9C57EEE1259E9D + E2FEFF4EEAF70BF01B1D4BFDFA38EC8D8B1762E8BB232A1A3BA8BFCF7A96FAFC + DBE334D8111FA784A4EDB1A428CA8BA47140048D0348DBC222680C11B9AEED61 + 94171A85AD41E1783A308CC621A12405FA0AC01B128EADA194174279C1A1782A + 88F228643D19182CC471CEE7BC2768CCF3388D879EF4E5315190684781C6C134 + 760AA4F114BF78CBF9D48E7F382982E2E178C287DA6310CC92D0D79B6CDEE178 + CA8724CA8451DD203C427AD427188F511BACC7FD42F13887DE6463EF5E86BC1C + 273DEA49F2201B854F78D1F99138FEC8D1003C7C840130E77DDF1B9843B5EE0C + 7E15F11EC00C7E37C41EBEBEC2E39743F6FEFD351D93E30F1FA331A6678080BA + 0FD2D8FA011A5B3F7480C6F147693C7E681F1E38B0170F1EA4348D99B90C4BEE + 352C01B058129AEB39F5208DFFEFDF75040F30C0DD4D63F93D1E229410D855EB + 0098E20F51995F535916835F097A0504666DA7B8537786C0ECC1AB78F44AF8AB + 78FF6E2CF57C1FEF0FBCF5A090F00026DDB3F500EE7E7AEFFA5EC06A00AC86C0 + F2457DB518044BC9B91C9EE79173462C9E5F7237C724ED328F43E9A4C0E0F7AE + 47140900FCC81EB1E2DDBD548E61B080C04F284B4673793EE62FD813F8B167C5 + F2CF0F3C49E51EDDBE3E57C7F37202023B212F3B684831CC95F64D8057057EA5 + 36E53BE7005DE701A55CE7106FAB5F72D92D14DF0C77DD01E03B8160B9EFAF70 + C471CED7FE82C47BF2CAF4FA5C2FCF113BE76B65DECFA91C2FAB2CE77137F2A8 + 3CC3DF5F3D4AC7610729F6049673D294CFDEB9F7D2E7B86F2B1EBCFB59FCEAAE + 67F0D0CFB7E2BE5F3C857BEEA6731120780BEEBD7F0B1E7C4011CFA5B377AF80 + B9D416CFABB34DCCB1DF4BC7BD87CEE36EFAACF7D077780F7D3F2C4A2B00983E + 8700BFBF768AE262FE9BDA74CEDFABC5F3FA3F465CD6DDDCBBBB7979019229ED + BE7DB66DD8D721B0BA1D2773504B9475E649B99659672C2EDC44B214359CFD5E + DE1DA46ECB5D3D77365709EEF4035C8AA566416EE5E45CB765623F55FF0CE353 + 7337777257E7BF2A67DBFFEFF100FE2108FC3F0180593FE50BFE6F94FCA3ABE5 + AE9C729E1B178ABB8B4BCADD05FA4FE9366F79FC64A9CF9BA5CA53DF74A4FEAB + 0098F57DF8AB16DF5095B8721E148AEF4EE6297BFEDEC30F4651861E7AF4E0E1 + 9B34C745C8376479737723D9CEEDB4E981C075D4F13B493C7837DE44BA5DFE9D + 40F0EDF2948E8022752740E9086CE4A9E55AFF670F915DC05E25DC90027F5DE5 + 0E0ACBB82BF8558BEDEEE02E035DB5D479B2ACB46F94533A891CAA01B0BA13A7 + EEF0B996E57C0980D97EF723ECD5FB7DE82B25E1AFDACEF1BB1EDE2EC4712E73 + 3F0360EE30AB3ACBA2A34C9D5CEEF02A21A755FBA738D36AC94EFA5DAC1F017E + D5922057C665475F825F3510569755D757A7D576D996A847E7269614120310F6 + D865EF5D1E94D040E369CADFA2805FB5042C2649282BC56929E5ADD8DB03E03B + 49C05FB904F44E1A54D1208BDB5F5F029A977EA6C1DF3A00767AFD4AF82B00B0 + 13080BF84B120346176F5F06B9BCDF10EF3BC49270570D7D5912FC7259657F22 + 25AD9457BC792500564B02DFC78E503D86B71E8A77EE230C8169A02BCA511957 + C8CB62D02BBD79D5E225A079DF25162F0BADBC61EE27C4CB8D49893DA318DE3A + BD7BD98357EE5BB5EED9EB84B92C018719F852B975CF607EB3DD872730360363 + 5947B4E704C0120A0B4FE0C0C0EF8997767E263814CF0406539CCAF0DE5D61A1 + D8E17CD37F774C347646C788491E7E939F277976C7C50989099FA82891B73B56 + 833D1A0D8531D8151D25EAED8A8BC52E6D9CD08E84586C8F8F11DA9940E512A9 + 0DD22E16E5EFD125605F7222F61912B0DFC89EBE0C7893B0DF9028D20C7EF791 + F65B7438C8DEBF56CAB76871C09C8823B6641C25310CDE97ACC111B316C79C4B + 44B3A7C49E8428EC8A0DC7CE18DE6798141B867DBCC7B0580E3A1CFBE32285E7 + C491E43878501D4F6AD3CB4AA1259EDA8983A78DC1AF163E193AF865E9E09F9D + 0C9FCC441C67F09BA1856F6612FC739311586844408101C1C56644963B105595 + 86F0F214B29B84828B2D4221BC1C34D9232A1C021233148EA430A6224D28A2C4 + 2E968C8E2CB62282A170AE41780707A56B1192A9134038203D01FEECF99B1687 + 886C2DA2B293109AAA41A84383E82C2D6272B488257B42BE1E492566249559A0 + ABB4C3509706634306F4B5A9D0D5DA616ECA80B53D07C6D60C2435A640DB6487 + B6D98EE48E34987A28AF2F0B8EA17C38C68A913A5E2494365982B4A93264CE54 + 227BBE86E2E5223F6DA41096DE2C183AD360EFCB466A5F2EEC0C813BD391D299 + 014BAB0309953A4416C622BA5083C48A24B1347472B51E7125F1D055E860A933 + C350A54772B90EBA52CA2F4F8691E2C945F148CE8F43524E0CB4BCBF61660462 + 2CFE62C94D3FCD7E046B8F20CAE48D787B0012520310E7F045748A1762D37C10 + 9BEE8B180AE3B3FC602C0E83A35A83941A0DACD5D1B0D7C4C25C19097D692892 + CB8261AC0C235B14D21A35486B8E83AD368AF2C3A12B0A407CAE1F92F2FC9198 + 4E6D5A8E2124693F02E3772334E900C2491189FB1114B31D8151DB10C15EB529 + 1EC828084126C30DDEB732C31B5ADE4F33D50349699ED0657AC052E48FACDA18 + E436C623B73901051D7A94F459513694828AB134944FA4A3623213D527F2D078 + AA0CAD676AD0B45285FA53A5685EA941CB993A342E558B78F35225DA562AD17B + B6160D3379B0D7462331CF1769751A94759B50D56345E3503ADA47B3D0D09B82 + CA163DCA9B74C8AF8E8383CE539FED015DD621A49607A2A223090D7D16D4741A + 50541F8B9CF21014D486A1B24D839A8E58D477C5A2A5371ECD7D71A86E0B4359 + 43002A9B4351D31E81DA8E0834F5C4A06B34197D9326B4F425A0AA251C158DA1 + 68E9D7A27FDA81D60103EABB7568ECD151D964D477EA50DD128F9AA67834B569 + D1DC9688DAFA181495042197BEF732FA7D2ACBC2909FE3038B793792B45B9196 + 760885C57EA400E415F820AFC80FF925FE70641E813DF330F24AFD51D51C4E6D + C7A0A62D9C8E1385DEB124F44E18D03A9C8C966113DA46CCE89AB0A379408FE2 + FA50FA9C3EA8EBD2606A2907F367F331B39289C50B7958BE9487C57359983D65 + C7D49C1923137A74F6C4A2B39B3F67027A67F4985A4EC3CAB562ACBD5085A5EB + A5983B9F8F93970BB078A51033AB59183F958A897907FAE9D8CD6D51A8A90944 + 51A1272A4BE998157EA829F14115A9BCC41715657E282FF7A5327E686909455D + 9D3F323377C1627902A525873132A8C1FCB409674EA6E0EC623AA6461231D81B + 85D1E178CCCC9A70762D0BCFDD2AC1ADE72BB1762E0F23233AB47744A1BD8DCE + B92311DD5D3AB435C7A3AE2A0C0D3561648BC5E8A801A7A9AD73E773B1722E1B + 2717D3303367C3C898017D43068C4DD2679F4AC5D8840DFD43F4195A23D1D014 + 81E1513DD5CBC4DAF9025CB95A8A8B970B716AC9811373064C9C48C2E8640229 + 0ED3B349983F6DC4C2A201A7964D585A4AC1A9392BA6C61230321C8EC1E160F4 + 8F0462683A0493271330B69044DFBF15F3E7EC38B162C134D5993F67A2EF3805 + 6BB732B070CE8ADEF138B4F5C6A0A397DA5FB060ED5A2E2E3C578285E53CF40D + 5B31386AC589856C9C984FC7C48C1913B35AF48D84A3B1FD38CAAA8FA0A4EC88 + B25C7813FD3F5AA2505B1748FFA7C3C82D388ABCFC00A467F822238341AA37F2 + 0BBCE93FE6859C8223947F04E502FCFAC091BD0FD6CC3D48CF3F82FC4A3FFACF + 45A1B93B11EDF41D758F19D149DF6BDBA0160D5DD1748D84A082977FE6DF9F7E + 8BAA560DEA3AB568EA35A2A1C748D79B9EAE2F235D436654B71B50D69484DC8A + 28E1B99F921F84E2BA44FA1F5BD1396445DFB80D4353742DF5D2355C4EE79AE7 + 89CCFCE3C862389BE5094786171C6914CF08405A7A103273C25050A2417E712C + B2F222904169B665E68723AD88EE7399FED053793D2F519FEE2520AD858E6B2F + E695138260CCA3FB67AE2F895F14390E337D272905FEC8280BA17B4814F2AAA2 + 9051120C7B9E37AC791E482FA56BB33E1A25744DE7D3BD28BB320AB974DF6539 + CAC261A2EFD7561C8AD4B248A4554623B72E0EB9B5741FAC4F444E5D02B2AB63 + 90511923566848AF8947067DF69CA6646435248934DBD9964165D36B12915A15 + 0F7B79AC90AD2C4684E9B55A64D62521A39AC2EA44E436D1FDACD386C2560BB2 + 1B0CC8AC4D464A45224C4571743EA45C0D3D3FC2109AEC4DCF144F84EA8ED37D + 9EE33E08D3F9917C1196EC0F8D250471B6107A0E0451F908BA776A909C158B44 + 8A33FC655B9C2302D1B6504458431145651900C76744212E2B0AF139D10AD82D + D0088F5E73B511F63A0BAC9526180A93C4FEBDDA82787AFEE961A9A16762A501 + 49FC4CA46723EFC36F60D1B392E1AFB1DE82E41A2392AAF8C52E0B12298CAFD4 + 0B101C951385888C70EA1F44212C2312A169E1082185A647203C331211590C88 + 13A0C9D522B150077D19B55162A067AF11A62A1B9D17F505C89658944CCFBF24 + 4467507FC3118D10F610B64708F1F613C1940EB48621C012067F928F31189EEC + 39AC0F4290858E678F410829D8168D30EAA34450DF495122A5E3294F4379B122 + 0CB444C3C7108EE3DA6078278508301C66E325AB13106EA7B2560D024C31F033 + 46C19BF2BC75E1F0D145E09826107B238E6357880776061FC3F6A023D841F1BD + 91BE38181D20A0EF316D183C92A82CE9A83642405C86B90AD00D174B3633D43D + 141B8CA3F111F0D269E0497D432F6D2C7CA91FEA437D502F7D9CF0C8E597058F + 539F93E54BFD515F9B0EDED4273D6EA2BE23C9CB42E5C9E64B7D536F5322D589 + 5556A2D184E36062348E3ABD77393C46FD4E0F6ADF9345C73846C738AC8B1162 + E8CB00F89881CAD3B10E51D903FA781CA0BA0718FEEAA86C32D949C2C62BDF50 + 7FFA00F7AB1902C746527F97DAA1BEF681F858ECA5F4CEC870B18CB35CCA995F + DEDC1D457D737EC193FAFF0C7DD9DB5759CA394278F872DF9EFBFD7BE2E3B12F + 91FAE45A2DF6262428630127EC65F0ABD6CE8478113218DE46E3826722D81398 + C6014E20BC2D3C125BC322F07468249E0E8EC036F602A65001C18A57EFD6105E + 563A5280630EB9FC16B23D1D1A2EC2C7696CF3188F771808332066084CF12768 + 4CC320F849B102124360F60E0EA1B11295F7660F611E5F05D37889CA9304F865 + 0F5F12435C294E6FF10DC756BF286CF58FC4162ACB50D715003FE14FE7E2178A + 4729FE28E53304162098E35E54C60981154F60450F1F0D103096212C7B043FEE + 1928E28F78F80B897D83556260CB10D8556A00AC78FC7AADC3DF870ED1D89497 + 7B56898FC9CB3F3F78D04B88EBB117302FED7CFF3E1AA71FA431FB8183B86FEF + 3ED2413CB09FC6E9FCF235E54BF8CBFA358DB5792CFEE01ECADBAB84ECF9AB06 + C012FEAEEFF1BB4301BE12FA4A71995F32FCDD43E37B270056EF03FC10EBD943 + 78D02906BAACFB77D079EE3840A1027F9597E415E0AB16435F09758577EFD6FD + B8E7E903420C8119FEDEB565CF3A00762791A782BD1C577B024B6702F55C8EEB + 5C0F4B3D7FC5929EBE1CBAE6492FDF75EF5F0AD9A1E21ECA6371199EEB12CB44 + 3FB21D3F7FEC594A2BFB018B6DD41EA5B8739E4EAEA627BD80C5BC9D13FC4AD0 + 2BF2DC405E99EFCEFEF35F3F4DDA223C5C7F4EED71BBEB5097E43A7FB80E7A7F + FD9452D7A5ECEDF60076B5A9D3B7951302B3D6813049CCD3F2DCAF73FED7752E + 574AD615F528FDB3871EC72F7E457AE831E105CC73E3C2CE00973D74EFA3CF41 + BAFF9E6D78E8AEA7F1E02FB6E0DEBB1800535BF750B97B956D141FB8EF493C70 + 3FD9EFDBB0B3783E5D78D10AF84BF6BB581B10984305002BC058F114FEB5531B + F3E7EB73EB821728716E5BCA35ADB60B06C073FDBCFCF426B1FD61C12C649B77 + 161F5B6AC32EE7F7455C757EAE12E7E1D4F7F31E5E6733EAE3489622F25CF2D7 + F37E84DCB6F923E5964349E6E3C6F64372CBC2FE2B9CEE9FE17CB2EC9DE4AEDE + 7FA7FE2BC753C3D77FA57E1A00E6B83A7D27FD0F7CD1FF02DD11E4BA48967595 + BBB22C712171197931C93726541799C87373912ADAB8C8D517FA3F7BE1BB6BE3 + 7612374FB907B0B8B837EACA3766D4ED093B97113742671B2EF93F45EE3C83F9 + 26AF1C9F42D5BEC0F22D1EE5C6AB2C0DAD4ECB7252EA078A2CF74365E403F08E + A27AEAB43BF02B6D42F76F7E98AB01ED26C98E80F361EEAE8EABFD76DAD459D8 + B4EFEF867E213A381B5EBD12ECAAE332AD8EBB0261D6DDDCB973C6D562BB3BF1 + BEC2726FE17BA9B3C7E2F2F7514791ED6A882B3B72D2C669B524E865C97C755D + 16036001739D4BCBF09E23EEF6FA55DB654793A5AE27CAF1BE25BC243285BCB4 + 0DDBEEE2E56E443EB5E17C13522E9D233BD212006F74AC7F5802C63A3BF112D2 + AA3BF5329FEDB263EF2E4FC6E520410C149C83083990908309B18C10C35F211A + A008BB04BC9B971FE2F0816736F6AC91831B1EF08841130DA2E49BB4CA2088F3 + 1598CC711E2C31CC154097BD7A690075FF4E6A6BE77EA1FB765059A71E6048BC + 8B0658BBA90E87AABA2CF6F895C0572D097F3701605EBE792F0D0C29FDC01EB2 + 3BDF1E6608CC30F7494F7F3CE545835E1A946EF2FAA5C12A2F63A51EF0AAF5D0 + 011AD0EEF7100355F176B24A3CC0154B5D51FB2C8ECB65ADD45A2F7794C12ED9 + 3C7CD725D38F7AFAD1A09E971353C469CE7BD883F2BDD8739706F0143EECC94B + 3E2B6F9BAF4B0587953DA802445CDAA54D2C5B46214BEE5FA5CE5F97AFB2DCB3 + 84C8ECF5BB55005D657F2D01879D7605042B792CDE8F6B7B68389EA550ECCF15 + 1E81AD5191D8BE0E7A35D8C96FF947F3F26E3122DC159F803D895A11EE88D128 + 4BBC31288EA53C8D06BB28DCA12139278076252608715CA6F72469B157972442 + D6BE641D0E18F4D84F3A6836E088D58883A6644A6B71C09484430C7D6D7A1C20 + 1DB426E3B03549C05F3500E6A5A179B968F6043E6888C72163228E9AB46249E8 + 8349B1D81B1F293C81A576C547D039860BED4A88C03E5D0CD58DC321731C8E98 + 1370D49A886336AD90A7430BEFF464F8661A847C324D389E4EF10C137CB38CF0 + C936C23797D2F92604145911526C4770698A5060B14D28A8C42E1456EA40544D + 06340D3988ADCD4464652AA2ABD3114F694D5D16C27899685234D9A30424A63A + 255604179A84773187A114869598105E6A4664991551A4084A87151910596244 + 48BE0E61053AC495599058E340426D2A12EBD2A0AD4F178AAF4B415C9D1DC9AD + 59B07417C0DC9D87E4CE1C68DB339148D2766743D79B03635F3EEC63E5483B51 + 0BC77435AC13E5B04E56C0365D8394B906A4CE35C2325503D35805CC2365300D + 97C0325402FB48291C94B60D16C3DC9B07734F368C5D59D0B5D27934DA91506F + A3F3B021B9394DC067434B2A8C24535B2A921B95A5A84D54CED19E8654122F17 + 6DA93642579C80B89C2844A58620D4E40BBFC4A3F08AD90BEFD8FD084C3A8610 + 8317C90341FA6308347A20D4E68DF0145F44A40720362F14FA720DECF5C9B0D4 + 6B61AC4F84AD2109A66A0DF415D130D7C4C146366B7D026C8D5AA43725C35C11 + 0B6D4E10E2D2FC909819047D36B591158A689327FC35FBE1CB4B3EC7D1F16376 + C22F7E0F02750710693D86A4EC4001201858A4D725C050128AF85C5FC4313CA9 + 0AA3CF138BF4F644E4F41891DB6F43569F1519BD16640FA7227F321B4533F928 + 9A2D42E94239CA4E57A17AA511756BADA85D6B41CDD966D49E6B215B13AA979B + D178B60D1D977BD17BAD175D973AD1B8548BDCC174981BB44299DD16140C3850 + 3C9481EAC97C34CF97A1ED64051A4F14A2623813396D66A4D4C6C151138994AA + 10A4568523AB261A39D5D1C8AC0C477A69B00039F53D06547526A2A02E1445B5 + A128AD0F476973248A9AC29159E58FDCDA609477C4A2A62F110DC33A348D1B51 + 3BA243795F2C4A3A234891A81D4C42EB940DD5437A94746950D11D87EA9E44AA + 1787F2760D1A078DE89B4E43EF542ADA472C68EED7A3A63D1E55CDD4466D18B2 + 0ABD61CF3A8CB47C4FE496FB238F9456E085B4A2E3C8A6787A890F322AFC51D6 + 168BFA413DEA468C6898B0A163361DC3CBF9185D2D42D7521E1A1772D072BA10 + A397AA31FF422B666E3662E87C093A4F67A2EF4C0EE66E54E1F48B75987BAE14 + 33570A31B19683D133E91898B7A26B528FF6B124B40C25A073428FAEC574F45F + 2CC2F48B0D587EA70B673FE8C3CABBDD587CB3138B6F7562F9CD0E2CBCD88889 + ABE598385F88FE9369A81F88437EAD2F52F3F623256B0F32F3F6A1B0E4081A1A + 83D1D51587E6960854D605A0BA360025C59E48363C8180C09F213CFC5E64E7EC + 475B7B14068774189D346168C2885EFA9E87A74D38B1948133749CE7DE68C3CB + EFF7E2A58FFA71EDDD4EACBE5C8715D2D9D79B70F6B5669C7985F472234EDDA8 + C1F4A5324C5F29C3F2CB2DB8FED1306E7D368E6B1F0CE1C25BBD38F75A27CEBC + D84E795D587DBD1717DE19C4E50F47B0F6762F4EDEA8C7ECD52AAC525BD7E8F3 + 5E7FA703CF916EBCDB85ABAF3763ED850AAC3C978FF9B554CC9EB1E1148573AB + 768C9F4CC2D4691D4EAFA563F9523ECED0F77BEE4611D65E2CC6EA4B4538F74A + 19B55946E75680C55B6538F35205969FAFC4CAF33558BC5181B96BC574DC623A + 67FA5D2E1760E25209C6E933CFBFD68A2BDF4CE2F93F9CC4D9F707D143BF7717 + FD9E276FF167E7CF5A8153D74A3072CA8E86BE2814D67921AFE2102A5A02D0D8 + 1D8D5AFA7F963506D0FF8AFF5B5E286F0C4735FDF76AE9F7A8E9A6FF75572C8A + 1A82E97AF0444E950F4A5BC350D11985E2F608E4D2EF9645BF575E5308AA0613 + D139EF40DF22FD9796B24438B89C8DA1C50C749E30A37E381195FDF49F66883E + 6D47C74216DA498D2752513B6947D5981555A356D44D39D0349B81FA69BA767B + 13914BD747055D374D9356348D5AD03A4675271CA8A76BA4AC350665CDA4160D + F26B22E83AE0E5E98F416B3B087B5E00B22A22E9FAA06B90EE6D552D7ABA7613 + E91AD6A080AEF18CD230D80A8360C8F623F9C3941F047371282C65E1B09487D2 + 3D300CA6320A29CE2FC7A4D6C720AD2E1AE9B55148A9245B7910326BC351C27B + A577EB50D4164FF78F10B19FB9A3321879CDF1C86D25B52720AF3D09D9A4CC36 + 2D521B35744F8F8489EE37768A675099AC8E4414F69951D06341664BA2588A3F + B3538F3CBA3FB2F2076CC8ED3121A35DA7A82319A96D49486F4B4666BB01196D + 06A4B52623A5494BF7F2782107C5531B1361AFA3741DDDE379D5878604B2E928 + 4C82AD260126BA57EB8BA3A0E5BDE7330211E9F0464CBA3FE2B3821147E92887 + 3FDDD77D1165F181C611086D2695A3675004C563B32395259A791FDEAC28C465 + 46501BE1884E0B45642A293D1C51A418CAE33D7B7969668300B67A686B28DE6C + 474A7716527AB260A6E79FB92D1DF6CE2CD83A389D0E436B9A784E9ADB3260EF + CA8195ECC94D0EF1C25652530A121BEC88AFB522B6CA8C982A1334B516EA4B58 + 91586F457C4532A28BE210531C8FD892444415C52324371AFE1961F07604C38F + CE2BBC80ECA57AC4961B11576986A6C2046DAD1DC6E64CE81BD39154E780AE81 + FA0E146A2A2D882AA13E46B15E28AC381921854908CA4FA4767508CAE16D3092 + 703C2D0E472D91F0B445C297E201995A0439B7B7E097DA228BCD082F3422345F + 8F60AE93958840DEF682E49F9AA06CB361E66D2E34E2C5B7B06C3A5E9E0911B9 + 468465E911C0656C31F0E42D384C51F036C5C043CF2FF205638FC60FBB627CB1 + 27D61F0712827124291C87741138A00D2745503C1247F4D138AC8FC1A164EAF3 + F1F2CAC91A1CA57EA200B10C64A92F7884ED3A6559E5A30C7BA9AF79DC4C9FCD + A2832785DED417F54E35C1CB61C0F1143DF5115906785078CCAE732A199E9CC7 + FD535EA98601AE2981FAAA5A1CA53CD96F651DA2360F515FF730F76FC9CEE141 + B21DA03EEC015ED1865F7EB468C5CB8F87ADD4173652DF591787037A5E0187F2 + E40B934249D8A7A77C2DF5B9E36329D4887207F449D89B94404AC46EEE93531F + 7D37F7C949DB79C51EEEB3C76B44FF9D9771661B7BF2CABEBB92CF2F6F26507F + 998EE1ECC3CBFEFD6EAD96C204017CB9FFBF5B9B28F20400A63102B7F92C8D27 + 58CF44C7605B5434B6464639417014B650F8348D419E8E88C49688083C191E8E + 2D91648BA23C0AB74444515EF4BAB68447E1C9D0083C11122EF4646898D05321 + 8A97B0F00E0E60EF5C16C5FD3720F0FAB2CDCEA59CD9CEE27D7BC5DEBD4E50CC + CB3AB32730835FB15434E53DEEA386BFC1140F2185515C81C02C06C40FF37EBF + EB9EC054C733108F1C5324A12E835EB92CB41AF64A9BDC3398C56579D966B9E7 + 2FC7659A412F4BE64910CC69598EED4A9E52FE81039E02004B90CC636B1E43CB + F1F3430714E8ABD8786CED8907F679083DB4CF53ECF72BC1AF1AFE0A00EC84BA + B7F3FC558BCB48582C967A56D985761C16F31B0F9004FC25496FDE75F1CBEEBC + 521AC5EF57415F57F07B3B713E035E0980659CA1B094B03961AF7ADE86A59ECF + 514BCC39B9CCF788B4739EC855EA3924B56D5DAAE59D655C025F86BFAE736C72 + 0E4DBDFCB3AB649E3B71BE9CCBBBAD1EDE4ADA22F6B51522DB66671316035ECE + DB829FFDEAA9CDFA25D90414569C587EF6D0D3F8859827DD0C7F37E64F5580D7 + 8D7E4E926D4B6FE2F5ADF954F3B3628EF63673BBAE76390F2CF6EC754AEE0F2C + F2799E57C4E9B8ECC873DF930AC4E57968A7D4C059405E95D473E19CBE9BCAC8 + B9F89FDF4DF17B14502CF3655C32830DB99F6F9792C77097B7591B5CE04E521F + DB5DFEF7F5C3C770C7385CDB972C64939CC0D75577AC731BFDB3E5D5FA1E7F72 + B29E75E6731BBB3A8F25B80FC75D19982B08BD5D5AADDBE47D8FB1B9E4FF68A9 + EBBB8BDF4EEAFA327E279B94BA8D3B490D5FFF95FAE900F87E67A886BDEEF44F + 7CE8FF07E97B7FB03B48967595BBB2AC4DF90C7F6F0380599B2F52BEB8A536E7 + C99BC53F73D3F867CA6F02C07CC352D575BD918AF2AAF87A1B4EDB4F911AFC4A + F131657CA3ACFA46ADC8F5C1A14EBB7BA8DCEE41C37696FAE127002EE5B9825C + 99DE94EF0CDD959136211700ECAAF5873AEB3600F89FD57A07832401B01A028B + CE88E8D06C06B912DAAAD36A9B04C0529CE63C86B6EA726AB14D8AD3B20D5997 + E12FD7671B03604E33C07585BD322D3B8CEECAC83C2969E34EE42F1802DFAE63 + EAB4A9F364BE5AB2AC04C6F73EB153D93B98A1EF63CFE01ECA9340F81EB2F35B + 92BF60A93ACF0C86D56F59AA3BD71C77CD5F07B7CE8EB84CABEBB896712DBB29 + 5F3508500F1E24D4957205BD9CBE7F1B7BF3D28086972E72DA391403231AECC8 + 410C87D2C60327294E7F5FCA5BB352FC26ED03BBC84E9210588AE1AF04C04A9C + 43A52C4B025EA94D4B3FBBD82404167609779D920098C5DEBCEC21CC121EC24E + 00EC0E02CBC12B0F6A1F12E51410FC10A7C9BEBE641549C605B42531EC554BDA + 5912F84A31F0E57D9EA478BFA7C78ED3B97AFAAE035F963ACD930CACC7BC1916 + FB501B1BB09743E9212CED0C76651E8732FE24859CC770788B5F909084BF526A + C8CB7106BFC20B986DECED1B122624F7DFDA111E895D115142DBC223B0859778 + 8E8DC18EF878EC888B174BB82993363C319480DD0949D893A8A3504BE9386589 + 3712C7190AEF4A60294BC03DAB61AF0086BE89D8A3D50A70BC272909FB929385 + 78F28827892404DE4BDA6FD4E390C52460F0BEE4241C32EB71D866C061BB0107 + ACC93860D1E108C50F8989323D8EA59870CC6E14936287CD64E34934B3564878 + 039B9370C44469D5BEC0EC19BC4F1F8BBD068DD03ED201533C0E5A182827519B + C9F04C4D86579A7E3DF4C934C32FDB06DF2CEB26F9655BE09B63824F1EE5176C + 967FA145C0DF903207C22AD2105E992E14519586D8FA6C4456A723B8D446B223 + 82E29CE6BCC84A875054552A222AD85B3805616536A1D0522B22CA2520B623BC + CC42692B222B6C42D155294A589982F8EA34C4D7A4218E145F9B4E613A34D50E + C454D9A0A963189B81E4B65C183BF360EACE87A13B0F7A52524F1E92FB0B601A + 2A8175B402B6896A58C72B611A2D8361B41286F16A98A7EA6099AE8771A20686 + B14A18C72A4419FB0489EAA4519DF4A95A38C89E32520A3BB565EB2F84A92707 + C9ED19D0B5F2B1B36068CF82B13D13E6CE6CD8BA7360EDCC84B523038ECE2CA4 + 533ABD23138E66072CD566E88A1285F7544C460462784F454728424D7E084A3E + 8E10BD3702759E08D67B2226350409D9D1D06485233A3D049ADC48E82BB5B037 + 9990D66683A3D5025B9B09B61623529A8DC8683723B7270579BD0EE474DB91D5 + 69454E9B1569F5C9B095C7C12A96108D83AD88F7730C45B8C10301090711AC3B + 8250FD310451186EF6427C4600CC25D1C8ACD721A3D9802C6A37A3558F94A604 + A4B7EB50C07BF84EA5A1622613E5B359289BC921E5A16CAE10A5274B50B65886 + 8A954A549CA946C5D946545F6843EDE56EB4DC1842EBAD61D45FEB43FDD55E34 + 5EEB47C3A51ED4AF75A2FE6C079ACE77A3F3EA00DA2E76A37CA10AE97DE948E9 + B022B3973ED3403AF286D2513C9E8DF2E97C54CF15A369A9124D8B956858A840 + F574114A473291DBA147469306392D89C8A88EA6CF791CFAECE3286E8C47D758 + 067A273250D7AD4769732C6A3BB468EED3A3A15F878ACE38E43586A3A0391275 + BCC4F38C030D936614F56A90DD1E8EFCCE7014F7C6A0B42F1E65FD8928271576 + C520AF9DA170344A295ED2198BCABE44B48C5BD039932AC2EA3EAD68BBA03E0C + 96DC2330641E80A3C80BD915FE28698C4055BB861487EACE78B40E9BD0379B8E + BEB90C8CAE1460FE5A2D4EDD6AC2CC730D98BDD586A557FB70F6AD612CBED68F + 132F7761F6CD419C7E771C173E9B259DC0D98FC6B0FCDE0096DEE9C6C2AB4D98 + 7DB116275F69C0A957EB3073A3147D2BE9689E31A266381E0DA35AF49EB461E2 + 6C0E16AE5750F9162CBCD38FC58F46B1F4D108963E1CC2994FC6B0F6C5342E7E + 39898B5F8C63EDE3119C7EB3130B2F3460E65A258657B3D03665446D77142A9A + 8350DD1A84E6EE7074F76BD04BDF4357771CFAFAB5181AD1A3BD230639F98791 + A47F1206E3D3C82F3886968E680C4F5930B99086E1193B8648D3CB993875A918 + ABB76A71ED8D76BCF07E1F9EFF7408D73F1DC4A50F7A70E5D37EDCFC660437BE + 1EC6F5AF8671EBDB71BCF0DB698A8FE3FCC743B8F0E918AE7F3B8BE77F7B1A2F + FD7609CF7F771AD73E9FC1F9F7E9FC3F98C0A5CFA671F58B195CFD6A0657BE9C + C265FA8CCF7D3E8E977F3787B7FF7A0A6FFF690EAF7E3B8997BE18C30D3ADEDA + 2B353873AB04A7AFE662E94A2EAEBD5685E7DEACC6B95BB9985FB36062D984A9 + B3A93875351F675E2CC7B9D7AB70E1DD463CF7690F6E7DD14BEAC1CD2FFA71ED + E35E5C7CB7031719AEBFDE81D32F35E12489C1FA858FFB70E5AB315CFA661267 + E89CCE7DB7801B7F5DC18D3F9DC6C2DB5D987CB181CA0CE085EFA6F0FC9723B8 + FE612F966E55636C250B5DD346B40CC7A2BE2F8AFED341C8ADF14261831F4A5B + 4250D313859E13664CACE6E2C4C562FA4FE5A063DA8AE631035A272CE83AE140 + DFE92C8CAEE663EC7C0986D78AD0BB9C85D6392B5A176CE85CCA44EF995CF452 + FED0F962FA1F5661EEB9728CD0EFDE3E6B41D38C15EDFCB2C15A21062F9563E0 + 7225FA2F96A3E77C295A56F250B7908EDAF934BA4633D175361F1DCB3968984F + 41ED0913AAC6F5A8E84F426947228A5BE350D21E8F1ABA16EB07CD28A3EB32A3 + 3A148EB200D84BFC612DF283BD3810699561C8AA8A4061BD06956D3A94D2359D + 5F138DBC9A48E1C16B2F0E466A6524290669BC447C531272DAF5C86C4D429A58 + 0E5F8B9C4E030AFA2C281AB0A2A8DF82B221DEA7DC849CD60414756A513968A0 + EBC344F70033AA070C28A4F3CB6ED620B78DF2FB0C281B31A364D48AC2410B0A + 872924657425C141F9695DC948A736D2A84E4E9F5180DEC2A114140D3B903F6A + 47E1441A0AC7D350329589D2E92C148DA50915908DEDF90329C8EA3623AF9FCA + 0EA58A30B3CB44F7791D52DB9291D169A4FB7E32ACF5BCD24424922BA360A98B + A37BB3813E9705996D460184ADD5F102065BAA34B0D726C05E9D006B451CDDD3 + 63909C1B8AC474DE822004DACC10C4A587223637029AFC2868F2A2109FCB9EBD + 9188CB8E10125EBEF9B1482C8C13E0379EC2F89244E8EB4C30B7D0F38C9E81D6 + EE2C3806F2913A548894C17CD88648C385B00F16C2DA9B47CFE54CF1ACD4D333 + D2D6930B477F113D2BB3115F9F825886BEA4B87A3B3DC75391D0904A7607C5D3 + A06BCB8489EA199A52C49EFEDA3A7A7636589054CFA0588F88920484152720A2 + 4C477D0133E2EA52443D6D53BA6827A9998ED9962D425D0BB545FD0343673E9D + 4B2ED9B2D695D89441E7C07D15AB78498D5F50E3154E020A8CF0CDD689955178 + CB8B085E11A58CFA2B65D4EFA1FE4F7405D529A33E8B4A51DC072AB22234DF88 + 80AC24F86768053066F12A28BC2586D81623D788C04C6A3B83CA64E9E09B9E08 + 6FDEA6C3160F1F7B028E99627050178E034961386288122BB9F08A2EC7F8A53E + EAE371E8C132515F8FFA84BC8F2EC3DFA3C644D1473C4AF230905D172B965466 + 1D624F5DF6CEA5BEE771EA677ADBF5F04A31E0789A193E1956111E4BD153BF34 + 191EA946786558E09966A23EAB8EFAB2D41FE515692864E0CBF99C7794CA33CC + DD4FE7B1CF188F3D74CCBD067EA991FABCD4AF3D6437E3708A9942935346D1E7 + 6510CCFD5E0E19021FA1F3396AA37CEA27EF3792CDC4ABE1701EF5A50DD457D7 + 25607FB216878C64E7972CF5C93864328AB8EC7F4B802B5FCE649BEC97B39D61 + 2EF7E7A538AD2E2B01B104C0DBD92BD8D9EF97E96762690CA18915A15A6A18FC + 4C248943677A0BA59F8E8A11E238035F5700FC5458A4D026101C1C862783C2F0 + 44207B029302D84337044FF2D2D2CE25A637BC8195D59478DFE027693CC55EC1 + 6A00FC98738CF7946F88D0133C8E233DEA43F9ECEDEBA37803B3C452D0EC091C + 108647A89D478ED3B8CF9BDA63AF6212A71FF10CC0C3ECE14BE1A35ED48E4A32 + 4FE64B49DBAF8FF18BC67EEB205778043BC12E43DD0DF8ABC4D54058DAD5A058 + 84DF1B4B537B871418CCC0578EABBF0780F77A0800AC16DBD4007813C475EA01 + 55BEDA13783DDF8D9DBD8025CC95F3191C97731DEBF16D8A1806ABED3F048037 + F2B89C0280D5105882600ED5F33652EA791DF50BFE1CAE3B1EA820B0AB3302CF + 39490FE0DB49405E06BE4F3EAB844EE0CBE2E59E655CCC91B1330749CEA5C979 + 359E479350D755B7B3ABE7F8D4DA3407C89EBF12FEBA01C08AC8263C859F5AD7 + CF7EF5A4220180D9C984EBD1E7F8E556DCE59C27554B0D796F67677D0F00ABC0 + AF5A3C5F2BE77F5DE77105DC65D0AB9E2366F132D02EF3BA4ADC599EF7E165F1 + FEBD627F5EA7B8AE0A00CB34EF2D2C57D4E479783927CE9EC51B2C40CE9D735C + EAFBF3EB725E5DE1084A5C9DFFCF497DACDB4B39962277F93F45AE8CC3B57D35 + 0B5917B314370058DD8E6BDEBF4AEEF8933BA9C1AF94DB7277B930305708EA0E + 8A4A9B6B9E8B7D136763B9E4FF68A9DB977137C773ABDB95FF31F5D575DD490D + 5FFF95FAD70360D68FF8C0FF0FD32648FB0352FF19D5725796B5299F2FA07500 + 4C17BCEB45B4E922E59B8153E202A3BAAA1B08DF2C5C6F689BEB6FD60F9673BD + 39C9B4AA9EAB36D5772377757E8CD4E0F77652CA2A0F02D787087F67EA3C57A9 + CBDE49B2BC04C06C13609745690973D570773DCF19CA7C097CD7CBC93CF9A075 + CAF521EF9AAF96BA93F093243B07CE0E8304C00A04760F8025A455E77128A1AD + 6BDEDDD4D1920058DD2192BA9B3A5CAC7B1EDEBA5E4E2E332DEBB238CEE5655C + 76EED49097E3B253C871EEB049A9F3BE2715CC557746E5F231D2AECE9352D7B9 + 5B65179D5C86C08F49E0CBF0F759DC4B1D6306C3F772E7FA31A563CD921D6D09 + 6D5D21ADDAE64EAE75D569751BEA506A93D7B07300C0E2818384BCD2E357825D + 39A8906524007695A8E76630A31EECC881108BA1AF1A08BB7A048BA5948407B0 + 22097D25F87D68CF212105026FC05F5700CC8057C25E35F065C93C29B6FDFA00 + EFDFAB88A12F7BFE32086620CC805842E2876800FAE07E05026F1EB42A62BB22 + 1EAC1E1352036029B994D5AF5CC0AF84BF6AAF5F2977F077DD0398C4B0F7D72A + 0F6029F116B980BF34E0679B0BEC65715C4A7AF8BA0260097E9F62AF5F9204C0 + 1C1769E9E1CBE03730184FFB2BDA1A188A6DC16102FE3E1DC2FBF6860BE8BBDD + B927D7F67086BF31623FAF6D5191D81219A1BCADEF84B72C9EC0E1F4CE84444A + 6B493CB1C36FFE2B133CAC5D942700AF96278512D6EBAD03E0A424ECD5E9D6E1 + AF1A00EF6528ACD7612F4902E00366030E18D91BD882A3760B0EDBC9663560BF + 39198728EFA099CB197034C54CF9943629CB461FB51970CCAE573C2B2C491492 + 8D27DF8C0C81296DD0C2C3AC786730E83DCA9E1A36C51BC38327F6520D389E6E + 824FA6655D0AE8B5290098E42D6C66E105CCF2CE32C037C700BF021302584576 + 2186BFEC051C5A9EEA04BF1922CE403892E21C4A482CCB45D56489E5A1434B19 + F8DA05F40D2FB7AF8B61308361098719088797DB1059A980E2E8EA3468EA3291 + D0908384BA2C686AD2105B938AB8DA3424D6678A90276D35F50EC435A421BE39 + 13DAD66C24B5E740D7950F5D4F01B4BDF9D0F6E541D75F00FD60098CC3A5300E + 15C3305402DD5019742395D08FD5C0385107D3442D4C9335B090EC53B5B04F56 + C3325A06EB68391CE355481DAF44DA641532C89E365E01DB50112CFDF9B052DB + 96BE7C18BAB28478C23B65A010A983C570F417C2D69D0B2B83E16687F0064EAE + 3623B14C87C49244E8CA9260AA34908C3094EAA0E5C9F51C06BEBCAF222FEF6C + 444ABD552CADA9AF4C82A1460F5BAB0D29ED2948E94881BDC3064BAB0986463D + 4CCD7A383A19963A90D5E3407AA70DE9ED5664B4599145CA6DB723A785D20D06 + 98CBE21093C67B127B0945A4F8213E2B0CDABC483A0FDE4F3819F95D29281BCE + 46312997E1EB601A8A46B350395B88BAD315A85B2C47CD5205AA17CB503A5F88 + 92F922542E57A3EE7C0BAAD79A504961EDA54ED45DED45C3730368BC398CF697 + A7D0F1CA09343F3F8EA65B63641B43EBCD71345F1B41ED857ED49CEB41ED79D2 + 5A272A969B50385785BC8912E48F17A1E444294A67CB503E5F4EAA40C54225EA + CFD4A3E3422B3A2F76A0E55C0BEA976A514BE757359E81F2E134E47518602C0D + 4652811FF29A12D0389482EA5E13CA3B9250DB6742C76426064F15A2FF741E5A + 4FA4A361D281460ABB960AD07FB614ADA772513DED40ED540AEA67D385C76DFD + 4C266AA6D25033998AF2112B4A87CC28ECD723BF5747C7B4A2793617EDD466DB + C90234CC64A17CC88A82CE64E4B66991521B4EDF6D048A7AF5A81A4941DD783A + 7A964A3176B10EF3B7DAB1FC5A3F565E1FC099B74670EE83499CFF784668EDD3 + 595CF8F234AE7CB782CBA4D5CF17B0FCD902D6BE39838BDFADE1C2B76771F6B3 + 45AC7E368F8BDF9CA232A7293D8585B77A71E29536CCBFD68ED9171B3170A118 + 6D8B99689E4BA5CF5788932F3563EDDD3E5CFA78141719927E791267B8EDCF67 + B1F2C52CB57F0A977EB7826B7F3C836BBF5BC2E56F4F61ED8B13743ED3B844ED + 5FF8600CCB6F7463E6461DC62E946274AD1053178B3045E1C0E96CF49FCCC1CC + 850AAC3EDF82E59B4D38B15686BEF94C744DA7A29BBECFB1B3C53879AB11675E + 69C7D24B6D5878A199D48885975A30FF722B4EBFDA8655FA0C6B1F8DE2EC2713 + 58A1E39DFB740A17BF9AA573A5EFE4AB393AA7D3B8FADB155CF9CD0A2E7DBB4C + DFD12AAEFD760D37FF78192FFCE53A8557C8B686739F2D63E5E3D358FDE414CE + 7E7C0A2B1FCE63F9A3199CFB648E3ECB495CFB6A09CFFF6E15AFFEE9AC00C737 + 3E9FC3C50F46B1F42AFD2E743E8B37EB3177B512E75F6BC3F31F0DE0FABB5D98 + BF568A81C5548CAFE5E2D4CD4A9C7EB106F32F56E3D4AB8D587EBB1DE73FEAC5 + 735F8FE1C6D753B8F4D108CEBED38795B7BAB1F226E9ED5E9C796F1817E87BBC + FADD3C9EFBE332AEFE61056BDF2EE134FD8E2B5FCFE3F21F96E9B79EC1F46B3D + 587C6F10D7BF9EC58DAFE87B7FBF0F675E6FC3A917EA317DA51CC32B39681CD3 + 23A721188E4A6F1477C4A065D286B6991474CCA66168B500D3576B307EB1123D + 8B79E85F29C4896BFC5240234EDC6CC438E50D5FAAC4E4730D3871AB1963D76A + D07FBE149DAB85E8582D46DF85724CDC6CC6C957BBE9F3B562EC7225FAE8FAE8 + 5A2D43CFF92A0C5DADC7C8CD368CBED08531D2D0CD76F45E6B46E7E53AB4AD55 + A279B5049D6B55E8A774D7A52A342CE7A3FA045D431369A81E71A0B2DF8CD23E + 23CA07ACA8A46BB4B8C784ECD6446434C7219397B1AF8982A53484EE5DB128EE + D4A3A84D8782C604E4D46990531F8782A64414342721BF351925DD561475DB90 + DFC990D781B2D10C14D1BD20A797BD6F6D281E4947D95436290B65749FA899C9 + A66B9CAED35107AAC7D2503946E7336217D766F3147DAFD379A819CF44C9A015 + E564AB9DCB45FDE942D42C14A17C2E0F15942E9ECE4421DD4B8AA99DFCD154BA + 075B90D94FC71B4945C5897C2A5B8AF205BA47CEE6A1703A9BEE6379A83A598C + EA8512542D14A36CAE00E5942E9B29A0FAD4CE440ECA4F14A0748ACAD33D3787 + EEBDD9FD0E6A3305E93D36A4769A616F37920C48EBB6207F301D2523592819CD + 11F7EBBCEE5464B4D2B3A0C38AD456332C743FB7D5EA61AF4D86B93C01C905D1 + 88CB0A47546A206233C3A12D4B84969E2F49BC9D418D01FAAA642414D3B3A848 + 83E4F22418AB0D305419905C654462A51E7115C9D0D173C9D29549CFB942648C + 952373B20219E3E5481DE1953348A3A5B0D1F3D64ACF426B7F9E78469A7AF260 + ECCE26F1CB53F4AC6EC9A067770A62EA6C48684D87B1271FE67E7AAE0E14C3D4 + 5B2064A567B8A52B07067A861A3AB360EAA2FA9DD9486EE317AF3261A2E7AB89 + 57FE68CF82BE2317E6BE12217D5701741D792234741752DB45EB7996FE522173 + 5F19E51553992268DBF210D79489886AEA7F54A722B63E0BB10D5988AECB4014 + BF74569F8DC4E67C24361640539B83981AB257A539FB29BCF24916121AA9AF42 + E53455E988A47E5048811981B90604E79B28342228DF80C03C030272F464B390 + CC08293423B8C8027FEA77F9A63130D62328DB0CFF4C03F5E11271CC1AA76CE1 + 91A2ACDEE2936EC47186B30E83F0CAF5E0FE9F8DFA7ED43764B0CB2F0BB287AE + 8773296606C10C7EA5180EF3CB849ED664017FD9DBD78B3D80491E94E63E278B + E35E69D4376408ECCC63793A4C741ED49FCCB0C29BC410988131436045CA2A37 + 87A82F7B38C5882354FE88C34CA2BE6F0AC364A3E8EF32E0658FDF23D4173E66 + 3753DB16111EA63C5E35C755FB93A95F6CD0E3B089EA537F9AE347CCD41E89FB + E0B24FCEA104BF2C7E2173BF5E2FFAEFDCF76758AB06C0121873DF9ECBAC8F03 + 282D6DB2FF2F2541F033B1DC9EF282A84847C7AC8B5F34DD1E1B4F63108D027F + 2363491A6C8DA63082141E8B2D6131782A34FA7B7A22240A8F0747E2C9A008A7 + C20508160A0CC716B26DA1F85381940E604F5DF6DE0DC4A30C7A490C7DA50730 + 4B8CDBC4584FF102DE00C00AEC5503600ED9F39797827E824386BEDE1B4B498B + A5A0D9CBD7097B655C425E5700AC1683DF5FB1172F85D20B98752700CC70572E + F12CA1AFAB4D8E975DC7D10F1D60BB12AAE1AF04C00C7A0500DE43DABB017F1F + DC7D5478010BA920B084BABC04F4BD3B0EE1BE9D8771EF4E8A6F3F887B19D2F2 + CBE82EE584789E82E72B38DF29B9AA994CCBB90CF51C084BE6AB213087F76CDD + 2B24E74924EC95F9D226F3A584CD393FA39EC79192E0572D6957436096DCA24C + CE2331FC15CB373B21B05CBD4E6D178097A1EF13CF2830D8098019FE4A4900BC + EEBDAB9A3363ADCFC33DACCCE1C9B9BD1F1297752FCEA7B604D8DD829F094990 + CBC7508BCB3EAD780A0B00CC9EC04E002C40AD328F28E45C025ACE5FCAF94C09 + 7865DAD57E5BB901C03C47AB9EEFE5B43A4F6ABD0CCF2B4B782B80AF530F39C3 + 75F0FBA8531B6D0B715DD5FCB4B039E7A99539F09F3A9FBE793E7E031EFF94B6 + D4E2767F9C5C01AD3BC9326AB92BC7FA21BEA1662032EE2AD73A772AFB4F4BCD + 72DC689D39DD06E8B224F05DE7564E76A5AEB39EEF04C0776261FF945CE0E91D + 399B1AB6DE4EEEEAFD77C9DDF1D47257472D357CFD57EAFF0260F7BAE39FCB45 + B2ACABDC9565FDE21EBA5864BEBC907EC1170F5DF0AA8B4D5C4CEB1728DF0836 + B49EAF82B4EE6E1EAC8D3636EBC794B99DD475D4EDFC505BAE657FACDC01DFF5 + 35FE9D69E5E6EC5EF241C2DF9DEB0346E6C9726AB8EB2A595E96616D7A4092D6 + C12E955F974BBEBA9CAB5C1FC2EA07BB7CB86F7AC8ABE4AE33F0CF6A534781B4 + 018115082B41AE84B99C56DB25EC659B3BBBAC27DB7015E7B124D865713B9CC7 + 1EBFD2762FE753A74BE96C299D30D9F153770425D465BB04BF1CAAF3589BCAAA + F60A517744DD016077121D5A06BE0C7B299476E905AC006005022BDA21968196 + 9D6B57082C40AC4A6ABBBB329C96F5D5369996A0571D57CB750968A91F02C0AE + E0575D963D81D592831E1EECB80E5CA45D8A074CCAB2D09CE67C86BF724FE00D + F8AB06C0EC01FC20EF15EC04C18A9D4305FC4AF82BBD751F72C25D09807FBDDF + 030FD3E09121EE2F9D10580261CEFB152FDBAC02BD2C35105644034D5EB28ADA + 7137685506B31CE7BC0DD02B61AF00BE4EEF5F218AFFEAE8F14DE057EDF12B97 + 76E634DB39AE06BFD22EEB88B4CA0358C25F9E54901EC08ACD97E2FCC6B93F78 + 8F5E8EB34D6AD39EBE14F21EC07209679684C112000B50ECE38FA70282C59E59 + 0C7AB70639F7CFA2F0D9D0083CC3FB6C39F7C6DAC24B3E33F8E5B7EDC323B18D + F238CD4B3DF3126DECFDCB933C3C39C3E2491B65E2874306C13C89C371E58D7F + 06BBCAA48FE21120B5E155A0C05FE1E1ABD7619F3E594C2CF112D03CD1C46519 + FE32F8DD67A03C27003E68E6092F138EDAAD389A62C3E1142B0ED88CD867E632 + DC965668BF498703C2A6050360F688D8240BB5674CC2417D220E1AB4386635E0 + 78AA45786D88093C87119EEB93743C496882778659405F35005603E1630E3DBC + D20C389642C7B325C2C3A1835746B2F0040E2AB422B43815E1A5E908A330AC24 + 85D2768414D9105AA284BC447454650622CA28BF943D72331159918660AA1B4E + E9A0122BFC0B8C08E47D834BD983D88ED07206C4290867CFDF4A65029521307B + 0AC7B0676F5DE6BA8DD3DAC65C24D66723B69AD30E0502D771394AD7A722BACE + 81886A1BA229AE69CE406C4B16E2DA7391D055006D4F21B4BD0548742AA9BF08 + 868112E8074AA11BAE10F0D730560BFD48350C24D368156CE30C806B609BA882 + 79A49CCA1643DF5F0CEB10EF115C85B47156251CA3E5C223D8315A466119CCFD + 8530F715C03E50041B1DC74E756C7D4530756723B9350DC6F634985A53A16FB4 + C348B2B4A422A53D1D691D19486BCF84A3250DB6261BAC8D560AED70509D142E + 4375B84C7A6F36D2FB7390DA9F8D94DE4CD8796FE3AE34D8BB52616AE765A88D + 30B659E0E87120A5D30E73A311E60603B56D47764F3AF2FB3390DB930A3BD975 + E57188C80A414446B0F0FCD296C4C1526B406A9315E9AD29C8EDCB42C97821CA + 674A51395F86525EC679A610C5330528A678F15C098A4F96A172B916D5AB0D28 + 5FAA46E9227BFB36A3F64A0F2A2F76A1E262376A9F1B44DD8D21D4DF1CA6F830 + 1A5E9844D38B27289C42FDAD09B24FA0E1C6381A9E5354777584EA0EA07CAD07 + B5177BD170B51FD56B5D2839558FD2D3F5A83ED382FAB50E34AC75A2F17C17DA + AEF4A38BDAEDBA318CA64B7DA8275BDFF55E8C3CD783763AAFEA193AD7E17464 + 75E8E0E0255FEB6391D11083FCB604D48CD8D13E978B81B31518385789AEC512 + 749D2947DFF96AF45FAE47CFC55A749CAF420FC5876FB463F4855E0CDFEC41E7 + 8506B49CAD423B956B3E53817AAA57BD908FAAF97CB49CA9C2E0B57652277A2F + B5A2EB7C235ACFD4A271B1026DAB7518B8D68181EBAC4EA1BEAB1D987D63024B + 1FCEE2CC6727B1F2E90216DE9BC4494A2F7D7E0A8B9F9FC6E9CF28FC62112BBF + 398BB37FBC80E5DF9EC5C2578B38FDCD2A56BFBD84C5CFD630FBC149D202955F + C4996F56B0F2D5698ACF61F6FD514CBED18F99370731F7D600A65EEDC6D42BDD + 38415A7C671417BF5CC025D2C52F4FE2E2574B38FFE50ACE7EB184D5AF97B1F6 + DD395CF9FD255CFBF3655CFEF3255CFCDD799CFFCD399CFFED395C265DFF3DE9 + 776771F99B459CFF6C0E673F98C4D97747B1F6DE88009B27DF19C6C25BC35879 + 7F1C173F9FC7D5AF4FE3B96F96709DC28B547EE583092C7F3881739F9DC0852F + E670FE8B799CF9F404563E9EC2F227D338F5D124E63F18C5E2C7548EF24ED1F7 + 73EAB3D3F499D770FE7797B0F6BB0B38F75B3AA7DF5FC4DAEF2F63F5B79770E1 + 4FD771F9AFCFE3EADF5EC473FF784DE8DABFBD822B7F7B1997FEF22256BF7B8E + DABA8CB35F5D215DC2992FCFE3DC3714D27778FA3DFA8E3F5CA2EFE32C2E7EB1 + 8C0B1F9DC2E54F4FE212FD16CF7FB58C5BF49D9E796B14CB6F0CE1FC3BE3B8F4 + FE14165FEDC7F48D062CBED28E0BEF0F63F5DD5ECCBFD98EE9D7DA84E6DFA6EF + 99ECA7DE1DC2C2DB03987F8B7E873707E837EF17DFCFD2879374EC79AC52DBE7 + E87B3CF3F512FD762B38F9E9692C7C4EBFE797A770EAE3594CBED687D1973B71 + E2F55E4CBFD289F19B8D98BEC5AAC7E4F56A8C5EAE40F7721E6A26AC243B3A4F + 1761F87C0D7A574BD142FFCD8EE552E57F7D89FE83ACCBB518BEDE80E1E79A30 + F102FD17AF35A2ED5C3906283DF97237265E241BC5BB2E56A3E34215FAAE3560 + 82EC7374FE7CEED3AFF460E8563B7AAEB5A0FB6A2B066E7662E4E5418CBE3A86 + A1978631F0C220065EA2F46B23187E7508BDCF77A3EF463795EB46EFAD0E74DF + 6843C7A546BA96EAC439F59EAF450B5D4775B37968982B44CBA912B49C2E45C3 + 7C212AC6D291CDDEAF4D8928E9B7A376221BA514E6B4E991DB6E44D9402A2A87 + 3350339E8BC613C56820D54C17A0722A5FC0D72ABA5FF16A01E553792818C940 + E178162AE818BC6241E57C016A4F15A1EE64216AE6E9FB9BC97586D9A89DCAA2 + EF320775278AD07CAA0C8D27A96D3AAF86A552B4ADD5A2F57C036A972A51798A + EE9164AFA16BBC7699D2272B503253823C3A9FDCB12C144F15A07CA10C65D446 + C5E93294D0B10BE9BC4AE99CAA4E9553F932144DB3AD90F24A503ACFB098CA53 + 58325B8682A922E44D1420672C17592339481FC840D650A668BF883E67319D5F + F154218A26F251365D840A3A76C95801B27AD391D19D0A6B8B19495589D057EA + 60E4A59AAB7530542621B1341E89C571D05790BDD102738B1D295DD4F670A178 + D6E8EB8C48AC4A86899E49FC9CE21798B4B556E8EAEDF41C4B172F3639060A91 + 46CFC0ACC94AE44C5723EB440D52272A601B2DA1E7283D1727AB60A7B47DAC54 + 3C2B6D43C530511D7D5F1E4C7DFCBCA438C35B12A7EDFC4C9DA8848D57E1A0B8 + 6598B75F28A1672A3D4BE9996AA667AB95D296BE12987A8B05CC758CD0F378A2 + 16767A9E5B872AE9587542A6C14A3A0E3D9787AAE97CEA601DA91536C36029CC + C37C8C1A3A46358C83E5300E90BDBF1CC9BDA548682F403C49D75D8AE49E3224 + 759720B1A308DAF622E83A4B90D4518C84E67CB1F5454C5D16F5413211D79C23 + 6CF14D7988E7BE4A559AD8F282BD88830A4C02F0B2077128F7838AACD4B7B288 + FE91E837953B44FFC82FCF88805CCA2FA07AA4E07C9B48FB64B287AE4EC83BC3 + 487DB88DFE1BF7EFF8253F4514A73E9F67AA51E903F24B80B664050AF392CDFA + 381C4CD688F0B03191FA94CE1562A8FFC710F8A855275E1EE4BEE4F154A54FE9 + 4176E1094C6D7B67D0F11808A751986E133A4EF2A4B487382EF541A99C17A749 + C7484753A9CF4B3A9666135220B01907A9EF2A00B06503FE1E65F86B55E03003 + 5FE1FDEB84BF078CFCA2A50E078D061C329984F7AFDC6E856D9C3E6030E0B0D9 + 2CC2DDE2E54DA54F2F5FD294E09625E12E97E19081EF26D09BA0C5CE441A0F68 + 7522BE3D8EF3B90EF7F57522641BC35D1E433004DE161383ADD1D1D816456124 + 7BFEF27EC171D81A152FC0AFD4D628B245C60900CCB0F7C9902811320C161200 + 381A8F0747E1C9E0481AEBA84170049EE230205CE8A94025CE00577AEFB21EA7 + 34EF052C25C0ED71B29378196786C04F78B3376FB0D8EB5778F9AA3C807929E8 + C77CB82E9753EACABD808554F097C57097C12E4BC25ED7FD7F392E41AF3ACEA1 + 8CAB21B01A064BE0EB2E545E8A56C6D112042BE271F206F89562F02B01F083CE + 3D7F19008BD00980C57EBF2A002CBD78D570F75E86B52A007C0F435786B40C6B + 295F9451CD55A85F5897C05788E20F6D3F8C5FEE3822E2F73F7B00F730A875CE + 79A8E73D382EE74324F09590F7B6B0D7257E8F736E86E760D4F3382C09765D25 + F3DC0160DEB3573D2F25A5B6C9B9AB7539C12FEBAE271428FCB3479FD9048045 + 9E738E4CCE9F7128E1AF986F73CED1C9793A1967A9E7FC5CE3523FFBD5160A79 + DE8FEB2AF3818AB81C9D87F4F87501C00C8A7F263C7FA53600B05AECC1EBCE03 + 580D7BD571A98DF95565EE96DBB893F7AF98A375CEF772DC354F9DBF3EEFCCF1 + 07A9ECFD728E97D2D2C3775DAA76E55CB4D313783D4D6DC9F96FF51CB8B4C97C + D6E6397CD7B9F40DBBBA0D77FA7EDD3B4969F74EA056EA87802E4B9651CB5D39 + D60FF10D3503917157C9B2EA63B92BF7AF909A3BDD49EB80779D5D6DA4379573 + E701AC4EABEDAA3CB7EC4C96B95D1B2C75991F23776DFC4849C6B7E95CDD1DE3 + 7652B5E5566AF8FAAFD49D00F0F7B40E7439FEFF2700F89FF9516E23F59F412D + 7765599BF2C545F4FD8B475C409B2E50BE116C48E4AB2E3CF60896370FB536B7 + B1593FB69C5AEA3AAE72575E2D77757EAC36815F15F0554360A52CDF347F9CD4 + 0F178ECB879A7CB0A9F3EE2435E0553F28BF077FD571922CEBAACD0F65B2A91E + EC6AA9CBB8EB24FC54B90260B5944ECCF79767961EBAAEB0D71D04E6508ADB91 + 9275D436F602BE8BDFB2E3F3727ECE7B28CDD0F7DE47B6E17ED51EC06AA8ABEE + 08BAA6A5D8EE2E4F740A1FDE469D3E2594C09725E312F0AA3BAD52328F25C0AE + 2AAE78FA6E0060755906C00AACA50EF8E3AC8DCEB42BB4E53CA5AC1297E56427 + DC55B2AC94BA2D199762F8FB633C80A524FC65F15E37BFDE750CBFDA79540C74 + 18FCCA7A0C84C55E38CE25A1D5031EF5608725074E325FA637446D89FD7C9550 + 0D80A5D83398F701562F07AD78047FDFEB572D69179057C25E270066893869D3 + 32CF0C6C9DA13B00CC83D1EF83DF0D29FB19799336967B66C9BD7FE532D0621F + 6027C055835C06BD12004B082CE12F2F05C621A7DD0160E9FD2B960E734AC25F + B9A498B2D458E0BA38CD009725E34FF9B3276F28B604F052640122CD710E9FF4 + E165CA02445A80DE00DEBB8AF7B00A12D0F7999070E1F1AB06C00C7819F4F2BE + 585B7819E8B068EC88D4603B4FB88447E299704A47F1DEBD1A3C1BA7C1330C80 + E3E3D6B53D215E09E3793FAF383169C370584E06C9B7FF79E2688F4E879D0C80 + 05DC55BC7E6528E20C82596413DE0649542E99F29D1EC00C810F9878A24B2586 + C1A47D263DF61892B0DF9844E5B88E56840C8479C28B97C1939EC01200739CF7 + 453B48758E5AF4F0B49BE095C29367CA84DF31122FC7273D3578F28EBD33BC48 + ECC5E193C55EBF76113F9E61A63CA398003C6C63AF0DDE934D8B23295A78A6EB + C45EC001F93C51694770518A08030B6CF0CF35C32FC780C07C33E593F278FF5E + 07A22AD2C5646664799A5068914DE4F9E61BE05F685AF70C96DEC10C80C32A78 + 39E9148457291E3791BCC473532E92DB8B91D45220F6156600CC13AAEC551353 + 95263C84D5DEC131A408AA17426D84D6501B8D19886608DC4A75D803A8AF4448 + DB5B84841EF6062E4232C3DFC172240F57C130510F3DC9305A03E34835F44364 + 1BAD868997861EA98269A41286A15218064B601A2A8375A402B6E17258C96621 + 1B4F46DB282E6030C936522A3C98F45D39D075668B50DF91419F2903B63E5E22 + B318B69E3CD87B7291DA5780B45EB2F19298DD3948732AB52B5B11D54F69CB10 + 4B487399ACE122E44F94217FBA02D993A5481FE5C9F77C648C14207D381F2903 + 54A73F131943D44E5F266C9D0E6A270D390339C81DCC450ED9D2BB1C020A6B2B + 121153148B84B244186A0DB035DB90D6918EBC817C14D2714A27CB503C5D8682 + E95214CF57A064A102A527CB51385F8C9C1345C83B41F6D375A83EDB8EFA4B3D + A8BDD089AAF31DA8BAD485DA6BFDA8BD3E889A6B43A8BD3182FAE72750FFC214 + EA9E9F44E34B33687E650E0D2F4CA3EED6146A6E4EA0FAC624C54FA0E1F913A8 + 275BF5F531545C1A44F5E52134DD9C44D38D71545FE845F96A276A2EF6A29DDA + EB7DF9047A5E9C42CF4BD3E8B83581B69BE368BD3986B6EB23E87F711C936F4C + 63E4F921B49D6B41F34A1D9A972A5031998DAC8E6464B526A2B8DF8AAAB154D4 + 4F67A163B14400B2A69305685C2C46EBB92AB4AE550BC8D375A505FDB77A30F4 + E200C65F1BC5D8ABA3E879AE1BAD179BD176A9052DCEB0F34A2B7AAE7762E4C5 + 214CBD3185C9D72731F2D208865F18C6F0F3831879611093AF4D62E993259CFE + 641173EFCF63E6DD594CBD358D931F9DC23CA7DF3B81E9774F60F40D3AFF7767 + 30F7C969CC7DBA88590A673F5FC2C2D767B1F0ED054C7FB98AC9CF9731FBF579 + 9CFEE612163E3B8F89F74E63F28353145FA63AA771E28359CC90A6A9CDD90F67 + B1F8E982008973EF4F62F9D339AC7CB280D5CF4EE1ECE7A7B1F4D11C163F66CF + DF25AC7CBA8CA54F97B0F81943E4B358A5E3AD7E77112BDF9DC719D2D9DF5DC4 + B9DF5EC2D9EFCE61F5EB3358F9621967BE58C2D9AF5770F1BB155CFDF60CAE7E + BD8C2BDFAEE2CAEFCFE3E26FCFE122C52FFDE6ACD095DF9DC7AD3F5FC1CD3F5D + C695DF9C23DB2ACE539D73DFAC60EDBB3358FB7605E74897FE701E97FEC46097 + CAFCE9022EFCF13256BEA173F9ED655CFAEB2D5CFBF79771E51F2F095DFB8F57 + 70F5DF5FC5E5BF93FEFD355CF98FD77185C2E7FEF36DD23BB8FA1F6F9328FCF7 + 77B0F6975771F6B72FE0D21F5FC1B53F53BDBFBC8CEB7F7B8D8EF7A200C267BF + BC880BDF5CC6B9CFCFE2DC676770FDDB4B78E9F7D7F0C1FF7A1D9FFD7FDFC2EB + 7FB98E2B5F2EE2FC47F3B8F4D9695CFC94BE43FA4ECF7D3081F30CAF3F18C3EC + 5B0338F1D620FDA6A302629FFC7092D243987F779CBEEB199CFE60060BA4A54F + E93B17DFDD19AC7E73068BF43D2CD2F778927E87A5AF57E9FB5EA3906C5FB247 + F00266DF9BC0F82B7D18B9D54E6AC3E88D668CB3B7EECD664C3CDF8A132F7460 + E6C52ECCBFDA8FC5374770FA8D614CBFD043F90C74FB317CAB0B632FF660EA15 + 8E77A2EB52037AAFB760E2D5014CBD3E88F1570731FDD618E6DE9DC4F49BA318 + A2BADDD75AE9BFDD4CFFF9360CBDD48DC9370731FBCE2466DF9DA6FFEEB800BC + 43AF0E63E48D314CBC7D0213EFCE61FC9D390A1730FEFE494C7D7092FE8B2731 + F9DE1CA6DF9FC3E43BF4FF7E7302C3547E98EBD1798DD2F90D3DD785FEEB1D18 + 7CAE03C337BB3042B6911BDDE2DC07AFB4A16DB90AB50C729D60B77C9C3D5D33 + 50399187E69315683D5D85B6C51A74ACD4A3E574355A97EBD07AA61E758B95A8 + 3E5986C6951A34AFD6A1628157112842C55C31C54B50B752858633D5683C5385 + FAA572542F1451588C1ABA17944E6423BF3F0D85A399C283B7F52CB5B9568F7A + 2E7FB61ECDE79B50778EDAA47B4BE572256A566B517386EE892BB542A5272B05 + BCCD1ECF43169D6FC16C21CA17CB517AAA1479D3F9C867E0BB5026EEABB95305 + C81AA3FB3483DCF972949DAA26D5A078AE0AB974AFCF1AE3760A914BF7E38219 + 2E5F24CA96CC95A17CA1721D1417928AE9DE5C3451823C7A36E40CD3B3A52B15 + BA7A8300BAA6060BACCD76B11F7D5A5706B2FBF2903748CF91813CA40DE5D3B1 + CA5074A20AD92345B074A6C1D8E2400A3D8BD2790F7D7A0EE99B1D629B83D4C1 + 22648E952363AC0CA9A3A562758C8CE91AA433009EAE867DAA122914A6529AF7 + DCB74D540AA06B1F29839DB75220D9C7149B856C167EAE52DA4EED58C62B6026 + 3BCB3A5A49CF5B2A3B44E2909EC71CB791CC035477B0829EBD35C8986A8463B2 + 015686BFA30DD44E034CC335F4DCAE225B3D1C53CDF43CAF837EB012467ABE9B + C76AE93875E2996FA467BE69989EF924F3482D8C544F3F50B9AEA4BE72687B4A + 85743D65D0769621B1A314495DA5886F2B444C732EE25AF211D79C8798861C44 + 719F84FA27DCBF112FBD9558C50A28BC1A0A7B0C4770FF88FA4C91159988AACE + A2FE4B2682292FB084B7C74847586906F5B9D2105C40F50B53A9EFE5805FAE15 + DE59FC421F8BFB7256B1828B2FF5E9FC32EDF0CF4E811FC937D3069F4CC53397 + 61F071EA073204F6E095634C89386C88C7212389E2476DD4577498158F5F9B4E + AC1AC32F0E1EB3339495CB42535F319DFB98CEBE24035F07CB0ACF542B3C522D + D4E7340A715B5E94F674B0C7B08DF2AC02F8B28E52FA585A0A3CD21DF0A050C2 + 5ED6115EF6D962107D5F06BE87AD2A104CFD60B114B473C9E70DE0CBCB3E735F + 9CFAC4CE17310FEA1538ACACC8C37DF2CD1058C6653F5E4A0D80E58BA23B1315 + 00BC3D3E91C611091BA1468B1D716CD752592E1F2F00308360E1E91B1D2BE0EF + D3BCE4B37379E7AD343ED9161127BC7EB7B0E72FA723E3F174781C9E0C89117A + 2A94BD81352214F190685294902B00660F600980D51058EED9FBB87F289EF40F + C353BE61EB0058F1E2DD00B88F33FCF50EA5719CE2F92BBD7FD57A8C3D8125EC + F5DCBCCCB32B00668F5FE9DDCB62C02BEB49CF6059C675F9675749E8AB8E33EC + E52D91C4B6484EF8AB962BE4554B7AFCAAD372F96706C0EEB4EEFDEB04C06AF0 + 2B3D79D9ABF77EB653283C829F55BC8045196759691775B83CE793D473181CAE + C3606719390FC252CF71C83C3927C250572EEDEC0A80D7812F859BCA39E765D4 + F33732CE927097C5733D322EC1AFBA1C0360F53C939C4BE2389757A765FCEE27 + A93C2FFD4C62F82B41B0F40666F1DC97BB7934969C7763A9E7EB5CA580DC1FA7 + 4D75D86BF7210ED5D057D12F7E4DC7FF359D2B95FBD9AF9F54B40E7B19D83E41 + 52C0AD748EB98BF26E077F5D81AF8CCBB9551917F93F308F2BE67DD93B9774DB + 7CB518E23A8F2B8EE79A7F1B6D9A9756CD59BBC25F57C9B973F51CBA3A74CD93 + EDDDA95DD77AB7D7233F2809585DF563CAB0D4E55CF543F96AFD101F51E7FFCB + 2558D4AFD6F913EFE52BB5CEA4D4CCE9B652CAB96560FF05CEF6A3A4E6793F24 + 77F57FAAD46DAA8FF193F4C0FF05C0FFA3925FBCBBBC1F2109745DE5AE2C6B53 + 3E5F302A002C2F2211DF7481F2CD60439B2EC6BB4854E69FBD71FCB3E559B2BC + BB1BA1BBF252EA723F45AEF057025F755A3927F70F0FB55C1F18EA3C09807FE8 + 01A7D63AE895629BB4CBB89BB4FAC1BA496C737D10FFC0435E0D80A5D4F93F46 + EBEDBA7448D4E24ECB8F01C0324F1DCAB83AED4EB26D16036006BDF73DBC0D0F + 5247F101EA14DEFB6BC5C610F8FE47B6539A3A6BD4260360D9C15377F4D490D7 + D5EE9A27F3D50058743E5D3AA4EAB48CB336015DA7D4791202BBC25FF602161E + C0A2E3CCF0D7D9B17E4C595A87E312D0725C4ADD2177B573B8DE1127AD035D67 + 5C4A025F35F895F6FBB6D080C2398870077C5DF318FA3EBAD74B882130835E99 + 2F3D83A5D7300F58D40323F560886DEE06489C66FB433B7959252EC3F5C9EE06 + 004BF0CBF13B2DF92CBD7AD590574A96539711E5549EBF0C7C25009610785DBC + 9C337B0AD360F44E0058B16F78012B6F342BD0572D098025F47585BFEA38435F + 968CABF3D47285BF52120073C84B8BF1FE532CF572636AFBD381BC64738408A5 + 9D972CE37DAC6499AD41E178363C5A680BC5398FF7B7624F5F16C35FA110C5E3 + 97BD7DB7B017708802807745C56367B406CF46C6627B442C7644C5617B6C2CB6 + 6962B13536468412FEEE704EEC88B7FA795287F7FE8D63E8CBCB412B93403C21 + 249691D351396D8200C0D28B404E18C9C924B5E4BEBF526AF0CBE975384CE16E + 1D7BFDF2BE6A3C0966C6415E26DA596E9F81ECBC2C344F7E399787E6893006C4 + 3C197694C12F4FA8A5587198BD829D7B081F62989BAAC751DE972DD50C0FD231 + 874978681CCFB0C1272BC5295EFA5959BACF8374C44175520D3896668467BA11 + DE592C933334E078A67E5D0C86FD724D02FE8696A488C9CCF0B254110F2CB090 + AC082BE549CE0C0431302EB42288F2824B79B9E834A1B08A0C523A89E3A922E4 + 25A523AB3311DF5400534F152C246D732162EBB211539F23C2C86AAA53E94028 + 29A2261DB18D39D034E521AA3E1BE1F599086FCC44744B2E625A7311DB9E8784 + 9E2268FBCBA01D28472285F1BDC588EF2B41C2401912072BA01DAA8476A40649 + 237548E6495F8EB36DB812BAE12A2453DC4071D36825CC24137B0C535D7D7F89 + F008360E94C03C582AC0307B0A5BC7B81C951964E09C8FA4DE0224F71540DB9D + 83C4AE1C2A5F04EB3095A5D0C4FB1253197D772E0C3D79B0F4172265A8048EE1 + 52A40C97C04A652C7D85642F104B4CB32D7DA20239B3B5C83DD980EC853A64CD + 55237D863DA5A8DC0895992846DA64311CA3141FCC45EA703ED2C9EEA078CA40 + 0EACBD1930763AA06BB120BECE80847A23121B2C30B4A5C3DA9383B4A16214CE + D4A1EC6433CA175B5178B2918E51839C5335C83B5D8BE2D566949D6F43E1D936 + 14AD76A2ECC2002AAF8EA0EAEA286AAE8FA39A547E6D0C65D7287E6B0695B766 + 51F6DC14AA5E9847EDAB8BA82135BC79064D6F9F45FDEB4BA87EE934CA9F5F40 + E58B2751F5F2499471F91B27507E631AA5D7265172651C95CF9D40EDF3B3A8A5 + F6EA5E9845C3CB0B687B6D115D6FAEACAB85DA6D7AF9149A5F398DC61729FFE5 + 79F4BF790ABDAF9C40CB8D61B45D1F44CFCD41B45FEE40F97C110A4633503C91 + 8DA2B10C148EA5A37CB64078F2D5ADD4A07EAD114D573B507FA915B5175AD170 + AD176D2F8DA3F7F5790CBD731A036F9F42FB8B93687D6102DDAFCE2A7A8D3587 + FE374E61F8DD158CBEBF8A31D208C587DE5E141A7E7759D8273EB98031D6C7E7 + 31F1D9454C7E7E09539F5FC4E887AB187C770903EF2DA2F7DDD3E87B6F09C31F + 9FC3F067E7496B18FBF222C6BFBA8A896F9FC3F0975730F0C5658C7C751D135F + DFC4CCB72F62EC8BEB18FEF412C6C93EF1C5254C7F7919F3BFB98EB96FAF62E1 + DB6B38FDDB6B98FDF202263F59C5DC17E771E2D37398A4638E7FB08261FA5CE3 + EFAF60EE938B98FFF812663EBA8813149FA5B666BFBC8A135F5CC50C8573DF5E + C7A9DF3F8FD37F7811277F7703F3DF5D13EDCF7DCBC7BA8A53BFBB8695DF5FC7 + EA1F6E60F52FCF63F5DF5EC219D2B97FBC82F3FFFE2ACEFEF5452CFF99EC149E + 137A09E7FFFE0AD6FE4179FFF6B2D00AE52FFEE92656FF4E69D232B5738EEAAE + FDE34D2CFFF135ACFCE5759CF9DB5BD4C6DB58F9DB3B58F9FBBB38F71F1FE1DC + 7F92FED70738FFFFF904AB145FFEB7F7C8F6312EFE9F2F70FE7F7F86D57FFF18 + 67FEFE1156FE8DF4E7F749EF62ED2FEFE2FC5FDEC1F9BFBE4FE17B58FBD3DBB8 + F0574AFFF90D9CF90D7B0BBF800B7F780557FEF43A9EA7E3BCFCBF3EC4737F7B + 03177E770B67BFB98655FAECABDF5DC5CA571770FAD3159CFE6C09A7BF58C1EC + A74BF4FD2E61EECB559CFC668DC273F49D9FC5E9AFCF6389BE2B21FAEECEFC9E + 3EE79F9EC7D93FBF84953FBE409FFB1696FEF83C4EFDE6392CFF81EC7F79194B + F43DF377BAF6E75B744E57E8373A83D90F4F63FEA353987C73725DE3AF8F63EE + BD19ACD0F1CFD071173F5DC6C98F1731F3FE49016527DE9FC7F87BF3142EE0C4 + C7F47B7F7812036F4ED16F3F83E94F9630F3F92A663E592151FB9F9EC1D487F4 + FF7B771E7D6F9C40EF6B53E8796D92FEEB13187C7B8EFEDB4B18A5FFEAE8FBCB + F47FA6FFD1E71730FED9058C7E4CFFD34F2F62EAAB6B38F1CD0D4C7E799D740D + 53ACAFAEE0C4D7D730FDD5652A43FFEB8FE8DA78FF3486DE9C45EF4B63E8BC35 + 84AE1746D1FFEA14065E9D44DFCBE318786512A3AFCF609CCA0CBD3C89B6ABDD + A85C62285A46F7A84A549CAA42F5E91A349F6B41DB854EB49DEF40D3B956349E + E397323AD07AA513D5ABF5283A59810A0AEB2FB5A392F2CB571A51B65C87D2A5 + 5A545F6841C5B906949FA1F4E96AE4CD94207FB618D91339C818CE45FA601E32 + 4619D696A172A509B5E7DB51BDD686F2D526549C6D41D5C57654D03DB182EC95 + 17DA51BED68A323A9FCAF39DA85AEB44C9723372E83E9D3A5184B413C5C85FAC + 22D5206B9EEEE527AB50B85C8F126A2B8F3E07A7F93E5BB8DC406A44E112D94F + 352073B61A69D395C898A9A2FB7E2DD5AF432E95CF5DA844FEC96A149EAA4511 + A960A11AB97315C89BAB44DE6C1572E8D9903DCDAB53E4D33327879E0745C81C + 2F43DE54058AE6A8CE7C1D85A4F97AE492724E35229F94355D05FB08AF78C1CF + 1D7A9E8CF0334959CA999F59C6017A268D926DA252F1F09DAC846DAA1AF6995A + D84ED4C2447123C94C710B3D4FA4ACD3B5B04ED551993AD84FD48B38BF68659A + A88185E22CF3642D0C243D8794364ED5C33C510FCB18D59F68A03A8DB050681C + A5320C78476A611E27DB6433B5D304C34423F463CD308CB72069B8819EF375D0 + 8D34412FD28D481CAAA3B6EB85924949E3B5D08D9146EBA81FA084C649A59D64 + 6A3779AC0149A3F5D427A0FCA15A24F45741D35D0E4D0FF51F066B90D8570D4D + 5719623B4B11D5568488E6024436E523A2310F910D7908AFCB4128F56BC26BB2 + 1141F128B245D4E521A43A0B61B5B9086FE0B205D47FC9178AA82B4048551E02 + CAB3E05F928180D24C219FE23478E5D9E199638357AE1D3E050E522ABCF353E1 + 93970ADF3C4AE7A6E0788E1D5E59548EFA77DCB7E31561789968DE2684B712E1 + FD7B59BCA508435E06B61C729EF2E2A051BC6CC8DB8F78A498D65797F1A0329E + EC15CC0098FA9EDCFF3C9E4EC71269F600E6BE26EF1F4CED0931FCB5E190DD82 + 8336EADF9278E51BB671C87B014B38CCF1BD261D76F14B90D40FE67CA54FCC7D + 63D37ABF982557D4D96F50FAD3B24FCD7DF3BD3AEA2327EBDD825E06C45C8643 + 051E735F7E435C87FBF5CACA401A1A1F509CC607CF6812B12D36418432BE3526 + 5E84CFC652198D5684CFC424605B74BCF0EE959EBE0C7BD9A3973D7DB74628C0 + F7A930B2B3F72FA525F0E590F3443EA519086F0989A5B10FE505C7E0A9A0683C + 1918852702228538FE944A4F53992D5C86F348D2FE847F041EF30DC3A33EA19B + F488377B0B33F8E534879BC12F4B006127D8656F5E57002C3C89E5D2D014E77C + E905FC4B06B9144A4F60A97540EC06F44AEF5FB51730879C2F812FC3DF4DD057 + 0D829D90574A025ED6FD7B8F09B15D0D80BF077A5512FBFFEED8F0FA55EFEBAB + 86BBAEA0576D53DBEFE5790AE71C867A6EE3C748D691F578CE4342DD4D6097E2 + 12FE7219097F65BED8FB97E43A4FC3924057C25D299EF3E1B9210980D51058D8 + 1E53E6A47E48F7A8749704BF2E00582DF51C995A72AE4D4ACED7C9393B398727 + E3EA393CB5A473889C1F94FA05E9AE5F52DD87A89D5F52BBA4BB7F45C756C515 + D1317EB985EA3C45DA00BA9B012FCF69F2DCE666D8CB92E09575BB3CF51C2B6F + C3C7F3B7728E57CECBAECFCFCA395B27005E4FBB916B5D713C751B77909C9F56 + 4BCE5BFFD01CB9BBB9F5DB493DE7FE63DAFC61296CE087C44B4ECB3D8B5D7986 + DAE62A751BB7D38F651FAEE5D4EDABF3FE47A4624F2C57F8CB5A674E779452EE + 4E0C6C937E80BFFDE87658B2AD1F2377F57FAAD46DAA8FF193F4C0FF05C0FF63 + 527FF1EEF2FF4949B8ABD6EDCA88345F303FD1035894E1FA4E00BC49EE6C2E92 + 379A7FE666A3AEE32A7519D7B29B3787DF7CA37327D7FA6AD0AB06BEAE69770F + 8EDBC9FDC34391BBF2B7D33AB8A57A9B74BB3CA74DFD50DD24B6B97920B3D40F + 7377763500762DF3435A6F577644BED739E10ECC66782B81AE84BF325FC65DC5 + E5DC9555B7A906C03F7F90F7D4D88A9D87FD61741442939C8A47B71DA0BCA795 + E5A01F7A1AF7FC6A9B80C05C5EDD19540361B5CDD52EF3385CEF64BA7440392D + 3BAA6A28ACCE574B0D77D5E5382DBD7F5DB5B104B4D2D996F0570260096F25F4 + 554BDD019736595E5D5776F2D5E93B01E07B9D0058025C0EE520428A6D12ECB2 + 772F2F6F2496387A9606316457C35F594EB4E51CBCA8073FEA38835EB56D3300 + A6F69DCB410BA9C0AF84BDD2E3570D7DD54B3FBBC25E96DA2ECB4B1B835FF606 + E6F8837BE8DCD893580581EF0880D721EFF7E12FCBD50358C2DEEFE9C8C67EBF + 12E8BA4AE609B8AB4AABED0C84D93358C897BD7315D0AB86C11200BB8AED324F + 82DE2D010C6EC38538CE3686BBACC703289FEC4F0747605B68147646C561774C + 8200BAF2AD76CE637139DEEB8AE3ECE1FB4C440C9E0E8DC453248EEF888EC3F6 + 2805003F1B19876738CE7B73C56904FC955EC0ECF5BB3321490060F956BFAB76 + B007B03611BBD8F397C47106C1CA04132FFDA6780BA821307B0033FC3DC07B93 + 59CC2214E097BD169C5EC03C69C51EC13C61C5F07797D35B98ED07CDCA24174B + 2E1D2D27C00E515B47D823C2B92C1E87BC5C9E0280CDD867D18BFD84795FE123 + A9261C4DB7E05886151EE936215E86CF23DD8AE359763141E89D63179385DE39 + 3678669AE19545657919C16C1B7CF252A88C55789BF012D0C7331908EB712C55 + 078FB4644A1B441E7B063318D6B594A27A79028E814661E3A5A3D94B38A83045 + 780D8714B3074C2A42CAD311E40C432B32105E95B5AEB0AA0C44D46420BA3E87 + 942D3C6892DA8BA1EF2C4342730162EB73115D9B4D626F997444D6A4238AA170 + 632EE25B8B90D8568CB8167A06B41640C34B3776168B30AA2517B11DF988EF29 + 41421FB5D55B86B8BE12A17886C1244D6F2962499A810A240E29D03771B81209 + C39C260D94218964182A879E94D44F75BBE9185D79D0F6E48BA5A4CDC3E542C6 + A132E8074BA11B2846525F21952DA27A65300E531B144FE8CE237B3EF40345D0 + F51540DF5F48F50B295D00238596A162A48C57206DAA1A99276A914172B04714 + 7B478D96C2315989F4995A64CD3720FB54133219CE9E6A40C6421DECD315B04C + 94908A609F2C856DBC180E4A67923D73A21CF6C17C187B32A0EB4841728703FA + AE5424B73B60EACE2065C1D65F20F670CCA5F64B4EB5A264B11D454B6D28586A + 41DE5233F2969B90BB5C8F82B556945FEB43C5F561945F1E42D9A511145F1846 + C1058A5F9B12C0B7E4FA148AAF4DA2ECF939D23C8A18E8BE780A35AFADA0EA95 + 25D4BDB18AC6B7D750FFE61AAA5F3D43798BA87C7911652F9C44C1F569E45E99 + A090DAA07AA5376750496D54BFB0807A2AD3F6D639B4921AA81D56F3EB67D0F9 + EE05B4BFB386E637CEA0E155B62FA2F5F565B4BD76126D2FCFA1EBD57974BE38 + 890EDE7FF87A3F6A57EB85E75FF96C21F2C7329133948A82A91C54AFD6A2ED7A + 37BA5F1C41D76B13687EA11FD557BA517D7D08CD2FCDA0FF9D3318F9E83C06DE + 5B45C7AB0BE8796B09E30C5C3FB9286CBD943FF4E11A86A94CFF7B6731F801C5 + 3FA1F21FAC62E8A37318FDEC320629AFEFFDB3E8FFE03CC52F60F8D38B8AA80D + 0E073EA6F254A78FCA777F70167D94EEE33485839F5DC1C8D7D731FE9BE731F8 + D573E8A574FF17CF61FCDB1731F3BBD731F5DDCB18A1F4C8E75731F6E575CCFE + E6164EFEE125CCFEF679115FF8FD2DCC7E771D335F5DC7FC3737304B6D4D7F79 + 15535F5CC1049DDB34D53BF9F52D2C7DF3224E7DFD024E7EF302967FF72A56FE + F83A96498B7F7C15A7FFF40A56FEF626CEFEC73B38F38FB771FACFAF62FEF72F + 62FE4F2F61F12FAF60F96FAF62F1AF2F63E10FCF63FE0F74FC3FBD80537F7E91 + CABE8EF3FFF90ECEFEFB3B58F9B737A81CB5F9D7D7A8ADD771EEDFDF16ED9DA5 + FCB5FFFD1E96FFF106E6FFFC124E715B7F7F1D27295CFD3FEFE2C2FFF91467FF + F11156FFFD23AAFF3EB5FD36E6FFF83616FEFC0E4E53FA94D07B58A2FC252A77 + FADFDEA7B63EC6EA7F7E8ED5FFF80C2BFFF8044B7F6708FC2956FEFA119D171D + EBAFEFE1DC5FDEC3CA1FDEC1F2EFDFC2EA9FDEC5DA5F3F50C0F05FDFC545D285 + 3FBF8D0B7F7A0B6BF41D9CF90D7DCE6F6E61F1EB1B58FEE626967F73132BBFE3 + F03A4E7F79012BDF5CC4B9DFDF20DB75CC7D7D91BEE30BC24B7B86F24EFF96CA + FCF6067DF757B0F02DC76F62E9F7CF63897E23D6A93FBE84853FBD4CE1CB58FA + D3AB38F3D737B0FA97D7B048DF2F975BA6EF7385DBA0FA4BDF5CC622B5B9F0E9 + 2A4E7D7616F39F9CC1F4FB0B98FB7891CEEB2296BE3A8FD98F9670E2A315CC52 + FECC176B98FAFC2C9DD305CC7D45A2733BF5DD15CC527C92F2A73E3F87692AC3 + 717E4160EAB3733841E9096A7FE4C3150C7FB08C81774EA1FBCD3974BF75127D + EF50FADD15CA63F87B05535FDDC038FDE746BFB886F1AF6F62F29BE729BC85D1 + AF6ED2FFF2454CFFE645B2B1FD2626BEBC86D1CF2F638C34FCF11AFAF96587B7 + E684FADF9E473F853DAF9F40CF1B33E87D7D06DD2F4FA1EBA509F4BC44E10BBC + 57385D97E7DA50B1D2486A40258535AB2D68BAD88596CBDD1476A3E56A2FA907 + 0D173B50B9DA8492957A9452B9D2158A9F6B45F5854E549E6F47C9194A9318C2 + E6F34B2EF355C8992F43C674213226F290335D86FCD92ABA2F2A50B578B181DA + 6A46F9D93694310466E87BA98BD483AA2B7DA8BEDA8FB20B3D283DDF4DB63ED4 + 5E1B42CDD5413A6627B24ED6226DB69CEED9748CC51AE42ED5A2F04CA3389F32 + 6AA798C2D2353A5F6A8B97CE2FA178D16A9BB8FFE62C36D13DBF11D9A7E99EEF + BCEFE7D3E729A4732F58A6F852BD00C61C2F58A4FB349D670195CD3959278071 + E61C1DEF6403CA563A51BDD68BDA0B74AEE77BE97374D1676A45FE4223F24ED3 + F7B0DA4E6DB621639EF7BFAF109EBC19F44C704C55C146CF07EB083DEB464AE9 + B946CFE7A12218393E4ACF47CA334F55C23A53435200B09E64A0FAC6E91A9867 + EA609B6F8463A1098EB92658A6EB619CE4FDF66BA95E9D088D13354A9AF34E34 + C234D3446AA678B380B1C6B10698279B61996AA53A941E6FA4E33688D038CEC0 + B719BAB146E828D44FB609258D35237EA81E7183F5481C692435911AE8DC9AA0 + 9B6840E2682D92C6EB455C3B568784911A61D3D3F14CD32D309D68A5F36F43F2 + 443392C79B8412076A10DD598A982EEA53503CA1BF46C06016DB23A94F124D8A + A17E092BAAB910E1D487614534E421BA89FA264D450863E0DB508088C6424452 + 9A4391AE2F42684D3E822A7314085C9609BFD20C01808F17A4E078BE033E8569 + F066F84BF22D48875F4186904F5E1ABCB253E09169A73E1DC359657B9075889B + 62104B3C2B10588723365E3186FA780E65159923FC22A12559C0DF63760504B3 + 8E51DF5278FC8A659A4DD43765306B145EBC12F81E213BA73DA89C077B08A7A6 + E068AA5D00E0FD16A3D001AB49806099E6B884C0DC87DDC72F3B52F94336CBFA + 0A3A0C7CD52F4FCA7E328BD3BBC9BEDFC87D6DAA9F4C369D5E2CDBACC0608380 + BBBC5C33C7F7EBA90F2ECB908D977256E0AFB2A4B3F21228EFF9CBE3001D9ED1 + 68D761AF84BF4FD35883C576B649F8CBE92D34EED82A967C96CB3EC7E3A9700D + 9E8EA074F8F781AFBBF40FC15F96AB8DD32C8E3FEE1FB16E7FDC8FBD82378BA1 + EFC30C802964382C01F123DE8A0730835FB99CF4C35E1B7BF972C8E0577AF2B2 + 772F835F097FE592CE12008B3D7E9D65D5F057C06127FC9520582D5708CC72E7 + F1CB527BF4DE0EFAB2EEDB73548881EFFA1EBF4EF82B40AF4AD22641AFD4ED00 + B014435EB904B43A4FD8490C80EF53BDDCCEE2B90B966B5ACE69B04D3DDFA186 + BB2C117FDAB9A4B30AFECAB82CC3690981052876CEDD48B902604E0B3D45F627 + 9C80D70D0056E68DB80CCF0FD131A8AC7AEE8943097DEFE57925675C0D7AC57E + C02AA9E7C5D4736752EB79724ECE8D5CE7EFD469D7F93E57FDC215FE3EF47DDD + F3CB6771CFAF2864A717161DF39E8715E71386BF1CB25CC1AE5A721E556A3DCF + B9CCF3A63C97F95B77723777EB4EEEEAFD507D39277D3BC9396A77F3E06AC979 + F41F0377EF94A7967A7EFECE7AE44749E1059BE5AEDC0FE97BACC2C941D452E7 + AB75BB326ABB3BFDD872FF8C2453FA21B92EFBFC7D51190ADDB12FB7FA01FEF6 + A3DB514BCDF56E2777F57EAAD46DAA8FF193F4C0FF05C0FF2372F7E5FF57CA91 + 24DC55EB7665449A2F989FBA04F4A67C173100FE0108ACBEE1FC14FDD836F9C6 + 7AA7B76D5CCBBAB3AB41EFBA54FBFFB29476BFFFE0B89DDC3F3C36E4AE8E3B7D + 0FF6BA13979172DAD40FD44D796C77F370762BFA4E6FB71488DBF2B791ACE3DA + 21919200F8E70F6D06BA12D4AA6DAE90F7C788DBB89B3A6E52B2CD9F3D409D21 + 3AE6D37B3C111E6F855F44127E4D1D59CEE78E19036086BFBC0C34DB64E74E74 + FCE81C3854770A858DCF8FE2EA0EA314772AEF7EC44D27D49966F8CB52E7B996 + 7127594642617700F8AEC764879A3BE2AC0D002C2130CB9D4D2DD92997B0579D + 96364EBB03C0B28C94B0390709EA41845A6C53C35D59469697796A8965A069A0 + 230740EAC1921C04499B6BA816DB78A967F70078B3C72FEB7676961A0E6F4E1F + C22F79A96735186600EC5C025A42605729FB017BAC2F01ED2A098415F9E0611A + 003F7C8421B0027B39FEC8511FA14D71D512CE12ECB2D4F63B89EB4800AC2CCD + BC01761900B3D480F7710A1FF1A601BE530F533E8BE38F713D86C08161EBE057 + 026109809F0C0A1310775B78B4D0B3111AEC888AA778AC58F28CF5747014B604 + 450A719A97437BDAB95796D8332B3C0ADB2214E8BB2D3256C49FA1769E89E4C9 + 19F6F855BC7E19FE0AC5513C8E2773383F81E25A31D1B33D9E81304FFA28DEC1 + BBB44902FC4AF82B977A66E0EB0A7FA5A701E7EF63E06B3428E5F53CB9C4CB47 + 2788F87E931EBBF449D891A4A4C5E495F0024E16E23A7B843D19FB789F339B15 + FB79F2CBAC777A4F289E143C8976C062A47CB33259E6144FA279A4DBE19991B2 + 2E4E7B64523CCB21E495CD4A81776EAA9057B6DD192A36DFFC740187FD72EC24 + 5E2E9A41AF05C733CD22F4CDB188E508E59284090DC5C81C6D475273097CB2CD + 0210071630FC750805E4DB11589C8E90F26C0497F132875908AECC4268750EC2 + 6BF39CCA41647D2E62D9DB971451978B6892A6B10031F57988AACB43646D2E22 + 6BB2A9ACE245C393A8312DC5D0B49621AEAD1C71EDA40E8A779621BEAB1CD16D + C508632F9CD64244779522A6A79C548698DE3268FA2B04F08DEDAF4414D923BB + 4A10DD532AECF183554818AE463C296180E27D64A33ADAFE72A1B8DE1268BA0A + A1E9CCA7E31421996C8641F60A2E45525F3112BA39AF00B11D7942F1DDEC815C + 02ED50197443E5308EB037711585BCC42543DB6A58A66A6099AC8671BC02A6F1 + 4AD8A66B9136DF446A8163A611A9271A91C21E5353B548996D40F6623BF2577B + 90B3D289F4D32DC8586C41E6522B1C0B0DB09DA8826DAA12A93335C89863CFB1 + 7A64507BA95355300C1420B12793C23C58C78A611B2B41DA6405D2A6AB914365 + 8B16DB50B2D285E233DDC85F6E47CE521BF2CF51FAF2004A2E0FA1E8CA000AAE + 0DA2F8E6284A6F4EA3E4DA340A2F4D21F7E204722F4D926D01952F2FA1FC85D3 + 28BD7512E52F9E4619A99454F9EA1954BFB686D2979651FECA19D4BC791ED5A4 + AA37D650FEEA2A0A9F3F85BCE7E6917B7D1639576748B32810ED2DA3F6F5B3A8 + 7E6505952F2D89B08A6C15021A53DE6BAB687AFB029ADFB98C86B72EA0E1CD73 + A27CED6B6750F7DA329A5E5F46E75BABE878ED34EAAE8EA2EECA203A6E8DA2F5 + 6A0F2A16EB907FA214BC946AC15C192A2F34A3F1C6201A9E1F41ED0B23A87A7E + 08E53729FEE21C5ADE3C838E77D7D0F3E125F47E7459C4BBDEBF80FE4FAEA2EF + 932BC236F8C9758C7C7113439F5E17E5BA493D1F5D12E538DE4765BBDFBF8CCE + F7A88D0FAFA2FFE3EBE8A3B0E7BD8B42831F5DC1F027D7A88D1B18FFEA798C7D + 7993740BA3D4E6F0E7CF916E6084EDDFBC8CA12F5F4437D5EFF9E4398C7EF50A + 46BE7C19FD9FDF42FF67373148E586A9DEF8372F60FCDB1730CA208EF50DB5FB + CD4DA5EDAF6E61F2EB1730FDDD4B38F1DD2B98FDED6B98FBEDEB58FAC3DB58FD + E3BB02869EFA1D7BDCBE83337FFD0067FEFE2196055025FDC70758FC8F0F71FA + 1F1F60E16FEF62F62F6F61EEAF6F63E1DFDEC5C2DFDFC5DCDFDEC2E41F5EC1F4 + 1F5EC5FC1FDFC0DCEF5FC7C93FBF89A57F7B4768D129B6CD8B3294FFD7B7B04C + 75CFFEE78758FCDBDB98FDFD2B98FFD3EB384576D6EAFFFA106BFFFB53ACD279 + 9C259DF9B70F70FA2FD4C69F289FC2D37F7D5784F37F7C1373248E2F5199C5BF + BE4FE5DEC3D2DFA8CE3F3EA5F63FC7DADF3FC399BF7C40EDBF8D5374FE2B7FA7 + 727FA6B6FEF03A9DCF1B98E7F3FDFD6B58FAF35B58A1632FFDF90D0AE9BBA0F0 + F46F5EC2EC37B730F3F54DCC7DFB3C167EF33C4EFDFE459CFEFD0B94778B7453 + 00DB85DFBE283C604F7CFB9C5337A97D86BBAF60EE372F60E1772FE1F41F5FC5 + A93FBC268E354FE1027DDE79FEBC7FA1F3E2CF45E772F2F74A99A53FBD46C7A1 + FA74BC930CF6A9EDF9AFAF62F1B73771E68F2FE2E4B7D770E2F335CC7E7999CE + E1064E7E771D273EBB84E9CF2F63EE3B2A4BE526A8CC1469FA6BF6C6BD4AE771 + 8B7EFB5B18F9EC0A463FBF84716A6FEC4B8AB307399599F8E61AC52F61F8B38B + F4DFBB2CCAF1CB093DEF9F41F77BAB18F888EC9FD1FF95FE9BFCBF1FFEFA798C + D0FF6D84FE7783F4FFEBFDF439F47D4EF66F5E12EAFB82FE9F7C7DD0FF7A84FE + 7FACC1CFAFD3F57289AE9D35F47DBC4AD7C32ABADF5944FB1B0BE878F3147ADE + 59222DA2E3F505F4BC710A03EF2CA3FBF59368BC39818A4B7D285DEB4419A9FC + 5C27AA2EF4A2E6523FEAE91ED5746384AEF701545CE842F5E55ED45D1F401585 + C554AEE44237AAA84CC5D57E14AE75D0BD94EE7B642F5A6DA7B00395577A507E + A113D5177B507BB91F156BDD285E6943D1722B0A575A917DAA4140D2F2CB7DA8 + A463945CEA41111DA794DAAFB83640A1A2928B7D5466502C675F46E99C9516BA + 77D7D37D95EFE1F5C86560BBDA82026AABF86CA780C655578648C3748E74AE17 + FA507CBE178567BB5170B60B79673A914BF7FC9CE56E6AAB03B967DA49ADD44E + 33B24FF30B4174BF27E52EB5A088DACBA3FC8C53CDC8A4674436DDCB73961828 + 339CA6F35B1B4021C5457BA718F83623FD542BB2CE50BB543767A51D59F43CC8 + 201B3F8B524FD4C3365903F3183DA7E819A6A3E759D250299247CAA1A3679996 + C2647A969967EA619D6B2235C334DB04FD34C3D55A2433E03DD100CB6C33E5B5 + C03CDD2CB65D105B2F4C36883079BC0EFA714A4F35C030DD28C0AF71A695C256 + CA6700CB75DAFE7FECFDE5971D47B6EE8DDA96D9B22DCBECB62D335B16333333 + 1548C5CCA45291A4123333B354629598C1CCD076F7DEE7A5F18E71EFE7FB27CC + FB3C3372AE8A4AAD926577EFBDFB9C7D3E3C2322664464AE5AB53233227E3967 + 40335C3E80BCD428D8872F9A21431695E37C9532727935CE59A5A26DC0BC1219 + BC70BA0C5B5221431797CBE02565326871A90C5A5822431697C9B0A5E59AB23C + 704131FA94A2ED7419BEACDCD5A13C7451990C5F82BE0B4AA47F6DBE6AD0FC22 + 4DFBCECC95FEB30B30C6C897DED539D2B33C13638F2CE985B427C646DD4B52A5 + 6B51B2742B4E919E65E9D21B75B4F7613DCAB4B3BE33C6435D0A901200E72749 + E7BC44E9883153BB8C78699316276D5262A56D6A9CB483DA24C7A8DAC2D62E35 + 1E6337B44B8AC3586E8A7C148FF1DE34D4254C96B6DCF203E3B8D6F1E3E4C318 + 8E1947C9BBE3095687CBBB6347C8BBE3E8FDCBED43E8E53B46A1B08B3EE35E36 + 74FBF38E978FC68F970FC78F83ADE1654517BD067D26A26EF204F9243E463E89 + 9B22EF4F403B8C5FDF9F38C18D63F982E3389E7BBC7C30C97905BF8D7E6F110A + 4F40FF98F1F21EC6B7D4FB38CE9B6347E93899ED5AC74E91B7C68CD697310981 + ED05497B99922F5272DCFDCE281C6F38C6CB23C6CA9BC3466B9EE96B834748AB + 41186B0FA1673084546D1CEB53A86348678A792BFF65D008797100D2014321A6 + 26964DC3E4A57E43E485BE83E579CC5D9EC33CE479CC3D9E67B8678678B67C4F + E41500D3C3D7415E5F84C02683BF2F528167AF7AF306509732F84BD84BB14C31 + FF54FB1E0A77995204BC04BFF406B67A02604ADB1008B7E91AD153B05B68E827 + DBD0DB1736A8456BEEF3DB599EF888B0B78BB46C0D3B52EA890F3ACAE3EF77D0 + F4C90F19E699D097B018F5415FDA1EFFB063A46DF3F71ABC7C7DD8EB97CD4338 + 1A00E65CF96EA0AFEFD1CB94E0B729F86B6DA2EDF96BF0D7F7EC35E0EBCBC0AF + E97EB6810CC6DA1A86AE5104F646EB165C0709D6427C717DC4D64E222FCB0779 + 4D5987FCFD2F0690F745D8B8AE12D487D7667C19FC3545D67890DADA91AD0F29 + 000ED6899C1C2436B12EB28E84D4D41400B6FD7EB9FFAFBF2E66EB69BE743D2D + 80BC067AFDB2AFF01A1E1559030C64EB8226EE01DC4CD70F5F86E848C214E5E6 + 383FF538D720712CD8088923DEC0E8C7F5C907A0FB71DC079A3F8FBC83C13EF4 + F5155E4BBD8F9EC3E8D7EC51D47BC0B75900844DFE5A6E530AAFE186D7737DDD + C95B38D2EE21077AC30AAF59DBDA7753EBE47F740DFDF764C7BB3BB5F85DFD1E + 87B81B35C53EC276BFAEA97AD3EFD59BEC7347ABFBB30AF3A73FAB7F26006E8A + A1FDAE7C56D794A2F5FBB3F28FE99FE34FE991FF0D80FFC315F58B87EEB61D65 + F55E7BFBC1FAF2EBFD365AFE9300987BFE46206F34D0DB94DD937F53FA338A76 + 3CFFA66A3683BF567F3F1E387EBBBB1101EFEFC91D33FA1B45BE8D0F89F0F749 + 5BB4B677231FEA46E4DB980FCB6F6B6D98E2BCD11ED0511580DF7F14FE52EC13 + 19787090121ABC347804D37E3BBC35606BF0D787C07EBDAF709B30FC35B14CC0 + FB608B97E541A4EAF9CB411EFA3E441B812DDAF8B0D71FF8B16C0345B3FB36AA + F1E0D0415E930F77FD32535FD6C614AE33F81B86C0B62F303D800975EF6BC981 + 7530086F69B6066F5F1F00B36C03715FFE20DDDA18D4F507EF2C33CC73930038 + 9844F8938A68930B4E4E0CECFA1316CA26357E59F7020E26430681AD6C36B347 + ABA7F8C6ACBE35EB015F1F041B0C36904B593B07761B83DE709D01DFC7097819 + F639282B04868DDEBE4C2DEF806F634F60EE03FC3827B2C19BCBA606AF5F2703 + C1BEB72FA1EF531FB55729FC45FAC4877C9BBA8D8A791FFA5A9E76AB336F5FA6 + 7E1B1F021BEC65EA7BFF5A8867D611F43E059BC15F136DAC534FDFA00DF304C2 + 06809FEBDA535EECD127E2CDFB7CB7DE0A774DB6EF9581DF17194A0DF66791A7 + 34B45AEF01F2622FBE3DDF5FC53C43ADBDD4877B730D56F81B01C003097D9D08 + 7B097F097E1DFC1DAA6A3564B87A0FBC361475415FF5FE1D35F236004CE86B61 + E5086F1D28766A35945EC483E55502E8810314EABE33768CBCC676A8A787306D + CE0BD881E337A9D1A374B18BA92E98058B67B4BD4D8FE089E3E51D7A508C1FA3 + E26219F754A3A745647FB5402C531FD20B38CE8160A60E044F913609312AE64D + 6D12B86048EF60E72D6C6105DBAB67F044E9903C493AA5C648D78C78E992168B + F26469336D1CDA8F439B0928B3CD44E99C3645BA65C66B7D87F438E99C3555C3 + 18766658E89C7805C0BD8A92A577718AF42D4D953E25A9D2BB84F934CDB38E29 + D5AF8CB624E941005C94A49E32F4A0E93B3D1D693AD24CE95F9123FDAB72A57F + 75AEF4ABCC923E1599D29BAACA92DE3554A6F49999257D67E748BF5AB49B93D7 + 90D2060D60684768E0BC0219C4508F7391D6E6CB60B41B3A374F86110E1312CF + CE9441B3D265682DE16F9E8C98972B43E666A35DA60CA84993DE9589D273C634 + E985B46F4D8AF46718EAF939327271818C5A424F2786BA2C96B12B8A64CC4AA4 + ABB8305EAA8BE76356166979C29A7299B466864C5EC345F86A89DD5429533654 + 405512B3B15AE2B6D448DCE66A99B2117655854C5C5B8A7AC260B7704F583C61 + 79A14CC639A6AC2E9671CB7265CCD26C19BF221F758512B7B658A6AEA7676FA5 + A46D9B29293866F2B6D99204256E9B25D30899F7CC95D4830B25EDD062C93CBA + 5CD28E2F97C423CB24E9F01AD83748CAC18D925CB759D28E6E95F4E35B25EDC4 + 56C938B55DD24F6D93F493284399A77748EEF93DD03ED859B75DB2CEEC54B18E + 6DA6D5AD9784C31B24F9C8461C7B83241C5A2FA9C7364BF6E99D92776EB764D5 + 6FC3B1374BCAB14D927A628BE49CDD2579E7F74A0EEA98165CD807ED97A2CB07 + 24EFC25E1C77A764A34DF1C53D5276719714D66F94ECBA65BADF70CDF9B55271 + 6AA9E4EEAA94EC1D3354F4C02BA89B2B4527964ACED12592716491649F5C2179 + 67D64BE9E55D527DF388545CAB93CAEB7532EBB3E35275E3B0945F39A0227CA6 + 7DF6972764D617C7A5E6B3A3520D55DCAC9319D70F4AF98D4332FDFA2129BB76 + 10E53AF43DA2C798FDE909A9E131D17FE6B54332FBFA61A985E67F7642967C7D + 46967E7356167D552F8BBF392D0B08783F3F2973BFAC97F95F9F9305DF9E97D9 + 9F9F929A5BC7A5F6F3D338E671A9BC794C6AD066D6172765CED768FFF5697CA6 + 5332E79B5332F7DB7A99F9451D44CFE163528BCF39FF9B7A59F4FD5959F2E379 + 59FECB1559F9F33559FFD79BB2E9D75BB2F1971BB2EE976BB216B6B57FBD2E6B + FE86F2DF6FCAC6FFE3535907ADFCB71BB2025AF9EF3765E5FFB8A5F9E57FBF26 + 4BFF764596FC7659B5E2B72BB2FAD7ABB2EAE74BB2F22782D92BB216E5B5BFE1 + 3CBF5DC7B1AFC8F2EFCEC9D26FCFCACA1F2FCAFA5FAFC9269C63EDCFE8FB3DC1 + 31012FDA421B61DFF2F7EBB2E56F5765FBBFDF506DFC15F57FBD249B087871EE + 0D7FBF226B7EBB24AB7FB980CF7C5136E0FCEBD0661DCEBBF9EF3764EBFFF854 + B6FEFBA7B2ED371C0BE75F85BA557F477D00B157FEF59C2CFFF18C2CFBBE1E9F + EB947A54AFFAE98C82EA0DA85FFFEB391CEB0C3E573D3E2FBD5A8FCBAA5F4EE1 + EF3A8DCF5C2FEB7F3A2DEB7E3E2B2BD077C9D727F4382B605BFA63BD6A058EBD + F2A7B3B2FAAFE7651D3F1BBF9F5FDC77B3129F772DFEBED5F8BBD6E2BB5CF3D7 + CB3807FE2FF8FFACFEF93CCE4D6F67FC5DC8AFFEF1B4ACFCF6A4ACFE8E61B8CF + E0FF85EFEAA793F8CC4765D58F27F07FC3E7F9E50CDAE173D2F3F6FBE3B2E8BB + E3B2E0FB13B2F0BB63B2E8DBA3B2F0EBA3911703F8B28095177C7554E67F7904 + 79BE387042F3733FAFD3364BF1772DFC86BF375E030765367E4B73BE3A21F3F0 + FB9A87DFE81CA8F65BA759F8BD56E2375E89DFF22CFC96677E755A2AF1DBACFA + E284CCE47502CDF9CAFD36E77C8B6BE1AB43F8EDEE93999F1D90EA1B7BA4FCF2 + 5655D5F55D32F3D61EA9413AEBC66E99F7D93EA9B9B25DAA2E6E91D9D7764AF9 + B9F5BAAF7821AEEBBC3A5CB3FBE76B5A7064B1AAF0D812293DB552CA4FAF9652 + 5ECFC7707DC39E8B34EBF022C93CB4406D45A8CB47CA7DCACBEA57A1ED4A8D1E + 5051BF5A0A79DCBD7314C6A6EC98290984A4DBAB256DDF1C7D292669CF6C49DE + 5B2BA9FBE7491A5F9439B450D271CF4CDE3D4752A0AC834B247DFF024924C4DD + 5125A93B6B2461EB0CDCA3E9115BAC22704DC3F133F7CD95F4BDF32469E71CF4 + 9FA74ADA8573EC9A27093BE6CAD4ED73641AD2C45D732461E72C89DF56A92FFF + C46DAA9018DCFBA9B88D3370BC6A99CA1784B654A10FDA41319BAAF49991B0A3 + 56C5F2643C37F8FC98BC01CF902DD53219ED27E3793075C76C49C03962D07E32 + 9E3B93D795CB845565787694AA462E2990618BF365D4B222198567D988A58532 + 1CCFB591C88F5E5926A3D74C9791D0B055A5327479910C457B6A18DA0F5F5E82 + F6A5326259A97ADE9A07EE70964DACA797EECAE9327CD50C19B1D281DE114B67 + E03804B2E5327431CACBAA71CE1A4D872C9A218316A26E69A50C46DDE0C53364 + F88A1A9CAF46F383A0A104C2CB2A65C0E27219B804361C8FF9018BCA64E0A2E9 + 68033BD27EF34BA43F347061990C80FACD2B9601F38B643041F092E93817EC28 + F7C318A1574D8EF4ACCE913EB30A30C628901E9539D2A53C5DBACEC890EE3332 + A56765B602E16EA5E9D2A53855BA608CD38DD017E59E18BB30ED847150278C75 + 3A431D0A53A443418A74CC4F960E7949D22E2741DA662748BB2CA499D3A41DD4 + 366DAA7C924C201C2FEDD3A749878C446997364DDAC0F611C6771FC64F91D609 + F1D2269140982FFF4DD0C830EF4FE118720CC690A3E51D8260A4EF057BEDD2A3 + 979EBC1F4E443B42DD712863FCF9C9648C2B0972C78C9177C7B84836F4CAD57D + 7AD1F65D4A5F509C20ADE36230E69CECC6AB18B772FCEA8F61DF1D3F4E3E9834 + 5145184C2F6085C013C7CA5BC19896E28B921CFF0E2A2E5231DF0AE36BEA758C + 9B39D6D6172C0300AC2F510E7370D700F01B434745E02FC5BCEDDF1B86BDB453 + D68EB6970877070E8F80E097FAA30DCAAF0E1E8939C30885BF2F86E12F528677 + 26F47DB1CFE08807F0F33D07CA4B9C97F446DB1E03E4E92E7DE4A94EBD3435F0 + EB7BFE2AFC65DE03C006797DD8DB925EBD1E006699F0D7E0AE79F73225006678 + 68F3026ED196E19F7B4A4B88E1A09F688D799C076D0DDCB66C1300E14FBA61AE + 89361F7652E86B00983683BAAC7B126505BD1F358060CBDF06800368CCF4B177 + DBA968A79857D0FB5E9BDB00B0BD2C6DD0371A04F6E1AF290C7B1F6AF5BECACA + 91B61EF835106C00D8C0AE7AF6FEC50160960D0C470031E5AD53D8DA85AD53D0 + 4699D7EEC3C1FA87C9C0AFAD9370EDC4BC73C36B2C96671DDB30CCB3EFEDABB6 + 40B64E13CE739B317FFDC7D67DB82EE4AF27314FD95A12F3FE9A93A53EFC0DAB + 5994B0CFD1D6C86C7D2C52F7246C5164EB75BF27AEE3E95ADF63CE09C4D4B0CE + 8776CD097D097F71DE4771FC47097A5F92664F04D107D91E7261A251DF1CE7A7 + D88E50B8F98BC83F87BA67917750F7BE600F601F00FBEBA811F86BDEBFC17A6D + 53D0D7D666A9A6EAFD35DC68F59AE7BAB2AE2F435E9FDBFAA1DD9D64EBD5B6F6 + ED83577F4D3C9AED1F917F9EDF578BDF95F1075F619611965F1F4D776A77C7E3 + 789CA6A9FE61F1F3DE6DDBBB15995353FBFD9AEDEE141CA309FE759BEE7B4CEE + BD974CADB1DDFADFD531C2F2395D538AD6EFCFCA3FA67F8E3FA547FEC500F06D + 4097B6BB04C07A0CA6A12FECBF5AE12F3D5A9B3BC9FA84FAFA3FDAA67EBC8DEC + 74A7FF470170A376772FFFA674B78A761C53B8AD7F537D000F9787F0D0FB33F0 + 97F241EF6DA22730E48E1BFD01E28B0F09FFBBF41F1ED1DAFF9EECA1A8E2B1A9 + 70396C0FCBEAF8900D3DA09BD43F190053068075B0E20D5CEE24B6B3414FD81E + AEBF5D0D9057DFACC320CF403065F56158AC29066A0F10FEA28F0F6F297FC017 + 1E18866DBF67E7DB82F40AA6782EA60F104223F507B0775218FE46B3DF1781BA + 1C80370CD27DD86B837093B5A16CA0EEABA97AB3D904C007C0D686F907BCC983 + 3F89F0CB363931B8CB7D806D3263A94D5E7C00EC4F966CF2649326DFEED745A0 + AF97D770D0E1B7765FC5B103CF6003BB4D82614C082DEC93E9B1D73F92C7DF6C + 2DCDDF688D94E1A4E8F9FB91DA9BBFCED0CEE6E1DB007CAD4C20EC7B06D303D8 + C0AF4D68A30160170E9A75DCFFD7858C7EE2FDF6F2E40706801DDC6D0A005356 + CF94D0D7BC7C290B096D61A035FC33ED01F00DCB3C809912E412F4FA9EC0BE9E + EAE03C81D986FBFE3284332130F3EA1DDCAD97825F7A033FD3B93BD433027C9F + F100B0AA7B83E72F45D8FB177A0CF71EE820306130F25C6461F8B597FA0DD4D0 + CFAA01C87BF0975EBFAD868C68048019CEEDD5203C343D03D8EE95C1832380F6 + F5616EEF5FF3F825F8A5140A332CB4E72DEC878F6E3504C7A057EF98D1F2068E + 4311F02AF0E5C215525B30634AB1FEED715C549BA88B66AF8F1A21AF710F342E + BA4D9E20EF41EF729F34A41FC54E910F6226C9FB31139D62910FF41ECAF4AE20 + F8FDC803C00E02C7489B69B1110FE18F20E60981097E15FE0600D817A16E97F4 + 38E9983205FDE95932566D9D03204C584C48CC3604C51D68CF8853F8DB39375E + 3AE5C44997BCA9D2BD9021115314E432ED56C03089C95AEE5B962EFDA66768BE + 7F39F265A9D2BB3809ED92A5770080FBCFC8947E50DF72A639D2B7225BFA56E6 + 486FD8088007CCCC957EB3F2A4EF2C0778FB13F8427D67654B9F9954969607CE + 2990FEB3F3D19640385706CF2F948173F361431D6C03D07EC0AC2CD5C0D9D90A + 8187CDCB9511F3F3653434920098FB0ACF4A5730DCBF260DE74E47BB2CB4CBD1 + FD870980872DCC0B52F4E582F9920219018D5A512CE3D694CB847515327E6DB9 + 8C5D3D5DF393D757419532655D95C46C9E2971DB664BDCD6D9A8AB1486C864BB + 891B2A65C27A86AA7420396653A52EF24F5A5B2AA397E5C9E825DC97B1542130 + F75DA43D664399C4AC2B55EFB0D8F565326D4BA524E1F8F11BAB60AB91F84DF4 + 302324982BD376CD93C4BD0B2461FF4249AE5B2A8950FCC12532EDD02A49A9DB + 20E947364BC6F16D92796A87A41DDF2A49473649F2F1CD92727C8B241D45FED8 + 66493DB54D32CEEC92AC33BB25EDE43655463DC1EF36D46F91041C27A16EBD24 + 1FDDA8D03785FD8E6C94F4135B71DCED7A6CE64D59E89B776E2FB44F72CE122C + 1300EF97C2F3FB60DB2379B0D19E7576B782E1C2B33B24F7F87A2938B14E4A4F + AFD570CE45871749FE8139527868AE141F9E2F25C716C9F4FA95527E769D14A1 + 4DEEA935927F6EB3145DDE29D3AFED93AA9B4764C68D3A85BAD59F1E85ED8014 + 5FDA2BD3AF1E9032A8FCFA21A9FEEC9854DF3A2A15D7EBA4E616F29F1E974AB4 + AD80ADECDA2129459B4AD809806B6E1E9599D78F48F5E5435273F5B0CC45DB39 + 68379380F9C621A9BD7544350BE79C8363CCF9FC84CC429BDACF4FA9E67E7546 + E67D7D566ABFA8874E49F5CD135283FA599F9F945A42E2EFCEC882EFCFCADC6F + EA3565B9F6AB1332FBCBE3329B80F82BD8BF3B278BBEBF208B7FB82C4B7FBC02 + 5D96D5BF5C97F5BFDE900DBFDD90F57FBB216B7FBD266BA055BF21FD3BCAFF7E + 53D6FC8F5BD0A7B2FADF6FC98ABF5D9765A85BFADB7559FAEB5559FCCB6559FC + D72BB21CE535FF764BD6FEFDA6AC826DE54F04B39715065384C16B083F7FBC20 + 2B7E38AF5A0D3BE5CAE764D54F179D0DE91AF45F8FF65BFF7E5576FD9F9FCAB6 + 7FC3E7FCF982AC8336FC7A5936FE762580A41715FAD2B6EE17F63BAFE9C65FAF + 689B7584CA84CD3FE133FD82BFFDA733B2E4C7D3B2F4A7D3B20CE9E26F4FC8E2 + 6F4EC8D26F4FCA926F8FC9B2EF4FE07BC0797EE3E73B8DBFA15E56FDB55E56FF + 7A1A7FD76959F3B773B2F66FE7F1594FCBDA1FCFCA069C6BD58FE764D9B7A715 + DE2EFB01C72410FEFE942CFDAE5E9620BF02ED56E173ADC0FF64C937A770BE93 + B2146D57FC7401DFE525598EBF83709A607839BE871504C13F9EC1F14EE37B39 + 23CB08A7A155B0ADC2675E83BF61E54F04CCA764E50F3C3E52D856FF7C0EDF2F + 7E075F1DC56FE430FEDF271C04FEE1A42C42BB05DF9D9405F87B1722A5E67F8D + DFE0E787A5F6D33A99F70581F071F43D21F33E3F26F3BF40FE4BE68FA30EEDBE + 3A2E333F3B0C1D95B95F9E92F9DF9C96D95F9D929A2F0877F19BFCFAB454E337 + 5B856B6216EBBE3DA3F0B7F2D363107EFFB0CFF98A2F2AA0CF97B07D81EBE0B3 + 4332FBF33A1CF3905410025FDB2533AEEF96F22B3BA5ECE236A9BCBA4BAAAFED + 948A4BDBF059EAF4FFB2E4DB2352716133AE6B5CB727574BC1D1E5920F151E5F + 21C52757C974D82BCF6F92F2731B5D9B5368037BFE8995B827A02DD2B233EB65 + 06DA302D3B837BC4A9B5527C7C8D949E582325384EE151EE6D4E2FDEF992BAAB + 5652F7CE55B04BC09BB4678EA41E982F69071748FAA14548174AE69165927578 + 99A4EC73A1F13370BF4CC5FD73DA8E59326D6BA5246EAFC23D76BA82DF718CD8 + B0BA40E237972B544E2224DE355B52F6CE57F89BB0638E4CDB590BCD91C4DD73 + 15CAC6E23910BB6DA64CDD016D9F29B19B71CFDF522D53B7CD92F82D35128BFB + 38A1AE025F94A76E9F2DF1786EC46E99A5CF8F845DF32511623E66CB4C7DB668 + DDF65A998CF2043C5398C66C9D85E74B954CE4B308E924A66B66C84468CC8A32 + 19B9AC44462F2FC5B387619A515ECED0CA25A2FBEA2E2F96212B4B64089E6DC3 + E8058CF2D025453278719E0C595C10805F177699F951CBA7433364E48A19E85F + 8EE7E3741CBF5C86AF805656E0F85538579542607AE50E594C085B8E6354A27F + A50C5B5C214317CD502F5F07719D862DAF560D5E52A10098709810D8CA9A4203 + 0340CC3EFD17944A9FB9C52AE6071000CF2992DE180FF49B5728831795A8A770 + DFB9056AEB51951D00E07CE93D330FE51CE9863148F78A2CE985F1481F8E4930 + 36E9559E25DD4AD2A56B499A742FCB901ED3D18650B8344DBA60BC4375467DA7 + C234E95890221DF292A53DC12FD43E27513AA2DC31977B03274AFB7402DF048C + AB9230AE4A934E59A9D2213D59DAA54C938F31A6FB686A2CC671F1D2666A9C7C + 1C37593E8C9BE0BC6C278F552FDB772662AC39618C7AD8BAC832633404F37BE3 + 611B472FE071F23181EE648C1BC78F970FC6A21FC6A78C6EA3DEB618A3BE87B1 + 28A3D16838676E6F128C61395EB594F097F0D6C6B5CCD34EB93CB730C1B81863 + DBD7460DD7712EC7BE84BC1F4D992C232B66C8D0B252F41D23AF62DC4D00CC97 + 283986B6483B1C97BF351263E66104BE18530F1B2BAF0F719EBF04B94C098309 + 870DF43A11FE120A0F9797FB1302BBF26B83799C510A7D5F800C00BFD09F7302 + 460BE2BC6184E62903C104C00A817B3999F7AF02E050C867C2DE67BAF68D48BD + 80BBF68B847DA6087F2983BD3EE835F86B00D83C83097DCDFB977504BD94857F + 260856D8FB4917A8ABB46883FAB6B0B541FE63E7E16B22FC35184C08FC14CB1F + 3A004CE86BF0573D7E03CF5F9609731F7DAF9D34FFC0C15D1FF8FA32D8CB3A03 + C08FBED3D6D9033DF27674CF5F13E7CE067E1BE5032F5FCECB6DEE6E0098C0F7 + C157DF8B805F1F02B38DB66DD5007F29BFEC03E0681EBFBE6C2D8329F5C08B6F + A96C8D821EBB5C2BB1F50F13D7467CB1DED6587CE8EBAFB798D8C664A19AA3C9 + BC7DA3ADFFD83A8FD90CEAAAB86F6FD086A9BFEE64EB50CD20AE5DD191C1525F + 3C06A1AF867C0EF2FE9A57781D2C520ED6DAFCB5B83F0580438AD8D5C317E76C + 8ECFFDD8CB9150CF84BA6190AB7BFB3EF6A2DCFB28FA34679F57E4BE4771AC47 + 50F7D8F372BF42DD6715042BE00DE4AF73DA9A68331E2F047FEF567FA44FB435 + 5EB7BE0C5939C8B3FD7D0F3B4583BEBE6CBD3AEC20656BE77EFA67D7D329EBFF + E7C435FD3BCB1844984334C532CCEED747B345B39B9AACFF130CE7F7CEF56744 + E64400EC835F5F613ED5B4F0D99836C1BF6ED37DD101307557FD7D19CFBB1B45 + EBFF67E51FD33FC79FD223FFC200586D84BF770180D9F6DE87E5DE7B786CFC61 + FFAAA1A0FF03E5FF802D1FB1F19FED016093BD85D1F802E5C5EE2B5CFFC76437 + 903FAA68C78AA670FBC79E7C499E7E1E831A3EC460E78DD76FE7DF90A3C9206F + 53F0D7E43F08A23D48A23D60FE91870EFBD94351055B04E65ADE57B8BDDF8E79 + 3E64ED01FD27151E0CFC51D99EBF066FC38398B0FC36772B0E90DCFE190EF412 + EA46BC7C03993D027C83BCD9AD2FFBF9833EA6D1E40F02297FB068790E32A30D + 3C3920B541E99DCABE14120703DA68835CCA07C8F7A2FD3D2D30B80C00B03F28 + 37F983701B88B39DC906EFE1BC49A16E48BEDD6FABF226164D4D3E58E624C507 + BC3699616A7514CB36F9E184C9F6AFF1E12F651329DF6EF93000A66CF2E64FDC + A24DE8FCB778ADCE806F343DFA9A9B58BABC4B9BBFC17D805BCBE3EF50C87B1E + BF94016067431D4340077BFD86E5206F3B85BC0D10387AFD930C99F551077992 + 1EC11F13E47694969F60321EECC96429C36AB19E7B37999E6ED3907FA62D8170 + 57886F7CF30D7086FF425D1BBE05DE20D63DDBBEBBEA998E7CCBBC9B70DF28B7 + 77944B193A8C7ABA23416F4F79A6530F85B904BB84B904BDBACF6F00779F463D + 1506C04FA38F896583BF04C106804D2FF545CA3D84FB0E510FE0E7FBF49317FA + F69797FA0F54BDD86F80AA71D8E7A19AFA65E71D4CEF5D866F1E26AF0F1B216F + 0C1FA17B8451DC3FECCD11A3D44E4FE15707B9BD84098EB56DB0A758AB61D008 + B4518D94563846ABE1662308E6A2D8187973F418CDBF319A8078B4BC3E6A9486 + C0A3B7EF7B932628F46D3566A4BCC63DCF18029AF077CA4479376692A6EF41B4 + BD3D697CA4EE3D02E1D8C99AB2FC3ED20FE2A640CE3B84FA283E46A5F9A96ED1 + F023E4E915DC3E3156DA25C44ADB693148097A27A807303D833BA6C448A7D458 + 957AF842CE86B66843110613FE7684AD4DCA64699F11275DF312A55B3E1730A7 + 42D3A40BCADD0B53A45749BAF4284CD5BAEE052CA749EF527AF766489FD20CE9 + 5D16E4A767482F2E9CC2D66B7AA6F4ADC895FE95B9D2A73C5B7A96394F1A2EAC + 523D098367E64B9F1A0869BFDA02A850FACE2E90DE337375C1960BB7CCF7838D + 8BB7BD6A5C7ED0DC12E9370BEDD0A66F4D9EF446BBEE33D2A467653ACA5932A8 + 364F86CE2B94A1730B64C89C7C9473A45F75AAF4AE489101A8A76DD85C685EBE + 0C9F5F20C3A0A1F3F264D802A48BB8E05D2C239633C424F2D0A81565326E6DA5 + 8C5F5B25A357CF9091ABCA65DC866A99B0AE5AC6A03C7E0D212FCAD078D8988E + 5D53216356CDD07434DA8F45BB49E83379638D8C437EF4F21219BDAC44F74AA4 + 2731356E759982E5299BAA2566530D542D13D757C8A4F595BAB83F859EC69B67 + 49FCD6B9AAA9DBE74BFC9E453275FF1289DBBF5CE2F62D95D8BD4B240669DC81 + D592706883A41ED92AA9C7B748CAB16DD00E49429A7074AB4C3BBC19E9664924 + E085924E6E97B4533B25154AAB7769CAC91D927C6CAB24D66D92A4C31B15FC3A + C8BB4D45F04BD84B2FE02CF4A1D486BE59F5BB61DF23B967097DF74BDEF943C8 + EF577BD6A9DD9279E680645D382C5967F64B06CE9D7D729B149CDE2145A7B748 + E1C9F5925BB742B20E2E5558547C72B54C3FBB41CA2F6E91E2F35B24E7F4461C + 67B3E45ED82505570F48E19583EA595C7469BF945DAB9319378F49096C8517F7 + 05E5A35279EBB8547D76422A50577AE590945F3F82F249A9FAFCA44CBF5E87FE + FB15002B0443FB8AAB87A5F2DA51A9BE714C66A2EF6CF4A56AA199378FC05E87 + F4B054B30F3413C7ADFDEC94CC8266DE429B4F4FC8BCAFCECAFC6FCE49EDD767 + 64D617A7A5E673D44173605BF8E32559FCF31559F0C37999FFFD7999FBCD19B4 + 3BA5606EF657CC130E9F43DD45B4BD2C8B7EB8228B7EBC22CB7FB9212BFF7A5D + 96FF7A4D56FC765D96FDF5AAE697FFEDBAACFCB79BB2F2DF6FC94AC2DFFFE333 + 5985FC52B45DFCF35559F2CB35E4AFE93917FF7C4D56FFDB67B2F5FFFD5EB6FC + 3FDFC86A1C67258EB30AC761BAE2972BB202ED56FE44087C553D8C57A3FF6AE4 + 55A85B8DBA55F85C2BBEBF24CBBFBB282BBEBD28ABF1376DFB1F3765D7FFFDA5 + 7AF4AEFDF9A2ACF999D097A19F9DD6FFC650D0D7905EC671CE2B685DFB0BDAFC + 7451B58EF0F9870BB2EAFB73B2EC87B3B28080FCDB53B2FCE7B3B21222002630 + 25FC64F8ECE53FD6CBDADFD0EF6FE88F74CD6FE765E56FE764E5DF90FE7A5E56 + FC7A0EDFD95959F6FD697C4E025982E28B38EF255986F32DC139167E5F8FEFF7 + 8C6AF1776C8B7343F4EC5EF475BD2CC1FF66F1F76765D1B767641ED2C5F88CCB + F09D2EFF09DFD92FD7F15DE17BC5FF7021DA538B088E7F38274B71ECE5F81BA9 + 65F80C4B7E226C86F05996724F68FCDD4B7FBA80BFEFB4D47E89DFD6D727D5E3 + 76FEF767F03F3F87DF067E03A89B87F32FF816DFC577E764EE57F5EEB7F8397F + 5FF5EA71AEBF4BFC8E99F22504BE40300BBFE3AA4FF9BBC76F18BF5FC2DE1A9C + 83BFF76AFE0EBF3C0DD54BCD17F8CDE2F7361BBFB79A2FEAA502BFDD4A1C83ED + 677ECE7AD8BE382EE55F1CC3B571526A78CD7C7A0CD7549D945D3F88EB0BD7DD + E5BD5274718F145FDC2D4517764AE9C55D528BF64BF1BF9D8FBFA7ECF26EC9C3 + F55A508FEBF7CC562939B3498AEA374AC1C97552786ABD94E19A2EBBB44D8A90 + E69F5E8FB6EB54B9A7A1936B25F7C45AB4DFA02AC6B5CFBDC50B4F6E92FCE3EB + 25E7F00AC939B25CF28EAF929CA3AB2415F7C2E4BD0B25EDC01249AB5B2A2907 + 9748CAE1A5927470B1241F5A82FBE132493FB242F7414FDEB74812F72C504DDD + 395762E961BB61BA4C585B84FB70914CDC508AB458266D2C93D8AD1512B3B942 + 623755A9F76DD29EB992803E09F4FCDD3D4FFBF3651C3DCE8E5A99BC6D96025A + 425C0776E7B8FBF576D4233F6553ADC46CA9D57CEC9640DBE749EC4EA718E4A7 + 6C9B2B537620651FE46376CD9729A89B8CFCA42DB365FC861A19BBAE0ACFA62A + 3C276AF07CA9C167AFC273A75C462D27F0354D87CAF14CA3A6AB864343574C97 + 21B00D5F3E2380B6650A4F872F45FF95556857A1FBF56A38E7E595780E56AB46 + 2EAFC2F1595F2543611FAA79D89754A17D15EAAB718C0A05C0831796CBC0F965 + 50A9E61DE4AD709EBEF40426DC45BB010BA7AB0C020F5A5429FDE69549FFF9B0 + 2DAA9661CB66C9D0A535D20FE5DEB525D2674E99F49A5D8C7C31F225D21BE381 + 1ED5B99AF69F5B2C7D319EE88DB1425F8C297AD51448AF6A8C1FAA61AB298490 + 56A15C9907E5635C90273DCB73A4E7F43CE95E9A2D3DCAF2A45F5511C612C5D2 + BD3C573A95664AE7E9D9D2A92453DA17A44BA702940B33A5635E9AB4CB4C9636 + 50BB9C14E9909B261DB35331764A950E19C9D23E1D63A9ACD406000C5BBBB444 + 699334553E9E1627AD13A7AA17F0875363E4FD788C05E326C97BB1EE05C177A7 + 4C9077186E1963C60F3066644A10CC3CA12C01EFFBE3E90D0CFBB871F22EC6A6 + 0A7F218B524311DC5AB41A2D072F373AB88B738C1B0FFB581DE352CC3B8D8940 + E237098031BEA5DEC418D78EC3FA4FE8513C65B28EA15F1E3254C7D0AF33EA4E + 704C03C27AFC1163301EC7F879984B5B0D190561DC8CFC1BC3C7AADDE06D249C + 73FFA1EAD94B11E6D2BB97FD98D203F8797AF8A28E00D8BC8029F66D159C8BED + 09809FC57C8422F0A59EEDD15FA54038D8DBF769EED34BC0DB9D5EC1B075EF27 + 2D3B733E84B94DB08F2F01EF73F40CEE42F8DB0F732702DFDED2B2032130C33E + 13F412FE12FC3214B4AB37BBD5519A6F8F7E1041EF939F7493E61F7796C73EEC + 24CDE9B9DBBAABB480CDBC7DA908008614F29A3D80BD6119E4D5325205B9EFB5 + 8B78FB1A0036C01B96416003C38F077A0C758FBC45C8DB26F2A2F49D64F097B2 + 3D7EC322E0F501B0D914FE06737EA6FE5A81A9299B7902FBA2E7AFBF26C197D1 + 9F78E3635DAB68F6FC1B119BEEDDFB9C03C0B6366270D7D64DC2796BCBF51583 + B8067AC36B3161BBADCDD0CE50CE16CED95778AD8736AE3DDDCFB684BF9E086E + EFB33490AD6785E50360B3717DCB2F876575DAEEA957E4A1A75BC9832D71EE27 + 9D7307D7EDFC35395B97F315AEB7753E5FBA06A85EC15C537C59BD7F1F68FE8A + 3CF804CEFBD80B28DBDAE4F33826DA3DF19CDCF7B8F3027600187FD7E3F85C8F + C286F60FE1BC0FB57849EE7F1CED426BA1E175525D538DB2DE4ADDCDDA6D5300 + D8B7FBEBC04DCA731C62391AE8F5E5AF559BEDFE879F8944CAF46170B435F2B0 + 9A6A67C7B8537D34FBED724C211A57F0EBFE5119DB30456B4385DBFDABAA31C4 + 85CD6352772565599085806ED6D81132C2BDC2BA2F04807D18EAB5B963396CBF + 1B85FB7ABAABCFEDCB3FA67F8E3F2572D31098FD8FD21F07C0D4C3D043417A87 + F6C131EFBBF751FC18425FD8BFA0FED03FFC2EE51FEBB6E3F29F6D003872F134 + BC69D1F802E58DC457B8FEEE14ED06F54714ED9861B19D42DBA04CAFDF675F7C + 4BFEC201171E56F7DC87EF38745C5FE19BB61ECB20EF1F00C077ABF043E7EE1F + 344E8D00EFEF09C7BE4DB4EBFF14A9FF86D63F287F90F047D410EED9A9A9018D + C91FF4FC9EFC37E3DC9B720EEE5284B9067F6D4F5FBFDECA3E1836084C451BF0 + F932F81B6D9068E5F060D21F601AD88D660BCBEA0DF01AE46D4A1CFCDE1702C0 + 36508F0680ADEC0FE6C379ABF707F93EF80D2B7C8C883D984CF8930E5FFE64C5 + 9FDC30CFBA26DB05E0D707C04C2D6F65DFEED771D2A5DEBFC104CD9FBC510678 + 297F32473DD28A13C08F906F780B98934293015FBF1C81BFD0634C39010D60AF + 79FCFA65F30A66086803B9E6EDEBBFDDECD7513E006699F057EB827D930CF63A + 6162EED99E6AED80B083BE98D00750D88979D78675D4336DBB2AE80D835F13E1 + EF731DB8B754F70800666AF9166D710E026585C43DA5654707789FEEDC4BD396 + 1DBB2B10665867DBF3D7BC7D6D9F5F867926F4554F6082E300FE1A00A6E7AFE9 + C5DE03E479E8853E28F7192C2FF51D2C2FF475C0D7E02F81F0F37DE80DECC23F + BFD47F30ECF40A761EC1AF0DE5E2CF08E40984B920E4C02EF314A12EE1EE1BC3 + E90D3C026D5D3B8AA1A369271C56F84B2F6286921E81F2487A348C9256A87B8D + E078E468791D692BD68D72F097F5B640F62617C8B880367182426002E037C68D + 51F8FB3617E862B8903759012FE1AEC1DFB7268E8B4060825F83BF0685B9F047 + 00CC94D0977903C21F4D8D0920B00B09DD66EA644F93A4EDB4490A81E915DC39 + 8D9EBD535506821D0C9EA2F0971EC0F40EA60887DBA6C54887CC78E9929B0825 + 49478683260046B97B41B274CB4F96AEC81B2066B947618A42E11E4550319526 + 3D4BD315FE12F6F6999123BDCB73A4CFF42CD833A41BEA1B0070BAEEBFD7B73A + 4F7AD7E429D8ED4D183CCB895E3AB4F58AD4B994509880B87F6D91EEE9D70736 + A6BDAAB3A5475506DA64AAF770FF59391A1A9A207800BD8A67674BBF9A0C889E + C20C119D2743E614C890B9F910C34653B9326C41A18C584A2FA93219C53D1091 + 5AF8CBD12BCB65DC9A4A190311008F5E57256391E71E885C8027F8656A6279CC + EA0A19857E635657CAA40DB3246673AD4260C2E0B12BA7CB381C6722F2F420A6 + 26AC9DA10078F24602E099EAD53565534DE0E55525B19B66C9D4AD732469D722 + 49DCB950A64153772F926904C007964BDCFE6512B377B14CD9BB5426EF5981FC + 5A4938B459928E6C46BA49A61DDA28D3904F3CB6552170D2F16D927882DAAE00 + 98E564E4A944D61D838E6C91A4C35B24F5D816493BBA59D290A61E657E8B64A0 + 2FA16FEED93D927D7A97825C8697CE3ABD5B32EB1B94757A8F649DD92B99A776 + 491A8E9B7A8CB0799FA49D3B88F3EE92241C33137D73A182FAAD52707C83641E + 5C2EE9FB974AEE915552729AF077AB545CD929C597B64BCED9AD381ED20B7B25 + FFEA2129BC76500A2EEE95C28BFBA5F46A9D945D3B22C5970980F74BC995430A + 842B3F3DA170ABFCC631B439AC6D66DC3C2E336E1D9792AB07A4E80ADA5E3B24 + 15845F0CFD8CB635B74E6A5AC5FED0CC5BC714B435E8B8CCA257F18DC352751D + C2B129F699F9E94999FD79BDCCFDEAAC02DFDA6FCE2BD49DF5E56999FB2DA1EF + 2559F0C325ADA36DE61727352CF5CC2F4F0670CEA916F5F3BEBB20F3BEBD28F3 + BFBB2C4B7EBC264B7FBA228B7EBEAC5AF8D36559F6EB4D59F177E8DF6FCA2A82 + DFFFF129F20CFBFCA92CFBEDA62CF9EB75E8862CFDF586E617FF7C5D56FCF6A9 + ACFFBFBE968DFFD737B20A6D56FEF59AACFA15F65FAECAF29FAFC8320A9F8F10 + 783581B102E01BB2F6AFB764FDAFB7641DD2D53F5D93953F5C9615DF5D94E5DF + 5C90D568BF15E7DDFE7F7E2E1BE8994CCFE00000D39B98E194D7FEF58AACFFED + AAACF9EB2559F61DC3399F81FDA2ACFDE58AAC83363004F52F9771EC0BEA59BB + F887B3B2F8A7B3F81BCFE3F3D1E3F68C2CD2BD934FA9D7EDCA9FCFC9AA5FCECB + 6AD4ADFE8D9EB968F3D78BB20C5AF1EB25E4CFCBD29FCFCAD21FCEC9F2EFCECB + 8AEFE9D17C51FFAEE5F8FE96E03C8B7FC279702ECD7F7F4E967E47008CF2B738 + F73767701E1CEFA74BB210F679F8CC8B7FBC80FF01BEF71FF15DFD721DDFD575 + 59FA23EBCFCA2286CCC6DF4DB84CB8BB946DA1253F9D53C0BCF0FB53484FE37F + 770EFF878BAA05F86C84FF0CBB4C2FDD39DFD4EB79087F677D85DFD117F4283F + 2DF3F11B9AFFCD59FCAE4EE2777742E610FE223F939EE50A85F97BC56F15BFC7 + 8A5BF83D7E7E54416EC5CD23321DB6CA2FF01BC671AA3E23E8752F44547D76CA + BD08C11717BE3C23355F9CD66B60FA8D237AADCCC06F793ACB3CCEE7A750774A + CA718EE93CE6CDC3FAD204AF9DA2CBFB70CD4117F748E9E5BD527675BF7AE197 + 5FA16DB7149EDDA99EFEC550C9D92D5258BF49728FAF95BC13EBA4F0CC26293E + 8FEBFEEC66C939B55EF24EAE93FCD31BA500F63CA627191D60BD149DDAA8A1E2 + 0B91E6E11E917B74BD641E5A2E99754B25FFE41A689DA41F5C2AC97B164AEA81 + A5927E78B9241D582C09FB71BF8498261F42FDC1250A7DE3B7D5AA576DDCF6C0 + 6B77EB2C896574860DE53265F30C9471FFDD5281FA6A99BA93F53512B76DA6C4 + EF9823F1E8A769007E1371CEA4BD0B1D4CDE3D5F6276CE91387A07EF9A8FFBF4 + 0299B603E2FD1A294170DCB67938C67C7C867912B705F96DB0EF5884BE0B1C04 + DE0E3BFAC5ED5A883C8E17C06042E0581C67F2D6B93201CF83F1EB6BF459E354 + A3CF1E3E77F8F2D198557CF9C8EDD13B6A259E452B18A6B95C862F2993A14BCB + 64F032AA5C46AC20DCADD4D0C90CA13C6CC90CB42997618BA7AB5C68E72AF5E4 + D5FD7B971100130457CBD0650EE20E81862E2C572FDFE1684B8FDFC10B087EE9 + 9D5BAC1AB400C7473B7AFC1A00663867F3E22500EECF3E38C6C08515D2772E43 + 3BC336BF5C062FAE52F599532A7D66174B7FD411007727CC9D5524BD307EE859 + 8531C14C42DF22B4413AAB10638562A4C5B0C336D3A504C104BF3D2BF8F218DA + 4184C0BDCAF3A57B29C619D30B64400D3ED3CC32E9519E275DCA72A41BD2CEA5 + 59D2212F5D3AE7664897FC2CE9989B2EEDB252A46D668A74C849970E282B00CE + 7222EC6D9B9AA0220CEE9881766949D23A315EC7726D92A649DBE404F918E50F + A7C5C807105302611B0732728CBF6D084500FC3A01EC98D1F201433813EE8E62 + 149C118D0030537AEA12D6362ED38B9780789C8E696D7CCBB130C7BF2CBF3D76 + AC83C618D7BEAEDEBF2390A2EF181C03C7D3AD5282633958DC702C0781711C9C + E3B59184C63806C7D70C013D0C6D86630C3D629CE61B40B0931FBE99320FDE88 + 37EF409737F8FB0CE616CFF26552E4FD70CF4C792C9E8329CB0A7FD1DE3C7F0D + 003FD703F96EC8D39B17A9DB03B8BF3C8D32A51EC004C301CC0DCBE0AFAF30E8 + 351908B6728B763DA4858681EEA9DEBE04C04F7CDC551EFFB88B83BFB0A9776F + 10F299792BAB4730217110BE997A0AE596B0130C2B1C0EA0B081DE273EEC288F + BDDFFE360F6083C006797DE86BA97AFB428F51EF702EDC56CB770380097D2DF4 + 7318FA86651098D097A058017064FEEFE6FCB66EE08B36DFEBD7F2067DFD32D7 + 2E6C7D82EB104FE16FF80BBEFB67F1B7FAEB170F31F473B0F6C135105B0BF1D7 + 4AFC3A5B6F6199A90F7F596FEB2A0679ADCE6436CA07C0FEBA91BFDEE3EBFE67 + 511F0500DF1BC8E06F64EBB200DC36A5C89A58508EB616E61FE3C1A75BC9A3F8 + 1B2883C0B61E175EAFFB3D59FBB0E8017CCFA368F3E8CBF2D0E3AFAAE7EFBD0F + 3F83F43979B0C58BF8BC84BFCFA0DD33383F8FF517B957C340BFA200F8C1C7F1 + F73FC93C3E8BAE973E8DBAA60170C4A9E611076C4D8DD65DEF62EDB6A93ABFAF + 2FBF8D9D53EB7C086C6BD0E175E9283220EC835F1F004713D7C82DBD93C27DFE + BCA233046E43C9D4670DFF887CE6E12B5A5B5FD1FA987EAFFE3F523EFC55FEE4 + C35D4F77AA7372F53E00BEEF0194C3FCCBCAF745F1006E04441B8B6D5DFB40D6 + A75994BA3B29D2EF76D9E7F615AD5D44FE31FD73FC293D22FF6900389A14F2D2 + BBD787BA1119FC354587C0F7367B145F1AFE1885C14C435FD8FF9693077E4D7A + 8145B9409D7083B8637D74856F4276530CDBA9A6FAF875BFA7FB70236E861B79 + 339C83E19F5B60A0F074E0017C9F77ACF0F1A9263F5718FE8600B0EBF7C7F58F + 3E747E17F052E17ACB9B3D28EB03D61EE0FA80F6CABEECC11DD48707023C0E07 + 34FE00A049A1DD7D482383923B0C5C347C49687013963F08BA2F34286AA6A155 + 0CFEBA90CE3E00F6BD8059BE13F8F5017078B0E77BFAFAF20786E141A2E539E0 + F46D2683BA4D29DCC6FA45DB4BD81707BF7C03F2BE962FCB3DF8BBEF69D10078 + C36A0A00DF49067F4D3EF0B5B25F6FB6072D2CB40770A3C99FC4D804C6EC4CC3 + 7DADAD015D4EA24CBE47B0AB43FBBFD0F6162652CEC6895658B4FB13369BE4B9 + D4C15E077CEDAD5F4EF6A2816107820D00DB44D107C111088C496803F07D5F9A + BF4DE8CBFC7B5A7E94FB05BF8D366FA36D007A7DF990978A56C7BCD56B1B0F02 + 53CCD3E6036083BF4C59B6762696FD76067B097F7D4F60DA9E6987C92CE12E52 + 957904236F309870F8E98EDC7BCA81627AFC3ED3A597B4E8D85D9EEAD04D41EF + AB7D07CB6B7CA39E8B27F4F00D00B0815EDBEFD744F0AB7B65751FA0A19EE9F5 + FB421FBE710F7B0FF441AAA1A0FB0D107AFFBED8AF9FBCD49FE541F222DABDD8 + 97A1A00D000FD17DBAFED28FFB828DD0107194ED1BF60ABD02B88FF0A0212A86 + 85A607B0C2DD40AF0CE23EC1831D1C26FC1D3152C1EEAB23E8EDCBC5ACC0BB77 + 54007E478E457E2CD2315AF7E66896B978E616B974A16B0CFA406F8D1B276F8D + 1F2F6F8D1D2B6F8E1D2DEF4C1827EF4E2200761EBE84BA0A8491BA32C1EF445D + FCFB309E8B7C5314123BCFDF29DA8665F30C36206C00D85286106C9DE0EF0BEC + BC82696B9334453AA4C54BA78C69D2313D4EF7F7759A2A6D9363E413D4B74B8D + 55B5499E8CF693908F938E990910FAA890CF9A2A1D73A649E7DC04E90475CE4B + 942EF949D2B52019F924E99297807C229424DD0A93559A27102ECD925ED3B391 + 664AD7C234E9529822DD8BD3A4D78C6CE95D91A3A115197A910BB4F400EE5595 + 2B3D5126E025EC250426E855185CC37290471DBD84FBCF29927E1A12BA40D597 + F6D9DC1F98A1A2919F1568A603C4036AF364D0DC021934A75006A11F3518C719 + 8CF683E9210C0D9E5724C31672EFC472191168D4F20A19492FA865D365E48A72 + 19BDAA52C6ACE6023B3D7C6B20425E96AB642C85B2DAB810BF61968C5F3753CB + E3D07EC2FA99D02CB4AB44196D5755C978D44D441B6ACA460788A76C99259337 + CF94491BD07E1D3DD250074DDE305B15BBC979FE4EDD4E30B040627740BB16CA + 94DD4B2566EF0A89D91768EF4A89DBB726F0DEDD2C530F6E90B8FDEB344D8C40 + E02D0A7DE9EDABD037B025A23D9544F80B251FDD2AA9E89352B751920F6D94C4 + 43EB71DC0D0A82D34FEC50B04BF84B489C55BF4B3D7FE9E9AB7628FDE44EB4A3 + 087F3749DA71EE33BC4FD2EA0F481ABD8103CFE1F463DB1402E79FDC2299756B + 25BD6EB5E49CD8288567B648F105C2DF7DEAD59B7B1EE738B753B22FEE979CCB + 8724FFCA7EC9BFB817DA2FC557EB5405970E40FBA51022B09A01955E3F2CA5D7 + 087F035D45196D4BAF1E82BD4EA65F3F22F40C2EBB794CCA6E1C95F29BC7A514 + 6911EA4AAF1D429950ED78441A4E976175BF3C89E31F451FB483CA3F3B2A555F + D64BF517A8FBFC54C4A3975E960A75912A1466F90BEEFD8BB69F9D545047EF4A + 7A64B25DA41FD239DF9E5770BCE0FB8B328F1EC3F406A5BEBF208B7EBE224B7E + BD2ACBFE764D96FFFD065217EE79F96F3764E5DF3F457A5396FC7C4D96FC02FB + 5F51FFB38396CB151CDF92E57FBBA96D56FC8D90F8AA2CFCE5B22CFCF9922C22 + F4FCF1A22C2200FDEB5559F9DB2D59F5B75BB202C75DFED76BB2949098A1A97F + C0F9BF735EB16BFEED7359FDF7CF518FF3D03B16E7607E851E9F21A9713E9417 + FD7059E67C7556E67D735ECBABFFF6A9ACF90DFAF596ACFEEB4D598D73ADF805 + FD780C4BF1372CFFF1AA2CF9FE922CC6F9967E4F08CBCF70D5D5E1FCCB14EA5E + 91C5FC5CF47CFEE9AA7A4E2FC1F996FF40E87C59FBB1BC18EDE85D3DFF878B32 + 1FDFE302A40CB9BDE8BBF3A8A77730527CD7CB7EBC20CB7EBAAC7573BF3F270B + 5036AF6AE7998D63A1BCE827025D1C1B5A8CEF6EF14F1764E10FE770FCB3AA79 + E83B5F3DBECFC93CD8791C7ECF0BD06FCEB767F17F3F1DBC0C50AFA9CBBBDF10 + 3DC7E9455E8BDFC26CB4A3ADE6B353527DEBA45431BCF867E8837A7AAF975E39 + 80DFE3619CE3AC2CFAE5A2CCFEA65ECAB927363DD709716F9DD097220881A972 + FED6217AFE120657E0773F1DD747C9657AD61F9462E6D1BE04F692EBC7713DF0 + 9A380C7B9D145D39A4D71AAF3FBE84517CF9807ADFE79FDF8BFBC00EC93BBD53 + 0A2FD043789F1430FC3B6CF9F55B25F7C466C93ABE49328E6F944C5CEFD9F55B + 70ED6F926C28EBE446C961192A38BB4D0AA1BC539B25FFD416BD1F641E5D2F59 + 47911E592F69752BA0E592796C15B45A520F2D93E4034B24FD30EC5012F2F17B + 16C8B47D8B9CF62E94A928C7D29B76EB1C89E1BD74D71299BA7BB124EC5EA010 + 975EBD897BE66B7EDA9E7992B07701FA10BC3AE01BBF1DFDB6CE9629DB9C62B6 + D72AB84DC4F193F72D96C4BD8B515EE400AE6AA1C4ED708ADDBE4862B6E17EBD + 7581A6B13B5C99F6F89D4BD076B14CC17D3D068AC7678A83A6E0334EDC42CFDF + 05C82FD2FA295BE7C9A42D7365E2A65AD5383C23F8DC19BDB65A3516F9F17CE6 + E0D932612DA34EE0B984E71743380F27045E3A5D862E6508E62A191E78EC0EA5 + F7EDC2E90A6E07CD731AB2006D83F0CD4316CC405D850C5B5C29C335B473B50C + 59522D831656C820F681B8EF2F0130F7FB25FC1D30AF54FACF2D511100332434 + F703B63D81AD8DF31276A19CFBA1EDE005552857A05C2A7D6BD17FDE74CD13FE + 52CCF79A59283DAAF3A5674D818A79C25D425EE7E95BA8F097809879B3F7ACCC + 93EE33A0F23CE95581F3559548DF8A6205BF5D8AB2A55B49AEF49E5124BDCA0B + A55B692E6C59D2A534473A97E448FB820CE9989F29DD0AB2A54B6E26C64969D2 + 0162BE734E86B4CF4891F6E9C91843A529F0A597EF47D3E230C64A924EB07548 + 4F51F04B08DC26799AB44D4A908FE8059C10271F605CC754F3181FDAD8F04348 + C792185FBE8DF1E61B6346CBABDCCA64E408791765EAED31631DB41DEFA02DF7 + E97D6B2CC6A98107B06D5F4218CCB1AE1BF33A19FC7D75D87015CBEE25478C75 + 71CCD7D19F0098F0F7CDE018F4F2D576A347CBFB132749EBD83869133F553E8E + 89C5B926C82B4347380DC1F1868D925791BE327898BC36147D46E0B80100269C + 35B84BB16C10D80FE34C110413F29A088115FA5A19F301825EDBEBF725D81836 + 9A5EC49C4768A8E75E0D7BFF12FE3ED39DF312A498B370DE6261A0097D9FEED4 + 1B73227AFA62DE025958673FA4B30A650BE1AC619C03F9E19E2DE4B35FA70A42 + 40DBBEC00CFFAC6AD3D5ED13CC3AA44F06F0F7D9F698A3B5C37C8DFB03C3669E + C084C1B6D7AFEFF96BF096DEBD2A82DF10DCF501F06D621B88E19E097A1B3C7E + 9D5CD9015EA6E1B0CF0FBD85F97B007F09729B02BEBECC03580130E7F3AD6E07 + C0FEDC5F6170006DC3A2A7AFAE2F78369302DEBFBCA3EB10CFE1EF7FA3535F79 + B54D0F79F2757CDE00EC72CBABE6AFE273BC82F378EB2526B3717DC420AEAD9B + 18E8F5ED2696A301608AD0F7DEA71BAF11DDCDBA51180013F85AAAF9A79C37EF + 43CFBE761B00E61A97BFDE1596B5F3DB9B0889E9FDFB303EF763F8DB28E6151E + 87D6D5C26B73A6701B6B17C9435C37E4DEBEF73DFE927AFE3EC42DDE3494F333 + F2508B67E4FE275AA2BE25EA9F45DBE7E49EE6CF6BFBFB9F78551E7812DFF793 + F81E9BE36FD6FD809F917B1F7A12F9A7D1B6C1B9E5B635515B5BF5D651752DF5 + 11D8B94ECBF55AE8B6BCADD106F2D77A7D5B34F96D22E70B3E83B609AF3FFB62 + 9DC9B3F990D67427001C6DBDDC177986E5FF68DFA6C5634687C056F7CF509879 + 8415AD0F15ADED7FA50CE81AF8F5654C2AA27B1F97FB9AE1BBA5B39A82DE40B0 + DFCB3A1E27681BF600BE8D7D990C7CFAF9FF0C853F8727FBCC04D706AFEFF877 + 448E8BBE770BA09BD423FFD50038907AFC7A659507809B51D1DB11FE3602C0A4 + FBFCE3A27D79FF9D651796A74617A07FB13673757AC185EBEEA06837A1A6F47B + FDA3D5DF267C4E42DE07F170BB9F0F07DCE49BE1C6FC201E687C0BA7191F4E0F + E3C112EAE79F27ACA8F097FA2F06C0EC17F5611928DC3E52E7B7B53C537C5FE1 + 07F96D6A02FE461EECD686B2B2D7CE1F8CA85817D4ABDD1BB81800BEE711D4B3 + 9F0773FD76BE58E7002FDB35EEE3CA0DB67B83BD7D2983C02603C3067D7DB1DE + 526D0BD920EF4EB28162386FB250337EBD2F7F20EAABA9366A436A83E2F00098 + 3200DCACE5ABEA017C4F0B825E0ED49B06C1BEFCC17B345B18EEDE8D22001883 + 701FDCFA32BB5FEFE7C3F2DBEAA42700BD84BEBE0C063B11003BD9848B132DF3 + 0036D904CD974EE850E7BC7C9DB72FC1AF03C056D7583EECF5651EC0BE9A6332 + DAFC6D425EEEF51B847B26F07D8B10D889E5473179D5379631A135A8EBCBE0AE + 415FCABC83ADFD93A8574FE000E09A67AF0160CA07C04C598E26D699D8D6C0EF + 93A8A35AD01E82C09627FCA5985719F0F5E0EF0B5DFB28E0A5F72FBD80E9FD4B + F8FBFA806111006C5EC03EF4352F60CAF7007E51175B06CAF3BDFBDF0E80FBF6 + 97172221A0E905ECBC7D993A00CC059FE1F2523F17CE4D178706D173979EBD5C + 1C1A26AF0C24E07561A0097B750FE12034B401E057073B30FC3AF21AFA991EBE + 5CF40A802F61EF9BA3C7213F465E1B315AC53C6D5CC87A67DC04B1707684BFEF + 8C1DEF3C1A180EDA20B02E948D564FE07727A0CDA4F1EAE96B22CC25F035B0CB + F07F84C0068099A7CDCA0E163B8FE006EF5F0780290780E3549F24D293242EA2 + D6090EF2B64D813D2546E12EE12FD336C9316AB732F34CDBA3DC21639AB445BE + 5D5ABC96DB111A674F95F6D92867C52B042600764A94CE792C370060A66A677D + 618A742D4A93CE0569D2312F0969B27A01F7653868A8677996F498912DBD6AF2 + A577759EF4A8CC91EE15F4E2750098F0D78582CE53F59F532C03E6162BFC55CD + 2E9401D040DA6B8B14023365B93FECFD089003F84B103CA0B64006CF2B9621D4 + DC22193C87F0B7D0E5E715C990F925326C61990C5F345D862F9C2E431794C9E0 + F9A5326269858C5D5523A35757CBE85504BDC8AF22E49D29E3D6CED23243659A + B40EF6711B6B65C2A6B93269E35C99B0BE16EDD9CF2DBE0F5B5AA6E9E81515BA + 283F71DD2C99B481A140E749EC8EF91AFA73D216F45FEFBCB918DA73F2C659DA + 6EC25AC2621C7F0DBDBD66EA39266E9E2BE3918EDFBA4026ED5C2671FB56C9D4 + 43EB64DA81B59258B75E528E6E91E4235B64DAC10D127F60BDF302860882E3EB + 362AF065A86782607A07C707755402616F1DF7FB451BB44D6548698682869209 + 835977782BCEB14DA12EBD7B35C4F3E93D924908EC836085C0DBA12D0A84534F + EC919413FB24E7FC4129BC724872CEEC96F4E35B25E3D866C93DB955957772BB + 149ED905ED94BCD3DB25E7F42EB7A7F039EE2BBC470170F6C503927B81006A9F + 02DF22867EBEECCA6653F00BD14EA045F84BE06520D80058D98D23524C788536 + 79570F4911FA14DF3C228537EAA4E4C62199FED95199A1E1708F4BE9CDC332FD + 669D5471AFD5EFCE481543DD7E86FEB7EAA414E98C2F4F490554F9B90BB93B8B + 90176915BD8B039847C0A7F920A5A7710DDA130E13FACEFEF26C4473BFB900FB + 79A9FD16E56FD037D0EC6FCFC8DC1FCECBBC1F2FC87C0F28122E2E612867C2DF + 5FAEA9C7F1C2EF2F28145DF6F33559F6D35559CCD0D2BF40DC1BF8AF5765C9AF + D764E1AF5764EECF1765CE4F1764C12F97643E8EC563130A2FFD9B83CB8BFE7A + 19C73F2FF37FA057EB4559FC03BD5FCFE3F81765D14FD7652134FF87AB68734D + 16FD7C43CBD4925F3FC5796E695DED371765F6D7F8CCC82FFBF53359F6CBA7DA + 975AF6CB2D59F9DBE79A2EFBE9862CFFF1A62CFDEE9A2CF9E68A2CF9F6AA2C85 + 167D8DCFFDD525590CDB2294977C775D967E8F36DF5FC167C1B9D17EE1F74821 + 9E83F9A5A85B82F32E86167D4BD88BBF0F9F59BFD7AFCFE13BC6DFF09D83C004 + C0F4E65D4C20ACC2F7F0ED397CFF67D0E77C007CAFC83CB4A7873641326D0B61 + 5BF0E32599F7C30508FF1768CE77675584C773619F83E3CEFA1AFFBFAFEB71BC + D3E88F3638F71CFD9F0770173210CC547F1FF6DB81ADEAD39332E33A7E8BD78E + 4147A552F7983E051B7FD7750A73099B97E0FFC9DF0C7FE70C7DEEBCDF0F2B00 + E60B0FFAD243702DD06BDE3CE6192A9DE53CEEDF7D19D702CE517CFD98A68557 + 8F48C1155C4FB8468A995EAA93FC0BFB712DEF91025C9385970880F76954005E + D76E3F7002E15DEAE99FC57DC68F6E76910470CDA71F477A6C93649CD82CD9F5 + B8879C628879DC0B70AFC8ABDF21F9B807E49EDA21D90C3D7F6493A4E03EC4BD + CD994F3FBA16F7A415924CF00B25D10318A9D9120F2C9184FDF4025EAC20386E + F7FC8862772E90A9BB9748D2FE95D00A49D9BF444343A7E118690797E13C4BD5 + 833891FDF72D9278F653A0CBFBF45C99BC758E4CD88C7BF3E6D91AA6397ED702 + 99B6679124EC598C7429DA2D429B05324541EF62949748DCAEA538EF12077209 + 82B7B936D68EE9A42DF3358DD941D8BB50266C99A7F7F8099B09825DFDA4CDF3 + 705E3E2B9087F82C18BB7EB68CC63382627EC2C6399167903DAF46D2DB77253D + 7E5DA8E6512B662AB0259CA5087C097E07CE2D8D0060A7197826CE80AD5C06A3 + 3C747125847E8BAA64E042EEED8B36D070D88731B433C3392B1476C735B11D21 + 313528F00EA6C72E9FF1546F3CCBA981F3585F8D71C074E933AB0CB632E9595D + 823143A9AACFAC52948BA47B25C33BD3ABB7487A5615062AC098223F5001C616 + F9509EAB63995EBDD373A4DBF45C8C430AA4CF0C9CB7BC487A94E64BD7C26C55 + 8F12B429CAD170CF1DF333A453519602E08EC5D96AEB96877639990A753B67A5 + 4B976CB441BE6D4A22C659090A7A696F9F96ACB60E483BC266EA909A24EDE8FD + 9B10AF80F843BE140831AF658C17194586624499B7268E9737C6611CAA63CDB1 + EA5DFB16C6A4EF4DC0387112DA4D9824EF4D9CA85B92E88B8813298C55312E35 + 004C11D8722C4B78EB00AE8B6A43106CDBA0BC1678F0BE8EF1AD9ECB1BEFB26C + FDEDC5488692FE684A8C7448C4DF342D51DE9F3845E1EF4B189BFF6530C7E16E + CCFE1A150A016DC0D7A02FC7F9067F4DE60D6C00989097B0D7FAD8FCC0EAFDBD + 7E097B29F3F82504F6F7FE25005675EBAB1EC0160A9A619F9FEAE0606F785F5F + DFF61C15050253EC4FB06B8A405F2862F31481BF812276EEFFDB9AF33ABECCDB + 4315F1020E42429BA76F18E012F23602C010ED067C59B636BE22ED19029A6D82 + BC0F7EA987DF6CECE5EBCB3C7E4D3EF46D4A84BE84BFF7BFFC8E3CF0CABBF208 + E6F09CDBDBBCDFE0AFAD05503EE0B5B50913EDEAFDEBD96D8DC26FF7F8EB1F29 + 047EFA6DFC8D7F7957D73A0CE472CD43B7BE7AB131FCB53511DA7DB8EBCB07BB + BECDF7F4B57A2BABD72F016AB0A6E3AF11F9EB43614503C0BE6CCD8AF0D70FDD + 6C22CC6D0A00372583BC4CE9F5FB083E3FC5BC5F676B6AFE9A9DC95F7BB3B5B8 + B0B45DA066283FF4C4CBF2F0E32FC9038F3F2F0F3CF1AC3CF0E45372CFE38FCB + 3DCD1FC7DFD0129FED39F471B0B8D993ADE4BEC75AC9BD8FBC0CA1FCC88BF2D0 + 63CF412DA5D9A32DE4DE479F8EAC81365A0FA5BCF556E61B01D930F4F5F368EF + AFDD9A6C4D37A2D05AAFC9DAFBE7D73AAE1FFBEBCE10CF19B1F9EBD25E1B5B2B + 8FB666EEDBCD43D8AF6F4AD1FA311FADEDDD09FF0B280C7F1D1F7075FF0C85F9 + 4534FDD97EFFA96A823D39D6D4984D29E88D56472746CAF248A341D3A800D5C0 + A79FFF8F56F83384649FFBBF37008EAA87E5DEFB1E6A10F7F88D728C7BD58B38 + 00C30A81832F29FCC5FD77977F1179F22F42FF8273F93F7FF3F93D453B0EF57B + F58DD40C9F136D19FA999EC0FA3705370EAD7B10C722BC8DD2D7CE135654F84B + 11FE3E0CFD0300F81F111F54F6706CF4C0F4DA4453A3B67E7F3E84BD07785445 + 003084B23DD8F5E14E4581BFE1768DDBA38EF57C9B2D34786904803990690473 + A343E0C66DA2951B6C6100DC08EA7A009865A661595B2A3C208CA6680342CA3C + 867D001CADBD0D30A30D34FDF68DDA04835C7F90EC8B0368EABEC0BB97F0F7F7 + 003007EA7EDE14CDE683DD68F668EDFE0800F6272A94DFD6263D36D989F40F26 + 4F94415F1F0037E4097E2D7593AC30FCB5899C03BFEFAB1CE8C52410F9300036 + 384C19F835DD0D00B6F2E398AC3EF12EF7FD75A19E4D0A7D0300AC30986F2FA3 + AD4D707DD01B96B5A1C2E1A10D001B046E0A001BFCB536BE5D81AF0788351F88 + E097623E027803B1DDD36DBACAB36D7BA89EFE84FB073B18FC5CE75EB7016002 + 5E03C0F4F47DB9D76079A9C740CDEBBEC00100F6451BF7006E897A0B074D00AC + EAC53DB6A0A04C00CC3221F00B7D600F20F00B7D07AA1A20B05BF0B1FDBC6C31 + E8656A206DCE4BD8423FD3F3F7A5FEF41E1EA4E0F735DA087C297AFF0E653A2A + E2054C104CF8EBBC7D038F5FC82030ED0680B968668B65DC33EDED71E3228B62 + 5C107B7B3C213061303D2F5886268CD590CF0CEF4CA84B592868BF4C19F8354F + 61A7DB01B079031B04A64D417052BCB4499906C5CBC789689BC0FA29D23A71B2 + 42DEF6E9095AD73A2956DAA54C958EE9890A81DBD3533833413A65A19CC1FE84 + C66C8FBA8C78E9903555DA65C62900A63730BD7FBB11EE16D013D889D0D7A070 + 97C264E9847CFB9C04E9904BF09B0A1106A76A58E87E5579D2172200D650D0E5 + 99D283E0B792FBFDE6689867F3F0A5C7AF857FEE3FA7500130ED7D61EF53430F + 5F7AFB12F616689979426005C20A909D17B1EB4B385C2883D07FF0DC12193207 + A297D1BC5219B260BA0C5DC4C56BA4F47EA227D2BC121934BF54862FA990D12B + 6A64CC6A7AEECE96716B6A65CCAA59327AD54CB58D411A01C0ABAA65243D83D7 + CF96F11BE7A8B8003F6EDD6C05C3235631F425CEB3B45486AD289751ABABD453 + 6B0C3461636D0003E609437B4EDC8C7EEA41CCF0CFB3A0D911F8CB74F40A7A22 + 57CBF80D68B77EAE8C593F4746213F7EEB4289DDBB4ABD73096FB90730012EF3 + 04C02AE4098263F7AD95B883EB65EA61D41FDDA2F0370EEDE350C77A7A0B334D + 401F1E231175C904C068C730D084C2765C7A0BA71EDF291927774BDAC95D9242 + D1F337F0002600562FE093DB25A37EBB7AFC261FDD817E5B25FFC20129BD7644 + B88F30F7184EAA5B2F9927B749EED9DD927F8E619D0F28F04D27343ABE0DC7DB + 29D9E7F74AEE25EE217C5032CEED93ACF3FB24EFD241E880E45DDC2FB917F649 + CE05A4E84B7BFEA5435278A54EE16EC9D53AF5EE9D7EF39830C4EDF4EB84BFC7 + 44C346A3AE88EDD1B600B6925B27A4E4D3E352F429DA41155F9E94CAAF4EC9F4 + CF8E4BE1B543927779AF94DEAA93AAAF4F49E5D7F5520A7BC1CDC310CEF5D931 + 5519FBE1188477046BE5381F434F577ECA90CFE8C710D3847B10C35473CF55F5 + 00FE8221A0E9097C06657A019F9359DF9C9399A8ABE1DEAD5FA19E7B0B7F87BA + 1FCECBACEFCFCBEC6FCFC99CEF2EC81CE4091CB9CFF0A29FAFCA5CD8E87DCC3D + 85177E7F593D6217FF785516D00B9740F2A70B520BCDFFF9B22CF8E5AAD4FE78 + 51661256A23CFFAF57A4166D78FCB9BF5C41F9AACCC37109150943E9D54A8039 + 97A189F139677D7D019FEDBC547F7D5E6ABFBF22B3A1EAAF2EA8667F873254F3 + F54515F3F37FB9258BFEFA198E731D7F278EFB35BD8E6FC8E29F6EE1F35D9785 + DF5D9745DF5E93B95F5E92399FE3337E85CFF8F51599FBC545A9FD0CE7453A1F + E58568B3086D99CEFBE6BACCF9067DBEC567FD0E65683EA1308E3DFFCB0B9A2E + F80E7FDBF738DF3717715E7C7FF8BC731866FBFB8BEA65BDE0DBF332EFEB73BA + 873335077F1F3D7467E2FF3FFB9B3332FF874BF8CCF80CF85E19A67B2E01F18F + 97A596DF35FACEC6FFA6167D6AF13FE1773F0B657A71CFF91EDF25EC7C31807B + 5313B4EA0B02FC3F1BE8D590CC0D7BF5F237E13C77F1BBE1EF0575FCDD12E696 + 5F0F429A43F4DC2DE76F0CED66E03767A207BCFEB6AFE03AC03552886BA3F8F2 + 2129C1EFDCC2A1335F886B9100975EF2FC9D96A08E70B7909057BD7E097F8F05 + 3A8232FA5DE37EDB47B45DDEF9FD927D66AFE49CC5F579761FAE5B57D690F0C1 + BD2003D77EFAA96D927C6CB3A41CDF22A9B8AE9379DD1FC6BD05B6CC53B847F0 + 5A473E15F68CE35BF57E907902D73E41316CA9B8BF3900CC36EB25B96E35EE53 + B8EF1D5A25D30E2C8756483CF741DFBB44A6EE5B262987D74812EA688BD9BD04 + 5A245351376DEF5249D8B75292B85F3AD284BDCB25697FA00384C904C8CB2461 + FF5299B68F9EC48B2576D72205B884B3F4C89DB0792EEED5B5327E13EED15BE6 + C8E4ED84C40B256ED762B4A1F72E537C965DCB9D762E935894276F5F2293B62E + 0AB450266E99EF5EE4E14B3D5B16AA7DC2E60528CFD794E097F9711BE7417365 + 2CEEF7D4F84DF3B49E62DDE875B578B6E019B516CF2B3C1BC643E3D6CD91316B + 66E319350BCFA81AA4109E6B2397E3F9B562B60C5D5C2D03E73BF83B7C51A50C + 5D304301B041E0C1F309802B5483E733B433B4907BFC56C990C5352E5D58A9F5 + 4399A26EC0BCE91001F00CD41308CF504F5FC2DE013C2E9EB304BE8CEED1BB06 + CFEA48B8E6422DF7AB2D43BB190A7F097B7BD514638C80BA9984C0C50A7F1DD0 + 453E50AFAA124DBB4ECF972E650CDD9C279DCB72A55349B6742ACD916E0CF15C + 5E205D59579A2D9D4BB2A46B49AE742FC598A4345FD5A3384FBA17E74A8F121C + A328473AE4398FDF4EC877869DB62E84C4F959EAF1DB213355BD803B65A76BBE + 4D6AA2B44E9E863114C64281C72FD531231563AF4415F3AC2328FE705A9C7CC4 + 10D0146130528684D6170215024F917762181D668A7C101B2B1FC6C21E1B2FEF + 4FC17870E264F96012EC9362E4FD0984C00D5B92BC83F127F33A2E65741A8E4D + 315625ECE58B8CE6F96B657BC9D1F70866F86682668E755F1B452F6017D6D945 + CA6900C0B68DCAFB182B7F307E326CE31400BF3C64B846D97963388E31DC857E + 36E8CBB1BC815F0261CA87B9B4FB73009B071800669E6D0CFCBE14B4631DBD7F + 0D001BE88D847B8E0680BB72DF5FA76721425D7AF61AECF53D7EAD1C81BFD0B3 + 4D006085C0ED9C67AF7AF712E806F0F70942DD366E2EF614BD7D3D11F82AE065 + 1FDA3E76619E15F4B6E64BBF0EFC86E1AF415D3F35F930D8EA98F7F7FAF5C570 + CF9190CF2813009B2730CB7F04003FF406E6E25160AF2FDB135801F02BEFCAFD + 2FBDADE0D680AFFF6278C39A81B3694A05EB12940F77C375BEB41E29D72C1EC6 + 791F0AD63A0CF81A98E5FA88AD85D87A88BF5E626B2A2683BAEC67A9C987BDD6 + CEB7F900D85FF789A6FBA107AC4CE81B48F31EFC35D86B6B56267F2DCBF2261F + F4DEA660BD8C70D75F3B63D907BE66B3B2BF66175EABF3DB98CD64FDA8FBD18E + 219C1F78F45979F009E8C9A7E5DEE68FCB3D8F3D22F73CFEA8DCF3C4E3D2ACC5 + B3FA373DF014BE9716AFA3EE65B9F7617C96C75E91071FFF8B3CF4C4F3F2F0E3 + 04AC2DE49E475BEAFAE76DF2E02BD514FCF5C1AF49D768A3ACDDEA9A6E13D037 + ACDBFA50B6FE8CD43F67641DDAD6A6436BD43EAC0DAF69FB75D10070B43E660F + F76BAAEDDDA9852A0C7F1D1F7075FF0C45E31761FDD97EFFA96A823BF9F63FA4 + 1000F6795754806AE0D3CFFF472BFC1942B2CF6E00D807C1D1DAABECB8E173FD + 61E1FE1315AAFE27A931F00DEB617CB807D10EBA13006EA44721F7C7D9177BDB + 97F7DF55C145E35F7861F917AAB3FDF99B4F538AD6FFCFC88EC5B49986AB76DE + C084C10C07AD9ECB768368E2BCF6997C4585BF14E1EF7F2100A6220FCD90A2B6 + 0D425E44DA316F62190FBFF003FC36D903DC1EE650A387BCB5A1FC7E81ACED6D + 7D1E811E83CD1BBC340580FD367756E3E3B9B26F7B5E1586BD14F37792B5B3B6 + 36B0BB93C283421B3C3655EF8B03CBB0A2B5A31AB5E3C035180CFB03641B0447 + CA4FBDF28700705877AAF7E16E584DD51B04E664C39F94F893105F3641B1C90C + 27389C00F98A4C7C9A98409938917AE045F3FCC5B1207F4F1D5F0673DD648E13 + 3B4EF6087B1D007EE815867E72F23D804D3EFC6D0A0087F5D86B1F49F3B79C07 + B00F80996FEC01FCA1347FC7C1DC680A035FDF46206CF0570130616F007C0D04 + FB40D852825D53B84C19FC8DE42183C0E6E9DB486DBA46F68222F87DA64D7797 + B6EB21CF75E8292F30BC59875E2EAC181739A0961D5CA8B2673A35BCE5AE8B21 + 1D61EBDC53812F61B086810EE0AF8910D8BC8015F606DEC02FF4EC2F2FF61AA0 + 62DEA0F073BD3C181C5110A2AD2FD4AF21FC33F5321779507EA9EF10D807CBCB + FD07CB2B03874AAB41C3E4D54104C22E1C3401B0825FC8F6FD7D73C468A45C68 + 1A25AD8632FC3317AD1A435F2B7381CBF642E3C21817B72C349E7A018FA36785 + F38CA0F7AF0160B7E8E63C2FE885C1907D04BC1F4E992CEF4F9E2C6F4E70FB00 + 9B87AF0F88CDE660B00B17CD9490D760AFED09FC7EFC64B767DCD429BA5FDC47 + 09B1D23A69AA7C923C55613041EF275432816EBC74C8488C00E0B6A9F4F64D74 + 901762B8670260E6097F3F498E51184C7BFB2CB4CD76D270D04118E8068FDF24 + B575E4BEC10C0B5D9802A54A87DC6494931500772D4A976EC5E96E5FE08A1C15 + 43411300734F600D074DCFDF9A3CE93B33DFC1DB5AE7FD4B004CF5517BA1F49F + 5DA421A3192E9AD097ED7B57E7AA0C08F7AF2D447F28F022EEA7DE4584CA8532 + 9061283D39D05BA6DE4A04C0260D65B9A85C46D89E87CBAB65F44A7ADE3A003C + 6259958C5C562DA356D628F41DB5A626A231EB6629041EBB016D918E5A532D23 + 56E218ABAB64C42AA60CD13953A1C198F5B364244349234F6FAE099B088E9DF7 + 30EBE9B9450FAEC9B04FC2B126C336855E5DEBE7C8C40D4837CCD745FE71EB09 + 05E6C9C46D8B65CAEE1512B76FADC4EF5FA72915B3778D025DC25A7AF6B23C65 + CF6A893DB85EA12FD39803EB64CA7EB445CAB604C4148F33F500E1F17A05CA49 + 756E4FE169873649EC7EF6DB20F1C8271DDD2EC9C776C9B4C3DB65EAE16D32ED + 18F714DE21A9277608C344A7D243F824433F13F0A0AE6EA34CC5F9B88770C185 + FD1A1EDA8072F2B1AD9276669764D1CBF7D27E493BB74B92EB774832FAA5D18B + F0C21EC9BAB44F322FEE978CF3FB258BA0F7729DE45CAC93CC73289F3B20B917 + 0F49DEA5C35270F5A8A6F9970F4B31A1D68DE35274FD88145F3B2265846706B0 + D0BEE0C201055945B0177F5A2F45B74E4A1EDAE55E3D2C45F492FCF498947D76 + 428A6FE298570E48CEE57D927FF5A014DF709EC254DEF5439273FDA0E4DEC0F9 + 6FE058375C486942397A21975C23803E128173D315463B110A5641153807F3AE + 7C4A616FF597F552F5453DFA9C9472D4733FD66A02D7EF1CFCADF9F6ACCC54E0 + E820632DA1ECF71715FE123EB26EF6371764EEB79761BB2CF3BEBF22737FBC82 + BE17A4066DABE995CA7E3F5F919A1F2FCA8C6FCF685AFBCB15A9C6F12B50AEFA + 016D150E5F9099DF3A204DD8C99450BA922188BF3E27555F9F97AA6F70CC6F2F + 217F51CABE382BA59F9F918A6F2E4157A4E22BD8A199DF5D93B93FDF92DA9F6E + 49F53797F1F711C65E9639DF5C95DAAFAFCA6CE691D67E7545667E7E51AA3FBB + 20B3BF64E8E8AB32EBF34B52F329FE36A4B56AC3DF83B67351379BF55FD3F3F5 + 9ACCE139BEBFA1C7645FB6E5F1E6A08E00BA1A9FA98A803A00D2FC6EB85FF3EC + 2FCFC8EC2F9C1736BDB5AB3FC7F7FFD9499981FF49F9E727D00FDF0FBED3AAAF + EA91E2FBE57788EFA4F2CBD35241604B988BEF44813D540DFB2C86F3C67745A8 + 4B984BAFDB327AE03244F9A7F89F33FC325F14C0EFC2FD06821703023B7F2BA5 + 7C3181BF6105B0F84DDEC4EF11BF6B425AA6AEFE28EAEBF09B3F20F9F89DF265 + 8502FC4E5926A4CD3B4F4F78FC36714D145E421D7EFBFABB27EC6548757AFE5E + E40B1575527019BF69B4A1F2D187623E57EB1B6C7CC92207D74FD6D9FD927916 + D7E6997D9201A5D7EF91D453BB5569C8A79FDE8B3CAE67DC13A894933B25897B + 8D1FDE2429B8EE791FA0F72F61305F08619A767C8B83C248D5462F60462038BC + 51920EF3BEB40E29CA4736E27EB75A26EF5A26317B96E3DEB50AF7985568B75E + 528F32743DEF890C8D0FFB81D5328DE0F7E05AD5D47D6B2476F74A89D9857E7B + 564AFC5E5FB8A7EEA156A2CD0A994C80BB63894CDCBE585FBA19837BEF28DC9B + 476D982D6370AF9EC8F0CCF4E445FDE41DCB229A4205E077C296850A74C76F5A + 209371DF260476B077BE960981C76E20E8754098FD98F23EAF2FFCAC9D8D67C7 + 2C05BE63150A3B38CCF288D53323750A84D7CCC6B367A60C5B518314CF2CE419 + F679084335338473006887233F72F92C1946AFDEB9D365C09C3215BD7E691BB6 + A85A062F202CAE9001F4065E582583695B483B8E050D9A87BA39D3A5CFEC523C + 7F0971CBF18C7540B8F72C17B2992199FBD596A0BE4443325BD8E67E28DB1EBD + 6CEB423EB35D99825F425F4B7B541445A06F38DF757A81742E2500CED7B45349 + 0ED25C05C284C3CC6BB93447BA94E44AD7E21C8C517234ED8E324340772BCE95 + 4E8539D22E2F43DAE7674AC7C26C5587822CE944F89B97A9FBFEB6CB4A95F6D9 + 692AE63F494F924FD212A51DF7FBCD4C95766949187325214D56E0FB49D23461 + 38687A047F42009C38553E4C99261FA641A909F231BD8219221AFA3861AA7C10 + 1F27EFC5C5CAFB71F412860DFA281EFD62E215FCBE37618ABC379110385661F0 + BB1331AEF45E46A4073043371302BF358E2F2CBA31AB6E6312005C03BF4C6DBC + 6BDEC09AE7F877F87079056360DA345434D2568CA683B1722B8CB35F6578E721 + 4C19EA19EDE8F53B9CE19E39E6C6783AF0F835F06B2F7452B4B3DE80AED97DF8 + 4BF01B960F8475DF5FC8C23B37057F7DFB33B045C06F77CC57B80F7030DFB1B9 + 0FE5E64398D7E83EC00E02D3EE036083BD6185BD7A5B06619F7DD06B30D7E4EF + F51B81BC41FAF8871D55B6E7AF1FEED940AFC15ECB5BD93C79AD8E1EBE0A797D + E0EBE529F3FA6DCA13D879033706BF26F3FAB5D0CE067BEF28CCD119CED9F6EC + F5017034E93A00C5BCB70EC1B2AD3958DEAFF7DB58DD837F795B1E78E14D79F8 + C57722619F0DE6FAEB23E1F5142BFB7683BAD6C6970F7BC38AECFB1BC503D814 + 5E2B22007EE069D89E7E45EE69F9F26D7BFE9AC25EBF11FB530DF201EE1DBD81 + 43EB65B61E67762B47D6D220B3DB7A9D9FB77ED1646D2DFF003EDF838FBF20CD + 1E794A1E7AF269B9FFC9C7E59E471FC2E77E541E79EE29FC9D4FE1EF7B461E68 + F91769D9EA43F9A8EB50F9187AF2D977E5FE475F96879FC477D0FC5969F6784B + E819B9B7F933F81CCF2AF08DAC9B06F2D75B097EA379FE9A9A0561A129BF1FA5 + EBB4213565BF4D5C0FB675674F7703800DC81AA8F5D7B47DBB01DC3F0271AD9F + DFFF6EFB46570B5518FE3A3EE0EAFE198AC62EA2E9CFF6FB0F57B310B8BD831A + 3129F29B7B9A37709CC01E694FBBD68558575332F0E9E7FF3314FE1C9E7E0F00 + 8753951D337C9E3FAC47F0FD4505A9FF02BA5B0FE0466A00C02AFBC2FE1BCB7E + 608D2E2C4F512F58156E2077AC6FACF0CD279AA2F5FB8785CFC8D0CF84BF1AF6 + 999032B801DF7787BF31FCD94C51E12FF51FE401ECEF4DF07B6AF4B0C483AB49 + 85DB9AFC3678F8457D78FBB20779D0F6F70608D1D4641F42E0D0E04515D81BC3 + DCBB51B80FCBBEADF13EC03E08B6F46E6510F74EB24160587E3D078836D08C26 + 7F306A8AD62E22B6F106CB96B74170A4DC04008E3668F7EDBECDAFB3D487BABF + 27BF3D01F003DEE4C32623BEC213124E5C0CF2860130F7C131081C9E38F9132C + 4A27527C9B3680BF4E0DF5BE0CE49AF7AFEFF1EB6C0CFD6450D801607B03D852 + CB9B0C044703C26AC384E0D137907FE33D79EC2DEEFFEB40F0E3EF7CA865DB07 + 98DEC004C08FBEC3379D1B26B53E0436991770D8E6878A366F5F13A12F41AE01 + 602B9BB7AFC15ED69B1AC15FA44FD0CE7200817D59286803C02D5B7775DEBF10 + 3D81097E9FEFD83BB2A7942D4C302508F6173FDC5BEF8DBD7E5F20D825E0F56C + 14BD7F0DFCFA61A11B0160CF1398B69714FA9A06E9028E890B3A2FF71FA620D8 + C2B931FF727FEEEF35D4ED03ACA018E5814314041B04A677B0C2E0A1C3E5F5E1 + 23E58D110EF672614A53E8D5612323A2DD89E5E1683342183ECFF647631A5924 + A32744E01DE116D5823D80278E6F00C0EA093C4EDE43F9BD4913E56DC8802FE1 + 2EF3F4F425F43569E83FD8DE994C4F610780297AFDB2ACEDA74E918F13E314FC + 326F40F8A38438B51304B74D6548E7046997364D812FC12F2130537A0433D433 + 433E1B04A6CC66E58ED950CE34154340DB5EC0F404A63A29FC4D083C7E519F9F + A4DEBE84BE5D0AA95415C341772F49971EA5190A827B97674BCFF24CE95E96AE + 699F6AE7C14BF521F8ADCE919E556853992D84BB84BE7D6772AFDF0294515F45 + E89BA71058E1710080A9BEF4080EF60B664AAF61E705CCB092C5EAE1CB3D0E09 + 7F55F34B23C097A2D7AF8900D820B042DFE5D5320C76426286B91CBEA25A86AF + 42BA12F5ABAA6484C1607AE9AEA3B715DAA38E621BD6110E8FDB3847E8F1C5D4 + DA72F19E8BFDB4D10BCC815D17469AD250D1500CEAE2B72E92D8AD0B65F2A605 + 3261E37C99B8911E648B14304CDEB55C26EE582E9376AE9029BB5749CC1E0291 + 9532197985C207D62BFC9DB87B6504F84EDEB74E26EE5B2393F6AE9629480D06 + 6B3D2132FAD11B5843431FD828B1FBD6CBE4BDE8B77783C4ECDF247187B648FC + A16D127770ABC41EDC227175DB64EA91EDD036493ABA559219661A690AF71B3E + 06A19C442FE5836B14E2A49DD826C947362B004E445DF289ED92747287A4D4EF + 50109C7A7AB7A49CDE23A967F748FAB9BD9271611FB45FB22E1D90EC4B0725E3 + E201C9BA7848B2CED749DAE9FD927EE680E45C3C22F9578E49C1D563927BE9B0 + E45D3E2285D750BE76442C978B1F0000FFF44944415472D087A1A3098D09B172 + CE1F508FC5BC73DC4B987B0A1F95DCAB10FA645EC4F1E94D8C3E05F4A0BCCA3E + 07149215DD388CE3A1FFA5FDAABC2B0725EF3AEAA19CAB2E9F7FF590F6D37D89 + 514FD10399C08F50D83C3399B24C18A8FB0F130EDF746090E0976078C6E7A7A4 + 14B66242657A7912327E73566A0867BF3E2DD58491048DB0299C856ABE3E033B + 442FD46FE8097C116D2E380FDCEF2F4128A35D25DB7D8BF63F5E94CAEFCF4B39 + 8E57F91DCA3F5D922A02E0EFCECA8C6F21D8CBBF382533E8A14A2F65426888E7 + 2DFFEC944CFF1CFD50AEFCEA9C547C791E9FF1BC947D76464ABF3883BE17A4F2 + 9B4BA8BB88BA8B52F5CD157CF6AB285F928A2F2E48D59797A4E6ABCB38D625A9 + FE0C9FEBF38B38FE659C079FE1F30B0A80D586F24CD45384C0D5B7CE6B3AF373 + D8BF44FF2F2E4A358FF5F515FCCD0E04CFFAEA2AEC57D197F510EA089D2BF03D + 4CFFF21C3EA7FBBCD59F9FC3B9F0F96F31CC323D72F1BD40336EE16FBB7E5CF7 + C1E5CB00A5F8FF947F71127F2FFE8F482BF0DDF37B29839DFF237E3FAAE0FF46 + 984B084C304CA8CBFF6B29F79BE6B1905A9EFF73F54EE7FF5761B0FB9D58BB62 + A48504BEF82D175D3F2125374E49F1CD13F89DF1375E073B7EE7578F38407BD9 + FD5EF9FBCB230CA62EC08EEB24FF023D760F49EE39FC262FA2DF257AF1E218F8 + FD67ABEA240B6DB221BE54917976AF64D3C3F7FC3ECD679CD9A369967AE0D3CE + 172F701D5C40DFF3B866CEEC73E0F7E46E49E73559BF4F924F12FEE23A261486 + 52509F1C0060BE54625EC1294899E79EE20C11CD880304C3AC4B3ABA493D8613 + EA703F3AB44E350DF9A4239B349DB207F7B5DD2B703FE15EE35B351AC2B483EB + 347201C3D6C7E31E17BF7F8DD3BED5B89FAD916910CB8C9E10BB97F9B5A873E5 + 38B48FD9B342EFAB31C1B127EE5822E3700F1E877B303576CB02198DFB32A330 + 2804DE502BE3713F271C9EB86DA94CD88EF6B83F8FDBEC20EF58681C3D7AE9E1 + 4B6FDF6D84BB8B70FF7720978098ED0CE0D2BB975098E9183C13A89184BAAB67 + A946AD65BB390A867DFB8835B364F89A99327455B50C595129439657CAD0E555 + 320CCFADA1D0E0A555323800C0031756C8E0C5553214657AEC0E9C373D028107 + CEA50730BD7CAB61AF947E73CAA5EFBC72E9C73D7A1506B37DA50C9A5F85676D + B9425A7AED12DCD283B75F2D8170B1C2DFEE8C0042C01BB4E9338B1EBD0EECF6 + 651FDA6616EB9EBE7D663BBB83BE25D2A38A9EBFC51AF6B97B4581C25EAA5B39 + CA330A23790260AA5B79A174A63730D46D066CA8A34730C3386B28E7921CE942 + 18CC94E19E09768BB215046BB9305BDAE5672AF4ED5098256D0B32A46D6EBAB4 + 870CFEB6CD4C51F8CB32539669EF909D2E6DD393E4E3A4A9F21144CF60C2E04F + 5212F4653DC26082E0D6B07F04DB87483F4C419E7B02274E433F82607A08C72B + FC7D372656DE9912A362FEBD49C88F9F2C6F8F9B8CF165AC7C3865AA7C08FBBB + 1327603C3A4AC7A33AFE840C081306BB1715E919ECBC807DD06B1098296536A6 + 1CFFBE32749827EE173C525E1B4A0FDE512AE6B935CBABD4607AF13A9947AF01 + 5E025FCA202F45BBC15FBFAD0F800DF4522C47EC1EF0B5BC81DF488867C840B0 + C2E0EE48BB39EFDFA7BBF491E7BBF693173C004CD84B1BCB067CFD32A5A01832 + F8EB83DE8837AF6733286C522F5F82DDD6DCC6A7412D3EC17C8D101873369DBF + A18DB6858DF504BF11101C05FC5216C2D9CA3EB8656AF504BE61599D0A65F3F6 + 6D00BEAEACC7D17A077B7DD1EB9722F8A557EF9D00700410638ECE085D9CA7DB + BC9E36A63EF4251C3659E8675D4F6019B2B2AD4D98DDA29399C2B6879812E23E + EF40AEC15E7F0D25BC6E12B645ABD3886CC10BF9666F047BA340605B23BA3F58 + DB31355A2B82087F1F6C89764FB7927B5BBE1C01C084BF7702C08DEC1EFCF5D7 + C5EE06008775A775B5F0DA1DE5AFDD85DB59DE4FA9FBF1B91E6AF1923CD8E219 + E82979E0C927E4D9562FCB875DDBCB87DDDA4BCB37F05DB4781A9FFF39791BD7 + 667ECD52995EBB56DE7ABF87DC73EFD3F2C0E37F91079E7841EE7B0C6DA8E6CF + E2733C1B1500EBDAA9B7E64ADDF71052C2CE8703F01BE4FFC30030C4F385D568 + FDD9D7036E7D9B3036ACF01AF81F691716DB19F8BDDFBE87BBEC1B5D2D5461F8 + EBF880ABFB6728CC2EEEA43FDBEF3F54CD1A43DE680A73295523004C1BFE2E3A + 25726B5255D0BF990747EF249F0B5AFE3F43E1CFE1C9F89C0F7E4DD1DAABEC98 + E1F3FC61FD4F0E80CD5BB8C1F6BF01B0CAFBDB233FA8D0C565171D2FD0E8370B + DAEEFE26E2DF787C456B7B9B7883A0A2D5DD85781E7AFD1202EBDF05312434F7 + 056E861BBCDFCE575337EB46D097F2F3FF04006C0F1DFB8EC3F56145DAFBE2DF + E529F2800DB7F3746FD02EF2408EF2E06E24CFB3373C40F0D5A80FC57E5494B6 + A6680316EDFB2701B01FEED9790EF318761CDA1A80AFA929F07B3F066D0F6050 + 48316F763B8641DC7F44E14124650349935FE70F549B1AB0120073B06BA0372C + 1B4037C340BB1906E0CD30107720F8D54683F6B0AC8D3F988F0CEA516F2907FF + D164C7895617112612FE64C3F2267FB2A293146FC26379CA60B0C25FDA820996 + 4DAA9A96F302F64340DBA4CCC4499E9BD439F8FBE86B1FCB136FB595E66F7CA2 + 6583BF3E00F6816F18FE523EF8B5B0CF14EDA6475F87DE44DB37087B9DB72F01 + 70F3B75DE8E7C7DE72505801F0DBAD2393DAA63C7C59B63ACA07C0D6C6C06FF3 + F7DB4600B0815D03C0067F9B02C0940160B66F8EE3F800D8F602F6F70336E86B + 00985EC0CFB5EBA97A1622FC350F602E647041C3DE6AE702872E72B04C75B93D + 0CB4EF016C00988097A97904FB21A109830D1213069B77F05FFA0C52BDD47B30 + CA2E7DA9CF109706B61791FE05B697FB0E9557FA0D9157FB0F55BDD21F3605C2 + F4081E2CF404566F602A00C2AF321CDD302E648D94573CE0FBEA7094B970C545 + A96163E4D5A1A3E5E52123E5E5A1C322C0970B60BA371ACAB648A60B6723600B + 16C9744FB6B1A3E5ED7163E51D86E043FA2EF75D1BE33C2FD44B7802433E4F54 + 4F5F02DEB72735780013FC120033CF3AE7293C51C1AF79FFB24CCF60C25E867C + 26EC65FE7D7A0227C446C410826D92A7A908820981097D1DF8B532530785CD1B + 989EBF2683C01DB20882132310D88030816F7BA4EDB84F30CB79B0E7274897A2 + 64E951922EDD0BD3141677E6BEC005F4067672E1A033A45779A6F49C9E213DA0 + DE9539D27F6681F4E39EC015391A1ABAFB8C4C21E425F435E06B1098E09722F8 + 250056CFE159850A7F7BCF72FB061B00EE3BBB50FAD5166818688675A608802D + AF5A305D862C9A21C31657C8882555327C49A50C4579D4B26A19B372B682DFA1 + 8BB9CFE17405C643088A9756CA906554B90C598EBE84BCABAA22E988350EFEBA + 3AD857D7C88875B365D47A7A8AD5CAE84DF364F4C6B96AA3466F982763601BBB + 79BE53E0FDC550D2E3D067FC9A59326EF54CF5048EDBB240A6703FC80D0C333D + 4F266E5A2093B62E9649DB97CA04A4630913B62C96893B5728D020FC9DB873B9 + 4C0A40303561D70A99B867954CDAB74626EE5D2B13F6AC9671A89FB8678D4CD9 + BFDE4161F49D843A7AF94EADDB2C530F6FD6FCA43DEB64C2EE75E8BB41A61CDC + 223175DB64CAA1AD32E9C066998C34E6F036D890D611066FD110D309C736412E + D474C2E1F5927C78ADA4D6AD9669FB5642AB25F1F006997678A3241EDF2649A7 + 76C8B4E3DBA11D92787297249EDA2349F57B25F9CC7E493DB75FD2CE3B655C38 + A04A3D7F00E5C3927EE1A8A49C3DA8CAB87058B22F1F93EC2B47603F24991751 + BE7454D3B473687F7A9F649E3B08A1EECC0155F69983927BBE4EB2A1AC737592 + 7BF6B0E45D382279178F48C1B9C3927FFE90E4A14FDED9035274E9884CBF7942 + 4AE8218CCF9273763FDA1E920285CD4EEA7D8C340B9F2FFBC24107A9A1027CA6 + E2EBC755F4B0A477B2DAAE1D97921B847AC7A594C2F1A7DFA2C7EF292947BEEC + E64929429FA2AB27A4086D4B51AEF8E28C54D0A3F40B0258B42534FCE294547C + 755A61AD02DB2FCF48F95767A5E2DB8B52F1CD0599FEC57955E5D71764166C33 + BF3A27D55F9E959AAF0861E9C50A318C33DA567F73515585B6E59F9FD6CF567C + 8DE1818F49F9B5935279F39454DDAA970AA465EA817A442A147EE2B3DF3C86CF + ECA069C9ADE332FD737C5E7A0A4315F89B6640159F3BAFE6CACFE8357B466ABE + C0672180FDF4B48258FE7DE5A8ABF8EC8C96AB9056E07C4CD9B60AEDCAAF9FD0 + F3D356494F63A4333EC3DFAEDF0D8F714E667C7E01B68B38CF559CF3323ECB25 + 29FB02FAF2A24CFFEAA294E0D845386EC98D7A7CE633527A8DDF393EEB6767F1 + 9DE2EF26FCA5E72DFE1F8548F3095A6F1D75A1BE3F7569D12D88FB48DFC4F780 + BF972AC17740204C605FC6FF25FE67FC5FF2B7538663D2039D7BEBAAD72EF79C + 46FF521CA794E1C7099B097B15F8E23748C84BB88BDF553E7F3B503E7E078538 + 266DBC16B22FE3778BDF52267E6F99F83D66D12BFEA2FBEDB16FDE65B4C36F3D + F9C46E493949884BCFE063928BDF38F3A9B8C678EDE45C3EAE691ACB67F729E0 + 655875025FC2DFF4D3BB2310D8CAF4EEE535C51731F81246328E9F726AAFE6F9 + 624612CE99789CE1E1F76A9929CB49C7B8073923066CD79745128FE03E0011F6 + A6E09EA1A1A2214261DA1270AF987A682DC410F61B25BE8E2FA1ACC7FD6835EE + 4BABF46515869CCE39B3575F3C71510CD64B3CA31A10F0E29E3615F7B7D8DDAB + 2506F7BC78D8E2A0D83DF406265CDE2A71FB37CA14DC0B63D076D22EDC2F77F0 + 859AD5FA520D81EE587AE3D293975EC0DB70CFDDBA10F7EFF9D05CDCD7E7E01E + 8EFBF946DCC7D18EF50A8C372F74F017E90494276E5BA2F7EBF184C3B887F319 + 306A9D03C9A30993D7E2790119181E45F0BB16CF8F75736424347CCD6C3C5708 + 79F10C8186236F656AE8AA99327865B50C5E512D835654C9E0E555781E31AD94 + C1CB905F5A23839756E31956833CDA2DAE92010CFF4CD1C3773EA12E5521DC93 + 97219909737BD79649AF3965D27B2EF250DF3950ED0CE93FB702F533F0EC2D45 + 5A8E676FB9F4AA71219B1DC42D949E3545EAC16BFBF9F69E49EF5E1C4FF7F635 + 5BA9C262559553F7CA42E95651205D67E44937AAA2508130D306EF5EE7F94B11 + FED2DBB743498E7462DD0CD495E74BC7D21CD81A0070A7925C0DED4CD8ABDEBD + 4539D28D7B01D37318F9F685B015E74847E4DB15644600B079FBB64E4B94B659 + 296A6B9B0D5B469296DB65A7219F2C1FA64C950F92A7CAC7A909D21665B6FF08 + E3B28F684B4950FB074953E5FDA469F27EE25479775A9CBC37354E3E983A1563 + BB69F241FC54792F163688F0F7EDC9531C049E1C83F1E464680AF271F2FE9478 + 798F7513DCBEBF0EF4627C692F227A2098B6B7BCBD7C3996F5C7B6DC5BF8AD51 + 8D41B08E77E9F51BE83546D0194ECF5ED88730DA4E030466CAED595E62949E81 + 6EFB167AF852CC1B0026E0352F5EC25C03C094D5190036D86BE19D09835F1E30 + 5C5EEE87FE21AF5F03C084BE2DBB621ED2ADAF03C0DD9CCCEBF769CC674CCF06 + C097E03732DF81C273A008FC0DC439135F9AB517684D0680A988ADADF3FA65C8 + E79604C25E2867F3F055006CFA10E50F310FFC88F33BF40BDA2B04A6D7EFFB0E + FC120037E5096CF54F301F8061425ECA07BD2C47BC7E518E806405BC2ED4B381 + 5FE65DE8670784C3D0D797C1DF68625D44AD1ABFBC6DDEBDCC735E6F1EC1B666 + E003E030E8F5D71BFC35091FF64693AD773035586B6B21BA4EC2B2C1DC67DF88 + ACC1E80BF790AEB94056A6FCF6BE78EC08EC451A59A3611D8EE9AFF1589EF2D7 + 8AEE0F44084C006CD0B729001C56641DCB03BFCC3F10C06005BD4D28BC66667D + ED3856CFF53EEEDB1B5E8BFBA3B2353EEABEC79FC771A996C83F2E2FBDFDA6C4 + E3DE5BB37CBE8C983A5E9E6CF592DCF3640BB9F7A917E4958FBBC9B4BC999251 + BA485ABDDB43EEB9EF19F4C3DF8C63347BF42979E0F1A7A5D913CFC93D8F434D + 444834CF5FDD76EFE186F5DE8882756083BF6100DC682D379AE86485FE51EB20 + 3B8FBF36ED9723E2BA7490E71AB7015ACBFB8AB6267EA7FA68623B03C0A6BBED + 1B5DF89F41C6117CF93CE11F95CF3AEE567FB6DF3F550668EF42613EA56C2A52 + CF7CA07B1BCA9176FFCA00383877534037C2E73C456B17917F5CFF3C7F4AFFE2 + 00F89EFB1EF2C47203F46DA4668FE287F0A8DC873FEADEBB7507FF5F55FE3F18 + E5C80FCBBBB01AA9C99B8C8393B7DBEF2C3B5ED3C70DC9E0EF9F00C09173F0EF + 40593D81916748682DDFE5CD98FDC2B6080486B47C47007C7BFFC60AB76FD03F + 0B00AB426DACBFB5B70771E481EC3DB0FD07B82F7F60104D1A620469B4BE945F + 170949429B077F1BB50D7906D3A67DA20C741A0B6D6F2B37D81AE0300652184C + 597AFF1384C0B7878636006C629D79118761AE0A75AAA6CA812D32280C06A294 + D94C6C1B6E63ED2CEF0F5A236299835D0FFAFA8A0CA431D06EF60C07E5A101BA + C1586F10EFD787156EEBF70FD735D5C6B74503BF940F7E6DA2C394931DBF8D4D + 7E6C5264654E9618E6D974DB444A275B6F223508DC3029F3F700E6C4ED81BF38 + 11F212FEB67CAFA3B4780713CFD75B63B2E73C805D48E860AF1FF431306C4037 + 9A7C00EC6C04C998A832FF8683BC6EAF5FC2DE8FC5F60466596D6F332CB4F3EA + F53D800D00473C7B91FA6DC200F849B4D110D001FC7DECBD365121B0C15D936F + 8F26F665586982DFA7E9ED8B54F7020EFAB3FC4CBB6E0A7A2DF4F3D39F749767 + DBF694E7DBF78AD80D0013F83EDFA5AFBCC045102E74049EBFCF76760B225C04 + 61D9F607260836184C350A0F1D805E077FDD628BBFE0C2FCF33DE8093C20503F + E709AC5EC203E5859E031D04E6FEC3BD07CBCB7D8644C02FE5F2B04304C184BF + AA015C701A040D9657A156F40086E809FCCA9061BA27D95F060F53BDEA01E057 + 8722E502D5F0B1AA57B850C570760C231DEC77C60530A6B6679A2D8CB94533EE + 25CCFC48D48F463DF75B1B23EF042917DD5EE762DB5882DD09F2D6A4F1F2E684 + B12A825E0BFDECBC7F27A98D7504C104BF0680097F0D00D3CB971EC0A6D6C90C + F11C2F1F538177B00AF9D64971D22E2D513A663214E134699B3E4D3A66254BFB + CC44F924355EDA7ADEC084BFF40466DA3ECD95DB67C296D9100A9A7B02B78508 + 7FDB661102E37879090A81BB14244BF7E254E984766D3362D1364EEB3AE6252A + 3466AA10B83443BA95A44B17B4ED569AA69EC0BD2BB2150A333474DFCA5C1932 + 7FBA0C9C5322DD6764A90C021B0026FCA597B00F801DF46D48FBD5BA3D80097A + E9F13B606EB1A62EFCB383C194EE011C4060C260C25E82DFB1AB66ABF72FA12F + C3640E45FDB025F494AA90418BCB65D012078087AEA888C8CA4C072F2B57203C + 920BFA04BEEB6B65F8BAD9320C623A74CD4C19B27626ECAE9E1081705841F1DA + D9321A293D84C7AEE65EC435320EED191A5AB5719E4CDEBC50A610266C59A4C0 + 81106134D231F42EDBB644E8B13669D77219B775098EBB50C66D5BAA60981AB7 + 63998C23DCD8BD1AF99532663BDAED5C25E3611BB773A58A75938350CF4C27ED + 5B2F13F6AC93B1BBD7406B65DC1E94F76F94890736CB84039B6472DD56D5C483 + 9B2194192AFAF066893BBA51628F6E9098BA8D127768AD4C3BB852120FAE90F8 + DD4B2476D752997A708DC4D7AD87364BFCD12D68BB55E28FEF9484FABD12776A + B7C49CD82971C7774902C3C79EDD27A9E70F4ACAB97D927C7A9724D6EF81ED90 + A412009F3F2CC9E7EA903F22E9171D104E52707C48D2CF1D91B4B3CE4B38B59E + 9EC207234A4339F5E45EC93C754072CE10FE1E917CA8E0FC512984F24ED749D6 + A9FD927D723FF207A508C72FB97C4C8A2E1E913C9C23B7FEA0D665D7A33F8E9F + 7B9EE1718F4AEEA5A3928E7CFA853AA407143E13CCD12B99F5F44C368F63968B + AE11FC9D127A7716DE38A17B111300322C75D155C2DF93D029C9BB744C0A2E13 + 2AD6CB7442D0CFCF41F552FAF90929FDE22452E8CB5350BD147F7E4A8ABF382D + C55F9F9392AFCE4BC997E7613B2B655F9C972AE4AB3E3D23D59F2185CA087271 + CC4A02627AF0A25D39EAA7DF427F9C9F809BDEA2A5574E48D9D57A29435A82CF + 5D88BFAFF0E241B43D22355F9D928ACF8E49C9B583527AB34E4367976888EC3A + 29FDF4308E7554CAAE1F96D2EB75321D76EE5B5B718B1017BA71523D6F2B6ED1 + 4BB65EC131FF7EC253026385AA3758669864D890AAF72CBD66191EFBD35310BE + 03DA99D22357612ABE875B6750BE2C259F5E92E24F2F4AD16797A5F4ABAB52FE + ED357C2797A4E0E65929BA791A6DCF48E1359CEB3ABE5BF429C667E2FF8869F1 + E7A7A510C7CDB9765872AF1D92C25BF88D68E86FE7F95D7003DF0FFEB6427C26 + D5B523F8DFF1FF4AD08FFA4BDC8B1ADFDFB5630A804BF07F6728E7E21BF83BF1 + 77E513F6E26F29BC795CF3D9970F49D625FCBEAED449CE15BEC8700829E1F329 + 9CFFA86432FC327E27B9F87D249FDD8BDF395F8638A42F4BA49C3DE07E7710AF + 17CB279DDE1779A982C0371DD746EA395C272827D5EFC33582F3E0F79D89EB29 + 1DBF6DBEC0C0F0CE84C084BD69F5BB24F5D44E4D097E5D99DA8DEB0AFD7DB87B + 825EBE0E04B39C706CA7DAD88E769613719D279F445BBEF07174B34C3BBC41E2 + 0EAE95588666463EF1D826D83749C2918DFA92483C4339D7ADD5FB4502EE1371 + 07B7E0BEB4565F5699B28F7B9C6F964486A13F8463E11EC4347EFF0689DD8BE3 + EEC771706F9A8636B1B8774DDEB94662701F8BDBBB4E6258BF0F6D0F6ED5E34C + DCB516F7C935B867AE94F1DB711FE5BD10F7BD09B86F8E57CFDEA59A3A39C84B + 8FDEB1B8F78EDDB4C0015B88F7628260CAF2EA114CDB46077E79EFA646ACAD95 + A1AB67CAF035CEB3770C612F41EF2A7AF2D23E5BE1AF1E3B0481596F6DAC3C64 + 658D0C5E41CDD4741085E719C12F357439CEB3669E8C5D8F73AF982D03165448 + DF39E50A7BE9D14BAFDFA18BD0677EA5F4AD9D2EBD66974AEF39D3A5F7BC72E9 + 3B7F867A02F721149E496FDFE9110D988BF6B3CB15D276AF6C00BEAC63EABC79 + E9DDDB0080D9AE5B4591EB5395275D66644BD78A9C40B9D2A53C573A95664997 + B2DC0800A6D76F47025A88E09729A16DE7D27CE4F3D46BB75D5156007EA1E22C + E98032C1AFEE135C92E7423DE7676AA86742DFAEE8DB99DEBF84BA410868C25F + 05C079B065A54A9BCC64F93823513E4E4F943659290EFEC2F6515A82DA5B63AC + C532ED6CF361EA3457873CF541CA54790FE3B2F7303E7B675A3C14276FC7C7C9 + 9BB131F2664C8CBC131727EF13FEC6C52BFC7D1FE9BB31B1183B4E56004CAF60 + 078431669C82BAC971F2D6F8C9F206C6A6AF8FC65874CCA8DBC0EF1B189BDAB6 + 256F067BFE36ECFD6B63D931F226C6BC6F6A786702E2862D52385EB63CF57A00 + 7C35DCB3A7D7309E36EF5D7AF23A00CC76186707DEBDB4B1DEF70036F06BB037 + 0C80CDAEE19E31F6FF0B2303613E40F84BE07B5B886703C0986FF8F0D7E62191 + F94830BFE13CC72030CB16FDC8E63E3AFF315B201F00FB0A7BFD32FFE4278D43 + 3D5316EA59C33D13F07E0C7B943D7D9FA42DE8F3243D806137481B81BC90D914 + DEBE8BF9A957677D087BA930007EF8AD4FE4A100EC9AC7B00F7F7D0FE087DEA0 + D7AF03C1D1C0AFE9D19097AF0F7CFD17B7ED656ECED59927DCD5D0CE419DCDFD + AD6C621B7BA13C0C7CC38AB64E61765DCB78C1454163E8675BEFB075104D9F7F + 4B1E7AEEAD08D0B57596C87A4BB0261386BD4D4160B6D5B59F000273DD45D3E0 + 58ACB36346DA0632EF5F02E007797E0F004783BFD1D6AFEE6B118479F6D6BC08 + 711F6CF9AA4260D6DF51686FEB66ECA7D0D83F16ED41DE5F8FFB3352881CACE3 + DD430794C79E95079E78461E7DF679F9B86B27299C5526954B674BA7A13DE4FE + 679BCB3D2D9E90FB9E7A5E1EC6FFEED58F7BC91B9FF493C79E7B4FEE79149FF1 + F197859EBFF73DF6943CF8E4F3D2EC89E71500DF0BF94E2FA6467BFF7AEBABAA + C039876BBE770D80CDA1C72BEB9A31D79CBD76DADFEC9E68F3ED917CB0366D10 + 372C1F06DB5A36E5B7F165757E5B5FACBB7B00DCE277D4D0C63883AFC66DFF31 + 45631FFF53C803BCB72908EB1C493DF97C4AD90CD248DB4066D3F47F010FE068 + 7511F9C7081FD3CA7F4A8FE0BB8C065FFF15E403E0661461EFA3EE43FBBAFF31 + 7C8114EA9AE18FB22FE6BFABFC7F70237BD317975E44417DE376FFC49BCF9F84 + BCBE6EBB39F266CB1B243F33DF0CE167469937606D0FD113F85E825CEF38BEEE + B51B4CD8EE9FC78EF91F0080A9689EC0F650F31F50F6000DCBDAF936BEE515E9 + 6F363CF0FC362A3ED81B95DDC3DC1EE83EB40D0F0ECC16B6DFA66000D1A82D41 + 2F15B4F18FA56D8276CCDB67082BD2FE51B4792CD40E65DD37C3064351064A94 + 83BF4C3168C3802D0C824D7E9D0DF20CEC86F3D1CA66B3BC3FF0A4C26DFD3696 + 0FDB6D306B20B81906B94D0D9CA948B925F4B4858076DEBDF772D0FD4C2B0C46 + 31488758EF44BBCB87BD806D221119E407037FB39BFC4941343BF39C6018C835 + A87B27B19DB5B53C273E617162149E4CB16C80D726630FBDDCB007B0D533B589 + 9AB66388E7975C08E8875B7DA49EBF8FBF8589E9EBCE039860D8BC83FD10D02C + FB62D8E8475F63E8A88FB4EC00B183BE0D5EC01FCBA3AD3EC62414ED30217DEC + 4DD4BDC9FD803F9627DE69A36AFE1626BFC184F5B1373F96E601C8F501B00F79 + 0978A379FF3600607AFFB68F0060C2DF300056901BE4096E99B29E2941AF015D + CAC0AF5FA7A0B76D57154130BD8359D7F2934EF26C3B17EE997210B8BB3CF349 + 90876CE1E1E9B62877E8E91641202E6C7011E32988A19F69B745100260CA01DF + 5E0A7C097E0D083F4720ACD0D781DE30F87D860B2CDDE9F93B50E12FF3CF7577 + C0982058F700368F5F4260867CEE3F4C2C0CB4867FEEC732F3A8EB3B081AAC00 + 9821A05F1E304845EFDF570711000F93564386CA2B4809805F46AA61EA8612F0 + D2D31736C25E85C0633C083C4243DD310C34412F650B66B6681681C1485F1B35 + 525E1B395C17DECC03832917E0E871F13ADABC316E82BC3D7192BC39D101E0B7 + 90BE3D895077A2825FD3DB939C07B07907FBDEBFBA3770FC6485BB0CFFFCC1B4 + 290E0027C52B14362908860880594F1B4342ABD212A53D3D55D213A535ECED08 + 84E9ED1B78FF12005311AFE02CE7094CA86BFB0013E4B6CF21D0B5FD7E53A453 + 6E9274CE4D96AEF929D22E235EDAA4C5A2DF540D15DD2927C97913A38D868586 + 3A1624497BC2E1021CB32859BA95A429FCE5FEC0F4041E309BFB0232B463B674 + 9D91A57BFF1AF4B5D4790AE74ABF990C1FED423F13FCBA7D7F8B14F81AE81D30 + 872126B93F70A10C9C5B2A431610FABAD0CF83E69728041EB1B442862E99A1C0 + 57C33DB38E907821CB0C055D2DC397120057CA60B45308BCB45C062E71E9E065 + E8CB632C23008656A0CFEA1AF5DA1AB5768E7A69A9B7962DD4AF75E2C23F3D81 + 87AD99A9A13B357CE7CA6ADDAF51C34AAF468ABE4ED5329A1EC1EBE706FB46BA + D0A03CC6C8F5B5EA49ACE14903E8307AF37C19B98EFB543A284C703161C75285 + C163B72F93B13B5622BF1CF915C83788F0D717C1F084DDF40A26045E2B6376AE + 96D13B56CB5842E07D1B64C2DE0D32F9C066997270B34CDABF513585A0E5D066 + 8939B81EF60D9A8FA3C7DE41865F5D29B17B57C8C4DDCB650A43B3A23EEED046 + F51C9E528776C7B62AF08D3FB153628EEE90D863DB65DA891D0EF89EDE2329F5 + 04473B248930EBF47ED80E4ACA9983480F48EAD9439A4E3BBE1B7D76C30EDB39 + D4D5EF939493F440DC27E9900B45BB07E5BDC8EF95E4E33B355C6DD659EE677A + 40B2D127871EC1046527774BDAF15D9285F6B90C777B7E3FEAF749E6E95DB0E3 + 731C65C86B86B9DE2E99F5BBB50D3D7F33E82579EE00D2FD0EA65DAA93DC8BF4 + 343EA8D23D8AAF1C71A1AAA1822BC71C18BE7644F2AEA2ED65EEE1CA70D347D4 + 53B880E0F8123D428F4A3161E1F5231A9658BD4D6F1D93924F8F4BD14D943F3D + 21C59F9D9402C2C41B27A4F0D64929B9C5BD8DEBA5E0D6290DCD4CC05B761DF9 + 9BF5329D1EC601902C63C8E34F4F4929FA15C3C63D66E9A9CC10C1849825F88C + 25574F4038173E4B1EFE0EEEA1CCBD8C5D88637A0AD7E158750A44F3094B9112 + 86EA5EB5F47EBD8E63D3E319F9E937702E1CAFECDA09A9C0E7294779FAA7F5F8 + FBF0F9B94F33DBA38E7F333F4711BE1F7A2197E1FCA5484BD8463DA8EBF13DE0 + 6F45FB421C9F5ED2FC5B4A09726F9E97C2AB6724F7CA69C9818A3FBF2C0BFEED + 7B59F3FFF93759F0EFDF4BD9E797A4E0C6197CC633F87F9C425B7A5C9FC2394F + 28A027902FBA75C281D92B8724877BE532BCF215FC3E2EE27F7D09FF5BDA2186 + 1867A871D6E5A32FFF9FD90C8F8CDF4B2E7E13C5579C1735F7E1CDC5F7967B89 + 619A29FCBF91A7B8A77516BED74C7D6980DA8FDF117E43380E8F9781BA94B30C + 877E4895846B22995EAF67F6E9EF9C9EB75917E8815E2769C8A710BA9EC26FFC + 147FF7B85E904F3A896B872016BFEB44C25AD4B19C786A3FFAD4E1377B10D709 + AE91933B5504BFDCBB3709D7093D76594E61B876D4A5E3FAA1577112AE57C25E + 03C04C097B999F86EB3811D724C341271CDB2553519E768CB69D92806B279E2F + 8BE01E108B7BC194FDEBF4C5137AF8B2ACF70DBE50827C7C1DF71E2710DEA661 + E819C160CA8175B8BFB02FDB6CD430F58C66308D2F951CD828F1B81F4D3BB445 + 1218B21EF9D87DB8D7ECC13D6B9703BB8C7EA0910D783F433A7EE71AA7EDEEFE + 3801F74915EE99D4F8AD4B65F4A6453266CB2285C07CC966DCE6453296B68D0B + 15D00EE3BD5D5FEE9987B67CB9C7BD9CC394368A909840D7A02E812EC5E7C468 + 7AFCAEA995A12B67CB9095B364C8AA593274359F1784BFB0B3BC8AB077760079 + 67A2AF2B6B7B68E80AA4CB6B64D0F2993270F92C19A41EBF35327071B50C5854 + 2DC396E3B9B16A8E0C85ADFFFC0AE93BB75CC33C3B088CE7DC822ACDF7AD2D53 + EFDF3EDCE3776E203C537B6BA8E7E97856974BFF5AF4AF455BE409761B423733 + 2D919E04BD150D619BB96F6F9F1A0780D59B777A9E7429CF87F290CFC1733F57 + A5F0B72C473A14D17337B721B473393D801DF8A518AA999EBC9D8BF3A513D40E + F9768599DACF206E7B940D18D3BBB77D7E86B4C94E75A19DF332DCBEBF390CF7 + 8CB605392833C518A4B440BAE0981D7251CFFD7D33929D9857289CE2F600C6D8 + 8A29BD7B5B07E58FD312E4438EC718FE39CDD5BD8B71D9BBD362E53DEEF50BBD + 37355EDE898D937721F5009E3A4DC12F4508FCF6E4C9F2D6248C156363E48378 + B48D899137274DC1583116E3CA588C2927CB9B1C638E23F8E5762413D423F82D + 94097E5F1B85712C23D58C836D0CC6A0C82BF01D3D0EE57148DDFEBF6F70BB94 + 00F6EA9628C3C684345AC7CB0DC0D7858266B86783B92F71BC8E31BA83BA6ECC + FDCAA006C84B19FC35F9F097A99559479BD999D20B989EBFCF23B570CF613D83 + 7948C3BCC3E55B76EEADF2ED1AD988739FCE98CB041EC09CEB70FE4311FAB620 + C425CC65D993C1DDA730D762595F9C85CC6EEDB40DCB9F38CFDFA75A07219EE9 + 0D1C06C081E7AFEFD5FBF80798177EC0F95F63984B19E425C825C4A50CF05A1B + CA40AFBFC7AFB5537B007B0979097F9D77AF83BCD100B0E9C1D73F5019F465FE + 81D7DED7D4E6D33EFCF55FD40E435DE6097BEF37401BD8ADCEDA32D597C883B5 + 045B5FF0D725C26B13CCDB4BEBBE9DEDEE7FFE4DF5BEE51A07239E719DC35F3B + D1F58FE7D197EB2AB6BE125A77F1D75F9A82BE26B6D1751EF6093C809B858E65 + EB40D1C47A02608A00F8018693F601704BE7A0105EB3A2CC46F8AB0AAD813D10 + 780047EA43E23134457BB6B3F532CAD6D4A8FBA9C06EEB7761B15D347B580D6B + 85CFCBBDCD9F93079E7C411E7BFA2569F1D22BD2EAC38FA563DFBEF271F7AEF2 + F8CB38DE13CDF1195BCA7D4F069EBD8FC2F630FEC6C7F0591EC777F3D88BD2EC + B167E5FEE65CD374E0F7F70030CFA98E2FE1B5D240F73DD252EE7DB80102FB75 + B626DB4821001C592FF6DA687FAE1F73AD3A5A5B4F9176DE5AB7BFFEED03DA7F + 969A3AA67F5E93EF00164DAE1DD3E80098B2FA305308D7FF9E7C2EF167F47BC7 + BB5BDB1F95B1A56832C6E43327C7711A8030EB1B016055736194DFFBEE7B5C23 + BD9ACD675D4D42559F8BF99CEC3F48FC9C1A91D8B8A4D5F99FC7F2D1EAA3D99A + 2AFF293DF22F0C8003DD4311FE3623E00DDB69737210187F94676B2CD6055FDA + FFAAB27FACBD2911B9401A2BFA45C8B4419178EB512EECFF2AF9373193D6F1EF + 51801A5DE1E3DCADA21DEB8FCB7BA84401BD7E1D537B28F90F2BDFFE47D4E881 + E73DF8EC787A5E485396D1461FD04C096CF100F707108D1EEAFE8000BAE300C2 + DA87FAF8F2FBDB3198DAF99B52B81F6D11E81B0C80EE6FEE20EF9D14F6043619 + 08F6CBCD50A6F8969FDFD6AF6F54369BD73E02963DF98351DF16AD0DF31CC8FA + 03DA664F60B0CC72306036F983E988D4EED48C03EFE0CDCBFBF92626C57A82E2 + 675E0D00B06B63792D3FEB807164F0CFB73A39B0F715D89A413611A1AC0F539B + 983CC44908262D9CC07082E14F68C252E01B4C5A14F2066FC0FAE2DBB1948584 + B6C9933F9932EFDEC73029D376AC0BFA59DF475EB63776DDC4CDBDC58BC91D6D + 9EFD41F4E564D02030A1AE79FF1AE88D7803BF4EB8FBB182E407702CEAE1D73E + 92E66F122A6342FB0626B06FB496C7507E8C935C4C5C99B2DC1C13DAE668433D + 1A4C781B26BD2EEC95E5B5CCF45DC25FF796B4B3F3AD69DA4C987C430CBBC5F0 + 5B54F3F7587650D8B7B3AC2926F88F73928FC97C8B8F18FA2B08FFC57060481B + 6C4EDC1FEAE94FBA0640B79BB4E41BE268FBE4871D5D1DE12EED9F10F67697A7 + A0275B77952750F7E4277C23DDBD95AE6FA6B7EDD6B0C0C1B7DAB988C1058D8E + EE0D782E8C504F777475B660428FE1E7BBF6959628B7ECDC4B9E42FD539A77F5 + CF048B2E4F23E51BF8CEE3D7C15FA6CF751FE0DECE2704E6FEC0F400EEEDF6FF + 75F981F262DF21F212F700EE3B38B208E4168DE815C03AE72DF0CAC0E1F2EA40 + C261426386801E26AF0F1E010DD73CF726D3052B6A985BC0E2C2D4CB831C206E + 3584E1EB46CBEB23C688DB0718368679A67704BD24C68C91D7478DF616D2C6A9 + 987F15ED5E1989B6F4B2E082DBD8B1F23ADA33FFD6F8F11AFEF9AD0993024D94 + 37C7B33C51DE99C4107E53904E5638FCEEE4C910D2292E5434C12F2130F304C3 + 94790CD34E7D3435563E9A46C5496B0DFF9C209F2431047482B4E19E7309F15A + FF7102F707863D3551C5FDE8A8F619F40E4E7461A1A90C7A094F458A3AD83B65 + 2549979C14E9989D2C9D7253A46B6186742E48938E792948D3A5537EAA74843A + E5D386341765F4E9989D249DF3180A1AB6EC142851BA22DF0DED9876CAC7F108 + 8EF353A4436E9296B97F708F924CE95E4C0F61868876DEBFDD66644AB7F20611 + 0A53AC63CA7D82A948A8E85904C205D26F76910C9C57AAC0D7790FE73BDB9C12 + 19BAA05C462CAE94210BCB6510EA872E9C212396563B2F60D806A05FDFDA42E9 + 37A75801F090251532583D7F0979D16F49B5EE9DD87FC10CE98BFEFDE695C9C0 + 45ACAFD2C5F461CB67CA8815B364E42AEEC938D3415F881E5D2357434C4DAB1D + F01DB6AC5246AEA89611F4DC22485ECEFD856BD0C61DC3A906C7A9D13CBD8499 + AAD7D8BAD9326AE39C8650D29BE6CA880DB364C81AD4AD9DA59E691319B294FB + 566E6598688627250C26DC80B6AF94F1041C3B57C94405211B64FCAEB5320676 + 426202608A5065CABEF5327EF75AD809865D5BA7F5AA89BBB94F303DF9B648EC + 812D9AA79D79EE174CB013C3FD8821C2E4897BE9D5B749A6D2A3EFF0168939B8 + 51158FFCB463DB25E1F80E5522947472A7249ED821C9C8A79CA417E25E85B85A + 7762977A1BA6D4EF57A0C5BD88E965488FC394FABD0AA068637BC25FF64938BE + 4DD2CFEE918C737BD107EDB92FE9993D92428F4A28937B0C5F3820E9B0A5D6EF + 923486BB3DB35BDBA79D41DB93F88C4737E2B81B71FECDF84CDB24F5D47649AB + 7720389340199F2703E7CF3E7B40F2CFD749CE99039289CF491BF74B55204C90 + 87BCEEAB4A5078F96064BF62EEF19A4F18CC7D8F512614D6D0C0D78F683EFBE2 + 7E6D5F708DED0E4BDE15C2D6E35274E304F26E3F637AB1165E3B2EDC17399FF0 + F456BDCCF8ECAC14126EA20F433B171258723FE30BFBA40069F115025F1C17F9 + 42EE617CE990EE239B8FCF9B8F63321475C98D939A2A2066386B1C8FE1AD0B71 + 0E82CA9C8BFC7CE88772C155DA8F6A9FD21BF50A590B2E9F10E7D97C524AAE9D + 5215D2B3F83A0174BD145DC1DF416F6B7A3D5F3C2A79E7719E7338FF799C1FB6 + D2ABA75C1D5478E5A4F665586382DB929BA7A5142AE6B9AED5E3FF784CB22E1E + 978CF3C7A5F8D64559F1FFFC263BFF7FFF5F59F17FFFAA9EC139974F4A1E8E97 + 8BCF94A521C48F49AE7A681F91BC6B47503EA81EB9FCBF10CE669DC7DF46CFDA + 332E547216432FD3D3FB347E4F2CA30DC13E612E416E3AFEBF4C9D0738F7EAAD + 930CFC7EE9459B85F67CA980FF7B176299BF83039AF278E967F05BC1EF87219C + EDF7CCDFBAF3DCDD2BC9A761C3E750EF5F94D9262CBD3620F3C625884D3EB107 + D7D936779D10CCE21A89E37583EB877BF8A6E19A4A3CB24DA6E25AE4F5967976 + BF5E73C927088109831D104E213C461F1EC779F6BA73B8F3EC51F1FAE3B9B84F + 78EC21DC1BA0B8BAAD6A73765CFF50CC814DEA85ABE2CB24B877301FC3974AF6 + B9BDC763F9720944704B111E1316F3FE4400ACF718A4D38E6CD110F6D38E6C95 + 24DC4BE26073DEC26E2F738D7810BCF4C20808FA424C1009812FBE4C449E0078 + CC96A52A7A033324F4E8CD4B64E4C6454817CBB82D04C2CB718F5D22A3372E96 + 511B16C9C80DF365F87A4260A7E1EBE6AA6DE4867932623DA341CC850D76D48D + 584BF8CBC80FF390C2BEA65686ADA277EF6C19BEBA56EB87AF99238302903B24 + 00C2F4EA1DBAAA167573919F2D0397D54004C1AC2314C6FD7F29F2D0C0C53365 + C0A2993268C92C19BAAC16CFA9996E2FDF7933A43FBD79097E99A71730F256A6 + 47709FD965C23D7D19029AFBFC32DF6B66B1CABC7EFBD7CEC0B3B64AF3F4E625 + F825D825FCA5CCD66D46A1742D27BC2D90EEE50E02F7AE2E951E95C5BA4F2FC1 + 2EC33333B433C33A13187729CB5788EB835BC25ED72E4F3A17E749C7C25CE950 + 908D7639D2A988219C9D1C0CCE93F6F9D9D226275DDAE612F26642590A7ADB65 + 6748DBAC34698FB4437696B4C94CC7B807C7CEC9812D4BDA666648CFD21219BF + 6409B40863943C69939E22EDB3D2A503D43E230D63A5544DDB67229F9E2A6DD3 + 52742FDF0FA632320BF7FF4DC1D82B11E3338CAD5292D4C6FD7D3F4C982A1F26 + 4ED5B6DA3E1E36F4F938C1E5DF8DC1388FDEC071B1F236C685EF4C99A400F87D + 8863468E235F1F3B4ED337319E7C6B3CC68B13D167528CBC3B01E346E81DD8DE + 1A8B76A3C6C86B23C7C8DBE38232F2EAC93B72ACBC49200CBD361263598C7DB9 + 5D0A61EF2B4347E9F628AF0C191D01C02FA3FC97412E54B31B338FC2787AA48E + B1F5A54CE825BE9819E4DDCB987C4193117A5C486813212FC7F4F4E8A56C7CAF + 9EC2039D9730E5EFFDCB76CF62AEF09CE7ED6B52F84B4FDF6E98937469987710 + F4B2DCA25310D988F3192FE4B3BDE44AB15E5F76C53C8773A227BDB9918260C2 + 5C9659178479A6CD606F58ECA7F0977331BE6CCB976F83FD7DE9D16BE19DE9E1 + 4BEF5E03BF6EEE071BF204B884B4067C0DFAFAA9C15D83BAB4F9E0D7C93C7EA1 + B750E6964BEFB4D7B9AA79FC1A0066DEE6C14ECC530D2F436BF867825DCFC3F7 + A156EFCB83F4F045992F52D31E512BCED79DA7AFE509741BA27B61EE0F9B9BF7 + 3B1BCB26B5FDC5BD486E51C46CED8176CA07C091D0D16C13D4110033B57E6A7B + 811EC06FCAFD50F805F846E520CF351303B71AB6996B30C8DFFB742BB79EE2C1 + 5F5B8FE1DA8CC9D66AFCF51ABF0DEB23CE0481744D2850A46F44386FB0E674A7 + F52A96CD5B97EB5C265BFFB2BCAE8B85C06F34E971BD75347F3DCD14B105524F + DE405CBB6B548ED24ECB4C03357BFC0579E8A997E4B1675E95E6CFB592475ABE + 2C0F100C3FD252EE81EE6BFEAC688868B423E0D5B5CD47B88EF97C20B79ED9B0 + AED978ED321CE5D0CA11791115A329BC866A6BB27F4AFE5AAFADF75A9E6BCBDE + 7AB30160CDFB65C8D6AA7F4F3C96AD67FBF035BA6EEF4FD9319C5AE0B3E033D0 + D92B0AF855FBC3AC8F52174D0FFCE34CC1EFFF6723A5F2EFBFED58415DD4F3FC + 13E59FC3773A8CB0273AEFF9CC2AC2B408729F50987B1BD455D6E5E7EF92EFF9 + 6D2DFFDF56E4A80150FD575563D0DB943D90EF191CD6FF8200582F88A817875D + 504815EE36967F011AE86D4AD12EE8FF2AF93792DF93DFC73FC61F51F8987F4E + 0D0F973B01608A0F2383B6B6497D04E0FE1365E762CA87ADA62CE35CFA20668A + 07761800373538F0070F4DB5B96BD9A001E2B1ECFCFEE760AA03A190DD14B6B1 + AD3F408A26C2DFA60030152E13DE46B39BA2D9CD66F630006E34F80CD9FDC16E + D8D6C8CEBC3778B6BC95A3C906E2266D0BDDFB14F4B483C006827D00AC10F899 + 86C1BD415F4E2A4CF7B4C4640029DF16E5E482C0D626193671B0FE0F3CC30988 + 9BACD824269A221038389E01E0B07C90CB32274E3A610A8581E6A4EA5186750E + DAF9FD1F7D1913372A98BCDD26F47DE415F756EF43481F7E95A983BC067CE9E9 + DBD8DBF74379F475025D4C5EDF688DC9E707F2203D806123D47DFC6D4C8815FC + 3AD06BED2C6D00C32873D20B3D14A40D935F7AF6E238C1449A93685560A30875 + 7D8501B0E9494CD8C3651327F336F9B705005D2840DEB75B1D17125AB6EE1659 + 28B036B4130CDBBE51BAC0F04937798200186A81BA166D7A345ABCB0850F7F21 + C4F25C246178347B3B9E9EC14C59669868E6097A9FEAE4D2A7BB3478FD5ACA85 + 18E7F1DB1FE586BC2ED0708FAE1E28F76C1CC68D220CD67DBE6E03C00D9E032F + 331D305C5E510D83E8FD3B425E1F325A5E1B3452BD885F41BF560681038F05E7 + 99304CF3AF0D1D216F0C1F2D6F28001EA521A035F4F3A8D10A79A9562347AA14 + 0A13F08E657867677F8D6DC68D9337A0D750D70A65A66F8C1B2F6F72112E0280 + 2745CA84BFEF4D21049E2CEF4C9AA479EAFD182EF211F04E52D8FB5E8C83C0F4 + 0AF60130530260868F7E9F61A4916F9D38553E4EE02262BC826005C053E30240 + 3C55A12F0170DBB424559B147A0DC7E97EC06D8310D104BF04C06DA10E1984B9 + 0C199DE4EC545692B4CB49960EB929D2368BFBE0252AFC251056E09B93225DF2 + D2A46B3ECAB9B0E7A44AD7BC74E986FA6EA8EF929F2ADD4BB3A40744E86B9EC4 + 5D0AD3A41BCAB475294A93CE50A7621C677ABAC2DEEE33B26F83C03D2BB91770 + AE7A085384C0BDA01ECCD7E4CB803925D2BFB658F3FD6616C800E4FB071078F0 + BC3285BFD4B085336408F73F447EF0FCE9D21F69DFDA2205C0030977174E9701 + D0C045E5D00CDD3371F0A22AF59AEA33A74CFACE2D43BE5C1846937584C0C397 + CD9211DC47780D41ED2C05C04C2D4F0D5B5923C356D013AB4A862DABD2D0D3C3 + 511E422FE2A5150A8347AE66BB6A05C10C0D4A0F61962902607A0E8F24FCDDCC + 7D28E955562BC3D7E2B86B70ACB5E8B381FB52BAFD85D9463DCFB8CFF0968532 + 69E7729948A8B16D858CA7B6AF9271DB57CB5868CCB69532660BBD83A10092B8 + 7D82D7C9F8DD6CB352C6ED58ADC057C3A6EE5E271376ADD53281F0E4BD1B550E + 0E13DE6C92297B37C9A4BDEB6422C3B6EA5EC3EB64F27E7AF86D96A947088036 + 6BB869EE4D3CE500BD8637A9181E9AF58904C2D4D1ED328D5EB7270898766A9E + 90C78160E76D48A8C4D4C01481136D2C279F82EDF876493CB14D087653EB77A2 + BC55A61DDB061B8EC990B6A7F748E6857DEA7D997666B78AB038F5B40B839B74 + 02FD8F6DC639374BEAC96D92717A3B444FC9EDA8E3DEA5289FDCA70038B37E6F + E0458CFA633B24FDC42E2D13AAD13B58C15ABDDB5735EBDC5E0DBB9B791EE786 + 081C73AF1C922C7C0E83D5E665CA34FB22BD4D9DC72981B003B10EB8123E122E + 13C6123C123AE6232DBE4EC87A12E7C1B1D1BFE00A3D7C0F48D6197CAE53DB90 + EE94FC8BFB2007840B2F1F90029EE7FC01C9C5F1785C424C1E47612F8F7D8940 + FA884267EE37CBF3F1DC84C02CD3BBB980209A9F0DF9FC8B482F1F8F88F09610 + 57F39791479A7F811EC64714FE1612F45E082030ECB4951022AB0DED088891CF + 65FEF23129BC7652D36CEE098DF65997508773645F3A2139974FC98CAFAEC99C + 5FBE928AAF6FA05CAF60380B9F29EBD271C9C4B132086EF1D9097DB32E53FB24 + 1DFF8F6CEE87ABDEB987207ACA12003B11F2D29B9C293DCBF57F1BC05FBE7460 + 10383BF8BF649CC16F0BBFBF4CFC5FF9BF776195F1DBC3EF2F0DED35B432E48E + E744D86A40971EF0890100A618E23909BF2B0258FEF6EDF7CFB6EC63D7867B11 + 02FD4E3600607AF0D24E08CB3689B8C652F07B4D3EB24DE20F6ED4BF87FF3B82 + 690260F5DA55B8EAFA53766C8AD75B0C432E079F8129CBDC3F9CE1E4751FF103 + BCBE03104C1DDADA08FA5204BD2CD32377C22EDC3376A31FEE257CB184A1EAB9 + 67F9D4C3B877E03EC1FB13012F534260DE0FF859799DF17BE43DC2EA09782DDC + BDDDDF180A9F629E2FC410F81AFCA5CC1378ECD6650A81C76E5EAAF0D70160B4 + D9E420F0C8F58B64F8BA7932949EBA04BA01041EBA668E0C5E352BA22184BCA8 + 2300A646AC9D27C356D7068097FBF8DEEEE56B657A000F5B4DAF6103C0336500 + 346839FAD2DB979EBE8BA859782E55E3F9548D14754BD076618D7AF312F20E58 + 501901BECC3355181C780433CCB3C1DF01103D84B9AF2F65909729E12FF3F4E6 + F5F7E9F5C340F7A44770A553F71968575EA079867366B94745B1EEDFDBB52C5F + C1AFEDED6BF0D7F2F4E4B570CE84BC04C084BCF4DAEDE0C981E11C699B9B259F + 64A7ABDAE46448EBAC346943809B93A9105801702EFAE7644BC72CA41919F261 + 62927C909828BD4A4A2471DD7A4958BF563AE565EB7EBD6DD2E905EC80AF42E0 + 00FC7E9C9420AD097B931215DC12E8B649C6380BFA04B68FA64D75F6788CD1A6 + 4D938FE8013C2D5EDEC718EDFDF858F9203E4EFBBC173B45A1EFBB18FBB1FDFB + 710CF98CB1E324BE0C3851C78C6F4FE4CB84E3E575BE7008BD336EA2BC3781F0 + 177D91779AA42901EFEBA3C62AFC65F9F551E3A415BD7CA1374663DC0A31FFCA + 30BEFC180060EED53B78A48ADBA4506A1B34C2415AC83C80F565CA603B161F04 + BF3C60A0BC327010C6EAEE454D7D593380BFCC1BFCA558E6F89EF5CC73BC4F11 + FE3EDB0B7383600FE0A7B9CD0CCB4D01E060DEE1CF3D9EEEDA479EEAECE638CF + 76EA23CF85B7B7095E7CA5AC6CD0D75E8C250CB67D7D39676A813265B0D7CA56 + 4FA93730E7593A0F435B882FEA3EF1512795BFF72FE76F9C039A5826C8B5F9A6 + 016083BE26DA0DF65A3DE50360E7F51B40E0B70300AC40F876EFE04819F35EF3 + FA8D005F4FEAF54BD84BCF5E83BCAF39101CF6FED5D4202EE6F73EE8A5580E83 + 615F6C43CFE007B8C6104060D3EF02600FFEFA00987696B98EE1C3DFF0FA88E5 + 0D0ADB9A895F47007C1FA1F073AF47D653C200D887B826DAA301605FD12070B8 + 1C5E77A26C3DAAD1FA940780A3AD7B45D6CBBCB52E5F3C86A6A13EBEC2367FDD + CE9C357C9B5F17CDDE582FC8034FBE280F3DF51715A1AEAE6F425C9B74E0F779 + 159D554C067CC32200BE0DF286D637C37551C12FEB426BA754642DF6CFC887BD + B696EBDB4C5C0BF7D69D236BCFDE9AB4AD5387D7AEA3C9D533BD931A1FCBD6D6 + 1B1F0B9F0B6D2DDAE76D40B7297B53FA0300381C25355A1BCA3FDEDDE84EFDFE + 91E3FE1945A06FC0A15C1E6914006C5CCB97E35B01C08CF0AE50F94EF2DB5AFE + BFAD1EC1771E00D57F55DD35000E03DFB0FE2705C0911F7D143B75DF038DDF90 + 50791799A901F886858BFF0E8A7611FF57C9BF5985C51BBCE5237DFEC4DB32BE + C2C7FD73F21F2ED1E53F904C04C00681ADFE6E7437EDED9C4CA301604DF5E18D + FC9F1C18F8FD7C456BDB48366880087175C01302BABEDD3F7634F1389AC760C7 + 1F2059E8E7DB6D0ED072BF5FDBF3D706727E9E6A6A6068C73085EBAD9FF6F506 + A561595D58776A779FFFA6634891013506D80F3CFD6AA381B789EDEC6DC97B5A + 206D893ECFA0AD7AFB62E0EEC15F2737A8D770409C50040098E0D7E02F6DFE04 + C326193671B089814D26F42D55B4B7D4262CCC9B9DE271F886ABC2DD60A2E3CB + B7EB318289934DA84C9C549997705300D8266DA6F0648EA94D120DFCD213380C + 80D5FB1779425C825E025EF504664868A4B49BCD40AF0F8129D6693D53DA31E1 + A522E1B402F86B5EBDFAB634059BD9A3815E1F06FBF6A600708BD0E49F32F0EB + DB0CF2FAA2CD646098FBFE1AFCE5DBE74F414F120873C1A25D10DE2C58A060CA + D0CF1A02DA131742C2B2051283BFCF75E19BF4FDE419E4DDC28B1321AF2DC630 + F51766E82D4CF8FB424F7A020F90670983E915DCABB107B09509805FA22730A4 + 8B42F4FCF54170BFA1F2521F57CFF0D0AF0E1C2EAD068D90D7068F9257068C70 + 5EC4BAB0345C17AD5A0DA107B0DB8B8C32EFDF37468C95D7210780E90931D279 + 498C821DE202D9AB0C8F37728CBC317AAC8A76A66F31FC1EC12ED25628BF4A8F + 8B315C98E302DD780D014D4F0DA62C1302D38BC379FD360060C2DF0F626315EE + 5284BB067C9D57F02485BE067F0D00BF47208C94A0D7A4C017626AFA3809A9E7 + 09CCF0D12E4C34433FC3C610D1DCBF8EF9D40469979E24ED3220A6E9B4A16F1A + BD8393A57D768AB445DA2633493AE612F0664B17A4DC6398E0971058BD80F3D3 + A54B41867A09B7CF71DEBE84BF14A16F87BC24E9909FA2C0B76B7186742DA1D2 + A553518A7428C4B14AD314FA1202779D8E3AEE154C10ACA1A11D08A60882D513 + 18EA893C53C25F8A5EC1DC2B98F0D752DAD523782E4342976BDEFA0C985BA2F0 + B72FDBD7A22DF70E5E305D062DE4A279A90C583043062DAA445AA1E0B7DFBCE9 + 6AA35730C53A7A09D313989077E88A6AD590E5558D34785925C490D1950A7EB9 + E7F050D8288699561B2131DA6A0868868D5E55A3ED07AF409F353365D4BA5A19 + B7C985831EB7719EF30A5E5121C35757CBD88D7365DC6686802680982D239012 + FE0E471F86229DC83D2D0378414F3602DF919B96AA77DBC84D8B650CBDDAB6AD + 90D1DB2907462C243441C9E8ADCB5076B08529210D013041CDB81D0D658260A6 + B4390F3BE755EC80B2F3DAA3087D096828E7294C584448BC3EE2154C110AD3C6 + D4C031EB59A68721A19679171A8C226C32B8A5C0F82881EF1605BF09C768DF22 + 530F3B6F418266822342B8B4D3BB24F9D40ED1BD4F4FEF965486C145DF04B4E5 + DEA449473749CAD1CD9286E3A49FD8266950F2B1CD9288FAA4236C8B7E277649 + 2A3D258FED5031AFFB9EE23C2906FB0883CF1000EF93EC73848938F799DD0A81 + B9076B26EA080B75DF5588B03093DEC2F4B0BD52A761A2F3AE1E56084CE86ADE + A74C095E097F09EF722EA0EEE2611C931EA57B25EF521D7408C7DF2399F53B25 + FBEC2E85BE84BFB9E7F7AA0A2E1D90C24B07255F01B00B5FCDE310022BFC0DCE + 63C099725EB2688BBC7D0EB6A7B770CE853AC931681B2897B01769FEA563AE7C + 1EEDCE1E52E01B01C0AC473B0786713CA46C937D06E7429A758EE2B970CCCBC7 + 14D2725F5AA66ABB721CF613928DE35079974FC15E8FCF7812DFC551C9387B04 + 9FF9283E2FF2E70EE2FB475FFCCDFAB7E0FB30004F884B784B306BE0D78512A7 + F7EF7ECDBB9713E835CEB0E3AE8EFF6BA6ECEF8E817EFC7FE2B74548E9422DBB + 1703CCC396EDD99F7D4D1656999EEE09273D29F87530D680ACC9BC7299B71721 + A8C87572D4C15F7B5122BE0ED706AEB504A4B1B80EF95B35F8ABD72AAF49D8E3 + B8CFEE61BE90E100EFE4FDF4E0E7F5E8C4631A14E671791D12FE4EDCCB173D1C + 08661F8AC0972F91B8174B087BD735B231A5087F59C7FB91DE2BF059ECBEC1CF + C57B14F3F45CE6E73690CE3CEB0DF63235E9FD6CDB3219B575A98C8108790DFC + 324F186CDEC0A3706F1CB161A18C5CBF503D7ED5EB77DD0219BE7621C474BE02 + 5F03BF23D6CF0B0070AD425F8AE19C1B602E3D7E6B1BC15F7AFC0E5C561D91F3 + 00B63D7D0982D96E960C60F8663C6B06408397CE9421CB6047AA619E17325205 + C33B57E119C5E7952BF79B57A1219D097A072EAC6AE4F54B306CF97E84C02C9B + 1730D35A57EE55E3F6F835E06B5EBEE6F1DB7546818675664A5BCFAA1205BD11 + AF5FE42D1C3453EECBCBD4A921BCB3DBB3D70161E62D9CB37905D3D39790975E + BDBE18C699B09762DEC43A05C1F4D8CD765EC00A8173D02F3B4B3A6465497BE8 + 93B434F92839598170BFD2E9D2ABA4585AA724692867BE3CD72E3D3910C64169 + C9FA72DD4789DC7AC379F5D283976A9D98A05EBDEABD1BC7682DA887088115F6 + 728F5FDDEBD7EDF7FB1EC67FDCE3D7690ACAAE8E7B00BFCE3124C78E1339669C + 226F237D6D8CDB66E4DDF193546F8E419B3118738EE67EBFE315FA52067A99A7 + DD1FCF52CC13003B39EF5F03C0CC531C3F13DEDAD8992F4F522F0FE4781CE3EF + 2025FC7DB10F23F80C9497FA1100377E6993E3711FFE725CCF3A4B0DF65A1DF3 + 84C0D4333D317740D900B0857D8EBC508A7986CD35289B7F703FE0F0BCC6A0AF + CD6B586F60D8B7337D92303700BED1F6FF659DEF156CE5C75B7756D1CB57F7F1 + FD18E50F3BAA0C042B0CF6E6719CEBF980D7F20678CD6EB09669B81D6D04BA36 + 9765F42ACE59CDFBF75102E1B7DD5CB601F462CE1B944DB485E1AF016083BB51 + 812F520262EA11E4C3D0D7970160A6D1DA11E636E39A03F45000706DBDC0E06F + 58B61E616B133E008E6AE7FA461035CD07BBE1B513B3993DBC96A2F600EA3605 + 767D5B53F5ECCB7C34F8EBEB3E88FB01475B83A26CAD8A7A2008F16C6B5F8DD6 + BB503680CBB5AB68D2ADD182B6D69E619A0DF6FA7993BF56F700FA528DD6EDBC + 7A3F7F7B1DCED9DCAD3546D61C1F7B5661EF0304C341E4C17BB83E49FBA36817 + 5AF76C4AB6C6696503C4D114EE732700ACF5B61EFB471586BD5E5D04F2527709 + 80EF566E6DDB406F536ABC0E6E6BE58D8F85CFE5035C03BE61F96DEE20E3003E + 53684AD6E64EEDFC36BFA7DFEBE31FF74EEDFEA8C8951CC80DF11796432CCAF1 + A920FD1D001CE15F01C00CDBA3B60DCBE0A79FFF6FAB47FE1702C0D47D8F36D4 + 47C0AFD5E30F0EFF18FE2750533F66FFC77EBBEE047C4DB8E8EF428D2EE07F01 + 856F5A6145EBF36715EDF87F5CFEC325BAEC21E43F987C99FD4EF28F658AD6D7 + B7B1BD3D70230F665F68FB670604FE4022ACDF6B133E8E0D5CFCC18B5FF6DB5B + 9F701B551300580741815CDDED00381AC4B5019E2FDAACBDDF8775E101A4C91F + 9486657556CFD41FC89A38387EB0E5AB3A50BE1300A6C200D81F689B0C00EB71 + 022FE0B0F72FC1B013F21CD07342104C247C2F60967512C2098737C1084F326C + F2A0F6E038269BC85899C7573BDA6A186803BCDE64C726382C4720703091B2C9 + 14276211CFDE2600B08680669D376963DE267336A1A33404F42B6817C0DF3000 + E6BEBE963EF6BA03BD04BB84BF3E0036C06BE568F057EB98A24EC36871D21BC8 + 26C13E005608CC49376400389AACCE07BEE136B4B508C0AE4DE86D22EF835F5F + B640608B0584C004BF067F4D84BFDCE3F799763D5484BD91B7D3B9B001E91BEA + 6DDD5BEBB68795C91640280D7FC6FE415F7A04BFD87D80EA258675EEDA5FC3AA + 31EC33C3403B08DC584F6B5D2F9503C0E817EC05FC7C4FCFDB3780C0A6460018 + 79952E0CB910715C20D2765E5BEE1B4C8F607F51C916ADDCC295F36050203C74 + B4EEFDFBFA88712A7A3FA8C7C330B70F9ABF404671E18CA1F35E5311068F53EF + 8B77C64DD2C5B5D7461900C6F1C68ED7B4D568979AEDCDF19374F18EE0979E1C + 9485807E677283B72FC1AFED0F6C3683C284BF1F4F8B8B00607A01D3F3D707BE + 61A977700081A94F529026C5EBFEC0EDD3933425EC25FC75FB05A7400E0877C8 + 4C918E590CF19C2A9DB2D2A45336C4BDF372D2545D0BB3A54761AE867CA6B730 + 213043467781E821DC361BC7A5C73052867EE67EC09D358474B2742A42DB920C + E9569AE9007029544ED89B2E5DCAD215027729A33770007F5167DEC0CE3B384B + 7A055EBF0CFFCC949EC0160A5A853C434113FED21398DEC1B42BF0459E90987D + 1416C34E0F60F302664868F5049E5F26FDE6974A5FE4C3E097D097DEBF4C235A + 5C290316739F60B4093468E90C85BE4E9542984BA91730F71ED67D841D00561B + C4F2C8952E0C34F717E6BEC383098D51E65E9104BF14F7037621A3AB619F29E3 + 61D370D0EBD06F55B5EE353C8A9098EDD87EF34219BD2980189B16AB86AF5F28 + C3D6CD57C0A10064FB7219A35E722B5D1EA2D71CD3119B172B3C216CD110AADB + 1DA4613A7AEB0AF522B690D284BF2C8F473B852DDC4F73F76AF5E0A30CE0100A + B36C50D8EA099B5866CA32BD90E9294CF04B703C793F8EC1B6FB3745E0937921 + 123A198C7230D885888D3BBC41E28E203DB2496170FC61B7D72843CA6AD85885 + 70DB24E1E816493AB245528E6D9364A48975687F70834C3DB44EE20EAC91A907 + D6416B25F1D07A49AE23F8DD284904C484D187B62A444B3AB2AD91E89D482845 + 484830C57311066610FC22E5FEAAF4324E39B553524FEE9464E691AA97E82907 + 0D1502ABD7F03EE1DEB1DC2F38F752B0F7B0C2E4FD01F0AD532F63056184940C + 298CF350F400568FE3D37B24FBCC6EC9BBC810D0071402679FDD2DB9E7F7A07C + 503D80F375FF63B40D3C5B796C93791913F8523E00A60881098B099BB9B7323D + 75B3CFD5A972CEA35F9037084C009CCBBA0002FBE097621D9559BF5FD24FEE95 + 747A5A0720985EBF3917097319A2F900BE0F7E8EC30A82E9B59B8DFE5950E639 + C2E8E3D049F43D2C19A709C98FA2ED1149E37ED2F5FB603BA0C09B209E403EB5 + 1EDFFF697AEB3A286BF097FF478A657EC7FCFF52067D991A1466596DECC3BDAD + F1BBA2E8D96EE195ED3761FDDDFEBA3B24E19803BA3E54E5EF3A865EB0C8B38E + 60971EBE04BE5A175C0FD687D70053B6637FC2578258B671C723A8A5F72D7EE3 + 7CE122B816E9F16B8095D7A0EECB8B7671F88DB31F8FC1F0F076ADD9B979FDD9 + 35C894E097EDC6ED5AA39AB087D7FF7A998CFB045F1AD1E803D0584624D8B13A + 027F5947D1AE6D701FE1672100F6EF11E6E16B9F9B2F88E8758D94ED087AED3E + C63C5F6E6199F73B7AF6FA32006CF0972FC7F0DE385C81EF7C19B6669E0C5D3D + 573D7187AC9AAB79DA4732A4F306DC4703F84B183C78D52C1984FB3845EF5EDD + A777950BE96C1098A9D92803C0067E0D04331C34E16F1F3C7BFAE219D47F219E + 374BB87D00DA2CAED672FF05550A7C072EA0F76F15444FE02AE95D3B437ACD76 + 7BFA12F41206F79A5DAA36973AF59E8336DCFF57BD7D8B14FA32DF6716F7F58D + 0E7FE9E94BD1CE3D7D098059C790CFEADD0B19F4654A319CB379F512FC9AE76F + 7B0DF59CDBA8DC3A2755DAE4A56B5E959FDD08EE5ADEF7F8A55D3D842103C09F + 606CF34906F7EF4DD3D0CF6DD2D3A5756AAA8AF0D7F2EDD233A44D4AAA7C9440 + C0EB5E9E23F06D500A6CC9EAF94BF84B8FDE8F92307E4A4956AF5F8A9EBD84BF + CC1B0C7E3786E3B65879272656DE9EC27C1CDA4D95B7274F51F84B18CC94E097 + 7A73E2240DF5FCA64691816DC2E4E08542BE5C88F1E3A4181D875A486703BE94 + 7900FB1EBF04BD2F0FC5583818D35A9952081C405F5F0680399E6E10C7D70EFE + 8601B00B07ED5EC0B4F6366637C0EB43608AF09790979EBECCB3BD7901B3CFF3 + C1BCC0003023086914A100F41A04E67C83F2E71F3E00E63CC6E6339401606BE3 + D733B59766756E843412123A983FD95CCAD7139F749127087D03F86B220456F0 + CBD49BC7855FECE53C8FF3419B13FAA9825A48E7A99C8F725E0A999DD0F7216E + 69A4F0D7CD631F7B1BEDDFC4DC960A80AFC9B63F6AD802C9E02F6D0DE0D70F03 + 4DE8CB50CF940F800DFE3EF8EA7BAA8617BA1B3C7A2D6F75566FC0D707C09479 + 00DBDAC1EFC9D6277CC8EBAF5BF8795DA708D63E6CBD83B275125B2B09CBAF8F + B4E15A4DB01E134DFE9A8DADD7F860D75FC3B1BAB0ACADA9D953D1D79FC2F23D + 80C3E2FA97AD8DF9EB56BE749DCC5B5363EAEFD71B86C1FEFA5DB4353B5BE7B3 + 7A7F9DCF6FD350EF202F65EB8CDC72AE61DB39EEFBFB4C040033FCF36DEB97BE + 6847DB88276F20B77E1928B29EE9646B9CD647FBD1D6C4396C3DF50F2B0C803D + 871E2A0C7A23ED4CB0D95AF61F915BD3667A27455F0B6F7CAC16F81C2D22A9C2 + DE87F1B918AE5B433FA30DED56FF3B6ACC02EEACF0E7357BD833F86E15E91FA5 + 8EB2FADF6BF74744B6E4A06E4801FCA58C4135D487FA34C1B822FCCB0066D026 + 5AFB48DBB0BCBE91FC7F5B91A306C0F45F5577EF01DC007FB5FE7F1100DC94FC + 1F7BF807EF6C4D43E0868BEE2EC4F6512EF4FF1914BEC1DD8DFED1FEB7CB7FB8 + DCBDFC0755B81C4DE1FE4DF5690480A1C88398A2CD1ED014F3DEC3BB29FDD9C1 + 03DBDA40A7A901CF9DEA283D9637C830FB6D7D4203A23B01601FFCDE4936D8F3 + E12EEDEC1F1E145A9BB0DD06A44D4907AF900D52ADEC8B83E38730802704E61E + C004BD617120EDE70DFEFA3693D9D4CE34B417703400AC837F6F42610098799D + 98B00E6D4C36A1F027160FD23BF8B9C66FB0866575947A000700D817DB190C36 + 1B274BD126549C7429C07D096914001CB15B3BC8C02FE58786E6BE3D6E72783B + 0026F4F501707302E000EC1A00F661AF0F80FDB26F8BD4719FA360B2ECCB80AF + 415F1F0287A1AED59B0CF4FA00D8AF7B12767A01FB137C1FF086C53AB661DEE0 + AF0F7E15FED2C67D80DB769767DBF75400CCBC01605BC4B0850BDF1E51C78637 + E51B164A90EF806376E15E5ABDE5F96E7DE4A51EFDE585EEFDA0BEF26C578682 + EE212D3AA3AD170EDA441BC5451A83BF0A803D0F60DFFBD75203BB4C5956F581 + 3D585CD214E242922D1AE94255B05845E8DB2A58C0A222309879D8758FB3E163 + 150447C2DD0D45FB08081E23AF50C8BF8AB4D5080780991206BF31729CBC399A + 8B6E13E475A4AD4613F6D2FB7782A60E00BB32F7FF7D7B22F7FE6DBC07B0DB2B + 78BCBC3961ACEAAD896375DF5F825F83BF04C12E3CF46405BF610F601FF4FADE + C016165AEB833D8275AF60868446BE1D3D5AB8B09992A865A604BF84BC04BE5C + F4EC9895265D7233559D73325047CFDE6CE9C43DF5B2B82770A6F49E5E289DF3 + D3A44D46827AFC762FC994AE4599D221D70160868E6E9B9D2CED1402A7A27FBA + 7AFE12FCF6989E2DDDCBB2A44B71BA42607AF532EC73D7329CB334433A43DDB8 + 2F7039EC508F19B9D2AB324F7A56E4219F83B60C034D0FE042E959952FDD50EE + 86FE84BA0A80671546E02EBD7E29E609890D1447E0EFCC42E9330BB639C5D27F + 6E8902E0FE41BEFF8232E98B72EFDA12E935BB58FACD2F6F0480CD0B98221CEE + BF68BA42E0414BD1066A80BF56463FA44316CF90A18B6668286883C043354F18 + 5CE16030F707867D10E1F08AEA608F617F8FE0590A8047AFA3B7B0DB2778E41A + DAAB6528C346335CF4FA393282FB4F323435434AAF77FB538E583FDF79AE05A2 + 771B3D80C76E5D2AE3772C97893B57C898AD4B64E4A6450A44C6D25B4E01C932 + 0D973A6EDB0AD897CBB8ED2E1D43CFE26DAB64D2AEB53269F7BA4878E949843E + 04305B9622BF5A62F6AE43FD6A99B86B954CDEBDC669CF5A89D9B75E35859017 + 6D62918FDBBF4162A129FBD6C98400F210FA9A973043CB122EA937E101175E96 + D08965F36C24D8625F86A19E7C60AD4C398473D46D90A947B98FE866997A083A + B849E2D16EDA617A054387364A02A4501769FCFE751287FEB1FB9D5C79B54C85 + 2DE1C00605C184C4F1DCAB743FD27D1BD0779BA41E751EC08987B7EADEAA0E00 + BA70BA146133C12E612FC57222BD938FE0331DDE20C9C7B76898E9F4FA1DAA8C + FA9D92C690D347B748CAF1ED0A71A9B4133CCF36493EBA55C3F8AAC7F191ED38 + EF16D8B6A14F0092EB9D4771FAE95D38EE2EC939B707DAABA1A0E909CC50D0CE + 2378BFE49D3B20B9DCDB36009EF45AA6176CE6E903E8DFE0294B30CC949EB004 + 970648592648A51769F63942E0230A572903C00A5B919A47B079F752CC1B10B6 + 32417016FAA4050098DEBF7A9C33DC8B9790FA90C25FE699BA3C81303D83199A + 99DECCC7A0E3E8735845004C389C8AE311A21220D3B338E31CFE1E7CB704B1FC + BFF1EF370F5FFE9D84B3AC3318CCBC015FBF6C622873425E03BD94FD0E4C7C09 + C1CFD3DBDD01DA00EAEACB0CF486DF88DFB2FB8D9BB72FD3308035B13D8FC376 + 066E0961D996FDD4C6EB12D71EAF09C253825FCA5ED430A01A43708CDF38F7FC + E671791C3D4F70DDF17AA4681FBB73B5825FDD431CF9313B56693A01657AF5C6 + ECDB1409F76C2F908CDA827B49008369FBFFB3F757DF761D6BBB2F66C94CCBB2 + C02C34C96266669C62664D31CB96C56CC992C54659B264A6657BC1F76D382739 + ED9C96E4242D97495A4BCB656EF28754DEDF5BF5F451A3CF3EA724DBCB7BAD6F + EF8BA715F731E69803AAEAD79FB748A99B43DD17F6BDF3E555FF1E00FAAEB1CF + 159FAF15F65A707E2F679AF37DE4E79B9BF85E417CF70074A99B73F3828BEFB3 + 39E926182987C088FC2CA22320FB7ECC0170D4692FCF22A473067F71FE027F27 + 5D3E1A26D8F7F944D3A48B471CF472BEAF9CBFA80C8127DB98299C4F7FF56498 + 718DB38081C2C75D93DE3B1AC67386EF99031EB299337C27007E4DB1DE1EE7DC + 3117E017E1041E77FAA0FD7EE1FA25DC336EE0FD61D4F17D61F489B73D0F081E + 79ECAD30DAEAE239BF6F87D147DF74E81B21F0BE08750FEF75017CC71EA74F3C + F39732EDC05FCEF5E57C5F402F2E5F9DF14B2A084C7EC0EECD09026F73D80BF8 + CD01300E609CBFFD776C0AFD776EB6FCB6D07F57CDD98BFAD95CC4539B9F08FE + E6EDD4F5B6794CEF8DEB432F9BF390F6D9D4E8C05702FAF65C17C33FF76904EE + 5ADD3AEBBF666DE84B28E7F5EB5D7DAC0FEABD36AA97F5EFB97A757863E5AA30 + EDC891B0E5E6CD30E5E02107BB7DAC6D00D7B114E82BB7EFEB4B57D83C6FA989 + 633F56851E2BD678FED5C5DCE4B7C6E67D2B4297F98B421787BCCC11973AFCED + 3A6F51E83C7761E8C49C13E7EF129B039A14EE9914C8AB9B170584057B754323 + A0B74500CC0D92CC8F933477AEC1DF1A00CEF5C2446ED49C1CCF019E146FD444 + 110047E0CB3C3D07C08876D2C2E96B79C1E27656C6E9ABF501A25C057F73E8EB + EB0ED2047773C82BB05B5BDF445106F016670067EB2387BF09EEE2EC25A5AC75 + 943B80FBD6CEF6CDC12FA21E918F00D8D67A69BDA7F59C449DC02E6B43A5CAE7 + C0372FEB0666C15FB53DFA5AAA4B00B8FE9CDF1A08F6340988FCD0CB3D5D0FBF + 62EBEB576DEDFC1A7DEA0170EEFACD21B0A06E19F4E679F501FC22606F0E7FD5 + C68DE139E445DA7B28D7E57B1455F2D0CF963E844A7B1EDAF7501EE5FB25525E + EF79A070DA9311ACCDF76A72E54017D5015D6BD75E4E55FFBC6F2BCC0AD93E53 + 95B41FD51CFC45DA03F3FDB212F8BDEF4FCF172AEFA9E5F0577B6BC8F377307E + 688F4F7B80AA57BEE93E1F297D6B20983C61A149D97B8C20B743B89FB37FEF08 + 804D69CF3207BADADBBCEFF158AEED693603809BBB7E26EDABDEB3D83FAEA8AF + DB5FCE95F6A05BDABBCE95EF73D7FA93B6A4DAB8E6AF5503B7452868C06F82BF + FF48007CFFC3ECCFF37CEAEB9D47FC0A085C37BE42F963B4D4EF5E54E34B2565 + 00B8A94A632A1857CEB71C5E667D9436DB3F97E0679EFFEF5670D40C9AFE33EA + 6E01702BFB635ADDF748B8CF54F4110476106C7F70F9CDF02FAA66DFE8E91F9B + B71721A233001C3FA4927DE0147FDD0FE34EFA5702C0CD7C3996BFE0EE46BF75 + 7C53E53F2EF56AFAE353AFF20F94CA55CAC7B5D4FF9E0030FA1510B8493B80B6 + ACD4A6319AF094273D2DD52B9F3F56DE476D9ECF2643A80C8055CFC4AE0C8099 + C895F3F904AF0A00E77DD44F79B5797FCA96E613D45CF90457F9B298083F6813 + 6AE4C0B64239D82D83DE62A2DDB63E048FDABDAC7AE06F0E80ADDF7D6D6C32DF + B67301737103E700D8F3D6AE054273F2F0D07700C0A868D7980CF44A39C895B4 + C05218689DA38334261F2BF82B07B080EFE39D7B789ACB0170A1E601B0C240E3 + 00468F96DCBF05D4CD802F521F94F7931EB7C5F1137E5E523C0729173058F036 + 07C265B05B96DAABFA146D2D805FEACBCADBF34D830202F7B5344160C4F9536D + D34685363172E84B5E6542420BFE6AB3A4C350CB037607DBF586585FD2A1A31C + 02BB468E7330FCB4D5B7313D356894BB7DDB595D4D714386CD19C1DF4209024B + 79783741E002FE6600984D238595A3AC8D24415EB517F0D7C439662F9AF2F076 + 65C5F07782C073C34B33E6849780C233D9549B1FBA362CF0B4D3AC79B16E16CE + 0AC2EFB161B728749DB738749BBFD837EBBACC0504C74DBC087E9759BACCD215 + 962E77101C9DC00BC32B8BE6BB5E5D3CBF12FC92973B18F82B47700E80390F18 + 4730754A01C1405F897E0A152DF5E30C3B4B076C5817066DC1ED4B48E78D8190 + 87B18CF37753018187EDDC1A066FDF14FA6F690CC3766F0DA3F7ED0A437671AE + 1EE7FA6E08C3F64607F0905D769D5D1B5D9C1D3C70FB7A3F3378D81ED39B1B1D + FC8E32018201C0EE0436017D87593AF2C07607BE48F9B147768709C7DF0A134F + BCE579EA00C0E38EEF0D638EEE0EC30F6C734D38F96698F9DE9130E3FC2177FB + 8E3BB1A770FDE600587572037B1D67009F7DDBC1EFB893D6EF948D3DBB2F4CB0 + BA31A7EC716DCCC433FBC3942CFC33CEE0781E30672A5ABD3DEE5436FBB310D0 + 0E712F5A3BEEE0F7E873C0AEB13F4C35CDB88873F860987EF9A80360E02F0204 + 4F676CCACFC2116CA21F30191731E70A038081BE33AE1EF67382A7132AFADA31 + 07C43886394378E6FBC0866361BA69D6C70A0B7D3A4CBB4618521CC3EF16E14D + E77C7A3E2CB875212CFAFCB2BB8539DB72F6F5E80E8E61522F4720ECF99A838E + 3280651180E7F36B61FEADABE99C61CE168E400548B3F4EB0FEDDA57C3C2DB57 + C262EBEBE0C6C7018FDF77200C005E66FD967FF3715861A2BCD4C1710D02C7B3 + 8301C39C2D5C1F5E56EEC6153F7D160480177F67D7F8C1AEF11D30F8FDB0FCFB + 8F1DFCAE36ADB2312BEC3A2B2D5D65FD01BC02C0ABACDFB2AFED797D79352CFF + F6431BF34974037F1FE1AF03E01F3EF6BA95C0DFEF19733DACFE11788C2BF8A6 + 036080700EFF1C203A00FEDCDDBF11CC120EF8F3B0EA277B1E7FFE38869BFEEB + 8DB0FE2F31DDF037C02FC0DA1ECB9E172E65A02F29CFD7F5671ED3FE26EB43DA + 68ED1BFE722BACFBCB670E99A31318180C40FEC2F4A5E97307BFA49BAD7EABF5 + D9F69FBE0E5BFFFD2B2BC7B3691BFF8E13F95BD77A779C2667EC7FFED625C72B + E26F0382E29A455BFFCB0F61CB7FFE3134FEED6B97BB76FFEB9F1DE20A086F07 + E626D09B6B9BB573AEF2766BDB99FAE000DE6275DBAC9D6B6DB032CF09001C5D + C0DF5A4AFEA70202AFB73E00DEADFFE597B0F5BFFEC59E17E0F8A7B0E5BFD0FF + 076BE36F207C7384C7FC6D0E6E7FB1D7CE52FE3EFE2E77F25ADE81AE9555AFBF + DBCFAC4E6D1221C80965CECD055C8F9B001090D76F42B07AE501AD2EDEBF3F10 + FE3CDED420B04B8AEB9D3CC057AEE0BC9D31489F05F5A59F20316DD4D38FBA25 + DF7E1C08D38E6316F02B876D9E275D6C9F13E02DCF0D10BCC8CAB8791DF82687 + 6F0E80F5F9A41E003C07087CFB9A435F85788E510422F09D7DE352947DCF38F8 + B5FC5C40B009B83B1FE7AE7DD70074B94104773D376FF0FDA1B0CDFA6ED2F78A + BE9F6807F4027495E6CA6130AE5F89737E1B12001604462AE7619FE5FC9D7CE5 + 58219CBCC05D815ECAB87C275E8860589A6CE589E7EDF7E13DBEFBADAF69F285 + 630E7E11B017F89B9FD91BC337933FE4B0170730CEDF715606FCAA1E290F0C76 + E07B0257F041174078CC71C23E1F0C13700C1F8B10588097541058750AFDACBA + 1187F778186880AFC23C0BFE0E7E739BA7110AEFF032219D01C0B10C148E4098 + F290BDDBC3409B5B0CB279C6A0DDF1BCDFFEC9F18BAB170900037AFD9C5F9BA3 + 20F2A8E7A6F5A147E39AD0B371ADC35FD4775363E1F6950BD801F0EA350E76FB + 6FD818FAAE6F0CBDD7E0DE5D1BFAAC01F6AE0B3DADBDE7AA35E18D95AB438F55 + AB637E05619D57878693A7C29EAFBF0DD30E1F71F76EDF75EB1D00C770CEDC9C + B726BCBE6CA5CDF1968797174600DC7DD96A9BD3ADF632EABE6C95B7339704F6 + 32977C79C1D2D0D5526E2804FEAA9E1B0A5FB779A5C23B2BE4730E80A903F402 + 7C25CA9D539F4E090247C5F92FF3E71C00E7101831AF7E697284C080DD22CD00 + 70276B675EAE1B339517F4CDE7ED5C8FBCCEFC551F07C023C6BB5817F80DA396 + B26E10ECCD01B0D618A41E75687004BBAC65F2B37F73002C084CEAE7FD9A80BF + 5A1709F0B26E02F2BABBD7F454BF0880F3FAA780C29CEB6B12F8D579BF799D9F + 076C6B39D67EF94DBD126D82B702BFB9047FC9335E003897C6AB2DDEE01C5341 + 5E87C2A95CB886931E79B9773CEF9790CFB88081C0E94CE03CFC73EEF805FE16 + 6E605BD3037059CFEB9CDF5C82BF82BC52B94E7097FD8332ECCDCB79BD602FA2 + 8CB44F51BEB11DA92EAF275FB5775225DF434912B46D0EE2E6AA07BA35177073 + 7DD827BAEFA9B85F84D85B6A49C5BE5402C0DAEB2AC35FC9DDBED9F82A08ACBE + 65005CECCD212B4BAAD7FE5DDE96B7B72CFAC63DC75C02C2DA8304ECB67EB4BD + 87802EEF77D629C15DA900BAA5BAAA7D4EF5F3B4EADA25B17FFAAB94EDF1E6F2 + FDE5B4D79CEF3B2B5FB577CD9E75BE6FDDBC18D792AAF7D2551705B8E539599A + 806F9D4A80B759A5FEF52CA0A62AC6A0E759D5A6BA7BD19DC6E6D76FA9DFBDAA + 80C0944B6CA668AB53A9AE25CE9503CC529F5C45FFB2B2B175D7FAEF528F86FF + 6000D8EA9A83C0FF4100F0DDBCD1CB7DA25A88BD8E0A088CD466A26FFAF0FED3 + AAF4252395BFE0EE46BF757C53E53F2EF56AFE47A8BE3DCF37A7F2D896C6A8AD + 090036799DEAE9633FBCFEA35DF1632EDD71D25086BFA5C94179BCAE41DADC44 + 485058ED77BA8E264002BD92EA6B6D11E0E62098495CD5648F49612EF52B2BAF + CFC7028E73D85B96DF8D98F24C5AAB26BDA8AE3D9B0037274DACC9E79366002F + A1A1151E3AAFCF852318F88B13B8B59599DC33F107C892B67A3A4EFCB9B314E7 + 2FE1852817C03649E05763D55EB54829CB21B08D7359BE6A1124A05B285B5C35 + 5968597B3E5E00380F019D4360B97E25DAB4F87BB86304BFE510D0A838071837 + B0A50F77B27E9D597446C05B954A2D01E027EE0100CB219C035D8161C161D41C + FC95CA00589057CA17FDDA28C8A57EE481C03A0398CD86FC8E734FFBD7EE562F + 602F1B1FA92E87C2AE048181BF005ED25CCF0C1F5B57DF76C8A80481ED9AD459 + BB0030F90E23E2664D1900BB2398CD9D1200E60E7FF282C0926F0E4D98165E98 + 34A370FAE64E83B83915C170BE9105FC7D9E7ADAEF088067874E967747F08CA8 + 9A0338025FD2BCFCF25CCE025E125E59B0D4DD193900EE361FC8ABF3DB70FF2E + 3701829759592EE01A0406000BFE0279057F552FF82B18DC27397D01BE120018 + 018573D88B7BD8AF9BFAA91D383C70E3FA3078F38630C8434037FA5977A48060 + 52E0AFBB8039476F93F5D9BA310CDDBDD5C3400FDE41C8E74DEEFA1DB463A3E7 + 87EEDD1286BFB9C501F0C01D8D9E0EC1FDBB7B6318B4674318F2A65DEFAD2DD6 + 6FB343637702BF65D7D9BBD101F018C0EED13D0E7A01C023F66F73E7EF84E36F + 864927F7791BE59180E163C0DB0881C758FD8CF347C2C20FCE8679574F8549A7 + DF7647307017C84B48E8B1276A0EE0A60018F7EFDB5E1E736CA75DDB1E13500C + E0C57575DAD2770E84C9670E390476C76F02C03527703C0F78DA4540EDE1047F + 0F15CE605200302EE329F658D3DF3B14A6164E601B6B65803079EA0484675EE1 + 6C60AE1B1DC1D32F1F760730A077C6D523EEFAD579C1D4CDFDF87468F8F05498 + 718DF3236B9A890BF823ABFFE0640C4D7AED549869AF17101898813378CEA7EF + 86B937CE3BD400680036E67C7AC1CF0F9EFF5974CECDB53260647602249C290C + D4D519C3730137960AB4006400304019811A52C164F202C4AEAF3E084BBF78DF + 21B1C0F0224B177E7135100A1A08BC0820FC25E1A83F2842C72EF8F203771C02 + 9D00544BBE8DE70C2FFBE193B0FCCF00E20FADCF95B0F8EBF71D30AFFAF6BA43 + E6A5366EF9571F859596C7ED0BD07557EF77C0E8F7EDF95C0B2B2C1FE1AF8D33 + ADF9E17A586B5A67D7A5EF9A1F81B19F3BF4C5C1ACBF67F59F6F84757F89E017 + D5C2FE7E161AFF4EA8E778E6307096FCEA9FED7ABF7C1AD6FF7223ACFA11D7F1 + B5B0F2870FC25AAB070AAFF9E993781EF1CF37C33AA0A23D366710E302DEF057 + BB9EA580DF8D964780ECB5BFDC0C847E3EF07FFDF7B0E77FFDC9CF36DEF0D75B + EEFEDDFE5FBF72F7EFC6BFDB18D3669ECBDFBF0C5BFE0D28DC1400932F0360C0 + 68CDFD1AFB93DF60D7D8F4EFDF4470FC57AB33017D01B70060E0AD4030EE5EA0 + 6FEEFC05FE6EF9FBB761EBBF7D17B6FDBBF5B174D3DFBE8E10D8FA6F06DEFEED + 9B785D77EFE250FE264457720CEB8C384717C8BBF93FDBE3FE2780AF5DC752C1 + E17576CDD82742E0F59CB16BFF335CB802B939D805EA528EE74BDF2AEA083FEC + B0D7C651D699D57E9E35A9494E5FC06F55D9C559BB26CEB95EF64304BBCBBE07 + 0A0373E9A3F0E635E8BB14308C54B63640AFF27C26F2327056C01637BD42B273 + 9385DF68919CBF845556DB12EBBBDC9FC7CDC2BDCB6790D0EF9E4F9F3D523E87 + BA2983FC9CDB5743837D3F007881BBA472FECAFD3BC734EBFA05FBCEC0A90BCC + E5BBC2DAEDBB442057379E90EA7B84EF95FCBB45DF2BF4C9C12EDF451ED2F963 + FBBE4B22043E22AFB6E91F9E71CDB2BCCEFC2D835F7D674EFFE01DEB6BDFA9F6 + 5D19CFFD3D11A65C3911C33EDBF7EC0CABE30C60CA5312F89D60DFF388BC8360 + D3E4F78E8449F6FB35D934F5C2710FF14CD9C33B231CBF67ED77E72C79D319CB + DB6F510E7715F679DCC90361CC096E5C0212C73601E131277307F081A8E39CFB + 7BC0E12F224F9DE0AF00B04459E7FE12FE59001807B0035CD7760F010D0C0604 + E7D0D79DBFD6070D7B2B9EFF0B00EEB78B10CE51B87F4907EDB231E9DC5F80EF + C0048101C239F0EDBD6543E865F315520400EEB5619D0BE76F1F1CC01B1A43AF + C6F5EEF8E5CCDFDE8D8DA1C79A350E7501BFEEDC5DD7E8C09710CD805EE030ED + 12D09736E06E0F136346EEDC15FA376E28A0AF0444267D39397F5FB1F91FE96B + 4B56BAE3B71B0E5F8E13B13A40307920B040708C2643E8E745DE46CA4D877EF6 + EF1C22D1C490CFDD1A884E33D7433F53F78AD5290C34479BF8F12696F73E26F2 + 520E8075E3A4A479B400704C997B037F997F7353E6F4F0BC877F8E0E60E6EA38 + 7A81BA1CDFE2A19C13E01500162026A5AFFAB86CEEAF50CFAC03047FCB8E5FA9 + 5D0680DB26D05B06C0B9DB57ED2AE7C7DDB03E4202BCA4F5795B5FF5B314F00B + E0CDE06E0E7D75EE2FA2CFD37D6C7C82BCF9DA4E6B3944395FFB9525B04B3E07 + C0C05C4920D895AD57CBEB5BEA1D0217AE60423FD7E06F590F75AD77FB0AFEE6 + 0018C09B3B7E5596CA00B8AC020627A08B72C05BA587D3CDE60F3D1F616FB12F + 91DAC93F90F626F2BD8E3B4937CB37AB6CDF457B32C5BE8A2987B9F7A354576E + 13E4CDEB540FEC555A00E0B4FF74376A6EDFAB49BDFA677B5B85B27EDA5BF3FD + B524EDCB4955FB75CDB555A9B627688F99CE00CEF71FEBC0AFEA494BFB9ADAC3 + 2CF63193AAFA15E50477FDFCDF745DAF13F44D69F99A2D49CFE137ABBCE7ACFD + E6547FA7BDEB96C5B89654BF4FAE71791D7BF67F040046396BA852D51854D5B7 + 4A7733E66EFAFC2A35C38D9A03C075F5151C2BE75B05C04C7DCA7D9BF4CF958D + 2DF2FFDDEAD1F01F030097C23F1772F0ABB1F60797DF0CFF622ABFC9CBAA1A53 + 2FEB53F7C1BB0BFD2B00E03B28FF92BB937EEDB8E6D5F48747D20FD0DDB43727 + EE8CAA1A2B558D9168AFFB313695013072007C0F00376FBB5B95AF21E5139CB2 + 34E1C9FB345B9726602D294ECE6C62F81867724437B0267A9AF44955133EE5AB + FAE679FA3ED4E6259726A4551200A60F93DD960070D18FC9EFD37122ACC97073 + CA27C8AA2BC3DEAAFAFB33004C086826FA02B89AF49312D2F9E1676DC16269EB + A7E3A241FD242D34B4A0788085882D5C0AC85B520180D363A1BC1E69F1C36229 + 2F6B6195DF712BB0DB1C007EECA5EECD9E012CA9BEB8EBB7A32D145FB24523EA + C882B2067F01C248F98748BB7027B2A549C0DE725900B80AFEA2C7BBD962F895 + DA62D817C469718C72C08BB4A8CEEBEAC0EE5D4A8B782DFCA57CA18F68CFEF12 + 4782C0A4B87FDBB121D1674878AAF7E0F0745F20B0F5B3B2C3606B13EC150406 + FAB2995186C0C59DEF94878C72D08BDB17E89B8B7A398381C0A4DC79DFD6EA3B + 587F41E008806BF0D7EFE4CF2400EC9B3C1904CEEB0B00CCC610B078EC640FFF + CC6614E097CD244160F2A45D67CC0DDD66CEF38D2B60F1B36C3A59BD5CC0B904 + 7F6943946348E89A03D8433F37C490CF1265D5BD3277717875C192B899970160 + 36EF80C0AFF8396EA4847F4684F65B64E27CE09AA213388681CEC12FA9A0AF00 + 3069EF55CB0BE84B0AE4CD016F2E1F97C0B0F75D1D43457B48681CC09B1A43FF + 0D6B43DF46CE038EE017108C7003E3041E6C759C1D3C74C7660F053D70DB063F + 0F78D8AEAD61F0AECD61E0F60D61809587EED91A86BFB9350CDA69D7D86EE377 + 72F6EF464FFB53B77B6318B867531868E9D037B738E025F4F3903737390096E3 + 574EDFA19C179C20306504241E756887877F16041E7FE2CD30F5ECC130E7F2C9 + 30FBE2F130D1CF3324BCF3DE30D665F993965A7F845B18C5B19C1DFCA65F63CC + 51BB36E1A6EDDAE3AD6DF2990361DAB92361CAD9C361D23B07C3E4770E593E86 + 7C2E6BB2D50382A7B2917FC1EA2E44E83BE1DCDBB5F0D0B880CFBE6DCF75BF3B + 9581C0332E1CF674DAF98361CAB90361C6C523EEF805FA168E6041E28B00E1A3 + A1E1FD936126CEDE2B87C2BC4F4E85059F9C71F88BE67DF24E68F8E874984A7B + 068001C2336CDCF4AB27E2199357083D7ADA439A3AE0F830BAD824E02F6AB87E + 2ECC2374EA67E743C3273164F4AC8F1065ABBF71D1213060D89DC49FBCE77D66 + 004B3E7EB700360830933BF54805808BB0D0C05EABA3BE003CD6CE399E38151D + 52591FA013E0576163E725972120184045386AC0553C07D8CADF0188ED9A5F5C + 7147F172DCC55F7FE89079E9571FBACB17E1B005F6925FF6CD07110A037D7FBC + 9EDCBE388681C09F84C65F6EB813973380D7FEF24558F797DB61E58F9F7AE86A + 9C938496C6FD0BE003DE391C34ADFAE9D3C079C3847C46387449977DFF515867 + D76CFCEBCDB0F2870880577D4FC8DB8F1D0CBB1BF8EF9F850D7FFDBC70001306 + 7A2BAE5DD3A6BF0370BF70072F0098E7BCFEAFB7C25BFFA7BF8653FF8FFF35BC + FD7FF937BBC66D1FB7E5DFBE0A3881792CE0F286BFDD0A9B81B67F8BD7D88C03 + D8AED3F8B7AF1CD6A2757FFDD281A79CB148CED80881C99B92D3B5F16F56F757 + CA8C8F1099F37B01BF720573DDCD262030E0770B4E5CEBBBD9C694B5896BFDC5 + 9E2363EC5A5C53D7205433219B1BFF5D6EE42F0367E322C0B480706312F077ED + 5F712FF3DC2304466BECFA31B432E7E8C670CC005FE581BB405DC15D95396B1A + 688B282BAF7172F8027975CEAE423E93971B583098EB2DC189FB3DE761DBB5AD + EC7D70FD96A02EE5C5DF7C121625E80AE41504CE1DB982C138721137502CB2F7 + FE527B4F037AF96CF119433C57DEBBBC8FA95FFA9DBDF77FFCCCDEDBD7FDB357 + 76F3E20E5E649F3B857CF610D0F6F9F2CFE5AD2B0E791DF47E56CB3700814DB8 + 7D01BE0D9F026DF95E00E65E8D00D8BE2714B259CE5CA02D4057B097367D9FA8 + CC770F7D04750579057DA99BFABE7D4F9AC8E775D34C39E8150056C40452CAD3 + AEC5680A8484269D7AF564986CBF417CB74EBD821BF8449878E9986BB2691221 + 9D2F1CB5DF06937DA74FB6EFFA69563FEDD209FB6E3F11A69C3F6ABF2180E098 + 4E02F6BECBEF0D8037CACB670EDB6FD39130E1F4A1301E706BE9242B4F7CE770 + 11A27914003703BDC05FCA3880DDF50B2826B57E847F1E6BE9181BC7F81C00E7 + E0D761EFA13D31E4F3815D9E1F79F84D87BF43DDF51BCFEF05F212CA79E4C1BD + EEFC05F2028607EEDE1A06D87C61C89EED61D89BD6FFCDE4F8B531FD776E0AFD + 6C0E8100C07D6D8E81D377C0AEED56DE66F309E6191C33B1D5D3FEDB3687BE5B + 36BA7ADAFCE50D9BCFF4B1790AE57E5B39FB774304BF4900DF9E6BD7853738E3 + 773DF31B1BBFBED15DBAEEFA5DBB3EF458B536745FC1D9BD2B2D5DE9D09750CF + 127D81C002C13ABBF7F565CB1D00EB7C5F449EF657972C0B2F13EE79D1729BE3 + 1109C6C624F7AF835D13F34724F88BEBB70B371932C70412CFB77622CE109166 + 7604BDA8DB6C9BEF027E4DA494D506E0150016042ED7754A0058D03787BDCCA1 + A9D30D96CA47F81BF5FCC46971EE3D617A78C9CA11EADA1C7EACCDDBC7C410CF + 02BCF40304774865E02FA097B97D0740709AF7EB6650F2CF1019A805F85B77FC + 0CEB105BCB1437B226F7AF24F0ABB200B0D799DAD36E75005FA435126AC39AC9 + D653445A02F272AE2FA9DF7CCB19BFA9ACB60200B36EEB6D7D6CBDA6359ED67F + F99A0F489BD7A91EA9BFEA25C6C8C1ABB5297981DDC74CDCE0FC046D56273D6E + F5E8D16EB636F63A5B2777B3F5324E5EB97933018081BD1EE639A90E00D32FC1 + DF3200F671A92E07C0EA8B1CFC529FF6040AC05B2A17F5CFD78BD0CE85DB3701 + 60DF5B28EF6564FB1DB9EE7FA6AB292B677B277ED66FAEB4E792EFCBE47B31C5 + FE0A75963ED0166340278F06F7603B2B631470B340A7D09A90CEBE57C49E514D + D449AD80B2A6FB9E8ED21E5595F2BDA9F27E97EF73A5FDB05CC578F2CD8C4339 + FCD59E5C59F97E1D6A697FAF4AB5BEA80680E5C0CDCB72EBB67EDC94ED5FE67B + 98757B9915F5556DE572BEDF9A4BED7752D5D83B2ADF2B265FDA5B2EEB6EF6AD + 91FAD5F7276D49F5E3AAAEC39EBD03E02CE47325E0BD93EE0080F59C72DEE0CA + CC6CE5BE887293317790AED352DBAFB9EE3D8BBFAB8A31350380EFA49C7B9112 + ED56116FCB7D0B097EE6F9FF6E0547AD02A7FF646ADDFAB128CB3BF82D5CBD80 + DD246BAF2AB77A8090C8B53B05FE95A537BCA4377C596AAFBA862B7DD0EADDC0 + 4DE5ED0F34FFC5E06349FF882F8F0ADDF1CBABC215DC9CAAAE915FFF6EA42FE8 + FA724D553F4048E12DAADAF2F6E6948FCB55D53757017BAB64E35BFAA16E2934 + 74D5E4412A4F4A72F91D6B6912A4BA962630AACF457F5D47D7F26B24C85B9642 + A4684206F415008EF54D016E4B52BFBC6F5EC6F50BF87DD426D0E8012B0BDE96 + 55555F077B99E8AADEAE7F1FA166B24934AA82BA0E7199906B829DF52FF7A91A + 4F9D4FF67100DB02E0FEF61100B350D09DA0941F79EED5F0D80BAF87479FB785 + 0FA19DD3C282B40C7F0BB110490B172D54F270D2F982866BF89DAB6951D49C00 + BA0E786D41A5455539AF3E920030F0D7176669C126E05B76FFA2873A12368A72 + 8FF018E057A99CBF84857E9145228B47EE26CEE07082BC8F14F91AF8458F76EE + 1DFB768D2EE09660F0A32FD72FA4FDAE694B594C132A4B0BEA1CFEE600B80A02 + 731630673EE5E7026BF19E2FEC73A95D6DB9D35767FE52070C76006CF54FF705 + F80E0DEDFA59BFFEDC711ECB6C5620362F74177BBEB9C166477BCE0D6643246D + 7EC43BE147850E387E09056D79C15EB98055961B18F8DB0E58CCD9C0090247C5 + 8D9AA62EE009A1031A15CFF912F47D86CDA294CF1DC0CF8E9E1CDAB309C4A6D0 + F8183E8E0D2ADF804A9B4A88BAD7E72E76B159F59CB5A32A00DC719A00704301 + 806B6DD119DC89338171FC12F219D76F82BE0A095DB8811B16866E591868E06F + 04C0C0DF2526806FEE08CE1CC00BA95F64F584F78BE057101870AB10D0E47BAD + 8A1038E6A3ABF775B9865746C02BC85BB8811310EEB32E3A7E15121A004C9E33 + 8091EAC80384391378D096E804261C342098323078101BAC5BD6FB79C0437612 + FA79AB835F40F090DD5B5C8376D63B8001BE03766F08837103EFD91C06EFD914 + 86BC49C867C0EECE30FCEDAD0E8111799DF9CB19C09C070C1C160C1E97202ECE + 5FC15F81E089A738EFF7AD30E6F89E30FED45B61E23B6F87719CE16BE531D63E + FA186EE1DD4DC68F3BB6D7C1F2C80496E52A9E789A70CD47C2249C54A7F6BB13 + 78CA99C3618A6FC64717F08433FB5D93CE1E4C4EE04361D2396B03FCBEFBB603 + E049EFED8FE700E3EA3D77C0AE7920CC78EF7084BFB8802D050003871B2E1F0F + 8B6E9C0FF371E126E72F9A7A21B983AD1D5730E70913FE79E9AD7361F9ED8B7E + 0E304EE0D99CF98BCB9730D4578F8519EF73DE24D0F768D2310F4BEAE1482FE3 + 583B1921F087EF780853DCC100608536459C73D9701D973070181072DE04E805 + 02A3989F0D64F9E85DBFDEF40FCE388801C80081056CDCA927B06B72D06BE9A2 + 2FAF3908467E96E72D14DB1C4C99E67D7EC51541F0070902473994026ADD8E70 + 78815D67E15784B6FDD0C12A0EE0855F5D0DF3BFB81C01F0B79C3D6CF55F7FE0 + 6710AFF83E3A7C1DF49A70FD7AE867ABC3E5BBDE5DB737BCBCD2AE45BF75C0DB + 5F00AA9F3B007697E85F6E3BD893B353604FF02FD64500BCFC077BDC1F3F7147 + 30753C4F7700FFE54658FBF3754F37FEEDB3C2FDBBCEEA36FCE566D8F8D7CFAC + 3EBA7C71EB0AF8FAF3FCE53307C11BFE72CBFF8ED5F69CB7FD4FDF857DFFE7BF + 851DFFC7EFED9AF1CCE08DF65C1BED5A3C86C3DF048FFDBA7FFB3C6C02EEE2EA + FDABFD5DBF7CE97FDF9A5FEADDB0C8DDBE0E8BA370D03A88B5E7B5FE6F36FE2F + 5F854DFFCE19C03F144E623980D7FD85761BFF577BFEC9DD4BBAC1C66C34E100 + 96B6FDDB77EE025E0F6837C5F380EDEFB1FE6BECF58FCE6A7BDDEDB559EB672A + E3CC05DCE2D6E579E25A8EE7010BF4A2E800C6098CF3F70BEF2B780CE415B857 + 5E6E5F6E2C10E0A50CB045E497FF18C33293F2F8BC4EFCDF81B80055BF39C0C6 + E93DA2F78697B3EBC5F3AE09CD1C6F28A0DF2A5CE600DF6F23F0F5F49B8F1DBE + 028171BECBE1EB30D644EA7D32F8BB10172F219881B57CEEEC7320B72F375B10 + 021AA8CEF3E433C7E78D1B2B047C51FCFCD9F56FDBF5A8FBEA23CFCF35A91ED0 + 2BC7AFB7D9E79A3A5277EF3BE4B5721265003050B8E1531CBFF1E611B97801B9 + 02C0DC4C429BE0AD6E38219DF6817DA7593DE914A21D581D63A873F76E0680D5 + 57FD01B953AF9E4A80374A65C6CEB4EFB6E996E7BB93337A39AF57A23C959B6B + 52DB0442385F381A265F3CE6CE5E00F0E4F72C7DCFC6994863D9EAED778530CF + 847676D86BBF291314EED9F2387F2778340A6B7FE770E1D605E04ECCDCBBA38F + BC19461CDE1B46709EEFF17D614472FD028147A63C7D73C83BE2E01ED36ECF53 + 3FF6A8B551EF9037867AA64DEEDFE107F6F899BF405F802F9017E88B80BDD493 + A7DEDBDEDAE9B097D0CE8389206222D433E9903777783DAEDF18FE795B11E2B9 + DF76D2CDA13F51482C1DB4DDC60B026FDDE4C2E9FB86CD6708F5DC6FF3060FF7 + DC6BC37A0FF1DC6BFDBAD07B433CFF37BA7ED7FA39BF9CF7DB33B97D01BBBD56 + DB355602805785D7962D0FAF037457ACF494324017F0AB10D1E471F8520F0026 + 9F3B84550F00264A0C00984830DC04080026DF6DFE12BF61D0C3410380B98150 + 8EDFB99CEF9B22CDD8FC92B46B430CF78C0318972F40D701B095DDF96B6DAA8B + 807776A18E9CF96BEA34035167ED1C7B62735D606F676E7C641E9CE6D739F0E5 + E6CAA6003846E2610E8E0BF8F9F1692E3EC6E6ED63A739047E66D4E4D00EA03B + 3A1EEFC21C5FF3FE76690D40BD5CBFCCFF49DBDA3A8236D60939F495DA09FE0E + 610DC29134B6CE488E5F77FF92DABA06C00BECA58DB58DDCBE0E8413EC6D936E + 8825CDD7475A13B563ADC4CDB429C25219009347EE06CECADE9EC15F015DF272 + FEB2CE63AD49BDEA8AB5226B44FAA7B5A0D6946A63FDA9F56ABE261504665DFB + B8AD6B1F677DCB7AD854AC796D2DCC7AF9515782B8A4B8794B7AD8EA05800B30 + 0CD04D30D841AFADE5E38DDCB6BECFDCC00FBF94E0AEE5057F057C0B086C79ED + 11E43787E779DCBE39F495F27D094160EA180318E6782CED6794E16EBE2742E4 + B5D61DE25E89F6409AF4672F25EDB9B027A37D9B5CDAAB79B05DD7F090F57FB0 + 5DDCFF5194B83C5A5CBE57F460DA272A4B7D048455CE95EF45E50038DFF7D2DE + 17A1A1F3F0D00E7D4BFDA502FA6675ECC7A11CEE5695F3FAAAB67CAF4FE57CCF + 2F3FFFB70C802501E07C1F33DFD7AC92F641B5C7D992F2FE6595AFABBE557577 + ABF29856EC173F54DBABCEF797D556D4998A7E0FB097DE74EFBCACB8A74DDABC + F2BEF9D87AB5B1E7627A38A5BF51390FA8529939E4AAEA8FAAFAB6A4CAF1191B + A9BA6655DD3D4B303B572567B2C7CAEB2A98959896B8569E4A77C5BF2A41E83F + 58558FFD7B3F9FFC5ABA76AEBCBEE8FFD83F37002E877376476F0E7EAB407049 + 110037F3A2FC0B2B7FE357A96A4CA1E2C3661F524B0582ABD43AFBE268EE4BE1 + 77F9B2F80DBA9BC7BF531FFD7DF7AAEA1F9ABC4FFD0F0C3F6892EA9AFB418A3F + 6A09D656B44B79BF96FAB6D427FFF175E9795ADF266DC8FECEFC07FF6ED5D264 + 82FAF264A5A5BEB9F2BAF2C4C765932CDD6DA7495831F132D540703E79E3AC8E + A613BBF2244F75E53E551346D501811FE62E49CBE713D3AAC92BD204369FC86A + 325CF4FB93D5651367C1DD7C82ADC978B91FCADB7395C716655B10DCDFBE1A00 + 036601BF8FBFD8DD8513B8EE8ED29234DE952F544C02C12AEBEE561E03B0CC82 + 285FE814ED69B124A0CB622ABFBB364FD58F31DE57632CE5B1EF63B1637DB598 + E31C609D059C87806641F8C84BF6377702FE5A9BE51F95EB97D0D0565668E847 + 3AF57208EC02FCE2F6050AB3902D16AD69B16BED9C210CF80508E31ACE21B0F2 + 38811F15E4150036517EB81BA1B1EC1AC0DFB4D0CEFB68D1ADC57ADDA23D2DD8 + 9140B0206F0E80B5F067A3A02C402FD057F03777037B7D6F20F0F0D016E0DB67 + 884360CF5BCA6645FBFEA37C1323862E1B1ED53F9E09CC064987C171A384CD0E + 41E176834646083C843BE32D0FF0B5BCA02F02043F37727CE10A768770067F63 + 28E8E802EE3082BBF8734DB0BE80E049BE4104E065D308917F7634A1DFA26897 + 6823AC1C9B51481B5172FFB241F5CAEC052E36B080BFCF5A1B120416E8ED345D + EE5F80EF9CD079C65C4F1DFCB22966EA3C7DAEBB803BCD9C17BACE5AE0EA62D7 + 465D1B0809BDC0DB3A0388D99C4B0E60DC1BE439C72D6EEACD0F9DE7583F53B7 + F90B0B00ECCE5F4B05805D4BAD9C9CC038803D2CF43242422F093D572C0FBD57 + AF0A7DD7AE097D56AF0E3D962F0BDD972EB514E8BB22F45ACD99736C424AD411 + 067A75E8B77E5DE8DFB8DE5DBF1E167AB55CC000E0F5A6D83E78D3C63070E386 + 3068D386307033619F3778E8E77E1B70CC343A081EBCC5FA6C6533D6FA11F639 + 415F0030653470070018E7EFA6D8F62680776B18FAE636137980EED630623F67 + FDEEF474D83E42436EB1746B1875705718731817706C1BB19FF380777ADDD8A3 + 847D8E1A7F629F0B583B0A6730E0F8E8AE309AD0CFA708EBBCDF5300F0381CBF + 9607FAA20879130C3E12E12F650030ED1308396DD71E7BFCAD30FE6484BFEE02 + 260CF4D9C31E1E5AF0373F0BB8D0B9A4E4FE75287C9673840F84E9EF1D09B32E + 1D0D3371809D3F58388001C04BAE9F0BFBFEEB7761D39F3F0D332F1F0B53700A + 5B5FE0EF94F708337DCC74C4F207FCECDFC537CF85A5B72FF899C094391F78C6 + 551B77F9480CFDFCC149D7F46B11FA0280631A5DC0800D602D00781AE14AAD9F + 1CC0A484368DAE60BB06E70A7F7826CCFEF84282C011A6C82D873B2E96DF7530 + 3C97D0AD372E8679372F99A253588EE1C55F5C0B4B0843EBB0F74A58FC792CBB + 0056B7AF86C526CE1576C7EF9738092F85D99FD9F500523885BFFCC8B5E08B08 + A304981C6A59FFA5DFE16E8C5AF2AD3DD6D780AE6B61993B7DAF7B2868DCBF9C + 35BC1228F7D3A761B5BDEEA4CB81C6DF7C1096D9D8553F72362F60D8DA2CBFC6 + F2F1BC5DFADE0C6B130006E0E5E7FD5216E893BB33023FBBD6CF371CFEAEE05A + BFE0FAC4157A3DACFBCB8DB0EE174BDD954B68E65B09087F62E9A70E81D7DAF3 + 03F8E2D875D76E02BE8483E679B93BD8AEB9D61E6BF54FF10C60424D13669AFA + 467B3E8C6FB4FC7A5CC57FB5F2DF3EB7BA0887D7D9736DFC0B8018F86B7F97FD + 7DAB7F026C47F7AF423E0383058455EF79D3A6FFFC4DD8FC9F71FA46F7B08790 + 06B6027B130826CFB5D7D8B537FC253A7B81C1EB7FB6BFE717FBDB6CAC00F056 + 137584DBE6F56E7437F0D7F637DA6BFF337F1FB0FA5658F9B3FD4FFE7233AC01 + 88DBF3100406E8C630CFDF3AEC05FCE2000602AFB6BF71A53D8795762DFAEADC + 5C422CAFF8332E5FEAEC7F64AFE5AA9F01C0387EE3F9BA9C374D19272E655277 + E512BAD9C475FC7900767F04F27FE220983222AFF37781C0DE278164F2B40165 + E566F7FEC05C1CEE96027B01AF0058EA389717B03B9733B8EDF330EF73C23ADB + 7B1930ECF5D71C0E3B00B6F7BEF7E173F6957DE6EC3180C07C7680C03C571E7F + EEEDCB568E67F8F2F95A689F35C2ACE306263FCFEA16705DDA81B749F42755E8 + 67D5CFC2E99B602FD017D88B282F4C509832E1A03D2474BA8104082C109CCBA1 + 2E379F2487B043DCF7ED3B2B015D897274010388CF8629405CD334BEFF3E3A17 + A6DB77D7D4ABA7C3E42BB87981BE36EEDA19CF4FB90208C6097C36CC78FF4C98 + 76D9EA2E9D0C932F027D71FC5AFFCBA7C22442385B3AEDCA3BD66E656BA76ED2 + F9E361F2B9A311FA5A7ED2BBC7EC37E1789870369EDB3BE1DD23EEFA05F68E3D + 65BF5BD4D96F4D3CABD77E3FCE5A3B2E5F1CBF27EDB7E4A4FDFE98DCBD7BBC3E + 8CF3B8E3FBC3E863FB1CFE027C872710AC10D00E7813FC05EEFA99BE87760760 + AF3B7E0F9B0EED758D4407230446EE003E60DA5F83BF387881BE23AC1ED72F6D + 40DFFE3627E84F286720EFDEED2EC15F04F4150406104708CCD9BF3B2CDD9600 + F06677FFF6B37907E19C71F60ED8BA290CB6F681DBB73A00E6AC5F0030201808 + DCC3E63B387D5184C08480DE10FAE0F85DB3D6E63F56BF3ABA7DDF58BE2AF45C + B926F45AB536F458C1F9BEAB1D02037FD16B805C135017C9E19BE7E5FC95EB17 + 91C7494CB8E757175ADD224B172D0F2FCFB779A11F1DB2AC96CF01B06B71E862 + 734C6E3664AED90D47F06CE02F11696CBEC90D86967625428DD5BF6C7D5EE1E6 + 442B779E312774B17969979467AE2B71E363ED06C8944FB01710CCFC983202F6 + 4AAAD30D97D475618ECC189B5BE3F2C5D9FBC284E9E1C5F133C2B36380BD710E + DF81397D8AE803D405F222E5FD26CF31361ED83B22C15D1320B8DDF0E4EE2D09 + F8EB9187FC86D5087DCB8E5F5FD3D87AC6FBA436CA85A3D7520160D2DA59BF11 + 007B1F53BBFE235D7EAC4E82BC72FB5609E7EF9F4CC05FD669ACE358DF39C4CD + D67B5A0B0A00AB9D3A01DE7CBD99AF33D5A675A643616475AC557DBD0AFC3569 + AD2BD5D6BF11E66ADD5CC0DDAC0E3D96B539F415DC4DEE5E815D04F815FCAD03 + B82895F31BC31F49FDD4B7C918137B058A2A26F0AB7D06F6170481F37D0A7F0C + F6279EADED6594F73A8ABD11D4AE4B74F776E812DBADAFB771737DEA47BBF65B + D893F1B0CC846AB63CAAC15FA9731DF0BD93F23D238DA3DC9AFD281369596ACF + F7A4BC3E333A94F37939DF132B4B7B64CAB3EFA6BD3894EFD1A1F2BEDE9DD4D2 + 75E23E62D330D02AABCEC335A77DCBDCBC52B78FF907A86A7F55F5BF45805E4C + 47F7B3A79CE47502C099CAFBD4F9DE75F3A26F9B3AD5EFCBDF8DD8F3FEFD547E + 3E6555B108A9AA3FAAEA5BA57BEDFF47A8CC97720E55A88A59E5CA60A65817E9 + DD991F6DEC1FADAAC7FEBD9F4F7E2D5D3B575E9FF7CF81EB3FBB0AE05B5605F8 + AD57332FCA7FB7B20F48FE814BE24E8C783746ED03D98A0F6DF64556F5A1FE57 + D09D9EBBFEBE7B55D58F4EBD6A3F2EFC4891E61018E57D5A52557F5D53F97B51 + 7E1DA900BC7A7ED6AF1EFCA61F6E527ED4EF657290879096AAFAA1525BF931AA + 1E53CFA54A55F0379F8C6992964FE4E25D7B4D277448E1A19B13D7C0EDDBDCC4 + 1051EFCE5DC6585E93D31CF2E6E55C9ACCE6935BCE39D1C4F9BE3FD9389326D1 + F9241BA95FAEBC5DBAD384BF7001DB228105060B07003065E02C2E60E0AF00AF + 20702EEAEBDA12F0CDA16F2E2D7ADCFD6B52B94A82C0BEC82A01602DDEC8E7E0 + D71763A9CC62ADEBC0B1A1CBA0B10E7CE9CF224EF01769A1F700D7F7F6D7C313 + 5D6CB1C9E2D1EAE3E2F08DF0A8951D16933AECED111EB48520D2B9C1D4F95DC7 + 5AD0028313FC7DBCAB2D762D051473763020D8A12F8E5F2D86E903E4A52E83BA + 2CBA154E4B75AAD7221BE58BF362319E2DD4CB8B74DD099E43606D0CE4E03797 + 36114801BF65017DDBF71F1EDA0F60C362B803604241B7EB3F2274601324DDC1 + 4EC868DF1431017E73F72F1B1E7163C4FA0D1AE900D83598BE40E051A15D7202 + 037D15225A50586A6F7A7AE818DFA489EEDF492EE0AFF2CF8E9A1C9E03F23AD8 + 9DE2F9A20CF0A53F7D533B8E02368A08072DC7AFC02FD226956F64A54D2DCE0C + E6EC60C15FCE024682BC2F4C8E75C0DF2E3317585D84C09D67B07136BFAEDC6D + F6C2D075F60287BD80DF97E72E0EDDE610922F6ED0BD3C2F3B03D8E16FDCD4C3 + 05DC79CEFCF0D2AC86F0E24C7B5EB3E7585B0C01DD6DFE7C1B03149EEF657701 + 2F5EE400386A61E8BE748943DE57172D0CAF2F59127AAF5A1506AC5F1FFAAE59 + 1BC1AF89BA9E2B23F4050493027E7BAFC1ED0BF05D13FAAE5BEB10B8EF3AC21D + B26919FBD106F81DB001E8BB310CDDBAC501307D290FDEBA39F4B7721F00F2C6 + 18127A10EE601361A0057995E61204765730E0771F67FE6D0F43DF02F4E2EA25 + FCF3764F112058E50880F7147098F2D8237B4D6C4C5BFD21E02D20F7AD30F6D8 + 9B61B4B58D3ABCDBEA7786918777380076E06BC2F53BEAA88DB7FCC4D3006342 + 3D47F89B435FC6090CE3229E7266BF95D90CDFED1078D2A9FD61D2E9781E70D4 + FE30E11DBBDE3B6F3B0406FAD603E14361CAF9C3EE089EF82E2E2EEB678F3FD9 + DAA659FD8C0B00E0A30E83294F3B6FFDADDF92EBEF8503FF879FC2861F6EF899 + C25C63E6E51361C6A56361AAF59D7EF1B8A740E5A9970E79D8E7391F9D72F72F + E017E1FC4535F01B5DBFD3AF513E1966BC7FDAD277C2745C6D260F070D30F910 + 181C81AF00B0F23AE312E88BDBD75D74387D530869E02FD097F0D0B4EBAC60EA + 80BE40DE859F5DF67A04E05D4E58DA2F3F88670C7396307D70FD5A99BE8B6F5F + 0B0BADDF7C075000ADCB0E81DD097C9B3344015280A9780EB05C87C02F801600 + 0BF80BF405002FFE8650B6D111BC8CB0B75FBF1F167D75CD85CB72F98F84DA05 + B2310E98F6818DE33CD4EBAE65DFC5740D3017A7A75D9F3386391795F0B80278 + 02BD39F825EF67C75A79E59F6B21A0579914FE19188C035867FE028049710347 + 006C7D7FFA38ACFAE1E3C099BF805FA02FD05AE1A023D8FDCCE1EFDA9F3EB3FE + F171FDF14C6BAC3FE707038C81BD6B7F8EE708E3706EFCF9965DE7968F5DF7F3 + 6D7B6CC1DF94BA9BD5EA13ECE5BA0A878C73557F1F7F2BEED12D9CC58BBBF62F + 8482FECA6132A20E0184B9EE2A7BED1AAD1EF72F219E5773A6F24FF6B7242730 + 02FEAEFEDE5ED7EF6FD8F3B3BF87B0DB7FB6F447FE0E1BFBEFF67CECEF5BF6A3 + FD7F7FB4D7E8971BEE065E6D7F670CC96C63FDF9D7A0AFB4CA9EC30ABBDE727B + 5CD2A8087301B8B88205856B8ACEE2BC9EBCC02F0E60DCB9A49CDBBB82737DED + BD22276F017BAD0C744502C14A69932397F7338E60DA16F299F982F0CDC0E18F + DD99BBC86F86881098549F05A02C65B977BD3E7D566A6768037CA3B35E2EE079 + F6596B48375BCCFFDC3E8326AE371BA7AEF507007326F022AE29009CC0ED2CF4 + E9C5C2E52BE88B949F4D3E81DF06FADA7571FE2AFC3375B33E21EC33C0F79C83 + 5B60AF2070399CB3C0700E7D15DA19382CA76F1C77CECA67C2E4ABA7C32460AF + E5A7BF7F364CBD667540DC4B271DF8528722048E30D875E59D30F9A27D1F5EB6 + 3ACB4FBD6CD74EE9C4F7ECFBD9D2E957CF3800263FC5FA4E3A77CCC1EFD40B27 + 3D9D70F6880BF88B269E3D6ABF19563E7DD0013075E3757EEF296E3CB2EF7C2B + 4F3A6D3A65FD4F58FD89E8F84580DF29760DFA090A8F3B75C0DDBE388111E19F + DDC97B383A7BE5EE15D8C535EC30F8C06E3FC317F03BDAFA3A004687ACDFC128 + 202FD057E19D29038081C2434C83F76E0BFDECF79F70CE02BDA48870CF72FB12 + F2993C70B8E6FADD1CFA6DDD187AD99CA30FC0775B04C0BD3772AEEF7ACB0380 + 7784413BB67B7D04BF36BFB1BEB4F5DEB8D1C16F8F74DEAF0360B46E7DE8BDD6 + FA9A7AAD5E6FF320EB03F45D0E08268DF91E2B2D5D09088E4EDE5CAF2EB1F998 + 09C82BE7AFDA3C24F4229BFB2DE2F80E22B8AC30ADB4B91C1078797865FEB2D0 + 6DDE9228E69096BEBA609969690AF36CF3C8390B4367E69A696E89987776B479 + 6A279B8B320F05F876B31475F5083596E706C599F37C6ECB9CB6EBCCF99E1637 + 34FA4D8EB5634F341FF6B9F3A4387FC601ECA9D595E7D53EA74E375D9207FE92 + 320F7F66ACCDDD09F33C7E7AD484E93657B77A9BBF0380C93F3706A76F74FF7A + C41F45FBF170D1536CCE3FD1D61711EEE2F265EDD0AE047C1DFA0AFEDADA05B8 + 9B3B7BABE4D0376B678D23F09BAF7DA8AB03C17D6CED652A222925F09B436039 + 7DE5FA454FF6B0F59C496BB91CF856496B41F25A1F6AFD98AF35C9E7F57999B5 + 25E3D4DFDB5E8912048ED05710187143740DF40AF2E672009CFAC9ED5B387793 + 047FF33A41DD2AB1BE674F407057378397DBB5EFE0FB01C901ACBD06F25E664F + 82BD08137DEF7FB65B1C6729FB11F91E45799F83B0CFB87A1DF2EA667A52ED9B + 58D9EB4DBEAF92F65CA47C2F0680FC60A6876C4C7400D70360ED11E5FB41799B + F693286B1C65ED3549F9DE93CADABB4239E4CDF7BCAAF265A94D7B64ECBBB54E + A9F6E0AA94B72B2F95F7FACAEDEA53CBD7C0AF40B054B717F9787DD4C2E6A43D + CDBC9CE7EF663FB425E57BAABFA7FCFA0FB70BAD33E0DB0400A77D66F6A2F33D + 6AF27924CCEA3CFDE8DFA690EA62FDDD883DEFDF4FF973A952158BC8F56BC648 + F7DAFF8F5025006E55CFA1C4A972905B077633D0D96C9FE6A4B17FA4AA1EFBF7 + 7E3EF9B574ED5C797DDEBF0AB4FED3A8F563F62679CC9E6805F4CDD504F896D5 + CC8BF2DFB5F8A0D807A6552EFB50BAFE54ABBB3F42E0AA0F739DB0F757D5FF93 + E84E5F84FAB2FCBD54BB6EFC61D18F55FD8FCDAF5773D7D2E3E4E20752CAEBAB + C64B75C037CBEB07BBF8E1AEF8A16F51770B7F537BD5C4A45C2ED757A90A00E7 + 13311421703E816B19F222F5CFC7A1AA0965B98C8A89699AE492CF21AF94C35F + B59527BA3900CE954FB699646B125E6E2BF741F9A4BE39C5903E11FAB28868A5 + BB49931BD8EBB23497161E75F529DC737320588B9EBB01C04810580B2B445E00 + D81768B6C87AE4C5D7FDCE5CFAF3388C69F76AFF306BF5B630A77157E8608B5A + C6B1B0CB177B2CEA501E360A404CBB2FF8BCFF6BE1B14E690C7D00C7963E94C2 + 4AF942332D489B2E4C81C2C9018C5338B98615064B8BE2020477B53C0B663F33 + A97FB10817F4D5425B755A689701707321BAD4478B7D49777F0B046BB340B057 + 1B086A030617D0D75DC100E2C1EEFC6D3F607868DBDFDAFB0D3171377AAC7F66 + E0C8D09ECD8E7EE96EF601B5B3AD74A77BBE2902006E3B704484BE84801E4438 + B4E80A7667F0D09A0BD8A12F21D880BD49DC6DCFE60C77DCB39123E82BF09B4B + F5ED8747A770B90FE05721E53C3F76AABB09D00BE3A799A6BB5E9C383DBC3429 + 6E5A71F62F1B57C5E65502BE2FB1F935C5F2531B4247423FA7BE71832CC25E42 + 4293EF32330260CA5D672E08AFCC59EC10B8CBAC0881B53107087E1507470AE5 + 07FCF5F3DCE62FF6325098B380BBCE5B103A35D863CC9E63F5F31DFA76993BB7 + 00C00827F02B0B080BCD99C03144F4EB4B968637967076306078B1035FDCBFBD + 56AE0C6F2C5B66E595A1376107ADBEBB955F5F46E8674241D72070740113E619 + F8CBF975111447D7706CC3098C066E0406E3088EF9C19B3786815B7001E306B6 + FCA6C6D06FB3B56F69F410D0FDB759BA7D4318B81DD80BF8C505BCB9CE0D4C7E + 302EE0BDD1013C6CDF8E30FC6D04ECB5BAB7A20482A98FA1A17725F76F04C0AA + 1B7508B7EEAE30FA08619F23008EF01707301BDA3BC388233B1CFCA291477686 + E187EC9A968EA52EB97DD1A8C300E03D610C21A0AD0D37300212134E7AE2C9B7 + DD61EC2E604BE502262524F484536F878967DE76A80BF41D77DAFAE2323E6DF5 + 7E26703CC7D1C37ABEB33F4C3D07E4258CA75DEB9DB70B100C04A66DDAB9C361 + D6E5136121E08310A25637FDE2B130FBEAA930E3D271CFCFBA722ACCB4B62917 + 0F8789EF1D08932F1D0A532F1F71F03BFDCA5177FE2ADC330E60E0EFA48B47AC + 3E42E019EFDBB580B6C0DB6B80E0D3D607776F04BFB880C90B0093120E7AD6C7 + A99CDCBE3A175310B8763E26A1A0CF17F0973A80AFA0AE5CC0805E9CBE387FA9 + A31D204CDD9C048D81C40B3833F8F66587530E7E01525FE0F205485D4D1038C2 + 29C1AF39C024EB07C822EC3302000BFECA0DBCE04BA0975DEF2BEBFB4D3A3B38 + 01B0A80F1DBAB91BD304D0A3ACF35DFD7C61CE5CFD360A60974B3018D82708EC + A0F4A71BA1F12FB7C29A9F2304C6F90B905E69CF6DB5E557FDC859C09FBAFB97 + 14ADF9F1E380F378F58FF17C62A02D4EDD153C1F2BE34C76288CDB17D8CC6326 + 87B2C3E75F38BF1658CC35AE3B24F690D63F7CE2F2F382ED7A1B7EF9C29DBF40 + 559CB6AB7FBA1D56015A7FFEC2FE065CB1385F23F0251CB24216F37A90528703 + 98F37F1BFFFE95835FC1DF55E9AC64C0AFA0B08781B6BAF596027F01BB2BBEC3 + 69FD59588FBBF7477BEE3FD873B5E78054471F9CC3EB79AE26FEBE957F06DCDB + FFF1DBF7EDF9D8DFF5730AD1FC23B095767BBE3C0FFBBBA2E317886B7FA73D0F + 60EE7277F9E216FED2FAD49CC0E40578C9D357F5AA13FC8DA19FA303983CE7F0 + 2EFBD6DE2B9612BA99F74BF19EA1EE87F8BE11F875C04BC8737BBF7B0A7CB5F7 + 3B3723D0C71DBAF61901ACE27C77D7AF20707AFFC710E8D1110F701588559BEA + 099DBED0AEE19FA3749385DEFBF3EDF3C3CD16D4CFFB82BE760D1BE7219DEDB3 + CA35FC71484D387781B7805C20F14CDCBB376C3C303795672600ECFD68B37A9D + 014C5ED017D7EF4C6E32314DFF9090CFF6BD62DF0972F99222C1DF691F44B7AF + 60AFC0AF20F0CC8F6BF9283980DF099392E317E08B00C0488E5FC1DF2997E997 + DA2F9E0C932E9CB4EF5DBBE6D5B39E0284A5299790F505125B3AFD92B59F3F11 + A6BC67632E589FF3C71DEE8E05F69A26BECB8D3EC7ED37E248187732B97E01C0 + 49E34E1EB0DF9FFD0E76279CB2DF9504817100E3E645134F1399E2A8A7A38FC6 + 30CEE3ED3A02C0C38FEC0D23F87DC4C17B7077A1C2D54B99737D0FECF6337D39 + BB973C2E60397B4987BC15CFFA55B867FA51060023DCC09CF39B435F5240B0D2 + BE5B6D4E4108E71D9B1DFA46F01BCFEE1DB433BA7A71FB027681BA0AF5DC77B3 + 69E3C6D07FF3E69837F5DA60FD36E20CDE14066CB36B6DDB1AFA50BF3E868006 + 02F75C13C33E3BFC5DB3DED56B55544FC23E2F5B655A9DB4CA5DC16FE0025E1E + C3384B805FCEFC452F13C9C552C16097E539EAE3D585CCD938F283BA15369F5B + 1E3C228C3B7EED1AF36CBCCD1D11F9573C4FC8679B2BDA3C93B9A6DF74C83C93 + 79A7CD473BDA7C547353815F1CBF405FE0AEA41B1C057FF3B2DA81C00E7F991F + A3C9F58E5FDD5829E9664BC16020F0733877998F037E299B70FDBE3469767875 + F662BB6E431D007E662447B9D81C7FD4240FFB8C9E05025B59B0B7CDE0D1C58D + A3A8EDF0715109FA161038AD2F9020AF006F2EC1DF42D97A07F8FB0C6B17DCC1 + A9EF5356EF0018F86B6BABA7FBD85AAB6FD3D0CF39F45559CE5F457ACAD76E65 + B1DE93B4FEF3751FEDAC0529A7BCD692486B50D69D44A6D20DCA88F3808BF5A6 + E581BF4FB2964D6BDD087F391E09E01BE16EAE2A002C45E76FBEBE6E2A770023 + CB7BCA1AFEC51AD8D5FA5F6B7FEA7443B8F605106DEC2F00735B3FD3353CF05C + DC8F605F427B190500668F8236F61F68E73CDFD2BE87C64A451B12F84DAA83C1 + 1565EDBD38F005F05A9B802F69BD9A3A80EFB44794B76B5C799F8954FB5079B9 + CEC050DAE72AF6BBB27DB1BC4F2EF5F37126EDBB95F7E3306BE4CADB72312EDF + FB53BE25B57ABCDEF92BB389F6218BFD485402BA55D29E66398FEE662F1495FB + 95F3756A69AFF65EF4A83D067BC6EC2927D5ED25E7F50FD6EF47E7FBD6CD2B82 + DE087BDBB8EAEBEE466DECF17F3FE97934A79C4154E9D78C91EEB5FF1FA14A00 + 7C170EE0FF0180EFA0FC5ABA76AEBC3EEF5F095EFF49D404F436A74AE89BAB99 + 17E57FC85EE7047AD17D49595DED4369AAF84017A2FD9F1802DFE98B505F9677 + A3BB19E37DFCF5A01C7FC0CA00566AFA23D4B2CA63F2727EDD96948FBF93FC8E + 2CC63DDA21DC9FA9F8D1FE0D13044D3A2A271E15EDCA6BF273B7FDCB932D9F70 + 65F0B73639CB2775D431817BB64EB5F6EABBFFA4E62690925FC3EAF2C9AA4F68 + ED310479254D74CBF575E313D42D4B13EEF2245BE52AE513F99665FDDBD864BF + 6D7D18E8FB9E7AA9165628815D2D38047C49CB8B115709FEE600385FFCE400B8 + BC3092EA164E09FE0A006BF12600FC683AF357E318D3E1F58161DCFCD561DCC2 + 35A19D2D5659C869AC1678523CDB37C25C5F04A6FAB8608C0B442D0E7DF16869 + 0E7F057EA90326D7447B4F07C082C00E8411B0D716C528BF4B5A0B664428AD27 + 4D5A50B3E8D662BC586497942FD0B56047B4697CD5E25F1B004A11FDD840C8DD + BF4875350D094FF719641A1C1E7FA36F78E4F59EE14FBD07847603D8B488E702 + E30A060EB71F30CA01B0DCC08483AE415F36462210AE858106FC928F6A3F1847 + F098F04C017AD98089E1D55004C1511E966D30A0B87900AC3AC06F5B1BD38E73 + BC2C4F9B3B8213F8CD01704DD4E11C48E571D3C20B26D21727CC081D27CF0E9D + A7CE095DA7CF0D5D67CC0B5DD8389B4E3ACFEB3BA60D2F36B43AE28848AE88FA + 90D084C49BED1B6CAFCC59E40E0B36DD08C5C7F9BF9CCFF6DA82A5E1F585CB1C + 02CB01ECE7B7599E738029BFBC8090CFD47136B03D87B9387F699BE70018F7AF + C2412B2FBDBE78B10360D7D2A50E7D11AE601CC31102AF28EABA2F5B5A805D81 + DE088463C867857FA61C4344AFF03ADA24C2444B7E26F0E6C63078EBA6301077 + 8D95FB6E581BFA6D5AE7101818ECE7010389710527083C78570C0B3D64B740B0 + A5BB3687A17BB786E16F6D0F23F7ED0C2370043B108E72086CCA4342E7505829 + A077C4C19D9E027F7102D7CA6F39CC8D4EE05D9E07F08EB0F28883005F20F1AE + 30FC805D6FFFF630FA30401887AF5D27B97D63786940F09B61720AF9ECB0F7E4 + DB6192E539139810D082C093CF1CB4BA7826302E5F00309A7026BA80DD010C20 + B636B97D094B8D3B9910D50061EAE837D552AEC378AE85D317F08B1358ED80E1 + 39EFBF132130E7FC5E3B663A11667E80EBEC449844A868AB9F72D9AE7909107C + C29DBE0AFB4C99B32AFD9C4AC03230F803CEFB3DE3E0973380750EF08C0F68B3 + C702A27CC479C0962600ACF0CF02C1B3BCFE9C03E0864F2E84060FDF7A3ECCFE + E4BC87709D4BE8E7CF2E39D05D70EB8AE739BF93738111A19837FEF54B87968C + 236CF4A2CFAFC510D10986C9014C4A39BA1A3F72B885E472F490B39F5D8CF0EA + 737BCC2FAF04B97F01BF82C20BADDECF05FEDA64E902424DDB7501612E1EF7EB + 0FC2D21FAF87A57FFE342CFAE6232F732EAB8413B2D0F711FC0AE49107FE2240 + 1F22F434619A01B88496F690D3A615DFE3EC8D301610BCF667CEFFE57C6140EF + 8DB0E6C7086971FAC63E88B38A19F3A903DF423FDC08ABD14F00E6186A383A73 + 81CFC064CE12BEE5AE5F9E0B4078AD69FD4F9F59FD1761030017300A2805C8E2 + 70FE01907AD35EC31A041704C6610B0C47D4ADFFDB170E8157D9F5047C71FA72 + 9DBC8C1C049B08FD8C00BFCBBFBD1E567D6FCFCD1ED35DBFDFDD70E88B0B78D3 + 5FBE7210BCFCEB4FBC0ED730E3716673A6B2003FF955F61ABBD3D61ED79DC6F6 + B7E0D4066A03B423D4A61EC87DCBF2310516E7A19C73C80BFC4572FA22F58D37 + 08D8F3B03C7D058509BFBCE41B6E16C0357EC3E1EFA2AF3EF63ACA72030BF0F2 + DE06C22E06C67E61EF41C2A5F33EB4F7DF227B4F0A00E37CE7FD0EF8E55AA480 + 59A4CF04C05567F0CA351CEBB96902880C10E6868AE800E67D4F5E9FB5D937ED + B36C9FA505DC8C61D705D836D8F570F092027305810B47AFC981AFC981B19567 + 7CF25E98C177421AA3BE0E819300BFC05EC0AF52402DC036BA81A9C7CD8BC337 + 07B9A7DDCD4B3DED319C336DF4613C0018507CD6DDBE7E4DEBE3E56BC0DD087C + 1DF466A9E771F05E3A15265F3869DFAD9CEB8B22E89D7481F0CE27C2C4F74E84 + F1E78EBAE84FDDD8778F8609A6A9EFD9F7EC457B7ED607F0EBE19FAD1FEE5F87 + C0EEFEE546A16361FCE9C30E80C773BEAFD54FB07634EED4C1309AD0CDB87A05 + 824FC670CF7EE6EF517EF7B851E96D4F471DE186A637BD3CCADA70F60AF872CE + EF08CEFB3D14C33DBBE3D7EA86EDE73772B73B7D1D02BF0DD4B5DF4A87C1A484 + 75DE11FAEDDC1206EC26A2078EDFBDD6B6C7EB07EDDDE17078803B7C01C0847D + B63A13619D71F50EDC4588675CBF9BC2D03D3B6D9EB0DD050CC6ED0BE41DBE77 + 5718B2739B3B7B2973D6EF403F0378B3CD37363B00269C73CFF5EBFC4C5F04EC + D539BF7D70FF5A1BE0B7FBEAD5E18D14AE99B4E7CAD50E7CA3D6795A388001BF + 49AF2F8B8E5EA0AFE0AE9CBF0E7C4D6508FCBAF5797D7102C0B8806DCE8738F3 + F7D545CB6C3E67FD175ADED2D7E6DB3570FE3A085E14BADA5C11676F17E69733 + 01BBCC35A3C357B0B78B47A601E2027DE9A710CFF58AF3589BDFA6396D0CF91C + 2130228FA8E7DCDF2EDC1069F361815EE44EDFACACA83B2F4CE266CB99563723 + 3C67F36E00B0B75BFF0E44E8193B3DBC326B71E8317FA5CFC19F19452868EB67 + F3F276405E6EF4B439BFDFC86973FC0E2327D6C23E27C84BDAA2033883BF3900 + 466500ECA29E766E784DF017017D7556B00360C161D6447D87D5C15F1CBFEEEE + 05F026F05BD693698D26E8AB345FD3498F6BBD97ADFFBC8DBEA9DED78B5959EB + 4B52C0AF8E2722EF4A20D86565AD69EBDDBF55F017872F603801DFAEB6A6EE62 + 6B6D1379CEFFD59ABB4A6A2315C49572002C69EDEFED364EEB7BD5B1B7409E94 + 7D838732D0AB3D06CA05FCCDF62DCA521BFDBCEFB356A62DEDA14865D05B96F6 + 59B42FC3DE4D74F9D683DF07AC3ED76F01C02A97F79C28E77B512AE70EE0BA3D + AEB4E795EF8195F7C1A4721FEDC969FF2DDF97AB02C0E5FEA8BCF75795CF15F7 + 0C4BA19E930B38DF8B742580CB7EA580EE9DA4FDCD7219E5FBA2BF5A32E3DCEB + 1E6F3E2E17FBC577A17CFFB96AFFBAACD8B7FA484614F7E56BD76C5E6DECF17F + 3F89073427B184E6F46BC648F7DAFF8F5053F88BB23CCC092E5502B994CB7585 + 9A039FCDD5FDD1AA7AECDFFBF9E4D7D2B573E5F579FF2AF0FACF223FA45810B7 + 0AFC4A75B0B74ACDBC28FF43F6FA49FA30A60FA194D7F3A1E5435CF5C5F24F0C + 80EFE68B507DEE467733A6D6871FA0087F1F78A47D9D13B7E98FCFEFABFA1FC7 + 5F2FFF51E67967F0B7B5FD2D006087C05513803B289FA8B4A43B4D76EEE67ADE + 3F4DB8F2895704BC39FC2D3B7E230096EE060257D595279148FD3439CD27AD39 + E8CD27B22D01602F93A649732E26D5523EC926AF493DD2A49CC97A79B2DFBC18 + 431AEF2425C4902F2C12F865A1515E8C14A0D744395FB0B46E6775A5854F1902 + 4B1AA78551BE382A164956475FCF3F570BABC4824C0B355F9C59DD23D9E24CE9 + 139D7A045CC04FBFDACF1777E5F112D779F085D7C3FDCF93C6459F443BF0373F + 3398343A80AB2170AE7866B02D6E13F82D007002C25A14D741DF52FE89976DF1 + 6C8B6816D4F9E29B30D102BE4800B8587CA7FE1AA385BC2FE649B38D006D0E48 + AA67F32007BD3904AE53CFC1E1E93E03439BDE0343DBBE8343FB8143C3D3FD87 + 7808680070711EB02986861E11DAF58D21CDDA5ACA8647017D078C09ED06C6CD + 1004F0F5CD1193C245B361C25DF3DA34416CB2D047F0F719A02F1A3A3E3C376C + 427816A83B6252783601DF5C02C1C05F5CC0E481BFCF73267082C01E223AA5CA + 037E1D0C9B5E18373DBC30767A01873983ACF3E486D06DDABCD075EADCD079CA + 9CD0C9CAA494512740EF94E866C021CC869820B036CCB481C6D9697259B8EBA2 + 0480D9CC7B39B97D3B5B5DC706CEFC25C4731616DA5DBF0B5DC0DF98528EE19F + DDFDBB30BA7E05825F31BD6679E06F2F423DAF58117A5A4A28689D194C1E017F + 81C03DAD4FEFD5AB3D2C34F0B7FBF2E80AEEBE7CB19597159017E89B4BF500E1 + BEEB56BB0483076D6A0C83B76C08FD37AE0D7DD65B5BE31A0F070D008E1038A6 + D1158C1B581078733CFF97FC8E4D2ECAC3F66E0B230819F9E676CB6F0FC3C9BF + BDDDCF052625F4330058CE60B9833D4D0078F8011B7360BBBB80D1D0FD76CD83 + 387CDF740912C7F25B61249BDF3606C08BC80380BD9F296E96731EE26ECF8F4E + 670CBBC31787D6A90361FC49F2071C080B0A5337C1DA28732630D01617F0B8D3 + 00DC7D81337F01BA00604243E31406F8229D4F3C9EB0D05606F60A164F38F3B6 + 435F423F932AB4F4E473073D14F4CCAB27C3B42BD676F57820C433AEDED900D9 + 0F4E079CBEB10E9061FDAE9DB6FC5977FA4EB332E19E2577FE5A3D0EDE99E90C + E0C9578E39009EFEE1A900FC653C2E3807BDD627867C06FCA204873FC4FD1BC1 + EFDC4F2F85B93780393124AB8766E50CCF1BEF15C077FE2D800F2160ADBFA568 + E1E757C32A60D9571FF898F93886814E56EFE1673FAFB97F9143B2AF74FE6F74 + 3652C6713893F0D30914CFFF220AD00B04260F10D679C08BBE895AF8B53DD657 + D72200FEF25A98678F31C7C6CFB5741140F7FB8F1DCA3978C625FC8D3D9EA5C0 + 3C813C415E409E42F85206EE910285977FFB715869D75AF1ED8761B95D03988B + 13D7216C82B1005DC237AF7727AFD525304C0A008EEEDE087F81C0AB7F041003 + 7F81C39F4628FCFDA761156580B14030D7B3C7023E03807101C7337F09F97CD3 + 81F03A7BDC35D67FDD4FB70BD76E3CEF15B87D3D703EADCEA805F64A0061EA81 + C19C83CCE32DB7E722D72FD0D561E78F76FD9FE52CBEE5294E60402E0098C705 + 0273D62F0058F0574EE0C69FBF2800F09AEFEDB12C8F1378A5BD6E84E326BCB6 + 427647F86BAFBD5D83C7E2B1F95BF49C04A5977E1BC319AB0F3712B87397F763 + 02BA3900C6FD0BEC95D45765F205FCB536CEE58D00383E0E79C02D8F431DCF81 + E729F72FEFB3A5F63E59FECD4761292EF42FDE0F4BEDFDBDCCEA97F0FE4B21A0 + 01B9EEC24DEF7DAEC9FBDF5DB6B7ED3D6D65FA28FCB2EA548F7B9EBE736F5F89 + EF7D7BDF0BFAA259F6999D79FD5C68B0CFE97C6EDAB06B3B044EB0165127F7AF + EA047D15F299FC34A02CAEDE4F08E17CB1902030299057E077965D87322077AA + 7DF7789D95A7DAF70EB017F7AE806E0CE7FC8ED545282C303C95F0CD963AF0E5 + 3AD64FE3667C78CE64FD92C3B72C3981A3DB17C87BDC457ECAA578C62F65A0EF + 84F3C7C2D8B3873D3FE31A8EE0D361CCD94361DC19CE6DE73BDCAE71EE5898F8 + 6E0CFBAC70D01E0AFADDA3D1F18B4E1DB2DF03CE018E2E6041E0B1F6BB33D27E + 9F1C029FD8EF9077DCF178EEAF43DE0472D168CBF3FB850B987AE4E19D01BF4A + 0F46F80B38A66E28375CBDBDD3E12F219F71FD0EDF8FCB77675D9867DCBD7D71 + EDEEE2CC5FC0308EE05D56BFDDA130E9C05DDBDCD13B701767FEEE0C83F7ECF0 + 320078C85E1BF3D61EAF1BBEC7AEBD735B18B463AB03DE3E36A7E8B7794318CA + B8048011E19F07007F3745F72FC09750CE3AD797FA3E5687DBB7E7DAB50E7E8B + F37FD770DEEFDAD07335219D573AE8EDEE6EDF5511FAE2F4B5B4E78A35A107A1 + 9F13087E7D89CDB36C3EF59ACDAB72F08B047C5FB6F9579D0B1865E097A33E98 + EBC51B0097799418D73C2B9BB869F0559B23328F1400266C73579B6B32DF44DD + 6CBED9CDE69BEEF4B5F9A7202E73D11CE6E62A036084DB5779A4BE00E0CE3617 + 7E89E8391308DD1C616F01819328E3F48DD1776CCE3D61860B676FBB319343DB + D13687B7B4A3CDAD5F9FB32C749BB130B41F3525B41D0EEC9D1E3AD83CFD2920 + 2E6B049BE3A3B636E76F07FC35017CA576005F1379C06F9D58772409F00AFE4A + 05F09500BF99DAD89A27BFF135AF7F3A4548CACFFC15042E5CBE02BE962FCAE4 + 4BEB3AA5F94DBE5E9F52AD0DF3345F4F6A8D592E23D69802BFE469635D9A8361 + 857BF60857BEDE25FA55BD22F4EDED8A30D8D6D35D01BFB646B7F4E12EB61EB7 + F5F5DD00606ECECED7F702BBB9F235BED6F9AA17F87DF0851869CCE1B1C9D314 + 025AFB0C287700FB5E45B6A721A9AD10F5157B2BF99E4ABECFA276A900C0562F + E88BCAE0572AEF09E5FB452A93AA1F65ED39E5FD55A77CAEF2DE15D21E57DD5E + 57DAFB525EE70197CF042EF6D12C2DEFC12101602987BF6A575E7B76552AF7A9 + 2FB367D874CF5175451B69BE677917CAF739354EF97CBFF58E02CEE6F92AE5FD + EF24F68C9B137BCAB92AEA7DDF39334FDD4971AFBA1A00D7F6E56B7BDACDAB8D + 3D97DF4FB5C7AE96384173FA3563A47BEDFF472807C035089C52F1A6FB7F0500 + BEDB7AEAFE68553DF6EFFD7CF26BE9DAB9F2FABC7F1578FD67511D004655F017 + E57D2AD5CC8BF21F56FCBDB9AAFA945582BD65005C82BF4DBE5CEEA78F49E53F + 504D9E4BA9BEB9F65CE5BEF7A2E67F7068ABC15F01E0FA1FADBB57712E6FFA61 + CCD5DC357FCDE394A51F69602FE017A97C5700B86202914F5A5A52D564A75C2E + F7AB549A70E513AF7C3286EE0480517310B825693288CA134A415D4D5AF3496D + 59CD01E0A23DBB6BB26E029D4DB035E9A69EBC26EF28EF53D45BFEC1A76D529F + F4002A2D045AB761324FB9B6B0D0A2235F88E48B10F529D7B70480CBD2B8BC6F + 7991942F9E7200FC600AE5ECF097FCF3715186F2337AB82621A9EF6341938DD5 + 024F8B3EF4A08DBB9F855D3300F8B14EB573830580B5E8CC17A54060C2483FC6 + 39C22C6A15F23941DF32087ED417BCBD8BF058287704E720F8319416E02CB2B5 + E0AE9216E42CE6B5A0CF17F67EC6300BFE6CA3A0D818487582BB40DF1CFCAA4D + F5455BCFFEE1D94123C2E0F94BC3B8D51BC22B13A685A7FB5A7DEF810E817100 + 93460DF7D0D10E8293E3D761EFA0B10E7FDB0E18139EEE1F3750DA0D1A6375DC + 413FDAD328C06F12FD2D45ED06E3004EE017E83B7C62D4B0983E479EF06F265C + BEE570CFCAEB2CE0E781BDD46762D3C843C859AA7EE8C5B1D342C7F133C24B13 + 663AFC45E4BB4C9E133A4D9AED6520312965E070E7C9B3C34B93A3FBB7331B61 + A67CC34C1018A99E3C9B726CC6759A3937746D58E01B792FCF5964E579A173C3 + C2D069F682F0E22C423DE3EE6503D0DA1AE65BD9C6CE25DC3370973380090D9D + 406F0E7D531E017F5FB5BAEE8B173B00EEB372A5A7DD13F4CD159DC0F13CE03E + AB5687DED6B7C78AE5E18DE556EF29A1A1710747080CE005EE0AF4224030655C + BFE4058301C0033737863E8D82BFEB3C14F400AB2B94016077026F07F86E0E43 + 08FF9CE0AF6BE7E63074CF5687C0A49E7F73BB87871EB87B5318B66F7B187570 + 4F204434E706132E7AC47E4241EF0A23916F5EEF0E43ACDF90B7AC1DD7D2815D + 61A89587DA9851E96C60E0AF03DF232AEF72E04B9DDA86913F64D73C1CCFF81D + A750CF27DF0E632C3FCAC6E10CA67EFC09CE598C7D22F42514274EAC3886FC24 + C23C9FDEEF10582098D4DDBF49D44DB0BA29E77106E3F202EC1EB0F2D130E3D2 + 09AF9B78F6A0958F8419974F86E95637F11C100037F1E130F5BDA361DA85E361 + CA85A361E2C5A361F2A56361A2E501BDD3FD5CDF087D81BFB33F7ECFDDB8805A + CEB19CF101619BC99F2934ED5A841D31DCA98DBD76324C011ADB78C02ED097FA + 18E659E374AD771DFC22F272FE0280E75C27946B84BF733FB3F24DCEFC7C2FCC + A19C802F6D0060C16095F37A6FFBCCC69AE4E8059021DCBA38181B6E5E71097A + 79B859CE1976F72FC038BA80057EE77E7E312CB4FCE2AFAE85C55F5F0B4B7106 + 7FFDBE3B2C17DB3551BC7E84C0849A5EFCD58761D9379F58FF8FC222CBE3E874 + F0675AF4CDC7D69750D111F42A9CAF0030A27EA5C9413065CE1BFEE60387C000 + 5F0030AE5C00EF6AEBB30608FB3370143048B860EBFFFDC7499C396C8FF5C38D + B0D2B41C800858C5A5FB63D48AEF81C0D7AD2F8F191DA640600F476D631D36DB + 633AF44D1078ADC35F9EC7A70E9157FFB9E69A05522EF926862C5E065CB5EB01 + 7A01BFA4CA4B7EF6F12F560F804E90B7761D9E7FBC36F5AB2D7507B08910D0B8 + 8E81C0847A76006C7F5B74FE7E5D3882F374E5B7F6DA7E175F0B39AE81CFA440 + 77FF1F7C17E13540DA6563790EC0580FDD6DCF4B7096D4C333DB6B578470E6DA + BCD6BCBE3F71567074FBF2BF4782C4A40BED3D2228ECEF0D7BBFE05207F6EA66 + 01C15A449EBA25DFC61B0C16D9FB6FC9D7BCDF12FCFDEA830880AD9EF7CCB26F + A8B3713656105821D0F332121C1690251504E6A6093E3311D2C6F0CF7C5E047E + 1D02935E3FEF9FCF79D607500BF005F6360070ED733EFFB3AB61913DE6021ED3 + 3EFBD43B54B6328F499EC705FEE20206ECD247B0D881712A03795D1F9D0BB3ED + BB04F1DD32CDBE7366D9F719DF693876275D3EE5DF5BF1DC5E6E5C396BB2EF26 + BEDF4C9CE54B3DD239BE333F3CE7B017A82BC89BC35F397E69E70C6009002C08 + 0CE805FA529E72E994E75537EEDD23DEA7E1A3F71C028F3D6BDFEF678F38009E + 6AC2E58BE39770CF93CEE1028E10983261A0C79CDC1F469BC69E3AE4E5D180DE + 1307C2F8D387ACED401871F42D3FC377ECA903AED1387F81BFC7F6150EE03144 + C020FC731D00E6C6A6B73CA54C08E81184754E1A86DBD77E6709E34C88E778E6 + 2FCEDE18DA19018007F11B4D78E7047A71FCE6EAEFCEDF2D85DB3742E06D5EE6 + 5C5F347CDF9E30F6F07EFBCDDFE5E017B88BCB57693FE6155B6CFEE08E5FCEFC + C5D9BBCE21AF877D4E00B8378EDFE4FCCD05FC7DDDE640387F7BAD5B17FAAE6F + 7411FAB9A7CD8B0480399797D0CCA4947B26082C10FCC6D2E581F0CDAF2C5AEA + 7AD543392FF79472B7058B5D2F2F5C12FB580AE845F1C80F22C1C41BFE70FEBE + C219BF3E57B47A9B3722C06F0CFB9CEAE62CF0B37D5F26EF4EE078D3A1A0AF8E + 32913AA6B9A94459F5B998BB02809F9F34C39DBA88BC4240037F5F9AC89C39C2 + 5E8E5311F4D579BE2FA4337D3DE433AE5FABA7DC21C1DFE76C3C21ABBB2FB4D7 + B561A98D9915DA8E60BE3ED9FACE7417F0D38474667D607379E6F1E473F08BE3 + F759135183DA0DB1F588DCBE441A72C54843A8ECFC2D5402BAA85C463AEB5720 + D85353DB7E238B4849F999BF4D5CC028ADCB94670D57560E7F8BF51D2975998A + 1B842D2FC85BAC196D3D2951CEDB54D6DAB2068063A8E70878E5F225AD0900EC + A19D6DDDEC7DBBDA1ABA9B00B0ADABBBD8FA1A758E6B6C899BABB5FECE555EDB + E70058EB7AF6007295EB1E26255A58DA5390B4B790C3DF727D791F43CA817055 + D867EDA79401704DF489FD248FDE56A17A20DCD40C20889B8B7AED0DE5FB4952 + 559DEAD993AADAC3CAF7C6AAC4DE5719FEAA5ED23E9C406FBE0F97EB8127625A + DEBB5339AFAB6A2BF78965F60BE39E6373F2A3E94C557B97F93E6795F27DD57C + 7FB5AAFE0F13FBC2AE04777355EC2D97EB25ED6BB7A45AFF3B03E0FAFE556A63 + CFE5F753EDB1AB55C5227E2FFD118F71AF6A110027017A9173A9042B9BAB8BEC + AA42F4919AABFFA3D4D273CAEB7E8BF26BE9DAB9F2FABC7F1578FD675113008C + FE0700BE83F85BEFFEEF6F656F8242AD62D8E742F7D90792F38033F85BF7817E + 30CBFF3700C0FA826BEE8BEE4EEDB9CA7DEF46CDFFD0E4ED4DCFE0CD7F70EEFC + 83545315FC65FCAF557E9D5CE5C7F01F69C9C649BF0A00A7B43C896949E58950 + D5C4487555F23E9A64590AD8CD275FAACB01709CBC95434247007CDF638C7FB6 + 4E793B69FD04F0B962C2599E78D23F87BAE5C96B0E7DEF0480EFD501ACB2A4FE + E41F64229F81DF5CF92200C9010C04463A0F385F84E84E532D527CE1512ABB32 + 00EC9037A555D2589D199C03602D92A823F5BA0C00231669C05CF22CC0B418E3 + 2E5DE519EF795BDC95E1AF52C4C210F0FB0075A50523E19FA3628828A9ECF82D + 169FA92D02E0087E0581E5FA55D9C130EAC25DD00A8D1561B0007001815FB1C5 + 767201A3874B775DE7A2EE714BB540CF17E9451FDA59F4A70D822A09F4E6692E + EAE40CF63BCF7BF40F1D478C0BB3B6EE0E2B0E1E0FFD662F084FF71D141EEFD1 + 37B4EB3F343C33685468D367884B00D8EF6AEF3F22DE2D8F83D7E1EF6887BF4F + F5E54EF851455D596A6B07341ECCF959631D207718323E74183A2181E0890E82 + C93F6B29792F0F27AC1B1B3E96CF602EB017E8FB9CA50514CE84F3571B471A23 + 973069C7093343A789B33C05F6E20416107687B000B0F5412F4DB43636BD26DB + B8A9D51098CD3436E1D844D3069B87E09B3937749C31A77001B379D7D1EA5FA2 + AFBDF6720023E06FD49CE2FCDF97D914344527707400037CC92387BF387C1300 + 960BB8279077E95207C21EEED9A4D0CF85FB77D52A3F1F983C4098B4EF9A3556 + BF22F45C697D4C805D87BEC9F9AB32B077C086752ECA3D572D7710CC59C0FD36 + AE73F76FBF0D6B1D06BB36AD0F034C03371302BA31F4C7FDBB6D631894E06FD4 + A630D4D2A1B880751E70928788DEB3250C320DDCBD390CD8B5C9A12FB077E85B + 110A0F7F7B57187960779D0849398476EB4B5A0B5549484ACE2C2484E5CEC0F9 + C2C3F773D66F7403038087BEBDCD4519F88B461DE10CE037FD8CDFF1405DD398 + 93FBC2E8136F8651C9194CF867C0AF80AF9CC0805FB987058523048EB0D781EF + 99FD0E7581BB805CDA280374279F3F1AA65F3C11665EE68CDFD3D6E788F5B136 + 4BA75C381E26BF77344C3A7FC435EDE24987C2532FA6FA0BC7C2E44BC71D00CB + D13BC9CA53AE003800B8E702F064EAD51AB49DFE7E74BC4DBB76B60E7ED067EA + 9577C234CEB4BCF64E98E1AE5E1BE3903802E0E804E6ACDFF3A121C1181E63E6 + 07E70AC80260D6E3120E9AB37F39DF17CDB1FC2C42429B047A057B17E040BC75 + A500C4B881014EB42120320078F66756F6D0B4174D97C35C07BE57C2CCEB171D + 3001B3804D0D9C33EC616C710DDB63DFB6FEA6F99F5F0EF36CEC82DB97C2A22F + AE84C55F5E0D4BBFBAE65AF26504C084DCF5F35701C238321DFA7E14967C0DB8 + E39C555C9BD1D9E990EFDB4FC23C1B0BEC03360A3AE2E6140876E72F60F5679C + B2D73DDC33101827300E5F5CB9E49701856DBC3B687F211431C092C7B136BFEE + C7358728D0D101660497004D9E97834C13501845E8793DACC20D6BD75B66CF85 + C77647B13D36201871BEF03AEBB3F6C79BD1518CDB3539622330FDCC1F333F03 + 99E744D86780F01A7BCE6BFF4A78E45B21869BE62CE02F0AA72D92BB96E7ABF3 + 7F0582D7DA63ADB3FC5ACB3B00FE73EDFCDFAD7FFF2EECFC4F3F868DBF7CE5A1 + A181BF0062DCC00A11BDD2AE195F7FFBBB12002E5E7BAB5B0A74B7E71BC17004 + D10828CBF3A9C15F4231DB6B657FBFFEC78060815E4030E5C2DD6BFF7FC161EA + 73F8CB7B673E6E5D6E10488FC5DF1F154130E7F02EF81C106CE23D67FF5FBF11 + 81B0CF960281A3EC7FFEB5D5D19E1CBFBC1701BEBAF101E81AAF5983C09240AC + FA71F38400F02C6E98488E79DD6CE10E7AC4E7EFE685E4DCBF5A688E7DC61A6C + ECBC1B97C382CFAE8685B7EC7373D3EAB901C4AECB73920B9814F8EBE7025B79 + 0E8F6965415FF2A433C893A61B4AE20D2C7C6F9DF5EF1B04A49D68DF997C6F51 + AF947E88EF353FAFD7BE53E93B3D7DD7A9AFBB7A057733184C5FEAB8F6F8F78E + 798A80BA390096E357F0571A7B9633D98FD71CC0F6FB30DEEADCE96B63E4E605 + F84E387B348C23CCB3BB7B0FFA79BFB885C700764FEE0FA34EE2F87DDB35FA84 + D51DDB1F461EDD17461EE38C797E5B22300608A3D1C9113C8E1B95ECF7C88F31 + 38B2378C02E626F83BFA28A1A2ED1A5637FCE09E087EF93D3DC00D5731CC3300 + 18017D71FCEA6C5F52E0EFC03DDBEC7737867A0602E304160C26DF0F77F00ECE + F3DDE41018F81B81F0A6D067EB4677FE8E3AF0963B833DB433E7006FDD14FA6F + DBEC4018C72FD097FA41265CC1BD1B995F6C7179E8679CBE49B87C5FE366B7B5 + 6B1D08AB0C00EE6D7539FCED412868E06E82C00E80130CC6F5DB6339F0574EE0 + 58FFEAE215AE088B19B3D2CFF3F5633E4C9CEBFBCA42CEF55DEC915E34EF435D + B8011028ECCEDF0880992F728488D4CDFAE9EC5F57433CFB17D72F73CF4ED3E3 + 5124520C015D3BC7B71CB586396BD1AE3ED62EF8FB2C0E5FD294E7EC5EC06FC7 + 49335D00615280EFB39CF1CB1C3B81DF6712F815FC7D8679BBD571644AF785CB + C38055F6BF5BBA2E749A3C2FB41BC10D9BD63E7A5A686FFD7003B71F35C94340 + 338767FE4F0AF475F86B6B023F2E0611452845192AD6279617FCCD01701BE06E + 09000BE80AEEA2BC5E7512E7FEB6E967EA332CB4E93D2CAE8F4C05ECB5D4DB12 + 1076E72FEBB03732086C62ED26E05BB5C6D3DA2D3FDB57C05710B85CD6BA9235 + 27525BDDDA32B56B6D1AF3B6FEF550CF84748E219F8B10CF9980BF0E813DDC73 + 4D05FC4DD29A9BB5763CB6A97BE9C8A5DA9A3E97D6FAB9B48750AE63EF40AA82 + BE12FB12EAA37EDAC3C85540DF243FFEAA047F25EDBBE475DA5F69F534477465 + 65D2B46F23E50058F91C006BBFA8BCAF8468CF017079FF49527F893AFA9601B0 + 97B3FDAEE694EF9B295FB7AF66F97CDF2D97F6E85AB377F758D33DBD5CCD8E6D + 417908E8E6C4DEE47DEC63125DB16AFF32497BAAE572592DB6DF8519276FFFCD + 7A3829DB3BD6FEB3EF69A7BDE57CCF592AEF594B557DCBFBEF4D5535A62C7B3E + 1520F7D7AAE973A857158BF8BDF4473CC6BDEA4E00D8EB057B0194084E9503E0 + BB91C6A6F14DEACB7DFE912A3F767375BF45F9B574ED5C797DDEBF0ABCFE516A + 5D512ED7DD1DE0BD939A7951FED5A47F70FE0FAC93FDADCDBE5EA56BA5F1AD6C + 0CF0F7BE56C83E608522FCE5C3D9CAC163F64116EC7595EAF37E7F805AFA32CD + DB8AF6EC39E663CA7DEF5602BFCD8160CA39FC6DFEC7AB5E77DB0FE5D755BE4A + F7FA1CA426F03795EF1A00B7A096261D795B59E5C9513E61AA924FB0928A0957 + 26EED27BC0269B9AD4C5095E792217E16F0E80559F4F18F3BC547FDD9AE85B86 + BA2DA92500ECF96CF22C9527DF5279122E69A2EF6EDF36562EA9BE7F84BFB9B8 + A3540B91FBDB754B8B8E1AF0D582447DCA0B16C1DC16E16FEA0F3006000B0217 + B0D7525D03795D068059B44994FD6EDDB4286311C6781664C0E018BE292EECB4 + 18242FB1E8F33AFAD06E8B4B1CBE82C08F75A2DCF47C20CA8FD9A2F5D16C412A + 182C082C97EFC39D7ADAE216B76F2C037E1FEE680B5E13E5FC2CE0DC05ACB2E7 + ADCFA3B4A585F843DD7ABB386FE9315B84E70B7187C256F63BB12D4FD9A1B0D5 + F9E23E6D0290963709CA65FAE1F075C8DB23BA7E49A92F3612BCDED2E4001EB2 + 6059188F0378FCD4F0542FBB56CF7EE1E97E8303CEDFA77A0F4E0098BBD95358 + 335CC0034686B6FD93068C0E6DFA8D2C01E03111F666A21FC0B7FDE071D139EC + 0E60C23F8F0FED87589D29CF130A5A00B83DF5887C82C0C05E0F0B97E06E1325 + 00EC60D8CA2F8C9E1A5EE4ECDF31B5F0D02F8D9FE1F0F7C5047F1D005B9DCBEA + 3B027E71FF9A3A4E98E5E704BF302186B92B4241B389C6669A36D4D2669A1C17 + AE690D5EEF6DB880B390D02FD297B3DBE6C5CDC09766CD0D2FCEB47EBE194868 + E879A1D31CEB3FDFFA9874167037CBBFBC60BE95ED7AE94C604242037E73084C + FABAD547008CAB3742DE1C020BFEE68EE07E6BD7863E80DE55B88197BA048371 + 04037AE5F605F6E200A6ACBA5C00601CC05102C08D6110F077CB064F876CDB14 + 86ECD8EC2078D0F604843D0C3479DCC11BC360137D00C1387F01C002BA43F7ED + 70F88BE402961C0CA77E51D6FEF6CE30EAD09BC9B9C406F66E770BE32266CC70 + 425B72AE70068139239850D0B87F01C084CCF4F3134FED0FA34F007FF78551C7 + DF0A238F7096E2BE30E1640CF70CE805F80282279D3EE020789CD5B1E93EEEB8 + F5B3F1E34FE3FE5528E8E4023E73204C3DC7F9BE387C39E3F14098F4CE4177F3 + CEBEFA4E9879F57498066CC0150C004ED017017BA75C3816A65E3E11665C3919 + A65D3AE110788A9527E11A4E206392952752E6CCCA2B35A82110423982102089 + 00F019EF1B810910F994BB7FA711FA9910D2405F423E7F78CE9D75844F25254C + 6BAC8BEEBC69D7DEF57C0CAB6AF538833F3EE74097B37C11B077D627E75CE4FD + 7CE0E42E04F812EE99B0D100DFC55F5EB13117428395E759FB9C1B17C36CC23A + 5B1E473021691B3C7FC5D5F0E9250F2DEB678E029F805438806F11F6D674CB1E + E7B3F3619E8D9D9F00F042D3E2CF2F87255F5C0D4BBEBC1A16DFE6BCE1F7FD79 + CCBF7DC59D98384709C5BBF82B42F37E14167DF1A13DB78FC27220E1B7D6F60D + A0373A41231CC4651A01B0E023500FA02C88EB6E5E6024EEDF1F3EF1B0CFAB70 + F65A790930FA9B0FC2EA9F6F84953FDF0C4BADBF43DB1F6368E5157FBE69CF83 + F0D31F396C04C82EFA1AE7288F955CAB404CD332CB033A1D1C9BA25B1760FC61 + 019F1D02E33CB6C78821A809057DCBD3D57FB6FE19FCC5A9BBF4FBEB1E229BBF + 05F04B086C20B9C3ED5F6C5C02D78461E6FCDD35BFDC2E5CC4AB7F8A2E604028 + A015F88B1C321302F97BC260DFF4D0CFABAC2CF8BBD6B4F92F5F871DFFFE43D8 + 0C00FEE6D3B0E2AB18FED95DC2DFDB73FCFA13FF9FE07846ABFCB9DFF4D71FF9 + 6BE870580098FF11E03782E0188AF94682E8BCA61FDA6B165DBFFC5F114E5EFE + CFBCC6F17F1D41B080AFBFF626EA15FE99366FE77D627267B1492E631E1727EE + C22F092D7ECDB5C8DE2B8B3FB7BCBD0781C0388157D87B702937257CCE8D0B38 + 6EA38B97F72400590058EE5EA43A015F447EE6A7175DEE08B6CF0AE96CFB7CC9 + 613F971B27FCE6091B6B7538F7F9CCF2195C6CD75DF4853D57FB9CCCB7CF09A1 + DE71FB0381D17C0FFD7E21CCB4BE4060857CC6318C7B18F72FED00DF1C0023FF + CE20FC33DF2DF65DC28D24405BA5AAE3FB6C827D6FEAFBCC21AF7F07C51B5AF4 + 9D27984B5EE53CEF70D7527D37920AFA8E3F7FD405D895E3570078ECD9C345B8 + 67CA08F72FC077DCBB47E359C1E78F3BD01D7F8670CFB87CADCF6900F01107C0 + 9CF10BD81DE58077BFB7F919C0D49F3CE075B44591379DB0DF276B4340E23156 + 1E65BF3912207884FD96F1BB86FB770C8EE1C335A72F02060380C90381D148DA + F647E82BD83B682F5097DF59FBFDB43A347037619E0907BDDB7E9B778501BBB6 + 877E3BB686FEBBE299C0B87FFBDB6FBA5CBFFD775017D577FB96D073D386D087 + 70CEA92FC077B0423D6FDE50387E7BD9FC0307F050BB0EF57D1A5398E70D1B8A + 737DBBAF5DE300B80FCEDFB5EB42EF14129AF63756ADF690CFBDD6D83CC6D4C3 + CADD57007F57F979BF3AF3D79DBE0906A3379611FE39D6AB4F772BD3060C966B + F8D5C5CB0367F9765BB0C4E66EF1988FCE36DFEB64F33C6EFEF31B00D39120E8 + 659B0F2AE4B31CBF85EBD7F48AD5BD3AD7AE637347015EE06FCD011CE7A094E5 + 0CEEA2B928F35493DFC0C89C947E5CC3D21727CFF4737CB9C99154E2AC5FF4C2 + C438077E71FC3487BE9D88888300C056F7DC58CEEFB579B7CDAF9983736E2F61 + 9F01C36D474D34D95C7CD2ACF0CA9C25A1C7A255A1E7E235E1E5998B43079B9F + 7BE8E77133C3B3636784A72DDF76C4E4F08CD5E3088ECE5F5B0FD8359FE10C60 + 0030C077083791D62B07C0B9A86B028059CBA414D0FB2720700E7AA9B3B50F79 + FA080A17ED005E6EACED4D48E721E1F1376C5D660200037E5D5A87A5F5572EAD + EDAAA4751BEBC05CC05CD68E65B0ABB2D695405D5F5F5A39AF47387E1FECDA2B + 3C94FA783FCB3F4E9867AB7F1CF72FC0B7F31B2E226591C6F5B2E56DCDFC4867 + CB037A931EEEFC5A4DB60EF73ED99A3B07C0BE8637698D9EAFD5F33AADFB25D5 + 176D09E60AEE2A2F69AFC1816E876EE1A1675E0E0F3F1B81B0D795A47D0C77FD + A2D29E49B92C09F4D6CA595DAE6600308AE57A005CDE43527D9ECFF79A50BE17 + 95F745D409FEDED7A6B677D51200AE3244E4FD5B672AEFBBD5E9F1E75CC05F07 + C0492DEDE1C9C84139DFFF535DBD688FA0B7CA7CA23D48CE006EDDCCFEA654DE + 1BCDF74B7335DB5682BF28BF5693EBE6E69DD45ED4AB4FA626E3733126ED21B7 + 22CF1E7292EF290B06270352BE772DE57BD848E3A31817F7F4F33D7814F7E5EF + 5EF759FFFB1E26B5EBE5A2AEAABEACD4AFFC3CCA127FF847E88F788C5FA3E600 + 7051FFC093A1F583946BAC8A3A54C7AF527D9EAFEB03D752BEAC829935D3276F + 2FF7ABAA2BAB6887BD91A672795C5E576EABEA5355AF7C5EAF7C2EF5CF95C3D6 + 3F5A77058051139879AFB23FB4EA05F95753D53FB04EFCADF7008033F1616B55 + 0E03CD87920F6CF94B0488AA767D90556FFAA3BE74789C1CB856B5E77DCAEDEA + A3F4D7886BB7349EF6FC07ABE98FD6EFABFC71CA3F96B9AAC6362701DF020097 + D5CC64E06EA40943D5A4216F2B2B9F20DDA92F13AB1CF656A9954D0099D4B57E + 229FC0D5F29AE495DDBF795B6D5CBDAA2685D4A15696F7C92A8F9D606E79629B + 43DE9600B08B729A3C4B9A74E7136F2F3F9D29B569427E3FAA80BF5500F87EE0 + 6EC59DA371A1D1C526F4B5B3669A5B98E47582B928CF232D82B803964513F9A2 + DE164A2CA074672D75795B0E801F78BEDE014CAA714A59A8017F49199F2FF4F2 + 05A01688F18E62ABB3943AF58DED3520ACFE40E2C781BC2C2EAD4E00382E4AC9 + 5B5F5BD43AF075081CC16F018533000CDC7D32B97D0580EBCF4C32510604A785 + B9EEB466914D1DD2A2DBEFBE2645D6570BF93FD942DFEFFA4EE09654C03787BE + 12109914F8FB74EF619EB2F1A0F18872DC8C18149EEAD5CFD43F741830D44130 + E7010385DBF4B1B6DE76FD1E032C1F4170DB7E84818E00B82DE22C60133018E8 + 2B004C1A1DBF11F84A0E7EE5F81D342E02E1C19CCD6575BE8913E5CEE02487C0 + C32678286887C0F4193A3E9E01C6E6CFF009EE0C68CBF80C022B543470386E16 + 4D0ACF8D9C1C3A8E99163A3AE88D1B534060C02FB0F705E0EF98A95E468486AE + 857D6E70101C81700C7BC746181B6408F89B4B20984D3836DED88493DB421B75 + D4797EF6FCE8006E5818BA24004C186804F46543B0CBDC45EE0AE9326F8195E7 + 39002614B49F17B710D708B01897F0DC08811744002CF81BC3412F0A6F2CC1FD + BB2CBCBE34BA8091CE0306FE0281CB00383A80EBE16FAF55CB638A56AF70B72F + F017D0AB90D04528684B073446F09B9F030C00468380BFA6C15B3786A10060A0 + 2F6E60C2417B4868F21BC280AD8D56B6BE2EABDBB1290CD869EDBB37174E5E00 + AFE0AF523982710BD32ED04BE8E761FB763AFC45BAC66836BBADDD01F29BDBBD + 3C8AB30E09154D98E9C37BAD3F0E29E0EF5B11FEB219EF1BEE71337D14CE2ADA + 8EEE0BE38FC770CF3A1F18682C67B00BF07B229E111C1DC011FC22423EFBF9BD + EF1E74772F221CE8A4B36CFA13D6F998877406F64E78F7509878CEEA2E1E0F93 + DE3BE27987C0B45F3CEA67F64E05D4E2FCBD782C02DFABA7C324AB9B689A84FB + 17489BDC6E000E09F011DDC07202D32702E049174FD958BBF6FBD6E783772CB5 + 6B5E3911A6589950CE736F5E71E79E9FA1891B9830AB96477EBEE607E7C2AC8F + 09D97A31CCC419ECE0E77C74FC7E1A1DBF2877FD2A4540E0B9372F85E9C09F8F + CE58DDF9D070FD5C721F5E2CFAEAFC60D2864F234CC24D88EB10C0D470C3DA01 + B984BEF57E172D0530737D42529FF3F262DCBFB72FD7F4F955876E8B6EBF1F16 + DEBA1216A4F387977EF5A187DF5DF285A55F02803FB0F4C3B01C88878BD3F2EE + E604060300BFB33CE703E3E424542F5037416052EA167E79D5612F1056619F71 + FE12867A39C01217B0D5036C01BFC04A002B0E53A0A2420C0B4E02259160A303 + 47002330F2472026B033B9627FC2E9FA7158F9433C8718E9CC61CE1226E51C61 + 1CC01E463A85495E6E8FB5F2CFB71C2AE38C16F84580605CCDCBFF0CF8A49FA5 + 56C671CBE3029111D7226CB5E02730183960FEEE86877AC6FDBBC6EA80C16508 + BCFEC7DB61EDF79F8555847C06027F7DDDC33FAFF826FE2F56DA7557593F9EAF + 83D614B21AF72FCF435A6E7F3B753CCF95F6772EFEDAFE5F5FE1A8E6B9DA6B6C + AF0967402F4AAF737C3DA3D317F1BA5216F4CDFF0788FF8BCE0956FBE26F3E09 + 3AA797BF1FF1BE892E5EDE17407C6E14B0F7A1BB7FDF77573C374644C72F3748 + 5CB2BFF35AD888CBDA9EFB027BBFE2F40520CBF10B7415EC05002B9FBB7F01BE + 38E61DC60A04DB678F90CF72FE3698FCFC5FFB1CF2199EF9F1BBFE59E3FC5F60 + 2EE7F59227FC330018CDB7EF88795CFBA3F3619A7DF671FC72EE2F8FCF99CE2B + EC6F6EB0C7C3E51B6F10E106901A0CE6A61277FD7E185DBF8A2420572FDF677C + AF016801C002B77CAF09049327557F81DE5CAAE31AA41A433DCE5F177017D76E + 09F24AF499C2B84BA7BC6DCCD9C3266B3B677D01BE26E0EF3807BEF63D6D1AFB + CE6177FDBA4E1F0A634F1C08E34E5A7AF2A06BC23B765D6BC3FDEBE19FAD0E01 + 81471CB3DFA5933144B43B84EDB766E4D137C388247EAF865B3AF4E0AE3094A8 + 16BAA1E9188EDF18E679A869F8C11A0C1E4E0A04B6FA61FB88AA111DBF3AEB17 + B04BD86745D718B4776718B06BA7FD5647F5DFB923F4DBB1CD21703F402F219D + 2D1DB8CBC6BBB65BDD761BB3C3EAB7879E9B37861E9B3684DE3647E8BD79433A + E37743E86BF9BE9B1A3DD433F017084C5DBFCD8DA18FCD3700BBC05E07BF263F + DFD7E634BD1A1B43BFF58D3667D9E06702037FDD09BC769DCD61D67B2AF8DB7D + 4584BABD56ADB339CFDA9A52C8670927B08030CADDC2405F14C341AF08AF2E5A + E62E60E676DCE4E7C0373983DDFDEB733D136D363F04FE028225B98163D867BB + 86CD233B4E8B2EDE1CF6FA8D879A679AA8EB461EE89B0410EE92D2AE292FD0CB + 5CD6F3136D4E9CE6BB1DA7CE0A9D38FB17083C7E9A9FFF9B8BBAE7710033C7E6 + E6CB31535238E819A1FDD829A1CDC809A1CD289BAB7383E5549EF7BCF09CCDAD + 3BD8FCBBC3681CC39C0F3C2B3C33667A683F72AACDE1B95973BA3B8299EB1305 + 28DEF01901705BC23D0F1CE570D741B0AD055019F8E62A83E032D8FD13501708 + 9C04FC7DB2CFD0A29F43DFD4E6EB1F5B0FE5619E25C0EF9F5072FEA2DAFAAB1E + 02E7EB38295FEBF97ACF74370058F902EA66F95CAC4773F7EFA3266E5E7EA25B + 6F87BF44C412C0AD93ADAF95F71BA83BD93A1C75B4B57AC757936C8D9FD6D8F9 + 3A5B62ED9EAFDB735157057DD557A9F614B49F80047C73F8AB947D05DFA368DF + 353C84ABF7D96AD72F7B18AED457C037177B28CA97F75672157B2F26EDD95401 + E07AB50C807309E896F79FCA7B52EAABFEC05B4160494057FB63B9EAF6BD32DD + F7E4B3A1D59F9E0D1838723DF0A7A82679C23E27089C2BEED5711DEDEDD5A071 + 19006B5F4F7D6A65DA5A7600FB1EE4A3F57B9ACAAB5C16F58C2903DA7C2FB4A8 + BBC3FE6C7EDDBA712DA8D5C3CDD437770D9E4379CF98547BCB09FA3A0436B147 + DDD2FE35CAF7A8A3B846B5EE1D00DBF3684902C12DAAFAB9E4CA59C4EFAD3FE2 + 317E8D047E95BAC9307124D703090097E06ECEABAAEAC94B5E07134B6D453957 + DE96ABDC9E8FC9EBABDAAA5406C0F9F8E6AED15C5B55BDCA799BF2B9F231520E + 5BFF68DD1500BEEB30CF2DC9FED0AA17E45F4DFAA755B515A21D955F83FA7EF9 + 87A5AC1CECA2D67C396765C1DF3A090AD35EF1A1FF47882FB7DF0A8025F5BD17 + B534AEBEADFEC705B5FC43F6FB48D7FEAD8F7347008CCA3FF855AA98ACA0E626 + 1D9A4CB4A4AA7165558D2BEB3E265B36518B13B69A62F816AB476922579BDCD5 + 2B9FF8D54F0263B9DCEEC099BAECEC9238818DD284369FEC96017059DE379B40 + 23AEAF335204715B7BBDB533C136A95C379907F66663FC1A69E29EAB751B26F0 + 2C0C381B38DE354ABE95D5A1D64FA3DA99BF08209C9F035CD4D942A3952D72E4 + EA255F2C5E6C01F4A02D8680BFDC2DFB28E7F63E638BAD6CE1A4C5157907C069 + E1E40B29E02F0B2B4F6D8CF57D10F8CB584B19AB6B687146590B370F0DCDC28D + C59D2DF4901681405C0F019DDA580CFA2230E5D54F7DE51046451BF9921EF685 + 6D0CF51CCF2BAA9511009890D04F74B34572E6F8CD2527B02FA449AB16E4AFC5 + 853A75F1DCA5E80CC6012C77B016FBDA08F07A4BF30D026D2AA07CC340A037DF + 60D0C643AC27B5FA5EFDC3D37DEC3A3D00CF3DC2136FF4B1F2E0D0BEFFB0D0B6 + AFF5E91D9DBF6DD9E848D03786344B0078C0A802FEFEC9EA1C00BB4378646867 + F56D4DB4038001BEEEFACD1CC12AB71FCC5DFBE33CAD03C083A30BD8CF01960B + 38816277028F98E81B420E80710624008C142A4E651CC0CF8F9A125E20345D72 + 05FB39C009F84A82C10E8193FB979430D0C8213061EF92E3C11D105366177200 + 3C79A6BB28B4F9C6E69C9F9B3675B66FCE51E721F3A637F8465E27AB2304344E + 100070B7B98B7C631008DCA561BE3B417081749B473D8EDF08805F5DC4F9718C + 996BF5B880E78757E6A3B9E1B585F3DDF50B047E7D11A1A001C011F6E2129614 + 363A0F03AD10D0D1158C531858BCC4EA97853EAB5785BE6B01BD11F00A00037F + 3D5DBF3AF4DF4008C5D5A1D79A1596AEF290CF02C00A074D1907F0E02D1B1DFE + 228160EA7004FBB9C0293CF440F280605763E86F1AB07D4318B47B8B035DCEF3 + 05F222A02F00572E6052AFB7BC802F9BD583DF64937AA7C35F36AE71060B10B3 + 810D301EE50078AFB7F9F8436F16AEE131C7089DB9BFD830279CE6489CBEC7F7 + 85F1270E8489270E8609D68E035861A015F69932A1A001BFE4279EE2CC46C23C + 5BFF335113CE1E0CE3CF5ADD990361CC29C0F0A130EDC2C93019A8701A57D7BE + 30EE9D8361E2BB47AC2F21428F78DB14428F5A8A2699265C381A265F3EEE1018 + F83B96B38109077DF574989000F0641CBC097E0036043828BBD3D7154149740A + 03515248683F4353A2DF89C099BFF36E5EF690B3F33FBB5238F4388F53AE6004 + AC01DE10A255A15B39BF937EB33EE63C4F20700DF84A805DE0120E439D314A7E + E64700225C87971CF6028249958FE3EC313FE19A97DCA538DBEB71F85E7550CB + F5E80F3C23942E4EE2D93CCECD0BEEFA5DFA6572FE7E6EFD19733B4F23045E6C + 7980F0A25BEF87A55F7CE4C097B38001C04BBEFAC8219883BDAF63B85F9C9C0B + BE783FCC37915FF20D21A0AD2F6090F45B9CC4D72C4F18E6086081C18BBFC4E9 + C979C01122A3953F7DE68E5FC6010C977C1FC310E7022E0286719C021B710703 + FEE6DB73C205ECE7EE0221BFFD24ACF9E956586DD703F2AEFAFE7A58F9C32796 + 4735F0BBD61ED3E1AF5D17000BB85BF9FD4D7B8E9FDAF38BE072898D5DFC2D8F + 0D84FED49FA3C21FE3108E7531FC32FD57FF74DBF4B9BB8881C02B7FBC5D4061 + E0AF875C36F1781B080B8D6398C7CE0030619E577E6DCFC99E03AEDF757FBE1D + 5600E1BFFAD801F02ABB16CF9594C774B0FAE5476119CFDBFE96C538A6D3EB0A + E00500471730CEE908E9BDDDCB318432F2337AED7F9BC35E5E57DCBCBCD6405C + CAEEE475176F04BC2BEDFFC1DF0AF8455C07F8CA3581D38C07DCF2DE61AC5FCF + FACCE726842FAEF9FB01F8BBE0B6CEC1C609FF9EBD261F8737FF979FC2E67FFB + CAEAAF8439764DC2A0CFB5F7E9BCDBD1092FB08B33DEDDBA263E17D4CFB98163 + FE6A68B0D46FD6B0CF0FAE794240CFB86E793E47F639A16EAA7D2E675C07FC02 + 6AA35B17C9B10BFC5D78DB3E57C9050C00E60C603EFBD3AC1DF80BECE5F137FD + FD9BB0FE972FFCF3CD4D1E006007BE09FEEABAFEDD91BE5B0484F96ED1F799A0 + AE00B0BEE3047049F9FE031AEB2618FAD026F08B00B802C0AAA76EDCF9A3AE09 + 9CCD9B423CE3EA456308D14CC8666EDEB978D2434B133981BA51F69D3E96D0CE + 366EFC594B4DC05CC0EEF8D3447DB0B2A572F08E3DC579F1567F26D5DBEF0C7D + 691B69BF47C3ED3708E04B48E8221434E017B7AF3B81F73BF81DE2C0D77E37ED + F7C821F091BD61980938CCEF19BF6B00E0A16F734EFE0E4B7726ED0AC30EEC09 + 234DFC86E2F695E3D7C33BEFC5ED1B01B0EA86BC9520306700EFB631BB01C2C0 + DD1DD1ED6B02F42A4F28E81806DAAE65EAB579A343E03EDBB678BEC7FAB5A1E7 + FA75A1AFE5FB6FDDECE0B78FCD311406FAF535ABBD0FA19D7BAE5B1FBAAF5E13 + 7AB8DBB731F4000213D63913A19F5FB7790D8E6007C06BD6853756AE0EAF13CE + 3973FF22016077FA2E23FC730D00BFB69430CF11F2E68EDF78F3DE22CF7B3868 + EA172C71D08B131851569D8784F61B016DEE6873441D19020846CC193BE3E69D + 11616F0E78AB0030F34E84C317000CEC150056049B1C08AB0EB90B7822003746 + BDD10D8F00E1E713ECC5F18BCBF7F97194A300C09CF34B1AC3404F09ED474F8E + B2FAF6B87A4D1DC610EA796A68677371A0EF336367B8EBB7DD8874F3A6A9C348 + 521B6FF3F9A771F70EB5B5C1F0F136F71F179EB6FCD38323CCCD01701EEE594E + 5FE50B386C79602E72C730FD00BC19E415FC95031891979EB63E6D59F7A430CF + 40E03CF433F9276C7DA6B55B6D0D16D765F91A4EEB37FA214585D2BA903AAD09 + 9FB032EB4794AF3195575962DD29D05BD4915A3F459E7A2CAD67DD019CE02FF0 + 96D4DDBF2955BE00C01D6DADDEE9D5F06047D34BB6AE472F5A1E486B6BED5CBE + 564FF55A9F6B4D2EA8EBEBF8A447880A96FAA83DEFE737982715FB0B28C1DCB8 + A760FD9FB7BECF66C7543D93F52DC9F72F4CC57E46067EABA43D95723987C000 + 60EDD7D4835EA90686056A51DCF3A907C079BDD262FF29F5259F435EEFDBD6C6 + 728DA7AC5DFB581A97ED7B55497B66E57EADFFF45C689D81DF087CD96F7B3EDC + 8FA8CBF200E02A09DE02802304D6FE60DC0B8CFB81F5CAF70EF3BE39F02DC0AE + EA30A054EC4916FD9AA92FEF734AE5FE557D7235E9D71C3056BDA500E01C02AB + DC4A7D7331AEB45FCCBE726BC6AB2E815FAFB7326DA815F96694EF514731BE5A + 4D0170B9ADBEFF1DF5D05DAA6A6CA69C41FCDEFABD1F236740CE8130FB95EACA + AABA8698512D5F1AF74053005CA7C4C0686F4E4DC6E44A1C2D3FFEB4606BA899 + FECDB637A772FFBA6BC0D9505697A9786EAD925ADBDF9BD4A4BFAE9D3F8EF265 + E5E3500E5BFF68DD1100B77EC49E24BA1B00CC1FDD9C2A5E08D4D20BF5CFA0F2 + 3FABAA4FB3A23FD26B53DFCE87840F59D5070D2770AB07A8E7431A3FA8927F60 + 0911AD544A1F6A17FD00C2A50FFF3F42F99769D5171E65BEE0F3BABCADAAFE4E + CA1FEBD7ABF663A41FBFB2F23EBF4677FAC12CC0EEDDCAAE512926047733E168 + 6E62D182AAAE9BD7A9BE5C974B13A6AAC9549D6C22D69A099AE97E9BCC211F43 + 3DE3D3E4ADCAED5B06BE92FA360784BD1EB06A93589487AD91BC8F89C9ED9D00 + 30CA27C639FC7598ABFC9FA2D4CFCBB427D8EBC09789794ABDEEA95447B95810 + A82E2E006A8B07CA519E37B5B27CAED656D7BA1DCEE028CFA73B5C639AF269D1 + 4299C50C794160E48B1C20B02D901EB2C5D2C3CFD942CCDDC1B6684AA23F6E61 + EA95579BD7A505D843763D416481609DCF835427B8AB4560B100B47A0F279DA4 + B67C419997058111503807C3E8919728DBE2F6251329C0B753740023E02FEE5F + 0030A98BBC2907C0459E94721221A13D2C34E5576A8B72BF0B9BC5B82DFE0580 + B580D70600F20D806CC300B1B9A08D81A22E6D12B0994039DF74A04CDAC6CA4F + BE31C0D2140EBAC7C0D0B60FE0373A7C9FEE3DB4D8C410FC4531A499F5EB3F3A + 3C3364426837706C78AADFC8F0442F5CC223BD4FBBFE23437B5CBF08D80B244E + 2018294C346D72053B142624B4A92D6902C21D068F0BCF0E9DE07A46E704931F + 3E317430293474079CC2005F933B7F057E71FAA68DA4BC2E87C002BEB9139850 + D0AAD379C0CF9B5E186F75136784CEC0DE49D10D4CBECBD4060F85F7125018B8 + 9B36D0146A8F3A42EF7565B36EFA1C2F93769D352F74B2BA4E33E6852EB3E3B9 + 6EAFCC59E421FEBACC9AEF9B792F179B7F8B1D06BF3C0F47C8A2D06DEE82D095 + F0CF73E6591D6EDFC5261B3367B695E738F8C5F9FBFAA2F9563FCFE5E5C58B1D + FC0A0203807516B04243AB8C700ABFBE7889D7C919DC77DD5A87C03D57018197 + 87DE6B391B7865E8B36E95BB7C7B0380D7AF0A7D4CFD375AFF4D26ABCF213021 + 9F877116E0D68DA1FFE6785E3067021312DACF06268FDB77FB264F07A430D0A4 + C0DF813B3684C1367E28670BBEB9DDDDC09C073CC85236A00726D7AF80700C01 + 4D78E79D9E1FFC968D7DDBEAF6D1667DBDBC3D0CE71CE0FD764DAB273FFA088E + A75D9E1F7968B72B3A8071F4EEAD73028F3D81E3777F9878FA50987CFA701877 + 2CC2DFE29C5F939F076C9AC079BFD6361A687C725F1873DAC69FB1BA77A3C69F + 3B14C69D3DE820780C70F8CCA130E5FCF130E9DDA361AC5D87B0D313CE027D8F + 17E141391B72DAC553A6D361CA7B27C2641C650062D3B44BA7C2D44B27C3F8F3 + 47C2B8F7905DE7BC8DC1257C091802DC0592708EA5CE048E75512974B4E53DE4 + F3B577C26C20ED47D179C739BE333E00224798A210AFC05DDA39FFB7E1930B45 + 6856200BF5390446F421F57A13E19C0149388A813D0D9FDA75712C7A7868E014 + 4E5F4095F5C1DDCB59A2372F87399FD9637F0A84B2C7BE69D7B881BBF0DD30D5 + 9EE334AEEB0EC60B61D60DD375BB0E50CCAE4B9EB6455F02683F0CF3BFB8161A + 3E8BE16E09F1BC18D727AE4BAB9F83F3F166AC27F4F382CFAF8679B72E8785D6 + 467E0160F873CE66FDD0C1AF42E102804909113DEFF30883017A11E6458008DC + 0348924608FC898B314BBFB16BD973C3CDB9C4C6E140C56DEBE0F85BC2154747 + EBE2AF6279F90F38543FB5BF0757310E5BCEBDFD3439482348E4B1786CCE8705 + AC024171D4AEF9F1565865D7C68989DB17AD36ADB2F1E437FDFDABB0EB7FFE73 + D8F8B7AFACCC19C0B7A21B977385BFC6651B5DB582A102CF72265347AA7AB4F2 + CFB703A1A34981BD3CDFE580E01F01A451D42FC1AD6B7F1FE097C7D439C082BF + 9E7E17CFFE551DE7FE027FBDCEC6F09CF9FB91A0AA8BB376EDB5E235276C37AF + 2DF233777178135E39FD5FE8E7103F85552EFE7FFC4FADAFA02FFF67B5539693 + 578FBD887A2BCBF58BBC3F7DECEFE47AF33FE73DC24D02F6D8368E3379E7DE22 + B432EFB96B9EFACD0CFC2FB9A9E01637335CB3FFE3A7FEBEC5F9CEE7851B2038 + 237BE1E7387371D4E2D8071C5F2DE4A19A3FB5CF85A9015DB7CF0100F8236EA4 + 00FC02812F86D97C164D33ECB31BC337732D5CF7298473066C1BEC73C475F98C + F3B9E7F3CE77C5ECD487CF1F29CF9190D17C56FCF36D7582C808188C8824E0E1 + E449B9B12441621CC572014BF1C6951ADC05E0026EA9F7EFA4CBEF38A4C5A53B + 2D9D6F4EA8FC09EFD977EC05FBFE23A4731275D244FBAEF5FA0B764DCB3BD03D + 87231817F07107C10060A02F2EDF09387B3DB4B37DF75A79520AE30CD41D77EA + 500176DDDD6B75634FD86F00CE5F6BA38EB6D18477C6E16B1A71645F1806B0B5 + DFA56126A250F07B34EED441FB8DE12C608E2578D36F4EF21B9580BD56C6D13B + 0CA7EE41C23FDB6F9089DF3314DB76DBEF27374BC5D0CEA4D48D3A688FE5BFA3 + 1CADC0EFEB4ED7E0BDC8FAEFB1DF63FB6D26A4F3E0BDB4EF0883F6E0F2B5DF65 + D3206030A1A039AF77EBA6D077DB664F7B6FD918FAE0EE35F5DBBAD964ED9B37 + 867EDBB6844140E26D5B438FC60DA6C6D07303C07763E849BE717DE8B5614352 + 63E8B57143E863ED3DD759DFD5EB42CF35EB6D6EB221F45CBDD6E12EEAEE8077 + 65789D30CFABD658BFB5369F89EABE628DBB7F01C0DD97C550CEA4387B05813D + B4F3E2E5E1F525CC9162A867A02F020413EA59CE5EE44EE0C5CBFCECDF57ACFC + CAFC2536275BE237FC75C5ED6BEAE6115F1686CE0DDC183837749C3127749A69 + 73469B273227F46343982F026A01B9005D9CBD367764AE98D77B1B297509F0E6 + A097D4E7A8CC57998FA67A815F39812520B040B043619CBDE308F93CD58F5391 + 88AC83FBB7BDCDB5FD881697CDD5D3DCBCC32804E4A5CCBC7C72E830823EC0DE + 89A10DF0961B3CD34D9CDCE459D3B84280E0A26C79DCC0C0605702BE3900464F + 25C02BC7EFD3FDA3ABB79DF5936AE3EADDBE02C22E5B1711FAD98FC4E1E6D704 + 7F71FE3ED1C3D65896F73ADCBF19E8CDC1AFAFDDDE4840D7C4DA0FA9AFCABE26 + 040C273DAE35A16E1ACECA82BB0E936D9CEAF3A3851E257295AD411F278AD5AB + FD5D9C071CD7AE44B2EAE900F8D114E619E04B197958E81C00273DD809086CEA + F87A781027303770DB9ABB00BF28ADC73DEC732AD7AFBFAD8E10CEB696D77150 + 8FBD646D261D0BC55E40AEBCCE6F0857DD732F3BC025E5DA3C5684C4D6378162 + 81DE320096680300FBDE88F64592FC4678F634DA778D37D3036FADFE81F69D7D + 6F25EEAF747669DFE521BB0EE2DCDE5C716FC7FA3CAD7D1CEDF3D4ABB60F94F6 + 79ACAE7502BE92EA04760BC0EBD27E54DC872A8BFD2C45C42BAB6ECFCBCA512F + 84FB4B7AE0C9174DF67C2DA5BDACFB1F4F226F02FEDEEFFB7635F81BDDC2E423 + CCAD450E643FB05E4D802FFB88692FB14A957B91CDE84E7B9DDA2345F9F5F37A + 545557A800BD292FA5BD5EEDFB16D037C9816D858A7DE24CAD1F88472372E631 + 7DD89F66CF9BF4FE47ECB9657D81C05520B8BC9F5DB5C7CEE3F8633D48FF727B + 640312E5DF6F7FFFDE55E60CBF55BFE735EBF88F94B84FCE87EE56F9B82A00DC + A212BF6A4939E7CACB855A27B09A7447C6A6F696FA54C9216F522B2B4B5ECEEA + 00BBC598FAE7D6DADAA4DF0C80CBCA01EC1FADBB03C00F9B2C6D027CCBCA5F80 + A4FC0F2DABA517F09F45F973FC95CFAFD503295F1A9F7F589A7C40D2186FCB3F + 98A6BA2F009443E0EC0BE10F01C0E931EEF445DA5C5D55FDDD48637F2F95C1AF + D4F407EEDED4D20F66FE03DB04F0AA5CAE2F2B6B6F6E1222B5D4D692AAC6A9EE + 6E752F932DF5CB0130E772A8EC7569B299435C87B4595D6D42DA54556370034B + 79BB743700386FD304DAC1B24DB0F349B54FB89B510E80FDCECC4C79B93CF1D7 + A2E1C176F5F512777AB66E571600D8DADAB33041D6B7832D58DAC5BB567DF162 + F9B88889A9E4D037498B1E2D801E4A90B72EDF3EF6A1EC75A98C70113FD421F6 + D3393C00609DDD23E88B54D6424F0B482DF8C8E77057F9BCAC71AA6B512FBD11 + 1EEB680B5DD3E345DA2B3C0600EE5203C0A49EC70DDC15180C048E7057A1A0EB + C2419B1C0627E10C7E82453A8B7B4BDD056C0B7016FA80602DD475B776BEF82F + 42804969A1EFEDA94E1B08CAE700B8A86323C25244A8E8767D47984686A77B0F + B7F230EBCB9DEC4060E02F8EDE910E839F24ACB4D5E1FA6DD36F4C78AAFFE8D0 + 76F0F8D07ED078EB637907C06854E830708C0B10CC3500BF521E1A1AF89BD7C9 + 152C385C0F8113EC350183259D15FCCCF0783EB0C3DE0478057BCB5018D1574E + 60E0AFF765CC98A95E76F00B0C060227184CA83BC1DF17C64F73F0DB75DA9C18 + 1ADADA3AB19936B5C1459ECD3436D53A9B482536DBBAE1DAB0B4CBCCF9E1E5D9 + 0B7D23CF9D1C33E686AE56C7A69E8B907F7316D6C20036CC0F9D67D9B8D9768D + 39F3AD6E61782D01E06E731B5CB880BB2F26043480784E82C20B42F74511FEBE + B678A93B82150E3A974241F758CEB9C04061E07054AF959CEBBB36F421742261 + A3572C0B3D575BFDDA950E7D7B35AE76F5DEB026F4B6B4CFC635A1CFA6B5A1DF + A6180ABA5FE35A77090FDDCE797D5B1DF4F6A6CFC6B5D1F5BB6DA3D7F5DBBCBE + 068081BE3881776C0A83766E8EA969206704EFDE1A06EFD91606592A17301A00 + 10E68C607723C50D6CDC49D40DDCBB390C7A737318B20FE81B413065C479BF3A + F7178D39BAC7CF47243FFCC08E7856E2913D56BF378C246CE6E13DEEEA1D738C + 337D81C1F1ECDF0927DE76788CEB57CE5F0FF90CBC3D11A12FF077D4C9B7C2E8 + 53FBC29877DE0E63CE1EA8E9DD8361D499FD0E8101BC93CE11121497D89130EE + 9D431E129410A10060DA62DDA130F5C24987C0D43B1CB6FEC005DCC3D32E9C0A + 131D4C1C0DE3CF1F0E63CF1D0CE3CE1F7297F0A4141E7AFC7B47EACA316F8F73 + 19F80B283E698AE974CEF94D900597B0200B02E022B9EBDCA567A24EFD802E82 + C002BFEA47BD00D1AC8FAD4C9D0988049C020C01B1FC5C61800F600890045C32 + 4577E2D930FD933361E6A7EF86D937A35371DAC7674D942F8439B7AD2FCEC51B + 17C32C0F671BCF3A6DC03D0978BA7D35CCFDFCFD30EF8BF7C3ECCFAE783D4078 + E157EF7BB8DFD99F31F63DBF0EE7F5CEFB82FE57BCCDF380EA5B84E405C67D50 + 88B2C24003EC008272A002FC04840583DD41FB4D74900201177F8D9314B8083C + BCEEB01767EC86BF7F131AFFF6B5F7672CD702102BBC711938EAFA2B1DA8DEB0 + 3C20F3A3B0F8AB4FC2527B8C95DFD97501B9D6DFCBDFE3B8AD857D26DDFC6F5F + 877DFFDBDFC2967FFFAE00C0B86AFD6C5D7BCEC8612A703AC15E815FA07004C3 + D4E1088E0098743910D9443DED313CB5F50314FF686D3FDCB6BFE9A6BD1EF16F + 01500380D7DA58423C73BE2F29A0973A52CA02C038817164235E1B5E3352A4D7 + 47691479FB3B08E99D5E434039AF3FF5F14CDEE8CE8DEDD73D4F286352C4FF23 + 42F6087911D7E6B14901BB8064FAA8CD457FBBF67C4BE7DBE3D00F50CF63CEBB + CD75A36299FFFBFB61A9FD0FF91FF07E8B67EDE286C7E96EEF4F5CC0F69EC7E9 + 4EC8689CF1DC64C1E78A9B2B1AEC3300B8253FEFE65587C080DF06FB7C018109 + DB3ED334C33E8B84822654349FB999760D77077323875DC321B25DC7C12F9F2B + 93E7ED33CDE79BCFBF6EFAE033EE4AA097CFB442C53B18CE00306DD47B68F96B + EF14527F8160BE5774B309DF39CA0B00E3E61DC3F9EA174F783DF09650CDB878 + A711E9E0927D5F5A9EEF4BCEF29D72E9B4435EC235037401BBD1F57B224C7ACF + EA2D2FF04B1BA9B7D918CAA34E1F0C634EC5F0CDC0DC4967ED7BF55DFBFE3E63 + D749F5EEE83D61DFFF09F822015F8D233FFCF09B61C491B7C2C8A3FB5C23ECB7 + 66B8FD2E3900B6366E4A9A70F2801F4340C40AA25B7084016D44B5A06E14A19C + DFDEE967F98E3DB2CFCB3A0EC185E33709E08B461CD863D7DAEB75DC5035749F + B5BFB53B0C7DD3FA9886ECDD6DBFC78472C6D9BBC57FA387D8EFF3E03D0061FB + 6DDF61BFE556C7B9BF7DB636865EF63BDF77DB062B03823786BE9B6268E7BE9B + 70F8DAEF35DAB6D5E600763D4B7B6EDCEC7AA3716378633DC01757706378631D + F3884D3666ABCD3736D9DC6353E869753D5711D299737DD77B8ADE58B926825D + 1CBE40DFD5B87EE3F9BD9E2639244E7017D12E37302E5F402FC057AE5F2FA73A + 1CBDB87B01BE72F87699CF8D7B711E47541752BFD9CFE67748E7FCBACB77D6BC + 4282C040DD8ED31A0A017BE5FCA58DA34610C09739A4CF2D4D82BC3E0F4DA057 + 30386F4372FD3E377EAA835FC160E02F751102DBDC77429C0F333F66CEECE097 + 79769203DF4C005DC02E20B7D606E8E56C5F9BC3A7326057A9DA9E26D473566E + 376C5C51471EB5193C3AFC69E048579B0C00E772876F72F29297F337978360D2 + 01564EFD73088C70FE2AF43302F60AFE72FE2F29658EDA89AA81DFA76C6D45FB + 63DD6DED8658C3991CD69AF2F55B2E415D60AD8E0B7A9435A3E5FD26E254CF35 + 7C4D48DF74CD02005B3FBF093957B6567DB46BAFF048971EF1EC5F4F6D1D0C10 + 262474523C13B8D6E7612BA3873A752FF420AE60D6DF49BE2E4FEB77ADD1B59E + D7FADBEB5FB07202BF52B137D0020056AA23A64839828A6B3E617F038F73BFD5 + B95B1827B0E02E37A727105C166DC57158D97E88F644D8C32005FEDEC74DF669 + 8FA595EFBFD4F65ABC6CAA06BFB9EAF7805A92EFF354D42300702E397C5179CF + 2AAF4339F4ADD3E3CF16F9627FED711BEF30F7C54CF6DC00C0966FFD7859B17F + DD98279F2B406E0E80058173B8AB7ECD95CB7B89E57D4629AF539F2A09CE56B5 + A17C9FB47CBDBCCDC3472B2FC09BB77BBD3D0E7BBBF99E6F8500BF40DB074CA4 + 95F0B7C93E72DAA306CC3ED026DC4FDDFD6D22E80500E7FDD33E730E81F3BDEC + A68A7BE782BF02C01EC9B4B4F7FE1F1900FF9EAAE43F89F7E452BF72DFBCBEA9 + ECEFCFCB25367527D5B12B71AB1654F46B1D01EB5DF135F5B953BF5C0E7EEDEF + 49671CDFD74AB2FA229F44DF629C3D377B5EFF0300FF6A009C5E98965E88BCCF + 9DFAFE16FDD6EBFEC6E756BCE12BAE51FE4054090770FE01CE3FD45E9F81DFA2 + 8E7EFF0D1CC0F99778DE5EEE9BE773A9DF9D5435F6B729FE705581DF3BFFD835 + 2FFD5856FD68D6FF1897447B2EABCB7FD4FD871DA576AF4B938C26138DA496DA + 5A52D575F3BABB912642779A5CA11CF496C5784FF30967929CBE82B8799B03DC + ACAC3AD593DE2D006E4EF9C45875F9843A9F5857D557C160549EB8A3AA097E79 + E1D0A44F5A80DC59DCA95A0B09ADB04582B55AE450A7B0460E7893CAFD80BDE4 + B548A28CD49FFC23720C5B3F6F4B77DB3AE84DA92417300B3A16702CF2F24520 + 792D2673E58B4C942F3095AFD2631D7B84C73BF58AEAD8D316A26F98A213F8E1 + 14065A6EE0DC151CC1700DFC96E12FE2BC6089F3839F4CD0D6EFD24E8B76DDED + 4D3D0B7516F582BE7E67B7F5E3EE6F01DF020C5BD9A130794B81BD405D6D3890 + E60E60FAC48D880881058023FCD5B9C1D6D68B3BDC23CCC5F5EB4038815C423E + FFA9CFC8D065FCEC307EFDEE3072C596D07EC018BFF33D3A866318E8C2099CE0 + 6E9572302C105CB88281C109023F3364BCA71D8610E62D415FA07082BF9C155C + 68440CFB2CE89B4BF0973C9B50E4E5FCD5E6159B596C6C49847E461104C7B3CF + 480981070CC6015C00E149B31C0A7B58E8B4A1A60D366DB669130E00DC950DBC + 597103501B796CEEB983236DEE018801C10E88AD5FA7590DA1D3CC86D0B5615E + 78792ECE61360E09171D9DBEC05E5220701900E30CF6F3811711121AE0BBCC45 + FEF52531DF7DE9F254871B98F0D06B422F364597AD0CDD975B79CD1A87C03D57 + 018097871E9C0BBC6645E8B96E5504BF26802EF017F76FBFCDEB1CE80E401BD6 + BB039890CFB87DFB1232DAFAD3E6E19E4D806020B0424093F6A3BC7D6318CC19 + 830E8101C21B3D3F24416017212971F8E202C60D9C9CC04060529CC103F6D8F8 + 3D9B1CF8027FD1C0BD9BACDEAEFFD6162F0B04037801BDC062441E004CFD2887 + C111088FB4FC28CB2BC4B383DFA33884F77A1BC2FD3BF19D8361ECE9FD617482 + C0E37002BFB3DF531CC0A3DF793B8C34017F49475BDDC404733D44E89923EEF4 + 1D7B9AEB1CF214F04B1DA940316984C311024FB632EEE171D66F3CE1A12F9D08 + 132F10BEF4888B50A6387F7100A318DAF448187B0E200C00D679C0B8E8089D1A + 1DC1805CEA119025CF035C0480813182BDD31334160412ECCDCBF42BE090B511 + 425AA017888480420E8005868044D709458BACFDC68530F3536BBF7E36CCBA69 + 633EBBE4C076C6A7E7FDAC52CE2E9D75C3EA7032DEB8E80098F29C5B57ACED4A + 84C137AF387C0308E374C435B9E0F32BEEF29DFFF9D530E7B6D5DD32DDBE1CE6 + 593DE097140732E17B81C00B807E5FD40360777B3AF4AB397E817EC034CA4042 + C1E0988F5050209736524020E9E6FFF47D38FFFFFEBF8733FFCFFF3DACF9F973 + 6F175C14D8A4ACEBF1787A9C1C10FB986F6C0C00F1EB4F1D222EB53AE467EEE2 + 064E0098B0CF1BFEFA45D8FE5F7E086B7FFADC612A6E5C043806FE2EB3EBE044 + D619C400DFF2F9B8E423E8BD6965EAAF3BF08D8EE05B0E842923FA2DB37ECBBE + FBCC9E730A97ECCFF91387C0AE6F629867B4D2AEBFC6C610FE99D0D08061C272 + 2FE27F60F271D6CE6B90035FBD66BC267A9D500CC11CA5FA08CE6B7D184FCAFF + 3607C0FA5FD34E99FF11651E87C78E63E3B5F47FF2BE5F5A5FC283FB73E3F58C + FFBB087D792F7C68EF9B6B96F27884768E67FC2EE239E24AE63D8833DDDEAF84 + 8A46C05FC2447BA8687B5F7333450E6CDD219C20F01C6EAAE0068D4F38B7F792 + 7D2E2FD867CE0498E533627D091F2D384BCA67B321015FCA9EC7A16FF57CAEF9 + 9CF31D401E07B09F012C70ABCFB35D4B6541DFBC4C5E2058F037CF737DC02EE2 + 7B48CA433BE3008EDF7DF67DE8EE5D5CBBF6FD78817284BF80DC29974E45F86B + DFA972F30A000BFC529646DB77317D68631CEDA3AD3CDA21B07D67DB7737A19C + 11619DC79DB0EFFEE3DC1C14D332F0C5E92B6730296520309213D875749FE92D + 77FDFAD104961F79889B9522001E918E3988C721BCE5471A8C3A08204EAEDE74 + C3142058F017E8CB79BEB40B06CBFDCB39C0C3F7ED4910786718B2370A083C68 + F77687BD03ED377BE0EE2D0E7FFB6DB7DF7213B0B7B7FDA6F7DAB2DE61F06080 + F1F618D219F5DBC24D5EF6183B7738F8EDB785B68DA1C7868DA1D7A62D09046F + 0A7D376F7108DCB371830360E4F946BBBE09B88B00C00E755770BEEFEAF03A73 + 194B73001C5DC1F1ECDF37521FC02EF0B7FBB2E80606FE92CAE95B76FD027C71 + 060382DDED6BF2737E93BB5700F8B5054BE3D11E73163A045698E71C0017E057 + 799B13027D3B026E4D72FC9227CC33F097883382BB9A7F027035EF543DF34F8F + 4853EA27E8AB90CF82BFEEFA2D5CC0D667629C0F73B32473668FBC93E6CE9A47 + BB98679BE4E675109C6ED404F49272A6AFE6E3F4C90170BB047B1179415F52A0 + 2FA28D5400F86901DF81B6A610D84DA9C0AF3B809304850B309C221EB54DF017 + D7EF53C9FD4B99A36E047E91F2B90BD841306BADEE11E8E66B31F202BFF441E4 + 0BB76F2A97DB7C1D98441DD2CDC2BA71D8DBB2BE80623FEF17772FCEDF24D6A9 + 8F74E9151EEE6CEBDAB46605000BE83A00EE4A9BAD7D13EC4500DEBCBE0AFEBA + 6C1D2E95D7DA5AA7B3861704F6757B02C0A4B900C0AE342697F6031CEE9600B0 + AE4F5DEB67BA7A1D6AFD2C475675897037816049D0B7381ACBA47D11ED6D68DF + 837C04C02F39E89500BEDA67290070DB96E06FFD5E0E79F5ABDBDB31696F08B0 + 5B6ECBDBF37E55FB502D49FB5AA8BCDF55EC95E1DEC5C9FBE40BEEFC25CD15DD + C035D197737BE3D9BDEA570BDD9C43E0BC3E07BE2D497B86DA3FCCF71A294B79 + B9A57DCE3BED81AABDACBAB62AE05B018019038C45BEE7DBCCFE307BCBC05FC9 + F783F3FE1A939531D5300E400698BDFF611BF760ECA7B43CC6C725E57BD9CDA9 + 0C80EFF77AF6D76B6A0E00AB5CA596DA9A539921E4BA9B3EFF2D5580D99272E6 + 93B39FE6DAF3B67AD9DF9F971397BA939A0B135DEE57A5A23F40351BDBA2CA2C + EE4E6CCEA16E7A1D496162A855D3D7A20E009B00BD005FAEEF8EE7A478DDEC71 + CB69397F27E500F68FD63F0400EBC591CA7FB054EED752DFDFA2DFE3DABFC773 + ABBA46FE4168463900AE7D98C947B5F23B18EC3A459B496FF4D217C93F44D997 + EC9DBE48CBFD7255F56F4E55E37F8B72D76F1504FEB5E287F27EFB2146F90FA7 + A7A6F28F6B21EB53A754AF1FF6263FEE94D384A1B949484B6D2D49E3F2B179DD + DDA83CD16A499AAC35D1136922E7E77AD4402EE017DDF75804C382B8799F2A15 + 6037CB4B8C2FD7DD0900A3BB05C0B9545F057F5179E28ECA137BD27CC150B540 + B87B006CB245491900E78B1DD2BC5D0B1FB5E7FD55473F24F02BE1F81500561D + 77DAE610D881AFE5B5E013002E2FF8946771972F0ACBF9F2C253A19FAB1501F0 + 63B87E5FB2C5EF8BB608363DCC79C01D2DED687509FCEA5CE0020677A907C05A + 645702E057FB87275F8B0BFA1CFEFA9DDB2CFA4DBA9B5B60375FD85396133897 + 3B7A7B0CCE206E0DF62A9F6F2CD0DFC75ACA188990D00E91FB7097FB0817F057 + 00183D6EED4FF51D19FA34AC08EB4E5D0DF3F61E0B1D068E0E4FD8E3D700B014 + 4130605750576017E0CBF50482D547009872B98E54E703037B9F1B31C9F5FCC8 + C93525001C379922E4CD01B0F2929CBF0A59C7C695BB80C74DF74D2E87BFA617 + FC8C33CE3B8B02F8FA39C0E94C60C03020D8457972DC7863834D1B6DCA179B71 + D363C83E36F86AC0372AC260CE71AB01E1D8D7C699BACC9E9B003061A3E7856E + 73E3F9BF9C031CCF029EE7E9CBF3E6BA3B3886845E145E4D21A023F05D1E7003 + 73A6302965EAC9AB8CEB1708DC6339C07765E8B57A8D69B503E01E2B57843756 + 2E0F3D562F0F3DD7ADAC04C0FDB7AC770730E0B7DFFA180ADAF326F2FD391378 + 53A3878276F0BBA51E00CB11DCDFCA8376E202AE0937B0848BA8BFD50DDE0304 + DEEE9BD90E802D2514346702171078B78DD9BBA900C0C060E02F90576078C4C1 + 1D61E4215CBF3882B75AFB264F871FC00DBCDDDA08111D01F18824202F4EDFDC + F94B0A08F6B6D3D676725F187BEAED30FE9D0361E299439EC633800F7828E8D1 + A7AD1D187C667F18FB2E670147E88BEB37EA909FF3C899909CF10804A6DDEB2C + AFD0D0EE003E77CCCB4060D2B1676DFCF9A361620220005E040401DCD6CEB63C + EEFDC6F939C2F10C4C044C01ACD097F2A44B11F80AF492E665811EB5A94E9037 + 87BEA4BA3E6D0E7ED5E703EB435D82BE001ED21C34F939A6B88009030DB832CD + FCF47C98FE89F5B5B4E1162EDF0B61E60DBBAE9581C4052C4E02260B8001B608 + 0D8D4BD7DDBC372F8579A6858482FEE2FD40386807C14034801A67AFE2FC0520 + DB383FBFF5B3AB11FC25E0E7217A91D501F900840281D4F19CF33652DA54BFFC + FBE814A60D58489ED0C3DBFFA79FC2F5FFDFFF277CF0FFFD7FB91318A0882B18 + 002C094A02156BC031867C169424BCF1D26F00A9405CCEF1BDEE90546E5977F6 + 7E8B1BF8BA03E07806700CF94CBB202C6E6000F0D22F3FB1EB45D08B70020B06 + E700184730E0D721A7A5D1F16B8FE5A1A1A31B983ADAA3EB97BF09701D9DD17A + 8E2BAD0F2E5FB4DA9EC32A9E8BD5037F3907B80E00DBDFCB385E1BAEA1FF01AF + 4B04C0119EE78097D788FF17E27FA0FF955E4B445FEA68533B2963C8AB8F1E8B + 723E9EBC1E0F017FE77FF981FFFD9C8BBCFACF5F58FD751B4FFB75BBEE07EEC4 + D57B6E8EBDE778BFF09E55B8744292F33E25DCB3E02FEEDF05B7A90708D7876B + 160046FEF94A0018F84B0868423D13F2D93F43690C605680563768A85E793ED3 + BAD983CF39793980A77D580F78950AF60276D52EE5103807C0EE0AB6EF9B5CFA + 7EC3F91BBFBB62DECFEFB5743291134C3AC7B71E009F76400CDC0500E7EE5E20 + AFC02FF5B4E3F645D4A99D337F47DA77F4E893B570CE0AFB3C2A415EC1DFDC01 + 9C4361F2120EE0A1005D4BD5CE35280F07F61EE2C6246E407A338C3AC26FD05B + A9BC378C0112E3203E68FD0ED835520AF805F0027C513CCB97DFCD087C3DE473 + 16167AE85BD677DFEE020013FEB9068177BA0BB81F113C7672AEEF66CFF7B1DF + F1DEF61BDF9B90CF5BED377BDBA63060DBE6D077338EDE75AEFE563F70BBFD2E + EFD8EE4EE03E9B70FAAE0FDDD7AD77F0DB1397EF26C66C29A02F7AC3DADF58BB + 3EF45A6FD727047472FD169017B89BA512E5D796ADAC01E0E5AB1DFC0A002377 + FBA63A802F2AF209F4CAF52B004C19F0EB10D8C4111ECCD780BF770380737526 + CD0030D219BF2A13F2590E5FE69E405CA49B0F25DAE98728E7AE5FF5214F5DCD + F94BE8679B03731324D1704C02BECC9BC9336F16F025D4B3DA1D00037F53EAED + CCB53331570710E7CEE07609F60A020B0C0B00BBE33783C04F0DB235C5401360 + 17C09BC06F0E7D9B03C0387BDB24772F00582EDF277BDB7A2A9D05FC645F4B13 + EC95047F0580D1E3B6AE7A3CADB924D65ABA19575057EB3194AF0373F9F14056 + 2FC8AB752012EC559BD68ACA03801FEADACB0500D63A3487C07702C082BD02BC + 6A535D13F86B7AC0D6E0F979BF5A67B30ECF01B0D6EA856CDDDF120096F27182 + BEDA1B50D9616F02BF4A558F13B8ECFA45F51038EE67B0AFE1FB15698F23DFEF + 606FA4555BF6539A02E05628957F0D006E090217E19DB3FDA22ADD0D00D6FE94 + EF43A5B254DEEBAADF2F936BB796E73CDEFC2CE0E24C60CE03B631F5FB72D500 + 5879D55701DF2AB1EFA87DC47CAFB1BC2FA97C4BFB9C77DA03557BA10476EBDA + 1F7A3AD6E7757700C07510B8BC3F6CD25EB3EF2D97F78725FAA63C4E61FAB2D7 + EDD77FD4FE6EC6C1190064BAB6C626E5FBDB77525300CCF836756A09005781DE + E6EA5B52991FFCABA90C2BA53AE6931850655B523E567D62DE5EA7BCAD046BEF + A4825595F22D49FD7E95C4F4CA6AD28FC7A9BD8E85525D2B93BBD2FDEFB6D7A8 + 34BE00BE65E58F574ECBF93B2907B07FB47E5F006C2ABF082DBD106ACF55D5EF + B7EAB75EFBF7786ECD8DBF8B0F43E519C07F24006EE95AA5B6265FB8A571F997 + 725955FD9B53D5F8DF26FBA1CBE0EFEF09807395EBC957FDC8EA47BD107DEDC7 + B92CAF47A589438B62A25131D928546AD3C4A598A894EAEE46E5895695D4465A + 40DF24AF4B00B8B54F006B2017F02B01822BE16DD61F95DB48ABC6E5FA4702E0 + BC6F1900FBE4BB62029F4B0B032D18A4BA05425A7C94E50B912C75B5AD415F49 + A05779940360C1DBBC3FA9C648F4C9616F0E80912FA26CC15576FE22C1DF8748 + 4B8B449525167B5AF8E5EDE4B5D894FBB71EF8463DDEB987CBDB81BC2FD98257 + F0F7055BD8BE68B2FA1CF896DDC08FA0047F5950B398D6023B07C2080730AE5D + 16E71EFEF9E518BE4B6701FBC2DDD26253C0F22CF2057AAB00307502C0B9D397 + CD833CAF6B225CBE4FF8DDE98C214C34F037C263520070594FF51D119EEC3D2C + 01E011A1D3989961E48AAD61C0FC550E79D92C91FB17F81B3751B89B7E6413C0 + 9B435ED2AA3CAA720577B0BC03E01113EBC16F0180A3A3A0BD6F244D08CF70EE + 181B4EA91E152E60CAE493F28D2C01604F09138DC64F2BDCBF0A07AD90D03900 + 7E0197F0446B2F6DB645D70567ADE1129EE9E1FAE4E410E4453AD34D9B7C453B + E705CFB27C0680BBCD61E310077004BF84808E2EE0587E65FEFCF0DA8245E18D + 254BC26B8B168697172C70000CDCC5E10BFC7D65218EE0E8F81504161C260FFC + EDB982737F573B00EEEE30787984C0AB6C9CA9C7DA15A1E77AEB934241F7D900 + 004E2EE08D6B43EFB556BF7A45E8BB2E425FEA1C0603874D1E329ABE005F00F0 + B618127AC0664242461771FFAD8DA1FFB64687BF8377C570D0FD092169F57D4D + 6C30E3381A9CC24E2A2434AE600030A942440FDC1BDDBE08F08BE377E8FE0883 + 29C730D03BC2C883B8A0767A1D7D87BDBD3D0CD519C1294434F077D8811D0E7B + 058047112A3A4160E0AF9F078C3BD8CA00E071A7F73BF4251D7BDACA670E84B1 + EFD8D833567FEE9087682654336077D489B7A3232C73FB8E3F159DC0E4D148CE + 783CB9DF1DC00A030DF49D60FD19E3EEE00B27DC012CC03B3673C0391049A017 + E5A0843E8062CA405AC1944909F6E69045003877DFD1977ACA82BF82BB82C2A4 + 3920CE81F08CF76D0CF0E89308801D04590A9872589B207003B23C700EE1029E + FE092EE0F30E7F67792868CBBB2E8659D72FA6B34BEDBA764DAE8B4B71CEA700 + B008C5B81EE24CDFC59C72E76F150000FFF449444154BBFBC5FB612940EE4B5C + A411ACCDFFEC7298975CC03C2E000EF7F0FCDB3508A87A3D378020650160FA08 + 2ED601C3AF0815CD35708B026B23BC551E38B8FAA7DBE1F0FFED7F0E07FFF7FF + EA8E560145AE8BBB15C8A9C70276E60058D7E331795EB88A577EFF5958655AF6 + CDF5B0E4EB8F12FCBDEEA1A0977C65D7FC963EF14C6080B0C357A0AFA5D18D6B + E3EC3A4BBEF8D8CF008E803702602907C0A4F3819E3616C72F61A2D58FF2AA9F + 3F77003ACF9E23E7E00280710047081C1F93BF91E700FCE5CCDFF53F7DE1E972 + 7BFE847F264FDB72EBBBF4CB8FECF9C5BF4BAF85402DAF1975F1F525943330B6 + 06CAF5FF54AAD791C727559BAE95FF6F49B9B6FE378C553B7929B6E1FEFDD043 + 91038101E3ABFE7CCB5403C00BBFFCD85F131CE7FC7F81BFFE3E4BEFB118F299 + B37EAD7CD3DEEFF65900029347F36CDCFCCFB889E1AABFC70558F94CE59FAB59 + F6F920F4F3AC8F2ED8E7E43D3F473B3AEEEDB393206DF1B9B4F18C11042E802D + 9FDF0F2DB5B1FABC17DF05693CFD742D04D4E59ABA362AD709FA0A02037F27F9 + 99E6F5DF45E4F5BD86F49DC4771BE52997ECFBEA32A1A009EB0C10E64CDFE8EE + 75777002C05EBE6063ED3B96B37FC7DBF7ECD877B9B9E64818676DF4C1010C00 + C6F54B7FDCC0A3DEB1F2A9749EEF09FBDEB6EFF3F1A6B15696831788CBF7BC9C + BE486037EF438AFB17009CBB8047027F0FEDB5DF26AB3FB03B3A7C0F037E23FC + 1D6675C371035B3FC24147001CE12F7D475A9A877A260FE89500BF02C303EDF7 + 95337EE5FCC5F51B4528E8588E6701C790CFA4387FFB6C8D67FD92F6CBC06FCF + 469B236C5C6F657EFF3786BE5B3639F8F5737D4D3D1AD787D7D7AE8F2EE00D9B + 6C8EC139C011FE027E7BAEB7748DF559B5C6D2E8FC05EE9601709E9700BFAF2E + 5D115EB3790D4E60007077C24427F85B84784E6E5FCEF4150046005F602F52F8 + E75C7E063063C827D8EBC7799400307977FDDA1CB093CDF7740EB08B729A0FE6 + D057F9621E697924F0CB4D87A4552018C9E50BE8D59C95BEAA537DE106F6F9AF + 29857FF6A356D27C993AE6D572F93A00B6B29FE95B283A81A303B8067B3BE0FE + 2D5CC2E9ACDF61D1F10BDC45E4E51006F60A003B101E32263C05041E30223C85 + 7B17B05B017A0BE09BEA9FEA6F6B1BEAFA46F85B05809F0000F71B9E00700DF4 + 0A0023D5B97AD46EBA650D56C8DB6A00581058E51C00D7B5696D9844596B4341 + E0BC0FAE60AD2F1FB21400FC88A5AC41CB00586B5607C0C9D51B433D236E7C26 + 0A163741770F0F026F3B752F9CBF0E7C3B9980BE2FBD5648EEDF2204745A832B + AFF57CDD9AFE455BB7DB9ABF0A00D39E8FD31A9F3AF239F4CD6130F9320CF6F6 + 67AB2510FC2030B85D04BCEC713CD8BEB6EFA13D8F5A3BFB291900D69E8BF65B + 4CE53D9BE6A47D1CF2ECF5D4EDED24B11724009C43E0BCAE4ED9BE93F6A1CA75 + AAD7BE565E57D7D7F2EC93B18F5603BB350180516C537B4CCBFB7331D4730D00 + E72AC361E49037A555D2DE23FB87F97E632ED5230FCF9CED83D6B5A5FABC2E97 + C6E4A25E8FE175690FD6C7587D5197A5DEF688E9E1760E68EF7FD49E672AE7FB + BFDA2FCEF796EBDA72E5FBC9289563DEDAEF6F135A5B3982647B0CF597D275F4 + 5877A35F0B80A5F2DEFFBDC25F54BE46C152CAF5CDF4FF6FAD9CFFE42A784F16 + 05B6495BA67C6CDEE75E01B01B17F37215B7CADAAB5435E6AE252677576CCEFE + C68AD7140900B7F6FC3D3C377B4C0F5FAD70D0A92E6F2FF277520E60FFE9D4FA + 11D3C39627A55CAFFB5A0BFEA67C6EE7E64568E985C85FB83BF5FD2DFA475EFB + 6E54F1D8F99BAD39157DED0D9C7F582B5502C0DEBFA537FEBD7CD1F1F8BA665D + BDA9AADE543C7E33EDBF557AFEBF9FEA7FB4EE5539DC2DD7E7AA6BB772F1A3AC + BCCA49EAEBE5F4A39E4F00249F3834A747ED9AA8AA2D4D448AC947B9DEF22D4D + 6AEE56E50956AEAA49585EA7495B6B4DE47C5217616FEEFE95CA934987B7595E + E5723E4E5C9B87C0770380CB6262EC00D7529595CFFB94DBEB6413754DE4A57C + C2AF72D56221576B53DD22242D4AF2BA285B5CA4454C592C7090DFDD9A16406A + CB213079F5555BB99F835E13D0D7CFFD4D759E26008C047EEB167D25B8ABC59E + F2E58560AEF26293B2EA00BF2A0B023F668BD9475E8AE0F7511CC0260160DCBF + 48CEDFDC0DEC4A8BE85CE5C5B5C479C08FA705F8C32CB809CDC5A29DC5BBC9C3 + 79590AD005EC521600D65DDFCA936A03C0DBD286031B06DA78C881709E46E8CB + DDE8F18E74E501C2DE9EEE66CF5DBDA80D40B77F74EEE2027EBA7F84B411FA46 + C5B0CF1102CB452C30DC766004C082BA39082E2B87C6928F490EE0FCFCDFBA10 + D094874E08EDE943FB887AA701D2D9C0AE9407000BFE2A55186887BF19007E6E + EC144F058105801D025BD937CCAC8ECD3436E0047F4905803917983A87C2569F + 6FEE09F84ADADC73403C9B0DC186D0D5D2E800660391F37F2304EE36778E0BD7 + 2F7A65DE82F0EAFCC5A1FBA22556C6A5124340037B1190570018D08B04836903 + 103B2CA66D6584C084827E7DD9522B5BFD2AB42C86815EBBD2B422F458B732F4 + 6A242434E70013E6796D3C2778CDCAD0AF718D0360CE02766D5A1FFA9AFCFC60 + FA6F5A9B40F0FA221C340098EB50D76FEBFA087DB7018337786868B98007017F + 390F78E7963060E7A630609709D80B08C6119CB98287BCB52D0CDD07CC25E4E5 + 8E30EA10E131097119EB6967A39B0D6FC266C6F3117146C53098230FEE09638F + ED0B638EBE19461CDE1386BCBD3D0C3FB82B700EB00030F05761A1A79C3D1CA6 + BE7B244C387DC043408F3CB6D7457E34E1A013001E7BF6409870FE7098700E1D + 75A83BEA04B038BA7881B92E1C64290F00961B18D0CB59C00060DA1C005B3BE5 + A938DCA8B7EB8E7DD7EACF1F0D932E9E08932F9D72D71B751300C20012AF3FE9 + 29D078F4994361DCB9230E3DA843532EC770CF82BC02C694118085328085BCA0 + 8F002F658162F248F5BA46018D33300458F270B209F8E2DE051E514F59EE4542 + 3BCFF8F4BD14AAF682F5B3BC09B835CFDA096FCB99A5D38150260F3B4DBB5D0F + 919FFDC9F930E7D30BEEFA5D8643F6CB0FC2229C93C9112C37E57CD33CCB03FA + 1CC2B9FB3786FF15007448C719AC403D2BCB0D0C10CC612022EF10F10BC2FDD2 + 0FB018EB63FF08110180F45FF9C34D0784BA16634801C240529E93EA8A6B5BAA + 30D27A7E0BECEFE40C5BCEEEE5B1B93667092F0396FAB53F084BBFE1BAD7C3B2 + EF3E0E8BED35A1EF8AEF6F44B7AF5D6B31A18C6DECC2CF134CFDEE5387B9B87F + 732DB0B1CA03B2E9A3B37FD50EF805020340E7F3BA388CE56FAA39803D4CB5A5 + CB53F867C240AFFFE97658FD3D61A83FAA3B139836CA05D436E5AF3F69143037 + BE4EB4F358B12EFE0FD457634969A33FAF25795D53F5A4F4D398A2BFFD6FD43F + 8EA93DFE9C5BF67F21B4F32DC6D3F6B1E509170EB0B7F715EF0B1B3BCFDAE7D1 + CFFE8773B92181F7A2BB7D398FD7DEAB9FC5737F81C273ED7DEB8E5CFFBC44B7 + 2F9F09015B7DAE04711D00E3D4B53C6771035EDDF96BEDB8E6FD73C96708D09B + A4B1FACCFA67F8839896BF03DCE19FFA09EECAD1AB32A23CF9AA7D2F5CB3EF8B + D4AE338055570580F55DC4F79020B07F1F5D3DE337C5F09D37C5BE0371FAF23D + E80E60BECB887E70CEBE2F09BFCFCD34EF5ABD7D7F4EC2FD6B291116C6017EED + 3B76EC3B26BE6BAD2F7987BDA70F1563C79DE1FBFB6874F5027A01C10900CBC1 + 8B047EE3D9BEF63B519CF35BCBA36187F64607302097B623F63B64BF610E794D + 847E76C84B08E8C37BEDB7694F18CCEFDABE1D0E7809033D06106CED3A226148 + 0AEF2C1770EDACDF9DEEF675E06BE521960EDACDCD54362E857B46B87E1179CE + 01A64F84C05B3C05F8F60102E3FADDB125F4DF6EE54D8D0E7F51DFCDB881AD7E + 2B219F37849EEB71F4AEB5F984B5AF5F17CFFDDDB8C901700FCB037F7B34365A + 7B630CF9BC6E43786DE5EAF01A1077C59A786E6FE99CDF9E2BD716219E81BD00 + 60DCBFAF2C59EE10B8FBB20880057F293BE4C5010C0016CC4D00F8F50C00CBE9 + 2BD7AFC35F4243A3F9F1DCDF6E0D0B0AE84BDA6576FDF9BF82BD9D66D89C707A + 83A93617EC6873449DF54B5EE597D23C5300586017909B0360893677F44EB0F9 + 6D92CAB421C0EFB3E36C1E9C01E017E84B049C342F2ECF93BDCEE6D1387C01B9 + 82BF8476AE93BB7BD1389BA7DB1A60C8D8D0C1F4CC50CA51D4B51B62737E6B6F + 0B001E686B0A2B038969A3FC141097737BD335DA0E1EE3D017585B40E04C11F8 + D6D7B521B4739F789E2F2A43E0A7FA0CF53E4F5B19484C598EDF3C0434EB255F + 6FA5737F5947E56B2ECFBF61EBB3B446D33A8D54EB3FD50B060B12ABEC75A409 + F4560160C15FC0EFC3B60EF5752667E19AB436CD4518E84708054D1B7D00BEB6 + 366E490F991E04F4167ADDD3075E7C35CAD6E582C0F97ABCBC56D71ADE656B7E + D6FD65B137F0605AE7D32FDF1390A8CB05E4CDC33EE7201801781FE8106F42CF + 8503D8531BEB4E60F637382ACBF2F79B1E686B69B6DFE17B1F6D13EC4DBA1FB5 + EF6463221CA63D77F4965576FAAA3EDFD7C9A5FDA04AD85BD27D7FAADF7F92F2 + BD2949C606DAB59FA5B490F5CBF7D55A520CF71CD3A8FAFD3A9DF50BD4555A07 + 749BA9AF92F611B5B7984B6DE5F6629FF3E1B64DEB92F2FEB9EAFAA53DD4E231 + A8F37DDEA78BFA624CB6E72AF93513F41500CE2170BE5F9CEF2B97DB5CFEB899 + E86BFD3803389E81FA94D5B5094FB6EF1CDAD9E7E411FB9FD68D4DF9E271EE42 + 65077015BCBD57009C2BEFD792AAC6FE2B0986D29C72EE53F096525DCE872ADB + DD4C98953356F56B05C3CAD55CB8E87F84F4981154DBEBC47B20297F4F140E60 + AFB3E767CAE1B6AE5577FD8CE701810BBE982BEF7F275582D77F16E500B8A2BD + 00C082C15500B8B91744F52DF5F93DF48FBE7E4B6AE671F506457C30F4E15039 + 6FFF4700E07BD2AF00C06A6BB6FD374A1FE23BA9A5FEF53F3CFF18B50480EB7E + 584995CF54D717A95F59FCE0671387267AB4A2EE4E2A4D46CACA273DC524A6A2 + BE4A793F4D8E90EAF33A94F77517F013569701E01C049396279471B2592B97EB + 9597AA40F0BD02604D92CB1365DA72A7B0EAF3BEF9357200DCDC849FBC160755 + 0B045F1498EE7BDAD20CFCB60480057073017DD5A6850ECAC1AEA47EAAA79CF7 + D358F285F3576DB6D0CAE12FCAEFFCF5859F4D52B540D4428F540B4AD59317D4 + 95CA0B4CF2EAABFEB57156F7A2A52FF5F010D01E06DAE46700273D443868D36F + 02C0DC81FD723FD7A3567E847A5BB8B388C709FC60068559C84B9473C7AF5406 + C0DA3C2075909B36224811F5F4F330CF84762EB5FB26058F93EE64E73C2B3642 + 80BAED70F226A0FB27D313361E37F093BDE2D9BF02BD8483F6FEFD4715FD0587 + 81BA12703707C0D4E99CE13C2FF82B18EC1078C8B8D06EF0D8D0769095D3B9C0 + 92BB838702811310CE9CBF129B54EE5C48F290D1B401811300D6E616AA6D7459 + 79ACE54D792868C1DF0208A3C93507465972690800E304E6FCB6DCDD51005F13 + 9B7F720613EE1917B003609CBF02C0A6AE0D76BD3900609CBF0BC2AB0B38736E + 617875FEA2F01A9B90BE41B9C081AFC23C03785F5EB0A800BD72FE2AEF5AB63C + BCBAC4FA2F5DE610180180D11BAB56849E6B563A00EE0DF85DBB22BC01045E0F + 045E1D7AE3F42534340EE0B52BADCF2A77FEF64DA1A0FBE30606005B1F87C072 + 10DB38E02F8A21A5D716A1A0FBE1F8DDB2DE451EF8CBF9C07E1E30107827E70A + 72FEE0A6D07F37A1A137867E2660B01CC1848546E4D1C8FDBBC3A8837BC37040 + 2FA130D3A6F8D07D3B1CFE224030E01708CCC6F9B8636FBB08A739ECC0AE30FC + 206701C7F0CFB87E91C23FCFBC7022CCBD76364C3B7FCCC1EF908376DDC3BB22 + 083E6E6308FF7C6A9F43E009670F3A109E7036BA7B010793CF9F70772F9077AC + D5011D280B00932F423E5BDEDDBFE425ABE32CE0F1EF7286E5A13096F0D3969F + 7AE954987AF9B443E1D1EF1CF4542E3800084E38D271C0EBD4A610A98015415C + 80490E80E5BA03B6E0B2537D0E79AB24202CD1D781D187D1010808722865126C + D239C0D4357C7AB1702FCEFCE44298713D8686567FC09400F11CFA5A1B616867 + 5B5B83F599FDF139AB0302277D723ECCBF71B170FEE2029E67E5591FE1163EEF + 502D86D38D8E61202AB035AAE6E675C06A750D8488A62FD0CFEA6817F88BF02F + 424780206394471E563AF5173C543942CAEB0E7C750D81465D8794C7559E34C2 + C678BD58C7637DE8EE53DABD8FB54757ECF5B0F81B7BAC6F3F0CCBBE07ECDAD8 + AF81BCD7DD69CC99BC7A5E9C9BCCB9B440DDC5DF45F7EF02EBEBD0D7AE411E67 + 2B12085EF0953D5F77065FB7FE8C8979956388E8E8FE0500CBBDBCD89E2B8EDE + A5367EB13DFF25F61C567ECB39C01F3AEC05FA028091BB7FEDEFC3350C38D66B + 978359015EFE16E5098F9DBFE6FEBA64FD78EDF4FAA238A6F6BA926A3C296DFE + 3A595FFD2F6A7DA2167C1ECFF805F436DCBC1666DFB0F74F9622DA16589FF937 + ED3ADC60E0EEDDE8F09D7BF33D57C3A7E72CBD60FDE27B8E7E02B439EC95284B + 791DE94CFB7CE8B3A6CF1F9F4B9CC0E4056B95A7CD5DBBF639961C067F78CE3F + EFFAAE285CBC8482B67139E0F5F1761DE0EF04CE23E72CF2D436F9DAE9087C13 + 18A63C3901DF1C0023C15F44F4826997DF71981B433DC7EF36BEE7D078424303 + 8301C07EF63ADFA3F6BDF7AED59DE17B9850FCF65D6CA987E64F65F2DE76F240 + 51E6661D4FED1AF413EC95A33777FACA019C435FEAC9E71A4E8AFBD7DA470385 + 2DCFEF0FBF4380607E8780C280DF11F65B35C2D2087377B8D3973380C71CB1C7 + B03CEE5E60EF98A36FBBF377D09B31C4B3423EFB79C0FB768541BBB6DA6FEA96 + 02F80EDC955207BD44DD882EE0013BB786FE3B623FEAFBEF601CE91607C07DB7 + 6FF232A19F71FDE2008EEE5FFBADDE0A00DE62BFF59B6DDEB021F4C2DDBB6E5D + D4FA46EB17CFFCEDBE765D11F2B9A709004CE8E7375645C0DB6BD53A9BA7C410 + D09C014C1EB0EB5AB6CAE635F15C5FDCBF00E0971727A09BA0AFB7950030A017 + F8CB79BF0E819380BC02BF65F8FBF27C5372F7769D3DDF216F0E8071FCAA8E14 + D7EF4B0E7E6BEE5E6E1044945F00E0DABC527344EA5E4C6057F34B415ED5310F + 95D406EC7587EF8418FA997AD5297D66EC6497EA5EB0BE7E14CA186BB7F9701E + 3D07318746D1E91B5DC0CAE7221474DB618472661E6F73FB41B616B014084C99 + B47DCA93A2B6B6AE28423AA7337E81BD94D5B79DAD07A8434D40AF494E609DED + FBA7046F73E52038CF3B10F66848C38AF591C678D9D64AAC9FAAD65339D8D5FA + 8C7A4459C7FEE4FD1E4DF2FE2975D97A315F17E6A24EAE5FD2474DAC3B01BC80 + 5EF45027CEF38D6B546FB3FCC356C75AF6B12E3DC3A39D58E7DADABA2407BF29 + F53C6B6B94D7A5BE72FF926ACDDDDCFABBA8CF6FFE66EDFF6CE6CC4509E2E672 + E0FBC22BDE4EBF47B8965D43D097EB92DEFF5C37EF4B7D2BC070E9CCDF26A2BD + 7D7683BB9713FC4DFB1D35599FF69D2A0130F097BD9A3200CEF772CA003897F6 + 7EB4FF53956F494060ED3DE5D2DE53BE4725F88BB447459AF7D3FE9AA47DB47C + 2F8D3C6DDAA3CBF7EEF271B9CB37CF175037ABCF616F95F2FD43ED2F56ED3F52 + 97B7E7E5725D5E5F565DBFB4875AB43D6C75DACBB5B6CA31B91EB5B18F58FB43 + F5C69FBCEC7913FBCB3877651472E57BC725B1AF0C54E6CC600780F73F155ADB + 637678A97BE8FC4ABFD0E6996E0E9BEBC6A4C898E5BDECE6F47B00E05CDACF6F + 695FBF4AE571FF6A82A154498CE55E7457E32A98D5BDA8D50335782AC1B5EAFA + FD03B99C1EF3BE07EC3179CF3E64EF01F4607A3DF5DE28E06F54EB8768B3D7A8 + C4E0EAAE9F3FEFAAE77FAF7F571558FDA7D11D01702E0070FAE3CBAAFAC3D19D + DA7F0FFDA3AFDF9C9AF9DBCA6FAC26C0B7248F635EF521BD937E4700DCECB580 + C37CA134A937FD239E4B52FEA16D49CDF5CF7F74EEE787B8E2C7EBF7500E7FCB + 3F9AFEA36A7575527D5293FEA83C462A4D1EEA26174C44EE0074EF55BA7EDDE3 + 54D4ABADB9B226609A8495EB24D547005C73FA962790B549644D9A84E66D77D3 + 3FD7AF05C09A38A3BC5D936BB5DD0D00CE1709E549BFDACA0B06F56902806D31 + 729FA52887C1AEE4EE2D4B10B71EE4D6D729458C513FEAF372DE57ED455B0900 + 934A947DE1A7455E02C102C05A38E670570B4B49EDF9F8BCAF16A0853200EC69 + C79E7500F8416B47E510D065002CD8AB3BACF37C1182CB16EB8F754DF584E3B2 + 320B7600308B77CAEDFA8E081D874D0CCFF41F5540DE2A085C286D34141B058C + A17FB621A13EE50D0AF23A9FCAC5DDEDDCF9DE87B0CE233C8D77BDA7F0D07D87 + 7B9EF3B0B449A2BEB87F11E097BE6C9EE84C60AF3311425A10988D1D3F97CBDA + 057D49AB00B02070BB4111FE224030105820588E603F1F78F8A4D0814D270F37 + 17C3404B82C000E1BCDE2171DAD0120856FEB9B153C3B36380C4930B009CBB81 + 495F12049E5C83C0DA74535900589B73B881BD0EE89B8070B1F96765E4F017CD + B67C92003050D8C1B0D5759EDD6075C9F9CB46E49C050E7E719FF846E4BC85A1 + DB7C2B2F24F433F0978DCAA85717018539BFAE762630A19F39FBF795C5D617E7 + B0E57101BFB674497875C9A2C019C03D5713DE7965E8B3769583DEC205BC7EB5 + 035F84FBD7CF0AC6299CDCC202C23884FBE01206009B0823CD59C23AFF5700B8 + FFE6463F17D843436FDB10FA5A1B7227F08E8D61D0CECD6120E703EFDE1286BD + B92D0CDEBDD9EA37847EDB090FDD1806ECB43E7BB614F0D721EF5E4259C67382 + 471ED81D46EC67B33B868B1E6C75B8A32441600917D5D8A3FBC218426C72E622 + 1BEF87F7FAD9C03AFF97F0D0B881A79D3B1AE65E3DE32EE0E15637F0EDAD0E80 + 471CDD1346E0043EB52F8C3AF996A77530F8DDC361CA7B275C805C42428F39B9 + 3F8C3B7D309EED8BEBCC443BCE5F07C284863601802725F04B1E780CE4F5B328 + 2D256C29AE37A450A53ADB12200200D1D998D4210F878AFB8DE77425025D046C + 01F2027372E802FC25DC346D82C002C07202EA1A8876815F496D3805F350D002 + 431E0EFA7A0C070DDCC2DD8B3B712A90E9E3F7C2CCEBC0AA1A9812D4C2F13B9F + 50CD2680EECC0FCF86D996CEB5FE0B3FBB1C16DC00109F0BF36F5C70F08B16E2 + E0B576FACFB26BCEB5360F036D0200731E6BED4CD6087A81C25E06FE9A66DBB5 + 29E3FC1400141C14141448A64D90913AF5410287804B4210036191DA18C73578 + 3E94C9F3B8A4F489C031824AFAC671AA8B0E63FAD00EFCF530C800E06F3E0C4B + BEB5BAEF3EB614F76D74D20A6A221E03004E9D877F166C05087F43B9D677E157 + F678D6BEE04BABE339D9E3D2273EDE270EA0695F42C868AB5BF2CD0D5374E4E2 + 3476F84BBB5D13B8BBD8AE894B9BFF970030C21DBC08286DCF0B68ACB399F91B + 056C11799501DF7A6D90FE4F1AA3B6F8F7D6A4F15C3F3AABE338BDFE94F53F89 + D03D87C8C05D427673ED1A0496EB9714018249E7E3FEBDC10D0888B0E500DBF7 + ECB360EFEF5BE4CF99CE3BC4D56744374914374BD8E7423750E42A3E2B26F593 + EA00AFA5FA4C3AC8B5549A0AC4E5F36B9FE5FCB39D7F5738B4BD72CAFADA583E + F30061CB5337C5FA710DF2132FD9F78E956947D479287AAE75355E63CAE50880 + F59D23104CBE00C017E3CD2D405FBEEB80BE63CF1E8E37C758CA0D327ED3CBBB + C7C2A473F67D07F8C5CD7B86EF51222E109A7FBF435E07BBEF10AE3F025F52DA + E8435E7D39EBD71DBE09F60AEEE600983C005865EF671A6D1A4B9D8D91EB9750 + CE9CED3B9A281487628867449E36A030E5A1387993BB17980BE4F5F0D0269CBE + 94679C3B1916BD7F314C3F7BC27E0B77D8EFA4FD2EEEC1E5CBCD523BEC3771A7 + D7E1E41DB833025F3468A795776C7547EFC0048601BDFDB613F63982E07E3BB8 + 196B5B1880E397F0CEDBEC377AC7B6C059BF82BFAE466EF6B2DFF54D1BEDB77E + 73E8B769939537DA9CA0D1D583F37D53C8E7D756AF09AFAD591BBAAF5E1BEB71 + 07AF6DB4B989CD15D66D7039F80500AF5E6FF3981A007677EFB2E8000600937F + 75490CE79C3B7F49A92BC23E27D8DB79EEC2D0C9E655B9E3973A0160A4F0CFDD + 12FCEDE2E7FA46D89B3B7E3DE4B3A9EBEC08803B0180D3FC4F7340C961AFCD17 + 058099173257646E8972C8FB522AD701E0C9D66673D31727C6B9EAF3B87B99B3 + FA4D8B33C273369F7D76ACCD6FB9C911F7AF976D0E9CD57113A4E6C40E7B6DCE + 4CC86707BAEEEE8D9057375A56CDAF35CE61F0E03111EC028113EC15D0A54E10 + 98B470F5027E07D540701325D02B017C515EF6737D13C0CDC53A46F92AA72F65 + 01600967EF1369ADD51C00CED765883ADAC8E3EC0500BB0B38B515D0D7EAB4C6 + F336FA0BF6027A4D02C2791D7D1E67ACAD3759676A7D0A04D67AB400C05617D7 + B544BA6A1900D32EE0DB9CEAC635B336CF0130E9A32F5A5B09006B8F40EEDCFB + 499338C7F74140B0B7BF5C405AFA3EDEF18DF042EFE1A1B3AD17DBD8DF88A317 + F9F51893AE9143DF1C0A037FD9ABC88513B8555B406F6DEF23B6018833000CF8 + 4D00D8C33F67FB36F99E4D592DEDEBE492BBB7AAADAC2A009CEF4B49F93E1569 + BE4725F87B5FB60717F7CB74DE6F94C240C770CFB5BD3AEDDD21DF634BF539F4 + 2DAB0AF2B624ED1DB2B7984BFB8F525E5FD52FAFCBEBCBAAEA93D701681DDCE6 + FBB1E5BD59EDD722F676F3BDDF2C9F0360E02F6A9DEAEAFA9645DB036DFC1CE0 + 071FB5D727F5C50DFC94BDC7DBDB67EE89A73BC66B59BF5F7B2462190097F7E1 + CB7BF194CBFBF555ED621279BF96240EF0AFAA7B86BE3204E6C6C09249B04595 + 98D59D04A72AABAA5F9DC4C77E673EC76317A6CA076BEF2507C02660702BDE4B + 5EE6B310CBAD2D05000B02371BE23A7FCE55CFFD5EFFA62AB0FA4FA3DF0B00A3 + AA3FBEB9FAFF08AAF8DBF5862ABFB1CAF5459B8FBF870F6EAEDF0BBAA62FDB66 + DB9AAB97B2E752F565AC0F68555B95F2FE77527363F21F95DF0B0057DD15F55B + 01B02BFDF856B6497C91E5130853DDE4239F5094FAFD5AE9FABF55CD42DEACBE + 2C9FD8A5C9A626909A3CAA1E882B90AB7C5ED75C7FB5977527005C4C869354AF + C972DE2F2F23C15F4DAEEBC444FDE9A690570B0226F3E5362D12D4EE137EF2B6 + 08F1C5872907C04D40703300B8358B9DACAC0550FDA2A726015D9535E64E0098 + 855D2E1665BE40B3BC168079F826202EA9202E2A435DD55729EF1B5DBFB903D8 + F2847B7E81F40D3F07D8C340A314F6595228E81A008E8B6909C8AB3396B4C826 + 65A14D9A9FC1449B2FD0D3E29D0D0040301B00DD27CD0963576C0A3DA7CCF390 + D00AF3A54D002907C0DA6CD075D8406023A26D9FE10E9481BCDAA4D0E604A2BE + 4EBD868436691324DFF4202D42A3A54D91E8FEAD854D93B46122000C14F636C6 + B219437EE0A8D08E4D1E934360DFB821BCB48DCF00B01CC2390416F4CD017019 + 02A30880E3E6936F380D8BE78C39E865238AF64C45FF0C0047017B6B0058C017 + 004C1E158E6036D8264D2F802F9B730AA3576CC6651210D659C1D439309E3ADB + 37F9BACD5EE0F0171770A71956974240776B98E7CEDFCE9C0B9CE4E1A1ADFEE5 + B99C2B17371901BFAFA61084EE4AC994BB584871B9442D71752754F48A95EEFE + C5054CF8671CC0DD972F332D0D6F58DA63E58AD06BF52A07C0B87B81BA7E26F0 + 1AEBCBF9C0AB97BBBAAF5C5AE40582091F0D087657F086B5F13CE00D6B22F02D + 03602B1316DA21F0960DD1119CA0F0C01D1B43FFED566F1AB47B7318C206362E + E0ED315CF400137D68F3709700DE047FE502C6F90B00565B0CFB4CC8E71D0E80 + 4929037F95E2082E1CC2BE990E08C685B52B8C4CE7032B1C34E7038F3E4A384E + EB6FF5C38FEC0EA38E7386E39B61CCC90880A59127DE74083CFE0C0EE0231EC6 + 796C82BF02C0D44993CF1D0DD32E9C74203CE6E4DBDE577DA610B614C00B98B0 + 3A3F9F92EBD97527BE17C12E009832675D72C625792F33D6CA40604031E097BE + 13CE9FF070D0401CE08E208BF2022F0060E490C7CA48E15F0580D556805E53DE + 86730FA833EB23C2315F0A0DC9D12B0095C32B77F7DEB8140130E34D4063CE0B + A68F80550CF97C3ECCB3BE736DDCAC0FCF8606DCBFC9F1BBD8AEB3C0FACF7CFF + 5468F8E86C5874EBB2E98AB7E5EE604245F378F1BCD5ABA1C1F29C8D8A3CF4B4 + 5D67F68DCB96B77A9E979567DD8C80580050505012448E7030C2454147B53356 + 6DD403800561818F1A43BE06166B40598E57C6A21AC8A41D101A812D1260E6CC + 5D00F0822FADCF57EF3B005EF6FD7587A47A5C3D26E91CCE974D653D26CF51FD + 0981CDF3D1E3F3B8722073F6B187A2362DFECAFA5B5B7C5E9CA16BCFE3EBEB5E + 5E60FD177326B35D0F080CE405020380A36B9BB0DD1F793D22BF98C7B3E79503 + 789E27CF8354793D6752F5531FEAF4FA5047BB443FD5F3BF45E4550608D307C5 + 7C7C4CF251FC8F794FD863DDFED0CA11F852F6BA0485BD9EBA9B57FD7331CFC6 + E1F0E59CDFD9D771A7E3FA25F4733CF717884B2867FF8CF0BCECFD0900A69ECF + 8540AF3E4BA488CF97DAB8D9429F39441EF1594472E6026F1DF23AB88D377D20 + 7D0708FEEA7B406552C9BF17EC7B84BC5FCBCA7CAFF899E0F6F99D61757C2FD0 + 4FD2F551017B6D8CBE8F3C4F4AE8E714C940CEDF7853CC6107C2A408F04BF405 + C15F07C0EFC410CFA312E0150006F4129D41D057EE60EA461D8F67F4027675C6 + AFC239E702F8CA05EC10983AD338CB8F071E5BBB3B7A9308EF3CF2F01E77FFE2 + F6CD21301A62BF4B83ED776BF05BA6BDF61B9780AF04100604CFBDFC6E58F3D9 + 4761F67BEF788867E0EFF07DBBFC4805CEEF65ECE0BD3BEDB7716718B8736BE8 + BB7DB3FDDE6EB1DFD72DEEE405E6F627B433F596F64BE57E5BED77D9F283760A + F8AE0FBD37F19BBDC5CFFAEDD90800C6D96B5A0FC84D2078C3064F1DFE720E30 + 00789DA5EB3758BA3EBCBE7AADCBCFFB4D21A001BE847BEEB77E930360E02F21 + 9E7B100EDA43424767AF87735EB6D2E61E56974243CBE52B00AC32F017C72F62 + 5EC41C09C0EB0038B97C01BD5D32182C75B63E5D92B3D7433FCF4AF91C00A7C8 + 2E2F37589DC9E778C9D91B5503C0805F77000375D3CD82CC1B35BF64BE291730 + 7348A511FAD68E29718DAFE51D06A79B199F4BF35B45B921FFCC689B2B9B7CEE + 9BE0AFE6C5E47308DCDEC45C1BF88BF279B7E6DE0E83131806EE026D81B91D08 + FF6C655207C056EFF0D7DA480B67AFA5B4D38FB10AF9ECD729D61111F60AF8BA + E39770CFD4D19FF58989754A59F9BAA7253D8938EFB70200E7EB2C44BB944360 + 4910385FDB3D666504C8ADABB7B2DF346CEB47D6790E8F4DAC2139EFD70130EB + 485B5B6ABDA97568546D4D2A67705CD756C3DDBC9E7C73FD72502C697D5DACB3 + A9AF58C30B009721701D0C76BD1C53AB533D02DCDEE780B67378FCA5EEA1DBA0 + 71A1CFB859E199EE833CDC33EDA45C2FBF66017E937C0F43FB143C16FB18568E + 7B1BF1FA0062DA5BFB31591DADADB6D7A2286B9439922BDFB7C9F76CCACAF776 + D457FB3AC5FE4E82BF88F0CE1EE239ABAB531B93B597F79ACAFB53DACB625FAA + 684B529BFAD6EF9DB1A7C6DE1AFB72F58A6DCD982CEC7A713FAE29F895CA80F7 + 4EF2FDC247B37DD03BEC5FAABDDCAF381718A5BA2A558EA59CEDBF7A286829AB + 2FCAECE756ED09E7E5529B00306A762F39D7036D229CA56F1AC359B00F3ED23E + 3CF0F833F13A40C70CFEDE2B0CFE2D0038DF9F978A3E892D10C237BF8EF26589 + 03FCABAA1200037413D46DD2DE1C002E97952F2B71A93B499C8AB46C642CF76D + A20A3EF66B943F2612FC8D4A219E81BD0FDBFBD7D48AD4E1AFEAECBDF388BDBF + 2A00B0ABFC98E5E79CFF1DE5B6BB511558FDA7D13F1A0067D23FB0AAED5F56A5 + BF3D7FA322DEA455F575635B91CF3E9CCDA9FC81E6CBA0E2CBE40F91E02FE2B9 + A47ABE8CF32FEAF29776DD3552FFDFA23B5D87C747FA616B4E553F6E6509F0E6 + C0D7EFC8CACAE531953FCC65F11CAAEA73F137E4930A536DF2613FF0AAF78946 + AE6C0C6D55CAFBE44A131B3DCEAF5119EC4A00E02A085CD4FB64304E24F3C963 + 3E89D40453F59AA0E6F5EA5716EDE53177E300D6A45813E37C62EDA9C9DB1F7F + 36A6A9BE0A00E7652D02081BA4D041C827FE3699F7C544D6E60B8754A68D7ED4 + 7948A1F6264B0947541B677D00BFED92DA66D03793C21CE58058E0D6173DA94C + EA8B2460AE29AF27D51852F5D135BC9C1670F9428E8518691500CE41700E7311 + 8BCA5C5A6856B56B21CAF9BF35D1DF16B51900261500CE1DBF406042419327B4 + D6239DB5A88E12007EC416E20E7DBBC485B600300B7189C5F993AFDA829E32E3 + 6C71EF0E605BE0BF36617618B37C63787D6283D7B3C04702C16520ACCD851C00 + 93B219D1BEDFC8D00180DA677871777ABE4921F0AB7AF2BE6961FA53CFC1C5A6 + C89F7A4508FC247DDD0D9C85474B1018D04CAA31A402C0A40E8781C0FDE31DF9 + 6CD8E420D8376DE85302C0B9A8EB3038025F3981CB8E6020B08785F64DA78971 + 932A6D4221DF84CAEA8A8D29C4E6D4A85A68BB02028F2165736C6ADA388B69E7 + 29B3DDF5AB8DB4E701C329B45EB1099760B0E0AE60301B78B94BC3DD1BA92FEE + 0E36F900C01E0ADACF87AB07C0C05E815F4207769A392774990D18C6FD1BC30C + 027F5F5BB0D4D31AE0AD49103817F09750D1AF102A7AE9B242AF2F5BEE2900F8 + 8D15CB8BB4E72A20F08AD01B572FEEDE0477BBAF581221F0AAE5E175F256EFE7 + 019B04804981C6FD36AC71000CECE5DCDF18027A7D208C34E703037C1DFE6EC5 + 25B42E0260EBE30098B3809300BE84861EB07D830360A4FCA05D9BFD2CE0A17B + B73924F63C7017876F72FE7276304E610F079D00B060308097F0CF82C2C881B2 + 8FB73E84823EB0330C3BB0C337E6C71CC309FC5688E733EEF4B382C98FB6BAD1 + E90C6001E0D1C0DF136F9A527AF26DABDB6F7DF63B40186D02FE8E3D09DC3D14 + C69BC69E3AE81AFFCEE13005500188B0FEE3A80340583A0988F1EEF1E84E0348 + BC0BE038529C5919CFB33CEE6E37897A3FDF12087CCEC603814D93780C6BA7BF + 9F15FCFF67EF3F9AEEB8B6754D8CF0200C094B784F9084F9E0BDF7DE5B92F0DE + 8320410B10240192A001BDDBDCE6987B6EDC88AAB836AAA45255A9A28CD49042 + 0AA92175AAA9A69AFA0753E31973BEB9C64AE4074372EFDAFB9CDB7863FA5CB9 + 5CE69CE3C931262159BFCC9E79022C4017855B5DFDF92DEF03D805065530C7F3 + D9E33703DEDB0E7E37037E0B14A25FD56E29A19A77FF9ED0CD5FA4ED3F13E2B9 + 0574814F82528251ECEDBBD18F4558E7ECE998EB335C0600CB9B77AB893C8077 + DF1FBE487B792D1BB7FB878FD3813FDC4B2FFEFD5769AFD5E31DBCEBE7CFD39E + 3F7CE92284F4CE9FEEA5DD785FFEF14B07665B0067F65A19E47E99B65A9F2D76 + BC4DD403DEF022FE63F6F00508028BE94B5EF0575052A010A00838A58E7652C4 + 31809178FEAA1D68A971E4192B380984A48EBE8CA3ACBE8C053E22202CEDD4EB + 35FC58FFF47DDAF30F5FA7DDF679E009FCE2BFE13819FEF23AA43A6E3E16EF33 + 1F5BEF81E350D6EBD29736423AE76309F6B62030E793FBF23A3F5B5FABB3D7E1 + 38C0DCFD768C8336268778FECEC33F23203040D8F7FD2DDEC02F729E360660AF + F7AD7391A89378FFFABED4576DFA8C688FE3794FF4AF7F9F02C21A4F1FBCAE69 + 575FBC7FB3F738E0F97BABCB219F499507FC0287775BFB8E9FED7763BFA1DDF6 + FBE33D017FB7D8EF16AFDF3D7F220C34DEF178F066880BFCC50B98FF8380AFEA + A98BFF23E5D58FFF0E905775EA432AF8EBFACAFEE7DF642F5FAE0BFE6087FD17 + C947584B7D0C07AD76C6712DD17F9FFFAB5F43AC5ED706F21BBE6A015F5D7B78 + E044F097F182BFDEA7EC610EF405002BCC3D0018EF5F453E58F9710B003BFC65 + 0FF68FB256DDE1219BB7FD3A4BA8670FFFECE5B7FC5A4C4A996BF25A1B8B00BA + 82BBAB6D3C7057219E15D65910384262BC7D01B980DF551CDFDADCB3973D7A4D + DC4B500EFFFC4A5AC8FDAA68C11B97D3DCD72EA4F9EC4F4F5BD9DB97B0CF1ED6 + D9845730E19ED7BCFF76DAF9C5C79ECEBE7CD6A1AF0360BB47CEE2BE7A85AD15 + EC5E4728E78BE7D2CCF36732E42D02E6CE36CDB1FC5C74D1EACF739FB67BF839 + BBEF028AAD7EFAA9E3A9E3F48934C7DA39C6D49327D2B453848106EE66008CA6 + 9D38E17BFF3E7FE4489A7AF498CD0F6C1E70E2549A79EA4C9A51BC7DF1FC9D06 + F43D76C2DA4E3BFC658FDF1947ED189677EFDE027D5FC0ABB778F6027701C3C0 + 5FF7042E75511EF23900603C80B5DFAF402FF057FB023BF4A55E2078F7BE34AE + C05FE660936C3E86C82301606DEDC11C4FF3BC0C80F1F8956C7E180030DB8528 + 5ACCF8008035A78CF3C851EB6DAEB9D6DACBDCD41F502CE07774A9D7438D0E80 + 8B872FE988553CE048997970D922A540DFB67971A993572F736879FE32B71EB4 + D0E6E945436C4E4E1F87C28B2DBF80683ED9D317A02B4F60D2ECE99BBD7C6927 + A54E65445900D8DB0AFC8DD0370AE83BD0C4BA244AEB97B87661ADA3540FC0B6 + 897510F5652DC5FA2BA69D89355A6700985480387AF66AFDA7352069EF491DB6 + CEB3B5A5A5A8A7AD2F7B4EB03569D193B606ED0B082EEA33917528FB01DB1A76 + ACAD6D1DFEDA9A96F529A1A103D88DA017CF6014EB057809F32CA9CC1EC0E841 + 00F8BEC85C23B22200569DCAB20B28950471C9771F3AD13D8107DA7A7ACC4CFB + 6DD87B1324563F1D4B75EE058CEDA2D82DDCB631D8FAD9B1D8BACAED1BFD46A6 + 27FA8CB0363C7CED38D839DC4602EC6D3D608F8D4500F83E7B8D49769DA8A67A + EA22FC753B5000BC0F05C0D898827DAA49D1AE2551EFB6AB22F523EDF1F428B7 + 9391CA7E966D6CA4CAB76C76F4555E92234797CAC697ED7D5182BA5D01BC7D86 + 54A29CEBAD0F632D95DD105BA3DB2CA33DB3D4D56D92557B51558F64132D754D + EA747C933D5575B19E3CF6DC9EA426B7EF0E6A0BF94C9DE7830DB8CDAE4C9FEE + 035257CB7726423F93FA189D4329FB316BB6EF58176DD94D766D290260F4201B + BC5880ECF5AA6B04C0D8F5FF3300AE98CF7DED9DC1DD58DF591F147895F24D52 + 3B6904C0119C5675A55C497C2BF0B15F221DBF594F6500CC6FA00060C1DFAE4F + 0E4EDDFD9AC17F7470EA0604EE35C0D50681EBAF593FE7F83EEA6D8FA226B0FA + 57A35F0B809BDE704DF52FADA9CF3F47F15EE39F26CAFBE833FC3300E0FF4D2E + 8C5CB0ED35E3459D7C635F93CEB1498FD2073D4A1FA41B5B676ABAB945C5BE11 + F80A00D3A79EFAB807C9FA35E6A3A82F62AF88A8AE9A74789B5DE4985C84FE5E + 1F27154D62BC26267595B6FB263F4A435BACA7AC895954BDDE416F68439A040A + 006B42D994479A506A8259CF23015E49ED28D633D18D8057D20458B0569365D5 + D7E5E33489B63C75DD078C4E3D068E71A99F0070B7A7486D8100C4AD09EF609F + CC5BAA3A0F2BC4F16CA1802877B33275E4A9F345852B2F281C0C0709F446F1A4 + 6B7752E50BB0D5A248D23EC182B96A579DC6C43E7500AC45991671CA93568BBD + B240D462510058F502BCA85A485A5E0B4DB5D5013029E0B75A94C63D80C74CF3 + 30D019009B8AC76F1F3C7D0300766F60C2449727A8FD29EA02799143DF028D1D + 145B594F629317001630EE377976EA6BF205BE2DFE07CF589C4610EED8522DF8 + 23F0ADC02F06044B65344018201086055280EEC08E4515D8A53E1A24047D9522 + 1D83F6A7A72FF4A7DDFB593BFB026314A90C2480608EEDE9423BDF25BE5F309E + BE32AEC82BB8CDC86292514620987DC01C08CF5E9606DA310600ADE7E4BD8225 + 07C27C2E08E05BA0EFA022F2F202160476A35381C143AD4E5E0AF20296B14A61 + A165D072104C3BF50E7F119E12D9304638BC711B76A467B7EC4963D76DF73246 + 3307C0C0E102802BA06B7919EBA893E7AFFAC88087518FBD81BDCD84F10FA3E0 + 846DADF0CF9507701B04B631780A6FDFEB5EBF0843631D0063B444E415AA50E0 + 576542411306FAD9FD56DE67C7B23402E0670FEC774D3E4858684030DEC12FA6 + A9875F4E1DC78EB8080FAD3221A201C1806160EF340F157D244DF5D0D084823E + 9A669E3A9E6660203E792CCD380DE005009FF6F090D34EE1157C22CDB98091F9 + 4C9A79F6A429836240B0E03062CFE1E9678EA559174EA67957CE96BD81CF3904 + 2624F4FC6B8488BEE4109870D1F20046EC173CE3E24987C002C3D42BFC33F057 + 0078AE1D07F80B186E03C56F6180BFEC46794274AE7CEF0D0FC9C9FE8C0BDFB4 + E3708C1BAF79E8CE6584EFBC712D2D7D0FEFB0D7D332D2F75F4F78F12EB3F252 + 1BAF50A08001DF5BF2FDB7D2AA0F80BBEF3828204FDDDA8FDE75799DB5ADBFFB + 7E5AFBE14D070E6B483FBA99567C68E380BA776FA51577DE766F352007614E37 + 7D7EDB538007E986CF3EA8000829E349A9C72B6EC33D3CF46EBB57F05A3CEAF0 + A403A0E05557CAA4F4D9F8D587691321B02D45D4ADB1B6D5D66783E5B77CF371 + DAEC403843620123441E39B42504F48F40DBEC7DB8F1BB8F7DFF4F2030650161 + 6014DAFCDD2769D3B72D48857600B3380E9EC1EC69FAFDC7698B8990CE40E0DD + A61D768C03BFFF22BDFABFFF37E9D6FFF25FA533FFF6EF1C0EEFFE9DC9EAF7FE + F19BB4EF4FDF7AF8E85D5EF7A51F97D7DF66E3050481A00E7FEDB5B6FC7C2FED + B4BA5D7F9F212E9078939D17297D0183C06352C0228050F05080953A8E8D6823 + A51ECF5AFA0856720CF2D421EA04300527739F9617B2CE176FE50C20F339D19E + C7581DFB1D170F60443D5EBB82BBA47BD9DBF7EF5A3079D7DFB5E033CAAF933F + 1FCA8CC9C2DBB6058033846E81618E99BD65A9E7B57208E8FD7FF75D3A00ECB5 + F201DAECBC0580D9B719CF5FE02F70D8C5B99A0803CDE7C179707E883C8AF5FA + EC548FD41FF119E97BD0679D416E86BDFAAED52F1E8FF79FDFDB37DE47DF7FDE + 0798EF1368CC79E4D0CF5B7F67BF11D38EDFDBD83F7D9F76FFE1BBC41ED73B6C + EC2E3CCB2D25DCF9667ECF3FDDB5327B66F3BFB0FF0F0F39D8B938F8E61CEDF7 + 8A1843397B0F7FE1FF157EC711EEA23A00F6871D4A1FFE73EEF9CBFF9150CDF7 + DEAFFEC7005CFD7FF93F2B05C6728DC0CB973E8CE1A112F5170006E466086CE7 + 6163B94600796903EAAEB5E3ACFFF28EF75B01C8BD7BC3C3CE531618E63AC4EB + C9DB3787B7FFC001B0A0AF03E14FDF4BABEDFAB8FC76F6E6250FF0E51AABEBE8 + 2ABBBE722DF6EB3150B6C0DA95EF732D6EB5215D8B4955265D4C18E7027BB9B6 + 577BFB5AFD62BB3F10F29950D178F50273B967781DF7811B79EF5EC45EBF887E + F200C6EB77EEF58B0E7FE712E1C2EE4B805F0030D0977D7DE71221C3EE7FA473 + B8075A1D1EC10BAF5F71000CF49D4F0868BB7712F679C1B5CB69015EBCE7EDDE + 7BEEB403DFCAF3B708AFDE7997EC7896BA2E9CB3BEDCAB4FDB3880F059BB379F + 4A33CFD87DF8FC39BB9F9F4E538E1F4B53D9DFF7D4494FA71C3B96759C79C0A9 + F4DCCB87D2F3A619EC057CF444EA387ECABD7B49A71E3BE15EBE805CDA3CFFF2 + 5107BA4AA702798B57AF4240E3114CFD732F1EB279CD8B0E7C01BD005F79FB52 + 76186CE3C87BD9DA81BEE376EE7569DEE4DEBE3B6CDE459DCDB99E25DA0AD058 + E19F01BD26CDC9A8F3D0CF005C1EEC63AE57C43C6F8CCDF746D9BC327BFF6618 + 4C9D8B870D4DE336EE34E5F96304BE23D7B2BFEF66D74807BD5BD268D3C8D544 + A8D99446AEB2B967A8D3438D9E5673DC0D360F667E6BF3D815AD90CF2ECB3327 + 96C72FF911D6877933736820AF1EB004F2528E75CCC98798060279FD61CD2C01 + 5D80AFA0AFA0AEBC7B4991E0B0DA2507C4F46980BF035C8BAB8752057925CA5A + 9B5016F055BBA0AFC23ED7F7FC25D5FA2BAEAFE23A4AA25D7D01BE750182110F + F146A9BECFE4BC16CC1EBFB61E9D30A5C8D6A156EE3DBE5D7D274DB7B52610B8 + C3D6B32FA4EEA36C4DCDDA98BD7F59B3B21E65FFDF12023A825EEAD4A67AD208 + 7B9B00B0CA5A634B5AAB6B1DAF357A13E48D0058ED6A531E09E4AACE81AE89B2 + A0AFFAB927B0A58490F6B0D0B4533738DB265CD827B05D0C1CEBB60A2070EF81 + E352BFC113D393CFD831B175585B4F6C190E8F5B91D6C8577B005B3E02DD8701 + E0CA6633C0EA07649BCEFD925DA825B70F151B51A552271B956C59AA47D101E1 + 893ECF5475EEFD6B65D9BE7062202F9B5A04C0D186267B9B8070EC433946F213 + 0096BD2FAAB211169BA044D9ED86766EDDCA58D90EDDFE58B74736D836D51EC7 + B828D7EA62DFBAD4AEB1F576C9CFA1131B6B979E45C53EDB66ABED31B0DD6E4B + 39CAEA647F6E126D006095BBF7B2BC95F104F63A7B5DFA487A8DFAEBC43EB1BE + 6AEF040063F7EF4CD12EAFBE62056A7F54FB7F94C6FC2D2A02DEA836DE13F520 + B8FB48CAECA9914B453655F25D2C953363D55EF29D1E43FDE2B19AEA1FA638AE + A8C5D532737A02017E0B00EED27B50EAD97F58EA3D80EB935D73FAD87FE0C9C1 + A9C79383528F08801B8EFDA0D7753DE4DCEE531358FDABD1C30030EA86FA98CA + 1B7ED807506B6BFC61FC0B10EFB933001C5579D13E4C7F0300983FA22EE8E841 + E7D0D4D6D979ABBEAEA61B4AB3F20DEB97AA3300DCD4B7DEA61B6B256B8B37F6 + AA2EF6515D11930136EC8F0202AB6F9E4C84FE6172A189449C50A8DEEB1A2627 + AE3271699BD084FAD856EF5F4DE282D437B62B7FDF9800809934FA84D4A40964 + 6C236D4D34737DCC3749FDA37CB26B6D753581DE07B521EA753C5226F4BD068F + 73018135F1D624BCA9DC1D31E9AFB539E835F5B08502C752D9FB86C5445C7000 + 80B380C663ADAE80DEA01E316F0BA01E654124C0DB048023F4452C9650AC4302 + C0150C661156931674D502AF2C1225415EF2D5C2B1B6A0242FD8ABF69857392E + 4ABD3CEA0587BF7D0B007E72942D7801BD9697C76F3B00B645F198BC781600E6 + 69EABCA0EE7060DCA60933523F602F00D89FCCCEE0575EC1C060F7042EA097C5 + 7D0F9ED69E68ED5696EAF03702601910A2C141105865F2F42595E1223EC14E1E + C5BCB711E66CAA1D675A0B00037D81C503420A68C60BD83D81CB1EC082BF32B2 + 0C99BD340DA6CE84B186A7F601BF43E72D4F432C1D0C009EB9D4F70BC61318E0 + EBF0570098F6E209EC60787E0E054D3BF9A10B6A00388486F6BAC5390434462B + 19AF047F25ED5F5619C4E867C22886114D06B1316BB7A5091B7759DD166F1FB5 + 9A507B793FB5516B31CA6D73208C1786E78B710EA39D202FE017E31D205861A0 + 2B386C1A6D7D31FE8DDFB6B382BFF20296F0FA8DD0378AD0CFCFEF7BC985C112 + 236615BA70D73EF758A11CDB80BF68D2BEFD69C25E3BC681830E7F09079D4342 + E315B3D7217086BF2DAFE06947E8F3B2F57FC9F3338E1FF310D1C0603C85A71D + 3D6C62BFE0A369BA09003C0D687CE2589A71E2B8A7D34F9A00C2674FB9C11923 + 32F9196708094D7A224D3B69E34E1EA92030A9E74DEC13CCDEC0EC0BBCF8B54B + 69D1ABEC41783A43E1CB67D3C26B17D28257CE3B209EF34AF6F85DF0EA8534F7 + 95730E81BD0CF87DFD72F60E0E1EC08060CA780F23EAA9AB4244571E59847F7E + 2DADB8F57A81C018EAF1DA225C67F6DE22050A2F0602DFBAEED077F9FB6FB816 + 5B7F79860918001310C0411E6594D5B6EEE35B15701010C61BCDC7E1790640BE + F38E835CED6509D84502BB02C20E7909116D79FAD247751B3FBF9D367FF571DA + F8E5871E221A08BCEAEEBB1E1E5AE1A2B56F700C21BD1EC85CCA84975EFDE92D + AF07006FFCD2C600814C401B8120044C22042C00088F417900029FF05CDCFEF3 + E70EA60051025579AF5080D5C769C3B7D93391FD451D7CFDF8991F4B62EF5FE0 + 2FE9E6AFEFD8B97C908EFCD30FE9ABFFF7FF39FDD3FFF77F4DD7FFFB7F9FF6FE + FECBB4F70F5F3BECDDFBC7AF1D349202813DB5FAEDF6DA00B96DA6ED3F036873 + 38DF0C42BF493BFEF4B58782DE0438E33C397FAB13389437307594050D05133D + 5FCA5B0B5414D4553F859FD67169D75840633B6CCC632867D80CE0FCCECF9936 + A0EEE9FFF4AF5D7E8E84C2FEA7EF3CFCF3DE7F0598CC218C691300DDF3F7D467 + 984CBABB783CEBB579BDFC5A1980A20C5CF36BA316ECCD21A125EA80A17BFFA1 + 406280AFF5A9F601A69FBD560C014D5DDE1BF83BFFCEF6D9EB027F513C1F445E + E719BF03F2887A0160B5915227004E9D7F8FA5AD2A737C2B93EA3BD2E7428A57 + 3BDA659F9BEF876CE7BBFDF78CE7B5794D1E08F8CAC67D9576FD6C7DFE68DFC7 + CFDFA42D3CCCC077C96FCE85B72E60967DAA3FB694872348F37F45DEBF02BBE4 + A9470A09AD36A4872B047CE3C316FE904549950700035FF97FF29085FE63FACF + F27FC63B18000B7405CE02711D005B4A6400FAF0FF07EC5207F4DD646310791E + 1E596BD710DAD7715DB134E773D8F9959FDCCCFAF4DDB4FC63BB5EE2DDEBD7A7 + AC18FA9936D2B50060BB7EADF230CE6FF99EE978F1722DE55A1B61AF602E5058 + D75B445ED766AEC948F9782D473A1E750B00B726E02F107789DD67F0F415D805 + 00D346FD9237B3072F50970789E897F703B6D7B274A1B5017E917BFD16E8CBC3 + 4A94F1FA1500C6FB97FD811D0CBF6AF557CFBBD8FB378782BEECFBFACE7FC5EE + 83572FD9BDF382C35E41600FF16CE9ECF3E7EC9E6BAF79E9A2B5017E81C28479 + 3E9766587EC6D933A9E3F469F7EE25B4B3EFEF6BE5A9C7D9FBFF549A4DF9E4A9 + F442F1F80506539E7AE4A8CD158EA58EA3C76D7E71C4E615563E62F7FE23C72B + 908BD72F6587C05607ECC5DBB70E7DE9A772E5E5BB3F7BFAE2E10BF8F5B0CECC + 834C82C20E804B1D732607C03BF6F8BC098D05FE16AF5ECDB7340FD31EBFBECF + AFF57100BC7DAF87701EC3FCCE5289B2423FE3ED2B002C8D451BAC6EFDF622F2 + 78FC5A7F4B47DABC7384CD4D7DAEEA0F28E6B9AACF494DE43300B679A989767F + B8B1C0DE36D05BA48720DDBB17806B79EA01C003F1E8B554F365015F52016045 + D8E1C1CB6718BFD0EA6C3E3EB044ECC1EB57020047182C200CD825C5D3B7DF0C + 5B6B14108CD44F7DEAE19FA5FB1E406D00C012E50880552700CC43B05A2F9122 + 415DD65FE425815EADB990FAD5E56368B335A0CBEA220CEEFD6C86C0FE20F164 + FAB0E7EFB49421F0D4ECF56B657900037F7B8DB3B5ABE581BFEE013CF685169C + 0D7097547057ED02C0486D5284BDE4637D6C63BD1DD7D99459A3EB61EE6A2D3E + CADA46D83ABEACFF057CDD0E60756A932210C67620D02B5B02A19A91CA6AAF34 + A428D82F649B20C4744F13F6902E03C6A43E239E4B33966C4E0B57EF4C83C775 + A42E4F13696D7CEAEDDEC1E37D4B2DE02FA90070170030769E628B9117AFEC32 + 75A9CD6D38C1C6F330B94DE8A96C1F728FDF22D99F6483726147EBC46655D559 + DACBCEBB8FBDFF27ED73F2F1368E34435ED22C8581CEFB00DBF84EC34123CA05 + FA5AB933F88B64038CF642496DB12FF54F045BA4CA4DF035F6A9FA3D62DF28B5 + C7B1EE9453CE4FE715FBD725A71DB7C116BBAC4BF65FD563DFADC9BD7CAD4D72 + A85B24DB73ACEFD17348F60856BF0280C9577663ECDBE135740C29D657EDBF31 + 008EF50FB2FB37A9CE0CFE96D4047F511BEFF94D95195DE450A8E25791E1D14F + F5B13D941B8F81749C70ACC7563C5E115CAD6B0F5EAF00E05E26FBFDC80B1800 + 8CF76FAFA7EDDAC4758732FF832749ED3757B85CD3B11FF4BAAE786EDEEF216A + 02AB7F357A280006FC4ABCA1DA0710BEF8EA03AD3E187D510FF8B0FF0675DFFB + 7D80D4278EA9EBCF0180FF37D1030070DB059A7E92EAEA6A686BBAC837DD509A + 45BFDF46110637B5D76F98D50DDDEADA546F8B65D515E9261D01F07D63D4979B + 7A4D0E8C2DADB7D7273D3E31D1A4A54C5CDA2631F4E77B2D75B1CD2733A12E4E + 86EA13A258A7D7887D9F20EC0B134F26AD01FED601706712D4553ED62B6542AB + 7E5E6F62A2DB244D925135A136C5B6FB26D3610C1375E02F936A52CA6A535FE4 + 13FAA2C6C97EA9573E0260F7082EA057E0B75DD66F908D1F441F5BF80C2AA0B7 + 28C2DF2AB58591BC7BA508839116504800580B2B8D517B6700382EDE2A7512FA + 19692119F3EA435975A471E1A905677B08685BE80279474F4D7DDD0BD8F2A65E + A3590CB3A06571CB0298A7A2590CB358B685B33F413D2DF5422C92594CF334B5 + 2D1281BE7D27CC7029DFCFCB1D0E887D115FBC8105817D3F612BF79ED8E10098 + 053F005821C09A4281090A47A302F9688440323E44C385E0AE806F34522080AE + 8C1B949F9A36373D3D7D9E436034B02303DF36005C2030E1A6074E2F4FD507E3 + 0B61A009478DC80BFE12FE792806A1B9CBD310423DCF5A9606CDC47063ED82BD + A43EAEE519EC9EC075CDCBE1A1150A3AC26001608C5A82BE4060015F0C5728B6 + C90B8254463419C1A897218D14203C6ACDD6349C3DD6D65A6A7543975B9B9547 + AFC348B7D5F3EEE98BD1CF52F7FCDDB8DD813075D99363B30360EADC4378E30E + 0FFF1C25F84B7EDCF6DD6D8647851E244F1DF077CA81436D5EBE32609217F86D + 09289C3D7F81BF39ECF3A14ACFBDF4529AFCE28B69F241DA5E74283C0DE3EDE1 + C30E7EA3777086BF8CCB80983E80E18EA3EC177CCC01F0942387DA41B07B0267 + CD3C7332CD2AE124A7593DC203983ED308277DEA689A7136EF113CFFF2B9B4E0 + 0A9E48676DCC2907C1D10398F0D0405F01E03997CFA459974F3BF8C540EE50F7 + DAF92A24B400B08B3C756FE43D7FC957E19FADEC7B0263A42F6017B147235EC0 + A8AD1E8FAEE2BD453D5A7E0B0FE0D71D042FB9F96A9AFFD6D5B4C8C603081602 + 026EBC568106798E910212E8A336C106F200E0D5054AF838E0450919ED21A12D + DF4A6F56DEBEDAF35240983E11086FFEE2C3B4E9AB8F1C00037081B900604030 + 65EA81BC9469732F6113801809020B10D37FD3571CCBC67C6EFD09295D2030FB + 890A040392143216E1D179F63FFC533AF46F7EAE40D48E9FF3FEA5C05E424A53 + 2FAF60F2402C01E0CDDF6638B5F53B1BF7E3DDB4E307D38FD66EE9A17FFA3EDD + F89FFF53FAE8FFFE3FA4F3FFF19FD2DEDF7F957601467FFADC433F038277FDCC + 1EC25F781E104CFDF61F3FF7E36EE2B856DE09202EF072B38DDD00443309BAED + 05E09A7602E04CEC23EC20B0B4B1572DE52D3F7EEAA9FA21A032F03142490023 + 7011F0A87A015EF5A19CA164869FC060CA4047C0AAEA8FFC977F4AAFFC1FFF43 + BAF2DFFD3B0F338D07F0FE7FFAD6C33FE3010CEC15301534DDED1EBF1900F35A + BB4CA4119E0A9C6ACCCE3F52CEE3A20482913C82F7FD231EC13F260F01FD77DF + A53DD68F90CE00DDFDF4F5EFE20B07C088EF45D01EEDE673B1D7748FD7F059D4 + A1ACCE538A9FA14499BE95F7AFD5C5EF50624F6A7D77E4DDB3D6C44300FE3D58 + 1BDF37709673DB63EF83CF43E1C305E41D08FF6CAFFD7BFB6C7E6F7D7EFACABD + DCB77C6FBF8D9F38461EBFF3F7F69AF61BDBFAFD47050217486CAFA3FF87002F + 79441F447B86C82D8FDF0C7D4BF8747EBFF6DBC6F37E1D219849BFBEEDFDB6D0 + FFEB0F1DE6920AFE0AE8EAC10E8F02503C70DD1398EB80FDF791602E02F6AEE7 + 1A63296D805F522031EDC0614512A0AC7A20309EC0C0DF25B7ED5A79C7AE7F1F + DB35EFEE4D171058F0172D07107FC435910765B8165A8A07F01DBB3696EB6BBC + DE92027B69E7BA4A9D2031A9A0AFC620CA88EB3829E3F128A68DEB3B001898BB + F49DD7D2E2B732F805F0AE78D78E615A62F702E0AE3C793D94339EBDA6C5ECE9 + FB0EF7181BFBE635BB2F5DB67B19F72CFA5FF53C75E4175C07025F49F35EBBE4 + A097FC22EAD8EFF7F2D934F70A7BE85BFD2BC8FA5CB998E65EB6FBA2497BFE56 + 00D83D7C4D6748ED9E7B2E7BF6769CE6212DF6E93FEBED84789E5A423B23EDF3 + 8B47F0ACD367D21CD34CC23C1F3F91E69E3D9766D9F1A61D3D96A61F3B9EBD7F + 0F97BD7C5FCA00F705C23A9BA61D3A9EA61F39E1120056B8E729D62E08ECF5C5 + 33983A60AE03E0027F81BCA4B4A10AFE325FE20139E64B659EC49C0908CC3EC0 + 687C01BFCCB7903C7E81BE63B6E4C82C9A8F21F6FD8D915DA4B8EFEFC8F5362F + DC90F359CC154D368704FE8E5E6762AE69734C34120179576EF679A843601379 + 9FB35ACA5C96B28070BD1F6904BF82BE517A2892763C8099036BCE8C1C109779 + B400B00361E6DE3C8CE90F72DA5C9E0735FDE14C1EDAB439BB09804B2AAFE028 + 00AF423DCB03B8FFCC4515089647701D007B5DED0154E5057F057925CA11003F + D55152BC80F1FEB53591C4BA08D5D75B92202E0097F595A4B5DA93A55C7FA0D7 + D774B4B3CEE34161D6872E5B2BB2A664AB200FFD6CEB45531F93206F9F09D35C + 805F4160A97701BA11F822F24800578A6057603896639DEA11F0B79BADC77BD5 + D6E35AA7D701709FD1760E23AD4F81BA4AB10B903629F691D7AF002FF603F523 + DFD6A7D8223A53B7A1E3DD86D16BE8C4D4F5A9D1A9DF88E7D3FCB5BBD2924D07 + D220FBECBBF2E03D768D81EC019CC12F219F7DDFDF222F0F1C953D760BE07D10 + 0096EAB61ED97BEAF5F572B435D515ED55B259DD27DAFA3CE3DEBD136DDDBE78 + FDEE3475C19AD4D73E3B07C0D6DE82BEC31CFA0AEA46B8DB99BAF99EBF435B65 + E07080BE11E84AD15E28A92DF66DEA872A7B256951633F54EC9AB1EF238931D8 + 5DCB581DB37E5EF78D8BD23124D972836D37DA97A3A21DBA0E69293BEC45E451 + 8F16F4551F3FBED5F9B1C278B54BB1BE6AFF0D00706792CDFF515567037F4BEA + 0CFE76818344E6E375B9ED3E153EC4984EFB54CA8CEE7E16753FBB7A503ED655 + 9C4FFCAF30C036C5365397AE36F621AA3346A95BCFA75D5D81BF6C77E9E0D77E + 631EEA19CE616513E5AE56EE6EF2F0CF0F63923ABFA63614CE1F75E9F260FD26 + 00B86BD73E6DE5279EB81FD836D53D4C4F74ED6DEA5552806F4D15FC2DAABD79 + 3E90F8A5D43FAC07B5FDADEAD7BEA7383E1FA3E90FFA70F99FBCE162F250F17A + A8A9ED578A0B71FD22AFFAAAACD7AFCEA1E1E2DD707E3A56BDEED1C439FD366A + DD749B8FAF9BA56EA86D377589725D4D6DF53A95EBFD427B057743DF0A188776 + AF6B98906802539F60797B6DA213DBEA758FAAFAEB7A9DA502BF82BFB9CECEC9 + 26A302B9F57C5453BD60EFFD616B86374F8C8B9A26CE9DD535D533596F02C075 + F81BCB71925F9FF047A90DC0DB04807D61510160940130218C24954911009850 + 4782BC02B912757802C7BD8251B5882A7DEAE322008E8B35A56DAA0160522D20 + 911694F5BAB8E0545EEA1C00DB027694B559DAC7DAFD89E531AD45B016B679A1 + 5BC263215F54DBC279ACF519676D84DD1A0708B6053AFB031302BA8490160CAE + EF05ECF0973A6B770F62CBF7253C3430F7D90C7E91C3E0027F23007643415134 + 2EB861A11822C863A810EC4511EE22F5573902E0FE53E638FC1DD0312FF59F3A + 273D65F9813316A441184D1C002FF4630F28205850182F61DF738B106C96FA7E + C0B397B97C4F603467691A6C02FE3EE31038C3DE26E1FD8B06530E0038E6F118 + C6BBA00900538707C260F2C57805EC8D1E0C0A575701E062F012E41500768398 + A5CA634073C31A0637C2ECADDB969E59B1C105F495B10E402CAF5F00308077C2 + A6ECF93B066F8EB5AD3D820589F1021EBB696B1A6D1ABB795BE5018C57F05800 + B0EFFD9B8D8EF23C21151426FC73F4009EB807F8BBCFD3497B316C0284F106CE + 7513F6ECF5D0CF930FBEE89EBF0AFD4C19203CA94061EA271FCCE9D4C380DE23 + A51F1EC380E14315FCCD65BC88B397B07B079F38E6E914EBF3FCE1971C04CF3C + 8911F8749A8DA7AFB52B24F48C5327DC3318283C5BED0E898FA68E53C7D3DC8B + E7D2A2572EA67997304CE7B0D040E139174FA799E74FA699174EA6B997CFB837 + F0528CE8780517004CC8E739AF9C4BB3AE9C49B3091B5DBC7F4901C208D81B81 + 2F8A7B000380DD1318388C576F01C0883C75ECCB480AF05D79EB75F70EA69DB2 + C360FADEC84078FE9BD6F72DBC83B3273012581080105C501900A1BEE4811384 + 2F5D73E7465A4F78E78F6EBAD72F7B0ABB871BFB005B1D1EBE9401BDC0DF0C80 + 69CB5AF3F14DF78E73CF6007C077D20679F55A0AFC655F6120AFE0AE802F6D40 + 604162F5133CD6984D80A12FEF3814F2FD852DDDF0C5EDB409AFC1E215085452 + BAF1EB3BEED579F6DFFF537AF95FFF2E4326ABDFFA530E5B0BB42215D8AA84C7 + 64E5990824FBC4F71DDE0A3CFEF113DFEB7737FBFCFEFE9E1DFFAB74E04F5FA7 + 7D4044F611FE21879F060213F29914989CC3497FE6F07787F5DB6629402E7B64 + 66CF4F422B6FB63E5B4D3B7EBE97F7A1B57AF7BC2CD04D00B082C3C0C0527680 + 677DF0D2A4CCD87DFF40F86379DEE6F0D0C047C14AE0A42027AAC34CC4790180 + A91374A50E2FE6FDFFF8433AFEEFFE211DFBB77FEF1EAEFBFEF1BB440868F601 + F610D096DFFDF719F6725C8EC7EB65A8FB950351CE01C9DB577095F616100612 + 5397C579D453FA027EF7FDE34F769CE2114CBDB5EFB3733E60C71600264CF701 + EB8F27B040BD3CB5DD5B9BF3F2D7CDE72C001CCF3F9F5706D5A41110EBFCE9CF + FBE1F3F7E351A6DDFAF31D0BA492DF55DAF8FE621B1098EFDFCF4963AD0FD017 + F8CB6FC6BDC94BFD8E9F4C8481B674674977D86F8D7DAEF7FCD13E673EDF9FED + 3BE6B7FFFDC7F69BB4DF3A0F410068ED37EF21D4F9BE8300BB1EC2D9FA50E6FC + 54CFFF68D37778F8E2457FC7EAB2B72FF077ED971FA43578EE967DBEE5E59BBD + 775B7BF346E1C9BFC1FEE70060DF8BD704C0CD5EBD19E84AC05C802FF5A4F423 + 8F2730E5559FDAF5A588FED4A1359FDD4AAB09034DDB5DBBBE15E80BEC05062F + FFF0ED0A08AF2464B45DE3B8FEE1F92B00ECD743221F705DB5EBAB60AE43DE92 + 727DA57E895D93757DAE54FA10167A955D73575A9EF1F4E31AEE6DE59A4D7919 + 755CDB4D4BCBBD60390FFDDCB46BF5BB76BC1B765D7FEB9A7BFEFA5EBEC0DF37 + AEA679AF5D49735FBDECE9C2D7B907BD66F7262030512A08F7CC3EBFDCABD8F3 + 37C35FFACFB17BE55CD36CBC7A2F9FCBFBFC5A7EFE5560EFF934FB027BEAB3F7 + AFDD03AD9E3D7C01BFD9E337EFF73BFBDCD934EB8CD503804D78F6CE3C7DC601 + EFF493847BA6ED9C954FA769C5FB17F09B3D81ED1E6D7D679B661C3F913A8ED9 + 7DDDFACD3B6BFDD9DFF7E851AF9F7EEC589A7AE4489A71CC8E79A440DE0277C9 + 3BFCB5F4F95247A86600F0F4C3C72B502CE84B4A598017C590CF82BFE3F71C68 + 01E0F2B01CFBFBFADCC9447ED4F6DD69B489D0CF9A7B45017E476FDEE11A63F3 + B30940E06D7B6CBE96433DD725F84BF8E791EBB73B0446FEF09F89B921F09739 + A4BC7891E695A34983A7AF348AB40060C15E015F3DD81861B03C8199F36ADE1B + 35B4A415180EF368E6CA0060CDA5072D2AF3E932F71EE2917A56F87C7CF812EB + 6775005DF7DE9DBBACF2E46D53A98B00F8294BFBCD58E802044BF4690FFDBCD8 + D71F82BCA482B95538E7027B69A7AFAF57E8471F1E92A5CFF4D22F847E96FA95 + B593D64F551BF5B60E9317AFAFC9CABAACEA1FE4F01795B2AFE300C771AD686B + 43D687ECEF2B8F5FC15F07C056CF43C92E0030DEC178FDDADAB5C7685B03B3B6 + 2D10983C0097D4D7B33CC46C69CFB0DE15E015EC95AAD0CFA59FC650A64D00B8 + 476D3DAEB57A5CB30389057FEB3600415D95057E631F140170ACA71CDB3C4583 + 9B1F50278FDD82E86700E09E5626EC33E077C824FBEE46D8795A5B8FC1D617DB + C7C0ECF54BC867C15F07C040E141A372B8E64700BF52DDCE13ED4192EC460FEB + 13E576296C68C53ED526D53F39D401F00B7356A40D7B8EA6B92BB7A6FE232667 + 388CE3457F3C7A8B1CE266A8DB19008EF5ECEBEB7BFB96FA26001C6D8ED1EE18 + F3B15CEF5797DB158BBD32DA1863BBA7EAFB4BC4F18BEAC7F6E376766CECB38C + 919DB6D86E2B5939DA946557AEC3D8962DBAF3FEDDB0F592A7AED4C7E37BBED4 + D55F57C740B1BE6A7F44001CEB5A36F3DF5E753EF04BF45B1EEB5125E81B9519 + 8F9D8BE773B98DFD38F02D6D5DACDE94C7368D8979132CAAF0BBCE94D9D42FA8 + AFF1C1FB14EAF12C7635805FB579BFC2CAE26B77EBF15401C0F6BBED6DBF4FC4 + 5EBFF69F63BF5FF68F8623F15D760304877D7F01C03ACE7D7AD0B9A378FE5D9A + A16FD4AF06C0805D007004BCBF04F636E93E005C07BE6DA2BFA5FA801EF641FD + 67B9E28F36FE9025879DF1CFF988F23F79BC90B441D507E851FB3D86F893E9E2 + DED48E7461ED4C4D63A4A6FE8FA378236B52D3CDAD3371C36D8DD36BB4DA75BC + 3F3B008EF5B15D938ACE64ED02C0D5642548939838C1EAACCF6F213F66994055 + AF5783BF11000BE246781B556FAB7B0D0B00D78F539F3447314156AA7CD3645B + 6D51F461A2DE63600ED98C28AB5D13F9CE26F3D4D7170675793B0B88027CEF5B + 5804209C21713B0096A89727300BA10878B53892687B62C0189760705C50D147 + 0B2B2DAE242DC8B4608B4FEB56F9027FBBB15F8F4965A90E7B558E1EBFB15D79 + 0F395544DF7A7FD5791F5B10EBE9685F1C5B3DCA2038ABB7A9E798E7530F80B1 + 2DB67BB1C006E88EEFB0311900BB07F0C4999EB2785708682DEC95171CEE678B + CDBC47707E0ADC0130205846849A22F425C5E0401E43044605F2182A80B482BC + 6A238FA2F1427D3330B67AA0EF34BC80E7A4A72D05FEA2011DF35304C00E7D8B + 34DE81309EC233F27EC03C811F43B29147EE1D0CCC9D933D7DEBF017E83BD0EA + D91B9810D082BEA4ED0018C35106C0BEFF2F6911F595E6AF4C8316DAD862C0C2 + 50E5462B1375EEED80B10B4398B5BB31AC18CC6424232F6318ED6E4C5BB1C981 + 2F107818A1F82C4F8A048647ACA1CF260FE13761F34EDFF7377B030387331896 + 0483C76CDCE60078CC96ED69CCE66D55486860F058AB93B11163E4C49D077CDF + 5FA02F29020657C6CC5DBBD3D81DBB2CBFB7D2B89DBBAD7E4F9AB4775F1ABFC7 + EAF6B5423FDF0780D913D8F2EE0D6CF53144744E0F158F60F6076E8587060647 + 08ACF0D000E0178E58FB51AB03F09EC133E86406BC278EA599969247CA534FBF + B978219D2104F4690F0DDD71EAA8835FBC80E75CC053E964EA3863FDCF1E770F + A7C5AF5D4ECBF0A4BA7621CDC62B180FE0AB671DFE765CB4D7BC74D2F7F75D44 + D8CC572FA539780F9BE695B0990B5E635FE0B23FB0F5C1139814080C309EFFEA + 0507BD848096772FD0570058020AAB8DBE2ECB53EFE1A2DFCA61A417E1557C33 + EF0F290F32C108A5820978A4C92B8DBEF3DFBCEAFD377EFA41DAF5DD6769E367 + EFA7151FBC9916DEB89696BDFF465AFBF1CDB4FE133C7E6FA6551FE2198C275C + 86BD6B81B57708117D236DB0716B00C1268EB119EFDD026E01B840DD087A01BF + 4A9120AF803065790AEB181B0044809F4F1993C3B40289367C71DBF70C165472 + 0F42874CB71D380161015BBED7B0D5E3F52B98258045085C0056F652CC21A405 + ACD05640F0B777D336EBBFF327006FDE1F98749BF5DFFEC36756FFB9A7805E3C + 7D0175A4BC3E0018900C88DBF387AF1DC4ED2674EFCF5F3A70DECA39027F7F22 + 447486B800BF1D05B4216035F58280F4A39D7C0487F4D531E4F90A1C557861F7 + 3835012DA9F7368E475B91CA80DEED788B96FE799FDF6F125ECBBC867B940255 + ED5CF1467EF15FFF98F6FFA395FFEECBB417086CDA4BC8E73F662F63BC5BF1F8 + DD0384B4B1D4F3FA40529D8BE02A529BBC7C01BD82D031CDA1AA01C600E4BC37 + AE8FE118A6FD568F3738A19E159ADB412F634CF2D0E6818183F6793924B5F3E4 + 3D0BE4C6CF4579B523DE9FEAE318F2BC173C8BF94EFC77415BF98E28F37AFA0E + 5587F83E117511F46FFBF1D3EC2D6CDFCF5687BFF93DECB43CBF311E2ED8CA6F + D17E7FBBF14CB7DFD9CE028181C93B7EE43769BFB7EF5B0F3BE4DFFB5DFFFDEB + 6188F8DBE3BF81A8E79CE24314EC1FBCE11BFB7F7EF5BEF5B1FFDF77C0E0FC1F + C4539FFF5EEBC18CFC90863C7E09074D68770FEFEEFF731E1CB171F6DF76086C + FF79C06E5D82BA2A47182CEFDF95769D102CAEFAFB8326799F5FF730A61D206C + 7D81BE4B3E78C3B5C2F26BB81655D7BAB7D3CADB6F3904465C1FB956AEBE6DF5 + 767D45C05C5260ADF6E95D0AF8B572AC937C0C6301C2F42135713DE7BAAE6B3A + 75F4F53D7EDFBD6E63E99F81EF92375E494B2D658F5EB4D8CA0BFD7E63C2BBF7 + B52B693630F755BB175D2734B4F53139E8BDC6034A19FACEBE6AF73BD3DC57B8 + A711E9C2EE51D75F4973AE5C48B32ED93DEEB2DDB7AE5C4A8BAF5D4D0B2E5FF4 + 50CF78FBBAE7AFA5005FC92130F0F7ECB90AFC3AFC2D20D881EF895396B227FF + 3987C11908E7F669274FA4A9C7ECDE7F2C7BF8CE3C7EC20504E621B2176C1E31 + 8BFEA74EA469C70EA70E9B27CC3C7E3A751C3D59416079F612DA99D4F7EC2DA1 + 9D49E5EDAB3A87BC050053C771E69E3E9F669D3853815FE0EEB85DFBDCB317D8 + 3B19506CC2FB973A8F96521EA21BB3634F0580C7D99C0BE0DB19004671CF5F60 + AFC0EF9812FA7994C4438026F702B6B922F097FD7EC7E0110C18B63924FBF7FA + 8384E44D0F05C0C05D9BB30AF8C6396CAC53BDA4F65866AE2BD8ABF931A28C2A + F8CBC394CCB917D8DC1CF0EB7BFEE63A00F0B0C53667B6F9B9C2380BF85610D8 + 52F7E29D656B840280FB762C70D0AB547BFF3A002E3018F0CB18DF5E2678FB92 + 2ADF16CED9D21CC16891472DF2E845B65E79DAD62E5A0FB1E76F3FE56BD2FA8A + 7597D6504FD1C6BACACA6D70D7D4CFCA82BCA85AC7591E8F605FC7216D0B5413 + 6B4507C0E359674EB3BAE9AEBAC7AFB73B0C66DB225B9FB2C62D6BD708712BF8 + 5B00702FEBC39A96B4A7B5932A8F80BA0E764B7D539B0030A9AFAD47B51ED6D6 + 3A9C3CEBF81EC39FADBC7923BC4582B72AD347AAF78BF057B057A1A0637B6700 + D8ED12C5AEE111D0B05BD05ECAFE503C360E2F633FE18176ABABC15F01E0AE26 + F6ED150026EDF2F4C84AB2CD783D0AF61E54B70321EC4575FB5393E23134A611 + 0053877DCCFA6113C356D6DF3EDB51CFCF4D43C64FCF362793BF9EE06F7F80ED + D004FC8D605770378784C6CE96FB3701604F1B007004C1B20FC67C678A76C9BA + 7D526D7F2EC5D76A7B3DC1E1D0B7D566EA39E8170360A06E05768BD4476DB1BD + 7EACA8EAF58B629BC677768CBF36008C9AF881F4287D1FD4F6E75206B7F7EB89 + AE762EDDEC5CBA5286F904F65307C0DEA7D53FABF42DF94AF0A8267657E77A9D + F1BDD8A749F563C4BE4DE3D4274A2194DB58596B4C97EEC8DE33DF15BFAB27ED + F7EEFF41FB4FC213AC9E071EF07CEF0E24EE5EDEF7C354BD7E431B0A6D0E78D5 + BF333581D7C791606F84BEF208564ADB2F81C22D005C207023F80DAABFB9F8C1 + FC673D5411FCB67ED4E18FF9A8E24FAD8B872E581C0795FABFA4FCCFC69F90F2 + 43CE41E75B57535FD4D4F771156F644D6ABAB97526FAB76EB2E49B8FD1290026 + FFA82A63AA63EA18124F7F9527C09AC6B5A9D6EE938F86C949A7139A07F4F935 + F26372C1B6BC267F7500EC80D6525407B731AF72BDAEDE16A57A9E7464A2FBA8 + D2645B6526D431AFB2FAA87FBDACC97A9CB0D7DB1F047F11ED80E526001CC16F + 4E7378E72600AC850E8A8097859116485A349117FC8DF511F892CAEB57635C61 + 411617755102BD9D01607F7A382C26EB00576DB15D7DA47A3FD5B980BB05FCEA + 2969D23A00EEC513D6639F6F03C03CA9EDB0B77801B701605B58390006D28645 + 3DA9003065DF1798FA621010E4AD0C044155FD73B32BF8DBAF405E52CA324EB8 + 11A200DECA286165B567E80B14B63C7BFE4E27B5F294D99E027D81BFAAC7EB17 + 030A632300D6B100C01E2ABA1860A22146861901600F0BCDFEC125FCB340B087 + 7DB632FB023FADBD81A90F00B802C1A48498D3DEC0C5F0E4C627CA0500BBE189 + 3A0C5584B32B72D85B24F8EB462E8C5D26794560108BA1F3A81FB66C437A66B9 + F52B9EBF02C0E4872C5BE779BC38460280576D4C63D66C496337640FDF51EBB7 + A6D1EB29E7FD803DF43375EBACCEF2180AC73AE8CD20983C10585EC00A374898 + C1E7F6BE9C5ED8DFD2E43D2FA689182A77EE49A3ADFFE86D3BD298ED3B1DFA02 + 7F01BF02C2847E66DF5F54F7024682C0F2007EF66001C2567EC1CACF1D243D94 + A61EC243277BFF0A00037D81C0780353E7E1A101C0470F79F867A490D0EC0FEC + C0F7D4099700306018F88BE69F3B9BE69DC7EBE8649A77C1F217CFA439674FA6 + 4597CEA7057835B117F0790CCF2752C7E96369E6B91369FE95736911212EF1FE + 3D7FD274C23D83E75E395B01E17957CFA505AF5EF67D10E7BD72C1CA17127B23 + CE2F7B237A9D09233CE13809CB49F868C0F1BCD72EB6435D9300307017454F60 + 816052FA0A0057DEC3375EF310A10003602FC040201809045327AF34EA689B87 + 37B28D25D4E8D62F3E4A1B3E79DFFABC9916DAB108370D005E0704F9E886C35F + 95B3B7EF2DAFA30CF0051493DFF4F9075E5E1B60EF0A1B2FB01BBD7C9160B040 + 2F79F6021600961771E51108C829F0660350F74B3C06ADBD08C004D002B89207 + 32510FC8A59E90D184A55DEF61693FB4D4DA4DC02B42D5E2CD88372421A20155 + 88710EC9EC78784EEEF8E933D7AE9FEF394003FACADB570078F7CF5FA65D96AA + 2E7B06DFCB30CE441E48B7F11B3BAFEFF1C22CDEC71CABC041C136E0A080AF24 + 38479EBEB40BDC21CA40430F13FD77792F5B014CC14AC149F2F272C58B94BA0C + 315BDEC079FC576937FBCDFE5E0038C34BBC57C9EFFF876FD37EC23EFF116049 + DB57A6AF33CCE61876AC0C6473E8E59D40CBB207B0A473140446DAF7377A000B + 00F3DE72486A80369EBF3F79CA18E0F41E3BFE5EEB7BE0EFCA1EBFA6FD26F664 + 069A5216040610BFFCAF7FF2F7C4FBD36724C5CFC98176F81CA5582F184C59DF + 111254E5F3E0B500F582BBFA5C23D447AAD3F85D7FB0B6DF5B9B7D17BBFF609F + 05DF09F037789923F2D4037C01BCFE40833F8C907F97FCAE73A8F34F5C846FDE + E85EBD3C189143A4F37BD239039FC9BB57B03F300134FE386DF8C6FE7FF63FDA + F4DD9DFC1F2AFF9DF86086FE8BD44710ECFF5FE06C78B083FFB73FE051C02EF0 + D6816E01BEF2EC5D71F7A6A7F2EEA50DF81B01B0F23EC6AE3F78F80281792D81 + 5FE4E19EAD0DADE08117BB6EF9F5CFE4619F81BDC51378A95D1FB946B27F3B60 + 1690BB1A6F5EBBB65256DD4A2BA3E58060131EBC5E57FA686F5EC23B93E71ACD + F59A90CF3CA0C3759D6B37F038F7CDE3D9128010CFDC6FFC3EC3C33C78FD027E + 5FBBE40F27CDC3BBB7005E3C7CE5E53BAFF2FCE501257B1DAB9B71C9EE93764F + 04FECEBECC034EAFA49537DFB4635D4DB3AFD8BDCB34CFEE9B8BAED93DE3EA25 + 07C08479C6EB77C619A2699C760F61FAB0E7EFEC73E7DBA0AF84976FF6003EED + 009874CAB11369EA71BC7FE97F364D3B7E32EFF77BE4A84360C23F0382057F09 + 053DEB948DB77B3C0078FAF116E88D0018BD80F0F23D983D783D6FA9C23CCBBB + 17AF60BC8329533FEFCC85B4FE4DBB6FBDFA861F1BC88B67AF423D037BF10066 + 0C0FCD8DD9BEDB433F67087CD0433F8FB3B9D6C45DFBFDA1BB26008CB7AFEFF7 + BBB915EE794C01BE510E7E7DFE87ACAEC05EB61419BBDEC6B2DFEF3A9BF79980 + BDC357B51E3274004C6AE5E8DD2BC56D4C90F29ACF76A6C63E36C705F0027A15 + 1DA70900FB9CBACCB9F1FA75CFDF320FAF22F150B760451ABA807ADA5B10982D + 6108E72C903B60EEB2F4349182ACFE19EB5BC16113C05700B8EA6FF5034DAC2B + B4D6201510AE03E0A7C2BA45D203ACAC6350BFB29E8AD2FA09C535160058219D + 01C182C15205852D5544277FD097B4ACE93C0A54591F4A5A1FB2DD109057FBFB + 2280AF47A22AA28C4730E19EB556D51A568096B5AC8784669D6BF51ED58AB4A8 + 5A0BD3AF568EF2B5735863BB77B0C9C1306BEE11F7AFBFBD9FBC80875B5B01C1 + 5102BA52651B0875D811A458D739006ED926A2649F903D03F89BA1EF38DF0FB8 + C720AB77A03BDAEAC67884B4EE43DA01B04241577B020BF03E3D323DF1D4884E + 01306DD15E844D47F69F28DA63BF0749C7D098FBD417A78A673C4FBF9E805ED5 + 95E333D66D4C9617D4ED0E00F63D7EDBC1AEE06E6700986DDDF2D66E0504D700 + B054D9001FC3CE18ED924DE3D4FEE750A7AF03FC45B12E8AB6BA8D362AD86BB1 + EF467BB5ECCFB12EB6B1DF6FECC3F8CED4F45A523C6EACAFDA1F11004745BBFC + 6FAD267E10F54BC6FCC554D84E3B006EE505772BC8EBD0B75D798CE581C34555 + 9DD42D83D4CC9E6A8CAACEF73A637C0F6A93D45EEFDB59B933A95F93BA667EE6 + BF2DFB4F11EED94341DBF7DADDCABDEC5AD6B3F7D0D4BD87D53B1F6B3846FD73 + 78D86BC7F647511378FDB5AA03E07A7D535B93FE3300FEDF4682BF1E8B9C1F66 + FC833E8AF853970B875FC8C8FB0FDCA40BCA5F509C832EEEF5738817593FD787 + A83EF6B750BC9135A9E9E61615FBE8C61AD5741C85E6A86EB0A4CAD7F5A07A53 + 755C1D43EA0C004BB16FADFEB702C08FF2845E5D9AE435D533097CA2CF309F8C + D601AE5487B79DB5D5A5B6D847F93F270046AA63724DBDE0ADC6A8AFF2B4B967 + 6F91FAEB782AABFD51007016003843E0A8087FD1A3006095EB8B292DA8623E8E + 791C001C812F69DBC2312C26E302B35A94967C6C531DD2A2B669C1CB4299D056 + A4F1E9695281E1DE6372F8E71ED5C29BA7AF9B01702E078FE0027D9100702CBB + 073010B818040479652088720302F9E73200C61021830529F5B4CB5821E386CA + F42175E385D57BF90580EF1C0FFB1C01F0A09984759E9FFA4F9DED754F4FC3E0 + 910D254F97A7E9517FABE35800607FAADEF47401C182BF32CCB4E0AFFAE0199C + 01305EC1F70160CA9D00E0A1A66700C0F35639ECAD00F042F6215BE56D00E221 + E417ADF1146F057902CBD055815F937B3C14C39942DFD14F46B6682C1BBACCC6 + 2E5FEFC017C9F317831DCA46BC2D69F45ACB7BDDA63482FCC6ED69DC460C7ED9 + 1028088CF7EFB84D3BD238BC48B6EE48A34A2868F7062EE1A0C76EB1BE05FE12 + FA19CF5F44E867BC7F81BFE3B7EF72C80BF845E401BF02C0E3760280F7A48984 + 7F2EF017080CDC05FA2279FB5287C83F4BFB81171DFEBEF0226113319C1E48CF + 1F7C294D7DA9058123007EEEC51C22DACB26C1DF170EBF9C9E7BD95EC7040416 + F405067714202CCF6042450380671538BCEC952B69F51BAFA6E5D72EA715AF5E + 750FE01967AC3FE1A0CF6600ECFB055BDD9C0BA7D2DC8BA7BD7EFAE9A369B695 + D913780190F72AE130CFA53926202FD097340260F2734C1100CF614F618E51E0 + 2EE9BCEB36D634FFF54B0E7601BAC05D8161F28850D0DA27B892F54582BD788D + 09002B5538517908AB9FBC866993C7D972C23EDF2EFB070335F06E23D4E9EDB7 + 5C82BD1B00BB00DD3B6F574018F80BF8DDF8D907DEBEF1B3F7131EC082BDCB6D + AC203002FA027581BDD13B585038C2DF963214121802FCB22FB0878C6D50065A + 77EF03C26BEF0188EC3DDC7B37ADFBCACEF51B7B2DD3A6EF3E4C9BBFFF30C3AB + EFCA9EC09602630911BCE347005A86C81C0B789643E916600BF4FB09AFCA1C36 + 7A5B01BA9E2F9098BE8873F37D80BFF9D8CE15AF642B1700473FF7D234454828 + 1828481815BD3169571F878600DA3F65A09BBD695B3018012709F1BC19706C63 + A81378750F53ABA30F295073F71FF166CD7092F0CAFB09DB5C60E5EE3F027D09 + 3F6DF9DFDFB37EC0EB0C2E39170400DE67E7B0170F683BCEB6E261CCB1B75B3B + 2965C153857FDE6112E895C72F109830D23ED6EA81BE02C0C0610160C261A3DD + F67E80BD1900E73C5EBF780303ED495FFA573FFAE7ED9FA1F5E11CF4F951E673 + E0F352586C9DBBFA50A64D63918F29609EEF90CF4ADF13A2CC67A3EF58B057DF + B7803EF5D491DFF133DF713E1E7B4FE3FD4BF8701E2A20ECB3435F0F356EF91F + EDFBFBDEBE63FBBD112E9DDF64DE8FFAB30AC60A026FFEEED3B4C9F21BEDB7CE + 7F40BF2B3D58A0730600AF67EC37FC7EF9DF0431CEDA047815DE196F5FEDFFEB + FF534BDD7BDFC47F5A0F77284FA8770F076D02E4027B4991E0EFB28FEC1A5540 + AF2030FD9120316DF471706C29619E05804901BED479F8E78FDE4E2B01C1C5C3 + D7231B708D020097F0CF08084C1F0030605600D8E1AC9581BBE4A9272FC82B08 + 4C9DEA631B00986B30D764EDFDAB3DDE2B485C5E63897BFCE6FBCB021EE6611F + FAD7F0DCB5BCA50B4CF2F015F84584779600C00BDFB896665D39EF9A73F5A27B + FC2E7EFD5A5AF5EE5B3EAEE3C29934EB62F0F8BD743ECD387BCAEE8DDC1FED7E + 7BF6649A7D8EA81AECAF6FED172EA439E7CF5BDDB9FB2030F0771A40D761EF39 + F70406002360F0CC5367D3F46319F60280A71D3DE6C0773A1EC0A4C78E7B7DC7 + C99369E9B5AB69E9D5CB3E4F00ECB2AF2FF05700581EBEAE032F3B041600C69B + 17D0AB3D7E6923AF7A3C8A975EBE96165DB8E2E3057F81BE78FD8EDFC59618FB + 1DF88EDDB1278DDAC6DC69B7E72758DB789B6BE5682BADFD7F7D8FDF6D79EF5F + 14BD7EDBBC7D79E8CFD20A005B79A4CDF946AD27428CCDEBD6017F0B0026B539 + A30030734907C06BCAB6228482665FDFE0F5CB7C14697EAA39AAEAE27CB59AB7 + 32E72D8A759AEF0E262D9171047BC953A77AD71213DBAE2C5C9386A105E47318 + 68AF3731271F30C7E6F07397D95CDCE6E8805FA02E1EBD45D53EBE405D60AEB5 + 8FB6D79CC46763EF8172DFE9F353EF29B67EB1D4FBB17E98B9C8F7FD1D64EB84 + E801ACB5864439C35FD482BEAC63B40E8A30B85F594F69CD84047D91D66582BC + 5A9B45D81BF34AABF55BD9EA072F606F0F0058E097B4F778BC7FA7FB3A330260 + F2AC41792019F84B886879F52AECB3F6FE1500C67BD7A12F6B5A5BE37A4A5D6D + 2DDC599DC49A3AAEB11F1700F7A47D78F60216FC658CDB010AD48DB6826ECF64 + DB413754E06FD7A12D9B8254B50D195FD91C648B906D42926DA20B360D130FC4 + F71E3A313D39D4CE6990F51F3836F5B2F1BDEDB83D3886D575050A17F82B00FC + 8434287BF6BA076D81BEF2068EF618CA5D00C0FD87B7D97564EB915DA89E7F90 + EAE32BDB1302F29A14DA59F6A6AEA66E56275B18D0D7F3D687FD7F81BA80DBEE + 01EC36C9E1EF5F18003FAAA20DF3B7D22F3EFE9F110047A9BEB221D78ED7F45A + F53E4DE3BDFD3F03E0DF4E85EDB4806F5D76BEF741E0FE6D5C28F7B37C0D0057 + F5F4EB169D0F6B6CAACEF77E0BC6573F4E28E7F0CFE44BBBDA4CD47B1BAA8FF5 + 3A52ABB3F7D8B5FB80D48DDF17BF4BFB2CF95E7BDA35A44FBF11A9F793764D8A + 0098F71DDF7BFD73687B8D50DFD4FE286A02AF8FABE805FCC413BD4AF97EF8FB + AB0030219E9BA06F25DE50C307D0F42135E971FBFF3354FD4FE77F44FD311F47 + F687AE2E1CE4FDC75DA4FABFA0F8C3E9E2DED416F30F537DEC6FA37CC3FAA58A + 37C0FA0DB6B39B6C05801F4536B6B11ED58F5BEA5CBF14009BFE9C00B8A95F94 + 26794DF53E09648F1026A44C4C6D325A87C1B1ADAE87B5C98B38D679BDE53519 + EE4C9A4493D7643BB6D5F39A84234DBEA9A74C5BEC1BC7ABFD7100B0F709E037 + AA1D006B8F605BE80C440A6BD482BF843FF2C590D5470F5FA5CAC77294DAA2A8 + F7904A6131A6859A16766D8BB79AD7AF2F14B5C0B4BC60705C4C761F91EB54A6 + 2D2E54057ADBF6FF0DE5D897C5B1F63862C18C546E03C036BEF5D4F594D48B45 + F6B8E9B6B0CEB017017E51AF3184E362419EA1B0806F94E0AF00B0EF055C24C8 + 1B8D0B9501A1B447D02B0900FB58938C1CF5F63623C70B5676E86BFDA7CEA9C2 + 400FEC205C1AB098F6595EDF6F0A651B6BED1902037F01C700E0BC57F0A09918 + 5E30C42C28925166611A342B87829671060F60857E8E7B023B0836791868531D + 00530600E3FDEB65244F043742AD4A438B17C2D045ABD3338BB351CB017081BA + 6D619F8BA897E14C06312463998C6B32AC0D59BE3E0D5EB62E0D5AB2C6A1EFB4 + BD8752C781A369DCC69D69F8EA4D6EC4734FE0B59BD3F0951BACBC398DC5C317 + 63A0D5510FF8D51EC0D48FDB64A969F4862D69E486CD9517F0043C8081C00500 + B3E7AFF6FECD4649F6A7638FE05D69FC0EBC58B2D72FA017E08B08093D6EE74E + CBEFF254005810387A049312221AD10FD8FB1C00F8450CB0EC334CA8E95C4F1D + 00F8F997B3C72F865C40F0E483D6563C80A99B6629F077F24B07ADEF4BEE050C + F445F2080610E31104EC9D762CB70183B51730A1A0175EBA9096BD7239ADB8FE + 8A7BFE4E396E9F3B9EBF678E3BFC4594679D030013BAF2649A7AF2709A71EE64 + 9AEFF0F77C9A77E57C9A7DE96C9A75E9B4E98C836009F0EB30F895F3A60B69FE + 6BD93B8B7436A1A2AD1ED80BFC9DFBDA8534FBDAB934EBDA59CF037CE5E90B14 + 9E7BDDEADECE1EBFEEED5BBC7E0580179A16D818015F415FE0AE423EAB8C00BD + EA2710DCD68F63BF6B633F20F4331EBF6F7B7EF9FBAF7BFDCADB6F7AFD9A8FDE + F1FA65EF5DF73CA2EFC6CF00BFEF799FF5C0954F6EA56577DE4E4B01C81FDF74 + 004C19F80BDC45D4D14E4A1FDA0480115EC1487B06036C1010D83D7FAD0F2166 + 09130B58CA9EC039FC2B2008C814415406437807BE9B567E6AE7F2F98DB419F8 + FBCD8769EB0F1FA76D3FDE4D1BF00CFEDA5EDB8EC53EA67838027881BBD9DB11 + C00BD0050C7F99F30EE40AACFBE1D3449869FA02C9E8CF58FAE790BB40622030 + A1773F710F60A01DB08E76DF8FF64FC0D12F2BA04B9ED7F0E3580A289428D3CF + 5FCB529D0BF23E015C3A54B5630BFED2462A68491F447861DAD86376931DD381 + A7D57B08677BCFECA90BFC15A8E475DCDBF8EFD09769EF9FBEB47EBCFE670E80 + 698B1098FC1E00F0EFEDB5794D3BBE7B1E733E96223F07934356EBE37B25037F + 8B76DAF89DBFB7D70606935ADDAE3FE20DFC8D8D0118DBEB15EF63F7B2B573DE + 47DEEAF702A57F4F38EE2F13FB02E30DEC61BBF9ECED7D397CE53CAC5DE233E4 + 5CB622CE99732A750E7B4BBFF85952E673A48ECF91EFA60ACB6DE7C2770B6005 + ACF2B9709EFA9CF84C69E73B9528730CFA137699FD7649B7FF0E280BA0B5DF26 + 0F1DF09081FD96D0F69FECBD98B6FF60BF81EFAC1FB2DFDFB61FEC7763697E08 + 21FF37F82DFA8308366EB3FD6E377DFFB17BC20B04EB37C6EB93F77D7FF9BF7C + 83E73C0F500084ED385F7FD8067FF1FAE5FFB9FA333C7573594018202BAF5F87 + BEE5FF4D7EE527005F2BDB7F19782B2F5EE5110078B95D7322049607F02AAE41 + D6BED4DA81C52B6D2C29C057A0190F602030DEBF4BEFBC9596DC79D341F062BB + BEE1E11BA12F0FC1786AD72B449B3C8023ECCD30978776B2772FA9C0B003DC00 + 7FE9BB18C00BEC2D1EBF7A2807F85BC9F78EE77E60D77F975DAF49DFB2B16F58 + 3BF799572FA6F9449E78952D082EA7C56F5EF3FD7E057DF1FAC50B585EBFD451 + 665FE0F996E6F0CFECFB7BDE8E71C5BD80F10C7EFEF4F1D4C13DF1D27987C078 + FD769C61EB84939EF7BD816DCC9C0B768F3B6FF74107C0767FAB01E01926C23B + 038081BFB3CE9E6F0160F6F53D7E2A7598A65B79DAD1BCB72F1EBF530E1FA9C2 + 41BF60791E365B7CF9723AF5D30FE9C4F7DFA58517CEBB072F00B8F2FC2D1EBD + 0AD32C705B01E122A02F6017F08BE4192CEF60CAC05F7906FBF12C650C1EBFF2 + FCA50F61A0298FC1BB3788799640300078D4A6ED1EF6D9217001C075088CE415 + 2C084C2498311B00C0965F9DC1AEEFF52B006C79E0AF0030619FABFD8043F867 + 7902A326D81B15816FAC6B83BF364796BC3F6D944BD49C8178F092B7BA0C81D7 + 7A88E7E18BD7B980BE1100331FF750CF26C15F815FF2126D82C0B40DB7E38FB3 + F70708A61D30DCAF8483F6FD7F6DADC19A02F09B1F266DDFEF577BFDB601E01A + E865FDA37590DA62BDD64C5A374902B7F2F6653DE5DBF698AA351A0FE7967E51 + 796D3733F5B1764244F7377904A8B22674F02B8D27DC3369DEE7B74F09010DF0 + CDE19F3300EE0300B6356AE5E56BFD0580A903D0B2C6F5B52EF9DA5AB95EA7B5 + 73EC83E8A3B5B9D6E3DDA92BA9EAA3B47ED7BA9E30D0BEE62F36016C01F20006 + F27A9DA9277682676C5CE9E7DEBC005CC02E7685625FE84ED431EBC718DAB037 + 0076BDBD26D926A2AD028F5F87BE03C6A65E8326A4DE43ECB5ADACBD817B94FE + 5D00C0835AF037C26007C0034739008E7618D9627A0D1DEF224F1B21A065CF89 + B61ED983EAF907A93EDE6D4F52DF1C0A5AF56EF72A753EC6EABB3CF98CDBC47A + 5276DB5B0BF866EFDFA1D68F7286B9399532247615100CF4750180B1FD3D0600 + A62EDA081F64577C9864977CA877EE9F5B7AFD689BEDC44E2BC9C61C85DDF751 + EBB115CB261DDBEBAFA9F6FA31627DD5FE0B01F06F6BDB6F57E40775D11ECFE3 + 61FDFFA2EAF654EAD2F5410018D93957B2319517B0E5DD5BB8D497727BBDBDD7 + C28F1AE12FA25E6A6A7F989A185FBD2E941F0880BB943D74D5566B7FA23BA9D5 + 1508AECF88F70B08EED17B48EA65D7B19EBD86F86F5300B8ABBD37A43D8695EF + 0A18AFBF06E5A8D8F6A8AA43D75FA20880BB740102B70360B5D721F0C3D402C0 + 8FE0FDEB5F44EDCDA1FA87D420FFC13DE6987F2EF21F5543BE126EECE58FF9A8 + F21F7AB970E41FBED5173DF4C2A6BE4D6DBF42F7BF663E8F5CCFEBF1BAB95FAB + BE36F601E7D6D9B84753EBA6F54B146F84A80E80D527A651F106DB26EBDBA6D0 + 16C7DD779CD0BF6A8BC769929DA7A7F1180F9B0475D21E27554D20B7B3BE4813 + 3C29D633D9EBEEF017317904D20E2B0098F47E381CF311EAC63212448E305963 + 1113619F20939798409749749C48C736AF63028D740C539C8857C733C589B9A4 + 897BACA39FC0AEDA74BCD8568945862D2E108B0C2D3A5C5EDF2AF3246A15CAA8 + 068059DCB0D0A92F94EA8A0BA6A8D816FBFB93B684822E0BBA283DA92B108CBA + 950562A51156CFE271E4B30E87BB3E63632CF53A5B44761B6EC7B2717161C962 + 538BD8A6C5AC140130FD086755ED6764E56E9622D5B180768DB1FE36DE21B0A9 + 377B2F8D63219EC16F2F34666A7AD2D23EE33A2A004C3DA1A0B598D713DEF701 + 60CBF7C580607907B8A8180CEA52F8301927C85721C51853A410D128B691C7C0 + A1A7DD81BF08108C3200C6DBD752C0F0945956CE70D8FB0289A7CF4D4F77CC73 + AFE1A7C94F273F3F0D988130BE6084297DBC1C25F89B6130B017AFE0217348B3 + C83B00C61BD8346876F602962AE85BF24317AC76F0EBA19E4B183AF6FEAD42D1 + 2D5A93062DCE7BFFB679319864DC4218BDDC9086E1C9CA48009814A3591B20A6 + 3F5E0A18AC36EC4C1BAFBE99B6BE7623BDB0F3453FC688559BDDA03772EDA634 + CA0D79DBDCDBC3F7075EBB397B046FC89EBF392C20FB026F49A3D6597F130018 + F8EBFB00EFD8ED7077DCB66C88C4EB37C3DF7D5EE7F0D73471E79E34C18D99BB + D3843DECF58BE7AFB5EFDAD9A6B13B76A471C0E1007AC7EDDEE38A75134C930A + E89526EF3FE830589AF2D2CB69EACBEC0B8CF77086BEE405803B8E1D75009CDB + 0FA6E70FBD94A61E3B9C6610E299B08F84873E72C8E1302930781AFB0417100C + 109E753A036041E239674FA77997CEA519678FA7A927ACFFC92305FE1E4B33AD + 0E30ECDEBFE74EBA37307B03CFB6F23C00EFE5B3EE0D3CCBCAB32D3FC3F2D24C + C600842F9F7343FCC2D74A184ECBCF71286C220C74F1F82514F49C6BE7D3EC57 + CE59FE425AF8C615DFE37781B5CD7D9550D1173C4F3ADFC6D0BE9810D06FE590 + D0F3AD8C161598EB02E41680C09E914BAC4DDE63D4017C117DE9470A10A66DF1 + 4D3BCEBBD7D2CADB6FA44D9FE3E5FB765AFEBEB57F703D2DBD95EB57DDC9E077 + 99D5AFB80D1CC14BF82DEFBBEEEE8DB4E1D3773D5D6375AB3FBE919614002C4F + DF251FBC99D6DDBB9D76FDEECBB4E5DBBB152066BF4D203002022B1C74F41806 + D80810016E50EE97C34C0B2E0966019C2867AFDF0C9FDCDBF6DB8FD2FAAF6CCC + E7EF58DD0DEBFBBE6B1310EB1B4246BFEF5EC56B6DECEA7BEF3904160023DDF2 + 03619AEF652FCB1FBF706826382750B719099679FF0C86333806F602810B0C36 + F9DEAC26EA7C9F603C527FFF6505DD32F0CBE02D025E893A5E5F521F07892660 + E4463B8E7BF9729E26014C87C00552D24E9BA0E7263B67C665988957A98DB1F7 + EEF09730C3D61750C9EBECFB7B4219A3BCF72F21A077FDE173079A7BDD1397F7 + 948135301790BCE377F7F27BB3D700B247F8A974EB4FF9BDF3DA849E66EFE43D + 843CE6BCF9DCEC7BA01EA0CCDECA3B1C007FE9DEC43B01BD7FB0367B6D0030B0 + 77B71D73C7EF3E2BF0B715FE19F1DDF03DB15F2FFB14F3BE910370C4E7CFE75D + 24F8CBE757C1DFD0DF3F37DA4DF4E73D29E433E7C477A4DF49F57DD1D7C6C5DF + 139F5114BF41F6AD1674DDF613BF3F0073FEFE81B7EBF18EF7870BEC33366DFB + BEA5ADDF7E9E367F63C7F8D65EE3FBCF1CE4F27B5FC743155FE3F96EBF15BCE7 + FD37FF61B5673629E1D3F10E063C6FF9813A1E94B0FFD0D7F65FFBF6B6BD6EF6 + F6D5FF0E804B5ED0D7FF9F78EA0279EDBFCBFEBB82BE80582428BBD2441F3FB7 + 028101C082BFABACBCE2939B0E801170579EC10E7CAD8F03E00FDFF63C750E8C + ADCC6BF09AABAD3F79B4C2AE5DCBAC6D995DBF16F1E0CB2DBBFEE109CC432C80 + 60D372BB86F9FEBFD4A1F700B92DCFDEE8CD8BB8565306F8D2EE6DEFE6BE82BF + 8BEC9ACE833E7E4D2FFBB2737D06FC728D667F6007C46F5CB67B01619B915DFB + DFC8619FD90778F1751B7BED622B3A85DD6B16BE7ED5EE1BC05E3C7FEDBE12F6 + FC8D9EC082C2ECFB3BE7B2DD934C7801779C3FED9A7AF6649A6E9A7B31036085 + 7E9600C00B00C71732EC05FC2A04F48C33A7D38CD378F65A4AE8E7E327D2B4E3 + DC9BEDD825FC331078C6C933D6762A4D397CCC35F588DDB30F1F7500FCFCCB87 + D2144BF10A7EEE25F6EA7D292DBE7429BDFCC53DD3E796BF98E12F1EC000E002 + 7F0580490570E515FCFC8B19FE661DCC10D852CA8060CA005DE4A19E0B30E638 + 9409038DB72FC27B9830D1CFEE7F318DDDBE278DDE9C23AE38ECDD9C1FBE1BC7 + DCAA78FF560038005FC15ECAC05E44BE02C20E7F4DEB33EC1D09D0C5B39710CF + BEC7AF8DB132F097A832C36D0EE96DF429F0577350528160E6A802B70E6F0BE0 + 6D82BD43AD1C812FF35C3C7FC97B3DF3659B2B037C075AAAF9B19709CD5CE6D1 + 3E97B6BE406024E81BE5FBFDD680AF04F89528D38F50CF4865A03061A301C90E + 82D90378DAFCFB00B0E0AF0030751518B6B54E670098351429E51CC6B90586D5 + 0769FD447FEA390621A001BBACB5DCAB9775585113FC450E80AD6FFF49ADB59F + D684A8F7C48E0C93BDCC7A717AEA4B4A98E70979AF5F3C7E3D347401BEA402C0 + C8D7AAAC7159ABDA7AB75781C1EEFD6B755A0B57EBE0B266563DA9D6D0483057 + 6B6D1F37FAB9BC6ED65AB9A6EEA5BFD6F478FFF61A6E7D59F303766D4D2F2F60 + 202E80972DA47A5ABED7B0675DE47D6BA92145258F8DA1B237C8E630B8D4157B + 84EA54DF7550CB46E130D952BC7F81C05DFA8F4ADD078E4B4F3E63AFCBB107B4 + EC255D898C06F82DE19E5D83510B02BBDDA5D85C047F95EF39042F636C2ED677 + C0A8CACEE476A3A74665B9AD4869B627757D2AD87CB0E194B63C364B36A4688F + 8A8AB626AFEB3B2C8FB763797B1FE0EDC8D46BA0BD3FB503688B472FA0378782 + 6E87BF5DFA0E494FF419DC02BAD8F11C18973CF63BB7E195F60645BBA06C822A + CBA6A8F2E34AE32B00ABF2AF513CAEA90BC2D61AEA2A45E81B257BAC6CB33511 + F951B667D97BA3BD5976E8D847799523006E6B33B5BD56505BBF5A9BB7FF4200 + DCC62F427F8D7F3CDB7E4B3A6693D4A7E9B5FE5A2488F928CA2C8834AB05795B + 75AD36E519036369F127C16077460C223AAD47A82DED0F55677CAF89F795BA2E + 5D50680FC76804C041790F60DE4B795FC5E399F7D9D53E077E8FDDFC379A3F13 + 3E037F4FF67EBA75EB9FBADB584FBB5AD9CE837CD7AEE57C9ACE1985F37B6435 + 81D75F22202FEA6A0705F2B6CA7DECE4FB567A1C00DCC501B064AFD1087E6BAA + DE1C654B3BF982A2AA1F91C6D6DAFFD604C8959ADAA52EE50F54BDFF92F7A710 + 78E2C03F0F7EB4E58FF988F23F74915F10D446FE6117B63F0700EEA1D7551D20 + 9A3A7B9D1E5C4CF41998C2389D6775BE0F382FBDAFAA6FADEECF2AE2CADBCD3B + DE103BBB3136D5D555DD70AD2F526867DA34DE6FE84C24EAFD952A5F2F2B6FF2 + 894843BED2C326410F68D7A4AA6932F6A0BE75C536267B02C07A6A304F38B372 + 997ADA33C415C8551ED5616F6CEF1400734CDA11FDA9F7C9F8C8FC54240AEDEA + 4359C7A34CCA24DADB191F26EF4893761DA33A96F5A9973536B6C5E3DC27C240 + 17F993A775B501603B8E975994E4BD81BB0F1A6B0B205B28D9C22442DD08749B + EAA2588CF922AB490F80BF02C05AFC01737B08FE967C4F168FDE0648B631369E + 328B4887BE618C16A5F51469714B882A0F835516BFEA27A9BD52195301E09A7A + 1520EC9EBFE3A7A75E08186C02F83E091C2E603887F12AB098B60919040B0667 + F82B23407E3A1C6FE0ACD959C19020981B01B04459860A416149F5281A370801 + 8DFA3CC778406F86C3FD5F98639A6DF9D90E7981C2FDACAE1FEDD3E6A701EE21 + 8C4165BEB5E7321EBF4F11A6ADA428435F8C35F20C6E85699301473018F02B0D + 360D44B30913B7D4532F9B06A1B9188E56B817B00360135078A0D5B39798879E + AB42435B1B462B3C828BE16AF062AB43C5B8E5B27A19CD641C0312BBE1ACE41D + 1087F2C8959B3D1DB77E475A7DEE5A5A7FE9F5F4FCF603DE6FC4CA8D69D46AD3 + 9A0D69ECFACD69F49ACD69045E1D6BB7A4E1EBF1F0DD96466EDC9ED3F55B7378 + C00D96AEDD9446AEB3FE9BB6A5F1DB76A5893B763B0076B8BB13104C7EAFA778 + FE8EDF5EE06FE903281E0310DE616381BA3B76BAC613FE79978D272C347900EF + DE7D691C9EC1780817B967B00983EAC43D8447DCEFF9497B01C007D3E4FD2FA6 + E70F120AFAB01B60DD6BE7650CB278DD1CB0F4607AE1A597ACEE509A76E448EA + 38762C4D3974C8DB08210DF0F570D096122A7A8A09E82B2F6007C15637DB3D7F + 4FA6E9C7B35770C789A3DE4E79C6C96369C629ABC3DBB778FD02813B4E1F7500 + 3CE7C2299703602B037BE75C3AE3107816A0F7DC09CF636807F8769CB7D73191 + 028101C3ECC148084EBC81675DB57E57CE3A0076188C971670D8DAE7123E1A58 + FC3AA1A2D9A71160CCDEC197D262E0AE89F27CF60DB63ED493028101C22EF2A6 + 45780ADF7C2D2D7EE7559740047B47E2E10B545848C8D01B8483CEE145810C82 + 0D8B6FDA98F7AEA7151F5C4FAB3FCADEBF0E7ADFC7C3F87A5AF3E1DB69D56DBC + 8C39A6F5B1FC2A0F13FD765AF709A1A06FA6F54015D3CA0FDF32BD9356DC7927 + 2DBFF3765AF3C9ADB4F2A31B692900F8B3F7D3CEEF3F4F9BBEF8302DBBFD96B7 + AFFC2887890608CB139894B2A0B04322D37A1BBF9E70D3EC0B6C297079EBB71F + 7B78DF2D9666EF5F53491DE6E269F8E9BB0EA180511B005626C0D5DA2FDEB331 + 781992B7315F7E50012F60177989BA0C3DBFF490BB78EDB2A730A1A21DCE7D8F + F7E45D179E93D47BC8681321A1B7E3E96B29F07B8BB5111A3A877DFEDC6130E1 + 790933ADF0BD02CF82C019FEB5A020C02F03D50C0CEBED3B7E2EE186019380C1 + D2E6809A3E5627408997AD434B93C34D2B7B7F5EA71C07600BFC0628E2290AC8 + 76CF5B20EA1FBF4E7BFF04F8CDFBFE22857FDE43C8E5023CF37902C1ED9C01AE + 76FCFC5E72886301D0F89E5AEDF9587BD987D88EEDEFCFBE073C891D0CFFE93B + D3F7D6463B61B3431F13FB32F31DF03D7928E8BFFBCE41B4EFD9CC77CAFB2B00 + 98BD93057201E30EC7CB79704C5241743C97F98C1C1673BE3686CF9C94F7C3FB + E2987C1611F0FA31E8FF00D12F7EB7ADDF827DFE7C873FD86FEB27FAD14EDB67 + 69FF3F7CEF309CEF0808BCE9DB4FD2A66FECBB04F8BAC85BFFEFEC38FCEEECF7 + 093076AF7742AF7F7DD7C701835BBF7FFB1FE13D6F0200E7F7F0A9E56D1CD0D7 + FF4FA4F93FC47F0DF82B009C1FCCB0FF9FA51BBEE0BF9B43B97BDE52F7F8FDD8 + FEEBFCFF81B7F6BF5F5B202DFF65A0F15AFEEF4060AE0FD6E60099874BEEBE9B + 965B7F87BBA6655C7BEC58F4C9F576EDE22111EBC77109F18C5691B7BED49177 + D9B8358CB36B930360BB66017B1DFA16F88B7C2F60DA6E0172F37EECDA9B1DE8 + BB8CEB290FD5587E895D5FB916037A17BC7E252D7EF36A5A66D7E06556B7042F + 5EC435D86130D76FEB6BD76AF2CBDE7D23AD7ACFAE9396FA7807C0847BE67EC1 + 3DE16A5AFAA6BDA669E175BB5F5CBBE4F71A224E70DF99732DDF77B887E47B0E + D12868BFE0F07736A19F2F13B18271D9FB77CE15BB6F5D3A97A69FE361A7B3AE + 8EB3A73CECB303DF2B17DD1BD8013075A6D9E7CEFADEBFC05EEDF3EBE19E4F12 + DAF9649A75D2EE9FA7ACFF09BB577A28E793761F3F919E3F72CCF7FC9D79F28C + 975F3804F435017F5FE63E6FE94B87D3E48378EFDADCC1F253AC1E4DB3B13C24 + 36F5700E012D39E42D0018689BE71BB98F20EEA47D2FB9D7EEA4BD36F7B0FC44 + 1E8C633B8C027F01C1F20C06F04EB6F264DA6D8C877FB6B90D9EBF847B9E6AC7 + 9D71C4DECBFE97DDD377DC16E65536C7DABE27435E20305EBE78FBE2156C790F + 031DE0AEEFEB6B1AB57E9B6B8CE5C76CDCE1F991EB6C7E671ABD8EAD4132ECAD + F6F725CCB3D711EED9E687ECF10B002E1EBE9EAEB87F2B12CAB43BE45DB2B69A + A37A841B93836295ADCDA3DD282DD057735F94017081BF0B6CFEECF3E62CF283 + D8C39770CEA5ACAD5788BA93812F7BFCE67D7EF3C39819DE0273A37CFFDE9217 + 0C1E6222AC737FD615B65E70E1C5CBDA61D6621779A06EDDCB374A75C060DFFF + B7005B01E00882B58EEAEB6BA1AC7E26AD91AA7552595B9156EDF4652CEBAAB2 + C67AD2D651BEDE32C5755B058079C81758CC7AAF067FA9CB6DAC0D5923E2F5CB + 564296B2B61C97433D4B02C0F2F68DF017A99FEAB406D683D05A0FD7E5EBDFB2 + 8696AAB5B2E51D16F31AD4AB8FADCDBDCCBADCFAB176CF6BF62C423D7B88E861 + 19F8223C7D29037AB11B44004C8A1CFA36A86EA3A8DB27BA137DACD2B8C41EC1 + DD24BC87ADAE4749B185D01F50AB87E47BD2869D04DB09F076F0687B9DFBE50F + E2BB6D25DB5C9A6C2F15FC7DBAEC11DC1F5B8FA5845BB6B49BB5751F6075848F + B6B66E4F63E7B163161B9094A12F36A2AC27FA0176B3BD48D0374AF56E536A68 + 03FEF6C05E64C7AEE0B01D2F03DD6C8BC34ED75DD01615FB1C10387BFA0EC975 + FD4D8C79CAC6DAFBAAF6072E63A3EA76C17A195B622C3F8EA23DD2A16DBD4EC2 + E6F930BB685097DE767CD25E45755BAB8E55EABC5D6DA1ECEA516CC4E138A44D + F6E6585707C028F6A98E1B54EFEB75A1BDB33E75B5E02F1AE0AA206B5157ECE7 + AAA30FEC22D8F09BA0ACF28F231D338EAF1FABA9FDAF46859F38D3A949DCC741 + 2F7C05F8E9B033B7937A9E7E850775B56375B56366085AB891EF9B0B9FCA4CA6 + 3309005710F841DC4E5C2F2AD677D2BF05796BED260F01DD491BF2763F57DE17 + E767924734EF0D96543E072FF339F07E786F56EE6629D0B75B01C0C05F07C04D + E72B95F37E2C3582D75F28E06EF7EED06A3B70F1FAA5DCA3076FA60580A5FA78 + 4163C9EB00BF8DB23E0A0BADBA3A0C6E53C307D620FF3135D4FFADA84BF9A148 + 553DEF2BC8E12F7FE89E96F6B43FA695A9E707C8D3073D4C3C7DD0E56100D89F + 6AA89555576FE30F8EE245E5CF267B3DFE7CFE3EED8FD5EB29BB09F15EED9CEC + 3749B96B2FFB6C7AD86FB187950581C331E285D92FC6B5328A37863F979EE8D9 + 5C2FB1B1F813BD2C6FE7F284DDECF434959EA8FAD5AA1D47377444D9270B4A25 + CAB1AE5E36559392A0D8564D62C244A971C2D4899AC6494DFDE3840ED5277F8C + F3B4C0DF3A006E4D3211F961AE08745517DBA867528BC83BA4ED93C74700ACE3 + 688CABE4639B2B1CBB874D94255E837A7904F3646704C075A0AB7CECA37EB14F + 67FDA2220CA6ACBE555D5974E849D4280F45C453AB43F2BE36710F6014C34137 + 2DAE58A891B2088BD0973AF525145367F0175107C4559B2F08B518B445A32F14 + 593C9A68F3FEA66A1159DA7C61C9E273540DE89645297916A3DDA807F216A90D + 5166D1DBD716CFFD4CA494FDE9674BDDEB97C5330BE392D762BAF7585B908FD7 + 5E4DECCDC493D9F2FAEDC861A22D0FEC551F815FD5A16804403C19DE7FF2EC4A + 840B8B4606897064324AC4D4BD7FA301A2182DA4BA51230B43486ECB4FB7F394 + BCF5C7139830CF40E16210A16DC0B4856E44F1A7EB8B3106A34B3D4F9A3D7D97 + 7A4A597B046B3F60F2B40D9D4B2868C2402F4B83E70270B3A780F6098B0624EA + D9576C28C6A7F92BD333F32C0500CF5E969EC67318008CA1AA18A8500C4D8717 + 833C19308439145EB4BA02BB32A611E64EED555DA9AF0C6CC508E77BB9ADDB9E + BD3200BDAB36A4512BD6A431ABD7A6B16BD7A791ABADFF9A8D69E8BA4D69E8FA + 2D69C4965D69A469E8FAAD69F8C6ED69C4A69DA60C84476FDAE1E104276CDB93 + 263AE4DDE57077D2AEBDEE153C6AE3961C1E7AABF52BDEBF13AC0F1204F6318C + B536F610164456FEF9FD184D0FA6F1F4314DDCB52F4DDCBD2F3DBBE7409ABCF7 + A07BC8B0375E3694E67DF3B24196BDF80EA5672B8F9C4369F241BC74F2F1F00A + 9EF6F291D471F478EA38763C4D3B72343DFF928D7BF125F7FEC1E03BE5D091CA + 53F885C3391CB4F60806F24EB1F2CC93C7D26CC2561E2754F4A1D471C28E69F9 + 0E6B233FF3E4D15C77F2489A0104B674DA296B3BCD1EC0D113F84C9A432868C0 + EFF993253DE5295078CEE5B3692675266030F017EF2B3CB110E559F4B97C2611 + 161A2FAC39785999E6BD7AC5EAB2417EC1758CF918F5AFBA919E3CF017D83BF7 + 1A7B095FF27236E25F4C4BDE7A252D7BFB154BF108C633F895440869BCC82479 + 9D119674E9CDEB69C1DBAFA6F984047D0700DC0A0D0D08160C66EFDFA5EFBD96 + 16DDBC9696DC7C352DBF954397E2E5B6DA8EB3FABDB7D2AA5B6F9A28BF99D67C + F0765AF7D10DDF0398FD80012800180FA3EA1E726FA71526A5D24AC0F0076FF9 + EB915FF5A18DFB10CFBCD65EC1E47378E89B0E79D622AB47ECC9B9DECAEB3FC3 + 2BF8DDF4E23F7C9BCEFE877FF0D0BE6B3FB773013AE1CD48886840F0E7596B3F + 273C2C61A26FA74DDF7E98D67F63E502B5805F6BBE783FADFEE23D075E805752 + 3C80A9CF9E8F1F39FC13A8DCF2D327EE2909E4DDF6DD2769C7779FA6ED966E35 + 6DC133D8C4BEAB5BBFB5F66F3F49DBBFFFD4F35BBEF9286DFB3ED7136E7AB3BD + 0E407813AF61DAF63DC7655FDFBBC93D877F649FE17BEE054B3E879806F2E225 + FAA9E7F1FA74599BB75BFD3641C4DF154FD4022E05202595051D2368A48D717E + 1CBC957FFEDACEE90B7B8D4FDD037737E1972D259F3D97F10CCE63F439098222 + 8EC7F902BE796FDB7FCEE097FE3A3F9D83BF66393F8E9161720ED1ACE3029901 + B03B7FB6F467BC79BF4D7BFEF09DE95B3B2F80AF1DF7C77B69E74F763E3FD918 + CBF33D0083775B7F87EDF6BE00C100613C86390F794ECBCBD753CAE57C749E7C + 2FBEFFAE9D2F9F379FBD8371FA93DA7176DB39EBFC79AFF5CF9A32AF19EBF51A + 82E47C1F7A1840722F72DAF90D98F80D93BEF27FF8F7E9D5FFFE3F3A04566867 + E420D81F3EB073B4F7CCC3061BBEC6CB37FFDE36D8EF72DD971F591DE0D8CA5F + 7FE4E3D77D95B5E60BFE2BB7FDB51D78DBF9F290C306FE4B5F67F8CB7F66BD3F + 744158761EACB0BA027F23F095273FFF53E02ED097A8013CE0C17F5C7B8CF33F + E7BFBFEED3F7FDE191B59FBC97D6DCBDE51EC2A46B3FB6BE3C4062FDB9E62CB3 + EB0A79EA56D9F8E576BD59F2815D67EC1AB3CEC6AEB331846F06DE2E7DEFF50C + 736D1C9EBE88B60DF63AA4B4AFE2C117BB16D1CF433EDFB16B92F5A78EFE005F + F66797048095CA0398680C8BDFCAE19AD9B797D0CDAE3729DBF5DBD2A5EFBC66 + 626FDFEB76BDB76B3260F7EDD7D2B21BF63A560F105EC478BB8E23EE1D73ED5E + C21EBE8BDFB2EBB6F59F57423B0380B9EF904A19041361227B035700F8125EC3 + 175C006017FB01B3DFAF49619FD9F317D8BBD0EE5BEC058C27F01CF7FA3D9D66 + 9E2E619E4F678F5FA0EFB41327D2D4E3761F3F7132CD3E657DADCFAC93A7D28C + E3274DA73DDCF3D423D6C7EEF7D32C15FCC503F8391E0CE321B19700C0793FDF + E75F648FDFE35E7EEEE06187B893F61FF487C99E03F012FA99BD7F1DF41EF6F9 + 45F6F605FEE6E33CBB9F7D7F5F4A13F702780FDABCE5609AB407B07B304DD8C5 + 9CA5150E9A3D7E27D9FCE539EBFF1C00D8FA4F209433626EB36BBFCD7DF6A7E7 + ACFE0540F2CEFDE9D91DFBAA7D7D9983797EB3CDB336DB9C8A39996942A9635B + 0F20F0D88D78F696BD7C990BAEB779D6269B876DDC59C0EED6348630CE96CF69 + 0B00FB38A07101C031CCB3E698510E7CCB5C7484B53B1C2EA0577BF74AD44531 + 46FDC8B7CD7F4D4317B7A0EF33567EA6A40E796D4EED5AC0BC3A035EE4C0D7A1 + 6F6E1B3CDFE6F1966630BCD2DB1D0493DAFCBDDAF7170FDF32F699B92B4CD63E + C3D604B64E18CC43A1B64670606B6B8941563F84B9BDAD17B4B620551ED1A6F5 + 4704C092C06F1D00F779C1525BEFA07E656DD4995877017A595BB13603023B0C + B663F4796156EA3D9990D01DE9C9676D3D87C76F512CF77ED6D67FD6277BF9B6 + 8B90CF8477CE5EBE19F40AF80AEA0AECD6CB92EA58ABFA3EBD65DDEAF0D6DA9B + D4B43ED65A1AD1E6EB628E312A0360BC8A7B5BBEF7084B8B872FF0576B7C41DE + 2A0FD81D7A3FD8951DA15EC69E1015DB50DD4651E587583D368EA22E4382C7AE + E5DD83B7B4751B5AFAD266EA69E301C03D2817BB89EC29955D4580977460CE0B + F8229551B53770E99701AFE59F1EE6EA316864EA3E7044EAF2D430B7FDC89623 + 7B9094C12F4E05597809238E97DBB31DC9212F76A9629BAACAD8A94ABBDB98E8 + 1FDA3946B69D0DCF0E1A56D7DDDA483D4F1FF7F4CDD27EBF5DFBF33EEC1876FE + 88F791F70736F56B87BFB2014AD1261815EB9E68B0313E5005EEC6E3DD771C07 + B628D4752207BF353B6B057A29F7CCED38F248D874DDB187FA4E24DBAFF76D68 + AFABA95FB42747C536E5E3B826A97F1C53950300AEECE8D8CB7BB7D485CFD36D + E896B73ECE0D1AECFF519D3102E9411CA133C5310F52D5B7E22AB08EFBFBFD96 + 6A3AC7087FC57A32D00562F633E1C5FA54EA1ED48D3E852BC15BBAF57CDA8447 + 2CC7E2BD645625D5CB52A700B80E381F26C648B1DC599F4AD477D23F8C7347CC + EEF4E1BD709E9677EFE05C0610C39EDADE4B39A6436417795E23D7B7BD9ECA0F + ABEB4C7508FBB812A805E80AF25256DE817077DE0450B7A74BFDBB966354E258 + 01003F61752DE05B97B5BB425D23F895ECCDC60FA613C52FE06F514D00D8DF93 + 4990D753BE273E13CB77B38B607700226DE569839EF667EF6DEAAA8B4C67AA41 + 5E7FBA833A496D96570CF97811F9F328BFDFFC5E2DB5CFA06B4FBBD8F4B28B4D + 81C05D7ADAFBEFCEEF26EF2FDDCDCADD7ADA793A386E1D4B17BE26D59F0CFA73 + 891BD72389BE7EDEA459F166F84B158FC30DB63E0170684B3FD454AEB5559391 + 5A3E96BD4E1399DA84488A939D2655FD34B16AE813455F4DE034C9AB14277B01 + 00A31C620600DC5E5787B64875A4CA03692300AEB7A94EC77898341E31B6A74D + CA511B600ED2E45D1371491378DA9AA4F62635F517E875D86BE5D8EF61001869 + 21E41A9481EF13034CB690100016048E0BABFA622B4AFD7D51668B27815EA909 + 00A358AEA06F69535EC7A0DC068001C6940B008E0B5552EA1C00174F612D7059 + C02ACFC297A7A101BFA82D0416290BE3B298D682BAE768EAADCC82BC40DCE8E5 + 2B904BBE0E7CC9E3F11B0170CCC73E31443461C5080DE6E1C68A3CDC5800BD82 + BF6D00F8F9396EA0C0A891439C6551F6A7D74B9BE480772AE1A15B7B07C73EE4 + 19EF4619007009B93610C34B31C8C80023834C3B006E79FE52462AD3C7FBCDB4 + FAD94BDC2360C8DCE5695080BE0A1727008C063B385E697D573800F6BD83D94B + 180F84E29540DEF70426243430D85207C201EACA1016612F691BF82D72431A06 + 358C6C2BB25746E5356CF52300C02BD6A791CB57A7312B4CABD6A5516B37A691 + 1BB6A767366C4B434DC3B7EC4EC336ED4E83D76E4BC336027FD9576E571ABD19 + CF92BC979C1B1E4D630805BD3D03DDB15B76A4511BB7BAC6587EEC56BC53B6BB + F05AA1CFA49DEC570708B6F1DBD8C38E3D84777A9E31A4184227EF3E907C6FBB + 1DFBAA3D85B5BF30C653E02FE113C7594AB844ED9D97BD6F72084594E1306115 + C9B32FF04B69EAA1C31EF2F1859731EC027B5FF6B2EA5E78F9C5F4FC8BA6975F + AA423BB3E72F7980F08C13C7D3AC93C7D3F46384847E39CD3C75D484B7D0E1D4 + E15EC0476D8C8DB3B68E134061EB77C2CAA77238E8BC0FF0C9349F70CEA6D917 + 4EBBD72FD077D6C553690630F8FCA90C814D734DF3AE9EF73D80058191A0F06C + 3CB1ACEC1E5657AF982EA739A6D957105EBFD9936BF19BAFBA613F03612031FB + 36B67B00BBF7D83BAFA6A5EC0FFCA695DFBA544070DE43180180F128F330A437 + AEE730A0AFB397241EBFC0DFEB69FE9B5667C75298D1C5400A20C6FB6FBAE7DB + 623BCECA5BAFA7B5776EA435B7DF4ECBAD7DF93BAFA535EFBD95D6DDBE91D602 + 7FAD6DD3DD5B6903400660FBC19B69D10776FCDBAFBB07DDAA0AF4BEE9C75D61 + 639659BAD85E072D7DEF0D87BF6B3E7E37AD2E200768E3219D3F2D9E7DD68E87 + 1E7D107068B5F55BFB097AC775E04F5FA553FFC51FD3EEDF7D9ED6DEBB95D699 + 367E0568BA93F2DEC1848C26FF61C28BD0F707FEFA4E5A6F7DF056C4F357E535 + F7DE4B78F5027B81BF848146402EF64415A0DBFCC3DDB4E13BAB03A201D94C5B + BEFE386D03FC3AE4CD0018E1D9BB91F0D376DC4D5F7F68F90FAC0F65CB7F7DDB + EA6EA7ADDF7D64E53BDE0E1C651F56ED0D8CE415ACD4F769B57E9B0BBCABCADF + 910285190FC8250CF0270E2077FC9C21ABC0AC03C60234051F0517818D0291B4 + E5FA4FD20EA0EA4F5FDA31EF598AF76D86AF80501D57C7E4B574DCAAFC3BC20E + E7F375486D6581618DA33F60546394D20F88DAAA031E67B80C84251CF4CE1FBF + 4ABB7FFE36EDFADD3769C70FF61EBFBF97765A0AFCDD6DE78E5A20D85ED3CE03 + 0F6C42481FF8FBEFFD58BC369EBCA882E7E1F3D27B7210FAE367698709680F58 + CD1EDF7875E77AF66DDE61E7C618BD07C6EA3DEA73D6F7411DC7551FBE3BF553 + 5FDA37D8EF84FD7B01BAFC06F84D90E7F771EDBFFB0FE9CDFFE97F970EFCC30F + 0E70F1EE05E6D2EEB2F3031E6F74C0CB7178A00020FC69DAF0951DDBB4FEAB8F + ED7F62FF19FB6F3844C61BD88E439ED7E63FB2C9FE230060B4D17ECBFC87903F + 4CF179F6ACDF485F13FF91FC3046DEDF9790CB40E00880FDC10EFBBF920268F1 + B2E5FFBEFE93F74DEF39C05D6BD71ABF16D83580EB000F90F05009D715AE35C8 + 1F2A29F2874EAC8EEB8C8FB7B1B4D37F895DDF2A005CBC7BB98EADC3C3D8FA50 + A7E3B9C72F80B95C1F19BBD45240AF3F20C36B585BF40246EE05ECFBF55ABF77 + AD1FDEBC764DF6BD7B81BF26EDC9BBE4CD0C80170380F1E8B5FA85AF17B88B80 + C2C05E6BD3DEBE1ED6D9EE1D40E005AF5DF53D7CF1E89DCD9EF56C4160F70EB6 + 1998C19EF53C88C4FDC4EAE7DBD8D9972FA49917EDFE64FD057F81C13380BA26 + BC7CB5EF2FB0B7E3CC8934EBEC2987BF1EEED9F2736DFCBC8BF67AE7CEB9C7AF + 877B3E637D09F77C22877B26EC33E0773661A0F1FE3D7ABC02C078FDA2A9878F + 3BFC7D1EEF5E4B01BA93F1DE7D09E87BCC4598E5E9474EA4292F1FF37A80F014 + 07BBEC0BCC561100646B7BF9687EB82C08208C00C082C0CFEEB3FCDE971CFEA2 + B1DBF7A631405BE02E9EBCDB899CB2CFE73AADAD33F6E6B98ED53F0B2CB6D4CB + D6973917D057E19C058001BD00DF894061D378CB8F23ECB3896D3C5C78FC1600 + 4C5498091BADDF069B9F95FD7BC7ACD996C65A1B292018F8EB7BFF16701CE12F + 219F058025CA5E0704662E6A735340EE48EA5557E6A782BD4D73D80702E0027B + 91036044B940DFBAE4E5EB5EBCA18E94B1A402C00A091D01B0E6F303AD6EA8B5 + 0F9D6373785B3B0CB4B5C210D60233AC1FEB8AE90BD3602B3B202E6B0CD61B31 + F4731B00661D5303C075F5B3B50CF0D705000EEB9CB8EE897531AA92AFAD8A88 + A0D4E779C15D5BD759DA6732D037AB2A3FC73ACED67AF431F5F130CFA6920700 + F70200B3164535F02B017A1DEA9632F9087FC9FB1EBDAC8B693345E01B15D7C4 + 7509FA22FAFA1A7994F5B7B537E0977CEF91010097F5BAD6F152CFE0D5AB5420 + B70E7D63BE494DED6DF6891A0016E095220046945177130FC94BB29944F88B22 + E08D8A7616445D04C0DD7CBFE051A9BBA540DF6E03867B8A1C080FB07CB0EBD4 + D504813B05C035FB1475557B29BBFA67DB1572FB593F4BADBD5B5FD276C9EE95 + ED6343DD2BB8879FB38D0700F71F6AC71E6AEF374360DADBEC7F266C82D12E18 + 61AFA43EE4EBF6C587AA13001C8FD518C2B98C6BAB330178DDAEDA63A0835EC9 + EDAC2601DFBAA8F7F64ED4D6B7A1BDAECEFA451B73B42FD7DBE298BA623FF56D + 2B57DEBFD89F8B9DFC6100B81B20EEC1B6FF87318287B537298EE94C555F8157 + 04386DE8FB203D2E87E9F41C79EDC277481D66763375E99BBA5B8A1361BB325F + C2C3556C0AEE02FC744E63A98350D854E157EA17E5F5A4A8096C3EAACAF13B2D + C7BAA02AC26EEC17A5BE311FA4F7A1F75BBD6FBD3757C3F1109F8BEAD5562F57 + 7D437D93DA00EC2F9000308A5EBD805F79FAF6ECD53FF500B0757FD2D43B75EB + 56BC804BDF6A7C91CA6D70F7418AA03796DBDAEDCDC60FE69FB13C6678115F32 + 4F6450EF3F2E7E78F643F310D940764BBBF51A907A3C39C82170B7EE3CA5D12F + F5B03F760F9E50D045E621D2D3207E618917274900B87E01F9CD65AF15DE2700 + 38C763CF7F367F8FBDB8A0017CED33EA69BFD96EBDED33B2CFC9613163F2854E + 17BDCEF49780BF521BE8ED4484C8C89F81958B1EE5265957673757146FFEBAB1 + 47B8DBA9E853FA3181F1C9086991FA55E5DA64466A9A1475A6AA9F2648619214 + 8F131527757132D7F5C930D92B90B792F5E189420F2FE3020003845B50364E40 + 9517A095D42ED8DBD416F375D58FA1E33339C6FB57EDFEF4A4E5E3A45A13E9D6 + 84BD954711D46AA22FD5C7C6FE511AABF1F1389E2F0B96A605098B0FC216B197 + 0D0BA16EB6F8C98BA7096DE0B70E8055AEABDEEE8BB21AFC451100A3B850547B + BD2FF9D82FC261B57998A9027EF5D4328BD6588EF571212BC5705871D14BD901 + 709116D559B6E066AFDFE2E52B017315D259650160E5D54E3982E3780CDAA380 + BFBD2676A49E45BD081506E82D203887172B6018E344315060B0E8E3C68960A8 + A05EB2B20C2128E67D4C49E529AC3A01603D4D8FB1C6CBD3F3D3F802BC4A0580 + 29CB488362BF98A7BF7B03CF5EE600D88D4BC5A884114946A36C38B26397FD81 + 256030FB030F994FFF1C96AE15AE6E751AB624EF5B86A271AC328401724B1D92 + 412D2A02E066E181B1368D5AB12E8D221CF4DA6D69D4C6DD69F8C65D69F8E6DD + 0E7D87AEDF95866DB0BA0D7BD2C88DC0DD7D69EC66420EEE4A633665009C430D + EE30ED74AF13F738D998C30EE279C23E7323D62984F4F6340E2F95EDFBDCAB65 + ACEF0F6C636C1C7BD68DC248696584115412F4253F1E8329E92EC2439B08378D + 3175B7F5C18306086CD21E7A78093F6B6906C0FBADDF5EABDB9B9E3D70204D26 + 2C74912030F09790D07800132E7AF28B073DECF3F463471D009357398786246C + F4A134F3D471D70C423DB3E7EF71BC8609098D8770CEBB682B005810182F60BC + 81675F10003E99665C38E15EC0EC093C1F43BB691E9EBE564728E8180E5AA23C + EB1220F8729A7BF5AA835F34EB4ADE87110090BD80F1F4C543EB4281BFE4ED35 + 8AE76F8607AF3A205EF8FAA5B4C821F0656F5BFA0EE08150A278010382333C98 + 7FFD158704F3F134C393AC0060E02F9EBF7808FB1EC2A6E5EFBD9596BCFB9A87 + 945E79EB8DB4F1EEBB69E3C737D3AAF75E4F2BDFBDEEE077FD8737D20664F51B + 8031967738F2C19BEE6DC7FE99809295D67705C00300EC101880F2465A6A6DC0 + 5F200A8025835D3CF3EC75004178F77E71C7F70B26842B0078C3A71FA48D9FDF + 367D90BD023FC103D0C6DE7D276DFFF6A3B4EB874FD2FA7BB7D2AA4F6FA4359F + DD4C6B3FB3F3FA347B0A661102F6FDB4FA93F77C3FE00CA8F052CCC24B1169FF + 52209780F01A6B07F4022E4981C3002F3C1DD75B7F8EC73114E276D3373994B3 + 5485BF150CFB8AD7B86D6DC037EBEF30987179ECC66FEE3ACC05002387A54580 + 3EBC6D0581914071ECEB753F09DCB680A2402412C82415DC74F869A25EE0B505 + 26F90C809A19006FF99E73F9D2C32F57E1967F9F3D73F51A02963A267BFAE2B1 + BAED078ED93A279D27AFA573515E7D744C1D17D14ED901F0EFBE4ABB7EC7FEC4 + 763EA61D3F7E9900C03BEC5C81BE82C0A4DBED7DECB073F07AF71ECE101BE0AA + F3D539C773E1B5E23911321A79F86FF7ACB5F76BDF39E51D3FD97B2C503802F5 + 787CBD6FBD1FFAC5D0DF02BF2AB7CE2BFF36367E437F1B6F79E02EA0F7E03FFE + 980EFF9B3F380C665FDFB55FF0A003BFB3FCBB1208F610CF3C9860794243B32F + F046E0EFBD8FECFFC1EF9AFFC807DE07D84CCAFF83DFBA3C7BF93DF39B450E79 + F1F435D187FF18FDBC8C4730C73369CF5D24285C953F01F0DEF4EB097200FCA9 + FDB74CD535C2AE377A20843C9057A096F6088633BC2D20B88832FDB90E719DF2 + FAD08776BF7E957E5E67AFC3432B8B6EDAB5D1447E191EC49607FCF2900C1098 + 90D0F1211CDFE7B780DE15375F77004CBA14284C88672232E0B9FBEA65BF0738 + DC05FE0278D9ABD744DE6130A09707792C45DACB9714A03BF7AADD4B80B39778 + D8E842628F60EE13DC47F0069E7EC1EE4B96CEE3BE72CDEE3D0AF36C29F077E6 + 45F6FD3DEB9A619A7DDEEE7D172CBDC09EF936FEB4DD474BDDB493C7EC5EC996 + 0967D21CF6043E63F7C6B3675D1D78009F20CC3351384E38F49D75C25EDB34FD + 28FBFBDAD863A74C390CB440309EBF40E0A902BC0100037E3B8EDABDD54439EF + E59BF7F445405E07C23E367BFA2201E0E8F98B08F70CFC457800E3FD3B6EC7BE + 349A79D28E3C8FD15C87390EA097F94DACF387DD0A001EBFCDDA80BB8C07F096 + 3CA93F9067E94452137BF9027F9566591E986B733DA2C1008149298FC603987A + CF6FC97BFFAEDD9A01307DAC5D5EBFCC1F05829963CAE397BC60AFCF41CBDC54 + DEBE82B9714E4B5EA2ADA95DF35E4160E6D9005F81E0A1CCBD9977D724EF5FCF + 6B6E6E72206C6D8CA5CCB62C02C07A8853D17C14E1A7FF8C8569C09CBC150C6B + 83BCD5CC427F0894723F5B770081D94A86B21E3265CDE1A0D7FA53565BFF29AD + 075A1179D62F9297AD8FBC7F3D2DEB1B893E5A13F9FAA726ADA9FC61DC675977 + B126C3FB37EB3E00FCACA59607FE46008CFABA477004C079CDD9E4F92B09FA46 + C5BE15000E6B5AAD79B5168E6A5BFB3EA86DA4AD9347E4885CCAF330765C9BEB + 416D5FA7532E00388260815CF22A677B44B625A81CF3F5B2EC10D11ED1B50E80 + 01BE4D523867F2D84DEAF692A23A00967D2582DF9897280B003BFC1D38227571 + E83B32F5180404B6FA01C35C78D152C63B38DA8164DB210474B63311FE390B08 + DC0DB86CFD643FAA00AF04E4954A9DF72B658DF3D7B2B40DFA0A02F7CF8E1559 + 769E05EE76EDCB3EC176FE7800037F1D000FF1D49D378AED2F2AC2E03A00569F + CA46688AB6C8471263C2F8A663C9ABD7EDA51AD70480834DB5B2BDCA2E5CEAB0 + E7CAD61B15EBEBF9A631AAEFACBD49F5310FD3AF3FC60057652F2FC0B74D56EF + 7D024FD0B86A7C68EBAC4FBD5F537B93E298CE54F51563E906EFB8BFDFC3F49B + 0160E73B4A391F1854DF447864C217037DE1499E1626451B2C06EFDF083EC568 + 3C1F78562541CB7AB9AE87B547C5BECAC7BAA67A53CB2BB7F479901AC657B2F7 + 4AAAF78FEEEB138F61790F3DAD3ABDC62F5504B0BF561100CBFBB767CFA74CFD + 539F3E0353C78C7969C1029B14D94D439EC051BF18007BDF1AF4BD4FB4970FF1 + 9FB92200EE4A0AC82D6DFA81F5EA3B240D183A26F5EE6F3717AB131CED691741 + E06FD727EC4FFA847D66E5C98E87A982BF21AFB24B79D2FA45E43795BD06EF91 + 3F935F4CF20545EF1DD0DBAD47BFF4CCF0F169DAEC8569DCB353D393FDEC86C8 + 6FC83E27CE4F173A5DF4FE1A54DDB81EA016906EDDF89A6E94F51BE9C3D4344E + 00987A81DD07AADE2F94057DA3DA26339AD078BD8DADB775A26AF21426524D13 + B5D816CBDA8723D6797D0921F3C493F63A26F72C2E00B8DD0BB81DE20AC0C654 + 7901DB7A7DECDF945799B435D1CD13E258A710D0DEDFA4C9735D9ABC4B9A78AB + AC497D546763EBFD23006E525C7C90B258894FB4FAA2A40100B3807A90B4B8CA + FDDBDB223C8E0017C923388AFAA6BA584F5E0B4AE5EF2B3700E0CECA9D01602D + 76057FD58F9485338B6894BD7F11654B01C0B6388F10D7F7009E903D7A15129A + 7C5B1F6B67BF27F25284BE00E228EA7A3FDB0E810580EFD3E4596EC410B0AD3F + A92EC385036293DAA568F810DC8D8693D816A57EFDA6CDAB8C3011F60AF02219 + 6D048B81BE110E0BFE92FA98D9D68E41695EF630708352F1081E38170392E5E7 + 58DBBC95BE27F0330B5667F9FEC084A0CB5EC0EC0F4C9A3D807338683C81DDE0 + 85014C6A00C00AB78730B8C9A0E6750104CB58E7752B36A451AB36A531AB4B38 + BF35DBD2B0B53B1C008FD8BC370D59B7C301F0A8AD2FA6919B0EA4D19B80BF07 + 4C7BD3984D7B1C0063741CBD7947D6A6ED95E1D1F70CB614F83B7CEDE6346CCD + A63462DD16AF070C63B4C4D00934C6D8491E00EC10794786BED409F6BAF70B06 + D0601C05FA0AFC0279E5E9EBF0B7082FE0565BDE3378FC2E3BC6CEDD897D849F + 3D70304DDA6FED26F2406001E0E75EA48DBD86F7A6E75E3A5879FE027FDD2BF8 + 101EC3967FE9C534C5EAD807788643604B4FB227F011572E1F4DD30484AD4E7B + 024F3D71B882C133CED8D8B3273C0CF4F4B3C7D2F43347D38C73271C08CFC3B3 + F7F2D934CBDAA69D3996A69AA69DB5FE780C5B9BE0EF74EB3FFDDCC934FBEAC5 + 34F7950C80F1029E5300B087877E2587EAC4603FE795F3A67600BCF4ED6BEE89 + 0B24666F60DF3BF8AD16000632B04FA4C30787BF1900031416000E4C78972DBE + F946F6F8BD79DD0130A2EC79C6DD60EFC9D7D3AAF708F3FC665A77E70DD35B69 + ED07E8CDB4F6F61B69F32737D3B67BEF3B20F6BD31DF7B3D3904BEF3765A8887 + 9C8DAFC0AF69D9AD37B2DE7BC3A18A03980281012C15CC01C4E0F10B2C020CDD + BD95D67DFA5EDA74EFB60908FC8187680512ADB94BC8D85B69D3178483B6F177 + DF76F0BB0EB86BAA03E0359F7C90567FFABEEF0DBA82BA4FDFADE0AD20ADA095 + 0017297D005D403D87BADF98BEFBD801B0FAD3378AFE02C00E750BFCCD7098F2 + 1D4BF1F6A5DC82C20ED2807A2640AE801D65D509F6D615E19E03E4021511D050 + 603103C45CA73C3093549071C7CFD95B55B0338FC7E3D4FA17F8BBF9BB0C80F1 + 02DEFEBBECC18A2204D631F5BA2EDEC30F9FDBFB005CB7C027D231047CF5FA11 + 00AB4EF5883A0F01FDF3370E8081BF7800EFB414017A01BE78FE926EB5F7417E + DF1FE8FF857BC40239F9BEF45A1C9FF3D5674259E7A6D0D578FA0AF292CFFB3A + DFAD00B07F9FFC2608F36DC7603CC793F4BE512CF35DE5BD77B354472872F284 + D9E677C06F0290CBF78DA7EF6AFBCFA84C5BDEDB17AFDCFB01F0FA0280F1EEDD + F895BD2EA19FBFBA9BD6DDBBE3F037FF37F86DF3FBB4E37C21CFDED6FF25FEF6 + 29EB77AEFFCD4AFB8FF13F634FDEECDDFF6E0581575A1DDEFEEE116CD791B596 + DF40A8E74FDEF36B010056C05712CCA53E4260EA49D55F30D7E16D01B95C6F48 + 297B1DE1F1ED9A446402F716264201D72AEB47BAFA433BD69D1BD5F56AA15D1F + 17DAB551C759FE818DB5EB171EC0A484800608230F07CDF5D5AEC70BDFB8EAC0 + D7AFCF9627A5BCFC868DB961D762AECFD7AF3ACC5D00CC0D70D7459DA90E7D05 + 82297B3868DFC7374BD1237CFF794BD10C4242DB3D69FEB58B69E16B36CEFA01 + 7F3BCE9F76CD60AFDFF356BE7036CDB860F722CB037F670185CF589F3344BE38 + ED02FE769C3EEE1EC0B3CF658F5FBC7F01C0847F9EE2A0F7A843E0190E7F01B8 + C71DFE12F219000CF805022B0500BB07F0CB475D82BBE4E5FD8BDCE3B7DA4602 + C89BE12E1EBE395CF41107BE88BCCA0AFB3CA1847B8E52086820B0EFED5BE63A + 02BC4D0018791FE63C8476364DDC6E79E6500DC203987958F4FC253F9A07F0D6 + 6F6985800E0018F88B47AF423F931700F6FE78FF5A59734807BD050003842300 + AEC35ECD55E3BC745829FBDCB5D4A96F94EA057F5D0BD9DBD7E6C9C8E6DBE4AB + C83BA445037DFE9DEBE817E7E64F037599A397B26FE152FA360160D22A24345B + C094B93FEB018030EB830C8473A421AD2BB49610001604C663B85F6DADA2750C + EB9C0AF45287E7AF89B2D6394DF2719DC8A3334D2A0078B2ADCF26DA7ACF24D0 + 8B62B9E7045B3B9A7A9908F78C048201C01E027A6C3BD89522F47D5C00ACB56F + B5BEADAD8D952A2F693DADFE82BB8AC8A5546B74A43E55B9067F3B1360D7ED08 + C1DE20E05B976C1131F57C00C0EEED5B93EAB083B8B08B14BB49B49D90A26843 + 4194633B7941611D43ED1900534FDF11EE292BD8EB619F9F7AA678CF3EE3F5B2 + 05D56D3FB20F3DD16F944900D85EC3FAE4FA027C5180BD15FC2DE3EBFD7164D0 + B1DDDED57778EADA67986F8BD6D5F2DA1EAD251C22ECBCFB3D53EC7543EDFDD9 + B1EC7D38FC457DB1E90DCA76C202792541DF28D904EB65B71336D922435D5D8F + D2A7DA1B181B6AB485C67102C23D50B1B32201E052EECC162C7B6F677A58FF7A + 7B93EA631EA6EADC8B9AFA3C588CB1B1B2973F22007E14E9D8510F6B6F521CF3 + 2079DF8AA7C03BEEEFF31793F31DA59C4F3F4B33A474088C373000D8CE531058 + 00B35B8FA7528FDEF67BB1D4B9155B91C26A186F75AAAF2468592F37E961ED52 + 3C96F251F57E75A9FD6152DFA6F196F2BEA57A5BDB38CBFFD500E0087CC96BFF + DF18FE99FD7F69EFDD7B405A6793C583078FA5679E199B9E78A26BEADAA5771B + F07D5C00AC7E951E0881FFE50160FFE3D99FCE7F3056EF3F2ED4A54F7A66E4A4 + B4D026E393A7CE71184C1D7F3800B08F79A28F1DC37E6CFA633F4411FA0AFCD6 + EBBCBEE922F26791BDBEBD9FEA3DF3DBC4C3F7895EA99BFD1666CC5C900E9D38 + 97566ED89E060D1DE3EFDF01B0C6DB05AB7E81FE25E2584DF58FABEAC6F500B5 + FA37DD00DBD574537E1C55C7B0D4271496EF54B473E3AED79571005FA96AD724 + 46523DD244E721D2840AC5495B9CA8C5BEAAF73ABD6E698BC78B00B80A2BED75 + 83532B1C346A87B475C536016079ED368D89F5CAC77284BD31EC7304C0DDE96F + D2C4394A136C4DE0C95793F1A2D81E158F51D76F0580BB58FD132C466C010400 + 06DC3E31A00573D90B5879A9BE08D3824B8A00B8CBE02C81DC3A00663108C08D + 754860B75E96B4906C2BB3C82C0B582D4EE3A255652D6AA3D44FEDB12CF5B0F1 + 2CA0252DA801C0BDC6D8221D08ECDEC02680AFCB16FF96F6183DC5D3DEC05FAF + CBEA3D0E50DC91FA4C281EBE9366A4BE1333E8150056F867858096A72FF230CF + 05E0CAF31791273CF35353E6F93E55F2069691A2EEFDABF175002C830746139E + B897F144869428EA1DFC967118549E0A80B7CD28D360B8218F7107838FEA0480 + A9F771336D4C3112613C72A391C90D5205000F9865E50281058081C1435000C0 + EC0D8CDC4B9810D17808230C5DC0E202848762205BBAD68D6E4396ACADE0B08C + 6618CAE21EC20E8881BE3617C8E97AF7001EB56A5B1ABD7A7B1AB30E70BB2B0D + 5BB7230D5BBF3B8DDCB23F8DD9FE721ABDEDA534623300F8601ABDE9401A6BE9 + 988DFBD2E80DBBD3A80D78F76E4F23376E4B23366CB5BA6D6E6C1C63E591C05E + 1375A423DCFB37036100B07BAB6CCD9E2B8490F630D2182DAD3C61FB5E1746D0 + B15B77B9413402601946F3FE78FB7CFF3C57F1F6F5D0CF45DA634F1078A21B66 + F73908663FE0C907ACDFFE0C81279AF0040600E3093CF96006C0CF1ED8EF30F8 + F943EC07FC729A7A18408CA77086C03925D4337B0413F6F968EAF09094969EC8 + 61A101C0EC138C2730F90E00F029EB5FBC819502803BCE1C4B2F9C38ECEA387B + DCBD8001C1E43B2C9D76DA8E6579C1E1F9AF5C48732EE7B0D178054F3D636D97 + CEA659972FA699972E14080CE0BD94D8FB77F6D5B3EEA195E1EFF9340BB87CED + 4202F622F60146E401C2E4F31EC0788EE11D963D88D94F12EF32428ACAA30CF8 + 8B2719214697DE7823EF4909D078E79A435F340FEFE3D72FFBBEBF1B0029EFB1 + 1FE595B4FCE6D5B4E6FDD7D39A5BD7ADEE5A5AFDFEAB69F327EFA45D5FDD495B + EFBD97565ADBE21BD7DC6B6EE9076FA545B76CDCBBAF3BF015FC05362F2D5098 + 90AC822A8C2115D821543475ECE5B9E2C3777C9F4F80EF7A20D14737DC23D0F7 + 08B536D2CD5FDC4E1BF148C423D8A47D467DCFDFE2714818E8759F53C7DEA4B7 + D36A3BD68ABB37D24AEB1F0130D04A5018D0254F46FA91070E0AEC02F81843BB + EA04FBE82B1846BDC6A94EFD00BE5BBFA7FD437B6D5EEFFDDC667D367E63C72B + A08EBC7B6B9A04F1EAA29E76C13EEFFB5DCBEB94149048586BEA0563A947828F + A4004E00AEC21553EF6D78B27EFBA9C3DFAD0E70EF3900C6D39463318EFECA23 + 8E27F0ECE273B273CD5ECA00DC0C71752E3B0AE0A54E799DA38E19F388D7403B + F0E62DF077DBF7F75C2D009CC12FDAF69DBDA7AF3EF21430EC65BCAEEDBC00B8 + EE4D2C806DE7C579B7BD07939FEBEFEC350AF0250434C0B72AF3DE4CD5F75E00 + 30E74B5A3F1EE5F81DA82E4260BE3BDFAFDA52FD16F4BDD7BF7F7E337CC6A482 + C0718CD75BD9C340BBB7BD9DE71756F7C547F61FC9DEBF6B3EB7FF9DFD277C1F + 6087BFEFFB7F83FF89DE978030A2ACDF3AF52B3FB56B88FD7756017BEFDE4C2B + 892460FF593CFCC96BBF6F0400CE619E33005E79A7E5D98BC847B02BE02BA9AF + AE27EA57975F6B6EDB58C0AE5D8B72D48337D32AAB235A811E8AE19AB5DAFAAC + 7CFFED7C2DB33E5CBF4801C5F9BA961F92F130CF95D76F0EFBCCFEEB78FF2EB0 + 6B3402F8E60777F275DBAFCF6FF1304F0E014D4867A02EF702015E521E0E72B8 + 5BEA49E750B6BC0360EB87F7EFBC6B36E655E0718E2431EF1AF798D369FA79BB + 175D651F79FA5C486C5380E6F1D0D12B97D2ECCBE7ECDE75DA35F33C512CCE65 + 9DB7FB8F69E6B9ECFD3BE32CE0D7EE65761F63FB8359D6DFBD804FDAF1CF5B9F + D367D20B47ED7E09E83D752A4D39C25EBE76EF3C66F7D813275D538F1CB7BAE3 + EEED3BFD580EFD9CF3B90DF83BF9C5BC672F62FF5E015ECA805F44BDE0AFF7B1 + 3C6017C8ABBD7F81BD02C0406179014F26EC73DCF37727F3950C802B95390DD0 + B702C0369F21CF1CC75520B13F24B725035E00F0A41DD647A19F993785F91411 + 58E4F10B085638E851CCC9D66EF6B0CEC0DCD16B721868E4219F8B0730A1A01D + 00D36F5D06C0B4017A99470AF42ADCB33C8209FB8CE72FF057B057D0D6E7A045 + 405FED0B1C21B0A0AFC4B8989706CE6FEDE94B5AA9005C490058E19EEB005892 + 872FF376A2F9F8584BA53C8FCF52BFA13CA8C97C7DE6E2D47FBAAD35662C4C03 + CB43A25A4B485A6B9067AD0128EEDF10FA99358AD629BE56B1BCB759DF7E0100 + B366EA5465AD555706C0ACC128DF0F802BE05BEA7B4CB0F5E2385B1F163503E0 + 766F5F29425F206FBD5EF05775FEF0B2A969DD5B5F3323FAA85ED2C3D4D543D5 + 05EE46C85B2F47F97ABD01F6D625DB425D75DB43CCCB3EA1D4F38333F89522FC + 75F08B047F79181E3B49B19B44DB0969B4A3D4A5FA0885956AFC134F8D7008DC + 7500B27C01C0BE6F6E7FE06F2B4F38E6087BA32DA855479E3E766E4FA1761B93 + E74BDFCE6C4EB94F691320A68FA5C05F24478AFB95C1AF877AB67C77F60CB6F7 + D595F3EF3B38F5B47CBF911353FFE113522F3C9FFB5A1FDF5338A76DF6C122D9 + 049BF4A03EF118F57E4D6D92F721EF76D0962DB46DAC6C9700E068373579E445 + E57B34DB81E5D8A3F6986F92DA1FD62FAA3EE661FAE70880EBED71CC43054B01 + FEA2A6F6BF84786DE73F96170B8241756D07C0DDBB3F957AF4783AF5B4318060 + EA9EC0B99008B4815BC19F10CC46DEC1B1DD550798BF56F1B8F175627D539BEA + 1F554D63ECF3519BE0AFF3AAD85779D292FFAB04C00A052DAF5F52B509048F1D + FB5C7AE18559A92F4FD93CD1C3DA7E2500464F589EFE9676EDC2F9D80FABD6CF + D5CDDEAC3EC47FE68A00D89FB8C055DDEAFD076679E0EEB09193D2F2B55BD3F4 + D94B533F6EE2D6DED5FEA8DDED4FE763DD03981F89FEE40FD67DA05775B5B6C6 + 0BC99F495DECA2D312DEBF7C167DDC0378D2E4E969E3B6BD69EEC255E9E98123 + 3300EEFA680038BEC6E3A8E9588FAAEAC6D5A470F3CA6ABA013EBE9A6EDCA8DE + 5E852441567F1F10A6FC00008CBA30E93055ED9AC448AA8F6D65F2D3993429F2 + B029963E0800C7FE5E0EAFA1BAAA5D005875F42F00387B0167002CA0DB3C19BD + 1FE83E6AFF26A9BD33005C89BE2626D21213F7F689BAC98EA77C9C8C234DC651 + 7D6C6C9304809BF430002C558B9202807D315400B0202E005830B7495A64558B + AD504F2A000CF89500BAD542B0A402BD92EA9BA485A414179BBDCA02362F643B + 07C0AA97E282568BE3FB16C8F46B148BED028047F1A4358B6D16DDB9CEEBADFC + E4D869A90FB0B788F293E36CA16FF9BE1366A47E9366A6FE405E0C0978069BA2 + F7AF00307A122343313844F89B8D0F84190B00D8F26D7B0103779FCFFBF74AF2 + 0A76301CC458194814E6194389A02FF50F04C0C520C353F931341B62AF60F7EA + 0504772C4A832C0FF015EC651CED3CF54FBDC3E20080EB86235201E00881D160 + 3722017A73F86704FC7DDAFAFB1EC1F42B1EC2313C345EC14317B580EFA04519 + F6CA6026C35B67001863DD889578726CB4FA2D96EE48A3D6ED4DA336EEF550CF + CFACDF9726EF3F9DB6BD7B2FEDFEE0EBD471E8721AB1F1601AB52103E051EBF7 + A691EB76A511EB0A005EBF2D0DC7B377C3B6EC796275181BF13891178A1B258B + A1128D2965ED6507181EBB29D7238C99C05E8588C6082AC3288652ED95479DEF + 0D4C78E8DD78F7668FE0097B0E38F48D6A0B096DF9E70E98F663D0B5F14593F1 + 0076F89BC34247C8FB3CA1A05F7AD101F0B423873D9D7A983D83498F789EB12F + 1CB2360CD3F439CADEBFC7D2F4E3C0614241E3117C3CCD3C8D67D331DFEF100F + A729C7ED18270E3B00C613781AF0D7EA09FBBCE4FA15F7FE9D713E7B07CF3C97 + 4343B35F30FB02D307104C19B17770C7853369C6C5F3693A61362D050667007C + C5A1AF00309EC0F2DEC20B58D077FE6B78739DAF6030E057AA00B07B99690FC9 + 0C7E058097BC7DDD85679AC309F60EC6EBD734FFF5ABD6EF8A877E26D4F38A5B + 8428BD9C56BEFB4A5A7FFBF5B4E3DEADB4F79BDB69FBBD9B69C3476FA48D1FBD + 99367C4CD8D357D3A21BAFF81EC278D42D7BEFEDB4EC1680A4058081270E502C + BFE2FDB71CF43A8C11F82DF0C6BDEB2CF53D362D8F471F40680D1EBB1FE21188 + 971F1E7F780DDB797CF681B56738EC5EC19FE450B22BF1382CDEBF39BC2C5E87 + 40E1DB1940591DB01750252F46F62C050C538F805A6B3ECBB078FD971F389C05 + 722141DC0CC33EF232E00B50261886047CC9D357A2CF86AF6EA72DEC236C9F29 + 7B17038005D6B46F2BDAF80D3010909AE15D8478C0DFE8294C7F077B3EE6A3B4 + B9061D01890046EA045611EDD43BD8FC39877226158C7478E96187ADEE3B3BDE + F79FF9DEBF28BF7E0EDF4C3FBD868E9BA126E76AE7C47995F3DEF47D86D002BE + 4879EA191FCF537DC8D38FF323F573FC16286FEDDFDF4B5BBFB3F3F836E73D0C + 347B01FFF0B9877D4602C09BBFB6F7F58D8DB5FC46F66F06FEFEF479DAF93B3B + AEA53B783FF65AFADC0465F1C2F53D70F95CEC7D0BF8CA1B98EF900704907E17 + A49BC267C231F43921FFAE4AEAAF63D2EBD14F62DC9A2FDEF7B0CEFC1E22E4D5 + EFC5DBAC4E5EE178940B0CAB9F7BFF222BAFBF67BF5B07C0C0DF3BF6DB7FCF94 + 416FFEDD66F89B0130C057FF970F2A004C9BD797DFAFA06F7EF882FF9D8DE57F + 5A04004680DFECFDFB5EF92FDBF1EEE63D78FD41101380F73E8F5FBB16AC0220 + 2300B05D0FB88670CDE07A2230CC78DF6F9C6B51B9EE109A1EE0CBB588C8075C + 97A8A30CFC5D68D74240306552416289FE3ECEB4DCDAB4E72F0098EBAAF65FCF + D7D4CB764DBD6CD7E31CA101B1FF2F7BFB0AF892729D8E6524003CFBEA854AB3 + 4C02C019FC5AFF574C006087C0F67AAFDA71ACDF0CBB17117942D12800BFA494 + 3BD8AE807B117BF9BAB7AFDD73CEE3ED8BCEA71967CFD93DF05C9A7EFA8CC9EE + 5D847806129F3B69A9DDE3CE5BFDA91369EAF11369FAC9ECFD3BF5B8DD1F4FDA + 7DD0F2D38E1EB3FBACDD5BED7EDB71CCEEA5C74E39E49514F21965307C2C3DF7 + D2E1F45C01C011FE2A6A48F4F8451E229ABE070E550018D08B37F078C233EFB5 + B9829581C29318BFCFDA8B08FB8C0482258030FBFB327F619E83F0F0654EA379 + 8D03603C7DAD6DDCD61CE259219F5D25AFF0CF02C2CC9FFC413B13732FEFC79C + CBCA02C02388FA62A903E00D3BB3D7AF7BFF664F60BC7F4716EF5FC15F79F946 + 00CC5CD2812FF34AC160CBB3F7AFA0AECF3F55B67929A9BC84EB5EC0DEA7CC6B + 1DF89679ADE6B3E86100D83D820BECADCA65CC001EC8B4397904C00E7D99AB5B + 9BE6EE78FA46309CE7F14B52BF0E5B37007B4BB9AF957B3C6F6B99A9B666B175 + 40FFB28E00F2B2B6786A7A5E5BB04E60DDE00F9F86F50AA9D635F5750BE528FA + F0106C1BF0ADABACBF9A1401B0C06F130026EC73EF892DCF5FA94F00C0DA03B8 + 5781BB5102BC51D4FF1A00AC75725DEA1FFB79DFE2F12BD5D7E45AA74BBE860F + 90578A65EC07B1ADFB9076D01B15EBA3ED41ED110047F05BD93C90E0AF896DB1 + 22B88DC25E9201EEA8CAA612C5B8870160A9CBD368B8D59B2CC50BB8F7D0B1E9 + C967C6A66ED6E610B6D87FEAB620F2DEEE7D28537FBF6D49E3A458DF762CCE89 + 36EC65EA83DD0C05001CED63D8BEDCFB97C87CFD9F493DEC9C7BDA71BA7B1D36 + BBA169F0D8E7D3F4A56BD34CBBD63C3D6CA26FFBA670CFBF04003F8A9A8E15EB + 1AD5CB841DB43300EC7DAC2DDA4C518F816D00D8CBA668DB4500E07ADDA3AAE9 + 7851B4FF12D501306AEAD7B9E88F0DBAA80E7F830DBD6EB727B226AAD74B2DBB + 7B4B0F6B47B14F5D8FDB1F3D4A9FDF549D01605852D7C29FBAD700B0E5F1FC7D + D2FE8303878C497D789004E859802479C1DF0A86D61501E6AF553C667C8D585F + EF5BAF7F983A195781DCD2C7F99CDE73A96BCAFFD500E026C91318E02B088C37 + 30F5A4F2087EC2816D6D6C91CA8D2037A802C03C4910F6226EEBA7FD80FF8502 + 60E41EB0A54DB1CB7BF71992868E9A949EB63F21A19FF9E1F9E6DDFC694D72D7 + 7FDC10D075B5F5B10BC543F700664C53FD2F5075F124CFFB3375EFFD74EA66EA + D56F481A6813A8A706D82480D003F667F0F36D1A5F53ECF3B86A3ADEA3A8BA71 + 3529DCBCB29A6E80BF5C9DDDC09928F8D362561F271975B8DB565757E9D385E3 + 045513199FCC98742CF498003882DCCEA4F638BE3EAE2D5F00B0EAEA00388781 + 1E56294E480574A362BDFA47C5761D27AA5EDF067D7DF2DB6AD3A41931A9168C + D504DBDB39072B57754111E0B68D33694C54BD7F67D242458B15162492974DBE + F0D162C8C16FDEC7374A60D7A1AE9501C3D13B588BAD28EF5B007004C102BC71 + 4118E16F13E47D10FC65011AC34F69911A17AFD562B556962200A6BD71816CE5 + 26B107B0402FF05700B8371EBE45B445D8EB00D8CAB4B54160BC7F01B8136738 + 009627B0BC81ABBC0908DC6E74C861A191802F4057701809020B0063EC100056 + BD8C243280C8602200EC638A514579291A54C8137E2D02603D9DEFE097106EEC + E365223F68C6E23478D6923468E6921CDEAD780403802B084C9B0C45C062EA82 + E168D0ECE56D0018EFDE36CD5B9186E211BC7075C2131800CC9EC11E0A7AD11A + AF17FC551EC398C0AF84912C1AD128CB00E721F830BA39FCC550B7D10D7A4396 + 6E4AC357ED4AC3D6EC49CFACD99986ADB774C3BE34FFCCEBE983FFF83FA7EFFE + 97FF57DA79EBAB3476EBA1347CEDDE3466E381346ADDEE3472ED8E3492D08105 + 00CBC3D73D80D7597E1DDE235BDCE8E85E2826528C91E32CC538E9FDAD8C4730 + A1A1315C62C8548868F702B614AF600F7D583C8327ED6C0160379CEE2CDE3380 + 5F4B1D0007AF5FF706260DCA50983E689F7BFF2A0434223FF9E04B69DA9123A9 + E3D83187BD78013F47B8E7027CA71CC2EB07E80B04B6FCCB79BF4052DF67F030 + 009830D078031F4BD38E933F96669E3C91669E3A95A6FBFE86273CCCE50BC75A + 00581078CE85D369E9F52B69F53BD7D392D72E3BE4F5FD812F9CCABA9853A0AF + EAC8CFC1FBF7D205F7FE25D4261098709B332F9FAB0CFD78FDCEBE9ABD7F2300 + 06FE027E81C3D4017F09010DF0150006FCB674CD0102AAC2405FCF7B483A1006 + FE9A96B017E53BA6B75F738F34852725DCF48A775F4DAB6FBD9A56DDBA9AB67D + 7E23BDFE5FFD53BAF77FF96FD36BFFE91FD3F6CF6FA665372EA6E537AEA4E5D6 + 6FF97BD7D3F25B84797E332D7FFF8643DE158226ECFD5B8729C01D4B57E29907 + B8B9F57A5A8C279DA50E74AC5EF067EDC7B7D29A8FDE75C80BF0F1F4C3771CF8 + E2052C284CDEF70FFDE89DB4FC433BAEA52B2B109C213020CA611461683F7FCF + F7F715E802664500BCEAB35B599F12021AC8FBB1F715EC5A8737E617199079D9 + 4199F64EE5B82D10ACD740B92D43B44D803D3C3BEF650FE07C1EE5F8763CBC2F + 37DAEB46E80BB8F3B0BDE59C807C1E06FAFB4F2B188C372B9012E80A7004D092 + 27C5F316B8B91DD00AA804B65A9D7BE4025701AD3F020FEDB50A7874A06BED78 + 006FF89A637F62C7F8C2E5AF67728FE19F3E7548497F01DB0C73F36BA82FE7A9 + FD94FDFCACBFA02F29F592BFB6D5D701B06031ED19C6DAD8B297AD007006C2D4 + D967F895BDD637771D0213027A9BBD17D28D5F7EE8C27B15800BB89507AF1FDB + CEC18F6FE70510261FC1AEF737C90B5CDFADBC6155CFFBD5B90A00FB7B071A97 + 76F69DC65BD85FD3B4E19BDC8FCF414078AD1D9FDF1840377FCF2DC0ABDFC516 + FB8C1D0097767E476B00B6FEBBCD20983ADF1FF81E5EEB1F39005EF3D9EDB4F2 + 93FC9BCFBFC1FC7B47FA9DE7FFCEFBDEC7CF23D4F3FBF7FFC1A7D6E7AE1D03A8 + 6B7594E5FDEB00D8F22BECBF4A5868A02EE01700CC5EBFFEC087D5F9C321766D + E01AB0460098EB0070F78EFDE73FBCE979FAE1D5EBD78C72BD710F6113D7158F + 7250BC7AB926017FF1EE1500F6FA728DA21C43E23BE82D0FAFE80116EAFC3A66 + 65C02F0018AF5F520030E0D7A32AD8B594F0CFBA26E7EB6A7E30671EDEBAAFF1 + B08E5D8BEDBA2CEF5EEA14DA5940B882BF57784028DF2B00BF78F046B107F05C + B614B872C18497EF85B4E0D58B76CF39E3112848D99260963F8C742A759C3B95 + 66DAFD67CE251B73D15EC3C33DE3F19BC1EFD493A7D2941327D3B493A753C729 + C23C9FF4879F103078FDCD1B69CB071FA4D967CFA629762F5618E8E9C78FFB7D + F805BF171F4DD38F9E742FDFA8B63D7F0B149687AFC23F0381499B20707E502C + 436052DFD777DF4B2EE0EF389B8B8C67EE416411D3F8E0ED2BC9E357E5713687 + 190BAC2DE09728271EE9C4E63A137764282C004CA8E7BAA72F62AEC4DC893669 + B21DFB397B1D52CAB4FB9C8A3999CDA93C04B47B016FB139DF2607C0633758DB + FAED1EEAD93D7E2D1D51003120587B034700AC7924F2BA32E704E6CA03587350 + CD3D057905738960A33EB15FAC13F41D08CC0DF3DC4701C00A0DADFA81260FF5 + 5C53DCBE85D0CEC0E00125D4337378857D4600DE3ED3E73BF4551DC7E0B5D84B + 1898FCE4345B4F58BB00300F970AFE0EB4B5C4D3B6AEA8AF4B58D3B09E897528 + AE79A4FB806F91B7A1B0A68A799515025AE0577B01F7964730FB00B3D7EF045B + 03D6F45B006087BD96DE57677A180046B19E34F6D77897F6FDADC1DE0880EF5B + BF0FB5357FB013900AE40AE62A5F07C50F1AA3FA6893000047E05B570580078F + 4EDD0BBC8DE0360A3B4A13006EB2B1A82DF6616C0B00739C91A9DB8011AE9E43 + 46A7FE239F4D4F8FB6CF93737E2ADB8064EF89761FEA056C6577AAEA6A921DA9 + A9BDAA17000E756E1BC30B99D0CF5696CD4BF6AF2CDA8679DAC3FA77C319E4C9 + A16EB7EBFED4B034C27EE3F3ED3AB760F5D63460F8A4D4A577761AA9877E960D + F0CF25D9153B95ECA10F04C0A6A610D09662BB8DF968DF458F028065FB6DEADB + 5417EDC6BF4411FC4A4DFD3A17FD6D5CB499D7556CE8757BBD0070BD2DF6A9EB + 71DA9BF4B8631EA5CF6FAE8AEDB4F20298CE91BAF44B3D702A2C00189107EE8E + 9F3C23ADDDB0334DB57B622FFBFFE180A76D4A3D1474C93F50816BDD07369BEA + 9A14C72B5F57BD6FAC7B147532AE0E803B556C8FE3D4F66B2468FA6B25C0AB32 + F0B757AFA7ABBA0C697BB581DA5F0D80ABBE766CD4D0E73F03E07E9DFF99CA8F + 803F6D779EBA28AEF900E21EF607672FE02E78C5567FF4CED5047E3D45406493 + 7B03D72F225165DC6F15D6A0BA28F23EEC3D126FBE3B31E77B717CCEA9F547E8 + 6617FF6E76A390E76FFD1851F1357E899A8EF9305537AE07A8D5BF7EF3FB75EA + EC46CE8DBE3B93244B2B386BF5951AEAFC78B1AD08E81B554D648ABC4EE057AA + F5A9AB9A14D5FAAABE499D1DD7DB429EC9A1F600AE3C82430868016026A68F02 + 80A39AA0711C475E13DC98577B7D5C5D7500DC3859E718A54E7D34D9567D539B + CA51B17F5D55BFB050E96C71429E4510E19F7D31E4C037036041DE3A048E6D82 + C05A6C558BAED217E0DB0480E38290F451003069D322B30E807D41DAB08805EE + AA8EB4AE070160CF9B047D9116D50060402ED0B70E7F81BB11F4C63A790693B6 + F50DFB016B4F6029C2E0EC09DCF2FA45181E3C2CB4A997C90D11CFCEF2B0D1A8 + EFE459A9DF7319F43A1CB6BCBC8205873D5F93802E121096012502E1262F61A0 + AFC23F93C748E3DEBCC540A371406029C3E0A50E7D2560B0CBA16D81C256F670 + 7073721DD077E0EC652E806FE5011C20F0D0F92D004C28683C7E290B00C7BC7B + 0B63E02A2058C2A8168D6DD1904619035DE57981966D48CF2CC7E8B7278D5CBB + 2F8D58BB378DDD72308DDC7C30CD3C7E355DFDBB7F976EFDBBFF21AD7FFDC334 + 76EBCB6998F51BB5DEB476A70983E2363720561078DDD6CAC808E0251FBD8025 + F712C620491F2B037C15161AA32572005C8C98D4539680C0CF027A77D8F99610 + D1184CF1049EBCFB609A041CC6CB77EFC1F4ACE52763B4B53C509894508E2ABB + C7306117F7ED4F930B0006FE0A064F3974384DC7CBC8D2E75E7CD1F5C2CB19F2 + CA68EC5E4296CFC6E4C369EACBC7D2B42339D424E1293B8E9F481D270841C95E + 85253CF4B11369EA91230E88A71E3F9AA69CC8E1A0317A037F679C3991669F3F + 9D165CB990166174BF7836CD3C7BD2EB6658DA411FDF2F98F0D078055367AF63 + 22C4E6AC4B17DDEBD741B0EFB98837D6991C1AFACAF934DB8E3BEB724EE7BE92 + F76E9CFF2A60803D823300260FFC2525B4A760307924CF5FE001602183DD571D + 280802E3E90BA0580AE47058914392129AB402C0375F4D2BF0EC7DEB42DAF5E5 + 7BE99BFFC7FF98FEEBFFDFFF277DF7FFFC9FD28B3FDE4DCB6E5CB2F1848CBE9E + D6DE793BAD7AFFEDB4F4E61B69D9CDB7D2F29BEC89F97A058109A32A70024441 + 009536AF3A20CA7B6F38F0897B7FAE01F41012F683B75CF2FE8DF01770C4FEC0 + 780303875714009CC1EFBB9EE63C7B8E0282DF4B6B80C1C029BC140158560F94 + 2244345A554018ED1BF11235ADBB9701ADE097C097F72BA02C83B0160416F005 + 0666CFC91C721A28489AA170866D28E7335876005C402F52385E3C37296B0F57 + 07C1E8DB1CCA98F24620A4BD86BC540191086089B7AA03CE521781A6F2C048B4 + E16B3B0760A4D79166A808C80532027349099D0C9C153016A4A5CE012E1EB200 + 6A3B3F0025AFE1E18CAD3F02700AF2928FF58052EA1DC2024DEDF8CA7BF90740 + ADA5DFD8F14D780167CF5F423CDBF1BEB2CF03C88BA7EF77762EDF7E9AB6A302 + 8301C31BEDB3D4F724002C4F5E5E2303F2CFFCB3A34EDF279FAD3E37445DFC7E + E3670CD405F6EA73D558FAE97894FD3BB331F4F1BEF639308EF6FC4041967FEF + 405EFB0DE877A1CFD77F03FA6D581DEDFC3EF97DF13B72005C7E6BEBBFB0F77E + EF43F700264C3A2097DF307B71F35F20E53FC2BEDCFEBF70886BFF51FB6FF1FF + 4078D5F3FF5A6FFF111EB288FFB77AC8E755005EFB8F3BBC35B9872FFB7D7F42 + 18E8AC0C81ED7FCCC324453C64C2F5421017CF5DEA56027D3DFFA65F5BE8437F + BFAE84EB0E758C457E5DB27A5D874825EA01C0E4194779DDC7B7D28BBFFF36ED + B5DF14D72479F9027E7DCF5FA0F15BD90BD8C1EFEBF5A80C78FE5EF5EB32E23A + BCF4EDEB9EC63A0160CAD103580078B6A5405E802FE097D0CFA4B3EDBE31837B + C9F933699EF55F73EBADB4E3B33B96BEE99EBF732E9FF58815FEB012F7AB3327 + 3DB4F3EC0BC05FC23DDBBDCCD471E66C9A76EA749A72FC647A01500BB03DC183 + 51C0E0E35677D8C69E4E87BEFD365DFA57FF2AAD78FDF534F9D0A10C81AD0FFB + FEE2FDFB3C0F5EBD44040EEEAB761F3C5EF6FDB5E35237E5D031BB5F1FB5FCB1 + 34FDB0BD4ED9FB772AF5368EFBB65260B00404D60363CC19100018017F272060 + EBCE0C7DC7016DAD3CD6E627636CEE82C8B3DF2F605800783C0018A05BC02F1A + B365A7CB433D17300CFC459A13692ED404800909FDC2DE97D2B4038753C78BF6 + FE08416DE7445F1EB4E301BB11252ACB689BA7C90378347BFB027DADECD057F9 + 959BD248530E15BDD5F7FA65CE08F01D1EE16F996B0AE0CAAB5775AAD79C540F + 256A5E2AB8AB7EA4B1BD0900FB7628F37970B2057F3D5F40AFC35C9347D6513D + 60570F65520EF568B0E535978FD0F7A9998B5CEEDD5BF494D5E305CCEB76EC7A + 29EDB866FFF35397D3087BDF00608579F6074C2DE58153DF17D8D616AC29E2DA + 84546B17F2A45A073D0E00AEA4F555631E10CC7A2CEFF7EBDEBE02C093729DC3 + 5F20700DFE22C15F0FFF3CCED68805E26ACDA9F5675D4D00586D555DC3FA16B1 + 06565A57EC57D7A300E0FABA5EC056B60252EAEAB057756A47F5314DF54A3DFF + 10008CE401DC7D506B6F5FD948A20471A34D45F56A8B75CAAB2CF8EBFDD58731 + 0346A69EF6FAFD864F4AFDF92CED3CEAB69E7A5E362529DA99625D5467ED1100 + C77AB76F5959C0B76EF702FC6297EBE6DEBCD8F00667B88BCDCEEAFB0D1D9746 + DAFF6018FF013EB7027C9B0070CC3FAE2A1B6227929DB153610BC56EFA300FE0 + 3A002EC206EB10B5E4A37DF76112F48D6AEA13CBBCC6AFD5AF83BF8831D94EEE + 7A0C001C15DB3AEB53EFF7B0F6263DEE98B63EF093DF88A13C5085FD3C1000DB + 794400EC4E863D9E4A533A16A4BD078EA585765FC70B585B90BAA31E699D5535 + 48AFE52A1CA752535D93E278E51F45F5E33C484DE3D183DAA2623FCBFFD50160 + 415DEDFB2B8F5F790277EBCE0BF53259BFAE1916D3FFD702E04ADD1AEABC5EF0 + F75F1600D69F4F00B8ABE53B87C0B9ADBBFD9988CFCE8F0BF1A7251CF42FD903 + F8BEF60A005BBB5FBCEFBF98B9976EB9903C14143FA2FCA2C8EBD93911DEBA1B + 9ECEF69E78DF8421C89B8EDBE763EF3D871DE8E41835D5FB3CAE9A8EF9305537 + AE26859B577E5AE9F16EBE8FD2AFE978947B1026A5B74D3C2C5F9F68548037D4 + F93852B535A80900FF125593A2DAA44AF5DE5660725BB9F4218D13BD38DEF340 + 5F9B3846008C077086BF1900A327FAE470D011D0AAAD5EAFB6274A281B4D5ED5 + 1627B4529EECDA84B55FAB5F9C04AB4EF5DD4C9A344731518FF92A347304B526 + 4DECE91727E0716C5DF563C4BE555A162B7121122500DC128BA70915F4150076 + 986BAAD7D7DBAB3E96FA02AC40DD26108C58147667411820701D007B9F52A745 + A58F29E53A00AE16A305F822F2DD6D7125C01B17B67131AC7AD5A9DE5353CFB2 + 98D6823A8B72F60016FCC5A3377AF94A11F8760A80691B6B6DEC21CC9EC14182 + C2D123B8F7B32DF82B788B81A297D5F59C30DDF7141630EE376956EAFF2C7D78 + 927D56EACD53EAC57081214421A1FB589E6308260B0223195194A72D825C195B + D407E34C5FC2B661A02921DA78725F7B733166002A461BC1E0C133973808F627 + FA4D02C7D12B983C0018EF008C4CEE6D5000B03C8001C002C3A478FC027E9FB6 + F1036C8C00B080AFBC8149DD1B188F61F612B6BA4A8BB3278524239A8C6E32B2 + B9A1AD08003C62D5AE346ACDBE346ADD813466D38B69F4D697D3B8DD47D39C93 + D7D282B36FA4497B4FA4E11BF6A5616B77A5E16B76A611ABB7A5E1AB73B8C0B1 + EBB7BB1171D8DACDE999D51B33FCC5C8B809CF93BC0F9D4030DEC0DA976E1286 + CB2DD9FB57C64C52C15ED268F48CC64FF26E18DDB63B8D2E865341603C839187 + 85DE95C3294E266F9A68C2882B6F1EBC7BA2A7F073FB5F4CCF1D7CB102C08483 + 562868BC81D1F356C71EC12FBC9CBD8808FD88B2475136226358C6D8FCEC8B2F + A7C92FB137308669C2511E2B3A9AA6E1B974287B0EFBBEC1276DCC89A3AE8ED3 + 27D28CB3A75C33D913F1DC69AFC35398749AA553AD7FC7E99369F685B36916C0 + 97509956A66DFA999369FAF97369E6A56CC0AF0CF64594558F2717A13CD9AB71 + FE35423F5FACBC7F175C07169C4FEC2729E84B8868DA725D86BCF22A8B500110 + 4C397B155BDB75018A6BDEB6E4AD57D2F21BD75DCBD8D7F7ED2B69D93B17D3D6 + 4FDF4937FFDBFF32FDF17FFDBFA63BFFD37FF450D02BDF255CF435D36B6915A1 + 5581288C7BE70D07C9781603810128F2A803A4005B54471928C35E9BC05DC2B8 + 2A3474F6EC7BBB023D8C41780FE31108005EFEC19B69C5EDB71C006FBA77DB01 + 30409810B11138915F6E75288785BE95D67C92BD1169978722EDE455A61D298C + ADE4C7C48391F1C0E20280016A1104037B11D00EC0479E7D542903FA32F0CD80 + 98BA0CF8008819ECD625004C3E43D45C27B58F0B303180465280A48023A9E0B3 + EA747E0E400BACE5F81BBF058466AF5F3C910581B7FDF8B97B13032A7D9FE1EF + B3D72ECA5EAF7820E730D59C1B5012A0BCEE6B4219DB797D9B41EF8EDFDF734F + 62C031D0555EAF82C0AA73205AEAF53A1EEAF8AB8FD3A6AFECB5BFFB3CEDFCE9 + ABB4CBB4E3877B0E7901E480DEADE84B3B77D316AB675FE09D3FDEB3FCDDB4F6 + 733C6B3FF0FD7C09058DB4B72F9F199F239F09797D66FABCF4390B1A53AFCFBB + 2E7DE7E4E9A3DF08658EC1EB91F2B92B4C39ED4A19A30710F82C11BF0B24F02B + D00BFCDD6EDF8FEAD48FDF0AA9FF6E3FBF9DD67F6EDFFD17F6BBF9F2AE8DCBAF + EBA19CF99D97FF88FE0F82BA2BDDC33EFFC756DA7F8CB6EAFFE1E53C4E0098FF + D7D23BF61FBF63FFEB0FAD6CFF753CFDFD3F6FFFF508849197CB7F5F7096EB07 + D784B51FD9EBDEB6E3591DD710EA0476FD3A61A9F2F4E17AA3F1D471ED5918BC + 7C179594B628DAD9F7F7F09F7E489FFDDFFE4FE9D6FFF8DFA46D5F7E9CE6D975 + 18D89B4340E7076916128ADF2332F0100E619CB976F3904EBE06EB9A8CC8D3A6 + 7D7CD9BF57D76ADA047EAB7D802D9D0308BE4418E7B37E9F20EC73EE7F25CDBA + 782E75D8BD897B0E0F28AD7DEFED7480FDBAEFBEEF7B02CF26528547A230319E + BD7ED9DFF78CDDC78A669CCD0078FAA9D369EAB19369CA515301C0ECF38B87EF + F347F25EBF78FF1EFCF2CBB4F4D5577DFFDFE78FB095C2F134F3E4297F900AEF + 5EC23AB3AFAFC23EBBA7EFE1A30E7E23E405FC922A4FBD3C7FE5092C10AC07C5 + EA0078BCCD2FC6F130D96E9B179430CF00E0F13BF6595DF6F475206CE50A009B + B2772F514B98BBECF1790BF097790CA23C7AF38EAA5E0FC18D29F3A238FF519D + E64882C080DF59874EA4D987EDB33C70D8EB01C0C05FE47330B6E808D0176FE0 + 612B377ADEC343AFB472794890FCA8551908AB4EF07748994F8EB0B242399357 + 59A2DEE7A28B6D1EBA243FA4C85C942D4C80BB0300BAD656D59739EB40E0AFA0 + 6FA993F0B8AD00AF89B40DF29A681F6AC726F579B8B501750589A9732F5E1373 + F6EA21509BD7B3AD4B04C1EEFD8BE7AF1D0300DCF385D9EEF9BBEDEA5BE9F37F + FFDFA4CBDFFE3E4DD8B023F59E666B8A1251C81F3025C2100F9C16C53588F24A + FB9AF0E265DDE30FBEDA3A26CAFB94B58D80AF3F54CB83B6654D24E0AB876679 + B8560038AFC73A3CD4738FF1535C02C13D27595AC23CF79DC883BD2DF02BE0AB + B4E7D817528FB1B6C63469DD59AD3D4B0AD49528C7B5697D4C1300563ED6691D + 2C51D7548F7ADB7ABB090293475ADB93AA3D42DBBAEA509754B685A63E12F5EA + 13FB3E12001E3C3AEFFD3B60740580E383F2B4793AE07EC8ABBA273CA473CBE6 + E265DA4ABBFA91FAB1B1BD500E369C1E03C7A41E760EE4D9BB17BB12527BDD4E + 5487B64D75D27DF5763CD557EDD8C950A9933D0C3B97DBD682DD2DB76157CBE1 + 9EBBBA9D6E70EAF1F4303B67ABC7D6F7E410DF17F809DAAC9FC23F4BB205D615 + 6D831275A8A96F67AA1FA34DC54EE9B6CA5E767C936C95557B5B7F936CAADEB7 + 94B1C3963CC7A8DB759BECBCF5B6BA1ED6AFDEFE4BF4EB8F39C0F544B499D755 + 6CE88F62A37F983DBFB3F658FF2045AFE34719DBD647DCE5CF0D81F53A701B18 + 8F739B76001CF700261C3400B87BCFA7D39061E3D37353E6A451DC2F9EB4DF25 + 1EC08C2DE3DBBC7B1F554D80F3618A63E3B19A541F13EB3A53ECFB30C5F71CC7 + D6F27F3500387AF32241DFE8094CBE5B77BC4A81BFBDBD0FF5ECDBFBDB01E002 + 799BEA2AD99BD587F8CF5C1100E790CFF67D58EAEDE10FE67F387E4C6CC86DA9 + FEA0F4252514B403602E24927B043F86EA63ACCC9EBC4F70B1F28B87D5D9C5A4 + 02C00E8A7F4300ECCA17D227EC429401B0BD06E76565CF3BF8E5DCF2B9B48FBD + 5FF1357EA99A8EFB205537AEA22EB5B2D759BF4701C0B1FD416A1A13EB78D20B + 0F605207BA56D726EA6AF5717CECA3494D35B9099319DFC3B7A89ADC943EB9DE + 26508D6DD4B74FB2EE9B6C6962A53CE34A9FFAC4AD1A53FA30996CC15E4439D7 + E1051C27A3ADB41DF83E61C746DD6CE2D9C326D88CA1DC8549A7D53121CD1359 + 9E54040833996D57F7A780C04C70F3313511CE00B8F55AAAAFEF01AC09B5423F + 5713EA328167321FFB6A821FEBE455AC7CECD7A438D6EBCA6203B158890B9AAA + 6CD2D3AF2C847C7135382FB8BA99BA06B8AB32A98B0596A5DD4B2A3910A69F2F + BEAC0DD8EB791BEFA2CE166CA61E805D972D0A5924BA2666593D65C623F5CB2A + E3C242B39EEF3ECCDA81C0B630251510D6423696B5F08D8A75E459386B618DBA + DB58C41EC0805C09802BF80BDC95577084BD12E508801957D5D307F05B40F07D + DEC0930A0436793868D26294C060D163C2F4D47D1C868696C7B04360F612C670 + 413F2B578017C3475104C93AA6B7BD30B70D14CB50E2F5D44DC9505765D79422 + 0C34FEC43E1EC101FE1202DA441A25188CA7304FF5038D153E5A1E041500C618 + 359B3073A612021A017D499F9EB9C4051C7EDACA4F51A62F90B8006052C0309E + C10A033D6CF1DAF4CCC235696829470D418B32F0C54827A35A84BFF2001E42DB + B28D69D8CA6D69F81AC240EF4ACF98466CDA9F466CD99F866DD89B9E59BFDBC3 + 420F5B47FBF6347CF5F6BC5FDCEABC579CEF19B76EAB8DDF989E59B9CE01F0F8 + 4D3BD304BC7531586ED896C614F88B048281BFB48FB2B2877F0614D3C7523770 + 16308C9193341A4165F0243C34CAA1A1F764D8BBE760153E11A3AAA0309EC01E + F6B918743D2C3400D8EA73C868F6053E987C5FE0027B9F3D687DCADEC0F2087E + 9E3D7E5F3AE4B0F78597302063483E96A61ECAA1252B03F28B876CCCCB761CEB + 7B98F094D9EB17183CD5CA7814BF70287B08FB3EC247AC7CF4709A76E2589A71 + FA649A09003E73D2816F0740B7805F87C3A51E400C009EE310F84C9A790EC33A + E0F8749A751103FEF934FBD2B934E3FCE934ED8C8D374D072A5BFFB997CFA779 + 84F22CCA7B3BE2E50534C01B2CEF0F3CFBCA19870B820CDA331848ECB0B70060 + 0F176A29A2BE157214A890C34B730C80EDF2775E37017F5F4FCBDE7E2D2D7BE7 + D5B4EADD6B69CDFBD7D2860FAFA7FDDFDD49C7FFEECB74E0FB0FD3BADBAFA615 + EF5E49ABDEBB66FD5F4D4BDF2104EA6B69898D5FFACE1BEEED96E148862A4B6F + 5E4F4B68B714A892C3455BBF028457DF2E90173803B829508830AE823A159C79 + 377BF2018C96BD67E5F75B5EC3C0DF55EC077AFBADB402086C024CADB236874F + D47F6C7D0BCC7270F57106C21968E57C2C0B7C914698A5BEF206169803B25176 + 8FC80281016DB49317D043849A0608CB63581037A6CAC77AE02F225F877A80BE + DC2F7B9F0A245296041E116D6A571E0944CADB3503CABB69EB0F9F1790FBA187 + 2F26F4B48777B6D722B4F17A8E8BD7303013586A7D14C21A68ECC7F8FE332B67 + 08EC61AAAD1F62EF5D85A6CE7B06DB7BE198268E8307AE03E0727CC2577B9D1F + CBDED3D7772B0F600F016DE956C241F39AD6B68DD0D5A62D5FD8797C71C7EBB6 + E3116CEFC1C338730C3B7FBC6F79EFC05FC46720784B3BDF1D29657D977C5E7C + 06F425AF3E7C6E59B97FFC3CF3B180B0FC7E5A9FBF8E45BB603129DFB5EA56F0 + 2083FFEE748CDC9EE17FF608CEC7E4353FB5D7CE90580F2828EFBF53FB5DB33F + F6FA7BF6DDB937F06D3BF6FB566FED1F0373F9ADF35004FF1909F0ABFF85F50D + 0F5464D8CB7F84FF172A63C87F68FF1FFB5FAFE07F6DFFF9953CE0615A86C72E + FF7F1E2601E65A3F423BF3FF97372FFF7FCAC05E3D1882EAF0D7AF39A5BF5F3B + 80BD443AB06B9AAE458B2D4FA866CACBACEF22F6F135D1467905D7201BBFE4E6 + 6B6995BDC68B3F7D956EFEF7FF55BAFE5FFFDBB4F5F33B69FEEB971CFA2EBB71 + 3D2D7EEB9A7BFDB2DF2F52D86707BC05EC1281C1F7E82DE95C2248BC72C1C339 + 2F7CED6A5AE8D765BB5617D88BA76F0580B98EDBBDC03D7DEDFE31D3EE15B32F + 9EB37BC5257F5088FBC41C2B73BF9973D1EE2197CFA5C5F6DAAB38FF375FB1FE + 44A43891B722B878D6DACFA7B944A13807EC3D91A612098310CE67886861F726 + 20F009BB3FE1B1EB21A04FFB5EBF847966AF5F802FFBFF028467300EE86B65EE + A35919F6126DE3D9833C70853770EB1E1C3D7A057E057863B92DFC73991B4C36 + 79BECC19983F54619E79B86C4F3B0006F40278279826CADB37C0DF09A5DDFB58 + 9EF94904C0E401C01E1525CC77C694F98F1E96F3395091B733772A22FC330078 + D129FBBE8F9D71204C9F91362F63AEE55B74ACDFD1088049BDBC32EFED5B01DF + 027D25EA2A006C29EDCC2B057C2947280CF47568BB684D1AB6C4E6A0CC476D0E + EBF3549BAF12BDC6F326DFDAC4E6B578F90EB2B9AF0B406C75DEC67CB8005FE4 + 10D824C82BF88B28AB5F04C0BEEFAF95058007D51EE64494F1FC75F83B6361F6 + FC35F1302700B8D794390E92171F3A9D4E7FFA4DDA79FDDD3472C506F700D65A + 8075C1D353F3C3A75A37C4B5481DEC0AEE56F0566B1D93D7973EEAA7B590E0AF + 00B0D646F1582D08DC917A4EB4F5DF045BFB017427DA1ACEF7FC35017CC767F5 + 1EDF0E7C49F10AA6AEE798E7530F1300F7C93153D2938479B63CE281E42A2A15 + 6B53D6AA96571BF59E967AF5ABAF6D9BA47571BD4E8A7544DBAAC35F49F03702 + 6052015A79FCFA43E326F2D44975B0FB2055768A92AFEA0300D65EC06DFB01CB + FE31B0652B912AFB89CBEA4A7B8F416352F781C8FA00649F1A5194C16D557E3A + 03E12A5FEC327E7C8EDD640F92CD083B4E519B5DA7F4AD6C43C8EA1C0C5317E4 + E369631CFD627D697395366C5A6A674CB677591BE7E3364060EFF0D4AD1F75D8 + CF72643E5241E0EEFDB1A959FD93387B587FEC78257D18F895642F6CD2A3F68F + 00B7AD3ED81C5DD84A51AC8B364C64EFC5ED9EF619B4D954656B0D924DB7C9BE + DB99D4B7B3310F6BFF25AA1FF3F18F3BC055D9CB3B03C0B4F568D9E70562EB40 + F661B6FCCEDA63FD8314C7348E757E717F7F8FA24A9B14FA3CB61E349EB6E2D8 + 5707C0AE8A3D15086CFDBBC394C8F3F93A6FB1769813796064297B0AA39222B0 + 8CF54D2AFD605E6DE33A531C178FD3A4FA9858F720C5FE7585F6C6F7AC7CA87F + 6400FCB0761401EC5F5ABF1A00DF07791FA2A60FA02E7DD07FC3D21F0F00DCD4 + DE267FCFF7BFEF0A005B5AFDA9CB9FF371E4E3C8EB8FDDD52E0076B3EA693742 + 0FB90C00E6E9102EB0C57BD83D899B2E3AFF82E517F770F3F29B51B871F9CDAB + DC049A6F808FA647B9C9D6FBB8ACBE02BD5676D5CB261DE3417D509CB8746522 + 57D43691296D316D1A5F4D8C8A9A26575E0E00F88193B55AE8E73A006682E9FB + 8CD8249749A9006C06C04C3E5B30D627A436A6F7A051E9A9E113539F21B6B828 + C7EB45A81D26E39C63799D3CBE25ED69429AEB2260BE1F38239F586B325DE493 + 7426CE0500B74DD83B910031791F53EA759CD887D7505A97F7298B1504EC6D12 + 6D02C0A8FBA07126C21FF104AC2DA200B945B96D5CEA618BAAA89E2CDA542E7D + BA5B7F1661D13B580BB2B8C8D302AF6DD137383FD11BDB354EF53D9F9994352C + 03DF087EEF136D65D15A7FC219AF607906C7856D948FA12F10993E2CB04DC0DF + 6EC0E50280EBFBFF0AFEAA8E72DD2B580018A94E9EC0F4EDA3BE135A5EBFC05F + 87C1133B32D065DF606B7BD2C4FEBF324A00807B8C9FD682BE25ED3F7976A57E + 1CCFFABAB1231830901B3FA8276FF500DFA730AA94549251458612F2D1F8A27C + 5D186B047A65B8510868EAE9F3347DA62DCCE0B83CED2FA3915287C0251D327B + 591A3C6B99EF15FCF48CC5E929537F3B1E12140604CB1B78C85CBC16B2A72FE0 + 570206E3E90B00561B75489EC1E407E15DBC705565BC93F7AF0C741EDA8F907E + 94576004DC9E86AFDD9986ADD99186ACDE929E59B73D8DDCBC270DDFB82B0D59 + B7350D63CF38D388D55BD398B5DBDDD361EC5AC202DA71D66E4E6336B0DFEFA6 + 346CD5BA346ACDE63461D3CE3479EB9E3471F32EEBBF258DB5638C5DBF2D8DDE + 6063ACBFF6069667B0D7F971B28770058B2D3F4AC6CD62F894578C8427B0F6BC + 0302037EF106460AA9F8DCDE17D3F3FB5F76D03B7687D59BC6EEB47E8480DE83 + 31775F1A4728682B4FC60BF8C04BEE0DFCECFEE2097CD0C6BFF4724B84807623 + 7386C0006044F867F71C3AF07231305BDB4BC73C24F4F4232753C7D193D933E9 + 90D51FCE5EC2BE87A11DEFB9975F4C538F1E4E334FB13F70D1C9E3A9C304F875 + CF5E00F09913EE113CFBFC1907C3C0606031A20E183CFB3C86F78BC5F07E3A7B + 0C9FB0D7C4BBD8FA63BC9F6FED8B5F7BC53D81090FED86FD10EE730661A32FDA + F1AEC81B18B82B007CC644FDA534EFB5EC69160170F43C133CC6536DC95BD7D2 + D237AFA5E56F5F4F2BDE79232D7BEBBA89F0CFD7D31AC23B7FF0AAE99AE995B4 + EADDCB69D93B17D252D3F29B974D780C2B6CF4F5B444B263E6F0A8AF79A854F7 + 04B63443E2EC19BCE2163037875B05F03810B676808DBC82EBF007D8A3BEAAC3 + 73182F6040B03C82D90F14C99B9870B20E810929FBF18DB406AFDD027FA50CB4 + 721D004B90178F460160DA97E9189F1122D7DA481DE065D00754030A0B0067E0 + 0624CCA19F058101BFCB3F7EC7FAD9EB9B80C11904038633549667B10030D00E + 9027F84B3943C17658CC18F28044A0A360A300237051105265C1CDD80FA90E08 + 998F935F4B3093D7504A1D6372DFEC7D2A582D30BD1908FBC39796DEF5CF8263 + E3651CC723E5F5DA9409BF8CA72E7578F42A9435A18937B0C7EFF778229BBEFD + 2C6DFADA5EF3ABBB69E397F6DEBEB0F778EFC3B4D35EFBC0EFBE4EBB00C356BF + E3AB4FD2D6AFB3C72CC7C51B788B1D777D79AFBC3EE785744ED473DE82BD9491 + 80AFA4CF4B620CDF1DDEB802B248DFA352BEF75CCE9F17DF2F29DF69CEE7EF97 + DF06FDD447FD10C70288C7F182C2FA5D514F7EC55D7E7F4060C1DC026BEFDAEB + 7D66FD3F010253E6F7CF03103C1C91CB6B3EC5339E90CF59D4ADF808009CB5EC + C39B69E99D1B6939DEBAF4FFC4FE07A6D51FD96FFF03F6FAE6C10D7BDD8F6DDC + 6DEBFB1EFF63A0B0FD1FE8F37E86BABA0EF8FFDD52EAD8EBD7AF1305022FB5B6 + 056FBD52451A88D708D22500E0F2500AD722AE43ECDD4BF40244FDC237ED9AF8 + B65D13ABBD7D5BD72EB4C6CE63BDBD1752C23E337EC54D3BB65DEF148D818775 + 48FD21983772C40501600FD96CD7F485AF5ECD0FF7BC92A12E7952AEF528E61D + 045FCE30D8F3760F00F6CE4597EC7A6F29F717CF135DE2A2A597ADEDF2D934E3 + 3C0F17D97DEECCB134E3DC71DF873EF761AFDF53693A7BFAB2B7EFA953764F03 + F2B2D76F16E037C3DF9CC7EB17D177CAF1E369E28B2FA7492FBD94A61D3B9E66 + 59FB0C80F1D113EEDD0BF8E57EFA8287793EE200184FE0EA412CBB0F932AEC33 + C0376EDBA050D053A82F1EBF0E7A6D2EA0C820AA639E50DFCF17B10FF0D81DD9 + DB17B03BDED2F1361FF1B4D409FC4EB2FECFB24D05636DAEC21C451EBF680C00 + 180F5D9BFF48630AFCE541399579780E98CB7C8987EC78780E0F5FE64778FECE + 3F7E362D38619FBDCD3F80CF9A5B21EDF33B7CB5CD0157F100604B0E8191CD11 + E5F5AB7D7F09038D0481057E995FFA03873C50181E36641CF077C862E0EFEA34 + 7CD1BA3462F1BAB64836E48703852DCD7357BC7A519ED7AA4EF51E25677679B8 + 72CE5287BB78F9CAD357B037025F0161CAC8416E194FAAB97A84BFD439F835F5 + 9D3EDF531DD3434197361F6FAF0918EE6FAFC5FEC03CFC3988D70D0F8CC63505 + 6B08D623FE202A6B195B9FE821D7FE28D40375E5E12B10ECE38AB4C6C9703747 + 5CAAB6DE29FDFDF8AC976C9D0500EE35C9D66ECFDA5ACDF2780413F2B92FA19F + 81C0E36CDD47C8E60280DDDB77CCF3A9DFB333D2103BF781BC9EADDF00B780DF + BEE390AD212DDFC7D69F4F9AC893B24645E455EE6C2D1BDBB40EEE4CB17F533B + 62EDCC3ADB1FD82E0F65C7725D0E84CB9ABF97ADDF492300665DAFB57E7DEDAF + 3AD914344E8A7606490018FBC613C5CE41B91B220F882DB612E5056E33FC1D9D + BA179B8B7B08530EAA6C2E26D97150B4C748F536D972A4D807B90DA8D87DEEAB + 8FF6241DCBF2D8A3640B93B09909E622D9AD625BF79837F5E4BDDA31C9F7B5EF + EC99F11D69D8848ED48FEF90687A4F0EB1B66C27937DCD255B1A10B8D475066B + 3BB307A238E641FD50B441AA7C1FC4ADF7F17ED9B659EFD7992AFB67B189BA64 + 6F2D8AF65C856EEECCCEABFA87E9616314C2F9518EFBB03E8F728C96820DBD49 + C59E0E046ED9D27F3B35D9F5FF561423BC4A82BE3104749DFF20774884FB1400 + 1C7992A4BE55F971595CE1778A609B1958AEFB67ADDAFB8F9F49FC5CDB14C747 + 00FB97D65F0E00D3DFD2F8C69B3E8CF821FD33107F82A6FA3635FC48F8E1E00D + CC9F95A733F4A7ECF407F5282A63BBD985B08FDDBC9FB6894EEF7E7633E6E262 + 3784EE76F3010893EFCAC5D2BD72ADED6FFCC2F96B74DF0D24DCACEA65D565C5 + 9BDEC355BF81EAA6DA7473ADB73169700F606BAB261656DF36D950D9A4E33CA8 + 8FA4094C5DF5B6586E1A7BDFC44893AA5ABDDAE224CEEB35E1B2D4EB0AF8AD03 + 609E2A94073013CD3A00CE62C2D92A77B3724F9B98F67F667C1A6C8B1720307D + 00CCC0DFDE4CF23997DF000057136A1313714DEC3591D744FDBE097B506CD3A2 + 202E04EAF9B86050DFBABCAD2C6A9A0070ACAB9E8435D5A16F1DFE3601E0BAD4 + 4F8BB4BAEE5BA0595D7D8147CAC25010389691C35F3C81CB62F34172383C8227 + 955B0B612D5CEB00388A7E2C7E59500B001376AB2F30B62CCC11FB2E01690581 + BB8F6C06C202BB9D016024501CFB7B3FCAB6F8F290CFE3A7B77B047B18B1D2F6 + EC8C4468678C1ABD27CD4CBDAD5EB017008C2AF82B181C8C206EC828C0178387 + 4B860DCBAB6F84C0D13822008C544F1AC16F948C34CACB88D364CCC173380260 + 198F24F609F67D81672FB3B625D66F717AAA8370D326CBBB077081C083F1F415 + 00C600E646B055BEDFAF60AF7BF916F85B19C6AC5E0635446868AFC3EB62C9DA + 6AEF3419E930DC0181DD53632560775B1A51BC7B87ACDA9C9E59B3358DD8B82B + 0DDBB0230D0500AFCBC6C2E12B37BBD72FF077F41A1BBFD28EB366631ABD7E73 + 1AB976A303E091AB37A5D16BB7A4711BB6A7718487B6FCA835261B3FCAF2A381 + BFEBB77A1842424457464C93F608C658E990D8EA3076CADB05CF5F0C9C51F206 + 66BF3BCA1851DDEB77EF4137B0028E275AFADC9E173D04B4C3DE9D8465DC9BC6 + 5B3F20B01B7B4B8AC70F10186FE089FB0EA48907F63B00C6FB17291C3486E5E7 + 0E142FDF1206FAD9FD80DF6C78C6D08C5730E077FA9113691A7B1162802E066B + 0CD8EE117C04187C384D397A284D3F7ED435ED187B0313E6F2689A71EA78CB23 + F8B48DF1D0CFD90B789AE581C2F208AEC030FD6C0C9EC00A252D500CFCC530BF + F0DA95B4E4FA35F702262C3420C0F709363904305580C0F704CEA19FD19CAB79 + 8FC8990E0B80C2D417E0FB5A86C0C83DD1AEB357F095B4F88D57DC436CD1F52B + 6981B52DB4B645D7AFA62556BFFC9DD7D29A5B6FA435EF5F4FABDFBB9656DFBA + 9A56BC7B392D7DE7625A76E3B2E98AE55F714FE165D67719DEBF6FBF961611FE + F9AD6B190ADFCCB045C0451258911C040387A97FEFCD0AF43AB8B13A52D54BCB + 6EE1158C871F9EC2D963307B14BEE37901604160B402788B67F0C726BC1425AB + ABAB02C200B2028529130E1AC9DB31C3DA2C401A6507B7F780BE2D8F4B00205A + F9297DB2E7AFA0AFBC80B3B257663E6E06C2C03B815D52C13C813D24A087E807 + 5C941729005260354255CE476DAA13B8A48E3482CC0DEC47EBC7CB001830CA6B + 4478ABD723E5BC367F9BF7A3E57C39C775F7ACFEABBB69FD17404D793EE7D7E3 + 181C0FA8AB30C8D4E91CF47A7A1DDA10001830BAE19B4FD27AF6FBB5E30381F1 + 062624F4867B76FCCF6FA7DD3FDC4B87FFFEA7B4F7A72FBDBCF58B8FDC2B98BD + 81090F8D37F05687B2F9B320E5F5D88797F3E4753987D8CE7991E73BA48FEAE3 + 78C6D1C677071427E5330104C7EF528096CF893EFAFDE8BB15B8F5CF91FE843D + 376DB0FC7AEBCB3EBD7CAEFE59F31B2DBF058E8D741C441FF55B8567FB4737D3 + 2AFB9DB317EFEA027B81BF0E813FFDC0A12F5057E097BAD57709F7CCDED9B9CD + A1AFFD17D58F319497DE7E27D7DFB1FF52D1AA0FADEFFB6FA725F63FA60C0426 + A56E1920F83DAE016FA735366EB58DC70B17E195EB5EBB96B2F7AF7BE972ED30 + 2D20FCB25D5FD87B1CB997AF89074BB8EEE8A114E06E06BDAFF9832888760031 + 7D4809E54C4A3DD731AFB7310E901DFEDA75C95407C03C5C931FB069815FE40F + E0940779107907BCE1BA8EDCDBB700E059763D9F75F1AC877C76D93D81878788 + 3011416EC799530E7E175CB5635BFD6CEB37EB82D59F65EB810C803BCEDABD88 + FB888D657B82193686ED0ADC8BD734FD24DEBDECFB8B87AFDD17D90FDF75D2EA + 4F791868F6019E79C6CEC1FA4F3D76CCEE8F765F74CFDF2CF6FA75080CFCB57B + 6ADE76213F5CA5B0CF92202F79EEDB78F7E2E9AB07B5F2BDDCEEE356478A80BE + 3C14D67A308C10D02FA767F7BEE40202CBEB77DC769B4F007B0BE805FAE63D7E + 4D80E06D4428B1E39814067A9289C825E3F0000EE221B718B299BC3F08C79C48 + 79130FCE69BB0D60F0789B07D11F48CC7C88FD80A7BF68EF7BBFCD4D384FEBE3 + 0FD559FB489B8B8D20ACF32AE67005FE0275F1F0B5BC87712E5EC01E45C6445E + 503802601E228CD05710587900B07BFE2E5AEBA0777879785102FC8E586AAF51 + 00B0CF5F4B349B2AAA8DD50904E768382DC85B9700302980D73D7C4B1958EB1E + C026F22A0BFEC6793C7377C2403F05E89D9961AFD4B76381878326C5E39770D0 + 780453879EC2F3D7E4EB83B07E603D2170ABB585AF55C2DA85B4BEF6898A6B19 + 07BFD417F01B0500F6876679C816D93AAC0F0FE5DA5A0CE8AB3D7F7B9B9E9CC8 + 83BC59405F49DEBFE401BF236D8D32CCD62DFD6C6DA7D0CEC0DF0A0017F05B07 + C00F92D6BE5AFF6A8D1BCB75C5B194B54ED6DA59A93C7B11793C82E3FA5BA9BC + 8101BF4F8E78CED7F25AEFA326FB406C479463BF087F1BED0B01000BFE761F32 + 36F518829D624C1B00CE1AE3D017FB897BF9027A9FCEF037DA5BEAF6179563DD + A348761DE551B419C93654D71378E1F6A72DABEB53D8A2B04BB5C0AF24A81BCB + B15D75F57A22E561E71A63BFC9DD87CEA6ADFB4FA4519367A72E4F62CF7B26F5 + 1C3032F520D475B1D735AACF50B7F53581DCA6BA0729DA149BA4F6CED4D6D7FB + 17DBE8630060D943EFB3B396B2ECB152B4F7C6FCE3EA6163653F7EB47EE40714 + 35B53FCA79E6F1759B79935AAFF56035D9EC9BF4B8FDFF1AD50880057D1F0280 + E58C5845A4ED9EF7F965DBCDBC0D678B2B559CE971795C617802C06C6D5A71BD + 7FCEAABDFFB6CFA433C5F111C0FEA5F5AB01F0632900E0CE3E8C58FF2F44FAF3 + E98F27F10705FEB277AEE74D6AAB1FE3D1D5D7E16E7F9B9C0C7A6642EACB936B + 3C0D64170E3C80BB01D8B85872C1F91BBF60FE5AE9A61155BF49E989A5FB6F5C + 4D37C0CE55BF81EAA61AD5D426F8FB9B00E05A3FA982B8B5BC8B6395BAA8D8D7 + CBB50953671330EA34C9D344CFDB3A01C04C1881AEAD0924F5D6CFDAA8D704B5 + 0564519EA456B28969371B07ECED63137C07BE7DED98760CC23F7B0868CEF701 + 00B85D2DD09BF3B91C27CB7500DC3E51EF7C128F62BBC6A3072D24E26243FDE2 + 78AF6F5BD464E05B2F4700CCE22842DF1EB6788AE0B749B10FF9588E8B312DCE + 9AC4E24DFDB4C0535E00989445A3C0AFC35FDACAA253D2A213C57A87C08805AC + 490B5A2D629116B69216C7D522DA16DAFD6DB13FE885B96900B0733C00D716C7 + 635A1EBF00DCEEA3080FFD7C230026ADE723FC8D52BDC41820AF7BFB027E6BF2 + B66767A67E766E4F3D3F37F52DD057B0973C7D04820580E9DBEFB9396EC4E865 + E30572318CC8382228AC32691D004B4DE526D8AB7CAC53BDE0AFBC811D02873D + 84A3F1A80580D132CB67002C0D42562FF88BF72F7B0323ED138CF0041E8AB10C + 105C0C6432864932A6518FC14C1E160E8781BF8B56FB5E6BA40AD987916FA41B + F536A76126E0EEF0555BD290951B1D020F5BBB353DE3DEBF9BD308BC7F3116AE + C8E346AD2224205E21EBAC7EBDC3DFE1ABEDB800E1D5B91F790F11BD666B1AC9 + B18B01718C1D738C1D1B4FE131EBB7A5711B77B4B4297BB58CD9900D9C40618C + 9B848B766DDD9D266EDBE3223FD6EA30888EB131EC7F8780C0A4BE17F0AE0395 + 419550D11377119A717F9AB00B2F9D3D692CDAB9B7F2F691B2D7CF8B39BF7FBF + 7B014B930E58DD3ECBEF7FC9F7032424E4A47D18933304C62B18004C3B29E017 + 008C119AF27326BC96F004CE21A18FA5A9478FA469C78F38F005FCBE70F8E5F4 + FCA1973DDF71E258F6063E7332CDB094325EC184D374007C3A036040B03C81A7 + D8B15E3876D8C3460B0A93562A1078FE958B6ED8C7E08F6718F077EAB9930E03 + 1C1C98665FCEFB4366017CCFB9B7EF1CC0C1D5CBAE59DE8750CF05FA9A16BC4E + 58524B5FBBECFBFF027ED1FC6B849C664FC98B69A1B52DB67ECBDF7E2DADBEF5 + 465AF5EEF5B4FCC62B69C5CDAB69D57BAFBA96DFC4E3F76A62CF60406F06C0AF + A525965FF496D5BF7DCDCB1E1A1A80429BA54B6E58DE014C06C3EC1B9CE14BF6 + 1806E608FE027E1DDC14D813CB80A1957810BE8F6720E1606F555E8500A41C5E + 166FC237D3925BAFA7A5EFBD5E40B0B59996DD7ED394C3442FBFF38EE5DF4E4B + 3F78D3859730206C2D216F2D75AF61C2D97E74C3E1D8A62F3F4C1BEFDD496B80 + 5CD64EBD7B513A2CCEC098321E950039001D809230BBBEA7EADD1B965A7B81BE + B4AD031C3AD86B85E615E84382810E96AD0D45B8A7318284781C0318018F8048 + 20651DD0F25ABC3EED88BCCE49F0527053C740196EE654C72455FFD82FF7C91E + B68C59FFC58726EBFB2590357BD3221D5FC7AC435EDA382F52BDA6C6D2C73FE7 + 2FEEA4B55F02C2EDFBF9EAAE8780DEFAEDE7EE01BC9ECFE3F3DB69E7779F39FC + DDF6F5DDB4FEB30F2A00BCC9CE85EF75EB571F9B2C6F9FB79FBF7DB6FE5A767E + EC0FBCD1EAB602A101DAE5FB12F4ADBE4F2BEBDC74BE1C8BEFC73F033BA6F27C + AF82B2D421817FDA917E036AABC6E209EC6199DF4DEBED3786F0E0E5E10479AB + E7DF207D0314B636EA3DECB39537DA711893C332BF9DD6DA6F7CDD27F65A1FF3 + FBB7D703023BEC0512DF6AD57D62E7655A73377B082FB5FFED62FB8F2D01D27E + 68BF6FEB0328060653B784073A6EF17F24CCF3DBFE7FC5E377217B8E7F80977E + 06C0D42DB9C9FF9D6BC13B6903FF31BB4EE87AB186EB835D0F787084F2CA92E7 + 9AC23586FC2A7B2DFA0070E7BF8157EF356F235D6CD728AE530BD9A797BDCF29 + DBF569D15BD7D2222222587EF15BAFA60584707EDDAE8F56CFDEE6D423F25CD3 + 160387CB750E00CC033500607F18E715C2EBE7EBAEF6F0F5D4AEC7F2F6259FE1 + 6FF1F4B5741610986BF655DAED3A7FD1DAD94BFE0220D8AEF1568E02E4CE3C67 + F791F3E7D2EC0B78005F487369BB60F5E74FA51976DF00FCBAA72F0F249DE1A1 + A3D369E669BC7C4FB932FC3D99A69E00FA1E4F2FD83D708AC9E16FC94F010A1F + C9C077FA899369267B025B3AE338D1338E7978673D40C5188FA201FC2DD057DE + BD405D07C0DC6F8BA8A3CDC33B977E2EBB57E707BF5EF2FEF20606FE8EDEBEDB + 1F18CBED2FBBC7AF7B01FB036596522ED037AA02C05B78506DCF7D9A0014060E + 5B9F28E62BF2F695C76F9567AE03FC050603874D40DD6A3B0D2B0F5FB3C9C33D + 739C49761EC0DF49765C3D5837DAE65FA3FD81BCACE8D18B047C95C78B571EC0 + 0E7DADAF3C7F9BA4FE5E5E62791E42B43929516BE4E9EBF3D232978DF59AC776 + 067F110018988BB76F3DEC7304C0F2EE8DF502C21100BB377099BBD71FE444D4 + F97EC0A63ED3E7BB3730D0D7F31D0B523F07C48B3CA5DCB7C3D60AB65EF0B541 + 593BC43504EB8EB67AD619AC45A80FF96A8D63A9D63E02C0BE4F7029CB4338C2 + 5FDFF7D7D6518061ADADB446EB4BDDA40E07BFBD27D8FA6DBC29ECF78B22FCC5 + FB973E405FD6974FDBB19F1CC7BA903566F6EC6D92D6A7CA6BCD2A501C81B1D6 + BD92D6B8B12E4AC7569FB83E46AC9FBB0DCBEBEBB8D68E21A1B5F6A68EF62E83 + C7F95ABEF7F0C9956D8032AADB03A22D40D2987A9FBABCCF20CB0F1957D93748 + F1FCED31646CEA5900F07D7611DF873783DF1EE405814DB2AF44453BCC83FA75 + A6680BAAEBC1EDC35397A78655EAFA74765A68B75765D5E12E8E14A8A95D7634 + 0FFBDCE719D758FB6FBC7CE66ADA7BE4421A3D794EEAD6D75E03D04C48677B4D + EC66D8D264C78B92CD2FDA00A5A6BA683BAC4B6D8FD2DEA4B6BEDEBFD83D1F05 + 0063F70C76D246FB2AF6534BA37DF72FA568476E6A97DAE1EFFDAA87666E49C7 + 68AF97ADFC812ACE67BF95AAD76E68FB5BD1AF01C0287BE4967CE14BC05F52EF + 53F85BC5981E97C7458EA7B1F5BA7F8EAABF7F951FA4383E02D8BFB4FE2A0070 + FC40EAF5FF82E47FBAF2E78B12FC15006E1AFBE8EA6B17C2A7536FBB61F7B7C9 + 4B2F405789250FF4E542EE171CEAC2C5E7CFAFBFAE8B73BC6121DDE4E24DCA6F + 287F2600ACBAA69B74AC13FCED6AA2AD9A68206BBF6FB251745F7B535D110017 + 55F9D85EC6A84E7DEBAA4F8E3A9B80698217277AF53E5EA7C9E2FF9FBDFF6AB2 + E3C8D63641422B82200401128422404265426BADA9B5264068ADB50641CDAA3A + F5D5774EF73763D33DDD66DD7DD16D366663363637D33733BFCD673DCBFD8DBD + B667ECCC0448D6A93A87176FBAFB72F7889D5B44B8AF2796FB100058035606A8 + 9DC16A070063F3270EC7E5CF92636BCF9151CF66A04B7B067F430160A280B33A + 0364ED0DDC296709003F8D22008E9309544F16342150BEAD6D63AB0070AD66F2 + 6393A11A00D7705752DD70544FC634B9535EE5676CB2459E7671A207E06D03C0 + B27B5D944D2E99704A9A90D6F6B126DFB7C826B5F58456135FE595D276A24DC6 + 9998BFD0B7D19FD2D6E47CEC1C9684B689FC1CB42C8DB73CC206C81DA3FA5A05 + 000B0237F037E463FB09D66E22A0F795EC60400DF86D9C0E19F83EB778ADA702 + BDD13111DB52A7B6383432ABF3880000FFF449444154B404DA600058793950A2 + 047B23048E00B8566D8F8E9BB81C74236CAB3ACB3F47B114DCB4D5DB5C0DFC2D + 658F0A2E103802602D05ED0078FDAEF48289749A95E53C532A479AF202C0332D + 2582983DD670C8017F114E3C202E911B2F15875E76DAB11CF4EBE905D38C9D07 + D20BBBCDBEC7DAE1500402B354A0D981BE2FEEDC975EDCB1CF8E9301F04B7B5F + F7E85FDAB02C74761C5A1B3B364E462D254879EEBE771D0AAB8E72A303EFA5F9 + AF7F607A3FA76FE6A50E0580179A70662E2C9A67F58A8E79D15E23792D0B0D24 + 06040387A977FB0744DF64F912D1EF7E98E6BEF7515EDAB12CF1A8FC42DF3398 + 88602070470B3FFB2C2DFCE4B3BC2CE4472C15090C56E46FDE0F1847B3EF35FC + E9976663BFE0CEF29300605FA2F220FBFF1E49CB0F037DD1E126F277D9E18369 + E9D7873C0F1406FA7A74B0D5F71F3B9A56B234B4D900C02B80C2A7B200C188E5 + 9E11368FFA3D7BCA538F0C3E093CCE4E7A4507AF3E7FC641C1CA8B2CFB7CCA97 + 77A60CD8EDC7F17FE9AC0360525F16FAEA85B4E1FAD5B4FEDA95B4EEEAE5B4E6 + D2056B4B9419116779096807C1BE67E485B491A54A6F5CF2743DC0A2C05F0414 + CECB40DF284B3C0375AF38F8DDF988FD82AF3BFCCD00B844FB0259EE0251AEF8 + 9E98E451B3CC34F024A801C08F6EF93E9A023A44FC017C370159383F90E7BB7B + 1E05CC3E9E88C85F01E01D8FEFB98818247210C0B4D5FA037FB73DBE6DF99B69 + CB233B8EEF154C24B0B5F3E5A27364B02285694FCA7EC22C190D08234F1BF617 + DE8BCCFEFA5FBE4BAFFFF9BBB4EFA7475EA60DD1C17B006E610F54A286017180 + B97D7FFED66D089B2F2B0D840308FF9C97A43EE01013109A619DE01FC04F90D0 + C1728185C04095050963A4673E463E260012A82A988A0603C0D40BC0AABDDA21 + 414F87A42601596CEA2B5B6E439EC85F7B0DFFFAA7F4D6BFFDC5EA7FB6E3E4F3 + EB1C02AA12751C879465B2796DFC0F081BF2D7F85F39B7FDDF00663B07D1BF00 + E0B7EC5CFBFEE5FBB4D73E9737FFFA8303DEB7AC0DD1BF6FFFEDA7F4DEBFFEE2 + 29752C110DFC7DFB6F3FA6B74C00E1FDF63EF3B9907F9DF75365FB7F10609828 + 6E457223BDEEA8FCFFE4CF489F6B9D477CA631FA5B9F3DED28BBDD6C0EBCFF85 + A8DC1C99CEF78EEF54F3DDB2B2EC07EC7F7DDDFE3FDA5387FC3B6ADF5BD237F8 + FFED3DF1EF7100C0005AA27401B32CDBBCEF17B3FD68B61FEC3B6776CA07FE6C + C7FE8BBD2F56BFD37EB75BEDB7BAD944C4AF228501C6DBADBC8D873AA823BA97 + 0738EC38805FF2A42CF7BCED117BF8B2B4B3FD4649ED37BDC77EFB005DAE0D40 + 5CBF46D8F100BCD451E6DA81B8A660A73D7900F0C6DB971CFC02883D8AF70ED7 + A41B0E7FD92BDD23752DBFE956D6D6BB79197B6C1B6EDAB5ECCE75BB3ED975C4 + 6C2CE9BCF5AE5DBFEED9B1ECBA86B8F67503601EC4E92CBD4FDE5763B892237B + D9B3170902037AD7DBF57AED651EDA61EF5F1EF0C16EC7BA60C73A7F3EADBD90 + B5FE92F5BF7239ADBB68EDCE0380ED1E50EA569F65EFDED369F5993369CDD933 + 96DAFDE5F429BBBFB0D47386BF805E41DF95668FF097BD7DD9C777C531C0AEF5 + 33017497B114F3D787EDDE08E4FDDAEF8D94FB8ED8FD0F606C76EE9D79C9E723 + 69318037DC5B79008B87B2B8EF720FD683589415F53B20F21715088C14010CF0 + 6575105F2DC494570EB1F1848D2388F6659F5F96727E8DFBBF29EFE96B7551EF + 98DE6695925ECA2B9A904609023376D1F8C523820FBCE31097259FB56A8AA488 + 606DB1A187E7107D9AAD3700C0BE824B9643E012D58B66DA388DBD7D1DE21618 + 2C017E6913A37EEBC85FFA7A7FEC5B4C05E46A5CCA585530378E5749E378368E + 6981BECFAFB571B269CA9A6DBEDC324057B0170908CBD644F716008C2200966A + 008C3476771B0F7A16303C19E06BF29465A12D2542181BE567FB4933F805004B + 710EA1B94553EEA1669EC3DC86B94E90A2817B01E08936878A73ABEEB9167333 + 5669CAB05791BFB209FE3602E8DA9C9294A85FE4B6053C306CF33FF295046891 + CACC55C90F0700B7496D48756CF2529C2B0B0033AF8E736D415FF2B18C00C09A + D36BDECFBC9EF93D790160D5D5BE01B5C12E618BC2E66DA65297FD1B0E7FA7CD + F5FD7BC7980DD5005800B756F4B134FE95967294FC316DEAF870DA7D42D4C563 + 0D6CD30D7FBB0130EAF8C890FC542AD700384ABE34B507004FB2F7EA55FB6D2E + ECB3DFA07D062327E555F03A5BB00D04C0F2094AD10738982DFA056BD57EC3C1 + EADBD4D5D6DB17BFE7D30060E55BCA82C051B5AF37DA7F0B0DEF98D9F7DDF16B + 0F947CE36D756D92AFBC51E54377FD018007E8D702E05A912DB9ADF0B786333D + 298F13C393DA6CFF1155FFFF2A0FA6D83F02D8BFB7FEFE00B8E5CDF8430324F8 + DBA6B6F64FA6C91EED3BDA6E52BED4F3483BAE5D389A8B6481BF5EE6E253CABF + AFB8384B6DF5BFB3AAFF51EF85D400E00AF676DDB48A2D2BDE4487D6D3DCD8E3 + 0DBC195444C91EEAD5B7AE731BB2418B03DBAADEDBC8DED25F36DAB4A91E1CF5 + 1A80698017077A5E57065CEAE776D206FC4A660F00588353D419B0667B1E8C92 + F2D4A21DDB0677A3EC9C63680B044602BC2C955ECEA101ACD419444B9D73C508 + E0A836001CC1EE508A11C071F210DBA85CB7956259F981CB1B758BC98F264775 + 04702BD42DF63136C94275BDEC284EC66AC53A00B0267D9AE43580B747B94B65 + CF6020B0269B9A78B6A90D00C7C92F5219A9CD389B08B3FCF3734CFC9B25A099 + A4036B6DE2FEB24DB22D9D34AF3F3D3B7F85A52B1C048F251278F6524F0587C7 + 5ADB9CEF2BEAE4D9F757CB3FD751C32A8FB3F3E364E80580057F656B13CE0A45 + 0123A28759CE0C27471DF11B15EB70A0B04F2F5244B01C2E35FC8D0E9AA85847 + 7B445E00983AE0EFB4955BD25413D1BED1711495EDDDE077068017D06B02F822 + 20AF835EABF3A5A04399FABC4FB01D2338D0E42C938D3C4BEEE178F3E861CBE3 + B09BBE656F0380A3036FD6B6FD6906EDB758DEEA5ED87120CDDC65DA6D79D3CC + 3D6FA4D965B9E79770026EDF6FDA9766EF3C905EDEFD669A6DF52FED7DA301C0 + 0E7BAD1D72C8CB528366033A7B04F0BE779BC81301603923C9CFDFFF5E5AF0FA + 07E995373E1C0080635E65226200C12C7108E405FA92E244C5B98A239574C9C7 + 5FA6451F58DD079FA445440197BD82E7BFFF7101BEECE5971DBD2A2F72D09B81 + EFC24FCCFEA9F21D00BCF0C32F3C32283B9389324221BAC884239A28A5BEAF8F + A665870EA7C55F1D4AAF7D697D2CC5C19D1DDD072D3D9421EFD1BC0C74CE0386 + ADDEEA961EFA2A2DFFFA5006C5D859229A68608F083ED64060F2CB1D0EB3EF62 + 51A95394B0228359E6938860A2C2569C3F9DFA4D2B2F9EB5F2B9B4FAE2B9D46F + 758061E06F06C465FFDFAB971D22ACBE4854D9C5B4AEC004F24499B1B474DE77 + F27CDA28086CE9FAABC065B3597903CB9712057C5B11BE0013404806BE80E0AD + 44F90A009BB6581911FDEB4BA722EB2F0994480E87EF5F775803C0D9F7FD83B2 + AFE62D873AA42CE9EA4B3FD306D063D27E9E00600011C048D18294777C7BCF41 + 13E017989BE12ED0E956DA69E99E9F1EA4DD3F009BF2D2D0D40B00AB9C9790CE + E05730580204B3BCF41EE0AEE53D65C95C8FC44419007B7425B00D805780DBF6 + 1FEFFBDEC18038A232771329FCD3FDB4F5BBDB9EEE03E49A007D08E8070014F8 + 13F005080253B16B9F61DA62CBCB3EB3B76E06C711420AD60223055B63D46A04 + 98B4AB236DD58736F4D37110ED053A4905801DCE02A4CD4654F2BE7F21AAD65E + E3BFFED9DAFD60FF5386BA3A0E7DF51A28EB38940559F57FD0B669E391B4D6FF + 6F3FA5B7FEFB0C7F81C044FFEEFF177BDDF67938F4FD2F3F997E7408FC2EA0F8 + AFF61AFF62AFD744FE5D87C12C176D36D35E7B7FF7DBFB0BEC7DC3FE8FBC3FEE + 4307C19489DC064CEF24AABBBC3E5E178AEF098A9F257907E265F96E4548EB73 + 46B445EAC767CDE7BF8BFEF6FF00773DDADCBED380E008810580F9DE69AFEADD + 6623051E93DFFEA37DF7AD0DD1ECBC1F805FBEDF02C0805F203070170183D9B3 + 372FDF4C5BFB9C7EB13A13E9DE9FED1C3F3CB2DF1FD1BEF65BFDC1DA02914DEC + EFEBFBF9A2EF1EB876A2723CA2F8B73EBC9336DF67CFDE7B69F7F78F725BFB4D + 737D00E8EEB7DF1DD708C160802ED70F445EED04841DF6726DF2659BF3CA036E + 23EAF7CE75177907C037EDBA55603036B4D16C48ED6567AFF3EDF63AD9E33702 + 60AE915CEBD65FBFDC3C6C2300CCEA0D7EFDBD9AF7FB4519FE0286B15DC9C0D7 + E40FEE5CB0EBF125BB7E5FB46BF785F31DD86BA9C3E000851DFC9EB17BC69933 + 0E7F81BA405EA27CBD7CDAEE152CDB6CEA3F41D42FF559B4F5BD7D1DFE1E4B2B + 8FB1C7FD89B4F2B8B563E967BB372EB1FBE1D24387FC7EB8C201314B3CB36286 + DDF3B847DABD5300983DF44997F0505581BD713F7EF2D8F52096C0AFD24605FC + 027CB5D72F625590BC1248B6B15DC4BCF73E2A4B3A7FECCB376BBF5F520160CA + 4A89F225DAB706BFF3DE02F27ED8005EC15EC62CA8B69127AA17BD7C20AF8CA2 + B191227B01BC882D36FC013AFA9695545E62FC666339C6712FD36E4FDECB5763 + 30C6681263345FDA39C8C76E95E21832026045FFCE62B9E722A27C354E4582BA + F51896BC6CCA23DA4A1902E7E85DC15DC0AFF6018E11C082BC6AABA8DF087F7B + 01E0A6CC1CC024BB47035B9E28DFC9ACFE032036B11210B6679775E6167A4054 + 73087F80B5CC3D487D2E328498D344E0CB3C0745003C60EF5F87BCECEBDB3DA7 + 6209E80936578CF097BD7F256C02BF136963F3B9BC955006C15AF699BCDB2BA0 + DB068023AC25559BD8B69EDB4A9AF7D6F53A56AF7AE6C5BE8A96CDA9EB07AE35 + 0F57394A737DE6F4C05FCAA4D8E4238820B899EB97FA08806B182C9BFB159EA7 + 8E07CC33FC75E04B446FC9CB0FD2F8498AFFA406C0F853A41815ACF6EE6B096D + 7A29FA73B266A7679EEDD8DBFA489D3E2F0E80BF239E9B39200238FAC86A4520 + 1CC170F4ADB99DF3E2579BF8427A069FE904B657B3FFD5DA0086E31EC00DF86D + 81BF28FA0007B345FF612F7BD450F5515DC7F33EC5DF395C003CA6F8466B9FA9 + CA25C587AB601EA9974D3EDCA1A47D7E6BC9671CD5D60E0D07ECFE7B03E0B673 + 44C576B1DF3F9B7E6B008CBA007051C399860B3351049AFFD914DF83B67C9B62 + FF0860FFDEFABB03E0B637E30F3D9522141E4CBEF76F5759178D2C96801EE537 + 026E1A76211915607075116A971DC7D556375C712EA9ADBE5B7A7D4FA2B6E3B4 + A9ADAF6E72BF17001E4ADDEF4F56577D9BEC33ED92D99AF6555D730CF236D070 + 681BDBA84F4BDF6857DF5AF5E0A8D7202C0EB8EA36E435F86BFA30786CE02FA2 + 1C6D19F24661675F5EEAF444227D469A7D8C0D867DC9E702850592D927D89785 + F6BEDD0078A0E239C9E7721C306B70FE24D0B751E913C1AE06FCB253D624037B + 9C74281FD5B4EF0180A30DF02BD500780C13354BA55EF628EAC6DA448DC9DB60 + 623247AA499B6CA80DF8AA4D9B9A3E44089749667CEA586A26A1E4CBE455D284 + B74DB41DC3A4D726C368B4E547D2C7F2E3E765F83B6EB64DB40B009EBC606586 + C073FBD3C4B936C1B7FA8956876843D9D322DAD0CFE1F102EB4794AFA513AC1C + 01B0C35F0032FD0AFC8D4B40CB0941AABAE89840C0DE988FF52C652627873B40 + 0AF4957094C427E749057EB5AC9A1C2E02C0929C31757D0D80E5D489D1BFE401 + C00E8181BCABB70E702621CA1100037F5F20BA17085C60AF227E518C005619F8 + CB9EC179AFE09DB91EE05B39CBDCA146C4C5C63D0E8FA793DFBACF01F0749680 + 2E0E3AF22F980DE7DCACCD7B7D6F365FDA0F78BBEBF5F4E29E37D2CC9DD67627 + 911F6623A2D78EF392F5C5A9F7B2B50500B334F42C40F1AE1C410CF4C541381B + 60DCE4DFCC2A4E4724E83B6FFF7B69DE81F7D3FC20016020AFA27DE5EC1404C6 + 468A3394FCF6D317D3B633971CF4CA690A0846006007C3EF7FE2D1BFAF026E2D + 6DF4F117E9958F3F73F85B2F07EDCE60EC1F7FE25A685A64361CC61E254C04D0 + A7D9991C9DCB389F7D094A13CE69DF8790885FF6FBF528A74369F157073D5D6A + 5AF2D517967E95FA8F1C2E3A92FA0E7F9D65E5E547E977302D03021F3EE47B06 + 1321BC92E860A2818F5BFBB227701FB272FFB1C31E290C2426D217E0BB8EE53D + CF9F719107FEAE3C7B2AAD387F36F59F3D9DFA4CFD67CFF8BEC0ABAC1E3BD0B7 + FFBCB5B11408ECBA70CEDBAF3867C7020017A09061301164E7D346CB6FBA7A3E + 6D22E2D70598B0F35F3993D65F3B9736DEB4FA5B971C68288297E8B6CDB78994 + BBD2EC1FEC90D7A1C765B35FF248BB4D772C8FB0599BDC4FF03844053B98B9E6 + 517A0E780BB4A10CC471B0F328C31DF60706FA027FD9EF978860415F450C1201 + BCFBFB8769D7F7398A31C2DC9DDF13710810BE93F6FC78DFB59B0842AB27EA57 + 91BF946B09880182C923F619E69839F2D78E075863A9DB9FBF493B7E7AE4A04E + 9016A8477987B5DB6E52C42EA2EFAE9FD91FF54EDAF9A39DCBDA011777984DA0 + 3742430140C15ED991DA0A0293C71601A4F28294E48196C05C14A3586B201B55 + 1F13A96D3C3E368F38FEAF00602258BFB5F7C8FE77A2A7FFC56C00E1BF66D82B + 65609CCF2DF81B8F8D54D639D89718000C507EFD5FD923F8CF1E65CC52D30758 + 0EFA6F3F7B042F0018D8CBB2CFC0DE37EC751CF8C55EEB4FDFA4D7FFF4AD97D1 + 7E40EA2F8FD28192527EDD5EBF035F7B5F2587F57CD60E6B3BE235F2DA78ED7A + DDBC277C4E7CB6DBECB3E6B3E4738CF057DF8BF899CB467B7DFE0E81ED7529C2 + DC9716B7B2003079EA1C0A97EF8783623BB77F57EC7DE7981C8725C9590A9BF7 + 450F39ECFF93BD06FB2E037F81C044F9EEFBE55BFBDE036F7928C2BEA73FD86B + FADECE4BFA9D9DEF7B3BEECFD6EEE71FD2CE6F1FA56DDFDC4FDB1EB31C3BA0D7 + 5EB769C7B70FD3F6C780607B2D5626BFED9B7B0E7EB73F22FA97287F3B87F5DB + E96D2DB5DFB5AE0D6FDA6774E27FFB1FD3D7FFD37FF36B0470170176A967CF5F + 4504736DE14114D20C7F5981C052DF9F9C259B89E2BDE9297057A01709040370 + 95A71E984B9E7E28B7D735927C86C702BFF4F125A0AFF0404E7E784751C0C8F7 + F8BD981FE8C90FE8E448E03517CD7E8EE59E33E015FC75B07BFA94DD4B4EA455 + 762F58CB433EE7CF3751BC00DE3567ED7E61F96547EDFE72CADA9CB3739F3997 + 569DA13F75B405FA929625A01DF89E70B04B442FEA3B92A37EB907BEF6E5577E + 3FC4BEE6949DC3040CC6E6F7CEC3C72CCFBD93A8E0E3766FB47289F2E57EDBF7 + F5F1B4FAF81917FBEFE76D19F2BD9876A4C060A0B0836113F7F17C2FEF805F49 + F58BD8E681ED237CBC409A977C06F4CEB53188802F629F5F4F7D89E70C806B08 + 3CCFE16F1EA3688967F211FE62278FC813BD2B017581BE1E0D6C6D49B1037AD9 + 5A63DE01B3A3FDEFA639FB78D8CEC669ECEDCBB8CE04D45544AFC66602C02A0B + F2FA837C05027B0470C86B2CD9058369BFED80EFEBFB928D1349192F12E10BE4 + 5534B020AF14217094C6B51ADBA2A925C217017C63E42FA01751A61EC02B088C + 0486057F1163771EE0D4D83D0260E02F6097742AF5D8CA9C20CE13981F3077F0 + B989A594B1F7AA437E0C2BD77319897213F15BE63E6A2708AC0860C1DF060097 + 39569C8B4DB0FCC40536F7237AB740E05AC0DFF126A27B81BC5DC017D05BA030 + 73CDD136FF04E2D6F037E6556ECB4B9ACF4688AB796F2CFB7CB792E6C96D76E6 + D46D0038E647CE58909E993A37CFC3CBDC5EAB7A69FECE5C5EF3FC38DF970F80 + 3C7057D037E6A30480F3C3E573D2C8E2EBA815FD234A6BF88BF0A7C8A7D2067F + 51E3CF29529FA84E3D7E1D52FA21EAA4CE31A25F48FD06C25F34D38E9F57AD8B + 7E29F9A9A4365B94EA48F1A53D834FCDF263D95A703C011D332D3FC7A387F183 + B10730E7C48F26002CA8DBA6E8078CFE3FA9F103F6520BA81D56BFA2AE7379BF + E2EFFC354B40C7BC0900DB067ADBECF2DFFE5AE9F8A8AD1E75EA9FFF4D15FDE5 + 3D354C00DC76FCC1D4768C7F160D17003BE369E1456D6A98D010B69E8A20F33F + B3F45EC4F745F95AB11F8A00F6EFADBF2B001E35C89BF2879E48FA910E4702C0 + 1EDACFD31E5C5CB9588C30BBA5804DF6FE6D0070BCE80CEBC269C7F27390B6D5 + F712C76D535BDB6EC58BFA93A8ED58D260ED9B72B849D565D9B2DA6FAA4FABF6 + F729AB69D326BB8137B272D336D8489BFEC5DE805BB50B1A50A763147B6BBF6A + 70D40CAA7AD87B4903C0D8BE1BBEDAC0D114017003814B2A004C9EA712F360D4 + 8EA5C1A81F231FAF19A806A8DC7DBE3695F3B9C8C772D680017BB00D266F5F06 + DA1AE86B4211270C9435E9C04E9E89882616EA532B4E707A2902E036A03B9822 + 2C96C6D8240D69F2564B93B938A9434CDC64D30431F68B13BC68EFEA6313CE51 + 2F583F93269F519AA08E266F135796AF9298CC6A321C27C49A3C8F359B9ECC66 + 523E8A89EC9CC569C23C9BC017004C0AE475906B02EC9202831165AF2F29AA01 + F004CB37A0B7EC2F5C4703BB0D40FCCA8A347E014F9A770070ED8C208F047905 + 7E1BE86B6D048149C7D3B7070056C4AF60AF5207BF72B0900E218E2B278DC363 + 8E69C25923478FC02FA91C3C5E0E8EA381CA11C05257047081BF805D20AF1421 + B0EAB55434E5A63E38C61A001C9C699EE270DBBC27CDD8B2B789D8F0D4340B08 + 6C752F599E085F2238666CDF975ED849BA37CDD8BAC7A38371DA018A5F02189B + 666F07ECBE955EDCF1467A61476EEFF0B8712A529F9D8882C05A0ADAA34A2C8F + C371EEDE771CFA027C01BF9297DFFAA001BDE415F94279D1BB1F7B1DCE50D277 + 6F3F4C07AEDD49AF7EF0993B4C71A2B2143400D897567C27CB9DB325624700F8 + 15D3820F3F49F3DEFFB881C051D966FD3EB2D7625AF8F1A7E9D54F7120E31CFE + 322D2E11C002C0723293CF0098A59FBF4A4BBE02E00275D9C39008A6AF1D02BF + F685B5FDE2736BF385D5D30601900FA5E5070F3A0096880E5E71F46889103E92 + 561D27A22A470C2F3B7E382FFD7C827D838FA59527F2F2D1EC1DBCE6ECA906FA + 6A39E855674EF852D00E8CCF9C4A2BCF9F750176057759025A91BF02C12EECD6 + 7615FB435E645F4945FF5E4C1B5C17D24613D0376AE315A287CFA68D37ACCC92 + A9772E3BD8C84B9D5E373B20842551AFA6752C217DF34ADE4FF896E56F5D32D9 + B14D0D04BE9DC16F1BFC7560722F8B2860208E470D5B0AF07500EC303847F851 + DEF6F0A62FFDBCE5C14D8F12DC7CFF6613F54BC420D252D0310218E80BFC1500 + 66F9E73DC0DC1F1E38E0657FE02DDFDCEC82C05A3A1A78ACA85FEA14112C00BC + 07B84C1F969506FCBABE49BBE9F3CB235F7618F91EC1DEE6A1A7CDBEC13F591E + 08F7B3BD1EB351DEFE83FD3F3F67500C0844403BB4D38E094416147410C8F1FF + 04C4FDD6E1A2C0E2EE3F036101A579EF5C4149C148E5019694058005300561C9 + 4748AC3AFAEB1848D1AEF158D801C0AF0342FFFA43DAF7971FD2EE3FD9F1585E + F8CFF67AFE6AE7F8AFF9F5E91C1C47D057E7D779754E9D97546D72FE4707C007 + FEF673DAFB97EF1D34EFFF971FD2DBFFF6A7F4F6BFFEECCB76B36FF3FE9FBF49 + 6F007C0BF47DD3DA9202FA89E63E60EF9D0BF06BEF2B62D967003091C06FFF9B + 1DCBDED75D7C6EF67E039FE37BA5A5AAF53A65E3337318EB9F179F4906B19DCF + 37C3DDD82ED661D7F7414B3FEFE7B501D2EDF566593D753CE8C0FF52EAB28D88 + 5C7B6DF67F51E7DF4FABE3BBAA88773DE4B0CB7F1F3CF49041B0A27F5DDF6590 + BBFD311037835D802E1018209CE1EE830C814D3B2C9FE1AFD91CF2DAEFD4CA68 + D3BD5B699D5D57B63C6099773B06A0D8DA6DB77E5B1FDA6FF191FD86EDB7CFB5 + E25DFB2CEFFD7FFE5FE9E6FFFBFF91DEFCE5B183DF8DECEF6BD713DAA0DD8FED + F76ED7081E54F16B903F6872356DF6550BAC9DD511BD0BF82505E4727D03EC92 + 2A0FC8953220CE7BF902766527BF9ED512EC1A48D4AF2FAF6F22FA97BA9CE7FA + CB7598BD7E01BD44FC76227DB94EF3300FD76CC0EF3A6C67CFD83DE0B4A782BF + E401BD2B4F9E7400DCCF5EBE21EA17F88B048381BBAB4EDB31CF9E4FAB4F9F4B + AB4E9DB5FBCFE9D477FC645ACEBEBDC74E5A9E7BCC713FE62A60F031E02D51BC + 39BA7709F73A1365EE8908080C2C5E7E98153378508A2D138E79D42FFB002FD1 + 32D076DF45DC6BB9CF027E01C144010B00B30D8340B12FEB6CF7735200AF967C + F67BF6C7ACFA91C1301210D638615181BF48D1BF5203830BE4ED15F9AB1409F6 + 4A3500969DA85F815F45F44AB3CD2E18FCD21EB311E96B29201801809507FECE + DC91C7691AAB313613D04502BEA4ECE12BC8AB31A3C68F5E0EE349C69668A68F + 278B78A8B044000B00FBF6242681E0384E553E42DF28EABC7EC3CE26A257D057 + 8057903702E00881A9A33C99BD7B4D02C012E37556F4210F007EB66F437A7679 + F75C0070CB1C410F864AD880B2AA8B7D62FB68D71CA59620AFE62411FEAA8EF9 + 10F077EC429B6F017EADCCC3B20E7A8B98478DB3B918AB338D7BC5E67E569E68 + 29FBFF36CB3C17B1FC331018005C835FF2B14C3D734FC15E8939691BE48DD25C + B69734C7950475A57A8E2C09064BF1016BE5639994793710D8E7E665BE5E6FE9 + A4B97D3DF78FE51A0047BF80A4365E3FB513E91B157D238A06CE11C27392F600 + 96E44F51B9CDE7225F0CFE1AE56BC99F9345997EF871B23A30B8D3AE0B000F00 + BF1DF506C06DB676F08B1411EC00D8ECC85F3FE9443BD784BC15DB98292F3918 + C647265F9A077014BF1EF23D8209B40836F2526D8BBEC35F2DA06E0576E3B99F + 18009B5A7DA1483E535384B11DE89AFDB4BDECBF85863A1EF5193A3F3F40BDFC + E5C351ED336F93B72BBEF7DF42CDB95BEAFE59F49B01608147CB378CA8D4A9DC + D81A4859D2988FB6FFECD27B55DEC7565BAC8B8A00F6EFAD5F0D804711D53B5C + B5BC197FE8A9157FAC8329B66F402D51BE5C2CEC22C232D04401B3DCB35F28B9 + E03CD1B2CFB495DAEA7B890B72962ED07F0FB5BD96B6766D1A70930A65D9B2DA + 6FAABF46DDEF57C7D6E491DDB49B01462C175BD336B6B3B4E9AF3237FE22B58D + B6A85EED5D3A578F8155B445BB067ADD833D3B0F3036B6650059415960AD9E2E + 04F066C83BCBDA5BFD241B1C7B24AF955DB98F0F4839C7043BAEF567991A06AC + F169450D5AE3B9DA3534001E6903E2671083E62206ECBD07FF591A604BD834D1 + 20E598F4D5C46384E5B1C5363A3EF95A4C704630482F939D289F04993A009865 + 916CB235AD03760703BC2A0F68435D99ACD513334DE26A69D2A6B2267CB14F3C + 862690B5985C6A922908DC2A26A4D52496096F3D21464CA479F29A49B900F058 + CB8F7E99FAC50E7D81B8A4E381C044020382E7F63511BE02C011F63A0C46D88A + FC3826F6FF1DCDD2D17370046400EC11C105044F2A4E07E0EFB8F97D9E5216FC + 1514769127AA18C86B12EC15F0D5BEC22AFB53EDC50982E3243A49C83BB0C531 + 139C246AE3F9526EA070903F891FE4CE194BDD59531C3682BD82C0A472E8B89D + A8E00282DD89B46A605441560704238F0A5E53227CADFF6493227D81BCA4D3D6 + E6BD80EB25A11D02AF63F9BBDD1E092C27999C6972B24DA3AE006041E0C6C167 + ED106017671DB078DA66EBBFCDDA6DDB9766585E8EBA1737ED4D2F6EDE9F666D + DA975EDAFA469ABDF39D3473FBEB76CC7DBE779C224B2200C63928A722D1C1B4 + 21458A3699B7EF5D87BE4D043040D8CA2C61E851BF6F5ADD1B39D2457BDB017D + 916C9B8F9F4D6B0E1D4F738AE31447AAF6D3235DC072CFEF5A7B6CD66FC17B05 + 00BF0F10FEC49782D67E7FBE14346A003032FB87769C0F5932BA2C014D6490E9 + B54FBF7447B30030F0771E60F963A06E06B9AF3AE4FDD221300E6EA29E80C194 + 177FF9A5C3DF9C6679DB2F0FA6E5D60667399150A48A9CD2D2D12B8F134565F5 + 08106C02FEAE06F09E3CEAF54400AF39D559FEB9FFE4F1B48CB6665FEE3AEE11 + 5F805CE4D1C005FE2E3FD389FC25ED3B07FC359BD5115126B880D65CBE90D65A + 79C3E54B69E3958B699369E3D5735DDA70E54CDA78FD7CDA7CFB62DA7CF76212 + 0006946CBA75236DB80E04068458FEE675D7FA1B1906AFBB7121ADBB793EADBF + 69E7B885EC1C0E91812A5982C15D40F8DEF512A197976F657957EDEDB9D501F1 + 55B3DF4EFB7E7890767C73C701F0863BD7D2C6BBD71D020380B73C00069396A8 + E047B7137B8C6EB3E30177373FBA91363FBCEEFB00038101B740DE1D2620EF96 + 8737AC4F06C0D8770180ADAF00708661443E1245DC81C40E832DDDF96D5E3A3A + 47467EE311937B7EB43ED4018DEDFF21CFB2D380E7DDF6BF9027DDF30BF0F7A1 + F501BA3D48DBBFB7E3FF607D7EC9919E190066782820B8E367007006AC39A2F8 + 818BBAADD617082C80982349AD9FB5DDFBD70EB8155025FA967C14E092768058 + 41DD6CCFF58AD8A55D948E4D3E1F0B10AD659081BCDFA57D7FB1F42F44FEFE98 + F610558AAD0B9A7EE3C7D1B174BCBCBC7286AA083BFD94E6D745D9CEF5B79FAC + 4D06CD0E99ED5C4400BFF1B71FD3813FD96BFBDE8E659FC9819FBF49EFB0E4F3 + 7FF9C923821D009B6DFF2F8FD35B761C96817EFDCF9DFC017B2FDFB0FFE30D00 + 35FF939577FC78CF97887EEB5F8972CE209BFF83CF88FF89E8DB6CE3F53FB2FF + 8BF75411CDB407DADB77CB3F333EDFDC97CF12E5CFF6A18BCF9AF61CC321B07D + 6710D0DAC17589DE15C005E8E6A59CEDF3B234DA116DDD6EFFF32E2BEBBB4F1D + ED76DA7BC4DEBC005F5FAED952966ACEFBF602775106C182B95B89E205F836E5 + 7B2E602EF0770740F8E15DFB9DDA6FA9406107C0B7D9EBFB4E6E471BFA599B2D + B47B64BFB78744EA5E4BFBEC3776F0FFF66FE9F3FFFEAF6917AB02D8F560C7FD + 9B05EA12D17BDDF2656FDEDB9D874E36F260CAAD4B5EBF8BA5E3AD8F602FD737 + 227581BE311A98B2A4EB1750D7F759BFC15EBF570AE8BD68E52B9D7D7ECD465E + 1078F5951CF9DB81C09D0772748DE67AEED76DCBAF3E7F2EEFE37BFA4C1700E6 + 3EE050F88CB5654F5F203091BBA74E79D42F7BFEB2DF2F36A27A579C3CEDEA3F + 6EE909EB73FC4C86BFC7890EB6FB0CCB3B1FE37EC3834A764F3A7E3CF52BEAF7 + 1060F7ABB484552D7CBF7BBB271E2612D8EE7F5F7E9596FA6A195FDBBDF3CBB4 + 0850CB72CF0779682AEF05FC5A89ECD53DF71556E4F03D80B90FF34056E79E4C + 0AE4E59E8E80BB44F7367BFE5A7D8C0C6EA28179F8AB6C17F18A09D89B1F1ECB + 2078D187AC26922382E7D858630ECB2EBF41E42ECB39E748DFB90DF0CDF98E2D + 035F415F9515152C29F2B701BD7BDFF4155A3CE2378261DF822347F94AD800C2 + 88FC2C5FD1C5EACA384CF0B781BA005D22760BECF571A28971230F11FACA31A5 + 4E7BFDA2E95B6C8C48DDC62CB62011EC4582BB715CAA716503760BE88D4B3E23 + D9A5E91B2C2DE09714B01BA37C057A65AF013079E0AFEFE76BE9943246D78A3D + 7AA0F3B97EA27FD7BB9A3901F383322FF0874E91E5B133CF00006B1EE180D7A4 + 7203814D4F02809577C01B14A37F297B7BE64E05FC4AE34C6317F4A5B13627F3 + BD7B81BCF55EBF266C887924F349B616420DF02D62CE4984B047FE5A99B9681B + F46D83BB51F59CB6AD5ED0B74DA36C7EDC067E651B836699CDE6D6A4110023E6 + E23397AE4F0BD7EFB6F9F1E234E2F9B9CD9C9EB97C5CFEB999CF5B5936CDF9A3 + 1F004500AC7C579BE9562662D71F6E6F59F2D9F25A0E1AF0EBCB43175F8B7C28 + BDF251D10713D35A1D3F0F29BE1D8E67AFAB88F24896580E7DF009291DF19CF5 + 9B3C73800607C02C0B9D6DF8C83A75037D59B41B353903E0911EA19CC5F9796F + B0E7A59F67A731F895F0C54DC87E34F9DC50EDEB1BAEADCD771815EB1BF582B7 + BF13008E1AE01B35C927FB241A0EBC6DB33F893200E638CF0FD093F8CC6BD53E + F32E95E5A04758BEAD6F9B6A5F7E7C6DBDCAFF8CAAE12FEA058023F7E952048F + 6D76CBC7FE23464CCAB2BA9121AFB4EB78FF293531A77A0FE3FB59DB54F63EA5 + 1F8A00F61F4DADD0B74DA3ED1F927E37F8CB97B2C717FB3F90F403ECA5276D1F + D589D4CD1743BF3072815134B0D947D9C5DF2F3A4FBC14F460E21883A9D356E7 + FCAD4464732CC773E97C5AE2596D623EB6F3727DD3AA94DBE59BE8608AFF7F5B + 7D2FB5F6E1863F94D4DFF252B4F3104004C023C7DBC0A70C5A62AA3AD54BB15D + 037E3D35D503230DAA5AEC0CF07C0FDE32D8D380CF075F138288DA65E9E6005F + 35B0EC0060CBDB80B301C12E1BC4FAC0169BBD0E1379967ED63148190893CAD6 + A6A6BEA4599D41B2F65861701C07CED200985B06F100621F4857EA806B13C730 + 1B93079F6830E8C7C6E0BB4C4ABC4FB1B54D3ED4CEED26E06F9B34F901FC8E34 + 8D9ACE840C006B93A6E72D4505EC46C03BC6265751B98E89554782BFA3A6772F + DBDC4CCA823469238D13B8D85E6549F077AC4D1A25CAD8D90738825E4D40D9F7 + 17A9CCA476CC6C13935426AF56AE27C9DEC6EA9920B30713F0978930CB713131 + 67823EE6259B4097A59F1DDE02810B105679DCEC5CCE40B8405F1710D8445B6F + 67C77CC926F8B49DDF9FC6CD319BC9816F5816DAF58A09E783E501C0381F80B7 + 0E740B00161406003BE05DB82A4D5E9421AFE4D0170707C7B3324FB4377B00CB + 79824305870B8E0F4BBDBC243F81EF8E10DA9ABD71A890D257CE9A52961A878C + 29EE1DEC7B09F775F60976074E69A73C7502C30E814D02C0A482C05994892E50 + DEDA20807089F26D643604207E61037B9CEDB6747713110C002642D8EBCC4E34 + 7076905979D39E66E93D9571D04DDDB4DB1D772F6E3F902333D6EF4E33ADEF4B + 9BCDB6699FF77F9E3DD5AC8D477690E2C4B3FE40E297B658BF4DFBD38B5B5EB7 + 63BC6DED5E4FD3B7EC4FB3769408DF0600E7085F1C8C82C0E4B5EF1CCB3FD31E + 354B419B9A7D800FBCEF4B18B294E1FCD75926FA5D7772C628602D011DEDA4EC + 0B8C231587AA9680F6A518DFFBD8E1EF5C07C09FF89EC008D80BF455B42F91C0 + 0E844BA490A71F5AFF0F8810A6DDE74D94EF828FA8CB51C3943B8EE6CFD32B9F + 7C9E5EFD1C98FB95A768B1E55F23FFD9179EB2F4E5F20283C913290CFC75007C + F0A03BCA7198F71DCECB64D20E69F9E8FE23C7ACEE68EA37AD3C9A1DECAB4E00 + 7F89A03A9A56F832D087F332D0651F601CF77D274E7864168EF93E6BDF07203E + 7FD6E1C04A4081E5110078F99913CDDEC0B2B354F48A73A71AF84B64D9EA0BE7 + D3BA0B17F2F2CFC05F07C096376DB87236ADBD78CA74326DB876366DBA7D216D + BC65F265A0F312A81B6E5C4FEBAF5FB7F486D5DF4E9BEEDC4A1B6EDE4CEBAE5F + 4D6BAF5D4A6BAFDBB16F986E929E379D73E80234D132D01B6F1211071401A000 + 980B14B6F48D5F1EA7AFFE877F4B9FFF5FFFE6FB7C02858922D61EC144036F21 + DAEFAEBD1696757D702BB114F4D687B71C066F249ACFE111D18577D24EC0D577 + F7D3D6C7B7D3C6FBD7D2FABBF61A5802F6B1F5633FE06F6F5B1D00F9A61F77DB + 371C8F08C40C6A775B5F805814600CD0DBD95B38B7019601D37C595C09E8CBB1 + 2CE5788035CA0061DF37F8870769DF4F00386BEB11C9B463D96A7B0D440A17E0 + B7EDC7BB96DE7365A89AC1AFC068868719022B2F18EA40D1DA62DBFE8BD9AD3D + 3056C0352A03478033A0392F039D41EC0F0E8EBD3EB4D76BC902CA76602C7D11 + 658ED1BC163B0651C9C0DFFD7FFD39EDFDCB4F96EF44FF9266889B8FE32A79FD + 7F7A7DB491D49F68DA2CC0B2BD6E963466EFDFBFFEE8D0993D9AF70149D947D6 + C492CF6F593BA282F7037E4D2C07ED4B41FFC5FE774B0FFCF95BDF2FF8754B89 + 060604B31F30CB797BF4F6CFF639FED9FADAFFBFD7FEC70C6CEDB59A78ADD872 + F46E06BC82EAF9FFEB7C86AA1700D67B4BDE3F3FFF2ED8F7C98EE59F89BD067D + EF781081EF21A91E54E87C277354AFBEBF02C4D8D4C66DF67E78547CB011019C + 1FA4E0E1067E037CFFEDBBE8F91CF14B24F08EC70F1CF2FA52CE8FEC376902EE + F27B04E02A0238FF3E73043079EFF7B080DE027C799003204CDAB4B3E3EE7E64 + BFB9FB2CB9CCF504904B242F11BC767D280078AB2FED0CACE52193CB0506B354 + 33D72EBBCED93587F24EBB66B05C330057A0574017F88B14C90BC845AA13F45D + 773DC35D496D01C0B279BBAB17D3EA4BACC470D697DF2765F97EF66807FEF250 + 0F4BF6AF38975775E01ABFE6FCF91C014CB92C034DDA7FEA645A4184EF19A06E + BE47C47D7CB977F497259D4989F25D76C474D8EE21C05EBB07A13EEC55BECFEE + 552B4CFDD8ED7EE5FBFB1E62D9E7AF1CFCF2F0122B592C39743003E08344031F + B6FBE65769E167DC130FA5D7BE62BB04B3B3AA4689FEE59EBBE0E3CFFD3E3D9F + FDFD7930EBD37C1F16107EA5C05D1ECA5A687607C3DCABB115204CCA38601EE3 + 01BBFF53D6D2CF82C00BCAEA210B0A0826458C2BD0FC3759DA1988CB18E4031B + 8BD838C453B35B9AF3B2BDEF0FAC39F02DA9E0EF8BECD50BF8B532D097310EE9 + CB07D80BD8C65001FC527E714F86BD922FF5BCD3C65D00DF5D36F62A0FE4312E + 631C260906333EF307F5CAD2CDA4B3D8FAC3DAF0D01E2BC308F6E608622B333E + E4A14200AF693A634B1BA3D6905763508D4D91DAC98E22F465AFDF588E7DA711 + DDBB362FF51C41AFE02F75CF5959B6AE3A1340F8D9951BD3A47E1BD3AFB0B17D + 81BF00DF29A6088319DB6B7C3F79699913945580D8FAC51FFEA40DF301E61400 + DB6A6E4119314FF108E1322F11FCA51CE72A92EC48739A6699679BF708022BDF + 067FA3C6337F6369E70503A1AF846D0CF0D604FC05F4B2B214F34CF2C8417099 + 7FBAAC8C2204F6F969C86BFE1AF392CF695BE6BA92EA63BB71B3ED3894057A43 + BDE0AF47069787AE47CDB4F979986B330F1F31637EEADBF1567AE3ABD369C6AB + 6BD23353F039E4393EF3769682D6FC7E1CF3799FD3DB71A7DB718B5D92BF4029 + 3099342FF7DCED4318417E1A3E8A0EE86D03C0BDF6F595A25FA5AD8CE4879122 + 3C8DE58E8F8832FD80BE52F7715A8FF1DCAC2EF03B140046D9869F09DF17FEAC + 8E2FACE3D3EAA4B98FE539A7BDCE5194A7107C60AFDBD475BC091CB313FD1BE5 + 7EC00257A3EF4FAA6DD16FD84B6A33A822FC0DF9D663C4B64328FA47BB7CA3F8 + 5B5B04748DF056E5DA1E55DB9BB6A3AD1CEAD46E289FB3FAD73EF1A8E80F7F52 + D5FEF26782CDC16FD966B1ADEF70A5D75EFBEE916CFF6C6A05C085DB3482D988 + F51436D4B022014729D657B656FB4894F3B2350AED9AFC70D4760CD9DBF251B1 + AFD4D62E6AB8ED24B555BFAEBE138B541E8E682F6E5AFAB781D77F2AE99F29B0 + 7784E551B4658537F189C497D1D47CD1C397FA3FA0F4031C4C4FDA3ECAA37BB9 + 788CB28BA1CBCA23F27B3BC2CAA3B87178BDD95A2E44BFB7E2C5FAB756AFF30D + 75B353BBC6566E58B5629BB69B6B5BFD6F01803B37EDDEF276B4275F247BAC63 + 9002DC1D35C1065B96760D5E68AF018DD5797D19E8A8AE19E49052AE06453E30 + EA3138C3A6415E54775BF2B9BFD7F33A8BB08F9A680355AB23F57EF61A1CF8DA + C0F399B1C0EB17BC6E54383EC0798C0D5A47DBE036C2DD2EC85BEC5DF5A80CAE + 47FAC0380F8A35A81E58EEA4CAFBF2CF2660AD96D3C18EC65819D576095B7EBA + F265978EDBD84A3F294E0C946F26242EB3053900A61D6DFC895764939FE9F3F2 + 139E36291A6193905136411BCD53BAD8DC9E35DA26535944FB92526F13A1A231 + D33916E0D6EA88C4A59D4DC41C0A5B5E62922669523798E2E48F344260C1DF06 + 0ADB64724C00C03C89CC04D32796B4B549AA3FA1FC924D6E4D7A5A997C5463B3 + 09F0389B088F6692CB716DC28B6D3C136CDABD44DE26E4A671B36D126E223F01 + 780BC495BDB4196F363461AE4DF24D44FCE636A697963A1C660FE10C8C33389E + 38CF34BFBF13054C04B1A0F0C22C391FC80B042B1F251BF097A5A07176B88363 + D1CA34F695FE346EA11DC7CAD1B122878A965DA3EC0E119C25384E70A29828CB + AE76F1386AD7805DCB2B62F8D9027823F08D9273A801C0453888E45462DFB06E + 082C40BCCDF21908038B5912DA9785363D6FE52966A7DD8C35DBD28C752C075D + 9684B6D41D64266070748E45C5080C84236EDA46ABDBB4BB2CD5B72FCD246263 + 03D1BDFBD2AC4D7BD374AB9F6A7D71E839B835691FB7465B7014BE6E75AFA717 + B610FD41A408CB0BE228DC975EDA99F707AE0130CE47967C9EB3E76D87C0C05F + 80B0435F07BEEF3B0CA6EEE5DD6FA7B9FBDF71F02B0176057A8902D6B2D04811 + 32406145017BE42F4E5940B1B5F5324018BDF7892F11BDE05DAB2F5A20F06B02 + 0A13FDB300E7AFA50B81BC440A1309F4D167E9B54F3200CE4E67ABFB38436157 + 7136AB2C28EC4EE7CF3B4B44932EF90AA80B00361D242298C82620304B461F4A + CBCDCEFE8738C3718A2F361B4E718F20F62860E0F0F1B4E2C889B4E2E849538E + 105E71ECA82F0DDD7F2C4756916739E815278EA77EA2B0DCB14FD4D6C9B4DCDA + 2FC34644D7A953A98FF4CC99B4E6C285B4E2EC99D447C4174B449F3BE3228F56 + 9C3DE96560C3AAF344919D75ADBB78CE9780DE6402FCAEBBC43E92A7ED78272D + 7F3AADBF7A2EADBF763EADB574ED55A2D980B597D286EB575C44FBAEBFC1BE96 + 4093027F0B1071C00B08BE0E0439EF65815F415F528EBDCECED1C0E1DB97D33B + FFF283EFED0904261298655BB7DECF91C140609687DE6802FE6EBC0B00BE99D8 + 1378EBC31B691351C4809D6FEFBBB611E1C7F2D14402037BEC189BEF5F4DDBBE + B9E111C03B4C00DF2D8F6EBA881256F46FD48EEF7364AF802F000D3B6D15090C + 3063FFD47DBF3C4E44FE1201AC36826C026EEAB3DBCA2E3BDE6E60AF9D67EF4F + 0FD23E223AADCC3EAD4484EEB0725EDED98E697917C098257BD9E7D5D21DD68F + 76BB431BEC44DB62537F228AF7B254F0DF7E7440CA9EB1D869831D61E3F81C83 + 3CCB331351EBF6720EDA91FA399A7CB6735C8F6AE558265E03C7D8C76BE17C76 + 6CCEC992CCECCBCB72D0BE6FB2D9F91FD4272BE7DD568EE7C7E07C26CEE5ED4A + 9D9F8BE8DFBFDAB1FFCB4FF69AF9FFED35712E2BB31CF4CE9FEDBD33EDFB93FD + 6F26402FDA07FCFCFE7EDAFFD3A3C6461E40FFA61DE78D3F65407CE0179682B6 + F3DA31B6DB67ACFD7777FF62EF2FC0D4CACDFBCDEBB4F785D79AA3B9EDDC96E7 + 7552B793C8EFF2FFF0DEF2FFFBFBC0E769E23D47D8B75B19F8CB430019A2DB71 + AD6D07D466C0CB77CB15BE7BA4D8F4FDA63D51BF7A98817A9620673972CF97EF + 6BAE2742FEAEFD3ECAF7D66CB4F1EFBFFD4677F09085FDDE72143ECBB1F37B64 + AF6EAB7B9CA378B73CB0DFCE63EBC792D1DFDC73B04BE4BE966CF748604B69CF + 3EC0D4F33007A21FFB006F2532D8EC3B88F0A7CEAE19AC1CB08BF3F0FBBE7BCD + 1F22612F72E0AF00B0A27FB9CEE85A4439825CA0ADE02F625967526C0DC4356D + B0EB1EA0782B7D81BD44FAB28F2FB0D7AE81AB2FB3CAC2F91C015C8EE97DAF00 + 7A735DDEEFF7ACC35E5FD2FFACA596E7E19E156759B981EBB01DE3DC3987BC5C + EBFBED9A9F977F06009F76F59DB4FB80DD1BFA4E007DCDE6FBF96A5967C030D1 + BDC7D252BBFF2C07FE1E36DB91E376AF3A967538A7ECD3DB6FF726AF3B74D4F7 + C25F75DC5ED3F1536909F7BB027959CD022D2BF7BAA50759CE39B7D73DF355A2 + 75BFB2BAB0B77EBDB4B33F84E5F7DABCA4B303DF706FF6FBB4B527EA77C91787 + BC8E07BEE6BE676300BBE773BF77D97D9ED5417CE9E7008159061A39F0655CF1 + F64769AE8D2D5EB6F1C682B2E72FF017088CE6BFC1EA25AC66F2BE035FF20BDE + FC30BD4204B0D9E6D81867367290FB4E7AC9C631B3F6BEE9624C33EFCD3CBE01 + F4529ECD72CEFBDEF67E5EDE67632C22824BB42FA0F745D32C1B83CD74809BF5 + A29501C0C05EC6647A488F7199C0AED7211BEFCDB6F1DC6C07C4762C1BBF3563 + 416F6336FA11155CA02F634B1E128C6017A98EBC406F37CCED805FC6B411FCAA + EDC0FAED694A81B97514307017F82B004CBB294400B3FA8EE5A7018C195B0381 + FBF3F2CE93FB6C0C5FC6EA3E9E57DE441DC2EEB2BCC6F70060F2CD7CC0E6070E + 82AB7905F38D3857691E5065CE415BDAD1069BA5B2690EE470B7D4D540586D1C + 0A33B72AF3AD28CDB926DA1C6ADCFC0C796BF0DB4405CFCB0F112BCA97340260 + A281FDA163602BED8A047B23F41D4A0DE4B563B95436C5FA087755379E879CCB + 7C58625ECDFC5AAB6CF97CDBE7D645948B6833D2E6ECF3566D4FEBF67D98A62F + 5ADDCCD7E3DC9DB9BD035CE6F9661B39C3FACE000213F9DBD1C869791968B507 + 266BBFDF0881553FAAF251A00111C0A48340DD68533BA4FA5A6A176D82B91100 + 3FF32CF6DE7B00AB7D978D55EC3C1278969501C00502BB6F0A757C511DBF1269 + B7FF2B8A7E7559BEAE0C8D0BE0C52FF69C7C6A79653DFC6D5EC7B151F1C1D5FE + 40D9A2625BA4B6B14DAD78CC9E8A5037E4E331EAE3C90FAAB2FB3A43D9F7401E + 6FAFD96C48ED23946D7CAF66AB7DB56D75D13E98689BFDAF398D7543F99B3BE7 + 238F8DF6DDCAC7787A455F79F4AD0F76CE5EAA7DF38349BEFC7F4665E80BA3E9 + 28B39C8E3A8CA7C3861A562408A9BCEC41EA5F1F6748D5C77E12B51DA3566C3F + 9886EA1BCB03DA6511DD3CA07D4BBBA6CDB0547868037F835AA1EA3FA1F40FB5 + D95C4F0581ED4BD880DFA827F872FE93ABFE51D6626F5FDFDFB7A56E80B85034 + CB05900E141038E6BDDC5C88C8FF0E17525DC4DAEA9E424F7AC18F3789DF5211 + 0C2BDFB9F975DF787F8D3A37ED5F2F07C30C5CCAE04550D807121AC468901307 + 48A5AEAB6D356892BAFA85BC0678F5E0CFDBE9F8E301BD335D2326D88092749C + B537290FBC1E65AF9F74ECA459693450B8FC4FA3CD86468ECBF5A3AD5E7B8F34 + 91C7A40C6CC320370F62B3DC56C0AFC4539579502DD08B181C0F04BFC35504BB + DD03F4EE76005F29DAE82B1BFDBA26064571D211272E48131152262863CB53AF + 92B729F57EAE607785495484BF4CA6288F9D41947079E276C63C8F12969A25A2 + 4D8A2A6692C6045080579017451B6DD44E757102A936006060EFD8528700BF0D + 28661239C3FADA04364E66E3E4368AE5AA34F96592EB4F265BFB0612DB441938 + 5C4FB47DE23C67719A68937CA47A26EC5ADE6BECDC3CF9F74862FABE9CA37F27 + CDEDCF4B4413190C00563AB72FEF1D6C75CDB2D00B73E42F4E0745FD52D61EBF + 1D874406C43175205C9C1E385170B24CC5C98243A5383F04720576C7E308C129 + 52EA48DDF112DAF83E591CD7CAB10D92B3A671DC5067221F612F6A1C414511FC + E2149228534F1F2206145100D045D381BA6B01BE2C039D238511F91839DCD516 + E7150E30778275A223909C6A72BEC9E60E3860EEA6BDBE34DFF4F5BB3C5238DB + 4B844659B24F6539ED80C6005F9C813805D12C2BBB88026189E82D764C96FF23 + BF6DAFE7B10381B5B420CE45DA6BCF5F81E1088089FE9DB3F73D2B1319FC769A + BDF3ED34675F06C00D082ED1301EE51B5224200C1CA68C4316E88BB424347980 + F02BE8DD8F9BFD81BD1D701827302018E7EF077989689CBE720263A79EFCE2B2 + FCB3969404FA125184F3D91DCEA58CA3B971609B8862D29E8548CEECC59F67BB + E74B5413B6A55F6687F7624B7DEFC38347DC99DE7F94A82B968266394D9CF0C5 + E9EE40384703AF74E84B047086C140E1151E8965E5A3C7ACFE445AEDCB769E4A + CBEC984B8F587FB32DB53AD295A7CFA67EA2BF4E9DF66831960745808315A74F + 9A0D70405D4ED79C3FE3E9DAB2D7F05AE451BFA71C02AF376DBC7A216DBE7939 + 6D02F05ECB00C39732BD9A41C6BAABE7D31A40C715E030A0F792C30EE0070218 + 23964465BF4CC02ED045E05710863C0098FC6674E78A03B8F7FECB4FE9CD3F7D + EBC03747FEDE4A3B1EDD7218BCFED6A5B4DEE1EF555F06DAA1EE03C0EF752F03 + 8289E0050A53DE0434B674B3A5B445DB1FDF36019F6E3918F6361CE3D1CD06F2 + 0A965116B075E055F20E772DA59E2862967106AA696F54F64ACD90ECB6D93B11 + 9A88FEA41122EFFA817D84EFA5FDBF3C72D1873D5A1D88FEE93B879740CC5DEC + 774A44A669C74F8FD2F61F1FBA7698765A798FBD6FBB7EFE26D7FF98CBD877FD + FCD8212B6552E02BCB30ABFD6EC0B5D911F67D7FFE3EDB38F75F7F7250BBDB8E + A173A86F6E9F01AEDB59CED9CA1C9F32C7D8F3CBB7697FB1F9D2CF6A6B796CFB + 89FEB5B2FE1F80ADF6EDCD6D7EB43EB9CCEBD07BE150D7CA1C8BFF0FA92F6DF6 + F39AEDFC02C09C4BE7E658EC7BEB4B235B1B227E81BD449D937AD904FC3D609F + E55B9CFB473BD6F7F659FE0438B6FEBCA77A5F4DBC8FECFB4CCA6726F8CC67C6 + FF96EDF93D23D567B7937EBCB796D28E14A94EF6ED3F3CB4CFF59EE5793880CF + FB817F470464E3F74BDF2D52EA0573D586B2BE936A07FCDD6EC7DF4A643CE731 + 9BBEF744D0EB0189A62FE7E037C3B2E92CD5FE903DB959923DFFD678000330CC + DEDC3952DFCEF11D11C077FD618D4DF788D6B7DFDA63E06F962032F0983EB4D1 + 7EDEC0E0CD77ED377BDFFEBF47F67F028D1FD8EF8FFD7BEFDB6FDB7EE71B7DCF + 71969C679FDF1C05AC6B0FF26B4D79E0447016582B000CF805F0027F1DEA16F8 + 4BBAC5EA36F0A0CBE5F39E0AEA9252DE70F55206B917CE34CB3C93AABDF7B16B + A8EA56B23A835D9759F18165FFD75ABFD566739D3DEB51BE40DE6544F9728DF7 + A85FEC5CE7B3969B7DF971227D33FC9588E6E55EB1C4EE33DC7BD87377C5116B + 070806FE1EC29653847DD5313B96E5B907F2C0D24A2B13C9CB3D0D2D3B74C474 + D4EE759622BBCFD19E7B9F1EA0E27E0AB4D5B2CFBABFFA12CEE5FEBAD86C1ED9 + 6BF7601ED4F287B5CCCE7D94BE1CCB1FC42A7DE9431B20F00200AFDDDB5F65F5 + 0E1EF6D283613CF4C53880E8DF777970EC531B33306EC87BFACE7DF30387BEC0 + 5FF2B319B394085F07C0AFDBD8E380E5D9E6C2CAAFBC65E38F37AD9F8D795EB6 + 310EE39CB9FBADCF011B030183F7DAF8887D7CF7E587DAD80A033B36C64BE8A5 + 3D6FA55905F6328E7AB18CA748B515076997B099189731060300333E63596722 + 79553F13186C633C3DF8C783824863C19777D8792DAFB1A68F3B7D1C69E346C6 + 98960ADEC63129634FD95587546E1BCFCACEB8778A8D831111BE8AE605FE6ABF + DF6659672DF92CF1D0256372C6E6E5A14AC4781CB8EB7BFCF6E787387D5C1FC0 + 2F7582C40E820B24D6D81F69DEE0637D2B93FA3C823988CD236AC53908A05773 + 9B1AF40AEC22D9233046FE90AA89B6F5DEBF519DF95806BD005FD44402530600 + DBDC2F4AFBFF0A0A6B5E88D4C6EBCA5C52F34941E0CEBCB3C0619B4FFA83CA65 + 0E1BA579EAA00202D3B69A1347F8CB764B9A678F7DC1E6D12695998F2B45CCCB + 27CEB639EE4BF65A6D1E2DBBA479FF33A611369F1F01EC05027B14704723A775 + DA7AFB027D95FA43E696767C0D737DAB2B41DF087F1B51968FE357AAF6BDC8E7 + 12FD3375DB58DF01C41DC5FA5EED083A90AF48FEA18E7FA91DF0C67227DF5D9F + 57DE9BE1C0B7D316BF19E5E2DB930F0DE16B2B7E37DFD20D05DF5F94FBE68A6F + 50BE44496D6ABBA4FAA1DA75C1E0A0A61F2ABEC8A8013E52133EC8D1A6519617 + 046EF3B9D66AF3ED46DB70D4FC0FD65751C711FE2A0AB93E7EAFFC4071ACE1A8 + 777BF7BF17BF3F65E02FCB4E0F17020FC7871FA1726CEF127708ED07B4F90791 + 03E036A653A98D210D47EA1F7952532F9019DA7729D62BFFB4AA8FFD1BCAFFA7 + B6F358B959EEDAD4D526B605FC1275ED91D7A56E48F500BFD233CF8CB73FE31B + 68FA4F255EFB087BEDE11F222F75FDA3E8899782E68DB61FE800852FE77F70E9 + 87391CB5F5EF12EF9BBF7F7651194402BF1D00DC266BFB5BA97CAE1E79DC56FF + 847AD28BB9DAF7EA57D70FA5087B7B29B7D5CDF1D74B37F2DF42CD20C6F2CDE0 + 41F63218AA07482E0D9ECAE0A369D7626F531CE04535C77759FDB859AE91E36D + C0DA06806963AF79ECC499691203677BDD0CC4C6DBE0761CA0D75E07FFCB18E0 + 30D1C1E5BC51C0E0D1BECC4D1ED8D60018BBA27FA34616E09B07D5F44543835B + 34983D2EFF1C07EBA86E5B1F8332ED34198893084D2A90261A71B221FB689300 + B0DAC4BCC36AAB478D8D7C9934F993B3010213FD3BEE85050E81B17B54F1B40E + F46D530D777B897A41DC58964DC768539C90AAEC4B4297C9AC1427B95D93E560 + 8F52FD785426E49A3C8FB3C9370078C2DC817B3639ECB554A2CC93DFE479F27B + E29C3E8F06F67D8459323A4060A285057F259677CE0E87BC1FB09C10883C36ED + 13AC3AEDF7EB51BF0B57B83365DED603AE19ABB7F9D3F5CD53EFC521D2E55009 + 75EE10C18112D352DF80601C284BF3F2CF82B572E628F237D6C5722D5EABC06F + 84C0D8713C0900E38CCAD1BEDB9A1445E05B0361E06F06C0443AE0FCCA2098FD + 7F33D0CD8E34C0AE968156B404F6191B76FBDEC1440F7B04F15A22898B7D6389 + D8282972F84B64B0A52C138D7006CEB474C6D6BC877096F614B614FBF6B24420 + 91220500531FCB805FA27B89029633D31D9AEEBCCC0202CFDDF77E7A79EFDBA6 + B7BAC5D28825EA252E8328202C691968EDAF1721B02283151D8C048571F64609 + 0093AA8C83182DFA88A523B39319017E3DB2B74421E1649654268A09E7B51CD1 + C05E39B4DDC96DA9036293A2943CD2E92BE02FFB26122575342D3D78A481C2EC + 87881D00DC7FE4B84701671D4EFD47890006FEB224F40917F097FD8481C41BCE + 9C4F1BCF5FCC40D9CA38FB4971EE939780BED2AAB344891DB7FC890C19D85BD8 + 52967E0602535EC152D3A6D5E74F3800DE70E55CDA70F57CDA74E352DA76E77A + DA7AFB7ADA74F35A8E6CBBC492A5673C726DED65A0C7798F7EF3A8DFAB79C953 + ED41BCCADA6A09D44DB7AEA50DB72E3500264218F202C29489E0DB04D8BD6379 + 60EEFDEB69FBC39B79E96700939537DCBE9CD6D9F100C080262DDD4C59B61C11 + 7CB300E06B69E3BD0C8B25412AEFEF00D9FA7B3BAB2F2018D8256DE3FCDFE768 + 60F29B1FDEC8A0F8DB0C80B77A94B109F05C96CA050003CAD867F800519C9ECF + 304E000EB8A6739127EA97E59FF3DEC419E2EDFCF161DAF34B06B140D25D0042 + C060D1F6EFEFBB76007C4DD4236CD43B58FD015898C1AD03518EF7E70C7BB109 + 1E4B0E6D5BDA3AE03461539E7619E266B8AAF602C43A0679A028ED80B66ABB87 + 68587B7FF6B14C7339077D6923404C5EC747F9589DF3729C1D3F3C72E9F561D7 + F9633BF573C06D9FC16EFBDF597E7BBFF5F325A1ED3D64596EF2BE34B7E9F59F + BE4D6FFDF27DDAF783FDCFDF11AD4DF4ECB78D784D3B0BC0E51CBC06DE47BD47 + 82B87A2DFC5F6AE39F8D9D6B37F5457CDEC83F0FABE3F3E4B3DD669FE956A271 + F9ACCDBE83A8F1188D2B905B20AF24604B8AF80EEAFB2CD10EE0EBF0D7B4DDBE + 7F1C6F9BC9217001C1E469EBDFD512B5EE7B5F3BC425AA370260A0AFFD1E1EE5 + 3DB94981BCB10CD88DB6CD2CEF6C8A36F2D836DC26C2D77E6FF7EE16DDB66B06 + 91BF372C9F570860AFF0CDAC0CC0B5C0AE152C5FCFF5A5B9C694E5E6F3B52783 + 5D8FD03529025860D81F7AB154D732E0F0FAAB17D3CAF31DC84B396AF505BBF6 + 5DCCE03747F9B204FF99B4F9A69DD7B49E07684C6BD99BDDAEC33C8043BAA63C + 94B3E6FC29130FEF007E4FA6BE93763D3F7D2EAD3ECB72D019FE626BA280EDBA + 9FA37D3BF017B1BFEFD2E3C7D352BB4F2C3F62F792AFEDBE0304260A9887900E + E5C85D415C00F0CAA327D36AF6093E7222835D9670661F5F134B3C738FC3CEB1 + 048F15FDCB3D5591BD82B6DC63F59095DF238BA8C7467BEEC9BEDFAFE501C3D4 + FBF1EC3EAD6D1BFCBE6C6A7BD86BEEDB1FB8E6BDC3C36176EF67A9E7027D1130 + 1809025327F0CB12CF44F8BAD8DA02C0BBEFDD34876D2DCCC616175EDE6B6397 + BD66B3366ACB0A2802BAB377D998A7AC884259A23E2FC59C416F536776C6590E + 732D8D4098259B49A9971CF86237F972CE44F532A62BC0D7E1EFA6FC0021ABC6 + 3450D8C682FEF0609080AEC0AD20AE8F29195BFA3625DD109831ACB72B791E6E + 7C7E0D0F399608606C05006BDCABA59FB5FCB3E411BFA6E7566E724D2D636DC6 + D53C843999B17A5F8EF0D5581B5156C42FE3750160C15F496D980B4802BD8832 + FDA36D30084C2A90DBCC5D6C6EA2547954B7535900590FD60AF84A1108E78771 + 998B65000CF81D4BC42F0F0303854DF57EBF92406F9B88046649E82ED06BAA01 + F004E69F01FCC6F9AACBDA2AD2374AF3572F8739729CF7521600F6086014E6DB + 92E6E9529C87036A15A9ABB6887AA02FAA017006BFF4CD11C06C57D5F4B5E38C + 2CD0770CE72D69CE0F5CA96C000C2EFE0DD4E63B89F6A8369F4B2F9BF21C2B3E + 58AFFA36D590B756577B2B47DF50947C5E12E5DAE630D79401701676F7914D9C + 5EEA639DFC67334A9AEB958FC2FF56FB00917C73E4BB7D82DDAAFBB5A9ADDF60 + 6AFAA171562E12F8AD85EF14F01B25FF6ACCB7A9F6ED3E8DFC75143F67F3BAEA + 7AD360E7FCAD5E4B47CF0FF0A7D77EF6278900AEFBB6A90B00B70599998D6D30 + BDBEAEFB079247FDB6309F5A6DDCA7AD4D5BDBBA9DD4C0CCAA7DA358AFFCD3AA + 3EB6A9791D2D75ADC7882AEDBA8E51D5917F6200EC1038B4EDA9160E1A25001C + 25C01AF3FF1E1A390CF93FD1B5D4732FD1D6D2FA0DD29BDB2ADE64415F814BD2 + F241FEA176F57A5F8701806BF8DB1B0023EBF36B55969BCE4B1C5475C3547DF1 + D6C53FDA86A3C1FAE8986D62EFE458AE616F9B72DB7873ECADF6F7BE5BBA91D7 + AA071F432946FC36F057A23E0E86AA415284BC03EC954DC7888A03BC28EFC3BE + BFDE97B2B571006C034BCBE734E733089E91C610DD5B9EC003008FB2BE23C795 + C100EFFFF86969DC732F39E4D579A304801D0297416F3340E58945D96C701B41 + F088027C338865409DCB1D5B4702B41A88F7AA57B9D77162A42F52BFD89F013F + 037F4D2C6A0D56873411D1044765D5938F0098F2D830998A0058C017F8AB28E0 + 5C1F27621DF0AB3213404DFE069343DBF0D4B0CA9A50C6492492AD96FA75ED07 + 5CA4096E9C34C7726D575D04C0529C9C37A2CE5201603DF11D01F04400EE9CE5 + A9D90B986860A27E4DC0DFB12C29CDD2D105FE0283B5143465815E3923C88F9D + C75264190C63D3D2CF82C0ECF98B236506CB22AFDAEA793946E4FC0008233946 + 6427AF27E7E544D192CED4630302E380E1B8005CC15ACAA817F0A58D3B892C1F + 1D41B1BE01BF967ABE388E241C4E723E49EEA40A8058F6088067AC051A6FF3BD + 8281B8405D966F46E47D396887BB38CE2C2D8E35C15FD57B9BB5C55E1C7472DC + 9107FEBAC3AF2C1DDD2C216D9ABE29478BB893B080603910B353912892FD8925 + A0597250D1C0CAD38EC893397B72044AEDD0C45989C393E5A001C0B3897AA904 + FC95B4F47384C04401FB7EC026F6D08BF0578A10D8A3824B44308E5EA27E70FC + 920AFCCA090CF87D8DE8DFB22C24695E2A3AEF238C63590E6B1CD4EE842ECE66 + 2F139954E44EED02811100B8B17D5996BA04FE027ABFB4365F65D8FBEA17077D + 194B8F642A76003091C12B8FE565A0977FCDD2D25FA56507BFB4FCD70E7CD94B + 78C531A2878F7914F08AE3C7D3F62B57D3AE1B37D34A40EFB1131EED8BD31F08 + BCF2D499B4F6DC856C3B71DCC1418EFEB576674EA4956781C019FE021A58F619 + E8001C5E7EE2A8A7ABCE9D486B2E9C4AEB2F9F4D6B4C44F80281375EBFEC116B + 44AB09FE2AE20DD8E120C38EA7C85F04FC251534C902B4E4685F441E00A3B222 + 8281360860BCFEE645073A44FE028189FE452CFFBC863D866F5BFB0739A257FB + 0147008C9DC8DE8D770BD8B53C7502C5B1ADDA6FF6A8C4EE6861B4CDCEAFA862 + 3FAEB5D9EA518A4411E7C847EC80605F0EF77BA0DC83B4F3870CE604DC80674D + 0A702B500D880C0866DF5F22867D796A409CD9B7DBF176FDC012BE803B2236AB + 7C8938DEF11DC7CB023E6313A8F4A864DAB24471019480500155C02402383A60 + 04283B60CC6058E054297D0437B1C5E3500FE0541BFAE7BEB9BDA02EF5D4E9B5 + 60278D654900973EBC265EE3767B7F753E8E4359E742DDAFC5DA109D5D8E0D14 + DFF7B3A53F591B7BAFF6F2BAF83FECD8BE57B3497B3303860FFCF0D8A37FF77E + 6FEFC5770FD39EF2BEEE21E2D8F2FE1AECFFF3285F3B8E03DBF2DE350AFF97BF + 376A533E3FBE2F7C9EFA0CB32D7FCE399A3CD76D2BFFBB1E000000F39DF24874 + 1E4AB0EF14DF373D6820608B047AD55E11BDFA4E6EB5EFE0968744C8DF691E6E + 8851F1B49500C34061F6B2D66F43BF03FF2D583EFF3672142F02E22A05EC0AFE + 2AD237D621EC4AD7DF26FADF7EB37780BE775C9BEFDC32D9B5818755F82D3F20 + BDE610782BBF797FF0A4B3E200F097A59F29EB7AA46B5484BF9481BFCDEA0600 + 62BB86B9AD44EF22AE8F405D6CF1FA48D9A383ED3A0800EE3F7BB2B95EAEB176 + 3C84B3CEC4B598A5F8911ECA597DCEAED596F69F2A51BF767D5F75D6AEB167CF + 5B7D5EFE9988E0A5768F4002BEF14120C4D2D0B45B061CB6FB04D0B7EF5096C0 + ADDFBB4C940576775FBF9D3EF9E9CF69DBA56B0E620588058B25D9B41286AF9E + 51EE93C05D3D6025D82BA9ADDF432D250F00F63D7DCB3D983AD239EF7E985EB6 + 7B3ADB3D08F82AE2578A00F8153B86DFEBCB32CF4401BFFA81DDC7AD0CF8C5A6 + 74D13B76BEB8F4F3811CE93B9B07DA88F805FA16F8EB70B748361E8813B415DC + 1DF8B0DC1B795F5EC65E405D2B7B3BC6530502AB3FA9E7AD6D13E54B3F5304C1 + 8CEBA66D6645171BDB9980BC2F9A6631F6E38140AB631C885D6342E5193746B0 + 1BC793C0615F61263C88C83627AE325ED5F8F579B645618C0B042EF548650031 + 51BF0800AC659F3DE2778D8D9BCBC3941A6B6B5C4D7E72187F6397284BEA5703 + 6087BBCBADCE52CD119000AF00B06C5102BE12E546944D9ABB68FEA2794B2CD7 + 731AEA7C5E13E64D6DF05773AE3C0FCB4B3E23C1DFB1F397A6F1F397D95CAD03 + 80351F24551E79D46FCC3357A46C6DEAB9266903800BFC6D8BFC951D09F67641 + DF22DFDB37CC8D6BC5259E519C87938F8AF36E5245EE66889BE7EBB2513F8ABD + 7F4D02C11100037F9F79BED3D7CFC179ADAFFC0A51A3A79152373800F62DB282 + 6F6438C2BFD20A628BDAFA443F8B7C2F759B01C7E21C936675AB9CB7397F917C + 43F22B75D4F16FA10C716B002C090207488C8FCCD23670AC08612D03DD26FC6F + B50FB0F6CF357EC14A75BF5A6A53F71B4A4D7F1D63ACD9A3AFB4F297FA4A8A3D + 94A35C7BABCD07FCA492BF1601811D04236CA52EBEC6B663FC56AF25AACD9F5E + FBD96379B88AC76B53DDAEE90B146E03C3FF6012C01C4A6DDC27D6D7ABC5B6B5 + 979A3643B1BA58AFFCD3AA3EF6506A3B46ADB63E75D9C41EC7BECFB154B7FD7B + 01E05A6D70F61F46FC13ADC0B745FE4F939637C7DFD4AA3C40BCD1404B814B52 + B3B5B6FD43CD7BD9EBFD7408CCFBD8AEBF3B00FE1D34E0223F4C0DD64F756D8A + 00B80DF6B629B76FBF493EA97413FFADD440603BF6003198D060A81A244903EA + DAA0708B74DC5A5EDF006064F671362874F84B9AC5204779967E067CF3FEF852 + 2C9366A4310C4A6DE0493ADA069AA3781AD18E0FEC25AD0799BE2C7419C036CB + 434B01022307C1E4EDBC8AD2CD03E00EB0AD07DAB2F71A8863575D94ECB57AD5 + 63F38981294E16EAC984D4AB4E13923811D131EB898A00708C029600BE525C06 + 5A4B3D6B82A649DA582685966AF2A789602F318924D54491BC2697B19DA449A7 + EA4965F389683D810D93DB3861A60CE8ED9A38D700D86C2CB5A589749C9CD7F2 + 7A9BB08F9FBF3C4D7A857D7CFB1A108CC87BA4EFCB39F5FD7F0BFC05FCD61A6B + F228E0B23730405810B8E388C891BFD149817064207794988804660FE031651F + E0E80091DA9C27729890BAA364C9DA2EE17CE1C979498E1D396C48A3A30709F4 + B60160492038C260A20E26F79BAD3897108E27815F39A1B047E75484C0D2F4D5 + 440C6F77A799835A1C73445658393ACDE454234572A43565522BCF284E3A4160 + 07C0C5B127E75E8CF2F07665C940C15F521C88EE74DC962383D973EEC51DD459 + DEEC0061C0308018189CA1F09B0D0046382BD9BB2E03E0772D7D2BEF6B172408 + 4C24300098686045012BAF3D8281BEBEB71E70D8F2CD72CF05004708DCD8C9BF + F3619AF3D6FBD9F1FB7E5E0A12473079E0EFE24FBECC00D8CAEC070808C6813C + D7DACDFBE0E3261238C25FB74956877050231CDB38BC718A6B496939C8655FC4 + 9E862687BF9F7FE565FA292258CB40AF38C6529C47D292AFD85FF84BEB7F302D + FFFAB0036087C0476973BCD1C6F317D2964B97CD7EDCFBF71D3B91965ACA129F + ECFDB8CA978126E2EBA80360D47FF27889FC3D91565B0A60F0A59F2F9CF574C5 + A9E3A9EFA4F5073A9C67DFDF335957CEA5F5A67597518E625B5522DE043714DD + 4679F5C57369157907BE199E90EF8227EC1F0C70B90A18A66D47EC030C0C262A + 6FDBBDEB69277B87DEBF9E36B194EBDD2B0E7600C04405AF03FCDEBC98D6B2B7 + B000B0D5038529037565239298326058701870EBB6D256902AC22BE06F04C0D8 + 057EE98354AF3AD29CCF4BDEEE06D659BA85BD871DCCDD4A3BBF2B900DE80650 + 03BC996DEF4F80DCFB1E590C7423F277D70FF712D195B4DF0624FB360B0818C1 + 2029527D5B3B96281664DC697D808600442D492CE0EA10D36C82ABE40581D506 + A8BAFB4F1920E763E43AB5250FDC64FF5D524032A21F7BE3EE24FAB48059042C + 25A52DCAED325496A8173425CF793826E7D7B9C967689DA130C20EF8A52F7BF2 + B29FAE2F0FCD7958B6D904C4F5259C2D654967802FE09765C889045604F0BEEF + 1FA63DDF5AF9FB470E82F7FF9C01329F3360DDCFF9A70C79E3FB465E22829794 + 25A2FDFF2A501779642EEF1FC7B3CF4951E4E4E3E74CB986C34430EFB4EF0B50 + D6A3D5EDBBC5F70900DC7CDFAC4E36016087B8FEDDCC70D7BF6BD61600CC7711 + 084C5FA2DD750CECE481C244CD6FC16EFDF5DBD16F410F5190DF7CFF86C3DB0D + 7788BECF9077E35D7E63FC5E00C777DCE680D7243BA9FA905F7F8763D8EFFA36 + D0F7B66BD36D3BD76DAB0700DB3181BE390A98FD7DEDF7EECB3C731D32956B0C + F6F8F00960976B14D7AE752CD16CD72AAE5F68B55D079B3C30F7C26987B8805F + 5D0729FB35D2AE8F82C00E7AB936FAF5D1AECDECC36ED7502DF9ACC85F3D8CD3 + 5C934F5B3B748A25A14F9B2D43DFA8B8F72F809748DF81F0372F07CD0342CBD8 + 32A0440047900BF8E5A125A5DCBFC8BFFDF0DB74F17FFE5FD3BBDF7CDFD4C77E + 7E9FB3FBA13FF004ECB5B23FE464F925D6761976DAD8BD13E0EB11BD405F2B2B + 2A78A1DD3B79208BBCA28579308B259E913F8865F7F057ECFE3DBFDCDB11F77A + 015FD9642765F9671EFE02FC2AF21769F9E766CFDFB7ADEDDB762EA27C5FB7E3 + 1DC891BD02C00E816D7C03E8150016D8553E8E89629D4499B192E02F79007054 + 04BF28825E8DD92204C68E6D06DB80D8D8CE9781DE62752DD1BE8301602DDF1C + C7942F98666EC863D51AF422CA1AAB327E8D750DF88D2A91BF82BF93576E0A00 + 388F930574193F2BAFB135E9732C15CDB8DCA471B7DBCBF83C0260C6F51AFF7B + B4ADA906BC4395057FF5E0A9EC9AC374CD4DCA1C46731BCD65EAF9CF84D76C9E + 637326015F00703D9FC2AE395704C031FAD723804DCCF322008EE05772B02BD9 + DCD0DB9A6A001CE796CDFC93396999C3FAFCB4E4BB6C2D7DD1182281C31C57F3 + E25A71FEEC7368606C257FD8BA805F497372E6F1E4491511EC65A06F00C08AFC + ED4400671F80E6F319F476FB1746FB83EAF81EF055647F450D7DA3FF6264F07F + 0CA65EFE1214DB49D137D3A6E1F6F173B480DF0885F151C95754FB9D1CC236BE + 2DF29D720D7465936A1BF908779F99307D580058920F30DA64EFF803DBA5BE6D + 6A6B3F949ABEF833E523EDE527357B6F1FEDE03EDBD8EFD78AD7117DA47ABD35 + A06EEBFB9BBF96C00C1A7F798B8F3DDA0693DA0F47759FE638FF2400B8E63835 + BC1D0CE60AE4D6DB850E58EAB992DA0DCEE74CB15EF9A7517DDCE1E849FA576D + 9BFFAF573BE51BBB49F077446837A802EC6D531BF07DE6997145B9DC0A5EFF51 + C43FD1067BDBA47FDACBF6E620B7912F6FF20051C7975010F80F00DCAA015F3C + DEB7606FDAF15EDA05A587060260B3FB45888B6694ECBFBDBA2ED04FA10117F9 + 21A4F683F5ABDB44FD4706C03A47D7C0665CFB00A9550C967E0B001CDB01764D + 1AD48C70184C5AECD6678C6934E71D33254D9E312FADDFF67AEA5BBBDDF3C0DF + 312C4933DEFE176B57C35D9DD7CB6540DB3A502D75517199E63C08CEA0B71E64 + E7FA6EB01B6D751EE9B811ECF612F5BE2C3313102B0B004B4C149840D4939438 + 9188138D28DAAA4D3CD67000B0A27FA33AF5ED00588AD0B697047CE34491BC26 + 93AA979A8961A99728FB8474566719AB66F21A26B671A22C9BE02F8A13635222 + 8099708F294F4F3793F030B1569E7A26F6131610B99BA5A7C15D7333F86DF4B2 + D90AFC05EE3AE005FC12095CA42860449E885F45FDA21AFECA51D1D881C0AFAE + 4AA3ED750080C799E404898A8E92E838912D0AE74A04BEC81D3AC5F1121D36B5 + 13074508DC0B004739000E4E25399990006F04BDB15EE05775595B9BA8091C66 + 75F46F749A75C15ECBABAC3C6D1C245B5F39EAFC9896EF0580E5D4F33C00785B + 27A204B09B8553716F7AC923815952708FA5DA13987D80DF7435ED77BE911D9C + 65B967202FE99C7D39FAB70D00AB3E2E014D1E20DC15016C796F4394B0E58908 + 06F22A2278AEE51519EC6593A0F0DCB707026045002396837CC5CA8BDEFFD49D + C34D0491B5C7C98C9A88E08FF39EC0929CD538BB7170CB198ED39B4825845D36 + 87C9051CCBA1AE7EEC9B08B8F565A1CB32D0CBBE3E9C961E3CE451C0CB0F67F8 + BBDC849DB4AFEC014CDA7784E5A14F3838F6BD854DA40060F6025E8283FF3840 + F744EA3F696D4EB024E889B4EAF4E9B4FAEC695F5E74CDB9BCD72430980833ED + 3B4979DD85D31EFD8B365FBB9836B287EF85B30D3476901C20B02F077DD1F297 + 33E05879FE94C3900D0060964C2502B802C01BAF5F4C1B2C5DEB51C6D6F70A91 + C2671C0013910798D97EFF860B5083D8CF13D84BE4EFB607371CFEAEBE76CE23 + 84013D924706DFBEECA9EF036A79DAB0F730F98DE4EFD831AD9ED4EB2CAFA59B + 05B0A2B021A016118ED86AF82BA90F6D89686499E01DDFDC33DBADB4FBBBFB56 + BEE7D1A5A49D76B7ADCD1DB711714ABAD3CEC3DEC43BBEBD9D97D6F5C8CC1C11 + 2AF0275106F692020B63997A458E92AABD20E27687BCD68ED7F493B5FBE5B12F + 310D9CDC6EF5D8588A987A9628065852AF258AA9F7A5A7819A00486F9701276D + 595E99F6B42162D5A127FDD4DF94CFF9ADB5FDCEDB2287C2B4B13AC15D6F6B76 + 14EDAA5319F80B60968D73E6769DC85BF622F6F3027CD9ABD852BD4FFB6963ED + 01BF7BED75EFB3D4F7033651DE639F2900F8F51F1FA7377EA2ADBD16B3F17E3A + 442EAF09F0EB7B32939728DBFB2FEDFA3E035D415CEF5FC46BA18D00B03E3352 + 1E2A70593D36E450B7C05E802C80B601BA8F6F3711BC8AFE1500A61EA82B104C + 4AD4F9D66FEC3B6DC7101C8E0098DF0BDF5F3D24A1EFB16CFA2DC4072500C080 + 5C04C8E537411AA37DB10185057C29935F7B930735EC1866DB64E9A6BB76EC5B + 40DF08816F35107813E7B80BFC2DD78F10E9BBAEAC3AC09EE488EB92A27CD7D8 + B56BCD65BBB65C6235830C7B7D3503AE6D769D03E472FDEB3B7DDCAE8176FDBA + 743EF59DB5EB2CD751BB46724DC49681AFE5ED182B00BE80DECB17D22A4BFBB8 + DE722DBE60C7F265FAED9AC9F594FE7E9D3D93FA7978E7945D6B913FC893F7F9 + 05F82E3F41543090F88CD5D9F5F7D45987BD405EAD06E1F928BB472CD78347DC + 830AC815F425D5FD8BFB15B62DE72EA5771E7E9BB65FBEEE76EE610E78AD9EBE + BAD7E9C128D56367258C6576BC087CC97B7B95E947342FF76020F0C766B7BA57 + CA360DACD2B1D0EAB97FF310170F73F1F016F7EDB8D20779227EFD9E5E1E1A63 + 4CB0D0EEF944F82A0A18F80BF025FFD27E1B8308041FB0F18569FE011B43B0AC + 73117BFD3691C07BBA21B0CAA402BFB293C7A63C6326815D40AF60B13F4007E8 + AD00B0B7311B12FCD5037C516E13D835017F2300D6F82F8E0F59229AD56258DE + D9237F81BFC0DCD256B617CA78953AC69E536C2C2BF958D4C6A6755D04C11AD7 + 7ABE82BF110023C6CD1A57C7ED551857F320E6B37D363EB7F13B70D71FD42CE3 + 71D20880BBA2809927548A7303CD19A2629D0030D27C44F315146DCC7390E630 + D4D70058B6F1365FD203B59A4B0182058323FC656EC5D2CF6D00D8217079E017 + 09FE7645FF3247443C144CB900E0A6BECC2FA3E25C1345F0AB39ABE4F696BE9A + A3AA1D73DCB639B284DD9781B6F9F5507370E5A98F73F638AFCFF505FC96E85F + A27E3B0038C3DF4EFF8E5F21FB19A4EEEDA9D0AF01C0F28D44DF8AA436BDEC52 + F4AF74FB6CBA7D336AA3F6EEE799D41EF9FB5B00E076C536DDFEB1DA57467E30 + 00ECED7AF9E24CF2D5D53E4157F033AADF00C5F64FAB71A6E8131D44DD3E5A52 + 69A0DF55529F5A6D75751F9505A9E5271D35C1DE3B13655E97DA49F198F5B1DA + EA86D6F3036CEE4B2FB055FE72F79987B2AFD0586C43497D86525B1FE5DDFECF + 0081C56F9E42029DC306C08519A9DD0086542BD62B3F0CE5E596C331E23187AB + 78CC96FA5163A7A4D1E39EF7B4FE5FFDFF1BE23DE83A6E04C0B17E5015E6D94B + DDE0B75DADE0F51F442C033DA206BD4F2C7BA3E29BDF083BF5BCE182BFA8E503 + FB4383AAF931FB7B691714577C4FB3B4147354E742C405538AF67F2CB55DF807 + 53BC213C8D9E1400E7B6DD37C7C134D84DB8ED262DDB93A8E94B6AE53878413E + A0A11D0308EA91C3DD32281A4CDEAE48E5BA8DA96DC0D6D45B1F015EA9038019 + D8D80051031D1B5C8EB63E0C26B868CF5DD89F2EDD7A9CDEFEE89003603E8351 + EC5332617A5EE23900E001AF011BEA3558AD341C005C835CC1DCDCA70371EB3E + 753FD5C56348949F0400D7130FD547453B29FD54261F01306A96840E93274DB6 + E2A4AB33C11A7E0470F7C46EA022FCA5AC89A5EA5546F5E433DA0703C0CD24B9 + 4C9C65D744BA7532FD62EEC7C4D927CF61322DC589B6036026F6685E27FA775C + 03802D7D79B9B5B749FF6C1C04D4B700E012252C00AC08E01A00D711C07A923D + DAC79B80BFD121121D21940570E54C896D1BB05B9C2888327DA6007197AD6FEA + A5E8D8A91D3DE405778702C0D413FDCB3E6482BA3899DAA4E85E39AAA24D76A4 + E59F7D8FB422C15C39C8A23DC2DE58769BB50726CBF1D688880D9C7DBEDC5FDE + D74DD037CAA33EB6E6C85F17D12494B7ED492F9A66EF00FA125D92530160A27F + 11D1C0334DB376589B5D44FD0281DF6A002F12F4A5BE0D000BFEB2F433A057CB + 410BFE0A0CAB8DF606D6B2D0CDDEC0C8FA90C7D10BD475C76F00C08A0472E730 + CE62968B7E9708E04F5DB27B6430CE6640EE07D63740602438DC0585E5ECB614 + 8736C2298EA817FCA52CA73A7DE4585FEA7B03B32F302962CFE0836999A544FF + 920284177FC992D1D6E7A0D90F6718BCC4ECF45973E24C5A75EC942F21BDF278 + 8EF65A76E4785A6C75FDC7891C633FC86369D9D1A31E01B6E2E4C9B4E6ECB9B4 + EE3CD163563E75DC812FF05730587078DD853369C3C5B369F3D58B69C365F69B + 3C95566337DBEAF3A79BA54C575EB0F68AF0BD46F4DB99B4F2BC9DE7D2D9B481 + 08BA121107401160595BF6161600F6E8E2ABE7D2EA2B96670968806C59A29574 + FD75B3B104B5D9D75CB3B6372E64C86B750061C0AE47F911696876A0AEF60DA6 + 0CE0A50F2902024700AC725E329665A1AFA4F5D80BB412F815E0559E3A2D25BD + A5AB2E033044F4224BDB6AFFD23D3F3CF4A8D2FD3F7FE3A0973E40B3BC6F6A06 + CB94818CBBB001E5007205E4ED34BB605F14B01748A8A85FE51D147E97978146 + B4958D3CC091B2E0302920F400D0F6E7C75E07A0DC69ED1D2CDB31B1AB9EF67B + CB71391FA98EA5F334C7F763E5F3D397F3E8183A2F65FAEEB632E016584A0AEC + 25EFF0B944FF926729678F3E2E757BFE6CC7001EFFD9DAFDE55B07BCBB38E64F + DFA66DDF3D4C5BBFB5FF19E0FB2F3FDA67F05DFE9FADCDFEBF6400ECAFEDC76F + 3CAA97CF687F03803304CEF0F79EE7F7D9F1F612096CE97E9684FE21FC1F760C + 87D265296820307BF502D411EF4B04B792BFDF267D1608BB3E4BF2EAEBF0D7BE + 5B4DD9B4CBCE01D4F5259B4B84AEEF456D29DF510030DF278F40F7EF56F97E91 + 0275AD1D90D79720F77206C0F9BB772F6D0940D897881E04006FE35C8FACDD03 + 6B773F3F24C16B00026FBACFF79EEF7C7E30425018E0CBEFC4A37EB117100CFC + 450E84ADEF66B36DBC6DBFD55B372B080C00C666FD6E027DED5A60D70900708E + 00EE2C3FCFC326BE1A815D5BF2C32997ECDA74D9C1EFAA8BE7EDDA76CE050C46 + 2AAFB5FC2A4B979F3E99969FB1EBE0B9339EAEB0EB2AFD2487C825DF47BBB3A7 + BDEFEA0B563E69D75CA27AA93F0BC4B5EBA8F507FCAE3E67C7315BDFA913768D + B6EBF409966E3E96961CB56B394B3CDB75DDA37A5D9D259F01BFFE109089FDE0 + 791068D9E16376CFE8C8977E6E89FE95B83771AF42DCAB88C875905BDAA98E7B + 993FCC64762DE52C30ECA9497BF8FAB2CFD8CBBDD2EF8DE55E0AECF5659AB93F + BF97F7EB07F82E2472977B6F59A943F7F1D8967BB8AFE4F1FEC79EF250175B42 + 300ED04362BE420811BEEF59BD89FCD24F0EA6BECF0F3B0C6609E839D6F6451B + 9F08006B29685F0EDA6CEC012C008C0481515C023A4260F2405DD90478A50880 + D58E3CF0570FE9017F89F415EC55F4AF2FFBBC29AFEAE2F6CD56CFAA32658C17 + 013063418FE8D5389071214098F63676D4C384D8048BA76D605FDFEE1569188F + 4600CCD89531A900B080AF1461F0141B0F0BFE6AE9E7E74CA49EB7B13363EDCE + 98B93386F6AD58FA36A6C925AAD7E1AF95D91F18D5F03702E049CBF23C20CE07 + 6AC81B41AFCAB5DD01307DB055F3156982951179DAA93FEDF4E02B79E64C131D + EAE6F992D2490B6D9EB3C88EC51C8A2D7898A31540CC43BE5D7BFF4A0E73A502 + 74CB1CD0F326E68851D8BA0071994F46D573CD1805DC3D5FCD2B5AC576DE9776 + A65E0078809863536FF36AE6DF9ABFD7F37349F3F4388727D5BC1E8D6A8ED589 + F8953208EEF6118C9C66A929FB163AE0B7CD1711E16FC73F5184AD875F2496E5 + 07198EE2F1E23150C767F3A2D5757C376A177D3AB13C40933A60187F90FC44F2 + 234923CAEA76D9CF35C3F375346FF6779166F9DEBE40DD02766BC0AB63445BAB + 8A1F4E921F4E65F9EAB0459FA17C848DBFB097A21F72305B5173FC582FFF2792 + 6FB4458AB2EDF85133E01C0C006BBBBEE87B45AA6FB3D5E218FE3AED35E327C5 + 273A7AE24C173E525E77DD271EB73E7E5BDDD07ABEA863737F7B0B0096F4DE90 + 976F7E30A94F3C469B7AF58DF5711FE0C60EFBE8E21FFF7E6A85942D6A6B27F6 + D3A6BA9DE70BBC6CDA500EED0648B033E687A12702C0B16F5B7D8B78ED63274C + 4B1327CF4C13EC1A451E101CEB01C0CF54FFB7EA3BE5A2BF27006E83ADFF90E2 + 9F1800749F46F666C50F461F4A23DE7420656DFF438349EF65E73DE57DB41FC1 + 5301E07F273DE1D339F1C23E1C0DE726325C39E41D6737BB007CDB94DB776E8E + 8329DEA8DBF4247D181C689021B5F51F30A0B17603063DA5CC20A3191CD58AFD + 505B9B3200D3602BAA6ECBB990E06F94063B00E07136501EC9B2D1F61B78D926 + 565F1C3D9F36EE78C3EDBCFFC0DF3136201D6383EAD1ECD35B0D380788BA9601 + EBC0C1310363815C06D419DA528E83EC086823A8D5A0BC06BAB15FACC356B751 + 39B6ED0580E344421391BA4E8A76DA027B5516FC8D00983C0058932BA99E70E5 + 09D6E0D1BF284E08E3E48E9449639CECC92E5B0D79DBA06FCF362D135A26B371 + E23B541E69828D7D0C20B800E066726D8A13F306FE328937DB18D36826E0736D + 625F9C0013710CCC5E6AF625A69C0A00379017006CB6DA5E2B426077589427D6 + 51746620227F9B48DFC59DA7E4A50878A3834475887C74B4B8BD3872548FBC5C + A06F84BDD1D6666F83C13896A6AED89CA6ADDC929EEBDBE4CE2439985C2BB204 + 866BE04B4AF48222181079A0ED94D5DBD2731CD724271A791C6435008E659C6F + 51BDEC2E223736EC6A96F3C31918356B538E20791188BBED803B09A7E3EC5B6F + AF7BE3F63473CB2E07BF73F6BC915EDECD72CFFBD34BBB3204965E60B9E8ED40 + 64F605C6719961AF202FF9593BEDD826F202C1D47B9B10051C412F69B4290500 + CFA3DEE41038E60B041600269DFFCE87690151BD0060F602C409FCF6876EC706 + 045EF8EE27D9595C1CC82C2D291171B400A77201C00E730B1C1604AE81B03BBB + 8B631CE777E3DCAEEC48E05811578AC402FEBEFAF9170E7C01BFAEAF5846D364 + 6D971CCCF0972860A286D9435811C052FFD113EEE427C2CBF7793C7E3C2D397C + 382DFEFAEBD47F82A5414FA4F5172EB81C2C1400AC086045012B0506AF634952 + 8F48235A8D3D83CFA5D56603FE12C5D64FBB8B677D4FCBB557ECB8E74E79341C + D1C144016BAF4B5F1615112D6C7580629697EE00E00C643AFB010369329CC9D0 + 8668BD0C70D75E23BACE5E9BA55B58269625654B1D51C180E1CD77AE96E55F59 + 2696285FA2808900CE6098E5A43304B6BCB5F54841203260F98EB5BD9DB5D96C + 802B44DEA13060996397E84696941600DBF10D91BC40302215ADEFA33B0EBA80 + 5A4438B22CB4A27C01BE1EF96BA9A473C90E2C56A4F016B303F922E01510248F + 94DFFA8D1D3B940564E9478A1D09360AC4AABCEF971C0DAB63084292A78E2592 + 0524753CCE4959C7221F5FA75E83F211FE9247D437AFDBDEA3AD40707B0F3CB2 + F8A710510C0406000356ADAD03608EE111C4DFE6286180AE1D5BE01500BCED3B + 4BBFB5D7FCA31DE7E7EFFD75F07AF67CCFF2CE76FE6FED3DB13600DCBD3F12E5 + 9B812FE057F0778FBDA6FDD6671F30F91B7B6F1FDB7B62FDF7FFF08D2F05ADF7 + 46AF8994D7C8FFC3FF25715EDAF11A246CF13DD77B8BF4DE51AFF75475EAE3D1 + BE7CF6F6FDE1FB181F56E03B445EDF35B521EA9D3C12BC05D8028DB73ECEB017 + F8CBFEBE1EF54B14B1B5250FCCA5BD3F08518EEFDF55134BB5EFB4EFFFCE47D6 + F7917D0F7808E2C1ED1CED6BA2CC6F833CCB3DAF63CF5D7ECF56DE4624F0FD5B + BECFEF5ABB06ACB96EBFFB1BFC26F9EDE6F66B697FF386AB03816F39FC5D7FC3 + 8E77DDEA5985A05C43D80338470113EDCB52CE79C9798F08BE66F66B575C6BAF + 5C72081C0130B63580612B23C02D6560EE2A1EA831ADB968D7AC027857D3CFCA + AAEB671FF6B3A7DDBEF21CFBF812C96BD74FBB0EAF396F6D8BB0AF36B16C7F5E + B9E17886BF3CF863D77EA27F7DF96796F93F7ECA977C8E11BF02C04B01C0A6A5 + 8AF805FCEA5E010C6ED9FB57E27E03B0E55E25D0ABBAFEC3C7BD9FD771EF2B70 + D7EF5126EE750E84B9CF9980BF0D04E6FE57EEA7FEA05580BE510E734D82BD73 + EDFECDF60E5AEAD9FBD086F6763F67658F1CE99BB786500430CA0F89BDEF11C0 + 00E045EF7F96561D3C9ED61F3D93B41FF05CAB7F69CF5B0E7A89FA8D7B017B14 + 305B5B10095C00B000AF60B0CA8ADC8DB057F918D92BBBA02FA920F10BD66606 + A097B6F4B13C63B6BC628BF5B5D4817059F2D9017089EA15E465AC27F8CB58B0 + 5EDE19A9DC80611365DAB224340F2C6A9C19C7A435008E75A886BFB9DC0D7FB5 + ECB3A0F0E412C5CB985A11BDA48CC3A7AD30314E06F6DA98BD19B707E0EBB600 + 7E116D18E733FF20ADA1AFCA0E696D1EA23AF282BE3EBFA07DC94BAA9368A33D + 7D590569D2927C7C3F9ECD5F782016F91CC7E64BB49B6CED51336F5AB0224D5A + 60F3AA576CCEC37CCBE663DA9267A247042FCB733B9BEFB52DFB2CF0DB006052 + DA46D1AEF4190A0093E6B9A5D9CADCB406C012A0173565E6B083A86BAE5CE6D4 + F5FCBA16F3F4982A1FCBCCE7F1013CF3FCDC667E0FEC8DCA10B8E323407E8CE9 + 766E4BD9E7372BAF40267F4454F4574410ECBE8AE2DF40F27FFC1AE157E9E573 + 41AA8F6DA20D45DF4E8CF48DD057F2E398E41B92CF48FEA31C6CD0F17175FBB7 + 6684BA1966C70766F989C53F87AFCCEA14E59B41716749E84EDF816A22832BDF + 9B7C7351759BDA4FE82A7EC5AE72554F44307E43F91FD9E397B2DBA8A33CDEEC + F82ED59F34FA466B155F679B5F1545DF2BEAE58FADCBB5BD97FCF593E7B8C85E + BFFB452D6DEA5AD4769EB6FCD32AFADCE52F8FBEF7A701C06D1AACADCA3A573C + 67DD7744612462206D6DFE6EAAB8CE601AC8CE9E4E0D3312CCACDB7481CE16B5 + B5EBD54F6D833ACCAAA54FD556521F4484F338BB2E4D7A6E968B3CD1C0B18DF7 + 8BC76C3BBE6F530BB0B5FC08EBD7A8B41D44ADCC34C801705BC53F8D7E33008C + EC4D8B6FFC1FFAD58A3F869163F8C2F383B60BCA3F1000FEAD2FAC4F7A3CB56F + D350F5B51CF0FEC600F869C5C0428A37FA5A6D7D7B0E6AEC589E0F760D965A55 + FAB91838691025C9661A743017C4F9DAE0AF0F746C00C912CF6381BB0C5AC74F + 4B139E9F9D66CD5B9A9E7BC126020C4A27585B13F9F84461DBB9651F6E14B022 + 7573B42EB60C8023988DF9A8BA1D8A10170916D77614FBAA5E6099BA7AD21055 + 4F3848E344071B9307B56983BDCAAB4D632F932852265CECFBAB4996EC83815F + 89495C9EF07543604DF06AC5BA38F9944D934AA43EE4EB892A7B00D7935A9FD8 + 86096F9C2C53E74B5D9576B14E62A2CD64DD27EC61528E9894037E25CAC0E231 + A6D1B461D28E03003B93F739E489F4CDD1BEA881BA05FAC6A85F897294B76F01 + C0E463B9B1037FA3A3647137F0255F2BB6A72C00DCD4E18C29C2E9E25AD201C3 + CA47A8AB3AD2D88EBAE850523F8F2EF08803B3E3480A8E26C901B0A9895600F6 + AECC62BFDF060803802917E718C077F28ACD0D04C639169D6688B21C6BD1C186 + 234E11BF5DD0B738E85C6B4CEB7736D11C72F4C9B9478AD370F6F603E9A56D07 + 3218DE92A37F11009874CEAE031E0D3C73EBEE347B27D128795F609689CE6906 + C044087B1D40B8005FD2993BAC4F05801BEDC99ABDEF4D87BC127B0123F202C2 + 8A00D6F2D044FD3A1C36CD2E00581018A7AF227F0485517600BFE7CE603986B1 + 3914C699FCFE271E412447B41CCF4404E3AC96C31A00CC3EBF442FC9812D48EC + CEEE02770578912F75696539C96597C35C4EF5C55F7CED7B2636E0B768D9A1C3 + 1EAD252D3F9C1DF802C04061F2883CB6C507BF4ECB8E090658BBC3871D020312 + 58027ADDF9F369CDB9B35E5E7EEC88835ED47FEAB8036181E015A7735E2098E5 + 491D0697A5A08920464406536659539639050CA315E74F39E805FEAEBB723EB1 + 64AADB2E9DF576CDF2D117ED980060F619B676C0994D370131977CD96822F836 + DEBA92F2D2D1ECDB79CD814EFFF9130E6FD8EB73D3AD6B05F4581B227D3DEAEF + 5ADA042C326DBC0520B663DEB86CE5EB690B4BC712E16BC7DD72E77A62B9586C + 39AAB013194C0A440664B1E434E5183D1CA38ED56EC7A35B2EF20EA04B342351 + 8E0EBE1E00CF4AA424FBAB5A5BA099F649AD811D700E1B4B5403D9807C40D5CD + 0F0171ECD7DA0D0A0181D8A907C80AC052AF3ADA23D593DF4394ED8F8F9A6364 + 9898A1AEA0B1443B1D8FBA2DC0406B477BEA0492E3B128731E969BD6B1F53A90 + 8E19EB48F5FA10F51C1B002D78AC7EB293D24FE7DDC6FFFCD8FE078EF90350FA + 91BDF776ECC7F63FFF40B4F1E3DCF6B1BD472CD54D44AD89259EF75A1FF6FAF5 + E59EED1C44FD46007CC08EB7972860131078D737F7F29ECFE57DD4FF45CA39F4 + 7FE8F5493B3D6A37BFB7AA278D65D9B687BEB291EA73DE5A1E22E0FB4554B9A0 + 6EDC8737C3DEFC1D140CE63B463BEAA50C7273543B50199B966406DC3ABCB57A + 9665468ACEDD76DFBE7BF72D6FB6ADF7ACEF4320F05DB76FB2DF62FE1D5EB33A + 3B8FC9976B0EBF53C4EFDC7FA7A53D62F5005612E018F4594BC4EEB56B26DA10 + F14B7BFB5F6F5D4FEBEC7AB0E6EAA5B4EEEA65BF6EF06089967BDEC0F100CA25 + 02D8977DE63A656D375CB76359EA51BEE73B11BD94D75DB9E4F097485EA0EE86 + 6B769DB96AD7A68B760C403091BC67EC1A6BE58D57EC18D8AC8CD69CB36B9D5D + 7701BEB4DB7DEF6E5A7FE9625A7EE2B8B7A30F7DB946B352030FEB38FC3D7AD4 + AEE7ACE090F7F65D79CAAEB327ED3A6B795679000893270A18F0EBCB3EDB3D02 + E0EBF705BB87C4FB84C36074F068B9F794485FEE3FE57E2400CCBD8E7B9E1E60 + A22CD02BC8ABFB1D6D742CEE99BA2F3AF4B594B21E8C7AE5A34FD3BC77ED5ECC + 035A3CB065F75DEEB9BE5F6F81BFDC8B231C26BFE0BDCEB60EF4F1153EEC3E2E + E8AB7BBFC60208F83BFB808D23DEA29DF5332DFFECEBB4F6F0A9B4E4A32FD37C + 2280F7DB5802F85BF6FE454403037D1DFEEEC9CB382345FC0AE04A0E6F89DC2D + 70572952546FAC971D4500CC0A2DD3CB52CFCDD2CF26EF4FBE2CF90CF0E501BE + 998CE94C9E2FE037C25C8931A1C68594191BFA7830F4C106FC75957127E3CA38 + C6D458955436DAC5716C1CDF3663DB12ED9BE16F678CECABE7D8589A31B5E02F + 636C521EC6D4039902C0CDB83EC2DE906F6C654C1FC7FE3E0729F38868F3F984 + B5A36E1C730EE62396677E11572BD25CC4812FF3084BC7BFCA03AAABD2E4C56B + D373AFD97180C0A5AFE631E39A87626DEE647323DA4D59B22E3DBFD45EA7E589 + F81D5B802F00D8E763612E369E559834A7330D06809B363DE4001831A72C73CB + 5A02C0AE0080DBE6AA68F48B36FF35C539EF606AE6C09A439739B5E6D5B11CED + 757BF271FE4E9EF97BF40174E0AFF29D7ACDEBFD18A6D121F23702607C10D90F + 313C005CFB4724F93E0653F4BB44B5B6C56F6312B88D8A50B751057BA3623F01 + 6014FD47D177157D5C1D9FD78C2A3FC3EA2D3F31FBCAF0A7459F1A29E5D1CFCE + CA81162C435C7C6AD445C9DEA6BA5DEDA76B55ED6B1C423A575B5D97386EF46B + 463F69F189A236BF2AAA7DAEC3F1C746D5ED51F4F3726E9547E3132D79F555BB + 5A6DE769CB3FAD8682AEE4A3FDD748C78CC78E76F2A3EC7364AB40449E7377F5 + 819154C1676DC7FEBBA885ED0C2980659704264BB9AD4FD000485AD5778ED743 + 6DED7AF553DB5E7A82B67ADD08E03B66827DCEE3A7762D052D799F78ECB673FC + 9E00B8CDF84FA5A701C0FE661675D9C88737FE0FFD6AC52F7BFEC20F0E80D1BF + 1700FEAD2EAEF5B1863AEE70EA8723BF790D03FEA2DCA77373FC3D146FEE83A9 + 6EDBEC01DC2240ABE7AD4FD780C706461A40750DA234582295649754AF3E4308 + A8DB00DC22415DD97D7F5F93EA94CA061CF6F60C287B0C2AF5BFE436966F06BE + 1D09FC7634CBEC1A483370CE5038C259F2C391DAC73E71D01EDB4AD8D5060900 + FB44823E66F38905B64AB1AE6D62A336B209FA469B8E215B1B0096287726634C + CE3A00380261A933E1EB2CEF2CC5899E14EB34B1241F2787B2A96D3DF9F47281 + BD35D8459AF06AA24C9E362367D8B14B9B3891D6441B4D787949239FB8970939 + F91A0033A9F77AD2E208E8380632FC1D3F2F805CB3C5885F1C0ECF2E5CE5F91A + 0AABDEFB2E30612335F1E4BADB4A3E3ED94EDE978326BAB73855E444196F3652 + 87BA957098B893A52AA3F19417779C38EE6421C54153F23875E4249213284A0E + A1BA8DF7B53AD97036E5A5E6CA1273FD1B1C0AB30C35CE285F2ABA38A8BC6C6D + 1110D89D5A669B6675D3576D4DD3719205E79924071B799C66312258CE33E549 + 81BFDA930DA75C04C272C6516679BFE8E823AF1447E1ECEDAF3B00C6D6F7FEE7 + E99D2BB7D29E5317D2E2B73E70F0EBF0778B1D77E37607C22F587EDAA61DE985 + ADBB1BF8AB8860CA2F6CDBDB40DF8E32FC8DC236D3FACC32018381B9C8E1EFDE + 2C07BF07DE75CD7BFDBDB4E0CD0F5C7989E8F773F46F890E260AD8978276106C + 79DAE004B67E0E7CDF11F0CDD14072120B1CE38006024BBE3F30CE67228A00C1 + EC1B5C60EFDCF73F4EF3885AFA302F133DAF941D0C171BA9802F296DE5409763 + 5D8A8E7640F1AB9F7FE5CB3E13F14B14F0E22F8804FEDAA3AD0480971E321D3C + 92FA8F9CF0685F1CFA0061228129CBD1BFE2C469870380DFE547ADEFB11C0DBC + FAF499B4EAD469137B451E4D4B3C32F8785A798A7AEB77F2585A75FAA447FC3A + F425F2B74408AF3A93978AEE3F092866FF4A96833E97365CBEE0297B0B13FD0B + D805061319DC942F9CB53C00382F17DD89023E9BD83F18109CA3852FA58DD7AF + A42DC0DB9B574B545E167B71D27783D937DCB892D612596C76604E063B571CEA + 20C15E873CECED09002A658012F088F6F405260181B7DE259210F894F70E4680 + 5E81DDED2C8B5B60AFAB4060C0F00EABDFC612B7568F8822DE7A0FF885ACEEE1 + ED440430D18FDB1509F9F056DA6CF5688B9D8365A78924DECAF1BEC991990037 + CED1C0E5C7777D196560DFA6075667C7D846846C0181D801B1D42160EF168159 + F69EB554A016091493DFFB535E7A99259E6923C0B8556DAC2C20E9D0D1CA82B3 + A488D70680956D7BA9DFFDC323AFDBFE38F7E1B8DB008A448096F34409E80A9E + 0AF252579725EC7A7DFC5FF1FF44BBFC353CB6D7CDEBB0B6DF3D4A7BBEFFC621 + F0AEEFF37B03A8E73512050CD005FEE634435E2D01BDD75E33CB3E6BFF5F227F + 81BF5BEF035179AFF2EBD9697D777FDF0DC2B1ABCC6BE401812DC07A3BF73622 + C5CDE6EF9DD5AB2D65C02E36F2B99CDF5FCF739CD26FCBC3BC8C38DF1F22C7F9 + CE6470CB32CD19F0EABBC552E52CD34C1938CCF70F50AC25CC89B2DDC0C311F6 + 3FE901062DD3DC0060FB7E6FC4463BFB0DF17DDF6EDF73525F7ED9C4EF6B9B95 + 1DFE96DFA843DCDBF67B02F496DF1FA28F2F276F6DB023DA227EFF2B2E9CF636 + 1C6FBD5D07D65C35FB554B0B04DE78F3865D2FAE7BC46ED695B4CEAE2B5C43B8 + 6EF09BCFD70CFBDF7868C4AE23F94194BCB4339017004C74EF0AF6E12DB0D723 + 7F81C144F096A85EB79FCDD1BC44EF12B50BD025BFE1B29D9B48E033768C9327 + D29AB376AD332D397AC4DBBFFDFD7769E79DDB0D10A60EE8CBB2FD5AAD0110CC + B5BBDFAEDDFD27EC7A7AF274037AF312CF79697FAEF580DFC55F1F49CBCCDEC0 + 5F00EF57768FF9F2906BF1E787D2922FF2FD27025BA4FB12363DA484C80BF292 + 027063742FF2FA22F2805EEE9B7A808AFB20F74BDD23596563FE3B9D2D1AB8D7 + 027F81C082BEA4C05EC46A1DAF7EF485D93FF532D1C02FBFC9835F799F5F8EC5 + 7D5D2B84F8AA21D6867B7E7E402C8F011040F8D5F73F4B7D9FD97B63631FC15E + D2F9FB2D35CD35017E59F6D9B5FBAD06D2528F046C25EAEB685ED904803DAA37 + 00E21A0ABB4D90B70060E0AFA0F0D48DBBD2D40D3B1DD666F8BB378F016D1CE7 + 0FF39918E339D8356197181336CB3E97B12363491F4FBA72B4B0003063498D3B + 69AF31663D3655398E61258D73C94F0E11C05A1DC7656365C6D002C07AB852E3 + EBF89026F26D604C02BDBECC7390E02FAAC7FDCC1B1AB80BC045CC1FCABC8139 + 007315441BE6036C390304662EC283AB9AB708F03297996836E63BC0DF4936E7 + 1128F6390B79B36975A48996677E24003C65F15A9F6FB13D8FE65D934C13996F + 3573B50EECD5FC4EB057F33C815FC43C50FBFE6A8E28D500D86D1C37480F1B7B + DEE6A2718E1A2528DC0B00D7ED9BBA307F1EAE98876B2EDE56D63165532AC92F + 10A53AF91046852DA922EC6DE06EC8D76AFA15BF07A9F26DFE8FE1089F4A0D68 + A37A45F0C672A3D28E7A006F54DD167F907C4B5207FEE24BEA06C09D72F483CD + 303BF526F2135FE880DEE25393FF4C60B8F6ADA93E2AD645C5FAC64F271FA2CA + 4FA272DC8ECDCE61A9EF138CBFAFABCE54CEE551C363AD1CFDA0E42BD5FED8E8 + 676DB3B5D5D5AADB0AEE4AEADF6663EBC6B663A8BE56B4F76AF32402B2B2351F + 1270C50F5EFBDAB1FF1A101C8F178F1BEBC8F31AC6DAF70991EFD43F9700BFCE + 3E0601C0BF3B1F09E7AE394E2F75B19F117999651730D28164470E2943FB5A6D + C77C2A1518DA9C2F96A37D300DD67638C7B0FA9EFF8FFAB7C9EBC5272D3FC2FA + 235899FAC663C956D4CA4C83062C01DDD6E81F5123ED4D41231CDA3EA122001E + 20DA546FE81F1AB67A7DC9B3DDF2FE2506F4DA85A502BF51BAB8FDD617385D88 + E385B44DB1DD506D51DDBE97DAFA36E2822BB5D517B51D5737ADE144FFA2117E + 83EB56BC21F752DDA796F668261FFBC4C1008AF9360D0681511708B69BA70649 + BD064D8D621DD231DBEA5AC4F179DA10881B25B82BC0ABCFC1216F91DA911221 + CCB228440DB79EA3920F08190C870171541E28230DA011F94E99413650967C67 + A0DD2D0DC6635EE5A16CBDF25E46D5C4234A130744FD609399A1EAA46682D432 + A9F28994835F3D85DB99A475266B79596889099D267D7192878D896133D9B37C + 3D5194548E134AB5956DEC4C9BD89A545F470037935C9326BE82BAD800C028B6 + 4311FAAA7D978D27B16D42DE4CE44BBE6B925FD56747C1725FFA79CCCBCB9AA7 + CB713610092CC08BA301918F6058A28F2070764898E65B9B007F716CC48860ED + 73A5E5A071BCB8F3253A465E5B9D616E2577C814E70C65B5AF9FCE9770DCF871 + 28CBB1538463470EA136C790EADDD114DA3400D82427549BEA360E82FB37B90D + 07568E08DE969E8BCE301C5B96E238AB9D6ADA2B586539D9A2E883E4984382C1 + 38F41C0017C7200EC10880710EBEB46DBF9727AFDA92DEBC7033FD5FFECFFF5F + 7AF4BFFCEF69CBD7271CF8CEDA6AED36EF743900B6F4F90DDBD28C2DBB9AC85F + 0908DC006120EF8E2C3946256CD1118A73D4A369F6BDE3CB2FBEB8FBCD661946 + 5F6EB144DFB8C3B5B49B6BE539073200CE10B8EC1DFCFA7B69EE8177D282B73E + 4C8B58EAF1AD8F7C59C797DD119C237FD8FB6F0110D881708E14C241AC48E157 + AC0DCB48BFCA52D2EC15FC5EDE5790A526E77D60FD716013298C13BB38B28912 + 7EA538B4BD4C94934738E5E5A11DEC56D151BE5C7470AE47077BE37CFF223BE4 + 29038A81BF79E9CD63563EE2F955C74FA51561C967A428AFBCD427F62369C5B1 + E36925D163478F794A19F5010F4C6E3B7E34F51FB3B696AE3C712CAD3E7522AD + 3B7B3A6D6419D3D34088C30E2300C82B2D05120386D7120577E97C5A071CB132 + 11C1AB81C6C091D3A73DE28D3C200558C25E992CA90A6C01120380D75D667968 + B35D00005F4E1BAF5FF5C83AC472AC1EC10790B99CF7E1648FCE7557ADDEDA91 + 66104C845F86C1085044A42076F248C0298262F20250825380212217379725A2 + B7DC25CA973290AB1319CCFEC4EC2B4A04F2F6FB372DE538C0DFACAD778880BC + E1B099141101E951A28FEE7A99A8C72DF7CC4E942570EEFEF506F602E57612A9 + 09A47B70DDF3EC092C0808E4140416F074986879815DF65925056AEEF9310355 + FA3B38B4F6C05DEFFBE84EDA51A257897E15DC43DABB983C11B28876943D5216 + B0FAFD43B7A95E7D1CA896763B4D2AD3D681B849FD74EEE675A0EF046F33D8E5 + FFE6F55346946553BAA34053DAAB5D2EDB31BE7B64FF37F099D70F9CCD009894 + 32EFCB0EFB6C047F3DBA970860A27BCDBEC78EEF10D85EDB1E7B9D07BE7FD405 + 80897475F11E22FEF7F2DAF87C909FA3FC5FFE593D00C6E6F797F7071B6DF85F + 80E6C073FA63D7FFEB9F359F9BF5031EB357AEBE0782B53C60E0EFA789762C3F + 8E549FCBC05DFB6E9A80C32C67BED1BEBBD801C1C05E45F6C6285F52FA6F7D68 + AFE7FE1DFB1D981D986BDF75E491BD7CBFED37E510B8FCF624EA48F57BA53E02 + 60FD96B1F3DBA42D36ED21AE6312E54B04F06AF6EDBD02DCB5D771F3A65D17AE + A6D59780BF56EF0098340361AE215C5736DEB0FF8D6BC5E5F369855D8BD8A3B7 + CFAE79005E403000986BD672F6F935E91AE60018307CE6745A66D749967106FC + 72AD238297BC035DAE7DC5061466C97D60300FE2C4A5F87D1968BBA63AECB53A + 0030A24C34F02AC0AF4360A282F39EBF5AEE7919D77813F70041DFE556D62A11 + 0E75BFFA3A2DFDF27073FFD1BDC8EF335656D42F925DF723DD835406F02A9297 + FB9D20B0967646DCF35CD6C6CBB42DF7494030A9A27C73546F67E50DA54DBDDD + 87E7FA3DDAEECD1FDA71DECBE098322017CDE3E12E93AF10C203618C050A04CE + 0F8DE53180EFF5CB988087C35E7F3F2D7CDBCEFDE6870DFCF571C7DEB79B085F + 52E0AF8F49CA1EBF4402C73D7F35B621CF78A61EEB28ED057E35166ADA61DB9A + 977516089EB6D9C67296FA72CF36768BD2B82E9681B98CFFD8EEC3816F81BC52 + 1C2F6A0C19C7939E5676A40718BBC6A5C5568F6B057EA37DF2CA4DCD32D0FE80 + A4C957C8B136406045FE6ADCAC072B198B6B1CAFF1B5C6ECB46FA27D0580CB98 + BE9E07688EA03C8A730CD533C7E0A155E61BD4C50860CD5D9A790AF932F789D2 + 7EC011FCEA4159EA81C5CF5A7F87C65616E8D53C4C6AE65A0500B72902E038FF + D3BC7030004CDE6DF417F0B594D5A4BAA2806D1E5A435CCD6B651B2C0278F42C + 9B1BDB7C993973DB1C5A736FCDB191E6E451B489E056A28EBE52DD0FA96FAD91 + 267C0CAC3C967D0D1DF0ABFC68B6AAEAE1AB18A0E0DBF03EC1E711D5E61F6953 + F4C17441DA169BFC3AF2FDB429FA7D24D5017E51DDCEEB8A4F2A836041DEDADF + 15CBF279990A0046A30B006E53ED6B9306F8DB7AD8A5D8B7F61D0ED9D6DBE7D7 + 0B94F636D8E43FB5BCFBF9ACDE61AFB505167B1BF7559AADA8F15FD6B26345BF + ACFCAA43F95D51EC17D5D6B6F6EBA2D8567DE513569DF28369B076F2334BC369 + C3F11471DB0B02475FBAEA8652F4CDF7525B5BCAF13C4DDD301880EF0D2C5E12 + DAEA3CF5B97EADC4757A89155E6BDB40FE03B8ECC0495768D3A552571FE7A954 + 9F2F96A3EA7E6D6D07ABAFEABADE87AADEEDB1AE97BC5ECCD2F235001E54C306 + C0E3FEA900B0E0EFEF0680F5C6FFA12756D797BE4DFFA800B8E5A2DBB36D8B62 + DBC1D4D6F749D5765CDD489E0400D73758CA8329B6EDA578C38DF6FA58C31988 + 480E833936A26C7DEB0150536E1B6C95369E8FF56AD3ABAE9206740C3623F86D + 132098CF8254B65113A7A7313658A51FCB448F6280D7F2BA759E5A1A30760F88 + B33A8365F63AD1A0BA0380EB81368AB6D8663029B2B7CD4EAA6345C0EC769326 + 1AB5E2644369DB4445D2E486B6B1ACBCD44C90CAA46AC414EB53443942E0E100 + 6026784CFEEA891E933FD995EF354944D46912AA09AA44394E5C5DE40BFC1500 + 565E936626CAE3D99FD7F2759D26C9B461CF5EA4B6110063F3C9F89CEEA7B6C9 + 6B92AF493B792F9B1DD00BF01DF592D98B6301208C6301A0AB3D80E3D2D035FC + E589F449F357585D0700530750AEE16FDC2B587582B7B51A288C7305A74A71CC + D44E1BB573686CA9835FF6D9B2544E9C261FFAEA58EE2092D3A8E4D54E7607B9 + 00DD027225F5E9A51A0023CA1E01BC323BB89E5FB1254DB6B243DFE2147B0E07 + 97E571B6F912D1ABB779EACE364BA3A34D4E3939DB0482A518CD81220C967310 + 47A19C8579EFDF5DE9B9D55BD39E9397D28FFFC7FF335DFB6FFF635AFBD9218F + FC4540E097B6B3DCE04E2FCFD864AFC1F202C08A0096048471664A3834E5E044 + BE871D76B3E1306DF6CFDBFDA603608F00C6A6C81B1CB0D8E57C05081F783F83 + 6013913AECD5A7681DF2AFBEFBA93B77E7BEFE7E7A69FF3B1EF5C33E7F388D71 + 0213050CF8554430621F41F6100400B3AF608C088E4EEAC6415D52A2965EFDF0 + 7307C1727A2FF8E853778E03817190B3FF2192F31CA7387924C7BA83611365B4 + E4CB23BE37A36C38E22923F272D80386292FFAFCABF4EA1707F312D038FC4BF4 + 97EF197CE458060802BE00E0A3C71C086317605879C2F22749AD8EA5A12D0F04 + 060053EE3F7E220309C0EE99334D4A641B02F846F8A1A838CF5BBB9596025980 + 262BADCC3E97D4AFBF78D1D35526400D105891780E6D0A0016BCD17E9CB2AD03 + F25CCD001830043802FE2281245281270747D72EBA5D406A33918E56669F50A0 + 2E00574BD2A2B83CAD40976C35F802F26E37ED20DAD752875DC0B3722CAFB73A + 87C2D6C797CBB57ED87601081F5AF9BED91FDE4CBB1EDF31DDB6B2BD766C0F88 + E0B4E302152D05C6B15FEAD607B7AC7CC753A28B777C43D4E7EDAE3AA02A11A9 + D88944F6E3083A1620484A19512F58483EC25BA0A2DAF9714D3A96C3CF727ED2 + 988FC7D17181BF02C8D8A9C72E980DF00496F23F033EC9E7E59F89E6BD97765A + 795781C308E00B38F508DC22EC5B78BDDFD0F61B07C15B1EF05AD9EBF75B3BF7 + 433B2780DBFA7E73CF612EE0D7A37E0B040600EFB6D7B78FF3DB6BDD6DEDF759 + 7FD57B3F6BE320B8BC077A4D82BFFEFF94F748FF2FFF2BE233A3EC9F1FFDADDF + 1E7B5F10509CCF9BCFD1DFBFF21EF1D9B2E4B2AB7C564811E7B4D5FBC9E7CBF7 + 45DAA87CE81BDB08F66223AFC8DF58DE6A79BEE3FA3DC45479498097DF5B8CEC + E577A7073114350CE4955D8AF017E5873BEC5A70FD5A5A87AE10D16BAFCB0400 + 9600C06B2E976B45B9BE74AE2D179ABD7E97976B1311BFBEBFAF5D67B4DC3375 + 5CBB80B9BAB6F9F50E506BD73C5FBAD96C523FC0B8D849519F227DEDDA070066 + 5F765661603586155666B506840DF88B1C06F3808E5DABFB8E018A4FFB92CF7D + 80E0A3277D2967568440CB5829E230D7751EF63951EAEC7840E1B2DCB3802E22 + 1F2538ECA0D7A4368AF205F2C6FB971E72F2FB98D9A40502BEA53EDF13C9B38D + 82D9EDBEC92A1BBECD02F75213F755EEB70D0CB6FBF0A20FECFE6962DF5EEEDD + BE576F89E0E5612D1EE872B06BE5B8676FDCBBB7B15B3BF505F8FAD8A1A45284 + BD8C359432F600FAD636015F8D4DEA68DF08839557594B3AD3D6F395E2D2CF44 + FECE3435CB3D9748DFAE685FCFE7C8DE17D677C68103C682A14CDB7A1C894D62 + CCE9E34FC6A525CF7895B2C6A68D18E35ACAD834E655F6316B89F615FC7500CC + F62926A27E23CCD5F89994326374C6D56EB354637A87C656D6589C7692E60E1A + E76B2EA07ACF53879DB942997FF85C84B989C9216FA913FC6D20B0CD771CE096 + 794F9C0749B2D70FC97A1D73199B5769CE25697E25E5AD7B72B46F04BE82BEB2 + 299A57F3C11AFC4AB48BFD992F326FAC01B020B0DB6D1E5ACF5791CA9AF74A9A + E34A71B96724001CE7D2717E2E5B3D37A78DD466ABEBEA7AF60616F495DF005F + 02F077DC0B0B1A081C41F09869F3D258D31820F0D4ECAF888AF0D7FD17959FA3 + CDF721DB7025BF4B1436E91952B38D4296976FA74DC3A997A25D7EA9AC6E00AC + 3D7C0703C0D14FD64B6DFEB5E87B537DCCD7E238A4B1CF60EDA39E9960ED8ACF + 71E438FC876607F0E28394FF1B9FA9D9D83FD7F3D495E37B1FAB6BFC86D421F9 + 478B8FD4FB05E14715AC1DCCF71AFB44B5B5EDA5B6FE48F59CBFADFE4914FDCC + 3DE5ECA053C63F0EF4451100D7BE75F9D251ED676F53ECDF4B4FD27658FA7706 + C0E239B51DE5ED3E3310AEDB650634045F8BF0F3B7908EA763C672AD5EFDDAEA + 7B48FF2B8AEF45F3FF97360D00463A767D3E9798A5D58FB03ECDE75EFAF6D4B0 + 00F03F17FC450D00B6BCBF316D9077806867FF27EA02BEB5AC2D6F7AEB1BFA87 + 8652FD458FB66CE70B6C1795210030FA3D00F0EFA578E11D4C6D7D7BA9ADFF50 + 6A83BDAE0A0CB701E0A7D15037FD5A6A3F5083D5E5E333B0D1C0A71E0075D918 + 2C31C82A03AD2E9581544FA99F14EA3488AB416F2C037715092C0914ABADA70C + E2CA322E7ACDCDF119FC515FCA510C3EB59C4D67201C15C12E60185B67D01D07 + DAD116DB3C8D2218D6F174CCD1B429938CAE4985D56BA2514F44E2E4A456DBC4 + 27A6D1DE0B00D710B886BF6DD2C42F4EF86AC549A14F042B1B29934F4993504D + 4A9126AECDA47586F52F51BDB5E2A45893684D9629A3BA4D037B4B7D04C0FE84 + F7DC81137CE50585BD7EDEB234DAF2C05F606D8CFA15ECC5C1000CD6F2639405 + 7DA326CD339BD59397A322B7031293EF4B935E2911BF010067C70711C0250A78 + 91A9A4EC11FC6CE55C71274C2507BB452C0B8D262F59EB4FE693CA8943EAB622 + CAB239D4C54154CAAA6B6BA32860C15C3997A2542747959C55B2CB4614C3B495 + 5B1D003BF05DBD2D4DC7D186C36D15CEAF6D69C6DA1D2ED911F6286CB4011073 + 0C39ECDCF9461BFAAF1BB847B09781BE44049768119C8538135FDCBADFD357F6 + BD9B367C7924ADFBFCEB347FEF9B0DF00500A3691BB65979B7B5DF93EBCA12D0 + A4CA2B0238C3E1371CF4FAF2855BF25286B300BE669FBDABB35F1EC2792A672A + 290ED6C6C9EA4ED5CE528AD8E7E3D43DF0BE4705ABAD970FBCD700631CBF44F6 + E0F89DF33A4B3D5B9FB73E4C0BE4302EF037CA2380DFC5C96C7AEF637746B347 + B0EA893A1200667968DAB37CA520F0A28F3F4FAF7CF8595EE2D2E4FB079B4DCE + 729CE772A04727BAEA00C438E0177E42F950EAFBFA78EA3F7CC21DF0ECBB085C + 6EA2B08A167D9AA38B1D1E7F79C801302018004C34307D01C6808015406074F4 + 585A75FC4403814929F71D3EE2E535274FA6B5A74E3A04261A1808BCF6CC298F + 08EE3B76CC23D458863446A9910A0C0B76B0ACA9F282209EDAB196737CE0F139 + F6123E9721F1D9B3BEDFE69AB2D7E6FACB97D23A1370A691D53998B1BC839C12 + 1D0CFC8DC08854B0A886489463B4B0EC191EB32CED95B4F916FB939ABD005F96 + 8ECE4B43670970A95E101829CA17F8BB9348D00281B7DFBF6DF5D7FD58B4A75D + 0462BC06DA011149B7DDCB11BFBBBFBDEB294B4F7B34B2A580625F46DA525E07 + D08CA85345A0125DACFD5605962374561F20A1A254050891C3C8CA1EE12076C1 + 4B8143B5030CC67A8E831C5A5A5940923C7D4815354C1FB5A5CE23944D005407 + DE05A40274F7FDFC6DDAFBE337DE5E0059C7D7397DEF65EB23784C94EC1620EF + F7DFB800C0405FE0EFB647F7BD6E37CB4217D8CBE7C7E7B1FFFB47BED72FA097 + 4860C15E00F09E47969237D1479F3BEF3FAF41AF3BFE0FFAFFF53AF95F01B87C + 3E8A20F6F78772F92C1BBBD9D44E797DB6FABCE367BEA588730816F35080002E + 29E273F5CFB2D477003091BE94B39D7AEAD6DFBEEAE2C1068F642FBF017DBF3C + 6F29E568D36F50BF3D3D8CC1C31AD8F83D6027625FBF53FD5633F0B5EB83FDAE + C9B3772FFBF6AEBF76356DBD733B6DBCC1D2CFD71CF822F2D4017F579CB76BC7 + A58B69D3757B0DD7AEDB3524C3DDD597CE3560986B0B2B14007A5710096CD7A3 + D5B4B970DE237E97F3200CD72EBB8EF975CBAE593CC442B9B9C6B980B427BCAD + DA7BBD5D277DB9E772AD0402733D5D73CA8E67E2619DA587EC9A6DA9967D0606 + F7F1E0CED78753FF51BBF69EB0EB6759EE791911BF2CF95FC4B59E952156B03D + 80A5BE67FCA1A32EEA2200268F9D32F716DD63048191DF5780C0942DCF3D2B3E + D4B4F8B30C865F353B0F3C51CFBD8DAD12E6BCF791DF0B698FA8E7BEC8FD3346 + FC6ADF5F848D7BAAEFF16BF761002F5AF4BE1DE35DEB6BA2CC3D7DFEEBD6A6DC + E395462DB0FA58F607BF480B188EF057AB8D4400EC6310C62636EE20F5B28D47 + 3CFA97B109E3983286C11E0130609754631B1FEB00814D8C830477C9C7715294 + 20F04C1BA379DE6CC0DF17CA588EB11B622CD73CE0C75EBF8C256DCC07E8D518 + 105BB40B0263ABC79051D47934AF298E43EBF129E3588D4F5587ED79EBA7316E + EE477BEC9DE59EC90386A7D858398EB535B61600968D31352900D7DB038D19CF + 5702EEEAA152CAF48F751AF7C779866C35F055DE1F62B5B90B22B21788EB91BC + 050233F7899097540058F6A66CF328C15FE65C9A5345090067F1906E0702D722 + 92D7D332FF93343F8CB631B35FB3395F96436041DEAA7D1708AEC0AFD4068023 + F895E29C3A8A39749B341FD79CBCAE8B52BBA858A779BEFB0842BEA3EEFD7E1B + E17B787E8E835F87BFF82A8A4FA297464E7D398DC47711FC1CC3917C2E75B997 + 5DBE19D7A4BC0C343EA3E8DB198EE41F8AE536BB7C52CF4CC467C5796ABF96F4 + 42952F0A11C002B4F29745E167AB6DD1BF365C45BF60545B5BD49CCBFD83D656 + BE49FCA296E2EF9B30E5E53479C68234CEDE17EC234DA3CCEEFE40F62EB67E40 + 613422A8F16DE2232D7ED21A000FEE7BEDA8EE23B5B5EDA5B6FE48F57F0F00AC + 36B17DF49347C02B5FBB6C02C3AA1F4AB5DF3EAA579BE1F41D543D0030FAD5C7 + 6E53E0394309CE03F4AC6D99FF587928BE26F0D956F734D2F174CC587E52D5C7 + 6E91FED75A6D6D1BE9D8F5F95C3049D8A4D53F0900B6BE6DFC34AA6B09E8B606 + FFF0E28D6905BEB56837CEDE940E00A67F07FC46D1BEE50DFD4383AAED0B3FD0 + C617D82E28FFC10030D285B797DAFAA05EF5B1EF5052FB0879BB14013091A97E + 83EB7DB346F1665A4B37F3A1A4F63A4E5B9BAC7CC36DAFEB280E7CBA063F12E7 + AA6DB5C280AD5184BE6DB2363E989BD881BD11F2D672805B5EA3F2D1E6AF83F7 + A394251FEC05F51A48A27610DC01BBDA03380EBC0793FAA108747B496DA27AB5 + AB01B0C02FD2E4430037DAA4BA5D84BE31AF365D75E5E9D9366962D53D61EBD6 + 189BE8A138D9AB2788D1AEB26CB17D9C64C6492762C22AF8ABC9ABF7230DD0B7 + 99F49609B1C4645913665207BA25D2577DC80BFC4A6A87BC1C26EA5171124F14 + 310018A7C0C8175F7309F2026E712A008111001891973342E097B6ECF93B71AE + F5B3BA890BE84F1B4032B018E744DE8F8ABA71F3ED18A609AF583BD3F805D6B6 + 483669D2A2BC17969C2572A8904661A3DD738BD736FB0B93971C0C17A70D691B + 008E36E51BD81BF24D04707018C901151D5192206F1B00EE2C639785238DE889 + 196B719665008CF32B47416C6B141D6CD971D62DD9A3F30E5196E32E3AFF481D + 04B33C60891899B565AFEF01CC5EC02F6CDAE57BFD027BE3F2CF40E01736B32F + DC8EF4E2D67DBE6C340E47F202C002BF82BFD8E4E8F408161C97D60F67298E54 + 77AA9A142923B8AB54CE544161779E5A8AC3B571CC967A521CB2387B17BEF991 + A72C138D5377018EDFD7F3B28E73AC0D11C3F3DF78CF9780F665A0591AB22C03 + 8D8802EE82C09667A968B55194B0203080184735799CD8883CCE6D1CDBAF7DF4 + 795A0CCC2D4E7139C7518C9C02FE2EFEFCABB4E4F343E9B54F0FA6451FE35C3F + 9C561E3D95561F3FED4EFA0C7BEB48E1AF72C4B189328E7C00F0C2CFACFF4122 + 7F8F790A18F63230C0D4E751C0471CF82E032A948860070F56060613110CF845 + 82C00E808FDB318EA1130E2F0430B0911710C6B68CB6D6CF973E2DF097E83787 + BDE7F2B2A82C0DED91BF05A2381036ADBD78C1013029906685A9D96FD3F20E80 + AFE47D3A9147F195E59E6B48447EFD8D4B0E6AB1AD223238C0A575D7732A580C + 90DA7CFBAA8360CADEA6B4158812B842CA0371957A24E3DD1BBE47A9A7776FA5 + 1DF7EFA62DF76E35D0D8C1317D0314F36860401BE0ECCE0D5F061A004C1430D1 + BF5A7A3A023F6FCB392CEFB0D944EA30B0D801925E2EC7469CC7DB525780AB03 + 405F2EFAAE2FEB8C00C40E0F4D448A0205C9B33C33FBD50A0AD39714A828884C + 3D10973A6C82B3A482A0405FC162E5753E015D52E56507F8B24FAF971FE57A9D + 476D48391F807807EFA195B75A5BA06FB3FCF38F8F33007E60C7BF0F447F9076 + 3FB2F6BC97F76F587AD3A1EF7E6B0B0826EF80D7B40B600C405619505B3E83ED + 567600CCEBE0F328FFBB5E17D2FBE079FB7CBC5F119F8B3E3FCAA4D8388F3F20 + 5014FBE8F3D5F781B200AC47EA96E360E77B17A5E8DBAE7ABECF76BCE638E42D + C5A6EFB0FAFBFEDA26B795DF01DFE16DF69D40F415C4E5FB8EF86DFAEFD37E57 + EAA336927EC7D8917EEBFA8DFBEFF6FAD5B4E5F64D07BE2CFB0CF095565DBC68 + BA94565E20CAD7DA5F32BB95579D3BEFD713A02FD7125F6E1E087CE1BCC35F5D + B3D8C377FDA58B7E6D02DE0276752D5B75EE4C8E063E9D1F88E1DAC77EEA8ADE + E59AE8D1BC96AEB06B2AD74907BAFE108D1DCBDA625B65F5AB4F9EF2EBAE96EA + E7FACCB57AB95D9349F3F5F9885DABEDB847BA97FDEF96F515F8E5E19F027F05 + 7E75FF100C260FFCF5085DBB97741E2ECA2B512CA25CEE3DA47EBFB1FC128EF1 + D9C1D4F7E5E1B4D4EE5DDED6EA380EF0F7E5773FF43D7FFDB8A844F9727F94F4 + F0D46B1FDB314DDC4301C0ECEFCBB2CFBEBCF35B1F3A0006FCE62D1AEC586F7F + DC44FD6AF9E6281E0893582D24D6D15EEA02C0E5E1318D417CF9E7325E01F862 + 63EC41BDE02F1220F6718DE535F621A53E8E851CE806A91DA2ACF11211BF0D08 + 06F0DA982D825EF28CDD904705973A1EF463ACD7405EC680653CC8389031601C + 176297347E8C79521FB316885B4B63D1582FF03BC5C6BD284260C6B90E7B576C + F2C85D2410EC2AE367C15A8DB56339DA24D569BC2E29EA57799F239880C29A2B + 44C84B59F308D955271B109807580580998BB44501932AEAB796DB6D4E35A140 + DE087D7B2BCFB934D792047FEB0780DBE4A0F7659B075A3A5C008CC694396B2F + 694E8B348F8DF3E1087CA566EE1CE6D9755D9C9BAB8DA4BEB15D9B34CF57A4EF + C869762C8E67F901C03740DF28F9295C948B4FA2A7F05DB4F83924D9E55F6993 + E0AECAF178EAFBB4F05730977CE3FFE11CA18D7C43B13D8A003847FC7656B7CB + 9A51E5B36A00DC4BD1E7279F5AB4457B9BEAB6B5DAFA64595DA9E77590367DC6 + 4D77F8BB7AF3BEF4E6478752BF5D47C7F3198C999A464F78218DB6F763F4B3F6 + FE58BB67464FCD70D7F252976F13FF67917CAF516D3ED5A8D8EE49FA49DED65E + 632F8D1A33CD158FFD34926FBA977FBA4D6DBEF558EEF8A03B52BBC1148F5BAB + AD4DEC5BD73D91B46AE9DF2102B8663D0D9CACEDA60EEBE92E47DBDF550D482D + AF37967F2BD5E72C1AD6FFDEEB784D79A2092E89ADC3C7061C27AAF41DC04B2B + 39006EABF8475603AC91C3DAE18837F00F00FC5B2B7EC1D16021EF790F600070 + F812F7925DD4FE5E00385E34075C38AB0B6C2FD5C768535BBFA1D4769C5A5D7D + 22F48DFA9500B8ADAD6EFA3CD185548EAA6FD4B16D779FCE4DB763EB486D795A + AE91D95A074031ADEDA80CBCBA14406FABDD5E1765C1DF2E08CC6B9860690580 + 39570372C7653D3396D74CDEFA74D5DBF12C655FE0A87A002B6920D908D00B10 + 9E2CD86B03FB0080DB06DED156D7D5ED623E025F29B66B535C023A4E3EE2A443 + 9397B60989EC284E74948F36A9B1B74DA28A9894E57C671257AB0B003F3FD7D3 + 3839D424D02782C54E3EB68B521FD533E1D4C4549354955DE4CBC4372A4E92DB + 24B84B1A6D71424D3E8A7A5799B047359378451033999FB73CEF515560AE80AF + 603012088ECE88ECA0C87B03639F30C754016041603D85EE0E8AF9BD01B020F0 + 24A27F17F6BBE43891047D25D90481631D3689A8E0E8C8894FF2CBB113CB40DF + 087B0585272F31FBB21C7D101D4C4865A4E80424002CF88B284BCF2DDF989EA3 + 6DE310DB529C64DD511038C814251185ADDB5E9C6FC589E70018C71B69117552 + 03803744089C9D8A381001C053D76F4B33366DF7259FD1CCCD2C036D6D4C2C2D + 08307ED1DACEB2493000D8F7A3DB0ADCED86C0D828E3DC94A31307A71CA53845 + 1525A348199CAAA4C05E815DC15F77B8E27CC5098B9335488E5AA2815F79E343 + 17D0D79DC24408E3C4F5889E77D37C7714E73D015D6FBC97E69AE6581EC8DB44 + FABEFD615AF8EEC7695181C0C062AF333B51C05AA6D265F95896831B47F6E28F + BE488BE5D826BAC96C1E0555E0AF228071A6BB83FD932F1DFEBEFA31CEF5AF3D + 826BE59193BE74A747647D9E23B3967C89E3FE707AED7396EACCCEFCC6B96F22 + 0218E84B4418A903609CFD262D15BAF4204B447F9D961CB47696127906FC6519 + E8B5A7CF78DA77F4485A75E278EAB774C5B10C81FB8F1FF3A834A085C0EFD2A3 + 47D3D223D6BF2C5B0A0871F0617D80BF8060608A2F7D0ADCBD70DEA1EFCAB29C + 2AF6584F4ABD4317DA15F04B041EF2A83D45095FBD943698D65CB67617AD5F01 + 4AAB2E9DEDE42FDA39814857739421117FD8236042B201990464057F250160B5 + 918057EA136D9B1D249B6E007401B2B71C966DBC4554B19D9BF3595BE01A504D + 705810190009FCDDF9C8FADE07C4B1E46E86B70EEAE8E7A02DDB22F0E378F918 + B73C829494F682785E2EC77130088C2C5092E58601AC79E9E1B2543370D8DA0A + F6D2D6CB9CAB4057D28D25AA94BC9625DE44D9FA71ACDDC0D8724C6028705650 + 941498EB7D3987E0A8A55E57FA6103A622954981CF44A592174C567431609BE5 + 93773C7E68C7E798F7D207FFF6D7F4E17FF7B7B4E7BB6FEC7DB0BA07F7EC7DB9 + 9B7692DE03C0B2B7F30D87AEBB4B94AF002CE93E96932ED0D7E1ACE5793FFDB3 + D16B34FB163E97F0FFE8FF23EFAF9DF6E5B36800B2C9E1ABBDBF7A9040AF83D4 + BF03265E8B4499CFB7F93EF0D994EF127B5AAB0F760157815EBEB3EA47EADF7F + 137594BD6DE943B9F9BED3D744DB75E52109D56DB8C1EF8063747E23FA6DD04E + BF2F5EFBFB7FFDC5E13ABFE3F83087FF762D553FF21100AFBB62BFCB6B76BEEB + 573B90F7CA1507C144FD2E3F63D78E0B769C2B57EDBA61B6B3E753DFE9B369C5 + 19969CBFE0C077D5C5FC5009D7141E3A5963FD58B21EADB3FCBAB2443DD7AB7E + 40AF5DAFFA00B85CEF4E9F6E60AFAE890DE03D7EC2AF8D00DD7E1EB221C217A8 + CBF5D5DA70CD74D9B577895D93FBAC4E4BF523A0F08673E7D37A5EF391FC408F + EFF52BC84BBE80605FF2DFEAFD5A6FF703DD1396963CF709815DF2D40177F560 + 9104E0E51E051416045674B0EE538B3FB57B8CDFD73E4B4BACBCF48BAF73DF02 + 92E7D97D8EBD7EE77F68F7C38F3ECDAB61D8FD4F1038AA89022EF5DA7E6136FB + FCDBFD9AE59F5FFDC08ECB92CFC0E0773E7179D42F51BE07BAA1AEEEFD8C0B24 + 815E497645FECAC698426390B8C208F207D14C8C59349E11002695A853CA1828 + C25F015F8D8D64577BD958F6594B3FCFB0719A47EE16F02B082CF0EB63B9CDA5 + 2E44FDCEB4FC8B80E0027D3516EC057B9B716491966E9E6E7945F60AEE228D41 + A35DD0B74DB91FF90C7F35F61508769B8D91192F6BECAD32E3E906E2F20067CB + 98BC6D8CAE3ECDB2CE657E50835DB755F50E7A59A259ED6C8E31C1E61CBE2C74 + 98AB34F30FCB0BF052471AB7C171F0CBCA47253F81D59134CF32699EA5F9572D + 2280C7CF63CED60D819F04000BEEC625A06B001C259BA7655E8A3437ADE7A7B2 + 31F7ED05803567D63C3BCEAF073C585DA99EAFABACF9BAA4659EA31ADFC1D4EC + 2B18331DDF01F6BCFA58E36FA8E0AF00B0FB2B9EB376944BDB9EE278C5B7D176 + 3CC4F1E42FF935EAF2D558DAA62E3F4F65C737247B07F4760215BAED1D3D0D00 + 26180250DAE517AB34C0BF87E4E30BFEBF0EB8EDA8AB4F54E8270DE8177D88FC + FFE5B5E4E59FA7A649D3E6A6F73F3F9E1EFCF8B7F4DE17C7D374FB3D347ED371 + F978791F60FC871D353ECD287C9DA6DA5F8BA22FB54DB15DDD6FB802F0B6C15F + 24004CBB78FCBF8FECBD1CC4772E5BF445C7B6516A1FFBB729B68F8AC76F1843 + E10CBDFAFCBB2A309FE12AF29F5A83D5FDE66A40AAA92EFF96AACF5BABAD4D7D + 0CA9AB7EA2092E49D9BE230D232B6D8A06BCA7D6B766A7B59E6933FEB3AA0370 + DB042C1E17D401DF5E3F0018EB43208DE5FCC636F93FE4CA807728003CFC1F7D + 731C53EB05E969152EB4922EB8AD17DE96B6B1FC9B48AF29AA6AC379EB1B52DD + C615A16F54BD04B41F4337C681D2CDBCADAE97D4A74DC369335C318041030640 + 76FC2EC53AFE7F97E5CB206D48C5B6E4D1047B0D15048E10374AF691E36D303B + C106BC26F2593620350D6CDB81C0F5E0B59606971A68B24C8C0F2E19049741F8 + 88492883E03CB0EE86B9E463B9565B9F1AFED6FD29B7D5F924C26C9A9C285F4B + 938FC16CB1DC80DE3291924D93214D9CE293B85D13AA32811B09DC2DEA02C23E + A1CB4FF5A23851D48410453B799535298D1353D5C5C9AB26A7A44D247099ECD6 + 935ED9A2E20419316916FC555EE5D896547528B6AD417074028C7E719103DA65 + BBDE491BDF3F98A62E5DEF91C0722A08FA4A1D6743CE3BF48D32BB227E057C91 + 003076D9DA00B0C0F04407BF2B5C7296C89922B88B54472A478B6CB2D36EF2A2 + 028771D404D02BE74D037F8B5D00D8C16FE52C9AFC9AD9977422833B8E25A20E + 3A910719FE02784B746F715A49CF5B792A0018F8BB2C03E0C938B35C9B3D9503 + 0CC7580D7B057C01C31D51EE0060A2883B60B81BFECA111801F0CC8D7B5C1E2D + 12403002F20AF43AECDDBACFD399EE5C34D186E5A339C666EC7BD374B3E708DF + 0C815186C2ED512E8D43144727756603E22239585F2A6037825E87C0C58E70C8 + 36CE5DCBAB4E65F605C6A98B73178730CEE257DEFAA82C1509007E2FCD0300BF + FEAEA71E111C00B0A280C92B325880B881C4EF7DE2020213B58480BF443479E4 + 6F89689216E0D8FEC0F4E1A70300708C0E7608FCC9571E0D8C167F46D45616C0 + 57D0D7A1F01738F70FBB8D8860AF070417E7BF4361EB473BF20203AEAF0EA6A5 + 00E022227FB75DBEE222221828412A18E191C126DF8BB2800A96845E72E448D7 + BE95EC6F29084C1900DC77CAF2A73BCB413B482192CEDAF9B2D1D68E94255581 + 2E4406038A233476386C5A792147EDADBD0CFCB990D65FBDE8E9AA0B67CC4634 + DF85B4C69775CD91822B8B1D61A77EC3B54B69FDB58B690310AAF497004B8258 + 025902BFB2C9AEBAD82EB705E202CEAEA44D37D997F46ADA78E35A23C1306016 + 79013CE01A76005D067FD73D0275EB5D209E7D36564611F202F01C14DECBD19F + 8AC894680758A44EFD90DA93177074106C6D2318A65EC7527BEA91834193DA53 + 17DBA80E712C96A86679658F6E359BA0A9CEAB3CFDFD753CB8E53019580A4C05 + E40AA25206F0529600ABBE2FADD553471BA03175442F6F7B783FED7AFC386D02 + EE3EBC9BDEFBAF7F49EFFDF5CF76CEBB76CE5B691791BFF7ED3558FDB6BBD6E6 + 4E86AA391218689E612CDAF5E04E3AF0CD83B4C7FAEEB847847781B7BCEEF2FA + FDFFE2DCF73A909CFF4579EAE2FFCBFBE1FF37E7B014BBDE4BB589EF916C12E5 + F8B9F3FE0BD0EA73919D070DF4C081F2EA478A0DA04B4A596DD5AE0DF892C6EF + B61EAE204FBD0033F5D8D58FF7ED23FB2C80DBFDE74FF9EF56BF251D83B6C81F + EA280F7860E7810E96876719E7FE73E71CFAAE2BFBFE92EF3B4BBDFD3659F6D9 + AE6D00E0FE33D6CED255E72F5ACA1EBF6752DF39BB0EF190895D7BD69CB7EB80 + D5AF3E87EC1C96AEB2762BCF9CB56BD819BB5E9DB6EB1D00D7AE8FECC5CB9EBC + C74FFABEBB6E3F76C2F7E8A58E322B262C3D72CC95574F38E569DF11BB861601 + 70057289E25D75E274DA76E57ADA7FF741DA75E3B697A9933CDAF790B5F7077A + 0EE7EB7BB9CE737F10F08D7907B8263D4CC46A1148F706DA017A33BCFD34C3E2 + 0285177EF2B9DD97F23D8D7B1CB01650EB65B3BF62ED9B079C283B0CFED4EDB4 + 71B85B01E0E6A129BBD7728FD51EFC68F147F6BA4C44FCCE7D2BEFDF3BAF2CE7 + BC8007BD0EBCEFAB7D08FA3AC46D83BF65AC20D81BA52D23C8D34FE3108D2534 + D62065B512EDEDCBB82582DF38C689A20DD2B847F606F4DAD849107800F82D02 + EE3AFCDD64E3AA326E1304263F8D659F79B0CFC67702BC1AEFCD200AB8D8A506 + EE5A5A835FE5B577AFEFFD1B24C82B08ACB1A8C6A54875AA27ED4060C6BD1DE0 + 8B58C219918F001869051D8DAD3B63F1CE383BDA19BFC77AD95C614E50CF0B24 + 20B0DAD2665C84C0612E12E727312FD8ABB65DF017E01B24F0AB9596E2DCAB11 + 7330D344967F668E55415F015DCDFBE2DCCF6DA6AEED83CAC3C64401FBD640CC + 1D5D1DD88B0484A566AE49DF027B6BD573D62E05A0ABBCE6DB51E398575BAAB6 + 48F370A56DA2AE06BE9AF793CA57D0ADEC5B18B0D29820EDD4974DB3D3986939 + 1DF5FC4B5966575BA9EB1801FE4ACD31F1755475F2930C2641DE5AB10D0FF0E7 + E0828ECF28FA80DAFC3D6A275B07F0E2A3421DBFD2C03682BC11F4465BB4CF48 + A39E7DA1B3225EF18DD5FE32D4E52FB3B2F228D6A91C6D2A3FA9725F13FEC7F1 + 537D19683F3775C56F39D6DED71536F77DFB93231E093C099F92D58DE418F243 + 96B4D933D834629CD94B3EFA3DA3AF55BED75AD1A7DA568FA813306EEC96E71C + B17F5403790701C0F11CBFB5EAA0A34EBE7BE96795958FB6BA5DABAF7D10C5E3 + 75F51F3325F9D2DEE5F8CF5087BFBF04BDC5630CA55EAFEBB70B9C0BC7AF194D + 6139E23A35DFE9C57A62BB5E6D7E73BEE610B5A82E0FA6FA388369A87E23EDFF + 1D31298DA8EB4B1FAF338DA49D83DE702C4F275A4ABE7C57460CE3BDB6E37533 + D0164580FACFAED67FD045D46F84BF1D08DCB4E982BFBDC407113E983FD425FD + B8A5367B6C3F98BAFA0CAA27BB68FE434AD037AA6AC3C57E5837A400791BE849 + DE6E3C5D36BFF1D0C7D2E606C94D2900D672431D4AF118F56000A95D5BDD70A5 + 4188A441C880C14F2DD5A95D84BAB504792DAF415F9BDAE06F1483B28E6CE039 + 0000E77C2E53DFE9433E460037E70C8358097B7CAD6E07F4B25C4C1948E74175 + 86C2190667DB70148F81E2805E62D03F583D52BD4F24685F14271852B4F76ADB + AB4D942646BEB74E00BFED0098495686B892267911D62A1AB89E147A9F29760C + CB33D1A45E7DBD5F9884AA5EF6087EEBBA7140E05979F96680EFC81976DEA236 + 085C4F90E3A49949B426D9D11EEB9A097C99748F97AD4CDC7100F8DEBF8058CB + 8F9AB5D061ECE2ED6FA50DEF7F95A62C59979EE1787388DCED5E06BA7640B8ED + E56E0181E5801000A6AC27C9C937D1C125DA370260D92616004C2A074974A644 + A74A5D279BEC40E1E75E5B9BA66829E8E2F0F1A7FE83B34711C148CE1F01E02E + 4791490EA5262AB894A3884A00003FB71C0D74504567D6646B2BF83B99B20908 + 2CA7187017902BA01BF3B5834D6501E04EBBED8D630F479F3B0083F30FA72011 + 2202B81D08BC2B4DDFB0D3A5BC40B0C0B0529C8DD10139CDEC1D080CF025DAA5 + 1BFE22F25DCE4EEA76E4C85F396915F13B7BF79B03806F1B04A69FA274D4576D + 7006B327300E611CC62C19E9CB46121D1CA28081BF02C01EE95B2281714613ED + 4BD42F79016096829E0338B654CE6AF60BD6BE8538B381BF4B80B1C5492EE737 + CEEE05EF7F9C2170114B41037F05801D0A7FF29543E0573EFC3C2DF8C0FA0292 + 7DB9E72FBB22B35E2D8E7B77E417873B4EFE255F7DED7B00E3CCCFF51D185C03 + 6096175D4624B0E5D79C3C95765EBB9EB65FB9EAE097BAD52C055DA0EF6A4BB1 + F71DC97B527A549B6979880006E8028001BA12916E2C75AAE5537D5FCC62CF00 + 18600CFC3D97D69E3BE77B01037B11A0788D0399BC4CF42A22862D2F004CD45E + 5686BBC05CE02E2017C0BBA600A20100983D8301BE960A006F6029694B575E3A + D3441C4601AA22E4A58D20976C12ED81B919DC9A6EDD489B6E5EF74845B4F916 + 1195562E6D33ECBDE9F00C0900D31F1145C9FEC45BEE50D781B1C03804B40300 + CA462AF02AF027704759F5314F7F52C13ED2D85EF08F360E394B5B2F3FC8CB0F + ABBD8E25E04819B8C97EB61E11CBFF56FAA92FB658262582D8F799B5D4A3694D + 4053549715451C236D01AF82AF9B790D8F89687E9436DEB99336DEBD6DF9FB69 + 277BFFDEE5F512C59B237F3300B6D742DEFA6DB7FEDBEE11F99AA361B7DBEBDB + FDE06EDA67ED7713397CCFDADCCD6058D2FF2EE01B237E1BF84B5433FF67F9DF + 15414C9934BE977AAF0580F5B9E8BDD3E715C5E725D5762DD74C9E9432791D37 + F6577DEC03FCE5F795BFFF393A3E46FA92C7A6DF09BF9D183D2C3BA20F76F2C0 + 5DFDA6282BDA97328A0078CD55CB5FB4FC651EE6B856967C36BBA51EF17BA92C + 01EDE925174B40AFBD7425AD3C77C12381FBECFAD367D72C20F00ABBE6F4FBF2 + CF76AD011CB34CF419967E26F237DB3D7FE25403741DFC0281CBBEBCB20BFC0A + FE2EFE9A0765ECBA696D579EB2E3587BC02FD078E5F15369C5B1930E7659C69F + 485EEC1BCF5F4A3BAFDFF294485FAEEBB4F1485F13CB38EBDA2ED84BBE01BE41 + CD4A130500FB9EF3762EDF36C06C8BECFEC27D847B0CF097540098FE1E0DCC4A + 16760FD37D8DFB9DC0AEDF2FDFFDC8EF639C83547BFE4ADC0381C67A608ABC97 + ED3EAB07AB48977CFC557AFBF6A3F4C183EFD3CA2F8FA517F7BDEDE27E0EB0CD + 5B3A94085DBBD7FBFDBE2CF1ECCB3E5B9DA4F1451BFC65BF5F443E8E4334BED0 + B8823185562801020B000B026B8CA3318F40AF96865684AFE06F037723FCB571 + D4B422003036AF676C551EB8F331978DC1A6B30F7000C07AD04F2057E33DD208 + 794965535BD54BB237F52B33C4AD616E1EBB6EF1312D79C6A58C49353E555BD5 + E732E3DF0C7B8702C0797CDD89008E5037E6511CBBC7F17B2CFB189E72990344 + 311F70595E6D0480D9EFD721B0CD371CDC567311897A45FC46BBEAB053AF36E3 + 4901BC25FA57732FCDC7C6DA5C6BBC6962D1B8399D79972FDD6C12F48DE07730 + 78CB7C71DC8BAF59FF57D328AB1BADFD7DD5B61C4373493F6E99734A9A7B4631 + 47ADE7B31276E6CB719EAEF973548ECA5DD00580D5A76D9E5EB7D15CBF560D7E + F111908E7C3E83DB115366BB1A5F43F14BD400588A0058E0371E27FA377A29D6 + 47BFC9606A03C051234CD9C7D3DBCF23C9DF93DB77CAB53FA9976F29D7BD909E + 9938C315E16F7744B0EA66780470ED1F93E41F8BC20E2C6EAB7B1275F90F7BA8 + 6957FC8B5E46948180C0C0F133D278FBDF9F9BB1204DB0EF07FE4DE02FCB3F7B + FBD2D6A131C7923FB308DFA160B2972D1D4A8D2FB5A54E8A3ED7A6ADA59CA7AE + 930603C0527D9E5E8ABE65243BE7511AF3AA8F6D3B7DBB0130FEF26893BD2EAB + ED93A8573FD9757C01E01123099AEB6EFBB472F8DBC2109E5CE1F5B7301A0797 + A333A351C09EEC03DA16354CA7A5AED16FCDD7389E549707537D9CC13454BF58 + 1FDB943260B881BFB1BDF2F0C71E00B855EADFC5415BD40652FF59D5FA0FBA32 + 006E83C0CDB2D003606F0FF9F1C8B7BCE9FFC9A51F77FD236FB30DA5AE3E83EA + B7B8D0FD3B8B8B759B421BDD38DA6E2A5DAA017084BE12368070B931766E90DC + 94BA6FE6BA890EA6D81FD5C7F8BD3428048E65F265F0E56200D5A650AF015E2D + 87BC2DF0970158CC67D9E0B301C082C036D89D6003DD008063DF5ACD797B0C68 + E36BF5FA0080E340BB1E7CF7523D106F536C13DB2AADD5B4B18989261C315FCB + 271C96AA4DDD4F8AE5BA9EC94FF364EC1000384FB2E63AC0AD27787192E893BB + 3231ACDB091AC763A89FFA4871A219279C4A95F748E032011E2CE237AA6EA376 + 1C47936CB58D936CEC7A8ABBCDA649BB00306526F763CC86B362EAF28D69DC2B + FD6914F639565F9C0E381B703CD44E881AFC2A0218078480AFA27D7B01E03601 + 80A3A2A3448E1514A37D639DA28465EB0580072C011DEC2E9C4272284555CEA5 + 5A7228E194F2A59D5D79893A89FE38AF247766E1E00ACBDFE1108B4E33D29C57 + 14702EAB5E0038C3DF6DA58FB4DDA3356A00ECFBC01508DC15051C00307017E8 + FB02D017E7E3FA1DBEE7AF60B0F2191667F82B87244ECB08805FDCFE669AB5ED + 8D8EF3B23845A98FED70A00A00BB73764F8EBC917355CED61A02374E57522BBB + 03B8386D25D9171C6099E70FD2FC373FF0E85FF2D959FC8E47FECE39607D4D80 + 60ED09DC80E1007BC923F2E865FA5A4A843010D8238571669B7072BFF6D1171E + FD2BE82B27B93BC17194BFFF719AFBDE47694ED92351CB6B221CE744FD028105 + 8017D9F1BC6CC79CFFE167690160D99DF540E0EC9CCF00D8CEFB79DEDB9188DE + D7BEFCAA810111FE7A7D69C33E93805E003051BEEB4E9F71E88B1DE8BB16307B + F294835F4503F715B16C34220FFC5D7E9C68B8BC0C2A4098A55029374BA3B2DF + A5C921B0DB592EDACE77E69C47DBAD3D77C164E9F9F3694D89F8CD4B44E765A1 + 891E061E7B54B02F037DA12C0D9DB5FA22A0372F0BED50F76A8EE65D43547089 + 160614AFF20862FA87655F01480E672F7B1F0014F009E024E84B197B946C6A4B + AA7C8660573D0278CBED5B658F5201E01B69CB9D1C09495B41338E2550960548 + E638C0E78B0E208900F6E35ABD205D0485E40514B1A91DE08E54704FEDA254D7 + D49B809940D04DD69FD7069856A42B7D38878024AF85BED8751CF2D4C7D7C3FF + D62C756C36414C1D8B94FAAD9612D10BC4059A46C82BB0DA0D793368D552D1F4 + 5504F1D68777D2F6470FD296FBF7D3A6BB77D3E67B772CCF6BB89B763F7E9876 + 7FFBD0FEB7DB0EEA01BF44021305EC2098D765AF71DBDD9BFEFF6FBB63EFC1ED + 1B69F7BDDB693720D9F2D83AE03EBF979B2D65896C5E5F7E0D1D00EC40DCC4FF + 8EF8BF81BB7AEFF49EE833E5BDD57B89CDDF473E23B391F2F9EA33D67BECB016 + 1BDF91624394F92C9BEFA889BC4499CFDEDB9A32F0BD9CD6DAEF63837D9F73F9 + 625ACD6F8A0729CA77DE75CDBEFB1E01DF39D746EBA3DF0751F7FE3BE1E18BD2 + 76ADFDE6FACEDA35E1FCA9E6F727F96F181BE7B2DF32F0D7A37E2F9FF3FCCA8B + 67FD37C5EF6BC375A27C2F597D1611BFC0E015E72FA465805BBBD6ACBB6CAFE5 + CA3507C0CBECDAB61CB0CB832717ECFFB97831F50384CD2E084C4A79397BF872 + 6D3C91237B057723005E6AD748602FA01779347081C202C0F4E93F69E73C9601 + B020B000AFF66CF728DF43ACBE60ED0FDB710E5AF920D7DDB2F72F91C0761DD7 + 3D00E9FACE353F3F2494ED7E0F300173250030EDD9366081DD8314F52B3900B6 + FB8ADA93F7FB5789FAD50A173CE884B0CFB57B2051C00E7F398EDDDF88EEE51E + C9BD10E8CBFEBEB3DF78D71F981200F6FBE7DB44F9BE9F5EDAFF767AEDC32FD2 + 7BF7BE4D9F3DFE39ADF9EA78037901C000DB1777BFE91097B224E0ABBD7F256C + 3E4628A0B706C01C8B54E30C8D21C8330ED1C36A7AC08C94720D8035DE89A097 + 54F5712CA407E7D446766CD4B9182F31CEB2F196C65C11003763B812FD3B7DED + CE06DA6ABC57035D8D0BA9A3DC1609ACE85FF5C930373FA428B01BE1AE1E68D4 + 18566D62BD207004C01A0B537E76B98DB90BF0D578398EA54935EE26D503991A + D3FBCA3E658CAF54798FEAB5B406C082BE805E2DF70CF08DF6B10B5738006E1E + 240D730ECD41240160449E7A49F582C09A6BA136003CCEE65CE35E06FA66F83B + 81B2D999CB69AEC532CE11D8464500EC60B7CC2B0580C700806D6E38F2459BB7 + 9A0481DB8E1501709C8F46C579ADE6AE63AAE59C35CF56AA793502FA0A0023B5 + 43719E1EFB20D925CDEBA3047D057E73CA2A621D788BE46B687C12A840E0A701 + C0C395FC2283A981BCA11CED5E36655F0E9A55D2817E2054FB7E94AFFD50D11F + D57D0CDA44000CECED25EABBFD63B5A2BFAC56E3F7ABFC7F83F90025B51B4A4D + DB727C87B8557F3FA6C0E838B38D9D9646D96B0752377D2DF555FE7C2B393B4E + 6947AABC245FECAFD5009FEB10F580DF31A37F7F003C94EA7EB96F06AFD1872E + 9B14CBE4A52E1FFB30D4F443C59F1F8FD7D4AB3D51BBE535C6E30CA501ED3917 + 30F9778A00AE390E79C1DF2E002C7859DAC47C2CB7AAEAFFABA5E3E998B13C98 + EAE3B4E949FAF5AAAFED2ACBD6D4D9FBF69F19006BEFDF5A4D7DF8C7BA6DD6AE + 81BD7F00E0DF4BFA71B7FDC8DB6CBDA4B65C509A3EA4ADB21F43D745EBB7D593 + 5E909F5A82BE52A8AB6F166D521BBF714508DCA24EBB72A32C91C0F106AA1B7A + AFFA28B5ED65478A2AAE23795557DBB2F28D38DF8C071EB379E2AD4DAAA31D83 + A330D86AA4015FADD0A61EEC71AC2E406B650DBEE2202CDB6DF05900306D3304 + B6016E1191C0D4D39EA7FDE271A3F41A06BC9652D640D797BFF12724EDB86170 + 4D04F03356FFCCC46CD340BB1E7CF72AD7527DAD41DB8609499C6CB4A9AE8F13 + 99B6898D6CB5048135A16A9B5849D835898BD2E4CEEBCAC491895F9C18463B91 + C04813C4060C87B69A60AA1FA9A476B23B04D6A4B7456D93651401308AF59A48 + C77E11F6CAD6C0DF32798F00584F85B3D4D7E8394BD3086B37CAECE4D913983D + A8D80778F46C6B5700700D7E2300F63C4E088E67E743110053060E03811BCD5F + 3640350C7EF6D5EEA59FE550898E1395A5D81EB104B4DB70ECE0E0599221AFCA + EE085A92459D60AF403091C0D214CA4B4B7FD3442B23399526915F667DADAD60 + 7147442A9896DBB100C466D33EC31ED91022227094B1B7DA0BEB76BAE32C435E + 9C6C9DA5F5B06BFF35CAB2BDB00E1BE54EBB1A00E3E403F6CEDAB4B77106A2E9 + EB32CCC589982349763BFC6589E717B7ECF5FC0CA2808B00C0CFAFB5BE764E39 + 225FDCBADF451EA7E5F44D44B0EC77F88BE4EC6C933B438B047971A4E208C5D9 + 3A73FBEB0D0896D395FD7F9B3D808BE3957E44E920396F91FAE0F85DF0FA074D + 24B03B9169BBCFDAED7F3BCDDEF75603805F79FBC30C7EDF28D1C105F80AF60A + 02E3A026AD9786D692D05ABED2C1708978EA02C045F32300FE8825333F4FAF7C + 9CF70806FC2A0A786181BFAF7E7AD0C12F62DF462D074D6430E0B759FED9EA9A + 25A20300160416FCC5F1BFF88BAFD232F69D3C7CB88904A6CF6B5F7CE97945FF + 027D81C111003BFCB53E4B0F1EF4A5A2B12D3B72242D3D7A242D3EFC75B32434 + E09728604160447EE529A2834FBB80C0C05F40F0AAD367D31ACBA355ECDD6952 + 141E10A6DFDAAE0216B3A7E7C54B69A5D5F55B9F7E969D3E7F26AD30B11CECBA + 2B0020A27F2F38DC8DA097655E575C3897881A949DF6A4EBAF5E4EEBD853B840 + 28A095003036A013F608BC1C6A1598461AC19A6CEBAF59FEC675D33597228237 + 5CCF001850463BCE4139823801E075D7CE5B9B0C81BDDEEA148D1941A1202010 + 9294E3090E3A942C6DE8473E0246DA732C00ACDB398ED579442B6D743EFA717C + CBEB3848B0B0494BDB18198B8D94BD7305193996F23AB6B77F909780160406A2 + 4A94B1BBAC0DED88165E4F943490F551DE6BD7F500504CFE8EC3DF4D7781BFF7 + D28E6F1EA65DDF3C70EDF9F691FDDF77FCB3610F60003079B4E3DE9DB4F3FE6D + D7D6DBF67FDAE7B6E5C695B4F3F6CDB4F3CECDB4FD96BD66B3038181C582DB7A + 1F785DBC5EC16B5ED3B67BB71C28F37EEBF3E37DF7F7D2FA019CFD7D31711C94 + DFE70C769BCFB0BC677C36FE59DBF748EF29EFB56CE4F5BE521FDBEABDC7A6EF + 3D65D9D6D9FF8B00C0EB59C21CBBFDFF6BECB7B296072EAED9EBB97ED57E3B44 + DE5A7B4B65D3777EDD55406F06BF1EA16FE9C6EBF65B31515E7D910733ECF70F + DC2D79DA28725F7645F1AB0F11C0F95C39021869FFDF95E7591A9E659ED9F7F7 + 8C03E05517ACCDC5BC1434F01708CC3524EF076C6DCBB506D84BB42FD71E4419 + C00BD005F6D602EC2E29A0B759A299259DCD564701535E094806FC96659F81BE + C05DC160DFCF9D0782ECDACD3EF0BEE7EFA17C4CEAE9071406ECFA75DF52AEEB + D848057F15ED8BBC2DD1B93C6C449DD95EFDDCEE2156D6EA1231025811C3005D + EE532CE7FC0AF7B9027EB9BF2DB27B1BFBF7CE07F0DA7D8D952DFC1896F2A0D3 + BC720FA4AD00F0CB6FDA3DB900608F06B6FBA7EEB1DC7359EE79F9675FA7555F + 1C4DAFBD67C7F207B9AC2F10F8F5F73D05E0FA8A1E766F77186CF7F608803536 + 00FE32569865E302816301658E81CDED3676A01D6307815EC6218C4910658D2D + 80BA8C573416A92380499B87E218C798D4075B7C184E760032F6A9360E7B9E31 + 979676E6A13B1EE42B633601E01980E1F2801F298AE33CC6804860378FFD3AD1 + C28C159F03E65ADDF3A50F1200F6B1A4298F473B0098541038C2E0989718EB76 + E06F06C0405F01651E8A14E0F571B88D9335961604F6B1BCEAA9B3B481B945CD + 181F781BC7FC05E4320FA0BDE02E6A83C0635FE9F734DA0480B131C7909893C4 + 79097945F58E07EA12E55BAB82BFD2589B7F31078B00B8997B91CE5E9AC6FA5C + 2DCFB36A00AC685DCDF990802ECB40FB1CB100E0092F599DA51C470098B6E359 + 26BAF4F307878334DFD4DC7528F99C36805ACD9DA34D02FED600187B3DD78E73 + EE5E7548F37ACDE9BBFC03668B0F97C707CEC7B01219BE8812999B7D131DF04B + 24300200B7F9276A003C98FF6328BF48EC3B989E99342B0B7F8CFB93F0E5E0D3 + 69F701B11A9C241B3E20FA3E33111F54967C531D1FD54C6BDBF157D5FBFED6E5 + 28EA5CC50726457F1992CF2CFACDBAEC2DBEBF5AB15F57DFE18863CB373836FB + 0C01C123CB72D023ED78A3C6777C7EBE059C89BE7E2EE43E453B56F031AABD6C + AE311D7FE9AF51F4B5BABF35D8EB368D7FB70060347AD4D42EB58160A463B529 + 43C88E641FEC356575DA76FA66B9CFBCF8D989BA955D75A41DBF73E8F304523F + A0AEA0DDC8725CF7BB73CC165FFF936AC0EBE378BF2300AE255623B9BD818F9D + 0861D9BBCABDA4FE6D754F231D4FC78CE5C1541FE7F754AFF3C77C04C028F66F + 157D3B4CB45502A5FF0C6A83BFA8AD6DB7AC5D037BBB21F01F00F8B7937EDCC3 + FA910FA2CE31CA85ACD745F2292E9E032E984817E2EA786A3BA0FDDF5943BD86 + F83AA5AE1BD8D872E32976526E40399FA59B64BE510EB4D575A8BE09D7527D33 + 3860C0526C432BBF7EA9FBFFEAB4E33C5DD0372ADAE3604CD2A02FAAAD1DA2CE + CEAD63FB002EA8396757BD0D441B08CC400D108C6C001D00707D2C0DEA246C03 + 5E8BA50C0C35E06DF63F0983E56670EC3606DF71903D500CC83B83F2AC38908F + F9BABE1EF40FA833F9A4C2EC7AA2B4C917BB2627B20D67D2429D2654B1ADF279 + A2D50D7E9B8954985809006B22D70681E30490BC268B71D2A825A4474E2D40B8 + 00E098A79D8E55DBA238A64F6459267A667B746F9426D19A50D7F5CD72D32A87 + 4935ED1BE05B9EC616146639309FA86BD25F9C01947114007A47E23C98B3348D + 653FA9E268C0EE8E8739B9ECCE87D94BB3E38127CF83E49010EC2515E8252F51 + D652659E9F97D50683272EECF7BD7BE55001F646451B79DAD15E7DE47899F44A + 790A1F47CDE2028017AF4D93880A7E6D6D9ABC789D97891076510F0C56BE3898 + 5CD1BE64BD6BD2121C51D9E91435D96CCF2D5B9BA62C47D9811505247650DC87 + D36B4B9A626229E8292B37A7E96B77A499EB763A0006F202727190E128A36DDB + 7EC0B29166875C96E070233B9E00F08B9BF7B973109B1C81381101BAD9999841 + AF2030A96CBEEFEFC6DD698A9D139107FACEDAC2FEC076DC4D7BDC193975C39E + 346DE3DEF4C2960369E6D6ECE4448A8AC1292A4728A2AC2861F2D191EA6DA3B3 + 15599E7A2DA9E830B8484E5BA582C7387E3DFAA744FCE0F09D0F107E1D87B1B5 + DDF386C36007C06F65004C64B0F6059E6B8A1018018081BD718FE06C231AF833 + 07BF0E858B735BCB63CA011EE5914F2C91F921CB3C67073B40185106FE028301 + C18B3EFEC297D30410B3F4335060D1A7396A1870AC25A1657307FFE7D9C98F00 + C482C09200C0E22F0FA665873200F63E26F240DF0C790F39F8CD30F858EA3B7C + D49581F1C1B4D4FA2E076A1C3E92969A169B9D5411C0DA0F98729F1D63F9B10C + 8281201E0D778265A0CF7A4A9925A1BD7C32EFAD49141E51C02B4E0363CEA435 + ECF179F1A24360E030C066C5997369256DCE9ECA7B785E02FC007B7294EF2A8F + F83D9FD65E22CA8F88C24B69C3B5AB09F0BBC66C82C35EBE6CEDCB32B4003281 + 3545220AFE5287625ED01609A061F37E578976B4BA9BD71B004C74B080A8609B + 8E138FB7FE3AD190E70A04EED8958FE7E2189D7E1DA0874DA08F7EA4B401E601 + 13E312C0C048C023B675372E35F0711311B5A58F834DEA79CDBC068E4D1BAFCB + F57A1D9CCBE1A389D70020DDF910D89ADB34AF3FF4A1DD967B37D246CE695294 + 2F11B52C9FFCD6BFFC943EF8D77F49FB7E7CECF68DD67E1DCB0EB3647481ADDB + EC1CDB2C050EAFB5E36EE4FC40E03B66336DBD7FCFFE4FA2926FF8F2CF3B1F3D + 7008BCC3EC44FEF239012FD90B78E7FDBB6987B5DD6CFFDBA61BF61A4D5B8916 + BE75D3977F0600F39A05803D82D9CEC7FFE4B05691C8006CB36FE275F27F5B1F + BD6F9E5A7BFDFFD42B258A368BEF4616AFA579EFCCAEEF266501608FB63591E7 + 7BE691DB764CBE03B4D5F9D4367E5F68C3773783DB0C7781ADF9E1850C7C23EC + 25A50D0F57F0F0853F64C1EF4A6D5CF63DE3F8D7EDD897F88D9E71900BE815D4 + 5D79FEB49769A325DD293BF02D7B7A03871D0A03873DEAF77CEA670F5FF60EB7 + 6B439F5D0FFAB9369CB5F2797B3D177860E482971130987A2070DF09F6243F93 + 569CCA7DFA00C376ADEAE32118BBFEB027303018C0AB659C15F58B14E5BBE46B + BBFE0179CBF2CC02C1CB8FDAB1CA3EBFD4F51D3EEECB3E13DD4B3B2DED0CD8A5 + 5D04B85CB79785077748733470D6D22FBEF6EB7F84BAF9FE902380EBEBBEEA68 + AF36BE0474C90BF8220160EE4DDC7B16994D5B1A704F63C967967E661B04EE67 + 7E6FB3760E7FDFB7FB2036D9B95F5ABE01BEDC3BED3EE9798F00FEC8C1EFCB6F + BC9FE6D83DDA237B59D2D9F7FCB57665050FDFCFFF8D0FFDFEEE80B7DCDF057C + 057D23FC655CC078827C5DA77106796FCBD883314A911E56D31805690C13CB1A + F368DC13012F6D5EDE951F5EA33CB3C05EB5E15833B766303CD5C65DCF97B199 + 60EF0BACDA42B4AF8DBF18B769EC368DD4C675332CEF10D8EAA79548E01AF82A + 45D4017F2703650BE88DD09771AAC361CB037DA7AFC9E34E87B63E46DD54C69D + 794C4A1EA88BBD132DDCB12145FB3EB722EFED3B8DE30388192787F13763F708 + 8195477A98927CDD2782E006FE02782DAFF13C69676C9F45592058F077E26B00 + DF0E00D632D013CB7CA39E93B8DDE6203C54ABBD7D1DECCEB3795590565DA29E + 7D7FD178E662360FD3FC8B876C9B3917623E66F28775CB1C8BF9973F706B7907 + BC65BE87E203C05E9E63E5D9962F734700307D98338E12E0B5BCCF292D3FFA45 + 9BC7568AF3D0385F8DF354CD6D29FB6A58CC8B839AB972C92301DF36008CE23C + 1BA93C62EAC079BAF2B5BA8E6B6501E0087F7B01E051CFCFF67D7F23001E6936 + F9246AB9FF82B4F8367A29FA46862BDA477F8C9701B7081BFE19937C3A832942 + 60FAD40058C7C97EA2E8A3CA3EABE8BB729F56F171A158D7F8B962FD846E488B + A2CF2CB68D7D1ADF5AADE013D4F186EA37C0EEBEC38ECF1090DBF826ADCE9774 + A65F68379AF7693C60179FE3D44E5FFC8BD1AF198F5BCAD4D5BED8A751F4B3CA + D71AF36D6D0480057D470D017F918ED5A6C1FCCF51F9FCA4DDCA7EE39CCAD6E6 + 438F8A6DE577466D6D7BA9E903BF28C0AE01C05A85B385393CA906BC368E67E7 + FA5D017003246B5E3390F974D94BBFB6765DAA8EFFAB2588AAE3C6F260AA8FF3 + F7507DFEFAB5B00CF450D1BF8DE81B606F9B62346C975A41EA3F96860B804792 + 3AB4B53EA380BD45F67FB603E0F086EB8DEC59F7877E1FD9975C4F3A940B5AAD + 5EF65ADD17B5BFAF7481EEBA50978B7E976D080DD556F552BCD98DB6C1C51806 + 2EDC7CFC26AA369DE85B6E92BAC9BA8D0105EF5F8F9BAE6EF883D5A11AFCB6B5 + 474DBD8965AA478CB7BCC9F326FD3FA36C9094F303C1B20F844ABFA63FC74376 + 8E265FD40C9C34002B03BC66E086189421E5ABBEB1BDFA0F940D56BBF251B15D + 5673EEA0B6BAD807F9A0B40C8A9BC1711828D7D240BA579B51937932B37DD09F + 07EF5971C0DFA6B6FA384150AA498B262071B2247B9CB4C4FAD8AE575DECDFD8 + CAE48A49DB8869436BE4F4B903346A864D104BDEDB0176A774F612D6645113CD + 5A71E21A27B052DD57ED6B8D65CFE04A0DF4A50DF930D9D6845A936C4900D927 + DD6512CF44DD9FE4B649FFC479CB5CAAC36910A373E332CE635E36FB6CB31745 + 47828E131D0A82BBF178922282750E3FCF5C4B0300AE2380272D5CE500B7719C + 14270ACE11815D494FDBD77AD68EF1ECA24E04B03B7F880A5EB4264D59B4364D + 7D755D9AF6DAFA34CDD2290061AB9FF49A1D73B1F55F62F9A5A6121D3C71A9C9 + C1EFA63465C9E63465F1E6F4DC6B9BACCFC6F4ECE2F5E9B9A52552D8F4BCB59F + 62FDA658FFE74D1E3DBC6C437A8E65ED4C13FAD6A7F168C5C63479D596F4DC2A + A22EB6A6A96B77A4991BF6A499EBF738FC55942F0EB3E8404372B2A1E938E470 + 9C05C79ADAE3A0D39EC0440823815D9675F6A59DD76D774D5DBBAD4BD868AB36 + CF63A7FFC6DD05F2E6BDE654F6A5072D8F73D2A3518A7D2651C05B3A7BDC4902 + BD383F251C9F6D4B21E21C25EF0E5B1CA7C1292B519633353A7001C2CACBD12B + 484C9988E005440DED7F27BDBCF7AD347BCF9B9E9FFFFA7B695E81BF8060DF13 + 18B06B298077F6016B0F182ED1BE9D7D81895CCACE6B20B02282E7BCF99E473A + 39E42D7227B83BCA0BFC35E1487FF523F60066E9E7EC78F728E08F5846336BD1 + 279F59993D83ED181F02873FF574C1C79F34C096A8DD859F7EE65AF4D9E79E62 + CB0000C73F7B02E7883239FA05039477C0F095A5070FE588E012154C1E18ECD0 + F760377C00660038B48CA9A2D790963F05EA2A1534A1BC18B87CE45813054CFD + D223C0152030FB6C5ABF6376ECB26F3029CAFB020B0A9F7128CC72D1D8487D89 + 6896840600D3CEC4BE9E7DA7331C06F46E228AD1C43EC2440EB31CB4E094A20A + 33F802DC5E306530862DEE492A882670462A280B50DB5C4061046B2C33CDB959 + B2161017011C7D635EC7CAC7CEFD751CEA745C7FADE1FC3A0665DA720CA5AA03 + 10021C23C4F5BADB39C233035EB3DFBA9436DFB5FFC5B4E5DEB5B415B87A3F03 + 62DA4570BC857D70018C56C7F138BF5E13E7A69DCEED4B19976320FAE898B413 + 84F63E66A38C78AD9CE3ECFFFEBFA66FFEBFFF673AF83FFC373F37E7531BFAE8 + 18DB1FE63D87E9B7E9F6F5B4E321FBECDEB13C11BA37AD2FE7B9E5B62D77EF58 + D95ED79D3BA6BB9E6E62BFE65B37D2B63BB7D2566BCFFFB1C58E03F0DD7CF39A + DBB7DFBD9DEBED3C88CF15F9D2D1265E5FF37A4A64F0FA9B17CD7EC55E9F9DDF + DE577F1FAC8D3E77F2805C606B8EA0CD10550F1020C0F4963B19F4FA772B7CC6 + A41C27DA794DF161037D3EFAFEC4F39257DD9A028091227C01BE08C09BF7DDB5 + EF6280BFE4495796687BBEF3FCC658867D1DE7BB4C84AEFDF64D405DA4E85EE0 + AEA27F01C048ED979F3E9EFACE9C68A0715E1AFA625A61BFEDE5674EF95EBEFD + 0E7F89F80500730DC8E017E8BBD2D2D567ED759DB63C0F9A9C3A9DC1EFC9330D + 08265A78E909BB4EF1D08B9557593F1E4601EE2245FC2ABA97EB9FAE7BCB0F5B + FED0B106D63AD8B56BA5227823C0A58D002D765D9311751EF11BDA22DAE82120 + 45F6720D17D045E4B5577CBCDEB33FFCD2722CEC31DAD701326D8BF45092E0AF + 3F7CF4B1B5F9C8EE2FE5C126E491BF764FE45E49DADCE300C3FEA054BE270277 + 75AF7CE5DD4F5C94E7BC61F7E352E765CBCF35DB2B6FDAB1D8BA817BF61B1F36 + DB38700F27C2570F8069A50F45FDCACEFDDEEFFD8C074CD4A1664968B329C297 + 9471C65CB3F370591C5B687CA11548B0FB38A58C59BC9DA5B2491AF3482F6DEB + 88D5533486D20375799C15C7526589672BEB413D566399CAF8CD44D96D653CC7 + D8CD1FD85BBF3B3FE4C798B2405FCA489017017801BD3C30E82BC830B634E921 + 42F28ADA658C1961AFC6A5CA6B3C4ADA9607FEB2CCB3AF9C5356C789CB3D4B11 + E8328E271F01B0228035C62775C00BCC0D7DA91BFFEAAAAE36C392CD0926BCBA + 228D7BB53F8D5BD8E7628EC283AA9316AD48AC5634695107004B1EDDDB80DD2C + 9FCB14E08BB4CA52D6729B57D9DC673EEACCAF00BA4873AF09C8EC1398A3515F + E65948201890CB0A509A9F45F903C04500DEA85837967298533673CB6AFE19ED + AAD3D64794C9BB662C68E6B59ADBC6393279E6DD0059E0EF386B132381D59E36 + 28CEAD99C33F33F5659FC7C7793DA22C8D2CE998E91D09F6E24B881A007EDB54 + 60B0006F9BAFA33E8EEAE42F89EDA3EA7A95A3047FDB44FD4853F4DF8C9A6CAF + E9597B1DCF5A3AA02E2BFA7862704127C860A0EF496552C15FF9B614E420A9EE + 69445FF58F3EB5C6D7D743F118C3AD6F7C85451D3F67C7E66DDDBF88BFB3D868 + 83DF32FA2E63B9A403824542DB365F6B2D7F3D2D766978F0B5730C87BAA3ACBD + 69B07EBF467A4D9DD736B4E443265FFBCF1B8D7A2ED551C1C351EB71F09F8FB4 + 3C0C039FBFB5D36B18C96BE6B56347557F8ED9EBF50E764E97CE57D70FA29A99 + 64D9390A7F81C908E63E8D06F29D411481E76F2141541D379607537D9C36B5F5 + 93866A13EBD4B657FBBADEF3D40598FB346A85BF520552FFD134140006FC3AFC + 450E807B28BE217F00DE7F00D90563845D7450B980D617275D986A7B2FB55DF4 + 7E6FE9421DE575E5E24C990B7CECD34B5DFD5B54D78FB49BE4286E32F61E8DB6 + 8105007894DDA479426BAC0D14C64F9C99C68CB301437533E5463E7A820DB6AC + 4DB4B54937E3B6BA5E529F5A4DBD4900587B16D700581058F90C8173BE0BFE4E + 28E238768E383052BE197CD5B26335ED6917077CD4593A60A057CA430D4C635B + A4C168D780B43AC750C76C44BB30A06E93F655C97BB93018EF058067D920BE1D + E04A7920DF1DED5B2BB6579B314C622CAFFEC8271A56A7B496EC6D93A0A8B67A + FA4A6AC3844AD1C12882DE5E8A13C25EA2DD33765C8F049E3610FE469BE06DAD + BA7D2CB7B5479A10D7F05793E436008C3409975872DA9FD62EE0B701BD262280 + 7120682930A9712A581A1D04EA435929A25D94EC1D709C2510CCDEBF1210D8E1 + AFEAE777A27EBB00B0B7EF3C351F25005C43E0363500B8387E785A7FC22BABAC + EFAAF4DCC2B5E9F985EBD2544BA702845FCD0078F2E215E9D92576FC252BD204 + CB4F58BCDAE1EF84A5EBD384251BD2B30E7EB75AFB2D69CA22CB2FDA989E7D75 + 7D9A4434B1B57B6EE9BA34C501F0EA3465D9CA3475F9AAF4BC47026707D7244B + C7F7AF4F63FB37A4712B36A649AB00C0ECAB86636E779AB9715F9ABE7E4F9A4A + 1915279C1C67C8C1EEEAAD69C6EA6D69C61AF29BD3B455397A82288AE88C8BE0 + B701C0261C8402C0521B0056C409E05700D8A34F2A67A4F603AEE16F94A25B14 + E58B708ACA192A07A924BBDAE15095ADCDC94AB976D246A7AFF272080B0A6727 + 7007FECED9F7769A7780C860968EB43A1440F0FC37DEF3A5A267EDB573EDB763 + 2A125870F71DD35B1904CBB1BDE09D8FAD5C1CDE1EDD04EC65194C1CE6444F7D + 9616BCF789DB48B1E73A13D1531FDAF13EB03E450E7B4D406060F0A24F80C4D9 + F6DA675FA4459F7FDE0060E0EFAB565EF4D9670E097254183001B8F0B597B32D + EF17E950C0ECE4051A80BE1EDDFB15D1BD19007B243080B86997972205007BB4 + DBA1236969953A042E117380124160848D48619689264218E5A8E01C35BC9A25 + 9DCD4679D9F1630DFC1500D6FEC0EC0BACB20030B018BBDA206CFDD677C5596B + 5BF60A06EA0291FACF9E74B0044C023E01A1565EB0F35FCC508A36D8A8F788C3 + 52AE4195DA50267211F0BB812572815405CE22E515FD2B10472A008764533D75 + 0820471965C897E12EC78D1038F6A39DFAE81C0E172D553D65814BF2D8D65EB7 + D77EED9CC3CA8DB7AD5D1191B95A261AB0C9D2C02C07CCF2C09B0192B7EDF5F3 + 9AEC7C883C122C76D85904B8454052815CDA6DB23CC7C7A6E3007229D3EF83BF + FD391DFF5FFEEFE9FDBFFE9281A5F51344D639685BDB78DD405860F5C65B195A + 6F7F70CFCE75D7CA2C516DFF97036022946F39E404C06EBF7BC7C57BC67BB3D5 + FECF2DB74CB7ADCF4D7BAD0060802C609873732E7BFD443AF39EEA7DD77BC67B + BAF5FEF5B4F31B7B2DF7ECB5DDE980DBF8F9007D055F8920D792CA82C09B6E75 + 606DFCBCF5DDC9DF8F0E08A61E91E7FBC2030D7CD749E9A7CF5FC7F47657ECFD + 2FD1BF02BB48AF4B65EA90BF562282A9BB48E42EC0F7A2FDCE7818E364037C57 + 9CB3DFA9FD9E942705F62A3A5836DAA81DF077D9A9634DDF6CA72DE0F7B4D5DB + 75E3B4FDDE2DE53A906575805E5617386BEDCF65F8BBF264D6AA53D6FF841DFB + D8A9B4ECD8C9B4F4C4A90C80C91FB5F3017C8F9B0DB87BD48E7DFCB45DDB4E78 + 99340360D341BB7E7D7D22F51F36DBD7ECDB6BD748AEB39FB38C7306C1886B28 + A903E2725DCD60B7805A2B47BB6C44022FF982FD78EDFAFEE9977E5C2DEDAC87 + 7922F4557921F7025688F8DCCAD68E88DF7C2F20FDD261B1EF451F04F8259277 + 9EC9F705B674E187768F3111FDCBFD8A3CDB19703FCCDB23F07014D037DF0717 + BEF7A9A7F9BE99412FA9DA71CF8C005851C073DFF8C00130C057FBFBE687B7AC + 5D01B80E7779B0CB52C15DDDEB1179015DC15D49201779A4AF8D3334A6509DC6 + 1FD4FB314BFB7AECA231892F156DE538EE896D194309F62A1FC74E8CA532E8CD + FBFB22EC0D00B6715B3396DBC0C3788CBDF2C37D8CE19E5FB3358FE34CCFAF61 + 9C99A16F84BF0E7D4981BB8C212D65DC8932F4CD364900574057634DC1DF582F + 29D2B796AF8613806E04BF92EA057149D55E525D0D776B00CC7EBE82C06DF279 + 4251637F9548DFFE34765107FE46004C4A393EC0EAF50BB234F791E27C487280 + 6B7322E654F1015BE65E02B2CCB3048027CEB1B995CFB1F23C2B8AF91573C038 + EF8BF3356CECF7CB5CB106C0B522F88DA05770B7B6AB4E00188D9C61F3DF0A00 + 6BEE1BE7D39A5F8FB1B200B0844D6DE203DA4DDF327F271DFD82D981BD655E1F + 01F088628B51BE11FA4A44EDFEFFDBFBAF26BB8E6E5D13832B14CAC00304E151 + DE57C113F416342008800448802440C2D0BBCFEFBDFBEC3E264E2BA24F48476D + 4E77F4E99042178AEEE850842E245DF60F902E15FA47A9F18CCC77AEB1B2E65A + 5500C18F601F5EBC919923CD9C6BD5AA3933C73347CEE88F584BF257D4EAD52E + FA347A29B66903BC31D2B7CD96FBE3A3B131466CCCA00881378D665F52ED1392 + 64CB0038C3DF5EBEA7DA2716CB51ABFC5F45B54FAEB1577E35B785B6CAF75357 + BF325E5D17C712D0ADD5F81F4B9AF3F826BBFD93F155776AD7D8423B016024BB + F709D238D1DECF3F1B7DC375DD5A8A7DDAC6FE398AE795D5EE6F8F920F99BCFC + E4ABB4F9F101E00C4E6DBC96367E8C088751ACB7F63ADFFA9CDBC673691C8D59 + D7F7501B2FC9B26330968FB71A00EB159D6B6935DF69511BF0AC6D0FABB6B11F + 665CF5EFA7B67E8FA2B5C66DEC855756ECB2669F0DCF5C4B1B376DB51F62ADDF + 06008E12EC6D93B7E1C346E81B15BF90CD23F947BB891F6EFE8177FD614CEBFE + 51FF27A487FA675F537CFF7671D205D2545F9C7AD97BA9EDC2F74B4B17EA28AF + 2B1767CA5CDC639F5EEAEADFA24E7DFE5E78C2688BDD7437DBCDD2C12875F63D + 6CB28B3AF601260A65FB8D81A1FD690B938770836D539C28A0187DBB5EB58DCB + 7938B0DE6A6331A102D86EEBA83D02388BCFC5D61A7C4ED99AF71D6B8C41CB5B + FD2A713E4C96C204CE277428B4710192633D133B4DF42A5BD7C474D86CA82E47 + 5B50731E61AC5E935DD9F534A4B769994C4BF5843B035E26E54CC8B1D9989E76 + DA0B00B74DE49547FD606F5DA732FD3A4F839A68532D82E2E286C508695C0469 + C1A2454EAD585F8F4DD9217059C045D0DB4B5A0C6A41D89DA7DE167E9EEFDE3A + 4A4F1B47A94E8A90B7AD4E8B5D29025ED5936F05C08CC9D8B6808E0B6C2DB2E3 + 42DBEBCAF65C380CE43CC011A0729743E1F04CE34888CE81D847FD047AA33341 + 7D5084BF02BC5DCE8DA21A00E330A92170768EE4A7E26BF88B9A7C80BD44FCC6 + 32020063670BE8C1099C38CB6908DB89936974EC74DA3E762AED183B99B68F13 + 157C32ED9C3A9576CEACA4D1191B7FC68E3FBD9006A7ADCFCC292B135DC05671 + 67D28EE9F3D6F699B483E8DFA9331E55BC0D47D1F4498F36DE3E7B3AED9C3B99 + 76CC2DA51DF32B262B2F946DEAE6CFA6D1C5736964E999B47DF9D9B4FDE40B69 + A769D729A236785F5BD61ECA2B8AC078B671A0ED5AB990F611D57BF23907BF19 + FE3EE3E9BE93CF7A34B0B68746FB4FBF909E3AFD62DA7FEA05174078AF0987A0 + A27BE54414F4256D6C05F80A049347FB1AE5F7CD315604C06C3D48E48974E0C2 + AB696F89EC9513B471921667284E52CA6D8057CE58D2183D2C51879316E1B005 + FEE2A415E0C5F12B288C0DE72D7939850FBD4244D05BE988D51D79ED523AF206 + DB43038CADCCFB81CB3B8281C0BC23F8E957AD2FEF0B2EF09748E0E3EF5E4B13 + EF7F982681B90E7CB3F31A8777767EBFEF6577685BFE28115205028F591F52C0 + 30F613D6DE1DE50E83D902FA5A3A7AF96A7E7FA29571BCE7A8E0BC55F4C47522 + 8671D2DF481337ECF81F587F4B11D1C040E031407170F22B0A18202008AC2833 + 60846083A71F9BDD34C53B8495DE2A00A2C00B871BB7EF3600982D4CA76E6600 + C1B88CA3F75F4EDFF93C03DFF2FE4B407006C26C099DDF09ACF7042B0FFC25A5 + 7EE1EEBD347F3F836040AEC0AFE02FA9A27E4981C4D828AB9DDA520F045EB274 + E5BB0E54428A4414CC150046B413FCA52CD88B00C1DAA2163B7901608758C0E5 + 3FE4E84A226A05DD04EB48739425D18C1DB8EBE502E090A09EC642E7FFC256CE + 00E31CED4994F0E93F583F6BD38CE9EDF31852A77F77D431529428E9D93F7D6F + E76ADFC91FBEB13AFA705E5FFBF9514F3430DB3D13217AD27416E8679F9BF33E + 65C715FC057846F00BAC8D80983276DA34B0D86CD81D085336717E9C9B228705 + 5929F3FD20C15745E392F773B5BCD2E7EDBC1DD40270FF11900D10CE11C0AEBF + B1FDF35FD3F93FFFD96126EF700608BFF04F1900F3F9499FFB1B5B42DBB91400 + 4CEA51BA56978F15BFCBCE671048BFF08F766EFFF427CBFF60C7CDE7ACF32505 + F40A000BFA02A40563B3F8BB67D8CFDF3A1F877C8E52E7B7C1EF84DF06BFBBBC + BD79AE43FC46B0C7DF95EC8C49FEE48F19F0EA5C88EA65BB65227F751E8AFC15 + ECCD11F5F65BC06E36202DD1B90B5FE5072EF83F02E21281AFFFC5F87F459BC5 + AF89E8457A4883ADDD3304C6467F52A02F0098087F7FC7B7FD8FC7EB447E7F78 + 2EFBC3209667D701C1DF95FBD6E7EE170E7781BE33F71EA469BBEECC7C76CFAE + 5F5CC3EC5A67790031A01809FED267C15260EFEC4DBBEEDDCAD7C7E5CFED5877 + EEFB0336C05A45EB22E0EF2211C13C48433FBB5E72DD6CAE9DE59A2C80ABEBEA + F447762DFEF0A6A519FE4EF1EE77AB0710FBB5DBAED1DE8EE3943CD017F87B9C + D708D875FFB8DD438E712FB1FB02F784E98FECFC80C002BE76CFE1DEA3FB0F3A + 52CABC86C0B77E66CBE6B28B45E79DBEE51E67F7330160449EFBA3C02FE96176 + DC28F74C52B505FE1EE2212CDAEABDFDA6B8C5B3E06F84C03E1FB0BCE02F369F + 0FB4005FE60C02BD31AA57F30B3D7CA6B90B79DF063ACC5F94D73C06EDB53CE5 + 0880D5863482DF087D994B693EA579579C5F91F7F6B463EE65625797FD36C742 + CCDF80BF3B6CBEB8D3E685399FB77096FC3DBF08A80BE8255F00F0BE325FDC5B + 6C7A5F6F06C0793E2A0904D78AE0572991BE515E67F3E23AEA17A82B785B4BD0 + 5765E0AE802F799581B76E2B7535E05D057A4BBD2071A389C5AC295B7B4C2E35 + D07764CCD60996C6354C0386AD4E02026B1D44DA0B006F33B1168BEBAC6D4758 + 7FE5B51ADB36D346ED3C02D8F24400D7EB2F97D6795DE3E5F1B7DA981E21AC2D + A06D2CD2C1A7CD66EA07806BB1EE641DDA80DEB226C54EBDD6A75ECF5AB68875 + 6F93D23FAC9915F92B102C004C9DDA692DEE365BCB13014C1A0170BDF6DF50FC + 01AB606F4905841F27006ECBD7C2CFA154F958277FC8BAD405806DAC11CB0FD9 + 1843960E53B6710581A92F0058D1BED1B723DF4FF6FF90E6B6EC3287DF29ABF8 + 9E5A7C5C8D7F0B1BBECAAA2EF6A915FD699E2FBEBA2E9BA91E4FF65A5DE386B1 + 625DEC2F20DBC059ECD6AF791770A9EFF237AA1CC4AE8CEE6FADDB03409537AD + 1AB38FF0BFF6F3CFF6F2D9367DD7B18D731CBFD7780FAB5F1400D3BEB4EB278D + 85EA711AF660E7E6F588B15D66875F6C847398EABED5D851AA57BE4DCE3BEC38 + 6D75B59AF36C91F3173F471BAB6234E23DEB51DD57FDBB6C2DC073E3C6919F25 + DF2EB91EBBED58BDA473EBA7B67E459DF36857575DDBD8A8AB9EBC3865875DD6 + 2C1435EDD65257C46FAD96817F4B5A1700EEFA42B0F5FED1FE6F52FA81F5525B + 9F5F5CF9A2C345D22F9445DD17A7D5B67E6ABBF8FD3D142FDE51EB69D34F75FF + 7863231528ADDB3B682DC2BEC5260A8336510302373758BE2F13138D78C38E37 + 71C1DF0880A3AD196B9D527F529F9C94D4EBC99B48752CA47E710CA4C94DD704 + 887C0FC549990B7BDDAF4CE6BC1E3B13BB30C993B0C509A426836DE55E8A6349 + 6D935C6CF5763844F74ACD84BAC0DC087F910030CAF59D7668CB8EEE0860A449 + 7C036DC3043FE6EB36B2493500E61DC15AD8C4858B161F5A8860A35DB4AD471A + AF3E06E51805BC96048B6BC52DA0C86FDED3595CB6293E7DAC548BD07E62F12A + 3570B7C556ABE96F792DA4252DB4B508F7B444FE0ADC920A0AF704C1C541405A + D7A97DED4088791C127AFFAFE4809736C1C1219B54BF03580038973300AE2170 + 97C63204D6966BE4E316D00E7F277882DFDA4ECEE777784D9E32FB990C80C74F + A5ED13A7D2E8F8494B57D2CE694B67781798F5F508609C3EF4C52174D2FAD1F7 + 948D839309E7D3D934327BC6E1F036346DC7330DB375B489ADA4476700C1A7D2 + 8EF94E74C3AEA567DCD94604863BE04EBD9CF69C7935ED3EF372DABDF262DABB + 02FC7DC9DA75227FE53CC3A9B607C84B54C6F2796B732EED360181F700804F3D + EF91181102037D638406929D8860406F04C0519CA303DEA27AFB404439B641FE + EE392030EFA92BF2F7D89904816B358ED5E24895F355799CB23862E53C955316 + 0918CB918BE4F425D57BFB9AC89D221CC4388371FA1E7915589CC1EFC1D7DE4E + 4FBFF2667AFAE537ADCD3B0E840FB3E53350D852BD1398A85E6D033D7BFDE3B4 + 74EBB334F3C1ADC496D038B57166E3C476E8AB4827A299DEB9960EBF7D251D61 + 8BCCCB1FA463BC27F80AC097AD32DF77877A8E02BEEE115513EF7FE4EF547488 + 5CEA058189C64238EB01C100619CF21EB1051CBE91A3B94E5CFF284DDA394EDD + F8A47987B06030EF03266DA0C2CD1C618628134DECBA65F545BECDF4471F3B64 + C880A2B335A9C08343E05B368669FA930C311C027FCA7B81EFE4ED53EF7C9E66 + 3EBF9B66EFDE4B7344FCDECFEF03EEA7F97B77D3FC17F7D3ECFD1C09DC44F49A + 3C2AD8C61034767DD90D840581C823F55FFACA6CDF67980B54022421C130DE2D + 0A9412984202C00258089BE0AFDA0281CFFD11F065C7FAE6BE435300EA79D3A9 + 3F7E93967FFCD253C1363F1E708EF3F9E95B87740EA0CD06481598A39DE023B0 + F4B4BF13184807F803FADAF14D7A6730F096F706E7770777DA9DFF0B40F98774 + E1AFF95DAE003CDFD2D78E71FEAF3F398844B4C97DBFB363D207380D5466CC1F + D2391FC7CECDFAFAD6C47FFB8B03E053D686CF779AE3F9981DF81B412F9F8DB4 + 8EFE55EA50D3E450F41FF376D54891C0B4535FC6C9E76F9FC3CE89EF4875B4D7 + 71293B0CFEEB5FECF39BFE316FFBFCCCDFD8BAFA1FD2737FFB07FB3C7FB2CF43 + 3D11C036F61FF276C76C07CDB81C87F4993FF3FD6548ABEF11404ADE3FB3FDBD + FD6F65E749CADF5A7F4B7D1649EF0E76F06BEDFDB3D0CFBE57E02FE771F68F7F + F47339FD538EC47510FC87FCC000BF0DE9D44F6C530EA8CF7F7BC46F41EAAECB + 7D047B49058AFD7FC17ECBFE9BFC9EDFA7FDEDED98805E203065FFBC26BD77DB + 01AC89B2CE91BC22816903A8255A97768CA10862F2125B3A2F315E694B3FDED1 + CD188AF225656BF745FBBFD7FF7C04BF44FBF29EF0E5AFB067017FF50EDF0C81 + ED7F1F80CB032D26227EA300BF535CC7EED8F5E8AEF5BBF7A5035F8FF8C55622 + 8067EC7AE740D5C4F571913AC6B46BA3A27511D76801DB78EDD5F597EB3279BF + A6161B7D5C5CD76F7C64E3D8F51938CCF5DCAEEB1E095CDAC7EB7A06C8F97A7F + C2EE2BC789D805FEDA58B4D716D38CC53DC561EF65BB9F050906F34EDF13762F + E2BEE5EFEEE55DBE26E06F06C0D9A67BE0317FF829EF8011237F9517FCA5EC29 + F7C937DF4B07FDFE7BD9237F057F81BDDAD143E05740184520EC30B8A8DEB659 + 738758A77ACD2D90E625EA43CA5C44AFAB88A097B2E62E2ACBF6D4B3667BE6D5 + 74E04207002BAAB74D82BE283E88E7E0F8196B531EE253F46F1D01ECF33A9BEB + 31D704FCF28E5F45FDEEB2F9DB5EE66FA4CCE5AC0DE2F523FB795770B10902D7 + 51C0CC4B0578B1C508E036381C213079AFE77C0AF4ADE16EAD582FD82BE04BDA + B4A31EF03B951FCA8CEFF5E58150A4BAFC7EDFAC58A77203804D02C082C071CD + 420A00F63444FF0AF66A3D540360AD875AD756C156AFC1E2F6CFACB99AAD9F2D + AF36718DA73E8305FA4A82BF2846FFF603C0AAD303C902BF5A93A2664D6AA9DA + 4508AC752FA96C12C0575B3F2B55BBB8F6567B87BB3DB6808ED203E1F804FCDD + BC405FFC08255F4B7E07D2B5245F459BFAB5AB7D1DE457F948AC5C835DF2FD94 + FBD9B82336E6300038A71B47CC66F5D9A783CF085FCE81E25F22CDEAB4E9F89B + 048025F9A41ADF55E5E7AAFD5FD1D71615DBB4A9F1F1A1167F5C1C27D6CB27D7 + A56AAC36E15B6C53E377947F11459F63C8B7F5F3BCDA44C5FA388E497E5201E3 + DA6F5A4BF53F57BFC4D835FCED25F9BF11FEF0E8276F536CD7E63F7F18A9AFF7 + 1FB0322C423C03B08A7AB089D8B719A3A55D2DF7DD33FE6300C07504F05A8AD0 + F7A1195A644DEBE54E91533DA96A3BEFA85E6D627FE515FD1B596629D7ECB369 + B7965AC1AF540DFA5B5337002E5F58ADAE2F045BF587F85DBF82DA01709BDA2F + 5CABD576F17B52D476B17F5471D3E29DBF88723C0E5B400F6EDBE75B4013013C + 38B4DFF3F1862A085BDF70A5FA461E25F8AB72AFBEB55DF23A934F5CA45057AB + EE2FC5498FCBDAD663AEB26912475AD799BA2682A56DDBC4B06DC2A8B2EAD692 + F7AB2695753D291355C15FA51BA8AFE06FAD38F18ED253981D1B5B447747ECB6 + 4950B7CED7525D94EC5B6C4133B0FBA82FA2E2A2262E6458B890D7E2470B18A9 + 59AC04D1AE6DD114EB5DF582AD02BF6B69150C0E0B4C015F415F49D057D222B4 + 970479DBCAD15EC35F2D8E5D968F0BEE7A91ED3A9001B0A06D1BC88D7680700D + 857B490E04A97646F08E2A9C105DFD8A734202007741E0E3730E810582BB9C29 + 05FED612FCF532EFD80AAA21F0E8E449D7F0E4A20900BC9446B04D9C49DBC74D + 13A7FD7DC023E3C0DD9369C7D42907C023D336C6B48DE5F0D7FA4E2DBB3388B6 + C3D66EEBF44A1A9CB171674F35CEACBC951D8EA8D30E81871863D6CEC36C38A3 + 5CD68648071C5E19D2BE90F69C7A31ED390D587D25B10DF48EA5E7D3EE65B3AF + BC98762EE6488BE8506B9C6AA48B365E1130786F890016EC8D12F895B00910C7 + C85FE5E5407427214E3FCEF534E7DA2DD901C348F0B706C168A78DBBD3C6E51D + BF7296CA811A238365539914C7295017C7ABCA3868299347E4B1352A9097485F + BDD74F0058D1C002C039FA877702BEEB80F7E0AB6FA7032FD99800E0D7DE4947 + 0BFC65EB67EAFD7DC00E79DF4B0729BFFBBEC3DF954FEEA6E96B1F390096135B + F09728268F646A9CDB579B68DFB12BDA061A00FC8147FE027A719E037BF58E45 + D9BC1DE0F74A8ECC221A4B1058515BA48A0EE67D8E40609CFA449B91028F1D22 + 5B1D502083810C15C847D800FCF528E04F3EEDC8CAAA8F80817E8DCAD6CFA4C0 + 60B68206004BF57B81172C5DFCFC6E9AFFDCEA4DB39F11216C6358CAD6CF926F + 017DBF1329BCF405117DF71D1EBB1C029BEE6510CCF6D1A7BEF9269DFEF69B74 + 8AED5E0310020E09102D7E99230881B8A48A2A74A80BFC2AB037825E52E06F8E + 5CECA402BFA459E40B58FED68E61E309B001D596BEB33E96CA16A12F765207C0 + 66532426521E30C816D694B3AD039129670898233BD517A008CCA3CE012400F7 + 4F8C93A12512A074B8E95B0B6750ACE85F94F3792B6AC41817FE42446D8E863D + F7A70EA8D678E7FF0CC8CC205430D4CFC7DAD18688572272558FBC9FA5D4F3F9 + 64531FA297152DAB7694918E4FDE61B6B5A12FC0D82387FF0180FC17CBB3C5F4 + DF125B3F9FFD33009B2D9CFF66DF0530F78F0E8189023EF5630699E7FF92B765 + E6BBE19DB66C737CE12F7FF23CF502E10E82FFC8E7CE109BEF9A73D1E7F6CF64 + 028023EC8A64A65DF3F91DFC66F8CC39017E3B10B800603B563E5EFE0DE8EFA2 + 3CBF81E68102FB8D626FDA116D5BCA449FC73EB4E777BAFC9DFD064B74AFA27E + 1DEA161B11BE48905780973C368161D99000F0320F697C976D2BF6FF4A993AB5 + 779BA582CA2A3B3CE6610EA2FC491DF65A6AFFDB0B0FEEA5D9F2EEF00C85ADCF + 5736D6D7F6BFC1BB801F64004CBAC23B80D9EEF9CEBD34FD29BB14D87528805F + A27B1160D8E1B0B573595BE0EF6289FEE5DA868DADF6D1ECC7DD0FC770CDE47A + 8974CD15D825F5EB7429F3A04EB40377D57FF2060FFDE4F7C2EB0120520033D7 + 79528D8F186BCC1FFEC900F818F7956B1FDAD89F3A9C5EB2CFB00000B663F883 + 43E53E212902D8A3814DF961A57CBF02F84ADCA73CF57BE5FBE9F8DBD7D2E1B7 + F2D6CEDC1301BE110037F7454BB1037EB95F3EFDFA2507C05DF0B725FA9787BD + 6A28ACA85F806ED7C36176EF77186B7302E60D82BE9A63684E91E705F9C133E6 + 1DCC4DB0D35F7315ECD46BAE225196341EDA5F806F0D8005784901BF1EE15BC0 + 7004C0ECCAE2ED98639DCBD1BFC8E76C7A702FCCE572DD4B5D005810189B0360 + E67305FE46017B15FD2B311FD5BC92B9A8207084BDE4558EF34EE6AF6A8F349F + 8D11C0FD00B014C1AF845D8098F7020BFA0A00D710D877040AB037C260F523BF + CD770FB2358649F03702E0FCC06AF500ABAD6F22F46DDB0E5AEB205F17D1CEA4 + 08E0B8CED276D071BD851C009B047F11EBB07ACDD6B526C38E2AD84BB9B635EB + CA22815CAD39B5168DF0370260AD4FD59EF2C3006087BEB6BE8ED29A5BEB70B5 + 8F70B7CE7B99B402C3B52FA0F625B83F21F820DAD4F8217AA8F669D4B6A8581F + B772EE02BAA15DAC6B93B70F003847FF9AAC2EFB6F5006BDBDD5ED6B8AAAFD53 + 3500AED5E6134311DE22B5EBF2A5F5F1AB218D11C741B14F5FD5E30F76D4F812 + 6339F822658BF54D1EB5F934A36D3D7512FE5E4BF197CA9F1A7DB76DAAFDAC0F + ABC739165A0F008E8A7EF2B67A497EF49F0380D5AFEEDB30897AFBE70AD8C6FE + F518EBD24300E05E1C25EEC0DACE6956EB6701E0083CEBFA363560F40956DB79 + AF57EADF8C5778651BBFAC5E85DB619A6BA815FC4A61C0DFA2D68C00AEBF8C36 + 00DC7CF945A1EEA17FE4BF6B9DB2EF75A35D9436DA8584A76474116A51DB85AB + 4D6D17BF2745F5C5FE61146F5288F104817513531BB685461B36D877BC81DFB2 + B5B5B27F3F76B1672B0F20313768C15CDD6CEB1B77E7E6CB71B32D4AEDFB6955 + 1F52B3C7C94B577DE9C77B8C9BAD4736DB670A75523309AA656D5B554DDCBC1C + 6C5D93C162EF9A1886F6BD268C710C49F6D8C6F361AC581F85BD99B0B27D73D9 + 4A67C3B0D51775B6D7E9164F6CE6A736AD6F15F9DBD15A93FAD58A60B74DAA97 + 0497B7EE3A9206F71E4F5BF71C5BB520F2858CF5D56244B636C53AF28CE54FE6 + 5663AE5A78D50BB66A51B7962200F685E19ECE0233825FD9E3A2732DADD527D6 + B108DE5AB6808E6A203040B82CA6E3625B8BEB66816EA916FAC05D45FE529613 + 21C25F5403E036A781ECAA535EAA9D133A56E3E0288A4E0F4F0300EE72A21CC5 + 5192A16FCC77A90700160406FEE628E08534343E63F6792B5BDDF8298F001E1D + 3F9386274E5BBD0950ECCE1F1C4219FE8E4C2DA451D762DA3EB992B633F6E452 + DA3AB398066617D356D3B6B99534326F632D9C4D238BE7D2D0FC19B3E788E091 + B93369FBFCB9B473EEBC4B0EAE0881D9E679EFC997D2BE532F9BED796B4FA445 + 8EB2D8B1F84C27EA1788CB567E38CBE66D2C1C6E05FE7A14F0F2791BEB42DE02 + BA484E3A20AFF2B5E4288CC269887CDBC0E567D3A89D871C86005F815E45034B + 3500565936C06F84BF40DEDDE7ED739FCB91C1D40906CBD18A6355EDE560151C + A60E476C74D8AA4F0D80057E1511DC0584CB18075F7AD3B77B3ECAF6CF96F23E + 60BD139808E083AFDB586CFDFCDADBCDFB80890AE65DC080E0E90F6EA599EB39 + FAF738D14C6F5D4D27D03BD73CD2F790F589DB5CCAD14D3AF6EEF534E6D0B7F3 + 7E60E030FD70AC037F4F94E860E44E77B3358E7852939CF4717BE808840102B3 + B76E7700B0F5A14D8C448BB040E529B6172DDB3FF32EE0D902808104DA3E1AD1 + 079B04E898057A14082C200CFC25422EBF27F3AE8307D79DCFD3E267D40153E8 + 67E7CAB1EEDCE9C0DFCFADCE52E0EEF2FD0769996DA1D92AFA8B070E7E914702 + 0382EF0280BF4AA7BEFE369DFEE63BD3B796FF2AAD7CF9655A2A11C31100038A + 16BFB03E5FDEEF56D7F6B25F66D8F40DE037C329C1A8580658097891A78F6F4B + 6BF58A687440F847B685CE6D68AFF708C7E861A46D70151949E4E35920ADA588 + B118933A405C06843F7ABE56B42B6A93FEEAEB119C0532BA008F054012210AF4 + ED4402035533C0056CFAB9797FA260819240E67C7E3AA6CA0E980BACD4B16417 + E8549B26FFA79CD738E4A9579EF7EFB2F5B2B64A06BE22FA22F2B4014CFB7B70 + F96C44F1FE8D8860A279FFDC15E50BF00500AB0CFC05B8F2B9FC3C0B000668F3 + 77E01DC057FFF7FF2E7DF8DFFC1FD3CBFFE29FDC76BE7C46E4EF992EDF33D037 + C3EC9CD7772A70EDE302A82D75086C9F8963E6EF91348BF30204E7327099EFA4 + FCFDEDF704BC456C971CC12C12AC450E5DBFE3379B7F7B7AC841BF41B77DDB01 + C048BF7981DD382EA9EA256CA738C63796379DB63C5AFECAFE87BEB4FFD56FED + 1CBFB3BE44E996F7F40AD692D2A6AD4CFBD897B17907F8B2FDCFCDDEB5EBC89D + 4F1D02E7073FBE4CF376EDA02FD705A02F91BFA427BFB2FF5500F067777D7BFA + 194B79D72F5B3B0384FDFDBE7733209E02941611ED4BC4705724F06D4B3FBDEB + F037BF73BDF3C00CD28332BA6E3AD0B53CA91ED401E0D6E29AADFE53BCCBF77D + AEEFECFA9021B040B0DF034C94198B6BB447FC9A80C0A447ECBE0204CE00F89E + BF5318A8CC8341470BF0D5FD84685F3D8444CA3D29DE9700BE6CFF4C942FAF44 + 00FEB2E3C589F8EEDE37F3BD31E649A35DA0171D7AED92A7D463A72CD0ABFBB9 + EEF3CA4BDCDF758F97B8D74761ABB77D16B41530262FB0AB3EB2D1566D34AFD1 + 9C05A99FA42D9F9F7EEE0D177941DE18E11B6D40DFA85D25A2773773BB007D77 + 5573398F0C0600D3C7DAD700D821B0CD15057A497930B1B3EDB3DEE36B79E681 + 73805B9B6BDADC91792580B786B91100A36817006E1E9C3479DF323716008E8A + D05791BE51CD56CF96575B1EB814F015FC8DB60C7633006E9320B0AF09C633EC + 25BA57A22C9BD62C8D02DC95B0C548E036390026656D54D65C5A4749F51A8B3E + 82C0C0DF58A7F6ACEF6AB541DE087FEB3AA4F5278A90B786BEB55D6B54525FB7 + 8635306BDB7A1DEC6B61D30065B3A3CDB60647F59A5CE3007781BC125B3DFB76 + CFC15603E0B8F6973F60953FA1F821A2E483A815FD1428FA37A26FA3579DB401 + 55005875F178DEB6D477A9F4F33E5DEFFFB5BCD93BFE9C0C812913F1BBDAC793 + 01B154FB996A008CDA206C97BF2BD4D7BE30B5ADFD65516A13DB451B8AE33792 + 9F2FF8FBFA490017B5FA224379CDB6D2C3DAA23D962DAD775544BD4070F4C5AE + A55E7EE1C7A5E87FAEFDE9BD147DE4FDE4BEF2E0375F4F9F5AEA17FB465B571D + B0769DC0F6B1AB85A97464E72C7ED2CA698A7AF0B18752ECBFDE71D4E74956DB + 79176D44DAA6BA9F9AF102B3AC19E6EF0078B5BA0070913E785DCE0A0038FE11 + 916CAAFF5DBFA0D601808B5D17A8B5D47AF1FB15B4EAE25FD91E56AB6E52E5DD + D59BCDBEC56E947A372E657D177A07F0666E9EF61D0252B76CDDEBF26D1F4C4C + 02782F707DE3EFBAF1F2BDFA77CBF13B6DA2E204425059E5B6F66D52DB669C00 + 8011D03A8EAB7C33E94194D790BF5BC4FAB64EE4C2C430DA629B754D08C338DE + BF4C246B3BD284B675225AC6EA6AA3C9739954F79A60D776F29A9877B7EB4CE6 + 35E15F8FE2E4BF5F9DE0EF802D4206771F4DDBF69D7000AC854D5CE46851E28B + 914A6AD7A67A91A3F67111E66DAB059B2FDA02E05D4B7111B88905A37D9E087F + 596446001C17AEA8598086BA68537FD456A77E80DE1AFC768905755954BB383E + 0BEC037981DDC816DA380E58E4037705801B8741710844F81B25C7439BAD969C + 0DB5E47C40726ED400B89640B09E8E6F05BEB50AF48D5B40D7659700F0C45C01 + C0271D028F8C9F36DB99343475266D9B3C95B68D1315B092462697D3E8D452DA + 3EB990764CCDA7ED530B0E7F775AFD76DE03360B009E4F5BA6E7D2C0B49DC78C + 1D77EE541A5A38EB1A9C3B9DB69946706C2D9C7760BB73E199B4631E913F9F76 + 2F5D487B969F4BBBD1CAF3967F31ED5A7C2E6D5FC049D6719CE120630CDAB2F5 + F32E77C265C718EF15EEC0DFFC1EE03DCBCF7649D1BE520D83DD89778AAD00B3 + A2F3100176E53424A52CA84B3E6E031D812FE56873281C9CA2384901BEBD0030 + 9213B6EE87C853573B75B129EF65441B1CB944F85400581098B60060B67B260A + 1811F98B1C003B04BE940E9528601421F02193835F135B557A94D21B97B353FB + 6D1CE1440B77DE73889AAD2DB1953638D03B11C0391A58CEF5E8748F51578AC8 + 6ADEC978ED863BFF1BA7BDA572E2030F00010001EA62A4980041041102C00E85 + D962943AB680BE6965FA58B9011AB4210DFDA80352F8D6CF4092023C880476F0 + 0B00BE7DB7898C9BFDE4765AB8F3994360003091C640E7D93B77D25C00C04404 + 2B2A586545FF12E5E730F8DE7DABBFDF44F50178961E10E567F57AB77001C071 + 1B68DE2DBCF00050C4D6D240A2FB56E65DC380E00C715184C011002B52917204 + 616C4F3BFBE09EDBB101CB4881B82ABBEDA70C47E913FB930213C9730CDA3AF8 + 353B125406E86638D781AEA41D7098CB6A2B008CFDA454CE43E0D541E99F6C3C + E0E58F5F27B60B1604CE5B4BD31E70093CE6B8194C9EFC211F4FC7F463D8D892 + CE0B711C014E1D5B7952CAE4E977F60F3F356D1179815DE0AFDEC18B5D8018C9 + 465E751AFBDC5FCCEEEF3F66AC1CE50BF42505080359F94C2A3B08B6BEFE5E5B + 8E6179FE2600E08FFEDBFF3A7DFE7FFA1FD3C57FFDAFF2DFAE7C061DAFF9EC7F + B4EFA644D70AFA2A1A18F04B3DDF6D8CFCCDDF017F3FBE4B7E0F7CAF3F366501 + 6141EA936C45CE313C823D6F99CC39EB77A7DF93445936415BFDDE1079A27E3B + BF33FBED612B0F38D0BEF94D9A5DFF2F3CF8A0F14E12896FE2FF0E9107CA0AF6 + 027001BC947957AF6CA4D8048AB1090053462B5FCB96FFAF01BE80DFE9CFEC1A + 52DE19CE35826DE7E7EC1AB164D7056DFF2C71DD10006677026C805F40AF2030 + D1BF13762D4344FAE6F79F5B3D11BE7EBDB36BD8A776CD2A0038BF6FBDF3900D + D75A07BDD65FD75287BE5C3729976BAA802EE2BA2EF935D9DB60BF61D777BB2F + D835DDAFC38C6F6D14B1EBD7796B4B442FD097C85F6DFF7C94FB88A59CDFFC27 + 77D3E4F55BF97E72D9EE51EF5E4D472DF5878A6CECE376EF11E4D5FD88F418F7 + 2DBBF71D35BB03602B7BF4AFDD1F8FD97D8F07A18EBFD981BEFE60D4454BCD36 + F696F57FE3BD2672374AC097FB297D8F5AB98EF08DC0570F7BF90E1FE5BE9EEF + ED9D7982E60882BBAAAB451BCD41F4F019527FD948D55E6D507CFF6F9CBB1001 + 4CE4AFD4068015E51BCBB231A7626E05D08DF33681E028ECDEE6346DAA6DA057 + 2E5839CF1105673527D4FC13F92B4B96ACCDE2059B776648CBDC9276F4AD216F + EC2BD82B401C6D5111F84609E822206F84BD12E568F37C517EA8332BC260F202 + BD883C4078707CC925384C2AD8EB0FA61ECF0FAA0A020B08AB6EDBF1F6F7FBFA + 1AA7650DA47AEC1100B729AFA9725E6B306C4408D76DA5B8866BF215E065DD18 + 416D9BE21AB386BB759DEAA5D8CEEBC3BA19C5F53175AC779BF7FEDAFA384260 + B58DEB675F4317B84B1ADFFF1B156D71DD8FE40FA87D0911FCCAEFD04BD17FB1 + 5E3F47ED1F91BDAD7F5D6E03C551DED6B479FBA1B479D4FA98ADE3CB11DCB536 + B4B531549FDB74C0AF546F011D7D4ED9EF945F6B165F6D265F97ECB12EFAC2D4 + 762DD136F66F536BDD3A7C7C5DC2B7376863E15794AF51FEC8CA2FD9DAAE4D55 + FDAA7EF2CFD66D95AF54FB5A7B29B65B4B3A87687BD4B1DAD47C4657F6A3B729 + FACABBFCE42D6DEA726D7F18B5F58D6346C5367F7745A6B24A76CE8519B4739A + A09FCBC61E85B3A9CF93ACB6F32EFA1D00FFC25A130093EF8A040E00B8FEF26B + FBEFFA05B50600C656ECBA40ADA5D68BDF2FACE6E2CED33DA8D81EF5C21FFBD6 + F27AFB9C8AF26D3E77F9CE80BE68C4266A8B2BCFA7B7DEFD28CDCC9F77084C3B + C1D9FCD2FA7CB3F69428DB9E37DE8E629B7E5AEF04406DE2D8EAAB3AE5057CD5 + 2E0A7BBFC94F543391B2C22107740000A4CE49444154899B268AAB267445ADF5 + 8F02802B7B9C98D679B557DBD806F9A4B84CAAF3BB583A20B89E64A3E67D2B96 + EFF47BAAB1AF1700F76B13EDDDEF83E90060142380B548890B165F8C94058A16 + 306D529F5A759B7A21562FD8A40879E3C2AEB6D70BC14D6191A98566BDE0D442 + 550BD95817DBC47CADD8362E88057CBBCAFB6CD15C16D2FD16D7CD623D805D81 + 5E015B52D9D4264A8E05EA63F4702FD50E07293A32E4F8F0A7D42D95C3233A42 + 6A35DB3CF7530580518C089686C617D2C8E4AC29BF07D8A37DC74E99DDE4D1BF + 67D3B6C902812756D2F0C4521A9D58342DB8B65BBF51B36D1F5F71303C3C6B63 + CED9F16716D2E0941D03DBF4497F1F3051BF430E80C9E76809B473FE1987C008 + 479A3BD000BF445EE03433DBCE8567D38E85EE880A77969988BED865E3C40809 + F23BAD0D10D8DF01BCC218B98D4719D3C7046C4638E7B4159F84C34F4EC4E84C + CC0EC51773E46E713646D04B4A39DA55A77A0900CC3B80E500DD63DA0DFC2D8A + 91C1EE38A58D95DD595ACAD4E168456A47BE06BE089B9CB6B18D3B884D388801 + C0127538827D0B48933B8F83B0B9D319E733C0B6BC239874ECD235DF0E9A6DA1 + D131DE7BF866765A1FF477075FF6A8268F72BA78C5EADE4B47DECE51BF6C8779 + D8FA12E9EBEF442CA922803BB68EA35D0018C9E60E792287AF64100C044072DC + 1F070097D4B7F4B43AD444EC16D02001218802034C0850489491A02F796F8B8D + B102108E1038BF23B8B3FD29A9835F22EC4A64306AB68566DBD54F73E4B06C39 + 62F8AE6F0F3DFD19EF0EBE93A66FDF4E539FDAB14DBE553450E76EDE521A80E3 + DB4ADF7BD00575B4E5F422D1C3E51DC16C114D54307900518E0C063C910281C9 + 7F9956D85EB6C0DE3600ACB26C0030601A6500F01C10D9EC0269D401D0806940 + 35876496F7779C32166DACAD52402D768E2D4817C7501B8D474A9D20A7C33AFA + 15514F0A5474085A60A9EA188FBE8295BCE71700CC3B63E3D6CF6C23DC6C4D5D + 00701C93BC8F6163710E8CABF3D2B9C5E3D336D6236CF443C0D0BCB5746E0FF8 + 4594694B0AEC8DC7A61D36D2F89DE8BC0486D9FE1A782AF87BEECF965AFEF4F7 + 3FA693DFD967FED39FD3F3FFF44FE9D9BFFED521A7403C5B3F3B0CFEE31FD28B + FFF84FE9E5FFEC5F785B87C4E13C743C815EBE336D07AE73C0A66D989BBC1DCB + F5837D1F3FDA795BCAF92C13ED6AE90AF013286A796F63C7F1DF19E09708608F + E8250AD88E53CE95BC7E9F927E53FA7DC9A63CBF5F87BA96F23BD16F6ACE7EDF + 330FECFFEAEB2FECB8F98105FAE8FFA029DB6F570F5CE8A10D45E5027B058105 + 7B1130585018B88B4EF2594DB4533D76C620BA97546D29037BE7ED5AC003214B + 36E69CD978EFB883E0724D401EF50BF8B5EB4E733D32BBB684F6685FAE5D01F8 + 6AFB67CA93762D440055C06F37FCCDD745AE8F7AD84679B79BF4D00ED0966B2A + A9767570F04B5B93AEF344FE220160EA68C7355EEF8727AF6B3FF701016069FC + FAAD34FFF1E769F1937BFE6E78402FF717C48346EC4081E256CF0E7E4D005FEE + 6944FBB21306227F9487A3EC5EC83D2F46001F3101835D8060AB8BA0D7237CB9 + 6F12156CF74CD9B8FF028A1B386CF7646DFDECEFF9350902BBCD2498ABFB3B79 + CD0FC86B7B67CD13D4166167EEA17984C06E5B59730C52D509FC22C6F179CC79 + 9BE798B4BD73DCE63942608FF035017D6314F09EF2409DE659CCD376B2338CCD + DB047C6B000CE8459AD7F93CCFFAEC29F33F9F03DADC10C5B2035D9B376A4719 + 20709EBFDA5C9387014B3B815DCD5763AA7C3F45001C816F84BC02BBEB05C011 + FCC6A8DE18092CBB2400BC756CB14BAC07047B15E51BC12FE9E0890C7201BAF1 + 1DBF4AEBF58FD4068025D64EBDD65AD1A6F6B1EDAA755D81BE35FC55BE5E43D6 + 523D8A6B50AD49DBEA6AA9ADD6CE5AF7B2BEE681EBB8561E30017D49EB6860F2 + B4F1B5337D589F17B81B21B0602FD27ABFF60368DD5FD737ED02E0757F431FDF + 44AD7E755284B56B8D55B76DE06FF1AFD4F2B14CEBF1F3E0E391A24F090900D7 + F6DA2FE56D03E4AD7D5C11FEB6F9C4D612ED359ECA3DF5B0C0378ABED1AF58F9 + 1B5DF826A36A7B4B3BA2767DBCBA8D24BF6CACABF3416D7E5324FFAAD4D6E6D7 + 52F3195DEBF3914BB1BDFA2890AA1EA36E1BD5AF5DE37FAFEC51EB39C62FADC8 + 08560906A3CFD1CA691EA322675B2F6F539F27596DE7FD28F2F17E07C00FA51A + 00B77E015DE24BEDEC5F0EA16FFB4336F5FF096C01BDD13EFBC68DBDB5A1C713 + 0C7C378FFEFD58BFFF0D44003717F71E00F8512EFE757F179FAF7C2FC05FA27E + B13520D8D22D3619C1BE6BCFB1F4C187F7D27FFC3FFF2FE9C35B0FD25626127C + CFE1A6EA409527D7D85E9928DB81D537FF780396ADDF6441EDD63B99686BA3BE + AA533E03E0DCA6EDBCDCBE1ED958C8276C71C25826721BEC183D2784D8635D5D + 2E8AE34A7172AB7CDD5E75F5186A4F7ECBE881B4A54CB0B76C7FDA6413F65593 + F2CEA47DC330B037CB27DD651B1FD9681F27FDFD1600A8579DECF11CB045003C + B0EB48F30E602D4CB468E9B528F1C54BC92B8D8B1DA95E78A95D97BD5AB0495A + D0D50BBBDA1EE12F6A03C0B5587CC6052C36B56721ABC56C6C1FD5ABAE86BFD1 + E600D8345047FC16B148DFC63B9C6CB10FB445D101A032A9C06EAC47722890C6 + 76D15E2B3A20E4B4A845BBCD8CC7317054986A4788CA4A8701B7C7172D9FB783 + AEB781F6770107F05B03E0981F1E23F277C601F010EFF21A5F4E832756D2E0D8 + 49CB9F4EDB2632001E9A3C65ED880E5E4AA3E38B69BB8D313A2608BC9C764C2C + A51D530B6964D6C69C9300C2F95DBFEEB4320D239C5980DF45B66D7E3E8BFCE2 + 0577A00164DDA9B6FCACB71BA13D70D7F2728AC981E675A69D05EC4A11F4E608 + 601C74D6DFDAAA4EF508875E1B0096141D2C08EC4EC4D325D2A402BD2A47E0AB + BCCA120ECB03CFBC96F65DC860372A3A49911CA782BE8DE3D4F272B036CE554B + E5B495E355CED8A82E072DCE5E6C94E9576C8A20523D6984C0D429DAC8813011 + C2268FF02D5B53E2A096535BDB586A5B4BDFBEB244341DB5BCDE77788CADA081 + BC56D696D0D8D9225A51C2D8145D2C004C1EA7FB6180F3A5AB6EC3413F71ED43 + 7F5F30C0E0D0253B2610F81AEF78C4E99FC1421B4C40C007A5DA129AEDA23D32 + B7805B80AFE0AEDA35F0D7E4FD4A1B410D0160F5279DE37D9805F2027E4901BC + 8B9FDF773BEF0B460D80296D1D04DFCD208708DFE9CFEFA6A9DB9FA5A94F81C1 + 36E6E7F71CF40270280BF86213DC910D28BC4C3421EF1046440597E86101DF0C + A6725E1185B2ABCC56B388FC7AE4EDCB7B4B05D780B5F340B5029515612CB828 + 2047FB58D7403A137D1DE606F0467BCA828112F5EA4B5991AC8A6205B22A8A77 + E5FB0C95D94AD823497FB43E96022F8196089889C89FF26D86819C80D4B20DB1 + 9D83C02729E57C8C0E14551B449DE06286B2396A16918F63024D0576E9431BF2 + D8BB8E675219694CEA048C1DBEFAB81D08ECE91FFEE85015D84A4A04F0853FFF + C5BF57BE5FBE37DEF9FBAC8DF18CB505C42E7CF9553AF3839D87F53FF3931DC3 + C6D6B9E4EFBB03B6B1916FCEAF805F7DD70DCCE5EFF69D7DC7DFDBF917084C1E + 0902ABFE247F376B9FDF476DBFBBEFED77C777CA6736C53C9F81548097B4F96D + 051B65B799F8BD227F88C0441B3D1441AA31484FDBB99CB7DF04BFFDF8AE6E22 + 7315959B017086C002B74880179117D015E4D556CED43540D7AE0D1EDD5BEC40 + DF18D1AB8742C8C7EB86CA33D8EC9A23718D02FE0281FDDDBF9667AB67040856 + 043010D8DFFB5B2280B5F5B3E02F2930986B22D746AE9182B35C33B986725DD6 + 8E0DD453D6B59A6B2EF90EFCE59AAEED9FF3353DDB8808CE3B4074DA956BBFDD + 274EF0BE79D3D8356B6F9AF8E0569ABD69D761D304B6B2C384E0AF1E38924D70 + 1800CCAB0D8E00797978E99DAB0E7D7DEB67D3C18B977C470C0160BFFF952860 + A4BCEE971100CB4EF9E957ECBE0B042EF017D8ABFB37F7E406000374CDAEC85E + DDF705766543E4A57ADE401FCD37A268E7730F9303602BBBC8175117E73802BF + F5FB7CA30480F751B6F953F370DD199B3FD1C704FCD53C4BBBAAF0905E7C688F + 3C4078B7A5FB4EDBFC8C1D5E80B4CBCFA41DCC0FA90B733E54CF09775A5B7FE0 + 907925A0D7C47C55734EECFE4021B612ED1BA3791DE8DA1C167B5B34B0EA9B79 + AE8D890480057181BD02BE520D80631D7D19C3EB285B0AEC150426955D11C06D + 00380A9BDEF72BE81BE5409808DC4A716D53AB57FBF80EE0B89E8AEB33D9C9CB + 1ED75AB17D5347DB0271230466BD28457B5C53D6525D03738BA2AD06BFBCF777 + E35E5BEFF2FE5F2BB701E07AAD0DD415EC251A581018C5BC0070DB9A5EEB7DF2 + B11E795DF121F803E5C5B64A3B3B7E078972AD585FB7A9EBA4D806F5F293C472 + 54D37EA41D02AB9D7C38D13F241F8FD4B1659F529400EF86A1BDD69F00836E7B + 94FC5A52F48DD500F851A4FEF2B345F5B237EAE5038C92FF4FFE4493FC8B2EF9 + 1B2BDF6353AEED41CD7875DB287CA0A42DFD6B453F699BA2CFF54951F4F3F6F5 + 8DB7A8CD9FDE0B00CB566BAD36993BE0F3CF7EFF56052EF06B29B283D5D2E730 + 159EF3F3B8CE3AB55E702A36F724ABEDBCA3D6DBD6DBF406C0917BAE8F7516B5 + 825FA91AF4B72687BF1BB6653AEE70377C81ADA24DFB0F3CDA7EF17F80BFA7E2 + 0FF051D4366651BFEF89BAA8EE7A2BFF2702809B36EBD45AFDF57981BF8AFAE5 + 7DC09BB7D90DDCEC4336397BF9F52BE9DFFC97FF4DBAFCC1ED3438641332BECB + 70531554E546BB9E1BBF2608516DEDD0C3B48D91BDE455D6B9A1CE3894F3F9D7 + EA6E574D80ACAE9E28F904AB4C149BC91C759AD44971C2A776FDCAA6382E2993 + 5934B0FD69579CDCAABFDAD5935ECAB5BC9FFDADE9B769C8C61ECDD05613F738 + 69D7443EDA4925B55BAF384EAD581FC7A42E0260A48554B3E0309B1638F5A2A4 + 6B6152DAF59216678AF855BFD877A32DD0EA459B6CF582AFB6D5F285E29ECE22 + B479BA38D8229C45B26B01ABC5ACEAEAF692FA455BBD308EA2AD6F4B6D8B68C0 + EC56002E0B7216E6B6681F3AC4423F2FFE070ADC1D2C0BFBB870D7421D9BEA3C + A5FDE10ED8950341F6DA4980B00DD93151ECEBED7B39252C5F3B4128D700D801 + 6F5104BF82BFC3275647FC4610CCD6CFAA07006F1F9F49A3E3730E80B75ADDC0 + D88A6BEBF8C934E810F84CEA00E0E5347AC234B69C466C8CD1C9C5B225B469DA + 6CB336F6EC82C3DFD185D369FB82A216886200C03E93B6CFE3207B3EED5E7E21 + 6B89F7AA3D67B667D32E8F04EE405CA0F1D04C7660456798DA90CFE5DCAF71C0 + 1527DDEEE50B69EFC91C014C596D945759CE3C77E855F0170900FBD67EC599A8 + F7C449350CC611A988949817FC959D08601C996CF91C1DA3729AFA3688CF7603 + 5F9549E5A0052233864419FBD3D4953EB49563373A6E9B63D1DEEA24B688F636 + 4538951555E4D0D7EA0FA1973BEF14F488E06A0BCAC3D870689B8E5FBCD25171 + 64E3DC76C7B6E581C080DD43172FA783C5E94DBDB6883EF1EE079E76DE139C25 + 10ECD1560500531E7FEF7A9AB89201303AFEDEFB0D008E2080BCE0826C020CC0 + 05459C61A73C0FC8F8F84E72D07BEBF62A00415BFA60577FDAFA38D82C057648 + 8C210185890A9E07A6DCBEEB9AB33C117553B73E75090E4F03603E66AB55B687 + BE9716EF3E708833071C0622DFB63605E420815EA51E090C5C06FE02904D0020 + A280F56E61D73D1B9BE85FC0EF0320D17D7FC73091C10E83498B9D94A861A287 + A5535F7FDD4413B7D59DFEE69B74F26BA094D501830308069C01D204CD1C9CFD + 9061A1801D36DF4ED7DA0ADE6107B8115D0C4806E661F348CB329EC01D79C13E + 8D271B12C803489EFF1300F50F5D0058603247039BFD073B474B2FFC1508FB93 + E797BFB7E3706E3FFE9896BFFB2E2D7D0B94CDE3FBB14DD40BF00A800ADE928F + 6507A226F22A9312254D0A34A5BD229681B8DD3037B77750C9672DE3A90F2900 + 987716EB3DBAA47C7674F6274B7F34FBF73FA4C5AFECEFF2CDB76E030A9FB3B6 + E77E320190FFFCC774E18F96BAFD27D7853FFED9FE863F589FAFADFD8FFEB754 + E4AEBE77CE53E0D73FB389EF580098F73E0B34F3775B220AD6C4F9E83C04827D + 5C13E7A8BF397F63E525FEC6F16F1F013375AA47E40576A9F7F3B3B6D87C4B73 + FB5DAF7C9BE133EF0FA60FD1EEC823DAF99D5B9BBCCDF317F63FC6FFA0FD9F16 + 801B237E17ECFF8B873B16F8DFE4E18C2F7968C2CE39C05FFA4CDBFF350F7DE8 + 7F9BFF65AE077AB84330173BF0973AAE05F499617B79AB5BE05A61D78FE6FA60 + 6D78C0243E9442590FA0F8C3279FDD4FB3769D9AE6BA5880AF228101BEBC43D7 + DFA36B790160802FF077FCFA2DCF67204C39C35FEDCC20003C51AEAFDAB5816B + AC3F58F3E1277E8DF56BAFD975DD060013FD3BFE011038F78D0FF950D6F59E54 + F0F7B8DD2F2487C0D7ED3AFE815DDF4BA46F84BDBECB84B67D2EA0D7DFF1CB83 + 48FE905207002B02D81F522202D8EE71399A37435E41DF087E6527D57D527976 + D2408701C3768FF5C85F40AFDD87B95F7701E0728FD6FDBF86B811EC7AFF32AF + 405D7384625724706CA37989B7A54FB151569DE63802BB40DE18E9AB7992E647 + C05C7F4D46B14700EC90D8EC8DADCCBD5C25CA57F3B5DD276D8E66F3C15D3617 + 647799BDD67E0FF33A9BEFED00047B546F7E1090799ECFF50AF865EEE7001870 + 5BE68DCC3335D7D4BC94B49E5F6217809568A77AF5ADDBE47973B6ED286581DE + 6D82B30102F703C21ACBF305FE2260AFA281058105856B005C0B3B00B881BD21 + 0298D4B784EEB19E91F27AA613152C451BED6A00CCD6CE82C05A57090023D653 + CD5A2FACBD90DB4CACCD58A7B1CE93EA351C8A36DAC4F562EC1BED48D0B70DFC + 4A80DF088159DFFA43CD659DABB5B3D6B988B5B70030C03742DF28B56B5BCFD7 + EBFD1A04CB27D0570F0980FBD5F56A53FB45A2AFA481BBA17DAD8701C0F2E5AC + F6F360CF60B706BCDA59AE17006EEA4DB58F4BFEAF36C576EAD726EAA30F0E51 + 7E54F5ED8FEF2EFA134D2A373EC7CAEFD8656FAB2F5A355E9BDAC6A86D96CA8F + 2A3FA9849F34961F55D1EF1AD5D6F661D4EDE36DF78BF752F497D77DEBBA5E8A + 7D5AFBE1EF875344C02B0EA03AD40F10FF3DA4F34070802EDE92EB9D29F4E535 + 1DAD55BF2EADC1951AD52CEA4954DB79473D54DBC02F23CBB47CCD3E9BBAB5D4 + 0A7EA56AD0DF9A6204F0A6CD23F6C3E44B2E2038AAF94228EBCB5EC71FE4773D + 94E205046D1AD8E15ADD960B5101C0CDC5A85D0E775BEC51DEA6EDE2F738555F + E82D7D981BCABAA49B4765AF6F466D0290F2DEDFAD3669D9B1F7681A9F3F930E + 9F58485BB7D924C6BE67BD4B976861DEEBCB4D3602D8475504B7A82EB78E5F6E + ECDE96F7129B8DF711EB9DC4D8B1C5494BF777215B1E271E57AA275175DECB02 + B8967AB99EE059BA6A32A93E6D629CD09E096904C05B771CCC51BC459AD0D693 + DF7A8C6827027864EFB134C882C3BEAF8D3699D6B63BFE642563993CDF3501CF + D1BE9AC86B628FBD56DD46E5688B7535786E6BCB1643CD4284FA96854EBD2851 + BDC0B114FB46F56AE3E5B250D3820EF8BBC1FE1E6D10384A4F0993575BB76981 + 5A1697F9E9E22C2D48E3C2330ABB2F544D11E292AFA53AD5B3E865214C3E2E8E + 11E56D4F4FA591A37369D8C4E21E08DC44F2B2902F0B7FCA6E0F0B7D2DDCB550 + 278F4D756A27478016FB6B497DDCC1D0E2848800584E0D393F708AF452769CD0 + 6E218D38845D76E84B1470B40BF246E83BC216CE962287C20E8B67D3F6B199B4 + 636CCEB7831E9C584E5B2757D2D689936970F2B4E96CDA3671266DF37703AF58 + DBE5347A7C298D9C602BE893261B6FCAC69D3E9DB7785EB4B6B327D3F0CCC9B4 + 73F66CDA33733EED9A3C97769AF6CC5C487BE69F4F7B965E4ABB4D3B179F7718 + 3CBAF06CDACE36CF38D39673A4AE9C5D725EB913CC1D65E7AC8C832C3BD0E43C + D3FBD770BCE1945364C66E77EA9DB5636568AC7AF52572788F958938A68F9C77 + 72FA09FC464720B65D96F2AEB89DD60F35EF8EB354F0578E483931E5BC949352 + 0E4EB79FC9EFB1933314E81BDF8127472929B6A79F7BA36973E885373DCFB688 + B439FCE25BE9E8CBEFA483CF5FF4765E6FE99E676CECE7AC3D8EE01732149663 + D79DB49617E415F05524913B8EAD9FC3DDB2C5A4836013B097ED2729D35E6D28 + AB9F472601737156BF72291DB3F271EBE3DB56027D83B33B0A707CE455B6B8CC + F097E85FA0EFC18B190823C15F39D91D0817A77CAC6B22844B84160E7C2282A7 + 3EB8E9C2B91FED51D403181469B6F2F98374E19B1FD2A97B5F366077F27A06BD + 020B02C05284C04406932AD20DC0A1486060B06F1F5D00B0BF1FD84149DE369A + 3AB599324D7C64F68F3E7630A3883C8D455FA0307017B8A3283FA0CEBC6F1B7D + 2F2DDFFBC2A38B8138A4FEEE61CABE5D34ED4D773F4F0BF7EF37F056EF09D636 + D18A14967D01585CF228025FDA5306F89EF9F6DB741A4856EA49FD38251AD861 + 3051C1A6D3DF7D9BCEFEF0BDE78165B4C1063CC3D6BC37151857FA6003BAF9B6 + BC26413E450A0BE43500F6871C554C8A5DED017BA4EA73EA7B80B089F3B1B600 + 49002FEFFA3DE5DB09DB39589A0125309AF3C8A090BE44BF2EB345EF37B4FFC1 + DA6738C871510340819F96726C9D9BCE8F36028E2AC7BCC640E41983BEBC87F7 + C25FFEE4EDB051AF31C97BFF723C00B0B6ADD63B7401AA488095F38FE0D5EBBF + FFD101F09902B7CFFDE1A7F40CC0987EA60B7FFC9343E0D3F61D9EB13EC061CA + D4D1BF89D6E57C4C0060BE1FBEFFC56FBF70F87BF60F440EFFD0F5593987E5AF + EDB35A7F9DA3035F2B736EE417ECF7067CE7EFA6BE8CADC875FDD6CE7C6F6394 + DF578C508F11EEA4EAA3DFE18AFDAE390F6DCFCCE7E3FDBB8AB85D36DB1C7057 + 65E0ADFDF697BEE0F7CFFFC7570E5C518EC437FB17D6E6BED9EF7E61FF9B0F5C + E4B12D3FC875AA671B66441BD9782F6FD6038FD48D30983CD785AE87443EBEDD + 5C4FA406FEDAB5853C36BF7ED8F808E0CB032ADAE6194DF9C32C79AB67E02ECA + 51BE791B6845008F717DB5143B79A0AF7649206DCA5C2BB90E5ACA7596FC34D7 + 5DBB46F3708FAEE594C71DEA66009CA380793027EF00317EED46D347BB43A0B1 + 2B37AC8C2DE779DF3CF91CF94B5D8EFA55E4AFB67AF67B8ADD63B8DF0079FD1E + 54C43D49F0F7F05B97BD3D0F25F18A040030F74F87B7767FE43E4844F0D83B36 + 96E589EE05F0EA2128EE89BA5F623FF0F25B6E775BB9E772BFE51EEFF767BB77 + 3A142E0098FB3375DCB7B9AF02823507604E20A01BE7051265D949058CC90BEC + C636BCE71791D77CC31F5A634E636D35BF5124AFBFF7D7CA02BE35F845CC9B6A + D84BCABC8B79981EC273017D0BFC45C05E9FCF318F639E662991C00830ACC85B + 5E3BB2D7E677FB5668DBE99701B0CD0F6DEEC8FC5100183167643E19E7A09A9B + 6AFE4A1F146DB18DA4728CE205DA7A746E91CF8D4D40DE5AEA53ABA9AF1441AF + CA02BC5284BE4851C0C3D5FB7F0581C96F3DD659D3B4415EC49A46F52A47384C + CAF6D083966A1D37626D474F2C7441E00C7EC35ACA52D65D3D55ADE5046DA32D + 4AEB39AD1581B7A4AAD3DA50E3D4D05711BE52AC272F08AC75ADD6BC02C0A859 + F316C5B5754C57E78F35D2DA5EEB7B147D04B55F40B63645DF43AD55BE084BA3 + 7F4365F940623E96DBA4368D465643DEAEF6B13EB4DB68C24FE30FEA5B2AA9DC + F1EBAC06C0520D7AEBBA06000388B7757C6BF26FC95716EB621BF9C9BA7C6E45 + 6AD356B74AF8F0FAF9F1FA88E3789EFEB53FB19423806D52E58356F5432D6DD5 + 6E555B548FABFEC5371A7DA851B55FB6D54FDB470FDBFEE7A8F681E3EBDE6CDF + BF828250F40F2BDF4FEAD7D63F967BC95FA5E8DF6FB017FFBD03D5227CED1BD6 + 39662FC573EA8CBB9A13B4C9DBB630932CAB1310AE194DCDC71E8597F5EAB3DE + B168F7A4ABEDBCA3FAB595ADA9A35D80B7A830CC9A7DAE6AD74BADE057AA06FD + AD2902E0E67DBF6D6ABE10CA9D3F00DB1F6FDA64E9C6F20792429BDFF570AA21 + 705B1B07C0BA0095A7507AA9B9E0F528CBD676F17B1CEA75E1FE3917F48711C7 + 8937BA28EC51DC148904DE449DDD9C1063007FB70C66C09A6FA8D8B3EA9BEDC3 + 8849402D015895BD9DABE48BCD6FA0F6B7DBC436147E1EFA3CA19FE72D65B265 + 65D720E5EE63C47EB5BA2647A638896A2655D5240F69E2D956A7C963CF7A53EC + CFC454D037C2DE08816BC531BCBD4D9A379A0ED9C2F3F98BEFA799532FA5C15D + 477C72C1F7B195C9BA8D0714DEC0E7A24FD7041C758359F235FC45B15EED9557 + 390A1BFDEAB6B10D0B0C2D367CC1818DB67D1640AA8F696D8FE5B60557575DB5 + 9803E84600CC42B059D085B6B5F2A2B103743B8BCBCE53C87A12B9573B294612 + 6B512BF82BE01BEB58047B846F81C002C00E7F0F4CA6C34BCFA673EF7C98165E + BE94B68F2DA64D4FD9B994853EF98183B6483F6C8B77E06F59E4D78B7616E1AA + D3229E7C3FB5B5D3B89243E0C641D12E3932E4F890707CB03534A9841DD8DB44 + FA962860E02F521D8077F0F84223203000D805CC75180CC89D4FDB4FCCA45100 + B047002FA5ADF6FFB675E2946BDBC4E9B46DDCF2C716D39089C8DF51EBBF7DCC + 64F691A90C698767CFA66D3367D220CE2BA2297088CD9E4BBB67CEA7BD33CF9A + 9E4FBB279E49BBA72EA47D4B2FA5BD00E085E7D3C8FC736968EEBCE96C1A01CA + E2242B8EB4D1591C5D38BC3A8EB1E82CCBE50C843907CAF413E815E4DDBE68E3 + E1DCB3FAE8B8533B45FE760160ABF7F7BB15E79F3B0E4FE668601C81380CF376 + 82D91929104C5ED057CE49392A7160524F590ECDC6B9792EBFCBCE235A0A048E + DB22AA0E9BE02F7582C5AA3FF1DAE5F4FC9DAFD3A51FFE219DBA7EC721B0DA1D + 783E47FFE2008E8E5C8483567502C4D8A343D89DC7AFE477012B9A085BE3642E + 4E65C15D399D493DC2C9040096631A8735EF31F4682740B095911CD60E9283C3 + DBDB99047E8910CE5B40BFEF51C180DEDAE9DEE489B852945601C1805E9CFE02 + 0138F815D925E1E86F00F0F5FCBE49C0ED4B3FFD39BDF58FFF9CCE7EF14D80BE + 194800790527042B90A2CCBC0E205CA0309003A0EBA0F7D6ED346176815E0161 + CA8A848B11C28876DED66C4403131DAC6DA20135BE2D7491000FB6FCDEE07B69 + E9EE035753FEFC7E5A020ADFFFA20022C0F11DCBDFED02BD11FE0AEAAA2CF01B + EB24EC8C851D00ECD0F7DE3D2F2F937F70BF81BD40368F2E3601D81051C8B401 + BCE5F71103DA328CA3AC2D74C9330E120076000BD83451264A13A8A82D9E23F4 + 258F00840E452DA5FF12C00ED0F80DE090B605F85A9FFC2E59F2448ADAE7F12D + 86B38DFCF2B7D401233B201538B960E7CEF9E8F83AAEE0ADECF13C386F3F1F93 + FA28AF73E633C5F7D93A8C250A982DA07F023C17D05BC6A55E795207CE7FC891 + BF2BDF7F6FF53F36DB2B939EFD0900CC6720B2F63B4F819D80DD4EB4EF0F26EB + EB3035D7290A580018F04B1D02FC7AC46C11E7C279FA6736E9BBF173339DFCBE + F3BD0077D95E9ABF8FA02FA9FFAD80BF56376F02FEEA6F4EDE1F1A288097F304 + FEF2FB231500A60EF1BBE2F7A8B280B044DB153B0F002F51D17E7C3B9F18D1AB + E8DD93C0EA2FADFC80FF0BFE77EC6F6A02FA2245EF2EDAFF23A057B077F1DE97 + 0E7481B9A402C282BDD869437BD24E1DF0385F0B047D7D5BE7929F010CDB35C0 + 1F3C29D03747F9027CED1A607684DD2130A0D91F52C95B3B63E3FA03FC9DB0EB + 9640AF207084C1D4216D01AD6DA0B9D6E97A27F0ABEBAA1EA4A15ED756AECF51 + 5CCB3304BE994E5CFD201D7DCFAEFF57DEF76B373AFA5EBEFEEBDACF359E6B3D + 36EE0D02BE02C182C1D4C5FB07797FC8E8528EF8D5FD87A8DFA3DCA3DEBA6CF7 + 29BB7F957BD2A18BEFA6836F5C72DB312B1FB77B570D75293BD0C566F742EE83 + 0DD42DF74DBF3FBE66F756BB974638AC7651DC9FBD6F11F778EEE9BAD7EBFEAE + FB3FF770B5D31C41D25C813C30379601BB88B2A0B07622919D74F7D9177DAE82 + 9893686EA3798EABCC8D9833499A4721015ECDA9542F084C3ED799EC78AEB2E5 + B3E671CCDF98C7C9A6399DEC9AEB35B6323FAC612F73C828D925CD597BD93597 + ED9ED36609F436752D5234AF00712D015D1EECD4B6CD11FED611C035EC6DB3A9 + 1C013022F27718306B12D05559C057D2DA47001809F84608DCAC814C7A7036AE + 9928EBA1DA0878DB6C6E47656D56AFE3DAD673B5D4A606B86DE52EC5F5EDBE13 + 0D10A68E14F82B00ACF52D6BE00880B526AED7D95A57C7F576775D6F00ACB57F + 2F1F405D1F25DF43ADE88BE8A5E8C7588F62DFE853E927EF4B7E24C0E1E2EF91 + BF6693EF26478ACF28FB6F3A0038835F820D047A6B653F13F97D45B5BD0381BB + 7C5B41F8C7541715DBD475F295A9AECD07F78B2840E046F253A2BA4E0A6DF03F + 22D5B96F54F575FBAAAF0B3FAEEAE85FC6F2317AF878F18BD6B6275B7C86B555 + FBCEFBA9AD5F57790DC0DAD6FFB1699D70773DAA5949B7AC8E203C984A4F56D3 + ADF5B4EBCF7E8A22676BE36EB2FD16B4DE738E9FAFB54F64958567C67250CD42 + 6B356D5BC1AFD4D2F1B7A47501607D112ECA9D2F5E00B8FB8F60AAFF48BF6BDD + D23F7FFF0BC06F0B00FF2217F99FA178F311F8CD1034DF2CFDA9239EECE1E26E + ED01A81E156B375326040D1C6D991C3CAC3496F26D001869B2427E6068BFC3E8 + FC741493169BB8D9B97BFD56CB531EB4FAAD36A91BC8DAB0B54C6CF81C1CC3DA + B9C2B1DAA4899114275B4DD9BE9F2E7BDB646F9D6A9B80FA938D61F2EA134713 + 51C119DA5A7B6CA55FDD3F4E7CA7969F4BD73EF92A9D7BF9DD347AC0164B3C4D + 697DB7D8E47D60BB2D3A466CB23FDC99D46B822FB89B27F640DE4E7D2D41DBDC + B603717B291EA7AD8EB401C0D6AE6B51D2222D8AD426B6555E0BA0BA5EB6286C + 718BE8B838539EBA81DDB6100CF5B1AEA96F168347D3C69D39D5E2B206BF5A8C + AA1C253B00584F376B81CBC2362A2E785526D5E29874EB531369F0C0643A76F2 + 85F4C2B5DBE9D4C56B69D7C4725E5C1FB045F7D31369E3FE13AE8183B6780F0B + F8B8D0D7825EB6D846B67E8AC0B74DBE15747156F4931C22727EB4293B4C3ADB + 3D932AFA5700D86DC717D2D663663791D736D01E091C00F0E8D87CDA7162DED3 + 6DE3D6677C310DD877280D8EAFA4A113CB69788CE8DF95B473F254DA3975266D + 675BE8B153698828E1B90B6960F185B479FE79D7D6E597D2E0E28B6970FAB934 + 3CF95CDA39F362DA3FF7527A6AF6A5B47FD6F20B2FA57D56BF6BE1D93432F74C + DA063C9EB5F1706ACDE5EDEEB4E51D4E301C59A49EC7D9D52239CDE4608B0E39 + BD4F4D90588057F5A482C2AA6B549C81110023ECD98158002F0E4913E5FD67EC + 739AF6E1BC0C76CAA4CA77B52B4ED12E47A889328AF58A9C11FC255514CDF81B + 57D2C5AFFF943EFD97FF657AE1B36F1C0013194C3BF2074C72E07A344E88D0A9 + 9DC2EEE025C5418C0DC76E29CBB12C273372206C6AA28BAC4E9018E73452BE01 + BC4060450207F88B289F78F3AA4744C52D31E52C278F7DCCEA4FBC932383E336 + D18061B689D63B8571C2D7D1C082BC8AE8CA65D2DC06619B7CFF866F3F4A1BE0 + 02E0F7C51FFF9496EEDC6BA27D811282BF40066083030A6005208363B0DDA8E5 + 81BC80E4083400BDDEBF8C25002CD84B19002CBBB68616242645D31500F6E8DE + CFEFA559DEFFF9195BB5DE6B227EF3D6ADF70AE4C9D0873251C14B053E018067 + 3FFBCC216D9B047B558EDB466307EC6A9B67F2D8198F3C51C0006040B12280E9 + 430A7803A8017B5104C04037405DAC4782BFE423A443B4570ADCA32FEF58D558 + 0EF3BEC9403582D4A87C0E19546678499A016106800F2CCF784484DA679600AD + D697ADA71D46F39901800053CEC5E1ED9776CC0C7A05660194404EC0AC802776 + C15EC150EA11FDB0E9DC691F01307DC933AEDA63573F415F6C1A9777F4F2CE5C + A26601C1805F8FCEFD9E685FB67FFED13E87F5F9FABBB4F2958D659F4B00189D + E7FCADAF03CFAFEC737DCDF79F23816927518F80A6E8E437767CEC01E066E8DE + 81E1FCDD107F0744BD43578E6579FDAD1C2C5B59009888EBB37F2800D9FE2EFC + EDF9FB44D0ABDF1B79815DFDDEF49BF1BFA9E5F94DEA3798DB2AB2370BF0ABF7 + F262E75C5CF65DF09D682B66C02CA940B01EC2C80F6D983D44FA027A79E72EB0 + 5700D8E1AFFDFF2A7A5800193106D1BE2A6BDBE7D9720DD0FF7FBC26442DDCB1 + 6B002018E87BDBFA7DD2794005700BD475B86BD72BC15D016045FF6A2B6845FE + 522F20DC890CFEB401C06C035D03605D534989E4D5353C3FC49341EEF8951B69 + CA21EE07E9E8E52BE9C87B760FB9F281D739ACB5EB7AEC03CC15DC5514B05245 + FFFA434425BA570F18D5F228DFF280127974DCECC781C06F5E4E87DEB0FBA289 + ED9F8F5CCCBB62F87D8C07A1EC1EA877FAEABDBED42BD257B057E0B72B3AB8DC + 6F752F6E54EEC99200B0A0AF14EB1D04BF68F7E917CCF6DC1BABA4F98747EC16 + 51D61CC3E71966D356CF9A5FF82B2A6C9EC23C86394B3DD7F13AE642CC9302D8 + 9554475E0FD2459B1EC0234FFF7DCC97CE9900C165FE56035FCDEFF25C2ED7A9 + 2DF5BCA797A8DFDD260160E68BCC2335F7D49C537350CD43EB7294ECF51C56F5 + 9435DF6DDA51660E8BCDE473E4628B75CA03773D4A78B21B000BECA2B500F0E8 + A4D60A79E7203D44DAD95D286FF78C047B7B0160C15E4F4DAC7724CA6DEB9DE6 + C1D7027491D64AA4B2B5C1DE3639007EDAF265EDB69E359EDA688DA8FCA6FD25 + B2B7AC51B52E453C9CACDDAA9422B5130046E437EC39E6D218F4F176651D1CD7 + C9BDD6D6B11C77E68AD037AA06BC5AE747C5FA5A6D7E03243F453FD5FE8AF5A8 + ADAFFB414CF85C7AF96EB0FB36D1A58DFC36B14FCCE7327EA1FDAE08732507BA + A1BC6934AB81BDA1AE17008E7995A3FAD545B5F9D06ADB63133E3BF90CE5E38C + 52DDA3D4F7AB435B7675F2D666E3E0DEB469DBBEB419DFAA09BFA8B7790C3EDE + 5F53F870A3BFBB9FDA7CE5BDD4D6AFABFC6B02E0C7A89A95742BD7395329BC26 + F29B87D56ACED34791B3B57137D97E0B5AEF39C7CFD7DAA7E295E297AD2C33AB + 66A252D3A615FC4A2D1D7F4BFAB900B8A7EA3FD2EF5A97D67F4178C200709F8B + FDAA1B02E9637C42477A941B8ADA4708ACFE44016FB61B28DB3E3BFCDD621305 + BE2B9B1068BBE5C73D41E066DD0B00FB79DA39E476D606B0BBD52674DB6C124A + BAD9CED56C9B076C8209F01DB0098C09180C088E924D7078F5B13A60BA6B2265 + 62A2542BDABBDA97895E3DC95C735259C15C9FB8DAA4CC8F41DF523F648B8A61 + 5B7C6C92ADEAD7754C26BE3681DE776C3E2D9C7B2D1D9E3A9506771F35DB019B + 50DB776602FE6E61C2CE04DFEC71724F04709E98E7A738F3B6D1B94DBD0888D2 + 22A09F68C718E4796254529DD79B58A03C0A00AEEDCA6B81D5AB4DB4D5F0B796 + 206F5B1D52BDB7F105A12D42771131CCE2F158037F0759ECB258ADEC71411AB5 + C9DAAC0580510D7D5914037DE3421ADBCE134BE9E0E285B46FF64C1A3E3C638B + F3B1B4619F2D9AF7DB189647E4375B5A835ECA523FBB141D03521BF445AA270A + B8ED5DC051726AE8C9769C24BD24E82BD5F05700B896BFF3B73872D0D0D8621A + 39B190B65B1DE9A09507C696D280D5090013113C3CB69CB69F28621B687F1FF0 + 19AB3F97B64C3F9306975E4C03CB2FA72D2BAFA62D272FA6CD2BAFA781C557D3 + D0FCAB6974F695B463E695B46FFEF57468E18DF4B4953D1A78F6D9B463FE7C1A + 9E3F9BB6CDB16DB4FDED8AA36BC76C06C0EECC32E108D336761102CB26C759DE + 061AA8DB1D91110130FD0479E58823C5267B8CF4C001882350E05792035170B7 + 0DF452DE4D448A29D62B15FC25BF13A76481B8384881B62ACB592A084C19076B + 04C038504971DC2EBC77339DBB792F4DBE79AD71AECA31EB0E5CD2173B5B3722 + 3969B19147E4057E25BDD78FBC00F02A088C13B9D822308EF582C482C0126509 + 87F8D85B793BCC136F5DCDF544475D2CD1C00504C7C860A2826B00ACADA31529 + DCBC43F8124E7F4030D0A0800387C1A4C002DAD8F8EF5E490061A2C5B4C5E8D4 + F55B69018871E356DE5214C00B903051E6FDC20087F1128DD6449B017FAD8D60 + 86C36193438E52160C06842000F0E29D7B9E52A77A45DF090C0363B0F916AD26 + 40CC0C5BB67EFA599346C043398A364063416089F68B9FDF4B8B77B575AC8D51 + 202E91C1DA269A7C14F5B403EE0274057801BF02C6F4533BD5D1DEFB7CDDD9E2 + 19C08604E080BCE48173D801B948004E100F38275847242A63522615D0D318F4 + 950D715C45806A3C2450992130E79701611EEB2BEBF38D1DEB3B17207899C8DE + 721C1D9F63F97B5C398FEF4A74EBD7F6B9BFB17102C005720AC43611AB56A64D + 04C06A5703DD588E505BEDD5863A4434ACBF6FB76ABF64E70EB006FA9EF2F7F6 + 62FB3603EC6FBE0FF097EF83CFFDBD03DE33440DDB77E4EF772EF0F794B53DF3 + DD8FE9ECF73F799A9501305054EFB205940ADC723C8160CE4BD1D2FA9B64105F + 8071F8FBF83999C8A34EF46D7E1080BF2D7F13C6505E4017E9F723C9CEDF331F + CFCECDF2FA4D22DA29DA579F45654F4D44FD12E1AB685F9573C47DF7BB77056F + A33C82BFD4A99D002F6524C04B4A1DA9003060D8A37EEDFF9EFF713D102208EC + 2AD78905AE19B7EF64006C228FE63EBDED10D8AF3501E0E66B5707FA9207F60A + 120381D55665207004C05C0FF50E60D22EE86BD75045FA3AD02D0FE9F8033676 + CD05E48ED9B57EF2CA8769DCAEDB5CBF8FF3BE776B2B404C1F95C977E02F3B3F + D838DC3FECBE3376F97A9ABC6AC7B2B17870480018A87B82FB07B2BCA27CA508 + 7F4F141DB3F2612280D145BBF7B183050F3485685FBF6F724F2D90370261D980 + C1E850B97F22DD6F759FF6FB2D2965BBFF4AC05DD9FC012FEEE5E44DFE305899 + 53304F00006BCE50C35FB5A32CBBA47A44F9C84B6FFBBC4476C63CF68A7D2EEB + C7DC442098B90DE0566A03C0B247004CAAA8E0D8D61FB02B91BFCCD7047BEB39 + 9C435E13B61AFE92EE2A734149801691D73C52362902DDD8AE9F623BCD77C933 + 9E80AE40AF60AFC0ADEA22002665DB67007084BA887ECA4B11F6229581BD71FD + 403E2B6FF7AC28DE7AABE7B8B5B3346465D639318A170179B5CB91E0AFD2B805 + 7404C028AEA31AD01BF25D0AEBB2F568C0D69BAC31A3588322F25A8BC6B5AA54 + F78BEB56C1DF1A00338EFA6ADC286CACB56BE01BCB82BF28DB7E19002C7F442D + EC927C11B5ED6125FF097E9EE88789FE16F2D10F23C5B2DAC63EB277DA52D781 + BEBD007004BD9BB7675F93DBD9B92EB44535008EF9087957F9C0D650F49D49BD + EC8F438DBF70A080D8A8E8476CAB977AB569B1CBB7D9F86CD5C604FC1D183990 + B6DADF6EC00404AE7DA2BF3575CE9FCF9B25BF779B1F3CDAD692FAC47E5DE527 + 18003FCC71DB7849479DFA9ADD3C8ADA59CF3AD4C6DD228F7BD2D5EF9CD7FA4C + B19E2DA07BB24B53ACEBD5A6488CF47700DCF5C5608B5F7850EB1FE4773DACD6 + 773178C200708B565DE0057E75DCAAFDCFD5AAE3F55380D0B407FC6EB1090952 + 24F0804D7C1060989BA8602F7096085C7F428CEFF231026024F02AB99D73B2F3 + DCBC85947301F612056C93CBA1A7AC6CE7B2C13ED7263B97CD36B11AD89B360F + 1F70A8B9619B4D181DF85ABE81C0B90CFCDD687DEB7340F11C9AC994C9216C51 + B43B24AF6CCD44AF4C30A3BA2682B1DC02727D526B1334C62712D8DFDB3BF254 + DAF5F444DACDBB5977D8A43EB497BA8EC9180E6FF3E43C6FE98CAD940B04D664 + 7E2D00BC6507F6BA4D7719695180DA6CB2AB5F5B1D6903808BB498695B08A9DC + AF1D792DB0627DB4D56D6BC5BE947B0160EA11F54401E72DA4562F3A23EC1500 + 6E5BA0222D4A0580EB856DAF05AFA4885F41E0A6DEF21BEDFCD8428BC5F4A67D + C76D317DC46D0E71ED37E711C12681DC08782501DEBA5E7D545FAB0DFEA258EF + 5B419BA2B3220A874654E3DCB03C4E90EE742E0D1C9E6DB40AFE923F9E237E71 + D244002C9B347C7C3E8D5A9F21D3566B3330C636D0C0DF2587BFBECD9B8D3572 + 74210D1F9E4BC34788165E49235367D3E0F4B369CBDC8B6960E9B5B469E5F5B4 + F1DCA534F8CA4769E8F55B69F885EB69C73357D3CE53EFA45DF317D3DEB98BE9 + A9B9D7D253332FA73DD3CFA5EDD3E7D3E8CCD9343C77260DCF9B4A3A6AE98E19 + 949D587272C9F92587181218DE6E7DB2930C3BCEB36E471BF09771D557A097ED + A6D506A79BEC5D5BFD2D9E77E1088C0E44770CBA2DC35DD5298244CEC58E3D83 + DF088281BF0D0066CCD3D6E66C7E1FF00E2B0385057C114E5241619CA9729A52 + 27276C6C4B1D6DA953540EFD283F5D1CC0387D15058C80C15D913B2FE6A85F81 + 5F015D897A9CCF8AEE95033A026095117907C09607022B12588A3018B1753480 + D76565DA286ACAE16FB11DB114F88B80BE31FA37825F1CFA0D00A6FE128E7F60 + 6F67CB67E0EFC435F2C0842B19025FBEECC061E6C6C7FECE49DA09068F5DBBD1 + D95A94883480458114E365CCC9AB1900D31E90E10018105CCA92400710C4DFEF + 8B1DB8FBF11D8F18A62C582CD00B7041E4BB00F0AD1C3D4CDFC95B9F3411C168 + 0EE0DBD4DFF6ADA3FD18764CEA0040DECE522010EFF85CF83C43A51920D0DD0C + 7C273FF9C453006E86B9F97DA2A4825502548B444096886140AF00307D11E302 + 8389143EF5D5D75E0F5C03EE01D504D8B001E1E6AC1EF02609CC09E891CA1EE1 + ABDA49D461575FD92300A65E6D168962B6CF92015FA71FE3007CCFFE40842B70 + 9063D2AFFBBC325026BA997A4BCDE6EFB62D128C4511E20ADA0A8022B521A52F + 79DA91C6361A5BE321F23A9EEA199FA858F5A76EC5CEB789CAFDEE7B572EDB79 + 15F0BBFCA5E5BF00F076226E1DC0027523F8FDE68774FAEBEF3D6DE06FB19DFC + 26F711342505FE3663990480E7BEC851B8F16F455BBD6FD78F6BC75B7A609FE3 + BEB5E1FC80C866CF00987286C702B9E4196BC97E1FFCF6E26F55F28713C89763 + 22EF63E7C2EF0A2D7D99A37DE7EE65C8AFCFC2DF7BDEFACFDBFFC2929D93A02C + 3096EF08F17FE260F636FF0B1D084CAAF6883CFD14394CDBC94FECFFD8FAA93D + E571FEEFCB58B1AF8F45342F5B3A17F0ABEB02D784C99B764DB995DF433EC7C3 + 239F705DB8DDC05F07C0B76FBB8D5D10163FBD97A63FFC344D5EFFD8AE7D5CBF + 7818A5805DB36373BBE5BD5D01C282BEE35C538BB8C629FA17008CB8A6027E9B + 6DF4CBF5BA96E0EEF8BB76DDB56BFD183B315CBEEA76AEDF88BEB411F8F52D9C + E973E5469A70017B3FF0078AB84770CF187FCFCEC3EE235D60B700E0087C1D06 + 23DA20AB43C05F22808F5C7C371D7AFD1DD7E137B86FE51D2F747F44BA2792E7 + 5E291BD27DD6EFA1561638E6BEAA7BB2FA08F64608AC32EDF6E9212FEAB8BF97 + 7983E61735FC459A63D00651969D3ED8E218C05F5E4B0100567BE0EFC4C5ABDE + 8FF98CDAEB7518C0DB7E605779D5C7D76FC43ADEDD1B616EFD305F04C0718E47 + 998706B1EDC66673C1B85B8CE68CA8EB41432B33C7446D0098720D88DBA4FA38 + 16A9402FA9E02E796DC72C085C4BEFFE8DF037025F54DB14E12BE0AB7583EC5A + 53D48057E0B75F59F0570058EB1DD64111004B0D00AEA4BE711D55B7E986BFAB + D76EFDE46BBE008023F8ADCB82BE516AA7725CCB7A3900E07A0B6854AF85B5DE + ADD7D7B1AC35B694DBAC1F00CBFE4B01E0E8A358AF1A1F8AE501C0405A49FE16 + F95C646FB3918FED551F6DB98C5FA71B004B11E86E18DE6BF97DAEB608E0BA7D + 976FCB14E1EFE306C0BDEA7E96F0ABCA5F38D001B4EE67258D7EC452DF26C6F0 + 7150ACABFACBAF293FAE7C9C0E81A9B3FE44FF0281B7E007C4DF487DE8FB5B53 + E33F5EC337DEAFAE97DAC66CF2C1BFDE4B6DFD1F875AC7ACCEE7618EBB8A97E8 + 9DBFAE505FD84C04BA519B06081EEBB675339D9FA136F626DB6F41FDCE79ADCF + 14EBFB01E0688F8A6D82C448FFD301C026FFF07DBF18EAE217FEBB7E1D3DD900 + 988BAB402AEABAD83EC2F174C196D66AD3562F35F53D00F0EAEDA0B970D386BA + 22CB7B44F06386BF2882D7C6CEB13897ADC8EC3639D9B4CD268444F15A798B4D + F8466DC2BFE7E0541ADA75C4B744DEE2A093E8569BC8587D06C136916B20B089 + 08E170EC7EEA9A50D56AAB6F01B9ABCAEB117F0F9B80224DD298E0F2F980BF7B + 59A8B1802893DE7E6A26C68C69F94D234FD964FE601AB0850662620F88252258 + 937881D90877E3445FF6A8B68580F2D42B1F257BDD4F6A03C0F56248D1C171C1 + 13DBC4B6A45A60C5F25A8B2E14FB49B2B749FD048007EDB7EA91BE2C4EF774A0 + 2E0B4F00306967C1B97A4B2AA445691B00D6E216918FD1C1B19E0537103882E0 + CD6CA745946F79929A6DBEF64C9F4ABB27571CBE0E1CB2F666ABC12E6201AEBC + 006F6C176DB5A2B32082DF28D5090023A281E5A09013438ACE0B543B43B23A00 + B886BF94DD06D42D4E1B396B6A3900B676C3472D6F630C986D8BD93C0AB800E0 + 6D63BC6B78318D1C5F72503C6202008F4E3F9386665F4A9BE65F499B96DF4C1B + CF5E49836FDE4BFB3EFDC774F0FEBF4E4FDDFC6B1A7EF54E1A387D35ED3A7525 + 1D5C792F1D5C7ADBF446DA3BFF621A9D399F46A6CFA691D9D336CEA9343463E2 + 9DBF966E9F3E9DB64F919AAC6E74068717CE2D3B2EF5C551E64E319C5CC54956 + 3BCEDC3E6736A27F81C456B76BE1BC6FE707DC8DCE36F238F2B04745C7A160AE + 1C87D9D601C0AA8F0058650160B517FC551430F05751BFA41100CBA9AABCCAB4 + A30D79815D39686BF84B2A07ED7E8413D7CA7B0BFC150CAE01B0DB01C3D4D116 + 4771B1B90339389C49A3135A4E69415FD948B525B424077704C1A45E67C2510E + F4D5969844461D7FEBAA8361DE87E8EDADDEB7D32CC0F830EDCB96D01EED1B22 + 81C9AB2C309C65F9F7AE79E4183A7199F73B5E719BA2781DEC06008C1DF8004C + 505499DA094A0850380C2E75D2F8FBD63F08D0012006F80A8478D42FD0B8883A + 015C87C5A58C72045E8EA4731B90E56606C0C8DF116C521F208D4459EFF9CC5B + BD6600CC3B8289120414CDDFC1466AED8902FCECB3B44044EF03A06E864F12A0 + 09F82400454422F61CFD982321CFF0FE582B03ACE6EFDD4F6FFCF37F9E5EFBC7 + FFCC8131507809D0FA200333FA9EFAFA9B74FA9BEFEC9839EA119BB6CC45DAF6 + 96D421D9975F39889BF9FCAE8339CA88F305D261638BE2B33F10219C01210218 + 12514AAA318078A494C93710F72BFA10590C1C044E12890C780630674809003E + F56D1EB3819436B6C0A4C6731BEF46667BECD2A681A0E5FC3C92B43A0FCAB405 + 8292C6739638763C3EB6786CEA88DCE59DB5F38C6F29E0B501BEE51DBFCB9607 + FC46002CE99C493D4F5BFB3B9FB276C0DE535F599F2FCC0630B6FCC92FADFF03 + 3B0FAB3FFD6DE76FC0F9C4CF4EDE3F97D767082BE8AAF3573B1FE38BAFEDF761 + BF117EBB0FF2DF94EF03519FDBB25D7506B927CBB9F25B94386F9D7FFECDDAF7 + 59D5C9A6DF1EBF57FE3EDAF219E9FC16AC5EFF1BFA9F88BF5FF26CD30E94CD91 + F7F7BBB66B471E951BFE27C9F37FA9FF6FF599E1A10E93DA92D23FC3DE0C7C11 + 6DB82620D9C63FBC95B7A92FD791E95B3C8462D70B9347FD4A1F7F9A16B85E7C + 6CC7FFD0C6B8F1898BFCCC4776ED31090A9352472A511EBBF6513A6AD75D749C + E85BAE83C0DE28B3017E79D006E99AAA6B32D755AEDDC0DDAC1CC53BE691BFD7 + AC6DF73B7F2300D66B01E85F0360DD3FF23D832D9EB9A7D87D06A00BD8751B3B + 4FD8FDC9ECC05E45FB027C953691BFD2EBDCEF78676FF77B7B753FD4FD1491D7 + 7D913CF750A4FB2636EEB37A40EB40E953BFE357009894FBBAEEEF82C29A3B38 + FC7DB6037C6B45005CCF3B344FD19C04F07BFCD5771B00AC39096098BCE638B4 + 8FD1BD92E06E1BE055FD761EE65BB6F956E9B3DBB487F6D8344FE381BE0A00AF + 9ECB75E02F73C69D8BE7D35E1B8307027D2E5860AFE68D9A334A35E4251F85AD + 6B6EBA8E36CC6F351E3017E02B00AC7204C0B14E1200A6CEE7D526815E015E14 + 41B0A0AFEC3500D6BA226EEF2CC04BAAB58AECCAD70078E4680704B3DE01F476 + D6397AE035DBB526AA616FCC6B1D854D11BF5A87D56A5BC7D57502C05A776ACD + 89EAB5A8606F0D7E91D6B6A8B115002C088C5417D7C0F53AB86D6D1DD7D7ABD5 + 01BAD2E302C0F221445F82EC28FA22A2EFA156F491F4ABAB01B0FC2EB5EF05B5 + D963FB589FF319FCD6B057C1031D30BC2F017FA3048223FC8D6338180E3EB2E8 + DF6A7C5C55DD7A14C7ACFBB7D5AD4BF2034AB56F9014FFE17A7D8D95F009A2B6 + BA2E55C72090A689000ED099B1146CE2E5D0E7B7A80CBC3BFE71F9C1A3FAD5F5 + 12ED57F9F6A50AB8B649C77CD8E3AEA59E63FE4C005C3393AC5057D88CC06E0D + 7BA362BB56B5C2CD3E6A6B2FDB6F41BDCE397E9EB6FA556D28177E298619D3B5 + 145867C3445BC1AF14E0E96F51350046F14B5825FF82E317FEBB7E1D3DD90098 + 1B0320556ABB51ACF7028C74C18EEAD5AECD1ED505778B8D3CB62D76C3DF5CD7 + F3FDD88DD4235CB9A172D1373181E882B48F41ABC06F250030E7E54FA66DB3C9 + 1E1077C3501ADD7930BDF3FE67E9FEB7FF9096CEBD9606466DA26B93189F8C9A + 36100D3C040466526613447F47B09501D9366EFEBCBDC539754DA4623E4AF638 + 018C62B287DAEA6A85B67152CBE48C63900EEF3A9246586410CD5B26AA6B69D3 + F053FEBEDF4DFE9D30A9B614F83A9227F0C05FDF125AF632B18FE5B800C876DA + 77FAC47AA9BB7D47D116DB45B9CDA4C54BB380298B212D881C00A3AABE6D3125 + 3B69DD56F9A8FA78B249D1CE18311AB8B66F05ACB21066E15A16962C3A598C46 + 69211A17A692FAF502C0282E949116D2AA27CF625BA27E93B5DBC476CFBCEF77 + EFF1F4D4DCD974F6EDEB69FEA577DCB9C0F6CF1E051C60AF16DCD1D64B8D33A0 + 52ED3488E0B79F14092C87859C17D181D138324C5D8E104F3B5B3ED791BF0D18 + A6EE787E0F7004BE516E3BBA90868ED81805000F1C5F4C035697A37FADDDD842 + 1AB5FCAE8995B46B7C398D5AFDF089953434713E6D9D79296D5EBC9886CE5F49 + 7BDFBC9BA6BEFCB7E9FC7FF17F35FD4F69FA87FF433A70FD0F69C72BB7D353CF + DD4CB3AF7D9E665FBE950E2CBF9176CC3C9776CC5D4844ECE298DA36B992B64D + 99A64FA691C99369C778D6769C59B35940E08E00C3DDB0574EB256CD9F4DDB8B + 736EA7A54060844D6DBC0EFB62DEE24F7984F3508057CE42390CE316CF4A6BD8 + 4B1951C6292900EC5B1416008C1354F0B796EAE454C571AA3A779C16C9B90AF0 + A55D140E5A529CB4E8E967DFC8513FC509ECE0D65241604506FB16D101FC7A3B + 93CA28025E8D234773A70DF5D9798D735AC017C9E1ED4EEBE2D45604B01CE244 + F86ACB67BD1B91770303819B88606BC3B6D1C72F5EF17607ADDD41A2AC7837F0 + DB57D24177DCE3B46F03BFD7DD060C00F846017FA7DE2732F88683076D174ADE + 23C70A74A0AC7747C688E20881739BFCEE4960844713BFFF613A56C006DB4693 + C62D4F012044FDCAD640E0027889060610C7C83945093760D8214E86BF131F59 + 7B00CF0DEA013C79BBD61835AC48E2F90081059032842A1018DD255AB2037E15 + 6128F80BE4220A1191179CA56EF1DE17E9E497DFA4F9CFEF7BB4E2F91F7E4A77 + FF87FF317DFADFFE77E9991FFF90DB585B001BFDE8AFB28E45AAE3C732ED5C05 + C8090023F2C05FECA4C041878B964A003B4021A9E0615D2F28D981918CC956D4 + 76DE5FDCB3FEF97DC08A3E5E78409BDC5763C4F1391FF282A0406E528E2110AA + 732245029E1A4BE7A4F1554F0A1C06F2528EF51A4FC7A0BCC8F12DF5A8DBAF79 + B7AE1D8F28DDB8E57381BF4A01ADCB5FD8985FDAF90274AD3DA014B8C9DF9948 + 5FC0EFE23DFBBE4C805F52DE5D4BCA38440BEB9CE3F9EB7C759E7CA739E29677 + 3D97BF93D5518F16EC98B300D21201BC6865FDCDA9CF9FBB03910572395FC1DD + 087FF94D45F18E6C7E5FF1F7E6BFC1720CFD3D0582393EEDE2EF95DFB7A42D97 + 81B5FE0E6E4B11FF6F11E60AF09207E622FD6FD2C623791DF2E694B68C472AE0 + 4B742ED1BFD4038DB92610F5EB63DDB1FF5BFBDFD775C5AF2337EDBA639AFCC8 + AE2B450E85ED7F966B113057D1BD405F81DEF1F7EDBA55E0AFA280230CA63D00 + F8C8BBEFA7635C3379DF6E01C07ECDB3E393F2504C04C07A9FEF44D95981E8DD + 08713DCA97EBFCBB44F7A26BCD75570098EBB85F93AD9FA27801BADC1710D1BF + F17E91CB9D77FB12E54B7B4160CA71BBE736004C04F05164F7A50C80F33D2F3E + F4848DFB2711BCFE9A04EE93DC07B947729FE57E5BEEB9B4E59EAA7B32A9FAAA + 6D04C07EAFAF843D3E24A6F982E60CD1465A03604976CD4728633F54B692A6CC + 3C467319E57D2C2B2BF2B72DCA372ADA81BF6D0038CFCBF27C8D14D8AB795B0D + 80554F1AA37FD9FA19F8AB1D619432378CF3CB66EE686A03C15182BA6D0F2EAA + 5E6D54DF8C45B99220702FF88BEA08E00880057411B037026095058AD506690D + 3132B698868E776F014D1A01AED62E48EFFEAD017003814BFB281FA702BE8AEA + 252FC57A6F73B83F00466DEB3A54AF3191F20E6F6DCD491AEDD1A636B2916A7D + DBD495B5670D80A9637DBC61D7D1D675F25A00B82E47A02B3D4E001C7D05D177 + 80A2BFA19F6A3F492FF502C0F2DFF453ED9FE9AEC39FD301B711DE9207FE6600 + 8C6D9FAB0D00F71AA306C0A8F673AD55DFA6BA8FD4AF6E4DC98F27DF200ABEBD + 06DE161F22FEC62E9FA214FB07F503C0AAEB55DFA8E5384D1FB3D5FED0DF927E + 09004CDB557EF41AF8FE4A0078BD5AEF71C54BDAB9899DBFDB29AF06927D216F + 2FF5849B7DD4D63E8EF3A42B9E733C77D9637DADAE3694DB816EE697A1AE974A + DB8687B6825F2980D3DFA2D602C075F9F708E027454F36008E1777DD24E205B7 + 2EAF25B58F6A6BD74BB15FBC7149BA59E5ED9C0B0CB6C989B7B3EFA6795A8CEF + 6963BEF00384790FF0E380C0F926CDB1DBEB39B69F43F3B7DB95B60E3D9D86B7 + 1F4E1B360CA73DFB8FA77FF897FF3EFDCFFFF7FF355DB9F9200DD9441DD0CB64 + 14B0B8A100D4CE44CECA0100B749305AE2F3764D96C8F79326807DA473EA3EB7 + A2007F01BCA48DBD9CC3263B2707C29627EAB91E67F5D8FBD2C0E8E1B46D972D + 98866D41319461EDC6115B2CB09DF6882D04EC3BDD3C6A75C366AF443DDA3C62 + 6DB6E7054386B7FD01702CAB8DEAA2D42EB697D613015C4B755A48B5F527A5AC + BCD25A6DC78C6D95D75882BDF5224E0078AB2D50E3E2332E36B12B12585B41AB + 3E826114DF011C17B65A1C4B7111DE2C9A83D48E0860B677669BE70D7B8FA77D + 33A7D3A98BD7D2D47317DD49001876C7C0C1A906EAB6815F2DC4EBBAC629D0E2 + 38505EF61AF422D5D56DDA1C1802BF7288D4CE90AC0C7E6B090CA381A31D0902 + E3B0894FEF63DF7604000C085E74F8BBF5C4521AB4360E7FC7E7D2F0D85C1A3D + 319B761C9F4DBBACDF0EB6963E46FDB9B4658E77FE5E4ABB5EBF9D16BFFEB7E9 + ADFFF0FF4CD7FF1FFFDFF4E6FFF4FF4E73FFFC7F49B33FFDFB34FEE9BF48C72F + 7D932E7CFC0FE9F495AFD3DEC5D7D290F5DD3E9BE1EBD0D44A1A9CB4F1783F99 + E579BFF0EEF15369E7388EA895343CBD9C466688FC8D00B82322843D4A780E75 + B68796DC19B790E56037AA38EBE4B0A32C9B3BF402005624B024C7628E32E980 + 6041DFD5EA3824C9EF3FF3627A8A285D206E89824144C40076E51CA52C1BF538 + 4B231096D355FDE594C5991B2373E4A0553BF2FB0BE495E318182CF04B9E7A87 + C1CFD106F00BE06DD7811701B897D2D32FBD930EBC60F917DF49872C4FFAB4D5 + 1D7EF95D2B5FB272DEFA598EED2E077725C161E51B277915E94B1E088C8EF2DE + E037AFFA3B83BD9DD96883F39EEDA189E8C2A94F19C7BE203029658FEABAF49E + 835F5204009E644BE8F7AE7BFDE1B7ADBE0004F247DFB1B61EFD4B441A51BD37 + 1D02031B14212C002C08A13AB6949EBC9EC1AD6F717AED46DEE2D4F20E7FDFFF + 30038F023EB03970296026421A4009ED50036E4A34B0A725CA8FC8BE4E7D7E0F + 2722AFF61102C7770967000C38BA63E96769F173A055670B5941278162F2AA6B + DAB0F5ACE519933602599CDBF96F7F48B7FFEBFF903EFAF7FF9503602019602D + 0236F28AB874E0F6F9BD063003D2A8574465046E8271B377EFA5293B77401DD0 + 5036D225E06C91C688E5F97B1918AB3D90CFC123EF2F2E51BE799B6704C825FA + 1318C8B1D9B23AF7893013102908AB3A953967F282B2EA474A5B1D5F7D116322 + DA20F2D4338EA27FC9AB9E7C6CE32A6310F9AB28608D0BA405000B023BF42D10 + 98A85B00EFC92FADFECB6E00BC626D14FDBB70F78BB474DF8E6D6D00BF33B7ED + B75220B083573B077D4E1D577F3F3EA300305B32D39EE360A33DFD80C8CB765E + 82CC1C87737038ABDF8FB5653CB6215FB8CB8306D91EFFF6FE0041F9FDF13BD2 + EF4D0F1A90F250422CF31B645C9D2FC7545DECEFBF4913EDFDB76BFF038BF61D + 006B056CF5BFA1E8DECEFFA07D5FE5FF4D36DA92575BFEBFE88B5DD1C2B99EFF + 65FE17A5CFBA22801718DFEC5C07B84E4CDFEC44FD027D81BF131FDA75EE2374 + CBAE57767D03C416A82B000CFC05E89222DA905227404C4A1BC15FE0B047FC72 + 3DE4FAA77C91203000D82130D754FA5E06DAF2504F96AEE5C72F716D661BFFBC + 0D34D760AEDD47EC9AEDB610350CC8CDD1BD5CFB6D1CDE3DFF767E07FDD8DBEF + FBB6CFB4112C8E00D8EF1BA6718E4FB9C05F04F495DCC6D6D200E0D7BAEF6FF1 + A127C15DF20E84ADACC8DF287FE0AAC05D815F415FC1DDA8F8C0176581DF087B + 7D6E606A03C09A4F38B435A9AD247B1476F5D3DC85390DA9E62C6CFF0CF8250A + 588A3058705791C0CA037E655399F9981EC2D35C2DCEBF489180EE6E9FBF31C7 + 03FCDA9CD1B4CBF23B2D45E4F7AC3C93F69DB2714F12496CF3C5F9D365CEC98E + 34365F9D3BD3CC2B359F8C4057305736015EE5A5265AB7D4C7317A01E07E7238 + 6CE2214BDF227AA223C15C815D52D5C9A67694A3B48600F8B226110096DA0070 + BD95333B210D1DE97EE05500587D9B71C23AA99F048625767E1AACD65F92D670 + CD3ACF14D79C12403782DE66DD59D6A011F8B6959597B4D6659DA935270F2EB3 + 7B95FAD0A6D743D2ACA7E33A3C4A6BEE66ADED6BF10ED0957E0900DC4BF237F4 + 937C1C525B1B140170ED7B898A6DA24F468A6D2947D85BC3DB68CBF67D7DD469 + AFFE0E7F792FF02ADF56BB4DF6181D5CD7AB4D632FBEB7D6BA4711E345DF20E5 + 788CE0D37B586D1CB4F333F5DD02BA45DEA7B6D77D4BB9CD3FFAA44B3E6501E0 + 5EFEF0BA6E2DA95FF4A37B5D04BEE41F0200376344AD638CBF87C44A3ABE773B + A7A66CE72E5BE03375B46F0D2BD70D865701CE1E6A83A1B2FD1614CF399EBBEC + B1BEAF3A00D7D503EEAEB24795360D0F6D05BF5269F45B553F005C97B3F892AA + 3FCAEFFA15F464036057DB05BCD87A5EF47B28DE287AF6D5F1EA639A56F52B6D + 56D9F4FDD84DB381C44DD9260D5BCC06FC1D2052D8261E2D37DE8755BC51B7D5 + 234D42780F30E7C476D403DB0EA4C1914356B72B0DEF3A945EBBFC71BAFBDDDF + D2E917DE4C8356DEB0CDCE5713B7E68941529B443AFC350D986D8B4D745A8EBD + 26008EC25ED7155B3341B3E3C749A4CE4DEA35398DF51BA8B3BF4B7D3C403000 + 38F68BD21844416F1AB689FAC891B471D8162480DCD1C30E771D000FD9041E30 + 5C80EF86215B4098560160EB83368D9ABD0B00F75F1CD4F0372E1A54AE1724AA + A35F13E18BB059BD14CB6B2D9CE2C228E6EBB6753BD5C7A773633F6F5FCE6D53 + 01C048F5E4D9021A00AC85A7169F2C2E25169BF5BB80B5188D8B555F949A78BA + 398A85AD16CF71F11D17CFF54258765FBCDB429F285FA27D712EEC9B3D93764D + AE34B6014BE3825F90373A00B41017048E6D509B1301A95E6D047FDDC16112F0 + 95BD01C3382782D3629533A3383F56BF172B43DE1AFAA2662BE8635617544700 + CB3E74946D9D97D3D0F195B4F5C47200C0F369DBD84C1A3E319D468E4EA69143 + E369F7B1B9B4677C256D1FC751F56C1A587C3D6D0200BFF5203DFB4FFF31DDF8 + BFFD7FD2A5FFD7FF2F9DFC8FFF6BDAF7D37F487B3EFB3769F7953FA45D2FDE49 + 136F7F91C65FFE38ED9A7B290D4F9C4B235367D2F0D4A93438B5E8C249353A7D + 2AED9A3E93764F9C4EBBC64EA691F1E53438697540E0660B68A21C3AF91D00E0 + 39B39548E11160B0FDEDFB01E01DC05EF205F846D5F097721C0387A23B0F4FE6 + 08129C8D3B97882CC910584058E057D1BF7240AA2FE017089CC7B8907617C02B + E02B50BBD3EC6C07AD68DFE834C5A1AA681B39683506F5B5D3557D48E9E365EB + 4784EF9E0BAFA6DDE76D7C130058CE6307C3960700EF7BEE4D136986C1405FD2 + 68DBFFBCB579D6F2D6F6E917DE761D78FEAD0C823D9F9DD44433017D8F02754D + 0E818314ED2429C21867B86F87F9C6658F045634B0F2D871DA4FBCF3413A71F1 + 8ABF43780C27FEA5CE7B7F81C04406EB7DC1C05FDE032C473EF0F7C465E0AF9D + DB3B970B080626E4AD424F94E85F200202FE66BDEFE0171D034858DAB53DA995 + FD9DBF660300FB7B811D7AE4A85D6D730AD820AF685F4501CB46342F297DB45D + 2AE016099CF8B6ADB4B3FE083B30873602C3DAFE9968C00C8033FCC59EDFFDF9 + 597E3FB089E840DE133C7BFB6E9AFD14209CDF09BA708708616BEB5B48E72862 + E0D182B5AB356F023001A5963ECF518C80AB06041788F5C21FFE9C9EFBE94F69 + 19B809D4BCFF459AB13EC0340134E01CF08C2DA5273FB6CF5AE0AF605B046D11 + B8915227A0376B65077C26C604FE29025400507604009EB6E3288258D015B00B + E0CDDB117F99CEFDF8437AE59FFE21BDF8B7BF268030EF2206120BB612054C9F + BC6D7406AF00C9398021D1A25F591FCA9677F86AA2BCC0776275F99880E50E30 + 66DC084CC9AB3EB603942A0AD861A995117984FD34366B334FDFD28FBA253B6F + 015F228149D96299EF25835F227FADAF09F08AF8FE566C0CA02C11C0D439E8B5 + B66C094D9B397E1B77EC6F54C030C7207217716CCE97CF3BFDD9E70EDFFDF3DF + E7E180BC9D3863B395B8FE66F9EFF7958FC5981C4F20D76168C9F31BF0316C6C + 7E6B4429AF7C61DFAFD5F35B01EEEAA10249BF2DA58A6C274F5B07C0FEDBBAEB + E73B479EDF8CB5C18E94D77180BFFCF615F9AB072448E3FF492DFE9F48058705 + 8C81B93504661CF2D9DE01C1F3F6BDD3971D02F4F00791FF5C67B826F00EE045 + 22FEED7F1D083C79F3561AFFD0AE270E81D999805D0C3A91BE1265C15FB67606 + F2929FBD69E75B227F81C2887A87C5FE1EE09BCDB5CD1F7C290FC44400EC0FC9 + D875D5A16E81B8A45CA31DEEFA033E5CDBD9C2FFBDE63A0DFCE5A19D436F5D76 + 1BD7F3293BEE3CD1CB766EFE80CF5BBCBBD7AEF56587093D68E4DB3B739DE718 + D60658ACEDA0950702C7885FF2805FE53D2298F26B44FD72AFEB6CEB4CAAFB9F + 6C79F78C1CED2BE0AB7BA1EECDDCAB754F25A5EC0F7015D8ABD401B0A57AF72F + F0F7608FA85FE60902C3887C8C1266EEA0F94494ECB1AECDC67C44DA65F32345 + F50AFA0AFCD6F057D1BE02BFB10DE5519BB3EDB43A765661DEB573C9E67565CE + C63CABEB013E93CFF3FC01BFF336BF3B6D3A63F97319F25A7E74FE74DA61E9EE + E5F30E7F115018002CED983F63734A80ACCD3D67CC56406F2FA02BA81BF348A0 + 96872149D54763B4015E24100CECADA38005805D05020374D792D60931AF3584 + 1E1E4582BD02BD92D62AB11C0130EB1B5F0785D7E15057F757BFADA8AC93D45E + 0FCA36FD431BE9A100B0AD3DA3E23A5492BD86BABE0E2DEB5744BE4DAC5B01BB + DE270060E02FA29E76D4EB21E91A046B2D1DD7D44AA5B83607D646A81B15812E + F0F75705C0C50F817AB5A90170CCF792FC3192ECEA9B7D35DDB057E056B698B6 + 835F298F83D41FF88BF08775054674F9B98A0FAD4056CAB4938F4DEDA39A7E35 + 9C0D754D9B50D7571A4BFEBAA8788CBA4DF423B6483E4A2FC77EB16F6D8FEA75 + 4EA5AF8F5D957F6B8A7E65F9C1A3CF5B8A75EB91FA3DD100B8EAB7EA380F3376 + 602452A76C63CA56D88CC0AFDEF9EB7601CA50DF268DD1A8F4595361FC55B6DF + 82E239C773973DD6F75507E0BA7AC0DD55F6A8D2A6E19FADE0572A8D7EABEA02 + C0D517D028DABC8D7DD1F51FE677FD9D2500FCF8D45CC8DA2E828F43BAE8960B + 6FEB45BF87E2CDA6EB426E5ACF38ABFA8573E8D53FDAC9EB1CE2B930CEC652DF + 4B9DF6ED8A376ACAFE8E6193C0ABEAC8534FAAC8634030F9CD36891AB4C9E7F6 + BDC77D5BE4CD4C0AE94B1FC6AAC6AC559F8F8E83D44F36526C5B866C3269A94F + 90343EE7858D7696C6FAA6CCB89C73E9AF367E8E005AEAC8ABAE4C12BB269E36 + 81535991BF5D13CA204D5AB3982CDBC260F080F5B3093CC017C83B688B022B4B + D99EEB046D370EDB647FD826FB23965A99C9F766B68A6652ED13FDDCB65E1848 + B26B3C95A35898742D722CEF0B98D227C2DFB8B0F1C50C8B8F2201DAF86E635F + 9C50C731685F14C78A63C6B2CE4963EB386D8A7D22284602C0A42C08DB168912 + F681BD2C408FA48D3BADBF2D12D55679EA37DA581B7666F01B17B75A486B914D + 8A5D6D545F2B2ECAD56E0BF01695C574632F0B71E5D5D78FC7184F8548E0A7AC + ED015BD007C08B701E08E84647821C0B02BD0D00C6F1112487859C16C89F7E0F + 65D43831828D364347C94F5A3E977198E8097A6DA7969FA42FF097B18FCF6507 + 4ED92ADA9D37C717D3E831DEEF0B005E322D7A3D29E2FD5E4363D6C7FA6E3B3A + 9346C796D28EC93369FBCC8534BAF0721A5E7E3D0D9D7E3BED7DEDE3B474FF9F + D34BFFF97F4CA7FEE9BF4F07EFFDAB34F8F65769F3731FA5813397D3F0E29BD6 + FED5343AF7A2BF3B78FB948D31792A0D4FDA712797D2F01480F7A43BBBFCFDBF + 56374A1402C2A9551C597274C5EDEE9A48883524671B121C8E12E86D80AFB573 + 28BCF44CDE427AE19C3B0D057E7128B24DA060B19C8A527434AA8FDE0D1C4130 + 3660300E4F393ADD017A36BF176F0F0ED26293B393E898A7CEBF9AF69F7BC5CB + D41D78E6B5F4F485D7BD8EF602BE3E46198BB29CBDE4B137C701FE9677FF12F5 + 1B9DC78A1E525980D8238D8A93DA9DCAA58F447D9718C7D218D124A82BA7B6A2 + A162E49374E0654B5F79BB790730796C942541606D178DE31E473EF24860B379 + 64B009E73ED15D72F4E3A0F748AE12D5D52645983928C0A15F6CBE75B449ED34 + 0E502246FE3A0C2E5168C00BC16007BF57AFA72397AFB928037E2587C180DDEB + 9696770FD357F929AB07EC027901284D1F602F762B63F7BA0F6F7934B0035B20 + 7081BE82C60D04FAF4F30C81AD8E7151BD457413256C76DA2FDEBE9716EFDC37 + 3B5099ADA5AD7D00558251402E601779EA005C083835071C33019A173EBF9B56 + 0081C02BB765C8D5BD35EEED62236AF281E5015D8C77CFF38045224C81C8D37C + A612698A7DCACE9F32A05002486A9BE2791B4F51A4DE87F3FDEC6EF2A8D17B80 + 68A02070390B98E851A95F7F9D9EFFD35FD2737FFCB3034A209F83C9025901B9 + C0446C795CA020E768E76DC77408EAC7CFDB5DEB5DB3806A40688C42A68C047F + 058381A402BF5204BE94E94F1B87BB565EB47156BEFC329DFAC6CAFE5EE34E04 + 71EE6FF92FED5C4AA42FDF0BE7CB77C9F96600FCBD8354079F768E6C630D903D + FDB59D17DB31DBE7213D6D6DCE7EFD9DA72B7C5E3E5B81AC82A9D888EEE5F32B + 9A56E0DB3F9B7D2719307FED6D3406A96070FE7BE5EF9F639322C1581D33E773 + 9DCAF4533BF2D8976853A02CBF4741DA086EF93DF29BF57AFBCDCD9407145CF6 + 3BD66F98EF41BF65EF63D21802B8FADF4002B800DE58271BF5FA9F95F87FC6B6 + C8DF88FF097E63764E7AE043FFE7EC0030F9215B3AE728DEE9529EB969FFB7B7 + 78C8C3523BCEF4475C57EC7A73E3A346F17AA33C228FB89E39B4E5411420AFA5 + DADD400FBE4882C54851C3C861B15D33B976720DE5BAA96B29290FDEF8833C7E + 3D060873DDB5EBF27B00E077D3A1B72E39FC3DF20EBB360080B38EBF7B2DCDD8 + F8330069AEDD6FBEE73B488CF17010D1C0DC2F5E7BD75F2F70E2AD72AF08F791 + 78BF39C22B07CA4E1551F445E405785184BDA4DCF774EF13F0D57D52F7D1087A + B9EFEEB736D8B139E40DF761EA79C08BD73D007005781179015A52D93C2DF309 + E61844E732DF40E4B121E61DCC37D48672AC571FDAA86DB4C9AE3A4F99DF30A7 + 61CE53293E60971FB8CB8A732ECDD37CAE0524B6F91951BEFB18C3CA9ADF6967 + 179FBB31CF0BF2A860A0B0E5EBF6751FD53387D4FCD2E7ABA67AFEA936AA53FB + D88E3A015B1F877C98EF46B02BD576B5AF25002C61F3D7AF4C74C35F45F90E8F + E71D831A95F581761C426B6DF9DC2680AEA06EB30E2A76EAD55FED2487C7A426 + AD77B42622E561DF8183B6FE0A0FD9B22E633D17D76B312F690D57ABA92BEBD1 + B836650D19D7945A57C6B5664C25EA91FA90678DABF5B0DAC507A723EC15F095 + FC78AC910B98DDB8C7D6DB7B8FBA36ECCEB6B84E8F521FB5F1757EF101F4927C + 036D76F921A236ED343B52B9F4278FEF01A85BBF2E6B43B1C917B385E39554F9 + 58AEEDAA2395DF44F69C47DAD65960B75D110A4B1EA05100AFB731C5D79A09DC + C67214B62EBF57E50B6B6D4B9B369F598B3D1E5BA2CDC641AB0B8A7DBC1FB6AD + 3656F1077A7EA0C0D5729CA63F6DF0035A2A3F63F48B467B2DD5C536EAD7D62E + FA346BE998F1D88F578C598F2B5BFFE3E9FC256C6DE7B9694BFD3956FBB66BD5 + 6DFA89F65D7EF0BA4DF1AFF7D39AC78E7C621DE3ADA5B663C5B2F847AD2E3662 + F94D9536F768D74B6AD3B75D2BF3A904F06CCBD7EA02A455BBBAEED7503C9FB5 + D4AF8FD7F5E097A69A7DCABEAA7DAD56F02B5583FED61401F0A6CD2369E316BE + DCF2C544E9CBF0F243FED17ED72FA00280375A8AEAFC23A8B928850BE4E352CF + 8B3CFA9917F77861EF7B1C53D705BFA5ADC65A8FBCCF1AE7DE699BD57593EE71 + 13C72600EA10B4AAC72600AC3CA9DAC4FE3AC65A8AC7D071DAA4B6717CD281A1 + FD5DDB61FB18B401B89ACDB7CEB6D4B76BA64DA9F34921ED4B1D3687C05BCBB9 + 9BCD279BD5C4D227972DF668D344B55B6D0078B5366E4336991FE6094BD25AD8 + CB984CAACBA2A09F047DD702C05AC878DE161D9E2F7DDA227FB5F8898B27165E + 686B5104C15AB868F1A245521C334A0B2C1D2796A5785CCABDFA53EF00D8DA36 + 8BC40A005346D40378B7EEB345A9E559F4C536D40DD8A274B38D1717BB5A10FB + 4296456F5918AB4E0B62490B71A4B65A903763A03246EC136DEAC7827EF0C0A4 + 4B10B897E414687322445127082C0785606EE3F4B07C2C47884B59005875A419 + F0D27ECA6CD91132727CDEEDEA8F7096282AD89D3200609EDA57B43060F818D1 + BF8B69FBF1A5346A223F5CDE098CC88F9C584EA34543E32B69DBE4E934347321 + 0D2DBE9446572EA6DD672FA5FDCF5D49875FFF308DBDF9693AFADAC769DF85AB + 6974F9621A5E78396D07FCCE3F9FB64D9F4D8393A7D2C8547EB7EFF6291B13C7 + D3841D73D2E45B40178716EF1DE33D64961F9D3E9D76CC9EED727AC979852D02 + 6039D46AA79AEA6A9BA43E38E854161456D42F4E45391CE568040CFBBBE28AF3 + 518ACE48441B015FEA544F5991C0BB2C1FA35A7088E24C95635470587572BAD6 + 6D25003070373A780584951255AC68636C720CD356A24CD44FBD4DA44704BD90 + 1DD2723623399FE5904687485FCE305780F7C02B6FA77D2FBCE1DAFFE2C5F4D4 + 4B368E09A8AB77FA46C82BF08B64537B815FFAA91EF996D005029352960D27FE + F8A50F5C00E0ECBCEF4474F9169F05E2B6C15DDA1DBA6863595EF58872143620 + F02411BF251A186091B78CCE005810F898958F5E79BF0125A4DAEAD4A37F0BF4 + 15FC00FCB2853429E509D20F3FF6C8398F9A0B20063974312902183918C6469B + 028B89FC230270E1D3CF3D05126B0CFA00951C2401B60268CAEF11060803A0F2 + 96D20E946E9A424422502C0A6825B8252D10E9F9E96DEBF38903322428A6F6F4 + 1500039C017C057EA380C04481022901C0DA6E9832229FCB390568D29E943260 + 33C3D80769DA8E9DA38DEDD877ED5826603029E077F90BC023403144191700E9 + 80F1BED90A70741079EF0B3FC6847D7F68350006406798A9B1349E2438397717 + 20FD453AF535EFAC056266407DF2ABAF5CD8502C73EE7C16816B7D8E935F7DED + D1C840DF26FAD7F24B0054B6562EE787F45DE9BC1D96DFCFE7CB7909BC1259BB + 6C7FC745B3033E4FDA38274B7A0AA80CE02EDF1911B244779307EE0AD852CFB9 + 48D4A90FFD05789BEFD7C43928E296FAD826D7E5E85CFF0E8B349EDA51F6EFFE + F3BB76FEF6F7B3DF9B7EBFFCFEF87DC6F7EC0AF0F25BD56F5E7DF80DAB1ED11F + 1B0F6520DA23FE67A4FA772FBB7EFBD83C4ABF3CBC21F01BA5873AF8DF05F8EA + 3A901FE2B0F3BA79DBFF67057C89D2CD51BCB7D28489ADEAF540C9E40DC0AE5D + 731A75003029D7ABE6BA52CA02BE8AE0958D7A41E2A3EFBDEFEF034680E01841 + 8C8D6D9BF5F00CE23A2A9BE0EF11AED3765DF7ADFDAF58DD7B805E01E077AD8D + 5DD74DC05FCA87DFBA9C1FFE41EC0AC1833D6FDA3DC274CCCA19DCDA3D01B04B + BE4404EB9E412A1DB1F6FEBEFA0AFC0AFE3286A0AF1E6C8AD0370AF0DBC05F54 + EEA90060A4885E8F0206149B740F26AF7B33103802E0787FD75C00459BCF07CE + BFEA730FCD31340F2165BEC15C857233E700DC02712D4FBD40AFECD8344FC116 + E733CDFCC5CAF5EB3404856B300CE06D03B8D890C02CB6BD8CB774A181B06A17 + FBC916ED02BC12FD9BB961C9D38E34CE290579EB7ED80477C9D7F35287BEA5CE + 45B99AEBFA7C9736A5AE2E0BF0027635A796DD6D966ABCC61640AF40F0C804E5 + 8502822D3D91A56D9F910030920DB581DC6893629B2DB6D6A9DBA9AFD63ECDFA + 06FBC1C9C46B7FB44622BFD9D66882BFACC3B46ED37A4CEBB3D8662DD50038AE + 25B586D4BA92F567DB03CBD11EEB5416006ED6C5661300AE77D08A00983AEF0F + F4B535B7035FD6D8FBADDEE420B8ACDB6B618F920F22FA256A45DF00E5E6E1F0 + 5287FF40FE09F7518C1E4C9BCBF81B761C74FF88C6525B7C31F5EBB22200DE62 + 7D50F481B4E5555ECB96CBF873F2BB7D057723D075A85BC06F2DB55DD5BEC05A + 5245FAD6925F2BE66B5F98EAEB3E6AB75EA99F0BBF5DF1E3C53C690D86DD16EB + 8BBF4F6D9B7AFC8BC1B7283FA2FC91EE0F2CED6AA90EC92FD9D60EC5B66DF668 + ABB5567D9B346E47D8F367CA221FD5DD2F96F5DDC4CFD8CF5FDB517D8C76B5F9 + B0DB44DBBE00F851842F3DFAD3C5267E261FE8A7F8B90564C5411A1EA2F3401B + B7A78D30960DA32EF240E0AE36A55D2F16D3736CD4F4EB033AA5B5EAA3345EEC + 136DBFA6E279FE1CF978815556AAD9675B9B56B5825FA91AF4B7A80D1BB6B936 + F2616BF02BC52FE47700FC04880B45BC6054179247945F94AA8BE42FAEF55CE0 + FBB4D1453C5ECC7B693DF56BA9AD5F9BBAFB744B37F728DDACC9D7377AF5A33E + DA631AF3C05881616C6B8971357E2FD18EF194975DC7637B6C2670C060243BA9 + 9F3FC7A21D6398C8AB1CF3883EC86D71E2191427B368D524B5526E6F13EE6D07 + 0AE0B50586A544F9AE02C043D686B64539EAD7DA55F0D7B7A4665CEAAB0582A0 + 6D54B4AB5DD4AA058FCAD67E831DB706C07101A54554541D011C173E92C6D278 + B11CEDAA533E2A1E33DA637BD5370B3C5BC4B17014008E65E0AECA19F4B250CD + 6DC80FEE63B19A6D5BD8EA6A77F742598B60F271315D2F94B5E88ED2825C6DA3 + 4D79E0AE9EDC6EEB2BB99DC8E1E238D036D09200702FB9B3A0B4C1C1A008609C + 14382C048165930482716ED400584E8F4E7D06C0190277A27ED50EE140C94E94 + BC55B4347078D6253BA07704D06B02F8D600D823844F2CA791F1936918F83B7D + 2E0DCD3D9B8601C04BAFA69D2B6FA4DD67DE49FB9FBD920E3C7735ED7FE672DA + 79FA621A5A7C398DCCBF94B6CFBD9246662FA46DBCEB17E03BB99CB6231C4CE3 + 6C3167E323EA7060E1C82A0E2D9C577254E1C01A1C5F728715D1B96CE32C8797 + ABF413BCAD553BDAA2A8533F52390201B7405EC15F015DC15F52415FDA4AB2A9 + 2DF9087E2541E10C8673544B7CA71DCE5129DAE4309573564ED7988F51BF3875 + 0576E5EC45B40114CBF92BB8DB38800BE8D5B68F51B2A30356662B69BD3F386E + 21ED8E68529CD1EE947E2B1D7829C35B24B81B61AF6C75594E7339C629CB792E + 673B0E76D970B4633F7EF18A4BF5D9916F76A283795FB0C99DF456E6FD8F3500 + 8EC2D60688952A1FDB1021ECEF99BCDAD9B61409E4CAE6516C8093EB396A8E3C + F01728023499FC208362DAD187BCE0B1036660CB47791B68DF163AC0181F175B + 01B81ED15B8E81046AB0D10FE8EB5BC016E0336DFDB09122C03011818CA54860 + 20D3C40DA290ADFD4DB691FEBC81C1A4C02A04DC02564D7CF4B1E705B15013A5 + F819E00CF0F5495A28368E453F013552A018F92CA223B101933B020203260194 + 3912D8EC05F42220B06CC821ACA50E8E4D44162F947640C0BCE5AF1DA7C053F2 + 02C240D70C86ED331408A9D401A5C3D34EF429C7F76302E100EE96AFCFCDCB80 + 426BCF58713C95A9CBE012E09AB735D6F9084C53E69CA91304262F1B91BFB4D7 + 76CD1EBD5B228B1565ECF09AEFA3405F0030C0174510CCF9728ECB768E0E78BF + F8DA5307BF26A27EB1014DB1AD589D3E07FDF82C7CCF94F99C94F9EE69C3F910 + FDCBB9743E77EE27681BA5BEB4F163DCB3EF9B070C8A4DFDD557B65AF3D65EEF + DDF5DFEAE7FC3E327C8D36B63A27EA37FE36F98D0BF6C6DFBC03E28F01AF796B + 76FEE73886C0319097DF3CA97EF7C060FE77E2FF0251F6FCFF318E802F79A9A9 + FBA80062FBBF9AF8D0AE33F63F0FF415FC45B3F67FBB78FBBE6FD50C7C656BE6 + FC7EDE7C6D4013376EA6F11B764D31B11534D72C5D775CE5BAA26B8C227D0580 + B916A108829B6B5D8800968E2AB237C05F24007C94876F4CC0DF4376DD3DACF7 + BCB37DFF65BBD65FB67BC0652BFBFB80D902FA523A78F16D87C0C7AC3FDB341F + 7AFD9D74E48D4BE9C8EB590E7F01C276AF88F78DE661A1F240911E2A42E4BDAD + DD7BB8FF90170096B827C5C8DF2EC85BA4ED9D91606E3F001C81AF1EC2F27ACB + 733FAE23804995D7FD3E02609F1394285FC15B520160EA986FC43989CF3BAC2E + CE5164535936DA74CD694C8AF2551A2170AB8D7C01B66D8AF33945F46AAE0794 + 55BB1AECD6D056E330CFD4D6CC6ADB55CF7CB4A89E57AA0DAAE79F28F6ED921D + 93392FA9E6C202BE9AFB928F65DA4B1EE12BD91C9B39B42030A2BC752C6FF11C + E16F16F6F946C05E015FAD2FA204750584D54E10B76E8754A67EB3AD8F80C0B1 + 9D4361535C1BF1C0AB00B0AFA30E4FA5D1E3AC61E69A3598D66E9481BF5A97F9 + 03B905FCEA01DEB866533EAA59FBB5AD15C31A927C5CAB2A1F6D2AAB2FF958AF + 35316097572A912201DF5AD479FF12ED8B360600BC799FD6DC9D9DBB5893B7A9 + 17008EBE80E823A0DC0BFE92971401EC00D852DAD6EDA39A7E26FC1DD10F82CF + 45E95A92BFA4BD9CC16F0D761DE4567629C2DE3609D8FA4E7345B2655F5607E2 + 463F572CF7F389A96F542FBBA4BED157D700DC2ADFB37E60773704A60E057F22 + 52598ABECADAD6A6D82EAA579B5EFD62FBB554F76DEBDFF10BAB8DCADDF64EFB + F6B12847BF707FD5C7E8AD365F76AD47E9B3A6F0F1377E7E5844E1118DEDF1CB + CF9FEF2F40D9A8557C646306C0518A04EE6A6BF6C8657A8E1DDA347DDCB60E38 + FA3000556D9F44B59D6F54AF76B5DDCB15AB0C5AC53D5BDAB4AA15FC4AD5A0BF + 557914B0BFDFB745F517F23B007E02C48562F585465A75A159A7BC7DCB85F249 + 1617F1CE5355FD6F464D7D8F9B8AC6E8A7BA7D2C47BB94CFAD73B3D60D5B131D + 079DD5CD9E7CAC8BFD625BE525F589F0B7AD5D2D1DB79FF81C9B7962AF7C46CA + 03769C2DF4B7DFCD96AD7BD3FE03936962E64C3AC202D026C644FED27733E743 + F4ADB5454C2095F788DF32A1D4C45091C0CD44324C4C3509ADD55617FBE44973 + 07006F18B445C4A02D104C9B866C020F081E46561FE06F9C74B7A9EDDDBF11FA + 0AF6C654F9A8AEA756AD3E6EF51C01B02F66CAA2672D699182E2C227AA592085 + 7EB2A1AE45546813A5059D166F4A6B310675BED0B3C5A314178EB59D459E43DF + FDB638359B4702DB183C2DBCD5EC5BF6E4F71A6921AC05B06C7141DCAB1C552F + A6A34D02FEB601E0A8A63FE5B2E8AF25A700CE8236C57A3D758EA302698B3281 + 5DD965C3C91125501CCBD90942590078BA0B00D32E42E0EC64E94060C06F04C0 + 9E96BCDE1F9CB78066CBB7AC6D2756D2B6F1D36978F28CE99C03DDD18517D208 + 90172DBF96B69FBE98769E7E33ED5C79338D2EBE9686175E49A3732FA7A199E7 + D3E0E4F934387E2A6D1B5B49A3BC4378E2A46BD4CAC3634BAB0070144E2A39B5 + 7056E1FCC2E9472A205C3BD248E578C349D7E67493334ECE3939E8D49E7180BC + 82B7A435DC553EC2DFD84652E42FC05729366D094D9B088005816B105C3B5B65 + AB1DB4D8F7E0783DDB01C151110EB7397F5506EED6DB4646E7717420EFA55F01 + BF5138A1251CD48D531A2735F6E2FC8E7057CE6FD5AB8DEA10EDA3135DCE7652 + 45530904BBE3BE445FD5B6E894070067089CE16D3F45384C5E65BD875211C2B4 + 55BB088257815FEB3756EAC689F2FDE0230725BE2534517157AC1E58626D0541 + 04827D3C74FDA6432440AEA2EF0460280BEE3A0C2A208736CAD386B66AA7326D + 04839567DB68CAC060B6945DFEFC8183DE717F8767864A0060045C6A80F0ED1C + BD38FEE1AD3476C38E63790096B6D0255D04B8DD066C11F5D80D793D52B208F8 + 2550960508237AF8BED9EF3612589D298058EF3DA51D29FD10DB472F038ACD4E + 3E8A760E9389F0FC8C88D10C8011F9E9DB763EC596C16B0692829A828F0E4F49 + 8B7C3C1BDBB7A6E67C391F4B15A93C69E73B0944A7CEDAD28771819502C01158 + 12C9AA68571D43E741BD206A1D09CB58F9BDB939653B656DE1CCB6CB8D3816DF + A59D0FE7CDE75124B080B59FABB569A27C8B6219004CAA2858A26A393F9D1B91 + C67C067D567D469D2B6DF82C440993EAF3737EB491E897FF5E19F8D286F7374F + D9EF8ABE8AFCD5F745AAEF507D3C6FBF3DB61A57343BBF53E035209732BFC708 + 75F51B252F30CCE7E46106E0306DA9239DE7376C7F67DEC13B6BBF43B7957A7E + D78CAF6320C06F14FD66ECFF2B47E1F3BF46E4BDFDAF9A2210A60CF075D0FCF1 + 272EEAB18DDBFF2C0F6E10B1EFFFAB1FDB6708EFE9F5F7F3BE7F334704DB7561 + FC03A0ED75BB567C68D781FCC00A3057A037BE9F3CBECBF7A85DC790C02FA904 + 00E67A07009600BF8779A8C6E16F07FCFA35B308BB443B4030EF7BF7EBEE25BB + 56038179AFFB7BECBA0024BE9C0EBE69F7888B760F21FAD7AEDD47EC7ACD7B7A + D9C23943DF9C2ACFFD4102FE1E01FCBE96DF3BAFD70E38142EF72049F71852D9 + 74AFD33D2D2ADEE7B857EA1E2A002C08AC7B69BCCFBACDF2BABF0AFEF24A075E + CF101FE4E2DEED0F6F59AA7BBFEA340F608E21784B342F738C38DFD01C447392 + DA26BB6C1A0B310EF31B6F435BE63A36EF01ECAA1C616F5779D9E64E2B761E96 + 46E01BB768661EA7085ED27E8A2097BCA439A114E78ED46B9E884D7DD547E3A9 + 2F8AF34E95E9DF4B8ADED51C18D5B05736D2D84E02FCEA9DC24D3E08F8DB0680 + C9037D7DE720A9AC2922E015C88D8AF097B2003079D96A4500ACB6DECFCA5AF7 + 681D447EF4C4423A62BF8BE9E72EA6DD7CF6C33369F4D87C1AB6B58DAFB9CA3A + 8CBCCAACC768C3DA2C6E0BADB6A85EE721A28AFD41E0B24E14F0D53AB36D5D49 + 5DDB9A35DAD42EDA2300466DD0B796F775006C6BFF3DC8D6E2805F93A06F5C9F + 77ADDD2B1F80F2FD7C076D92BF41EAAADB61E7B4FD6907C00E814B9BC64F527C + 26B13FEA078025F9621EB6FCB0003882DE5EC2B704F4DD6263A3C6DFD4E2DF92 + 625D5B7D2FADB79FEA5701E0E2B7ABD5D4C77C4B5B00B07C91F261AEF63FE6FA + 365F68ECD7ABBF14FBB4D5B729F6594B6DFDBBC5B9F7536EB79EF1F43DACAD76 + 3FF5A3AA3EE7B6368FA25E633EEEE3444556D2A62E3EB2B11BFAF604C0526033 + ED63768FDFCD74D6E06C3500ADD4F54E61B57D1215CEB955BDDAD5762FB7F0CA + A29A79B6B569552BF895AA417FAB7A2800EC5F72CB1FE477FD1DC545225C2CA2 + CAC5A4ED02B696BC7DCB45F24956BC69AC759358EF4DA41EB39FD6D3579316A9 + 6DC2405EF52A6B3253B78B7949B65EEAD5AF5FDFEE76949974E4CF3860E7B56D + D826A8D68EED30062D3F35772EBDF0CAE5B47CFAA5B483AD79F95D318E4D1881 + D2FEF9AC2CE8CBE48FB40BF6F279CD263513D0F2CE903821ADED71128B54DFBC + 27D8C6CB00B8037F63B4AFB678CEB6CE645B93EF5ABEFD334F6796050112E08D + 62911053E5A31A006C758DB0DB98B2FB42460B218ED5528ED262470B9E36A9AD + C6505A2FA2749C3669B1A7055F3F356DAB4524797FA2B7E4F594715C743AFCA5 + CE7E5B00E0813D393A78CB5E5BECEEE92C86B500462C82252D94516C2369015D + 8F11EBD43F2AF68BF2FA2AEAB756740EA036002CBB3B180AE46D03C0CA2301DF + 087DA3E40819390EE0A54D86C631FA57ED50C7F182D325435E52E59BF779017E + 4F142710D0F7C4721A76389B45F4AF6FFF3C01903D938667CEA7EDF3CFA791F9 + 17D2D0ECF369DBDC8B6970E1258FFA1D9E7FC9CAA6F917ADDDF36968EA7C1AB4 + 3EDB264EFAB6CEDB2701C0CB59E36C33B768F6EE2DA0E5B8DA5652CA8A02561E + E7D59EA50B69FAD5CBE9F033AF354E3339BAC8CBE126471BA9845D4EBA28EC9C + C756E0B2F501E8027B47E6CE344097FCF0ECE9A68C7A4160015F89B2B67E46D8 + BC8FD9057FEB28601CA7283A5529532F87ACEABD1F4ED233563EFB926FF34C94 + 6F94007004C402BF51388223E8ED256F8F13F9396B5B1CD7D169AD32F5484E6B + CF5317DA487274474587394E7420AE003029CEF5087FE570575E75E40580151D + EC0E7AA2B770E2BF79B90BF65216D0555E6D1C1E947682BC0060ECB18D0031F5 + 400B016087B9973F70303C16A0EE89ABD7F376A897AF391839F181B50794587B + 6D1F2DF84B1FCA822F405AC19508706577506C79D21A142345FBCA463D36CAA4 + 8BB7EFFAF6CFE401488ADA8D51848024DFFED9F2B25356C430118C4060C05684 + 64025F19FC66017E0161D9DE0DD75086BF19002B0218C8EBC0D6CE8B14884B5A + 0360C1626CB459664BE30080959FBB9D41716E0B90B5E3DCCDB097E8DF592BCF + DFB17A00F0BD0C2A1D3E0688E891C376BE405F814DDE39EB91C59C4F89A005A0 + 22E581C1D39F025C019866B77111E092B111E5C5FB0F2C5F60B29DB7A27181B3 + 444033BEEC9EB7F3C8EF34069E5AFF0272055881A9C052E4EF1AFEE28BB4088C + ADCE514018E9B300B217FDBBBBEF0F07ACD8F13CD2B788E85FD20CFC0B1CF5F3 + B07EF6DD207D46FF1EC3E714DCD577AA7694D55E36FAD006A93FF668535F8070 + B435B2EF9DDF977E87A49C2BA2CC6F9854E09672FC6D777ED3F9B74A7FF2A4D4 + 691CFDAEC9AB2CF05B435FEC9EE7210CFBDFD6FF950030FF9711022345E912BD + 3BFB0991F5760ED607FB0976122800986DE4FDFFF6433BC6F58FD3445714305B + 42EB9DBE760DFAE0865D1FF283225CA762746F14EDA9E39AA61D0D047D9527E5 + 7AE7D1BE763D6C2281ED9A894E60B3EB1F11BF5C03057D1DF816F0ABFCF14BD6 + 9E3EB4BB64D7E8B7EDDAFECEE574F41DBB56937F2B5FC30FBE4124B0952DEF5B + 3F5F646708BB960BFC0639F4E59E62F78E1A001FE2DE51EAB9C7C4FB0C925DF7 + AF781FAB2538CCAB13FCF50994696FF745F244FF52D63D54F754EAC96B170EF2 + BCFF77DF85CE7D5C70B70DF8D665015D52BD7282798620AECA9AA3D4F392B6B9 + 8B6C31BF8F36B467DEC27CA6E41BE06BE5AE08E1E5ACDD4B790B68815FDEF18B + 14ED2B302C58CBFC4E76C15A52B58BED351754CA9C917CB4C73A8DD31C2B48D0 + 96792BC2462A7B3F69CEAB7CB445E04B5E5097BC24E8ABB66AA77A7F88722C4B + F05702006F3D36EB12FCAD01700D74554FCADA03A02BA8ABB65A97E4B549A71C + DB36F6B02ED29A8935CED3F65DBF7CFDB374F9EEF769D2E6FFACCB36DA9A1218 + 3B7C6826EFBC54C0AFD66C5A9F6DDA676BBD121D5CAFD9DAD6788F028011F55A + 87C6B2D6B671AD1AD7BC4870B77EDF6F9B68C316D00E805D9DF539D27A3D2A46 + E2D66B7DD2F5F810506CD3D61E9B83DF0280EBF6EE27A9FC16527C0730C2E742 + BAC5FA467B2DB5535E65E5B3F0E7E42DA0EBF70047F02B45D0DB4BF8976A002C + 9F96FC52927C61BDEC8D8F0C5FD86029B7B4979A7E2DA21EBF9C7C750E71F101 + 4A948B9AFA2ABFAA9DA9DBD768366BD3E68B6CB3F5526C1BB59E3651B1FD5A6A + EBDFAD5D6B28B76B1B4FFEE1A8585FABD39FB6DD7EEA9FA3FA9CDBDA3C8A7A8D + F9B88F13254E52ABE121918F6C5C6704B0835D93D577DB6B758FFF3B006E51AF + 76B5DDCB6DBC32AB669E6D6D5AD50A7EA56AD0DFB4F8B03DA16FD4EF00F8D717 + 178970B1682E1A9D8B497D41EBA7D8A7ED22F9242BDE34D6BA49ACA78D14C7EC + A7F5F5597D73AE55DFD4B979B70160D5A95E75B1CD5A9303F597D6D346ED3826 + 3097F216CE6FC0EA36EF4A5B879FF2C8DFC5932FA4B1295B58DAC49AFA2D8054 + C6B4F3A10F133E26905BACBDC06F2C6B72E993C332E1F4C927654D64C384B499 + D49672DB44B663B3E33900B645026AE02F936BA47C678C5ADD00D88E41BE2C0A + 562F0CEC1805F63E3400C6C6786571D32C64CA82270ABBDA6B1184389FB8C0A9 + 8F299BFA6A7CE5555E8F0476FBF56DE02F6A594CCAA6C52651BF005E167BDE3F + D4D1CEE1EF1EDE7364DAD30D6CDB16C32C9AA5B6B671B15D4B0B75EA633EB661 + 0CE53596B72B8E807E8A4E82E848505DE3602800B84D82BFCACBD9D14B405E22 + 803300CEB09714AD02C036DE96436CA3369388FA6D05C0C70BFC25D2B744FC0E + B3E5B347EB9E749107040B027B14F0CC85346C1A9A364D3D9BB64D3FEF307878 + E6D93434F39CE59F4DC3D3E7ADFDD9B46DDCFA58DFD149D3C4928DB9E4009877 + 00F39EB1D1891587C3724AF971EC1C06C79733889D3A9976E2589BC951BF0327 + 165CC79E7D233DFFD13D87C002C038BBE883E3CB8172B12BDF0B0053A61E27DE + 948DB7F2EE47E9C885D7BD0E900B0406D40A06FBD6D0C561A84892BA1C252765 + E3A85C79B6792F9D3B1C49CD4E8A00B982C0B5E440551B3953A9539FDDB409D0 + 17102C181C01702CBBD33768BF494E613986A3621DDB401365141DD25144F47A + 5492A538BE0573BB229E70720745E02BA77874940BECAA5DB40BF046877C5D47 + 6496B686A6ECF68BEFA56302BA45CDFB2183D83294BAE380DDD28E3E272E5D4B + E3EFBE9FC6800FC088027F2304061013F1EBD036006052CAC7DF05625C4B47DE + BD9A0E5FB6F605AA8C0186AD0F12288E00F858012B02BD8016C00AF0972D9B01 + B8D8A823EF0006F862656C94398EDA9352A6ADA27F2947E8C4D6CFBEFD33B0A8 + 6CFD4C243030698C7712171BCA79EBF771670B68C133876205B06508063CCB50 + 5560AC0DA24D7D9C4158EE4374714E057A057F4F3EF83AB1953365415DCABEBD + B395B10B16AB0D296390EF94050189F80404DA312C55B4F2C26719E821453403 + F9E883A6F85E052AEFD9B8D646D054C05630D5A3854DD465086C9F8B63D93122 + D8448C0714C5AEF11165A0A7227D55AFF7DE22FA93D28E3682D3F49FA13D5B5A + 0380CDC6FB7FE339CDDC26BD9726EC6F4EB4F22CDF957D06BE5F81CF256BBB72 + 2F43DF18092C00CCF7A4685A87A29C73394F7D3EC1596CFA5C7ECEE57BE6FBF6 + BF8D89BCBE7781561FD7CADED63EA76F016D756ACB589344F8868862DFF2D9DA + 20A27469A7BF2D36FD2E115B30F37E5D07AA1FDAFF89A9FECD52D6D6CDFA1DE5 + DF523E3FD9F89FD00312FE3F5240AF6FDB6EF22864FB3F52F42E12008ED057F9 + 1CE19BA374FDFFF9C6CD347DEBE3B460DFA700F0B8BFDFD7C6BC911FF4F0FF55 + CB4F5DB7F31000E6FF99FEEF0374AFDBB586EB8B5D8BDEBFDE89DEB5EB89F737 + E99AA2D4AF37D6CE01B05DE7B86ED50FB3E42D9D7314F03891C7440217D09BF3 + EFA7C36FB385B35D6FED5A7AC4AEA987DF64CB67B3D97599BC6FFDFC0EE0378B + 6B2EB0F7C0EB760F20CA97EBB5D98ED29F3ED8CAF5FC9895057FB515F4E1D7DE + 49875EB57B07B2FBC721BBA734E0B7C8A1B0897B4CBC4F797B13F97EF732D9B8 + 0FEEE70128EA83F430951E8C6A03C08AF6DDF38CDDBB652BF76D3DC4C57D5DF7 + 7ADDC73D7F2EBFF3F7C033761FB75411BF82C052B4310F8975943547613E52DB + 6A69DE020046F11DBF35FC559D227F33FCED80DE087FEB3998E676A4B269CEE7 + F3BBD01609E26AAEA839246DA9F3F95AE9AF7AF29ACBF9B82601DF08741F5640 + DB988FC01709E6D6F05765E6CBDA292702E058DF067F11EFFD75006C6B0AC15D + 015E415E41DD9846FB5A00384A6DB75A9EF58BD6435ADFF89AC6D649A4FBECFB + 5D79E36ABAC03CDD7E0FACB700BB83960ED97A87076FB54E53A42FF50E89CBBA + 6C8BD684612DA73E2A77D94A7BA96BBD58D69575B95E5B9267CDA9F56D5CCBC6 + 752F8AEFFB6D03C1B1AE01C0BB9E76B8AB5DB7B4EE8E527D5CEF2B4F1F95DBFC + 01BDEADACA5203808BCF827A1D27FA31A26AF82BBF07E916FA57F6F5AAD33E43 + DF8701C0F53B7FA3BC6E68DF9A0058FEADD57EAE502F9F59F1BDC95756B7557B + A5FD848FAECB4F37B07B15D46D54EC4D5BD9421F01E0DADF492AD5756B49FDDA + EAD05AF5B51EA66D7FED5A436D7DB2A28F58FEDDF59D577D8CDE8AFEEC5E7A94 + 3EEB51AF311FF771A22203513EDAA2047BD166AB8F69DD7615DC6D55D5E66101 + 709BBD4D0E479F50B59D6FADB676B5CDC7FB1D003F9488FCF5E85F142380DBBE + 8846BF03E05F5F05F6B6805FA9BE98F5536CDF76917C92156F1A6BDD24D6D346 + 6A1B33DA625D9BBD5B6D37E52CDDC0E34D5D76D46BD2A3FA58A7FC5A1384D8B7 + ADBEAD8D04FC55342FED54260F041EDD75246D63726CE74D7910E059C6E4DDC0 + 4C1C99F86912A9496117FCAD212FA29D26B39443AA492DAA27B4B5AD037AD786 + BDBDD401C04CD699D477160535E08DE0B7AD5EEA0780E32248E528EC71F1E3E3 + 98E2C2468B95DA8662DF5A8CAF05562FC576B15F5B3DD216D0F56252A20EB00B + E0E59D3F9C1F7D1AE0BBD3CE7BF7A1B4B5BC936883D5C585AF16CA71412C5B6D + 8F8A6DA2629D16E528DA7A2DD2D78A008E92932002E06873474389FC6D939C1C + CA4BD8DA246788A27FA35D5B41ABCDD6C3B30E7FBB00F0D1BC05B4B681DE7A7C + 310D9C5832FB421A3CB698B6990481057F8900F677003720F8741A9E3CEB02F0 + 0E4E007A01C1A6D90B6978EE99343C732E6D9B04D89EF4AD9F4700C0A6E1138B + 2EF2D870348DE070C231E53AD90580E5BCC291266718799C57FB569E4B33AFBD + E711C072C4017A25CAD1AEBE887708231C798876C0E3A74EBF98DEB8FB7DFAF8 + 6FFF262DBD73C38E792AED58C8EF0226EA17F80B08C6C988B34F4E43CAB5F351 + 8E4214EB954ADE1EE7E1722702B87696027615192CC769B4C9D14A9E74F7E917 + 9A085F0160C1E00880956F05C0C1418CD42E0ABB03E0675FF748231CD0F556D0 + 6C4BE98E6D4B230096739C3C36DAA89D80702CCBA11E1DE6B2C532D017A08B54 + A776D44509064B1EFDF55606BB82BB11FC0AF62A5FB7233F76E95A9A00EA1289 + F64EF736D1C00745050B0447185CEB9849C0578A65811307C1A4BC0BF8FD8FD2 + B8D51DBF62E35E23422F47F00262003002C0A4C74D7A6F277500618011E02883 + A1BCAD3436EF6FF56C49CD36AED8815EC026B689255A1010CC76B4005FA20925 + 450277452836E0B6405FAB6B221B3F224AF1D3347FFBB306160B96A90F796F6B + A22C008C26390650CCCE0F18B9729FED86BFF0B2C0700484F91CF21802C4B4A1 + 0C18A62F69E7D88C9DA16F8E52CE654020DBF832A6E0A6802136449E63BB3886 + 9D4F86A939DA17980A280606035A01AB137C2F9FD87707B4FB2C036024400AC0 + 04EAD690546545C5529EF8D8BE9B003A49D50600AC54C7503BA284E7EDF310F1 + CB794EB1A5F71D80F57DCF13A53C67657D7F599D085F80EF32DF4729EBBB218D + 00B8F98EAC8EF3EA7C3E3B5E381FC6D1F8EAABDF05E3EAEF2B5116C0D531C8D3 + 9F31270A0026E2577DA7CBEF91DF257DB023C15CB7DBDF8A8720F8FDF23FC16F + 1B40CBBB7DE9A3F34202B31EAD6BA23F63E95C746EFACD037BF97FA9FF1F25CA + FCEF92676CB5F1FF61CEC3ECFCAF730D20EF0F72109D7CF3969D77FE9FCB8018 + BB9D1BEFF6B5F1B4DDB3A27FB59DB3C3DDB27BC1F855C02DD7A272AD7AD7AE67 + 76DDD17BC9E3358A5436DA720D4344F352563B6C44F0027B1151BF8A00CE5B3C + B385B35D4B1DF09ADD52DEDBEE10D8441EDB8977ED7A6AF5C060C43517F8AB5D + 1C00C77EED2ED76D00308AE0B706C0075FB3FB0EE9AB567EED92A748F097FB89 + DF870469CB7D47AAEF516A1FEF713C2445D42EF936E8ABDD3110905780F7A902 + 7F779FB7FBBA0070D9F2997A0160EED9BAE7EBFEAEE85E54E70582995F9052C6 + 0EDC15E4C54E1AE71F82C3A4D86497A26DAFB5D9CF3896D600B886C000E07AFB + 67E653BD007084B5A4D8A8979DB44DB497981B228D415E3BCD50666E3958F2DE + 36CC3391002E8AF056F359A957FB98AFC53C58E3694C52C15DB6772615F0A56E + 6422CFBB33E4CD00586A7602F232AF84E9805E290260806E54DD567057F52A23 + 8161E537B326AAD63EAC63B4BB1165AD8D58E76CB7CFB6FDF88247FB4A79BB67 + 5B7395F51965B67D1600665DC636D1ACF358770E84351CAAD7758DBD44006B1D + D9AC155B6C5A67C6352A76AD33B109F2AA8D243B120096DA00B00400160416E0 + D5BABC5EB77B5D58EBC7F57E2CCBD6AFAE5F39DA49E527A12CFFC4AAB6A62D66 + 47B5CF43BE905817FD310F277C3A19F60A000BF42A1FEB0479DB2030F58F0D00 + CB5F868A9F4DBEB2556DC318CAF713E3F8588C3B50602E2AC769546C4D5BD942 + FB4D56D70678C9CBAEBA58DF4BEAD7AFFD7AC77AFCDAE5DAB0B587AC0D6AEB1B + FDBAFA2ED7F739F231D792FCDC6BE951FBADA55E633EEEE3D48A2C24CA594AE1 + 2B11FE46018091B8CBC329F413CB6998CE1A8CED61189CC3D1A236DBAFA9789E + 6DEAD5AEB67B7935BB14EB5CAF7D955AC1AF5406F9ADCAE1EF866D6983A901C0 + 6D5F42977E07C0BFBEF24569BD6ABBB845C5366D17C82759F1A6B1D64D62ADB6 + FDFA5397DFE7DB3DC67A94B74FEE4C66E2CD39DED4DBF44B4C543466DBD8755D + 94CE9D3C2965A0AE0361BB996DDCB2DBA37E3731D666B689DE9706876C12BB35 + 470DD3963E5D134064E3352A13D6551353FA295FEC4D5AD5C57ED196C544FAD1 + E12F6A03C05A4408F0B68976311FB5EA1DC0D85894540B9BB6858EA47EDED7A4 + 454D4CE38247791D4B6954DB028B7C2DC1DD5E6D54D7B40D0B492D2C572D2EB7 + 3F9D76D8227DFEDC6B69E199D7D36E5BA8E76DA189FE05125B7BFA94F7006FD8 + 75D417BE5A1C232D80FBD97B496D63590B73E5E3825D6DD5CF45FBE2086813DB + 8149947110B40160296EFD1CC16F545D2F07486C238788EABCFEE08CD74500EC + CE12138017D0BBF9F04C1A2851BF036D00786C296DB174CB319EF8CFD1C0DA06 + 9A1408BC7DE254038187789FEFC4698FEC1D1C3B99B68E913F9B8680C2D3E74C + 6CF97C2A6DC5093566E39C58F1314619CB8EC1398EE064E25DC32716D2D0185B + 40DBB853271DC2D2671B10D83484A38ABAC9CE3B80D9FA19788B730BBB3BEA70 + 92599DA06F74DEB9CD24071B8A5BF4A91DC7DF77F2F974E6EA27E9D53BDFA4B1 + 17DF7627DF88D50BFE22390E95CAA92845E7A3CE037B04BFEA23E725EFBF93C3 + 13A7A820AF1C9F6CEB5CC3611CA802C0B55335C25E54C35F015D95D54EB0378A + F7FBEEA51D0EDDD24EDA7B3E3B853D02D8C456D0AD10F885CEF6CE947160B395 + 25C2B92DBB3BB5AD2F69B4E1108FD15382C3E471A447088C047BD596B29CF46A + 1FA1B083E037DE6D00B0E02EA9E080A2C4047DD18977AE36ED22240602A33A02 + 5800384260DF36BA00144950042042DEDB595E30450045E004B8027899FAE066 + 036088D60300137117B75A05E44ABC9753704770171B10093B2900CAE1CF8D5B + BE952F5BFA52A69EBA89EBB71C1269CB582209A76FDD494402037EC7AD7ED26C + 4032B6966E0056016BBCC3D4C118027801DD3EBA99A66EDEB4313230537BEA68 + AB8847876D26F2B31F7700F00476DA14D82B980B6C1374732057F28274405EB6 + 8146E405808199E4390F3E33B097286594C12FC02F7F1ECE133126402F824880 + 39EF4C46C06220A1CE0F4DDB31C6017E7C1E8E6F360160815B406806B2199022 + EC8A6075385A002E65006A04C002C96A875D79A27F353676C660FC0C7CEDFB2B + 29EF2526F217003CF71911C1C06A6030B03B475003CDF37749A47501E1D63F7F + CF19C6920A0EABECDF91A5BC1798E3EB5CA33837815F7DBF82ACFCCEF87EF55D + B33D3979ECA40E65ED3BA5ECDB97DB38F4E73BE6FB023CEB5C188FBFA5A27AF5 + 7BC1A67C0D7F7D6CFE7EE57844B6935F601B6FB3FBEFA7A47AD882325BAB2FF2 + 1DD9B93076DE8A39FFBEFD010CFB9F53447E86B619FECAAEB200B0C616FCE521 + 10A2FA7DEBF69BB7ECB797FF976843C42FFD18C72375DFBB9EC6AE027AED1A61 + F277F8BE6FD719AB9FB2FF7501E0896B40DE7CED8A209794B2AE53C81F4CA9AE + 5F02C0E46507F40AFC6A5BE78EAE3A003E6E75405E142130A9228033F8E59A9B + A1B060F0A18B97F3B6CFE17A4E7AF8E26AF0DBE4DFC87AFA15BB5FD93D230260 + 2282B98738E4E55E53EE49B249DC87740F934DF7274160A49D329A7B5EA50606 + D34610D7EEBD80DF36004C9BA72FD898DC9B7B0060C15A524910983C7311CA8A + 10C6C65C23F661DEA1F90976443FCD5134A7897317F28AEE8DC05779951D0207 + F84B04709C4345F05BA782BAE4EBB6B15E625E17251BF3483D4C8818E31080DC + CE8DB2A0301056D0B60DE80AC4C676487353B59354A68FFA291FC16EEC834D00 + 380AB01BE16F84C0805F7604D2AE40C3CCCD8FB3535037045E0B00ABDEFBD2A7 + D8D55E91BE9B6D9D23082C9BA27FA304817DBD636DDC66D21A0CB82BF8AB685F + E5058669C75A6CE4889DDFD35369E3CE2369C38EC30E76BBD66A658D176D6EAF + 006FBD9E545936E56B9BD6A5F53A56EDA2AD86BCBD00B08FB5D7D6D1AEEE353A + 36BD9229AED3B5C68FEB7EF90354EE25FA445F475B7D94ECF27390D771A88F3E + 8CCD661BB03AB48576D88AA23F448A75FDB4BA0D3E9D6E004C39025E6CB2ABAE + 0D00AB0E3F530D80DBFD59C14756DBF1A5611FB4FAE25F93AF6C55DB3086F2FD + E4FDC2B8ABFC7895BDAB6D5527002CBFE25A7EC828B5AFB556FDCFD5A38FBFCB + D50A7F91B569BECBB67C29F37D1230A3EF4DE7D2EE4BCEC7ECA7369F772FFD9C + BEFDF44B8CD94F3A4ECD425C5D307635F055E4AFD2A6DFCF51D731D7606C56DF + B5CD732F39180DEA57F76B289E4F9B7AB5ABED5EFE0D026087AF2DF968439BEC + C3F56AF3A8EA8A002ED2076FB335F0B7ED0FF2BB1E4EFA1EA34A1DFFD8FDFFB9 + B940F4B87854F678715B8FEA8BE4AFA5E67CF81C2DF5880B780D65DBDA49B1DD + 5A6D6BD57D1F4602C0B5EA1B756D6F83C3BDFAF652BF311E4631D217A98CA8E7 + 1800DEE1D18319FA0E581FC0AF0910BCD526B06A2BB1F58B6FFD6C13DD8D56B7 + 81770C737E36B9F427214999685693D27AD2DA5617EBFBB56F531C23F623BFC9 + 26E79B46B2F2249EC97D5E14A036C02B5B54ACF736265FD4581D50392E8AB4A0 + E9273D11DB8C61A9CE498B9B7AA183BC7D911657715CD9D6B39843B15F3D866C + A49B89DAB5C5234F10D700983A8EB761706F9A587E2E7DF1A77F99EEFDF19FD3 + F1A50BD6DF3E97B563214ADB2D7B6C315B16BB82BA5A0093DFC0E2DCA445B2D2 + A86823BF71378BE8CE221BB08C3D2EAC4959ACF3CEA6F8DE26B56954DE112567 + 801C02CA6B2BB108806BC57E11FA2AAFB29C1AB2018B51E3E8C079121C1F1276 + 14DB090023BDDF37C3DECEB6CF5B8FE528E06DC5B6E5F842DA72022DBA80C15B + C7711EADA421406C81C1A313A75C82C28A0626529836C3274CD48D9DF47EB4D9 + 06FCB5B180BD44FD8ED871468328CBC9343C66F5805E6BBFD5FA6EB5F1064DD8 + B64F9D4EDB274FA5A1E2E87247D9EC19CF2B429808E2E844EB82BF256DEA2A51 + 1F85B30F27A1FAC95108A8251A586DE430A40E67A01C8BB2CB715803608D271B + E2BDC07B4F3DDFA47A4730CAB617DC194AA4302951337B8ACD1DA4C5894AEA0E + 55149CB0CA4B005D45EE78B9A4BC3B588EDFE800561BCF47005C1CC98A340218 + E38C7607F4F319F4BAB31A67B6091B80588057F09732E017C9891DFB20F2712C + 2FBF9C21AE80AE1CE938CEE56097C31DC82B082C073C52FF43AFD006477FDE0A + 1AF93B817D8B4F537947F0B137891CBBDAD41FE1BD91A54C9B43AF5F4A47DF7C + CF6DB43DF1F6B5268F0425042688547359BB0C8AAF3AE020026E82AD572DF508 + 386C8A18760092CB6C1F3D616D260B04166411083E5A41E509ABF7770C5FF920 + 1DBA7C2D1DE63DC3009E6B375C40A40C85324022A52D30EBBD7FF56FD3957FFD + 5F789E3EB425FA17000CFC953C12F8A3DB2E203030185006F4D29882521D0865 + 366B4BD4E3C48776EEA6A99B19FE0A6C91A73DE378C470E99F8130E377A28F49 + E788522D761FBB9C0BC24E1426E9FCA777D30230F0CEFDB4F8D983B4F4F903B7 + E5F3FEDC2375F3B819004FFBD6D3F6FD98A66E119D0C44CE0052001888076C14 + FCCD025802F9329046B265FBE769DAC6607B6295A9671C0147F2C04987940518 + 725CD501340554E9C336D9B477D069A903D0725EC057815440AD2271353EED01 + B002BF5133FEBDF09D7D61DFD9979ECE0185819E76CE8A045EB2F101AD02A588 + DF0F705610D62383ED78C8CFAD7C0EFF3EEC73B1B5F3DC1DFB5BDAB9F877F359 + 01BFD6DFC7E53B2B9F4BD2E760FBE6F8FDC43C75406907B2FE1BBAE37F6B8EAD + EF85F60E643FB2DFA829FE06698FF43B944D9FD57F2BE5B7EAF0B9B4A59EBC7E + DBA4F937CC6FD37E671CCBFE1FF43FE8703680DD2ED06BA91EE800F6D21E5117 + FB208F02E65C6953CAFC0F52C7031DBA06F0BFCD35A201B626ED2E10AF315C5F + 949780B87A902503DD0C886BF88BDDA3792F5DEDB2B9FDD2B574C8AE85877968 + C67750E8C861AEEFAC60C710D8250258D767AEDD3C8C63E921BB961FBAC87596 + 6BE60D6B6BE3028089EAB56B3AFDE2C33B4701C3405FEE1582BFAFBDED3AF43A + CAFD80BEC0E0A75FEEDC5F0E730FE19E52CA52730F2AE21ED6763F935D754A1D + 1C97F2D3961E7AFE623AFCBCDD579FB3FA12D9EBEFF7B5D4EFBF761FA64CBDEE + CB0E824DDCAF779DB13902F76DBBB72BC257F7F62841DF980700930AE6AA8EF9 + 01294017C9465EAFB510EC15F0DD03CC25A5BDCD6378082EE681BC48EFFC95B4 + FDB3CF9BE8E36DCBAB34CABC4C73AC988FF32DCDC3344FDB453DED685F4976CD + 2FC9EFB6FEBC8264E5D28769EA9577BDEC70D8E6A1C060F23EF7240D60B6566C + 5BCB4133735B4B63D42EF9D1C9932EF26D525BE6D308A8CBDC5FD1BD02BD1102 + AB8DD7B146B07584C0AD14E12FD26B6724AD3F4698D397BCDA9022202FEB165F + BB98BD6D5D13A57EE47D4725D654AC93CA1A4B3B2D017D878FCCBA80BE0060D6 + 5FACD336B0E6B4B6B421A57EBB7D56D56BACA8AEB55C00C0AC11B5F6245FDB64 + AFD5A9B3B400DBCDEC8855B409159BA06D7E783AEFAC15D7DCB16E4BD9654BA2 + CED7E4966A072EEF53FC01A411C0465B1476D5459F40ED7B90621BF26A475EE5 + 556DE403B17C84BF28FA3C103E17D22DD60E457B542F7B94FB734CF8B190A275 + A52660A02AD7EDDAEA3526F66E3FD4EAF154466ADB4891C0A5BCAA6D08BC68FA + 48A11FDA3898E560B24D03D98F1781656B1B521B67F3B6EC475480C95A3E4AB5 + E9A7876DFF4B4A3ED8EC87B57490CF6E298AF93600CCDF84EFBAD83C6F3687BF + 25A0A6FE9CD1DF9BEDF9B86BA9CDEFDDA647EDF7D0DABC23ABADEE3148E7DEC5 + 3F223F094C65A329C2DF2807C06AABF6666F1BB37E8770573FA9E2418FACC896 + 9E74B59DB7F2B12E2AB669FAAC3790759D6A05BF5280A46DFA39B0567D01BF28 + 9663BB9FA3F500E0AE2FC3F53B047EEC7AE8EFD32E1E3D2E30AB2F260F0781EB + 8BE4E3D0CFBA49AC71038837A2B58EF3306D6BD57D1F4EDD13825E5AEB468EB8 + E1C79BBEA47651DD138FEEBAD8B76DBC36E9D84CD67C3BE762D7E48DD4DFFBBB + 756FDABA6D7F13FDBB89C8609B382A5A98B60E8B771D497B6CB1B5FBE0641A64 + AB5FCED9B4B94C689BC9672D4D465BECAB26B645AD13DB1E8A6374F763B26D2A + 00383FB1D91D011C170535F08DF9D847FD9A858DB501026F2E6D64EF2BFA8472 + DCFEA8D72247636B21A57C2D2DB2D6525CE8B589B138371F938560D95E4A8B50 + 69E32EB3F164B1B57B6A6225BDF0EE47E9A5CB37D353932BF9FD3F3BECB3D96F + 670B0B715FA866385B2F9881B602C0405C49EDA40C7DBBC1B016E88832407860 + BF2DE26DF1AE6391AA0D7629DA58ECF7720A2000305A2F0016D4AD21AF20B0A4 + 7A5443DE68C319E290B7D4A1DA79A2E8DF2D65FBE7088049878EF074FE42DA3A + B69406C64D963AF8B5BF1DDA367132F1DE5E40AE802F11C04D3E08D88CD83A7A + DBB1A5344819E06BE38D9A46C633FC65CBB69D13CBAE51CB037F5D38A24E9428 + 84B10C7F89480600F3DEE0ED53A7D2F6E2F01A3679A44581C10D002E0EB5E858 + 535406CE34A02DCEBB26EAD7F2EE9C2B755E6F76CABC4358EF12660C3910B7CF + E62810DAC971583B18A35311090047608CD447ED77AFE0FC24E28548E09C9776 + 2D67E8CBBB881136C030A058ED3308CEA018B92316472A0E55954B5ECE599CB7 + D1C11BEBA4E81C268FD3D8E170890E468D03FA991C61E411C305E6027623DC45 + 7584B0EA49292B2FA7B83BC14B5EEDC9BBB33D08888BA35D115582C082C3CA03 + 80A34D5244B1C3609CFC405F1CFD38ED71DEBFD2D95E9AF7060B24030BF43E61 + F2DA221400E1D0D7ECCA3B280E3003017F896423EFC002A8FC0E5004887BB381 + C01E297C29470C4B2A3B1806A25CE90017604B84330E578ADD01F0D50F1CFE22 + 45FB22C1250918449BA5CFEEA79BFFEEDFA7EBFFBB7FE780E9D0BB57D3D8F5B2 + 3D2C518165BB586D1F0B080600CF7FF2791288053E094EF9D6B426C1298E3501 + 40E6DDA71F7EE451C048759C1779C136A4BE8C09DCE53DC49E96ADA7D98E5A65 + A21CC90B12BBECFC48058139CF39CBFBF9DEBAD3D40182F39805009B263F222A + D4CEC9A395ED5805DE09F85106262A1AD5616581A34052B69C264A9954DB4F3B + 9C04DAD29FF2C7440867A009B41404A61D72785980A704B454C42DED6334B203 + 5FFB3BAA7F84AED8819EE401AFD81D805A19E00B04D6FB89678866FD04591BD3 + C26D3B9E0900CC79EBF3F099F9FBF3F9F51D505E02B417288C16F81CE4ED587C + 3EC1695287D826FF6ECA67D779FBF75C445B3F771B87EF90B13CD2DACE05A0CA + EF85BCB729E72320EB7F571BDBE57F932C7D4F3AB683FD0075A7EDF744FFE66F + 8EADD43326BF59FD6EB1E958B413EC954DF2B14A5B8DADFF01EAC9EB7F5465A2 + 7611FF53D889E40508C7887FFF3FA55F39278FF4B714104C3FB58D7DE3F583BC + EF2C60D2F504680B109EBE6E631418EC0097285D4BB1D14690D7AF41A51F2976 + 5DAB342636DEF1ABF7FC0AF4F2C08CA27CE3C3335E4F59D758802F7D2F5E7628 + 7C98EBEFDB440A7FE0FD0EDA35F6E9728DF6077CACECDB3F17F8ABFB836FFBFC + 0AD7FD8E7254B05DA3AD2F0F1FE9DEE0ED4DE475FF89F2FB076DCB3D4CF742DD + F3748F93749F6B6465DEB10FD43DF4BCDDFB020016EC8D4058F764EECF7A488B + 3CBB80ECE441B3EA412FDDE325C15B95991790AABDCAAAD37C42FDB41389F28A + 02DE1B1E6C93F4E09BE632CC775C8B5902C1F99DBFCCB1B200B97B199363D9DC + 4B0FFDC5F996F2F5FC4BF332B5C706D86D5E0D82ADE46B1BED983F3E659F7BF6 + 75FBBD3D77D1CBCC3B4925E6979A8722E68E4865450CF7AD2FF35C415A09C8CB + 7C98B9B140B0CF9F0BE0A57D84C4B18E54D2385D6D987B036A7D1D91D5068049 + 01BD82BD5A974409DCA206E296358BD62D28AE61D42E9691B7637D54D6588AF6 + 15E81504A62C28AC351836D66B1B6DDD497EE6D98B69F1A54B69E4E89CC361EA + A2B47E436E63CD57D69971DD8A5837465B5C97D66DB3CDD2BD479B6D9B058337 + 061B6B6A41DC0876A36447B12D79496D3D5FD6FA02BC529BAD564F9F40656F6B + A3BCD465C30752FC2B5BAC5F9B3A3E1074C06CB4C517A2F7F776FB66D44736D5 + 4BD9A7F354DA34D281B5358C15A0953DE6DB14DBF703C01A23E63BBEAA760110 + 6917E575950F2DF6913DCAC7296AA0AE54FC788DEAFA20FA0333DB003065F918 + A3A21F536DFBA96EFFF756C70FDBF123AF09804D7C7ED2F85D0BFC0ED8EF8EE0 + 197D3EA56DBEDEE698954FFB51A5F11EF7B85DFA85E16F94738FC8507A4851C0 + 522B00265FD43015D5C776A62E001CFAB5339F4790B8D293ACB5CEBBAD5E527D + 57FB02801F170406F46ED8B8B52F0016A4ADE12CD0F65120701C6FC3063B7E19 + 63F3E691627B74B01CD50E80F9E2462C8FC297B8EA4BA5CC1FA2D8EDDCBAFE38 + 4FB0D6153AFF44ABBA603C06FD120078D50DA2BAA8C79B489B623BE57BA95F9B + 38E6AFA17CF3AF6FCA8FA68799D474261E9CC7EA7A8DD536D16A6B4B3B04BCAD + 01B026706EB3CFAAED9F370FE4B2DAA81D3A6A0BC557DEFC203DF3F2A5B4E3A9 + 31FB6DD80DD5EC5BB613116C1344264765B2D935010D93D5F5AA6BB2DBCBA671 + 4DAAABDB90D784DB27DD4CCE47BA17059D854196CA405D95B5B8A81727757F95 + 5DD44B959DBE518CA7AD8F506C1FDB6921A585569416585A88B5B591EABAD847 + 0B3FECCD982C04AB85A5169D4A114F1D0F3F3D914659D4EF3FE16018004CFF01 + 00B02D74075CB618B6C572BDA595B409017F2D1DB085F400801650ABF6A6011B + 7F80057A89FEED5E7093C7BE7A914E3E42E06E003C9636D17E3F201727422D9E + 1ACF750307703E4C55F559D8B73E8DD3C252931C13D1B9101D19D491E2C4A825 + 87467494902ADFA5C39D085F6DF78C00BF9247081F5B4C5B8F67F02BF8ABAD97 + 7DFBE51201AC28604F4D44FD7AC42F11BA948FB2ADF3621AA66D81C1DB8ECD3B + DC1D195B74D8ABA8DF1DE34B490098BA91311BC38F93B79F0304037ED99A9AE3 + E398927076ED3011EDEB9116120EB3DA669273ADCB295839F4A2B3D06D260161 + 39E5548F8D72741ACA99A87C5DA61D7DE2716AF88B5DD0B70374B35A1DA30E84 + 3BF519FC3E1FDA3ED7057FDBA43A39705174E22AAF2821450AB9B3F8746E43BD + 471217088C9399A8609CCCD1469EE8A4BDCFE5F71BB2C5A5DE75B8E782F531C9 + 8EA3DB451BB3E108C7818E539DE8272F9B70A41F7EB9037DA3E3DDDBE2702F60 + 97BC9CF0B46F8B02563BD5AB8D035DAB93839F3C362083DE314C7AE2CDAB8D4D + EF84A40CF075C850A2D31409AC14300CC4187FEF4602641C7AF33D8F500354C8 + 3E76F9BA0BD0CBFB845117F82D11C1C05D608A008BA04D842BB28F017B3EE8BC + D317380408021E291250C048118100ABE7BEFF433AFBE5B71E114C3F074EB4B1 + 7AA091BF37D40404F6EDA06F7CD28904FEB003B2D44F704C1A0724912F0078E2 + 4607FAC6760265D81D7831E68D8F1BE8ACC8638160F2D4D710987C5DA63D7945 + 0A23EC9C1BA25EA02F83C502ED0200248DD093ADAFB139802C4019F8CCBB8A25 + 6D5F0D64640BEBCE36D69F39DCCC8033034C606484C14A1B085ADA7B5DB00BEA + 92471E215BA25C5526A52C75E034B2713EB9E79A03FE7E7ADF3E0F909548DB07 + 69E14E8E9CD6672475A8EEB263DA5840591E26E0BBD1DF50DF9B22C2694B1BCE + 3B0260445EE7AFEF5AE3D20F788A8DB2203363EA38B4658B66F2D8F82DB1BDF3 + C4CD0C8101F7FCFDB1ABBDBFD3973CE7609F4BBF294FCB6F7CAAE4D912BDF93F + F880DF39BFF7FC40846C3CA8003CE62183D99B76BCB27DBA8BDF28BF65527E8B + 8C6BE7E7FF9FC8FE67903F4051FE1FF83FD2FF2B76415A415B8159B75999FF69 + FE7F694F14B0C35FAB9BB2F3641B79DAD6EF1F671CEA80BD9481BC8AF2F571B5 + 65B45D8F8ED9F5293EAC425EF26B96B5A71FFD01BE7E3CCB33E6E1B73B909787 + 62B82E92CA86E276CFBEC342B996724DE55A8C8D727E1807C04B9430D762EBCB + 75BB3CE0A3877574CDF7877CFC1E40942F11BE769F798DF7FC027C79A0E84DB3 + E78788740FD17D43F70952EE4112F52EEE4DDC87B099B88F718FC3061C2615F0 + 1504E66129DF31837605F41E7CDEC66881BDD891EEC382BF31EFAF8228F7FFE6 + 1E5E4439CE03F6997837AF1E2A23AF3291BAA47A6FAFF248BB93F86E2425AF79 + 0B73941AFE6AEED2CC5F4A942E5284AF1EAE43CA93AA1D65E6519A63C51431FF + D21C4CE5389FF3F6D4D186B6CCF14C40DC28D9047BF7D93C8B579250C6AEBE6A + AF3924C7902823AF6B513D8FC5C6FC17A82B104C59E036DA2540AEECB10E7B8C + F4A54EF36BB5615E0FDC6D03C03508169CED2707B72DEB16893A1E708D405876 + 441F8DA37592802F65F22A23D655F1015D450563DBB0FBA8E74FBF712D9D7FFB + 86BF1F58D1C16D6AA07001C05A376A3D29455B67FD992520ACD4FB84C85F2902 + 60ADA5D792D6E0ACAFF560F6AA357A59CBAFA57AEDAFB2FC03F23BC472B4C7BA + A87E6344BF083E8F28F93B6244300058E0B7038079A01FC996E1B06C9D7A7C39 + F87FB20480238CAD7D45D1BE5EF5EA17EDEB19D77D5783594044A5CAAF570E6E + 8BEFADAD1E7541DE810234EB7C25760394DF517EC5C7A1E8C3FCFB6A570F5B56 + 2B00266FED9AEFC4F2F84EB1F96729DFADFB5087ECF7567CB0DEAEA4EDEA1CB7 + 9FDA7CE76DA26DDED532ABE9FB7702B68F5B621F5162229191D400382A42DCB6 + B11E4AADCCE71114C1E893283B47ED765B6BD567A915C6E8CA373CF2674A9C13 + F82B3D2A005E0FB0555FDA92DFBA7547DAB265346DB69311F81D18D8EEB6FA38 + 3F471D08CC98E58B2CEA2A3BE00D5F0C8A5F5823DA853FD413A8E607163E6BDD + E689D626537C7AE4675E70BAFAB65C201F555D3716C1DF729350DD7A15C77D58 + B58DF7F75404B1FD80EC7AD46B62D3665BEB781AAB01B745BDDAAA9D00AEA47E + 8D2D6CFF4C1E18AC3A4160DEFB01007EF9E2FBE9C24B97D21E5B7CF9184C2A86 + 81AD960ED9D865F2DA2581DADA2E69825A4D823511AEEBBAFA55E3C676ABE4EF + 70C950B706B675593652161F2C4AB418E92C483A6DA3B47851BFBA6DAC97347E + AFF6D8912FA2ECBCDA165CB2D58BB75E52BB366911D8B42B0BCB36001CB79AA2 + DEA37EBBB67EEE3CD14CDBADFB80B85AC07616B2D8A264571FF543D800CC83B6 + 40070C7716D95A70B31574B669815E2FC46597F2821E008C03A00378DB20AFC3 + DDA2BA9EF2E0C119971C1651D1C9503B3500BEB26D359BCA921C23384422F8F5 + F760910F0078EB116D03DD8902CE0038BFEB17D8EAD1BF63CBFEEEDDA86D44FF + 3AD42D6D2D1D3C2AC0BB90862C4F24F1B069C4EAD0D031C6CECE2117D101254A + 206EFDCCB6CFC0DFD171A288398EF525FA80E396630900CB4985C3CBA3814D23 + 966F9C64C169563BD3464CEECC2B0E3FF28D93AF084721DBFC45001C9D864865 + D5AB0DA23F12D08DA28F00B0DA44A9DDEE027571922AD237825E394FC9D775D1 + 89AAB63B494F766FC728D0DB66AF1DC2AA13008E91421AC79DC4050A3B247E26 + 6F3189C319A7344E6A5201E00875C9CBB90DFCA52CB8ABFA5E115238CF95CAB9 + 0EE01550C6E9DE38F48B831E9117D495A33EDA107987B896D2466390C7C638E4 + 15ED4B19BBE02F91C0D80585D1D1372E7745050B549002861D625CBEEEA04350 + 43E043F0E3B8A50DE8BD840D407CCDB77F46D857C1E10266045A046A48C72DF5 + 7704BFFFA1C35C450FA226FACFEA1DD25AD98153814F7A9F30F0C9B78F2D7D26 + DEE71DC4B72CED8E048E11C1713C52EFC7711D62019F33141EBF0190CED189CD + 36B5D68E7E7E2E9607F4F93B85AD8DB7BBF1490380BBA05A817511000B4E2A95 + 32F8B36301E04A3F955D7C3E1F2347660A60AA0C2C941DF0E840D28EC13848A0 + 394AE037470293E67206C3D90EF8047876815D6B2728EA5B1313855CCA1A87BC + E0AF80311240956847DA44FD5A99F6795CFA312620D4CEED26DF938DF931C095 + 285C3B1F9343E14FB5D57306A502C0F9BBCDDF956CD4EB3BC86DF2F7C4F74D1B + B6E3062637DF957F0F7C8EFCDD60A30F6D194BC7D1DF97FE4B9F3D68C6D6F8FA + 5BAA1D62BC7180AF8DCFB6D68CCD79907ABDE5491778C7B18DE7BF89026CF9DD + F15BD36F1D9BFF4E4CAAD36F517669FA46AE8BF58CC1FF0FA2CC180E97EDFCF4 + BFA6FF21FD2FF0FB27DF44F5966D9A81B6CAFBFF3D362BFB431EE5A10DFA722D + A8DB4E5EB5FF4DCB0B1EB3053DB669FE9FAFB09D32D72ABB0E9AB84E710D7385 + 77F372EDCA0FAFE4E8DBA326DFF1C0AE658CC756CE0E7489F87D33BF9B1745C0 + 2B61E3E1187F40A689DCCD5BF2738DF5876CEC9AEA0FDEF875F7BDB2D5B3D9DF + B0F394ECDAAC6BBDAEEBA49E97B85EBFFA763AF0D2C526F2973C0F27E99A4FFB + 786FD1BD4176DD5FBC4D01BE883CBB57700FE3FE87AD06C07E0F44DC0701C0CF + BCDA0A7FB1A30881C9EBF50C82BFDED6EEE1DCB3E35C20DEC3B16B5E80FD29FA + 16484BBAFF8CCD0FC23C43F30ECD43F60043ADBFDAAB0FA9E625DEAEC05EA479 + 0DF2B16957C02EE097E85E0160CD9FEA7C9C47D5F32AD43697932DCEE598FFC5 + F9A1206E94CF292DF539257D8B5D65D9BAE6A0551B95639B662E1BE47D4D31CA + 57F36149D0B681B73DECAA23F52D9E4D1EED1BEC8AFE45CCDD23F005020FD87A + 431218AE616F9B58A708F00AE622F202BDD4AB4D2D07BFA6FA8158AD9FB46E92 + B009DC52A7F69B6CEDC8AB7B58439D38FD529A38F7AAD70186B52E431A576378 + 997C592FEA81E1BC56EC06C0D1CE1A33AA0D006FD96FEB5913796D018DE21AB8 + 9FB43E06FED6EBF246F82A5A14D7F9BDCAF21308DCCABFD0948B1FA297E43BA8 + C771BB690BC7236FB6E8E340F27D90CFFE16DA74C35F2942DE369B1401B043E0 + 0265FBA9D58FD443BDDA3FF27805F6B6415F078E211FCBD1D60BE0A29EEDDB14 + FB59B9967C8A8FAAB6317FBE7605B5D547B5B5EBF46FA06F84BF0E833BDF8B7F + 4F1BB69B469B31B001CB95768F9FBFB7DA168F2BD5AF4E446DBEF336A9FFE641 + 7CBE7CDF79ACB6B6BF0545FED14F80DE18F5DB0680DBFAB5A966305D6A633EA8 + E654915FFD56659F63D3807D87A67501E0D07763E91FC76A67928FAAE1F4FF07 + F8F674027A9261910000000049454E44AE426082 + } + end + end + object paSync: TPanel + Left = 0 + Height = 428 + Top = 0 + Width = 728 + Align = alClient + Anchors = [akTop, akLeft, akRight] + TabOrder = 0 + object Label8: TLabel + Left = 152 + Height = 32 + Top = 32 + Width = 105 + Alignment = taRightJustify + AutoSize = False + Caption = 'Node Status:' + Font.Height = -16 + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblNodeStatus: TLabel + Left = 264 + Height = 32 + Top = 32 + Width = 451 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Initialising' + Font.Height = -16 + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object btnBack: TSpeedButton + Left = 14 + Height = 74 + Top = 8 + Width = 132 + Flat = True + Glyph.Data = { + 368C0000424D368C000000000000360000002800000080000000460000000100 + 200000000000008C000064000000640000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000FFFF012196F7442193F6792094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094F67C2094 + F67C2094F67C2094F67C2193F6792093F74700FFFF0100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000000000FF + FF012194F69E2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F7A400FFFF01000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000007FBF041F8FEF100000 + 0000000000000000000000000000000000000000000000000000000000002496 + FA382294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2393F740000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002193F5871F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002394 + F7652294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F86E000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002392F5502294F6FE1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F8262293F7F02294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000001C8DFE092194F6D22294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F6C92393F9322393F9322393F9322393F9322393F9322393F9322393 + F9322393F9322393F9322393F9322393F9322393F9322393F9322393F9322393 + F9322393F9322393F9322393F9322393F9322393F9322393F9322393F9322393 + F9322393F9322393F9322393F9322393F9322393F9322393F9322393F9322393 + F9322393F9322393F9322393F9322393F9322393F9322393F9322393F9322393 + F9322393F9322393F9322393F9322393F9322393F9322393F9322393F9322393 + F9322393F9322393F9322393F9322393F9322294F7C22294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F7A52294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F7AB0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002195F89F2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002195F76A2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2393F7830000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002193F8792294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002496F5382194F6F92294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2196F7440000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002292F63B2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002190 + F3172193F6E42294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F7E300AAFF030000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000AAFF032293F6DF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000007FFF042194 + F6BD2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293 + F870000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002393F7662294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002193F5892294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7CC3F7F + FF04000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001F94F4182294F7602393F6982294 + F6BF2294F7D52393F6DF2394F6D72393F7C32293F69D2293F7681E92F7210000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000AAFF032194F7C62294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002394F84F2293F6FE2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F6E91E92F7210000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000007FFF022293F5512294F6BF2294F6FB2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F6FD2394 + F6CB2293F7613399FF0500000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002395F61D2294 + F7E52294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002297F8252193F6F22294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F7E62193F92D000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002393FA392294F7CD2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2293F6DB2195F84D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000001F92 + F8282193F7E22294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000178BFE0B2193F6D32294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F6BD2597FE1B00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000AAFF032294 + F7862293F6FC2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2393F6FE2394F7A01C8DFE0900000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00001F94F4182193F8B72294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000FFFF012293F7A42294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F6FD2294F7C72295F852007FFF020000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000001F9FFF082294F6B12394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2294F7C9268CF214000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000FFFF012195F84D2194F7C42294F6FD2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F76C2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7AE2292F6762193 + F6552395F61D0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000003399FF052292F6B82394F7FF2294 + F7F12294F8E62294F8E62294F8E62393F7E72394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2293F7CF2299FF0F0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002597F51B2193F5532294F6752294 + F7AA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000001F91F5382194F6F92294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002194F6972394F7FF2394F7FF2293 + F68E0000FF010000FF010000FF010000FF012393F7E12394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2293F8B7007F + FF02000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002296F3162293F6E62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002191F5542394F6FE2394F7FF2394F7FF2294 + F7C2000000000000000000000000000000002293F5A92394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2294 + F675000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000003FBF + FF042194F8BD2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002789EB0D2293F7E62394F7FF2394F7FF2394F7FF2294 + F7F200AAFF030000000000000000000000002393F6742394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2294 + F6F42290F61E0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002294 + F5862294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F6792394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2193F92D0000000000000000000000002491F63F2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2293F69D0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002295F5522293 + F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002AAAFF062393F6E82394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2192F7630000000000000000000000001E96F0112393F6FD2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2393F6F72699F21400000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002297F8252293F6F02294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002294F84A2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F897000000000000000000000000000000002293F6D82394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2393F56D00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001999FF0A2193F6D42294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002193F6962394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2393F6CC000000000000000000000000000000002393F69F2394F7FF2394 + F7FF2394F7EE2294F7F22394F7FF2394F7FF2294F7F12394F7EF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2294F8B800000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000FF012194F7A52294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002293F7D42394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2393F6F72AAAFF060000000000000000000000002192F76A2394F7FF2293 + F7F31C8DFE092299FF1E2394F6FD2394F6FD1D93F51A2789EB0D2294F6F42394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2293F7ED007FFF02000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002295F7682294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002AAAFF062393F6F62394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2192F5360000000000000000000000002491F4312293F6DD2294 + F6DC2491F215007FFF022394F7CA2293F6DD1F94F930000000002294F7AF2293 + F6DD2293F6DD2294F8DE2394F7F42394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF1E8EF419000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002395F63A2193F6FA2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002699F2142394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2392F56C00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002AAAFF062094F5372293F6AA2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF1F91F538000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002292F7422294F6FD2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002290F61E2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2293F5A100000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002294F87D2394F7FF2394 + F7FF2394F7FF2394F7FF2395F341000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002294F67C2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00001E98F4192394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2293F6D500000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000003FBFFF042293F7E22394 + F7FF2394F7FF2394F7FF2192F63D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000055AA032294F7B12294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00001999FF0A2294F6FB2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2294F6FB2A94E90C0000000000000000000000002397F7402295 + F8B72295F8B72295F8B72295F8B72295F8B72295F8B72295F8B72295F8B72293 + F6B72193F6961F94F418000000000000000000000000000000002394F7AD2394 + F7FF2394F7FF2394F7FF2193F826000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001C9BFE122293F6DD2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000007FFF022294F6E52394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2391F7410000000000000000000000002193F32D2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2395F7A6000000000000000000000000000000002395F7AB2394 + F7FF2394F7FF2394F7F72A7FFF06000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000001F94F9302194F7F92294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002393F6AD2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2294F8750000000000000000000000002491FE072394 + F6F62394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F8C4000000000000000000000000000000002395F7AB2394 + F7FF2394F7FF2294F7CE00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002293F7612294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002294F7672394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2393F5C32194F65B2194F65B2194F65B2194F65B2293 + F6DF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2395F7C3000000000000000000000000000000002395F7AB2394 + F7FF2394F7FF2393F58A00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002294 + F69C2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002190F3172294F6F92394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2294F8C0000000000000000000000000000000002395F7AB2394 + F7FF2394F7FF1F94F93000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000001C8D + FE092193F6CC2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F7A52394F7FF2293F6FA2393F7802294F8752294 + F8752294F8752294F8752294F8752294F8752294F8752294F8752294F8752294 + F8752294F8752294F8752294F8752294F8752294F8752294F8752294F8752294 + F8752294F8752296F958000000000000000000000000000000002393F6B52394 + F7FF2294F7C60000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002596F7222293F7EE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000001F95F8292294F6FB2394F7FF2394F7AE007FFF020000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002296FE162294F7F42394 + F7FF2393F7470000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002292F8492294F7FD2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002295F78E2394F7FF2394F7FF2195F7870000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001E96FF112293F7C02394F7FF2294 + F7AF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F7832294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000001999FF0A2393F6D32394F7FF2393F6FE2192 + F7631C8DFE121C8DFE121C8DFE121C8DFE121C8DFE121C8DFE121C8DFE121C8D + FE121C8DFE121C8DFE122491FE0E00000000007FFF041C8DFE121F8FFF100000 + 0000007FFF022893FE132297FA3B2293F7852394F7E92394F7FF2393F7E72190 + FE17000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000AAFF032293F6B92294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F54F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002394F7242294F6EA2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2294F7E500FFFF012491F51C2394F6FE2294F6F81999 + E50A2491FE072393F6F72394F7FF2394F7FF2394F7FF2393F7F52391F63A0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002093 + F7472294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002491F2152293F7E02294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6B22292F5842395 + F65E1E92F7210000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000001F94F4302294F7EA2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F6FD2293F6582295F5522394F7FC2394F7FF2294 + F56E2193F7452293F6F32394F7FF2394F7FF2394F6F62393F747000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002094F61F2094F65D2195F7822193 + F6AF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002192F5362193F6FA2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F6FE2194F6CD2193F95A00FFFF010000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002394F7242293 + F6D42394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2293F7E42094F53700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000FF012094F6562294F7C92293F6FE2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002393F7662294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2193F7C32299F61E00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000000178B + FE0B2394F7902394F6FD2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F6FE2394F7A72491F2150000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002793FE1A2194F6BD2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000FFFF012294F7A32294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7E82491F431000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002193F32D2293F7A92393F6FC2394F7FF2394F7FF2394F7FF2394 + F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2394F7FF2393 + F6FE2294F6BA2294F63C00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002290 + F32C2194F7E52294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000001C8DFE092293 + F6D12294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F6EB2394F0240000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000238CED1D2293F8702294F6B82293F6F22394 + F6FE2394F7FF2394F7FF2394F7FF2394F7FE2394F6F62293F7C02394F67A2193 + F826000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002094F61F2293 + F7E72294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002193 + F1262294F7EF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6CF3F7F + FF04000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000AAFF031E8E + F4192193F92D2192F5362092F42F2491F51C3FBFFF0400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000AAFF032294F7C92294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002392F5502294F6FE2294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F872000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002293F7682294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002294F7862294F7FF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F7E400AAFF030000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000AAFF032293F7E02294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000003F7FFF042194F6BF2294F7FF2294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2193F7450000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F63C2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002491F2152193F7E42294F7FF2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2292F7840000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002194F67A2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002395FA3A2194F7F92294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F7AB0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002195F69F2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F86E2294F7FF1F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7C92391F5332391F5332391F5332391F5332391F5332391F5332391 + F5332391F5332391F5332391F5332391F5332391F5332391F5332391F5332391 + F5332391F5332391F5332391F5332391F5332391F5332391F5332391F5332391 + F5332391F5332391F5332391F5332391F5332391F5332391F5332391F5332391 + F5332391F5332391F5332391F5332391F5332391F5332391F5332391F5332391 + F5332391F5332391F5332391F5332391F5332391F5332391F5332391F5332391 + F5332391F5332391F5332391F5332391F5332294F7C22294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002194F7A51F94F9300000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002A94FF0C1F94F4180000 + 0000000000000000000000000000000000000000000000000000000000002294 + F7672294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F56F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002393 + F5662294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F86E000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002496 + FA382294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2393F740000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000000000FF + FF012194F69E2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F7A400FFFF01000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000FFFF012193F7452294F6782193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193F67B2193 + F67B2193F67B2193F67B2294F8782093F74700FFFF0100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000 + } + OnClick = btnBackClick + end + object GroupBox1: TGroupBox + Left = 14 + Height = 327 + Top = 88 + Width = 701 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Network Details' + TabOrder = 0 + object lblTotalBlocksLabel: TLabel + Left = 74 + Height = 15 + Top = 24 + Width = 66 + Alignment = taRightJustify + Caption = 'Total Blocks:' + ParentColor = False + end + object lblTotalBlocksValue: TLabel + Left = 154 + Height = 15 + Top = 24 + Width = 18 + Caption = '000' + ParentColor = False + end + object lblBlockAgeLabel: TLabel + Left = 84 + Height = 15 + Top = 48 + Width = 56 + Alignment = taRightJustify + Caption = 'Block Age:' + ParentColor = False + end + object lblBlockAgeValue: TLabel + Left = 154 + Height = 15 + Top = 48 + Width = 87 + Caption = '000 seconds ago' + ParentColor = False + end + object lblPendingOperationsLabel: TLabel + Left = 32 + Height = 15 + Top = 72 + Width = 108 + Alignment = taRightJustify + Caption = 'Pending Operations:' + ParentColor = False + end + object lblPendingOperationsValue: TLabel + Left = 154 + Height = 15 + Top = 72 + Width = 18 + Caption = '000' + ParentColor = False + end + object lblTotalAccountsValue: TLabel + Left = 153 + Height = 15 + Top = 96 + Width = 18 + Caption = '000' + ParentColor = False + end + object lblProtocolVersion: TLabel + Left = 154 + Height = 15 + Top = 120 + Width = 18 + Caption = '000' + ParentColor = False + end + object lblNetProtocolVersion: TLabel + Left = 154 + Height = 15 + Top = 144 + Width = 18 + Caption = '000' + ParentColor = False + end + object lblCurrentDifficultyCaption2: TLabel + Left = 32 + Height = 15 + Top = 144 + Width = 108 + Alignment = taRightJustify + Caption = 'Net Protocol Version' + ParentColor = False + end + object lblCurrentDifficultyCaption1: TLabel + Left = 54 + Height = 15 + Top = 120 + Width = 86 + Alignment = taRightJustify + Caption = 'Protocol Version' + ParentColor = False + end + object lblTotalAccountsLabel: TLabel + Left = 58 + Height = 15 + Top = 96 + Width = 82 + Alignment = taRightJustify + Caption = 'Total Accounts:' + ParentColor = False + end + object lblMiningStatusCaption: TLabel + Left = 270 + Height = 15 + Top = 24 + Width = 73 + Alignment = taRightJustify + Caption = 'Miner Clients:' + ParentColor = False + end + object lblMinersClientsValue: TLabel + Left = 357 + Height = 15 + Top = 24 + Width = 18 + Caption = '000' + ParentColor = False + end + object lblBlockTargetValue: TLabel + Left = 357 + Height = 15 + Top = 48 + Width = 18 + Caption = '000' + ParentColor = False + end + object lblBlockTargetLabel: TLabel + Left = 275 + Height = 15 + Top = 48 + Width = 68 + Alignment = taRightJustify + Caption = 'Block Target:' + ParentColor = False + end + object Label16: TLabel + Left = 277 + Height = 13 + Top = 72 + Width = 66 + Alignment = taRightJustify + Caption = 'Blocks Found:' + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object lblBlocksFound: TLabel + Left = 357 + Height = 13 + Hint = 'Blocks found while Miner is running...' + Top = 72 + Width = 18 + Caption = '000' + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + ParentShowHint = False + ShowHint = True + end + object lblTimeAverage: TLabel + Left = 357 + Height = 15 + Top = 96 + Width = 18 + Caption = '000' + ParentColor = False + end + object Label4: TLabel + Left = 270 + Height = 15 + Top = 96 + Width = 74 + Alignment = taRightJustify + Caption = 'Time average:' + ParentColor = False + end + object lblTimeAverageAux: TLabel + Left = 357 + Height = 13 + Top = 120 + Width = 18 + Caption = '000' + Font.Color = clGray + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object lblReceivedMessages: TLabel + Cursor = crHandPoint + Left = 32 + Height = 120 + Top = 184 + Width = 651 + Anchors = [akTop, akLeft, akRight, akBottom] + AutoSize = False + Caption = 'Received Messages' + Font.Color = clRed + Font.Height = -19 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + Visible = False + OnClick = lblReceivedMessagesClick + end + end + end +end diff --git a/src/gui-experimental/UCTRLSyncronization.lfm b/src/gui-experimental/UCTRLSyncronization.lfm index 14bf37362..b28887a75 100644 --- a/src/gui-experimental/UCTRLSyncronization.lfm +++ b/src/gui-experimental/UCTRLSyncronization.lfm @@ -10,6 +10,7 @@ object CTRLSyncronization: TCTRLSyncronization ClientWidth = 728 OnCreate = FormCreate OnDestroy = FormDestroy + OnShow = FormShow object paSplash: TPanel Left = 0 Height = 428 @@ -45542,7 +45543,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblTotalBlocksLabel: TLabel Left = 74 Height = 15 - Top = 16 + Top = 24 Width = 66 Alignment = taRightJustify Caption = 'Total Blocks:' @@ -45551,7 +45552,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblTotalBlocksValue: TLabel Left = 154 Height = 15 - Top = 16 + Top = 24 Width = 18 Caption = '000' ParentColor = False @@ -45559,7 +45560,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblBlockAgeLabel: TLabel Left = 84 Height = 15 - Top = 40 + Top = 48 Width = 56 Alignment = taRightJustify Caption = 'Block Age:' @@ -45568,7 +45569,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblBlockAgeValue: TLabel Left = 154 Height = 15 - Top = 40 + Top = 48 Width = 87 Caption = '000 seconds ago' ParentColor = False @@ -45576,7 +45577,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblPendingOperationsLabel: TLabel Left = 32 Height = 15 - Top = 64 + Top = 72 Width = 108 Alignment = taRightJustify Caption = 'Pending Operations:' @@ -45585,7 +45586,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblPendingOperationsValue: TLabel Left = 154 Height = 15 - Top = 64 + Top = 72 Width = 18 Caption = '000' ParentColor = False @@ -45593,7 +45594,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblTotalAccountsValue: TLabel Left = 153 Height = 15 - Top = 88 + Top = 96 Width = 18 Caption = '000' ParentColor = False @@ -45601,7 +45602,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblProtocolVersion: TLabel Left = 154 Height = 15 - Top = 112 + Top = 120 Width = 18 Caption = '000' ParentColor = False @@ -45609,7 +45610,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblNetProtocolVersion: TLabel Left = 154 Height = 15 - Top = 136 + Top = 144 Width = 18 Caption = '000' ParentColor = False @@ -45617,7 +45618,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblCurrentDifficultyCaption2: TLabel Left = 32 Height = 15 - Top = 136 + Top = 144 Width = 108 Alignment = taRightJustify Caption = 'Net Protocol Version' @@ -45626,7 +45627,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblCurrentDifficultyCaption1: TLabel Left = 54 Height = 15 - Top = 112 + Top = 120 Width = 86 Alignment = taRightJustify Caption = 'Protocol Version' @@ -45635,7 +45636,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblTotalAccountsLabel: TLabel Left = 58 Height = 15 - Top = 88 + Top = 96 Width = 82 Alignment = taRightJustify Caption = 'Total Accounts:' @@ -45644,7 +45645,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblMiningStatusCaption: TLabel Left = 270 Height = 15 - Top = 16 + Top = 24 Width = 73 Alignment = taRightJustify Caption = 'Miner Clients:' @@ -45653,7 +45654,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblMinersClientsValue: TLabel Left = 357 Height = 15 - Top = 16 + Top = 24 Width = 18 Caption = '000' ParentColor = False @@ -45661,7 +45662,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblBlockTargetValue: TLabel Left = 357 Height = 15 - Top = 40 + Top = 48 Width = 18 Caption = '000' ParentColor = False @@ -45669,7 +45670,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblBlockTargetLabel: TLabel Left = 275 Height = 15 - Top = 40 + Top = 48 Width = 68 Alignment = taRightJustify Caption = 'Block Target:' @@ -45678,7 +45679,7 @@ object CTRLSyncronization: TCTRLSyncronization object Label16: TLabel Left = 277 Height = 13 - Top = 64 + Top = 72 Width = 66 Alignment = taRightJustify Caption = 'Blocks Found:' @@ -45692,7 +45693,7 @@ object CTRLSyncronization: TCTRLSyncronization Left = 357 Height = 13 Hint = 'Blocks found while Miner is running...' - Top = 64 + Top = 72 Width = 18 Caption = '000' Font.Color = clWindowText @@ -45706,7 +45707,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblTimeAverage: TLabel Left = 357 Height = 15 - Top = 88 + Top = 96 Width = 18 Caption = '000' ParentColor = False @@ -45714,7 +45715,7 @@ object CTRLSyncronization: TCTRLSyncronization object Label4: TLabel Left = 270 Height = 15 - Top = 88 + Top = 96 Width = 74 Alignment = taRightJustify Caption = 'Time average:' @@ -45723,7 +45724,7 @@ object CTRLSyncronization: TCTRLSyncronization object lblTimeAverageAux: TLabel Left = 357 Height = 13 - Top = 112 + Top = 120 Width = 18 Caption = '000' Font.Color = clGray @@ -45736,7 +45737,7 @@ object CTRLSyncronization: TCTRLSyncronization Cursor = crHandPoint Left = 32 Height = 120 - Top = 176 + Top = 184 Width = 651 Anchors = [akTop, akLeft, akRight, akBottom] AutoSize = False diff --git a/src/gui-experimental/UCTRLSyncronization.pas b/src/gui-experimental/UCTRLSyncronization.pas index ee08aa63a..ef880c4d5 100644 --- a/src/gui-experimental/UCTRLSyncronization.pas +++ b/src/gui-experimental/UCTRLSyncronization.pas @@ -13,18 +13,41 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Displays startup progress, synchronization state, network details and mining + statistics. Secondary node reads run on an owned worker and publish + completed snapshots to the view. } + +{ Public types in this unit + + Type | Purpose + --------------------+------------------------------------------------------------------------- + TCTRLSyncronization | Displays loading progress and current node/network statistics. + TSyncNodeSnapshot | Carries display values returned by a node-statistics read. +} interface {$I ..\config.inc} uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls, - StdCtrls, ComCtrls, Buttons, UCommon.UI, UNetProtocol, UBaseTypes; + Classes, SysUtils, Types, {$IFDEF FPC}FileUtil,{$ENDIF} Forms, Controls, Graphics, Dialogs, ExtCtrls, + StdCtrls, ComCtrls, Buttons, UCommon, UCommon.UI, UNetProtocol, UBaseTypes, UAccounts, UNode; type + TSyncNodeSnapshot = record + BlockCount: Cardinal; + PeerCount: Integer; + TotalBlocks, TotalAccounts, BlockAge, PendingOperations, BlockTarget: String; + TimeAverage, TimeAverageAux, ProtocolVersion, ChainStatus: String; + TimeAverageColour: TColor; + end; + TSyncNodeSnapshotReader = procedure(ANode: TNode; out ASnapshot: TSyncNodeSnapshot) of object; + { TCTRLSyncronization } TCTRLSyncronization = class(TApplicationForm) @@ -60,97 +83,424 @@ TCTRLSyncronization = class(TApplicationForm) procedure btnBackClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); + procedure FormShow(Sender: TObject); procedure lblReceivedMessagesClick(Sender:TObject); private FMinedBlocksCount: Integer; FShowSplash : boolean; FMessagesUnreadCount : Integer; - procedure OnLoading(Sender: TObject; const message : String; curPos, totalCount : Int64); + FLoadingText : TLabel; + FCancelLoading : TButton; + FSnapshotWorker : TThread; + FSnapshotNotify : TThreadNotify; + FSnapshotPending, FSnapshotStopping : Boolean; + FHasSecondarySnapshot: Boolean; + FSecondaryBlockCount: Cardinal; + FSecondaryRevision: Int64; + FWasSynchronizing: Boolean; + FLastActiveConnections : Integer; + FChainStatusText : String; + FHasNodeSnapshot : Boolean; + FSnapshotBlockCount : Cardinal; + FNodeSnapshot : TSyncNodeSnapshot; + FSnapshotChanged : TNotifyManyEvent; + FDisplayedSynchronizationStatus: TNodeSynchronizationStatus; + procedure StartNodeSnapshot; + procedure OnSnapshotCompleted(Sender: TObject); + procedure ApplyNodeSnapshot(const ASnapshot: TSyncNodeSnapshot); + procedure ApplySynchronizationStatus; + procedure OnSynchronizationChanged(Sender: TObject); + procedure UpdateMiningStatus; + {$IFNDEF FPC} + function Scale96ToForm(AValue: Integer): Integer; + {$ENDIF} + procedure LayoutLoadingControls(Sender : TObject); + procedure OnCancelLoading(Sender : TObject); + procedure OnLoading(Sender: TObject; const AProgress : TProgressInfo); procedure OnLoaded(Sender: TObject); + procedure OnStateChanged(Sender: TObject); procedure SetMinedBlocksCount(const Value: Integer); procedure SetShowSplash(ABool : boolean); - procedure SetMessagesNotificationText(const text : AnsiString); - function GetMessagesNotificationText : AnsiString; + procedure SetMessagesNotificationText(const text : String); + function GetMessagesNotificationText : String; procedure SetStatusText(AColour: TColor; AText: String); procedure UpdateNodeStatus; procedure UpdateBlockChainState; procedure OnAppStarted(Sender: TObject); procedure OnBlocksChanged(Sender: TObject); - procedure OnUIRefreshTimer(Sender: TObject); + procedure OnNetworkStatusChanged(Sender: TObject); + procedure OnMiningStatusChanged(Sender: TObject); procedure OnNodeMessageEvent(NetConnection: TNetConnection; MessageData: String); protected procedure ActivateFirstTime; override; + // Invoked by the owned worker. Overrides must not touch UI controls. + procedure ReadSecondarySnapshot(ANode: TNode; out ASnapshot: TSyncNodeSnapshot); virtual; public + class function CurrentNodeStatusText: String; static; + // Hosted forms may become visible through their parent panel without a + // native activation. Refresh immediately, retaining the last snapshot if + // the node is busy; its owned task notifies completion without UI polling. + procedure RefreshNodeState; + procedure ClearMessagesNotification; + property ChainStatusText : String read FChainStatusText; + property HasNodeSnapshot : Boolean read FHasNodeSnapshot; + property SnapshotBlockCount : Cardinal read FSnapshotBlockCount; + property SnapshotChanged : TNotifyManyEvent read FSnapshotChanged; + property DisplayedSynchronizationStatus: TNodeSynchronizationStatus read FDisplayedSynchronizationStatus; property MinedBlocksCount : Integer read FMinedBlocksCount write SetMinedBlocksCount; property ShowSplash : boolean read FShowSplash write SetShowSplash; - property MessagesNotificationText : AnsiString read GetMessagesNotificationText write SetMessagesNotificationText; + property MessagesNotificationText : String read GetMessagesNotificationText write SetMessagesNotificationText; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses - UCommon, UTime, UConst, UAccounts, UNode, UBlockChain, - UUserInterface; + UTime, UConst, UBlockChain, UProgressDisplay, + UUserInterface, UThread, ULog; + +type + TSyncNodeSnapshotThread = class(TPCThread) + private + FNode: TNode; + FNotify: TThreadNotify; + FOnCompleted: TNotifyEvent; + FReadSnapshot: TSyncNodeSnapshotReader; + FCancelRequested: Boolean; + protected + procedure BCExecute; override; + public + Snapshot: TSyncNodeSnapshot; + Succeeded: Boolean; + Revision: Int64; + ErrorMessage: String; + constructor Create(ANode: TNode; ANotify: TThreadNotify; AOnCompleted: TNotifyEvent; AReadSnapshot: TSyncNodeSnapshotReader; ARevision: Int64); + function WasCanceled: Boolean; + procedure Cancel; + end; + +{$IFNDEF FPC} +type + TImageAccess = class(TImage); + +function TCTRLSyncronization.Scale96ToForm(AValue: Integer): Integer; +begin + Result := ScaleValue(AValue); +end; +{$ENDIF} + +procedure ReadNodeSnapshot(ANode: TNode; out ASnapshot: TSyncNodeSnapshot); +var + LStatus: TNodeSynchronizationStatus; + LDeviation, LAverage: Double; + LAverages: array[0..5] of Double; + LWindows: array[0..5] of Cardinal; + I: Integer; +begin + LStatus := ANode.GetSynchronizationStatus; + ASnapshot.BlockCount := LStatus.LocalBlockCount; + ASnapshot.PeerCount := LStatus.PeerCount; + if ASnapshot.BlockCount > 0 then begin + ASnapshot.TotalBlocks := IntToStr(ASnapshot.BlockCount) + ' (0..' + + IntToStr(ASnapshot.BlockCount - 1) + ')'; + ASnapshot.BlockAge := UnixTimeToLocalElapsedTime(LStatus.LastBlockTimestamp); + end else begin + ASnapshot.TotalBlocks := '(none)'; + ASnapshot.BlockAge := '(none)'; + end; + ASnapshot.TotalAccounts := UIntToStr(UInt64(LStatus.LocalBlockCount) * CT_AccountsPerBlock); + ASnapshot.PendingOperations := IntToStr(LStatus.PendingOperationsCount); + ASnapshot.BlockTarget := IntToHex(LStatus.CompactTarget, 8); + ASnapshot.ProtocolVersion := Format('%d (%d)', + [LStatus.ProtocolVersion, CT_BlockChain_Protocol_Available]); + LWindows[0] := CT_CalcNewTargetBlocksAverage; + LWindows[1] := CT_CalcNewTargetBlocksAverage * 2; + LWindows[2] := (CT_CalcNewTargetBlocksAverage * 3) div 2; + LWindows[3] := (CT_CalcNewTargetBlocksAverage div 4) * 3; + LWindows[4] := CT_CalcNewTargetBlocksAverage div 2; + LWindows[5] := CT_CalcNewTargetBlocksAverage div 4; + for I := 0 to High(LWindows) do begin + if TThread.CheckTerminated then Abort; + LAverages[I] := ANode.Bank.GetActualTargetSecondsAverage(LWindows[I]); + end; + if TThread.CheckTerminated then Abort; + LAverage := LAverages[0]; + LDeviation := (CT_NewLineSecondsAvg - LAverage) / CT_NewLineSecondsAvg; + ASnapshot.TimeAverage := 'Last ' + IntToStr(CT_CalcNewTargetBlocksAverage) + ': ' + + FormatFloat('0.0', LAverage) + ' sec. (Optimal ' + IntToStr(CT_NewLineSecondsAvg) + + 's) Deviation ' + FormatFloat('0.00%', LDeviation * 100); + if LAverage >= CT_NewLineSecondsAvg then ASnapshot.TimeAverageColour := clNavy + else ASnapshot.TimeAverageColour := clOlive; + ASnapshot.TimeAverageAux := Format('Last %d: %s sec. - %d: %s sec. - %d: %s sec. - %d: %s sec. - %d: %s sec.', [ + CT_CalcNewTargetBlocksAverage * 2, FormatFloat('0.0', LAverages[1]), + (CT_CalcNewTargetBlocksAverage * 3) div 2, FormatFloat('0.0', LAverages[2]), + (CT_CalcNewTargetBlocksAverage div 4) * 3, FormatFloat('0.0', LAverages[3]), + CT_CalcNewTargetBlocksAverage div 2, FormatFloat('0.0', LAverages[4]), + CT_CalcNewTargetBlocksAverage div 4, FormatFloat('0.0', LAverages[5])]); + // Secondary averages never supply committed-tip or connection presentation. + ASnapshot.ChainStatus := ''; +end; + +constructor TSyncNodeSnapshotThread.Create(ANode: TNode; ANotify: TThreadNotify; AOnCompleted: TNotifyEvent; AReadSnapshot: TSyncNodeSnapshotReader; ARevision: Int64); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FNotify := ANotify; + FOnCompleted := AOnCompleted; + FReadSnapshot := AReadSnapshot; + Revision := ARevision; +end; + +procedure TSyncNodeSnapshotThread.BCExecute; +begin + try + if not Terminated then begin + // No mempool lock may span a bank/safebox read or its disk I/O. + FReadSnapshot(FNode, Snapshot); + Succeeded := not Terminated; + end; + except + on E: Exception do + if not Terminated then ErrorMessage := E.ClassName + ': ' + E.Message; + end; + // Canceled workers still retire their owned slot, without publishing values. + FNotify.Invoke(Self, FOnCompleted); +end; + +function TSyncNodeSnapshotThread.WasCanceled: Boolean; +begin + Result := FCancelRequested; +end; + +procedure TSyncNodeSnapshotThread.Cancel; +begin + // TPCThread marks even normal completion Terminated in its finalizer. + // Only this owner-issued flag distinguishes a canceled result at publication. + FCancelRequested := True; + Terminate; +end; + +procedure TCTRLSyncronization.ReadSecondarySnapshot(ANode: TNode; out ASnapshot: TSyncNodeSnapshot); +begin + ReadNodeSnapshot(ANode, ASnapshot); +end; procedure TCTRLSyncronization.FormCreate(Sender: TObject); begin + {$IFNDEF FPC} + // Keep the previous frame visible until VCL has painted all status children. + // The themed parent-background path bypasses a panel's own buffering, so + // these solid-background details containers must paint their own background. + DoubleBuffered := true; + paSync.DoubleBuffered := true; + paSync.ParentBackground := false; + GroupBox1.DoubleBuffered := true; + GroupBox1.ParentBackground := false; + // Paint the transparent progress label and artwork in the same VCL buffer. + paSplash.DoubleBuffered := true; + {$ENDIF} TUserInterface.AppStarted.Add(OnAppStarted); TUserInterface.Loading.Add(OnLoading); + TUserInterface.Loaded.Add(OnLoaded); + TUserInterface.StateChanged.Add(OnStateChanged); TUserInterface.BlocksChanged.Add(OnBlocksChanged); - TUserInterface.UIRefreshTimer.Add(OnUIRefreshTimer); + TUserInterface.SynchronizationChanged.Add(OnSynchronizationChanged); + TUserInterface.NetStatisticsChanged.Add(OnNetworkStatusChanged); + TUserInterface.NetConnectionsUpdated.Add(OnNetworkStatusChanged); + TUserInterface.MiningStatusChanged.Add(OnMiningStatusChanged); TUserInterface.NodeMessageEvent.Add(OnNodeMessageEvent); + FSnapshotNotify := TThreadNotify.Create(TThread.CurrentThread); + FLastActiveConnections := -1; FMessagesUnreadCount := 0; + FLoadingText := TLabel.Create(Self); + FLoadingText.Name := 'lblLoadingStatus'; + FLoadingText.Parent := paSplash; + FLoadingText.AutoSize := false; + FLoadingText.ParentFont := false; + FLoadingText.Font.Color := clWhite; + FLoadingText.Font.Size := 16; + FLoadingText.Font.Style := [fsBold]; + FLoadingText.Alignment := taCenter; + FLoadingText.Layout := tlCenter; + FLoadingText.WordWrap := true; + FLoadingText.Transparent := true; + FLoadingText.Anchors := [akLeft, akRight]; + FLoadingText.Caption := 'Loading local blockchain...'; + FCancelLoading := TButton.Create(Self); + FCancelLoading.Name := 'btnCancelLoading'; + FCancelLoading.Parent := paSplash; + FCancelLoading.SetBounds(paSplash.ClientWidth - 128, paSplash.ClientHeight - 48, 112, 32); + FCancelLoading.Anchors := [akRight, akBottom]; + FCancelLoading.Caption := 'Cancel startup'; + FCancelLoading.OnClick := OnCancelLoading; + paSplash.OnResize := LayoutLoadingControls; + {$IFDEF FPC} + imgSplash.OnResize := LayoutLoadingControls; + {$ELSE} + TImageAccess(imgSplash).OnResize := LayoutLoadingControls; + {$ENDIF} + LayoutLoadingControls(paSplash); +end; + +procedure TCTRLSyncronization.LayoutLoadingControls(Sender : TObject); +var + LImageRect : TRect; + LMargin, LTextHeight, LTop, LMaximumTop : Integer; +begin + if (not Assigned(FLoadingText)) or (not Assigned(FCancelLoading)) then Exit; + LMargin := Scale96ToForm(16); + FCancelLoading.SetBounds(paSplash.ClientWidth - Scale96ToForm(112) - LMargin, + paSplash.ClientHeight - Scale96ToForm(32) - LMargin, + Scale96ToForm(112), Scale96ToForm(32)); + // The existing artwork's logo ends at y=353 in its 630-pixel source image. + // Use the actual image rectangle so the text follows the centered logo. + {$IFDEF FPC} + LImageRect := imgSplash.DestRect; + {$ELSE} + LImageRect := TImageAccess(imgSplash).DestRect; + {$ENDIF} + LTextHeight := Scale96ToForm(56); + LTop := imgSplash.Top + LImageRect.Top + + ((LImageRect.Bottom - LImageRect.Top) * 353 div 630) + LMargin; + LMaximumTop := FCancelLoading.Top - LTextHeight - Scale96ToForm(12); + if LTop > LMaximumTop then LTop := LMaximumTop; + if LTop < 0 then LTop := 0; + FLoadingText.SetBounds(LMargin, LTop, paSplash.ClientWidth - LMargin * 2, LTextHeight); +end; + +procedure TCTRLSyncronization.OnCancelLoading(Sender : TObject); +begin + if Assigned(Application.MainForm) then + Application.MainForm.Close; end; procedure TCTRLSyncronization.FormDestroy(Sender: TObject); begin + FSnapshotStopping := True; TUserInterface.AppStarted.Remove(OnAppStarted); TUserInterface.Loading.Remove (OnLoading); + TUserInterface.Loaded.Remove(OnLoaded); + TUserInterface.StateChanged.Remove(OnStateChanged); TUserInterface.BlocksChanged.Remove(OnBlocksChanged); - TUserInterface.UIRefreshTimer.Remove(OnUIRefreshTimer); + TUserInterface.SynchronizationChanged.Remove(OnSynchronizationChanged); + TUserInterface.NetStatisticsChanged.Remove(OnNetworkStatusChanged); + TUserInterface.NetConnectionsUpdated.Remove(OnNetworkStatusChanged); + TUserInterface.MiningStatusChanged.Remove(OnMiningStatusChanged); TUserInterface.NodeMessageEvent.Remove(OnNodeMessageEvent); + // Producers must stop before their owned main-thread dispatcher is disposed. + if Assigned(FSnapshotWorker) then begin + TSyncNodeSnapshotThread(FSnapshotWorker).Cancel; + FSnapshotWorker.WaitFor; + FreeAndNil(FSnapshotWorker); + end; + FreeAndNil(FSnapshotNotify); end; procedure TCTRLSyncronization.ActivateFirstTime; begin - ShowSplash := true; + ShowSplash := TUserInterface.State = uisLoading; + RefreshNodeState; +end; + +procedure TCTRLSyncronization.FormShow(Sender: TObject); +begin + RefreshNodeState; +end; + +procedure TCTRLSyncronization.RefreshNodeState; +begin + // Hosted view entry renders the last completed values immediately. All new + // bank/safebox reads belong to the worker, even when the mempool is free. + if FNodeSnapshot.TimeAverage <> '' then begin + lblTimeAverage.Caption := FNodeSnapshot.TimeAverage; + lblTimeAverage.Font.Color := FNodeSnapshot.TimeAverageColour; + lblTimeAverageAux.Caption := FNodeSnapshot.TimeAverageAux; + end else begin + lblTimeAverage.Caption := 'Calculating...'; + lblTimeAverageAux.Caption := ''; + end; + ApplySynchronizationStatus; + UpdateBlockChainState; end; procedure TCTRLSyncronization.OnAppStarted(Sender: TObject); begin + ShowSplash := TUserInterface.State = uisLoading; + btnBack.Enabled := not (TUserInterface.State in [uisLoading, uisError]); UpdateBlockChainState; end; -procedure TCTRLSyncronization.OnLoading(Sender: TObject; const message : String; curPos, totalCount : Int64); +procedure TCTRLSyncronization.OnLoading(Sender: TObject; const AProgress : TProgressInfo); var pct : String; begin - if (totalCount>0) then - pct := Format('%.2f',[curPos*100/totalCount])+'%' - else - pct := ''; - pct := message + ' ' + pct; + if TUserInterface.State=uisError then begin + UpdateNodeStatus; + Exit; + end; + pct := FormatProgressInfo(AProgress); + if FLoadingText.Caption <> pct then + FLoadingText.Caption := pct; SetStatusText(clGreen, pct); end; procedure TCTRLSyncronization.OnLoaded(Sender: TObject); begin + if TUserInterface.State=uisError then begin + ShowSplash := False; + btnBack.Enabled := False; + UpdateNodeStatus; + Exit; + end; + ShowSplash := false; btnBack.Enabled:=true; + // Capture the restored values before the normal loaded handler enables + // networking. A first visit during a busy block batch can then retain these + // values instead of exposing the designer's placeholder text. + RefreshNodeState; TUserInterface.ShowWallet; end; +procedure TCTRLSyncronization.OnStateChanged(Sender: TObject); +begin + ShowSplash := TUserInterface.State = uisLoading; + btnBack.Enabled := not (TUserInterface.State in [uisLoading, uisError]); + UpdateBlockChainState; +end; + procedure TCTRLSyncronization.OnBlocksChanged(Sender: TObject); begin + // A committed replacement can change a tip without changing its height. + FHasSecondarySnapshot := False; + Inc(FSecondaryRevision); UpdateBlockChainState; end; -procedure TCTRLSyncronization.OnUIRefreshTimer(Sender: TObject); +procedure TCTRLSyncronization.OnNetworkStatusChanged(Sender: TObject); +var + LConnections: Integer; begin + if FSnapshotStopping or TUserInterface.Stopping then Exit; + UpdateNodeStatus; + // Byte counters are frequent events. Only a connection-count change needs + // another blockchain-validity snapshot; traffic does not reread the bank. + LConnections := TNetData.NetData.NetStatistics.ActiveConnections; + if LConnections <> FLastActiveConnections then begin UpdateBlockChainState; - UpdateNodeStatus; + end; +end; + +procedure TCTRLSyncronization.OnMiningStatusChanged(Sender: TObject); +begin + if FSnapshotStopping or TUserInterface.Stopping then Exit; + UpdateMiningStatus; end; procedure TCTRLSyncronization.OnNodeMessageEvent(NetConnection: TNetConnection; MessageData: String); @@ -164,27 +514,25 @@ procedure TCTRLSyncronization.OnNodeMessageEvent(NetConnection: TNetConnection; procedure TCTRLSyncronization.SetShowSplash(ABool : boolean); begin - if ABool = FShowSplash then exit; FShowSplash := ABool; + FCancelLoading.Enabled := FShowSplash; if FShowSplash then begin - TUserInterface.Enabled := false; paSplash.Visible:= true; paSync.Visible := false; end else begin - TUserInterface.Enabled := true; paSplash.Visible:= false; paSync.Visible := true; end; end; -function TCTRLSyncronization.GetMessagesNotificationText : AnsiString; +function TCTRLSyncronization.GetMessagesNotificationText : String; begin Result := lblReceivedMessages.Caption; end; -procedure TCTRLSyncronization.SetMessagesNotificationText(const text : AnsiString); +procedure TCTRLSyncronization.SetMessagesNotificationText(const text : String); begin - if (text = '') then lblReceivedMessages.Visible := false; + lblReceivedMessages.Visible := text <> ''; lblReceivedMessages.Caption := text; end; @@ -195,69 +543,214 @@ procedure TCTRLSyncronization.SetStatusText(AColour: TColor; AText: String); end; procedure TCTRLSyncronization.UpdateNodeStatus; -Var status : AnsiString; begin - if not TUserInterface.Started then exit; + if (not TUserInterface.Started) or (not IsVisible) then exit; // State text case TUserInterface.State of - uisLoading: ShowSplash := false; // text set by OnLoading + uisLoading: ShowSplash := true; // text set by OnLoading uisLoaded: SetStatusText(clGreen, 'Loaded'); uisDiscoveringPeers: SetStatusText(clGreen, 'Discovering Peers'); - uisSyncronizingBlockchain: SetStatusText(clGreen, 'Syncronizing'); + // ApplySynchronizationStatus owns detailed progress. Byte-stat updates + // must not replace it with a generic caption and invalidate it twice. + uisSyncronizingBlockchain: ; uisActive: SetStatusText(clGreen, 'Active'); uisIsolated: SetStatusText(clRed, 'Isolated'); - uisError: SetStatusText(clRed, Format('Error: %s', [TUserInterface.StateText])); + uisDisconnected: begin + if TUserInterface.Offline then + SetStatusText(clNavy, 'Offline mode') + else + SetStatusText(clRed, 'Disconnected'); + end; + uisError: begin + FChainStatusText := TUserInterface.StateText; + SetStatusText(clRed, Format('Error: %s', [TUserInterface.StateText])); + end; end; + // A disk restore owns the bank/mempool locks and may deliver UI progress. + // Do not wait for those locks from the UI or query partially restored state. + if TUserInterface.State in [uisLoading, uisError] then Exit; + // Protocol labels - lblProtocolVersion.Caption := Format('%d (%d)', [TUserInterface.Node.Bank.SafeBox.CurrentProtocol,CT_BlockChain_Protocol_Available]); lblNetProtocolVersion.Caption := Format('%d (%d)', [CT_NetProtocol_Version, CT_NetProtocol_Available]); end; +class function TCTRLSyncronization.CurrentNodeStatusText: String; +begin + // Banner and details consume the same event-owned application state. This + // formatter never reads the bank or acquires a pending-operation lock. + case TUserInterface.State of + uisLoading: Result := 'Initializing'; + uisLoaded: Result := 'Loaded'; + uisDiscoveringPeers: Result := 'Discovering Peers'; + uisSyncronizingBlockchain: Result := 'Synchronizing'; + uisActive: Result := 'Active'; + uisIsolated: Result := 'Isolated'; + uisDisconnected: + if TUserInterface.Offline then Result := 'Offline mode' + else Result := 'Disconnected'; + uisError: Result := Format('Error: %s', [TUserInterface.StateText]); + else + Result := ''; + end; +end; + +procedure TCTRLSyncronization.ApplySynchronizationStatus; +var S: TNodeSynchronizationStatus; P: String; +begin + if not Assigned(TUserInterface.Node) then Exit; + S := TUserInterface.SynchronizationStatus; + FDisplayedSynchronizationStatus := S; + if S.StorageFailure.HasFailure then begin + FChainStatusText := FormatStorageFailure(S.StorageFailure); + // Details and the banner use the same error projection; the footer keeps + // the underlying diagnostic without a duplicated status prefix. + SetStatusText(clRed,'Error: '+FChainStatusText); + Exit; + end; + FHasNodeSnapshot := True; + FSnapshotBlockCount := S.LocalBlockCount; + if S.LocalBlockCount > 0 then begin + lblTotalBlocksValue.Caption := IntToStr(S.LocalBlockCount) + ' (0..' + IntToStr(S.LocalBlockCount - 1) + ')'; + lblBlockAgeValue.Caption := UnixTimeToLocalElapsedTime(S.LastBlockTimestamp); + end else begin + lblTotalBlocksValue.Caption := '(none)'; + lblBlockAgeValue.Caption := '(none)'; + end; + lblTotalAccountsValue.Caption := UIntToStr(UInt64(S.LocalBlockCount) * CT_AccountsPerBlock); + if S.HasMempoolSummary then begin + lblPendingOperationsValue.Caption := IntToStr(S.PendingOperationsCount); + lblBlockTargetValue.Caption := IntToHex(S.CompactTarget, 8); + end; + lblProtocolVersion.Caption := Format('%d (%d)', [S.ProtocolVersion, CT_BlockChain_Protocol_Available]); + if S.State = nssSynchronizing then begin + P := FormatProgressInfo(S.Progress); + if P = '' then P := 'Synchronizing'; + SetStatusText(clGreen, P); + end; + // Footer and basic labels are one projection of this exact cached commit. + // A slow/paused secondary reader cannot leave the footer on an older tip. + if S.LocalBlockCount = 0 then + FChainStatusText := 'Blocks: 0 | No local blockchain' + else + FChainStatusText := Format('Blocks: %d | Last account time: %s', + [S.LocalBlockCount, FormatDateTime('dd/mm/yyyy hh:nn:ss', + UnivDateTime2LocalDateTime(UnixToUnivDateTime(S.LastBlockTimestamp)))]); + if not S.NetworkActive then FChainStatusText := FChainStatusText + ' | Network off' + else if S.PeerCount = 0 then FChainStatusText := FChainStatusText + ' | No connections'; +end; + +procedure TCTRLSyncronization.OnSynchronizationChanged(Sender: TObject); +var LSync: Boolean; +begin + if FSnapshotStopping or TUserInterface.Stopping then Exit; + LSync := TUserInterface.SynchronizationStatus.State = nssSynchronizing; + if FWasSynchronizing and not LSync then begin + FHasSecondarySnapshot := False; + Inc(FSecondaryRevision); + end; + FWasSynchronizing := LSync; + UpdateBlockChainState; +end; + procedure TCTRLSyncronization.UpdateBlockChainState; -Var - f, favg : real; - LMemPool : TPCOperationsComp; +var S: TNodeSynchronizationStatus; begin - if not TUserInterface.Started then exit; + if FSnapshotStopping or (not TUserInterface.Started) or TUserInterface.Stopping then Exit; UpdateNodeStatus; - if Assigned(TUserInterface.Node) then begin - if TUserInterface.Node.Bank.BlocksCount>0 then begin - lblTotalBlocksValue.Caption := Inttostr(TUserInterface.Node.Bank.BlocksCount)+' (0..'+Inttostr(TUserInterface.Node.Bank.BlocksCount-1)+')'; ; - end else lblTotalBlocksValue.Caption := '(none)'; - lblTotalAccountsValue.Caption := Inttostr(TUserInterface.Node.Bank.AccountsCount); - lblBlockAgeValue.Caption := UnixTimeToLocalElapsedTime(TUserInterface.Node.Bank.LastOperationBlock.timestamp); - LMemPool := TUserInterface.Node.LockMempoolRead; - try - lblPendingOperationsValue.Caption := Inttostr(LMemPool.Count); - lblBlockTargetValue.Caption := InttoHex(LMemPool.OperationBlock.compact_target,8); - finally - TUserInterface.Node.UnlockMempoolRead; - end; - favg := TUserInterface.Node.Bank.GetActualTargetSecondsAverage(CT_CalcNewTargetBlocksAverage); - f := (CT_NewLineSecondsAvg - favg) / CT_NewLineSecondsAvg; - lblTimeAverage.Caption := 'Last '+Inttostr(CT_CalcNewTargetBlocksAverage)+': '+FormatFloat('0.0',favg)+' sec. (Optimal '+Inttostr(CT_NewLineSecondsAvg)+'s) Deviation '+FormatFloat('0.00%',f*100); - if favg>=CT_NewLineSecondsAvg then begin - lblTimeAverage.Font.Color := clNavy; - end else begin - lblTimeAverage.Font.Color := clOlive; - end; - lblTimeAverageAux.Caption := Format('Last %d: %s sec. - %d: %s sec. - %d: %s sec. - %d: %s sec. - %d: %s sec.',[ - CT_CalcNewTargetBlocksAverage * 2 ,FormatFloat('0.0',TUserInterface.Node.Bank.GetActualTargetSecondsAverage(CT_CalcNewTargetBlocksAverage * 2)), - ((CT_CalcNewTargetBlocksAverage * 3) DIV 2) ,FormatFloat('0.0',TUserInterface.Node.Bank.GetActualTargetSecondsAverage((CT_CalcNewTargetBlocksAverage * 3) DIV 2)), - ((CT_CalcNewTargetBlocksAverage DIV 4)*3),FormatFloat('0.0',TUserInterface.Node.Bank.GetActualTargetSecondsAverage(((CT_CalcNewTargetBlocksAverage DIV 4)*3))), - CT_CalcNewTargetBlocksAverage DIV 2,FormatFloat('0.0',TUserInterface.Node.Bank.GetActualTargetSecondsAverage(CT_CalcNewTargetBlocksAverage DIV 2)), - CT_CalcNewTargetBlocksAverage DIV 4,FormatFloat('0.0',TUserInterface.Node.Bank.GetActualTargetSecondsAverage(CT_CalcNewTargetBlocksAverage DIV 4))]); - end else begin - lblTotalBlocksValue.Caption := ''; - lblTotalAccountsValue.Caption := ''; - lblBlockAgeValue.Caption := ''; - lblPendingOperationsValue.Caption := ''; - lblBlockTargetValue.Caption := ''; - lblTimeAverage.Caption := ''; - lblTimeAverageAux.Caption := ''; + UpdateMiningStatus; + if TUserInterface.State=uisError then begin + FDisplayedSynchronizationStatus := TUserInterface.SynchronizationStatus; + FSnapshotPending := False; + if Assigned(FSnapshotWorker) then TSyncNodeSnapshotThread(FSnapshotWorker).Cancel; + Exit; + end; + if TUserInterface.State=uisLoading then Exit; + ApplySynchronizationStatus; + FSnapshotChanged.Invoke(Self); + S := TUserInterface.SynchronizationStatus; + if S.State = nssSynchronizing then begin + FSnapshotPending := False; + if Assigned(FSnapshotWorker) then TSyncNodeSnapshotThread(FSnapshotWorker).Cancel; + Exit; + end; + if FLastActiveConnections <> S.PeerCount then begin + FHasSecondarySnapshot := False; + Inc(FSecondaryRevision); + end; + FLastActiveConnections := S.PeerCount; + if (not FHasSecondarySnapshot) or (FSecondaryBlockCount <> S.LocalBlockCount) then begin + FSnapshotPending := True; + StartNodeSnapshot; + end; +end; + +procedure TCTRLSyncronization.StartNodeSnapshot; +var + LNode: TNode; +begin + if FSnapshotStopping or TUserInterface.Stopping or + Assigned(FSnapshotWorker) or (not FSnapshotPending) then Exit; + if TUserInterface.SynchronizationStatus.State = nssSynchronizing then Exit; + if FHasSecondarySnapshot and + (FSecondaryBlockCount = TUserInterface.SynchronizationStatus.LocalBlockCount) then begin + FSnapshotPending := False; + Exit; + end; + FSnapshotPending := False; + LNode := TUserInterface.Node; + if not Assigned(LNode) then Exit; + // SafeBox has a separate mutex with no public try-lock. Acquiring the node + // lock on the UI would not make its subsequent bank getters nonblocking. + FSnapshotWorker := TSyncNodeSnapshotThread.Create(LNode, FSnapshotNotify, + OnSnapshotCompleted, ReadSecondarySnapshot, FSecondaryRevision); + FSnapshotWorker.Start; +end; + +procedure TCTRLSyncronization.OnSnapshotCompleted(Sender: TObject); +var + LWorker: TSyncNodeSnapshotThread; +begin + if FSnapshotStopping or TUserInterface.Stopping or (Sender <> FSnapshotWorker) then Exit; + LWorker := TSyncNodeSnapshotThread(FSnapshotWorker); + FSnapshotWorker := nil; + // Completion is queued without Synchronize. Joining here cannot wait for a + // UI callback on the worker; it only finishes the producer's final teardown. + LWorker.WaitFor; + try + if LWorker.Succeeded and (not LWorker.WasCanceled) and + (LWorker.Revision = FSecondaryRevision) and + (TUserInterface.SynchronizationStatus.State <> nssSynchronizing) and + (LWorker.Snapshot.PeerCount = TUserInterface.SynchronizationStatus.PeerCount) and + (LWorker.Snapshot.BlockCount = TUserInterface.SynchronizationStatus.LocalBlockCount) then + ApplyNodeSnapshot(LWorker.Snapshot) + else if LWorker.ErrorMessage <> '' then + TLog.NewLog(lterror, ClassName, LWorker.ErrorMessage); + finally + LWorker.Free; end; + // Requests received during this read coalesce; completion itself is not a + // new status request and cannot create a self-sustaining refresh loop. + StartNodeSnapshot; +end; + +procedure TCTRLSyncronization.ApplyNodeSnapshot(const ASnapshot: TSyncNodeSnapshot); +begin + FHasSecondarySnapshot := True; + FSecondaryBlockCount := ASnapshot.BlockCount; + FNodeSnapshot := ASnapshot; + // This worker owns averages only. Basic values and the footer are always + // rendered together from the current cached synchronization record below. + lblTimeAverage.Caption := ASnapshot.TimeAverage; + lblTimeAverage.Font.Color := ASnapshot.TimeAverageColour; + lblTimeAverageAux.Caption := ASnapshot.TimeAverageAux; + ApplySynchronizationStatus; + FSnapshotChanged.Invoke(Self); +end; + +procedure TCTRLSyncronization.UpdateMiningStatus; +begin if (Assigned(TUserInterface.PoolMiningServer)) And (TUserInterface.PoolMiningServer.Active) then begin If TUserInterface.PoolMiningServer.ClientsCount>0 then begin lblMinersClientsValue.Caption := IntToStr(TUserInterface.PoolMiningServer.ClientsCount)+' connected JSON-RPC clients'; @@ -284,11 +777,17 @@ procedure TCTRLSyncronization.SetMinedBlocksCount(const Value: Integer); procedure TCTRLSyncronization.lblReceivedMessagesClick(Sender:TObject); begin - lblReceivedMessages.Visible := false; - lblReceivedMessages.Caption := text; + ClearMessagesNotification; TUserInterface.ShowMessagesForm; end; +procedure TCTRLSyncronization.ClearMessagesNotification; +begin + lblReceivedMessages.Visible := false; + lblReceivedMessages.Caption := ''; + FMessagesUnreadCount := 0; +end; + procedure TCTRLSyncronization.btnBackClick(Sender: TObject); begin TUserInterface.ShowWallet; diff --git a/src/gui-experimental/UCTRLWallet.dfm b/src/gui-experimental/UCTRLWallet.dfm new file mode 100644 index 000000000..455a185c5 --- /dev/null +++ b/src/gui-experimental/UCTRLWallet.dfm @@ -0,0 +1,212 @@ +object CTRLWallet: TCTRLWallet + Left = -7 + Height = 571 + Top = 2 + Width = 1151 + ActiveControl = cbAccounts + BorderStyle = bsNone + Caption = 'CTRLWallet' + ClientHeight = 571 + ClientWidth = 1151 + OnCreate = FormCreate + OnDestroy = FormDestroy + OnResize = FormResize + OnShow = FormResize + object PairSplitter1: TPanel + BevelOuter = bvNone + Cursor = crDefault + Left = 0 + Height = 571 + Top = 0 + Width = 1151 + Align = alClient + object PairSplitterSide1: TPanel + BevelOuter = bvNone + Align = alLeft + Cursor = crArrow + Left = 0 + Height = 571 + Top = 0 + Width = 360 + object GroupBox1: TGroupBox + Left = 8 + Height = 105 + Top = 8 + Width = 344 + Anchors = [akTop, akLeft, akRight] + Caption = 'Overview' + TabOrder = 0 + object Label1: TLabel + Left = 16 + Height = 14 + Top = 35 + Width = 68 + Caption = 'Total PASC' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object Label2: TLabel + Left = 16 + Height = 14 + Top = 67 + Width = 69 + Caption = 'Total PASA' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblTotalPASA: TLabel + Left = 220 + Height = 15 + Top = 67 + Width = 108 + Alignment = taRightJustify + Anchors = [akTop, akRight] + AutoSize = False + Caption = '-' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblTotalPASC: TLabel + Left = 220 + Height = 15 + Top = 35 + Width = 106 + Alignment = taRightJustify + Anchors = [akTop, akRight] + AutoSize = False + Caption = '-' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + end + object gpMyAccounts: TGroupBox + Left = 8 + Height = 440 + Top = 120 + Width = 344 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'My Accounts' + TabOrder = 1 + object cbAccounts: TComboBox + Left = 8 + Height = 22 + Top = 20 + Width = 320 + Anchors = [akTop, akLeft, akRight] + ItemHeight = 14 + ItemIndex = 0 + Items.Strings = ( + 'Show All' + 'Show non-zero balances' + 'Get my first account!' + ) + OnChange = cbAccountsChange + Style = csDropDownList + TabOrder = 0 + Text = 'Show All' + end + object paAccounts: TPanel + Left = 8 + Height = 365 + Top = 52 + Width = 320 + Anchors = [akTop, akLeft, akRight, akBottom] + BevelOuter = bvNone + Caption = 'MY ACCOUNT PANEL' + ParentColor = False + TabOrder = 1 + end + end + end + object PairSplitterSide2: TPanel + BevelOuter = bvNone + Align = alClient + Cursor = crArrow + Left = 365 + Height = 571 + Top = 0 + Width = 786 + object gpRecentOps: TGroupBox + Left = 8 + Height = 552 + Top = 8 + Width = 776 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Recent Operations' + TabOrder = 0 + object paOperations: TPanel + Left = 8 + Height = 513 + Top = 16 + Width = 760 + Anchors = [akTop, akLeft, akRight, akBottom] + BevelOuter = bvNone + Caption = 'OPERATIONS PANEL' + ParentColor = False + TabOrder = 0 + end + end + end + end + object mnuAccountsPopup: TPopupMenu + Left = 768 + Top = 64 + object miSendPASC: TMenuItem + Caption = 'Send PASC' + OnClick = miSendPASCClick + end + object miChangeKey: TMenuItem + Caption = 'Change Key' + OnClick = miChangeKeyClick + end + object miChangeAccountInfo: TMenuItem + Caption = 'Change Account Info' + OnClick = miChangeAccountInfoClick + end + object miAccountsMarket: TMenuItem + Caption = 'Account Market' + object miEnlistAccountsForSale: TMenuItem + Caption = 'Enlist Account(s) For Sale' + OnClick = miEnlistAccountsForSaleClick + end + object miDelistAccountsFromSale: TMenuItem + Caption = 'Delist Account(s) From Sale' + OnClick = miDelistAccountsFromSaleClick + end + end + object miBuyAccount: TMenuItem + Caption = 'Buy Account' + OnClick = miBuyAccountClick + end + object miSep1: TMenuItem + Caption = '-' + end + object miAccountInfo: TMenuItem + Caption = 'Account Info' + OnClick = miAccountInfoClick + end + end + object mnuOperationsPopup: TPopupMenu + Left = 896 + Top = 64 + object miCopyOphash: TMenuItem + Caption = 'Copy OPHASH' + OnClick = miCopyOphashClick + end + object miSep2: TMenuItem + Caption = '-' + end + object miOperationInfo: TMenuItem + Caption = 'Details' + OnClick = miOperationInfoClick + end + end + object mnuFirstAccountPopup: TPopupMenu + Left = 1024 + Top = 64 + end +end diff --git a/src/gui-experimental/UCTRLWallet.lfm b/src/gui-experimental/UCTRLWallet.lfm index 5a583ac30..08905a6c6 100644 --- a/src/gui-experimental/UCTRLWallet.lfm +++ b/src/gui-experimental/UCTRLWallet.lfm @@ -9,7 +9,9 @@ object CTRLWallet: TCTRLWallet ClientHeight = 571 ClientWidth = 1151 OnCreate = FormCreate + OnDestroy = FormDestroy OnResize = FormResize + OnShow = FormResize LCLVersion = '2.0.10.0' object PairSplitter1: TPairSplitter Cursor = crDefault @@ -40,7 +42,7 @@ object CTRLWallet: TCTRLWallet object Label1: TLabel Left = 16 Height = 14 - Top = 16 + Top = 19 Width = 68 Caption = 'Total PASC' Font.Style = [fsBold] @@ -50,7 +52,7 @@ object CTRLWallet: TCTRLWallet object Label2: TLabel Left = 16 Height = 14 - Top = 48 + Top = 51 Width = 69 Caption = 'Total PASA' Font.Style = [fsBold] @@ -60,12 +62,12 @@ object CTRLWallet: TCTRLWallet object lblTotalPASA: TLabel Left = 220 Height = 15 - Top = 48 + Top = 51 Width = 108 Alignment = taRightJustify Anchors = [akTop, akRight] AutoSize = False - Caption = '0' + Caption = '-' Font.Style = [fsBold] ParentColor = False ParentFont = False @@ -73,12 +75,12 @@ object CTRLWallet: TCTRLWallet object lblTotalPASC: TLabel Left = 220 Height = 15 - Top = 16 + Top = 19 Width = 106 Alignment = taRightJustify Anchors = [akTop, akRight] AutoSize = False - Caption = '0' + Caption = '-' Font.Style = [fsBold] ParentColor = False ParentFont = False @@ -208,7 +210,7 @@ object CTRLWallet: TCTRLWallet Caption = '-' end object miOperationInfo: TMenuItem - Caption = 'Operation Info' + Caption = 'Details' OnClick = miOperationInfoClick end end diff --git a/src/gui-experimental/UCTRLWallet.pas b/src/gui-experimental/UCTRLWallet.pas index cc1b2af5c..de876ebd5 100644 --- a/src/gui-experimental/UCTRLWallet.pas +++ b/src/gui-experimental/UCTRLWallet.pas @@ -13,16 +13,40 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Hosts wallet account and operation grids, key selection, balances and + operation actions. Node synchronization pauses queries; completed reads + update the grids and overview. } + +{ Public types in this unit + + Type | Purpose + -----------------------------+------------------------------------------------------------------------- + TCTRLWallet | Hosts account/history grids, wallet totals and operation actions. + TCTRLWalletAccountsMode | Selects the account grid or the first-account prompt. + TCTRLWalletOperationsHistory | Selects seven-day, thirty-day or all available stored history. + TCTRLWalletOperationsMode | Selects history for chosen accounts or the whole wallet. +} interface uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, Menus, - ExtCtrls, PairSplitter, Buttons, UVisualGrid, UCommon, UCommon.UI, Generics.Collections, + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + Classes, SysUtils, Types, Math, Messages, {$IFDEF FPC}FileUtil, LCLType, LCLIntf, LMessages,{$ENDIF} Forms, Controls, Graphics, Dialogs, StdCtrls, Menus, + ExtCtrls, {$IFDEF FPC}PairSplitter,{$ENDIF} Buttons, ActnList, UVisualGrid, UCommon, UCommon.UI, Generics.Collections, ULog, UAccounts, UPCDataTypes, UDataSources, UNode, UCoreObjects, UCoreUtils, UCTRLNoAccount; +const + CM_WalletControlKeysChanged = {$IFDEF FPC}LM_USER{$ELSE}WM_USER{$ENDIF} + 101; + type { TCTRLWallet } @@ -52,9 +76,15 @@ TCTRLWallet = class(TApplicationForm) miAccountInfo: TMenuItem; miSep1: TMenuItem; miSep2: TMenuItem; + {$IFDEF FPC} PairSplitter1: TPairSplitter; PairSplitterSide1: TPairSplitterSide; PairSplitterSide2: TPairSplitterSide; + {$ELSE} + PairSplitter1: TPanel; + PairSplitterSide1: TPanel; + PairSplitterSide2: TPanel; + {$ENDIF} paAccounts: TPanel; paOperations: TPanel; mnuAccountsPopup: TPopupMenu; @@ -74,39 +104,67 @@ TCTRLWallet = class(TApplicationForm) procedure miSendPASCClick(Sender: TObject); procedure miEnlistAccountsForSaleClick(Sender: TObject); procedure miDelistAccountsFromSaleClick(Sender: TObject); + private type + TAccountKeyBoxList = TObjectList>; private FNodeNotifyEvents: TNodeNotifyEvents; - FBalanceUpdatedEvent : TThrottledEvent; + FStopping: Boolean; + FQueriesSuspended: Boolean; + FStorageErrorDisplayed: Boolean; FAccountsMode: TCTRLWalletAccountsMode; FOperationsMode: TCTRLWalletOperationsMode; FOperationsHistory: TCTRLWalletOperationsHistory; FCTRLNoAccounts: TCTRLNoAccounts; FAccountsGrid: TVisualGrid; FOperationsGrid: TVisualGrid; + FOperationDetailsAction: TAction; + FOperationDetailsButton: TBitBtn; + FLayoutOperationActionsActive, FScalingOperationActions: Boolean; FBalance : TBalanceSummary; + FHasBalanceSummary: Boolean; + FAccountKeyBoxes: TAccountKeyBoxList; FAccountsDataSource: TMyAccountsDataSource; FOperationsDataSource: TAccountsOperationsDataSource; + function TryGetSelectedOperationHash(out AOperationHash: String): Boolean; procedure SetAccountsMode(AMode: TCTRLWalletAccountsMode); procedure SetOperationsMode(AMode: TCTRLWalletOperationsMode); procedure SetOperationsHistory(AHistory: TCTRLWalletOperationsHistory); procedure RefreshMyAccountsCombo; - procedure RefreshBalances; procedure RefreshTotals; procedure RefreshAccountsGrid; procedure RefreshOperationsGrid; + procedure OnSynchronizationChanged(Sender: TObject; const AStatus: TNodeSynchronizationStatus); + procedure OnBalanceSummaryCompleted(Sender: TObject); + procedure UpdateOperationDetailsAction(Sender: TObject); + procedure LayoutOperationActions(Sender: TObject); + procedure OnOperationsGridFinishedUpdating(Sender: TObject); function GetSelectedAccounts : TArray; + procedure LayoutOverviewLabels; + procedure CM_PrivateKeysChanged(var AMsg: TMessage); message CM_WalletControlKeysChanged; protected + {$IFDEF FPC} + procedure ChangeScale(M, D: Integer); override; + {$ELSE} + procedure ChangeScale(M, D: Integer; isDpiChange: Boolean); override; + {$ENDIF} + // Keep source configuration, invalidation and grid wiring in this control; + // descendants can supply another typed backend through the same lifecycle. + function CreateAccountsDataSource: TMyAccountsDataSource; virtual; + function CreateOperationsDataSource: TAccountsOperationsDataSource; virtual; + procedure ShowOperationDetails(const AOperationHash: String); virtual; + property AccountsGrid: TVisualGrid read FAccountsGrid; + property OperationsGrid: TVisualGrid read FOperationsGrid; + procedure RefreshBalances; procedure ActivateFirstTime; override; procedure OnPrivateKeysChanged(Sender: TObject); procedure OnUserKeyActivityDetected(Sender: TObject); - procedure OnUserBalanceChanged(Sender: TObject); procedure OnNodeBlocksChanged(Sender: TObject); procedure OnNodeNewOperation(Sender: TObject); - procedure OnAccountsSelected(Sender: TObject; constref ASelection: TVisualGridSelection); + procedure OnAccountsSelected(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); procedure OnAccountsGridFinishedUpdating(Sender: TObject); - procedure OnOperationSelected(Sender: TObject; constref ASelection: TVisualGridSelection); - procedure OnPrepareAccountPopupMenu(Sender: TObject; constref ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); - procedure OnPrepareOperationsPopupMenu(Sender: TObject; constref ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); + procedure OnOperationSelected(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); + procedure OnPrepareAccountPopupMenu(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); + procedure OnPrepareOperationsPopupMenu(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); public property SelectedAccounts : TArray read GetSelectedAccounts; property AccountsMode: TCTRLWalletAccountsMode read FAccountsMode write SetAccountsMode; @@ -118,35 +176,56 @@ implementation uses UUserInterface, UCellRenderers, UBlockChain, UWallet, UCrypto, - UMemory, Generics.Defaults, UCommon.Data, UCommon.Collections, UWIZOperation; + Generics.Defaults, UCommon.Data, UCommon.Collections, UWIZOperation, + Variants, Clipbrd; + +type + TOverviewGroupBoxAccess = class(TGroupBox); +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TCTRLWallet } procedure TCTRLWallet.FormCreate(Sender: TObject); var cmbDuration: TComboBox; + LOperationActions: TPanel; + LActionList: TActionList; begin + {$IFNDEF FPC} + with TSplitter.Create(Self) do begin + Parent := PairSplitter1; + Left := PairSplitterSide1.Width; + Width := 5; + Align := alLeft; + end; + {$ENDIF} + // The combo borrows its key boxes. Its native HWND can be destroyed before + // this embedded form, so ownership must survive outside Items.Objects. + FAccountKeyBoxes := TAccountKeyBoxList(AutoDispose(TAccountKeyBoxList.Create(True))); + // event registrations FNodeNotifyEvents := TNodeNotifyEvents.Create(self); FNodeNotifyEvents.WatchKeys := TWallet.Keys.AccountsKeyList; FNodeNotifyEvents.OnKeyActivity:= OnUserKeyActivityDetected; FNodeNotifyEvents.OnBlocksChanged := OnNodeBlocksChanged; FNodeNotifyEvents.OnOperationsChanged := OnNodeNewOperation; + FNodeNotifyEvents.OnSynchronizationChanged := OnSynchronizationChanged; TWallet.Keys.OnChanged.Add(OnPrivateKeysChanged); TWallet.Keys.AccountsKeyList.ClearAccountKeyChanges; // XXXXX CLEAR BUFFER on start - FBalanceUpdatedEvent := TThrottledEvent.Create(Self); - FBalanceUpdatedEvent.Mode := temNotifyOnEventBurstStartAndFinished; - FBalanceUpdatedEvent.Interval := TTimeSpan.FromSeconds(10); - FBalanceUpdatedEvent.Add(OnUserBalanceChanged); // fields - FAccountsDataSource := TMyAccountsDataSource.Create(Self); + FAccountsDataSource := CreateAccountsDataSource; + FAccountsDataSource.OnBalanceSummaryCompleted := OnBalanceSummaryCompleted; + FAccountsDataSource.SnapshotCacheEnabled := true; FAccountsDataSource.IncludePending := true; - FAccountsDataSource.BalancePointer := @FBalance; - FOperationsDataSource := TAccountsOperationsDataSource.Create(Self); - FOperationsDataSource.Accounts := TCoreTool.GetUserAccountNumbers; + FOperationsDataSource := CreateOperationsDataSource; + FOperationsDataSource.SnapshotCacheEnabled := true; + FOperationsDataSource.UseWalletAccounts := true; FOperationsDataSource.BlockDepth:=TTimeSpan.FromDays(7).TotalBlockCount; FOperationsHistory := woh7Days; FOperationsMode:= womAllAccounts; @@ -279,6 +358,7 @@ procedure TCTRLWallet.FormCreate(Sender: TObject); Filters := SORTABLE_TEXT_FILTER; end; FOperationsGrid.OnSelection := OnOperationSelected; + FOperationsGrid.OnFinishedUpdating := OnOperationsGridFinishedUpdating; FOperationsGrid.OnPreparePopupMenu := OnPrepareOperationsPopupMenu; FOperationsGrid.Caption.Alignment := taCenter; FOperationsGrid.Caption.Text := 'All Account Operations'; @@ -287,9 +367,67 @@ procedure TCTRLWallet.FormCreate(Sender: TObject); // key combo RefreshMyAccountsCombo; - // duration combo + // Native controls need their parent hierarchy before creating window handles. + paAccounts.AddControlDockCenter(FAccountsGrid); + paOperations.AddControlDockCenter(FOperationsGrid); + + // Selection remains passive. A shared explicit action opens details from + // either the toolbar button or the selected row's context menu. + LActionList := TActionList.Create(Self); + FOperationDetailsAction := TAction.Create(Self); + FOperationDetailsAction.Name := 'OperationDetailsAction'; + FOperationDetailsAction.ActionList := LActionList; + FOperationDetailsAction.Caption := 'Details'; + FOperationDetailsAction.Hint := 'Details for the selected operation'; + FOperationDetailsAction.Enabled := False; + FOperationDetailsAction.OnExecute := miOperationInfoClick; + FOperationDetailsAction.OnUpdate := UpdateOperationDetailsAction; + miOperationInfo.Action := FOperationDetailsAction; + + LOperationActions := TPanel.Create(FOperationsGrid); + LOperationActions.Name := 'OperationActions'; + LOperationActions.BevelOuter := bvNone; + LOperationActions.Caption := ''; + LOperationActions.Height := 24; + // Keep both controls in the supported grid widget slot, so native scaling + // and header visibility treat them as a single component-owned region. cmbDuration := TComboBox.Create(FOperationsGrid); - cmbDuration.ReadOnly := True; + cmbDuration.Name := 'OperationsHistory'; + cmbDuration.Parent := LOperationActions; + cmbDuration.Align := alLeft; + cmbDuration.Style := csDropDownList; + FOperationDetailsButton := TBitBtn.Create(Self); + FOperationDetailsButton.Name := 'OperationDetailsButton'; + FOperationDetailsButton.Parent := LOperationActions; + FOperationDetailsButton.Align := alNone; + FOperationDetailsButton.Anchors := [akLeft,akTop]; + FOperationDetailsButton.Width := cmbDuration.Height; + FOperationDetailsButton.Action := FOperationDetailsAction; + FOperationDetailsButton.Caption := ''; + FOperationDetailsButton.Margin := -1; + FOperationDetailsButton.Spacing := 0; + FOperationDetailsButton.ShowHint := True; + // A small portable information glyph needs no extra runtime image file. + with FOperationDetailsButton.Glyph do begin + PixelFormat := pf24bit; + SetSize(16, 16); + Canvas.Brush.Color := clBtnFace; + Canvas.FillRect(Types.Rect(0, 0, 16, 16)); + Canvas.Pen.Color := clNavy; + Canvas.Brush.Style := bsClear; + Canvas.Ellipse(1, 1, 15, 15); + Canvas.Pen.Width := 2; + Canvas.MoveTo(8, 7); + Canvas.LineTo(8, 12); + Canvas.MoveTo(8, 4); + Canvas.LineTo(8, 6); + TransparentColor := clBtnFace; + Transparent := True; + end; + LOperationActions.Width := cmbDuration.Width + 4 + FOperationDetailsButton.Width; + FOperationsGrid.WidgetControl := LOperationActions; + LOperationActions.OnResize := LayoutOperationActions; + LayoutOperationActions(LOperationActions); cmbDuration.Items.BeginUpdate; try cmbDuration.AddItem('7 Days', TObject(woh7Days)); @@ -300,23 +438,40 @@ procedure TCTRLWallet.FormCreate(Sender: TObject); cmbDuration.ItemIndex := 0; end; cmbDuration.OnChange := cmbDurationChange; - FOperationsGrid.WidgetControl := cmbDuration; - - // NOTE: datasources are assigned to grid in FormResize - - // Add grid to panels - paAccounts.AddControlDockCenter(FAccountsGrid); - paOperations.AddControlDockCenter(FOperationsGrid); + // Node lifecycle owns query suspension. The operations grid's existing + // throttle coalesces rapid selection/filter requests to their latest input. + FAccountsGrid.RefreshThrottle.Mode := temNone; + FOperationsGrid.RefreshThrottle.Interval := TTimeSpan.FromMilliseconds(250); + FOperationsGrid.RefreshThrottle.Mode := temNotifyOnEventBurstFinished; + OnSynchronizationChanged(Self, TUserInterface.SynchronizationStatus); + // VCL does not publish the group box's resize event for form resources. + TOverviewGroupBoxAccess(GroupBox1).OnResize := FormResize; + // Assign each source once after the controls are configured and parented. + FormResize(Self); end; procedure TCTRLWallet.FormDestroy(Sender: TObject); begin - TWallet.Keys.OnChanged.Add(OnPrivateKeysChanged); + FStopping := True; + if Assigned(FAccountsDataSource) then + FAccountsDataSource.OnBalanceSummaryCompleted := nil; + if Assigned(TWallet.Keys) then + TWallet.Keys.OnChanged.Remove(OnPrivateKeysChanged); + if Assigned(FNodeNotifyEvents) then begin + FNodeNotifyEvents.OnKeyActivity := nil; + FNodeNotifyEvents.OnBlocksChanged := nil; + FNodeNotifyEvents.OnOperationsChanged := nil; + FNodeNotifyEvents.OnSynchronizationChanged := nil; + end; + // AutoDispose releases the owning key-box list even after VCL has already + // destroyed the ancestor HWND and discarded the combo's native item data. end; procedure TCTRLWallet.FormResize(Sender: TObject); -var x : integer; begin + if not Assigned(FAccountsGrid) or not Assigned(FOperationsGrid) then Exit; + LayoutOverviewLabels; + LayoutOperationActions(Self); // Grid data-sources are set here on "first form resize" in order to avoid // excessive datasource fetching during initialising sequence. Note, grid // refreshes on size changed, blockchain activity and when assigned to grid @@ -327,6 +482,90 @@ procedure TCTRLWallet.FormResize(Sender: TObject); FOperationsGrid.DataSource := FOperationsDataSource; end; +{$IFDEF FPC} +procedure TCTRLWallet.ChangeScale(M, D: Integer); +{$ELSE} +procedure TCTRLWallet.ChangeScale(M, D: Integer; isDpiChange: Boolean); +{$ENDIF} +var LWasScaling: Boolean; +begin + LWasScaling := FScalingOperationActions; + FScalingOperationActions := True; + try + {$IFDEF FPC} + inherited ChangeScale(M,D); + {$ELSE} + inherited ChangeScale(M,D,isDpiChange); + {$ENDIF} + finally + FScalingOperationActions := LWasScaling; + end; + if not FScalingOperationActions then LayoutOperationActions(Self); +end; + +procedure TCTRLWallet.LayoutOperationActions(Sender: TObject); +var LHistory: TComboBox; LPanel: TPanel; LHeight, LLeft: Integer; +begin + if FStopping or FLayoutOperationActionsActive or FScalingOperationActions or + not Assigned(FOperationsGrid) or not Assigned(FOperationDetailsButton) then Exit; + if not (FOperationDetailsButton.Parent is TPanel) then Exit; + LHistory := TComboBox(FOperationsGrid.FindComponent('OperationsHistory')); + if not Assigned(LHistory) then Exit; + LPanel := TPanel(FOperationDetailsButton.Parent); + FLayoutOperationActionsActive := True; + LPanel.DisableAlign; + try + // Native combo boxes choose their own height. Match the button to that + // actual height instead of stretching it to the header container. + LHeight := LHistory.Height; + LLeft := LHistory.Left+LHistory.Width+Max(1,MulDiv(4,PixelsPerInch,96)); + FOperationDetailsButton.SetBounds(LLeft,LHistory.Top,LHeight,LHeight); + LPanel.Height := LHistory.Top+LHeight; + LPanel.Width := LLeft+LHeight; + finally + LPanel.EnableAlign; + FLayoutOperationActionsActive := False; + end; +end; + +procedure TCTRLWallet.LayoutOverviewLabels; +var + LContent: Types.TRect; + LFirstHeight, LSecondHeight, LGap, LTop: Integer; +begin + if not HandleAllocated or not Assigned(GroupBox1) then Exit; + // VCL reserves its caption inside ClientRect; LCL already excludes it. + // Let the native group box define its content, then center both label pairs. + LContent := GroupBox1.ClientRect; + TOverviewGroupBoxAccess(GroupBox1).AdjustClientRect(LContent); + LFirstHeight := Max(Label1.Height, lblTotalPASC.Height); + LSecondHeight := Max(Label2.Height, lblTotalPASA.Height); + LGap := Min(MulDiv(17, PixelsPerInch, 96), + Max(0, LContent.Bottom - LContent.Top - LFirstHeight - LSecondHeight)); + LTop := LContent.Top + Max(0, + (LContent.Bottom - LContent.Top - LFirstHeight - LGap - LSecondHeight) div 2); + Label1.Top := LTop; + lblTotalPASC.Top := LTop; + Inc(LTop, LFirstHeight + LGap); + Label2.Top := LTop; + lblTotalPASA.Top := LTop; +end; + +function TCTRLWallet.CreateAccountsDataSource: TMyAccountsDataSource; +begin + Result := TMyAccountsDataSource.Create(Self); +end; + +function TCTRLWallet.CreateOperationsDataSource: TAccountsOperationsDataSource; +begin + Result := TAccountsOperationsDataSource.Create(Self); +end; + +procedure TCTRLWallet.ShowOperationDetails(const AOperationHash: String); +begin + TUserInterface.ShowOperationInfoDialog(Self, AOperationHash); +end; + procedure TCTRLWallet.ActivateFirstTime; begin // add first time-init here @@ -334,15 +573,36 @@ procedure TCTRLWallet.ActivateFirstTime; procedure TCTRLWallet.RefreshBalances; begin + if FStopping or TUserInterface.Stopping then Exit; + // Invalidation is atomic; only the grids' admitted deliveries start queries. + // Never defer this input behind visibility or an existing page worker. + FAccountsDataSource.InvalidateWalletData; + // The persisted key index does not emit every legacy key-activity event. + // Resolve current membership once on this worker refresh as well, so newly + // received or transferred accounts cannot remain absent from cached history. + FOperationsDataSource.InvalidateWalletAccounts; FAccountsGrid.RefreshGrid; FOperationsGrid.RefreshGrid; - // Note: RefreshTotals is called after FAccountsGrid finishes updating (since it uses it's internal data) + UpdateOperationDetailsAction(Self); + // Page completion requests the independent aggregate; its owned worker + // reports completion without polling or delaying the page. end; procedure TCTRLWallet.RefreshTotals; begin - lblTotalPASC.Caption := TAccountComp.FormatMoney(FBalance.TotalPASC); - lblTotalPASA.Caption := Format('%d', [FBalance.TotalPASA]); + // The node owns query suspension. A storage error retains the last totals + // until restart; ordinary synchronization resumes completed snapshots. + if FQueriesSuspended then Exit; + if FAccountsDataSource.TryGetBalanceSummary(FBalance) then begin + FHasBalanceSummary := True; + lblTotalPASC.Caption := TAccountComp.FormatMoney(FBalance.TotalPASC); + lblTotalPASA.Caption := Format('%d', [FBalance.TotalPASA]); + end else if not FHasBalanceSummary then begin + // Unknown is not a zero balance. Retain a completed total on later + // refreshes; only the first successful summary replaces these placeholders. + lblTotalPASC.Caption := '-'; + lblTotalPASA.Caption := '-'; + end; end; procedure TCTRLWallet.RefreshMyAccountsCombo; @@ -351,7 +611,21 @@ procedure TCTRLWallet.RefreshMyAccountsCombo; selectFirst, selectLast: boolean; last_key: TAccountKey; key: TWalletKey; - str: ansistring; + str: String; + + procedure AddKey(const ACaption: String; const AKey: TAccountKey); + var LBox: TBox; + begin + LBox := TBox.Create(AKey); + try + FAccountKeyBoxes.Add(LBox); + except + LBox.Free; + raise; + end; + cbAccounts.Items.AddObject(ACaption,LBox); + end; + begin // determine current selection if cbAccounts.ItemIndex >= 1 then begin @@ -372,10 +646,10 @@ procedure TCTRLWallet.RefreshMyAccountsCombo; // update combo items cbAccounts.items.BeginUpdate; try - // free existing items - for i := 0 to cbAccounts.Items.Count - 1 do - cbAccounts.Items.Objects[i].Free; + // Clear borrowed references before releasing their independently owned boxes. cbAccounts.Items.Clear; + FAccountKeyBoxes.Clear; + AddKey('Show All',Default(TAccountKey)); // add new items for i := 0 to TWallet.Keys.Count - 1 do begin // get i'th key @@ -389,10 +663,9 @@ procedure TCTRLWallet.RefreshMyAccountsCombo; end; if not Assigned(key.PrivateKey) then str := str + '(*)'; - cbAccounts.Items.AddObject(str, TBox.Create(key.AccountKey)); + AddKey(str,key.AccountKey); end; - cbAccounts.Items.InsertObject(0, 'Show All', TBox.Create); - cbAccounts.Items.AddObject('Get An Account', TBox.Create); + AddKey('Get An Account',Default(TAccountKey)); finally cbAccounts.Items.EndUpdate; end; @@ -402,6 +675,8 @@ procedure TCTRLWallet.RefreshMyAccountsCombo; else if selectLast then cbAccounts.ItemIndex := cbAccounts.Items.Count - 1 else begin + // The previously selected key may have been deleted from the wallet. + cbAccounts.ItemIndex := 0; for i := 1 to cbAccounts.Items.Count - 2 do begin if TAccountKeyEqualityComparer.AreEqual(TBox( cbAccounts.Items.Objects[i]).Value, last_key) then begin cbAccounts.ItemIndex := i; @@ -413,18 +688,30 @@ procedure TCTRLWallet.RefreshMyAccountsCombo; procedure TCTRLWallet.RefreshAccountsGrid; var - index: integer; + index, keyIndex: integer; sel: TBox; begin if Self.AccountsMode <> wamMyAccounts then exit; // showing getpasa if cbAccounts.ItemIndex = cbAccounts.Items.Count - 1 then exit; // not a key index := cbAccounts.ItemIndex; + if (index < 0) or (index >= cbAccounts.Items.Count) then begin + index := 0; + cbAccounts.ItemIndex := index; + end; + if index > 0 then begin + sel := TBox(cbAccounts.Items.Objects[index]); + keyIndex := TWallet.Keys.IndexOfAccountKey(sel.Value); + if keyIndex < 0 then begin + index := 0; + cbAccounts.ItemIndex := index; + end; + end; if index = 0 then begin gpMyAccounts.Caption := 'My Accounts'; - FAccountsDataSource.FilterKeys := TWallet.Keys.AccountsKeyList.ToArray; + FAccountsDataSource.FilterKeys := TArray(TWallet.Keys.AccountsKeyList.ToArray); end else begin sel := TBox(cbAccounts.Items.Objects[cbAccounts.ItemIndex]); - gpMyAccounts.Caption := Format('%s Accounts', [TWallet.Keys[TWallet.Keys.IndexOfAccountKey(sel.Value)].Name]); + gpMyAccounts.Caption := Format('%s Accounts', [TWallet.Keys[keyIndex].Name]); FAccountsDataSource.FilterKeys := TArray.Create(sel.Value); end; FAccountsGrid.RefreshGrid; @@ -432,30 +719,37 @@ procedure TCTRLWallet.RefreshAccountsGrid; procedure TCTRLWallet.RefreshOperationsGrid; begin + // The same provider is reconfigured below. Cooperatively retire its old + // query so obsolete history cannot delay or replace this selection's page. + FOperationsGrid.CancelActiveFetch; case FOperationsMode of womAllAccounts: begin FOperationsGrid.Caption.Text := ''; - FOperationsDataSource.Accounts := TCoreTool.GetUserAccountNumbers; + FOperationsDataSource.UseWalletAccounts := true; end; womSelectedAccounts: begin FOperationsGrid.Caption.Text := 'Selected Accounts'; + FOperationsDataSource.UseWalletAccounts := false; FOperationsDataSource.Accounts := SelectedAccounts; end else raise ENotSupportedException.Create(Format('AMode %d not supported', [integer(FOperationsMode)])); end; FOperationsGrid.RefreshGrid; + UpdateOperationDetailsAction(Self); end; -function TCTRLWallet.GetSelectedAccounts : TArray; - - function GetAccNoWithoutChecksum(constref ARow: variant): cardinal; - begin - Result := ARow.__KEY; - end; +function GetAccNoWithoutChecksum({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ARow: variant): cardinal; +begin + Result := ARow.__KEY; +end; +function TCTRLWallet.GetSelectedAccounts : TArray; +var + LAccountTransform: TGlobalTransformFunc; begin - Result := TListTool.Transform(FAccountsGrid.SelectedRows, GetAccNoWithoutChecksum); + LAccountTransform := GetAccNoWithoutChecksum; + Result := TListTool.Transform(FAccountsGrid.SelectedRows, LAccountTransform); end; procedure TCTRLWallet.SetAccountsMode(AMode: TCTRLWalletAccountsMode); @@ -471,7 +765,8 @@ procedure TCTRLWallet.SetAccountsMode(AMode: TCTRLWalletAccountsMode); begin // reset account combo cbAccounts.OnChange := nil; // disable event - cbAccounts.ItemIndex :=0; + if (cbAccounts.ItemIndex < 0) or (cbAccounts.ItemIndex >= cbAccounts.Items.Count - 1) then + cbAccounts.ItemIndex := 0; cbAccounts.OnChange := cbAccountsChange; // re-enable event // ensure on accounts panel if FAccountsGrid.Parent <> paAccounts then begin @@ -498,10 +793,6 @@ procedure TCTRLWallet.SetOperationsMode(AMode: TCTRLWalletOperationsMode); FUILock.Acquire; try FOperationsMode := AMode; - case AMode of - womSelectedAccounts: FOperationsDataSource.Accounts := SelectedAccounts; - womAllAccounts: FOperationsDataSource.Accounts := TCoreTool.GetUserAccountNumbers; - end; RefreshOperationsGrid; finally FUILock.Release; @@ -510,51 +801,115 @@ procedure TCTRLWallet.SetOperationsMode(AMode: TCTRLWalletOperationsMode); procedure TCTRLWallet.SetOperationsHistory(AHistory: TCTRLWalletOperationsHistory); begin + // Changing the history window supersedes an in-flight traversal as well. + FOperationsGrid.CancelActiveFetch; FOperationsHistory := AHistory; case FOperationsHistory of woh7Days: FOperationsDataSource.BlockDepth := TTimeSpan.FromDays(7).TotalBlockCount; woh30Days: FOperationsDataSource.BlockDepth := TTimeSpan.FromDays(30).TotalBlockCount; - wohFullHistory: FOperationsDataSource.BlockDepth := TTimeSpan.FromDays(10 * 365).TotalBlockCount; + // Maximum means the whole local chain, without an arbitrary age limit. + wohFullHistory: FOperationsDataSource.BlockDepth := High(Cardinal); end; FOperationsGrid.RefreshGrid; + UpdateOperationDetailsAction(Self); end; procedure TCTRLWallet.OnPrivateKeysChanged(Sender: TObject); begin + if FStopping or TUserInterface.Stopping then Exit; + if TThread.CurrentThread.ThreadID <> MainThreadID then begin + // RPC key imports notify on their worker. Posting keeps that producer + // independent and binds delivery to this control's native lifetime. + if HandleAllocated then PostMessage(Handle, CM_WalletControlKeysChanged, 0, 0); + Exit; + end; + FAccountsDataSource.InvalidateWalletData; + FOperationsDataSource.InvalidateWalletAccounts; RefreshMyAccountsCombo; + RefreshAccountsGrid; + RefreshOperationsGrid; end; -procedure TCTRLWallet.OnUserKeyActivityDetected(Sender: TObject); +procedure TCTRLWallet.CM_PrivateKeysChanged(var AMsg: TMessage); begin - // This handler is called every time a block is downloaded. If we refreshed GUI here it would - // result in severe back-end blocking and slow-down, as the GUI refreshed on every block during - // a syncronization. As a result, we throttle the GUI refresh via FBalanceUpdatedEvent which - // waits until 5 seconds of idle before calling OnUserBalanceChanged which then updates GUI. - FBalanceUpdatedEvent.Notify; + OnPrivateKeysChanged(nil); end; -procedure TCTRLWallet.OnUserBalanceChanged(Sender: TObject); +procedure TCTRLWallet.OnUserKeyActivityDetected(Sender: TObject); begin - // Invocation of this event hander is throttled by FBalanceUpdatedEvent. RefreshBalances; end; +procedure TCTRLWallet.OnSynchronizationChanged(Sender: TObject; const AStatus: TNodeSynchronizationStatus); +var LWasSuspended, LStorageError, LCardChanged: Boolean; + LTitle, LSubtitle: String; +begin + if FStopping or TUserInterface.Stopping then Exit; + LWasSuspended := FQueriesSuspended; + // A queued older status cannot resume reads after the first storage error. + // Only a new control/node lifetime may clear this restart-only suspension. + LStorageError := FStorageErrorDisplayed or AStatus.StorageFailure.HasFailure or + (AStatus.State=nssStorageError); + LCardChanged := (not LWasSuspended) or (FStorageErrorDisplayed<>LStorageError); + FStorageErrorDisplayed := LStorageError; + FQueriesSuspended := LStorageError or (AStatus.State=nssSynchronizing); + if not Assigned(FAccountsGrid) or not Assigned(FOperationsGrid) then Exit; + if FQueriesSuspended then begin + FAccountsGrid.RefreshSuspended := True; + FOperationsGrid.RefreshSuspended := True; + if LCardChanged then begin + FAccountsDataSource.InvalidateWalletData; + FAccountsDataSource.CancelBalanceSummary; + FOperationsDataSource.InvalidateWalletAccounts; + end; + if LStorageError then begin + LTitle := 'Storage error'; + LSubtitle := 'Restart required'; + end else begin + LTitle := 'Please Wait'; + LSubtitle := 'Synchronizing'; + end; + if LCardChanged or not FAccountsGrid.LoadingVisible then + FAccountsGrid.ShowLoading(LTitle,LSubtitle); + if LCardChanged or not FOperationsGrid.LoadingVisible then + FOperationsGrid.ShowLoading(LTitle,LSubtitle); + end else if LWasSuspended then begin + // Resume one latest request after ordinary synchronization, never after a + // storage failure. Its retained rows remain unavailable until restart. + FAccountsGrid.HideLoading; + FOperationsGrid.HideLoading; + FAccountsGrid.RefreshSuspended := False; + FOperationsGrid.RefreshSuspended := False; + end; + UpdateOperationDetailsAction(Self); +end; + + +procedure TCTRLWallet.OnBalanceSummaryCompleted(Sender: TObject); +begin + if FStopping or TUserInterface.Stopping then Exit; + // A newer input may have invalidated this worker while it was finishing. + // RefreshTotals retains the last summary until the current one is ready. + RefreshTotals; +end; + procedure TCTRLWallet.OnNodeBlocksChanged(Sender: TObject); begin - // TODO: play block sound chime + RefreshBalances; end; procedure TCTRLWallet.OnNodeNewOperation(Sender: TObject); begin - // TODO: play operation sound tick + RefreshBalances; end; procedure TCTRLWallet.OnAccountsGridFinishedUpdating(Sender: TObject); begin - RefreshTotals; // totals are updated by datasource, via a pointer + if FStopping or TUserInterface.Stopping then Exit; + RefreshTotals; end; -procedure TCTRLWallet.OnAccountsSelected(Sender: TObject; constref ASelection: TVisualGridSelection); +procedure TCTRLWallet.OnAccountsSelected(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); begin if ASelection.Page >= 0 then if FOperationsMode <> womSelectedAccounts then @@ -565,22 +920,24 @@ procedure TCTRLWallet.OnAccountsSelected(Sender: TObject; constref ASelection: T OperationsMode := womAllAccounts; end; -procedure TCTRLWallet.OnOperationSelected(Sender: TObject; constref ASelection: TVisualGridSelection); +procedure TCTRLWallet.OnOperationSelected(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); +begin + // Mouse, wheel and keyboard selection only change the available actions. + // Opening a modal here also clears selection while users are navigating. + UpdateOperationDetailsAction(Sender); +end; + +procedure TCTRLWallet.UpdateOperationDetailsAction(Sender: TObject); var - row: longint; - v: variant; - ophash: ansistring; -begin - if ASelection.Page < 0 then exit; - row := ASelection.Row; - if (row >= 0) and (row < FOperationsGrid.RowCount) then begin - v := FOperationsGrid.Rows[row]; - ophash := FOperationsGrid.Rows[row].OPHASH; - if TPCOperation.IsValidOperationHash(ophash) then begin - TUserInterface.ShowOperationInfoDialog(self, ophash); - FOperationsGrid.ClearSelection; - end; - end; + LOperationHash: String; +begin + if Assigned(FOperationDetailsAction) then + FOperationDetailsAction.Enabled := TryGetSelectedOperationHash(LOperationHash); +end; + +procedure TCTRLWallet.OnOperationsGridFinishedUpdating(Sender: TObject); +begin + UpdateOperationDetailsAction(Sender); end; procedure TCTRLWallet.cbAccountsChange(Sender: TObject); @@ -612,7 +969,7 @@ procedure TCTRLWallet.cmbDurationChange(Sender: TObject); end; end; -procedure TCTRLWallet.OnPrepareAccountPopupMenu(Sender: TObject; constref ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); +procedure TCTRLWallet.OnPrepareAccountPopupMenu(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); var accNo: cardinal; account: TAccount; @@ -671,28 +1028,56 @@ procedure TCTRLWallet.miBuyAccountClick(Sender: TObject); TUserInterface.ShowBuyAccountDialog(SelectedAccounts); end; -procedure TCTRLWallet.OnPrepareOperationsPopupMenu(Sender: TObject; constref ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); +procedure TCTRLWallet.OnPrepareOperationsPopupMenu(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu); +var + operationHash: String; begin - if (ASelection.RowCount <> 1) or ((ASelection.RowCount = 1) and (FOperationsGrid.SelectedRows[0].__KEY <> variant(nil))) then begin + if (ASelection.RowCount = 1) and TryGetSelectedOperationHash(operationHash) then begin miSep2.Visible := True; miOperationInfo.Visible := True; + miCopyOphash.Visible := True; APopupMenu := mnuOperationsPopup; end else begin miSep2.Visible := False; miOperationInfo.Visible := False; + miCopyOphash.Visible := False; APopupMenu := nil; // is empty, so dont show end; end; +function TCTRLWallet.TryGetSelectedOperationHash(out AOperationHash: String): Boolean; +var + rows: TArray; + value: Variant; +begin + Result := False; + AOperationHash := ''; + if FStopping or TUserInterface.Stopping or FQueriesSuspended or + not Assigned(FOperationsGrid) then Exit; + if FOperationsGrid.IsUpdating or FOperationsGrid.LoadingVisible or + FOperationsGrid.RefreshThrottle.Waiting or FOperationsGrid.RefreshSuspended then Exit; + rows := FOperationsGrid.SelectedRows; + if Length(rows) <> 1 then exit; + value := rows[0].__KEY; + if VarIsNull(value) or VarIsEmpty(value) then exit; + AOperationHash := VarToStr(value); + Result := TPCOperation.IsValidOperationHash(AOperationHash); +end; + procedure TCTRLWallet.miCopyOphashClick(Sender: TObject); +var + operationHash: String; begin - raise ENotImplemented.Create('Not Implemented'); + if TryGetSelectedOperationHash(operationHash) then + Clipboard.AsText := operationHash; end; procedure TCTRLWallet.miOperationInfoClick(Sender: TObject); +var + operationHash: String; begin - if FOperationsGrid.Selection.RowCount = 0 then exit; - TUserInterface.ShowOperationInfoDialog(Self, FOperationsGrid.SelectedRows[0].__KEY); + if TryGetSelectedOperationHash(operationHash) then + ShowOperationDetails(operationHash); end; end. diff --git a/src/gui-experimental/UCellRenderers.pas b/src/gui-experimental/UCellRenderers.pas index a282d3c35..b5613c6e5 100644 --- a/src/gui-experimental/UCellRenderers.pas +++ b/src/gui-experimental/UCellRenderers.pas @@ -13,12 +13,24 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Formats account and operation values for visual-grid cells, including + amounts, timestamps, payloads and operation hashes. } + +{ Public types in this unit + + Type | Purpose + ---------------+------------------------------------------------------------------------- + TCellRenderers | Formats and paints wallet account and operation cells. +} interface uses - Classes, Forms, Controls, Graphics, Dialogs, ExtCtrls, Grids, + Classes, Types, Forms, Controls, Graphics, Dialogs, ExtCtrls, Grids, UVisualGrid, variants; type @@ -39,15 +51,16 @@ TCellRenderers = class class procedure OPHASH(Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean); // General purpose - class function OperationShortHash(const AOpHash : AnsiString) : AnsiString; - class function OperationShortText(const OpType, OpSubType : DWord) : AnsiString; - class function AccountKeyShortText(const AText : AnsiString) : AnsiString; + class function OperationShortHash(const AOpHash : String) : String; + class function OperationShortText(const OpType, OpSubType : Cardinal) : String; + class function AccountKeyShortText(const AText : String) : String; end; implementation uses - SysUtils, DateUtils, UCommon, UCommon.Data, UAccounts, UNode, UConst, UCoreUtils; + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + SysUtils, DateUtils, UCommon, UCommon.Data, UAccounts, UConst, UCoreUtils; const CT_PASCBALANCE_POS_COL = clGreen; @@ -56,6 +69,23 @@ implementation CT_PASCBALANCE_0CONF_COL = clBlue; +procedure DrawRendererText(ACanvas: TCanvas; ARect: TRect; const AText: String; AAlignment: TAlignment); +{$IFNDEF FPC} +var LFlags: Cardinal; LText: String; +{$ENDIF} +begin + {$IFDEF FPC} + ACanvas.TextRect(ARect, ARect.Left, ARect.Top, AText, ACanvas.TextStyle); + {$ELSE} + LFlags := DT_SINGLELINE or DT_VCENTER or DT_NOPREFIX or DT_END_ELLIPSIS; + case AAlignment of + taRightJustify: LFlags := LFlags or DT_RIGHT; + taCenter: LFlags := LFlags or DT_CENTER; + end; + LText := String(AText); + DrawText(ACanvas.Handle, PChar(LText), Length(LText), ARect, LFlags); + {$ENDIF} +end; { TCellRenderers } class function TCellRenderers.UnixTimeSanitizer(const CellData, RowData: Variant) : Variant; @@ -68,7 +98,7 @@ class function TCellRenderers.UnixTimeSanitizer(const CellData, RowData: Variant class function TCellRenderers.OperationTypeSanitizer(const CellData, RowData: Variant) : Variant; var LData : TDataRowData absolute RowData; - LType, LSubType : DWord; + LType, LSubType : Cardinal; begin LType := LData.Data['Type']; LSubType := LData.Data['SubType']; @@ -79,9 +109,8 @@ class function TCellRenderers.OperationTypeSanitizer(const CellData, RowData: Va class procedure TCellRenderers.OperationTime (Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean); var LValue : UInt64; - LTextStyle: TTextStyle; LRowData : TDataRowData; - LStr : AnsiString; + LStr : String; begin if NOT VarIsNumeric(CellData) then exit; LValue := CellData; @@ -91,22 +120,21 @@ class procedure TCellRenderers.OperationTime (Sender: TObject; ACol, ARow: Longi LStr := 'PENDING'; Canvas.Font.Color := CT_PASCBALANCE_0CONF_COL; end; - Canvas.TextRect(Rect, Rect.Left, Rect.Top, LStr, Canvas.TextStyle); + DrawRendererText(Canvas, Rect, LStr, taLeftJustify); Handled := true; end; class procedure TCellRenderers.PASC_CheckAllBalance (Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean); var LValue : Int64; - LTextStyle: TTextStyle; LRowData : TDataRowData; - LStr : AnsiString; + LStr : String; LAllBalance : boolean; begin LRowData := TDataRowData(RowData); if LRowData.HasData('AllBalance') AND TVariantTool.TryParseBool(LRowData['AllBalance'], LAllBalance) AND LAllBalance then begin Canvas.Font.Style := [fsBold]; - Canvas.TextRect(Rect, Rect.Left, Rect.Top, 'ALL BALANCE', Canvas.TextStyle); + DrawRendererText(Canvas, Rect, 'ALL BALANCE', taRightJustify); Handled := true; exit; end; @@ -116,26 +144,27 @@ class procedure TCellRenderers.PASC_CheckAllBalance (Sender: TObject; ACol, ARow class procedure TCellRenderers.PASC_CheckPendingBalance (Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean); var LValue : Int64; - LTextStyle: TTextStyle; LRowData : TDataRowData; - LStr : AnsiString; - LAllBalance, LPending : boolean; + LStr : String; + LPending : boolean; begin if NOT TVariantTool.IsNumeric(CellData) then exit; LValue := CellData; LRowData := TDataRowData(RowData); - if LRowData.HasData('UnixTime') AND (LRowData['UnixTime'] = 0) then - LPending := true - else if LRowData.HasData('LastUpdatedBlock') AND (LRowData['LastUpdatedBlock'] > TNode.Node.BlockTip) then - LPending := true - else - LPending := false; + // Paint only the fetched row. A live node query here can wait behind disk + // restore or a snapshot worker while Windows is dispatching WM_PAINT. + LPending := false; + // The explicit account snapshot flag takes precedence over UnixTime. + if not (LRowData.HasData('IsPending') and + TVariantTool.TryParseBool(LRowData['IsPending'], LPending)) then + if LRowData.HasData('UnixTime') then + LPending := LRowData['UnixTime'] = 0; if LPending then begin Canvas.Font.Color := CT_PASCBALANCE_0CONF_COL; LStr := '(' + TAccountComp.FormatMoney(LValue) + ')'; - Canvas.TextRect(Rect, Rect.Left, Rect.Top, LStr, Canvas.TextStyle); + DrawRendererText(Canvas, Rect, LStr, taRightJustify); Handled := true; end else PASC(Sender, ACol, ARow, Canvas, Rect, State, CellData, RowData, Handled); end; @@ -143,9 +172,8 @@ class procedure TCellRenderers.PASC_CheckPendingBalance (Sender: TObject; ACol, class procedure TCellRenderers.PASC (Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean); var LValue : Int64; - LTextStyle: TTextStyle; LRowData : TDataRowData; - LStr : AnsiString; + LStr : String; begin if NOT TVariantTool.IsNumeric(CellData) then exit; @@ -153,7 +181,7 @@ class procedure TCellRenderers.PASC (Sender: TObject; ACol, ARow: Longint; Canva LRowData := TDataRowData(RowData); Canvas.Font.Color := IIF (LValue < 0, CT_PASCBALANCE_NEG_COL, IIF(LValue > 0, CT_PASCBALANCE_POS_COL, CT_PASCBALANCE_NEU_COL)); LStr := TAccountComp.FormatMoney(LValue); - Canvas.TextRect(Rect, Rect.Left, Rect.Top, LStr, Canvas.TextStyle); + DrawRendererText(Canvas, Rect, LStr, taRightJustify); Handled := true; end; @@ -168,7 +196,7 @@ class procedure TCellRenderers.Payload (Sender: TObject; ACol, ARow: Longint; Ca class procedure TCellRenderers.OPHASH (Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean); var LRowData : TDataRowData; - LStr : AnsiString; + LStr : String; begin if NOT VarIsStr(CellData) then exit; @@ -176,9 +204,9 @@ class procedure TCellRenderers.OPHASH (Sender: TObject; ACol, ARow: Longint; Can LRowData := TDataRowData(RowData); // Canvas.Font.Size := Canvas.Font.Size - 1; Canvas.Font.Color := clGrayText; - Canvas.Font.Bold := true; + Canvas.Font.Style := Canvas.Font.Style + [fsBold]; LStr := OperationShortHash(LStr); - Canvas.TextRect(Rect, Rect.Left, Rect.Top, LStr, Canvas.TextStyle); + DrawRendererText(Canvas, Rect, LStr, taLeftJustify); Handled := true; end; @@ -191,26 +219,27 @@ class procedure TCellRenderers.OPHASH (Sender: TObject; ACol, ARow: Longint; Can Result := TAccountComp.AccountPublicKeyExport(AItem.accountInfo.accountKey); } -class function TCellRenderers.OperationShortHash(const AOpHash : AnsiString) : AnsiString; +class function TCellRenderers.OperationShortHash(const AOpHash : String) : String; var len : SizeInt; begin len := Length(AOpHash); if len > 8 then - result := AOpHash.Substring(0, 4) + '...' + AOpHash.Substring(len - 4 - 1, 4) + // Copy uses one-based offsets: retain the actual final four characters. + result := Copy(AOpHash, 1, 4) + '...' + Copy(AOpHash, len - 3, 4) else result := AOpHash; end; -class function TCellRenderers.OperationShortText(const OpType, OpSubType : DWord) : AnsiString; +class function TCellRenderers.OperationShortText(const OpType, OpSubType : Cardinal) : String; begin Result := TCoreTool.GetOperationShortText(OpType, OpSubType); end; -class function TCellRenderers.AccountKeyShortText(const AText : AnsiString) : AnsiString; +class function TCellRenderers.AccountKeyShortText(const AText : String) : String; begin If Length(AText) > 20 then - Result := AText.SubString(0, 17) + '...' + Result := Copy(AText, 1, 17) + '...' else Result := AText; end; diff --git a/src/gui-experimental/UFRMAbout.dfm b/src/gui-experimental/UFRMAbout.dfm new file mode 100644 index 000000000..a8662780c --- /dev/null +++ b/src/gui-experimental/UFRMAbout.dfm @@ -0,0 +1,232 @@ +object FRMAbout: TFRMAbout + Left = -3141 + Height = 437 + Top = 507 + Width = 585 + ActiveControl = bbClose + BorderIcons = [biSystemMenu] + BorderStyle = bsDialog + Caption = 'About...' + ClientHeight = 437 + ClientWidth = 585 + Color = clBtnFace + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + OnCreate = FormCreate + Position = poOwnerFormCenter + Visible = False + object Image1: TImage + Left = 15 + Height = 64 + Top = 15 + Width = 64 + AutoSize = True + Picture.Data = { + 0954506E67496D61676589504E470D0A1A0A0000000D49484452000000400000 + 00400806000000AA6971DE000000017352474200AECE1CE90000000467414D41 + 0000B18F0BFC6105000000097048597300000EC300000EC301C76FA864000000 + 1974455874536F667477617265007061696E742E6E657420342E302E3231F120 + 6995000007E449444154785ED55B698C1CC5156E82C3259C008A4240F6CE74D5 + 8285831210A00422613B26022560C912267F40803087253010290841E2153F40 + 38968F99EA9EF50688459470D8E607421C510C8ECD259065B0B08471000B212E + 058CBDDEEE99D9B567F85EFB6DB667E6F54CCFB1DBBD9FF46977E6D5AB7AF5EA + D5F1AA7BACC9467560EE71A3057DC188B1977A79B5CA376A53D1A8ED45A377F9 + 46EFF51DFD01FEEEF28CDA5674D4539ED12BF1F7861127735E7560DE0CAE667A + C177F56C7476B9EFA897D061AFE8E86A2784EE30EA780E0EBB0D0EF909579F4E + EC18BAE0FBBEB1976034B760548F481DEA8670C418EA7EDECBD95755375AC772 + B3C9A38A8E17F3FA668CD63EC9F0C920DADAE3E7ED6B1377C488A37E4B735832 + 726AA8DE1D75B3F3D89CA94375E8EC1F61D17A5A366A6A8969512939EA914AAE + FF076CDEE402ABF77C78FE33C998248969F191E7DA17B199930374FE0E5A8C24 + 03D240D8568223AE67737B876AD53A06A3BE5A6AB4A7C479A0E8D8FF2CB9EA45 + 692729397A3BC9F1FF6BF5B27142AF0247FCA96A59C7B0F9DD813A8F8607A5C6 + 7A4918FD61F8E083CFF91AB9D12FB0C8A2CEE1F3CB617923ED87B97877C02164 + 8DDC406F8985EC716E3200A2C0D4C85D750F8B02C0415BC3728925A3EFE7E29D + A164D49D52C59341386039371B009F5FAF2993B72F639145FB3F1C70B0462E90 + 7608DFC95EC76AEDA1E8EA0558500E3756AAF68D15ECCBC606F5FC5E72185B2B + 371D6074485F12965756CF3A91455675EBBC19F41DD6A585E0AD25576F68B233 + F9948BB06A3C1C2AA81F23F43FAFAF0C9D1F2EBBF6CFB858AA4051512CE8CBE1 + 082C940D76FFF77F2BE7CCE4A2AD01854D8D95E8C398538BB8486A11EC58057D + 0B1C51AAB37F3D17690E4A36C28AE3C49CFC23179916C0FAB538BC95D2FF7E5E + FD8AC532F6E6FA8FA77009779C4873AC67FBEA1482463DDC0F7CDE511DB0BEC7 + E24660EF5C165608E8EA57C9315C645AA13898C9D61FA8B02B5CC3E25AEC1E98 + 7B9C67F427E1C2E0B7B4DA961DFDD33432CE2509FA80D365C80146EDA27582C5 + 13F0F3FAF7E182D381E5825ACDE64702D3F7DF0DBAC6FE0D8B2710E764953676 + EC00476D66F151EC77B21929F9483B3B750006BBF4EDDACC295C045B9F5177D5 + 176A97985B1F950A7A5153BAEAEF926EA7A47C81BB1009390282C570E2880CE3 + FF25156A8BA6369991801DE50951B743224B75B9EA484439007C322840F7F670 + 40C757D721DE1E54D8041425825EC7ECCA01467F1114F090288805DAA497CBFC + 32A8300294EC4CC23AB38EAB8F4493084086D9675B48336F12856D9016957D03 + 9913B84D11B4F548BADDB0ECA81BB8FA48347300D6BEC574CFF71749D80E11DA + 3BB9BD48F88E7D9FA4DB39D55B951827D4660EC0C0DD4B3B40D7D7DBA86888DB + 8B84EFAA1558B5B7764547BD5274C95E7B597543F3881B071CB045B29988C3DF + 606F0E40462FE5F652054A7C10299F8A36138D7E8656E69DA2B00D96F3D99F73 + 9BA9C2A8AB2F91EC1D27B2C32D74E9B95B12C6A65187E0E9D43DC6A6D16F19DD + 46FD87B2A5F71A046D51BDC16DA606C1AD509CC59D1C8030785B14C6A5D1396E + 3715A014191D8BB5B023425EEAFA185C76ED6BB9EDC4403BC2E8FAECA5256317 + 109123929D123D3A0E23023648C2B83CB8363B87ED10E1E5FA67055B5788E542 + FFF92C0E9EF2608B7B242CA7A7CF2CB6E8A96F581666D9D59B61C30EB0C3A33C + B24944C00A59188346ED6F7AC706948DBDA45EAF3AA4FA586C1D5A679F8E2372 + 655C86B03C187EF1C177FBE785757B492F8FFCC573ECAB25611C22197999ED8C + 44FD62049DAFC25752F8EE8AB01C7C8D45014A8E7D779DBC77CCAB5F5B07D6F4 + 2951188746AF643B2351BF15E133CDD17E92058FB88C7AAC468E8CF1C0EA59A7 + 919C9E0621CAB685E5BD22256655BA1439FAA4B5F129501C6274AE2643A340E7 + 03D43D5CAF87EF467144A54759FBEB654418E71F9537EAF68AB061379B4961DA + FE4D0DCD5B7FDDC45C96507233E748BA69A09FB72752697ABD4D2AD4825F8AD7 + CB21C049D7097A29A15AC8665AD657CEDC93110587E4821134FA79568F04E67B + CD4B0EA9A1515F34BC62873951B318B5A6FD00AB46025E7E53D64D9A6A159B38 + 01CFA80BC3FB712BE2D47525AB8A08EE1AB19849BAC9528D1573FD9ACDAC05A2 + E01559A996E8D8113AC0B09A88915CE67C493779AA8D6C62237C277B69CC28F8 + 845522E1197A462FEA26460CF0E172419DCB26CAF0F3EA5949B9867493D20268 + ECAFA26E82C4E0FE8DCD8BC601CC8F5673B74497892D8095F61D493739AA6F5A + 4DDBFF03E7823FC8958C33B4870AF86CE08C931001A3B26E32843DF1DF20E5D7 + D022AE93D5D88142DFA95C540452EC8B65DD6488CE6F62D3E223B859916F543F + E02291C01A7187A09708D1F93DFB87D40FD9B4F6103C36AB3B212232DE474A7B + 5F73A6E500A4BE2EB5B8B069098CE6E5E874CD2B67D38218389A8ADC8DEE5072 + EDDFA1B2A2D8502AA90E62E0E6B3F9BD4131A717A052317F4F19BF9CB41F4E0C + 23BFA74545683425543B8B6B3359367772F035DDD21AF50FD9806488E959F1F3 + F660DC07A63D8197A3CBD4E47F3B8405FA63FCBD82CD9A5A1C8D06BD3289B417 + 5371185C117E8D3E318CACE93B03C6AC09565FC1D85E12ED7C8329F860F8E149 + 6A507974CE4CCFA8DB60E0EB74672075A013A2D38791A56E45BD37529EC1CDA5 + 1B1533FBCCA2B197824FD03C8D79CF10909C07EE45871F47E7AFA71F7070B5D3 + 17559CC73DE7AC5FD03BC9382A2FA7F98B29F31085331CF4678F7E9368FA97D0 + 1EDED6AF3BBA82657D074099B85A872D467E0000000049454E44AE426082 + } + end + object Label1: TLabel + Left = 90 + Height = 25 + Top = 15 + Width = 349 + Caption = 'PascalCoin Experimental Wallet' + Font.Color = clBlack + Font.Height = -21 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblBuild: TLabel + Left = 15 + Height = 13 + Top = 388 + Width = 30 + Anchors = [akLeft, akBottom] + Caption = 'Build:' + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblProtocolVersion: TLabel + Left = 15 + Height = 13 + Top = 407 + Width = 50 + Anchors = [akLeft, akBottom] + Caption = 'Protocol:' + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object Label2: TLabel + Left = 88 + Height = 13 + Top = 368 + Width = 65 + Anchors = [akLeft, akBottom] + Caption = 'Source Code:' + ParentColor = False + end + object Label4: TLabel + Cursor = crHandPoint + Left = 168 + Height = 13 + Top = 368 + Width = 240 + Anchors = [akLeft, akBottom] + Caption = 'https://github.com/PascalCoin/PascalCoin' + Font.Color = clBlue + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + OnClick = Label4Click + end + object Memo1: TMemo + Left = 90 + Height = 290 + Top = 46 + Width = 478 + BorderStyle = bsNone + Lines.Strings = ( + 'Copyright '#169' 2016-2026 Albert Molina and Herman Schoenfeld' + 'Original PascalCoin: Albert Molina' + 'Experimental GUI: Herman Schoenfeld' + '' + 'PascalCoin is P2P cryptocurrency designed for infinite scalabilty by implementing a new technology known as SafeBox.' + '' + 'Distributed under the MIT software license, see the accompanying file LICENSE or visit ' + 'http://www.opensource.org/licenses/mit-license.php.' + '' + 'ACKNOWLEDGEMENTS' + '' + 'Ugochukwu Mmaduekwe: core developer' + 'Maciej Izak: contributor and author of Generics.Collections' + 'OpenSSL Project: https://github.com/Arvur/OpenSSL-Delphi' + 'Synapse: network library copyright Lukas Gebauer' + 'Sphere 10 Pascal Framework: Herman Schoenfeld' + 'https://sphere10.com' + 'Ladar Levison: cryptographic functions' + 'Marco Ferrante: cryptographic functions' + '' + 'THIS IS EXPERIMENTAL SOFTWARE. Use it for educational purposes only.' + ) + ParentColor = True + ReadOnly = True + ScrollBars = ssVertical + TabOrder = 0 + end + object bbClose: TBitBtn + Left = 456 + Height = 31 + Top = 390 + Width = 111 + Anchors = [akRight, akBottom] + Caption = 'Close' + Kind = bkOK + ModalResult = 1 + TabOrder = 1 + end + object Label6: TLabel + Left = 110 + Height = 13 + Top = 344 + Width = 43 + Anchors = [akLeft, akBottom] + Caption = 'Website:' + ParentColor = False + end + object Label7: TLabel + Cursor = crHandPoint + Left = 168 + Height = 13 + Top = 344 + Width = 150 + Anchors = [akLeft, akBottom] + Caption = 'http://www.pascalcoin.org' + Font.Color = clBlue + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + OnClick = Label4Click + end +end diff --git a/src/gui-experimental/UFRMAbout.lfm b/src/gui-experimental/UFRMAbout.lfm index aea92952a..f235f5f03 100644 --- a/src/gui-experimental/UFRMAbout.lfm +++ b/src/gui-experimental/UFRMAbout.lfm @@ -99,8 +99,8 @@ object FRMAbout: TFRMAbout Left = 90 Height = 25 Top = 15 - Width = 188 - Caption = 'PascalCoin Wallet' + Width = 349 + Caption = 'PascalCoin Experimental Wallet' Font.Color = clBlack Font.Height = -21 Font.Name = 'Tahoma' @@ -168,8 +168,9 @@ object FRMAbout: TFRMAbout Width = 478 BorderStyle = bsNone Lines.Strings = ( - 'Copyright (c) 2018 PascalCoin Developers' - '(based on original source-code by Albert Molina)' + 'Copyright © 2016-2026 Albert Molina and Herman Schoenfeld' + 'Original PascalCoin: Albert Molina' + 'Experimental GUI: Herman Schoenfeld' '' 'PascalCoin is P2P cryptocurrency designed for infinite scalabilty by implementing a new technology known as SafeBox.' '' @@ -178,13 +179,12 @@ object FRMAbout: TFRMAbout '' 'ACKNOWLEDGEMENTS' '' - 'Alber Molina: founder and lead developer' - 'Herman Schoenfeld: core developer' 'Ugochukwu Mmaduekwe: core developer' 'Maciej Izak: contributor and author of Generics.Collections' - 'OpenSSL Project: https://github.com/Arvur/OpenSSL-Delph' + 'OpenSSL Project: https://github.com/Arvur/OpenSSL-Delphi' 'Synapse: network library copyright Lukas Gebauer' - 'Sphere 10 Software: framework code, gui libraries and visual grid' + 'Sphere 10 Pascal Framework: Herman Schoenfeld' + 'https://sphere10.com' 'Ladar Levison: cryptographic functions' 'Marco Ferrante: cryptographic functions' '' diff --git a/src/gui-experimental/UFRMAbout.pas b/src/gui-experimental/UFRMAbout.pas index e5c03ebf3..543d51518 100644 --- a/src/gui-experimental/UFRMAbout.pas +++ b/src/gui-experimental/UFRMAbout.pas @@ -1,6 +1,7 @@ unit UFRMAbout; { Copyright (c) 2016 by Albert Molina + Copyright © 2017-2026 Herman Schoenfeld Distributed under the MIT software license, see the accompanying file LICENSE or visit http://www.opensource.org/licenses/mit-license.php. @@ -16,14 +17,26 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Displays the wallet version, project links and software credits. } + +{ Public types in this unit + + Type | Purpose + ----------+------------------------------------------------------------------------- + TFRMAbout | Shows version information, credits and project links. +} interface {$I ../config.inc} uses - LCLIntf, LCLType, LMessages, + {$IFNDEF FPC}Winapi.Windows, Winapi.ShellAPI,{$ENDIF} + {$IFDEF FPC}LCLIntf, LCLType, LMessages,{$ENDIF} Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls, Buttons, UCommon.UI; @@ -57,11 +70,16 @@ implementation uses UFolderHelper, UConst, UNode; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} procedure TFRMAbout.FormCreate(Sender: TObject); begin + // Keep attribution in the paired form resources; runtime versions use node data. lblBuild.Caption := 'Build: '+CT_ClientAppVersion; lblProtocolVersion.Caption := Format('BlockChain Protocol: %d (%d) - Net Protocol: %d (%d)',[TNode.Node.Bank.SafeBox.CurrentProtocol,CT_BlockChain_Protocol_Available, CT_NetProtocol_Version, CT_NetProtocol_Available]); @@ -79,7 +97,11 @@ procedure TFRMAbout.Label5Click(Sender: TObject); procedure TFRMAbout.OpenURL(Url: String); begin - OpenDocument(pchar(URL)) + {$IFDEF FPC} + OpenDocument(PChar(URL)); + {$ELSE} + ShellExecute(Handle, 'open', PChar(URL), nil, nil, SW_SHOWNORMAL); + {$ENDIF} end; end. diff --git a/src/gui-experimental/UFRMAccountExplorer.dfm b/src/gui-experimental/UFRMAccountExplorer.dfm new file mode 100644 index 000000000..e4d8e15ee --- /dev/null +++ b/src/gui-experimental/UFRMAccountExplorer.dfm @@ -0,0 +1,498 @@ +object FRMAccountExplorer: TFRMAccountExplorer + Left = 207 + Height = 451 + Top = 94 + Width = 868 + ActiveControl = cbMyPrivateKeys + Caption = 'Accounts Explorer' + ClientHeight = 432 + ClientWidth = 868 + Menu = meAccountExplorerMenu + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + object Splitter1: TSplitter + Left = 380 + Height = 366 + Top = 66 + Width = 5 + end + object pnlMyAccountsTop: TPanel + Left = 0 + Height = 66 + Top = 0 + Width = 868 + Align = alTop + BevelOuter = bvNone + TabOrder = 0 + object Label18: TLabel + Left = 11 + Height = 14 + Top = 40 + Width = 70 + Caption = 'Find account' + ParentColor = False + end + object cbMyPrivateKeys: TComboBox + Left = 308 + Height = 22 + Top = 5 + Width = 411 + ItemHeight = 14 + OnChange = cbMyPrivateKeysChange + Style = csDropDownList + TabOrder = 0 + end + object cbExploreMyAccounts: TCheckBox + Left = 11 + Height = 18 + Top = 9 + Width = 268 + Caption = 'Explore accounts with one of my Wallet Keys' + OnClick = cbExploreMyAccountsChange + TabOrder = 1 + end + object ebFindAccountNumber: TEdit + Left = 95 + Height = 22 + Top = 33 + Width = 83 + OnChange = ebFindAccountNumberChange + OnExit = ebFindAccountNumberExit + TabOrder = 3 + end + object bbChangeKeyName: TBitBtn + Left = 733 + Height = 25 + Top = 5 + Width = 126 + Caption = 'Change Key name' + OnClick = bbChangeKeyNameClick + TabOrder = 2 + end + object cbFilterAccounts: TCheckBox + Left = 308 + Height = 18 + Top = 35 + Width = 161 + Caption = 'Filter accounts by balance' + OnClick = cbFilterAccountsChange + TabOrder = 4 + end + object ebFilterAccountByBalanceMin: TEdit + Left = 472 + Height = 22 + Hint = 'Min balance' + Top = 33 + Width = 83 + OnExit = ebFilterAccountByBalanceMinExit + OnKeyPress = ebFilterAccountByBalanceMinKeyPress + TabOrder = 5 + end + object ebFilterAccountByBalanceMax: TEdit + Left = 568 + Height = 22 + Hint = 'Max balance' + Top = 33 + Width = 83 + OnExit = ebFilterAccountByBalanceMinExit + OnKeyPress = ebFilterAccountByBalanceMinKeyPress + TabOrder = 6 + end + object sbSearchAccount: TSpeedButton + Left = 184 + Height = 23 + Top = 33 + Width = 24 + Glyph.Data = { + 36030000424D3803000000000000360000002800000010000000100000000100 + 18000000000000000000120B0000120B00000000000000000000FF00FF4A667C + BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF + 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF + 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 + B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD + EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF + FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 + C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF + FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF + E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 + C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 + C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 + DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 + 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF + } + OnClick = sbSearchAccountClick + end + end + object pnlAccounts: TPanel + Left = 0 + Height = 366 + Top = 66 + Width = 380 + Align = alLeft + BevelOuter = bvNone + TabOrder = 1 + object dgAccounts: TDrawGrid + Left = 0 + Height = 332 + Top = 0 + Width = 380 + Align = alClient + TabOrder = 0 + OnClick = dgAccountsClick + end + object pnlAccountsInfo: TPanel + Left = 0 + Height = 34 + Top = 332 + Width = 380 + Align = alBottom + BevelOuter = bvNone + TabOrder = 1 + object Label17: TLabel + Left = 5 + Height = 14 + Top = 10 + Width = 55 + Caption = 'Accounts:' + ParentColor = False + end + object Label19: TLabel + Left = 136 + Height = 14 + Top = 10 + Width = 45 + Caption = 'Balance:' + ParentColor = False + end + object lblAccountsCount: TLabel + Left = 60 + Height = 14 + Top = 10 + Width = 21 + Caption = '000' + ParentColor = False + end + object lblAccountsBalance: TLabel + Left = 200 + Height = 14 + Top = 10 + Width = 21 + Caption = '000' + ParentColor = False + end + object bbAccountsRefresh: TBitBtn + Left = 302 + Height = 25 + Top = 6 + Width = 75 + Anchors = [akTop, akRight] + Caption = 'Refresh' + Glyph.Data = { + 36030000424D3603000000000000360000002800000010000000100000000100 + 18000000000000030000120B0000120B00000000000000000000FF00FFFF00FF + C2A6A4C2A6A4C2A6A4C2A6A4C2A6A4C2A6A4C2A6A4C2A6A4C2A6A4C2A6A4C2A6 + A4C2A6A4FF00FFFF00FFFF00FFFF00FFC2A6A4FEFCFBFEFCFBFEFCFBFEFCFBFE + FCFBFEFCFBFEFCFBFEFCFBFEFCFBFEFCFBC2A6A4FF00FFFF00FFFF00FFFF00FF + C2A6A4FEFCFBFEFCFBFEFCFBFEFCFBD8EBD6018A02018A02D8EBD6FEFCFBFEFC + FBC2A6A4FF00FFFF00FFFF00FFFF00FFC2A6A4FEFBF7FEFBF7018A02D8EAD201 + 8A02D8EAD2D8EAD2018A02FEFBF7FEFBF7C2A6A4FF00FFFF00FFFF00FFFF00FF + C2A6A4FEF9F4FEF9F4018A02018A02D8E8D0FEF9F4FEF9F4D8E8D0FEF9F4FEF9 + F4C2A6A4FF00FFFF00FFFF00FFFF00FFC2A6A4FEF7F0FEF7F0018A02018A0201 + 8A02FEF7F0FEF7F0FEF7F0FEF7F0FEF7F0C2A6A4FF00FFFF00FFFF00FFFF00FF + C2A6A4FEF5ECFEF5ECFEF5ECFEF5ECFEF5EC018A02018A02018A02FEF5ECFEF5 + ECC2A6A4FF00FFFF00FFFF00FFFF00FFC2A6A4FEF3E9FEF3E9D8E3C7FEF3E9FE + F3E9D8E3C7018A02018A02FEF3E9FEF3E9C2A6A4FF00FFFF00FFFF00FFFF00FF + C2A6A4FFF1E5FFF1E5018A02D9E2C3D9E2C3018A02D9E2C3018A02FFF1E5FFF1 + E5C2A6A4FF00FFFF00FFFF00FFFF00FFC2A6A4FFF0E2FFF0E2D9E1C1018A0201 + 8A02D9E1C1DDCFC2DDCFC2DDCFC2DDCFC2C2A6A4FF00FFFF00FFFF00FFFF00FF + C2A6A4FFEEDEFFEEDEFFEEDEFFEEDEFFEEDEFFEEDEC5B5A9C3B4A8C2B3A7C1B2 + A6C2A6A4FF00FFFF00FFFF00FFFF00FFC2A6A4FFECDAFFECDAFFECDAFFECDAFF + ECDAFFECDAB0A296B0A296B0A296B0A296C2A6A4FF00FFFF00FFFF00FFFF00FF + C2A6A4FFEAD7FFEAD7FFEAD7FFEAD7FFEAD7C9B9ACFBF8F4FBF8F4E6DAD9C2A6 + A4FF00FFFF00FFFF00FFFF00FFFF00FFC2A6A4FFE8D3FFE8D3FFE8D3FFE8D3FF + E8D3C9B9ACFBF8F4DFCEC7C2A6A4FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + C2A6A4FFE6D0FFE6D0FFE6D0FFE6D0FFE6D0C9B9ACDFCEC7C2A6A4FF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFC2A6A4C2A6A4C2A6A4C2A6A4C2A6A4C2 + A6A4C2A6A4C2A6A4FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + } + OnClick = bbAccountsRefreshClick + TabOrder = 0 + end + end + end + object pcAccountsOptions: TPageControl + Left = 385 + Height = 366 + Top = 66 + Width = 483 + ActivePage = tsAccountOperations + Align = alClient + TabIndex = 0 + TabOrder = 2 + object tsAccountOperations: TTabSheet + Caption = 'Operations of selected Account' + object dgAccountOperations: TDrawGrid + Left = 0 + Height = 339 + Top = 0 + Width = 475 + Align = alClient + TabOrder = 0 + OnClick = dgAccountOperationsClick + OnDblClick = dgAccountOperationsClick + end + end + object tsMultiSelectAccounts: TTabSheet + Caption = 'Selected accounts for massive operations' + ImageIndex = 1 + object dgSelectedAccounts: TDrawGrid + Left = 41 + Height = 300 + Top = 31 + Width = 325 + Align = alLeft + Anchors = [akTop, akLeft, akRight, akBottom] + TabOrder = 0 + end + object pnlSelectedAccountsTop: TPanel + Left = 0 + Height = 31 + Top = 0 + Width = 475 + Align = alTop + BevelOuter = bvNone + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 1 + object Label15: TLabel + Left = 41 + Height = 16 + Top = 4 + Width = 361 + Caption = 'Select multiple accounts to execute massive operations' + ParentColor = False + end + end + object pnlSelectedAccountsBottom: TPanel + Left = 0 + Height = 26 + Top = 331 + Width = 475 + Align = alBottom + BevelOuter = bvNone + TabOrder = 2 + object Label20: TLabel + Left = 41 + Height = 15 + Top = 6 + Width = 53 + Caption = 'Accounts:' + ParentColor = False + end + object lblSelectedAccountsCount: TLabel + Left = 96 + Height = 15 + Top = 6 + Width = 18 + Caption = '000' + ParentColor = False + end + object Label22: TLabel + Left = 156 + Height = 15 + Top = 6 + Width = 97 + Caption = 'Accounts Balance:' + ParentColor = False + end + object lblSelectedAccountsBalance: TLabel + Left = 250 + Height = 15 + Top = 6 + Width = 18 + Caption = '000' + ParentColor = False + end + end + object pnlSelectedAccountsLeft: TPanel + Left = 0 + Height = 300 + Top = 31 + Width = 41 + Align = alLeft + BevelOuter = bvNone + TabOrder = 3 + object sbSelectedAccountsAdd: TSpeedButton + Left = 2 + Height = 31 + Top = 0 + Width = 33 + Caption = '>' + OnClick = sbSelectedAccountsAddClick + end + object sbSelectedAccountsAddAll: TSpeedButton + Left = 2 + Height = 31 + Top = 37 + Width = 33 + Caption = '>>' + OnClick = sbSelectedAccountsAddAllClick + end + object sbSelectedAccountsDel: TSpeedButton + Left = 2 + Height = 31 + Top = 74 + Width = 33 + Caption = '<' + OnClick = sbSelectedAccountsDelClick + end + object sbSelectedAccountsDelAll: TSpeedButton + Left = 2 + Height = 31 + Top = 111 + Width = 33 + Caption = '<<' + OnClick = sbSelectedAccountsDelAllClick + end + end + object bbSelectedAccountsOperation: TBitBtn + Left = 372 + Height = 61 + Top = 31 + Width = 75 + Anchors = [akTop, akRight] + Caption = 'Operations' + Glyph.Data = { + F6060000424DF606000000000000360000002800000018000000180000000100 + 180000000000C0060000120B0000120B00000000000000000000FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FF019ACF019ACF019ACF019ACFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF0C8518FF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF0D9FD18BD4EE6BD3F845C0ED28B0E0019ACF01 + 9ACF019ACF019ACFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF0C85180C8518 + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF069CD076C8E5A9E9FE6DD8 + FF75DBFF77DCFF77DBFF63D1F930B3E3029BD0019ACF019ACF019ACF019ACFFF + 00FF0C85181399220C8518FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF019ACF + 34AFD9BCE9F86ED8FF6FD8FE70D8FE70D8FE71D8FF0C85180C85180C85180C85 + 180C85180C85180C85180C85181DAC31139A220C8518FF00FFFF00FFFF00FFFF + 00FFFF00FF019ACF1FA9D68FD3EB97E4FF6FD9FE71D9FE71D9FE71D9FE0C8518 + 57E38851DD7E4AD77443D0693BC95E34C1522BBA4725B33C1EAE33149B230C85 + 18FF00FFFF00FFFF00FFFF00FF019ACF31B1DC49B7DEBDEEFB71DDFE77DEFE77 + DEFE77DEFE0C85185EE89059E48953DE804CD87645D16C3DCA6035C2542DBB49 + 26B53F1FAF35149B250C8518FF00FFFF00FFFF00FF019ACF52C2E71DA7D5ADE2 + F38FE8FF7CE2FE7CE3FE7CE3FE0C851861EB955FE9925AE58B54DF824DD97846 + D26D3ECB6237C4562FBD4C27B64021B037159B250C8518FF00FFFF00FF019ACF + 60CAEF1FA8D85EC1E1C2E6ED8ACEE08FCFE18ECFE10C851861EB9561EB955FEA + 935CE58D56E0844FDB7A48D47040CD6538C65931BF4D1DA3320C8518FF00FFFF + 00FFFF00FF019ACF65CFF53EB7E52CA9D4C5EFF8ACF3FEA5F2FFA5F2FF0C8518 + 61EB9561EB9561EB9561EB945CE68E57E18650DC7C49D57242CE6727AD410C85 + 18FF00FFFF00FFFF00FFFF00FF019ACF69D1F855C4F32A9CC673CBE7D6FEFDB1 + FBFDB2FBFD0C85180C85180C85180C85180C85180C85180C85180C851852DD7F + 32B6500C851898FAFF019ACFFF00FFFF00FFFF00FF019ACF77D5FC5CC8FB748E + A224A8D5B9E7F3D5F5F9D5F6F9D6F6FADCFAFBCDFDFCB9FCFCAFFAFCB0FAFCB1 + FAFC0C85183ABE5C0C85189FFCFFA4FFFF43C1E2019ACFFF00FFFF00FF019ACF + 8BDBFF5FCDFFB7898973C3DD18A2D218A2D216A2D215A1D21AA4D391D7EBEBFE + FDDBFDFCC5FBFBC2FBFB0C85180C851883E4F3B6FDFFBAFFFFB5FCFD019ACFFF + 00FFFF00FF019ACF99E2FF67D3FFB88989FEF5ECFDF3EBF0EFEAE5EBE8D6E5E6 + A4D2E025A6D34DB9DDE5F8FBF5FDFCEBFCFB0C8518C4FBFF9CE4F2DAFEFFD9FE + FFE3FFFFADE9F5019ACFFF00FF019ACF9FE9FF70DCFFB88989FEF3E9FFF2E6FE + F3E9FEF3E9FEF3E9FEF3E9D4E4E439ADD422A5D49DD8ECF1F9FBEEEFEFE9FDFF + CEEEF7F8FFFFF7FFFFFEFFFFE9F9FD019ACFFF00FF019ACFA7EFFF76E5FFB889 + 89FFF2E5FFF0E2FFF2E5FFF2E5FFF2E5FFF2E5FFF2E5EAEBE38EC9DA44B0D501 + 9ACF019ACF019ACF019ACF019ACF019ACF019ACF019ACF019ACFFF00FF019ACF + ABF6FF7EEDFFB88989FFF0E2FFEFDFFFF0E2FFF0E2FFF0E2FFF0E2FFF0E2FEEE + E0FBECDEFAEBDEF6E6D9B8898993F7FF019ACFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FF019ACFC7FFFF82F5FFB88989FFEEDFFFECDBFFEEDFFFEEDFFFEEDF + FFEEDFF9E8D9DECCC1D9CABDCFBDB4C8B3ACB88989B5FFFF019ACFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF019ACFA4E0F0A0FDFFB88989FFECDBFFEBD8FF + ECDBFFECDBFFECDBFFECDBF5E2D2C4ABA7C2A8A5BBA39FC2AFA9B88989019ACF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF019ACFECFFFFB889 + 89FFEBD8FFEAD5FFEBD8FFEBD8FFEBD8FFEBD8FFEBD8D9C8C5FEFEFDFEF6EFDE + C9C0B88989FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FF019ACFB88989FFE9D5FFE8D3FFE9D5FFE9D5FFE9D5FFE9D5FFE9D5C6AD + A9FEF8F2E8D4CACD9999FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFB88989FFE7D1FFE7D0FFE7D1FFE7D1FFE7D1 + FFE7D1E7CEBFD3BFB9E8D5CCCD9999FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB88989FFE6CFFFE6CFFF + E6CFFFE6CFFFE6CFFFE6CFD5BBB2E0CCC5CD9999FF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB889 + 89B88989B88989B88989B88989B88989B88989B88989B88989FF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + } + Layout = blGlyphTop + OnClick = bbSelectedAccountsOperationClick + TabOrder = 4 + end + end + end + object meAccountExplorerMenu: TMainMenu + Left = 240 + Top = 16 + object miTools: TMenuItem + Caption = 'Tools' + object miNewOperation: TMenuItem + Caption = 'New Operation' + OnClick = miNewOperationClick + end + object miDecodePayload: TMenuItem + Caption = 'Decode Payload' + OnClick = miDecodePayloadClick + end + object N1: TMenuItem + Caption = '-' + end + object miFindAccount: TMenuItem + Caption = 'Find account' + ShortCut = 16454 + OnClick = sbSearchAccountClick + end + object miAccountInformation: TMenuItem + Caption = 'Account Information' + ShortCut = 112 + OnClick = MiAccountInformationClick + end + object N2: TMenuItem + Caption = '-' + end + object miAddAccountToSelected: TMenuItem + Caption = 'Add account to selected' + ShortCut = 117 + OnClick = miAddAccountToSelectedClick + end + object miRemoveAccountFromSelected: TMenuItem + Caption = 'Remove account from selected' + ShortCut = 118 + OnClick = miRemoveAccountFromSelectedClick + end + object N3: TMenuItem + Caption = '-' + end + object miFindPreviousAccountWithHighBalance: TMenuItem + Caption = 'Find previous account with high balance' + ShortCut = 16498 + OnClick = miFindPreviousAccountWithHighBalanceClick + end + object miFindNextAccountWithHighBalance: TMenuItem + Caption = 'Find next account with high balance' + ShortCut = 114 + OnClick = miFindNextAccountWithHighBalanceClick + end + end + end +end diff --git a/src/gui-experimental/UFRMAccountExplorer.lfm b/src/gui-experimental/UFRMAccountExplorer.lfm index c389b3c7d..61b0a9837 100644 --- a/src/gui-experimental/UFRMAccountExplorer.lfm +++ b/src/gui-experimental/UFRMAccountExplorer.lfm @@ -489,6 +489,7 @@ object FRMAccountExplorer: TFRMAccountExplorer object miFindAccount: TMenuItem Caption = 'Find account' ShortCut = 16454 + OnClick = sbSearchAccountClick end object miAccountInformation: TMenuItem Caption = 'Account Information' diff --git a/src/gui-experimental/UFRMAccountExplorer.pas b/src/gui-experimental/UFRMAccountExplorer.pas index 123939c5e..c3c42757d 100644 --- a/src/gui-experimental/UFRMAccountExplorer.pas +++ b/src/gui-experimental/UFRMAccountExplorer.pas @@ -16,16 +16,34 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Explores accounts and their operations, with wallet-key and balance filters + and a batch-selection list. Account painting reads cached snapshots while + owned workers load node data. } + +{ Public types in this unit + + Type | Purpose + --------------------+------------------------------------------------------------------------- + TFRMAccountExplorer | Explores accounts, account history and batch selections. +} interface {$I ..\config.inc} uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ComCtrls, + Classes, SysUtils, Messages, + {$IFDEF FPC}FileUtil, LCLIntf, LMessages,{$ELSE}Winapi.Windows,{$ENDIF} Forms, Controls, Graphics, Dialogs, ComCtrls, ExtCtrls, StdCtrls, Buttons, Grids, Menus, - UCommon.UI, UBaseTypes, UPCDataTypes, UGridUtils, UNode, UAccounts, UBlockChain; + UCommon, UCommon.UI, UBaseTypes, UPCDataTypes, UGridUtils, UNode, UAccounts, + UBlockChain, UExplorerAccountSnapshots; + +const + CM_ExplorerKeysChanged = {$IFDEF FPC}LM_USER{$ELSE}WM_USER{$ENDIF} + 102; type @@ -191,7 +209,10 @@ TFRMAccountExplorer = class(TApplicationForm) FAccountsGrid : TAccountsGrid; FAccountOperationsGrid : TOperationsGrid; FAccountsSelectedGrid : TAccountsGrid; - FOrderedAccountsKeyList : TOrderedAccountKeysList; + FAccountSnapshots: TExplorerAccountSnapshots; + FBackendRefresh: TThrottledEvent; + FBackendPending, FBackendReady, FStopping: Boolean; + FSelectedAccountsRefreshPending: Boolean; FMinAccountBalance : Int64; FMaxAccountBalance : Int64; FLastAccountsGridInvalidateTC : TTickCount; @@ -202,19 +223,39 @@ TFRMAccountExplorer = class(TApplicationForm) procedure OnBlocksChanged(Sender: TObject); procedure OnPrivateKeysChanged(Sender: TObject); procedure OnAccountsSelectedGridUpdated(Sender: TObject); + function ReadAccount(Sender: TObject; AAccountNumber: Integer; out AAccount: TAccount; out ABlockCount, AAccountCount: Integer): Boolean; + function ReadSummary(Sender: TObject; out AAccountCount: Integer; out ABalance: Int64): Boolean; + procedure OnSnapshotUpdated(Sender: TObject); + procedure OnBackendReady(Sender: TObject); + procedure TryRefreshBackend; + procedure CM_PrivateKeysChanged(var AMsg: TMessage); message CM_ExplorerKeysChanged; + protected + function CreateAccountSnapshots: TExplorerAccountSnapshots; virtual; + procedure InitializeBackendRefresh; virtual; + function CanRefreshBackend: Boolean; virtual; + procedure RefreshBackend; virtual; + procedure DoShow; override; + procedure OnViewReady(Sender: TObject); + procedure ScheduleBackendRefresh; + property BackendRefreshEvent: TThrottledEvent read FBackendRefresh; public { public declarations } procedure OnSelectedAccountChanged; + procedure OnAccountsGridUpdatedData(Sender: TObject); procedure Refresh; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UConst, USettings, UWallet, UCrypto, UCoreUtils, - UFRMAccountSelect, UFRMMemoText, UUserInterface, UCommon, UPCOrderedLists; + UFRMAccountSelect, UFRMMemoText, UUserInterface, UPCOrderedLists, UMemory; { TFRMAccountExplorer } @@ -225,45 +266,77 @@ procedure TFRMAccountExplorer.FormCreate(Sender: TObject); FLastAccountsGridInvalidateTC := TPlatform.GetTickCount; FMinAccountBalance := 0; FMaxAccountBalance := CT_MaxWalletAmount; - FOrderedAccountsKeyList := Nil; + FAccountSnapshots := CreateAccountSnapshots; + FAccountSnapshots.OnUpdated := OnSnapshotUpdated; + FAccountSnapshots.OnCompleted := OnViewReady; + InitializeBackendRefresh; FAccountsGrid := TAccountsGrid.Create(Self); + FAccountsGrid.OnReadAccount := ReadAccount; + FAccountsGrid.OnReadSummary := ReadSummary; FAccountsGrid.DrawGrid := dgAccounts; FAccountsGrid.Node := TUserInterface.Node; FAccountsGrid.AllowMultiSelect := True; + FAccountsGrid.OnUpdated := OnAccountsGridUpdatedData; + FAccountsGrid.OnAccountsGridUpdatedData := OnAccountsGridUpdatedData; + // Retired/cancelled filter work can become idle without publishing rows. + FAccountsGrid.OnRefreshStateChanged := OnViewReady; FAccountsSelectedGrid := TAccountsGrid.Create(Self); + FAccountsSelectedGrid.OnReadAccount := ReadAccount; + FAccountsSelectedGrid.AccountsGridDatasource := acds_InternalList; FAccountsSelectedGrid.DrawGrid :=dgSelectedAccounts; FAccountsSelectedGrid.Node := TUserInterface.Node; FAccountsSelectedGrid.OnUpdated := OnAccountsSelectedGridUpdated; FAccountOperationsGrid := TOperationsGrid.Create(Self); + FAccountOperationsGrid.AsynchronousUpdates := True; + FAccountOperationsGrid.AutoRefreshOnNodeEvents := False; + FAccountOperationsGrid.MustShowAlwaysAnAccount := True; + FAccountOperationsGrid.AccountNumber := -1; FAccountOperationsGrid.DrawGrid := dgAccountOperations; FAccountOperationsGrid.Node := TUserInterface.Node; - FAccountOperationsGrid.MustShowAlwaysAnAccount := true; pcAccountsOptions.ActivePage := tsAccountOperations; - // Get account list from SafeBox - FOrderedAccountsKeyList := TOrderedAccountKeysList.Create(TUserInterface.Node.Bank.SafeBox,false); - // Subscribe to events TUserInterface.Loaded.Add(OnLoaded); TWallet.Keys.OnChanged.Add(OnPrivateKeysChanged); TUserInterface.AccountsChanged.Add(OnAccountsChanged); TUserInterface.BlocksChanged.Add(OnBlocksChanged); + TUserInterface.AppStarted.Add(OnViewReady); + TUserInterface.StateChanged.Add(OnViewReady); Refresh; end; procedure TFRMAccountExplorer.FormDestroy(Sender: TObject); begin + FStopping := True; + if Assigned(FBackendRefresh) then FBackendRefresh.Remove(OnBackendReady); // Unsubscribe from events TUserInterface.Loaded.Remove(OnLoaded); TWallet.Keys.OnChanged.Remove(OnPrivateKeysChanged); TUserInterface.AccountsChanged.Remove(OnAccountsChanged); TUserInterface.BlocksChanged.Remove(OnBlocksChanged); + TUserInterface.AppStarted.Remove(OnViewReady); + TUserInterface.StateChanged.Remove(OnViewReady); + + if Assigned(FAccountSnapshots) then begin + FAccountSnapshots.OnCompleted := nil; + FAccountSnapshots.Stop; + end; + if Assigned(FAccountsGrid) then begin + FAccountsGrid.OnReadAccount := nil; + FAccountsGrid.OnReadSummary := nil; + FAccountsGrid.OnUpdated := nil; + FAccountsGrid.OnAccountsGridUpdatedData := nil; + FAccountsGrid.OnRefreshStateChanged := nil; + end; + if Assigned(FAccountsSelectedGrid) then begin + FAccountsSelectedGrid.OnReadAccount := nil; + FAccountsSelectedGrid.OnUpdated := nil; + end; // Nullify fields - FAccountOperationsGrid.Node := Nil; - FAccountsGrid.Node := Nil; - FAccountsSelectedGrid.Node := Nil; - FAccountsGrid.Node := Nil; + if Assigned(FAccountOperationsGrid) then FAccountOperationsGrid.Node := Nil; + if Assigned(FAccountsGrid) then FAccountsGrid.Node := Nil; + if Assigned(FAccountsSelectedGrid) then FAccountsSelectedGrid.Node := Nil; // Note: grids themselves are collected with Self (TComponent dependency) end; @@ -274,8 +347,106 @@ procedure TFRMAccountExplorer.FormDestroy(Sender: TObject); procedure TFRMAccountExplorer.Refresh; begin - RefreshAccountsGrid(true); + if FStopping or TUserInterface.Stopping then Exit; RefreshMyKeysCombo; + FSelectedAccountsRefreshPending := True; + FAccountSnapshots.Invalidate; + RefreshAccountsGrid(true); +end; + +function TFRMAccountExplorer.CreateAccountSnapshots: TExplorerAccountSnapshots; +begin + Result := TExplorerAccountSnapshots.Create(Self, TUserInterface.Node); +end; + +function TFRMAccountExplorer.ReadAccount(Sender: TObject; AAccountNumber: Integer; out AAccount: TAccount; out ABlockCount, AAccountCount: Integer): Boolean; +begin + Result := FAccountSnapshots.TryGetAccount(AAccountNumber, AAccount, + ABlockCount, AAccountCount); +end; + +function TFRMAccountExplorer.ReadSummary(Sender: TObject; out AAccountCount: Integer; out ABalance: Int64): Boolean; +begin + Result := FAccountSnapshots.TryGetSummary(AAccountCount, ABalance); +end; + +procedure TFRMAccountExplorer.OnSnapshotUpdated(Sender: TObject); +begin + if FStopping or TUserInterface.Stopping then Exit; + if FAccountsGrid.AccountsGridDatasource = acds_Node then + FAccountsGrid.UpdateData + else dgAccounts.Invalidate; + dgSelectedAccounts.Invalidate; + // A data revision changes selected totals; viewport batches only repaint. + if FSelectedAccountsRefreshPending then begin + FSelectedAccountsRefreshPending := False; + FAccountsSelectedGrid.UpdateData; + end; + OnAccountsGridUpdatedData(Self); + OnSelectedAccountChanged; + TryRefreshBackend; +end; + +procedure TFRMAccountExplorer.InitializeBackendRefresh; +begin + FBackendRefresh := TThrottledEvent.Create(Self); + FBackendRefresh.Mode := temNotifyOnEventBurstStartAndFinished; + FBackendRefresh.Interval := TTimeSpan.FromSeconds(5); + FBackendRefresh.Add(OnBackendReady); +end; + +function TFRMAccountExplorer.CanRefreshBackend: Boolean; +begin + Result := IsVisible and TUserInterface.Started and + (not FAccountsGrid.IsUpdatingData) and (not FAccountSnapshots.Updating); +end; + +procedure TFRMAccountExplorer.RefreshBackend; +begin + FSelectedAccountsRefreshPending := True; + FAccountSnapshots.Invalidate; + RefreshAccountsGrid(True); + FAccountOperationsGrid.UpdateAccountOperations; +end; + +procedure TFRMAccountExplorer.ScheduleBackendRefresh; +begin + if FStopping or TUserInterface.Stopping then Exit; + FBackendPending := True; + FBackendRefresh.Notify; +end; + +procedure TFRMAccountExplorer.OnBackendReady(Sender: TObject); +begin + if FStopping or TUserInterface.Stopping then Exit; + // Only inputs delivered to the owned throttle determine its deadline. + // A late callback after deferred work was consumed must not create new work. + if not FBackendPending then Exit; + FBackendReady := True; + TryRefreshBackend; +end; + +procedure TFRMAccountExplorer.TryRefreshBackend; +begin + if FStopping or TUserInterface.Stopping or not FBackendPending or not FBackendReady then Exit; + if not CanRefreshBackend then Exit; + FBackendPending := False; + FBackendReady := False; + RefreshBackend; +end; + +procedure TFRMAccountExplorer.DoShow; +begin + inherited; + OnViewReady(Self); +end; + +procedure TFRMAccountExplorer.OnViewReady(Sender: TObject); +begin + if FStopping or TUserInterface.Stopping then Exit; + // Visibility, application state and source completion consume already admitted + // work. They never Notify or extend the owned five-second quiet deadline. + TryRefreshBackend; end; {%endregion} @@ -291,7 +462,7 @@ procedure TFRMAccountExplorer.RefreshAccountsGrid(RefreshData : Boolean); acc : TAccount; LFilters : TAccountsGridFilter; begin - If Not Assigned(FOrderedAccountsKeyList) Then exit; + if FStopping or not Assigned(FAccountsGrid) then Exit; if Not RefreshData then begin if TPlatform.GetElapsedMilliseconds(FLastAccountsGridInvalidateTC)>1000 then begin @@ -308,18 +479,19 @@ procedure TFRMAccountExplorer.RefreshAccountsGrid(RefreshData : Boolean); LFilters := FAccountsGrid.AccountsGridFilter; LFilters.MinBalance := FMinAccountBalance; LFilters.MaxBalance := FMaxAccountBalance; + LFilters.UseAccountKey := False; + LFilters.AccountKey := CT_TECDSA_Public_Nul; if cbExploreMyAccounts.Checked then begin - TNode.Node.Bank.SafeBox.StartThreadSafe; - try - LFilters.OrderedAccountsKeyList := TWallet.Keys.AccountsKeyList; - if cbMyPrivateKeys.ItemIndex>0 then begin - i := PtrInt(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]); - if (i>=0) And (i0 then begin + i := PtrInt(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]); + if (i>=0) And (i=0) then last_i := PtrInt(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]) @@ -346,9 +518,6 @@ procedure TFRMAccountExplorer.RefreshMyKeysCombo; cbMyPrivateKeys.Items.Clear; For i:=0 to TWallet.Keys.Count-1 do begin wk := TWallet.Keys.Key[i]; - if assigned(FOrderedAccountsKeyList) then begin - FOrderedAccountsKeyList.AddAccountKey(wk.AccountKey); - end; if (wk.Name='') then begin s := 'Sha256='+TCrypto.ToHexaString( TCrypto.DoSha256( TAccountComp.AccountKey2RawString(wk.AccountKey) ) ); end else begin @@ -404,24 +573,36 @@ function TFRMAccountExplorer.DoUpdateAccountsFilter: Boolean; procedure TFRMAccountExplorer.OnLoaded(Sender: TObject); begin - RefreshAccountsGrid(true); + ScheduleBackendRefresh; end; procedure TFRMAccountExplorer.OnAccountsChanged(Sender: TObject); begin - RefreshAccountsGrid(true); + ScheduleBackendRefresh; end; procedure TFRMAccountExplorer.OnBlocksChanged(Sender: TObject); begin - RefreshAccountsGrid(false); + ScheduleBackendRefresh; end; procedure TFRMAccountExplorer.OnPrivateKeysChanged(Sender: TObject); begin + if FStopping or TUserInterface.Stopping then Exit; + if TThread.CurrentThread.ThreadID <> MainThreadID then begin + // Wallet notifications may originate in RPC. Never read key choices or + // mutate controls there; native messages disappear with their receiver. + if HandleAllocated then PostMessage(Handle, CM_ExplorerKeysChanged, 0, 0); + Exit; + end; Refresh; end; +procedure TFRMAccountExplorer.CM_PrivateKeysChanged(var AMsg: TMessage); +begin + OnPrivateKeysChanged(nil); +end; + procedure TFRMAccountExplorer.OnSelectedAccountChanged; Var accn : Int64; begin @@ -429,6 +610,24 @@ procedure TFRMAccountExplorer.OnSelectedAccountChanged; FAccountOperationsGrid.AccountNumber := accn; end; +procedure TFRMAccountExplorer.OnAccountsGridUpdatedData(Sender: TObject); +begin + if FStopping or TUserInterface.Stopping then Exit; + if FAccountsGrid.IsUpdatingData or + ((FAccountsGrid.AccountsGridDatasource = acds_Node) and FAccountSnapshots.Updating) then begin + lblAccountsCount.Caption := '(Calculating)'; + lblAccountsBalance.Caption := '(Calculating)'; + end else begin + lblAccountsCount.Caption := IntToStr(FAccountsGrid.AccountsCount); + lblAccountsBalance.Caption := TAccountComp.FormatMoney(FAccountsGrid.AccountsBalance); + // A cached new page may require no snapshot callback. Its completed row + // replacement must still bind history to the account now selected. + if Assigned(FAccountOperationsGrid) then OnSelectedAccountChanged; + end; + // The account/filter worker has published; retry a previously admitted input. + OnViewReady(Sender); +end; + {%endregion} {%region Event Handlers: Combo Boxes} @@ -471,6 +670,8 @@ procedure TFRMAccountExplorer.bbChangeKeyNameClick(Sender: TObject); procedure TFRMAccountExplorer.bbAccountsRefreshClick(Sender: TObject); begin + FSelectedAccountsRefreshPending := True; + FAccountSnapshots.Invalidate; RefreshAccountsGrid(true); end; @@ -583,22 +784,34 @@ procedure TFRMAccountExplorer.ebFilterAccountByBalanceMinKeyPress(Sender: TObjec end; procedure TFRMAccountExplorer.ebFindAccountNumberChange(Sender: TObject); -Var an : Cardinal =0; +var + an: Cardinal; + accountName: TRawBytes; + matches: TOrderedRawList; + disposed: TDisposables; + found: Boolean; begin + an := 0; if Trim(ebFindAccountNumber.Text)='' then begin ebFindAccountNumber.Color := clWindow; ebFindAccountNumber.Font.Color := clDkGray; - end else if TAccountComp.AccountTxtNumberToAccountNumber(ebFindAccountNumber.Text,an) then begin - ebFindAccountNumber.Color := clWindow; - if FAccountsGrid.MoveRowToAccount(an) then begin - ebFindAccountNumber.Font.Color := clWindowText; + end else begin + found := TAccountComp.AccountTxtNumberToAccountNumber(ebFindAccountNumber.Text, an); + if not found then begin + accountName.FromString(ebFindAccountNumber.Text); + matches := disposed.AddObject(TOrderedRawList.Create) as TOrderedRawList; + found := TUserInterface.Node.Bank.SafeBox.FindAccountsStartingByName(accountName, matches, 1) > 0; + if found then an := matches.GetTag(0); + end; + if found then begin + ebFindAccountNumber.Color := clWindow; + if FAccountsGrid.MoveRowToAccount(an) then + ebFindAccountNumber.Font.Color := clWindowText + else ebFindAccountNumber.Font.Color := clRed; end else begin - ebFindAccountNumber.Font.Color := clRed; + ebFindAccountNumber.Color := clRed; + ebFindAccountNumber.Font.Color := clWindowText; end; - end else begin - // Invalid value - ebFindAccountNumber.Color := clRed; - ebFindAccountNumber.Font.Color := clWindowText; end; end; @@ -685,7 +898,7 @@ procedure TFRMAccountExplorer.miFindNextAccountWithHighBalanceClick(Sender: TObj procedure TFRMAccountExplorer.miAccountInformationClick(Sender: TObject); Var F : TFRMMemoText; - accn : Int64 =-1; + accn : Int64; acc : TAccount; i : Integer; opr : TOperationResume; @@ -703,7 +916,7 @@ procedure TFRMAccountExplorer.miAccountInformationClick(Sender: TObject); i := FAccountOperationsGrid.DrawGrid.Row; if (i>0) and (i<=FAccountOperationsGrid.OperationsResume.Count) then begin - opr := FAccountOperationsGrid.OperationsResume.OperationResume[i-1]; + opr := FAccountOperationsGrid.OperationsResume[i-1]; end; If opr.valid then diff --git a/src/gui-experimental/UFRMAccountInfo.dfm b/src/gui-experimental/UFRMAccountInfo.dfm new file mode 100644 index 000000000..72a8f1a44 --- /dev/null +++ b/src/gui-experimental/UFRMAccountInfo.dfm @@ -0,0 +1,99 @@ +object FRMAccountInfo: TFRMAccountInfo + Left = 0 + Height = 300 + Top = 0 + Width = 635 + BorderIcons = [biSystemMenu] + Caption = 'Account Information' + ClientHeight = 300 + ClientWidth = 635 + Color = clBtnFace + Font.Color = clWindowText + Font.Name = 'Tahoma' + Position = poOwnerFormCenter + Visible = False + object Label1: TLabel + Left = 25 + Height = 19 + Top = 26 + Width = 57 + Caption = 'Account' + ParentColor = False + end + object lblAccount: TLabel + Left = 77 + Height = 19 + Top = 21 + Width = 97 + Caption = '0000000-00' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object Label6: TLabel + Left = 24 + Height = 19 + Top = 56 + Width = 71 + Caption = 'Public key' + ParentColor = False + end + object lblBalance: TLabel + Left = 252 + Height = 19 + Top = 21 + Width = 97 + Caption = '0000000-00' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object Label3: TLabel + Left = 200 + Height = 19 + Top = 26 + Width = 53 + Caption = 'Balance' + ParentColor = False + end + object Label2: TLabel + Left = 25 + Height = 19 + Top = 91 + Width = 35 + Caption = 'State' + ParentColor = False + end + object Label4: TLabel + Left = 77 + Height = 19 + Top = 86 + Width = 200 + Caption = 'Normal/For sale/Private' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object ebPublicKey: TEdit + Left = 77 + Height = 27 + Top = 51 + Width = 533 + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + Text = 'ebPublicKey' + end +end diff --git a/src/gui-experimental/UFRMAccountInfo.pas b/src/gui-experimental/UFRMAccountInfo.pas index ab5296668..5af1adbd7 100644 --- a/src/gui-experimental/UFRMAccountInfo.pas +++ b/src/gui-experimental/UFRMAccountInfo.pas @@ -13,6 +13,16 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Defines the account-information form layout for account number, balance and + public key. } + +{ Public types in this unit + + Type | Purpose + ----------------+------------------------------------------------------------------------- + TFRMAccountInfo | Provides the account-information form layout. +} + interface {$I ..\config.inc} @@ -39,6 +49,10 @@ TFRMAccountInfo = class(TApplicationForm) implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} end. diff --git a/src/gui-experimental/UFRMAccountSelect.dfm b/src/gui-experimental/UFRMAccountSelect.dfm new file mode 100644 index 000000000..16bd5e18a --- /dev/null +++ b/src/gui-experimental/UFRMAccountSelect.dfm @@ -0,0 +1,258 @@ +object FRMAccountSelect: TFRMAccountSelect + Left = 534 + Height = 322 + Top = 308 + Width = 601 + ActiveControl = cbOnlyForSale + BorderIcons = [biSystemMenu, biMaximize] + Caption = 'Accounts' + ClientHeight = 322 + ClientWidth = 601 + Color = clBtnFace + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + object pnlAccountsTop: TPanel + Left = 395 + Height = 282 + Top = 0 + Width = 206 + Align = alRight + BevelOuter = bvNone + TabOrder = 0 + object cbOnlyForSale: TCheckBox + Left = 10 + Height = 19 + Top = 65 + Width = 81 + Caption = 'Only for sale or swap' + TabOrder = 0 + end + object bbSearch: TBitBtn + Left = 56 + Height = 36 + Top = 240 + Width = 86 + Caption = 'Search' + Glyph.Data = { + F6060000424DF606000000000000360000002800000018000000180000000100 + 180000000000C0060000120B0000120B00000000000000000000FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFABA7A68C8888C09596FF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFACA8A786868A2A669A4B7BA0C09494FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF9B9D9D5492C22E99FF1B7C + CB557A97C19595FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF3DADFF + 57C1FF4DB1FF3196FA197CC9537997C19495FF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FF41ABFF5DBFFE4DAFFF3196FA197BC85A7A97C29594FF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF42ABFE5CC0FE4DAFFF3096FA19 + 7AC8587A96C19494FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF42AC + FE5CC0FE4DAFFF2E94FA187AC75F7C97BB9396FF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FF44ADFE5CBFFE4DB0FF2D94F81979C6437397FF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF46AEFE5CBFFE4CB0FF3F97ED + 577B94FF00FFFF00FFFF00FFAE837EAE837EAE837EAE837EAE837EFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF46 + AFFE66C5FFA3CCF1BEB3AB867170A9807AAE837EE5D8BDFFFFE2FFFFE2FFFFE0 + EEDFC6AE837EAE837EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFDCBFB8C69E8ED8BDA3FFFFDDFFFFDDFF + FFDBFFFFDAFFFFDBFFFFDDFFFFE9DEC9B7AE837EFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFAE857FDEC0A5FFF3 + C1FCF1C8FFFFDAFFFFD9FFFFDAFFFFE0FFFFE4FFFFEFFFFFFFDDCAC8AE837EFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + AE837EFFEFB9F5D4A5FBF0C6FFFFDBFFFFDAFFFFDDFFFFEBFFFFF5FFFFFBFFFF + FDFFFFFEAE837EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFAE837EECD5B4F8D6A2F2C997FCEDC4FFFFDBFFFFDAFFFFE0FFFFF0 + FFFFFEFFFFFDFFFFF5FFFFEBE1D3B8AE837EFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFAE837EFFF3BFF4CB95F0C18EF9E5BAFEFDD7FF + FFDCFFFFDFFFFFECFFFFF8FFFFF8FFFFECFFFFE0FFFFE3AE837EFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFAE837EFFF4C0F3C994EEB8 + 84F5D5A5FDF3CBFFFFDCFFFFDDFFFFE2FFFFE8FFFFE9FFFFE2FFFFDDFFFFE3AE + 837EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFAE837E + FFF5C3F6D19CEEB27DF1C593F7E0B3FDF7D0FFFFDCFFFFDDFFFFDDFFFFDDFFFF + DBFFFFDAFFFFE1AE837EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFAE837EECD5B4FDE6B2F3C996F2C792F3D09CF8E3B5FCF5CDFFFFDA + FFFFDCFFFFDBFFFFDBFFFFDCE2D3B8AE837EFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFAE837EFFFED0FCF0D6F8E0BEF4CF9DF3 + CD9AF6D8AAFBEABFFDF4CBFCF7CFFEF8D1FFFFDEAE837EFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFAE837EE0CAB2FFFF + FFFEF9F2F8E1BEF1C48DEEBA85F1C491F4D1A1F8DEB0FFF7C6DDC7AEAE837EFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFAE837ED9C6C0FFFFFFFFF7D7F8DAA6F4CB95F5CD99F9D9A6FFF1BBDEC2 + A9AE837EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFAE837EAE837EE8D5B4FFF9C5FFF5C0FFF5C2 + EED8B7AE837EAE837EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFAE837EAE + 837EAE837EAE837EAE837EFF00FFFF00FFFF00FFFF00FFFF00FF + } + OnClick = bbSearchClick + TabOrder = 1 + end + object cbOnlyForPrivateSaleToMe: TCheckBox + Left = 10 + Height = 19 + Top = 111 + Width = 148 + Caption = 'Only for private sale to me' + TabOrder = 2 + end + object cbAccountsBalance: TCheckBox + Left = 10 + Height = 19 + Top = 134 + Width = 104 + Caption = 'Accounts Balance' + TabOrder = 3 + end + object cbMyAccounts: TCheckBox + Left = 10 + Height = 19 + Top = 10 + Width = 81 + Caption = 'My Accounts' + OnClick = cbMyAccountsClick + TabOrder = 4 + end + object cbMyPrivateKeys: TComboBox + Left = 10 + Height = 21 + Top = 35 + Width = 176 + ItemHeight = 13 + OnChange = cbMyPrivateKeysChange + Style = csDropDownList + TabOrder = 5 + end + object cbOnlyForPublicSale: TCheckBox + Left = 10 + Height = 19 + Top = 88 + Width = 111 + Caption = 'Only for public sale' + TabOrder = 6 + end + object ebMinBalance: TEdit + Left = 16 + Height = 21 + Top = 160 + Width = 73 + OnExit = ebMinBalanceExit + TabOrder = 7 + end + object ebMaxBalance: TEdit + Left = 103 + Height = 21 + Top = 160 + Width = 73 + OnExit = ebMaxBalanceExit + TabOrder = 8 + end + object cbAccountsName: TCheckBox + Left = 10 + Height = 19 + Top = 186 + Width = 93 + Caption = 'Accounts name' + TabOrder = 9 + end + object ebAccountName: TEdit + Left = 18 + Height = 21 + Top = 209 + Width = 158 + TabOrder = 10 + end + end + object pnlBottom: TPanel + Left = 0 + Height = 40 + Top = 282 + Width = 601 + Align = alBottom + BevelOuter = bvNone + TabOrder = 1 + object bbSelect: TBitBtn + Left = 394 + Height = 30 + Top = 5 + Width = 95 + Anchors = [akTop, akRight] + Caption = 'Select' + Default = True + Kind = bkOK + ModalResult = 1 + TabOrder = 0 + end + object bbCancel: TBitBtn + Left = 498 + Height = 30 + Top = 5 + Width = 95 + Anchors = [akTop, akRight] + Cancel = True + Caption = 'Cancel' + Kind = bkCancel + ModalResult = 2 + TabOrder = 1 + end + object Label17: TLabel + Left = 5 + Height = 13 + Top = 10 + Width = 48 + Caption = 'Accounts:' + ParentColor = False + end + object lblAccountsCount: TLabel + Left = 60 + Height = 13 + Top = 10 + Width = 18 + Caption = '000' + ParentColor = False + end + object Label19: TLabel + Left = 136 + Height = 13 + Top = 10 + Width = 41 + Caption = 'Balance:' + ParentColor = False + end + object lblAccountsBalance: TLabel + Left = 200 + Height = 13 + Top = 10 + Width = 18 + Caption = '000' + ParentColor = False + end + object bbTypeStats: TBitBtn + Left = 280 + Height = 30 + Top = 5 + Width = 95 + Caption = 'Type stats' + OnClick = bbTypeStatsClick + TabOrder = 2 + end + end + object dgAccounts: TDrawGrid + Left = 0 + Height = 282 + Top = 0 + Width = 395 + Align = alClient + TabOrder = 2 + end +end diff --git a/src/gui-experimental/UFRMAccountSelect.lfm b/src/gui-experimental/UFRMAccountSelect.lfm index e67657f40..22898ee71 100644 --- a/src/gui-experimental/UFRMAccountSelect.lfm +++ b/src/gui-experimental/UFRMAccountSelect.lfm @@ -31,7 +31,7 @@ object FRMAccountSelect: TFRMAccountSelect Height = 19 Top = 65 Width = 81 - Caption = 'Only for sale' + Caption = 'Only for sale or swap' TabOrder = 0 end object bbSearch: TBitBtn diff --git a/src/gui-experimental/UFRMAccountSelect.pas b/src/gui-experimental/UFRMAccountSelect.pas index de1bb581b..c8217aef0 100644 --- a/src/gui-experimental/UFRMAccountSelect.pas +++ b/src/gui-experimental/UFRMAccountSelect.pas @@ -16,58 +16,48 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$MODE Delphi} +{$ENDIF} + +{ Selects an account using owner, sale, balance and name filters. Queries and + account-type statistics run outside the UI thread. } + +{ Public types in this unit + + Type | Purpose + ------------------+------------------------------------------------------------------------- + TFRMAccountSelect | Filters accounts and returns the chosen account number. +} interface {$I ..\config.inc} uses - LCLIntf, LCLType, LMessages, - Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, - Dialogs, UAccounts, Grids, StdCtrls, Buttons, ExtCtrls, UCommon.UI, - UWallet, UNode, UGridUtils, UFRMMemoText, UConst, UThread, UPCOrderedLists, UBaseTypes, UPCDataTypes; + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + {$IFDEF FPC}LCLIntf, LCLType, LMessages,{$ENDIF} + Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, + UAccounts, Grids, StdCtrls, Buttons, ExtCtrls, UCommon.UI, UWallet, UNode, + UConst, UThread, UPCOrderedLists, UBaseTypes, UPCDataTypes, + Generics.Collections, UVisualGrid, UCommon.Data, UAccountSelectorData, + UWalletStartupTask; const CT_AS_MyAccounts = $0001; CT_AS_OnlyForSale = $0002; type - { TSearchThread } - TSearchValues = Record - SafeBox : TPCSafeBox; - inWalletKeys : TWalletKeys; - inAccountKey : TAccountKey; - onlyForSale, - onlyForPublicSale, - onlyForPrivateSaleToMe : Boolean; - minBal,maxBal : Int64; - searchName : TRawBytes; - end; - - TSearchProcedure = procedure(Const searchFound : TCardinalsArray; const searchValues : TSearchValues) of object; - - TSearchThread = Class(TPCThread) + TAccountSelectorStatsTask = class(TWalletStartupTask) private - FIsReadyForSearch: Boolean; - FOnSearchFinished: TSearchProcedure; - FDoStopSearch : Boolean; - FStartSearch : Boolean; - FAccounts : TCardinalsArray; - FSearchValues : TSearchValues; - procedure SetIsReadyForSearch(AValue: Boolean); + FSource: TAccountSelectorDataSource; protected - procedure BCExecute; override; - Procedure DoNotifySearchFinished; + procedure ExecuteLoad; override; public - property IsReadyForSearch : Boolean read FIsReadyForSearch write SetIsReadyForSearch; - Property OnSearchFinished : TSearchProcedure read FOnSearchFinished write FOnSearchFinished; - Procedure DoSearch(Const newSearchValues : TSearchValues); - Procedure StopSearch; + Stats: TAccountSelectorTypeStats; + constructor Create(ASource: TAccountSelectorDataSource); reintroduce; end; - { TFRMAccountSelect } - TFRMAccountSelect = class(TApplicationForm) bbTypeStats: TBitBtn; cbAccountsName: TCheckBox; @@ -83,7 +73,7 @@ TFRMAccountSelect = class(TApplicationForm) bbSearch: TBitBtn; cbOnlyForPrivateSaleToMe: TCheckBox; pnlBottom: TPanel; - dgAccounts: TDrawGrid; + dgAccounts: Grids.TDrawGrid; bbSelect: TBitBtn; bbCancel: TBitBtn; cbAccountsBalance: TCheckBox; @@ -99,473 +89,500 @@ TFRMAccountSelect = class(TApplicationForm) procedure ebMinBalanceExit(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); + private type + TKeyChoice = class + Key: TAccountKey; + Available: Boolean; + end; private - FAllowSelect: Boolean; + FAllowSelect, FChangingControls, FStopping, FDefaultSelectionApplied: Boolean; FDefaultAccount: Int64; FFilters: Integer; FWalletKeys: TWalletKeys; FNode: TNode; - FSafeBox : TPCSafeBox; - FAccountsGrid : TAccountsGrid; - FSearchThread : TSearchThread; - { Private declarations } + FSafeBox: TPCSafeBox; + FAccountsGrid: TVisualGrid; + FSource: TAccountSelectorDataSource; + FStatsTask: TAccountSelectorStatsTask; + FKeyChoices: TObjectList; procedure SetAllowSelect(AValue: Boolean); procedure SetDefaultAccount(AValue: Int64); procedure SetFilters(AValue: Integer); procedure SetNode(const Value: TNode); procedure SetWalletKeys(const Value: TWalletKeys); - Procedure SearchFiltered; - Procedure UpdateControls; - procedure OnSearchFinished(Const searchFound : TCardinalsArray; const searchValues : TSearchValues); - procedure OnAccountsGridUpdated(Sender : TObject); + procedure SearchFiltered; + procedure UpdateControls; + procedure OnAccountsGridUpdated(Sender: TObject); + procedure OnGridSelection(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); + procedure OnStatsCompleted(Sender: TObject); + function GetStatsRunning: Boolean; protected - FAccounts : TOrderedAccountList; + function AccountSafeBox: TPCSafeBox; virtual; + function CreateAccountSource: TAccountSelectorDataSource; virtual; + function CreateStatsTask: TAccountSelectorStatsTask; virtual; + procedure BuildTypeStatsReport(const AStats: TAccountSelectorTypeStats; AText: TStrings); + procedure ShowTypeStatsReport(const AStats: TAccountSelectorTypeStats); virtual; public - { Public declarations } - Property WalletKeys : TWalletKeys read FWalletKeys write SetWalletKeys; - Property Node : TNode read FNode write SetNode; - Property Filters : Integer read FFilters write SetFilters; - Property DefaultAccount : Int64 read FDefaultAccount write SetDefaultAccount; - Function GetSelected : Int64; - Property AllowSelect : Boolean read FAllowSelect write SetAllowSelect; + property WalletKeys: TWalletKeys read FWalletKeys write SetWalletKeys; + property Node: TNode read FNode write SetNode; + property Filters: Integer read FFilters write SetFilters; + property DefaultAccount: Int64 read FDefaultAccount write SetDefaultAccount; + function GetSelected: Int64; + property AllowSelect: Boolean read FAllowSelect write SetAllowSelect; + property AccountGrid: TVisualGrid read FAccountsGrid; + property AccountSource: TAccountSelectorDataSource read FSource; + property StatsRunning: Boolean read GetStatsRunning; end; implementation - {$R *.lfm} +{$IFDEF FPC}{$R *.lfm}{$ELSE}{$R *.dfm}{$ENDIF} -Uses strutils; +uses UFRMMemoText; -{ TSearchThread } +{$IFNDEF FPC} +type + TSelectorNativeGridAccess = class(UVisualGrid.TDrawGrid) + public + procedure PublishCurrentSelection; + end; -procedure TSearchThread.SetIsReadyForSearch(AValue: Boolean); +procedure TSelectorNativeGridAccess.PublishCurrentSelection; begin - if FIsReadyForSearch=AValue then Exit; - FIsReadyForSearch:=AValue; + // The native adapter retains its own ranges, including a one-row page whose + // current row does not move. Reuse its normal selection publication path. + SelectionMoved(Selection); end; +{$ENDIF} -procedure TSearchThread.BCExecute; - procedure SearchFilteredInThread; - Var c,maxC : Cardinal; - account : TAccount; - isValid : Boolean; - validAccKey : Boolean; - errors : AnsiString; - i : Integer; - begin - SetLength(FAccounts,0); - c := 0; - maxC := FSearchValues.SafeBox.AccountsCount-1; - validAccKey := TAccountComp.IsValidAccountKey(FSearchValues.inAccountKey, FSearchValues.SafeBox.CurrentProtocol, errors); - while (c<=maxC) And (Not Terminated) And (Not FDoStopSearch) do begin - account := FSearchValues.SafeBox.Account(c); - isValid := True; - If validAccKey then begin - isValid := TAccountComp.EqualAccountKeys(account.accountInfo.accountKey,FSearchValues.inAccountKey); - end else if (Assigned(FSearchValues.inWalletKeys)) then begin - isValid := FSearchValues.inWalletKeys.IndexOfAccountKey(account.accountInfo.accountKey)>=0; - end; - If isValid And (FSearchValues.onlyForSale) then begin - isValid := TAccountComp.IsAccountForSale(account.accountInfo); - end; - If IsValid and (FSearchValues.onlyForPublicSale) then begin - isValid := (TAccountComp.IsAccountForSale(account.accountInfo)) And (Not TAccountComp.IsAccountForSaleOrSwapAcceptingTransactions(account, FSearchValues.SafeBox.BlocksCount, FSearchValues.SafeBox.CurrentProtocol, nil)); // Skybuck: todo check/debug ! - end; - If IsValid and (FSearchValues.onlyForPrivateSaleToMe) then begin - isValid := ( TAccountComp.IsAccountForSaleOrSwapAcceptingTransactions(account,FSearchValues.SafeBox.BlocksCount, FSearchValues.SafeBox.CurrentProtocol, nil)) And // Skybuck: todo check/debug ! - (Assigned(FSearchValues.inWalletKeys)) And (FSearchValues.inWalletKeys.IndexOfAccountKey(account.accountInfo.new_publicKey)>=0); - end; - If IsValid then begin - IsValid := (account.balance>=FSearchValues.minBal) And ((FSearchValues.maxBal<0) Or (account.balance<=FSearchValues.maxBal)); - end; - If IsValid And (Length(FSearchValues.searchName)>0) then begin - i := TBaseType.FindIn(FSearchValues.searchName,account.name); - IsValid := i>0; - end; - // - if IsValid then begin - setLength(FAccounts,length(FAccounts)+1); - FAccounts[high(FAccounts)] := c; - end; - inc(c); - end; - If (Not Terminated) And (Not FDoStopSearch) then begin - Synchronize(DoNotifySearchFinished); - end; - end; +constructor TAccountSelectorStatsTask.Create(ASource: TAccountSelectorDataSource); begin - FIsReadyForSearch := True; - FDoStopSearch := False; - FStartSearch := False; - while (Not Terminated) do begin - FIsReadyForSearch := True; - If (FStartSearch) then begin - FStartSearch := False; - FIsReadyForSearch:=False; - FDoStopSearch:=False; - SearchFilteredInThread; - end; - end; + inherited Create; + FSource := ASource; end; -procedure TSearchThread.DoNotifySearchFinished; +procedure TAccountSelectorStatsTask.ExecuteLoad; begin - If Assigned(FOnSearchFinished) then begin - FOnSearchFinished(FAccounts,FSearchValues); - end; + CheckCancelled; + FSource.CalculateTypeStats(Stats); + CheckCancelled; end; -procedure TSearchThread.DoSearch(const newSearchValues: TSearchValues); +function TFRMAccountSelect.CreateAccountSource: TAccountSelectorDataSource; begin - FDoStopSearch := True; - FStartSearch := False; - While (Not FIsReadyForSearch) do sleep(1); - FSearchValues := newSearchValues; - FStartSearch := True; + Result := TAccountSelectorDataSource.Create(Self); end; -procedure TSearchThread.StopSearch; +function TFRMAccountSelect.AccountSafeBox: TPCSafeBox; begin - FDoStopSearch := True; - FStartSearch := False; - While (Not FIsReadyForSearch) do sleep(1); + Result := FSafeBox; end; -{ TFRMAccountSelect } - -procedure TFRMAccountSelect.SetDefaultAccount(AValue: Int64); +function TFRMAccountSelect.CreateStatsTask: TAccountSelectorStatsTask; begin - if FDefaultAccount=AValue then Exit; - FDefaultAccount:=AValue; + Result := TAccountSelectorStatsTask.Create(FSource); end; -procedure TFRMAccountSelect.SetAllowSelect(AValue: Boolean); +procedure TFRMAccountSelect.FormCreate(Sender: TObject); + + procedure AddColumn(const ACaption, ABinding: String; AWidth: Integer); + begin + with FAccountsGrid.AddColumn(ACaption) do begin + Binding := ABinding; + Width := AWidth; + end; + end; + begin - FAllowSelect:=AValue; - bbSelect.Visible:=AValue; - bbSelect.Enabled:=AValue; + FChangingControls := True; + FDefaultAccount := -1; + FKeyChoices := TObjectList.Create(True); + FAccountsGrid := TVisualGrid.Create(Self); + FAccountsGrid.Name := 'AccountSelectorGrid'; + FAccountsGrid.Parent := Self; + FAccountsGrid.Align := alClient; + FAccountsGrid.CanSearch := False; + FAccountsGrid.SortMode := smNone; + FAccountsGrid.PageMode := vpmAuto; + FAccountsGrid.SelectionType := stRow; + FAccountsGrid.DeselectionType := dtDefault; + FAccountsGrid.Options := [vgoColSizing,vgoAutoHidePaging]; + FAccountsGrid.OnFinishedUpdating := OnAccountsGridUpdated; + FAccountsGrid.OnSelection := OnGridSelection; + AddColumn('Account No','Account',95); + AddColumn('Name','Name',140); + AddColumn('Balance','BalanceDecimal',105); + AddColumn('N Op.','NOperation',55); + AddColumn('Type','Type',55); + AddColumn('Price','PriceDecimal',95); + AddColumn('State','State',70); + dgAccounts.Visible := False; + FSource := CreateAccountSource; + cbMyAccounts.OnClick := cbMyAccountsClick; + {$IFDEF FPC}cbMyAccounts.OnChange := nil;{$ENDIF} + // Opening a combo is not a key change; only its selection event refreshes. + cbMyPrivateKeys.OnClick := nil; + cbMyPrivateKeys.OnChange := cbMyPrivateKeysChange; + cbOnlyForSale.OnClick := cbMyAccountsClick; + cbOnlyForPrivateSaleToMe.OnClick := cbMyAccountsClick; + cbOnlyForPublicSale.OnClick := cbMyAccountsClick; + cbAccountsBalance.OnClick := cbMyAccountsClick; + cbAccountsName.OnClick := cbMyAccountsClick; + FChangingControls := False; + UpdateControls; + lblAccountsCount.Caption := '-'; + lblAccountsBalance.Caption := '-'; + AllowSelect := False; end; -procedure TFRMAccountSelect.SetFilters(AValue: Integer); +procedure TFRMAccountSelect.FormDestroy(Sender: TObject); begin - FFilters:=AValue; - cbMyAccounts.Checked := (AValue and CT_AS_MyAccounts) = CT_AS_MyAccounts; - cbOnlyForSale.Checked := (AValue and CT_AS_OnlyForSale) = CT_AS_OnlyForSale; - SearchFiltered; + FStopping := True; + // The existing task owns its queued completion and cooperatively joins; no + // worker can call Synchronize while this form is releasing its receivers. + if Assigned(FStatsTask) then begin + FStatsTask.OnCompleted := nil; + FStatsTask.CancelAndWait; + FreeAndNil(FStatsTask); + end; + if Assigned(FAccountsGrid) then begin + FAccountsGrid.OnFinishedUpdating := nil; + FAccountsGrid.OnSelection := nil; + FreeAndNil(FAccountsGrid); + end; + FreeAndNil(FSource); + FKeyChoices.Free; end; -procedure TFRMAccountSelect.FormCreate(Sender: TObject); +procedure TFRMAccountSelect.SetNode(const Value: TNode); begin - FSearchThread := TSearchThread.Create(false); - FSearchThread.OnSearchFinished := OnSearchFinished; - FAccounts := TOrderedAccountList.Create; - FWalletKeys := Nil; - FNode := Nil; - FSafeBox:=Nil; - FFilters:=0; - FDefaultAccount:=-1; - FAccountsGrid := TAccountsGrid.Create(Self); - FAccountsGrid.DrawGrid := dgAccounts; - FAccountsGrid.OnUpdated:=OnAccountsGridUpdated; - // - cbMyAccounts.OnClick:=cbMyAccountsClick; - cbMyPrivateKeys.OnClick:=cbMyAccountsClick; - cbOnlyForSale.OnClick:=cbMyAccountsClick; - cbOnlyForPrivateSaleToMe.OnClick:=cbMyAccountsClick; - cbOnlyForPublicSale.OnClick:=cbMyAccountsClick; - cbAccountsBalance.OnClick:=cbMyAccountsClick; - cbAccountsName.OnClick:=cbMyAccountsClick; - UpdateControls; - AllowSelect:=False; + FNode := Value; + FSafeBox := nil; + if Assigned(FNode) then FSafeBox := FNode.Bank.SafeBox; + SearchFiltered; end; -procedure TFRMAccountSelect.bbSearchClick(Sender: TObject); +procedure TFRMAccountSelect.SetWalletKeys(const Value: TWalletKeys); begin + FWalletKeys := Value; + UpdateControls; SearchFiltered; end; -procedure TFRMAccountSelect.bbTypeStatsClick(Sender: TObject); -Type - TAccTypeStats = Record - accounts:Integer; - balance:Int64; - end; -Const - TAccTypeStats_NUL : TAccTypeStats = (accounts:0;balance:0); -Var i,j,k : Integer; - account : TAccount; - c : Cardinal; - s_0,s_inv : TAccTypeStats; - s_5 : Array[0..6] of Array[1..4,0..9] of TAccTypeStats; - FRM : TFRMMemoText; - sl : TStringList; +procedure TFRMAccountSelect.UpdateControls; +var LSelected: TAccountKey; LHasSelected: Boolean; LChoice: TKeyChoice; I: Integer; begin - c := 0; - s_0 := TAccTypeStats_NUL; - s_inv := TAccTypeStats_NUL; - for i := low(s_5) to high(s_5) do - for j := 1 to 4 do - for k := 0 to 9 do - s_5[i,j,k] := TAccTypeStats_NUL; - - - while (c6) then k:=0; - for i := 0 to 9 do begin - j := ((account.account_type MOD 10000) DIV 1000); // j is 0..9 - if (j=i) then begin - inc(s_5[k][1][j].accounts); - inc(s_5[k][1][j].balance,account.balance); - end; - end; - for i := 0 to 9 do begin - j := ((account.account_type MOD 1000) DIV 100); // j is 0..9 - if (j=i) then begin - inc(s_5[k][2][j].accounts); - inc(s_5[k][2][j].balance,account.balance); + LHasSelected := (cbMyPrivateKeys.ItemIndex > 0) and + (cbMyPrivateKeys.ItemIndex < cbMyPrivateKeys.Items.Count); + if LHasSelected then + LSelected := TKeyChoice(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]).Key; + FChangingControls := True; + cbMyPrivateKeys.Items.BeginUpdate; + try + cbMyPrivateKeys.Clear; + FKeyChoices.Clear; + cbMyAccounts.Enabled := Assigned(FWalletKeys); + if not Assigned(FWalletKeys) then cbMyAccounts.Checked := False; + cbMyPrivateKeys.Sorted := True; + if Assigned(FWalletKeys) then + for I := 0 to FWalletKeys.Count-1 do begin + LChoice := TKeyChoice.Create; + LChoice.Key := FWalletKeys.Key[I].AccountKey; + LChoice.Available := True; + FKeyChoices.Add(LChoice); + cbMyPrivateKeys.Items.AddObject(FWalletKeys.Key[I].Name,LChoice); end; + cbMyPrivateKeys.Sorted := False; + cbMyPrivateKeys.Items.InsertObject(0,'(All)',nil); + cbMyPrivateKeys.ItemIndex := 0; + if LHasSelected then + for I := 1 to cbMyPrivateKeys.Items.Count-1 do + if TAccountComp.EqualAccountKeys(LSelected,TKeyChoice(cbMyPrivateKeys.Items.Objects[I]).Key) then begin + cbMyPrivateKeys.ItemIndex := I; + Break; + end; + if LHasSelected and (cbMyPrivateKeys.ItemIndex = 0) then begin + // A removed key is an unavailable selection, never an implicit All. + LChoice := TKeyChoice.Create; + LChoice.Key := LSelected; + FKeyChoices.Add(LChoice); + cbMyPrivateKeys.Items.InsertObject(1,'(Unavailable key)',LChoice); + cbMyPrivateKeys.ItemIndex := 1; end; - for i := 0 to 9 do begin - j := ((account.account_type MOD 100) DIV 10); // j is 0..9 - if (j=i) then begin - inc(s_5[k][3][j].accounts); - inc(s_5[k][3][j].balance,account.balance); - end; + cbMyPrivateKeys.Enabled := cbMyAccounts.Checked and Assigned(FWalletKeys); + finally + cbMyPrivateKeys.Items.EndUpdate; + FChangingControls := False; + end; +end; + +procedure TFRMAccountSelect.SearchFiltered; +var LFilter: TAccountSelectorFilter; I,LKeyCount: Integer; +begin + if FStopping or FChangingControls or not Assigned(FSource) then Exit; + LFilter := Default(TAccountSelectorFilter); + LFilter.MinBalance := 0; + LFilter.MaxBalance := -1; + LFilter.MyAccounts := cbMyAccounts.Checked and Assigned(FWalletKeys); + // These public identities were captured by the UI-owned WalletKeys setter. + // Searching never follows mutable wallet positions or subscribes controls + // to key notifications which may originate on an RPC worker. + SetLength(LFilter.WalletKeys,FKeyChoices.Count); + LKeyCount := 0; + for I := 0 to FKeyChoices.Count-1 do + if FKeyChoices[I].Available then begin + LFilter.WalletKeys[LKeyCount] := FKeyChoices[I].Key; + Inc(LKeyCount); end; - for i := 0 to 9 do begin - j := ((account.account_type MOD 10) DIV 1); // j is 0..9 - if (j=i) then begin - inc(s_5[k][4][j].accounts); - inc(s_5[k][4][j].balance,account.balance); + SetLength(LFilter.WalletKeys,LKeyCount); + if LFilter.MyAccounts then begin + LFilter.SpecificOwner := cbMyPrivateKeys.ItemIndex > 0; + if LFilter.SpecificOwner then begin + if (cbMyPrivateKeys.ItemIndex < cbMyPrivateKeys.Items.Count) and + TKeyChoice(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]).Available then begin + SetLength(LFilter.OwnerKeys,1); + LFilter.OwnerKeys[0] := TKeyChoice(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]).Key; end; - end; - inc(c); + end else LFilter.OwnerKeys := Copy(LFilter.WalletKeys); end; - sl := TStringList.Create; - Try - sl.Add('*** Account type stats ***'); - sl.Add(Format('Current block:%d accounts:%d coins:%s',[FSafeBox.BlocksCount,FSafeBox.AccountsCount,TAccountComp.FormatMoney(FSafeBox.TotalBalance)])); - sl.Add(''); - sl.Add('** Basic stats **'); - sl.Add(Format('Account type=0 accounts:%d coins:%s',[s_0.accounts,TAccountComp.FormatMoney(s_0.balance)])); - sl.Add(Format('Account type [1..9999] accounts:%d coins:%s',[s_inv.accounts,TAccountComp.FormatMoney(s_inv.balance)])); - sl.Add(''); - sl.Add('*** Stats by group ***'); - for k := 0 to 6 do begin - // Note: Account type is a Word [0..65535], so left digit can be only 0..6, deprecating 6 to not fill, result for stats is 0..5 - sl.Add(''); - sl.Add(Format('** Group [%d..%d] **',[(k)*10000,((k+1)*10000)-1])); - for j := 0 to 9 do begin - sl.Add(Format('Account type like %d%d*** accounts:%d coins:%s',[k,j,s_5[k][1][j].accounts,TAccountComp.FormatMoney(s_5[k][1][j].balance)])); - end; - for j := 0 to 9 do begin - sl.Add(Format('Account type like %d*%d** accounts:%d coins:%s',[k,j,s_5[k][2][j].accounts,TAccountComp.FormatMoney(s_5[k][2][j].balance)])); - end; - for j := 0 to 9 do begin - sl.Add(Format('Account type like %d**%d* accounts:%d coins:%s',[k,j,s_5[k][3][j].accounts,TAccountComp.FormatMoney(s_5[k][3][j].balance)])); - end; - for j := 0 to 9 do begin - sl.Add(Format('Account type like %d***%d accounts:%d coins:%s',[k,j,s_5[k][4][j].accounts,TAccountComp.FormatMoney(s_5[k][4][j].balance)])); - end; + LFilter.OnlyForSale := cbOnlyForSale.Checked; + LFilter.OnlyForPublicSale := cbOnlyForPublicSale.Checked; + LFilter.OnlyForPrivateSaleToMe := cbOnlyForPrivateSaleToMe.Checked; + if cbAccountsBalance.Checked then begin + if not TAccountComp.TxtToMoney(ebMinBalance.Text,LFilter.MinBalance) then + raise Exception.Create('Invalid Min Balance'); + ebMinBalance.Text := TAccountComp.FormatMoney(LFilter.MinBalance); + if Trim(ebMaxBalance.Text) <> '' then begin + if not TAccountComp.TxtToMoney(ebMaxBalance.Text,LFilter.MaxBalance) then + raise Exception.Create('Invalid Max Balance'); + ebMaxBalance.Text := TAccountComp.FormatMoney(LFilter.MaxBalance); end; - FRM := TFRMMemoText.Create(Self); - Try - FRM.InitData('Account Type Stats',sl.Text); - FRM.ShowModal; - Finally - FRM.Free; - End; - Finally - sl.Free; - End; + end; + if cbAccountsName.Checked then LFilter.Name.FromString(ebAccountName.Text); + cbMyPrivateKeys.Enabled := LFilter.MyAccounts; + if LFilter.MyAccounts then cbMyPrivateKeys.ParentFont := True + else cbMyPrivateKeys.Font.Color := clGray; + if cbAccountsBalance.Checked then begin + ebMinBalance.ParentFont := True; + ebMaxBalance.ParentFont := True; + end else begin + ebMinBalance.Font.Color := clGray; + ebMaxBalance.Font.Color := clGray; + end; + if cbAccountsName.Checked then ebAccountName.ParentFont := True + else ebAccountName.Font.Color := clGray; + FDefaultSelectionApplied := False; + bbSelect.Enabled := False; + lblAccountsCount.Caption := '-'; + lblAccountsBalance.Caption := '-'; + // Retain the latest input and cancel an obsolete provider without joining it. + // The grid discards its stale result before admitting this generation. + FAccountsGrid.RefreshSuspended := True; + FSource.Configure(AccountSafeBox,LFilter); + FAccountsGrid.PageIndex := 0; + if not Assigned(FAccountsGrid.DataSource) then FAccountsGrid.DataSource := FSource + else FAccountsGrid.RefreshGrid; + if not StatsRunning then FAccountsGrid.RefreshSuspended := False; end; -procedure TFRMAccountSelect.cbMyAccountsChange(Sender: TObject); +procedure TFRMAccountSelect.SetDefaultAccount(AValue: Int64); begin - SearchFiltered; + FDefaultAccount := AValue; + FDefaultSelectionApplied := False; end; -procedure TFRMAccountSelect.cbMyAccountsClick(Sender: TObject); +procedure TFRMAccountSelect.SetAllowSelect(AValue: Boolean); begin - SearchFiltered; + FAllowSelect := AValue; + bbSelect.Visible := AValue; + bbSelect.Enabled := AValue and (GetSelected >= 0) and not StatsRunning; end; -procedure TFRMAccountSelect.cbMyPrivateKeysChange(Sender: TObject); +procedure TFRMAccountSelect.SetFilters(AValue: Integer); begin + FFilters := AValue; + FChangingControls := True; + try + cbMyAccounts.Checked := (AValue and CT_AS_MyAccounts) <> 0; + cbOnlyForSale.Checked := (AValue and CT_AS_OnlyForSale) <> 0; + finally + FChangingControls := False; + end; SearchFiltered; end; -procedure TFRMAccountSelect.ebMaxBalanceExit(Sender: TObject); +procedure TFRMAccountSelect.OnAccountsGridUpdated(Sender: TObject); +var LTotal: TAccountSelectorTotal; LIndex,LPage,I: Integer; + LDeselection: TDeselectionType; begin - if ebMaxBalance.text<>'' then begin - cbAccountsBalance.Checked:=True; - SearchFiltered; + if FStopping then Exit; + if FSource.TryGetSummary(LTotal) then begin + lblAccountsCount.Caption := IntToStr(LTotal.Accounts); + lblAccountsBalance.Caption := TAccountComp.FormatMoney(LTotal.Balance); end; + if not FDefaultSelectionApplied and (FDefaultAccount >= 0) then begin + LIndex := FSource.PublishedAccountIndex(FDefaultAccount); + if LIndex >= 0 then begin + LPage := 0; + if FAccountsGrid.PageMode <> vpmSingle then LPage := LIndex div FAccountsGrid.PageSize; + if LPage <> FAccountsGrid.PageIndex then begin + FAccountsGrid.PageIndex := LPage; + Exit; + end; + for I := 0 to FAccountsGrid.RowCount-1 do + if Int64(TDataRowData(FAccountsGrid.Rows[I])['AccountNumber']) = FDefaultAccount then begin + LDeselection := FAccountsGrid.DeselectionType; + FAccountsGrid.DeselectionType := dtNone; + FAccountsGrid.InternalDrawGrid.Row := I+1; + {$IFNDEF FPC} + TSelectorNativeGridAccess(FAccountsGrid.InternalDrawGrid).PublishCurrentSelection; + {$ENDIF} + FAccountsGrid.DeselectionType := LDeselection; + Break; + end; + end; + FDefaultSelectionApplied := True; + end; + bbSelect.Enabled := FAllowSelect and (GetSelected >= 0) and not StatsRunning; end; -procedure TFRMAccountSelect.ebMinBalanceExit(Sender: TObject); +procedure TFRMAccountSelect.OnGridSelection(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); begin - if ebMinBalance.text<>'' then begin - cbAccountsBalance.Checked:=True; - SearchFiltered; - end; + bbSelect.Enabled := FAllowSelect and (GetSelected >= 0) and not StatsRunning; end; -procedure TFRMAccountSelect.FormDestroy(Sender: TObject); +function TFRMAccountSelect.GetSelected: Int64; +var LRows: TArray; begin - FSearchThread.Terminate; - FSearchThread.WaitFor; - FSearchThread.Free; - FAccounts.Free; + Result := -1; + if not Assigned(FAccountsGrid) or FAccountsGrid.IsUpdating then Exit; + LRows := FAccountsGrid.SelectedRows; + if Length(LRows) = 1 then Result := Int64(TDataRowData(LRows[0])['AccountNumber']); end; -procedure TFRMAccountSelect.SetNode(const Value: TNode); +function TFRMAccountSelect.GetStatsRunning: Boolean; begin - FNode := Value; - FSafeBox := FNode.Bank.SafeBox; - UpdateControls; + Result := Assigned(FStatsTask); end; -procedure TFRMAccountSelect.SetWalletKeys(const Value: TWalletKeys); +procedure TFRMAccountSelect.bbSearchClick(Sender: TObject); begin - FWalletKeys := Value; - UpdateControls; SearchFiltered; end; -procedure TFRMAccountSelect.SearchFiltered; -Var - i : Integer; - searchValues : TSearchValues; +procedure TFRMAccountSelect.cbMyAccountsChange(Sender: TObject); begin - FSearchThread.StopSearch; - searchValues.SafeBox := FSafeBox; - searchValues.inAccountKey := CT_AccountInfo_NUL.accountKey; - searchValues.inWalletKeys := Nil; - If (cbMyAccounts.Checked) And (Assigned(FWalletKeys)) then begin - If (cbMyPrivateKeys.ItemIndex<=0) then begin - // All - searchValues.inWalletKeys := FWalletKeys; - end else begin - i := PtrInt(cbMyPrivateKeys.Items.Objects[cbMyPrivateKeys.ItemIndex]); - if (i>=0) and (i '' then begin + cbAccountsBalance.Checked := True; + SearchFiltered; end; end; -procedure TFRMAccountSelect.UpdateControls; -var i : Integer; +procedure TFRMAccountSelect.ebMaxBalanceExit(Sender: TObject); begin - cbMyAccounts.Enabled:=Assigned(FWalletKeys); - If not Assigned(FWalletKeys) then begin - cbMyAccounts.Checked:=False; - cbMyPrivateKeys.Enabled:=False; - cbMyPrivateKeys.Clear; - end else begin - cbMyPrivateKeys.Enabled:=True; - cbMyPrivateKeys.Items.BeginUpdate; - try - cbMyPrivateKeys.Items.Clear; - For i:=0 to FWalletKeys.Count-1 do begin - cbMyPrivateKeys.Items.AddObject(FWalletKeys.Key[i].Name,TObject(i)); - end; - cbMyPrivateKeys.Sorted:=True; - cbMyPrivateKeys.Sorted:=False; - cbMyPrivateKeys.items.InsertObject(0,'(All)',TObject(-1)); - finally - cbMyPrivateKeys.Items.EndUpdate; - end; + if ebMaxBalance.Text <> '' then begin + cbAccountsBalance.Checked := True; + SearchFiltered; end; end; -procedure TFRMAccountSelect.OnSearchFinished(const searchFound: TCardinalsArray; const searchValues: TSearchValues); -Var l : TOrderedCardinalList; - i, foundpos : Integer; +procedure TFRMAccountSelect.bbTypeStatsClick(Sender: TObject); begin - foundpos := -1; - l := FAccountsGrid.LockAccountsList; + if FStopping or StatsRunning or not Assigned(FSource) then Exit; + FAccountsGrid.RefreshSuspended := True; + FAccountsGrid.ShowLoading('Please Wait','Calculating account type statistics'); + bbTypeStats.Enabled := False; + bbSelect.Enabled := False; try - l.Clear; - for i:=0 to High(searchFound) do begin - l.Add(searchFound[i]); - If searchFound[i]=FDefaultAccount then foundpos := i; - end; - lblAccountsCount.Caption := inttostr(l.Count); - finally - FAccountsGrid.UnlockAccountsList; - end; - If foundpos>=0 then begin - FAccountsGrid.DrawGrid.Row := foundpos + 1; + FStatsTask := CreateStatsTask; + FStatsTask.OnCompleted := OnStatsCompleted; + FStatsTask.Start; + except + FreeAndNil(FStatsTask); + FAccountsGrid.HideLoading; + FAccountsGrid.RefreshSuspended := False; + bbTypeStats.Enabled := True; + raise; end; end; -procedure TFRMAccountSelect.OnAccountsGridUpdated(Sender: TObject); +procedure TFRMAccountSelect.OnStatsCompleted(Sender: TObject); +var LTask: TAccountSelectorStatsTask; LStats: TAccountSelectorTypeStats; LError: String; begin - lblAccountsBalance.Caption := TAccountComp.FormatMoney(FAccountsGrid.AccountsBalance); + if FStopping or (Sender <> FStatsTask) then Exit; + LTask := FStatsTask; + FStatsTask := nil; + LStats := LTask.Stats; + LError := LTask.ErrorMessage; + LTask.Free; + FAccountsGrid.HideLoading; + FAccountsGrid.RefreshSuspended := False; + bbTypeStats.Enabled := True; + if LError <> '' then ShowMessage(LError) + else ShowTypeStatsReport(LStats); end; -function TFRMAccountSelect.GetSelected: Int64; -Var ocl : TOrderedCardinalList; +procedure TFRMAccountSelect.BuildTypeStatsReport(const AStats: TAccountSelectorTypeStats; AText: TStrings); +var K,J: Integer; begin - Result := -1; - ocl := TOrderedCardinalList.Create; + AText.Clear; + AText.Add('*** Account type stats ***'); + AText.Add(Format('Current block:%d accounts:%d coins:%s', + [AStats.Chain.Blocks,AStats.Chain.Accounts,TAccountComp.FormatMoney(AStats.Chain.Balance)])); + AText.Add(''); + AText.Add('** Basic stats **'); + AText.Add(Format('Account type=0 accounts:%d coins:%s', + [AStats.Zero.Accounts,TAccountComp.FormatMoney(AStats.Zero.Balance)])); + AText.Add(Format('Account type [1..9999] accounts:%d coins:%s', + [AStats.Invalid.Accounts,TAccountComp.FormatMoney(AStats.Invalid.Balance)])); + AText.Add(''); + AText.Add('*** Stats by group ***'); + for K := 0 to 6 do begin + AText.Add(''); + AText.Add(Format('** Group [%d..%d] **',[K*10000,(K+1)*10000-1])); + for J := 0 to 9 do AText.Add(Format('Account type like %d%d*** accounts:%d coins:%s', + [K,J,AStats.Digits[K,1,J].Accounts,TAccountComp.FormatMoney(AStats.Digits[K,1,J].Balance)])); + for J := 0 to 9 do AText.Add(Format('Account type like %d*%d** accounts:%d coins:%s', + [K,J,AStats.Digits[K,2,J].Accounts,TAccountComp.FormatMoney(AStats.Digits[K,2,J].Balance)])); + for J := 0 to 9 do AText.Add(Format('Account type like %d**%d* accounts:%d coins:%s', + [K,J,AStats.Digits[K,3,J].Accounts,TAccountComp.FormatMoney(AStats.Digits[K,3,J].Balance)])); + for J := 0 to 9 do AText.Add(Format('Account type like %d***%d accounts:%d coins:%s', + [K,J,AStats.Digits[K,4,J].Accounts,TAccountComp.FormatMoney(AStats.Digits[K,4,J].Balance)])); + end; +end; + +procedure TFRMAccountSelect.ShowTypeStatsReport(const AStats: TAccountSelectorTypeStats); +var LText: TStringList; LForm: TFRMMemoText; +begin + // Keep the established global report and wording; only computation moved off + // the UI thread. Formatting this bounded report is presentation work. + LText := TStringList.Create; try - If FAccountsGrid.SelectedAccounts(ocl)=1 then Result := ocl.Get(0); + BuildTypeStatsReport(AStats,LText); + LForm := TFRMMemoText.Create(Self); + try + LForm.InitData('Account Type Stats',LText.Text); + LForm.ShowModal; + finally + LForm.Free; + end; finally - ocl.Free; + LText.Free; end; end; diff --git a/src/gui-experimental/UFRMBlockExplorer.dfm b/src/gui-experimental/UFRMBlockExplorer.dfm new file mode 100644 index 000000000..ca5c1e40e --- /dev/null +++ b/src/gui-experimental/UFRMBlockExplorer.dfm @@ -0,0 +1,102 @@ +object FRMBlockExplorer: TFRMBlockExplorer + Left = 87 + Height = 424 + Top = 135 + Width = 864 + ActiveControl = ebBlockChainBlockEnd + Caption = 'Block Explorer' + ClientHeight = 424 + ClientWidth = 864 + Menu = BlockExplorerMenu + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + Visible = False + object Panel2: TPanel + Left = 0 + Height = 41 + Top = 0 + Width = 864 + Align = alTop + BevelOuter = bvNone + TabOrder = 0 + object Label9: TLabel + Left = 11 + Height = 15 + Top = 10 + Width = 112 + Caption = 'Filter by blocks range' + ParentColor = False + end + object ebBlockChainBlockEnd: TEdit + Left = 185 + Height = 23 + Top = 7 + Width = 57 + OnExit = ebBlockChainBlockStartExit + OnKeyPress = ebBlockChainBlockStartKeyPress + TabOrder = 0 + end + object lblHashRateBackBlocks: TLabel + Left = 264 + Top = 10 + Caption = 'Hashrate back blocks' + end + object ebHashRateBackBlocks: TEdit + Left = 392 + Top = 7 + Width = 58 + Height = 23 + TabOrder = 1 + OnExit = ebHashRateBackBlocksExit + OnKeyPress = ebHashRateBackBlocksKeyPress + end + object lblHashRateUnits: TLabel + Left = 472 + Top = 10 + Caption = 'Units' + end + object cbHashRateUnits: TComboBox + Left = 510 + Top = 7 + Width = 82 + Height = 23 + Style = csDropDownList + TabOrder = 2 + OnChange = cbHashRateUnitsChange + end + end + object dgBlockChainExplorer: TDrawGrid + Left = 0 + Height = 363 + Top = 41 + Width = 864 + Align = alClient + TabOrder = 1 + end + object ebBlockChainBlockStart: TEdit + Left = 125 + Height = 23 + Top = 7 + Width = 57 + OnExit = ebBlockChainBlockStartExit + OnKeyPress = ebBlockChainBlockStartKeyPress + TabOrder = 2 + end + object BlockExplorerMenu: TMainMenu + left = 560 + top = 8 + object miTools: TMenuItem + Caption = '&Tools' + object miRefresh: TMenuItem + Caption = '&Refresh' + ShortCut = 116 + OnClick = miRefreshClick + end + object miResetBlockFilters: TMenuItem + Caption = 'Reset &block filters' + OnClick = miResetBlockFiltersClick + end + end + end +end diff --git a/src/gui-experimental/UFRMBlockExplorer.lfm b/src/gui-experimental/UFRMBlockExplorer.lfm index 723e1ba15..249ab1c44 100644 --- a/src/gui-experimental/UFRMBlockExplorer.lfm +++ b/src/gui-experimental/UFRMBlockExplorer.lfm @@ -40,6 +40,34 @@ object FRMBlockExplorer: TFRMBlockExplorer OnKeyPress = ebBlockChainBlockStartKeyPress TabOrder = 0 end + object lblHashRateBackBlocks: TLabel + Left = 264 + Top = 10 + Caption = 'Hashrate back blocks' + end + object ebHashRateBackBlocks: TEdit + Left = 392 + Top = 7 + Width = 58 + Height = 23 + TabOrder = 1 + OnExit = ebHashRateBackBlocksExit + OnKeyPress = ebHashRateBackBlocksKeyPress + end + object lblHashRateUnits: TLabel + Left = 472 + Top = 10 + Caption = 'Units' + end + object cbHashRateUnits: TComboBox + Left = 510 + Top = 7 + Width = 82 + Height = 23 + Style = csDropDownList + TabOrder = 2 + OnChange = cbHashRateUnitsChange + end end object dgBlockChainExplorer: TDrawGrid Left = 0 @@ -66,7 +94,16 @@ object FRMBlockExplorer: TFRMBlockExplorer left = 560 top = 8 object miTools: TMenuItem - Caption = 'Tools' + Caption = '&Tools' + object miRefresh: TMenuItem + Caption = '&Refresh' + ShortCut = 116 + OnClick = miRefreshClick + end + object miResetBlockFilters: TMenuItem + Caption = 'Reset &block filters' + OnClick = miResetBlockFiltersClick + end end end end diff --git a/src/gui-experimental/UFRMBlockExplorer.pas b/src/gui-experimental/UFRMBlockExplorer.pas index 89741ec47..77001d683 100644 --- a/src/gui-experimental/UFRMBlockExplorer.pas +++ b/src/gui-experimental/UFRMBlockExplorer.pas @@ -16,14 +16,26 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Displays blockchain blocks with a block-range filter. Its Tools actions + refresh the list or reset that filter. } + +{ Public types in this unit + + Type | Purpose + ------------------+------------------------------------------------------------------------- + TFRMBlockExplorer | Browses blocks and applies or resets a block-range filter. +} interface {$I ..\config.inc} uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls, + Classes, SysUtils, {$IFDEF FPC}FileUtil,{$ENDIF} Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, Grids, Menus, UCommon.UI, UGridUtils; type @@ -34,15 +46,25 @@ TFRMBlockExplorer = class(TApplicationForm) dgBlockChainExplorer: TDrawGrid; ebBlockChainBlockEnd: TEdit; ebBlockChainBlockStart: TEdit; + ebHashRateBackBlocks: TEdit; + cbHashRateUnits: TComboBox; + lblHashRateBackBlocks: TLabel; + lblHashRateUnits: TLabel; Label9: TLabel; BlockExplorerMenu: TMainMenu; miTools: TMenuItem; + miRefresh: TMenuItem; + miResetBlockFilters: TMenuItem; Panel2: TPanel; procedure ebBlockChainBlockStartExit(Sender: TObject); - procedure ebBlockChainBlockStartKeyPress(Sender: TObject; - var Key: Char); + procedure ebBlockChainBlockStartKeyPress(Sender: TObject; var Key: Char); + procedure miRefreshClick(Sender: TObject); + procedure miResetBlockFiltersClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender:TObject); + procedure ebHashRateBackBlocksExit(Sender: TObject); + procedure ebHashRateBackBlocksKeyPress(Sender: TObject; var Key: Char); + procedure cbHashRateUnitsChange(Sender: TObject); private FUpdating : boolean; FBlockChainGrid : TBlockChainGrid; @@ -55,9 +77,13 @@ TFRMBlockExplorer = class(TApplicationForm) FRMBlockExplorer: TFRMBlockExplorer = nil; implementation -uses UFRMMainForm, UUserInterface; +uses UFRMMainForm, UUserInterface, USettings; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} procedure TFRMBlockExplorer.ebBlockChainBlockStartExit(Sender: TObject); var bstart,bend : Int64; begin @@ -76,21 +102,82 @@ procedure TFRMBlockExplorer.ebBlockChainBlockStartExit(Sender: TObject); End; end; -procedure TFRMBlockExplorer.ebBlockChainBlockStartKeyPress(Sender: TObject; - var Key: Char); +procedure TFRMBlockExplorer.ebBlockChainBlockStartKeyPress(Sender: TObject; var Key: Char); begin if key=#13 then ebBlockChainBlockStartExit(Nil); end; +procedure TFRMBlockExplorer.miRefreshClick(Sender: TObject); +begin + FBlockChainGrid.UpdateBlockChainGrid; +end; + +procedure TFRMBlockExplorer.miResetBlockFiltersClick(Sender: TObject); +begin + FUpdating := True; + try + ebBlockChainBlockStart.Clear; + ebBlockChainBlockEnd.Clear; + // Reset both range bounds in one request; keep the hash-rate settings. + if (FBlockChainGrid.BlockStart < 0) and (FBlockChainGrid.BlockEnd < 0) then + FBlockChainGrid.UpdateBlockChainGrid + else + FBlockChainGrid.SetBlocks(-1,-1); + finally + FUpdating := False; + end; +end; + procedure TFRMBlockExplorer.FormCreate(Sender: TObject); +var + units: Integer; begin + FUpdating := true; FBlockChainGrid := TBlockChainGrid.Create(Self); FBlockChainGrid.DrawGrid := dgBlockChainExplorer; FBlockChainGrid.Node := TUserInterface.Node; FBlockChainGrid.ShowTimeAverageColumns:={$IFDEF SHOW_AVERAGE_TIME_STATS}True;{$ELSE}False;{$ENDIF} + cbHashRateUnits.Items.CommaText := 'h/s,Kh/s,Mh/s,Gh/s,Th/s,Ph/s,Eh/s'; + FBlockChainGrid.HashRateAverageBlocksCount := TSettings.HashRateAvgBlocksCount; + units := Ord(TSettings.ShowHashRateAs); + if (units < Ord(Low(TShowHashRateAs))) or (units > Ord(High(TShowHashRateAs))) then + units := Ord({$IFDEF TESTNET}hr_Mega{$ELSE}hr_Tera{$ENDIF}); + FBlockChainGrid.HashRateAs := TShowHashRateAs(units); + ebHashRateBackBlocks.Text := IntToStr(FBlockChainGrid.HashRateAverageBlocksCount); + cbHashRateUnits.ItemIndex := units; FUpdating := false; end; +procedure TFRMBlockExplorer.ebHashRateBackBlocksExit(Sender: TObject); +begin + if FUpdating then exit; + FUpdating := true; + try + FBlockChainGrid.HashRateAverageBlocksCount := StrToIntDef(ebHashRateBackBlocks.Text, + FBlockChainGrid.HashRateAverageBlocksCount); + ebHashRateBackBlocks.Text := IntToStr(FBlockChainGrid.HashRateAverageBlocksCount); + TSettings.HashRateAvgBlocksCount := FBlockChainGrid.HashRateAverageBlocksCount; + finally + FUpdating := false; + end; +end; + +procedure TFRMBlockExplorer.ebHashRateBackBlocksKeyPress(Sender: TObject; var Key: Char); +begin + if Key = #13 then begin + ebHashRateBackBlocksExit(Sender); + Key := #0; + end; +end; + +procedure TFRMBlockExplorer.cbHashRateUnitsChange(Sender: TObject); +begin + if FUpdating or (cbHashRateUnits.ItemIndex < Ord(Low(TShowHashRateAs))) or + (cbHashRateUnits.ItemIndex > Ord(High(TShowHashRateAs))) then exit; + FBlockChainGrid.HashRateAs := TShowHashRateAs(cbHashRateUnits.ItemIndex); + TSettings.ShowHashRateAs := FBlockChainGrid.HashRateAs; +end; + procedure TFRMBlockExplorer.FormDestroy(Sender:TObject); begin FreeAndNil(FBlockChainGrid); diff --git a/src/gui-experimental/UFRMExecuteOperations.dfm b/src/gui-experimental/UFRMExecuteOperations.dfm new file mode 100644 index 000000000..6bda2d77b --- /dev/null +++ b/src/gui-experimental/UFRMExecuteOperations.dfm @@ -0,0 +1,40 @@ +object ExecuteOperations: TExecuteOperations + Left = 0 + Height = 307 + Top = 0 + Width = 545 + ActiveControl = btnClose + Caption = 'Execute Operations' + ClientHeight = 307 + ClientWidth = 545 + OnCreate = FormCreate + Visible = False + object GroupBox1: TGroupBox + Left = 0 + Height = 260 + Top = 8 + Width = 545 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Executing Operations' + TabOrder = 0 + object paGrid: TPanel + Left = 6 + Height = 220 + Top = 8 + Width = 527 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'GRID PANEL' + TabOrder = 0 + end + end + object btnClose: TButton + Left = 429 + Height = 25 + Top = 276 + Width = 107 + Anchors = [akRight, akBottom] + Caption = 'Close' + OnClick = btnCloseClick + TabOrder = 1 + end +end diff --git a/src/gui-experimental/UFRMExecuteOperations.pas b/src/gui-experimental/UFRMExecuteOperations.pas index aef31aa36..83f182c0b 100644 --- a/src/gui-experimental/UFRMExecuteOperations.pas +++ b/src/gui-experimental/UFRMExecuteOperations.pas @@ -16,12 +16,23 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Defines the operation-execution dialog shell and its grid placeholder. } + +{ Public types in this unit + + Type | Purpose + -------------------+------------------------------------------------------------------------- + TExecuteOperations | Provides the operation-execution dialog shell. +} interface uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls, + Classes, SysUtils, {$IFDEF FPC}FileUtil,{$ENDIF} Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, UCommon, UCommon.Data, UCommon.Collections, UCommon.UI, UVisualGrid, UCoreObjects, UCoreUtils; type @@ -50,7 +61,11 @@ implementation //type // TExecuteOperationResultDataSource : class(TDataSource< +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TExecuteOperations } diff --git a/src/gui-experimental/UFRMLogs.dfm b/src/gui-experimental/UFRMLogs.dfm new file mode 100644 index 000000000..9e641802d --- /dev/null +++ b/src/gui-experimental/UFRMLogs.dfm @@ -0,0 +1,40 @@ +object FRMLogs: TFRMLogs + Left = 0 + Height = 466 + Top = 0 + Width = 868 + Caption = 'Logs' + ClientHeight = 466 + ClientWidth = 868 + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + Visible = False + object pnlTopLogs: TPanel + Left = 0 + Height = 41 + Top = 0 + Width = 868 + Align = alTop + BevelOuter = bvNone + TabOrder = 0 + object cbShowDebugLogs: TCheckBox + Left = 15 + Height = 19 + Top = 10 + Width = 115 + Caption = 'Show Debug Logs' + TabOrder = 0 + end + end + object memoLogs: TMemo + Left = 0 + Height = 425 + Top = 41 + Width = 868 + Align = alClient + ScrollBars = ssBoth + TabOrder = 1 + WordWrap = False + end +end diff --git a/src/gui-experimental/UFRMLogs.pas b/src/gui-experimental/UFRMLogs.pas index 35991ee43..ec6086e01 100644 --- a/src/gui-experimental/UFRMLogs.pas +++ b/src/gui-experimental/UFRMLogs.pas @@ -16,17 +16,29 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Displays runtime log messages and lets the user include debug output. } + +{ Public types in this unit + + Type | Purpose + ---------+------------------------------------------------------------------------- + TFRMLogs | Displays application logs with an optional debug-message filter. +} interface {$I ..\config.inc} uses - LCLIntf, + {$IFNDEF FPC}Winapi.Windows, Winapi.Messages,{$ENDIF} + {$IFDEF FPC}LCLIntf,{$ENDIF} SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls, UCommon.UI, - ULog, UBlockChain; + ULog, UBlockChain, UConst; type @@ -38,9 +50,13 @@ TFRMLogs = class(TApplicationForm) pnlTopLogs: TPanel; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); - procedure OnNewLog(logtype : TLogType; Time : TDateTime; ThreadID : TThreadID; Const sender, logtext : AnsiString); + procedure OnNewLog(logtype : TLogType; Time : TDateTime; ThreadID : TThreadID; Const sender, logtext : String); private - { private declarations } + FLogBatchLines, FLogEntryLines: TStringList; + FLogBatchActive, FLogBatchChanged, FLogBatchTerminated: Boolean; + procedure OnLogBatchBegin(Sender: TObject); + procedure OnLogBatchEnd(Sender: TObject); + procedure SplitLogLines(const AText: String; ALines: TStrings); public { public declarations } end; @@ -49,40 +65,142 @@ TFRMLogs = class(TApplicationForm) FRMLogs: TFRMLogs = nil; implementation -uses UUserInterface; +uses {$IFDEF FPC}WSControls,{$ENDIF} UUserInterface; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TFRMLogs } procedure TFRMLogs.FormCreate(Sender: TObject); begin + FLogBatchLines := TStringList(AutoDispose(TStringList.Create)); + FLogEntryLines := TStringList(AutoDispose(TStringList.Create)); + TUserInterface.Log.OnNewLogBatchBegin := OnLogBatchBegin; + TUserInterface.Log.OnNewLogBatchEnd := OnLogBatchEnd; TUserInterface.Log.OnNewLog := OnNewLog; end; procedure TFRMLogs.FormDestroy(Sender: TObject); begin TUserInterface.Log.OnNewLog := nil; + TUserInterface.Log.OnNewLogBatchBegin := nil; + TUserInterface.Log.OnNewLogBatchEnd := nil; end; -procedure TFRMLogs.OnNewLog(logtype: TLogType; Time : TDateTime; ThreadID : TThreadID; const sender,logtext: AnsiString); -Var s : AnsiString; +procedure TFRMLogs.OnLogBatchBegin(Sender: TObject); +var LText: String; +begin + // BeginUpdate suppresses painting, but native memo Add/Delete still shape + // every edit and can stall the UI. Build the batch away from the control. + FLogBatchChanged := False; + LText := memoLogs.Lines.Text; + FLogBatchTerminated := (LText='') or (Copy(LText,Length(LText)-Length(sLineBreak)+1,Length(sLineBreak))=sLineBreak); + SplitLogLines(LText,FLogBatchLines); + FLogBatchActive := True; +end; + +procedure TFRMLogs.OnLogBatchEnd(Sender: TObject); +var LText: String; +begin + FLogBatchActive := False; + try + if FLogBatchChanged then begin + // One native text replacement avoids per-entry text shaping and keeps + // the caret at the end, as with the existing immediate append path. + // Lines.Text normalizes bare CR/LF; native Add preserves them. Publish + // the raw text through the widget without changing those log bytes. + LText := FLogBatchLines.Text; + if not FLogBatchTerminated then SetLength(LText,Length(LText)-Length(sLineBreak)); + {$IFDEF FPC} + memoLogs.HandleNeeded; + TWSWinControlClass(memoLogs.WidgetSetClass).SetText(memoLogs,LText); + memoLogs.Perform(CM_TEXTCHANGED,0,0); + {$ELSE} + memoLogs.Text := LText; + {$ENDIF} + memoLogs.SelStart := memoLogs.GetTextLen; + {$IFNDEF FPC} + // VCL EM_SETSEL moves the caret without scrolling; the replaced native + // Add did scroll, so keep following the newest line explicitly. + memoLogs.Perform(EM_SCROLLCARET,0,0); + {$ENDIF} + end; + finally + FLogBatchChanged := False; + FLogBatchLines.Clear; + FLogEntryLines.Clear; + end; +end; + +procedure TFRMLogs.OnNewLog(logtype: TLogType; Time : TDateTime; ThreadID : TThreadID; const sender,logtext: String); +Var s, LText : String; + LLines : TStrings; begin if (logtype=ltdebug) And (Not cbShowDebugLogs.Checked) then exit; if ThreadID=MainThreadID then s := 'MAIN:' else s:='TID:'; - if MemoLogs.Lines.Count>300 then begin + if FLogBatchActive then begin + LLines := FLogBatchLines; + FLogBatchChanged := True; + end else LLines := memoLogs.Lines; + if LLines.Count>300 then begin // Limit max lines in logs... - memoLogs.Lines.BeginUpdate; + LLines.BeginUpdate; try - while memoLogs.Lines.Count>250 do memoLogs.Lines.Delete(0); + while LLines.Count>250 do LLines.Delete(0); finally - memoLogs.Lines.EndUpdate; + LLines.EndUpdate; end; end; //memoLogs.Lines.Add('%s %s %p [%s] <%s> %s', XXXXXXXXXXXXXXXX Windows %p is invalid (not a pointer) // [FormatDateTime('dd/mm/yyyy hh:nn:ss.zzz',Time), s, ThreadID, CT_LogType[Logtype], sender, logtext]); XXXXXXXXXXXXXXXX Windows %p is invalid (not a pointer) - memoLogs.Lines.Add('%s %s %s [%s] <%s> %s', + LText := Format('%s %s %s [%s] <%s> %s', [FormatDateTime('dd/mm/yyyy hh:nn:ss.zzz',Time), s, IntToHex(PtrInt(ThreadID),8), CT_LogType[Logtype], sender, logtext]); + if FLogBatchActive then begin + // Native memo appends split embedded newlines. Keep those physical lines + // when applying the same per-entry retention rule to the in-memory batch. + {$IFDEF FPC} + LText := LText+sLineBreak; + {$ELSE} + // VCL leaves an unterminated user-edited tail unterminated when appending; + // LCL always appends a line separator. Retain each original native rule. + if FLogBatchTerminated then LText := LText+sLineBreak; + {$ENDIF} + FLogBatchTerminated := Copy(LText,Length(LText)-Length(sLineBreak)+1,Length(sLineBreak))=sLineBreak; + SplitLogLines(LText,FLogEntryLines); + FLogBatchLines.AddStrings(FLogEntryLines); + end else LLines.Add(LText); +end; + +procedure TFRMLogs.SplitLogLines(const AText: String; ALines: TStrings); +var I, LStart, LLength, LBreakLength: Integer; + LBreak: String; + LMatches: Boolean; +begin + // Split only the native line separator: on Windows a bare CR or LF is + // part of a memo line, not an additional line for the retention count. + ALines.Clear; + LBreak := sLineBreak; + LBreakLength := Length(LBreak); + LLength := Length(AText); + LStart := 1; + I := 1; + while I<=LLength do begin + LMatches := AText[I]=LBreak[1]; + if LMatches and (LBreakLength>1) then begin + if I + SimplePanel = False + OnResize = FormResize + object tbStatusToolBar: TToolBar + Left = 783 + Height = 22 + Top = 4 + Width = 70 + Align = alNone + Anchors = [akTop, akRight] + AutoSize = True + ButtonHeight = 22 + ButtonWidth = 23 + Caption = 'tbStatusToolBar' + EdgeBorders = [] + EdgeInner = esNone + EdgeOuter = esNone + Images = ilSmallIcons + List = True + TabOrder = 0 + Wrapable = False + OnResize = FormResize + object tbtnWalletLock: TToolButton + Left = 0 + Hint = 'Show Private Keys' + Top = 0 + Caption = 'Private Keys' + ImageIndex = 2 + OnClick = tbtnWalletLockClick + end + object tbtnConnectivity: TToolButton + Left = 23 + Hint = 'Network is active. Click to deactivate' + Top = 0 + Caption = 'Net' + ImageIndex = 0 + OnClick = tbtnConnectivityClick + end + object tbtnSync: TToolButton + Left = 46 + Hint = 'Show &Syncronization' + Top = 0 + Caption = '&Synchronization' + ImageIndex = 4 + OnClick = tbtnSyncClick + end + end + end + object meMainMenu: TMainMenu + Images = ilSmallIcons + left = 560 + top = 496 + object miWallet: TMenuItem + Caption = 'Wallet' + object miPrivateKeys: TMenuItem + Caption = 'Private Keys' + Hint = 'Show Private Keys' + ImageIndex = 2 + ShortCut = 16464 + OnClick = miPrivateKeysClick + end + object miN1: TMenuItem + Caption = '-' + end + object miAdvancedOperation: TMenuItem + Caption = 'Advanced Operation...' + OnClick = miAdvancedOperationClick + end + object miOptions: TMenuItem + Caption = 'Options' + ShortCut = 16463 + OnClick = miOptionsClick + end + object miSeedNodes: TMenuItem + Caption = 'Seed Nodes' + OnClick = miSeedNodesClick + end + object N1: TMenuItem + Caption = '-' + end + object miClose: TMenuItem + Caption = 'Close' + OnClick = miCloseClick + end + end + object miViews: TMenuItem + Caption = 'Tools' + object miAccountExplorer: TMenuItem + Caption = 'Accounts' + OnClick = miAccountExplorerClick + end + object miPendingOperations: TMenuItem + Caption = 'Operations' + OnClick = miPendingOperationsClick + end + object miBlockExplorer: TMenuItem + Caption = 'Blocks' + OnClick = miBlockExplorerClick + end + object miOperationsExplorer: TMenuItem + Caption = 'Operations Explorer' + OnClick = miOperationsExplorerClick + end + object miOperationsWorkbench: TMenuItem + Caption = 'RAW / Multioperation Workbench...' + OnClick = miOperationsWorkbenchClick + end + object miRPCCalls: TMenuItem + Caption = 'RPC Calls...' + OnClick = miRPCCallsClick + end + object miLogs: TMenuItem + Caption = '&Logs' + OnClick = miLogsClick + end + object miNodes: TMenuItem + Caption = 'Nodes' + OnClick = miNodesClick + end + object miMessages: TMenuItem + Caption = 'Messages' + OnClick = miMessagesClick + end + object miSyncDialog: TMenuItem + Caption = '&Synchronization' + Hint = 'Show &Syncronization' + ImageIndex = 4 + OnClick = miSyncDialogClick + end + end + object miAboutMenu: TMenuItem + Caption = 'About' + object miAbout: TMenuItem + Caption = 'About Pascal Coin...' + OnClick = miAboutClick + end + end + end + object ilIcons: TImageList + Height = 48 + left = 616 + top = 496 + Bitmap = { + 494C010102000300040010003000FFFFFFFF2100FFFFFFFFFFFFFFFF424D3600 + 0000000000003600000028000000400000003000000001002000000000000030 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000290000000B0000001100000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000290000000B0000001100000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000002800000000000000000000000000000000000000000000003F0000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 006D000000000000000000000000000000000000000000000000000000000000 + 002700000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 006D000000000000000000000000000000000000000000000000000000000000 + 002700000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000002A000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000290000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000E000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000290000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000E000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000007F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000290000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000014000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000E0000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000014000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000E0000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000006800000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001F00000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001F00000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000001F00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000004000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000004000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000023000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000011000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000011000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000005000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000002000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000002000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000D000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000023000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000023000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003F00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000002000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000002000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000002900000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000006E00000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000006E00000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000029000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000E0000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000074000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000002E0000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000074000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000002E0000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000290000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000E000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000780000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000001D000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000780000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000001D000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0052000000000000000000000000000000000000000000000000000000000000 + 002400000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000220000000000000000000000000000000000000000000000400000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000220000000000000000000000000000000000000000000000400000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000016000000010000000C00000071000000000000 + 000000000000000000000000000000000000000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000424D3E000000000000003E000000 + 2800000040000000300000000100010000000000800100000000000000000000 + 000000000000000000000000FFFFFF00FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 + FFFF000000000000FFFF000000000000 + } + end + object ilSmallIcons: TImageList + left = 664 + top = 496 + Bitmap = { + 494C010105000600040010001000FFFFFFFF2100FFFFFFFFFFFFFFFF424D3600 + 0000000000003600000028000000400000002000000001002000000000000020 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000320000 + 0008000000000000001C00000075000000CA000000F3000000E2000000940000 + 0029000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000730000 + 00CA000000AF000000FF000000FF000000D3000000A0000000D9000000FF0000 + 00FF000000A30000000300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000B20000 + 00FF000000FF000000C700000015000000000000000000000000000000200000 + 00DA000000FF000000B500000001000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000F50000 + 00FF000000FF000000ED00000013000000000000000000000000000000000000 + 0005000000D7000000FF0000005C000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000030000007E0000 + 005800000039000000210000000C000000000000000000000000000000000000 + 000000000017000000FF000000F5000000020000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000C0000000FF0000001A0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000002F0000007F0000 + 0015000000000000000000000000000000000000000000000000000000000000 + 00000000000000000079000000FF0000002E0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000098000000FF0000 + 0020000000000000000000000000000000000000000000000000000000000000 + 000000000000000000350000003D000000060000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000006B000000FF0000 + 0052000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000024000000FF0000 + 00E4000000030000000000000000000000000000000000000000000000330000 + 0050000000740000009E0000007C000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000C70000 + 00FF000000A200000001000000000000000000000000000000000000002E0000 + 00FF000000FF000000FF00000066000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000000D0000 + 00F3000000FF000000C50000001D000000000000000000000008000000660000 + 00FF000000FF000000FF00000039000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0013000000E0000000FF000000FF000000F7000000EF000000FF000000FF0000 + 00F100000057000000F500000019000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000200000049000000B4000000ED000000E6000000A6000000460000 + 0004000000000000002E00000006000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1880CCFF1882CEFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000050000 + 0086000000AA000000AA000000AA000000AA000000AA000000AA000000AA0000 + 00AA000000860000000400000000000000001EA1FFFF1D9DFAFF0E4C79FF0A35 + 55FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0E4C + 79FF1D9EFBFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 0000000000000000000000000000000000360000003400000000000000000000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1A8FE3FF051C2DFF051D2FFF1A8FE3FF1EA1FFFF1EA1 + FFFF1677BDFF1A8EE1FF1EA1FFFF1EA1FFFF00000000000000000000004B0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000004B00000000000000001EA1FFFF1571B4FF000000FF0A35 + 55FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0000 + 00FF1571B4FF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 000000000000000000000000001E000000D6000000D50000001E000000000000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1C9AF4FF0A3656FF000000FF000000FF0A3859FF1C98F1FF136A + A9FF020B13FF146CACFF1EA1FFFF1EA1FFFF0000000000000000000000550000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000005500000000000000001EA1FFFF146BAAFF000000FF0001 + 02FF000102FF000102FF000102FF000102FF000102FF000102FF000102FF0000 + 00FF146BAAFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 0000000000000000000F000000B3000000FF000000FF000000B00000000E0000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1D9EFBFF0C446DFF000001FF000000FF000000FF000102FF072840FF020B + 13FF12629CFF1D9DFAFF1EA1FFFF1EA1FFFF0000000000000000000000550000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000005500000000000000001EA1FFFF146BAAFF000000FF0001 + 02FF000102FF000102FF000102FF000102FF000102FF000102FF000102FF0000 + 00FF146BAAFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 000000000005000000A4000000FE000000FF000000FF000000FE000000A20000 + 0005000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF136AA8FF01060BFF000000FF000000FF000000FF000000FF01080DFF1262 + 9CFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000550000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000005500000000000000001EA1FFFF146BAAFF000000FF0001 + 02FF000102FF000102FF000102FF000102FF000102FF000102FF000102FF0000 + 00FF146BAAFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 00000000006D000000F6000000FF000000FF000000FF000000FF000000F60000 + 006B000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF167B + C3FF020B12FF000000FF000000FF000000FF000000FF000103FF12629CFF1A8C + DEFF177EC9FF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000550000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000005500000000000000001EA1FFFF146BAAFF000000FF0001 + 02FF000102FF000102FF000102FF000102FF000102FF000102FF000102FF0000 + 00FF146BAAFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 004E000000FC000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FC0000004B0000000000000000000000001EA1FFFF1EA1FFFF1A8DE0FF0419 + 28FF000000FF000000FF000000FF000000FF01080DFF12629CFF1A8CDEFF051F + 32FF041A2AFF1A8EE2FF1EA1FFFF1EA1FFFF0000000000000000000000550000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000005500000000000000001EA1FFFF146BAAFF000000FF0001 + 02FF000102FF000102FF000102FF000102FF000102FF000102FF000102FF0000 + 00FF146BAAFF1EA1FFFF1EA1FFFF1EA1FFFF00000000000000000000002F0000 + 00E2000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000E10000002E00000000000000001EA1FFFF1D9EFBFF082E49FF0001 + 02FF000000FF000000FF000000FF000103FF12629CFF1A8CDEFF051F32FF0001 + 02FF000102FF082F4CFF1D9FFCFF1EA1FFFF0000000000000000000000550000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000005500000000000000001EA1FFFF146BAAFF000000FF0001 + 02FF000102FF000102FF000102FF000102FF000102FF000102FF000102FF0000 + 00FF146BAAFF1EA1FFFF1EA1FFFF1EA1FFFF000000000000000A000000D90000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000D600000009000000001D9CF8FF0C456EFF000000FF0000 + 00FF000000FF000000FF01080DFF12629CFF1A8CDEFF051F32FF000000FF0000 + 00FF000000FF000000FF0D4772FF1D9DF9FF00000000000000000000004B0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF0000004B00000000000000001EA1FFFF1571B4FF000000FF0A35 + 55FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF0000 + 00FF1571B4FF1EA1FFFF1EA1FFFF1EA1FFFF00000010000000B1000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000AF0000000F1368A5FF01060AFF000000FF0000 + 00FF000000FF000103FF12619BFF1A8CDEFF051F32FF000102FF000000FF0000 + 00FF000000FF000000FF01060BFF136AA9FF0000000000000000000000050000 + 0087000000C6000000FF000000AA000000AA000000AA000000AA000000F90000 + 00C6000000870000000400000000000000001EA1FFFF1D9DFAFF0E4B78FF0A35 + 55FF0A3555FF0A3555FF0A3555FF0A3555FF0A3555FF000306FF062339FF0E4B + 78FF1D9EFBFF1EA1FFFF1EA1FFFF1EA1FFFF0000006B000000FC000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FB000000681679C1FF062034FF000204FF0000 + 00FF01080DFF12619BFF1A8CDEFF051F32FF000000FF000000FF000000FF0000 + 00FF000000FF000204FF062236FF177BC4FF0000000000000000000000000000 + 000000000055000000FF00000000000000000000000000000000000000EE0000 + 0055000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF146BAAFF020A11FF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000001C0000008A000000F50000 + 00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000F4000000880000001B1EA1FFFF1B93EAFF0C4167FF0001 + 03FF12619BFF1A8CDEFF051F32FF000102FF000000FF000000FF000000FF0000 + 00FF062135FF136AA9FF1B94EBFF1EA1FFFF0000000000000000000000000000 + 000000000048000000F90000000A00000000000000000000000B000000F90000 + 0047000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1574B8FF000306FF1C9AF4FF1EA1 + FFFF1EA1FFFF1C9AF5FF000306FF1573B7FF00000000000000000000001D0000 + 0097000000D9000000EB000000F7000000FD000000FD000000F7000000EB0000 + 00D8000000940000001C00000000000000001EA1FFFF1573B7FF020B12FF1261 + 9BFF1EA1FFFF136AA9FF0A395BFF082A44FF082B45FF0A3A5DFF10588CFF1885 + D4FF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 00000000000B000000E90000009C0000000A0000000A0000009C000000E90000 + 000B000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1C9AF4FF020D16FF0B3E63FF1C9A + F5FF1C9AF5FF0B3E63FF020D16FF1C9AF4FF0000000000000000000000000000 + 0000000000100000002900000039000000410000004100000039000000280000 + 000F000000000000000000000000000000001EA1FFFF1987D7FF1368A5FF1D9D + FAFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 0000000000000000003E000000E9000000F9000000F9000000E90000003D0000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF167AC2FF020D16FF0003 + 06FF000306FF020D16FF1679C1FF1EA1FFFF0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF0000000000000000000000000000 + 000000000000000000000000000B00000048000000480000000B000000000000 + 0000000000000000000000000000000000001EA1FFFF1EA1FFFF1EA1FFFF1EA1 + FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1EA1FFFF1C9AF4FF1573 + B7FF1573B7FF1C9AF4FF1EA1FFFF1EA1FFFF424D3E000000000000003E000000 + 2800000040000000200000000100010000000000000100000000000000000000 + 000000000000000000000000FFFFFF00FFFF000000000000FE1F000000000000 + E007000000000000C3E3000000000000C3F3000000000000FFF9000000000000 + FFF9000000000000FFFD0000000000009FFF000000000000DFFF000000000000 + CFFB000000000000C7E3000000000000E3E3000000000000F00B000000000000 + FC3F000000000000FFFF000000000000FFFF0000FFFF0000FFFF0000E0070000 + FFFF0000E0070000FE7F0000E0070000FC3F0000E0070000F81F0000E0070000 + F81F0000E0070000F00F0000E0070000E0070000E0070000C0030000E0070000 + 80010000E007000080010000FBDF000080010000FBDF0000E0070000F99F0000 + FFFF0000FC3F0000FFFF0000FFFF0000 + } + end +end diff --git a/src/gui-experimental/UFRMMainForm.lfm b/src/gui-experimental/UFRMMainForm.lfm index 7e3bce54d..21db53e73 100644 --- a/src/gui-experimental/UFRMMainForm.lfm +++ b/src/gui-experimental/UFRMMainForm.lfm @@ -76,9 +76,11 @@ object FRMMainForm: TFRMMainForm end object sbStatusBar: TStatusBar Left = 0 - Height = 18 - Top = 582 + Height = 30 + Top = 570 Width = 870 + Align = alBottom + AutoSize = False Panels = < item Alignment = taCenter @@ -98,53 +100,50 @@ object FRMMainForm: TFRMMainForm Width = 30 end> SimplePanel = False - end - object tbStatusToolBar: TToolBar - AnchorSideTop.Control = sbStatusBar - AnchorSideTop.Side = asrCenter - AnchorSideBottom.Control = sbStatusBar - AnchorSideBottom.Side = asrBottom - Left = 783 - Height = 15 - Top = 584 - Width = 70 - Align = alNone - Anchors = [akTop, akRight] - AutoSize = True - ButtonHeight = 15 - ButtonWidth = 23 - Caption = 'tbStatusToolBar' - EdgeBorders = [] - EdgeInner = esNone - EdgeOuter = esNone - Images = ilSmallIcons - List = True - TabOrder = 0 - Wrapable = False - object tbtnWalletLock: TToolButton - Left = 1 - Hint = 'Show Private Keys' - Top = 0 - Caption = 'Private Keys' - ImageIndex = 2 - Marked = True - OnClick = tbtnWalletLockClick - end - object tbtnConnectivity: TToolButton - Left = 47 - Hint = 'Network is active. Click to deactivate' - Top = 0 - Caption = 'Net' - ImageIndex = 0 - OnClick = tbtnConnectivityClick - end - object tbtnSync: TToolButton - Left = 24 - Hint = 'Show &Syncronization' - Top = 0 - Caption = '&Synchronization' - ImageIndex = 4 - OnClick = tbtnSyncClick + OnResize = FormResize + object tbStatusToolBar: TToolBar + Left = 783 + Height = 22 + Top = 4 + Width = 70 + Align = alNone + Anchors = [akTop, akRight] + AutoSize = True + ButtonHeight = 22 + ButtonWidth = 23 + Caption = 'tbStatusToolBar' + EdgeBorders = [] + EdgeInner = esNone + EdgeOuter = esNone + Images = ilSmallIcons + List = True + TabOrder = 0 + Wrapable = False + OnResize = FormResize + object tbtnWalletLock: TToolButton + Left = 0 + Hint = 'Show Private Keys' + Top = 0 + Caption = 'Private Keys' + ImageIndex = 2 + OnClick = tbtnWalletLockClick + end + object tbtnConnectivity: TToolButton + Left = 23 + Hint = 'Network is active. Click to deactivate' + Top = 0 + Caption = 'Net' + ImageIndex = 0 + OnClick = tbtnConnectivityClick + end + object tbtnSync: TToolButton + Left = 46 + Hint = 'Show &Syncronization' + Top = 0 + Caption = '&Synchronization' + ImageIndex = 4 + OnClick = tbtnSyncClick + end end end object meMainMenu: TMainMenu @@ -163,6 +162,10 @@ object FRMMainForm: TFRMMainForm object miN1: TMenuItem Caption = '-' end + object miAdvancedOperation: TMenuItem + Caption = 'Advanced Operation...' + OnClick = miAdvancedOperationClick + end object miOptions: TMenuItem Caption = 'Options' ShortCut = 16463 @@ -198,6 +201,14 @@ object FRMMainForm: TFRMMainForm Caption = 'Operations Explorer' OnClick = miOperationsExplorerClick end + object miOperationsWorkbench: TMenuItem + Caption = 'RAW / Multioperation Workbench...' + OnClick = miOperationsWorkbenchClick + end + object miRPCCalls: TMenuItem + Caption = 'RPC Calls...' + OnClick = miRPCCallsClick + end object miLogs: TMenuItem Caption = '&Logs' OnClick = miLogsClick diff --git a/src/gui-experimental/UFRMMainForm.pas b/src/gui-experimental/UFRMMainForm.pas index 5a779b2c0..1613671ea 100644 --- a/src/gui-experimental/UFRMMainForm.pas +++ b/src/gui-experimental/UFRMMainForm.pas @@ -16,15 +16,31 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Hosts the wallet, banner and synchronization view, menus and status + controls. Routes application events and user commands to the controller and + owned views. } + +{ Public types in this unit + + Type | Purpose + -----------------+------------------------------------------------------------------------- + TFRMMainForm | Hosts the experimental wallet window, views and main commands. + TFRMMainFormMode | Selects the wallet or synchronization view. +} interface {$I ..\config.inc} uses + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + {$IFNDEF FPC}Vcl.AppEvnts,{$ENDIF} Messages, SysUtils, Classes, Graphics, Controls, Forms, - Dialogs, ExtCtrls, ComCtrls, LMessages, UWallet, + Dialogs, ExtCtrls, ComCtrls, {$IFDEF FPC}LMessages,{$ENDIF} UWallet, ULog, UBlockChain, UNode, UGridUtils, UAccounts, Menus, UNetProtocol, UCrypto, Buttons, ActnList, UPoolMining, @@ -36,6 +52,7 @@ interface CM_PC_ConnectivityChanged = WM_USER + 3; CM_PC_Terminate = WM_USER + 4; CM_PC_ModeChanged = WM_USER + 5; + CM_PC_LayoutFooter = WM_USER + 6; type @@ -64,13 +81,17 @@ TFRMMainForm = class(TApplicationForm) miWallet: TMenuItem; miOptions: TMenuItem; miPrivateKeys: TMenuItem; + miAdvancedOperation: TMenuItem; + miOperationsWorkbench: TMenuItem; + miRPCCalls: TMenuItem; miN1: TMenuItem; miAboutMenu: TMenuItem; miAbout: TMenuItem; N1: TMenuItem; miClose: TMenuItem; ilIcons: TImageList; - ApplicationEvents: TApplicationProperties; + {$IFDEF FPC}ApplicationEvents: TApplicationProperties; + {$ELSE}ApplicationEvents: TApplicationEvents;{$ENDIF} miSeedNodes: TMenuItem; pnlSelectedAccountsBottom: TPanel; tbStatusToolBar: TToolBar; @@ -92,12 +113,21 @@ TFRMMainForm = class(TApplicationForm) procedure miOptionsClick(Sender: TObject); procedure miAboutClick(Sender: TObject); procedure miPrivateKeysClick(Sender: TObject); + procedure miAdvancedOperationClick(Sender: TObject); + procedure miOperationsWorkbenchClick(Sender: TObject); + procedure miRPCCallsClick(Sender: TObject); procedure miCloseClick(Sender: TObject); procedure tbtnSyncClick(Sender: TObject); procedure tbtnWalletLockClick(Sender:TObject); procedure tbtnConnectivityClick(Sender:TObject); procedure miSeedNodesClick(Sender: TObject); private + FSubscribed : Boolean; + FFooterLayoutActive : Boolean; + FFooterLayoutPending : Boolean; + FStartupHeightLocked : Boolean; + FStartupMinHeight, FStartupMaxHeight : Integer; + FStartupBorderIcons : TBorderIcons; FMode : TFRMMainFormMode; FSyncControl : TCTRLSyncronization; FWalletControl : TCTRLWallet; @@ -106,14 +136,17 @@ TFRMMainForm = class(TApplicationForm) procedure CM_ConnectivityChanged(var Msg: TMessage); message CM_PC_ConnectivityChanged; procedure CM_Terminate(var Msg: TMessage); message CM_PC_Terminate; procedure CM_ModeChanged(var Msg: TMessage); message CM_PC_ModeChanged; + procedure CM_LayoutFooter(var Msg: TMessage); message CM_PC_LayoutFooter; + procedure LayoutFooter; procedure OnAppStarted(Sender: TObject); - procedure OnLoading(Sender: TObject; const message : String; curPos, totalCount : Int64); + procedure OnLoading(Sender: TObject; const AProgress : TProgressInfo); procedure OnLoaded(Sender: TObject); + procedure OnStateChanged(Sender: TObject); procedure OnConnectivityChanged(Sender: TObject); procedure OnNetStatisticsChanged(Sender: TObject); procedure OnWalletChanged(Sender: TObject); - procedure OnUIRefreshTimer(Sender: TObject); + procedure OnSyncSnapshotChanged(Sender: TObject); {$IFDEF TESTNET} procedure InitMenuForTesting; procedure Test_RandomOperations(Sender: TObject); @@ -124,50 +157,50 @@ TFRMMainForm = class(TApplicationForm) {$ENDIF} Procedure InitMacOSMenu; protected + function CreateWalletControl: TCTRLWallet; virtual; + property WalletControl: TCTRLWallet read FWalletControl; + procedure WndProc(var Msg: {$IFDEF FPC}TLMessage{$ELSE}TMessage{$ENDIF}); override; procedure RefreshConnectionStatusDisplay; procedure RefreshWalletLockIcon; procedure RefreshConnectivityIcon; + procedure RefreshStartupControls; + procedure SetStartupHeightLocked(ALocked: Boolean); procedure ActivateFirstTime; override; Function ForceMining : Boolean; virtual; procedure SetMode(AMode: TFRMMainFormMode); public + // Bind after the controller has opened settings and wallet keys. Window + // activation is not guaranteed on a background or private desktop. + procedure SubscribeApplicationEvents; property SyncControl : TCTRLSyncronization read FSyncControl; property Mode : TFRMMainFormMode read FMode write SetMode; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} -uses LCLIntf, LCLType, UUserInterface, UThread, UOpTransaction, UWizard, UTime; +uses {$IFDEF FPC}LCLIntf, LCLType, {$IFDEF MSWINDOWS}Windows, CommCtrl,{$ENDIF} + {$ELSE}Winapi.CommCtrl,{$ENDIF} Themes, Math, UUserInterface, UThread, UOpTransaction, UWizard, UTime, + UMemory, UPCOrderedLists, USettings; const CT_FOOTER_TOOLBAR_LEFT_PADDING = 8; CT_FOOTER_TOOLBAR_RIGHT_PADDING = 20; + CT_FOOTER_TOOLBAR_VERTICAL_PADDING = 4; { TFRMWallet } {%region Form life-cycle } procedure TFRMMainForm.FormCreate(Sender: TObject); - - procedure AdjustStatusToolBar; - var - LHeightDiff: Integer; - begin - LHeightDiff := sbStatusBar.Height - tbStatusToolBar.Height; - if LHeightDiff >= 0 then - Exit; - - tbStatusToolBar.AnchorSideTop.Side := asrTop; - tbStatusToolBar.Anchors := [akRight, akBottom]; - - end; - begin - AdjustStatusToolBar; - CloseAction := caNone; // Will handle terminate in separate method + SetStartupHeightLocked(true); Mode := wmSync; FSyncControl := TCTRLSyncronization.Create(self); paLogoPanel.AddControlDockCenter(TCTRLBanner.Create(Self)); @@ -184,10 +217,18 @@ procedure TFRMMainForm.FormCreate(Sender: TObject); InitMenuForTesting; {$ENDIF} InitMacOSMenu; + FormResize(Self); end; procedure TFRMMainForm.FormCloseQuery(Sender: TObject; var CanClose: boolean); begin + // A local restore can take time. Keep native Close usable and cancel the + // owned loader before the controller destroys its receivers and node. + if TUserInterface.State = uisLoading then begin + CanClose := false; + PostMessage(Self.Handle, CM_PC_Terminate, 0, 0); + Exit; + end; case TUserInterface.AskQuestion(Self, mtConfirmation, 'Exit PascalCoin', 'Are you sure you want to quit?', [mbNo, mbYes]) of mbYes: begin @@ -209,25 +250,49 @@ procedure TFRMMainForm.CM_Terminate(var Msg: TMessage); procedure TFRMMainForm.ActivateFirstTime; begin - // Event subscriptions on activate, not create, due to load-time sequence + inherited; + SubscribeApplicationEvents; +end; + +procedure TFRMMainForm.SubscribeApplicationEvents; +begin + if FSubscribed or (not TUserInterface.Started) then Exit; + // Settings and wallet keys must already be open before these subscriptions. TUserInterface.AppStarted.Add(OnAppStarted); TUserInterface.Loading.Add(OnLoading); + TUserInterface.Loaded.Add(OnLoaded); + TUserInterface.StateChanged.Add(OnStateChanged); + TUserInterface.SynchronizationChanged.Add(OnStateChanged); TWallet.Keys.OnChanged.Add(OnWalletChanged); TNetData.NetData.OnConnectivityChanged.Add(OnConnectivityChanged); TUserInterface.NetStatisticsChanged.Add(OnNetStatisticsChanged); - TUserInterface.UIRefreshTimer.Add(OnUIRefreshTimer); + // Settings are applied by the controller before this cheap display receiver. + TSettings.OnChanged.Add(OnNetStatisticsChanged); + FSyncControl.SnapshotChanged.Add(OnSyncSnapshotChanged); + FSubscribed := true; RefreshWalletLockIcon; RefreshConnectivityIcon; + RefreshStartupControls; + // Startup can finish before the window receives its first activation. + OnAppStarted(Self); + if TUserInterface.State <> uisLoading then + OnLoaded(Self); end; procedure TFRMMainForm.FormDestroy(Sender: TObject); begin + if not FSubscribed then Exit; + FSubscribed := false; TUserInterface.AppStarted.Remove(OnAppStarted); TUserInterface.Loading.Remove(OnLoading); + TUserInterface.Loaded.Remove(OnLoaded); + TUserInterface.StateChanged.Remove(OnStateChanged); + TUserInterface.SynchronizationChanged.Remove(OnStateChanged); TWallet.Keys.OnChanged.Remove(OnWalletChanged); TNetData.NetData.OnConnectivityChanged.Remove(OnConnectivityChanged); TUserInterface.NetStatisticsChanged.Remove(OnNetStatisticsChanged); - TUserInterface.UIRefreshTimer.Remove(OnUIRefreshTimer); + TSettings.OnChanged.Remove(OnNetStatisticsChanged); + FSyncControl.SnapshotChanged.Remove(OnSyncSnapshotChanged); end; {%endregion} @@ -239,8 +304,17 @@ function TFRMMainForm.ForceMining: Boolean; Result := false; end; +function TFRMMainForm.CreateWalletControl: TCTRLWallet; +begin + Result := TCTRLWallet.Create(Self); +end; + procedure TFRMMainForm.RefreshWalletLockIcon; begin + if TUserInterface.State in [uisLoading, uisError] then begin + tbtnWalletLock.Enabled := false; + Exit; + end; if NOT TWallet.Keys.HasPassword then begin { No password has been set } tbtnWalletLock.Enabled := false; @@ -266,28 +340,82 @@ procedure TFRMMainForm.RefreshConnectivityIcon; begin tbtnConnectivity.ImageIndex := ImageIndexConst[TNetData.NetData.NetConnectionsActive]; tbtnConnectivity.Hint := HintConst[TNetData.NetData.NetConnectionsActive]; + tbtnConnectivity.Enabled := (not TUserInterface.Offline) and + (not (TUserInterface.State in [uisLoading, uisError])); + if TUserInterface.Offline then + tbtnConnectivity.Hint := 'Offline mode: restart without --offline to enable networking.'; +end; + +procedure TFRMMainForm.RefreshStartupControls; +var + LReady, LPendingReady : Boolean; +begin + SetStartupHeightLocked(TUserInterface.State = uisLoading); + LReady := TUserInterface.Started and + (not (TUserInterface.State in [uisLoading, uisError])); + // Confirmed-block progress remains available while the pending overlay is + // deliberately withheld. Commands needing pending balances wait for replay. + LPendingReady := LReady and TUserInterface.SynchronizationStatus.PendingOperationsAvailable; + // Keep the window and its Close command enabled. Only commands which need + // loaded wallet/node state are gated; modal dialogs manage their own host. + miViews.Enabled := TUserInterface.Started; + miAccountExplorer.Enabled := LPendingReady; + miPendingOperations.Enabled := LPendingReady; + miBlockExplorer.Enabled := LReady; + miOperationsExplorer.Enabled := LPendingReady; + miNodes.Enabled := LReady; + miMessages.Enabled := LReady; + miSyncDialog.Enabled := LReady; + miOptions.Enabled := LReady; + miPrivateKeys.Enabled := LReady; + miAdvancedOperation.Enabled := LPendingReady and (not TUserInterface.Offline); + miOperationsWorkbench.Enabled := LPendingReady; + miRPCCalls.Enabled := LReady and (not TUserInterface.Offline); + miSeedNodes.Enabled := LReady; + miAboutMenu.Enabled := LReady; + tbtnSync.Enabled := LReady; + RefreshWalletLockIcon; + RefreshConnectivityIcon; +end; + +procedure TFRMMainForm.SetStartupHeightLocked(ALocked: Boolean); +begin + if FStartupHeightLocked = ALocked then Exit; + FStartupHeightLocked := ALocked; + if ALocked then begin + FStartupMinHeight := Constraints.MinHeight; + FStartupMaxHeight := Constraints.MaxHeight; + FStartupBorderIcons := BorderIcons; + // Keep horizontal sizing and native Close available while the splash is + // shown. Maximizing would also change its height, so restore that command + // together with the original constraints when local loading finishes. + BorderIcons := BorderIcons - [biMaximize]; + Constraints.MinHeight := Height; + Constraints.MaxHeight := Height; + end else begin + Constraints.MinHeight := FStartupMinHeight; + Constraints.MaxHeight := FStartupMaxHeight; + BorderIcons := FStartupBorderIcons; + end; end; procedure TFRMMainForm.RefreshConnectionStatusDisplay; -var errors : AnsiString; begin - //HS XXXXXXXXXXXX was triggering below handlers, ignore? - //FMainForm.SyncControl.UpdateNodeStatus; - //HandleNetStatisticsChangedBackend(FMainForm); - if Assigned(TUserInterface.Node) then begin - if TUserInterface.Node.IsBlockChainValid(errors) then begin - sbStatusBar.Panels[2].Text := Format('Last account time:%s', [FormatDateTime('dd/mm/yyyy hh:nn:ss',UnivDateTime2LocalDateTime(UnixToUnivDateTime( TUserInterface.Node.Bank.LastOperationBlock.timestamp )))]); - end else begin - sbStatusBar.Panels[2].Text := 'NO BLOCKCHAIN: '+errors; - end; - end else begin + if TUserInterface.State in [uisLoading, uisError] then + sbStatusBar.Panels[2].Text := TUserInterface.StateText + else if Assigned(FSyncControl) then + // Footer and basic sync labels share one cached committed-node record. + // Secondary average jobs cannot overwrite this with an older block time. + sbStatusBar.Panels[2].Text := FSyncControl.ChainStatusText + else sbStatusBar.Panels[2].Text := ''; - end; end; procedure TFRMMainForm.SetMode(AMode: TFRMMainFormMode); var nestedForm : TForm; begin + if (AMode = wmWallet) and + ((not TUserInterface.Started) or (TUserInterface.State in [uisLoading, uisError])) then Exit; if FMode = AMode then exit; FMode := AMode; @@ -301,7 +429,7 @@ procedure TFRMMainForm.CM_ModeChanged(var Msg: TMessage); case FMode of wmWallet: begin if NOT Assigned(FWalletControl) then begin - FWalletControl := TCTRLWallet.Create(self); + FWalletControl := CreateWalletControl; paWalletPanel.AddControlDockCenter(FWalletControl); end; paWalletPanel.Visible := true; @@ -309,7 +437,8 @@ procedure TFRMMainForm.CM_ModeChanged(var Msg: TMessage); end; wmSync: begin paSyncPanel.Visible := true; - paWalletPanel.Visible := false;; + paWalletPanel.Visible := false; + FSyncControl.RefreshNodeState; end; else raise Exception.Create('[Internal Error] - TFRMMainForm.SetMode: unsupported mode passed'); end; @@ -322,10 +451,114 @@ procedure TFRMMainForm.CM_ModeChanged(var Msg: TMessage); {%region Handlers: Form} +procedure TFRMMainForm.WndProc(var Msg: {$IFDEF FPC}TLMessage{$ELSE}TMessage{$ENDIF}); +begin + // LCL supplies LM_* on every widgetset; VCL uses the native WM_* identifier. + // A native maximize command can bypass a disabled caption button. + if FStartupHeightLocked and + (Msg.Msg = {$IFDEF FPC}LM_SYSCOMMAND{$ELSE}WM_SYSCOMMAND{$ENDIF}) and + ((Msg.WParam and $FFF0) = SC_MAXIMIZE) then begin + {$IFDEF FPC} + // The Win32 LCL widgetset resumes native system-command processing from + // the message ID, independently of Result. Consume that message as well. + Msg.Msg := LM_NULL; + {$ENDIF} + Msg.Result := 0; + Exit; + end; + inherited; +end; + procedure TFRMMainForm.FormResize(Sender: TObject); begin - // Adjust status bar so that footer toolbar panel remains fixed size (by expanding out previous panel) - sbStatusBar.Panels[2].Width := ClipValue(sbStatusBar.Width - (sbStatusBar.Panels[0].Width + sbStatusBar.Panels[1].Width + tbStatusToolBar.Width + CT_FOOTER_TOOLBAR_RIGHT_PADDING) - CT_FOOTER_TOOLBAR_LEFT_PADDING, 0, sbStatusBar.Width); + if FFooterLayoutActive or FFooterLayoutPending or + (csLoading in ComponentState) or (csDestroying in ComponentState) then Exit; + if not Assigned(sbStatusBar) or not Assigned(tbStatusToolBar) then Exit; + // LCL can raise OnResize before the native status panel has processed its + // new bounds. Reconcile child layout after that resize/scaling pass completes. + FFooterLayoutPending := True; + PostMessage(Handle, CM_PC_LayoutFooter, 0, 0); +end; + +procedure TFRMMainForm.CM_LayoutFooter(var Msg: TMessage); +begin + FFooterLayoutPending := False; + LayoutFooter; +end; + +procedure TFRMMainForm.LayoutFooter; +var + LLeft, LTop, LHeight, LDeficit, LMargin: Integer; + LContent, LPanelRect: TRect; + + function Scaled(AValue: Integer): Integer; + begin + {$IFDEF FPC}Result := Scale96ToFont(AValue); + {$ELSE}Result := ScaleValue(AValue);{$ENDIF} + end; + + function FooterContentRect: TRect; + var LThemedContent: TRect; + begin + LPanelRect := sbStatusBar.ClientRect; + LPanelRect.Left := Max(0, LPanelRect.Right - tbStatusToolBar.Width - + Scaled(CT_FOOTER_TOOLBAR_LEFT_PADDING + CT_FOOTER_TOOLBAR_RIGHT_PADDING)); + {$IFDEF MSWINDOWS} + if sbStatusBar.HandleAllocated then + sbStatusBar.Perform(SB_GETRECT, sbStatusBar.Panels.Count - 1, LPARAM(@LPanelRect)); + {$ENDIF} + Result := LPanelRect; + // A status bar paints panel borders inside its client rectangle. Child + // window containment alone does not keep the toolbar off those borders. + InflateRect(Result, -LMargin, -LMargin); + if sbStatusBar.HandleAllocated then begin + {$IFDEF FPC} + LThemedContent := ThemeServices.ContentRect(sbStatusBar.Canvas.Handle, + ThemeServices.GetElementDetails(tsPane), LPanelRect); + {$ELSE} + if not StyleServices(sbStatusBar).GetElementContentRect(sbStatusBar.Canvas.Handle, + StyleServices(sbStatusBar).GetElementDetails(tsPane), LPanelRect, LThemedContent) then + LThemedContent := Result; + {$ENDIF} + Result.Left := Max(Result.Left, LThemedContent.Left); + Result.Top := Max(Result.Top, LThemedContent.Top); + Result.Right := Min(Result.Right, LThemedContent.Right); + Result.Bottom := Min(Result.Bottom, LThemedContent.Bottom); + end; + Result.Right := Min(Result.Right, + sbStatusBar.ClientWidth - Scaled(CT_FOOTER_TOOLBAR_RIGHT_PADDING)); + end; +begin + if (not Assigned(sbStatusBar)) or (not Assigned(tbStatusToolBar)) then Exit; + if FFooterLayoutActive or (csLoading in ComponentState) or + (csDestroying in ComponentState) or (sbStatusBar.Panels.Count < 4) then Exit; + FFooterLayoutActive := True; + try + LMargin := Max(1, Scaled(1)); + LContent := FooterContentRect; + // Recalculate the complete height so scaling back down also releases space + // temporarily reserved before the toolbar's own DPI resize has completed. + LHeight := tbStatusToolBar.Height + Max(2 * Scaled(CT_FOOTER_TOOLBAR_VERTICAL_PADDING), + sbStatusBar.Height - (LContent.Bottom - LContent.Top) + 2 * LMargin); + if sbStatusBar.Height <> LHeight then sbStatusBar.Height := LHeight; + // The native status bar stretches its last panel; reserve its space by + // sizing the preceding text panel, including padding and the sizing grip. + sbStatusBar.Panels[2].Width := ClipValue(sbStatusBar.ClientWidth - + sbStatusBar.Panels[0].Width - sbStatusBar.Panels[1].Width - tbStatusToolBar.Width - + Scaled(CT_FOOTER_TOOLBAR_LEFT_PADDING + CT_FOOTER_TOOLBAR_RIGHT_PADDING), + 0, sbStatusBar.ClientWidth); + LContent := FooterContentRect; + LDeficit := tbStatusToolBar.Height + 2 * LMargin - (LContent.Bottom - LContent.Top); + if LDeficit > 0 then begin + sbStatusBar.Height := sbStatusBar.Height + LDeficit; + LContent := FooterContentRect; + end; + LLeft := Max(LContent.Left, LContent.Right - tbStatusToolBar.Width); + LTop := LContent.Top + Max(0, (LContent.Bottom - LContent.Top - tbStatusToolBar.Height) div 2); + tbStatusToolBar.SetBounds(LLeft, LTop, tbStatusToolBar.Width, tbStatusToolBar.Height); + finally + FFooterLayoutActive := False; + end; end; {%endregion} @@ -334,7 +567,7 @@ procedure TFRMMainForm.FormResize(Sender: TObject); procedure TFRMMainForm.CM_FinishedLoadingDatabase(var Msg: TMessage); begin - Self.Enabled:=true; + RefreshStartupControls; end; procedure TFRMMainForm.OnLoaded(Sender: TObject); @@ -343,6 +576,13 @@ procedure TFRMMainForm.OnLoaded(Sender: TObject); PostMessage(Self.Handle,CM_PC_FinishedLoadingDatabase,0,0); end; +procedure TFRMMainForm.OnStateChanged(Sender: TObject); +begin + RefreshStartupControls; + RefreshConnectionStatusDisplay; + OnNetStatisticsChanged(Sender); +end; + procedure TFRMMainForm.CM_WalletChanged(var Msg: TMessage); begin RefreshWalletLockIcon; @@ -357,14 +597,18 @@ procedure TFRMMainForm.OnWalletChanged(Sender: TObject); procedure TFRMMainForm.CM_ConnectivityChanged(var Msg: TMessage); begin RefreshConnectivityIcon; + OnNetStatisticsChanged(Self); + FSyncControl.RefreshNodeState; end; procedure TFRMMainForm.OnAppStarted(Sender: TObject); begin + RefreshStartupControls; RefreshConnectionStatusDisplay; + OnNetStatisticsChanged(Sender); end; -procedure TFRMMainForm.OnLoading(Sender: TObject; const message : String; curPos, totalCount : Int64); +procedure TFRMMainForm.OnLoading(Sender: TObject; const AProgress : TProgressInfo); begin // TODO: show progress bar end; @@ -391,7 +635,7 @@ procedure TFRMMainForm.OnNetStatisticsChanged(Sender: TObject); end; end; -procedure TFRMMainForm.OnUIRefreshTimer(Sender: TObject); +procedure TFRMMainForm.OnSyncSnapshotChanged(Sender: TObject); begin RefreshConnectionStatusDisplay; end; @@ -505,6 +749,29 @@ procedure TFRMMainForm.miPrivateKeysClick(Sender: TObject); TUserInterface.ShowPrivateKeysDialog(Self); end; +procedure TFRMMainForm.miAdvancedOperationClick(Sender: TObject); +var + LScoped : TDisposables; + LAccounts : TOrderedCardinalList; + LAccount : Cardinal; +begin + if not Assigned(FWalletControl) then Exit; + LAccounts := LScoped.AddObject(TOrderedCardinalList.Create) as TOrderedCardinalList; + for LAccount in FWalletControl.SelectedAccounts do + LAccounts.Add(LAccount); + TUserInterface.ShowNewOperationDialog(Self, LAccounts, TSettings.DefaultFee); +end; + +procedure TFRMMainForm.miOperationsWorkbenchClick(Sender: TObject); +begin + TUserInterface.ShowOperationsWorkbench(Self); +end; + +procedure TFRMMainForm.miRPCCallsClick(Sender: TObject); +begin + TUserInterface.ShowRPCCallsDialog(Self); +end; + {%endregion} {%region Handlers: Footer Toolbar} @@ -525,8 +792,9 @@ procedure TFRMMainForm.tbtnWalletLockClick(Sender:TObject); procedure TFRMMainForm.tbtnConnectivityClick(Sender:TObject); begin + if TUserInterface.Offline or (TUserInterface.State in [uisLoading, uisError]) then Exit; // Toggle connectivity (icons update with event handler) - TNetData.NetData.NetConnectionsActive := NOT TNetData.NetData.NetConnectionsActive; + TUserInterface.ToggleConnectivity; end; procedure TFRMMainForm.tbtnSyncClick(Sender: TObject); diff --git a/src/gui-experimental/UFRMMemoText.dfm b/src/gui-experimental/UFRMMemoText.dfm new file mode 100644 index 000000000..882dd151c --- /dev/null +++ b/src/gui-experimental/UFRMMemoText.dfm @@ -0,0 +1,81 @@ +object FRMMemoText: TFRMMemoText + Left = 410 + Height = 428 + Top = 234 + Width = 745 + BorderIcons = [biSystemMenu, biMaximize] + Caption = 'Information' + ClientHeight = 428 + ClientWidth = 745 + Color = clBtnFace + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + OnCreate = FormCreate + OnResize = FormResize + Position = poOwnerFormCenter + object pnlPrompt: TPanel + Left = 0 + Top = 0 + Width = 745 + Height = 110 + Align = alTop + BevelOuter = bvNone + TabOrder = 2 + Visible = False + object lblPrompt: TLabel + Left = 12 + Top = 12 + Width = 721 + Height = 86 + Anchors = [akLeft, akTop, akRight] + AutoSize = False + Caption = '' + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + WordWrap = True + end + end + object pnlBottom: TPanel + Left = 0 + Height = 55 + Top = 373 + Width = 745 + Align = alBottom + BevelOuter = bvNone + TabOrder = 0 + object bbCancel: TBitBtn + Left = 620 + Height = 31 + Top = 14 + Width = 116 + Anchors = [akTop, akRight] + Cancel = True + Caption = 'Close' + Kind = bkCancel + ModalResult = 2 + TabOrder = 0 + end + end + object Memo: TMemo + Left = 0 + Height = 373 + Top = 0 + Width = 745 + Align = alClient + Font.Color = clWindowText + Font.Height = -16 + Font.Name = 'Tahoma' + Lines.Strings = ( + 'Memo' + ) + ParentFont = False + ReadOnly = True + ScrollBars = ssBoth + TabOrder = 1 + WordWrap = False + end +end diff --git a/src/gui-experimental/UFRMMemoText.lfm b/src/gui-experimental/UFRMMemoText.lfm index 5dd6e7474..ace904de9 100644 --- a/src/gui-experimental/UFRMMemoText.lfm +++ b/src/gui-experimental/UFRMMemoText.lfm @@ -12,8 +12,34 @@ object FRMMemoText: TFRMMemoText Font.Height = -11 Font.Name = 'Tahoma' OnCreate = FormCreate + OnResize = FormResize Position = poOwnerFormCenter LCLVersion = '1.8.0.6' + object pnlPrompt: TPanel + Left = 0 + Top = 0 + Width = 745 + Height = 110 + Align = alTop + BevelOuter = bvNone + TabOrder = 2 + Visible = False + object lblPrompt: TLabel + Left = 12 + Top = 12 + Width = 721 + Height = 86 + Anchors = [akLeft, akTop, akRight] + AutoSize = False + Caption = '' + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + WordWrap = True + end + end object pnlBottom: TPanel Left = 0 Height = 55 diff --git a/src/gui-experimental/UFRMMemoText.pas b/src/gui-experimental/UFRMMemoText.pas index f8a1b5553..583500d5c 100644 --- a/src/gui-experimental/UFRMMemoText.pas +++ b/src/gui-experimental/UFRMMemoText.pas @@ -1,5 +1,16 @@ unit UFRMMemoText; +{ Shows or edits multiline text in a bounded, scrollable dialog. Confirmation + and result helpers keep long operation details inside the available work + area. } + +{ Public types in this unit + + Type | Purpose + -------------+------------------------------------------------------------------------- + TFRMMemoText | Displays or edits multiline text with confirmation controls. +} + interface uses @@ -16,25 +27,210 @@ TFRMMemoText = class(TForm) pnlBottom: TPanel; Memo: TMemo; bbCancel: TBitBtn; + pnlPrompt: TPanel; + lblPrompt: TLabel; procedure FormCreate(Sender: TObject); + procedure FormResize(Sender: TObject); private + FAllowInput: Boolean; + FbbOk : TBitBtn; + FMessageLayout, FArrangingMessage: Boolean; + procedure EnsureOkButton; + procedure InitMessage(AParent: TForm; const ATitle, APrompt, ADetails: String; AConfirm: Boolean); + procedure SetAllowInput(const Value: Boolean); + procedure SetDataText(const Value: String); + function GetDataText: String; { Private declarations } public { Public declarations } Procedure InitData(const Title : String; const text : String); + property AllowInput : Boolean read FAllowInput write SetAllowInput; + property DataText : String read GetDataText write SetDataText; end; + +function InputMemoQuery(const ATitle : String; AAllowMultiline : Boolean; var AText : String) : Boolean; +function ConfirmMemoQuery(AParent: TForm; const ATitle, APrompt, ADetails: String): Boolean; +procedure ShowResultMemo(AParent: TForm; const ATitle, APrompt, ADetails: String); + implementation -{$IFnDEF FPC} - {$R *.dfm} +uses UMemory, Math, Types; + +{$IFDEF FPC} +{$R *.lfm} {$ELSE} - {$R *.lfm} +{$R *.dfm} {$ENDIF} +function InputMemoQuery(const ATitle : String; AAllowMultiline : Boolean; var AText : String) : Boolean; +Var LFRM : TFRMMemoText; + LDisposables: TDisposables; +begin + LFRM := LDisposables.AddObject(TFRMMemoText.Create(Nil)) as TFRMMemoText; + LFRM.InitData(ATitle,AText); + LFRM.AllowInput := True; + if AAllowMultiline then begin + LFRM.Memo.ScrollBars := ssBoth; + end else begin + LFRM.Memo.ScrollBars := ssNone; + LFRM.Memo.WordWrap := True; + end; + if LFRM.ShowModal=MrOk then begin + AText := LFRM.DataText; + Result := True; + end else Result := False; + +end; + +function ConfirmMemoQuery(AParent: TForm; const ATitle, APrompt, ADetails: String): Boolean; +var + LDisposables: TDisposables; + LForm: TFRMMemoText; +begin + // The local bag owns this modal form; PopupParent supplies presentation only. + LForm := LDisposables.AddObject(TFRMMemoText.Create(nil)) as TFRMMemoText; + LForm.InitMessage(AParent, ATitle, APrompt, ADetails, True); + // Only the affirmative button authorizes submission; close and Esc decline. + Result := LForm.ShowModal = mrYes; +end; + +procedure ShowResultMemo(AParent: TForm; const ATitle, APrompt, ADetails: String); +var + LDisposables: TDisposables; + LForm: TFRMMemoText; +begin + LForm := LDisposables.AddObject(TFRMMemoText.Create(nil)) as TFRMMemoText; + LForm.InitMessage(AParent, ATitle, APrompt, ADetails, False); + LForm.ShowModal; +end; + +procedure TFRMMemoText.InitMessage(AParent: TForm; const ATitle, APrompt, ADetails: String; AConfirm: Boolean); +var + LMonitor: TMonitor; + LWork, LWindow: TRect; + LWidth, LHeight, LFrameWidth, LFrameHeight: Integer; +begin + // A stable modal owner avoids LCL's pmNone handle recreation, which would + // recreate the loaded memo with its complete text as an initial window caption. + PopupMode := pmExplicit; + if Assigned(AParent) then + PopupParent := AParent + else if Screen.ActiveForm <> Self then + PopupParent := Screen.ActiveForm; + AllowInput := False; + Memo.WordWrap := False; + Memo.ScrollBars := ssBoth; + Memo.MaxLength := MaxInt; + InitData(ATitle, ''); + lblPrompt.Caption := APrompt; + // This mode owns child bounds; right anchors would reapply deferred panel sizing. + lblPrompt.Anchors := [akLeft, akTop]; + bbCancel.Anchors := [akLeft, akTop]; + pnlPrompt.Visible := True; + if AConfirm then begin + EnsureOkButton; + FbbOk.Name := 'bbConfirmYes'; + FbbOk.Kind := bkYes; + FbbOk.Caption := 'Yes'; + FbbOk.ModalResult := mrYes; + FbbOk.Default := False; + FbbOk.Cancel := False; + FbbOk.Anchors := [akLeft, akTop]; + bbCancel.Kind := bkNo; + bbCancel.Caption := 'No'; + bbCancel.ModalResult := mrNo; + end else begin + bbCancel.Kind := bkCancel; + bbCancel.Caption := 'Close'; + bbCancel.ModalResult := mrCancel; + end; + bbCancel.Cancel := True; + bbCancel.Default := True; + ActiveControl := bbCancel; + BorderStyle := bsSizeable; + Position := poDesigned; + if Assigned(AParent) then + LMonitor := AParent.Monitor + else if Assigned(Screen.ActiveForm) then + LMonitor := Screen.ActiveForm.Monitor + else + LMonitor := Screen.PrimaryMonitor; + LWork := LMonitor.WorkareaRect; + // VCL bounds include the native frame; LCL form bounds can exclude it. Measure + // the difference after handle creation so either toolkit fits the whole window. + HandleNeeded; + GetWindowRect(Handle, LWindow); + LFrameWidth := Max(0, LWindow.Right - LWindow.Left - Width); + LFrameHeight := Max(0, LWindow.Bottom - LWindow.Top - Height); + LWidth := Min(MulDiv(900, PixelsPerInch, 96), + Max(1, LWork.Right - LWork.Left - LFrameWidth)); + LHeight := Min(MulDiv(620, PixelsPerInch, 96), + Max(1, LWork.Bottom - LWork.Top - LFrameHeight)); + Constraints.MinWidth := Min(MulDiv(480, PixelsPerInch, 96), LWidth); + Constraints.MinHeight := Min(MulDiv(300, PixelsPerInch, 96), LHeight); + SetBounds(LWork.Left + (LWork.Right - LWork.Left - LWidth - LFrameWidth) div 2, + LWork.Top + (LWork.Bottom - LWork.Top - LHeight - LFrameHeight) div 2, + LWidth, LHeight); + // LCL Win32 passes preloaded memo text to CreateWindowEx. Create the native + // control empty, then load the complete list through its existing text API. + Memo.HandleNeeded; + Memo.Lines.Text := ADetails; + FMessageLayout := True; + FormResize(Self); +end; + +procedure TFRMMemoText.FormResize(Sender: TObject); +var + LCanvas: TControlCanvas; + LDisposables: TDisposables; + LText: TRect; + LMargin, LButtonHeight, LButtonWidth, LTextHeight: Integer; +begin + if not FMessageLayout or FArrangingMessage then + Exit; + FArrangingMessage := True; + DisableAlign; + try + LMargin := MulDiv(12, PixelsPerInch, 96); + LCanvas := LDisposables.AddObject(TControlCanvas.Create) as TControlCanvas; + LCanvas.Control := Self; + LCanvas.Font.Assign(lblPrompt.Font); + LText := Rect(0, 0, Max(1, ClientWidth - LMargin * 2), 0); + DrawText(LCanvas.Handle, PChar(lblPrompt.Caption), Length(lblPrompt.Caption), + LText, DT_CALCRECT or DT_WORDBREAK or DT_NOPREFIX); + LTextHeight := Max(LCanvas.TextHeight('Wg'), LText.Bottom - LText.Top); + // Coalesce wrapped-label bounds before LCL can re-enter parent alignment. + pnlPrompt.Height := LTextHeight + LMargin * 2; + lblPrompt.SetBounds(LMargin, LMargin, Max(1, ClientWidth - LMargin * 2), LTextHeight); + LCanvas.Font.Assign(bbCancel.Font); + LButtonHeight := Max(MulDiv(31, PixelsPerInch, 96), + LCanvas.TextHeight('Wg') + LMargin); + LButtonWidth := Max(MulDiv(116, PixelsPerInch, 96), + LCanvas.TextWidth(bbCancel.Caption) + LMargin * 2); + pnlBottom.Height := LButtonHeight + LMargin * 2; + bbCancel.SetBounds(Max(LMargin, ClientWidth - LMargin - LButtonWidth), + LMargin, LButtonWidth, LButtonHeight); + if Assigned(FbbOk) then + FbbOk.SetBounds(Max(LMargin, bbCancel.Left - LMargin - LButtonWidth), + LMargin, LButtonWidth, LButtonHeight); + finally + EnableAlign; + FArrangingMessage := False; + end; +end; + procedure TFRMMemoText.FormCreate(Sender: TObject); begin Memo.Clear; + FbbOk := Nil; + FMessageLayout := False; + FArrangingMessage := False; +end; + +function TFRMMemoText.GetDataText: String; +begin + Result := Memo.Lines.Text; end; procedure TFRMMemoText.InitData(const Title, text: String); @@ -43,4 +239,35 @@ procedure TFRMMemoText.InitData(const Title, text: String); Memo.Lines.Text := text; end; +procedure TFRMMemoText.SetAllowInput(const Value: Boolean); +begin + FAllowInput := Value; + Memo.ReadOnly := Not FAllowInput; + if FAllowInput then begin + EnsureOkButton; + end else FreeAndNil(FbbOk); + +end; + +procedure TFRMMemoText.EnsureOkButton; +begin + if Assigned(FbbOk) then + Exit; + FbbOk := TBitBtn.Create(Self); + FbbOk.Parent := bbCancel.Parent; + FbbOk.Anchors := bbCancel.Anchors; + FbbOk.Left := bbCancel.Left - bbCancel.Width - 10; + FbbOk.Top := bbCancel.Top; + FbbOk.Width := bbCancel.Width; + FbbOk.Height := bbCancel.Height; + FbbOk.Kind := bkOK; + FbbOk.ModalResult := MrOk; + FbbOk.Caption := 'Ok'; +end; + +procedure TFRMMemoText.SetDataText(const Value: String); +begin + Memo.Lines.Text := Value; +end; + end. diff --git a/src/gui-experimental/UFRMMessages.dfm b/src/gui-experimental/UFRMMessages.dfm new file mode 100644 index 000000000..a93f752b4 --- /dev/null +++ b/src/gui-experimental/UFRMMessages.dfm @@ -0,0 +1,91 @@ +object FRMMessages: TFRMMessages + Left = 208 + Height = 441 + Top = 47 + Width = 874 + ActiveControl = lbNetConnections + Caption = 'Messages' + ClientHeight = 441 + ClientWidth = 874 + OnActivate = FormActivate + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + object Label11: TLabel + Left = 15 + Height = 15 + Top = 151 + Width = 54 + Caption = 'Messages:' + ParentColor = False + end + object Label12: TLabel + Left = 315 + Height = 15 + Top = 11 + Width = 91 + Caption = 'Message to send:' + ParentColor = False + end + object Label13: TLabel + Left = 15 + Height = 15 + Top = 11 + Width = 121 + Caption = 'Available Connections:' + ParentColor = False + end + object Label14: TLabel + Left = 410 + Height = 13 + Top = 11 + Width = 361 + Caption = '(Messages will be encrypted, so only dest connection will be able to read it)' + Font.Color = clGrayText + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object lbNetConnections: TListBox + Left = 15 + Height = 96 + Top = 30 + Width = 275 + ItemHeight = 0 + MultiSelect = True + ScrollWidth = 273 + TabOrder = 0 + end + object bbSendAMessage: TButton + Left = 315 + Height = 25 + Top = 101 + Width = 525 + Caption = 'Send a Message' + OnClick = bbSendAMessageClick + TabOrder = 1 + end + object memoMessages: TMemo + Left = 15 + Height = 253 + Top = 170 + Width = 821 + Anchors = [akTop, akLeft, akRight, akBottom] + Font.Color = clWindowText + Font.Height = -16 + Font.Name = 'Tahoma' + ParentFont = False + ReadOnly = True + ScrollBars = ssBoth + TabOrder = 2 + end + object memoMessageToSend: TMemo + Left = 315 + Height = 61 + Top = 30 + Width = 525 + TabOrder = 3 + WantReturns = False + end +end diff --git a/src/gui-experimental/UFRMMessages.pas b/src/gui-experimental/UFRMMessages.pas index cecb90b3f..5bba49044 100644 --- a/src/gui-experimental/UFRMMessages.pas +++ b/src/gui-experimental/UFRMMessages.pas @@ -16,14 +16,27 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Lists received node messages and sends messages to selected peer + connections. } + +{ Public types in this unit + + Type | Purpose + -------------+------------------------------------------------------------------------- + TFRMMessages | Displays received messages and composes messages for peers. +} interface {$I ..\config.inc} uses - LCLIntf, LCLType, + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + {$IFDEF FPC}LCLIntf,{$ENDIF} {$IFDEF FPC}LCLType,{$ENDIF} SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Menus, UCommon.UI, UBaseTypes, UNode, UNetProtocol, UCrypto, UFRMMainForm,UConst; @@ -50,37 +63,60 @@ TFRMMessages = class(TApplicationForm) { private declarations } procedure UpdateAvailableConnections; procedure OnNodeMessageEvent(NetConnection : TNetConnection; MessageData : String); + procedure OnNetConnectionsUpdated(Sender : TObject); public - { public declarations } + procedure MarkRead; end; implementation -uses UUserInterface, USettings, Generics.Collections; +uses UUserInterface, USettings, UCommon, Generics.Collections; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TFRMMessages } procedure TFRMMessages.FormActivate(Sender: TObject); begin + MarkRead; UpdateAvailableConnections; end; procedure TFRMMessages.FormCreate(Sender: TObject); begin TUserInterface.NodeMessageEvent.Add(OnNodeMessageEvent); + TUserInterface.NetConnectionsUpdated.Add(OnNetConnectionsUpdated); end; procedure TFRMMessages.FormDestroy(Sender: TObject); begin TUserInterface.NodeMessageEvent.Remove(OnNodeMessageEvent); + TUserInterface.NetConnectionsUpdated.Remove(OnNetConnectionsUpdated); +end; + +procedure TFRMMessages.MarkRead; +begin + FMessagesUnreadCount := 0; + if Owner is TFRMMainForm then + TFRMMainForm(Owner).SyncControl.ClearMessagesNotification; +end; + +procedure TFRMMessages.OnNetConnectionsUpdated(Sender : TObject); +begin + UpdateAvailableConnections; end; procedure TFRMMessages.OnNodeMessageEvent(NetConnection: TNetConnection; MessageData: String); Var s : String; begin - inc(FMessagesUnreadCount); + if Visible then + MarkRead + else + inc(FMessagesUnreadCount); if Assigned(NetConnection) then begin s := DateTimeToStr(now)+' Message received from '+NetConnection.ClientRemoteAddr; memoMessages.Lines.Add(DateTimeToStr(now)+' Message received from '+NetConnection.ClientRemoteAddr+' Length '+inttostr(Length(MessageData))+' bytes'); @@ -136,7 +172,7 @@ procedure TFRMMessages.UpdateAvailableConnections; procedure TFRMMessages.bbSendAMessageClick(Sender: TObject); Var basem,m : String; - them, errors : AnsiString; + them, errors : String; i,n : Integer; nc : TNetConnection; begin diff --git a/src/gui-experimental/UFRMNodes.dfm b/src/gui-experimental/UFRMNodes.dfm new file mode 100644 index 000000000..b9e1aa23a --- /dev/null +++ b/src/gui-experimental/UFRMNodes.dfm @@ -0,0 +1,72 @@ +object FRMNodes: TFRMNodes + Left = 570 + Height = 444 + Top = 333 + Width = 861 + ActiveControl = memoNetConnections + Caption = 'Nodes' + ClientHeight = 444 + ClientWidth = 861 + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + object Label3: TLabel + Left = 15 + Height = 15 + Top = 15 + Width = 106 + Caption = 'Active Connections:' + ParentColor = False + end + object Label6: TLabel + Left = 15 + Height = 15 + Top = 293 + Width = 198 + Anchors = [akLeft, akRight, akBottom] + Caption = 'Known Node Servers:' + ParentColor = False + end + object Label7: TLabel + Left = 15 + Height = 15 + Top = 189 + Width = 99 + Anchors = [akLeft, akRight, akBottom] + Caption = 'Blacklisted Nodes' + ParentColor = False + end + object memoNetConnections: TMemo + Left = 15 + Height = 149 + Top = 34 + Width = 830 + Anchors = [akTop, akLeft, akRight, akBottom] + ReadOnly = True + ScrollBars = ssBoth + TabOrder = 0 + WordWrap = False + end + object memoNetServers: TMemo + Left = 15 + Height = 116 + Top = 314 + Width = 830 + Anchors = [akLeft, akRight, akBottom] + ReadOnly = True + ScrollBars = ssBoth + TabOrder = 1 + WordWrap = False + end + object memoNetBlackLists: TMemo + Left = 16 + Height = 79 + Top = 210 + Width = 829 + Anchors = [akLeft, akRight, akBottom] + ReadOnly = True + ScrollBars = ssBoth + TabOrder = 2 + WordWrap = False + end +end diff --git a/src/gui-experimental/UFRMNodes.pas b/src/gui-experimental/UFRMNodes.pas index 9f876300d..92d29c177 100644 --- a/src/gui-experimental/UFRMNodes.pas +++ b/src/gui-experimental/UFRMNodes.pas @@ -16,16 +16,29 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Displays peer connections, known node addresses and network statistics. + Refreshes the lists from network-change events. } + +{ Public types in this unit + + Type | Purpose + ----------+------------------------------------------------------------------------- + TFRMNodes | Shows peer connections, known nodes and network statistics. +} interface {$I ..\config.inc} uses - LCLIntf, LCLType, + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + {$IFDEF FPC}LCLIntf,{$ENDIF} {$IFDEF FPC}LCLType,{$ENDIF} Messages, SysUtils, Classes, Graphics, Controls, Forms, - Dialogs, ExtCtrls, StdCtrls, LMessages, UCommon.UI, + Dialogs, ExtCtrls, StdCtrls, {$IFDEF FPC}LMessages,{$ENDIF} UCommon.UI, ULog, UBlockChain, UNode, Menus, UNetProtocol; Const @@ -62,7 +75,11 @@ implementation uses UCommon, UUserInterface, UTime, Generics.Collections; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TFRMNodes } @@ -227,7 +244,7 @@ procedure TFRMNodes.OnNetNodeServersUpdated(Sender: TObject); end; procedure TFRMNodes.CM_BlackListUpdated(var Msg: TMessage); -Const CT_TRUE_FALSE : Array[Boolean] Of AnsiString = ('FALSE','TRUE'); +Const CT_TRUE_FALSE : Array[Boolean] Of String = ('FALSE','TRUE'); Var i,j,n : integer; P : PNodeServerAddress; l : TList; diff --git a/src/gui-experimental/UFRMNodesIp.dfm b/src/gui-experimental/UFRMNodesIp.dfm new file mode 100644 index 000000000..19fb12148 --- /dev/null +++ b/src/gui-experimental/UFRMNodesIp.dfm @@ -0,0 +1,85 @@ +object FRMNodesIp: TFRMNodesIp + Left = 771 + Height = 367 + Top = 23 + Width = 334 + BorderIcons = [biSystemMenu] + BorderStyle = bsSingle + Caption = 'Node IP Addresses' + ClientHeight = 367 + ClientWidth = 334 + Color = clBtnFace + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Position = poOwnerFormCenter + Visible = False + object Label1: TLabel + Left = 30 + Height = 13 + Top = 25 + Width = 143 + Caption = 'Availble Node IP''s to connect:' + ParentColor = False + end + object memoNodesIp: TMemo + Left = 30 + Height = 226 + Top = 45 + Width = 276 + Lines.Strings = ( + 'memoNodesIp' + ) + ScrollBars = ssBoth + TabOrder = 0 + end + object bbOk: TBitBtn + Left = 138 + Height = 25 + Top = 317 + Width = 75 + Caption = 'OK' + Default = True + Glyph.Data = { + DE010000424DDE01000000000000760000002800000024000000120000000100 + 0400000000006801000000000000000000001000000000000000000000000000 + 80000080000000808000800000008000800080800000C0C0C000808080000000 + FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333333333 + 3333333333333333333333330000333333333333333333333333F33333333333 + 00003333344333333333333333388F3333333333000033334224333333333333 + 338338F3333333330000333422224333333333333833338F3333333300003342 + 222224333333333383333338F3333333000034222A22224333333338F338F333 + 8F33333300003222A3A2224333333338F3838F338F33333300003A2A333A2224 + 33333338F83338F338F33333000033A33333A222433333338333338F338F3333 + 0000333333333A222433333333333338F338F33300003333333333A222433333 + 333333338F338F33000033333333333A222433333333333338F338F300003333 + 33333333A222433333333333338F338F00003333333333333A22433333333333 + 3338F38F000033333333333333A223333333333333338F830000333333333333 + 333A333333333333333338330000333333333333333333333333333333333333 + 0000 + } + NumGlyphs = 2 + OnClick = bbOkClick + TabOrder = 1 + end + object bbCancel: TBitBtn + Left = 231 + Height = 25 + Top = 317 + Width = 75 + Cancel = True + Caption = 'Cancel' + Kind = bkCancel + ModalResult = 2 + TabOrder = 2 + end + object cbTryOnlyWithThisServers: TCheckBox + Left = 35 + Height = 19 + Top = 285 + Width = 164 + Caption = 'Connect ONLY to these nodes' + OnClick = cbTryOnlyWithThisServersClick + TabOrder = 3 + end +end diff --git a/src/gui-experimental/UFRMNodesIp.pas b/src/gui-experimental/UFRMNodesIp.pas index cec411548..fd96273bc 100644 --- a/src/gui-experimental/UFRMNodesIp.pas +++ b/src/gui-experimental/UFRMNodesIp.pas @@ -16,7 +16,19 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$MODE Delphi} +{$ENDIF} + +{ Edits the node addresses used for peer discovery and fixed-server + connections. } + +{ Public types in this unit + + Type | Purpose + ------------+------------------------------------------------------------------------- + TFRMNodesIp | Edits discovery addresses and fixed-server connection settings. +} interface @@ -53,7 +65,11 @@ implementation uses UUserInterface, USettings, UNetProtocol, UNode, UConst; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TFRMNodesIp } @@ -66,8 +82,8 @@ procedure TFRMNodesIp.Refresh; Var nsarr : TNodeServerAddressArray; i : Integer; - ips : AnsiString; - aux : AnsiString; + ips : String; + aux : String; begin memoNodesIp.Clear; setlength(nsarr,0); @@ -95,7 +111,7 @@ procedure TFRMNodesIp.Refresh; procedure TFRMNodesIp.bbOkClick(Sender: TObject); Var nsarr : TNodeServerAddressArray; - ips : AnsiString; + ips : String; begin TNode.DecodeIpStringToNodeServerAddressArray(memoNodesIp.Lines.Text,nsarr); if (length(nsarr)=0) And (cbTryOnlyWithThisServers.Checked) then begin diff --git a/src/gui-experimental/UFRMOperation.dfm b/src/gui-experimental/UFRMOperation.dfm new file mode 100644 index 000000000..955f33d23 --- /dev/null +++ b/src/gui-experimental/UFRMOperation.dfm @@ -0,0 +1,1165 @@ +object FRMOperation: TFRMOperation + Left = 564 + Height = 650 + Top = 267 + Width = 760 + BorderIcons = [biSystemMenu] + BorderStyle = bsSingle + Caption = 'New Operation' + ClientHeight = 650 + ClientWidth = 760 + Color = clBtnFace + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + OnCreate = FormCreate + OnDestroy = FormDestroy + OnResize = FormResize + OnShow = FormShow + Position = poOwnerFormCenter + object lblAccountCaption: TLabel + Left = 25 + Height = 13 + Top = 20 + Width = 43 + Caption = 'Account:' + ParentColor = False + end + object lblAccountBalance: TLabel + Left = 432 + Height = 19 + Top = 15 + Width = 149 + Caption = 'lblAccountBalance' + Font.Color = clGreen + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblBalanceCaption: TLabel + Left = 387 + Height = 13 + Top = 20 + Width = 41 + Caption = 'Balance:' + ParentColor = False + end + object lblAccountsCount: TLabel + Left = 387 + Height = 13 + Top = 58 + Width = 18 + Caption = 'XXX' + ParentColor = False + end + object bbExecute: TBitBtn + Left = 300 + Height = 31 + Top = 464 + Width = 130 + Caption = 'Execute (F12)' + Glyph.Data = { + DE010000424DDE01000000000000760000002800000024000000120000000100 + 0400000000006801000000000000000000001000000000000000000000000000 + 80000080000000808000800000008000800080800000C0C0C000808080000000 + FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333333333 + 3333333333333333333333330000333333333333333333333333F33333333333 + 00003333344333333333333333388F3333333333000033334224333333333333 + 338338F3333333330000333422224333333333333833338F3333333300003342 + 222224333333333383333338F3333333000034222A22224333333338F338F333 + 8F33333300003222A3A2224333333338F3838F338F33333300003A2A333A2224 + 33333338F83338F338F33333000033A33333A222433333338333338F338F3333 + 0000333333333A222433333333333338F338F33300003333333333A222433333 + 333333338F338F33000033333333333A222433333333333338F338F300003333 + 33333333A222433333333333338F338F00003333333333333A22433333333333 + 3338F38F000033333333333333A223333333333333338F830000333333333333 + 333A333333333333333338330000333333333333333333333333333333333333 + 0000 + } + NumGlyphs = 2 + OnClick = actExecuteExecute + TabOrder = 2 + end + object bbCancel: TBitBtn + Left = 465 + Height = 31 + Top = 464 + Width = 116 + Cancel = True + Caption = 'Cancel' + Kind = bkCancel + ModalResult = 2 + TabOrder = 3 + end + object PageControlLocked: TPageControl + Left = 25 + Height = 383 + Top = 72 + Width = 556 + ActivePage = tsOperation + TabIndex = 0 + TabOrder = 1 + object tsOperation: TTabSheet + Caption = 'Operation' + TabVisible = False + object lblFee: TLabel + Left = 15 + Height = 13 + Top = 188 + Width = 69 + Caption = 'Operation Fee' + ParentColor = False + end + object gbPayload: TGroupBox + Left = 13 + Height = 145 + Top = 207 + Width = 521 + Caption = ' Payload: ' + TabOrder = 2 + object lblEncryptPassword: TLabel + Left = 38 + Height = 13 + Top = 67 + Width = 50 + Caption = 'Password:' + ParentColor = False + end + object Label4: TLabel + Left = 256 + Height = 13 + Top = 6 + Width = 67 + Caption = 'Payload data:' + ParentColor = False + end + object lblEncryptionErrors: TLabel + Left = 255 + Height = 33 + Top = 87 + Width = 153 + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + WordWrap = True + end + object lblPayloadLength: TLabel + Left = 352 + Height = 13 + Top = 6 + Width = 149 + Alignment = taRightJustify + AutoSize = False + Caption = 'XXX' + ParentColor = False + end + object rbEncryptedWithEC: TRadioButton + Left = 15 + Height = 19 + Top = 26 + Width = 207 + Caption = 'Encrypted with dest account public key' + Checked = True + OnClick = memoPayloadClick + TabOrder = 1 + TabStop = True + end + object rbEncrptedWithPassword: TRadioButton + Left = 15 + Height = 19 + Top = 44 + Width = 141 + Caption = 'Encrypted with password' + OnClick = memoPayloadClick + TabOrder = 2 + end + object rbNotEncrypted: TRadioButton + Left = 15 + Height = 19 + Top = 84 + Width = 162 + Caption = 'Dont encrypt (Public payload)' + OnClick = memoPayloadClick + TabOrder = 4 + end + object ebEncryptPassword: TEdit + Left = 94 + Height = 21 + Top = 64 + Width = 122 + OnChange = ebEncryptPasswordChange + OnClick = memoPayloadClick + TabOrder = 3 + end + object memoPayload: TMemo + Left = 255 + Height = 57 + Top = 28 + Width = 246 + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + OnChange = memoPayloadClick + OnClick = memoPayloadClick + ParentFont = False + TabOrder = 5 + WantReturns = False + end + object rbEncryptedWithOldEC: TRadioButton + Left = 15 + Height = 19 + Top = 7 + Width = 159 + Caption = 'Encrypted with old public key' + OnClick = memoPayloadClick + TabOrder = 0 + end + object cbPayloadDataInputType: TComboBox + Left = 412 + Height = 21 + Top = 88 + Width = 89 + ItemHeight = 13 + Items.Strings = ( + 'As String'#11 + 'As Hexadecimal'#11 + 'As Base58' + ) + Style = csDropDownList + TabOrder = 6 + end + end + object ebFee: TEdit + Left = 90 + Height = 21 + Top = 184 + Width = 70 + TabOrder = 1 + end + object PageControlOpType: TPageControl + Left = 16 + Height = 167 + Top = 11 + Width = 521 + ActivePage = tsTransaction + TabIndex = 0 + TabOrder = 0 + OnChange = PageControlOpTypeChange + object tsTransaction: TTabSheet + Caption = 'Transaction' + object lblDestAccount: TLabel + Left = 13 + Height = 13 + Top = 32 + Width = 95 + Caption = 'Destination account' + ParentColor = False + end + object lblAmount: TLabel + Left = 72 + Height = 13 + Top = 61 + Width = 37 + Caption = 'Amount' + ParentColor = False + end + object lblTransactionErrors: TLabel + Left = 13 + Height = 13 + Top = 10 + Width = 331 + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object ebDestAccount: TEdit + Left = 115 + Height = 21 + Top = 29 + Width = 357 + TabOrder = 0 + end + object ebAmount: TEdit + Left = 115 + Height = 21 + Top = 58 + Width = 86 + TabOrder = 1 + end + object sbSearchDestinationAccount: TSpeedButton + Left = 480 + Height = 22 + Top = 28 + Width = 23 + Glyph.Data = { + 36030000424D3803000000000000360000002800000010000000100000000100 + 18000000000000000000120B0000120B00000000000000000000FF00FF4A667C + BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF + 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF + 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 + B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD + EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF + FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 + C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF + FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF + E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 + C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 + C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 + DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 + 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF + } + OnClick = sbSearchDestinationAccountClick + end + object memoEPASA: TMemo + Left = 13 + Height = 48 + Top = 88 + Width = 492 + BorderStyle = bsNone + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + OnChange = memoPayloadClick + OnClick = memoPayloadClick + ParentFont = False + ReadOnly = True + TabOrder = 2 + TabStop = False + WantReturns = False + end + end + object tsChangePrivateKey: TTabSheet + Caption = 'Change key' + ImageIndex = 1 + object lblNewPrivateKey: TLabel + Left = 61 + Height = 13 + Top = 41 + Width = 78 + Caption = 'New private key' + ParentColor = False + end + object lblNewOwnerPublicKey: TLabel + Left = 28 + Height = 13 + Top = 91 + Width = 109 + Caption = 'New owners public key' + ParentColor = False + end + object lblNewOwnerErrors: TLabel + Left = 177 + Height = 13 + Top = 69 + Width = 302 + Alignment = taRightJustify + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object lblChangeKeyErrors: TLabel + Left = 149 + Height = 13 + Top = 16 + Width = 331 + Alignment = taRightJustify + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object rbChangeKeyWithAnother: TRadioButton + Left = 16 + Height = 19 + Top = 21 + Width = 114 + Caption = 'Change private key' + TabOrder = 0 + end + object cbNewPrivateKey: TComboBox + Left = 149 + Height = 21 + Top = 38 + Width = 294 + ItemHeight = 13 + Sorted = True + Style = csDropDownList + TabOrder = 1 + end + object ebNewPublicKey: TEdit + Left = 148 + Height = 21 + Top = 88 + Width = 331 + TabOrder = 2 + end + object bbChangePrivateKeyKeys: TBitBtn + Left = 449 + Height = 22 + Top = 37 + Width = 31 + Glyph.Data = { + 36050000424D3605000000000000360400002800000010000000100000000100 + 0800000000000001000000000000000000000001000000010000FF00FF008C6B + 6C0087707000AE877C000D7FA9006F7C88006D7C94001F7ECE000E80AA001180 + A7001081AB00048CB900078DBC000B8DBA000C8DBA00088EBC001285B0007882 + 95006092BD005EA8BE000A91C1000F9DCE002087DE0011A7D10030BCDC001F89 + E00059A9DC0044BADD004ABFE00057AEF4004DB1F90049B2FF004EB7FF0057B1 + F60050B6FE0022D7FC0024D7FF0024D8FD0039D7FB0035D8FD004BC6DC0046C6 + E40048D5EE0075D3E90058E9FD006FE6FF0070E6FF0071F9FE007BFFFF008683 + 88008B8697008F989B00969594009C919000AD858000AD868500AB939500A49E + 9900B1979400B5A09F008AA5AD00CAA08C00CDAC9300C2A69A00F3BE8000C6AE + A000CFB7A100D3BBA200F4C88E00F5CB9A00F5D09C00F8D09800DAC5B700E4CC + A900EFD2A900ECD1AC00F6DAAB00F5DEB500F5E1B600F9E1B100FEEAB900FFF2 + BA00A1C6C8008DE6FA0081F8FE008CFAFD008DFCFE0097FCFD009BFBFD00B8ED + F600A7FFFF00AAFFFE00ADFFFE00B6F6FF00B1FCFD00B4FFFF00ECDDCC00E8DD + D600FFF7C600FCF5CD00FCF7D100FAF6D600FFFBD500FEFED600F7F2D900FEFF + D900FFFEDD00C6F5FF00C6FEFF00D2FFFF00FEF7E000FBFCE100FDFFE100FFFF + E400E3FEFF00F9F6F200FFFFF400F1FBFC00F5FFFE00FBFFFF00000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000605000004080408080A000000000011191A000B2A23272D + 531B080000001116201D0B552C23272E531C1509003207201D000F552C23272B + 3A3F41030112202000000F552C252938606771684236200000000B5F5D6B3B61 + 74676A67513D000000000B59181735716A676A63474B360000000F282C23396A + 6A6A6A4C404D360000000B552C2534656A654F455049360000000B552C251343 + 6247446E7336000000000B552C25263C3E4B4E483636000000000F55542F3057 + 523331020000000000000B77766D5F5C5C5C2F08000000000000001476726C5C + 5A58100000000000000000000F0F0B0F0F0F0000000000000000 + } + OnClick = bbChangePrivateKeyKeysClick + TabOrder = 3 + end + object rbChangeKeyTransferAccountToNewOwner: TRadioButton + Left = 16 + Height = 19 + Top = 68 + Width = 180 + Caption = 'Transfer account to a new owner' + TabOrder = 4 + end + end + object tsListAccount: TTabSheet + Caption = 'List Account' + ImageIndex = 3 + object lblPrice: TLabel + Left = 38 + Height = 13 + Top = 68 + Width = 50 + Alignment = taRightJustify + Caption = 'Sale price:' + ParentColor = False + end + object lblSeller: TLabel + Left = 200 + Height = 13 + Top = 68 + Width = 136 + Alignment = taRightJustify + AutoSize = False + Caption = 'Seller Account' + ParentColor = False + end + object lblNewKey: TLabel + Left = 39 + Height = 13 + Top = 91 + Width = 49 + Alignment = taRightJustify + Caption = 'Buyer Key' + ParentColor = False + end + object lblTimeLock: TLabel + Left = 41 + Height = 13 + Top = 115 + Width = 47 + Alignment = taRightJustify + Caption = 'Time-Lock' + ParentColor = False + end + object lblListAccountErrors: TLabel + Left = 16 + Height = 13 + Top = 8 + Width = 483 + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object rbListAccountForPublicSale: TRadioButton + Left = 16 + Height = 19 + Top = 26 + Width = 106 + Caption = 'List for Public Sale' + TabOrder = 0 + end + object rbListAccountForPrivateSale: TRadioButton + Left = 16 + Height = 19 + Top = 46 + Width = 113 + Caption = 'List for Private Sale' + TabOrder = 1 + end + object ebPrice: TEdit + Left = 96 + Height = 21 + Top = 65 + Width = 94 + TabOrder = 4 + TextHint = 'PASC Quantity' + end + object ebSellerAccount: TEdit + Left = 344 + Height = 21 + Top = 65 + Width = 122 + TabOrder = 5 + TextHint = 'Account Number' + end + object ebNewKey: TEdit + Left = 96 + Height = 21 + Top = 88 + Width = 370 + TabOrder = 6 + TextHint = 'BASE58 Encoded Public Key Of New Owner' + end + object ebTimeLock: TEdit + Left = 96 + Height = 21 + Top = 112 + Width = 91 + TabOrder = 7 + TextHint = 'Block number' + end + object sbSearchListerSellerAccount: TSpeedButton + Left = 470 + Height = 22 + Top = 65 + Width = 23 + Glyph.Data = { + 36030000424D3803000000000000360000002800000010000000100000000100 + 18000000000000000000120B0000120B00000000000000000000FF00FF4A667C + BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF + 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF + 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 + B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD + EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF + FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 + C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF + FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF + E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 + C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 + C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 + DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 + 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF + } + OnClick = sbSearchListerSellerAccountClick + end + object rbListAccountForAccountSwap: TRadioButton + Left = 160 + Height = 19 + Top = 26 + Width = 159 + Caption = 'List for Atomic Account Swap' + TabOrder = 2 + end + object rbListAccountForCoinSwap: TRadioButton + Left = 160 + Height = 19 + Top = 46 + Width = 141 + Caption = 'List for Atomic Coin Swap' + TabOrder = 3 + end + object sbTimeLock: TSpeedButton + Left = 192 + Height = 22 + Top = 112 + Width = 23 + Glyph.Data = { + 36030000424D3603000000000000360000002800000010000000100000000100 + 18000000000000030000120B0000120B00000000000000000000FF00FF53B6F0 + 4A95DFC6CEEDFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF1B59D3298DEA7AFDFF47A2E6C1C9ECFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF3F9BE4207EE2 + 1C74DE7AFFFF4398E1CFD5F0FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFA3B5E758CDF52B81DD1676DC72FCFF4390DFE5E9F7FF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB385EA + 57D1F7287CD90F72DB71FFFF4DA9E8ECB4F9FF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FF96A7E258D3F72883D90F5FCF6EFFFF50 + AFE8B0B7E5FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FF9EADE456D8F8277CD40E56C86DF6FF2DBEF10E85DD1777D7167EDB166D + D17C92D8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF97A7E356DAF92781D40D + 97E30ECFFC0ECCFA0ECFFB0ED0FB0ED5FF12BAF45279D1FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFB486EA56CCF31ED1FA09BCF411BFF410BFF310BFF310BE + F30FD3FD05B2EFA2B4E6FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF6486D72C + E3FD09C1F311C4F410C4F40EC8F60ECCF70BC1F30BDCFE3581D6FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF4A95DF1DE4FD0CC5F311C7F40ED1F70FC2F139C6 + F01FD6F805D1F843BBEBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4491DF1A + E8FD00C5F206CEF600C2F27AAAE5FF00FF8CC9ED64F4FF70CFEFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF6390DC44F3FE0ECFF429DAF635D8F5FF00FFFF00 + FF9DC4EA63FFFF7EBDE8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFBFCEF0AD + EBF972F8FF5DE3F65EEAF964D7F384E4F559F1FC5CE8F8B1C6EDFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FF9EB9E99DE5F77DF9FD63F2FC60F7FD66FC + FE77EBF9A7C3EBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFCDDAF39CC1EC8AC7EC85C5EB9CC8EDB3C5EDFF00FFFF00FF + } + OnClick = sbSearchListerSellerAccountClick + end + object ebHashLock: TEdit + Left = 280 + Height = 21 + Top = 112 + Width = 186 + TabOrder = 8 + end + object btnHashLock: TSpeedButton + Left = 470 + Height = 22 + Top = 112 + Width = 23 + Glyph.Data = { + 36030000424D3603000000000000360000002800000010000000100000000100 + 18000000000000030000120B0000120B00000000000000000000FF00FF53B6F0 + 4A95DFC6CEEDFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF1B59D3298DEA7AFDFF47A2E6C1C9ECFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF3F9BE4207EE2 + 1C74DE7AFFFF4398E1CFD5F0FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFA3B5E758CDF52B81DD1676DC72FCFF4390DFE5E9F7FF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB385EA + 57D1F7287CD90F72DB71FFFF4DA9E8ECB4F9FF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FF96A7E258D3F72883D90F5FCF6EFFFF50 + AFE8B0B7E5FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FF9EADE456D8F8277CD40E56C86DF6FF2DBEF10E85DD1777D7167EDB166D + D17C92D8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF97A7E356DAF92781D40D + 97E30ECFFC0ECCFA0ECFFB0ED0FB0ED5FF12BAF45279D1FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFB486EA56CCF31ED1FA09BCF411BFF410BFF310BFF310BE + F30FD3FD05B2EFA2B4E6FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF6486D72C + E3FD09C1F311C4F410C4F40EC8F60ECCF70BC1F30BDCFE3581D6FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF4A95DF1DE4FD0CC5F311C7F40ED1F70FC2F139C6 + F01FD6F805D1F843BBEBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4491DF1A + E8FD00C5F206CEF600C2F27AAAE5FF00FF8CC9ED64F4FF70CFEFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF6390DC44F3FE0ECFF429DAF635D8F5FF00FFFF00 + FF9DC4EA63FFFF7EBDE8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFBFCEF0AD + EBF972F8FF5DE3F65EEAF964D7F384E4F559F1FC5CE8F8B1C6EDFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FF9EB9E99DE5F77DF9FD63F2FC60F7FD66FC + FE77EBF9A7C3EBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFCDDAF39CC1EC8AC7EC85C5EB9CC8EDB3C5EDFF00FFFF00FF + } + OnClick = sbSearchListerSellerAccountClick + end + object lblHashLock: TLabel + Left = 222 + Height = 13 + Top = 115 + Width = 49 + Alignment = taRightJustify + Caption = 'Hash-Lock' + ParentColor = False + end + end + object tsDelistAccount: TTabSheet + Caption = 'Delist account' + ImageIndex = 3 + object lblDelistErrors: TLabel + Left = 13 + Height = 13 + Top = 10 + Width = 331 + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + end + object tsBuyAccount: TTabSheet + Caption = 'Buy account' + ImageIndex = 4 + object lblAccountToBuy: TLabel + Left = 13 + Height = 13 + Top = 32 + Width = 100 + Alignment = taRightJustify + AutoSize = False + Caption = 'Account to Buy' + ParentColor = False + end + object lblBuyAmount: TLabel + Left = 13 + Height = 13 + Top = 61 + Width = 100 + Alignment = taRightJustify + AutoSize = False + Caption = 'Amount' + ParentColor = False + end + object lblBuyAccountErrors: TLabel + Left = 13 + Height = 13 + Top = 10 + Width = 331 + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object lblBuyNewKey: TLabel + Left = 13 + Height = 13 + Top = 88 + Width = 100 + Alignment = taRightJustify + AutoSize = False + Caption = 'New private key' + ParentColor = False + end + object Label2: TLabel + Left = 204 + Height = 13 + Top = 61 + Width = 234 + Caption = 'any over-payment will remain on bought account' + Font.Color = clGray + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object ebAccountToBuy: TEdit + Left = 118 + Height = 21 + Top = 29 + Width = 76 + TabOrder = 0 + end + object ebBuyAmount: TEdit + Left = 118 + Height = 21 + Top = 58 + Width = 76 + TabOrder = 1 + end + object cbBuyNewKey: TComboBox + Left = 118 + Height = 21 + Top = 85 + Width = 294 + ItemHeight = 13 + Sorted = True + Style = csDropDownList + TabOrder = 2 + end + object bbBuyNewKey: TBitBtn + Left = 418 + Height = 23 + Top = 84 + Width = 39 + Glyph.Data = { + 36050000424D3605000000000000360400002800000010000000100000000100 + 0800000000000001000000000000000000000001000000010000FF00FF008C6B + 6C0087707000AE877C000D7FA9006F7C88006D7C94001F7ECE000E80AA001180 + A7001081AB00048CB900078DBC000B8DBA000C8DBA00088EBC001285B0007882 + 95006092BD005EA8BE000A91C1000F9DCE002087DE0011A7D10030BCDC001F89 + E00059A9DC0044BADD004ABFE00057AEF4004DB1F90049B2FF004EB7FF0057B1 + F60050B6FE0022D7FC0024D7FF0024D8FD0039D7FB0035D8FD004BC6DC0046C6 + E40048D5EE0075D3E90058E9FD006FE6FF0070E6FF0071F9FE007BFFFF008683 + 88008B8697008F989B00969594009C919000AD858000AD868500AB939500A49E + 9900B1979400B5A09F008AA5AD00CAA08C00CDAC9300C2A69A00F3BE8000C6AE + A000CFB7A100D3BBA200F4C88E00F5CB9A00F5D09C00F8D09800DAC5B700E4CC + A900EFD2A900ECD1AC00F6DAAB00F5DEB500F5E1B600F9E1B100FEEAB900FFF2 + BA00A1C6C8008DE6FA0081F8FE008CFAFD008DFCFE0097FCFD009BFBFD00B8ED + F600A7FFFF00AAFFFE00ADFFFE00B6F6FF00B1FCFD00B4FFFF00ECDDCC00E8DD + D600FFF7C600FCF5CD00FCF7D100FAF6D600FFFBD500FEFED600F7F2D900FEFF + D900FFFEDD00C6F5FF00C6FEFF00D2FFFF00FEF7E000FBFCE100FDFFE100FFFF + E400E3FEFF00F9F6F200FFFFF400F1FBFC00F5FFFE00FBFFFF00000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000605000004080408080A000000000011191A000B2A23272D + 531B080000001116201D0B552C23272E531C1509003207201D000F552C23272B + 3A3F41030112202000000F552C252938606771684236200000000B5F5D6B3B61 + 74676A67513D000000000B59181735716A676A63474B360000000F282C23396A + 6A6A6A4C404D360000000B552C2534656A654F455049360000000B552C251343 + 6247446E7336000000000B552C25263C3E4B4E483636000000000F55542F3057 + 523331020000000000000B77766D5F5C5C5C2F08000000000000001476726C5C + 5A58100000000000000000000F0F0B0F0F0F0000000000000000 + } + OnClick = bbBuyNewKeyClick + TabOrder = 3 + end + object sbSearchBuyAccount: TSpeedButton + Left = 201 + Height = 22 + Top = 29 + Width = 23 + Glyph.Data = { + 36030000424D3803000000000000360000002800000010000000100000000100 + 18000000000000000000120B0000120B00000000000000000000FF00FF4A667C + BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF + 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF + 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 + B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD + EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF + FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 + C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF + FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF + E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 + C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 + C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 + DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 + 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF + } + OnClick = sbSearchBuyAccountClick + end + end + object tsChangeInfo: TTabSheet + Caption = 'Change Info' + object lblChangeInfoErrors: TLabel + Left = 13 + Height = 13 + Top = 10 + Width = 331 + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object lblChangeName: TLabel + Left = 13 + Height = 13 + Top = 32 + Width = 100 + Alignment = taRightJustify + AutoSize = False + Caption = 'Change name' + ParentColor = False + end + object ebChangeName: TEdit + Left = 118 + Height = 21 + Top = 29 + Width = 258 + TabOrder = 0 + TextHint = 'Account name (null or 3..64 chars)' + end + object lblChangeType: TLabel + Left = 13 + Height = 13 + Top = 60 + Width = 100 + Alignment = taRightJustify + AutoSize = False + Caption = 'Change type' + ParentColor = False + end + object ebChangeType: TEdit + Left = 118 + Height = 21 + Top = 56 + Width = 76 + TabOrder = 1 + TextHint = '0..65535' + end + object lblChangeAccountData: TLabel + Left = 13 + Height = 13 + Top = 88 + Width = 100 + Alignment = taRightJustify + AutoSize = False + Caption = 'Change Data' + ParentColor = False + end + object ebChangeAccountData: TEdit + Left = 118 + Height = 21 + Top = 84 + Width = 386 + TabOrder = 2 + TextHint = 'Hexadecimal value (0..32 bytes)' + end + end + end + object lblSignerAccount: TLabel + Left = 278 + Height = 13 + Top = 188 + Width = 131 + Caption = 'Signer account (Fee payer)' + ParentColor = False + end + object ebSignerAccount: TEdit + Left = 427 + Height = 21 + Top = 184 + Width = 82 + TabOrder = 3 + end + object sbSearchSignerAccount: TSpeedButton + Left = 513 + Height = 22 + Top = 184 + Width = 23 + Glyph.Data = { + 36030000424D3803000000000000360000002800000010000000100000000100 + 18000000000000000000120B0000120B00000000000000000000FF00FF4A667C + BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF + 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF + 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 + B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD + EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF + FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 + C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF + FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF + E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 + C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 + C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 + DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 + 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF + } + OnClick = sbSearchSignerAccountClick + end + end + object tsGlobalError: TTabSheet + Caption = 'Notification' + ImageIndex = 1 + TabVisible = False + object lblGlobalErrors: TLabel + Left = 40 + Height = 156 + Top = 50 + Width = 456 + Alignment = taCenter + AutoSize = False + Caption = '????' + Font.Color = clRed + Font.Height = -21 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + WordWrap = True + end + object bbPassword: TBitBtn + Left = 191 + Height = 38 + Top = 243 + Width = 159 + Caption = 'Password' + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + Glyph.Data = { + 76060000424D7606000000000000360400002800000018000000180000000100 + 0800000000004002000000000000000000000001000000010000000000000101 + 0100020202000303030004040400050505000606060007070700080808000909 + 09000A0A0A000B0B0B000C0C0C000D0D0D000E0E0E000F0F0F00101010001111 + 1100121212001313130014141400151515001616160017171700181818001919 + 19001A1A1A001B1B1B001C1C1C001D1D1D001E1E1E001F1F1F00202020002121 + 2100222222002323230024242400252525002626260027272700282828002929 + 29002A2A2A002B2B2B002C2C2C002D2D2D002E2E2E002F2F2F00303030003131 + 3100323232003333330034343400353535003636360037373700383838003939 + 39003A3A3A003B3B3B003C3C3C003D3D3D003E3E3E003F3F3F00404040004141 + 4100424242004343430044444400454545004646460047474700484848004949 + 49004A4A4A004B4B4B004C4C4C004D4D4D004E4E4E004F4F4F00505050005151 + 5100525252005353530054545400555555005656560057575700585858005959 + 59005A5A5A005B5B5B005C5C5C005D5D5D005E5E5E00685968007C4F7C009441 + 9400B72CB700DD15DD00F506F500FD01FD00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00F008F900D517F000BD24E800A830E1007B4B + D0005065C1003774B7002B7DB100237FAC001F82AD001C83AD001785AE001687 + AF001489B100128AB300108CB6000F8EB9000D91BB000C94BF000A97C3000A99 + C5000A9AC7000B9BC8000B9CCA000C9ECC000C9FCD000DA0CE000DA1CE000EA2 + CF0011A3CF0015A4CF0018A5CF001CA7D10021A9D1002AAAD00035AACD0035AD + D00035AFD30035B1D60037B5D8003AB8DB003EB9DC0040BBDD0045BEDE004CC1 + E00055C6E3005BC8E40061CAE40066CBE3006BCCE30072D3E60078D8EB0080D8 + EE0083D9F10085DCF40088DBF5008BDBF6008EDAF70091DAF70095DCF70097E0 + F7009EE3F800A4E7F900A9E9F900AEE9F900B5EBF900B2EBF800919191919191 + 919191919191919191919191919191919191919191919191D5E7E8E7E3DED591 + 919191919191919191919191919191D6DDF7F8F8F7F6F4EEE6D5919191919191 + 919191919191D7DADEF8F8F6F7F6EDF1F8F1E69191919191919191919191DADE + DFFAFAF8F7F7E9EDF7F7F8EB91919191919191919191DDE5E3FCFCFAFAEEE0E9 + F3F7F7F7DC919191919191919191DEE7E4FEFEFCFCEDC5E0EFF7F6F6DC919191 + 919191919191DEEAE7FEFEFEFDFBC4C4F5F7F6F6DC919191919191919191E3EC + E9FEFEFEFEFEE0C3F9F7F6F6DC919191919191919191E3E9E3F8F8F1F1F8FBFB + FCFAF7F6DC919191919191919191DCEAEAE0E1E9E5E3DEE4EBF2FAF9DC919191 + 919191919191DBF3EEC5C5EFF3EBE5DDD9D6E6F5DC91919191919191919191D5 + E8EDF1FAF6F0EAE4DAD1D2DBDC919191919191919191919191DBE8F0F3F3F3E4 + D0CFE4D8DC919191919191919191919191919191D5DEE4DED2CFD4D491919191 + 91919191919191919191919191919191D2CE9191919191919191919191919191 + 91C8C89191919191D2CE919191919191919191919191919191D9D09191919191 + D2CE919191919191919191919191919191E6E39191919191D2CE919191919191 + 919191919191919191E0F3CE91919191D2CE9191919191919191919191919191 + 9191EFF6E1C9C8CDD9CC91919191919191919191919191919191C9EDF9F3F3EB + DC91919191919191919191919191919191919191CBE0E1CF9191919191919191 + 9191919191919191919191919191919191919191919191919191 + } + OnClick = bbPasswordClick + ParentFont = False + TabOrder = 0 + end + end + end + object memoAccounts: TMemo + Left = 74 + Height = 59 + Top = 12 + Width = 307 + Font.Color = clWindowText + Font.Height = -12 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + Lines.Strings = ( + '123456-11 (0.0000); 123456-11 (0.0000); ' + '123456-11 (0.0000); 123456-11 (0.0000); ' + ) + ParentFont = False + ReadOnly = True + ScrollBars = ssVertical + TabOrder = 4 + end + object ebSenderAccount: TEdit + Left = 74 + Height = 27 + Top = 12 + Width = 97 + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + OnExit = ebSenderAccountExit + OnKeyPress = ebSenderAccountKeyPress + ParentFont = False + TabOrder = 0 + Text = 'ebSenderAccount' + end + object ActionList: TActionList + Left = 140 + Top = 350 + object actExecute: TAction + Caption = 'Execute (F12)' + OnExecute = actExecuteExecute + ShortCut = 123 + end + end +end diff --git a/src/gui-experimental/UFRMOperation.lfm b/src/gui-experimental/UFRMOperation.lfm index afeabea42..1fed36580 100644 --- a/src/gui-experimental/UFRMOperation.lfm +++ b/src/gui-experimental/UFRMOperation.lfm @@ -1,22 +1,23 @@ object FRMOperation: TFRMOperation - Left = 772 - Height = 493 - Top = 245 - Width = 608 - ActiveControl = ebSenderAccount + Left = 564 + Height = 650 + Top = 267 + Width = 760 BorderIcons = [biSystemMenu] BorderStyle = bsSingle Caption = 'New Operation' - ClientHeight = 493 - ClientWidth = 608 + ClientHeight = 650 + ClientWidth = 760 Color = clBtnFace Font.Color = clWindowText Font.Height = -11 Font.Name = 'Tahoma' OnCreate = FormCreate OnDestroy = FormDestroy + OnResize = FormResize + OnShow = FormShow Position = poOwnerFormCenter - LCLVersion = '2.1.0.0' + LCLVersion = '2.0.10.0' object lblAccountCaption: TLabel Left = 25 Height = 13 @@ -57,7 +58,7 @@ object FRMOperation: TFRMOperation object bbExecute: TBitBtn Left = 300 Height = 31 - Top = 445 + Top = 464 Width = 130 Caption = 'Execute (F12)' Glyph.Data = { @@ -85,7 +86,7 @@ object FRMOperation: TFRMOperation object bbCancel: TBitBtn Left = 465 Height = 31 - Top = 445 + Top = 464 Width = 116 Cancel = True Caption = 'Cancel' @@ -95,15 +96,15 @@ object FRMOperation: TFRMOperation end object PageControlLocked: TPageControl Left = 25 - Height = 367 + Height = 383 Top = 72 Width = 556 - ActivePage = tsGlobalError - TabIndex = 1 + ActivePage = tsOperation + TabIndex = 0 TabOrder = 1 object tsOperation: TTabSheet Caption = 'Operation' - ClientHeight = 341 + ClientHeight = 357 ClientWidth = 548 TabVisible = False object lblFee: TLabel @@ -116,11 +117,11 @@ object FRMOperation: TFRMOperation end object gbPayload: TGroupBox Left = 13 - Height = 129 + Height = 145 Top = 207 Width = 521 Caption = ' Payload: ' - ClientHeight = 111 + ClientHeight = 127 ClientWidth = 517 TabOrder = 2 object lblEncryptPassword: TLabel @@ -141,9 +142,9 @@ object FRMOperation: TFRMOperation end object lblEncryptionErrors: TLabel Left = 255 - Height = 13 + Height = 33 Top = 87 - Width = 246 + Width = 153 AutoSize = False Caption = 'Errors detected' Font.Color = clRed @@ -151,6 +152,7 @@ object FRMOperation: TFRMOperation Font.Name = 'Tahoma' ParentColor = False ParentFont = False + WordWrap = True end object lblPayloadLength: TLabel Left = 352 @@ -223,6 +225,20 @@ object FRMOperation: TFRMOperation OnClick = memoPayloadClick TabOrder = 0 end + object cbPayloadDataInputType: TComboBox + Left = 412 + Height = 21 + Top = 88 + Width = 89 + ItemHeight = 13 + Items.Strings = ( + 'As String'#11 + 'As Hexadecimal'#11 + 'As Base58' + ) + Style = csDropDownList + TabOrder = 6 + end end object ebFee: TEdit Left = 90 @@ -232,12 +248,12 @@ object FRMOperation: TFRMOperation TabOrder = 1 end object PageControlOpType: TPageControl - Left = 15 + Left = 16 Height = 167 Top = 11 Width = 521 - ActivePage = tsChangeInfo - TabIndex = 5 + ActivePage = tsTransaction + TabIndex = 0 TabOrder = 0 OnChange = PageControlOpTypeChange object tsTransaction: TTabSheet @@ -277,7 +293,7 @@ object FRMOperation: TFRMOperation Left = 115 Height = 21 Top = 29 - Width = 85 + Width = 357 TabOrder = 0 end object ebAmount: TEdit @@ -288,9 +304,9 @@ object FRMOperation: TFRMOperation TabOrder = 1 end object sbSearchDestinationAccount: TSpeedButton - Left = 208 + Left = 480 Height = 22 - Top = 29 + Top = 28 Width = 23 Glyph.Data = { 36030000424D3803000000000000360000002800000010000000100000000100 @@ -322,294 +338,401 @@ object FRMOperation: TFRMOperation } OnClick = sbSearchDestinationAccountClick end + object memoEPASA: TMemo + Left = 13 + Height = 48 + Top = 88 + Width = 492 + BorderStyle = bsNone + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + OnChange = memoPayloadClick + OnClick = memoPayloadClick + ParentFont = False + ReadOnly = True + TabOrder = 2 + TabStop = False + WantReturns = False + end end object tsChangePrivateKey: TTabSheet Caption = 'Change key' ClientHeight = 141 ClientWidth = 513 ImageIndex = 1 - object gbChangeKey: TGroupBox - Left = 7 - Height = 130 - Top = 6 - Width = 488 - Caption = ' Change type: ' - ClientHeight = 112 - ClientWidth = 484 + object lblNewPrivateKey: TLabel + Left = 61 + Height = 13 + Top = 41 + Width = 78 + Caption = 'New private key' + ParentColor = False + end + object lblNewOwnerPublicKey: TLabel + Left = 28 + Height = 13 + Top = 91 + Width = 109 + Caption = 'New owners public key' + ParentColor = False + end + object lblNewOwnerErrors: TLabel + Left = 177 + Height = 13 + Top = 69 + Width = 302 + Alignment = taRightJustify + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object lblChangeKeyErrors: TLabel + Left = 149 + Height = 13 + Top = 16 + Width = 331 + Alignment = taRightJustify + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object rbChangeKeyWithAnother: TRadioButton + Left = 16 + Height = 19 + Top = 21 + Width = 114 + Caption = 'Change private key' TabOrder = 0 - object lblNewPrivateKey: TLabel - Left = 57 - Height = 13 - Top = 33 - Width = 78 - Caption = 'New private key' - ParentColor = False - end - object lblNewOwnerPublicKey: TLabel - Left = 24 - Height = 13 - Top = 83 - Width = 109 - Caption = 'New owners public key' - ParentColor = False - end - object lblNewOwnerErrors: TLabel - Left = 173 - Height = 13 - Top = 61 - Width = 302 - Alignment = taRightJustify - AutoSize = False - Caption = 'Errors detected' - Font.Color = clRed - Font.Height = -11 - Font.Name = 'Tahoma' - ParentColor = False - ParentFont = False - end - object lblChangeKeyErrors: TLabel - Left = 145 - Height = 13 - Top = 8 - Width = 331 - Alignment = taRightJustify - AutoSize = False - Caption = 'Errors detected' - Font.Color = clRed - Font.Height = -11 - Font.Name = 'Tahoma' - ParentColor = False - ParentFont = False - end - object rbChangeKeyWithAnother: TRadioButton - Left = 12 - Height = 19 - Top = 13 - Width = 114 - Caption = 'Change private key' - TabOrder = 0 - end - object cbNewPrivateKey: TComboBox - Left = 145 - Height = 21 - Top = 30 - Width = 294 - ItemHeight = 13 - Sorted = True - Style = csDropDownList - TabOrder = 1 - end - object ebNewPublicKey: TEdit - Left = 144 - Height = 21 - Top = 80 - Width = 331 - TabOrder = 4 - end - object bbChangePrivateKeyKeys: TBitBtn - Left = 445 - Height = 22 - Top = 29 - Width = 31 - Glyph.Data = { - 36050000424D3605000000000000360400002800000010000000100000000100 - 0800000000000001000000000000000000000001000000010000FF00FF008C6B - 6C0087707000AE877C000D7FA9006F7C88006D7C94001F7ECE000E80AA001180 - A7001081AB00048CB900078DBC000B8DBA000C8DBA00088EBC001285B0007882 - 95006092BD005EA8BE000A91C1000F9DCE002087DE0011A7D10030BCDC001F89 - E00059A9DC0044BADD004ABFE00057AEF4004DB1F90049B2FF004EB7FF0057B1 - F60050B6FE0022D7FC0024D7FF0024D8FD0039D7FB0035D8FD004BC6DC0046C6 - E40048D5EE0075D3E90058E9FD006FE6FF0070E6FF0071F9FE007BFFFF008683 - 88008B8697008F989B00969594009C919000AD858000AD868500AB939500A49E - 9900B1979400B5A09F008AA5AD00CAA08C00CDAC9300C2A69A00F3BE8000C6AE - A000CFB7A100D3BBA200F4C88E00F5CB9A00F5D09C00F8D09800DAC5B700E4CC - A900EFD2A900ECD1AC00F6DAAB00F5DEB500F5E1B600F9E1B100FEEAB900FFF2 - BA00A1C6C8008DE6FA0081F8FE008CFAFD008DFCFE0097FCFD009BFBFD00B8ED - F600A7FFFF00AAFFFE00ADFFFE00B6F6FF00B1FCFD00B4FFFF00ECDDCC00E8DD - D600FFF7C600FCF5CD00FCF7D100FAF6D600FFFBD500FEFED600F7F2D900FEFF - D900FFFEDD00C6F5FF00C6FEFF00D2FFFF00FEF7E000FBFCE100FDFFE100FFFF - E400E3FEFF00F9F6F200FFFFF400F1FBFC00F5FFFE00FBFFFF00000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 00000000000000000605000004080408080A000000000011191A000B2A23272D - 531B080000001116201D0B552C23272E531C1509003207201D000F552C23272B - 3A3F41030112202000000F552C252938606771684236200000000B5F5D6B3B61 - 74676A67513D000000000B59181735716A676A63474B360000000F282C23396A - 6A6A6A4C404D360000000B552C2534656A654F455049360000000B552C251343 - 6247446E7336000000000B552C25263C3E4B4E483636000000000F55542F3057 - 523331020000000000000B77766D5F5C5C5C2F08000000000000001476726C5C - 5A58100000000000000000000F0F0B0F0F0F0000000000000000 - } - OnClick = bbChangePrivateKeyKeysClick - TabOrder = 2 - end - object rbChangeKeyTransferAccountToNewOwner: TRadioButton - Left = 12 - Height = 19 - Top = 60 - Width = 180 - Caption = 'Transfer account to a new owner' - TabOrder = 3 - end + end + object cbNewPrivateKey: TComboBox + Left = 149 + Height = 21 + Top = 38 + Width = 294 + ItemHeight = 13 + Sorted = True + Style = csDropDownList + TabOrder = 1 + end + object ebNewPublicKey: TEdit + Left = 148 + Height = 21 + Top = 88 + Width = 331 + TabOrder = 2 + end + object bbChangePrivateKeyKeys: TBitBtn + Left = 449 + Height = 22 + Top = 37 + Width = 31 + Glyph.Data = { + 36050000424D3605000000000000360400002800000010000000100000000100 + 0800000000000001000000000000000000000001000000010000FF00FF008C6B + 6C0087707000AE877C000D7FA9006F7C88006D7C94001F7ECE000E80AA001180 + A7001081AB00048CB900078DBC000B8DBA000C8DBA00088EBC001285B0007882 + 95006092BD005EA8BE000A91C1000F9DCE002087DE0011A7D10030BCDC001F89 + E00059A9DC0044BADD004ABFE00057AEF4004DB1F90049B2FF004EB7FF0057B1 + F60050B6FE0022D7FC0024D7FF0024D8FD0039D7FB0035D8FD004BC6DC0046C6 + E40048D5EE0075D3E90058E9FD006FE6FF0070E6FF0071F9FE007BFFFF008683 + 88008B8697008F989B00969594009C919000AD858000AD868500AB939500A49E + 9900B1979400B5A09F008AA5AD00CAA08C00CDAC9300C2A69A00F3BE8000C6AE + A000CFB7A100D3BBA200F4C88E00F5CB9A00F5D09C00F8D09800DAC5B700E4CC + A900EFD2A900ECD1AC00F6DAAB00F5DEB500F5E1B600F9E1B100FEEAB900FFF2 + BA00A1C6C8008DE6FA0081F8FE008CFAFD008DFCFE0097FCFD009BFBFD00B8ED + F600A7FFFF00AAFFFE00ADFFFE00B6F6FF00B1FCFD00B4FFFF00ECDDCC00E8DD + D600FFF7C600FCF5CD00FCF7D100FAF6D600FFFBD500FEFED600F7F2D900FEFF + D900FFFEDD00C6F5FF00C6FEFF00D2FFFF00FEF7E000FBFCE100FDFFE100FFFF + E400E3FEFF00F9F6F200FFFFF400F1FBFC00F5FFFE00FBFFFF00000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000605000004080408080A000000000011191A000B2A23272D + 531B080000001116201D0B552C23272E531C1509003207201D000F552C23272B + 3A3F41030112202000000F552C252938606771684236200000000B5F5D6B3B61 + 74676A67513D000000000B59181735716A676A63474B360000000F282C23396A + 6A6A6A4C404D360000000B552C2534656A654F455049360000000B552C251343 + 6247446E7336000000000B552C25263C3E4B4E483636000000000F55542F3057 + 523331020000000000000B77766D5F5C5C5C2F08000000000000001476726C5C + 5A58100000000000000000000F0F0B0F0F0F0000000000000000 + } + OnClick = bbChangePrivateKeyKeysClick + TabOrder = 3 + end + object rbChangeKeyTransferAccountToNewOwner: TRadioButton + Left = 16 + Height = 19 + Top = 68 + Width = 180 + Caption = 'Transfer account to a new owner' + TabOrder = 4 end end - object tsListForSale: TTabSheet - Caption = 'List account for sale' + object tsListAccount: TTabSheet + Caption = 'List Account' ClientHeight = 141 ClientWidth = 513 ImageIndex = 3 - object gbSaleType: TGroupBox - Left = 8 - Height = 135 - Top = 5 - Width = 499 - Caption = ' Sale type: ' - ClientHeight = 117 - ClientWidth = 495 + object lblPrice: TLabel + Left = 38 + Height = 13 + Top = 68 + Width = 50 + Alignment = taRightJustify + Caption = 'Sale price:' + ParentColor = False + end + object lblSeller: TLabel + Left = 200 + Height = 13 + Top = 68 + Width = 136 + Alignment = taRightJustify + AutoSize = False + Caption = 'Seller Account' + ParentColor = False + end + object lblNewKey: TLabel + Left = 39 + Height = 13 + Top = 91 + Width = 49 + Alignment = taRightJustify + Caption = 'Buyer Key' + ParentColor = False + end + object lblTimeLock: TLabel + Left = 41 + Height = 13 + Top = 115 + Width = 47 + Alignment = taRightJustify + Caption = 'Time-Lock' + ParentColor = False + end + object lblListAccountErrors: TLabel + Left = 16 + Height = 13 + Top = 8 + Width = 483 + AutoSize = False + Caption = 'Errors detected' + Font.Color = clRed + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object rbListAccountForPublicSale: TRadioButton + Left = 16 + Height = 19 + Top = 26 + Width = 106 + Caption = 'List for Public Sale' TabOrder = 0 - object Label1: TLabel - Left = 81 - Height = 13 - Top = 44 - Width = 50 - Caption = 'Sale price:' - ParentColor = False - end - object Label3: TLabel - Left = 229 - Height = 13 - Top = 44 - Width = 142 - Caption = 'Seller account (where to pay)' - ParentColor = False - end - object lblSaleNewOwnerPublicKey: TLabel - Left = 20 - Height = 13 - Top = 67 - Width = 109 - Caption = 'New owners public key' - ParentColor = False - end - object lblSaleLockedUntilBlock: TLabel - Left = 44 - Height = 13 - Top = 91 - Width = 87 - Caption = 'Locked until block:' - ParentColor = False - end - object lblListAccountErrors: TLabel - Left = 145 - Height = 13 - Top = 0 - Width = 331 - Alignment = taRightJustify - AutoSize = False - Caption = 'Errors detected' - Font.Color = clRed - Font.Height = -11 - Font.Name = 'Tahoma' - ParentColor = False - ParentFont = False - end - object rbListAccountForPublicSale: TRadioButton - Left = 10 - Height = 19 - Top = 2 - Width = 146 - Caption = 'List account for public sale' - TabOrder = 0 - end - object rbListAccountForPrivateSale: TRadioButton - Left = 10 - Height = 19 - Top = 22 - Width = 153 - Caption = 'List account for private sale' - TabOrder = 1 - end - object ebSalePrice: TEdit - Left = 137 - Height = 21 - Top = 41 - Width = 86 - TabOrder = 2 - end - object ebSellerAccount: TEdit - Left = 378 - Height = 21 - Top = 41 - Width = 82 - TabOrder = 3 - end - object ebSaleNewOwnerPublicKey: TEdit - Left = 137 - Height = 21 - Top = 64 - Width = 323 - TabOrder = 4 - end - object ebSaleLockedUntilBlock: TEdit - Left = 137 - Height = 21 - Top = 88 - Width = 86 - TabOrder = 5 - end - object sbSearchListerSellerAccount: TSpeedButton - Left = 464 - Height = 22 - Top = 41 - Width = 23 - Glyph.Data = { - 36030000424D3803000000000000360000002800000010000000100000000100 - 18000000000000000000120B0000120B00000000000000000000FF00FF4A667C - BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 - FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF - 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF - 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 - FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF - 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF - 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 - FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF - 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF - FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 - B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD - EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF - FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF - FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 - C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF - FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF - E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 - C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF - FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 - C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 - DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF - FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 - 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF - 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF - } - OnClick = sbSearchListerSellerAccountClick - end + end + object rbListAccountForPrivateSale: TRadioButton + Left = 16 + Height = 19 + Top = 46 + Width = 113 + Caption = 'List for Private Sale' + TabOrder = 1 + end + object ebPrice: TEdit + Left = 96 + Height = 21 + Top = 65 + Width = 94 + TabOrder = 4 + TextHint = 'PASC Quantity' + end + object ebSellerAccount: TEdit + Left = 344 + Height = 21 + Top = 65 + Width = 122 + TabOrder = 5 + TextHint = 'Account Number' + end + object ebNewKey: TEdit + Left = 96 + Height = 21 + Top = 88 + Width = 370 + TabOrder = 6 + TextHint = 'BASE58 Encoded Public Key Of New Owner' + end + object ebTimeLock: TEdit + Left = 96 + Height = 21 + Top = 112 + Width = 91 + TabOrder = 7 + TextHint = 'Block number' + end + object sbSearchListerSellerAccount: TSpeedButton + Left = 470 + Height = 22 + Top = 65 + Width = 23 + Glyph.Data = { + 36030000424D3803000000000000360000002800000010000000100000000100 + 18000000000000000000120B0000120B00000000000000000000FF00FF4A667C + BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF + 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF + 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 + B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD + EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF + FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 + C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF + FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF + E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 + C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 + C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 + DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 + 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF + } + OnClick = sbSearchListerSellerAccountClick + end + object rbListAccountForAccountSwap: TRadioButton + Left = 160 + Height = 19 + Top = 26 + Width = 159 + Caption = 'List for Atomic Account Swap' + TabOrder = 2 + end + object rbListAccountForCoinSwap: TRadioButton + Left = 160 + Height = 19 + Top = 46 + Width = 141 + Caption = 'List for Atomic Coin Swap' + TabOrder = 3 + end + object sbTimeLock: TSpeedButton + Left = 192 + Height = 22 + Top = 112 + Width = 23 + Glyph.Data = { + 36030000424D3603000000000000360000002800000010000000100000000100 + 18000000000000030000120B0000120B00000000000000000000FF00FF53B6F0 + 4A95DFC6CEEDFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF1B59D3298DEA7AFDFF47A2E6C1C9ECFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF3F9BE4207EE2 + 1C74DE7AFFFF4398E1CFD5F0FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFA3B5E758CDF52B81DD1676DC72FCFF4390DFE5E9F7FF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB385EA + 57D1F7287CD90F72DB71FFFF4DA9E8ECB4F9FF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FF96A7E258D3F72883D90F5FCF6EFFFF50 + AFE8B0B7E5FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FF9EADE456D8F8277CD40E56C86DF6FF2DBEF10E85DD1777D7167EDB166D + D17C92D8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF97A7E356DAF92781D40D + 97E30ECFFC0ECCFA0ECFFB0ED0FB0ED5FF12BAF45279D1FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFB486EA56CCF31ED1FA09BCF411BFF410BFF310BFF310BE + F30FD3FD05B2EFA2B4E6FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF6486D72C + E3FD09C1F311C4F410C4F40EC8F60ECCF70BC1F30BDCFE3581D6FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF4A95DF1DE4FD0CC5F311C7F40ED1F70FC2F139C6 + F01FD6F805D1F843BBEBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4491DF1A + E8FD00C5F206CEF600C2F27AAAE5FF00FF8CC9ED64F4FF70CFEFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF6390DC44F3FE0ECFF429DAF635D8F5FF00FFFF00 + FF9DC4EA63FFFF7EBDE8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFBFCEF0AD + EBF972F8FF5DE3F65EEAF964D7F384E4F559F1FC5CE8F8B1C6EDFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FF9EB9E99DE5F77DF9FD63F2FC60F7FD66FC + FE77EBF9A7C3EBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFCDDAF39CC1EC8AC7EC85C5EB9CC8EDB3C5EDFF00FFFF00FF + } + OnClick = sbSearchListerSellerAccountClick + end + object ebHashLock: TEdit + Left = 280 + Height = 21 + Top = 112 + Width = 186 + TabOrder = 8 + end + object btnHashLock: TSpeedButton + Left = 470 + Height = 22 + Top = 112 + Width = 23 + Glyph.Data = { + 36030000424D3603000000000000360000002800000010000000100000000100 + 18000000000000030000120B0000120B00000000000000000000FF00FF53B6F0 + 4A95DFC6CEEDFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF1B59D3298DEA7AFDFF47A2E6C1C9ECFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF3F9BE4207EE2 + 1C74DE7AFFFF4398E1CFD5F0FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFA3B5E758CDF52B81DD1676DC72FCFF4390DFE5E9F7FF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB385EA + 57D1F7287CD90F72DB71FFFF4DA9E8ECB4F9FF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FF96A7E258D3F72883D90F5FCF6EFFFF50 + AFE8B0B7E5FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FF9EADE456D8F8277CD40E56C86DF6FF2DBEF10E85DD1777D7167EDB166D + D17C92D8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF97A7E356DAF92781D40D + 97E30ECFFC0ECCFA0ECFFB0ED0FB0ED5FF12BAF45279D1FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFB486EA56CCF31ED1FA09BCF411BFF410BFF310BFF310BE + F30FD3FD05B2EFA2B4E6FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF6486D72C + E3FD09C1F311C4F410C4F40EC8F60ECCF70BC1F30BDCFE3581D6FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF4A95DF1DE4FD0CC5F311C7F40ED1F70FC2F139C6 + F01FD6F805D1F843BBEBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4491DF1A + E8FD00C5F206CEF600C2F27AAAE5FF00FF8CC9ED64F4FF70CFEFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FF6390DC44F3FE0ECFF429DAF635D8F5FF00FFFF00 + FF9DC4EA63FFFF7EBDE8FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFBFCEF0AD + EBF972F8FF5DE3F65EEAF964D7F384E4F559F1FC5CE8F8B1C6EDFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FF9EB9E99DE5F77DF9FD63F2FC60F7FD66FC + FE77EBF9A7C3EBFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFCDDAF39CC1EC8AC7EC85C5EB9CC8EDB3C5EDFF00FFFF00FF + } + OnClick = sbSearchListerSellerAccountClick + end + object lblHashLock: TLabel + Left = 222 + Height = 13 + Top = 115 + Width = 49 + Alignment = taRightJustify + Caption = 'Hash-Lock' + ParentColor = False end end - object tsDelist: TTabSheet + object tsDelistAccount: TTabSheet Caption = 'Delist account' ClientHeight = 141 ClientWidth = 513 @@ -680,8 +803,8 @@ object FRMOperation: TFRMOperation Left = 204 Height = 13 Top = 61 - Width = 231 - Caption = 'excessive amount will remain on bought account' + Width = 234 + Caption = 'any over-payment will remain on bought account' Font.Color = clGray Font.Height = -11 Font.Name = 'Tahoma' @@ -833,6 +956,7 @@ object FRMOperation: TFRMOperation Top = 29 Width = 258 TabOrder = 0 + TextHint = 'Account name (null or 3..64 chars)' end object lblChangeType: TLabel Left = 13 @@ -850,11 +974,30 @@ object FRMOperation: TFRMOperation Top = 56 Width = 76 TabOrder = 1 + TextHint = '0..65535' + end + object lblChangeAccountData: TLabel + Left = 13 + Height = 13 + Top = 88 + Width = 100 + Alignment = taRightJustify + AutoSize = False + Caption = 'Change Data' + ParentColor = False + end + object ebChangeAccountData: TEdit + Left = 118 + Height = 21 + Top = 84 + Width = 386 + TabOrder = 2 + TextHint = 'Hexadecimal value (0..32 bytes)' end end end object lblSignerAccount: TLabel - Left = 176 + Left = 278 Height = 13 Top = 188 Width = 131 @@ -862,14 +1005,14 @@ object FRMOperation: TFRMOperation ParentColor = False end object ebSignerAccount: TEdit - Left = 325 + Left = 427 Height = 21 Top = 184 Width = 82 TabOrder = 3 end object sbSearchSignerAccount: TSpeedButton - Left = 411 + Left = 513 Height = 22 Top = 184 Width = 23 @@ -906,7 +1049,7 @@ object FRMOperation: TFRMOperation end object tsGlobalError: TTabSheet Caption = 'Notification' - ClientHeight = 341 + ClientHeight = 357 ClientWidth = 548 ImageIndex = 1 TabVisible = False @@ -1030,8 +1173,8 @@ object FRMOperation: TFRMOperation Text = 'ebSenderAccount' end object ActionList: TActionList - left = 140 - top = 350 + Left = 140 + Top = 350 object actExecute: TAction Caption = 'Execute (F12)' OnExecute = actExecuteExecute diff --git a/src/gui-experimental/UFRMOperation.pas b/src/gui-experimental/UFRMOperation.pas index 7b19b8fdd..253802d8f 100644 --- a/src/gui-experimental/UFRMOperation.pas +++ b/src/gui-experimental/UFRMOperation.pas @@ -16,18 +16,34 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } -{$mode delphi} +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} -interface +{ Prepares and executes transactions, key changes, account sales and + account-information changes. Validates destination, fee-payer and payload + choices before submitting operations. } + +{ Public types in this unit + + Type | Purpose + --------------+------------------------------------------------------------------------- + TFRMOperation | Prepares, validates and submits advanced wallet operations. +} -{$I ..\config.inc} +interface uses +{$IFnDEF FPC} + Windows, + System.Actions, +{$ELSE} LCLIntf, LCLType, LMessages, +{$ENDIF} Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, - Dialogs, StdCtrls, UCommon.UI, - UNode, UWallet, UCrypto, Buttons, UBlockChain, UBaseTypes, - UAccounts, UFRMAccountSelect, ActnList, ComCtrls, Types, UCommon, UPCOrderedLists, UPCDataTypes; + Dialogs, StdCtrls, UNode, UWallet, UCrypto, Buttons, UBlockChain, + UAccounts, UFRMAccountSelect, ActnList, ComCtrls, Types, UFRMMemoText, + UPCEncryption, UBaseTypes, UPCDataTypes, UPCOrderedLists, UEPasa, UEncoding; Const CM_PC_WalletKeysChanged = WM_USER + 1; @@ -36,7 +52,7 @@ interface { TFRMOperation } - TFRMOperation = class(TApplicationForm) + TFRMOperation = class(TForm) ebChangeName: TEdit; ebChangeType: TEdit; ebSignerAccount: TEdit; @@ -52,7 +68,6 @@ TFRMOperation = class(TApplicationForm) lblChangeInfoErrors: TLabel; PageControlLocked: TPageControl; sbSearchBuyAccount: TSpeedButton; - sbSearchListerSellerAccount: TSpeedButton; sbSearchDestinationAccount: TSpeedButton; sbSearchSignerAccount: TSpeedButton; tsChangeInfo: TTabSheet; @@ -85,31 +100,9 @@ TFRMOperation = class(TApplicationForm) ebDestAccount: TEdit; ebAmount: TEdit; tsChangePrivateKey: TTabSheet; - gbChangeKey: TGroupBox; - lblNewPrivateKey: TLabel; - lblNewOwnerPublicKey: TLabel; - lblNewOwnerErrors: TLabel; - lblChangeKeyErrors: TLabel; - rbChangeKeyWithAnother: TRadioButton; - cbNewPrivateKey: TComboBox; - ebNewPublicKey: TEdit; - bbChangePrivateKeyKeys: TBitBtn; - rbChangeKeyTransferAccountToNewOwner: TRadioButton; - tsListForSale: TTabSheet; - gbSaleType: TGroupBox; - Label1: TLabel; - Label3: TLabel; - lblSaleNewOwnerPublicKey: TLabel; - lblSaleLockedUntilBlock: TLabel; - rbListAccountForPublicSale: TRadioButton; - rbListAccountForPrivateSale: TRadioButton; - ebSalePrice: TEdit; - ebSellerAccount: TEdit; - ebSaleNewOwnerPublicKey: TEdit; - ebSaleLockedUntilBlock: TEdit; - tsDelist: TTabSheet; + tsListAccount: TTabSheet; + tsDelistAccount: TTabSheet; tsBuyAccount: TTabSheet; - lblListAccountErrors: TLabel; lblAccountToBuy: TLabel; ebAccountToBuy: TEdit; lblBuyAmount: TLabel; @@ -120,9 +113,42 @@ TFRMOperation = class(TApplicationForm) bbBuyNewKey: TBitBtn; Label2: TLabel; lblDelistErrors: TLabel; + lblListAccountErrors: TLabel; + rbListAccountForPublicSale: TRadioButton; + rbListAccountForPrivateSale: TRadioButton; + rbListAccountForAccountSwap: TRadioButton; + rbListAccountForCoinSwap: TRadioButton; + lblPrice: TLabel; + ebPrice: TEdit; + lblSeller: TLabel; + ebSellerAccount: TEdit; + sbSearchListerSellerAccount: TSpeedButton; + ebNewKey: TEdit; + lblNewKey: TLabel; + lblTimeLock: TLabel; + ebTimeLock: TEdit; + rbChangeKeyWithAnother: TRadioButton; + lblNewPrivateKey: TLabel; + cbNewPrivateKey: TComboBox; + lblChangeKeyErrors: TLabel; + bbChangePrivateKeyKeys: TBitBtn; + lblNewOwnerErrors: TLabel; + rbChangeKeyTransferAccountToNewOwner: TRadioButton; + lblNewOwnerPublicKey: TLabel; + ebNewPublicKey: TEdit; + lblHashLock: TLabel; + ebHashLock: TEdit; + btnHashLock: TSpeedButton; + sbTimeLock: TSpeedButton; + lblChangeAccountData: TLabel; + ebChangeAccountData: TEdit; + cbPayloadDataInputType: TComboBox; + memoEPASA: TMemo; procedure ebNewPublicKeyExit(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); + procedure FormResize(Sender: TObject); + procedure FormShow(Sender: TObject); procedure memoPayloadClick(Sender: TObject); procedure ebEncryptPasswordChange(Sender: TObject); procedure bbChangePrivateKeyKeysClick(Sender: TObject); @@ -135,41 +161,48 @@ TFRMOperation = class(TApplicationForm) procedure sbSearchDestinationAccountClick(Sender: TObject); procedure sbSearchListerSellerAccountClick(Sender: TObject); procedure sbSearchSignerAccountClick(Sender: TObject); + procedure sbHashLockClick(Sender: TObject); procedure updateInfoClick(Sender: TObject); procedure bbBuyNewKeyClick(Sender: TObject); procedure ebAccountNumberExit(Sender: TObject); procedure ebCurrencyExit(Sender: TObject); + procedure sbTimeLockClick(Sender: TObject); private FNode : TNode; + FExplicitNode: Boolean; FWalletKeys: TWalletKeys; FDefaultFee: Int64; -// FEncodedPayload : TRawBytes; - FOperationPayload : TOperationPayload; + FEncodedPayload : TOperationPayload; FDisabled : Boolean; + FUpdating : Boolean; + FLayoutReady, FArrangingLayout: Boolean; FSenderAccounts: TOrderedCardinalList; // TODO: TOrderedCardinalList should be replaced with a "TCardinalList" since signer account should be processed last procedure SetWalletKeys(const Value: TWalletKeys); Procedure UpdateWalletKeys; { Private declarations } Procedure UpdateAccountsInfo; - Function UpdateFee(var Fee : Int64; errors : AnsiString) : Boolean; - Function UpdateOperationOptions(var errors : AnsiString) : Boolean; - Function UpdatePayload(Const SenderAccount : TAccount; var errors : AnsiString) : Boolean; - Function UpdateOpTransaction(Const SenderAccount : TAccount; var DestAccount : TAccount; var amount : Int64; var errors : AnsiString) : Boolean; - Function UpdateOpChangeKey(Const TargetAccount : TAccount; var SignerAccount : TAccount; var NewPublicKey : TAccountKey; var errors : AnsiString) : Boolean; - Function UpdateOpListForSale(Const TargetAccount : TAccount; var SalePrice : Int64; var SellerAccount,SignerAccount : TAccount; var NewOwnerPublicKey : TAccountKey; var LockedUntilBlock : Cardinal; var errors : AnsiString) : Boolean; - Function UpdateOpDelist(Const TargetAccount : TAccount; var SignerAccount : TAccount; var errors : AnsiString) : Boolean; - Function UpdateOpBuyAccount(Const SenderAccount : TAccount; var AccountToBuy : TAccount; var amount : Int64; var NewPublicKey : TAccountKey; var errors : AnsiString) : Boolean; - Function UpdateOpChangeInfo(Const TargetAccount : TAccount; var SignerAccount : TAccount; var changeName : Boolean; var newName : TRawBytes; var changeType : Boolean; var newType : Word; var errors : AnsiString) : Boolean; + Function UpdateFee(var Fee : Int64; errors : String) : Boolean; + Function UpdateOperationOptions(var errors : String) : Boolean; + Function UpdatePayload(Const ASenderAccount : TAccount; var AErrors : String) : Boolean; + Function UpdateOpTransaction(const ASenderAccount: TAccount; out ATargetEPASA : TEPasa; out ATargetAccount : TAccount; out AResolvedTargetKey : TAccountKey; out ATargetRequiresPurchase : Boolean; out AAmount: Int64; out AErrors: String) : Boolean; + Function UpdateOpChangeKey(Const TargetAccount : TAccount; var SignerAccount : TAccount; var NewPublicKey : TAccountKey; var errors : String) : Boolean; + Function UpdateOpListAccount(Const TargetAccount : TAccount; var SalePrice : Int64; var SellerAccount,SignerAccount : TAccount; var NewOwnerPublicKey : TAccountKey; var LockedUntilBlock : Cardinal; var HashLock : T32Bytes; var errors : String) : Boolean; + Function UpdateOpDelist(Const TargetAccount : TAccount; var SignerAccount : TAccount; var errors : String) : Boolean; + Function UpdateOpBuyAccount(Const SenderAccount : TAccount; var AccountToBuy : TAccount; var amount : Int64; var NewPublicKey : TAccountKey; var ARecipientSigned : Boolean; var errors : String) : Boolean; + Function UpdateOpChangeInfo(Const TargetAccount : TAccount; var SignerAccount : TAccount; var changeName : Boolean; var newName : TRawBytes; var changeType : Boolean; var newType : Word; var AChangeData : Boolean; var ANewData : TRawBytes; var errors : String) : Boolean; procedure SetDefaultFee(const Value: Int64); Procedure OnSenderAccountsChanged(Sender : TObject); procedure OnWalletKeysChanged(Sender : TObject); procedure CM_WalletChanged(var Msg: TMessage); message CM_PC_WalletKeysChanged; Function GetDefaultSenderAccount : TAccount; procedure ebAccountKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); + function CaptureEPasa(const AEPasaTxt : String; out AEPasa : TEPasa) : Boolean; protected procedure searchAccount(editBox : TCustomEdit); public { Public declarations } + constructor Create(AOwner: TComponent); overload; override; + constructor Create(AOwner: TComponent; ANode: TNode); reintroduce; overload; Property SenderAccounts : TOrderedCardinalList read FSenderAccounts; Property WalletKeys : TWalletKeys read FWalletKeys write SetWalletKeys; Property DefaultFee : Int64 read FDefaultFee write SetDefaultFee; @@ -178,192 +211,503 @@ TFRMOperation = class(TApplicationForm) implementation uses - UPCEncryption, - UCoreUtils, UConst, UOpTransaction, UFRMWalletKeys, UUserInterface; + {$IFDEF USE_GNUGETTEXT}gnugettext,{$ENDIF}UConst, UOpTransaction, UFRMNewPrivateKeyType, UFRMWalletKeys, UFRMHashLock, + UCommon, ULog, UGUIUtils, USettings, UCoreUtils, Math; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TFRMOperation } +type + TOperationGroupBoxAccess = class(TGroupBox); + +constructor TFRMOperation.Create(AOwner: TComponent); +begin + inherited Create(AOwner); +end; + +constructor TFRMOperation.Create(AOwner: TComponent; ANode: TNode); +begin + FExplicitNode := True; + FNode := ANode; + inherited Create(AOwner); +end; + +procedure TFRMOperation.FormShow(Sender: TObject); +begin + FormResize(Self); +end; + +procedure TFRMOperation.FormResize(Sender: TObject); +var + LFontHeight, LPad, LGap, LEditHeight, LRadioHeight, LButtonHeight: Integer; + LWidth, LLabelWidth, LEditLeft, LRight, LTop, LMiddle, LRadioWidth, + LStatusHeight: Integer; + LContent: TRect; + + function S(AValue: Integer): Integer; + begin + Result := MulDiv(AValue, LFontHeight, 13); + end; + + function RadioWidth(ARadio: TRadioButton): Integer; + begin + Canvas.Font.Assign(ARadio.Font); + // The native check glyph does not scale from the form's text-height ratio. + Result := Canvas.TextWidth(StringReplace(ARadio.Caption, '&', '', [rfReplaceAll])) + + GetSystemMetrics(SM_CXMENUCHECK) + Max(8, S(12)); + end; + + procedure Put(AControl: TControl; X, Y, AWidth, AHeight: Integer); + begin + {$IFDEF FPC} + // LCL radios auto-size by default; preserve this layout's measured caption width. + if AControl is TRadioButton then + TRadioButton(AControl).AutoSize := False; + {$ENDIF} + AControl.SetBounds(X, Y, Max(1, AWidth), Max(1, AHeight)); + end; + + procedure PutLabel(ALabel: TLabel; X, Y, AWidth: Integer); + begin + Canvas.Font.Assign(ALabel.Font); + ALabel.AutoSize := False; + Put(ALabel, X, Y, AWidth, Canvas.TextHeight('Wg') + S(2)); + end; + + procedure PutEdit(AEdit: TEdit; X, Y, AWidth: Integer); + var + LHeight: Integer; + begin + Canvas.Font.Assign(AEdit.Font); + LHeight := Max(LEditHeight, Canvas.TextHeight('Wg') + + Max(S(6), AEdit.Height - AEdit.ClientHeight)); + Put(AEdit, X, Y, AWidth, LHeight); + end; + + procedure PutWrappedLabel(ALabel: TLabel; X, Y, AWidth, AMinimumLines: Integer); + var + LText: TRect; + begin + Canvas.Font.Assign(ALabel.Font); + ALabel.WordWrap := True; + ALabel.AutoSize := False; + LText := Rect(0, 0, Max(1, AWidth), 0); + DrawText(Canvas.Handle, PChar(ALabel.Caption), Length(ALabel.Caption), LText, + DT_CALCRECT or DT_WORDBREAK or DT_NOPREFIX); + Put(ALabel, X, Y, AWidth, + Max(Canvas.TextHeight('Wg') * AMinimumLines, LText.Bottom - LText.Top)); + end; + + procedure ErrorLabel(ALabel: TLabel; AParent: TWinControl); + begin + PutWrappedLabel(ALabel, LPad, LGap, AParent.ClientWidth - LPad * 2, 2); + end; + +begin + if not FLayoutReady or FArrangingLayout then + Exit; + FArrangingLayout := True; + DisableAlign; + gbPayload.DisableAlign; + try + Canvas.Font.Assign(Font); + LFontHeight := Max(1, Canvas.TextHeight('Wg')); + LPad := S(12); + LGap := S(6); + LEditHeight := S(24); + LRadioHeight := S(22); + LButtonHeight := S(32); + + PutLabel(lblAccountCaption, LPad, S(18), S(70)); + PutEdit(ebSenderAccount, S(90), LPad, S(140)); + Put(memoAccounts, S(90), LPad, ClientWidth - S(390), S(60)); + PutLabel(lblBalanceCaption, ClientWidth - S(280), S(18), S(65)); + lblAccountBalance.Alignment := taRightJustify; + PutLabel(lblAccountBalance, ClientWidth - S(210), S(14), S(195)); + PutLabel(lblAccountsCount, ClientWidth - S(280), S(52), S(265)); + Put(bbCancel, ClientWidth - LPad - S(116), ClientHeight - LPad - LButtonHeight, + S(116), LButtonHeight); + Put(bbExecute, bbCancel.Left - LPad - S(150), bbCancel.Top, S(150), LButtonHeight); + Put(PageControlLocked, LPad, S(84), ClientWidth - LPad * 2, + bbCancel.Top - S(84) - LPad); + + LWidth := tsOperation.ClientWidth; + Put(PageControlOpType, LPad, LGap, LWidth - LPad * 2, S(232)); + LTop := PageControlOpType.Top + PageControlOpType.Height + LGap; + PutLabel(lblFee, LPad, LTop + S(4), S(100)); + PutEdit(ebFee, S(118), LTop, S(100)); + LRight := LWidth - LPad - S(24); + Put(sbSearchSignerAccount, LRight, LTop, S(24), LEditHeight); + PutEdit(ebSignerAccount, LRight - LGap - S(110), LTop, S(110)); + PutLabel(lblSignerAccount, ebSignerAccount.Left - LGap - S(175), + LTop + S(4), S(175)); + LTop := LTop + LEditHeight + LGap; + Put(gbPayload, LPad, LTop, LWidth - LPad * 2, + tsOperation.ClientHeight - LTop - LPad); + + // Respect each toolkit's native caption inset rather than sharing a raw Y. + LContent := gbPayload.ClientRect; + TOperationGroupBoxAccess(gbPayload).AdjustClientRect(LContent); + LRadioWidth := Max(RadioWidth(rbEncryptedWithEC), RadioWidth(rbEncryptedWithOldEC)); + LRadioWidth := Max(LRadioWidth, RadioWidth(rbNotEncrypted)); + LRadioWidth := Max(LRadioWidth, RadioWidth(rbEncrptedWithPassword)); + LTop := LContent.Top + LGap; + Put(rbEncryptedWithOldEC, LContent.Left + LPad, LTop, LRadioWidth, LRadioHeight); + Inc(LTop, LRadioHeight + LGap); + Put(rbEncryptedWithEC, LContent.Left + LPad, LTop, LRadioWidth, LRadioHeight); + Inc(LTop, LRadioHeight + LGap); + Put(rbEncrptedWithPassword, LContent.Left + LPad, LTop, LRadioWidth, LRadioHeight); + Inc(LTop, LRadioHeight + LGap); + PutLabel(lblEncryptPassword, LContent.Left + S(28), LTop + S(4), S(72)); + PutEdit(ebEncryptPassword, LContent.Left + S(104), LTop, + LRadioWidth - S(104) + LPad); + Inc(LTop, LEditHeight + LGap); + Put(rbNotEncrypted, LContent.Left + LPad, LTop, LRadioWidth, LRadioHeight); + LEditLeft := LContent.Left + LPad * 2 + LRadioWidth; + LRight := LContent.Right - LPad; + PutLabel(Label4, LEditLeft, LContent.Top + LGap, S(100)); + PutLabel(lblPayloadLength, LRight - S(150), Label4.Top, S(150)); + LTop := Label4.Top + Label4.Height + LGap; + PutWrappedLabel(lblEncryptionErrors, LEditLeft, LTop, + LRight - S(140) - LEditLeft - LGap, 2); + LStatusHeight := Max(LEditHeight, lblEncryptionErrors.Height); + Put(memoPayload, LEditLeft, LTop, LRight - LEditLeft, + LContent.Bottom - LTop - LStatusHeight - LGap * 2); + LTop := memoPayload.Top + memoPayload.Height + LGap; + Put(cbPayloadDataInputType, LRight - S(140), LTop, S(140), LEditHeight); + Put(lblEncryptionErrors, LEditLeft, LTop, + cbPayloadDataInputType.Left - LEditLeft - LGap, + LStatusHeight); + + // Lay out all operation pages from the same measured label/edit rows. + LLabelWidth := S(128); + LEditLeft := LPad + LLabelWidth; + LWidth := tsTransaction.ClientWidth; + ErrorLabel(lblTransactionErrors, tsTransaction); + LTop := lblTransactionErrors.Top + lblTransactionErrors.Height + LGap; + PutLabel(lblDestAccount, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebDestAccount, LEditLeft, LTop, LWidth - LEditLeft - LPad - S(30)); + Put(sbSearchDestinationAccount, LWidth - LPad - S(24), LTop, S(24), LEditHeight); + Inc(LTop, LEditHeight + LGap); + PutLabel(lblAmount, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebAmount, LEditLeft, LTop, S(110)); + Put(memoEPASA, LPad, LTop + LEditHeight + LGap, LWidth - LPad * 2, S(64)); + + LWidth := tsChangePrivateKey.ClientWidth; + ErrorLabel(lblChangeKeyErrors, tsChangePrivateKey); + LTop := lblChangeKeyErrors.Top + lblChangeKeyErrors.Height + LGap; + Put(rbChangeKeyWithAnother, LPad, LTop, LWidth - LPad * 2, LRadioHeight); + Inc(LTop, LRadioHeight + LGap); + PutLabel(lblNewPrivateKey, LPad, LTop + S(4), LLabelWidth - LGap); + Put(cbNewPrivateKey, LEditLeft, LTop, LWidth - LEditLeft - LPad - S(42), LEditHeight); + Put(bbChangePrivateKeyKeys, LWidth - LPad - S(36), LTop, S(36), LEditHeight); + Inc(LTop, LEditHeight + LGap); + Put(rbChangeKeyTransferAccountToNewOwner, LPad, LTop, + LWidth - LPad * 2, LRadioHeight); + Inc(LTop, LRadioHeight + LGap); + PutLabel(lblNewOwnerPublicKey, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebNewPublicKey, LEditLeft, LTop, LWidth - LEditLeft - LPad); + PutWrappedLabel(lblNewOwnerErrors, LPad, LTop + LEditHeight + LGap, + LWidth - LPad * 2, 1); + + LWidth := tsListAccount.ClientWidth; + LMiddle := LWidth div 2; + ErrorLabel(lblListAccountErrors, tsListAccount); + LTop := lblListAccountErrors.Top + lblListAccountErrors.Height + LGap; + Put(rbListAccountForPublicSale, LPad, LTop, LMiddle - LPad, LRadioHeight); + Put(rbListAccountForAccountSwap, LMiddle, LTop, LMiddle - LPad, LRadioHeight); + Inc(LTop, LRadioHeight + LGap); + Put(rbListAccountForPrivateSale, LPad, LTop, LMiddle - LPad, LRadioHeight); + Put(rbListAccountForCoinSwap, LMiddle, LTop, LMiddle - LPad, LRadioHeight); + Inc(LTop, LRadioHeight + LGap); + PutLabel(lblPrice, LPad, LTop + S(4), S(80)); + PutEdit(ebPrice, S(100), LTop, S(110)); + PutLabel(lblSeller, LMiddle, LTop + S(4), S(90)); + PutEdit(ebSellerAccount, LMiddle + S(96), LTop, + LWidth - LMiddle - S(126) - LPad); + Put(sbSearchListerSellerAccount, LWidth - LPad - S(24), LTop, S(24), LEditHeight); + Inc(LTop, LEditHeight + LGap); + PutLabel(lblNewKey, LPad, LTop + S(4), S(80)); + PutEdit(ebNewKey, S(100), LTop, LWidth - S(100) - LPad); + Inc(LTop, LEditHeight + LGap); + PutLabel(lblTimeLock, LPad, LTop + S(4), S(80)); + PutEdit(ebTimeLock, S(100), LTop, S(90)); + Put(sbTimeLock, S(196), LTop, S(24), LEditHeight); + PutLabel(lblHashLock, LMiddle, LTop + S(4), S(65)); + PutEdit(ebHashLock, LMiddle + S(71), LTop, + LWidth - LMiddle - S(101) - LPad); + Put(btnHashLock, LWidth - LPad - S(24), LTop, S(24), LEditHeight); + + ErrorLabel(lblDelistErrors, lblDelistErrors.Parent); + LWidth := tsBuyAccount.ClientWidth; + ErrorLabel(lblBuyAccountErrors, tsBuyAccount); + LTop := lblBuyAccountErrors.Top + lblBuyAccountErrors.Height + LGap; + PutLabel(lblAccountToBuy, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebAccountToBuy, LEditLeft, LTop, S(110)); + Put(sbSearchBuyAccount, LEditLeft + S(116), LTop, S(24), LEditHeight); + Inc(LTop, LEditHeight + LGap); + PutLabel(lblBuyAmount, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebBuyAmount, LEditLeft, LTop, S(110)); + Inc(LTop, LEditHeight + LGap); + PutLabel(lblBuyNewKey, LPad, LTop + S(4), LLabelWidth - LGap); + Put(cbBuyNewKey, LEditLeft, LTop, LWidth - LEditLeft - LPad - S(42), LEditHeight); + Put(bbBuyNewKey, LWidth - LPad - S(36), LTop, S(36), LEditHeight); + PutWrappedLabel(Label2, LPad, LTop + LEditHeight + LGap, + LWidth - LPad * 2, 1); + + LWidth := tsChangeInfo.ClientWidth; + ErrorLabel(lblChangeInfoErrors, tsChangeInfo); + LTop := lblChangeInfoErrors.Top + lblChangeInfoErrors.Height + LGap; + PutLabel(lblChangeName, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebChangeName, LEditLeft, LTop, LWidth - LEditLeft - LPad); + Inc(LTop, LEditHeight + LGap); + PutLabel(lblChangeType, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebChangeType, LEditLeft, LTop, S(110)); + Inc(LTop, LEditHeight + LGap); + PutLabel(lblChangeAccountData, LPad, LTop + S(4), LLabelWidth - LGap); + PutEdit(ebChangeAccountData, LEditLeft, LTop, LWidth - LEditLeft - LPad); + + lblGlobalErrors.WordWrap := True; + lblGlobalErrors.AutoSize := False; + Put(lblGlobalErrors, LPad, LPad, tsGlobalError.ClientWidth - LPad * 2, + tsGlobalError.ClientHeight - S(80)); + Put(bbPassword, (tsGlobalError.ClientWidth - S(180)) div 2, + tsGlobalError.ClientHeight - S(60), S(180), S(38)); + finally + gbPayload.EnableAlign; + EnableAlign; + FArrangingLayout := False; + end; +end; + procedure TFRMOperation.actExecuteExecute(Sender: TObject); -Var errors : AnsiString; - P : PAccount; - i,iAcc : Integer; - wk : TWalletKey; - ops : TOperationsHashTree; - op : TPCOperation; - account,signerAccount,destAccount,accountToBuy : TAccount; - operation_to_string, operationstxt, auxs : String; - _amount,_fee, _totalamount, _totalfee, _totalSignerFee, _salePrice : Int64; - _lockedUntil, _signer_n_ops : Cardinal; - dooperation : Boolean; - _newOwnerPublicKey : TECDSA_Public; - _newName : TRawBytes; - _newType : Word; - _changeName, _changeType, _V2, _executeSigner : Boolean; - _senderAccounts : TCardinalsArray; - LNewAccountState : TAccountState; - LAccountTarget : cardinal; +var + LErrors : String; + LAccountPtr : PAccount; + i,LAccountNo : Integer; + LKey : TWalletKey; + LOperations : TOperationsHashTree; + LOperation : TPCOperation; + LAccount,LSignerAccount,LTargetAccount,LPurchaseAccount : TAccount; + LTargetEPASA : TEPasa; + LOperationString, LOperationsTxt, LAuxStr, LPrompt, LDetails : String; + LAmount, LFee, LTotalamount, LTotalFee, LTotalSignerFee, LSalePrice : Int64; + LLockedUntil, LSignerNOps : Cardinal; + LDoOperation : Boolean; + LNewOwnerPublicKey : TECDSA_Public; LHashLock : T32Bytes; - lChangeData : boolean; - LNewData : TRawBytes; + LNewName, LNewAccountData : TRawBytes; + LNewType : Word; + LChangeName, LChangeType, LChangeAccountData, LIsV2, LExecuteSigner, LRecipientSigned, LTargetRequiresPurchase : Boolean; + LSenderAccounts : TCardinalsArray; + LMempool: TPCOperationsComp; label loop_start; begin if Not Assigned(WalletKeys) then raise Exception.Create('No wallet keys'); - If Not UpdateOperationOptions(errors) then raise Exception.Create(errors); - ops := TOperationsHashTree.Create; + If Not UpdateOperationOptions(LErrors) then raise Exception.Create(LErrors); + LOperations := TOperationsHashTree.Create; Try - _V2 := FNode.Bank.SafeBox.CurrentProtocol >= CT_PROTOCOL_2; - _totalamount := 0; - _totalfee := 0; - _totalSignerFee := 0; - _signer_n_ops := 0; - operationstxt := ''; - operation_to_string := ''; + LIsV2 := FNode.Bank.SafeBox.CurrentProtocol >= CT_PROTOCOL_2; + LTotalamount := 0; + LTotalFee := 0; + LTotalSignerFee := 0; + LSignerNOps := 0; + LOperationsTxt := ''; + LOperationString := ''; // Compile FSenderAccounts into a reorderable array - _senderAccounts := FSenderAccounts.ToArray; + LSenderAccounts := FSenderAccounts.ToArray; // Loop through each sender account - for iAcc := 0 to Length(_senderAccounts) - 1 do begin + for LAccountNo := 0 to Length(LSenderAccounts) - 1 do begin loop_start: - op := Nil; - account := FNode.GetAccount(_senderAccounts[iAcc]); - If Not UpdatePayload(account, errors) then - raise Exception.Create('Error encoding payload of sender account '+TAccountComp.AccountNumberToAccountTxtNumber(account.account)+': '+errors); - i := WalletKeys.IndexOfAccountKey(account.accountInfo.accountKey); - if i<0 then begin - Raise Exception.Create('Sender account private key not found in Wallet'); + LOperation := Nil; + LAccount := FNode.GetMempoolAccount(LSenderAccounts[LAccountNo]); + If Not UpdatePayload(LAccount, LErrors) then + raise Exception.Create('Error encoding payload of sender account '+TAccountComp.AccountNumberToAccountTxtNumber(LAccount.account)+': '+LErrors); + if NOT WalletKeys.TryGetKey(LAccount.accountInfo.accountKey, LKey) then begin + + if ( + (TAccountComp.IsAccountForPrivateSale(LAccount.accountInfo)) or + (TAccountComp.IsAccountForAccountSwap(LAccount.accountInfo)) + ) + and (Not TAccountComp.IsNullAccountKey(LAccount.accountInfo.new_publicKey)) then begin + + if NOT WalletKeys.TryGetKey(LAccount.accountInfo.new_publicKey, LKey) then + Raise Exception.Create('New sender account private key not found in Wallet'); + end else Raise Exception.Create('Sender account private key not found in Wallet'); end; - - wk := WalletKeys.Key[i]; - dooperation := true; + LDoOperation := true; // Default fee - if account.balance > uint64(DefaultFee) then _fee := DefaultFee else _fee := account.balance; + if LAccount.balance > uint64(DefaultFee) then LFee := DefaultFee else LFee := LAccount.balance; // Determine which operation type it is if PageControlOpType.ActivePage = tsTransaction then begin {%region Operation: Transaction} - if Not UpdateOpTransaction(account,destAccount,_amount,errors) then raise Exception.Create(errors); - if Length(_senderAccounts) > 1 then begin - if account.balance>0 then begin - if account.balance>DefaultFee then begin - _amount := account.balance - DefaultFee; - _fee := DefaultFee; + if Not UpdateOpTransaction(LAccount, LTargetEPASA, LTargetAccount,LNewOwnerPublicKey, LTargetRequiresPurchase, LAmount, LErrors) then + raise Exception.Create(LErrors); + + if Length(LSenderAccounts) > 1 then begin + if LAccount.balance>0 then begin + if LAccount.balance>DefaultFee then begin + LAmount := LAccount.balance - DefaultFee; + LFee := DefaultFee; end else begin - _amount := account.balance; - _fee := 0; + LAmount := LAccount.balance; + LFee := 0; end; - end else dooperation := false; - end else begin + end else LDoOperation := false; end; - if dooperation then begin - op := TOpTransaction.CreateTransaction(FNode.Bank.SafeBox.CurrentProtocol, - account.account,account.n_operation+1,destAccount.account,wk.PrivateKey,_amount,_fee,FOperationPayload); - inc(_totalamount,_amount); - inc(_totalfee,_fee); + + if LDoOperation then begin + if NOT LTargetRequiresPurchase then begin + LOperation := TOpTransaction.CreateTransaction(FNode.Bank.Safebox.CurrentProtocol,LAccount.account,LAccount.n_operation+1,LTargetAccount.account,LKey.PrivateKey,LAmount,LFee,FEncodedPayload); + LOperationsTxt := 'Transaction to '+TAccountComp.AccountNumberToAccountTxtNumber(LTargetAccount.account); + end else begin + // Pay-to-Key + LOperation := TOpBuyAccount.CreateBuy(FNode.Bank.SafeBox.CurrentProtocol, LAccount.account, LAccount.n_operation + 1, LTargetAccount.account, LTargetAccount.accountInfo.account_to_pay, LTargetAccount.accountInfo.price, LAmount, LFee, LNewOwnerPublicKey, LKey.PrivateKey, FEncodedPayload); + end; + inc(LTotalamount,LAmount); + inc(LTotalFee,LFee); end; - operationstxt := 'Transaction to '+TAccountComp.AccountNumberToAccountTxtNumber(destAccount.account); {%endregion} end else if (PageControlOpType.ActivePage = tsChangePrivateKey) then begin {%region Operation: Change Private Key} - if Not UpdateOpChangeKey(account,signerAccount,_newOwnerPublicKey,errors) then raise Exception.Create(errors); - if _V2 then begin + if Not UpdateOpChangeKey(LAccount,LSignerAccount,LNewOwnerPublicKey,LErrors) then raise Exception.Create(LErrors); + if LIsV2 then begin // must ensure is Signer account last if included in sender accounts (not necessarily ordered enumeration) - if (iAcc < Length(_senderAccounts) - 1) AND (account.account = signerAccount.account) then begin - TArrayTool.Swap(_senderAccounts, iAcc, Length(_senderAccounts) - 1); // ensure signer account processed last + if (LAccountNo < Length(LSenderAccounts) - 1) AND (LAccount.account = LSignerAccount.account) then begin + TArrayTool.Swap(LSenderAccounts, LAccountNo, Length(LSenderAccounts) - 1); // ensure signer account processed last goto loop_start; // TODO: remove ugly hack with refactoring! end; // Maintain correct signer fee distribution - if uint64(_totalSignerFee) >= signerAccount.balance then _fee := 0 - else if signerAccount.balance - uint64(_totalSignerFee) > uint64(DefaultFee) then _fee := DefaultFee - else _fee := signerAccount.balance - uint64(_totalSignerFee); - op := TOpChangeKeySigned.Create(FNode.Bank.SafeBox.CurrentProtocol,signerAccount.account,signerAccount.n_operation+_signer_n_ops+1,account.account,wk.PrivateKey,_newOwnerPublicKey,_fee,FOperationPayload); - inc(_signer_n_ops); - inc(_totalSignerFee, _fee); + if uint64(LTotalSignerFee) >= LSignerAccount.balance then LFee := 0 + else if LSignerAccount.balance - uint64(LTotalSignerFee) > uint64(DefaultFee) then LFee := DefaultFee + else LFee := LSignerAccount.balance - uint64(LTotalSignerFee); + LOperation := TOpChangeKeySigned.Create(FNode.Bank.SafeBox.CurrentProtocol,LSignerAccount.account,LSignerAccount.n_operation+LSignerNOps+1,LAccount.account,LKey.PrivateKey,LNewOwnerPublicKey,LFee,FEncodedPayload); + inc(LSignerNOps); + inc(LTotalSignerFee, LFee); end else begin - op := TOpChangeKey.Create(FNode.Bank.SafeBox.CurrentProtocol,account.account,account.n_operation+1,account.account,wk.PrivateKey,_newOwnerPublicKey,_fee,FOperationPayload); + LOperation := TOpChangeKey.Create(FNode.Bank.SafeBox.CurrentProtocol,LAccount.account,LAccount.n_operation+1,LAccount.account,LKey.PrivateKey,LNewOwnerPublicKey,LFee,FEncodedPayload); end; - inc(_totalfee,_fee); - operationstxt := 'Change private key to '+TAccountComp.GetECInfoTxt(_newOwnerPublicKey.EC_OpenSSL_NID); + inc(LTotalFee,LFee); + LOperationsTxt := 'Change private key to '+TAccountComp.GetECInfoTxt(LNewOwnerPublicKey.EC_OpenSSL_NID); {%endregion} - end else if (PageControlOpType.ActivePage = tsListForSale) then begin + end else if (PageControlOpType.ActivePage = tsListAccount) then begin {%region Operation: List For Sale} - If Not UpdateOpListForSale(account,_salePrice,destAccount,signerAccount,_newOwnerPublicKey,_lockedUntil,errors) then raise Exception.Create(errors); + If Not UpdateOpListAccount(LAccount,LSalePrice,LTargetAccount,LSignerAccount,LNewOwnerPublicKey, LLockedUntil, LHashLock, LErrors) then raise Exception.Create(LErrors); + if (LAccountNo < Length(LSenderAccounts) - 1) and (LAccount.account = LSignerAccount.account) then + begin + TArrayTool.Swap(LSenderAccounts, LAccountNo, High(LSenderAccounts)); + goto loop_start; + end; // Special fee account: - if signerAccount.balance>DefaultFee then _fee := DefaultFee - else _fee := signerAccount.balance; + if LSignerAccount.balance>DefaultFee then LFee := DefaultFee + else LFee := LSignerAccount.balance; if (rbListAccountForPublicSale.Checked) then begin - LHashLock := CT_HashLock_NUL; // Skybuck: check or debug ! LHashLock zero value array passed in function below perhaps replace with correctly filled hash array ! - // Skybuck: LNewAccountState parameter added, check or debug this later ! passed in function below ! LNewAccountState is not set yet ! - op := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap(FNode.Bank.SafeBox.CurrentProtocol, LNewAccountState, signerAccount.account,signerAccount.n_operation+1+iAcc, account.account,_salePrice,_fee, destAccount.account,CT_TECDSA_Public_Nul,0,wk.PrivateKey,LHashLock, FOperationPayload); + LOperation := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap(FNode.Bank.SafeBox.CurrentProtocol, as_ForSale, LSignerAccount.account,LSignerAccount.n_operation+1+LAccountNo, LAccount.account,LSalePrice,LFee, + LTargetAccount.account,CT_TECDSA_Public_Nul,0,LKey.PrivateKey, CT_HashLock_NUL, FEncodedPayload); end else if (rbListAccountForPrivateSale.Checked) then begin - op := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap(FNode.Bank.SafeBox.CurrentProtocol, LNewAccountState, signerAccount.account,signerAccount.n_operation+1+iAcc, account.account,_salePrice,_fee, destAccount.account,_newOwnerPublicKey,_lockedUntil,wk.PrivateKey,LHashLock, FOperationPayload); + LOperation := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap(FNode.Bank.SafeBox.CurrentProtocol, as_ForSale, LSignerAccount.account,LSignerAccount.n_operation+1+LAccountNo, LAccount.account,LSalePrice,LFee, + LTargetAccount.account,LNewOwnerPublicKey,LLockedUntil,LKey.PrivateKey, CT_HashLock_NUL, FEncodedPayload); + end else if (rbListAccountForAccountSwap.Checked) then begin + LOperation := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap(FNode.Bank.SafeBox.CurrentProtocol, as_ForAtomicAccountSwap, LSignerAccount.account,LSignerAccount.n_operation+1+LAccountNo, LAccount.account,LSalePrice,LFee, + LTargetAccount.account,LNewOwnerPublicKey,LLockedUntil,LKey.PrivateKey, LHashLock, FEncodedPayload); + end else if (rbListAccountForCoinSwap.Checked) then begin + LOperation := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap(FNode.Bank.SafeBox.CurrentProtocol, as_ForAtomicCoinSwap, LSignerAccount.account,LSignerAccount.n_operation+1+LAccountNo, LAccount.account,LSalePrice,LFee, + LTargetAccount.account,CT_TECDSA_Public_Nul,LLockedUntil,LKey.PrivateKey, LHashLock, FEncodedPayload); end else raise Exception.Create('Select Sale type'); {%endregion} - end else if (PageControlOpType.ActivePage = tsDelist) then begin + end else if (PageControlOpType.ActivePage = tsDelistAccount) then begin {%region Operation: Delist For Sale} - if Not UpdateOpDelist(account,signerAccount,errors) then raise Exception.Create(errors); + if Not UpdateOpDelist(LAccount,LSignerAccount,LErrors) then raise Exception.Create(LErrors); // Special fee account: - if signerAccount.balance>DefaultFee then _fee := DefaultFee - else _fee := signerAccount.balance; - op := TOpDelistAccountForSale.CreateDelistAccountForSale(FNode.Bank.SafeBox.CurrentProtocol,signerAccount.account,signerAccount.n_operation+1+iAcc,account.account,_fee,wk.PrivateKey,FOperationPayload); + if LSignerAccount.balance>DefaultFee then LFee := DefaultFee + else LFee := LSignerAccount.balance; + LOperation := TOpDelistAccountForSale.CreateDelistAccountForSale(FNode.Bank.SafeBox.CurrentProtocol,LSignerAccount.account,LSignerAccount.n_operation+1+LAccountNo,LAccount.account,LFee,LKey.PrivateKey,FEncodedPayload); {%endregion} end else if (PageControlOpType.ActivePage = tsBuyAccount) then begin {%region Operation: Buy Account} - if Not UpdateOpBuyAccount(account,accountToBuy,_amount,_newOwnerPublicKey,errors) then raise Exception.Create(errors); - op := TOpBuyAccount.CreateBuy(FNode.Bank.SafeBox.CurrentProtocol,account.account,account.n_operation+1,accountToBuy.account,accountToBuy.accountInfo.account_to_pay, - accountToBuy.accountInfo.price,_amount,_fee,_newOwnerPublicKey,wk.PrivateKey,FOperationPayload); + if Not UpdateOpBuyAccount(LAccount,LPurchaseAccount,LAmount,LNewOwnerPublicKey, LRecipientSigned, LErrors) then raise Exception.Create(LErrors); + if LRecipientSigned AND (NOT WalletKeys.TryGetKey(LAccount.accountInfo.new_publicKey, LKey)) then + raise Exception.Create('Recipient-signed key not found in Wallet'); + LOperation := TOpBuyAccount.CreateBuy(FNode.Bank.Safebox.CurrentProtocol,LAccount.account,LAccount.n_operation+1,LPurchaseAccount.account,LPurchaseAccount.accountInfo.account_to_pay, + LPurchaseAccount.accountInfo.price,LAmount,LFee,LNewOwnerPublicKey,LKey.PrivateKey,FEncodedPayload); {%endregion} end else if (PageControlOpType.ActivePage = tsChangeInfo) then begin {%region Operation: Change Info} - if not UpdateOpChangeInfo(account,signerAccount,_changeName,_newName,_changeType,_newType,errors) then raise Exception.Create(errors); - if signerAccount.balance>DefaultFee then _fee := DefaultFee - else _fee := signerAccount.balance; - // Skybuck: check or debug LChangeData and LNewData !! (newly added, no correct values yet set in these two variables !?!) - op := TOpChangeAccountInfo.CreateChangeAccountInfo(FNode.Bank.SafeBox.CurrentProtocol,signerAccount.account,signerAccount.n_operation+1,account.account,wk.PrivateKey,false,CT_TECDSA_Public_Nul, - _changeName,_newName,_changeType,_newType,LChangeData,LNewData,_fee,FOperationPayload); + if not UpdateOpChangeInfo(LAccount,LSignerAccount,LChangeName,LNewName,LChangeType,LNewType,LChangeAccountData,LNewAccountData,LErrors) then begin + If Length(LSenderAccounts)=1 then raise Exception.Create(LErrors); + end else begin + if LSignerAccount.balance>DefaultFee then LFee := DefaultFee + else LFee := LSignerAccount.balance; + LOperation := TOpChangeAccountInfo.CreateChangeAccountInfo(FNode.Bank.SafeBox.CurrentProtocol,LSignerAccount.account,LSignerAccount.n_operation+1,LAccount.account,LKey.PrivateKey,false,CT_TECDSA_Public_Nul, + LChangeName,LNewName,LChangeType,LNewType, + LChangeAccountData,LNewAccountData, + LFee,FEncodedPayload); + end; {%endregion} end else begin raise Exception.Create('No operation selected'); end; - if Assigned(op) And (dooperation) then begin - ops.AddOperationToHashTree(op); - if operation_to_string<>'' then operation_to_string := operation_to_string + #10; - operation_to_string := operation_to_string + op.ToString; + if Assigned(LOperation) And (LDoOperation) then begin + LOperations.AddOperationToHashTree(LOperation); + if LOperationString<>'' then LOperationString := LOperationString + #10; + LOperationString := LOperationString + LOperation.ToString; end; - FreeAndNil(op); + FreeAndNil(LOperation); end; - if (ops.OperationsCount=0) then raise Exception.Create('No valid operation to execute'); - - if (Length(_senderAccounts)>1) then begin - if PageControlOpType.ActivePage = tsTransaction then auxs := 'Total amount that dest will receive: '+TAccountComp.FormatMoney(_totalamount)+#10 - else auxs:=''; - if Application.MessageBox(PChar('Execute '+Inttostr(Length(_senderAccounts))+' operations?'+#10+ - 'Operation: '+operationstxt+#10+ - auxs+ - 'Total fee: '+TAccountComp.FormatMoney(_totalfee)+#10+#10+'Note: This operation will be transmitted to the network!'), - PChar(Application.Title),MB_YESNO+MB_ICONINFORMATION+MB_DEFBUTTON2)<>IdYes then exit; + if (LOperations.OperationsCount=0) then raise Exception.Create('No valid operation to execute'); + LMempool := FNode.LockMempoolRead; + try + if not TWIZOperationsHelper.ValidatePreparedOperations(LOperations, LMempool.SafeBoxTransaction, LErrors) then + raise Exception.Create(LErrors); + finally + FNode.UnlockMempoolRead; + end; + LTotalFee := LOperations.TotalFee; + + if PageControlOpType.ActivePage = tsTransaction then + LAuxStr := 'Total amount that dest will receive: ' + + TAccountComp.FormatMoney(LTotalamount) + #10 + else + LAuxStr := ''; + LAuxStr := LAuxStr + 'Total fee: ' + TAccountComp.FormatMoney(LTotalFee); + if (Length(LSenderAccounts)>1) then begin + LPrompt := 'Execute ' + IntToStr(LOperations.OperationsCount) + ' operations?'; + LDetails := 'Operation: ' + LOperationsTxt + #10 + #10 + LOperationString; end else begin - if Application.MessageBox(PChar('Execute this operation:'+#10+#10+operation_to_string+#10+#10+'Note: This operation will be transmitted to the network!'), - PChar(Application.Title),MB_YESNO+MB_ICONINFORMATION+MB_DEFBUTTON2)<>IdYes then exit; + LPrompt := 'Execute this operation:' + #10 + + 'Number of operations: ' + IntToStr(LOperations.OperationsCount); + LDetails := LOperationString; end; - i := FNode.AddOperations(nil,ops,Nil,errors); - if (i=ops.OperationsCount) then begin - Application.MessageBox(PChar('Successfully executed '+inttostr(i)+' operations!'+#10+#10+operation_to_string),PChar(Application.Title),MB_OK+MB_ICONINFORMATION); + LPrompt := LPrompt + #10 + LAuxStr + #10 + #10 + + 'Note: This operation will be transmitted to the network!'; + // The full prepared list scrolls; the default-negative decision stays visible. + if not ConfirmMemoQuery(Self, Application.Title, LPrompt, LDetails) then + Exit; + i := FNode.AddOperations(nil,LOperations,Nil,LErrors); + if (i=LOperations.OperationsCount) then begin + LOperationsTxt := 'Successfully executed ' + IntToStr(i) + ' operations!' + + #10 + LAuxStr; + ShowResultMemo(Self, Application.Title, LOperationsTxt, LOperationString); ModalResult := MrOk; end else if (i>0) then begin - Application.MessageBox(PChar('One or more of your operations has not been executed:'+#10+ - 'Errors:'+#10+ - errors+#10+#10+ - 'Total successfully executed operations: '+inttostr(i)),PChar(Application.Title),MB_OK+MB_ICONWARNING); + LOperationsTxt := 'One or more of your operations has not been executed:' + #10 + + 'Total successfully executed operations: ' + IntToStr(i); + ShowResultMemo(Self, Application.Title, LOperationsTxt, 'Errors:' + #10 + LErrors); ModalResult := MrOk; end else begin - raise Exception.Create(errors); + raise Exception.Create(LErrors); end; Finally - ops.Free; + LOperations.Free; End; end; @@ -372,6 +716,7 @@ procedure TFRMOperation.bbBuyNewKeyClick(Sender: TObject); begin FRM := TFRMWalletKeys.Create(Self); Try + // The experimental key manager uses the application's loaded wallet. FRM.ShowModal; cbBuyNewKey.SetFocus; UpdateWalletKeys; @@ -385,6 +730,7 @@ procedure TFRMOperation.bbChangePrivateKeyKeysClick(Sender: TObject); begin FRM := TFRMWalletKeys.Create(Self); Try + // The experimental key manager uses the application's loaded wallet. FRM.ShowModal; rbChangeKeyWithAnother.Checked := true; cbNewPrivateKey.SetFocus; @@ -395,30 +741,76 @@ procedure TFRMOperation.bbChangePrivateKeyKeysClick(Sender: TObject); end; procedure TFRMOperation.bbPasswordClick(Sender: TObject); -Var - errors : AnsiString; +Var s : String; + errors : String; begin - if NOT FWalletKeys.IsValidPassword then begin - TUserInterface.UnlockWallet(Self); - SetWalletKeys(TWallet.Keys); + if FWalletKeys.IsValidPassword then begin + end else begin + s := ''; + Repeat + if Not InputQueryPassword('Wallet password','Enter wallet password',s) then exit; + FWalletKeys.WalletPassword := s; + Until FWalletKeys.IsValidPassword; + SetWalletKeys(WalletKeys); UpdateOperationOptions(errors); end; end; + +function TFRMOperation.CaptureEPasa(const AEPasaTxt: String; out AEPasa: TEPasa): Boolean; +var LEPasaErrorCode : EPasaErrorCode; +begin + Result := TEPasa.TryParse(AEPasaTxt,True,AEPasa); + // + if ((FUpdating) or (Not Result)) then Exit; + FUpdating := True; + try + if AEPasa.PayloadType.HasTrait(ptPublic) then rbNotEncrypted.Checked := True + else if AEPasa.PayloadType.HasTrait(ptSenderKeyEncrypted) then rbEncryptedWithOldEC.Checked := True + else if AEPasa.PayloadType.HasTrait(ptRecipientKeyEncrypted) then rbEncryptedWithEC.Checked := True + else if AEPasa.PayloadType.HasTrait(ptPasswordEncrypted) then begin + rbEncrptedWithPassword.Checked := True; + ebEncryptPassword.Text := AEPasa.Password; + end; + if AEPasa.PayloadType.HasTrait(ptAsciiFormatted) then cbPayloadDataInputType.ItemIndex := 0 + else if AEPasa.PayloadType.HasTrait(ptHexFormatted) then cbPayloadDataInputType.ItemIndex := 1 + else if AEPasa.PayloadType.HasTrait(ptBase58Formatted) then cbPayloadDataInputType.ItemIndex := 2; + if (AEPasa.PayloadType.HasTrait(ptAsciiFormatted) or AEPasa.PayloadType.HasTrait(ptHexFormatted) or AEPasa.PayloadType.HasTrait(ptBase58Formatted)) then begin + memoPayload.Lines.Text := AEPasa.Payload; + end; + + memoEPASA.Lines.Clear; + if AEPasa.IsPayToKey then memoEPASA.Lines.Add('PayToKey EPASA'); + if AEPasa.IsAddressedByName then memoEPASA.Lines.Add('Addressed by name: '+AEPasa.AccountName); + memoEPASA.Lines.Add( AEPasa.ToString ); + if AEPasa.PayloadType.HasTrait(ptAsciiFormatted) or AEPasa.PayloadType.HasTrait(ptBase58Formatted) or AEPasa.PayloadType.HasTrait(ptHexFormatted) then begin + memoEPasa.Lines.Add( 'AS BYTES: 0x'+AEPasa.GetRawPayloadBytes.ToHexaString); + memoEPasa.Lines.Add( 'AS PRINTABLE: '+AEPasa.GetRawPayloadBytes.ToPrintable); + end; + + finally + FUpdating := False; + end; +end; + procedure TFRMOperation.CM_WalletChanged(var Msg: TMessage); begin UpdateWalletKeys; end; procedure TFRMOperation.ebAccountNumberExit(Sender: TObject); -Var an : Cardinal; +Var LEPasa : TEPASA; eb : TEdit; begin - if (Not assigned(Sender)) then exit; + if (Not assigned(Sender)) or (FUpdating) then exit; if (Not (Sender is TEdit)) then exit; eb := TEdit(Sender); - If TAccountComp.AccountTxtNumberToAccountNumber(eb.Text,an) then begin - eb.Text := TAccountComp.AccountNumberToAccountTxtNumber(an); + if CaptureEPasa(eb.Text,LEPasa) then begin + if LEPasa.IsClassicPASA then + eb.Text := LEPasa.ToClassicPASAString() + else if eb=ebDestAccount then + eb.Text := LEPasa.ToString(False) + else eb.Text := LEPasa.ToString(True); end else begin eb.Text := ''; end; @@ -429,7 +821,7 @@ procedure TFRMOperation.ebCurrencyExit(Sender: TObject); Var m : Int64; eb : TEdit; begin - if (Not assigned(Sender)) then exit; + if (Not assigned(Sender)) or (FUpdating) then exit; if (Not (Sender is TEdit)) then exit; eb := TEdit(Sender); If Not (eb.ReadOnly) then begin @@ -474,12 +866,14 @@ procedure TFRMOperation.ebSenderAccountKeyPress(Sender: TObject; var Key: Char); procedure TFRMOperation.FormCreate(Sender: TObject); begin + {$IFDEF USE_GNUGETTEXT}TranslateComponent(self);{$ENDIF} FDisabled := false; + FUpdating := False; FWalletKeys := Nil; FSenderAccounts := TOrderedCardinalList.Create; FSenderAccounts.OnListChanged := OnSenderAccountsChanged; - FDisabled := true; - FNode := TNode.Node; + if not FExplicitNode then + FNode := TNode.Node; ebSenderAccount.OnKeyDown:=ebAccountKeyDown; ebSenderAccount.Tag:=CT_AS_MyAccounts; ebSignerAccount.Text:=''; @@ -508,19 +902,21 @@ procedure TFRMOperation.FormCreate(Sender: TObject); lblListAccountErrors.Caption := ''; rbListAccountForPublicSale.OnClick := updateInfoClick; rbListAccountForPrivateSale.OnClick := updateInfoClick; - ebSalePrice.Text := TAccountComp.FormatMoney(0); - ebSalePrice.OnChange := updateInfoClick; - ebSalePrice.OnExit := ebCurrencyExit; + rbListAccountForAccountSwap.OnClick := updateInfoClick; + rbListAccountForCoinSwap.OnClick := updateInfoClick; + ebPrice.Text := TAccountComp.FormatMoney(0); + ebPrice.OnChange := updateInfoClick; + ebPrice.OnExit := ebCurrencyExit; ebSellerAccount.Text := ''; ebSellerAccount.OnChange := updateInfoClick; ebSellerAccount.OnExit := ebAccountNumberExit; ebSellerAccount.OnKeyDown := ebAccountKeyDown; ebSellerAccount.tag := CT_AS_MyAccounts; - ebSaleNewOwnerPublicKey.Text := ''; - ebSaleNewOwnerPublicKey.OnChange := updateInfoClick; - ebSaleLockedUntilBlock.Text := ''; - ebSaleLockedUntilBlock.OnChange := updateInfoClick; + ebNewKey.Text := ''; + ebNewKey.OnChange := updateInfoClick; + ebTimeLock.Text := ''; + ebTimeLock.OnChange := updateInfoClick; // lblDelistErrors.Caption := ''; @@ -537,19 +933,42 @@ procedure TFRMOperation.FormCreate(Sender: TObject); // ebChangeName.OnChange:=updateInfoClick; ebChangeType.OnChange:=updateInfoClick; + ebChangeAccountData.OnChange:=updateInfoClick; // sbSearchDestinationAccount.OnClick := sbSearchDestinationAccountClick; sbSearchListerSellerAccount.OnClick := sbSearchListerSellerAccountClick; + btnHashLock.OnClick := sbHashLockClick; sbSearchBuyAccount.OnClick := sbSearchBuyAccountClick; + sbTimeLock.OnClick := sbTimeLockClick; // ebFee.Text := TAccountComp.FormatMoney(0); ebFee.OnExit:= ebCurrencyExit; memoAccounts.Lines.Clear; PageControlOpType.ActivePage := tsTransaction; + cbPayloadDataInputType.Items.Clear; + cbPayloadDataInputType.Items.Add('As String'); + cbPayloadDataInputType.Items.Add('As Hexadecimal'); + cbPayloadDataInputType.Items.Add('As Base58'); + cbPayloadDataInputType.ItemIndex := 0; + cbPayloadDataInputType.OnChange := memoPayloadClick; + memoPayload.OnChange := memoPayloadClick; + memoPayload.OnClick := memoPayloadClick; + memoEPASA.ScrollBars := ssBoth; + memoEPASA.ReadOnly := False; + memoEPASA.TabStop := False; + memoEPASA.ParentFont := True; + {$IFDEF TESTNET} + memoEPASA.Visible := True; + {$ELSE} + memoEPASA.Visible := False; + {$ENDIF} + memoEPASA.Enabled := memoEPASA.Visible; + FLayoutReady := True; + FormResize(Self); end; procedure TFRMOperation.ebNewPublicKeyExit(Sender: TObject); -var errors : AnsiString; +var errors : String; begin UpdateOperationOptions(errors); end; @@ -562,7 +981,7 @@ procedure TFRMOperation.FormDestroy(Sender: TObject); function TFRMOperation.GetDefaultSenderAccount: TAccount; begin - if FSenderAccounts.Count>=1 then Result := FNode.GetAccount( FSenderAccounts.Get(0) ) + if FSenderAccounts.Count>=1 then Result := FNode.GetMempoolAccount( FSenderAccounts.Get(0) ) else Result := CT_Account_NUL; end; @@ -578,14 +997,16 @@ procedure TFRMOperation.ebAccountKeyDown(Sender: TObject; var Key: Word; Shift: procedure TFRMOperation.searchAccount(editBox: TCustomEdit); Var F : TFRMAccountSelect; - c : Cardinal; + LEPasa : TEPasa; begin F := TFRMAccountSelect.Create(Self); try F.Node := FNode; F.WalletKeys := FWalletKeys; F.Filters:=editBox.Tag; - If TAccountComp.AccountTxtNumberToAccountNumber(editBox.Text,c) then F.DefaultAccount := c; + if TEPasa.TryParse(editBox.Text,LEPasa) then begin + if LEPasa.Account.HasValue then F.DefaultAccount := LEPasa.Account.Value + end; F.AllowSelect:=True; If F.ShowModal=MrOk then begin editBox.Text := TAccountComp.AccountNumberToAccountTxtNumber(F.GetSelected); @@ -596,15 +1017,16 @@ procedure TFRMOperation.searchAccount(editBox: TCustomEdit); end; procedure TFRMOperation.memoPayloadClick(Sender: TObject); -Var errors : AnsiString; +Var errors : String; begin if SenderAccounts.Count>0 then begin UpdatePayload(TNode.Node.Bank.SafeBox.Account(SenderAccounts.Get(0)),errors); end; + FormResize(Self); end; procedure TFRMOperation.OnSenderAccountsChanged(Sender: TObject); -Var errors : AnsiString; +Var errors : String; begin if SenderAccounts.Count>1 then begin ebAmount.Text := 'ALL BALANCE'; @@ -617,15 +1039,18 @@ procedure TFRMOperation.OnSenderAccountsChanged(Sender: TObject); end; If SenderAccounts.Count>=1 then begin ebSignerAccount.text := TAccountComp.AccountNumberToAccountTxtNumber(SenderAccounts.Get(0)); - ebChangeName.Text := FNode.GetAccount(SenderAccounts.Get(0)).name.ToPrintable; - ebChangeType.Text := IntToStr(FNode.GetAccount(SenderAccounts.Get(0)).account_type); + ebChangeName.Text := FNode.GetMempoolAccount(SenderAccounts.Get(0)).name.ToPrintable; + ebChangeType.Text := IntToStr(FNode.GetMempoolAccount(SenderAccounts.Get(0)).account_type); + ebChangeAccountData.Text := FNode.GetMempoolAccount(SenderAccounts.Get(0)).account_data.ToHexaString; end else begin ebSignerAccount.text := ''; ebChangeName.Text := ''; ebChangeType.Text := ''; + ebChangeAccountData.Text := ''; end; UpdateAccountsInfo; UpdateOperationOptions(errors); + FormResize(Self); end; procedure TFRMOperation.OnWalletKeysChanged(Sender: TObject); @@ -634,9 +1059,10 @@ procedure TFRMOperation.OnWalletKeysChanged(Sender: TObject); end; procedure TFRMOperation.PageControlOpTypeChange(Sender: TObject); -var errors : AnsiString; +var errors : String; begin UpdateOperationOptions(errors); + FormResize(Self); end; procedure TFRMOperation.sbSearchBuyAccountClick(Sender: TObject); @@ -659,6 +1085,38 @@ procedure TFRMOperation.sbSearchSignerAccountClick(Sender: TObject); searchAccount(ebSignerAccount); end; +procedure TFRMOperation.sbTimeLockClick(Sender: TObject); +begin + Application.MessageBox(PChar(Format('Current block is %d' + +#10 + +#10+'Average seconds per block %d' + +#10 + +#10+'Average Blocks per day %d (Next 24 hours = block %d)' + +#10+'Average Blocks per week %d (Next 24*7 hours = block %d)' + ,[TNode.Node.Bank.BlocksCount, + CT_NewLineSecondsAvg, + (86400 DIV CT_NewLineSecondsAvg), + ((86400 DIV CT_NewLineSecondsAvg) + TNode.Node.Bank.BlocksCount), + ((86400 DIV CT_NewLineSecondsAvg)*7), + (((86400 DIV CT_NewLineSecondsAvg)*7) + TNode.Node.Bank.BlocksCount)])), + PChar(Application.Title),MB_OK); +end; + +procedure TFRMOperation.sbHashLockClick(Sender: TObject); +var + LFRM : TFRMHashLock; +begin + LFRM := TFRMHashLock.Create(Self); + try + case TModalResult(LFRM.ShowModal) of + mrOK: ebHashLock.Text := TBaseType.ToRawBytes ( LFRM.HashLock ).ToHexaString; + end; + finally + FreeAndNil(LFRM); + end; +end; + + procedure TFRMOperation.SetDefaultFee(const Value: Int64); var wd : Boolean; begin @@ -709,7 +1167,7 @@ procedure TFRMOperation.UpdateAccountsInfo; ebSenderAccount.Text := TAccountComp.AccountNumberToAccountTxtNumber(SenderAccounts.Get(0)); memoAccounts.Visible := false; ebSenderAccount.Visible := true; - balance := TNode.Node.GetAccount(SenderAccounts.Get(0)).balance; + balance := TNode.Node.GetMempoolAccount(SenderAccounts.Get(0)).balance; end else begin // Multiple sender accounts lblAccountCaption.Caption := 'Accounts'; @@ -717,7 +1175,7 @@ procedure TFRMOperation.UpdateAccountsInfo; ebSenderAccount.Visible := false; accountstext := ''; for i := 0 to SenderAccounts.Count - 1 do begin - acc := TNode.Node.GetAccount(SenderAccounts.Get(i)); + acc := TNode.Node.GetMempoolAccount(SenderAccounts.Get(i)); balance := balance + acc.balance; if (accountstext<>'') then accountstext:=accountstext+'; '; accountstext := accountstext+TAccountComp.AccountNumberToAccountTxtNumber(acc.account)+' ('+TAccountComp.FormatMoney(acc.balance)+')'; @@ -732,29 +1190,46 @@ procedure TFRMOperation.UpdateAccountsInfo; End; end; -function TFRMOperation.UpdateFee(var Fee: Int64; errors: AnsiString): Boolean; +function TFRMOperation.UpdateFee(var Fee: Int64; errors: String): Boolean; begin errors := ''; if trim(ebFee.Text)<>'' then begin Result := TAccountComp.TxtToMoney(Trim(ebFee.Text),Fee); if not Result then errors := 'Invalid fee value "'+ebFee.Text+'"'; end else begin - Fee := 0; + Fee := TSettings.DefaultFee; Result := true; end; + if (Fee<0) then begin + Result := False; + errors := 'Invalid fee value "'+ebFee.Text+'"'; + ebFee.Text := TAccountComp.FormatMoney(TSettings.DefaultFee); + end; + if (Fee=0) and (Not CT_AllowPropagate0feeOperations) then begin + Result := False; + errors := '0 fee not allowed'; + ebFee.Text := TAccountComp.FormatMoney(TSettings.DefaultFee); + end; end; procedure TFRMOperation.updateInfoClick(Sender: TObject); -Var errors : AnsiString; +Var errors : String; begin - UpdateOperationOptions(errors); + if FDisabled or FUpdating then Exit; + FDisabled := True; + try + UpdateOperationOptions(errors); + finally + FDisabled := False; + end; + FormResize(Self); end; -function TFRMOperation.UpdateOpBuyAccount(const SenderAccount: TAccount; var AccountToBuy: TAccount; var amount: Int64; var NewPublicKey: TAccountKey; var errors: AnsiString): Boolean; +function TFRMOperation.UpdateOpBuyAccount(const SenderAccount: TAccount; var AccountToBuy: TAccount; var amount: Int64; var NewPublicKey: TAccountKey; var ARecipientSigned : Boolean; var errors: String): Boolean; var c : Cardinal; i : Integer; begin - // + ARecipientSigned := false; lblBuyAccountErrors.Caption := ''; c:=0; errors := ''; Try @@ -766,24 +1241,38 @@ function TFRMOperation.UpdateOpBuyAccount(const SenderAccount: TAccount; var Acc errors := 'Invalid account to buy '+ebAccountToBuy.Text; exit; end; - If (c<0) Or (c>=FNode.Bank.AccountsCount) Or (c=SenderAccount.account) then begin + If (c<0) Or (c>=FNode.Bank.AccountsCount) then begin errors := 'Invalid account number'; exit; end; - AccountToBuy := FNode.GetAccount(c); - If not TAccountComp.IsAccountForSale(AccountToBuy.accountInfo) then begin - errors := 'Account '+TAccountComp.AccountNumberToAccountTxtNumber(c)+' is not for sale'; + AccountToBuy := FNode.GetMempoolAccount(c); + ARecipientSigned := TAccountComp.IsOperationRecipientSignable(SenderAccount, AccountToBuy, FNode.Bank.BlocksCount, FNode.Bank.SafeBox.CurrentProtocol); + if (SenderAccount.account = AccountToBuy.Account) AND (NOT ARecipientSigned) then begin + errors := 'Not recipient signable'; + exit; + end; + + If not TAccountComp.IsAccountForSaleOrSwap(AccountToBuy.accountInfo) then begin + errors := 'Account '+TAccountComp.AccountNumberToAccountTxtNumber(c)+' is not for sale or swap'; + exit; + end else if (TAccountComp.IsAccountForCoinSwap(AccountToBuy.accountInfo)) then begin + errors := 'Account '+TAccountComp.AccountNumberToAccountTxtNumber(c)+' is for '+TAccountComp.FormatMoney(AccountToBuy.accountInfo.price)+' COIN swap, cannot buy'; exit; end; + If Not TAccountComp.TxtToMoney(ebBuyAmount.Text,amount) then begin errors := 'Invalid amount value'; exit; end; - if (AccountToBuy.accountInfo.price>amount) then begin - errors := 'Account price '+TAccountComp.FormatMoney(AccountToBuy.accountInfo.price); + if (AccountToBuy.accountInfo.price>amount) AND (NOT ARecipientSigned) then begin + errors := Format('Account price %s < (amount:%s + balance:%s = %s)',[TAccountComp.FormatMoney(AccountToBuy.accountInfo.price), + TAccountComp.FormatMoney(amount), + TAccountComp.FormatMoney(AccountToBuy.balance), + TAccountComp.FormatMoney(amount + AccountToBuy.balance) + ]); exit; end; - if (amount+DefaultFee > SenderAccount.balance) then begin + if TAccountComp.IsAccountForSale(AccountToBuy.accountInfo) AND (amount+DefaultFee > SenderAccount.balance) then begin errors := 'Insufficient funds'; exit; end; @@ -804,8 +1293,7 @@ function TFRMOperation.UpdateOpBuyAccount(const SenderAccount: TAccount; var Acc End; end; -function TFRMOperation.UpdateOpChangeInfo(const TargetAccount: TAccount; var SignerAccount : TAccount; - var changeName : Boolean; var newName: TRawBytes; var changeType : Boolean; var newType: Word; var errors: AnsiString): Boolean; +function TFRMOperation.UpdateOpChangeInfo(const TargetAccount: TAccount; var SignerAccount : TAccount; var changeName : Boolean; var newName: TRawBytes; var changeType : Boolean; var newType: Word; var AChangeData : Boolean; var ANewData : TRawBytes; var errors: String): Boolean; var auxC : Cardinal; i : Integer; errCode : Integer; @@ -815,24 +1303,24 @@ function TFRMOperation.UpdateOpChangeInfo(const TargetAccount: TAccount; var Sig lblChangeInfoErrors.Caption:=''; if not (PageControlOpType.ActivePage=tsChangeInfo) then exit; try - if SenderAccounts.Count<>1 then begin - errors := 'Cannot change info with multioperations. Use only 1 account'; - exit; - end; if (TAccountComp.IsAccountLocked(TargetAccount.accountInfo,FNode.Bank.BlocksCount)) then begin errors := 'Account '+TAccountComp.AccountNumberToAccountTxtNumber(TargetAccount.account)+' is locked until block '+IntToStr(TargetAccount.accountInfo.locked_until_block); exit; end; // Signer: - If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSignerAccount.Text,auxC) then begin - errors := 'Invalid signer account'; - exit; + if SenderAccounts.Count=1 then begin + If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSignerAccount.Text,auxC) then begin + errors := 'Invalid signer account'; + exit; + end; + end else begin + auxC := TargetAccount.account; end; if (auxC<0) Or (auxC >= FNode.Bank.AccountsCount) then begin errors := 'Signer account does not exists '+TAccountComp.AccountNumberToAccountTxtNumber(auxC); exit; end; - SignerAccount := FNode.GetAccount(auxC); + SignerAccount := FNode.GetMempoolAccount(auxC); if (TAccountComp.IsAccountLocked(SignerAccount.accountInfo,FNode.Bank.BlocksCount)) then begin errors := 'Signer account '+TAccountComp.AccountNumberToAccountTxtNumber(SignerAccount.account)+' is locked until block '+IntToStr(SignerAccount.accountInfo.locked_until_block); exit; @@ -845,21 +1333,23 @@ function TFRMOperation.UpdateOpChangeInfo(const TargetAccount: TAccount; var Sig errors := 'This operation needs PROTOCOL 2 active'; exit; end; - // New name and type - newName.FromString(LowerCase( Trim(ebChangeName.Text) )); - If Not TBaseType.Equals(newName,TargetAccount.name) then begin - changeName:=True; - If Length(newName)>0 then begin - if (Not TPCSafeBox.ValidAccountName(newName,errors)) then begin - errors := '"'+newName.ToPrintable+'" is not a valid name: '+errors; - Exit; - end; - i := (FNode.Bank.SafeBox.FindAccountByName(newName)); - if (i>=0) then begin - errors := 'Name "'+newName.ToPrintable+'" is used by account '+TAccountComp.AccountNumberToAccountTxtNumber(i); - Exit; + // New name and type (only when single operation) + If (SenderAccounts.Count=1) then begin + newName.FromString(LowerCase( Trim(ebChangeName.Text) )); + If Not TBaseType.Equals(newName,TargetAccount.name) then begin + changeName:=True; + If Length(newName)>0 then begin + if (Not TPascalCoinProtocol.IsValidAccountName(TNode.Node.Bank.SafeBox.CurrentProtocol,newName,errors)) then begin + errors := '"'+newName.ToPrintable+'" is not a valid name: '+errors; + Exit; + end; + i := (FNode.Bank.SafeBox.FindAccountByName(newName)); + if (i>=0) then begin + errors := 'Name "'+newName.ToPrintable+'" is used by account '+TAccountComp.AccountNumberToAccountTxtNumber(i); + Exit; + end; end; - end; + end else changeName := False; end else changeName := False; val(ebChangeType.Text,newType,errCode); if (errCode>0) then begin @@ -868,8 +1358,21 @@ function TFRMOperation.UpdateOpChangeInfo(const TargetAccount: TAccount; var Sig end; changeType := TargetAccount.account_type<>newType; // - If (newName=TargetAccount.name) And (newType=TargetAccount.account_type) then begin - errors := 'Account name and type are the same. Not changed'; + if FNode.Bank.SafeBox.CurrentProtocol>=CT_PROTOCOL_5 then begin + // Allow Change Account.Data PIP-0024 + if Not TCrypto.HexaToRaw(ebChangeAccountData.Text,ANewData) then begin + errors := 'Invalid hexadecimal value at Data'; + Exit; + end; + AChangeData := Not TBaseType.Equals( TargetAccount.account_data , ANewData); + if Length(ANewData)>CT_MaxAccountDataSize then begin + errors := Format('Data size (%d) greater than %d',[Length(ANewData),CT_MaxAccountDataSize]); + Exit; + end; + end else AChangeData:=False; + If (SenderAccounts.Count=1) And (TBaseType.Equals(newName,TargetAccount.name)) And (newType=TargetAccount.account_type) + And (TBaseType.Equals(ANewData,TargetAccount.account_data)) then begin + errors := 'No changes on fields. Not changed'; Exit; end; finally @@ -879,12 +1382,14 @@ function TFRMOperation.UpdateOpChangeInfo(const TargetAccount: TAccount; var Sig lblChangeInfoErrors.Caption := errors; end else begin lblChangeInfoErrors.Font.Color := clGreen; - lblChangeInfoErrors.Caption := TAccountComp.AccountNumberToAccountTxtNumber(TargetAccount.account)+' can be updated'; + If (SenderAccounts.Count=1) then + lblChangeInfoErrors.Caption := TAccountComp.AccountNumberToAccountTxtNumber(TargetAccount.account)+' can be updated' + else lblChangeInfoErrors.Caption := IntToStr(SenderAccounts.Count)+' accounts can be updated' end; end; end; -function TFRMOperation.UpdateOpChangeKey(Const TargetAccount : TAccount; var SignerAccount : TAccount; var NewPublicKey: TAccountKey; var errors: AnsiString): Boolean; +function TFRMOperation.UpdateOpChangeKey(Const TargetAccount : TAccount; var SignerAccount : TAccount; var NewPublicKey: TAccountKey; var errors: String): Boolean; var i : Integer; auxC : Cardinal; begin @@ -927,7 +1432,7 @@ function TFRMOperation.UpdateOpChangeKey(Const TargetAccount : TAccount; var Sig errors := 'Signer account does not exists '+TAccountComp.AccountNumberToAccountTxtNumber(auxC); exit; end; - SignerAccount := FNode.GetAccount(auxC); + SignerAccount := FNode.GetMempoolAccount(auxC); if (TAccountComp.IsAccountLocked(SignerAccount.accountInfo,FNode.Bank.BlocksCount)) then begin errors := 'Signer account '+TAccountComp.AccountNumberToAccountTxtNumber(SignerAccount.account)+' is locked until block '+IntToStr(SignerAccount.accountInfo.locked_until_block); exit; @@ -948,16 +1453,16 @@ function TFRMOperation.UpdateOpChangeKey(Const TargetAccount : TAccount; var Sig end; end; -function TFRMOperation.UpdateOpDelist(const TargetAccount : TAccount; var SignerAccount : TAccount; var errors: AnsiString): Boolean; +function TFRMOperation.UpdateOpDelist(const TargetAccount : TAccount; var SignerAccount : TAccount; var errors: String): Boolean; Var auxC : Cardinal; begin lblDelistErrors.Caption := ''; errors := ''; Result := false; - if not (PageControlOpType.ActivePage=tsDelist) then exit; + if not (PageControlOpType.ActivePage=tsDelistAccount) then exit; try - if Not TAccountComp.IsAccountForSale(TargetAccount.accountInfo) then begin - errors := 'Account '+TAccountComp.AccountNumberToAccountTxtNumber(TargetAccount.account)+' is not for sale'; + if Not TAccountComp.IsAccountForSaleOrSwap(TargetAccount.accountInfo) then begin + errors := 'Account '+TAccountComp.AccountNumberToAccountTxtNumber(TargetAccount.account)+' is not for sale or swap'; exit; end; if (TAccountComp.IsAccountLocked(TargetAccount.accountInfo,FNode.Bank.BlocksCount)) then begin @@ -973,7 +1478,7 @@ function TFRMOperation.UpdateOpDelist(const TargetAccount : TAccount; var Signer errors := 'Signer account does not exists '+TAccountComp.AccountNumberToAccountTxtNumber(auxC); exit; end; - SignerAccount := FNode.GetAccount(auxC); + SignerAccount := FNode.GetMempoolAccount(auxC); if (TAccountComp.IsAccountLocked(SignerAccount.accountInfo,FNode.Bank.BlocksCount)) then begin errors := 'Signer account '+TAccountComp.AccountNumberToAccountTxtNumber(SignerAccount.account)+' is locked until block '+IntToStr(SignerAccount.accountInfo.locked_until_block); exit; @@ -998,18 +1503,21 @@ function TFRMOperation.UpdateOpDelist(const TargetAccount : TAccount; var Signer end; end; -function TFRMOperation.UpdateOperationOptions(var errors : AnsiString) : Boolean; +function TFRMOperation.UpdateOperationOptions(var errors : String) : Boolean; Var iWallet,iAcc : Integer; wk : TWalletKey; - e : AnsiString; + e : String; sender_account,dest_account,seller_account, account_to_buy, signer_account : TAccount; publicKey : TAccountKey; + LHashLock : T32Bytes; salePrice, amount : Int64; auxC : Cardinal; - changeName,changeType : Boolean; - newName : TRawBytes; + changeName,changeType, LRecipientSigned, LChangeAccountData : Boolean; + newName, LNewAccountData : TRawBytes; newType : Word; + LTargetEPASA : TEPasa; + LRequiresPurchase : Boolean; begin Result := false; sender_account := CT_Account_NUL; @@ -1031,6 +1539,13 @@ function TFRMOperation.UpdateOperationOptions(var errors : AnsiString) : Boolean for iAcc := 0 to SenderAccounts.Count - 1 do begin sender_account := TNode.Node.Bank.SafeBox.Account(SenderAccounts.Get(iAcc)); iWallet := WalletKeys.IndexOfAccountKey(sender_account.accountInfo.accountKey); + if (iWallet<0) and ( + (TAccountComp.IsAccountForPrivateSale(sender_account.accountInfo)) or + (TAccountComp.IsAccountForAccountSwap(sender_account.accountInfo)) + ) + and (Not TAccountComp.IsNullAccountKey(sender_account.accountInfo.new_publicKey)) then begin + iWallet := WalletKeys.IndexOfAccountKey(sender_account.accountInfo.new_publicKey); + end; if (iWallet<0) then begin errors := 'Private key of account '+TAccountComp.AccountNumberToAccountTxtNumber(sender_account.account)+' not found in wallet'; lblGlobalErrors.Caption := errors; @@ -1038,7 +1553,7 @@ function TFRMOperation.UpdateOperationOptions(var errors : AnsiString) : Boolean end; wk := WalletKeys.Key[iWallet]; if not assigned(wk.PrivateKey) then begin - if wk.HasPrivateKey then begin + if Length(wk.CryptedKey)>0 then begin errors := 'Wallet is password protected. Need password'; bbPassword.Visible := true; bbPassword.Enabled := true; @@ -1054,7 +1569,7 @@ function TFRMOperation.UpdateOperationOptions(var errors : AnsiString) : Boolean Finally if lblGlobalErrors.Caption<>'' then begin tsGlobalError.visible := true; - tsGlobalError.tabvisible := {$IFDEF LINUX}true{$ELSE}false{$ENDIF}; + tsGlobalError.tabvisible := {$IFDEF unix}true{$ELSE}false{$ENDIF}; tsOperation.TabVisible := false; PageControlLocked.ActivePage := tsGlobalError; if bbPassword.CanFocus then begin @@ -1062,23 +1577,23 @@ function TFRMOperation.UpdateOperationOptions(var errors : AnsiString) : Boolean end; end else begin tsOperation.visible := true; - tsOperation.tabvisible := {$IFDEF LINUX}true{$ELSE}false{$ENDIF}; + tsOperation.tabvisible := {$IFDEF unix}true{$ELSE}false{$ENDIF}; tsGlobalError.TabVisible := false; PageControlLocked.ActivePage := tsOperation; end; End; if (PageControlOpType.ActivePage = tsTransaction) then begin - Result := UpdateOpTransaction(GetDefaultSenderAccount,dest_account,amount,errors); + Result := UpdateOpTransaction(GetDefaultSenderAccount,LTargetEPASA, dest_account, publicKey, LRequiresPurchase, amount,errors); end else if (PageControlOpType.ActivePage = tsChangePrivateKey) then begin Result := UpdateOpChangeKey(GetDefaultSenderAccount,signer_account,publicKey,errors); - end else if (PageControlOpType.ActivePage = tsListForSale) then begin - Result := UpdateOpListForSale(GetDefaultSenderAccount,salePrice,seller_account,signer_account,publicKey,auxC,errors); - end else if (PageControlOpType.ActivePage = tsDelist) then begin + end else if (PageControlOpType.ActivePage = tsListAccount) then begin + Result := UpdateOpListAccount(GetDefaultSenderAccount,salePrice,seller_account,signer_account,publicKey,auxC,LHashLock,errors); + end else if (PageControlOpType.ActivePage = tsDelistAccount) then begin Result := UpdateOpDelist(GetDefaultSenderAccount,signer_account,errors); end else if (PageControlOpType.ActivePage = tsBuyAccount) then begin - Result := UpdateOpBuyAccount(GetDefaultSenderAccount,account_to_buy,amount,publicKey,errors); + Result := UpdateOpBuyAccount(GetDefaultSenderAccount,account_to_buy,amount,publicKey,LRecipientSigned, errors); end else if (PageControlOpType.ActivePage = tsChangeInfo) then begin - Result := UpdateOpChangeInfo(GetDefaultSenderAccount,signer_account,changeName,newName,changeType,newType,errors); + Result := UpdateOpChangeInfo(GetDefaultSenderAccount,signer_account,changeName,newName,changeType,newType,LChangeAccountData,LNewAccountData,errors); end else begin errors := 'Must select an operation'; end; @@ -1088,7 +1603,7 @@ function TFRMOperation.UpdateOperationOptions(var errors : AnsiString) : Boolean end else if (PageControlOpType.ActivePage=tsChangePrivateKey) then begin rbEncryptedWithOldEC.Caption := 'Encrypted with old public key'; rbEncryptedWithEC.Caption := 'Encrypted with new public key'; - end else if ((PageControlOpType.ActivePage=tsListForSale) Or (PageControlOpType.ActivePage=tsDelist)) then begin + end else if ((PageControlOpType.ActivePage=tsListAccount) Or (PageControlOpType.ActivePage=tsDelistAccount)) then begin rbEncryptedWithOldEC.Caption := 'Encrypted with target public key'; rbEncryptedWithEC.Caption := 'Encrypted with signer public key'; end else if (PageControlOpType.ActivePage=tsBuyAccount) then begin @@ -1096,41 +1611,60 @@ function TFRMOperation.UpdateOperationOptions(var errors : AnsiString) : Boolean rbEncryptedWithEC.Caption := 'Encrypted with target public key'; end; ebSignerAccount.Enabled:= ((PageControlOpType.ActivePage=tsChangePrivateKey) And (FNode.Bank.SafeBox.CurrentProtocol>=CT_PROTOCOL_2)) - Or (PageControlOpType.ActivePage=tsChangeInfo) - Or (PageControlOpType.ActivePage=tsListForSale) - Or (PageControlOpType.ActivePage=tsDelist); + Or ((PageControlOpType.ActivePage=tsChangeInfo) And (SenderAccounts.Count=1)) + Or (PageControlOpType.ActivePage=tsListAccount) + Or (PageControlOpType.ActivePage=tsDelistAccount); sbSearchSignerAccount.Enabled:=ebSignerAccount.Enabled; lblSignerAccount.Enabled := ebSignerAccount.Enabled; - // - UpdatePayload(sender_account, e); + lblChangeName.Enabled:= (PageControlOpType.ActivePage=tsChangeInfo) And (SenderAccounts.Count=1); + ebChangeName.Enabled:= lblChangeName.Enabled; end; -function TFRMOperation.UpdateOpListForSale(const TargetAccount: TAccount; - var SalePrice: Int64; var SellerAccount, SignerAccount: TAccount; - var NewOwnerPublicKey: TAccountKey; var LockedUntilBlock: Cardinal; - var errors: AnsiString): Boolean; -var auxC : Cardinal; +function TFRMOperation.UpdateOpListAccount(const TargetAccount: TAccount; var SalePrice: Int64; var SellerAccount, SignerAccount: TAccount; var NewOwnerPublicKey: TAccountKey; var LockedUntilBlock: Cardinal; var HashLock : T32Bytes; var errors: String): Boolean; +var auxC : Cardinal; LBytes : TBytes; begin Result := False; SalePrice := 0; SellerAccount := CT_Account_NUL; NewOwnerPublicKey := CT_TECDSA_Public_Nul; LockedUntilBlock := 0; errors := ''; - if (PageControlOpType.ActivePage <> tsListForSale) then exit; + if (PageControlOpType.ActivePage <> tsListAccount) then exit; lblListAccountErrors.Caption := ''; Try if (rbListAccountForPublicSale.Checked) Or (rbListAccountForPrivateSale.Checked) then begin + {%region 'List Account Options'} + lblPrice.Visible := true; + lblPrice.Caption := 'Sale Price'; + lblPrice.Enabled := true; + ebPrice.Visible := true; + ebPrice.Enabled := true; + lblNewKey.Visible := true; + lblNewKey.Enabled := true; + lblNewKey.Caption := 'Buyer Key'; + ebNewKey.Visible := true; + ebNewKey.Enabled := true; + lblSeller.Visible := true; + lblSeller.Caption := 'Seller Account'; + ebSellerAccount.Visible := true; + sbSearchListerSellerAccount.Visible := true; + lblTimeLock.Visible := true; + ebTimeLock.Visible := true; + sbTimeLock.Visible := true; + lblHashLock.Visible := false; + ebHashLock.Visible := false; + btnHashLock.Visible := false; + btnHashLock.Enabled := false; if rbListAccountForPublicSale.Checked then begin - lblSaleNewOwnerPublicKey.Enabled := false; - ebSaleNewOwnerPublicKey.Enabled := false; - ebSaleLockedUntilBlock.Enabled := false; - lblSaleLockedUntilBlock.Enabled := false; + lblNewKey.Enabled := false; + ebNewKey.Enabled := false; + ebTimeLock.Enabled := false; + lblTimeLock.Enabled := false; end else if rbListAccountForPrivateSale.Checked then begin - lblSaleNewOwnerPublicKey.Enabled := true; - ebSaleNewOwnerPublicKey.Enabled := true; - ebSaleLockedUntilBlock.Enabled := true; - lblSaleLockedUntilBlock.Enabled := true; + lblNewKey.Enabled := true; + ebNewKey.Enabled := true; + ebTimeLock.Enabled := true; + lblTimeLock.Enabled := true; end; - if not TAccountComp.TxtToMoney(ebSalePrice.Text,salePrice) then begin + if not TAccountComp.TxtToMoney(ebPrice.Text,salePrice) then begin errors := 'Invalid price'; exit; end; @@ -1143,7 +1677,7 @@ function TFRMOperation.UpdateOpListForSale(const TargetAccount: TAccount; errors := 'Signer account does not exists '+TAccountComp.AccountNumberToAccountTxtNumber(auxC); exit; end; - SignerAccount := FNode.GetAccount(auxC); + SignerAccount := FNode.GetMempoolAccount(auxC); // Seller If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSellerAccount.Text,auxC) then begin errors := 'Invalid seller account'; @@ -1158,13 +1692,15 @@ function TFRMOperation.UpdateOpListForSale(const TargetAccount: TAccount; exit; end; - SellerAccount := FNode.GetAccount(auxC); + SellerAccount := FNode.GetMempoolAccount(auxC); if rbListAccountForPrivateSale.Checked then begin - lblSaleNewOwnerPublicKey.Enabled := true; - ebSaleNewOwnerPublicKey.Enabled := true; - ebSaleLockedUntilBlock.Enabled := true; - lblSaleLockedUntilBlock.Enabled := true; - If Not TAccountComp.AccountKeyFromImport(ebSaleNewOwnerPublicKey.Text,NewOwnerPublicKey,errors) then begin + lblNewKey.Visible := true; + lblNewKey.Enabled := true; + ebNewKey.Visible := true; + ebNewKey.Enabled := true; + ebTimeLock.Enabled := true; + lblTimeLock.Enabled := true; + If Not TAccountComp.AccountKeyFromImport(ebNewKey.Text,NewOwnerPublicKey,errors) then begin errors := 'Public key: '+errors; exit; end else begin @@ -1175,7 +1711,7 @@ function TFRMOperation.UpdateOpListForSale(const TargetAccount: TAccount; errors := 'New public key for private sale is the same public key'; Exit; end; - LockedUntilBlock := StrToIntDef(ebSaleLockedUntilBlock.Text,0); + LockedUntilBlock := StrToIntDef(ebTimeLock.Text,0); if LockedUntilBlock=0 then begin errors := 'Insert locking block'; exit; @@ -1185,11 +1721,147 @@ function TFRMOperation.UpdateOpListForSale(const TargetAccount: TAccount; errors := 'This operation needs PROTOCOL 2 active'; exit; end; + {%endregion} + end else if (rbListAccountForAccountSwap.Checked) Or (rbListAccountForCoinSwap.Checked) then begin + {%region 'Atomic Swap Options'} + lblPrice.Visible := true; + ebPrice.Visible := true; + lblNewKey.Visible := true; + ebNewKey.Visible := true; + lblTimeLock.Visible := true; + lblTimeLock.Enabled := true; + ebTimeLock.Visible := true; + ebTimeLock.Enabled := true; + sbTimeLock.Visible := true; + sbTimeLock.Enabled := true; + lblHashLock.Visible := true; + ebHashLock.Visible := true; + btnHashLock.Visible := true; + btnHashLock.Enabled := true; + + if rbListAccountForAccountSwap.Checked then begin + lblSeller.Visible := False; + ebSellerAccount.Visible := False; + sbSearchListerSellerAccount.Visible := false; + lblNewKey.Caption := 'Counterparty Key'; + lblNewKey.Visible := true; + lblNewKey.Enabled := true; + ebNewKey.Visible := true; + ebNewKey.Enabled := true; + lblPrice.Visible := false; + lblPrice.Enabled := false; + ebPrice.Enabled := false; + ebPrice.Visible := false; + end else if rbListAccountForCoinSwap.Checked then begin + lblSeller.Visible := true; + lblSeller.Caption := 'Counterparty Account'; + ebSellerAccount.Visible := true; + sbSearchListerSellerAccount.Visible := true; + lblNewKey.Visible := false; + ebNewKey.Visible := false; + lblPrice.Visible := true; + lblPrice.Caption := 'Swap Amount'; + lblPrice.Enabled := true; + ebPrice.Visible := true; + ebPrice.Enabled := true; + end; + + // COMMON SWAP VALDATION + // V5 Check + If (FNode.Bank.SafeBox.CurrentProtocol= FNode.Bank.AccountsCount) then begin + errors := 'Signer account does not exists '+TAccountComp.AccountNumberToAccountTxtNumber(auxC); + exit; + end; + SignerAccount := FNode.GetMempoolAccount(auxC); + + // Time-Lock + LockedUntilBlock := StrToIntDef(ebTimeLock.Text,0); + if LockedUntilBlock=0 then begin + errors := 'Insert locking block'; + exit; + end; + + // Hash-Lock + HashLock := CT_HashLock_NUL; + if (NOT TCrypto.HexaToRaw(ebHashLock.Text, LBytes)) OR (Length(LBytes) <> 32) then begin + errors := 'Hash-Lock is required (32byte hexstring)'; + exit; + end; + HashLock := TBaseType.To32Bytes(LBytes); + + // Account Swap Validation + if rbListAccountForAccountSwap.Checked then begin + // Sale price is 0 + SalePrice := 0; + + // Counterparty key + If Not TAccountComp.AccountKeyFromImport(ebNewKey.Text,NewOwnerPublicKey,errors) then begin + errors := 'Counterparty Key: '+errors; + exit; + end else begin + lblListAccountErrors.Font.Color := clGreen; + lblListAccountErrors.Caption := 'New key type: '+TAccountComp.GetECInfoTxt(NewOwnerPublicKey.EC_OpenSSL_NID); + end; + if TAccountComp.EqualAccountKeys(NewOwnerPublicKey,TargetAccount.accountInfo.accountKey) then begin + errors := 'New public key for private sale is the same public key'; + Exit; + end; + + // Seller account is target account (but price is 0, never receives) + SellerAccount := TargetAccount; + end else if (rbListAccountForCoinSwap.Checked) then begin + // Coin Swap Validation + // Price + IF NOT TAccountComp.TxtToMoney(ebPrice.Text,salePrice) then begin + errors := 'Invalid swap amount'; + exit; + end; + + // Counterparty Account (Seller variable) + If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSellerAccount.Text,auxC) then begin + errors := 'Invalid counterparty account'; + exit; + end; + if (auxC<0) Or (auxC >= FNode.Bank.AccountsCount) then begin + errors := 'Counterparty account does not exists '+TAccountComp.AccountNumberToAccountTxtNumber(auxC); + exit; + end; + if (auxC=TargetAccount.account) then begin + errors := 'Counterparty account cannot be same account'; + exit; + end; + SellerAccount := FNode.GetMempoolAccount(auxC); + + // New Owner Public Key is the same public key + NewOwnerPublicKey := TargetAccount.accountInfo.accountKey; + end; + + {%endregion} end else begin - lblSaleNewOwnerPublicKey.Enabled := false; - ebSaleNewOwnerPublicKey.Enabled := false; - ebSaleLockedUntilBlock.Enabled := false; - lblSaleLockedUntilBlock.Enabled := false; + lblPrice.Visible := false; + ebPrice.Visible := false; + lblNewKey.Visible := false; + ebNewKey.Visible := false; + lblSeller.Visible := false; + ebSellerAccount.Visible := false; + sbSearchListerSellerAccount.Visible := false; + lblTimeLock.Visible := false; + ebTimeLock.Visible := false; + sbTimeLock.Visible := false; + lblHashLock.Visible := false; + ebHashLock.Visible := false; + btnHashLock.Visible := false; + btnHashLock.Enabled := false; errors := 'Select a sale type'; exit; end; @@ -1202,165 +1874,265 @@ function TFRMOperation.UpdateOpListForSale(const TargetAccount: TAccount; End; end; -function TFRMOperation.UpdateOpTransaction(const SenderAccount: TAccount; var DestAccount: TAccount; var amount: Int64; var errors: AnsiString): Boolean; -Var c : Cardinal; +function TFRMOperation.UpdateOpTransaction(const ASenderAccount: TAccount; out ATargetEPASA : TEPasa; out ATargetAccount : TAccount; out AResolvedTargetKey : TECDSA_Public; out ATargetRequiresPurchase : Boolean; out AAmount: Int64; out AErrors: String): Boolean; +Var + LResolvedAccountNo : Cardinal; + LPublicKey : TAccountKey; begin - Result := False; - errors := ''; + AErrors := ''; lblTransactionErrors.Caption := ''; if PageControlOpType.ActivePage<>tsTransaction then exit; - if not (TAccountComp.AccountTxtNumberToAccountNumber(ebDestAccount.Text,c)) then begin - errors := 'Invalid dest. account ('+ebDestAccount.Text+')'; - lblTransactionErrors.Caption := errors; - exit; + + if (Trim(ebDestAccount.Text)='') then begin + AErrors := 'Need a destintation or EPASA'; + lblTransactionErrors.Caption := AErrors; + Exit(False); end; - if (c<0) Or (c>=TNode.Node.Bank.AccountsCount) then begin - errors := 'Invalid dest. account ('+TAccountComp.AccountNumberToAccountTxtNumber(c)+')'; - lblTransactionErrors.Caption := errors; - exit; + + + if Not CaptureEPasa(ebDestAccount.Text,ATargetEPasa) then begin + AErrors := 'Invalid EPASA value: '+ebDestAccount.Text; + lblTransactionErrors.Caption := AErrors; + Exit(False); end; - DestAccount := TNode.Node.GetAccount(c); - if SenderAccounts.Count=1 then begin - if not TAccountComp.TxtToMoney(ebAmount.Text,amount) then begin - errors := 'Invalid amount ('+ebAmount.Text+')'; - lblTransactionErrors.Caption := errors; - exit; + + Result := TNode.Node.TryResolveEPASA(ATargetEPASA, LResolvedAccountNo, AResolvedTargetKey, ATargetRequiresPurchase, AErrors); + if NOT Result then begin + lblTransactionErrors.Caption := AErrors; + Exit(False); + end; + + // GUI Base58 protection: In order to prevent manual mistake, Base58 is only allowed when introducing + // a Public key, otherwise will need to use String ("") or Hexadecimal (0x..) input + if (ATargetEPASA.PayloadType.HasTrait(ptBase58Formatted)) then begin + if Not TAccountComp.AccountPublicKeyImport(ATargetEPASA.Payload,LPublicKey,AErrors) then begin + AErrors := 'Not a Base58 Public key: '+AErrors; + lblTransactionErrors.Caption := AErrors; + Exit(False); + end; + end; + + if LResolvedAccountNo <> CT_AccountNo_NUL then begin + ATargetAccount := TNode.Node.GetMempoolAccount(LResolvedAccountNo); + if ATargetAccount.account=ASenderAccount.account then begin + AErrors := 'Sender and dest account are the same'; + lblTransactionErrors.Caption := AErrors; + Exit(False); end; - end else amount := 0; // ALL BALANCE - if DestAccount.account=SenderAccount.account then begin - errors := 'Sender and dest account are the same'; - lblTransactionErrors.Caption := errors; - exit; end; + + if SenderAccounts.Count=1 then begin + if not TAccountComp.TxtToMoney(ebAmount.Text,AAmount) then begin + AErrors := 'Invalid amount ('+ebAmount.Text+')'; + lblTransactionErrors.Caption := AErrors; + Exit(False); + end; + end else AAmount := 0; // ALL BALANCE + + if (SenderAccounts.Count=1) then begin - if (SenderAccount.balance<(amount+FDefaultFee)) then begin - errors := 'Insufficient funds'; - lblTransactionErrors.Caption := errors; - exit; + if (ASenderAccount.balance<(AAmount+FDefaultFee)) then begin + AErrors := 'Insufficient funds'; + lblTransactionErrors.Caption := AErrors; + Exit(False); end; end; - Result := True; end; -function TFRMOperation.UpdatePayload(const SenderAccount: TAccount; - var errors: AnsiString): Boolean; -Var payload_u : AnsiString; - payload_encrypted : TRawBytes; - account : TAccount; - public_key : TAccountKey; - dest_account_number : Cardinal; +function TFRMOperation.UpdatePayload(const ASenderAccount: TAccount; var AErrors: String): Boolean; +Var + LUserPayloadString : String; + LEncryptedPayloadBytes : TRawBytes; + LAccount : TAccount; + LTargetEPASA : TEPasa; + LPublicKey : TAccountKey; + LAccountNumber : Cardinal; i : Integer; - valid : Boolean; - wk : TWalletKey; + LValid : Boolean; + LWalletKey : TWalletKey; + LPassword : String; + LPayloadBytes : TRawBytes; begin - valid := false; - payload_encrypted := Nil; - FOperationPayload.payload_raw := nil; - errors := 'Unknown error'; - payload_u := memoPayload.Lines.Text; + LValid := false; + LPayloadBytes := Nil; + LEncryptedPayloadBytes := Nil; + FEncodedPayload := CT_TOperationPayload_NUL; + LUserPayloadString := memoPayload.Lines.Text; + FEncodedPayload.payload_type := 0; // [ptNonDeterministic] + AErrors := 'Unknown error'; try - if (payload_u='') then begin - valid := true; - exit; + LTargetEPASA := TEPasa.Empty; + + If (PageControlOpType.ActivePage=tsTransaction) then begin + if NOT TEPasa.TryParse(ebDestAccount.Text,True, LTargetEPASA) then begin + AErrors := 'Indeterminable target'; + Exit(False); + end; end; - if (rbEncryptedWithOldEC.Checked) then begin + + if (LUserPayloadString='') then begin + LValid := True; + Exit(True); + end; + + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType - [ptPublic] - [ptRecipientKeyEncrypted] - [ptSenderKeyEncrypted] + - [ptPasswordEncrypted] - [ptAsciiFormatted] - [ptHexFormatted] - [ptBase58Formatted]; + + case cbPayloadDataInputType.ItemIndex of + 0 : begin + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType + [ptAsciiFormatted]; + LPayloadBytes := TEncoding.ASCII.GetBytes(LUserPayloadString); + end; + 1 : begin + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType + [ptHexFormatted]; + if Not THexEncoding.TryDecode(LUserPayloadString,LPayloadBytes) then begin + AErrors := 'Payload is not an Hexadecimal string'; + Exit(False); + end; + end; + 2 : begin + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType + [ptBase58Formatted]; + if Not TPascalBase58Encoding.TryDecode(LUserPayloadString,LPayloadBytes) then begin + AErrors := 'Payload is not a Base 58 string'; + Exit(False); + end; + + // GUI Base58 protection: In order to prevent manual mistake, Base58 is only allowed when introducing + // a Public key, otherwise will need to use String ("") or Hexadecimal (0x..) input + if Not TAccountComp.AccountPublicKeyImport(LUserPayloadString,LPublicKey,AErrors) then begin + AErrors := 'Not a Public key: '+AErrors; + Exit(False); + end; + end + else + end; + LTargetEPASA.Payload := LUserPayloadString; + + if (Length(LPayloadBytes)>0) and (rbEncryptedWithOldEC.Checked) then begin // Use sender - errors := 'Error encrypting'; - account := FNode.GetAccount(SenderAccount.account); - TPCEncryption.DoPascalCoinECIESEncrypt(account.accountInfo.accountKey,TEncoding.ANSI.GetBytes(payload_u), payload_encrypted); - valid := Length(payload_encrypted)>0; - end else if (rbEncryptedWithEC.Checked) then begin - errors := 'Error encrypting'; - if (PageControlOpType.ActivePage=tsTransaction) or (PageControlOpType.ActivePage=tsListForSale) or (PageControlOpType.ActivePage=tsDelist) + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType + [ptSenderKeyEncrypted]; + AErrors := 'Error encrypting by sender'; + LAccount := FNode.GetMempoolAccount(ASenderAccount.account); + TPCEncryption.DoPascalCoinECIESEncrypt(LAccount.accountInfo.accountKey,LPayloadBytes,LEncryptedPayloadBytes); + LValid := Length(LEncryptedPayloadBytes)>0; + end else if (Length(LPayloadBytes)>0) and (rbEncryptedWithEC.Checked) then begin + AErrors := 'Error encrypting by recipient'; + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType + [ptRecipientKeyEncrypted]; + if (PageControlOpType.ActivePage=tsTransaction) or (PageControlOpType.ActivePage=tsListAccount) or (PageControlOpType.ActivePage=tsDelistAccount) or (PageControlOpType.ActivePage=tsBuyAccount) then begin + // With dest public key If (PageControlOpType.ActivePage=tsTransaction) then begin - If Not TAccountComp.AccountTxtNumberToAccountNumber(ebDestAccount.Text,dest_account_number) then begin - errors := 'Invalid dest account number'; + If Not Self.FNode.TryResolveEPASA(LTargetEPASA, LAccountNumber) then begin + AErrors := 'Invalid dest account EPASA'; exit; end; - end else if (PageControlOpType.ActivePage=tsListForSale) then begin - If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSignerAccount.Text,dest_account_number) then begin - errors := 'Invalid signer account number'; + end else if (PageControlOpType.ActivePage=tsListAccount) then begin + If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSignerAccount.Text,LAccountNumber) then begin + AErrors := 'Invalid signer account number'; exit; end; - end else if (PageControlOpType.ActivePage=tsDelist) then begin - If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSignerAccount.Text,dest_account_number) then begin - errors := 'Invalid signer account number'; + end else if (PageControlOpType.ActivePage=tsDelistAccount) then begin + If Not TAccountComp.AccountTxtNumberToAccountNumber(ebSignerAccount.Text,LAccountNumber) then begin + AErrors := 'Invalid signer account number'; exit; end; end else if (PageControlOpType.ActivePage=tsBuyAccount) then begin - If Not TAccountComp.AccountTxtNumberToAccountNumber(ebAccountToBuy.Text,dest_account_number) then begin - errors := 'Invalid account to buy number'; + If Not TAccountComp.AccountTxtNumberToAccountNumber(ebAccountToBuy.Text,LAccountNumber) then begin + AErrors := 'Invalid account to buy number'; exit; end; end else begin - errors := 'ERROR DEV 20170512-1'; + AErrors := 'ERROR DEV 20170512-1'; exit; end; - if (dest_account_number<0) or (dest_account_number>=FNode.Bank.AccountsCount) then begin - errors := 'Invalid payload encrypted account number: '+TAccountComp.AccountNumberToAccountTxtNumber(dest_account_number); + if (LAccountNumber<0) or (LAccountNumber>=FNode.Bank.AccountsCount) then begin + AErrors := 'Invalid payload encrypted account number: '+TAccountComp.AccountNumberToAccountTxtNumber(LAccountNumber); exit; end; - account := FNode.GetAccount(dest_account_number); - TPCEncryption.DoPascalCoinECIESEncrypt(account.accountInfo.accountKey,TEncoding.ANSI.GetBytes(payload_u),payload_encrypted); - valid := Length(payload_encrypted)>0; + LAccount := FNode.GetMempoolAccount(LAccountNumber); + LPublicKey := LAccount.accountInfo.accountKey; + if PageControlOpType.ActivePage = tsBuyAccount then + begin + if cbBuyNewKey.ItemIndex < 0 then + begin + AErrors := 'Select the new owner key before encrypting its payload'; + Exit(False); + end; + i := PtrInt(cbBuyNewKey.Items.Objects[cbBuyNewKey.ItemIndex]); + if (i < 0) or (i >= WalletKeys.Count) then + begin + AErrors := 'The selected new owner key is unavailable'; + Exit(False); + end; + LPublicKey := WalletKeys.Key[i].AccountKey; + end; + TPCEncryption.DoPascalCoinECIESEncrypt(LPublicKey,LPayloadBytes,LEncryptedPayloadBytes); + LValid := Length(LEncryptedPayloadBytes)>0; end else if (PageControlOpType.ActivePage=tsChangePrivateKey) then begin if (rbChangeKeyWithAnother.Checked) then begin // With new key generated if (cbNewPrivateKey.ItemIndex>=0) then begin i := PtrInt(cbNewPrivateKey.Items.Objects[cbNewPrivateKey.ItemIndex]); - if (i>=0) then public_key := WalletKeys.Key[i].AccountKey; + if (i>=0) then LPublicKey := WalletKeys.Key[i].AccountKey; end else begin - errors := 'Must select a private key'; + AErrors := 'Must select a private key'; exit; end; end else if (rbChangeKeyTransferAccountToNewOwner.Checked) then begin - If Not TAccountComp.AccountKeyFromImport(ebNewPublicKey.Text,public_key,errors) then begin - errors := 'Public key: '+errors; + If Not TAccountComp.AccountKeyFromImport(ebNewPublicKey.Text,LPublicKey,AErrors) then begin + AErrors := 'Public key: '+AErrors; exit; end; end else begin - errors := 'Must select change type'; + AErrors := 'Must select change type'; exit; end; - if public_key.EC_OpenSSL_NID<>CT_Account_NUL.accountInfo.accountKey.EC_OpenSSL_NID then begin - TPCEncryption.DoPascalCoinECIESEncrypt(public_key,TEncoding.ANSI.GetBytes(payload_u),payload_encrypted); - valid := Length(payload_encrypted)>0; + if LPublicKey.EC_OpenSSL_NID<>CT_Account_NUL.accountInfo.accountKey.EC_OpenSSL_NID then begin + TPCEncryption.DoPascalCoinECIESEncrypt(LPublicKey,LPayloadBytes,LEncryptedPayloadBytes); + LValid := Length(LEncryptedPayloadBytes)>0; end else begin - valid := false; - errors := 'Selected private key is not valid to encode'; + LValid := false; + AErrors := 'Selected private key is not valid to encode'; exit; end; end else begin - errors := 'This operation does not allow this kind of payload'; + AErrors := 'This operation does not allow this kind of payload'; end; - end else if (rbEncrptedWithPassword.Checked) then begin - payload_encrypted := TPCEncryption.DoPascalCoinAESEncrypt(TEncoding.ANSI.GetBytes(payload_u),TEncoding.ANSI.GetBytes(ebEncryptPassword.Text)); - valid := Length(payload_encrypted)>0; - end else if (rbNotEncrypted.Checked) then begin - payload_encrypted := TEncoding.ANSI.GetBytes(payload_u); - valid := true; + end else if (Length(LPayloadBytes)>0) AND (rbEncrptedWithPassword.Checked) then begin + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType + [ptPasswordEncrypted]; + LPassword := ebEncryptPassword.Text; + LEncryptedPayloadBytes := TPCEncryption.DoPascalCoinAESEncrypt(LPayloadBytes,TEncoding.ANSI.GetBytes(LPassword)); + LTargetEPASA.Password := LPassword; + LValid := Length(LEncryptedPayloadBytes)>0; + end else if (Length(LPayloadBytes)>0) AND (rbNotEncrypted.Checked) then begin + LTargetEPASA.PayloadType := LTargetEPASA.PayloadType + [ptPublic]; + LEncryptedPayloadBytes := LPayloadBytes; + LValid := true; end else begin - errors := 'Must select an encryption option for payload'; + AErrors := 'Must select an encryption option for payload'; end; finally - if valid then begin - if length(payload_encrypted)>CT_MaxPayloadSize then begin - valid := false; - errors := 'Payload size is bigger than '+inttostr(CT_MaxPayloadSize)+' ('+Inttostr(length(payload_encrypted))+')'; + if LValid then begin + if length(LEncryptedPayloadBytes)>CT_MaxPayloadSize then begin + LValid := false; + AErrors := 'Payload size is bigger than '+inttostr(CT_MaxPayloadSize)+' ('+Inttostr(length(LEncryptedPayloadBytes))+')'; end; end; - if valid then begin + if LValid then begin lblEncryptionErrors.Caption := ''; - lblPayloadLength.Caption := Format('(%db -> %db)',[length(payload_u),length(payload_encrypted)]); + lblPayloadLength.Caption := Format('(%db -> %db)',[length(LTargetEPASA.Payload),length(LEncryptedPayloadBytes)]) end else begin - lblEncryptionErrors.Caption := errors; - lblPayloadLength.Caption := Format('(%db -> ?)',[length(payload_u)]); + if Trim(AErrors)='' then AErrors := 'Undefined'; + lblEncryptionErrors.Caption := AErrors; + lblPayloadLength.Caption := Format('(%db -> ?)',[length(LTargetEPASA.Payload)]) + end; + FEncodedPayload.payload_type := LTargetEPASA.PayloadType.ToProtocolValue; + FEncodedPayload.payload_raw := LEncryptedPayloadBytes; + Result := LValid; + if (LValid) And (Not FUpdating) then begin + ebDestAccount.Text := LTargetEPASA.ToString(False); end; - FOperationPayload.payload_raw := payload_encrypted; - - Result := valid; end; end; diff --git a/src/gui-experimental/UFRMOperationExplorer.dfm b/src/gui-experimental/UFRMOperationExplorer.dfm new file mode 100644 index 000000000..a55a01c97 --- /dev/null +++ b/src/gui-experimental/UFRMOperationExplorer.dfm @@ -0,0 +1,76 @@ +object FRMOperationExplorer: TFRMOperationExplorer + Left = 190 + Height = 424 + Top = 287 + Width = 864 + ActiveControl = dgOperationsExplorer + Caption = 'Operations Explorer' + ClientHeight = 424 + ClientWidth = 864 + Menu = OperationsExplorerMenu + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + Visible = False + object dgOperationsExplorer: TDrawGrid + Left = 0 + Height = 363 + Top = 41 + Width = 864 + Align = alClient + TabOrder = 0 + OnMouseDown = dgOperationsExplorerMouseDown + OnMouseUp = dgOperationsExplorerMouseUp + end + object Panel1: TPanel + Left = 0 + Height = 41 + Top = 0 + Width = 864 + Align = alTop + BevelOuter = bvNone + TabOrder = 1 + object Label2: TLabel + Left = 11 + Height = 15 + Top = 10 + Width = 112 + Caption = 'Filter by blocks range' + ParentColor = False + end + end + object ebFilterOperationsStartBlock: TEdit + Left = 125 + Height = 23 + Top = 7 + Width = 57 + OnExit = ebFilterOperationsAccountExit + OnKeyPress = ebFilterOperationsAccountKeyPress + TabOrder = 2 + end + object ebFilterOperationsEndBlock: TEdit + Left = 185 + Height = 23 + Top = 7 + Width = 57 + OnExit = ebFilterOperationsAccountExit + OnKeyPress = ebFilterOperationsAccountKeyPress + TabOrder = 3 + end + object OperationsExplorerMenu: TMainMenu + left = 560 + object miTools: TMenuItem + Caption = 'Tools' + object miFindOperationByOpHash: TMenuItem + Caption = 'Find Operation by OpHash' + ShortCut = 116 + OnClick = miFindOperationByOpHashClick + end + object miDecodePayload: TMenuItem + Caption = 'Decode Payload' + ShortCut = 113 + OnClick = miDecodePayloadClick + end + end + end +end diff --git a/src/gui-experimental/UFRMOperationExplorer.lfm b/src/gui-experimental/UFRMOperationExplorer.lfm index e7a9575fe..bf8366094 100644 --- a/src/gui-experimental/UFRMOperationExplorer.lfm +++ b/src/gui-experimental/UFRMOperationExplorer.lfm @@ -24,7 +24,8 @@ object FRMOperationExplorer: TFRMOperationExplorer TitleFont.Color = clWindowText TitleFont.Height = -11 TitleFont.Name = 'Tahoma' - OnClick = dgOperationsExplorerClick + OnMouseDown = dgOperationsExplorerMouseDown + OnMouseUp = dgOperationsExplorerMouseUp end object Panel1: TPanel Left = 0 diff --git a/src/gui-experimental/UFRMOperationExplorer.pas b/src/gui-experimental/UFRMOperationExplorer.pas index fa3757147..e78f93d95 100644 --- a/src/gui-experimental/UFRMOperationExplorer.pas +++ b/src/gui-experimental/UFRMOperationExplorer.pas @@ -16,14 +16,28 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Browses blockchain operations within a block range. Opens operation details + on a click without treating row scrolling as a request to open a dialog. } + +{ Public types in this unit + + Type | Purpose + ----------------------+------------------------------------------------------------------------- + TFRMOperationExplorer | Browses operation history and opens details for a clicked row. +} interface {$I ..\config.inc} uses - LCLIntf, LCLType, SysUtils, Controls, Forms, Dialogs, ExtCtrls, StdCtrls, Grids, Menus, Classes, + Types, + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + {$IFDEF FPC}LCLIntf,{$ENDIF} {$IFDEF FPC}LCLType,{$ENDIF} SysUtils, Controls, Forms, Dialogs, ExtCtrls, StdCtrls, Grids, Menus, Classes, UCommon.UI, UGridUtils, UConst; type @@ -40,7 +54,8 @@ TFRMOperationExplorer = class(TApplicationForm) miFindOperationByOpHash: TMenuItem; miTools: TMenuItem; Panel1: TPanel; - procedure dgOperationsExplorerClick(Sender: TObject); + procedure dgOperationsExplorerMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); + procedure dgOperationsExplorerMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure ebFilterOperationsAccountExit(Sender: TObject); procedure ebFilterOperationsAccountKeyPress(Sender: TObject; var Key: Char); procedure FormCreate(Sender: TObject); @@ -52,13 +67,22 @@ TFRMOperationExplorer = class(TApplicationForm) { private declarations } FUpdating : boolean; FOperationsExplorerGrid : TOperationsGrid; + FOperationClickPending: Boolean; + FMouseDownCell, FMouseDownPoint: TPoint; + FMouseDownTopRow: Integer; + function TryGetOperationCell(X, Y: Integer; out ACell: TPoint): Boolean; public { public declarations } end; implementation -{$r *.lfm} +// Keep Lazarus-only streamed properties out of VCL form creation. +{$IFDEF FPC} +{$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UUserInterface, UFRMPayloadDecoder, UBlockChain; @@ -111,9 +135,53 @@ procedure TFRMOperationExplorer.ebFilterOperationsAccountExit(Sender: TObject); End; end; -procedure TFRMOperationExplorer.dgOperationsExplorerClick(Sender: TObject); +function TFRMOperationExplorer.TryGetOperationCell(X, Y: Integer; out ACell: TPoint): Boolean; +begin + Result := False; + if (X < 0) or (Y < 0) or (X >= dgOperationsExplorer.ClientWidth) or + (Y >= dgOperationsExplorer.ClientHeight) then + Exit; + dgOperationsExplorer.MouseToCell(X, Y, ACell.X, ACell.Y); + Result := (ACell.X >= dgOperationsExplorer.FixedCols) and + (ACell.X < dgOperationsExplorer.ColCount) and + (ACell.Y >= dgOperationsExplorer.FixedRows) and (ACell.Y > 0) and + (ACell.Y < dgOperationsExplorer.RowCount) and + (ACell.Y <= FOperationsExplorerGrid.OperationsResume.Count); +end; + +procedure TFRMOperationExplorer.dgOperationsExplorerMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +begin + FOperationClickPending := False; + if Button <> mbLeft then + Exit; + if not TryGetOperationCell(X, Y, FMouseDownCell) then + Exit; + FMouseDownPoint := Point(X, Y); + FMouseDownTopRow := dgOperationsExplorer.TopRow; + FOperationClickPending := True; +end; + +procedure TFRMOperationExplorer.dgOperationsExplorerMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +var + LWasPending: Boolean; + LCell: TPoint; begin - TUserInterface.ShowOperationInfoDialog(Self, FOperationsExplorerGrid.SelectedOperation); + // VCL also raises Click when wheel/keyboard navigation changes the row. + // Only a matching left-button press/release may open a details dialog. + LWasPending := FOperationClickPending; + FOperationClickPending := False; + if (Button <> mbLeft) or not LWasPending then + Exit; + if not TryGetOperationCell(X, Y, LCell) then + Exit; + if (LCell.X <> FMouseDownCell.X) or (LCell.Y <> FMouseDownCell.Y) or + (dgOperationsExplorer.TopRow <> FMouseDownTopRow) or + (Abs(X - FMouseDownPoint.X) > GetSystemMetrics(SM_CXDRAG)) or + (Abs(Y - FMouseDownPoint.Y) > GetSystemMetrics(SM_CYDRAG)) then + Exit; + // Use the clicked data row; headers and blank space must not reuse an old selection. + TUserInterface.ShowOperationInfoDialog(Self, + FOperationsExplorerGrid.OperationsResume.Items[LCell.Y - 1]); end; procedure TFRMOperationExplorer.ebFilterOperationsAccountKeyPress(Sender: TObject; var Key: Char); diff --git a/src/gui-experimental/UFRMPascalCoinWalletConfig.dfm b/src/gui-experimental/UFRMPascalCoinWalletConfig.dfm new file mode 100644 index 000000000..9282dfac5 --- /dev/null +++ b/src/gui-experimental/UFRMPascalCoinWalletConfig.dfm @@ -0,0 +1,439 @@ +object FRMPascalCoinWalletConfig: TFRMPascalCoinWalletConfig + Left = -1385 + Height = 601 + Top = 29 + Width = 370 + ActiveControl = bbUpdatePassword + BorderIcons = [biSystemMenu] + BorderStyle = bsSingle + Caption = 'Options' + ClientHeight = 601 + ClientWidth = 370 + Color = clBtnFace + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + Visible = False + object Label1: TLabel + Left = 24 + Height = 13 + Top = 380 + Width = 120 + Caption = 'Default fee for operation' + ParentColor = False + end + object Label2: TLabel + Left = 24 + Height = 13 + Top = 116 + Width = 98 + Caption = 'Internet Server Port' + ParentColor = False + end + object lblDefaultInternetServerPort: TLabel + Left = 281 + Height = 13 + Top = 116 + Width = 70 + Caption = '(Default XXXX)' + ParentColor = False + end + object Label3: TLabel + Left = 15 + Height = 13 + Top = 67 + Width = 60 + Caption = 'Miner Name:' + ParentColor = False + end + object Label4: TLabel + Left = 90 + Height = 13 + Top = 90 + Width = 259 + Caption = 'This name will be included in each new block you mine!' + Font.CharSet = ANSI_CHARSET + Font.Color = clGray + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Pitch = fpVariable + Font.Quality = fqDraft + ParentColor = False + ParentFont = False + end + object Label5: TLabel + Left = 24 + Height = 13 + Top = 226 + Width = 73 + Caption = 'JSON-RPC Port' + ParentColor = False + end + object lblDefaultJSONRPCMinerServerPort: TLabel + Left = 281 + Height = 13 + Top = 226 + Width = 70 + Caption = '(Default XXXX)' + ParentColor = False + end + object cbJSONRPCMinerServerActive: TCheckBox + Left = 15 + Height = 19 + Top = 200 + Width = 225 + Caption = 'JSON-RPC Server Miner (TCP/IP, no HTTP)' + TabOrder = 6 + end + object ebDefaultFee: TEdit + Left = 297 + Height = 21 + Top = 376 + Width = 56 + Alignment = taRightJustify + TabOrder = 10 + Text = '0' + end + object cbSaveLogFiles: TCheckBox + Left = 15 + Height = 19 + Top = 440 + Width = 78 + Caption = 'Save log file' + OnClick = cbSaveLogFilesClick + TabOrder = 11 + end + object cbShowLogs: TCheckBox + Left = 222 + Height = 19 + Top = 440 + Width = 68 + Caption = 'Show logs' + TabOrder = 13 + end + object bbOk: TBitBtn + Left = 184 + Height = 30 + Top = 550 + Width = 75 + Kind = bkOK + ModalResult = 1 + OnClick = bbOkClick + TabOrder = 16 + Caption = 'OK' + end + object bbCancel: TBitBtn + Left = 278 + Height = 30 + Top = 550 + Width = 75 + Cancel = True + Kind = bkCancel + ModalResult = 2 + TabOrder = 17 + Caption = 'Cancel' + end + object udInternetServerPort: TUpDown + Left = 256 + Height = 21 + Top = 113 + Width = 16 + Associate = ebInternetServerPort + Max = 25000 + Min = 1 + Position = 4004 + TabOrder = 3 + Thousands = False + Wrap = False + end + object ebInternetServerPort: TEdit + Left = 200 + Height = 21 + Top = 113 + Width = 56 + Alignment = taRightJustify + TabOrder = 2 + Text = '4004' + end + object bbUpdatePassword: TBitBtn + Left = 15 + Height = 38 + Top = 14 + Width = 337 + Caption = 'Wallet Password' + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + Glyph.Data = { + 76060000424D7606000000000000360400002800000018000000180000000100 + 0800000000004002000000000000000000000001000000010000000000000101 + 0100020202000303030004040400050505000606060007070700080808000909 + 09000A0A0A000B0B0B000C0C0C000D0D0D000E0E0E000F0F0F00101010001111 + 1100121212001313130014141400151515001616160017171700181818001919 + 19001A1A1A001B1B1B001C1C1C001D1D1D001E1E1E001F1F1F00202020002121 + 2100222222002323230024242400252525002626260027272700282828002929 + 29002A2A2A002B2B2B002C2C2C002D2D2D002E2E2E002F2F2F00303030003131 + 3100323232003333330034343400353535003636360037373700383838003939 + 39003A3A3A003B3B3B003C3C3C003D3D3D003E3E3E003F3F3F00404040004141 + 4100424242004343430044444400454545004646460047474700484848004949 + 49004A4A4A004B4B4B004C4C4C004D4D4D004E4E4E004F4F4F00505050005151 + 5100525252005353530054545400555555005656560057575700585858005959 + 59005A5A5A005B5B5B005C5C5C005D5D5D005E5E5E00685968007C4F7C009441 + 9400B72CB700DD15DD00F506F500FD01FD00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00 + FE00FE00FE00FE00FE00FE00FE00F008F900D517F000BD24E800A830E1007B4B + D0005065C1003774B7002B7DB100237FAC001F82AD001C83AD001785AE001687 + AF001489B100128AB300108CB6000F8EB9000D91BB000C94BF000A97C3000A99 + C5000A9AC7000B9BC8000B9CCA000C9ECC000C9FCD000DA0CE000DA1CE000EA2 + CF0011A3CF0015A4CF0018A5CF001CA7D10021A9D1002AAAD00035AACD0035AD + D00035AFD30035B1D60037B5D8003AB8DB003EB9DC0040BBDD0045BEDE004CC1 + E00055C6E3005BC8E40061CAE40066CBE3006BCCE30072D3E60078D8EB0080D8 + EE0083D9F10085DCF40088DBF5008BDBF6008EDAF70091DAF70095DCF70097E0 + F7009EE3F800A4E7F900A9E9F900AEE9F900B5EBF900B2EBF800919191919191 + 919191919191919191919191919191919191919191919191D5E7E8E7E3DED591 + 919191919191919191919191919191D6DDF7F8F8F7F6F4EEE6D5919191919191 + 919191919191D7DADEF8F8F6F7F6EDF1F8F1E69191919191919191919191DADE + DFFAFAF8F7F7E9EDF7F7F8EB91919191919191919191DDE5E3FCFCFAFAEEE0E9 + F3F7F7F7DC919191919191919191DEE7E4FEFEFCFCEDC5E0EFF7F6F6DC919191 + 919191919191DEEAE7FEFEFEFDFBC4C4F5F7F6F6DC919191919191919191E3EC + E9FEFEFEFEFEE0C3F9F7F6F6DC919191919191919191E3E9E3F8F8F1F1F8FBFB + FCFAF7F6DC919191919191919191DCEAEAE0E1E9E5E3DEE4EBF2FAF9DC919191 + 919191919191DBF3EEC5C5EFF3EBE5DDD9D6E6F5DC91919191919191919191D5 + E8EDF1FAF6F0EAE4DAD1D2DBDC919191919191919191919191DBE8F0F3F3F3E4 + D0CFE4D8DC919191919191919191919191919191D5DEE4DED2CFD4D491919191 + 91919191919191919191919191919191D2CE9191919191919191919191919191 + 91C8C89191919191D2CE919191919191919191919191919191D9D09191919191 + D2CE919191919191919191919191919191E6E39191919191D2CE919191919191 + 919191919191919191E0F3CE91919191D2CE9191919191919191919191919191 + 9191EFF6E1C9C8CDD9CC91919191919191919191919191919191C9EDF9F3F3EB + DC91919191919191919191919191919191919191CBE0E1CF9191919191919191 + 9191919191919191919191919191919191919191919191919191 + } + OnClick = bbUpdatePasswordClick + ParentFont = False + TabOrder = 0 + end + object ebMinerName: TEdit + Left = 90 + Height = 21 + Top = 64 + Width = 261 + TabOrder = 1 + Text = 'ebMinerName' + end + object cbShowModalMessages: TCheckBox + Left = 224 + Height = 19 + Top = 464 + Width = 127 + Caption = 'Show modal messages' + TabOrder = 14 + end + object udJSONRPCMinerServerPort: TUpDown + Left = 256 + Height = 21 + Top = 223 + Width = 16 + Associate = ebJSONRPCMinerServerPort + Max = 25000 + Min = 1 + Position = 4009 + TabOrder = 8 + Thousands = False + Wrap = False + end + object ebJSONRPCMinerServerPort: TEdit + Left = 200 + Height = 21 + Top = 223 + Width = 56 + Alignment = taRightJustify + TabOrder = 7 + Text = '4009' + end + object gbMinerPrivateKey: TGroupBox + Left = 15 + Height = 121 + Top = 250 + Width = 338 + Caption = ' Miner Server Private Key: ' + TabOrder = 9 + object rbGenerateANewPrivateKeyEachBlock: TRadioButton + Left = 18 + Height = 19 + Top = 22 + Width = 277 + Caption = 'Generate a new private key for each generated block' + TabOrder = 0 + end + object rbUseARandomKey: TRadioButton + Left = 18 + Height = 19 + Top = 42 + Width = 146 + Caption = 'Use a random existing key' + TabOrder = 1 + end + object rbMineAllwaysWithThisKey: TRadioButton + Left = 18 + Height = 19 + Top = 63 + Width = 146 + Caption = 'Always mine with this key:' + TabOrder = 2 + end + object cbPrivateKeyToMine: TComboBox + Left = 43 + Height = 21 + Top = 87 + Width = 266 + ItemHeight = 13 + Style = csDropDownList + TabOrder = 3 + end + end + object cbSaveDebugLogs: TCheckBox + Left = 16 + Height = 19 + Top = 464 + Width = 118 + Caption = 'Save debug logs too' + TabOrder = 12 + end + object bbOpenDataFolder: TBitBtn + Left = 15 + Height = 30 + Top = 550 + Width = 131 + Caption = 'Open Data Folder' + Glyph.Data = { + 36040000424D3604000000000000360000002800000010000000100000000100 + 2000000000000004000064000000640000000000000000000000078DBE4D078D + BEFF078DBEFF078DBEFF078DBEFF078DBEFF078DBEFF078DBEFF078DBEFF078D + BEFF078DBEFF078DBEFF078DBEFF078DBEFFFFFFFF00FFFFFF00078DBEFF25A1 + D1FF70C6E7FF6BCFF9FF66CDF9FF65CDF9FF65CDF9FF65CDF9FF65CDF8FF65CD + F9FF65CDF8FF66CEF9FF39ADD8FF078DBEFF078DBE4DFFFFFF00078DBEFF4CBC + E7FF5EB8DAFF94DFFBFF6FD4FAFF6FD4F9FF6ED4FAFF6FD4F9FF6FD4FAFF6FD4 + FAFF6FD4FAFF6ED4F9FF3EB1D9FF84D7EBFF078DBEFFFFFFFF00078DBEFF72D6 + FAFF1593C2FFB6ECFDFF7DDDFBFF79DCFBFF79DCFBFF79DCFBFF79DCFBFF7ADC + FBFF79DCFAFF79DCFAFF44B5D9FFAEF1F9FF078DBEFFFFFFFF00078DBEFF79DD + FBFF1899C7FF94DDF3FFA2EBFCFF84E4FBFF83E4FCFF83E4FCFF84E4FCFF83E4 + FCFF83E4FBFF84E5FCFF48B9DAFFB3F4F9FF078DBEFF078DBE4D078DBEFF82E3 + FCFF43B7DCFF4BB9DBFFBFF4FDFF8EEBFCFF8DEBFCFF8DEBFDFF8DEBFDFF8DEB + FCFF8DEBFDFF8DEBFCFF4CBBDAFFB6F7F9FF6DCAE0FF078DBEFF078DBEFF8AEA + FCFF77DCF3FF1496C3FFFFFFFFFFC9F8FEFFC9F8FEFFC9F8FEFFC9F8FFFFC9F7 + FFFFC9F8FEFFC9F8FFFF9CD6E7FFDFFAFBFFDBF7FAFF078DBEFF078DBEFF93F0 + FEFF93F0FDFF1697C5FF078DBEFF078DBEFF078DBEFF078DBEFF078DBEFF078D + BEFF078DBEFF078DBEFF078DBEFF078DBEFF078DBEFF078DBEFF078DBEFF9BF5 + FEFF9AF6FEFF9AF6FEFF9BF5FDFF9BF6FEFF9AF6FEFF9BF5FEFF9AF6FDFF9BF5 + FEFF9AF6FEFF9AF6FEFF0989BAFFFFFFFF00FFFFFF00FFFFFF00078DBEFFFEFE + FEFFA0FBFFFFA0FBFEFFA0FBFEFFA1FAFEFFA1FBFEFFA0FAFEFFA1FBFEFFA1FB + FFFFA0FBFFFFA1FBFFFF0989BAFFFFFFFF00FFFFFF00FFFFFF00078DBE4D078D + BEFFFEFEFEFFA5FEFFFFA5FEFFFFA5FEFFFF078DBEFF078DBEFF078DBEFF078D + BEFF078DBEFF078DBEFFFFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00078D + BE4D078DBEFF078DBEFF078DBEFF078DBEFF078DBE4DFFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFF + FF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00FFFFFF00 + } + OnClick = bbOpenDataFolderClick + TabOrder = 15 + end + object cbJSONRPCPortEnabled: TCheckBox + Left = 15 + Height = 19 + Top = 136 + Width = 205 + Caption = 'JSON-RPC Server port enabled (HTTP)' + OnClick = cbJSONRPCPortEnabledClick + TabOrder = 4 + end + object Label6: TLabel + Left = 24 + Height = 13 + Top = 163 + Width = 57 + Caption = 'Allowed IP''s' + ParentColor = False + end + object ebJSONRPCAllowedIPs: TEdit + Left = 90 + Height = 21 + Top = 159 + Width = 261 + TabOrder = 5 + Text = 'ebJSONRPCAllowedIPs' + end + object Label7: TLabel + Left = 90 + Height = 13 + Top = 183 + Width = 209 + Caption = 'Ip''s separated by semicolon or empty for all' + Font.CharSet = ANSI_CHARSET + Font.Color = clGray + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Pitch = fpVariable + Font.Quality = fqDraft + ParentColor = False + ParentFont = False + end + object cbDownloadNewCheckpoint: TCheckBox + Left = 15 + Height = 19 + Top = 416 + Width = 271 + Caption = 'Download new Checkpoint if blockchain older than...' + OnClick = cbDownloadNewCheckpointClick + TabOrder = 18 + end + object ebMinFutureBlocksToDownloadNewSafebox: TEdit + Left = 298 + Height = 21 + Top = 416 + Width = 56 + Alignment = taRightJustify + TabOrder = 19 + Text = '0' + end + object lblInterfaceLanguage: TLabel + Left = 15 + Top = 502 + Caption = 'Language' + end + object cbInterfaceLanguage: TComboBox + Left = 90 + Top = 498 + Width = 263 + Height = 23 + Style = csDropDownList + TabOrder = 20 + OnChange = cbInterfaceLanguageChange + end + object lblLanguageRestart: TLabel + Left = 90 + Top = 526 + Caption = 'Takes effect after restarting PascalCoin.' + Visible = False + end +end diff --git a/src/gui-experimental/UFRMPascalCoinWalletConfig.lfm b/src/gui-experimental/UFRMPascalCoinWalletConfig.lfm index 8af600875..3410072f1 100644 --- a/src/gui-experimental/UFRMPascalCoinWalletConfig.lfm +++ b/src/gui-experimental/UFRMPascalCoinWalletConfig.lfm @@ -1,13 +1,13 @@ object FRMPascalCoinWalletConfig: TFRMPascalCoinWalletConfig Left = -1385 - Height = 551 + Height = 601 Top = 29 Width = 370 ActiveControl = bbUpdatePassword BorderIcons = [biSystemMenu] BorderStyle = bsSingle Caption = 'Options' - ClientHeight = 551 + ClientHeight = 601 ClientWidth = 370 Color = clBtnFace Font.Color = clWindowText @@ -118,7 +118,7 @@ object FRMPascalCoinWalletConfig: TFRMPascalCoinWalletConfig object bbOk: TBitBtn Left = 184 Height = 30 - Top = 500 + Top = 550 Width = 75 Kind = bkOK ModalResult = 1 @@ -129,7 +129,7 @@ object FRMPascalCoinWalletConfig: TFRMPascalCoinWalletConfig object bbCancel: TBitBtn Left = 278 Height = 30 - Top = 500 + Top = 550 Width = 75 Cancel = True Kind = bkCancel @@ -319,7 +319,7 @@ object FRMPascalCoinWalletConfig: TFRMPascalCoinWalletConfig object bbOpenDataFolder: TBitBtn Left = 15 Height = 30 - Top = 500 + Top = 550 Width = 131 Caption = 'Open Data Folder' Glyph.Data = { @@ -419,4 +419,24 @@ object FRMPascalCoinWalletConfig: TFRMPascalCoinWalletConfig TabOrder = 19 Text = '0' end + object lblInterfaceLanguage: TLabel + Left = 15 + Top = 502 + Caption = 'Language' + end + object cbInterfaceLanguage: TComboBox + Left = 90 + Top = 498 + Width = 263 + Height = 23 + Style = csDropDownList + TabOrder = 20 + OnChange = cbInterfaceLanguageChange + end + object lblLanguageRestart: TLabel + Left = 90 + Top = 526 + Caption = 'Takes effect after restarting PascalCoin.' + Visible = False + end end diff --git a/src/gui-experimental/UFRMPascalCoinWalletConfig.pas b/src/gui-experimental/UFRMPascalCoinWalletConfig.pas index ce28b7142..fbf24f978 100644 --- a/src/gui-experimental/UFRMPascalCoinWalletConfig.pas +++ b/src/gui-experimental/UFRMPascalCoinWalletConfig.pas @@ -16,14 +16,27 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Edits wallet, network, mining, fee, logging and language settings and saves + the selected configuration. } + +{ Public types in this unit + + Type | Purpose + ---------------------------+------------------------------------------------------------------------- + TFRMPascalCoinWalletConfig | Edits and saves wallet, network, mining and display settings. +} interface uses - LCLIntf, LCLType, LMessages, + {$IFNDEF FPC}Winapi.Windows, Winapi.ShellAPI,{$ENDIF} + {$IFDEF FPC}LCLIntf, LCLType, LMessages,{$ENDIF} Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, - Dialogs, StdCtrls, Buttons, ComCtrls, UCommon.UI; + Dialogs, StdCtrls, Buttons, ComCtrls, UCommon.UI, UWalletLanguages; const CM_PC_WalletKeysChanged = WM_USER + 1; @@ -66,6 +79,9 @@ TFRMPascalCoinWalletConfig = class(TApplicationForm) ebJSONRPCAllowedIPs: TEdit; Label6: TLabel; Label7: TLabel; + lblInterfaceLanguage: TLabel; + lblLanguageRestart: TLabel; + cbInterfaceLanguage: TComboBox; procedure cbDownloadNewCheckpointClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure bbOkClick(Sender: TObject); @@ -74,7 +90,11 @@ TFRMPascalCoinWalletConfig = class(TApplicationForm) procedure bbOpenDataFolderClick(Sender: TObject); procedure cbJSONRPCPortEnabledClick(Sender: TObject); procedure FormDestroy(Sender: TObject); + procedure cbInterfaceLanguageChange(Sender: TObject); private + FLanguages: TArray; + FNewUILanguage: String; + FLanguageChanged, FRefreshingLanguage: Boolean; procedure CM_WalletChanged(var Msg: TMessage); message CM_PC_WalletKeysChanged; procedure OnWalletChanged(Sender: TObject); procedure RefreshUI; @@ -86,7 +106,11 @@ implementation uses UConst, USettings, UAccounts, ULog, UCrypto, UFolderHelper, UWallet, UUserInterface, UCommon, UNode; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} procedure TFRMPascalCoinWalletConfig.bbOkClick(Sender: TObject); Var df : Int64; @@ -96,12 +120,14 @@ procedure TFRMPascalCoinWalletConfig.bbOkClick(Sender: TObject); if udInternetServerPort.Position = udJSONRPCMinerServerPort.Position then raise Exception.Create('Server port and JSON-RPC Server miner port are equal!'); if TAccountComp.TxtToMoney(ebDefaultFee.Text,df) then begin - TSettings.DefaultFee := df; + if (df < 0) or (df > CT_MaxTransactionFee) then + raise Exception.Create('The default fee is outside the protocol range'); + if (df = 0) and (not CT_AllowPropagate0feeOperations) then + raise Exception.Create('The default fee must be at least ' + TAccountComp.FormatMoney(CT_MOLINA)); end else begin ebDefaultFee.Text := TAccountComp.FormatMoney(TSettings.DefaultFee); raise Exception.Create('Invalid Fee value'); end; - TSettings.InternetServerPort := udInternetServerPort.Position; if rbGenerateANewPrivateKeyEachBlock.Checked then mpk := mpk_NewEachTime else if rbUseARandomKey.Checked then mpk := mpk_Random else if rbMineAllwaysWithThisKey.Checked then begin @@ -110,25 +136,31 @@ procedure TFRMPascalCoinWalletConfig.bbOkClick(Sender: TObject); i := PtrInt(cbPrivateKeyToMine.Items.Objects[cbPrivateKeyToMine.ItemIndex]); if (i<0) Or (i>=TWallet.Keys.Count) then raise Exception.Create('Invalid private key'); if NOT TWallet.Keys.Key[i].HasPrivateKey then raise Exception.Create('Cannot use key "' + TWallet.Keys.Key[i].Name + '" for mining since it is watch-only'); - TSettings.MinerSelectedPrivateKey := TAccountComp.AccountKey2RawString(TWallet.Keys.Key[i].AccountKey); end else mpk := mpk_Random; - if cbDownloadNewCheckpoint.Checked then begin + // Validate all inputs before mutating settings, whose parameter setters persist. + TSettings.DefaultFee := df; + TSettings.InternetServerPort := udInternetServerPort.Position; + if mpk = mpk_Selected then + TSettings.MinerSelectedPublicKey := TAccountComp.AccountKey2RawString(TWallet.Keys.Key[i].AccountKey); + if cbDownloadNewCheckpoint.Checked then begin i := StrToIntDef(ebMinFutureBlocksToDownloadNewSafebox.Text,0); TSettings.MinFutureBlocksToDownloadNewSafebox := i; TSettings.AllowDownloadNewCheckpointIfOlderThan := i > 200; end else TSettings.AllowDownloadNewCheckpointIfOlderThan := False; TSettings.MinerPrivateKeyType := mpk; - TSettings.MinerServerRpcActive := cbJSONRPCMinerServerActive.Checked; - TSettings.MinerServerRpcPort := udJSONRPCMinerServerPort.Position; + TSettings.JsonRpcMinerServerActive := cbJSONRPCMinerServerActive.Checked; + TSettings.JsonRpcMinerServerPort := udJSONRPCMinerServerPort.Position; TSettings.SaveLogFiles := cbSaveLogFiles.Checked; TSettings.ShowLogs := cbShowLogs.Checked; TSettings.SaveDebugLogs := cbSaveDebugLogs.Checked; TSettings.MinerName := ebMinerName.Text; TSettings.ShowModalMessages := cbShowModalMessages.Checked; - TSettings.RpcPortEnabled := cbJSONRPCPortEnabled.Checked; - TSettings.RpcAllowedIPs := ebJSONRPCAllowedIPs.Text; + TSettings.JsonRpcPortEnabled := cbJSONRPCPortEnabled.Checked; + TSettings.JsonRpcAllowedIPs := ebJSONRPCAllowedIPs.Text; + if FLanguageChanged then + TWalletLanguages.WritePreference(FNewUILanguage); TSettings.Save; ModalResult := MrOk; end; @@ -141,7 +173,11 @@ procedure TFRMPascalCoinWalletConfig.bbOpenDataFolderClick(Sender: TObject); // 1: TNode.GetPascalCoinDataFolder // 2. TFolderHelper.GetDataFolder(CT_PascalCoin_Data_Folder // I will try TNode first if this does not work use solution 2. + {$IFDEF FPC} OpenDocument(PChar(TNode.GetPascalCoinDataFolder)); + {$ELSE} + ShellExecute(Handle, 'open', PChar(TNode.GetPascalCoinDataFolder), nil, nil, SW_SHOWNORMAL); + {$ENDIF} end; procedure TFRMPascalCoinWalletConfig.bbUpdatePasswordClick(Sender: TObject); @@ -160,6 +196,8 @@ procedure TFRMPascalCoinWalletConfig.cbSaveLogFilesClick(Sender: TObject); end; procedure TFRMPascalCoinWalletConfig.FormCreate(Sender: TObject); +var + i: Integer; begin lblDefaultInternetServerPort.Caption := Format('(Default %d)',[CT_NetServer_Port]); udInternetServerPort.Position := CT_NetServer_Port; @@ -169,10 +207,32 @@ procedure TFRMPascalCoinWalletConfig.FormCreate(Sender: TObject); lblDefaultJSONRPCMinerServerPort.Caption := Format('(Default %d)',[CT_JSONRPCMinerServer_Port]); RefreshUI; TWallet.Keys.OnChanged.Add(OnWalletChanged); + FNewUILanguage := TWalletLanguages.ReadPreference; + FLanguages := TWalletLanguages.Available; + FRefreshingLanguage := true; + try + cbInterfaceLanguage.Items.Clear; + cbInterfaceLanguage.ItemIndex := -1; + for i := 0 to High(FLanguages) do begin + cbInterfaceLanguage.Items.Add(FLanguages[i].DisplayName); + if SameText(FLanguages[i].Code, FNewUILanguage) then + cbInterfaceLanguage.ItemIndex := i; + end; + cbInterfaceLanguage.Enabled := Length(FLanguages) > 1; + finally + FRefreshingLanguage := false; + end; +end; + +procedure TFRMPascalCoinWalletConfig.cbInterfaceLanguageChange(Sender: TObject); +begin + if FRefreshingLanguage or (cbInterfaceLanguage.ItemIndex < 0) then exit; + FNewUILanguage := FLanguages[cbInterfaceLanguage.ItemIndex].Code; + FLanguageChanged := not SameText(FNewUILanguage, TWalletLanguages.ReadPreference); + lblLanguageRestart.Visible := FLanguageChanged; end; -procedure TFRMPascalCoinWalletConfig.cbDownloadNewCheckpointClick( - Sender: TObject); +procedure TFRMPascalCoinWalletConfig.cbDownloadNewCheckpointClick( Sender: TObject); begin RefreshUI_WalletAspect; end; @@ -199,7 +259,7 @@ procedure TFRMPascalCoinWalletConfig.RefreshUI; Try udInternetServerPort.Position := TSettings.InternetServerPort; ebDefaultFee.Text := TAccountComp.FormatMoney(TSettings.DefaultFee); - cbJSONRPCMinerServerActive.Checked := TSettings.MinerServerRpcActive; + cbJSONRPCMinerServerActive.Checked := TSettings.JsonRpcMinerServerActive; case TSettings.MinerPrivateKeyType of mpk_NewEachTime : rbGenerateANewPrivateKeyEachBlock.Checked := true; mpk_Random : rbUseARandomKey.Checked := true; @@ -212,9 +272,9 @@ procedure TFRMPascalCoinWalletConfig.RefreshUI; cbSaveDebugLogs.Checked := TSettings.SaveDebugLogs; ebMinerName.Text := TSettings.MinerName; cbShowModalMessages.Checked := TSettings.ShowModalMessages; - udJSONRPCMinerServerPort.Position := TSettings.MinerServerRpcPort; - cbJSONRPCPortEnabled.Checked := TSettings.RpcPortEnabled; - ebJSONRPCAllowedIPs.Text := TSettings.RpcAllowedIPs; + udJSONRPCMinerServerPort.Position := TSettings.JsonRpcMinerServerPort; + cbJSONRPCPortEnabled.Checked := TSettings.JsonRpcPortEnabled; + ebJSONRPCAllowedIPs.Text := TSettings.JsonRpcAllowedIPs; cbDownloadNewCheckpoint.Checked := TSettings.AllowDownloadNewCheckpointIfOlderThan; ebMinFutureBlocksToDownloadNewSafebox.Text := IntToStr(TSettings.MinFutureBlocksToDownloadNewSafebox); Except @@ -249,7 +309,7 @@ procedure TFRMPascalCoinWalletConfig.RefreshUI_WalletAspect; end; end; cbPrivateKeyToMine.Sorted := true; - raw := TSettings.MinerSelectedPrivateKey; + raw := TSettings.MinerSelectedPublicKey; iselected := TWallet.Keys.IndexOfAccountKey(TAccountComp.RawString2Accountkey(raw)); if iselected >= 0 then begin iselected := cbPrivateKeyToMine.Items.IndexOfObject(TObject(iselected)); diff --git a/src/gui-experimental/UFRMPayloadDecoder.dfm b/src/gui-experimental/UFRMPayloadDecoder.dfm new file mode 100644 index 000000000..682baf600 --- /dev/null +++ b/src/gui-experimental/UFRMPayloadDecoder.dfm @@ -0,0 +1,431 @@ +object FRMPayloadDecoder: TFRMPayloadDecoder + Left = 344 + Height = 406 + Top = 184 + Width = 674 + ActiveControl = ebOphash + BorderIcons = [biSystemMenu] + BorderStyle = bsSingle + Caption = 'Operation Information/Decoder' + ClientHeight = 406 + ClientWidth = 674 + Color = clBtnFace + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + OnCreate = FormCreate + Position = poOwnerFormCenter + object Label1: TLabel + Left = 20 + Height = 13 + Top = 56 + Width = 46 + Caption = 'Block/Op:' + ParentColor = False + end + object lblBlock: TLabel + Left = 72 + Height = 19 + Top = 51 + Width = 109 + Caption = '000000/0000' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblDateTime: TLabel + Left = 280 + Height = 19 + Top = 51 + Width = 30 + Caption = '000' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object Label6: TLabel + Left = 220 + Height = 13 + Top = 56 + Width = 52 + Caption = 'Date Time:' + ParentColor = False + end + object Label2: TLabel + Left = 20 + Height = 13 + Top = 106 + Width = 52 + Caption = 'Operation:' + ParentColor = False + end + object lblOperationTxt: TLabel + Left = 90 + Height = 22 + Top = 101 + Width = 561 + AutoSize = False + Caption = '000' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + WordWrap = True + end + object lblAmountCaption: TLabel + Left = 220 + Height = 13 + Top = 81 + Width = 41 + Caption = 'Amount:' + ParentColor = False + end + object lblAmount: TLabel + Left = 270 + Height = 19 + Top = 76 + Width = 30 + Caption = '000' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblFeeCaption: TLabel + Left = 517 + Height = 13 + Top = 56 + Width = 22 + Caption = 'Fee:' + ParentColor = False + end + object lblFee: TLabel + Left = 547 + Height = 19 + Top = 51 + Width = 30 + Caption = '000' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object Label4: TLabel + Left = 20 + Height = 13 + Top = 24 + Width = 38 + Caption = 'OpHash' + ParentColor = False + end + object lblSenderCaption: TLabel + Left = 20 + Height = 13 + Top = 81 + Width = 38 + Caption = 'Sender:' + ParentColor = False + end + object lblSender: TLabel + Left = 64 + Height = 19 + Top = 76 + Width = 97 + Caption = '9999999-99' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblReceiverCaption: TLabel + Left = 411 + Height = 13 + Top = 81 + Width = 46 + Caption = 'Receiver:' + ParentColor = False + end + object lblReceiver: TLabel + Left = 463 + Height = 19 + Top = 76 + Width = 30 + Caption = '000' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblReceiverInfo: TLabel + Left = 481 + Height = 19 + Top = 50 + Width = 121 + Caption = '(Receiver info)' + Font.Color = clRed + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object PageControl: TPageControl + Left = 20 + Height = 206 + Top = 140 + Width = 631 + ActivePage = tsDecoded + TabIndex = 0 + TabOrder = 2 + OnChanging = PageControlChanging + object tsDecoded: TTabSheet + Caption = 'Payload' + object Label7: TLabel + Left = 15 + Height = 13 + Top = 94 + Width = 87 + Caption = 'Decoded Payload:' + ParentColor = False + end + object lblDecodedMethod: TLabel + Left = 192 + Height = 13 + Top = 94 + Width = 414 + Alignment = taRightJustify + AutoSize = False + Caption = '(Method)' + Font.Color = clGray + Font.Height = -11 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + end + object Label3: TLabel + Left = 15 + Height = 13 + Top = 9 + Width = 155 + Caption = 'Original Payload in Hexadecimal:' + ParentColor = False + end + object memoDecoded: TMemo + Left = 15 + Height = 55 + Top = 111 + Width = 591 + Font.Color = clBlack + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + Lines.Strings = ( + 'memoDecoded' + ) + OnKeyDown = memoDecodedKeyDown + ParentFont = False + ReadOnly = True + TabOrder = 0 + TabStop = False + end + object memoOriginalPayloadInHexa: TMemo + Left = 15 + Height = 55 + Top = 28 + Width = 591 + Color = clBtnFace + Font.Color = clBlack + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + Lines.Strings = ( + 'memoDecoded' + ) + OnKeyDown = memoDecodedKeyDown + ParentFont = False + ReadOnly = True + TabOrder = 1 + TabStop = False + end + object cbShowAsHexadecimal: TCheckBox + Left = 112 + Height = 19 + Top = 91 + Width = 122 + Caption = 'Show as hexadecimal' + OnClick = cbShowAsHexadecimalClick + TabOrder = 2 + end + end + object tsDecodeMethods: TTabSheet + Caption = 'Decode methods' + ImageIndex = 1 + object lblPasswordsInfo: TLabel + Left = 235 + Height = 13 + Top = 162 + Width = 81 + Caption = 'lblPasswordsInfo' + ParentColor = False + end + object cbMethodPublicPayload: TCheckBox + Left = 16 + Height = 17 + Top = 15 + Width = 225 + Caption = 'Not encrypted (Public payload)' + OnClick = cbMethodPublicPayloadClick + TabOrder = 0 + end + object cbUsingPrivateKeys: TCheckBox + Left = 16 + Height = 17 + Top = 38 + Width = 225 + Caption = 'Using Private keys of my Wallet' + OnClick = cbMethodPublicPayloadClick + TabOrder = 1 + end + object cbUsingPasswords: TCheckBox + Left = 16 + Height = 17 + Top = 61 + Width = 225 + Caption = 'Using Passwords of the list' + OnClick = cbMethodPublicPayloadClick + TabOrder = 2 + end + object memoPasswords: TMemo + Left = 235 + Height = 146 + Top = 10 + Width = 371 + OnChange = cbMethodPublicPayloadClick + ScrollBars = ssBoth + TabOrder = 3 + end + object bbSaveMethods: TBitBtn + Left = 60 + Height = 25 + Top = 110 + Width = 75 + Caption = 'Save' + Glyph.Data = { + DE010000424DDE01000000000000760000002800000024000000120000000100 + 0400000000006801000000000000000000001000000000000000000000000000 + 80000080000000808000800000008000800080800000C0C0C000808080000000 + FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333333333 + 3333333333333333333333330000333333333333333333333333F33333333333 + 00003333344333333333333333388F3333333333000033334224333333333333 + 338338F3333333330000333422224333333333333833338F3333333300003342 + 222224333333333383333338F3333333000034222A22224333333338F338F333 + 8F33333300003222A3A2224333333338F3838F338F33333300003A2A333A2224 + 33333338F83338F338F33333000033A33333A222433333338333338F338F3333 + 0000333333333A222433333333333338F338F33300003333333333A222433333 + 333333338F338F33000033333333333A222433333333333338F338F300003333 + 33333333A222433333333333338F338F00003333333333333A22433333333333 + 3338F38F000033333333333333A223333333333333338F830000333333333333 + 333A333333333333333338330000333333333333333333333333333333333333 + 0000 + } + NumGlyphs = 2 + OnClick = bbSaveMethodsClick + TabOrder = 4 + end + end + end + object bbClose: TBitBtn + Left = 572 + Height = 25 + Top = 362 + Width = 79 + Cancel = True + Caption = 'Close' + Glyph.Data = { + 76010000424D7601000000000000760000002800000020000000100000000100 + 04000000000000010000120B0000120B00001000000000000000000000000000 + 800000800000008080008000000080008000808000007F7F7F00BFBFBF000000 + FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00555555555555 + 555555555555555555555555555555555555555555FF55555555555559055555 + 55555555577FF5555555555599905555555555557777F5555555555599905555 + 555555557777FF5555555559999905555555555777777F555555559999990555 + 5555557777777FF5555557990599905555555777757777F55555790555599055 + 55557775555777FF5555555555599905555555555557777F5555555555559905 + 555555555555777FF5555555555559905555555555555777FF55555555555579 + 05555555555555777FF5555555555557905555555555555777FF555555555555 + 5990555555555555577755555555555555555555555555555555 + } + ModalResult = 2 + NumGlyphs = 2 + TabOrder = 3 + end + object bbFind: TBitBtn + Left = 599 + Height = 25 + Top = 19 + Width = 52 + Caption = '&Find' + Glyph.Data = { + 36030000424D3603000000000000360000002800000010000000100000000100 + 18000000000000030000120B0000120B00000000000000000000FF00FF4A667C + BE9596FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF6B9CC31E89E84B7AA3C89693FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF4BB4FE51B5FF + 2089E94B7AA2C69592FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF51B7FE51B3FF1D87E64E7AA0CA9792FF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + 51B7FE4EB2FF1F89E64E7BA2B99497FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF52B8FE4BB1FF2787D95F6A76FF + 00FFB0857FC09F94C09F96BC988EFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FF55BDFFB5D6EDBF9D92BB9B8CE7DAC2FFFFE3FFFFE5FDFADAD8C3 + B3B58D85FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFCEA795FD + EEBEFFFFD8FFFFDAFFFFDBFFFFE6FFFFFBEADDDCAE837FFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFC1A091FBDCA8FEF7D0FFFFDBFFFFE3FFFFF8FFFF + FDFFFFFDC6A99CFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFC1A091FEE3ACF1 + C491FCF2CAFFFFDDFFFFE4FFFFF7FFFFF7FFFFE9EEE5CBB9948CFF00FFFF00FF + FF00FFFF00FFFF00FFC2A191FFE6AEEEB581F7DCAEFEFDD8FFFFDFFFFFE3FFFF + E4FFFFE0F3ECD2BB968EFF00FFFF00FFFF00FFFF00FFFF00FFBC978CFBE7B7F4 + C791F2C994F8E5B9FEFCD8FFFFDDFFFFDCFFFFE0E2D2BAB68E86FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFD9C3A9FFFEE5F7DCB8F2C994F5D4A5FAE8BDFDF4 + C9FDFBD6B69089FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB58D85E8 + DEDDFFFEF2F9D8A3F4C48CF9D49FFDEAB8D0B49FB89086FF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFAD827FC9AA9EEFE0B7EFDFB2E7CEACB890 + 86B89086FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFBA968ABB988CB79188FF00FFFF00FFFF00FFFF00FF + } + OnClick = bbFindClick + TabOrder = 1 + TabStop = False + end + object ebOphash: TEdit + Left = 64 + Height = 22 + Top = 20 + Width = 529 + Font.Color = clWindowText + Font.Height = -12 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + OnExit = ebOphashExit + OnKeyPress = ebOphashKeyPress + ParentFont = False + TabOrder = 0 + Text = '12345678901234567890123456789012345678901234567890123456789012' + end +end diff --git a/src/gui-experimental/UFRMPayloadDecoder.pas b/src/gui-experimental/UFRMPayloadDecoder.pas index 82edb04b2..b249db7af 100644 --- a/src/gui-experimental/UFRMPayloadDecoder.pas +++ b/src/gui-experimental/UFRMPayloadDecoder.pas @@ -16,14 +16,27 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode Delphi} +{$ENDIF} + +{ Decodes operation payloads as text or bytes and attempts decryption with + wallet keys or a supplied password. } + +{ Public types in this unit + + Type | Purpose + -------------------+------------------------------------------------------------------------- + TFRMPayloadDecoder | Inspects and decrypts operation payload data. +} interface {$I ../config.inc} uses - LCLIntf, LCLType, LMessages, + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + {$IFDEF FPC}LCLIntf, LCLType, LMessages,{$ENDIF} Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, UCommon.UI, UBlockChain, UCrypto, UWallet, Buttons, ComCtrls,UAppParams; @@ -80,10 +93,20 @@ TFRMPayloadDecoder = class(TApplicationForm) FOpResume : TOperationResume; FSavedDecodeMethods : boolean; FSemaphor : Boolean; + FDetailsLayoutReady, FArrangingDetails: Boolean; { Private declarations } Procedure TryToDecode; Procedure SaveMethods; procedure SetOpResume(const Value: TOperationResume); + procedure LayoutDetails; + protected + procedure DoShow; override; + procedure Resize; override; + {$IFDEF FPC} + procedure ChangeScale(M, D: Integer); override; + {$ELSE} + procedure ChangeScale(M, D: Integer; isDpiChange: Boolean); override; + {$ENDIF} public { Public declarations } Procedure Init(Const AOperationResume : TOperationResume; WalletKeys : TWalletKeys; AppParams : TAppParams); overload; @@ -94,10 +117,14 @@ TFRMPayloadDecoder = class(TApplicationForm) implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} Uses - UNode, UTime, UAccounts, UCommon, USettings, UBaseTypes, + Math, Types, UNode, UTime, UAccounts, UCommon, USettings, UBaseTypes, UPCEncryption, UFRMMemoText; { TFRMPayloadDecoder } @@ -150,9 +177,9 @@ procedure TFRMPayloadDecoder.DoFind(Const OpHash : String); raise Exception.Create('Invalid OPHASH'); end; Case TNode.Node.FindNOperation(nBlock,nAccount,nN_Operation,opr) of - invalid_params : raise Exception.Create(Format('Not a valid OpHash searching at Block:%d Account:%d N_Operation:%d',[nBlock,nAccount,nN_Operation])); - blockchain_block_not_found : raise Exception.Create('Your blockchain file does not contain all blocks to find'); - found : ; + OpHash_invalid_params : raise Exception.Create(Format('Not a valid OpHash searching at Block:%d Account:%d N_Operation:%d',[nBlock,nAccount,nN_Operation])); + OpHash_block_not_found : raise Exception.Create('Your blockchain file does not contain all blocks to find'); + OpHash_found : ; else raise Exception.Create('ERROR DEV 20171120-6'); end; If (TPCOperation.EqualOperationHashes(opr.OperationHash,r)) Or @@ -218,6 +245,162 @@ procedure TFRMPayloadDecoder.FormCreate(Sender: TObject); finally FSemaphor := false; end; + FDetailsLayoutReady := True; + LayoutDetails; +end; + +procedure TFRMPayloadDecoder.DoShow; +begin + inherited; + LayoutDetails; +end; + +procedure TFRMPayloadDecoder.Resize; +begin + inherited; + LayoutDetails; +end; + +{$IFDEF FPC} +procedure TFRMPayloadDecoder.ChangeScale(M, D: Integer); +{$ELSE} +procedure TFRMPayloadDecoder.ChangeScale(M, D: Integer; isDpiChange: Boolean); +{$ENDIF} +var + LWasArranging: Boolean; +begin + LWasArranging := FArrangingDetails; + FArrangingDetails := True; + try + {$IFDEF FPC} + inherited ChangeScale(M, D); + {$ELSE} + inherited ChangeScale(M, D, isDpiChange); + {$ENDIF} + finally + FArrangingDetails := LWasArranging; + end; + // VCL scales inherited fonts after sizing its children. Layout from an + // intermediate Resize would retain bounds measured with the previous font. + LayoutDetails; +end; + +procedure TFRMPayloadDecoder.LayoutDetails; +var + LFontHeight, LMargin, LGap, LPairGap, LLeft, LRight, LTop, LRowHeight: Integer; + LInputHeight, LFindWidth, LHashWidth, LPageHeight, LButtonHeight, LHeight: Integer; + + function S(AValue: Integer): Integer; + begin + Result := MulDiv(AValue, LFontHeight, 13); + end; + + function TextWidth(ALabel: TLabel): Integer; + begin + Canvas.Font.Assign(ALabel.Font); + Result := Canvas.TextWidth(ALabel.Caption); + end; + + function LabelHeight(ALabel: TLabel; AWidth: Integer): Integer; + var + LText: TRect; + begin + Canvas.Font.Assign(ALabel.Font); + LText := Types.Rect(0, 0, Max(1, AWidth), 0); + DrawText(Canvas.Handle, PChar(ALabel.Caption), Length(ALabel.Caption), LText, DT_CALCRECT or DT_WORDBREAK or DT_NOPREFIX); + Result := Max(Canvas.TextHeight('Wg'), LText.Bottom - LText.Top); + end; + + procedure NextRow; + begin + if LRowHeight > 0 then + Inc(LTop, LRowHeight + LGap); + LLeft := LMargin; + LRowHeight := 0; + end; + + procedure AddPair(ACaption, AValue: TLabel); + var + LCaptionWidth, LValueWidth, LCaptionHeight, LValueHeight, LPairWidth, LPairHeight: Integer; + begin + if not AValue.Visible then + Exit; + LCaptionWidth := 0; + if Assigned(ACaption) then + LCaptionWidth := TextWidth(ACaption) + LGap; + LValueWidth := Max(1, TextWidth(AValue)); + LPairWidth := LCaptionWidth + LValueWidth; + if (LLeft > LMargin) and (LLeft + LPairWidth > LRight) then + NextRow; + LValueWidth := Min(LValueWidth, Max(1, LRight - LLeft - LCaptionWidth)); + LValueHeight := LabelHeight(AValue, LValueWidth); + LCaptionHeight := 0; + if Assigned(ACaption) then + LCaptionHeight := LabelHeight(ACaption, Max(1, LCaptionWidth - LGap)); + LPairHeight := Max(LCaptionHeight, LValueHeight); + if Assigned(ACaption) then begin + ACaption.AutoSize := False; + ACaption.WordWrap := True; + ACaption.SetBounds(LLeft, LTop + (LPairHeight - LCaptionHeight) div 2, Max(1, LCaptionWidth - LGap), LCaptionHeight); + end; + AValue.AutoSize := False; + AValue.WordWrap := True; + AValue.SetBounds(LLeft + LCaptionWidth, LTop + (LPairHeight - LValueHeight) div 2, LValueWidth, LValueHeight); + LLeft := LLeft + LCaptionWidth + LValueWidth + LPairGap; + LRowHeight := Max(LRowHeight, LPairHeight); + end; + +begin + if not FDetailsLayoutReady or FArrangingDetails or (csLoading in ComponentState) then + Exit; + FArrangingDetails := True; + DisableAlign; + try + Canvas.Font.Assign(Font); + LFontHeight := Max(1, Canvas.TextHeight('Wg')); + LMargin := S(20); + LGap := Max(4, S(8)); + LPairGap := S(20); + LRight := Max(LMargin + 1, ClientWidth - LMargin); + LTop := S(20); + LInputHeight := Max(bbFind.Height, ebOphash.Height); + Canvas.Font.Assign(bbFind.Font); + LFindWidth := Max(S(52), Canvas.TextWidth(StringReplace(bbFind.Caption, '&', '', [rfReplaceAll])) + S(28)); + LHashWidth := TextWidth(Label4); + Label4.AutoSize := False; + Label4.SetBounds(LMargin, LTop + (LInputHeight - LabelHeight(Label4, LHashWidth)) div 2, LHashWidth, LabelHeight(Label4, LHashWidth)); + bbFind.SetBounds(LRight - LFindWidth, LTop, LFindWidth, LInputHeight); + ebOphash.SetBounds(LMargin + LHashWidth + LGap, LTop + (LInputHeight - ebOphash.Height) div 2, Max(1, bbFind.Left - LGap - LMargin - LHashWidth - LGap), ebOphash.Height); + Inc(LTop, LInputHeight + LGap); + LLeft := LMargin; + LRowHeight := 0; + // Measure complete caption/value pairs: block counters, translated dates + // and monetary values can outgrow their old fixed designer columns. + AddPair(Label1, lblBlock); + AddPair(Label6, lblDateTime); + AddPair(lblFeeCaption, lblFee); + AddPair(nil, lblReceiverInfo); + NextRow; + AddPair(lblSenderCaption, lblSender); + AddPair(lblAmountCaption, lblAmount); + AddPair(lblReceiverCaption, lblReceiver); + NextRow; + AddPair(Label2, lblOperationTxt); + NextRow; + LPageHeight := S(206); + LButtonHeight := Max(S(25), bbClose.Height); + LHeight := LTop + LGap + LPageHeight + S(16) + LButtonHeight + LMargin; + // Grow only when metadata needs more room; retain a larger window and + // give its extra space to the payload rather than undoing a resize. + if ClientHeight < Max(S(406), LHeight) then + ClientHeight := Max(S(406), LHeight); + LPageHeight := Max(LPageHeight, ClientHeight - LTop - LGap - S(16) - LButtonHeight - LMargin); + PageControl.SetBounds(LMargin, LTop + LGap, LRight - LMargin, LPageHeight); + bbClose.SetBounds(LRight - S(79), ClientHeight - LMargin - LButtonHeight, S(79), LButtonHeight); + finally + EnableAlign; + FArrangingDetails := False; + end; end; // Needed for classic compat @@ -234,8 +417,7 @@ procedure TFRMPayloadDecoder.Init(Const AOperationResume : TOperationResume); TryToDecode; end; -procedure TFRMPayloadDecoder.memoDecodedKeyDown(Sender: TObject; var Key: Word; - Shift: TShiftState); +procedure TFRMPayloadDecoder.memoDecodedKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin if key=VK_ESCAPE then Close; @@ -341,6 +523,7 @@ procedure TFRMPayloadDecoder.SetOpResume(const Value: TOperationResume); TryToDecode; Finally FSemaphor := sem; + LayoutDetails; End; end; @@ -363,7 +546,7 @@ procedure TFRMPayloadDecoder.TryToDecode; end; - Function UsePassword(const raw : TRawBytes; var Decrypted : TRawBytes; var PasswordUsed : AnsiString) : Boolean; + Function UsePassword(const raw : TRawBytes; var Decrypted : TRawBytes; var PasswordUsed : String) : Boolean; Var i : Integer; Begin Result := false; @@ -382,7 +565,7 @@ procedure TFRMPayloadDecoder.TryToDecode; Var raw : TRawBytes; WalletKey : TWalletKey; Decrypted : TRawBytes; - PasswordUsed : AnsiString; + PasswordUsed : String; ok : boolean; begin ok := true; diff --git a/src/gui-experimental/UFRMPendingOperations.dfm b/src/gui-experimental/UFRMPendingOperations.dfm new file mode 100644 index 000000000..88f40ec19 --- /dev/null +++ b/src/gui-experimental/UFRMPendingOperations.dfm @@ -0,0 +1,65 @@ +object FRMPendingOperations: TFRMPendingOperations + Left = 156 + Height = 337 + Top = 188 + Width = 860 + ActiveControl = dgPendingOperations + Caption = 'Pending Operations' + ClientHeight = 337 + ClientWidth = 860 + Menu = PendingOperationsMenu + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + Visible = False + object pnlPendingOperations: TPanel + Left = 0 + Height = 86 + Top = 0 + Width = 860 + Align = alTop + BevelOuter = bvNone + BorderWidth = 10 + TabOrder = 0 + object Label10: TLabel + Left = 10 + Height = 66 + Top = 10 + Width = 840 + Align = alClient + AutoSize = False + Caption = 'Here you can see Operations transmited/received from other nodes that will be included in next block. There is no guarantee that other nodes will include them when mining, so it''s important that you mine too to help include Operations to the main BlockChain' + Font.Color = clWindowText + Font.Height = -16 + Font.Name = 'Tahoma' + ParentColor = False + ParentFont = False + WordWrap = True + end + end + object dgPendingOperations: TDrawGrid + Left = 0 + Height = 231 + Top = 86 + Width = 860 + Align = alClient + TabOrder = 1 + OnDblClick = dgPendingOperationsDblClick + end + object PendingOperationsMenu: TMainMenu + left = 696 + top = 48 + object miTools: TMenuItem + Caption = 'Tools' + object miFindOperationbyOpHash: TMenuItem + Caption = 'Find Operation by OpHash' + ShortCut = 116 + OnClick = miFindOperationbyOpHashClick + end + object miDecodePayLoad: TMenuItem + Caption = 'Decode Payload' + OnClick = miDecodePayLoadClick + end + end + end +end diff --git a/src/gui-experimental/UFRMPendingOperations.pas b/src/gui-experimental/UFRMPendingOperations.pas index 7187736a8..ef2350318 100644 --- a/src/gui-experimental/UFRMPendingOperations.pas +++ b/src/gui-experimental/UFRMPendingOperations.pas @@ -16,7 +16,19 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Displays the node's pending operations and opens details for a selected + operation. } + +{ Public types in this unit + + Type | Purpose + ----------------------+------------------------------------------------------------------------- + TFRMPendingOperations | Lists pending operations and their details. +} interface @@ -57,7 +69,11 @@ implementation uses UUserInterface, UBlockChain; +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TFRMPendingOperations } @@ -82,7 +98,7 @@ procedure TFRMPendingOperations.miDecodePayLoadClick(Sender: TObject); end; procedure TFRMPendingOperations.miFindOperationbyOpHashClick(Sender: TObject); -var ophash:AnsiString; +var ophash:String; begin if Not TUserInterface.AskEnterString(Self, 'Search operation by OpHash','Insert Operation Hash value (OpHash)',ophash) then exit; diff --git a/src/gui-experimental/UFRMRandomOperations.dfm b/src/gui-experimental/UFRMRandomOperations.dfm new file mode 100644 index 000000000..f497c4991 --- /dev/null +++ b/src/gui-experimental/UFRMRandomOperations.dfm @@ -0,0 +1,81 @@ +object FRMRandomOperations: TFRMRandomOperations + Left = 876 + Height = 229 + Top = 391 + Width = 439 + Caption = 'Random Operations' + ClientHeight = 229 + ClientWidth = 439 + OnClose = FormClose + OnCloseQuery = FormCloseQuery + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + Visible = False + object pnlTop: TPanel + Left = 0 + Height = 66 + Top = 0 + Width = 439 + Align = alTop + BevelOuter = bvNone + BorderWidth = 10 + TabOrder = 0 + object lblTopCaption: TLabel + Left = 10 + Height = 46 + Top = 10 + Width = 419 + Align = alClient + AutoSize = False + Caption = 'WARNING! This form will generate RANDOM operations using your private keys!!!' + Font.Color = clRed + Font.Height = -13 + Font.Style = [fsBold] + Layout = tlCenter + ParentColor = False + ParentFont = False + WordWrap = True + end + end + object pnlClient: TPanel + Left = 0 + Height = 163 + Top = 66 + Width = 439 + Align = alClient + BevelOuter = bvNone + TabOrder = 1 + object pnlTop1: TPanel + Left = 0 + Height = 42 + Top = 0 + Width = 439 + Align = alTop + BevelOuter = bvNone + TabOrder = 0 + object bbRandomOperations: TButton + Left = 16 + Height = 25 + Top = 8 + Width = 195 + Caption = 'Random Operations' + OnClick = bbRandomOperationsClick + TabOrder = 0 + end + end + object mLogs: TMemo + Left = 0 + Height = 121 + Top = 42 + Width = 439 + Align = alClient + Lines.Strings = ( + 'mLogs' + ) + ScrollBars = ssBoth + TabOrder = 1 + WordWrap = False + end + end +end diff --git a/src/gui-experimental/UFRMRandomOperations.pas b/src/gui-experimental/UFRMRandomOperations.pas index c68f4ccfa..a75a91ea3 100644 --- a/src/gui-experimental/UFRMRandomOperations.pas +++ b/src/gui-experimental/UFRMRandomOperations.pas @@ -17,6 +17,18 @@ } +{ Generates randomized transactions and multioperation batches for node + testing. The form controls the worker and reports generation and execution + results. } + +{ Public types in this unit + + Type | Purpose + -------------------------+------------------------------------------------------------------------- + TFRMRandomOperations | Controls random-operation generation and displays its results. + TRandomGenerateOperation | Builds randomized operations from available accounts and keys. +} + interface {$I ../config.inc} @@ -111,10 +123,10 @@ TFRMRandomOperations = class(TForm) implementation -{$IFnDEF FPC} - {$R *.dfm} +{$IFDEF FPC} +{$R *.lfm} {$ELSE} - {$R *.lfm} +{$R *.dfm} {$ENDIF} Uses ULog; @@ -130,7 +142,7 @@ procedure TRandomGeneratorThread.BCExecute; Var nCounter, nTotalRound, iLastSend, i : Integer; operationsComp : TPCOperationsComp; ohtToAdd : TOperationsHashTree; - errors : AnsiString; + errors : String; nAddedOperations : Integer; begin operationsComp := TPCOperationsComp.Create(Nil); @@ -292,7 +304,7 @@ class function TRandomGenerateOperation.GenerateOpTransactions(current_protocol opTx : TOpTransaction; senderAcc : TAccount; amount,fees : Int64; - errors : AnsiString; + errors : String; begin Result := 0; If Not GetRandomSigner(operationsComp,aWalletKeys,iKey,nAccount) then Exit; @@ -350,7 +362,7 @@ class function TRandomGenerateOperation.GenerateOpMultiOperation(current_protoco receiver : TMultiOpReceiver; changer: TMultiOpChangeInfo; acc : TAccount; - errors : AnsiString; + errors : String; begin Result := False; opMulti := TOpMultiOperation.Create; diff --git a/src/gui-experimental/UFRMSaleAccounts.pas b/src/gui-experimental/UFRMSaleAccounts.pas index 6ecdb78ed..aac085527 100644 --- a/src/gui-experimental/UFRMSaleAccounts.pas +++ b/src/gui-experimental/UFRMSaleAccounts.pas @@ -13,10 +13,22 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} {$I ..\config.inc} +{ Contains the account-sale dialog and its seller, price, lock-period and + buyer-key inputs. } + +{ Public types in this unit + + Type | Purpose + -----------------+------------------------------------------------------------------------- + TFRMSaleAccounts | Provides account-sale inputs and execution controls. +} + interface uses @@ -71,7 +83,7 @@ implementation {$R *.dfm} procedure TFRMSaleAccounts.actExecuteExecute(Sender: TObject); -Var errors : AnsiString; +Var errors : String; P : PAccount; i,iAcc : Integer; wk : TWalletKey; diff --git a/src/gui-experimental/UFRMWallet.pas b/src/gui-experimental/UFRMWallet.pas index 7d1fe3e28..9c07b6f57 100644 --- a/src/gui-experimental/UFRMWallet.pas +++ b/src/gui-experimental/UFRMWallet.pas @@ -17,6 +17,17 @@ } +{ Combines account and operation grids with explorer, network, mining and + key-management controls in one wallet form. } + +{ Public types in this unit + + Type | Purpose + -----------------+------------------------------------------------------------------------- + TFRMWallet | Combines wallet, explorer, networking and mining controls. + TMinerPrivateKey | Selects how the mining reward key is chosen. +} + interface {$I ../config.inc} @@ -179,8 +190,7 @@ TFRMWallet = class(TForm) procedure miAboutPascalCoinClick(Sender: TObject); procedure miNewOperationClick(Sender: TObject); procedure miPrivatekeysClick(Sender: TObject); - procedure dgAccountsColumnMoved(Sender: TObject; FromIndex, - ToIndex: Integer); + procedure dgAccountsColumnMoved(Sender: TObject; FromIndex, ToIndex: Integer); procedure dgAccountsFixedCellClick(Sender: TObject; ACol, ARow: Integer); procedure PageControlChange(Sender: TObject); procedure ebFilterOperationsAccountExit(Sender: TObject); @@ -211,8 +221,7 @@ TFRMWallet = class(TForm) procedure MiFindaccountClick(Sender: TObject); procedure bbAccountsRefreshClick(Sender: TObject); procedure ebFilterAccountByBalanceMinExit(Sender: TObject); - procedure ebFilterAccountByBalanceMinKeyPress(Sender: TObject; - var Key: Char); + procedure ebFilterAccountByBalanceMinKeyPress(Sender: TObject; var Key: Char); procedure cbFilterAccountsClick(Sender: TObject); procedure MiFindOperationbyOpHashClick(Sender: TObject); procedure MiAccountInformationClick(Sender: TObject); @@ -251,7 +260,7 @@ TFRMWallet = class(TForm) Procedure OnNewAccount(Sender : TObject); Procedure OnReceivedHelloMessage(Sender : TObject); Procedure OnNetStatisticsChanged(Sender : TObject); - procedure OnNewLog(logtype : TLogType; Time : TDateTime; ThreadID : Cardinal; Const sender, logtext : AnsiString); + procedure OnNewLog(logtype : TLogType; Time : TDateTime; ThreadID : Cardinal; Const sender, logtext : String); procedure OnWalletChanged(Sender : TObject); procedure OnNetConnectionsUpdated(Sender : TObject); procedure OnNetNodeServersUpdated(Sender : TObject); @@ -293,7 +302,7 @@ implementation {$R *.lfm} {$ENDIF} -Uses UFolderHelper, UConst, UTime, UFileStorage, +Uses UProgressDisplay, UFolderHelper, UConst, UTime, UFileStorage, UThread, UOpTransaction, UFRMPascalCoinWalletConfig, UFRMAbout, UFRMOperation, UFRMWalletKeys, UFRMPayloadDecoder, UFRMNodesIp, UFRMMemoText; @@ -318,7 +327,7 @@ procedure TThreadActivate.BCExecute; { TFRMWallet } procedure TFRMWallet.Activate; -Var ips : AnsiString; +Var ips : String; nsarr : TNodeServerAddressArray; begin inherited; @@ -444,7 +453,7 @@ procedure TFRMWallet.bbSelectedAccountsOperationClick(Sender: TObject); procedure TFRMWallet.bbSendAMessageClick(Sender: TObject); Var basem,m : String; - them, errors : AnsiString; + them, errors : String; i,n : Integer; nc : TNetConnection; begin @@ -509,11 +518,17 @@ procedure TFRMWallet.cbMyPrivateKeysChange(Sender: TObject); end; procedure TFRMWallet.CheckIsReady; -Var isready : AnsiString; +Var isready, LBankDiagnostic : String; + LProgress : TProgressInfo; begin if Not Assigned(FNode) then Abort; if Not FNode.IsReady(isready) then begin + // Keep domain readiness reasons; reconstruct only the active download text. + if FNode.Bank.IsReady(LBankDiagnostic) then + if FNode.NetServer.Active then + if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then + isready := FormatProgressInfo(LProgress); Raise Exception.Create('You cannot do this operation now:'+#10+#10+isready); end; end; @@ -537,7 +552,9 @@ procedure TFRMWallet.CM_NetConnectionUpdated(var Msg: TMessage); strings.BeginUpdate; Try for i := 0 to l.Count - 1 do begin - NC := l[i]; + // Keep the outer registry lock while copying the row. Membership + // alone includes retiring entries whose transport is already freed. + if not TNetData.NetData.GetConnection(i,NC) then Continue; If NC.Client.BytesReceived>0 then begin sClientApp := '['+IntToStr(NC.NetProtocolVersion.protocol_version)+'-'+IntToStr(NC.NetProtocolVersion.protocol_available)+'] '+NC.ClientAppVersion; end else begin @@ -616,8 +633,7 @@ procedure TFRMWallet.dgAccountsColumnMoved(Sender: TObject; FromIndex, ToIndex: SaveAppParams; end; -procedure TFRMWallet.dgAccountsFixedCellClick(Sender: TObject; ACol, - ARow: Integer); +procedure TFRMWallet.dgAccountsFixedCellClick(Sender: TObject; ACol, ARow: Integer); begin SaveAppParams; end; @@ -677,8 +693,7 @@ procedure TFRMWallet.ebBlockChainBlockStartExit(Sender: TObject); End; end; -procedure TFRMWallet.ebBlockChainBlockStartKeyPress(Sender: TObject; - var Key: Char); +procedure TFRMWallet.ebBlockChainBlockStartKeyPress(Sender: TObject; var Key: Char); begin if key=#13 then ebBlockChainBlockStartExit(Nil); end; @@ -688,8 +703,7 @@ procedure TFRMWallet.ebFilterAccountByBalanceMinExit(Sender: TObject); DoUpdateAccountsFilter; end; -procedure TFRMWallet.ebFilterAccountByBalanceMinKeyPress(Sender: TObject; - var Key: Char); +procedure TFRMWallet.ebFilterAccountByBalanceMinKeyPress(Sender: TObject; var Key: Char); begin if key=#13 then DoUpdateAccountsFilter; end; @@ -710,8 +724,7 @@ procedure TFRMWallet.ebFilterOperationsAccountExit(Sender: TObject); End; end; -procedure TFRMWallet.ebFilterOperationsAccountKeyPress(Sender: TObject; - var Key: Char); +procedure TFRMWallet.ebFilterOperationsAccountKeyPress(Sender: TObject; var Key: Char); begin if key=#13 then ebFilterOperationsAccountExit(Nil); end; @@ -1046,7 +1059,7 @@ procedure TFRMWallet.lblReceivedMessagesClick(Sender: TObject); procedure TFRMWallet.LoadAppParams; Var ms : TMemoryStream; - s : AnsiString; + s : String; fvi : TFileVersionInfo; begin ms := TMemoryStream.Create; @@ -1291,7 +1304,7 @@ procedure TFRMWallet.OnMiningServerNewBlockFound(Sender: TObject); end; procedure TFRMWallet.OnNetBlackListUpdated(Sender: TObject); -Const CT_TRUE_FALSE : Array[Boolean] Of AnsiString = ('FALSE','TRUE'); +Const CT_TRUE_FALSE : Array[Boolean] Of String = ('FALSE','TRUE'); Var i,j,n : integer; P : PNodeServerAddress; l : TList; @@ -1413,8 +1426,8 @@ procedure TFRMWallet.OnNewAccount(Sender: TObject); end; end; -procedure TFRMWallet.OnNewLog(logtype: TLogType; Time : TDateTime; ThreadID : Cardinal; const sender,logtext: AnsiString); -Var s : AnsiString; +procedure TFRMWallet.OnNewLog(logtype: TLogType; Time : TDateTime; ThreadID : Cardinal; const sender,logtext: String); +Var s : String; begin if (logtype=ltdebug) And (Not cbShowDebugLogs.Checked) then exit; if ThreadID=MainThreadID then s := ' MAIN:' else s:=' TID:'; @@ -1461,7 +1474,7 @@ procedure TFRMWallet.OnNodeMessageEvent(NetConnection: TNetConnection; MessageDa procedure TFRMWallet.OnReceivedHelloMessage(Sender: TObject); Var nsarr : TNodeServerAddressArray; i : Integer; - s : AnsiString; + s : String; begin //CheckMining; // Update node servers Peer Cache @@ -1518,7 +1531,7 @@ procedure TFRMWallet.PageControlChange(Sender: TObject); procedure TFRMWallet.SaveAppParams; Var ms : TMemoryStream; - s : AnsiString; + s : String; begin ms := TMemoryStream.Create; Try @@ -1836,7 +1849,7 @@ procedure TFRMWallet.UpdateConfigChanged; end; procedure TFRMWallet.UpdateConnectionStatus; -var errors : AnsiString; +var errors : String; begin UpdateNodeStatus; OnNetStatisticsChanged(Nil); @@ -1853,7 +1866,8 @@ procedure TFRMWallet.UpdateConnectionStatus; end; procedure TFRMWallet.UpdateNodeStatus; -Var status : AnsiString; +Var status, LBankDiagnostic : String; + LProgress : TProgressInfo; begin If Not Assigned(FNode) then begin lblNodeStatus.Font.Color := clRed; @@ -1864,8 +1878,8 @@ procedure TFRMWallet.UpdateNodeStatus; lblNodeStatus.Font.Color := clGreen; if TNetData.NetData.IsDiscoveringServers then begin lblNodeStatus.Caption := 'Discovering servers'; - end else if TNetData.NetData.IsGettingNewBlockChainFromClient then begin - lblNodeStatus.Caption := 'Obtaining new blockchain'; + end else if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then begin + lblNodeStatus.Caption := 'Obtaining new blockchain ' + FormatProgressInfo(LProgress); end else begin lblNodeStatus.Caption := 'Running'; end; @@ -1874,6 +1888,11 @@ procedure TFRMWallet.UpdateNodeStatus; lblNodeStatus.Caption := 'Alone in the world...'; end; end else begin + // Recreate download text here; bank/server failures keep their own reason. + if FNode.Bank.IsReady(LBankDiagnostic) then + if FNode.NetServer.Active then + if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then + status := FormatProgressInfo(LProgress); lblNodeStatus.Font.Color := clRed; lblNodeStatus.Caption := status; end; @@ -1894,7 +1913,7 @@ procedure TFRMWallet.UpdateOperations; procedure TFRMWallet.UpdatePrivateKeys; Var i,last_i : Integer; wk : TWalletKey; - s : AnsiString; + s : String; begin If (Not Assigned(FOrderedAccountsKeyList)) And (Assigned(FNode)) Then begin FOrderedAccountsKeyList := TOrderedAccountKeysList.Create(FNode.Bank.SafeBox,false); diff --git a/src/gui-experimental/UFRMWalletKeys.dfm b/src/gui-experimental/UFRMWalletKeys.dfm new file mode 100644 index 000000000..6af24118f --- /dev/null +++ b/src/gui-experimental/UFRMWalletKeys.dfm @@ -0,0 +1,2609 @@ +object FRMWalletKeys: TFRMWalletKeys + Left = 200 + Height = 477 + Top = 191 + Width = 661 + ActiveControl = lbWalletKeys + Caption = 'Wallet Keys' + ClientHeight = 477 + ClientWidth = 661 + OnCreate = FormCreate + OnDestroy = FormDestroy + Position = poOwnerFormCenter + Visible = False + object lbWalletKeys: TListBox + Left = 24 + Height = 210 + Top = 80 + Width = 612 + Anchors = [akTop, akLeft, akRight, akBottom] + ItemHeight = 0 + OnClick = lbWalletKeysClick + PopupMenu = pmKeyMenu + ScrollWidth = 344 + TabOrder = 0 + end + object lblEncryptionTypeCaption: TLabel + Left = 55 + Height = 15 + Top = 305 + Width = 50 + Anchors = [akLeft, akBottom] + Caption = 'Key Type:' + ParentColor = False + end + object lblEncryptionType: TLabel + Left = 115 + Height = 15 + Top = 304 + Width = 521 + Anchors = [akLeft, akRight, akBottom] + AutoSize = False + Caption = '000000000' + ParentColor = False + end + object lblKeyNameCaption: TLabel + Left = 48 + Height = 15 + Top = 328 + Width = 57 + Anchors = [akLeft, akBottom] + Caption = 'Key Name:' + ParentColor = False + end + object lblKeyName: TLabel + Left = 115 + Height = 15 + Top = 328 + Width = 521 + Anchors = [akLeft, akRight, akBottom] + AutoSize = False + Caption = '000000000' + ParentColor = False + end + object lblPrivateKeyCaption: TLabel + Left = 47 + Height = 15 + Top = 352 + Width = 58 + Anchors = [akLeft, akBottom] + Caption = 'Public Key:' + ParentColor = False + end + object memoPublicKey: TMemo + Left = 115 + Height = 61 + Top = 352 + Width = 522 + Anchors = [akLeft, akRight, akBottom] + Color = clBtnFace + Font.Color = clBlack + Font.Height = -11 + Font.Name = 'Tahoma' + Lines.Strings = ( + 'memoPublicKey' + ) + ParentFont = False + ReadOnly = True + TabOrder = 1 + TabStop = False + end + object btnBackupWallet: TBitBtn + Left = 24 + Height = 36 + Top = 426 + Width = 193 + Anchors = [akBottom] + Caption = 'Backup Wallet' + Glyph.Data = { + F6060000424DF606000000000000360000002800000018000000180000000100 + 180000000000C0060000120B0000120B00000000000000000000FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FF019ACF019ACF019ACF019ACFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF0C8518FF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF0D9FD18BD4EE6BD3F845C0ED28B0E0019ACF01 + 9ACF019ACF019ACFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF0C85180C8518 + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF069CD076C8E5A9E9FE6DD8 + FF75DBFF77DCFF77DBFF63D1F930B3E3029BD0019ACF019ACF019ACF019ACFFF + 00FF0C85181399220C8518FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF019ACF + 34AFD9BCE9F86ED8FF6FD8FE70D8FE70D8FE71D8FF0C85180C85180C85180C85 + 180C85180C85180C85180C85181DAC31139A220C8518FF00FFFF00FFFF00FFFF + 00FFFF00FF019ACF1FA9D68FD3EB97E4FF6FD9FE71D9FE71D9FE71D9FE0C8518 + 57E38851DD7E4AD77443D0693BC95E34C1522BBA4725B33C1EAE33149B230C85 + 18FF00FFFF00FFFF00FFFF00FF019ACF31B1DC49B7DEBDEEFB71DDFE77DEFE77 + DEFE77DEFE0C85185EE89059E48953DE804CD87645D16C3DCA6035C2542DBB49 + 26B53F1FAF35149B250C8518FF00FFFF00FFFF00FF019ACF52C2E71DA7D5ADE2 + F38FE8FF7CE2FE7CE3FE7CE3FE0C851861EB955FE9925AE58B54DF824DD97846 + D26D3ECB6237C4562FBD4C27B64021B037159B250C8518FF00FFFF00FF019ACF + 60CAEF1FA8D85EC1E1C2E6ED8ACEE08FCFE18ECFE10C851861EB9561EB955FEA + 935CE58D56E0844FDB7A48D47040CD6538C65931BF4D1DA3320C8518FF00FFFF + 00FFFF00FF019ACF65CFF53EB7E52CA9D4C5EFF8ACF3FEA5F2FFA5F2FF0C8518 + 61EB9561EB9561EB9561EB945CE68E57E18650DC7C49D57242CE6727AD410C85 + 18FF00FFFF00FFFF00FFFF00FF019ACF69D1F855C4F32A9CC673CBE7D6FEFDB1 + FBFDB2FBFD0C85180C85180C85180C85180C85180C85180C85180C851852DD7F + 32B6500C851898FAFF019ACFFF00FFFF00FFFF00FF019ACF77D5FC5CC8FB748E + A224A8D5B9E7F3D5F5F9D5F6F9D6F6FADCFAFBCDFDFCB9FCFCAFFAFCB0FAFCB1 + FAFC0C85183ABE5C0C85189FFCFFA4FFFF43C1E2019ACFFF00FFFF00FF019ACF + 8BDBFF5FCDFFB7898973C3DD18A2D218A2D216A2D215A1D21AA4D391D7EBEBFE + FDDBFDFCC5FBFBC2FBFB0C85180C851883E4F3B6FDFFBAFFFFB5FCFD019ACFFF + 00FFFF00FF019ACF99E2FF67D3FFB88989FEF5ECFDF3EBF0EFEAE5EBE8D6E5E6 + A4D2E025A6D34DB9DDE5F8FBF5FDFCEBFCFB0C8518C4FBFF9CE4F2DAFEFFD9FE + FFE3FFFFADE9F5019ACFFF00FF019ACF9FE9FF70DCFFB88989FEF3E9FFF2E6FE + F3E9FEF3E9FEF3E9FEF3E9D4E4E439ADD422A5D49DD8ECF1F9FBEEEFEFE9FDFF + CEEEF7F8FFFFF7FFFFFEFFFFE9F9FD019ACFFF00FF019ACFA7EFFF76E5FFB889 + 89FFF2E5FFF0E2FFF2E5FFF2E5FFF2E5FFF2E5FFF2E5EAEBE38EC9DA44B0D501 + 9ACF019ACF019ACF019ACF019ACF019ACF019ACF019ACF019ACFFF00FF019ACF + ABF6FF7EEDFFB88989FFF0E2FFEFDFFFF0E2FFF0E2FFF0E2FFF0E2FFF0E2FEEE + E0FBECDEFAEBDEF6E6D9B8898993F7FF019ACFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FF019ACFC7FFFF82F5FFB88989FFEEDFFFECDBFFEEDFFFEEDFFFEEDF + FFEEDFF9E8D9DECCC1D9CABDCFBDB4C8B3ACB88989B5FFFF019ACFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF019ACFA4E0F0A0FDFFB88989FFECDBFFEBD8FF + ECDBFFECDBFFECDBFFECDBF5E2D2C4ABA7C2A8A5BBA39FC2AFA9B88989019ACF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF019ACFECFFFFB889 + 89FFEBD8FFEAD5FFEBD8FFEBD8FFEBD8FFEBD8FFEBD8D9C8C5FEFEFDFEF6EFDE + C9C0B88989FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FF019ACFB88989FFE9D5FFE8D3FFE9D5FFE9D5FFE9D5FFE9D5FFE9D5C6AD + A9FEF8F2E8D4CACD9999FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFB88989FFE7D1FFE7D0FFE7D1FFE7D1FFE7D1 + FFE7D1E7CEBFD3BFB9E8D5CCCD9999FF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB88989FFE6CFFFE6CFFF + E6CFFFE6CFFFE6CFFFE6CFD5BBB2E0CCC5CD9999FF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFB889 + 89B88989B88989B88989B88989B88989B88989B88989B88989FF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + } + OnClick = btnBackupWalletClick + TabOrder = 2 + end + object btnRestoreBackup: TBitBtn + Left = 432 + Height = 36 + Top = 426 + Width = 206 + Anchors = [akRight, akBottom] + Caption = 'Restore Backup' + Glyph.Data = { + F6060000424DF606000000000000360000002800000018000000180000000100 + 180000000000C0060000120B0000120B00000000000000000000FF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FF019ACF019ACF019ACF019ACFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF0D9FD18BD4EE6BD3F845C0ED28B0E0019ACF01 + 9ACF019ACF019ACFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF069CD076C8E5A9E9FE6DD8 + FF75DBFF77DCFF77DBFF63D1F930B3E3029BD0019ACF019ACF019ACF019ACFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF019ACF + 34AFD9BCE9F86ED8FF6FD8FE70D8FE70D8FE71D8FF74DBFF7ADEFF79DDFF73D9 + FF5CCCF522ACDD019ACF019ACF019ACFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FF019ACF1FA9D68FD3EB97E4FF6FD9FE71D9FE71D9FE71D9FE71D9FE + 71D9FE71D9FE73DAFE76DCFF7BDFFF7ADEFF78DCFF77DCFF019ACFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FF019ACF31B1DC49B7DEBDEEFB71DDFE77DEFE77 + DEFE77DEFE77DEFE77DEFE77DEFE77DEFE76DEFE76DEFE76DEFE78DFFF7CE1FF + 65D2F8019ACFFF00FFFF00FFFF00FFFF00FFFF00FF019ACF52C2E71DA7D5ADE2 + F38FE8FF7CE2FE7CE3FE7CE3FE7CE3FE7CE3FE7CE3FE7CE3FE7CE3FE7CE3FE7C + E3FE7DE4FE7DE3FE5ED1F3019ACFFF00FFFF00FFFF00FFFF00FFFF00FF019ACF + 60CAEF1FA8D85EC1E2BBF4FE7DE7FE82E8FE81E8FE81E8FE81E8FE81E8FE81E8 + FE81E8FE81E8FE81E8FE82E8FE84E9FE5ED3F18DEEFF019ACFFF00FFFF00FFFF + 00FFFF00FF019ACF65CFF53EB7E529ACD8BFEEF88DEFFF85EDFF85EDFF85EDFF + 85EDFF86EDFF86EDFF86EDFF86EDFF86EDFF87EDFF89EEFF65D9F396F5FF019A + CFFF00FFFF00FFFF00FFFF00FF019ACF69D1F855C4F31FA7D773CBE7C5FCFF93 + F7FF93F7FF92F6FF8DF4FF89F3FF89F2FF8BF2FF8BF2FF8BF2FF8BF2FF8EF3FF + 6ADEF395F8FF98FAFF019ACFFF00FFFF00FFFF00FF019ACF77D5FC5CC8FB3EB8 + E920A7D5B6E6F3D0F4FAD1F5FAD2F6FAD5F9FCB9FBFE9BF8FF8FF6FF91F6FF92 + F6FF93F7FF6BD0B70C85188BEAE0A4FFFF43C1E2019ACFFF00FFFF00FF019ACF + 8BDBFF5FCDFF64CDFE2CAFE30D9FD30FA0D310A0D310A0D317A3D38ED7ECE2FD + FEC3FAFFA5F8FFA3F8FF84DDCF0C851838B5570C8518ABF3EBB5FCFD019ACFFF + 00FFFF00FF019ACF99E2FF67D3FF6DD4FE6CD4FE69D1FE64CEFB61CDF95BC9F5 + 48BEEB17A3D54BB8DDDFF7FBE8FCFFB1E7DD0C85184ACE7361EB9541C1640C85 + 18D6F6F0ADE9F5019ACFFF00FF019ACF9FE9FF70DCFF76DDFE76DDFE76DDFE75 + DCFE74DCFE73DCFE73DBFE61CEF61CA8D91CA5D58CCED70C851842C5665BE68C + 59E1895DE78F3EBD600C8518DBF1EF019ACFFF00FF019ACFA7EFFF76E5FF7CE5 + FF7CE5FF7CE5FF7CE5FF7DE5FF7DE5FF7DE5FF7DE3FF72DDFB40B8D20C85182D + AD474AD47250D97B55DE8359E1885AE38B33AF510C85180197C3FF00FF019ACF + ABF6FF7EEDFF85ECFF85ECFF85ECFF85ECFF84ECFF80ECFF7CECFF7DECFF7EEC + FF0C85180C85180C85180C851840C7634FDA7A55DF830C85180C85180C85180C + 8518FF00FF019ACFC7FFFF82F5FF8FF5FF8FF5FF8FF5FF8EF5FF8DF4FFA0FDFF + B7FFFFAFFFFFAEFFFFA6F9F4A5FBF8A3FCFA4CB07732B74F48D6704AD3720C85 + 18FF00FFFF00FFFF00FFFF00FF019ACFA4E0F0A0FDFF8AFCFF90FCFF90FCFF90 + FCFF99FDFF86E8F5019ACF019ACF019ACF019ACF019ACF019ACF0486642CB347 + 41D16636BC540C8518FF00FFFF00FFFF00FFFF00FFFF00FF019ACFECFFFFBCFF + FFBCFFFFBCFFFFC0FFFF9DF5FB019ACFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FF0C85182BB74538C9580C8518FF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FF019ACF019ACF019ACF019ACF019ACF019ACFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FF0C851828BB4126B13E0C8518FF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + FF00FFFF00FFFF00FFFF00FFFF00FF0C851814A4241CAE310C8518FF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF + 00FFFF00FFFF00FFFF00FFFF00FFFF00FF0C85180C85180C96170D991A0C8518 + FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00 + FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF0C85180C85180C85180C85180C + 8518FF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FF + } + OnClick = btnRestoreBackupClick + TabOrder = 3 + end + object btnCopyClipboard: TSpeedButton + Left = 81 + Height = 24 + Top = 376 + Width = 24 + Anchors = [akLeft, akBottom] + Glyph.Data = { + 36090000424D3609000000000000360000002800000018000000180000000100 + 2000000000000009000064000000640000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0003000000080000000800000008000000080000000800000008000000080000 + 0008000000080000000800000003000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 006B000000CB000000CE000000CE000000CE000000CE000000CE000000CE0000 + 00CE000000CE000000CB0000007B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00C70000007D0000003C0000003C0000003C0000003C0000003C0000003C0000 + 003C0000003C00000099000000F8000000000000000000000000000000000000 + 0000000000000000002300000036000000360000003600000036000000360000 + 00DB000000550000000000000000000000000000000000000000000000000000 + 0000000000000000007A000000F9000000000000000000000000000000000000 + 000000000000000000E3000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000550000000000000000000000000000000000000000000000000000 + 0000000000000000007A000000F9000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000550000000000000000000000000000000000000000000000000000 + 0000000000000000007A000000F9000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000550000000000000000000000000000000000000000000000000000 + 0000000000000000007A000000F9000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000550000000000000000000000000000000000000000000000000000 + 0000000000000000007A000000F9000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000550000000000000000000000000000000E0000003B0000003D0000 + 003D0000003D0000009A000000E9000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF0000005500000000000000000000000000000085000000F0000000CC0000 + 00CC000000DA000000FF0000005C000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF00000055000000000000000000000000000000A8000000BF000000000000 + 000E0000009B0000007000000004000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF00000055000000000000000000000000000000A8000000BF0000002F0000 + 00B4000000800000000A00000000000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF00000056000000010000000100000001000000A9000000E3000000CB0000 + 005F000000050000000000000000000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000930000005E0000005E0000005E000000D0000000EF0000005D0000 + 0004000000000000000000000000000000000000000000000000000000000000 + 000000000000000000FF000000FF000000FF000000FF000000FF000000FF0000 + 00FF000000FF000000FF000000FF000000FF000000FF000000C3000000040000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000FF000000FF000000E3000000D3000000D3000000D30000 + 00D3000000D3000000D3000000D8000000F7000000FF000000BF000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000FF000000FF000000530000000200000002000000020000 + 000200000002000000020000001B000000CC000000FF000000BF000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000EB000000F9000000EF000000EA000000EA000000EA0000 + 00EA000000EA000000EA000000EC000000F6000000F9000000AC000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000350000006E0000006E0000006E0000006E0000006E0000 + 006E0000006E0000006E0000006E0000006E0000006D0000001A000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000 + } + OnClick = btnCopyClipboardClick + end + object btnAddKey: TSpeedButton + Left = 604 + Height = 32 + Top = 40 + Width = 32 + Anchors = [akTop, akRight] + Glyph.Data = { + 36040000424D3604000000000000360000002800000010000000100000000100 + 2000000000000004000064000000640000000000000000000000000000000000 + 0000000000000000000000000000503E2C03503E2CD3503E2CFF503E2CFF503E + 2C8B000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000503E2C03503E + 2C14503E2C14503E2C14503E2C14503E2C3B503E2CFF503E2CFF503E2CFF503E + 2CFF503E2C14503E2C14503E2C14503E2C14503E2C1200000000503E2CD3503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2C64503E2CFF503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2C85503E2CFF503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2C83503E2C8B503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E + 2CFF503E2CFF503E2CFF503E2CFF503E2CFF503E2CF9503E2C38000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C14503E2CFF503E2CFF503E2CFF503E + 2CFF000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000503E2C12503E2CFF503E2CFF503E2CFF503E + 2CF9000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000503E2C64503E2C85503E2C83503E + 2C38000000000000000000000000000000000000000000000000 + } + OnClick = btnAddKeyClick + end + object lblKeysEncrypted: TLabel + Left = 97 + Height = 32 + Top = 40 + Width = 496 + Alignment = taCenter + AutoSize = False + Caption = 'lblKeysEncrypted' + Font.Color = clBlack + Font.Height = -16 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + WordWrap = True + end + object btnLock: TSpeedButton + Left = 25 + Height = 64 + Top = 8 + Width = 64 + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + OnClick = btnLockClick + ParentFont = False + end + object btnChangePassword: TBitBtn + Left = 228 + Height = 36 + Top = 426 + Width = 193 + Anchors = [akLeft, akBottom] + Caption = 'Change Password' + Glyph.Data = { + 76060000424D7606000000000000360400002800000018000000180000000100 + 0800000000004002000000000000000000000001000000010000FF00FF005251 + 5100565758005A5958005B5D5D005D5E5E006361610066666600696969006C6B + 6B0073706F0072707000747271000E80AA001788AF000B86B2000C85B000098A + B700078CBA00198EB7000E91BE001E94BC002898BA003E99BB000493C4000997 + C7000A9ECF00229BC20035A0C30030AACA0032AACE003DAACC003FB0CB0036A9 + D0003BB1D9003DBDDA0039B8DE0029BBE0004DBBD90068B0CA0019C7F1001BD4 + FE002FC6E7003ECDE9002CD3F9002ED8FE003CE0FE0054C0D40059C4D70059C2 + DD006FC2D6007CC0D50078C1D80049DDFE0054DFFE0061C6E5006CD4EE0064D9 + E90076DFE9007DDFE90055E0FF0058E0FE006BE3F40063E6FE0060ECFE0075E2 + FA0077E6FF007BE8FF0071F2FE0088828000928E8C0096908F00969291009794 + 94009E969400A09C9B00A19F9E00ACA09E00A6A2A100A6A4A400ADA5A300AAA5 + A400ABA8A800ADAAA900B0A5A300B2A7A500B2ABAB00B4ABA900BBB1AE00BEB1 + AF00BAB3B200BAB4B200BEB6B400C4B8B600C1BCBB0083DFE9008ADAEB0094DF + E90089DDF100A4DFEA009AE2F40095EBFD0095EDFE009EEDFF0084F3F90080F5 + FC008FF6FB0083F8FE0093F2FE0097F9FE0094FDFE0099FFFE009EFFFE00AAEF + FD00B4EEF300A4FFFE00A8F9FE00ADFBFE00AAFFFE00B6F1FF00BCF2F800B3F9 + FF00B3FFFE00B9FFFE00C7C2C100C7C6C600CCC4C200CAC7C600CFCCCC00D1C7 + C500D4CBC900D1CDCD00D5CFCE00D8D0CE00D7D1D000D9D5D400DCD7D600DFD8 + D600E3DEDD00C3DFEA00CCF6FF00C9FFFE00CFFFFF00D1F7FF00DBF8FE00DFFF + FF00E7E3E200E9E5E500ECE9E900EFEEED00F1EBEA00F1EFEE00E5FFFF00F3F1 + F100F6F2F000F8F5F300F8F5F400F0F7FA00F5FFFF00FBF8F800FAFFFF00FFFE + FE00000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0007070707000000000000000000000000000000000000000746967E07000000 + 0000000000000000000000000000000D085E489A5E080D0D0000000000000000 + 00000000000D0D2626075E48A1071F130D0D000000000000000000000D163940 + 3F35075E4C93070707100D000000000000000000123A6B402E353D075E519580 + 4E0707070707000000000000123A6B402E293D42075E4EA19289858281550700 + 00000000123A6B402E292D4266075E4C9689865A475D4D0700000000123A6B40 + 2E292D3C6671077D928A7D0A0A58580700000000123A6B402E292D3C4371079A + 958007035151550700000000123A6D6C6667778C8F90079A860701574C024A07 + 0000000012727860311E1B151C3407A17E0B534C000445070000000011322023 + 2A282C36426407539A8882040404570700000000103169402E292D3643716207 + 5393825A515A5A0700000000123A6B402E292D3C436662370707070708070700 + 00000000123A6B402E292D3C43663837222117000000000000000000123A6B40 + 2E292D3C436638231A180D000000000000000000123A6B402E292D3C43663822 + 1A180D000000000000000000123A6B402E292D3C436638231A180D0000000000 + 00000000123B757975756F6F6F6F6A3E25190D000000000000000000128BA190 + 8E79736F6F6F6F6D442B0D00000000000000000012279D9D908E796F6F6F6F6F + 691E0D000000000000000000001214333363615F3B3A2F2F0E0D000000000000 + 0000000000000014121212121212121200000000000000000000 + } + OnClick = btnChangePasswordClick + TabOrder = 4 + end + object ilIcons: TImageList + Height = 64 + Width = 64 + left = 489 + top = 136 + Bitmap = { + 494C010102000300040040004000FFFFFFFF2100FFFFFFFFFFFFFFFF424D3600 + 0000000000003600000028000000000100004000000001002000000000000000 + 0100000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000001000000211D94E7811F95 + E8C31E95E7F01E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95 + E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95 + E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21E95E7F21C91 + E3F21788DAEE1789DCD600000058000000040000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000060000 + 005E0819FEB50719FEE4081AFEF1081AFEF2081AFEF2081AFEF2081AFEF2081A + FEF2081AFEF2081AFEF2081AFEF2081AFEF2081AFEF2081AFEF2081AFEF2081A + FEF2081AFEF2081AFEF2081AFEF2081AFEF2081AFEF2081AFEF2081AFEF2081A + FEF20618FDF2000CF3F1000CF3E00000007D0000001100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003B1F96E8D51F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1D93E6FF1787 + DBFF1684D8FF1684D8FF1684D8FF1989DEA30000000400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000022071AFEB6081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0618 + FEFF000DF4FF000CEEFF000CEEFF000CEEFF000EF5B400000012000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000191F96E7D21F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1E94E6FF1888DBFF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF0000005000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000015081BFEBC081AFEFE081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0718FEFF010F + F6FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000006C000000010000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000001D93E5951F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1D93E5FF1889DCFF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1787DAC600000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000780819FEF6081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0718FEFF0210F6FF000C + EFFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CF2ED000000030000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000052095E6E42197E9FF2C9EF0FF2399EBFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1D92E4FF1888DCFF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1685D9D800000008000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000003081AFEB9081AFEFD1A29FFFF1525 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF0719FEFF0210F7FF000CEFFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFE000000110000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001A1E96E7F82599EBFF3CA7F8FF299CEDFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1D93E5FF1888DCFF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E00000001F000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000120819FECF081AFFFF343FFFFF2936 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF0110F8FF000CEFFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000000380000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1E95E7FF1889DDFF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF0010FAFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1E95E7FF198BDEFF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF0719FFFF0110F9FF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1D93 + E5FF1A8CDFFF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081A + FFFF0617FEFF0110F9FF000CEFFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F85CDFF145B8CFF125889FF155F + 92FF1D85CEFF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1D92E5FF1889 + DDFF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0B1DEEFF0915ABFF040F + 96FF061094FF0918C8FF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0618 + FEFF0111F8FF000CEFFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF2283C6FF082538FF051D2DFF0925 + 39FF186BA6FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1D92E4FF1888DBFF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0D20E9FF071064FF0104 + 31FF010531FF081275FF081AFEFF081AFFFF081AFFFF081AFFFF0819FFFF010F + F6FF000CEFFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF2091DFFF092840FF051B2AFF061D + 2DFF155B8DFF1F94E5FF1F96E8FF1F96E8FF1E95E7FF1A89DAFF1683D5FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0A1CF9FF070F75FF0105 + 30FF01042EFF050A4FFF0B1DF5FF081AFFFF081AFFFF081AFFFF0011FDFF000D + E8FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1E95E7FF124366FF051B2BFF051B + 2AFF13507BFF1F90DEFF1F96E8FF1E94E7FF1B8CDEFF1682D4FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081596FF0206 + 36FF01042EFF02073FFF0B1DE3FF081AFFFF0618FFFF0111FDFF000DE7FF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1C6397FF051C2BFF051B + 2AFF124468FF1F8AD5FF1E94E6FF1B8DDFFF1782D4FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF1020B9FF0409 + 3FFF01042EFF03073CFF0F21CCFF0618FFFF0111FBFF010DECFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF257FC1FF051C2CFF051B + 2AFF0D3450FF1C81C8FF1C8CDDFF1873B8FF167AC6FF1683D7FF1575BFFF177D + CBFF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF1023DCFF040A + 47FF01042EFF020636FF0B1AABFF0313FAFF0513D2FF0614D4FF000CEDFF0310 + E0FF0513D5FF010EEBFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF2291DEFE082437FF051B + 2AFF082236FF176FB1FF187FCDFF0D3555FF0D3F66FF1880D0FF0D446EFF1350 + 81FF1681D3FF1683D7FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0E22F4FF050C + 56FF01042FFF010531FF050F83FF0311E7FF050D75FF050C59FF010DE8FF0411 + B5FF071274FF0310D7FF000DEDFF000DEDFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF0F3B5DFF051B + 2AFF051A29FF092B46FF0B3759FF071F33FF061D30FF0D3759FF072238FF071F + 33FF0C395EFF0B385BFF0D3A5FFF124E7FFF1573BBFF1683D6FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFEFF0A16 + 7CFF010531FF01042EFF01053BFF030C5CFF030740FF00022DFF040E60FF030B + 52FF010433FF030A5CFF060F64FF040E61FF061077FF0410B9FF010DE7FF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1C5C89FF051A + 29FF041827FF041624FF041826FF041827FF041827FF041827FF041827FF0418 + 27FF041827FF041827FF041827FF041827FF062237FF1668A8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0F1F + A8FF020533FF00022BFF00022AFF00022AFF00022BFF00022BFF00022BFF0002 + 2BFF00022BFF00022BFF00022BFF00022BFF00022BFF000435FF020C99FF000C + E7FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1D6CA7FF0418 + 28FF041725FF051929FF082740FF082A44FF082A44FF082A45FF082B46FF092C + 47FF092C47FF082841FF051C2DFF041827FF041827FF0A3150FF1683D6FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF1022 + C8FF010433FF00022AFF000229FF02053FFF02064AFF01064BFF02074CFF0207 + 4CFF02084DFF02084EFF03084AFF010535FF00022BFF00022BFF02074CFF010C + D6FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1C91E3FF1772BAFF0419 + 28FF041827FF071F32FF1569AAFF1777C1FF1777C2FF1678C3FF187AC7FF187B + C8FF187BC8FF1670B6FF0D3A5EFF041827FF041827FF07253BFF1683D6FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0515FCFF0715 + DAFF010435FF00022AFF010431FF050E91FF0513D2FF0413D4FF0515D6FF0515 + D7FF0717DAFF0718DBFF0717D0FF061389FF00022BFF00022BFF01053BFF000B + D2FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1F96E8FF1B8FE1FF1786DAFF167CCAFF0723 + 38FF041827FF051A2AFF156DB0FF1684D7FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1683D6FF10558AFF041827FF041827FF07253BFF1683D6FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF081AFFFF0515FCFF010EF3FF010E + E9FF02084BFF00022BFF00022CFF030C91FF010DE8FF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF0515D3FF00022CFF00022BFF01053BFF000B + D2FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1F96E8FF1B8FE1FF1786D9FF1684D8FF1683D6FF186B + ACFF19629BFF19629BFF1779C5FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1683D6FF10568CFF041827FF041827FF07253BFF1683D6FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF081AFFFF0718FDFF000DF2FF000CEEFF000C + EEFF0916C2FF0915AAFF0915AAFF0613C9FF010DEAFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF0313D7FF00022BFF00022BFF01053BFF000B + D2FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1F96E8FF1D8FDFFF1A7FCCFF197DC9FF197DC9FF197DC9FF177D + CCFF177DCCFF167ECDFF177DCCFF197DC9FF197DC9FF197DC9FF197DC9FF197D + C9FF197DC9FF1679C4FF104D7DFF041827FF041827FF07253CFF1683D6FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF081AFFFF0819FFFF0516E5FF0512DCFF0512DCFF0512 + DCFF010DE1FF010DE1FF010DE1FF0410DFFF0512DDFF0612DEFF0612DEFF0512 + DCFF0512DCFF0512DCFF0411DAFF0714C1FF00022BFF00022BFF01053BFF000B + D3FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1F96E8FF1D93E5FF177CC7FF0E3C61FF082135FF082137FF082035FF0821 + 37FF082137FF082137FF082137FF082137FF082137FF082137FF082137FF0821 + 37FF082035FF082034FF061B2CFF041827FF041827FF0B3454FF1683D6FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF081AFFFF0718FEFF000EF6FF0A179AFF03073CFF03073AFF0307 + 3AFF03073CFF03073CFF03073CFF03073CFF03073CFF03073CFF03073CFF0307 + 3CFF03073BFF03073AFF030639FF030535FF00022BFF00022BFF02084DFF000C + D7FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F8259AECFF3DA8F9FF299CEEFF1F96 + E8FF1D93E5FF1888DCFF1684D7FF1465A4FF092A43FF041928FF041827FF0418 + 27FF041827FF041827FF041827FF041827FF041827FF041827FF041827FF0418 + 27FF041827FF041827FF041928FF061F32FF092F4DFF146AACFF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF3640FFFF2B37 + FFFF081AFFFF0617FEFF010EF5FF000CEEFF020DDDFF050C6AFF010330FF0002 + 2BFF00022BFF00022BFF00022BFF00022BFF00022BFF00022BFF00022BFF0002 + 2BFF00022BFF00022BFF00022BFF00022BFF010331FF02064AFF030DA2FF010D + E7FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F82499EBFF37A5F6FF279BEDFF1D91 + E4FF1888DCFF1684D8FF1684D8FF1682D5FF1262A0FF0E4B79FF0E4977FF0E49 + 77FF0E4977FF0E4977FF0E4977FF0E4977FF0E4773FF092B47FF0A2B46FF0E46 + 72FF092B46FF051C2DFF0E456FFF115B94FF1574BCFF1683D6FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF2F3BFFFF2533 + FFFF0617FDFF010FF5FF000CEFFF000CEEFF000CEDFF030FCAFF030B8CFF0209 + 82FF020982FF020982FF020982FF020982FF020983FF020983FF030A66FF0104 + 38FF030970FF020859FF00032FFF02086CFF020A94FF020DC1FF010DE7FF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F81F96E8FF2298EAFF1D92E4FF1787 + DAFF1684D8FF1684D8FF1684D8FF1684D8FF1683D7FF1681D3FF1681D3FF1681 + D3FF1681D3FF1681D3FF1681D3FF1681D3FF167FD0FF0F5286FF104975FF167B + C8FF145E97FF092E4BFF1776C0FF1683D7FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF0D1FFFFF0A1B + FEFF010FF5FF000CEEFF000CEEFF000CEEFF000CEEFF000CEDFF010DE9FF010C + E8FF010CE8FF010CE8FF010CE8FF010CE8FF010CE8FF010CE8FF030ECBFF030A + 5FFF030EC5FF0510B4FF02074FFF030FC2FF000CEBFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F81F96E8FF1D93E5FF1888DBFF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED0081AFFFF0719FFFF000F + F8FF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1E96E8F81E94E6FF1889DDFF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130819FED00719FFFF000FFAFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000001C1D95E7F8198BDEFF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8E000000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000130617FED00010FBFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF0000003B0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000018198ADEF81684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1584D8DF0000001C000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000011000EF7CF000CEFFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000000330000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000051785DAF21684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1686D9D700000006000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000004000CF0C6000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000BEFFE0000000D0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000001A8ADE9C1684D8FD1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684D8FF1684 + D8FF1684D8FF1684D8FF1684D8FF1684D8FE188ADCB100000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000007F000CEEF6000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000C + EEFF000CEEFF000CEEFF000CEEFF000CEEFF000CEEFF000EF5D5000000030000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000198DE1801E88D7EB2289D5FF2088 + D6FF1F88D6FF1F88D6FF1C87D7FB1684D8F41684D8F31684D8F31684D8F31684 + D8F31684D8F31684D8F31684D8F31684D8F31684D8F31684D8F31684D8F31684 + D8F31684D8F31684D8F31684D8F31684D8F31784D7F61D86D5FE1E87D5FF1E87 + D5FF1E87D5FF2087D4FF1886D8ED178ADCC20000000600000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000068000BF4DA000B + EDEA000BEDEA000BEDEA000BEDEA000BEDEA000BEDEA000BEDEA000BEDEA000B + EDEA000BEDEA000BEDEA000BEDEA000BEDEA000BEDEA000BEDEA000BEDEA000B + EDEA000BEDEA000BEDEA000BEDEA030EECEE0D19E9FC0D1AEAFF0C19EAFE0C19 + EAFE0715EBF9000CEDEC000BEDEA000CEFE9000DF4C900000017000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000010000000200000003000000050000000C0000000D0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075AFC0B6FF9CB9 + BCFF90B5BFFF90B5BFFF80AFC3CD000000630000005800000058000000580000 + 0058000000580000005800000058000000580000005800000058000000580000 + 0058000000580000005800000058000000580000007E84A7B0F586A8AFFF86A8 + AFFF8BAAAEFF98AEA8FF00000032000000010000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032BCC3B0DBA6BDBAFC9BB8BBF99DB9 + BBF88DB5BFB20000001200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0001000000020000000B0000002C000000430000005F99AFA7A1A0B1A4AC0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032CCCAADE0CDCAB0FFCCCAAFFFD0CB + ADFBCBCAACAE0000000800000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000100000023C3BA969FBEB597CEBCB797E0BEB797F4BDB899CE0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032CCCAADE0D0CBAEFFD0CBAEFFD0CB + ADF8D0CBACAE0000000700000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000031C2B997D6C1B795FDC1B896FFC1B896FFC0B998D00000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032CCCAADE0CFCBAFFFCECAAEFFCECA + AEF8CECBADB20000000A00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000031C0BA98D4BFB796FDBFB897FFBFB897FFC0B897CF0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032CCCAADE0CFCBAFFFCECAAEFFCECA + AEF8CDCAADB30000000E00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000031C0B997D4BEB795FDBFB897FFBFB897FFC0B897CF0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032CCCAADE0CFCBAFFFCECAAEFFCECA + AEF8CDCAADB30000000E00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000031C0B997D4BEB795FDBFB897FFBFB897FFC0B897CF0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032CCCAADE0CFCBAFFFCECAAEFFCECA + AEF8CDCAADB30000000E00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000031C0B997D4BEB795FDBFB897FFBFB897FFC0B897CF0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000032CDCBAEDFCECAAEFFCECAAEFFCECA + AEF8CDCAADB30000000E00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000031C0B997D4BEB796FDBFB897FFBFB796FFC0B897CF0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000100000075CECAAEFFCECA + AEFFCECAAEFFCECAAEFFCDCAADB3000000100000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000003ABFB796F0BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000002B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000002DCECBAFDECECAAEFFCECAAEFFCECA + AEF8CCC9ACC00000001900000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000200000045C1B998E0BFB796FFBFB897FFBFB796FFC0B897CF0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000006CCECAAEFBCECA + AEFFCECAAEFFCECAAEFFCECAAEC1000000210000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000062BFB897F4BFB897FFBFB8 + 97FFBFB897FFBFB897FF0000001E000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000022CDCAADD5CDC9ADFFCECAAEFFCDC9 + ADFFCCC8A9E20000003C00000002000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000900000068BFB898F6BFB897FFBFB897FFBFB998FFC0B897CF0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000005FCDC9ADF2CECA + AEFFCECAAEFFCECAAEFFCEC9ADD80000003A0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000C4BE9F93BFB897F9BFB897FFBFB8 + 97FFBFB897FFC1B99AFA0000001D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000012CFCBACB1CECAAEFCCECAAEFFCDC9 + ADFFCBC7ABFE0000007E00000011000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000018C3BD9D9BBFB897FDBFB897FFBFB897FFBFB998FFC1BB9AC40000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000049CEC9AEE1CECA + AEFFCECAAEFFCECAAEFFCDCAADF2000000690000000700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000011C0BB9BC0BFB897FDBFB897FFBFB8 + 97FFBFB897FFC1BB9BE90000001B000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000400000074CECAADECCECAAEFFCECA + AEFFCECAAEFFCBC9ADD700000049000000090000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000010000 + 001100000058C0BA99E2BFB897FFBFB897FFBFB796FFBFB997F5C2BD9C8F0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000001DCECAAEC1CECA + AEFFCECAAEFFCECAAEFFCECAAEFFCDCAAED00000003900000002000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000040000006DBFB898EEBFB897FFBFB897FFBFB8 + 97FFBFB897FFC1BB9CAB00000011000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000032CFCBAEC8CECAAEFCCECA + AEFFCECAAEFFCDC9ADFBCCC8ABC00000005D0000002000000009000000000000 + 00000000000200000008000000090000000900000011000000230000003D0000 + 007AC2BA9BCDBFB897FFBFB897FFBFB897FFBFB897FFC1BA99CC0000003E0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000300000073CECA + ADF1CECAAEFFCECAAEFFCECAAEFFCDC9ADFECEC9ADD10000005E000000020000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000A0000007EBFB898E5BFB897FFBFB897FFBFB897FFBFB8 + 97FFBFB998F90000005200000003000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000090000006ACBC8ABE6CECA + ADFFCECAAEFFCECAAEFFCECAADFBCECCB0DFCECBAEAFCECAAE88000000770000 + 00760000007BC4BC9E87C3BA9B8AC3BA9B8AC2BB9D97C3BC9BB3C2BB9BCDC0B9 + 99EFBFB897FCBFB897FFBFB897FFBFB796FFC1B898EF0000007E000000080000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000000009CDCA + ADB3CDC9ADFDCECAAEFFCECAAEFFCECAAEFFCECAAEFFCDCAADF8CDCAADBF0000 + 006D0000003900000030000000300000004300000055000000550000005DC6C0 + A383C5BFA0BAC1BB9AE5BFB897FABFB897FFBFB897FFBFB897FFBFB897FFBFB8 + 97FFC1BB99A90000001900000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000001ECFCAAE9BCEC9 + ADF5CECAAEFFCECAAEFFCECAAEFFCCC8ABFECBC7ABFCCECAAEFBCECAAEF7CECA + AEF7CAC5A7F9C1B99BFBBEB696FBBEB797FBBFB896FCBFB897FDBFB797FEBFB7 + 96FFBFB897FFBFB897FFBFB897FFC0B897FCC3BC9DA200000021000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0023CDC9AEE3CECAAEFFCECAAEFFCECAAEFFCECAAEFFCECAAEFFCECAAEFFCECA + AEFFCECAAEFFCECAAEFFCECAAEFFC6C0A2FFBFB897FFBFB897FFBFB897FFBFB8 + 97FFBFB897FFBFB897FFBFB897FFBFB897FFBFB897FFBFB897FFBFB897FFC0B9 + 99EC000000560000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000200000030CFC9 + B0A8CECAADF1CECAAEFCCECAAEFFCECAAEFFCECAAEFFCECAAEFFCECAAEFFCFCB + AFFFCAC5A8FFC1BA9BFFBFB796FFBFB897FFBFB897FFBFB897FFBFB897FFBFB8 + 97FFBFB897FFBFB897FFBFB896F8C2BA9AB40000003500000002000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000055CEC9AEE6CDC9ADFECECAAEFFCECAAEFFCECAAEFFCECAAEFFCECA + AEFFCECAAEFFCECAAEFFCECAAEFFC6C0A2FFBFB897FFBFB897FFBFB897FFBFB8 + 97FFBFB897FFBFB897FFBFB897FFBFB897FFBFB897FFBFB897FFBFB897FAC1BC + 9A99000000040000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 002800000078CECAACC7CEC9AEF5CEC9AEFCCECAAEFFCECAAEFFCECAAEFFCFCB + AFFFCAC5A8FFC1BA9BFFBFB796FFBFB897FFBFB897FFBFB897FFBFB897FFBFB8 + 97FCBEB796F8BEB896EAC1BB9AA2000000370000000400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000100000056CECAAEC1CDC9ADF8CECAAEFFCECAAEFFCECAAEFFCECA + AEFFCECAAEFFCECAAEFFCECAAEFFC6C0A2FFBFB897FFBFB897FFBFB897FFBFB8 + 97FFBFB897FFBFB897FFBFB897FFBFB897FFBFB897FFC0B999F3C1BC9A960000 + 0018000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000600000043CFC9AE95CEC9ADC9CECAADEBCECAAEF7CECAAEFCCECA + AFFCCAC5A7FCC1BA9AFCBEB796FDBEB796FDBFB896FCBFB897FCBFB797F4C2BA + 9AD2C5BFA0A40000007100000021000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000200000027CDC8AD8CCDC9ADEFCECAAEFFCECAAEFFCECA + AEFFCECAAEFFCECAAEFFCECAAEFFC6C0A2FFBFB897FFBFB897FFBFB897FFBFB8 + 97FFBFB897FFBFB897FFBFB897FFC0B998F9C2BA9BB000000056000000080000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000100000015000000320000004A00000054000000580000 + 0058000000580000005800000058000000580000005800000058000000510000 + 00390000001A0000000900000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000090000002900000073CDCBAFC0CDC9 + ADE4CEC9ADFACDC9ADFECDC9ADFEC6C0A2FEBEB797FEBEB797FEBEB797FEBEB7 + 97FEC1B99AFAC1BB9BEAC2BB9B97000000330000000E00000001000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000424D3E000000000000003E000000 + 2800000000010000400000000100010000000000000800000000000000000000 + 000000000000000000000000FFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFC0000003FFFFFFF0000001FFFFFF + 00000000000000000000000000000000FFFF80000000FFFFFFC00000007FFFFF + 00000000000000000000000000000000FFFF00000000FFFFFF800000007FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF800000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF000000003FFFFF + 00000000000000000000000000000000FFFE000000007FFFFF800000003FFFFF + 00000000000000000000000000000000FFFF00000000FFFFFFC00000007FFFFF + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFF9F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF83FFFFFFFFFFFF07FFFC1F + 00000000000000000000000000000000FFFFC1FFFF03FFFFFFFFFFFF07FFF81F + 00000000000000000000000000000000FFFFC1FFFF03FFFFFFFFFFFF83FFF81F + 00000000000000000000000000000000FFFFC0FFFF03FFFFFFFFFFFF81FFF03F + 00000000000000000000000000000000FFFFE07FFE07FFFFFFFFFFFFC038007F + 00000000000000000000000000000000FFFFE01FE007FFFFFFFFFFFFC000007F + 00000000000000000000000000000000FFFFF000000FFFFFFFFFFFFFE00000FF + 00000000000000000000000000000000FFFFF800000FFFFFFFFFFFFFF80001FF + 00000000000000000000000000000000FFFFFC00001FFFFFFFFFFFFFFC0007FF + 00000000000000000000000000000000FFFFFE00007FFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFC001FFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + 00000000000000000000000000000000 + } + end + object pmKeyMenu: TPopupMenu + left = 393 + top = 168 + object miChangeName: TMenuItem + Caption = 'Change Name' + OnClick = miChangeNameClick + end + object miExportPublicKey: TMenuItem + Caption = 'Export Public Key ' + OnClick = miExportPublicKeyClick + end + object miExportPrivateKey: TMenuItem + Caption = 'Export Private Key' + OnClick = miExportPrivateKeyClick + end + object miSep1: TMenuItem + Caption = '-' + end + object miDeleteKey: TMenuItem + Caption = 'Delete' + OnClick = miDeleteKeyClick + end + end + object SaveDialog: TSaveDialog + DefaultExt = '.dat' + Filter = 'Wallet keys file|*.dat|All files|*.*' + FilterIndex = 0 + left = 145 + top = 168 + end + object OpenDialog: TOpenDialog + DefaultExt = '.dat' + Filter = 'Wallet keys file (*.dat)|*.dat|All files (*.*)|*.*' + FilterIndex = 0 + left = 234 + top = 183 + end +end diff --git a/src/gui-experimental/UFRMWalletKeys.pas b/src/gui-experimental/UFRMWalletKeys.pas index 5417e03c1..08bdeff30 100644 --- a/src/gui-experimental/UFRMWalletKeys.pas +++ b/src/gui-experimental/UFRMWalletKeys.pas @@ -16,13 +16,26 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Manages wallet keys, their names and export/import formats. Provides wallet + backup, restore, password and lock controls. } + +{ Public types in this unit + + Type | Purpose + ---------------+------------------------------------------------------------------------- + TFRMWalletKeys | Manages keys, wallet backups, passwords and wallet locking. +} interface uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, Messages, - Buttons, Menus, LMessages, UCommon.UI, UWallet; + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + Classes, SysUtils, {$IFDEF FPC}FileUtil,{$ENDIF} Forms, Controls, Graphics, Dialogs, StdCtrls, Messages, + Buttons, Menus, {$IFDEF FPC}LMessages,{$ENDIF} UCommon.UI, UWallet; const CM_PC_WalletChanged = WM_USER + 1; @@ -95,14 +108,21 @@ TFRMWalletKeys = class(TApplicationForm) implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} -uses LCLIntf, Clipbrd, UUserInterface, USettings, UCommon, UAccounts, UWIZAddKey, UMemory; +uses {$IFDEF FPC}LCLIntf,{$ENDIF} Clipbrd, UUserInterface, USettings, UCommon, UAccounts, UWIZAddKey, UMemory, UBaseTypes; {%region Form life-cycle} procedure TFRMWalletKeys.FormCreate(Sender: TObject); begin + // Let the native button draw the original image-list alpha/mask, including + // its disabled state, instead of flattening the padlock into Glyph. + btnLock.Images := ilIcons; TWallet.Keys.OnChanged.Add(OnWalletChanged); RefreshUI; end; @@ -145,7 +165,7 @@ procedure TFRMWalletKeys.DeleteSelectedKey; begin if Not GetSelectedWalletKeyAndIndex(wk,index) then exit; - if (TSettings.MinerPrivateKeyType = mpk_Selected) AND (TSettings.MinerSelectedPrivateKey = TAccountComp.AccountKey2RawString(wk.AccountKey)) then begin + if (TSettings.MinerPrivateKeyType = mpk_Selected) AND TBaseType.Equals(TSettings.MinerSelectedPublicKey, TAccountComp.AccountKey2RawString(wk.AccountKey)) then begin TUserInterface.ShowError(Self, 'Delete Key Faied', 'This key is currently being used for mining and cannot be deleted.' + #10 + #10 + 'Remove this key from the Options dialog and try again.'); @@ -209,12 +229,16 @@ procedure TFRMWalletKeys.ExportSelectedPrivateKey; if not CheckIsWalletKeyValidPassword then exit; if Not GetSelectedWalletKey(wk) then exit; if Assigned(wk.PrivateKey) then begin - if TUserInterface.AskEnterProtectedString(Self, 'Export Private Key','Enter a password to encrypt private key',pwd1) then begin + if TUserInterface.AskEnterProtectedString(Self, 'Export Private Key','Enter an export password (leave empty for an unencrypted key)',pwd1) then begin if TUserInterface.AskEnterProtectedString(Self, 'Export Private Key','Re-enter the password',pwd2) then begin if pwd1<>pwd2 then raise Exception.Create('Passwords does not match!'); enc := TWallet.ExportPrivateKey(wk, pwd1); Clipboard.AsText := enc; - TUserInterface.ShowInfo(Self, 'Completed', + if pwd1 = '' then + TUserInterface.ShowInfo(Self, 'Completed', 'The unencrypted private key has been copied to the clipboard.' + + #10+#10+'Key value:'+#10+enc) + else + TUserInterface.ShowInfo(Self, 'Completed', 'The private key has been encrypted with your password and copied to the clipboard.'+ #10+#10+ 'Encrypted key value (Copied to the clipboard):'+#10+ @@ -315,7 +339,7 @@ procedure TFRMWalletKeys.RefreshUI; var lasti,i : Integer; selected_wk,wk : TWalletKey; - keyName : AnsiString; + keyName : String; begin GetSelectedWalletKeyAndIndex(wk,lasti); lbWalletKeys.Items.BeginUpdate; @@ -326,7 +350,7 @@ procedure TFRMWalletKeys.RefreshUI; If NOT TWallet.Keys.HasPassword then begin // NO PASSWORD lblKeysEncrypted.Caption := 'Wallet has no password'; - ilIcons.GetBitmap(1, btnLock.Glyph); + btnLock.ImageIndex := 1; btnLock.Enabled := false; btnChangePassword.Enabled := true; btnAddKey.Visible := true; @@ -339,7 +363,7 @@ procedure TFRMWalletKeys.RefreshUI; end else if TWallet.Keys.IsValidPassword then begin // UNLOCKED lblKeysEncrypted.Caption := 'Wallet is unlocked'; - ilIcons.GetBitmap(1, btnLock.Glyph); + btnLock.ImageIndex := 1; btnLock.Enabled := true; btnChangePassword.Enabled := true; btnAddKey.Enabled := true; @@ -353,7 +377,7 @@ procedure TFRMWalletKeys.RefreshUI; end else begin // LOCKED lblKeysEncrypted.Caption := 'Wallet is locked'; - ilIcons.GetBitmap(0, btnLock.Glyph); + btnLock.ImageIndex := 0; btnLock.Enabled := true; btnChangePassword.Enabled := false; btnAddKey.Visible := false; diff --git a/src/gui-experimental/UUserInterface.pas b/src/gui-experimental/UUserInterface.pas index 3bcf39058..8aa93505d 100644 --- a/src/gui-experimental/UUserInterface.pas +++ b/src/gui-experimental/UUserInterface.pas @@ -16,18 +16,34 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Coordinates application startup and shutdown, node services, UI events and + wallet dialogs. Owns background loading and serialized network/mining + configuration changes. } + +{ Public types in this unit + + Type | Purpose + --------------------+------------------------------------------------------------------------- + EUserInterface | Reports an invalid or unavailable UI operation. + TUserInterface | Coordinates node services, application events and wallet dialogs. + TUserInterfaceState | Identifies loading, connectivity, synchronization and error states. +} interface {$I ..\config.inc} uses + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} SysUtils, Classes, Forms, Controls, {$IFDEF WINDOWS}Windows,{$ENDIF} ExtCtrls, - Dialogs, LCLType, + Dialogs, {$IFDEF FPC}LCLType,{$ENDIF} UCommon, UCommon.UI, UBlockChain, UAccounts, UNode, UWallet, UConst, UFolderHelper, UGridUtils, URPC, UPoolMining, - ULog, UThread, UNetProtocol, UCrypto, UBaseTypes, UPCDataTypes, + ULog, UThread, UNetProtocol, UCrypto, UBaseTypes, UPCDataTypes, UWalletStartup, UWalletStartupTask, UFRMMainForm, UCTRLSyncronization, UFRMAccountExplorer, UFRMOperationExplorer, UFRMPendingOperations, UFRMOperation, UFRMLogs, UFRMMessages, UFRMNodes, UFRMBlockExplorer, UFRMWalletKeys, UPCOrderedLists {$IFDEF TESTNET},UFRMRandomOperations, UAccountKeyStorage{$ENDIF}; @@ -44,86 +60,148 @@ TUserInterface = class { TLoadingThread } - TLoadingThread = Class(TPCThread) + TLoadingThread = Class(TWalletStartupTask) private - FLastTC : TTickCount; - FMessage : String; - FCurPos : Int64; - FTotalCount : Int64; - procedure OnProgressNotify(sender : TObject; const message : String; curPos, totalCount : Int64); - procedure ThreadSafeNotify; - procedure ThreadSafeNotifyLoaded; + FMinerPayload : TBytes; + FMinerName: String; + procedure OnProgressNotify(sender : TObject; const AProgress : TProgressInfo); protected - procedure BCExecute; override; + procedure ExecuteLoad; override; End; + TMiningSettings = record + Pool: TPoolMiningServer; + Active, HasKey: Boolean; + Port: Word; + Key: TAccountKey; + MinerName: String; + end; + + { Listener shutdown can wait for an already received block buffer. Keep + that join on an owned worker; its completion is queued by the existing + startup-task lifecycle, never polled by the UI. } + TNetworkSettingsTask = class(TWalletStartupTask) + private + FNode: TNode; + FNetData: TNetData; + FStopRPCServer: TRPCServer; + FActive: Boolean; + FPort: Word; + FRequestVersion: Cardinal; + FMinerName: String; + FApplyMinerPayload: Boolean; + FMining: TMiningSettings; + FApplyMining, FApplyMiningKey: Boolean; + FDidApplyPayload, FDidApplyMining, FDidApplyMiningKey: Boolean; + FMiningTouched: Boolean; + FSuperseded: LongInt; + function RequestCurrent: Boolean; + protected + procedure ExecuteLoad; override; + public + constructor Create(ANode: TNode; ANetData: TNetData; AStopRPCServer: TRPCServer; AActive: Boolean; APort: Word; ARequestVersion: Cardinal; const AMinerName: String; AApplyMinerPayload: Boolean; const AMining: TMiningSettings; AApplyMining, AApplyMiningKey: Boolean); reintroduce; + procedure Supersede; + end; + private // Root-form - FUILock : TPCCriticalSection; static; - + class var FUILock : UThread.TReadWriteLock; // Subforms - FAccountExplorer : TFRMAccountExplorer; static; - FPendingOperationForm : TFRMPendingOperations; static; - FOperationsExplorerForm : TFRMOperationExplorer; static; - FBlockExplorerForm : TFRMBlockExplorer; static; - FLogsForm : TFRMLogs; static; - FNodesForm : TFRMNodes; static; - FMessagesForm : TFRMMessages; static; - + class var FAccountExplorer : TFRMAccountExplorer; + class var FPendingOperationForm : TFRMPendingOperations; + class var FOperationsExplorerForm : TFRMOperationExplorer; + class var FBlockExplorerForm : TFRMBlockExplorer; + class var FLogsForm : TFRMLogs; + class var FNodesForm : TFRMNodes; + class var FMessagesForm : TFRMMessages; // Components - FRPCServer : TRPCServer; static; - FPoolMiningServer : TPoolMiningServer; static; - + class var FRPCServer : TRPCServer; + class var FPoolMiningServer : TPoolMiningServer; // Local fields - FStarted : boolean; static; - FMainForm : TFRMMainForm; static; - FIsActivated : Boolean; static; - FUpdating : Boolean; static; - FLog : TLog; static; - FNode : TNode; static; - FTimerUpdateStatus: TTimer; static; - FTrayIcon: TTrayIcon; static; - FNodeNotifyEvents : TNodeNotifyEvents; static; - FStatusBar0Text : AnsiString; static; - FStatusBar1Text : AnsiString; static; - FStatusBar2Text : AnsiString; static; - FMessagesNotificationText : AnsiString; static; - FDisplayedStartupSyncDialog : boolean; static; - FAppStarted : TNotifyManyEvent; static; - FLoading : TProgressNotifyMany; static; - FLoaded : TNotifyManyEvent; static; - FStateChanged : TNotifyManyEvent; static; - FAccountsChanged : TNotifyManyEvent; static; - FBlocksChanged : TNotifyManyEvent; static; - FReceivedHelloMessage : TNotifyManyEvent; static; - FNodeMessageEvent : TNodeMessageManyEvent; static; - FNetStatisticsChanged : TNotifyManyEvent; static; - FNetConnectionsUpdated : TNotifyManyEvent; static; - FNetNodeServersUpdated : TNotifyManyEvent; static; - FNetBlackListUpdated : TNotifyManyEvent; static; - FMiningServerNewBlockFound : TNotifyManyEvent; static; - FUIRefreshTimer : TNotifyManyEvent; static; - FState : TUserInterfaceState; static; - FStateText : String; static; - FLoadingThread : TThread; static; - + class var FStarted : boolean; + class var FStopping : Boolean; + class var FDispatchingAppStarted, FExitRequested : Boolean; + class var FDispatchingLoaded: Boolean; + class var FDispatchingNetworkSettings: Boolean; + class var FSettingsLoaded : Boolean; + class var FStartupOptions : TWalletStartupOptions; + class var FNetData : TNetData; + class var FMainForm : TFRMMainForm; + class var FIsActivated : Boolean; + class var FUpdating : Boolean; + class var FLog : TLog; + class var FNode : TNode; + class var FEventNotify: TThreadNotify; + class var FTrayIcon: TTrayIcon; + class var FNodeNotifyEvents : TNodeNotifyEvents; + class var FStatusBar0Text : String; + class var FStatusBar1Text : String; + class var FStatusBar2Text : String; + class var FMessagesNotificationText : String; + class var FDisplayedStartupSyncDialog : boolean; + class var FAppStarted : TNotifyManyEvent; + class var FLoading : TProgressNotifyMany; + class var FLoaded : TNotifyManyEvent; + class var FStateChanged : TNotifyManyEvent; + class var FSynchronizationChanged : TNotifyManyEvent; + class var FAccountsChanged : TNotifyManyEvent; + class var FBlocksChanged : TNotifyManyEvent; + class var FReceivedHelloMessage : TNotifyManyEvent; + class var FNodeMessageEvent : TNodeMessageManyEvent; + class var FNetStatisticsChanged : TNotifyManyEvent; + class var FNetConnectionsUpdated : TNotifyManyEvent; + class var FNetNodeServersUpdated : TNotifyManyEvent; + class var FNetBlackListUpdated : TNotifyManyEvent; + class var FMiningServerNewBlockFound : TNotifyManyEvent; + class var FMiningStatusChanged : TNotifyManyEvent; + class var FState : TUserInterfaceState; + class var FStateText : String; + class var FStorageFailureHandled: Boolean; + class var FLastLoggedSafeboxProgress : TProgressInfo; + class var FLoadingThread : TWalletStartupTask; + class var FNetworkSettingsTask: TNetworkSettingsTask; + class var FDesiredNetworkActive, FNetworkSettingsInitialized: Boolean; + class var FDesiredNetworkPort: Word; + class var FNetworkRequestVersion: Cardinal; + class var FDesiredMinerName, FAppliedMinerName: String; + class var FDesiredApplyMinerPayload, FHasAppliedMinerPayload: Boolean; + class var FDesiredMining, FQueuedMining, FAppliedMining: TMiningSettings; + class var FHasAppliedMining, FHasAppliedMiningKey: Boolean; + class var FAppliedMiningKey: TAccountKey; // Getters/Setters class function GetEnabled : boolean; static; class procedure SetEnabled(ABool: boolean); static; class procedure SetState(AState : TUserInterfaceState); static; + class function GetOffline : Boolean; static; + class function GetNoStartupDialogs : Boolean; static; + class function GetSynchronizationStatus : TNodeSynchronizationStatus; static; // Handlers class procedure OnSettingsChanged(Sender: TObject); class procedure OnLoaded(Sender: TObject); - class procedure OnUITimerRefresh(Sender: TObject); + class procedure OnLoadingCompleted(Sender: TObject); + class procedure RequestNetworkSettings(AActive: Boolean; APort: Word); + class function SameMiningSettings(const A, B: TMiningSettings): Boolean; + class function HasUnappliedMempoolSettings: Boolean; + class procedure StartNetworkSettingsTask; + class procedure OnNetworkSettingsCompleted(Sender: TObject); + class procedure ApplyStorageFailure(const AFailure: TNodeStorageFailure); + class procedure RefreshNodeState; + class procedure OnNodeSnapshotChanged(Sender: TObject); + class procedure OnSynchronizationChanged(Sender: TObject; const AStatus: TNodeSynchronizationStatus); + class procedure OnConnectivityChanged(Sender: TObject); + class procedure DispatchConnectivityChanged(Sender: TObject); + class procedure OnMiningStatusChanged(Sender: TObject); + class procedure DispatchMiningStatusChanged(Sender: TObject); class procedure OnReceivedHelloMessage(Sender: TObject); class procedure OnMiningServerNewBlockFound(Sender: TObject); + class procedure UpdateMiningConfiguration(AAfterMinedBlock: Boolean = False); class procedure OnSubFormDestroyed(Sender: TObject); class procedure OnTrayIconDblClick(Sender: TObject); // Aux class procedure NotifyLoadedEvent(Sender: TObject); - class procedure NotifyLoadingEvent(Sender: TObject; const message : String; curPos, totalCount : Int64); + class procedure NotifyLoadingEvent(Sender: TObject; const AProgress : TProgressInfo); class procedure NotifyStateChanged(Sender: TObject); class procedure NotifyAccountsChangedEvent(Sender: TObject); class procedure NotifyBlocksChangedEvent(Sender: TObject); @@ -134,22 +212,27 @@ TUserInterface = class class procedure NotifyNetNodeServersUpdatedEvent(Sender: TObject); class procedure NotifyNetBlackListUpdatedEvent(Sender: TObject); class procedure NotifyMiningServerNewBlockFoundEvent(Sender: TObject); - class procedure NotifyUIRefreshTimerEvent(Sender: TObject); public // Properties class property Enabled : boolean read GetEnabled write SetEnabled; class property Started : boolean read FStarted; + class property Stopping : Boolean read FStopping; + class property Offline : Boolean read GetOffline; + class property NoStartupDialogs : Boolean read GetNoStartupDialogs; class property State : TUserInterfaceState read FState write SetState; class property StateText : string read FStateText; class property Node : TNode read FNode; + class property SynchronizationStatus : TNodeSynchronizationStatus read GetSynchronizationStatus; class property Log : TLog read FLog; class property PoolMiningServer : TPoolMiningServer read FPoolMiningServer; + class property RPCServer : TRPCServer read FRPCServer; // Events class property AppStarted : TNotifyManyEvent read FAppStarted; class property Loading : TProgressNotifyMany read FLoading; class property Loaded : TNotifyManyEvent read FLoaded; class property StateChanged : TNotifyManyEvent read FStateChanged; + class property SynchronizationChanged : TNotifyManyEvent read FSynchronizationChanged; class property AccountsChanged : TNotifyManyEvent read FAccountsChanged; class property BlocksChanged : TNotifyManyEvent read FBlocksChanged; class property ReceivedHelloMessage : TNotifyManyEvent read FReceivedHelloMessage; @@ -159,11 +242,18 @@ TUserInterface = class class property NetNodeServersUpdated : TNotifyManyEvent read FNetNodeServersUpdated; class property NetBlackListUpdated : TNotifyManyEvent read FNetBlackListUpdated; class property MiningServerNewBlockFound : TNotifyManyEvent read FMiningServerNewBlockFound; - class property UIRefreshTimer : TNotifyManyEvent read FUIRefreshTimer; + class property MiningStatusChanged : TNotifyManyEvent read FMiningStatusChanged; // Methods - class procedure StartApplication(mainForm : TForm); + // Apply before constructing forms: their event hooks can initialize the node. + class procedure ConfigureStartup(const AOptions : TWalletStartupOptions); + // An optional task transfers ownership to the UI and uses the same joined + // shutdown as normal disk restore. Useful for controlled startup hosts. + class procedure StartApplication(mainForm : TForm; ALoadingTask : TWalletStartupTask = nil); class procedure ExitApplication; + // Requests are serialized; repeated clicks replace the desired result + // while an earlier listener change finishes without blocking the caller. + class procedure ToggleConnectivity; class procedure RunInBackground; class procedure RunInForeground; class procedure CheckNodeIsReady; @@ -182,16 +272,18 @@ TUserInterface = class class procedure ShowBuyAccountDialog(const AAccounts : array of Cardinal); class procedure ShowAboutBox(parentForm : TForm); class procedure ShowOptionsDialog(parentForm: TForm); + class procedure ShowRPCCallsDialog(parentForm : TForm); + class procedure ShowOperationsWorkbench(parentForm : TForm); class procedure ShowAccountInfoDialog(parentForm: TForm; const account : Cardinal); overload; class procedure ShowAccountInfoDialog(parentForm: TForm; const account : TAccount); overload; - class procedure ShowOperationInfoDialog(parentForm: TForm; const ophash : AnsiString); overload; + class procedure ShowOperationInfoDialog(parentForm: TForm; const ophash : String); overload; class procedure ShowOperationInfoDialog(parentForm: TForm; const operation : TOperationResume); overload; class procedure ShowAccountOperationInfoDialog(parentForm: TForm; const account : TAccount; const operation : TOperationResume); overload; - class procedure ShowNewOperationDialog(parentForm : TForm; accounts : TOrderedCardinalList; defaultFee : Cardinal); + class procedure ShowNewOperationDialog(parentForm : TForm; accounts : TOrderedCardinalList; defaultFee : Int64); class procedure ShowSeedNodesDialog(parentForm : TForm); class procedure ShowPrivateKeysDialog(parentForm: TForm); - class procedure ShowMemoText(parentForm: TForm; const ATitle : AnsiString; text : utf8string); overload; - class procedure ShowMemoText(parentForm: TForm; const ATitle : AnsiString; text : TStrings); overload; + class procedure ShowMemoText(parentForm: TForm; const ATitle : String; text : String); overload; + class procedure ShowMemoText(parentForm: TForm; const ATitle : String; text : TStrings); overload; class procedure UnlockWallet(parentForm: TForm); class procedure ChangeWalletPassword(parentForm: TForm); {$IFDEF TESTNET} @@ -225,89 +317,368 @@ EUserInterface = class(Exception); implementation uses + {$IFNDEF FPC}System.SyncObjs,{$ENDIF} Generics.Collections, UFRMAbout, UFRMNodesIp, UFRMPascalCoinWalletConfig, UFRMPayloadDecoder, UFRMMemoText, - UFileStorage, UTime, USettings, UCoreUtils, UMemory, - UWIZOperation, UWIZSendPASC, UWIZChangeKey, UWIZEnlistAccountForSale, UWIZDelistAccountFromSale, UWIZChangeAccountInfo, UWIZBuyAccount, UCoreObjects; + UFileStorage, {$IFDEF USE_ABSTRACTMEM_BLOCKCHAIN_STORAGE}UAbstractMemBlockchainStorage,{$ENDIF} UTime, USettings, UCoreUtils, UMemory, UProgressDisplay, + UWIZOperation, UWIZSendPASC, UWIZChangeKey, UWIZEnlistAccountForSale, UWIZDelistAccountFromSale, UWIZChangeAccountInfo, UWIZBuyAccount, UCoreObjects, + UFRMRPCCalls, UFRMOperationsExplorer, UWalletLanguages; -{ TUserInterface.TLoadingThread } +const CT_ExperimentalAccountCacheRegisters = 250000; -procedure TUserInterface.TLoadingThread.OnProgressNotify(sender: TObject; const message: String; curPos, totalCount: Int64); -var pct : String; +procedure ConfigureExperimentalAccountCache(ANode: TNode); begin - If TPlatform.GetElapsedMilliseconds(FLastTC)>250 then begin - FLastTC := TPlatform.GetTickCount; - FMessage := Message; - FCurPos := curPos; - FTotalCount := totalCount; - Queue(ThreadSafeNotify); - end; + {$IFDEF USE_ABSTRACTMEM} + // A bounded, lazy product budget; core and classic keep their 50,000 default. + if Assigned(ANode) and + (ANode.Bank.SafeBox.PCAbstractMem.AccountCache.MaxRegisters <> CT_ExperimentalAccountCacheRegisters) then + ANode.Bank.SafeBox.PCAbstractMem.AccountCache.MaxRegisters := CT_ExperimentalAccountCacheRegisters; + {$ENDIF} +end; + +{ TUserInterface.TNetworkSettingsTask } + +constructor TUserInterface.TNetworkSettingsTask.Create(ANode: TNode; ANetData: TNetData; AStopRPCServer: TRPCServer; AActive: Boolean; APort: Word; ARequestVersion: Cardinal; const AMinerName: String; AApplyMinerPayload: Boolean; const AMining: TMiningSettings; AApplyMining, AApplyMiningKey: Boolean); +begin + inherited Create; + FNode := ANode; + FNetData := ANetData; + FStopRPCServer := AStopRPCServer; + FActive := AActive; + FPort := APort; + FRequestVersion := ARequestVersion; + FMinerName := AMinerName; + FApplyMinerPayload := AApplyMinerPayload; + FMining := AMining; + FApplyMining := AApplyMining; + FApplyMiningKey := AApplyMiningKey; end; -procedure TUserInterface.TLoadingThread.ThreadSafeNotify; +procedure TUserInterface.TNetworkSettingsTask.Supersede; begin - TUserInterface.NotifyLoadingEvent(Self, FMessage, FCurPos, FTotalCount); + {$IFDEF FPC}InterlockedExchange(FSuperseded,1); + {$ELSE}TInterlocked.Exchange(FSuperseded,1);{$ENDIF} end; -procedure TUserInterface.TLoadingThread.ThreadSafeNotifyLoaded; +function TUserInterface.TNetworkSettingsTask.RequestCurrent: Boolean; begin - TUserInterface.State := uisLoaded; + CheckCancelled; + {$IFDEF FPC}Result := InterlockedCompareExchange(FSuperseded,0,0) = 0; + {$ELSE}Result := TInterlocked.CompareExchange(FSuperseded,0,0) = 0;{$ENDIF} end; -procedure TUserInterface.TLoadingThread.BCExecute; -Var currentProcess : String; +procedure TUserInterface.TNetworkSettingsTask.ExecuteLoad; +var LLockedMempool: TPCOperationsComp; begin - FLastTC := 0; - FMessage := ''; - FCurPos := 0; - FTotalCount:=0; - // Read Operations saved from disk - TNode.Node.InitSafeboxAndOperations($FFFFFFFF,OnProgressNotify); // New Build 2.1.4 to load pending operations buffer - TNode.Node.AutoDiscoverNodes(CT_Discover_IPs); - TNode.Node.NetServer.Active := true; - FLastTC := 0; - FMessage := ''; - FCurPos := 0; - FTotalCount:=0; - if (TNode.Node.Bank.BlocksCount<=1) then begin - while (Not Terminated) And (Not TNode.Node.IsReady(currentProcess) Or (TNode.Node.Bank.BlocksCount<=1)) do begin - Queue( ThreadSafeNotify ); - Sleep(200); + if not RequestCurrent then Exit; + // A storage failure is restart-only. Retire services on this owned worker, + // never by joining RPC, peer or miner listeners from a UI state callback. + if FNode.HasStorageFailure then begin + FActive := False; + FApplyMinerPayload := False; + FApplyMiningKey := False; + FMining.Active := False; + FMining.HasKey := False; + end; + if Assigned(FStopRPCServer) then FStopRPCServer.Active := False; + if not RequestCurrent then Exit; + // Publish OFF before a potentially long listener join. The core preserves + // physical buffered work and denies new acquisition independently of the UI. + if not FActive and FNetData.NetConnectionsActive then + FNetData.NetConnectionsActive := false; + if not RequestCurrent then Exit; + if FNode.NetServer.Port <> FPort then begin + FNode.NetServer.Active := false; + if not RequestCurrent then Exit; + FNode.NetServer.Port := FPort; + end; + if not RequestCurrent then Exit; + if FNetData.NetConnectionsActive <> FActive then + FNetData.NetConnectionsActive := FActive + else if FNode.NetServer.Active <> FActive then + FNode.NetServer.Active := FActive; + if (FApplyMinerPayload or FApplyMiningKey) and FNode.PendingOperationsAvailable then begin + try + // This is a cancellable lock wait on the worker, not a UI settings read. + // An unchanged payload/key snapshot never enters the mempool gate at all. + while not FNode.TryLockNode(50) do + if not RequestCurrent then Exit; + try + if not RequestCurrent then Exit; + LLockedMempool := FNode.LockMempoolWrite; + try + if FApplyMinerPayload then begin + LLockedMempool.BlockPayload := TEncoding.ANSI.GetBytes(FMinerName); + FDidApplyPayload := true; + end; + if FApplyMiningKey then begin + LLockedMempool.AccountKey := FMining.Key; + FDidApplyMiningKey := true; + end; + finally + FNode.UnlockMempoolWrite; + end; + finally + FNode.UnlockNode; + end; + except + // A catch-up scope can begin after the cached availability check. Keep + // the settings intent; its final availability event retries this part. + on EPendingOperationsUnavailable do ; end; end; - if Not Terminated then begin - Queue(ThreadSafeNotifyLoaded); + if not RequestCurrent then Exit; + // Pool callbacks may acquire their own locks or join listeners. Never retain + // the node write scope across any of these operations. + if FApplyMining and Assigned(FMining.Pool) then begin + FMiningTouched := true; + if (not FMining.Active) or (FMining.Pool.Port <> FMining.Port) then + FMining.Pool.Active := false; + if not RequestCurrent then Exit; + FMining.Pool.Port := FMining.Port; + if FMining.HasKey then + FMining.Pool.UpdateAccountAndPayload(FMining.Key, TEncoding.ANSI.GetBytes(FMining.MinerName)) + else + FMining.Pool.MinerPayload := TEncoding.ANSI.GetBytes(FMining.MinerName); + if not RequestCurrent then Exit; + if FMining.Pool.Active <> FMining.Active then + FMining.Pool.Active := FMining.Active; + FDidApplyMining := true; end; - TUserInterface.FLoadingThread := nil; +end; + +class function TUserInterface.SameMiningSettings(const A, B: TMiningSettings): Boolean; +begin + Result := (A.Pool = B.Pool) and (A.Active = B.Active) and + (A.Port = B.Port) and (A.MinerName = B.MinerName) and (A.HasKey = B.HasKey); + if Result and A.HasKey then Result := TAccountComp.EqualAccountKeys(A.Key, B.Key); +end; + +class function TUserInterface.HasUnappliedMempoolSettings: Boolean; +begin + Result := FNetworkSettingsInitialized and + ((FDesiredApplyMinerPayload and ((not FHasAppliedMinerPayload) or + (FAppliedMinerName <> FDesiredMinerName))) or + (FDesiredMining.HasKey and ((not FHasAppliedMiningKey) or + not TAccountComp.EqualAccountKeys(FDesiredMining.Key,FAppliedMiningKey)))); +end; + +class procedure TUserInterface.RequestNetworkSettings(AActive: Boolean; APort: Word); +var LMinerName: String; LApplyPayload: Boolean; +begin + if FStopping or not Assigned(FNetData) or not Assigned(FNode) then Exit; + AActive := AActive and not Offline and not FNode.HasStorageFailure; + LMinerName := TSettings.MinerName; + LApplyPayload := not (FState in [uisLoading, uisError]) and not FNode.HasStorageFailure; + if FNetworkSettingsInitialized and (FDesiredNetworkActive = AActive) and + (FDesiredNetworkPort = APort) and (FDesiredMinerName = LMinerName) and + (FDesiredApplyMinerPayload = LApplyPayload) and + SameMiningSettings(FDesiredMining, FQueuedMining) then Exit; + FNetworkSettingsInitialized := true; + FDesiredNetworkActive := AActive; + FDesiredNetworkPort := APort; + FDesiredMinerName := LMinerName; + FDesiredApplyMinerPayload := LApplyPayload; + FQueuedMining := FDesiredMining; + Inc(FNetworkRequestVersion); + // Superseded tasks exit at a complete mutation boundary and still deliver + // normal queued completion; cancellation is reserved for application quit. + if Assigned(FNetworkSettingsTask) then FNetworkSettingsTask.Supersede; + StartNetworkSettingsTask; +end; + +class procedure TUserInterface.StartNetworkSettingsTask; +var LStopRPCServer: TRPCServer; +begin + if FStopping or FDispatchingNetworkSettings or Assigned(FNetworkSettingsTask) or + not FNetworkSettingsInitialized then Exit; + LStopRPCServer := Nil; + if Assigned(FNode) and FNode.HasStorageFailure then LStopRPCServer := FRPCServer; + FNetworkSettingsTask := TNetworkSettingsTask.Create(FNode, FNetData, LStopRPCServer, + FDesiredNetworkActive, FDesiredNetworkPort, FNetworkRequestVersion, + FDesiredMinerName, FDesiredApplyMinerPayload and + ((not FHasAppliedMinerPayload) or (FAppliedMinerName <> FDesiredMinerName)), + FDesiredMining, Assigned(FDesiredMining.Pool) and + ((not FHasAppliedMining) or not SameMiningSettings(FDesiredMining, FAppliedMining)), + FDesiredMining.HasKey and ((not FHasAppliedMiningKey) or + not TAccountComp.EqualAccountKeys(FDesiredMining.Key, FAppliedMiningKey))); + FNetworkSettingsTask.OnCompleted := OnNetworkSettingsCompleted; + FNetworkSettingsTask.Start; +end; + +class procedure TUserInterface.OnNetworkSettingsCompleted(Sender: TObject); +var + LTask: TNetworkSettingsTask; + LVersion: Cardinal; + LError: String; +begin + if FStopping or (Sender <> FNetworkSettingsTask) then Exit; + LTask := FNetworkSettingsTask; + LVersion := LTask.FRequestVersion; + LError := LTask.ErrorMessage; + FDispatchingNetworkSettings := true; + try + LTask.OnCompleted := nil; + // Keep the field assigned through the join: reentrant settings callbacks may + // replace the desired request, but cannot start a second listener mutation. + LTask.CancelAndWait; + FNetworkSettingsTask := nil; + if LTask.FDidApplyPayload then begin + FAppliedMinerName := LTask.FMinerName; + FHasAppliedMinerPayload := true; + end; + if LTask.FDidApplyMiningKey then begin + FAppliedMiningKey := LTask.FMining.Key; + FHasAppliedMiningKey := true; + end; + if LTask.FDidApplyMining then begin + FAppliedMining := LTask.FMining; + FHasAppliedMining := true; + end else if LTask.FMiningTouched then + // A superseding request may have interrupted between pool mutations. + // The next snapshot must restore the complete desired configuration. + FHasAppliedMining := false; + LTask.Free; + if LError <> '' then + TLog.NewLog(lterror, ClassName, 'Unable to apply networking settings: ' + LError); + // A port-only change need not produce connectivity traffic. Publish its + // completed primitive state explicitly so the footer never waits for I/O. + NotifyNetStatisticsChangedEvent(FNetData); + FNetConnectionsUpdated.Invoke(FNetData); + finally + FDispatchingNetworkSettings := false; + if FExitRequested then begin + FExitRequested := false; + ExitApplication; + end else + // A failed request is not retried forever. A newer explicit request still + // wins after full retirement, even if a UI subscriber raised above. + if (FNetworkRequestVersion <> LVersion) or + ((LError='') and Assigned(FNode) and FNode.PendingOperationsAvailable and + HasUnappliedMempoolSettings) then StartNetworkSettingsTask; + end; +end; + +class procedure TUserInterface.ToggleConnectivity; +begin + if FStopping or Offline or (FState in [uisLoading, uisError]) then Exit; + if FNetworkSettingsInitialized then + RequestNetworkSettings(not FDesiredNetworkActive, TSettings.InternetServerPort) + else + RequestNetworkSettings(not FNetData.NetConnectionsActive, TSettings.InternetServerPort); +end; + +{ TUserInterface.TLoadingThread } + +procedure TUserInterface.TLoadingThread.OnProgressNotify(sender: TObject; const AProgress: TProgressInfo); +begin + ReportProgress(AProgress); +end; + +procedure TUserInterface.TLoadingThread.ExecuteLoad; +var + LLockedMempool : TPCOperationsComp; + {$IFDEF USE_ABSTRACTMEM_BLOCKCHAIN_STORAGE} + LIndexedStorage: TAbstractMemBlockchainStorage; + LAutoFlushCache: Boolean; + {$ENDIF} +begin + // Loading local files is independent of network readiness. An empty chain is a + // valid first run; discovery and synchronization are reported after loading. + {$IFDEF USE_ABSTRACTMEM_BLOCKCHAIN_STORAGE} + LIndexedStorage := TAbstractMemBlockchainStorage(TUserInterface.FNode.Bank.Storage); + LAutoFlushCache := LIndexedStorage.AutoFlushCache; + // Match Classic: legacy blocks imported into the index during replay must + // not flush that index after every update. Restore the policy on every exit. + LIndexedStorage.AutoFlushCache := False; + try + {$ENDIF} + TUserInterface.FNode.InitSafeboxAndOperations($FFFFFFFF, OnProgressNotify); + {$IFDEF USE_ABSTRACTMEM_BLOCKCHAIN_STORAGE} + finally + LIndexedStorage.AutoFlushCache := LAutoFlushCache; + end; + {$ENDIF} + CheckCancelled; + // Final mempool work can wait for the pending-buffer saver. Keep that wait + // on the owned loader, rather than blocking the UI in the Loaded callback. + ReportProgress(TProgressInfo.Create(psFinalizingRestore)); + LLockedMempool := TUserInterface.FNode.LockMempoolWrite; + try + LLockedMempool.BlockPayload := FMinerPayload; + finally + TUserInterface.FNode.UnlockMempoolWrite; + end; + CheckCancelled; end; {%region UI Lifecyle} -class procedure TUserInterface.StartApplication(mainForm : TForm); -Var ips : AnsiString; +class procedure TUserInterface.ConfigureStartup(const AOptions : TWalletStartupOptions); +begin + if FIsActivated then + raise EUserInterface.Create('Startup options must be applied before starting the wallet.'); + FStartupOptions := AOptions; + if FStartupOptions.DataDirectory <> '' then + TNode.SetPascalCoinDataFolder(FStartupOptions.DataDirectory); + // The networking singleton starts its maintenance threads in its constructor. + // Disable it before any persisted peers can be supplied or forms constructed. + if Offline then begin + TCrypto.InitCrypto; + FNetData := TNetData.NetData; + FNetData.NetConnectionsActive := false; + FNode := TNode.Node; + end; +end; + +class function TUserInterface.GetOffline : Boolean; +begin + Result := FStartupOptions.Offline; +end; + +class function TUserInterface.GetNoStartupDialogs : Boolean; +begin + Result := FStartupOptions.NoStartupDialogs; +end; + +class procedure TUserInterface.StartApplication(mainForm : TForm; ALoadingTask : TWalletStartupTask); +Var ips : String; nsarr : TNodeServerAddressArray; begin inherited; if FIsActivated then exit; FIsActivated := true; + FLoadingThread := ALoadingTask; try // Initialize crypto module TCrypto.InitCrypto; // Create UI lock - FUILock := TPCCriticalSection.Create('TUserInterface.UILock'); + FUILock := UThread.TReadWriteLock.Create('TUserInterface.UILock',UThread.lrpSupportsRecursion); // Initialise field defaults - FIsActivated := false; FStarted := false; + FStopping := false; + FNetworkSettingsTask := nil; + FDispatchingNetworkSettings := false; + FNetworkSettingsInitialized := false; + FNetworkRequestVersion := 0; + FHasAppliedMinerPayload := false; + FDesiredApplyMinerPayload := false; + FHasAppliedMining := false; + FHasAppliedMiningKey := false; + FDesiredMining := Default(TMiningSettings); + FQueuedMining := Default(TMiningSettings); + FAppliedMining := Default(TMiningSettings); FRPCServer := Nil; - FNode := Nil; FPoolMiningServer := Nil; FNodeNotifyEvents := nil; FUpdating := false; FStatusBar0Text := ''; FStatusBar1Text := ''; FStatusBar2Text := ''; + FStorageFailureHandled := False; FMessagesNotificationText := ''; + FLastLoggedSafeboxProgress := Default(TProgressInfo); // Create root form and dependent components FMainForm := mainForm as TFRMMainForm; @@ -316,6 +687,7 @@ class procedure TUserInterface.StartApplication(mainForm : TForm); then raise Exception.Create('Main form is not TWallet'); FTrayIcon := TTrayIcon.Create(FMainForm); + FTrayIcon.Icon.Assign(Application.Icon); FTrayIcon.OnDblClick := OnTrayIconDblClick; {$IFNDEF LCLCarbon} FTrayIcon.Visible := true; @@ -324,17 +696,16 @@ class procedure TUserInterface.StartApplication(mainForm : TForm); FTrayIcon.BalloonTitle := 'Restoring the window.'; FTrayIcon.BalloonHint := 'Double click the system tray icon to restore Pascal Coin'; FTrayIcon.BalloonFlags := bfInfo; - {$IFNDEF LCLCarbon} + {$IF Defined(FPC) and not Defined(LCLCarbon)} FTrayIcon.Show; {$ENDIF} FDisplayedStartupSyncDialog:=false; - // Create the UI refresh timer - FTimerUpdateStatus := TTimer.Create(FMainForm); - FTimerUpdateStatus.Enabled := false; - FTimerUpdateStatus.OnTimer := NotifyUIRefreshTimerEvent; - FUIRefreshTimer.Add(OnUITimerRefresh); + // Core producers publish real changes; this owned dispatcher marshals their + // callbacks without a periodic UI refresh. It outlives producer shutdown. + FEventNotify := TThreadNotify.Create(TThread.CurrentThread); + FMainForm.SyncControl.SnapshotChanged.Add(OnNodeSnapshotChanged); // Create log FLog := TLog.Create(nil); // independent component @@ -347,62 +718,113 @@ class procedure TUserInterface.StartApplication(mainForm : TForm); // Load settings TSettings.Load; + FSettingsLoaded := true; + TWalletLanguages.Apply(TWalletLanguages.ReadPreference); TSettings.OnChanged.Add(OnSettingsChanged); // Open Wallet TWallet.Load; + // Keep the node disconnected until its local database has finished loading. + // In offline mode no peer list is ever supplied to the networking threads. + FNetData := TNetData.NetData; + FNetData.NetConnectionsActive := false; + FNode := TNode.Node; + ConfigureExperimentalAccountCache(FNode); // Load peer list ips := TSettings.TryConnectOnlyWithThisFixedServers; - TNode.DecodeIpStringToNodeServerAddressArray(ips,nsarr); - TNetData.NetData.DiscoverFixedServersOnly(nsarr); + if not Offline then begin + TNode.DecodeIpStringToNodeServerAddressArray(ips,nsarr); + FNetData.DiscoverFixedServersOnly(nsarr); + end; setlength(nsarr,0); // Start Node - FNode := TNode.Node; FNode.NetServer.Port := TSettings.InternetServerPort; - FNode.PeerCache := TSettings.PeerCache+';'+CT_Discover_IPs; + FDesiredNetworkActive := false; + FDesiredNetworkPort := TSettings.InternetServerPort; + FDesiredMinerName := TSettings.MinerName; + FNetworkSettingsInitialized := true; + if not Offline then + FNode.PeerCache := TSettings.PeerCache+';'+CT_Discover_IPs; FReceivedHelloMessage.Add(OnReceivedHelloMessage); + FMiningServerNewBlockFound.Add(OnMiningServerNewBlockFound, True); // Subscribe to Node events (TODO refactor with FNotifyEvents) FNodeNotifyEvents := TNodeNotifyEvents.Create(FMainForm); FNodeNotifyEvents.OnBlocksChanged := NotifyBlocksChangedEvent; + FNodeNotifyEvents.OnOperationsChanged := NotifyAccountsChangedEvent; + FNodeNotifyEvents.OnSynchronizationChanged := OnSynchronizationChanged; FNodeNotifyEvents.OnNodeMessageEvent := NotifyNodeMessageEventEvent; + // Keep the message history subscribed even before its window is opened. + FMessagesForm := TFRMMessages.Create(FMainForm); + FMessagesForm.CloseAction := caHide; + // Start RPC server FRPCServer := TRPCServer.Create; FRPCServer.WalletKeys := TWallet.Keys; - FRPCServer.Active := TSettings.RpcPortEnabled; - FRPCServer.ValidIPs := TSettings.RpcAllowedIPs; + FRPCServer.Active := false; + FRPCServer.ValidIPs := TSettings.JsonRpcAllowedIPs; TWallet.Keys.SafeBox := FNode.Bank.SafeBox; // Initialise Database + // Use Classic's configured indexed reader, including its legacy-file + // fallback, rather than leaving Experimental on the older raw-file path. + {$IFDEF USE_ABSTRACTMEM_BLOCKCHAIN_STORAGE} + FNode.Bank.StorageClass := TAbstractMemBlockchainStorageSecondary; + {$ELSE} FNode.Bank.StorageClass := TFileStorage; - TFileStorage(FNode.Bank.Storage).DatabaseFolder := TNode.GetPascalCoinDataFolder+PathDelim+'Data'; - TFileStorage(FNode.Bank.Storage).Initialize; + {$ENDIF} + FNode.Bank.Storage.Initialize; // Reading database State := uisLoading; FLoaded.Add(OnLoaded); - FLoadingThread := TLoadingThread.Create(true); - FLoadingThread.FreeOnTerminate := true; - FLoadingThread.Suspended := False; + if not Assigned(FLoadingThread) then + FLoadingThread := TLoadingThread.Create; + FLoadingThread.OnProgress := NotifyLoadingEvent; + FLoadingThread.OnCompleted := OnLoadingCompleted; // Init - TNetData.NetData.OnReceivedHelloMessage := NotifyReceivedHelloMessageEvent; - TNetData.NetData.OnStatisticsChanged := NotifyNetStatisticsChangedEvent; - TNetData.NetData.OnNetConnectionsUpdated := NotifyNetConnectionsUpdatedEvent; - TNetData.NetData.OnNodeServersUpdated := NotifyNetNodeServersUpdatedEvent; - TNetData.NetData.OnBlackListUpdated := NotifyNetBlackListUpdatedEvent; + FNetData.OnReceivedHelloMessage := NotifyReceivedHelloMessageEvent; + FNetData.OnStatisticsChanged := NotifyNetStatisticsChangedEvent; + FNetData.OnNetConnectionsUpdated := NotifyNetConnectionsUpdatedEvent; + FNetData.OnNodeServersUpdated := NotifyNetNodeServersUpdatedEvent; + FNetData.OnBlackListUpdated := NotifyNetBlackListUpdatedEvent; + FNetData.OnConnectivityChanged.Add(OnConnectivityChanged); // open the sync dialog FStarted := true; - FAppStarted.Invoke(nil); + FMainForm.SubscribeApplicationEvents; + FDispatchingAppStarted := true; + try + FAppStarted.Invoke(nil); + finally + FDispatchingAppStarted := false; + end; + if FExitRequested then begin + ExitApplication; + Exit; + end; + if FStopping then Exit; + // Snapshot mutable UI settings before the worker starts. TAppParams lookup + // must not race the RunCount update below or settings callbacks. + if FLoadingThread is TLoadingThread then begin + TLoadingThread(FLoadingThread).FMinerName := TSettings.MinerName; + TLoadingThread(FLoadingThread).FMinerPayload := TEncoding.ANSI.GetBytes(TSettings.MinerName); + end; + FLoadingThread.Start; Except On E:Exception do begin - E.Message := 'An error occurred during initialization. Application cannot continue:'+#10+#10+E.Message+#10+#10+'Application will close...'; - Application.MessageBox(PChar(E.Message),PChar(Application.Title),MB_ICONERROR+MB_OK); - Halt; + FStateText := 'An error occurred during initialization: ' + E.Message; + State := uisError; + TLog.NewLog(lterror, ClassName, FStateText); + if not NoStartupDialogs then + Application.MessageBox(PChar(FStateText), PChar(Application.Title), MB_ICONERROR + MB_OK); + ExitCode := 1; + ExitApplication; + Exit; end; end; @@ -414,34 +836,80 @@ class procedure TUserInterface.StartApplication(mainForm : TForm); // Final loading sequence TSettings.RunCount := TSettings.RunCount + 1; - if TSettings.RunCount = 1 then begin - ShowAboutBox(nil); - end; + // First run must not enter a modal loop before the main window is shown. + // About remains available from its explicitly owned menu command. TSettings.Save; end; class procedure TUserInterface.ExitApplication; var - i : Integer; step : String; begin - // Exit application + if FStopping then Exit; + // Event receivers may request shutdown during AppStarted. Finish dispatching + // before destroying receivers or mutating their subscription arrays. + if FDispatchingAppStarted or FDispatchingNetworkSettings then begin + FExitRequested := true; + Exit; + end; + FStopping := true; + FStarted := false; + // Publish application cancellation before closing listeners can join workers + // that are finishing a received buffer; network-off alone must drain it. + if Assigned(FNetData) then FNetData.BeginShutdown; TLog.NewLog(ltinfo,Classname,'Quit Application - START'); Try + // A main-thread WaitFor may dispatch synchronized core notifications. Keep + // the receiver graph alive, but silence forwarding before joining. + step := 'Stopping networking settings task'; + if Assigned(FNetworkSettingsTask) then begin + FNetworkSettingsTask.OnCompleted := nil; + FNetworkSettingsTask.CancelAndWait; + FreeAndNil(FNetworkSettingsTask); + end; + + step := 'Stopping database loader'; + if Assigned(FLoadingThread) then begin + FLoadingThread.CancelAndWait; + FreeAndNil(FLoadingThread); + end; + step := 'Deregistering events'; - TSettings.OnChanged.Remove(OnSettingsChanged); - FUIRefreshTimer.Remove(OnUITimerRefresh); + if Assigned(FMainForm) and Assigned(FMainForm.SyncControl) then + FMainForm.SyncControl.SnapshotChanged.Remove(OnNodeSnapshotChanged); + if FSettingsLoaded then + TSettings.OnChanged.Remove(OnSettingsChanged); FReceivedHelloMessage.Remove(OnReceivedHelloMessage); + FMiningServerNewBlockFound.Remove(OnMiningServerNewBlockFound); FLoaded.Remove(OnLoaded); - FUIRefreshTimer.Remove(OnUITimerRefresh); step := 'Saving Settings'; - TSettings.Save; + if FSettingsLoaded then + TSettings.Save; + + step := 'Disconnecting event sources'; + if Assigned(FNetData) then begin + FNetData.OnReceivedHelloMessage := Nil; + FNetData.OnStatisticsChanged := Nil; + FNetData.OnNetConnectionsUpdated := Nil; + FNetData.OnNodeServersUpdated := Nil; + FNetData.OnBlackListUpdated := Nil; + FNetData.OnConnectivityChanged.Remove(OnConnectivityChanged); + end; + if Assigned(FPoolMiningServer) then begin + FPoolMiningServer.OnStatusChanged := nil; + FPoolMiningServer.OnMiningServerNewBlockFound := nil; + end; + if Assigned(FNodeNotifyEvents) then begin + FNodeNotifyEvents.OnBlocksChanged := nil; + FNodeNotifyEvents.OnOperationsChanged := nil; + FNodeNotifyEvents.OnSynchronizationChanged := nil; + FNodeNotifyEvents.OnNodeMessageEvent := nil; + end; // Destroys root form, non-modal forms and all their attached components step := 'Destroying UI graph'; - FMainForm.Destroy; - FMainForm := nil; // destroyed by FWallet + FreeAndNil(FMainForm); FAccountExplorer := nil; // destroyed by FWallet FPendingOperationForm := nil; // destroyed by FWallet FOperationsExplorerForm := nil; // destroyed by FWallet @@ -456,34 +924,26 @@ class procedure TUserInterface.ExitApplication; FreeAndNil(FRPCServer); FreeAndNil(FPoolMiningServer); - step := 'Assigning nil events'; - FLog.OnNewLog :=Nil; + // The wallet owns a safebox key index, so release it while the node is alive. + step := 'Closing wallet'; + TWallet.Unload; - TNetData.NetData.OnReceivedHelloMessage := Nil; - TNetData.NetData.OnStatisticsChanged := Nil; - TNetData.NetData.OnNetConnectionsUpdated := Nil; - TNetData.NetData.OnNodeServersUpdated := Nil; - TNetData.NetData.OnBlackListUpdated := Nil; - - step := 'Assigning Nil to TNetData'; - TNetData.NetData.OnReceivedHelloMessage := Nil; - TNetData.NetData.OnStatisticsChanged := Nil; + step := 'Assigning nil events'; + if Assigned(FLog) then + FLog.OnNewLog := Nil; step := 'Desactivating Node'; - TNode.Node.NetServer.Active := false; - FNode := Nil; + if Assigned(FNode) then + FNode.NetServer.Active := false; // Destroy NetData - TNetData.NetData.Free; - - step := 'Processing messages 1'; - Application.ProcessMessages; + FreeAndNil(FNetData); step := 'Destroying Node'; - TNode.Node.Free; + FreeAndNil(FNode); - step := 'Processing messages 2'; - Application.ProcessMessages; + // All core producers are joined before discarding queued UI notifications. + FreeAndNil(FEventNotify); FreeAndNil(FUILock); Except @@ -491,7 +951,7 @@ class procedure TUserInterface.ExitApplication; TLog.NewLog(lterror,Classname,'Error quiting application step: '+step+' Errors ('+E.ClassName+'): ' +E.Message); end; End; - TLog.NewLog(ltinfo,Classname,'Error quiting application - END'); + TLog.NewLog(ltinfo,Classname,'Quit Application - END'); FreeAndNil(FLog); Application.Terminate; end; @@ -500,7 +960,6 @@ class procedure TUserInterface.RunInBackground; begin FMainForm.Hide(); FMainForm.WindowState := wsMinimized; - FTimerUpdateStatus.Enabled := false; FTrayIcon.Visible := True; FTrayIcon.ShowBalloonHint; end; @@ -508,9 +967,9 @@ class procedure TUserInterface.RunInBackground; class procedure TUserInterface.RunInForeground; begin FTrayIcon.Visible := False; - FTimerUpdateStatus.Enabled := true; FMainForm.Show(); FMainForm.WindowState := wsNormal; + RefreshNodeState; Application.BringToFront(); end; @@ -525,7 +984,8 @@ class procedure TUserInterface.OnTrayIconDblClick(Sender: TObject); class procedure TUserInterface.OnSubFormDestroyed(Sender: TObject); begin - FUILock.Acquire; + // Form creation/destruction callbacks may re-enter this recursive writer. + FUILock.AcquireWriteLock; try if Sender = FAccountExplorer then FAccountExplorer := nil // form free's on close @@ -544,73 +1004,189 @@ class procedure TUserInterface.OnSubFormDestroyed(Sender: TObject); else raise Exception.Create('Internal Error: [NotifySubFormDestroyed] encountered an unknown sub-form instance'); finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; class procedure TUserInterface.OnLoaded(Sender: TObject); -var LLockedMempool : TPCOperationsComp; begin - // Start timer - FTimerUpdateStatus.Interval := 1000; - FTimerUpdateStatus.Enabled := true; - + if FStopping then Exit; + // The normal loader applied its copied payload before completion. Custom + // loaders and settings changed during restore use the same owned worker below. + if FLoadingThread is TLoadingThread then begin + FAppliedMinerName := TLoadingThread(FLoadingThread).FMinerName; + FHasAppliedMinerPayload := true; + end; FPoolMiningServer := TPoolMiningServer.Create; - FPoolMiningServer.Port := TSettings.MinerServerRpcPort; - FPoolMiningServer.MinerAccountKey := TWallet.MiningKey; - FPoolMiningServer.MinerPayload := TEncoding.ANSI.GetBytes(TSettings.MinerName); - LLockedMempool := FNode.LockMempoolWrite; - try - LLockedMempool.AccountKey := TWallet.MiningKey; - finally - FNode.UnlockMempoolWrite; - end; - FPoolMiningServer.Active := TSettings.MinerServerRpcActive; + FPoolMiningServer.OnStatusChanged := OnMiningStatusChanged; + FDesiredNetworkActive := not Offline; + UpdateMiningConfiguration; FPoolMiningServer.OnMiningServerNewBlockFound := NotifyMiningServerNewBlockFoundEvent; + FRPCServer.Active := (not Offline) and (not FNode.HasStorageFailure) and TSettings.JsonRpcPortEnabled; + RequestNetworkSettings(not Offline, TSettings.InternetServerPort); + if not Offline then begin + FNode.AutoDiscoverNodes(CT_Discover_IPs); + end; State := uisLoaded; + RefreshNodeState; ShowWallet; end; -class procedure TUserInterface.OnUITimerRefresh(Sender: TObject); -var - LActive, LDiscoveringPeers, LGettingNewBlockchain, LRemoteHasBiggerBlock, LNoConnections : boolean; - LState : TUserInterfaceState; - LLocalTip, LRemoteTip : Cardinal; - LMsg : AnsiString; +class procedure TUserInterface.OnLoadingCompleted(Sender: TObject); +var LStatus: TNodeSynchronizationStatus; +begin + if FStopping or (Sender <> FLoadingThread) then Exit; + try + if FLoadingThread.ErrorMessage <> '' then + raise EUserInterface.Create(FLoadingThread.ErrorMessage); + State := uisLoaded; + except + on E : Exception do begin + if FNode.HasStorageFailure then begin + LStatus := FNode.GetSynchronizationStatus; + ApplyStorageFailure(LStatus.StorageFailure); + end else + FStateText := 'Unable to load the local blockchain: ' + E.Message; + TLog.NewLog(lterror, ClassName, FStateText); + if not FNode.HasStorageFailure then FRPCServer.Active := false; + FDesiredMining.Active := false; + FDesiredMining.HasKey := false; + State := uisError; + RequestNetworkSettings(false, TSettings.InternetServerPort); + // Keep the error visible and permit closing the wallet after a load error. + FMainForm.SyncControl.ShowSplash := false; + ShowSyncDialog; + end; + end; +end; + +class function TUserInterface.GetSynchronizationStatus: TNodeSynchronizationStatus; begin - LState := FState; - LActive := FNode.NetServer.Active; - LDiscoveringPeers := TNetData.NetData.IsDiscoveringServers; - LGettingNewBlockchain := TNetData.NetData.IsGettingNewBlockChainFromClient(LMsg); - LLocalTip := Node.Bank.BlocksCount; - LRemoteTip := TNetData.NetData.MaxRemoteOperationBlock.block; - LRemoteHasBiggerBlock := LRemoteTip > LLocalTip; - LNoConnections := TNetData.NetData.NetStatistics.ActiveConnections = 0; + if Assigned(FNode) then Result := FNode.GetSynchronizationStatus + else Result := Default(TNodeSynchronizationStatus); +end; - if LActive then begin - if LDiscoveringPeers then - LState := uisDiscoveringPeers; +class procedure TUserInterface.ApplyStorageFailure(const AFailure: TNodeStorageFailure); +var LWasError: Boolean; +begin + if not AFailure.HasFailure or FStorageFailureHandled then Exit; + LWasError := FState=uisError; + FStateText := FormatStorageFailure(AFailure); + FDesiredMining.Active := False; + FDesiredMining.HasKey := False; + // Force one stop snapshot even if networking was already off. This also + // retires an active RPC listener without changing persisted user settings. + FNetworkSettingsInitialized := False; + RequestNetworkSettings(False,TSettings.InternetServerPort); + State := uisError; + // A deferred loader event may have selected Error before this queued record + // supplied its diagnostic text. Publish that first text even if state is equal. + if LWasError then NotifyStateChanged(Nil); + if Assigned(FMainForm) then begin + FMainForm.SyncControl.ShowSplash := False; + ShowSyncDialog; + end; + FStorageFailureHandled := True; +end; - if LGettingNewBlockchain OR LRemoteHasBiggerBlock then - LState := uisSyncronizingBlockchain; +class procedure TUserInterface.RefreshNodeState; +var LStatus: TNodeSynchronizationStatus; +begin + if FStopping or FDispatchingLoaded or (not FStarted) then Exit; + LStatus := GetSynchronizationStatus; + if LStatus.StorageFailure.HasFailure then begin + ApplyStorageFailure(LStatus.StorageFailure); + Exit; + end; + if FState in [uisLoading, uisError] then Exit; + // Physical work and final pending replay remain synchronization even when + // acquisition is disabled. Let the node end that state before showing the + // configured offline caption; the CLI flag still prevents all networking. + case LStatus.State of + nssOffline: State := uisDisconnected; + nssDisconnected: State := uisIsolated; + nssSynchronizing: State := uisSyncronizingBlockchain; + nssIdle: begin + if TNetData.NetData.IsDiscoveringServers then State := uisDiscoveringPeers + else State := uisActive; + end; + end; +end; + +class procedure TUserInterface.OnSynchronizationChanged(Sender: TObject; const AStatus: TNodeSynchronizationStatus); +begin + if FStopping then Exit; + if (AStatus.State = nssSynchronizing) and + (AStatus.Progress.Stage = psDownloadingSafeboxChunks) then begin + // The producer repeats progress while waiting for chunks. Log only changed + // counters on this existing UI event, outside the downloading thread. + if (FLastLoggedSafeboxProgress.Stage <> AStatus.Progress.Stage) or + (FLastLoggedSafeboxProgress.CurrentPosition <> AStatus.Progress.CurrentPosition) or + (FLastLoggedSafeboxProgress.TotalCount <> AStatus.Progress.TotalCount) or + (FLastLoggedSafeboxProgress.ChunksCount <> AStatus.Progress.ChunksCount) or + (FLastLoggedSafeboxProgress.BlockCount <> AStatus.Progress.BlockCount) or + (FLastLoggedSafeboxProgress.Context <> AStatus.Progress.Context) then begin + FLastLoggedSafeboxProgress := AStatus.Progress; + TLog.NewLog(ltInfo, TNetData.ClassName, FormatProgressInfo(AStatus.Progress)); + end; + end else + FLastLoggedSafeboxProgress := Default(TProgressInfo); + RefreshNodeState; + FSynchronizationChanged.Invoke(Sender); + // Connectivity changes never wait for catch-up's pending overlay. Reapply + // only its deferred mining fields when the final coherent state is ready. + if AStatus.PendingOperationsAvailable and HasUnappliedMempoolSettings then + StartNetworkSettingsTask; +end; - if LNoConnections then - LState := uisIsolated; +class procedure TUserInterface.OnNodeSnapshotChanged(Sender: TObject); +begin + RefreshNodeState; +end; - if (NOT LDiscoveringPeers) AND (NOT LGettingNewBlockchain) AND (NOT LRemoteHasBiggerBlock) AND (NOT LNoConnections) then - LState := uisActive; +class procedure TUserInterface.OnConnectivityChanged(Sender: TObject); +begin + if FStopping or not Assigned(FEventNotify) then Exit; + FEventNotify.Invoke(Sender, DispatchConnectivityChanged); +end; - end else LState := uisDisconnected; - State := LState; +class procedure TUserInterface.DispatchConnectivityChanged(Sender: TObject); +begin + if FStopping then Exit; + RefreshNodeState; + FNetConnectionsUpdated.Invoke(Sender); +end; + +class procedure TUserInterface.OnMiningStatusChanged(Sender: TObject); +begin + if FStopping or not Assigned(FEventNotify) then Exit; + FEventNotify.Invoke(Sender, DispatchMiningStatusChanged); +end; + +class procedure TUserInterface.DispatchMiningStatusChanged(Sender: TObject); +begin + if FStopping then Exit; + FMiningStatusChanged.Invoke(Sender); end; class procedure TUserInterface.OnSettingsChanged(Sender: TObject); var - wa : Boolean; i : Integer; - LLockedMempool : TPCOperationsComp; begin + if FStopping then Exit; + FMainForm.miLogs.Visible := TSettings.ShowLogs; + if TSettings.ShowLogs then begin + if not Assigned(FLogsForm) then begin + FLogsForm := TFRMLogs.Create(FMainForm); + FLogsForm.CloseAction := caHide; + end; + FLog.OnNewLog := FLogsForm.OnNewLog; + end else begin + FLog.OnNewLog := nil; + if Assigned(FLogsForm) then + FLogsForm.Hide; + end; if TSettings.SaveLogFiles then begin if TSettings.SaveDebugLogs then FLog.SaveTypes := CT_TLogTypes_ALL @@ -622,35 +1198,39 @@ class procedure TUserInterface.OnSettingsChanged(Sender: TObject); FLog.FileName := ''; end; if Assigned(FNode) then begin - wa := FNode.NetServer.Active; - FNode.NetServer.Port := TSettings.InternetServerPort; - FNode.NetServer.Active := wa; - LLockedMempool := FNode.LockMempoolWrite; - try - LLockedMempool.BlockPayload := TEncoding.ANSI.GetBytes(TSettings.MinerName); - finally - FNode.UnlockMempoolWrite; - end; + // Port changes and toolbar connectivity share one owner. In particular, + // settings cannot reopen a listener while an earlier OFF request drains it. FNode.NodeLogFilename := TNode.GetPascalCoinDataFolder+PathDelim+'blocks.log'; end; if Assigned(FPoolMiningServer) then begin - if FPoolMiningServer.Port <> TSettings.MinerServerRpcPort then begin - FPoolMiningServer.Active := false; - FPoolMiningServer.Port := TSettings.MinerServerRpcPort; - end; - FPoolMiningServer.Active :=TSettings.MinerServerRpcActive; - FPoolMiningServer.UpdateAccountAndPayload(TWallet.MiningKey, TEncoding.ANSI.GetBytes(TSettings.MinerName)); + UpdateMiningConfiguration; end; if Assigned(FRPCServer) then begin - FRPCServer.Active := TSettings.RpcPortEnabled; - FRPCServer.ValidIPs := TSettings.RpcAllowedIPs; + // The storage-error stop snapshot owns listener retirement on its worker. + // A later settings event must not join that listener on the UI thread. + if not FNode.HasStorageFailure then + FRPCServer.Active := (not Offline) and (not (FState in [uisLoading, uisError])) and TSettings.JsonRpcPortEnabled; + FRPCServer.ValidIPs := TSettings.JsonRpcAllowedIPs; end; + if Assigned(FNetData) then begin + // Match the classic wallet's persisted keys and its existing runtime + // default when no checkpoint preference has yet been stored. + if TSettings.AppParams.ParamByName[CT_PARAM_AllowDownloadNewCheckpointIfOlderThan].GetAsBoolean( + FNetData.MinFutureBlocksToDownloadNewSafebox > 200) then + FNetData.MinFutureBlocksToDownloadNewSafebox := + TSettings.AppParams.ParamByName[CT_PARAM_MinFutureBlocksToDownloadNewSafebox].GetAsInteger( + FNetData.MinFutureBlocksToDownloadNewSafebox) + else + FNetData.MinFutureBlocksToDownloadNewSafebox := 0; + end; + RequestNetworkSettings(FDesiredNetworkActive, TSettings.InternetServerPort); + RefreshNodeState; end; class procedure TUserInterface.OnReceivedHelloMessage(Sender: TObject); Var nsarr : TNodeServerAddressArray; i : Integer; - s : AnsiString; + s : String; begin // Internal handler // No lock required @@ -668,7 +1248,32 @@ class procedure TUserInterface.OnReceivedHelloMessage(Sender: TObject); class procedure TUserInterface.OnMiningServerNewBlockFound(Sender: TObject); begin - FPoolMiningServer.MinerAccountKey := TWallet.MiningKey; + if FStopping then Exit; + UpdateMiningConfiguration(True); +end; + +class procedure TUserInterface.UpdateMiningConfiguration(AAfterMinedBlock: Boolean); +var + LMiningKey : TAccountKey; +begin + if not Assigned(FPoolMiningServer) then Exit; + FDesiredMining.Pool := FPoolMiningServer; + FDesiredMining.Port := TSettings.JsonRpcMinerServerPort; + FDesiredMining.MinerName := TSettings.MinerName; + FDesiredMining.Active := false; + FDesiredMining.HasKey := false; + if (not Offline) and (not FNode.HasStorageFailure) and TSettings.JsonRpcMinerServerActive then begin + // Wallet selection/creation/rotation stays on its UI owner with the exact + // existing policy. Only this copied public key crosses to the worker. + if TWallet.Keys.TryGetMiningKey(TSettings.MinerPrivateKeyType, + TSettings.MinerSelectedPublicKey, LMiningKey, AAfterMinedBlock) then begin + FDesiredMining.Key := LMiningKey; + FDesiredMining.HasKey := true; + FDesiredMining.Active := true; + end else + TLog.NewLog(ltinfo, ClassName, 'Mining is inactive: select or add a wallet private key for rewards.'); + end; + RequestNetworkSettings(FDesiredNetworkActive, TSettings.InternetServerPort); end; {%endregion} @@ -727,8 +1332,7 @@ class procedure TUserInterface.ShowDelistAccountsDialog(const AAccounts : array wiz.Start(model); end; -class procedure TUserInterface.ShowChangeAccountInfoDialog( - const AAccounts: array of Cardinal); +class procedure TUserInterface.ShowChangeAccountInfoDialog( const AAccounts: array of Cardinal); var Scoped: TDisposables; wiz: TWIZChangeAccountInfoWizard; @@ -741,8 +1345,7 @@ class procedure TUserInterface.ShowChangeAccountInfoDialog( wiz.Start(model); end; -class procedure TUserInterface.ShowBuyAccountDialog( - const AAccounts: array of Cardinal); +class procedure TUserInterface.ShowBuyAccountDialog( const AAccounts: array of Cardinal); var Scoped: TDisposables; wiz: TWIZBuyAccountWizard; @@ -775,7 +1378,35 @@ class procedure TUserInterface.ShowOptionsDialog(parentForm: TForm); end; end; -class procedure TUserInterface.ShowOperationInfoDialog(parentForm: TForm; const ophash: AnsiString); +class procedure TUserInterface.ShowRPCCallsDialog(parentForm : TForm); +var + LScoped : TDisposables; + LForm : TFRMRPCCalls; +begin + if Offline then + raise EUserInterface.Create('RPC calls are unavailable in offline mode.'); + LForm := LScoped.AddObject(TFRMRPCCalls.Create(nil)) as TFRMRPCCalls; + LForm.PopupParent := parentForm; + LForm.Position := poOwnerFormCenter; + LForm.ServerURL := '127.0.0.1:' + IntToStr(FRPCServer.Port); + LForm.ShowModal; +end; + +class procedure TUserInterface.ShowOperationsWorkbench(parentForm : TForm); +var + LScoped : TDisposables; + LForm : TFRMOperationsExplorer; +begin + LForm := LScoped.AddObject(TFRMOperationsExplorer.Create(nil)) as TFRMOperationsExplorer; + LForm.PopupParent := parentForm; + LForm.Position := poOwnerFormCenter; + LForm.Offline := Offline; + LForm.SourceNode := FNode; + LForm.SourceWalletKeys := TWallet.Keys; + LForm.ShowModal; +end; + +class procedure TUserInterface.ShowOperationInfoDialog(parentForm: TForm; const ophash: String); begin with TFRMPayloadDecoder.Create(parentForm) do try @@ -789,7 +1420,7 @@ class procedure TUserInterface.ShowOperationInfoDialog(parentForm: TForm; const end; end; -class procedure TUserInterface.ShowOperationInfoDialog(parentForm: TForm; const operation : TOperationResume); overload; +class procedure TUserInterface.ShowOperationInfoDialog(parentForm: TForm; const operation : TOperationResume); begin with TFRMPayloadDecoder.Create(parentForm) do try @@ -808,20 +1439,21 @@ class procedure TUserInterface.ShowAccountInfoDialog(parentForm: TForm; const ac ShowAccountInfoDialog(parentForm, TUserInterface.Node.GetAccount(account)); end; -class procedure TUserInterface.ShowAccountInfoDialog(parentForm: TForm; const account : TAccount); overload; +class procedure TUserInterface.ShowAccountInfoDialog(parentForm: TForm; const account : TAccount); begin ShowMemoText(parentForm, Format('Account: %s', [account.GetAccountString]), account.GetInfoText(Self.Node.Bank)); end; class procedure TUserInterface.ShowAccountOperationInfoDialog(parentForm: TForm; const account: TAccount; const operation : TOperationResume); -var text : utf8string; +var text : String; begin - text := account.GetInfoText(Self.Node.Bank) + sLineBreak + sLineBreak + operation.GetInfoText(Self.Node.Bank); + text := account.GetInfoText(Self.Node.Bank) + sLineBreak + sLineBreak + + operation.GetInfoText(Self.Node.Bank.BlocksCount, Self.Node, TWallet.Keys); ShowMemoText(parentForm, Format('Account/Operation: %s/%s', [account.GetAccountString, operation.GetPrintableOPHASH]), text); end; // TODO - refactor with accounts as ARRAY -class procedure TUserInterface.ShowNewOperationDialog(parentForm : TForm; accounts : TOrderedCardinalList; defaultFee : Cardinal); +class procedure TUserInterface.ShowNewOperationDialog(parentForm : TForm; accounts : TOrderedCardinalList; defaultFee : Int64); begin If accounts.Count = 0 then raise Exception.Create('No sender accounts provided'); CheckNodeIsReady; @@ -858,12 +1490,12 @@ class procedure TUserInterface.ShowPrivateKeysDialog(parentForm: TForm); end; end; -class procedure TUserInterface.ShowMemoText(parentForm: TForm; const ATitle : AnsiString; text : utf8string); +class procedure TUserInterface.ShowMemoText(parentForm: TForm; const ATitle : String; text : String); begin with TFRMMemoText.Create(parentForm) do begin try Caption := ATitle; - Memo.Append(text); + Memo.Lines.Add(text); Position := poMainFormCenter; ShowModal; finally @@ -872,7 +1504,7 @@ class procedure TUserInterface.ShowMemoText(parentForm: TForm; const ATitle : An end; end; -class procedure TUserInterface.ShowMemoText(parentForm: TForm; const ATitle : AnsiString; text : TStrings); +class procedure TUserInterface.ShowMemoText(parentForm: TForm; const ATitle : String; text : TStrings); begin with TFRMMemoText.Create(parentForm) do begin try @@ -997,7 +1629,7 @@ class procedure TUserInterface.CreateABlock; var ops : TPCOperationsComp; nba : TBlockAccount; - errors : AnsiString; + errors : String; begin ops := TPCOperationsComp.Create(Nil); Try @@ -1024,27 +1656,28 @@ class procedure TUserInterface.UnlockWallet(parentForm: TForm); if Not TWallet.Keys.IsValidPassword then ShowError(parentForm, 'Invalid Password', 'The password you have entered is incorrect.'); Until TWallet.Keys.IsValidPassword; + UpdateMiningConfiguration; end; class procedure TUserInterface.ShowInfo(parentForm : TForm; const ACaption, APrompt : String); begin - MessageDlg(ACaption, APrompt, mtInformation, [mbOK], 0, mbOK); + {$IFDEF FPC}MessageDlg{$ELSE}TaskMessageDlg{$ENDIF}(ACaption, APrompt, mtInformation, [mbOK], 0, mbOK); end; class procedure TUserInterface.ShowWarning(parentForm : TForm; const ACaption, APrompt : String); begin - MessageDlg(ACaption, APrompt, mtWarning, [mbOK], 0, mbOK); + {$IFDEF FPC}MessageDlg{$ELSE}TaskMessageDlg{$ENDIF}(ACaption, APrompt, mtWarning, [mbOK], 0, mbOK); end; class procedure TUserInterface.ShowError(parentForm : TForm; const ACaption, APrompt : String); begin - MessageDlg(ACaption, APrompt, mtError, [mbOK], 0, mbOK); + {$IFDEF FPC}MessageDlg{$ELSE}TaskMessageDlg{$ENDIF}(ACaption, APrompt, mtError, [mbOK], 0, mbOK); end; class function TUserInterface.AskQuestion(parentForm: TForm; AType:TMsgDlgType; const ACaption, APrompt : String; buttons: TMsgDlgButtons) : TMsgDlgBtn; var modalResult : TModalResult; begin - modalResult := MessageDlg(ACaption, APrompt, AType, Buttons, 0, mbNo); + modalResult := {$IFDEF FPC}MessageDlg{$ELSE}TaskMessageDlg{$ENDIF}(ACaption, APrompt, AType, Buttons, 0, mbNo); case modalResult of mrYes: Result := mbYes; mrNo: Result := mbNo; @@ -1067,8 +1700,19 @@ class function TUserInterface.AskEnterString(parentForm: TForm; const ACaption, end; class function TUserInterface.AskEnterProtectedString(parentForm: TForm; const ACaption, APrompt : String; var Value : String) : Boolean; +{$IFNDEF FPC} +var + LValues: TArray; +{$ENDIF} begin + {$IFDEF FPC} Result := InputQuery(ACaption, APrompt, true, Value); + {$ELSE} + LValues := TArray.Create(Value); + Result := InputQuery(ACaption, [#31 + APrompt], LValues); + if Result then + Value := LValues[0]; + {$ENDIF} end; {%endregion} @@ -1077,7 +1721,7 @@ class function TUserInterface.AskEnterProtectedString(parentForm: TForm; const A class procedure TUserInterface.ShowAccountExplorer; begin - FUILock.Acquire; + FUILock.AcquireWriteLock; try if not Assigned(FAccountExplorer) then begin FAccountExplorer := TFRMAccountExplorer.Create(FMainForm); @@ -1087,13 +1731,13 @@ class procedure TUserInterface.ShowAccountExplorer; FAccountExplorer.Refresh; FAccountExplorer.Show; finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; class procedure TUserInterface.ShowBlockExplorer; begin - FUILock.Acquire; + FUILock.AcquireWriteLock; try if not Assigned(FBlockExplorerForm) then begin FBlockExplorerForm := TFRMBlockExplorer.Create(FMainForm); @@ -1102,13 +1746,13 @@ class procedure TUserInterface.ShowBlockExplorer; end; FBlockExplorerForm.Show; finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; class procedure TUserInterface.ShowOperationsExplorer; begin - FUILock.Acquire; + FUILock.AcquireWriteLock; try if not Assigned(FOperationsExplorerForm) then begin FOperationsExplorerForm := TFRMOperationExplorer.Create(FMainForm); @@ -1117,13 +1761,13 @@ class procedure TUserInterface.ShowOperationsExplorer; end; FOperationsExplorerForm.Show; finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; class procedure TUserInterface.ShowPendingOperations; begin - FUILock.Acquire; + FUILock.AcquireWriteLock; try if not Assigned(FPendingOperationForm) then begin FPendingOperationForm := TFRMPendingOperations.Create(FMainForm); @@ -1132,28 +1776,28 @@ class procedure TUserInterface.ShowPendingOperations; end; FPendingOperationForm.Show; finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; class procedure TUserInterface.ShowMessagesForm; begin - FUILock.Acquire; + FUILock.AcquireWriteLock; try if not Assigned(FMessagesForm) then begin FMessagesForm := TFRMMessages.Create(FMainForm); - FMessagesForm.CloseAction:= caFree; - FMessagesForm.OnDestroyed:= Self.OnSubFormDestroyed; + FMessagesForm.CloseAction:= caHide; end; + FMessagesForm.MarkRead; FMessagesForm.Show; finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; class procedure TUserInterface.ShowNodesForm; begin - FUILock.Acquire; + FUILock.AcquireWriteLock; try if not Assigned(FNodesForm) then begin FNodesForm := TFRMNodes.Create(FMainForm); @@ -1162,22 +1806,22 @@ class procedure TUserInterface.ShowNodesForm; end; FNodesForm.Show; finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; class procedure TUserInterface.ShowLogsForm; begin - FUILock.Acquire; + if not TSettings.ShowLogs then Exit; + FUILock.AcquireWriteLock; try if not Assigned(FLogsForm) then begin FLogsForm := TFRMLogs.Create(FMainForm); - FLogsForm.CloseAction:= caFree; - FLogsForm.OnDestroyed:= Self.OnSubFormDestroyed; + FLogsForm.CloseAction:= caHide; end; FLogsForm.Show; finally - FUILock.Release; + FUILock.ReleaseWriteLock; end; end; @@ -1196,9 +1840,17 @@ class procedure TUserInterface.ShowSyncDialog; {%region Public methods} class procedure TUserInterface.CheckNodeIsReady; -Var errorMessage : AnsiString; -begin - if Not TNode.Node.IsReady(errorMessage) then begin +Var errorMessage, LBankDiagnostic : String; + LNode : TNode; + LProgress : TProgressInfo; +begin + LNode := TNode.Node; + if Not LNode.IsReady(errorMessage) then begin + // Recreate download text here; bank/server failures keep their own reason. + if LNode.Bank.IsReady(LBankDiagnostic) then + if LNode.NetServer.Active then + if TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then + errorMessage := FormatProgressInfo(LProgress); Raise Exception.Create('You cannot do this operation now:'+#10+#10+errorMessage); end; end; @@ -1209,7 +1861,7 @@ class procedure TUserInterface.CheckNodeIsReady; class function TUserInterface.GetEnabled : boolean; begin - Result := FMainForm.Enabled; + Result := Assigned(FMainForm) and FMainForm.Enabled; end; class procedure TUserInterface.SetEnabled(ABool: boolean); @@ -1218,14 +1870,26 @@ class procedure TUserInterface.SetEnabled(ABool: boolean); FMainForm.Enabled:=ABool; end; -class procedure TUserInterface.SetState(AState : TUserInterfaceState); static; +class procedure TUserInterface.SetState(AState : TUserInterfaceState); begin + // Connectivity, progress and deferred loader events cannot clear a latched + // storage error; only constructing a fresh node after restart can do that. + if Assigned(FNode) and FNode.HasStorageFailure then AState := uisError; if AState = FState then exit; FState := AState; - if (FState = uisLoaded) then - NotifyLoadedEvent(nil); + if FState = uisLoaded then begin + // Loading listeners may publish a snapshot synchronously. Finish this + // lifecycle dispatch before those events can choose the connected state. + FDispatchingLoaded := True; + try + NotifyLoadedEvent(nil); + finally + FDispatchingLoaded := False; + end; + end; NotifyStateChanged(nil); + if FState = uisLoaded then RefreshNodeState; end; class procedure TUserInterface.NotifyLoadedEvent(Sender: TObject); @@ -1233,9 +1897,10 @@ class procedure TUserInterface.NotifyLoadedEvent(Sender: TObject); TUserInterface.FLoaded.Invoke(Sender); end; -class procedure TUserInterface.NotifyLoadingEvent(Sender: TObject; const message : String; curPos, totalCount : Int64); +class procedure TUserInterface.NotifyLoadingEvent(Sender: TObject; const AProgress : TProgressInfo); begin - TUserInterface.FLoading.Invoke(Sender, message, curPos, totalCount); + if FStopping then Exit; + TUserInterface.FLoading.Invoke(Sender, AProgress); end; class procedure TUserInterface.NotifyStateChanged(Sender: TObject); @@ -1245,54 +1910,63 @@ class procedure TUserInterface.NotifyStateChanged(Sender: TObject); class procedure TUserInterface.NotifyAccountsChangedEvent(Sender: TObject); begin + if FStopping then Exit; TUserInterface.FAccountsChanged.Invoke(Sender); end; class procedure TUserInterface.NotifyBlocksChangedEvent(Sender: TObject); begin + if FStopping then Exit; + RefreshNodeState; TUserInterface.FBlocksChanged.Invoke(Sender); end; class procedure TUserInterface.NotifyNodeMessageEventEvent(NetConnection: TNetConnection; MessageData: String); begin + if FStopping then Exit; TUserInterface.FNodeMessageEvent.Invoke(NetConnection, MessageData); end; class procedure TUserInterface.NotifyReceivedHelloMessageEvent(Sender: TObject); begin + if FStopping then Exit; + RefreshNodeState; TUserInterface.FReceivedHelloMessage.Invoke(Sender);; end; class procedure TUserInterface.NotifyNetStatisticsChangedEvent(Sender: TObject); begin + if FStopping then Exit; + RefreshNodeState; TUserInterface.FNetStatisticsChanged.Invoke(Sender); end; class procedure TUserInterface.NotifyNetConnectionsUpdatedEvent(Sender: TObject); begin + if FStopping then Exit; + RefreshNodeState; TUserInterface.FNetConnectionsUpdated.Invoke(Sender); end; class procedure TUserInterface.NotifyNetNodeServersUpdatedEvent(Sender: TObject); begin + if FStopping then Exit; + RefreshNodeState; TUserInterface.NetNodeServersUpdated.Invoke(Sender); end; class procedure TUserInterface.NotifyNetBlackListUpdatedEvent(Sender: TObject); begin + if FStopping then Exit; TUserInterface.FNetBlackListUpdated.Invoke(Sender); end; class procedure TUserInterface.NotifyMiningServerNewBlockFoundEvent(Sender: TObject); begin + if FStopping then Exit; TUserInterface.FMiningServerNewBlockFound.Invoke(Sender); end; -class procedure TUserInterface.NotifyUIRefreshTimerEvent(Sender: TObject); -begin - TUserInterface.FUIRefreshTimer.Invoke(Sender); -end; - {%endregion} initialization diff --git a/src/gui-experimental/UWalletLanguages.pas b/src/gui-experimental/UWalletLanguages.pas new file mode 100644 index 000000000..e8fc9eedd --- /dev/null +++ b/src/gui-experimental/UWalletLanguages.pas @@ -0,0 +1,301 @@ +unit UWalletLanguages; + +{ Copyright © 2026 Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. } + +{$IFDEF FPC} +{$mode delphi} +{$ENDIF} + +{ Finds installed translation catalogs and reads, saves and applies the wallet + language preference for Delphi and Lazarus forms. } + +{ Public types in this unit + + Type | Purpose + -----------------+------------------------------------------------------------------------- + TWalletLanguage | Pairs a translation code with its display name. + TWalletLanguages | Discovers catalogs and manages the wallet language preference. +} + +interface + +uses SysUtils, UCommon, UAppParams; + +type + TWalletLanguage = record + Code, DisplayName: String; + end; + + { Product composition over the installed gettext implementation. Catalogs keep + the classic locale//LC_MESSAGES/default.po/.mo layout. Apply at startup; + preferences changed in the UI take effect on the next start. } + + TWalletLanguages = class + public + class function LocaleDirectory: String; static; + class function Available(const ALocaleDirectory: String = ''): TArray; static; + class function ReadPreference(AParams: TAppParams = nil): String; static; + class procedure WritePreference(const ACode: String; AParams: TAppParams = nil); static; + class function Apply(const ALanguage: String; const ALocaleDirectory: String = ''): String; static; + end; + +implementation + +{$I ..\config.inc} + +uses + Classes, UMemory, USettings, + {$IFDEF FPC}LCLTranslator, LResources, Forms, Translations, GetText + {$ELSE}Vcl.Forms, Vcl.AppEvnts, gnugettext{$ENDIF}; + +{$IFDEF FPC} +type + TCatalogFile = class(TMOFile) + function HasTranslations: Boolean; + end; + +function TCatalogFile.HasTranslations: Boolean; +begin + // msgfmt omits untranslated entries; the header alone is not a translation. + Result := StringCount > 1; +end; +{$ENDIF} + +{$IFNDEF FPC} +type + { The VCL event dispatcher preserves other application subscribers. GNU + gettext owns its original-text marker on each translated component, so no + registry of form pointers can outlive a destroyed form. } + TWalletFormTranslator = class(TComponent) + private + FEvents: TApplicationEvents; + FApplying: Boolean; + procedure OnIdle(Sender: TObject; var Done: Boolean); + procedure OnActivateOrModal(Sender: TObject); + procedure TranslateComponent(AComponent: TComponent; ARetranslate: Boolean); + public + constructor Create(AOwner: TComponent); override; + procedure TranslateForms(ARetranslate: Boolean); + end; + +var + VclFormTranslator: TWalletFormTranslator; + +constructor TWalletFormTranslator.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + FEvents := TApplicationEvents.Create(Self); + FEvents.OnIdle := OnIdle; + FEvents.OnActivate := OnActivateOrModal; + FEvents.OnModalBegin := OnActivateOrModal; +end; + +procedure TWalletFormTranslator.TranslateComponent(AComponent: TComponent; ARetranslate: Boolean); +begin + if (csLoading in AComponent.ComponentState) or + (csDestroying in AComponent.ComponentState) then Exit; + if not Assigned(AComponent.FindComponent('GNUgettextMarker')) then + gnugettext.TranslateComponent(AComponent) + else if ARetranslate then + gnugettext.RetranslateComponent(AComponent); +end; + +procedure TWalletFormTranslator.TranslateForms(ARetranslate: Boolean); +var + i: Integer; +begin + if FApplying then Exit; + FApplying := True; + try + for i := Screen.CustomFormCount - 1 downto 0 do + if i < Screen.CustomFormCount then TranslateComponent(Screen.CustomForms[i], ARetranslate); + for i := Screen.DataModuleCount - 1 downto 0 do + if i < Screen.DataModuleCount then TranslateComponent(Screen.DataModules[i], ARetranslate); + finally + FApplying := False; + end; +end; + +procedure TWalletFormTranslator.OnIdle(Sender: TObject; var Done: Boolean); +begin + // New modeless/embedded forms need the same translation as startup forms. + // Leave Done unchanged so this subscriber never turns idle into a busy loop. + TranslateForms(False); +end; + +procedure TWalletFormTranslator.OnActivateOrModal(Sender: TObject); +begin + // Modal forms have finished construction here, before their first showing. + TranslateForms(False); +end; +{$ENDIF} + +function IsLanguageCode(const ACode: String): Boolean; +var + i: Integer; +begin + Result := ACode <> ''; + for i := 1 to Length(ACode) do + if not (ACode[i] in ['a'..'z', 'A'..'Z', '_', '-']) then exit(false); +end; + +function LanguageName(const ACode: String): String; +begin + {$IFNDEF FPC} + Result := CodeToNativeLanguage(ACode); + {$ELSE} + // Native labels for the supplied catalogs; additional languages retain their code. + if ACode = 'en' then Result := 'English' + else if ACode = 'nl' then Result := 'Nederlands' + else if ACode = 'de' then Result := 'Deutsch' + else if ACode = 'fr' then Result := 'Français' + else Result := ACode; + {$ENDIF} +end; + +function HasCatalogTranslations(const ADirectory, ACode: String): Boolean; +var + disposed: TDisposables; + path: String; + {$IFDEF FPC} + po: TPOFile; + mo: TCatalogFile; + {$ELSE} + catalog: TFileStream; + mo: gnugettext.TMoFile; + {$ENDIF} +begin + Result := false; + path := IncludeTrailingPathDelimiter(ADirectory) + ACode + PathDelim + + 'LC_MESSAGES' + PathDelim + 'default'; + try + {$IFDEF FPC} + if FileExists(path + '.po') then begin + po := disposed.AddObject(TPOFile.Create(path + '.po')) as TPOFile; + Result := po.Statistics.Translated > 0; + end else if FileExists(path + '.mo') then begin + mo := disposed.AddObject(TCatalogFile.Create(path + '.mo')) as TCatalogFile; + Result := mo.HasTranslations; + end; + {$ELSE} + if FileExists(path + '.mo') then begin + catalog := disposed.AddObject(TFileStream.Create(path + '.mo', + fmOpenRead or fmShareDenyNone)) as TFileStream; + if catalog.Size < 28 then Exit; + // Use the existing parser with an independent in-memory copy. A listing + // must inspect this exact directory without changing the active domain, + // retaining file mappings or advertising header-only catalogs. + mo := disposed.AddObject(gnugettext.TMoFile.Create(path + '.mo', 0, 0, + False, '')) as gnugettext.TMoFile; + Result := mo.StringCount > 1; + end; + {$ENDIF} + except + // A damaged optional catalog must not prevent opening the wallet or options. + on E: Exception do Result := false; + end; +end; + +class function TWalletLanguages.LocaleDirectory: String; +begin + Result := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + 'locale'; +end; + +class function TWalletLanguages.Available(const ALocaleDirectory: String): TArray; +var + directory: String; + search: TSearchRec; + language: TWalletLanguage; +begin + Result := nil; + language.Code := 'en'; + language.DisplayName := LanguageName(language.Code); + TArrayTool.Add(Result, language); + directory := ALocaleDirectory; + if directory = '' then directory := LocaleDirectory; + if FindFirst(IncludeTrailingPathDelimiter(directory) + '*', faDirectory, search) <> 0 then exit; + try + repeat + if (search.Attr and faDirectory <> 0) and IsLanguageCode(search.Name) and + (not SameText(search.Name, 'en')) and HasCatalogTranslations(directory, search.Name) then begin + language.Code := search.Name; + language.DisplayName := LanguageName(language.Code); + TArrayTool.Add(Result, language); + end; + until FindNext(search) <> 0; + finally + FindClose(search); + end; +end; + +class function TWalletLanguages.ReadPreference(AParams: TAppParams): String; +begin + if not Assigned(AParams) then AParams := TSettings.AppParams; + Result := AParams.ParamByName[CT_PARAM_UILanguage].GetAsString('en'); +end; + +class procedure TWalletLanguages.WritePreference(const ACode: String; AParams: TAppParams); +begin + if not IsLanguageCode(ACode) then + raise EArgumentException.Create('Invalid interface language code'); + if not Assigned(AParams) then AParams := TSettings.AppParams; + AParams.ParamByName[CT_PARAM_UILanguage].SetAsString(ACode); +end; + +class function TWalletLanguages.Apply(const ALanguage: String; const ALocaleDirectory: String): String; +var + directory, catalog: String; + {$IFDEF FPC} + translator: TUpdateTranslator; + disposed: TDisposables; + i: Integer; + {$ENDIF} +begin + directory := ALocaleDirectory; + if directory = '' then directory := LocaleDirectory; + Result := 'en'; + if IsLanguageCode(ALanguage) and ((ALanguage = 'en') or + HasCatalogTranslations(directory, ALanguage)) then Result := ALanguage; + {$IFDEF FPC} + // Use the LCL's existing translator with the exact GNU catalog path. Its + // default search does not resolve a custom directory's LC_MESSAGES layout. + catalog := IncludeTrailingPathDelimiter(directory) + Result + PathDelim + + 'LC_MESSAGES' + PathDelim + 'default'; + translator := nil; + if FileExists(catalog + '.po') then begin + translator := disposed.AddObject(TPOTranslator.Create(catalog + '.po')) as TUpdateTranslator; + Translations.TranslateResourceStrings(catalog + '.po'); + end else if FileExists(catalog + '.mo') then begin + translator := disposed.AddObject(TDefaultTranslator.Create(catalog + '.mo')) as TUpdateTranslator; + GetText.TranslateResourceStrings(catalog + '.mo'); + end; + FreeAndNil(LRSTranslator); + LRSTranslator := translator; + disposed.RemoveObject(translator); + if Assigned(translator) then begin + for i := 0 to Screen.CustomFormCount - 1 do + translator.UpdateTranslation(Screen.CustomForms[i]); + for i := 0 to Screen.DataModuleCount - 1 do + translator.UpdateTranslation(Screen.DataModules[i]); + end; + {$ELSE} + bindtextdomain('default', directory); + textdomain('default'); + UseLanguage(Result); + if not Assigned(VclFormTranslator) then VclFormTranslator := TWalletFormTranslator.Create(nil); + VclFormTranslator.TranslateForms(True); + {$ENDIF} +end; + +{$IFNDEF FPC} +initialization + VclFormTranslator := nil; + +finalization + FreeAndNil(VclFormTranslator); +{$ENDIF} + +end. diff --git a/src/gui-experimental/UWalletStartup.pas b/src/gui-experimental/UWalletStartup.pas new file mode 100644 index 000000000..0cac5c4f2 --- /dev/null +++ b/src/gui-experimental/UWalletStartup.pas @@ -0,0 +1,96 @@ +unit UWalletStartup; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$IFDEF FPC} +{$mode delphi} +{$ENDIF} + +{ Parses startup options for data location, offline mode and startup dialogs + without opening wallet data or sockets. } + +{ Public types in this unit + + Type | Purpose + ----------------------+------------------------------------------------------------------------- + TWalletStartupOptions | Carries parsed data-directory, offline, dialog and help options. +} + +interface + +uses + SysUtils; + +type + + { Options are parsed without opening settings, keys, blockchain files or sockets. } + + TWalletStartupOptions = record + DataDirectory : String; + Offline : Boolean; + NoStartupDialogs : Boolean; + ShowHelp : Boolean; + end; + +function TryParseWalletStartupOptions(const AArguments : array of String; out AOptions : TWalletStartupOptions; out AError : String) : Boolean; +function WalletStartupHelp : String; + +implementation + +function TryParseWalletStartupOptions(const AArguments : array of String; out AOptions : TWalletStartupOptions; out AError : String) : Boolean; +const + CT_DataDirectoryPrefix = '--data-dir='; +var + i : Integer; + LArgument : String; + LHasDataDirectory : Boolean; +begin + AOptions.DataDirectory := ''; + AOptions.Offline := false; + AOptions.NoStartupDialogs := false; + AOptions.ShowHelp := false; + AError := ''; + LHasDataDirectory := false; + Result := false; + for i := Low(AArguments) to High(AArguments) do begin + LArgument := AArguments[i]; + if LArgument = '--offline' then + AOptions.Offline := true + else if LArgument = '--no-startup-dialogs' then + AOptions.NoStartupDialogs := true + else if (LArgument = '--help') or (LArgument = '-h') then + AOptions.ShowHelp := true + else if Copy(LArgument, 1, Length(CT_DataDirectoryPrefix)) = CT_DataDirectoryPrefix then begin + if LHasDataDirectory then begin + AError := 'Specify --data-dir only once.'; + Exit; + end; + AOptions.DataDirectory := Copy(LArgument, Length(CT_DataDirectoryPrefix) + 1, MaxInt); + if Trim(AOptions.DataDirectory) = '' then begin + AError := '--data-dir requires a non-empty directory.'; + Exit; + end; + AOptions.DataDirectory := ExpandFileName(AOptions.DataDirectory); + LHasDataDirectory := true; + end else begin + AError := 'Unknown argument: ' + LArgument; + Exit; + end; + end; + Result := true; +end; + +function WalletStartupHelp : String; +begin + Result := 'PascalCoin experimental wallet' + sLineBreak + + ' --data-dir= Use this directory for all wallet and node data.' + sLineBreak + + ' --offline Disable peer discovery, connections and all servers.' + sLineBreak + + ' --no-startup-dialogs Suppress startup error dialogs.' + sLineBreak + + ' --help, -h Show this help without opening wallet data.'; +end; + +end. diff --git a/src/gui-experimental/UWalletStartupTask.pas b/src/gui-experimental/UWalletStartupTask.pas new file mode 100644 index 000000000..8074287b8 --- /dev/null +++ b/src/gui-experimental/UWalletStartupTask.pas @@ -0,0 +1,175 @@ +unit UWalletStartupTask; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$IFDEF FPC} +{$mode delphi} +{$ENDIF} + +{ Runs an owned, cancellable node task with queued progress and completion + notifications. Cancellation joins the worker and removes callbacks before + its receivers are destroyed. } + +{ Public types in this unit + + Type | Purpose + -------------------+------------------------------------------------------------------------- + TWalletStartupTask | Runs cancellable node work and delivers progress/completion events. +} + +interface + +uses + Classes, SysUtils, {$IFNDEF FPC}System.SyncObjs,{$ENDIF} UThread, UAccounts; + +type + + { An owned, single-use node worker, using the core's thread tracking, lock and + progress event type. Call CancelAndWait before destroying its receivers. + The extra lifecycle is needed because queued methods must not outlive either + the worker or UI graph. The owner retains errors until the task is freed. } + + TWalletStartupTask = class(TPCThread) + private + FCancelled : LongInt; + FHasStarted : Boolean; + FProgressLock : TReadWriteLock; + FProgressQueued : Boolean; + FProgress : TProgressInfo; + FErrorMessage : String; + FOnProgress : TProgressNotify; + FOnCompleted : TNotifyEvent; + function GetCancelled : Boolean; + procedure DispatchProgress; + procedure DispatchCompleted; + protected + procedure BCExecute; override; + procedure ExecuteLoad; virtual; abstract; + procedure CheckCancelled; + procedure ReportProgress(const AProgress : TProgressInfo); + public + constructor Create; + destructor Destroy; override; + procedure Start; + procedure Cancel; + procedure CancelAndWait; + property Cancelled : Boolean read GetCancelled; + property ErrorMessage : String read FErrorMessage; + property OnProgress : TProgressNotify read FOnProgress write FOnProgress; + property OnCompleted : TNotifyEvent read FOnCompleted write FOnCompleted; + end; + +implementation + +constructor TWalletStartupTask.Create; +begin + FProgressLock := TReadWriteLock.Create('WalletStartup.Progress',lrpNoRecursion); + inherited Create(true); + FreeOnTerminate := false; +end; + +destructor TWalletStartupTask.Destroy; +begin + CancelAndWait; + inherited; + FProgressLock.Free; +end; + +procedure TWalletStartupTask.Start; +begin + if FHasStarted then + raise EThread.Create('The wallet startup task has already started.'); + FHasStarted := true; + inherited Start; +end; + +function TWalletStartupTask.GetCancelled : Boolean; +begin + {$IFDEF FPC} + Result := InterlockedCompareExchange(FCancelled, 0, 0) <> 0; + {$ELSE} + Result := TInterlocked.CompareExchange(FCancelled, 0, 0) <> 0; + {$ENDIF} +end; + +procedure TWalletStartupTask.CheckCancelled; +begin + if Cancelled then Abort; +end; + +procedure TWalletStartupTask.Cancel; +begin + {$IFDEF FPC} + InterlockedExchange(FCancelled, 1); + {$ELSE} + TInterlocked.Exchange(FCancelled, 1); + {$ENDIF} + Terminate; +end; + +procedure TWalletStartupTask.CancelAndWait; +begin + Cancel; + if not FHasStarted then Start; + WaitFor; + TThread.RemoveQueuedEvents(Self); +end; + +procedure TWalletStartupTask.BCExecute; +begin + try + CheckCancelled; + ExecuteLoad; + except + on E : Exception do + if not Cancelled then + FErrorMessage := E.ClassName + ': ' + E.Message; + end; + if not Cancelled then Queue(DispatchCompleted); +end; + +procedure TWalletStartupTask.ReportProgress(const AProgress : TProgressInfo); +var + LQueue : Boolean; +begin + CheckCancelled; + FProgressLock.AcquireWriteLock; + try + FProgress := AProgress; + LQueue := not FProgressQueued; + FProgressQueued := true; + finally + FProgressLock.ReleaseWriteLock; + end; + if LQueue then Queue(DispatchProgress); +end; + +procedure TWalletStartupTask.DispatchProgress; +var + LProgress : TProgressInfo; +begin + // Dispatch also clears the queued flag, so it retains write ownership. + FProgressLock.AcquireWriteLock; + try + // Copy one complete measurement, including managed filename/peer metadata, + // before dropping the lock. Formatting never runs on the producer thread. + LProgress := FProgress; + FProgressQueued := false; + finally + FProgressLock.ReleaseWriteLock; + end; + if (not Cancelled) and Assigned(FOnProgress) then + FOnProgress(Self, LProgress); +end; + +procedure TWalletStartupTask.DispatchCompleted; +begin + if (not Cancelled) and Assigned(FOnCompleted) then + FOnCompleted(Self); +end; + +end. diff --git a/src/gui-experimental/core.utils/UAccountSelectorData.pas b/src/gui-experimental/core.utils/UAccountSelectorData.pas new file mode 100644 index 000000000..ff7e08fd7 --- /dev/null +++ b/src/gui-experimental/core.utils/UAccountSelectorData.pas @@ -0,0 +1,586 @@ +unit UAccountSelectorData; + +{ Copyright © 2026 Herman Schoenfeld . + Distributed under the MIT software license; see LICENSE. + Confirmed account queries and type statistics for the experimental selector. + Controls supply immutable filters; only owned workers read the safebox. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +{ Queries confirmed accounts using owner, sale, balance and name filters. + Caches matching account numbers for paging and calculates account-type + totals on a consistent chain snapshot. } + +{ Public types in this unit + + Type | Purpose + --------------------------------+------------------------------------------------------------------------- + EAccountSelectorSnapshotChanged | Reports that the queried chain snapshot changed during a read. + TAccountSelectorChain | Captures the safebox identity and chain totals for a query. + TAccountSelectorDataSource | Filters and pages confirmed accounts and calculates type statistics. + TAccountSelectorFilter | Carries owner, sale, balance and account-name query filters. + TAccountSelectorTotal | Pairs an account count with its total balance. + TAccountSelectorTypeStats | Groups account counts and balances by account-type digits. +} + +interface + +{$I ..\..\config.inc} + +uses Classes, SysUtils, SyncObjs, Generics.Collections, UAccounts, UPCDataTypes, + UCommon, UCommon.Data, UDataSources, UCache, UMemory, UBaseTypes, UThread; + +type + TAccountSelectorFilter = record + MyAccounts, SpecificOwner: Boolean; + OwnerKeys, WalletKeys: TArray; + OnlyForSale, OnlyForPublicSale, OnlyForPrivateSaleToMe: Boolean; + MinBalance, MaxBalance: Int64; + Name: TRawBytes; + end; + + TAccountSelectorChain = record + SafeBox: TPCSafeBox; + Blocks, Accounts: Integer; + Balance: Int64; + Hash: TRawBytes; + end; + + TAccountSelectorTotal = record + Accounts: Integer; + Balance: Int64; + end; + + TAccountSelectorTypeStats = record + Chain: TAccountSelectorChain; + Zero, Invalid: TAccountSelectorTotal; + Digits: array[0..6,1..4,0..9] of TAccountSelectorTotal; + end; + + EAccountSelectorSnapshotChanged = class(Exception); + + TAccountSelectorDataSource = class(TAccountsDataSourceBase) + private type + TQuerySnapshot = class + Chain: TAccountSelectorChain; + ImplicitAll: Boolean; + Numbers: TArray; + Total: TAccountSelectorTotal; + end; + TSnapshotCache = TActionCache; + private + FSafeBox: TPCSafeBox; + FConfigLock, FQueryLock: UThread.TReadWriteLock; + FFilter, FBuildingFilter: TAccountSelectorFilter; + FBuildingChain: TAccountSelectorChain; + FGeneration: UInt64; + FCache: TSnapshotCache; + FSummary: TAccountSelectorTotal; + FSummaryReady, FPublishedImplicitAll: Boolean; + FPublishedNumbers: TArray; + function FetchSnapshot(const AKey: String): TQuerySnapshot; + function CurrentSnapshot(out AGeneration: UInt64): TQuerySnapshot; + procedure PublishSnapshot(ASnapshot: TQuerySnapshot; AGeneration: UInt64); + function EstimateSnapshot(const AValue: TQuerySnapshot): SizeInt; + function AcceptAccount(const AAccount: TAccount; const AFilter: TAccountSelectorFilter): Boolean; + procedure ReadConfiguration(out AFilter: TAccountSelectorFilter; out AGeneration: UInt64; out ASafeBox: TPCSafeBox); + procedure ReadContiguous(const AChain: TAccountSelectorChain; AStart, ACount: Integer; out ARows: TArray); + procedure CheckChain(const AChain: TAccountSelectorChain); + protected + // Test backends replace storage only; filtering, paging and workers stay real. + function ReadChain(ASafeBox: TPCSafeBox): TAccountSelectorChain; virtual; + procedure ReadKeyNumbers(const AChain: TAccountSelectorChain; const AKey: TAccountKey; const ANumbers: TList); virtual; + procedure ReadAccounts(const AChain: TAccountSelectorChain; const ANumbers: TArray; out ARows: TArray); virtual; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; override; + public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + procedure Configure(ASafeBox: TPCSafeBox; const AFilter: TAccountSelectorFilter); + procedure FetchAll(const AContainer: TList); override; + procedure CalculateTypeStats(out AStats: TAccountSelectorTypeStats); + function TryGetSummary(out ATotal: TAccountSelectorTotal): Boolean; + function PublishedAccountIndex(AAccount: Cardinal): Integer; + end; + +procedure CheckAccountSelectorCancelled; + +implementation + +uses Math, UCrypto; + +const + SELECTOR_READ_CHUNK = 128; + SELECTOR_SNAPSHOT_ATTEMPTS = 3; + +procedure CheckAccountSelectorCancelled; +begin + // Delphi does not permit CheckTerminated on an externally created UI thread. + if TThread.CurrentThread.ThreadID = MainThreadID then Exit; + if TThread.CheckTerminated then Abort; +end; + +function ContainsKey(const AKeys: TArray; const AKey: TAccountKey): Boolean; +var I: Integer; +begin + for I := 0 to High(AKeys) do + if TAccountComp.EqualAccountKeys(AKeys[I],AKey) then Exit(True); + Result := False; +end; + +constructor TAccountSelectorDataSource.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + FConfigLock := UThread.TReadWriteLock.Create('AccountSelector.ConfigLock',UThread.lrpSupportsRecursion); + FQueryLock := UThread.TReadWriteLock.Create('AccountSelector.QueryLock',UThread.lrpSupportsRecursion); + FGeneration := 1; + FCache := TSnapshotCache.Create(Self, + TCacheValueFetcher.FromObjectFunction(FetchSnapshot), + TCacheSizeEstimator.FromObjectFunction(EstimateSnapshot), + idpFree,TCacheReapPolicy.crpOldest,TCacheExpirationPolicy.epNone, + TCacheNullValuePolicy.nvpCacheNormally,2,TTimeSpan.MaxValue); +end; + +destructor TAccountSelectorDataSource.Destroy; +begin + FCache.Free; + FQueryLock.Free; + FConfigLock.Free; + inherited Destroy; +end; + +procedure TAccountSelectorDataSource.Configure(ASafeBox: TPCSafeBox; const AFilter: TAccountSelectorFilter); +begin + // Configuration never waits for the cache or a provider's storage/query lock. + FConfigLock.AcquireWriteLock; + try + FSafeBox := ASafeBox; + FFilter := AFilter; + FFilter.OwnerKeys := Copy(AFilter.OwnerKeys); + FFilter.WalletKeys := Copy(AFilter.WalletKeys); + FFilter.Name := Copy(AFilter.Name); + Inc(FGeneration); + FSummaryReady := False; + finally + FConfigLock.ReleaseWriteLock; + end; + // The optional inherited generic-query cache has its own logical revision. + // Invalidating it is atomic and does not wait for an active provider/cache. + InvalidateSnapshot; +end; + +procedure TAccountSelectorDataSource.ReadConfiguration(out AFilter: TAccountSelectorFilter; out AGeneration: UInt64; out ASafeBox: TPCSafeBox); +begin + FConfigLock.AcquireReadLock; + try + AFilter := FFilter; + AGeneration := FGeneration; + ASafeBox := FSafeBox; + finally + FConfigLock.ReleaseReadLock; + end; +end; + +function TAccountSelectorDataSource.ReadChain(ASafeBox: TPCSafeBox): TAccountSelectorChain; +begin + CheckAccountSelectorCancelled; + Result := Default(TAccountSelectorChain); + Result.SafeBox := ASafeBox; + if not Assigned(Result.SafeBox) then Exit; + Result.SafeBox.StartThreadSafe; + try + CheckAccountSelectorCancelled; + Result.Blocks := Result.SafeBox.BlocksCount; + Result.Accounts := Result.SafeBox.AccountsCount; + Result.Balance := Result.SafeBox.TotalBalance; + Result.Hash := Copy(Result.SafeBox.SafeBoxHash); + finally + Result.SafeBox.EndThreadSave; + end; +end; + +procedure TAccountSelectorDataSource.CheckChain(const AChain: TAccountSelectorChain); +begin + if (AChain.SafeBox.BlocksCount <> AChain.Blocks) or + (TBaseType.BinStrComp(AChain.SafeBox.SafeBoxHash,AChain.Hash) <> 0) then + raise EAccountSelectorSnapshotChanged.Create('The confirmed account snapshot changed'); +end; + +procedure TAccountSelectorDataSource.ReadKeyNumbers(const AChain: TAccountSelectorChain; const AKey: TAccountKey; const ANumbers: TList); +var LList: TAccountsNumbersList; +begin + CheckAccountSelectorCancelled; + ANumbers.Clear; + if not Assigned(AChain.SafeBox) then Exit; + AChain.SafeBox.StartThreadSafe; + try + CheckAccountSelectorCancelled; + CheckChain(AChain); + // Both safebox implementations expose their existing public-key index. + // An absent key supplies no numbers; it never means the entire chain. + LList := AChain.SafeBox.OrderedAccountKeysList.GetAccountsUsingThisKey(AKey); + if Assigned(LList) then LList.FillList(0,LList.Count,ANumbers); + CheckAccountSelectorCancelled; + finally + AChain.SafeBox.EndThreadSave; + end; +end; + +procedure TAccountSelectorDataSource.ReadAccounts(const AChain: TAccountSelectorChain; const ANumbers: TArray; out ARows: TArray); +var I: Integer; +begin + CheckAccountSelectorCancelled; + SetLength(ARows,Length(ANumbers)); + if not Assigned(AChain.SafeBox) then Exit; + AChain.SafeBox.StartThreadSafe; + try + CheckChain(AChain); + for I := 0 to High(ANumbers) do begin + CheckAccountSelectorCancelled; + ARows[I] := AChain.SafeBox.Account(ANumbers[I]); + end; + finally + AChain.SafeBox.EndThreadSave; + end; +end; + +procedure TAccountSelectorDataSource.ReadContiguous(const AChain: TAccountSelectorChain; AStart, ACount: Integer; out ARows: TArray); +var LNumbers: TArray; I: Integer; +begin + SetLength(LNumbers,ACount); + for I := 0 to ACount-1 do LNumbers[I] := AStart+I; + ReadAccounts(AChain,LNumbers,ARows); +end; + +function TAccountSelectorDataSource.AcceptAccount(const AAccount: TAccount; const AFilter: TAccountSelectorFilter): Boolean; +begin + Result := False; + if AFilter.MyAccounts then begin + if AFilter.SpecificOwner then begin + if not ContainsKey(AFilter.OwnerKeys,AAccount.accountInfo.accountKey) then Exit; + end else if not AFilter.OnlyForPrivateSaleToMe then + if not ContainsKey(AFilter.OwnerKeys,AAccount.accountInfo.accountKey) then Exit; + end; + if AFilter.OnlyForSale and not TAccountComp.IsAccountForSaleOrSwap(AAccount.accountInfo) then Exit; + if AFilter.OnlyForPublicSale and not TAccountComp.IsAccountForPublicSale(AAccount.accountInfo) then Exit; + if AFilter.OnlyForPrivateSaleToMe then + if not ((TAccountComp.IsAccountForPrivateSale(AAccount.accountInfo) or + TAccountComp.IsAccountForAccountSwap(AAccount.accountInfo)) and + ContainsKey(AFilter.WalletKeys,AAccount.accountInfo.new_publicKey)) then Exit; + if AAccount.balance < UInt64(Max(0,AFilter.MinBalance)) then Exit; + if (AFilter.MaxBalance >= 0) and (AAccount.balance > UInt64(AFilter.MaxBalance)) then Exit; + // FindIn is zero-based: a match at the start of the name is valid. + if (Length(AFilter.Name) > 0) and (TBaseType.FindIn(AFilter.Name,AAccount.name) < 0) then Exit; + Result := True; +end; + +function TAccountSelectorDataSource.EstimateSnapshot(const AValue: TQuerySnapshot): SizeInt; +begin + Result := 1; +end; + +function TAccountSelectorDataSource.CurrentSnapshot(out AGeneration: UInt64): TQuerySnapshot; +var LKey: String; LSafeBox: TPCSafeBox; +begin + ReadConfiguration(FBuildingFilter,AGeneration,LSafeBox); + FBuildingChain := ReadChain(LSafeBox); + LKey := UIntToStr(AGeneration)+':'+TCrypto.ToHexaString(FBuildingChain.Hash); + Result := FCache[LKey]; +end; + +procedure TAccountSelectorDataSource.PublishSnapshot(ASnapshot: TQuerySnapshot; AGeneration: UInt64); +begin + FConfigLock.AcquireWriteLock; + try + if AGeneration = FGeneration then begin + FSummary := ASnapshot.Total; + FPublishedNumbers := ASnapshot.Numbers; + FPublishedImplicitAll := ASnapshot.ImplicitAll; + FSummaryReady := True; + end; + finally + FConfigLock.ReleaseWriteLock; + end; +end; + +function TAccountSelectorDataSource.FetchSnapshot(const AKey: String): TQuerySnapshot; +var LCandidates,LPart,LMatches: TList; LRows: TArray; + LNumbers: TArray; LScanAll: Boolean; I,J,LStart,LTake,LUnique: Integer; + LOwned: TDisposables; +begin + Result := TQuerySnapshot.Create; + try + Result.Chain := FBuildingChain; + LScanAll := not FBuildingFilter.MyAccounts or + (not FBuildingFilter.SpecificOwner and FBuildingFilter.OnlyForPrivateSaleToMe); + if FBuildingFilter.MyAccounts and (Length(FBuildingFilter.OwnerKeys) = 0) then Exit; + if FBuildingFilter.OnlyForPrivateSaleToMe and (Length(FBuildingFilter.WalletKeys) = 0) then Exit; + if LScanAll and not FBuildingFilter.OnlyForSale and + not FBuildingFilter.OnlyForPublicSale and not FBuildingFilter.OnlyForPrivateSaleToMe and + (FBuildingFilter.MinBalance <= 0) and (FBuildingFilter.MaxBalance < 0) and + (Length(FBuildingFilter.Name) = 0) then begin + // Unfiltered global pages use consecutive IDs and O(1) chain totals. + Result.ImplicitAll := True; + Result.Total.Accounts := FBuildingChain.Accounts; + Result.Total.Balance := FBuildingChain.Balance; + Exit; + end; + LCandidates := LOwned.AddObject(TList.Create) as TList; + LPart := LOwned.AddObject(TList.Create) as TList; + LMatches := LOwned.AddObject(TList.Create) as TList; + if not LScanAll then begin + for I := 0 to High(FBuildingFilter.OwnerKeys) do begin + ReadKeyNumbers(FBuildingChain,FBuildingFilter.OwnerKeys[I],LPart); + LCandidates.AddRange(LPart); + end; + LCandidates.Sort; + LUnique := 0; + for I := 0 to LCandidates.Count-1 do + if (LUnique=0) or (LCandidates[I] <> LCandidates[LUnique-1]) then begin + LCandidates[LUnique] := LCandidates[I]; + Inc(LUnique); + end; + LCandidates.Count := LUnique; + end; + LStart := 0; + while (LScanAll and (LStart < FBuildingChain.Accounts)) or + (not LScanAll and (LStart < LCandidates.Count)) do begin + CheckAccountSelectorCancelled; + if LScanAll then begin + LTake := Min(SELECTOR_READ_CHUNK,FBuildingChain.Accounts-LStart); + ReadContiguous(FBuildingChain,LStart,LTake,LRows); + end else begin + LTake := Min(SELECTOR_READ_CHUNK,LCandidates.Count-LStart); + SetLength(LNumbers,LTake); + for J := 0 to LTake-1 do LNumbers[J] := LCandidates[LStart+J]; + ReadAccounts(FBuildingChain,LNumbers,LRows); + end; + for I := 0 to High(LRows) do + if AcceptAccount(LRows[I],FBuildingFilter) then begin + LMatches.Add(LRows[I].account); + Inc(Result.Total.Balance,LRows[I].balance); + end; + Inc(LStart,LTake); + end; + Result.Numbers := LMatches.ToArray; + Result.Total.Accounts := Length(Result.Numbers); + except + Result.Free; + raise; + end; +end; + +function TAccountSelectorDataSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; +var LGeneration: UInt64; LSnapshot: TQuerySnapshot; LRows: TArray; + LNumbers: TArray; LStart,LTake,LRead,I,LAttempt: Integer; + LRow: Variant; LComplete: Boolean; +begin + // The selector's controls own its filters. Retain the base contract if a + // separate caller explicitly supplies generic search/sort criteria. + if Length(AParams.Filter) > 0 then Exit(inherited FetchPage(AParams,ADataTable)); + EnterLock; + try + // Query work updates shared provider/cache state, including on fetch paths. + FQueryLock.AcquireWriteLock; + try + LAttempt := 0; + repeat + Inc(LAttempt); + CheckAccountSelectorCancelled; + LComplete := False; + try + LSnapshot := CurrentSnapshot(LGeneration); + Result.TotalDataCount := LSnapshot.Total.Accounts; + if AParams.PageSize <= 0 then begin + Result.PageCount := Ord(Result.TotalDataCount > 0); + Result.PageIndex := Result.PageCount-1; + LStart := 0; + LTake := Result.TotalDataCount; + end else begin + Result.PageCount := (Int64(Result.TotalDataCount)+AParams.PageSize-1) div AParams.PageSize; + Result.PageIndex := Min(Max(0,AParams.PageIndex),Result.PageCount-1); + LStart := Max(0,Result.PageIndex)*AParams.PageSize; + LTake := Min(AParams.PageSize,Result.TotalDataCount-LStart); + end; + ADataTable.Columns := GetColumns; + SetLength(ADataTable.Rows,LTake); + FSnapshotBlockTip := Max(0,LSnapshot.Chain.Blocks-1); + LRead := 0; + while LRead < LTake do begin + SetLength(LNumbers,Min(SELECTOR_READ_CHUNK,LTake-LRead)); + for I := 0 to High(LNumbers) do + if LSnapshot.ImplicitAll then LNumbers[I] := LStart+LRead+I + else LNumbers[I] := LSnapshot.Numbers[LStart+LRead+I]; + ReadAccounts(LSnapshot.Chain,LNumbers,LRows); + for I := 0 to High(LRows) do begin + LRow := TDataRow.New(PtrInt(TAccountSelectorDataSource),ADataTable.Columns); + DehydrateItem(LRows[I],LRow); + ADataTable.Rows[LRead+I] := LRow; + end; + Inc(LRead,Length(LRows)); + end; + PublishSnapshot(LSnapshot,LGeneration); + LComplete := True; + except + on E: EAccountSelectorSnapshotChanged do begin + CheckAccountSelectorCancelled; + // Continuous catch-up must finish with an ordinary retryable error, + // not retain an unbounded provider loop or a forever-busy view. + if LAttempt >= SELECTOR_SNAPSHOT_ATTEMPTS then + raise EInvalidOperation.Create('The blockchain changed during the account search. Please search again.'); + Sleep(10); + end; + end; + until LComplete; + finally + FQueryLock.ReleaseWriteLock; + end; + finally + ReleaseLock; + end; +end; + +procedure TAccountSelectorDataSource.FetchAll(const AContainer: TList); +var LGeneration: UInt64; LSnapshot: TQuerySnapshot; LRows: TArray; + LNumbers: TArray; LStart,I,LAttempt,LOriginalCount: Integer; + LComplete: Boolean; +begin + // Generic external criteria are uncommon; apply them to the complete + // selected query without first materializing and rereading Variant rows. + EnterLock; + try + // Query work updates shared provider/cache state, including on fetch paths. + FQueryLock.AcquireWriteLock; + try + LAttempt := 0; + LOriginalCount := AContainer.Count; + repeat + Inc(LAttempt); + CheckAccountSelectorCancelled; + LComplete := False; + try + LSnapshot := CurrentSnapshot(LGeneration); + FSnapshotBlockTip := Max(0,LSnapshot.Chain.Blocks-1); + LStart := 0; + while LStart < LSnapshot.Total.Accounts do begin + SetLength(LNumbers,Min(SELECTOR_READ_CHUNK,LSnapshot.Total.Accounts-LStart)); + for I := 0 to High(LNumbers) do + if LSnapshot.ImplicitAll then LNumbers[I] := LStart+I + else LNumbers[I] := LSnapshot.Numbers[LStart+I]; + ReadAccounts(LSnapshot.Chain,LNumbers,LRows); + AContainer.AddRange(LRows); + Inc(LStart,Length(LRows)); + end; + PublishSnapshot(LSnapshot,LGeneration); + LComplete := True; + except + on E: EAccountSelectorSnapshotChanged do begin + AContainer.Count := LOriginalCount; + CheckAccountSelectorCancelled; + if LAttempt >= SELECTOR_SNAPSHOT_ATTEMPTS then + raise EInvalidOperation.Create('The blockchain changed during the account search. Please search again.'); + Sleep(10); + end; + end; + until LComplete; + finally + FQueryLock.ReleaseWriteLock; + end; + finally + ReleaseLock; + end; +end; + +function TAccountSelectorDataSource.TryGetSummary(out ATotal: TAccountSelectorTotal): Boolean; +begin + FConfigLock.AcquireReadLock; + try + ATotal := FSummary; + Result := FSummaryReady; + finally + FConfigLock.ReleaseReadLock; + end; +end; + +function TAccountSelectorDataSource.PublishedAccountIndex(AAccount: Cardinal): Integer; +var LNumbers: TArray; LImplicit: Boolean; LCount,L,H,M: Integer; +begin + FConfigLock.AcquireReadLock; + try + LNumbers := FPublishedNumbers; + LImplicit := FPublishedImplicitAll; + LCount := FSummary.Accounts; + finally + FConfigLock.ReleaseReadLock; + end; + Result := -1; + if LImplicit then begin + if AAccount < Cardinal(LCount) then Result := AAccount; + Exit; + end; + L := 0; + H := High(LNumbers); + while L <= H do begin + M := L+(H-L) div 2; + if LNumbers[M] = AAccount then Exit(M); + if LNumbers[M] < AAccount then L := M+1 else H := M-1; + end; +end; + +procedure TAccountSelectorDataSource.CalculateTypeStats(out AStats: TAccountSelectorTypeStats); +var LRows: TArray; LCurrent: TAccountSelectorChain; + LSafeBox: TPCSafeBox; + LStart,I,K,J,LType,LAttempt: Integer; LComplete: Boolean; +begin + FConfigLock.AcquireReadLock; + try + LSafeBox := FSafeBox; + finally + FConfigLock.ReleaseReadLock; + end; + LAttempt := 0; + repeat + Inc(LAttempt); + CheckAccountSelectorCancelled; + AStats := Default(TAccountSelectorTypeStats); + AStats.Chain := ReadChain(LSafeBox); + LComplete := False; + try + LStart := 0; + while LStart < AStats.Chain.Accounts do begin + ReadContiguous(AStats.Chain,LStart, + Min(SELECTOR_READ_CHUNK,AStats.Chain.Accounts-LStart),LRows); + for I := 0 to High(LRows) do begin + CheckAccountSelectorCancelled; + LType := LRows[I].account_type; + if LType = 0 then begin + Inc(AStats.Zero.Accounts); + Inc(AStats.Zero.Balance,LRows[I].balance); + end else if LType < 10000 then begin + Inc(AStats.Invalid.Accounts); + Inc(AStats.Invalid.Balance,LRows[I].balance); + end; + K := LType div 10000; + for J := 4 downto 1 do begin + Inc(AStats.Digits[K,J,LType mod 10].Accounts); + Inc(AStats.Digits[K,J,LType mod 10].Balance,LRows[I].balance); + LType := LType div 10; + end; + end; + Inc(LStart,Length(LRows)); + end; + LCurrent := ReadChain(LSafeBox); + if TBaseType.BinStrComp(LCurrent.Hash,AStats.Chain.Hash) <> 0 then + raise EAccountSelectorSnapshotChanged.Create('The account type snapshot changed'); + LComplete := True; + except + on E: EAccountSelectorSnapshotChanged do begin + CheckAccountSelectorCancelled; + if LAttempt >= SELECTOR_SNAPSHOT_ATTEMPTS then + raise EInvalidOperation.Create('The blockchain changed during account type statistics. Please try again.'); + Sleep(10); + end; + end; + until LComplete; +end; + +end. diff --git a/src/gui-experimental/core.utils/UCoreObjects.pas b/src/gui-experimental/core.utils/UCoreObjects.pas index 58f7b149c..e4bdfa5de 100644 --- a/src/gui-experimental/core.utils/UCoreObjects.pas +++ b/src/gui-experimental/core.utils/UCoreObjects.pas @@ -13,7 +13,27 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Defines the operation choices and balance/block summaries shared by wallet + controls, data sources and operation wizards. } + +{ Public types in this unit + + Type | Purpose + -----------------------+------------------------------------------------------------------------- + PBalanceSummary | References a balance summary shared with an owning control. + TAccountSaleMode | Selects a public sale or a sale reserved for a buyer key. + TBalanceSummary | Stores total wallet currency and account counts. + TBlockSummary | Stores block identity, amounts, work and timing statistics. + TChangeKeyMode | Selects ownership transfer or a change of account signing key. + TExecuteOperationType | Identifies the wallet operation to prepare or execute. + TOperationSigningMode | Selects the primary or a separate operation signer. + TPayloadEncryptionMode | Selects sender-key, recipient-key, password or public payloads. + TSendPASCMode | Selects a specified transfer amount or the whole balance. +} interface diff --git a/src/gui-experimental/core.utils/UCoreUtils.pas b/src/gui-experimental/core.utils/UCoreUtils.pas index 20fa07354..58057af28 100644 --- a/src/gui-experimental/core.utils/UCoreUtils.pas +++ b/src/gui-experimental/core.utils/UCoreUtils.pas @@ -16,49 +16,79 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Provides account/key comparisons, wallet summaries and bounded + operation-history queries. Prepares, validates and executes operation + batches using supplied accounts and keys. } + +{ Public types in this unit + + Type | Purpose + ----------------------------+------------------------------------------------------------------------- + TAccountComparer | Orders accounts for wallet collections. + TAccountEqualityComparer | Compares and hashes account identities. + TAccountHelper | Formats account identifiers, display text and detailed information. + TAccountKeyComparer | Orders account public keys. + TAccountKeyEqualityComparer | Compares and hashes account public keys. + TCoreTool | Collects wallet accounts, balances, signer candidates and pending history. + TNodeHelper | Reads account snapshots and bounded pending/stored history. + TOperationResumeHelper | Formats operation hashes and operation-detail text. + TTimeSpanHelper | Converts a time span to its expected block count. + TWalletOperationRequest | Carries the accounts, fees, keys and payload for operation preparation. + TWIZOperationsHelper | Prepares, validates, signs and submits wallet operation batches. +} interface uses - Classes, SysUtils, Forms, Dialogs, LCLType, UAccounts, UBlockChain, UNode, UWallet, + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + Classes, SysUtils, Forms, Dialogs, {$IFDEF FPC}LCLType,{$ENDIF} UAccounts, UBlockChain, UNode, UWallet, UBaseTypes, UCommon, UCoreObjects, UCommon.Collections, Generics.Defaults, UPCDataTypes; type + {$IFDEF FPC}TAccountHashCode = UInt32;{$ELSE}TAccountHashCode = Integer;{$ENDIF} + { TAccountComparer } TAccountComparer = class(TComparer) - function Compare(constref ALeft, ARight: TAccount): integer; override; - class function DoCompare(constref ALeft, ARight: TAccount): integer; inline; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): integer; override; + class function DoCompare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): integer; inline; end; { TAccountEqualityComparer } TAccountEqualityComparer = class(TEqualityComparer) public - function Equals(constref ALeft, ARight: TAccount): boolean; override; - function GetHashCode(constref AValue: TAccount): UInt32; override; - class function AreEqual(constref ALeft, ARight: TAccount): boolean; - class function CalcHashCode(constref AValue: TAccount): UInt32; + function Equals({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): boolean; override; + function GetHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccount): TAccountHashCode; override; + class function AreEqual({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): boolean; + class function CalcHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccount): UInt32; end; { TAccountKeyComparer } TAccountKeyComparer = class(TComparer) - function Compare(constref ALeft, ARight: TAccountKey): integer; override; - class function DoCompare(constref ALeft, ARight: TAccountKey): integer; inline; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): integer; override; + class function DoCompare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): integer; inline; end; { TAccountKeyEqualityComparer } TAccountKeyEqualityComparer = class(TEqualityComparer) public - function Equals(constref ALeft, ARight: TAccountKey): boolean; override; - function GetHashCode(constref AValue: TAccountKey): UInt32; override; - class function AreEqual(constref ALeft, ARight: TAccountKey): boolean; - class function CalcHashCode(constref AValue: TAccountKey): UInt32; + function Equals({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): boolean; override; + function GetHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccountKey): TAccountHashCode; override; + class function AreEqual({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): boolean; + class function CalcHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccountKey): UInt32; end; { TCoreTool } @@ -66,12 +96,15 @@ TAccountKeyEqualityComparer = class(TEqualityComparer) TCoreTool = class public class function GetSignerCandidates(ANumOps: integer; ASingleOperationFee: int64; const ACandidates: array of TAccount): TArray; static; - class function GetOperationShortText(const OpType, OpSubType: DWord): ansistring; static; inline; + class function GetOperationShortText(const OpType, OpSubType: DWord): String; static; inline; class function GetUserBalance(IncludePending: boolean = False): TBalanceSummary; class function GetUserAccounts(IncludePending: boolean = False): TArray; overload; class function GetUserAccounts(out Balance: TBalanceSummary; IncludePending: boolean = False): TArray; overload; class function GetUserAccountNumbers: TArray; class function AreAccountBalancesGreaterThan(const ACandidates: array of TAccount; AAmount: int64; var AFaultyAccount: TAccount): boolean; static; + // The caller retains the pending-state lock and owns both inputs. Build the + // account histories with one operation-tree traversal, then apply paging. + class function GetPendingOperationsAffectingAccounts(AOperations: TOperationsHashTree; ASnapshot: TPCSafeBoxTransaction; ABlock: Cardinal; const AAccountNumbers: array of Cardinal; ASkipCount, ATakeCount: Integer): TArray; static; end; { TNodeHelper } @@ -82,24 +115,26 @@ TNodeHelper = class helper for TNode function GetAccount(AAccountNumber: cardinal; AIncludePending: boolean = True): TAccount; function GetAccounts(const AAccountNumbers: array of cardinal; AIncludePending: boolean = True): TArray; function GetPendingOperationsAffectingAccounts(const AAccountNumbers: array of cardinal; ASkipCount, ATakeCount: integer): TArray; - function GetStoredOperationsAffectingAccounts(const AAccountNumbers: array of cardinal; ABlockDepth, ASkipCount, ATakeCount: integer): TArray; + function GetStoredOperationsAffectingAccounts(const AAccountNumbers: array of cardinal; ABlockDepth: Cardinal; ASkipCount, ATakeCount: integer): TArray; end; { TAccountHelper } TAccountHelper = record helper for TAccount - function GetAccountString : AnsiString; - function GetDisplayString : AnsiString; - function GetInfoText(const ABank : TPCBank) : utf8string; - property AccountString : AnsiString read GetAccountString; - property DisplayString : AnsiString read GetDisplayString; + function GetAccountString : String; + function GetDisplayString : String; + function GetInfoText(const ABank : TPCBank) : String; overload; + function GetInfoText(ABlocksCount : Cardinal) : String; overload; + property AccountString : String read GetAccountString; + property DisplayString : String read GetDisplayString; end; { TOperationResumeHelper } TOperationResumeHelper = record helper for TOperationResume - function GetPrintableOPHASH : AnsiString; - function GetInfoText(const ABank : TPCBank) : utf8string; + function GetPrintableOPHASH : String; + function GetInfoText(const ABank : TPCBank) : String; overload; + function GetInfoText(ABlocksCount : Cardinal; const ANode: TNode = nil; const AWalletKeys: TWalletKeys = nil) : String; overload; end; { TTimeSpanHelper } @@ -108,22 +143,54 @@ TTimeSpanHelper = record helper for TTimeSpan function TotalBlockCount : Integer; end; + { TWalletOperationRequest } + + // Account snapshots and keys are supplied explicitly so preparation never opens + // a wallet, contacts a node or transmits an operation. The caller owns the keys. + TWalletOperationRequest = record + OperationType: TExecuteOperationType; + Accounts, SignerAccounts: TArray; + DestinationAccount, SellerAccount: TAccount; + PublicKey: TAccountKey; + Amount, Fee, SalePrice: Int64; + LockedUntilBlock: Cardinal; + SendMode: TSendPASCMode; + SaleMode: TAccountSaleMode; + SigningMode: TOperationSigningMode; + PayloadMode: TPayloadEncryptionMode; + PayloadContent, PayloadPassword: string; + NewName: TRawBytes; + NewType: Word; + // Nonempty for a reviewed wizard batch. Refreshed targets must still match + // these account IDs before preparation may charge fees or sign operations. + ApprovedChangeInfoTargets: TArray; + end; + { TWIZOperationsHelper } TWIZOperationsHelper = class private class function IsOwnerOfWallet(AAccount: TAccount; AWalletKeys: TWalletKeys; out AWalletKey: TWalletKey; var AErrorMessage: string): boolean; static; class function ValidateOperationsInput(const ASelectedAccounts: TArray; AWalletKeys: TWalletKeys; ANode: TNode; var AErrorMessage: string): boolean; static; - class function UpdatePayload(const ASenderPublicKey, ADestinationPublicKey: TAccountKey; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent: string; var AEncodedPayloadBytes: TRawBytes; const APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; static; overload; - class function UpdatePayload(const ASenderPublicKey, ADestinationPublicKey: TAccountKey; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent: string; var AOperationPayload: TOperationPayload; const APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; static; overload; + class function UpdatePayload(const ASenderPublicKey, ADestinationPublicKey: TAccountKey; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent: string; var AEncodedPayloadBytes: TRawBytes; const APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; overload; static; + class function UpdatePayload(const ASenderPublicKey, ADestinationPublicKey: TAccountKey; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent: string; var AOperationPayload: TOperationPayload; const APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; overload; static; class function SendPASCFinalizeAndDisplayMessage(const AOperationsTxt, AOperationToString: string; ANoOfOperations: integer; ATotalAmount, ATotalFee: int64; AOperationsHashTree: TOperationsHashTree; var AErrorMessage: string): boolean; static; class function OthersFinalizeAndDisplayMessage(const AOperationsTxt, AOperationToString: string; ANoOfOperations: integer; ATotalFee: int64; AOperationsHashTree: TOperationsHashTree; var AErrorMessage: string): boolean; static; + class function ExecuteOperations(var ARequest: TWalletOperationRequest; var AErrorMessage: string): Boolean; static; public + // Batch type changes leave matching accounts untouched. A single-account + // request retains its account so the caller can still change its name. + class function GetChangeAccountInfoTargets(const AAccounts: TArray; ANewType: Word): TArray; static; + // On success the caller owns AOperations. On failure it is nil. The entire + // batch is prepared and signed before confirmation or node submission. + class function PrepareOperations(const ARequest: TWalletOperationRequest; AProtocol: Word; ABlocksCount: Cardinal; AWalletKeys: TWalletKeys; out AOperations: TOperationsHashTree; var AErrorMessage: string): Boolean; static; + // Validates a private copy while the caller retains its pending-state lock. + class function ValidatePreparedOperations(AOperations: TOperationsHashTree; ASnapshot: TPCSafeBoxTransaction; var AErrorMessage: String): Boolean; static; class function ExecuteSendPASC(const ASelectedAccounts: TArray; const ADestinationAccount: TAccount; AAmount, AFee: int64; const ASendPASCMode: TSendPASCMode; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; static; - class function ExecuteChangeKey(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; APublicKey: TAccountKey; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; static; - class function ExecuteEnlistAccountForSale(const ASelectedAccounts: TArray; const ASignerAccount, ASellerAccount: TAccount; const APublicKey: TAccountKey; AFee, ASalePrice: int64; ALockedUntilBlock: UInt32; const AAccountSaleMode: TAccountSaleMode; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; static; - class function ExecuteDelistAccountFromSale(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; static; - class function ExecuteChangeAccountInfo(const ASelectedAccounts, ASignerAccounts: TArray; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; const ANewName: TRawBytes; const ANewType: word; var AErrorMessage: string): boolean; static; + class function ExecuteChangeKey(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; APublicKey: TAccountKey; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode = akaPrimary): boolean; static; + class function ExecuteEnlistAccountForSale(const ASelectedAccounts: TArray; const ASignerAccount, ASellerAccount: TAccount; const APublicKey: TAccountKey; AFee, ASalePrice: int64; ALockedUntilBlock: UInt32; const AAccountSaleMode: TAccountSaleMode; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode = akaPrimary): boolean; static; + class function ExecuteDelistAccountFromSale(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode = akaPrimary): boolean; static; + class function ExecuteChangeAccountInfo(const ASelectedAccounts, ASignerAccounts: TArray; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; const ANewName: TRawBytes; const ANewType: word; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode = akaPrimary; const AApprovedTargets: TArray = nil): boolean; static; class function ExecuteBuyAccount(const ASelectedAccounts: TArray; const AAccountToBuy: TAccount; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; const AAmount: int64; const ANewOwnerPublicKey: TAccountKey; var AErrorMessage: string): boolean; static; end; @@ -138,8 +205,33 @@ implementation UMemory, UNetProtocol, UOpTransaction, + URPC, UJSONFunctions, UPCOrderedLists, - Generics.Collections; + Generics.Collections, UOrderedList, UFRMMemoText; + +procedure CheckWalletQueryCancelled; inline; +begin + // UI calls remain synchronous. Grid workers are terminated before their + // owner joins them, so do not continue reading a discarded history snapshot. + if TThread.CurrentThread.ThreadID = MainThreadID then Exit; + if TThread.CheckTerminated then Abort; +end; + +function LockQueryMempool(ANode: TNode): TPCOperationsComp; +begin + if TThread.CurrentThread.ThreadID = MainThreadID then Exit(ANode.LockMempoolRead); + // Attempt the read mode directly so readers can overlap and closing stays cancellable. + while not ANode.TryLockMempoolRead(0, Result) do begin + CheckWalletQueryCancelled; + Sleep(10); + end; + try + CheckWalletQueryCancelled; + except + ANode.UnlockMempoolRead; + raise; + end; +end; { TCoreTool } @@ -171,7 +263,7 @@ class function TCoreTool.GetSignerCandidates(ANumOps: integer; ASingleOperationF end; end; -class function TCoreTool.GetOperationShortText(const OpType, OpSubType: DWord): ansistring; +class function TCoreTool.GetOperationShortText(const OpType, OpSubType: DWord): String; begin case OpType of CT_PseudoOp_Reward: case OpSubType of @@ -233,53 +325,102 @@ class function TCoreTool.GetUserAccounts(out Balance: TBalanceSummary; IncludePe i, j: integer; LAccs: TList; LAcc: TAccount; + LNumbers: TList; + LIndex: TOrderedAccountKeysList; LList: TAccountsNumbersList; + LNode: TNode; LMemPool: TPCOperationsComp; Disposables: TDisposables; begin + CheckWalletQueryCancelled; Balance := CT_BalanceSummary_Nil; LAccs := Disposables.AddObject(TList.Create) as TList; - - TNode.Node.Bank.SafeBox.StartThreadSafe; - LMemPool := TNode.Node.LockMempoolRead; + LNumbers := Disposables.AddObject(TList.Create) as TList; + LNode := TNode.Node; + LIndex := TWallet.Keys.AccountsKeyList; + LMemPool := nil; + // Core operation application takes mempool before safebox. Confirmed reads + // need only the safebox; pending reads keep the same lock order as the core. + if IncludePending then LMemPool := LockQueryMempool(LNode); try - for i := 0 to TWallet.Keys.Count - 1 do - begin - LList := TWallet.Keys.AccountsKeyList.AccountKeyList[i]; - for j := 0 to LList.Count - 1 do - begin - if IncludePending then - LAcc := LMemPool.SafeBoxTransaction.Account(LList.Get(j)) - else - LAcc := TNode.Node.Bank.SafeBox.Account(LList.Get(j)); - LAccs.Add(LAcc); - Inc(Balance.TotalPASA); - Inc(Balance.TotalPASC, LAcc.Balance); + LNode.Bank.SafeBox.StartThreadSafe; + try + LIndex.Lock; + try + for i := 0 to LIndex.Count-1 do begin + CheckWalletQueryCancelled; + LList := LIndex.AccountKeyList[i]; + LList.FillList(0,LList.Count,LNumbers); + for j := 0 to LNumbers.Count-1 do begin + CheckWalletQueryCancelled; + if IncludePending then + LAcc := LMemPool.SafeBoxTransaction.Account(LNumbers[j]) + else + LAcc := LNode.Bank.SafeBox.Account(LNumbers[j]); + LAccs.Add(LAcc); + Inc(Balance.TotalPASA); + Inc(Balance.TotalPASC,LAcc.Balance); + end; + end; + finally + LIndex.Unlock; end; + finally + LNode.Bank.SafeBox.EndThreadSave; end; finally - TNode.Node.UnlockMempoolRead; - TNode.Node.Bank.SafeBox.EndThreadSave; + if IncludePending then LNode.UnlockMempoolRead; end; + CheckWalletQueryCancelled; LAccs.Sort(TAccountComparer.Create); + CheckWalletQueryCancelled; Result := LAccs.ToArray; end; class function TCoreTool.GetUserAccountNumbers: TArray; var - i, j: integer; - LAccs: TSortedList; + i, LUniqueCount: integer; + LAccs, LNumbers: TList; + LIndex: TOrderedAccountKeysList; + LSafeBox: TPCSafeBox; LList: TAccountsNumbersList; Disposables: TDisposables; begin - LAccs := Disposables.AddObject(TSortedList.Create) as TSortedList; - for i := 0 to TWallet.Keys.AccountsKeyList.Count - 1 do - begin - LList := TWallet.Keys.AccountsKeyList.AccountKeyList[i]; - for j := 0 to LList.Count - 1 do - LAccs.Add(LList.Get(j)); + CheckWalletQueryCancelled; + LAccs := Disposables.AddObject(TList.Create) as TList; + LNumbers := Disposables.AddObject(TList.Create) as TList; + LIndex := TWallet.Keys.AccountsKeyList; + LSafeBox := LIndex.SafeBox; + if Assigned(LSafeBox) then LSafeBox.StartThreadSafe; + try + LIndex.Lock; + try + for i := 0 to LIndex.Count-1 do begin + CheckWalletQueryCancelled; + LList := LIndex.AccountKeyList[i]; + LList.FillList(0,LList.Count,LNumbers); + LAccs.AddRange(LNumbers); + end; + finally + LIndex.Unlock; + end; + finally + if Assigned(LSafeBox) then LSafeBox.EndThreadSave; end; - Result := LAccs.ToArray; + // Append each key's snapshot, then sort once. Inserting every account into + // a sorted list would introduce another quadratic pass across many keys. + CheckWalletQueryCancelled; + LAccs.Sort; + SetLength(Result,LAccs.Count); + LUniqueCount := 0; + for i := 0 to LAccs.Count-1 do begin + CheckWalletQueryCancelled; + if (LUniqueCount=0) or (LAccs[i]<>Result[LUniqueCount-1]) then begin + Result[LUniqueCount] := LAccs[i]; + Inc(LUniqueCount); + end; + end; + SetLength(Result,LUniqueCount); end; class function TCoreTool.AreAccountBalancesGreaterThan(const ACandidates: array of TAccount; AAmount: int64; var AFaultyAccount: TAccount): boolean; @@ -305,11 +446,12 @@ class function TCoreTool.AreAccountBalancesGreaterThan(const ACandidates: array function TNodeHelper.HasBestKnownBlockchainTip: boolean; var LReady: boolean; - LMsg: ansistring; + LMsg: string; + LProgress: TProgressInfo; LDestBlock: cardinal; begin LReady := Self.Bank.IsReady(LMsg); - if LReady and TNetData.NetData.IsGettingNewBlockChainFromClient(LMsg) then + if LReady and TNetData.NetData.IsGettingNewBlockChainFromClient(LProgress) then begin LDestBlock := TNetData.NetData.MaxRemoteOperationBlock.block; Result := Self.Bank.BlocksCount = TNetData.NetData.MaxRemoteOperationBlock.block; @@ -334,80 +476,177 @@ function TNodeHelper.GetAccounts(const AAccountNumbers: array of cardinal; AIncl i: integer; LMemPool : TPCOperationsComp; begin + CheckWalletQueryCancelled; SetLength(Result, Length(AAccountNumbers)); if AIncludePending then begin - LMemPool := Self.LockMempoolRead; + LMemPool := LockQueryMempool(Self); try - for i := Low(AAccountNumbers) to High(AAccountNumbers) do - Result[i] := LMemPool.SafeBoxTransaction.Account(AAccountNumbers[i]) + for i := Low(AAccountNumbers) to High(AAccountNumbers) do begin + CheckWalletQueryCancelled; + Result[i] := LMemPool.SafeBoxTransaction.Account(AAccountNumbers[i]); + end; finally Self.UnlockMempoolRead; end; end else - for i := Low(AAccountNumbers) to High(AAccountNumbers) do + for i := Low(AAccountNumbers) to High(AAccountNumbers) do begin + CheckWalletQueryCancelled; Result[i] := Self.Bank.SafeBox.Account(AAccountNumbers[i]); + end; end; -function TNodeHelper.GetPendingOperationsAffectingAccounts(const AAccountNumbers: array of cardinal; ASkipCount, ATakeCount: integer): TArray; +class function TCoreTool.GetPendingOperationsAffectingAccounts(AOperations: TOperationsHashTree; ASnapshot: TPCSafeBoxTransaction; ABlock: Cardinal; const AAccountNumbers: array of Cardinal; ASkipCount, ATakeCount: Integer): TArray; +type + TOperationIndexes = TList; var - LList: TList; + LByAccount: TObjectDictionary; + LOperations: TList; + LAffected: TOrderedList; + LIndexes: TOperationIndexes; LOps: TList; - LOp: TPCOperation; LOpResume: TOperationResume; - LMemPool : TPCOperationsComp; - LAccNo: cardinal; - LNumOps, i: integer; + LAccNo: Cardinal; + LNumOps, LLastWanted, LBalance: Int64; + i, j, LOperationIndex: Integer; + LHasBalance: Boolean; GC: TDisposables; begin - LNumOps := 0; - LList := GC.AddObject(TList.Create) as TList; + CheckWalletQueryCancelled; + Result := nil; + if (Length(AAccountNumbers) = 0) or (ATakeCount <= 0) then Exit; + LLastWanted := Int64(ASkipCount) + Int64(ATakeCount); + if LLastWanted <= 0 then Exit; + if not Assigned(AOperations) then + raise EArgumentException.Create('Pending operations are required'); + if AOperations.OperationsCount = 0 then Exit; + if not Assigned(ASnapshot) then + raise EArgumentException.Create('A pending account snapshot is required'); + + LByAccount := GC.AddObject(TObjectDictionary.Create([doOwnsValues])) as TObjectDictionary; + LOperations := GC.AddObject(TList.Create) as TList; + LAffected := GC.AddObject(TOrderedList.Create(False, TComparison_Cardinal)) as TOrderedList; LOps := GC.AddObject(TList.Create) as TList; - for LAccNo in AAccountNumbers do - begin - LMemPool := Self.LockMempoolRead; - try - LList.Clear; - LMemPool.OperationsHashTree.GetOperationsAffectingAccount(LAccNo, LList); - if LList.Count > 0 then - for i := LList.Count - 1 downto 0 do - begin + for LAccNo in AAccountNumbers do begin + CheckWalletQueryCancelled; + if not LByAccount.ContainsKey(LAccNo) then + LByAccount.Add(LAccNo, nil); + end; + + AOperations.GetOperationsList(LOperations, False); + // AffectedAccounts is evaluated once per operation, independently of the + // number of wallet accounts. Store only matching account/index pairs. + for i := 0 to LOperations.Count - 1 do begin + CheckWalletQueryCancelled; + LAffected.Clear; + LOperations[i].AffectedAccounts(LAffected); + for j := 0 to LAffected.Count - 1 do begin + CheckWalletQueryCancelled; + if LByAccount.TryGetValue(LAffected[j], LIndexes) then begin + if not Assigned(LIndexes) then begin + LIndexes := TOperationIndexes.Create; + try + LByAccount.AddOrSetValue(LAffected[j], LIndexes); + except + LIndexes.Free; + raise; + end; + end; + LIndexes.Add(Cardinal(i)); + end; + end; + end; + + LNumOps := 0; + // Preserve the existing account-major order and per-account newest-first + // order, including duplicate requested accounts and account-specific balances. + for LAccNo in AAccountNumbers do begin + CheckWalletQueryCancelled; + LIndexes := LByAccount[LAccNo]; + LHasBalance := False; + if Assigned(LIndexes) then + for i := LIndexes.Count - 1 downto 0 do begin + CheckWalletQueryCancelled; Inc(LNumOps); - if (LNumOps > ASkipCount) and (LNumOps <= ASkipCount + ATakeCount) then - begin - LOp := LMemPool.OperationsHashTree.GetOperation(LList[i]); - if TPCOperation.OperationToOperationResume(0, LOp, False, LAccNo, LOpResume) then - begin - LOpResume.NOpInsideBlock := i; - LOpResume.Block := LMemPool.OperationBlock.block; - LOpResume.Balance := LMemPool.SafeBoxTransaction.Account(LAccNo {Op.SignerAccount}).balance; + if (LNumOps > ASkipCount) and (LNumOps <= LLastWanted) then begin + LOperationIndex := LIndexes[i]; + if TPCOperation.OperationToOperationResume(0, LOperations[LOperationIndex], False, LAccNo, LOpResume) then begin + if not LHasBalance then begin + LBalance := ASnapshot.Account(LAccNo).balance; + LHasBalance := True; + end; + LOpResume.NOpInsideBlock := LOperationIndex; + LOpResume.Block := ABlock; + LOpResume.Balance := LBalance; LOps.Add(LOpResume); end; end; + if LNumOps >= LLastWanted then begin + CheckWalletQueryCancelled; + Exit(LOps.ToArray); + end; end; - finally - Self.UnlockMempoolRead; - end; end; + CheckWalletQueryCancelled; Result := LOps.ToArray; end; -function TNodeHelper.GetStoredOperationsAffectingAccounts(const AAccountNumbers: array of cardinal; ABlockDepth, ASkipCount, ATakeCount: integer): TArray; +function TNodeHelper.GetPendingOperationsAffectingAccounts(const AAccountNumbers: array of Cardinal; ASkipCount, ATakeCount: Integer): TArray; +var + LMemPool: TPCOperationsComp; + LAccounts: TArray; + I, LCount: Integer; +begin + Result := nil; + if (Length(AAccountNumbers) = 0) or (ATakeCount <= 0) then + Exit; + LMemPool := LockQueryMempool(Self); + try + // Successful live admissions already record every affected account here. + // Use that index before walking the pool, under the same pending-state lock. + // Arbitrary operation trees still use the unfiltered TCoreTool helper. + SetLength(LAccounts, Length(AAccountNumbers)); + LCount := 0; + for I := 0 to High(AAccountNumbers) do + begin + CheckWalletQueryCancelled; + if LMemPool.PreviousUpdatedBlocks.IndexOfAccount(AAccountNumbers[I]) >= 0 then + begin + LAccounts[LCount] := AAccountNumbers[I]; + Inc(LCount); + end; + end; + if LCount = 0 then + Exit; + SetLength(LAccounts, LCount); + Result := TCoreTool.GetPendingOperationsAffectingAccounts(LMemPool.OperationsHashTree, + LMemPool.SafeBoxTransaction, LMemPool.OperationBlock.block, + LAccounts, ASkipCount, ATakeCount); + finally + Self.UnlockMempoolRead; + end; +end; + +function TNodeHelper.GetStoredOperationsAffectingAccounts(const AAccountNumbers: array of cardinal; ABlockDepth: Cardinal; ASkipCount, ATakeCount: integer): TArray; type __TList_Cardinal = TList; var i: integer; - LBlock: cardinal; + LBlock, LBlockTip, LBlockEnd: Cardinal; LRelevantBlockOps: TList; LOp: TPCOperation; LOpResume: TOperationResume; LFoundOps: TList; LOpsComp: TPCOperationsComp; - LAccountBalances: TDictionary; + LAccountBalances: TDictionary; LAccounts: TArray; LDisposables: TDisposables; - LBlockEnd, LNumOps: integer; + LNumOps, LLastWanted: Int64; + {$IFDEF FPC} LBlockTraversal: TSortedHashSet; + {$ELSE} + LBlockTraversal: TOrderedCardinalList; + {$ENDIF} LAccountsToScanAtBlock: TObjectDictionary; LAcc: TAccount; @@ -425,11 +664,14 @@ function TNodeHelper.GetStoredOperationsAffectingAccounts(const AAccountNumbers: LPrevUpdatedBlock: cardinal; LDisposables: TDisposables; begin + CheckWalletQueryCancelled; LOpsComp := LDisposables.AddObject(TPCOperationsComp.Create(nil)) as TPCOperationsComp; LRelevantBlockOps := LDisposables.AddObject(TList.Create) as TList; - // load block - if not Bank.Storage.LoadBlockChainBlock(LOpsComp, ABlockNum) then + // Match Classic history: resolve secondary legacy storage before the load's + // indexed-range check. Otherwise an older archived block is reported absent. + if not Bank.Storage.BlockExists(ABlockNum) or + not Bank.Storage.LoadBlockChainBlock(LOpsComp, ABlockNum) then begin TLog.NewLog(ltdebug, ClassName, 'Block ' + IntToStr(ABlockNum) + ' not found. Cannot read operations'); exit; @@ -438,12 +680,16 @@ function TNodeHelper.GetStoredOperationsAffectingAccounts(const AAccountNumbers: // scan for each account for LAccNo in LAccountsToScanAtBlock[ABlockNum] do begin + CheckWalletQueryCancelled; LRelevantBlockOps.Clear; LOpsComp.OperationsHashTree.GetOperationsAffectingAccount(LAccNo, LRelevantBlockOps); for i := LRelevantBlockOps.Count - 1 downto 0 do begin + CheckWalletQueryCancelled; LOp := LOpsComp.Operation[LRelevantBlockOps.Items[i]]; - if TPCOperation.OperationToOperationResume(i, LOp, False, LAccNo, LOpResume) then + // Conversion encodes the block into operation hashes as well as Block. + // Pass the stored height, never this account's local operation index. + if TPCOperation.OperationToOperationResume(ABlockNum, LOp, False, LAccNo, LOpResume) then begin LOpResume.NOpInsideBlock := LRelevantBlockOps.Items[i]; LOpResume.time := LOpsComp.OperationBlock.timestamp; @@ -458,7 +704,7 @@ function TNodeHelper.GetStoredOperationsAffectingAccounts(const AAccountNumbers: // Apply skip/take Inc(LNumOps); - if (LNumOps > ASkipCount) and (LNumOps <= ASkipCount + ATakeCount) then + if (LNumOps > ASkipCount) and (LNumOps <= LLastWanted) then LFoundOps.Add(LOpResume); // short-cirtcuit exit if taken enough @@ -477,25 +723,52 @@ function TNodeHelper.GetStoredOperationsAffectingAccounts(const AAccountNumbers: end; end; - function GetAccountLastUpdateBlock(constref AAccount: TAccount): cardinal; + function GetAccountLastUpdateBlock({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AAccount: TAccount): cardinal; begin Result := AAccount.GetLastUpdatedBlock; end; begin + CheckWalletQueryCancelled; + Result := nil; + if (Length(AAccountNumbers) = 0) or (ATakeCount <= 0) then + Exit; + LLastWanted := Int64(ASkipCount) + Int64(ATakeCount); + if LLastWanted <= 0 then + Exit; // Init LNumOps := 0; + {$IFDEF FPC} LBlockTraversal := LDisposables.AddObject(TSortedHashSet.Create(TComparerTool.Inverted(TComparer.Default))) as TSortedHashSet; + {$ELSE} + LBlockTraversal := LDisposables.AddObject(TOrderedCardinalList.Create) as TOrderedCardinalList; + {$ENDIF} LAccountsToScanAtBlock := LDisposables.AddObject(TObjectDictionary.Create([doOwnsValues])) as TObjectDictionary; LFoundOps := LDisposables.AddObject(TList.Create) as TList; - LAccountBalances := LDisposables.AddObject(TDictionary.Create) as TDictionary; - LBlockEnd := ClipValue(Self.BlockTip - ABlockDepth, 0, Self.BlockTip); + // Money and the undetermined (-1) history sentinel use signed 64-bit values, + // matching TOperationResume.Balance while walking older operations backwards. + LAccountBalances := LDisposables.AddObject(TDictionary.Create) as TDictionary; + LBlockTip := Self.BlockTip; + // Saturate before subtracting unsigned heights. A full-history request can + // exceed the chain length; it must include genesis rather than wrap around. + if ABlockDepth = 0 then + LBlockEnd := LBlockTip + else if ABlockDepth >= LBlockTip then + LBlockEnd := 0 + else + LBlockEnd := LBlockTip - ABlockDepth; // First get all accounts, their balances and initial traversal set LAccounts := Self.GetAccounts(AAccountNumbers, False); for i := Low(LAccounts) to High(LAccounts) do begin + CheckWalletQueryCancelled; // if account is modified in block-tip LAcc := LAccounts[i]; + // Accounts older than this window cannot produce a matching linked block. + // Exclude them before building balance/frontier dictionaries, rather than + // allocating and sorting entries which the traversal will only discard. + if LAcc.GetLastUpdatedBlock < LBlockEnd then + Continue; LAccountBalances.AddOrSetValue(LAcc.account, LAcc.Balance); // track account balances LBlockTraversal.Add(LAcc.GetLastUpdatedBlock); MarkAccountAsScannableAtBlock(LAcc.account, LAcc.GetLastUpdatedBlock); @@ -504,43 +777,62 @@ function TNodeHelper.GetStoredOperationsAffectingAccounts(const AAccountNumbers: // Traverse the set of "last updated" blocks in DESCENDING order while LBlockTraversal.Count > 0 do begin + CheckWalletQueryCancelled; + {$IFDEF FPC} LBlock := TSortedHashSetTool.Pop(LBlockTraversal); + {$ELSE} + LBlock := LBlockTraversal.Get(LBlockTraversal.Count - 1); + LBlockTraversal.Remove(LBlock); + {$ENDIF} if LBlock < LBlockEnd then continue; ScanBlock(LBlock); // note: this will update LBlockTraversals with prev updated blocks, so loops until finished if LFoundOps.Count >= ATakeCount then - exit; + begin + // Return a completed finite page before releasing its scoped list. + Result := LFoundOps.ToArray; + Exit; + end; end; // return array result + CheckWalletQueryCancelled; Result := LFoundOps.ToArray; end; { TAccountComparer } -function TAccountComparer.Compare(constref ALeft, ARight: TAccount): integer; +function TAccountComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): integer; begin Result := TAccountComparer.DoCompare(ALeft, ARight); end; -class function TAccountComparer.DoCompare(constref ALeft, ARight: TAccount): integer; +class function TAccountComparer.DoCompare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): integer; begin + {$IFDEF FPC} Result := TCompare.UInt64(ALeft.account, ARight.account); + {$ELSE} + Result := TComparer.Default.Compare(ALeft.account, ARight.account); + {$ENDIF} end; { TAccountEqualityComparer } -function TAccountEqualityComparer.Equals(constref ALeft, ARight: TAccount): boolean; +function TAccountEqualityComparer.Equals({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): boolean; begin Result := TAccountEqualityComparer.AreEqual(ALeft, ARight); end; -function TAccountEqualityComparer.GetHashCode(constref AValue: TAccount): UInt32; +function TAccountEqualityComparer.GetHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccount): TAccountHashCode; begin + {$IFDEF FPC} Result := TAccountEqualityComparer.CalcHashCode(AValue); + {$ELSE} + Result := Integer(TAccountEqualityComparer.CalcHashCode(AValue)); + {$ENDIF} end; -class function TAccountEqualityComparer.AreEqual(constref ALeft, ARight: TAccount): boolean; +class function TAccountEqualityComparer.AreEqual({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccount): boolean; begin Result := (ALeft.account = ARight.account) and @@ -550,19 +842,19 @@ class function TAccountEqualityComparer.AreEqual(constref ALeft, ARight: TAccoun TAccountKeyEqualityComparer.AreEqual(ALeft.accountInfo.accountKey, ARight.accountInfo.accountKey); end; -class function TAccountEqualityComparer.CalcHashCode(constref AValue: TAccount): UInt32; +class function TAccountEqualityComparer.CalcHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccount): UInt32; begin Result := AValue.account; end; { TAccountKeyComparer } -function TAccountKeyComparer.Compare(constref ALeft, ARight: TAccountKey): integer; +function TAccountKeyComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): integer; begin Result := TAccountKeyComparer.DoCompare(ALeft, ARight); end; -class function TAccountKeyComparer.DoCompare(constref ALeft, ARight: TAccountKey): integer; +class function TAccountKeyComparer.DoCompare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): integer; begin Result := BytesCompare(ALeft.x, ARight.x); if Result = 0 then @@ -571,95 +863,133 @@ class function TAccountKeyComparer.DoCompare(constref ALeft, ARight: TAccountKey { TAccountKeyEqualityComparer } -function TAccountKeyEqualityComparer.Equals(constref ALeft, ARight: TAccountKey): boolean; +function TAccountKeyEqualityComparer.Equals({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): boolean; begin Result := TAccountKeyEqualityComparer.AreEqual(ALeft, ARight); end; -function TAccountKeyEqualityComparer.GetHashCode(constref AValue: TAccountKey): UInt32; +function TAccountKeyEqualityComparer.GetHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccountKey): TAccountHashCode; begin + {$IFDEF FPC} Result := TAccountKeyEqualityComparer.CalcHashCode(AValue); + {$ELSE} + Result := Integer(TAccountKeyEqualityComparer.CalcHashCode(AValue)); + {$ENDIF} end; -class function TAccountKeyEqualityComparer.AreEqual(constref ALeft, ARight: TAccountKey): boolean; +class function TAccountKeyEqualityComparer.AreEqual({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: TAccountKey): boolean; begin Result := TAccountKeyComparer.DoCompare(ALeft, ARight) = 0; end; -class function TAccountKeyEqualityComparer.CalcHashCode(constref AValue: TAccountKey): UInt32; +class function TAccountKeyEqualityComparer.CalcHashCode({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: TAccountKey): UInt32; begin - Result := TEqualityComparer.Default.GetHashCode(IntToStr(AValue.EC_OpenSSL_NID) + AValue.x.ToString + AValue.y.ToString); + // Delphi string hashes are signed; retain their bits in the unsigned contract. + Result := UInt32(TEqualityComparer.Default.GetHashCode(IntToStr(AValue.EC_OpenSSL_NID) + AValue.x.ToString + AValue.y.ToString)); end; { TAccountHelper } -function TAccountHelper.GetAccountString: ansistring; +function TAccountHelper.GetAccountString: String; begin Result := TAccountComp.AccountNumberToAccountTxtNumber(Self.account); end; -function TAccountHelper.GetDisplayString: ansistring; +function TAccountHelper.GetDisplayString: String; begin Result := GetAccountString; if Self.Name <> nil then Result := Result + ': ' + Self.Name.ToString; end; -function TAccountHelper.GetInfoText(const ABank: TPCBank): utf8string; +function TAccountHelper.GetInfoText(const ABank: TPCBank): String; +begin + Result := GetInfoText(ABank.BlocksCount); +end; + +function TAccountHelper.GetInfoText(ABlocksCount: Cardinal): String; var builder: TStrings; GC: TDisposables; + json: TPCJSONObject; begin builder := GC.AddObject(TStringList.Create) as TStrings; builder.Append(Format('Account: %s %s Type:%d', [TAccountComp.AccountNumberToAccountTxtNumber(self.account), IIF(Self.Name <> nil, 'Name: ' + Self.Name.ToString, ''), Self.account_type])); builder.Append(''); builder.Append(Format('Current balance: %s', [TAccountComp.FormatMoney(Self.balance)])); builder.Append(''); - builder.Append(Format('Passively updated on block: %d (%d blocks ago)', [Self.updated_on_block_passive_mode, ABank.BlocksCount - Self.updated_on_block_passive_mode])); - builder.Append(Format('Actively updated on block: %d (%d blocks ago)', [Self.updated_on_block_active_mode, ABank.BlocksCount - Self.updated_on_block_active_mode])); + builder.Append(Format('Passively updated on block: %d (%d blocks ago)', [Self.updated_on_block_passive_mode, Int64(ABlocksCount) - Self.updated_on_block_passive_mode])); + builder.Append(Format('Actively updated on block: %d (%d blocks ago)', [Self.updated_on_block_active_mode, Int64(ABlocksCount) - Self.updated_on_block_active_mode])); builder.Append(Format('Public key type: %s', [TAccountComp.GetECInfoTxt(Self.accountInfo.accountKey.EC_OpenSSL_NID)])); builder.Append(Format('Base58 Public key: %s', [TAccountComp.AccountPublicKeyExport(Self.accountInfo.accountKey)])); + if Length(Self.account_data) > 0 then + builder.Append('Account Data: ' + Self.account_data.ToHexaString) + else + builder.Append('Account Data: (No data)'); + builder.Append('Account Seal: ' + Self.account_seal.ToHexaString); if TAccountComp.IsAccountForSale(Self.accountInfo) then begin builder.Append(''); builder.Append('** Account is for sale: **'); builder.Append(Format('Price: %s', [TAccountComp.FormatMoney(Self.accountInfo.price)])); builder.Append(Format('Seller account (where to pay): %s', [TAccountComp.AccountNumberToAccountTxtNumber(Self.accountInfo.account_to_pay)])); -// if TAccountComp.IsAccountForSaleAcceptingTransactions(Self.accountInfo) then // Skybuck *old* - // Skybuck: funtionality partially restored, unsure of last parameter, unsure of parameter 2 and 3, needs testing and debugging - if TAccountComp.IsAccountForSaleOrSwapAcceptingTransactions(Self, ABank.SafeBox.BlocksCount, ABank.SafeBox.CurrentProtocol, nil ) then // Skybuck *new* + if TAccountComp.IsAccountForPrivateSale(Self.accountInfo) then begin builder.Append(''); builder.Append('** Private sale **'); builder.Append(Format('New Base58 Public key: %s', [TAccountComp.AccountPublicKeyExport(Self.accountInfo.new_publicKey)])); builder.Append(''); - if TAccountComp.IsAccountLocked(Self.accountInfo, ABank.BlocksCount) then + if TAccountComp.IsAccountLocked(Self.accountInfo, ABlocksCount) then builder.Append(Format('PURCHASE IS SECURE UNTIL BLOCK %d (current %d, remains %d)', - [Self.accountInfo.locked_until_block, ABank.BlocksCount, Self.accountInfo.locked_until_block - ABank.BlocksCount])) + [Self.accountInfo.locked_until_block, ABlocksCount, Self.accountInfo.locked_until_block - ABlocksCount])) else builder.Append(Format('PURCHASE IS NOT SECURE (Expired on block %d, current %d)', - [Self.accountInfo.locked_until_block, ABank.BlocksCount])); + [Self.accountInfo.locked_until_block, ABlocksCount])); + end; + end; + if TAccountComp.IsAccountForSwap(Self.accountInfo) then begin + builder.Append(''); + if TAccountComp.IsAccountForAccountSwap(Self.accountInfo) then begin + builder.Append('** Account is for Atomic Account Swap: **'); + builder.Append('New Base58 Public key: ' + TAccountComp.AccountPublicKeyExport(Self.accountInfo.new_publicKey)); + end else begin + builder.Append('** Account is for Atomic Coin Swap: **'); + builder.Append('Amount to swap: ' + TAccountComp.FormatMoney(Self.accountInfo.price)); + builder.Append('Counterparty account: ' + TAccountComp.AccountNumberToAccountTxtNumber(Self.accountInfo.account_to_pay)); end; + builder.Append('Public secret to find: ' + Self.accountInfo.hashed_secret.ToHexaString); + builder.Append(Format('Locked until block: %d (current %d)', [Self.accountInfo.locked_until_block, ABlocksCount])); end; + // Reuse the same canonical account JSON as the classic wallet's info dialog. + json := GC.AddObject(TPCJSONObject.Create) as TPCJSONObject; + TPascalCoinJSONComp.FillAccountObject(Self, json); + builder.Append('ACCOUNT JSON:'); + builder.Append(json.ToJSON(false)); Result := builder.Text; end; { TOperationResumeHelper } -function TOperationResumeHelper.GetPrintableOPHASH: ansistring; +function TOperationResumeHelper.GetPrintableOPHASH: String; begin Result := TCrypto.ToHexaString(Self.OperationHash); end; -function TOperationResumeHelper.GetInfoText(const ABank: TPCBank): utf8string; +function TOperationResumeHelper.GetInfoText(const ABank: TPCBank): String; +begin + Result := GetInfoText(ABank.BlocksCount); +end; + +function TOperationResumeHelper.GetInfoText(ABlocksCount: Cardinal; const ANode: TNode; const AWalletKeys: TWalletKeys): String; var builder: TStrings; GC: TDisposables; + json: TPCJSONObject; begin if (not Self.valid) then exit; builder := GC.AddObject(TStringList.Create) as TStrings; - if Self.Block < ABank.BlocksCount then + if Self.Block < ABlocksCount then if (Self.NOpInsideBlock >= 0) then builder.Add(Format('Block: %d/%d', [Self.Block, Self.NOpInsideBlock])) else @@ -670,6 +1000,9 @@ function TOperationResumeHelper.GetInfoText(const ABank: TPCBank): utf8string; builder.Add('** Pending operation not included on blockchain **'); builder.Add(Format('%s', [Self.OperationTxt])); builder.Add(Format('OpType:%d Subtype:%d', [Self.OpType, Self.OpSubtype])); + if Self.isMultiOperation then + builder.Add(Format('Multioperation: %d senders, %d receivers, %d account changes', + [Length(Self.Senders), Length(Self.Receivers), Length(Self.Changers)])); builder.Add(Format('Operation Hash (ophash): %s', [TCrypto.ToHexaString(Self.OperationHash)])); if (Self.OperationHash_OLD <> nil) then builder.Add(Format('Old Operation Hash (old_ophash): %s', [TCrypto.ToHexaString(Self.OperationHash_OLD)])); @@ -682,6 +1015,12 @@ function TOperationResumeHelper.GetInfoText(const ABank: TPCBank): utf8string; end; if Self.Balance >= 0 then builder.Add(Format('Final balance: %s', [TAccountComp.FormatMoney(Self.Balance)])); + // Context-free callers stay offline; the UI can supply its loaded node/keys + // for the same EPASA/payload information shown by the classic wallet. + json := GC.AddObject(TPCJSONObject.Create) as TPCJSONObject; + TPascalCoinJSONComp.FillOperationObject(Self, ABlocksCount, ANode, AWalletKeys, nil, json); + builder.Add('OPERATION JSON:'); + builder.Add(json.ToJSON(false)); Result := builder.Text; end; @@ -741,52 +1080,46 @@ class function TWIZOperationsHelper.IsOwnerOfWallet(AAccount: TAccount; AWalletK class function TWIZOperationsHelper.SendPASCFinalizeAndDisplayMessage(const AOperationsTxt, AOperationToString: string; ANoOfOperations: integer; ATotalAmount, ATotalFee: int64; AOperationsHashTree: TOperationsHashTree; var AErrorMessage: string): boolean; var - LAuxs, LOperationsTxt: string; + LAuxs, LOperationsTxt, LPrompt, LDetails: string; i: integer; begin - LOperationsTxt := AOperationsTxt; + Result := False; + AErrorMessage := ''; + LAuxs := 'Total amount that dest will receive: ' + + TAccountComp.FormatMoney(ATotalAmount) + #10 + + 'Total fee: ' + TAccountComp.FormatMoney(ATotalFee); if (ANoOfOperations > 1) then begin - LAuxs := 'Total amount that dest will receive: ' + TAccountComp.FormatMoney( - ATotalAmount) + #10; - if Application.MessageBox( - PChar('Execute ' + IntToStr(ANoOfOperations) + - ' operations?' + #10 + 'Operation: ' + LOperationsTxt + #10 + - LAuxs + 'Total fee: ' + TAccountComp.FormatMoney(ATotalFee) + - #10 + #10 + 'Note: This operation will be transmitted to the network!'), - PChar(Application.Title), MB_YESNO + MB_ICONINFORMATION + MB_DEFBUTTON2) <> - idYes then - Exit; + LPrompt := 'Execute ' + IntToStr(ANoOfOperations) + ' operations?'; + LDetails := 'Operation: ' + AOperationsTxt; + if AOperationToString <> AOperationsTxt then + LDetails := LDetails + #10 + #10 + AOperationToString; end - else - if Application.MessageBox(PChar('Execute this operation:' + - #10 + #10 + AOperationToString + #10 + #10 + - 'Note: This operation will be transmitted to the network!'), - PChar(Application.Title), MB_YESNO + MB_ICONINFORMATION + MB_DEFBUTTON2) <> - idYes then + else begin + LPrompt := 'Execute this operation:' + #10 + + 'Number of operations: ' + IntToStr(ANoOfOperations); + LDetails := AOperationToString; + end; + LPrompt := LPrompt + #10 + LAuxs + #10 + #10 + + 'Note: This operation will be transmitted to the network!'; + // Keep totals and the network notice visible while the complete list scrolls. + if not ConfirmMemoQuery(Screen.ActiveForm, Application.Title, LPrompt, LDetails) then Exit; Result := True; i := TNode.Node.AddOperations(nil, AOperationsHashTree, nil, AErrorMessage); if (i = AOperationsHashTree.OperationsCount) then begin LOperationsTxt := 'Successfully executed ' + IntToStr(i) + - ' operations!' + #10 + #10 + AOperationToString; - if i > 1 then - ShowMessage(LOperationsTxt) - else - begin - Application.MessageBox( - PChar('Successfully executed ' + IntToStr(i) + ' operations!' + - #10 + #10 + AOperationToString), - PChar(Application.Title), MB_OK + MB_ICONINFORMATION); - end; + ' operations!' + #10 + LAuxs; + ShowResultMemo(Screen.ActiveForm, Application.Title, LOperationsTxt, + AOperationToString); end else if (i > 0) then begin - LOperationsTxt := 'One or more of your operations has not been executed:' + - #10 + 'Errors:' + #10 + AErrorMessage + #10 + #10 + + LOperationsTxt := 'One or more of your operations has not been executed:' + #10 + 'Total successfully executed operations: ' + IntToStr(i); - ShowMessage(LOperationsTxt); + ShowResultMemo(Screen.ActiveForm, Application.Title, LOperationsTxt, + 'Errors:' + #10 + AErrorMessage); end else Result := False; @@ -796,7 +1129,8 @@ class function TWIZOperationsHelper.UpdatePayload(const ASenderPublicKey, ADesti var LValid: boolean; begin - + AEncodedPayloadBytes := nil; + AErrorMessage := ''; if (APayloadContent = '') then Exit(True); @@ -857,852 +1191,618 @@ class function TWIZOperationsHelper.UpdatePayload(const ASenderPublicKey, ADesti LValid := False; AErrorMessage := Format('Payload Size Is %d Which Is Bigger Than %d', [Length(AEncodedPayloadBytes), CT_MaxPayloadSize]); end; + if LValid then + AErrorMessage := '' + else + AEncodedPayloadBytes := nil; Result := LValid; end; end; class function TWIZOperationsHelper.UpdatePayload(const ASenderPublicKey, ADestinationPublicKey: TAccountKey; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent: string; var AOperationPayload: TOperationPayload; const APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; begin + AOperationPayload := CT_TOperationPayload_NUL; result := UpdatePayload( ASenderPublicKey, ADestinationPublicKey, APayloadEncryptionMode, APayloadContent, AOperationPayload.payload_raw, APayloadEncryptionPassword, AErrorMessage ); end; class function TWIZOperationsHelper.OthersFinalizeAndDisplayMessage(const AOperationsTxt, AOperationToString: string; ANoOfOperations: integer; ATotalFee: int64; AOperationsHashTree: TOperationsHashTree; var AErrorMessage: string): boolean; var - LAuxs, LOperationsTxt: string; + LAuxs, LOperationsTxt, LPrompt, LDetails: string; i: integer; begin - LOperationsTxt := AOperationsTxt; + Result := False; + AErrorMessage := ''; + LAuxs := 'Total fee: ' + TAccountComp.FormatMoney(ATotalFee); if (ANoOfOperations > 1) then begin - LAuxs := ''; - if Application.MessageBox( - PChar('Execute ' + IntToStr(ANoOfOperations) + - ' operations?' + #10 + 'Operation: ' + LOperationsTxt + #10 + - LAuxs + 'Total fee: ' + TAccountComp.FormatMoney(ATotalFee) + - #10 + #10 + 'Note: This operation will be transmitted to the network!'), - PChar(Application.Title), MB_YESNO + MB_ICONINFORMATION + MB_DEFBUTTON2) <> - idYes then - Exit; + LPrompt := 'Execute ' + IntToStr(ANoOfOperations) + ' operations?'; + LDetails := 'Operation: ' + AOperationsTxt; + if AOperationToString <> AOperationsTxt then + LDetails := LDetails + #10 + #10 + AOperationToString; end - else - if Application.MessageBox(PChar('Execute this operation:' + - #10 + #10 + AOperationToString + #10 + #10 + - 'Note: This operation will be transmitted to the network!'), - PChar(Application.Title), MB_YESNO + MB_ICONINFORMATION + MB_DEFBUTTON2) <> - idYes then + else begin + LPrompt := 'Execute this operation:' + #10 + + 'Number of operations: ' + IntToStr(ANoOfOperations); + LDetails := AOperationToString; + end; + LPrompt := LPrompt + #10 + LAuxs + #10 + #10 + + 'Note: This operation will be transmitted to the network!'; + // Use the same bounded presentation for one operation and large batches. + if not ConfirmMemoQuery(Screen.ActiveForm, Application.Title, LPrompt, LDetails) then Exit; Result := True; i := TNode.Node.AddOperations(nil, AOperationsHashTree, nil, AErrorMessage); if (i = AOperationsHashTree.OperationsCount) then begin LOperationsTxt := 'Successfully executed ' + IntToStr(i) + - ' operations!' + #10 + #10 + AOperationToString; - if i > 1 then - ShowMessage(LOperationsTxt) - else - begin - Application.MessageBox( - PChar('Successfully executed ' + IntToStr(i) + ' operations!' + - #10 + #10 + AOperationToString), - PChar(Application.Title), MB_OK + MB_ICONINFORMATION); - end; + ' operations!' + #10 + LAuxs; + ShowResultMemo(Screen.ActiveForm, Application.Title, LOperationsTxt, + AOperationToString); end else if (i > 0) then begin - LOperationsTxt := 'One or more of your operations has not been executed:' + - #10 + 'Errors:' + #10 + AErrorMessage + #10 + #10 + + LOperationsTxt := 'One or more of your operations has not been executed:' + #10 + 'Total successfully executed operations: ' + IntToStr(i); - ShowMessage(LOperationsTxt); + ShowResultMemo(Screen.ActiveForm, Application.Title, LOperationsTxt, + 'Errors:' + #10 + AErrorMessage); end else Result := False; end; -class function TWIZOperationsHelper.ExecuteSendPASC(const ASelectedAccounts: TArray; const ADestinationAccount: TAccount; AAmount, AFee: int64; const ASendPASCMode: TSendPASCMode; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; +class function TWIZOperationsHelper.GetChangeAccountInfoTargets( const AAccounts: TArray; ANewType: Word): TArray; var - LWalletKey: TWalletKey; - LWalletKeys: TWalletKeys; - LNode: TNode; - LPCOperation: TPCOperation; - LOperationsHashTree: TOperationsHashTree; - LTotalAmount, LTotalSignerFee, LAmount, LFee: int64; - LDoOperation: boolean; - LOperationsTxt, LOperationToString, LTemp: string; - LAccountIdx, LNoOfOperations: integer; - LCurrentAccount: TAccount; - LOperationPayload : TOperationPayload; + I, LCount: Integer; begin - - LWalletKeys := TWallet.Keys; - LNode := TNode.Node; - - if not TWIZOperationsHelper.ValidateOperationsInput(ASelectedAccounts, LWalletKeys, LNode, AErrorMessage) then - Exit(False); - - LOperationsHashTree := TOperationsHashTree.Create; - try - LTotalAmount := 0; - LTotalSignerFee := 0; - LNoOfOperations := 0; - LOperationsTxt := ''; - LOperationToString := ''; - for LAccountIdx := Low(ASelectedAccounts) to High(ASelectedAccounts) do + if Length(AAccounts) <= 1 then + Exit(TArrayTool.Copy(AAccounts)); + SetLength(Result, Length(AAccounts)); + LCount := 0; + for I := 0 to High(AAccounts) do + if AAccounts[I].account_type <> ANewType then begin - LPCOperation := nil; // reset LPCOperation to Nil - LCurrentAccount := ASelectedAccounts[LAccountIdx]; - - if not TWIZOperationsHelper.IsOwnerOfWallet(LCurrentAccount, LWalletKeys, LWalletKey, AErrorMessage) then - Exit(False); - - if LCurrentAccount.account = ADestinationAccount.account then - begin - AErrorMessage := Format('Sender "%s" And Destination "%s" Accounts Are The Same', [LCurrentAccount.AccountString, ADestinationAccount.AccountString]); - Exit(False); - end; - - if not UpdatePayload(LCurrentAccount.accountInfo.accountKey, ADestinationAccount.accountInfo.accountKey, APayloadEncryptionMode, APayloadContent, LOperationPayload, APayloadEncryptionPassword, AErrorMessage) then - begin - AErrorMessage := Format('Error Encoding Payload Of Selected Account "%s. ", Specific Error Is "%s"', [LCurrentAccount.AccountString, AErrorMessage]); - Exit(False); - end; - - LDoOperation := True; - - case ASendPASCMode of - akaAllBalance: - if LCurrentAccount.balance > 0 then - begin - if LCurrentAccount.balance > AFee then - begin - LAmount := LCurrentAccount.balance - AFee; - LFee := AFee; - end - else - begin - LAmount := LCurrentAccount.balance; - LFee := 0; - end; - end - else - LDoOperation := False; - - akaSpecifiedAmount: - if LCurrentAccount.balance > UInt64(AFee) then - LFee := AFee - else - LFee := LCurrentAccount.balance; - end; - - if LDoOperation then - begin - LPCOperation := TOpTransaction.CreateTransaction( - TNode.Node.Bank.Safebox.CurrentProtocol, LCurrentAccount.account, LCurrentAccount.n_operation + 1, ADestinationAccount.account, LWalletKey.PrivateKey, LAmount, LFee, LOperationPayload); - try - LTemp := Format('%d. Transfer of %s PASC from %s to %s %s', [LNoOfOperations + 1, TAccountComp.FormatMoney(LAmount), LCurrentAccount.AccountString, ADestinationAccount.AccountString, sLineBreak]); - if LOperationsTxt <> '' then - LOperationsTxt := LOperationsTxt + LTemp + sLineBreak - else - LOperationsTxt := sLineBreak + LTemp; - - if Assigned(LPCOperation) then - begin - LOperationsHashTree.AddOperationToHashTree(LPCOperation); - Inc(LTotalAmount, LAmount); - Inc(LTotalSignerFee, LFee); - Inc(LNoOfOperations); - if LOperationToString <> '' then - LOperationToString := LOperationToString + #10; - LOperationToString := LOperationToString + LPCOperation.ToString; - end; - finally - FreeAndNil(LPCOperation); - end; - end; - + Result[LCount] := AAccounts[I]; + Inc(LCount); end; - if (LOperationsHashTree.OperationsCount = 0) then - begin - AErrorMessage := 'No Valid Operation To Execute'; - Exit(False); - end; - - Exit(TWIZOperationsHelper.SendPASCFinalizeAndDisplayMessage(LOperationsTxt, LOperationToString, LNoOfOperations, LTotalAmount, LTotalSignerFee, LOperationsHashTree, AErrorMessage)); - finally - LOperationsHashTree.Free; - end; - + SetLength(Result, LCount); end; -class function TWIZOperationsHelper.ExecuteChangeKey(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; APublicKey: TAccountKey; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; +class function TWIZOperationsHelper.PrepareOperations(const ARequest: TWalletOperationRequest; AProtocol: Word; ABlocksCount: Cardinal; AWalletKeys: TWalletKeys; out AOperations: TOperationsHashTree; var AErrorMessage: string): Boolean; var + LAccounts, LChangeInfoTargets: TArray; + LStates: TDictionary; + LDisposables: TDisposables; + LTree: TOperationsHashTree; + LOperation: TPCOperation; + LAccount, LSigner, LDestination: TAccount; LWalletKey: TWalletKey; - LWalletKeys: TWalletKeys; - LNode: TNode; - LPCOperation: TPCOperation; - LOperationsHashTree: TOperationsHashTree; - LTotalSignerFee, LFee: int64; - LIsV2: boolean; - LOperationsTxt, LOperationToString, LTemp: string; - LAccountIdx, LNoOfOperations: integer; - LCurrentAccount, LSignerAccount: TAccount; - LOperationPayload : TOperationPayload; -label - loop_start; -begin + LPayload: TOperationPayload; + LNewKey: TAccountKey; + LNewName: TRawBytes; + LAmount: Int64; + LLock: Cardinal; + LChangeName, LChangeType: Boolean; + i, j: Integer; + + function Fail(const AMessage: string): Boolean; + begin + AErrorMessage := AMessage; + Result := False; + end; - LWalletKeys := TWallet.Keys; - LNode := TNode.Node; - LSignerAccount := ASignerAccount; + function IsUsable(const AAccount: TAccount): Boolean; + begin + Result := False; + if AAccount.balance > CT_MaxWalletAmount then + Exit(Fail('The account balance exceeds the protocol limit')); + if TAccountComp.IsAccountBlockedByProtocol(AAccount.account, ABlocksCount) then + Exit(Fail(Format('Account %s is blocked by the current protocol', [AAccount.AccountString]))); + if TAccountComp.IsAccountLocked(AAccount.accountInfo, ABlocksCount) then + Exit(Fail(Format('Account %s is locked until block %u', [AAccount.AccountString, AAccount.accountInfo.locked_until_block]))); + Result := True; + end; - if not TWIZOperationsHelper.ValidateOperationsInput(ASelectedAccounts, LWalletKeys, LNode, AErrorMessage) then - Exit(False); + procedure Remember(const AAccount: TAccount); + begin + // A repeated signer must use the state resulting from earlier operations. + if not LStates.ContainsKey(AAccount.account) then + LStates.Add(AAccount.account, AAccount); + end; - LOperationsHashTree := TOperationsHashTree.Create; - try - LIsV2 := LNode.Bank.SafeBox.CurrentProtocol >= CT_PROTOCOL_2; - LTotalSignerFee := 0; - LNoOfOperations := 0; - LOperationsTxt := ''; - LOperationToString := ''; - for LAccountIdx := Low(ASelectedAccounts) to High(ASelectedAccounts) do - begin - loop_start: - LPCOperation := nil; // reset LPCOperation to Nil - LCurrentAccount := ASelectedAccounts[LAccountIdx]; + function AppendOperation(AOperation: TPCOperation): Boolean; + var + LOperationDisposables: TDisposables; + begin + LOperationDisposables.AddObject(AOperation); + Result := LTree.AddOperationToHashTree(AOperation); + if not Result then + Fail('The prepared operation conflicts with another operation in the batch'); + end; - if not TWIZOperationsHelper.IsOwnerOfWallet(LCurrentAccount, LWalletKeys, LWalletKey, AErrorMessage) then - Exit(False); +begin + Result := False; + AOperations := nil; + AErrorMessage := ''; + if Length(ARequest.Accounts) = 0 then + Exit(Fail('No selected account found')); + if not Assigned(AWalletKeys) then + Exit(Fail('No wallet keys found')); + if not (ARequest.OperationType in [omtSendPasc, omtChangeKey, omtEnlistAccountForSale, omtDelistAccountFromSale, omtChangeInfo, omtBuyAccount]) then + Exit(Fail('Unsupported wallet operation')); + if (ARequest.Fee < 0) or (ARequest.Fee > CT_MaxTransactionFee) then + Exit(Fail('Invalid operation fee')); + if (ARequest.OperationType <> omtSendPasc) and (AProtocol < CT_PROTOCOL_2) and + (ARequest.OperationType <> omtChangeKey) then + Exit(Fail('This operation requires protocol 2')); + if (ARequest.OperationType in [omtSendPasc, omtBuyAccount]) and + ((ARequest.Amount < 0) or (ARequest.Amount > CT_MaxTransactionAmount)) then + Exit(Fail('Invalid operation amount')); + if (ARequest.OperationType = omtSendPasc) and (ARequest.SendMode = akaSpecifiedAmount) and (ARequest.Amount = 0) then + Exit(Fail('The amount to send must be greater than zero')); + if (ARequest.OperationType = omtBuyAccount) and (Length(ARequest.Accounts) <> 1) then + Exit(Fail('Buying an account requires exactly one funding account')); + if (ARequest.OperationType in [omtChangeKey, omtEnlistAccountForSale, omtDelistAccountFromSale, omtChangeInfo]) and + (Length(ARequest.SignerAccounts) <> 1) and (Length(ARequest.SignerAccounts) <> Length(ARequest.Accounts)) then + Exit(Fail('A signer is required for every selected account')); + if (ARequest.OperationType in [omtChangeKey, omtEnlistAccountForSale, omtDelistAccountFromSale]) and + (Length(ARequest.SignerAccounts) <> 1) then + Exit(Fail('Choose one common signer for this operation')); + if (ARequest.OperationType = omtEnlistAccountForSale) and + ((ARequest.SalePrice <= 0) or (ARequest.SalePrice > CT_MaxTransactionAmount)) then + Exit(Fail('The sale price must be greater than zero and within the protocol limit')); + + if (ARequest.OperationType = omtChangeInfo) and + (Length(ARequest.ApprovedChangeInfoTargets) > 0) then + begin + // Finish refreshes pending accounts. Do not silently change the reviewed + // skip set if another operation changed an account while the wizard waited. + LChangeInfoTargets := GetChangeAccountInfoTargets(ARequest.Accounts, ARequest.NewType); + if Length(LChangeInfoTargets) <> Length(ARequest.ApprovedChangeInfoTargets) then + Exit(Fail('The accounts to change have changed. Return to Details to review the skipped accounts again.')); + for i := 0 to High(LChangeInfoTargets) do + if LChangeInfoTargets[i].account <> ARequest.ApprovedChangeInfoTargets[i].account then + Exit(Fail('The accounts to change have changed. Return to Details to review the skipped accounts again.')); + end; - if (TAccountComp.EqualAccountKeys(LCurrentAccount.accountInfo.accountKey, - APublicKey)) then + LAccounts := TArrayTool.Copy(ARequest.Accounts); + // Changing or locking a common signer's own account must be the last action. + // Work on a copy: the caller's selected-account order belongs to the UI. + if (Length(ARequest.SignerAccounts) = 1) and + (ARequest.OperationType in [omtChangeKey, omtEnlistAccountForSale]) then + for i := 0 to High(LAccounts) - 1 do + if LAccounts[i].account = ARequest.SignerAccounts[0].account then begin - AErrorMessage := 'New Key Is Same As Current Key'; - Exit(False); + TArrayTool.Swap(LAccounts, i, High(LAccounts)); + Break; end; - if LNode.Bank.SafeBox.CurrentProtocol >= CT_PROTOCOL_1 then - begin - // Signer: - LSignerAccount := ASignerAccount; - if (TAccountComp.IsAccountLocked(LSignerAccount.accountInfo, - LNode.Bank.BlocksCount)) then - begin - AErrorMessage := Format('Signer Account "%s" Is Locked Until Block %u', [LSignerAccount.AccountString, LSignerAccount.accountInfo.locked_until_block]); - Exit(False); - end; - if (not TAccountComp.EqualAccountKeys( - LSignerAccount.accountInfo.accountKey, LCurrentAccount.accountInfo.accountKey)) then - begin - AErrorMessage := Format('Signer Account %s Is Not The Owner Of Account %s', [LSignerAccount.AccountString, LCurrentAccount.AccountString]); - Exit(False); - end; - end - else - LSignerAccount := LCurrentAccount; + LStates := LDisposables.AddObject(TDictionary.Create) as TDictionary; + LTree := LDisposables.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + for i := 0 to High(LAccounts) do + begin + if LStates.ContainsKey(LAccounts[i].account) then + Exit(Fail('A selected account occurs more than once')); + LStates.Add(LAccounts[i].account, LAccounts[i]); + end; + for i := 0 to High(ARequest.SignerAccounts) do + Remember(ARequest.SignerAccounts[i]); + Remember(ARequest.DestinationAccount); - if not UpdatePayload(LCurrentAccount.accountInfo.accountKey, APublicKey, APayloadEncryptionMode, APayloadContent, LOperationPayload, APayloadEncryptionPassword, AErrorMessage) then + for i := 0 to High(LAccounts) do + begin + LAccount := LStates[LAccounts[i].account]; + // Skipping this target creates no operation, fee or nonce use of its own. + // It may still be the chosen payer for another target. Preserve batch name rules. + if (ARequest.OperationType = omtChangeInfo) and (Length(LAccounts) > 1) and + (LAccount.account_type = ARequest.NewType) then + Continue; + if not IsUsable(LAccount) then + Exit; + LSigner := LAccount; + if ARequest.OperationType in [omtChangeKey, omtEnlistAccountForSale, omtDelistAccountFromSale, omtChangeInfo] then + begin + if Length(ARequest.SignerAccounts) = 1 then + LSigner := LStates[ARequest.SignerAccounts[0].account] + else begin - AErrorMessage := Format('Error Encoding Payload Of Selected Account "%s. ", Specific Error Is "%s"', [LCurrentAccount.AccountString, AErrorMessage]); - Exit(False); + // Resolve by the original target index after any signer-last reorder. + for j := 0 to High(ARequest.Accounts) do + if ARequest.Accounts[j].account = LAccount.account then + LSigner := LStates[ARequest.SignerAccounts[j].account]; end; - - if LIsV2 then - begin - // must ensure is Signer account last if included in sender accounts (not necessarily ordered enumeration) - if (LAccountIdx < Length(ASelectedAccounts) - 1) and - (LCurrentAccount.account = LSignerAccount.account) then + if (ARequest.SigningMode = akaSecondary) and (LAccount.balance >= UInt64(ARequest.Fee)) then + LSigner := LAccount; + if (AProtocol < CT_PROTOCOL_2) and (ARequest.OperationType = omtChangeKey) then + LSigner := LAccount; + if not IsUsable(LSigner) then + Exit; + if not TAccountComp.EqualAccountKeys(LAccount.accountInfo.accountKey, LSigner.accountInfo.accountKey) then + Exit(Fail(Format('Signer %s does not own account %s', [LSigner.AccountString, LAccount.AccountString]))); + end; + if not IsOwnerOfWallet(LSigner, AWalletKeys, LWalletKey, AErrorMessage) then + Exit; + if LSigner.n_operation = High(Cardinal) then + Exit(Fail('The signer operation counter is exhausted')); + if LSigner.balance < UInt64(ARequest.Fee) then + Exit(Fail(Format('Insufficient funds in signer %s for all operation fees', [LSigner.AccountString]))); + + LDestination := LSigner; + LAmount := 0; + LNewKey := ARequest.PublicKey; + LLock := ARequest.LockedUntilBlock; + LChangeName := False; + LChangeType := False; + LNewName := nil; + + case ARequest.OperationType of + omtSendPasc: begin - TArrayTool.Swap(ASelectedAccounts, LAccountIdx, - Length(ASelectedAccounts) - 1); // ensure signer account processed last - goto loop_start; // TODO: remove ugly hack with refactoring! + LDestination := LStates[ARequest.DestinationAccount.account]; + if LAccount.account = LDestination.account then + Exit(Fail('Sender and destination accounts are the same')); + if TAccountComp.IsAccountBlockedByProtocol(LDestination.account, ABlocksCount) then + Exit(Fail('The destination account is blocked by the current protocol')); + case ARequest.SendMode of + akaSpecifiedAmount: LAmount := ARequest.Amount; + akaAllBalance: + begin + if LAccount.balance = 0 then + Continue; + if LAccount.balance <= UInt64(ARequest.Fee) then + Exit(Fail('The sender balance does not cover the fee and a positive amount')); + LAmount := LAccount.balance - UInt64(ARequest.Fee); + end; + else + Exit(Fail('Invalid send mode')); + end; + if (LAmount > CT_MaxTransactionAmount) or (UInt64(LAmount) > LAccount.balance - UInt64(ARequest.Fee)) then + Exit(Fail(Format('Insufficient funds in account %s for the amount and fee', [LAccount.AccountString]))); + if (LDestination.balance > CT_MaxWalletAmount) or (UInt64(LAmount) > CT_MaxWalletAmount - LDestination.balance) then + Exit(Fail('The destination balance would exceed the protocol limit')); end; - - // Maintain correct signer fee distribution - if UInt64(LTotalSignerFee) >= LSignerAccount.balance then - LFee := 0 - else if LSignerAccount.balance - UInt64(LTotalSignerFee) > - UInt64(AFee) then - LFee := AFee - else - LFee := LSignerAccount.balance - UInt64(LTotalSignerFee); - - LPCOperation := TOpChangeKeySigned.Create(LNode.Bank.Safebox.CurrentProtocol, LSignerAccount.account, - LSignerAccount.n_operation + LNoOfOperations + 1, LCurrentAccount.account, - LWalletKey.PrivateKey, APublicKey, LFee, LOperationPayload ); - end - else - LPCOperation := TOpChangeKey.Create(LNode.Bank.Safebox.CurrentProtocol, LCurrentAccount.account, LCurrentAccount.n_operation + - 1, LCurrentAccount.account, LWalletKey.PrivateKey, APublicKey, LFee, LOperationPayload); - - try - LTemp := Format('%d. Change Key To %s', [LNoOfOperations + 1, TAccountComp.GetECInfoTxt(APublicKey.EC_OpenSSL_NID), sLineBreak]); - if LOperationsTxt <> '' then - LOperationsTxt := LOperationsTxt + LTemp + sLineBreak - else - LOperationsTxt := sLineBreak + LTemp; - - if Assigned(LPCOperation) then + omtChangeKey: begin - LOperationsHashTree.AddOperationToHashTree(LPCOperation); - Inc(LNoOfOperations); - Inc(LTotalSignerFee, LFee); - if LOperationToString <> '' then - LOperationToString := LOperationToString + #10; - LOperationToString := LOperationToString + LPCOperation.ToString; + if not TAccountComp.IsValidAccountKey(LNewKey, AProtocol, AErrorMessage) then + Exit; + if TAccountComp.EqualAccountKeys(LNewKey, LAccount.accountInfo.accountKey) then + Exit(Fail('The new public key is the same as the current key')); end; - finally - FreeAndNil(LPCOperation); - end; - - end; - - if (LOperationsHashTree.OperationsCount = 0) then - begin - AErrorMessage := 'No Valid Operation to Execute'; - Exit(False); - end; - - Exit(TWIZOperationsHelper.OthersFinalizeAndDisplayMessage(LOperationsTxt, LOperationToString, LNoOfOperations, LTotalSignerFee, LOperationsHashTree, AErrorMessage)); - finally - LOperationsHashTree.Free; - end; - -end; - -class function TWIZOperationsHelper.ExecuteEnlistAccountForSale(const ASelectedAccounts: TArray; const ASignerAccount, ASellerAccount: TAccount; const APublicKey: TAccountKey; AFee, ASalePrice: int64; ALockedUntilBlock: UInt32; const AAccountSaleMode: TAccountSaleMode; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; -var - LWalletKey: TWalletKey; - LWalletKeys: TWalletKeys; - LNode: TNode; - LPCOperation: TPCOperation; - LOperationsHashTree: TOperationsHashTree; - LTotalSignerFee, LFee: int64; - LOperationsTxt, LOperationToString, LTemp: string; - LAccountIdx, LNoOfOperations: integer; - LCurrentAccount, LSignerAccount: TAccount; - LOperationPayload : TOperationPayload; - LANewAccountState : TAccountState; - LHashLock : T32Bytes; -begin - - LWalletKeys := TWallet.Keys; - LNode := TNode.Node; - LSignerAccount := ASignerAccount; - - if not TWIZOperationsHelper.ValidateOperationsInput(ASelectedAccounts, LWalletKeys, LNode, AErrorMessage) then - Exit(False); - - LOperationsHashTree := TOperationsHashTree.Create; - try - LTotalSignerFee := 0; - LNoOfOperations := 0; - LOperationsTxt := ''; - LOperationToString := ''; - - for LAccountIdx := Low(ASelectedAccounts) to High(ASelectedAccounts) do - begin - LPCOperation := nil; // reset LPCOperation to Nil - LCurrentAccount := ASelectedAccounts[LAccountIdx]; - - if not TWIZOperationsHelper.IsOwnerOfWallet(LCurrentAccount, LWalletKeys, LWalletKey, AErrorMessage) then - Exit(False); - - if TAccountComp.IsAccountForSale(LCurrentAccount.accountInfo) then - begin - AErrorMessage := Format('Account "%s" Is Already Enlisted For Sale', [LCurrentAccount.AccountString]); - Exit(False); - end; - - if (ASellerAccount.account = LCurrentAccount.account) then - begin - AErrorMessage := 'Seller Account Cannot Be Same As Account To Be Sold.'; - Exit(False); - end; - - if AAccountSaleMode = akaPrivateSale then - begin - - if TAccountComp.EqualAccountKeys(APublicKey, - LCurrentAccount.accountInfo.accountKey) then + omtEnlistAccountForSale: begin - AErrorMessage := 'You Cannot Sell To An Account That You Want To Enlist For Sale.'; - Exit(False); + if TAccountComp.IsAccountForSaleOrSwap(LAccount.accountInfo) then + Exit(Fail('The account is already listed for sale or swap')); + if ARequest.SellerAccount.account = LAccount.account then + Exit(Fail('The seller account cannot be the account being sold')); + if TAccountComp.IsAccountBlockedByProtocol(ARequest.SellerAccount.account, ABlocksCount) then + Exit(Fail('The seller account is blocked by the current protocol')); + case ARequest.SaleMode of + akaPublicSale: + begin + LNewKey := CT_TECDSA_Public_Nul; + LLock := 0; + end; + akaPrivateSale: + begin + if not TAccountComp.IsValidAccountKey(LNewKey, AProtocol, AErrorMessage) then + Exit; + if TAccountComp.EqualAccountKeys(LNewKey, LAccount.accountInfo.accountKey) then + Exit(Fail('The buyer public key is the current owner key')); + if (LLock <= ABlocksCount) or (UInt64(LLock) > UInt64(ABlocksCount) + CT_MaxFutureBlocksLockedAccount) then + Exit(Fail('The private sale requires a valid future locking block')); + end; + else + Exit(Fail('Invalid sale mode')); + end; end; - - if ALockedUntilBlock = 0 then + omtDelistAccountFromSale: + if not TAccountComp.IsAccountForSale(LAccount.accountInfo) then + Exit(Fail('The account is not listed for sale')); + omtChangeInfo: begin - AErrorMessage := 'You Didn''t Insert a Valid Locking Block.'; - Exit(False); + if Length(LAccounts) = 1 then + begin + LNewName := ARequest.NewName; + LChangeName := not TBaseType.Equals(LNewName, LAccount.name); + if LChangeName and (Length(LNewName) > 0) and + not TPascalCoinProtocol.IsValidAccountName(AProtocol, LNewName, AErrorMessage) then + Exit; + end; + LChangeType := ARequest.NewType <> LAccount.account_type; + if not LChangeName and not LChangeType then + Exit(Fail('The account name and type have not changed')); end; - end; - - if (LNode.Bank.SafeBox.CurrentProtocol = CT_PROTOCOL_1) then - begin - AErrorMessage := 'This Operation Needs PROTOCOL 2 Active'; - Exit(False); - end; - - if LSignerAccount.balance > UInt64(AFee) then - LFee := AFee - else - LFee := LSignerAccount.balance; - - if not UpdatePayload(LCurrentAccount.accountInfo.accountKey, LSignerAccount.accountInfo.accountKey, APayloadEncryptionMode, APayloadContent, LOperationPayload, APayloadEncryptionPassword, AErrorMessage) then - begin - AErrorMessage := Format('Error Encoding Payload Of Selected Account "%s. ", Specific Error Is "%s"', [LCurrentAccount.AccountString, AErrorMessage]); - Exit(False); - end; - - LHashLock := CT_HashLock_NUL; // Skybuck: check or debug if this needs to be a parameter, calculated or if it can be a nil record. Passed below into function - - case AAccountSaleMode of - akaPublicSale: - LPCOperation := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap( - LNode.Bank.Safebox.CurrentProtocol, - LANewAccountState, // Skybuck: check or debug LANewAccountState, perhaps this needs to be a parameter in the above function header ?!? - LSignerAccount.account, LSignerAccount.n_operation + 1 + LAccountIdx, - LCurrentAccount.account, ASalePrice, LFee, ASellerAccount.account, - APublicKey, 0, LWalletKey.PrivateKey, LHashLock, LOperationPayload); // Skybuck: check or debug LHashLock ! Perhaps needs to be a parameter to above function header ?!? - - akaPrivateSale: - - LPCOperation := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap( - LNode.Bank.Safebox.CurrentProtocol, - LANewAccountState, // Skybuck not sure if this must be a parameter or acquired locally or left empty, for now adding to code. - LSignerAccount.account, LSignerAccount.n_operation + 1 + LAccountIdx, - LCurrentAccount.account, ASalePrice, LFee, ASellerAccount.account, - APublicKey, ALockedUntilBlock, LWalletKey.PrivateKey, CT_HashLock_NUL, LOperationPayload); // Skybuck: somebody check if hashlock can be a nil record - else - raise Exception.Create('Invalid Account Sale Type') - end; - - try - LTemp := Format('%d. Enlist Account %s For Sale At a Price Of %s PASC %s', [LNoOfOperations + 1, LCurrentAccount.DisplayString, TAccountComp.FormatMoney(ASalePrice), sLineBreak]); - if LOperationsTxt <> '' then - LOperationsTxt := LOperationsTxt + LTemp + sLineBreak - else - LOperationsTxt := sLineBreak + LTemp; - - if Assigned(LPCOperation) then + omtBuyAccount: begin - LOperationsHashTree.AddOperationToHashTree(LPCOperation); - Inc(LNoOfOperations); - Inc(LTotalSignerFee, LFee); - if LOperationToString <> '' then - LOperationToString := LOperationToString + #10; - LOperationToString := LOperationToString + LPCOperation.ToString; + LDestination := ARequest.DestinationAccount; + if LAccount.account = LDestination.account then + Exit(Fail('Choose a separate funding account for this purchase')); + if not TAccountComp.IsAccountForSale(LDestination.accountInfo) then + Exit(Fail('The account to buy is not listed for sale')); + if TAccountComp.IsAccountBlockedByProtocol(LDestination.account, ABlocksCount) then + Exit(Fail('The account to buy is blocked by the current protocol')); + if (AProtocol >= CT_PROTOCOL_5) and not TAccountComp.IsAccountForPublicSale(LDestination.accountInfo) and + not TAccountComp.IsAccountLocked(LDestination.accountInfo, ABlocksCount) then + Exit(Fail('The private sale locking period has expired')); + if not TAccountComp.IsValidAccountKey(LNewKey, AProtocol, AErrorMessage) or + not TAccountComp.IsValidNewAccountKey(LDestination.accountInfo, LNewKey, AProtocol) then + Exit(Fail('The new owner public key does not satisfy the sale')); + LAmount := ARequest.Amount; + if (LAmount <= 0) or (UInt64(LAmount) > LAccount.balance - UInt64(ARequest.Fee)) then + Exit(Fail('Insufficient funds for the purchase amount and fee')); + // The purchased account may already hold part of its sale price. + if (LDestination.balance > CT_MaxWalletAmount) or (UInt64(LAmount) > CT_MaxWalletAmount - LDestination.balance) then + Exit(Fail('The purchase would exceed the account balance limit')); + if LDestination.balance + UInt64(LAmount) < LDestination.accountInfo.price then + Exit(Fail('The purchase amount and existing account balance do not cover its price')); + // The recipient in the purchase wizard is the new owner. Encrypting + // to the former owner would disclose its private recipient payload. + LDestination.accountInfo.accountKey := LNewKey; end; - finally - FreeAndNil(LPCOperation); - end; - end; - if (LOperationsHashTree.OperationsCount = 0) then + if ARequest.OperationType = omtChangeKey then + LDestination.accountInfo.accountKey := LNewKey; + if not UpdatePayload(LSigner.accountInfo.accountKey, LDestination.accountInfo.accountKey, + ARequest.PayloadMode, ARequest.PayloadContent, LPayload, ARequest.PayloadPassword, AErrorMessage) then + Exit; + + LOperation := nil; + case ARequest.OperationType of + omtSendPasc: + LOperation := TOpTransaction.CreateTransaction(AProtocol, LSigner.account, LSigner.n_operation + 1, + LDestination.account, LWalletKey.PrivateKey, LAmount, ARequest.Fee, LPayload); + omtChangeKey: + if AProtocol >= CT_PROTOCOL_2 then + LOperation := TOpChangeKeySigned.Create(AProtocol, LSigner.account, LSigner.n_operation + 1, + LAccount.account, LWalletKey.PrivateKey, LNewKey, ARequest.Fee, LPayload) + else + LOperation := TOpChangeKey.Create(AProtocol, LSigner.account, LSigner.n_operation + 1, + LAccount.account, LWalletKey.PrivateKey, LNewKey, ARequest.Fee, LPayload); + omtEnlistAccountForSale: + LOperation := TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap(AProtocol, as_ForSale, + LSigner.account, LSigner.n_operation + 1, LAccount.account, ARequest.SalePrice, ARequest.Fee, + ARequest.SellerAccount.account, LNewKey, LLock, LWalletKey.PrivateKey, CT_HashLock_NUL, LPayload); + omtDelistAccountFromSale: + LOperation := TOpDelistAccountForSale.CreateDelistAccountForSale(AProtocol, LSigner.account, + LSigner.n_operation + 1, LAccount.account, ARequest.Fee, LWalletKey.PrivateKey, LPayload); + omtChangeInfo: + LOperation := TOpChangeAccountInfo.CreateChangeAccountInfo(AProtocol, LSigner.account, + LSigner.n_operation + 1, LAccount.account, LWalletKey.PrivateKey, False, CT_TECDSA_Public_Nul, + LChangeName, LNewName, LChangeType, ARequest.NewType, False, nil, ARequest.Fee, LPayload); + omtBuyAccount: + LOperation := TOpBuyAccount.CreateBuy(AProtocol, LSigner.account, LSigner.n_operation + 1, + ARequest.DestinationAccount.account, ARequest.DestinationAccount.accountInfo.account_to_pay, + ARequest.DestinationAccount.accountInfo.price, LAmount, ARequest.Fee, LNewKey, LWalletKey.PrivateKey, LPayload); + end; + if not AppendOperation(LOperation) then + Exit; + Dec(LSigner.balance, UInt64(ARequest.Fee) + UInt64(LAmount)); + Inc(LSigner.n_operation); + LStates.AddOrSetValue(LSigner.account, LSigner); + if ARequest.OperationType = omtSendPasc then begin - AErrorMessage := 'No Valid Operation to Execute'; - Exit(False); + Inc(LDestination.balance, UInt64(LAmount)); + LStates.AddOrSetValue(LDestination.account, LDestination); end; - - Exit(TWIZOperationsHelper.OthersFinalizeAndDisplayMessage(LOperationsTxt, LOperationToString, LNoOfOperations, LTotalSignerFee, LOperationsHashTree, AErrorMessage)); - - finally - LOperationsHashTree.Free; end; + if LTree.OperationsCount = 0 then + begin + if (ARequest.OperationType = omtChangeInfo) and (Length(LAccounts) > 1) then + Exit(Fail(Format('All selected accounts already have type %u. No changes or fees are needed.', [ARequest.NewType]))); + Exit(Fail('No valid operation to execute')); + end; + LDisposables.RemoveObject(LTree); + AOperations := LTree; + Result := True; end; -class function TWIZOperationsHelper.ExecuteDelistAccountFromSale(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; +class function TWIZOperationsHelper.ValidatePreparedOperations(AOperations: TOperationsHashTree; ASnapshot: TPCSafeBoxTransaction; var AErrorMessage: String): Boolean; var - LWalletKey: TWalletKey; - LWalletKeys: TWalletKeys; - LNode: TNode; - LPCOperation: TPCOperation; - LOperationsHashTree: TOperationsHashTree; - LTotalSignerFee, LFee: int64; - LOperationsTxt, LOperationToString, LTemp: string; - LAccountIdx, LNoOfOperations: integer; - LCurrentAccount, LSignerAccount: TAccount; - LOperationPayload : TOperationPayload; + LDisposables: TDisposables; + LValidation: TPCSafeBoxTransaction; + LPrevious: TAccountPreviousBlockInfo; + i: Integer; begin - - LWalletKeys := TWallet.Keys; - LNode := TNode.Node; - LSignerAccount := ASignerAccount; - - if not TWIZOperationsHelper.ValidateOperationsInput(ASelectedAccounts, LWalletKeys, LNode, AErrorMessage) then - Exit(False); - - LOperationsHashTree := TOperationsHashTree.Create; - try - LTotalSignerFee := 0; - LNoOfOperations := 0; - LOperationsTxt := ''; - LOperationToString := ''; - - for LAccountIdx := Low(ASelectedAccounts) to High(ASelectedAccounts) do - begin - LPCOperation := nil; // reset LPCOperation to Nil - LCurrentAccount := ASelectedAccounts[LAccountIdx]; - - if not TWIZOperationsHelper.IsOwnerOfWallet(LCurrentAccount, LWalletKeys, LWalletKey, AErrorMessage) then - Exit(False); - - if not TAccountComp.IsAccountForSale(LCurrentAccount.accountInfo) then - begin - AErrorMessage := Format('Account "%s" is not enlisted for sale so cannot be delisted', [LCurrentAccount.AccountString]); - Exit(False); - end; - - if (TAccountComp.IsAccountLocked(LCurrentAccount.accountInfo, LNode.Bank.BlocksCount)) then - begin - AErrorMessage := Format('Target Account "%s" Is Locked Until Block %u', [LCurrentAccount.AccountString, LCurrentAccount.accountInfo.locked_until_block]); - Exit(False); - end; - - if (TAccountComp.IsAccountLocked(LSignerAccount.accountInfo, LNode.Bank.BlocksCount)) then - begin - AErrorMessage := Format('Signer Account "%s" Is Locked Until Block %u', [LSignerAccount.AccountString, LSignerAccount.accountInfo.locked_until_block]); - Exit(False); - end; - - if (not TAccountComp.EqualAccountKeys(LSignerAccount.accountInfo.accountKey, LCurrentAccount.accountInfo.accountKey)) then - begin - AErrorMessage := Format('Signer Account %s Is Not The Owner Of Delisted Account %s', [LSignerAccount.AccountString, LCurrentAccount.AccountString]); - Exit(False); - end; - - if (LNode.Bank.SafeBox.CurrentProtocol = CT_PROTOCOL_1) then - begin - AErrorMessage := 'This Operation Needs PROTOCOL 2 Active'; - Exit(False); - end; - - if LSignerAccount.balance > UInt64(AFee) then - LFee := AFee - else - LFee := LSignerAccount.balance; - - if not UpdatePayload(LCurrentAccount.accountInfo.accountKey, LSignerAccount.accountInfo.accountKey, APayloadEncryptionMode, APayloadContent, LOperationPayload, APayloadEncryptionPassword, AErrorMessage) then - begin - AErrorMessage := Format('Error Encoding Payload Of Selected Account "%s. ", Specific Error Is "%s"', [LCurrentAccount.AccountString, AErrorMessage]); - Exit(False); - end; - - LPCOperation := TOpDelistAccountForSale.CreateDelistAccountForSale(LNode.Bank.Safebox.CurrentProtocol, - LSignerAccount.account, LSignerAccount.n_operation + 1 + LAccountIdx, LCurrentAccount.account, LFee, LWalletKey.PrivateKey, - LOperationPayload); - - try - LTemp := Format('%d. Delist Account %s From Sale %s', [LNoOfOperations + 1, LCurrentAccount.DisplayString, sLineBreak]); - if LOperationsTxt <> '' then - LOperationsTxt := LOperationsTxt + LTemp + sLineBreak - else - LOperationsTxt := sLineBreak + LTemp; - - if Assigned(LPCOperation) then - begin - LOperationsHashTree.AddOperationToHashTree(LPCOperation); - Inc(LNoOfOperations); - Inc(LTotalSignerFee, LFee); - if LOperationToString <> '' then - LOperationToString := LOperationToString + #10; - LOperationToString := LOperationToString + LPCOperation.ToString; - end; - finally - FreeAndNil(LPCOperation); - end; - - end; - - if (LOperationsHashTree.OperationsCount = 0) then - begin - AErrorMessage := 'No Valid Operation to Execute'; - Exit(False); - end; - - Exit(TWIZOperationsHelper.OthersFinalizeAndDisplayMessage(LOperationsTxt, LOperationToString, LNoOfOperations, LTotalSignerFee, LOperationsHashTree, AErrorMessage)); - - finally - LOperationsHashTree.Free; + Result := False; + AErrorMessage := ''; + if not Assigned(AOperations) or not Assigned(ASnapshot) then + begin + AErrorMessage := 'Operation validation requires a prepared batch and account snapshot.'; + Exit; end; + LValidation := LDisposables.AddObject(TPCSafeBoxTransaction.Create(ASnapshot.FreezedSafeBox)) as TPCSafeBoxTransaction; + LValidation.CopyFrom(ASnapshot); + LPrevious := LDisposables.AddObject(TAccountPreviousBlockInfo.Create) as TAccountPreviousBlockInfo; + for i := 0 to AOperations.OperationsCount - 1 do + if not AOperations.GetOperation(i).DoOperation(LPrevious, LValidation, AErrorMessage) then + Exit; + Result := AOperations.OperationsCount > 0; + if not Result then AErrorMessage := 'No operation is available to validate.'; end; -class function TWIZOperationsHelper.ExecuteChangeAccountInfo(const ASelectedAccounts, ASignerAccounts: TArray; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; const ANewName: TRawBytes; const ANewType: word; var AErrorMessage: string): boolean; +class function TWIZOperationsHelper.ExecuteOperations(var ARequest: TWalletOperationRequest; var AErrorMessage: string): Boolean; var - LWalletKey: TWalletKey; - LWalletKeys: TWalletKeys; LNode: TNode; - LPCOperation: TPCOperation; - LOperationsHashTree: TOperationsHashTree; - LTotalSignerFee, LFee: int64; - LOperationsTxt, LOperationToString, LTemp: string; - LAccountIdx, LNoOfOperations, LAccNumberIndex: integer; - LCurrentAccount, LSignerAccount: TAccount; - LPayloadEncodedBytes, LNewName: TRawBytes; - LChangeType, LChangeName: boolean; - LChangeData : boolean; - LNewData : TRawBytes; - LOperationPayload : TOperationPayload; -begin - - LWalletKeys := TWallet.Keys; - LNode := TNode.Node; - LChangeName := False; - LChangeType := False; - - if not TWIZOperationsHelper.ValidateOperationsInput(ASelectedAccounts, LWalletKeys, LNode, AErrorMessage) then - Exit(False); - - LOperationsHashTree := TOperationsHashTree.Create; - try - LTotalSignerFee := 0; - LNoOfOperations := 0; - LOperationsTxt := ''; - LOperationToString := ''; - - for LAccountIdx := Low(ASelectedAccounts) to High(ASelectedAccounts) do - begin - LPCOperation := nil; // reset LPCOperation to Nil - LCurrentAccount := ASelectedAccounts[LAccountIdx]; - - if Length(ASelectedAccounts) = 1 then - begin - LSignerAccount := ASignerAccounts[0]; - - LNewName := ANewName; - - if not TBaseType.Equals(LNewName, LCurrentAccount.Name) then - begin - LChangeName := True; - if LNewName <> nil then - begin - if (not TPCSafeBox.ValidAccountName(LNewName, AErrorMessage)) then - begin - AErrorMessage := Format('New name "%s" is not a valid name: %s ', [LNewName.ToPrintable, AErrorMessage]); - Exit(False); - end; - LAccNumberIndex := (TNode.Node.Bank.SafeBox.FindAccountByName(LNewName)); - if (LAccNumberIndex >= 0) then - begin - AErrorMessage := Format('Name "%s" is used by account %s ', [LNewName.ToPrintable, TAccountComp.AccountNumberToAccountTxtNumber(LAccNumberIndex)]); - Exit(False); - end; - end; - end; - - if (TBaseType.Equals(LNewName, LCurrentAccount.Name)) and (ANewType = LCurrentAccount.account_type) then - begin - AErrorMessage := 'New account name and type are same as former.'; - Exit(False); - end; - - end - else - begin - LSignerAccount := LCurrentAccount; - end; - - LChangeType := ANewType <> LCurrentAccount.account_type; - - if not TWIZOperationsHelper.IsOwnerOfWallet(LCurrentAccount, LWalletKeys, LWalletKey, AErrorMessage) then - Exit(False); - - if (TAccountComp.IsAccountLocked(LCurrentAccount.accountInfo, LNode.Bank.BlocksCount)) then - begin - AErrorMessage := Format('Target Account "%s" Is Locked Until Block %u', [LCurrentAccount.AccountString, LCurrentAccount.accountInfo.locked_until_block]); - Exit(False); - end; - - if (TAccountComp.IsAccountLocked(LSignerAccount.accountInfo, LNode.Bank.BlocksCount)) then - begin - AErrorMessage := Format('Signer Account "%s" Is Locked Until Block %u', [LSignerAccount.AccountString, LSignerAccount.accountInfo.locked_until_block]); - Exit(False); - end; - - if (not TAccountComp.EqualAccountKeys(LSignerAccount.accountInfo.accountKey, LCurrentAccount.accountInfo.accountKey)) then - begin - AErrorMessage := Format('Signer Account %s Is Not The Owner Of Target Account %s', [LSignerAccount.AccountString, LCurrentAccount.AccountString]); - Exit(False); - end; - - if (LNode.Bank.SafeBox.CurrentProtocol = CT_PROTOCOL_1) then - begin - AErrorMessage := 'This Operation Needs PROTOCOL 2 Active'; - Exit(False); - end; - - if LSignerAccount.balance > UInt64(AFee) then - LFee := AFee - else - LFee := LSignerAccount.balance; - - if not UpdatePayload(LCurrentAccount.accountInfo.accountKey, LSignerAccount.accountInfo.accountKey, APayloadEncryptionMode, APayloadContent, LPayloadEncodedBytes, APayloadEncryptionPassword, AErrorMessage) then - begin - AErrorMessage := Format('Error Encoding Payload Of Selected Account "%s. ", Specific Error Is "%s"', [LCurrentAccount.AccountString, AErrorMessage]); - Exit(False); - end; - - LPCOperation := TOpChangeAccountInfo.CreateChangeAccountInfo( LNode.Bank.Safebox.CurrentProtocol, - LSignerAccount.account, LSignerAccount.n_operation + 1, LCurrentAccount.account, LWalletKey.PrivateKey, False, CT_TECDSA_Public_Nul, - LChangeName, LNewName, LChangeType, ANewType, LChangeData, LNewData, LFee, LOperationPayload); - - try - if (LChangeName) and (LChangeType) then - begin - LTemp := Format('%d. Change Account %s Name and Type from [%s, %d] To [%s, %d] %s', [LNoOfOperations + 1, LCurrentAccount.DisplayString, LCurrentAccount.Name.ToPrintable, LCurrentAccount.account_type, LNewName.ToPrintable, ANewType, sLineBreak]); - end - else if LChangeName then - begin - LTemp := Format('%d. Change Account %s Name from [%s] To [%s] %s', [LNoOfOperations + 1, LCurrentAccount.DisplayString, LCurrentAccount.Name.ToPrintable, LNewName.ToPrintable, sLineBreak]); - end - else if LChangeType then - begin - LTemp := Format('%d. Change Account %s Type from [%d] To [%d] %s', [LNoOfOperations + 1, LCurrentAccount.DisplayString, LCurrentAccount.account_type, ANewType, sLineBreak]); - end; - - if LOperationsTxt <> '' then - LOperationsTxt := LOperationsTxt + LTemp + sLineBreak - else - LOperationsTxt := sLineBreak + LTemp; - - if Assigned(LPCOperation) then - begin - LOperationsHashTree.AddOperationToHashTree(LPCOperation); - Inc(LNoOfOperations); - Inc(LTotalSignerFee, LFee); - if LOperationToString <> '' then - LOperationToString := LOperationToString + #10; - LOperationToString := LOperationToString + LPCOperation.ToString; - end; - finally - FreeAndNil(LPCOperation); - end; + LTree: TOperationsHashTree; + LDisposables: TDisposables; + LDescription: string; + LNameAccount, i: Integer; + LMempool: TPCOperationsComp; - end; + function Refresh(var AAccount: TAccount): Boolean; + begin + Result := AAccount.account < Cardinal(LNode.Bank.AccountsCount); + if Result then + AAccount := LNode.GetAccount(AAccount.account) + else + AErrorMessage := Format('Account %u no longer exists in the current blockchain', [AAccount.account]); + end; - if (LOperationsHashTree.OperationsCount = 0) then +begin + Result := False; + AErrorMessage := ''; + LNode := TNode.Node; + if not ValidateOperationsInput(ARequest.Accounts, TWallet.Keys, LNode, AErrorMessage) then + Exit; + // Re-read the pending state at Finish. A wizard can stay open while new + // operations change balances, owners, prices or operation counters. + for i := 0 to High(ARequest.Accounts) do + if not Refresh(ARequest.Accounts[i]) then + Exit; + for i := 0 to High(ARequest.SignerAccounts) do + if not Refresh(ARequest.SignerAccounts[i]) then + Exit; + if ARequest.OperationType in [omtSendPasc, omtBuyAccount] then + if not Refresh(ARequest.DestinationAccount) then + Exit; + if ARequest.OperationType = omtEnlistAccountForSale then + if not Refresh(ARequest.SellerAccount) then + Exit; + if (ARequest.OperationType = omtBuyAccount) and + (ARequest.DestinationAccount.accountInfo.account_to_pay >= Cardinal(LNode.Bank.AccountsCount)) then + begin + AErrorMessage := 'The sale proceeds account no longer exists'; + Exit; + end; + if (ARequest.OperationType = omtChangeInfo) and (Length(ARequest.Accounts) = 1) and + (Length(ARequest.NewName) > 0) then + begin + LNameAccount := LNode.Bank.SafeBox.FindAccountByName(ARequest.NewName); + if (LNameAccount >= 0) and (Cardinal(LNameAccount) <> ARequest.Accounts[0].account) then begin - AErrorMessage := 'No Valid Operation to Execute'; - Exit(False); + AErrorMessage := 'The account name is already in use'; + Exit; end; - - Exit(TWIZOperationsHelper.OthersFinalizeAndDisplayMessage(LOperationsTxt, LOperationToString, LNoOfOperations, LTotalSignerFee, LOperationsHashTree, AErrorMessage)); - + end; + if not PrepareOperations(ARequest, LNode.Bank.SafeBox.CurrentProtocol, LNode.Bank.BlocksCount, TWallet.Keys, LTree, AErrorMessage) then + Exit; + LDisposables.AddObject(LTree); + LMempool := LNode.LockMempoolRead; + try + if not ValidatePreparedOperations(LTree, LMempool.SafeBoxTransaction, AErrorMessage) then Exit; finally - LOperationsHashTree.Free; + LNode.UnlockMempoolRead; + end; + LDescription := ''; + for i := 0 to LTree.OperationsCount - 1 do + begin + if LDescription <> '' then + LDescription := LDescription + sLineBreak; + LDescription := LDescription + LTree.GetOperation(i).ToString; end; + if ARequest.OperationType = omtSendPasc then + Result := SendPASCFinalizeAndDisplayMessage(LDescription, LDescription, LTree.OperationsCount, + LTree.TotalAmount, LTree.TotalFee, LTree, AErrorMessage) + else + Result := OthersFinalizeAndDisplayMessage(LDescription, LDescription, LTree.OperationsCount, + LTree.TotalFee, LTree, AErrorMessage); end; -class function TWIZOperationsHelper.ExecuteBuyAccount(const ASelectedAccounts: TArray; const AAccountToBuy: TAccount; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; const AAmount: int64; const ANewOwnerPublicKey: TAccountKey; var AErrorMessage: string): boolean; +class function TWIZOperationsHelper.ExecuteSendPASC(const ASelectedAccounts: TArray; const ADestinationAccount: TAccount; AAmount, AFee: int64; const ASendPASCMode: TSendPASCMode; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string): boolean; var - LWalletKey: TWalletKey; - LWalletKeys: TWalletKeys; - LNode: TNode; - LPCOperation: TPCOperation; - LOperationsHashTree: TOperationsHashTree; - LTotalSignerFee, LFee: int64; - LOperationsTxt, LOperationToString, LTemp: string; - LAccountIdx, LNoOfOperations, LAccNumberIndex: integer; - LCurrentAccount, LSignerAccount: TAccount; - LPayloadEncodedBytes, LNewName: TRawBytes; - LOperationPayload : TOperationPayload; + LRequest: TWalletOperationRequest; begin + LRequest := Default(TWalletOperationRequest); + LRequest.OperationType := omtSendPasc; + LRequest.Accounts := TArrayTool.Copy(ASelectedAccounts); + LRequest.DestinationAccount := ADestinationAccount; + LRequest.Amount := AAmount; + LRequest.Fee := AFee; + LRequest.SendMode := ASendPASCMode; + LRequest.PayloadMode := APayloadEncryptionMode; + LRequest.PayloadContent := APayloadContent; + LRequest.PayloadPassword := APayloadEncryptionPassword; + Result := ExecuteOperations(LRequest, AErrorMessage); +end; - LWalletKeys := TWallet.Keys; - LNode := TNode.Node; - - if not TWIZOperationsHelper.ValidateOperationsInput(ASelectedAccounts, LWalletKeys, LNode, AErrorMessage) then - Exit(False); - - LOperationsHashTree := TOperationsHashTree.Create; - try - LTotalSignerFee := 0; - LNoOfOperations := 0; - LOperationsTxt := ''; - LOperationToString := ''; - // although buyaccount does not support multioperation for now, this loop was intentionally put here - for LAccountIdx := Low(ASelectedAccounts) to High(ASelectedAccounts) do - begin - LPCOperation := nil; // reset LPCOperation to Nil - LCurrentAccount := ASelectedAccounts[LAccountIdx]; - LSignerAccount := LCurrentAccount; - - if Length(ASelectedAccounts) <> 1 then - begin - AErrorMessage := 'Cannot Buy Accounts With MultiOperations. Use Only 1 Account'; - Exit(False); - end; - - if (LNode.Bank.SafeBox.CurrentProtocol = CT_PROTOCOL_1) then - begin - AErrorMessage := 'This Operation Needs PROTOCOL 2 Active'; - Exit(False); - end; - - if not TAccountComp.IsAccountForSale(AAccountToBuy.accountInfo) then - begin - AErrorMessage := Format('Account "%s" is not enlisted for sale', [AAccountToBuy.AccountString]); - Exit(False); - end; - - if LSignerAccount.balance > UInt64(AFee) then - LFee := AFee - else - LFee := LSignerAccount.balance; - - - if ((AAmount + LFee) > LCurrentAccount.balance) then - begin - AErrorMessage := Format('Insufficient Funds in Account "%s" to buy Account "%s"', [LCurrentAccount.AccountString, AAccountToBuy.AccountString]); - Exit(False); - end; - - if not UpdatePayload(LCurrentAccount.accountInfo.accountKey, LSignerAccount.accountInfo.accountKey, APayloadEncryptionMode, APayloadContent, LPayloadEncodedBytes, APayloadEncryptionPassword, AErrorMessage) then - begin - AErrorMessage := Format('Error Encoding Payload Of Selected Account "%s. ", Specific Error Is "%s"', [LCurrentAccount.AccountString, AErrorMessage]); - Exit(False); - end; - - LOperationPayload.payload_raw := LPayloadEncodedBytes; - LPCOperation := TOpBuyAccount.CreateBuy(LNode.Bank.Safebox.CurrentProtocol, LCurrentAccount.account, LCurrentAccount.n_operation + 1, AAccountToBuy.account, AAccountToBuy.accountInfo.account_to_pay, - AAccountToBuy.accountInfo.price, AAmount, LFee, ANewOwnerPublicKey, LWalletKey.PrivateKey, LOperationPayload); - - try - LTemp := Format('%d. Buy Account %s for %s PASC %s', [LNoOfOperations + 1, AAccountToBuy.AccountString, TAccountComp.FormatMoney(AAmount), sLineBreak]); - - if LOperationsTxt <> '' then - LOperationsTxt := LOperationsTxt + LTemp + sLineBreak - else - LOperationsTxt := sLineBreak + LTemp; - - if Assigned(LPCOperation) then - begin - LOperationsHashTree.AddOperationToHashTree(LPCOperation); - Inc(LNoOfOperations); - Inc(LTotalSignerFee, LFee); - if LOperationToString <> '' then - LOperationToString := LOperationToString + #10; - LOperationToString := LOperationToString + LPCOperation.ToString; - end; - finally - FreeAndNil(LPCOperation); - end; - - end; - - if (LOperationsHashTree.OperationsCount = 0) then - begin - AErrorMessage := 'No Valid Operation to Execute'; - Exit(False); - end; +class function TWIZOperationsHelper.ExecuteChangeKey(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; APublicKey: TAccountKey; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode): boolean; +var + LRequest: TWalletOperationRequest; +begin + LRequest := Default(TWalletOperationRequest); + LRequest.OperationType := omtChangeKey; + LRequest.Accounts := TArrayTool.Copy(ASelectedAccounts); + LRequest.SignerAccounts := TArray.Create(ASignerAccount); + LRequest.PublicKey := APublicKey; + LRequest.Fee := AFee; + LRequest.SigningMode := AOperationSigningMode; + LRequest.PayloadMode := APayloadEncryptionMode; + LRequest.PayloadContent := APayloadContent; + LRequest.PayloadPassword := APayloadEncryptionPassword; + Result := ExecuteOperations(LRequest, AErrorMessage); +end; - Exit(TWIZOperationsHelper.OthersFinalizeAndDisplayMessage(LOperationsTxt, LOperationToString, LNoOfOperations, LTotalSignerFee, LOperationsHashTree, AErrorMessage)); +class function TWIZOperationsHelper.ExecuteEnlistAccountForSale(const ASelectedAccounts: TArray; const ASignerAccount, ASellerAccount: TAccount; const APublicKey: TAccountKey; AFee, ASalePrice: int64; ALockedUntilBlock: UInt32; const AAccountSaleMode: TAccountSaleMode; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode): boolean; +var + LRequest: TWalletOperationRequest; +begin + LRequest := Default(TWalletOperationRequest); + LRequest.OperationType := omtEnlistAccountForSale; + LRequest.Accounts := TArrayTool.Copy(ASelectedAccounts); + LRequest.SignerAccounts := TArray.Create(ASignerAccount); + LRequest.SellerAccount := ASellerAccount; + LRequest.PublicKey := APublicKey; + LRequest.Fee := AFee; + LRequest.SigningMode := AOperationSigningMode; + LRequest.SalePrice := ASalePrice; + LRequest.LockedUntilBlock := ALockedUntilBlock; + LRequest.SaleMode := AAccountSaleMode; + LRequest.PayloadMode := APayloadEncryptionMode; + LRequest.PayloadContent := APayloadContent; + LRequest.PayloadPassword := APayloadEncryptionPassword; + Result := ExecuteOperations(LRequest, AErrorMessage); +end; - finally - LOperationsHashTree.Free; - end; +class function TWIZOperationsHelper.ExecuteDelistAccountFromSale(const ASelectedAccounts: TArray; const ASignerAccount: TAccount; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode): boolean; +var + LRequest: TWalletOperationRequest; +begin + LRequest := Default(TWalletOperationRequest); + LRequest.OperationType := omtDelistAccountFromSale; + LRequest.Accounts := TArrayTool.Copy(ASelectedAccounts); + LRequest.SignerAccounts := TArray.Create(ASignerAccount); + LRequest.Fee := AFee; + LRequest.SigningMode := AOperationSigningMode; + LRequest.PayloadMode := APayloadEncryptionMode; + LRequest.PayloadContent := APayloadContent; + LRequest.PayloadPassword := APayloadEncryptionPassword; + Result := ExecuteOperations(LRequest, AErrorMessage); end; +class function TWIZOperationsHelper.ExecuteChangeAccountInfo(const ASelectedAccounts, ASignerAccounts: TArray; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; const ANewName: TRawBytes; const ANewType: word; var AErrorMessage: string; AOperationSigningMode: TOperationSigningMode; const AApprovedTargets: TArray): boolean; +var + LRequest: TWalletOperationRequest; +begin + LRequest := Default(TWalletOperationRequest); + LRequest.OperationType := omtChangeInfo; + LRequest.Accounts := TArrayTool.Copy(ASelectedAccounts); + LRequest.SignerAccounts := TArrayTool.Copy(ASignerAccounts); + LRequest.Fee := AFee; + LRequest.SigningMode := AOperationSigningMode; + LRequest.NewName := ANewName; + LRequest.NewType := ANewType; + LRequest.ApprovedChangeInfoTargets := TArrayTool.Copy(AApprovedTargets); + LRequest.PayloadMode := APayloadEncryptionMode; + LRequest.PayloadContent := APayloadContent; + LRequest.PayloadPassword := APayloadEncryptionPassword; + Result := ExecuteOperations(LRequest, AErrorMessage); +end; +class function TWIZOperationsHelper.ExecuteBuyAccount(const ASelectedAccounts: TArray; const AAccountToBuy: TAccount; AFee: int64; const APayloadEncryptionMode: TPayloadEncryptionMode; const APayloadContent, APayloadEncryptionPassword: string; const AAmount: int64; const ANewOwnerPublicKey: TAccountKey; var AErrorMessage: string): boolean; +var + LRequest: TWalletOperationRequest; +begin + LRequest := Default(TWalletOperationRequest); + LRequest.OperationType := omtBuyAccount; + LRequest.Accounts := TArrayTool.Copy(ASelectedAccounts); + LRequest.DestinationAccount := AAccountToBuy; + LRequest.PublicKey := ANewOwnerPublicKey; + LRequest.Amount := AAmount; + LRequest.Fee := AFee; + LRequest.PayloadMode := APayloadEncryptionMode; + LRequest.PayloadContent := APayloadContent; + LRequest.PayloadPassword := APayloadEncryptionPassword; + Result := ExecuteOperations(LRequest, AErrorMessage); +end; end. diff --git a/src/gui-experimental/core.utils/UDataSources.pas b/src/gui-experimental/core.utils/UDataSources.pas index 97cbe26ef..6a5ef666a 100644 --- a/src/gui-experimental/core.utils/UDataSources.pas +++ b/src/gui-experimental/core.utils/UDataSources.pas @@ -13,37 +13,94 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Adapts accounts, pending/stored operations and block summaries to visual + grid data sources. Wallet sources page account records, cache history + snapshots and publish balance totals from owned workers. } + +{ Public types in this unit + + Type | Purpose + ----------------------------------------+------------------------------------------------------------------------- + TAccountOwnershipChange | Identifies a pending account ownership change. + TAccountReadContext | Carries the node, safebox, pending overlay and snapshot identity for a read. + TAccountsDataSource | Reads accounts filtered by owner keys and pending-state policy. + TAccountsDataSourceBase | Maps account records to common grid columns and values. + TAccountsOperationsDataSource | Loads bounded operation history for selected or wallet accounts. + TBlockSummarySource | Loads block summaries for the selected block range. + TBlockSummarySourceBase | Maps block summaries and configures range/hash-rate display. + TBlockSummarySourceBase.TShowHashRateAs | Selects kilo-, mega-, giga- or tera-hash display units. + TMyAccountsDataSource | Pages wallet-owned accounts and publishes asynchronous wallet totals. + TOperationsDataSource | Loads operations within a configured block range. + TOperationsDataSourceBase | Maps operation summaries to grid columns and values. + TPendingOperationsDataSource | Loads the node's available pending operation summaries. +} interface uses + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} Classes, SysUtils, UAccounts, UNode, UBlockchain, UCrypto, UCoreObjects, UCommon, UMemory, UConst, UCommon.Data, UCoreUtils, UBaseTypes, - UCommon.Collections, Generics.Collections, Generics.Defaults, syncobjs, UPCDataTypes; + UCommon.Collections, Generics.Collections, Generics.Defaults, syncobjs, UPCDataTypes, + UCache, UWalletStartupTask, UThread; type + {$IFDEF FPC} + TAccountKeySet = TSortedHashSet; + {$ELSE} + TAccountKeySet = THashSet; + {$ENDIF} + + TAccountReadContext = record + Node: TNode; + SafeBox: TPCSafeBox; + Mempool: TPCOperationsComp; + KeyIndex: TOrderedAccountKeysList; + BlockTip: Cardinal; + StateID: String; + end; + + TAccountOwnershipChange = record + Account: Cardinal; + NewKey: TAccountKey; + end; + { TAccountsDataSourceBase } TAccountsDataSourceBase = class(TCustomDataSource) protected + // Captured with the raw account snapshot; warm pages retain its chain tip. + FSnapshotBlockTip: Cardinal; function GetColumns: TDataColumns; override; public - function GetItemField(constref AItem: TAccount; const ABindingName: ansistring): variant; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TAccount; const ABindingName: String): variant; override; end; { TAccountsDataSource } TAccountsDataSource = class(TAccountsDataSourceBase) + private + FConfigLock: UThread.TReadWriteLock; + function GetIncludePending: Boolean; + procedure SetIncludePending(AIncludePending: Boolean); protected FIncludePending: boolean; - FKeys: TSortedHashSet; + FKeys: TAccountKeySet; function GetFilterKeys: TArray; procedure SetFilterKeys(const AKeys: TArray); + procedure ReadConfiguration(out AKeys: TArray; out AIncludePending: Boolean); public - property IncludePending: boolean read FIncludePending write FIncludePending; + property IncludePending: boolean read GetIncludePending write SetIncludePending; property FilterKeys: TArray read GetFilterKeys write SetFilterKeys; constructor Create(AOwner: TComponent); override; destructor Destroy; override; @@ -53,10 +110,59 @@ TAccountsDataSource = class(TAccountsDataSourceBase) { TMyAccountsDataSource } TMyAccountsDataSource = class(TAccountsDataSource) + private type + TAccountNumbers = TArray; + TPrefixCache = TActionCache; private FPBalanceSummary : PBalanceSummary; + FPrefixCache: TPrefixCache; + FPrefixStateID: String; + FPrefixContext: TAccountReadContext; + FPrefixKey: TAccountKey; + FPrefixCount: Integer; + FOwnershipStateID: String; + FOwnershipChanges: TArray; + FFullSnapshotBlockTip: Cardinal; + FBalanceTask: TWalletStartupTask; + FBalanceLock: UThread.TReadWriteLock; + FBalanceGeneration, FBalanceTaskGeneration: Int64; + FBalanceIncludePending: Boolean; + FBalanceReady: Boolean; + FBalanceSummary: TBalanceSummary; + FBalanceCompletedNotify: TThreadNotify; + FOnBalanceSummaryCompleted: TNotifyEvent; + FStopping: Boolean; + procedure BalanceTaskCompleted(Sender: TObject); + function FetchPrefix(const AKey: String): TAccountNumbers; + function EstimatePrefix(const AValues: TAccountNumbers): SizeInt; + function CachedPrefix(const AKey: TAccountKey; ACount: Integer; const AContext: TAccountReadContext): TAccountNumbers; + function CollectMembership(const AKeys: TArray; const AContext: TAccountReadContext; ARequiredPrefix: Integer; out ATotalCount: Integer): TArray; + procedure CalculateBalance(ATask: TWalletStartupTask; AGeneration: Int64; AIncludePending: Boolean); + protected + procedure BeginReadSnapshot(AIncludePending: Boolean; out AContext: TAccountReadContext); virtual; + function BeginBalanceSnapshot(ATask: TWalletStartupTask; AGeneration: Int64; AIncludePending: Boolean; out AContext: TAccountReadContext): Boolean; virtual; + procedure EndReadSnapshot(const AContext: TAccountReadContext); virtual; + function WalletKeys(const AContext: TAccountReadContext): TArray; virtual; + function KeyAccountCount(const AKey: TAccountKey; const AContext: TAccountReadContext): Integer; virtual; + function ReadKeyPrefix(const AKey: TAccountKey; ACount: Integer; const AContext: TAccountReadContext): TArray; virtual; + function OwnershipChanges(const AContext: TAccountReadContext): TArray; virtual; + function ReadPageAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; virtual; + function ReadBalanceAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; virtual; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; override; public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; property BalancePointer : PBalanceSummary read FPBalanceSummary write FPBalanceSummary; + // Backend/key changes invalidate totals. A view's FilterKeys change does not. + procedure InvalidateWalletData; + // The owning UI can pause aggregate work without waiting for its core locks. + // Keep the last complete summary until a later request publishes new data. + procedure CancelBalanceSummary; + // Request the owned aggregate from the UI; completion is delivered on that + // same thread, retaining the last complete summary during a newer request. + function TryGetBalanceSummary(out ASummary: TBalanceSummary): Boolean; + property OnBalanceSummaryCompleted: TNotifyEvent read FOnBalanceSummaryCompleted + write FOnBalanceSummaryCompleted; procedure FetchAll(const AContainer: TList); override; end; @@ -66,20 +172,40 @@ TOperationsDataSourceBase = class(TCustomDataSource) protected function GetColumns: TDataColumns; override; public - function GetItemField(constref AItem: TOperationResume; const ABindingName: ansistring): variant; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TOperationResume; const ABindingName: String): variant; override; end; { TAccountsOperationsDataSource } TAccountsOperationsDataSource = class(TOperationsDataSourceBase) private + FConfigLock: UThread.TReadWriteLock; + FConfigRevision: UInt64; FBlockDepth: Cardinal; FAccounts: TArray; FLastFetchID : String; FLastFetchBlockOperations : TArray; + FUseWalletAccounts: Boolean; + FWalletAccountsGeneration: Int64; + FLastWalletAccountsGeneration: Int64; + FWalletAccounts: TArray; + function GetAccounts: TArray; + procedure SetAccounts(const AAccounts: TArray); + function GetBlockDepth: Cardinal; + procedure SetBlockDepth(ABlockDepth: Cardinal); + function GetUseWalletAccounts: Boolean; + procedure SetUseWalletAccounts(AUseWalletAccounts: Boolean); + protected + function FetchWalletAccounts: TArray; virtual; + procedure RefreshWalletAccounts; public - property Accounts: TArray read FAccounts write FAccounts; - property BlockDepth: cardinal read FBlockDepth write FBlockDepth; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + property Accounts: TArray read GetAccounts write SetAccounts; + property BlockDepth: cardinal read GetBlockDepth write SetBlockDepth; + // Resolve all-wallet membership on the fetch worker, not on the UI thread. + property UseWalletAccounts: Boolean read GetUseWalletAccounts write SetUseWalletAccounts; + procedure InvalidateWalletAccounts; procedure FetchAll(const AContainer: TList); override; end; @@ -121,7 +247,7 @@ TBlockSummarySourceBase = class(TCustomDataSource) property EndBlock: cardinal read FEnd write FEnd; property HashRateAs: TShowHashRateAs read FHashRateAs write FHashRateAs; property HashRateAverageBlocksCount: integer read FHashRateAverageBlocksCount write FHashRateAverageBlocksCount; - function GetItemField(constref AItem: TBlockSummary; const ABindingName: ansistring): variant; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TBlockSummary; const ABindingName: String): variant; override; end; { TBlockSummarySource } @@ -134,7 +260,65 @@ TBlockSummarySource = class(TBlockSummarySourceBase) implementation uses - Math, UWallet, UTime; + Math, UWallet, UTime, UOpTransaction, UTxMultiOperation, UOrderedList; + +type + TWalletBalanceTask = class(TWalletStartupTask) + private + FSource: TMyAccountsDataSource; + FGeneration: Int64; + FIncludePending: Boolean; + protected + procedure BCExecute; override; + procedure ExecuteLoad; override; + public + constructor Create(ASource: TMyAccountsDataSource; AGeneration: Int64; AIncludePending: Boolean); + end; + +procedure CheckWalletQueryCancelled; inline; +begin + if TThread.CurrentThread.ThreadID = MainThreadID then Exit; + if TThread.CheckTerminated then Abort; +end; + +function LockQueryMempool(ANode: TNode): TPCOperationsComp; +begin + if TThread.CurrentThread.ThreadID = MainThreadID then Exit(ANode.LockMempoolRead); + // Cooperative read admission must not briefly acquire an exclusive node lock. + while not ANode.TryLockMempoolRead(0, Result) do begin + CheckWalletQueryCancelled; + Sleep(10); + end; + try + CheckWalletQueryCancelled; + except + ANode.UnlockMempoolRead; + raise; + end; +end; + +constructor TWalletBalanceTask.Create(ASource: TMyAccountsDataSource; AGeneration: Int64; AIncludePending: Boolean); +begin + inherited Create; + FSource := ASource; + FGeneration := AGeneration; + FIncludePending := AIncludePending; +end; + +procedure TWalletBalanceTask.ExecuteLoad; +begin + FSource.CalculateBalance(Self, FGeneration, FIncludePending); +end; + +procedure TWalletBalanceTask.BCExecute; +begin + inherited BCExecute; + // Queue the source receiver, not a callback on the task being retired. The + // source joins its computed worker before publishing completion to controls. + // Cancellation also retires the owned task. Otherwise its assigned reference + // would prevent a later generation from starting after synchronization ends. + FSource.FBalanceCompletedNotify.Invoke(FSource, FSource.BalanceTaskCompleted); +end; { TAccountsDataSourceBase } @@ -154,14 +338,16 @@ function TAccountsDataSourceBase.GetColumns: TDataColumns; TDataColumn.From('PriceDecimal'), TDataColumn.From('LockedUntil'), TDataColumn.From('NOperation'), - TDataColumn.From('LastUpdatedBlock') + TDataColumn.From('LastUpdatedBlock'), + TDataColumn.From('IsPending') ); end; -function TAccountsDataSourceBase.GetItemField(constref AItem: TAccount; const ABindingName: ansistring): variant; +function TAccountsDataSourceBase.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TAccount; const ABindingName: String): variant; var index: integer; begin + CheckWalletQueryCancelled; if ABindingName = 'AccountNumber' then Result := AItem.account else if ABindingName = 'Account' then @@ -190,44 +376,102 @@ function TAccountsDataSourceBase.GetItemField(constref AItem: TAccount; const AB Result := AItem.n_operation else if ABindingName = 'LastUpdatedBlock' then Result := AItem.GetLastUpdatedBlock + else if ABindingName = 'IsPending' then + Result := AItem.GetLastUpdatedBlock > FSnapshotBlockTip else raise Exception.Create(Format('Field not found "%s"', [ABindingName])); end; +function CreateAccountKeySet: TAccountKeySet; +begin + {$IFDEF FPC} + Result := TAccountKeySet.Create(TAccountKeyComparer.Create, TAccountKeyEqualityComparer.Create); + {$ELSE} + Result := TAccountKeySet.Create(TAccountKeyEqualityComparer.Create); + {$ENDIF} +end; { TAccountsDataSource } constructor TAccountsDataSource.Create(AOwner: TComponent); begin inherited Create(AOwner); - FKeys := TSortedHashSet.Create(TAccountKeyComparer.Create, TAccountKeyEqualityComparer.Create); + FConfigLock := UThread.TReadWriteLock.Create('DataSource.ConfigLock',UThread.lrpSupportsRecursion); + FKeys := CreateAccountKeySet; end; destructor TAccountsDataSource.Destroy; begin FKeys.Free; + FConfigLock.Free; + inherited Destroy; end; function TAccountsDataSource.GetFilterKeys: TArray; begin - EnterLock; + // Set enumeration initializes shared RTL wrappers, so retain write ownership. + FConfigLock.AcquireWriteLock; try Result := FKeys.ToArray; finally - ReleaseLock; + FConfigLock.ReleaseWriteLock; end; end; procedure TAccountsDataSource.SetFilterKeys(const AKeys: TArray); var i: integer; + LChanged: Boolean; begin - EnterLock; + FConfigLock.AcquireWriteLock; try + LChanged := FKeys.Count <> Length(AKeys); + if not LChanged then + for i := Low(AKeys) to High(AKeys) do + if not FKeys.Contains(AKeys[i]) then begin + LChanged := True; + Break; + end; + if not LChanged then Exit; FKeys.Clear; for i := Low(AKeys) to High(AKeys) do FKeys.Add(AKeys[i]); + InvalidateSnapshot; finally - ReleaseLock; + FConfigLock.ReleaseWriteLock; + end; +end; + +function TAccountsDataSource.GetIncludePending: Boolean; +begin + FConfigLock.AcquireReadLock; + try + Result := FIncludePending; + finally + FConfigLock.ReleaseReadLock; + end; +end; + +procedure TAccountsDataSource.SetIncludePending(AIncludePending: Boolean); +begin + FConfigLock.AcquireWriteLock; + try + if FIncludePending = AIncludePending then Exit; + FIncludePending := AIncludePending; + InvalidateSnapshot; + finally + FConfigLock.ReleaseWriteLock; + end; +end; + +procedure TAccountsDataSource.ReadConfiguration(out AKeys: TArray; out AIncludePending: Boolean); +begin + // Set enumeration initializes shared RTL wrappers, so retain write ownership. + FConfigLock.AcquireWriteLock; + try + AKeys := FKeys.ToArray; + AIncludePending := FIncludePending; + finally + FConfigLock.ReleaseWriteLock; end; end; @@ -235,45 +479,534 @@ procedure TAccountsDataSource.FetchAll(const AContainer: TList); var i: integer; LAcc: TAccount; + LNode: TNode; LSafeBox: TPCSafeBox; LMemPool : TPCOperationsComp; + LKeys: TArray; + LIncludePending: Boolean; + LKeySet: TAccountKeySet; + LDisposables: TDisposables; begin - LSafeBox := TNode.Node.Bank.SafeBox; - LMemPool := TNode.Node.LockMempoolRead; + ReadConfiguration(LKeys, LIncludePending); + LKeySet := LDisposables.AddObject(CreateAccountKeySet) as TAccountKeySet; + LKeySet.AddRange(LKeys); + LNode := TNode.Node; + LSafeBox := LNode.Bank.SafeBox; + LMemPool := nil; + if LIncludePending then LMemPool := LockQueryMempool(LNode); try - if FKeys.Count = 0 then - for i := 0 to LSafeBox.AccountsCount - 1 do - AContainer.Add(LSafeBox.Account(i)) // Load all accounts - else + LSafeBox.StartThreadSafe; + try + if LSafeBox.BlocksCount > 0 then + FSnapshotBlockTip := LSafeBox.BlocksCount - 1 + else FSnapshotBlockTip := 0; + // Filter while streaming. A small key selection must not allocate a + // second array containing every account in the entire safebox. for i := 0 to LSafeBox.AccountsCount - 1 do begin - // Load key-matching accounts - if FIncludePending then + CheckWalletQueryCancelled; + if LIncludePending then LAcc := LMemPool.SafeBoxTransaction.Account(i) else LAcc := LSafeBox.Account(i); - if FKeys.Contains(LAcc.accountInfo.accountKey) then + if (LKeySet.Count = 0) or LKeySet.Contains(LAcc.accountInfo.accountKey) then AContainer.Add(LAcc); end; + finally + LSafeBox.EndThreadSave; + end; finally - TNode.Node.UnlockMempoolRead; + if LIncludePending then LNode.UnlockMempoolRead; end; end; { TMyAccountsDataSource } +constructor TMyAccountsDataSource.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + FBalanceLock := UThread.TReadWriteLock.Create('DataSource.BalanceLock',UThread.lrpSupportsRecursion); + FBalanceCompletedNotify := TThreadNotify.Create(TThread.CurrentThread); + FBalanceTaskGeneration := -1; +end; + +destructor TMyAccountsDataSource.Destroy; +begin + FStopping := True; + FOnBalanceSummaryCompleted := nil; + if Assigned(FBalanceTask) then FBalanceTask.CancelAndWait; + FreeAndNil(FBalanceTask); + // Producers are joined before the owned dispatcher removes queued callbacks. + FBalanceCompletedNotify.Free; + FPrefixCache.Free; + FBalanceLock.Free; + inherited Destroy; +end; + +procedure TMyAccountsDataSource.BalanceTaskCompleted(Sender: TObject); +begin + if FStopping then Exit; + FreeAndNil(FBalanceTask); + if Assigned(FOnBalanceSummaryCompleted) then FOnBalanceSummaryCompleted(Self); +end; + +procedure TMyAccountsDataSource.BeginReadSnapshot(AIncludePending: Boolean; out AContext: TAccountReadContext); +begin + AContext := Default(TAccountReadContext); + AContext.Node := TNode.Node; + AContext.SafeBox := AContext.Node.Bank.SafeBox; + CheckWalletQueryCancelled; + if AIncludePending then AContext.Mempool := LockQueryMempool(AContext.Node); + try + AContext.SafeBox.StartThreadSafe; + try + AContext.KeyIndex := TWallet.Keys.AccountsKeyList; + AContext.KeyIndex.Lock; + try + if AContext.SafeBox.BlocksCount > 0 then + AContext.BlockTip := AContext.SafeBox.BlocksCount - 1; + AContext.StateID := TCrypto.ToHexaString(AContext.SafeBox.SafeBoxHash); + if Assigned(AContext.Mempool) then + AContext.StateID := AContext.StateID + ':' + + TCrypto.ToHexaString(AContext.Mempool.OperationsHashTree.HashTree); + except + AContext.KeyIndex.Unlock; + raise; + end; + except + AContext.SafeBox.EndThreadSave; + raise; + end; + except + if AIncludePending then AContext.Node.UnlockMempoolRead; + raise; + end; +end; + +procedure TMyAccountsDataSource.EndReadSnapshot(const AContext: TAccountReadContext); +begin + AContext.KeyIndex.Unlock; + AContext.SafeBox.EndThreadSave; + if Assigned(AContext.Mempool) then AContext.Node.UnlockMempoolRead; +end; + +function TMyAccountsDataSource.WalletKeys(const AContext: TAccountReadContext): TArray; +begin + Result := TArray(AContext.KeyIndex.ToArray); +end; + +function TMyAccountsDataSource.KeyAccountCount(const AKey: TAccountKey; const AContext: TAccountReadContext): Integer; +var LNumbers: TAccountsNumbersList; +begin + LNumbers := AContext.KeyIndex.GetAccountsUsingThisKey(AKey); + if Assigned(LNumbers) then Result := LNumbers.Count else Result := 0; +end; + +function TMyAccountsDataSource.ReadKeyPrefix(const AKey: TAccountKey; ACount: Integer; const AContext: TAccountReadContext): TArray; +var LNumbers: TAccountsNumbersList; LList: TList; LScope: TDisposables; +begin + Result := nil; + LNumbers := AContext.KeyIndex.GetAccountsUsingThisKey(AKey); + if not Assigned(LNumbers) then Exit; + LList := LScope.AddObject(TList.Create) as TList; + LNumbers.FillList(0, Min(ACount, LNumbers.Count), LList); + Result := LList.ToArray; +end; + +function TMyAccountsDataSource.ReadPageAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; +begin + if Assigned(AContext.Mempool) then + Result := AContext.Mempool.SafeBoxTransaction.Account(AAccountNumber) + else + Result := AContext.SafeBox.Account(AAccountNumber); +end; + +function TMyAccountsDataSource.ReadBalanceAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; +begin + if Assigned(AContext.Mempool) then + Result := AContext.Mempool.SafeBoxTransaction.Account(AAccountNumber) + else + Result := AContext.SafeBox.Account(AAccountNumber); +end; + +function TMyAccountsDataSource.EstimatePrefix(const AValues: TAccountNumbers): SizeInt; +begin + Result := SizeInt(Length(AValues)) * SizeOf(Cardinal); +end; + +function TMyAccountsDataSource.FetchPrefix(const AKey: String): TAccountNumbers; +begin + Result := ReadKeyPrefix(FPrefixKey, FPrefixCount, FPrefixContext); +end; + +function TMyAccountsDataSource.CachedPrefix(const AKey: TAccountKey; ACount: Integer; const AContext: TAccountReadContext): TAccountNumbers; +var LKey: String; LTarget: Integer; +begin + if not Assigned(FPrefixCache) then + FPrefixCache := TPrefixCache.Create(nil, + TCacheValueFetcher.FromObjectFunction(FetchPrefix), + TCacheSizeEstimator.FromObjectFunction(EstimatePrefix), + idpNone, TCacheReapPolicy.crpOldest, + TCacheExpirationPolicy.epNone, + TCacheNullValuePolicy.nvpCacheNormally, 64*1024*1024, TTimeSpan.MaxValue); + if FPrefixStateID <> AContext.StateID then begin + FPrefixCache.Flush; + FPrefixStateID := AContext.StateID; + end; + LKey := TCrypto.ToHexaString(TAccountComp.AccountKey2RawString(AKey)); + if FPrefixCache.ContainsCachedItem(LKey) then begin + Result := FPrefixCache[LKey]; + if Length(Result) >= ACount then Exit; + FPrefixCache.Remove(LKey); + end; + // Grow geometrically so walking consecutive pages does not repeatedly scan + // a B-tree prefix for every individual page. Account records are not loaded. + LTarget := 32; + while (LTarget < ACount) and (LTarget <= High(Integer) div 2) do LTarget := LTarget * 2; + if LTarget < ACount then LTarget := ACount; + FPrefixKey := AKey; + FPrefixCount := LTarget; + FPrefixContext := AContext; + if SizeInt(LTarget) * SizeOf(Cardinal) > FPrefixCache.MaxCapacity then + Result := ReadKeyPrefix(AKey, ACount, AContext) + else + Result := FPrefixCache[LKey]; +end; + +function TMyAccountsDataSource.OwnershipChanges(const AContext: TAccountReadContext): TArray; +var + LOwners: TDictionary; + LPair: TPair; + LOperation: TPCOperation; + LChanges: TMultiOpChangesInfo; + LScope: TDisposables; + i, j: Integer; +begin + Result := nil; + if not Assigned(AContext.Mempool) then Exit; + if FOwnershipStateID = AContext.StateID then Exit(FOwnershipChanges); + LOwners := LScope.AddObject(TDictionary.Create) as TDictionary; + // Read ownership metadata, not hundreds of thousands of account records. + // Applied pending operations expose their final recipient/new-key data. + for i := 0 to AContext.Mempool.OperationsHashTree.OperationsCount - 1 do begin + CheckWalletQueryCancelled; + LOperation := AContext.Mempool.OperationsHashTree.GetOperation(i); + if LOperation is TOpChangeKey then + LOwners.AddOrSetValue(TOpChangeKey(LOperation).Data.account_target, TOpChangeKey(LOperation).Data.new_accountkey) + else if LOperation is TOpRecoverFounds then + LOwners.AddOrSetValue(TOpRecoverFounds(LOperation).Data.account, TOpRecoverFounds(LOperation).Data.new_accountkey) + else if LOperation is TOpChangeAccountInfo then begin + if public_key in TOpChangeAccountInfo(LOperation).Data.changes_type then + LOwners.AddOrSetValue(TOpChangeAccountInfo(LOperation).Data.account_target, + TOpChangeAccountInfo(LOperation).Data.new_accountkey); + end else if LOperation is TOpTransaction then begin + if TOpTransaction(LOperation).Data.opTransactionStyle <> transaction then + LOwners.AddOrSetValue(TOpTransaction(LOperation).Data.target, + TOpTransaction(LOperation).Data.new_accountkey); + end else if LOperation is TOpMultiOperation then begin + LChanges := TOpMultiOperation(LOperation).Data.changesInfo; + for j := 0 to High(LChanges) do begin + CheckWalletQueryCancelled; + if public_key in LChanges[j].Changes_type then + LOwners.AddOrSetValue(LChanges[j].Account, LChanges[j].New_Accountkey); + end; + end; + end; + SetLength(Result, LOwners.Count); + i := 0; + for LPair in LOwners do begin + CheckWalletQueryCancelled; + Result[i].Account := LPair.Key; + Result[i].NewKey := LPair.Value; + // Some legacy automatic-swap/recovery encodings omit the final key. Only + // those exceptional changed accounts need a pending account lookup. + if Result[i].NewKey.EC_OpenSSL_NID = 0 then + Result[i].NewKey := AContext.Mempool.SafeBoxTransaction.Account(LPair.Key).accountInfo.accountKey; + Inc(i); + end; + CheckWalletQueryCancelled; + FOwnershipChanges := Result; + FOwnershipStateID := AContext.StateID; +end; + +function TMyAccountsDataSource.CollectMembership(const AKeys: TArray; const AContext: TAccountReadContext; ARequiredPrefix: Integer; out ATotalCount: Integer): TArray; +var + LKeys: TArray; + LKeySet: TAccountKeySet; + LChanges: TArray; + LValues: TList; + LMembers: TDictionary; + LNumbers: TArray; + LKey: TAccountKey; + LNumber: Cardinal; + LCount, LNeeded, i: Integer; + LScope: TDisposables; +begin + LKeys := AKeys; + if Length(LKeys) = 0 then LKeys := WalletKeys(AContext); + LKeySet := LScope.AddObject(CreateAccountKeySet) as TAccountKeySet; + LKeySet.AddRange(LKeys); + LValues := LScope.AddObject(TList.Create) as TList; + LChanges := OwnershipChanges(AContext); + ATotalCount := 0; + if Length(LChanges) = 0 then begin + for LKey in LKeySet do begin + CheckWalletQueryCancelled; + LCount := KeyAccountCount(LKey, AContext); + Inc(ATotalCount, LCount); + LNeeded := LCount; + if ARequiredPrefix >= 0 then LNeeded := Min(LNeeded, ARequiredPrefix); + if LNeeded > 0 then begin + if ARequiredPrefix < 0 then LNumbers := ReadKeyPrefix(LKey, LNeeded, AContext) + else LNumbers := CachedPrefix(LKey, LNeeded, AContext); + LValues.AddRange(LNumbers); + end; + end; + end else begin + // Ownership changes can add/remove accounts without changing the count. + // Correct the membership using IDs only; even this conservative path never + // reads unrelated wallet account records before populating a page. + LMembers := LScope.AddObject(TDictionary.Create) as TDictionary; + for LKey in LKeySet do begin + CheckWalletQueryCancelled; + LNumbers := ReadKeyPrefix(LKey, KeyAccountCount(LKey, AContext), AContext); + for LNumber in LNumbers do begin + CheckWalletQueryCancelled; + LMembers.AddOrSetValue(LNumber, 0); + end; + end; + for i := 0 to High(LChanges) do begin + CheckWalletQueryCancelled; + if LKeySet.Contains(LChanges[i].NewKey) then + LMembers.AddOrSetValue(LChanges[i].Account, 0) + else + LMembers.Remove(LChanges[i].Account); + end; + for LNumber in LMembers.Keys do begin + CheckWalletQueryCancelled; + LValues.Add(LNumber); + end; + ATotalCount := LValues.Count; + end; + CheckWalletQueryCancelled; + LValues.Sort; + Result := LValues.ToArray; + if (ARequiredPrefix >= 0) and (Length(Result) > ARequiredPrefix) then + SetLength(Result, ARequiredPrefix); +end; + +function TMyAccountsDataSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; +var + LFilter: TColumnFilter; + LContext: TAccountReadContext; + LKeys: TArray; + LIncludePending, LFast: Boolean; + LNumbers: TArray; + LRows: TArray; + LTotal, LStart, LTake, LRequired, i: Integer; + LRequested: Int64; +begin + LFast := AParams.PageSize > 0; + for LFilter in AParams.Filter do + if (LFilter.Filter <> vgfSortable) or + ((LFilter.Sort <> sdNone) and + ((LFilter.ColumnName <> 'AccountNumber') or (LFilter.Sort <> sdAscending))) then + LFast := False; + if not LFast then begin + EnterLock; + try + // The full query cache carries a different snapshot than a later fast + // page. Its pending-color comparison must retain its own captured tip. + FSnapshotBlockTip := FFullSnapshotBlockTip; + Result := inherited FetchPage(AParams, ADataTable); + finally + ReleaseLock; + end; + Exit; + end; + EnterLock; + try + ReadConfiguration(LKeys, LIncludePending); + LRequested := (Int64(Max(AParams.PageIndex, 0)) + 1) * AParams.PageSize; + if LRequested > High(Integer) then LRequired := High(Integer) else LRequired := LRequested; + BeginReadSnapshot(LIncludePending, LContext); + try + FSnapshotBlockTip := LContext.BlockTip; + LNumbers := CollectMembership(LKeys, LContext, LRequired, LTotal); + Result.TotalDataCount := LTotal; + if LTotal = 0 then begin + Result.PageCount := 0; + Result.PageIndex := -1; + LTake := 0; + LStart := 0; + end else begin + Result.PageCount := ((LTotal - 1) div AParams.PageSize) + 1; + Result.PageIndex := ClipValue(AParams.PageIndex, 0, Result.PageCount - 1); + LStart := Result.PageIndex * AParams.PageSize; + LTake := Min(AParams.PageSize, LTotal - LStart); + end; + SetLength(LRows, LTake); + for i := 0 to LTake - 1 do begin + CheckWalletQueryCancelled; + LRows[i] := ReadPageAccount(LNumbers[LStart + i], LContext); + end; + finally + EndReadSnapshot(LContext); + end; + ADataTable.Columns := GetColumns; + SetLength(ADataTable.Rows, LTake); + for i := 0 to LTake - 1 do begin + CheckWalletQueryCancelled; + ADataTable.Rows[i] := TDataRow.New(PtrInt(ClassInfo), ADataTable.Columns); + DehydrateItem(LRows[i], ADataTable.Rows[i]); + end; + finally + ReleaseLock; + end; +end; + procedure TMyAccountsDataSource.FetchAll(const AContainer: TList); var - i: integer; - LAccs: TArray; - LBalanceSummary : TBalanceSummary; + i, LTotal: integer; + LContext: TAccountReadContext; + LNumbers: TArray; + LKeys: TArray; + LIncludePending: Boolean; +begin + ReadConfiguration(LKeys, LIncludePending); + BeginReadSnapshot(LIncludePending, LContext); + try + FSnapshotBlockTip := LContext.BlockTip; + FFullSnapshotBlockTip := LContext.BlockTip; + LNumbers := CollectMembership(LKeys, LContext, -1, LTotal); + for i := 0 to High(LNumbers) do begin + CheckWalletQueryCancelled; + AContainer.Add(ReadPageAccount(LNumbers[i], LContext)); + end; + finally + EndReadSnapshot(LContext); + end; +end; + +procedure TMyAccountsDataSource.InvalidateWalletData; +begin + InterlockedIncrement64(FBalanceGeneration); + InvalidateSnapshot; +end; + +procedure TMyAccountsDataSource.CancelBalanceSummary; +begin + InterlockedIncrement64(FBalanceGeneration); + if Assigned(FBalanceTask) then FBalanceTask.Cancel; +end; + +function TMyAccountsDataSource.TryGetBalanceSummary(out ASummary: TBalanceSummary): Boolean; +var LGeneration: Int64; LIncludePending: Boolean; begin - LAccs := TCoreTool.GetUserAccounts(LBalanceSummary, FIncludePending); - FPBalanceSummary^ := LBalanceSummary; - if FKeys.Count > 0 then begin - for i := Low(LAccs) to High(LAccs) do - if FKeys.Contains(LAccs[i].accountInfo.accountKey) then - AContainer.Add(LAccs[i]); - end else AContainer.AddRange(LAccs); + LIncludePending := IncludePending; + if FBalanceIncludePending <> LIncludePending then begin + FBalanceIncludePending := LIncludePending; + InterlockedIncrement64(FBalanceGeneration); + end; + LGeneration := InterlockedCompareExchange64(FBalanceGeneration, 0, 0); + if (not Assigned(FBalanceTask)) and (FBalanceTaskGeneration <> LGeneration) then begin + FBalanceTaskGeneration := LGeneration; + FBalanceTask := TWalletBalanceTask.Create(Self, LGeneration, LIncludePending); + FBalanceTask.Start; + end; + FBalanceLock.AcquireReadLock; + try + Result := FBalanceReady; + ASummary := FBalanceSummary; + finally + FBalanceLock.ReleaseReadLock; + end; + // Keep the legacy pointer usable, but write it only from this UI request, not + // from a worker that could outlive the form owning the pointed-to record. + if Result and Assigned(FPBalanceSummary) then FPBalanceSummary^ := ASummary; +end; + +function TMyAccountsDataSource.BeginBalanceSnapshot(ATask: TWalletStartupTask; AGeneration: Int64; AIncludePending: Boolean; out AContext: TAccountReadContext): Boolean; +var LNode: TNode; LMempool: TPCOperationsComp; +begin + Result := False; + LNode := TNode.Node; + // A pending-buffer save can retain the mempool. Cancel promptly instead of + // blocking a form's owned aggregate worker in an unconditional acquisition. + while not LNode.TryLockMempoolRead(0, LMempool) do begin + if ATask.Cancelled or + (InterlockedCompareExchange64(FBalanceGeneration, 0, 0) <> AGeneration) then Exit; + Sleep(10); + end; + try + if ATask.Cancelled or + (InterlockedCompareExchange64(FBalanceGeneration, 0, 0) <> AGeneration) then Exit; + BeginReadSnapshot(AIncludePending, AContext); + Result := True; + finally + LNode.UnlockMempoolRead; + end; +end; + +procedure TMyAccountsDataSource.CalculateBalance(ATask: TWalletStartupTask; AGeneration: Int64; AIncludePending: Boolean); +const CChunkSize = 128; +var + LContext: TAccountReadContext; + LNumbers: TArray; + LStateID: String; + LSummary: TBalanceSummary; + LAccount: TAccount; + LTotal, i, j, LLast: Integer; +begin + if not BeginBalanceSnapshot(ATask, AGeneration, AIncludePending, LContext) then Exit; + try + if ATask.Cancelled or + (InterlockedCompareExchange64(FBalanceGeneration, 0, 0) <> AGeneration) then Exit; + LStateID := LContext.StateID; + LNumbers := CollectMembership(nil, LContext, -1, LTotal); + if ATask.Cancelled or + (InterlockedCompareExchange64(FBalanceGeneration, 0, 0) <> AGeneration) then Exit; + finally + EndReadSnapshot(LContext); + end; + LSummary := CT_BalanceSummary_Nil; + LSummary.TotalPASA := Length(LNumbers); + i := 0; + while i < Length(LNumbers) do begin + if not BeginBalanceSnapshot(ATask, AGeneration, AIncludePending, LContext) then Exit; + try + if LContext.StateID <> LStateID then Exit; + LLast := Min(i + CChunkSize - 1, High(LNumbers)); + for j := i to LLast do begin + if ATask.Cancelled or + (InterlockedCompareExchange64(FBalanceGeneration, 0, 0) <> AGeneration) then Exit; + LAccount := ReadBalanceAccount(LNumbers[j], LContext); + Inc(LSummary.TotalPASC, LAccount.balance); + end; + finally + EndReadSnapshot(LContext); + end; + i := LLast + 1; + // Yield between short, coherent chunks instead of retaining core locks + // while reading every account for the global Overview total. + Sleep(0); + end; + // Recheck the actual core state immediately before publication too: a quiet + // debounce can leave the UI generation unchanged while new blocks arrive. + if not BeginBalanceSnapshot(ATask, AGeneration, AIncludePending, LContext) then Exit; + try + if LContext.StateID <> LStateID then Exit; + FBalanceLock.AcquireWriteLock; + try + if ATask.Cancelled or + (InterlockedCompareExchange64(FBalanceGeneration, 0, 0) <> AGeneration) then Exit; + FBalanceSummary := LSummary; + FBalanceReady := True; + finally + FBalanceLock.ReleaseWriteLock; + end; + finally + EndReadSnapshot(LContext); + end; end; { TOperationsDataSourceBase } @@ -303,10 +1036,11 @@ function TOperationsDataSourceBase.GetColumns: TDataColumns; ); end; -function TOperationsDataSourceBase.GetItemField(constref AItem: TOperationResume; const ABindingName: ansistring): variant; +function TOperationsDataSourceBase.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TOperationResume; const ABindingName: String): variant; var index: integer; begin + CheckWalletQueryCancelled; if ABindingName = 'OPHASH' then Result := TPCOperation.OperationHashAsHexa(AItem.OperationHash) else if ABindingName = 'UnixTime' then @@ -320,7 +1054,12 @@ function TOperationsDataSourceBase.GetItemField(constref AItem: TOperationResume else if ABindingName = 'BlockLocation' then Result := IIF(AItem.OpType <> CT_PseudoOp_Reward, IntToStr(AItem.Block) + '/' + IntToStr(AItem.NOpInsideBlock + 1), IntToStr(AItem.Block)) else if ABindingName = 'BlockLocationSortable' then - Result := IIF(AItem.OpType <> CT_PseudoOp_Reward, UInt64(AItem.Block) * 4294967296 + UInt32(AItem.NOpInsideBlock), 0) // number pattern = [block][opindex] + begin + if AItem.OpType <> CT_PseudoOp_Reward then + Result := (UInt64(AItem.Block) shl 32) + UInt32(AItem.NOpInsideBlock) + else + Result := UInt64(0); // number pattern = [block][opindex] + end else if ABindingName = 'Account' then Result := TAccountComp.AccountNumberToAccountTxtNumber(AItem.AffectedAccount) else if ABindingName = 'AccountNumber' then @@ -351,42 +1090,223 @@ function TOperationsDataSourceBase.GetItemField(constref AItem: TOperationResume { TAccountsOperationsDataSource } +constructor TAccountsOperationsDataSource.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + FConfigLock := UThread.TReadWriteLock.Create('DataSource.ConfigLock',UThread.lrpSupportsRecursion); + FLastWalletAccountsGeneration := -1; +end; + +destructor TAccountsOperationsDataSource.Destroy; +begin + FConfigLock.Free; + inherited Destroy; +end; + +function TAccountsOperationsDataSource.GetAccounts: TArray; +begin + FConfigLock.AcquireReadLock; + try + if FUseWalletAccounts then + Result := TArrayTool.Copy(FWalletAccounts) + else + Result := TArrayTool.Copy(FAccounts); + finally + FConfigLock.ReleaseReadLock; + end; +end; + +procedure TAccountsOperationsDataSource.SetAccounts(const AAccounts: TArray); +type + TAccountNumbers = TArray; +begin + FConfigLock.AcquireWriteLock; + try + if TEqualityComparer.Default.Equals(FAccounts, AAccounts) then exit; + FAccounts := TArrayTool.Copy(AAccounts); + Inc(FConfigRevision); + // A different middle account must not collide with the old first/last key. + FLastFetchID := ''; + InvalidateSnapshot; + finally + FConfigLock.ReleaseWriteLock; + end; +end; + +function TAccountsOperationsDataSource.GetBlockDepth: Cardinal; +begin + FConfigLock.AcquireReadLock; + try + Result := FBlockDepth; + finally + FConfigLock.ReleaseReadLock; + end; +end; + +procedure TAccountsOperationsDataSource.SetBlockDepth(ABlockDepth: Cardinal); +begin + FConfigLock.AcquireWriteLock; + try + if FBlockDepth = ABlockDepth then exit; + FBlockDepth := ABlockDepth; + Inc(FConfigRevision); + FLastFetchID := ''; + InvalidateSnapshot; + finally + FConfigLock.ReleaseWriteLock; + end; +end; + +function TAccountsOperationsDataSource.GetUseWalletAccounts: Boolean; +begin + FConfigLock.AcquireReadLock; + try + Result := FUseWalletAccounts; + finally + FConfigLock.ReleaseReadLock; + end; +end; + +procedure TAccountsOperationsDataSource.SetUseWalletAccounts(AUseWalletAccounts: Boolean); +begin + FConfigLock.AcquireWriteLock; + try + if FUseWalletAccounts = AUseWalletAccounts then Exit; + FUseWalletAccounts := AUseWalletAccounts; + Inc(FConfigRevision); + FLastFetchID := ''; + InvalidateSnapshot; + finally + FConfigLock.ReleaseWriteLock; + end; +end; + +procedure TAccountsOperationsDataSource.InvalidateWalletAccounts; +begin + InterlockedIncrement64(FWalletAccountsGeneration); + InvalidateSnapshot; +end; + +function TAccountsOperationsDataSource.FetchWalletAccounts: TArray; +begin + Result := TCoreTool.GetUserAccountNumbers; +end; + +procedure TAccountsOperationsDataSource.RefreshWalletAccounts; +type + TAccountNumbers = TArray; +var + LConfigRevision: UInt64; + LGeneration: Int64; + LAccounts: TArray; +begin + FConfigLock.AcquireReadLock; + try + if not FUseWalletAccounts then Exit; + LGeneration := InterlockedCompareExchange64(FWalletAccountsGeneration, 0, 0); + if FLastWalletAccountsGeneration = LGeneration then Exit; + LConfigRevision := FConfigRevision; + finally + FConfigLock.ReleaseReadLock; + end; + // Enumeration can visit hundreds of thousands of accounts. Keep it on the + // fetch worker and outside the short settings lock used by UI callbacks. + LAccounts := FetchWalletAccounts; + CheckWalletQueryCancelled; + FConfigLock.AcquireWriteLock; + try + if (not FUseWalletAccounts) or (FConfigRevision <> LConfigRevision) or + (InterlockedCompareExchange64(FWalletAccountsGeneration, 0, 0) <> LGeneration) then Exit; + if not (TEqualityComparer.Default.Equals(FWalletAccounts, LAccounts)) then + FLastFetchID := ''; + FWalletAccounts := LAccounts; + FLastWalletAccountsGeneration := LGeneration; + finally + FConfigLock.ReleaseWriteLock; + end; +end; + procedure TAccountsOperationsDataSource.FetchAll(const AContainer: TList); var LNode: TNode; - LBlockDepth : Integer; - LFetchID : String; - LAccounts : TArray; + LAccounts: TArray; + LRequestedBlockDepth: Cardinal; + LConfigRevision: UInt64; + LUseWalletAccounts: Boolean; + LWalletGeneration: Int64; + LBlockDepth: Cardinal; + LFetchID, LCurrentFetchID, LCachedFetchID : String; + LStoredOperations : TArray; function ComputeFetchID : String; - var LAccFetchID : string; begin - // note: FAccounts is never empty - LAccFetchID := Length(FAccounts).ToString + FAccounts[Low(FAccounts)].ToString + FAccounts[High(FAccounts)].ToString; - Result := LAccFetchID + FBlockDepth.ToString + TCrypto.ToHexaString(LNode.Bank.SafeBox.SafeBoxHash); - + // Snapshot only the chain identity under the safebox lock. Pending-state + // reads and stored-history I/O must not retain this lock on a grid worker. + LNode.Bank.SafeBox.StartThreadSafe; + try + // Keep depths unsigned: Maximum uses High(Cardinal), which cannot pass + // through the signed ClipValue helper without becoming a negative depth. + LBlockDepth := LRequestedBlockDepth; + if LBlockDepth > LNode.Bank.BlocksCount then + LBlockDepth := LNode.Bank.BlocksCount; + Result := LRequestedBlockDepth.ToString + ':' + TCrypto.ToHexaString(LNode.Bank.SafeBox.SafeBoxHash); + finally + LNode.Bank.SafeBox.EndThreadSave; + end; end; begin - if Length(FAccounts) = 0 then + // FetchPage retains the base data-source lock throughout filtering and I/O. + // UI settings use a separate, short-held lock so a refresh cannot freeze them. + CheckWalletQueryCancelled; + RefreshWalletAccounts; + FConfigLock.AcquireReadLock; + try + LUseWalletAccounts := FUseWalletAccounts; + LWalletGeneration := FLastWalletAccountsGeneration; + if LUseWalletAccounts then + LAccounts := TArrayTool.Copy(FWalletAccounts) + else + LAccounts := TArrayTool.Copy(FAccounts); + LRequestedBlockDepth := FBlockDepth; + LConfigRevision := FConfigRevision; + LCachedFetchID := FLastFetchID; + LStoredOperations := FLastFetchBlockOperations; + finally + FConfigLock.ReleaseReadLock; + end; + if Length(LAccounts) = 0 then exit; LNode := TNode.Node; if not Assigned(LNode) then exit; - LBlockDepth := ClipValue(FBlockDepth, 0, LNode.Bank.BlocksCount); - LNode.Bank.SafeBox.StartThreadSafe; - try - AContainer.AddRange( LNode.GetPendingOperationsAffectingAccounts(FAccounts, 0, MaxInt) ); - // Performance: only fetch from storage if state/args are different from last call - LFetchID := ComputeFetchID; - if LFetchID <> FLastFetchID then begin - FLastFetchID := LFetchID; - FLastFetchBlockOperations := LNode.GetStoredOperationsAffectingAccounts(FAccounts, LBlockDepth, 0, MaxInt); + // The pending helper owns its mempool lock. Taking it while holding safebox + // would invert the core operation-application lock order. + AContainer.AddRange(LNode.GetPendingOperationsAffectingAccounts(LAccounts, 0, MaxInt)); + CheckWalletQueryCancelled; + LFetchID := ComputeFetchID; + if LFetchID <> LCachedFetchID then begin + LStoredOperations := LNode.GetStoredOperationsAffectingAccounts(LAccounts, LBlockDepth, 0, MaxInt); + CheckWalletQueryCancelled; + LCurrentFetchID := ComputeFetchID; + FConfigLock.AcquireWriteLock; + try + // Publish only successful results for this unchanged settings snapshot + // and chain. An older worker must not replace a newer selection's cache. + if (FConfigRevision = LConfigRevision) and + ((not LUseWalletAccounts) or + (InterlockedCompareExchange64(FWalletAccountsGeneration, 0, 0) = LWalletGeneration)) then begin + if LCurrentFetchID = LFetchID then begin + FLastFetchBlockOperations := LStoredOperations; + FLastFetchID := LFetchID; + end else FLastFetchID := ''; + end; + finally + FConfigLock.ReleaseWriteLock; end; - AContainer.AddRange( FLastFetchBlockOperations ); - finally - LNode.Bank.SafeBox.EndThreadSave; end; + AContainer.AddRange(LStoredOperations); + CheckWalletQueryCancelled; end; { TPendingOperationsDataSource } @@ -402,10 +1322,11 @@ procedure TPendingOperationsDataSource.FetchAll(const AContainer: TList); LOperationComp.bank := LNode.Bank; for LIdx := LEnd downto LStart do begin + CheckWalletQueryCancelled; LBlockChainData := CT_BlockSummary_NUL; LOperationBlock := LNode.Bank.SafeBox.GetBlock(LIdx).blockchainInfo; LBlockChainData.Block := LOperationBlock.block; diff --git a/src/gui-experimental/core.utils/UExplorerAccountSnapshots.pas b/src/gui-experimental/core.utils/UExplorerAccountSnapshots.pas new file mode 100644 index 000000000..cd0b94a4e --- /dev/null +++ b/src/gui-experimental/core.utils/UExplorerAccountSnapshots.pas @@ -0,0 +1,283 @@ +unit UExplorerAccountSnapshots; + +{ Copyright (c) 2026 Herman Schoenfeld + Distributed under the MIT software license; see the accompanying LICENSE. + + Bounded snapshots for the explorer's existing renderer. Painting only reads + the cache; node locks and disk reads belong to the owned worker. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +{ Supplies cached account rows and totals to the Accounts Explorer. Owned + background reads update a bounded cache so painting does not enter node + locks or storage. } + +{ Public types in this unit + + Type | Purpose + --------------------------+------------------------------------------------------------------------- + TExplorerAccountSnapshots | Caches explorer account rows and totals using owned background reads. +} + +interface + +uses Classes, SysUtils, ExtCtrls, Generics.Collections, UCommon, UCache, + UMemory, UNode, UPCDataTypes, UThread; + +type + TExplorerCachedAccount = record + Account: TAccount; + Generation: UInt64; + end; + + TExplorerAccountSnapshots = class; + + TExplorerAccountSnapshotThread = class(TPCThread) + private + FSource: TExplorerAccountSnapshots; + protected + procedure BCExecute; override; + public + Generation: UInt64; + Accounts: TArray; + Rows: TArray; + AccountCount, BlockCount: Integer; + Balance: Int64; + Succeeded: Boolean; + ErrorMessage: String; + constructor Create(ASource: TExplorerAccountSnapshots; AGeneration: UInt64; const AAccounts: TArray); + end; + + TExplorerAccountSnapshots = class(TComponent) + private type + TAccountCache = TActionCache; + private + FNode: TNode; + FCache: TAccountCache; + FPending: TList; + FWorker: TExplorerAccountSnapshotThread; + FTimer: TTimer; + FGeneration: UInt64; + FForceRefresh, FHasSummary, FStopping: Boolean; + FAccountCount, FBlockCount: Integer; + FBalance: Int64; + FErrorMessage: String; + FOnUpdated: TNotifyEvent; + FOnCompleted: TNotifyEvent; + procedure QueueAccount(AAccount: Cardinal); + function IsUpdating: Boolean; + procedure OnTimer(Sender: TObject); + function MissingAccount(const AAccount: Cardinal): TExplorerCachedAccount; + function AccountSize(const AValue: TExplorerCachedAccount): SizeInt; + protected + { Runs exclusively on the owned worker. Overrides must not touch controls. } + function ReadSnapshot(const AAccounts: TArray; out ARows: TArray; out AAccountCount, ABlockCount: Integer; out ABalance: Int64): Boolean; virtual; + public + constructor Create(AOwner: TComponent; ANode: TNode); reintroduce; + destructor Destroy; override; + procedure Invalidate; + procedure Stop; + function TryGetAccount(AAccountNumber: Integer; out AAccount: TAccount; out ABlockCount, AAccountCount: Integer): Boolean; + function TryGetSummary(out AAccountCount: Integer; out ABalance: Int64): Boolean; + property Updating: Boolean read IsUpdating; + property ErrorMessage: String read FErrorMessage; + property OnUpdated: TNotifyEvent read FOnUpdated write FOnUpdated; + // Every retired read, including stale/error results; no new data is implied. + property OnCompleted: TNotifyEvent read FOnCompleted write FOnCompleted; + end; + +implementation + +uses UAccounts, UBlockChain; + +constructor TExplorerAccountSnapshotThread.Create(ASource: TExplorerAccountSnapshots; AGeneration: UInt64; const AAccounts: TArray); +begin + inherited Create(True); + FreeOnTerminate := False; + FSource := ASource; + Generation := AGeneration; + Accounts := Copy(AAccounts); +end; + +procedure TExplorerAccountSnapshotThread.BCExecute; +begin + try + Succeeded := FSource.ReadSnapshot(Accounts, Rows, AccountCount, BlockCount, Balance); + except + on E: EAbort do Succeeded := False; + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; +end; + +constructor TExplorerAccountSnapshots.Create(AOwner: TComponent; ANode: TNode); +begin + inherited Create(AOwner); + FNode := ANode; + FGeneration := 1; + FForceRefresh := True; + FPending := TList.Create; + FCache := TAccountCache.Create(Self, + TCacheValueFetcher.FromObjectFunction(MissingAccount), + TCacheSizeEstimator.FromObjectFunction(AccountSize), idpNone, + TCacheReapPolicy.crpOldest, + TCacheExpirationPolicy.epNone, + TCacheNullValuePolicy.nvpCacheNormally, 512, TTimeSpan.MaxValue); + FTimer := TTimer.Create(Self); + FTimer.Enabled := False; + FTimer.Interval := 100; + FTimer.OnTimer := OnTimer; + FTimer.Enabled := True; +end; + +destructor TExplorerAccountSnapshots.Destroy; +begin + Stop; + FPending.Free; + inherited; +end; + +function TExplorerAccountSnapshots.AccountSize(const AValue: TExplorerCachedAccount): SizeInt; +begin + Result := 1; +end; + +function TExplorerAccountSnapshots.MissingAccount(const AAccount: Cardinal): TExplorerCachedAccount; +begin + // Only completed workers publish records. Painting never fetches a cache miss. + raise EInvalidOperation.Create('Account snapshot is not cached'); +end; + +procedure TExplorerAccountSnapshots.Stop; +begin + FStopping := True; + FOnUpdated := nil; + if Assigned(FTimer) then FTimer.Enabled := False; + if Assigned(FWorker) then begin + FWorker.Terminate; + FWorker.WaitFor; + FreeAndNil(FWorker); + end; +end; + +procedure TExplorerAccountSnapshots.Invalidate; +begin + if FStopping then Exit; + Inc(FGeneration); + FForceRefresh := True; + FErrorMessage := ''; + if Assigned(FWorker) then FWorker.Terminate; +end; + +function TExplorerAccountSnapshots.IsUpdating: Boolean; +begin + Result := not FStopping and (FForceRefresh or Assigned(FWorker) or (FPending.Count > 0)); +end; + +procedure TExplorerAccountSnapshots.QueueAccount(AAccount: Cardinal); +var I: Integer; +begin + if FStopping or (FPending.IndexOf(AAccount) >= 0) then Exit; + if Assigned(FWorker) and (FWorker.Generation = FGeneration) then + for I := 0 to High(FWorker.Accounts) do + if FWorker.Accounts[I] = AAccount then Exit; + if FPending.Count < 128 then FPending.Add(AAccount); +end; + +function TExplorerAccountSnapshots.TryGetAccount(AAccountNumber: Integer; out AAccount: TAccount; out ABlockCount, AAccountCount: Integer): Boolean; +var LValue: TExplorerCachedAccount; +begin + ABlockCount := FBlockCount; + AAccountCount := FAccountCount; + Result := False; + if FStopping or (AAccountNumber < 0) then Exit; + if FCache.ContainsCachedItem(Cardinal(AAccountNumber)) then begin + LValue := FCache[Cardinal(AAccountNumber)]; + AAccount := LValue.Account; + Result := True; + if LValue.Generation = FGeneration then Exit; + end; + QueueAccount(Cardinal(AAccountNumber)); +end; + +function TExplorerAccountSnapshots.TryGetSummary(out AAccountCount: Integer; out ABalance: Int64): Boolean; +begin + AAccountCount := FAccountCount; + ABalance := FBalance; + Result := FHasSummary; +end; + +function TExplorerAccountSnapshots.ReadSnapshot(const AAccounts: TArray; out ARows: TArray; out AAccountCount, ABlockCount: Integer; out ABalance: Int64): Boolean; +var I, LCount: Integer; LMempool: TPCOperationsComp; +begin + Result := False; + if not Assigned(FNode) then Exit; + // Snapshot readers share the mempool; only writers exclude this bounded copy. + while not FNode.TryLockMempoolRead(0, LMempool) do begin + if TThread.CheckTerminated then Exit; + Sleep(10); + end; + try + if TThread.CheckTerminated then Exit; + FNode.Bank.SafeBox.StartThreadSafe; + try + AAccountCount := FNode.Bank.AccountsCount; + ABlockCount := FNode.Bank.BlocksCount; + ABalance := FNode.Bank.SafeBox.TotalBalance; + SetLength(ARows, Length(AAccounts)); + LCount := 0; + for I := 0 to High(AAccounts) do begin + if TThread.CheckTerminated then Exit; + if AAccounts[I] >= Cardinal(AAccountCount) then Continue; + ARows[LCount] := LMempool.SafeBoxTransaction.Account(AAccounts[I]); + Inc(LCount); + end; + SetLength(ARows, LCount); + Result := True; + finally + FNode.Bank.SafeBox.EndThreadSave; + end; + finally + FNode.UnlockMempoolRead; + end; +end; + +procedure TExplorerAccountSnapshots.OnTimer(Sender: TObject); +var + LWorker: TExplorerAccountSnapshotThread; + LValue: TExplorerCachedAccount; + I: Integer; +begin + if FStopping then Exit; + if Assigned(FWorker) and FWorker.Finished then begin + LWorker := FWorker; + FWorker := nil; + try + LWorker.WaitFor; + if (LWorker.Generation = FGeneration) and LWorker.Succeeded then begin + FAccountCount := LWorker.AccountCount; + FBlockCount := LWorker.BlockCount; + FBalance := LWorker.Balance; + FHasSummary := True; + for I := 0 to High(LWorker.Rows) do begin + LValue.Account := LWorker.Rows[I]; + LValue.Generation := FGeneration; + FCache[LValue.Account.account] := LValue; + end; + if Assigned(FOnUpdated) then FOnUpdated(Self); + end else if (LWorker.Generation = FGeneration) and (LWorker.ErrorMessage <> '') then + FErrorMessage := LWorker.ErrorMessage; + finally + LWorker.Free; + end; + // Readiness consumes completion even after an error. Keep data publication + // separate so a failed read cannot create a repaint/request retry loop. + if Assigned(FOnCompleted) then FOnCompleted(Self); + end; + if FStopping or Assigned(FWorker) or not (FForceRefresh or (FPending.Count > 0)) then Exit; + FWorker := TExplorerAccountSnapshotThread.Create(Self, FGeneration, FPending.ToArray); + FPending.Clear; + FForceRefresh := False; + FWorker.Start; +end; + +end. diff --git a/src/gui-experimental/wizards/operations/UWIZBuyAccount.pas b/src/gui-experimental/wizards/operations/UWIZBuyAccount.pas index 45657fcbc..74790061c 100644 --- a/src/gui-experimental/wizards/operations/UWIZBuyAccount.pas +++ b/src/gui-experimental/wizards/operations/UWIZBuyAccount.pas @@ -17,7 +17,20 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Coordinates account purchase details, optional fee and payload steps, + confirmation and execution. +} + +{ Public types in this unit + + Type | Purpose + ---------------------+-------------------------------------------------------------------------- + TWIZBuyAccountWizard | Runs the account purchase workflow and submits the confirmed purchase. +} interface @@ -33,8 +46,8 @@ TWIZBuyAccountWizard = class(TWizard) constructor Create(AOwner: TComponent); override; function DetermineHasNext: boolean; override; function DetermineHasPrevious: boolean; override; - function FinishRequested(out message: ansistring): boolean; override; - function CancelRequested(out message: ansistring): boolean; override; + function FinishRequested(out message: String): boolean; override; + function CancelRequested(out message: String): boolean; override; end; implementation @@ -70,16 +83,17 @@ function TWIZBuyAccountWizard.DetermineHasPrevious: boolean; Result := inherited DetermineHasPrevious; end; -function TWIZBuyAccountWizard.FinishRequested(out message: ansistring): boolean; +function TWIZBuyAccountWizard.FinishRequested(out message: String): boolean; begin // Execute Buy Account here Result := TWIZOperationsHelper.ExecuteBuyAccount(Model.Account.SelectedAccounts, Model.BuyAccount.AccountToBuy, Model.Fee.SingleOperationFee, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, Model.BuyAccount.Amount, Model.BuyAccount.NewOwnerPublicKey, message); - if TWIZOperationsModel.RelockOnFinish then - TWallet.Keys.LockWallet; + if Result then + Model.RelockWallet; end; -function TWIZBuyAccountWizard.CancelRequested(out message: ansistring): boolean; +function TWIZBuyAccountWizard.CancelRequested(out message: String): boolean; begin + Model.RelockWallet; Result := True; end; diff --git a/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.dfm b/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.dfm new file mode 100644 index 000000000..9f7c350a1 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.dfm @@ -0,0 +1,940 @@ +object WIZBuyAccount_Details: TWIZBuyAccount_Details + Left = 0 + Height = 561 + Top = 0 + Width = 450 + ActiveControl = txtAccountToBuy + Anchors = [akTop, akLeft, akBottom] + Caption = 'WIZBuyAccount_Details' + ClientHeight = 561 + ClientWidth = 450 + object gbAccountToBuy: TGroupBox + Left = 8 + Height = 124 + Top = 8 + Width = 431 + Anchors = [akTop, akLeft, akRight] + Caption = 'Account to Buy' + TabOrder = 0 + object txtAccountToBuy: TEdit + Left = 16 + Height = 56 + Top = 32 + Width = 328 + Alignment = taRightJustify + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Font.Height = -43 + Font.Style = [fsBold] + OnChange = txtAccountToBuyChange + ParentFont = False + TabOrder = 0 + end + object btnSearch: TSpeedButton + Left = 354 + Height = 56 + Top = 32 + Width = 56 + Anchors = [akTop, akRight] + Glyph.Data = { + 36310000424D3631000000000000360000002800000038000000380000000100 + 2000000000000031000064000000640000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002395F9572293F6CA2293F6E62294F6BA2094 + F537000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F8732294F6FE2294F7FF2294F7FF2294F7FF2294 + F7F62094F5370000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002093F6742294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F6BA0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002093F6742294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7E50000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002094 + F6752294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F6CA0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002292F6762294 + F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F9590000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002293F6772294F6FE2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F67A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000007FFF042192F5362094F6752293F7A22294F6BF2194F6D22193F6DB2294 + F7D02193F6BE2194F7A02193F6722396FA3300AAFF0300000000000000000000 + 0000000000000000000000000000000000002292F6782294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F6790000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002096F8272193 + F7912293F6E82294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7E52294F58D2491F7230000 + 00000000000000000000000000002193F6792294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F679000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002394F7242294F6AA2293F6FE2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6FC2193 + F69F1D93FE1A000000002193F6792294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F67900000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000003F7FFF042094F67C2194F6F82294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2194F6F12193F7A62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2193F6790000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002491F2152293F7C02294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2192F67A000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002094F61F2194F6DA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F67A00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002190 + F3172194F6DC2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2194F67A0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002A7FFF062194 + F7C42294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F6FE2193F7CC2194F7812193F5532391F63A1F91F4312294 + F63C2395F6572193F5892194F6D52294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F67A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002294F7862294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F6FE2294F6AC2195F92E00000000000000000000000000000000000000000000 + 000000000000000000000000FF012292F63B2193F6BD2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F7A60000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002195F32E2193F6FA2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6ED2195 + F84D000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002192F7632294F6F62294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F6F11D93 + F51A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002193F6B72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F7E12193F8260000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002094F5372293 + F7EE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F69F000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002192F5362294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7EA2394F724000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002094 + F5372294F6F62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293 + F6FC2491F0230000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002293F7A32294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F6FE2093F44700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002192F7632294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F78C0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000178BE70B2294F6F42294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F7A00000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002193F6BD2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7E500AAFF0300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002093F54E2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2293F6FC1F97F7200000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002292F63B2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2396F53300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F58A2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F7B6000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000012194F6D52294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2193F67200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F6BC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2393F56D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F5892294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F7A000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002194F6DC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1F91F538000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002392F6572294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6BD00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F6F02294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2491F51C000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002292FA3B2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F6D000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000FFFF012194F6FB2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1E96FF11000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000001F94F9302294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2193F6DB00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000FF012193F6F22294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1D93F51A000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002393F6392294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6D200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F6DF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2293F534000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F5532294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6BF00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F7C12294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2094F765000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F7812294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2293F7A200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002194F7902294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2193F7AD000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F6CC2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2094F67500000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F6552294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F6F82190F3170000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002195F92E2294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2192F53600000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000001E96F0112193F6F92294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F68F0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002294F6AC2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F6E8007FFF0400000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002193F6AF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2193F6FB2293FA3400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002195F84D2294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2193F7910000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F7432294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F6DE2296F316000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002193 + F8262293F6ED2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293 + F6FE2096F8270000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002193F6C72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6D12296F3160000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002290F1252194 + F7E12294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7AA000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002294F63C2294F6FD2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F6DE2293 + FA34000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002093F4472193F6EB2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F6F82394 + F724000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002193F69A2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F7FA2194F68F2190F31700000000000000000000000000000000000000000000 + 00000000000000000000000000001F97F7202194F7A02294F6FE2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F67C0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002789EB0D2193 + F6D62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6F82193F7AD2092F7652396FA331D93F51A1E96F0112491 + F51C1F91F5382393F56D2194F7B62293F6FC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F7C0007FFF040000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002394 + F7242194F7EA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7DA2491F215000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002092F42F2194F7EA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F6DC2094F61F00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002394F7242193F6D62294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F7C42190F3170000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002789EB0D2193F69A2294F6FD2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2193F6FA2294F7862A7FFF06000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F63C2193F6C72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F6B72195F32E0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002294F7432193 + F6AF2193F6F92294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6F42293F7A22195FA350000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00001F9FFF102193F6552194F7902293F7C12293F6DF2193F6F22194F6FB2293 + F6F02194F6DC2193F6BC2193F58A2195F84D178BE70B00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000FF0100FFFF010000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000 + } + OnClick = btnSearchClick + end + object lblAccountToBuyNotice: TLabel + Left = 16 + Height = 15 + Top = 8 + Width = 166 + Caption = 'Please enter the account to buy' + ParentColor = False + ParentFont = False + end + object lblAccountToBuyDetails: TLabel + Left = 320 + Height = 15 + Top = 8 + Width = 87 + Anchors = [akTop, akRight] + Caption = 'Account Details' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + end + object gpAmount: TGroupBox + Left = 8 + Height = 122 + Top = 144 + Width = 431 + Anchors = [akTop, akLeft, akRight] + Caption = 'Amount' + ParentFont = False + TabOrder = 1 + object txtAmount: TEdit + Left = 16 + Height = 56 + Top = 32 + Width = 394 + Alignment = taRightJustify + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Font.Height = -43 + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + end + object lblAmountNotice: TLabel + Left = 16 + Height = 15 + Top = 11 + Width = 259 + Caption = 'Excessive amount will remain on bought account' + ParentColor = False + end + end + object gpOptions: TGroupBox + Left = 8 + Height = 122 + Top = 416 + Width = 431 + Anchors = [akTop, akLeft, akRight] + Caption = 'Options' + TabOrder = 2 + object chkCustomFee: TCheckBox + Left = 16 + Height = 19 + Top = 8 + Width = 164 + Caption = 'Allow me to choose the fee' + TabOrder = 0 + end + object chkPayload: TCheckBox + Left = 16 + Height = 19 + Top = 40 + Width = 158 + Caption = 'Attach a message payload' + TabOrder = 1 + end + end + object gbNewPrivateKey: TGroupBox + Left = 8 + Height = 122 + Top = 280 + Width = 431 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'New Private Key' + TabOrder = 3 + object btnGetNewKey: TSpeedButton + Left = 354 + Height = 56 + Top = 22 + Width = 56 + Anchors = [akTop, akRight, akBottom] + Glyph.Data = { + 36310000424D3631000000000000360000002800000038000000380000000100 + 2000000000000031000064000000640000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002395F9572293F6CA2293F6E62294F6BA2094 + F537000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F8732294F6FE2294F7FF2294F7FF2294F7FF2294 + F7F62094F5370000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002093F6742294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F6BA0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002093F6742294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7E50000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002094 + F6752294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F6CA0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002292F6762294 + F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F9590000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002293F6772294F6FE2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F67A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000007FFF042192F5362094F6752293F7A22294F6BF2194F6D22193F6DB2294 + F7D02193F6BE2194F7A02193F6722396FA3300AAFF0300000000000000000000 + 0000000000000000000000000000000000002292F6782294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F6790000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002096F8272193 + F7912293F6E82294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7E52294F58D2491F7230000 + 00000000000000000000000000002193F6792294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F679000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002394F7242294F6AA2293F6FE2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6FC2193 + F69F1D93FE1A000000002193F6792294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F67900000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000003F7FFF042094F67C2194F6F82294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2194F6F12193F7A62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2193F6790000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002491F2152293F7C02294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2192F67A000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002094F61F2194F6DA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F67A00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002190 + F3172194F6DC2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2194F67A0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002A7FFF062194 + F7C42294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F6FE2193F7CC2194F7812193F5532391F63A1F91F4312294 + F63C2395F6572193F5892194F6D52294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F67A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002294F7862294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F6FE2294F6AC2195F92E00000000000000000000000000000000000000000000 + 000000000000000000000000FF012292F63B2193F6BD2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F7A60000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002195F32E2193F6FA2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6ED2195 + F84D000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002192F7632294F6F62294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F6F11D93 + F51A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002193F6B72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F7E12193F8260000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002094F5372293 + F7EE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F69F000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002192F5362294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7EA2394F724000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002094 + F5372294F6F62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293 + F6FC2491F0230000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002293F7A32294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F6FE2093F44700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002192F7632294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F78C0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000178BE70B2294F6F42294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F7A00000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002193F6BD2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7E500AAFF0300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002093F54E2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2293F6FC1F97F7200000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002292F63B2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2396F53300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F58A2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F7B6000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000012194F6D52294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2193F67200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F6BC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2393F56D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F5892294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F7A000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002194F6DC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1F91F538000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002392F6572294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6BD00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F6F02294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2491F51C000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002292FA3B2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F6D000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000FFFF012194F6FB2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1E96FF11000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000001F94F9302294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2193F6DB00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000FF012193F6F22294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1D93F51A000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002393F6392294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6D200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F6DF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2293F534000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F5532294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6BF00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F7C12294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2094F765000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F7812294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2293F7A200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002194F7902294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2193F7AD000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F6CC2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2094F67500000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F6552294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F6F82190F3170000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002195F92E2294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2192F53600000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000001E96F0112193F6F92294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F68F0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002294F6AC2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F6E8007FFF0400000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002193F6AF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2193F6FB2293FA3400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002195F84D2294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2193F7910000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F7432294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F6DE2296F316000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002193 + F8262293F6ED2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293 + F6FE2096F8270000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002193F6C72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6D12296F3160000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002290F1252194 + F7E12294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7AA000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002294F63C2294F6FD2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F6DE2293 + FA34000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002093F4472193F6EB2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F6F82394 + F724000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002193F69A2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F7FA2194F68F2190F31700000000000000000000000000000000000000000000 + 00000000000000000000000000001F97F7202194F7A02294F6FE2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F67C0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002789EB0D2193 + F6D62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6F82193F7AD2092F7652396FA331D93F51A1E96F0112491 + F51C1F91F5382393F56D2194F7B62293F6FC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F7C0007FFF040000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002394 + F7242194F7EA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7DA2491F215000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002092F42F2194F7EA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F6DC2094F61F00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002394F7242193F6D62294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F7C42190F3170000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002789EB0D2193F69A2294F6FD2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2193F6FA2294F7862A7FFF06000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F63C2193F6C72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F6B72195F32E0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002294F7432193 + F6AF2193F6F92294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6F42293F7A22195FA350000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00001F9FFF102193F6552194F7902293F7C12293F6DF2193F6F22194F6FB2293 + F6F02194F6DC2193F6BC2193F58A2195F84D178BE70B00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000FF0100FFFF010000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000 + } + OnClick = btnGetNewKeyClick + end + object cbPrivateKeys: TComboBox + OnChange = cbPrivateKeysChange + Left = 16 + Height = 23 + Top = 22 + Width = 328 + Anchors = [akTop, akLeft, akRight, akBottom] + Font.Height = -35 + ItemHeight = 47 + ParentFont = False + TabOrder = 0 + Text = 'Select Private Key' + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.lfm b/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.lfm index 51638d2a3..e4eb8c0da 100644 --- a/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.lfm +++ b/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.lfm @@ -934,6 +934,7 @@ object WIZBuyAccount_Details: TWIZBuyAccount_Details OnClick = btnGetNewKeyClick end object cbPrivateKeys: TComboBox + OnChange = cbPrivateKeysChange Left = 16 Height = 23 Top = 22 diff --git a/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.pas b/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.pas index 9558cba7b..9c35e60e7 100644 --- a/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.pas +++ b/src/gui-experimental/wizards/operations/UWIZBuyAccount_Details.pas @@ -17,8 +17,25 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Collects the account to buy and its new owner key, with optional fee and + payload settings. +} + +{ Public types in this unit + + Type | Purpose + -----------------------+-------------------------------------------------------------------------- + TWIZBuyAccount_Details | Validates purchase details and inserts optional fee and payload steps. +} interface @@ -47,21 +64,27 @@ TWIZBuyAccount_Details = class(TWizardForm) procedure btnGetNewKeyClick(Sender: TObject); procedure btnSearchClick(Sender: TObject); procedure txtAccountToBuyChange(Sender: TObject); + procedure cbPrivateKeysChange(Sender: TObject); procedure UpdateUI(); private + FUpdatingWalletKeys: Boolean; procedure UpdateWalletKeys(); public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UNode, @@ -94,10 +117,6 @@ procedure TWIZBuyAccount_Details.OnPresent; UpdateUI(); UpdateWalletKeys(); - if TWallet.Keys.Count = 1 then - cbPrivateKeys.ItemIndex := 1 - else - cbPrivateKeys.ItemIndex := Model.BuyAccount.SelectedIndex; cbPrivateKeys.SetFocus; if Model.Account.Count > 1 then @@ -134,6 +153,7 @@ procedure TWIZBuyAccount_Details.UpdateWalletKeys(); LWalletKey: TWalletKey; LBuilder: string; begin + FUpdatingWalletKeys := True; cbPrivateKeys.items.BeginUpdate; try cbPrivateKeys.Items.Clear; @@ -148,8 +168,31 @@ procedure TWIZBuyAccount_Details.UpdateWalletKeys(); LBuilder := LBuilder + '(*)'; cbPrivateKeys.Items.AddObject(LBuilder, TObject(LIdx)); end; + cbPrivateKeys.ItemIndex := 0; + for LIdx := 0 to TWallet.Keys.Count - 1 do + if TAccountComp.EqualAccountKeys(TWallet.Keys.Key[LIdx].AccountKey, Model.BuyAccount.NewOwnerPublicKey) then + cbPrivateKeys.ItemIndex := LIdx + 1; + if (cbPrivateKeys.ItemIndex = 0) and (TWallet.Keys.Count = 1) then + cbPrivateKeys.ItemIndex := 1; finally cbPrivateKeys.Items.EndUpdate; + FUpdatingWalletKeys := False; + end; + cbPrivateKeysChange(Self); +end; + +procedure TWIZBuyAccount_Details.cbPrivateKeysChange(Sender: TObject); +var + LIdx: Integer; +begin + if FUpdatingWalletKeys or not Assigned(Model) then + Exit; + Model.BuyAccount.SelectedIndex := cbPrivateKeys.ItemIndex; + if cbPrivateKeys.ItemIndex > 0 then + begin + LIdx := PtrInt(cbPrivateKeys.Items.Objects[cbPrivateKeys.ItemIndex]); + if (LIdx >= 0) and (LIdx < TWallet.Keys.Count) then + Model.BuyAccount.NewOwnerPublicKey := TWallet.Keys.Key[LIdx].AccountKey; end; end; @@ -183,11 +226,11 @@ procedure TWIZBuyAccount_Details.OnNext; Model.BuyAccount.SelectedIndex := cbPrivateKeys.ItemIndex; end; -function TWIZBuyAccount_Details.Validate(out message: ansistring): boolean; +function TWIZBuyAccount_Details.Validate(out message: String): boolean; var LAccountNumbersWithChecksum: TArray; LAccountNumber: cardinal; - LAmount: int64; + LAmount, LFee: int64; LIdx: integer; LAccountToBuy: TAccount; begin @@ -219,13 +262,31 @@ function TWIZBuyAccount_Details.Validate(out message: ansistring): boolean; Exit(False); end; - if (LAccountToBuy.accountInfo.price > LAmount) then + if LAmount < 1 then + begin + message := 'The purchase amount must be greater than zero.'; + Exit(False); + end; + + // Existing funds in the sale account contribute to the purchase price. + if (LAccountToBuy.balance < LAccountToBuy.accountInfo.price) and + (UInt64(LAmount) < LAccountToBuy.accountInfo.price - LAccountToBuy.balance) then begin message := Format('Account Price is "%s"', [TAccountComp.FormatMoney(LAccountToBuy.accountInfo.price)]); Exit(False); end; - if ((LAmount + TSettings.DefaultFee) > Model.Account.SelectedAccounts[0].balance) then + if chkCustomFee.Checked then + LFee := Model.Fee.SingleOperationFee + else + LFee := TSettings.DefaultFee; + if LFee < 0 then + begin + message := 'The operation fee cannot be negative.'; + Exit(False); + end; + if (UInt64(LAmount) > Model.Account.SelectedAccounts[0].balance) or + (UInt64(LFee) > Model.Account.SelectedAccounts[0].balance - UInt64(LAmount)) then begin message := Format('Insufficient Funds in Account "%s" to buy Account "%s"', [Model.Account.SelectedAccounts[0].AccountString, LAccountToBuy.AccountString]); Exit(False); diff --git a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo.pas b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo.pas index bd126b40e..2d2949397 100644 --- a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo.pas +++ b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo.pas @@ -17,7 +17,20 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Coordinates account name or type changes, approval of skipped targets, fees + and execution. +} + +{ Public types in this unit + + Type | Purpose + ----------------------------+-------------------------------------------------------------------------- + TWIZChangeAccountInfoWizard | Runs account-information updates for the approved target accounts. +} interface @@ -33,8 +46,8 @@ TWIZChangeAccountInfoWizard = class(TWizard) constructor Create(AOwner: TComponent); override; function DetermineHasNext: boolean; override; function DetermineHasPrevious: boolean; override; - function FinishRequested(out message: ansistring): boolean; override; - function CancelRequested(out message: ansistring): boolean; override; + function FinishRequested(out message: String): boolean; override; + function CancelRequested(out message: String): boolean; override; end; implementation @@ -42,6 +55,7 @@ implementation uses UWallet, UCommon, + UCoreObjects, UAccounts, UPCDataTypes, UCoreUtils, @@ -72,27 +86,44 @@ function TWIZChangeAccountInfoWizard.DetermineHasPrevious: boolean; Result := inherited DetermineHasPrevious; end; -function TWIZChangeAccountInfoWizard.FinishRequested(out message: ansistring): boolean; +function TWIZChangeAccountInfoWizard.FinishRequested(out message: String): boolean; var - LAccountArray: array of TAccount; + LAccountArray: TArray; begin - if Model.Account.Count = 1 then + if Length(Model.ChangeAccountInfo.TargetAccounts) = 0 then + begin + message := 'No accounts remain to change. Return to Details and choose another type.'; + Exit(False); + end; + if (Model.ChangeAccountInfo.SkippedCount > 0) and + not Model.ChangeAccountInfo.SkippedAccountsApproved then + begin + message := 'Approve the skipped accounts before continuing.'; + Exit(False); + end; + if Model.ChangeAccountInfo.UseSingleFeePayer then begin + if not Model.ChangeAccountInfo.FeePayerSelected then + begin + message := 'Select the account which will pay the fee.'; + Exit(False); + end; SetLength(LAccountArray, 1); - LAccountArray[0] := Model.Signer.SignerAccount; + LAccountArray[0] := Model.ChangeAccountInfo.FeePayerAccount; end else begin LAccountArray := Model.Account.SelectedAccounts; end; // Execute the Change Account Info here - Result := TWIZOperationsHelper.ExecuteChangeAccountInfo(Model.Account.SelectedAccounts, LAccountArray, Model.Fee.SingleOperationFee, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, Model.ChangeAccountInfo.NewName, Model.ChangeAccountInfo.NewType, message); - if TWIZOperationsModel.RelockOnFinish then - TWallet.Keys.LockWallet; + Result := TWIZOperationsHelper.ExecuteChangeAccountInfo(Model.Account.SelectedAccounts, LAccountArray, Model.Fee.SingleOperationFee, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, Model.ChangeAccountInfo.NewName, Model.ChangeAccountInfo.NewType, message, akaPrimary, Model.ChangeAccountInfo.TargetAccounts); + if Result then + Model.RelockWallet; end; -function TWIZChangeAccountInfoWizard.CancelRequested(out message: ansistring): boolean; +function TWIZChangeAccountInfoWizard.CancelRequested(out message: String): boolean; begin + Model.RelockWallet; Result := True; end; diff --git a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.dfm b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.dfm new file mode 100644 index 000000000..45da4d9d7 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.dfm @@ -0,0 +1,124 @@ +object WIZChangeAccountInfo_Details: TWIZChangeAccountInfo_Details + Left = 0 + Height = 379 + Top = 0 + Width = 450 + ActiveControl = txtName + Anchors = [akTop, akLeft, akBottom] + Caption = 'WIZChangeAccountInfo_Details' + ClientHeight = 379 + ClientWidth = 450 + OnResize = FormResize + object gbName: TGroupBox + Left = 8 + Height = 124 + Top = 8 + Width = 431 + Anchors = [akTop, akLeft, akRight] + Caption = 'Name' + TabOrder = 0 + object txtName: TEdit + Left = 16 + Height = 56 + Top = 32 + Width = 394 + Alignment = taRightJustify + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Font.Height = -43 + Font.Style = [fsBold] + OnChange = txtNameChange + ParentFont = False + TabOrder = 0 + end + object lblNameNotice: TLabel + Left = 16 + Height = 14 + Top = 8 + Width = 130 + Caption = 'Please enter new name' + ParentColor = False + ParentFont = False + end + object lblNameDetails: TLabel + Left = 329 + Height = 14 + Top = 8 + Width = 78 + Anchors = [akTop, akRight] + Caption = 'Name Details' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + end + object gpType: TGroupBox + Left = 8 + Height = 122 + Top = 144 + Width = 431 + Anchors = [akTop, akLeft, akRight] + Caption = 'Type' + ParentFont = False + TabOrder = 1 + object txtType: TEdit + Left = 16 + Height = 56 + Top = 32 + Width = 394 + Alignment = taRightJustify + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Font.Height = -43 + Font.Style = [fsBold] + NumbersOnly = True + OnChange = txtTypeChange + ParentFont = False + TabOrder = 0 + end + object lblTypeNotice: TLabel + Left = 16 + Height = 14 + Top = 11 + Width = 125 + Caption = 'Please enter new type' + ParentColor = False + end + object lblTypeDetails: TLabel + Left = 333 + Height = 14 + Top = 8 + Width = 74 + Anchors = [akTop, akRight] + Caption = 'Type Details' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + end + object gpOptions: TGroupBox + Left = 8 + Height = 89 + Top = 280 + Width = 431 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Options' + TabOrder = 2 + object chkCustomFee: TCheckBox + Left = 16 + Height = 18 + Top = 8 + Width = 174 + Caption = 'Allow me to choose the fee' + TabOrder = 0 + end + object chkPayload: TCheckBox + Left = 16 + Height = 18 + Top = 40 + Width = 163 + Caption = 'Attach a message payload' + TabOrder = 1 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.lfm b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.lfm index a8a6d65d1..9d9bb2d5c 100644 --- a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.lfm +++ b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.lfm @@ -8,6 +8,7 @@ object WIZChangeAccountInfo_Details: TWIZChangeAccountInfo_Details Caption = 'WIZChangeAccountInfo_Details' ClientHeight = 379 ClientWidth = 450 + OnResize = FormResize LCLVersion = '2.0.10.0' object gbName: TGroupBox Left = 8 diff --git a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.pas b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.pas index 3742ca36e..0658d7792 100644 --- a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.pas +++ b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Details.pas @@ -17,13 +17,30 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Collects a new account name or type and builds the approval, fee and payload + route. +} + +{ Public types in this unit + + Type | Purpose + ------------------------------+-------------------------------------------------------------------------- + TWIZChangeAccountInfo_Details | Validates requested changes and separates changed and skipped accounts. +} interface uses - Classes, SysUtils, Forms, Dialogs, Controls, StdCtrls, ExtCtrls, Buttons, UWizard, UWIZOperation; + Types, Math, Classes, SysUtils, Forms, Dialogs, Controls, StdCtrls, ExtCtrls, Buttons, UWizard, UWIZOperation; type @@ -44,19 +61,23 @@ TWIZChangeAccountInfo_Details = class(TWizardForm) procedure txtNameChange(Sender: TObject); procedure txtTypeChange(Sender: TObject); procedure UpdateUI(); - - - + procedure FormResize(Sender: TObject); + private + FPresented: Boolean; public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UNode, @@ -73,11 +94,56 @@ implementation UCommon.Collections, Generics.Collections, UWIZOperationFee_Custom, - UWIZOperationSigner_Select, - UWIZOperationPayload_Encryption; + UWIZOperationPayload_Encryption, + UWIZOperationConfirmation, + UWIZChangeAccountInfo_Skipped; { TWIZChangeAccountInfo_Details } +type + TCaptionGroupBoxAccess = class(TGroupBox); + +procedure TWIZChangeAccountInfo_Details.FormResize(Sender: TObject); + + procedure FitEditText(AEdit: TEdit); + var + LTextHeight, LBorderHeight: Integer; + begin + Canvas.Font.Assign(AEdit.Font); + LTextHeight := Canvas.TextHeight('Mg'); + LBorderHeight := Max(4, AEdit.Height - AEdit.ClientHeight); + // Preserve the large font while fitting its native line and edit borders. + AEdit.Height := Max(AEdit.Height, LTextHeight + LBorderHeight); + end; + + procedure PositionContents(AGroup: TGroupBox); + var + LContent: TRect; + LPadding, LTop, LDelta, I: Integer; + begin + LContent := AGroup.ClientRect; + TCaptionGroupBoxAccess(AGroup).AdjustClientRect(LContent); + Canvas.Font.Assign(AGroup.Font); + LPadding := Max(6, Canvas.TextHeight('0') * 2 div 3); + LTop := High(Integer); + for I := 0 to AGroup.ControlCount - 1 do + LTop := Min(LTop, AGroup.Controls[I].Top); + LDelta := LContent.Top + LPadding - LTop; + for I := 0 to AGroup.ControlCount - 1 do + AGroup.Controls[I].Top := AGroup.Controls[I].Top + LDelta; + end; + +begin + if not HandleAllocated or not Assigned(chkPayload) then Exit; + FitEditText(txtName); + FitEditText(txtType); + // VCL includes the caption in ClientRect; LCL already excludes it. Use each + // toolkit's native content inset, including after a font/DPI change. + PositionContents(gbName); + PositionContents(gpType); + PositionContents(gpOptions); +end; + procedure TWIZChangeAccountInfo_Details.txtNameChange(Sender: TObject); begin UpdateUI(); @@ -90,8 +156,19 @@ procedure TWIZChangeAccountInfo_Details.txtTypeChange(Sender: TObject); procedure TWIZChangeAccountInfo_Details.OnPresent; begin + FormResize(Self); + if not FPresented then + begin + if Model.Account.Count = 1 then + begin + txtName.Text := Model.Account.SelectedAccounts[0].name.ToPrintable; + txtType.Text := IntToStr(Model.Account.SelectedAccounts[0].account_type); + end + else + txtType.Text := IntToStr(Model.ChangeAccountInfo.NewType); + FPresented := True; + end; UpdateUI(); - txtType.Text := '0'; if Model.Account.Count > 1 then begin @@ -119,29 +196,34 @@ procedure TWIZChangeAccountInfo_Details.OnNext; begin LWizStepsToInject := LDisposables.AddObject(TList.Create) as TList; + // Rebuild the complete continuation after every Details validation. Going + // back and changing the type must not reuse an old skip list or its approval. + Model.ChangeAccountInfo.TargetAccounts := TWIZOperationsHelper.GetChangeAccountInfoTargets( + Model.Account.SelectedAccounts, Model.ChangeAccountInfo.NewType); + Model.ChangeAccountInfo.SkippedCount := Model.Account.Count - Length(Model.ChangeAccountInfo.TargetAccounts); + Model.ChangeAccountInfo.SkippedAccountsApproved := False; + if Model.ChangeAccountInfo.SkippedCount > 0 then + LWizStepsToInject.Add(TWIZChangeAccountInfo_Skipped); + // Fee Section Model.Fee.SingleOperationFee := TSettings.DefaultFee; if chkCustomFee.Checked then - LWizStepsToInject.Add(TWIZOperationFee_Custom); + LWizStepsToInject.Add(TWIZOperationFee_Custom) + else + Model.ChangeAccountInfo.UseSingleFeePayer := False; // Payload Section Model.Payload.HasPayload := IIF(chkPayload.Checked, True, False); if Model.Payload.HasPayload then LWizStepsToInject.Add(TWIZOperationPayload_Encryption); - // Signer section - if Model.Account.Count = 1 then - begin - LWizStepsToInject.Add(TWIZOperationSigner_Select); // special case for changeaccount info wizard - end; - - // Update wizard flow if applicable - if LWizStepsToInject.Count > 0 then - UpdatePath(ptInject, LWizStepsToInject.ToArray); + LWizStepsToInject.Add(TWIZOperationConfirmation); + UpdatePath(ptReplaceAllNext, LWizStepsToInject.ToArray); end; -function TWIZChangeAccountInfo_Details.Validate(out message: ansistring): boolean; +function TWIZChangeAccountInfo_Details.Validate(out message: String): boolean; var + LMessage: String; LAccNumberIndex, LIdx, LErrCode: integer; LCurrentAccount: TAccount; LNewName: TRawBytes; @@ -169,9 +251,9 @@ function TWIZChangeAccountInfo_Details.Validate(out message: ansistring): boolea begin if LNewName <> nil then begin - if (not TPCSafeBox.ValidAccountName(LNewName, message)) then + if (not TPascalCoinProtocol.IsValidAccountName(TNode.Node.Bank.SafeBox.CurrentProtocol, LNewName, LMessage)) then begin - message := Format('New name "%s" is not a valid name: %s ', [LNewName.ToPrintable, message]); + message := Format('New name "%s" is not a valid name: %s ', [LNewName.ToPrintable, LMessage]); Exit(False); end; LAccNumberIndex := (TNode.Node.Bank.SafeBox.FindAccountByName(LNewName)); diff --git a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.dfm b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.dfm new file mode 100644 index 000000000..206fabac6 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.dfm @@ -0,0 +1,53 @@ +object WIZChangeAccountInfo_Skipped: TWIZChangeAccountInfo_Skipped + Left = 0 + Top = 0 + Width = 450 + Height = 320 + Caption = 'Skipped accounts' + ClientWidth = 450 + ClientHeight = 320 + OnResize = FormResize + object gbSkipped: TGroupBox + Left = 8 + Top = 8 + Width = 434 + Height = 304 + Anchors = [akLeft, akTop, akRight, akBottom] + Caption = 'Review skipped accounts' + TabOrder = 0 + object lblSummary: TLabel + Left = 16 + Top = 32 + Width = 402 + Height = 48 + AutoSize = False + Font.Style = [fsBold] + ParentFont = False + WordWrap = True + end + object lblRemaining: TLabel + Left = 16 + Top = 88 + Width = 402 + Height = 32 + AutoSize = False + WordWrap = True + end + object lblFees: TLabel + Left = 16 + Top = 128 + Width = 402 + Height = 32 + AutoSize = False + WordWrap = True + end + object chkApprove: TCheckBox + Left = 16 + Top = 176 + Width = 402 + Height = 24 + Caption = 'I approve skipping these accounts.' + TabOrder = 0 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.lfm b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.lfm new file mode 100644 index 000000000..206fabac6 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.lfm @@ -0,0 +1,53 @@ +object WIZChangeAccountInfo_Skipped: TWIZChangeAccountInfo_Skipped + Left = 0 + Top = 0 + Width = 450 + Height = 320 + Caption = 'Skipped accounts' + ClientWidth = 450 + ClientHeight = 320 + OnResize = FormResize + object gbSkipped: TGroupBox + Left = 8 + Top = 8 + Width = 434 + Height = 304 + Anchors = [akLeft, akTop, akRight, akBottom] + Caption = 'Review skipped accounts' + TabOrder = 0 + object lblSummary: TLabel + Left = 16 + Top = 32 + Width = 402 + Height = 48 + AutoSize = False + Font.Style = [fsBold] + ParentFont = False + WordWrap = True + end + object lblRemaining: TLabel + Left = 16 + Top = 88 + Width = 402 + Height = 32 + AutoSize = False + WordWrap = True + end + object lblFees: TLabel + Left = 16 + Top = 128 + Width = 402 + Height = 32 + AutoSize = False + WordWrap = True + end + object chkApprove: TCheckBox + Left = 16 + Top = 176 + Width = 402 + Height = 24 + Caption = 'I approve skipping these accounts.' + TabOrder = 0 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.pas b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.pas new file mode 100644 index 000000000..c76c195b0 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZChangeAccountInfo_Skipped.pas @@ -0,0 +1,152 @@ +unit UWIZChangeAccountInfo_Skipped; + +{ Copyright (c) 2026 Herman Schoenfeld . + Distributed under the MIT software license; see LICENSE. + Explains unchanged batch targets and requires approval before continuing. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +{ Explains which accounts already have the requested type and requires + approval to skip them. +} + +{ Public types in this unit + + Type | Purpose + ------------------------------+-------------------------------------------------------------------------- + TWIZChangeAccountInfo_Skipped | Confirms skipped accounts before a batch type change continues. +} + +interface + +uses Classes, SysUtils, Types, Math, Forms, Controls, StdCtrls, UWizard, UWIZOperation; + +type + TWIZChangeAccountInfo_Skipped = class(TWizardForm) + gbSkipped: TGroupBox; + lblSummary: TLabel; + lblRemaining: TLabel; + lblFees: TLabel; + chkApprove: TCheckBox; + procedure FormResize(Sender: TObject); + public + procedure OnPresent; override; + procedure OnNext; override; + function Validate(out message: String): Boolean; override; + end; + +implementation + +uses Graphics, {$IFDEF FPC}LCLType, LCLIntf{$ELSE}Windows{$ENDIF}; + +{$IFDEF FPC} +{$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} + +type + TGroupBoxContentAccess = class(TGroupBox); + +procedure TWIZChangeAccountInfo_Skipped.FormResize(Sender: TObject); +var + LContent: TRect; + LGap, LTop, LWidth: Integer; + LMeasure: TControlCanvas; + + procedure PositionLabel(ALabel: TLabel; ALines: Integer); + var + LText: TRect; + LHeight: Integer; + begin + LMeasure.Control := ALabel; + LMeasure.Font.Assign(ALabel.Font); + LText := Rect(0, 0, LWidth, 0); + DrawText(LMeasure.Handle, PChar(ALabel.Caption), Length(ALabel.Caption), LText, + DT_CALCRECT or DT_WORDBREAK or DT_NOPREFIX); + LHeight := Max(LMeasure.TextHeight('Mg') * ALines, LText.Bottom - LText.Top); + ALabel.SetBounds(LContent.Left + LGap, LTop, LWidth, LHeight); + // Reserve the measured wrapped text, not a temporarily adjusted LCL bound. + Inc(LTop, LHeight + LGap); + end; + +begin + if not HandleAllocated or not Assigned(chkApprove) then Exit; + LMeasure := TControlCanvas.Create; + // Changing a wrapped LCL label's width can realign its siblings before its + // new anchor bounds are stored. Commit the complete layout as one update. + gbSkipped.DisableAlign; + try + LContent := gbSkipped.ClientRect; + TGroupBoxContentAccess(gbSkipped).AdjustClientRect(LContent); + LMeasure.Control := gbSkipped; + LMeasure.Font.Assign(gbSkipped.Font); + LGap := Max(8, LMeasure.TextHeight('Mg') * 2 div 3); + LTop := LContent.Top + LGap; + LWidth := Max(1, LContent.Right - LContent.Left - 2 * LGap); + // Measure each control's actual font and wrapping after native parenting. + PositionLabel(lblSummary, 3); + PositionLabel(lblRemaining, 2); + PositionLabel(lblFees, 2); + LMeasure.Control := chkApprove; + LMeasure.Font.Assign(chkApprove.Font); + chkApprove.SetBounds(LContent.Left + LGap, LTop, LWidth, + Max(chkApprove.Height, LMeasure.TextHeight('Mg') + 6)); + finally + try + gbSkipped.EnableAlign; + finally + LMeasure.Free; + end; + end; +end; + +procedure TWIZChangeAccountInfo_Skipped.OnPresent; +var + LCount: Integer; +begin + LCount := Model.ChangeAccountInfo.SkippedCount; + if LCount = 1 then + lblSummary.Caption := Format('1 selected account already has type %d and will be skipped.', + [Model.ChangeAccountInfo.NewType]) + else + lblSummary.Caption := Format('%d selected accounts already have type %d and will be skipped.', + [LCount, Model.ChangeAccountInfo.NewType]); + LCount := Length(Model.ChangeAccountInfo.TargetAccounts); + if LCount = 0 then + lblRemaining.Caption := 'No accounts remain to change. Go back and choose another type.' + else if LCount = 1 then + lblRemaining.Caption := '1 account will be changed.' + else + lblRemaining.Caption := Format('%d accounts will be changed.', [LCount]); + lblFees.Caption := 'Fees apply only to accounts being updated. No operation fee is charged for skipped changes.'; + // Returning to this decision requires a fresh explicit approval. + Model.ChangeAccountInfo.SkippedAccountsApproved := False; + chkApprove.Checked := False; + chkApprove.Enabled := LCount > 0; + FormResize(Self); +end; + +procedure TWIZChangeAccountInfo_Skipped.OnNext; +begin + Model.ChangeAccountInfo.SkippedAccountsApproved := chkApprove.Checked; +end; + +function TWIZChangeAccountInfo_Skipped.Validate(out message: String): Boolean; +begin + message := ''; + Result := False; + if Length(Model.ChangeAccountInfo.TargetAccounts) = 0 then + begin + message := 'No accounts remain to change. Go back and choose another type.'; + Exit; + end; + if not chkApprove.Checked then + begin + message := 'Approve skipping the unchanged accounts before continuing.'; + Exit; + end; + Result := True; +end; + +end. diff --git a/src/gui-experimental/wizards/operations/UWIZChangeKey.pas b/src/gui-experimental/wizards/operations/UWIZChangeKey.pas index 8c749ae6b..4efd72658 100644 --- a/src/gui-experimental/wizards/operations/UWIZChangeKey.pas +++ b/src/gui-experimental/wizards/operations/UWIZChangeKey.pas @@ -1,6 +1,8 @@ unit UWIZChangeKey; +{$IFDEF FPC} {$mode delphi} +{$ENDIF} { Copyright (c) 2018 Sphere 10 Software (http://www.sphere10.com/) @@ -12,10 +14,22 @@ Herman Schoenfeld - designer } +{ Coordinates account ownership changes to an imported public key or an + existing wallet key. +} + +{ Public types in this unit + + Type | Purpose + --------------------+-------------------------------------------------------------------------- + TWIZChangeKeyWizard | Runs key-change setup, signer selection, confirmation and execution. +} + interface uses - Classes, SysUtils, Forms, Dialogs, UCrypto, UCommon, UWizard, UAccounts, UPCDataTypes, UCoreObjects, UWIZOperation, LCLType; + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + Classes, SysUtils, Forms, Dialogs, UCrypto, UCommon, UWizard, UAccounts, UPCDataTypes, UCoreObjects, UWIZOperation{$IFDEF FPC}, LCLType{$ENDIF}; type @@ -26,8 +40,8 @@ TWIZChangeKeyWizard = class(TWizard) constructor Create(AOwner: TComponent); override; function DetermineHasNext: boolean; override; function DetermineHasPrevious: boolean; override; - function FinishRequested(out message: ansistring): boolean; override; - function CancelRequested(out message: ansistring): boolean; override; + function FinishRequested(out message: String): boolean; override; + function CancelRequested(out message: String): boolean; override; end; implementation @@ -64,7 +78,7 @@ function TWIZChangeKeyWizard.DetermineHasPrevious: boolean; Result := inherited DetermineHasPrevious; end; -function TWIZChangeKeyWizard.FinishRequested(out message: ansistring): boolean; +function TWIZChangeKeyWizard.FinishRequested(out message: String): boolean; var LPublicKey: TAccountKey; begin @@ -78,13 +92,14 @@ function TWIZChangeKeyWizard.FinishRequested(out message: ansistring): boolean; end; - Result := TWIZOperationsHelper.ExecuteChangeKey(Model.Account.SelectedAccounts, Model.Signer.SignerAccount, LPublicKey, Model.Fee.SingleOperationFee, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, message); - if TWIZOperationsModel.RelockOnFinish then - TWallet.Keys.LockWallet; + Result := TWIZOperationsHelper.ExecuteChangeKey(Model.Account.SelectedAccounts, Model.Signer.SignerAccount, LPublicKey, Model.Fee.SingleOperationFee, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, message, Model.Signer.OperationSigningMode); + if Result then + Model.RelockWallet; end; -function TWIZChangeKeyWizard.CancelRequested(out message: ansistring): boolean; +function TWIZChangeKeyWizard.CancelRequested(out message: String): boolean; begin + Model.RelockWallet; Result := True; end; diff --git a/src/gui-experimental/wizards/operations/UWIZChangeKey_EnterKey.dfm b/src/gui-experimental/wizards/operations/UWIZChangeKey_EnterKey.dfm new file mode 100644 index 000000000..ab9f1757f --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZChangeKey_EnterKey.dfm @@ -0,0 +1,54 @@ +object WIZChangeKey_EnterKey: TWIZChangeKey_EnterKey + Left = 0 + Height = 291 + Top = 0 + Width = 510 + ActiveControl = chkChooseFee + Caption = 'ChangeKey_EnterKey' + ClientHeight = 291 + ClientWidth = 510 + Visible = False + object gbNewPublicKey: TGroupBox + Left = 4 + Height = 282 + Top = 4 + Width = 501 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'New Public Key' + TabOrder = 0 + object lblPublicKeyNotice: TLabel + Left = 8 + Height = 15 + Top = 7 + Width = 199 + Caption = 'Enter the public key of the new owner' + ParentColor = False + end + object chkChooseFee: TCheckBox + Left = 8 + Height = 19 + Top = 160 + Width = 203 + Anchors = [akLeft, akBottom] + Caption = 'Use custom fee''s for this operation' + TabOrder = 0 + end + object chkAttachPayload: TCheckBox + Left = 8 + Height = 19 + Top = 192 + Width = 258 + Anchors = [akLeft, akBottom] + Caption = 'Attach a message/payload to this transaction' + TabOrder = 1 + end + object mmoNewPrivateKey: TMemo + Left = 8 + Height = 118 + Top = 32 + Width = 480 + Anchors = [akTop, akLeft, akRight, akBottom] + TabOrder = 2 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZChangeKey_EnterKey.pas b/src/gui-experimental/wizards/operations/UWIZChangeKey_EnterKey.pas index 360a6cd55..a20f7c3c3 100644 --- a/src/gui-experimental/wizards/operations/UWIZChangeKey_EnterKey.pas +++ b/src/gui-experimental/wizards/operations/UWIZChangeKey_EnterKey.pas @@ -17,8 +17,25 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Collects a new owner public key for account transfer and optional fee or + payload settings. +} + +{ Public types in this unit + + Type | Purpose + -----------------------+-------------------------------------------------------------------------- + TWIZChangeKey_EnterKey | Validates an imported owner key and adds the selected operation steps. +} interface @@ -39,13 +56,17 @@ TWIZChangeKey_EnterKey = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UAccounts, @@ -65,7 +86,6 @@ procedure TWIZChangeKey_EnterKey.OnPresent; chkChooseFee.Checked := True; chkChooseFee.Enabled := False; end; - mmoNewPrivateKey.Clear; mmoNewPrivateKey.SetFocus; end; @@ -89,7 +109,7 @@ procedure TWIZChangeKey_EnterKey.OnNext; end; -function TWIZChangeKey_EnterKey.Validate(out message: ansistring): boolean; +function TWIZChangeKey_EnterKey.Validate(out message: String): boolean; var LTempAccountKey: TAccountKey; LIdx: integer; diff --git a/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectKey.dfm b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectKey.dfm new file mode 100644 index 000000000..e470dc921 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectKey.dfm @@ -0,0 +1,69 @@ +object WIZChangeKey_SelectKey: TWIZChangeKey_SelectKey + Left = 0 + Height = 253 + Top = 0 + Width = 429 + ActiveControl = cbNewPrivateKey + Caption = 'WIZChangeKey_SelectKey' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + object gbNewPrivateKey: TGroupBox + Left = 4 + Height = 244 + Top = 4 + Width = 420 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'New Private Key' + TabOrder = 0 + object lblPrivateKeyNote: TLabel + Left = 8 + Height = 15 + Top = 8 + Width = 329 + Caption = 'Please select the new private key to update your accounts with' + ParentColor = False + end + object cbNewPrivateKey: TComboBox + Left = 8 + Height = 23 + Top = 40 + Width = 240 + Anchors = [akTop, akLeft, akRight] + ItemHeight = 15 + OnChange = cbNewPrivateKeyChange + TabOrder = 0 + Text = 'Select Private Key' + end + object lblKeyName: TLabel + Left = 256 + Height = 15 + Top = 45 + Width = 152 + Anchors = [akTop, akRight] + AutoSize = False + Caption = 'Please Select Private Key' + Font.Color = clRed + ParentColor = False + ParentFont = False + end + object chkChooseFee: TCheckBox + Left = 8 + Height = 19 + Top = 80 + Width = 203 + Anchors = [akLeft, akBottom] + Caption = 'Use custom fee''s for this operation' + TabOrder = 1 + end + object chkAttachPayload: TCheckBox + Left = 8 + Height = 19 + Top = 112 + Width = 258 + Anchors = [akLeft, akBottom] + Caption = 'Attach a message/payload to this transaction' + TabOrder = 2 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectKey.pas b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectKey.pas index 1a1eb0e82..a6f6b3cc1 100644 --- a/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectKey.pas +++ b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectKey.pas @@ -17,8 +17,25 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Selects an existing wallet key as the accounts' new owner and configures + optional operation steps. +} + +{ Public types in this unit + + Type | Purpose + ------------------------+-------------------------------------------------------------------------- + TWIZChangeKey_SelectKey | Chooses a wallet key and rejects an unchanged account owner. +} interface @@ -42,13 +59,17 @@ TWIZChangeKey_SelectKey = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UWallet, @@ -149,7 +170,7 @@ procedure TWIZChangeKey_SelectKey.OnNext; end; -function TWIZChangeKey_SelectKey.Validate(out message: ansistring): boolean; +function TWIZChangeKey_SelectKey.Validate(out message: String): boolean; var LIdx: integer; LAccountKey: TAccountKey; diff --git a/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectOption.dfm b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectOption.dfm new file mode 100644 index 000000000..f1336dbdb --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectOption.dfm @@ -0,0 +1,70 @@ +object WIZChangeKey_SelectOption: TWIZChangeKey_SelectOption + Left = 0 + Height = 253 + Top = 0 + Width = 429 + ActiveControl = rbTransferAccountOwnership + Caption = 'WIZChangeKey_SelectOption' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + object gbChangeKeyOptions: TGroupBox + Left = 4 + Height = 245 + Top = 4 + Width = 420 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Change Key Options' + TabOrder = 0 + object lblNote: TLabel + Left = 8 + Height = 15 + Top = 8 + Width = 310 + Caption = 'How would you like to change the keys of your account(s).' + ParentColor = False + end + object rbTransferAccountOwnership: TRadioButton + Left = 8 + Height = 19 + Top = 40 + Width = 203 + Caption = 'Transfer account to another user' + Checked = True + Font.Style = [fsBold] + ParentFont = False + TabOrder = 1 + TabStop = True + end + object lblTransferAccountOwnership: TLabel + Left = 24 + Height = 40 + Top = 72 + Width = 385 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Use this to transfer ownership of the account to another user''s key.' + ParentColor = False + end + object rbChangeAccountPrivateKey: TRadioButton + Left = 8 + Height = 19 + Top = 120 + Width = 216 + Caption = 'Change to another key in my wallet' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + end + object lblChangeAccountPrivateKey: TLabel + Left = 8 + Height = 56 + Top = 152 + Width = 393 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'This will change the key of your account to another key that you control.' + ParentColor = False + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectOption.pas b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectOption.pas index 73177820a..bde29cc2c 100644 --- a/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectOption.pas +++ b/src/gui-experimental/wizards/operations/UWIZChangeKey_SelectOption.pas @@ -17,8 +17,25 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Chooses whether to transfer accounts to an imported public key or select an + existing wallet key. +} + +{ Public types in this unit + + Type | Purpose + ---------------------------+-------------------------------------------------------------------------- + TWIZChangeKey_SelectOption | Selects the key-change mode and inserts its key-entry screen. +} interface @@ -44,7 +61,11 @@ TWIZChangeKey_SelectOption = class(TWizardForm) implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UCoreObjects, diff --git a/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale.pas b/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale.pas index 2766238f1..65f09bc96 100644 --- a/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale.pas +++ b/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale.pas @@ -17,7 +17,20 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Coordinates removal of accounts from sale, including fees, payload, signer + and confirmation. +} + +{ Public types in this unit + + Type | Purpose + --------------------------------+-------------------------------------------------------------------------- + TWIZDelistAccountFromSaleWizard | Runs and executes confirmed account-delisting operations. +} interface @@ -33,8 +46,8 @@ TWIZDelistAccountFromSaleWizard = class(TWizard) constructor Create(AOwner: TComponent); override; function DetermineHasNext: boolean; override; function DetermineHasPrevious: boolean; override; - function FinishRequested(out message: ansistring): boolean; override; - function CancelRequested(out message: ansistring): boolean; override; + function FinishRequested(out message: String): boolean; override; + function CancelRequested(out message: String): boolean; override; end; implementation @@ -75,16 +88,17 @@ function TWIZDelistAccountFromSaleWizard.DetermineHasPrevious: boolean; Result := inherited DetermineHasPrevious; end; -function TWIZDelistAccountFromSaleWizard.FinishRequested(out message: ansistring): boolean; +function TWIZDelistAccountFromSaleWizard.FinishRequested(out message: String): boolean; begin // Execute the Delist Account From Sale Action here - Result := TWIZOperationsHelper.ExecuteDelistAccountFromSale(Model.Account.SelectedAccounts, Model.Signer.SignerAccount, Model.Fee.SingleOperationFee, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, message); - if TWIZOperationsModel.RelockOnFinish then - TWallet.Keys.LockWallet; + Result := TWIZOperationsHelper.ExecuteDelistAccountFromSale(Model.Account.SelectedAccounts, Model.Signer.SignerAccount, Model.Fee.SingleOperationFee, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, message, Model.Signer.OperationSigningMode); + if Result then + Model.RelockWallet; end; -function TWIZDelistAccountFromSaleWizard.CancelRequested(out message: ansistring): boolean; +function TWIZDelistAccountFromSaleWizard.CancelRequested(out message: String): boolean; begin + Model.RelockWallet; Result := True; end; diff --git a/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale_Options.dfm b/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale_Options.dfm new file mode 100644 index 000000000..477b53f7c --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale_Options.dfm @@ -0,0 +1,36 @@ +object WIZDelistAccountFromSale_Options: TWIZDelistAccountFromSale_Options + Left = 0 + Height = 263 + Top = 0 + Width = 429 + ActiveControl = chkChooseFee + Caption = 'WIZDelistAccountFromSale_Options' + ClientHeight = 263 + ClientWidth = 429 + Visible = False + object gbOptions: TGroupBox + Left = 4 + Height = 254 + Top = 4 + Width = 420 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Options' + TabOrder = 0 + object chkChooseFee: TCheckBox + Left = 16 + Height = 19 + Top = 8 + Width = 164 + Caption = 'Allow me to choose the fee' + TabOrder = 0 + end + object chkAttachPayload: TCheckBox + Left = 16 + Height = 19 + Top = 40 + Width = 158 + Caption = 'Attach a message payload' + TabOrder = 1 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale_Options.pas b/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale_Options.pas index ae0714fbb..b709f13f4 100644 --- a/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale_Options.pas +++ b/src/gui-experimental/wizards/operations/UWIZDelistAccountFromSale_Options.pas @@ -17,8 +17,25 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Selects optional fee and payload settings before confirming account + delisting. +} + +{ Public types in this unit + + Type | Purpose + ----------------------------------+-------------------------------------------------------------------------- + TWIZDelistAccountFromSale_Options | Builds the optional fee, payload and signer route for delisting. +} interface @@ -37,13 +54,17 @@ TWIZDelistAccountFromSale_Options = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses USettings, @@ -81,7 +102,7 @@ procedure TWIZDelistAccountFromSale_Options.OnNext; end; end; -function TWIZDelistAccountFromSale_Options.Validate(out message: ansistring): boolean; +function TWIZDelistAccountFromSale_Options.Validate(out message: String): boolean; begin Result := True; end; diff --git a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale.pas b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale.pas index c93ca3d2a..6b2281c9c 100644 --- a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale.pas +++ b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale.pas @@ -17,7 +17,20 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Coordinates public or private account sale terms, signer selection, + confirmation and execution. +} + +{ Public types in this unit + + Type | Purpose + -------------------------------+-------------------------------------------------------------------------- + TWIZEnlistAccountForSaleWizard | Runs and executes confirmed account-for-sale listings. +} interface @@ -33,8 +46,8 @@ TWIZEnlistAccountForSaleWizard = class(TWizard) constructor Create(AOwner: TComponent); override; function DetermineHasNext: boolean; override; function DetermineHasPrevious: boolean; override; - function FinishRequested(out message: ansistring): boolean; override; - function CancelRequested(out message: ansistring): boolean; override; + function FinishRequested(out message: String): boolean; override; + function CancelRequested(out message: String): boolean; override; end; implementation @@ -80,7 +93,7 @@ function TWIZEnlistAccountForSaleWizard.DetermineHasPrevious: boolean; Result := inherited DetermineHasPrevious; end; -function TWIZEnlistAccountForSaleWizard.FinishRequested(out message: ansistring): boolean; +function TWIZEnlistAccountForSaleWizard.FinishRequested(out message: String): boolean; var LPublicKey: TAccountKey; begin @@ -94,14 +107,15 @@ function TWIZEnlistAccountForSaleWizard.FinishRequested(out message: ansistring) end; - Result := TWIZOperationsHelper.ExecuteEnlistAccountForSale(Model.Account.SelectedAccounts, Model.Signer.SignerAccount, Model.EnlistAccountForSale.SellerAccount, LPublicKey, Model.Fee.SingleOperationFee, Model.EnlistAccountForSale.SalePrice, Model.EnlistAccountForSale.LockedUntilBlock, Model.EnlistAccountForSale.AccountSaleMode, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, message); - if TWIZOperationsModel.RelockOnFinish then - TWallet.Keys.LockWallet; + Result := TWIZOperationsHelper.ExecuteEnlistAccountForSale(Model.Account.SelectedAccounts, Model.Signer.SignerAccount, Model.EnlistAccountForSale.SellerAccount, LPublicKey, Model.Fee.SingleOperationFee, Model.EnlistAccountForSale.SalePrice, Model.EnlistAccountForSale.LockedUntilBlock, Model.EnlistAccountForSale.AccountSaleMode, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, message, Model.Signer.OperationSigningMode); + if Result then + Model.RelockWallet; end; -function TWIZEnlistAccountForSaleWizard.CancelRequested(out message: ansistring): boolean; +function TWIZEnlistAccountForSaleWizard.CancelRequested(out message: String): boolean; begin + Model.RelockWallet; Result := True; end; diff --git a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_EnterLockingBlock.dfm b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_EnterLockingBlock.dfm new file mode 100644 index 000000000..fbed9d789 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_EnterLockingBlock.dfm @@ -0,0 +1,35 @@ +object WIZEnlistAccountForSale_EnterLockingBlock: TWIZEnlistAccountForSale_EnterLockingBlock + Left = 0 + Height = 253 + Top = 40 + Width = 429 + ActiveControl = edtBlockLock + Caption = 'WIZEnlistAccountForSale_EnterLockingBlock' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + object gbLockBlock: TGroupBox + Left = 4 + Height = 244 + Top = 4 + Width = 420 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Enter Locking Block' + TabOrder = 0 + object lblBlockLock: TLabel + Left = 8 + Height = 15 + Top = 8 + Width = 378 + Caption = 'Please enter the block at which this sale will be allowed to succeed from' + ParentColor = False + end + object edtBlockLock: TEdit + Left = 8 + Height = 23 + Top = 48 + Width = 152 + TabOrder = 0 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_EnterLockingBlock.pas b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_EnterLockingBlock.pas index c0572a799..e33a9f936 100644 --- a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_EnterLockingBlock.pas +++ b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_EnterLockingBlock.pas @@ -17,8 +17,24 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Collects the block height until which a private account sale remains locked. +} + +{ Public types in this unit + + Type | Purpose + -------------------------------------------+-------------------------------------------------------------------------- + TWIZEnlistAccountForSale_EnterLockingBlock | Validates and stores the sale's locking block height. +} interface @@ -35,13 +51,17 @@ TWIZEnlistAccountForSale_EnterLockingBlock = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UNode, @@ -119,10 +139,10 @@ procedure TWIZEnlistAccountForSale_EnterSeller.OnNext; LAccount: TAccount; begin TAccountComp.AccountTxtNumberToAccountNumber(edtBeneficiaryAcc.Text, LAccountNumber); - Model.SendPASC.DestinationAccount := TNode.Node.GetAccount(LAccountNumber); + Model.EnlistAccountForSale.SellerAccount := TNode.Node.GetAccount(LAccountNumber); end; -function TWIZEnlistAccountForSale_EnterSeller.Validate(out message: ansistring): boolean; +function TWIZEnlistAccountForSale_EnterSeller.Validate(out message: String): boolean; var LIdx: integer; LAccountNumber: cardinal; diff --git a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_SelectOption.dfm b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_SelectOption.dfm new file mode 100644 index 000000000..3887f431f --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_SelectOption.dfm @@ -0,0 +1,66 @@ +object WIZEnlistAccountForSale_SelectOption: TWIZEnlistAccountForSale_SelectOption + Left = 0 + Height = 253 + Top = 0 + Width = 429 + ActiveControl = rbPublicSale + Caption = 'WIZEnlistAccountForSale_SelectOption' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + object Label1: TLabel + Left = 24 + Height = 32 + Top = 16 + Width = 370 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'What type of account sale would you want to perform?' + ParentColor = False + WordWrap = True + end + object rbPublicSale: TRadioButton + Left = 24 + Height = 19 + Top = 56 + Width = 79 + Caption = 'Public Sale' + Checked = True + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + TabStop = True + end + object Label2: TLabel + Left = 24 + Height = 48 + Top = 88 + Width = 352 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Account is available to be sold to any willing buyer.' + ParentColor = False + WordWrap = True + end + object rbPrivateSale: TRadioButton + Left = 24 + Height = 19 + Top = 144 + Width = 86 + Caption = 'Private Sale' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 1 + end + object Label3: TLabel + Left = 24 + Height = 48 + Top = 176 + Width = 350 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'This account is only available for sale to the specified public key.' + ParentColor = False + WordWrap = True + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_SelectOption.pas b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_SelectOption.pas index aa3714a62..c28778fba 100644 --- a/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_SelectOption.pas +++ b/src/gui-experimental/wizards/operations/UWIZEnlistAccountForSale_SelectOption.pas @@ -17,8 +17,25 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Chooses a public or private account sale and adds buyer restrictions for + private sales. +} + +{ Public types in this unit + + Type | Purpose + --------------------------------------+-------------------------------------------------------------------------- + TWIZEnlistAccountForSale_SelectOption | Selects the sale mode and inserts private-buyer key entry. +} interface @@ -44,7 +61,11 @@ TWIZEnlistAccountForSale_SelectOption = class(TWizardForm implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UCoreObjects, diff --git a/src/gui-experimental/wizards/operations/UWIZOperation.pas b/src/gui-experimental/wizards/operations/UWIZOperation.pas index 1be9d388f..1661e0fe6 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperation.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperation.pas @@ -17,7 +17,34 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Shares selected accounts, operation inputs, fees and payload state across + wizard screens. Tracks wallet unlock ownership so only wizard-owned + unlocks are relocked. +} + +{ Public types in this unit + + Type | Purpose + ---------------------------------------------------+-------------------------------------------------------------------------- + TWalletUnlockScope | Relocks a password-protected wallet only when it owns the unlock. + TWIZOperationsModel | Owns the shared inputs and unlock scope for an operation wizard. + TWIZOperationsModel.TAccountModel | Stores the selected source accounts and their count. + TWIZOperationsModel.TBuyAccountModel | Stores the purchase target, price and new owner key. + TWIZOperationsModel.TChangeAccountInfoModel | Stores name/type changes, approved targets and an optional fee payer. + TWIZOperationsModel.TChangeAccountPrivateKeyModel | Stores the selected wallet key for an account ownership change. + TWIZOperationsModel.TChangeKeyModel | Selects transfer to an imported key or an existing wallet key. + TWIZOperationsModel.TEnlistAccountForSaleModel | Stores sale price, beneficiary, buyer restrictions and lock height. + TWIZOperationsModel.TFeeModel | Stores the default and selected fee per operation. + TWIZOperationsModel.TOperationExecuteResultHandler | Receives an operation's source account, outcome and result text. + TWIZOperationsModel.TPayloadModel | Stores payload content, encryption mode, password and encoded bytes. + TWIZOperationsModel.TSendPASCModel | Stores transfer amount, recipient and specified-amount or send-all mode. + TWIZOperationsModel.TSignerModel | Stores eligible signers, the selected signer and fee-paying mode. + TWIZOperationsModel.TTransferAccountModel | Stores the imported public key that will own transferred accounts. +} interface @@ -26,6 +53,23 @@ interface type + { Borrows the wallet keys and only relocks a password-protected wallet which + this scope successfully unlocked. No wallet files or global wallet state + are accessed until a caller supplies the keys. } + + TWalletUnlockScope = class(TComponent) + private + FWalletKeys: TWalletKeys; + FOwnsUnlock: Boolean; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + destructor Destroy; override; + function TryAcquire(AWalletKeys: TWalletKeys; AUnlock: TNotifyEvent; out AMessage: String): Boolean; + procedure Release; + property OwnsUnlock: Boolean read FOwnsUnlock; + end; + { TWIZOperationsModel } TWIZOperationsModel = class(TComponent) @@ -33,8 +77,7 @@ TWIZOperationsModel = class(TComponent) { TOperationExecuteResultHandler } - TOperationExecuteResultHandler = - procedure(const ASourceAccount: TAccount; AOpType: TExecuteOperationType; const AOpText: ansistring; Result: boolean; const Message: ansistring) of object; + TOperationExecuteResultHandler = procedure(const ASourceAccount: TAccount; AOpType: TExecuteOperationType; const AOpText: String; Result: boolean; const Message: String) of object; { TAccountModel } @@ -119,6 +162,13 @@ TChangeAccountInfoModel = class(TComponent) public NewName: TRawBytes; NewType: Word; + // Keep the user's original selection separate from approved updates. + TargetAccounts: TArray; + SkippedCount: Integer; + SkippedAccountsApproved: Boolean; + UseSingleFeePayer: Boolean; + FeePayerSelected: Boolean; + FeePayerAccount: TAccount; end; { TBuyAccountModel } @@ -133,7 +183,7 @@ TBuyAccountModel = class(TComponent) private FExecuteOperationType: TExecuteOperationType; - FRelockOnFinish: Boolean; static; + FUnlockScope: TWalletUnlockScope; FAccount: TAccountModel; FSendPASC: TSendPASCModel; FChangeKey: TChangeKeyModel; @@ -147,6 +197,12 @@ TBuyAccountModel = class(TComponent) FBuyAccount: TBuyAccountModel; public constructor Create(AOwner: TComponent; AType: TExecuteOperationType); overload; + function TryUnlockWallet(AWalletKeys: TWalletKeys; AUnlock: TNotifyEvent; out AMessage: String): Boolean; + procedure RelockWallet; + { Filters a common fee payer against the same ownership, lock and total + fee rules used by preparation. Secondary mode lets funded targets pay + their own fee before falling back to the selected fee payer. } + class function FilterSignerCandidates(const ACandidates, ATargets: TArray; ABlocksCount: Cardinal; AFee: Int64; AMode: TOperationSigningMode): TArray; static; property ExecuteOperationType: TExecuteOperationType read FExecuteOperationType; property Account: TAccountModel read FAccount; property SendPASC: TSendPASCModel read FSendPASC; @@ -159,18 +215,83 @@ TBuyAccountModel = class(TComponent) property Payload: TPayloadModel read FPayload; property ChangeAccountInfo: TChangeAccountInfoModel read FChangeAccountInfo; property BuyAccount: TBuyAccountModel read FBuyAccount; - class property RelockOnFinish: Boolean read FRelockOnFinish write FRelockOnFinish; + property UnlockScope: TWalletUnlockScope read FUnlockScope; end; implementation +uses UConst, UCommon; + +{ TWalletUnlockScope } + +destructor TWalletUnlockScope.Destroy; +begin + Release; + inherited Destroy; +end; + +procedure TWalletUnlockScope.Notification(AComponent: TComponent; Operation: TOperation); +begin + if (Operation = opRemove) and (AComponent = FWalletKeys) then + begin + FWalletKeys := nil; + FOwnsUnlock := False; + end; + inherited Notification(AComponent, Operation); +end; + +function TWalletUnlockScope.TryAcquire(AWalletKeys: TWalletKeys; AUnlock: TNotifyEvent; out AMessage: String): Boolean; +begin + AMessage := ''; + if not Assigned(AWalletKeys) then + begin + AMessage := 'No wallet keys found.'; + Exit(False); + end; + if FWalletKeys <> AWalletKeys then + begin + Release; + if Assigned(FWalletKeys) then + FWalletKeys.RemoveFreeNotification(Self); + FWalletKeys := AWalletKeys; + FWalletKeys.FreeNotification(Self); + end; + if not AWalletKeys.HasPassword or AWalletKeys.IsValidPassword then + Exit(True); + + // An external lock ends any previous unlock owned by this scope. + FOwnsUnlock := False; + try + if Assigned(AUnlock) then + AUnlock(Self); + finally + // Also retain ownership if a dialog callback raises after unlocking. + FOwnsUnlock := Assigned(FWalletKeys) and FWalletKeys.HasPassword and FWalletKeys.IsValidPassword; + end; + Result := Assigned(FWalletKeys) and FWalletKeys.IsValidPassword; + if not Result then + AMessage := 'Wallet remains locked. Unlock it before executing the operation.'; +end; + +procedure TWalletUnlockScope.Release; +var + LWalletKeys: TWalletKeys; +begin + if not FOwnsUnlock then + Exit; + LWalletKeys := FWalletKeys; + FOwnsUnlock := False; + if Assigned(LWalletKeys) then + LWalletKeys.LockWallet; +end; + { TWIZOperationsModel } constructor TWIZOperationsModel.Create(AOwner: TComponent; AType: TExecuteOperationType); begin inherited Create(AOwner); FExecuteOperationType := AType; - FRelockOnFinish := False; + FUnlockScope := TWalletUnlockScope.Create(Self); FAccount := TAccountModel.Create(Self); FSendPASC := TSendPASCModel.Create(Self); FChangeKey := TChangeKeyModel.Create(Self); @@ -184,4 +305,55 @@ constructor TWIZOperationsModel.Create(AOwner: TComponent; AType: TExecuteOperat FBuyAccount := TBuyAccountModel.Create(Self); end; +function TWIZOperationsModel.TryUnlockWallet(AWalletKeys: TWalletKeys; AUnlock: TNotifyEvent; out AMessage: String): Boolean; +begin + Result := FUnlockScope.TryAcquire(AWalletKeys, AUnlock, AMessage); +end; + +procedure TWIZOperationsModel.RelockWallet; +begin + FUnlockScope.Release; +end; + +class function TWIZOperationsModel.FilterSignerCandidates(const ACandidates, ATargets: TArray; ABlocksCount: Cardinal; AFee: Int64; AMode: TOperationSigningMode): TArray; +var + LCandidate, LTarget: TAccount; + LCandidateIdx, LTargetIdx: Integer; + LRequiredFee, LFee: UInt64; + LValid: Boolean; +begin + Result := nil; + if (AFee < 0) or (AFee > CT_MaxTransactionFee) or (Length(ATargets) = 0) then + Exit; + for LTargetIdx := Low(ATargets) to High(ATargets) do + if TAccountComp.IsAccountLocked(ATargets[LTargetIdx].accountInfo, ABlocksCount) or + TAccountComp.IsAccountBlockedByProtocol(ATargets[LTargetIdx].account, ABlocksCount) then + Exit; + LFee := UInt64(AFee); + for LCandidateIdx := Low(ACandidates) to High(ACandidates) do + begin + LCandidate := ACandidates[LCandidateIdx]; + LValid := not TAccountComp.IsAccountLocked(LCandidate.accountInfo, ABlocksCount) and + not TAccountComp.IsAccountBlockedByProtocol(LCandidate.account, ABlocksCount); + LRequiredFee := 0; + for LTargetIdx := Low(ATargets) to High(ATargets) do + begin + LTarget := ATargets[LTargetIdx]; + if (AMode = akaSecondary) and (LTarget.balance >= LFee) and + (LTarget.account <> LCandidate.account) then + Continue; + if not TAccountComp.EqualAccountKeys(LCandidate.accountInfo.accountKey, LTarget.accountInfo.accountKey) then + LValid := False; + if LCandidate.balance - LRequiredFee < LFee then + begin + LValid := False; + Break; + end; + Inc(LRequiredFee, LFee); + end; + if LValid then + TArrayTool.Add(Result, LCandidate); + end; +end; + end. diff --git a/src/gui-experimental/wizards/operations/UWIZOperationConfirmation.dfm b/src/gui-experimental/wizards/operations/UWIZOperationConfirmation.dfm new file mode 100644 index 000000000..52a7228cb --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZOperationConfirmation.dfm @@ -0,0 +1,68 @@ +object WIZOperationConfirmation: TWIZOperationConfirmation + Left = 0 + Height = 320 + Top = 0 + Width = 511 + Caption = 'WIZOperationConfirmation' + ClientHeight = 320 + ClientWidth = 511 + Visible = False + object gpConfirmOperation: TGroupBox + Left = 10 + Height = 299 + Top = 8 + Width = 490 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Confirm Transaction' + TabOrder = 0 + object paGrid: TPanel + Left = 8 + Height = 147 + Top = 24 + Width = 464 + Anchors = [akTop, akLeft, akRight, akBottom] + BevelOuter = bvNone + Caption = 'GRID PANEL' + TabOrder = 0 + end + object mmoPayload: TMemo + Left = 8 + Height = 82 + Top = 192 + Width = 464 + Anchors = [akLeft, akRight, akBottom] + Enabled = False + ScrollBars = ssBoth + TabOrder = 1 + end + object lblPayload: TLabel + Left = 8 + Height = 15 + Top = 172 + Width = 42 + Anchors = [akLeft, akBottom] + Caption = 'Payload' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblBeneficiaryAccount: TLabel + Left = 8 + Height = 15 + Top = 2 + Width = 115 + Caption = 'Beneficiary Account:' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblBenAcc: TLabel + Left = 130 + Height = 15 + Top = 2 + Width = 53 + Caption = 'lblBenAcc' + ParentColor = False + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationConfirmation.pas b/src/gui-experimental/wizards/operations/UWIZOperationConfirmation.pas index cf0a7ea63..644377059 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationConfirmation.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperationConfirmation.pas @@ -17,8 +17,25 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Summarizes operation targets, signers, fees and payload before execution, + and requests wallet unlock when needed. +} + +{ Public types in this unit + + Type | Purpose + --------------------------+-------------------------------------------------------------------------- + TWIZOperationConfirmation | Presents the final operation summary and validates wallet unlock. +} interface @@ -41,15 +58,20 @@ TWIZOperationConfirmation = class(TWizardForm) paGrid: TPanel; private FConfirmationGrid: TVisualGrid; + procedure UnlockWallet(Sender: TObject); public procedure OnPresent; override; - procedure OnNext; override; + function Validate(out message: String): Boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UConst, @@ -79,7 +101,7 @@ TOperationConfirmationDataSource = class(TAccountsDataSourceBase) public property Model: TWIZOperationsModel read FModel write FModel; procedure FetchAll(const AContainer: TList); override; - function GetItemField(constref AItem: TAccount; const ABindingName: ansistring): variant; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TAccount; const ABindingName: String): variant; override; end; { TWIZOperationConfirmation } @@ -88,11 +110,13 @@ procedure TWIZOperationConfirmation.OnPresent; var LData: TOperationConfirmationDataSource; LCaption: string; + LTargetCount: Integer; begin lblBeneficiaryAccount.Visible := False; lblBenAcc.Visible := False; + FreeAndNil(FConfirmationGrid); FConfirmationGrid := TVisualGrid.Create(Self); FConfirmationGrid.CanSearch := False; FConfirmationGrid.SortMode := smMultiColumn; @@ -101,7 +125,10 @@ procedure TWIZOperationConfirmation.OnPresent; FConfirmationGrid.SelectionType := stNone; FConfirmationGrid.Options := [vgoColAutoFill, vgoColSizing, vgoSortDirectionAllowNone, vgoAutoHidePaging]; LCaption := 'Confirm Operation'; - gpConfirmOperation.Caption := IIF(Model.Account.Count > 1, Format('%ss', [LCaption]), Format('%s', [LCaption])); + LTargetCount := Model.Account.Count; + if Model.ExecuteOperationType = omtChangeInfo then + LTargetCount := Length(Model.ChangeAccountInfo.TargetAccounts); + gpConfirmOperation.Caption := IIF(LTargetCount > 1, Format('%ss', [LCaption]), Format('%s', [LCaption])); with FConfirmationGrid.AddColumn('Sender') do begin StretchedToFill := True; @@ -158,15 +185,16 @@ procedure TWIZOperationConfirmation.OnPresent; LData := TOperationConfirmationDataSource.Create(FConfirmationGrid); LData.Model := Model; - FConfirmationGrid.DataSource := LData; + // Synchronous fetching can measure native cells, so establish parenting first. paGrid.AddControlDockCenter(FConfirmationGrid); + FConfirmationGrid.DataSource := LData; case Model.ExecuteOperationType of omtEnlistAccountForSale: begin lblBeneficiaryAccount.Visible := True; lblBenAcc.Visible := True; - lblBenAcc.Caption := Model.EnlistAccountForSale.SellerAccount.AccountString; + lblBenAcc.Caption := TAccountComp.AccountNumberToAccountTxtNumber(Model.EnlistAccountForSale.SellerAccount.account); end; end; @@ -185,16 +213,14 @@ procedure TWIZOperationConfirmation.OnPresent; end; end; -procedure TWIZOperationConfirmation.OnNext; -var - LLocked: boolean; +procedure TWIZOperationConfirmation.UnlockWallet(Sender: TObject); begin - LLocked := (not TWallet.Keys.HasPassword) or (not TWallet.Keys.IsValidPassword); - if LLocked then - begin - TUserInterface.UnlockWallet(Self); - TWIZOperationsModel.RelockOnFinish := LLocked; - end; + TUserInterface.UnlockWallet(Self); +end; + +function TWIZOperationConfirmation.Validate(out message: String): Boolean; +begin + Result := Model.TryUnlockWallet(TWallet.Keys, UnlockWallet, message); end; { TOperationConfirmationDataSource } @@ -213,7 +239,7 @@ function TOperationConfirmationDataSource.GetColumns: TDataColumns; ]); end; -function TOperationConfirmationDataSource.GetItemField(constref AItem: TAccount; const ABindingName: ansistring): variant; +function TOperationConfirmationDataSource.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: TAccount; const ABindingName: String): variant; var LAmount: string; LCurrentAccountKey, LDestinationAccountKey: TAccountKey; @@ -258,7 +284,7 @@ function TOperationConfirmationDataSource.GetItemField(constref AItem: TAccount; case Model.ExecuteOperationType of omtSendPasc: begin - Result := Model.SendPASC.DestinationAccount.AccountString; + Result := TAccountComp.AccountNumberToAccountTxtNumber(Model.SendPASC.DestinationAccount.account); Result := TCellRenderers.OperationShortHash(Result); end; omtChangeKey: @@ -298,25 +324,25 @@ function TOperationConfirmationDataSource.GetItemField(constref AItem: TAccount; end end else if ABindingName = 'Signer' then + if Model.ExecuteOperationType = omtChangeInfo then + begin + if Model.ChangeAccountInfo.UseSingleFeePayer then + Result := TAccountComp.AccountNumberToAccountTxtNumber(Model.ChangeAccountInfo.FeePayerAccount.account) + else + Result := inherited GetItemField(AItem, 'Account'); + end + else if (Model.Signer.OperationSigningMode = akaSecondary) and + (AItem.balance >= UInt64(Model.Fee.SingleOperationFee)) then + Result := inherited GetItemField(AItem, 'Account') + else case Model.ExecuteOperationType of omtSendPasc, omtBuyAccount: begin Result := inherited GetItemField(AItem, 'Account'); end; - omtChangeInfo: - begin - if Model.Account.Count = 1 then - begin - Result := Model.Signer.SignerAccount.AccountString; - end - else - begin - Result := inherited GetItemField(AItem, 'Account'); - end; - end else begin - Result := Model.Signer.SignerAccount.AccountString; + Result := TAccountComp.AccountNumberToAccountTxtNumber(Model.Signer.SignerAccount.account); end; end else if ABindingName = 'Fee' then @@ -329,9 +355,14 @@ function TOperationConfirmationDataSource.GetItemField(constref AItem: TAccount; procedure TOperationConfirmationDataSource.FetchAll(const AContainer: TList); var LIdx: integer; + LAccounts: TArray; begin - for LIdx := Low(Model.Account.SelectedAccounts) to High(Model.Account.SelectedAccounts) do - AContainer.Add(Model.Account.SelectedAccounts[LIdx]); + LAccounts := Model.Account.SelectedAccounts; + if Model.ExecuteOperationType = omtChangeInfo then + LAccounts := Model.ChangeAccountInfo.TargetAccounts; + // Confirmation and its fee rows contain only the approved updates. + for LIdx := Low(LAccounts) to High(LAccounts) do + AContainer.Add(LAccounts[LIdx]); end; diff --git a/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.dfm b/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.dfm new file mode 100644 index 000000000..421c6b5ed --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.dfm @@ -0,0 +1,113 @@ +object WIZOperationFee_Custom: TWIZOperationFee_Custom + Left = 0 + Height = 253 + Top = 0 + Width = 429 + ActiveControl = fseFee + Caption = 'WIZOperationFee_Custom' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + OnResize = FormResize + object gbTransactionFee: TGroupBox + Left = 0 + Height = 248 + Top = 0 + Width = 422 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Operation Fee' + TabOrder = 0 + object fseFee: TNumberBox + Mode = nbmFloat + Left = 11 + Height = 23 + Top = 35 + Width = 125 + Decimal = 4 + SmallStep = 0.0001 + MaxValue = 1 + MinValue = 0 + OnChange = fseFeeChange + TabOrder = 0 + Value = 0 + end + object lblNote1: TLabel + Left = 11 + Height = 15 + Top = 8 + Width = 179 + Caption = 'Please enter the fee per operation.' + ParentColor = False + end + object lblPASC: TLabel + Left = 144 + Height = 15 + Top = 40 + Width = 28 + Caption = 'PASC' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblestimatedfee: TLabel + Left = 246 + Height = 15 + Top = 40 + Width = 53 + Alignment = taRightJustify + Anchors = [akTop, akRight] + Caption = 'Total Fee:' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object lblTotalFeeValue: TLabel + Left = 310 + Height = 15 + Top = 40 + Width = 112 + Anchors = [akTop, akRight] + AutoSize = False + Caption = 'xxxx' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + object cbOverrideDefaultFee: TCheckBox + Left = 11 + Height = 19 + Top = 72 + Width = 262 + Caption = 'Make this my default fee for future operations' + TabOrder = 1 + end + object chkSingleFeePayer: TCheckBox + Left = 11 + Top = 112 + Width = 350 + Height = 23 + Caption = 'Select a single fee payer' + TabOrder = 2 + Visible = False + OnClick = SingleFeePayerChange + end + object cbFeePayer: TComboBox + Left = 11 + Top = 144 + Width = 300 + Height = 23 + Style = csDropDownList + TabOrder = 3 + Visible = False + OnChange = FeePayerChange + end + object lblFeePayerBalance: TLabel + Left = 11 + Top = 176 + Width = 350 + Height = 20 + Caption = 'Each updated account pays its own fee.' + Visible = False + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.lfm b/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.lfm index de6230660..ef8e30f18 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.lfm +++ b/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.lfm @@ -9,6 +9,7 @@ object WIZOperationFee_Custom: TWIZOperationFee_Custom ClientWidth = 429 LCLVersion = '1.8.2.0' Visible = False + OnResize = FormResize object gbTransactionFee: TGroupBox Left = 0 Height = 248 @@ -82,5 +83,33 @@ object WIZOperationFee_Custom: TWIZOperationFee_Custom Caption = 'Make this my default fee for future operations' TabOrder = 1 end + object chkSingleFeePayer: TCheckBox + Left = 11 + Top = 112 + Width = 350 + Height = 23 + Caption = 'Select a single fee payer' + TabOrder = 2 + Visible = False + OnClick = SingleFeePayerChange + end + object cbFeePayer: TComboBox + Left = 11 + Top = 144 + Width = 300 + Height = 23 + Style = csDropDownList + TabOrder = 3 + Visible = False + OnChange = FeePayerChange + end + object lblFeePayerBalance: TLabel + Left = 11 + Top = 176 + Width = 350 + Height = 20 + Caption = 'Each updated account pays its own fee.' + Visible = False + end end end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.pas b/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.pas index 02b34b9c8..b070ac1c5 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperationFee_Custom.pas @@ -17,16 +17,38 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Chooses the operation fee and, for account-information changes, an optional + single fee payer. +} + +{ Public types in this unit + + Type | Purpose + ------------------------+-------------------------------------------------------------------------- + TWIZOperationFee_Custom | Validates per-operation fees and optional batch fee-payer selection. +} interface uses - SysUtils, StdCtrls, Spin, UWizard, UWIZOperation; + Types, Math, Controls, ExtCtrls, + SysUtils, StdCtrls, {$IFDEF FPC}Spin{$ELSE}Vcl.NumberBox{$ENDIF}, UPCDataTypes, UWizard, UWIZOperation; type + {$IFNDEF FPC} + TFloatSpinEdit = TNumberBox; + {$ENDIF} + { TWIZOperationFee_Custom } TWIZOperationFee_Custom = class(TWizardForm) @@ -37,8 +59,18 @@ TWIZOperationFee_Custom = class(TWizardForm) lblPASC: TLabel; lblNote1: TLabel; lblTotalFeeValue: TLabel; + chkSingleFeePayer: TCheckBox; + cbFeePayer: TComboBox; + lblFeePayerBalance: TLabel; procedure fseFeeChange(Sender: TObject); + procedure FormResize(Sender: TObject); + procedure SingleFeePayerChange(Sender: TObject); + procedure FeePayerChange(Sender: TObject); private + FUpdatingFeePayer: Boolean; + FFeePayerRequiresChoice: Boolean; + FFeePayerCandidates: TArray; + procedure UpdateFeePayers; procedure UpdateUI(); procedure SetFee(const AValue: int64); function GetFee: int64; @@ -47,17 +79,21 @@ TWIZOperationFee_Custom = class(TWizardForm) procedure Initialize; override; procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UAccounts, - UPCDataTypes, + UNode, USettings, UCoreUtils, UCoreObjects, @@ -66,13 +102,66 @@ implementation { TWIZOperationFee_Custom } +type + TCaptionGroupBoxAccess = class(TGroupBox); + procedure TWIZOperationFee_Custom.Initialize; begin Fee := TSettings.DefaultFee; end; +procedure TWIZOperationFee_Custom.FormResize(Sender: TObject); +var + LContent: TRect; + LPadding: Integer; + + procedure PositionContents(AGroup: TGroupBox); + var + LContent: TRect; + LPadding, LTop, LDelta, I: Integer; + LControl: TControl; + begin + LContent := AGroup.ClientRect; + TCaptionGroupBoxAccess(AGroup).AdjustClientRect(LContent); + Canvas.Font.Assign(AGroup.Font); + LPadding := Max(6, Canvas.TextHeight('0') * 2 div 3); + LTop := High(Integer); + for I := 0 to AGroup.ControlCount - 1 do + LTop := Min(LTop, AGroup.Controls[I].Top); + LDelta := LContent.Top + LPadding - LTop; + for I := 0 to AGroup.ControlCount - 1 do begin + LControl := AGroup.Controls[I]; + LControl.Top := LControl.Top + LDelta; + if (LControl is TPanel) and (akBottom in LControl.Anchors) then + LControl.Height := Max(0, LContent.Bottom - LControl.Top - LPadding); + end; + end; + +begin + if not HandleAllocated or not Assigned(cbOverrideDefaultFee) then Exit; + // VCL reserves its caption inside ClientRect; LCL already excludes it. + // Use the native content inset so controls stay clear at either font scale. + PositionContents(gbTransactionFee); + // This fixed-width value must retain the full bold font line after scaling. + Canvas.Font.Assign(lblTotalFeeValue.Font); + lblTotalFeeValue.Height := Max(lblTotalFeeValue.Height, Canvas.TextHeight('Mg')); + LContent := gbTransactionFee.ClientRect; + TCaptionGroupBoxAccess(gbTransactionFee).AdjustClientRect(LContent); + Canvas.Font.Assign(gbTransactionFee.Font); + LPadding := Max(6, Canvas.TextHeight('0') * 2 div 3); + // Keep the right-aligned summary inside the group box's native border. + lblTotalFeeValue.Left := LContent.Right - LPadding - lblTotalFeeValue.Width; + lblestimatedfee.Left := lblTotalFeeValue.Left - LPadding - lblestimatedfee.Width; +end; + procedure TWIZOperationFee_Custom.OnPresent; begin + chkSingleFeePayer.Visible := Model.ExecuteOperationType = omtChangeInfo; + cbFeePayer.Visible := chkSingleFeePayer.Visible; + lblFeePayerBalance.Visible := chkSingleFeePayer.Visible; + if chkSingleFeePayer.Visible then + chkSingleFeePayer.Checked := Model.ChangeAccountInfo.UseSingleFeePayer; + FormResize(Self); UpdateUI(); fseFee.SetFocus; end; @@ -86,8 +175,11 @@ procedure TWIZOperationFee_Custom.OnNext; TSettings.DefaultFee := Fee; TSettings.Save; end; - LAllUserAccountsExcludingPending := TCoreTool.GetUserAccounts(False); - Model.Signer.SignerCandidates := TCoreTool.GetSignerCandidates(Model.Account.Count, Fee, LAllUserAccountsExcludingPending); + if Model.ExecuteOperationType <> omtChangeInfo then + begin + LAllUserAccountsExcludingPending := TCoreTool.GetUserAccounts(True); + Model.Signer.SignerCandidates := LAllUserAccountsExcludingPending; + end; Model.Fee.SingleOperationFee := Fee; // TODO: move this out -- inappropriate to have payload/signer considerations here @@ -99,18 +191,11 @@ procedure TWIZOperationFee_Custom.OnNext; else begin case Model.ExecuteOperationType of - omtSendPasc, omtBuyAccount: + omtSendPasc, omtBuyAccount, omtChangeInfo: begin // do nothing end; - omtChangeInfo: - begin - if Model.Account.Count = 1 then - begin - UpdatePath(ptInject, [TWIZOperationSigner_Select]); - end; - end else begin UpdatePath(ptInject, [TWIZOperationSigner_Select]); @@ -121,16 +206,56 @@ procedure TWIZOperationFee_Custom.OnNext; end; -function TWIZOperationFee_Custom.Validate(out message: ansistring): boolean; +function TWIZOperationFee_Custom.Validate(out message: String): boolean; var LAcc: TAccount; + LIdx, LCount: Integer; begin Result := True; - if (Model.Account.Count > 1) and (Fee = 0) then + if Fee < 0 then + begin + message := 'The operation fee cannot be negative.'; + Exit(False); + end; + LCount := Model.Account.Count; + if Model.ExecuteOperationType = omtChangeInfo then + LCount := Length(Model.ChangeAccountInfo.TargetAccounts); + if (LCount > 1) and (Fee = 0) then begin message := 'Zero fees only allowed for a single operation.'; Exit(False); end; + if (Model.ExecuteOperationType = omtChangeInfo) and chkSingleFeePayer.Checked and + not Model.ChangeAccountInfo.FeePayerSelected then + begin + message := 'Select an eligible account which will pay the fee.'; + Exit(False); + end; + if (Model.ExecuteOperationType = omtSendPasc) and + (Model.SendPASC.SendPASCMode = akaSpecifiedAmount) then + for LIdx := Low(Model.Account.SelectedAccounts) to High(Model.Account.SelectedAccounts) do + begin + LAcc := Model.Account.SelectedAccounts[LIdx]; + if (Model.SendPASC.SingleAmountToSend < 1) or + (LAcc.balance < UInt64(Model.SendPASC.SingleAmountToSend)) or + (LAcc.balance - UInt64(Model.SendPASC.SingleAmountToSend) < UInt64(Fee)) then + begin + message := Format('Account %s cannot pay the specified amount and fee.', [TAccountComp.AccountNumberToAccountTxtNumber(LAcc.account)]); + Exit(False); + end; + end; + if Model.ExecuteOperationType = omtBuyAccount then + for LIdx := Low(Model.Account.SelectedAccounts) to High(Model.Account.SelectedAccounts) do + begin + LAcc := Model.Account.SelectedAccounts[LIdx]; + if (Model.BuyAccount.Amount < 1) or + (LAcc.balance < UInt64(Model.BuyAccount.Amount)) or + (LAcc.balance - UInt64(Model.BuyAccount.Amount) < UInt64(Fee)) then + begin + message := Format('Account %s cannot pay the purchase amount and fee.', [TAccountComp.AccountNumberToAccountTxtNumber(LAcc.account)]); + Exit(False); + end; + end; end; procedure TWIZOperationFee_Custom.SetFee(const AValue: int64); @@ -140,13 +265,94 @@ procedure TWIZOperationFee_Custom.SetFee(const AValue: int64); function TWIZOperationFee_Custom.GetFee: int64; begin - if not TAccountComp.TxtToMoney(Trim(fseFee.ValueToStr(fseFee.Value)), Result) then + if not TAccountComp.TxtToMoney(Trim( + {$IFDEF FPC}fseFee.ValueToStr(fseFee.Value) + {$ELSE}FloatToStr(fseFee.Value, TFormatSettings.Invariant){$ENDIF}), Result) then raise Exception.Create('Invalid value in fee selector'); end; procedure TWIZOperationFee_Custom.UpdateUI(); +var + LCount: Integer; +begin + LCount := Model.Account.Count; + if Model.ExecuteOperationType = omtChangeInfo then + begin + LCount := Length(Model.ChangeAccountInfo.TargetAccounts); + UpdateFeePayers; + end; + lblTotalFeeValue.Caption := Format('%s PASC', [TAccountComp.FormatMoney(Fee * LCount)]); +end; + +procedure TWIZOperationFee_Custom.UpdateFeePayers; +var + I, LSelected: Integer; + LHadSelection: Boolean; begin - lblTotalFeeValue.Caption := Format('%s PASC', [TAccountComp.FormatMoney(Fee * Model.Account.Count)]); + if FUpdatingFeePayer then Exit; + FUpdatingFeePayer := True; + try + // Only approved targets contribute to the common payer's cumulative fee. + FFeePayerCandidates := TWIZOperationsModel.FilterSignerCandidates( + Model.Signer.SignerCandidates, Model.ChangeAccountInfo.TargetAccounts, + TNode.Node.Bank.BlocksCount, Fee, akaPrimary); + LSelected := 0; + LHadSelection := Model.ChangeAccountInfo.FeePayerSelected; + cbFeePayer.Items.BeginUpdate; + try + cbFeePayer.Items.Clear; + cbFeePayer.Items.Add('Select fee payer'); + for I := 0 to High(FFeePayerCandidates) do + begin + cbFeePayer.Items.Add(TAccountComp.AccountNumberToAccountTxtNumber(FFeePayerCandidates[I].account)); + if Model.ChangeAccountInfo.FeePayerSelected and + (FFeePayerCandidates[I].account = Model.ChangeAccountInfo.FeePayerAccount.account) then + LSelected := I + 1; + end; + if LHadSelection and (LSelected = 0) then + FFeePayerRequiresChoice := True; + // A fee increase must not silently replace a now-ineligible selection. + if (LSelected = 0) and not FFeePayerRequiresChoice and (Length(FFeePayerCandidates) = 1) then + LSelected := 1; + cbFeePayer.ItemIndex := LSelected; + finally + cbFeePayer.Items.EndUpdate; + end; + cbFeePayer.Enabled := chkSingleFeePayer.Checked; + finally + FUpdatingFeePayer := False; + end; + FeePayerChange(Self); +end; + +procedure TWIZOperationFee_Custom.SingleFeePayerChange(Sender: TObject); +begin + if FUpdatingFeePayer or not Assigned(Model) or + (Model.ExecuteOperationType <> omtChangeInfo) then Exit; + Model.ChangeAccountInfo.UseSingleFeePayer := chkSingleFeePayer.Checked; + UpdateFeePayers; +end; + +procedure TWIZOperationFee_Custom.FeePayerChange(Sender: TObject); +var + I: Integer; +begin + if FUpdatingFeePayer or not Assigned(Model) or + (Model.ExecuteOperationType <> omtChangeInfo) then Exit; + I := cbFeePayer.ItemIndex - 1; + Model.ChangeAccountInfo.UseSingleFeePayer := chkSingleFeePayer.Checked; + Model.ChangeAccountInfo.FeePayerSelected := (I >= 0) and (I < Length(FFeePayerCandidates)); + if Sender = cbFeePayer then + FFeePayerRequiresChoice := not Model.ChangeAccountInfo.FeePayerSelected; + if Model.ChangeAccountInfo.FeePayerSelected then + Model.ChangeAccountInfo.FeePayerAccount := FFeePayerCandidates[I]; + if not chkSingleFeePayer.Checked then + lblFeePayerBalance.Caption := 'Each updated account pays its own fee.' + else if Model.ChangeAccountInfo.FeePayerSelected then + lblFeePayerBalance.Caption := Format('Balance: %s PASC', + [TAccountComp.FormatMoney(Model.ChangeAccountInfo.FeePayerAccount.balance)]) + else + lblFeePayerBalance.Caption := 'Select an eligible fee payer.'; end; procedure TWIZOperationFee_Custom.fseFeeChange(Sender: TObject); diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.dfm b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.dfm new file mode 100644 index 000000000..e9356e860 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.dfm @@ -0,0 +1,46 @@ +object WIZOperationPayload_Content: TWIZOperationPayload_Content + Left = 0 + Height = 253 + Top = 0 + Width = 429 + ActiveControl = mmoPayload + Caption = 'Form1' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + OnResize = FormResize + object grpPayload: TGroupBox + Left = 8 + Height = 244 + Top = 2 + Width = 416 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Payload Content' + TabOrder = 0 + object paPayload: TPanel + Left = 8 + Height = 214 + Top = 2 + Width = 396 + Anchors = [akTop, akLeft, akRight, akBottom] + BevelOuter = bvNone + TabOrder = 0 + object mmoPayload: TMemo + Left = 0 + Height = 176 + Top = 32 + Width = 392 + Anchors = [akTop, akLeft, akRight, akBottom] + TabOrder = 0 + end + object Label1: TLabel + Left = 0 + Height = 15 + Top = 6 + Width = 153 + Caption = 'Enter Payload Content Below' + ParentColor = False + end + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.lfm b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.lfm index 2c9d742ad..d7c58e2f0 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.lfm +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.lfm @@ -9,6 +9,7 @@ object WIZOperationPayload_Content: TWIZOperationPayload_Content ClientWidth = 429 LCLVersion = '1.8.2.0' Visible = False + OnResize = FormResize object grpPayload: TGroupBox Left = 8 Height = 244 diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.pas b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.pas index 8d3986133..a49efd2b3 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Content.pas @@ -17,12 +17,30 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Collects an operation message and checks message text length against the limit before the + next wizard step. +} + +{ Public types in this unit + + Type | Purpose + -----------------------------+-------------------------------------------------------------------------- + TWIZOperationPayload_Content | Edits payload content and checks its length against the size limit. +} interface uses + Types, Math, Controls, SysUtils, StdCtrls, ExtCtrls, UWizard, UWIZOperation; type @@ -34,16 +52,21 @@ TWIZOperationPayload_Content = class(TWizardForm) Label1: TLabel; mmoPayload: TMemo; paPayload: TPanel; + procedure FormResize(Sender: TObject); public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UConst, @@ -52,8 +75,43 @@ implementation { TWIZOperationPayload_Content } +type + TCaptionGroupBoxAccess = class(TGroupBox); + +procedure TWIZOperationPayload_Content.FormResize(Sender: TObject); + + procedure PositionContents(AGroup: TGroupBox); + var + LContent: TRect; + LPadding, LTop, LDelta, I: Integer; + LControl: TControl; + begin + LContent := AGroup.ClientRect; + TCaptionGroupBoxAccess(AGroup).AdjustClientRect(LContent); + Canvas.Font.Assign(AGroup.Font); + LPadding := Max(6, Canvas.TextHeight('0') * 2 div 3); + LTop := High(Integer); + for I := 0 to AGroup.ControlCount - 1 do + LTop := Min(LTop, AGroup.Controls[I].Top); + LDelta := LContent.Top + LPadding - LTop; + for I := 0 to AGroup.ControlCount - 1 do begin + LControl := AGroup.Controls[I]; + LControl.Top := LControl.Top + LDelta; + if (LControl is TPanel) and (akBottom in LControl.Anchors) then + LControl.Height := Max(0, LContent.Bottom - LControl.Top - LPadding); + end; + end; + +begin + if not HandleAllocated or not Assigned(Label1) then Exit; + // VCL reserves its caption inside ClientRect; LCL already excludes it. + // Use the native content inset so controls stay clear at either font scale. + PositionContents(grpPayload); +end; + procedure TWIZOperationPayload_Content.OnPresent; begin + FormResize(Self); mmoPayload.SetFocus; end; @@ -69,10 +127,7 @@ procedure TWIZOperationPayload_Content.OnNext; omtChangeInfo: begin - if Model.Account.Count = 1 then - begin - UpdatePath(ptInject, [TWIZOperationSigner_Select]); - end; + // Change Info chooses its optional common payer on the fee page. end else begin @@ -82,7 +137,7 @@ procedure TWIZOperationPayload_Content.OnNext; end; -function TWIZOperationPayload_Content.Validate(out message: ansistring): boolean; +function TWIZOperationPayload_Content.Validate(out message: String): boolean; begin Result := True; if Length(mmoPayload.Lines.Text) > CT_MaxPayloadSize then diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.dfm b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.dfm new file mode 100644 index 000000000..8b3c803bf --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.dfm @@ -0,0 +1,106 @@ +object WIZOperationPayload_Encryption: TWIZOperationPayload_Encryption + Left = 0 + Height = 320 + Top = 0 + Width = 511 + ActiveControl = rbEncryptedWithSender + Caption = 'Form1' + ClientHeight = 320 + ClientWidth = 511 + Visible = False + OnResize = FormResize + object grpPayload: TGroupBox + Left = 8 + Height = 311 + Top = 2 + Width = 498 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Operation Payload' + TabOrder = 0 + object lblNote1: TLabel + Left = 16 + Height = 15 + Top = 8 + Width = 343 + Caption = 'What type of payload would you like to attach to your operation?' + ParentColor = False + end + object rbNotEncrypted: TRadioButton + Left = 16 + Height = 19 + Top = 40 + Width = 98 + Caption = 'No Encryption' + Checked = True + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + TabStop = True + end + object lblNoEncryption: TLabel + Left = 32 + Height = 15 + Top = 64 + Width = 366 + Caption = 'Use this option to specify a publicly visible message anyone can view.' + ParentColor = False + end + object rbEncryptedWithRecipient: TRadioButton + Left = 16 + Height = 19 + Top = 96 + Width = 166 + Caption = 'Encrypt with recipient key' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 1 + end + object lblEncryptWithRecipientKey: TLabel + Left = 32 + Height = 15 + Top = 120 + Width = 373 + Caption = 'Use this option to specify a message which only the recipient can view.' + ParentColor = False + end + object rbEncryptedWithSender: TRadioButton + Left = 16 + Height = 19 + Top = 160 + Width = 154 + Caption = 'Encrypt with sender key' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 2 + end + object lblEncryptWithSenderKey: TLabel + Left = 32 + Height = 40 + Top = 184 + Width = 453 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Only you will be able to decrypt this message. This is useful for tagging transactions for your own record keeping purposes.' + ParentColor = False + WordWrap = True + end + object rbEncryptedWithPassword: TRadioButton + Left = 22 + Height = 19 + Top = 232 + Width = 145 + Caption = 'Encrypt with password' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 3 + end + object lblEncryptWithPassword: TLabel + Left = 32 + Height = 15 + Top = 256 + Width = 313 + Caption = 'Only those with password will be able to view this message.' + ParentColor = False + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.lfm b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.lfm index cafe7fb73..99d30a07c 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.lfm +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.lfm @@ -9,6 +9,7 @@ object WIZOperationPayload_Encryption: TWIZOperationPayload_Encryption ClientWidth = 511 LCLVersion = '1.8.2.0' Visible = False + OnResize = FormResize object grpPayload: TGroupBox Left = 8 Height = 311 diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.pas b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.pas index 206de433c..9871ee3d0 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Encryption.pas @@ -17,12 +17,30 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Selects public payloads or encryption using the sender, recipient or a + password. +} + +{ Public types in this unit + + Type | Purpose + --------------------------------+-------------------------------------------------------------------------- + TWIZOperationPayload_Encryption | Chooses payload encryption and inserts password or content entry. +} interface uses + Types, Math, Controls, StdCtrls, ExtCtrls, UWizard, UWIZOperation; type @@ -41,16 +59,21 @@ TWIZOperationPayload_Encryption = class(TWizardForm) rbEncryptedWithSender: TRadioButton; rbEncryptedWithPassword: TRadioButton; rbNotEncrypted: TRadioButton; + procedure FormResize(Sender: TObject); public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UAccounts, @@ -60,8 +83,43 @@ implementation { TWIZOperationPayload_Encryption } +type + TCaptionGroupBoxAccess = class(TGroupBox); + +procedure TWIZOperationPayload_Encryption.FormResize(Sender: TObject); + + procedure PositionContents(AGroup: TGroupBox); + var + LContent: TRect; + LPadding, LTop, LDelta, I: Integer; + LControl: TControl; + begin + LContent := AGroup.ClientRect; + TCaptionGroupBoxAccess(AGroup).AdjustClientRect(LContent); + Canvas.Font.Assign(AGroup.Font); + LPadding := Max(6, Canvas.TextHeight('0') * 2 div 3); + LTop := High(Integer); + for I := 0 to AGroup.ControlCount - 1 do + LTop := Min(LTop, AGroup.Controls[I].Top); + LDelta := LContent.Top + LPadding - LTop; + for I := 0 to AGroup.ControlCount - 1 do begin + LControl := AGroup.Controls[I]; + LControl.Top := LControl.Top + LDelta; + if (LControl is TPanel) and (akBottom in LControl.Anchors) then + LControl.Height := Max(0, LContent.Bottom - LControl.Top - LPadding); + end; + end; + +begin + if not HandleAllocated or not Assigned(lblEncryptWithPassword) then Exit; + // VCL reserves its caption inside ClientRect; LCL already excludes it. + // Use the native content inset so controls stay clear at either font scale. + PositionContents(grpPayload); +end; + procedure TWIZOperationPayload_Encryption.OnPresent; begin + FormResize(Self); case Model.ExecuteOperationType of omtChangeInfo: begin @@ -92,7 +150,7 @@ procedure TWIZOperationPayload_Encryption.OnNext; end; end; -function TWIZOperationPayload_Encryption.Validate(out message: ansistring): boolean; +function TWIZOperationPayload_Encryption.Validate(out message: String): boolean; begin Result := True; if (not rbNotEncrypted.Checked) and (not rbEncryptedWithSender.Checked) and diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.dfm b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.dfm new file mode 100644 index 000000000..af94573bb --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.dfm @@ -0,0 +1,54 @@ +object WIZOperationPayload_Password: TWIZOperationPayload_Password + Left = 0 + Height = 253 + Top = 0 + Width = 429 + ActiveControl = edtPassword + Caption = 'Form1' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + OnResize = FormResize + object grpPayload: TGroupBox + Left = 8 + Height = 244 + Top = 2 + Width = 416 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Payload Password' + TabOrder = 0 + object paPayload: TPanel + Left = 8 + Height = 214 + Top = 2 + Width = 396 + Anchors = [akTop, akLeft, akRight, akBottom] + BevelOuter = bvNone + TabOrder = 0 + object lblPassword: TLabel + Left = 3 + Height = 15 + Top = 8 + Width = 341 + Caption = 'Enter the shared secret that will enable decryption of the payload' + ParentColor = False + end + object edtPassword: TEdit + Left = 3 + Height = 23 + Top = 40 + Width = 160 + TabOrder = 0 + end + object lblNote: TLabel + Left = 8 + Height = 45 + Top = 80 + Width = 384 + Anchors = [akTop, akLeft, akRight] + Caption = 'Only the bearers of the password will be able to decrypt the '#13#10'payload contents of this operation(s). Please ensure only the appropriate '#13#10'parties are given this password.' + ParentColor = False + end + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.lfm b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.lfm index d81bebc9e..d76275e27 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.lfm +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.lfm @@ -9,6 +9,7 @@ object WIZOperationPayload_Password: TWIZOperationPayload_Password ClientWidth = 429 LCLVersion = '1.8.2.0' Visible = False + OnResize = FormResize object grpPayload: TGroupBox Left = 8 Height = 244 diff --git a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.pas b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.pas index 20de5dfc0..ea74f8da6 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperationPayload_Password.pas @@ -17,12 +17,29 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Collects a nonempty password used to encrypt an operation payload. +} + +{ Public types in this unit + + Type | Purpose + ------------------------------+-------------------------------------------------------------------------- + TWIZOperationPayload_Password | Validates a nonempty payload password before content entry. +} interface uses + Types, Math, Controls, SysUtils, StdCtrls, ExtCtrls, UWizard, UWIZOperation; type @@ -35,24 +52,64 @@ TWIZOperationPayload_Password = class(TWizardForm) lblNote: TLabel; lblPassword: TLabel; paPayload: TPanel; + procedure FormResize(Sender: TObject); public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UWIZOperationPayload_Content; { TWIZOperationPayload_Password } +type + TCaptionGroupBoxAccess = class(TGroupBox); + +procedure TWIZOperationPayload_Password.FormResize(Sender: TObject); + + procedure PositionContents(AGroup: TGroupBox); + var + LContent: TRect; + LPadding, LTop, LDelta, I: Integer; + LControl: TControl; + begin + LContent := AGroup.ClientRect; + TCaptionGroupBoxAccess(AGroup).AdjustClientRect(LContent); + Canvas.Font.Assign(AGroup.Font); + LPadding := Max(6, Canvas.TextHeight('0') * 2 div 3); + LTop := High(Integer); + for I := 0 to AGroup.ControlCount - 1 do + LTop := Min(LTop, AGroup.Controls[I].Top); + LDelta := LContent.Top + LPadding - LTop; + for I := 0 to AGroup.ControlCount - 1 do begin + LControl := AGroup.Controls[I]; + LControl.Top := LControl.Top + LDelta; + if (LControl is TPanel) and (akBottom in LControl.Anchors) then + LControl.Height := Max(0, LContent.Bottom - LControl.Top - LPadding); + end; + end; + +begin + if not HandleAllocated or not Assigned(lblNote) then Exit; + // VCL reserves its caption inside ClientRect; LCL already excludes it. + // Use the native content inset so controls stay clear at either font scale. + PositionContents(grpPayload); +end; + procedure TWIZOperationPayload_Password.OnPresent; begin + FormResize(Self); edtPassword.SetFocus; end; @@ -62,7 +119,7 @@ procedure TWIZOperationPayload_Password.OnNext; UpdatePath(ptInject, [TWIZOperationPayload_Content]); end; -function TWIZOperationPayload_Password.Validate(out message: ansistring): boolean; +function TWIZOperationPayload_Password.Validate(out message: String): boolean; begin Result := True; if Length(Trim(edtPassword.Text)) = 0 then diff --git a/src/gui-experimental/wizards/operations/UWIZOperationSelected.dfm b/src/gui-experimental/wizards/operations/UWIZOperationSelected.dfm new file mode 100644 index 000000000..ae7c9c71c --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZOperationSelected.dfm @@ -0,0 +1,52 @@ +object WIZOperationSelected: TWIZOperationSelected + Left = 0 + Height = 261 + Top = 0 + Width = 429 + Caption = 'OperationSelected' + ClientHeight = 261 + ClientWidth = 429 + Visible = False + object gpSelectedAccount: TGroupBox + Left = 4 + Height = 256 + Top = 4 + Width = 420 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Selected Account' + TabOrder = 0 + object paGrid: TPanel + Left = 8 + Height = 192 + Top = 32 + Width = 400 + Anchors = [akTop, akLeft, akRight, akBottom] + BevelOuter = bvNone + Caption = 'GRID PANEL' + TabOrder = 0 + end + object lblTotalBalances: TLabel + Left = 192 + Height = 15 + Top = 8 + Width = 116 + Alignment = taRightJustify + Anchors = [akTop, akRight] + AutoSize = False + Caption = 'Total Balance: ' + ParentColor = False + end + object lblTotalBalanceValue: TLabel + Left = 312 + Height = 15 + Top = 8 + Width = 96 + Anchors = [akTop, akRight] + AutoSize = False + Caption = 'Total Balance' + Font.Color = clGreen + ParentColor = False + ParentFont = False + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationSelected.pas b/src/gui-experimental/wizards/operations/UWIZOperationSelected.pas index 9a7f767f5..fd4869f39 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationSelected.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperationSelected.pas @@ -17,7 +17,20 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Reviews selected operation accounts and their total balance before + validating operation eligibility. +} + +{ Public types in this unit + + Type | Purpose + ----------------------+-------------------------------------------------------------------------- + TWIZOperationSelected | Displays selected accounts and checks their operation prerequisites. +} interface @@ -38,14 +51,18 @@ TWIZOperationSelected = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UCommon, @@ -81,6 +98,7 @@ procedure TWIZOperationSelected.OnPresent; LTotalBalance: int64; LCaption: string; begin + FreeAndNil(FSelectedAccountsGrid); FSelectedAccountsGrid := TVisualGrid.Create(Self); FSelectedAccountsGrid.CanSearch := False; FSelectedAccountsGrid.SortMode := smMultiColumn; @@ -114,8 +132,9 @@ procedure TWIZOperationSelected.OnPresent; end; LData := TOperationSelectedDataSource.Create(FSelectedAccountsGrid); LData.Model := Model; - FSelectedAccountsGrid.DataSource := LData; + // Synchronous fetching can measure native cells, so establish parenting first. paGrid.AddControlDockCenter(FSelectedAccountsGrid); + FSelectedAccountsGrid.DataSource := LData; LTotalBalance := 0; for LIdx := Low(Model.Account.SelectedAccounts) to High(Model.Account.SelectedAccounts) do @@ -132,11 +151,11 @@ procedure TWIZOperationSelected.OnNext; var LAllUserAccountsExcludingPending: TArray; begin - LAllUserAccountsExcludingPending := TCoreTool.GetUserAccounts(False); - Model.Signer.SignerCandidates := TCoreTool.GetSignerCandidates(Model.Account.Count, IIF(TSettings.DefaultFee = 0, 1, TSettings.DefaultFee), LAllUserAccountsExcludingPending); + LAllUserAccountsExcludingPending := TCoreTool.GetUserAccounts(True); + Model.Signer.SignerCandidates := LAllUserAccountsExcludingPending; end; -function TWIZOperationSelected.Validate(out message: ansistring): boolean; +function TWIZOperationSelected.Validate(out message: String): boolean; var LIdx: integer; LAccount: TAccount; diff --git a/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.dfm b/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.dfm new file mode 100644 index 000000000..328c75865 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.dfm @@ -0,0 +1,71 @@ +object WIZOperationSigner_Select: TWIZOperationSigner_Select + Left = 0 + Height = 253 + Top = 0 + Width = 429 + ActiveControl = cbSignerAccount + Caption = 'WIZOperationSigner_Select' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + object gbTransaction: TGroupBox + Left = 16 + Height = 232 + Top = 8 + Width = 400 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Fee Payer' + TabOrder = 0 + object cbSignerAccount: TComboBox + Left = 8 + Height = 23 + Top = 40 + Width = 168 + ItemHeight = 15 + Items.Strings = ( + '' + ) + OnChange = cbSignerAccountChange + TabOrder = 0 + Text = 'Select Signer Account' + end + object lblBalance: TLabel + Left = 200 + Height = 15 + Top = 45 + Width = 151 + Caption = 'Please Select Signer Account' + Font.Color = clRed + ParentColor = False + ParentFont = False + end + object lblNote: TLabel + Left = 8 + Height = 15 + Top = 8 + Width = 214 + Caption = 'Select the account which will pay the fee' + ParentColor = False + end + object rbPrimary: TRadioButton + Left = 8 + Height = 19 + Top = 88 + Width = 219 + Caption = 'Sign all transactions with this account' + Checked = True + OnClick = SigningModeChange + TabOrder = 2 + TabStop = True + end + object rbSecondary: TRadioButton + Left = 8 + Height = 19 + Top = 124 + Width = 307 + Caption = 'Only Sign with this account if sender cannot afford fee' + OnClick = SigningModeChange + TabOrder = 1 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.lfm b/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.lfm index a99f5457d..21783b480 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.lfm +++ b/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.lfm @@ -56,6 +56,7 @@ object WIZOperationSigner_Select: TWIZOperationSigner_Select Width = 219 Caption = 'Sign all transactions with this account' Checked = True + OnChange = SigningModeChange TabOrder = 2 TabStop = True end @@ -65,6 +66,7 @@ object WIZOperationSigner_Select: TWIZOperationSigner_Select Top = 124 Width = 307 Caption = 'Only Sign with this account if sender cannot afford fee' + OnChange = SigningModeChange TabOrder = 1 end end diff --git a/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.pas b/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.pas index 8ff6027e5..d7ac45cba 100644 --- a/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.pas +++ b/src/gui-experimental/wizards/operations/UWIZOperationSigner_Select.pas @@ -17,13 +17,30 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Selects an eligible signer and whether it pays every fee or serves as a + fallback payer. +} + +{ Public types in this unit + + Type | Purpose + ---------------------------+-------------------------------------------------------------------------- + TWIZOperationSigner_Select | Filters signers by ownership, locks and fees, then records the choice. +} interface uses - SysUtils, Graphics, StdCtrls, ExtCtrls, UWizard, UWIZOperation; + SysUtils, Graphics, StdCtrls, ExtCtrls, UCommon, UWizard, UWIZOperation, UPCDataTypes; type @@ -37,21 +54,28 @@ TWIZOperationSigner_Select = class(TWizardForm) rbPrimary: TRadioButton; rbSecondary: TRadioButton; procedure cbSignerAccountChange(Sender: TObject); - + procedure SigningModeChange(Sender: TObject); + private + FCandidates: TArray; + procedure UpdateSignerCandidates; public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UAccounts, - UPCDataTypes, + UNode, UCoreObjects; { TWIZOperationSigner_Select } @@ -67,12 +91,26 @@ procedure TWIZOperationSigner_Select.cbSignerAccountChange(Sender: TObject); begin lblBalance.Font.Color := clGreen; lblBalance.Caption := Format('%s PASC', - [TAccountComp.FormatMoney(Model.Signer.SignerCandidates[PtrInt( + [TAccountComp.FormatMoney(FCandidates[PtrInt( cbSignerAccount.Items.Objects[cbSignerAccount.ItemIndex])].Balance)]); end; end; procedure TWIZOperationSigner_Select.OnPresent; +begin + rbPrimary.Checked := Model.Signer.OperationSigningMode = akaPrimary; + rbSecondary.Checked := Model.Signer.OperationSigningMode = akaSecondary; + UpdateSignerCandidates; + cbSignerAccount.SetFocus; +end; + +procedure TWIZOperationSigner_Select.SigningModeChange(Sender: TObject); +begin + if Assigned(Model) then + UpdateSignerCandidates; +end; + +procedure TWIZOperationSigner_Select.UpdateSignerCandidates; function GetAccountNumberWithChecksum(AAccountNumber: cardinal): string; begin @@ -83,32 +121,44 @@ procedure TWIZOperationSigner_Select.OnPresent; LAccount: TAccount; LAccountNumberWithChecksum: string; LIdx: integer; + LMode: TOperationSigningMode; begin + if rbSecondary.Checked then + LMode := akaSecondary + else + LMode := akaPrimary; + FCandidates := TWIZOperationsModel.FilterSignerCandidates(Model.Signer.SignerCandidates, + Model.Account.SelectedAccounts, TNode.Node.Bank.BlocksCount, + Model.Fee.SingleOperationFee, LMode); cbSignerAccount.Items.BeginUpdate; try cbSignerAccount.Items.Clear; cbSignerAccount.Items.Add('Select Signer Account'); - for LIdx := Low(Model.Signer.SignerCandidates) to High(Model.Signer.SignerCandidates) do + for LIdx := Low(FCandidates) to High(FCandidates) do begin - LAccount := Model.Signer.SignerCandidates[LIdx]; + LAccount := FCandidates[LIdx]; LAccountNumberWithChecksum := GetAccountNumberWithChecksum(LAccount.account); cbSignerAccount.Items.AddObject(LAccountNumberWithChecksum, TObject(LIdx)); end; finally cbSignerAccount.Items.EndUpdate; end; - if Length(Model.Signer.SignerCandidates) = 1 then + if Length(FCandidates) = 1 then cbSignerAccount.ItemIndex := 1 else - cbSignerAccount.ItemIndex := Model.Signer.SelectedIndex; + begin + cbSignerAccount.ItemIndex := 0; + for LIdx := Low(FCandidates) to High(FCandidates) do + if FCandidates[LIdx].account = Model.Signer.SignerAccount.account then + cbSignerAccount.ItemIndex := LIdx + 1; + end; cbSignerAccountChange(Self); - cbSignerAccount.SetFocus; end; procedure TWIZOperationSigner_Select.OnNext; begin Model.Signer.SelectedIndex := cbSignerAccount.ItemIndex; - Model.Signer.SignerAccount := Model.Signer.SignerCandidates[PtrInt( + Model.Signer.SignerAccount := FCandidates[PtrInt( cbSignerAccount.Items.Objects[cbSignerAccount.ItemIndex])]; if rbPrimary.Checked then Model.Signer.OperationSigningMode := akaPrimary @@ -116,7 +166,7 @@ procedure TWIZOperationSigner_Select.OnNext; Model.Signer.OperationSigningMode := akaSecondary; end; -function TWIZOperationSigner_Select.Validate(out message: ansistring): boolean; +function TWIZOperationSigner_Select.Validate(out message: String): boolean; begin Result := True; if cbSignerAccount.ItemIndex < 1 then diff --git a/src/gui-experimental/wizards/operations/UWIZSendPASC.pas b/src/gui-experimental/wizards/operations/UWIZSendPASC.pas index d5b0aa1dc..7fa726048 100644 --- a/src/gui-experimental/wizards/operations/UWIZSendPASC.pas +++ b/src/gui-experimental/wizards/operations/UWIZSendPASC.pas @@ -17,7 +17,20 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Coordinates recipient and amount entry, optional fee and payload steps, + confirmation and transfer execution. +} + +{ Public types in this unit + + Type | Purpose + -------------------+-------------------------------------------------------------------------- + TWIZSendPASCWizard | Runs and executes confirmed PASC transfers from selected accounts. +} interface @@ -33,8 +46,8 @@ TWIZSendPASCWizard = class(TWizard) constructor Create(AOwner: TComponent); override; function DetermineHasNext: boolean; override; function DetermineHasPrevious: boolean; override; - function FinishRequested(out message: ansistring): boolean; override; - function CancelRequested(out message: ansistring): boolean; override; + function FinishRequested(out message: String): boolean; override; + function CancelRequested(out message: String): boolean; override; end; implementation @@ -70,16 +83,17 @@ function TWIZSendPASCWizard.DetermineHasPrevious: boolean; Result := inherited DetermineHasPrevious; end; -function TWIZSendPASCWizard.FinishRequested(out message: ansistring): boolean; +function TWIZSendPASCWizard.FinishRequested(out message: String): boolean; begin // Execute the PASC Sending here Result := TWIZOperationsHelper.ExecuteSendPASC(Model.Account.SelectedAccounts, Model.SendPASC.DestinationAccount, Model.SendPASC.SingleAmountToSend, Model.Fee.SingleOperationFee, Model.SendPASC.SendPASCMode, Model.Payload.PayloadEncryptionMode, IIF(Model.Payload.HasPayload, Model.Payload.Content, ''), Model.Payload.Password, message); - if TWIZOperationsModel.RelockOnFinish then - TWallet.Keys.LockWallet; + if Result then + Model.RelockWallet; end; -function TWIZSendPASCWizard.CancelRequested(out message: ansistring): boolean; +function TWIZSendPASCWizard.CancelRequested(out message: String): boolean; begin + Model.RelockWallet; Result := True; end; diff --git a/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.dfm b/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.dfm new file mode 100644 index 000000000..53308a015 --- /dev/null +++ b/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.dfm @@ -0,0 +1,525 @@ +object WIZSendPASC_Details: TWIZSendPASC_Details + Left = 0 + Height = 379 + Top = 0 + Width = 450 + ActiveControl = txtRecipient + Anchors = [akTop, akLeft, akBottom] + Caption = 'WIZSendPASC_Details' + ClientHeight = 379 + ClientWidth = 450 + OnResize = FormResize + object gbRecipient: TGroupBox + Left = 8 + Height = 124 + Top = 8 + Width = 431 + Anchors = [akTop, akLeft, akRight] + Caption = 'Recipient' + TabOrder = 0 + object txtRecipient: TEdit + Left = 16 + Height = 56 + Top = 32 + Width = 328 + Alignment = taRightJustify + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Font.Height = -43 + Font.Style = [fsBold] + OnChange = txtRecipientChange + ParentFont = False + TabOrder = 0 + end + object btnSearch: TSpeedButton + Left = 354 + Height = 56 + Top = 32 + Width = 56 + Anchors = [akTop, akRight] + Glyph.Data = { + 36310000424D3631000000000000360000002800000038000000380000000100 + 2000000000000031000064000000640000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002395F9572293F6CA2293F6E62294F6BA2094 + F537000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F8732294F6FE2294F7FF2294F7FF2294F7FF2294 + F7F62094F5370000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002093F6742294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F6BA0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002093F6742294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7E50000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002094 + F6752294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F6CA0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002292F6762294 + F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F9590000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002293F6772294F6FE2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F67A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000007FFF042192F5362094F6752293F7A22294F6BF2194F6D22193F6DB2294 + F7D02193F6BE2194F7A02193F6722396FA3300AAFF0300000000000000000000 + 0000000000000000000000000000000000002292F6782294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F6790000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002096F8272193 + F7912293F6E82294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7E52294F58D2491F7230000 + 00000000000000000000000000002193F6792294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F679000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002394F7242294F6AA2293F6FE2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6FC2193 + F69F1D93FE1A000000002193F6792294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F67900000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000003F7FFF042094F67C2194F6F82294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2194F6F12193F7A62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2193F6790000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002491F2152293F7C02294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2192F67A000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002094F61F2194F6DA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F67A00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002190 + F3172194F6DC2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2194F67A0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002A7FFF062194 + F7C42294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F6FE2193F7CC2194F7812193F5532391F63A1F91F4312294 + F63C2395F6572193F5892194F6D52294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194 + F67A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002294F7862294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F6FE2294F6AC2195F92E00000000000000000000000000000000000000000000 + 000000000000000000000000FF012292F63B2193F6BD2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F7A60000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002195F32E2193F6FA2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6ED2195 + F84D000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002192F7632294F6F62294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F6F11D93 + F51A000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002193F6B72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193F7E12193F8260000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002094F5372293 + F7EE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F69F000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002192F5362294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7EA2394F724000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002094 + F5372294F6F62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293 + F6FC2491F0230000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002293F7A32294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F6FE2093F44700000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002192F7632294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F78C0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000178BE70B2294F6F42294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F7A00000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002193F6BD2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7E500AAFF0300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002093F54E2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2293F6FC1F97F7200000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002292F63B2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2396F53300000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F58A2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F7B6000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000012194F6D52294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2193F67200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F6BC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2393F56D000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F5892294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F7A000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002194F6DC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1F91F538000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002392F6572294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6BD00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F6F02294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2491F51C000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002292FA3B2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F6D000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000FFFF012194F6FB2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1E96FF11000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000001F94F9302294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2193F6DB00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000FF012193F6F22294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF1D93F51A000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002393F6392294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6D200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F6DF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2293F534000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F5532294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6BF00000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002293F7C12294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2094F765000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002194F7812294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2293F7A200000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002194F7902294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2193F7AD000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002193F6CC2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2094F67500000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000002193F6552294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F6F82190F3170000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002195F92E2294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2192F53600000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000001E96F0112193F6F92294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F68F0000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002294F6AC2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2293F6E8007FFF0400000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002193F6AF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2193F6FB2293FA3400000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002195F84D2294F6FE2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2193F7910000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F7432294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F6DE2296F316000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002193 + F8262293F6ED2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293 + F6FE2096F8270000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002193F6C72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6D12296F3160000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002290F1252194 + F7E12294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7AA000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002294F63C2294F6FD2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F6DE2293 + FA34000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000002093F4472193F6EB2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F6F82394 + F724000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002193F69A2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F7FA2194F68F2190F31700000000000000000000000000000000000000000000 + 00000000000000000000000000001F97F7202194F7A02294F6FE2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2094F67C0000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002789EB0D2193 + F6D62294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2194F6F82193F7AD2092F7652396FA331D93F51A1E96F0112491 + F51C1F91F5382393F56D2194F7B62293F6FC2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F7C0007FFF040000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000002394 + F7242194F7EA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2194F7DA2491F215000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00002092F42F2194F7EA2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2194F6DC2094F61F00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000002394F7242193F6D62294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2194F7C42190F3170000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000002789EB0D2193F69A2294F6FD2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2193F6FA2294F7862A7FFF06000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000002294F63C2193F6C72294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2193 + F6B72195F32E0000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000002294F7432193 + F6AF2193F6F92294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294F7FF2294 + F7FF2294F7FF2294F7FF2294F7FF2294F7FF2293F6F42293F7A22195FA350000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00001F9FFF102193F6552194F7902293F7C12293F6DF2193F6F22194F6FB2293 + F6F02194F6DC2193F6BC2193F58A2195F84D178BE70B00000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000FF0100FFFF010000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000 + } + OnClick = btnSearchClick + end + object lblDestNotice: TLabel + Left = 16 + Height = 15 + Top = 8 + Width = 178 + Caption = 'Please enter the recipient account' + ParentColor = False + ParentFont = False + end + object lblRecipientDetails: TLabel + Left = 313 + Height = 15 + Top = 8 + Width = 94 + Anchors = [akTop, akRight] + Caption = 'Recipient Details' + Font.Style = [fsBold] + ParentColor = False + ParentFont = False + end + end + object gpAmount: TGroupBox + Left = 8 + Height = 122 + Top = 144 + Width = 431 + Anchors = [akTop, akLeft, akRight] + Caption = 'Amount' + ParentFont = False + TabOrder = 1 + object txtAmount: TEdit + Left = 16 + Height = 56 + Top = 32 + Width = 394 + Alignment = taRightJustify + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Font.Height = -43 + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + end + object lblQuantityNotice: TLabel + Left = 16 + Height = 15 + Top = 11 + Width = 181 + Caption = 'Enter the quantity of PASC to send' + ParentColor = False + end + object chkSendAll: TCheckBox + Left = 347 + Height = 19 + Top = 7 + Width = 63 + Anchors = [akTop, akRight] + Caption = 'Send All' + OnClick = chkSendAllChange + TabOrder = 1 + end + end + object gpOptions: TGroupBox + Left = 8 + Height = 89 + Top = 280 + Width = 431 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Options' + TabOrder = 2 + object chkCustomFee: TCheckBox + Left = 16 + Height = 19 + Top = 8 + Width = 164 + Caption = 'Allow me to choose the fee' + TabOrder = 0 + end + object chkPayload: TCheckBox + Left = 16 + Height = 19 + Top = 40 + Width = 158 + Caption = 'Attach a message payload' + TabOrder = 1 + end + end +end diff --git a/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.lfm b/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.lfm index 456c24fd1..a5697ed3d 100644 --- a/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.lfm +++ b/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.lfm @@ -9,6 +9,7 @@ object WIZSendPASC_Details: TWIZSendPASC_Details ClientHeight = 379 ClientWidth = 450 LCLVersion = '2.0.2.0' + OnResize = FormResize object gbRecipient: TGroupBox Left = 8 Height = 124 diff --git a/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.pas b/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.pas index c83062e22..e740972e2 100644 --- a/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.pas +++ b/src/gui-experimental/wizards/operations/UWIZSendPASC_Details.pas @@ -17,13 +17,31 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + + +{$IFDEF FPC} {$modeswitch nestedprocvars} +{$ENDIF} + +{ Collects a transfer recipient and amount or send-all choice, with optional + fee and payload settings. +} + +{ Public types in this unit + + Type | Purpose + ---------------------+-------------------------------------------------------------------------- + TWIZSendPASC_Details | Validates transfer inputs and inserts optional fee and payload steps. +} interface uses - Classes, SysUtils, Forms, Dialogs, Controls, StdCtrls, ExtCtrls, Buttons, UWizard, UWIZOperation; + Types, Math, Controls, + Classes, SysUtils, Forms, Dialogs, StdCtrls, ExtCtrls, Buttons, UWizard, UWIZOperation; type @@ -46,18 +64,24 @@ TWIZSendPASC_Details = class(TWizardForm) procedure chkSendAllChange(Sender: TObject); procedure txtRecipientChange(Sender: TObject); procedure UpdateUI(); - - + procedure FormResize(Sender: TObject); + private + FSpecifiedAmountText: String; + FPresented: Boolean; public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message: ansistring): boolean; override; + function Validate(out message: String): boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UNode, @@ -79,13 +103,70 @@ implementation { TWIZSendPASC_Details } +type + TCaptionGroupBoxAccess = class(TGroupBox); + procedure TWIZSendPASC_Details.txtRecipientChange(Sender: TObject); begin UpdateUI(); end; +procedure TWIZSendPASC_Details.FormResize(Sender: TObject); + + procedure FitEditText(AEdit: TEdit); + var + LTextHeight, LBorderHeight: Integer; + begin + Canvas.Font.Assign(AEdit.Font); + LTextHeight := Canvas.TextHeight('Mg'); + LBorderHeight := Max(4, AEdit.Height - AEdit.ClientHeight); + // The large entry font needs its full native line height plus edit borders. + AEdit.Height := Max(AEdit.Height, LTextHeight + LBorderHeight); + end; + + procedure PositionContents(AGroup: TGroupBox); + var + LContent: TRect; + LPadding, LTop, LDelta, I: Integer; + LControl: TControl; + begin + LContent := AGroup.ClientRect; + TCaptionGroupBoxAccess(AGroup).AdjustClientRect(LContent); + Canvas.Font.Assign(AGroup.Font); + LPadding := Max(6, Canvas.TextHeight('0') * 2 div 3); + LTop := High(Integer); + for I := 0 to AGroup.ControlCount - 1 do + LTop := Min(LTop, AGroup.Controls[I].Top); + LDelta := LContent.Top + LPadding - LTop; + for I := 0 to AGroup.ControlCount - 1 do begin + LControl := AGroup.Controls[I]; + LControl.Top := LControl.Top + LDelta; + if (LControl is TPanel) and (akBottom in LControl.Anchors) then + LControl.Height := Max(0, LContent.Bottom - LControl.Top - LPadding); + end; + end; + +begin + if not HandleAllocated or not Assigned(chkPayload) then Exit; + FitEditText(txtRecipient); + FitEditText(txtAmount); + btnSearch.Height := txtRecipient.Height; + // VCL reserves its caption inside ClientRect; LCL already excludes it. + // Use the native content inset so controls stay clear at either font scale. + PositionContents(gbRecipient); + PositionContents(gpAmount); + PositionContents(gpOptions); +end; + procedure TWIZSendPASC_Details.OnPresent; begin + FormResize(Self); + if not FPresented then + begin + txtAmount.Text := IIF(Model.SendPASC.SingleAmountToSend = 0, '', TAccountComp.FormatMoney(Model.SendPASC.SingleAmountToSend)); + FSpecifiedAmountText := txtAmount.Text; + FPresented := True; + end; UpdateUI(); // Recipient section @@ -117,14 +198,17 @@ procedure TWIZSendPASC_Details.UpdateUI(); // Quantity section if chkSendAll.Checked then begin + if txtAmount.Enabled then + FSpecifiedAmountText := txtAmount.Text; txtAmount.Text := 'ALL BALANCE'; txtAmount.Enabled := False; Model.SendPASC.SendPASCMode := akaAllBalance; end else begin + if not txtAmount.Enabled then + txtAmount.Text := FSpecifiedAmountText; txtAmount.Enabled := True; - txtAmount.Text := IIF(Model.SendPASC.SingleAmountToSend = 0, '', TAccountComp.FormatMoney(Model.SendPASC.SingleAmountToSend)); Model.SendPASC.SendPASCMode := akaSpecifiedAmount; end; end; @@ -169,17 +253,17 @@ procedure TWIZSendPASC_Details.OnNext; UpdatePath(ptInject, LWizStepsToInject.ToArray); end; -function TWIZSendPASC_Details.Validate(out message: ansistring): boolean; - - function GetAccountNumberWithChecksum(constref AAccount: TAccount): string; - begin - Result := AAccount.AccountString; - end; +function GetAccountNumberWithChecksum({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AAccount: TAccount): string; +begin + Result := AAccount.AccountString; +end; +function TWIZSendPASC_Details.Validate(out message: String): boolean; var + LAccountTransform: TGlobalTransformFunc; LAccountNumbersWithChecksum: TArray; LAccountNumber: cardinal; - LAmount: int64; + LAmount, LFee: int64; LIdx: integer; LAccount: TAccount; begin @@ -197,7 +281,8 @@ function TWIZSendPASC_Details.Validate(out message: ansistring): boolean; Exit(False); end; - LAccountNumbersWithChecksum := TListTool.Transform(Model.Account.SelectedAccounts, GetAccountNumberWithChecksum); + LAccountTransform := GetAccountNumberWithChecksum; + LAccountNumbersWithChecksum := TListTool.Transform(Model.Account.SelectedAccounts, LAccountTransform); if TArrayTool.Contains(LAccountNumbersWithChecksum, txtRecipient.Text) then begin @@ -220,12 +305,22 @@ function TWIZSendPASC_Details.Validate(out message: ansistring): boolean; Exit(False); end; + if chkCustomFee.Checked then + LFee := Model.Fee.SingleOperationFee + else + LFee := TSettings.DefaultFee; + if LFee < 0 then + begin + message := 'The operation fee cannot be negative.'; + Exit(False); + end; for LIdx := Low(Model.Account.SelectedAccounts) to High(Model.Account.SelectedAccounts) do begin LAccount := Model.Account.SelectedAccounts[LIdx]; - if LAccount.balance < LAmount then + if (LAccount.balance < UInt64(LAmount)) or + (LAccount.balance - UInt64(LAmount) < UInt64(LFee)) then begin - message := Format('Account balance in %s (%s PASC) is less than specified amount (%s PASC) to send', [LAccount.AccountString, TAccountComp.FormatMoney(LAccount.balance), TAccountComp.FormatMoney(LAmount)]); + message := Format('Account %s needs enough balance for %s PASC and the %s PASC fee.', [LAccount.AccountString, TAccountComp.FormatMoney(LAmount), TAccountComp.FormatMoney(LFee)]); Exit(False); end; end; diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey.pas b/src/gui-experimental/wizards/wallet/UWIZAddKey.pas index 7d3391c9a..d2cb006b8 100644 --- a/src/gui-experimental/wizards/wallet/UWIZAddKey.pas +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey.pas @@ -13,7 +13,22 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Coordinates wallet key generation and private or public key import through a + shared wizard model. +} + +{ Public types in this unit + + Type | Purpose + -----------------+-------------------------------------------------------------------------- + TWIZAddKeyAction | Selects key generation, private-key import or public-key import. + TWIZAddKeyModel | Stores the key name, import text, password, curve and chosen action. + TWIZAddKeyWizard | Runs the Add Key workflow and commits the selected key action. +} interface @@ -45,8 +60,8 @@ TWIZAddKeyWizard = class(TWizard) constructor Create(AOwner: TComponent); override; function DetermineHasNext : boolean; override; function DetermineHasPrevious : boolean; override; - function FinishRequested(out message : AnsiString) : boolean; override; - function CancelRequested(out message : AnsiString) : boolean; override; + function FinishRequested(out message : String) : boolean; override; + function CancelRequested(out message : String) : boolean; override; end; implementation @@ -77,7 +92,7 @@ function TWIZAddKeyWizard.DetermineHasPrevious : boolean; Result := inherited DetermineHasPrevious; end; -function TWIZAddKeyWizard.FinishRequested(out message : AnsiString) : boolean; +function TWIZAddKeyWizard.FinishRequested(out message : String) : boolean; begin // Execute the key addition here try @@ -95,7 +110,7 @@ function TWIZAddKeyWizard.FinishRequested(out message : AnsiString) : boolean; end; end; -function TWIZAddKeyWizard.CancelRequested(out message : AnsiString) : boolean; +function TWIZAddKeyWizard.CancelRequested(out message : String) : boolean; begin Result := true; end; diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_EnterName.dfm b/src/gui-experimental/wizards/wallet/UWIZAddKey_EnterName.dfm new file mode 100644 index 000000000..15609385f --- /dev/null +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_EnterName.dfm @@ -0,0 +1,28 @@ +object WIZAddKey_EnterName: TWIZAddKey_EnterName + Left = -665 + Height = 100 + Top = 643 + Width = 334 + Caption = 'WIZAddKey_EnterName' + ClientHeight = 100 + ClientWidth = 334 + Visible = False + object Label2: TLabel + Left = 17 + Height = 15 + Top = 16 + Width = 130 + Caption = 'Enter a name for this key' + ParentColor = False + WordWrap = True + end + object txtName: TEdit + Left = 17 + Height = 23 + Top = 40 + Width = 299 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + TabOrder = 0 + end +end diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_EnterName.pas b/src/gui-experimental/wizards/wallet/UWIZAddKey_EnterName.pas index e16d57a27..7b8cfd3b6 100644 --- a/src/gui-experimental/wizards/wallet/UWIZAddKey_EnterName.pas +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_EnterName.pas @@ -13,7 +13,19 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Collects a nonempty display name for the wallet key being added. +} + +{ Public types in this unit + + Type | Purpose + ---------------------+-------------------------------------------------------------------------- + TWIZAddKey_EnterName | Edits and validates the new wallet key's display name. +} interface @@ -30,18 +42,23 @@ TWIZAddKey_EnterName = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message : AnsiString) : boolean; override; + procedure OnPrevious; override; + function Validate(out message : String) : boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} { TWIZAddKey_EnterName } procedure TWIZAddKey_EnterName.OnPresent; begin - txtName.Clear; + txtName.Text := Model.Name; txtName.SetFocus; end; @@ -50,7 +67,12 @@ procedure TWIZAddKey_EnterName.OnNext; Model.Name := Trim(txtName.Text); end; -function TWIZAddKey_EnterName.Validate(out message : AnsiString) : boolean; +procedure TWIZAddKey_EnterName.OnPrevious; +begin + OnNext; +end; + +function TWIZAddKey_EnterName.Validate(out message : String) : boolean; begin Result := Length(Trim(txtName.Text)) > 0; if not result then diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_GenerateOrImport.dfm b/src/gui-experimental/wizards/wallet/UWIZAddKey_GenerateOrImport.dfm new file mode 100644 index 000000000..30e29f153 --- /dev/null +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_GenerateOrImport.dfm @@ -0,0 +1,65 @@ +object WIZAddKey_GenerateOrImport: TWIZAddKey_GenerateOrImport + Left = 500 + Height = 285 + Top = 383 + Width = 448 + Caption = 'Add Key' + ClientHeight = 285 + ClientWidth = 448 + Visible = False + object rbPrivateKey: TRadioButton + Left = 32 + Height = 19 + Top = 56 + Width = 175 + Caption = 'Generate a new private key' + Checked = True + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + TabStop = True + end + object rbPublicKey: TRadioButton + Left = 32 + Height = 19 + Top = 168 + Width = 188 + Caption = 'Import an existing private key' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 1 + end + object Label1: TLabel + Left = 32 + Height = 32 + Top = 16 + Width = 370 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'How will the private key be added into your wallet?' + ParentColor = False + WordWrap = True + end + object Label2: TLabel + Left = 52 + Height = 64 + Top = 80 + Width = 352 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Use this option to generate a brand new private key which will only exist on your machine. You will then be able to export this key and import it into other machines.' + ParentColor = False + WordWrap = True + end + object Label3: TLabel + Left = 48 + Height = 66 + Top = 192 + Width = 350 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Use this option to import a private key that you have exported from another wallet.' + ParentColor = False + WordWrap = True + end +end diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_GenerateOrImport.pas b/src/gui-experimental/wizards/wallet/UWIZAddKey_GenerateOrImport.pas index 57ebbfda9..f9448beee 100644 --- a/src/gui-experimental/wizards/wallet/UWIZAddKey_GenerateOrImport.pas +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_GenerateOrImport.pas @@ -13,7 +13,19 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Chooses whether to generate a private key or import an existing private key. +} + +{ Public types in this unit + + Type | Purpose + ----------------------------+-------------------------------------------------------------------------- + TWIZAddKey_GenerateOrImport | Selects generation or import and inserts the matching key step. +} interface @@ -32,11 +44,16 @@ TWIZAddKey_GenerateOrImport = class(TWizardForm) rbPublicKey: TRadioButton; public procedure OnNext; override; + procedure OnPresent; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UWIZAddKey_ImportPrivKey, @@ -54,5 +71,11 @@ procedure TWIZAddKey_GenerateOrImport.OnNext; end; end; +procedure TWIZAddKey_GenerateOrImport.OnPresent; +begin + rbPublicKey.Checked := Model.Action = akaImportPrivateKey; + rbPrivateKey.Checked := not rbPublicKey.Checked; +end; + end. diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPrivKey.dfm b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPrivKey.dfm new file mode 100644 index 000000000..c8e8208d9 --- /dev/null +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPrivKey.dfm @@ -0,0 +1,50 @@ +object WIZAddKey_ImportPrivKey: TWIZAddKey_ImportPrivKey + Left = -679 + Height = 225 + Top = 316 + Width = 438 + Caption = 'WIZAddKey_ImportPrivKey' + ClientHeight = 225 + ClientWidth = 438 + Visible = False + object Label1: TLabel + Left = 32 + Height = 32 + Top = 16 + Width = 362 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Enter the encrypted private key that you have exported from another wallet. ' + ParentColor = False + WordWrap = True + end + object Label2: TLabel + Left = 25 + Height = 24 + Top = 160 + Width = 362 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Enter the password used to encrypt the key when it was exported.' + ParentColor = False + WordWrap = True + end + object txtPassword: TEdit + Left = 24 + Height = 23 + Top = 184 + Width = 386 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + PasswordChar = '*' + TabOrder = 1 + end + object txtPrivateKey: TMemo + Left = 24 + Height = 88 + Top = 56 + Width = 385 + Anchors = [akTop, akLeft, akRight] + TabOrder = 0 + end +end diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPrivKey.pas b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPrivKey.pas index a5cbe3b41..33a7be787 100644 --- a/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPrivKey.pas +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPrivKey.pas @@ -13,7 +13,20 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Collects private-key import text and its password, then validates that the + key can be decoded. +} + +{ Public types in this unit + + Type | Purpose + -------------------------+-------------------------------------------------------------------------- + TWIZAddKey_ImportPrivKey | Edits and validates an imported private key and password. +} interface @@ -32,36 +45,47 @@ TWIZAddKey_ImportPrivKey = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message : AnsiString) : boolean; override; + procedure OnPrevious; override; + function Validate(out message : String) : boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses - UCrypto, + SysUtils, UCrypto, UWallet; { TWIZAddKey_ImportPrivKey } procedure TWIZAddKey_ImportPrivKey.OnPresent; begin - txtPrivateKey.Clear; - txtPassword.Clear; + txtPrivateKey.Text := Model.KeyText; + txtPassword.Text := Model.Password; txtPrivateKey.SetFocus; end; procedure TWIZAddKey_ImportPrivKey.OnNext; begin - Model.KeyText := txtPrivateKey.Text; + Model.KeyText := Trim(txtPrivateKey.Text); Model.Password := txtPassword.Text; end; -function TWIZAddKey_ImportPrivKey.Validate(out message : AnsiString) : boolean; +procedure TWIZAddKey_ImportPrivKey.OnPrevious; +begin + OnNext; +end; + +function TWIZAddKey_ImportPrivKey.Validate(out message : String) : boolean; var privateKey : TECPrivateKey; begin + privateKey := nil; try Result := TWallet.TryDecryptPrivateKey(txtPrivateKey.Text, txtPassword.Text, privateKey, message); finally diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPubKey.dfm b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPubKey.dfm new file mode 100644 index 000000000..98b884fc1 --- /dev/null +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPubKey.dfm @@ -0,0 +1,29 @@ +object WIZAddKey_ImportPubKey: TWIZAddKey_ImportPubKey + Left = -722 + Height = 253 + Top = 31 + Width = 429 + Caption = 'WIZAddKey_ImportPubKey' + ClientHeight = 253 + ClientWidth = 429 + Visible = False + object Label1: TLabel + Left = 32 + Height = 32 + Top = 16 + Width = 370 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Enter the public key either in the PascalCoin encoding format or raw hex format.' + ParentColor = False + WordWrap = True + end + object txtPublicKey: TMemo + Left = 24 + Height = 160 + Top = 56 + Width = 379 + Anchors = [akTop, akLeft, akRight, akBottom] + TabOrder = 0 + end +end diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPubKey.pas b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPubKey.pas index b51b4d968..67bbf4cf3 100644 --- a/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPubKey.pas +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_ImportPubKey.pas @@ -13,7 +13,19 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Collects and validates an imported public key for a watch-only wallet entry. +} + +{ Public types in this unit + + Type | Purpose + ------------------------+-------------------------------------------------------------------------- + TWIZAddKey_ImportPubKey | Edits and validates an imported public key. +} interface @@ -30,16 +42,21 @@ TWIZAddKey_ImportPubKey = class(TWizardForm) public procedure OnPresent; override; procedure OnNext; override; - function Validate(out message : AnsiString) : boolean; override; + procedure OnPrevious; override; + function Validate(out message : String) : boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses - UAccounts, + SysUtils, UWallet, UPCDataTypes; { TWIZAddKey_ImportPubKey } @@ -47,20 +64,25 @@ implementation procedure TWIZAddKey_ImportPubKey.OnPresent; begin - txtPublicKey.Clear; + txtPublicKey.Text := Model.KeyText; txtPublicKey.SetFocus; end; procedure TWIZAddKey_ImportPubKey.OnNext; begin - Model.KeyText := txtPublicKey.Text; + Model.KeyText := Trim(txtPublicKey.Text); +end; + +procedure TWIZAddKey_ImportPubKey.OnPrevious; +begin + OnNext; end; -function TWIZAddKey_ImportPubKey.Validate(out message : AnsiString) : boolean; +function TWIZAddKey_ImportPubKey.Validate(out message : String) : boolean; var accountKey : TAccountKey; begin - Result := TAccountComp.AccountPublicKeyImport(txtPublicKey.Text, accountKey, message); + Result := TWallet.TryParsePublicKey(txtPublicKey.Text, accountKey, message); end; end. diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_SelectEncryption.dfm b/src/gui-experimental/wizards/wallet/UWIZAddKey_SelectEncryption.dfm new file mode 100644 index 000000000..c9140db81 --- /dev/null +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_SelectEncryption.dfm @@ -0,0 +1,22 @@ +object WIZAddKey_SelectEncryption: TWIZAddKey_SelectEncryption + Left = -1207 + Height = 240 + Top = 90 + Width = 320 + Caption = 'WIZAddKey_SelectEncryption' + ClientHeight = 240 + ClientWidth = 320 + OnCreate = FormCreate + object rgKeyType: TRadioGroup + Left = 24 + Height = 209 + Top = 16 + Width = 278 + Anchors = [akTop, akLeft, akRight, akBottom] + Caption = 'Select the type of encryption:' + Items.Strings = ( + 'asdf' + ) + TabOrder = 0 + end +end diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_SelectEncryption.pas b/src/gui-experimental/wizards/wallet/UWIZAddKey_SelectEncryption.pas index fdf737e63..4a306d215 100644 --- a/src/gui-experimental/wizards/wallet/UWIZAddKey_SelectEncryption.pas +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_SelectEncryption.pas @@ -13,7 +13,19 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Selects a supported elliptic curve for a newly generated wallet key. +} + +{ Public types in this unit + + Type | Purpose + ----------------------------+-------------------------------------------------------------------------- + TWIZAddKey_SelectEncryption | Lists supported key curves and stores the chosen OpenSSL NID. +} interface @@ -27,15 +39,20 @@ interface TWIZAddKey_SelectEncryption = class(TWizardForm) rgKeyType: TRadioGroup; procedure FormCreate(Sender: TObject); - class function GetOptionText(const EC_OpenSSL_NID: Word): AnsiString; + class function GetOptionText(const EC_OpenSSL_NID: Word): String; public + procedure OnPresent; override; procedure OnNext; override; - function Validate(out message : AnsiString) : boolean; override; + function Validate(out message : String) : boolean; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UAccounts, UConst, UMemory, Generics.Collections; @@ -61,7 +78,23 @@ procedure TWIZAddKey_SelectEncryption.OnNext; Self.Model.EncryptionTypeNID := PtrInt(rgKeyType.Items.Objects[rgKeyType.ItemIndex]); end; -function TWIZAddKey_SelectEncryption.Validate(out message : AnsiString) : boolean; +procedure TWIZAddKey_SelectEncryption.OnPresent; +var + i: Integer; + LNID: Word; +begin + LNID := Model.EncryptionTypeNID; + if LNID = 0 then + LNID := CT_Default_EC_OpenSSL_NID; + for i := 0 to rgKeyType.Items.Count - 1 do + if PtrInt(rgKeyType.Items.Objects[i]) = LNID then + begin + rgKeyType.ItemIndex := i; + Break; + end; +end; + +function TWIZAddKey_SelectEncryption.Validate(out message : String) : boolean; begin Result := true; if rgKeyType.ItemIndex < 0 then begin @@ -71,7 +104,7 @@ function TWIZAddKey_SelectEncryption.Validate(out message : AnsiString) : boolea end; -class function TWIZAddKey_SelectEncryption.GetOptionText(const EC_OpenSSL_NID: Word): AnsiString; +class function TWIZAddKey_SelectEncryption.GetOptionText(const EC_OpenSSL_NID: Word): String; begin case EC_OpenSSL_NID of CT_NID_secp256k1 : begin diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_Start.dfm b/src/gui-experimental/wizards/wallet/UWIZAddKey_Start.dfm new file mode 100644 index 000000000..1824ff9ec --- /dev/null +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_Start.dfm @@ -0,0 +1,65 @@ +object WIZAddKey_Start: TWIZAddKey_Start + Left = 26 + Height = 300 + Top = 27 + Width = 437 + Caption = 'Add Key' + ClientHeight = 300 + ClientWidth = 437 + Visible = False + object rbPrivateKey: TRadioButton + Left = 32 + Height = 19 + Top = 56 + Width = 84 + Caption = 'Private Key' + Checked = True + Font.Style = [fsBold] + ParentFont = False + TabOrder = 1 + TabStop = True + end + object rbPublicKey: TRadioButton + Left = 32 + Height = 19 + Top = 168 + Width = 144 + Caption = 'Watch-only Public Key' + Font.Style = [fsBold] + ParentFont = False + TabOrder = 0 + end + object Label1: TLabel + Left = 32 + Height = 32 + Top = 16 + Width = 370 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'What type of key would you like to add into your wallet?' + ParentColor = False + WordWrap = True + end + object Label2: TLabel + Left = 52 + Height = 64 + Top = 80 + Width = 352 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'Adding a private key allows you to send operations from accounts bound to this key. Please ensure your wallet is adequately protected if using this option since sensitive information is stored on your machine.' + ParentColor = False + WordWrap = True + end + object Label3: TLabel + Left = 48 + Height = 66 + Top = 192 + Width = 350 + Anchors = [akTop, akLeft, akRight] + AutoSize = False + Caption = 'A watch-only key allows you to track accounts that are bound to this key but without the ability to authorize new operations. There is no sensitive information kept on your machine when using this option.' + ParentColor = False + WordWrap = True + end +end diff --git a/src/gui-experimental/wizards/wallet/UWIZAddKey_Start.pas b/src/gui-experimental/wizards/wallet/UWIZAddKey_Start.pas index 3bbba700a..f39b33597 100644 --- a/src/gui-experimental/wizards/wallet/UWIZAddKey_Start.pas +++ b/src/gui-experimental/wizards/wallet/UWIZAddKey_Start.pas @@ -13,7 +13,19 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{$IFDEF FPC} {$mode delphi} +{$ENDIF} + +{ Chooses between adding a private key and importing a watch-only public key. +} + +{ Public types in this unit + + Type | Purpose + -----------------+-------------------------------------------------------------------------- + TWIZAddKey_Start | Routes Add Key to private-key setup or public-key import. +} interface @@ -35,11 +47,16 @@ TWIZAddKey_Start = class(TWizardForm) public { public declarations } procedure OnNext; override; + procedure OnPresent; override; end; implementation +{$IFDEF FPC} {$R *.lfm} +{$ELSE} +{$R *.dfm} +{$ENDIF} uses UWIZAddKey_EnterName, @@ -53,12 +70,29 @@ implementation procedure TWIZAddKey_Start.OnNext; begin if rbPublicKey.Checked = true then begin + if Model.Action <> akaImportPublicKey then + begin + Model.KeyText := ''; + Model.Password := ''; + end; Model.Action := akaImportPublicKey; UpdatePath(ptReplaceAllNext, [TWIZAddKey_ImportPubKey, TWIZAddKey_EnterName]); end else begin + if Model.Action = akaImportPublicKey then + begin + Model.KeyText := ''; + Model.Password := ''; + Model.Action := akaGenerateKey; + end; UpdatePath(ptReplaceAllNext, [TWIZAddKey_GenerateOrImport, TWIZAddKey_ImportPrivKey]); end; end; +procedure TWIZAddKey_Start.OnPresent; +begin + rbPublicKey.Checked := Model.Action = akaImportPublicKey; + rbPrivateKey.Checked := not rbPublicKey.Checked; +end; + end. diff --git a/src/libraries/abstractmem/UFileMem.pas b/src/libraries/abstractmem/UFileMem.pas index 43348b97a..d0df0ce6f 100644 --- a/src/libraries/abstractmem/UFileMem.pas +++ b/src/libraries/abstractmem/UFileMem.pas @@ -61,6 +61,9 @@ TFileMemStats = record FFileName: String; FIsStableCache: Boolean; FIsFlushingCache : Boolean; + FUnstableMarkerPending: Boolean; + FUncachedWriteFailed, FResizeFailed: Boolean; + FFailedWriteStart, FFailedWriteEnd: Int64; FIncreaseFileBytes: Int64; {$IFDEF ABSTRACTMEM_ENABLE_STATS} FStats : TFileMemStats; @@ -71,7 +74,10 @@ TFileMemStats = record function GetMaxCacheSize: Int64; function GetMaxCacheDataBlocks: Int64; procedure SetMaxCacheDataBlocks(const Value: Int64); - procedure CacheIsNOTStable; inline; + procedure CacheIsNOTStable; + procedure FlushFile; + procedure RecordFailedWrite(APosition: Int64; ASize: Integer); + procedure ReportCloseFailure(AError: Exception); function GetUseCache: Boolean; procedure SetUseCache(const Value: Boolean); procedure SetIncreaseFileBytes(const Value: Int64); @@ -80,6 +86,9 @@ TFileMemStats = record procedure SetGridCache(const Value: Boolean); function GetDefaultCacheDataBlocksSize: Int64; protected + // Owners of a partially updated logical index can preserve its unstable + // marker instead of retrying incomplete cached work during destruction. + function ShouldFlushOnDestroy: Boolean; virtual; function AbsoluteWrite(const AAbsolutePosition : Int64; const ABuffer; ASize : Integer) : Integer; override; function AbsoluteRead(const AAbsolutePosition : Int64; var ABuffer; ASize : Integer) : Integer; override; procedure DoIncreaseSize(var ANextAvailablePos, AMaxAvailablePos : Int64; ANeedSize : Integer); override; @@ -113,6 +122,11 @@ TFileMemStats = record implementation +{$IFNDEF FPC} +uses + {$IFDEF MSWINDOWS}Windows{$ELSE}Posix.Unistd{$ENDIF}; +{$ENDIF} + {$IFDEF ABSTRACTMEM_ENABLE_STATS} { TFileMemStats } @@ -152,28 +166,51 @@ function TFileMem.AbsoluteRead(const AAbsolutePosition: Int64; var ABuffer; ASiz function TFileMem.AbsoluteWrite(const AAbsolutePosition: Int64; const ABuffer; ASize: Integer): Integer; begin if (AAbsolutePosition<0) then raise EFileMem.Create(Format('%s.AbsoluteWrite out of range %d size %d',[ClassName,AAbsolutePosition,ASize])); + // Invalidate the persisted marker before a partial write can change data. + CacheIsNOTStable; FFileStream.Position := AAbsolutePosition; Result := FFileStream.Write(ABuffer,ASize); - CacheIsNOTStable; {$IFDEF ABSTRACTMEM_ENABLE_STATS} inc(FStats.WriteCount); inc(FStats.WriteBytesCount,ASize); {$ENDIF} end; +procedure TFileMem.FlushFile; +begin + {$IFDEF FPC} + if not FileFlush(FFileStream.Handle) then RaiseLastOSError; + {$ELSE} + {$IFDEF MSWINDOWS} + if not Windows.FlushFileBuffers(FFileStream.Handle) then RaiseLastOSError; + {$ELSE} + if fsync(FFileStream.Handle)<>0 then RaiseLastOSError; + {$ENDIF} + {$ENDIF} +end; + procedure TFileMem.CacheIsNOTStable; begin - If (FIsStableCache) // Only will mark first time - And (Not FIsFlushingCache) // Only will mark when not Flushing cache - And (Assigned(FCache)) then begin + if (FIsStableCache or FUnstableMarkerPending) and (not FIsFlushingCache) and Assigned(FFileStream) then begin FIsStableCache := False; - SaveHeader; + // A failed header write/flush must be retried before ANY later payload. + // FIsStableCache alone cannot distinguish that failure from a durable zero. + FUnstableMarkerPending := True; + // Cache eviction calls this while iterating cache cells. Write the header + // directly; reentering the cache here would invalidate that iteration. + FIsFlushingCache := True; + try + SaveHeader; + FlushFile; + FUnstableMarkerPending := False; + finally + FIsFlushingCache := False; + end; end; end; constructor TFileMem.Create(const AFileName: String; AReadOnly: Boolean); var LFileMode : Integer; - LReadOnly : Boolean; begin {$IFDEF ABSTRACTMEM_ENABLE_STATS} FStats.Clear; @@ -195,27 +232,64 @@ constructor TFileMem.Create(const AFileName: String; AReadOnly: Boolean); end; FCache := TCacheMem.Create(OnCacheNeedDataProc,OnCacheSaveDataProc); - LReadOnly := True; + // Do not initialize the abstract reader from a failed stream constructor: + // that would mask the original open error with a nil-stream read exception. + FFileStream := TFileStream.Create(AFileName,LFileMode); + inherited Create(0,AReadOnly); +end; + +procedure TFileMem.ReportCloseFailure(AError: Exception); +begin + if Assigned(FCache) and Assigned(FCache.OnLog) then begin + try + FCache.OnLog(Self,'Cannot close AbstractMem file '+FFileName+': '+AError.ClassName+': '+AError.Message); + except + // A diagnostic callback must not prevent resource release while unwinding. + end; + end; +end; + +destructor TFileMem.Destroy; +begin try - FFileStream := TFileStream.Create(AFileName,LFileMode); - LReadOnly := AReadOnly; // To protect against raise exception + try + if Assigned(FFileStream) and HeaderInitialized and (not ReadOnly) and ShouldFlushOnDestroy then begin + if not FlushCache then raise EWriteError.Create('Cannot flush AbstractMem file '+FFileName); + end; + except + // Explicit Write/Flush/Save report failure to the owner. Cleanup must not + // throw it a second time and leak Self or obscure an earlier exception. + on E: EStreamError do ReportCloseFailure(E); + on E: EFileMem do ReportCloseFailure(E); + on E: EAbstractMem do ReportCloseFailure(E); + on E: EOSError do ReportCloseFailure(E); + on E: EInOutError do ReportCloseFailure(E); + end; finally - inherited Create(0,LReadOnly); + // TCacheMem.Destroy otherwise retries dirty writes implicitly. Discard only + // this retiring object's cache; the persisted unstable marker stays intact. + if Assigned(FCache) then FCache.Clear; + FreeAndNil(FCache); + try + inherited Destroy; + finally + FreeAndNil(FFileStream); + end; end; end; -destructor TFileMem.Destroy; +function TFileMem.ShouldFlushOnDestroy: Boolean; begin - if Not ReadOnly then FlushCache; - FreeAndNil(FCache); - inherited; - FreeAndNil(FFileStream); - FreeAndNil(FCache); + Result := True; end; procedure TFileMem.DoIncreaseSize(var ANextAvailablePos, AMaxAvailablePos: Int64; ANeedSize: Integer); var LBuff : TBytes; begin + // File growth/truncation is also a write boundary. A failed allocation must + // never be certified by a later close merely because no cache cells remain. + try + CacheIsNOTStable; if (ANeedSize<=0) And (AMaxAvailablePos<=0) then begin If Assigned(FCache) then FCache.Clear; FFileStream.Seek(0,soFromEnd); @@ -231,7 +305,7 @@ procedure TFileMem.DoIncreaseSize(var ANextAvailablePos, AMaxAvailablePos: Int64 if (FFileStream.Position6-AStartPos) then begin + // An old cached header must not restore the stable bit during eviction or + // a partial flush. Only FlushCache may publish that final commit marker. + SetLength(LBuffer,ASize); + Move(ABuffer,LBuffer[0],ASize); + LBuffer[6-AStartPos] := 0; + Result := inherited Write(AStartPos,LBuffer[0],ASize); + end else begin + Result := inherited Write(AStartPos,ABuffer,ASize); + end; end; function TFileMem.Read(const APosition: Int64; var ABuffer; ASize: Integer): Integer; @@ -427,7 +559,8 @@ procedure TFileMem.SetUseCache(const Value: Boolean); FLock.Acquire; Try if Not Value then begin - If Not ReadOnly then FlushCache; + if (not ReadOnly) and (not FlushCache) then + raise EWriteError.Create('Cannot disable a dirty AbstractMem cache after failed flush: '+FFileName); FreeAndNil(FCache); end else if Not Assigned(FCache) then begin FCache := TCacheMem.Create(OnCacheNeedDataProc,OnCacheSaveDataProc); @@ -442,17 +575,53 @@ procedure TFileMem.UnlockCache; FLock.Release; end; -function TFileMem.Write(const APosition: Int64; const ABuffer; ASize: Integer) : Integer; +procedure TFileMem.RecordFailedWrite(APosition: Int64; ASize: Integer); +var LEnd: Int64; begin - if (Not Assigned(FCache)) Or (FIsFlushingCache) then begin - inherited; + if (APosition<0) or (ASize<0) or (APosition>High(Int64)-ASize) then begin + FResizeFailed := True; Exit; end; + LEnd := APosition+ASize; + if not FUncachedWriteFailed then begin + FFailedWriteStart := APosition; + FFailedWriteEnd := LEnd; + end else begin + if APositionFFailedWriteEnd then FFailedWriteEnd := LEnd; + end; + FUncachedWriteFailed := True; +end; +function TFileMem.Write(const APosition: Int64; const ABuffer; ASize: Integer): Integer; +begin + if (not Assigned(FCache)) or FIsFlushingCache then begin + FLock.Acquire; + try + try + Result := inherited Write(APosition,ABuffer,ASize); + if FIsFlushingCache and (Result<>ASize) then + raise EWriteError.Create('Cannot write AbstractMem header: '+FFileName); + except + if (not FIsFlushingCache) and (not Assigned(FCache)) then RecordFailedWrite(APosition,ASize); + raise; + end; + if (not FIsFlushingCache) and (not Assigned(FCache)) then begin + if Result<>ASize then RecordFailedWrite(APosition,ASize) + else if FUncachedWriteFailed and (APosition<=FFailedWriteStart) and + (APosition<=High(Int64)-ASize) and (APosition+ASize>=FFailedWriteEnd) then + FUncachedWriteFailed := False; + end; + finally + FLock.Release; + end; + Exit; + end; CheckInitialized(True); FLock.Acquire; try FCache.SaveToCache(ABuffer,ASize,APosition,True); + Result := ASize; finally FLock.Release; end; diff --git a/src/libraries/abstractmem/UOrderedList.pas b/src/libraries/abstractmem/UOrderedList.pas index b198d16f3..6b61006e2 100644 --- a/src/libraries/abstractmem/UOrderedList.pas +++ b/src/libraries/abstractmem/UOrderedList.pas @@ -88,7 +88,11 @@ function TComparison_Integer(const ALeft, ARight: Integer): Integer; end; function TComparison_Cardinal(const ALeft, ARight: Cardinal): Integer; begin - Result := ALeft - ARight; + // A Cardinal difference can underflow or exceed Integer's signed range. + // Comparisons need its order, without narrowing either account number. + if ALeftARight then Result := 1 + else Result := 0; end; function TComparison_Word(const ALeft, ARight: Word): Integer; begin diff --git a/src/libraries/gnugettext/README.md b/src/libraries/gnugettext/README.md new file mode 100644 index 000000000..36ac3250f --- /dev/null +++ b/src/libraries/gnugettext/README.md @@ -0,0 +1,22 @@ +# GNU gettext backend + +PascalCoin uses this existing GNU gettext implementation for Delphi translation. +Add `src/libraries/gnugettext` to the Delphi unit search path. The original +authors, upstream history and redistribution terms remain in +[gnugettext.pas](gnugettext.pas); this directory retains those terms. + +The experimental wallet composes it through +[UWalletLanguages.pas](../../gui-experimental/UWalletLanguages.pas): + +```pascal +// Catalog: MyLocaleDirectory/nl/LC_MESSAGES/default.mo +TWalletLanguages.Apply('nl', MyLocaleDirectory); +``` + +The wrapper checks the requested directory using GNU's existing MO reader and +its read-only `StringCount` metadata. Header-only catalogs are excluded. GNU +gettext retains each form's original strings and owns its translation marker; +language changes retranslate from those originals. VCL application events apply +translation to existing forms, future modal forms before showing, and new +modeless or embedded forms during UI idle. Existing application subscribers are +preserved. diff --git a/src/libraries/gnugettext/gnugettext.pas b/src/libraries/gnugettext/gnugettext.pas index d4cb39a33..55e0861e7 100644 --- a/src/libraries/gnugettext/gnugettext.pas +++ b/src/libraries/gnugettext/gnugettext.pas @@ -397,6 +397,9 @@ procedure HookIntoResourceStrings (enabled:boolean=true; SupportPackages:boolean momemory: PAnsiChar; function autoswap32(i: cardinal): cardinal; function CardinalInMem(baseptr: PAnsiChar; Offset: Cardinal): Cardinal; + public + // PascalCoin: expose parsed catalog metadata without duplicating the MO reader. + property StringCount: Cardinal read N; end; {*------------------------------------------------------------------------------ @@ -4005,7 +4008,10 @@ function GetBeforeDestructionVmtAddress(AClass: TClass): PPointer; overload; {$ENDIF} asm {$IFDEF CPU386} -// lea eax, eax + VMTOFFSET TObject.BeforeDestruction TO-DO + {$IFNDEF FPC} + // Hook the actual destructor slot so freed objects leave the registry before finalization. + lea eax, eax + VMTOFFSET TObject.BeforeDestruction + {$ENDIF} {$ENDIF CPU386} {$IFDEF CPUx64} lea rax, rcx + VMTOFFSET TObject.BeforeDestruction diff --git a/src/libraries/pascalcoin/UProgressDisplay.pas b/src/libraries/pascalcoin/UProgressDisplay.pas new file mode 100644 index 000000000..2504019c6 --- /dev/null +++ b/src/libraries/pascalcoin/UProgressDisplay.pas @@ -0,0 +1,130 @@ +unit UProgressDisplay; + +{ Copyright © 2026 Herman Schoenfeld + Presentation of typed node progress for GUI, console and RPC clients. + Distributed under the MIT software license; see the accompanying LICENSE. } + +{$IFDEF FPC}{$mode delphi}{$H+}{$ENDIF} + +interface + +uses SysUtils, UAccounts, UNode; + +// Recreate the producer's original text separately from each application's +// percentage/prefix decoration; raw counters and metadata remain unchanged. +function FormatProgressMessage(const AProgress: TProgressInfo): String; +function FormatProgressInfo(const AProgress: TProgressInfo): String; +function FormatProgressLogInfo(const AProgress: TProgressInfo): String; +// A storage failure is operational state, not peer or chain progress. +function FormatStorageFailure(const AFailure: TNodeStorageFailure): String; + +implementation + +function FormatStorageFailure(const AFailure: TNodeStorageFailure): String; +var LStage: String; +begin + Result := ''; + if not AFailure.HasFailure then Exit; + case AFailure.Stage of + sfsSafeboxFlush: LStage := 'safebox flush'; + sfsCheckpoint: LStage := 'checkpoint save'; + sfsBlockStream: LStage := 'block storage'; + sfsPendingOperations: LStage := 'pending operations save'; + sfsRestore: LStage := 'local blockchain restore'; + else + LStage := 'local storage'; + end; + // Restart-only recovery is stated first so a narrow status surface cannot + // disguise the persistent error as ordinary disconnected/sync progress. + Result := 'Storage error (restart required) during '+LStage; + if AFailure.ErrorClass<>'' then Result := Result+': '+AFailure.ErrorClass; + if AFailure.ErrorMessage<>'' then Result := Result+': '+AFailure.ErrorMessage; + if AFailure.FileName<>'' then Result := Result+' ['+AFailure.FileName+']'; +end; + +function FormatProgressMessage(const AProgress: TProgressInfo): String; +begin + case AProgress.Stage of + psNone: Result := ''; + psReadingSafebox: + Result := Format('Reading Safebox block %d/%d', + [AProgress.BlockNumber, AProgress.BlockCount]); + psValidatingOperationBlocks: + Result := Format('Validating OperationBlock info %d/%d', + [AProgress.CurrentPosition, AProgress.TotalCount]); + psCheckingSafeboxIntegrity: Result := 'Checking Safebox integrity'; + psReadingBlockBatch: + Result := Format('Reading blocks from %d to %d with %d operations', + [AProgress.StartBlock, AProgress.EndBlock, AProgress.OperationsCount]); + psReadingBlockchain: + Result := Format('Reading blockchain block %d/%d', + [AProgress.BlockNumber, AProgress.BlockCount]); + psLoadingPendingOperations: Result := 'Loading saved pending operations'; + psReadingOperations: Result := Format('Reading operations %d/%d', [AProgress.CurrentPosition, AProgress.TotalCount]); + psPreparingOperations: Result := Format('Preparing operations %d/%d', [AProgress.CurrentPosition, AProgress.TotalCount]); + psValidatingSignatures: Result := Format('Validating signatures %d/%d', [AProgress.CurrentPosition, AProgress.TotalCount]); + psApplyingOperations: Result := Format('Applying operations %d/%d', [AProgress.CurrentPosition, AProgress.TotalCount]); + psFinalizingOperations: Result := Format('Finalizing operations %d/%d', [AProgress.CurrentPosition, AProgress.TotalCount]); + psFinalizingRestore: Result := 'Finalizing local restore...'; + psDownloadingSafeboxChunks: + Result := Format('Downloading Safebox chunks %d/%d', + [AProgress.CurrentPosition, AProgress.ChunksCount]); + psSavingSafebox: + Result := Format('Saving Safebox %d chunks to %s', + [AProgress.ChunksCount, ExtractFileName(AProgress.FileName)]); + psReceivedSafebox: + Result := Format('Received new safebox with %d blocks from %s', + [AProgress.BlockCount, AProgress.PeerAddress]); + psDownloadingBlockchain: + Result := Format('Downloading block %d from %s', + [AProgress.BlockNumber, AProgress.PeerAddress]); + else + Result := ''; + end; +end; + +function FormatProgressInfo(const AProgress: TProgressInfo): String; +var + LPercentage: Extended; + LNetworkPercentage: Boolean; + LPercentText: String; +begin + Result := FormatProgressMessage(AProgress); + if Result = '' then Exit; + // These two values were already complete network status strings. They did + // not pass through a progress callback's percentage/separator wrapper. + if AProgress.Stage in [psReceivedSafebox, psDownloadingBlockchain] then Exit; + + LPercentText := ''; + if AProgress.TotalCount > 0 then begin + // Keep the original arithmetic order wherever the Int64 multiply is safe. + // For wider counters use numeric real conversion, never a bit-level cast. + // The original callbacks displayed the raw ratio without clamping. + if (AProgress.CurrentPosition >= Low(Int64) div 100) and + (AProgress.CurrentPosition <= High(Int64) div 100) then + LPercentage := AProgress.CurrentPosition * 100 / AProgress.TotalCount + else begin + LPercentage := AProgress.CurrentPosition; + LPercentage := LPercentage / AProgress.TotalCount * 100; + end; + LNetworkPercentage := (AProgress.Context = pcCheckingNewSafebox) or + (AProgress.Stage in [psDownloadingSafeboxChunks, psSavingSafebox]); + if LNetworkPercentage then LPercentText := FormatFloat('0.00', LPercentage) + '%' + else LPercentText := Format('%.2f', [LPercentage]) + '%'; + end; + // The separator is intentional even for an unknown total: both original + // GUI callbacks and network wrappers retained that trailing space. + Result := Result + ' ' + LPercentText; + if AProgress.Context = pcCheckingNewSafebox then + Result := 'Checking new safebox: ' + Result; +end; + +function FormatProgressLogInfo(const AProgress: TProgressInfo): String; +begin + if AProgress.Stage = psNone then Exit(''); + // The daemon logged raw callback positions and text, not the GUI percentage. + Result := Format('Progress (%d/%d): %s', [AProgress.CurrentPosition, + AProgress.TotalCount, FormatProgressMessage(AProgress)]); +end; + +end. diff --git a/src/libraries/regex/regexpr_compilers.inc b/src/libraries/regex/regexpr_compilers.inc index 8072878e5..4b4ffee98 100644 --- a/src/libraries/regex/regexpr_compilers.inc +++ b/src/libraries/regex/regexpr_compilers.inc @@ -327,7 +327,10 @@ {$DEFINE D2} {$ENDIF} // D 10.4 -{$IFDEF VER350} +{ PascalCoin compatibility: retain these features for Delphi 11 and later. } +{$IFNDEF FPC} +{$IFDEF CONDITIONALEXPRESSIONS} +{$IF CompilerVersion >= 35.0} {$DEFINE D_105} {$DEFINE D_104} {$DEFINE D_103} @@ -351,7 +354,9 @@ {$DEFINE D4} {$DEFINE D3} {$DEFINE D2} -{$ENDIF} // D 10.5 +{$IFEND} // Delphi 11 and later +{$ENDIF} +{$ENDIF} {$IFDEF D6} diff --git a/src/libraries/sphere10/UCache.pas b/src/libraries/sphere10/UCache.pas new file mode 100644 index 000000000..f39956774 --- /dev/null +++ b/src/libraries/sphere10/UCache.pas @@ -0,0 +1,1481 @@ +unit UCache; + +{ Copyright (c) 2017 - 2018 Herman Schoenfeld + + Generic caching with expiration and capacity management. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + Acknowledgements: + - Herman Schoenfeld: main author + + THIS LICENSE HEADER MUST NOT BE REMOVED. +} + +{ Public types in this unit + + Type | Purpose + ----------------------------------------+------------------------------------------------------------------------- + ICacheBulkValueFetcher | Fills a complete batch of cache values. + ICacheSizeEstimator | Reports the capacity units used by a value. + ICacheValueFetcher | Supplies one value for a requested key. + TActionCache | Fetches and sizes individual values through callbacks. + TBulkFetchActionCache | Fills bulk cache snapshots through a callback provider. + TBulkFetchCacheBase | Loads and refreshes complete batches of values. + TCacheBase | Manages cached entries, ownership, expiration and eviction. + TCacheBulkValueFetcher | Adapts bulk-fill callbacks to a shared fetch interface. + TCacheBulkValues | Holds the dictionary filled by a bulk-fetch provider. + TCachedItem | Retains a value and its lifetime, size and access metadata. + TCacheExpirationPolicy | Selects fixed, sliding or unlimited entry lifetime. + TCacheGlobalBulkFetchProc | Defines a global bulk-fill callback. + TCacheGlobalEstimateFunc | Defines a global value-size callback. + TCacheGlobalFetchFunc | Defines a global per-key fetch callback. + TCacheItemDisposePolicy | Selects how owned cache values are released. + TCacheItemFetchingEvent | Notifies a listener before a per-key fetch. + TCacheItemRemovingEvent | Notifies a listener before an entry is removed. + TCacheItemValueEvent | Notifies a listener with a key and value. + TCacheNestedBulkFetchProc | Defines a captured bulk-fill callback. + TCacheNestedEstimateFunc | Defines a captured value-size callback. + TCacheNestedFetchFunc | Defines a captured per-key fetch callback. + TCacheNullValuePolicy | Controls admission and return of null values. + TCacheObjectBulkFetchProc | Defines an object-method bulk-fill callback. + TCacheObjectEstimateFunc | Defines an object-method value-size callback. + TCacheObjectFetchFunc | Defines an object-method per-key fetch callback. + TCacheReapPolicy | Selects eviction order when capacity is needed. + TCacheSizeEstimator | Adapts size callbacks to a shared estimation interface. + TCacheStaleValuePolicy | Controls on-demand checks for changed source values. + TCacheValueFetcher | Adapts global, object and captured callbacks to a fetch interface. + TPreloadedCache | Caches a borrowed dictionary as a fetch-once snapshot. +} + +{$IFDEF FPC} + {$MODE Delphi} + {$MODESWITCH NESTEDPROCVARS} +{$ENDIF} + +interface + +uses + {$IFDEF FPC} + Classes, SysUtils, SyncObjs, Generics.Collections, Generics.Defaults, + {$ELSE} + System.Classes, System.SysUtils, System.SyncObjs, System.Generics.Collections, + System.Generics.Defaults, System.TimeSpan, + {$ENDIF} + UCommon, UMemory; + +type + TCacheItemDisposePolicy = TDisposePolicy; + + // Cache policies are shared by every key/value specialization and cache kind. + TCacheExpirationPolicy = (epSinceFetchedTime, epSinceLastAccessedTime, epNone); + TCacheReapPolicy = (crpLeastUsed, crpLargest, crpSmallest, crpOldest, crpIdle, crpASAP, crpNone); + TCacheNullValuePolicy = (nvpCacheNormally, nvpReturnButDontCache, nvpThrow); + TCacheStaleValuePolicy = (svpAssumeNeverStale, svpCheckStaleOnDemand); + + { TCachedItem } + + TCachedItem = class(TInterfacedObject) + private + FCanPurge : boolean; + FPurged : boolean; + FRemoving : boolean; + FProtectedCount : Integer; + FInsertionOrder : QWord; + FAccessOrder : QWord; + procedure ReleaseValue; + public + FInvalidated : boolean; + FFetchedOn : TDateTime; + FLastAccessedOn : TDateTime; + FAccessedCount : DWord; + FSize : SizeInt; + FDisposePolicy : TCacheItemDisposePolicy; + FValue : TValue; + constructor Create(const AValue : TValue; ASize: SizeInt; ADisposePolicy : TCacheItemDisposePolicy); + destructor Destroy; override; + property Invalidated : boolean read FInvalidated; + property Purged : boolean read FPurged; + property CanPurge : boolean read FCanPurge write FCanPurge; + property FetchedOn : TDateTime read FFetchedOn; + property LastAccessedOn : TDateTime read FLastAccessedOn; + property AccessedCount : DWord read FAccessedCount; + property Size : SizeInt read FSize; + property Value : TValue read FValue; + end; + + // Public callback contracts are independent of a particular cache class. + TCacheItemFetchingEvent = procedure(Sender: TObject; const AKey: TKey) of object; + TCacheItemValueEvent = procedure(Sender: TObject; const AKey: TKey; const AValue: TValue) of object; + TCacheItemRemovingEvent = procedure(Sender: TObject; const AKey: TKey; AItem: TCachedItem) of object; + + ICacheValueFetcher = interface + function Fetch(const AKey: TKey): TValue; + end; + + ICacheSizeEstimator = interface + function Estimate(const AValue: TValue): SizeInt; + end; + + {$IFDEF FPC} + TCacheNestedFetchFunc = function(const AKey: TKey): TValue is nested; + TCacheNestedEstimateFunc = function(const AValue: TValue): SizeInt is nested; + {$ELSE} + TCacheNestedFetchFunc = reference to function(const AKey: TKey): TValue; + TCacheNestedEstimateFunc = reference to function(const AValue: TValue): SizeInt; + {$ENDIF} + TCacheObjectFetchFunc = function(const AKey: TKey): TValue of object; + TCacheGlobalFetchFunc = function(const AKey: TKey): TValue; + TCacheObjectEstimateFunc = function(const AValue: TValue): SizeInt of object; + TCacheGlobalEstimateFunc = function(const AValue: TValue): SizeInt; + + TCacheValueFetcher = class(TInterfacedObject, ICacheValueFetcher) + private type + GlobalFunctionValueFetcher = class(TInterfacedObject, ICacheValueFetcher) + private + FFunc: TCacheGlobalFetchFunc; + public + constructor Create(const AFunc: TCacheGlobalFetchFunc); + function Fetch(const AKey: TKey): TValue; + end; + ObjectFunctionValueFetcher = class(TInterfacedObject, ICacheValueFetcher) + private + FFunc: TCacheObjectFetchFunc; + public + constructor Create(const AFunc: TCacheObjectFetchFunc); + function Fetch(const AKey: TKey): TValue; + end; + NestedFunctionValueFetcher = class(TInterfacedObject, ICacheValueFetcher) + private + FFunc: TCacheNestedFetchFunc; + public + constructor Create(const AFunc: TCacheNestedFetchFunc); + function Fetch(const AKey: TKey): TValue; + end; + private + FFetcher: ICacheValueFetcher; + public + constructor Create(const AFetcher: ICacheValueFetcher); + function Fetch(const AKey: TKey): TValue; + class function FromNestedFunction(const AFunc: TCacheNestedFetchFunc): ICacheValueFetcher; + class function FromObjectFunction(const AFunc: TCacheObjectFetchFunc): ICacheValueFetcher; + class function FromGlobalFunction(const AFunc: TCacheGlobalFetchFunc): ICacheValueFetcher; + end; + + TCacheSizeEstimator = class(TInterfacedObject, ICacheSizeEstimator) + private type + GlobalFunctionEstimator = class(TInterfacedObject, ICacheSizeEstimator) + private + FFunc: TCacheGlobalEstimateFunc; + public + constructor Create(const AFunc: TCacheGlobalEstimateFunc); + function Estimate(const AValue: TValue): SizeInt; + end; + ObjectFunctionEstimator = class(TInterfacedObject, ICacheSizeEstimator) + private + FFunc: TCacheObjectEstimateFunc; + public + constructor Create(const AFunc: TCacheObjectEstimateFunc); + function Estimate(const AValue: TValue): SizeInt; + end; + NestedFunctionEstimator = class(TInterfacedObject, ICacheSizeEstimator) + private + FFunc: TCacheNestedEstimateFunc; + public + constructor Create(const AFunc: TCacheNestedEstimateFunc); + function Estimate(const AValue: TValue): SizeInt; + end; + private + FEstimator: ICacheSizeEstimator; + public + constructor Create(const AEstimator: ICacheSizeEstimator); + function Estimate(const AValue: TValue): SizeInt; + class function FromNestedFunction(const AFunc: TCacheNestedEstimateFunc): ICacheSizeEstimator; + class function FromObjectFunction(const AFunc: TCacheObjectEstimateFunc): ICacheSizeEstimator; + class function FromGlobalFunction(const AFunc: TCacheGlobalEstimateFunc): ICacheSizeEstimator; + end; + + // A named dictionary subclass keeps this generic contract portable to FPC. + TCacheBulkValues = class(TDictionary) + end; + ICacheBulkValueFetcher = interface + procedure Fetch(AValues: TCacheBulkValues); + end; + {$IFDEF FPC} + TCacheNestedBulkFetchProc = procedure(AValues: TCacheBulkValues) is nested; + {$ELSE} + TCacheNestedBulkFetchProc = reference to procedure(AValues: TCacheBulkValues); + {$ENDIF} + TCacheObjectBulkFetchProc = procedure(AValues: TCacheBulkValues) of object; + TCacheGlobalBulkFetchProc = procedure(AValues: TCacheBulkValues); + + TCacheBulkValueFetcher = class(TInterfacedObject, ICacheBulkValueFetcher) + private type + TGlobalFetcher = class(TInterfacedObject, ICacheBulkValueFetcher) + private + FProc: TCacheGlobalBulkFetchProc; + public + constructor Create(const AProc: TCacheGlobalBulkFetchProc); + procedure Fetch(AValues: TCacheBulkValues); + end; + TObjectFetcher = class(TInterfacedObject, ICacheBulkValueFetcher) + private + FProc: TCacheObjectBulkFetchProc; + public + constructor Create(const AProc: TCacheObjectBulkFetchProc); + procedure Fetch(AValues: TCacheBulkValues); + end; + TNestedFetcher = class(TInterfacedObject, ICacheBulkValueFetcher) + private + FProc: TCacheNestedBulkFetchProc; + public + constructor Create(const AProc: TCacheNestedBulkFetchProc); + procedure Fetch(AValues: TCacheBulkValues); + end; + private + FFetcher: ICacheBulkValueFetcher; + public + constructor Create(const AFetcher: ICacheBulkValueFetcher); + procedure Fetch(AValues: TCacheBulkValues); + class function FromNestedFunction(const AProc: TCacheNestedBulkFetchProc): ICacheBulkValueFetcher; static; + class function FromObjectFunction(const AProc: TCacheObjectBulkFetchProc): ICacheBulkValueFetcher; static; + class function FromGlobalFunction(const AProc: TCacheGlobalBulkFetchProc): ICacheBulkValueFetcher; static; + end; + + { TCacheBase } + + // Operations and callbacks run under a recursive lock. A callback may call the + // cache again, but must not destroy it. Borrowed entries/objects require caller + // synchronization when used after an operation; the value indexer copies its + // result while the lock is held. Standard Delphi events have one handler each. + TCacheBase = class(TComponent) + protected type + // Generic implementation aliases kept protected for FPC. + __TCachedItem_TValue = TCachedItem; + __TPair_TKey_TValue = TPair; + __TPair_TKey_TCachedItem = TPair; + private + FItemFetching : TCacheItemFetchingEvent; + FItemFetched : TCacheItemValueEvent; + FItemRemoving : TCacheItemRemovingEvent; + FItemRemoved : TCacheItemValueEvent; + FReapLevel : Double; + FStaleValuePolicy : TCacheStaleValuePolicy; + FLastUpdateOn : TDateTime; + FLastAccessedOn : TDateTime; + FTotalAccesses : QWord; + FOrder : QWord; + FLastResult : __TCachedItem_TValue; + FDestroying : boolean; + function GetItemCount : Integer; + function GetCachedItems : TArray<__TCachedItem_TValue>; + function GetValue(const AKey: TKey) : TValue; + function IsExpiredAt(AItem: __TCachedItem_TValue; ATimestamp: TDateTime) : boolean; + function CompareVictims(ALeft, ARight: __TCachedItem_TValue; ATimestamp: TDateTime) : Integer; + procedure SortVictims(var AEntries: TArray<__TPair_TKey_TCachedItem>; ALeft, ARight: Integer; ATimestamp: TDateTime); + function StoreValue(const AKey: TKey; const AValue: TValue; AFetched: boolean) : __TCachedItem_TValue; + function IsNullValue(const AValue: TValue) : boolean; + procedure CheckAvailable; + procedure RememberResult(AItem: __TCachedItem_TValue); + protected type + __TDictionary_TKey_TCachedItem_TValue = TDictionary; + protected + FCurrentSize : SizeInt; + FMaxCapacity : SizeInt; + FItemDisposePolicy : TCacheItemDisposePolicy; + FExpirationDuration : TTimeSpan; + FExpirationPolicy : TCacheExpirationPolicy; + FReapPolicy : TCacheReapPolicy; + FNullValuePolicy : TCacheNullValuePolicy; + FInternalStorage : __TDictionary_TKey_TCachedItem_TValue; + FLock : TCriticalSection; + function EstimateSize(const AVal: TValue) : SizeInt; virtual; abstract; + function Fetch(const AKey: TKey): TValue; virtual; abstract; + function CheckStaleness(const AKey: TKey; AItem: __TCachedItem_TValue) : boolean; virtual; + // Derived snapshot caches may prepare a complete read before delegating + // to Get. Ordinary caches retain their per-entry expiry/staleness check. + function ShouldFetch(const AKey: TKey; AItem: __TCachedItem_TValue) : boolean; virtual; + procedure MakeSpace(const requestedSpace: SizeInt); + procedure MakeSpaceFast(const requestedSpace: SizeInt); + procedure OnItemFetching(const AKey: TKey); virtual; + procedure OnItemFetched(const AKey: TKey; const AVal: TValue); virtual; + // Raising here retains the entry and capacity. Recursive removal skips it; + // replacement of the pending entry is rejected until preparation returns. + procedure OnItemRemoving(const AKey: TKey; AItem: __TCachedItem_TValue); virtual; + procedure OnItemRemoved(const AKey: TKey; const AVal: TValue); virtual; + procedure NotifyItemFetching(const AKey: TKey); + procedure NotifyItemFetched(const AKey: TKey; const AVal: TValue); + procedure NotifyItemRemoving(const AKey: TKey; AItem: __TCachedItem_TValue); + procedure NotifyItemRemoved(const AKey: TKey; const AVal: TValue); + function AddItemInternal(const AKey: TKey; const AVal: TValue) : __TCachedItem_TValue; + procedure RemoveItemInternal(const AKey: TKey); + public + constructor Create(AOwner:TComponent; const AItemDisposePolicy : TCacheItemDisposePolicy; const AReapPolicy : TCacheReapPolicy; const AExpirationPolicy : TCacheExpirationPolicy; const ANullValuePolicy : TCacheNullValuePolicy; const AMaxCapacity : SizeInt; const AExpirationDuration : TTimeSpan; AReapLevel: Double = 0.1; AStaleValuePolicy: TCacheStaleValuePolicy = svpAssumeNeverStale); overload; + destructor Destroy; override; + // Fetching precedes Fetch; Fetched follows successful admission (or an + // uncached null result). Removing precedes removal; Removed follows it. + property ItemFetching : TCacheItemFetchingEvent read FItemFetching write FItemFetching; + property ItemFetched : TCacheItemValueEvent read FItemFetched write FItemFetched; + property ItemRemoving : TCacheItemRemovingEvent read FItemRemoving write FItemRemoving; + property ItemRemoved : TCacheItemValueEvent read FItemRemoved write FItemRemoved; + property ItemCount : Integer read GetItemCount; + // Snapshot of membership. Entries are borrowed and must not be freed by callers. + property CachedItems : TArray<__TCachedItem_TValue> read GetCachedItems; + property CurrentSize : SizeInt read FCurrentSize; + property MaxCapacity : SizeInt read FMaxCapacity; + property ReapLevel : Double read FReapLevel; + // LastUpdateOn tracks explicit writes. FetchedOn begins after Fetch; + // LastAccessedOn is updated after the fetched notification completes. + property LastUpdateOn : TDateTime read FLastUpdateOn; + property LastAccessedOn : TDateTime read FLastAccessedOn; + property TotalAccesses : QWord read FTotalAccesses; + property ExpirationDuration : TTimeSpan read FExpirationDuration; + property ExpirationPolicy : TCacheExpirationPolicy read FExpirationPolicy; + property ReapPolicy : TCacheReapPolicy read FReapPolicy; + property NullValuePolicy : TCacheNullValuePolicy read FNullValuePolicy; + property StaleValuePolicy : TCacheStaleValuePolicy read FStaleValuePolicy; + + // Borrowed until removal; the most recent Get result also survives until the next Get. + function Get(const AKey: TKey) : __TCachedItem_TValue; virtual; + // Admission transfers ownership according to the configured disposal + // policy. A rejected explicit value remains owned by its caller; rejected + // fetched values are disposed by the cache. Set preserves FetchedOn. + procedure SetValue(const AKey: TKey; const AValue: TValue); virtual; + function ContainsCachedItem(const AKey : TKey) : boolean; virtual; + function IsExpired(ACachedItem: __TCachedItem_TValue) : boolean; + function IsStale(const AKey: TKey; AItem: __TCachedItem_TValue) : boolean; + procedure BulkLoad(const ABulkValues : TEnumerable<__TPair_TKey_TValue>); + // Copies current values; object payloads remain borrowed from their owner. + function GetAllCachedValues : TArray; virtual; + procedure Invalidate(const AKey: TKey); + procedure Remove(const AKey: TKey); virtual; + // Reap respects pins and selects expired entries first. crpNone reaps only + // expired/invalidated entries; explicit Remove/Purge ignore pins. + function Reap(AMinimumRelease: SizeInt) : SizeInt; virtual; + // Purge/Flush remove only original membership, retaining callback additions. + procedure Purge; virtual; + procedure Flush; + property Values[const AKey: TKey] : TValue read GetValue write SetValue; default; + end; + + { TActionCache } + + TActionCache = class(TCacheBase) + private + FValueFetcher: ICacheValueFetcher; + FSizeEstimator: ICacheSizeEstimator; + protected + function EstimateSize(const AVal: TValue): SizeInt; override; + function Fetch(const AKey: TKey): TValue; override; + public + constructor Create(AOwner: TComponent; const AValueFetcher: ICacheValueFetcher; const ASizeEstimator: ICacheSizeEstimator; const AItemDisposePolicy: TCacheItemDisposePolicy; const AReapPolicy: TCacheReapPolicy; const AExpirationPolicy: TCacheExpirationPolicy; const ANullPolicy: TCacheNullValuePolicy; const AMaxCapacity: SizeInt; const AExpirationDuration: TTimeSpan; AReapLevel: Double = 0.1; AStaleValuePolicy: TCacheStaleValuePolicy = svpAssumeNeverStale); overload; + end; + + { A whole-source cache. A first read loads every entry; an ordinary missing, + expired or stale read reloads once. FetchOnceOnly retains its initial + snapshot until explicit ForceRefresh or Purge. Payload objects are borrowed; + the fetch callback fills a temporary dictionary owned by the cache. } + TBulkFetchCacheBase = class(TCacheBase) + private + FFetchOnceOnly, FLoaded, FRefreshing: Boolean; + FFetchCount: Cardinal; + FKeyComparer: IEqualityComparer; + protected + function EstimateSize(const AVal: TValue) : SizeInt; override; + function Fetch(const AKey: TKey): TValue; override; + function ShouldFetch(const AKey: TKey; AItem: TCachedItem) : boolean; override; + procedure BulkFetch(AValues: TCacheBulkValues); virtual; abstract; + public + constructor Create(AOwner: TComponent; const AExpirationPolicy: TCacheExpirationPolicy; const AExpirationDuration: TTimeSpan; AFetchOnceOnly: Boolean = false; ANullValuePolicy: TCacheNullValuePolicy = nvpCacheNormally; AStaleValuePolicy: TCacheStaleValuePolicy = svpAssumeNeverStale; const AKeyComparer: IEqualityComparer = nil); overload; + function Get(const AKey: TKey): TCachedItem; override; + function ContainsCachedItem(const AKey: TKey): Boolean; override; + function GetAllCachedValues: TArray; override; + // Purge-first refresh: a failed provider leaves no previous snapshot. + procedure ForceRefresh; + procedure Purge; override; + procedure Remove(const AKey: TKey); override; + // Whole snapshots have zero-sized entries; per-entry reaping cannot release + // capacity and must not turn an otherwise retained snapshot into partial data. + function Reap(AMinimumRelease: SizeInt): SizeInt; override; + property FetchOnceOnly: Boolean read FFetchOnceOnly; + property FetchCount: Cardinal read FFetchCount; + end; + + TBulkFetchActionCache = class(TBulkFetchCacheBase) + private + FFetcher: ICacheBulkValueFetcher; + protected + procedure BulkFetch(AValues: TCacheBulkValues); override; + public + constructor Create(AOwner: TComponent; const AFetcher: ICacheBulkValueFetcher; const AExpirationPolicy: TCacheExpirationPolicy; const AExpirationDuration: TTimeSpan; AFetchOnceOnly: Boolean = false; ANullValuePolicy: TCacheNullValuePolicy = nvpCacheNormally; AStaleValuePolicy: TCacheStaleValuePolicy = svpAssumeNeverStale; const AKeyComparer: IEqualityComparer = nil); overload; + end; + + { Borrows both the supplied dictionary and its object values. Keep the source + alive until this cache is freed. ForceRefresh sees edits made to the source. } + TPreloadedCache = class(TBulkFetchCacheBase) + private + FValues: TDictionary; + protected + procedure BulkFetch(AValues: TCacheBulkValues); override; + public + constructor Create(AOwner: TComponent; AValues: TDictionary; const AKeyComparer: IEqualityComparer = nil); overload; + end; + +implementation + +uses + {$IFDEF FPC}TypInfo, Math{$ELSE}System.TypInfo, System.Math{$ENDIF}; + +{ TCachedItem } + +constructor TCachedItem.Create(const AValue : TValue; ASize: SizeInt; ADisposePolicy : TCacheItemDisposePolicy); +begin + inherited Create; + FCanPurge := true; + FFetchedOn := Now; + FLastAccessedOn := FFetchedOn; + FSize := ASize; + FDisposePolicy := ADisposePolicy; + FValue := AValue; +end; + +procedure TCachedItem.ReleaseValue; +begin + case FDisposePolicy of + idpFree, idpFreeAndNil: if PTypeInfo(TypeInfo(TValue))^.Kind = tkClass then + TObject(PPointer(@FValue)^).Free; + idpFreeMem: if PTypeInfo(TypeInfo(TValue))^.Kind = tkPointer then + System.FreeMem(PPointer(@FValue)^); + end; + FValue := Default(TValue); +end; + +destructor TCachedItem.Destroy; +begin + ReleaseValue; + inherited; +end; + +{ TCacheBase } + +constructor TCacheBase.Create(AOwner:TComponent; const AItemDisposePolicy : TCacheItemDisposePolicy; const AReapPolicy : TCacheReapPolicy; const AExpirationPolicy : TCacheExpirationPolicy; const ANullValuePolicy : TCacheNullValuePolicy; const AMaxCapacity : SizeInt; const AExpirationDuration : TTimeSpan; AReapLevel: Double; AStaleValuePolicy: TCacheStaleValuePolicy); +begin + inherited Create(AOwner); + if AMaxCapacity < 0 then + raise EArgumentException.Create('Cache capacity must be nonnegative.'); + if not ((AReapLevel > 0) and (AReapLevel < 1)) then + raise EArgumentException.Create('Reap level must be greater than zero and less than one.'); + if AExpirationDuration < TTimeSpan.FromMilliseconds(0) then + raise EArgumentException.Create('Expiration duration must be nonnegative.'); + if (AItemDisposePolicy in [idpFree, idpFreeAndNil]) and (PTypeInfo(TypeInfo(TValue))^.Kind <> tkClass) then + raise EArgumentException.Create('Object disposal requires an object-valued cache.'); + if (AItemDisposePolicy = idpFreeMem) and (PTypeInfo(TypeInfo(TValue))^.Kind <> tkPointer) then + raise EArgumentException.Create('Memory disposal requires a pointer-valued cache.'); + FItemDisposePolicy := AItemDisposePolicy; + FNullValuePolicy := ANullValuePolicy; + FInternalStorage := __TDictionary_TKey_TCachedItem_TValue.Create; + FReapPolicy := AReapPolicy; + FExpirationPolicy := AExpirationPolicy; + FMaxCapacity := AMaxCapacity; + FExpirationDuration := AExpirationDuration; + FReapLevel := AReapLevel; + FStaleValuePolicy := AStaleValuePolicy; + FLastUpdateOn := Now; + FLastAccessedOn := FLastUpdateOn; + FLock := TCriticalSection.Create; +end; + +destructor TCacheBase.Destroy; +var + item : __TCachedItem_TValue; +begin + FDestroying := true; + // Destruction releases ownership without invoking application callbacks. + if Assigned(FInternalStorage) then begin + for item in FInternalStorage.Values do + item._Release; + FreeAndNil(FInternalStorage); + end; + if Assigned(FLastResult) then begin + FLastResult._Release; + FLastResult := nil; + end; + FreeAndNil(FLock); + inherited; +end; + +procedure TCacheBase.CheckAvailable; +begin + if FDestroying then + raise EInvalidOperation.Create('The cache is being destroyed.'); +end; + +function TCacheBase.GetItemCount : Integer; +begin + FLock.Acquire; + try + Result := FInternalStorage.Count; + finally + FLock.Release; + end; +end; + +function TCacheBase.GetCachedItems : TArray<__TCachedItem_TValue>; +begin + FLock.Acquire; + try + Result := FInternalStorage.Values.ToArray; + finally + FLock.Release; + end; +end; + +function TCacheBase.GetValue(const AKey: TKey) : TValue; +begin + FLock.Acquire; + try + Result := Get(AKey).Value; + finally + FLock.Release; + end; +end; + +procedure TCacheBase.RememberResult(AItem: __TCachedItem_TValue); +var + previous : __TCachedItem_TValue; +begin + AItem._AddRef; + previous := FLastResult; + FLastResult := AItem; + if Assigned(previous) then + previous._Release; +end; + +function TCacheBase.IsNullValue(const AValue: TValue) : boolean; +begin + Result := false; + case PTypeInfo(TypeInfo(TValue))^.Kind of + tkClass, tkInterface, tkPointer, tkClassRef, {$IFDEF FPC}tkProcVar{$ELSE}tkProcedure{$ENDIF}: + Result := PPointer(@AValue)^ = nil; + end; +end; + +function TCacheBase.Get(const AKey: TKey) : __TCachedItem_TValue; +var + item, current, original : __TCachedItem_TValue; + value : TValue; + requiresFetch : boolean; +begin + original := nil; + FLock.Acquire; + try + CheckAvailable; + requiresFetch := not FInternalStorage.TryGetValue(AKey, item); + if not requiresFetch then begin + // A staleness hook may remove this entry and reenter Get, replacing the + // retained last result. Keep our own reference until this operation ends. + original := item; + original._AddRef; + requiresFetch := ShouldFetch(AKey, item); + end; + if requiresFetch then begin + RemoveItemInternal(AKey); + NotifyItemFetching(AKey); + value := Fetch(AKey); + if IsNullValue(value) and (FNullValuePolicy = nvpThrow) then + raise EInvalidOperation.Create('Cache fetched a null value and its NullValuePolicy prohibits null values.'); + if IsNullValue(value) and (FNullValuePolicy = nvpReturnButDontCache) then begin + item := __TCachedItem_TValue.Create(value, 0, FItemDisposePolicy); + item.FInvalidated := true; + item.FCanPurge := false; + end else begin + try + item := StoreValue(AKey, value, true); + except + // A fetched value has no external owner when admission fails. + item := __TCachedItem_TValue.Create(value, 0, FItemDisposePolicy); + item.Free; + raise; + end; + end; + item._AddRef; + try + NotifyItemFetched(AKey, value); + RememberResult(item); + finally + item._Release; + end; + end else + RememberResult(item); + if item.FAccessedCount < High(DWord) then + Inc(item.FAccessedCount); + if FTotalAccesses < High(QWord) then + Inc(FTotalAccesses); + FLastAccessedOn := Now; + item.FLastAccessedOn := FLastAccessedOn; + Inc(FOrder); + item.FAccessOrder := FOrder; + if IsNullValue(item.Value) then begin + if FNullValuePolicy = nvpThrow then + raise EInvalidOperation.Create('Cache returned a null value and its NullValuePolicy prohibits null values.'); + if FNullValuePolicy = nvpReturnButDontCache then begin + if FInternalStorage.TryGetValue(AKey, current) and (current = item) then + RemoveItemInternal(AKey); + item.FInvalidated := true; + end; + end; + Result := item; + finally + try + if Assigned(original) then + original._Release; + finally + FLock.Release; + end; + end; +end; + +function TCacheBase.StoreValue(const AKey: TKey; const AValue: TValue; AFetched: boolean) : __TCachedItem_TValue; +var + item, original : __TCachedItem_TValue; + size, delta : SizeInt; + timestamp : TDateTime; + sameObject : boolean; + value : TValue; +begin + value := AValue; + size := EstimateSize(value); + if size < 0 then + raise EInvalidOperation.Create('A cache size estimator returned a negative size.'); + if size > FMaxCapacity then + raise EInvalidOperation.CreateFmt('Cache capacity insufficient for requested space [%d].', [size]); + timestamp := Now; + original := nil; + FInternalStorage.TryGetValue(AKey, original); + if Assigned(original) and original.FRemoving then + raise EInvalidOperation.Create('Cannot replace an entry during removal preparation.'); + if Assigned(original) then begin + original._AddRef; + Inc(original.FProtectedCount); + end; + try + delta := size; + if Assigned(original) then + Dec(delta, original.Size); + MakeSpace(delta); + item := nil; + FInternalStorage.TryGetValue(AKey, item); + delta := size; + if Assigned(item) then + Dec(delta, item.Size); + if delta > FMaxCapacity - FCurrentSize then + raise EInvalidOperation.Create('A cache callback left insufficient capacity for replacement.'); + if not Assigned(item) then begin + item := __TCachedItem_TValue.Create(value, size, FItemDisposePolicy); + item._AddRef; + try + FInternalStorage.Add(AKey, item); + except + // Admission failed: ownership of AValue remains with the caller. + item.FDisposePolicy := idpNone; + item._Release; + raise; + end; + Inc(FOrder); + item.FInsertionOrder := FOrder; + item.FFetchedOn := timestamp; + end else begin + sameObject := false; + if PTypeInfo(TypeInfo(TValue))^.Kind in [tkClass, tkPointer] then + sameObject := PPointer(@item.FValue)^ = PPointer(@value)^; + if not sameObject then + item.ReleaseValue; + item.FValue := value; + item.FSize := size; + end; + if AFetched then + item.FFetchedOn := timestamp; + item.FInvalidated := false; + item.FPurged := false; + item.FLastAccessedOn := timestamp; + Inc(FOrder); + item.FAccessOrder := FOrder; + Inc(FCurrentSize, delta); + Result := item; + finally + if Assigned(original) then begin + Dec(original.FProtectedCount); + original._Release; + end; + end; +end; + +procedure TCacheBase.SetValue(const AKey: TKey; const AValue: TValue); +begin + FLock.Acquire; + try + CheckAvailable; + StoreValue(AKey, AValue, false); + FLastUpdateOn := Now; + FLastAccessedOn := FLastUpdateOn; + finally + FLock.Release; + end; +end; + +function TCacheBase.ContainsCachedItem(const AKey: TKey) : boolean; +var + item : __TCachedItem_TValue; +begin + FLock.Acquire; + try + CheckAvailable; + Result := FInternalStorage.TryGetValue(AKey, item); + if Result then + Result := not IsExpired(item); + finally + FLock.Release; + end; +end; + +function TCacheBase.IsExpired(ACachedItem: __TCachedItem_TValue) : boolean; +begin + Result := IsExpiredAt(ACachedItem, Now); +end; + +function TCacheBase.IsExpiredAt(AItem: __TCachedItem_TValue; ATimestamp: TDateTime) : boolean; +var + timestamp : TDateTime; +begin + if not Assigned(AItem) then + raise EArgumentNilException.Create('AItem'); + Result := AItem.Invalidated; + if Result or (FExpirationPolicy = epNone) then + exit; + if FExpirationPolicy = epSinceFetchedTime then + timestamp := AItem.FetchedOn + else + timestamp := AItem.LastAccessedOn; + Result := TTimeSpan.Subtract(ATimestamp, timestamp) > FExpirationDuration; +end; + +function TCacheBase.CheckStaleness(const AKey: TKey; AItem: __TCachedItem_TValue) : boolean; +begin + Result := false; +end; + +function TCacheBase.IsStale(const AKey: TKey; AItem: __TCachedItem_TValue) : boolean; +begin + Result := (FStaleValuePolicy = svpCheckStaleOnDemand) and CheckStaleness(AKey, AItem); +end; + +procedure TCacheBase.Invalidate(const AKey: TKey); +var + item : __TCachedItem_TValue; +begin + FLock.Acquire; + try + CheckAvailable; + if FInternalStorage.TryGetValue(AKey, item) then + item.FInvalidated := true; + finally + FLock.Release; + end; +end; + +procedure TCacheBase.Remove(const AKey: TKey); +begin + FLock.Acquire; + try + CheckAvailable; + RemoveItemInternal(AKey); + finally + FLock.Release; + end; +end; + +procedure TCacheBase.RemoveItemInternal(const AKey: TKey); +var + item : __TCachedItem_TValue; +begin + if not FInternalStorage.TryGetValue(AKey, item) then + exit; + if item.FRemoving then + exit; + item.FRemoving := true; + try + NotifyItemRemoving(AKey, item); + finally + item.FRemoving := false; + end; + FInternalStorage.Remove(AKey); + Dec(FCurrentSize, item.Size); + item.FPurged := true; + item.FCanPurge := false; + try + NotifyItemRemoved(AKey, item.Value); + finally + try + if FItemDisposePolicy <> idpNone then + item.ReleaseValue; + finally + item._Release; + end; + end; +end; + +procedure TCacheBase.Purge; +var + entries : TArray<__TPair_TKey_TCachedItem>; + entry : __TPair_TKey_TCachedItem; + current : __TCachedItem_TValue; +begin + FLock.Acquire; + try + CheckAvailable; + entries := FInternalStorage.ToArray; + for entry in entries do + entry.Value._AddRef; + try + for entry in entries do + if FInternalStorage.TryGetValue(entry.Key, current) and (current = entry.Value) then + RemoveItemInternal(entry.Key); + finally + for entry in entries do + entry.Value._Release; + end; + finally + FLock.Release; + end; +end; + +procedure TCacheBase.Flush; +begin + Purge; +end; + +function TCacheBase.ShouldFetch(const AKey: TKey; AItem: __TCachedItem_TValue): boolean; +begin + Result := (not AItem.FRemoving) and (IsExpired(AItem) or IsStale(AKey, AItem)); +end; + +function TCacheBase.GetAllCachedValues: TArray; +var item: __TCachedItem_TValue; i: Integer; +begin + FLock.Acquire; + try + CheckAvailable; + SetLength(Result,FInternalStorage.Count); + i := 0; + for item in FInternalStorage.Values do begin + Result[i] := item.Value; + Inc(i); + end; + finally + FLock.Release; + end; +end; + +procedure TCacheBase.BulkLoad(const ABulkValues: TEnumerable<__TPair_TKey_TValue>); +var + pair : __TPair_TKey_TValue; +begin + if not Assigned(ABulkValues) then + raise EArgumentNilException.Create('ABulkValues'); + FLock.Acquire; + try + CheckAvailable; + for pair in ABulkValues do + SetValue(pair.Key, pair.Value); + finally + FLock.Release; + end; +end; + +function TCacheBase.CompareVictims(ALeft, ARight: __TCachedItem_TValue; ATimestamp: TDateTime) : Integer; +var + leftExpired, rightExpired : boolean; +begin + leftExpired := IsExpiredAt(ALeft, ATimestamp); + rightExpired := IsExpiredAt(ARight, ATimestamp); + if leftExpired <> rightExpired then begin + if leftExpired then Result := -1 else Result := 1; + exit; + end; + Result := 0; + case FReapPolicy of + crpLeastUsed: begin + if ALeft.AccessedCount < ARight.AccessedCount then Result := -1; + if ALeft.AccessedCount > ARight.AccessedCount then Result := 1; + if Result = 0 then begin + if ALeft.Size > ARight.Size then Result := -1; + if ALeft.Size < ARight.Size then Result := 1; + end; + end; + crpOldest: begin + if ALeft.FInsertionOrder < ARight.FInsertionOrder then Result := -1; + if ALeft.FInsertionOrder > ARight.FInsertionOrder then Result := 1; + end; + crpIdle: begin + if ALeft.FAccessOrder < ARight.FAccessOrder then Result := -1; + if ALeft.FAccessOrder > ARight.FAccessOrder then Result := 1; + end; + crpLargest: begin + if ALeft.Size > ARight.Size then Result := -1; + if ALeft.Size < ARight.Size then Result := 1; + end; + crpSmallest: begin + if ALeft.Size < ARight.Size then Result := -1; + if ALeft.Size > ARight.Size then Result := 1; + end; + end; + if Result = 0 then begin + if ALeft.FInsertionOrder < ARight.FInsertionOrder then Result := -1; + if ALeft.FInsertionOrder > ARight.FInsertionOrder then Result := 1; + end; +end; + +procedure TCacheBase.SortVictims(var AEntries: TArray<__TPair_TKey_TCachedItem>; ALeft, ARight: Integer; ATimestamp: TDateTime); +var + i, j : Integer; + pivot : __TCachedItem_TValue; + temp : __TPair_TKey_TCachedItem; +begin + if ALeft >= ARight then + exit; + i := ALeft; + j := ARight; + pivot := AEntries[ALeft + (ARight - ALeft) div 2].Value; + repeat + while CompareVictims(AEntries[i].Value, pivot, ATimestamp) < 0 do Inc(i); + while CompareVictims(AEntries[j].Value, pivot, ATimestamp) > 0 do Dec(j); + if i <= j then begin + temp := AEntries[i]; + AEntries[i] := AEntries[j]; + AEntries[j] := temp; + Inc(i); + Dec(j); + end; + until i > j; + if ALeft < j then SortVictims(AEntries, ALeft, j, ATimestamp); + if i < ARight then SortVictims(AEntries, i, ARight, ATimestamp); +end; + +function TCacheBase.Reap(AMinimumRelease: SizeInt) : SizeInt; +var + entries : TArray<__TPair_TKey_TCachedItem>; + entry : __TPair_TKey_TCachedItem; + current : __TCachedItem_TValue; + before : SizeInt; + timestamp : TDateTime; +begin + if AMinimumRelease < 0 then + raise EArgumentException.Create('The requested release must be nonnegative.'); + FLock.Acquire; + try + CheckAvailable; + Result := 0; + if AMinimumRelease = 0 then + exit; + before := FCurrentSize; + timestamp := Now; + entries := FInternalStorage.ToArray; + for entry in entries do + entry.Value._AddRef; + try + // Equal-sized and equal-time victims are ordered by insertion. + if FReapPolicy <> crpNone then + SortVictims(entries, 0, High(entries), timestamp); + for entry in entries do begin + if before - FCurrentSize >= AMinimumRelease then + break; + if not FInternalStorage.TryGetValue(entry.Key, current) then + continue; + if (current <> entry.Value) or (not current.CanPurge) or current.FRemoving or (current.FProtectedCount > 0) then + continue; + if (FReapPolicy = crpNone) and (not IsExpired(current)) then + continue; + RemoveItemInternal(entry.Key); + end; + Result := Max(0, before - FCurrentSize); + finally + for entry in entries do + entry.Value._Release; + end; + finally + FLock.Release; + end; +end; + +procedure TCacheBase.MakeSpace(const requestedSpace: SizeInt); +var + deficit, target : SizeInt; + fraction : Int64; +begin + if requestedSpace <= FMaxCapacity - FCurrentSize then + exit; + deficit := requestedSpace - (FMaxCapacity - FCurrentSize); + fraction := Ceil(FMaxCapacity * FReapLevel); + if fraction >= FMaxCapacity then + target := FMaxCapacity + else + target := Max(1, fraction); + Reap(Max(deficit, target)); + if requestedSpace > FMaxCapacity - FCurrentSize then + raise EInvalidOperation.CreateFmt('Insufficient cache capacity (requested space [%d]).', [requestedSpace]); +end; + +procedure TCacheBase.MakeSpaceFast(const requestedSpace: SizeInt); +begin + MakeSpace(requestedSpace); +end; + +function TCacheBase.AddItemInternal(const AKey: TKey; const AVal: TValue) : __TCachedItem_TValue; +begin + Result := StoreValue(AKey, AVal, true); +end; + +procedure TCacheBase.OnItemFetching(const AKey: TKey); +begin +end; + +procedure TCacheBase.OnItemFetched(const AKey: TKey; const AVal: TValue); +begin +end; + +procedure TCacheBase.OnItemRemoving(const AKey: TKey; AItem: __TCachedItem_TValue); +begin +end; + +procedure TCacheBase.OnItemRemoved(const AKey: TKey; const AVal: TValue); +begin +end; + +procedure TCacheBase.NotifyItemFetching(const AKey: TKey); +begin + OnItemFetching(AKey); + if Assigned(FItemFetching) then + FItemFetching(Self, AKey); +end; + +procedure TCacheBase.NotifyItemFetched(const AKey: TKey; const AVal: TValue); +begin + OnItemFetched(AKey, AVal); + if Assigned(FItemFetched) then + FItemFetched(Self, AKey, AVal); +end; + +procedure TCacheBase.NotifyItemRemoving(const AKey: TKey; AItem: __TCachedItem_TValue); +begin + OnItemRemoving(AKey, AItem); + if Assigned(FItemRemoving) then + FItemRemoving(Self, AKey, AItem); +end; + +procedure TCacheBase.NotifyItemRemoved(const AKey: TKey; const AVal: TValue); +begin + OnItemRemoved(AKey, AVal); + if Assigned(FItemRemoved) then + FItemRemoved(Self, AKey, AVal); +end; + +{ TActionCache } + +constructor TActionCache.Create(AOwner:TComponent; const AValueFetcher : ICacheValueFetcher; const ASizeEstimator : ICacheSizeEstimator; const AItemDisposePolicy : TCacheItemDisposePolicy; const AReapPolicy : TCacheReapPolicy; const AExpirationPolicy : TCacheExpirationPolicy; const ANullPolicy : TCacheNullValuePolicy; const AMaxCapacity : SizeInt; const AExpirationDuration : TTimeSpan; AReapLevel: Double; AStaleValuePolicy: TCacheStaleValuePolicy); +begin + inherited Create(AOwner, AItemDisposePolicy, AReapPolicy, AExpirationPolicy, ANullPolicy, AMaxCapacity, AExpirationDuration, AReapLevel, AStaleValuePolicy); + if not Assigned(AValueFetcher) then + raise EArgumentNilException.Create('AValueFetcher'); + FValueFetcher := AValueFetcher; + FSizeEstimator := ASizeEstimator; +end; + +function TActionCache.Fetch(const AKey: TKey): TValue; +begin + Result := FValueFetcher.Fetch(AKey); +end; + +function TActionCache.EstimateSize(const AVal: TValue) : SizeInt; +begin + if Assigned(FSizeEstimator) then + Result := FSizeEstimator.Estimate(AVal) + else + Result := 0; +end; + +constructor TCacheValueFetcher.Create(const AFetcher: ICacheValueFetcher); +begin + inherited Create; + if not Assigned(AFetcher) then + raise EArgumentNilException.Create('AFetcher'); + FFetcher := AFetcher; +end; + +function TCacheValueFetcher.Fetch(const AKey: TKey): TValue; +begin + Result := FFetcher.Fetch(AKey); +end; + +class function TCacheValueFetcher.FromNestedFunction(const AFunc: TCacheNestedFetchFunc) : ICacheValueFetcher; +begin + Result := NestedFunctionValueFetcher.Create(AFunc); +end; + +class function TCacheValueFetcher.FromObjectFunction(const AFunc: TCacheObjectFetchFunc) : ICacheValueFetcher; +begin + Result := ObjectFunctionValueFetcher.Create(AFunc); +end; + +class function TCacheValueFetcher.FromGlobalFunction(const AFunc: TCacheGlobalFetchFunc) : ICacheValueFetcher; +begin + Result := GlobalFunctionValueFetcher.Create(AFunc); +end; + +constructor TCacheSizeEstimator.Create(const AEstimator: ICacheSizeEstimator); +begin + inherited Create; + if not Assigned(AEstimator) then + raise EArgumentNilException.Create('AEstimator'); + FEstimator := AEstimator; +end; + +function TCacheSizeEstimator.Estimate(const AValue: TValue) : SizeInt; +begin + Result := FEstimator.Estimate(AValue); +end; + +class function TCacheSizeEstimator.FromNestedFunction(const AFunc: TCacheNestedEstimateFunc) : ICacheSizeEstimator; +begin + Result := NestedFunctionEstimator.Create(AFunc); +end; + +class function TCacheSizeEstimator.FromObjectFunction(const AFunc: TCacheObjectEstimateFunc) : ICacheSizeEstimator; +begin + Result := ObjectFunctionEstimator.Create(AFunc); +end; + +class function TCacheSizeEstimator.FromGlobalFunction(const AFunc: TCacheGlobalEstimateFunc) : ICacheSizeEstimator; +begin + Result := GlobalFunctionEstimator.Create(AFunc); +end; + +constructor TCacheValueFetcher.GlobalFunctionValueFetcher.Create(const AFunc: TCacheGlobalFetchFunc); +begin + inherited Create; + if not Assigned(AFunc) then + raise EArgumentNilException.Create('AFunc'); + FFunc := AFunc; +end; + +function TCacheValueFetcher.GlobalFunctionValueFetcher.Fetch(const AKey: TKey): TValue; +begin + Result := FFunc(AKey); +end; + +constructor TCacheValueFetcher.ObjectFunctionValueFetcher.Create(const AFunc: TCacheObjectFetchFunc); +begin + inherited Create; + if not Assigned(AFunc) then + raise EArgumentNilException.Create('AFunc'); + FFunc := AFunc; +end; + +function TCacheValueFetcher.ObjectFunctionValueFetcher.Fetch(const AKey: TKey): TValue; +begin + Result := FFunc(AKey); +end; + +constructor TCacheValueFetcher.NestedFunctionValueFetcher.Create(const AFunc: TCacheNestedFetchFunc); +begin + inherited Create; + if not Assigned(AFunc) then + raise EArgumentNilException.Create('AFunc'); + FFunc := AFunc; +end; + +function TCacheValueFetcher.NestedFunctionValueFetcher.Fetch(const AKey: TKey): TValue; +begin + Result := FFunc(AKey); +end; + +constructor TCacheSizeEstimator.GlobalFunctionEstimator.Create(const AFunc: TCacheGlobalEstimateFunc); +begin + inherited Create; + if not Assigned(AFunc) then + raise EArgumentNilException.Create('AFunc'); + FFunc := AFunc; +end; + +function TCacheSizeEstimator.GlobalFunctionEstimator.Estimate(const AValue: TValue): SizeInt; +begin + Result := FFunc(AValue); +end; + +constructor TCacheSizeEstimator.ObjectFunctionEstimator.Create(const AFunc: TCacheObjectEstimateFunc); +begin + inherited Create; + if not Assigned(AFunc) then + raise EArgumentNilException.Create('AFunc'); + FFunc := AFunc; +end; + +function TCacheSizeEstimator.ObjectFunctionEstimator.Estimate(const AValue: TValue): SizeInt; +begin + Result := FFunc(AValue); +end; + +constructor TCacheSizeEstimator.NestedFunctionEstimator.Create(const AFunc: TCacheNestedEstimateFunc); +begin + inherited Create; + if not Assigned(AFunc) then + raise EArgumentNilException.Create('AFunc'); + FFunc := AFunc; +end; + +function TCacheSizeEstimator.NestedFunctionEstimator.Estimate(const AValue: TValue): SizeInt; +begin + Result := FFunc(AValue); +end; + +{ TBulkFetchCacheBase } + +constructor TBulkFetchCacheBase.Create(AOwner: TComponent; const AExpirationPolicy: TCacheExpirationPolicy; const AExpirationDuration: TTimeSpan; AFetchOnceOnly: Boolean; ANullValuePolicy: TCacheNullValuePolicy; AStaleValuePolicy: TCacheStaleValuePolicy; const AKeyComparer: IEqualityComparer); +begin + inherited Create(AOwner,idpNone,TCacheReapPolicy.crpNone, + AExpirationPolicy,ANullValuePolicy,High(SizeInt),AExpirationDuration,0.1,AStaleValuePolicy); + FFetchOnceOnly := AFetchOnceOnly; + FKeyComparer := AKeyComparer; + if Assigned(AKeyComparer) then begin + FreeAndNil(FInternalStorage); + FInternalStorage := TDictionary{$ENDIF}>.Create(AKeyComparer); + end; +end; + +function TBulkFetchCacheBase.EstimateSize(const AVal: TValue): SizeInt; +begin + Result := 0; // A complete source snapshot has no per-entry capacity eviction. +end; + +function TBulkFetchCacheBase.ShouldFetch(const AKey: TKey; AItem: {$IFDEF FPC}__TCachedItem_TValue{$ELSE}TCachedItem{$ENDIF}): boolean; +begin + // Get has already checked/refreshed the whole snapshot. Rechecking here would + // remove fetch-once values or refetch twice for one initial missing-key read. + Result := False; +end; + +function TBulkFetchCacheBase.Fetch(const AKey: TKey): TValue; +var item: {$IFDEF FPC}__TCachedItem_TValue{$ELSE}TCachedItem{$ENDIF}; +begin + if FInternalStorage.TryGetValue(AKey,item) then Result := item.Value + else if FNullValuePolicy = TCacheNullValuePolicy.nvpThrow then + raise EInvalidOperation.Create('The bulk cache source does not contain the requested key.') + else Result := Default(TValue); +end; + +function TBulkFetchCacheBase.Get(const AKey: TKey): {$IFDEF FPC}__TCachedItem_TValue{$ELSE}TCachedItem{$ENDIF}; +var item: {$IFDEF FPC}__TCachedItem_TValue{$ELSE}TCachedItem{$ENDIF}; refresh: Boolean; +begin + FLock.Acquire; + try + CheckAvailable; + if not FRefreshing then begin + refresh := not FLoaded; + if FLoaded and not FFetchOnceOnly then begin + refresh := not FInternalStorage.TryGetValue(AKey,item); + if not refresh then begin + item._AddRef; + try + refresh := (not item.FRemoving) and (IsExpired(item) or IsStale(AKey,item)); + finally + item._Release; + end; + end; + end; + if refresh then ForceRefresh; + end; + Result := inherited Get(AKey); + finally + FLock.Release; + end; +end; + +function TBulkFetchCacheBase.ContainsCachedItem(const AKey: TKey): Boolean; +begin + FLock.Acquire; + try + CheckAvailable; + if (not FLoaded) and (not FRefreshing) then ForceRefresh; + if FFetchOnceOnly then Result := FInternalStorage.ContainsKey(AKey) + else Result := inherited ContainsCachedItem(AKey); + finally + FLock.Release; + end; +end; + +function TBulkFetchCacheBase.GetAllCachedValues: TArray; +begin + FLock.Acquire; + try + CheckAvailable; + if (not FLoaded) and (not FRefreshing) then ForceRefresh; + Result := inherited GetAllCachedValues; + finally + FLock.Release; + end; +end; + +procedure TBulkFetchCacheBase.ForceRefresh; +var values: TCacheBulkValues; pair: TPair; +begin + FLock.Acquire; + try + CheckAvailable; + if FRefreshing then + raise EInvalidOperation.Create('A bulk refresh is already running on this cache.'); + FRefreshing := True; + try + // Keep purge-first semantics explicit: provider/admission failure does not + // resurrect the previous snapshot. The temporary container is always ours; + // class/pointer payloads stay borrowed from the provider/source owner. + Purge; + if Assigned(FKeyComparer) then values := TCacheBulkValues.Create(FKeyComparer) + else values := TCacheBulkValues.Create; + try + BulkFetch(values); + for pair in values do SetValue(pair.Key,pair.Value); + FLoaded := True; + Inc(FFetchCount); + finally + values.Free; + end; + finally + FRefreshing := False; + end; + finally + FLock.Release; + end; +end; + +procedure TBulkFetchCacheBase.Purge; +begin + FLock.Acquire; + try + inherited; + FLoaded := False; + FFetchCount := 0; + finally + FLock.Release; + end; +end; + +procedure TBulkFetchCacheBase.Remove(const AKey: TKey); +begin + raise ENotSupportedException.Create('Items cannot be manually removed from a bulk-fetch cache.'); +end; + +function TBulkFetchCacheBase.Reap(AMinimumRelease: SizeInt): SizeInt; +begin + if AMinimumRelease < 0 then + raise EArgumentException.Create('The requested release must be nonnegative.'); + // Reap(0) still performs the base lifecycle check under its lock, without + // removing any entry or asking the whole-source provider to run. + Result := inherited Reap(0); +end; + +{ TBulkFetchActionCache } + +constructor TBulkFetchActionCache.Create(AOwner: TComponent; const AFetcher: ICacheBulkValueFetcher; const AExpirationPolicy: TCacheExpirationPolicy; const AExpirationDuration: TTimeSpan; AFetchOnceOnly: Boolean; ANullValuePolicy: TCacheNullValuePolicy; AStaleValuePolicy: TCacheStaleValuePolicy; const AKeyComparer: IEqualityComparer); +begin + inherited Create(AOwner,AExpirationPolicy,AExpirationDuration,AFetchOnceOnly, + ANullValuePolicy,AStaleValuePolicy,AKeyComparer); + if not Assigned(AFetcher) then raise EArgumentNilException.Create('AFetcher'); + FFetcher := AFetcher; +end; + +procedure TBulkFetchActionCache.BulkFetch(AValues: TCacheBulkValues); +begin + FFetcher.Fetch(AValues); +end; + +constructor TCacheBulkValueFetcher.Create(const AFetcher: ICacheBulkValueFetcher); +begin + inherited Create; + if not Assigned(AFetcher) then raise EArgumentNilException.Create('AFetcher'); + FFetcher := AFetcher; +end; + +procedure TCacheBulkValueFetcher.Fetch(AValues: TCacheBulkValues); +begin + FFetcher.Fetch(AValues); +end; + +class function TCacheBulkValueFetcher.FromGlobalFunction(const AProc: TCacheGlobalBulkFetchProc): ICacheBulkValueFetcher; +begin + Result := TGlobalFetcher.Create(AProc); +end; + +class function TCacheBulkValueFetcher.FromObjectFunction(const AProc: TCacheObjectBulkFetchProc): ICacheBulkValueFetcher; +begin + Result := TObjectFetcher.Create(AProc); +end; + +class function TCacheBulkValueFetcher.FromNestedFunction(const AProc: TCacheNestedBulkFetchProc): ICacheBulkValueFetcher; +begin + Result := TNestedFetcher.Create(AProc); +end; + +constructor TCacheBulkValueFetcher.TGlobalFetcher.Create(const AProc: TCacheGlobalBulkFetchProc); +begin + inherited Create; + if not Assigned(AProc) then raise EArgumentNilException.Create('AProc'); + FProc := AProc; +end; + +procedure TCacheBulkValueFetcher.TGlobalFetcher.Fetch(AValues: TCacheBulkValues); +begin + FProc(AValues); +end; + +constructor TCacheBulkValueFetcher.TObjectFetcher.Create(const AProc: TCacheObjectBulkFetchProc); +begin + inherited Create; + if not Assigned(AProc) then raise EArgumentNilException.Create('AProc'); + FProc := AProc; +end; + +procedure TCacheBulkValueFetcher.TObjectFetcher.Fetch(AValues: TCacheBulkValues); +begin + FProc(AValues); +end; + +constructor TCacheBulkValueFetcher.TNestedFetcher.Create(const AProc: TCacheNestedBulkFetchProc); +begin + inherited Create; + if not Assigned(AProc) then raise EArgumentNilException.Create('AProc'); + FProc := AProc; +end; + +procedure TCacheBulkValueFetcher.TNestedFetcher.Fetch(AValues: TCacheBulkValues); +begin + FProc(AValues); +end; + +{ TPreloadedCache } + +constructor TPreloadedCache.Create(AOwner: TComponent; AValues: TDictionary; const AKeyComparer: IEqualityComparer); +begin + inherited Create(AOwner,TCacheExpirationPolicy.epNone, + TTimeSpan.MaxValue,True,TCacheNullValuePolicy.nvpCacheNormally, + TCacheStaleValuePolicy.svpAssumeNeverStale,AKeyComparer); + if not Assigned(AValues) then raise EArgumentNilException.Create('AValues'); + FValues := AValues; +end; + +procedure TPreloadedCache.BulkFetch(AValues: TCacheBulkValues); +var pair: TPair; +begin + for pair in FValues do AValues.AddOrSetValue(pair.Key,pair.Value); +end; + +end. diff --git a/src/libraries/sphere10/UCommon.Collections.pas b/src/libraries/sphere10/UCommon.Collections.pas index 4d4df1af7..098a69bc8 100644 --- a/src/libraries/sphere10/UCommon.Collections.pas +++ b/src/libraries/sphere10/UCommon.Collections.pas @@ -1,6 +1,6 @@ unit UCommon.Collections; -{ Copyright (c) 2017 - 2018 Sphere 10 Software +{ Copyright (c) 2017 - 2018 Herman Schoenfeld Common tools and extensions for Generics.Collections and collections in general usable across all tiers. @@ -14,6 +14,28 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Public types in this unit + + Type | Purpose + -----------------------------+------------------------------------------------------------------------- + IPredicate | Tests whether a value satisfies a condition. + ITransformer | Converts a value from one generic type to another. + TComparerTool | Creates and combines comparison functions and comparer objects. + TGlobalComparerFunc | Defines a global comparison callback. + TGlobalPredicateFunc | Defines a global value-testing callback. + TGlobalTransformFunc | Defines a global value-conversion callback. + TListTool | Transforms list or array values into another element type. + TListTool | Copies, slices, filters and removes list contents. + TNestedComparerFunc | Defines a captured comparison callback. + TNestedPredicateFunc | Defines a captured value-testing callback. + TNestedTransformFunc | Defines a captured value-conversion callback. + TObjectComparerFunc | Defines an object-method comparison callback. + TObjectPredicateFunc | Defines an object-method value-testing callback. + TObjectTransformFunc | Defines an object-method value-conversion callback. + TPredicateTool | Creates and combines reusable value predicates. + TSortedHashSetTool | Removes and returns one value from a Free Pascal sorted hash set. +} + {$IFDEF FPC} {$mode delphi} {$modeswitch nestedprocvars} @@ -22,7 +44,12 @@ interface uses - Classes, SysUtils, Generics.Collections, Generics.Defaults,UMemory, UCommon; + {$IFDEF FPC} + Classes, SysUtils, Generics.Collections, Generics.Defaults, + {$ELSE} + System.Classes, System.SysUtils, System.Generics.Collections, System.Generics.Defaults, + {$ENDIF} + UMemory, UCommon; type @@ -30,11 +57,15 @@ interface // Note: this tries to follow pattern from Generics.Collections for supporting nested/object/global delegates. + {$IFDEF FPC} TNestedComparerFunc = function (constref Left, Right: T): Integer is nested; + {$ELSE} + TNestedComparerFunc = reference to function (const Left, Right: T): Integer; + {$ENDIF} - TObjectComparerFunc = function (constref Left, Right: T): Integer of object; + TObjectComparerFunc = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer of object; - TGlobalComparerFunc = function (constref Left, Right: T): Integer; + TGlobalComparerFunc = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; { TComparerTool } @@ -54,27 +85,35 @@ TComparerTool = class class function Inverted(const AComparer : IComparer) : IComparer; private // These should be nested but FPC doesn't support nested functions in generics - class function AlwaysEqualHandler(constref Left, Right: T) : Integer; + class function AlwaysEqualHandler({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T) : Integer; end; { Predicate API } // Note: the pattern for nested/object/global delegates is custom + {$IFDEF FPC} TNestedPredicateFunc = function (constref AVal : T) : boolean is nested; + {$ELSE} + TNestedPredicateFunc = reference to function (const AVal : T) : boolean; + {$ENDIF} - TObjectPredicateFunc = function (constref AVal : T) : boolean of object; + TObjectPredicateFunc = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AVal : T) : boolean of object; - TGlobalPredicateFunc = function (constref AVal : T) : boolean; + TGlobalPredicateFunc = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AVal : T) : boolean; + {$IFDEF FPC} TNestedTransformFunc = function (constref AItem : T1) : T2 is nested; + {$ELSE} + TNestedTransformFunc = reference to function (const AItem : T1) : T2; + {$ENDIF} - TObjectTransformFunc = function (constref AItem : T1) : T2 of object; + TObjectTransformFunc = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem : T1) : T2 of object; - TGlobalTransformFunc = function (constref AItem : T1) : T2; + TGlobalTransformFunc = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem : T1) : T2; IPredicate = interface - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; ITransformer = interface @@ -101,8 +140,8 @@ TPredicateTool = class class function NegatePredicate(const APredicate : IPredicate) : IPredicate; private // These should be nested but FPC doesn't support nested functions in generics - class function TrueHandler(constref AItem: T) : boolean; - class function FalseHandler(constref AItem: T) : boolean; + class function TrueHandler({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T) : boolean; + class function FalseHandler({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T) : boolean; end; { TListTool } @@ -116,7 +155,8 @@ TListTool = class class function RemoveBy(const AList: TList; const APredicate: IPredicate; const ADisposePolicy : TDisposePolicy) : SizeInt; overload; class function FilterBy(const AList: TList; const APredicate: IPredicate) : SizeInt; overload; class function FilterBy(const AList: TList; const APredicate: IPredicate; const ADisposePolicy : TDisposePolicy) : SizeInt; overload; - class procedure DiposeItem(const AList: TList; const index : SizeInt; const ADisposePolicy : TDisposePolicy); + class procedure DisposeItem(const AList: TList; const index : SizeInt; const ADisposePolicy : TDisposePolicy); + class procedure DiposeItem(const AList: TList; const index : SizeInt; const ADisposePolicy : TDisposePolicy); deprecated 'Use DisposeItem'; end; TListTool = class @@ -133,9 +173,11 @@ TListTool = class { TSortedHashSetTool } + {$IFDEF FPC} TSortedHashSetTool = class class function Pop(ASet : TSortedHashSet) : T; end; + {$ENDIF} { ================================================================================================== } @@ -149,7 +191,7 @@ TNestedComparer = class(TInterfacedObject, IComparer) FFunc: TNestedComparerFunc; public constructor Create(const AComparerFunc: TNestedComparerFunc); overload; - function Compare(constref Left, Right: T): Integer; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; end; { TObjectComparer } @@ -159,7 +201,7 @@ TObjectComparer = class(TInterfacedObject, IComparer) FFunc: TObjectComparerFunc; public constructor Create(const AComparerFunc: TObjectComparerFunc); overload; - function Compare(constref Left, Right: T): Integer; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; end; { TGlobalComparer } @@ -169,7 +211,7 @@ TGlobalComparer = class(TInterfacedObject, IComparer) FFunc: TGlobalComparerFunc; public constructor Create(const AComparerFunc: TGlobalComparerFunc); overload; - function Compare(constref Left, Right: T): Integer; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; end; { TManyComparer } @@ -181,7 +223,7 @@ TManyComparer = class(TInterfacedObject, IComparer) FComparers : TArray; public constructor Create(const comparers: TArray); overload; - function Compare(constref Left, Right: T): Integer; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; end; { TInvertedComparer } @@ -191,7 +233,7 @@ TInvertedComparer = class(TInterfacedObject, IComparer) FComparer : IComparer; public constructor Create(const AComparer : IComparer); overload; - function Compare(constref Left, Right: T): Integer; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; end; { TNestedPredicate } @@ -201,7 +243,7 @@ TNestedPredicate = class (TInterfacedObject, IPredicate) FFunc : TNestedPredicateFunc; public constructor Create(const AFunc: TNestedPredicateFunc); overload; - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; { TObjectPredicate } @@ -211,7 +253,7 @@ TObjectPredicate = class (TInterfacedObject, IPredicate) FFunc : TObjectPredicateFunc; public constructor Create(const AFunc: TObjectPredicateFunc); overload; - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; { TGlobalPredicate } @@ -221,7 +263,7 @@ TGlobalPredicate = class (TInterfacedObject, IPredicate) FFunc : TGlobalPredicateFunc; public constructor Create(const AFunc: TGlobalPredicateFunc); overload; - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; { TNotPredicate } @@ -231,7 +273,7 @@ TNotPredicate = class (TInterfacedObject, IPredicate) FPredicate : IPredicate; public constructor Create(const APredicate : IPredicate); overload; - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; { TAndManyPredicate } @@ -244,7 +286,7 @@ TAndManyPredicate = class (TInterfacedObject, IPredicate) FPredicates : TArray<__IPredicate_T>; public constructor Create(const APredicates: TArray<__IPredicate_T>); overload; - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; { TOrManyPredicate } @@ -257,7 +299,7 @@ TOrManyPredicate = class (TInterfacedObject, IPredicate) FPredicates : TArray<__IPredicate_T>; public constructor Create(const APredicates: TArray<__IPredicate_T>); overload; - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; { TNestedTransformer } @@ -345,7 +387,6 @@ class function TComparerTool.Many(const comparers: array of TGlobalComparerFu class function TComparerTool.Many(const comparers: array of IComparer) : IComparer; type __TArrayTool_IComparer_T = TArrayTool<__IComparer_T>; - begin Result := TManyComparer.Create( __TArrayTool_IComparer_T.Copy(comparers) ); end; @@ -370,7 +411,7 @@ class function TComparerTool.AlwaysEqual : IComparer; Result := TComparerTool.FromFunc( AlwaysEqualHandler ); end; -class function TComparerTool.AlwaysEqualHandler(constref Left, Right: T) : Integer; +class function TComparerTool.AlwaysEqualHandler({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T) : Integer; begin Result := 0; end; @@ -387,7 +428,7 @@ constructor TNestedComparer.Create(const AComparerFunc: TNestedComparerFunc.Compare(constref Left, Right: T): Integer; +function TNestedComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; begin Result := FFunc(Left, Right); end; @@ -399,7 +440,7 @@ constructor TObjectComparer.Create(const AComparerFunc: TObjectComparerFunc.Compare(constref Left, Right: T): Integer; +function TObjectComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; begin Result := FFunc(Left, Right); end; @@ -411,7 +452,7 @@ constructor TGlobalComparer.Create(const AComparerFunc: TGlobalComparerFunc.Compare(constref Left, Right: T): Integer; +function TGlobalComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; begin Result := FFunc(Left, Right); end; @@ -423,7 +464,7 @@ constructor TManyComparer.Create(const comparers: TArray); FComparers := comparers; end; -function TManyComparer.Compare(constref Left, Right: T): Integer; +function TManyComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; var i : Integer; begin @@ -442,7 +483,7 @@ constructor TInvertedComparer.Create(const AComparer: IComparer); FComparer := AComparer; end; -function TInvertedComparer.Compare(constref Left, Right: T): Integer; +function TInvertedComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right: T): Integer; begin Result := FComparer.Compare(Left, Right) * -1; end; @@ -466,8 +507,6 @@ class function TPredicateTool.FromFunc(const AFunc: TGlobalPredicateFunc) end; class function TPredicateTool.AndMany(const APredicates : array of IPredicate) : IPredicate; -// type -// __TArrayTool_IPredicate_T = TArrayTool<__IPredicate_T>; // Skybuck: Disabled not needed var arr : TArray<__IPredicate_T>; i : Integer; @@ -568,15 +607,15 @@ class function TPredicateTool.NegatePredicate(const APredicate : IPredicate.TrueHandler(constref AItem: T) : boolean; +class function TPredicateTool.TrueHandler({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T) : boolean; begin Result := true; end; // Shold be nested funcion but generics can't have in FPC! -class function TPredicateTool.FalseHandler(constref AItem: T) : boolean; +class function TPredicateTool.FalseHandler({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T) : boolean; begin - Result := true; + Result := false; end; { TNestedPredicate } @@ -586,7 +625,7 @@ constructor TNestedPredicate.Create(const AFunc: TNestedPredicateFunc); FFunc := AFunc; end; -function TNestedPredicate.Evaluate (constref AValue: T) : boolean; +function TNestedPredicate.Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; begin Result := FFunc(AValue); end; @@ -598,7 +637,7 @@ constructor TObjectPredicate.Create(const AFunc: TObjectPredicateFunc); FFunc := AFunc; end; -function TObjectPredicate.Evaluate (constref AValue: T) : boolean; +function TObjectPredicate.Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; begin Result := FFunc(AValue); end; @@ -610,7 +649,7 @@ constructor TGlobalPredicate.Create(const AFunc: TGlobalPredicateFunc); FFunc := AFunc; end; -function TGlobalPredicate.Evaluate (constref AValue: T) : boolean; +function TGlobalPredicate.Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; begin Result := FFunc(AValue); end; @@ -622,7 +661,7 @@ constructor TNotPredicate.Create(const APredicate: IPredicate); FPredicate := APredicate; end; -function TNotPredicate.Evaluate (constref AValue: T) : boolean; +function TNotPredicate.Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; begin Result := NOT FPredicate.Evaluate(AValue); end; @@ -637,7 +676,7 @@ constructor TAndManyPredicate.Create(const APredicates: TArray<__IPredicate_T FPredicates := APredicates; end; -function TAndManyPredicate.Evaluate (constref AValue: T) : boolean; +function TAndManyPredicate.Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; var i : integer; begin @@ -659,7 +698,7 @@ constructor TOrManyPredicate.Create(const APredicates: TArray<__IPredicate_T> FPredicates := APredicates; end; -function TOrManyPredicate.Evaluate (constref AValue: T) : boolean; +function TOrManyPredicate.Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; var i : integer; begin @@ -673,7 +712,7 @@ function TOrManyPredicate.Evaluate (constref AValue: T) : boolean; { Transformers } -constructor TNestedTransformer.Create(const AFunc: TNestedTransformFunc); overload; +constructor TNestedTransformer.Create(const AFunc: TNestedTransformFunc); {$IFDEF FPC}overload;{$ENDIF} begin FFunc := AFunc; end; @@ -683,7 +722,7 @@ function TNestedTransformer.Transform(const AItem : T1) : T2; Result := FFunc(AItem); end; -constructor TObjectTransformer.Create(const AFunc: TObjectTransformFunc); overload; +constructor TObjectTransformer.Create(const AFunc: TObjectTransformFunc); {$IFDEF FPC}overload;{$ENDIF} begin FFunc := AFunc; end; @@ -693,7 +732,7 @@ function TObjectTransformer.Transform(const AItem : T1) : T2; Result := FFunc(AItem); end; -constructor TGlobalTransformer.Create(const AFunc: TGlobalTransformFunc); overload; +constructor TGlobalTransformer.Create(const AFunc: TGlobalTransformFunc); {$IFDEF FPC}overload;{$ENDIF} begin FFunc := AFunc; end; @@ -707,42 +746,49 @@ function TGlobalTransformer.Transform(const AItem : T1) : T2; class function TListTool.Copy(const AList: TList; const AIndex, ACount : SizeInt) : TList; var - i : Integer; + i : SizeInt; begin + // Count is a length, not an inclusive final index. Subtraction avoids an + // overflowing AIndex + ACount when rejecting an invalid requested range. + if (AIndex < 0) or (ACount < 0) or (AIndex > AList.Count) then + raise EArgumentOutOfRangeException.Create('Invalid list copy index/count'); + if ACount > AList.Count - AIndex then + raise EArgumentOutOfRangeException.Create('List copy range exceeds the source'); Result := TList.Create; - - for i := 0 to ACount do + try + for i := 0 to ACount - 1 do Result.Add(AList[AIndex + i]); - + except + Result.Free; + raise; + end; end; class function TListTool.Range(const AList: TList; const AIndex, ACount : SizeInt) : SizeInt; var - from, to_, listCount : SizeInt; + from, keepCount, listCount : SizeInt; begin - listCount := AList.Count; - - from := ClipValue(AIndex, 0, listCount - 1); - to_ := ClipValue(AIndex + ACount, 0, listCount - 1); - - if to_ <= from then begin - Result := 0; - exit; - end; - + // Retain at most Count items from the clamped starting index. DeleteRange + // takes a remaining length; passing the whole list size after an index fails. + from := AIndex; + if from < 0 then from := 0 + else if from > listCount then from := listCount; + keepCount := ACount; + if keepCount < 0 then keepCount := 0 + else if keepCount > listCount - from then keepCount := listCount - from; if from > 0 then AList.DeleteRange(0, from); - - if to_ < (listCount - 1) then - AList.DeleteRange(to_ - from, AList.Count); - + if keepCount < AList.Count then + AList.DeleteRange(keepCount, AList.Count - keepCount); + // Preserve the existing signed size-delta return contract. Result := AList.Count - listCount; end; class function TListTool.Skip(const AList: TList; const ACount : SizeInt) : SizeInt; begin - Result := Range(AList, 0 + ACount, AList.Count - ACount); + // Range clamps to the available suffix, without subtracting extreme counts. + Result := Range(AList, ACount, AList.Count); end; class function TListTool.Take(const AList: TList; const ACount : SizeInt) : SizeInt; @@ -765,7 +811,7 @@ class function TListTool.RemoveBy(const AList: TList; const APredicate: IP while i >= 0 do begin item := AList[i]; if APredicate.Evaluate(item) then begin - DiposeItem(AList, i, ADisposePolicy); + DisposeItem(AList, i, ADisposePolicy); AList.Delete(i); inc(Result); end; @@ -779,11 +825,13 @@ class function TListTool.FilterBy(const AList: TList; const APredicate: IP end; class function TListTool.FilterBy(const AList: TList; const APredicate: IPredicate; const ADisposePolicy : TDisposePolicy) : SizeInt; +var LRejected : IPredicate; begin - Result := RemoveBy(AList, TPredicateTool.NegatePredicate ( APredicate ) ); + LRejected := TPredicateTool.NegatePredicate(APredicate); + Result := RemoveBy(AList, LRejected, ADisposePolicy); end; -class procedure TListTool.DiposeItem(const AList: TList; const index : SizeInt; const ADisposePolicy : TDisposePolicy); +class procedure TListTool.DisposeItem(const AList: TList; const index : SizeInt; const ADisposePolicy : TDisposePolicy); var item : T; begin @@ -793,26 +841,43 @@ class procedure TListTool.DiposeItem(const AList: TList; const index : Siz idpNil: AList[index] := default(T); idpFreeAndNil: begin item := AList[index]; + {$IFDEF FPC} FreeAndNil(item); + {$ELSE} + TObject(PPointer(@item)^).Free; + item := Default(T); + {$ENDIF} AList[index] := default(T); end - else raise ENotSupportedException(Format('TItemDisposePolicy: [%d]', [Ord(ADisposePolicy)])); + else raise ENotSupportedException.Create(Format('TItemDisposePolicy: [%d]', [Ord(ADisposePolicy)])); end; end; +class procedure TListTool.DiposeItem(const AList: TList; const index : SizeInt; const ADisposePolicy : TDisposePolicy); +begin + DisposeItem(AList, index, ADisposePolicy); +end; + class function TListTool.Transform(const AArray : TArray; const ATransform : TNestedTransformFunc) : TArray; +var LTransformer: ITransformer; begin - Result := Transform(AArray, TNestedTransformer.Create(ATransform)); + LTransformer := TNestedTransformer.Create(ATransform); + Result := Transform(AArray, LTransformer); end; class function TListTool.Transform(const AArray : TArray; const ATransform : TObjectTransformFunc) : TArray; +var LTransformer: ITransformer; begin - Result := Transform(AArray, TObjectTransformer.Create(ATransform)); + LTransformer := TObjectTransformer.Create(ATransform); + Result := Transform(AArray, LTransformer); end; class function TListTool.Transform(const AArray : TArray; const ATransform : TGlobalTransformFunc) : TArray; +var LTransformer: ITransformer; begin - Result := Transform(AArray, TGlobalTransformer.Create(ATransform)); + // Keep an explicit interface owner until the forwarding call finishes. + LTransformer := TGlobalTransformer.Create(ATransform); + Result := Transform(AArray, LTransformer); end; class function TListTool.Transform(const AArray : TArray; const ATransformer : ITransformer) : TArray; @@ -825,18 +890,24 @@ class function TListTool.Transform(const AArray : TArray; const ATra end; class function TListTool.Transform(const AList : TList; const ATransform : TNestedTransformFunc) : TArray; +var LTransformer: ITransformer; begin - Result := Transform(AList, TNestedTransformer.Create(ATransform)); + LTransformer := TNestedTransformer.Create(ATransform); + Result := Transform(AList, LTransformer); end; class function TListTool.Transform(const AList : TList; const ATransform : TObjectTransformFunc) : TArray; +var LTransformer: ITransformer; begin - Result := Transform(AList, TObjectTransformer.Create(ATransform)); + LTransformer := TObjectTransformer.Create(ATransform); + Result := Transform(AList, LTransformer); end; class function TListTool.Transform(const AList : TList; const ATransform : TGlobalTransformFunc) : TArray; +var LTransformer: ITransformer; begin - Result := Transform(AList, TGlobalTransformer.Create(ATransform)); + LTransformer := TGlobalTransformer.Create(ATransform); + Result := Transform(AList, LTransformer); end; class function TListTool.Transform(const AList : TList; const ATransformer : ITransformer) : TArray; @@ -848,6 +919,7 @@ class function TListTool.Transform(const AList : TList; const ATrans Result[i] := ATransformer.Transform(AList[i]); end; +{$IFDEF FPC} { TSortedHashSetTool } class function TSortedHashSetTool.Pop(ASet : TSortedHashSet): T; @@ -861,6 +933,7 @@ class function TSortedHashSetTool.Pop(ASet : TSortedHashSet): T; end; ASet.Remove(Result); end; +{$ENDIF} end. diff --git a/src/libraries/sphere10/UCommon.Data.pas b/src/libraries/sphere10/UCommon.Data.pas index 09f6cdfe1..2f3541e11 100644 --- a/src/libraries/sphere10/UCommon.Data.pas +++ b/src/libraries/sphere10/UCommon.Data.pas @@ -1,6 +1,6 @@ unit UCommon.Data; -{ Copyright (c) 2017 - 2018 Sphere 10 Software +{ Copyright (c) 2017 - 2018 Herman Schoenfeld Common data tools. @@ -13,17 +13,58 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Public types in this unit + + Type | Purpose + ---------------------------------+------------------------------------------------------------------------- + EDataRow | Reports invalid data-row access or operations. + EDataTable | Reports invalid table configuration or operations. + ESearchExpressionParserException | Reports an invalid search expression. + IDataSource | Supplies filtered and sorted pages of named Variant rows. + PDataTable | Points to a data table. + TApplyFilterDelegate | Tests one item against a column filter. + TApplySortDelegate | Compares two items using a column sort definition. + TColumnFilter | Holds a column's search values and sorting configuration. + TCustomDataSource | Provides generic paging, filtering, sorting and snapshot caching. + TDataColumn | Describes a named column and whether it identifies a row. + TDataColumns | Holds an ordered array of column definitions. + TDataFilter | Selects an allowed text, numeric or sorting operation. + TDataFilters | Combines allowed column filter operations. + TDataRow | Implements named-field Variant rows and dynamic field access. + TDataRowData | Provides named-field access to a row's Variant data. + TDataSourceTool | Builds row comparers and predicates from column filters. + TDataTable | Pairs column metadata with an array of Variant rows. + TExpressionKind | Identifies text, numeric or range search expressions. + TExpressionRecord | Stores a parsed text, numeric or range search expression. + TFilterCriteria | Holds an array of column filters. + TFilterOperand | Combines filters using AND or OR. + TNumericComparisionKind | Deprecated alias of TNumericComparisonKind. + TNumericComparisonKind | Selects the numeric comparison in a parsed expression. + TPageFetchParams | Describes a page request, its filters and filter-combination rule. + TPageFetchResult | Reports the returned page index, page count and total row count. + TSearchExpressionService | Parses text, numeric and range search expressions. + TSetKind | Selects inclusive or exclusive numeric range matching. + TSortDirection | Selects unsorted, ascending or descending order. + TSortNullPolicy | Controls placement of null values during sorting. + TTextMatchKind | Selects exact, prefix, suffix or substring text matching. +} + {$IFDEF FPC} {$MODE Delphi} + {$MODESWITCH NESTEDPROCVARS} {$ENDIF} -{$MODESWITCH NESTEDPROCVARS} - interface uses - Classes, SysUtils, Generics.Collections, UMemory, UCommon, UCommon.Collections, Generics.Defaults, - Variants, LazUTF8, math, typinfo, syncobjs; + {$IFDEF FPC} + Classes, SysUtils, Generics.Collections, Generics.Defaults, Variants, LazUTF8, + Math, TypInfo, SyncObjs, + {$ELSE} + System.Classes, System.SysUtils, System.Generics.Collections, System.Generics.Defaults, + System.Variants, System.Math, System.TypInfo, System.SyncObjs, System.TimeSpan, + {$ENDIF} + UMemory, UCommon, UCommon.Collections, UCache; type TSortDirection = (sdNone, sdAscending, sdDescending); @@ -49,12 +90,12 @@ interface TDataColumn = record private - FName : AnsiString; + FName : String; FKey : boolean; public - property Name : AnsiString read FName; + property Name : String read FName; property IsKey : boolean read FKey; - class function From(const AName: AnsiString; IsKey : boolean = False) : TDataColumn; overload; static; + class function From(const AName: String; IsKey : boolean = False) : TDataColumn; overload; static; end; TDataColumns = TArray; @@ -66,15 +107,18 @@ TDataRow = class(TInvokeableVariantType) class constructor Create; class destructor Destroy; protected type - TColumnMapToIndex = TDictionary; + TColumnMapToIndex = TDictionary; TColumnsDictionary = TObjectDictionary; protected class var FColumns: TColumnsDictionary; + FColumnsLock: TCriticalSection; protected + {$IFNDEF FPC} + function FixupIdent(const AText : String) : String; override; {$ENDIF} class function MapColumns(AClassID : PtrInt; const AColumns: TDataColumns): TColumnMapToIndex; public function GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; override; - function SetProperty(var V: TVarData; const Name: string; const Value: TVarData): Boolean; override; + function SetProperty({$IFDEF FPC}var{$ELSE}const{$ENDIF} V: TVarData; const Name: string; const Value: TVarData): Boolean; override; procedure Clear(var V: TVarData); override; procedure Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); override; function DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; override; @@ -91,17 +135,17 @@ EDataRow = class(Exception); vfiller2: int32; // Variant filler vcolumnmap: TDataRow.TColumnMapToIndex; vvalues: TArray; - function GetData(const ABinding : AnsiString): Variant; - procedure SetData(const ABinding : AnsiString; const AValue: Variant); + function GetData(const ABinding : String): Variant; + procedure SetData(const ABinding : String; const AValue: Variant); public - function HasData(const ABinding : AnsiString): boolean; - property Data[const ABinding : AnsiString] : Variant read GetData write SetData; default; + function HasData(const ABinding : String): boolean; + property Data[const ABinding : String] : Variant read GetData write SetData; default; end; { TColumnFilter } TColumnFilter = record - ColumnName: utf8string; + ColumnName: String; Sort: TSortDirection; Filter: TDataFilter; Values: array of Variant; @@ -114,11 +158,11 @@ TColumnFilter = record { TApplyFilterDelegate } - TApplyFilterDelegate = function (constref AItem : T; constref AColumnFilter: TColumnFilter) : boolean of object; + TApplyFilterDelegate = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem : T; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AColumnFilter: TColumnFilter) : boolean of object; { TApplySortDelegate } - TApplySortDelegate = function (constref Left, Right : T; constref AColumnFilter: TColumnFilter) : Integer of object; + TApplySortDelegate = function ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right : T; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AColumnFilter: TColumnFilter) : Integer of object; { TDataTable } @@ -137,7 +181,7 @@ TColumnFilterPredicate = class (TInterfacedObject, IPredicate) FDelegate : TApplyFilterDelegate; public constructor Create(const AFilter : TColumnFilter; const ADelegate : TApplyFilterDelegate); overload; - function Evaluate (constref AValue: T) : boolean; + function Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; end; {TColumnFieldComparer } @@ -148,7 +192,7 @@ TColumnFieldComparer = class (TInterfacedObject, IComparer) FDelegate: TApplySortDelegate; public constructor Create(const AFilter : TColumnFilter; const ADelegate: TApplySortDelegate); - function Compare(constref ALeft, ARight: T): Integer; virtual; + function Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: T): Integer; virtual; end; { TPageFetchParams } @@ -174,39 +218,65 @@ TPageFetchResult = record { IDataSource } IDataSource = interface - function FetchPage(constref AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; end; { TCustomDataSource } TCustomDataSource = class(TComponent, IDataSource) + private type + TSnapshotItems = TArray; + TSnapshotCache = TActionCache; private FLock : TCriticalSection; FClassID : PtrInt; + FSnapshotCache : TSnapshotCache; + FSnapshotGeneration : Int64; + FCachedGeneration : Int64; + FSnapshotCacheEnabled : LongInt; + FSnapshotParams : TPageFetchParams; + FHasSnapshotParams : Boolean; + procedure EnsureSnapshotCache; + function GetSnapshotCacheEnabled : Boolean; + procedure SetSnapshotCacheEnabled(AEnabled : Boolean); + function FetchSnapshot(const AKey : Integer) : TSnapshotItems; + function EstimateSnapshot(const AItems : TSnapshotItems) : SizeInt; + function SnapshotQueryMatches({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams : TPageFetchParams) : Boolean; + procedure CopySnapshotQuery({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams : TPageFetchParams); protected function GetNullPolicy(const AFilter : TColumnFilter) : TSortNullPolicy; virtual; function GetItemDisposePolicy : TDisposePolicy; virtual; function GetColumns : TDataColumns; virtual; abstract; - function ApplyColumnSort(constref Left, Right : T; constref AFilter: TColumnFilter) : Integer; virtual; - function ApplyColumnFilter(constref AItem: T; constref AFilter: TColumnFilter) : boolean; virtual; - function GetItemField(constref AItem: T; const ABindingName : AnsiString) : Variant; virtual; abstract; - procedure DehydrateItem(constref AItem: T; var ADataRow: Variant); - function FetchPage(constref AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; - procedure OnBeforeFetchAll(constref AParams: TPageFetchParams); virtual; + function ApplyColumnSort({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right : T; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AFilter: TColumnFilter) : Integer; virtual; + function ApplyColumnFilter({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AFilter: TColumnFilter) : boolean; virtual; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T; const ABindingName : String) : Variant; virtual; abstract; + procedure DehydrateItem({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T; var ADataRow: Variant); + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; virtual; + procedure OnBeforeFetchAll({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams); virtual; procedure FetchAll(const AContainer : TList); virtual; abstract; - procedure OnAfterFetchAll(constref AParams: TPageFetchParams); virtual; + procedure OnAfterFetchAll({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams); virtual; procedure EnterLock; inline; procedure ReleaseLock; inline; public - constructor Create(AOwner: TComponent); override; overload; + constructor Create(AOwner: TComponent); override; {$IFDEF FPC}overload;{$ENDIF} destructor Destroy; override; + // Opt in for immutable value snapshots. The cache retains one raw dataset + // and one filtered/sorted view; page navigation only converts visible rows. + // Object items remain borrowed and must outlive reuse of the cached + // snapshot and all active fetches. Invalidation does not join a worker or + // transfer ownership; it only makes the next fetch rebuild backing values. + property SnapshotCacheEnabled : Boolean read GetSnapshotCacheEnabled write SetSnapshotCacheEnabled; + // Logical invalidation never waits for a fetch/cache lock. An in-flight + // worker may finish its old snapshot; the next fetch rebuilds the dataset. + procedure InvalidateSnapshot; end; { Expression API } TExpressionKind = (ekUnknown, ekText, ekNum, ekSet); TTextMatchKind = (tmkUnknown, tmkMatchTextExact, tmkMatchTextBeginning, tmkMatchTextEnd, tmkMatchTextAnywhere); - TNumericComparisionKind = (nckUnknown, nckNumericEQ, nckNumericLT, nckNumericLTE, nckNumericGT, nckNumericGTE); + TNumericComparisonKind = (nckUnknown, nckNumericEQ, nckNumericLT, nckNumericLTE, nckNumericGT, nckNumericGTE); + TNumericComparisionKind = TNumericComparisonKind deprecated 'Use TNumericComparisonKind'; TSetKind = (skUnknown, skNumericBetweenInclusive, skNumericBetweenExclusive); TExpressionRecord = record @@ -215,7 +285,7 @@ TExpressionRecord = record case Kind: TExpressionKind of ekUnknown: (); ekText: (TextMatchKind: TTextMatchKind); - ekNum: (NumericComparisionKind: TNumericComparisionKind); + ekNum: (NumericComparisonKind: TNumericComparisonKind); ekSet: (SetKind: TSetKind); end; @@ -236,7 +306,6 @@ TDataSourceTool = class __IPredicate_T = IPredicate; __TList_IPredicate_T = TList<__IPredicate_T>; public - class function ToArrayWorkAround( ParaList : __TList_IPredicate_T ) : TArray>; // Skybuck: work around for buggy TList.ToArray class function ConstructRowComparer(const AFilters : TArray; const ADelegate : TApplySortDelegate) : IComparer; class function ConstructRowPredicate(const AFilters : TArray; const ADelegate : TApplyFilterDelegate; const AOperand : TFilterOperand) : IPredicate; end; @@ -262,7 +331,7 @@ TDataSourceTool = class implementation -uses dateutils; +uses {$IFDEF FPC}DateUtils{$ELSE}System.DateUtils{$ENDIF}; { Variables } @@ -271,7 +340,7 @@ implementation { TDataColumn } -class function TDataColumn.From(const AName: AnsiString; IsKey : boolean = False) : TDataColumn; +class function TDataColumn.From(const AName: String; IsKey : boolean = False) : TDataColumn; begin Result.FName := AName; Result.FKey:= IsKey; @@ -281,24 +350,37 @@ class function TDataColumn.From(const AName: AnsiString; IsKey : boolean = False class constructor TDataRow.Create; begin + FColumnsLock := TCriticalSection.Create; FColumns := TColumnsDictionary.Create([doOwnsValues]); end; class destructor TDataRow.Destroy; begin FColumns.Free; + FColumnsLock.Free; end; class function TDataRow.MapColumns(AClassID : PtrInt; const AColumns: TDataColumns): TColumnMapToIndex; var i: Integer; + disposed: TDisposables; begin - Result := TColumnMapToIndex.Create; + Result := disposed.AddObject(TColumnMapToIndex.Create) as TColumnMapToIndex; for i := Low(AColumns) to High(AColumns) do Result.Add(AColumns[i].Name, i); - Result.Add('__KEY', i + 1); + // A for-loop's final counter value is undefined in Delphi. + Result.Add('__KEY', Length(AColumns)); FColumns.Add(AClassID, Result); + disposed.RemoveObject(Result); +end; + +{$IFNDEF FPC} +function TDataRow.FixupIdent(const AText : String) : String; +begin + // Delphi's default uppercases identifiers; column maps retain their names. + Result := AText; end; +{$ENDIF} function TDataRow.GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; var @@ -308,7 +390,7 @@ function TDataRow.GetProperty(var Dest: TVarData; const V: TVarData; const Name: Result := true; end; -function TDataRow.SetProperty(var V: TVarData; const Name: string; const Value: TVarData): Boolean; +function TDataRow.SetProperty({$IFDEF FPC}var{$ELSE}const{$ENDIF} V: TVarData; const Name: string; const Value: TVarData): Boolean; var LRow: TDataRowData absolute V; begin @@ -322,8 +404,7 @@ procedure TDataRow.Clear(var V: TVarData); FillChar(V, SizeOf(V), #0); end; -procedure TDataRow.Copy(var Dest: TVarData; const Source: TVarData; - const Indirect: Boolean); +procedure TDataRow.Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); var LDestRow: TDataRowData absolute Dest; LSourceRow: TDataRowData absolute Source; @@ -340,21 +421,21 @@ procedure TDataRow.Copy(var Dest: TVarData; const Source: TVarData; end; end; -function TDataRow.DoFunction(var Dest: TVarData; const V: TVarData; - const Name: string; const Arguments: TVarDataArray): Boolean; +function TDataRow.DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; var LRow: TDataRowData absolute V; begin - Result := (Name = '_') and (Length(Arguments)=1); + // FPC 3.2.2 emits an empty default-member name for Row._(index). + Result := ((Name = '_') {$IFDEF FPC}or (Name = ''){$ENDIF}) and (Length(Arguments)=1); if Result then - Variant(Dest) := LRow.vvalues[Variant(Arguments[0])]; + Variant(Dest) := LRow.vvalues[{$IFNDEF FPC}Integer({$ENDIF}Variant(Arguments[0]){$IFNDEF FPC}){$ENDIF}]; end; class function TDataRow.New(AClassID:PtrInt; const AColumns: TDataColumns): Variant; var LColumnMap: TColumnMapToIndex; - AKey : AnsiString; + AKey : String; i : Integer; begin if not Assigned(AColumns) then @@ -363,16 +444,22 @@ class function TDataRow.New(AClassID:PtrInt; const AColumns: TDataColumns): Vari VarClear(Result); FillChar(Result, SizeOf(Result), #0); TDataRowData(Result).vtype:=DataRowType.VarType; - if not FColumns.TryGetValue(AClassID, LColumnMap) then - LColumnMap := MapColumns(AClassID, AColumns) - else begin - // Ensure all instances have consistent columns for AClassID - if Length(AColumns) <> (LColumnMap.Count - 1) then // -1 to account for __KEY - raise EDataRow.Create('Internal Error: Inconsistent column counts for given AClassID. Ensure AClassID values are unique for each class of constructed TDataRow'); - for i := Low(AColumns) to High(AColumns) do begin - if NOT LColumnMap.ContainsKey(AColumns[i].Name) then - raise EDataRow.Create(Format('Internal Error: Inconsistent columns for given AClassID. Unknown column "%s".', [AColumns[i].Name])); + // Account and operation grids construct their first rows on different workers. + FColumnsLock.Acquire; + try + if not FColumns.TryGetValue(AClassID, LColumnMap) then + LColumnMap := MapColumns(AClassID, AColumns) + else begin + // Ensure all instances have consistent columns for AClassID + if Length(AColumns) <> (LColumnMap.Count - 1) then + raise EDataRow.Create('Internal Error: Inconsistent column counts for given AClassID. Ensure AClassID values are unique for each class of constructed TDataRow'); + for i := Low(AColumns) to High(AColumns) do begin + if NOT LColumnMap.ContainsKey(AColumns[i].Name) then + raise EDataRow.Create(Format('Internal Error: Inconsistent columns for given AClassID. Unknown column "%s".', [AColumns[i].Name])); + end; end; + finally + FColumnsLock.Release; end; TDataRowData(Result).vcolumnmap:=LColumnMap; SetLength(TDataRowData(Result).vvalues, LColumnMap.Count); @@ -380,12 +467,12 @@ class function TDataRow.New(AClassID:PtrInt; const AColumns: TDataColumns): Vari { TDataRowData } -function TDataRowData.HasData(const ABinding : AnsiString): boolean; +function TDataRowData.HasData(const ABinding : String): boolean; begin Result := Self.vcolumnmap.ContainsKey(ABinding) end; -function TDataRowData.GetData(const ABinding: AnsiString): Variant; +function TDataRowData.GetData(const ABinding: String): Variant; var LIndex : Integer; begin if NOT Self.vcolumnmap.TryGetValue(ABinding, LIndex) then @@ -393,7 +480,7 @@ function TDataRowData.GetData(const ABinding: AnsiString): Variant; Result := Self.vvalues[LIndex]; end; -procedure TDataRowData.SetData(const ABinding: AnsiString; const AValue: Variant); +procedure TDataRowData.SetData(const ABinding: String; const AValue: Variant); var LIndex : Integer; begin if NOT Self.vcolumnmap.TryGetValue(ABinding, LIndex) then @@ -408,6 +495,18 @@ constructor TCustomDataSource.Create(AOwner: TComponent); inherited Create(AOwner); FLock := TCriticalSection.Create; FClassID := PtrInt(ClassInfo); // Use pointer to ClassInfo as unique identifier for DataSource + FCachedGeneration := -1; +end; + +procedure TCustomDataSource.EnsureSnapshotCache; +begin + if Assigned(FSnapshotCache) then Exit; + FSnapshotCache := TSnapshotCache.Create(nil, + TCacheValueFetcher.FromObjectFunction(FetchSnapshot), + TCacheSizeEstimator.FromObjectFunction(EstimateSnapshot), + idpNone, TCacheReapPolicy.crpOldest, + TCacheExpirationPolicy.epNone, + TCacheNullValuePolicy.nvpCacheNormally, 2, TTimeSpan.MaxValue); end; destructor TCustomDataSource.Destroy; @@ -415,6 +514,7 @@ destructor TCustomDataSource.Destroy; i : integer; policy : TDisposePolicy; begin + FreeAndNil(FSnapshotCache); inherited; FreeAndNil(FLock); end; @@ -429,12 +529,115 @@ function TCustomDataSource.GetNullPolicy(const AFilter : TColumnFilter) : TSo end; end; +function TCustomDataSource.GetSnapshotCacheEnabled : Boolean; +begin + {$IFDEF FPC} + Result := InterlockedCompareExchange(FSnapshotCacheEnabled, 0, 0) <> 0; + {$ELSE} + Result := TInterlocked.CompareExchange(FSnapshotCacheEnabled, 0, 0) <> 0; + {$ENDIF} +end; + +procedure TCustomDataSource.SetSnapshotCacheEnabled(AEnabled : Boolean); +begin + {$IFDEF FPC} + if InterlockedExchange(FSnapshotCacheEnabled, Ord(AEnabled)) <> Ord(AEnabled) then + {$ELSE} + if TInterlocked.Exchange(FSnapshotCacheEnabled, Ord(AEnabled)) <> Ord(AEnabled) then + {$ENDIF} + InvalidateSnapshot; +end; + +procedure TCustomDataSource.InvalidateSnapshot; +begin + {$IFDEF FPC} + InterlockedIncrement64(FSnapshotGeneration); + {$ELSE} + TInterlocked.Increment(FSnapshotGeneration); + {$ENDIF} +end; + +function TCustomDataSource.EstimateSnapshot(const AItems : TSnapshotItems) : SizeInt; +begin + // Capacity counts snapshots, not individual rows: at most raw + current view. + Result := 1; +end; + +function TCustomDataSource.SnapshotQueryMatches({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams : TPageFetchParams) : Boolean; +var + i, j : Integer; + LLeft, LRight : TColumnFilter; +begin + Result := False; + if not FHasSnapshotParams then Exit; + if (FSnapshotParams.Operand <> AParams.Operand) or + (Length(FSnapshotParams.Filter) <> Length(AParams.Filter)) then Exit; + for i := 0 to High(AParams.Filter) do begin + LLeft := FSnapshotParams.Filter[i]; + LRight := AParams.Filter[i]; + if (LLeft.ColumnName <> LRight.ColumnName) or (LLeft.Sort <> LRight.Sort) or + (LLeft.Filter <> LRight.Filter) or (LLeft.SortSequence <> LRight.SortSequence) or + (Length(LLeft.Values) <> Length(LRight.Values)) then Exit; + for j := 0 to High(LLeft.Values) do begin + if VarType(LLeft.Values[j]) <> VarType(LRight.Values[j]) then Exit; + // Search criteria use scalar values. Unusual array/interface criteria are + // rebuilt instead of accepting an uncertain equality comparison. + if VarIsArray(LLeft.Values[j]) or VarIsByRef(LLeft.Values[j]) then Exit; + if not VarSameValue(LLeft.Values[j], LRight.Values[j]) then Exit; + end; + end; + Result := True; +end; + +procedure TCustomDataSource.CopySnapshotQuery({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams : TPageFetchParams); +var + i, j : Integer; +begin + FSnapshotParams := AParams; + FSnapshotParams.Filter := nil; + SetLength(FSnapshotParams.Filter, Length(AParams.Filter)); + for i := 0 to High(AParams.Filter) do begin + FSnapshotParams.Filter[i] := AParams.Filter[i]; + FSnapshotParams.Filter[i].Values := nil; + SetLength(FSnapshotParams.Filter[i].Values, Length(AParams.Filter[i].Values)); + for j := 0 to High(AParams.Filter[i].Values) do + FSnapshotParams.Filter[i].Values[j] := AParams.Filter[i].Values[j]; + end; + FHasSnapshotParams := True; +end; + +function TCustomDataSource.FetchSnapshot(const AKey : Integer) : TSnapshotItems; +var + LData : TList; + LRaw : TSnapshotItems; + LFilters : TArray; + LDisposables : TDisposables; +begin + LData := LDisposables.AddObject(TList.Create) as TList; + if AKey = 0 then begin + FetchAll(LData); + end else begin + LRaw := FSnapshotCache[0]; + LFilters := FSnapshotParams.GetSearchFilters; + if (Length(LFilters) = 0) and (Length(FSnapshotParams.GetSortFilters) = 0) then + Exit(LRaw); + LData.AddRange(LRaw); + if Length(LFilters) > 0 then + TListTool.FilterBy(LData, + TDataSourceTool.ConstructRowPredicate(LFilters, ApplyColumnFilter, FSnapshotParams.Operand)); + LFilters := FSnapshotParams.GetSortFilters; + if Length(LFilters) > 0 then + LData.Sort(TDataSourceTool.ConstructRowComparer(LFilters, ApplyColumnSort)); + end; + Result := LData.ToArray; +end; + function TCustomDataSource.GetItemDisposePolicy : TDisposePolicy; begin Result := idpNone; end; -function TCustomDataSource.ApplyColumnSort(constref Left, Right : T; constref AFilter: TColumnFilter) : Integer; +function TCustomDataSource.ApplyColumnSort({$IFDEF FPC}constref{$ELSE}const{$ENDIF} Left, Right : T; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AFilter: TColumnFilter) : Integer; var leftField, rightField : Variant; leftType, rightType : Integer; @@ -459,14 +662,14 @@ function TCustomDataSource.ApplyColumnSort(constref Left, Right : T; constref end; // Compare left/right values - Result := TCompare.Variant(@leftField, @rightField); + Result := TVariantTool.CompareVariant(leftField, rightField); // Invert result for descending if AFilter.Sort = sdDescending then Result := Result * -1; end; -function TCustomDataSource.ApplyColumnFilter(constref AItem: T; constref AFilter: TColumnFilter) : boolean; +function TCustomDataSource.ApplyColumnFilter({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AFilter: TColumnFilter) : boolean; var value : Variant; begin @@ -502,69 +705,83 @@ function TCustomDataSource.ApplyColumnFilter(constref AItem: T; constref AFil end else Result := false; end; -function TCustomDataSource.FetchPage(constref AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; +function TCustomDataSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; var i, j : SizeInt; data : TList; GC : TDisposables; pageStart, pageEnd : SizeInt; - entity : T; - comparer : IComparer; + LItems : TSnapshotItems; + LCount : SizeInt; + LGeneration : Int64; + LUseSnapshot : Boolean; filters : TArray; - filter : IPredicate; begin OnBeforeFetchAll(AParams); FLock.Acquire; try - // Fetch underlying data if stale - data := GC.AddObject( TList.Create ) as TList; - EnterLock; - try + LUseSnapshot := GetSnapshotCacheEnabled; + if LUseSnapshot then begin + EnsureSnapshotCache; + {$IFDEF FPC} + LGeneration := InterlockedCompareExchange64(FSnapshotGeneration, 0, 0); + {$ELSE} + LGeneration := TInterlocked.CompareExchange(FSnapshotGeneration, 0, 0); + {$ENDIF} + if LGeneration <> FCachedGeneration then begin + FSnapshotCache.Flush; + FHasSnapshotParams := False; + FCachedGeneration := LGeneration; + end; + if not SnapshotQueryMatches(AParams) then begin + FSnapshotCache.Remove(1); + CopySnapshotQuery(AParams); + end; + LItems := FSnapshotCache[1]; + LCount := Length(LItems); + end else begin + if Assigned(FSnapshotCache) then FSnapshotCache.Flush; + FHasSnapshotParams := False; + data := GC.AddObject(TList.Create) as TList; FetchAll(data); - finally - ReleaseLock; + filters := AParams.GetSearchFilters; + if Length(filters) > 0 then + TListTool.FilterBy(data, + TDataSourceTool.ConstructRowPredicate(filters, ApplyColumnFilter, AParams.Operand)); + filters := AParams.GetSortFilters; + if Length(filters) > 0 then + data.Sort(TDataSourceTool.ConstructRowComparer(filters, ApplyColumnSort)); + LCount := data.Count; end; - // Filter the data - filters := AParams.GetSearchFilters; - if Length(filters) > 0 then - TListTool.FilterBy( data, TDataSourceTool.ConstructRowPredicate( filters, ApplyColumnFilter, AParams.Operand ) ); - - // Sort the data - filters := AParams.GetSortFilters; - if Length(filters) > 0 then - data.Sort( TDataSourceTool.ConstructRowComparer( filters, ApplyColumnSort ) ); - - // Setup result - Result.TotalDataCount := data.Count; - if (Result.TotalDataCount = 0) then begin - Result.PageCount := 0; - Result.PageIndex := -1; - pageStart := 0; - pageEnd := -1; - end; - Result.PageCount := Ceil (data.Count / (1.0 *AParams.PageSize)); - if (AParams.PageSize >= 0) and (data.Count > 0) then begin + Result.TotalDataCount := LCount; + if LCount = 0 then begin + Result.PageCount := 0; + Result.PageIndex := -1; + pageStart := 0; + pageEnd := -1; + end else if AParams.PageSize > 0 then begin + Result.PageCount := ((LCount - 1) div AParams.PageSize) + 1; Result.PageIndex := ClipValue(AParams.PageIndex, 0, Result.PageCount - 1); pageStart := Result.PageIndex * AParams.PageSize; - pageEnd := ClipValue(pageStart + (AParams.PageSize - 1), pageStart, data.Count - 1); + pageEnd := ClipValue(pageStart + (AParams.PageSize - 1), pageStart, LCount - 1); end else begin + Result.PageCount := 1; Result.PageIndex := 0; pageStart := 0; - pageEnd := data.Count - 1; + pageEnd := LCount - 1; end; - - // Dehydrate the page of data only - - // Set columns ADataTable.Columns := GetColumns; - + // A reused table must also lose its previous rows when the result is empty. + SetLength(ADataTable.Rows, pageEnd - pageStart + 1); if pageEnd >= pageStart then begin j := 0; - SetLength(ADataTable.Rows, pageEnd - pageStart + 1); for i := pageStart to pageEnd do begin ADataTable.Rows[j] := TDataRow.New(FClassID, ADataTable.Columns); - DehydrateItem(data[i], ADataTable.Rows[j]); + if LUseSnapshot then + DehydrateItem(LItems[i], ADataTable.Rows[j]) + else + DehydrateItem(data[i], ADataTable.Rows[j]); inc(j); end; end; @@ -574,7 +791,7 @@ function TCustomDataSource.FetchPage(constref AParams: TPageFetchParams; var OnAfterFetchAll(AParams); end; -procedure TCustomDataSource.DehydrateItem(constref AItem: T; var ADataRow: Variant); +procedure TCustomDataSource.DehydrateItem({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: T; var ADataRow: Variant); var i : integer; LCols : TArray; @@ -591,11 +808,11 @@ procedure TCustomDataSource.DehydrateItem(constref AItem: T; var ADataRow: Va end; end; -procedure TCustomDataSource.OnBeforeFetchAll(constref AParams: TPageFetchParams); +procedure TCustomDataSource.OnBeforeFetchAll({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams); begin end; -procedure TCustomDataSource.OnAfterFetchAll(constref AParams: TPageFetchParams); +procedure TCustomDataSource.OnAfterFetchAll({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams); begin end; @@ -604,7 +821,7 @@ procedure TCustomDataSource.EnterLock; FLock.Acquire; end; -procedure TCustomDataSource.ReleaseLock; inline; +procedure TCustomDataSource.ReleaseLock; {$IFDEF FPC}inline;{$ENDIF} begin FLock.Release; end; @@ -617,7 +834,7 @@ constructor TColumnFilterPredicate.Create(const AFilter : TColumnFilter; cons FDelegate := ADelegate; end; -function TColumnFilterPredicate.Evaluate (constref AValue: T) : boolean; +function TColumnFilterPredicate.Evaluate ({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AValue: T) : boolean; begin Result := FDelegate(AValue, FFilter); end; @@ -630,7 +847,7 @@ constructor TColumnFieldComparer.Create(const AFilter : TColumnFilter; const FDelegate := ADelegate; end; -function TColumnFieldComparer.Compare(constref ALeft, ARight: T): Integer; +function TColumnFieldComparer.Compare({$IFDEF FPC}constref{$ELSE}const{$ENDIF} ALeft, ARight: T): Integer; begin Result := FDelegate(ALeft, ARight, FFilter); end; @@ -660,12 +877,6 @@ class function TDataSourceTool.ConstructRowComparer(const AFilters : TArray.ToArrayWorkAround( ParaList : __TList_IPredicate_T ) : TArray>; -begin - result := ParaList.ToArray; -end; - class function TDataSourceTool.ConstructRowPredicate(const AFilters : TArray; const ADelegate : TApplyFilterDelegate; const AOperand : TFilterOperand) : IPredicate; type __TColumnFilterPredicate_T = TColumnFilterPredicate; @@ -673,6 +884,7 @@ class function TDataSourceTool.ConstructRowPredicate(const AFilters : TArray< var i : integer; filters : __TList_IPredicate_T; + predicates : TArray<__IPredicate_T>; GC : TDisposables; begin filters := GC.AddObject( __TList_IPredicate_T.Create ) as __TList_IPredicate_T; @@ -682,16 +894,16 @@ class function TDataSourceTool.ConstructRowPredicate(const AFilters : TArray< end; end; - // Skybuck: Free Pascal Compiler Bug/Internal Error 2015052501 caused by buggy TList.ToArray case filters.Count of 0: Result := nil; 1: Result := filters[0]; - else case AOperand of -// foAnd: Result := __TPredicateTool_T.AndMany(filters.ToArray); // Skybuck: TList.ToArray causes FPC compiler to crash here. -// foOr: Result := __TPredicateTool_T.OrMany(filters.ToArray); - - foAnd: Result := __TPredicateTool_T.AndMany(ToArrayWorkAround(filters)); - foOr: Result := __TPredicateTool_T.OrMany(ToArrayWorkAround(filters)); + else begin + // FPC 3.2.0 cannot resolve the overloaded generic call on ToArray directly. + predicates := filters.ToArray; + case AOperand of + foAnd: Result := __TPredicateTool_T.AndMany(predicates); + foOr: Result := __TPredicateTool_T.OrMany(predicates); + end; end; end; end; @@ -710,8 +922,11 @@ function TPageFetchParams.GetSortFilters : TArray; var sortFilters : TList; GC : TDisposables; + {$IFNDEF FPC} + LPredicate : TNestedPredicateFunc; + {$ENDIF} - function IsSortFilter(constref AColFilter : TColumnFilter) : boolean; + function IsSortFilter({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AColFilter : TColumnFilter) : boolean; begin Result := AColFilter.Sort <> sdNone; end; @@ -719,7 +934,15 @@ function TPageFetchParams.GetSortFilters : TArray; begin sortFilters := GC.AddObject( TList.Create ) as TList; sortFilters.AddRange( Filter); + {$IFDEF FPC} TListTool.FilterBy( sortFilters, TPredicateTool.FromFunc( IsSortFilter )); + {$ELSE} + LPredicate := function(const AColFilter : TColumnFilter) : Boolean + begin + Result := AColFilter.Sort <> sdNone; + end; + TListTool.FilterBy(sortFilters, TPredicateTool.FromFunc(LPredicate)); + {$ENDIF} SetLength(Result, 0); if sortFilters.Count > 0 then Result := sortFilters.ToArray; @@ -729,8 +952,11 @@ function TPageFetchParams.GetSearchFilters : TArray; var searchFilters : TList; GC : TDisposables; + {$IFNDEF FPC} + LPredicate : TNestedPredicateFunc; + {$ENDIF} - function IsSearchFilter(constref AColFilter : TColumnFilter) : boolean; + function IsSearchFilter({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AColFilter : TColumnFilter) : boolean; begin Result := AColFilter.Filter <> vgfSortable; end; @@ -738,7 +964,15 @@ function TPageFetchParams.GetSearchFilters : TArray; begin searchFilters := GC.AddObject( TList.Create ) as TList; searchFilters.AddRange(Filter); + {$IFDEF FPC} TListTool.FilterBy( searchFilters, TPredicateTool.FromFunc( IsSearchFilter )); + {$ELSE} + LPredicate := function(const AColFilter : TColumnFilter) : Boolean + begin + Result := AColFilter.Filter <> vgfSortable; + end; + TListTool.FilterBy(searchFilters, TPredicateTool.FromFunc(LPredicate)); + {$ENDIF} SetLength(Result, 0); if searchFilters.Count > 0 then Result := searchFilters.ToArray; @@ -746,9 +980,7 @@ function TPageFetchParams.GetSearchFilters : TArray; { TSearchExpressionService } -class procedure TSearchExpressionService.Parse(const AExpression: utf8string; - const AExpressionKind: TExpressionKind; out - AExpressionRecord: TExpressionRecord); +class procedure TSearchExpressionService.Parse(const AExpression: utf8string; const AExpressionKind: TExpressionKind; out AExpressionRecord: TExpressionRecord); const MAX_VALUES = 2; type @@ -760,6 +992,9 @@ TUTF8Char = record Length: byte; Char: array [0..3] of AnsiChar; end; + {$IFNDEF FPC} + PUTF8String = ^UTF8String; + {$ENDIF} const CONVERTABLE_TOKENS_TO_STR = [tkLess..tkClosingBracket, tkComma]; @@ -767,7 +1002,15 @@ TUTF8Char = record procedure GetChar(APos: PAnsiChar; out AChar: TUTF8Char); begin + {$IFDEF FPC} AChar.Length := UTF8CharacterLength(APos); + {$ELSE} + // The expression was validated before scanning; retain UTF8 byte offsets. + if Byte(APos[0])<$80 then AChar.Length := 1 + else if Byte(APos[0])<$E0 then AChar.Length := 2 + else if Byte(APos[0])<$F0 then AChar.Length := 3 + else AChar.Length := 4; + {$ENDIF} if AChar.Length >= 1 then AChar.Char[0] := APos[0]; if AChar.Length >= 2 then AChar.Char[1] := APos[1]; @@ -788,18 +1031,18 @@ TUTF8Char = record var c, nc: PAnsiChar; i: Integer; - LDot: boolean = false; + LDot: boolean; LChar: TUTF8Char; - LValueIdx: Integer = -1; + LValueIdx: Integer; LValues: array[0..MAX_VALUES-1] of utf8string; // for now only 2 values for set "between" LValue: PUTF8String; - LToken: TToken = tkNone; - LPrevToken: TToken = tkNone; + LToken: TToken; + LPrevToken: TToken; LExpression: utf8string; - LLastPercent: boolean = false; + LLastPercent: boolean; LExpressionKind: TExpressionKind; LWasUnknow: boolean; - LHasDecimals: boolean = false; + LHasDecimals: boolean; procedure NextValue; begin @@ -809,7 +1052,7 @@ TUTF8Char = record LValue := @LValues[LValueIdx]; end; - procedure EscapeSequence(AChar: Char); + procedure EscapeSequence(AChar: AnsiChar); begin LToken := tkText; LValue^ := LValue^ + AChar; @@ -817,6 +1060,12 @@ TUTF8Char = record end; begin + LDot := False; + LValueIdx := -1; + LToken := tkNone; + LPrevToken := tkNone; + LLastPercent := False; + LHasDecimals := False; AExpressionRecord := Default(TExpressionRecord); if AExpression = '' then Exit; @@ -830,7 +1079,11 @@ TUTF8Char = record LExpression:=AExpression; c := @LExpression[1]; + {$IFDEF FPC} if FindInvalidUTF8Character(c, Length(LExpression)) <> -1 then + {$ELSE} + if not TEncoding.UTF8.IsBufferValid(Pointer(c), Length(LExpression)) then + {$ENDIF} raise ESearchExpressionParserException.Create(sInvalidUTF8String); NextValue; @@ -953,7 +1206,7 @@ TUTF8Char = record // text mode has precedence (parsing the expressions like: 123abs) if (LExpressionKind = ekNum) and (AExpressionKind = ekUnknown) - and (LToken in [tkText, tkPercent]) and (AExpressionRecord.NumericComparisionKind = nckUnknown) then + and (LToken in [tkText, tkPercent]) and (AExpressionRecord.NumericComparisonKind = nckUnknown) then begin LExpressionKind := ekText; end; @@ -1006,10 +1259,8 @@ TUTF8Char = record AExpressionRecord.TextMatchKind:=tmkMatchTextAnywhere; LLastPercent:=true; end; - tkNone: - AExpressionRecord.TextMatchKind:=tmkMatchTextEnd; - tkPercent: - raise ESearchExpressionParserException.CreateFmt(sUnexpectedOccurrenceOf_Found, ['%']); + tkNone: AExpressionRecord.TextMatchKind:=tmkMatchTextEnd; + tkPercent: raise ESearchExpressionParserException.CreateFmt(sUnexpectedOccurrenceOf_Found, ['%']); end; end else @@ -1022,14 +1273,13 @@ TUTF8Char = record else with AExpressionRecord do case LToken of - tkLess: NumericComparisionKind:=nckNumericLT; - tkGreater: NumericComparisionKind:=nckNumericGT; - tkEqual: NumericComparisionKind:=nckNumericEQ; - tkLessOrEqual: NumericComparisionKind:=nckNumericLTE; - tkGreaterOrEqual: NumericComparisionKind:=nckNumericGTE; + tkLess: NumericComparisonKind:=nckNumericLT; + tkGreater: NumericComparisonKind:=nckNumericGT; + tkEqual: NumericComparisonKind:=nckNumericEQ; + tkLessOrEqual: NumericComparisonKind:=nckNumericLTE; + tkGreaterOrEqual: NumericComparisonKind:=nckNumericGTE; end; - ekSet: - raise ESearchExpressionParserException.CreateFmt(sUnexpectedTokenFound, [TokenToStr(LToken)]); + ekSet: raise ESearchExpressionParserException.CreateFmt(sUnexpectedTokenFound, [TokenToStr(LToken)]); end; tkOpeningParenthesis, tkOpeningBracket: if LExpressionKind = ekSet then diff --git a/src/libraries/sphere10/UCommon.UI.pas b/src/libraries/sphere10/UCommon.UI.pas index 349209274..2b6e28a33 100644 --- a/src/libraries/sphere10/UCommon.UI.pas +++ b/src/libraries/sphere10/UCommon.UI.pas @@ -1,6 +1,6 @@ unit UCommon.UI; -{ Copyright (c) 2017 - 2018 Sphere 10 Software +{ Copyright (c) 2017 - 2018 Herman Schoenfeld Common UI tools. @@ -13,15 +13,34 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Public types in this unit + + Type | Purpose + ------------------------------------+------------------------------------------------------------------------- + TApplicationForm | Adds first-activation events and owned-object cleanup to forms. + TFormHelper | Provides the form-helper extension point. + TImageHelper | Copies an image-list entry into an image control. + TLoadingCircle | Draws and animates a component-owned loading indicator. + TThrottledEvent | Coalesces notifications using interval or quiet-burst timers. + TThrottledEventMode | Selects immediate, periodic or quiet-burst event delivery. + TWinControlHelper | Removes child controls and docks a control at the center. +} + +{$IFDEF FPC} {$mode delphi} +{$ENDIF} interface uses - Classes, SysUtils, Forms, Controls,ExtCtrls, - FGL, Graphics, Generics.Collections, Generics.Defaults, syncobjs, - UMemory, LMessages, UCommon; + Classes, SysUtils, Math, Generics.Collections, Generics.Defaults, SyncObjs, + {$IFDEF FPC} + Forms, Controls, ExtCtrls, FGL, Graphics, LMessages, + {$ELSE} + Vcl.Forms, Vcl.Controls, Vcl.ExtCtrls, Vcl.Graphics, Vcl.ImgList, Winapi.Windows, + {$ENDIF} + UMemory, UCommon; type @@ -47,21 +66,42 @@ TApplicationForm = class(TForm) public property ActivationCount : UInt32 read FActivatedCount; function AutoDispose(AObject : TObject) : TObject; + {$IFNDEF FPC}function IsVisible:Boolean;{$ENDIF} published property CloseAction : TCloseAction read FCloseAction write FCloseAction; property OnActivateFirstTime : TNotifyEvent read FActivateFirstTime write FActivateFirstTime; property OnDestroyed : TNotifyEvent read FDestroyed write FDestroyed; end; + // Shared delivery policy for throttled UI notifications. + TThrottledEventMode = (temNone, temNotifyEveryInterval, temNotifyOnEventBurstFinished, temNotifyOnEventBurstStartAndFinished); + { TThrottledEvent } TThrottledEvent = class(TComponent) public const CT_DEFAULT_DELAYEDREFRESH_MS = 1000; - public type - TThrottledEventMode = (temNone, temNotifyEveryInterval, temNotifyOnEventBurstFinished, temNotifyOnEventBurstStartAndFinished); + private type + // A callback may dispose its sender. Reuse component free notification + // to keep the current invocation from touching that released component. + TInvocation = class(TComponent) + private + FSubject: TComponent; + function GetAlive: Boolean; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + constructor Create(ASubject: TComponent); override; + destructor Destroy; override; + property Alive: Boolean read GetAlive; + end; private FHandler : TNotifyManyEvent; + FBurstStarted, FBurstFinishing, FBurstFinished : TNotifyManyEvent; + FWaitingChanged: TNotifyManyEvent; + FBurstActive : Boolean; + // Stop obsolete callback snapshots after cancellation or a replacement burst. + FStateVersion: UInt64; FTimer: TTimer; FMode : TThrottledEventMode; FInterval : TTimeSpan; @@ -69,19 +109,44 @@ TThrottledEvent = class(TComponent) FLastActualNotify : TDateTime; FSuppressedInvocations : Integer; procedure SetInterval(const ATimeSpan : TTimeSpan); + procedure SetMode(AMode: TThrottledEventMode); procedure OnTimer(Sender: TObject); - procedure NotifyNow; - procedure NotifyLater; + procedure NotifyNow(AInvocation: TInvocation; ALeading: Boolean = False); + procedure NotifyLater(AInvocation: TInvocation); + function BeginBurst(AInvocation: TInvocation): Boolean; + procedure FinishBurst(AInvocation: TInvocation); + procedure ArmBurstTimer; + procedure Cancel(AInvocation: TInvocation); overload; + procedure InvokeCallbacks(const AEvent: TNotifyManyEvent; ASender: TObject; AInvocation: TInvocation; AVersion: UInt64); + procedure EndWaiting(AInvocation: TInvocation; AFinished: Boolean); public property Interval : TTimeSpan read FInterval write SetInterval; - property Mode : TThrottledEventMode read FMode write FMode; + property Mode : TThrottledEventMode read FMode write SetMode; property LastClientNotify : TDateTime read FLastClientNotify; property LastActualNotify : TDateTime read FLastActualNotify; property SuppressedInvocations : Integer read FSuppressedInvocations; + // Quiet-burst lifecycle, driven by this component's owned timer. A single + // leading input finishes its lifecycle without another Add notification. + property BurstActive : Boolean read FBurstActive; + // Canonical quiet-burst state, independent of consumers or their work. + // The state and owned timer are committed before change callbacks run. + property Waiting: Boolean read FBurstActive; + property OnWaitingChanged: TNotifyManyEvent read FWaitingChanged; + property OnBurstStarted : TNotifyManyEvent read FBurstStarted; + // Optional synchronous listeners can explicitly rearm the current burst. + // OnBurstFinished is raised only when this attempt was not rearmed. + property OnBurstFinishing : TNotifyManyEvent read FBurstFinishing; + property OnBurstFinished : TNotifyManyEvent read FBurstFinished; constructor Create(Owner:TComponent); override; procedure Add(AListener : TNotifyEvent; ExecuteMainThread : Boolean = False); procedure Remove(AListener : TNotifyEvent); procedure Notify; + // Rearm a requested delivery without starting a new leading burst. The + // optional elapsed quiet time is supplied explicitly by that caller. + procedure Postpone(AQuietMilliseconds: Cardinal = 0); + // Cancel pending delivery and waiting without publishing a quiet finish. + // Changing Mode also cancels its previous pending burst/interval. + procedure Cancel; overload; end; { TWinControlHelper } @@ -99,21 +164,85 @@ TFormHelper = class helper for TForm { TImageHelper } TImageHelper = class helper for TImage - procedure SetImageListPicture(AImageList: TImageList; AIndex : SizeInt); + procedure SetImageListPicture(AImageList: TImageList; AIndex : {$IFDEF FPC}SizeInt{$ELSE}NativeInt{$ENDIF}); + end; + +{ Native LCL/VCL port copyright © 2026 Herman Schoenfeld + + Rounded-spoke loading indicator adapted from Sphere10 Framework's + Windows.Forms.LoadingCircle, using its MacOSX appearance and fading palette. + + Original LoadingCircle copyright © 2006, 2007 Martin R. Gagné + (martingagne@gmail.com). All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +} + +type + { TLoadingCircle + + Component-owned animation: no worker or queued callback outlives the + control. Active must be changed on the UI thread. Colors are composited + against Color to reproduce the original alpha-faded spokes on an opaque + native control. } + TLoadingCircle = class(TCustomControl) + private + FTimer : TTimer; + FActive : Boolean; + FPosition : Integer; + FSpokeColor : TColor; + procedure SetActive(AValue : Boolean); + procedure SetSpokeColor(AValue : TColor); + procedure OnTimer(Sender : TObject); + protected + procedure Paint; override; + public + constructor Create(AOwner : TComponent); override; + destructor Destroy; override; + published + property Active : Boolean read FActive write SetActive default False; + property SpokeColor : TColor read FSpokeColor write SetSpokeColor default $00A9A9A9; + property Align; + property Anchors; + property Color; + property ParentColor; + property Visible; end; implementation +{$IFDEF FPC} uses LCLIntf; +{$ENDIF} {%region TApplicationForm} procedure TApplicationForm.DoCreate; begin - inherited; + {$IFDEF FPC}inherited;{$ENDIF} FUILock := TCriticalSection.Create; FActivatedCount := 0; FCloseAction:=caHide; + {$IFNDEF FPC}inherited;{$ENDIF} end; procedure TApplicationForm.Activate; @@ -125,7 +254,8 @@ procedure TApplicationForm.Activate; end; procedure TApplicationForm.ActivateFirstTime; -begin; +begin + ; end; procedure TApplicationForm.DoClose(var CloseAction: TCloseAction); @@ -143,7 +273,8 @@ procedure TApplicationForm.DoDestroy; end; procedure TApplicationForm.DoDestroyed; -begin; +begin + ; end; function TApplicationform.AutoDispose(AObject : TObject) : TObject; @@ -151,6 +282,19 @@ function TApplicationform.AutoDispose(AObject : TObject) : TObject; Result := FDisposables.AddObject(AObject); end; +{$IFNDEF FPC} +function TApplicationForm.IsVisible:Boolean; +var LParent:TControl; +begin + Result:=Visible; + LParent:=Parent; + while Result and Assigned(LParent) do begin + Result:=LParent.Visible; + LParent:=LParent.Parent; + end; +end; +{$ENDIF} + procedure TApplicationForm.NotifyActivateFirstTime; begin ActivateFirstTime; @@ -169,6 +313,30 @@ procedure TApplicationForm.NotifyDestroyed; {%region TThrottledEvent } +constructor TThrottledEvent.TInvocation.Create(ASubject: TComponent); +begin + inherited Create(nil); + FSubject := ASubject; + FSubject.FreeNotification(Self); +end; + +destructor TThrottledEvent.TInvocation.Destroy; +begin + if Assigned(FSubject) then FSubject.RemoveFreeNotification(Self); + inherited; +end; + +function TThrottledEvent.TInvocation.GetAlive: Boolean; +begin + Result := Assigned(FSubject); +end; + +procedure TThrottledEvent.TInvocation.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited; + if (Operation = opRemove) and (AComponent = FSubject) then FSubject := nil; +end; + constructor TThrottledEvent.Create(Owner:TComponent); begin Inherited Create(Owner); @@ -194,27 +362,157 @@ procedure TThrottledEvent.Remove(AListener : TNotifyEvent); procedure TThrottledEvent.Notify; var LIdleDuration : TTimeSpan; + LImmediate: Boolean; + LDisposed: TDisposables; + LInvocation: TInvocation; begin - FLastClientNotify:=Now; - LIdleDuration := TTimeSpan.Subtract(Now, FLastActualNotify); - if (FMode = temNone) OR ((NOT FTimer.Enabled) AND (LIdleDuration > Interval) AND (FMode <> temNotifyOnEventBurstFinished)) then - NotifyNow + LInvocation := LDisposed.AddObject(TInvocation.Create(Self)) as TInvocation; + // A completed trailing delivery does not extend the previous input burst. + // Measure its quiet time before recording the next client notification. + if FMode = temNotifyOnEventBurstStartAndFinished then + LIdleDuration := TTimeSpan.Subtract(Now, FLastClientNotify) else - NotifyLater; + LIdleDuration := TTimeSpan.Subtract(Now, FLastActualNotify); + FLastClientNotify:=Now; + // A single leading input also has a lifecycle timer now, but that timer is + // not a pending ordinary delivery. Preserve immediate independent inputs. + LImmediate := (FMode = temNone) or + (((not FTimer.Enabled) or ((FMode = temNotifyOnEventBurstStartAndFinished) and + (FSuppressedInvocations = 0))) and (LIdleDuration > Interval) and + (FMode <> temNotifyOnEventBurstFinished)); + try + if LImmediate then NotifyNow(LInvocation, True) else NotifyLater(LInvocation); + finally + if LInvocation.Alive then + if FBurstActive and not FTimer.Enabled then ArmBurstTimer; + end; +end; + +procedure TThrottledEvent.InvokeCallbacks(const AEvent: TNotifyManyEvent; ASender: TObject; AInvocation: TInvocation; AVersion: UInt64); +var LHandlers, LMainHandlers: TArray; LHandler: TNotifyEvent; +begin + // Snapshot the existing event record, not a new event mechanism. Subscription + // changes cannot invalidate enumeration, and disposal ends the invocation. + LHandlers := TArrayTool.Copy(AEvent.Handlers); + LMainHandlers := TArrayTool.Copy(AEvent.MainThreadHandlers); + for LHandler in LHandlers do begin + if not AInvocation.Alive then Exit; + if FStateVersion <> AVersion then Exit; + if TArrayTool.Contains(AEvent.Handlers, LHandler) then LHandler(ASender); + end; + for LHandler in LMainHandlers do begin + if not AInvocation.Alive then Exit; + if FStateVersion <> AVersion then Exit; + if TArrayTool.Contains(AEvent.MainThreadHandlers, LHandler) then + TThreadNotify.InvokeMainThread(ASender, LHandler); + end; +end; + +function TThrottledEvent.BeginBurst(AInvocation: TInvocation): Boolean; +var LVersion: UInt64; +begin + Result := True; + if FBurstActive or not (FMode in [temNotifyOnEventBurstFinished, + temNotifyOnEventBurstStartAndFinished]) then Exit; + FBurstActive := True; + Inc(FStateVersion); + LVersion := FStateVersion; + // Waiting always owns an armed timer before its outward start callbacks. + ArmBurstTimer; + InvokeCallbacks(FWaitingChanged, Self, AInvocation, LVersion); + if not AInvocation.Alive then Exit(False); + if FStateVersion <> LVersion then Exit(False); + InvokeCallbacks(FBurstStarted, Self, AInvocation, LVersion); + if not AInvocation.Alive then Exit(False); + Result := FBurstActive and (FStateVersion = LVersion); +end; + +procedure TThrottledEvent.ArmBurstTimer; +var LQuiet: Double; +begin + LQuiet := TTimeSpan.Subtract(Now,FLastClientNotify).TotalMilliseconds; + FTimer.Enabled := False; + FTimer.Interval := ClipValue(Round(Abs(FInterval.TotalMilliseconds)-LQuiet),10,High(Integer)); + FTimer.Enabled := True; +end; + +procedure TThrottledEvent.EndWaiting(AInvocation: TInvocation; AFinished: Boolean); +var LVersion: UInt64; +begin + if not FBurstActive then Exit; + FBurstActive := False; + Inc(FStateVersion); + LVersion := FStateVersion; + FTimer.Enabled := False; + InvokeCallbacks(FWaitingChanged, Self, AInvocation, LVersion); + if not AInvocation.Alive then Exit; + if FStateVersion <> LVersion then Exit; + if AFinished then InvokeCallbacks(FBurstFinished, Self, AInvocation, LVersion); +end; + +procedure TThrottledEvent.FinishBurst(AInvocation: TInvocation); +var LException: Pointer; LVersion: UInt64; +begin + if not FBurstActive then Exit; + LException := nil; + LVersion := FStateVersion; + try + InvokeCallbacks(FBurstFinishing, Self, AInvocation, LVersion); + except + LException := AcquireExceptionObject; + end; + if AInvocation.Alive then + if (FStateVersion = LVersion) and not FTimer.Enabled then begin + try + EndWaiting(AInvocation, True); + except + if not Assigned(LException) then LException := AcquireExceptionObject; + end; + end; + if Assigned(LException) then raise TObject(LException); end; -procedure TThrottledEvent.NotifyNow; +procedure TThrottledEvent.Postpone(AQuietMilliseconds: Cardinal); +var LDisposed: TDisposables; LInvocation: TInvocation; +begin + LInvocation := LDisposed.AddObject(TInvocation.Create(Self)) as TInvocation; + if FSuppressedInvocations=0 then FSuppressedInvocations := 1; + FLastClientNotify := Now-TTimeSpan.FromMilliseconds(AQuietMilliseconds).TotalDays; + try + if BeginBurst(LInvocation) then ArmBurstTimer; + finally + if LInvocation.Alive then + if FBurstActive and not FTimer.Enabled then ArmBurstTimer; + end; +end; + +procedure TThrottledEvent.NotifyNow(AInvocation: TInvocation; ALeading: Boolean); +var LVersion: UInt64; begin FTimer.Enabled := false; + if ALeading then FSuppressedInvocations := 0; FLastActualNotify:=Now; - FHandler.Invoke(nil); - FSuppressedInvocations:=0; + if not BeginBurst(AInvocation) then Exit; + LVersion := FStateVersion; + try + InvokeCallbacks(FHandler, nil, AInvocation, LVersion); + finally + // A listener may postpone this delivery again, or notify during its callback. + // Retain that pending delayed invocation and its observable suppressed count. + if AInvocation.Alive then + if FStateVersion = LVersion then + if not FTimer.Enabled then FSuppressedInvocations:=0; + end; end; -procedure TThrottledEvent.NotifyLater; +procedure TThrottledEvent.NotifyLater(AInvocation: TInvocation); begin inc(FSuppressedInvocations); - if NOT FTimer.Enabled then begin + // Burst modes restart the actual native timer for each input. Periodic mode + // keeps its existing deadline while notifications continue to arrive. + if FMode in [temNotifyOnEventBurstFinished,temNotifyOnEventBurstStartAndFinished] then begin + if BeginBurst(AInvocation) then ArmBurstTimer; + end else if NOT FTimer.Enabled then begin FTimer.Interval := ClipValue( Round( Abs( FInterval.TotalMilliseconds ) ), 10, High(integer)) ; FTimer.Enabled:=true; end; @@ -222,24 +520,72 @@ procedure TThrottledEvent.NotifyLater; procedure TThrottledEvent.OnTimer(Sender: TObject); var LDuration : TTimeSpan; + LDisposed: TDisposables; + LInvocation: TInvocation; + LException: Pointer; begin + if not FTimer.Enabled then Exit; + LInvocation := LDisposed.AddObject(TInvocation.Create(Self)) as TInvocation; + LException := nil; + try case FMode of - temNone: NotifyNow; + temNone: NotifyNow(LInvocation); temNotifyEveryInterval: begin LDuration := TTimeSpan.Subtract(Now, FLastActualNotify); if LDuration > FInterval then - NotifyNow + NotifyNow(LInvocation) else FTimer.Interval := ClipValue( Round( Abs ( (FInterval - LDuration).TotalMilliseconds)), 10, High(integer)); end; temNotifyOnEventBurstStartAndFinished, temNotifyOnEventBurstFinished: begin LDuration := TTimeSpan.Subtract(Now, FLastClientNotify); - if LDuration > FInterval then - NotifyNow + if LDuration > FInterval then begin + FTimer.Enabled := False; + if FSuppressedInvocations > 0 then NotifyNow(LInvocation); + end else FTimer.Interval := ClipValue( Round( Abs ( (FInterval - LDuration).TotalMilliseconds)), 10, High(integer)); end; end; + except + LException := AcquireExceptionObject; + end; + // The ordinary callback may fail. Complete its quiet lifecycle anyway, or + // retain an explicit rearm, before propagating the original exception. + if LInvocation.Alive then + if FBurstActive and not FTimer.Enabled then begin + try + FinishBurst(LInvocation); + except + if not Assigned(LException) then LException := AcquireExceptionObject; + end; + end; + if Assigned(LException) then raise TObject(LException); +end; + +procedure TThrottledEvent.Cancel(AInvocation: TInvocation); +begin + FTimer.Enabled := False; + FSuppressedInvocations := 0; + FLastClientNotify := MinDateTime; + FLastActualNotify := MinDateTime; + EndWaiting(AInvocation, False); +end; + +procedure TThrottledEvent.Cancel; +var LDisposed: TDisposables; LInvocation: TInvocation; +begin + LInvocation := LDisposed.AddObject(TInvocation.Create(Self)) as TInvocation; + Cancel(LInvocation); +end; + +procedure TThrottledEvent.SetMode(AMode: TThrottledEventMode); +var LDisposed: TDisposables; LInvocation: TInvocation; +begin + if FMode = AMode then Exit; + LInvocation := LDisposed.AddObject(TInvocation.Create(Self)) as TInvocation; + FMode := AMode; + Cancel(LInvocation); end; procedure TThrottledEvent.SetInterval(const ATimeSpan : TTimeSpan); @@ -259,6 +605,8 @@ procedure TWinControlHelper.RemoveAllControls(destroy : boolean); begin while self.ControlCount > 0 do begin control := self.Controls[0]; + // A visible form becomes a top-level window when its parent is removed. + control.Hide; self.RemoveControl(control); if destroy then control.Destroy; end; @@ -267,7 +615,7 @@ procedure TWinControlHelper.RemoveAllControls(destroy : boolean); procedure TWinControlHelper.AddControlDockCenter(AControl: TWinControl); begin if AControl.ClassType.InheritsFrom(TCustomForm) then begin - with TCustomForm(AControl) do begin + with TForm(AControl) do begin // Needed to avoid infinite WMSize loop Constraints.MinHeight := 0; Constraints.MaxHeight := 0; @@ -278,8 +626,16 @@ procedure TWinControlHelper.AddControlDockCenter(AControl: TWinControl); end; with AControl do begin - Align := alClient; + Hide; + Align := alNone; + if AControl is TCustomForm then begin + TForm(AControl).BorderStyle := bsNone; + TForm(AControl).Position := poDesigned; + end; Parent := Self; + BoundsRect := Self.ClientRect; + HandleNeeded; + Align := alClient; Show; if AControl.ClassType.InheritsFrom(TApplicationForm) then TApplicationForm(AControl).Activate; @@ -295,11 +651,122 @@ procedure TWinControlHelper.AddControlDockCenter(AControl: TWinControl); {%region TImageHelper} -procedure TImageHelper.SetImageListPicture(AImageList: TImageList; AIndex : SizeInt); +procedure TImageHelper.SetImageListPicture(AImageList: TImageList; AIndex : {$IFDEF FPC}SizeInt{$ELSE}NativeInt{$ENDIF}); begin + {$IFDEF FPC} AImageList.GetBitmap(AIndex, Self.Picture.Bitmap); + {$ELSE} + // VCL GetBitmap flattens the image onto an opaque canvas. Its native icon + // extraction preserves the existing image-list mask and alpha channel. + AImageList.GetIcon(AIndex, Self.Picture.Icon); + {$ENDIF} end; {%endregion} +{ TLoadingCircle } + +constructor TLoadingCircle.Create(AOwner : TComponent); +begin + inherited Create(AOwner); + Width := 36; + Height := 36; + Color := clWindow; + FSpokeColor := $00A9A9A9; // System.Drawing.Color.DarkGray in the original control. + DoubleBuffered := True; + FTimer := TTimer.Create(Self); + FTimer.Enabled := False; + FTimer.Interval := 100; + FTimer.OnTimer := OnTimer; +end; + +destructor TLoadingCircle.Destroy; +begin + if Assigned(FTimer) then FTimer.Enabled := False; + inherited Destroy; +end; + +procedure TLoadingCircle.SetActive(AValue : Boolean); +begin + if FActive = AValue then Exit; + FActive := AValue; + FTimer.Enabled := AValue; + if not AValue then FPosition := 0; + Invalidate; +end; + +procedure TLoadingCircle.SetSpokeColor(AValue : TColor); +begin + if FSpokeColor = AValue then Exit; + FSpokeColor := AValue; + Invalidate; +end; + +procedure TLoadingCircle.OnTimer(Sender : TObject); +begin + {$IFDEF FPC}if not IsVisible then Exit;{$ELSE}if not IsWindowVisible(Handle) then Exit;{$ENDIF} + FPosition := (FPosition + 1) mod 12; + Invalidate; +end; + +procedure TLoadingCircle.Paint; +var + i, LAlpha, LThickness : Integer; + LRed, LGreen, LBlue, LBackRed, LBackGreen, LBackBlue : Byte; + LAngle, LRadius, LCenterX, LCenterY : Double; + {$IFNDEF FPC} + LBrush: TLogBrush; + LPen, LPreviousPen: HGDIOBJ; + {$ENDIF} +begin + Canvas.Brush.Color := Color; + Canvas.FillRect(ClientRect); + {$IFDEF FPC} + RedGreenBlue(ColorToRGB(FSpokeColor),LRed,LGreen,LBlue); + RedGreenBlue(ColorToRGB(Color),LBackRed,LBackGreen,LBackBlue); + {$ELSE} + LRed:=GetRValue(ColorToRGB(FSpokeColor)); LGreen:=GetGValue(ColorToRGB(FSpokeColor)); LBlue:=GetBValue(ColorToRGB(FSpokeColor)); + LBackRed:=GetRValue(ColorToRGB(Color)); LBackGreen:=GetGValue(ColorToRGB(Color)); LBackBlue:=GetBValue(ColorToRGB(Color)); + {$ENDIF} + LRadius := Min(Width,Height) * 11 / 32; + LCenterX := Width / 2; + LCenterY := Height / 2; + LThickness := Max(2,Round(Min(Width,Height) / 16)); + Canvas.Pen.Width := LThickness; + {$IFDEF FPC}Canvas.Pen.EndCap := pecRound;{$ENDIF} + for i := 0 to 11 do begin + if (not FActive) or (i = 0) then LAlpha := 255 + else LAlpha := i * (255 div 12); + Canvas.Pen.Color := {$IFDEF FPC}RGBToColor{$ELSE}RGB{$ENDIF}( + (LRed * LAlpha + LBackRed * (255-LAlpha)) div 255, + (LGreen * LAlpha + LBackGreen * (255-LAlpha)) div 255, + (LBlue * LAlpha + LBackBlue * (255-LAlpha)) div 255); + LAngle := (FPosition + i + 1) * (2 * Pi / 12); + {$IFDEF FPC} + Canvas.Line( + Round(LCenterX + LRadius * 5 / 11 * Cos(LAngle)), + Round(LCenterY + LRadius * 5 / 11 * Sin(LAngle)), + Round(LCenterX + LRadius * Cos(LAngle)), + Round(LCenterY + LRadius * Sin(LAngle))); + {$ELSE} + // VCL's TPen has no EndCap property. Retain rounded spokes with a native + // geometric pen, restoring the canvas HDC before releasing its handle. + LBrush.lbStyle:=BS_SOLID; + LBrush.lbColor:=ColorToRGB(Canvas.Pen.Color); + LBrush.lbHatch:=0; + LPen:=ExtCreatePen(PS_GEOMETRIC or PS_SOLID or PS_ENDCAP_ROUND or PS_JOIN_ROUND, + LThickness,LBrush,0,nil); + if LPen=0 then RaiseLastOSError; + LPreviousPen:=SelectObject(Canvas.Handle,LPen); + try + MoveToEx(Canvas.Handle,Round(LCenterX+LRadius*5/11*Cos(LAngle)),Round(LCenterY+LRadius*5/11*Sin(LAngle)),nil); + Winapi.Windows.LineTo(Canvas.Handle,Round(LCenterX+LRadius*Cos(LAngle)),Round(LCenterY+LRadius*Sin(LAngle))); + finally + SelectObject(Canvas.Handle,LPreviousPen); + DeleteObject(LPen); + end; + {$ENDIF} + end; +end; + end. diff --git a/src/libraries/sphere10/UCommon.pas b/src/libraries/sphere10/UCommon.pas index 7ea9e9bdc..3502710df 100644 --- a/src/libraries/sphere10/UCommon.pas +++ b/src/libraries/sphere10/UCommon.pas @@ -1,4 +1,4 @@ -{ Copyright (c) 2017 - 2018 Sphere 10 Software +{ Copyright (c) 2017 - 2018 Herman Schoenfeld Common tools. @@ -12,6 +12,34 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Public types in this unit + + Type | Purpose + -----------------------+------------------------------------------------------------------------- + DWord | Aliases a 32-bit unsigned integer for Delphi compatibility. + PtrInt | Aliases a pointer-sized signed integer for Delphi compatibility. + QWord | Aliases a 64-bit unsigned integer for Delphi compatibility. + SizeInt | Aliases a native-sized signed integer for Delphi compatibility. + TArrayTool | Searches, copies and edits generic arrays. + TBox | Holds a generic value in an object with reference identity. + TCPUTool | Reports the number of logical processors. + TDateTimeHelper | Adds date boundaries, arithmetic, comparisons and formatting. + TFileTool | Appends UTF-8 text lines to files. + TLogMessage | Stores diagnostic text with its severity and timestamp. + TLogSeverity | Classifies diagnostic messages by severity. + TMemoryStreamHelper | Copies memory-stream contents into a byte array. + TNotifyManyEvent | Retains multicast handlers and their main-thread delivery choices. + TNotifyManyEventHelper | Adds, removes and invokes multicast handlers. + TNullable | Stores an optional generic value with an explicit empty state. + TResult | Collects diagnostics and an optional result value with success status. + TScope | Runs a retained cleanup method when the final owning scope ends. + TStatistics | Maintains running totals, extrema, means and variance estimates. + TStreamHelper | Adds UTF-8 string writing and byte reads to streams. + TThreadNotify | Queues notifications for delivery on a selected thread. + TTimeSpan | Represents durations with arithmetic and millisecond conversions. + TVariantTool | Compares, converts and matches text and numeric Variant values. +} + unit UCommon; {$IFDEF FPC} @@ -28,7 +56,7 @@ interface {$ELSE}{$IFDEF LINUX} {$linklib c} ctypes, {$ENDIF LINUX} {$IFDEF WINDOWS} Windows, {$ENDIF WINDOWS} {$IF DEFINED(DARWIN) OR DEFINED(FREEBSD)} ctypes, sysctl, {$ENDIF} - {$ENDIF} Variants, math, typinfo, UMemory, syncobjs; + {$ENDIF} Variants, math, typinfo, syncobjs; { CONSTANTS } @@ -88,9 +116,44 @@ function GetSetValue(const aSet:PTypeInfo; Name: String): Integer; type + { A managed cleanup owner. Copies share one deferred method; the last copy + invokes it. Keep a local variable for routine-scoped ownership. Cleanup + runs on the thread releasing the final reference, so thread-affine owners + must not escape their acquiring thread. } + TScope = record + private type + TGuard = class(TInterfacedObject) + private + FCleanup: TThreadMethod; + public + constructor Create(const ACleanup: TThreadMethod); + destructor Destroy; override; + end; + private + FGuardian: IInterface; + function GetIsEmpty: Boolean; + public + // Registers a deferred method without invoking it. Its borrowed target + // must stay alive until cleanup; Target.Free can arrange its release. + class function Create(const ACleanup: TThreadMethod): TScope; static; + {$IFDEF FPC} + class operator Copy(constref ASource: TScope; var ADestination: TScope); + {$ELSE} + class operator Assign(var ADestination: TScope; const [ref] ASource: TScope); + {$ENDIF} + class operator Finalize(var AScope: TScope); + // Drops only this copy's ownership; other copies keep cleanup alive. + procedure Reset; + property IsEmpty: Boolean read GetIsEmpty; + end; + {$IFNDEF FPC} // Delphi compatibility SizeInt = NativeInt; + PtrInt = NativeInt; + QWord = UInt64; + DWord = Cardinal; + TTimeSpan = System.TimeSpan.TTimeSpan; {$ENDIF} { TNullable } @@ -360,6 +423,7 @@ TPendingNotifyManyEvent = record FLock : TCriticalSection; FPendingNotifications : TList; procedure InvokePendingOnTargetThread; + procedure InvokeQueued(Sender : TObject); public constructor Create(ATargetThread : TThread); destructor Destroy; override; @@ -476,6 +540,8 @@ TStatistics = record property Maximum : Double read FMax; procedure Reset; function Mean : Double; inline; + // Population variance needs one value; sample variance needs two. + // Undefined variances return NaN, without changing the accumulated data. function PopulationVariance : Double; inline; function PopulationStandardDeviation : Double; inline; function PopulationVariationCoefficient : Double; inline; @@ -503,6 +569,76 @@ implementation uses dateutils, StrUtils; +{ TScope } + +constructor TScope.TGuard.Create(const ACleanup: TThreadMethod); +begin + inherited Create; + FCleanup := ACleanup; +end; + +destructor TScope.TGuard.Destroy; +var CleanupMethod: TThreadMethod; +begin + CleanupMethod := FCleanup; + FCleanup := nil; + try + try + if Assigned(CleanupMethod) then CleanupMethod; + finally + inherited; + end; + except + // The outer destructor skips its instance release when Cleanup raises. + // The private guardian must still be freed before its exception propagates. + FreeInstance; + raise; + end; +end; + +class function TScope.Create(const ACleanup: TThreadMethod): TScope; +begin + if not Assigned(ACleanup) then + raise EArgumentNilException.Create('ACleanup'); + Result.FGuardian := TGuard.Create(ACleanup); +end; + +function TScope.GetIsEmpty: Boolean; +begin + Result := not Assigned(FGuardian); +end; + +{$IFDEF FPC} +class operator TScope.Copy(constref ASource: TScope; var ADestination: TScope); +{$ELSE} +class operator TScope.Assign(var ADestination: TScope; const [ref] ASource: TScope); +{$ENDIF} +var Guardian: IInterface; +begin + if @ASource = @ADestination then Exit; + // Pin the source before releasing the destination. If cleanup raises, the + // destination stays empty and the original source retains its ownership. + Guardian := ASource.FGuardian; + ADestination.Reset; + ADestination.FGuardian := Guardian; +end; + +class operator TScope.Finalize(var AScope: TScope); +begin + // Detach before the RTL finalizes managed fields, including overwrite paths. + AScope.Reset; +end; + +procedure TScope.Reset; +var Guardian: Pointer; +begin + // Detach this reference before invoking user cleanup, which may raise. A raw + // local prevents a second compiler-managed release during exception unwind. + Guardian := Pointer(FGuardian); + Pointer(FGuardian) := nil; + if Assigned(Guardian) then IInterface(Guardian)._Release; +end; + const IntlDateTimeFormat : TFormatSettings = ( DateSeparator : '-'; @@ -521,9 +657,7 @@ implementation {$IF DEFINED(LINUX)} _SC_NPROCESSORS_ONLN = 83; - function sysconf(i: cint): clong; cdecl; external Name 'sysconf'; - {$ENDIF LINUX} - {$ENDIF} + function sysconf(i: cint): clong; cdecl; external Name 'sysconf'; {$ENDIF LINUX} {$ENDIF} var @@ -1017,7 +1151,8 @@ function TTimeSpan.GetTotalSeconds: Double; function TTimeSpan.GetTotalMilliseconds: Double; begin - Result := Double(FMillis); + // Assignment performs a numeric conversion in both Delphi and FPC Delphi mode. + Result := FMillis; end; class function TTimeSpan.Normalize(const ADateTime : TDateTime) : Int64; static; @@ -1223,7 +1358,10 @@ destructor TBox.Destroy; class function TLogMessage.From(const AText : String; ASeverity : TLogSeverity; ATimeStamp : TDateTime) : TLogMessage; begin - Result := From(AText, lsError); + // Explicit metadata belongs to the caller; do not replace it with defaults. + Result.FTimeStamp := ATimeStamp; + Result.FSeverity := ASeverity; + Result.FText := AText; end; class function TLogMessage.From(const AText : String; ASeverity : TLogSeverity = lsError) : TLogMessage; @@ -1369,14 +1507,12 @@ function TDateTimeHelper.CompareTo(const aDateTime: TDateTime): TValueRelationsh Result := CompareDateTime(Self, aDateTime); end; -class function TDateTimeHelper.Create(const aYear, aMonth, - aDay: Word): TDateTime; +class function TDateTimeHelper.Create(const aYear, aMonth, aDay: Word): TDateTime; begin Result := EncodeDate(aYear, aMonth, aDay); end; -class function TDateTimeHelper.Create(const aYear, aMonth, aDay, aHour, aMinute, - aSecond, aMillisecond: Word): TDateTime; +class function TDateTimeHelper.Create(const aYear, aMonth, aDay, aHour, aMinute, aSecond, aMillisecond: Word): TDateTime; begin Result := EncodeDateTime(aYear, aMonth, aDay, aHour, aMinute, aSecond, aMillisecond); end; @@ -1591,50 +1727,42 @@ function TDateTimeHelper.WeeksBetween(const aDateTime: TDateTime): Integer; Result := DateUtils.WeeksBetween(Self, aDateTime); end; -function TDateTimeHelper.WithinDays(const aDateTime: TDateTime; - const aDays: Integer): Boolean; +function TDateTimeHelper.WithinDays(const aDateTime: TDateTime; const aDays: Integer): Boolean; begin Result := DateUtils.WithinPastDays(Self, aDateTime, aDays); end; -function TDateTimeHelper.WithinHours(const aDateTime: TDateTime; - const aHours: Int64): Boolean; +function TDateTimeHelper.WithinHours(const aDateTime: TDateTime; const aHours: Int64): Boolean; begin Result := DateUtils.WithinPastHours(Self, aDateTime, aHours); end; -function TDateTimeHelper.WithinMilliseconds(const aDateTime: TDateTime; - const AMilliseconds: Int64): Boolean; +function TDateTimeHelper.WithinMilliseconds(const aDateTime: TDateTime; const AMilliseconds: Int64): Boolean; begin Result := DateUtils.WithinPastMilliSeconds(Self, aDateTime, AMilliseconds); end; -function TDateTimeHelper.WithinMinutes(const aDateTime: TDateTime; - const aMinutes: Int64): Boolean; +function TDateTimeHelper.WithinMinutes(const aDateTime: TDateTime; const aMinutes: Int64): Boolean; begin Result := DateUtils.WithinPastMinutes(Self, aDateTime, aMinutes); end; -function TDateTimeHelper.WithinMonths(const aDateTime: TDateTime; - const aMonths: Integer): Boolean; +function TDateTimeHelper.WithinMonths(const aDateTime: TDateTime; const aMonths: Integer): Boolean; begin Result := DateUtils.WithinPastMonths(Self, aDateTime, aMonths); end; -function TDateTimeHelper.WithinSeconds(const aDateTime: TDateTime; - const aSeconds: Int64): Boolean; +function TDateTimeHelper.WithinSeconds(const aDateTime: TDateTime; const aSeconds: Int64): Boolean; begin Result := DateUtils.WithinPastSeconds(Self, aDateTime, aSeconds); end; -function TDateTimeHelper.WithinWeeks(const aDateTime: TDateTime; - const aWeeks: Integer): Boolean; +function TDateTimeHelper.WithinWeeks(const aDateTime: TDateTime; const aWeeks: Integer): Boolean; begin Result := DateUtils.WithinPastWeeks(Self, aDateTime, aWeeks); end; -function TDateTimeHelper.WithinYears(const aDateTime: TDateTime; - const aYears: Integer): Boolean; +function TDateTimeHelper.WithinYears(const aDateTime: TDateTime; const aYears: Integer): Boolean; begin Result := DateUtils.WithinPastYears(Self, aDateTime, aYears); end; @@ -1683,7 +1811,33 @@ constructor TThreadNotify.Create(ATargetThread : TThread); end; destructor TThreadNotify.Destroy; -begin +var + LHandler : TNotifyEvent; + LMethod : TMethod; + LPending : TPendingNotifyManyEvent; + i : Integer; +begin + // Producers must be joined and disposal must run on the target thread. + if (not Assigned(GMainThreadNotify)) or (Self=GMainThreadNotify) then + TThread.RemoveQueuedEvents(nil, InvokePendingOnTargetThread) + else begin + // FPC's RTL compares only method code. Remove this receiver's adapter from + // the existing dispatcher using both code and data, preserving other work. + LHandler := InvokeQueued; + LMethod := TMethod(LHandler); + GMainThreadNotify.FLock.Acquire; + try + for i := GMainThreadNotify.FPendingNotifications.Count-1 downto 0 do begin + LPending := GMainThreadNotify.FPendingNotifications[i]; + if Length(LPending.Handlers)=1 then + if (TMethod(LPending.Handlers[0]).Code=LMethod.Code) and + (TMethod(LPending.Handlers[0]).Data=LMethod.Data) then + GMainThreadNotify.FPendingNotifications.Delete(i); + end; + finally + GMainThreadNotify.FLock.Release; + end; + end; FTargetThread := nil; FLock.Acquire; try @@ -1694,8 +1848,14 @@ destructor TThreadNotify.Destroy; end; end; +procedure TThreadNotify.InvokeQueued(Sender : TObject); +begin + InvokePendingOnTargetThread; +end; + procedure TThreadNotify.InvokePendingOnTargetThread; var + i, LCount : Integer; LPendings : TArray; LPending : TPendingNotifyManyEvent; LNotify : TNotifyEvent; @@ -1704,17 +1864,43 @@ procedure TThreadNotify.InvokePendingOnTargetThread; exit; if TThread.CurrentThread.ThreadID = FTargetThread.ThreadID then begin - FLock.Acquire; - try - LPendings := FPendingNotifications.ToArray; - FPendingNotifications.Clear; - finally - FLock.Release; + if Self=GMainThreadNotify then begin + FLock.Acquire; + try + LCount := FPendingNotifications.Count; + finally + FLock.Release; + end; + // Take one record at a time: an earlier callback may dispose a receiver + // whose adapter is still pending. New producer work gets its own turn. + for i := 1 to LCount do begin + FLock.Acquire; + try + if FPendingNotifications.Count=0 then Break; + LPending := FPendingNotifications[0]; + FPendingNotifications.Delete(0); + finally + FLock.Release; + end; + for LNotify in LPending.Handlers do + LNotify(LPending.Sender); + end; + end else begin + FLock.Acquire; + try + LPendings := FPendingNotifications.ToArray; + FPendingNotifications.Clear; + finally + FLock.Release; + end; + for LPending in LPendings do + for LNotify in LPending.Handlers do + LNotify(LPending.Sender); end; - for LPending in LPendings do - for LNotify in LPending.Handlers do - LNotify(LPending.Sender); - end else TThread.Queue(FTargetThread, InvokePendingOnTargetThread); + end else if Self=GMainThreadNotify then + TThread.Queue(FTargetThread, InvokePendingOnTargetThread) + else + GMainThreadNotify.Invoke(Self, InvokeQueued); end; procedure TThreadNotify.Invoke(Sender: TObject; Handler : TNotifyEvent); @@ -1943,7 +2129,8 @@ class function TArrayTool.Create(const item0 : T; const item1 : T; const item result[3] := item3; end; -class function TArrayTool.Create(const item0 : T; const item1 : T; const item2 : T; const item3 : T; const item4 : T) : TArray;begin +class function TArrayTool.Create(const item0 : T; const item1 : T; const item2 : T; const item3 : T; const item4 : T) : TArray; +begin SetLength(result,5); result[0] := item0; result[1] := item1; @@ -1998,7 +2185,9 @@ class function TVariantTool.IsNumeric(const AValue : Variant) : boolean; case VarType(AValue) of varsmallint, varinteger, varsingle, vardouble, varcurrency, varboolean, {$IFDEF FPC}vardecimal,{$ENDIF} - varshortint, varbyte, varword, varlongword, varint64 {$IFDEF FPC},varqword {$ENDIF} : Result := true; + // The unsigned 64-bit Variant tag has a different name in each compiler. + varshortint, varbyte, varword, varlongword, varint64, + {$IFDEF FPC}varqword{$ELSE}varUInt64{$ENDIF} : Result := true; else Result := false; end; end; @@ -2008,7 +2197,7 @@ class function TVariantTool.CompareVariant(const ALeft, ARight : Variant) : Inte {$IFDEF FPC} Result := TCompare.Variant(@ALeft, @ARight); {$ELSE} - Result := Integer(VarCompareValue(ALeft, ARight)); + Result := TComparer.Default.Compare(ALeft, ARight); {$ENDIF} end; @@ -2144,12 +2333,26 @@ class function TVariantTool.NumericBetweenExclusive(const AValue, Lower, Upper : Result := (lowercmp = 1) AND (uppercmp = -1); end; +// Encode only at the file boundary. Lazarus String values contain UTF-8; +// Delphi String values are UTF-16. GetBytes emits neither a BOM nor a prefix. +procedure WriteUTF8Text(AStream: TStream; const AText: String); +var LBytes: TBytes; +begin + {$IFDEF FPC} + LBytes := TEncoding.UTF8.GetBytes(UTF8Decode(AText)); + {$ELSE} + LBytes := TEncoding.UTF8.GetBytes(AText); + {$ENDIF} + if Length(LBytes) > 0 then + AStream.WriteBuffer(LBytes[0], Length(LBytes)); +end; + { TStreamHelper } {$IFNDEF FPC} procedure TStreamHelper.WriteString(const AString : String); begin - Self.WriteBuffer(Pointer(AString)^, Length(AString)); + WriteUTF8Text(Self, AString); end; {$ENDIF} @@ -2187,7 +2390,7 @@ class procedure TFileTool.AppendText(const AFileName: string; const AText: strin fstream.Seek(0, soFromEnd); end; try - fstream.{$IFDEF FPC}WriteAnsiString{$ELSE}WriteString{$ENDIF}(AText+#13#10); + WriteUTF8Text(fstream, AText + #13#10); finally fstream.Free; end; @@ -2273,11 +2476,12 @@ function TStatistics.PopulationStandardDeviation : Double; end; function TStatistics.PopulationVariance : Double; -var LSum : Double; +var LSum, LCount : Double; begin LSum := Sum; - if SampleCount > 2 then - Result := ((SampleCount * SquaredSum) - (LSum * LSum)) / (SampleCount * SampleCount) + LCount := SampleCount; + if SampleCount > 0 then + Result := ((LCount * SquaredSum) - (LSum * LSum)) / (LCount * LCount) else Result := Nan; end; @@ -2331,11 +2535,14 @@ function TStatistics.SampleStandardDeviation : Double; end; function TStatistics.SampleVariance : Double; -var LSum : Double; +var LSum, LCount : Double; begin LSum := Sum; - if SampleCount > 2 then - Result := ((SampleCount * SquaredSum) - (LSum * LSum)) / ((SampleCount - 1) * (SampleCount - 1)) + LCount := SampleCount; + // Bessel's correction divides the population numerator by n*(n-1). + // Keep the count real so large populations do not multiply integer counts. + if SampleCount >= 2 then + Result := ((LCount * SquaredSum) - (LSum * LSum)) / (LCount * (LCount - 1)) else Result := Nan; end; diff --git a/src/libraries/sphere10/UConcurrency.pas b/src/libraries/sphere10/UConcurrency.pas new file mode 100644 index 000000000..bb1d5c0d3 --- /dev/null +++ b/src/libraries/sphere10/UConcurrency.pas @@ -0,0 +1,546 @@ +unit UConcurrency; + +{ Copyright © 2026 Herman Schoenfeld + Distributed under the MIT software license, see the accompanying LICENSE. + + Protects shared memory with multiple concurrent readers or one exclusive + writer. One upgradeable reader can reserve a later write; timed acquisition + and optional recursion control contention. Waiters sleep on RTL events. + Ownership semantics follow System.Threading.ReaderWriterLockSlim. +} + +{ Public types in this unit + + Type | Purpose + ------------------------------+------------------------------------------------------------------------- + ELockDisposedException | Reports access to a disposed lock. + ELockRecursionException | Reports prohibited recursive acquisition or promotion. + ESynchronizationLockException | Reports release without the required thread ownership. + TLockRecursionPolicy | Allows or rejects recursive lock acquisition. + TReadWriteLock | Grants concurrent reads or one write, with upgrades and timeouts. +} + +{$IFDEF FPC}{$MODE DELPHI}{$H+}{$ENDIF} + +interface + +uses Classes, SysUtils, SyncObjs, Generics.Collections, UCommon; + +type + TLockRecursionPolicy = (lrpNoRecursion, lrpSupportsRecursion); + ELockRecursionException = class(Exception); + ESynchronizationLockException = class(Exception); + ELockDisposedException = class(Exception); + + { All acquisitions and releases are thread-affine. Ordinary read ownership + cannot be promoted: acquire upgradeable read before a possible write. + Stop/join consumers before Free. Dispose rejects outstanding ownership or + waiters; it never releases another thread's lock or terminates a thread. } + TReadWriteLock = class + private type + TLockKind = (lkRead, lkWrite, lkUpgradeable); + TThreadCounts = record + Reads, Writes, Upgrades: Integer; + end; + TLockWaiter = class + Kind: TLockKind; + Signal: TEvent; + constructor Create(AKind: TLockKind); + destructor Destroy; override; + end; + private + // This gate protects ownership/admission bookkeeping only. The caller's + // resource stays protected by the granted ownership after the gate is left. + FGate: TCriticalSection; + FThreads: TDictionary; + FWaiters: TList; + FPolicy: TLockRecursionPolicy; + // Reserving upgradeable ownership for one thread prevents competing readers + // from each waiting to promote while preventing the other from completing. + FWriter, FUpgrader: TThreadID; + // Readers counts distinct explicit read owners, not recursive acquisitions. + // Upgradeable-only access is tracked separately by FUpgrader. + FReaders, FWaitingReaders, FWaitingWriters, FWaitingUpgraders: Integer; + FDisposed: Boolean; + function ThreadCounts(AThreadID: TThreadID): TThreadCounts; + procedure CheckAcquisition(AKind: TLockKind; const ACounts: TThreadCounts); + function CanAcquire(AKind: TLockKind; AThreadID: TThreadID; const ACounts: TThreadCounts): Boolean; + procedure Grant(AKind: TLockKind; AThreadID: TThreadID; var ACounts: TThreadCounts); + function TryAcquire(AKind: TLockKind; AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseLock(AKind: TLockKind); + procedure ChangeWaitCount(AKind: TLockKind; ADelta: Integer); + procedure WakeWaiters; + function GetHeld(AKind: TLockKind): Boolean; + function GetRecursiveCount(AKind: TLockKind): Integer; + function GetCount(AIndex: Integer): Integer; + function GetIsReadLockHeld: Boolean; + function GetIsWriteLockHeld: Boolean; + function GetIsUpgradeableReadLockHeld: Boolean; + function GetRecursiveReadCount: Integer; + function GetRecursiveWriteCount: Integer; + function GetRecursiveUpgradeCount: Integer; + function GetCurrentReadCount: Integer; + function GetWaitingReadCount: Integer; + function GetWaitingWriteCount: Integer; + function GetWaitingUpgradeCount: Integer; + public + constructor Create(ARecursionPolicy: TLockRecursionPolicy = lrpNoRecursion); + destructor Destroy; override; + procedure Dispose; + // Keep the returned managed record in a local variable. Each scope owns + // one acquisition; copies retain it until the last copy is finalized. + // The lock must outlive every scope, all on the acquiring thread. + function EnterReadScope: TScope; + function EnterWriteScope: TScope; + function EnterUpgradeableReadScope: TScope; + // Pair every successful Acquire/TryAcquire with the matching Release on + // the same thread. Promotion adds a write acquisition to upgradeable read. + procedure AcquireReadLock; + function TryAcquireReadLock(AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseReadLock; + procedure AcquireWriteLock; + function TryAcquireWriteLock(AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseWriteLock; + procedure AcquireUpgradeableReadLock; + function TryAcquireUpgradeableReadLock(AMillisecondsTimeout: Integer): Boolean; + procedure ReleaseUpgradeableReadLock; + property RecursionPolicy: TLockRecursionPolicy read FPolicy; + property IsReadLockHeld: Boolean read GetIsReadLockHeld; + property IsWriteLockHeld: Boolean read GetIsWriteLockHeld; + property IsUpgradeableReadLockHeld: Boolean read GetIsUpgradeableReadLockHeld; + property RecursiveReadCount: Integer read GetRecursiveReadCount; + property RecursiveWriteCount: Integer read GetRecursiveWriteCount; + property RecursiveUpgradeCount: Integer read GetRecursiveUpgradeCount; + // Counts distinct explicit read owners, excluding upgradeable-only access. + property CurrentReadCount: Integer read GetCurrentReadCount; + property WaitingReadCount: Integer read GetWaitingReadCount; + property WaitingWriteCount: Integer read GetWaitingWriteCount; + property WaitingUpgradeCount: Integer read GetWaitingUpgradeCount; + end; + +implementation + +constructor TReadWriteLock.Create(ARecursionPolicy: TLockRecursionPolicy); +begin + inherited Create; + FPolicy := ARecursionPolicy; + FGate := TCriticalSection.Create; + FThreads := TDictionary.Create; + FWaiters := TList.Create; +end; + +destructor TReadWriteLock.Destroy; +begin + if Assigned(FGate) then Dispose; + FWaiters.Free; + FThreads.Free; + FGate.Free; + inherited; +end; + +procedure TReadWriteLock.Dispose; +begin + FGate.Acquire; + try + if FDisposed then Exit; + // Consumers must be stopped/joined by their owner before disposal. Reject + // active use without changing it; never unlock another thread or join it here. + if (Assigned(FThreads) and (FThreads.Count <> 0)) or + (Assigned(FWaiters) and (FWaiters.Count <> 0)) then + raise ESynchronizationLockException.Create('Cannot dispose a lock with owners or waiters.'); + FDisposed := True; + finally + FGate.Release; + end; +end; + +function TReadWriteLock.EnterReadScope: TScope; +var Scope: TScope; +begin + AcquireReadLock; + try + Scope := TScope.Create(ReleaseReadLock); + except + // Scope creation did not adopt this acquisition. Unwind it exactly once. + ReleaseReadLock; + raise; + end; + // Assignment to the caller can itself release a previous scope. Keep it + // outside the construction handler: Scope now owns this acquisition. + Result := Scope; +end; + +function TReadWriteLock.EnterWriteScope: TScope; +var Scope: TScope; +begin + AcquireWriteLock; + try + Scope := TScope.Create(ReleaseWriteLock); + except + ReleaseWriteLock; + raise; + end; + Result := Scope; +end; + +function TReadWriteLock.EnterUpgradeableReadScope: TScope; +var Scope: TScope; +begin + AcquireUpgradeableReadLock; + try + Scope := TScope.Create(ReleaseUpgradeableReadLock); + except + ReleaseUpgradeableReadLock; + raise; + end; + Result := Scope; +end; + +procedure TReadWriteLock.AcquireReadLock; +begin + TryAcquireReadLock(-1); +end; + +function TReadWriteLock.TryAcquireReadLock(AMillisecondsTimeout: Integer): Boolean; +begin + Result := TryAcquire(lkRead, AMillisecondsTimeout); +end; + +procedure TReadWriteLock.ReleaseReadLock; +begin + ReleaseLock(lkRead); +end; + +procedure TReadWriteLock.AcquireWriteLock; +begin + TryAcquireWriteLock(-1); +end; + +function TReadWriteLock.TryAcquireWriteLock(AMillisecondsTimeout: Integer): Boolean; +begin + Result := TryAcquire(lkWrite, AMillisecondsTimeout); +end; + +procedure TReadWriteLock.ReleaseWriteLock; +begin + ReleaseLock(lkWrite); +end; + +procedure TReadWriteLock.AcquireUpgradeableReadLock; +begin + TryAcquireUpgradeableReadLock(-1); +end; + +function TReadWriteLock.TryAcquireUpgradeableReadLock(AMillisecondsTimeout: Integer): Boolean; +begin + Result := TryAcquire(lkUpgradeable, AMillisecondsTimeout); +end; + +procedure TReadWriteLock.ReleaseUpgradeableReadLock; +begin + ReleaseLock(lkUpgradeable); +end; + +function MonotonicMilliseconds: UInt64; +begin + {$IFDEF FPC}Result := GetTickCount64;{$ELSE}Result := TThread.GetTickCount64;{$ENDIF} +end; + +constructor TReadWriteLock.TLockWaiter.Create(AKind: TLockKind); +begin + inherited Create; + Kind := AKind; + Signal := TEvent.Create(nil, False, False, ''); +end; + +destructor TReadWriteLock.TLockWaiter.Destroy; +begin + Signal.Free; + inherited; +end; + +function TReadWriteLock.ThreadCounts(AThreadID: TThreadID): TThreadCounts; +begin + if not FThreads.TryGetValue(AThreadID, Result) then + Result := Default(TThreadCounts); +end; + +procedure TReadWriteLock.CheckAcquisition(AKind: TLockKind; const ACounts: TThreadCounts); +var LInvalid: Boolean; +begin + if FDisposed then raise ELockDisposedException.Create('The lock is disposed.'); + // An ordinary reader cannot promote, even with recursion enabled: two such + // readers would each wait for the other. Choose upgradeable mode in advance. + if FPolicy = lrpNoRecursion then + case AKind of + lkRead: LInvalid := (ACounts.Reads > 0) or (ACounts.Writes > 0); + lkWrite: LInvalid := (ACounts.Writes > 0) or (ACounts.Reads > 0); + lkUpgradeable: LInvalid := (ACounts.Reads > 0) or (ACounts.Writes > 0) or + (ACounts.Upgrades > 0); + end + else + LInvalid := (AKind <> lkRead) and (ACounts.Reads > 0) and + (ACounts.Writes = 0) and (ACounts.Upgrades = 0); + if LInvalid then + raise ELockRecursionException.Create('This acquisition violates the lock recursion policy.'); +end; + +function TReadWriteLock.CanAcquire(AKind: TLockKind; AThreadID: TThreadID; const ACounts: TThreadCounts): Boolean; +begin + // RTL thread IDs may be integers or pointers; use their typed empty value. + // Existing owners can recurse without queuing behind a writer that needs + // them to finish. New readers/upgraders wait once any writer has queued. + case AKind of + lkRead: + Result := (ACounts.Reads > 0) or (ACounts.Writes > 0) or + (ACounts.Upgrades > 0) or ((FWriter = Default(TThreadID)) and (FWaitingWriters = 0)); + lkWrite: + // Only the reserved upgrader may discount its own explicit read ownership; + // all other readers must leave before that thread can acquire write mode. + Result := (ACounts.Writes > 0) or ((FWriter = Default(TThreadID)) and + ((FUpgrader = Default(TThreadID)) or (FUpgrader = AThreadID)) and + (FReaders = Ord(ACounts.Reads > 0))); + lkUpgradeable: + Result := (ACounts.Upgrades > 0) or (ACounts.Writes > 0) or + ((FUpgrader = Default(TThreadID)) and (FWriter = Default(TThreadID)) and (FWaitingWriters = 0)); + else + Result := False; + end; +end; + +procedure TReadWriteLock.Grant(AKind: TLockKind; AThreadID: TThreadID; var ACounts: TThreadCounts); +var LFirst: Boolean; +begin + LFirst := False; + case AKind of + lkRead: begin + LFirst := ACounts.Reads = 0; + Inc(ACounts.Reads); + end; + lkWrite: Inc(ACounts.Writes); + lkUpgradeable: Inc(ACounts.Upgrades); + end; + // Publish counters only after the dictionary allocation has succeeded. + FThreads.AddOrSetValue(AThreadID, ACounts); + case AKind of + lkRead: if LFirst then Inc(FReaders); + lkWrite: FWriter := AThreadID; + lkUpgradeable: FUpgrader := AThreadID; + end; +end; + +procedure TReadWriteLock.ChangeWaitCount(AKind: TLockKind; ADelta: Integer); +begin + case AKind of + lkRead: Inc(FWaitingReaders, ADelta); + lkWrite: Inc(FWaitingWriters, ADelta); + lkUpgradeable: Inc(FWaitingUpgraders, ADelta); + end; +end; + +procedure TReadWriteLock.WakeWaiters; +var LWaiter: TLockWaiter; +begin + // Per-waiter auto-reset events avoid lost wakes and repeated wakes without a + // state change. Admission is rechecked under the gate, with writer priority. + for LWaiter in FWaiters do LWaiter.Signal.SetEvent; +end; + +function TReadWriteLock.TryAcquire(AKind: TLockKind; AMillisecondsTimeout: Integer): Boolean; +var + LThreadID: TThreadID; + LCounts: TThreadCounts; + LWaiter: TLockWaiter; + LStarted, LElapsed: UInt64; + LRemaining: Cardinal; + LWaitResult: TWaitResult; +begin + if AMillisecondsTimeout < -1 then + raise EArgumentOutOfRangeException.Create('Timeout must be -1 or a nonnegative number of milliseconds.'); + // Keep the original deadline across state-change wakes; contention must not + // restart a caller's finite timeout. This clock is independent of wall time. + LStarted := MonotonicMilliseconds; + LThreadID := TThread.CurrentThread.ThreadID; + LWaiter := nil; + FGate.Acquire; + try + LCounts := ThreadCounts(LThreadID); + CheckAcquisition(AKind, LCounts); + while True do begin + if CanAcquire(AKind, LThreadID, LCounts) then begin + Grant(AKind, LThreadID, LCounts); + Exit(True); + end; + if AMillisecondsTimeout = 0 then Exit(False); + if AMillisecondsTimeout = -1 then LRemaining := High(Cardinal) + else begin + LElapsed := MonotonicMilliseconds - LStarted; + if LElapsed >= UInt64(AMillisecondsTimeout) then Exit(False); + LRemaining := Cardinal(UInt64(AMillisecondsTimeout) - LElapsed); + end; + if not Assigned(LWaiter) then begin + LWaiter := TLockWaiter.Create(AKind); + try + FWaiters.Add(LWaiter); + except + FreeAndNil(LWaiter); + raise; + end; + ChangeWaitCount(AKind, 1); + end; + // No protected resource or admission lock is held during an event wait. + FGate.Release; + try + LWaitResult := LWaiter.Signal.WaitFor(LRemaining); + finally + FGate.Acquire; + end; + if LWaitResult = wrTimeout then Exit(False); + if LWaitResult <> wrSignaled then + raise ESynchronizationLockException.Create('Waiting for lock ownership failed.'); + LCounts := ThreadCounts(LThreadID); + end; + finally + if Assigned(LWaiter) then begin + // Remove registration before freeing its event, while the gate prevents + // a releaser from signalling that event. Failure never grants ownership. + FWaiters.Remove(LWaiter); + ChangeWaitCount(AKind, -1); + // A timed-out writer must not keep new readers asleep indefinitely. + WakeWaiters; + end; + FGate.Release; + LWaiter.Free; + end; +end; + +procedure TReadWriteLock.ReleaseLock(AKind: TLockKind); +var LThreadID: TThreadID; LCounts: TThreadCounts; LCount: Integer; +begin + // Every typed Release balances ONE same-thread acquisition of that mode. In + // particular, ReleaseWriteLock after promotion retains the surrounding upgrader. + LThreadID := TThread.CurrentThread.ThreadID; + FGate.Acquire; + try + LCounts := ThreadCounts(LThreadID); + case AKind of + lkRead: LCount := LCounts.Reads; + lkWrite: LCount := LCounts.Writes; + lkUpgradeable: LCount := LCounts.Upgrades; + else LCount := 0; + end; + if LCount = 0 then + raise ESynchronizationLockException.Create('This thread does not hold the requested lock.'); + case AKind of + lkRead: begin + Dec(LCounts.Reads); + if LCounts.Reads = 0 then Dec(FReaders); + end; + lkWrite: begin + Dec(LCounts.Writes); + if LCounts.Writes = 0 then FWriter := Default(TThreadID); + end; + lkUpgradeable: begin + Dec(LCounts.Upgrades); + if LCounts.Upgrades = 0 then FUpgrader := Default(TThreadID); + end; + end; + if (LCounts.Reads = 0) and (LCounts.Writes = 0) and (LCounts.Upgrades = 0) then + FThreads.Remove(LThreadID) + else + FThreads.AddOrSetValue(LThreadID, LCounts); + WakeWaiters; + finally + FGate.Release; + end; +end; + +function TReadWriteLock.GetRecursiveCount(AKind: TLockKind): Integer; +var LCounts: TThreadCounts; +begin + FGate.Acquire; + try + LCounts := ThreadCounts(TThread.CurrentThread.ThreadID); + case AKind of + lkRead: Result := LCounts.Reads; + lkWrite: Result := LCounts.Writes; + lkUpgradeable: Result := LCounts.Upgrades; + else Result := 0; + end; + finally + FGate.Release; + end; +end; + +function TReadWriteLock.GetHeld(AKind: TLockKind): Boolean; +begin + Result := GetRecursiveCount(AKind) > 0; +end; + +function TReadWriteLock.GetCount(AIndex: Integer): Integer; +begin + FGate.Acquire; + try + case AIndex of + 0: Result := FReaders; + 1: Result := FWaitingReaders; + 2: Result := FWaitingWriters; + 3: Result := FWaitingUpgraders; + else Result := 0; + end; + finally + FGate.Release; + end; +end; + +function TReadWriteLock.GetIsReadLockHeld: Boolean; +begin + Result := GetHeld(lkRead); +end; + +function TReadWriteLock.GetIsWriteLockHeld: Boolean; +begin + Result := GetHeld(lkWrite); +end; + +function TReadWriteLock.GetIsUpgradeableReadLockHeld: Boolean; +begin + Result := GetHeld(lkUpgradeable); +end; + +function TReadWriteLock.GetRecursiveReadCount: Integer; +begin + Result := GetRecursiveCount(lkRead); +end; + +function TReadWriteLock.GetRecursiveWriteCount: Integer; +begin + Result := GetRecursiveCount(lkWrite); +end; + +function TReadWriteLock.GetRecursiveUpgradeCount: Integer; +begin + Result := GetRecursiveCount(lkUpgradeable); +end; + +function TReadWriteLock.GetCurrentReadCount: Integer; +begin + Result := GetCount(0); +end; + +function TReadWriteLock.GetWaitingReadCount: Integer; +begin + Result := GetCount(1); +end; + +function TReadWriteLock.GetWaitingWriteCount: Integer; +begin + Result := GetCount(2); +end; + +function TReadWriteLock.GetWaitingUpgradeCount: Integer; +begin + Result := GetCount(3); +end; + +end. diff --git a/src/libraries/sphere10/UMemory.pas b/src/libraries/sphere10/UMemory.pas index 4aba1e80c..384d361c2 100644 --- a/src/libraries/sphere10/UMemory.pas +++ b/src/libraries/sphere10/UMemory.pas @@ -1,6 +1,6 @@ unit UMemory; -{ Copyright (c) 2017 - 2018 Sphere 10 Software +{ Copyright (c) 2017 - 2018 Herman Schoenfeld Memory management routines. @@ -14,6 +14,14 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Public types in this unit + + Type | Purpose + ---------------+------------------------------------------------------------------------- + TDisposables | Releases registered objects and memory in reverse order at scope exit. + TDisposePolicy | Selects how an owned object, pointer or memory block is released. +} + {$IFDEF FPC} {$MODE DELPHI}{$H+} @@ -48,13 +56,14 @@ TGuard = class(TInterfacedObject) destructor Destroy; override; end; private - FGuardian: IInterface; + {$IFDEF FPC}FGuardian: IInterface;{$ENDIF} FPointers: array of TDisposablePointer; FLastIndex: Integer; + // Delphi finalizes managed fields in reverse declaration order. + {$IFNDEF FPC}FGuardian: IInterface;{$ENDIF} {$IFNDEF DELPHI_SYDNEY_PLUS} class procedure Initialize(var ADisposables: TDisposables); static; - class procedure Finalize(var ADisposables: TDisposables); static; - {$ENDIF} + class procedure Finalize(var ADisposables: TDisposables); static; {$ENDIF} procedure RegisterPointer(Ptr: Pointer; ADisposePolicy: TDisposePolicy); procedure UnregisterPointer(Ptr: Pointer); public @@ -111,7 +120,7 @@ destructor TDisposables.TGuard.Destroy; class operator TDisposables.Finalize (var Dest: TDisposables); var FirstException: Pointer; - i, x: Integer; + i: Integer; begin FirstException := nil; for i := Dest.FLastIndex downto 0 do @@ -124,7 +133,7 @@ destructor TDisposables.TGuard.Destroy; Dest.FPointers[i].Ptr := nil; end; idpRelease: begin - raise ENotSupportedException.Create('Dispose policy idoRelease not supported'); + raise ENotSupportedException.Create('Dispose policy idpRelease not supported'); end; idpFreeMem: if Assigned(Dest.FPointers[i].Ptr) then System.FreeMem(Dest.FPointers[i].Ptr); else raise ENotSupportedException.Create('Dispose policy not supported'); @@ -150,7 +159,7 @@ class procedure TDisposables.Initialize(var ADisposables: TDisposables); class procedure TDisposables.Finalize(var ADisposables: TDisposables); var FirstException: Pointer; - i, x: Integer; + i: Integer; begin FirstException := nil; for i := ADisposables.FLastIndex downto 0 do @@ -163,7 +172,7 @@ class procedure TDisposables.Finalize(var ADisposables: TDisposables); ADisposables.FPointers[i].Ptr := nil; end; idpRelease: begin - raise ENotSupportedException.Create('Dispose policy idoRelease not supported'); + raise ENotSupportedException.Create('Dispose policy idpRelease not supported'); end; idpFreeMem: if Assigned(ADisposables.FPointers[i].Ptr) then System.FreeMem(ADisposables.FPointers[i].Ptr); else raise ENotSupportedException.Create('Dispose policy not supported'); @@ -183,7 +192,7 @@ class procedure TDisposables.Finalize(var ADisposables: TDisposables); procedure TDisposables.RegisterPointer(Ptr: Pointer; ADisposePolicy: TDisposePolicy); begin - if FLastIndex > High(FPointers) then + if FLastIndex >= High(FPointers) then SetLength(FPointers, Length(FPointers) * 2); Inc(FLastIndex); @@ -204,16 +213,20 @@ procedure TDisposables.UnregisterPointer(Ptr: Pointer); function TDisposables.AddObject(const AnObject: TObject): TObject; begin + {$IFNDEF DELPHI_SYDNEY_PLUS} if not Assigned(FGuardian) then FGuardian := TGuard.Create(@Self); + {$ENDIF} RegisterPointer(Pointer(AnObject), idpFree); Result := AnObject; end; procedure TDisposables.RemoveObject(const AnObject: TObject); begin + {$IFNDEF DELPHI_SYDNEY_PLUS} if not Assigned(FGuardian) then FGuardian := TGuard.Create(@Self); + {$ENDIF} UnregisterPointer(Pointer(AnObject)); end; @@ -225,16 +238,20 @@ procedure TDisposables.GetMem(out P: Pointer; Size: Integer); procedure TDisposables.FreeMem(var P: Pointer); begin + {$IFNDEF DELPHI_SYDNEY_PLUS} if not Assigned(FGuardian) then FGuardian := TGuard.Create(@Self); + {$ENDIF} UnregisterPointer(P); System.FreeMem(P); end; procedure TDisposables.AddMem(const P: Pointer); begin + {$IFNDEF DELPHI_SYDNEY_PLUS} if not Assigned(FGuardian) then FGuardian := TGuard.Create(@Self); + {$ENDIF} RegisterPointer(P, idpFreeMem); end; @@ -242,8 +259,10 @@ procedure TDisposables.ReallocMem(var P: Pointer; Size:Integer); var i: Integer; begin + {$IFNDEF DELPHI_SYDNEY_PLUS} if not Assigned(FGuardian) then FGuardian := TGuard.Create(@Self); + {$ENDIF} for i := FLastIndex downto 0 do if (FPointers[i].DisposePolicy = idpFreeMem) and (FPointers[i].Ptr = P) then @@ -256,8 +275,10 @@ procedure TDisposables.ReallocMem(var P: Pointer; Size:Integer); procedure TDisposables.RemoveMem(const P: Pointer); begin + {$IFNDEF DELPHI_SYDNEY_PLUS} if not Assigned(FGuardian) then FGuardian := TGuard.Create(@Self); + {$ENDIF} UnregisterPointer(P); end; diff --git a/src/libraries/sphere10/UStreams.pas b/src/libraries/sphere10/UStreams.pas new file mode 100644 index 000000000..97439459e --- /dev/null +++ b/src/libraries/sphere10/UStreams.pas @@ -0,0 +1,422 @@ +unit UStreams; + +{ Copyright © 2026 Herman Schoenfeld . + + Stream decorators with bounded read-ahead and explicit ownership. + Distributed under the MIT license; see LICENSE. + + Public types in this unit + + Type | Purpose + ----------------+------------------------------------------------------------ + TBufferedStream | Buffers reads, with optional length caching and ownership. +} + +{$IFDEF FPC} + {$MODE DELPHI}{$H+} +{$ENDIF} + +interface + +uses Classes, SysUtils; + +type + { Reads ahead into one fixed-size buffer. Writes are forwarded immediately. + Access the input only through this decorator while it is in use. The input + determines whether seeking, sizing and writing are supported; sequential + reads do not require those capabilities. This class adds no synchronization. + + Read-ahead can put WrappedStream.Position beyond this stream's Position. + FlushUnreadData explicitly restores that cursor when seeking is supported. + Destruction never seeks: an owned input is freed, while a borrowed input is + left at its physical cursor. Ownership transfers only after Create succeeds. + + EnableReadBuffering and EnableStreamLengthCaching both default to True. + Length caching retains one successful Size query; disable it for inputs that + can grow independently. Writes/resizes invalidate the retained length before + attempting mutation. Construction never queries the input's size or position. + + Disabling read buffering forwards reads/writes directly and releases + the read-ahead memory. BufferSize retains the configured capacity for the + next enable. Unread bytes must first be consumed or returned to the input by + FlushUnreadData; disabling never attempts an implicit rewind. Length caching + works independently of read buffering, including when read buffering is off. } + TBufferedStream = class(TStream) + private + FStream: TStream; + FBufferedData: TBytes; + FBufferSize: Integer; + FBufferOffset: Integer; + FBufferCount: Integer; + FPosition: Int64; + FPositionKnown: Boolean; + FOwnsStream: Boolean; + FEnableReadBuffering: Boolean; + FEnableStreamLengthCaching: Boolean; + FCachedStreamLength: Int64; + FSizeKnown: Boolean; + function GetBufferSize: Integer; + procedure SetEnableReadBuffering(AValue: Boolean); + procedure SetEnableStreamLengthCaching(AValue: Boolean); + procedure DiscardReadBuffer; + procedure InvalidateAfterFailure; + function AddOffset(APosition, AOffset: Int64): Int64; + function TrySeekInBuffer(APosition: Int64): Boolean; + protected + {$IFNDEF FPC}{$IF DECLARED(TStreamCapabilities)} + function GetCapabilities: TStreamCapabilities; override; + {$ENDIF}{$ENDIF} + function GetSize: Int64; override; + procedure SetSize(NewSize: Longint); override; + procedure SetSize(const NewSize: Int64); override; + public + constructor Create(AStream: TStream; ABufferSize: Integer = 65536; AOwnsStream: Boolean = False; AEnableStreamLengthCaching: Boolean = True; AEnableReadBuffering: Boolean = True); + destructor Destroy; override; + function Read(var Buffer; Count: Longint): Longint; overload; override; + function Write(const Buffer; Count: Longint): Longint; overload; override; + function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; override; + // Convenience methods set both flags. DisableBuffering throws before + // changing either flag if unread bytes have not been explicitly flushed. + procedure EnableBuffering; + procedure DisableBuffering; + // Returns unread bytes to the input's cursor; it does not discard them. + // An unsupported rewind throws and keeps the pre-read buffer intact. + procedure FlushUnreadData; + // Compatibility name for FlushUnreadData. + procedure SynchronizePosition; + property WrappedStream: TStream read FStream; + property BufferSize: Integer read GetBufferSize; + property OwnsStream: Boolean read FOwnsStream; + property EnableStreamLengthCaching: Boolean read FEnableStreamLengthCaching write SetEnableStreamLengthCaching; + property EnableReadBuffering: Boolean read FEnableReadBuffering write SetEnableReadBuffering; + end; + +implementation + +constructor TBufferedStream.Create(AStream: TStream; ABufferSize: Integer; AOwnsStream: Boolean; AEnableStreamLengthCaching: Boolean; AEnableReadBuffering: Boolean); +begin + inherited Create; + if AStream = nil then + raise EArgumentNilException.Create('AStream must not be nil.'); + if ABufferSize <= 0 then + raise EArgumentOutOfRangeException.Create('ABufferSize must be positive.'); + FBufferSize := ABufferSize; + if AEnableReadBuffering then SetLength(FBufferedData, FBufferSize); + // Do not probe Position/Size here: a forward-only stream may still be read. + FStream := AStream; + FOwnsStream := AOwnsStream; + FEnableStreamLengthCaching := AEnableStreamLengthCaching; + FEnableReadBuffering := AEnableReadBuffering; +end; + +destructor TBufferedStream.Destroy; +begin + try + if FOwnsStream then + FreeAndNil(FStream); + finally + inherited Destroy; + end; +end; + +function TBufferedStream.GetBufferSize: Integer; +begin + Result := FBufferSize; +end; + +procedure TBufferedStream.EnableBuffering; +begin + // Allocation happens first; a failure leaves both settings unchanged. + SetEnableReadBuffering(True); + SetEnableStreamLengthCaching(True); +end; + +procedure TBufferedStream.DisableBuffering; +begin + // Reject unread bytes before changing either setting or releasing memory. + SetEnableReadBuffering(False); + SetEnableStreamLengthCaching(False); +end; + +procedure TBufferedStream.SetEnableReadBuffering(AValue: Boolean); +begin + if AValue = FEnableReadBuffering then Exit; + // Switching modes must never perform an implicit seek or lose unread bytes. + // The caller explicitly chooses when to attempt an unsupported rewind. + if not AValue and (FBufferCount > FBufferOffset) then + raise EStreamError.Create('Call FlushUnreadData before disabling buffering with unread bytes.'); + // Allocate before publishing the enabled state, so allocation failure leaves + // the direct stream usable. Disabling releases the entire pre-read allocation. + if AValue then + SetLength(FBufferedData, FBufferSize) + else + SetLength(FBufferedData, 0); + DiscardReadBuffer; + FPositionKnown := False; + FSizeKnown := False; + FEnableReadBuffering := AValue; +end; + +procedure TBufferedStream.SetEnableStreamLengthCaching(AValue: Boolean); +begin + if AValue = FEnableStreamLengthCaching then Exit; + // Only length metadata changes here; unread bytes and the cursor stay intact. + FSizeKnown := False; + FEnableStreamLengthCaching := AValue; +end; + +procedure TBufferedStream.DiscardReadBuffer; +begin + FBufferOffset := 0; + FBufferCount := 0; +end; + +procedure TBufferedStream.InvalidateAfterFailure; +begin + // A failing input may already have advanced, including before returning a + // negative count. Obtain its actual position only on a later query. + DiscardReadBuffer; + FPositionKnown := False; +end; + +function TBufferedStream.AddOffset(APosition, AOffset: Int64): Int64; +var LOverflow: Boolean; +begin + LOverflow := False; + if AOffset > 0 then + LOverflow := APosition > High(Int64) - AOffset + else if AOffset < 0 then + LOverflow := APosition < Low(Int64) - AOffset; + if LOverflow then + raise ERangeError.Create('Stream position exceeds the Int64 range.'); + Result := APosition + AOffset; +end; + +function TBufferedStream.TrySeekInBuffer(APosition: Int64): Boolean; +var LStart: Int64; +begin + Result := False; + if not FPositionKnown then Exit; + LStart := FPosition - FBufferOffset; + if APosition < LStart then Exit; + if APosition - LStart > FBufferCount then Exit; + FBufferOffset := Integer(APosition - LStart); + FPosition := APosition; + Result := True; +end; + +function TBufferedStream.Read(var Buffer; Count: Longint): Longint; +var LDestination: PByte; LAvailable, LCopy, LRead, LRemaining: Integer; LReadFromSource: Boolean; +begin + if Count < 0 then + raise EArgumentOutOfRangeException.Create('Count must not be negative.'); + if Count = 0 then Exit(0); + if not FEnableReadBuffering then Exit(FStream.Read(Buffer, Count)); + Result := 0; + LDestination := @Buffer; + LReadFromSource := False; + while Result < Count do begin + LAvailable := FBufferCount - FBufferOffset; + LRemaining := Count - Result; + if LAvailable > 0 then begin + LCopy := LAvailable; + if LCopy > LRemaining then LCopy := LRemaining; + Move(FBufferedData[FBufferOffset], LDestination^, LCopy); + Inc(FBufferOffset, LCopy); + Inc(Result, LCopy); + Inc(LDestination, LCopy); + if FPositionKnown then Inc(FPosition, LCopy); + end else begin + // Read returns available bytes. A second input read could block waiting + // for a network peer, even after this call has already received data. + if LReadFromSource then Exit; + DiscardReadBuffer; + LReadFromSource := True; + try + // Large reads need no intermediate copy or second full-size allocation. + if LRemaining >= Length(FBufferedData) then begin + LRead := FStream.Read(LDestination^, LRemaining); + if LRead > LRemaining then + raise EStreamError.Create('The input returned more bytes than requested.'); + if LRead <= 0 then begin + if LRead < 0 then begin + InvalidateAfterFailure; + Result := LRead; + end; + Exit; + end; + Inc(Result, LRead); + Inc(LDestination, LRead); + if FPositionKnown then Inc(FPosition, LRead); + end else begin + LRead := FStream.Read(FBufferedData[0], Length(FBufferedData)); + if LRead > Length(FBufferedData) then + raise EStreamError.Create('The input returned more bytes than requested.'); + if LRead <= 0 then begin + if LRead < 0 then begin + InvalidateAfterFailure; + Result := LRead; + end; + Exit; + end; + FBufferCount := LRead; + end; + except + InvalidateAfterFailure; + raise; + end; + end; + end; +end; + +procedure TBufferedStream.FlushUnreadData; +var LUnread: Integer; LPosition: Int64; +begin + LUnread := FBufferCount - FBufferOffset; + if LUnread > 0 then begin + // On failure keep the unread bytes. In particular, an unsupported seek must + // not turn a rejected read-to-write transition into lost buffered data. + LPosition := FStream.Seek(-Int64(LUnread), soCurrent); + if LPosition < 0 then + raise EStreamError.Create('The input could not synchronize its position.'); + FPosition := LPosition; + FPositionKnown := True; + end; + DiscardReadBuffer; +end; + +procedure TBufferedStream.SynchronizePosition; +begin + FlushUnreadData; +end; + +function TBufferedStream.Write(const Buffer; Count: Longint): Longint; +begin + if Count < 0 then + raise EArgumentOutOfRangeException.Create('Count must not be negative.'); + if Count = 0 then Exit(0); + // Even a short or failed write may change the input's length. + FSizeKnown := False; + if not FEnableReadBuffering then Exit(FStream.Write(Buffer, Count)); + SynchronizePosition; + try + Result := FStream.Write(Buffer, Count); + if Result > Count then + raise EStreamError.Create('The input wrote more bytes than requested.'); + if Result < 0 then InvalidateAfterFailure; + if FPositionKnown and (Result > 0) then Inc(FPosition, Result); + except + InvalidateAfterFailure; + raise; + end; +end; + +function TBufferedStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; +var LTarget, LAdjusted: Int64; +begin + if not FEnableReadBuffering then Exit(FStream.Seek(Offset, Origin)); + if (Origin = soCurrent) and (Offset = 0) then begin + if not FPositionKnown then begin + // Obtain an absolute cursor once. Ordinary reads and repeated Position + // checks subsequently use the logical cursor without asking the input. + LTarget := FStream.Seek(0, soCurrent); + // Some handle streams report a failed seek with -1 instead of raising. + // Preserve that sentinel and keep unread data available to Read. + if LTarget < 0 then Exit(LTarget); + FPosition := LTarget - (FBufferCount - FBufferOffset); + FPositionKnown := True; + end; + Exit(FPosition); + end; + case Origin of + soBeginning: begin + LTarget := Offset; + if TrySeekInBuffer(LTarget) then Exit(LTarget); + LAdjusted := Offset; + end; + soCurrent: begin + if FPositionKnown then begin + LTarget := AddOffset(FPosition, Offset); + if TrySeekInBuffer(LTarget) then Exit(LTarget); + end; + LAdjusted := AddOffset(Offset, -Int64(FBufferCount - FBufferOffset)); + end; + soEnd: LAdjusted := Offset; + else + raise EArgumentOutOfRangeException.Create('Invalid seek origin.'); + end; + try + Result := FStream.Seek(LAdjusted, Origin); + if Result < 0 then begin + FPositionKnown := False; + Exit; + end; + DiscardReadBuffer; + FPosition := Result; + FPositionKnown := True; + except + // Unsupported seeks must not consume unread bytes from a forward-only + // input. Its failure state is defined by the input; keep the cached bytes + // and obtain an absolute position again only if a later caller requests it. + FPositionKnown := False; + raise; + end; +end; + +{$IFNDEF FPC}{$IF DECLARED(TStreamCapabilities)} +function TBufferedStream.GetCapabilities: TStreamCapabilities; +begin + // Modern Delphi uses these flags for ReadBuffer/WriteBuffer OS diagnostics + // and CopyFrom preallocation. The wrapped stream still owns those behaviors. + Result := FStream.Capabilities; +end; +{$ENDIF}{$ENDIF} + +function TBufferedStream.GetSize: Int64; +begin + // Length metadata is independent of the byte buffer, including direct reads. + if FEnableStreamLengthCaching and FSizeKnown then Exit(FCachedStreamLength); + try + Result := FStream.Size; + if Result < 0 then + FPositionKnown := False + else if FEnableStreamLengthCaching then begin + FCachedStreamLength := Result; + FSizeKnown := True; + end; + except + // A size query may be unsupported even when sequential reads are valid. + // Keep unread data rather than silently skipping it after that exception. + FPositionKnown := False; + raise; + end; +end; + +procedure TBufferedStream.SetSize(NewSize: Longint); +begin + SetSize(Int64(NewSize)); +end; + +procedure TBufferedStream.SetSize(const NewSize: Int64); +begin + if NewSize < 0 then + raise EArgumentOutOfRangeException.Create('NewSize must not be negative.'); + // Direct-mode mutations must also invalidate independently cached length. + FSizeKnown := False; + if not FEnableReadBuffering then begin + FStream.Size := NewSize; + Exit; + end; + // Query the actual length afterward: an input may ignore or partly apply it. + SynchronizePosition; + try + FStream.Size := NewSize; + // Input implementations differ on whether resizing moves their cursor. + FPositionKnown := False; + except + InvalidateAfterFailure; + raise; + end; +end; + +end. diff --git a/src/libraries/sphere10/UVisualGrid.Search.png b/src/libraries/sphere10/UVisualGrid.Search.png new file mode 100644 index 000000000..6ef087061 Binary files /dev/null and b/src/libraries/sphere10/UVisualGrid.Search.png differ diff --git a/src/libraries/sphere10/UVisualGrid.VCL.inc b/src/libraries/sphere10/UVisualGrid.VCL.inc new file mode 100644 index 000000000..31789dcd5 --- /dev/null +++ b/src/libraries/sphere10/UVisualGrid.VCL.inc @@ -0,0 +1,433 @@ +{ Native VCL adapters used only by UVisualGrid's Delphi branch. Column metadata + uses the existing VCL header-section collection; drawing and input remain on + TDrawGrid. Child windows/components keep normal VCL ownership. } +{$IFDEF VISUALGRID_VCL_DECLARATIONS} +type + TGridColumn = THeaderSection; + TVisualGridHeaderClick = procedure(Sender:TObject; IsColumn:Boolean; Index:Integer) of object; + TVisualGridCellSelected = procedure(Sender:TObject; ACol,ARow:Integer) of object; + TGridZone = (gzInvalid, gzFixedCells, gzNormal); + + TDrawGrid = class(Vcl.Grids.TDrawGrid) + private + FHeader: THeaderControl; + FAutoFillColumns,FAdjusting,FKeepRanges,FHasRanges: Boolean; + FDataRows:Integer; + FRanges: TArray; + FHeaderClick: TVisualGridHeaderClick; + FSelectionChanged: TVisualGridCellSelected; + function GetColumns:THeaderSections; + function GetSelectedRangeCount:Integer; + function GetSelectedRange(AIndex:Integer):TRect; + function GetGridHeight:Integer; + procedure SetAutoFillColumns(AValue:Boolean); + function GetGridOptions:TGridOptions; + procedure SetGridOptions(AValue:TGridOptions); + protected + procedure Resize; override; + procedure ColWidthsChanged; override; + procedure FixedCellClick(ACol,ARow:Longint); override; + procedure SelectionMoved(const OldSel:TGridRect); override; + procedure MouseDown(Button:TMouseButton; Shift:TShiftState; X,Y:Integer); override; + procedure DrawCell(ACol,ARow:Longint; ARect:TRect; AState:TGridDrawState); override; + function SelectCell(ACol,ARow:Longint):Boolean; override; + public + constructor Create(AOwner:TComponent); override; + procedure FitColumns; + procedure InvalidateCell(ACol,ARow:Longint; ARedraw:Boolean); reintroduce; + procedure InvalidateRow(ARow:Longint); reintroduce; + procedure MoveExtend(AExtend:Boolean; ACol,ARow:Integer); + function IsFixedCellVisible(ACol,ARow:Integer):Boolean; + function MouseToGridZone(X,Y:Integer):TGridZone; + property Columns:THeaderSections read GetColumns; + property AutoFillColumns:Boolean read FAutoFillColumns write SetAutoFillColumns; + property SelectedRangeCount:Integer read GetSelectedRangeCount; + property SelectedRange[AIndex:Integer]:TRect read GetSelectedRange; + property GridHeight:Integer read GetGridHeight; + property DataRowCount:Integer read FDataRows write FDataRows; + property OnHeaderClick:TVisualGridHeaderClick read FHeaderClick write FHeaderClick; + property OnSelection:TVisualGridCellSelected read FSelectionChanged write FSelectionChanged; + property Options:TGridOptions read GetGridOptions write SetGridOptions; + end; + + TCheckComboItemChange = procedure(Sender:TObject; AIndex:Integer) of object; + TCheckComboBox = class(TComboBox) + private + FPopup:TForm; + FChecklist:TCheckListBox; + FChecks:TArray; + FOpening:Boolean; + FItemChange:TCheckComboItemChange; + FStoredItems:TStringList; + function ReadStoredItems:TStrings; + procedure SyncItems(Sender:TObject); + function GetChecked(AIndex:Integer):Boolean; + procedure SetChecked(AIndex:Integer; AValue:Boolean); + function GetObject(AIndex:Integer):TObject; + procedure SetObject(AIndex:Integer; AValue:TObject); + procedure CheckClicked(Sender:TObject); + procedure PopupDeactivated(Sender:TObject); + procedure PopupKeyDown(Sender:TObject; var Key:Word; Shift:TShiftState); + protected + procedure DropDown; override; + procedure WndProc(var Message:TMessage); override; + procedure CreateWnd; override; + public + constructor Create(AOwner:TComponent); override; + destructor Destroy; override; + procedure AddItem(const AText:string; AState:TCheckBoxState); reintroduce; + procedure Clear; override; + property Checked[AIndex:Integer]:Boolean read GetChecked write SetChecked; + property Objects[AIndex:Integer]:TObject read GetObject write SetObject; + property OnItemChange:TCheckComboItemChange read FItemChange write FItemChange; + property Items:TStrings read ReadStoredItems; + end; +{$ELSE} +constructor TDrawGrid.Create(AOwner:TComponent); +begin + inherited Create(AOwner); + FHeader:=THeaderControl.Create(Self); + FHeader.Visible:=False; + Options:=(Options-[goDrawFocusSelected])+[goFixedRowClick]; +end; + +function TDrawGrid.GetColumns:THeaderSections; +begin + Result:=FHeader.Sections; +end; + +procedure TDrawGrid.SetAutoFillColumns(AValue:Boolean); +begin + FAutoFillColumns:=AValue; + FitColumns; +end; + +function TDrawGrid.GetGridOptions:TGridOptions; +begin + Result:=Vcl.Grids.TDrawGrid(Self).Options; +end; + +procedure TDrawGrid.SetGridOptions(AValue:TGridOptions); +begin + Vcl.Grids.TDrawGrid(Self).Options:=AValue; + if not (goRowSelect in AValue) and not (goDrawFocusSelected in AValue) then begin + FRanges:=nil; + FHasRanges:=True; + end; +end; + +procedure TDrawGrid.FitColumns; +var i,LUsed,LStretch,LExtra,LWidth:Integer; +begin + // Buffer column metadata until VCL creates the grid window. + if FAdjusting or not Assigned(FHeader) or not HandleAllocated then Exit; + if Columns.Count=0 then Exit; + FAdjusting:=True; + try + ColCount:=Columns.Count; + LUsed:=0; LStretch:=0; + for i:=0 to Columns.Count-1 do begin + LWidth:=Columns[i].Width; + if LWidth<=0 then LWidth:=DefaultColWidth; + ColWidths[i]:=LWidth; + Inc(LUsed,LWidth+GridLineWidth); + if Columns[i].AutoSize then Inc(LStretch); + end; + if FAutoFillColumns then begin + LExtra:=Max(0,ClientWidth-LUsed); + if LStretch=0 then ColWidths[ColCount-1]:=ColWidths[ColCount-1]+LExtra + else for i:=0 to Columns.Count-1 do + if Columns[i].AutoSize then ColWidths[i]:=ColWidths[i]+LExtra div LStretch; + end; + finally FAdjusting:=False; end; +end; + +procedure TDrawGrid.Resize; +begin + inherited; + FitColumns; +end; + +procedure TDrawGrid.ColWidthsChanged; +var i:Integer; +begin + inherited; + if FAdjusting or not Assigned(FHeader) then Exit; + for i:=0 to Min(ColCount,Columns.Count)-1 do Columns[i].Width:=ColWidths[i]; +end; + +procedure TDrawGrid.FixedCellClick(ACol,ARow:Longint); +begin + inherited; + if (ARow=0) and Assigned(FHeaderClick) then FHeaderClick(Self,True,ACol); +end; + +procedure TDrawGrid.SelectionMoved(const OldSel:TGridRect); +begin + inherited; + if not FKeepRanges then begin + FHasRanges:=True; + SetLength(FRanges,1); + FRanges[0]:=Rect(Selection.Left,Selection.Top,Selection.Right,Selection.Bottom); + if Assigned(FSelectionChanged) then FSelectionChanged(Self,Col,Row); + end; +end; + +procedure TDrawGrid.MouseDown(Button:TMouseButton; Shift:TShiftState; X,Y:Integer); +var LCol,LRow,i,j:Integer; LToggle:Boolean; +begin + LToggle:=(Button=mbLeft) and (ssCtrl in Shift) and + (goRowSelect in Options) and (goRangeSelect in Options) and + (MouseToGridZone(X,Y)=gzNormal); + FKeepRanges:=LToggle; + try inherited; finally FKeepRanges:=False; end; + if not LToggle then begin + if (Button=mbLeft) and (MouseToGridZone(X,Y)=gzNormal) and + ((goRowSelect in Options) or (goDrawFocusSelected in Options)) then begin + FHasRanges:=True; + SetLength(FRanges,1); + FRanges[0]:=Rect(Selection.Left,Selection.Top,Selection.Right,Selection.Bottom); + if Assigned(FSelectionChanged) then FSelectionChanged(Self,Col,Row); + end; + Exit; + end; + FHasRanges:=True; + MouseToCell(X,Y,LCol,LRow); + for i:=0 to High(FRanges) do + if (LRow>=FRanges[i].Top) and (LRow<=FRanges[i].Bottom) then begin + // Toggle one row while retaining the rest of a selected range. + if FRanges[i].Top< LRow then begin + SetLength(FRanges,Length(FRanges)+1); + FRanges[High(FRanges)]:=Rect(0,FRanges[i].Top,ColCount-1,LRow-1); + end; + if FRanges[i].Bottom>LRow then begin + SetLength(FRanges,Length(FRanges)+1); + FRanges[High(FRanges)]:=Rect(0,LRow+1,ColCount-1,FRanges[i].Bottom); + end; + for j:=i to High(FRanges)-1 do FRanges[j]:=FRanges[j+1]; + SetLength(FRanges,Length(FRanges)-1); + Invalidate; + if Assigned(FSelectionChanged) then FSelectionChanged(Self,LCol,LRow); + Exit; + end; + SetLength(FRanges,Length(FRanges)+1); + FRanges[High(FRanges)]:=Rect(0,LRow,ColCount-1,LRow); + Invalidate; + if Assigned(FSelectionChanged) then FSelectionChanged(Self,LCol,LRow); +end; + +procedure TDrawGrid.DrawCell(ACol,ARow:Longint; ARect:TRect; AState:TGridDrawState); +var i:Integer; +begin + if FHasRanges and (Length(FRanges)=0) and (ARow>=FixedRows) then begin + Exclude(AState,gdSelected); + Canvas.Brush.Color:=Color; + Canvas.Font.Color:=Font.Color; + Canvas.FillRect(ARect); + end; + if (goRowSelect in Options) and FHasRanges and (ARow>=FixedRows) then begin + Exclude(AState,gdSelected); + Canvas.Brush.Color:=Color; + Canvas.Font.Color:=Font.Color; + Canvas.FillRect(ARect); + for i:=0 to High(FRanges) do + if (ARow>=FRanges[i].Top) and (ARow<=FRanges[i].Bottom) then begin + Include(AState,gdSelected); + Canvas.Brush.Color:=clHighlight; + Canvas.Font.Color:=clHighlightText; + Canvas.FillRect(ARect); + Break; + end; + end; + inherited DrawCell(ACol,ARow,ARect,AState); +end; + +function TDrawGrid.SelectCell(ACol,ARow:Longint):Boolean; +begin + Result:=Assigned(FHeader) and (Columns.Count>0) and (ARow>0) and (ARow<=FDataRows); + if Result then Result:=inherited SelectCell(ACol,ARow); +end; + +function TDrawGrid.GetSelectedRangeCount:Integer; +begin + if FHasRanges then Result:=Length(FRanges) else Result:=1; +end; + +function TDrawGrid.GetSelectedRange(AIndex:Integer):TRect; +begin + if Length(FRanges)>0 then Result:=FRanges[AIndex] + else Result:=Rect(Selection.Left,Selection.Top,Selection.Right,Selection.Bottom); +end; + +function TDrawGrid.GetGridHeight:Integer; +var i:Integer; +begin + Result:=0; + for i:=0 to RowCount-1 do Inc(Result,RowHeights[i]+GridLineWidth); +end; + +procedure TDrawGrid.InvalidateCell(ACol,ARow:Longint; ARedraw:Boolean); +begin + if HandleAllocated then inherited InvalidateCell(ACol,ARow); +end; + +procedure TDrawGrid.InvalidateRow(ARow:Longint); +begin + if HandleAllocated then inherited InvalidateRow(ARow); +end; + +procedure TDrawGrid.MoveExtend(AExtend:Boolean; ACol,ARow:Integer); +begin + MoveColRow(ACol,ARow,not AExtend,True); +end; + +function TDrawGrid.IsFixedCellVisible(ACol,ARow:Integer):Boolean; +var R:TRect; +begin + R:=CellRect(ACol,ARow); + Result:=(R.Right>0) and (R.Left0) and (R.Top=ColCount) or (R>=RowCount) or (R>FDataRows) then Exit; + if (C0 then ItemIndex:=0; +end; + +procedure TCheckComboBox.CreateWnd; +begin + inherited; + SyncItems(Self); +end; + +procedure TCheckComboBox.AddItem(const AText:string; AState:TCheckBoxState); +begin + Items.Add(AText); + SetLength(FChecks,Items.Count); + FChecks[Items.Count-1]:=AState=cbChecked; +end; + +procedure TCheckComboBox.Clear; +begin + if Assigned(FPopup) then FPopup.Hide; + if Assigned(FChecklist) then FChecklist.Clear; + FStoredItems.Clear; + FChecks:=nil; +end; + +function TCheckComboBox.GetChecked(AIndex:Integer):Boolean; +begin + Result:=(AIndex>=0) and (AIndex0)) or + (Message.Msg=WM_LBUTTONDOWN) or + ((Message.Msg=WM_KEYDOWN) and (Message.WParam=VK_F4)) or + ((Message.Msg=WM_SYSKEYDOWN) and (Message.WParam=VK_DOWN)) then begin + if Enabled then DropDown; + Message.Result:=0; + Exit; + end; + inherited; + if (Message.Msg=CM_ENABLEDCHANGED) and not Enabled and Assigned(FPopup) then FPopup.Hide; +end; + +procedure TCheckComboBox.CheckClicked(Sender:TObject); +begin + if FChecklist.ItemIndex>=0 then + SetChecked(FChecklist.ItemIndex,FChecklist.Checked[FChecklist.ItemIndex]); +end; + +procedure TCheckComboBox.DropDown; +var P:TPoint; i:Integer; +begin + if FOpening then Exit; + FOpening:=True; + try + DroppedDown:=False; + if not Assigned(FPopup) then begin + FPopup:=TForm.CreateNew(Self); + FPopup.BorderStyle:=bsNone; + FPopup.Position:=poDesigned; + FPopup.PopupMode:=pmExplicit; + FPopup.PopupParent:=GetParentForm(Self); + FPopup.OnDeactivate:=PopupDeactivated; + FChecklist:=TCheckListBox.Create(FPopup); + FChecklist.Parent:=FPopup; + FChecklist.Align:=alClient; + FChecklist.OnClickCheck:=CheckClicked; + FChecklist.OnKeyDown:=PopupKeyDown; + end; + FChecklist.Items.Assign(Items); + for i:=0 to Items.Count-1 do FChecklist.Checked[i]:=GetChecked(i); + P:=ClientToScreen(Point(0,Height)); + FPopup.SetBounds(P.X,P.Y,Max(Width,160),Min(240,Max(32,Items.Count*(Font.Height*-1+8)+4))); + FPopup.Show; + FChecklist.SetFocus; + finally FOpening:=False; end; +end; +{$ENDIF} diff --git a/src/libraries/sphere10/UVisualGrid.VCL.rc b/src/libraries/sphere10/UVisualGrid.VCL.rc new file mode 100644 index 000000000..8ebd98857 --- /dev/null +++ b/src/libraries/sphere10/UVisualGrid.VCL.rc @@ -0,0 +1 @@ +VISUALGRID_SEARCH RCDATA "UVisualGrid.Search.png" \ No newline at end of file diff --git a/src/libraries/sphere10/UVisualGrid.pas b/src/libraries/sphere10/UVisualGrid.pas index f2e49f61e..9455bb70c 100644 --- a/src/libraries/sphere10/UVisualGrid.pas +++ b/src/libraries/sphere10/UVisualGrid.pas @@ -1,8 +1,8 @@ -unit UVisualGrid; +unit UVisualGrid; -{ Copyright (c) 2017 - 2018 Sphere 10 Software +{ Copyright (c) 2017 - 2018 Herman Schoenfeld - Enterprise-class grid control for Lazarus/FPC. + Data-bound grid with filtering, sorting and paging for Delphi/VCL and Lazarus/LCL. Distributed under the MIT software license, see the accompanying file LICENSE or visit http://www.opensource.org/licenses/mit-license.php. @@ -13,19 +13,65 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Public types in this unit + + Type | Purpose + ---------------------------------------+----------------------------------------------------------------------- + EVisualGridError | Reports invalid grid configuration or operations. + TCheckComboBox | Supplies the VCL column-picker control used by the grid. + TCheckComboItemChange | Notifies a VCL column-picker checkbox change. + TColumnInitializeEvent | Configures a column when its native counterpart is created. + TCustomVisualGrid | Coordinates rendering, paging, filtering, loading and refreshes. + TDeselectionType | Selects how existing grid selections may be cleared. + TDrawGrid | Adapts VCL drawing and geometry for the exposed native grid. + TDrawVisualCellEvent | Customizes cell painting using its value and row data. + TGridColumn | Aliases the VCL header-section column metadata. + TGridZone | Identifies fixed, ordinary or invalid VCL grid cells. + TPreparePopupMenuEvent | Supplies a context menu for the current selection. + TSearchMode | Selects a single expression or per-column search. + TSelectionEvent | Notifies changes to selected cells or rows. + TSelectionType | Selects cell, row or multiple-row selection. + TSortMode | Selects no sorting, one column or multiple columns. + TVisualCellRenderers | Supplies reusable cell-drawing callbacks. + TVisualColumn | Configures a column's binding, layout, filters and appearance. + TVisualColumnDataSanitizer | Converts source values before rendering a cell. + TVisualColumnRenderer | Renders a cell using its value, row and draw state. + TVisualGrid | Publishes the reusable grid's design-time properties and events. + TVisualGridCaption | Exposes the grid caption's text, font, visibility and alignment. + TVisualGridCellSelected | Notifies native VCL cell selection. + TVisualGridHeaderClick | Notifies native VCL column or row-header clicks. + TVisualGridOptions | Configures column sizing, searching and paging presentation. + TVisualGridPageMode | Selects all rows, a viewport-sized page or a manual page size. + TVisualGridSearchExpressionService | Provides the grid search-service extension point. + TVisualGridSelection | Describes selected cells, rows and their associated data. +} + +{$IFDEF FPC} {$MODE DELPHI} +{$ENDIF} -{$modeswitch nestedprocvars} +{$IFDEF FPC}{$modeswitch nestedprocvars}{$ENDIF} {.$DEFINE VISUALGRID_DEBUG} interface uses - Classes, SysUtils, StdCtrls, ExtCtrls, Controls, Grids, Types, Graphics, - UCommon, UCommon.Data, UCommon.Collections, Generics.Collections, Generics.Defaults, Menus, ComboEx, Buttons, Math, - LResources, syncobjs; + Classes, SysUtils, Types, Math, Generics.Collections, Generics.Defaults, SyncObjs, + {$IFDEF FPC} + StdCtrls, ExtCtrls, Controls, Grids, Graphics, Menus, ComboEx, Buttons, LResources, + {$ELSE} + Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Controls, Vcl.Grids, Vcl.Graphics, Vcl.Menus, + Vcl.Buttons, Vcl.ComCtrls, Vcl.CheckLst, Vcl.Forms, Winapi.Windows, Winapi.Messages, + {$ENDIF} + UCommon, UCommon.UI, UCommon.Data, UCommon.Collections; + +{$IFNDEF FPC} + {$DEFINE VISUALGRID_VCL_DECLARATIONS} + {$I UVisualGrid.VCL.inc} + {$UNDEF VISUALGRID_VCL_DECLARATIONS} +{$ENDIF} type @@ -77,8 +123,8 @@ TVisualColumn = class procedure SetStretchedToFill(AValue: boolean); inline; procedure SetAutoWidth(AValue: boolean); inline; procedure SetWidth(AValue: Integer); inline; - procedure SetName(const AValue: utf8string); - procedure SetBinding(const AValue : AnsiString); + procedure SetName(const AValue: String); + procedure SetBinding(const AValue : String); procedure SetHeaderAlignment(const AAlignment : TAlignment); procedure SetHeaderFontStyles(const AStyles : TFontStyles); procedure SetDataAlignment(const AAlignment : TAlignment); @@ -91,11 +137,11 @@ TVisualColumn = class // Functional FIndex : Integer; - FName : utf8string; + FName : String; FFilters : TDataFilters; - FBinding : AnsiString; - FSortBinding: AnsiString; - FDisplayBinding: AnsiString; + FBinding : String; + FSortBinding: String; + FDisplayBinding: String; FSanitizer : TVisualColumnDataSanitizer; FWidth : Integer; FStretchColumn : Boolean; @@ -117,11 +163,11 @@ TVisualColumn = class public constructor Create(AGrid: TCustomVisualGrid); property Index : Integer read FIndex write FIndex; - property Name : utf8string read FName write SetName; + property Name : String read FName write SetName; property Filters : TDataFilters read FFilters write FFilters; - property Binding : AnsiString read FBinding write SetBinding; - property SortBinding : AnsiString read FSortBinding write FSortBinding; - property DisplayBinding : AnsiString read FDisplayBinding write FDisplayBinding; + property Binding : String read FBinding write SetBinding; + property SortBinding : String read FSortBinding write FSortBinding; + property DisplayBinding : String read FDisplayBinding write FDisplayBinding; property Sanitizer: TVisualColumnDataSanitizer read FSanitizer write FSanitizer; property AutoWidth : boolean read FAutoWidth write SetAutoWidth; property Visible : boolean read FVisible write FVisible; @@ -145,8 +191,8 @@ TVisualColumn = class { TVisualGrid Events } - TPreparePopupMenuEvent = procedure(Sender: TObject; constref ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu) of object; - TSelectionEvent = procedure(Sender: TObject; constref ASelection: TVisualGridSelection) of object; + TPreparePopupMenuEvent = procedure(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection; out APopupMenu: TPopupMenu) of object; + TSelectionEvent = procedure(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection) of object; TDrawVisualCellEvent = procedure(Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean) of object; TColumnInitializeEvent = procedure(Sender: TObject; AColumn : TVisualColumn) of object; @@ -156,6 +202,8 @@ EVisualGridError = class(Exception); { TVisualGridOptions } + // vgoAutoHidePaging hides page navigation while every row fits one page; the + // footer keeps the paging-mode menu and the total visible. TVisualGridOptions = set of (vgoColAutoFill, vgoColSizing, vgoAutoHidePaging, vgoAutoHideSearchPanel, vgoMultiSearchCheckComboBox, vgoSortDirectionAllowNone); { TSortMode } @@ -166,6 +214,10 @@ EVisualGridError = class(Exception); TSearchMode = (smSingle, smMulti); + { TVisualGridPageMode } + + TVisualGridPageMode = (vpmSingle, vpmAuto, vpmManual); + { TVisualGridCaption } TVisualGridCaption = class(TPersistent) @@ -246,7 +298,10 @@ TSearchEdit = class FClientPanel: TPanel; FLoadDataPanel: TPanel; FLoadDataLabel: TLabel; - FLoadDataProgressLabel: TLabel; + FLoadingSubtitle: TLabel; + FLoadingImage: TImage; + FLoadingCircle : TLoadingCircle; + FLoadingStartedAt : QWord; FBottomPanel: TPanel; FBottomCenterPanel: TPanel; FBottomRightPanel: TPanel; @@ -262,10 +317,12 @@ TSearchEdit = class FPageSizeEdit: TEdit; FPageSizeLabel: TLabel; FAllRecordsCountLabel: TLabel; + FPageModeButton: TBitBtn; + FPageModeMenu: TPopupMenu; FDrawGrid: TDrawGrid; FDelayedBoundsChangeTimer: TTimer; - FFetchDataThreadTimer: TTimer; + FLoadingDelayTimer: TTimer; FEditingDoneTimer: TTimer; FSearchKindPopupMenu: TPopupMenu; FSingleSearchMenuItem: TMenuItem; @@ -282,15 +339,36 @@ TSearchEdit = class procedure GridHeaderClick(Sender: TObject; IsColumn: Boolean; Index: Integer); procedure PageIndexEditingDone(Sender: TObject); procedure PageSizeEditingDone(Sender: TObject); + procedure PageModeChanged(Sender: TObject); + procedure PageModeButtonClick(Sender: TObject); procedure PageNavigationClick(Sender: TObject); procedure MultiSearchCheckComboBoxChange(Sender: TObject; AIndex: Integer); + procedure GridGeometryChanged(Sender: TObject); + procedure ScheduleBoundsChange; procedure DelayedBoundsChange(Sender: TObject); - procedure FetchDataThreadProgress(Sender: TObject); + procedure FetchDataThreadCompleted(Sender: TObject); + procedure ShowDelayedLoading(Sender: TObject); procedure SearchButtonClick(Sender: TObject); + {$IFNDEF FPC}procedure LoadSearchGlyph;{$ENDIF} private FCaption: TVisualGridCaption; FFetchDataInThread: boolean; FActiveThread: TThread; + FFetchCompletion: TThreadNotify; + FFetchInProgress, FJoiningFetch: Boolean; + FShuttingDown: Boolean; + FManualLoading, FManualLoadingDisablesInput: Boolean; + FFetchControlsEnabled, FAutomaticLoading: Boolean; + FManualLoadingTitle, FManualLoadingSubtitle: String; + FAutomaticLoadingTitle, FLoadingError: String; + FRefreshPending, FRefreshColumnsPending: Boolean; + FRefreshThrottle: TThrottledEvent; + FRefreshReady, FRefreshClientPending, FExecutingRefresh, FRefreshCancelled: Boolean; + FRefreshSuspended, FActiveRefreshCancelled: Boolean; + FInternalLayoutDepth: Integer; + FPendingRefreshParams, FActiveRefreshParams: TPageFetchParams; + FPendingRefreshSource, FActiveRefreshSource: IDataSource; + FBoundsClientPending, FBoundsAlreadyNotified, FBoundsVisualPending: Boolean; FOnPreparePopupMenu: TPreparePopupMenuEvent; FOnSelection: TSelectionEvent; FOnFinishedUpdating: TNotifyEvent; @@ -299,6 +377,9 @@ TSearchEdit = class FSearchMode : TSearchMode; FShowAllData: boolean; FAutoPageSize: boolean; + FPageMode: TVisualGridPageMode; + FModeScrollResetPending: Boolean; + FManualPageSize: Integer; FCanPage: boolean; FCanSearch: boolean; FSelectionType: TSelectionType; @@ -316,19 +397,36 @@ TSearchEdit = class function GetColumn(Index: Integer): TVisualColumn; function GetActiveDataTable: PDataTable; function GetRowCount: Integer; inline; + function GetIsUpdating : Boolean; inline; function GetRows(ARow: Integer): Variant; function GetSelection: TVisualGridSelection; function GetSelectedRows : TArray; procedure ControlsEnable(AEnable: boolean); + function CaptureRefreshParams: TPageFetchParams; + procedure QueueRefresh(ARefreshColumns, AClientInput, ANotify: Boolean); + procedure ExecutePendingRefresh(AOrdinaryAdmission: Boolean = False); + procedure RefreshThrottleDelivery(Sender: TObject); + procedure RefreshThrottleWaitingChanged(Sender: TObject); + procedure RefreshThrottleFinished(Sender: TObject); + procedure ChangePageSize(Value: Integer; AClientInput, ANotify: Boolean); + procedure UpdatePagingControls; + procedure ResetModeScrollPosition(AClearPending: Boolean); + procedure UpdatePageModeGlyph; + function CanChangePageMode: Boolean; + procedure ApplyControlsState; + procedure RefreshLoadingPresentation; function GetCanvas: TCanvas; procedure SetCells(ACol, ARow: Integer; AValue: Variant); procedure SetFetchDataInThread(AValue: boolean); + procedure SetRefreshSuspended(AValue: Boolean); procedure SetSortMode(AValue: TSortMode); procedure SetSearchMode(AValue: TSearchMode); procedure SetOptions(AValue: TVisualGridOptions); procedure SetRows(ARow: Integer; AValue: Variant); procedure SetShowAllData(AValue: boolean); procedure SetAutoPageSize(AValue: boolean); + procedure SetPageMode(AValue: TVisualGridPageMode); + procedure SetManualPageSize(AValue: Integer); procedure SetCanPage(AValue: boolean); procedure SetCanSearch(AValue: boolean); {$IFDEF VISUALGRID_DEBUG} @@ -343,7 +441,16 @@ TSearchEdit = class protected { TComponent } procedure Loaded; override; protected { TControl } - procedure BoundsChanged; override; + procedure LayoutLoadingPanel(Sender : TObject); + procedure BringLoadingPanelToFront; + procedure BeginLoading; + procedure UpdateLoading; + procedure EndLoading; + procedure ShowLoadingError(const AMessage : String); + {$IFDEF FPC}procedure BoundsChanged; override;{$ELSE}procedure Resize; override; + function Scale96ToFont(AValue:Integer):Integer; + {$ENDIF} + procedure SetParent(AParent:TWinControl); override; protected FGUIUpdates: TUpdateOfVisualGridGUI; FDataTable: TDataTable; @@ -376,24 +483,53 @@ TSearchEdit = class procedure RefreshPageIndexAndGridInterface; procedure RefreshPageIndexData(ARefreshColumns: boolean); procedure ResizeSearchEdit(ACol: Integer); - procedure SetPageIndexEditText(const AStr: utf8string); - procedure SetPageSizeEditText(const AStr: utf8string); + procedure SetPageIndexEditText(const AStr: String); + procedure SetPageSizeEditText(const AStr: String); procedure BeforeFetchPage; procedure FetchPage(out AResult: TPageFetchResult); procedure AfterFetchPage; property ActiveDataTable: PDataTable read GetActiveDataTable; procedure UpdateSelection(ASelectionType: TSelectionType; AResetSelection: boolean); - function SelectionsEquals(constref A, B: TVisualGridSelection): boolean; + function SelectionsEquals({$IFDEF FPC}constref{$ELSE}const{$ENDIF} A, B: TVisualGridSelection): boolean; procedure ResetSelection(out ASelection: TVisualGridSelection); inline; public constructor Create(Owner: TComponent); override; destructor Destroy; override; property DataSource: IDataSource read FDataSource write SetDataSource; property PageSize: Integer read FPageSize write SetPageSize default 100; + // Single returns all matching rows; Auto fits the viewport; Manual uses + // ManualPageSize. Mode changes start at page zero and preserve manual size. + property PageMode: TVisualGridPageMode read FPageMode write SetPageMode default vpmManual; + property ManualPageSize: Integer read FManualPageSize write SetManualPageSize default 100; property PageIndex: Integer read FPageIndex write SetPageIndex default -1; property AutoPageSize: boolean read FAutoPageSize write SetAutoPageSize default false; property ShowAllData: boolean read FShowAllData write SetShowAllData default false; property FetchDataInThread: boolean read FFetchDataInThread write SetFetchDataInThread; + // True while an asynchronous page fetch is running or awaiting UI completion. + property IsUpdating : Boolean read GetIsUpdating; + // UI-thread refresh admission for every client request. temNone preserves + // immediate behavior; burst modes retain only the latest pending parameters. + // The grid owns this component. Internal worker/layout replay never notifies it. + property RefreshThrottle: TThrottledEvent read FRefreshThrottle; + // UI-thread policy boundary. Requests and visual layout continue while + // suspended, but no provider query is admitted. Active work is cancelled + // cooperatively and its result is ignored; this setter never joins it. + // Cancel RefreshThrottle while suspended to resume immediately, otherwise + // the retained request still respects its current quiet interval. + property RefreshSuspended: Boolean read FRefreshSuspended write SetRefreshSuspended; + // UI-thread cooperative cancellation of the active asynchronous fetch only. + // Never joins, queues input or changes throttle, suspension or manual cards. + // Existing rows remain; the provider must outlive its normal retirement. + procedure CancelActiveFetch; + // UI-thread presentation only. The graphic is copied; its owner may free it + // immediately. A nil/empty graphic uses the existing loading circle. + // Repeated calls replace this manual state without fetching any data. + procedure ShowLoading(const ATitle: String; const ASubtitle: String = ''; AImage: TGraphic = nil; ADisableInput: Boolean = True); + // Releases only manual presentation, retaining a current fetch/error card + // and its input restrictions. Neither method changes this grid's Enabled. + procedure HideLoading; + // True for a held manual overlay, independently of worker-only IsUpdating. + property LoadingVisible: Boolean read FManualLoading; property DefaultStretchedColumn: Integer read FDefaultStretchedColumn write FDefaultStretchedColumn; property CanPage: boolean read FCanPage write SetCanPage default true; @@ -419,11 +555,13 @@ TSearchEdit = class property OnDrawVisualCell: TDrawVisualCellEvent read FOnDrawVisualCell write FOnDrawVisualCell; property OnSelection: TSelectionEvent read FOnSelection write FOnSelection; property OnPreparePopupMenu: TPreparePopupMenuEvent read FOnPreparePopupMenu write FOnPreparePopupMenu; + // Raised once after page population, layout and selection restoration. + // Resizing an already populated page does not raise a fetch completion. property OnFinishedUpdating : TNotifyEvent read FOnFinishedUpdating write FOnFinishedUpdating; property WidgetControl: TControl read FWidgetControl write SetWidgetControl; property InternalDrawGrid : TDrawGrid read FDrawGrid; - function AddColumn(const AName: utf8string) : TVisualColumn; + function AddColumn(const AName: String) : TVisualColumn; procedure RefreshGrid; procedure ClearSelection(AIgnoreDeselectionType: boolean = false); end; @@ -436,6 +574,8 @@ TVisualGrid = class(TCustomVisualGrid) property Align; property PageSize; + property PageMode; + property ManualPageSize; property AutoPageSize; property ShowAllData; property CanPage; @@ -465,11 +605,34 @@ TVisualCellRenderers = class TVisualGridSearchExpressionService = class end; +{$IFDEF VISUALGRID_TEST_WORKER_START} +var + // Dedicated test builds can force cancellation before the RTL schedules Execute. + VisualGridBeforeWorkerStart: TNotifyEvent; +{$ENDIF} + procedure Register; implementation -uses Variants, UMemory, Dialogs; +uses Variants, UMemory, + {$IFDEF FPC}Dialogs;{$ELSE}Vcl.Dialogs, Vcl.Imaging.pngimage;{$ENDIF} + +{$IFNDEF FPC} +{$R UVisualGrid.VCL.res} +{$I UVisualGrid.VCL.inc} + +procedure DrawGridText(ACanvas:TCanvas; ARect:TRect; const AText:string; AAlignment:TAlignment); +var LFlags:Cardinal; +begin + LFlags:=DT_SINGLELINE or DT_VCENTER or DT_END_ELLIPSIS or DT_NOPREFIX; + case AAlignment of + taRightJustify: LFlags:=LFlags or DT_RIGHT; + taCenter: LFlags:=LFlags or DT_CENTER; + end; + Winapi.Windows.DrawText(ACanvas.Handle,PChar(AText),Length(AText),ARect,LFlags); +end; +{$ENDIF} resourcestring sTotal = 'Total: %d'; @@ -477,9 +640,14 @@ implementation sMultiColumnSearch = 'Multi-Column Search'; sPageSize = 'Page size:'; sSearchExpression = 'Search expression'; - sDataLoading = 'DATA LOADING'; + sDataLoading = 'Loading...'; + sDataUpdating = 'Updating...'; sExpression = 'Expression'; sImproperColumnIndex = 'Improper column index. Max expected is %d but %d found.'; + sPagingModeSingle = 'Single'; + sPagingModeAuto = 'Auto'; + sPagingModeManual = 'Manual'; + sPagingModeHint = 'Paging mode: %s. Choose %s, %s or %s.'; const CT_DEFAULT_CELL_PADDING_LEFT = 0; @@ -498,10 +666,12 @@ TFetchDataThread = class(TThread) FGrid: TCustomVisualGrid; FLastFetchDataResult: TCustomVisualGrid.TLastFetchDataResult; FIgnoreRecalcPageCount: boolean; + FError: String; + FResultReady: LongInt; procedure Execute; override; + procedure DoTerminate; override; public constructor Create(AGrid: TCustomVisualGrid; ARefreshColumns: boolean); - destructor Destroy; override; end; procedure Register; @@ -546,7 +716,7 @@ procedure TVisualGridCaption.SetAlignment(AValue: TAlignment); procedure TVisualGridCaption.SetFont(AValue: TFont); begin - if FLabel.Font.IsEqual(AValue) then Exit; + if FLabel.Font.{$IFDEF FPC}IsEqual{$ELSE}Equals{$ENDIF}(AValue) then Exit; FLabel.Font.Assign(AValue); FOwner.LayoutChanged; end; @@ -567,13 +737,16 @@ constructor TVisualGridCaption.Create(AOwner: TCustomVisualGrid); with FLabel do begin + {$IFDEF FPC} AnchorSideLeft.Control := AOwner.FTopPanelLeft; AnchorSideLeft.Side := asrBottom; AnchorSideTop.Control := AOwner.FTopPanelRight; AnchorSideTop.Side := asrCenter; AnchorSideRight.Control := AOwner.FTopPanelRight; + {$ENDIF} Anchors := [akTop, akLeft, akRight]; Visible := false; + {$IFNDEF FPC}Align:=alClient; Layout:=tlCenter;{$ENDIF} end; end; @@ -604,12 +777,15 @@ constructor TVisualColumn.Create(AGrid: TCustomVisualGrid); function TVisualColumn.GetStretchedToFill: boolean; begin - Result := FColumn.SizePriority > 0; + {$IFDEF FPC}Result := FColumn.SizePriority > 0;{$ELSE}Result := FStretchColumn;{$ENDIF} end; function TVisualColumn.GetWidth: Integer; begin - Result := FColumn.Width; + {$IFDEF FPC}Result := FColumn.Width;{$ELSE} + if FGrid.FDrawGrid.HandleAllocated then Result:=FGrid.FDrawGrid.ColWidths[FColumn.Index] + else Result:=FColumn.Width; + {$ENDIF} end; procedure TVisualColumn.SetSortDirection(AValue: TSortDirection); @@ -642,13 +818,13 @@ procedure TVisualColumn.SetWidth(AValue: Integer); AttachGridColumn(FColumn) end; -procedure TVisualColumn.SetName(const AValue: utf8string); +procedure TVisualColumn.SetName(const AValue: String); begin FName := AValue; Binding := UTF8Decode(AValue); end; -procedure TVisualColumn.SetBinding(const AValue: AnsiString); +procedure TVisualColumn.SetBinding(const AValue: String); begin FBinding := AValue; FSortBinding := AValue; @@ -690,15 +866,24 @@ procedure TVisualColumn.AttachGridColumn(AGridColumn : TGridColumn); if FAutoWidth then FStretchColumn := false else if NOT FStretchColumn then begin - FColumn.Width := FWidth; + {$IFDEF FPC}FColumn.Width := FWidth;{$ELSE} + FColumn.Width := Max(1,IfThen(FWidth>0,FWidth,FGrid.FDrawGrid.DefaultColWidth)); + {$ENDIF} end; // StretchedToFill - FColumn.SizePriority := ifthen(FStretchColumn, 1); + {$IFDEF FPC}FColumn.SizePriority := ifthen(FStretchColumn, 1);{$ELSE}FColumn.AutoSize:=FStretchColumn;{$ENDIF} // Caption - already painted in default drawing event - if Self.FColumn.Title.Caption <> '' then - Self.FColumn.Title.Caption:=''; + {$IFDEF FPC} + if Self.FColumn.Title.Caption <> '' then Self.FColumn.Title.Caption:=''; + {$ELSE} + FColumn.Text:=''; + // Text metrics require a native window; retain AutoWidth until docking. + if FAutoWidth and Assigned(FGrid.Parent) then FColumn.Width:=FGrid.Canvas.TextWidth(string(FName))+16; + FGrid.FDrawGrid.FitColumns; + {$ENDIF} + FGrid.GridGeometryChanged(FGrid.FDrawGrid); end; { TVisualGridSelection } @@ -736,7 +921,7 @@ function TVisualGridSelection.GetRowCount: longint; class procedure TVisualCellRenderers.DollarValue (Sender: TObject; ACol, ARow: Longint; Canvas: TCanvas; Rect: TRect; State: TGridDrawState; const CellData, RowData: Variant; var Handled: boolean); var LReal : Real; - LTextStyle: TTextStyle; + {$IFDEF FPC}LTextStyle: TTextStyle;{$ENDIF} begin if NOT VarIsNumeric(CellData) then exit; @@ -744,10 +929,14 @@ class procedure TVisualCellRenderers.DollarValue (Sender: TObject; ACol, ARow: L LReal := CellData; Canvas.Font.Color:= IIF (LReal < 0, clRed, clGreen); Canvas.Font.Style:=[fsBold]; + {$IFDEF FPC} LTextStyle := Canvas.TextStyle; LTextStyle.Alignment:=taRightJustify; Canvas.TextStyle:=LTextStyle; - Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2, Format('$%.2F', [LReal]), LTextStyle); + Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2, Format('$%.2F', [LReal]), LTextStyle); + {$ELSE} + DrawGridText(Canvas,Rect,Format('$%.2F',[LReal]),taRightJustify); + {$ENDIF} Handled := true; end; @@ -755,20 +944,38 @@ class procedure TVisualCellRenderers.DollarValue (Sender: TObject; ACol, ARow: L procedure TFetchDataThread.Execute; begin - FGrid.FetchPage(FLastFetchDataResult.FetchResult); + if Terminated then Exit; + try + FGrid.FetchPage(FLastFetchDataResult.FetchResult); + except + on E: Exception do FError := E.Message; + end; +end; + +procedure TFetchDataThread.DoTerminate; +begin + // FPC skips Execute if cancellation wins before scheduling, but still calls + // DoTerminate. Always retire that request through the owned async dispatcher; + // inherited OnTerminate would synchronously wait for the UI during shutdown. + // Publish readiness first so an old queued wake cannot join a newer worker. + InterlockedExchange(FResultReady, 1); + FGrid.FFetchCompletion.Invoke(FGrid, FGrid.FetchDataThreadCompleted); end; -constructor TFetchDataThread.Create(AGrid: TCustomVisualGrid; - ARefreshColumns: boolean); +constructor TFetchDataThread.Create(AGrid: TCustomVisualGrid; ARefreshColumns: boolean); +var LTransferred:Boolean; begin + // Delphi starts an unsuspended thread after the complete constructor returns. + inherited Create({$IFDEF FPC}True{$ELSE}False{$ENDIF}); + FreeOnTerminate := false; AGrid.FActiveThread := Self; FGrid := AGrid; + LTransferred:=False; + try FGrid.ControlsEnable(false); - FGrid.FFetchDataThreadTimer.Enabled:=true; - FGrid.FLoadDataPanel.Visible:=True; - FGrid.FLoadDataPanel.BringToFront; + FGrid.FFetchInProgress:=True; + FGrid.BeginLoading; FGrid.BeforeFetchPage; - FreeOnTerminate:=true; FLastFetchDataResult.RefreshColumns:=ARefreshColumns; FIgnoreRecalcPageCount:=FGrid.FIgnoreRecalcPageCount; @@ -776,15 +983,25 @@ constructor TFetchDataThread.Create(AGrid: TCustomVisualGrid; New(FGrid.FCachedDataTable); Move(FGrid.FDataTable, FGrid.FCachedDataTable^, SizeOf(TDataTable)); FillChar(FGrid.FDataTable, SizeOf(TDataTable), #0); - inherited Create(false); -end; - -destructor TFetchDataThread.Destroy; -begin - FLastFetchDataResult.FromThread:=true; - FGrid.FLastFetchDataResult := FLastFetchDataResult; - Synchronize(FGrid.AfterFetchPage); - inherited Destroy; + LTransferred:=True; + {$IFDEF VISUALGRID_TEST_WORKER_START} + if Assigned(VisualGridBeforeWorkerStart) then VisualGridBeforeWorkerStart(Self); + {$ENDIF} + {$IFDEF FPC}Start;{$ENDIF} + except + // Failed native control preparation must not leave a pointer to the thread + // whose constructor is unwinding. Its inherited destructor joins it. + FGrid.FActiveThread:=nil; + FGrid.FFetchInProgress:=False; + if LTransferred then begin + FGrid.FDataTable:=FGrid.FCachedDataTable^; + Dispose(FGrid.FCachedDataTable); + FGrid.FCachedDataTable:=nil; + end; + FGrid.EndLoading; + FGrid.ControlsEnable(True); + raise; + end; end; { TCustomVisualGrid.TSearchEdit } @@ -810,8 +1027,7 @@ procedure TCustomVisualGrid.TSearchEdit.SetVisible(AValue: boolean); FPanel.Visible := AValue; end; -constructor TCustomVisualGrid.TSearchEdit.Create(AParent: TWinControl; - AGrid: TCustomVisualGrid); +constructor TCustomVisualGrid.TSearchEdit.Create(AParent: TWinControl; AGrid: TCustomVisualGrid); begin FGrid := AGrid; FPanel := TPanel.Create(nil); @@ -836,7 +1052,7 @@ destructor TCustomVisualGrid.TSearchEdit.Destroy; procedure TCustomVisualGrid.TSearchEdit.Clear; begin - FEdit.Clear; + FEdit.Text := ''; end; { TCustomVisualGrid } @@ -849,9 +1065,14 @@ procedure TCustomVisualGrid.ClickTest(Sender: TOBject); {$ENDIF} constructor TCustomVisualGrid.Create(Owner: TComponent); +var LMode: Integer; LItem: TMenuItem; begin inherited; + FFetchControlsEnabled := True; + FPageMode := vpmManual; + FManualPageSize := 100; + FMultiSearchEdits := TObjectList.Create; FColumns := TObjectList.Create; FFilters := TList.Create; @@ -860,7 +1081,7 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); { component layout } - ControlStyle := ControlStyle - [csAcceptsControls] + [csOwnedChildrenNotSelectable]; + ControlStyle := ControlStyle - [csAcceptsControls]{$IFDEF FPC} + [csOwnedChildrenNotSelectable]{$ENDIF}; FSearchKindPopupMenu := TPopupMenu.Create(Self); FSingleSearchMenuItem := TMenuItem.Create(Self); @@ -896,7 +1117,10 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); begin Width := 217; Height := 40; - Align := alRight; + Align := alNone; + // This layout owns right placement; a second right-anchor move during + // DPI scaling would offset the group after its bounds were calculated. + Anchors := [akTop, akLeft]; BevelOuter := bvNone; FPageCountLabel := TLabel.Create(Self); FPageCountLabel.Parent := FBottomRightPanel; @@ -966,19 +1190,23 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); Width := 56; Height := 21; + {$IFDEF FPC} AnchorSideLeft.Control := FButtonPrevious; AnchorSideLeft.Side := asrBottom; AnchorSideTop.Control := FBottomRightPanel; AnchorSideRight.Control := FPageCountLabel; AnchorSideBottom.Control := FBottomRightPanel; AnchorSideBottom.Side := asrBottom; - Anchors := [akLeft, akTop, akRight, akBottom]; + {$ENDIF} + Anchors := [akLeft, akTop]; + {$IFDEF FPC} BorderSpacing.Top := 10; BorderSpacing.Right := 2; BorderSpacing.Left := 2; BorderSpacing.Bottom := 8; + {$ENDIF} - OnEditingDone := PageIndexEditingDone; + {$IFDEF FPC}OnEditingDone{$ELSE}OnExit{$ENDIF} := PageIndexEditingDone; end; end; @@ -1018,19 +1246,74 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); Width := 52; Height := 21; + {$IFDEF FPC} AnchorSideLeft.Control := FPageSizeLabel; AnchorSideLeft.Side := asrBottom; AnchorSideTop.Control := FBottomCenterPanel; - AnchorSideBottom.Control := FBottomCenterPanel; - AnchorSideBottom.Side := asrBottom; - Anchors := [akLeft, akTop, akBottom]; + {$ENDIF} + Anchors := [akLeft, akTop]; + {$IFDEF FPC} BorderSpacing.Top := 10; BorderSpacing.Left := 2; BorderSpacing.Bottom := 8; + {$ENDIF} - OnEditingDone:=PageSizeEditingDone; + {$IFDEF FPC}OnEditingDone{$ELSE}OnExit{$ENDIF}:=PageSizeEditingDone; end; + FPageModeMenu := TPopupMenu.Create(Self); + FPageModeMenu.Name := 'PageModeMenu'; + FPageModeMenu.AutoPopup := False; + {$IFNDEF FPC}FPageModeMenu.AutoHotkeys := maManual;{$ENDIF} + for LMode := 0 to 2 do begin + LItem := TMenuItem.Create(FPageModeMenu); + // Component names stay fixed identifiers; captions may be translated. + case LMode of + 0: begin + LItem.Caption := sPagingModeSingle; + LItem.Name := 'PageModeSingle'; + end; + 1: begin + LItem.Caption := sPagingModeAuto; + LItem.Name := 'PageModeAuto'; + end; + 2: begin + LItem.Caption := sPagingModeManual; + LItem.Name := 'PageModeManual'; + end; + end; + LItem.Tag := LMode; + LItem.RadioItem := True; + LItem.GroupIndex := 1; + LItem.OnClick := PageModeChanged; + FPageModeMenu.Items.Add(LItem); + end; + FPageModeButton := TBitBtn.Create(Self); + FPageModeButton.Name := 'PageModeButton'; + FPageModeButton.Parent := FBottomRightPanel; + FPageModeButton.Caption := ''; + FPageModeButton.Spacing := 0; + FPageModeButton.Margin := -1; + FPageModeButton.ShowHint := True; + FPageModeButton.OnClick := PageModeButtonClick; + FPageModeButton.PopupMenu := FPageModeMenu; + {$IFDEF FPC}FPageModeButton.AutoSize := False;{$ENDIF} + FPageModeButton.TabOrder := 0; + FButtonFirst.TabOrder := 1; + FButtonPrevious.TabOrder := 2; + FPageIndexEdit.TabOrder := 3; + FButtonNext.TabOrder := 4; + FButtonLast.TabOrder := 5; + {$IFDEF FPC} + // The shared footer layout positions these controls on both widgetsets. + FPageSizeEdit.AnchorSideLeft.Control := nil; + FPageSizeEdit.AnchorSideTop.Control := nil; + FPageIndexEdit.AnchorSideLeft.Control := nil; + FPageIndexEdit.AnchorSideRight.Control := nil; + FPageIndexEdit.AnchorSideTop.Control := nil; + FPageIndexEdit.AnchorSideBottom.Control := nil; + {$ENDIF} end; + FBottomRightPanel.BringToFront; end; FTopPanel := TPanel.Create(Self); @@ -1045,16 +1328,21 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); FMultiSearchCheckComboBox.Parent := FTopPanel; with FMultiSearchCheckComboBox do begin + {$IFDEF FPC} AnchorSideTop.Control := FTopPanel; AnchorSideLeft.Control := FTopPanel; AnchorSideBottom.Control := FTopPanel; AnchorSideBottom.Side := asrBottom; + {$ENDIF} Anchors := [akLeft, akTop, akBottom]; + {$IFDEF FPC} BorderSpacing.Top := 6; BorderSpacing.Left := 4; BorderSpacing.Bottom := 6; + {$ENDIF} Width := 120; + {$IFNDEF FPC}SetBounds(4,6,120,24);{$ENDIF} OnItemChange:=MultiSearchCheckComboBoxChange; PopupMenu:=FSearchKindPopupMenu; Visible:=False; @@ -1085,20 +1373,29 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); //{$IFDEF WINDOWS} //FSearchButton.LoadGlyphFromResourceName(HINSTANCE, 'VISUALGRID_SEARCH'); //{$ELSE} + {$IFDEF FPC} FSearchButton.LoadGlyphFromLazarusResource('VISUALGRID_SEARCH'); + {$ELSE} + LoadSearchGlyph; + {$ENDIF} //{$ENDIF} with FSearchButton do begin + {$IFDEF FPC} AnchorSideTop.Control := FTopPanelRight; AnchorSideRight.Control := FTopPanelRight; AnchorSideRight.Side := asrBottom; AnchorSideBottom.Control := FTopPanelRight; AnchorSideBottom.Side := asrBottom; + {$ENDIF} Anchors := [akTop, akRight, akBottom]; + {$IFDEF FPC} BorderSpacing.Top := 6; BorderSpacing.Right := 4; BorderSpacing.Bottom := 6; + {$ENDIF} Width := 23; + {$IFNDEF FPC}SetBounds(127,6,23,24);{$ENDIF} PopupMenu:=FSearchKindPopupMenu; OnClick:=SearchButtonClick; end; @@ -1107,15 +1404,20 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); FSearchEdit.Parent := FTopPanelRight; with FSearchEdit do begin + {$IFDEF FPC} AnchorSideTop.Control := FTopPanelRight; AnchorSideRight.Control := FSearchButton; AnchorSideBottom.Control := FTopPanelRight; AnchorSideBottom.Side := asrBottom; + {$ENDIF} Anchors := [akTop, akRight, akBottom]; + {$IFDEF FPC} BorderSpacing.Top := 6; BorderSpacing.Right := 2; BorderSpacing.Bottom := 6; + {$ENDIF} Width := 121; + {$IFNDEF FPC}SetBounds(2,6,121,24);{$ENDIF} TextHint := sSearchExpression; PopupMenu := FSearchKindPopupMenu; end; @@ -1180,50 +1482,56 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); OnMouseUp := GridMouseUp; OnSelection := GridSelection; OnHeaderClick := GridHeaderClick; + OnResize := GridGeometryChanged; Options := (Options - [goRangeSelect]); FixedCols := 0; - ColCount:=0; - RowCount:=0; + {$IFDEF FPC}ColCount:=0;{$ELSE}ColCount:=1;{$ENDIF} + {$IFDEF FPC}RowCount:=0;{$ELSE}RowCount:=2;{$ENDIF} end; FDefaultDrawGridOptions := FDrawGrid.Options; end; FLoadDataPanel := TPanel.Create(Self); - FLoadDataPanel.Parent := Self; - with FLoadDataPanel do - begin - //BevelOuter := bvNone; - Width := 300; - Height := 150; - //Align:=alClient; - Color:=clWindow; - Visible:=false; - AnchorSideLeft.Control := Self; - AnchorSideLeft.Side := asrCenter; - AnchorSideTop.Control := Self; - AnchorSideTop.Side := asrCenter; - - FLoadDataLabel := TLabel.Create(Self); - FLoadDataLabel.Parent := FLoadDataPanel; - with FLoadDataLabel do - begin - AnchorSideLeft.Control := FLoadDataPanel; - AnchorSideLeft.Side := asrCenter; - AnchorSideTop.Control := FLoadDataPanel; - AnchorSideTop.Side := asrCenter; - Caption := sDataLoading; - end; - FLoadDataProgressLabel := TLabel.Create(Self); - FLoadDataProgressLabel.Parent := FLoadDataPanel; - with FLoadDataProgressLabel do - begin - AnchorSideLeft.Control := FLoadDataLabel; - AnchorSideLeft.Side := asrCenter; - AnchorSideTop.Control := FLoadDataLabel; - AnchorSideTop.Side := asrBottom; - Caption := '-'; - end - end; + FLoadDataPanel.Name := 'LoadingOverlay'; + FLoadDataPanel.Caption := ''; + FLoadDataPanel.Parent := FClientPanel; + FLoadDataPanel.BevelOuter := bvNone; + FLoadDataPanel.BorderStyle := bsSingle; + FLoadDataPanel.Color := clWindow; + FLoadDataPanel.DoubleBuffered := True; + FLoadDataPanel.Visible := False; + FLoadDataPanel.TabStop := False; + + FLoadingCircle := TLoadingCircle.Create(Self); + FLoadingCircle.Name := 'LoadingCircle'; + FLoadingCircle.Parent := FLoadDataPanel; + FLoadingCircle.Color := clWindow; + + FLoadDataLabel := TLabel.Create(Self); + FLoadDataLabel.Name := 'LoadingStatus'; + FLoadDataLabel.Parent := FLoadDataPanel; + FLoadDataLabel.AutoSize := False; + FLoadDataLabel.Font.Style := [fsBold]; + FLoadDataLabel.Caption := sDataLoading; + FLoadDataLabel.WordWrap := True; + FLoadDataLabel.Layout := tlCenter; + FLoadingSubtitle := TLabel.Create(Self); + FLoadingSubtitle.Name := 'LoadingSubtitle'; + FLoadingSubtitle.Parent := FLoadDataPanel; + FLoadingSubtitle.AutoSize := False; + FLoadingSubtitle.Font.Style := []; + FLoadingSubtitle.WordWrap := True; + FLoadingSubtitle.Visible := False; + FLoadingImage := TImage.Create(Self); + FLoadingImage.Name := 'LoadingImage'; + FLoadingImage.Parent := FLoadDataPanel; + FLoadingImage.Center := True; + FLoadingImage.Proportional := True; + FLoadingImage.Stretch := True; + FLoadingImage.Visible := False; + FLoadDataPanel.OnResize := LayoutLoadingPanel; + FClientPanel.OnResize := LayoutLoadingPanel; + LayoutLoadingPanel(FClientPanel); FDelayedBoundsChangeTimer := TTimer.Create(Self); with FDelayedBoundsChangeTimer do @@ -1233,12 +1541,15 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); OnTimer:=DelayedBoundsChange; end; - FFetchDataThreadTimer := TTimer.Create(Self); - with FFetchDataThreadTimer do + FFetchCompletion := TThreadNotify.Create(TThread.CurrentThread); + // A one-shot presentation delay preserves the nonflashing loading card. + // Fetch completion itself is queued by the worker, never polled by a timer. + FLoadingDelayTimer := TTimer.Create(Self); + with FLoadingDelayTimer do begin Enabled:=false; - Interval:=250; - OnTimer:=FetchDataThreadProgress; + Interval:=200; + OnTimer:=ShowDelayedLoading; end; FEditingDoneTimer := TTimer.Create(Self); @@ -1249,6 +1560,10 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); end; { default values for properties } + FRefreshThrottle := TThrottledEvent.Create(Self); + FRefreshThrottle.Add(RefreshThrottleDelivery); + FRefreshThrottle.OnWaitingChanged.Add(RefreshThrottleWaitingChanged); + FRefreshThrottle.OnBurstFinished.Add(RefreshThrottleFinished); FCellPadding := TRect.Create(CT_DEFAULT_CELL_PADDING_LEFT, CT_DEFAULT_CELL_PADDING_TOP, CT_DEFAULT_CELL_PADDING_RIGHT, CT_DEFAULT_CELL_PADDING_BOTTOM); DefaultStretchedColumn := -1; FSearchMode := smSingle; @@ -1257,6 +1572,7 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); FCanPage := true; FCanSearch := true; FTotalDataCount := -1; + UpdatePagingControls; {$IFDEF VISUALGRID_DEBUG} with TButton.Create(Self) do @@ -1279,6 +1595,25 @@ constructor TCustomVisualGrid.Create(Owner: TComponent); destructor TCustomVisualGrid.Destroy; begin + // Join the producer before disposing its queued UI dispatcher or borrowed data. + FShuttingDown := true; + FreeAndNil(FRefreshThrottle); + if Assigned(FLoadingDelayTimer) then FLoadingDelayTimer.Enabled := false; + if Assigned(FDelayedBoundsChangeTimer) then FDelayedBoundsChangeTimer.Enabled := false; + if Assigned(FActiveThread) then begin + FActiveThread.Terminate; + FJoiningFetch := True; + try FActiveThread.WaitFor; + finally FJoiningFetch := False; end; + FreeAndNil(FActiveThread); + end; + FreeAndNil(FFetchCompletion); // Cancels this receiver's queued adapters. + FFetchInProgress := False; + if Assigned(FCachedDataTable) then begin + Dispose(FCachedDataTable); + FCachedDataTable := nil; + end; + FDataSource := nil; FFilters.Free; FColumns.Free; FMultiSearchEdits.Free; @@ -1317,7 +1652,7 @@ procedure TCustomVisualGrid.DoDrawCell(Sender: TObject; ACol, ARow: Longint; Rec RIGHT_MARIGN = 4; var - LText: utf8string; + LText: String; LColumn: TVisualColumn; LHalfOfHeight: longint; LP1,LP2,LP3: TPoint; @@ -1328,7 +1663,9 @@ procedure TCustomVisualGrid.DoDrawCell(Sender: TObject; ACol, ARow: Longint; Rec LText := LColumn.Name; Rect := CalculateCellContentRect(Rect); - FDrawGrid.Canvas.TextRect(Rect, Rect.Left, Rect.Top, LText); + {$IFDEF FPC}FDrawGrid.Canvas.TextRect(Rect, Rect.Left, Rect.Top, LText);{$ELSE} + if LColumn.HasHeaderAlignment then DrawGridText(Canvas,Rect,string(LText),LColumn.HeaderAlignment) + else DrawGridText(Canvas,Rect,string(LText),taLeftJustify);{$ENDIF} FDrawGrid.Canvas.Pen.Width:=1; FDrawGrid.Canvas.Pen.Color:=clBlack; @@ -1347,8 +1684,7 @@ procedure TCustomVisualGrid.DoDrawCell(Sender: TObject; ACol, ARow: Longint; Rec Rect.Top + LHalfOfHeight + P3_Y + P3_Y_CENTER_DELTA); case LColumn.SortDirection of sdNone: ; - sdAscending: - FDrawGrid.Canvas.Polygon([LP1, LP2, LP3]); + sdAscending: FDrawGrid.Canvas.Polygon([LP1, LP2, LP3]); sdDescending: begin // swap y for points @@ -1360,7 +1696,10 @@ procedure TCustomVisualGrid.DoDrawCell(Sender: TObject; ACol, ARow: Longint; Rec end; end else begin - FDrawGrid.Canvas.TextRect(Rect, Rect.Left, Rect.Top, RowData); + {$IFDEF FPC}FDrawGrid.Canvas.TextRect(Rect, Rect.Left, Rect.Top, RowData);{$ELSE} + LColumn:=GetColumn(ACol); + if LColumn.HasDataAlignment then DrawGridText(Canvas,Rect,VarToStr(RowData),LColumn.DataAlignment) + else DrawGridText(Canvas,Rect,VarToStr(RowData),taLeftJustify);{$ENDIF} end; end; @@ -1370,11 +1709,36 @@ procedure TCustomVisualGrid.Loaded; ReloadColumns; end; -procedure TCustomVisualGrid.BoundsChanged; +{$IFDEF FPC}procedure TCustomVisualGrid.BoundsChanged;{$ELSE}procedure TCustomVisualGrid.Resize;{$ENDIF} +begin + {$IFDEF FPC}inherited BoundsChanged;{$ELSE}inherited Resize;{$ENDIF} + ScheduleBoundsChange; +end; + +procedure TCustomVisualGrid.GridGeometryChanged(Sender: TObject); +begin + if not FAutoPageSize or not FCanPage or not Assigned(Parent) then Exit; + // Horizontal scrollbars alter the native viewport without changing this + // composite's bounds. Refit through the same deferred layout admission. + ScheduleBoundsChange; +end; + +procedure TCustomVisualGrid.ScheduleBoundsChange; begin - inherited BoundsChanged; + if FShuttingDown then Exit; + if FInternalLayoutDepth = 0 then FBoundsVisualPending := True; + if Assigned(FRefreshThrottle) and (FInternalLayoutDepth = 0) then begin + FBoundsClientPending := True; + // A real resize is input now, even when page-capacity layout is deferred. + // The deferred geometry pass must not notify a second time for this input. + if FRefreshThrottle.Waiting then begin + FBoundsAlreadyNotified := True; + FRefreshReady := False; + FRefreshThrottle.Notify; + end; + end; // fix maximize form problem for AutoPageSize (new size of grid is not yet fully propagated) - FDelayedBoundsChangeTimer.Enabled:=true; + if Assigned(FDelayedBoundsChangeTimer) then FDelayedBoundsChangeTimer.Enabled:=true; end; procedure TCustomVisualGrid.SortDirectionGlyphRefresh; @@ -1382,9 +1746,10 @@ procedure TCustomVisualGrid.SortDirectionGlyphRefresh; procedure VisualColumnsSortDirection(AStopOnFirstSortable, AAllNone: boolean); var i: integer; - LStop: boolean = false; + LStop: boolean; LColumn: TVisualColumn; begin + LStop:=False; for i := 0 to FColumns.Count - 1 do try LColumn := FColumns[i]; @@ -1417,53 +1782,325 @@ procedure TCustomVisualGrid.SortDirectionGlyphRefresh; case FSortMode of smNone: begin - FDrawGrid.Options := FDrawGrid.Options - [goHeaderHotTracking, goHeaderPushedLook]; + FDrawGrid.Options := FDrawGrid.Options - [{$IFDEF FPC}goHeaderHotTracking,goHeaderPushedLook{$ELSE}goFixedHotTrack{$ENDIF}]; VisualColumnsSortDirection(false, true) end; smSingleColumn, smMultiColumn: begin - FDrawGrid.Options := FDrawGrid.Options + [goHeaderHotTracking, goHeaderPushedLook]; + FDrawGrid.Options := FDrawGrid.Options + [{$IFDEF FPC}goHeaderHotTracking,goHeaderPushedLook{$ELSE}goFixedHotTrack{$ENDIF}]; VisualColumnsSortDirection(FSortMode=smSingleColumn, false); end; end; RefreshPageIndexData(false); end; +{$IFNDEF FPC} +function TCustomVisualGrid.Scale96ToFont(AValue:Integer):Integer; +begin + Result:=ScaleValue(AValue); +end; +{$ENDIF} + +procedure TCustomVisualGrid.SetParent(AParent:TWinControl); +var i:Integer; +begin + inherited; + if Assigned(AParent) and Assigned(FFetchCompletion) and not FShuttingDown then begin + {$IFNDEF FPC} + // Apply column metrics deferred while the composite control had no parent. + for i:=0 to FColumns.Count-1 do FColumns[i].AttachGridColumn(FColumns[i].FColumn); + {$ENDIF} + RefreshPageIndexAndGridInterface; + ResetModeScrollPosition(not FRefreshPending and not FFetchInProgress); + for i:=0 to FMultiSearchEdits.Count-1 do ResizeSearchEdit(i); + LayoutLoadingPanel(Self); + RefreshLoadingPresentation; + end; +end; + +function TCustomVisualGrid.GetIsUpdating : Boolean; +begin + Result := FFetchInProgress; +end; + +procedure TCustomVisualGrid.LayoutLoadingPanel(Sender : TObject); +var + LMargin, LWidth, LHeight, LClientWidth, LClientHeight, + LCircleSize, LTextLeft : Integer; +begin + if not Assigned(FLoadDataPanel) then Exit; + {$IFNDEF FPC} + // VCL ClientRect creates the native window. A composite control's children + // must wait until its caller assigns a parent before requesting those HWNDs. + if not Assigned(Parent) then Exit; + {$ENDIF} + LMargin := Scale96ToFont(8); + if FManualLoading then LWidth := Scale96ToFont(320) else LWidth := Scale96ToFont(240); + if FLoadingSubtitle.Visible then LHeight := Scale96ToFont(80) else LHeight := Scale96ToFont(64); + LWidth := Min(LWidth,Max(0,FClientPanel.ClientWidth-2*LMargin)); + LHeight := Min(LHeight,Max(0,FClientPanel.ClientHeight-2*LMargin)); + FLoadDataPanel.SetBounds((FClientPanel.ClientWidth-LWidth) div 2, + (FClientPanel.ClientHeight-LHeight) div 2,LWidth,LHeight); + LClientWidth := FLoadDataPanel.ClientWidth; + LClientHeight := FLoadDataPanel.ClientHeight; + LCircleSize := Scale96ToFont(36); + FLoadingCircle.SetBounds(Scale96ToFont(10),(LClientHeight-LCircleSize) div 2, + LCircleSize,LCircleSize); + FLoadingImage.SetBounds(Scale96ToFont(10),(LClientHeight-LCircleSize) div 2, + LCircleSize,LCircleSize); + if FLoadingCircle.Visible or FLoadingImage.Visible then begin + LTextLeft := Scale96ToFont(56); + if FLoadingSubtitle.Visible then begin + FLoadDataLabel.SetBounds(LTextLeft,Scale96ToFont(8), + Max(0,LClientWidth-LTextLeft-Scale96ToFont(10)),Scale96ToFont(22)); + FLoadingSubtitle.SetBounds(LTextLeft,Scale96ToFont(32), + Max(0,LClientWidth-LTextLeft-Scale96ToFont(10)),Max(0,LClientHeight-Scale96ToFont(40))); + end else + FLoadDataLabel.SetBounds(LTextLeft,(LClientHeight-Scale96ToFont(22)) div 2, + Max(0,LClientWidth-LTextLeft-Scale96ToFont(10)),Scale96ToFont(22)); + end else + FLoadDataLabel.SetBounds(Scale96ToFont(12),Scale96ToFont(10), + Max(0,LClientWidth-Scale96ToFont(24)),Max(0,LClientHeight-Scale96ToFont(20))); +end; + +procedure TCustomVisualGrid.BringLoadingPanelToFront; +begin + {$IFDEF FPC} + FLoadDataPanel.BringToFront; + {$ELSE} + // VCL BringToFront activates a child window's top-level parent. Background + // refreshes must only raise the card within this grid, preserving the user's + // active modeless window and keyboard focus. + if FLoadDataPanel.HandleAllocated then + SetWindowPos(FLoadDataPanel.Handle, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE) + else + FLoadDataPanel.BringToFront; + {$ENDIF} +end; + +procedure TCustomVisualGrid.BeginLoading; +begin + // Queued throttle input is already outstanding work. Keep one presentation + // interval through admission and completion instead of hiding it while waiting. + if not FAutomaticLoading then FLoadingStartedAt := GetTickCount64; + FAutomaticLoading := True; + FLoadingError := ''; + FLoadingDelayTimer.Enabled := False; + FLoadingDelayTimer.Interval := 200; + FLoadingDelayTimer.Enabled := True; + if Length(FDataTable.Rows) > 0 then + FAutomaticLoadingTitle := sDataUpdating + else + FAutomaticLoadingTitle := sDataLoading; + RefreshLoadingPresentation; +end; + +procedure TCustomVisualGrid.UpdateLoading; +begin + RefreshLoadingPresentation; +end; + +procedure TCustomVisualGrid.ShowDelayedLoading(Sender: TObject); +begin + FLoadingDelayTimer.Enabled := False; + if not FShuttingDown and FAutomaticLoading then UpdateLoading; +end; + +procedure TCustomVisualGrid.EndLoading; +begin + FLoadingDelayTimer.Enabled := False; + // Accepted rows end their automatic card. Only newer client input retains + // it; a deferred internal capacity refit already has usable published data. + FAutomaticLoading := FRefreshPending and FRefreshClientPending and + not FRefreshSuspended and not FRefreshCancelled and not FShuttingDown; + if FAutomaticLoading and (GetTickCount64-FLoadingStartedAt < 200) then + FLoadingDelayTimer.Enabled := True; + FLoadingError := ''; + RefreshLoadingPresentation; +end; + +procedure TCustomVisualGrid.ShowLoadingError(const AMessage : String); +begin + FLoadingDelayTimer.Enabled := False; + FAutomaticLoading := False; + FLoadingError := AMessage; + RefreshLoadingPresentation; +end; + +procedure TCustomVisualGrid.ShowLoading(const ATitle: String; const ASubtitle: String; AImage: TGraphic; ADisableInput: Boolean); +begin + FLoadingImage.Picture.Assign(AImage); + FManualLoadingTitle := ATitle; + FManualLoadingSubtitle := ASubtitle; + FManualLoadingDisablesInput := ADisableInput; + FManualLoading := True; + ApplyControlsState; + RefreshLoadingPresentation; +end; + +procedure TCustomVisualGrid.HideLoading; +begin + if not FManualLoading then Exit; + FManualLoading := False; + FManualLoadingDisablesInput := False; + FLoadingImage.Picture.Assign(nil); + ApplyControlsState; + RefreshLoadingPresentation; +end; + +procedure TCustomVisualGrid.RefreshLoadingPresentation; +var + LShow, LImage, LCircle: Boolean; +begin + if not Assigned(FLoadDataPanel) then Exit; + // A manual request may precede parenting. Keep its state without causing + // VCL to create an HWND whose ancestor has no parent window yet. + if not Assigned(Parent) then begin + FLoadingCircle.Active := False; + Exit; + end; + LImage := FManualLoading and Assigned(FLoadingImage.Picture.Graphic) and + (not FLoadingImage.Picture.Graphic.Empty); + LCircle := FManualLoading or (FAutomaticLoading and (FLoadingError = '')); + LShow := FManualLoading or (FLoadingError <> '') or + (FAutomaticLoading and (GetTickCount64-FLoadingStartedAt >= 200) and + (FClientPanel.ClientWidth >= Scale96ToFont(100)) and + (FClientPanel.ClientHeight >= Scale96ToFont(40))); + FLoadingImage.Visible := LImage; + FLoadingCircle.Visible := LCircle and not LImage; + FLoadingCircle.Active := LShow and FLoadingCircle.Visible; + FLoadingSubtitle.Visible := FManualLoading and (FManualLoadingSubtitle <> ''); + FLoadingSubtitle.Caption := FManualLoadingSubtitle; + FLoadDataPanel.ShowHint := (not FManualLoading) and (FLoadingError <> ''); + FLoadDataPanel.Hint := FLoadingError; + if FManualLoading then begin + FLoadDataLabel.Caption := FManualLoadingTitle; + FLoadDataLabel.Font.Color := clWindowText; + end else if FLoadingError <> '' then begin + FLoadDataLabel.Caption := FLoadingError; + FLoadDataLabel.Font.Color := clRed; + end else begin + FLoadDataLabel.Caption := FAutomaticLoadingTitle; + FLoadDataLabel.Font.Color := clWindowText; + end; + LayoutLoadingPanel(Self); + FLoadDataPanel.Visible := LShow; + if LShow then BringLoadingPanelToFront; +end; + procedure TCustomVisualGrid.DelayedBoundsChange(Sender: TObject); +var LClientInput, LAlreadyNotified: Boolean; begin - // check data loading. try in next cycle - if FFetchDataThreadTimer.Enabled then + {$IFNDEF FPC} + if not Assigned(Parent) then begin FDelayedBoundsChangeTimer.Enabled:=False; Exit; end; + {$ENDIF} + // Coalesce visual reflow independently from data admission, including while + // a provider is busy. This pass never issues a source query. + if FBoundsVisualPending then begin + FBoundsVisualPending := False; + Inc(FInternalLayoutDepth); + try + UpdatePagingControls; + finally + Dec(FInternalLayoutDepth); + end; + end; + // Data admission can wait for a provider; native footer layout cannot. + if FFetchInProgress and (FRefreshThrottle.Mode = temNone) and not FRefreshSuspended then Exit; FDelayedBoundsChangeTimer.Enabled:=false; - if AutoPageSize then + LClientInput := FBoundsClientPending; + LAlreadyNotified := FBoundsAlreadyNotified; + FBoundsClientPending := False; + FBoundsAlreadyNotified := False; + Inc(FInternalLayoutDepth); + try + UpdatePagingControls; + finally + Dec(FInternalLayoutDepth); + end; + if AutoPageSize and FCanPage then begin // for vgoAutoHidePaging and AutoPageSize more space may be available // and paging panel may be hidden if (PageSize = ClientRowCount) and (vgoAutoHidePaging in FOptions) and FAutoPageSize then - RefreshPageIndexAndGridInterface + begin + Inc(FInternalLayoutDepth); + try RefreshPageIndexAndGridInterface; + finally Dec(FInternalLayoutDepth); end; + end else - PageSize := ClientRowCount; + ChangePageSize(ClientRowCount, LClientInput, LClientInput and not LAlreadyNotified); end; + ExecutePendingRefresh; end; -procedure TCustomVisualGrid.FetchDataThreadProgress(Sender: TObject); -{$J+} -const - PROGRESS: Integer = 0; -{$J-} - PROGRESS_CHARS: array[0..3] of string = ('-', '\', '|', '/'); +procedure TCustomVisualGrid.FetchDataThreadCompleted(Sender: TObject); +var + worker: TFetchDataThread; + disposed: TDisposables; +begin + if FShuttingDown or FJoiningFetch or not Assigned(FActiveThread) then Exit; + worker := TFetchDataThread(FActiveThread); + if InterlockedCompareExchange(worker.FResultReady, 0, 0) = 0 then Exit; + // Provider work has ended or was skipped before scheduling. There is no + // OnTerminate/Synchronize callback for WaitFor to wait on. Guard other queued + // wakes while WaitFor services the main-thread queue during a source change. + FJoiningFetch := True; + try worker.WaitFor; + finally FJoiningFetch := False; end; + disposed.AddObject(worker); + FActiveThread := nil; + if FActiveRefreshCancelled then begin + // A provider may finish normally after cancellation, or raise Abort while + // releasing its own locks. Neither outcome can replace the retained rows. + FDataTable := FCachedDataTable^; + Dispose(FCachedDataTable); + FCachedDataTable := nil; + FFetchInProgress := False; + FActiveRefreshSource := nil; + FActiveRefreshCancelled := False; + EndLoading; + ControlsEnable(True); + ExecutePendingRefresh; + Exit; + end; + FLastFetchDataResult := worker.FLastFetchDataResult; + FLastFetchDataResult.FromThread := true; + FIgnoreRecalcPageCount := worker.FIgnoreRecalcPageCount; + if worker.FError <> '' then begin + FDataTable := FCachedDataTable^; + FLastFetchDataResult.FromThread := false; + Dispose(FCachedDataTable); + FCachedDataTable := nil; + FFetchInProgress := false; + FActiveRefreshSource := nil; + ShowLoadingError(worker.FError); + ControlsEnable(true); + end else + AfterFetchPage; + if FRefreshPending then ExecutePendingRefresh; +end; + +{$IFNDEF FPC} +procedure TCustomVisualGrid.LoadSearchGlyph; +var LStream:TResourceStream; LPng:TPngImage; begin - FLoadDataProgressLabel.Caption := PROGRESS_CHARS[PROGRESS]; - Inc(PROGRESS); - if PROGRESS > High(PROGRESS_CHARS) then - PROGRESS := 0; + LStream:=TResourceStream.Create(HInstance,'VISUALGRID_SEARCH',RT_RCDATA); + try + LPng:=TPngImage.Create; + try LPng.LoadFromStream(LStream); FSearchButton.Glyph.Assign(LPng); + finally LPng.Free; end; + finally LStream.Free; end; end; +{$ENDIF} procedure TCustomVisualGrid.SearchButtonClick(Sender: TObject); var LFormat: TFormatSettings; LNewStrFilter: utf8string; - LException: boolean = false; + LException: boolean; LNum : SizeInt; LDataFilter : TDataFilter; e: TSearchEdit; @@ -1473,10 +2110,10 @@ procedure TCustomVisualGrid.SearchButtonClick(Sender: TObject); LColumn: TVisualColumn; LAccepted: TDataFilters; LColumnFilter: TColumnFilter; - LCandidates: TDataFilters = []; + LCandidates: TDataFilters; LExpressionRecord: TExpressionRecord; - procedure AddFilter( const ABinding : AnsiString; const AFilters: TDataFilters); + procedure AddFilter( const ABinding : String; const AFilters: TDataFilters); var LFilter: TDataFilter; begin @@ -1521,6 +2158,7 @@ procedure TCustomVisualGrid.SearchButtonClick(Sender: TObject); end; begin + LCandidates:=[]; try LExpressionRecord := TSearchExpressionService.Parse(AExpression); except @@ -1531,10 +2169,9 @@ procedure TCustomVisualGrid.SearchButtonClick(Sender: TObject); with LExpressionRecord do begin case Kind of - ekUnknown: - Exit; + ekUnknown: Exit; ekNum: - case NumericComparisionKind of + case NumericComparisonKind of // nckUnknown is special case - pure number can be text and num nckUnknown: LCandidates := [vgfNumericEQ, vgfMatchTextExact]; nckNumericEQ: LCandidates := [vgfNumericEQ]; @@ -1580,8 +2217,9 @@ procedure TCustomVisualGrid.SearchButtonClick(Sender: TObject); if not Assigned(FDataSource) then exit; + LException:=False; FFilters.Clear; - LFormat := DefaultFormatSettings; + LFormat := {$IFDEF FPC}DefaultFormatSettings{$ELSE}TFormatSettings.Create{$ENDIF}; LFormat.DecimalSeparator:='.'; try AddExpression(FSearchEdit.Text, nil); @@ -1605,17 +2243,33 @@ procedure TCustomVisualGrid.SearchButtonClick(Sender: TObject); end; procedure TCustomVisualGrid.ControlsEnable(AEnable: boolean); +begin + FFetchControlsEnabled := AEnable; + ApplyControlsState; +end; + +procedure TCustomVisualGrid.ApplyControlsState; var e: TSearchEdit; - LReadOnly: boolean; -begin - LReadOnly:=not AEnable; - FMultiSearchCheckComboBox.Enabled:=AEnable; + I: Integer; + LReadOnly, LManualInputAllowed, LInputAllowed: boolean; +begin + LManualInputAllowed := not (FManualLoading and FManualLoadingDisablesInput); + LInputAllowed := FFetchControlsEnabled and LManualInputAllowed; + LReadOnly:=not LInputAllowed; + // The card's parent remains enabled. Gate complete input regions for a + // manual hold without changing ordinary automatic-fetch interaction. + FTopPanel.Enabled := LManualInputAllowed; + FTopPanelMultiSearch.Enabled := LManualInputAllowed; + FBottomPanel.Enabled := LManualInputAllowed; + FMultiSearchCheckComboBox.Enabled:=LInputAllowed; FSearchEdit.ReadOnly:=LReadOnly; FPageSizeEdit.ReadOnly:=LReadOnly; FPageIndexEdit.ReadOnly:=LReadOnly; - FDrawGrid.Enabled:=AEnable; - FBottomRightPanel.Enabled:=AEnable; + FDrawGrid.Enabled:=LInputAllowed; + FBottomRightPanel.Enabled:=LInputAllowed; + FPageModeButton.Enabled:=LInputAllowed; + for I := 0 to FPageModeMenu.Items.Count-1 do FPageModeMenu.Items[I].Enabled := LInputAllowed; for e in FMultiSearchEdits do e.FEdit.ReadOnly:=LReadOnly; end; @@ -1637,7 +2291,7 @@ function TCustomVisualGrid.GetColumn(Index: Integer): TVisualColumn; function TCustomVisualGrid.GetActiveDataTable: PDataTable; begin - if FFetchDataThreadTimer.Enabled then + if FFetchInProgress and Assigned(FCachedDataTable) then Result := FCachedDataTable else Result := @FDataTable; @@ -1672,13 +2326,21 @@ function TCustomVisualGrid.GetSelectedRows : TArray; sel : TVisualGridSelection; selectedRows : TList; GC: TDisposables; - row : Integer; + row, rangeIndex: Integer; + selected: TArray; begin + if FCurrentSelectionType = stNone then Exit(nil); sel := GetSelection; selectedRows := GC.AddObject( TList.Create ) as TList; - if sel.RowCount > 0 then - for row := sel.Row to (sel.Row + sel.RowCount - 1) do - selectedRows.Add(Self.Rows[row]); + // Multi-selection can contain disjoint or overlapping grid rectangles. + // Return each selected data row once, in its displayed order. + SetLength(selected, RowCount); + for rangeIndex := 0 to High(sel.Selections) do + for row := Max(0,sel.Selections[rangeIndex].Top) to + Min(High(selected),sel.Selections[rangeIndex].Bottom) do + selected[row] := True; + for row := 0 to High(selected) do + if selected[row] then selectedRows.Add(Self.Rows[row]); result := selectedRows.ToArray; end; @@ -1719,6 +2381,76 @@ procedure TCustomVisualGrid.PageSizeEditingDone(Sender: TObject); FIgnoreRecalcPageCount := false; end; +function TCustomVisualGrid.CanChangePageMode: Boolean; +var LControl: TControl; +begin + Result := False; + if not FCanPage or not Assigned(Parent) then Exit; + LControl := FPageModeButton; + while Assigned(LControl) do begin + if not LControl.Enabled then Exit; + LControl := LControl.Parent; + end; + Result := True; +end; + +procedure TCustomVisualGrid.PageModeButtonClick(Sender: TObject); +var LPoint: TPoint; +begin + if not CanChangePageMode then Exit; + LPoint := FPageModeButton.ClientToScreen(Point(0,FPageModeButton.Height)); + FPageModeMenu.PopupComponent := FPageModeButton; + FPageModeMenu.PopUp(LPoint.X,LPoint.Y); +end; + +procedure TCustomVisualGrid.PageModeChanged(Sender: TObject); +begin + // A menu can outlive the input state in which it opened (fetch/manual hold). + // Keep its command subject to the same enabled parent chain as the button. + if not CanChangePageMode or not (Sender is TMenuItem) then Exit; + if TMenuItem(Sender).Tag in [Ord(Low(TVisualGridPageMode))..Ord(High(TVisualGridPageMode))] then + PageMode := TVisualGridPageMode(TMenuItem(Sender).Tag); +end; + +procedure TCustomVisualGrid.UpdatePageModeGlyph; +var LBitmap: {$IFDEF FPC}Graphics.TBitmap{$ELSE}Vcl.Graphics.TBitmap{$ENDIF}; LSize: Integer; + + function P(AValue: Integer): Integer; + begin + Result := (AValue*LSize+8) div 16; + end; + +begin + LSize := Max(12,Min(FPageModeButton.Width,FPageModeButton.Height)-Scale96ToFont(8)); + if (FPageModeButton.Glyph.Width = LSize) and (FPageModeButton.Glyph.Height = LSize) then Exit; + LBitmap := {$IFDEF FPC}Graphics.TBitmap{$ELSE}Vcl.Graphics.TBitmap{$ENDIF}.Create; + try + LBitmap.PixelFormat := pf24bit; + LBitmap.Width := LSize; + LBitmap.Height := LSize; + LBitmap.Canvas.Brush.Color := clFuchsia; + LBitmap.Canvas.FillRect(Types.Rect(0,0,LSize,LSize)); + LBitmap.Canvas.Brush.Style := bsClear; + LBitmap.Canvas.Pen.Color := clBtnText; + LBitmap.Canvas.Rectangle(P(1),P(1),P(12),P(12)); + LBitmap.Canvas.MoveTo(P(1),P(5)); + LBitmap.Canvas.LineTo(P(12),P(5)); + LBitmap.Canvas.MoveTo(P(1),P(8)); + LBitmap.Canvas.LineTo(P(12),P(8)); + LBitmap.Canvas.MoveTo(P(5),P(1)); + LBitmap.Canvas.LineTo(P(5),P(12)); + LBitmap.Canvas.Brush.Style := bsSolid; + LBitmap.Canvas.Brush.Color := clBtnText; + LBitmap.Canvas.Polygon([Point(P(9),P(12)),Point(P(15),P(12)),Point(P(12),P(15))]); + LBitmap.TransparentColor := clFuchsia; + LBitmap.Transparent := True; + FPageModeButton.Glyph.Assign(LBitmap); + FPageModeButton.NumGlyphs := 1; + finally + LBitmap.Free; + end; +end; + procedure TCustomVisualGrid.PageNavigationClick(Sender: TObject); begin if FPageCount = 0 then @@ -1733,7 +2465,7 @@ procedure TCustomVisualGrid.PageNavigationClick(Sender: TObject); procedure TCustomVisualGrid.MultiSearchCheckComboBoxChange(Sender: TObject; AIndex: Integer); var - LState: TCheckComboItemState; + {$IFDEF FPC}LState: TCheckComboItemState;{$ENDIF} LOldHasEdit, LNewHasEdit: boolean; function HasOneOrMoreEdit: boolean; @@ -1748,11 +2480,15 @@ procedure TCustomVisualGrid.MultiSearchCheckComboBoxChange(Sender: TObject; AInd begin LOldHasEdit := HasOneOrMoreEdit; + {$IFDEF FPC} LState := TCheckComboItemState(FMultiSearchCheckComboBox.Items.Objects[AIndex]); - if Assigned(LState.Data) then - TSearchEdit(LState.Data).EditVisible:=LState.State=cbChecked - else - FSearchEdit.Visible:=LState.State=cbChecked; + if Assigned(LState.Data) then TSearchEdit(LState.Data).EditVisible:=LState.State=cbChecked + else FSearchEdit.Visible:=LState.State=cbChecked; + {$ELSE} + if Assigned(FMultiSearchCheckComboBox.Objects[AIndex]) then + TSearchEdit(FMultiSearchCheckComboBox.Objects[AIndex]).EditVisible:=FMultiSearchCheckComboBox.Checked[AIndex] + else FSearchEdit.Visible:=FMultiSearchCheckComboBox.Checked[AIndex]; + {$ENDIF} LNewHasEdit := HasOneOrMoreEdit; FTopPanelMultiSearch.Visible := LNewHasEdit; @@ -1769,7 +2505,7 @@ function TCustomVisualGrid.GetCanvas: TCanvas; end; procedure TCustomVisualGrid.SetCells(ACol, ARow: Integer; AValue: Variant); -var LBinding : AnsiString; LDataColIndex : Integer; LRow : TDataRowData; +var LBinding : String; LDataColIndex : Integer; LRow : TDataRowData; begin LRow := TDataRowData(FDataTable.Rows[ARow]); LBinding := FColumns[ACol].Binding; @@ -1811,7 +2547,9 @@ procedure TCustomVisualGrid.SetSearchMode(AValue: TSearchMode); // Clear all filters for i := 0 to FMultiSearchEdits.Count - 1 do FMultiSearchEdits[i].Clear; - FSearchEdit.Clear; + // VCL Clear forces a HWND; Text also retains the intended empty value while + // a composite grid is configured before its native parent is assigned. + FSearchEdit.Text := ''; FSearchMode := AValue; @@ -1857,39 +2595,67 @@ procedure TCustomVisualGrid.SetRows(ARow: Integer; AValue: Variant); procedure TCustomVisualGrid.SetShowAllData(AValue: boolean); begin - if FShowAllData=AValue then - Exit; - - FShowAllData:=AValue; - if FShowAllData then - AutoPageSize:=false; - - HidePageSizeControls(not AValue); - HidePageNavigationControls(not AValue); - PageSize:=FTotalDataCount; + if AValue then PageMode := vpmSingle + else if FPageMode = vpmSingle then PageMode := vpmManual; end; procedure TCustomVisualGrid.SetAutoPageSize(AValue: boolean); +begin + if AValue then PageMode := vpmAuto + else if FPageMode = vpmAuto then PageMode := vpmManual; +end; +procedure TCustomVisualGrid.ResetModeScrollPosition(AClearPending: Boolean); begin - if FAutoPageSize=AValue then + if not FModeScrollResetPending or not Assigned(Parent) then Exit; + // A paging mode starts at a new viewport origin. Reapply after publication: + // native row clamping and retained selection can move the scrollbars again. + if FDrawGrid.RowCount > FDrawGrid.FixedRows then + FDrawGrid.TopRow := FDrawGrid.FixedRows; + if FDrawGrid.ColCount > FDrawGrid.FixedCols then + FDrawGrid.LeftCol := FDrawGrid.FixedCols; + if AClearPending and not FRefreshPending then + FModeScrollResetPending := False; +end; + +procedure TCustomVisualGrid.SetPageMode(AValue: TVisualGridPageMode); +begin + if FPageMode = AValue then Exit; + FModeScrollResetPending := True; + FPageMode := AValue; + FShowAllData := AValue = vpmSingle; + FAutoPageSize := AValue = vpmAuto; + // A mode change supersedes an in-flight slice without joining the provider + // on the UI thread. Completion restores the previous rows until the latest + // retained request is admitted by this grid's existing refresh policy. + CancelActiveFetch; + FPageIndex := 0; + if not FAutoPageSize then FPageSize := FManualPageSize; + Inc(FInternalLayoutDepth); + try + UpdatePagingControls; + if FAutoPageSize and FCanPage and Assigned(Parent) then + FPageSize := ClientRowCount; + finally + Dec(FInternalLayoutDepth); + end; + SetPageSizeEditText(IntToStr(FPageSize)); + ResetModeScrollPosition(False); + RefreshPageIndexData(False); +end; - FAutoPageSize:=AValue; - if FAutoPageSize then - ShowAllData:=false; - - HidePageSizeControls(not FAutoPageSize); - - if FAutoPageSize then - FDrawGrid.ScrollBars:=ssNone - else - FDrawGrid.ScrollBars:=ssAutoBoth; - - PageSize := ClientRowCount; - - // for proper handling of vgoAutoHidePaging we need to refresh - RefreshPageIndexAndGridInterface; +procedure TCustomVisualGrid.SetManualPageSize(AValue: Integer); +begin + if AValue <= 0 then Exit; + AValue := Min(AValue,1000000); + if FManualPageSize = AValue then Exit; + FManualPageSize := AValue; + if FPageMode = vpmManual then ChangePageSize(AValue,True,True) + else if FPageMode = vpmSingle then begin + FPageSize := AValue; + SetPageSizeEditText(IntToStr(AValue)); + end; end; procedure TCustomVisualGrid.SetCanPage(AValue: boolean); @@ -1898,6 +2664,16 @@ procedure TCustomVisualGrid.SetCanPage(AValue: boolean); Exit; FCanPage:=AValue; + CancelActiveFetch; + FPageIndex := 0; + Inc(FInternalLayoutDepth); + try + UpdatePagingControls; + if FAutoPageSize and FCanPage and Assigned(Parent) then + FPageSize := ClientRowCount; + finally + Dec(FInternalLayoutDepth); + end; FBottomPanel.Visible:=FCanPage; if csDesigning in ComponentState then if FBottomPanel.Visible then @@ -1906,6 +2682,7 @@ procedure TCustomVisualGrid.SetCanPage(AValue: boolean); FBottomPanel.ControlStyle := FBottomPanel.ControlStyle + [csNoDesignVisible]; LayoutChanged; + RefreshPageIndexData(False); end; procedure TCustomVisualGrid.SetCanSearch(AValue: boolean); @@ -1929,21 +2706,165 @@ procedure TCustomVisualGrid.RefreshGrid; RefreshPageIndexData( False ); end; +procedure TCustomVisualGrid.CancelActiveFetch; +begin + if not Assigned(FActiveThread) then + Exit; + // Terminate is a cooperative request. Completion owns joining and restoring + // the retained page, including when a provider ignores cancellation briefly. + FActiveRefreshCancelled := True; + FActiveThread.Terminate; +end; + +procedure TCustomVisualGrid.SetRefreshSuspended(AValue: Boolean); +begin + if FRefreshSuspended = AValue then Exit; + if FShuttingDown then Exit; + if AValue then begin + FRefreshSuspended := True; + // The caller owns synchronization artwork while ordinary refreshes pause. + EndLoading; + // Keep newer queued input intact. With no queued input, retain the active + // page intent so resuming also refreshes a view which received no events. + if not FRefreshPending then begin + FPendingRefreshParams := CaptureRefreshParams; + FPendingRefreshSource := FDataSource; + FRefreshPending := True; + FRefreshClientPending := True; + end; + if Assigned(FActiveThread) then begin + FRefreshColumnsPending := FRefreshColumnsPending or + TFetchDataThread(FActiveThread).FLastFetchDataResult.RefreshColumns; + // This flag belongs to the active request, not to the pause state: an + // intervening resume must never make its cancelled result valid again. + CancelActiveFetch; + end; + end else begin + // Apply a queued real resize before admitting the one latest page. Layout + // remains visual while the suspension guard is still in force. + if FDelayedBoundsChangeTimer.Enabled then DelayedBoundsChange(Self); + FRefreshSuspended := False; + FRefreshCancelled := False; + if FRefreshPending then FRefreshReady := True; + ExecutePendingRefresh; + end; +end; + procedure TCustomVisualGrid.RefreshPageIndexData(ARefreshColumns: boolean); begin - if Assigned(FDataSource) and FetchDataInThread then - begin - if not Assigned(FActiveThread) then - TFetchDataThread.Create(Self, ARefreshColumns) - end - else - begin - BeforeFetchPage; - FLastFetchDataResult.FromThread:=false; - FLastFetchDataResult.RefreshColumns:=ARefreshColumns; - FetchPage(FLastFetchDataResult.FetchResult); - AfterFetchPage; + QueueRefresh(ARefreshColumns, True, True); +end; + +procedure TCustomVisualGrid.QueueRefresh(ARefreshColumns, AClientInput, ANotify: Boolean); +begin + if FShuttingDown then exit; + // Old result layout cannot replace newer client page/filter/source intent. + if not AClientInput and FRefreshCancelled then Exit; + if not AClientInput and FRefreshClientPending then Exit; + if AClientInput then FRefreshCancelled := False; + FRefreshPending := True; + FRefreshColumnsPending := FRefreshColumnsPending or ARefreshColumns; + FRefreshClientPending := AClientInput; + FPendingRefreshParams := CaptureRefreshParams; + FPendingRefreshSource := FDataSource; + FRefreshReady := not AClientInput; + // A throttled selection/page change must show that its retained rows are + // awaiting replacement even before a provider worker may start. + if AClientInput and not FRefreshSuspended and Assigned(FDataSource) then + BeginLoading; + if ANotify then FRefreshThrottle.Notify; +end; + +procedure TCustomVisualGrid.RefreshThrottleDelivery(Sender: TObject); +begin + if FShuttingDown then Exit; + FRefreshReady := True; + ExecutePendingRefresh(True); +end; + +procedure TCustomVisualGrid.RefreshThrottleWaitingChanged(Sender: TObject); +begin + if FShuttingDown then Exit; + // Cancel and mode changes also end Waiting. They cannot admit page work or + // allow an older worker's result layout to restart a cancelled request. + if not FRefreshThrottle.Waiting then begin + FRefreshReady := False; + FRefreshCancelled := True; + if not FFetchInProgress then EndLoading; + end; +end; + +procedure TCustomVisualGrid.RefreshThrottleFinished(Sender: TObject); +begin + if FShuttingDown then Exit; + // Only the owned timer's genuine quiet finish releases deferred single-input + // work. Explicit Cancel/mode changes do not publish this lifecycle event. + FRefreshCancelled := False; + if FRefreshPending then begin + FRefreshReady := True; + if FRefreshClientPending and not FRefreshSuspended and + Assigned(FPendingRefreshSource) then begin + // WaitingChanged must hide cancelled work immediately. A genuine quiet + // finish restores this request's existing presentation interval before + // admission, so a deferred leading request does not flash another delay. + FAutomaticLoading := True; + RefreshLoadingPresentation; + end; + end; + ExecutePendingRefresh; +end; + +procedure TCustomVisualGrid.ExecutePendingRefresh(AOrdinaryAdmission: Boolean); +var + LRefreshColumns: Boolean; + LPreviousDataTable: TDataTable; +begin + if FShuttingDown or FRefreshSuspended or FExecutingRefresh or (FInternalLayoutDepth > 0) or + Assigned(FActiveThread) or not FRefreshPending or not FRefreshReady then Exit; + // Only the ordinary admission itself can lead while Waiting. Worker and + // layout replay cannot bypass the latest request's quiet interval. + if not AOrdinaryAdmission and FRefreshThrottle.Waiting then Exit; + FExecutingRefresh := True; + try + FActiveRefreshParams := FPendingRefreshParams; + FActiveRefreshSource := FPendingRefreshSource; + FActiveRefreshCancelled := False; + LRefreshColumns := FRefreshColumnsPending; + FRefreshPending := False; + FRefreshColumnsPending := False; + FRefreshClientPending := False; + FRefreshReady := False; + FPendingRefreshSource := nil; + FPageIndex := FActiveRefreshParams.PageIndex; + if FActiveRefreshParams.PageSize > 0 then + FPageSize := FActiveRefreshParams.PageSize; + SetPageSizeEditText(IntToStr(FPageSize)); + if Assigned(FActiveRefreshSource) and FetchDataInThread then + TFetchDataThread.Create(Self, LRefreshColumns) + else begin + LPreviousDataTable := FDataTable; + try + BeforeFetchPage; + FDataTable := Default(TDataTable); + FLastFetchDataResult.FromThread := False; + FLastFetchDataResult.RefreshColumns := LRefreshColumns; + FetchPage(FLastFetchDataResult.FetchResult); + except + on E: Exception do begin + // A synchronous provider can fail after replacing its output table. + // Retain the accepted page and end loading before preserving the error. + FDataTable := LPreviousDataTable; + ShowLoadingError(E.Message); + raise; + end; + end; + AfterFetchPage; + end; + finally + FExecutingRefresh := False; + if not Assigned(FActiveThread) then FActiveRefreshSource := nil; end; + if not Assigned(FActiveThread) then ExecutePendingRefresh; end; procedure TCustomVisualGrid.ResizeSearchEdit(ACol: Integer); @@ -1953,6 +2874,9 @@ procedure TCustomVisualGrid.ResizeSearchEdit(ACol: Integer); //LFixedRect: TRect; LRect: TRect; begin + // Data may be assigned before a native parent exists. CellRect creates VCL + // handles, so replay this geometry from SetParent instead of forcing them. + if not Assigned(Parent) then Exit; if ACol > FMultiSearchEdits.Count - 1 then exit; LEdit := FMultiSearchEdits[ACol]; @@ -1967,10 +2891,10 @@ procedure TCustomVisualGrid.ResizeSearchEdit(ACol: Integer); else LEditOnRight := nil; - if ACol = TDrawGridAccess(FDrawGrid).GCache.VisibleGrid.Right then + if ACol = {$IFDEF FPC}TDrawGridAccess(FDrawGrid).GCache.VisibleGrid.Right{$ELSE}(FDrawGrid.LeftCol+FDrawGrid.VisibleColCount-1){$ENDIF} then if Assigned(LEditOnRight) then LEditOnRight.Visible:=false; - if ACol = TDrawGridAccess(FDrawGrid).GCache.VisibleGrid.Left then + if ACol = {$IFDEF FPC}TDrawGridAccess(FDrawGrid).GCache.VisibleGrid.Left{$ELSE}FDrawGrid.LeftCol{$ENDIF} then begin if Assigned(LEditOnLeft) then if (ACol > FDrawGrid.FixedCols) or (not FDrawGrid.IsFixedCellVisible(ACol-1,0)) then @@ -1990,8 +2914,7 @@ procedure TCustomVisualGrid.ResizeSearchEdit(ACol: Integer); procedure TCustomVisualGrid.RefreshPageIndexAndGridInterface; var - LCountOnPage2: Integer; - LGridUnusedHeight, LPotentialHeight: integer; + LPreviousFooterHeight: Integer; LWasVisible: boolean; LHasFilter: boolean; @@ -1999,25 +2922,26 @@ procedure TCustomVisualGrid.RefreshPageIndexAndGridInterface; var LCount: Integer; begin - if FIgnoreRecalcPageCount then + if FIgnoreRecalcPageCount or not FAutoPageSize or not FCanPage then Exit(false); LCount := ClientRowCount; Result := LCount <> PageSize; if Result then - PageSize := LCount; + ChangePageSize(LCount, FInternalLayoutDepth = 0, FInternalLayoutDepth = 0); end; function FindProperFilter: boolean; var - f: ^TColumnFilter; + f: TColumnFilter; begin - for f in FFilters.Ptr^ do + for f in FFilters do if f.Filter <> vgfSortable then Exit(true); Result := false; end; begin + {$IFNDEF FPC}if not Assigned(Parent) then Exit;{$ENDIF} // if filter is active then vgoAutoHideSearchPanel should be not considered. // Note: vgfSortable is not considered as 'real' filter LHasFilter := (FFilters.Count > 0) and FindProperFilter; @@ -2025,54 +2949,11 @@ procedure TCustomVisualGrid.RefreshPageIndexAndGridInterface; SetPageIndexEditText(IntToStr(Succ(FPageIndex))); FPageCountLabel.Caption := Format('/%d',[FPageCount]); - if not FFetchDataThreadTimer.Enabled then - if (vgoAutoHidePaging in FOptions) and (FPageCount in [0,1,2]) and (FPageIndex = 0) then - case FPageCount of - // simple situation - just hide bottom panel - 0,1: - FBottomPanel.Visible := False; - // for PageCount = 2 may be also possible but more complicated especially for AutoPageSize - 2: - if AutoPageSize then - begin - LCountOnPage2 := FTotalDataCount - ClientRowCount; - LGridUnusedHeight := FDrawGrid.ClientHeight - FDrawGrid.GridHeight; - LPotentialHeight := FBottomPanel.Height + LGridUnusedHeight; - if (vgoAutoHideSearchPanel in FOptions) and not LHasFilter then - begin - // vgoAutoHideSearchPanel + AutoPageSize + vgoAutoHidePaging is killer combo :P - if FTopPanel.Visible and not Assigned(FWidgetControl) then - LPotentialHeight := LPotentialHeight + FTopPanel.Height; - // the above killer combo is not all :D - there is also FTopPanelMultiSearch - if FTopPanelMultiSearch.Visible then - LPotentialHeight := LPotentialHeight + FTopPanelMultiSearch.Height; - end; - - if FBottomPanel.Visible - and (FDrawGrid.DefaultRowHeight * LCountOnPage2 <= LPotentialHeight) then - begin - FBottomPanel.Visible := False; - PageSize := PageSize + LCountOnPage2; - Exit; - end else - begin - if FCanPage then - begin - FBottomPanel.Visible := true; - if RecalcPageCount then - Exit; - end; - end; - end else - FBottomPanel.Visible := FCanPage - end - else - begin - LWasVisible := FBottomPanel.Visible; - FBottomPanel.Visible := FCanPage; - if (FTopPanel.Visible <> LWasVisible) and RecalcPageCount then - Exit; - end; + LWasVisible := FBottomPanel.Visible; + LPreviousFooterHeight := FBottomPanel.Height; + UpdatePagingControls; + if ((FBottomPanel.Visible <> LWasVisible) or + (FBottomPanel.Height <> LPreviousFooterHeight)) and RecalcPageCount then Exit; // show or hide Search Panel (related to option vgoAutoHideSearchPanel) if (vgoAutoHideSearchPanel in FOptions) and (FPageCount in [0,1]) and not LHasFilter then @@ -2099,11 +2980,9 @@ procedure TCustomVisualGrid.RefreshPageIndexAndGridInterface; end; FDrawGrid.Refresh; - if Assigned(FOnFinishedUpdating) then - FOnFinishedUpdating(Self); end; -function TCustomVisualGrid.AddColumn(const AName: utf8string) : TVisualColumn; +function TCustomVisualGrid.AddColumn(const AName: String) : TVisualColumn; begin Result := TVisualColumn.Create(Self); Result.Index := FColumns.Count; @@ -2119,6 +2998,7 @@ procedure TCustomVisualGrid.ReloadColumns; i: Integer; LEdit: TSearchEdit; LColumn: TVisualColumn; + LWasIgnoringRefresh: Boolean; begin FSortColumn := nil; FDrawGrid.Columns.Clear; // clear TDrawGrid columns @@ -2131,6 +3011,7 @@ procedure TCustomVisualGrid.ReloadColumns; FOnColumnInitialize(Self, LColumn); end; FDrawGrid.Columns.EndUpdate; + {$IFNDEF FPC}FDrawGrid.FitColumns;{$ENDIF} // TODO: may be optimized FMultiSearchEdits.Clear; FSearchButton.Visible := true; @@ -2147,8 +3028,15 @@ procedure TCustomVisualGrid.ReloadColumns; FMultiSearchCheckComboBox.Objects[FMultiSearchCheckComboBox.Items.Count-1] := LEdit; FMultiSearchCheckComboBox.Checked[FMultiSearchCheckComboBox.Items.Count-1] := FMultiSearchMenuItem.Checked; end; - if (vgfSortable in FColumns[i].Filters) and not (vgoSortDirectionAllowNone in Options) and (SortMode <> smNone) then - FColumns[i].SortDirection := sdAscending; + if (vgfSortable in FColumns[i].Filters) and not (vgoSortDirectionAllowNone in Options) and + (SortMode <> smNone) and (FColumns[i].SortDirection = sdNone) then begin + // This default belongs to the admitted column population, not a user + // sort request. Explicit consumer callbacks above retain normal admission. + LWasIgnoringRefresh := FColumns[i].FIgnoreRefresh; + FColumns[i].FIgnoreRefresh := True; + try FColumns[i].SortDirection := sdAscending; + finally FColumns[i].FIgnoreRefresh := LWasIgnoringRefresh; end; + end; end; if FDrawGrid.Columns.Count > 0 then FTopPanelMultiSearch.Height:=FMultiSearchEdits.Last.FPanel.Height; @@ -2159,16 +3047,52 @@ procedure TCustomVisualGrid.ReloadColumns; procedure TCustomVisualGrid.LayoutChanged; begin + {$IFNDEF FPC}if not Assigned(Parent) then Exit;{$ENDIF} // layout has changed, maybe more space is available - if AutoPageSize then - PageSize := ClientRowCount; + if AutoPageSize and FCanPage then + ChangePageSize(ClientRowCount,True,True); end; function TCustomVisualGrid.ClientRowCount: Integer; -begin - Result := ((FDrawGrid.ClientHeight - FDrawGrid.GridLineWidth) div FDrawGrid.DefaultRowHeight) - FDrawGrid.FixedRows; - if Result = 0 then - Result := 1; +var + LAvailable: Int64; + LRow, LRowHeight, LLineWidth: Integer; +begin + // ClientHeight already excludes native borders/scrollbars. LCL includes the + // separator in RowHeights; VCL adds its DPI-scaled separator to each row. + LAvailable := FDrawGrid.ClientHeight; + LLineWidth := 0; + {$IFDEF FPC} + if (FDrawGrid.FixedRows > 0) and (FDrawGrid.RowCount > 0) and + (FDrawGrid.ColCount > 0) then + Dec(LAvailable, FDrawGrid.CellRect(0,0).Top); + {$ELSE} + if FDrawGrid.Options * [goFixedHorzLine,goHorzLine] <> [] then + LLineWidth := TDrawGridAccess(FDrawGrid).ActualGridLineWidth; + // VCL's final separator is outside CellRect: the row is complete when its + // content reaches ClientHeight, even if that following separator is clipped. + Inc(LAvailable,LLineWidth); + {$ENDIF} + for LRow := 0 to FDrawGrid.FixedRows - 1 do + Dec(LAvailable, Max(0,FDrawGrid.RowHeights[LRow]) + LLineWidth); + + // Honor explicit row heights on the current page, then use the default for + // rows not fetched yet. Never admit a row whose bottom lies off the viewport. + Result := 0; + LRow := FDrawGrid.FixedRows; + while (LRow < FDrawGrid.RowCount) and (LAvailable >= 0) do begin + LRowHeight := Max(0,FDrawGrid.RowHeights[LRow]) + LLineWidth; + if LRowHeight > LAvailable then Break; + Dec(LAvailable,LRowHeight); + Inc(Result); + Inc(LRow); + end; + if LRow >= FDrawGrid.RowCount then begin + LRowHeight := Max(1,FDrawGrid.DefaultRowHeight + LLineWidth); + if LAvailable > 0 then Inc(Result,LAvailable div LRowHeight); + end; + // PageSize has a one-row minimum even before the first usable native layout. + Result := Max(1,Result); end; procedure TCustomVisualGrid.HidePageSizeControls(AVisible: boolean); @@ -2179,11 +3103,174 @@ procedure TCustomVisualGrid.HidePageSizeControls(AVisible: boolean); procedure TCustomVisualGrid.HidePageNavigationControls(AVisible: boolean); begin - FBottomRightPanel.Visible:=AVisible; + FButtonFirst.Visible := AVisible; + FButtonPrevious.Visible := AVisible; + FPageIndexEdit.Visible := AVisible; + FPageCountLabel.Visible := AVisible; + FButtonNext.Visible := AVisible; + FButtonLast.Visible := AVisible; end; -function TCustomVisualGrid.CheckRangeForPageSize(var APageSize: Integer - ): boolean; +procedure TCustomVisualGrid.UpdatePagingControls; +var + LNavigation, LManual, LInline, LManualBelow: Boolean; + // FPC 3.2.0 Win64 -O2 omits the inline scalar-height assignment and reuses the button-height register, clipping controls. + // Keep dimensions in TSize so the compiler stores/reloads the calculated footer height instead of that stale register. + LFooterSize: TSize; + LAvailable, LMargin, LGap, LModeGap, LModeWidth, + LTotalWidth, LManualWidth, LLabelWidth, LEditWidth, LEditHeight, + LButtonWidth, LButtonMinimum, LButtonHeight, LPageWidth, + LRequiredPageWidth, LCountWidth, LRowHeight, + LGroupWidth, LGroupHeight, LGroupRowHeight, LGroupLeft, LGroupTop, + LLeftWidth, LTopHeight, LManualLeft, LManualTop, LX, LY, I: Integer; + + function ControlTextWidth(AControl: TControl; AFont: TFont; const AText: String): Integer; + var LCanvas: TControlCanvas; + begin + // Native child fonts can differ from the composite canvas after scaling + // or custom painting; measure the control which will display this text. + LCanvas := TControlCanvas.Create; + try + LCanvas.Control := AControl; + LCanvas.Font.Assign(AFont); + Result := LCanvas.TextWidth(AText); + finally + LCanvas.Free; + end; + end; + + procedure PlaceNavigation(AControl: TControl; AWidth, AHeight: Integer); + begin + // Only extreme widths wrap the tail. The selector and first-page button + // are placed together before this flow and always remain adjacent. + if LX+AWidth > LGroupWidth then begin + LX := 0; + Inc(LY,LGroupRowHeight+LGap); + end; + AControl.SetBounds(LX,LY+(LGroupRowHeight-AHeight) div 2,AWidth,AHeight); + Inc(LX,AWidth+LGap); + end; +begin + if not Assigned(FPageModeButton) then Exit; + for I := 0 to FPageModeMenu.Items.Count-1 do + FPageModeMenu.Items[I].Checked := I = Ord(FPageMode); + FPageModeButton.Hint := Format(sPagingModeHint, [FPageModeMenu.Items[Ord(FPageMode)].Caption, sPagingModeSingle, sPagingModeAuto, sPagingModeManual]); + if not Assigned(Parent) then Exit; + FBottomPanel.Visible := FCanPage; + FBottomRightPanel.Visible := FCanPage; + FPageModeButton.Visible := FCanPage; + LManual := FCanPage and (FPageMode = vpmManual); + LNavigation := FCanPage and (FPageMode <> vpmSingle); + if (vgoAutoHidePaging in FOptions) and (FTotalDataCount >= 0) and + (FTotalDataCount <= FPageSize) then LNavigation := False; + HidePageSizeControls(LManual); + HidePageNavigationControls(LNavigation); + if FAutoPageSize and FCanPage then + FDrawGrid.ScrollBars := {$IFDEF FPC}ssAutoHorizontal{$ELSE}ssHorizontal{$ENDIF} + else FDrawGrid.ScrollBars := {$IFDEF FPC}ssAutoBoth{$ELSE}ssBoth{$ENDIF}; + + LMargin := Scale96ToFont(7); + LGap := Scale96ToFont(4); + LModeGap := Scale96ToFont(4); + LFooterSize.cx := FBottomPanel.ClientWidth; + LAvailable := Max(1,LFooterSize.cx-2*LMargin); + LTotalWidth := 0; + if FAllRecordsCountLabel.Visible then LTotalWidth := FAllRecordsCountLabel.Width; + LLabelWidth := Max(FPageSizeLabel.Width,Scale96ToFont(60)); + LEditWidth := Max(Scale96ToFont(52),4*Abs(Font.Height)+Scale96ToFont(8)); + LManualWidth := LLabelWidth+LMargin+LEditWidth; + LEditHeight := Max(Scale96ToFont(21),Abs(Font.Height)+Scale96ToFont(8)); + // LCL edits can auto-size taller than the requested height. Reserve their + // actual native height before placing the separate manual-size band. + FPageSizeEdit.Height := LEditHeight; + FPageIndexEdit.Height := LEditHeight; + LEditHeight := Max(LEditHeight,Max(FPageSizeEdit.Height,FPageIndexEdit.Height)); + LButtonMinimum := ControlTextWidth(FButtonFirst,FButtonFirst.Font,'|<')+Scale96ToFont(6); + LButtonWidth := Max(Scale96ToFont(25),LButtonMinimum); + LButtonHeight := Max(Scale96ToFont(25),LEditHeight); + LModeWidth := LButtonHeight; + // Required text space applies in both the normal and compact layout. LCL + // can scale a child font independently of these nominal control widths. + LRequiredPageWidth := + ControlTextWidth(FPageIndexEdit,FPageIndexEdit.Font,IntToStr(Max(1,FPageCount)))+ + Max(0,FPageIndexEdit.Width-FPageIndexEdit.ClientWidth)+Scale96ToFont(8); + LPageWidth := Max(Scale96ToFont(56),LRequiredPageWidth); + LCountWidth := Max(FPageCountLabel.Width,Scale96ToFont(18)); + LLeftWidth := LTotalWidth; + if LManual then begin + if LLeftWidth > 0 then Inc(LLeftWidth,2*LMargin); + Inc(LLeftWidth,LManualWidth); + end; + LGroupWidth := LModeWidth; + if LNavigation then + Inc(LGroupWidth,LModeGap+4*LButtonWidth+LPageWidth+LCountWidth+5*LGap); + if LNavigation and ((LGroupWidth > LAvailable) or + (LLeftWidth+LGroupWidth+3*LMargin > LFooterSize.cx)) then begin + // Try compact navigation beside the total and manual fields before wrapping + // the footer. A pager can fit alone while forcing a six-digit total above it. + LGap := Scale96ToFont(2); + LButtonWidth := Max(Scale96ToFont(21),LButtonMinimum); + // Compact spacing must still display the largest valid page number, + // including the native edit border and room for its text margins/caret. + LPageWidth := Max(Scale96ToFont(36),LRequiredPageWidth); + LGroupWidth := LModeWidth+LModeGap+4*LButtonWidth+LPageWidth+LCountWidth+5*LGap; + end; + LGroupWidth := Min(LGroupWidth,LAvailable); + if LNavigation then LGroupWidth := Max(LGroupWidth,LModeWidth+LModeGap+LButtonWidth) + else LGroupWidth := Max(LGroupWidth,LModeWidth); + LGroupRowHeight := Max(LButtonHeight,LEditHeight); + FPageModeButton.SetBounds(0,(LGroupRowHeight-LButtonHeight) div 2,LModeWidth,LButtonHeight); + LGroupRowHeight := Max(LGroupRowHeight,FPageModeButton.Height); + FPageModeButton.Top := (LGroupRowHeight-FPageModeButton.Height) div 2; + UpdatePageModeGlyph; + LX := LModeWidth+LModeGap; + LY := 0; + if LNavigation then begin + FButtonFirst.SetBounds(LX,(LGroupRowHeight-LButtonHeight) div 2,LButtonWidth,LButtonHeight); + Inc(LX,LButtonWidth+LGap); + PlaceNavigation(FButtonPrevious,LButtonWidth,LButtonHeight); + PlaceNavigation(FPageIndexEdit,LPageWidth,LEditHeight); + PlaceNavigation(FPageCountLabel,LCountWidth,FPageCountLabel.Height); + PlaceNavigation(FButtonNext,LButtonWidth,LButtonHeight); + PlaceNavigation(FButtonLast,LButtonWidth,LButtonHeight); + end; + LGroupHeight := LY+LGroupRowHeight; + LRowHeight := Max(Scale96ToFont(40),LGroupRowHeight+Scale96ToFont(14)); + LInline := LLeftWidth+LGroupWidth+3*LMargin <= LFooterSize.cx; + LManualBelow := LManual and (LLeftWidth > LAvailable); + LTopHeight := FAllRecordsCountLabel.Height; + if LManual then LTopHeight := Max(LTopHeight,LEditHeight); + if LManualBelow then LTopHeight := FAllRecordsCountLabel.Height+LGap+LEditHeight; + if LInline then begin + LFooterSize.cy := Max(LRowHeight,LGroupHeight+Scale96ToFont(4)); + LGroupTop := (LFooterSize.cy-LGroupHeight) div 2; + LManualTop := (LFooterSize.cy-LEditHeight) div 2; + FAllRecordsCountLabel.Top := (LFooterSize.cy-FAllRecordsCountLabel.Height) div 2; + end else begin + LFooterSize.cy := Max(LRowHeight,LTopHeight+LGroupHeight+Scale96ToFont(2)); + LGroupTop := LFooterSize.cy-LGroupHeight-Scale96ToFont(2); + LManualTop := (LTopHeight-LEditHeight) div 2; + FAllRecordsCountLabel.Top := (LTopHeight-FAllRecordsCountLabel.Height) div 2; + if LManualBelow then begin + FAllRecordsCountLabel.Top := 0; + LManualTop := FAllRecordsCountLabel.Height+LGap; + end; + end; + LManualLeft := LMargin; + if (LTotalWidth > 0) and not LManualBelow then Inc(LManualLeft,LTotalWidth+2*LMargin); + FAllRecordsCountLabel.Left := LMargin; + FPageSizeLabel.SetBounds(LManualLeft,LManualTop+ + (LEditHeight-FPageSizeLabel.Height) div 2,LLabelWidth,FPageSizeLabel.Height); + FPageSizeEdit.SetBounds(LManualLeft+LLabelWidth+LMargin,LManualTop,LEditWidth,LEditHeight); + FBottomPanel.Height := LFooterSize.cy; + LGroupLeft := Max(0,LFooterSize.cx-LMargin-LGroupWidth); + // LCL may round a scaled button up before a busy fetch permits refitting. + // Its navigation host must retain the same minimum height during that gap. + FBottomRightPanel.Constraints.MinHeight := LGroupHeight; + FBottomRightPanel.SetBounds(LGroupLeft,LGroupTop,LGroupWidth,LGroupHeight); +end; + +function TCustomVisualGrid.CheckRangeForPageSize(var APageSize: Integer ): boolean; begin if APageSize <= 0 then begin @@ -2203,6 +3290,38 @@ procedure TCustomVisualGrid.SetDataSource(ADataSource: IDataSource); if FDataSource = ADataSource then Exit; + if Assigned(FActiveThread) and (FRefreshThrottle.Mode <> temNone) then begin + // Component-backed sources are not necessarily reference counted. Record + // the new input before joining, but retain the existing safe replacement + // lifetime: a caller may release its old source after this setter returns. + FDataSource := ADataSource; + RefreshPageIndexData(True); + FJoiningFetch := True; + try FActiveThread.WaitFor; + finally FJoiningFetch := False; end; + FetchDataThreadCompleted(Self); + Exit; + end; + + // FetchPage reads this interface on the worker; never replace it mid-fetch. + if Assigned(FActiveThread) then begin + FJoiningFetch := True; + try FActiveThread.WaitFor; + finally FJoiningFetch := False; end; + FetchDataThreadCompleted(Self); + // A pending refresh may have started another fetch of the old source. + while Assigned(FActiveThread) do begin + FRefreshPending := false; + FRefreshColumnsPending := false; + FRefreshClientPending := False; + FRefreshReady := False; + FPendingRefreshSource := nil; + FJoiningFetch := True; + try FActiveThread.WaitFor; + finally FJoiningFetch := False; end; + FetchDataThreadCompleted(Self); + end; + end; FDataSource := ADataSource; RefreshPageIndexData(true); @@ -2210,6 +3329,7 @@ procedure TCustomVisualGrid.SetDataSource(ADataSource: IDataSource); procedure TCustomVisualGrid.SetPageIndex(Value: Integer); begin + if (FPageMode = vpmSingle) or not FCanPage then Value := 0; if Value >= FPageCount then Value := FPageCount - 1; if Value < 0 then @@ -2222,14 +3342,14 @@ procedure TCustomVisualGrid.SetPageIndex(Value: Integer); RefreshPageIndexData(false); end; -procedure TCustomVisualGrid.SetPageIndexEditText(const AStr: utf8string); +procedure TCustomVisualGrid.SetPageIndexEditText(const AStr: String); begin Include(FGUIUpdates, updPageIndex); FPageIndexEdit.Text := AStr; Exclude(FGUIUpdates, updPageIndex); end; -procedure TCustomVisualGrid.SetPageSizeEditText(const AStr: utf8string); +procedure TCustomVisualGrid.SetPageSizeEditText(const AStr: String); begin Include(FGUIUpdates, updPageSize); FPageSizeEdit.Text := AStr; @@ -2238,12 +3358,27 @@ procedure TCustomVisualGrid.SetPageSizeEditText(const AStr: utf8string); procedure TCustomVisualGrid.BeforeFetchPage; begin - if Assigned(FDataSource) then + if FActiveRefreshParams.PageSize <= 0 then FPageIndex := 0 + else if Assigned(FActiveRefreshSource) then if FPageIndex >= FPageCount then FPageIndex := FPageCount - 1; + FActiveRefreshParams.PageIndex := FPageIndex; end; procedure TCustomVisualGrid.FetchPage(out AResult: TPageFetchResult); +begin + if Assigned(FActiveRefreshSource) then + AResult := FActiveRefreshSource.FetchPage(FActiveRefreshParams, FDataTable) + else begin + // Clearing a source publishes an empty page too. Stale counts/rows would + // otherwise keep automatic layout replaying the removed page indefinitely. + FDataTable := Default(TDataTable); + FillChar(AResult, SizeOf(AResult), #0); + AResult.PageIndex := -1; + end; +end; + +function TCustomVisualGrid.CaptureRefreshParams: TPageFetchParams; // additionall fill with sort direction procedure FillFilter; @@ -2252,7 +3387,7 @@ procedure TCustomVisualGrid.FetchPage(out AResult: TPageFetchResult); i, j, idx: Integer; LFilter: TColumnFilter; - function UpdateFilterSortDirection(const AColumnName: utf8string; ASortDirection: TSortDirection): boolean; + function UpdateFilterSortDirection(const AColumnName: String; ASortDirection: TSortDirection): boolean; var i: Integer; begin @@ -2306,13 +3441,15 @@ procedure TCustomVisualGrid.FetchPage(out AResult: TPageFetchResult); end; begin - if Assigned(FDataSource) then - begin - FillFilter; - AResult := FDataSource.FetchPage(TPageFetchParams.Create(FPageIndex, FPageSize, FFilters.ToArray, IIF(FSearchMode = smMulti, foAnd, foOr)), FDataTable) - end - else - FillChar(AResult, SizeOf(AResult), #0); + FillFilter; + Result := TPageFetchParams.Create(FPageIndex, FPageSize, FFilters.ToArray, + IIF(FSearchMode = smMulti, foAnd, foOr)); + // The source's existing nonpositive-size contract means all matching rows, + // including rows added after an earlier result or an initially unknown total. + if (FPageMode = vpmSingle) or not FCanPage then begin + Result.PageIndex := 0; + Result.PageSize := -1; + end; end; procedure TCustomVisualGrid.AfterFetchPage; @@ -2322,8 +3459,29 @@ procedure TCustomVisualGrid.AfterFetchPage; LNewRect: TGridRect; LOptions: TGridOptions; begin - LNewRect := TGridRect.Create(FLastSelectionEvent.Col, FLastSelectionEvent.Row + 1, - FLastSelectionEvent.Col + FLastSelectionEvent.ColCount, FLastSelectionEvent.Row + FLastSelectionEvent.RowCount); + {$IFDEF FPC} + LNewRect := TGridRect.Create(FLastSelectionEvent.Col,FLastSelectionEvent.Row+1, + FLastSelectionEvent.Col+FLastSelectionEvent.ColCount,FLastSelectionEvent.Row+FLastSelectionEvent.RowCount); + {$ELSE} + // An empty page and an absent selection both use -1. That sentinel is + // not a native grid coordinate; retain no selection until rows exist. + if (Length(FDataTable.Rows)=0) or (FLastSelectionEvent.Col<0) or + (FLastSelectionEvent.Row<0) or (FLastSelectionEvent.ColCount<=0) or + (FLastSelectionEvent.RowCount<=0) then begin + UpdateSelection(stNone,False); + Exit; + end; + LNewRect.Left:=FLastSelectionEvent.Col; + LNewRect.Top:=FLastSelectionEvent.Row+1; + LNewRect.Right:=Min(FDrawGrid.ColCount-1, + FLastSelectionEvent.Col+FLastSelectionEvent.ColCount-1); + LNewRect.Bottom:=Min(Length(FDataTable.Rows), + FLastSelectionEvent.Row+FLastSelectionEvent.RowCount); + if (LNewRect.Left>LNewRect.Right) or (LNewRect.Top>LNewRect.Bottom) then begin + UpdateSelection(stNone,False); + Exit; + end; + {$ENDIF} FIgnoreSelectionEvent:=true; TDrawGridAccess(FDrawGrid).MoveExtend(false, LNewRect.Left, LNewRect.Bottom); @@ -2336,17 +3494,16 @@ procedure TCustomVisualGrid.AfterFetchPage; end; begin + Inc(FInternalLayoutDepth); + try with FLastFetchDataResult do - if Assigned(FDataSource) then begin if FromThread then begin - FIgnoreRecalcPageCount := TFetchDataThread(FActiveThread).FIgnoreRecalcPageCount; - FActiveThread := nil; Dispose(FCachedDataTable); FCachedDataTable := nil; - FFetchDataThreadTimer.Enabled:=false; - FLoadDataPanel.Visible:=False; + FFetchInProgress:=false; + EndLoading; ControlsEnable(true); end; @@ -2362,12 +3519,25 @@ procedure TCustomVisualGrid.AfterFetchPage; FPageIndex := FetchResult.PageIndex; - if RefreshColumns then + if RefreshColumns and Assigned(FActiveRefreshSource) then ReloadColumns; // when last page has different count than all other pages // we need to handle events on own way FIgnoreSelectionEvent:=true; + {$IFDEF FPC} FDrawGrid.RowCount := Length(FDataTable.Rows) + 1; + {$ELSE} + // VCL must be able to clamp its previous current row during a shrink. + // Its structural placeholder is selectable only for that update; a final + // empty result still rejects all data-row selection. + FDrawGrid.DataRowCount:=Max(1,Length(FDataTable.Rows)); + try + FDrawGrid.RowCount:=Max(2,Length(FDataTable.Rows)+1); + finally + FDrawGrid.DataRowCount:=Length(FDataTable.Rows); + FIgnoreSelectionEvent:=false; + end; + {$ENDIF} FIgnoreSelectionEvent:=false; end; RefreshPageIndexAndGridInterface; @@ -2382,6 +3552,28 @@ procedure TCustomVisualGrid.AfterFetchPage; end else UpdateSelection(stNone, false); + ResetModeScrollPosition(not FRefreshClientPending); + finally + Dec(FInternalLayoutDepth); + end; + // Publishing an older page must not overwrite a newer queued client intent. + if FRefreshClientPending then begin + FPageIndex := FPendingRefreshParams.PageIndex; + if FPendingRefreshParams.PageSize > 0 then + FPageSize := FPendingRefreshParams.PageSize; + SetPageSizeEditText(IntToStr(FPageSize)); + FFilters.Clear; + FFilters.AddRange(FPendingRefreshParams.Filter); + end; + // An interface does not retain a component-backed provider. Result handling + // is finished before consumers can replace and release their old source. + FActiveRefreshSource := nil; + // Synchronous cached fetches also finish the queued loading presentation. + // EndLoading retains it when a newer client request is still waiting. + EndLoading; + // A consumer requesting another refresh here supplies a new client input. + if Assigned(FOnFinishedUpdating) then + FOnFinishedUpdating(Self); end; procedure TCustomVisualGrid.UpdateSelection(ASelectionType: TSelectionType; AResetSelection: boolean); @@ -2401,8 +3593,7 @@ procedure TCustomVisualGrid.UpdateSelection(ASelectionType: TSelectionType; ARes GridSelection(nil, 0, 0); end; -function TCustomVisualGrid.SelectionsEquals(constref A, B: TVisualGridSelection - ): boolean; +function TCustomVisualGrid.SelectionsEquals({$IFDEF FPC}constref{$ELSE}const{$ENDIF} A, B: TVisualGridSelection ): boolean; begin Result := A.Page = B.Page; if Result then @@ -2421,14 +3612,59 @@ procedure TCustomVisualGrid.ResetSelection(out ASelection: TVisualGridSelection) end; procedure TCustomVisualGrid.SetPageSize(Value: Integer); +begin + if Value <= 0 then Exit; + CheckRangeForPageSize(Value); + FManualPageSize := Value; + if FPageMode = vpmSingle then begin + FPageSize := Value; + SetPageSizeEditText(IntToStr(Value)); + Exit; + end; + ChangePageSize(Value, True, True); +end; + +procedure TCustomVisualGrid.ChangePageSize(Value: Integer; AClientInput, ANotify: Boolean); begin if FPageSize = Value then Exit; CheckRangeForPageSize(Value); + // Invalid inputs normalize to the current capacity. A transient tiny client + // rectangle must not keep refetching that unchanged effective page size. + if FPageSize = Value then + Exit; FPageSize := Value; SetPageSizeEditText(IntToStr(FPageSize)); - RefreshPageIndexData(false); + if not AClientInput and FAutoPageSize and FRefreshClientPending then begin + // Result chrome may reveal the final capacity while newer client work is + // waiting. Keep its page/filter/source intent and merge geometry only. + FPendingRefreshParams.PageSize := Value; + Exit; + end; + // Hiding paging/search controls can expose extra rows after a fetch. If the + // entire result already fits, changing capacity needs layout only; fetching + // again would bypass the consumer's refresh policy for the same data. + if (not Assigned(FActiveThread)) and not FRefreshPending and (FPageIndex <= 0) and + (FTotalDataCount >= 0) and (Length(FDataTable.Rows) = FTotalDataCount) and + (Value >= FTotalDataCount) then begin + if FTotalDataCount > 0 then begin + FPageIndex := 0; + FPageCount := 1; + end else begin + FPageIndex := -1; + FPageCount := 0; + end; + Inc(FInternalLayoutDepth); + try RefreshPageIndexAndGridInterface; + finally Dec(FInternalLayoutDepth); end; + if AClientInput and ANotify then begin + FRefreshReady := False; + FRefreshThrottle.Notify; + end; + Exit; + end; + QueueRefresh(False, AClientInput, ANotify); end; procedure TCustomVisualGrid.SetSelectionType(AValue: TSelectionType); @@ -2463,10 +3699,10 @@ procedure TCustomVisualGrid.ClearSelection(AIgnoreDeselectionType: boolean); if (FDeselectionType <> dtNone) or AIgnoreDeselectionType then begin ResetSelection(FLastSelection); + // Physical selection must also clear when no selection event is registered. + UpdateSelection(stNone, true); if FLastSelectionEvent.Page <> FPageIndex then - GridSelection(nil, 0, 0) - else - UpdateSelection(stNone, true); + GridSelection(nil, 0, 0); end; end; @@ -2492,10 +3728,12 @@ procedure TCustomVisualGrid.SetWidgetControl(AValue: TControl); FWidgetControl.Parent := FTopPanelLeft; with FWidgetControl do begin + {$IFDEF FPC} AnchorSideLeft.Control := FTopPanelLeft; AnchorSideLeft.Side := asrCenter; AnchorSideTop.Control := FTopPanelLeft; AnchorSideTop.Side := asrCenter; + {$ENDIF} end; end else FTopPanelLeft.Width:=0; @@ -2509,16 +3747,18 @@ procedure TCustomVisualGrid.StandardDrawCell(Sender: TObject; ACol, ARow: Longin LHandled: boolean; LCellData, LRow: Variant; LColumn : TVisualColumn; - LStyle : TTextStyle; + {$IFDEF FPC}LStyle : TTextStyle;{$ENDIF} LDataTable: PDataTable; procedure DrawHeaderCell; begin // Pre-set user defined styles if LColumn.HasHeaderAlignment then begin + {$IFDEF FPC} LStyle := Canvas.TextStyle; LStyle.Alignment := LColumn.HeaderAlignment; Canvas.TextStyle := LStyle; + {$ENDIF} end; if LColumn.HasHeaderFontStyles then Canvas.Font.Style := LColumn.HeaderFontStyles; @@ -2530,9 +3770,11 @@ procedure TCustomVisualGrid.StandardDrawCell(Sender: TObject; ACol, ARow: Longin begin // Pre-set user defined styles if LColumn.HasDataAlignment then begin + {$IFDEF FPC} LStyle := Canvas.TextStyle; LStyle.Alignment := LColumn.DataAlignment; Canvas.TextStyle := LStyle; + {$ENDIF} end; if LColumn.HasDataFontStyles then Canvas.Font.Style := LColumn.DataFontStyles; @@ -2572,8 +3814,7 @@ procedure TCustomVisualGrid.StandardDrawCell(Sender: TObject; ACol, ARow: Longin DrawDataCell; end; -procedure TCustomVisualGrid.GridMouseDown(Sender: TObject; Button: TMouseButton; - Shift: TShiftState; X, Y: Integer); +procedure TCustomVisualGrid.GridMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var LCol, LRow: Integer; begin @@ -2612,15 +3853,15 @@ procedure TCustomVisualGrid.GridMouseDown(Sender: TObject; Button: TMouseButton; end; end; -procedure TCustomVisualGrid.GridMouseUp(Sender: TObject; Button: TMouseButton; - Shift: TShiftState; X, Y: Integer); +procedure TCustomVisualGrid.GridMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var LPopup: TPopupMenu; LSelection: TVisualGridSelection; i: integer; LCol, LRow: longint; - LContains: boolean = false; + LContains: boolean; begin + LContains:=False; if ColCount = 0 then Exit; case Button of @@ -2681,7 +3922,7 @@ procedure TCustomVisualGrid.SearchKindPopupMenuClick(Sender: TObject); procedure TCustomVisualGrid.GridSelection(Sender: TObject; aCol, aRow: Integer); var - LSelection: TVisualGridSelection; + LSelection, LAfterSelection: TVisualGridSelection; begin if Assigned(FOnSelection) and (not FIgnoreSelectionEvent) then begin @@ -2692,22 +3933,29 @@ procedure TCustomVisualGrid.GridSelection(Sender: TObject; aCol, aRow: Integer); if not SelectionsEquals(FLastSelectionEvent, LSelection) then begin - FIgnoreSelectionEvent := true; - FLastSelectionEvent := LSelection; - FOnSelection(Self, LSelection); - // maybe inside FOnSelection was called ClearSelection, we can handle this - if not SelectionsEquals(FLastSelection, LSelection) then - begin - FLastSelectionEvent := FLastSelection; - FOnSelection(Self, FLastSelection); + FIgnoreSelectionEvent := True; + try + FLastSelectionEvent := LSelection; + FOnSelection(Self, LSelection); + // Only an actual callback change, such as ClearSelection, is another + // event. FLastSelection remembers mouse-up state and can be older. + if FCurrentSelectionType = stNone then + ResetSelection(LAfterSelection) + else + LAfterSelection := Selection; + if not SelectionsEquals(LAfterSelection, LSelection) then + begin + FLastSelectionEvent := LAfterSelection; + FOnSelection(Self, LAfterSelection); + end; + finally + FIgnoreSelectionEvent := False; end; - FIgnoreSelectionEvent := false; end; end; end; -procedure TCustomVisualGrid.GridHeaderClick(Sender: TObject; IsColumn: Boolean; - Index: Integer); +procedure TCustomVisualGrid.GridHeaderClick(Sender: TObject; IsColumn: Boolean; Index: Integer); var LColumn: TVisualColumn; begin @@ -2761,6 +4009,6 @@ function TCustomVisualGrid.CalculateCellContentRect(const ARect : TRect) : TRect end; initialization - {$I *.inc} + {$IFDEF FPC}{$I UVisualGrid.inc}{$ENDIF} end. diff --git a/src/libraries/sphere10/UWizard.dfm b/src/libraries/sphere10/UWizard.dfm new file mode 100644 index 000000000..a23808bd7 --- /dev/null +++ b/src/libraries/sphere10/UWizard.dfm @@ -0,0 +1,50 @@ +object WizardHostForm: TWizardHostForm + Left = 187 + Height = 120 + Top = 35 + Width = 360 + BorderIcons = [biSystemMenu] + Caption = 'Wizard' + ClientHeight = 120 + ClientWidth = 360 + OnCloseQuery = FormCloseQuery + Position = poOwnerFormCenter + Visible = False + object FHorizontalLine: TPanel + Left = 0 + Height = 1 + Top = 65 + Width = 358 + Anchors = [akLeft, akRight, akBottom] + BorderStyle = bsSingle + TabOrder = 0 + end + object FPreviousButton: TButton + Left = 16 + Height = 25 + Top = 81 + Width = 136 + Anchors = [akLeft, akBottom] + Caption = '&Back' + OnClick = FPreviousButtonClick + TabOrder = 1 + end + object FNextButton: TButton + Left = 210 + Height = 25 + Top = 81 + Width = 133 + Anchors = [akRight, akBottom] + Caption = '&Next' + OnClick = FNextButtonClick + TabOrder = 2 + end + object FWizardScreenPanel: TPanel + Left = 0 + Height = 64 + Top = 0 + Width = 360 + Anchors = [akTop, akLeft, akRight, akBottom] + TabOrder = 3 + end +end diff --git a/src/libraries/sphere10/UWizard.lfm b/src/libraries/sphere10/UWizard.lfm index 7665ce499..8c62c6b2a 100644 --- a/src/libraries/sphere10/UWizard.lfm +++ b/src/libraries/sphere10/UWizard.lfm @@ -11,7 +11,7 @@ object WizardHostForm: TWizardHostForm Position = poOwnerFormCenter LCLVersion = '1.8.4.0' Visible = False - object FHorizonalLine: TPanel + object FHorizontalLine: TPanel Left = 0 Height = 1 Top = 65 diff --git a/src/libraries/sphere10/UWizard.pas b/src/libraries/sphere10/UWizard.pas index 597f8b69c..704d50d3b 100644 --- a/src/libraries/sphere10/UWizard.pas +++ b/src/libraries/sphere10/UWizard.pas @@ -1,8 +1,9 @@ unit UWizard; -{ Copyright (c) 2017 - 2018 Sphere 10 Software +{ Copyright (c) 2017 - 2018 Herman Schoenfeld - Wizard scren framework for Lazarus/FPC. + Typed-model wizard screens with validation and dynamic routes for Delphi/VCL + and Lazarus/LCL. Distributed under the MIT software license, see the accompanying file LICENSE or visit http://www.opensource.org/licenses/mit-license.php. @@ -13,6 +14,27 @@ THIS LICENSE HEADER MUST NOT BE REMOVED. } +{ Public types in this unit + + Type | Purpose + --------------------------------------------------+------------------------------------------------------------------------- + TActionWizard | Runs a typed wizard with supplied finish and cancel callbacks. + TActionWizard.TActionWizardCancelFunc | Validates cancellation using the current screen and model. + TActionWizard.TActionWizardFinishFunc | Executes and validates wizard completion. + TGenericWizardBag | Stores named Variant values as a shared wizard model. + TPathUpdateType | Selects insertion, replacement or removal of wizard steps. + TWizard | Coordinates navigation, dynamic routes, finish and cancellation. + TWizardForm | Provides typed model access and screen validation lifecycle hooks. + TWizardFormBase | Identifies a hosted wizard screen's model type. + TWizardFormClass | Identifies a wizard screen class for dynamic creation. + TWizardHostForm | Hosts one wizard screen and its navigation buttons in one window. + TWizardHostForm.TWizardHostFormCloseQueryDelegate | Approves or rejects closing the wizard host. + TWizardScreenPath | Holds an ordered array of wizard screen classes. + TWizardState | Reports active, finishing, cancelling or completed wizard state. + TWizardStepTrait | Declares a screen navigation capability. + TWizardStepTraits | Combines a screen's navigation capabilities. +} + {$IFDEF FPC} {$MODE Delphi} @@ -21,25 +43,36 @@ interface uses - Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, UCommon, Generics.Collections; + Classes, SysUtils, TypInfo, Types, + {$IFDEF FPC} + FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, + {$ELSE} + Vcl.Forms, Vcl.Controls, Vcl.Graphics, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls, + {$ENDIF} + UCommon, Generics.Collections; const - CT_WIZARD_DEFAULT_NEXT : AnsiString = '&Next'; - CT_WIZARD_DEFAULT_PREVIOUS : AnsiString = '&Previous'; - CT_WIZARD_DEFAULT_FINISH : AnsiString = '&Finish'; - CT_WIZARD_DEFAULT_TITLE : AnsiString = 'Wizard'; + // UI captions and messages use the compiler's native String, matching callers. + CT_WIZARD_DEFAULT_NEXT : String = '&Next'; + CT_WIZARD_DEFAULT_PREVIOUS : String = '&Previous'; + CT_WIZARD_DEFAULT_FINISH : String = '&Finish'; + CT_WIZARD_DEFAULT_TITLE : String = 'Wizard'; type - { Forward Declarations } -// TWizardForm = class; // Skybuck: disabled, not needed anymore. - { Enums } - TPathUpdateType = (ptInject, ptReplaceAllNext, ptReplaceAll); + { Path updates preserve screen instances. ptRemoveNext only removes future occurrences. } + TPathUpdateType = (ptInject, ptReplaceAllNext, ptReplaceAll, ptRemoveNext); + { A rejected or failed terminal callback returns to wsActive and can be retried. } + TWizardState = (wsActive, wsFinishing, wsCancelling, wsFinished, wsCancelled); + { A dynamic screen promises to register a successor from OnNext. } + TWizardStepTrait = (wstAssumeHasNext); + TWizardStepTraits = set of TWizardStepTrait; + TWizardScreenPath = array of TComponentClass; { TWizardHostForm - the host form that contains the wizard screens. } TWizardHostForm = class(TForm) - FHorizonalLine: TPanel; + FHorizontalLine: TPanel; FNextButton: TButton; FPreviousButton: TButton; FWizardScreenPanel: TPanel; @@ -47,52 +80,70 @@ TWizardHostForm = class(TForm) procedure FNextButtonClick(Sender: TObject); procedure FormCloseQuery(Sender: TObject; var CanClose: boolean); type - TWizardHostFormCloseQueryDelegate = function(out message : AnsiString) : boolean of object; + TWizardHostFormCloseQueryDelegate = function(out message : String) : boolean of object; protected FNextEvent : TNotifyManyEvent; FPreviousEvent : TNotifyManyEvent; FCloseQueryEvent : TWizardHostFormCloseQueryDelegate; - procedure SetTitleText(title : AnsiString); virtual; - function GetTitleText : AnsiString; virtual; + procedure SetTitleText(title : String); virtual; + function GetTitleText : String; virtual; procedure SetHideNext(hideNext : boolean); virtual; function GetHideNext : boolean; virtual; procedure SetHidePrevious(hidePrevious : boolean); virtual; function GetHidePrevious : boolean; virtual; - procedure SetNextText(nextText : AnsiString); virtual; - function GetNextText : AnsiString; virtual; - procedure SetPreviousText(previousText : AnsiString); virtual; - function GetPreviousText : AnsiString; virtual; + procedure SetNextText(nextText : String); virtual; + function GetNextText : String; virtual; + procedure SetPreviousText(previousText : String); virtual; + function GetPreviousText : String; virtual; public property NextEvent : TNotifyManyEvent read FNextEvent write FNextEvent; property PreviousEvent : TNotifyManyEvent read FPreviousEvent write FPreviousEvent; property CloseQueryEvent : TWizardHostFormCloseQueryDelegate read FCloseQueryEvent write FCloseQueryEvent; property HideNext : boolean read GetHideNext write SetHideNext; property HidePrevious : boolean read GetHidePrevious write SetHidePrevious; - property NextText : AnsiString read GetNextText write SetNextText; - property PreviousText : AnsiString read GetPreviousText write SetPreviousText; - property TitleText : AnsiString read GetTitleText write SetTitleText; + property NextText : String read GetNextText write SetNextText; + property PreviousText : String read GetPreviousText write SetPreviousText; + property TitleText : String read GetTitleText write SetTitleText; procedure SetContentSize(size : TPoint); virtual; procedure SetContent(screen : TForm); end; - { TWizardForm is the base class for wizard screens } - TWizardForm = class(TForm) + { FPC can emit distinct generic class identities in different units. Validate + the nongeneric base and model RTTI instead of generic InheritsFrom checks. } + TWizardFormBase = class(TForm) + protected + class function GetModelTypeInfo : PTypeInfo; virtual; abstract; + end; + + TWizardFormClass = class of TWizardFormBase; + + { TWizardForm is the base class for wizard screens. The wizard owns the form; + class/interface models retain their caller's ownership. } + TWizardForm = class(TWizardFormBase) private FModel : T; + FTraits : TWizardStepTraits; //FWizard : TWizard; // FPG Bug: cyclic generic dependencies, via forward decls, don't work protected UpdatePath : procedure (APathUpdateType: TPathUpdateType; const screens : array of TComponentClass) of object; // FPC Bug workaround: calling this is equivalent to FWizard.InjectScreen + class function GetModelTypeInfo : PTypeInfo; override; property Model : T read FModel write FModel; public + property Traits : TWizardStepTraits read FTraits write FTraits; + { Called once after Start assigns Model, including for injected screens. } procedure Initialize; virtual; procedure OnPresent; virtual; procedure OnPrevious; virtual; + { Runs after successful Validate. May update the continuation through UpdatePath. } procedure OnNext; virtual; - function Validate(out message : AnsiString) : boolean; virtual; + { Rejection leaves the current screen active and prevents OnNext. } + function Validate(out message : String) : boolean; virtual; end; - { TWizard - Base class for wizards. Encapsulates the entire wizard flow. } + { TWizard - Base class for wizards. Encapsulates the entire wizard flow. + Call navigation and path operations on the UI thread. One accepted Finish or + Cancel ends the flow; recursive navigation/completion requests are rejected. } TWizard = class(TComponent) type __TScreenType = TWizardForm; // FPC Bug: doesn't support nested generics @@ -101,62 +152,108 @@ TWizard = class(TComponent) FHost : TWizardHostForm; FStarted : Boolean; FFinished : Boolean; - FCurrentScreen : __TScreenType; // Skybuck: Using __TScreenType instead of TWizard solved the FPC Compiler bug ! + FState : TWizardState; + FBusy : Boolean; + FNavigating : Boolean; + FIsCancellable : Boolean; + FChangedEvent : TNotifyManyEvent; + FFinishedEvent : TNotifyManyEvent; + FCurrentScreen : TWizardForm; FModel : T; FScreenPath : TList; FScreenPathBackup : TDictionary; FScreenInstances : TDictionary; + FInitializedScreens : TList; FCurrentScreenIndex : Integer; - FNextText : AnsiString; - FPreviousText : AnsiString; - FFinishText : AnsiString; - FTitleText : AnsiString; + FNextText : String; + FPreviousText : String; + FFinishText : String; + FTitleText : String; procedure CheckStarted; procedure CheckNotStarted; function CalculateFitSize : TPoint; procedure NextHandler(sender : TObject); procedure PreviousHandler(sender : TObject); + procedure ValidateScreenType(AType : TComponentClass); + function CloseQueryHandler(out message : String) : boolean; + function GetScreens : TWizardScreenPath; + procedure SavePath; + procedure ResetPathBackups; + procedure InitializeScreen(AType : TComponentClass); + procedure RefreshNavigation; + procedure CloseHost(AModalResult : Integer); + function CompleteFinish(out message : String) : boolean; protected - function CreateScreen(AType: TComponentClass) : __TScreenType; + function CreateScreen(AType: TComponentClass) : TWizardForm; + property Host : TWizardHostForm read FHost; + { Modal presentation hook. Start releases the host even if this raises. } + procedure ShowHost; virtual; + procedure ShowError(const message : String); virtual; function DetermineHasNext : boolean; virtual; function DetermineHasPrevious : boolean; virtual; - procedure PresentScreen(screen : __TScreenType); virtual; - function FinishRequested(out message : AnsiString) : boolean; virtual; abstract; - function CancelRequested(out message : AnsiString) : boolean; virtual; abstract; + procedure PresentScreen(screen : TWizardForm); virtual; + { Return false with a message to retain the active flow. Exceptions also + release busy admission so the caller can retry. Do not navigate here. } + function FinishRequested(out message : String) : boolean; virtual; abstract; + function CancelRequested(out message : String) : boolean; virtual; abstract; public - constructor Create(AOwner:TComponent; const screens: array of TComponentClass); overload; + constructor Create(AOwner:TComponent; const screens: array of TComponentClass); overload; destructor Destroy; override; - property CurrentScreen : __TScreenType read FCurrentScreen; + property CurrentScreen : TWizardForm read FCurrentScreen; property Model : T read FModel; + { Returns a membership snapshot. Cached screen forms remain wizard-owned + after they leave this path and are freed with the wizard. } + property Screens : TWizardScreenPath read GetScreens; + property CurrentScreenIndex : Integer read FCurrentScreenIndex; + property State : TWizardState read FState; + property IsBusy : boolean read FBusy; + property IsCancellable : boolean read FIsCancellable write FIsCancellable; + property ChangedEvent : TNotifyManyEvent read FChangedEvent; + property FinishedEvent : TNotifyManyEvent read FFinishedEvent; property HasNext : boolean read DetermineHasNext; property HasPrevious : boolean read DetermineHasPrevious; - property NextText : AnsiString read FNextText write FNextText; - property PreviousText : AnsiString read FPreviousText write FPreviousText; - property FinishText : AnsiString read FFinishText write FFinishText; - property TitleText : AnsiString read FTitleText write FTitleText; - procedure Start(constref model : T); virtual; + property NextText : String read FNextText write FNextText; + property PreviousText : String read FPreviousText write FPreviousText; + property FinishText : String read FFinishText write FFinishText; + property TitleText : String read FTitleText write FTitleText; + { Starts once, initializes screens with model, and presents a modal host. + The wizard does not explicitly dispose model instances. } + procedure Start({$IFDEF FPC}constref{$ELSE}const{$ENDIF} model : T); virtual; + { Convenience commands display nonempty rejection messages through ShowError. } procedure Next; virtual; procedure Previous; virtual; + { Noninteractive outcomes: TryNext validates before OnNext; TryPrevious + runs OnPrevious. A terminal Next attempts Finish. Missing dynamic + successors remain active. } + function TryNext(out message : String) : boolean; virtual; + function TryPrevious(out message : String) : boolean; virtual; + { Require Start to assign the model. Finish validates/runs OnNext before + checking the final path. Accepted outcomes remain idempotent after closure. } + function Finish(out message : String) : boolean; virtual; + { Checks IsCancellable both before and after CancelRequested. } + function Cancel(out message : String) : boolean; virtual; + { Allowed from OnNext/OnPrevious for dynamic branching. Repeated identical + injection is idempotent; Previous restores its screen's saved branch. } procedure UpdatePath(APathUpdateType: TPathUpdateType; const screens : array of TComponentClass); virtual; + { Removes one occurrence while idle; the final screen cannot be removed. } + procedure RemoveScreen(AType : TComponentClass); virtual; end; { TActionWizard - a generic Wizard that can be used without subclassing } TActionWizard = class(TWizard) public type - TActionWizardCancelFunc = function(screenIndex : Integer; constref model : T; out message : AnsiString) : boolean of object; - TActionWizardFinishFunc = function(constref model : T; out message : AnsiString) : boolean of object; + TActionWizardCancelFunc = function(screenIndex : Integer; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} model : T; out message : String) : boolean of object; + TActionWizardFinishFunc = function({$IFDEF FPC}constref{$ELSE}const{$ENDIF} model : T; out message : String) : boolean of object; private FCancelHandler : TActionWizardCancelFunc; FFinishHandler : TActionWizardFinishFunc; protected - function CancelRequested(out message : AnsiString) : boolean; override; - function FinishRequested(out message : AnsiString) : boolean; override; + function CancelRequested(out message : String) : boolean; override; + function FinishRequested(out message : String) : boolean; override; public - constructor Create(AOwner:TComponent; title, finish: AnsiString; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); - class procedure Show(AParent: TForm; title, finish: AnsiString; constref bag : T; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); - property FinishText : AnsiString read FTitleText; - property TitleText : AnsiString read FFinishText; + constructor Create(AOwner:TComponent; title, finish: String; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); + class procedure Show(AParent: TForm; title, finish: String; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} bag : T; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); end; { TGenericWizardBag - a generic bag for use within wizard } @@ -164,13 +261,19 @@ TActionWizard = class(TWizard) implementation +{$IFDEF FPC} {$R *.lfm} - -uses - UCommon.UI; +{$ELSE} +{$R *.dfm} +{$ENDIF} {%region TWizardForm } +class function TWizardForm.GetModelTypeInfo : PTypeInfo; +begin + Result := TypeInfo(T); +end; + procedure TWizardForm.Initialize; begin // Default implementation @@ -191,7 +294,7 @@ procedure TWizardForm.OnNext; // Default implementation end; -function TWizardForm.Validate(out message : AnsiString) : boolean; +function TWizardForm.Validate(out message : String) : boolean; begin // Default implementation message := ''; @@ -214,8 +317,9 @@ procedure TWizardHostForm.FNextButtonClick(Sender: TObject); procedure TWizardHostForm.FormCloseQuery(Sender: TObject; var CanClose: boolean); var - message : string; + message : String; begin + message := ''; if Assigned(FCloseQueryEvent) then begin CanClose := FCloseQueryEvent(message); if message <> '' then @@ -224,12 +328,12 @@ procedure TWizardHostForm.FormCloseQuery(Sender: TObject; var CanClose: boolean) CanClose := true end; -procedure TWizardHostForm.SetTitleText(title : AnsiString); +procedure TWizardHostForm.SetTitleText(title : String); begin self.Caption:= title; end; -function TWizardHostForm.GetTitleText : AnsiString; +function TWizardHostForm.GetTitleText : String; begin Result := self.Caption; end; @@ -256,35 +360,44 @@ function TWizardHostForm.GetHideNext : boolean; Result := NOT FNextButton.Enabled; end; -procedure TWizardHostForm.SetNextText(nextText : AnsiString); +procedure TWizardHostForm.SetNextText(nextText : String); begin FNextButton.Caption := nextText; end; -function TWizardHostForm.GetNextText : AnsiString; +function TWizardHostForm.GetNextText : String; begin Result := FNextButton.Caption; end; -procedure TWizardHostForm.SetPreviousText(previousText : AnsiString); +procedure TWizardHostForm.SetPreviousText(previousText : String); begin FPreviousButton.Caption := previousText; end; -function TWizardHostForm.GetPreviousText : AnsiString; +function TWizardHostForm.GetPreviousText : String; begin Result := FPreviousButton.Caption; end; procedure TWizardHostForm.SetContent(screen : TForm); begin - if FWizardScreenPanel.ControlCount > 0 then - FWizardScreenPanel.RemoveAllControls(false); + while FWizardScreenPanel.ControlCount > 0 do begin + // A visible LCL form becomes a standalone window when its parent clears. + FWizardScreenPanel.Controls[0].Hide; + FWizardScreenPanel.Controls[0].Parent := nil; + end; with screen do begin - Align := alClient; + Hide; + Align := alNone; BorderStyle := bsNone; + Position := poDesigned; Parent := self.FWizardScreenPanel; + // Apply alignment to the real child handle, not just cached LCL bounds. + BoundsRect := self.FWizardScreenPanel.ClientRect; + HandleNeeded; + Align := alClient; Show; end; end; @@ -302,66 +415,93 @@ procedure TWizardHostForm.SetContentSize(size : TPoint); {%region TWizard } constructor TWizard.Create(AOwner:TComponent; const screens: array of TComponentClass); -var - i : integer; - screen : __TScreenType; begin inherited Create(AOwner); if Length(screens) = 0 then raise Exception.Create('Wizard needs at least 1 screen'); - //self.FHost = nil; // Created/destroyed on Show self.FNextText := CT_WIZARD_DEFAULT_NEXT; self.FPreviousText := CT_WIZARD_DEFAULT_PREVIOUS; self.FFinishText := CT_WIZARD_DEFAULT_FINISH; self.FTitleText := CT_WIZARD_DEFAULT_TITLE; self.FStarted := false; self.FFinished := false; + self.FState := wsActive; + self.FIsCancellable := true; + self.FCurrentScreenIndex := 0; self.FScreenPath := TList.Create; self.FScreenInstances := TDictionary.Create; + self.FInitializedScreens := TList.Create; + self.FScreenPathBackup := TDictionary.Create; // Create the screen path UpdatePath(ptReplaceAll, screens); - FScreenPathBackup := TDictionary.Create; end; destructor TWizard.Destroy; var - i : integer; - screen : __TScreenType; - key : TComponentClass; + screen : TWizardForm; backup : TList; begin - // screens destroyed as sub-components - for screen in FScreenInstances.Values do begin - if screen.Owner = nil then - screen.Free; // only destroy if dangling screen component + if Assigned(FHost) then begin + FHost.CloseQueryEvent := nil; + while FHost.FWizardScreenPanel.ControlCount > 0 do begin + FHost.FWizardScreenPanel.Controls[0].Hide; + FHost.FWizardScreenPanel.Controls[0].Parent := nil; + end; + FreeAndNil(FHost); end; + // Screens removed by branching still belong to this wizard. + if Assigned(FScreenInstances) then + for screen in FScreenInstances.Values do + screen.Free; FScreenInstances.Free; + FInitializedScreens.Free; FScreenPath.Free; - for backup in FScreenPathBackup.Values do - backup.Free; + if Assigned(FScreenPathBackup) then + for backup in FScreenPathBackup.Values do + backup.Free; FScreenPathBackup.Free; inherited Destroy; // note: Property bag not destroyed, left for user to destroy end; -function TWizard.CreateScreen(AType: TComponentClass) : __TScreenType; +function TWizard.CreateScreen(AType: TComponentClass) : TWizardForm; begin if NOT FScreenInstances.ContainsKey(AType) then begin - Result := __TScreenType(AType.Create(self)); - if Result = nil then - raise Exception.Create('Supplied type was not correct TWizardForm type'); + ValidateScreenType(AType); + Result := TWizardForm(AType.Create(self)); Result.UpdatePath := UpdatePath; FScreenInstances.Add(AType, Result); - Result.Initialize; end else Result := FScreenInstances[AType]; end; +procedure TWizard.ValidateScreenType(AType : TComponentClass); +begin + if (AType = nil) or NOT AType.InheritsFrom(TWizardFormBase) then + raise Exception.Create('Supplied type was not a wizard screen'); + if TWizardFormClass(AType).GetModelTypeInfo <> TypeInfo(T) then + raise Exception.Create('Supplied wizard screen has a different model type'); +end; + +procedure TWizard.InitializeScreen(AType : TComponentClass); +var + screen : TWizardForm; +begin + if FInitializedScreens.Contains(AType) then exit; + screen := CreateScreen(AType); + screen.Model := FModel; + screen.Initialize; + FModel := screen.Model; // Preserve changes when the model is a record. + if FNavigating and Assigned(FCurrentScreen) then + FCurrentScreen.Model := FModel; + FInitializedScreens.Add(AType); +end; + function TWizard.CalculateFitSize : TPoint; var - maxWidth, maxHeight, i : Integer; - screen : __TScreenType; + maxWidth, maxHeight : Integer; + screen : TWizardForm; begin maxWidth := 0; maxHeight := 0; @@ -374,139 +514,388 @@ function TWizard.CalculateFitSize : TPoint; function TWizard.DetermineHasNext : boolean; begin - CheckStarted; - DetermineHasNext := FCurrentScreenIndex < FScreenPath.Count - 1; + Result := (FCurrentScreenIndex < FScreenPath.Count - 1) or + (wstAssumeHasNext in FScreenInstances[FScreenPath[FCurrentScreenIndex]].Traits); end; function TWizard.DetermineHasPrevious : boolean; begin - CheckStarted; DetermineHasPrevious := FCurrentScreenIndex > 0; end; -procedure TWizard.Start(constref model : T); +procedure TWizard.Start({$IFDEF FPC}constref{$ELSE}const{$ENDIF} model : T); var i : integer; begin CheckNotStarted; + if FState <> wsActive then + raise Exception.Create('Only an active wizard can be started'); self.FModel := model; - self.FHost := TWizardHostForm.Create(Self.GetParentComponent); - self.FHost.NextEvent.Add(NextHandler); - self.FHost.PreviousEvent.Add(PreviousHandler); - self.FHost.CloseQueryEvent := CancelRequested; - self.FHost.PreviousText := self.PreviousText; - self.Fhost.NextText := self.NextText; - self.FHost.TitleText := self.TitleText; - self.FCurrentScreenIndex := 0; self.FStarted := true; - self.FFinished := false; - self.FHost.SetContentSize ( CalculateFitSize ); - self.FScreenPathBackup.Add(0, TList.Create(FScreenPath)); - self.PresentScreen(FScreenInstances[FScreenPath[FCurrentScreenIndex]]); - //self.FHost.NextEvent.Remove(NextHandler); - //self.FHost.PreviousEvent.Remove(PreviousHandler); - self.FHost.ShowModal; - self.FHost.Free; - self.FHost := nil; + self.FHost := TWizardHostForm.Create(self); + try + self.FHost.NextEvent.Add(NextHandler); + self.FHost.PreviousEvent.Add(PreviousHandler); + self.FHost.CloseQueryEvent := CloseQueryHandler; + self.FHost.PreviousText := self.PreviousText; + self.FHost.TitleText := self.TitleText; + // Initialization sees the model, including for screens injected later. + i := 0; + while i < FScreenPath.Count do begin + InitializeScreen(FScreenPath[i]); + inc(i); + end; + self.FHost.SetContentSize(CalculateFitSize); + ResetPathBackups; + self.PresentScreen(FScreenInstances[FScreenPath[FCurrentScreenIndex]]); + ShowHost; + finally + self.FHost.CloseQueryEvent := nil; + // Detach borrowed controls before destroying their native host. + while FHost.FWizardScreenPanel.ControlCount > 0 do begin + FHost.FWizardScreenPanel.Controls[0].Hide; + FHost.FWizardScreenPanel.Controls[0].Parent := nil; + end; + FreeAndNil(FHost); + end; end; procedure TWizard.Next; var - message : AnsiString; + message : String; begin + if NOT TryNext(message) and (message <> '') then + ShowError(message); +end; + +procedure TWizard.Previous; +var + message : String; +begin + if NOT TryPrevious(message) and (message <> '') then + ShowError(message); +end; + +function TWizard.TryNext(out message : String) : boolean; +begin + message := ''; + Result := false; + if FBusy or (FState <> wsActive) then exit; CheckStarted; - if NOT FCurrentScreen.Validate(message) then begin - ShowMessage (message); - exit; + FBusy := true; + FNavigating := true; + try + if NOT FCurrentScreen.Validate(message) then exit; + FCurrentScreen.OnNext; + FModel := FCurrentScreen.Model; + if FCurrentScreenIndex < FScreenPath.Count - 1 then begin + inc(FCurrentScreenIndex); + SavePath; + PresentScreen(FScreenInstances[FScreenPath[FCurrentScreenIndex]]); + Result := true; + end else if HasNext then + message := 'The current wizard screen expects a next screen' + else begin + // Release navigation admission before entering the terminal operation. + FNavigating := false; + FBusy := false; + Result := CompleteFinish(message); + end; + finally + FNavigating := false; + FBusy := false; end; - FCurrentScreen.OnNext; - FModel := FCurrentScreen.Model; // Restore model in case it's a record - if HasNext then begin - inc(FCurrentScreenIndex); - // Backup current path in case user goes back +end; + +function TWizard.TryPrevious(out message : String) : boolean; +begin + message := ''; + Result := false; + if FBusy or (FState <> wsActive) then exit; + CheckStarted; + if NOT HasPrevious then exit; + FBusy := true; + FNavigating := true; + try + FCurrentScreen.OnPrevious; + FModel := FCurrentScreen.Model; + dec(FCurrentScreenIndex); + // Restore the branch as it was when the previous screen was presented. if FScreenPathBackup.ContainsKey(FCurrentScreenIndex) then begin - FScreenPathBackup[FCurrentScreenIndex].Free; + FScreenPath.Free; + FScreenPath := TList.Create(FScreenPathBackup[FCurrentScreenIndex]); end; - FScreenPathBackup.AddOrSetValue(FCurrentScreenIndex, TList.Create(FScreenPath)); PresentScreen(FScreenInstances[FScreenPath[FCurrentScreenIndex]]); - exit; + Result := true; + finally + FNavigating := false; + FBusy := false; end; +end; - if NOT FinishRequested(message) then - ShowMessage(message); - // else - // TODO: Fire finished event - FHost.Close; +function TWizard.Finish(out message : String) : boolean; +begin + message := ''; + Result := false; + if FBusy then exit; + if (FState <> wsActive) or NOT Assigned(FHost) then begin + Result := CompleteFinish(message); + exit; + end; + FBusy := true; + FNavigating := true; + try + if NOT FCurrentScreen.Validate(message) then exit; + FCurrentScreen.OnNext; + FModel := FCurrentScreen.Model; + FNavigating := false; + FBusy := false; + Result := CompleteFinish(message); + finally + FNavigating := false; + FBusy := false; + end; end; -procedure TWizard.Previous; +function TWizard.CompleteFinish(out message : String) : boolean; begin + message := ''; + Result := false; + if FBusy then exit; + if FState <> wsActive then begin + Result := FState = wsFinished; + exit; + end; CheckStarted; - if not HasPrevious then exit; - FCurrentScreen.OnPrevious; - FModel := FCurrentScreen.Model; // Restore model in case it's a record - dec (FCurrentScreenIndex); + if HasNext then begin + message := 'The wizard still has remaining screens'; + exit; + end; + FBusy := true; + FState := wsFinishing; + try + Result := FinishRequested(message); + if Result then begin + FState := wsFinished; + FFinished := true; + CloseHost(mrOK); + FFinishedEvent.Invoke(self); + end; + finally + if FState = wsFinishing then FState := wsActive; + FBusy := false; + FChangedEvent.Invoke(self); + end; +end; - // Restore backup path when this screen was first displayed - FScreenPath.Free; - FScreenPath := TList.Create(FScreenPathBackup[FCurrentScreenIndex]); - PresentScreen(FScreenInstances[FScreenPath[FCurrentScreenIndex]]); +function TWizard.Cancel(out message : String) : boolean; +begin + message := ''; + Result := false; + if FBusy then exit; + if FState <> wsActive then begin + Result := FState = wsCancelled; + exit; + end; + CheckStarted; + if NOT IsCancellable then exit; + FBusy := true; + FState := wsCancelling; + try + Result := CancelRequested(message) and IsCancellable; + if Result then begin + FState := wsCancelled; + CloseHost(mrCancel); + end; + finally + if FState = wsCancelling then FState := wsActive; + FBusy := false; + FChangedEvent.Invoke(self); + end; end; procedure TWizard.UpdatePath(APathUpdateType: TPathUpdateType; const screens : array of TComponentClass); var i, n : SizeInt; + samePath : boolean; + screenType : TComponentClass; begin + if (FState <> wsActive) or (FBusy and NOT FNavigating) then + raise Exception.Create('Only an active, idle wizard can change screens'); + if (APathUpdateType = ptReplaceAll) and (Length(screens) = 0) then + raise Exception.Create('Wizard needs at least 1 screen'); + // Validate the complete request before changing the live path. + for screenType in screens do + ValidateScreenType(screenType); + if FNavigating and Assigned(FCurrentScreen) then + FModel := FCurrentScreen.Model; + for screenType in screens do + if APathUpdateType <> ptRemoveNext then begin + CreateScreen(screenType); + if FStarted then InitializeScreen(screenType); + end; case APathUpdateType of ptInject: begin - for i := Low(screens) to High(screens) do begin - n := FCurrentScreenIndex + 1 + i - Low(screens); - if FScreenPath[n] <> screens[i] then begin - // only insert screen if not already there - FScreenPath.Insert(n, screens[i]); - CreateScreen(screens[i]); - end; - end; + // An identical contiguous continuation has already been injected. + samePath := FScreenPath.Count - FCurrentScreenIndex - 1 >= Length(screens); + if samePath then + for i := Low(screens) to High(screens) do + if FScreenPath[FCurrentScreenIndex + 1 + i] <> screens[i] then + samePath := false; + if samePath then exit; + for i := Low(screens) to High(screens) do + FScreenPath.Insert(FCurrentScreenIndex + 1 + i, screens[i]); end; ptReplaceAllNext: begin + samePath := FScreenPath.Count - FCurrentScreenIndex - 1 = Length(screens); + if samePath then + for i := Low(screens) to High(screens) do + if FScreenPath[FCurrentScreenIndex + 1 + i] <> screens[i] then + samePath := false; + if samePath then exit; FScreenPath.DeleteRange(FCurrentScreenIndex + 1, FScreenPath.Count - FCurrentScreenIndex - 1); - for i := Low(screens) to High(screens) do begin + for i := Low(screens) to High(screens) do FScreenPath.Add(screens[i]); - CreateScreen(screens[i]); - end; end; ptReplaceAll: begin + samePath := FScreenPath.Count = Length(screens); + if samePath then + for i := Low(screens) to High(screens) do + if FScreenPath[i] <> screens[i] then + samePath := false; + if samePath then exit; FScreenPath.Clear; - for i := Low(screens)to High(screens) do begin + for i := Low(screens)to High(screens) do FScreenPath.Add(screens[i]); - CreateScreen(screens[i]); - end; + if FCurrentScreenIndex >= FScreenPath.Count then + FCurrentScreenIndex := FScreenPath.Count - 1; + ResetPathBackups; + end; + ptRemoveNext: begin + for n := FScreenPath.Count - 1 downto FCurrentScreenIndex + 1 do + for screenType in screens do + if FScreenPath[n] = screenType then begin + FScreenPath.Delete(n); + break; + end; end; end; if Assigned(FHost) then begin - FHost.SetContentSize ( CalculateFitSize ); - FHost.NextText := IIF(NOT HasNext, FinishText, NextText); + FHost.SetContentSize(CalculateFitSize); + if Assigned(FCurrentScreen) and + (FCurrentScreen <> FScreenInstances[FScreenPath[FCurrentScreenIndex]]) then + PresentScreen(FScreenInstances[FScreenPath[FCurrentScreenIndex]]) + else + RefreshNavigation; + end; + FChangedEvent.Invoke(self); +end; + +procedure TWizard.RemoveScreen(AType : TComponentClass); +var + index : Integer; +begin + if (FState <> wsActive) or FBusy then + raise Exception.Create('Only an active, idle wizard can remove screens'); + index := FScreenPath.IndexOf(AType); + if index < 0 then exit; + if FScreenPath.Count = 1 then + raise Exception.Create('The last wizard screen cannot be removed'); + FScreenPath.Delete(index); + if index < FCurrentScreenIndex then dec(FCurrentScreenIndex); + if FCurrentScreenIndex >= FScreenPath.Count then + FCurrentScreenIndex := FScreenPath.Count - 1; + ResetPathBackups; + if Assigned(FHost) then begin + if FCurrentScreen <> FScreenInstances[FScreenPath[FCurrentScreenIndex]] then + PresentScreen(FScreenInstances[FScreenPath[FCurrentScreenIndex]]) + else + RefreshNavigation; + end; + FChangedEvent.Invoke(self); +end; + +function TWizard.GetScreens : TWizardScreenPath; +begin + Result := {$IFNDEF FPC}TWizardScreenPath({$ENDIF}FScreenPath.ToArray{$IFNDEF FPC}){$ENDIF}; +end; + +procedure TWizard.SavePath; +begin + if FScreenPathBackup.ContainsKey(FCurrentScreenIndex) then + FScreenPathBackup[FCurrentScreenIndex].Free; + FScreenPathBackup.AddOrSetValue(FCurrentScreenIndex, TList.Create(FScreenPath)); +end; + +procedure TWizard.ResetPathBackups; +var + backup : TList; + i : Integer; +begin + for backup in FScreenPathBackup.Values do + backup.Free; + FScreenPathBackup.Clear; + for i := 0 to FCurrentScreenIndex do + FScreenPathBackup.Add(i, TList.Create(FScreenPath)); +end; + +procedure TWizard.RefreshNavigation; +begin + if Assigned(FHost) then begin + FHost.HidePrevious := NOT HasPrevious; + if HasNext then + FHost.NextText := NextText + else + FHost.NextText := FinishText; end; end; -procedure TWizard.PresentScreen(screen : __TScreenType); +procedure TWizard.CloseHost(AModalResult : Integer); begin + if Assigned(FHost) then + FHost.ModalResult := AModalResult; +end; + +procedure TWizard.ShowHost; +begin + FHost.ShowModal; +end; + +procedure TWizard.ShowError(const message : String); +begin + ShowMessage(message); +end; + +function TWizard.CloseQueryHandler(out message : String) : boolean; +begin + message := ''; + if FState in [wsFinished, wsCancelled] then + Result := true + else + Result := Cancel(message); +end; + +procedure TWizard.PresentScreen(screen : TWizardForm); +begin + InitializeScreen(TComponentClass(screen.ClassType)); FCurrentScreen := screen; FCurrentScreen.Model := Model; - FHost.HidePrevious := NOT HasPrevious; - FHost.NextText := IIF( NOT HasNext, FinishText, NextText); + RefreshNavigation; FHost.SetContent(screen); screen.OnPresent; + RefreshNavigation; + FChangedEvent.Invoke(self); end; procedure TWizard.CheckStarted; begin - if not FStarted then raise Exception.Create('Wizard has not been started'); + if NOT FStarted or NOT Assigned(FHost) then + raise Exception.Create('Wizard has not been started or its host has closed'); end; procedure TWizard.CheckNotStarted; begin - if FStarted then raise Exception.Create('Wizard has already been started'); + if FStarted then raise Exception.Create('Wizard has already been started'); end; procedure TWizard.NextHandler(sender : TObject); @@ -523,23 +912,22 @@ procedure TWizard.PreviousHandler(sender : TObject); {%region TActionWizard } -constructor TActionWizard.Create(AOwner: TComponent; title, finish: AnsiString; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); +constructor TActionWizard.Create(AOwner: TComponent; title, finish: String; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); begin inherited Create(AOwner, screens); - self.FTitleText := title; - self.FFinishText := finishText; + self.TitleText := title; + self.FinishText := finish; self.FCancelHandler := cancelFunc; self.FFinishHandler := finishFunc; end; - -class procedure TActionWizard.Show(AParent: TForm; title, finish: AnsiString; constref bag : T; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); +class procedure TActionWizard.Show(AParent: TForm; title, finish: String; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} bag : T; const screens : array of TComponentClass; cancelFunc: TActionWizardCancelFunc; finishFunc : TActionWizardFinishFunc); type MyWizard = TActionWizard; var wizard : MyWizard; begin - wizard := MyWizard.Create(nil, title, finish, screens, cancelFunc, finishFunc); + wizard := MyWizard.Create(AParent, title, finish, screens, cancelFunc, finishFunc); try wizard.Start(bag); finally @@ -547,22 +935,22 @@ class procedure TActionWizard.Show(AParent: TForm; title, finish: AnsiString; end; end; -function TActionWizard.CancelRequested(out message : AnsiString) : boolean; +function TActionWizard.CancelRequested(out message : String) : boolean; begin - if Assigned(FCancelHandler) and NOT FFinished then - Result := FCancelHandler(Self.FCurrentScreenIndex, self.Model, message) + message := ''; + if Assigned(FCancelHandler) then + Result := FCancelHandler(Self.CurrentScreenIndex, self.Model, message) else Result := true; end; -function TActionWizard.FinishRequested(out message : AnsiString) : boolean; +function TActionWizard.FinishRequested(out message : String) : boolean; begin + message := ''; if Assigned(FFinishHandler) then Result := FFinishHandler(self.Model, message) else Result := true; - FFinished := true; end; {%endregion} - end. diff --git a/src/pascalcoin_daemon.lpi b/src/pascalcoin_daemon.lpi index dbf7c0da3..83c476b12 100644 --- a/src/pascalcoin_daemon.lpi +++ b/src/pascalcoin_daemon.lpi @@ -17,6 +17,14 @@ + + + + + + + + diff --git a/src/pascalcoin_daemon.pp b/src/pascalcoin_daemon.pp index 34daa4ac2..891ffbf1b 100644 --- a/src/pascalcoin_daemon.pp +++ b/src/pascalcoin_daemon.pp @@ -2,6 +2,7 @@ {$mode objfpc}{$H+} {$define usecthreads} +{$R *.res} uses {$IFDEF UNIX}{$IFDEF UseCThreads} diff --git a/src/pascalcoin_miner.lpi b/src/pascalcoin_miner.lpi index cc7f1f747..f3293a838 100644 --- a/src/pascalcoin_miner.lpi +++ b/src/pascalcoin_miner.lpi @@ -16,6 +16,14 @@ + + + + + + + + diff --git a/src/pascalcoin_miner.pp b/src/pascalcoin_miner.pp index 4c260c743..d0f918416 100644 --- a/src/pascalcoin_miner.pp +++ b/src/pascalcoin_miner.pp @@ -2,6 +2,7 @@ {$mode delphi}{$H+} {$DEFINE UseCThreads} +{$R *.res} {$I config.inc} { Copyright (c) 2017 by Albert Molina @@ -23,7 +24,7 @@ {$IFDEF UNIX}{$IFDEF UseCThreads} cthreads, {$ENDIF}{$ENDIF} - Classes, SysUtils, CustApp, crt, SyncObjs, + Classes, SysUtils, CustApp, crt, UBlockChain, UPoolMinerThreads, UGPUMining, UPoolMining, ULog, UThread, UAccounts, UCrypto, UConst, UTime, UNode, UNetProtocol, USha256, @@ -45,7 +46,8 @@ TPascalMinerApp = class(TCustomApplication) protected FOutputAsLogs : Boolean; FWindow32X1,FWindow32Y1,FWindow32X2,FWindow32Y2: DWord; - FLock : TCriticalSection; + // Log rendering re-enters WriteLine while holding the console writer. + FLock : TReadWriteLock; FPoolMinerThread : TPoolMinerThread; FDeviceThreads : TList; FAppStartTime : TDateTime; @@ -57,7 +59,7 @@ TPascalMinerApp = class(TCustomApplication) end; Const - CT_MINER_VERSION = {$IFDEF PRODUCTION}'5.4'{$ELSE}{$IFDEF TESTNET}'5.4 TESTNET'{$ELSE}ERROR{$ENDIF}{$ENDIF}; + CT_MINER_VERSION = {$IFDEF PRODUCTION}'5.9.5'{$ELSE}{$IFDEF TESTNET}'5.9.5 TESTNET'{$ELSE}ERROR{$ENDIF}{$ENDIF}; CT_Line_DeviceStatus = 3; CT_Line_ConnectionStatus = 4; CT_Line_MinerValues = 7; @@ -148,7 +150,7 @@ procedure TPascalMinerApp.OnFoundNOnce(Sender : TCustomMinerDeviceThread; const procedure TPascalMinerApp.WriteLine(nline: Integer; txt: String); Var i : Integer; begin - FLock.Acquire; + FLock.AcquireWriteLock; try if FOutputAsLogs then begin WriteLn(Format('%s %s',[FormatDateTime('hh:nn:ss.zzz',Now()),txt])); @@ -166,7 +168,7 @@ procedure TPascalMinerApp.WriteLine(nline: Integer; txt: String); write(txt); end; finally - FLock.Release; + FLock.ReleaseWriteLock; end; end; @@ -176,7 +178,7 @@ procedure TPascalMinerApp.OnInThreadNewLog(logtype: TLogType; Time: TDateTime; i,nline : Integer; begin If logtype=ltdebug then exit; - FLock.Acquire; + FLock.AcquireWriteLock; try msg := formatdatetime('hh:nn:ss',now)+' '+CT_LogType[logtype]+' '+logtext; // TODO - test logtype is properly casted/stored/retrieved/accessed. @@ -194,7 +196,7 @@ procedure TPascalMinerApp.OnInThreadNewLog(logtype: TLogType; Time: TDateTime; for i:=1 to CT_MaxLogs do FLastLogs.Delete(0); end; Finally - FLock.Release; + FLock.ReleaseWriteLock; end; end; @@ -427,7 +429,7 @@ procedure TPascalMinerApp.DoRun; end; FLastLogs := TStringList.Create; - FLock := TCriticalSection.Create; + FLock := TReadWriteLock.Create('TPascalMinerApp.Console',lrpSupportsRecursion); Try // quick check parameters ErrorMsg:=CheckOptions('hp:d:s::c:n::t:u::x::l::', 'help platform: device: server: cpu: minername: testmode: user: pwd: logfile: logall: outputlogs:'); diff --git a/src/pascalcoin_wallet_classic.dproj b/src/pascalcoin_wallet_classic.dproj index 1f1d7ae67..35404cf51 100644 --- a/src/pascalcoin_wallet_classic.dproj +++ b/src/pascalcoin_wallet_classic.dproj @@ -66,15 +66,20 @@ 00400000 pascalcoin_wallet_classic Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Data;Datasnap;Web;Soap;Posix;Winapi;$(DCC_Namespace) + 5 + 9 + 5 + 0 + true 1033 - CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= + CompanyName=;FileDescription=;FileVersion=5.9.5.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=5.9.5.0;Comments=;CFBundleName= .\libraries\gnugettext;.\libraries\abstractmem;.\libraries\hashlib4pascal;.\libraries\sphere10;.\libraries\pascalcoin;.\libraries\synapse;.\core;.\gui-classic;.\libraries\cryptolib4pascal;.\libraries\simplebaselib4pascal;.\libraries\regex;$(DCC_UnitSearchPath) .\lib\$(Platform)\$(Config) System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) Debug - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=5.9.5.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=5.9.5.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) $(BDS)\bin\default_app.manifest true $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png @@ -86,7 +91,7 @@ $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) Debug - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=5.9.5.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=5.9.5.0;Comments= $(BDS)\bin\default_app.manifest pascalcoin_wallet_classic.ico @@ -113,7 +118,7 @@ true true - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=5.9.5.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=5.9.5.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) PerMonitor diff --git a/src/pascalcoin_wallet_classic.lpi b/src/pascalcoin_wallet_classic.lpi index 9b237b4f0..c766aec69 100644 --- a/src/pascalcoin_wallet_classic.lpi +++ b/src/pascalcoin_wallet_classic.lpi @@ -20,10 +20,11 @@ - - - - + + + + + diff --git a/src/pascalcoin_wallet_classic.res b/src/pascalcoin_wallet_classic.res index 1450bc9f9..d688384fd 100644 Binary files a/src/pascalcoin_wallet_classic.res and b/src/pascalcoin_wallet_classic.res differ diff --git a/src/pascalcoin_wallet_experimental.dpr b/src/pascalcoin_wallet_experimental.dpr index a4ae4fa7f..a40807426 100644 --- a/src/pascalcoin_wallet_experimental.dpr +++ b/src/pascalcoin_wallet_experimental.dpr @@ -1,35 +1,76 @@ program PascalCoinWalletExperimental; +{$IFDEF FPC} {$mode delphi} +{$ENDIF} {$DEFINE UseCThreads} uses + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} {$IFDEF UNIX}{$IFDEF UseCThreads} cthreads, {$ENDIF}{$ENDIF} - Interfaces, + {$IFDEF FPC}Interfaces,{$ENDIF} sysutils, {$ifdef Use_OpenSSL} UOpenSSL, {$endif} UCrypto, Forms, + UWalletStartup, UUserInterface, UFRMMainForm; -//{$R *.res} +{$IFDEF FPC} +{$R *.res} +{$ELSE} +{$R pascalcoin_wallet_experimental.res} +{$ENDIF} var mainForm : TFRMMainForm; + LArguments : array of String; + LOptions : TWalletStartupOptions; + LError : String; + i : Integer; begin + // Reject invalid arguments and serve help before loading any wallet state. + SetLength(LArguments, ParamCount); + for i := 1 to ParamCount do + LArguments[i - 1] := ParamStr(i); + if not TryParseWalletStartupOptions(LArguments, LOptions, LError) then begin + {$IFDEF FPC} + WriteLn(StdErr, LError); + WriteLn(StdErr, WalletStartupHelp); + {$ELSE} + // A GUI launch has no standard stream; redirected CLI output remains usable. + if GetFileType(GetStdHandle(STD_ERROR_HANDLE)) <> FILE_TYPE_UNKNOWN then begin + WriteLn(ErrOutput, LError); + WriteLn(ErrOutput, WalletStartupHelp); + end; + {$ENDIF} + Halt(1); + end; + if LOptions.ShowHelp then begin + {$IFNDEF FPC} + if GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)) <> FILE_TYPE_UNKNOWN then + {$ENDIF} + WriteLn(WalletStartupHelp); + Halt(0); + end; + TUserInterface.ConfigureStartup(LOptions); // Load application Application.Initialize; - {$IFDEF WINDOWS}{$Warnings OFF} + {$IF Defined(WINDOWS) or Defined(MSWINDOWS)}{$Warnings OFF} Application.MainFormOnTaskBar := True; {$Warnings ON}{$ENDIF} Application.Title := 'Pascal Coin Wallet, Miner & Explorer'; + // Use the same embedded identity for the executable, window and taskbar. + Application.Icon.LoadFromResourceName(HInstance, 'MAINICON'); Application.CreateForm(TFRMMainForm, mainForm); + mainForm.Icon.Assign(Application.Icon); TUserInterface.StartApplication(mainForm); - Application.Run; + if TUserInterface.Started then + Application.Run; end. diff --git a/src/pascalcoin_wallet_experimental.dproj b/src/pascalcoin_wallet_experimental.dproj index 2bc020b31..79df5b45d 100644 --- a/src/pascalcoin_wallet_experimental.dproj +++ b/src/pascalcoin_wallet_experimental.dproj @@ -4,11 +4,12 @@ pascalcoin_wallet_experimental.dpr True Debug - 1 + 3 Application VCL - 18.5 - Win32 + 20.3 + Win64 + pascalcoin_wallet_experimental true @@ -45,6 +46,12 @@ true true + + true + Cfg_2 + true + true + false false @@ -53,15 +60,28 @@ false 00400000 pascalcoin_wallet_experimental - Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + core;gui-experimental;gui-experimental\core.utils;gui-experimental\wizards\wallet;gui-experimental\wizards\operations;gui-classic;libraries\sphere10;libraries\simplebaselib4pascal;libraries\hashlib4pascal;libraries\cryptolib4pascal;libraries\synapse;libraries\pascalcoin;libraries\paszlib;libraries\abstractmem;libraries\regex;libraries\gnugettext;$(DCC_UnitSearchPath) + libraries\generics.collections\inc;$(DCC_IncludePath) + lib\Delphi\$(Platform)\experimental\$(Config) + lib\Delphi\$(Platform)\experimental\$(Config)\res + libraries\sphere10;$(BRCC_IncludePath) + lib\Delphi\$(Platform)\experimental\$(Config)\res;$(DCC_ResourcePath) + Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias) + Winapi;System.Win;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + 5 + 9 + 5 + 0 + true + pascalcoin_wallet_experimental.ico 3081 - CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= + CompanyName=;FileDescription=;FileVersion=5.9.5.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=5.9.5.0;Comments=;CFBundleName= - Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) Debug true - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=5.9.5.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=5.9.5.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName) 1033 $(BDS)\bin\default_app.manifest true @@ -91,14 +111,14 @@ true PerMonitorV2 + + Debug + MainSource - - Cfg_2 - Base - + Base @@ -106,6 +126,10 @@ Cfg_1 Base + + Cfg_2 + Base + Delphi.Personality.12 @@ -118,7 +142,7 @@ True - False + True 12 diff --git a/src/pascalcoin_wallet_experimental.ico b/src/pascalcoin_wallet_experimental.ico new file mode 100644 index 000000000..124dc432b Binary files /dev/null and b/src/pascalcoin_wallet_experimental.ico differ diff --git a/src/pascalcoin_wallet_experimental.lpi b/src/pascalcoin_wallet_experimental.lpi index 2db9b0c5a..437a886b5 100644 --- a/src/pascalcoin_wallet_experimental.lpi +++ b/src/pascalcoin_wallet_experimental.lpi @@ -13,11 +13,20 @@ <ResourceType Value="res"/> + <Icon Value="0"/> </General> <i18n> <EnableI18N LFM="False"/> </i18n> - <BuildModes Count="2"> + <VersionInfo> + <UseVersionInfo Value="True"/> + <MajorVersionNr Value="5"/> + <MinorVersionNr Value="9"/> + <RevisionNr Value="5"/> + <BuildNr Value="0"/> + <StringTable ProductName="PascalCoin" ProductVersion="5.9.5.0" FileVersion="5.9.5.0"/> + </VersionInfo> + <BuildModes Count="3"> <Item1 Name="Default" Default="True"/> <Item2 Name="Debug"> <CompilerOptions> @@ -28,8 +37,8 @@ </Target> <SearchPaths> <IncludeFiles Value="$(ProjOutDir);libraries\generics.collections\inc"/> - <OtherUnitFiles Value="core;gui-experimental;libraries\synapse;libraries\abstractmem;\libraries\regex;libraries\sphere10;libraries\simplebaselib4pascal;libraries\cryptolib4pascal;libraries\hashlib4pascal;libraries\generics.collections;libraries\pascalcoin;gui-experimental\core.utils;gui-experimental\wizards\wallet;gui-experimental\wizards\operations;gui-classic;libraries\paszlib;libraries\regex"/> - <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)"/> + <OtherUnitFiles Value="core;gui-experimental;libraries\synapse;libraries\abstractmem;libraries\sphere10;libraries\simplebaselib4pascal;libraries\cryptolib4pascal;libraries\hashlib4pascal;libraries\generics.collections;libraries\pascalcoin;gui-experimental\core.utils;gui-experimental\wizards\wallet;gui-experimental\wizards\operations;gui-classic;libraries\paszlib;libraries\regex"/> + <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)\experimental\Debug"/> </SearchPaths> <Parsing> <SyntaxOptions> @@ -43,6 +52,7 @@ </CodeGeneration> <Linking> <Debugging> + <GenerateDebugInfo Value="True"/> <DebugInfoType Value="dsDwarf2Set"/> <TrashVariables Value="True"/> <UseExternalDbgSyms Value="True"/> @@ -58,10 +68,53 @@ <ShowHintsForUnusedUnitsInMainSrc Value="True"/> <ShowHintsForSenderNotUsed Value="True"/> </Verbosity> - <CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL -vd"/> + <CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL -dDEBUG"/> </Other> </CompilerOptions> </Item2> + <Item3 Name="Release"> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <Target> + <Filename Value="PascalCoinWalletExperimental"/> + </Target> + <SearchPaths> + <IncludeFiles Value="$(ProjOutDir);libraries\generics.collections\inc"/> + <OtherUnitFiles Value="core;gui-experimental;libraries\synapse;libraries\abstractmem;libraries\sphere10;libraries\simplebaselib4pascal;libraries\cryptolib4pascal;libraries\hashlib4pascal;libraries\generics.collections;libraries\pascalcoin;gui-experimental\core.utils;gui-experimental\wizards\wallet;gui-experimental\wizards\operations;gui-classic;libraries\paszlib;libraries\regex"/> + <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)\experimental\Release"/> + </SearchPaths> + <Parsing> + <SyntaxOptions> + <SyntaxMode Value="Delphi"/> + </SyntaxOptions> + </Parsing> + <CodeGeneration> + <Optimizations> + <OptimizationLevel Value="2"/> + </Optimizations> + </CodeGeneration> + <Linking> + <Debugging> + <GenerateDebugInfo Value="False"/> + <StripSymbols Value="True"/> + <UseExternalDbgSyms Value="False"/> + </Debugging> + <Options> + <Win32> + <GraphicApplication Value="True"/> + </Win32> + </Options> + </Linking> + <Other> + <Verbosity> + <ShowHintsForUnusedUnitsInMainSrc Value="True"/> + <ShowHintsForSenderNotUsed Value="True"/> + </Verbosity> + <CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL -dRELEASE"/> + </Other> + </CompilerOptions> + </Item3> </BuildModes> <PublishOptions> <Version Value="2"/> @@ -77,7 +130,7 @@ <PackageName Value="LCL"/> </Item1> </RequiredPackages> - <Units Count="107"> + <Units Count="113"> <Unit0> <Filename Value="pascalcoin_wallet_experimental.dpr"/> <IsPartOfProject Value="True"/> @@ -654,6 +707,37 @@ <Filename Value="libraries\regex\uregexpr.pas"/> <IsPartOfProject Value="True"/> </Unit106> + <Unit107> + <Filename Value="libraries\sphere10\UCache.pas"/> + <IsPartOfProject Value="True"/> + <UnitName Value="UCache"/> + </Unit107> + <Unit108> + <Filename Value="gui-experimental\UWalletStartup.pas"/> + <IsPartOfProject Value="True"/> + </Unit108> + <Unit109> + <Filename Value="gui-experimental\UWalletStartupTask.pas"/> + <IsPartOfProject Value="True"/> + </Unit109> + <Unit110> + <Filename Value="gui-experimental\UWalletLanguages.pas"/> + <IsPartOfProject Value="True"/> + </Unit110> + <Unit111> + <Filename Value="gui-classic\UFRMOperationsExplorer.pas"/> + <IsPartOfProject Value="True"/> + <ComponentName Value="FRMOperationsExplorer"/> + <HasResources Value="True"/> + <ResourceBaseClass Value="Form"/> + </Unit111> + <Unit112> + <Filename Value="gui-classic\UFRMRPCCalls.pas"/> + <IsPartOfProject Value="True"/> + <ComponentName Value="FRMRPCCalls"/> + <HasResources Value="True"/> + <ResourceBaseClass Value="Form"/> + </Unit112> </Units> </ProjectOptions> <CompilerOptions> @@ -665,7 +749,7 @@ <SearchPaths> <IncludeFiles Value="$(ProjOutDir);libraries\generics.collections\inc"/> <OtherUnitFiles Value="core;gui-experimental;libraries\simplebaselib4pascal;libraries\hashlib4pascal;libraries\cryptolib4pascal;libraries\synapse;libraries\sphere10;libraries\generics.collections;libraries\pascalcoin;gui-experimental\core.utils;gui-experimental\wizards\wallet;gui-experimental\wizards\operations;libraries\paszlib;gui-classic;libraries\abstractmem;libraries\regex"/> - <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)"/> + <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)\experimental\Default"/> </SearchPaths> <Parsing> <SyntaxOptions> @@ -689,7 +773,7 @@ <ShowHintsForUnusedUnitsInMainSrc Value="True"/> <ShowHintsForSenderNotUsed Value="True"/> </Verbosity> - <CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL -vd"/> + <CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL"/> </Other> </CompilerOptions> <Debugging> diff --git a/src/pascalcoin_wallet_experimental.rc b/src/pascalcoin_wallet_experimental.rc new file mode 100644 index 000000000..62840e3fb --- /dev/null +++ b/src/pascalcoin_wallet_experimental.rc @@ -0,0 +1 @@ +MAINICON ICON "pascalcoin_wallet_experimental.ico" diff --git a/src/tests/UCommon.Collections.Tests.pas b/src/tests/UCommon.Collections.Tests.pas index a949c5b0e..25225d153 100644 --- a/src/tests/UCommon.Collections.Tests.pas +++ b/src/tests/UCommon.Collections.Tests.pas @@ -38,6 +38,31 @@ TFilterTests = class(TTestCase) procedure OrManyFilter; end; + TTransformTests = class(TTestCase) + private + FTransformCount, FTransformOffset: Integer; + function ObjectTransform(constref AItem: Integer): String; + procedure CheckTransforms(AFamily: Integer); + published + procedure GlobalTransformsOwnTheirAdapters; + procedure ObjectTransformsOwnTheirAdapters; + procedure NestedTransformsOwnTheirAdapters; + end; + + TPublicCollectionContractTests = class(TTestCase) + published + procedure ConstantPredicatesRespectTheirNames; + procedure CopyUsesExclusiveCount; + procedure CopyAcceptsEmptyAndExactEnd; + procedure CopyRejectsInvalidBounds; + procedure RangeRetainsRequestedItems; + procedure RangeHandlesEmptyAndZeroCount; + procedure RangeClampsToAvailableItems; + procedure SkipTakePreserveSignedDelta; + procedure FilterByDisposesOnlyRejectedObjects; + procedure FilterByPreservesBorrowedObjects; + end; + implementation uses Generics.Defaults, Generics.Collections, UCommon, UCommon.Collections, UMemory, LazLogger; @@ -309,7 +334,366 @@ procedure TFilterTests.TearDown; end; +type + ETransformFixtureError = class(Exception); + +var + GTransformCount, GTransformOffset: Integer; + +function GlobalTransform(constref AItem: Integer): String; +begin + Inc(GTransformCount); + if AItem < 0 then raise ETransformFixtureError.Create('Controlled transform failure'); + Result := 'mapped:' + IntToStr(AItem + GTransformOffset); +end; + +function TTransformTests.ObjectTransform(constref AItem: Integer): String; +begin + Inc(FTransformCount); + if AItem < 0 then raise ETransformFixtureError.Create('Controlled transform failure'); + Result := 'mapped:' + IntToStr(AItem + FTransformOffset); +end; + +procedure TTransformTests.CheckTransforms(AFamily: Integer); +var + LItems, LEmpty: TArray<Integer>; + LList, LEmptyList: TList<Integer>; + LMapped: TArray<String>; + LGlobal: TGlobalTransformFunc<Integer, String>; + LObject: TObjectTransformFunc<Integer, String>; + LNested: TNestedTransformFunc<Integer, String>; + LOffset, i: Integer; + LBefore, LAfter: Int64; + LDisposed: TDisposables; + + function NestedTransform(constref AItem: Integer): String; + begin + Inc(FTransformCount); + if AItem < 0 then raise ETransformFixtureError.Create('Controlled transform failure'); + Result := 'mapped:' + IntToStr(AItem + LOffset); + end; + + function MapArray(const AItems: TArray<Integer>): TArray<String>; + begin + case AFamily of + 0: Result := TListTool<Integer, String>.Transform(AItems, LGlobal); + 1: Result := TListTool<Integer, String>.Transform(AItems, LObject); + 2: Result := TListTool<Integer, String>.Transform(AItems, LNested); + end; + end; + + function MapList(AItems: TList<Integer>): TArray<String>; + begin + case AFamily of + 0: Result := TListTool<Integer, String>.Transform(AItems, LGlobal); + 1: Result := TListTool<Integer, String>.Transform(AItems, LObject); + 2: Result := TListTool<Integer, String>.Transform(AItems, LNested); + end; + end; + + procedure ResetCalls; + begin + GTransformCount := 0; + FTransformCount := 0; + end; + + function Calls: Integer; + begin + if AFamily = 0 then Result := GTransformCount else Result := FTransformCount; + end; + + procedure CheckFailure(AFromList: Boolean); + var LFailed: Boolean; + begin + ResetCalls; + LFailed := False; + try + if AFromList then LMapped := MapList(LList) else LMapped := MapArray(LItems); + except + on E: ETransformFixtureError do LFailed := True; + end; + AssertTrue('The original callback exception must propagate', LFailed); + AssertEquals('Transformation stops at the failing item', 2, Calls); + LMapped := nil; + end; + +begin + GTransformOffset := 20; + FTransformOffset := 30; + LOffset := 40; + if AFamily = 0 then LOffset := GTransformOffset; + if AFamily = 1 then LOffset := FTransformOffset; + LGlobal := GlobalTransform; + LObject := ObjectTransform; + LNested := NestedTransform; + LList := LDisposed.AddObject(TList<Integer>.Create) as TList<Integer>; + LEmptyList := LDisposed.AddObject(TList<Integer>.Create) as TList<Integer>; + LItems := TArray<Integer>.Create(1, 3, 5); + LEmpty := nil; + LList.AddRange(LItems); + + ResetCalls; + LMapped := MapArray(LItems); + AssertEquals(3, Length(LMapped)); + AssertEquals('mapped:' + IntToStr(1 + LOffset), LMapped[0]); + AssertEquals('mapped:' + IntToStr(3 + LOffset), LMapped[1]); + AssertEquals('mapped:' + IntToStr(5 + LOffset), LMapped[2]); + AssertEquals(3, Calls); + ResetCalls; + LMapped := MapList(LList); + AssertEquals(3, Length(LMapped)); + AssertEquals('mapped:' + IntToStr(1 + LOffset), LMapped[0]); + AssertEquals('mapped:' + IntToStr(3 + LOffset), LMapped[1]); + AssertEquals('mapped:' + IntToStr(5 + LOffset), LMapped[2]); + AssertEquals(3, Calls); + + ResetCalls; + LMapped := MapArray(LEmpty); + AssertEquals(0, Length(LMapped)); + LMapped := MapList(LEmptyList); + AssertEquals(0, Length(LMapped)); + AssertEquals('Empty inputs never invoke the callback', 0, Calls); + LMapped := nil; + LBefore := GetFPCHeapStatus.CurrHeapUsed; + for i := 1 to 16 do begin + LMapped := MapArray(LEmpty); + LMapped := nil; + LMapped := MapList(LEmptyList); + LMapped := nil; + end; + LAfter := GetFPCHeapStatus.CurrHeapUsed; + AssertEquals('Completed empty transforms release their delegate adapters', LBefore, LAfter); + + LItems[1] := -1; + LList[1] := -1; + CheckFailure(False); + CheckFailure(True); + LBefore := GetFPCHeapStatus.CurrHeapUsed; + for i := 1 to 16 do begin + CheckFailure(False); + CheckFailure(True); + end; + LAfter := GetFPCHeapStatus.CurrHeapUsed; + AssertEquals('Exceptions release adapters and partially transformed managed strings', LBefore, LAfter); + AssertEquals('Array input remains unchanged after failure', -1, LItems[1]); + AssertEquals('List input remains unchanged after failure', -1, LList[1]); + AssertEquals(3, LList.Count); + + LItems[1] := 3; + LList[1] := 3; + LMapped := MapArray(LItems); + AssertEquals('mapped:' + IntToStr(3 + LOffset), LMapped[1]); + LMapped := MapList(LList); + AssertEquals('mapped:' + IntToStr(3 + LOffset), LMapped[1]); +end; + +procedure TTransformTests.GlobalTransformsOwnTheirAdapters; +begin + CheckTransforms(0); +end; + +procedure TTransformTests.ObjectTransformsOwnTheirAdapters; +begin + CheckTransforms(1); +end; + +procedure TTransformTests.NestedTransformsOwnTheirAdapters; +begin + CheckTransforms(2); +end; + +{ Public count/bounds and ownership contracts used by the README examples. } + +procedure TPublicCollectionContractTests.ConstantPredicatesRespectTheirNames; +var LTrue, LFalse: IPredicate<Integer>; +begin + LTrue := TPredicateTool<Integer>.TruePredicate; + LFalse := TPredicateTool<Integer>.FalsePredicate; + AssertTrue(LTrue.Evaluate(0)); + AssertTrue(LTrue.Evaluate(-1)); + AssertFalse(LFalse.Evaluate(0)); + AssertFalse(LFalse.Evaluate(1)); +end; + +procedure TPublicCollectionContractTests.CopyUsesExclusiveCount; +var LSource, LCopy: TList<Integer>; LScope: TDisposables; +begin + LSource := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + LSource.AddRange([10,20,30,40]); + LCopy := LScope.AddObject(TListTool<Integer>.Copy(LSource,1,2)) as TList<Integer>; + AssertEquals(2,LCopy.Count); + AssertEquals(20,LCopy[0]); + AssertEquals(30,LCopy[1]); + LCopy[0] := 99; + AssertEquals('The source storage remains independent',20,LSource[1]); +end; + +procedure TPublicCollectionContractTests.CopyAcceptsEmptyAndExactEnd; +var LSource, LCopy: TList<Integer>; LScope: TDisposables; +begin + LSource := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + LCopy := LScope.AddObject(TListTool<Integer>.Copy(LSource,0,0)) as TList<Integer>; + AssertEquals(0,LCopy.Count); + LSource.AddRange([10,20,30]); + LCopy := LScope.AddObject(TListTool<Integer>.Copy(LSource,1,2)) as TList<Integer>; + AssertEquals(2,LCopy.Count); + AssertEquals(30,LCopy[1]); + LCopy := LScope.AddObject(TListTool<Integer>.Copy(LSource,3,0)) as TList<Integer>; + AssertEquals(0,LCopy.Count); +end; + +procedure TPublicCollectionContractTests.CopyRejectsInvalidBounds; +var LSource, LCopy: TList<Integer>; LScope: TDisposables; + + procedure CheckRejected(AIndex, ACount: SizeInt); + var LRejected: Boolean; + begin + LRejected := False; + LCopy := nil; + try LCopy := TListTool<Integer>.Copy(LSource,AIndex,ACount); + except on E: EArgumentOutOfRangeException do LRejected := True; end; + LCopy.Free; + AssertTrue('Reject invalid Copy index/count',LRejected); + AssertEquals(2,LSource.Count); + end; + +begin + LSource := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + LSource.AddRange([10,20]); + CheckRejected(-1,1); + CheckRejected(0,-1); + CheckRejected(3,0); + CheckRejected(1,2); + CheckRejected(0,High(SizeInt)); +end; + +procedure TPublicCollectionContractTests.RangeRetainsRequestedItems; +var LItems: TList<Integer>; LScope: TDisposables; LDelta: SizeInt; +begin + LItems := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + LItems.AddRange([10,20,30,40,50]); + LDelta := TListTool<Integer>.Range(LItems,1,2); + AssertEquals(-3,LDelta); + AssertEquals(2,LItems.Count); + AssertEquals(20,LItems[0]); + AssertEquals(30,LItems[1]); +end; + +procedure TPublicCollectionContractTests.RangeHandlesEmptyAndZeroCount; +var LItems: TList<Integer>; LScope: TDisposables; +begin + LItems := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + AssertEquals(0,TListTool<Integer>.Range(LItems,0,0)); + AssertEquals(0,LItems.Count); + LItems.AddRange([10,20,30]); + AssertEquals(-3,TListTool<Integer>.Range(LItems,1,0)); + AssertEquals(0,LItems.Count); +end; + +procedure TPublicCollectionContractTests.RangeClampsToAvailableItems; +var LItems: TList<Integer>; LScope: TDisposables; +begin + LItems := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + LItems.AddRange([10,20,30]); + AssertEquals(-1,TListTool<Integer>.Range(LItems,1,High(SizeInt))); + AssertEquals(2,LItems.Count); + AssertEquals(20,LItems[0]); + AssertEquals(30,LItems[1]); + AssertEquals(-2,TListTool<Integer>.Range(LItems,High(SizeInt),1)); + AssertEquals(0,LItems.Count); + LItems.AddRange([10,20,30]); + AssertEquals(-1,TListTool<Integer>.Range(LItems,-100,2)); + AssertEquals(2,LItems.Count); + AssertEquals(10,LItems[0]); + AssertEquals(20,LItems[1]); +end; + +procedure TPublicCollectionContractTests.SkipTakePreserveSignedDelta; +var LItems: TList<Integer>; LScope: TDisposables; +begin + LItems := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + LItems.AddRange([10,20,30,40]); + AssertEquals(-1,TListTool<Integer>.Skip(LItems,1)); + AssertEquals(20,LItems[0]); + AssertEquals(-1,TListTool<Integer>.Take(LItems,2)); + AssertEquals(2,LItems.Count); + AssertEquals(30,LItems[1]); + AssertEquals(0,TListTool<Integer>.Skip(LItems,Low(SizeInt))); + AssertEquals(-2,TListTool<Integer>.Skip(LItems,High(SizeInt))); + AssertEquals(0,LItems.Count); +end; + +type + TFilterOwnedItem = class + Value: Integer; + Destroyed: PBoolean; + constructor Create(AValue: Integer; ADestroyed: PBoolean); + destructor Destroy; override; + end; + +constructor TFilterOwnedItem.Create(AValue: Integer; ADestroyed: PBoolean); +begin + inherited Create; + Value := AValue; + Destroyed := ADestroyed; +end; + +destructor TFilterOwnedItem.Destroy; +begin + Destroyed^ := True; + inherited; +end; + +function KeepFilterItem(constref AItem: TFilterOwnedItem): Boolean; +begin + Result := AItem.Value>1; +end; + +procedure CheckFilterOwnership(ATest: TTestCase; APolicy: TDisposePolicy); +var LItems: TList<TFilterOwnedItem>; LValues: array[0..2] of TFilterOwnedItem; + LDestroyed: array[0..2] of Boolean; LPredicate: IPredicate<TFilterOwnedItem>; + I: Integer; +begin + FillChar(LDestroyed,SizeOf(LDestroyed),0); + FillChar(LValues,SizeOf(LValues),0); + LItems := TList<TFilterOwnedItem>.Create; + try + for I := 0 to High(LValues) do begin + LValues[I] := TFilterOwnedItem.Create(I+1,@LDestroyed[I]); + LItems.Add(LValues[I]); + end; + LPredicate := TPredicateTool<TFilterOwnedItem>.FromFunc(KeepFilterItem); + ATest.AssertEquals(1,TListTool<TFilterOwnedItem>.FilterBy(LItems,LPredicate,APolicy)); + ATest.AssertEquals(2,LItems.Count); + ATest.AssertEquals(2,LItems[0].Value); + ATest.AssertEquals(3,LItems[1].Value); + ATest.AssertEquals('Only the rejected item follows the disposal policy', + APolicy=idpFreeAndNil,LDestroyed[0]); + ATest.AssertFalse(LDestroyed[1]); + ATest.AssertFalse(LDestroyed[2]); + finally + // Retain original pointers in the fixture so the failing pre-fix test does + // not itself leak the rejected object or double-free a successful removal. + LItems.Free; + for I := 0 to High(LValues) do + if Assigned(LValues[I]) and not LDestroyed[I] then LValues[I].Free; + end; +end; + +procedure TPublicCollectionContractTests.FilterByDisposesOnlyRejectedObjects; +begin + CheckFilterOwnership(Self,idpFreeAndNil); +end; + +procedure TPublicCollectionContractTests.FilterByPreservesBorrowedObjects; +begin + CheckFilterOwnership(Self,idpNone); +end; + initialization + RegisterTest(TCompareTests); RegisterTest(TFilterTests); + RegisterTest(TTransformTests); + RegisterTest(TPublicCollectionContractTests); end. diff --git a/src/tests/UCommon.Tests.pas b/src/tests/UCommon.Tests.pas index 6a8f588ec..595f8fae2 100644 --- a/src/tests/UCommon.Tests.pas +++ b/src/tests/UCommon.Tests.pas @@ -30,6 +30,13 @@ THexStringTests = class(TTestCase) procedure Hex2Bytes_4; end; + TLogMessageTests = class(TTestCase) + published + procedure ExplicitMetadataPreservesEverySeverity; + procedure ExplicitInfoMessageKeepsResultSuccessful; + procedure DefaultTimestampPreservesRequestedSeverity; + end; + TResultTests = class(TTestCase) published procedure Success; @@ -43,6 +50,8 @@ TResultTests = class(TTestCase) TVariantToolTests = class(TTestCase) published + procedure IsNumeric64BitIntegers; + procedure IsNumericRejectsNonNumericValues; procedure NumericEQ; procedure NumericLT; procedure TVariantCompare_1; @@ -57,18 +66,79 @@ TBoxTests = class(TTestCase) procedure TestStringListReleaseOnly; end; + TFileToolTests = class(TTestCase) + private + procedure CheckAppends(const ATexts: array of String; const AExpected: array of Byte); + published + procedure EmptyAppendWritesOnlyCRLF; + procedure ASCIIAppendHasNoBOMOrLengthPrefix; + procedure UnicodeAppendIncludesSupplementaryCharacter; + procedure RepeatedAppendsPreserveExactUTF8Lines; + end; TStatsTests = class(TTestCase) published procedure TestBasic; procedure Test100kData; + procedure VarianceOfThreeKnownValues; + procedure VarianceSmallSampleDomains; + procedure VarianceWithLargeCountDoesNotOverflow; end; implementation uses - Variants, Generics.Defaults, Generics.Collections, UCommon, UMemory, + Variants, Math, Generics.Defaults, Generics.Collections, UCommon, UMemory, UCommon.Collections; +{ TLogMessageTests } + +procedure TLogMessageTests.ExplicitMetadataPreservesEverySeverity; +var + LSeverity: TLogSeverity; + LTimeStamp: TDateTime; + LMessage: TLogMessage; +begin + LTimeStamp := EncodeDate(2001, 2, 3) + EncodeTime(4, 5, 6, 7); + for LSeverity := Low(TLogSeverity) to High(TLogSeverity) do begin + LMessage := TLogMessage.From('Retained text', LSeverity, LTimeStamp); + AssertEquals('Retained text', LMessage.Text); + AssertEquals(Ord(LSeverity), Ord(LMessage.Severity)); + AssertTrue('The explicit timestamp must not be replaced with the current time', + LMessage.TimeStamp = LTimeStamp); + end; +end; + +procedure TLogMessageTests.ExplicitInfoMessageKeepsResultSuccessful; +var + LResult: TResult; + LTimeStamp: TDateTime; +begin + LTimeStamp := EncodeDate(2001, 2, 3); + LResult := TResult.Success; + LResult.Add(TLogMessage.From('Imported three rows', lsInfo, LTimeStamp)); + AssertTrue('An informational message is not a failure', LResult.IsSuccess); + AssertEquals(1, Length(LResult.Messages)); + AssertEquals(Ord(lsInfo), Ord(LResult.Messages[0].Severity)); + AssertTrue(LResult.Messages[0].TimeStamp = LTimeStamp); +end; + +procedure TLogMessageTests.DefaultTimestampPreservesRequestedSeverity; +var + LSeverity: TLogSeverity; + LBefore, LAfter: TDateTime; + LMessage: TLogMessage; +begin + for LSeverity := Low(TLogSeverity) to High(TLogSeverity) do begin + LBefore := TDateTime.NowUtc; + LMessage := TLogMessage.From('Current message', LSeverity); + LAfter := TDateTime.NowUtc; + AssertEquals(Ord(LSeverity), Ord(LMessage.Severity)); + AssertTrue((LMessage.TimeStamp >= LBefore) and (LMessage.TimeStamp <= LAfter)); + end; + LMessage := TLogMessage.From('Default severity'); + AssertEquals(Ord(lsError), Ord(LMessage.Severity)); +end; + { TSortedHashTests } procedure TSortedHashTests.SortedHashSet_Large; @@ -325,6 +395,35 @@ procedure TResultTests.Complex; { TVariantToolTests } +procedure TVariantToolTests.IsNumeric64BitIntegers; +var + LValue: Variant; + LUnsigned: UInt64; +begin + LValue := Low(Int64); + AssertEquals(varInt64, VarType(LValue)); + AssertTrue(TVariantTool.IsNumeric(LValue)); + LValue := High(Int64); + AssertTrue(TVariantTool.IsNumeric(LValue)); + + // Recognize the type without narrowing a value which cannot fit in Int64. + LUnsigned := UInt64(High(Int64)) + 1; + LValue := LUnsigned; + AssertEquals({$IFDEF FPC}varQWord{$ELSE}varUInt64{$ENDIF}, VarType(LValue)); + AssertTrue(TVariantTool.IsNumeric(LValue)); + LValue := High(UInt64); + AssertTrue(TVariantTool.IsNumeric(LValue)); + AssertTrue(UInt64(LValue) = High(UInt64)); +end; + +procedure TVariantToolTests.IsNumericRejectsNonNumericValues; +begin + AssertFalse(TVariantTool.IsNumeric(Null)); + AssertFalse(TVariantTool.IsNumeric(Unassigned)); + AssertFalse(TVariantTool.IsNumeric('16537985')); + AssertFalse(TVariantTool.IsNumeric(VarArrayOf([1, 2]))); +end; + procedure TVariantToolTests.NumericEQ; var lbyte : byte; @@ -507,6 +606,75 @@ TTestRecord = record strList.Free; end; +{ TFileToolTests } + +function FileExampleUnicodeText: String; +var LText: UnicodeString; + {$IFDEF FPC}LUTF8: UTF8String;{$ENDIF} +begin + LText := #$0041#$03A9#$6F22#$D83D#$DE80; + {$IFDEF FPC} + // Emulate Lazarus UTF-8 String bytes even in a console using Windows ANSI. + // An ordinary assignment would transcode the fixture before AppendText runs. + LUTF8 := UTF8Encode(LText); + SetLength(Result, Length(LUTF8)); + Move(LUTF8[1], Result[1], Length(LUTF8)); + {$ELSE} + Result := LText; + {$ENDIF} +end; + +procedure TFileToolTests.CheckAppends(const ATexts: array of String; const AExpected: array of Byte); +var + LGuid: TGUID; + LFileName: String; + LFile: TFileStream; + LActual: TBytes; + I: Integer; +begin + CreateGUID(LGuid); + LFileName := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'Sphere10UTF8-' + GUIDToString(LGuid) + '.tmp'; + try + for I := 0 to High(ATexts) do TFileTool.AppendText(LFileName, ATexts[I]); + LFile := TFileStream.Create(LFileName, fmOpenRead or fmShareDenyWrite); + try + AssertEquals('Exact byte length, without a BOM or length prefix', + Int64(Length(AExpected)), LFile.Size); + SetLength(LActual, Length(AExpected)); + if Length(LActual) > 0 then LFile.ReadBuffer(LActual[0], Length(LActual)); + finally + LFile.Free; + end; + for I := 0 to High(AExpected) do + AssertEquals('UTF-8 byte ' + IntToStr(I), Integer(AExpected[I]), Integer(LActual[I])); + finally + if FileExists(LFileName) then DeleteFile(LFileName); + end; +end; + +procedure TFileToolTests.EmptyAppendWritesOnlyCRLF; +begin + CheckAppends([''], [$0D, $0A]); +end; + +procedure TFileToolTests.ASCIIAppendHasNoBOMOrLengthPrefix; +begin + CheckAppends(['ABC'], [$41, $42, $43, $0D, $0A]); +end; + +procedure TFileToolTests.UnicodeAppendIncludesSupplementaryCharacter; +begin + CheckAppends([FileExampleUnicodeText], + [$41, $CE, $A9, $E6, $BC, $A2, $F0, $9F, $9A, $80, $0D, $0A]); +end; + +procedure TFileToolTests.RepeatedAppendsPreserveExactUTF8Lines; +begin + CheckAppends([FileExampleUnicodeText, '', 'END'], + [$41, $CE, $A9, $E6, $BC, $A2, $F0, $9F, $9A, $80, $0D, $0A, + $0D, $0A, $45, $4E, $44, $0D, $0A]); +end; { TStatsTests } procedure TStatsTests.TestBasic; @@ -526,9 +694,8 @@ procedure TStatsTests.TestBasic; AssertEquals(4, LStats.Mean); AssertEquals(6, LStats.PopulationVariance); AssertEquals(RoundEx(2.4495, 4), RoundEx(LStats.PopulationStandardDeviation, 4)); - // note: inaccurate estimate, since using running algo - AssertEquals(RoundEx(9.3750, 4), LStats.SampleVariance); - AssertEquals(RoundEx(3.0619, 4), RoundEx(LStats.SampleStandardDeviation, 4)); + AssertEquals(7.5, LStats.SampleVariance); + AssertEquals(2.7386, RoundEx(LStats.SampleStandardDeviation, 4)); end; @@ -544,11 +711,48 @@ procedure TStatsTests.Test100kData; AssertEquals(100000, LStats.Maximum); end; +procedure TStatsTests.VarianceOfThreeKnownValues; +var LStats: TStatistics; +begin + LStats.Reset; + LStats.AddDatum(1); + LStats.AddDatum(2); + LStats.AddDatum(3); + AssertEquals(2.0 / 3.0, LStats.PopulationVariance, EPSILON); + AssertEquals(1.0, LStats.SampleVariance, EPSILON); +end; + +procedure TStatsTests.VarianceSmallSampleDomains; +var LStats: TStatistics; +begin + LStats.Reset; + AssertTrue('Empty population variance is undefined', IsNaN(LStats.PopulationVariance)); + AssertTrue('Empty sample variance is undefined', IsNaN(LStats.SampleVariance)); + LStats.AddDatum(1); + AssertEquals(0.0, LStats.PopulationVariance); + AssertTrue('One value cannot estimate sample variance', IsNaN(LStats.SampleVariance)); + LStats.AddDatum(3); + AssertEquals(1.0, LStats.PopulationVariance); + AssertEquals(2.0, LStats.SampleVariance); +end; + +procedure TStatsTests.VarianceWithLargeCountDoesNotOverflow; +var LStats: TStatistics; +begin + LStats.Reset; + LStats.AddDatum(1, 100000); + AssertEquals(0.0, LStats.PopulationVariance); + AssertEquals(0.0, LStats.SampleVariance); +end; + initialization RegisterTest(TSortedHashTests); RegisterTest(THexStringTests); + RegisterTest(TLogMessageTests); RegisterTest(TResultTests); RegisterTest(TVariantToolTests); + RegisterTest(TBoxTests); + RegisterTest(TFileToolTests); RegisterTest(TStatsTests); end. diff --git a/src/tests/UDisposables.Scope.Checks.pas b/src/tests/UDisposables.Scope.Checks.pas new file mode 100644 index 000000000..b8cb7fac3 --- /dev/null +++ b/src/tests/UDisposables.Scope.Checks.pas @@ -0,0 +1,399 @@ +unit UDisposables.Scope.Checks; + +{ Copyright (c) 2026 Herman Schoenfeld <https://www.sphere10.com> + Shared Delphi/FPC checks for TDisposables managed ownership. } + +{$IFDEF FPC}{$mode Delphi}{$H+}{$ENDIF} + +interface + +const DisposablesScopeCheckCount = 15; + +procedure CheckNormalReturn; +procedure CheckEarlyExit; +procedure CheckExceptionUnwind; +procedure CheckInnerCatchRetainsBag; +procedure CheckEmptyBags; +procedure CheckGrowthReverseOrder; +procedure CheckRemoveObjectOwnership; +procedure CheckClassFieldAutomatic; +procedure CheckNestedRecordAutomatic; +procedure CheckMixedResources; +procedure CheckReallocMemPreservesData; +procedure CheckRemoveMemOwnership; +procedure CheckExplicitFreeMem; +procedure CheckUnregisteredReallocDoesNotTakeOwnership; +procedure CheckCleanupFailureContinues; +procedure RunDisposablesScopeChecks; + +implementation + +uses SysUtils, UMemory; + +type + ECheckedDisposeFailure = class(Exception); + TTrackedObject = class + private + FID: Integer; + FFail: Boolean; + public + constructor Create(AID: Integer; AFail: Boolean = False); + destructor Destroy; override; + end; + TBagOwner = class + public + Bag: TDisposables; + end; + TBagRecord = record + Bag: TDisposables; + end; + TMemoryObserver = class(TTrackedObject) + private + FData: PByte; + public + constructor Create(AData: PByte); + destructor Destroy; override; + end; + +var + LiveCount: Integer; + Events: array of Integer; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create(AMessage); +end; + +procedure BeginCheck; +begin + Require(LiveCount = 0, 'Previous check left a live object'); + SetLength(Events, 0); +end; + +procedure ExpectCount(ACount: Integer); +begin + Require(Length(Events) = ACount, 'Unexpected cleanup count'); +end; + +constructor TTrackedObject.Create(AID: Integer; AFail: Boolean); +begin + inherited Create; + FID := AID; + FFail := AFail; + Inc(LiveCount); +end; + +destructor TTrackedObject.Destroy; +var LIndex: Integer; +begin + try + LIndex := Length(Events); + SetLength(Events, LIndex + 1); + Events[LIndex] := FID; + Dec(LiveCount); + try + if FFail then + raise ECheckedDisposeFailure.CreateFmt('dispose %d', [FID]); + finally + inherited; + end; + except + // This deliberately failing test destructor owns its instance cleanup. + // A raising destructor skips its normal instance-release epilogue. + FreeInstance; + raise; + end; +end; + +constructor TMemoryObserver.Create(AData: PByte); +begin + inherited Create(1); + FData := AData; +end; + +destructor TMemoryObserver.Destroy; +begin + try + Require(FData^ = 42, 'Object cleanup must precede its earlier registered memory'); + finally + inherited; + end; +end; + +procedure CheckNormalReturn; + procedure Run; + var Bag: TDisposables; + begin + Bag.AddObject(TTrackedObject.Create(1)); + Bag.AddObject(TTrackedObject.Create(2)); + Require(LiveCount = 2, 'Normal scope cleaned up before return'); + end; +begin + BeginCheck; + Run; + ExpectCount(2); + Require(LiveCount = 0, 'Normal return leaked objects'); +end; + +procedure CheckEarlyExit; + procedure Run; + var Bag: TDisposables; + begin + Bag.AddObject(TTrackedObject.Create(1)); + Exit; + end; +begin + BeginCheck; + Run; + ExpectCount(1); + Require(LiveCount = 0, 'Exit skipped bag cleanup'); +end; + +procedure CheckExceptionUnwind; + procedure Run; + var Bag: TDisposables; + begin + Bag.AddObject(TTrackedObject.Create(1)); + raise EAbort.Create('body failure'); + end; +var Caught: Boolean; +begin + BeginCheck; + Caught := False; + try Run; + except + on E: EAbort do begin + Caught := True; + Require(E.Message = 'body failure', 'Bag changed the body exception'); + end; + end; + Require(Caught, 'Body exception did not propagate'); + ExpectCount(1); + Require(LiveCount = 0, 'Exception unwinding skipped bag cleanup'); +end; + +procedure CheckInnerCatchRetainsBag; + procedure Run; + var Bag: TDisposables; + begin + Bag.AddObject(TTrackedObject.Create(1)); + try raise EAbort.Create('caught inside scope'); + except on E: EAbort do ; end; + ExpectCount(0); + Bag.AddObject(TTrackedObject.Create(2)); + Require(LiveCount = 2, 'Inner handler prematurely released the routine bag'); + end; +begin + BeginCheck; + Run; + ExpectCount(2); +end; + +procedure CheckEmptyBags; + procedure Run; + var Bag: TDisposables; + begin + Bag.RemoveObject(nil); + Bag.AddObject(nil); + Bag.RemoveMem(nil); + end; +begin + BeginCheck; + Run; + ExpectCount(0); +end; + +procedure CheckGrowthReverseOrder; + procedure Run; + var Bag: TDisposables; i: Integer; + begin + for i := 1 to 65 do Bag.AddObject(TTrackedObject.Create(i)); + end; +var i: Integer; +begin + BeginCheck; + Run; + ExpectCount(65); + for i := 0 to 64 do + Require(Events[i] = 65 - i, 'Growth changed reverse registration order'); +end; + +procedure CheckRemoveObjectOwnership; +var Transferred: TTrackedObject; + procedure Run; + var Bag: TDisposables; + begin + Transferred := Bag[TTrackedObject.Create(1)] as TTrackedObject; + Bag.RemoveObject(Transferred); + end; +begin + BeginCheck; + Transferred := nil; + try + Run; + ExpectCount(0); + Require(LiveCount = 1, 'RemoveObject disposed a transferred resource'); + finally + Transferred.Free; + end; + ExpectCount(1); +end; + +procedure CheckClassFieldAutomatic; +var Owner: TBagOwner; +begin + BeginCheck; + Owner := TBagOwner.Create; + Owner.Bag.AddObject(TTrackedObject.Create(1)); + Owner.Bag.AddObject(TTrackedObject.Create(2)); + Owner.Free; + ExpectCount(2); + Require((Events[0] = 2) and (Events[1] = 1), 'Class field did not clean up automatically'); +end; + +procedure CheckNestedRecordAutomatic; + procedure Run; + var Container: TBagRecord; + begin + Container.Bag.AddObject(TTrackedObject.Create(1)); + end; +begin + BeginCheck; + Run; + ExpectCount(1); +end; + +procedure CheckMixedResources; + procedure Run; + var Bag: TDisposables; Data: Pointer; + begin + System.GetMem(Data, 8); + Bag.AddMem(Data); + PByte(Data)^ := 42; + Bag.AddObject(TMemoryObserver.Create(PByte(Data))); + end; +begin + BeginCheck; + Run; + ExpectCount(1); +end; + +procedure CheckReallocMemPreservesData; + procedure Run; + var Bag: TDisposables; Data: Pointer; i: Integer; + begin + Bag.GetMem(Data, 8); + for i := 0 to 7 do PByte(NativeUInt(Data) + NativeUInt(i))^ := i + 1; + Bag.ReallocMem(Data, 128); + for i := 0 to 7 do + Require(PByte(NativeUInt(Data) + NativeUInt(i))^ = i + 1, 'Reallocation lost memory contents'); + PByte(Data)^ := 42; + Bag.ReallocMem(Data, 4); + Require(PByte(Data)^ = 42, 'Shrinking lost memory contents'); + Bag.ReallocMem(Data, 0); + Require(Data = nil, 'Zero-size reallocation did not release registered memory'); + end; +begin + BeginCheck; + Run; + ExpectCount(0); +end; + +procedure CheckRemoveMemOwnership; +var Data: Pointer; + procedure Run; + var Bag: TDisposables; + begin + Bag.GetMem(Data, 8); + PByte(Data)^ := 42; + Bag.RemoveMem(Data); + end; +begin + BeginCheck; + Data := nil; + try + Run; + Require(PByte(Data)^ = 42, 'RemoveMem did not transfer memory to the caller'); + finally + System.FreeMem(Data); + end; +end; + +procedure CheckExplicitFreeMem; + procedure Run; + var Bag: TDisposables; Data, Original: Pointer; + begin + Bag.GetMem(Data, 8); + Original := Data; + Bag.FreeMem(Data); + Require(Data = Original, 'FreeMem changed the established raw-pointer behavior'); + Data := nil; + end; +begin + BeginCheck; + Run; + ExpectCount(0); +end; + +procedure CheckUnregisteredReallocDoesNotTakeOwnership; +var Bag: TDisposables; Data, Original: Pointer; +begin + BeginCheck; + System.GetMem(Data, 8); + try + Original := Data; + PByte(Data)^ := 42; + Bag.ReallocMem(Data, 128); + Require(Data = Original, 'ReallocMem changed unregistered memory'); + Require(PByte(Data)^ = 42, 'Unregistered memory contents changed'); + finally + System.FreeMem(Data); + end; +end; + +procedure CheckCleanupFailureContinues; + procedure Run; + var Bag: TDisposables; + begin + Bag.AddObject(TTrackedObject.Create(1)); + Bag.AddObject(TTrackedObject.Create(2, True)); + Bag.AddObject(TTrackedObject.Create(3, True)); + end; +var Caught: Boolean; +begin + BeginCheck; + Caught := False; + try Run; + except + on E: ECheckedDisposeFailure do begin + Caught := True; + Require(E.Message = 'dispose 3', 'Cleanup did not preserve the first failure'); + end; + end; + Require(Caught, 'Cleanup failure did not propagate'); + ExpectCount(3); + Require((Events[0] = 3) and (Events[1] = 2) and (Events[2] = 1), 'Failure stopped reverse cleanup'); + Require(LiveCount = 0, 'Failure left objects undisposed'); +end; + +procedure RunDisposablesScopeChecks; +begin + CheckNormalReturn; + CheckEarlyExit; + CheckExceptionUnwind; + CheckInnerCatchRetainsBag; + CheckEmptyBags; + CheckGrowthReverseOrder; + CheckRemoveObjectOwnership; + CheckClassFieldAutomatic; + CheckNestedRecordAutomatic; + CheckMixedResources; + CheckReallocMemPreservesData; + CheckRemoveMemOwnership; + CheckExplicitFreeMem; + CheckUnregisteredReallocDoesNotTakeOwnership; + CheckCleanupFailureContinues; +end; + +end. diff --git a/src/tests/UMemory.Tests.pas b/src/tests/UMemory.Tests.pas index 5fdfd65fc..065a945f3 100644 --- a/src/tests/UMemory.Tests.pas +++ b/src/tests/UMemory.Tests.pas @@ -15,11 +15,33 @@ TDisposablesTest = class(TTestCase) published procedure TestNestedScope; procedure TestRecordScope; + procedure TestBagGrowsPastInitialCapacity; + procedure TestRemovedObjectTransfersOwnership; + procedure TestExceptionDisposesScope; + end; + + TDisposablesScopeTests = class(TTestCase) + published + procedure NormalReturn; + procedure EarlyExit; + procedure ExceptionUnwind; + procedure InnerCatchRetainsBag; + procedure EmptyBags; + procedure GrowthReverseOrder; + procedure RemoveObjectOwnership; + procedure ClassFieldAutomatic; + procedure NestedRecordAutomatic; + procedure MixedResources; + procedure ReallocMemPreservesData; + procedure RemoveMemOwnership; + procedure ExplicitFreeMem; + procedure UnregisteredReallocDoesNotTakeOwnership; + procedure CleanupFailureContinues; end; implementation -uses Generics.Defaults, Generics.Collections, UCommon.Collections, UMemory, LazLogger; +uses UDisposables.Scope.Checks, Generics.Defaults, Generics.Collections, UCommon.Collections, UMemory, LazLogger; type TTestObject = class(TObject) @@ -67,6 +89,56 @@ procedure TDisposablesTest.TestRecordScope; Self.AssertEquals('Memory leak', 0, TTestObject.Instances); end; +procedure TDisposablesTest.TestBagGrowsPastInitialCapacity; + procedure RunTest; + var + scoped: TDisposables; + i: Integer; + begin + for i := 1 to 65 do + scoped.AddObject(TTestObject.Create); + AssertEquals(65, TTestObject.Instances); + end; +begin + RunTest; + AssertEquals('Every registered object must be disposed after bag growth', 0, TTestObject.Instances); +end; + +procedure TDisposablesTest.TestRemovedObjectTransfersOwnership; + function CreateTransferred: TTestObject; + var scoped: TDisposables; + begin + Result := scoped.AddObject(TTestObject.Create) as TTestObject; + scoped.RemoveObject(Result); + end; +var + transferred: TTestObject; +begin + transferred := CreateTransferred; + try + AssertEquals(1, TTestObject.Instances); + finally + transferred.Free; + end; + AssertEquals(0, TTestObject.Instances); +end; + +procedure TDisposablesTest.TestExceptionDisposesScope; + procedure RunTest; + var scoped: TDisposables; + begin + scoped.AddObject(TTestObject.Create); + raise EAbort.Create('Fixture exception'); + end; +begin + try + RunTest; + except + on E: EAbort do ; + end; + AssertEquals('Exception unwinding must dispose registered objects', 0, TTestObject.Instances); +end; + class constructor TTestObject.Create; begin Instances := 0; @@ -84,8 +156,84 @@ destructor TTestObject.Destroy; Dec(Instances); end; +procedure TDisposablesScopeTests.NormalReturn; +begin + CheckNormalReturn; +end; + +procedure TDisposablesScopeTests.EarlyExit; +begin + CheckEarlyExit; +end; + +procedure TDisposablesScopeTests.ExceptionUnwind; +begin + CheckExceptionUnwind; +end; + +procedure TDisposablesScopeTests.InnerCatchRetainsBag; +begin + CheckInnerCatchRetainsBag; +end; + +procedure TDisposablesScopeTests.EmptyBags; +begin + CheckEmptyBags; +end; + +procedure TDisposablesScopeTests.GrowthReverseOrder; +begin + CheckGrowthReverseOrder; +end; + +procedure TDisposablesScopeTests.RemoveObjectOwnership; +begin + CheckRemoveObjectOwnership; +end; + +procedure TDisposablesScopeTests.ClassFieldAutomatic; +begin + CheckClassFieldAutomatic; +end; + +procedure TDisposablesScopeTests.NestedRecordAutomatic; +begin + CheckNestedRecordAutomatic; +end; + +procedure TDisposablesScopeTests.MixedResources; +begin + CheckMixedResources; +end; + +procedure TDisposablesScopeTests.ReallocMemPreservesData; +begin + CheckReallocMemPreservesData; +end; + +procedure TDisposablesScopeTests.RemoveMemOwnership; +begin + CheckRemoveMemOwnership; +end; + +procedure TDisposablesScopeTests.ExplicitFreeMem; +begin + CheckExplicitFreeMem; +end; + +procedure TDisposablesScopeTests.UnregisteredReallocDoesNotTakeOwnership; +begin + CheckUnregisteredReallocDoesNotTakeOwnership; +end; + +procedure TDisposablesScopeTests.CleanupFailureContinues; +begin + CheckCleanupFailureContinues; +end; + initialization RegisterTest(TDisposablesTest); + RegisterTest(TDisposablesScopeTests); end. diff --git a/src/tests/experimental/ExperimentalWalletTests.lpr b/src/tests/experimental/ExperimentalWalletTests.lpr new file mode 100644 index 000000000..41d2f5314 --- /dev/null +++ b/src/tests/experimental/ExperimentalWalletTests.lpr @@ -0,0 +1,84 @@ +program ExperimentalWalletTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. } + +{$mode delphi} + +uses + // Heap tracing must precede POSIX thread-variable relocation. + heaptrc, {$IFDEF UNIX}cthreads,{$ENDIF} + Interfaces, Forms, SysUtils, fpcunit, testregistry, plaintestreport, + UBufferedAdoptionTests, + UWalletStartupTests, UPendingRestoreTests, UPendingCleanupTests, UOperationPreparationTests, UChangeInfoBatchTests, UOperationWizardTests, + UWalletRestoreTests, UWalletViewTests, UWalletAccountEnumerationTests, UWalletHistoryTests, + UWalletSnapshotTests, UWalletMempoolReadSafetyTests, UWalletProgressDisplayTests, UThreadConcurrencyTests, UCoreLockMigrationTests, ULogGuiHandoffTests, + UNodeSynchronizationTests, + UChangeAccountInfoReplayTests, + UBlockSanitizeTests, + UPendingCatchupTests, + UPendingSaveLifecycleTests, UCheckpointStorageTests, UAbstractMemStorageFailureTests, + UAccountCachePolicyTests, + UDownloadWorkerLifetimeTests, + ULegacyConnectionLifetimeTests, + UOrderedCardinalTests, + UWalletExplorerTests, UWalletExplorerSchedulingTests, + UWalletPagingTests, + UWalletRenderTests, UWalletGridRefreshTests, + UWalletMiningTests, + UWalletLanguageTests, UWalletParityTests, UWalletInfoTests, + UCache.Tests, UWizard.Tests, UCommon.Data.Tests, UCommon.UI.Tests, UVisualGrid.Tests, + UVisualGrid.Refresh.Tests, UFramework.UI.Integration.Tests, UMemory.Tests, UConcurrency.Tests, + UScope.Tests, UReadWriteScope.Tests, UCommon.Tests, UCommon.Collections.Tests, UStreams.Tests, + UMemory, UCommon; + +type + TProgressResultsWriter = class(TPlainResultsWriter) + public + procedure StartTest(ATest: TTest); override; + procedure EndTest(ATest: TTest); override; + end; + +procedure TProgressResultsWriter.StartTest(ATest: TTest); +begin + // A blocked native case leaves its name before the final report is written. + TFileTool.AppendText(ParamStr(1) + '.progress.log', + 'START ' + ATest.TestSuiteName + '.' + ATest.TestName); + inherited StartTest(ATest); +end; + +procedure TProgressResultsWriter.EndTest(ATest: TTest); +begin + TFileTool.AppendText(ParamStr(1) + '.progress.log', + 'END ' + ATest.TestSuiteName + '.' + ATest.TestName); + inherited EndTest(ATest); +end; + +procedure RunTests; +var + results: TTestResult; + writer: TPlainResultsWriter; + disposed: TDisposables; +begin + // Reverse-order disposal follows FPCUnit's result-before-listener lifetime. + writer := disposed.AddObject(TProgressResultsWriter.Create(nil)) as TPlainResultsWriter; + results := disposed.AddObject(TTestResult.Create) as TTestResult; + writer.FileName := ParamStr(1); + results.AddListener(writer); + GetTestRegistry.Run(results); + writer.WriteResult(results); + if (results.NumberOfErrors = 0) and (results.NumberOfFailures = 0) then + ExitCode := 0 + else + ExitCode := 1; +end; + +begin + if ParamCount <> 1 then + Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + Application.Initialize; + RunTests; +end. diff --git a/src/tests/experimental/README.md b/src/tests/experimental/README.md new file mode 100644 index 000000000..7acf2c13f --- /dev/null +++ b/src/tests/experimental/README.md @@ -0,0 +1,254 @@ +# Experimental wallet regression tests + +This folder holds FPCUnit fixtures and native Delphi and FPC probes for the +experimental wallet, the core and the shared framework units. + +## Run all tests on Windows + +Build the experimental wallet, then run from the repository root: + +```powershell +& src/tests/experimental/RunAllWalletTests.ps1 +``` + +- `-FpcPath` and `-LazarusDirectory` select FPC and Lazarus. By default the + runner finds them on `PATH`. +- `-OutputDirectory` sets a new folder for compiler outputs and reports. The + default is `lib/all-<run-id>` in this folder. +- `-WalletExecutable` selects the built shipping wallet for the startup checks. + The default is the Lazarus build, `src/PascalCoinWalletExperimental.exe`. + +The runner needs: + +- a Windows x64 FPC and Lazarus with the Win32 widgetset +- Python 3 on `PATH`, to recompute the proof-of-work vectors +- a Full HD desktop for the Advanced Operation checks at 150% scale + +Native checks run on private Windows desktops through the private-desktop +broker. The runner executes these sixteen phases in order, each in its own +process: + +| Phase | Covers | +|---|---| +| `unit` | Every registered [unit fixture](#fpcunit-fixtures) except `UWalletGridIntegrationTests` | +| `log-memo-batch` | Experimental [log-memo checks](native/log-memo-batch/README.md) | +| `classic-log-memo-batch` | Classic log-memo checks | +| `grid-integration` | The main-grid integration fixture, `UWalletGridIntegrationTests` | +| `grid-native` | The thirteen [grid refresh demo](native/grid-refresh/README.md) groups | +| `network-toggle` | The connectivity toggle and deferred refresh | +| `buffered-backlog` | [Received block backlog](native/network-backlog/README.md) scenarios | +| `startup` | [Startup and shutdown](native/README.md) | +| `download-stop` | [Safebox download shutdown](native/download-stop/README.md) | +| `safebox-recovery` | [Safebox recovery](native/safebox-recovery/README.md) | +| `classic` | [Classic](native/classic/README.md) key selection, filtering and shutdown | +| `connection-restart` | [Connection restart](native/connection-restart/README.md) | +| `pending-protocol` | All nine [pending-protocol](native/pending-protocol/README.md) cases | +| `buffered-frames` | [Buffered block frames](native/network-backlog/BufferedBlockFrameProbe.md) | +| `node-sync` | Main-form synchronization, storage failures and grid cancellation on a real node | +| `mempool-lock` | Mempool lock upgrades | + +Each phase writes to the `<phase>` subfolder and `<phase>-runner.log` in the +output folder. `all-tests.json` records each phase's exit code. The runner runs +every phase and exits 1 if any failed. Every process must exit naturally, and +each heap-traced process must leave a clean heap report. + +The runner stops before the first phase if a registered fixture unit is missing +from both unit runners, `ExperimentalWalletTests.lpr` and +`ui/WalletGridRefreshTests.lpr`. `test-coverage.json` lists the native phases +and the shared helpers that the unit fixtures already cover. Delphi-only probes +and the opt-in [large-chain checks](#large-chain-checks) have separate inputs +and commands. + +### CI + +CI builds both wallets on Windows, Linux and macOS with the stable Lazarus +release and runs the [portable lock tests](#portable-lock-tests) on each. On +Windows it also runs the `operation-messages` demo group at the default +resolution, switches the display to Full HD with `native/SetCiDisplayMode.ps1` +and runs this runner. That script changes the display only on GitHub-hosted +runners. Locally, run it with `-InspectOnly` for diagnostics. CI uploads the +reports as the `Wallet-tests-Windows`, `Native-display-Windows` and +`Threading-<OS>` artifacts. + +## Portable lock tests + +[ThreadingTests.lpr](ThreadingTests.lpr) runs the core and shared read-write +lock cases without a GUI, Lazarus units or OpenSSL. It covers concurrent +readers, exclusive writers, upgrades, recursion, timed waits, ownership and +disposal. From the repository root on Linux or macOS: + +```sh +mkdir -p src/tests/experimental/lib/threading/units +fpc -B -Mdelphi -Sh -gl -Cr -Co \ + -FUsrc/tests/experimental/lib/threading/units \ + -FEsrc/tests/experimental/lib/threading \ + -Fusrc/core -Fusrc/libraries/sphere10 -Fusrc/libraries/pascalcoin \ + -Fusrc/libraries/generics.collections -Fisrc/libraries/generics.collections/inc \ + -Fusrc/tests/experimental/core -Fusrc/tests/experimental/framework \ + src/tests/experimental/ThreadingTests.lpr +src/tests/experimental/lib/threading/ThreadingTests src/tests/experimental/lib/threading/tests.log +``` + +The same compiler arguments work on Windows, where you run +`ThreadingTests.exe`. The program writes the test report to the log path you +pass and the heap report to `<log>.heap.log`. It exits 0 only when every test +passes, and a clean heap report contains `0 unfreed memory blocks`. In +`ThreadingTests.lpr`, keep `heaptrc` before `cthreads`. + +## Run individual suites + +Run these from the repository root on Windows: + +```powershell +& src\tests\experimental\RunExperimentalWalletTests.ps1 +& src\tests\experimental\native\RunWalletStartupNativeTests.ps1 +& src\tests\experimental\RunWalletGridRefreshTests.ps1 +& src\tests\experimental\RunWalletDownloadStopTests.ps1 +& src\tests\experimental\RunWalletSafeboxRecoveryTests.ps1 +``` + +- Each script finds FPC, and Lazarus if it uses the LCL, on `PATH`, or takes + `-FpcPath` and `-LazarusDirectory`. +- Each script takes the OpenSSL 1.1 runtime from `src` or `windows-dll`. It + writes to a new folder under `lib` in this folder, or to a new + `-OutputDirectory`, and prints that folder when it finishes. +- `RunExperimentalWalletTests.ps1` builds and runs `ExperimentalWalletTests.lpr`. + Each run writes its build log, test report, heap report and broker log to + `lib/<target>/<run-id>`. +- `RunWalletGridRefreshTests.ps1` runs `grid-integration` without switches, + `grid-native` with `-Demo -AllDemoCases`, `network-toggle` with `-NetworkOff` + and `buffered-backlog` with `-Backlog`. For one demo group, use + `RunWalletGridRefreshTests.ps1 -Demo -DemoCase operation-messages`. + `-DemoCase` and `-AllDemoCases` are mutually exclusive. +- `native/RunWalletRegressionTests.ps1 -Probe <phase> -OutputDirectory <new-directory>` + runs the `classic`, `connection-restart`, `pending-protocol`, + `buffered-frames`, `node-sync` or `mempool-lock` phase. +- [`Run-CheckpointStorageChecks.ps1`](native/storage-recovery/Run-CheckpointStorageChecks.ps1) + runs the [checkpoint storage checks](native/storage-recovery/README.md) alone. + They use generated data, never a user's wallet. The unit suite also runs + them. + +## Delphi + +- The scripts compile with Free Pascal in its `-MDelphi` mode, never with + Embarcadero Delphi. +- Delphi 13 Community Edition's command-line compilers are edition-restricted. + Build Delphi projects in its IDE. +- The classic DUnit project, + [`../PascalCoinUnitTests.dproj`](../PascalCoinUnitTests.dproj), and the + native VCL probes are separate from the FPCUnit fixtures. The native Delphi + checks do not cover the full wallet on Win32. +- With `-WalletExecutable src/pascalcoin_wallet_experimental.exe`, + `native/RunWalletStartupNativeTests.ps1` also starts the Delphi Win64 Debug + build offline and closes it normally. + +## FPCUnit fixtures + +| Suite | Covers | +|---|---| +| `startup/UWalletStartupTests.pas` | Option parsing, startup outcomes, main-thread completion and progress records | +| `startup/UPendingRestoreTests.pas` | Restoring saved pending operations, with cancellation, retry and truncated streams | +| `startup/UPendingCleanupTests.pas` | Bulk pending cleanup, byte-identical buffered saving and stage-based cancellation | +| `startup/UPendingSaveLifecycleTests.pas` | Pending saves at shutdown, on failure and by atomic replacement | +| `startup/UBufferedAdoptionTests.pas` | Core use of buffered streams for pending packages, history files and RandomHash tables | +| `startup/UCheckpointStorageTests.pas` | [Checkpoint storage](native/storage-recovery/README.md) failures and recovery | +| `core/UThreadConcurrencyTests.pas` | Core read-write lock: readers, writers, promotion, recursion, timeouts and ownership | +| `core/UCoreLockMigrationTests.pas` | Application lock owners: recursive leases, exclusion, timeouts and destruction | +| `core/UNodeSynchronizationTests.pas` | Node synchronization targets, request generations, notification order and storage failures | +| `core/UPendingCatchupTests.pas` | [Deferred pending replay](native/block-throughput/README.md#pending-catch-up) | +| `core/UChangeAccountInfoReplayTests.pas` | Signed-operation equivalence when signer and target accounts are reused | +| `core/UBlockSanitizeTests.pas` | Block replay and sanitizing: rejected and duplicate operations, fee limits and signature caches | +| `core/UAccountCachePolicyTests.pas` | Account cache capacity and reloads, and safebox cleanup failures | +| `core/UAbstractMemStorageFailureTests.pas` | [AbstractMem block-storage](native/abstractmem-storage/README.md) failures | +| `core/UDownloadWorkerLifetimeTests.pas` | Download worker, socket lease and discovery ownership through shutdown | +| `core/ULegacyConnectionLifetimeTests.pas` | Connection retirement after legacy connection-lock releases | +| `core/UOrderedCardinalTests.pas` | AbstractMem `Cardinal` comparator and ordered list | +| `core/ULogGuiHandoffTests.pas` | [Logger handoff](native/log-gui-handoff/README.md) to GUI receivers | +| `operations/UOperationPreparationTests.pas` | Wizard operation preparation, fees, payloads, signers and swaps, accepted by core `DoOperation` | +| `operations/UChangeInfoBatchTests.pas` | Change-info batches, fees, signers and signatures (`UChangeInfoBatchChecks.pas`) | +| `operations/UOperationWizardTests.pas` | Wizard unlock ownership, cancellation, retry and signer eligibility | +| `operations/UWalletParityTests.pas` | Key formats, wizard inputs, sale and swap filtering, RAW workbench, import and partial signatures | +| `ui/UWalletProgressDisplayTests.pas` | Progress text, including legacy strings | +| `ui/UWalletRestoreTests.pas` | Key backup and restore, wrong passwords and watch-only upgrade | +| `ui/UWalletViewTests.pas` | Core-work accounting: silent activity, nested scopes and exceptional cleanup | +| `ui/UWalletGridRefreshTests.pas` | Grid refresh on node notifications | +| `ui/UWalletGridIntegrationTests.pas` | Main-form grid scenarios | +| `ui/UWalletExplorerTests.pas` | Accounts Explorer snapshots, worker reads, history, totals and cancellation | +| `ui/UWalletExplorerSchedulingTests.pas` | Explorer refresh scheduling | +| `ui/UWalletAccountEnumerationTests.pas` | Persisted account snapshots and wallet account traversal | +| `ui/UWalletHistoryTests.pas` | Pending and stored history: order, balances, paging and archive fallback | +| `ui/UWalletSnapshotTests.pas` | Wallet membership resolution and responsive settings during work | +| `ui/UWalletMempoolReadSafetyTests.pas` | Mempool reads during active writes | +| `ui/UWalletPagingTests.pas` | Account paging, sorting, global totals and query cancellation | +| `ui/UWalletMiningTests.pas` | Mining reward keys across wallet states, and key rotation | +| `ui/UWalletRenderTests.pas` | Rendering from captured pending state, with PASC amounts | +| `ui/UWalletInfoTests.pas` | Account data, sale and swap details, multioperations and JSON | +| `ui/UWalletLanguageTests.pas` | Language catalogs, preferences, fallback and form translation | +| `framework/UCache.Tests.pas` | Cache expiration, capacity, pins, notifications and concurrency | +| `framework/UCommon.Data.Tests.pas` | Data snapshots: paging, search, sort, invalidation and schemas | +| `framework/UCommon.UI.Tests.pas` | Refresh throttle timers, Waiting state, callbacks and cancellation | +| `framework/UVisualGrid.Tests.pas` | Grid selection deduplication and clearing | +| [`framework/UVisualGrid.Refresh.Tests.pas`](framework/UVisualGrid.Refresh.Tests.pas) | Per-grid request admission, loading, replay, cancellation and `RefreshSuspended` | +| `framework/UFramework.UI.Integration.Tests.pas` | Docking, worker completion, source replacement and error recovery | +| `framework/UWizard.Tests.pas` | Wizard lifecycle, validation, routes, retries and model binding | +| `framework/UStreams.Tests.pas` | Stream decorator contracts shared with PascalFramework: read-ahead, seeking, writes, resizing and failure recovery | +| [`framework/UScope.Tests.pas`](framework/UScope.Tests.pas) | Managed cleanup scopes | +| `framework/UConcurrency.Tests.pas` | Shared framework read-write lock | +| [`framework/UReadWriteScope.Tests.pas`](framework/UReadWriteScope.Tests.pas) | Scoped read, write and upgradeable lock acquisition | +| `../UCommon.Tests.pas` | Shared `UCommon` helpers | +| `../UCommon.Collections.Tests.pas` | Collection comparers, filters and transforms | +| [`../UMemory.Tests.pas`](../UMemory.Tests.pas) | Disposal scopes, capacity growth, ownership transfer and exception cleanup | + +The imported framework tests are kept here so this checkout has no dependency on +a separate PascalFramework repository. Their original MIT notices are retained. +PascalFramework mirrors the shared component test sources and the +[native Delphi timer probe](native/common/README.md). + +The fixtures never load a user wallet, connect to peers, mine or broadcast. + +## Native probes + +These probes have their own projects and commands: + +- [ScopeProbe](native/common/ScopeProbe.dproj) runs the shared scope contract + checks in Delphi. +- The [native common probe](native/common/README.md#per-grid-request-admission) + runs the VCL grid request-admission scenarios. +- The [logger handoff regression](native/log-gui-handoff/README.md) shares its + `ULogGuiHandoffTests` assertions with native Delphi and FPC probes. +- The [core synchronization probe](native/node-sync/README.md#core-lifecycle-probe) + checks node synchronization across batch gaps, network-off, disconnects and + storage failures, in Delphi and Free Pascal. +- Shared Delphi and FPC helpers cover + [core and lifetime checks](native/network-lifetime/README.md), + [buffered frames and transport](native/network-backlog/BufferedBlockFrameProbe.md), + [full-range Cardinal ordering](native/ordered-cardinal/README.md) and + [stored history](native/stored-history/README.md). +- [Buffered adoption](native/buffered-adoption/README.md) probes run the + `UBufferedAdoptionTests` checks in Delphi and FPC. +- The [block application probes](native/block-throughput/README.md) test + pending catch-up, sanitizing and pending saves in Delphi and FPC. +- The [account selector probe](native/account-selector/README.md) tests the + account-selection form in Delphi and FPC. + +## Large-chain checks + +These opt-in checks use a copy of the public chain. They cover received block +backlogs, dense paging, startup and busy close, pending cleanup and saving, and +Accounts Explorer key changes. See +[received block backlog](native/network-backlog/README.md), +[block throughput](native/block-throughput/README.md) and the +[startup checks](native/README.md) for setup. Never use a live wallet or its +private keys. + +## Limits + +- All checks run offline. Live synchronization, peer throughput and + transaction delivery need separate testing. +- The five-second shutdown assertion in the `buffered-backlog` phase once + failed at 6,062 ms although the process exited cleanly. The cause is unknown. + +## Recorded results + +An earlier full FPCUnit run passed 525 tests with zero leaks under FPC 3.2.0 +and Lazarus 2.1.0. diff --git a/src/tests/experimental/RunAllWalletTests.ps1 b/src/tests/experimental/RunAllWalletTests.ps1 new file mode 100644 index 000000000..bb2738475 --- /dev/null +++ b/src/tests/experimental/RunAllWalletTests.ps1 @@ -0,0 +1,157 @@ +<# +.SYNOPSIS +Runs the complete Windows wallet unit and native regression suites. +.DESCRIPTION +The controller integration fixture runs in its own process because it owns +process-lifetime application state. Native checks use fresh data and private +desktops. The optional large-chain fixture and Delphi-only probes are separate. +#> +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [string] $FpcPath, + [string] $LazarusDirectory, + [string] $OutputDirectory, + [string] $WalletExecutable +) + +$ErrorActionPreference = 'Stop' +if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { + throw 'The full native suite requires Windows. Run ThreadingTests.lpr for portable lock coverage.' +} + +# CI exposes the installed tools on PATH; explicit paths remain available locally. +if (-not $FpcPath) { + $command = Get-Command fpc -ErrorAction SilentlyContinue + if ($command) { $FpcPath = $command.Source } + else { throw 'Pass -FpcPath or add fpc to PATH.' } +} +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +if (-not $LazarusDirectory) { + $command = Get-Command lazbuild -ErrorAction SilentlyContinue + if ($command) { $LazarusDirectory = Split-Path -Parent $command.Source } + else { throw 'Pass -LazarusDirectory or add lazbuild to PATH.' } +} +$lazarus = (Resolve-Path -LiteralPath $LazarusDirectory).Path +if (-not (Test-Path -LiteralPath (Join-Path $lazarus 'lcl') -PathType Container)) { + throw "Lazarus LCL directory not found under $lazarus. Pass -LazarusDirectory explicitly." +} +$repository = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../../..')).Path +if (-not $WalletExecutable) { + $WalletExecutable = Join-Path $repository 'src/PascalCoinWalletExperimental.exe' +} +$shippingWallet = (Resolve-Path -LiteralPath $WalletExecutable).Path +if (-not $OutputDirectory) { + $runId = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8) + $OutputDirectory = Join-Path $PSScriptRoot "lib/all-$runId" +} +$output = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $output) { throw "Choose a new output directory: $output" } +New-Item -ItemType Directory -Path $output | Out-Null + +# Every registering unit must belong to the general or isolated controller runner. +$general = [IO.File]::ReadAllText((Join-Path $PSScriptRoot 'ExperimentalWalletTests.lpr')) +$isolated = [IO.File]::ReadAllText((Join-Path $PSScriptRoot 'ui/WalletGridRefreshTests.lpr')) +$fixtureFiles = @() +foreach ($directory in @('core', 'startup', 'operations', 'ui', 'framework')) { + $fixtureFiles += Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot $directory) -Filter '*.pas' -File +} +foreach ($name in @('UCommon.Tests.pas', 'UCommon.Collections.Tests.pas', 'UMemory.Tests.pas')) { + $fixtureFiles += Get-Item -LiteralPath (Join-Path $PSScriptRoot "../$name") +} +$registeredUnits = @() +foreach ($file in $fixtureFiles) { + $text = [IO.File]::ReadAllText($file.FullName) + if ($text -notmatch '(?im)^\s*RegisterTests?\s*\(') { continue } + if ($text -notmatch '(?im)^\s*unit\s+([A-Za-z0-9_.]+)\s*;') { + throw "Cannot identify registered fixture unit: $($file.FullName)" + } + $unitName = $Matches[1] + $pattern = '(?<![A-Za-z0-9_.])' + [regex]::Escape($unitName) + '(?![A-Za-z0-9_.])' + $runner = if ($general -match $pattern) { 'general' } + elseif ($isolated -match $pattern) { 'isolated-controller' } + else { throw "Registered fixture unit is missing from the test runners: $unitName" } + $registeredUnits += [pscustomobject]@{ Unit = $unitName; Runner = $runner } +} +$registeredUnits | Sort-Object Unit | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $output 'fixture-registration.json') + +$phases = @( + @{ Name = 'unit'; Script = 'RunExperimentalWalletTests.ps1'; Lazarus = $true }, + @{ Name = 'log-memo-batch'; Script = 'native/log-memo-batch/RunLogMemoBatchChecks.ps1'; Lazarus = $true }, + @{ Name = 'classic-log-memo-batch'; Script = 'native/log-memo-batch/RunLogMemoBatchChecks.ps1'; Lazarus = $true; Switch = '-Classic' }, + @{ Name = 'grid-integration'; Script = 'RunWalletGridRefreshTests.ps1'; Lazarus = $true }, + @{ Name = 'grid-native'; Script = 'RunWalletGridRefreshTests.ps1'; Lazarus = $true; Switch = '-Demo'; ExtraSwitch = '-AllDemoCases' }, + @{ Name = 'network-toggle'; Script = 'RunWalletGridRefreshTests.ps1'; Lazarus = $true; Switch = '-NetworkOff' }, + @{ Name = 'buffered-backlog'; Script = 'RunWalletGridRefreshTests.ps1'; Lazarus = $true; Switch = '-Backlog' }, + @{ Name = 'startup'; Script = 'native/RunWalletStartupNativeTests.ps1'; Lazarus = $true; Shipping = $true }, + @{ Name = 'download-stop'; Script = 'RunWalletDownloadStopTests.ps1'; Lazarus = $true }, + @{ Name = 'safebox-recovery'; Script = 'RunWalletSafeboxRecoveryTests.ps1'; Lazarus = $false }, + @{ Name = 'classic'; Script = 'native/RunWalletRegressionTests.ps1'; Lazarus = $true; Probe = 'classic' }, + @{ Name = 'connection-restart'; Script = 'native/RunWalletRegressionTests.ps1'; Lazarus = $true; Probe = 'connection-restart' }, + @{ Name = 'pending-protocol'; Script = 'native/RunWalletRegressionTests.ps1'; Lazarus = $true; Probe = 'pending-protocol' }, + @{ Name = 'buffered-frames'; Script = 'native/RunWalletRegressionTests.ps1'; Lazarus = $true; Probe = 'buffered-frames' }, + @{ Name = 'node-sync'; Script = 'native/RunWalletRegressionTests.ps1'; Lazarus = $true; Probe = 'node-sync' }, + @{ Name = 'mempool-lock'; Script = 'native/RunWalletRegressionTests.ps1'; Lazarus = $true; Probe = 'mempool-lock' } +) +$coverage = @{ + RegisteredFixtureUnits = @($registeredUnits | Sort-Object Unit) + NativePhases = @($phases | Where-Object Name -ne 'unit' | ForEach-Object { $_.Name }) + SharedHelpersInUnitFixtures = @('UAccountCachePolicyChecks', 'UChangeAccountInfoReplayChecks', 'UCheckpointStorageChecks', 'UAbstractMemStorageFailureChecks', 'UCoreLockMigrationChecks', 'ULogGuiHandoffChecks', 'UStreams.Checks', 'UBufferedAdoptionChecks', + 'UDownloadLifetimeChecks', 'ULegacyConnectionLifetimeChecks', 'UMempoolSanitizeChecks', + 'UOperationReadHashChecks', 'UOrderedCardinalChecks', + 'UPendingCatchupChecks', 'UPendingSaveChecks', 'UStoredHistoryChecks') + EquivalentNodeLifecycleScenarios = 'UNodeSynchronizationTests mirrors the seventeen native UNodeSynchronizationChecks cases' + NativeDemoCases = @('default', 'selection', 'safebox-info', 'sync-paint', 'tools', + 'history-depth', 'operation-input', 'block-tools', 'change-info-approval', + 'change-info-ui', 'send-ui', 'operation-messages', 'advanced-ui') + SeparateInputs = @('Delphi-only probes require Delphi', + 'BlockThroughputProbe requires an owned public-chain dataset', + 'LargeBufferedBlockBacklogProbe requires an owned matching-chain dataset') +} +$coverage | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $output 'test-coverage.json') +$shell = (Get-Process -Id $PID).Path +$results = @() +foreach ($phase in $phases) { + $arguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', + (Join-Path $PSScriptRoot $phase.Script), '-FpcPath', $compiler, + '-OutputDirectory', (Join-Path $output $phase.Name)) + if ($phase.Lazarus) { $arguments += @('-LazarusDirectory', $lazarus) } + if ($phase.Shipping) { $arguments += @('-WalletExecutable', $shippingWallet) } + if ($phase.Probe) { $arguments += @('-Probe', $phase.Probe) } + if ($phase.Switch) { $arguments += $phase.Switch } + if ($phase.ExtraSwitch) { $arguments += $phase.ExtraSwitch } + Write-Host "Running wallet tests: $($phase.Name)" + $started = Get-Date + $phaseLog = Join-Path $output ($phase.Name + '-runner.log') + $previousErrors = $ErrorActionPreference + $nativeSetting = Get-Variable PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue + $previousNativeErrors = if ($nativeSetting) { $nativeSetting.Value } else { $null } + try { + # A failed child is a recorded phase failure, not permission to omit later phases. + $ErrorActionPreference = 'Continue' + $PSNativeCommandUseErrorActionPreference = $false + $status = 1 + & $shell @arguments 2>&1 | Tee-Object -FilePath $phaseLog + # Native exit codes are global even when another script invoked this runner. + $status = $global:LASTEXITCODE + } catch { + $status = 1 + $_ | Out-String | Tee-Object -FilePath $phaseLog -Append + } finally { + $ErrorActionPreference = $previousErrors + if ($nativeSetting) { + $PSNativeCommandUseErrorActionPreference = $previousNativeErrors + } else { + Remove-Variable PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue + } + } + $results += [pscustomobject]@{ + Phase = $phase.Name + ExitCode = $status + Seconds = [Math]::Round(((Get-Date) - $started).TotalSeconds, 2) + } + $results | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $output 'all-tests.json') +} +$results | Format-Table -AutoSize +Write-Host "Wallet regression evidence: $output" +if (@($results | Where-Object ExitCode -ne 0).Count -ne 0) { exit 1 } +exit 0 diff --git a/src/tests/experimental/RunExperimentalWalletTests.ps1 b/src/tests/experimental/RunExperimentalWalletTests.ps1 new file mode 100644 index 000000000..1ca342ce3 --- /dev/null +++ b/src/tests/experimental/RunExperimentalWalletTests.ps1 @@ -0,0 +1,95 @@ +param( + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string] $LazarusDirectory = $(if ($c = Get-Command lazbuild -ErrorAction SilentlyContinue) { Split-Path -Parent $c.Source } else { '' }), + [string] $OutputDirectory +) + +$ErrorActionPreference = 'Stop' +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +$lazarus = (Resolve-Path -LiteralPath $LazarusDirectory).Path +$repository = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..\..')).Path +$targetInfo = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if ($LASTEXITCODE -ne 0 -or $targetInfo.Count -ne 2 -or $targetInfo[1] -notin @('win64', 'win32')) { + throw 'This native Lazarus wallet suite requires a Windows Free Pascal target.' +} +$target = $targetInfo -join '-' +$runId = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8) +if (-not $OutputDirectory) { + $OutputDirectory = Join-Path $PSScriptRoot "lib\$target\$runId" +} +$outputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $outputDirectory) { throw "Choose a new output directory: $outputDirectory" } +$units = Join-Path $outputDirectory 'units' +$brokerUnits = Join-Path $outputDirectory 'broker-units' +New-Item -ItemType Directory -Path $units, $brokerUnits -Force | Out-Null + +# Tests use generated keys; OpenSSL must be resolvable next to their executable. +$cryptoName = if ($targetInfo[0] -eq 'x86_64') { 'libcrypto-1_1-x64.dll' } else { 'libcrypto-1_1.dll' } +# Prefer the deployed runtime, then the repository's canonical bundled copy. +$crypto = @((Join-Path $repository "src\$cryptoName"), (Join-Path $repository "windows-dll\$cryptoName")) | + Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 +if (-not $crypto) { throw "Place the existing OpenSSL 1.1 runtime ($cryptoName) in src or windows-dll." } +Copy-Item -LiteralPath $crypto -Destination (Join-Path $outputDirectory $cryptoName) +# Public historical blocks let checkpoint recovery exercise real local replay. +Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'fixtures\mainnet-blocks-0-4.bin') -Destination $outputDirectory + +$arguments = @('-B', '-Mdelphi', '-gl', '-Cr', '-Co', '-dVISUALGRID_TEST_WORKER_START', + '-dBorland', '-dVer150', '-dDelphi7', '-dCompiler6_Up', '-dPUREPASCAL', + "-FU$units", "-FE$outputDirectory", "-Fi$repository\src\libraries\generics.collections\inc", + "-Fu$lazarus\lcl\units\$target\win32", "-Fu$lazarus\lcl\units\$target", + "-Fu$lazarus\components\lazutils\lib\$target", "-Fu$lazarus\packager\units\$target") +$sourcePaths = @('core', 'gui-experimental', 'gui-experimental\core.utils', + 'gui-experimental\wizards\wallet', 'gui-experimental\wizards\operations', 'gui-classic', + 'libraries\simplebaselib4pascal', 'libraries\hashlib4pascal', 'libraries\cryptolib4pascal', + 'libraries\synapse', 'libraries\sphere10', 'libraries\generics.collections', + 'libraries\pascalcoin', 'libraries\paszlib', 'libraries\abstractmem', 'libraries\regex', + 'libraries\gnugettext') +foreach ($path in $sourcePaths) { $arguments += "-Fu$repository\src\$path" } +foreach ($path in @('startup', 'operations', 'ui', 'framework', 'core', 'native\mempool-lock', 'native\block-throughput', 'native\network-lifetime', 'native\ordered-cardinal', 'native\stored-history', 'native\storage-recovery', 'native\abstractmem-storage', 'native\buffered-adoption')) { $arguments += "-Fu$PSScriptRoot\$path" } +$arguments += "-Fu$repository\src\tests" +$arguments += "$PSScriptRoot\ExperimentalWalletTests.lpr" +$arguments | Set-Content -LiteralPath (Join-Path $outputDirectory 'compiler-arguments.txt') +& $compiler @arguments *> (Join-Path $outputDirectory 'build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $outputDirectory 'build.log') -Tail 60 + exit $LASTEXITCODE +} + +$broker = Join-Path $outputDirectory 'PrivateDesktopBroker.exe' +& $compiler '-Mdelphi' '-O2' '-gl' "-FU$brokerUnits" "-o$broker" ` + "$PSScriptRoot\native\PrivateDesktopBroker.lpr" *> (Join-Path $outputDirectory 'broker-build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $outputDirectory 'broker-build.log') -Tail 60 + exit $LASTEXITCODE +} +$results = Join-Path $outputDirectory 'tests.log' +$testTemp = Join-Path $outputDirectory 'temporary-data' +New-Item -ItemType Directory -Path $testTemp -Force | Out-Null +$previousTemp = $env:TEMP +$previousTmp = $env:TMP +try { + $env:TEMP = $testTemp + $env:TMP = $testTemp + & $broker (Join-Path $outputDirectory 'ExperimentalWalletTests.exe') $results *> (Join-Path $outputDirectory 'broker.log') + $status = $LASTEXITCODE +} finally { + $env:TEMP = $previousTemp + $env:TMP = $previousTmp +} +if (Test-Path -LiteralPath $results) { Get-Content -LiteralPath $results } +if ($status -eq 0) { + $heapReport = "$results.heap.log" + if (-not (Test-Path -LiteralPath $heapReport) -or + -not ([IO.File]::ReadAllText($heapReport) -match '(?m)^0 unfreed memory blocks')) { + Write-Error 'The experimental wallet suite did not produce a clean heap report.' -ErrorAction Continue + $status = 1 + } +} +if ($status -ne 0) { + Get-Content -LiteralPath (Join-Path $outputDirectory 'broker.log') -Tail 40 + if (Test-Path -LiteralPath "$results.progress.log") { + Get-Content -LiteralPath "$results.progress.log" -Tail 30 + } +} +Write-Host "Experimental wallet test evidence: $outputDirectory" +exit $status diff --git a/src/tests/experimental/RunWalletDownloadStopTests.ps1 b/src/tests/experimental/RunWalletDownloadStopTests.ps1 new file mode 100644 index 000000000..fbfa919c9 --- /dev/null +++ b/src/tests/experimental/RunWalletDownloadStopTests.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Builds and tests safebox-download cancellation on an isolated private desktop. +.DESCRIPTION +Two controlled real download workers contend for the owner's chunk-list lock +while shutdown joins them. Fresh settings disable peers and use an ephemeral +listener. The test never opens the user's wallet or switches the input desktop. +#> +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string] $LazarusDirectory = $(if ($c = Get-Command lazbuild -ErrorAction SilentlyContinue) { Split-Path -Parent $c.Source } else { '' }), + [string] $OutputDirectory +) + +$ErrorActionPreference = 'Stop' +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +$lazarus = (Resolve-Path -LiteralPath $LazarusDirectory).Path +$source = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +$targetInfo = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if ($LASTEXITCODE -ne 0 -or $targetInfo.Count -ne 2 -or $targetInfo[1] -ne 'win64') { + throw 'The native download-stop check requires a Windows x64 FPC compiler.' +} +$target = $targetInfo -join '-' +if (-not $OutputDirectory) { + $OutputDirectory = Join-Path $PSScriptRoot ('lib\download-stop-' + [guid]::NewGuid().ToString('N')) +} +$output = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $output) { throw "Choose a new output directory: $output" } +$units = Join-Path $output 'units' +$brokerUnits = Join-Path $output 'broker-units' +New-Item -ItemType Directory -Path $units, $brokerUnits -Force | Out-Null +$probeDirectory = Join-Path $PSScriptRoot 'native\download-stop' +$executable = Join-Path $output 'DownloadSafeboxStopProbe.exe' +$arguments = @('-B', '-MDelphi', '-O1', '-gl', '-Cr', '-Co', '-WG', + '-dBorland', '-dVer150', '-dDelphi7', '-dCompiler6_Up', '-dPUREPASCAL', + "-FU$units", "-FE$output", "-o$executable", + "-Fi$source\libraries\generics.collections\inc", + "-Fu$lazarus\lcl\units\$target", "-Fu$lazarus\lcl\units\$target\win32", + "-Fu$lazarus\components\lazutils\lib\$target", "-Fu$lazarus\packager\units\$target", + "-Fu$probeDirectory") +$directories = @('core', 'gui-experimental', 'gui-experimental\core.utils', + 'gui-experimental\wizards\wallet', 'gui-experimental\wizards\operations', 'gui-classic', + 'libraries\simplebaselib4pascal', 'libraries\hashlib4pascal', 'libraries\cryptolib4pascal', + 'libraries\synapse', 'libraries\sphere10', 'libraries\generics.collections', + 'libraries\pascalcoin', 'libraries\paszlib', 'libraries\abstractmem', 'libraries\regex', + 'libraries\gnugettext') +foreach ($directory in $directories) { $arguments += "-Fu$source\$directory" } +$arguments += Join-Path $probeDirectory 'DownloadSafeboxStopProbe.lpr' +$arguments | Set-Content -LiteralPath (Join-Path $output 'compiler-arguments.txt') +& $compiler @arguments *> (Join-Path $output 'build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'build.log') -Tail 60 + exit $LASTEXITCODE +} +# Prefer the deployed runtime, then the repository's canonical Windows DLL. +$crypto = Join-Path $source 'libcrypto-1_1-x64.dll' +if (-not (Test-Path -LiteralPath $crypto)) { + $crypto = Join-Path (Split-Path -Parent $source) 'windows-dll\libcrypto-1_1-x64.dll' +} +if (-not (Test-Path -LiteralPath $crypto)) { throw "OpenSSL runtime not found: $crypto" } +Copy-Item -LiteralPath $crypto -Destination $output + +$broker = Join-Path $output 'PrivateDesktopBroker.exe' +& $compiler '-MDelphi' '-O2' '-gl' "-FU$brokerUnits" "-o$broker" ` + (Join-Path $PSScriptRoot 'native\PrivateDesktopBroker.lpr') *> (Join-Path $output 'broker-build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'broker-build.log') -Tail 40 + exit $LASTEXITCODE +} +$log = Join-Path $output 'tests.log' +$temporaryData = Join-Path $output 'temporary-data' +New-Item -ItemType Directory -Path $temporaryData | Out-Null +$previousTemp = $env:TEMP +$previousTmp = $env:TMP +try { + $env:TEMP = $temporaryData + $env:TMP = $temporaryData + & $broker $executable $log *> (Join-Path $output 'broker.log') + $status = $LASTEXITCODE +} finally { + $env:TEMP = $previousTemp + $env:TMP = $previousTmp +} +if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log } +elseif ($status -eq 0) { + Write-Error 'The download-stop check did not produce its result log.' -ErrorAction Continue + $status = 1 +} +$heap = "$log.heap.log" +if ($status -eq 0 -and (-not (Test-Path -LiteralPath $heap) -or + -not ([IO.File]::ReadAllText($heap) -match '(?m)^0 unfreed memory blocks'))) { + Write-Error 'The download-stop check did not produce a clean heap report.' -ErrorAction Continue + $status = 1 +} +if ($status -ne 0) { Get-Content -LiteralPath (Join-Path $output 'broker.log') -Tail 25 } +Write-Host "Wallet download-stop evidence: $output" +exit $status diff --git a/src/tests/experimental/RunWalletGridRefreshTests.ps1 b/src/tests/experimental/RunWalletGridRefreshTests.ps1 new file mode 100644 index 000000000..310561331 --- /dev/null +++ b/src/tests/experimental/RunWalletGridRefreshTests.ps1 @@ -0,0 +1,233 @@ +<# +.SYNOPSIS +Builds and runs main-wallet refresh and received-block checks on a private desktop. +.DESCRIPTION +The default target is the dedicated FPCUnit fixture. Use -Demo to run the native +interactive demo's automatic scenarios instead. Add -AllDemoCases to include +every distinct native presentation/logging/dialog group in fresh processes. +Use -DemoCase <name> with -Demo to run one of those groups instead, or one +part of the default group alone. +Native demo checks use the shipping Release optimization level so layout bugs +that disappear in less optimized builds remain covered. +Use -NetworkOff for the real +connectivity toggle and deferred-refresh lifecycle regression. Use -Backlog for +the actual buffered-response processor, outgoing/incoming transport ownership, +explicit shutdown, inherited data sources and committed footer/status checks +using five historical blocks. One build runs eight fresh private processes. +Use -LargeBacklog with -LargeDataRoot for the opt-in matching public-chain +fixture. It reuses an explicitly owned Data junction while creating fresh +settings and one watch-only key; it is not part of the eight small scenarios. +The other targets use typed +mock sources and real wallet controls. Network cases restrict peers to loopback; +all targets use fresh isolated wallet settings. Existing wallet keys and the input desktop +are never used by this script. +#> +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string] $LazarusDirectory = $(if ($c = Get-Command lazbuild -ErrorAction SilentlyContinue) { Split-Path -Parent $c.Source } else { '' }), + [string] $OutputDirectory, + [switch] $Demo, + [switch] $AllDemoCases, + [string] $DemoCase, + [switch] $NetworkOff, + [switch] $Backlog, + [switch] $LargeBacklog, + [string] $LargeDataRoot +) + +$ErrorActionPreference = 'Stop' +if (@($Demo, $NetworkOff, $Backlog, $LargeBacklog).Where({ $_.IsPresent }).Count -gt 1) { + throw 'Choose only one of -Demo, -NetworkOff, -Backlog or -LargeBacklog.' +} +if (($AllDemoCases -or $DemoCase) -and -not $Demo) { throw '-AllDemoCases and -DemoCase require -Demo.' } +if ($AllDemoCases -and $DemoCase) { throw 'Choose only one of -AllDemoCases or -DemoCase.' } +$demoSelectors = [ordered]@{ + 'selection' = @('PASCAL_WALLET_SELECTION_ONLY') + 'safebox-info' = @('PASCAL_WALLET_SAFEBOX_LOG_ONLY') + 'sync-paint' = @('PASCAL_WALLET_SYNC_PAINT_ONLY') + 'tools' = @('PASCAL_WALLET_TOOLS_ONLY') + 'history-depth' = @('PASCAL_WALLET_TOOLS_ONLY', 'PASCAL_WALLET_HISTORY_DEPTH_ONLY') + 'operation-input' = @('PASCAL_WALLET_TOOLS_ONLY', 'PASCAL_WALLET_OPERATION_INPUT_ONLY') + 'block-tools' = @('PASCAL_WALLET_TOOLS_ONLY', 'PASCAL_WALLET_BLOCK_TOOLS_ONLY') + 'change-info-approval' = @('PASCAL_WALLET_CHANGE_INFO_APPROVAL_ONLY') + 'change-info-ui' = @('PASCAL_WALLET_CHANGE_INFO_UI_ONLY') + 'send-ui' = @('PASCAL_WALLET_SEND_UI_ONLY') + 'operation-messages' = @('PASCAL_WALLET_OPERATION_MESSAGE_ONLY') + 'advanced-ui' = @('PASCAL_WALLET_ADVANCED_UI_ONLY') +} +$allDemoCaseNames = @('default') + @($demoSelectors.Keys) +# Each of these runs one part of the default case on its own. -AllDemoCases +# already runs 'default', so only -DemoCase selects them. +$defaultPartSelectors = [ordered]@{ + 'key-notifications' = @('PASCAL_WALLET_KEY_NOTIFICATIONS_ONLY') + 'consecutive-selection' = @('PASCAL_WALLET_CONSECUTIVE_SELECTION_ONLY') + 'refresh-lifecycle' = @('PASCAL_WALLET_REFRESH_LIFECYCLE_ONLY') + 'selection-completion' = @('PASCAL_WALLET_SELECTION_COMPLETION_ONLY') +} +$demoCaseNames = $allDemoCaseNames + @($defaultPartSelectors.Keys) +foreach ($part in $defaultPartSelectors.GetEnumerator()) { $demoSelectors[$part.Key] = $part.Value } +if ($DemoCase) { + $DemoCase = $DemoCase.ToLowerInvariant() + if ($DemoCase -notin $demoCaseNames) { + throw "Unknown demo case '$DemoCase'. Choose: $($demoCaseNames -join ', ')." + } +} +$useDemoSelectors = $AllDemoCases -or [bool]$DemoCase +$ownedLargeRoot = $null +if ($LargeBacklog) { + if ([string]::IsNullOrWhiteSpace($LargeDataRoot)) { throw '-LargeBacklog requires -LargeDataRoot.' } + $ownedLargeRoot = (Resolve-Path -LiteralPath $LargeDataRoot).Path + if (-not (Test-Path -LiteralPath (Join-Path $ownedLargeRoot 'LargeBacklog.owned') -PathType Leaf)) { + throw 'The large fixture requires its explicitly owned data marker.' + } +} elseif ($LargeDataRoot) { throw '-LargeDataRoot is only valid with -LargeBacklog.' } +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +$lazarus = (Resolve-Path -LiteralPath $LazarusDirectory).Path +$source = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +$targetInfo = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if ($LASTEXITCODE -ne 0 -or $targetInfo.Count -ne 2 -or $targetInfo[1] -ne 'win64') { + throw 'These native grid refresh checks require a Windows x64 FPC compiler.' +} +$target = $targetInfo -join '-' +$runId = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8) +if (-not $OutputDirectory) { $OutputDirectory = Join-Path $PSScriptRoot "lib\grid-refresh-$runId" } +$output = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $output) { throw "Choose a new output directory: $output" } +$units = Join-Path $output 'units' +$brokerUnits = Join-Path $output 'broker-units' +New-Item -ItemType Directory -Path $units, $brokerUnits -Force | Out-Null + +$gridDirectory = Join-Path $PSScriptRoot 'native\grid-refresh' +$backlogDirectory = Join-Path $PSScriptRoot 'native\network-backlog' +$program = if ($Demo) { Join-Path $gridDirectory 'WalletGridRefreshDemo.lpr' } + elseif ($NetworkOff) { Join-Path $gridDirectory 'WalletNetworkOffProbe.lpr' } + elseif ($Backlog) { Join-Path $backlogDirectory 'BufferedBlockBacklogProbe.lpr' } + elseif ($LargeBacklog) { Join-Path $backlogDirectory 'LargeBufferedBlockBacklogProbe.lpr' } + else { Join-Path $PSScriptRoot 'ui\WalletGridRefreshTests.lpr' } +$name = [IO.Path]::GetFileNameWithoutExtension($program) +$executable = Join-Path $output "$name.exe" +$optimization = if ($Demo) { '-O2' } else { '-O1' } +$arguments = @('-B', '-MDelphi', $optimization, '-gl', '-Cr', '-Co', '-WG', + '-dBorland', '-dVer150', '-dDelphi7', '-dCompiler6_Up', '-dPUREPASCAL', + "-FU$units", "-FE$output", "-o$executable", + "-Fi$source\libraries\generics.collections\inc", + "-Fu$lazarus\lcl\units\$target", "-Fu$lazarus\lcl\units\$target\win32", + "-Fu$lazarus\components\lazutils\lib\$target", "-Fu$lazarus\packager\units\$target", + "-Fu$gridDirectory", "-Fu$backlogDirectory", "-Fu$PSScriptRoot\ui", "-Fu$PSScriptRoot\framework") +$directories = @('core', 'gui-experimental', 'gui-experimental\core.utils', + 'gui-experimental\wizards\wallet', 'gui-experimental\wizards\operations', 'gui-classic', + 'libraries\simplebaselib4pascal', 'libraries\hashlib4pascal', 'libraries\cryptolib4pascal', + 'libraries\synapse', 'libraries\sphere10', 'libraries\generics.collections', + 'libraries\pascalcoin', 'libraries\paszlib', 'libraries\abstractmem', 'libraries\regex', + 'libraries\gnugettext') +foreach ($directory in $directories) { $arguments += "-Fu$source\$directory" } +if ($LargeBacklog) { $arguments += '-dLARGE_BACKLOG_PROBE' } +$arguments += $program +$arguments | Set-Content -LiteralPath (Join-Path $output 'compiler-arguments.txt') +& $compiler @arguments *> (Join-Path $output 'build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'build.log') -Tail 60 + exit $LASTEXITCODE +} +# Prefer the deployed runtime, then the repository's canonical Windows DLL. +$crypto = Join-Path $source 'libcrypto-1_1-x64.dll' +if (-not (Test-Path -LiteralPath $crypto)) { + $crypto = Join-Path (Split-Path -Parent $source) 'windows-dll\libcrypto-1_1-x64.dll' +} +if (-not (Test-Path -LiteralPath $crypto)) { throw "OpenSSL runtime not found: $crypto" } +Copy-Item -LiteralPath $crypto -Destination $output +if ($Demo) { + # Tools and operation-dialog cases validate the repository's historic genesis. + $fixture = Join-Path $PSScriptRoot 'fixtures/mainnet-blocks-0-4.bin' + Copy-Item -LiteralPath $fixture -Destination $output + Get-FileHash -LiteralPath $fixture -Algorithm SHA256 | Select-Object Algorithm, Hash | + ConvertTo-Json | Set-Content -LiteralPath (Join-Path $output 'fixture.json') +} + +$broker = Join-Path $output 'PrivateDesktopBroker.exe' +& $compiler '-MDelphi' '-O2' '-gl' "-FU$brokerUnits" "-o$broker" ` + (Join-Path $PSScriptRoot 'native\PrivateDesktopBroker.lpr') *> (Join-Path $output 'broker-build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'broker-build.log') -Tail 40 + exit $LASTEXITCODE +} +$temporaryData = Join-Path $output 'temporary-data' +New-Item -ItemType Directory -Path $temporaryData | Out-Null +$previousTemp = $env:TEMP +$previousTmp = $env:TMP +$previousBacklogCase = $env:PASCALCOIN_BACKLOG_CASE +$previousLargeDataRoot = $env:PASCALCOIN_LARGE_BACKLOG_ROOT +$selectorNames = @($demoSelectors.Values | ForEach-Object { $_ }) +$selectorNames = @($selectorNames | Sort-Object -Unique) +$previousSelectors = @{} +if ($useDemoSelectors) { + foreach ($name in $selectorNames) { + $previousSelectors[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + } +} +$status = 0 +$overallStatus = 0 +$caseResults = @() +try { + $env:TEMP = $temporaryData + $env:TMP = $temporaryData + if ($LargeBacklog) { $env:PASCALCOIN_LARGE_BACKLOG_ROOT = $ownedLargeRoot } + $cases = if ($Backlog) { + @('direct', 'synchronization', 'outgoing', 'outgoing-shutdown', 'incoming', + 'incoming-settings', 'incoming-shutdown', 'connected-miner') + } elseif ($AllDemoCases) { + $allDemoCaseNames + } elseif ($DemoCase) { + @($DemoCase) + } else { @('') } + foreach ($case in $cases) { + $suffix = if ($case) { "-$case" } else { '' } + $log = Join-Path $output "tests$suffix.log" + $brokerLog = Join-Path $output "broker$suffix.log" + if ($Backlog) { $env:PASCALCOIN_BACKLOG_CASE = $case } + if ($useDemoSelectors) { + # Focused selectors replace the default branch; clear all before each child. + foreach ($name in $selectorNames) { + [Environment]::SetEnvironmentVariable($name, $null, 'Process') + } + foreach ($name in $demoSelectors[$case]) { + [Environment]::SetEnvironmentVariable($name, '1', 'Process') + } + } + & $broker $executable $log *> $brokerLog + $status = $LASTEXITCODE + if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log } + elseif ($status -eq 0) { + Write-Error 'The grid refresh check did not produce its result log.' -ErrorAction Continue + $status = 1 + } + if ($status -eq 0 -and -not $Demo -and -not $LargeBacklog) { + $heap = "$log.heap.log" + if (-not (Test-Path -LiteralPath $heap) -or + -not ([IO.File]::ReadAllText($heap) -match '(?m)^0 unfreed memory blocks')) { + Write-Error 'The grid refresh fixture did not produce a clean heap report.' -ErrorAction Continue + $status = 1 + } + } + $caseResults += [pscustomobject]@{ Case = $case; ExitCode = $status; Log = $log } + $caseResults | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $output 'cases.json') + if ($status -ne 0) { + $overallStatus = $status + Get-Content -LiteralPath $brokerLog -Tail 25 + if (-not $AllDemoCases) { break } + } + } +} finally { + $env:TEMP = $previousTemp + $env:TMP = $previousTmp + $env:PASCALCOIN_BACKLOG_CASE = $previousBacklogCase + $env:PASCALCOIN_LARGE_BACKLOG_ROOT = $previousLargeDataRoot + if ($useDemoSelectors) { + foreach ($name in $selectorNames) { + [Environment]::SetEnvironmentVariable($name, $previousSelectors[$name], 'Process') + } + } +} +Write-Host "Wallet grid refresh evidence: $output" +exit $overallStatus diff --git a/src/tests/experimental/RunWalletSafeboxRecoveryTests.ps1 b/src/tests/experimental/RunWalletSafeboxRecoveryTests.ps1 new file mode 100644 index 000000000..ab0bfce87 --- /dev/null +++ b/src/tests/experimental/RunWalletSafeboxRecoveryTests.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS +Builds and tests interrupted safebox recovery using real loopback connections. +.DESCRIPTION +The probe tests late replies, sole-peer reconnection, connection leases, worker +handoff and strict rejection of invalid packets/chunks. It uses fresh data on an +owned private desktop and never connects to public peers or opens a user's wallet. +#> +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string] $OutputDirectory +) + +$ErrorActionPreference = 'Stop' +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +$source = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +$targetInfo = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if ($LASTEXITCODE -ne 0 -or $targetInfo.Count -ne 2 -or $targetInfo[1] -ne 'win64') { + throw 'The safebox recovery check requires a Windows x64 FPC compiler.' +} +if (-not $OutputDirectory) { + $OutputDirectory = Join-Path $PSScriptRoot ('lib\safebox-recovery-' + [guid]::NewGuid().ToString('N')) +} +$output = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $output) { throw "Choose a new output directory: $output" } +$units = Join-Path $output 'units' +$brokerUnits = Join-Path $output 'broker-units' +$temporaryData = Join-Path $output 'temporary-data' +New-Item -ItemType Directory -Path $units, $brokerUnits, $temporaryData -Force | Out-Null +$probeDirectory = Join-Path $PSScriptRoot 'native\safebox-recovery' +$executable = Join-Path $output 'SafeboxRecoveryProbe.exe' +$arguments = @('-B', '-MDelphi', '-O2', '-gl', '-Cr', '-Co', '-Ci', + '-dBorland', '-dVer150', '-dDelphi7', '-dCompiler6_Up', '-dPUREPASCAL', + "-FU$units", "-FE$output", "-o$executable", + "-Fi$source\libraries\generics.collections\inc", "-Fu$probeDirectory") +$directories = @('core', 'libraries\simplebaselib4pascal', 'libraries\hashlib4pascal', + 'libraries\cryptolib4pascal', 'libraries\synapse', 'libraries\sphere10', + 'libraries\generics.collections', 'libraries\pascalcoin', 'libraries\paszlib', + 'libraries\abstractmem', 'libraries\regex') +foreach ($directory in $directories) { $arguments += "-Fu$source\$directory" } +$arguments += Join-Path $probeDirectory 'SafeboxRecoveryProbe.lpr' +$arguments | Set-Content -LiteralPath (Join-Path $output 'compiler-arguments.txt') + +$previousTemp, $previousTmp = $env:TEMP, $env:TMP +try { + $env:TEMP = $temporaryData + $env:TMP = $temporaryData + & $compiler @arguments *> (Join-Path $output 'build.log') + if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'build.log') -Tail 60 + exit $LASTEXITCODE + } + # Prefer the deployed runtime, then the repository's canonical Windows DLL. + $crypto = Join-Path $source 'libcrypto-1_1-x64.dll' + if (-not (Test-Path -LiteralPath $crypto)) { + $crypto = Join-Path (Split-Path -Parent $source) 'windows-dll\libcrypto-1_1-x64.dll' + } + if (-not (Test-Path -LiteralPath $crypto)) { throw "OpenSSL runtime not found: $crypto" } + Copy-Item -LiteralPath $crypto -Destination $output + $broker = Join-Path $output 'PrivateDesktopBroker.exe' + & $compiler '-MDelphi' '-O2' '-gl' "-FU$brokerUnits" "-o$broker" ` + (Join-Path $PSScriptRoot 'native\PrivateDesktopBroker.lpr') *> (Join-Path $output 'broker-build.log') + if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'broker-build.log') -Tail 40 + exit $LASTEXITCODE + } + $log = Join-Path $output 'tests.log' + & $broker $executable $log *> (Join-Path $output 'broker.log') + $status = $LASTEXITCODE + if (Test-Path -LiteralPath $log) { + $text = [IO.File]::ReadAllText($log) + Write-Output $text + if ($text -notmatch '(?m)^RESULT cases=8 failures=0\s*$' -or $text -match '(?m)^FAIL') { + $status = 1 + } + } else { + Write-Error 'The safebox recovery check did not produce its result log.' -ErrorAction Continue + $status = 1 + } + $heap = "$log.heap.log" + if ($status -eq 0 -and (-not (Test-Path -LiteralPath $heap) -or + [IO.File]::ReadAllText($heap) -notmatch '(?m)^0 unfreed memory blocks')) { + Write-Error 'The safebox recovery check did not produce a clean heap report.' -ErrorAction Continue + $status = 1 + } + if ($status -ne 0) { Get-Content -LiteralPath (Join-Path $output 'broker.log') -Tail 25 } + Write-Host "Safebox recovery evidence: $output" + exit $status +} finally { + $env:TEMP, $env:TMP = $previousTemp, $previousTmp +} diff --git a/src/tests/experimental/ThreadingTests.lpr b/src/tests/experimental/ThreadingTests.lpr new file mode 100644 index 000000000..7a48ce77c --- /dev/null +++ b/src/tests/experimental/ThreadingTests.lpr @@ -0,0 +1,88 @@ +program ThreadingTests; + +{ Copyright (c) 2026 Herman Schoenfeld <https://www.sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. } + +{$mode delphi}{$H+} + +uses + // Install heap tracing before POSIX threads relocate the main thread's heap. + heaptrc, + {$IFDEF UNIX}cthreads,{$ENDIF} + SysUtils, fpcunit, testregistry, plaintestreport, + UThreadConcurrencyTests, UConcurrency.Tests, UMemory; + +procedure Trace(const AMessage: String); +begin + WriteLn(AMessage); + Flush(Output); +end; + +type + TProgressResultsWriter = class(TPlainResultsWriter) + public + procedure StartTest(ATest: TTest); override; + procedure EndTest(ATest: TTest); override; + procedure AddError(ATest: TTest; AError: TTestFailure); override; + procedure AddFailure(ATest: TTest; AFailure: TTestFailure); override; + end; + +procedure TProgressResultsWriter.StartTest(ATest: TTest); +begin + Trace('START ' + ATest.TestSuiteName + '.' + ATest.TestName); + inherited StartTest(ATest); +end; + +procedure TProgressResultsWriter.EndTest(ATest: TTest); +begin + Trace('END ' + ATest.TestSuiteName + '.' + ATest.TestName); + inherited EndTest(ATest); +end; + +procedure TProgressResultsWriter.AddError(ATest: TTest; AError: TTestFailure); +begin + Trace('ERROR ' + ATest.TestSuiteName + '.' + ATest.TestName + ': ' + AError.AsString); + inherited AddError(ATest, AError); +end; + +procedure TProgressResultsWriter.AddFailure(ATest: TTest; AFailure: TTestFailure); +begin + Trace('FAIL ' + ATest.TestSuiteName + '.' + ATest.TestName + ': ' + AFailure.AsString); + inherited AddFailure(ATest, AFailure); +end; + +procedure RunTests; +var + LResults: TTestResult; + LWriter: TPlainResultsWriter; + LDisposed: TDisposables; +begin + Trace('Creating result writer'); + // The result releases its listener before the disposal bag frees the writer. + LWriter := LDisposed.AddObject(TProgressResultsWriter.Create(nil)) as TPlainResultsWriter; + Trace('Creating test result'); + LResults := LDisposed.AddObject(TTestResult.Create) as TTestResult; + LWriter.FileName := ParamStr(1); + LResults.AddListener(LWriter); + Trace('Running registered fixtures'); + GetTestRegistry.Run(LResults); + Trace('Writing final test report'); + LWriter.WriteResult(LResults); + WriteLn('Errors: ', LResults.NumberOfErrors, '; failures: ', LResults.NumberOfFailures); + if (LResults.NumberOfErrors = 0) and (LResults.NumberOfFailures = 0) then + ExitCode := 0 + else + ExitCode := 1; + Trace('Disposing runner objects'); +end; + +begin + Trace('Program started; unit initialization completed'); + if ParamCount <> 1 then + Halt(64); + Trace('Configuring heap report'); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + Trace('Starting runner'); + RunTests; + Trace('Runner disposal completed'); +end. diff --git a/src/tests/experimental/core/UAbstractMemStorageFailureTests.pas b/src/tests/experimental/core/UAbstractMemStorageFailureTests.pas new file mode 100644 index 000000000..abfaa5809 --- /dev/null +++ b/src/tests/experimental/core/UAbstractMemStorageFailureTests.pas @@ -0,0 +1,66 @@ +unit UAbstractMemStorageFailureTests; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Registers the real optional-storage worker and shutdown regressions. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses SysUtils, fpcunit, testregistry, UAbstractMemStorageFailureChecks; + +type + TAbstractMemStorageFailureTests = class(TTestCase) + private + procedure RunCheck(ACheck: TAbstractMemStorageFailureCheck); + published + procedure FalseCompletionFlushPausesStorage; + procedure BackgroundWriteFailurePausesStorage; + procedure FailedQueueDrainReturnsWithoutDeadlock; + procedure FailedCleanupPreservesUnstableMarker; + procedure SuccessfulShutdownDrainsQueuedWrites; + procedure ShutdownWaitsForInFlightWrite; + end; + +implementation + +procedure TAbstractMemStorageFailureTests.RunCheck(ACheck: TAbstractMemStorageFailureCheck); +var LID: TGUID; +begin + AssertEquals('Create a unique owned test log',0,CreateGUID(LID)); + AssertEquals(0,RunAbstractMemStorageFailureCheck(ACheck,IncludeTrailingPathDelimiter(GetTempDir)+'AbstractMemStorage-'+GUIDToString(LID)+'.log')); +end; + +procedure TAbstractMemStorageFailureTests.FalseCompletionFlushPausesStorage; +begin + RunCheck(amscFalseCompletionFlush); +end; + +procedure TAbstractMemStorageFailureTests.BackgroundWriteFailurePausesStorage; +begin + RunCheck(amscBackgroundWriteFailure); +end; + +procedure TAbstractMemStorageFailureTests.FailedQueueDrainReturnsWithoutDeadlock; +begin + RunCheck(amscFailedQueueDrain); +end; + +procedure TAbstractMemStorageFailureTests.FailedCleanupPreservesUnstableMarker; +begin + RunCheck(amscFailedCleanupPreservesMarker); +end; + +procedure TAbstractMemStorageFailureTests.SuccessfulShutdownDrainsQueuedWrites; +begin + RunCheck(amscSuccessfulDrain); +end; + +procedure TAbstractMemStorageFailureTests.ShutdownWaitsForInFlightWrite; +begin + RunCheck(amscInFlightShutdown); +end; + +initialization + RegisterTest(TAbstractMemStorageFailureTests); +end. diff --git a/src/tests/experimental/core/UAccountCachePolicyTests.pas b/src/tests/experimental/core/UAccountCachePolicyTests.pas new file mode 100644 index 000000000..e81a55bec --- /dev/null +++ b/src/tests/experimental/core/UAccountCachePolicyTests.pas @@ -0,0 +1,65 @@ +unit UAccountCachePolicyTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses fpcunit, testregistry; + +type + TAccountCachePolicyTests = class(TTestCase) + published + procedure CoreDefaultRemains50000; + procedure ConfiguredLimitSurvivesMemoryReplacement; + procedure FileReplacementAndClearRetainPolicyWithoutStaleRecords; + procedure FailedReplacementFinallyPreservesOriginalOpenError; + procedure FailedReplacementDisposalPreservesOriginalOpenError; + procedure FailedConstructorPreservesOriginalOpenError; + procedure NormalClearPreservesSemanticsAndReleasesSnapshots; + end; + +implementation + +uses UAccountCachePolicyChecks; + +procedure TAccountCachePolicyTests.CoreDefaultRemains50000; +begin + CheckDefaultAccountCacheLimit; +end; + +procedure TAccountCachePolicyTests.ConfiguredLimitSurvivesMemoryReplacement; +begin + CheckAccountCacheLimitSurvivesMemoryReload; +end; + +procedure TAccountCachePolicyTests.FileReplacementAndClearRetainPolicyWithoutStaleRecords; +begin + CheckAccountCacheLimitSurvivesFileReloadAndClear; +end; + +procedure TAccountCachePolicyTests.FailedReplacementFinallyPreservesOriginalOpenError; +begin + CheckFailedSafeboxReplacementPreservesFinallyException; +end; + +procedure TAccountCachePolicyTests.FailedReplacementDisposalPreservesOriginalOpenError; +begin + CheckFailedSafeboxReplacementPreservesDisposalException; +end; + +procedure TAccountCachePolicyTests.FailedConstructorPreservesOriginalOpenError; +begin + CheckFailedSafeboxConstructionPreservesException; +end; + +procedure TAccountCachePolicyTests.NormalClearPreservesSemanticsAndReleasesSnapshots; +begin + CheckNormalSafeboxClearRetainsItsExistingSemantics; +end; + +initialization + RegisterTest(TAccountCachePolicyTests); + +end. diff --git a/src/tests/experimental/core/UBlockSanitizeTests.pas b/src/tests/experimental/core/UBlockSanitizeTests.pas new file mode 100644 index 000000000..b7c5b7813 --- /dev/null +++ b/src/tests/experimental/core/UBlockSanitizeTests.pas @@ -0,0 +1,88 @@ +unit UBlockSanitizeTests; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + FPCUnit registration reuses the same actual-operation checks as both native + compiler probes; operation, replay and index implementations are not copied. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses Classes, SysUtils, fpcunit, testregistry, UMempoolSanitizeChecks; + +type + TBlockSanitizeTests = class(TTestCase) + private + procedure RunCheck(ACheck: TMempoolSanitizeCheck); + published + procedure AllValidPreservesOrderHashFeesAndTreeOwnership; + procedure RejectedAndDuplicateOperationsRetainLegacySequence; + procedure DeletedDuplicateIndexForcesMaterializedFallback; + procedure CurrentProtocolFeeLimitAndSignerMetadataAreAdopted; + procedure ReplayExceptionPreservesOriginalSequenceAndReleasesLocks; + procedure OperationRegistryRetainsIndependentTreeOwners; + procedure RejectedRecoverMetadataAndEmptyReplayRemainCorrect; + procedure BulkIndexesPreserveFirstOccurrenceAndUnsignedReferenceBoundaries; + procedure SignatureCachesRespectBlockAndOperationProtocols; + end; + +implementation + +procedure TBlockSanitizeTests.RunCheck(ACheck: TMempoolSanitizeCheck); +var LID: TGUID; LPath: String; +begin + CreateGUID(LID); + LPath := IncludeTrailingPathDelimiter(GetTempDir) + + 'BlockSanitize-' + GUIDToString(LID) + '.log'; + AssertEquals('The shared actual-core scenario must succeed',0, + RunMempoolSanitizeCheck(ACheck,LPath)); +end; + +procedure TBlockSanitizeTests.AllValidPreservesOrderHashFeesAndTreeOwnership; +begin + RunCheck(mscAllValid); +end; + +procedure TBlockSanitizeTests.RejectedAndDuplicateOperationsRetainLegacySequence; +begin + RunCheck(mscRejectedAndDuplicates); +end; + +procedure TBlockSanitizeTests.DeletedDuplicateIndexForcesMaterializedFallback; +begin + RunCheck(mscDeletedDuplicateIndex); +end; + +procedure TBlockSanitizeTests.CurrentProtocolFeeLimitAndSignerMetadataAreAdopted; +begin + RunCheck(mscZeroFeeSignerLimit); +end; + +procedure TBlockSanitizeTests.ReplayExceptionPreservesOriginalSequenceAndReleasesLocks; +begin + RunCheck(mscReplayException); +end; + +procedure TBlockSanitizeTests.OperationRegistryRetainsIndependentTreeOwners; +begin + RunCheck(mscRegistryOwnership); +end; + +procedure TBlockSanitizeTests.RejectedRecoverMetadataAndEmptyReplayRemainCorrect; +begin + RunCheck(mscRecoverRejection); +end; + +procedure TBlockSanitizeTests.BulkIndexesPreserveFirstOccurrenceAndUnsignedReferenceBoundaries; +begin + RunCheck(mscReferenceBoundaries); +end; + +procedure TBlockSanitizeTests.SignatureCachesRespectBlockAndOperationProtocols; +begin + RunCheck(mscProtocolSignatureCaches); +end; + +initialization + RegisterTest(TBlockSanitizeTests); +end. diff --git a/src/tests/experimental/core/UChangeAccountInfoReplayTests.pas b/src/tests/experimental/core/UChangeAccountInfoReplayTests.pas new file mode 100644 index 000000000..389488fc9 --- /dev/null +++ b/src/tests/experimental/core/UChangeAccountInfoReplayTests.pas @@ -0,0 +1,47 @@ +unit UChangeAccountInfoReplayTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses fpcunit, testregistry; + +type + TChangeAccountInfoReplayTests = class(TTestCase) + published + procedure SelfTargetRetainsFeeNonceSealAndBaseIsolation; + procedure DistinctTargetRetainsSeparateSignerState; + procedure PendingEditsAndRollbackRetainExactAccountState; + procedure InvalidNonceCannotMutateAnAccount; + end; + +implementation + +uses UChangeAccountInfoReplayChecks; + +procedure TChangeAccountInfoReplayTests.SelfTargetRetainsFeeNonceSealAndBaseIsolation; +begin + CheckChangeInfoSelfTarget; +end; + +procedure TChangeAccountInfoReplayTests.DistinctTargetRetainsSeparateSignerState; +begin + CheckChangeInfoDistinctTarget; +end; + +procedure TChangeAccountInfoReplayTests.PendingEditsAndRollbackRetainExactAccountState; +begin + CheckChangeInfoRepeatedAndRollback; +end; + +procedure TChangeAccountInfoReplayTests.InvalidNonceCannotMutateAnAccount; +begin + CheckChangeInfoRejectsWithoutMutation; +end; + +initialization + RegisterTest(TChangeAccountInfoReplayTests); + +end. diff --git a/src/tests/experimental/core/UCoreLockMigrationChecks.pas b/src/tests/experimental/core/UCoreLockMigrationChecks.pas new file mode 100644 index 000000000..f6ef96f7e --- /dev/null +++ b/src/tests/experimental/core/UCoreLockMigrationChecks.pas @@ -0,0 +1,606 @@ +unit UCoreLockMigrationChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Exercises the migrated core owners and their public leases, without replacing + their collections, admission helpers or operation-component implementation. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type + TCoreLockMigrationCheck = (clmNamedDefault, clmNamedRecursive, clmThreadListRecursive, clmThreadListTryExclusion, clmThreadListCountExclusion, clmThreadListTimedWake, clmProtectedWrite, clmTimedWriteExclusion, clmTimedWriteWake, clmUnsignedTimeouts, clmOperationsRecursive, clmOperationsDestruction); + +function CoreLockMigrationCheckCount: Integer; +function RunCoreLockMigrationCheck(ACheck: TCoreLockMigrationCheck; const ALogFile: String): Integer; +function RunCoreLockMigrationChecks(const ALogFile: String): Integer; + +implementation + +uses Classes, SysUtils, SyncObjs, UThread, UBaseTypes, UBlockChain, UCrypto, + {$IFDEF FPC}Generics.Collections{$ELSE}System.Generics.Collections{$ENDIF}; + +type + TMigrationWorkerAction = (mwaProtectedWrite, mwaTimedWrite, mwaTimedList, mwaListCount, mwaOperations); + + TMigrationWorker = class(TThread) + private + FAction: TMigrationWorkerAction; + FLock: TReadWriteLock; + FList: TPCThreadList<Integer>; + FOperations: TPCOperationsComp; + FTimeout: Cardinal; + FStarted: Boolean; + protected + procedure Execute; override; + public + Attempting, Entered, ReleaseGate, Done: TEvent; + Acquired: Boolean; + ObservedCount: Integer; + ErrorText: String; + constructor Create(AAction: TMigrationWorkerAction; ALock: TReadWriteLock; AList: TPCThreadList<Integer>; AOperations: TPCOperationsComp; ATimeout: Cardinal); + destructor Destroy; override; + procedure StartOwned; + end; + +procedure Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure Await(AEvent: TEvent; const AMessage: String); +begin + Require(AEvent.WaitFor(5000)=wrSignaled,AMessage); +end; + +constructor TMigrationWorker.Create(AAction: TMigrationWorkerAction; ALock: TReadWriteLock; AList: TPCThreadList<Integer>; AOperations: TPCOperationsComp; ATimeout: Cardinal); +begin + inherited Create(True); + FreeOnTerminate := False; + FAction := AAction; + FLock := ALock; + FList := AList; + FOperations := AOperations; + FTimeout := ATimeout; + Attempting := TEvent.Create(nil,True,False,''); + Entered := TEvent.Create(nil,True,False,''); + ReleaseGate := TEvent.Create(nil,True,False,''); + Done := TEvent.Create(nil,True,False,''); + // Keep the native thread suspended until Delphi's AfterConstruction returns. + // Starting here changes its creation flag and makes Delphi start it twice. +end; + +procedure TMigrationWorker.StartOwned; +begin + Start; + FStarted := True; +end; + +destructor TMigrationWorker.Destroy; +begin + // Callers first release every owner lease and observe Done. This is a natural + // join; no test terminates a thread that might still own a production lock. + Terminate; + if Assigned(ReleaseGate) then ReleaseGate.SetEvent; + if FStarted then WaitFor; + // RTL destruction handles a never-started thread. Keep any constructed gates + // alive until that native retirement finishes, including constructor failure. + inherited; + Done.Free; + ReleaseGate.Free; + Entered.Free; + Attempting.Free; +end; + +procedure TMigrationWorker.Execute; +var LMutable: TList<Integer>; +begin + // A constructor failure can retire a suspended thread through RTL cleanup. + // That path must not touch incomplete event state or borrowed test owners. + if Terminated then Exit; + try + try + Attempting.SetEvent; + case FAction of + mwaProtectedWrite: begin + TPCThread.ProtectAcquireWriteLock(Self,FLock); + Acquired := True; + end; + mwaTimedWrite: Acquired := TPCThread.TryProtectAcquireWriteLock(Self,FTimeout,FLock); + mwaTimedList: Acquired := FList.TryLockList(FTimeout,LMutable); + mwaListCount: ObservedCount := FList.Count; + mwaOperations: begin + FOperations.Lock; + Acquired := True; + end; + end; + if Acquired then begin + try + if FAction=mwaTimedList then LMutable.Add(71); + Entered.SetEvent; + Await(ReleaseGate,'Worker release gate was not opened'); + finally + case FAction of + mwaProtectedWrite, mwaTimedWrite: FLock.ReleaseWriteLock; + mwaTimedList: FList.UnlockList; + mwaOperations: FOperations.Unlock; + end; + end; + end; + except + on E: Exception do ErrorText := E.ClassName+': '+E.Message; + end; + finally + Done.SetEvent; + end; +end; + +procedure FinishWorker(var AWorker: TMigrationWorker); +var LError: String; +begin + if not Assigned(AWorker) then Exit; + if not AWorker.FStarted then begin + FreeAndNil(AWorker); + Exit; + end; + AWorker.ReleaseGate.SetEvent; + Await(AWorker.Done,'Worker did not finish after owner release'); + AWorker.WaitFor; + LError := AWorker.ErrorText; + FreeAndNil(AWorker); + Require(LError='',LError); +end; + +procedure WaitForWriter(ALock: TReadWriteLock; AWorker: TMigrationWorker); +var LStarted: TTickCount; +begin + Await(AWorker.Attempting,'Writer did not reach admission'); + LStarted := TPlatform.GetTickCount; + // Observe the real admission counter, rather than copy the lock's timeout + // algorithm or guess when the worker should have started waiting. + while (ALock.WaitingWriteCount=0) and (AWorker.Done.WaitFor(0)<>wrSignaled) and + (TPlatform.GetElapsedMilliseconds(LStarted)<3000) do + AWorker.Done.WaitFor(1); + if AWorker.Done.WaitFor(0)=wrSignaled then + Require(AWorker.ErrorText='',AWorker.ErrorText); + Require(ALock.WaitingWriteCount=1,'Writer was not queued behind the held owner'); +end; + +procedure CheckNamedDefault; +var LLock, LUnnamed: TReadWriteLock; + LRaised, LUnexpectedAcquisition: Boolean; +begin + LLock := TReadWriteLock.Create('migration.default'); + LUnnamed := TReadWriteLock.Create; + try + Require(LLock.Name='migration.default','Named constructor lost its diagnostic name'); + Require(LLock.RecursionPolicy=lrpNoRecursion,'Named constructor changed the default recursion policy'); + Require(LUnnamed.Name='','Unnamed constructor did not preserve its empty name'); + Require(LUnnamed.RecursionPolicy=lrpNoRecursion,'Unnamed constructor changed the default recursion policy'); + LLock.AcquireWriteLock; + try + LRaised := False; + LUnexpectedAcquisition := False; + try + LUnexpectedAcquisition := LLock.TryAcquireWriteLock(0); + except + on E: ELockRecursionException do LRaised := True; + end; + if LUnexpectedAcquisition then LLock.ReleaseWriteLock; + Require(LRaised,'Named default lock allowed a recursive write'); + Require(LLock.RecursiveWriteCount=1,'Rejected recursion changed the existing owner'); + finally + LLock.ReleaseWriteLock; + end; + finally + LUnnamed.Free; + LLock.Free; + end; +end; + +procedure CheckNamedRecursive; +var LLock: TReadWriteLock; +begin + LLock := TReadWriteLock.Create('migration.recursive',lrpSupportsRecursion); + try + Require(LLock.Name='migration.recursive','Recursive named constructor lost its name'); + Require(LLock.RecursionPolicy=lrpSupportsRecursion,'Named constructor lost recursive policy'); + LLock.AcquireWriteLock; + try + LLock.AcquireWriteLock; + try + LLock.AcquireReadLock; + try + Require(LLock.RecursiveWriteCount=2,'Recursive writer depth was not preserved'); + Require(LLock.RecursiveReadCount=1,'Writer could not make a nested read'); + finally + LLock.ReleaseReadLock; + end; + finally + LLock.ReleaseWriteLock; + end; + finally + LLock.ReleaseWriteLock; + end; + Require(not LLock.IsWriteLockHeld,'Recursive writes were not fully released'); + Require(LLock.CurrentReadCount=0,'Nested read ownership leaked'); + finally + LLock.Free; + end; +end; + +procedure CheckThreadListRecursive; +var LOwner: TPCThreadList<Integer>; + LFirst, LSecond, LTimed: TList<Integer>; +begin + LOwner := TPCThreadList<Integer>.Create('migration.list.recursive'); + try + LFirst := LOwner.LockList; + try + LFirst.Add(11); + LSecond := LOwner.LockList; + try + Require(LFirst=LSecond,'Nested mutable lease returned another list'); + Require(LOwner.Add(22)=1,'Add did not preserve recursive owner admission'); + Require(LOwner.Count=2,'Count could not read inside a mutable write lease'); + LOwner.Remove(11); + Require(LOwner.TryLockList(0,LTimed),'Same-thread nonblocking recursive lease failed'); + try + Require(LTimed=LFirst,'Timed lease did not return the owned list'); + LTimed.Add(33); + finally + LOwner.UnlockList; + end; + Require(LOwner.Count=2,'Nested mutation/count result differs'); + LOwner.Clear; + Require(LOwner.Count=0,'Clear could not reenter the mutable owner'); + finally + LOwner.UnlockList; + end; + finally + LOwner.UnlockList; + end; + Require(LOwner.Count=0,'List owner was not usable after releasing all leases'); + finally + LOwner.Free; + end; +end; + +procedure CheckThreadListTryExclusion; +var LOwner: TPCThreadList<Integer>; + LWorker: TMigrationWorker; + LHeld: Boolean; +begin + LOwner := TPCThreadList<Integer>.Create('migration.list.try'); + LWorker := nil; + LHeld := False; + try + LOwner.LockList; + LHeld := True; + LWorker := TMigrationWorker.Create(mwaTimedList,nil,LOwner,nil,0); + LWorker.StartOwned; + Await(LWorker.Done,'Nonblocking list admission did not return'); + Require(LWorker.ErrorText='',LWorker.ErrorText); + Require(not LWorker.Acquired,'Another thread obtained the mutable list while owned'); + Require(LOwner.Count=0,'Failed list admission changed the protected list'); + FinishWorker(LWorker); + LOwner.UnlockList; + LHeld := False; + LWorker := TMigrationWorker.Create(mwaTimedList,nil,LOwner,nil,0); + LWorker.StartOwned; + Await(LWorker.Entered,'List remained locked after its owner released'); + FinishWorker(LWorker); + Require(LOwner.Count=1,'Successful mutable admission did not apply its mutation'); + finally + if LHeld then LOwner.UnlockList; + try + FinishWorker(LWorker); + finally + LOwner.Free; + end; + end; +end; + +procedure CheckThreadListCountExclusion; +var LOwner: TPCThreadList<Integer>; + LWorker: TMigrationWorker; + LMutable: TList<Integer>; + LHeld: Boolean; +begin + LOwner := TPCThreadList<Integer>.Create('migration.list.count'); + LWorker := nil; + LHeld := False; + try + LMutable := LOwner.LockList; + LHeld := True; + LMutable.Add(19); + LWorker := TMigrationWorker.Create(mwaListCount,nil,LOwner,nil,0); + LWorker.StartOwned; + Await(LWorker.Attempting,'Count reader did not start'); + // Count must not observe a mutable lease owned by another thread. This is + // a bounded negative event assertion, not a production debounce delay. + Require(LWorker.Done.WaitFor(150)=wrTimeout,'Count bypassed another thread''s mutable lease'); + LOwner.UnlockList; + LHeld := False; + Await(LWorker.Done,'Count did not wake after the mutable lease ended'); + Require(LWorker.ObservedCount=1,'Count did not observe the completed mutation'); + finally + if LHeld then LOwner.UnlockList; + try + FinishWorker(LWorker); + finally + LOwner.Free; + end; + end; +end; + +procedure CheckThreadListTimedWake; +var LOwner: TPCThreadList<Integer>; + LWorker: TMigrationWorker; + LHeld: Integer; +begin + LOwner := TPCThreadList<Integer>.Create('migration.list.wait'); + LWorker := nil; + LHeld := 0; + try + LOwner.LockList; + Inc(LHeld); + LOwner.LockList; + Inc(LHeld); + LWorker := TMigrationWorker.Create(mwaTimedList,nil,LOwner,nil,2500); + LWorker.StartOwned; + Await(LWorker.Attempting,'Timed list writer did not start'); + Require(LWorker.Entered.WaitFor(150)=wrTimeout,'Timed writer bypassed nested mutable leases'); + LOwner.UnlockList; + Dec(LHeld); + Require(LWorker.Entered.WaitFor(150)=wrTimeout,'Releasing one nested lease released another owner''s protection'); + LOwner.UnlockList; + Dec(LHeld); + Await(LWorker.Entered,'Timed list writer did not wake after the final owner release'); + FinishWorker(LWorker); + Require(LOwner.Count=1,'Woken writer did not mutate the actual list'); + finally + while LHeld>0 do begin + LOwner.UnlockList; + Dec(LHeld); + end; + try + FinishWorker(LWorker); + finally + LOwner.Free; + end; + end; +end; + +procedure CheckProtectedWrite; +var LLock: TReadWriteLock; + LWorker: TMigrationWorker; + LHeld: Integer; +begin + LLock := TReadWriteLock.Create('migration.protect',lrpSupportsRecursion); + LWorker := nil; + LHeld := 0; + try + TPCThread.ProtectAcquireWriteLock(nil,LLock); + Inc(LHeld); + TPCThread.ProtectAcquireWriteLock(nil,LLock); + Inc(LHeld); + Require(LLock.RecursiveWriteCount=2,'Protected admission lost recursive write ownership'); + LWorker := TMigrationWorker.Create(mwaProtectedWrite,LLock,nil,nil,0); + LWorker.StartOwned; + WaitForWriter(LLock,LWorker); + LLock.ReleaseWriteLock; + Dec(LHeld); + Require(LLock.WaitingWriteCount=1,'Partial recursive release woke the competing writer'); + LLock.ReleaseWriteLock; + Dec(LHeld); + Await(LWorker.Entered,'Protected writer did not wake after final release'); + finally + while LHeld>0 do begin + LLock.ReleaseWriteLock; + Dec(LHeld); + end; + try + FinishWorker(LWorker); + finally + LLock.Free; + end; + end; +end; + +procedure CheckTimedWriteExclusion; +var LLock: TReadWriteLock; + LWorker: TMigrationWorker; + LHeld: Boolean; +begin + LLock := TReadWriteLock.Create('migration.try',lrpSupportsRecursion); + LWorker := nil; + LHeld := False; + try + LLock.AcquireReadLock; + LHeld := True; + LWorker := TMigrationWorker.Create(mwaTimedWrite,LLock,nil,nil,0); + LWorker.StartOwned; + Await(LWorker.Done,'Nonblocking helper did not return'); + Require(LWorker.ErrorText='',LWorker.ErrorText); + Require(not LWorker.Acquired,'Write helper bypassed another thread''s read ownership'); + Require(LLock.CurrentReadCount=1,'Failed helper admission changed the reader owner'); + Require(LLock.WaitingWriteCount=0,'Nonblocking helper left a queued writer'); + finally + if LHeld then LLock.ReleaseReadLock; + try + FinishWorker(LWorker); + finally + LLock.Free; + end; + end; +end; + +procedure CheckTimedWriteWake(ATimeout: Cardinal); +var LLock: TReadWriteLock; + LWorker: TMigrationWorker; + LHeld, LUnexpectedRead: Boolean; +begin + LLock := TReadWriteLock.Create('migration.timed',lrpSupportsRecursion); + LWorker := nil; + LHeld := False; + try + LLock.AcquireWriteLock; + LHeld := True; + LWorker := TMigrationWorker.Create(mwaTimedWrite,LLock,nil,nil,ATimeout); + LWorker.StartOwned; + WaitForWriter(LLock,LWorker); + Require(not LWorker.Acquired,'Timed helper acquired before the owner released'); + LLock.ReleaseWriteLock; + LHeld := False; + Await(LWorker.Entered,'Timed helper did not wake after owner release'); + Require(LWorker.Acquired,'Timed helper failed after a prompt owner release'); + LUnexpectedRead := LLock.TryAcquireReadLock(0); + if LUnexpectedRead then LLock.ReleaseReadLock; + Require(not LUnexpectedRead,'Acquired helper did not hold exclusive ownership'); + finally + if LHeld then LLock.ReleaseWriteLock; + try + FinishWorker(LWorker); + finally + LLock.Free; + end; + end; +end; + +procedure CheckUnsignedTimeouts; +begin + // Values beyond signed Integer range exercise the helper's unsigned clamp. + // Observe queued admission then release promptly instead of waiting a minute. + CheckTimedWriteWake(60001); + CheckTimedWriteWake(High(Cardinal)-1); + CheckTimedWriteWake(High(Cardinal)); +end; + +procedure CheckOperationsRecursive; +var LOperations: TPCOperationsComp; + LWorker: TMigrationWorker; + LHeld: Integer; +begin + // Operation construction hashes the empty tree; initialize its real backend + // here so this case does not depend on another fixture having run first. + TCrypto.InitCrypto; + LOperations := TPCOperationsComp.Create(nil); + LWorker := nil; + LHeld := 0; + try + LOperations.Lock; + Inc(LHeld); + LOperations.Lock; + Inc(LHeld); + LOperations.Clear(True); + Require(LOperations.Count=0,'Clear did not reenter the operation component'); + LWorker := TMigrationWorker.Create(mwaOperations,nil,nil,LOperations,0); + LWorker.StartOwned; + Await(LWorker.Attempting,'Operation-component writer did not start'); + Require(LWorker.Entered.WaitFor(150)=wrTimeout,'Operation writer bypassed nested ownership'); + LOperations.Unlock; + Dec(LHeld); + Require(LWorker.Entered.WaitFor(150)=wrTimeout,'Partial operation release lost the outer write'); + LOperations.Unlock; + Dec(LHeld); + Await(LWorker.Entered,'Operation writer did not wake after the outer lease ended'); + finally + while LHeld>0 do begin + LOperations.Unlock; + Dec(LHeld); + end; + try + FinishWorker(LWorker); + finally + LOperations.Free; + end; + end; +end; + +procedure CheckOperationsDestruction; +var I: Integer; + LOperations: TPCOperationsComp; +begin + TCrypto.InitCrypto; + for I := 1 to 8 do begin + LOperations := TPCOperationsComp.Create(nil); + try + LOperations.Lock; + try + LOperations.Clear(True); + finally + LOperations.Unlock; + end; + finally + // The real destructor calls Clear while holding its own recursive write; + // it must balance that outer write before disposing the migrated lock. + LOperations.Free; + end; + end; +end; + +function CoreLockMigrationCheckCount: Integer; +begin + Result := Ord(High(TCoreLockMigrationCheck))+1; +end; + +function RunCoreLockMigrationCheck(ACheck: TCoreLockMigrationCheck; const ALogFile: String): Integer; +var LLog: TStringList; +begin + Result := 1; + Require(not FileExists(ALogFile),'Refusing an existing core-lock migration log'); + LLog := TStringList.Create; + try + try + case ACheck of + clmNamedDefault: CheckNamedDefault; + clmNamedRecursive: CheckNamedRecursive; + clmThreadListRecursive: CheckThreadListRecursive; + clmThreadListTryExclusion: CheckThreadListTryExclusion; + clmThreadListCountExclusion: CheckThreadListCountExclusion; + clmThreadListTimedWake: CheckThreadListTimedWake; + clmProtectedWrite: CheckProtectedWrite; + clmTimedWriteExclusion: CheckTimedWriteExclusion; + clmTimedWriteWake: CheckTimedWriteWake(2500); + clmUnsignedTimeouts: CheckUnsignedTimeouts; + clmOperationsRecursive: CheckOperationsRecursive; + clmOperationsDestruction: CheckOperationsDestruction; + end; + LLog.Add(Format('PASS core-lock migration %d',[Ord(ACheck)+1])); + Result := 0; + except + on E: Exception do LLog.Add('FAIL '+E.ClassName+': '+E.Message); + end; + LLog.SaveToFile(ALogFile); + finally + LLog.Free; + end; +end; + +function RunCoreLockMigrationChecks(const ALogFile: String): Integer; +var LCheck: TCoreLockMigrationCheck; + LLog: TStringList; +begin + Result := 1; + Require(not FileExists(ALogFile),'Refusing an existing core-lock migration summary'); + LLog := TStringList.Create; + try + for LCheck := Low(TCoreLockMigrationCheck) to High(TCoreLockMigrationCheck) do begin + if RunCoreLockMigrationCheck(LCheck,ALogFile+'.'+IntToStr(Ord(LCheck)+1)+'.log')<>0 then begin + LLog.Add(Format('FAIL core-lock migration %d; see scenario log',[Ord(LCheck)+1])); + LLog.SaveToFile(ALogFile); + Exit; + end; + LLog.Add(Format('PASS core-lock migration %d',[Ord(LCheck)+1])); + LLog.SaveToFile(ALogFile); + end; + LLog.Add(Format('PASS total: %d',[CoreLockMigrationCheckCount])); + LLog.SaveToFile(ALogFile); + Result := 0; + finally + LLog.Free; + end; +end; + +end. diff --git a/src/tests/experimental/core/UCoreLockMigrationTests.pas b/src/tests/experimental/core/UCoreLockMigrationTests.pas new file mode 100644 index 000000000..f107bf8cc --- /dev/null +++ b/src/tests/experimental/core/UCoreLockMigrationTests.pas @@ -0,0 +1,102 @@ +unit UCoreLockMigrationTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Registers actual core owner/lease checks shared with native Delphi probes. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses SysUtils, fpcunit, testregistry, UCoreLockMigrationChecks; + +type + TCoreApplicationLockMigrationTests = class(TTestCase) + private + procedure RunCheck(ACheck: TCoreLockMigrationCheck); + published + procedure NamedConstructorPreservesDefaultPolicy; + procedure NamedConstructorPreservesRecursiveOwnership; + procedure MutableThreadListLeaseRetainsRecursion; + procedure ThreadListTryLeaseExcludesOtherOwners; + procedure ThreadListCountWaitsForMutableOwner; + procedure ThreadListWaiterWakesAfterFinalNestedRelease; + procedure ProtectedWriteRetainsRecursiveOwnership; + procedure TimedWriteExcludesExistingReaders; + procedure TimedWriteWakesAfterOwnerRelease; + procedure UnsignedTimeoutsAreClampedBeforeSignedConversion; + procedure OperationsComponentRetainsRecursiveLease; + procedure OperationsComponentDestructionBalancesOwnership; + end; + +implementation + +procedure TCoreApplicationLockMigrationTests.RunCheck(ACheck: TCoreLockMigrationCheck); +var LID: TGUID; +begin + AssertEquals('Create a unique owned core-lock log',0,CreateGUID(LID)); + AssertEquals(0,RunCoreLockMigrationCheck(ACheck,IncludeTrailingPathDelimiter(GetTempDir)+'CoreLockMigration-'+GUIDToString(LID)+'.log')); +end; + +procedure TCoreApplicationLockMigrationTests.NamedConstructorPreservesDefaultPolicy; +begin + RunCheck(clmNamedDefault); +end; + +procedure TCoreApplicationLockMigrationTests.NamedConstructorPreservesRecursiveOwnership; +begin + RunCheck(clmNamedRecursive); +end; + +procedure TCoreApplicationLockMigrationTests.MutableThreadListLeaseRetainsRecursion; +begin + RunCheck(clmThreadListRecursive); +end; + +procedure TCoreApplicationLockMigrationTests.ThreadListTryLeaseExcludesOtherOwners; +begin + RunCheck(clmThreadListTryExclusion); +end; + +procedure TCoreApplicationLockMigrationTests.ThreadListCountWaitsForMutableOwner; +begin + RunCheck(clmThreadListCountExclusion); +end; + +procedure TCoreApplicationLockMigrationTests.ThreadListWaiterWakesAfterFinalNestedRelease; +begin + RunCheck(clmThreadListTimedWake); +end; + +procedure TCoreApplicationLockMigrationTests.ProtectedWriteRetainsRecursiveOwnership; +begin + RunCheck(clmProtectedWrite); +end; + +procedure TCoreApplicationLockMigrationTests.TimedWriteExcludesExistingReaders; +begin + RunCheck(clmTimedWriteExclusion); +end; + +procedure TCoreApplicationLockMigrationTests.TimedWriteWakesAfterOwnerRelease; +begin + RunCheck(clmTimedWriteWake); +end; + +procedure TCoreApplicationLockMigrationTests.UnsignedTimeoutsAreClampedBeforeSignedConversion; +begin + RunCheck(clmUnsignedTimeouts); +end; + +procedure TCoreApplicationLockMigrationTests.OperationsComponentRetainsRecursiveLease; +begin + RunCheck(clmOperationsRecursive); +end; + +procedure TCoreApplicationLockMigrationTests.OperationsComponentDestructionBalancesOwnership; +begin + RunCheck(clmOperationsDestruction); +end; + +initialization + RegisterTest(TCoreApplicationLockMigrationTests); +end. diff --git a/src/tests/experimental/core/UDownloadWorkerLifetimeTests.pas b/src/tests/experimental/core/UDownloadWorkerLifetimeTests.pas new file mode 100644 index 000000000..42590ba23 --- /dev/null +++ b/src/tests/experimental/core/UDownloadWorkerLifetimeTests.pas @@ -0,0 +1,74 @@ +unit UDownloadWorkerLifetimeTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + The same socket/owner checks run in the native Delphi and FPC probes. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses SysUtils, fpcunit, testregistry, UDownloadLifetimeChecks; + +type + TDownloadWorkerLifetimeTests = class(TTestCase) + private + procedure RunCheck(ACheck: TDownloadLifetimeCheck); + published + procedure SelectedConnectionOutlivesDownloadAndFactoryFailureDoesNotPoisonAdmission; + procedure UnselectedAndIncomingTransportsRetireAtTheirOwnBoundaries; + procedure ShutdownDeniesNewWorkersAndJoinsBeforeOwnerDestruction; + procedure OwnedServerCallbacksRetainNodeResourcesDuringDestruction; + procedure ShutdownOwnsDiscoveryBeforeTheWorkerStarts; + procedure ShutdownJoinsDiscoveryAndDeniesLateClientCreation; + procedure DrainRechecksRegistryAfterAnEarlierEmptyScan; + end; + +implementation + +procedure TDownloadWorkerLifetimeTests.RunCheck(ACheck: TDownloadLifetimeCheck); +var LID: TGUID; LLog: String; +begin + CreateGUID(LID); + LLog:=IncludeTrailingPathDelimiter(GetTempDir)+'DownloadLifetime-'+GUIDToString(LID)+'.log'; + AssertEquals('See actual connection/owner scenario log '+LLog,0, + RunDownloadLifetimeCheck(ACheck,LLog)); +end; + +procedure TDownloadWorkerLifetimeTests.SelectedConnectionOutlivesDownloadAndFactoryFailureDoesNotPoisonAdmission; +begin + RunCheck(dlcOutgoing); +end; + +procedure TDownloadWorkerLifetimeTests.UnselectedAndIncomingTransportsRetireAtTheirOwnBoundaries; +begin + RunCheck(dlcIncoming); +end; + +procedure TDownloadWorkerLifetimeTests.ShutdownDeniesNewWorkersAndJoinsBeforeOwnerDestruction; +begin + RunCheck(dlcShutdown); +end; + +procedure TDownloadWorkerLifetimeTests.OwnedServerCallbacksRetainNodeResourcesDuringDestruction; +begin + RunCheck(dlcNodeCallbacks); +end; + +procedure TDownloadWorkerLifetimeTests.ShutdownOwnsDiscoveryBeforeTheWorkerStarts; +begin + RunCheck(dlcPendingDiscovery); +end; + +procedure TDownloadWorkerLifetimeTests.ShutdownJoinsDiscoveryAndDeniesLateClientCreation; +begin + RunCheck(dlcActiveDiscovery); +end; + +procedure TDownloadWorkerLifetimeTests.DrainRechecksRegistryAfterAnEarlierEmptyScan; +begin + RunCheck(dlcLateConnection); +end; + +initialization + RegisterTest(TDownloadWorkerLifetimeTests); +end. diff --git a/src/tests/experimental/core/ULegacyConnectionLifetimeTests.pas b/src/tests/experimental/core/ULegacyConnectionLifetimeTests.pas new file mode 100644 index 000000000..6feb966fe --- /dev/null +++ b/src/tests/experimental/core/ULegacyConnectionLifetimeTests.pas @@ -0,0 +1,50 @@ +unit ULegacyConnectionLifetimeTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Reuse the actual native acceptance helper in the public FPCUnit suite. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses SysUtils, fpcunit, testregistry, ULegacyConnectionLifetimeChecks; + +type + TLegacyConnectionLifetimeTests = class(TTestCase) + private + procedure RunCheck(ACheck: TLegacyConnectionLifetimeCheck); + published + procedure RetirementWaitsForHeldConnectionLock; + procedure NestedConnectionLocksRetainLifetimeUntilBothExits; + procedure TimedAndNonblockingFailuresReleaseLifetimePins; + end; + +implementation + +procedure TLegacyConnectionLifetimeTests.RunCheck(ACheck: TLegacyConnectionLifetimeCheck); +var LID: TGUID; LLog: String; +begin + CreateGUID(LID); + LLog:=IncludeTrailingPathDelimiter(GetTempDir)+'LegacyConnection-'+GUIDToString(LID)+'.log'; + AssertEquals('See actual connection ownership scenario log '+LLog,0, + RunLegacyConnectionLifetimeCheck(ACheck,LLog)); +end; + +procedure TLegacyConnectionLifetimeTests.RetirementWaitsForHeldConnectionLock; +begin + RunCheck(llcHeldLock); +end; + +procedure TLegacyConnectionLifetimeTests.NestedConnectionLocksRetainLifetimeUntilBothExits; +begin + RunCheck(llcRecursiveLock); +end; + +procedure TLegacyConnectionLifetimeTests.TimedAndNonblockingFailuresReleaseLifetimePins; +begin + RunCheck(llcTimedAdmission); +end; + +initialization + RegisterTest(TLegacyConnectionLifetimeTests); +end. diff --git a/src/tests/experimental/core/ULogGuiHandoffChecks.pas b/src/tests/experimental/core/ULogGuiHandoffChecks.pas new file mode 100644 index 000000000..d38bf801b --- /dev/null +++ b/src/tests/experimental/core/ULogGuiHandoffChecks.pas @@ -0,0 +1,394 @@ +unit ULogGuiHandoffChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Checks the actual logger's producer-to-GUI handoff without starting a node. } + +{$IFDEF FPC}{$MODE Delphi}{$H+}{$ENDIF} + +interface + +const LogGuiHandoffCheckCount = 6; +function RunLogGuiHandoffChecks(const ALogPath: String; AProducerOnly: Boolean = False): Integer; + +implementation + +uses Classes, SysUtils, SyncObjs, ULog, UConst; + +type + TLogProducer = class(TThread) + private + FLog: TLog; + FStarted: Boolean; + protected + procedure Execute; override; + public + Entered, Attempted, Done: TEvent; + ErrorText: String; + constructor Create(ALog: TLog); + destructor Destroy; override; + procedure StartOwned; + end; + + TLogReceiver = class + public + Log: TLog; + Producer: TLogProducer; + Messages, Journal: TStringList; + BeginCount, EndCount, SynchronousCount: Integer; + ProducerAttempted, ProducerCompleted: Boolean; + Reenter, RaiseRecord, RaiseBegin, RaiseEnd, RemoveHandler: Boolean; + constructor Create; + destructor Destroy; override; + procedure Receive(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); + procedure ReceiveSynchronous(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); + procedure BeginBatch(Sender: TObject); + procedure EndBatch(Sender: TObject); + end; + + TLogFixture = class + public + Log: TLog; + Dispatch: TThreadSafeLogEvent; + Receiver: TLogReceiver; + constructor Create; + destructor Destroy; override; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create(AMessage); +end; + +constructor TLogProducer.Create(ALog: TLog); +begin + inherited Create(True); + FreeOnTerminate := False; + FLog := ALog; + Entered := TEvent.Create(nil,True,False,''); + Attempted := TEvent.Create(nil,True,False,''); + Done := TEvent.Create(nil,True,False,''); +end; + +procedure TLogProducer.StartOwned; +begin + Start; + FStarted := True; +end; + +destructor TLogProducer.Destroy; +begin + Terminate; + if Assigned(Entered) then + Entered.SetEvent; + if FStarted then + WaitFor; + inherited Destroy; + Done.Free; + Attempted.Free; + Entered.Free; +end; + +procedure TLogProducer.Execute; +begin + try + try + Require(Entered.WaitFor(5000)=wrSignaled,'GUI callback did not release the producer barrier'); + if Terminated then + Exit; + Attempted.SetEvent; + FLog.NotifyNewLog(ltupdate,'producer','late-one'); + FLog.NotifyNewLog(lterror,'producer','late-two'); + except + on E: Exception do + ErrorText := E.ClassName+': '+E.Message; + end; + finally + Done.SetEvent; + end; +end; + +constructor TLogReceiver.Create; +begin + inherited Create; + Messages := TStringList.Create; + Journal := TStringList.Create; +end; + +destructor TLogReceiver.Destroy; +begin + Journal.Free; + Messages.Free; + inherited Destroy; +end; + +procedure TLogReceiver.Receive(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); +begin + Require(TThread.CurrentThread.ThreadID=MainThreadID,'GUI callback left the main thread'); + Messages.Add(AText); + Journal.Add(AText); + if (Messages.Count=1) and Assigned(Producer) then begin + Producer.Entered.SetEvent; + ProducerAttempted := Producer.Attempted.WaitFor(3000)=wrSignaled; + // A bounded wait exposes the old queue lock without leaving RED deadlocked: + // returning from this callback lets that old producer finish normally. + ProducerCompleted := Producer.Done.WaitFor(1000)=wrSignaled; + end; + if (Messages.Count=1) and Reenter then + Log.NotifyNewLog(ltdebug,'reentrant','next-batch'); + if RemoveHandler then + Log.OnNewLog := nil; + if RaiseRecord then + raise Exception.Create('expected record callback failure'); +end; + +procedure TLogReceiver.ReceiveSynchronous(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); +begin + Require(AThreadID=TThread.CurrentThread.ThreadID,'synchronous callback changed producer thread'); + Inc(SynchronousCount); +end; + +procedure TLogReceiver.BeginBatch(Sender: TObject); +begin + Inc(BeginCount); + Journal.Add('begin'); + if RaiseBegin then + raise Exception.Create('expected begin callback failure'); +end; + +procedure TLogReceiver.EndBatch(Sender: TObject); +begin + Inc(EndCount); + Journal.Add('end'); + if RaiseEnd then + raise Exception.Create('expected end callback failure'); +end; + +constructor TLogFixture.Create; +begin + inherited Create; + Receiver := TLogReceiver.Create; + Log := TLog.Create(nil); + Log.ProcessGlobalLogs := False; + Receiver.Log := Log; + Log.OnNewLog := Receiver.Receive; + {$IFNDEF LOG_GUI_HANDOFF_OLD_API} + Log.OnNewLogBatchBegin := Receiver.BeginBatch; + Log.OnNewLogBatchEnd := Receiver.EndBatch; + {$ENDIF} + // Invoke the public delivery helper on the real queue. Its background sibling + // cannot dispatch until this console's main thread pumps synchronization. + Dispatch := TThreadSafeLogEvent.Create(True); + Dispatch.FLog := Log; +end; + +destructor TLogFixture.Destroy; +begin + Dispatch.Free; + Log.Free; + Receiver.Free; + inherited Destroy; +end; + +procedure CheckProducerHandoff; +var LFixture: TLogFixture; LProducer: TLogProducer; +begin + LFixture := TLogFixture.Create; + LProducer := nil; + try + LProducer := TLogProducer.Create(LFixture.Log); + LFixture.Receiver.Producer := LProducer; + LProducer.StartOwned; + LFixture.Log.NotifyNewLog(ltinfo,'main','initial'); + LFixture.Dispatch.SynchronizedProcess; + Require(LProducer.Done.WaitFor(5000)=wrSignaled,'producer did not finish after GUI callback returned'); + LProducer.WaitFor; + Require(LProducer.ErrorText='',LProducer.ErrorText); + Require(LFixture.Receiver.ProducerAttempted,'producer never attempted admission during the GUI callback'); + Require(LFixture.Receiver.ProducerCompleted,'producer waited for the GUI callback to return'); + Require(LFixture.Receiver.Messages.Count=1,'new producer records entered the detached batch'); + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.Messages.CommaText='initial,late-one,late-two','producer FIFO or next-batch delivery changed'); + finally + LProducer.Free; + LFixture.Free; + end; +end; + +procedure CheckReentrantFIFO; +var LFixture: TLogFixture; +begin + LFixture := TLogFixture.Create; + try + LFixture.Receiver.Reenter := True; + LFixture.Log.NotifyNewLog(ltinfo,'main','first'); + LFixture.Log.NotifyNewLog(ltinfo,'main','second'); + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.Messages.CommaText='first,second','current batch changed order or swallowed reentrant input'); + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.Messages.CommaText='first,second,next-batch','reentrant message did not remain for the next batch'); + Require(LFixture.Receiver.Journal.CommaText='begin,first,second,end,begin,next-batch,end','batch brackets do not surround FIFO delivery'); + finally + LFixture.Free; + end; +end; + +procedure CheckRecordException; +var LFixture: TLogFixture; LRaised: Boolean; +begin + LFixture := TLogFixture.Create; + try + LFixture.Receiver.RaiseRecord := True; + LFixture.Log.NotifyNewLog(ltinfo,'main',StringOfChar('a',257)); + LFixture.Log.NotifyNewLog(ltinfo,'main',StringOfChar('b',263)); + LRaised := False; + try + LFixture.Dispatch.SynchronizedProcess; + except + on E: Exception do begin + LRaised := True; + Require(E.Message='expected record callback failure','record exception changed'); + end; + end; + Require(LRaised,'record callback exception was swallowed'); + Require((LFixture.Receiver.BeginCount=1) and (LFixture.Receiver.EndCount=1),'batch end was skipped after a record exception'); + LFixture.Receiver.RaiseRecord := False; + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.Messages.Count=1,'retired exceptional batch was delivered again'); + LFixture.Log.NotifyNewLog(ltinfo,'main','after-error'); + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.Messages.Count=2,'queue was unusable after an exceptional batch'); + finally + LFixture.Free; + end; +end; + +procedure CheckHookExceptions; +var LFixture: TLogFixture; LRaised: Boolean; I: Integer; +begin + for I := 0 to 1 do begin + LFixture := TLogFixture.Create; + try + LFixture.Receiver.RaiseBegin := I=0; + LFixture.Receiver.RaiseEnd := I=1; + LFixture.Log.NotifyNewLog(ltinfo,'main',StringOfChar('c',269)); + LRaised := False; + try + LFixture.Dispatch.SynchronizedProcess; + except + on E: Exception do begin + LRaised := True; + if I=0 then + Require(E.Message='expected begin callback failure','begin exception changed') + else + Require(E.Message='expected end callback failure','end exception changed'); + end; + end; + Require(LRaised,'hook exception was swallowed'); + Require(LFixture.Receiver.BeginCount=1,'batch begin count changed'); + Require(LFixture.Receiver.EndCount=I,'end ran after failed begin or failed to run after successful begin'); + LFixture.Receiver.RaiseBegin := False; + LFixture.Receiver.RaiseEnd := False; + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.BeginCount=1,'exceptional detached data remained queued'); + LFixture.Log.NotifyNewLog(ltinfo,'main','after-hook-error'); + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.BeginCount=2,'queue was unusable after a hook exception'); + finally + LFixture.Free; + end; + end; +end; + +procedure CheckEmptyAndUnsubscribe; +var LFixture: TLogFixture; +begin + LFixture := TLogFixture.Create; + try + LFixture.Dispatch.SynchronizedProcess; + Require((LFixture.Receiver.BeginCount=0) and (LFixture.Receiver.EndCount=0),'empty batch emitted presentation hooks'); + LFixture.Receiver.RemoveHandler := True; + LFixture.Log.NotifyNewLog(ltinfo,'main','first'); + LFixture.Log.NotifyNewLog(ltinfo,'main',StringOfChar('d',277)); + LFixture.Dispatch.SynchronizedProcess; + Require((LFixture.Receiver.Messages.Count=1) and (LFixture.Receiver.EndCount=1),'handler removal changed batch cleanup'); + LFixture.Receiver.RemoveHandler := False; + LFixture.Log.OnNewLog := LFixture.Receiver.Receive; + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.Messages.Count=1,'unsubscribed records were delivered again'); + finally + LFixture.Free; + end; +end; + +procedure CheckLegacyAndFile(const ALogPath: String); +var LFixture: TLogFixture; LContents: TStringList; LPath: String; +begin + LFixture := TLogFixture.Create; + LContents := TStringList.Create; + try + LPath := ALogPath+'.file.log'; + Require(not FileExists(LPath),'file compatibility sample already exists'); + LFixture.Log.OnNewLog := nil; + LFixture.Log.FileName := LPath; + LFixture.Log.SaveTypes := [ltinfo]; + LFixture.Log.OnNewLog := LFixture.Receiver.Receive; + LFixture.Log.OnInThreadNewLog := LFixture.Receiver.ReceiveSynchronous; + {$IFNDEF LOG_GUI_HANDOFF_OLD_API} + LFixture.Log.OnNewLogBatchBegin := nil; + LFixture.Log.OnNewLogBatchEnd := nil; + {$ENDIF} + LFixture.Log.NotifyNewLog(ltinfo,'compatibility','saved-info'); + Require(LFixture.Receiver.SynchronousCount=1,'in-thread callback no longer completes before NotifyNewLog returns'); + LFixture.Log.NotifyNewLog(ltdebug,'compatibility','filtered-debug'); + Require(LFixture.Receiver.SynchronousCount=2,'filtered file record lost its synchronous callback'); + LFixture.Dispatch.SynchronizedProcess; + Require(LFixture.Receiver.Messages.CommaText='saved-info,filtered-debug','legacy single-record receiver changed'); + Require((LFixture.Receiver.BeginCount=0) and (LFixture.Receiver.EndCount=0),'optional hooks became mandatory'); + LFixture.Log.FileName := ''; + LContents.LoadFromFile(LPath); + Require(Pos('[Info] <compatibility> saved-info',LContents.Text)>0,'existing file format or accepted record changed'); + Require(Pos('filtered-debug',LContents.Text)=0,'file SaveTypes filtering changed'); + finally + LContents.Free; + LFixture.Free; + end; +end; + +function RunLogGuiHandoffChecks(const ALogPath: String; AProducerOnly: Boolean): Integer; +const Names: array[1..LogGuiHandoffCheckCount] of String = ('ProducerCompletesDuringGuiCallback','DetachedFifoAndReentrantNextBatch','RecordExceptionEndsBatchAndDisposes','HookExceptionsDisposeAndPreserveQueue','EmptyBatchAndUnsubscribe','LegacyHandlerAndFileCompatibility'); +var LLines: TStringList; I, LCount: Integer; +begin + Result := 0; + LCount := LogGuiHandoffCheckCount; + if AProducerOnly then + LCount := 1; + LLines := TStringList.Create; + try + for I := 1 to LCount do begin + try + case I of + 1: CheckProducerHandoff; + 2: CheckReentrantFIFO; + 3: CheckRecordException; + 4: CheckHookExceptions; + 5: CheckEmptyAndUnsubscribe; + 6: CheckLegacyAndFile(ALogPath); + end; + LLines.Add('PASS '+Names[I]); + except + on E: Exception do begin + Inc(Result); + LLines.Add('FAIL '+Names[I]+' '+E.ClassName+': '+E.Message); + end; + end; + end; + if Result=0 then + LLines.Add('PASS '+IntToStr(LCount)); + LLines.SaveToFile(ALogPath); + finally + LLines.Free; + end; +end; + +end. diff --git a/src/tests/experimental/core/ULogGuiHandoffTests.pas b/src/tests/experimental/core/ULogGuiHandoffTests.pas new file mode 100644 index 000000000..437c21d02 --- /dev/null +++ b/src/tests/experimental/core/ULogGuiHandoffTests.pas @@ -0,0 +1,31 @@ +unit ULogGuiHandoffTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Registers the logger handoff contracts shared with the native Delphi probe. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses SysUtils, fpcunit, testregistry, ULogGuiHandoffChecks; + +type + TLogGuiHandoffTests = class(TTestCase) + published + procedure ProducersDoNotWaitForGuiRendering; + end; + +implementation + +procedure TLogGuiHandoffTests.ProducersDoNotWaitForGuiRendering; +var LID: TGUID; LPath: String; +begin + AssertEquals('Create an owned logger test path',0,CreateGUID(LID)); + LPath := IncludeTrailingPathDelimiter(GetTempDir)+'LogGuiHandoff-'+GUIDToString(LID)+'.log'; + AssertEquals('Logger handoff contracts; details: '+LPath,0,RunLogGuiHandoffChecks(LPath)); +end; + +initialization + RegisterTest(TLogGuiHandoffTests); + +end. diff --git a/src/tests/experimental/core/UNodeSynchronizationTests.pas b/src/tests/experimental/core/UNodeSynchronizationTests.pas new file mode 100644 index 000000000..31ad5f262 --- /dev/null +++ b/src/tests/experimental/core/UNodeSynchronizationTests.pas @@ -0,0 +1,922 @@ +unit UNodeSynchronizationTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Deterministic checks of the actual node synchronization facts and notifier. + Generated empty-node data and owned peer identities only; no sockets or keys + from an existing wallet are used. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses Classes, SysUtils, fpcunit, testregistry, UNode, UAccounts, UBlockChain, + UCrypto, UNetProtocol, UFileStorage; + +type + TNodeSynchronizationTests = class(TTestCase) + private + FNode: TNode; + FNotify: TNodeNotifyEvents; + FWatch: TOrderedAccountKeysList; + FPeer, FSecondPeer: TObject; + FKey: TECPrivateKey; + FOperations: TOperationsHashTree; + FTrace: TStringList; + FLast: TNodeSynchronizationStatus; + FCallbacks: Integer; + FCallbackThread: UInt64; + FStopping: Boolean; + FExpectOperationCallback: Boolean; + FThrowNextBlock: Boolean; + FThrownBlockCallbacks: Integer; + FPreviousDirectory: String; + procedure OnSynchronization(Sender: TObject; const AStatus: TNodeSynchronizationStatus); + procedure OnBlocks(Sender: TObject); + procedure OnOperations(Sender: TObject); + procedure OnKeys(Sender: TObject); + procedure OnMessage(Sender: TNetConnection; AMessage: String); + procedure PumpFor(AMilliseconds: Cardinal); + procedure WaitForState(AState: TNodeSynchronizationState); + procedure StartSynchronizing; + procedure QueueBackendCallbacks; + procedure WaitForCombinedCallbacks; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure SelectedTargetSurvivesInterBatchSilence; + procedure NetworkOffWaitsForBufferedWork; + procedure DisconnectClearsTargetsBeforeReconnect; + procedure SynchronizingCallbackPrecedesBackendInputs; + procedure OfflineCallbackFollowsEveryBackendInput; + procedure ActualWriteSummaryCompletesSelectedTarget; + procedure FinalStatusSurvivesSubscriberException; + procedure BufferedWorkSurvivesReconnectUntilEveryScopeDrains; + procedure StorageFailureLatchesFirstCauseUntilRestart; + procedure StorageFailureNotificationNeverWaitsForUi; + procedure StorageFailureRetiresBufferedWorkSafely; + procedure StorageFailurePrecedesBackendCallbacks; + procedure NonStorageExceptionsDoNotLatchFailure; + procedure ActualCheckpointSaveFailureLatchesCanonicalNode; + procedure ActualFalseBlockSaveLatchesWithoutAcknowledging; + procedure SafeboxFlushFailureLatchesBeforeUiDelivery; + procedure FalseBlockCacheFlushLatchesBeforeUiDelivery; + end; + +implementation + +uses SyncObjs, UThread, UConst, UPCDataTypes, UOpTransaction; + +type + TSynchronizationSafeBoxAccess = class(TPCSafeBox) + public + procedure AddGeneratedBlock(const AKey: TAccountKey); + end; + + TSynchronizationScopeThread = class(TPCThread) + private + FNode: TNode; + FPeer: TObject; + protected + procedure BCExecute; override; + public + Entered, ReleaseScope, Completed, ExerciseRetiredScope, RetiredScopeChecked: TEvent; + ErrorMessage: String; + constructor Create(ANode: TNode; APeer: TObject); + destructor Destroy; override; + end; + + TOwnedOutOfMemory = class(EOutOfMemory) + public + procedure FreeInstance; override; + end; + + TFalseBlockStorage = class(TFileStorage) + protected + function DoSaveBlockChain(Operations: TPCOperationsComp): Boolean; override; + public + SaveCalls: Integer; + end; + +type + TStorageFlushNode = class(TNode) + protected + procedure DoFlushSafebox; override; + function DoFlushBlockStorage: Boolean; override; + public + SafeboxFault: Integer; + BlockFlushFails: Boolean; + end; + +procedure TStorageFlushNode.DoFlushSafebox; +begin + case SafeboxFault of + 1: raise EAbort.Create('Synthetic flush cancellation'); + 2: raise EWriteError.Create('Synthetic safebox flush failure'); + else + inherited; + end; +end; + +function TStorageFlushNode.DoFlushBlockStorage: Boolean; +begin + if BlockFlushFails then Result := False + else Result := inherited DoFlushBlockStorage; +end; + +procedure TOwnedOutOfMemory.FreeInstance; +begin + // RTL heap exceptions normally represent protected preallocated instances. + // This synthetic test object is normally allocated and must own its cleanup. + CleanupInstance; + FreeMem(Pointer(Self)); +end; + +function TFalseBlockStorage.DoSaveBlockChain(Operations: TPCOperationsComp): Boolean; +begin + Inc(SaveCalls); + Result := False; +end; + +function ReadStorageFixture(const AFileName: String): TBytes; +var LFile: TFileStream; +begin + Result := Nil; + LFile := TFileStream.Create(AFileName,fmOpenRead+fmShareDenyNone); + try + SetLength(Result,LFile.Size); + if Length(Result)>0 then LFile.ReadBuffer(Result[0],Length(Result)); + finally + LFile.Free; + end; +end; + +function StorageFixtureEquals(const ALeft, ARight: TBytes): Boolean; +begin + Result := Length(ALeft)=Length(ARight); + if Result and (Length(ALeft)>0) then Result := CompareMem(@ALeft[0],@ARight[0],Length(ALeft)); +end; + +type + TStorageFailureThread = class(TPCThread) + private + FNode: TNode; + protected + procedure BCExecute; override; + public + Completed: TEvent; + ErrorText: String; + constructor Create(ANode: TNode); + destructor Destroy; override; + end; + +constructor TStorageFailureThread.Create(ANode: TNode); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + Completed := TEvent.Create(nil,True,False,''); +end; + +destructor TStorageFailureThread.Destroy; +begin + Terminate; + if Suspended then Start; + WaitFor; + Completed.Free; + inherited; +end; + +procedure TStorageFailureThread.BCExecute; +var LError: EWriteError; +begin + try + try + LError := EWriteError.Create('owned injected disk full'); + try + FNode.ReportStorageFailure(sfsCheckpoint,'owned-checkpoint.am_safebox',LError); + finally + LError.Free; + end; + except + on E: Exception do ErrorText := E.ClassName+': '+E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TSynchronizationScopeThread.Create(ANode: TNode; APeer: TObject); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FPeer := APeer; + Entered := TEvent.Create(nil,True,False,''); + ReleaseScope := TEvent.Create(nil,True,False,''); + Completed := TEvent.Create(nil,True,False,''); + ExerciseRetiredScope := TEvent.Create(nil,True,False,''); + RetiredScopeChecked := TEvent.Create(nil,True,False,''); +end; + +destructor TSynchronizationScopeThread.Destroy; +begin + Terminate; + ReleaseScope.SetEvent; + if Suspended then Start; + WaitFor; + RetiredScopeChecked.Free; + ExerciseRetiredScope.Free; + Completed.Free; + ReleaseScope.Free; + Entered.Free; + inherited; +end; + +procedure TSynchronizationScopeThread.BCExecute; +var LProgress: TProgressInfo; +begin + try + try + if Terminated then Exit; + FNode.BeginBlockChainUpdate; + try + Entered.SetEvent; + while not Terminated do begin + if ExerciseRetiredScope.WaitFor(0) = wrSignaled then begin + ExerciseRetiredScope.ResetEvent; + FNode.BeginBlockChainUpdate; + try + FNode.SetSynchronizationTarget(FPeer,1000); + LProgress := TProgressInfo.Create(psDownloadingBlockchain); + LProgress.BlockNumber := 777; + FNode.SetSynchronizationProgress(LProgress); + finally FNode.EndBlockChainUpdate; end; + RetiredScopeChecked.SetEvent; + end; + if ReleaseScope.WaitFor(10) = wrSignaled then Break; + end; + finally FNode.EndBlockChainUpdate; end; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally Completed.SetEvent; end; +end; + +procedure TSynchronizationSafeBoxAccess.AddGeneratedBlock(const AKey: TAccountKey); +var LBlock: TOperationBlock; +begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := BlocksCount; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + AddNew(LBlock); +end; + +procedure TNodeSynchronizationTests.SetUp; +var LID: TGUID; LDirectory: String; LOperation: TPCOperation; I: Integer; +begin + inherited; + AssertFalse('Each state fixture owns a fresh node',TNode.NodeExists); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'NodeSynchronization-' + GUIDToString(LID); + AssertFalse(DirectoryExists(LDirectory)); + ForceDirectories(LDirectory); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(LDirectory); + TCrypto.InitCrypto; + FStopping := False; + FThrowNextBlock := False; + FThrownBlockCallbacks := 0; + FCallbacks := 0; + FTrace := TStringList.Create; + FPeer := TObject.Create; + FSecondPeer := TObject.Create; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FOperations := TOperationsHashTree.Create; + for I := 1 to 3 do begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_1,1000,I,1001, + FKey,100,1,CT_TOperationPayload_NUL); + try FOperations.AddOperationToHashTree(LOperation); + finally LOperation.Free; end; + end; + FNode := TStorageFlushNode.Create(nil); + FNode.BroadcastData := False; + AssertFalse('The state fixture never opens a listener',FNode.NetServer.Active); + FWatch := TOrderedAccountKeysList.Create(FNode.Bank.SafeBox,False); + FNotify := TNodeNotifyEvents.Create(nil); + FNotify.OnSynchronizationChanged := OnSynchronization; + FNotify.OnBlocksChanged := OnBlocks; + FNotify.OnOperationsChanged := OnOperations; + FNotify.OnKeyActivity := OnKeys; + FNotify.OnNodeMessageEvent := OnMessage; + FNotify.WatchKeys := FWatch; + FNotify.Node := FNode; + FNode.SetSynchronizationNetworkActive(False); + WaitForState(nssOffline); +end; + +procedure TNodeSynchronizationTests.TearDown; +begin + FStopping := True; + if Assigned(FNode) then FNode.SetSynchronizationNetworkActive(False); + if Assigned(FNotify) then FNotify.Node := nil; + FNotify.Free; + FWatch.Free; + FNode.Free; + FOperations.Free; + FKey.Free; + FSecondPeer.Free; + FPeer.Free; + FTrace.Free; + TNode.SetPascalCoinDataFolder(FPreviousDirectory); + inherited; +end; + +procedure TNodeSynchronizationTests.OnSynchronization(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); +begin + if FStopping then Exit; + FLast := AStatus; + Inc(FCallbacks); + FCallbackThread := TThread.CurrentThread.ThreadID; + FTrace.Add('sync'); +end; + +procedure TNodeSynchronizationTests.OnBlocks(Sender: TObject); +begin + if FStopping then Exit; + FTrace.Add('blocks'); + if FThrowNextBlock then begin + FThrowNextBlock := False; + Inc(FThrownBlockCallbacks); + raise Exception.Create('Owned one-shot block subscriber failure'); + end; +end; + +procedure TNodeSynchronizationTests.OnOperations(Sender: TObject); +begin + if not FStopping then FTrace.Add('operations'); +end; + +procedure TNodeSynchronizationTests.OnKeys(Sender: TObject); +begin + if not FStopping then FTrace.Add('keys'); +end; + +procedure TNodeSynchronizationTests.OnMessage(Sender: TNetConnection; AMessage: String); +begin + if not FStopping then FTrace.Add('message'); +end; + +procedure TNodeSynchronizationTests.PumpFor(AMilliseconds: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TNodeSynchronizationTests.WaitForState(AState: TNodeSynchronizationState); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + 3000; + repeat + CheckSynchronize(0); + if (FCallbacks > 0) and (FLast.State = AState) then begin + AssertEquals('Existing notifier keeps UI listeners on their owner thread', + UInt64(MainThreadID),FCallbackThread); + Exit; + end; + Sleep(1); + until GetTickCount64 >= LUntil; + Fail('The actual node notifier did not publish its current synchronization state'); +end; + +procedure TNodeSynchronizationTests.StartSynchronizing; +begin + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + FNode.SetSynchronizationTarget(FPeer,FNode.GetSynchronizationStatus.LocalBlockCount+10); + WaitForState(nssSynchronizing); +end; + +procedure TNodeSynchronizationTests.SelectedTargetSurvivesInterBatchSilence; +var LStatus: TNodeSynchronizationStatus; +begin + StartSynchronizing; + FNode.BeginBlockChainUpdate; + FNode.EndBlockChainUpdate; + PumpFor(6200); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue('No transfer activity is needed between batches',LStatus.ActiveTransfers = 0); + AssertTrue('The selected peer target survives a gap longer than five seconds',LStatus.HasRemoteTarget); + AssertTrue(LStatus.TargetBlockCount > LStatus.LocalBlockCount); + AssertTrue('Elapsed silence does not complete synchronization',LStatus.State = nssSynchronizing); + AssertTrue(FLast.State = nssSynchronizing); +end; + +procedure TNodeSynchronizationTests.NetworkOffWaitsForBufferedWork; +var LStatus: TNodeSynchronizationStatus; +begin + StartSynchronizing; + FNode.BeginBlockChainUpdate; + try + FNode.SetSynchronizationNetworkActive(False); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue('Already buffered work is still synchronization',LStatus.State = nssSynchronizing); + AssertFalse(LStatus.NetworkActive); + AssertFalse('Stopping transport discards the remote download target',LStatus.HasRemoteTarget); + AssertEquals(0,LStatus.PeerCount); + AssertEquals('The active physical scope is not retired by network off',1,LStatus.ActiveTransfers); + AssertTrue(FNode.IsUpdatingBlockChain); + WaitForState(nssSynchronizing); + finally FNode.EndBlockChainUpdate; end; + LStatus := FNode.GetSynchronizationStatus; + AssertEquals('Local availability resumes only after buffered work drains',0,LStatus.ActiveTransfers); + AssertTrue(LStatus.State = nssOffline); + WaitForState(nssOffline); +end; + +procedure TNodeSynchronizationTests.DisconnectClearsTargetsBeforeReconnect; +var LStatus: TNodeSynchronizationStatus; +begin + StartSynchronizing; + FNode.SetSynchronizationPeerConnected(FSecondPeer,True); + FNode.SetSynchronizationTarget(FSecondPeer,5); + FNode.BeginBlockChainUpdate; + try + FNode.SetSynchronizationPeerConnected(FPeer,False); + AssertTrue(FNode.GetSynchronizationStatus.State = nssSynchronizing); + FNode.SetSynchronizationPeerConnected(FSecondPeer,False); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue('No peers stops downloads but does not finish buffered work',LStatus.State = nssSynchronizing); + AssertFalse(LStatus.HasRemoteTarget); + AssertEquals(0,LStatus.PeerCount); + AssertEquals(1,LStatus.ActiveTransfers); + WaitForState(nssSynchronizing); + finally FNode.EndBlockChainUpdate; end; + WaitForState(nssDisconnected); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue('Reconnect is available when no buffered work remains',LStatus.State = nssIdle); + AssertFalse('Reconnecting has no remembered target',LStatus.HasRemoteTarget); + WaitForState(nssIdle); +end; + +procedure TNodeSynchronizationTests.QueueBackendCallbacks; +var LError: String; +begin + FWatch.Clear; + FNode.NotifyBlocksChanged; + // Exercise real admission as well: catch-up now reports unavailability before + // validating the absent senders, while the offline path rejects them normally. + FExpectOperationCallback := FNode.PendingOperationsAvailable; + if FExpectOperationCallback then + AssertEquals(0,FNode.AddOperations(nil,FOperations,nil,LError)) + else + AssertTrue(FNode.AddOperations(nil,FOperations,nil,LError)<0); + // Unavailable admission changes no operations and must not invent a pending + // data notification. The offline path still exercises that real callback. + FNode.NotifyNetClientMessage(nil,'owned notification-order check'); +end; + +procedure TNodeSynchronizationTests.WaitForCombinedCallbacks; +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + 3000; + repeat + CheckSynchronize(0); + if (FTrace.IndexOf('sync') >= 0) and (FTrace.IndexOf('blocks') >= 0) and + ((not FExpectOperationCallback) or (FTrace.IndexOf('operations') >= 0)) and (FTrace.IndexOf('message') >= 0) and + (FTrace.IndexOf('keys') >= 0) then Exit; + Sleep(1); + until GetTickCount64 >= LUntil; + Fail('The actual combined notifier batch was incomplete: ' + FTrace.CommaText); +end; + +procedure TNodeSynchronizationTests.SynchronizingCallbackPrecedesBackendInputs; +begin + StartSynchronizing; + PumpFor(200); + FTrace.Clear; + FNode.SetSynchronizationTarget(FPeer,20); + QueueBackendCallbacks; + WaitForCombinedCallbacks; + AssertEquals('Pause arrives before every backend request',0,FTrace.IndexOf('sync')); + AssertEquals('Unavailable admission must not publish an operation change',-1,FTrace.IndexOf('operations')); +end; + +procedure TNodeSynchronizationTests.OfflineCallbackFollowsEveryBackendInput; +begin + StartSynchronizing; + PumpFor(200); + FTrace.Clear; + FNode.SetSynchronizationNetworkActive(False); + QueueBackendCallbacks; + WaitForCombinedCallbacks; + AssertEquals('Resume follows blocks, operations, messages and key activity', + FTrace.Count-1,FTrace.IndexOf('sync')); + AssertTrue(FLast.State = nssOffline); +end; + +procedure TNodeSynchronizationTests.ActualWriteSummaryCompletesSelectedTarget; +var LMempool: TPCOperationsComp; LStatus: TNodeSynchronizationStatus; +begin + // Retain this empty fixture's pool before catch-up makes account scopes busy. + // Confirmed bank mutation uses the node's ordinary bank writer scope below. + LMempool := FNode.LockMempoolRead; + FNode.UnlockMempoolRead; + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + FNode.SetSynchronizationTarget(FPeer,1); + WaitForState(nssSynchronizing); + AssertTrue(FNode.TryLockNode(3000)); + try + TSynchronizationSafeBoxAccess(FNode.Bank.SafeBox).AddGeneratedBlock(FKey.PublicKey); + LMempool.Clear(True); + finally FNode.UnlockNode; end; + LStatus := FNode.GetSynchronizationStatus; + AssertTrue(LStatus.HasMempoolSummary); + AssertEquals(1,Int64(LStatus.LocalBlockCount)); + AssertTrue('Actual write release updates the cached local tip',LStatus.State = nssIdle); + WaitForState(nssIdle); +end; + +procedure TNodeSynchronizationTests.BufferedWorkSurvivesReconnectUntilEveryScopeDrains; +var LOld, LNew: TSynchronizationScopeThread; LStatus: TNodeSynchronizationStatus; +begin + LOld := nil; + LNew := nil; + try + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LOld := TSynchronizationScopeThread.Create(FNode,FPeer); + LOld.Start; + AssertEquals(Ord(wrSignaled),Ord(LOld.Entered.WaitFor(3000))); + AssertTrue(FNode.GetSynchronizationStatus.State = nssSynchronizing); + FNode.SetSynchronizationPeerConnected(FPeer,False); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue('Reconnect cannot finish already buffered processing',LStatus.State = nssSynchronizing); + AssertEquals(1,LStatus.ActiveTransfers); + AssertTrue(FNode.IsUpdatingBlockChain); + LOld.ExerciseRetiredScope.SetEvent; + AssertEquals(Ord(wrSignaled),Ord(LOld.RetiredScopeChecked.WaitFor(3000))); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue('Nested buffered work preserves synchronization',LStatus.State = nssSynchronizing); + AssertEquals('The completed nested scope retains its outer processing scope',1,LStatus.ActiveTransfers); + AssertFalse('Stale producers cannot restore a peer download target',LStatus.HasRemoteTarget); + LNew := TSynchronizationScopeThread.Create(FNode,FPeer); + LNew.Start; + AssertEquals(Ord(wrSignaled),Ord(LNew.Entered.WaitFor(3000))); + AssertEquals('Both physical processing scopes remain active',2,FNode.GetSynchronizationStatus.ActiveTransfers); + LOld.ReleaseScope.SetEvent; + AssertEquals(Ord(wrSignaled),Ord(LOld.Completed.WaitFor(3000))); + AssertEquals('',LOld.ErrorMessage); + LStatus := FNode.GetSynchronizationStatus; + AssertEquals('An old End cannot release the new processing scope',1,LStatus.ActiveTransfers); + AssertTrue(LStatus.State = nssSynchronizing); + LNew.ReleaseScope.SetEvent; + AssertEquals(Ord(wrSignaled),Ord(LNew.Completed.WaitFor(3000))); + AssertEquals('',LNew.ErrorMessage); + AssertEquals(0,FNode.GetSynchronizationStatus.ActiveTransfers); + AssertTrue(FNode.GetSynchronizationStatus.State = nssIdle); + AssertFalse(FNode.IsUpdatingBlockChain); + WaitForState(nssIdle); + finally + LNew.Free; + LOld.Free; + end; +end; + +procedure TNodeSynchronizationTests.StorageFailureLatchesFirstCauseUntilRestart; +var LError: Exception; LStatus: TNodeSynchronizationStatus; LProgress: TProgressInfo; + LReason: String; LMempool, LBlock: TPCOperationsComp; LRaised: Boolean; +begin + LError := EWriteError.Create('owned first storage failure'); + try + FNode.ReportStorageFailure(sfsCheckpoint,'first-checkpoint.am_safebox',LError); + finally + LError.Free; + end; + LError := EOSError.Create('owned later failure'); + try + FNode.ReportStorageFailure(sfsBlockStream,'later.blocks',LError); + finally + LError.Free; + end; + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + FNode.SetSynchronizationTarget(FPeer,1000); + LProgress := TProgressInfo.Create(psDownloadingBlockchain); + LProgress.BlockNumber := 999; + FNode.SetSynchronizationProgress(LProgress); + FNode.BeginBlockChainUpdate; + FNode.EndBlockChainUpdate; + LStatus := FNode.GetSynchronizationStatus; + AssertTrue(FNode.HasStorageFailure); + AssertTrue(LStatus.State=nssStorageError); + AssertTrue(LStatus.StorageFailure.Stage=sfsCheckpoint); + AssertEquals('first-checkpoint.am_safebox',LStatus.StorageFailure.FileName); + AssertEquals('EWriteError',LStatus.StorageFailure.ErrorClass); + AssertEquals('owned first storage failure',LStatus.StorageFailure.ErrorMessage); + AssertFalse(FNode.CanRequestBlockChainData); + AssertFalse(FNode.PendingOperationsAvailable); + AssertFalse(FNode.IsReady(LReason)); + AssertEquals('owned first storage failure',LReason); + AssertFalse(FNode.IsBlockChainValid(LReason)); + AssertEquals('owned first storage failure',LReason); + AssertTrue(FNode.AddOperations(nil,FOperations,nil,LReason)<0); + AssertEquals('owned first storage failure',LReason); + LBlock := TPCOperationsComp.Create(FNode.Bank); + try + AssertFalse(FNode.AddNewBlockChain(nil,LBlock,LReason)); + AssertEquals('owned first storage failure',LReason); + finally + LBlock.Free; + end; + AssertFalse(FNode.TryLockMempoolRead(0,LMempool)); + LRaised := False; + try + LMempool := FNode.LockMempoolWrite; + FNode.UnlockMempoolWrite; + except + on E: ENodeStorageUnavailable do LRaised := True; + end; + AssertTrue('Storage failure prevents new operation writers',LRaised); + AssertEquals(0,Int64(FNode.Bank.BlocksCount)); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationTests.StorageFailureNotificationNeverWaitsForUi; +var LThread: TStorageFailureThread; LBefore: Integer; +begin + LBefore := FCallbacks; + LThread := TStorageFailureThread.Create(FNode); + try + LThread.Start; + // No CheckSynchronize here: a producer must finish even while its actual + // main-thread subscriber cannot run. The event record owns copied strings. + AssertEquals(Ord(wrSignaled),Ord(LThread.Completed.WaitFor(3000))); + AssertEquals('',LThread.ErrorText); + AssertEquals(LBefore,FCallbacks); + AssertTrue(FNode.GetSynchronizationStatus.State=nssStorageError); + WaitForState(nssStorageError); + AssertEquals('owned injected disk full',FLast.StorageFailure.ErrorMessage); + finally + LThread.Free; + end; +end; + +procedure TNodeSynchronizationTests.StorageFailureRetiresBufferedWorkSafely; +var LThread: TSynchronizationScopeThread; LError: Exception; +begin + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LThread := TSynchronizationScopeThread.Create(FNode,FPeer); + try + LThread.Start; + AssertEquals(Ord(wrSignaled),Ord(LThread.Entered.WaitFor(3000))); + LError := EWriteError.Create('owned buffered storage failure'); + try + FNode.ReportStorageFailure(sfsBlockStream,'owned.blocks',LError); + finally + LError.Free; + end; + AssertEquals(1,FNode.GetSynchronizationStatus.ActiveTransfers); + AssertTrue('Failure has precedence while physical work retires',FNode.GetSynchronizationStatus.State=nssStorageError); + AssertFalse(FNode.CanRequestBlockChainData); + LThread.ReleaseScope.SetEvent; + AssertEquals(Ord(wrSignaled),Ord(LThread.Completed.WaitFor(3000))); + AssertEquals('',LThread.ErrorMessage); + AssertEquals(0,FNode.GetSynchronizationStatus.ActiveTransfers); + AssertTrue('Draining work never clears the storage failure',FNode.GetSynchronizationStatus.State=nssStorageError); + WaitForState(nssStorageError); + finally + LThread.Free; + end; +end; + +procedure TNodeSynchronizationTests.StorageFailurePrecedesBackendCallbacks; +var LError: Exception; +begin + FTrace.Clear; + LError := EWriteError.Create('owned callback-order storage failure'); + try + FNode.ReportStorageFailure(sfsCheckpoint,'owned-checkpoint',LError); + finally + LError.Free; + end; + QueueBackendCallbacks; + WaitForCombinedCallbacks; + AssertEquals('Storage error is delivered before backend refresh requests',0,FTrace.IndexOf('sync')); + AssertTrue(FLast.State=nssStorageError); + AssertEquals(-1,FTrace.IndexOf('operations')); +end; + +procedure TNodeSynchronizationTests.NonStorageExceptionsDoNotLatchFailure; +var LError: Exception; I: Integer; +begin + for I := 0 to 2 do begin + case I of + 0: LError := TOwnedOutOfMemory.Create('owned exhaustion'); + 1: LError := EAbort.Create('owned cancellation'); + else + LError := EAccessViolation.Create('owned programmer fault'); + end; + try + FNode.ReportStorageFailure(sfsUnknown,'not-a-storage-failure',LError); + finally + LError.Free; + end; + AssertFalse(FNode.HasStorageFailure); + AssertTrue(FNode.GetSynchronizationStatus.State=nssOffline); + end; +end; + +procedure TNodeSynchronizationTests.ActualCheckpointSaveFailureLatchesCanonicalNode; +var LDirectory, LDestination, LPrevious, LReason: String; + LBefore: TBytes; LFailed: Boolean; LStatus: TNodeSynchronizationStatus; + LIncoming: TPCOperationsComp; +begin + FNode.Bank.StorageClass := TFileStorage; + TSynchronizationSafeBoxAccess(FNode.Bank.SafeBox).AddGeneratedBlock(FKey.PublicKey); + LDirectory := FNode.Bank.GetStorageFolder(''); + LPrevious := LDirectory+PathDelim+'previous.am_safebox'; + FNode.Bank.SafeBox.SaveCheckpointing(LPrevious); + LBefore := ReadStorageFixture(LPrevious); + LDestination := FNode.Bank.GetSafeboxCheckpointingFileName(LDirectory,FNode.Bank.BlocksCount); + AssertTrue(ForceDirectories(LDestination)); + LFailed := False; + try + FNode.Bank.SaveBank(True); + except + on E: EOSError do LFailed := True; + end; + AssertTrue('Real atomic publication to a directory must fail',LFailed); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue(LStatus.State=nssStorageError); + AssertTrue(LStatus.StorageFailure.Stage=sfsCheckpoint); + AssertEquals(LDestination,LStatus.StorageFailure.FileName); + AssertTrue('The complete earlier checkpoint is preserved',StorageFixtureEquals(LBefore,ReadStorageFixture(LPrevious))); + AssertTrue(DirectoryExists(LDestination)); + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + AssertFalse(FNode.AddNewBlockChain(nil,LIncoming,LReason)); + AssertFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + finally + LIncoming.Free; + end; + AssertEquals(1,Int64(FNode.Bank.BlocksCount)); + AssertFalse(FNode.CanRequestBlockChainData); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationTests.ActualFalseBlockSaveLatchesWithoutAcknowledging; +var LPrevious, LReason: String; LBefore: TBytes; LFailed: Boolean; + LIncoming: TPCOperationsComp; LStorage: TFalseBlockStorage; + LCount, I: Integer; LStatus: TNodeSynchronizationStatus; +begin + LPrevious := FNode.Bank.GetStorageFolder('')+PathDelim+'previous-empty.am_safebox'; + FNode.Bank.SafeBox.SaveCheckpointing(LPrevious); + LBefore := ReadStorageFixture(LPrevious); + FNode.Bank.StorageClass := TFalseBlockStorage; + LStorage := TFalseBlockStorage(FNode.Bank.Storage); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + for I := 0 to FOperations.OperationsCount-1 do + LIncoming.OperationsHashTree.AddOperationToHashTree(FOperations.GetOperation(I)); + LCount := LIncoming.Count; + AssertTrue(LCount>0); + LFailed := False; + try + FNode.Bank.Storage.SaveBlockChainBlock(LIncoming); + except + on E: EWriteError do LFailed := True; + end; + AssertTrue('False storage acknowledgement must become a real write error',LFailed); + AssertEquals(1,LStorage.SaveCalls); + AssertEquals(LCount,LIncoming.Count); + LStatus := FNode.GetSynchronizationStatus; + AssertTrue(LStatus.State=nssStorageError); + AssertTrue(LStatus.StorageFailure.Stage=sfsBlockStream); + AssertTrue(Pos('BlockChainStream.blocks',LStatus.StorageFailure.FileName)>0); + AssertTrue('Earlier checkpoint bytes remain intact',StorageFixtureEquals(LBefore,ReadStorageFixture(LPrevious))); + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + AssertFalse(FNode.AddNewBlockChain(nil,LIncoming,LReason)); + AssertFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + AssertEquals(1,LStorage.SaveCalls); + AssertEquals(LCount,LIncoming.Count); + AssertEquals(0,Int64(FNode.Bank.BlocksCount)); + WaitForState(nssStorageError); + finally + LIncoming.Free; + end; +end; + +procedure TNodeSynchronizationTests.SafeboxFlushFailureLatchesBeforeUiDelivery; +var LFileName, LReason: String; LStatus: TNodeSynchronizationStatus; + LIncoming: TPCOperationsComp; LBefore: TBytes; +begin + LFileName := FNode.Bank.GetStorageFolder('')+PathDelim+'flush-source.am_safebox'; + FNode.Bank.SafeBox.SetSafeboxFileName(LFileName); + LBefore := ReadStorageFixture(LFileName); + TStorageFlushNode(FNode).SafeboxFault := 1; + try + FNode.FlushSafebox; + Fail('Synthetic cancellation must propagate'); + except + on E: EAbort do AssertEquals('Synthetic flush cancellation',E.Message); + end; + AssertFalse('Cancellation cannot latch a storage error',FNode.HasStorageFailure); + TStorageFlushNode(FNode).SafeboxFault := 2; + try + FNode.FlushSafebox; + Fail('Synthetic safebox write error must propagate'); + except + on E: EWriteError do AssertEquals('Synthetic safebox flush failure',E.Message); + end; + // No UI pump occurred between the actual flush boundary and these guards. + LStatus := FNode.GetSynchronizationStatus; + AssertTrue(LStatus.State=nssStorageError); + AssertTrue(LStatus.StorageFailure.Stage=sfsSafeboxFlush); + AssertEquals(LFileName,LStatus.StorageFailure.FileName); + AssertFalse(FNode.CanRequestBlockChainData); + AssertTrue('The pre-flush source bytes remain intact',StorageFixtureEquals(LBefore,ReadStorageFixture(LFileName))); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + AssertFalse(FNode.AddNewBlockChain(Nil,LIncoming,LReason)); + AssertFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + finally + LIncoming.Free; + end; + TStorageFlushNode(FNode).SafeboxFault := 0; + FNode.FlushSafebox; + FNode.SetSynchronizationNetworkActive(True); + AssertEquals('Synthetic safebox flush failure',FNode.GetSynchronizationStatus.StorageFailure.ErrorMessage); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationTests.FalseBlockCacheFlushLatchesBeforeUiDelivery; +var LStatus: TNodeSynchronizationStatus; LReason: String; + LIncoming: TPCOperationsComp; +begin + FNode.Bank.StorageClass := TFileStorage; + TStorageFlushNode(FNode).BlockFlushFails := True; + try + FNode.FlushBlockStorage; + Fail('False block-cache flush must become a write error'); + except + on E: EWriteError do AssertTrue(Pos('Cannot flush blockchain storage',E.Message)>0); + end; + LStatus := FNode.GetSynchronizationStatus; + AssertTrue(LStatus.State=nssStorageError); + AssertTrue(LStatus.StorageFailure.Stage=sfsBlockStream); + AssertEquals('EWriteError',LStatus.StorageFailure.ErrorClass); + AssertFalse(FNode.CanRequestBlockChainData); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + AssertFalse(FNode.AddNewBlockChain(Nil,LIncoming,LReason)); + AssertFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + finally + LIncoming.Free; + end; + TStorageFlushNode(FNode).BlockFlushFails := False; + FNode.FlushBlockStorage; + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + AssertEquals(LStatus.StorageFailure.ErrorMessage,FNode.GetSynchronizationStatus.StorageFailure.ErrorMessage); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationTests.FinalStatusSurvivesSubscriberException; +begin + StartSynchronizing; + PumpFor(200); + FTrace.Clear; + FThrowNextBlock := True; + // Both flags are set without pumping the UI. The actual notifier must run + // the final status callback even when the preceding block subscriber fails. + FNode.SetSynchronizationNetworkActive(False); + FNode.NotifyBlocksChanged; + WaitForState(nssOffline); + AssertEquals('The expected subscriber failure was exercised exactly once',1,FThrownBlockCallbacks); + AssertTrue('No additional producer event was needed to deliver final status', + (FTrace.IndexOf('blocks') >= 0) and (FTrace.IndexOf('sync') > FTrace.IndexOf('blocks'))); + AssertEquals(0,FNode.GetSynchronizationStatus.ActiveTransfers); +end; + +initialization + RegisterTest(TNodeSynchronizationTests); + +end. diff --git a/src/tests/experimental/core/UOrderedCardinalTests.pas b/src/tests/experimental/core/UOrderedCardinalTests.pas new file mode 100644 index 000000000..3cd075c86 --- /dev/null +++ b/src/tests/experimental/core/UOrderedCardinalTests.pas @@ -0,0 +1,38 @@ +unit UOrderedCardinalTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ENDIF} + +interface + +uses fpcunit, testregistry, UOrderedCardinalChecks; + +type + TOrderedCardinalTests = class(TTestCase) + published + procedure FullCardinalBoundaryPairs; + procedure ActualListInsertionFindRemovalPermutations; + procedure ActualListDuplicateAndMissingValues; + end; + +implementation + +procedure TOrderedCardinalTests.FullCardinalBoundaryPairs; +begin + CheckOrderedCardinal(occBoundaryPairs); +end; + +procedure TOrderedCardinalTests.ActualListInsertionFindRemovalPermutations; +begin + CheckOrderedCardinal(occInsertionPermutations); +end; + +procedure TOrderedCardinalTests.ActualListDuplicateAndMissingValues; +begin + CheckOrderedCardinal(occDuplicateAndMissingValues); +end; + +initialization + RegisterTest(TOrderedCardinalTests); +end. diff --git a/src/tests/experimental/core/UPendingCatchupTests.pas b/src/tests/experimental/core/UPendingCatchupTests.pas new file mode 100644 index 000000000..9f1d2118f --- /dev/null +++ b/src/tests/experimental/core/UPendingCatchupTests.pas @@ -0,0 +1,92 @@ +unit UPendingCatchupTests; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + FPCUnit and the native Delphi probe execute the same pending catch-up cases. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses Classes, SysUtils, fpcunit, testregistry, UPendingCatchupChecks; + +type + TPendingCatchupTests = class(TTestCase) + private + procedure RunCheck(ACheck: TPendingCatchupCheck); + published + procedure BufferedCommitsDeferOneFinalPendingReplay; + procedure RemoteTargetHoldsPendingAcrossBatchGaps; + procedure FinalReplayRejectsConfirmedOperationsExactly; + procedure PendingReadsAndNewAdmissionReportUnavailable; + procedure NewPhysicalWorkPreventsPrematureCompletion; + procedure RetiredAndReconnectedOwnersDrainBeforeReplay; + procedure QuitPersistsOneCompletePendingPackage; + procedure UnchangedEmptyScopeNeedsNoReplay; + procedure FailedReplayKeepsCompleteInputAndRetriesWithoutNewEvents; + procedure QuitAfterFailedReplayPersistsTheCompleteOriginalPackage; + end; + +implementation + +procedure TPendingCatchupTests.RunCheck(ACheck: TPendingCatchupCheck); +var LID: TGUID; LPath: String; +begin + CreateGUID(LID); + LPath:=IncludeTrailingPathDelimiter(GetTempDir)+'PendingCatchup-'+GUIDToString(LID)+'.log'; + AssertEquals('The shared actual-node catch-up scenario must pass',0, + RunPendingCatchupCheck(ACheck,LPath)); +end; + +procedure TPendingCatchupTests.BufferedCommitsDeferOneFinalPendingReplay; +begin + RunCheck(pccBufferedCommits); +end; + +procedure TPendingCatchupTests.RemoteTargetHoldsPendingAcrossBatchGaps; +begin + RunCheck(pccInterBatchTarget); +end; + +procedure TPendingCatchupTests.FinalReplayRejectsConfirmedOperationsExactly; +begin + RunCheck(pccMixedRejections); +end; + +procedure TPendingCatchupTests.PendingReadsAndNewAdmissionReportUnavailable; +begin + RunCheck(pccUnavailableContracts); +end; + +procedure TPendingCatchupTests.NewPhysicalWorkPreventsPrematureCompletion; +begin + RunCheck(pccNewWorkDuringReplay); +end; + +procedure TPendingCatchupTests.RetiredAndReconnectedOwnersDrainBeforeReplay; +begin + RunCheck(pccRetiredScopes); +end; + +procedure TPendingCatchupTests.QuitPersistsOneCompletePendingPackage; +begin + RunCheck(pccShutdownPackage); +end; + +procedure TPendingCatchupTests.UnchangedEmptyScopeNeedsNoReplay; +begin + RunCheck(pccEmptyScope); +end; + +procedure TPendingCatchupTests.FailedReplayKeepsCompleteInputAndRetriesWithoutNewEvents; +begin + RunCheck(pccReplayFailureRetry); +end; + +procedure TPendingCatchupTests.QuitAfterFailedReplayPersistsTheCompleteOriginalPackage; +begin + RunCheck(pccReplayFailureShutdown); +end; + +initialization + RegisterTest(TPendingCatchupTests); +end. diff --git a/src/tests/experimental/core/UThreadConcurrencyTests.pas b/src/tests/experimental/core/UThreadConcurrencyTests.pas new file mode 100644 index 000000000..cda210f16 --- /dev/null +++ b/src/tests/experimental/core/UThreadConcurrencyTests.pas @@ -0,0 +1,724 @@ +unit UThreadConcurrencyTests; + +{ Copyright © 2026 Herman Schoenfeld <https://www.sphere10.com> + Distributed under the MIT software license, see the accompanying LICENSE. } + +{$MODE DELPHI}{$H+} + +interface + +uses Classes, SysUtils, SyncObjs, Generics.Collections, fpcunit, testregistry, + UThread; + +type + TLockAction = (laRead, laWrite, laUpgrade, laReleaseRead, laReleaseWrite, + laReleaseUpgrade, laCancellableRead, laUpgradeThenWrite); + + TLockWorker = class(TThread) + private + FLock: TReadWriteLock; + FAction: TLockAction; + FTimeout: Integer; + protected + procedure Execute; override; + public + Entered, ReleaseGate, Done: TEvent; + Acquired: Boolean; + ErrorClass, ErrorMessage: String; + Elapsed: UInt64; + OwnReadCount, OwnWriteCount, OwnUpgradeCount: Integer; + constructor Create(ALock: TReadWriteLock; AAction: TLockAction; ATimeout: Integer); + destructor Destroy; override; + procedure RequestStop; + end; + + TCoreReadWriteLockTests = class(TTestCase) + private + FLock: TReadWriteLock; + FWorkers: TList<TLockWorker>; + function Worker(AAction: TLockAction; ATimeout: Integer = 2500): TLockWorker; + procedure Await(AEvent: TEvent; const ADescription: String); + procedure WaitCount(AKind, ACount: Integer); + procedure Completed(AWorker: TLockWorker); + procedure Release(AWorker: TLockWorker); + procedure ExpectRecursion(AAction: TLockAction); + procedure RecursivePolicy; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure DefaultPolicyAndEmptyCounts; + procedure DifferentThreadsReadConcurrently; + procedure WriterExcludesOtherReadersWritersAndUpgraders; + procedure WaitingWriterPrecedesNewReader; + procedure WaitingWriterPrecedesNewUpgrader; + procedure RecursiveReadsCountOneReader; + procedure RecursiveWriterCanReadWriteAndUpgrade; + procedure NoRecursionRejectsRepeatedAcquisition; + procedure OrdinaryReadCannotPromoteUnderEitherPolicy; + procedure NoRecursionUpgradeableReadThenWriteIsRejected; + procedure UpgradeableReadPromotesWithoutOtherReaders; + procedure RecursiveUpgradeableWithExplicitReadCanPromote; + procedure UpgradeWaitsForOtherReaderAndRetainsOwnershipOnTimeout; + procedure WaitingUpgradePromotesWhenOtherReaderReleases; + procedure OnlyOneUpgradeableOwnerAndReadersCanShare; + procedure WriteToReadDowngradeRetainsReader; + procedure UpgradeableToReadDowngradeRetainsReader; + procedure FiniteTimeoutIsBoundedAndRemovesWaiter; + procedure ZeroTimeoutDoesNotRegisterWaiter; + procedure InfiniteTimeoutWakesOnRelease; + procedure TimedOutWriterReleasesWaitingReaders; + procedure WrongThreadCannotReleaseAnyMode; + procedure UnmatchedReleaseDoesNotChangeOwnership; + procedure InvalidTimeoutDoesNotAcquire; + procedure DisposeRejectsActiveOwnershipAndCanBeRetried; + procedure DisposeRejectsWaiterAndWaiterLaterCompletes; + procedure DisposedLockRejectsAcquisitions; + procedure ExceptionCleanupReleasesOnlyOwnedAcquisitions; + procedure PromotedWriteExceptionRetainsSurroundingUpgradeableLock; + procedure HeldAndRecursivePropertiesAreThreadSpecific; + procedure FiniteWaitPermitsCooperativeCancellation; + end; + +implementation + +function Tick: UInt64; +begin + Result := GetTickCount64; +end; + +constructor TLockWorker.Create(ALock: TReadWriteLock; AAction: TLockAction; ATimeout: Integer); +begin + inherited Create(True); + FLock := ALock; + FAction := AAction; + FTimeout := ATimeout; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseGate := TEvent.Create(nil, True, False, ''); + Done := TEvent.Create(nil, True, False, ''); + Start; +end; + +destructor TLockWorker.Destroy; +begin + RequestStop; + WaitFor; + Done.Free; + ReleaseGate.Free; + Entered.Free; + inherited; +end; + +procedure TLockWorker.RequestStop; +begin + Terminate; + ReleaseGate.SetEvent; +end; + +procedure TLockWorker.Execute; +var LStarted: UInt64; +begin + LStarted := Tick; + try + try + case FAction of + laRead: Acquired := FLock.TryAcquireReadLock(FTimeout); + laWrite: Acquired := FLock.TryAcquireWriteLock(FTimeout); + laUpgrade: Acquired := FLock.TryAcquireUpgradeableReadLock(FTimeout); + laReleaseRead: FLock.ReleaseReadLock; + laReleaseWrite: FLock.ReleaseWriteLock; + laReleaseUpgrade: FLock.ReleaseUpgradeableReadLock; + laCancellableRead: + while not Terminated do + if FLock.TryAcquireReadLock(FTimeout) then begin + FLock.ReleaseReadLock; + Break; + end; + laUpgradeThenWrite: begin + FLock.AcquireUpgradeableReadLock; + try + Entered.SetEvent; + if ReleaseGate.WaitFor(4000) <> wrSignaled then + raise Exception.Create('Promotion start gate timed out.'); + Acquired := FLock.TryAcquireWriteLock(FTimeout); + if Acquired then FLock.ReleaseWriteLock; + finally FLock.ReleaseUpgradeableReadLock; end; + end; + end; + if Acquired and (FAction in [laRead, laWrite, laUpgrade]) then begin + try + OwnReadCount := FLock.RecursiveReadCount; + OwnWriteCount := FLock.RecursiveWriteCount; + OwnUpgradeCount := FLock.RecursiveUpgradeCount; + Entered.SetEvent; + if ReleaseGate.WaitFor(4000) <> wrSignaled then + raise Exception.Create('Test release gate timed out.'); + finally + case FAction of + laRead: FLock.ReleaseReadLock; + laWrite: FLock.ReleaseWriteLock; + laUpgrade: FLock.ReleaseUpgradeableReadLock; + end; + end; + end; + except + on E: Exception do begin + ErrorClass := E.ClassName; + ErrorMessage := E.Message; + end; + end; + finally + Elapsed := Tick - LStarted; + Done.SetEvent; + end; +end; + +procedure TCoreReadWriteLockTests.SetUp; +begin + inherited; + FLock := TReadWriteLock.Create; + FWorkers := TList<TLockWorker>.Create; +end; + +procedure TCoreReadWriteLockTests.TearDown; +var LWorker: TLockWorker; +begin + for LWorker in FWorkers do LWorker.RequestStop; + // Release test-thread ownership before joining blocked workers after a failure. + while FLock.IsWriteLockHeld do FLock.ReleaseWriteLock; + while FLock.IsUpgradeableReadLockHeld do FLock.ReleaseUpgradeableReadLock; + while FLock.IsReadLockHeld do FLock.ReleaseReadLock; + for LWorker in FWorkers do LWorker.Free; + FWorkers.Free; + FLock.Free; + inherited; +end; + +procedure TCoreReadWriteLockTests.RecursivePolicy; +begin + FLock.Free; + FLock := TReadWriteLock.Create(lrpSupportsRecursion); +end; + +function TCoreReadWriteLockTests.Worker(AAction: TLockAction; ATimeout: Integer): TLockWorker; +begin + Result := TLockWorker.Create(FLock, AAction, ATimeout); + FWorkers.Add(Result); +end; + +procedure TCoreReadWriteLockTests.Await(AEvent: TEvent; const ADescription: String); +begin + AssertEquals(ADescription, Ord(wrSignaled), Ord(AEvent.WaitFor(3000))); +end; + +procedure TCoreReadWriteLockTests.WaitCount(AKind, ACount: Integer); +var LStarted: UInt64; LCount: Integer; +begin + LStarted := Tick; + repeat + case AKind of + 0: LCount := FLock.WaitingReadCount; + 1: LCount := FLock.WaitingWriteCount; + else LCount := FLock.WaitingUpgradeCount; + end; + if LCount = ACount then Exit; + Sleep(1); + until Tick - LStarted >= 2000; + AssertEquals('Actual registered waiter count', ACount, LCount); +end; + +procedure TCoreReadWriteLockTests.Completed(AWorker: TLockWorker); +begin + Await(AWorker.Done, 'Worker completed'); + AssertEquals(AWorker.ErrorMessage, '', AWorker.ErrorClass); +end; + +procedure TCoreReadWriteLockTests.Release(AWorker: TLockWorker); +begin + AWorker.ReleaseGate.SetEvent; + Completed(AWorker); +end; + +procedure TCoreReadWriteLockTests.ExpectRecursion(AAction: TLockAction); +begin + try + case AAction of + laRead: FLock.TryAcquireReadLock(0); + laWrite: FLock.TryAcquireWriteLock(0); + laUpgrade: FLock.TryAcquireUpgradeableReadLock(0); + end; + Fail('This recursion/promotion should be rejected.'); + except on E: ELockRecursionException do ; end; +end; + +procedure TCoreReadWriteLockTests.DefaultPolicyAndEmptyCounts; +begin + AssertEquals(Ord(lrpNoRecursion), Ord(FLock.RecursionPolicy)); + AssertFalse(FLock.IsReadLockHeld or FLock.IsWriteLockHeld or FLock.IsUpgradeableReadLockHeld); + AssertEquals(0, FLock.CurrentReadCount); + AssertEquals(0, FLock.WaitingReadCount + FLock.WaitingWriteCount + FLock.WaitingUpgradeCount); +end; + +procedure TCoreReadWriteLockTests.DifferentThreadsReadConcurrently; +var LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LReader := Worker(laRead); + Await(LReader.Entered, 'Second reader acquires while first still owns'); + AssertEquals(2, FLock.CurrentReadCount); + AssertEquals(1, FLock.RecursiveReadCount); + FLock.ReleaseReadLock; + Release(LReader); + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TCoreReadWriteLockTests.WriterExcludesOtherReadersWritersAndUpgraders; +var LAction: TLockAction; LWorker: TLockWorker; +begin + FLock.AcquireWriteLock; + for LAction := laRead to laUpgrade do begin + LWorker := Worker(LAction, 0); + Completed(LWorker); + AssertFalse('Writer excludes other owners', LWorker.Acquired); + end; + AssertTrue(FLock.IsWriteLockHeld); + AssertEquals(0, FLock.WaitingReadCount + FLock.WaitingWriteCount + FLock.WaitingUpgradeCount); + FLock.ReleaseWriteLock; +end; + +procedure TCoreReadWriteLockTests.WaitingWriterPrecedesNewReader; +var LWriter, LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite); + WaitCount(1, 1); + LReader := Worker(laRead); + WaitCount(0, 1); + FLock.ReleaseReadLock; + Await(LWriter.Entered, 'Waiting writer wins admission'); + AssertEquals(Ord(wrTimeout), Ord(LReader.Entered.WaitFor(0))); + Release(LWriter); + Await(LReader.Entered, 'Reader follows completed writer'); + Release(LReader); +end; + +procedure TCoreReadWriteLockTests.WaitingWriterPrecedesNewUpgrader; +var LWriter, LUpgrade: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite); + WaitCount(1, 1); + LUpgrade := Worker(laUpgrade); + WaitCount(2, 1); + FLock.ReleaseReadLock; + Await(LWriter.Entered, 'Writer precedes new upgrader'); + AssertEquals(Ord(wrTimeout), Ord(LUpgrade.Entered.WaitFor(0))); + Release(LWriter); + Await(LUpgrade.Entered, 'Upgrader follows writer'); + Release(LUpgrade); +end; + +procedure TCoreReadWriteLockTests.RecursiveReadsCountOneReader; +begin + RecursivePolicy; + FLock.AcquireReadLock; + FLock.AcquireReadLock; + AssertEquals(2, FLock.RecursiveReadCount); + AssertEquals(1, FLock.CurrentReadCount); + FLock.ReleaseReadLock; + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TCoreReadWriteLockTests.RecursiveWriterCanReadWriteAndUpgrade; +begin + RecursivePolicy; + FLock.AcquireWriteLock; + FLock.AcquireReadLock; + FLock.AcquireWriteLock; + FLock.AcquireUpgradeableReadLock; + FLock.AcquireUpgradeableReadLock; + AssertEquals(2, FLock.RecursiveWriteCount); + AssertEquals(1, FLock.RecursiveReadCount); + AssertEquals(2, FLock.RecursiveUpgradeCount); + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsWriteLockHeld); + FLock.ReleaseReadLock; + FLock.ReleaseUpgradeableReadLock; + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TCoreReadWriteLockTests.NoRecursionRejectsRepeatedAcquisition; +begin + FLock.AcquireReadLock; + ExpectRecursion(laRead); + AssertEquals(1, FLock.RecursiveReadCount); + FLock.ReleaseReadLock; + FLock.AcquireWriteLock; + ExpectRecursion(laWrite); + ExpectRecursion(laRead); + ExpectRecursion(laUpgrade); + AssertEquals(1, FLock.RecursiveWriteCount); + FLock.ReleaseWriteLock; + FLock.AcquireUpgradeableReadLock; + ExpectRecursion(laUpgrade); + AssertEquals(1, FLock.RecursiveUpgradeCount); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TCoreReadWriteLockTests.OrdinaryReadCannotPromoteUnderEitherPolicy; +var LPolicy: TLockRecursionPolicy; +begin + for LPolicy := lrpNoRecursion to lrpSupportsRecursion do begin + FLock.Free; + FLock := TReadWriteLock.Create(LPolicy); + FLock.AcquireReadLock; + ExpectRecursion(laWrite); + ExpectRecursion(laUpgrade); + AssertEquals(1, FLock.RecursiveReadCount); + FLock.ReleaseReadLock; + end; +end; + +procedure TCoreReadWriteLockTests.NoRecursionUpgradeableReadThenWriteIsRejected; +begin + FLock.AcquireUpgradeableReadLock; + FLock.AcquireReadLock; + ExpectRecursion(laWrite); + FLock.ReleaseReadLock; + AssertTrue(FLock.TryAcquireWriteLock(0)); + FLock.ReleaseWriteLock; + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TCoreReadWriteLockTests.UpgradeableReadPromotesWithoutOtherReaders; +begin + FLock.AcquireUpgradeableReadLock; + AssertEquals(0, FLock.CurrentReadCount); + AssertTrue(FLock.TryAcquireWriteLock(0)); + AssertTrue(FLock.IsWriteLockHeld and FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TCoreReadWriteLockTests.RecursiveUpgradeableWithExplicitReadCanPromote; +begin + RecursivePolicy; + FLock.AcquireUpgradeableReadLock; + FLock.AcquireReadLock; + AssertTrue(FLock.TryAcquireWriteLock(0)); + FLock.ReleaseUpgradeableReadLock; + AssertTrue(FLock.IsWriteLockHeld and FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + FLock.ReleaseWriteLock; +end; + +procedure TCoreReadWriteLockTests.UpgradeWaitsForOtherReaderAndRetainsOwnershipOnTimeout; +var LReader: TLockWorker; +begin + FLock.AcquireUpgradeableReadLock; + LReader := Worker(laRead); + Await(LReader.Entered, 'Reader shares upgradeable ownership'); + AssertFalse(FLock.TryAcquireWriteLock(70)); + AssertTrue(FLock.IsUpgradeableReadLockHeld); + AssertFalse(FLock.IsWriteLockHeld); + AssertEquals(0, FLock.WaitingWriteCount); + Release(LReader); + AssertTrue(FLock.TryAcquireWriteLock(2000)); + FLock.ReleaseWriteLock; + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TCoreReadWriteLockTests.OnlyOneUpgradeableOwnerAndReadersCanShare; +var LUpgrade, LReader: TLockWorker; +begin + FLock.AcquireUpgradeableReadLock; + LUpgrade := Worker(laUpgrade, 60); + LReader := Worker(laRead); + Await(LReader.Entered, 'Reader shares first upgrader'); + Completed(LUpgrade); + AssertFalse(LUpgrade.Acquired); + Release(LReader); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TCoreReadWriteLockTests.WaitingUpgradePromotesWhenOtherReaderReleases; +var LUpgrade, LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LUpgrade := Worker(laUpgradeThenWrite); + Await(LUpgrade.Entered, 'Upgradeable read shares existing reader'); + LUpgrade.ReleaseGate.SetEvent; + WaitCount(1, 1); + LReader := Worker(laRead); + WaitCount(0, 1); + AssertEquals(Ord(wrTimeout), Ord(LUpgrade.Done.WaitFor(0))); + FLock.ReleaseReadLock; + Completed(LUpgrade); + AssertTrue('Pending promotion succeeds after last other reader releases its lock', LUpgrade.Acquired); + Await(LReader.Entered, 'New reader follows completed promotion'); + Release(LReader); +end; + +procedure TCoreReadWriteLockTests.WriteToReadDowngradeRetainsReader; +var LWriter, LReader: TLockWorker; +begin + RecursivePolicy; + FLock.AcquireWriteLock; + FLock.AcquireReadLock; + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsReadLockHeld); + LWriter := Worker(laWrite, 0); + Completed(LWriter); + AssertFalse(LWriter.Acquired); + LReader := Worker(laRead); + Await(LReader.Entered, 'Downgrade permits another reader'); + Release(LReader); + FLock.ReleaseReadLock; +end; + +procedure TCoreReadWriteLockTests.UpgradeableToReadDowngradeRetainsReader; +begin + FLock.AcquireUpgradeableReadLock; + FLock.AcquireReadLock; + FLock.ReleaseUpgradeableReadLock; + AssertTrue(FLock.IsReadLockHeld); + ExpectRecursion(laUpgrade); + FLock.ReleaseReadLock; +end; + +procedure TCoreReadWriteLockTests.FiniteTimeoutIsBoundedAndRemovesWaiter; +var LReader: TLockWorker; +begin + FLock.AcquireWriteLock; + LReader := Worker(laRead, 90); + Completed(LReader); + AssertFalse(LReader.Acquired); + AssertTrue('Actually waited', LReader.Elapsed >= 75); + AssertTrue('Bounded timeout', LReader.Elapsed < 1000); + AssertEquals(0, FLock.WaitingReadCount); + FLock.ReleaseWriteLock; +end; + +procedure TCoreReadWriteLockTests.ZeroTimeoutDoesNotRegisterWaiter; +var LWorker: TLockWorker; +begin + FLock.AcquireReadLock; + LWorker := Worker(laWrite, 0); + Completed(LWorker); + AssertFalse(LWorker.Acquired); + AssertTrue(LWorker.Elapsed < 500); + AssertEquals(0, FLock.WaitingWriteCount); + FLock.ReleaseReadLock; +end; + +procedure TCoreReadWriteLockTests.InfiniteTimeoutWakesOnRelease; +var LReader: TLockWorker; +begin + FLock.AcquireWriteLock; + LReader := Worker(laRead, -1); + WaitCount(0, 1); + AssertEquals(Ord(wrTimeout), Ord(LReader.Done.WaitFor(0))); + FLock.ReleaseWriteLock; + Await(LReader.Entered, 'Infinite waiter wakes on release'); + Release(LReader); +end; + +procedure TCoreReadWriteLockTests.TimedOutWriterReleasesWaitingReaders; +var LWriter, LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite, 160); + WaitCount(1, 1); + LReader := Worker(laRead); + WaitCount(0, 1); + Completed(LWriter); + AssertFalse(LWriter.Acquired); + Await(LReader.Entered, 'Writer timeout releases reader while original read remains held'); + AssertTrue(FLock.IsReadLockHeld); + Release(LReader); + FLock.ReleaseReadLock; +end; + +procedure TCoreReadWriteLockTests.WrongThreadCannotReleaseAnyMode; +var LAction: TLockAction; LWorker: TLockWorker; +begin + for LAction := laReleaseRead to laReleaseUpgrade do begin + case LAction of + laReleaseRead: FLock.AcquireReadLock; + laReleaseWrite: FLock.AcquireWriteLock; + laReleaseUpgrade: FLock.AcquireUpgradeableReadLock; + end; + LWorker := Worker(LAction); + Await(LWorker.Done, 'Wrong-thread release completed'); + AssertEquals('ESynchronizationLockException', LWorker.ErrorClass); + case LAction of + laReleaseRead: begin + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + end; + laReleaseWrite: begin + AssertTrue(FLock.IsWriteLockHeld); + FLock.ReleaseWriteLock; + end; + laReleaseUpgrade: begin + AssertTrue(FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseUpgradeableReadLock; + end; + end; + end; +end; + +procedure TCoreReadWriteLockTests.UnmatchedReleaseDoesNotChangeOwnership; +begin + FLock.AcquireReadLock; + try FLock.ReleaseWriteLock; Fail('Unmatched release'); + except on E: ESynchronizationLockException do ; end; + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + try FLock.ReleaseReadLock; Fail('Extra release'); + except on E: ESynchronizationLockException do ; end; + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TCoreReadWriteLockTests.InvalidTimeoutDoesNotAcquire; +begin + try FLock.TryAcquireReadLock(-2); Fail('Invalid read timeout'); + except on E: EArgumentOutOfRangeException do ; end; + try FLock.TryAcquireWriteLock(-2); Fail('Invalid write timeout'); + except on E: EArgumentOutOfRangeException do ; end; + try FLock.TryAcquireUpgradeableReadLock(-2); Fail('Invalid upgrade timeout'); + except on E: EArgumentOutOfRangeException do ; end; + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TCoreReadWriteLockTests.DisposeRejectsActiveOwnershipAndCanBeRetried; +begin + FLock.AcquireReadLock; + try FLock.Dispose; Fail('Dispose with owner'); + except on E: ESynchronizationLockException do ; end; + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + FLock.Dispose; + FLock.Dispose; + try FLock.AcquireReadLock; Fail('Disposed acquisition'); + except on E: ELockDisposedException do ; end; +end; + +procedure TCoreReadWriteLockTests.DisposeRejectsWaiterAndWaiterLaterCompletes; +var LWriter: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite); + WaitCount(1, 1); + try FLock.Dispose; Fail('Dispose with waiter'); + except on E: ESynchronizationLockException do ; end; + AssertEquals(1, FLock.WaitingWriteCount); + FLock.ReleaseReadLock; + Await(LWriter.Entered, 'Rejected dispose leaves waiter usable'); + Release(LWriter); + FLock.Dispose; +end; + +procedure TCoreReadWriteLockTests.DisposedLockRejectsAcquisitions; +begin + FLock.Dispose; + try FLock.AcquireReadLock; Fail('Disposed read'); + except on E: ELockDisposedException do ; end; + try FLock.AcquireWriteLock; Fail('Disposed write'); + except on E: ELockDisposedException do ; end; + try FLock.AcquireUpgradeableReadLock; Fail('Disposed upgrade'); + except on E: ELockDisposedException do ; end; +end; + +procedure TCoreReadWriteLockTests.ExceptionCleanupReleasesOnlyOwnedAcquisitions; +var LWriter: TLockWorker; +begin + RecursivePolicy; + FLock.AcquireWriteLock; + try + try + FLock.AcquireWriteLock; + try + FLock.AcquireReadLock; + try raise EAbort.Create('Expected protected work failure'); + finally FLock.ReleaseReadLock; end; + finally FLock.ReleaseWriteLock; end; + except on E: EAbort do ; end; + AssertTrue(FLock.IsWriteLockHeld); + AssertEquals(1, FLock.RecursiveWriteCount); + finally FLock.ReleaseWriteLock; end; + LWriter := Worker(laWrite); + Await(LWriter.Entered, 'Exception cleanup released ownership'); + Release(LWriter); +end; + +procedure TCoreReadWriteLockTests.HeldAndRecursivePropertiesAreThreadSpecific; +var LReader: TLockWorker; +begin + RecursivePolicy; + FLock.AcquireUpgradeableReadLock; + FLock.AcquireUpgradeableReadLock; + LReader := Worker(laRead); + Await(LReader.Entered, 'Independent reader'); + AssertEquals(1, LReader.OwnReadCount); + AssertEquals(0, LReader.OwnWriteCount); + AssertEquals(0, LReader.OwnUpgradeCount); + AssertFalse(FLock.IsReadLockHeld); + AssertEquals(2, FLock.RecursiveUpgradeCount); + Release(LReader); + FLock.ReleaseUpgradeableReadLock; + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TCoreReadWriteLockTests.FiniteWaitPermitsCooperativeCancellation; +var LReader: TLockWorker; LStarted: UInt64; +begin + FLock.AcquireWriteLock; + LReader := Worker(laCancellableRead, 35); + WaitCount(0, 1); + LStarted := Tick; + LReader.RequestStop; + Completed(LReader); + AssertTrue('Cooperative stop completes after finite wait', Tick - LStarted < 700); + AssertTrue('Cancellation retains owner', FLock.IsWriteLockHeld); + FLock.ReleaseWriteLock; +end; + +procedure TCoreReadWriteLockTests.PromotedWriteExceptionRetainsSurroundingUpgradeableLock; +var LWriter, LReader: TLockWorker; +begin + FLock.AcquireUpgradeableReadLock; + try + try + FLock.AcquireWriteLock; + try + raise EAbort.Create('Expected promoted-write failure'); + finally + FLock.ReleaseWriteLock; + end; + except on E: EAbort do ; end; + AssertFalse('Only write ownership was released', FLock.IsWriteLockHeld); + AssertTrue('Surrounding upgradeable ownership survives', FLock.IsUpgradeableReadLockHeld); + AssertEquals(1, FLock.RecursiveUpgradeCount); + LWriter := Worker(laWrite, 0); + Completed(LWriter); + AssertFalse('The remaining upgrader excludes another writer', LWriter.Acquired); + LReader := Worker(laRead); + Await(LReader.Entered, 'Readers can share the remaining upgradeable lock'); + Release(LReader); + finally + FLock.ReleaseUpgradeableReadLock; + end; + LWriter := Worker(laWrite); + Await(LWriter.Entered, 'Final typed release frees the surrounding ownership'); + Release(LWriter); +end; + +initialization + RegisterTest(TCoreReadWriteLockTests); +end. diff --git a/src/tests/experimental/fixtures/README.md b/src/tests/experimental/fixtures/README.md new file mode 100644 index 000000000..145f9c855 --- /dev/null +++ b/src/tests/experimental/fixtures/README.md @@ -0,0 +1,22 @@ +# Historical block fixtures + +Each `.bin` file holds a little-endian `Cardinal` count (5), then five public +PascalCoin mainnet blocks saved with +`TPCOperationsComp.SaveBlockToStream(False, Stream)`. + +`mainnet-blocks-0-4.bin` holds blocks 0–4, which have no operations. Regression +tests load it. The 1,630-byte file has SHA-256 +`6A47E07B8D0B4696C9D223A6A48B6C449A32FEDE3A95A5E7A9F1B9D0885A8306`. Its headers +come from a mainnet safebox via `TPCAbstractMem.GetBlockInfo`. + +| Height | Unix timestamp | Proof of work | +|---|---|---| +| 0 | 1470934813 | `00000003A29C32E84A539ADE24397D41D30116A6FAFEC17B7D9CED68A4238C92` | +| 1 | 1470935312 | `00000009C81199431ACCC786CE5A1DDAA7381DC55261E56E096AA2B8389502F4` | +| 2 | 1470935390 | `0000000C2EAC295B4A182490E1C92C1F450ED99B2C64983ECD20850A0CE1C9FF` | +| 3 | 1470935433 | `0000000333BF4E90CEF1FD2472781DB946837F1F824CA86515E94C6DF6F90FE7` | +| 4 | 1470935652 | `00000001C3B233BA3E49B5BF6272B3A89C29A716A2790A5BF30778E3EF093CB2` | + +The [large backlog probe](../native/network-backlog/README.md) embeds +`mainnet-blocks-841529-841533.bin`, described in +`mainnet-blocks-841529-841533.json`. diff --git a/src/tests/experimental/fixtures/mainnet-blocks-0-4.bin b/src/tests/experimental/fixtures/mainnet-blocks-0-4.bin new file mode 100644 index 000000000..940847f03 Binary files /dev/null and b/src/tests/experimental/fixtures/mainnet-blocks-0-4.bin differ diff --git a/src/tests/experimental/fixtures/mainnet-blocks-841529-841533.bin b/src/tests/experimental/fixtures/mainnet-blocks-841529-841533.bin new file mode 100644 index 000000000..2e10da629 Binary files /dev/null and b/src/tests/experimental/fixtures/mainnet-blocks-841529-841533.bin differ diff --git a/src/tests/experimental/fixtures/mainnet-blocks-841529-841533.json b/src/tests/experimental/fixtures/mainnet-blocks-841529-841533.json new file mode 100644 index 000000000..68344bcf3 --- /dev/null +++ b/src/tests/experimental/fixtures/mainnet-blocks-841529-841533.json @@ -0,0 +1,16 @@ +{ + "network": "PascalCoin mainnet", + "format": "Little-endian Cardinal count 5, followed by five unmodified TPCOperationsComp.SaveBlockToStream(False) network block serializations", + "bytes": 4465, + "sha256": "BA574C8A608115157FD8B89D8F8843D723D9B9265554C5BBB7EB11B50F5E6FDC", + "initialBlockCount": 841529, + "finalBlockCount": 841534, + "blocks": [ + { "block": 841529, "bytes": 461, "sha256": "961537103B83D1676CBB0FF8D12B2CEE56FBF422747415EB867BE40E9D9947A1" }, + { "block": 841530, "bytes": 1422, "sha256": "106DF45F617C1C3F8D8B1A912E7E8DBB3160ECBC7E61DB4DC5449C80F9E17EE3" }, + { "block": 841531, "bytes": 555, "sha256": "0DDBBF3276454BC3E806710BF234603AE8D06FF9723B80CC87D6D10761A29C87" }, + { "block": 841532, "bytes": 1468, "sha256": "3FA915A9FFDC6237DB9C8DF6C3BA3CD1C4F1BDCB705A79A6C69950877C9437C2" }, + { "block": 841533, "bytes": 555, "sha256": "9A98E64BF9A96BFCCE732621143817520935F9BA0690D0CE0688007CDA1C91A8" } + ], + "scope": "Public block headers and operations only. No wallet keys or private account material. The test uses normal proof-of-work, operation and bank validation; no trusted-header bypass. These blocks require a separate matching owned chain fixture at height 841529." +} diff --git a/src/tests/experimental/framework/UBulkCacheChecks.pas b/src/tests/experimental/framework/UBulkCacheChecks.pas new file mode 100644 index 000000000..d05c58ce2 --- /dev/null +++ b/src/tests/experimental/framework/UBulkCacheChecks.pas @@ -0,0 +1,715 @@ +unit UBulkCacheChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://www.sphere10.com> + Distributed under the MIT software license, see LICENSE. } + +{$IFDEF FPC} + {$MODE Delphi} + {$MODESWITCH NESTEDPROCVARS} +{$ENDIF} + +interface + +const BulkCacheCheckCount = 29; + +// The same assertions run from FPCUnit and the native Delphi probe. +procedure RunBulkCacheCheck(AIndex: Integer); + +implementation + +uses + {$IFDEF FPC}Classes, SysUtils, Generics.Collections, Generics.Defaults, + {$ELSE}System.Classes, System.SysUtils, System.Generics.Collections, + System.Generics.Defaults, System.TimeSpan,{$ENDIF} + UCommon, UMemory, UCache; + +type + TPlainBase = TCacheBase<Integer, String>; + TBulkBase = TBulkFetchCacheBase<Integer, String>; + TBulkAction = TBulkFetchActionCache<Integer, String>; + TPerKeyAction = TActionCache<Integer, String>; + TScalarAction = TActionCache<String, Integer>; + TValues = TCacheBulkValues<Integer, String>; + TTestBulk = class(TBulkBase) + public + Source: TValues; + Calls, StaleChecks: Integer; + RaiseOnFetch, Stale, ReenterRefresh, ReenterRead: Boolean; + protected + procedure BulkFetch(AValues: TValues); override; + function CheckStaleness(const AKey: Integer; AItem: TCachedItem<String>): Boolean; override; + end; + + TContext = class + private + FResources: TDisposables; + public + Source: TValues; + Cache: TTestBulk; + FetchingCount, FetchedCount, RemovingCount, RemovedCount: Integer; + constructor Create(AOnce: Boolean; AExpiry: TCacheExpirationPolicy; AStale: TCacheStaleValuePolicy; ANull: TCacheNullValuePolicy); + procedure FillValues(AValues: TValues); + function FetchValue(const AKey: Integer): String; + function EstimateValue(const AValue: String): SizeInt; + procedure Fetching(Sender: TObject; const AKey: Integer); + procedure Fetched(Sender: TObject; const AKey: Integer; const AValue: String); + procedure Removing(Sender: TObject; const AKey: Integer; AItem: TCachedItem<String>); + procedure Removed(Sender: TObject; const AKey: Integer; const AValue: String); + end; + + TBulkReader = class(TThread) + private FCache: TTestBulk; + protected procedure Execute; override; + public + Error: String; + constructor Create(ACache: TTestBulk); + end; + + TObjectBulk = TBulkFetchActionCache<Integer, TObject>; + TObjectValues = TCacheBulkValues<Integer, TObject>; + TInterfaceBulk = TBulkFetchActionCache<Integer, IInterface>; + TInterfaceCache = TCacheBase<Integer, IInterface>; + TInterfaceValues = TCacheBulkValues<Integer, IInterface>; + TDestructionCheckValue = class(TInterfacedObject) + private + FCache: TInterfaceCache; + FRejected: PBoolean; + public + constructor Create(ACache: TInterfaceCache; ARejected: PBoolean); + destructor Destroy; override; + end; + TTrackedValue = class destructor Destroy; override; + end; + +var DestroyedValues: Integer; + +procedure Check(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +constructor TContext.Create(AOnce: Boolean; AExpiry: TCacheExpirationPolicy; AStale: TCacheStaleValuePolicy; ANull: TCacheNullValuePolicy); +begin + inherited Create; + Source := TValues(FResources.AddObject(TValues.Create)); + Source.Add(1,'one'); Source.Add(2,'two'); + Cache := TTestBulk(FResources.AddObject(TTestBulk.Create(nil,AExpiry, + TTimeSpan.FromSeconds(1),AOnce,ANull,AStale))); + Cache.Source := Source; +end; + +procedure TContext.FillValues(AValues: TValues); +var pair: TPair<Integer,String>; +begin + for pair in Source do AValues.AddOrSetValue(pair.Key,pair.Value); +end; + +function TContext.FetchValue(const AKey: Integer): String; +begin + Result := IntToStr(AKey); +end; + +function TContext.EstimateValue(const AValue: String): SizeInt; +begin + Result := Length(AValue); +end; + +procedure TContext.Fetching(Sender: TObject; const AKey: Integer); +begin + Inc(FetchingCount); +end; + +procedure TContext.Fetched(Sender: TObject; const AKey: Integer; const AValue: String); +begin + Inc(FetchedCount); +end; + +procedure TContext.Removing(Sender: TObject; const AKey: Integer; AItem: TCachedItem<String>); +begin + Inc(RemovingCount); +end; + +procedure TContext.Removed(Sender: TObject; const AKey: Integer; const AValue: String); +begin + Inc(RemovedCount); +end; + +procedure TTestBulk.BulkFetch(AValues: TValues); +var pair: TPair<Integer,String>; +begin + Inc(Calls); + if RaiseOnFetch then raise EInvalidOperation.Create('provider failed'); + if ReenterRefresh then ForceRefresh; + if ReenterRead then Check(not ContainsCachedItem(99),'Reentrant query must see the current snapshot'); + for pair in Source do AValues.AddOrSetValue(pair.Key,pair.Value); +end; + +function TTestBulk.CheckStaleness(const AKey: Integer; AItem: TCachedItem<String>): Boolean; +begin + Inc(StaleChecks); Result := Stale; +end; + +constructor TBulkReader.Create(ACache: TTestBulk); +begin + inherited Create(True); + FCache := ACache; +end; + +procedure TBulkReader.Execute; +var i: Integer; +begin + try + for i := 1 to 100 do Check(FCache[1] = 'one','Concurrent snapshot read changed'); + except on E: Exception do Error := E.Message; end; +end; + +destructor TTrackedValue.Destroy; +begin + Inc(DestroyedValues); + inherited; +end; + +constructor TDestructionCheckValue.Create(ACache: TInterfaceCache; ARejected: PBoolean); +begin + inherited Create; + FCache := ACache; + FRejected := ARejected; +end; + +destructor TDestructionCheckValue.Destroy; +begin + // The last managed value reference is released while the cache destructor + // still owns its lock. This tests rejection without using a freed instance. + try FCache.Reap(1); + except on E: EInvalidOperation do FRejected^ := True; end; + inherited; +end; + +procedure GlobalFill(AValues: TValues); +begin + AValues.Add(7,'global'); +end; + +function FetchPerKey(const AKey: Integer): String; +begin + Result := IntToStr(AKey); +end; + +function FetchScalar(const AKey: String): Integer; +begin + Result := Length(AKey); +end; + +function EstimateText(const AValue: String): SizeInt; +begin + Result := Length(AValue); +end; + +procedure CheckSiblingCallbacks; +var + resources: TDisposables; + ctx: TContext; + perKey: TPerKeyAction; + bulk: TBulkAction; + preloaded: TPreloadedCache<Integer, String>; + values: TCacheBulkValues<Integer, String>; + globalFetch: TCacheGlobalFetchFunc<Integer, String>; + objectFetch: TCacheObjectFetchFunc<Integer, String>; + nestedFetch: TCacheNestedFetchFunc<Integer, String>; + globalEstimate: TCacheGlobalEstimateFunc<String>; + objectEstimate: TCacheObjectEstimateFunc<String>; + nestedEstimate: TCacheNestedEstimateFunc<String>; + globalBulk: TCacheGlobalBulkFetchProc<Integer, String>; + objectBulk: TCacheObjectBulkFetchProc<Integer, String>; + nestedBulk: TCacheNestedBulkFetchProc<Integer, String>; + fetcher: ICacheValueFetcher<Integer, String>; + estimator: ICacheSizeEstimator<String>; + bulkFetcher: ICacheBulkValueFetcher<Integer, String>; + fetching: TCacheItemFetchingEvent<Integer>; + fetched, removed: TCacheItemValueEvent<Integer, String>; + removing: TCacheItemRemovingEvent<Integer, String>; + suffix: String; + {$IFDEF FPC} + function FetchNested(const AKey: Integer): String; + begin + Result := IntToStr(AKey) + suffix; + end; + function EstimateNested(const AValue: String): SizeInt; + begin + Result := Length(AValue) + Length(suffix); + end; + procedure FillNested(AValues: TCacheBulkValues<Integer, String>); + begin + AValues.Add(9,suffix); + end; + {$ENDIF} +begin + ctx := TContext(resources.AddObject(TContext.Create(False,TCacheExpirationPolicy.epNone, + TCacheStaleValuePolicy.svpAssumeNeverStale,TCacheNullValuePolicy.nvpCacheNormally))); + values := TCacheBulkValues<Integer, String>(resources.AddObject(TCacheBulkValues<Integer, String>.Create)); + globalFetch := FetchPerKey; + globalEstimate := EstimateText; + fetcher := TCacheValueFetcher<Integer, String>.FromGlobalFunction(globalFetch); + estimator := TCacheSizeEstimator<String>.FromGlobalFunction(globalEstimate); + fetcher := TCacheValueFetcher<Integer, String>.Create(fetcher); + estimator := TCacheSizeEstimator<String>.Create(estimator); + Check(fetcher.Fetch(7) = '7','Unit-scoped global fetch adapter and wrapper'); + Check(estimator.Estimate('seven') = 5,'Unit-scoped global estimator and wrapper'); + objectFetch := ctx.FetchValue; + objectEstimate := ctx.EstimateValue; + Check(TCacheValueFetcher<Integer, String>.FromObjectFunction(objectFetch).Fetch(8) = '8', + 'Unit-scoped object fetch adapter'); + Check(TCacheSizeEstimator<String>.FromObjectFunction(objectEstimate).Estimate('eight') = 5, + 'Unit-scoped object estimator'); + globalBulk := GlobalFill; + objectBulk := ctx.FillValues; + bulkFetcher := TCacheBulkValueFetcher<Integer, String>.FromGlobalFunction(globalBulk); + bulkFetcher := TCacheBulkValueFetcher<Integer, String>.Create(bulkFetcher); + bulkFetcher.Fetch(values); + Check(values[7] = 'global','Unit-scoped global bulk adapter and wrapper'); + values.Clear; + TCacheBulkValueFetcher<Integer, String>.FromObjectFunction(objectBulk).Fetch(values); + Check(values[2] = 'two','Unit-scoped object bulk adapter'); + values.Clear; + suffix := 'nested'; + {$IFDEF FPC} + nestedFetch := FetchNested; + nestedEstimate := EstimateNested; + nestedBulk := FillNested; + {$ELSE} + nestedFetch := function(const AKey: Integer): String + begin + Result := IntToStr(AKey) + suffix; + end; + nestedEstimate := function(const AValue: String): SizeInt + begin + Result := Length(AValue) + Length(suffix); + end; + nestedBulk := procedure(AValues: TCacheBulkValues<Integer, String>) + begin + AValues.Add(9,suffix); + end; + {$ENDIF} + Check(TCacheValueFetcher<Integer, String>.FromNestedFunction(nestedFetch).Fetch(9) = '9nested', + 'Unit-scoped nested fetch adapter'); + Check(TCacheSizeEstimator<String>.FromNestedFunction(nestedEstimate).Estimate('nine') = 10, + 'Unit-scoped nested estimator'); + TCacheBulkValueFetcher<Integer, String>.FromNestedFunction(nestedBulk).Fetch(values); + Check(values[9] = 'nested','Unit-scoped nested bulk adapter'); + + // One event variable now binds to each cache kind without inherited-type aliases or casts. + fetching := ctx.Fetching; + fetched := ctx.Fetched; + removing := ctx.Removing; + removed := ctx.Removed; + perKey := TPerKeyAction(resources.AddObject(TPerKeyAction.Create(nil,fetcher,estimator, + idpNone,TCacheReapPolicy.crpNone,TCacheExpirationPolicy.epNone, + TCacheNullValuePolicy.nvpCacheNormally,10,TTimeSpan.MaxValue))); + bulk := TBulkAction(resources.AddObject(TBulkAction.Create(nil,bulkFetcher, + TCacheExpirationPolicy.epNone,TTimeSpan.MaxValue))); + preloaded := TPreloadedCache<Integer, String>(resources.AddObject( + TPreloadedCache<Integer, String>.Create(nil,ctx.Source))); + perKey.ItemFetching := fetching; + bulk.ItemFetching := fetching; + preloaded.ItemFetching := fetching; + perKey.ItemFetched := fetched; + bulk.ItemFetched := fetched; + preloaded.ItemFetched := fetched; + perKey.ItemRemoving := removing; + bulk.ItemRemoving := removing; + preloaded.ItemRemoving := removing; + perKey.ItemRemoved := removed; + bulk.ItemRemoved := removed; + preloaded.ItemRemoved := removed; + Check(perKey[7] = '7','Ordinary cache uses sibling adapters and shared events'); + Check(bulk[7] = 'global','Bulk cache uses sibling adapters and shared events'); + Check(preloaded[1] = 'one','Preloaded cache uses the shared events'); + perKey.Purge; + bulk.Purge; + preloaded.Purge; + // Bulk snapshots use explicit admission; only the ordinary request invokes per-key fetching. + Check(ctx.FetchingCount = 1,'Bulk admission does not invent per-key fetching notifications'); + Check(ctx.FetchedCount = 1,'Bulk admission does not invent per-key fetched notifications'); + Check(ctx.RemovingCount = 4,'All cache kinds publish removing through one event type'); + Check(ctx.RemovedCount = 4,'All cache kinds publish removed through one event type'); +end; + +procedure CheckUnitScopedPolicies; +var + resources: TDisposables; + perKey: TPerKeyAction; + scalar: TScalarAction; + bulk: TBulkAction; + preloaded: TPreloadedCache<Integer, String>; + source: TDictionary<Integer, String>; + expiration: TCacheExpirationPolicy; + reap: TCacheReapPolicy; + nulls: TCacheNullValuePolicy; + stale: TCacheStaleValuePolicy; +begin + // Every specialization must accept the same policy variables without a base alias or cast. + for expiration := Low(TCacheExpirationPolicy) to High(TCacheExpirationPolicy) do + for reap := Low(TCacheReapPolicy) to High(TCacheReapPolicy) do + for nulls := Low(TCacheNullValuePolicy) to High(TCacheNullValuePolicy) do + for stale := Low(TCacheStaleValuePolicy) to High(TCacheStaleValuePolicy) do begin + perKey := TPerKeyAction.Create(nil, + TCacheValueFetcher<Integer,String>.FromGlobalFunction(FetchPerKey),nil, + idpNone,reap,expiration,nulls,4,TTimeSpan.MaxValue,0.1,stale); + try + scalar := TScalarAction.Create(nil, + TCacheValueFetcher<String,Integer>.FromGlobalFunction(FetchScalar),nil, + idpNone,perKey.ReapPolicy,perKey.ExpirationPolicy, + perKey.NullValuePolicy,4,TTimeSpan.MaxValue,0.1,perKey.StaleValuePolicy); + try + Check(scalar.ReapPolicy = reap,'Reap policy is shared across specializations'); + Check(scalar.ExpirationPolicy = expiration,'Expiration policy is shared across specializations'); + Check(scalar.NullValuePolicy = nulls,'Null policy is shared across specializations'); + Check(scalar.StaleValuePolicy = stale,'Stale policy is shared across specializations'); + Check(perKey[7] = '7','Per-key cache uses shared policy configuration'); + Check(scalar['seven'] = 5,'A different key/value specialization uses the same policies'); + finally + scalar.Free; + end; + finally + perKey.Free; + end; + end; + + for expiration := Low(TCacheExpirationPolicy) to High(TCacheExpirationPolicy) do + for nulls := Low(TCacheNullValuePolicy) to High(TCacheNullValuePolicy) do + for stale := Low(TCacheStaleValuePolicy) to High(TCacheStaleValuePolicy) do begin + bulk := TBulkAction.Create(nil, + TCacheBulkValueFetcher<Integer,String>.FromGlobalFunction(GlobalFill), + expiration,TTimeSpan.MaxValue,False,nulls,stale); + try + Check(bulk.ExpirationPolicy = expiration,'Bulk constructor accepts the unit expiration type'); + Check(bulk.NullValuePolicy = nulls,'Bulk constructor accepts the unit null type'); + Check(bulk.StaleValuePolicy = stale,'Bulk constructor accepts the unit stale type'); + Check(bulk[7] = 'global','Bulk cache remains usable with every policy configuration'); + finally + bulk.Free; + end; + end; + + // Preloaded sources still accept an ordinary caller-owned RTL dictionary. + source := TDictionary<Integer, String>(resources.AddObject(TDictionary<Integer, String>.Create)); + source.Add(7,'preloaded'); + preloaded := TPreloadedCache<Integer, String>(resources.AddObject( + TPreloadedCache<Integer, String>.Create(nil,source))); + expiration := preloaded.ExpirationPolicy; + reap := preloaded.ReapPolicy; + nulls := preloaded.NullValuePolicy; + stale := preloaded.StaleValuePolicy; + Check(expiration = TCacheExpirationPolicy.epNone,'Preloaded cache exposes the shared expiration type'); + Check(reap = TCacheReapPolicy.crpNone,'Preloaded cache exposes the shared reap type'); + Check(nulls = TCacheNullValuePolicy.nvpCacheNormally,'Preloaded cache exposes the shared null type'); + Check(stale = TCacheStaleValuePolicy.svpAssumeNeverStale,'Preloaded cache exposes the shared stale type'); + Check(preloaded[7] = 'preloaded','Preloaded cache retains its source behavior'); +end; + +procedure RunBulkCacheCheck(AIndex: Integer); +var + ctx: TContext; + resources: TDisposables; + action: TBulkAction; + preloaded: TPreloadedCache<Integer,String>; + values: TArray<String>; + failed: Boolean; + i: Integer; + readers: array[0..3] of TBulkReader; + fetcher: ICacheBulkValueFetcher<Integer,String>; + wrapper: TCacheBulkValueFetcher<Integer,String>; + nullPolicy: TCacheNullValuePolicy; + objectCache: TObjectBulk; + objectValue: TTrackedValue; + nestedValue: String; + original: TCachedItem<String>; + interfaceCache: TInterfaceBulk; + interfaceFetcher: TCacheNestedBulkFetchProc<Integer,IInterface>; + destructionRejected: Boolean; + objectFetcher: TCacheNestedBulkFetchProc<Integer,TObject>; + nestedFetcher: TCacheNestedBulkFetchProc<Integer,String>; + {$IFDEF FPC} + procedure NestedFill(AValues: TValues); + begin + AValues.Add(9,nestedValue); + end; + procedure ObjectFill(AValues: TObjectValues); + begin + AValues.Add(1,nil); + AValues.Add(2,objectValue); + end; + procedure InterfaceFill(AValues: TInterfaceValues); + begin + AValues.Add(1,TDestructionCheckValue.Create(interfaceCache,@destructionRejected)); + end; + {$ENDIF} +begin + if AIndex = 29 then begin + CheckSiblingCallbacks; + Exit; + end; + if AIndex = 28 then begin + CheckUnitScopedPolicies; + Exit; + end; + if AIndex = 10 then begin + // Bulk cache payloads stay owned by the source, including nil policies. + DestroyedValues := 0; + objectValue := TTrackedValue(resources.AddObject(TTrackedValue.Create)); + {$IFDEF FPC} + objectFetcher := ObjectFill; + {$ELSE} + objectFetcher := procedure(AValues: TObjectValues) + begin + AValues.Add(1,nil); + AValues.Add(2,objectValue); + end; + {$ENDIF} + for nullPolicy := Low(TCacheNullValuePolicy) to High(TCacheNullValuePolicy) do begin + objectCache := TObjectBulk.Create(nil,TCacheBulkValueFetcher<Integer,TObject>.FromNestedFunction(objectFetcher), + TCacheExpirationPolicy.epNone,TTimeSpan.MaxValue,True,nullPolicy); + try + failed := False; + try Check(objectCache[1] = nil,'Null snapshot entry must stay nil'); + except on E: EInvalidOperation do failed := True; end; + Check(failed = (nullPolicy = TCacheNullValuePolicy.nvpThrow),'Null read policy'); + Check(objectCache[2] = objectValue,'Non-null borrowed payload'); + Check(objectCache.ContainsCachedItem(1) = (nullPolicy <> TCacheNullValuePolicy.nvpReturnButDontCache), + 'Null membership policy'); + objectCache.ForceRefresh; + finally objectCache.Free; end; + Check(DestroyedValues = 0,'Refresh/destruction must not free borrowed payloads'); + end; + Exit; + end; + + ctx := TContext(resources.AddObject(TContext.Create((AIndex in [5,6,7,8,9,16,17,24,25]), + TCacheExpirationPolicy.epSinceFetchedTime, + TCacheStaleValuePolicy.svpCheckStaleOnDemand,TCacheNullValuePolicy.nvpCacheNormally))); + case AIndex of + 1: begin + Check(ctx.Cache[1] = 'one','Initial get'); + Check((ctx.Cache.ItemCount = 2) and (ctx.Cache.Calls = 1),'One get loads the whole source'); + Check(ctx.Cache[2] = 'two','Second prefetched key'); + Check(ctx.Cache.Calls = 1,'Hit must not fetch'); + Check(ctx.Cache.CurrentSize = 0,'Bulk snapshots do not consume per-entry eviction capacity'); + end; + 2: begin + Check(ctx.Cache[99] = '','Missing key default'); + Check(ctx.Cache.Calls = 1,'Cold missing-key request must fetch exactly once'); + Check(ctx.Cache[100] = '','Subsequent missing key'); + Check(ctx.Cache.Calls = 2,'Each ordinary missing request fetches at most once'); + end; + 3: begin + Check(ctx.Cache.ContainsCachedItem(1),'Contains initializes the complete snapshot'); + Check(not ctx.Cache.ContainsCachedItem(99),'Contains miss'); + Check(ctx.Cache.Calls = 1,'Contains miss does not refetch'); + end; + 4: begin + values := ctx.Cache.GetAllCachedValues; + Check(Length(values) = 2,'All-values initializes snapshot'); + ctx.Source.Add(3,'three'); + Check(Length(values) = 2,'All-values result is a copied array'); + Check(Length(ctx.Cache.GetAllCachedValues) = 2,'All-values does not silently refresh'); + Check(ctx.Cache.Calls = 1,'All-values loads once'); + end; + 5,6,7: begin + Check(ctx.Cache[1] = 'one','Fetch-once initial value'); + ctx.Source[1] := 'changed'; + case AIndex of + 5: ctx.Cache.Invalidate(1); + 6: ctx.Cache.Get(1).FFetchedOn := Now - 1; + 7: ctx.Cache.Stale := True; + end; + Check(ctx.Cache.ContainsCachedItem(1),'Fetch-once keeps original membership'); + Check(ctx.Cache[1] = 'one','Fetch-once retains original snapshot despite invalidation/expiry/staleness'); + Check(ctx.Cache.Calls = 1,'Fetch-once must not refetch automatically'); + ctx.Cache.ForceRefresh; + Check(ctx.Cache[1] = 'changed','Explicit refresh replaces fetch-once snapshot'); + Check(ctx.Cache.Calls = 2,'Explicit refresh provider call'); + end; + 8: begin + Check(ctx.Cache[99] = '','Fetch-once initial missing'); + ctx.Source.Add(99,'later'); + Check(ctx.Cache[99] = '','Missing remains part of fetch-once snapshot'); + Check(ctx.Cache.Calls = 1,'Fetch-once missing never refetches'); + end; + 9: begin + Check(ctx.Cache[1] = 'one','Initial snapshot'); ctx.Cache.Purge; + Check(ctx.Cache.FetchCount = 0,'Purge resets load count'); + ctx.Source[1] := 'changed'; + Check(ctx.Cache[1] = 'changed','Purge permits new first snapshot'); + Check(ctx.Cache.Calls = 2,'Provider called after purge'); + end; + 11,12,13: begin + Check(ctx.Cache[1] = 'one','Initial snapshot'); ctx.Source[1] := 'changed'; + case AIndex of + 11: ctx.Cache.Invalidate(1); + 12: ctx.Cache.Get(1).FFetchedOn := Now - 1; + 13: ctx.Cache.Stale := True; + end; + Check(ctx.Cache[1] = 'changed','Ordinary invalidated/expired/stale snapshot reloads'); + Check(ctx.Cache.Calls = 2,'One refresh per triggering request'); + Check(ctx.Cache.ItemCount = 2,'Refresh restores all source keys'); + end; + 14: begin + Check(ctx.Cache[1] = 'one','Initial snapshot'); ctx.Cache.RaiseOnFetch := True; + failed := False; + try ctx.Cache.ForceRefresh; except on E: EInvalidOperation do failed := True; end; + Check(failed,'Provider failure must propagate'); + Check(ctx.Cache.ItemCount = 0,'Failed purge-first refresh leaves no old snapshot'); + Check(ctx.Cache.FetchCount = 0,'Failed refresh does not mark loaded'); + ctx.Cache.RaiseOnFetch := False; ctx.Source[1] := 'recovered'; + Check(ctx.Cache[1] = 'recovered','A failed provider can be retried'); + Check(ctx.Cache.Calls = 3,'Failed batch is attempted once'); + end; + 15: begin + preloaded := TPreloadedCache<Integer,String>(resources.AddObject(TPreloadedCache<Integer,String>.Create(nil,ctx.Source))); + Check(preloaded[1] = 'one','Preloaded initial snapshot'); ctx.Source[1] := 'changed'; + Check(preloaded[1] = 'one','Preloaded snapshot retained'); + preloaded.ForceRefresh; + Check(preloaded[1] = 'changed','Preloaded refresh sees changes to the original borrowed dictionary'); + Check(ctx.Source.Count = 2,'Preloaded does not clear source'); + end; + 16: begin + ctx := TContext(resources.AddObject(TContext.Create(True,TCacheExpirationPolicy.epNone, + TCacheStaleValuePolicy.svpAssumeNeverStale,TCacheNullValuePolicy.nvpThrow))); + failed := False; + try nestedValue := ctx.Cache[99]; + except on E: EInvalidOperation do failed := True; end; + Check(failed,'Missing key obeys throw policy'); + Check(ctx.Cache.Calls = 1,'Cold missing key with throw policy fetches once'); + Check(ctx.Cache[1] = 'one','A missing key does not invalidate the complete snapshot'); + Check(ctx.Cache.Calls = 1,'Fetch-once hit after missing key retains loaded snapshot'); + end; + 17: begin + Check(ctx.Cache[1] = 'one','Initial snapshot'); failed := False; + try TCacheBase<Integer,String>(ctx.Cache).Remove(1); + except on E: ENotSupportedException do failed := True; end; + Check(failed and ctx.Cache.ContainsCachedItem(1),'Bulk removal is rejected through a base reference'); + end; + 18: begin + action := TBulkAction(resources.AddObject(TBulkAction.Create(nil, + TCacheBulkValueFetcher<Integer,String>.FromGlobalFunction(GlobalFill), + TCacheExpirationPolicy.epNone,TTimeSpan.MaxValue))); + Check(action[7] = 'global','Global adapter'); + action := TBulkAction(resources.AddObject(TBulkAction.Create(nil, + TCacheBulkValueFetcher<Integer,String>.FromObjectFunction(ctx.FillValues), + TCacheExpirationPolicy.epNone,TTimeSpan.MaxValue))); + Check(action[2] = 'two','Object adapter'); + nestedValue := 'nested'; + {$IFDEF FPC} + nestedFetcher := NestedFill; + {$ELSE} + nestedFetcher := procedure(AValues: TValues) + begin + AValues.Add(9,nestedValue); + end; + {$ENDIF} + fetcher := TCacheBulkValueFetcher<Integer,String>.FromNestedFunction(nestedFetcher); + wrapper := TCacheBulkValueFetcher<Integer,String>.Create(fetcher); + fetcher := wrapper; + action := TBulkAction(resources.AddObject(TBulkAction.Create(nil,fetcher, + TCacheExpirationPolicy.epNone,TTimeSpan.MaxValue))); + Check(action[9] = 'nested','Nested adapter and interface wrapper'); + end; + 19: begin + failed := False; + try fetcher := TCacheBulkValueFetcher<Integer,String>.FromGlobalFunction(nil); + except on E: EArgumentNilException do failed := True; end; + Check(failed,'Nil global callback rejected'); failed := False; + try fetcher := TCacheBulkValueFetcher<Integer,String>.FromObjectFunction(nil); + except on E: EArgumentNilException do failed := True; end; + Check(failed,'Nil object callback rejected'); failed := False; + try fetcher := TCacheBulkValueFetcher<Integer,String>.FromNestedFunction(nil); + except on E: EArgumentNilException do failed := True; end; + Check(failed,'Nil nested callback rejected'); failed := False; + try action := TBulkAction.Create(nil,nil,TCacheExpirationPolicy.epNone,TTimeSpan.MaxValue); + except on E: EArgumentNilException do failed := True; end; + Check(failed,'Nil fetcher rejected'); failed := False; + try wrapper := TCacheBulkValueFetcher<Integer,String>.Create(nil); + except on E: EArgumentNilException do failed := True; end; + Check(failed,'Nil wrapper fetcher rejected'); failed := False; + try preloaded := TPreloadedCache<Integer,String>.Create(nil,nil); + except on E: EArgumentNilException do failed := True; end; + Check(failed,'Nil preloaded source rejected'); + end; + 20: begin + for i := 0 to High(readers) do readers[i] := TBulkReader(resources.AddObject(TBulkReader.Create(ctx.Cache))); + for i := 0 to High(readers) do readers[i].Start; + for i := 0 to High(readers) do readers[i].WaitFor; + for i := 0 to High(readers) do Check(readers[i].Error = '',readers[i].Error); + Check(ctx.Cache.Calls = 1,'Concurrent reads share one initial batch'); + end; + 21: begin + ctx.Cache.ReenterRefresh := True; failed := False; + try values := ctx.Cache.GetAllCachedValues; + except on E: EInvalidOperation do failed := True; end; + Check(failed,'Recursive ForceRefresh is rejected instead of recursively fetching forever'); + Check(ctx.Cache.Calls = 1,'One attempt for recursive provider'); + ctx.Cache.ReenterRefresh := False; ctx.Cache.ReenterRead := True; + Check(ctx.Cache[1] = 'one','Read-only callback reentry does not refetch'); + Check(ctx.Cache.Calls = 2,'One subsequent retry'); + end; + 22: begin + preloaded := TPreloadedCache<Integer,String>(resources.AddObject(TPreloadedCache<Integer,String>.Create( + nil,ctx.Source,TEqualityComparer<Integer>.Default))); + Check(preloaded[2] = 'two','Configured key comparer works for both storage and source snapshot'); + end; + 23: begin + ctx.Cache.Stale := True; + Check(ctx.Cache[1] = 'one','Always-stale initial source still resolves'); + Check(ctx.Cache.Calls = 1,'Initial stale snapshot cannot cause a second fetch in the same request'); + Check(ctx.Cache[1] = 'one','Subsequent stale source resolves'); + Check(ctx.Cache.Calls = 2,'Next request may refresh once'); + end; + 24,25: begin + original := ctx.Cache.Get(1); + ctx.Source[1] := 'changed'; + if AIndex = 24 then ctx.Cache.Invalidate(1) + else original.FFetchedOn := Now - 1; + Check(TPlainBase(ctx.Cache).Reap(1) = 0,'Bulk reaping through base reference releases no capacity'); + Check(ctx.Cache.ItemCount = 2,'Reaping must preserve the complete snapshot'); + Check(ctx.Cache.Get(1) = original,'Fetch-once must retain the original entry after reaping'); + Check(ctx.Cache[1] = 'one','Fetch-once retained original value'); + Check(ctx.Cache.Calls = 1,'Reaping must not refetch the source'); + end; + 26: begin + original := ctx.Cache.Get(1); + original.FFetchedOn := Now - 1; + ctx.Cache.Invalidate(2); + ctx.Source[1] := 'changed'; + Check(TPlainBase(ctx.Cache).Reap(1) = 0,'Refreshable bulk snapshot also has no per-entry reaping'); + Check(ctx.Cache.ItemCount = 2,'Refreshable snapshot remains complete until next read'); + Check(ctx.Cache.Calls = 1,'Reaping does not fetch'); + Check(ctx.Cache[1] = 'changed','Ordinary expired read still refreshes after no-op reaping'); + Check(ctx.Cache.Calls = 2,'Ordinary expired read refreshes once'); + end; + 27: begin + failed := False; + try TPlainBase(ctx.Cache).Reap(-1); + except on E: EArgumentException do failed := True; end; + Check(failed,'Negative bulk release remains invalid'); + Check(TPlainBase(ctx.Cache).Reap(0) = 0,'Zero release is harmless'); + Check(ctx.Cache.Calls = 0,'Reaping does not initialize the source'); + destructionRejected := False; + {$IFDEF FPC} + interfaceFetcher := InterfaceFill; + {$ELSE} + interfaceFetcher := procedure(AValues: TInterfaceValues) + begin + AValues.Add(1,TDestructionCheckValue.Create(interfaceCache,@destructionRejected)); + end; + {$ENDIF} + interfaceCache := TInterfaceBulk.Create(nil,TCacheBulkValueFetcher<Integer,IInterface>.FromNestedFunction(interfaceFetcher), + TCacheExpirationPolicy.epNone,TTimeSpan.MaxValue,True); + try + interfaceCache.Get(1); // The entry owns the last managed interface reference. + finally interfaceCache.Free; end; + Check(destructionRejected,'Bulk reaping rejects destruction while the object is still alive'); + end; + else raise EArgumentOutOfRangeException.Create('AIndex'); end; +end; + +end. diff --git a/src/tests/experimental/framework/UCache.Tests.pas b/src/tests/experimental/framework/UCache.Tests.pas new file mode 100644 index 000000000..d3fa33ec8 --- /dev/null +++ b/src/tests/experimental/framework/UCache.Tests.pas @@ -0,0 +1,1358 @@ +unit UCache.Tests; + +{ Copyright (c) 2017 - 2018 Herman Schoenfeld <https://www.sphere10.com> + + Cache regression tests. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + THIS LICENSE HEADER MUST NOT BE REMOVED. +} + +{$MODE Delphi} +{$H+} +{$MODESWITCH NESTEDPROCVARS} + +interface + +uses + Classes, SysUtils, Generics.Collections, fpcunit, testregistry, UCache, UCommon, UMemory, UBulkCacheChecks; + +type + TStringPair = TPair<Integer, AnsiString>; + TStringPairList = TList<TStringPair>; + TStringCacheBase = TCacheBase<Integer, AnsiString>; + TStringActionCache = TActionCache<Integer, AnsiString>; + TObjectActionCache = TActionCache<Integer, TObject>; + + TTestStringCache = class(TStringCacheBase) + public + FetchCount : Integer; + FetchValue : AnsiString; + FetchCompletedOn : TDateTime; + RaiseOnFetch : boolean; + NegativeSize : boolean; + Stale : boolean; + RemoveDuringStaleness : boolean; + protected + function EstimateSize(const AValue: AnsiString) : SizeInt; override; + function Fetch(const AKey: Integer) : AnsiString; override; + function CheckStaleness(const AKey: Integer; AItem: TCachedItem<AnsiString>) : boolean; override; + end; + + TCacheTests = class(TTestCase) + private + FCache : TTestStringCache; + FFetchingCount : Integer; + FFetchedCount : Integer; + FRemovingCount : Integer; + FRemovedCount : Integer; + FCallbackMode : Integer; + FNotificationOn : TDateTime; + FObjectFetchCount : Integer; + FCreateObjects : boolean; + FObjectSize : SizeInt; + function NewCache(ACapacity: SizeInt = 100; AReapPolicy: TCacheReapPolicy = TCacheReapPolicy.crpLeastUsed; AExpirationPolicy: TCacheExpirationPolicy = TCacheExpirationPolicy.epNone; AStalePolicy: TCacheStaleValuePolicy = TCacheStaleValuePolicy.svpAssumeNeverStale) : TTestStringCache; + procedure Fetching(Sender: TObject; const AKey: Integer); + procedure Fetched(Sender: TObject; const AKey: Integer; const AValue: AnsiString); + procedure Removing(Sender: TObject; const AKey: Integer; AItem: TCachedItem<AnsiString>); + procedure Removed(Sender: TObject; const AKey: Integer; const AValue: AnsiString); + function FetchObject(const AKey: Integer) : TObject; + function EstimateObject(const AValue: TObject) : SizeInt; + procedure ObjectFetched(Sender: TObject; const AKey: Integer; const AValue: TObject); + function FetchString(const AKey: Integer) : AnsiString; + function EstimateString(const AValue: AnsiString) : SizeInt; + protected + procedure TearDown; override; + published + procedure CacheHitFetchesOnce; + procedure InvalidationRefetches; + procedure ExpirationSinceFetch; + procedure ExpirationSinceAccess; + procedure EmptyStringIsAValue; + procedure ZeroIntegerIsAValue; + procedure NullIsCachedNormally; + procedure NullReturnDoesNotCache; + procedure NullThrowDoesNotAdmit; + procedure NullNotificationRetainsCallbackValue; + procedure LeastUsedEviction; + procedure LargestEviction; + procedure SmallestEviction; + procedure OldestEviction; + procedure IdleEviction; + procedure ASAPEvictsEnough; + procedure ExpiredEntriesAreEvictedFirst; + procedure NoneOnlyReapsInvalidatedEntries; + procedure PinsBlockReaping; + procedure ExplicitRemovalIgnoresPins; + procedure OversizedValuePreservesCache; + procedure NegativeSizePreservesCache; + procedure ReplacementCreditsExistingSize; + procedure SetPreservesFetchTimestamp; + procedure BulkLoadDoesNotFetch; + procedure BulkLoadPreservesEarlierWritesOnFailure; + procedure BulkLoadRespectsPinsAndCapacity; + procedure BulkLoadRejectedOwnedValueRemainsCallerOwned; + procedure BulkLoadHonorsNullPolicies; + procedure RemoveMissingIsHarmless; + procedure FlushClearsMembershipAndSize; + procedure ReapReturnsReleasedSize; + procedure ZeroSizeFitsZeroCapacity; + procedure InvalidConstructionRejected; + procedure StalenessRefetchesOnDemand; + procedure StalenessHookCanRemoveCurrentEntry; + procedure StaleRemovedEntryRefetchesSafely; + procedure FetchEventsFollowAdmission; + procedure FetchFailureDoesNotAdmit; + procedure AccessTimestampFollowsNotification; + procedure RemovingFailureCanRetry; + procedure RemovedFailureLeavesRemovalCommitted; + procedure RecursiveRemovalSkipsPendingEntry; + procedure ReplacementDuringRemovalRejected; + procedure AdmissionCannotSpendPendingCapacity; + procedure PurgeRetainsCallbackAddedEntries; + procedure ObjectDisposalOccursOnce; + procedure FetchedObjectRemovalDisposesImmediately; + procedure RejectedFetchDisposesObject; + procedure RejectedSetRetainsCallerOwnership; + procedure ConcurrentMissFetchesOnce; + procedure ConcurrentReadsAndRemovals; + procedure GlobalAdapters; + procedure ObjectAdapters; + procedure NestedAdapters; + end; + + TBulkCacheTests = class(TTestCase) + published + procedure InitialGetLoadsCompleteSnapshot; + procedure InitialMissingKeyFetchesOnce; + procedure ContainsLoadsOnlyOnce; + procedure AllValuesReturnsCopiedSnapshot; + procedure FetchOnceIgnoresInvalidation; + procedure FetchOnceIgnoresExpiration; + procedure FetchOnceIgnoresStaleness; + procedure FetchOnceMissingDoesNotReload; + procedure PurgeStartsNewSnapshot; + procedure NullPoliciesKeepBorrowedOwnership; + procedure InvalidatedSnapshotReloads; + procedure ExpiredSnapshotReloads; + procedure StaleSnapshotReloads; + procedure ProviderFailurePurgesAndCanRetry; + procedure PreloadedBorrowsOriginalDictionary; + procedure MissingKeyThrowFetchesOnce; + procedure RemoveIsUnsupportedThroughBaseReference; + procedure GlobalObjectNestedAndInterfaceAdapters; + procedure NilProvidersRejected; + procedure ConcurrentReadsShareInitialBatch; + procedure CallbackReentryIsBounded; + procedure ConfiguredComparer; + procedure InitiallyStaleFetchesOnce; + procedure ReapRetainsInvalidatedFetchOnceEntry; + procedure ReapRetainsExpiredFetchOnceEntry; + procedure ReapPreservesRefreshableSnapshot; + procedure ReapKeepsArgumentAndDestructionGuards; + procedure UnitScopedPoliciesAreSharedAcrossCacheKinds; + procedure UnitScopedCallbacksAreSharedAcrossCacheKinds; + end; + +implementation + +var + DisposedObjectCount : Integer; + +type + TTrackedObject = class(TObject) + destructor Destroy; override; + end; + + TCacheReadThread = class(TThread) + private + FCache : TTestStringCache; + FMutate : boolean; + protected + procedure Execute; override; + public + ErrorMessage : AnsiString; + constructor Create(ACache: TTestStringCache; AMutate: boolean = false); + end; + +constructor TCacheReadThread.Create(ACache: TTestStringCache; AMutate: boolean); +begin + inherited Create(true); + FCache := ACache; + FMutate := AMutate; +end; + +procedure TCacheReadThread.Execute; +var + i : Integer; +begin + try + for i := 1 to 1000 do + if FMutate then + FCache.Remove(1) + else if FCache[1] <> 'first' then + raise EInvalidOperation.Create('Concurrent read returned an incorrect value'); + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; +end; + +destructor TTrackedObject.Destroy; +begin + Inc(DisposedObjectCount); + inherited; +end; + +function GlobalFetch(const AKey: Integer) : AnsiString; +begin + Result := IntToStr(AKey); +end; + +function GlobalEstimate(const AValue: AnsiString) : SizeInt; +begin + Result := Length(AValue); +end; + +function IntegerFetch(const AKey: Integer) : Integer; +begin + Result := AKey; +end; + +function TTestStringCache.EstimateSize(const AValue: AnsiString) : SizeInt; +begin + if NegativeSize then Result := -1 else Result := Length(AValue); +end; + +function TTestStringCache.Fetch(const AKey: Integer) : AnsiString; +begin + Inc(FetchCount); + if RaiseOnFetch then + raise EInvalidOperation.Create('fetch failed'); + FetchCompletedOn := Now; + Result := FetchValue; +end; + +function TTestStringCache.CheckStaleness(const AKey: Integer; AItem: TCachedItem<AnsiString>) : boolean; +begin + if RemoveDuringStaleness then + Remove(AKey); + Result := Stale; +end; + +function TCacheTests.NewCache(ACapacity: SizeInt; AReapPolicy: TCacheReapPolicy; AExpirationPolicy: TCacheExpirationPolicy; AStalePolicy: TCacheStaleValuePolicy) : TTestStringCache; +begin + FreeAndNil(FCache); + FCache := TTestStringCache.Create(nil, idpNone, AReapPolicy, AExpirationPolicy, TCacheNullValuePolicy.nvpCacheNormally, ACapacity, TTimeSpan.FromSeconds(1), 0.1, AStalePolicy); + FCache.FetchValue := 'first'; + Result := FCache; +end; + +procedure TCacheTests.TearDown; +begin + FreeAndNil(FCache); + inherited; +end; + +procedure TCacheTests.Fetching(Sender: TObject; const AKey: Integer); +begin + Inc(FFetchingCount); + AssertFalse(FCache.ContainsCachedItem(AKey)); +end; + +procedure TCacheTests.Fetched(Sender: TObject; const AKey: Integer; const AValue: AnsiString); +begin + Inc(FFetchedCount); + AssertTrue('Fetched notification follows storage', FCache.ContainsCachedItem(AKey)); + FNotificationOn := Now; +end; + +procedure TCacheTests.Removing(Sender: TObject; const AKey: Integer; AItem: TCachedItem<AnsiString>); +begin + Inc(FRemovingCount); + AssertTrue('Removal preparation retains membership', FCache.ContainsCachedItem(AKey)); + AssertTrue(FCache.CurrentSize >= AItem.Size); + case FCallbackMode of + 1: raise EInvalidOperation.Create('removal preparation failed'); + 2: begin + FCache.Remove(AKey); + FCache.Flush; + FCache.Reap(100); + end; + 3: FCache[AKey] := 'replacement'; + 4: FCache[99] := 'new'; + end; +end; + +procedure TCacheTests.Removed(Sender: TObject; const AKey: Integer; const AValue: AnsiString); +begin + Inc(FRemovedCount); + AssertFalse('Removed notification follows membership removal', FCache.ContainsCachedItem(AKey)); + if FCallbackMode = 5 then + raise EInvalidOperation.Create('removal notification failed'); + if FCallbackMode = 6 then + FCache[99] := 'new'; +end; + +function TCacheTests.FetchObject(const AKey: Integer) : TObject; +begin + Inc(FObjectFetchCount); + if FCreateObjects then Result := TTrackedObject.Create else Result := nil; +end; + +function TCacheTests.EstimateObject(const AValue: TObject) : SizeInt; +begin + Result := FObjectSize; +end; + +procedure TCacheTests.ObjectFetched(Sender: TObject; const AKey: Integer; const AValue: TObject); +begin + if not Assigned(AValue) then + TObjectActionCache(Sender)[AKey] := Self; +end; + +function TCacheTests.FetchString(const AKey: Integer) : AnsiString; +begin + Result := IntToStr(AKey); +end; + +function TCacheTests.EstimateString(const AValue: AnsiString) : SizeInt; +begin + Result := Length(AValue); +end; + +procedure TCacheTests.CacheHitFetchesOnce; +begin + NewCache; + AssertEquals('first', FCache[1]); + FCache.FetchValue := 'second'; + AssertEquals('first', FCache[1]); + AssertEquals(1, FCache.FetchCount); + AssertEquals(1, FCache.ItemCount); + AssertEquals(2, Integer(FCache.Get(1).AccessedCount) - 1); + AssertEquals(3, Integer(FCache.TotalAccesses)); +end; + +procedure TCacheTests.InvalidationRefetches; +begin + NewCache; + FCache.Get(1); + FCache.Invalidate(1); + FCache.Invalidate(999); + AssertFalse(FCache.ContainsCachedItem(1)); + AssertEquals(1, FCache.ItemCount); + FCache.FetchValue := 'second'; + AssertEquals('second', FCache[1]); + AssertEquals(2, FCache.FetchCount); + AssertEquals(6, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.ExpirationSinceFetch; +begin + NewCache(100, TCacheReapPolicy.crpLeastUsed, TCacheExpirationPolicy.epSinceFetchedTime); + FCache.Get(1).FFetchedOn := Now - 1; + AssertFalse(FCache.ContainsCachedItem(1)); + FCache.FetchValue := 'second'; + AssertEquals('second', FCache[1]); + AssertEquals(2, FCache.FetchCount); +end; + +procedure TCacheTests.ExpirationSinceAccess; +begin + NewCache(100, TCacheReapPolicy.crpLeastUsed, TCacheExpirationPolicy.epSinceLastAccessedTime); + FCache.Get(1).FFetchedOn := Now - 1; + AssertTrue(FCache.ContainsCachedItem(1)); + FCache.Get(1).FLastAccessedOn := Now - 1; + AssertFalse(FCache.ContainsCachedItem(1)); + AssertEquals('first', FCache[1]); + AssertEquals(2, FCache.FetchCount); +end; + +procedure TCacheTests.EmptyStringIsAValue; +begin + NewCache; + FCache.FetchValue := ''; + AssertEquals('', FCache[1]); + AssertEquals('', FCache[1]); + AssertEquals(1, FCache.FetchCount); +end; + +procedure TCacheTests.ZeroIntegerIsAValue; +type + TIntegerCache = TActionCache<Integer, Integer>; +var + cache : TIntegerCache; +begin + cache := TIntegerCache.Create(nil, TCacheValueFetcher<Integer,Integer>.FromGlobalFunction(IntegerFetch), nil, idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpThrow, 0, TTimeSpan.MaxValue); + try + AssertEquals(0, cache[0]); + AssertEquals(1, cache.ItemCount); + finally + cache.Free; + end; +end; + +procedure TCacheTests.NullIsCachedNormally; +var + cache : TObjectActionCache; +begin + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), nil, idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 10, TTimeSpan.MaxValue); + try + AssertNull(cache[1]); + AssertNull(cache[1]); + AssertEquals(1, FObjectFetchCount); + AssertEquals(1, cache.ItemCount); + finally + cache.Free; + end; +end; + +procedure TCacheTests.NullReturnDoesNotCache; +var + cache : TObjectActionCache; +begin + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), nil, idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpReturnButDontCache, 10, TTimeSpan.MaxValue); + try + AssertNull(cache[1]); + AssertNull(cache[1]); + AssertEquals(2, FObjectFetchCount); + AssertEquals(0, cache.ItemCount); + AssertEquals(0, Integer(cache.CurrentSize)); + finally + cache.Free; + end; +end; + +procedure TCacheTests.NullThrowDoesNotAdmit; +var + cache : TObjectActionCache; +begin + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), nil, idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpThrow, 10, TTimeSpan.MaxValue); + try + try + cache.Get(1); + Fail('Null fetch must be rejected'); + except + on E: EInvalidOperation do ; + end; + AssertEquals(0, cache.ItemCount); + finally + cache.Free; + end; +end; + +procedure TCacheTests.NullNotificationRetainsCallbackValue; +var + cache : TObjectActionCache; +begin + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), nil, idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpReturnButDontCache, 10, TTimeSpan.MaxValue); + try + cache.ItemFetched := ObjectFetched; + AssertNull(cache[1]); + AssertTrue(cache.ContainsCachedItem(1)); + AssertTrue(cache[1] = Self); + AssertEquals(1, FObjectFetchCount); + finally + cache.Free; + end; +end; + +procedure TCacheTests.LeastUsedEviction; +begin + NewCache(3); + FCache[1] := 'a'; + FCache[2] := 'b'; + FCache[3] := 'c'; + FCache.Get(1); + FCache.Get(3); + FCache[4] := 'd'; + AssertFalse(FCache.ContainsCachedItem(2)); + AssertTrue(FCache.ContainsCachedItem(1)); + AssertTrue(FCache.ContainsCachedItem(3)); + AssertEquals(3, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.LargestEviction; +begin + NewCache(6, TCacheReapPolicy.crpLargest); + FCache[1] := 'a'; + FCache[2] := 'bb'; + FCache[3] := 'ccc'; + AssertEquals(3, Integer(FCache.Reap(1))); + AssertFalse(FCache.ContainsCachedItem(3)); +end; + +procedure TCacheTests.SmallestEviction; +begin + NewCache(6, TCacheReapPolicy.crpSmallest); + FCache[1] := 'a'; + FCache[2] := 'bb'; + FCache[3] := 'ccc'; + AssertEquals(1, Integer(FCache.Reap(1))); + AssertFalse(FCache.ContainsCachedItem(1)); +end; + +procedure TCacheTests.OldestEviction; +begin + NewCache(3, TCacheReapPolicy.crpOldest); + FCache[1] := 'a'; + FCache[2] := 'b'; + FCache[3] := 'c'; + FCache.Get(1); + FCache[4] := 'd'; + AssertFalse(FCache.ContainsCachedItem(1)); +end; + +procedure TCacheTests.IdleEviction; +begin + NewCache(3, TCacheReapPolicy.crpIdle); + FCache[1] := 'a'; + FCache[2] := 'b'; + FCache[3] := 'c'; + FCache.Get(1); + FCache[4] := 'd'; + AssertFalse(FCache.ContainsCachedItem(2)); +end; + +procedure TCacheTests.ASAPEVIctsEnough; +begin + NewCache(5, TCacheReapPolicy.crpASAP); + FCache[1] := 'a'; + FCache[2] := 'b'; + FCache[3] := 'c'; + FCache[4] := 'd'; + FCache[5] := 'e'; + FCache[6] := 'long'; + AssertEquals(5, Integer(FCache.CurrentSize)); + AssertEquals(2, FCache.ItemCount); + AssertTrue(FCache.ContainsCachedItem(6)); +end; + +procedure TCacheTests.ExpiredEntriesAreEvictedFirst; +begin + NewCache(3, TCacheReapPolicy.crpOldest); + FCache[1] := 'a'; + FCache[2] := 'b'; + FCache[3] := 'c'; + FCache.Invalidate(3); + FCache[4] := 'd'; + AssertTrue(FCache.ContainsCachedItem(1)); + AssertEquals(3, FCache.ItemCount); + AssertFalse(FCache.CachedItems[0].Invalidated); +end; + +procedure TCacheTests.NoneOnlyReapsInvalidatedEntries; +begin + NewCache(2, TCacheReapPolicy.crpNone); + FCache[1] := 'a'; + FCache[2] := 'b'; + AssertEquals(0, Integer(FCache.Reap(2))); + FCache.Invalidate(1); + AssertEquals(1, Integer(FCache.Reap(2))); + AssertTrue(FCache.ContainsCachedItem(2)); +end; + +procedure TCacheTests.PinsBlockReaping; +begin + NewCache(1); + FCache[1] := 'a'; + FCache.Get(1).CanPurge := false; + AssertEquals(0, Integer(FCache.Reap(1))); + try + FCache[2] := 'b'; + Fail('A pinned cache cannot admit another value'); + except + on E: EInvalidOperation do ; + end; + AssertTrue(FCache.ContainsCachedItem(1)); + AssertEquals(1, FCache.ItemCount); +end; + +procedure TCacheTests.ExplicitRemovalIgnoresPins; +begin + NewCache(2); + FCache[1] := 'a'; + FCache[2] := 'b'; + FCache.Get(1).CanPurge := false; + FCache.Get(2).CanPurge := false; + FCache.Remove(1); + FCache.Flush; + AssertEquals(0, FCache.ItemCount); +end; + +procedure TCacheTests.OversizedValuePreservesCache; +begin + NewCache(2); + FCache[1] := 'a'; + try + FCache[2] := 'big'; + Fail('Oversized values must be rejected'); + except + on E: EInvalidOperation do ; + end; + AssertEquals('a', FCache[1]); + AssertEquals(1, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.NegativeSizePreservesCache; +begin + NewCache; + FCache[1] := 'a'; + FCache.NegativeSize := true; + try + FCache[1] := 'replacement'; + Fail('Negative estimates must be rejected'); + except + on E: EInvalidOperation do ; + end; + AssertEquals('a', FCache[1]); + AssertEquals(1, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.ReplacementCreditsExistingSize; +begin + NewCache(3, TCacheReapPolicy.crpNone); + FCache[1] := 'aa'; + FCache[2] := 'b'; + FCache[1] := 'cc'; + AssertEquals(2, FCache.ItemCount); + AssertEquals(3, Integer(FCache.CurrentSize)); + AssertEquals('cc', FCache[1]); +end; + +procedure TCacheTests.SetPreservesFetchTimestamp; +var + item : TCachedItem<AnsiString>; + timestamp : TDateTime; +begin + NewCache; + item := FCache.Get(1); + timestamp := Now - 1; + item.FFetchedOn := timestamp; + FCache[1] := 'replacement'; + AssertTrue(item = FCache.Get(1)); + AssertTrue(timestamp = item.FetchedOn); + AssertEquals(1, FCache.FetchCount); +end; + +procedure TCacheTests.BulkLoadDoesNotFetch; +var + values : TStringPairList; +begin + NewCache; + FCache.ItemFetched := Fetched; + values := TStringPairList.Create; + try + values.Add(TStringPair.Create(1, 'a')); + values.Add(TStringPair.Create(2, 'bb')); + values.Add(TStringPair.Create(1, 'ccc')); + FCache.BulkLoad(values); + AssertEquals('ccc', FCache[1]); + AssertEquals(0, FCache.FetchCount); + AssertEquals(0, FFetchedCount); + AssertEquals(2, FCache.ItemCount); + AssertEquals(5, Integer(FCache.CurrentSize)); + finally + values.Free; + end; +end; + +procedure TCacheTests.BulkLoadPreservesEarlierWritesOnFailure; +var + values : TStringPairList; +begin + NewCache(3, TCacheReapPolicy.crpOldest); + values := TStringPairList.Create; + try + values.Add(TStringPair.Create(1, 'a')); + values.Add(TStringPair.Create(2, 'oversized')); + values.Add(TStringPair.Create(3, 'b')); + try + FCache.BulkLoad(values); + Fail('An oversized batch value must be rejected'); + except + on E: EInvalidOperation do ; + end; + AssertEquals('a', FCache[1]); + AssertFalse(FCache.ContainsCachedItem(2)); + AssertFalse(FCache.ContainsCachedItem(3)); + AssertEquals(1, FCache.ItemCount); + AssertEquals(1, Integer(FCache.CurrentSize)); + AssertEquals(0, FCache.FetchCount); + finally values.Free; end; +end; + +procedure TCacheTests.BulkLoadRespectsPinsAndCapacity; +var + values : TStringPairList; +begin + NewCache(6, TCacheReapPolicy.crpOldest); + FCache[1] := 'pin'; + FCache.Get(1).CanPurge := false; + values := TStringPairList.Create; + try + values.Add(TStringPair.Create(2, 'two')); + values.Add(TStringPair.Create(3, 'new')); + FCache.BulkLoad(values); + AssertEquals('pin', FCache[1]); + AssertFalse(FCache.ContainsCachedItem(2)); + AssertEquals('new', FCache[3]); + AssertEquals(2, FCache.ItemCount); + AssertEquals(6, Integer(FCache.CurrentSize)); + AssertEquals(0, FCache.FetchCount); + finally values.Free; end; +end; + +procedure TCacheTests.BulkLoadRejectedOwnedValueRemainsCallerOwned; +type + TObjectPair = TPair<Integer, TObject>; +var + cache : TObjectActionCache; + values : TList<TObjectPair>; + first, second : TObject; +begin + DisposedObjectCount := 0; + FObjectSize := 1; + cache := TObjectActionCache.Create(nil, + TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), + TCacheSizeEstimator<TObject>.FromObjectFunction(EstimateObject), + idpFreeAndNil, TCacheReapPolicy.crpNone, + TCacheExpirationPolicy.epNone, + TCacheNullValuePolicy.nvpCacheNormally, 1, TTimeSpan.MaxValue); + values := TList<TObjectPair>.Create; + first := TTrackedObject.Create; + second := TTrackedObject.Create; + try + values.Add(TObjectPair.Create(1, first)); + values.Add(TObjectPair.Create(2, second)); + try + cache.BulkLoad(values); + Fail('A full non-reaping cache must reject the second batch value'); + except + on E: EInvalidOperation do ; + end; + AssertEquals(0, DisposedObjectCount); + AssertTrue(first = cache[1]); + AssertFalse(cache.ContainsCachedItem(2)); + second.Free; + second := nil; + AssertEquals(1, DisposedObjectCount); + finally + // Only admitted values transfer ownership; the list itself owns no values. + if Assigned(second) and (not cache.ContainsCachedItem(2)) then second.Free; + cache.Free; + values.Free; + end; + AssertEquals(2, DisposedObjectCount); +end; + +procedure TCacheTests.BulkLoadHonorsNullPolicies; +type + TObjectPair = TPair<Integer, TObject>; +var + cache : TObjectActionCache; + values : TList<TObjectPair>; + value : TObject; + policy : TCacheNullValuePolicy; +begin + FObjectFetchCount := 0; + values := TList<TObjectPair>.Create; + value := TObject.Create; + try + values.Add(TObjectPair.Create(1, nil)); + values.Add(TObjectPair.Create(2, value)); + for policy := Low(TCacheNullValuePolicy) to High(TCacheNullValuePolicy) do begin + cache := TObjectActionCache.Create(nil, + TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), nil, + idpNone, TCacheReapPolicy.crpNone, + TCacheExpirationPolicy.epNone, policy, 1, TTimeSpan.MaxValue); + try + // Prefetched nulls obey the configured policy when read, just as SetValue does. + cache.BulkLoad(values); + AssertEquals(2, cache.ItemCount); + if policy = TCacheNullValuePolicy.nvpThrow then begin + try + cache.Get(1); + Fail('A prohibited prefetched null must throw on read'); + except on E: EInvalidOperation do ; end; + AssertEquals(2, cache.ItemCount); + end else begin + AssertNull(cache[1]); + AssertEquals(policy = TCacheNullValuePolicy.nvpCacheNormally, + cache.ContainsCachedItem(1)); + end; + AssertTrue(value = cache[2]); + finally cache.Free; end; + end; + AssertEquals(0, FObjectFetchCount); + finally value.Free; values.Free; end; +end; + +procedure TCacheTests.RemoveMissingIsHarmless; +begin + NewCache; + FCache.Remove(1); + AssertEquals(0, FCache.ItemCount); +end; + +procedure TCacheTests.FlushClearsMembershipAndSize; +begin + NewCache; + FCache[1] := 'a'; + FCache[2] := 'bb'; + FCache.Flush; + AssertEquals(0, FCache.ItemCount); + AssertEquals(0, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.ReapReturnsReleasedSize; +begin + NewCache; + FCache[1] := 'aa'; + FCache[2] := 'bb'; + AssertEquals(2, Integer(FCache.Reap(1))); + AssertEquals(2, Integer(FCache.CurrentSize)); + AssertEquals(0, Integer(FCache.Reap(0))); +end; + +procedure TCacheTests.ZeroSizeFitsZeroCapacity; +begin + NewCache(0); + FCache[1] := ''; + FCache[2] := ''; + AssertEquals(2, FCache.ItemCount); + AssertEquals(0, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.InvalidConstructionRejected; +var + cache : TTestStringCache; +begin + cache := nil; + try + try + cache := TTestStringCache.Create(nil, idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, -1, TTimeSpan.MaxValue); + Fail('Negative capacity must be rejected'); + except + on E: EArgumentException do ; + end; + try + cache := TTestStringCache.Create(nil, idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 1, TTimeSpan.MaxValue, 1); + Fail('Invalid reap level must be rejected'); + except + on E: EArgumentException do ; + end; + finally + cache.Free; + end; +end; + +procedure TCacheTests.StalenessRefetchesOnDemand; +begin + NewCache(100, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheStaleValuePolicy.svpCheckStaleOnDemand); + FCache.Get(1); + FCache.Stale := true; + FCache.FetchValue := 'second'; + AssertEquals('second', FCache[1]); + AssertEquals(2, FCache.FetchCount); +end; + +procedure TCacheTests.StalenessHookCanRemoveCurrentEntry; +var + item : TCachedItem<AnsiString>; +begin + NewCache(100, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheStaleValuePolicy.svpCheckStaleOnDemand); + FCache[1] := 'original'; + FCache.RemoveDuringStaleness := true; + item := FCache.Get(1); + AssertEquals('original', item.Value); + AssertTrue(item.Purged); + AssertEquals(0, FCache.ItemCount); + AssertEquals(0, FCache.FetchCount); + AssertEquals(0, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.StaleRemovedEntryRefetchesSafely; +var + item : TCachedItem<AnsiString>; +begin + NewCache(100, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheStaleValuePolicy.svpCheckStaleOnDemand); + FCache[1] := 'original'; + FCache.RemoveDuringStaleness := true; + FCache.Stale := true; + item := FCache.Get(1); + AssertEquals('first', item.Value); + AssertFalse(item.Purged); + AssertTrue(FCache.ContainsCachedItem(1)); + AssertEquals(1, FCache.FetchCount); + AssertEquals(5, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.FetchEventsFollowAdmission; +begin + NewCache; + FCache.ItemFetching := Fetching; + FCache.ItemFetched := Fetched; + FCache.Get(1); + FCache.Get(1); + AssertEquals(1, FFetchingCount); + AssertEquals(1, FFetchedCount); + AssertTrue(FCache.Get(1).FetchedOn >= FCache.FetchCompletedOn); +end; + +procedure TCacheTests.FetchFailureDoesNotAdmit; +begin + NewCache; + FCache.RaiseOnFetch := true; + FCache.ItemFetching := Fetching; + FCache.ItemFetched := Fetched; + try + FCache.Get(1); + Fail('Fetch failure must propagate'); + except + on E: EInvalidOperation do ; + end; + AssertEquals(1, FFetchingCount); + AssertEquals(0, FFetchedCount); + AssertEquals(0, FCache.ItemCount); +end; + +procedure TCacheTests.AccessTimestampFollowsNotification; +var + item : TCachedItem<AnsiString>; +begin + NewCache; + FCache.ItemFetched := Fetched; + item := FCache.Get(1); + AssertTrue(item.LastAccessedOn >= FNotificationOn); + AssertTrue(FCache.LastAccessedOn >= FNotificationOn); +end; + +procedure TCacheTests.RemovingFailureCanRetry; +begin + NewCache; + FCache[1] := 'a'; + FCache.ItemRemoving := Removing; + FCache.ItemRemoved := Removed; + FCallbackMode := 1; + try + FCache.Remove(1); + Fail('Preparation failure must propagate'); + except + on E: EInvalidOperation do ; + end; + AssertEquals(1, FCache.ItemCount); + AssertEquals(1, Integer(FCache.CurrentSize)); + AssertEquals(0, FRemovedCount); + FCallbackMode := 0; + FCache.Remove(1); + AssertEquals(2, FRemovingCount); + AssertEquals(1, FRemovedCount); + AssertEquals(0, FCache.ItemCount); +end; + +procedure TCacheTests.RemovedFailureLeavesRemovalCommitted; +begin + NewCache; + FCache[1] := 'a'; + FCache.ItemRemoved := Removed; + FCallbackMode := 5; + try + FCache.Remove(1); + Fail('Notification failure must propagate'); + except + on E: EInvalidOperation do ; + end; + AssertEquals(0, FCache.ItemCount); + AssertEquals(0, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.RecursiveRemovalSkipsPendingEntry; +begin + NewCache; + FCache[1] := 'a'; + FCache.ItemRemoving := Removing; + FCache.ItemRemoved := Removed; + FCallbackMode := 2; + FCache.Remove(1); + AssertEquals(1, FRemovingCount); + AssertEquals(1, FRemovedCount); + AssertEquals(0, FCache.ItemCount); +end; + +procedure TCacheTests.ReplacementDuringRemovalRejected; +begin + NewCache; + FCache[1] := 'a'; + FCache.ItemRemoving := Removing; + FCallbackMode := 3; + try + FCache.Remove(1); + Fail('Replacing an entry while preparing removal must fail'); + except + on E: EInvalidOperation do ; + end; + AssertEquals('a', FCache[1]); + AssertEquals(1, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.AdmissionCannotSpendPendingCapacity; +begin + NewCache(3); + FCache[1] := 'aaa'; + FCache.ItemRemoving := Removing; + FCallbackMode := 4; + try + FCache.Remove(1); + Fail('Pending removals cannot fund admissions'); + except + on E: EInvalidOperation do ; + end; + AssertTrue(FCache.ContainsCachedItem(1)); + AssertFalse(FCache.ContainsCachedItem(99)); + AssertEquals(3, Integer(FCache.CurrentSize)); +end; + +procedure TCacheTests.PurgeRetainsCallbackAddedEntries; +begin + NewCache; + FCache[1] := 'a'; + FCache.ItemRemoved := Removed; + FCallbackMode := 6; + FCache.Purge; + AssertEquals(1, FCache.ItemCount); + AssertEquals('new', FCache[99]); +end; + +procedure TCacheTests.ObjectDisposalOccursOnce; +var + cache : TObjectActionCache; + first, second : TObject; +begin + DisposedObjectCount := 0; + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), nil, idpFreeAndNil, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 10, TTimeSpan.MaxValue); + first := TTrackedObject.Create; + second := TTrackedObject.Create; + try + cache[1] := first; + cache[1] := first; + AssertEquals(0, DisposedObjectCount); + cache[1] := second; + AssertEquals(1, DisposedObjectCount); + cache.Remove(1); + AssertEquals(2, DisposedObjectCount); + finally + cache.Free; + end; + AssertEquals(2, DisposedObjectCount); +end; + +procedure TCacheTests.FetchedObjectRemovalDisposesImmediately; +var + cache : TObjectActionCache; +begin + DisposedObjectCount := 0; + FCreateObjects := true; + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), nil, idpFreeAndNil, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 1, TTimeSpan.MaxValue); + try + AssertNotNull(cache[1]); + cache.Remove(1); + AssertEquals(1, DisposedObjectCount); + finally + cache.Free; + end; + AssertEquals(1, DisposedObjectCount); +end; + +procedure TCacheTests.RejectedFetchDisposesObject; +var + cache : TObjectActionCache; +begin + DisposedObjectCount := 0; + FCreateObjects := true; + FObjectSize := 2; + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), TCacheSizeEstimator<TObject>.FromObjectFunction(EstimateObject), idpFreeAndNil, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 1, TTimeSpan.MaxValue); + try + try + cache.Get(1); + Fail('Oversized fetched object must be rejected'); + except + on E: EInvalidOperation do ; + end; + AssertEquals(1, DisposedObjectCount); + AssertEquals(0, cache.ItemCount); + finally + cache.Free; + end; + AssertEquals(1, DisposedObjectCount); +end; + +procedure TCacheTests.RejectedSetRetainsCallerOwnership; +var + cache : TObjectActionCache; + value : TObject; +begin + DisposedObjectCount := 0; + FObjectSize := 2; + value := TTrackedObject.Create; + cache := TObjectActionCache.Create(nil, TCacheValueFetcher<Integer,TObject>.FromObjectFunction(FetchObject), TCacheSizeEstimator<TObject>.FromObjectFunction(EstimateObject), idpFreeAndNil, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 1, TTimeSpan.MaxValue); + try + try + cache[1] := value; + Fail('Oversized explicit object must be rejected'); + except + on E: EInvalidOperation do ; + end; + AssertEquals(0, DisposedObjectCount); + AssertEquals(0, cache.ItemCount); + finally + cache.Free; + value.Free; + end; + AssertEquals(1, DisposedObjectCount); +end; + +procedure TCacheTests.ConcurrentMissFetchesOnce; +var + readers : array[0..3] of TCacheReadThread; + i : Integer; +begin + NewCache; + for i := Low(readers) to High(readers) do + readers[i] := TCacheReadThread.Create(FCache); + try + for i := Low(readers) to High(readers) do + readers[i].Start; + for i := Low(readers) to High(readers) do + readers[i].WaitFor; + for i := Low(readers) to High(readers) do + AssertEquals('', readers[i].ErrorMessage); + AssertEquals(1, FCache.FetchCount); + AssertEquals(4000, Integer(FCache.TotalAccesses)); + finally + for i := Low(readers) to High(readers) do + readers[i].Free; + end; +end; + +procedure TCacheTests.ConcurrentReadsAndRemovals; +var + readers : array[0..3] of TCacheReadThread; + i : Integer; +begin + NewCache; + for i := Low(readers) to High(readers) do + readers[i] := TCacheReadThread.Create(FCache, i = 0); + try + for i := Low(readers) to High(readers) do + readers[i].Start; + for i := Low(readers) to High(readers) do + readers[i].WaitFor; + for i := Low(readers) to High(readers) do + AssertEquals('', readers[i].ErrorMessage); + AssertEquals(3000, Integer(FCache.TotalAccesses)); + finally + for i := Low(readers) to High(readers) do + readers[i].Free; + end; +end; + +procedure TCacheTests.GlobalAdapters; +var + cache : TStringActionCache; +begin + cache := TStringActionCache.Create(nil, TCacheValueFetcher<Integer,AnsiString>.FromGlobalFunction(GlobalFetch), TCacheSizeEstimator<AnsiString>.FromGlobalFunction(GlobalEstimate), idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 10, TTimeSpan.MaxValue); + try + AssertEquals('123', cache[123]); + AssertEquals(3, Integer(cache.CurrentSize)); + finally + cache.Free; + end; +end; + +procedure TCacheTests.ObjectAdapters; +var + cache : TStringActionCache; +begin + cache := TStringActionCache.Create(nil, TCacheValueFetcher<Integer,AnsiString>.FromObjectFunction(FetchString), TCacheSizeEstimator<AnsiString>.FromObjectFunction(EstimateString), idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 10, TTimeSpan.MaxValue); + try + AssertEquals('123', cache[123]); + AssertEquals(3, Integer(cache.CurrentSize)); + finally + cache.Free; + end; +end; + +procedure TCacheTests.NestedAdapters; +var + cache : TStringActionCache; + suffix : AnsiString; + function Fetch(const AKey: Integer) : AnsiString; + begin + Result := IntToStr(AKey) + suffix; + end; + function Estimate(const AValue: AnsiString) : SizeInt; + begin + Result := Length(AValue); + end; +begin + suffix := '!'; + cache := TStringActionCache.Create(nil, TCacheValueFetcher<Integer,AnsiString>.FromNestedFunction(Fetch), TCacheSizeEstimator<AnsiString>.FromNestedFunction(Estimate), idpNone, TCacheReapPolicy.crpNone, TCacheExpirationPolicy.epNone, TCacheNullValuePolicy.nvpCacheNormally, 10, TTimeSpan.MaxValue); + try + AssertEquals('123!', cache[123]); + AssertEquals(4, Integer(cache.CurrentSize)); + finally + cache.Free; + end; +end; + +procedure TBulkCacheTests.InitialGetLoadsCompleteSnapshot; +begin + RunBulkCacheCheck(1); +end; + +procedure TBulkCacheTests.InitialMissingKeyFetchesOnce; +begin + RunBulkCacheCheck(2); +end; + +procedure TBulkCacheTests.ContainsLoadsOnlyOnce; +begin + RunBulkCacheCheck(3); +end; + +procedure TBulkCacheTests.AllValuesReturnsCopiedSnapshot; +begin + RunBulkCacheCheck(4); +end; + +procedure TBulkCacheTests.FetchOnceIgnoresInvalidation; +begin + RunBulkCacheCheck(5); +end; + +procedure TBulkCacheTests.FetchOnceIgnoresExpiration; +begin + RunBulkCacheCheck(6); +end; + +procedure TBulkCacheTests.FetchOnceIgnoresStaleness; +begin + RunBulkCacheCheck(7); +end; + +procedure TBulkCacheTests.FetchOnceMissingDoesNotReload; +begin + RunBulkCacheCheck(8); +end; + +procedure TBulkCacheTests.PurgeStartsNewSnapshot; +begin + RunBulkCacheCheck(9); +end; + +procedure TBulkCacheTests.NullPoliciesKeepBorrowedOwnership; +begin + RunBulkCacheCheck(10); +end; + +procedure TBulkCacheTests.InvalidatedSnapshotReloads; +begin + RunBulkCacheCheck(11); +end; + +procedure TBulkCacheTests.ExpiredSnapshotReloads; +begin + RunBulkCacheCheck(12); +end; + +procedure TBulkCacheTests.StaleSnapshotReloads; +begin + RunBulkCacheCheck(13); +end; + +procedure TBulkCacheTests.ProviderFailurePurgesAndCanRetry; +begin + RunBulkCacheCheck(14); +end; + +procedure TBulkCacheTests.PreloadedBorrowsOriginalDictionary; +begin + RunBulkCacheCheck(15); +end; + +procedure TBulkCacheTests.MissingKeyThrowFetchesOnce; +begin + RunBulkCacheCheck(16); +end; + +procedure TBulkCacheTests.RemoveIsUnsupportedThroughBaseReference; +begin + RunBulkCacheCheck(17); +end; + +procedure TBulkCacheTests.GlobalObjectNestedAndInterfaceAdapters; +begin + RunBulkCacheCheck(18); +end; + +procedure TBulkCacheTests.NilProvidersRejected; +begin + RunBulkCacheCheck(19); +end; + +procedure TBulkCacheTests.ConcurrentReadsShareInitialBatch; +begin + RunBulkCacheCheck(20); +end; + +procedure TBulkCacheTests.CallbackReentryIsBounded; +begin + RunBulkCacheCheck(21); +end; + +procedure TBulkCacheTests.ConfiguredComparer; +begin + RunBulkCacheCheck(22); +end; + +procedure TBulkCacheTests.InitiallyStaleFetchesOnce; +begin + RunBulkCacheCheck(23); +end; + +procedure TBulkCacheTests.ReapRetainsInvalidatedFetchOnceEntry; +begin + RunBulkCacheCheck(24); +end; + +procedure TBulkCacheTests.ReapRetainsExpiredFetchOnceEntry; +begin + RunBulkCacheCheck(25); +end; + +procedure TBulkCacheTests.ReapPreservesRefreshableSnapshot; +begin + RunBulkCacheCheck(26); +end; + +procedure TBulkCacheTests.ReapKeepsArgumentAndDestructionGuards; +begin + RunBulkCacheCheck(27); +end; + +procedure TBulkCacheTests.UnitScopedPoliciesAreSharedAcrossCacheKinds; +begin + RunBulkCacheCheck(28); +end; + +procedure TBulkCacheTests.UnitScopedCallbacksAreSharedAcrossCacheKinds; +begin + RunBulkCacheCheck(29); +end; + +initialization + RegisterTest(TCacheTests); + RegisterTest(TBulkCacheTests); +end. diff --git a/src/tests/experimental/framework/UCommon.Data.Tests.pas b/src/tests/experimental/framework/UCommon.Data.Tests.pas new file mode 100644 index 000000000..f4757f7e0 --- /dev/null +++ b/src/tests/experimental/framework/UCommon.Data.Tests.pas @@ -0,0 +1,568 @@ +unit UCommon.Data.Tests; + +{ Copyright © 2026 Herman Schoenfeld + Distributed under the MIT software license, see the accompanying LICENSE. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, SyncObjs, Variants, fpcunit, testregistry, + Generics.Collections, UCommon.Data; + +type + TDataSourceSnapshotTests = class(TTestCase) + published + procedure PagingReusesFetchedAndSortedSnapshot; + procedure SearchAndSortReuseRawSnapshot; + procedure CriteriaValuesAndOperandArePartOfTheQuery; + procedure InvalidationsCoalesceAndClearEmptyRows; + procedure DefaultSourceStillFetchesEveryTime; + procedure InvalidatingDoesNotWaitForBlockedFetch; + procedure FailedFetchDoesNotCachePartialData; + procedure FilteringDoesNotModifyTheRawSnapshot; + procedure PageSizesAndClampingUseTheCachedCount; + procedure EmptyColumnsAreRejected; + procedure KeyAndColumnValuesRoundTripIndependently; + procedure PositionalCallsWithRuntimeIndexPreserveColumnsAndKey; + procedure PositionalDispatcherRejectsUnknownNamesAndWrongArity; + procedure UnicodeColumnNamesBindingsAndValuesRoundTrip; + end; + +implementation + +uses UMemory; + +type + TUnicodeBindingSource = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + function GetItemField(constref AItem: Integer; const ABindingName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + end; + + TCountingSnapshotSource = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + function GetItemField(constref AItem: Integer; const ABindingName: String): Variant; override; + function ApplyColumnSort(constref ALeft, ARight: Integer; constref AFilter: TColumnFilter): Integer; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + public + ItemCount, FetchCount, FieldCalls, SortCalls: Integer; + BlockFetch, FailNext: Boolean; + Entered, ReleaseFetch: TEvent; + constructor Create; + destructor Destroy; override; + function Fetch(AIndex, ASize: Integer; const AFilter: TFilterCriteria; AOperand: TFilterOperand; var ATable: TDataTable): TPageFetchResult; + end; + + TSnapshotTestThread = class(TThread) + private + FSource: TCountingSnapshotSource; + FInvalidate: Boolean; + protected + procedure Execute; override; + public + Completed: TEvent; + ErrorMessage: String; + constructor Create(ASource: TCountingSnapshotSource; AInvalidate: Boolean); + destructor Destroy; override; + end; + +function UnicodeBinding(ASecond: Boolean): String; +var LText: UnicodeString; +begin + if ASecond then LText := #$65E5#$672C#$D83D#$DE80 + else LText := #$4E2D#$6587#$D83D#$DE80; + Result := UTF8Encode(LText); +end; + +function UnicodePayload: UnicodeString; +begin + Result := #$03A9#$0416#$6F22#$D83D#$DE80; +end; + +function TUnicodeBindingSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From(UnicodeBinding(False), True), + TDataColumn.From(UnicodeBinding(True))); +end; + +function TUnicodeBindingSource.GetItemField(constref AItem: Integer; const ABindingName: String): Variant; +begin + if ABindingName = UnicodeBinding(False) then Result := AItem + else if ABindingName = UnicodeBinding(True) then + Result := UnicodePayload + UnicodeString(IntToStr(AItem)) + else raise Exception.Create('Unicode binding was changed before field lookup.'); +end; + +procedure TUnicodeBindingSource.FetchAll(const AContainer: TList<Integer>); +begin + AContainer.Add(2); + AContainer.Add(1); +end; + +constructor TCountingSnapshotSource.Create; +begin + inherited Create(nil); + ItemCount := 100; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseFetch := TEvent.Create(nil, True, False, ''); +end; + +destructor TCountingSnapshotSource.Destroy; +begin + Entered.Free; + ReleaseFetch.Free; + inherited Destroy; +end; + +function TCountingSnapshotSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Value', True)); +end; + +function TCountingSnapshotSource.GetItemField(constref AItem: Integer; const ABindingName: String): Variant; +begin + Inc(FieldCalls); + Result := AItem; +end; + +function TCountingSnapshotSource.ApplyColumnSort(constref ALeft, ARight: Integer; constref AFilter: TColumnFilter): Integer; +begin + Inc(SortCalls); + Result := inherited ApplyColumnSort(ALeft, ARight, AFilter); +end; + +procedure TCountingSnapshotSource.FetchAll(const AContainer: TList<Integer>); +var i, LCount: Integer; +begin + Inc(FetchCount); + LCount := ItemCount; + if BlockFetch then begin + Entered.SetEvent; + if ReleaseFetch.WaitFor(10000) <> wrSignaled then + raise Exception.Create('Snapshot fetch gate timed out'); + end; + for i := LCount - 1 downto 0 do begin + AContainer.Add(i); + if FailNext then begin + FailNext := False; + raise Exception.Create('Expected snapshot fetch failure'); + end; + end; +end; + +function TCountingSnapshotSource.Fetch(AIndex, ASize: Integer; const AFilter: TFilterCriteria; AOperand: TFilterOperand; var ATable: TDataTable): TPageFetchResult; +begin + Result := FetchPage(TPageFetchParams.Create(AIndex, ASize, AFilter, AOperand), ATable); +end; + +constructor TSnapshotTestThread.Create(ASource: TCountingSnapshotSource; AInvalidate: Boolean); +begin + inherited Create(True); + FreeOnTerminate := False; + FSource := ASource; + FInvalidate := AInvalidate; + Completed := TEvent.Create(nil, True, False, ''); + Start; +end; + +destructor TSnapshotTestThread.Destroy; +begin + Completed.Free; + inherited Destroy; +end; + +procedure TSnapshotTestThread.Execute; +var i: Integer; LTable: TDataTable; +begin + try + if FInvalidate then + for i := 1 to 1000 do FSource.InvalidateSnapshot + else + FSource.Fetch(0, 10, nil, foAnd, LTable); + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + Completed.SetEvent; +end; + +function SortByValue(ADirection: TSortDirection): TFilterCriteria; +begin + SetLength(Result, 1); + Result[0] := Default(TColumnFilter); + Result[0].ColumnName := 'Value'; + Result[0].Filter := vgfSortable; + Result[0].Sort := ADirection; +end; + +function AtLeast(AValue: Integer): TFilterCriteria; +begin + SetLength(Result, 1); + Result[0] := Default(TColumnFilter); + Result[0].ColumnName := 'Value'; + Result[0].Filter := vgfNumericGTE; + SetLength(Result[0].Values, 1); + Result[0].Values[0] := AValue; +end; + +procedure TDataSourceSnapshotTests.PagingReusesFetchedAndSortedSnapshot; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LResult: TPageFetchResult; + LFilters: TFilterCriteria; + LFieldCalls, LSortCalls: Integer; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + LSource.ItemCount := 50000; + LSource.SnapshotCacheEnabled := True; + LFilters := SortByValue(sdAscending); + LResult := LSource.Fetch(0, 25, LFilters, foAnd, LTable); + AssertEquals(50000, LResult.TotalDataCount); + AssertEquals(0, Integer(LTable.Rows[0].Value)); + LFieldCalls := LSource.FieldCalls; + LSortCalls := LSource.SortCalls; + AssertTrue(LSortCalls > 0); + LResult := LSource.Fetch(1, 25, LFilters, foAnd, LTable); + AssertEquals(25, Integer(LTable.Rows[0].Value)); + AssertEquals(1, LSource.FetchCount); + AssertEquals(LSortCalls, LSource.SortCalls); + AssertEquals('Only the requested page should be converted', 25, LSource.FieldCalls - LFieldCalls); + AssertEquals(2000, LResult.PageCount); +end; + +procedure TDataSourceSnapshotTests.SearchAndSortReuseRawSnapshot; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LFilters: TFilterCriteria; + LResult: TPageFetchResult; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + LSource.SnapshotCacheEnabled := True; + LFilters := AtLeast(50); + LFilters[0].Sort := sdDescending; + LResult := LSource.Fetch(0, 10, LFilters, foAnd, LTable); + AssertEquals(50, LResult.TotalDataCount); + AssertEquals(99, Integer(LTable.Rows[0].Value)); + LFilters[0].Sort := sdAscending; + LSource.Fetch(0, 10, LFilters, foAnd, LTable); + AssertEquals(50, Integer(LTable.Rows[0].Value)); + AssertEquals(1, LSource.FetchCount); + LFilters[0].Values[0] := 95; + LResult := LSource.Fetch(0, 10, LFilters, foAnd, LTable); + AssertEquals(5, LResult.TotalDataCount); + AssertEquals(95, Integer(LTable.Rows[0].Value)); + AssertEquals(1, LSource.FetchCount); +end; + +procedure TDataSourceSnapshotTests.CriteriaValuesAndOperandArePartOfTheQuery; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LFilters: TFilterCriteria; + LResult: TPageFetchResult; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + LSource.SnapshotCacheEnabled := True; + LFilters := AtLeast(90); + LSource.Fetch(0, 25, LFilters, foAnd, LTable); + LFilters[0].Values[0] := 95; // Same caller array must not mutate the cached query. + LResult := LSource.Fetch(0, 25, LFilters, foAnd, LTable); + AssertEquals(5, LResult.TotalDataCount); + SetLength(LFilters, 2); + LFilters[0].Values[0] := 90; + LFilters[1] := Default(TColumnFilter); + LFilters[1].ColumnName := 'Value'; + LFilters[1].Filter := vgfNumericLT; + SetLength(LFilters[1].Values, 1); + LFilters[1].Values[0] := 10; + LResult := LSource.Fetch(0, 25, LFilters, foOr, LTable); + AssertEquals(20, LResult.TotalDataCount); + LResult := LSource.Fetch(0, 25, LFilters, foAnd, LTable); + AssertEquals(0, LResult.TotalDataCount); + AssertEquals(0, Length(LTable.Rows)); + AssertEquals(1, LSource.FetchCount); +end; + +procedure TDataSourceSnapshotTests.InvalidationsCoalesceAndClearEmptyRows; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LResult: TPageFetchResult; + i: Integer; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + LSource.SnapshotCacheEnabled := True; + LSource.Fetch(0, 10, nil, foAnd, LTable); + AssertEquals(10, Length(LTable.Rows)); + LSource.ItemCount := 0; + for i := 1 to 1000 do LSource.InvalidateSnapshot; + LResult := LSource.Fetch(0, 10, nil, foAnd, LTable); + AssertEquals(2, LSource.FetchCount); + AssertEquals(0, Length(LTable.Rows)); + AssertEquals(0, LResult.TotalDataCount); + AssertEquals(0, LResult.PageCount); + AssertEquals(-1, LResult.PageIndex); + LSource.Fetch(0, 10, nil, foAnd, LTable); + AssertEquals(2, LSource.FetchCount); +end; + +procedure TDataSourceSnapshotTests.DefaultSourceStillFetchesEveryTime; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + AssertFalse(LSource.SnapshotCacheEnabled); + LSource.Fetch(0, 10, nil, foAnd, LTable); + LSource.Fetch(1, 10, nil, foAnd, LTable); + AssertEquals(2, LSource.FetchCount); +end; + +procedure TDataSourceSnapshotTests.InvalidatingDoesNotWaitForBlockedFetch; +var + LSource: TCountingSnapshotSource; + LFetch, LInvalidate: TSnapshotTestThread; + LCompleted: Boolean; + LTable: TDataTable; +begin + LSource := TCountingSnapshotSource.Create; + try + LFetch := nil; + LInvalidate := nil; + try + LSource.SnapshotCacheEnabled := True; + LSource.BlockFetch := True; + LFetch := TSnapshotTestThread.Create(LSource, False); + AssertEquals('Fetch did not enter its gate', Ord(wrSignaled), Ord(LSource.Entered.WaitFor(5000))); + LInvalidate := TSnapshotTestThread.Create(LSource, True); + LCompleted := LInvalidate.Completed.WaitFor(500) = wrSignaled; + AssertTrue('Logical invalidation waited for the fetch lock', LCompleted); + AssertEquals('', LInvalidate.ErrorMessage); + AssertEquals('The fetch gate must remain closed during invalidation', Ord(wrTimeout), + Ord(LSource.ReleaseFetch.WaitFor(0))); + finally + LSource.ReleaseFetch.SetEvent; + if Assigned(LFetch) then LFetch.WaitFor; + if Assigned(LInvalidate) then LInvalidate.WaitFor; + LInvalidate.Free; + LFetch.Free; + end; + // The in-flight snapshot used an old generation and must not be reused. + LSource.Fetch(0, 10, nil, foAnd, LTable); + AssertEquals(2, LSource.FetchCount); + finally + LSource.Free; + end; +end; + +procedure TDataSourceSnapshotTests.FailedFetchDoesNotCachePartialData; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LResult: TPageFetchResult; + LFailed: Boolean; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + LSource.SnapshotCacheEnabled := True; + LSource.FailNext := True; + LFailed := False; + try + LSource.Fetch(0, 10, nil, foAnd, LTable); + except + on E: Exception do LFailed := E.Message = 'Expected snapshot fetch failure'; + end; + AssertTrue(LFailed); + LResult := LSource.Fetch(0, 10, nil, foAnd, LTable); + AssertEquals(2, LSource.FetchCount); + AssertEquals(100, LResult.TotalDataCount); + AssertEquals(10, Length(LTable.Rows)); +end; + +procedure TDataSourceSnapshotTests.FilteringDoesNotModifyTheRawSnapshot; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LResult: TPageFetchResult; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + LSource.SnapshotCacheEnabled := True; + LResult := LSource.Fetch(0, 10, AtLeast(95), foAnd, LTable); + AssertEquals(5, LResult.TotalDataCount); + LResult := LSource.Fetch(0, 10, nil, foAnd, LTable); + AssertEquals(100, LResult.TotalDataCount); + AssertEquals(99, Integer(LTable.Rows[0].Value)); + AssertEquals(1, LSource.FetchCount); +end; + +procedure TDataSourceSnapshotTests.PageSizesAndClampingUseTheCachedCount; +var + LSource: TCountingSnapshotSource; + LTable: TDataTable; + LResult: TPageFetchResult; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TCountingSnapshotSource.Create) as TCountingSnapshotSource; + LSource.ItemCount := 3; + LSource.SnapshotCacheEnabled := True; + LResult := LSource.Fetch(99, 2, nil, foAnd, LTable); + AssertEquals(1, LResult.PageIndex); + AssertEquals(2, LResult.PageCount); + AssertEquals(1, Length(LTable.Rows)); + LResult := LSource.Fetch(0, -1, nil, foAnd, LTable); + AssertEquals(0, LResult.PageIndex); + AssertEquals(1, LResult.PageCount); + AssertEquals(3, Length(LTable.Rows)); + AssertEquals(1, LSource.FetchCount); +end; + +procedure TDataSourceSnapshotTests.EmptyColumnsAreRejected; +var LColumns : TDataColumns; + LRow : Variant; + LRejected : Boolean; +begin + LColumns := nil; + LRejected := False; + try + LRow := TDataRow.New($5343484D,LColumns); + except + on E : EDataRow do LRejected := True; + end; + AssertTrue('A row must reject an empty schema before allocating values',LRejected); +end; + +procedure TDataSourceSnapshotTests.KeyAndColumnValuesRoundTripIndependently; +var LColumns : TDataColumns; + LRow, LCopy : Variant; + LData : TDataRowData; +begin + SetLength(LColumns,2); + LColumns[0] := TDataColumn.From('Value',True); + LColumns[1] := TDataColumn.From('Title'); + LRow := TDataRow.New($5343484D,LColumns); + LData := TDataRowData(LRow); + LData['Value'] := 17; + LData['Title'] := 'saved'; + LData['__KEY'] := 29; + AssertEquals(17,Integer(LRow.Value)); + AssertEquals('saved',String(LRow.Title)); + AssertEquals('The key follows every declared column',29,Integer(LRow.__KEY)); + AssertEquals('Positional reads retain the original first column',17,Integer(LRow._(0))); + LCopy := LRow; + LCopy.__KEY := 31; + AssertEquals('A copied row owns an independent value array',29,Integer(LRow.__KEY)); + AssertEquals(31,Integer(LCopy.__KEY)); +end; + +procedure TDataSourceSnapshotTests.PositionalCallsWithRuntimeIndexPreserveColumnsAndKey; +var LColumns: TDataColumns; + LRow, LValue: Variant; + LData: TDataRowData; + LIndex, LNumber: Integer; +begin + SetLength(LColumns,2); + LColumns[0] := TDataColumn.From('Value',True); + LColumns[1] := TDataColumn.From('Title'); + LRow := TDataRow.New($53434958,LColumns); + LData := TDataRowData(LRow); + LData['Value'] := 17; + LData['Title'] := 'saved'; + LData['__KEY'] := 29; + LIndex := 0; + LValue := LRow._(LIndex); + AssertEquals(17,Integer(LValue)); + LNumber := LRow._(LIndex); + AssertEquals(17,LNumber); + LIndex := 1; + LValue := LRow._(LIndex); + AssertEquals('saved',String(LValue)); + LIndex := 2; + AssertEquals(29,Integer(LRow._(LIndex))); + AssertEquals(17,Integer(LRow.Value)); + AssertEquals(29,Integer(LRow.__KEY)); +end; + +procedure TDataSourceSnapshotTests.PositionalDispatcherRejectsUnknownNamesAndWrongArity; +var LColumns: TDataColumns; + LRow, LValue: Variant; + LData: TDataRowData; + LHandler: TCustomVariantType; + LArguments: TVarDataArray; +begin + SetLength(LColumns,1); + LColumns[0] := TDataColumn.From('Value',True); + LRow := TDataRow.New($53434446,LColumns); + LData := TDataRowData(LRow); + LData['Value'] := 17; + AssertTrue(FindCustomVariantType(VarType(LRow),LHandler)); + LValue := 93; + AssertFalse(TDataRow(LHandler).DoFunction(TVarData(LValue),TVarData(LRow),'_',nil)); + AssertFalse(TDataRow(LHandler).DoFunction(TVarData(LValue),TVarData(LRow),'',nil)); + SetLength(LArguments,2); + Variant(LArguments[0]) := 0; + Variant(LArguments[1]) := 0; + AssertFalse(TDataRow(LHandler).DoFunction(TVarData(LValue),TVarData(LRow),'_',LArguments)); + AssertFalse(TDataRow(LHandler).DoFunction(TVarData(LValue),TVarData(LRow),'',LArguments)); + SetLength(LArguments,1); + AssertFalse(TDataRow(LHandler).DoFunction(TVarData(LValue),TVarData(LRow),'UnknownMethod',LArguments)); + AssertEquals('Rejected dispatch must leave its destination unchanged',93,Integer(LValue)); + AssertTrue(TDataRow(LHandler).DoFunction(TVarData(LValue),TVarData(LRow),'_',LArguments)); + AssertEquals(17,Integer(LValue)); + LValue := 93; + AssertTrue(TDataRow(LHandler).DoFunction(TVarData(LValue),TVarData(LRow),'',LArguments)); + AssertEquals(17,Integer(LValue)); +end; + +procedure TDataSourceSnapshotTests.UnicodeColumnNamesBindingsAndValuesRoundTrip; +var + LSource: TUnicodeBindingSource; + LTable: TDataTable; + LFilter: TFilterCriteria; + LData, LCopiedData: TDataRowData; + LCopy: Variant; + LResult: TPageFetchResult; +begin + LSource := TUnicodeBindingSource.Create(nil); + try + SetLength(LFilter, 1); + LFilter[0] := Default(TColumnFilter); + LFilter[0].ColumnName := UnicodeBinding(False); + LFilter[0].Filter := vgfSortable; + LFilter[0].Sort := sdAscending; + LResult := LSource.FetchPage(TPageFetchParams.Create(0, 1, LFilter, foAnd), LTable); + AssertEquals(2, LResult.TotalDataCount); + AssertTrue('First Unicode column name', LTable.Columns[0].Name = UnicodeBinding(False)); + AssertTrue('Distinct Unicode column name', LTable.Columns[1].Name = UnicodeBinding(True)); + LData := TDataRowData(LTable.Rows[0]); + AssertTrue(LData.HasData(UnicodeBinding(False)) and LData.HasData(UnicodeBinding(True))); + AssertEquals('Sort by Unicode binding selects first item', 1, Integer(LData[UnicodeBinding(False)])); + AssertTrue('Unicode field value survives dehydration', + VarToWideStr(LData[UnicodeBinding(True)]) = UnicodePayload + UnicodeString('1')); + AssertEquals(1, Integer(LData['__KEY'])); + + LCopy := LTable.Rows[0]; + LCopiedData := TDataRowData(LCopy); + LCopiedData[UnicodeBinding(True)] := UnicodePayload + UnicodeString(' copy'); + AssertTrue('Copied Unicode field remains independent', + VarToWideStr(LData[UnicodeBinding(True)]) = UnicodePayload + UnicodeString('1')); + AssertTrue(VarToWideStr(LCopiedData[UnicodeBinding(True)]) = + UnicodePayload + UnicodeString(' copy')); + finally + LSource.Free; + end; +end; + +initialization + RegisterTest(TDataSourceSnapshotTests); + +end. diff --git a/src/tests/experimental/framework/UCommon.UI.Tests.pas b/src/tests/experimental/framework/UCommon.UI.Tests.pas new file mode 100644 index 000000000..fde0d8490 --- /dev/null +++ b/src/tests/experimental/framework/UCommon.UI.Tests.pas @@ -0,0 +1,1363 @@ +unit UCommon.UI.Tests; + +{ Copyright © 2026 Herman Schoenfeld + Distributed under the MIT software license, see the accompanying LICENSE. } + +{$mode delphi} + +interface + +uses + {$IFDEF WINDOWS}Windows,{$ENDIF} + Classes, SysUtils, Forms, ExtCtrls, SyncObjs, Generics.Collections, fpcunit, testregistry, + UCommon, UCommon.UI; + +type + TWaitingTestAction = (wtaNone, wtaNotify, wtaCancel, wtaChangeMode, wtaDispose); + + TThrottledEventTests = class(TTestCase) + private + FEvent : TThrottledEvent; + FDeliveryTicks : TList<QWord>; + FLastInputTick : QWord; + FCallbacksOnMainThread : Boolean; + FRearmNextDelivery : Boolean; + FLastSuppressedCount : Integer; + FNativeTimer : TTimer; + FOriginalTimerHandler : TNotifyEvent; + FTimerWakeTicks : TList<QWord>; + FTimingTrace : TStringList; + FNotifyDuringDelivery : Boolean; + FThrowNextDelivery: Boolean; + FAsyncExceptionCount: Integer; + FUnexpectedException: String; + FPreviousException: TExceptionEvent; + FStartedCount, FFinishingCount, FFinishedCount: Integer; + FFinishedTick: QWord; + FLifecycleStateValid, FPostponeFinish, FNotifyOnStart, FNotifyOnFinish: Boolean; + FWaitingStates: TList<Boolean>; + FWaitingStateValid: Boolean; + FWaitingAction: TWaitingTestAction; + FWaitingActionState: Boolean; + FThrowStarted, FThrowFinishing, FThrowFinished: Boolean; + FThrowWaitingTrue, FThrowWaitingFalse: Boolean; + FNotifyOnTrailing, FCancelOnDelivery, FDisposeOnDelivery, FDisposeOnFinishing: Boolean; + FSlowLeadingMilliseconds: Cardinal; + FSlowLeadingEntered: TEvent; + procedure OnExpectedException(Sender: TObject; E: Exception); + procedure OnWaitingChanged(Sender: TObject); + procedure ThrowOnce(var ARequested: Boolean); + procedure DisposeEvent; + procedure OnBurstStarted(Sender: TObject); + procedure OnBurstFinishing(Sender: TObject); + procedure OnBurstFinished(Sender: TObject); + procedure WaitForFinishes(ACount: Integer); + procedure OnNativeTimer(Sender : TObject); + procedure CheckPhysicalTimerReset(AMode : TThrottledEventMode); + procedure OnNotified(Sender : TObject); + procedure Notify; + procedure PumpFor(AMilliseconds : Cardinal); + procedure WaitForDeliveries(ACount : Integer); + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure FirstNotificationIsImmediate; + procedure IntervalRetainsItsNumericMilliseconds; + procedure FirstNotificationAfterTrailingStartsImmediately; + procedure ContinuousBurstHasOnlyLeadingAndFinalDeliveries; + procedure LaterNotificationsExtendTheQuietDeadline; + procedure SingleNotificationDoesNotDeliverTwice; + procedure PostponeDoesNotStartAnotherLeadingDelivery; + procedure PostponeRetainsElapsedProducerQuietTime; + procedure ListenerCanPostponeATrailingDeliveryAgain; + procedure LeadingBurstInputRestartsItsNativeTimer; + procedure TrailingBurstInputRestartsItsNativeTimer; + procedure PeriodicInputsRetainTheArmedTimerDeadline; + procedure ReentrantBusyListenerRetainsTheResetDeadline; + procedure SingleInputFinishesItsLifecycleWithoutAnotherDelivery; + procedure ContinuousInputExtendsTheLifecycleDeadline; + procedure FinishCandidateCanPostponeWithoutPublishingFinished; + procedure StartedListenerCanNotifyWithoutDuplicatingTheStart; + procedure FinishedListenerCanStartTheNextBurst; + procedure DisposingActiveBurstCancelsItsLifecycleTimer; + procedure NonBurstModesDoNotPublishBurstLifecycle; + procedure TrailingOnlyModePublishesOneStartAndFinish; + procedure TrailingListenerExceptionDoesNotLeaveAnUnarmedBurst; + procedure WaitingIsCommittedBeforeStartAndLeadingDelivery; + procedure WaitingChangesOnceForAQuietSingleInput; + procedure PendingInputsRetainWaitingUntilTheirOwnDeadline; + procedure CancelClearsWaitingWithoutDeliveringPendingWork; + procedure CancelWhileIdleDoesNotPublishAStateChange; + procedure ModeChangeCancelsThePreviousWaitingRequest; + procedure ChangingBetweenBurstModesCancelsTheOldBurst; + procedure WaitingStartListenerCanCancelItsRequest; + procedure WaitingStartListenerCanChangeMode; + procedure WaitingStartListenerCanNotifyReentrantly; + procedure WaitingEndListenerCanStartANewRequest; + procedure CancelStateListenerCanStartANewRequest; + procedure WaitingStartListenerCanDisposeTheComponent; + procedure WaitingEndListenerCanDisposeTheComponent; + procedure OrdinaryListenerCanDisposeTheComponent; + procedure FinishingListenerCanDisposeTheComponent; + procedure LeadingListenerFailureRetainsTheOwnedTimer; + procedure StartedListenerFailureRetainsTheOwnedTimer; + procedure FinishingListenerFailureCommitsIdleState; + procedure FinishedListenerFailureLeavesIdleState; + procedure WaitingStartFailureRetainsTheOwnedTimer; + procedure WaitingEndFailureLeavesIdleState; + procedure CancelStateFailureStillLeavesIdleState; + procedure PostponedFinishingFailureRetainsTheRearmedRequest; + procedure ReentrantTrailingRequestDeliversImmediatelyOnce; + procedure FiveSecondInputsAt4900MsRetainOneOwnedBurst; + procedure CapturedLateTimerAfterFreshInputCannotFinishWaiting; + procedure SlowLeadingListenerAcceptsSynchronizedUiInputsWithoutEarlyFinish; + end; + + TThreadNotifyTests = class(TTestCase) + private + FNotifyCount : Integer; + FNotifierToDispose : TThreadNotify; + procedure OnNotified(Sender : TObject); + procedure DisposeOtherNotifier(Sender : TObject); + published + procedure DisposingNotifierCancelsQueuedMethod; + procedure DisposingNotifierPreservesOtherQueuedNotifications; + procedure CallbackCanDisposeAnotherPendingNotifier; + end; + +implementation + +type + EThrottleTestCallback = class(Exception); + + TReusedThreadNotify = class(TThreadNotify) + public + procedure FreeInstance; override; + procedure ReleaseMemory; + end; + + TNotifyProducerThread = class(TThread) + private + FNotifier : TThreadNotify; + FOtherNotifier : TThreadNotify; + FHandler : TNotifyEvent; + FOtherHandler : TNotifyEvent; + protected + procedure Execute; override; + public + constructor Create(ANotifier : TThreadNotify; AHandler : TNotifyEvent; AOtherNotifier : TThreadNotify = nil; AOtherHandler : TNotifyEvent = nil); + end; + + TSynchronizedThrottleInputs = class(TThread) + private + FStarted, FCompleted: TEvent; + FInput: TThreadMethod; + procedure InvokeInput; + protected + procedure Execute; override; + public + InputCount: Integer; + WrongThread, StartTimedOut: Boolean; + constructor Create(AStarted: TEvent; AInput: TThreadMethod); + destructor Destroy; override; + procedure Stop; + end; + +const + CT_TEST_INTERVAL = 120; + +constructor TSynchronizedThrottleInputs.Create(AStarted: TEvent; AInput: TThreadMethod); +begin + inherited Create(True); + FreeOnTerminate := False; + FStarted := AStarted; + FInput := AInput; + FCompleted := TEvent.Create(nil, True, False, ''); +end; + +destructor TSynchronizedThrottleInputs.Destroy; +begin + Stop; + FCompleted.Free; + inherited; +end; + +procedure TSynchronizedThrottleInputs.InvokeInput; +begin + WrongThread := WrongThread or (GetCurrentThreadID <> MainThreadID); + FInput; +end; + +procedure TSynchronizedThrottleInputs.Execute; +begin + try + StartTimedOut := FStarted.WaitFor(5000) <> wrSignaled; + if StartTimedOut then Exit; + while not Terminated do begin + Synchronize(InvokeInput); + Inc(InputCount); + Sleep(50); + end; + finally + FCompleted.SetEvent; + end; +end; + +procedure TSynchronizedThrottleInputs.Stop; +var LDeadline: QWord; +begin + Terminate; + FStarted.SetEvent; + LDeadline := GetTickCount64 + 2500; + while (FCompleted.WaitFor(0) <> wrSignaled) and (GetTickCount64 < LDeadline) do begin + CheckSynchronize(0); + Sleep(1); + end; + if FCompleted.WaitFor(0) <> wrSignaled then + raise Exception.Create('Synchronized throttle-input fixture did not stop'); + WaitFor; +end; + +procedure TReusedThreadNotify.FreeInstance; +begin + CleanupInstance; + // Retain the allocation and simulate allocator reuse. A stale queued method + // now fails deterministically, without reading memory returned to the heap. + FillChar((PByte(Self)+SizeOf(Pointer))^,InstanceSize-SizeOf(Pointer),$A5); +end; + +procedure TReusedThreadNotify.ReleaseMemory; +begin + System.FreeMem(Pointer(Self)); +end; + +constructor TNotifyProducerThread.Create(ANotifier : TThreadNotify; AHandler : TNotifyEvent; AOtherNotifier : TThreadNotify; AOtherHandler : TNotifyEvent); +begin + inherited Create(True); + FreeOnTerminate := False; + FNotifier := ANotifier; + FOtherNotifier := AOtherNotifier; + FHandler := AHandler; + FOtherHandler := AOtherHandler; +end; + +procedure TNotifyProducerThread.Execute; +begin + FNotifier.Invoke(Self,FHandler); + if Assigned(FOtherNotifier) then begin + if Assigned(FOtherHandler) then FOtherNotifier.Invoke(Self,FOtherHandler) + else FOtherNotifier.Invoke(Self,FHandler); + end; +end; + +procedure TThreadNotifyTests.OnNotified(Sender : TObject); +begin + Inc(FNotifyCount); +end; + +procedure TThreadNotifyTests.DisposeOtherNotifier(Sender : TObject); +begin + Inc(FNotifyCount); + FreeAndNil(FNotifierToDispose); +end; + +procedure TThreadNotifyTests.DisposingNotifierCancelsQueuedMethod; +var + LNotifier : TReusedThreadNotify; + LProducer : TNotifyProducerThread; + LDisposed, LInvokedAfterDisposal : Boolean; + {$IFNDEF WINDOWS}LDeadline : QWord;{$ENDIF} +begin + CheckSynchronize(0); + FNotifyCount := 0; + LNotifier := TReusedThreadNotify.Create(TThread.CurrentThread); + LProducer := TNotifyProducerThread.Create(LNotifier,OnNotified); + LDisposed := False; + try + LProducer.Start; + // Join the producer without pumping its queued notification. Main-thread + // TThread.WaitFor can dispatch that notification before disposal is tested. + {$IFDEF WINDOWS} + AssertEquals('Notification producer did not finish',LongInt(WAIT_OBJECT_0), + LongInt(WaitForSingleObject(LProducer.Handle,5000))); + {$ELSE} + LDeadline := GetTickCount64+5000; + while (not LProducer.Finished) and (GetTickCount64<LDeadline) do Sleep(1); + AssertTrue('Notification producer did not finish',LProducer.Finished); + {$ENDIF} + LNotifier.Free; + LDisposed := True; + LInvokedAfterDisposal := False; + try + CheckSynchronize(0); + except + on E : EAccessViolation do LInvokedAfterDisposal := True; + end; + AssertFalse('A disposed notifier left its queued method behind',LInvokedAfterDisposal); + AssertEquals('A disposed notifier must not deliver its callback',0,FNotifyCount); + finally + LProducer.WaitFor; + LProducer.Free; + if not LDisposed then LNotifier.Free; + CheckSynchronize(0); + LNotifier.ReleaseMemory; + end; +end; + +procedure TThreadNotifyTests.DisposingNotifierPreservesOtherQueuedNotifications; +var + LNotifier, LOtherNotifier : TThreadNotify; + LProducer : TNotifyProducerThread; + {$IFNDEF WINDOWS}LDeadline : QWord;{$ENDIF} +begin + CheckSynchronize(0); + FNotifyCount := 0; + LNotifier := TThreadNotify.Create(TThread.CurrentThread); + LOtherNotifier := TThreadNotify.Create(TThread.CurrentThread); + LProducer := TNotifyProducerThread.Create(LNotifier,OnNotified,LOtherNotifier); + try + LProducer.Start; + {$IFDEF WINDOWS} + AssertEquals('Notification producer did not finish',LongInt(WAIT_OBJECT_0), + LongInt(WaitForSingleObject(LProducer.Handle,5000))); + {$ELSE} + LDeadline := GetTickCount64+5000; + while (not LProducer.Finished) and (GetTickCount64<LDeadline) do Sleep(1); + AssertTrue('Notification producer did not finish',LProducer.Finished); + {$ENDIF} + FreeAndNil(LNotifier); + CheckSynchronize(0); + AssertEquals('Another notifier on the same thread must retain its callback',1,FNotifyCount); + finally + LProducer.WaitFor; + LProducer.Free; + LNotifier.Free; + LOtherNotifier.Free; + end; +end; + +procedure TThreadNotifyTests.CallbackCanDisposeAnotherPendingNotifier; +var + LFirst : TThreadNotify; + LSecond : TReusedThreadNotify; + LProducer : TNotifyProducerThread; + LInvokedAfterDisposal : Boolean; + {$IFNDEF WINDOWS}LDeadline : QWord;{$ENDIF} +begin + CheckSynchronize(0); + FNotifyCount := 0; + LFirst := TThreadNotify.Create(TThread.CurrentThread); + LSecond := TReusedThreadNotify.Create(TThread.CurrentThread); + FNotifierToDispose := LSecond; + LProducer := TNotifyProducerThread.Create(LFirst,DisposeOtherNotifier,LSecond,OnNotified); + try + LProducer.Start; + {$IFDEF WINDOWS} + AssertEquals('Notification producer did not finish',LongInt(WAIT_OBJECT_0), + LongInt(WaitForSingleObject(LProducer.Handle,5000))); + {$ELSE} + LDeadline := GetTickCount64+5000; + while (not LProducer.Finished) and (GetTickCount64<LDeadline) do Sleep(1); + AssertTrue('Notification producer did not finish',LProducer.Finished); + {$ENDIF} + LInvokedAfterDisposal := False; + try + CheckSynchronize(0); + except + on E : EAccessViolation do LInvokedAfterDisposal := True; + end; + AssertFalse('An earlier callback disposed a receiver still present in the dispatch batch', + LInvokedAfterDisposal); + AssertEquals('Only the first callback may run',1,FNotifyCount); + AssertTrue('The first callback must have disposed the second receiver', + FNotifierToDispose=nil); + finally + LProducer.WaitFor; + LProducer.Free; + FreeAndNil(FNotifierToDispose); + CheckSynchronize(0); + LSecond.ReleaseMemory; + LFirst.Free; + end; +end; +procedure TThrottledEventTests.SetUp; +begin + inherited SetUp; + FCallbacksOnMainThread := True; + FRearmNextDelivery := False; + FLastSuppressedCount := 0; + FNotifyDuringDelivery := False; + FThrowNextDelivery := False; + FAsyncExceptionCount := 0; + FUnexpectedException := ''; + FStartedCount := 0; + FFinishingCount := 0; + FFinishedCount := 0; + FFinishedTick := 0; + FLifecycleStateValid := True; + FPostponeFinish := False; + FNotifyOnStart := False; + FNotifyOnFinish := False; + FWaitingStateValid := True; + FWaitingAction := wtaNone; + FWaitingActionState := True; + FThrowStarted := False; + FThrowFinishing := False; + FThrowFinished := False; + FThrowWaitingTrue := False; + FThrowWaitingFalse := False; + FNotifyOnTrailing := False; + FCancelOnDelivery := False; + FDisposeOnDelivery := False; + FDisposeOnFinishing := False; + FSlowLeadingMilliseconds := 0; + FSlowLeadingEntered := nil; + FWaitingStates := TList<Boolean>.Create; + FDeliveryTicks := TList<QWord>.Create; + FEvent := TThrottledEvent.Create(nil); + FEvent.Mode := temNotifyOnEventBurstStartAndFinished; + FEvent.Interval := TTimeSpan.FromMilliseconds(CT_TEST_INTERVAL); + FEvent.Add(OnNotified); + FEvent.OnBurstStarted.Add(OnBurstStarted); + FEvent.OnBurstFinishing.Add(OnBurstFinishing); + FEvent.OnBurstFinished.Add(OnBurstFinished); + FEvent.OnWaitingChanged.Add(OnWaitingChanged); + FTimerWakeTicks := TList<QWord>.Create; + FTimingTrace := TStringList.Create; + FNativeTimer := FEvent.Components[0] as TTimer; + FOriginalTimerHandler := FNativeTimer.OnTimer; + FNativeTimer.OnTimer := OnNativeTimer; + FPreviousException := Application.OnException; + Application.OnException := OnExpectedException; +end; + +procedure TThrottledEventTests.TearDown; +begin + Application.OnException := FPreviousException; + if Assigned(FEvent) then begin + FNativeTimer.OnTimer := FOriginalTimerHandler; + FEvent.Remove(OnNotified); + FEvent.OnBurstStarted.Remove(OnBurstStarted); + FEvent.OnBurstFinishing.Remove(OnBurstFinishing); + FEvent.OnBurstFinished.Remove(OnBurstFinished); + FEvent.OnWaitingChanged.Remove(OnWaitingChanged); + end; + FEvent.Free; + FDeliveryTicks.Free; + FTimerWakeTicks.Free; + FTimingTrace.Free; + FWaitingStates.Free; + FSlowLeadingEntered.Free; + inherited TearDown; +end; + +procedure TThrottledEventTests.OnNotified(Sender : TObject); +var LSlowUntil: QWord; +begin + FCallbacksOnMainThread := FCallbacksOnMainThread and + (GetCurrentThreadID = MainThreadID); + FDeliveryTicks.Add(GetTickCount64); + FTimingTrace.Add(Format('%d invoke suppressed=%d',[GetTickCount64,FEvent.SuppressedInvocations])); + FLastSuppressedCount := FEvent.SuppressedInvocations; + if FEvent.Mode in [temNotifyOnEventBurstFinished, temNotifyOnEventBurstStartAndFinished] then + FWaitingStateValid := FWaitingStateValid and FEvent.Waiting; + if FSlowLeadingMilliseconds > 0 then begin + LSlowUntil := GetTickCount64 + FSlowLeadingMilliseconds; + FSlowLeadingMilliseconds := 0; + FSlowLeadingEntered.SetEvent; + // Keep the actual leading listener active while accepting real worker + // Synchronize inputs on this UI thread. Do not manually fire its timer. + repeat + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LSlowUntil; + end; + if FRearmNextDelivery then begin + FRearmNextDelivery := False; + FEvent.Postpone; + end; + if FNotifyDuringDelivery then begin + FNotifyDuringDelivery := False; + Sleep(40); + Notify; + end; + if FNotifyOnTrailing and (FEvent.SuppressedInvocations > 0) then begin + FNotifyOnTrailing := False; + Notify; + end; + if FCancelOnDelivery then begin + FCancelOnDelivery := False; + FEvent.Cancel; + end; + if FDisposeOnDelivery then begin + DisposeEvent; + Exit; + end; + if FThrowNextDelivery then begin + FThrowNextDelivery := False; + raise EThrottleTestCallback.Create('Expected throttle callback failure'); + end; +end; + +procedure TThrottledEventTests.ThrowOnce(var ARequested: Boolean); +begin + if not ARequested then Exit; + ARequested := False; + raise EThrottleTestCallback.Create('Expected throttle callback failure'); +end; + +procedure TThrottledEventTests.DisposeEvent; +var LEvent: TThrottledEvent; +begin + LEvent := FEvent; + FEvent := nil; + FNativeTimer := nil; + LEvent.Free; +end; + +procedure TThrottledEventTests.OnWaitingChanged(Sender: TObject); +var LWaiting: Boolean; LAction: TWaitingTestAction; +begin + LWaiting := FEvent.Waiting; + FWaitingStates.Add(LWaiting); + FWaitingStateValid := FWaitingStateValid and (Sender = FEvent) and + (FEvent.Waiting = FEvent.BurstActive) and (FNativeTimer.Enabled = LWaiting) and + (GetCurrentThreadID = MainThreadID); + if LWaiting = FWaitingActionState then begin + LAction := FWaitingAction; + FWaitingAction := wtaNone; + case LAction of + wtaNotify: Notify; + wtaCancel: FEvent.Cancel; + wtaChangeMode: FEvent.Mode := temNone; + wtaDispose: begin + DisposeEvent; + Exit; + end; + end; + end; + if LWaiting then ThrowOnce(FThrowWaitingTrue) else ThrowOnce(FThrowWaitingFalse); +end; + +procedure TThrottledEventTests.OnExpectedException(Sender: TObject; E: Exception); +begin + if E is EThrottleTestCallback then Inc(FAsyncExceptionCount) + else FUnexpectedException := E.ClassName + ': ' + E.Message; +end; + +procedure TThrottledEventTests.OnNativeTimer(Sender : TObject); +begin + FTimerWakeTicks.Add(GetTickCount64); + FTimingTrace.Add(Format('%d native timer quiet=%d interval=%d', + [GetTickCount64,GetTickCount64-FLastInputTick,FNativeTimer.Interval])); + try + FOriginalTimerHandler(Sender); + except + on E: EThrottleTestCallback do Inc(FAsyncExceptionCount); + end; +end; + +procedure TThrottledEventTests.OnBurstStarted(Sender: TObject); +begin + Inc(FStartedCount); + FLifecycleStateValid := FLifecycleStateValid and (Sender = FEvent) and FEvent.BurstActive and + (GetCurrentThreadID = MainThreadID); + FWaitingStateValid := FWaitingStateValid and (FWaitingStates.Count > 0) and FEvent.Waiting; + ThrowOnce(FThrowStarted); + if FNotifyOnStart then begin + FNotifyOnStart := False; + Notify; + end; +end; + +procedure TThrottledEventTests.OnBurstFinishing(Sender: TObject); +begin + Inc(FFinishingCount); + FLifecycleStateValid := FLifecycleStateValid and (Sender = FEvent) and FEvent.BurstActive and + (GetCurrentThreadID = MainThreadID); + if FPostponeFinish then begin + FPostponeFinish := False; + FEvent.Postpone; + end; + if FDisposeOnFinishing then begin + DisposeEvent; + Exit; + end; + ThrowOnce(FThrowFinishing); +end; + +procedure TThrottledEventTests.OnBurstFinished(Sender: TObject); +begin + Inc(FFinishedCount); + FFinishedTick := GetTickCount64; + FLifecycleStateValid := FLifecycleStateValid and (Sender = FEvent) and not FEvent.BurstActive and + not FNativeTimer.Enabled and (GetCurrentThreadID = MainThreadID); + FWaitingStateValid := FWaitingStateValid and (FWaitingStates.Count > 0) and not FEvent.Waiting; + ThrowOnce(FThrowFinished); + if FNotifyOnFinish then begin + FNotifyOnFinish := False; + Notify; + end; +end; + +procedure TThrottledEventTests.WaitForFinishes(ACount: Integer); +var LDeadline: QWord; +begin + LDeadline := GetTickCount64 + 1500; + while (FFinishedCount < ACount) and (GetTickCount64 < LDeadline) do PumpFor(2); + AssertEquals('Expected owned-timer lifecycle finish did not arrive', ACount, FFinishedCount); + AssertTrue('Lifecycle events must report the committed state on the UI thread', FLifecycleStateValid); + AssertTrue('Waiting and its owned timer are committed before outward callbacks', FWaitingStateValid); +end; + +procedure TThrottledEventTests.Notify; +begin + FLastInputTick := GetTickCount64; + FTimingTrace.Add(Format('%d notify',[FLastInputTick])); + FEvent.Notify; +end; + +procedure TThrottledEventTests.PumpFor(AMilliseconds : Cardinal); +var + LDeadline : QWord; +begin + LDeadline := GetTickCount64+AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + if FUnexpectedException <> '' then Fail(FUnexpectedException); + Sleep(1); + until GetTickCount64>=LDeadline; +end; + +procedure TThrottledEventTests.WaitForDeliveries(ACount : Integer); +var + LDeadline : QWord; +begin + // This is a failure bound, not an expected delivery time. Allow scheduler + // jitter while requiring delivery counts and the quiet-period lower bound. + LDeadline := GetTickCount64+1500; + while (FDeliveryTicks.Count<ACount) and (GetTickCount64<LDeadline) do + PumpFor(2); + AssertEquals('Expected timer delivery did not arrive',ACount,FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.TrailingListenerExceptionDoesNotLeaveAnUnarmedBurst; +begin + Notify; + FThrowNextDelivery := True; + Notify; + PumpFor(2 * CT_TEST_INTERVAL + 80); + AssertEquals('The actual timer propagates the callback failure', 1, FAsyncExceptionCount); + AssertEquals('A failed trailing listener still closes the quiet lifecycle', 1, FFinishedCount); + AssertFalse('No active burst remains with its owned timer disabled', FEvent.BurstActive); + AssertFalse(FNativeTimer.Enabled); +end; + +procedure TThrottledEventTests.WaitingIsCommittedBeforeStartAndLeadingDelivery; +begin + AssertFalse(FEvent.Waiting); + Notify; + AssertTrue(FEvent.Waiting and FEvent.BurstActive and FNativeTimer.Enabled); + AssertEquals('One owned timer remains the only child component', 1, FEvent.ComponentCount); + AssertEquals(1, FWaitingStates.Count); + AssertTrue(FWaitingStates[0]); + AssertTrue(FWaitingStateValid); + AssertEquals(1, FStartedCount); + AssertEquals(1, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.WaitingChangesOnceForAQuietSingleInput; +begin + Notify; + WaitForFinishes(1); + AssertEquals(2, FWaitingStates.Count); + AssertTrue(FWaitingStates[0]); + AssertFalse(FWaitingStates[1]); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + AssertEquals('A lifecycle timer adds no duplicate ordinary notification', 1, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.PendingInputsRetainWaitingUntilTheirOwnDeadline; +begin + Notify; + PumpFor(CT_TEST_INTERVAL div 2); + Notify; + PumpFor(CT_TEST_INTERVAL div 2); + AssertTrue(FEvent.Waiting); + AssertEquals(1, FWaitingStates.Count); + AssertEquals(0, FFinishedCount); + WaitForFinishes(1); + AssertEquals(2, FDeliveryTicks.Count); + AssertEquals(2, FWaitingStates.Count); +end; + +procedure TThrottledEventTests.CancelClearsWaitingWithoutDeliveringPendingWork; +begin + Notify; + Notify; + FEvent.Cancel; + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + AssertEquals(2, FWaitingStates.Count); + AssertFalse(FWaitingStates[1]); + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(1, FDeliveryTicks.Count); + AssertEquals('Cancellation is not a quiet completion', 0, FFinishedCount); + AssertTrue(FWaitingStateValid); +end; + +procedure TThrottledEventTests.CancelWhileIdleDoesNotPublishAStateChange; +begin + FEvent.Cancel; + AssertEquals(0, FWaitingStates.Count); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); +end; + +procedure TThrottledEventTests.ModeChangeCancelsThePreviousWaitingRequest; +begin + Notify; + Notify; + FEvent.Mode := temNotifyEveryInterval; + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + AssertEquals(2, FWaitingStates.Count); + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(1, FDeliveryTicks.Count); + AssertEquals(0, FFinishedCount); + Notify; + AssertEquals('The new mode has no stale delivery cooldown', 2, FDeliveryTicks.Count); + AssertFalse(FEvent.Waiting); +end; + +procedure TThrottledEventTests.ChangingBetweenBurstModesCancelsTheOldBurst; +begin + Notify; + FEvent.Mode := temNotifyOnEventBurstFinished; + AssertFalse(FEvent.Waiting); + Notify; + AssertTrue(FEvent.Waiting); + AssertEquals(1, FDeliveryTicks.Count); + WaitForFinishes(1); + AssertEquals(4, FWaitingStates.Count); + AssertEquals(2, FStartedCount); + AssertEquals(2, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.WaitingStartListenerCanCancelItsRequest; +begin + FWaitingAction := wtaCancel; + Notify; + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + AssertEquals(2, FWaitingStates.Count); + AssertEquals('The canceled start does not continue into data delivery', 0, FDeliveryTicks.Count); + AssertEquals(0, FStartedCount); + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(0, FFinishedCount); +end; + +procedure TThrottledEventTests.WaitingStartListenerCanChangeMode; +begin + FWaitingAction := wtaChangeMode; + Notify; + AssertEquals(Ord(temNone), Ord(FEvent.Mode)); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + AssertEquals(0, FDeliveryTicks.Count); + Notify; + AssertEquals(1, FDeliveryTicks.Count); + AssertEquals(2, FWaitingStates.Count); +end; + +procedure TThrottledEventTests.WaitingStartListenerCanNotifyReentrantly; +begin + FWaitingAction := wtaNotify; + Notify; + AssertTrue(FEvent.Waiting); + AssertEquals(1, FWaitingStates.Count); + AssertEquals(1, FStartedCount); + AssertEquals(1, FDeliveryTicks.Count); + WaitForFinishes(1); + AssertEquals(2, FDeliveryTicks.Count); + AssertEquals(2, FWaitingStates.Count); +end; + +procedure TThrottledEventTests.WaitingEndListenerCanStartANewRequest; +begin + FWaitingAction := wtaNotify; + FWaitingActionState := False; + Notify; + WaitForFinishes(1); + AssertEquals('A newly committed waiting request survives the old state callback', 4, FWaitingStates.Count); + AssertEquals(2, FStartedCount); + AssertEquals(2, FDeliveryTicks.Count); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); +end; + +procedure TThrottledEventTests.CancelStateListenerCanStartANewRequest; +begin + Notify; + FWaitingAction := wtaNotify; + FWaitingActionState := False; + FEvent.Cancel; + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + AssertEquals(3, FWaitingStates.Count); + AssertEquals(2, FStartedCount); + AssertEquals(2, FDeliveryTicks.Count); + WaitForFinishes(1); + AssertEquals(4, FWaitingStates.Count); +end; + +procedure TThrottledEventTests.WaitingStartListenerCanDisposeTheComponent; +begin + FWaitingAction := wtaDispose; + Notify; + AssertFalse(Assigned(FEvent)); + AssertEquals(1, FWaitingStates.Count); + AssertEquals(0, FStartedCount); + AssertEquals(0, FDeliveryTicks.Count); + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(0, FFinishedCount); +end; + +procedure TThrottledEventTests.WaitingEndListenerCanDisposeTheComponent; +begin + FWaitingAction := wtaDispose; + FWaitingActionState := False; + Notify; + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertFalse(Assigned(FEvent)); + AssertEquals(2, FWaitingStates.Count); + AssertEquals(0, FFinishedCount); + AssertEquals(1, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.OrdinaryListenerCanDisposeTheComponent; +begin + FDisposeOnDelivery := True; + Notify; + AssertFalse(Assigned(FEvent)); + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(1, FDeliveryTicks.Count); + AssertEquals(0, FFinishedCount); +end; + +procedure TThrottledEventTests.FinishingListenerCanDisposeTheComponent; +begin + FDisposeOnFinishing := True; + Notify; + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertFalse(Assigned(FEvent)); + AssertEquals(1, FFinishingCount); + AssertEquals(0, FFinishedCount); +end; + +procedure TThrottledEventTests.LeadingListenerFailureRetainsTheOwnedTimer; +begin + FThrowNextDelivery := True; + try + Notify; + Fail('A direct ordinary callback exception must propagate'); + except on E: EThrottleTestCallback do ; end; + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + WaitForFinishes(1); + AssertEquals(1, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.StartedListenerFailureRetainsTheOwnedTimer; +begin + FThrowStarted := True; + try + Notify; + Fail('A start callback exception must propagate'); + except on E: EThrottleTestCallback do ; end; + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + WaitForFinishes(1); + AssertEquals(0, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.FinishingListenerFailureCommitsIdleState; +begin + FThrowFinishing := True; + Notify; + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(1, FAsyncExceptionCount); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + AssertEquals(1, FFinishedCount); + AssertTrue(FWaitingStateValid); +end; + +procedure TThrottledEventTests.FinishedListenerFailureLeavesIdleState; +begin + FThrowFinished := True; + Notify; + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(1, FAsyncExceptionCount); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + AssertEquals(1, FFinishedCount); + Notify; + AssertEquals(2, FDeliveryTicks.Count); + WaitForFinishes(2); +end; + +procedure TThrottledEventTests.WaitingStartFailureRetainsTheOwnedTimer; +begin + FThrowWaitingTrue := True; + try + Notify; + Fail('A waiting-start callback exception must propagate'); + except on E: EThrottleTestCallback do ; end; + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + WaitForFinishes(1); + AssertEquals(0, FStartedCount); + AssertEquals(0, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.WaitingEndFailureLeavesIdleState; +begin + FThrowWaitingFalse := True; + Notify; + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(1, FAsyncExceptionCount); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + Notify; + AssertEquals(2, FDeliveryTicks.Count); + WaitForFinishes(1); +end; + +procedure TThrottledEventTests.CancelStateFailureStillLeavesIdleState; +begin + Notify; + FThrowWaitingFalse := True; + try + FEvent.Cancel; + Fail('A cancellation state callback exception must propagate'); + except on E: EThrottleTestCallback do ; end; + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + PumpFor(2 * CT_TEST_INTERVAL + 30); + AssertEquals(0, FFinishedCount); + AssertEquals(1, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.PostponedFinishingFailureRetainsTheRearmedRequest; +var LDeadline: QWord; +begin + FPostponeFinish := True; + FThrowFinishing := True; + Notify; + LDeadline := GetTickCount64 + 1000; + while (FFinishingCount = 0) and (GetTickCount64 < LDeadline) do PumpFor(2); + AssertEquals(1, FAsyncExceptionCount); + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + AssertEquals(0, FFinishedCount); + WaitForFinishes(1); + AssertEquals(2, FFinishingCount); + AssertEquals(1, FStartedCount); +end; + +procedure TThrottledEventTests.ReentrantTrailingRequestDeliversImmediatelyOnce; +begin + Notify; + FNotifyOnTrailing := True; + Notify; + WaitForDeliveries(3); + AssertEquals('A new request during the trailing callback is immediate', 0, FLastSuppressedCount); + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + WaitForFinishes(1); + AssertEquals('The reentrant request needs no duplicate trailing delivery', 3, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.FirstNotificationIsImmediate; +var + LBefore, LAfter : QWord; +begin + LBefore := GetTickCount64; + Notify; + LAfter := GetTickCount64; + // No message-loop pumping occurred: the leading callback is synchronous. + AssertEquals('The first event must deliver before Notify returns',1,FDeliveryTicks.Count); + AssertTrue(FDeliveryTicks[0]>=LBefore); + AssertTrue(FDeliveryTicks[0]<=LAfter); + AssertTrue('Callbacks must run on the LCL thread',FCallbacksOnMainThread); +end; + +procedure TThrottledEventTests.IntervalRetainsItsNumericMilliseconds; +begin + AssertEquals('Timer intervals require numeric duration conversion', + 120.0,FEvent.Interval.TotalMilliseconds,0.0); +end; + +procedure TThrottledEventTests.FirstNotificationAfterTrailingStartsImmediately; +begin + Notify; + PumpFor(25); + Notify; + WaitForDeliveries(2); + // The previous input has been quiet for a full interval, even though its + // trailing callback has just run. This input begins a new burst. + Notify; + AssertEquals('A new burst must lead synchronously after the trailing callback', + 3,FDeliveryTicks.Count); + AssertTrue('Callbacks must run on the LCL thread',FCallbacksOnMainThread); +end; + +procedure TThrottledEventTests.ContinuousBurstHasOnlyLeadingAndFinalDeliveries; +var + i : Integer; +begin + Notify; + AssertEquals(1,FDeliveryTicks.Count); + // Keep notifying for longer than two intervals. A periodic implementation + // would deliver in the middle of this burst instead of waiting for silence. + for i := 1 to 12 do begin + PumpFor(25); + Notify; + AssertEquals('The burst must not produce middle deliveries',1,FDeliveryTicks.Count); + end; + PumpFor(50); + AssertEquals('A short pause is not the end of the burst',1,FDeliveryTicks.Count); + WaitForDeliveries(2); + AssertTrue('The final callback must follow a full quiet period', + FDeliveryTicks[1]-FLastInputTick>=CT_TEST_INTERVAL-20); + PumpFor(2*CT_TEST_INTERVAL+30); + AssertEquals('Exactly one trailing callback is allowed',2,FDeliveryTicks.Count); + AssertTrue('Callbacks must run on the LCL thread',FCallbacksOnMainThread); +end; + +procedure TThrottledEventTests.LaterNotificationsExtendTheQuietDeadline; +const + CT_INTERVAL = 150; +begin + FEvent.Interval := TTimeSpan.FromMilliseconds(CT_INTERVAL); + Notify; + PumpFor(25); + Notify; // Starts a delayed delivery with its original deadline near 175ms. + PumpFor(90); + Notify; // The final delivery must now follow this later input instead. + PumpFor(95); + AssertEquals('A later input must extend the original deadline',1,FDeliveryTicks.Count); + WaitForDeliveries(2); + AssertTrue('Quiet time is measured from the latest notification', + FDeliveryTicks[1]-FLastInputTick>=CT_INTERVAL-20); + AssertTrue('Callbacks must run on the LCL thread',FCallbacksOnMainThread); +end; + +procedure TThrottledEventTests.SingleNotificationDoesNotDeliverTwice; +begin + Notify; + AssertEquals(1,FDeliveryTicks.Count); + PumpFor(3*CT_TEST_INTERVAL+40); + AssertEquals('An isolated leading event needs no duplicate trailing callback', + 1,FDeliveryTicks.Count); + AssertTrue('Callbacks must run on the LCL thread',FCallbacksOnMainThread); +end; + +procedure TThrottledEventTests.PostponeDoesNotStartAnotherLeadingDelivery; +begin + Notify; + FEvent.Postpone; + AssertEquals('Postponing a requested delivery must not lead immediately',1,FDeliveryTicks.Count); + PumpFor(40); + AssertEquals('Postpone must retain a full quiet interval',1,FDeliveryTicks.Count); + WaitForDeliveries(2); + PumpFor(CT_TEST_INTERVAL+30); + AssertEquals('Postpone delivers only one final notification',2,FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.PostponeRetainsElapsedProducerQuietTime; +var LElapsed: Double; +begin + Notify; + FEvent.Postpone(CT_TEST_INTERVAL-40); + LElapsed := TTimeSpan.Subtract(Now,FEvent.LastClientNotify).TotalMilliseconds; + AssertTrue('The deadline must include time the producer was already quiet', + LElapsed>=CT_TEST_INTERVAL-60); + AssertEquals('Remaining quiet time must not produce an immediate leading callback',1,FDeliveryTicks.Count); + WaitForDeliveries(2); +end; + +procedure TThrottledEventTests.ListenerCanPostponeATrailingDeliveryAgain; +begin + Notify; + PumpFor(20); + Notify; + FRearmNextDelivery := True; + WaitForDeliveries(2); + AssertTrue('A trailing callback exposes its suppressed count',FLastSuppressedCount>0); + AssertTrue('A rearmed timer must retain delayed delivery state',FEvent.SuppressedInvocations>0); + AssertEquals('An ordinary listener rearm must defer lifecycle completion',0,FFinishedCount); + WaitForDeliveries(3); + AssertTrue('The retry must still be recognizable as delayed',FLastSuppressedCount>0); + AssertEquals('A completed retry clears its pending count',0,FEvent.SuppressedInvocations); + AssertEquals(1,FFinishedCount); +end; + +procedure TThrottledEventTests.CheckPhysicalTimerReset( AMode : TThrottledEventMode); +var LExpected : Integer; +begin + FEvent.Mode := AMode; + FEvent.Interval := TTimeSpan.FromMilliseconds(200); + Notify; + PumpFor(30); + Notify; + PumpFor(130); + Notify; + PumpFor(100); + AssertEquals('Every new input must rearm the physical quiet timer: '+FTimingTrace.Text, + 0,FTimerWakeTicks.Count); + if AMode=temNotifyOnEventBurstStartAndFinished then LExpected := 2 + else LExpected := 1; + WaitForDeliveries(LExpected); + AssertTrue('The native timer starts from the final input: '+FTimingTrace.Text, + FTimerWakeTicks[0]-FLastInputTick>=180); +end; + +procedure TThrottledEventTests.LeadingBurstInputRestartsItsNativeTimer; +begin + CheckPhysicalTimerReset(temNotifyOnEventBurstStartAndFinished); +end; + +procedure TThrottledEventTests.TrailingBurstInputRestartsItsNativeTimer; +begin + CheckPhysicalTimerReset(temNotifyOnEventBurstFinished); +end; + +procedure TThrottledEventTests.PeriodicInputsRetainTheArmedTimerDeadline; +begin + FEvent.Mode := temNotifyEveryInterval; + FEvent.Interval := TTimeSpan.FromMilliseconds(500); + Notify; + PumpFor(20); + Notify; + PumpFor(430); + Notify; + PumpFor(200); + AssertTrue('Periodic mode keeps its previously armed timer: '+FTimingTrace.Text, + FTimerWakeTicks.Count>0); + AssertEquals(2,FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.ReentrantBusyListenerRetainsTheResetDeadline; +begin + FEvent.Interval := TTimeSpan.FromMilliseconds(200); + FNotifyDuringDelivery := True; + Notify; + AssertEquals('Reentrant input must be coalesced while the leading listener runs',1,FDeliveryTicks.Count); + PumpFor(120); + Notify; + PumpFor(100); + AssertEquals('A later input resets a timer armed by the busy listener: '+FTimingTrace.Text, + 0,FTimerWakeTicks.Count); + WaitForDeliveries(2); +end; + +procedure TThrottledEventTests.SingleInputFinishesItsLifecycleWithoutAnotherDelivery; +begin + Notify; + AssertEquals('Start is synchronous',1,FStartedCount); + AssertEquals(1,FDeliveryTicks.Count); + AssertTrue(FEvent.BurstActive and FNativeTimer.Enabled); + AssertEquals(0,FFinishedCount); + WaitForFinishes(1); + AssertTrue('Finish is driven by the actual owned timer',FTimerWakeTicks.Count > 0); + AssertTrue(FFinishedTick-FLastInputTick >= CT_TEST_INTERVAL-20); + AssertEquals('A single event has no duplicate ordinary callback',1,FDeliveryTicks.Count); + AssertEquals(1,FFinishingCount); + AssertFalse(FEvent.BurstActive); + PumpFor(2*CT_TEST_INTERVAL); + AssertEquals(1,FFinishedCount); +end; + +procedure TThrottledEventTests.ContinuousInputExtendsTheLifecycleDeadline; +var I: Integer; +begin + Notify; + for I := 1 to 12 do begin + PumpFor(25); + Notify; + AssertEquals('Continuous input must keep the lifecycle active',0,FFinishedCount); + end; + PumpFor(50); + AssertEquals(0,FFinishedCount); + WaitForFinishes(1); + AssertEquals(1,FStartedCount); + AssertEquals(2,FDeliveryTicks.Count); + AssertTrue(FFinishedTick-FLastInputTick >= CT_TEST_INTERVAL-20); +end; + +procedure TThrottledEventTests.FinishCandidateCanPostponeWithoutPublishingFinished; +var LDeadline: QWord; +begin + FPostponeFinish := True; + Notify; + LDeadline := GetTickCount64+1500; + while (FFinishingCount = 0) and (GetTickCount64 < LDeadline) do PumpFor(2); + AssertEquals(1,FFinishingCount); + AssertEquals('A rearmed candidate is not a finished burst',0,FFinishedCount); + AssertTrue(FEvent.BurstActive and FNativeTimer.Enabled); + PumpFor(40); + AssertEquals(0,FFinishedCount); + WaitForFinishes(1); + AssertEquals(2,FFinishingCount); + AssertEquals('Postpone does not publish another start',1,FStartedCount); +end; + +procedure TThrottledEventTests.StartedListenerCanNotifyWithoutDuplicatingTheStart; +begin + FNotifyOnStart := True; + Notify; + AssertEquals(1,FStartedCount); + AssertEquals(1,FDeliveryTicks.Count); + WaitForFinishes(1); + AssertEquals(2,FDeliveryTicks.Count); + AssertEquals(1,FStartedCount); +end; + +procedure TThrottledEventTests.FinishedListenerCanStartTheNextBurst; +begin + FNotifyOnFinish := True; + Notify; + WaitForFinishes(1); + AssertEquals(2,FStartedCount); + AssertEquals(2,FDeliveryTicks.Count); + AssertTrue('The finished listener starts a separately timed burst',FEvent.BurstActive and FNativeTimer.Enabled); + WaitForFinishes(2); + AssertFalse(FEvent.BurstActive); + AssertEquals('Both isolated bursts deliver only their leading callback',2,FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.DisposingActiveBurstCancelsItsLifecycleTimer; +begin + Notify; + FreeAndNil(FEvent); + FNativeTimer := nil; + PumpFor(2*CT_TEST_INTERVAL+30); + AssertEquals(1,FStartedCount); + AssertEquals('Disposal must not deliver a stale lifecycle callback',0,FFinishedCount); + AssertEquals(1,FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.NonBurstModesDoNotPublishBurstLifecycle; +begin + FEvent.Mode := temNone; + Notify; + Notify; + AssertEquals(2,FDeliveryTicks.Count); + FEvent.Mode := temNotifyEveryInterval; + Notify; + WaitForDeliveries(3); + AssertEquals(0,FStartedCount); + AssertEquals(0,FFinishingCount); + AssertEquals(0,FFinishedCount); + AssertFalse(FEvent.BurstActive); +end; + +procedure TThrottledEventTests.TrailingOnlyModePublishesOneStartAndFinish; +begin + FEvent.Mode := temNotifyOnEventBurstFinished; + Notify; + AssertEquals(1,FStartedCount); + AssertEquals(0,FDeliveryTicks.Count); + PumpFor(25); + Notify; + WaitForFinishes(1); + AssertEquals(1,FDeliveryTicks.Count); + AssertEquals(1,FStartedCount); +end; + +procedure TThrottledEventTests.FiveSecondInputsAt4900MsRetainOneOwnedBurst; +var I: Integer; +begin + FEvent.Interval := TTimeSpan.FromSeconds(5); + Notify; + for I := 1 to 2 do begin + PumpFor(4900); + AssertEquals('The next near-deadline input still belongs to this burst: ' + + FTimingTrace.Text, 1, FDeliveryTicks.Count); + AssertTrue(FEvent.Waiting); + AssertEquals('The physical timer must not wake before the latest quiet deadline: ' + + FTimingTrace.Text, 0, FTimerWakeTicks.Count); + Notify; + AssertEquals(1, FStartedCount); + AssertEquals(0, FFinishedCount); + AssertEquals('Waiting must not flicker between these inputs', 1, FWaitingStates.Count); + end; + PumpFor(4900); + AssertEquals(1, FDeliveryTicks.Count); + AssertTrue(FEvent.Waiting); + WaitForDeliveries(2); + WaitForFinishes(1); + AssertTrue('The final delivery follows a real five-second quiet interval: ' + + FTimingTrace.Text, FDeliveryTicks[1] - FLastInputTick >= 4980); + AssertEquals(2, FWaitingStates.Count); + AssertFalse(FEvent.Waiting); + PumpFor(150); + AssertEquals('There is exactly one leading and one final delivery', 2, FDeliveryTicks.Count); +end; + +procedure TThrottledEventTests.CapturedLateTimerAfterFreshInputCannotFinishWaiting; +var LCapturedHandler: TNotifyEvent; +begin + FEvent.Interval := TTimeSpan.FromSeconds(5); + LCapturedHandler := FOriginalTimerHandler; + Notify; + PumpFor(4900); + Notify; + // Model an already posted WM_TIMER from the old deadline. Invoke the actual + // saved component handler after that deadline, not a copy of its condition. + PumpFor(200); + AssertEquals('Resetting the owned native timer prevents an ordinary old wake: ' + + FTimingTrace.Text, 0, FTimerWakeTicks.Count); + FTimingTrace.Add('Deliver captured real timer handler after a fresh input'); + LCapturedHandler(FNativeTimer); + AssertEquals('A stale timer cannot publish pending work early', 1, FDeliveryTicks.Count); + AssertEquals(0, FFinishedCount); + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + AssertEquals('The stale callback cannot publish Waiting=False', 1, FWaitingStates.Count); + + FEvent.Cancel; + LCapturedHandler(FNativeTimer); + AssertEquals('An already queued callback after Cancel cannot deliver', 1, FDeliveryTicks.Count); + AssertEquals('Cancellation is not a quiet finish', 0, FFinishedCount); + AssertFalse(FEvent.Waiting or FNativeTimer.Enabled); + Notify; + AssertEquals('The next independent input still leads immediately', 2, FDeliveryTicks.Count); + FEvent.Cancel; +end; + +procedure TThrottledEventTests.SlowLeadingListenerAcceptsSynchronizedUiInputsWithoutEarlyFinish; +var LProducer: TSynchronizedThrottleInputs; +begin + FEvent.Interval := TTimeSpan.FromSeconds(5); + FSlowLeadingEntered := TEvent.Create(nil, True, False, ''); + LProducer := TSynchronizedThrottleInputs.Create(FSlowLeadingEntered, Notify); + try + LProducer.Start; + FSlowLeadingMilliseconds := 5500; + Notify; + LProducer.Stop; + AssertFalse('The real worker must reach the leading-listener gate', LProducer.StartTimedOut); + AssertTrue('A slow leading callback accepted repeated real worker inputs', LProducer.InputCount >= 20); + AssertFalse('Every Synchronize input executes on the UI thread', LProducer.WrongThread); + AssertEquals('Inputs during the slow listener stay coalesced', 1, FDeliveryTicks.Count); + AssertEquals('No intermediate Waiting=False may be published', 1, FWaitingStates.Count); + AssertTrue(FEvent.Waiting and FNativeTimer.Enabled); + PumpFor(4800); + AssertEquals('A stale first deadline cannot end the refreshed burst: ' + + FTimingTrace.Text, 1, FDeliveryTicks.Count); + AssertTrue(FEvent.Waiting); + WaitForDeliveries(2); + WaitForFinishes(1); + AssertTrue('Quiet is measured from the last actual synchronized input: ' + + FTimingTrace.Text, FDeliveryTicks[1] - FLastInputTick >= 4980); + AssertEquals(2, FWaitingStates.Count); + AssertTrue(FCallbacksOnMainThread); + AssertFalse(FEvent.Waiting); + finally + LProducer.Free; + end; +end; + +initialization + RegisterTest(TThrottledEventTests); + RegisterTest(TThreadNotifyTests); + +end. diff --git a/src/tests/experimental/framework/UConcurrency.Tests.pas b/src/tests/experimental/framework/UConcurrency.Tests.pas new file mode 100644 index 000000000..3c6fba70b --- /dev/null +++ b/src/tests/experimental/framework/UConcurrency.Tests.pas @@ -0,0 +1,724 @@ +unit UConcurrency.Tests; + +{ Copyright © 2026 Herman Schoenfeld <https://www.sphere10.com> + Distributed under the MIT software license, see the accompanying LICENSE. } + +{$MODE DELPHI}{$H+} + +interface + +uses Classes, SysUtils, SyncObjs, Generics.Collections, fpcunit, testregistry, + UConcurrency; + +type + TLockAction = (laRead, laWrite, laUpgrade, laReleaseRead, laReleaseWrite, + laReleaseUpgrade, laCancellableRead, laUpgradeThenWrite); + + TLockWorker = class(TThread) + private + FLock: TReadWriteLock; + FAction: TLockAction; + FTimeout: Integer; + protected + procedure Execute; override; + public + Entered, ReleaseGate, Done: TEvent; + Acquired: Boolean; + ErrorClass, ErrorMessage: String; + Elapsed: UInt64; + OwnReadCount, OwnWriteCount, OwnUpgradeCount: Integer; + constructor Create(ALock: TReadWriteLock; AAction: TLockAction; ATimeout: Integer); + destructor Destroy; override; + procedure RequestStop; + end; + + TReadWriteLockTests = class(TTestCase) + private + FLock: TReadWriteLock; + FWorkers: TList<TLockWorker>; + function Worker(AAction: TLockAction; ATimeout: Integer = 2500): TLockWorker; + procedure Await(AEvent: TEvent; const ADescription: String); + procedure WaitCount(AKind, ACount: Integer); + procedure Completed(AWorker: TLockWorker); + procedure Release(AWorker: TLockWorker); + procedure ExpectRecursion(AAction: TLockAction); + procedure RecursivePolicy; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure DefaultPolicyAndEmptyCounts; + procedure DifferentThreadsReadConcurrently; + procedure WriterExcludesOtherReadersWritersAndUpgraders; + procedure WaitingWriterPrecedesNewReader; + procedure WaitingWriterPrecedesNewUpgrader; + procedure RecursiveReadsCountOneReader; + procedure RecursiveWriterCanReadWriteAndUpgrade; + procedure NoRecursionRejectsRepeatedAcquisition; + procedure OrdinaryReadCannotPromoteUnderEitherPolicy; + procedure NoRecursionUpgradeableReadThenWriteIsRejected; + procedure UpgradeableReadPromotesWithoutOtherReaders; + procedure RecursiveUpgradeableWithExplicitReadCanPromote; + procedure UpgradeWaitsForOtherReaderAndRetainsOwnershipOnTimeout; + procedure WaitingUpgradePromotesWhenOtherReaderReleases; + procedure OnlyOneUpgradeableOwnerAndReadersCanShare; + procedure WriteToReadDowngradeRetainsReader; + procedure UpgradeableToReadDowngradeRetainsReader; + procedure FiniteTimeoutIsBoundedAndRemovesWaiter; + procedure ZeroTimeoutDoesNotRegisterWaiter; + procedure InfiniteTimeoutWakesOnRelease; + procedure TimedOutWriterReleasesWaitingReaders; + procedure WrongThreadCannotReleaseAnyMode; + procedure UnmatchedReleaseDoesNotChangeOwnership; + procedure InvalidTimeoutDoesNotAcquire; + procedure DisposeRejectsActiveOwnershipAndCanBeRetried; + procedure DisposeRejectsWaiterAndWaiterLaterCompletes; + procedure DisposedLockRejectsAcquisitions; + procedure ExceptionCleanupReleasesOnlyOwnedAcquisitions; + procedure PromotedWriteExceptionRetainsSurroundingUpgradeableLock; + procedure HeldAndRecursivePropertiesAreThreadSpecific; + procedure FiniteWaitPermitsCooperativeCancellation; + end; + +implementation + +function Tick: UInt64; +begin + Result := GetTickCount64; +end; + +constructor TLockWorker.Create(ALock: TReadWriteLock; AAction: TLockAction; ATimeout: Integer); +begin + inherited Create(True); + FLock := ALock; + FAction := AAction; + FTimeout := ATimeout; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseGate := TEvent.Create(nil, True, False, ''); + Done := TEvent.Create(nil, True, False, ''); + Start; +end; + +destructor TLockWorker.Destroy; +begin + RequestStop; + WaitFor; + Done.Free; + ReleaseGate.Free; + Entered.Free; + inherited; +end; + +procedure TLockWorker.RequestStop; +begin + Terminate; + ReleaseGate.SetEvent; +end; + +procedure TLockWorker.Execute; +var LStarted: UInt64; +begin + LStarted := Tick; + try + try + case FAction of + laRead: Acquired := FLock.TryAcquireReadLock(FTimeout); + laWrite: Acquired := FLock.TryAcquireWriteLock(FTimeout); + laUpgrade: Acquired := FLock.TryAcquireUpgradeableReadLock(FTimeout); + laReleaseRead: FLock.ReleaseReadLock; + laReleaseWrite: FLock.ReleaseWriteLock; + laReleaseUpgrade: FLock.ReleaseUpgradeableReadLock; + laCancellableRead: + while not Terminated do + if FLock.TryAcquireReadLock(FTimeout) then begin + FLock.ReleaseReadLock; + Break; + end; + laUpgradeThenWrite: begin + FLock.AcquireUpgradeableReadLock; + try + Entered.SetEvent; + if ReleaseGate.WaitFor(4000) <> wrSignaled then + raise Exception.Create('Promotion start gate timed out.'); + Acquired := FLock.TryAcquireWriteLock(FTimeout); + if Acquired then FLock.ReleaseWriteLock; + finally FLock.ReleaseUpgradeableReadLock; end; + end; + end; + if Acquired and (FAction in [laRead, laWrite, laUpgrade]) then begin + try + OwnReadCount := FLock.RecursiveReadCount; + OwnWriteCount := FLock.RecursiveWriteCount; + OwnUpgradeCount := FLock.RecursiveUpgradeCount; + Entered.SetEvent; + if ReleaseGate.WaitFor(4000) <> wrSignaled then + raise Exception.Create('Test release gate timed out.'); + finally + case FAction of + laRead: FLock.ReleaseReadLock; + laWrite: FLock.ReleaseWriteLock; + laUpgrade: FLock.ReleaseUpgradeableReadLock; + end; + end; + end; + except + on E: Exception do begin + ErrorClass := E.ClassName; + ErrorMessage := E.Message; + end; + end; + finally + Elapsed := Tick - LStarted; + Done.SetEvent; + end; +end; + +procedure TReadWriteLockTests.SetUp; +begin + inherited; + FLock := TReadWriteLock.Create; + FWorkers := TList<TLockWorker>.Create; +end; + +procedure TReadWriteLockTests.TearDown; +var LWorker: TLockWorker; +begin + for LWorker in FWorkers do LWorker.RequestStop; + // Release test-thread ownership before joining blocked workers after a failure. + while FLock.IsWriteLockHeld do FLock.ReleaseWriteLock; + while FLock.IsUpgradeableReadLockHeld do FLock.ReleaseUpgradeableReadLock; + while FLock.IsReadLockHeld do FLock.ReleaseReadLock; + for LWorker in FWorkers do LWorker.Free; + FWorkers.Free; + FLock.Free; + inherited; +end; + +procedure TReadWriteLockTests.RecursivePolicy; +begin + FLock.Free; + FLock := TReadWriteLock.Create(lrpSupportsRecursion); +end; + +function TReadWriteLockTests.Worker(AAction: TLockAction; ATimeout: Integer): TLockWorker; +begin + Result := TLockWorker.Create(FLock, AAction, ATimeout); + FWorkers.Add(Result); +end; + +procedure TReadWriteLockTests.Await(AEvent: TEvent; const ADescription: String); +begin + AssertEquals(ADescription, Ord(wrSignaled), Ord(AEvent.WaitFor(3000))); +end; + +procedure TReadWriteLockTests.WaitCount(AKind, ACount: Integer); +var LStarted: UInt64; LCount: Integer; +begin + LStarted := Tick; + repeat + case AKind of + 0: LCount := FLock.WaitingReadCount; + 1: LCount := FLock.WaitingWriteCount; + else LCount := FLock.WaitingUpgradeCount; + end; + if LCount = ACount then Exit; + Sleep(1); + until Tick - LStarted >= 2000; + AssertEquals('Actual registered waiter count', ACount, LCount); +end; + +procedure TReadWriteLockTests.Completed(AWorker: TLockWorker); +begin + Await(AWorker.Done, 'Worker completed'); + AssertEquals(AWorker.ErrorMessage, '', AWorker.ErrorClass); +end; + +procedure TReadWriteLockTests.Release(AWorker: TLockWorker); +begin + AWorker.ReleaseGate.SetEvent; + Completed(AWorker); +end; + +procedure TReadWriteLockTests.ExpectRecursion(AAction: TLockAction); +begin + try + case AAction of + laRead: FLock.TryAcquireReadLock(0); + laWrite: FLock.TryAcquireWriteLock(0); + laUpgrade: FLock.TryAcquireUpgradeableReadLock(0); + end; + Fail('This recursion/promotion should be rejected.'); + except on E: ELockRecursionException do ; end; +end; + +procedure TReadWriteLockTests.DefaultPolicyAndEmptyCounts; +begin + AssertEquals(Ord(lrpNoRecursion), Ord(FLock.RecursionPolicy)); + AssertFalse(FLock.IsReadLockHeld or FLock.IsWriteLockHeld or FLock.IsUpgradeableReadLockHeld); + AssertEquals(0, FLock.CurrentReadCount); + AssertEquals(0, FLock.WaitingReadCount + FLock.WaitingWriteCount + FLock.WaitingUpgradeCount); +end; + +procedure TReadWriteLockTests.DifferentThreadsReadConcurrently; +var LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LReader := Worker(laRead); + Await(LReader.Entered, 'Second reader acquires while first still owns'); + AssertEquals(2, FLock.CurrentReadCount); + AssertEquals(1, FLock.RecursiveReadCount); + FLock.ReleaseReadLock; + Release(LReader); + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TReadWriteLockTests.WriterExcludesOtherReadersWritersAndUpgraders; +var LAction: TLockAction; LWorker: TLockWorker; +begin + FLock.AcquireWriteLock; + for LAction := laRead to laUpgrade do begin + LWorker := Worker(LAction, 0); + Completed(LWorker); + AssertFalse('Writer excludes other owners', LWorker.Acquired); + end; + AssertTrue(FLock.IsWriteLockHeld); + AssertEquals(0, FLock.WaitingReadCount + FLock.WaitingWriteCount + FLock.WaitingUpgradeCount); + FLock.ReleaseWriteLock; +end; + +procedure TReadWriteLockTests.WaitingWriterPrecedesNewReader; +var LWriter, LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite); + WaitCount(1, 1); + LReader := Worker(laRead); + WaitCount(0, 1); + FLock.ReleaseReadLock; + Await(LWriter.Entered, 'Waiting writer wins admission'); + AssertEquals(Ord(wrTimeout), Ord(LReader.Entered.WaitFor(0))); + Release(LWriter); + Await(LReader.Entered, 'Reader follows completed writer'); + Release(LReader); +end; + +procedure TReadWriteLockTests.WaitingWriterPrecedesNewUpgrader; +var LWriter, LUpgrade: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite); + WaitCount(1, 1); + LUpgrade := Worker(laUpgrade); + WaitCount(2, 1); + FLock.ReleaseReadLock; + Await(LWriter.Entered, 'Writer precedes new upgrader'); + AssertEquals(Ord(wrTimeout), Ord(LUpgrade.Entered.WaitFor(0))); + Release(LWriter); + Await(LUpgrade.Entered, 'Upgrader follows writer'); + Release(LUpgrade); +end; + +procedure TReadWriteLockTests.RecursiveReadsCountOneReader; +begin + RecursivePolicy; + FLock.AcquireReadLock; + FLock.AcquireReadLock; + AssertEquals(2, FLock.RecursiveReadCount); + AssertEquals(1, FLock.CurrentReadCount); + FLock.ReleaseReadLock; + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TReadWriteLockTests.RecursiveWriterCanReadWriteAndUpgrade; +begin + RecursivePolicy; + FLock.AcquireWriteLock; + FLock.AcquireReadLock; + FLock.AcquireWriteLock; + FLock.AcquireUpgradeableReadLock; + FLock.AcquireUpgradeableReadLock; + AssertEquals(2, FLock.RecursiveWriteCount); + AssertEquals(1, FLock.RecursiveReadCount); + AssertEquals(2, FLock.RecursiveUpgradeCount); + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsWriteLockHeld); + FLock.ReleaseReadLock; + FLock.ReleaseUpgradeableReadLock; + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TReadWriteLockTests.NoRecursionRejectsRepeatedAcquisition; +begin + FLock.AcquireReadLock; + ExpectRecursion(laRead); + AssertEquals(1, FLock.RecursiveReadCount); + FLock.ReleaseReadLock; + FLock.AcquireWriteLock; + ExpectRecursion(laWrite); + ExpectRecursion(laRead); + ExpectRecursion(laUpgrade); + AssertEquals(1, FLock.RecursiveWriteCount); + FLock.ReleaseWriteLock; + FLock.AcquireUpgradeableReadLock; + ExpectRecursion(laUpgrade); + AssertEquals(1, FLock.RecursiveUpgradeCount); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TReadWriteLockTests.OrdinaryReadCannotPromoteUnderEitherPolicy; +var LPolicy: TLockRecursionPolicy; +begin + for LPolicy := lrpNoRecursion to lrpSupportsRecursion do begin + FLock.Free; + FLock := TReadWriteLock.Create(LPolicy); + FLock.AcquireReadLock; + ExpectRecursion(laWrite); + ExpectRecursion(laUpgrade); + AssertEquals(1, FLock.RecursiveReadCount); + FLock.ReleaseReadLock; + end; +end; + +procedure TReadWriteLockTests.NoRecursionUpgradeableReadThenWriteIsRejected; +begin + FLock.AcquireUpgradeableReadLock; + FLock.AcquireReadLock; + ExpectRecursion(laWrite); + FLock.ReleaseReadLock; + AssertTrue(FLock.TryAcquireWriteLock(0)); + FLock.ReleaseWriteLock; + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TReadWriteLockTests.UpgradeableReadPromotesWithoutOtherReaders; +begin + FLock.AcquireUpgradeableReadLock; + AssertEquals(0, FLock.CurrentReadCount); + AssertTrue(FLock.TryAcquireWriteLock(0)); + AssertTrue(FLock.IsWriteLockHeld and FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TReadWriteLockTests.RecursiveUpgradeableWithExplicitReadCanPromote; +begin + RecursivePolicy; + FLock.AcquireUpgradeableReadLock; + FLock.AcquireReadLock; + AssertTrue(FLock.TryAcquireWriteLock(0)); + FLock.ReleaseUpgradeableReadLock; + AssertTrue(FLock.IsWriteLockHeld and FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + FLock.ReleaseWriteLock; +end; + +procedure TReadWriteLockTests.UpgradeWaitsForOtherReaderAndRetainsOwnershipOnTimeout; +var LReader: TLockWorker; +begin + FLock.AcquireUpgradeableReadLock; + LReader := Worker(laRead); + Await(LReader.Entered, 'Reader shares upgradeable ownership'); + AssertFalse(FLock.TryAcquireWriteLock(70)); + AssertTrue(FLock.IsUpgradeableReadLockHeld); + AssertFalse(FLock.IsWriteLockHeld); + AssertEquals(0, FLock.WaitingWriteCount); + Release(LReader); + AssertTrue(FLock.TryAcquireWriteLock(2000)); + FLock.ReleaseWriteLock; + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TReadWriteLockTests.OnlyOneUpgradeableOwnerAndReadersCanShare; +var LUpgrade, LReader: TLockWorker; +begin + FLock.AcquireUpgradeableReadLock; + LUpgrade := Worker(laUpgrade, 60); + LReader := Worker(laRead); + Await(LReader.Entered, 'Reader shares first upgrader'); + Completed(LUpgrade); + AssertFalse(LUpgrade.Acquired); + Release(LReader); + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TReadWriteLockTests.WaitingUpgradePromotesWhenOtherReaderReleases; +var LUpgrade, LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LUpgrade := Worker(laUpgradeThenWrite); + Await(LUpgrade.Entered, 'Upgradeable read shares existing reader'); + LUpgrade.ReleaseGate.SetEvent; + WaitCount(1, 1); + LReader := Worker(laRead); + WaitCount(0, 1); + AssertEquals(Ord(wrTimeout), Ord(LUpgrade.Done.WaitFor(0))); + FLock.ReleaseReadLock; + Completed(LUpgrade); + AssertTrue('Pending promotion succeeds after last other reader releases its lock', LUpgrade.Acquired); + Await(LReader.Entered, 'New reader follows completed promotion'); + Release(LReader); +end; + +procedure TReadWriteLockTests.WriteToReadDowngradeRetainsReader; +var LWriter, LReader: TLockWorker; +begin + RecursivePolicy; + FLock.AcquireWriteLock; + FLock.AcquireReadLock; + FLock.ReleaseWriteLock; + AssertTrue(FLock.IsReadLockHeld); + LWriter := Worker(laWrite, 0); + Completed(LWriter); + AssertFalse(LWriter.Acquired); + LReader := Worker(laRead); + Await(LReader.Entered, 'Downgrade permits another reader'); + Release(LReader); + FLock.ReleaseReadLock; +end; + +procedure TReadWriteLockTests.UpgradeableToReadDowngradeRetainsReader; +begin + FLock.AcquireUpgradeableReadLock; + FLock.AcquireReadLock; + FLock.ReleaseUpgradeableReadLock; + AssertTrue(FLock.IsReadLockHeld); + ExpectRecursion(laUpgrade); + FLock.ReleaseReadLock; +end; + +procedure TReadWriteLockTests.FiniteTimeoutIsBoundedAndRemovesWaiter; +var LReader: TLockWorker; +begin + FLock.AcquireWriteLock; + LReader := Worker(laRead, 90); + Completed(LReader); + AssertFalse(LReader.Acquired); + AssertTrue('Actually waited', LReader.Elapsed >= 75); + AssertTrue('Bounded timeout', LReader.Elapsed < 1000); + AssertEquals(0, FLock.WaitingReadCount); + FLock.ReleaseWriteLock; +end; + +procedure TReadWriteLockTests.ZeroTimeoutDoesNotRegisterWaiter; +var LWorker: TLockWorker; +begin + FLock.AcquireReadLock; + LWorker := Worker(laWrite, 0); + Completed(LWorker); + AssertFalse(LWorker.Acquired); + AssertTrue(LWorker.Elapsed < 500); + AssertEquals(0, FLock.WaitingWriteCount); + FLock.ReleaseReadLock; +end; + +procedure TReadWriteLockTests.InfiniteTimeoutWakesOnRelease; +var LReader: TLockWorker; +begin + FLock.AcquireWriteLock; + LReader := Worker(laRead, -1); + WaitCount(0, 1); + AssertEquals(Ord(wrTimeout), Ord(LReader.Done.WaitFor(0))); + FLock.ReleaseWriteLock; + Await(LReader.Entered, 'Infinite waiter wakes on release'); + Release(LReader); +end; + +procedure TReadWriteLockTests.TimedOutWriterReleasesWaitingReaders; +var LWriter, LReader: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite, 160); + WaitCount(1, 1); + LReader := Worker(laRead); + WaitCount(0, 1); + Completed(LWriter); + AssertFalse(LWriter.Acquired); + Await(LReader.Entered, 'Writer timeout releases reader while original read remains held'); + AssertTrue(FLock.IsReadLockHeld); + Release(LReader); + FLock.ReleaseReadLock; +end; + +procedure TReadWriteLockTests.WrongThreadCannotReleaseAnyMode; +var LAction: TLockAction; LWorker: TLockWorker; +begin + for LAction := laReleaseRead to laReleaseUpgrade do begin + case LAction of + laReleaseRead: FLock.AcquireReadLock; + laReleaseWrite: FLock.AcquireWriteLock; + laReleaseUpgrade: FLock.AcquireUpgradeableReadLock; + end; + LWorker := Worker(LAction); + Await(LWorker.Done, 'Wrong-thread release completed'); + AssertEquals('ESynchronizationLockException', LWorker.ErrorClass); + case LAction of + laReleaseRead: begin + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + end; + laReleaseWrite: begin + AssertTrue(FLock.IsWriteLockHeld); + FLock.ReleaseWriteLock; + end; + laReleaseUpgrade: begin + AssertTrue(FLock.IsUpgradeableReadLockHeld); + FLock.ReleaseUpgradeableReadLock; + end; + end; + end; +end; + +procedure TReadWriteLockTests.UnmatchedReleaseDoesNotChangeOwnership; +begin + FLock.AcquireReadLock; + try FLock.ReleaseWriteLock; Fail('Unmatched release'); + except on E: ESynchronizationLockException do ; end; + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + try FLock.ReleaseReadLock; Fail('Extra release'); + except on E: ESynchronizationLockException do ; end; + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TReadWriteLockTests.InvalidTimeoutDoesNotAcquire; +begin + try FLock.TryAcquireReadLock(-2); Fail('Invalid read timeout'); + except on E: EArgumentOutOfRangeException do ; end; + try FLock.TryAcquireWriteLock(-2); Fail('Invalid write timeout'); + except on E: EArgumentOutOfRangeException do ; end; + try FLock.TryAcquireUpgradeableReadLock(-2); Fail('Invalid upgrade timeout'); + except on E: EArgumentOutOfRangeException do ; end; + AssertEquals(0, FLock.CurrentReadCount); +end; + +procedure TReadWriteLockTests.DisposeRejectsActiveOwnershipAndCanBeRetried; +begin + FLock.AcquireReadLock; + try FLock.Dispose; Fail('Dispose with owner'); + except on E: ESynchronizationLockException do ; end; + AssertTrue(FLock.IsReadLockHeld); + FLock.ReleaseReadLock; + FLock.Dispose; + FLock.Dispose; + try FLock.AcquireReadLock; Fail('Disposed acquisition'); + except on E: ELockDisposedException do ; end; +end; + +procedure TReadWriteLockTests.DisposeRejectsWaiterAndWaiterLaterCompletes; +var LWriter: TLockWorker; +begin + FLock.AcquireReadLock; + LWriter := Worker(laWrite); + WaitCount(1, 1); + try FLock.Dispose; Fail('Dispose with waiter'); + except on E: ESynchronizationLockException do ; end; + AssertEquals(1, FLock.WaitingWriteCount); + FLock.ReleaseReadLock; + Await(LWriter.Entered, 'Rejected dispose leaves waiter usable'); + Release(LWriter); + FLock.Dispose; +end; + +procedure TReadWriteLockTests.DisposedLockRejectsAcquisitions; +begin + FLock.Dispose; + try FLock.AcquireReadLock; Fail('Disposed read'); + except on E: ELockDisposedException do ; end; + try FLock.AcquireWriteLock; Fail('Disposed write'); + except on E: ELockDisposedException do ; end; + try FLock.AcquireUpgradeableReadLock; Fail('Disposed upgrade'); + except on E: ELockDisposedException do ; end; +end; + +procedure TReadWriteLockTests.ExceptionCleanupReleasesOnlyOwnedAcquisitions; +var LWriter: TLockWorker; +begin + RecursivePolicy; + FLock.AcquireWriteLock; + try + try + FLock.AcquireWriteLock; + try + FLock.AcquireReadLock; + try raise EAbort.Create('Expected protected work failure'); + finally FLock.ReleaseReadLock; end; + finally FLock.ReleaseWriteLock; end; + except on E: EAbort do ; end; + AssertTrue(FLock.IsWriteLockHeld); + AssertEquals(1, FLock.RecursiveWriteCount); + finally FLock.ReleaseWriteLock; end; + LWriter := Worker(laWrite); + Await(LWriter.Entered, 'Exception cleanup released ownership'); + Release(LWriter); +end; + +procedure TReadWriteLockTests.HeldAndRecursivePropertiesAreThreadSpecific; +var LReader: TLockWorker; +begin + RecursivePolicy; + FLock.AcquireUpgradeableReadLock; + FLock.AcquireUpgradeableReadLock; + LReader := Worker(laRead); + Await(LReader.Entered, 'Independent reader'); + AssertEquals(1, LReader.OwnReadCount); + AssertEquals(0, LReader.OwnWriteCount); + AssertEquals(0, LReader.OwnUpgradeCount); + AssertFalse(FLock.IsReadLockHeld); + AssertEquals(2, FLock.RecursiveUpgradeCount); + Release(LReader); + FLock.ReleaseUpgradeableReadLock; + FLock.ReleaseUpgradeableReadLock; +end; + +procedure TReadWriteLockTests.FiniteWaitPermitsCooperativeCancellation; +var LReader: TLockWorker; LStarted: UInt64; +begin + FLock.AcquireWriteLock; + LReader := Worker(laCancellableRead, 35); + WaitCount(0, 1); + LStarted := Tick; + LReader.RequestStop; + Completed(LReader); + AssertTrue('Cooperative stop completes after finite wait', Tick - LStarted < 700); + AssertTrue('Cancellation retains owner', FLock.IsWriteLockHeld); + FLock.ReleaseWriteLock; +end; + +procedure TReadWriteLockTests.PromotedWriteExceptionRetainsSurroundingUpgradeableLock; +var LWriter, LReader: TLockWorker; +begin + FLock.AcquireUpgradeableReadLock; + try + try + FLock.AcquireWriteLock; + try + raise EAbort.Create('Expected promoted-write failure'); + finally + FLock.ReleaseWriteLock; + end; + except on E: EAbort do ; end; + AssertFalse('Only write ownership was released', FLock.IsWriteLockHeld); + AssertTrue('Surrounding upgradeable ownership survives', FLock.IsUpgradeableReadLockHeld); + AssertEquals(1, FLock.RecursiveUpgradeCount); + LWriter := Worker(laWrite, 0); + Completed(LWriter); + AssertFalse('The remaining upgrader excludes another writer', LWriter.Acquired); + LReader := Worker(laRead); + Await(LReader.Entered, 'Readers can share the remaining upgradeable lock'); + Release(LReader); + finally + FLock.ReleaseUpgradeableReadLock; + end; + LWriter := Worker(laWrite); + Await(LWriter.Entered, 'Final typed release frees the surrounding ownership'); + Release(LWriter); +end; + +initialization + RegisterTest(TReadWriteLockTests); +end. diff --git a/src/tests/experimental/framework/UFramework.UI.Integration.Tests.pas b/src/tests/experimental/framework/UFramework.UI.Integration.Tests.pas new file mode 100644 index 000000000..90087fbb3 --- /dev/null +++ b/src/tests/experimental/framework/UFramework.UI.Integration.Tests.pas @@ -0,0 +1,387 @@ +unit UFramework.UI.Integration.Tests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see LICENSE. + Shared UI regressions mirrored in the embedded PascalCoin framework suite. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, SyncObjs, Forms, Controls, ExtCtrls, Generics.Collections, + fpcunit, testregistry, UCommon, UCommon.UI, UCommon.Data, UVisualGrid; + +type + TFrameworkUIIntegrationTests = class(TTestCase) + private + FCompleted: Integer; + FCallbackThread: TThreadID; + procedure OnCompleted(Sender: TObject); + procedure PumpUntil(AValue: Integer); + published + procedure MillisecondDurationIsNumeric; + procedure EmbeddedFormsHideBeforeDetaching; + procedure GridCompletesOnMainThread; + procedure GridDestructionJoinsBlockedFetch; + procedure GridCoalescesRefreshDuringFetch; + procedure GridSourceReplacementWaitsForFetch; + procedure GridRecoversAfterFetchError; + procedure ConcurrentDataRowSchemasRemainConsistent; + end; + +implementation + +uses Windows, Types; + +type + TTestSource = class(TCustomDataSource<Integer>) + public + Entered, ReleaseFetch: TEvent; + FetchCount: Integer; + BlockFetch: Boolean; + FailFetch: Boolean; + Value: Integer; + constructor Create; + destructor Destroy; override; + function GetColumns: TDataColumns; override; + function GetItemField(constref AItem: Integer; const ABindingName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + end; + + TReleaseThread = class(TThread) + private + FSource: TTestSource; + protected + procedure Execute; override; + public + constructor Create(ASource: TTestSource); + end; + + TSchemaThread = class(TThread) + private + FStart: TEvent; + FNumber: Integer; + protected + procedure Execute; override; + public + ErrorMessage: String; + constructor Create(AStart: TEvent; ANumber: Integer); + end; + +constructor TTestSource.Create; +begin + inherited Create(nil); + Value := 42; + Entered := TEvent.Create(nil, true, false, ''); + ReleaseFetch := TEvent.Create(nil, true, false, ''); +end; + +destructor TTestSource.Destroy; +begin + Entered.Free; + ReleaseFetch.Free; + inherited Destroy; +end; + +function TTestSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Value', true)); +end; + +function TTestSource.GetItemField(constref AItem: Integer; const ABindingName: String): Variant; +begin + Result := AItem; +end; + +procedure TTestSource.FetchAll(const AContainer: TList<Integer>); +begin + InterlockedIncrement(FetchCount); + Entered.SetEvent; + if BlockFetch and (ReleaseFetch.WaitFor(5000) <> wrSignaled) then + raise Exception.Create('Fixture release timed out'); + if FailFetch then raise Exception.Create('Fixture fetch error'); + AContainer.Add(Value); +end; + +constructor TReleaseThread.Create(ASource: TTestSource); +begin + inherited Create(true); + FSource := ASource; + FreeOnTerminate := false; +end; + +procedure TReleaseThread.Execute; +begin + Sleep(75); + FSource.ReleaseFetch.SetEvent; +end; + +constructor TSchemaThread.Create(AStart: TEvent; ANumber: Integer); +begin + inherited Create(true); + FreeOnTerminate := false; + FStart := AStart; + FNumber := ANumber; +end; + +procedure TSchemaThread.Execute; +var + row: Variant; + i: Integer; +begin + try + if FStart.WaitFor(5000) <> wrSignaled then raise Exception.Create('Schema gate timed out'); + for i := 0 to 63 do begin + row := TDataRow.New(-2000000 - FNumber * 1000 - i, + TDataColumns.Create(TDataColumn.From('Value', true))); + row.Value := i; + row.__KEY := FNumber; + if (Integer(row.Value) <> i) or (Integer(row.__KEY) <> FNumber) then + raise Exception.Create('Concurrent schema returned an inconsistent row'); + end; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; +end; + +procedure TFrameworkUIIntegrationTests.OnCompleted(Sender: TObject); +begin + Inc(FCompleted); + FCallbackThread := GetCurrentThreadID; +end; + +procedure TFrameworkUIIntegrationTests.PumpUntil(AValue: Integer); +var + deadline: QWord; +begin + deadline := GetTickCount64 + 5000; + while (FCompleted < AValue) and (GetTickCount64 < deadline) do begin + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(5); + end; + AssertTrue('Fetch completion timed out', FCompleted >= AValue); +end; + +procedure TFrameworkUIIntegrationTests.MillisecondDurationIsNumeric; +begin + AssertEquals(10000.0, TTimeSpan.FromSeconds(10).TotalMilliseconds, 0.0); + AssertEquals(-250.0, TTimeSpan.FromMilliseconds(-250).TotalMilliseconds, 0.0); +end; + +procedure TFrameworkUIIntegrationTests.EmbeddedFormsHideBeforeDetaching; +var + host, first, second: TForm; + panel: TPanel; + origin: TPoint; +begin + host := TForm.CreateNew(nil); + first := TForm.CreateNew(nil); + second := TForm.CreateNew(nil); + try + host.SetBounds(137, 83, 640, 420); + host.Position := poDesigned; + panel := TPanel.Create(host); + panel.BevelOuter := bvNone; + panel.Parent := host; + panel.Align := alClient; + host.Show; + panel.AddControlDockCenter(first); + Application.ProcessMessages; + AssertEquals(PtrInt(panel.Handle), PtrInt(GetParent(first.Handle))); + panel.RemoveAllControls(false); + AssertFalse('Outgoing form must be hidden before native reparenting', first.Visible); + AssertFalse(IsWindowVisible(first.Handle)); + panel.AddControlDockCenter(second); + Application.ProcessMessages; + AssertEquals(PtrInt(panel.Handle), PtrInt(GetParent(second.Handle))); + AssertTrue((GetWindowLongPtr(second.Handle, GWL_STYLE) and WS_CHILD) <> 0); + origin := second.ClientToScreen(Types.Point(0, 0)); + AssertEquals(panel.ClientToScreen(Types.Point(0, 0)).X, origin.X); + AssertEquals(panel.ClientToScreen(Types.Point(0, 0)).Y, origin.Y); + AssertEquals(137, host.Left); + AssertEquals(83, host.Top); + panel.RemoveAllControls(false); + finally + second.Free; + first.Free; + host.Free; + end; +end; + +procedure TFrameworkUIIntegrationTests.GridCompletesOnMainThread; +var + source: TTestSource; + grid: TVisualGrid; +begin + source := TTestSource.Create; + grid := TVisualGrid.Create(nil); + try + FCompleted := 0; + grid.OnFinishedUpdating := OnCompleted; + grid.DataSource := source; + PumpUntil(1); + AssertEquals(1, grid.RowCount); + AssertEquals(PtrInt(GetCurrentThreadID), PtrInt(FCallbackThread)); + AssertEquals(42, Integer(grid.Rows[0].Value)); + finally + grid.Free; + source.Free; + end; +end; + +procedure TFrameworkUIIntegrationTests.GridDestructionJoinsBlockedFetch; +var + source: TTestSource; + grid: TVisualGrid; + release: TReleaseThread; +begin + source := TTestSource.Create; + grid := TVisualGrid.Create(nil); + release := nil; + try + source.BlockFetch := true; + FCompleted := 0; + grid.OnFinishedUpdating := OnCompleted; + grid.DataSource := source; + AssertEquals(Ord(wrSignaled), Ord(source.Entered.WaitFor(1000))); + release := TReleaseThread.Create(source); + release.Start; + FreeAndNil(grid); + AssertEquals('Destruction must not dispatch a stale completion', 0, FCompleted); + AssertEquals(Ord(wrSignaled), Ord(source.ReleaseFetch.WaitFor(0))); + Application.ProcessMessages; + AssertEquals(0, FCompleted); + finally + if Assigned(release) then begin + release.WaitFor; + release.Free; + end; + grid.Free; + source.Free; + end; +end; + +procedure TFrameworkUIIntegrationTests.GridCoalescesRefreshDuringFetch; +var + source: TTestSource; + grid: TVisualGrid; +begin + source := TTestSource.Create; + grid := TVisualGrid.Create(nil); + try + source.BlockFetch := true; + // Keep layout-driven page-size refreshes outside this scheduling assertion. + grid.Options := []; + grid.CanPage := false; + grid.CanSearch := false; + FCompleted := 0; + grid.OnFinishedUpdating := OnCompleted; + grid.DataSource := source; + AssertEquals(Ord(wrSignaled), Ord(source.Entered.WaitFor(1000))); + grid.RefreshGrid; + grid.RefreshGrid; + grid.RefreshGrid; + source.ReleaseFetch.SetEvent; + PumpUntil(2); + AssertEquals('Refresh requests during one fetch collapse to one follow-up', 2, source.FetchCount); + AssertEquals(1, grid.RowCount); + finally + grid.Free; + source.Free; + end; +end; + +procedure TFrameworkUIIntegrationTests.GridSourceReplacementWaitsForFetch; +var + first, second: TTestSource; + grid: TVisualGrid; + release: TReleaseThread; +begin + first := TTestSource.Create; + second := TTestSource.Create; + grid := TVisualGrid.Create(nil); + release := nil; + try + first.BlockFetch := true; + second.Value := 73; + grid.DataSource := first; + AssertEquals(Ord(wrSignaled), Ord(first.Entered.WaitFor(1000))); + release := TReleaseThread.Create(first); + release.Start; + grid.DataSource := second; + AssertEquals('Replacement must join the previous source', Ord(wrSignaled), Ord(first.ReleaseFetch.WaitFor(0))); + FCompleted := 0; + grid.OnFinishedUpdating := OnCompleted; + PumpUntil(1); + AssertEquals(73, Integer(grid.Rows[0].Value)); + finally + if Assigned(release) then begin + release.WaitFor; + release.Free; + end; + grid.Free; + second.Free; + first.Free; + end; +end; + +procedure TFrameworkUIIntegrationTests.GridRecoversAfterFetchError; +var + failed, recovered: TTestSource; + grid: TVisualGrid; +begin + failed := TTestSource.Create; + recovered := TTestSource.Create; + grid := TVisualGrid.Create(nil); + try + failed.FailFetch := true; + recovered.Value := 91; + grid.DataSource := failed; + AssertEquals(Ord(wrSignaled), Ord(failed.Entered.WaitFor(1000))); + // Source replacement collects the failed worker before starting a new one. + grid.DataSource := recovered; + FCompleted := 0; + grid.OnFinishedUpdating := OnCompleted; + PumpUntil(1); + AssertEquals(91, Integer(grid.Rows[0].Value)); + finally + grid.Free; + recovered.Free; + failed.Free; + end; +end; + +procedure TFrameworkUIIntegrationTests.ConcurrentDataRowSchemasRemainConsistent; +var + gate: TEvent; + threads: array[0..3] of TSchemaThread; + i: Integer; +begin + gate := TEvent.Create(nil, true, false, ''); + FillChar(threads, SizeOf(threads), 0); + try + for i := Low(threads) to High(threads) do begin + threads[i] := TSchemaThread.Create(gate, i); + threads[i].Start; + end; + gate.SetEvent; + for i := Low(threads) to High(threads) do begin + threads[i].WaitFor; + AssertEquals('', threads[i].ErrorMessage); + end; + finally + gate.SetEvent; + for i := Low(threads) to High(threads) do + if Assigned(threads[i]) then begin + threads[i].WaitFor; + threads[i].Free; + end; + gate.Free; + end; +end; + + +initialization + RegisterTest(TFrameworkUIIntegrationTests); +end. diff --git a/src/tests/experimental/framework/UReadWriteScope.Checks.pas b/src/tests/experimental/framework/UReadWriteScope.Checks.pas new file mode 100644 index 000000000..f928eb44b --- /dev/null +++ b/src/tests/experimental/framework/UReadWriteScope.Checks.pas @@ -0,0 +1,439 @@ +unit UReadWriteScope.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + These assertions run unchanged under FPCUnit and native Delphi. } + +{$IFDEF FPC}{$mode Delphi}{$H+}{$ENDIF} + +interface + +const ReadWriteScopeCheckCount = 26; + +function ReadWriteScopeCheckName(AIndex: Integer): String; +procedure RunReadWriteScopeCheck(AIndex: Integer); + +implementation + +uses Classes, SysUtils, SyncObjs, UCommon, UConcurrency; + +type + TMode = (smRead, smWrite, smUpgradeable); + TScopeRecord = record + Scope: TScope; + end; + TScopeOwner = class + Scope: TScope; + constructor Create(ALock: TReadWriteLock); + end; + TScopeWorker = class(TThread) + private + FLock: TReadWriteLock; + FMode: TMode; + procedure RunOwned; + protected + procedure Execute; override; + public + Started, Acquired, ReleaseGate, Done: TEvent; + Error: String; + constructor Create(ALock: TReadWriteLock; AMode: TMode); + destructor Destroy; override; + end; + TWrongThreadWorker = class(TThread) + private + FScope: TScope; + protected + procedure Execute; override; + public + ErrorClass: String; + constructor Create(const AScope: TScope); + end; + +const Names: array[1..ReadWriteScopeCheckCount] of String = ( + 'ReadScopeNormalReturn', 'WriteScopeNormalReturn', 'UpgradeableScopeNormalReturn', + 'AllModesEarlyExit', 'AllModesExceptionUnwind', 'CaughtExceptionKeepsScope', + 'CopiesRetainSingleAcquisition', 'OverwriteReleasesPreviousLock', + 'ReturnedScopeRetainsAcquisition', 'RecordFieldFinalizesScope', + 'ClassFieldFinalizesScope', 'RecursiveReadScopesBalance', + 'RecursiveWriteScopesBalance', 'NonrecursiveAcquisitionPreservesOuter', + 'OrdinaryReadCannotPromote', 'DisposedLockRejectsAllScopes', + 'PromotionRetainsOuterUpgradeable', 'PromotionExceptionRetainsOuter', + 'WriteToReadDowngrade', 'UpgradeableToReadDowngrade', + 'ConcurrentReadersShareScopes', 'WriterWaitsForLastCopy', + 'ReaderWaitsForWriterScope', 'FailedAssignmentPreservesExistingScope', + 'ManualAndScopedAcquisitionsBalance', 'WrongThreadReleaseIsRejected'); + +procedure Check(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +function Enter(ALock: TReadWriteLock; AMode: TMode): TScope; +begin + case AMode of + smRead: Result := ALock.EnterReadScope; + smWrite: Result := ALock.EnterWriteScope; + smUpgradeable: Result := ALock.EnterUpgradeableReadScope; + end; +end; + +procedure SetScope(var AScope: TScope; ALock: TReadWriteLock; AMode: TMode); +begin + // Function-result records can introduce compiler-owned temporary copies. + // End that routine before tests deliberately release the final named owner. + AScope := Enter(ALock, AMode); +end; + +constructor TScopeOwner.Create(ALock: TReadWriteLock); +begin + inherited Create; + Scope := ALock.EnterWriteScope; +end; + +procedure CheckHeld(ALock: TReadWriteLock; AMode: TMode); +begin + case AMode of + smRead: Check(ALock.IsReadLockHeld, 'Read scope was released before protected work'); + smWrite: Check(ALock.IsWriteLockHeld, 'Write scope was released before protected work'); + smUpgradeable: Check(ALock.IsUpgradeableReadLockHeld, 'Upgradeable scope released early'); + end; +end; + +procedure CheckReleased(ALock: TReadWriteLock); +begin + Check(not ALock.IsReadLockHeld and not ALock.IsWriteLockHeld and + not ALock.IsUpgradeableReadLockHeld, 'Scope left same-thread ownership'); + Check(ALock.CurrentReadCount = 0, 'Scope left a reader registered'); +end; + +procedure WaitForWaiter(ALock: TReadWriteLock; AWrite: Boolean); +var LAttempt: Integer; +begin + // The timed negative assertions mean something only once the worker is + // registered as a waiter; a slow start would otherwise pass them vacuously. + LAttempt := 0; + while (ALock.WaitingWriteCount + ALock.WaitingReadCount = 0) and (LAttempt < 2000) do begin + Sleep(1); + Inc(LAttempt); + end; + if AWrite then Check(ALock.WaitingWriteCount = 1, 'Worker did not queue for write ownership') + else Check(ALock.WaitingReadCount = 1, 'Worker did not queue for read ownership'); +end; + +procedure RunLocal(ALock: TReadWriteLock; AMode: TMode; AExitKind: Integer); +var Scope: TScope; +begin + Scope := Enter(ALock, AMode); + // The local is deliberately never read: only external lock state is checked. + CheckHeld(ALock, AMode); + case AExitKind of + 1: Exit; + 2: raise EAbort.Create('protected-work failure'); + 3: begin + try raise EAbort.Create('caught inside protected work'); + except on E: EAbort do CheckHeld(ALock, AMode); end; + CheckHeld(ALock, AMode); + end; + end; +end; + +function ReturnScope(ALock: TReadWriteLock): TScope; +var Scope: TScope; +begin + Scope := ALock.EnterReadScope; + Result := Scope; +end; + +procedure ReceiveReturnedScope(var AScope: TScope; ALock: TReadWriteLock); +begin + AScope := ReturnScope(ALock); +end; + +procedure RunRecordField(ALock: TReadWriteLock); +var Owner: TScopeRecord; +begin + Owner.Scope := ALock.EnterWriteScope; + CheckHeld(ALock, smWrite); +end; + +procedure RunPromoted(ALock: TReadWriteLock; ARaise: Boolean); +var Scope: TScope; +begin + Scope := ALock.EnterWriteScope; + Check(ALock.IsWriteLockHeld and ALock.IsUpgradeableReadLockHeld, + 'Promoted scope lost its outer upgradeable ownership'); + if ARaise then raise EAbort.Create('promoted-work failure'); +end; + +constructor TScopeWorker.Create(ALock: TReadWriteLock; AMode: TMode); +begin + inherited Create(True); + FLock := ALock; + FMode := AMode; + Started := TEvent.Create(nil, True, False, ''); + Acquired := TEvent.Create(nil, True, False, ''); + ReleaseGate := TEvent.Create(nil, True, False, ''); + Done := TEvent.Create(nil, True, False, ''); +end; + +destructor TScopeWorker.Destroy; +begin + ReleaseGate.SetEvent; + WaitFor; + Started.Free; Acquired.Free; ReleaseGate.Free; Done.Free; + inherited; +end; + +procedure TScopeWorker.RunOwned; +var Scope: TScope; +begin + Scope := Enter(FLock, FMode); + CheckHeld(FLock, FMode); + Acquired.SetEvent; + Check(ReleaseGate.WaitFor(4000) = wrSignaled, 'Worker release gate timed out'); +end; + +procedure TScopeWorker.Execute; +begin + Started.SetEvent; + try + try + RunOwned; + except on E: Exception do Error := E.ClassName + ': ' + E.Message; end; + finally + // RunOwned's managed local has already finalized before completion is sent. + Done.SetEvent; + end; +end; + +constructor TWrongThreadWorker.Create(const AScope: TScope); +begin + inherited Create(True); + FScope := AScope; +end; + +procedure TWrongThreadWorker.Execute; +begin + try FScope.Reset; + except on E: Exception do ErrorClass := E.ClassName; end; +end; + +function ReadWriteScopeCheckName(AIndex: Integer): String; +begin + if (AIndex < 1) or (AIndex > ReadWriteScopeCheckCount) then + raise EArgumentOutOfRangeException.Create('AIndex'); + Result := Names[AIndex]; +end; + +procedure RunReadWriteScopeCheck(AIndex: Integer); +var + Lock, OtherLock: TReadWriteLock; + Scope, Other: TScope; + Owner: TScopeOwner; + Worker: TScopeWorker; + WrongWorker: TWrongThreadWorker; + Mode: TMode; + Policy: TLockRecursionPolicy; + Rejected: Boolean; +begin + ReadWriteScopeCheckName(AIndex); + Lock := TReadWriteLock.Create(lrpSupportsRecursion); + OtherLock := nil; Worker := nil; WrongWorker := nil; + try + case AIndex of + 1..3: begin + RunLocal(Lock, TMode(AIndex - 1), 0); + CheckReleased(Lock); + end; + 4..6: for Mode := Low(TMode) to High(TMode) do begin + try RunLocal(Lock, Mode, AIndex - 3); + except on E: EAbort do Check(AIndex = 5, 'Unexpected protected-work exception'); end; + CheckReleased(Lock); + end; + 7: for Mode := Low(TMode) to High(TMode) do begin + SetScope(Scope, Lock, Mode); + Other := Scope; + Check((Lock.RecursiveReadCount + Lock.RecursiveWriteCount + + Lock.RecursiveUpgradeCount) = 1, 'Copy acquired the lock again'); + Scope.Reset; + CheckHeld(Lock, Mode); + Other.Reset; + CheckReleased(Lock); + end; + 8: begin + OtherLock := TReadWriteLock.Create; + SetScope(Scope, Lock, smRead); + SetScope(Scope, OtherLock, smWrite); + CheckReleased(Lock); + CheckHeld(OtherLock, smWrite); + Scope.Reset; + CheckReleased(OtherLock); + end; + 9: begin + ReceiveReturnedScope(Scope, Lock); + CheckHeld(Lock, smRead); + Scope.Reset; + CheckReleased(Lock); + end; + 10: begin + RunRecordField(Lock); + CheckReleased(Lock); + end; + 11: begin + Owner := TScopeOwner.Create(Lock); + try CheckHeld(Lock, smWrite); + finally Owner.Free; end; + CheckReleased(Lock); + end; + 12, 13: begin + if AIndex = 12 then Mode := smRead else Mode := smWrite; + SetScope(Scope, Lock, Mode); + SetScope(Other, Lock, Mode); + if Mode = smRead then Check(Lock.RecursiveReadCount = 2, 'Missing nested read') + else Check(Lock.RecursiveWriteCount = 2, 'Missing nested write'); + Other.Reset; + CheckHeld(Lock, Mode); + Scope.Reset; + CheckReleased(Lock); + end; + 14: begin + OtherLock := TReadWriteLock.Create(lrpNoRecursion); + for Mode := Low(TMode) to High(TMode) do begin + SetScope(Scope, OtherLock, Mode); + Rejected := False; + try Other := Enter(OtherLock, Mode); + except on E: ELockRecursionException do Rejected := True; end; + Check(Rejected and Other.IsEmpty, 'Recursive scope was not rejected cleanly'); + CheckHeld(OtherLock, Mode); + Scope.Reset; + CheckReleased(OtherLock); + end; + end; + 15: begin + SetScope(Scope, Lock, smRead); + Rejected := False; + try Other := Lock.EnterWriteScope; + except on E: ELockRecursionException do Rejected := True; end; + Check(Rejected and Other.IsEmpty, 'Ordinary read scope promoted'); + CheckHeld(Lock, smRead); + Scope.Reset; + CheckReleased(Lock); + end; + 16: begin + Lock.Dispose; + for Mode := Low(TMode) to High(TMode) do begin + Rejected := False; + try Scope := Enter(Lock, Mode); + except on E: ELockDisposedException do Rejected := True; end; + Check(Rejected and Scope.IsEmpty, 'Disposed lock returned an owning scope'); + end; + end; + 17, 18: for Policy := Low(TLockRecursionPolicy) to High(TLockRecursionPolicy) do begin + OtherLock := TReadWriteLock.Create(Policy); + try + SetScope(Scope, OtherLock, smUpgradeable); + try RunPromoted(OtherLock, AIndex = 18); + except on E: EAbort do Check(AIndex = 18, 'Unexpected promotion exception'); end; + CheckHeld(OtherLock, smUpgradeable); + Check(not OtherLock.IsWriteLockHeld, 'Inner write acquisition survived its routine'); + Scope.Reset; + CheckReleased(OtherLock); + finally + Scope.Reset; + FreeAndNil(OtherLock); + end; + end; + 19, 20: begin + if AIndex = 19 then SetScope(Scope, Lock, smWrite) + else SetScope(Scope, Lock, smUpgradeable); + SetScope(Other, Lock, smRead); + Scope.Reset; + CheckHeld(Lock, smRead); + Check(not Lock.IsWriteLockHeld and not Lock.IsUpgradeableReadLockHeld, + 'Downgrade retained the released mode'); + Other.Reset; + CheckReleased(Lock); + end; + 21: begin + SetScope(Scope, Lock, smRead); + Worker := TScopeWorker.Create(Lock, smRead); + Worker.Start; + Check(Worker.Acquired.WaitFor(2000) = wrSignaled, 'Readers did not coexist'); + CheckHeld(Lock, smRead); + Worker.ReleaseGate.SetEvent; + Check(Worker.Done.WaitFor(2000) = wrSignaled, 'Reader scope did not finish'); + Check(Worker.Error = '', Worker.Error); + Check(Lock.CurrentReadCount = 1, 'Worker did not release its own read scope'); + Scope.Reset; + CheckReleased(Lock); + end; + 22, 23: begin + if AIndex = 22 then begin + SetScope(Scope, Lock, smRead); Other := Scope; + Worker := TScopeWorker.Create(Lock, smWrite); + end else begin + SetScope(Scope, Lock, smWrite); + Worker := TScopeWorker.Create(Lock, smRead); + end; + Worker.Start; + Check(Worker.Started.WaitFor(2000) = wrSignaled, 'Blocked worker did not start'); + WaitForWaiter(Lock, AIndex = 22); + Check(Worker.Acquired.WaitFor(40) = wrTimeout, 'Worker bypassed owning scope'); + Scope.Reset; + if AIndex = 22 then begin + Check(Worker.Acquired.WaitFor(40) = wrTimeout, 'Writer bypassed last scope copy'); + Other.Reset; + end; + Check(Worker.Acquired.WaitFor(2000) = wrSignaled, 'Worker did not acquire after scope release'); + Worker.ReleaseGate.SetEvent; + Check(Worker.Done.WaitFor(2000) = wrSignaled, 'Worker did not finish'); + Check(Worker.Error = '', Worker.Error); + CheckReleased(Lock); + end; + 24: begin + OtherLock := TReadWriteLock.Create; + OtherLock.Dispose; + SetScope(Scope, Lock, smRead); + Rejected := False; + try Scope := OtherLock.EnterWriteScope; + except on E: ELockDisposedException do Rejected := True; end; + Check(Rejected and not Scope.IsEmpty, 'Failed RHS discarded existing scope'); + CheckHeld(Lock, smRead); + Scope.Reset; + CheckReleased(Lock); + end; + 25: begin + Lock.AcquireReadLock; + try + SetScope(Scope, Lock, smRead); + Check(Lock.RecursiveReadCount = 2, 'Scoped/manual acquisitions not independent'); + Scope.Reset; + Check(Lock.RecursiveReadCount = 1, 'Scope released manual read acquisition'); + finally Lock.ReleaseReadLock; end; + CheckReleased(Lock); + end; + 26: begin + SetScope(Scope, Lock, smWrite); + WrongWorker := TWrongThreadWorker.Create(Scope); + Scope.Reset; + WrongWorker.Start; + WrongWorker.WaitFor; + Check(WrongWorker.ErrorClass = 'ESynchronizationLockException', + 'Cross-thread finalization failed to reject release'); + CheckHeld(Lock, smWrite); + // Invalid cross-thread ownership transfer is rejected. The original + // thread must still release the actual lock acquisition it owns. + Lock.ReleaseWriteLock; + CheckReleased(Lock); + end; + end; + finally + Scope.Reset; + Other.Reset; + Worker.Free; + WrongWorker.Free; + OtherLock.Free; + Lock.Free; + end; +end; + +end. diff --git a/src/tests/experimental/framework/UReadWriteScope.Tests.pas b/src/tests/experimental/framework/UReadWriteScope.Tests.pas new file mode 100644 index 000000000..6e0a03395 --- /dev/null +++ b/src/tests/experimental/framework/UReadWriteScope.Tests.pas @@ -0,0 +1,177 @@ +unit UReadWriteScope.Tests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$mode Delphi}{$H+} + +interface + +uses fpcunit, testregistry; + +type + TReadWriteScopeTests = class(TTestCase) + published + procedure ReadScopeNormalReturn; + procedure WriteScopeNormalReturn; + procedure UpgradeableScopeNormalReturn; + procedure AllModesEarlyExit; + procedure AllModesExceptionUnwind; + procedure CaughtExceptionKeepsScope; + procedure CopiesRetainSingleAcquisition; + procedure OverwriteReleasesPreviousLock; + procedure ReturnedScopeRetainsAcquisition; + procedure RecordFieldFinalizesScope; + procedure ClassFieldFinalizesScope; + procedure RecursiveReadScopesBalance; + procedure RecursiveWriteScopesBalance; + procedure NonrecursiveAcquisitionPreservesOuter; + procedure OrdinaryReadCannotPromote; + procedure DisposedLockRejectsAllScopes; + procedure PromotionRetainsOuterUpgradeable; + procedure PromotionExceptionRetainsOuter; + procedure WriteToReadDowngrade; + procedure UpgradeableToReadDowngrade; + procedure ConcurrentReadersShareScopes; + procedure WriterWaitsForLastCopy; + procedure ReaderWaitsForWriterScope; + procedure FailedAssignmentPreservesExistingScope; + procedure ManualAndScopedAcquisitionsBalance; + procedure WrongThreadReleaseIsRejected; + end; + +implementation + +uses UReadWriteScope.Checks; + +procedure TReadWriteScopeTests.ReadScopeNormalReturn; +begin + RunReadWriteScopeCheck(1); +end; + +procedure TReadWriteScopeTests.WriteScopeNormalReturn; +begin + RunReadWriteScopeCheck(2); +end; + +procedure TReadWriteScopeTests.UpgradeableScopeNormalReturn; +begin + RunReadWriteScopeCheck(3); +end; + +procedure TReadWriteScopeTests.AllModesEarlyExit; +begin + RunReadWriteScopeCheck(4); +end; + +procedure TReadWriteScopeTests.AllModesExceptionUnwind; +begin + RunReadWriteScopeCheck(5); +end; + +procedure TReadWriteScopeTests.CaughtExceptionKeepsScope; +begin + RunReadWriteScopeCheck(6); +end; + +procedure TReadWriteScopeTests.CopiesRetainSingleAcquisition; +begin + RunReadWriteScopeCheck(7); +end; + +procedure TReadWriteScopeTests.OverwriteReleasesPreviousLock; +begin + RunReadWriteScopeCheck(8); +end; + +procedure TReadWriteScopeTests.ReturnedScopeRetainsAcquisition; +begin + RunReadWriteScopeCheck(9); +end; + +procedure TReadWriteScopeTests.RecordFieldFinalizesScope; +begin + RunReadWriteScopeCheck(10); +end; + +procedure TReadWriteScopeTests.ClassFieldFinalizesScope; +begin + RunReadWriteScopeCheck(11); +end; + +procedure TReadWriteScopeTests.RecursiveReadScopesBalance; +begin + RunReadWriteScopeCheck(12); +end; + +procedure TReadWriteScopeTests.RecursiveWriteScopesBalance; +begin + RunReadWriteScopeCheck(13); +end; + +procedure TReadWriteScopeTests.NonrecursiveAcquisitionPreservesOuter; +begin + RunReadWriteScopeCheck(14); +end; + +procedure TReadWriteScopeTests.OrdinaryReadCannotPromote; +begin + RunReadWriteScopeCheck(15); +end; + +procedure TReadWriteScopeTests.DisposedLockRejectsAllScopes; +begin + RunReadWriteScopeCheck(16); +end; + +procedure TReadWriteScopeTests.PromotionRetainsOuterUpgradeable; +begin + RunReadWriteScopeCheck(17); +end; + +procedure TReadWriteScopeTests.PromotionExceptionRetainsOuter; +begin + RunReadWriteScopeCheck(18); +end; + +procedure TReadWriteScopeTests.WriteToReadDowngrade; +begin + RunReadWriteScopeCheck(19); +end; + +procedure TReadWriteScopeTests.UpgradeableToReadDowngrade; +begin + RunReadWriteScopeCheck(20); +end; + +procedure TReadWriteScopeTests.ConcurrentReadersShareScopes; +begin + RunReadWriteScopeCheck(21); +end; + +procedure TReadWriteScopeTests.WriterWaitsForLastCopy; +begin + RunReadWriteScopeCheck(22); +end; + +procedure TReadWriteScopeTests.ReaderWaitsForWriterScope; +begin + RunReadWriteScopeCheck(23); +end; + +procedure TReadWriteScopeTests.FailedAssignmentPreservesExistingScope; +begin + RunReadWriteScopeCheck(24); +end; + +procedure TReadWriteScopeTests.ManualAndScopedAcquisitionsBalance; +begin + RunReadWriteScopeCheck(25); +end; + +procedure TReadWriteScopeTests.WrongThreadReleaseIsRejected; +begin + RunReadWriteScopeCheck(26); +end; + +initialization + RegisterTest(TReadWriteScopeTests); +end. diff --git a/src/tests/experimental/framework/UScope.Checks.pas b/src/tests/experimental/framework/UScope.Checks.pas new file mode 100644 index 000000000..0a29d994b --- /dev/null +++ b/src/tests/experimental/framework/UScope.Checks.pas @@ -0,0 +1,362 @@ +unit UScope.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://www.sphere10.com> + Distributed under the MIT software license, see LICENSE. } + +{$IFDEF FPC}{$MODE Delphi}{$H+}{$ENDIF} + +interface + +const ScopeCheckCount = 22; +procedure RunScopeCheck(AIndex: Integer); +function ScopeCheckName(AIndex: Integer): String; + +implementation + +uses Classes, SysUtils, UCommon; + +type + ECallbackFailure = class(Exception); + EGetterFailure = class(Exception); + EBodyFailure = class(Exception); + TCallbackTarget = class; + TState = class + Targets: array of TCallbackTarget; + Created, Freed, Calls: Integer; + RaiseOnCallback: Boolean; + Order: String; + destructor Destroy; override; + end; + TCallbackTarget = class + private + FState: TState; + FID, FSlot: Integer; + FRegistered: Boolean; + public + constructor Create(AState: TState; AID: Integer); + destructor Destroy; override; + procedure Cleanup; + function Callback(AFail: Boolean = False): TThreadMethod; + end; + TScopeOwner = class + private + FScope: TScope; + public + constructor Create(AState: TState); + end; + TScopeRecordOwner = record + Scope: TScope; + end; + +procedure Check(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +destructor TState.Destroy; +var I: Integer; +begin + for I := 0 to High(Targets) do Targets[I].Free; + inherited; +end; + +constructor TCallbackTarget.Create(AState: TState; AID: Integer); +begin + inherited Create; + FState := AState; + FID := AID; + FSlot := Length(FState.Targets); + SetLength(FState.Targets, FSlot + 1); + FState.Targets[FSlot] := Self; + FRegistered := True; + Inc(FState.Created); +end; + +destructor TCallbackTarget.Destroy; +begin + if FRegistered then begin + FState.Targets[FSlot] := nil; + Inc(FState.Freed); + end; + inherited; +end; + +procedure TCallbackTarget.Cleanup; +begin + Inc(FState.Calls); + if FState.Order <> '' then FState.Order := FState.Order + ','; + FState.Order := FState.Order + IntToStr(FID); + if FState.RaiseOnCallback then + raise ECallbackFailure.Create('Expected cleanup callback failure'); +end; + +function TCallbackTarget.Callback(AFail: Boolean): TThreadMethod; +begin + if AFail then raise EGetterFailure.Create('Expected callback argument failure'); + Result := Cleanup; +end; + +function NewScope(AState: TState; AID: Integer): TScope; +var Target: TCallbackTarget; +begin + Target := TCallbackTarget.Create(AState, AID); + Result := TScope.Create(Target.Cleanup); +end; + +procedure InitScope(var ADestination: TScope; AState: TState; AID: Integer); +begin + // End compiler-result copies before deliberately releasing a last owner. + ADestination := NewScope(AState, AID); +end; + +constructor TScopeOwner.Create(AState: TState); +begin + inherited Create; + FScope := NewScope(AState, 1); +end; + +procedure OwnNormally(AState: TState); +var Scope: TScope; +begin + Scope := NewScope(AState, 1); + // Deliberately never read Scope; external state must still prove retention. + Check(AState.Calls = 0, 'Callback ran before the protected body'); +end; + +procedure OwnAndExit(AState: TState); +var Scope: TScope; +begin + Scope := NewScope(AState, 1); + Check(AState.Calls = 0, 'Callback ran before Exit'); + Exit; +end; + +procedure OwnAndRaise(AState: TState); +var Scope: TScope; +begin + Scope := NewScope(AState, 1); + Check(AState.Calls = 0, 'Callback ran before the body exception'); + raise EBodyFailure.Create('Expected body failure'); +end; + +procedure OwnAndCatchInside(AState: TState); +var Scope: TScope; +begin + try + Scope := NewScope(AState, 1); + raise EBodyFailure.Create('Expected handled body failure'); + except + on E: EBodyFailure do + Check(AState.Calls = 0, 'Inner handler lost the routine owner'); + end; + Check(AState.Calls = 0, 'Callback ran before routine exit after inner handler'); +end; + +function ReturnLocalCopy(AState: TState): TScope; +var Local: TScope; +begin + Local := NewScope(AState, 1); + Result := Local; +end; + +procedure OwnReturnedCopy(AState: TState); +var Scope: TScope; +begin + Scope := ReturnLocalCopy(AState); + Check(AState.Calls = 0, 'Returned copy released its callback early'); +end; + +procedure OwnIndependentLocals(AState: TState); +var First, Second: TScope; +begin + First := NewScope(AState, 1); + Second := NewScope(AState, 2); + Check((AState.Created = 2) and (AState.Calls = 0), 'Independent local callbacks did not stay deferred'); +end; + +procedure OwnRecordField(AState: TState); +var Owner: TScopeRecordOwner; +begin + Owner.Scope := NewScope(AState, 1); + Check(AState.Calls = 0, 'Containing record did not retain its scope'); +end; + +function ScopeCheckName(AIndex: Integer): String; +begin + case AIndex of + 1: Result := 'DefaultScopeIsEmpty'; + 2: Result := 'NilCallbackRejected'; + 3: Result := 'CreationDefersCallback'; + 4: Result := 'LocalScopeReleasesOnReturn'; + 5: Result := 'LocalScopeReleasesOnExit'; + 6: Result := 'LocalScopeReleasesOnException'; + 7: Result := 'InnerHandlerRetainsLocalScope'; + 8: Result := 'ReturnedRecordRetainsCallback'; + 9: Result := 'CopiesInvokeOnlyLastOwner'; + 10: Result := 'OverwriteInvokesPreviousCallback'; + 11: Result := 'OverwriteKeepsAliasedPreviousCallback'; + 12: Result := 'ClassFieldFinalizesCallback'; + 13: Result := 'ArrayCopiesShareCallback'; + 14: Result := 'CallbackExceptionEmptiesOwner'; + 15: Result := 'CallbackExceptionDuringOverwrite'; + 16: Result := 'FailedCopyAssignmentPreservesSource'; + 17: Result := 'SelfAssignmentPreservesOwner'; + 18: Result := 'FreeMethodDisposesOwnedTarget'; + 19: Result := 'BorrowedTargetRemainsCallerOwned'; + 20: Result := 'FailedCallbackGetterKeepsPreviousOwner'; + 21: Result := 'IndependentLocalsOwnIndependentCallbacks'; + 22: Result := 'RecordFieldFinalizesCallback'; + else raise EArgumentOutOfRangeException.Create('AIndex'); end; +end; + +procedure CheckWithState(AIndex: Integer; State: TState); +var + Scope, Alias: TScope; + Owners: TArray<TScope>; + Owner: TScopeOwner; + Target: TCallbackTarget; + NilMethod: TThreadMethod; + Raised: Boolean; +begin + Owner := nil; + try + case AIndex of + 1: begin + Check(Scope.IsEmpty, 'Default scope must be empty'); + Scope.Reset; Scope.Reset; + Check(Scope.IsEmpty and (State.Calls = 0), 'Empty Reset must be harmless'); + end; + 2: begin + NilMethod := nil; Raised := False; + try Scope := TScope.Create(NilMethod); + except on E: EArgumentNilException do Raised := True; end; + Check(Raised and Scope.IsEmpty, 'Nil callback must be rejected without an owner'); + end; + 3: begin + InitScope(Scope, State, 1); + Check(State.Calls = 0, 'Create invoked cleanup instead of registering it'); + Scope.Reset; Check(State.Calls = 1, 'Final owner did not invoke deferred callback'); + end; + 4: begin + OwnNormally(State); + Check(State.Calls = 1, 'Normal return did not invoke cleanup'); + end; + 5: begin + OwnAndExit(State); + Check(State.Calls = 1, 'Exit did not invoke cleanup'); + end; + 6: begin + Raised := False; + try OwnAndRaise(State); + except on E: EBodyFailure do Raised := True; end; + Check(Raised and (State.Calls = 1), 'Unwind must invoke cleanup and preserve body failure'); + end; + 7: begin + OwnAndCatchInside(State); + Check(State.Calls = 1, 'Routine owner did not finalize'); + end; + 8: begin + OwnReturnedCopy(State); + Check(State.Calls = 1, 'Returned copy did not finalize once'); + end; + 9: begin + InitScope(Scope, State, 1); Alias := Scope; Scope.Reset; + Check(Scope.IsEmpty and not Alias.IsEmpty, 'Reset changed another owner'); + Check(State.Calls = 0, 'Reset invoked a live alias'); + Alias.Reset; Alias.Reset; Check(State.Calls = 1, 'Last alias must invoke callback once'); + end; + 10: begin + InitScope(Scope, State, 1); InitScope(Scope, State, 2); + Check(State.Order = '1', 'Overwrite did not invoke previous callback'); + Scope.Reset; Check(State.Order = '1,2', 'Replacement lost its callback'); + end; + 11: begin + InitScope(Scope, State, 1); Alias := Scope; InitScope(Scope, State, 2); + Check(State.Calls = 0, 'Overwrite invoked the aliased previous callback'); + Alias.Reset; Check(State.Order = '1', 'Previous callback lost its last owner'); + Scope.Reset; Check(State.Order = '1,2', 'Replacement callback did not run'); + end; + 12: begin + Owner := TScopeOwner.Create(State); Check(State.Calls = 0, 'Class field lost its scope'); + FreeAndNil(Owner); Check(State.Calls = 1, 'Class destruction did not finalize scope'); + end; + 13: begin + SetLength(Owners, 3); + InitScope(Owners[0], State, 1); Owners[1] := Owners[0]; InitScope(Owners[2], State, 2); + Owners[0].Reset; Check(State.Calls = 0, 'Array alias lost its callback'); + SetLength(Owners, 1); Check(State.Calls = 2, 'Array shrink did not release discarded owners'); + end; + 14, 15: begin + InitScope(Scope, State, 1); State.RaiseOnCallback := True; Raised := False; + try + if AIndex = 14 then Scope.Reset else Scope := Default(TScope); + except on E: ECallbackFailure do Raised := True; end; + State.RaiseOnCallback := False; + Check(Raised and Scope.IsEmpty, 'Callback failure must propagate with owner detached'); + Check(State.Calls = 1, 'Throwing callback must run once'); + Scope.Reset; Check(State.Calls = 1, 'Reset after failure invoked callback again'); + end; + 16: begin + InitScope(Scope, State, 1); InitScope(Alias, State, 2); + State.RaiseOnCallback := True; Raised := False; + try Scope := Alias; + except on E: ECallbackFailure do Raised := True; end; + State.RaiseOnCallback := False; + Check(Raised and Scope.IsEmpty and not Alias.IsEmpty, 'Failed copy must preserve source and clear destination'); + Check(State.Order = '1', 'Failed copy invoked source callback'); + Alias.Reset; Check(State.Order = '1,2', 'Source lost its independent callback'); + end; + 17: begin + InitScope(Scope, State, 1); Scope := Scope; Alias := Scope; Scope := Alias; + Check(State.Calls = 0, 'Self/shared assignment invoked cleanup'); + Alias.Reset; Check(State.Calls = 0, 'Remaining alias lost ownership'); + Scope.Reset; Check(State.Calls = 1, 'Self/shared assignment duplicated cleanup'); + end; + 18: begin + Target := TCallbackTarget.Create(State, 1); + Scope := TScope.Create(Target.Free); + Check(State.Freed = 0, 'Registering Free destroyed the target immediately'); + // The outer check observes destruction after this routine and any + // compiler-created result copies have completed their cleanup. + end; + 19: begin + InitScope(Scope, State, 1); Scope.Reset; + Check((State.Calls = 1) and (State.Freed = 0), 'Scope must not free a borrowed callback target'); + State.Targets[0].Free; Check(State.Freed = 1, 'Caller lost ownership of borrowed target'); + end; + 20: begin + InitScope(Scope, State, 1); Target := TCallbackTarget.Create(State, 2); Raised := False; + try Scope := TScope.Create(Target.Callback(True)); + except on E: EGetterFailure do Raised := True; end; + Check(Raised and not Scope.IsEmpty, 'Argument failure discarded previous owner'); + Check(State.Calls = 0, 'Argument failure invoked a callback'); + Scope.Reset; Check(State.Order = '1', 'Previous callback was not retained'); + end; + 21: begin + OwnIndependentLocals(State); + Check(State.Calls = 2, 'Independent locals lost callbacks'); + end; + 22: begin + OwnRecordField(State); + Check(State.Calls = 1, 'Containing record did not finalize scope'); + end; + else raise EArgumentOutOfRangeException.Create('AIndex'); end; + finally + State.RaiseOnCallback := False; + Owner.Free; SetLength(Owners, 0); Alias.Reset; Scope.Reset; + end; +end; + +procedure RunScopeCheck(AIndex: Integer); +var State: TState; +begin + State := TState.Create; + try + CheckWithState(AIndex, State); + if AIndex = 18 then Check(State.Freed = 1, 'Free callback did not destroy its target once'); + finally + State.Free; + end; +end; + +end. diff --git a/src/tests/experimental/framework/UScope.Tests.pas b/src/tests/experimental/framework/UScope.Tests.pas new file mode 100644 index 000000000..f17e70ad6 --- /dev/null +++ b/src/tests/experimental/framework/UScope.Tests.pas @@ -0,0 +1,152 @@ +unit UScope.Tests; + +{ Copyright © 2026 Herman Schoenfeld <https://www.sphere10.com> + Distributed under the MIT software license, see LICENSE. } + +{$MODE Delphi}{$H+} +interface +uses fpcunit, testregistry; +type + TScopeTests = class(TTestCase) + published + procedure DefaultScopeIsEmpty; + procedure NilCallbackRejected; + procedure CreationDefersCallback; + procedure LocalScopeReleasesOnReturn; + procedure LocalScopeReleasesOnExit; + procedure LocalScopeReleasesOnException; + procedure InnerHandlerRetainsLocalScope; + procedure ReturnedRecordRetainsCallback; + procedure CopiesInvokeOnlyLastOwner; + procedure OverwriteInvokesPreviousCallback; + procedure OverwriteKeepsAliasedPreviousCallback; + procedure ClassFieldFinalizesCallback; + procedure ArrayCopiesShareCallback; + procedure CallbackExceptionEmptiesOwner; + procedure CallbackExceptionDuringOverwrite; + procedure FailedCopyAssignmentPreservesSource; + procedure SelfAssignmentPreservesOwner; + procedure FreeMethodDisposesOwnedTarget; + procedure BorrowedTargetRemainsCallerOwned; + procedure FailedCallbackGetterKeepsPreviousOwner; + procedure IndependentLocalsOwnIndependentCallbacks; + procedure RecordFieldFinalizesCallback; + end; + +implementation + +uses UScope.Checks; + +procedure TScopeTests.DefaultScopeIsEmpty; +begin + RunScopeCheck(1); +end; + +procedure TScopeTests.NilCallbackRejected; +begin + RunScopeCheck(2); +end; + +procedure TScopeTests.CreationDefersCallback; +begin + RunScopeCheck(3); +end; + +procedure TScopeTests.LocalScopeReleasesOnReturn; +begin + RunScopeCheck(4); +end; + +procedure TScopeTests.LocalScopeReleasesOnExit; +begin + RunScopeCheck(5); +end; + +procedure TScopeTests.LocalScopeReleasesOnException; +begin + RunScopeCheck(6); +end; + +procedure TScopeTests.InnerHandlerRetainsLocalScope; +begin + RunScopeCheck(7); +end; + +procedure TScopeTests.ReturnedRecordRetainsCallback; +begin + RunScopeCheck(8); +end; + +procedure TScopeTests.CopiesInvokeOnlyLastOwner; +begin + RunScopeCheck(9); +end; + +procedure TScopeTests.OverwriteInvokesPreviousCallback; +begin + RunScopeCheck(10); +end; + +procedure TScopeTests.OverwriteKeepsAliasedPreviousCallback; +begin + RunScopeCheck(11); +end; + +procedure TScopeTests.ClassFieldFinalizesCallback; +begin + RunScopeCheck(12); +end; + +procedure TScopeTests.ArrayCopiesShareCallback; +begin + RunScopeCheck(13); +end; + +procedure TScopeTests.CallbackExceptionEmptiesOwner; +begin + RunScopeCheck(14); +end; + +procedure TScopeTests.CallbackExceptionDuringOverwrite; +begin + RunScopeCheck(15); +end; + +procedure TScopeTests.FailedCopyAssignmentPreservesSource; +begin + RunScopeCheck(16); +end; + +procedure TScopeTests.SelfAssignmentPreservesOwner; +begin + RunScopeCheck(17); +end; + +procedure TScopeTests.FreeMethodDisposesOwnedTarget; +begin + RunScopeCheck(18); +end; + +procedure TScopeTests.BorrowedTargetRemainsCallerOwned; +begin + RunScopeCheck(19); +end; + +procedure TScopeTests.FailedCallbackGetterKeepsPreviousOwner; +begin + RunScopeCheck(20); +end; + +procedure TScopeTests.IndependentLocalsOwnIndependentCallbacks; +begin + RunScopeCheck(21); +end; + +procedure TScopeTests.RecordFieldFinalizesCallback; +begin + RunScopeCheck(22); +end; + +initialization + RegisterTest(TScopeTests); +end. diff --git a/src/tests/experimental/framework/UStreams.Checks.pas b/src/tests/experimental/framework/UStreams.Checks.pas new file mode 100644 index 000000000..b5fde2250 --- /dev/null +++ b/src/tests/experimental/framework/UStreams.Checks.pas @@ -0,0 +1,2604 @@ +unit UStreams.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://www.sphere10.com> + Independent buffered-stream contracts shared by FPCUnit and native Delphi. + Distributed under the MIT software license; see LICENSE. } + +{$IFDEF FPC}{$MODE Delphi}{$H+}{$ENDIF} +{$IFNDEF FPC}{$IF SizeOf(NativeInt)>SizeOf(Longint)}{$DEFINE STREAM_TEST_NATIVE_COUNTS}{$IFEND}{$ENDIF} + +interface + +const BufferedStreamCheckCount = 101; +procedure RunBufferedStreamCheck(AIndex: Integer); +function BufferedStreamCheckName(AIndex: Integer): String; + +implementation + +uses Classes, SysUtils, UStreams; + +type + ESourceFailure = class(EStreamError); + PDisposeCount = ^Integer; + + TObservedStream = class(TMemoryStream) + private + FDisposeCount: PDisposeCount; + protected + function GetSize: Int64; override; + procedure SetSize(const ANewSize: Int64); override; + public + ReadCalls, WriteCalls, SeekCalls, SizeCalls: Integer; + LargestRead, MaxRead, MaxWrite: Longint; + FailRead, FailWrite, FailSeek, FailSize, FailResize: Boolean; + MoveBeforeReadFailure, MoveBeforeWriteFailure, MoveBeforeSeekFailure: Boolean; + NegativeReadResult, NegativeWriteResult, RejectSecondRead: Boolean; + constructor Create(ADisposeCount: PDisposeCount = nil); + destructor Destroy; override; + function Read(var ABuffer; ACount: Longint): Longint; overload; override; + function Write(const ABuffer; ACount: Longint): Longint; overload; override; + {$IFDEF STREAM_TEST_NATIVE_COUNTS} + function Read(var ABuffer; ACount: NativeInt): NativeInt; overload; override; + function Write(const ABuffer; ACount: NativeInt): NativeInt; overload; override; + {$ENDIF} + function Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; override; + procedure ResetCounts; + end; + + TSparseStream = class(TStream) + private + FSize, FPosition: Int64; + protected + function GetSize: Int64; override; + procedure SetSize(const ANewSize: Int64); override; + public + ReadCalls, LargestRead: Integer; + constructor Create; + function Read(var ABuffer; ACount: Longint): Longint; override; + function Write(const ABuffer; ACount: Longint): Longint; override; + function Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; override; + end; + + TForwardStream = class(TStream) + private + FData: TBytes; + FPosition: Integer; + protected + function GetSize: Int64; override; + procedure SetSize(const ANewSize: Int64); override; + public + ReadCalls, WriteCalls, SeekCalls, SizeCalls, ResizeCalls: Integer; + LastWritePosition: Integer; + ReturnNegativeSeek, ReturnNegativeSize: Boolean; + constructor Create; + function Read(var ABuffer; ACount: Longint): Longint; override; + function Write(const ABuffer; ACount: Longint): Longint; override; + function Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; override; + end; + + TSizeObservedStream = class(TObservedStream) + protected + function GetSize: Int64; override; + procedure SetSize(const ANewSize: Int64); override; + public + ResizeCalls: Integer; + NegativeSize, IgnoreResize, FailResizeAfterMutation: Boolean; + procedure ResetCounts; reintroduce; + end; + + TCountedSparseStream = class(TSparseStream) + protected + function GetSize: Int64; override; + public + SizeCalls: Integer; + end; + + TInjectedReadFailureStream = class(TStream) + public + Failure: Exception; + ReadCalls: Integer; + destructor Destroy; override; + function Read(var ABuffer; ACount: Longint): Longint; override; + function Write(const ABuffer; ACount: Longint): Longint; override; + function Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; override; + end; + +procedure Check(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('Buffered stream: '+AMessage); +end; + +constructor TObservedStream.Create(ADisposeCount: PDisposeCount); +begin + inherited Create; + FDisposeCount := ADisposeCount; +end; + +destructor TObservedStream.Destroy; +begin + if Assigned(FDisposeCount) then Inc(FDisposeCount^); + inherited; +end; + +procedure TObservedStream.ResetCounts; +begin + ReadCalls := 0; + WriteCalls := 0; + SeekCalls := 0; + SizeCalls := 0; + LargestRead := 0; +end; + +function TObservedStream.GetSize: Int64; +begin + Inc(SizeCalls); + if FailSize then raise ESourceFailure.Create('source-size'); + Result := inherited GetSize; +end; + +procedure TObservedStream.SetSize(const ANewSize: Int64); +begin + if FailResize then raise ESourceFailure.Create('source-resize'); + inherited SetSize(ANewSize); +end; + +function TObservedStream.Read(var ABuffer; ACount: Longint): Longint; +begin + {$IFDEF STREAM_TEST_NATIVE_COUNTS} + Result := Longint(Read(ABuffer,NativeInt(ACount))); + {$ELSE} + Inc(ReadCalls); + if ACount>LargestRead then LargestRead := ACount; + if RejectSecondRead and (ReadCalls>1) then raise ESourceFailure.Create('unexpected-second-read'); + if NegativeReadResult then begin + if ACount>=2 then inherited Read(ABuffer,2); + Exit(-1); + end; + if FailRead then begin + if MoveBeforeReadFailure and (ACount>=2) then inherited Read(ABuffer,2); + raise ESourceFailure.Create('source-read'); + end; + if (MaxRead>0) and (ACount>MaxRead) then ACount := MaxRead; + Result := inherited Read(ABuffer,ACount); + {$ENDIF} +end; + +function TObservedStream.Write(const ABuffer; ACount: Longint): Longint; +begin + {$IFDEF STREAM_TEST_NATIVE_COUNTS} + Result := Longint(Write(ABuffer,NativeInt(ACount))); + {$ELSE} + Inc(WriteCalls); + if NegativeWriteResult then begin + if ACount>=2 then inherited Write(ABuffer,2); + Exit(-1); + end; + if FailWrite then begin + if MoveBeforeWriteFailure and (ACount>=2) then inherited Write(ABuffer,2); + raise ESourceFailure.Create('source-write'); + end; + if (MaxWrite>0) and (ACount>MaxWrite) then ACount := MaxWrite; + Result := inherited Write(ABuffer,ACount); + {$ENDIF} +end; + +{$IFDEF STREAM_TEST_NATIVE_COUNTS} +function TObservedStream.Read(var ABuffer; ACount: NativeInt): NativeInt; +begin + // Delphi64 TMemoryStream implements this virtual overload directly; its + // Longint overload calls back into it. Count each physical read only once. + Inc(ReadCalls); + if ACount>LargestRead then LargestRead := Longint(ACount); + if RejectSecondRead and (ReadCalls>1) then raise ESourceFailure.Create('unexpected-second-read'); + if NegativeReadResult then begin + if ACount>=2 then inherited Read(ABuffer,NativeInt(2)); + Exit(-1); + end; + if FailRead then begin + if MoveBeforeReadFailure and (ACount>=2) then inherited Read(ABuffer,NativeInt(2)); + raise ESourceFailure.Create('source-read'); + end; + if (MaxRead>0) and (ACount>MaxRead) then ACount := MaxRead; + Result := inherited Read(ABuffer,ACount); +end; + +function TObservedStream.Write(const ABuffer; ACount: NativeInt): NativeInt; +begin + Inc(WriteCalls); + if NegativeWriteResult then begin + if ACount>=2 then inherited Write(ABuffer,NativeInt(2)); + Exit(-1); + end; + if FailWrite then begin + if MoveBeforeWriteFailure and (ACount>=2) then inherited Write(ABuffer,NativeInt(2)); + raise ESourceFailure.Create('source-write'); + end; + if (MaxWrite>0) and (ACount>MaxWrite) then ACount := MaxWrite; + Result := inherited Write(ABuffer,ACount); +end; +{$ENDIF} + +function TObservedStream.Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; +begin + Inc(SeekCalls); + if FailSeek then begin + if MoveBeforeSeekFailure then inherited Seek(Int64(13),soBeginning); + raise ESourceFailure.Create('source-seek'); + end; + Result := inherited Seek(AOffset,AOrigin); +end; + +constructor TSparseStream.Create; +begin + inherited Create; + FSize := (Int64(1) shl 40)+123; +end; + +function TSparseStream.GetSize: Int64; +begin + Result := FSize; +end; + +procedure TSparseStream.SetSize(const ANewSize: Int64); +begin + Check(ANewSize>=0,'negative sparse size'); + FSize := ANewSize; + if FPosition>FSize then FPosition := FSize; +end; + +function TSparseStream.Read(var ABuffer; ACount: Longint): Longint; +var I: Integer; + P: PByte; +begin + Inc(ReadCalls); + if ACount>LargestRead then LargestRead := ACount; + Result := ACount; + if FPosition>=FSize then Result := 0 + else if FSize-FPosition<Result then Result := Integer(FSize-FPosition); + P := @ABuffer; + for I := 0 to Result-1 do begin + P^ := Byte((FPosition xor (FPosition shr 8)) and 255); + Inc(P); + Inc(FPosition); + end; +end; + +function TSparseStream.Write(const ABuffer; ACount: Longint): Longint; +begin + Result := ACount; + Inc(FPosition,Result); + if FPosition>FSize then FSize := FPosition; +end; + +function TSparseStream.Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; +begin + case AOrigin of + soBeginning: Result := AOffset; + soCurrent: Result := FPosition+AOffset; + soEnd: Result := FSize+AOffset; + else + raise EStreamError.Create('Invalid sparse seek origin'); + end; + Check(Result>=0,'negative sparse position'); + FPosition := Result; +end; + +function Pattern(ACount: Integer): TBytes; +var I: Integer; +begin + SetLength(Result,ACount); + for I := 0 to ACount-1 do Result[I] := Byte(I mod 251); +end; + +procedure Seed(AStream: TStream; const AData: TBytes); +begin + if Length(AData)>0 then AStream.WriteBuffer(AData[0],Length(AData)); + AStream.Position := 0; +end; + +constructor TForwardStream.Create; +begin + inherited Create; + FData := Pattern(24); +end; + +function TForwardStream.GetSize: Int64; +begin + Inc(SizeCalls); + if ReturnNegativeSize then Exit(-1); + raise ESourceFailure.Create('forward-size'); +end; + +procedure TForwardStream.SetSize(const ANewSize: Int64); +begin + Inc(ResizeCalls); + raise ESourceFailure.Create('forward-resize'); +end; + +function TForwardStream.Read(var ABuffer; ACount: Longint): Longint; +begin + Inc(ReadCalls); + Result := ACount; + if Result>Length(FData)-FPosition then Result := Length(FData)-FPosition; + if Result>0 then Move(FData[FPosition],ABuffer,Result); + Inc(FPosition,Result); +end; + +function TForwardStream.Write(const ABuffer; ACount: Longint): Longint; +begin + Inc(WriteCalls); + LastWritePosition := FPosition; + Result := ACount; + if FPosition+Result>Length(FData) then SetLength(FData,FPosition+Result); + if Result>0 then Move(ABuffer,FData[FPosition],Result); + Inc(FPosition,Result); +end; + +function TForwardStream.Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; +begin + Inc(SeekCalls); + if ReturnNegativeSeek then Exit(-1); + raise ESourceFailure.Create('forward-seek'); +end; + +function TSizeObservedStream.GetSize: Int64; +begin + if NegativeSize then begin + Inc(SizeCalls); + Exit(-1); + end; + Result := inherited GetSize; +end; + +procedure TSizeObservedStream.ResetCounts; +begin + inherited ResetCounts; + ResizeCalls := 0; +end; + +procedure TSizeObservedStream.SetSize(const ANewSize: Int64); +begin + Inc(ResizeCalls); + if IgnoreResize then Exit; + inherited SetSize(ANewSize); + if FailResizeAfterMutation then raise ESourceFailure.Create('source-resize-after-mutation'); +end; + +function TCountedSparseStream.GetSize: Int64; +begin + Inc(SizeCalls); + Result := inherited GetSize; +end; + +destructor TInjectedReadFailureStream.Destroy; +begin + Failure.Free; + inherited Destroy; +end; + +function TInjectedReadFailureStream.Read(var ABuffer; ACount: Longint): Longint; +var LFailure: Exception; +begin + Inc(ReadCalls); + LFailure := Failure; + Failure := nil; + raise LFailure; +end; + +function TInjectedReadFailureStream.Write(const ABuffer; ACount: Longint): Longint; +begin + raise ESourceFailure.Create('injected-write-unsupported'); +end; + +function TInjectedReadFailureStream.Seek(const AOffset: Int64; AOrigin: TSeekOrigin): Int64; +begin + raise ESourceFailure.Create('injected-seek-unsupported'); +end; + +procedure CheckPattern(AStream: TStream; ACount, AStart: Integer); +var I, LRead: Integer; + LBytes: TBytes; +begin + SetLength(LBytes,ACount); + if ACount=0 then Exit; + LRead := AStream.Read(LBytes[0],ACount); + Check(LRead=ACount,'unexpected read length'); + for I := 0 to ACount-1 do + Check(LBytes[I]=Byte((AStart+I) mod 251),'incorrect byte at '+IntToStr(AStart+I)); +end; + +procedure CheckMemoryEqual(ASource, AGold: TMemoryStream); +begin + Check(ASource.Size=AGold.Size,'source contents have a different size'); + if AGold.Size>0 then + Check(CompareMem(ASource.Memory,AGold.Memory,Integer(AGold.Size)),'source contents differ from independent TMemoryStream'); +end; + +procedure CheckArguments(AInvalidBuffer: Boolean); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LRaised: Boolean; + LDisposed, I, LSize: Integer; +begin + if not AInvalidBuffer then begin + LRaised := False; + LBuffered := nil; + try + try + LBuffered := TBufferedStream.Create(nil); + except + on E: EArgumentException do LRaised := True; + end; + Check(LRaised,'nil source was accepted'); + finally + LBuffered.Free; + end; + Exit; + end; + for I := 0 to 3 do begin + LDisposed := 0; + LSource := TObservedStream.Create(@LDisposed); + LBuffered := nil; + LRaised := False; + LSize := -(I mod 2); + try + try + LBuffered := TBufferedStream.Create(LSource,LSize,I>=2); + except + on E: EArgumentException do LRaised := True; + end; + Check(LRaised,'nonpositive buffer size was accepted'); + Check(LDisposed=0,'failed argument validation took source ownership'); + finally + LBuffered.Free; + if LDisposed=0 then LSource.Free; + end; + end; +end; + +procedure CheckOwnership(AOwns: Boolean; AReadFails: Boolean); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LDisposed: Integer; + B: Byte; + {$IFNDEF FPC}{$IF DECLARED(TStreamCapabilities)} + LHandle: THandleStream; + LHandleBuffered: TBufferedStream; + {$IFEND}{$ENDIF} +begin + LDisposed := 0; + LSource := TObservedStream.Create(@LDisposed); + LBuffered := nil; + try + Seed(LSource,Pattern(10)); + if AOwns then LBuffered := TBufferedStream.Create(LSource,3,True) + else LBuffered := TBufferedStream.Create(LSource); + Check((LBuffered.WrappedStream=LSource) and (LBuffered.OwnsStream=AOwns), + 'wrapped stream or ownership property changed'); + if AOwns then Check(LBuffered.BufferSize=3,'explicit buffer size changed') + else Check(LBuffered.BufferSize=65536,'default buffer size changed'); + {$IFNDEF FPC}{$IF DECLARED(TStreamCapabilities)} + Check(LBuffered.Capabilities=LSource.Capabilities,'memory-stream capability flags were not forwarded'); + if not AOwns then begin + // Query metadata only; this handle wrapper performs no OS I/O and does + // not own the supplied handle. Its flags differ from TMemoryStream. + LHandle := THandleStream.Create(0); + try + LHandleBuffered := TBufferedStream.Create(LHandle,3); + try + Check(LHandleBuffered.Capabilities=LHandle.Capabilities, + 'handle-stream OS-error capability was not forwarded'); + finally + LHandleBuffered.Free; + end; + finally + LHandle.Free; + end; + end; + {$IFEND}{$ENDIF} + if AReadFails then begin + LSource.FailRead := True; + try + LBuffered.Read(B,1); + Check(False,'expected owned source failure'); + except + on E: ESourceFailure do Check(E.Message='source-read','changed source exception'); + end; + LSource.FailRead := False; + end else CheckPattern(LBuffered,1,0); + FreeAndNil(LBuffered); + if AOwns then Check(LDisposed=1,'owned source was not disposed exactly once') + else begin + Check(LDisposed=0,'borrowed source was disposed'); + LSource.Position := 0; + CheckPattern(LSource,10,0); + end; + finally + LBuffered.Free; + if LDisposed=0 then LSource.Free; + end; + Check(LDisposed=1,'source disposal count is incorrect'); +end; + +procedure CheckReadScenario(AKind: Integer); +const Sizes: array[0..3] of Integer = (1,2,3,65536); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LBytes: TBytes; + LSize, I, J, N, LTotal, LRequested: Integer; + B: Byte; + LRaised: Boolean; + {$IFNDEF FPC} + LOffsetBytes: TBytes; + {$IF SizeOf(NativeInt)>SizeOf(Longint)} + LNativeCount: NativeInt; + {$IFEND}{$ENDIF} +begin + for J := Low(Sizes) to High(Sizes) do begin + LSize := Sizes[J]; + LSource := TObservedStream.Create; + try + if AKind=3 then LBytes := nil else LBytes := Pattern(2*LSize+17); + Seed(LSource,LBytes); + if AKind=0 then LSource.Position := 7; + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,LSize); + try + B := 123; + case AKind of + 0: begin + Check(LBuffered.Position=7,'lost nonzero initial position'); + CheckPattern(LBuffered,9,7); + Check(LBuffered.Position=16,'wrong logical position after initial-offset read'); + end; + 1: begin + CheckPattern(LBuffered,1,0); + Check(LBuffered.Position=1,'logical cursor followed physical read-ahead'); + Check((LSource.Position>=1) and (LSource.Position<=LSize),'unbounded physical read-ahead'); + Check(LBuffered.Size=Length(LBytes),'Size followed buffer contents'); + end; + 2: begin + for I := 0 to Length(LBytes)-1 do begin + Check(LBuffered.Read(B,1)=1,'single-byte read ended early'); + Check(B=LBytes[I],'single-byte read changed data'); + end; + Check(LSource.ReadCalls<=(Length(LBytes)+LSize-1) div LSize,'single-byte reads did not collapse wrapped reads'); + end; + 3: begin + B := 123; + Check((LBuffered.Size=0) and (LBuffered.Position=0),'empty stream state changed'); + Check(LBuffered.Read(B,1)=0,'empty read did not return EOF'); + Check((B=123) and (LBuffered.Position=0),'EOF changed buffer/cursor'); + end; + 4: begin + CheckPattern(LBuffered,Length(LBytes),0); + Check(LBuffered.Position=Length(LBytes),'exact EOF cursor mismatch'); + B := 123; + Check(LBuffered.Read(B,1)=0,'EOF did not return zero'); + Check((B=123) and (LBuffered.Position=Length(LBytes)),'EOF advanced cursor or overwrote buffer'); + end; + 5: begin + LSource.ResetCounts; + B := 123; + Check(LBuffered.Read(B,0)=0,'zero read returned data'); + Check(LBuffered.Write(B,0)=0,'zero write returned data'); + Check((LSource.ReadCalls=0) and (LSource.WriteCalls=0),'zero count performed source I/O'); + Check((B=123) and (LBuffered.Position=0),'zero count changed state'); + end; + 6: begin + LRaised := False; + try + LBuffered.Read(B,-1); + except + on E: EArgumentException do LRaised := True; + end; + Check(LRaised,'negative read count was accepted'); + LRaised := False; + try + LBuffered.Write(B,-1); + except + on E: EArgumentException do LRaised := True; + end; + Check(LRaised,'negative write count was accepted'); + Check((LSource.ReadCalls=0) and (LSource.WriteCalls=0) and (LBuffered.Position=0),'negative count touched the source'); + end; + 7: begin + SetLength(LBytes,LSize+5); + LBuffered.ReadBuffer(LBytes[0],Length(LBytes)); + for I := 0 to Length(LBytes)-1 do Check(LBytes[I]=Byte(I mod 251),'ReadBuffer boundary mismatch'); + {$IFNDEF FPC} + LBuffered.Position := 0; + SetLength(LOffsetBytes,7); + FillChar(LOffsetBytes[0],Length(LOffsetBytes),123); + Check(LBuffered.Read(LOffsetBytes,2,3)=3,'derived TBytes Read overload count'); + Check((LOffsetBytes[0]=123) and (LOffsetBytes[1]=123) and + (LOffsetBytes[2]=0) and (LOffsetBytes[3]=1) and (LOffsetBytes[4]=2) and + (LOffsetBytes[5]=123),'derived TBytes Read ignored offset/boundaries'); + {$IF SizeOf(NativeInt)>SizeOf(Longint)} + LNativeCount := 2; + Check(LBuffered.Read(LOffsetBytes[0],LNativeCount)=LNativeCount,'derived NativeInt Read overload count'); + Check((LOffsetBytes[0]=3) and (LOffsetBytes[1]=4),'derived NativeInt Read bypassed logical cursor'); + {$IFEND}{$ENDIF} + end; + 8: begin + LSource.MaxRead := 1; + SetLength(LBytes,11); + LTotal := 0; + repeat + LRequested := 1+(LTotal mod Length(LBytes)); + N := LBuffered.Read(LBytes[0],LRequested); + Check((N>=0) and (N<=LRequested),'invalid short-read length'); + for I := 0 to N-1 do Check(LBytes[I]=Byte((LTotal+I) mod 251),'short reads lost or repeated data'); + Inc(LTotal,N); + until N=0; + Check(LTotal=2*LSize+17,'short source read was treated as permanent EOF'); + end; + end; + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; + end; +end; + +procedure CheckSeekScenario(AKind: Integer); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LReads, LSeeks: Integer; + B: Byte; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(100)); + LBuffered := TBufferedStream.Create(LSource,16); + try + CheckPattern(LBuffered,1,0); + case AKind of + 0: begin + Check(LBuffered.Seek(20,soBeginning)=20,'absolute seek result'); + CheckPattern(LBuffered,3,20); + Check(LBuffered.Seek(-2,soCurrent)=21,'relative seek result'); + CheckPattern(LBuffered,2,21); + Check(LBuffered.Seek(-3,soEnd)=97,'end seek result'); + CheckPattern(LBuffered,3,97); + Check(LBuffered.Seek(Longint(9),Word(0))=9,'legacy32-bit Seek overload result'); + CheckPattern(LBuffered,2,9); + end; + 1: begin + Check(LBuffered.Position=1,'cannot anchor the retained window'); + LReads := LSource.ReadCalls; + LSeeks := LSource.SeekCalls; + LBuffered.Position := 8; + CheckPattern(LBuffered,1,8); + LBuffered.Position := 0; + CheckPattern(LBuffered,1,0); + LBuffered.Position := 15; + CheckPattern(LBuffered,1,15); + Check((LSource.ReadCalls=LReads) and (LSource.SeekCalls=LSeeks), + 'seeking within a known retained window performed underlying I/O'); + CheckPattern(LBuffered,1,16); + end; + 2: begin + LBuffered.Position := 73; + CheckPattern(LBuffered,7,73); + LBuffered.Position := 2; + CheckPattern(LBuffered,5,2); + end; + 3: begin + Check(LBuffered.Seek(200,soBeginning)=200,'beyond-EOF seek result'); + Check((LBuffered.Position=200) and (LBuffered.Size=100),'beyond-EOF seek resized source'); + Check(LBuffered.Read(B,1)=0,'beyond-EOF read returned data'); + Check(LBuffered.Position=200,'beyond-EOF read moved cursor'); + LBuffered.Seek(-2,soEnd); + CheckPattern(LBuffered,2,98); + end; + end; + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckMutation(AKind: Integer); +var LSource: TObservedStream; + LGold: TMemoryStream; + LBuffered: TBufferedStream; + LBytes: TBytes; + LReadBytes: array[0..4] of Byte; + B: Byte; + I: Integer; + {$IFNDEF FPC}{$IF SizeOf(NativeInt)>SizeOf(Longint)} + LNativeCount: NativeInt; + {$IFEND}{$ENDIF} +begin + LSource := TObservedStream.Create; + LGold := TMemoryStream.Create; + try + Seed(LSource,Pattern(20)); + Seed(LGold,Pattern(20)); + LBuffered := TBufferedStream.Create(LSource,8); + try + CheckPattern(LBuffered,1,0); + LGold.Position := 1; + LBytes := Pattern(5); + for I := 0 to High(LBytes) do LBytes[I] := Byte(240+I); + case AKind of + 0: begin + Check(LBuffered.Write(LBytes[0],3)=3,'write count changed'); + LGold.WriteBuffer(LBytes[0],3); + {$IFNDEF FPC} + Check(LBuffered.Write(LBytes,1,3)=3,'derived TBytes Write overload count'); + LGold.WriteBuffer(LBytes[1],3); + {$IF SizeOf(NativeInt)>SizeOf(Longint)} + LNativeCount := 2; + Check(LBuffered.Write(LBytes[0],LNativeCount)=LNativeCount,'derived NativeInt Write overload count'); + LGold.WriteBuffer(LBytes[0],LNativeCount); + {$IFEND}{$ENDIF} + end; + 1: begin + LBuffered.Position := 0; + LGold.Position := 0; + LBuffered.WriteBuffer(LBytes[0],5); + LGold.WriteBuffer(LBytes[0],5); + LBuffered.Position := 0; + LBuffered.ReadBuffer(LReadBytes,SizeOf(LReadBytes)); + Check(CompareMem(@LReadBytes,@LBytes[0],SizeOf(LReadBytes)),'write left stale bytes in retained window'); + LGold.Position := 5; + end; + 2: begin + LSource.MaxWrite := 2; + Check(LBuffered.Write(LBytes[0],5)=2,'short write count was hidden'); + LGold.WriteBuffer(LBytes[0],2); + LSource.MaxWrite := 0; + end; + 3: begin + LBuffered.Position := 7; + LGold.Position := 7; + LBuffered.Size := 3; + LGold.Size := 3; + end; + 4: begin + LBuffered.Size := 30; + LGold.Size := 30; + LBuffered.Position := 20; + LGold.Position := 20; + LBuffered.WriteBuffer(LBytes[0],5); + LGold.WriteBuffer(LBytes[0],5); + LBuffered.WriteBuffer(LBytes[0],5); + LGold.WriteBuffer(LBytes[0],5); + end; + end; + Check(LBuffered.Position=LGold.Position,'mutation left incorrect logical cursor'); + Check(LBuffered.Size=LGold.Size,'mutation left incorrect size'); + CheckMemoryEqual(LSource,LGold); + LBuffered.Position := 0; + LGold.Position := 0; + while LGold.Position<LGold.Size do begin + LGold.ReadBuffer(B,1); + LBuffered.ReadBuffer(LBytes[0],1); + Check(B=LBytes[0],'mutation left stale buffered data'); + end; + finally + LBuffered.Free; + end; + finally + LGold.Free; + LSource.Free; + end; +end; + +procedure CheckSourceFailure(AKind: Integer); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LRaised: Boolean; + B: Byte; + LExpected: String; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(20)); + LBuffered := TBufferedStream.Create(LSource,8); + try + B := 123; + if AKind<>0 then CheckPattern(LBuffered,1,0); + LRaised := False; + case AKind of + 0: begin + LSource.FailRead := True; + LExpected := 'source-read'; + end; + 1,4: begin + LSource.FailWrite := True; + LExpected := 'source-write'; + end; + 2: begin + LSource.FailResize := True; + LExpected := 'source-resize'; + end; + 3: begin + LSource.FailSize := True; + LExpected := 'source-size'; + end; + end; + try + case AKind of + 0: LBuffered.Read(B,1); + 1,4: LBuffered.Write(B,1); + 2: LBuffered.Size := 3; + 3: Check(LBuffered.Size=20,'size query result'); + end; + except + on E: ESourceFailure do begin + Check(E.Message=LExpected,'wrapped exception identity/message changed'); + LRaised := True; + end; + end; + Check(LRaised,'wrapped failure did not propagate'); + LSource.FailRead := False; + LSource.FailWrite := False; + LSource.FailResize := False; + LSource.FailSize := False; + if AKind=0 then Check(LBuffered.Position=0,'failed read advanced cursor') + else Check(LBuffered.Position=1,'failed mutation advanced cursor'); + Check(LBuffered.Size=20,'failed operation changed size'); + if AKind=0 then CheckPattern(LBuffered,5,0) + else CheckPattern(LBuffered,5,1); + finally + LSource.FailRead := False; + LSource.FailWrite := False; + LSource.FailResize := False; + LSource.FailSize := False; + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckSparse; +var LSource: TSparseStream; + LBuffered: TBufferedStream; + LStart: Int64; + I: Integer; + LBytes: array[0..16] of Byte; +begin + LSource := TSparseStream.Create; + try + LStart := (Int64(1) shl 33)+123; + LSource.Position := LStart; + LBuffered := TBufferedStream.Create(LSource,3); + try + Check(LBuffered.Position=LStart,'initial64-bit cursor truncated'); + Check(LBuffered.Size=(Int64(1) shl 40)+123,'64-bit size truncated'); + LBuffered.ReadBuffer(LBytes,SizeOf(LBytes)); + for I := 0 to High(LBytes) do + Check(LBytes[I]=Byte(((LStart+I) xor ((LStart+I) shr 8)) and 255),'sparse64-bit read mismatch'); + Check(LBuffered.Seek(-2,soEnd)=(Int64(1) shl 40)+121,'64-bit end seek truncated'); + Check(LBuffered.Read(LBytes,SizeOf(LBytes))=2,'sparse EOF count mismatch'); + Check(LSource.LargestRead<=SizeOf(LBytes),'read-ahead copied a huge sparse source'); + Check(LSource.ReadCalls<30,'small sparse reads scanned the source'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +function NextRandom(var AState: UInt32; AMaximum: Integer): Integer; +begin + AState := AState xor (AState shl 13); + AState := AState xor (AState shr 17); + AState := AState xor (AState shl 5); + Result := Integer(AState and $7FFFFFFF) mod AMaximum; +end; + +procedure CheckMixed; +const Sizes: array[0..3] of Integer = (1,2,3,65536); +var LSource: TObservedStream; + LGold: TMemoryStream; + LBuffered: TBufferedStream; + LState: UInt32; + A, B: array[0..22] of Byte; + I, J, K, LKind, LCount, LReadA, LReadB: Integer; + LOffset: Int64; +begin + for J := Low(Sizes) to High(Sizes) do begin + LSource := TObservedStream.Create; + LGold := TMemoryStream.Create; + try + Seed(LSource,Pattern(128)); + Seed(LGold,Pattern(128)); + LBuffered := TBufferedStream.Create(LSource,Sizes[J]); + try + LState := $12345678; + for I := 1 to 1000 do begin + LKind := NextRandom(LState,7); + LCount := NextRandom(LState,Length(A)+1); + case LKind of + 0,1: begin + FillChar(A,SizeOf(A),113); + FillChar(B,SizeOf(B),113); + LReadA := LBuffered.Read(A,LCount); + LReadB := LGold.Read(B,LCount); + Check((LReadA=LReadB) and CompareMem(@A,@B,SizeOf(A)),'differential read mismatch at step '+IntToStr(I)); + end; + 2: begin + LOffset := NextRandom(LState,Integer(LGold.Size)+1); + Check(LBuffered.Seek(LOffset,soBeginning)=LGold.Seek(LOffset,soBeginning),'differential beginning seek'); + end; + 3: begin + LOffset := -NextRandom(LState,Integer(LGold.Size)+1); + Check(LBuffered.Seek(LOffset,soEnd)=LGold.Seek(LOffset,soEnd),'differential end seek'); + end; + 4: begin + for K := 0 to LCount-1 do A[K] := Byte(NextRandom(LState,256)); + Check(LBuffered.Write(A,LCount)=LGold.Write(A,LCount),'differential write count'); + end; + 5: begin + LOffset := NextRandom(LState,Integer(LGold.Size)+1); + LBuffered.Size := LOffset; + LGold.Size := LOffset; + end; + 6: begin + LOffset := NextRandom(LState,Integer(LGold.Size)+1); + LBuffered.Position := LOffset; + LGold.Position := LOffset; + end; + end; + Check(LBuffered.Position=LGold.Position,'differential cursor mismatch at step '+IntToStr(I)); + Check(LBuffered.Size=LGold.Size,'differential size mismatch at step '+IntToStr(I)); + CheckMemoryEqual(LSource,LGold); + end; + finally + LBuffered.Free; + end; + finally + LGold.Free; + LSource.Free; + end; + end; +end; + +procedure CheckWriteOnly; +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LBytes: TBytes; +begin + LSource := TObservedStream.Create; + try + LSource.FailRead := True; + LBuffered := TBufferedStream.Create(LSource,3); + try + LBytes := Pattern(10); + LBuffered.WriteBuffer(LBytes[0],Length(LBytes)); + Check((LSource.ReadCalls=0) and (LBuffered.Position=10) and (LBuffered.Size=10),'write-through performed hidden reads'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckForwardOnly(AKind: Integer); +var LSource: TForwardStream; + LBuffered: TBufferedStream; + B: Byte; + I, LRaised: Integer; +begin + LSource := TForwardStream.Create; + try + if AKind=3 then LBuffered := TBufferedStream.Create(LSource,1) + else LBuffered := TBufferedStream.Create(LSource,8); + try + B := 92; + Check((LSource.SeekCalls=0) and (LSource.SizeCalls=0), + 'construction probed unsupported source capabilities'); + if AKind=4 then begin + B := 91; + Check(LBuffered.Write(B,1)=1,'forward-only direct write failed'); + Check((LSource.WriteCalls=1) and (LSource.LastWritePosition=0) and (LSource.SeekCalls=0), + 'direct write required seeking or used a wrong cursor'); + Exit; + end; + CheckPattern(LBuffered,1,0); + if AKind=0 then begin + CheckPattern(LBuffered,23,1); + Check(LBuffered.Read(B,1)=0,'forward-only EOF incorrect'); + Check((LSource.SeekCalls=0) and (LSource.SizeCalls=0),'forward-only reads required seeking or size'); + end else if AKind=1 then begin + LRaised := 0; + for I := 0 to 2 do begin + try + case I of + 0: Check(LBuffered.Position=1,'forward position query'); + 1: LBuffered.Seek(0,soBeginning); + 2: Check(LBuffered.Size=24,'forward size query'); + end; + except + on E: ESourceFailure do begin + if I=2 then Check(E.Message='forward-size','changed unsupported Size exception') + else Check(E.Message='forward-seek','changed unsupported Position/Seek exception'); + Inc(LRaised); + end; + end; + CheckPattern(LBuffered,1,I+1); + end; + Check(LRaised=3,'unsupported source capability was silently invented'); + CheckPattern(LBuffered,3,4); + Check(LSource.ReadCalls=1,'unsupported capability query discarded unread data'); + end else if AKind=3 then begin + B := 92; + Check(LBuffered.Write(B,1)=1,'fully consumed forward buffer could not write'); + Check((LSource.SeekCalls=0) and (LSource.WriteCalls=1) and (LSource.LastWritePosition=1), + 'fully consumed buffer write required seeking'); + end else begin + LRaised := 0; + try + case AKind of + 2: LBuffered.Write(B,1); + 5: LBuffered.SynchronizePosition; + 6: LBuffered.Size := 10; + end; + except + on E: ESourceFailure do begin + Check(E.Message='forward-seek','changed unread-buffer synchronization failure'); + Inc(LRaised); + end; + end; + Check(LRaised=1,'unread forward buffer was silently discarded'); + Check((LSource.WriteCalls=0) and (LSource.ResizeCalls=0), + 'unsupported rewind mutated the underlying stream'); + CheckPattern(LBuffered,3,1); + Check(LSource.ReadCalls=1,'failed rewind discarded retained bytes'); + end; + finally + I := LSource.SeekCalls; + LBuffered.Free; + Check(LSource.SeekCalls=I,'destruction implicitly sought a forward-only source'); + end; + finally + LSource.Free; + end; +end; + +procedure CheckExplicitSynchronization(AFail: Boolean); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LRaised: Boolean; + LReads: Integer; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8); + try + CheckPattern(LBuffered,1,0); + Check(LSource.Position=8,'expected real read-ahead before synchronization'); + LReads := LSource.ReadCalls; + LSource.FailSeek := AFail; + LRaised := False; + try + LBuffered.SynchronizePosition; + except + on E: ESourceFailure do begin + Check(E.Message='source-seek','changed explicit synchronization exception'); + LRaised := True; + end; + end; + Check(LRaised=AFail,'explicit synchronization failure contract changed'); + LSource.FailSeek := False; + Check(LBuffered.Position=1,'explicit synchronization changed logical position'); + if not AFail then Check(LSource.Position=1,'explicit synchronization did not align wrapped position'); + CheckPattern(LBuffered,3,1); + if AFail then Check(LSource.ReadCalls=LReads,'failed synchronization discarded valid retained bytes'); + finally + LSource.FailSeek := False; + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckBorrowedDestruction; +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LPhysical: Int64; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8); + try + Check(LBuffered.WrappedStream=LSource,'WrappedStream property changed'); + Check((LBuffered.BufferSize=8) and not LBuffered.OwnsStream,'constructor properties changed'); + CheckPattern(LBuffered,1,0); + LPhysical := LSource.Position; + LSource.ResetCounts; + LSource.FailSeek := True; + finally + // Disposal releases ownership only. SynchronizePosition is explicit so + // borrowed pipes remain usable and cleanup never hides an I/O exception. + LBuffered.Free; + end; + Check(LSource.SeekCalls=0,'borrowed destruction sought implicitly'); + LSource.FailSeek := False; + Check((LPhysical=8) and (LSource.Position=LPhysical),'borrowed destruction rewound physical read-ahead'); + finally + LSource.Free; + end; +end; + +procedure CheckPartialFailure(AKind: Integer); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LBytes: array[0..4] of Byte; + LRaised: Boolean; + LExpected: String; + LPosition: Integer; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8); + try + Check(LBuffered.Position=0,'initial partial-failure cursor'); + FillChar(LBytes,SizeOf(LBytes),99); + LPosition := 0; + LExpected := ''; + case AKind of + 0: begin + LSource.FailRead := True; + LSource.MoveBeforeReadFailure := True; + LExpected := 'source-read'; + LPosition := 2; + end; + 1: begin + CheckPattern(LBuffered,1,0); + LSource.FailWrite := True; + LSource.MoveBeforeWriteFailure := True; + LExpected := 'source-write'; + LPosition := 3; + end; + 2: begin + CheckPattern(LBuffered,1,0); + LSource.FailSeek := True; + LSource.MoveBeforeSeekFailure := True; + LExpected := 'source-seek'; + LPosition := 13; + end; + end; + LRaised := False; + try + case AKind of + 0: LBuffered.Read(LBytes,SizeOf(LBytes)); + 1: LBuffered.Write(LBytes,SizeOf(LBytes)); + 2: LBuffered.Seek(17,soBeginning); + end; + except + on E: ESourceFailure do begin + Check(E.Message=LExpected,'partial-I/O source exception was replaced'); + LRaised := True; + end; + end; + Check(LRaised,'partial-I/O exception did not propagate'); + LSource.FailRead := False; + LSource.FailWrite := False; + LSource.FailSeek := False; + if AKind=2 then begin + // An arbitrary failing seek may already have moved the source. Retain + // pending bytes, then restore an absolute anchor before using Position. + CheckPattern(LBuffered,3,1); + LBuffered.Seek(LPosition,soBeginning); + end; + Check(LBuffered.Position=LPosition,'failed I/O retained a stale logical cursor'); + CheckPattern(LBuffered,3,LPosition); + finally + LSource.FailRead := False; + LSource.FailWrite := False; + LSource.FailSeek := False; + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckNegativeSeekResult(AKind: Integer); +var LSource: TForwardStream; + LBuffered: TBufferedStream; + LSeeks, I: Integer; + LRaised: Boolean; + B: Byte; +begin + LSource := TForwardStream.Create; + try + LSource.ReturnNegativeSeek := True; + LSource.ReturnNegativeSize := True; + LBuffered := TBufferedStream.Create(LSource,8); + try + B := 119; + CheckPattern(LBuffered,1,0); + case AKind of + 0: begin + LSeeks := LSource.SeekCalls; + Check(LBuffered.Position=-1,'negative Position was adjusted by unread data'); + CheckPattern(LBuffered,1,1); + Check(LBuffered.Position=-1,'failed Position was cached as a real cursor'); + Check(LSource.SeekCalls=LSeeks+2,'negative Position suppressed a later capability query'); + CheckPattern(LBuffered,3,2); + end; + 1: begin + Check(LBuffered.Seek(0,soBeginning)=-1,'negative Seek result was changed'); + CheckPattern(LBuffered,3,1); + Check(LBuffered.Size=-1,'negative Size result was changed'); + CheckPattern(LBuffered,3,4); + Check(LSource.ReadCalls=1,'negative capability result discarded buffered bytes'); + end; + 2: begin + for I := 0 to 1 do begin + LRaised := False; + try + if I=0 then LBuffered.Write(B,1) + else LBuffered.Size := 5; + except + on E: EStreamError do LRaised := True; + end; + Check(LRaised,'negative synchronization result allowed a direction change'); + Check((LSource.WriteCalls=0) and (LSource.ResizeCalls=0), + 'negative synchronization result mutated its source'); + end; + CheckPattern(LBuffered,3,1); + end; + end; + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckNegativeIoResult(AWrite: Boolean); +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LBytes: array[0..4] of Byte; + LExpectedPosition, LSeeks: Integer; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8); + try + Check(LBuffered.Position=0,'cannot anchor negative-I/O fixture'); + FillChar(LBytes,SizeOf(LBytes),99); + if AWrite then begin + CheckPattern(LBuffered,1,0); + LSource.NegativeWriteResult := True; + Check(LBuffered.Write(LBytes,SizeOf(LBytes))=-1,'negative Write result was changed'); + LSource.NegativeWriteResult := False; + LExpectedPosition := 3; + end else begin + LSource.NegativeReadResult := True; + LSeeks := LSource.SeekCalls; + Check(LBuffered.Read(LBytes,SizeOf(LBytes))=-1,'negative Read result was changed'); + Check(LSource.SeekCalls=LSeeks,'negative read performed recovery I/O'); + LSource.NegativeReadResult := False; + LExpectedPosition := 2; + end; + Check(LBuffered.Position=LExpectedPosition,'negative I/O retained a stale logical cursor'); + CheckPattern(LBuffered,3,LExpectedPosition); + finally + LSource.NegativeReadResult := False; + LSource.NegativeWriteResult := False; + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckSingleSourceRead; +var LSource: TObservedStream; + LBuffered: TBufferedStream; + LBytes: array[0..4095] of Byte; + I: Integer; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(256)); + LSource.MaxRead := 30; + LSource.ResetCounts; + LSource.RejectSecondRead := True; + LBuffered := TBufferedStream.Create(LSource,64); + try + // The next source read would block a peer. Throw deterministically if it + // is attempted so this test proves one-call behavior without timing. + Check(LBuffered.Read(LBytes,SizeOf(LBytes))=30,'positive short read did not return available bytes'); + Check(LSource.ReadCalls=1,'one caller Read attempted multiple source reads'); + for I := 0 to 29 do Check(LBytes[I]=I,'positive short read changed data'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckUncachedSizeIsDynamic; +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,False); + try + Check(not LBuffered.EnableStreamLengthCaching,'explicitly disabled length caching was ignored'); + Check((LSource.SizeCalls=0) and (LSource.SeekCalls=0),'construction probed metadata'); + Check(LBuffered.Size=24,'uncached size was incorrect'); + LSource.Size := 31; + Check(LBuffered.Size=31,'uncached size did not observe source growth'); + Check(LSource.SizeCalls=2,'uncached size did not forward both queries'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSizeQueriesOnce(AEmpty: Boolean); +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; + LExpected, LTotal: Int64; + I, LInitialSizeSeeks: Integer; + B: Byte; +begin + LSource := TSizeObservedStream.Create; + try + if not AEmpty then Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True); + try + Check(LBuffered.EnableStreamLengthCaching and not LBuffered.OwnsStream,'constructor flags were not retained independently'); + Check((LSource.SizeCalls=0) and (LSource.SeekCalls=0),'cached construction probed metadata'); + if AEmpty then LExpected := 0 else LExpected := 24; + // Delphi may seek and restore inside the first physical Size query. + LTotal := LBuffered.Size; + LInitialSizeSeeks := LSource.SeekCalls; + for I := 2 to 1000000 do Inc(LTotal,LBuffered.Size); + Check(LTotal=LExpected*1000000,'cached size queries changed the result'); + Check(LSource.SizeCalls=1,'cached size made more than one physical query'); + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=LInitialSizeSeeks),'cached queries read input or moved the source'); + Check(LSource.Position=0,'size queries changed the source cursor'); + if not AEmpty then begin + // Opting in means that out-of-band growth is deliberately not polled. + LSource.Size := 31; + Check(LBuffered.Size=24,'cached length unexpectedly polled external growth'); + Check(LSource.SizeCalls=1,'cached length queried after external growth'); + B := 99; + LBuffered.WriteBuffer(B,1); + Check(LSource.SizeCalls=1,'write proactively queried length'); + Check(LBuffered.Size=31,'mutation did not reload source length'); + Check(LSource.SizeCalls=2,'mutation did not invalidate the cached length'); + end; + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedConstructionDoesNotProbe; +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; + LRaised: Boolean; + LUnusedSize: Int64; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LSource.FailSize := True; + LSource.FailSeek := True; + LBuffered := TBufferedStream.Create(LSource,8,False,True); + try + Check((LSource.SizeCalls=0) and (LSource.SeekCalls=0),'cached constructor probed unsupported metadata'); + CheckPattern(LBuffered,1,0); + LRaised := False; + try + LUnusedSize := LBuffered.Size; + except + on E: ESourceFailure do LRaised := E.Message='source-size'; + end; + Check(LRaised,'first cached Size query did not preserve the source failure'); + CheckPattern(LBuffered,3,1); + Check(LSource.ReadCalls=1,'metadata failure lost unread buffered data'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSizeExceptionIsRetried(AReadBuffering: Boolean = True); +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; + LRaised: Boolean; + LUnusedSize: Int64; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True,AReadBuffering); + try + LSource.FailSize := True; + LRaised := False; + try + LUnusedSize := LBuffered.Size; + except + on E: ESourceFailure do LRaised := E.Message='source-size'; + end; + Check(LRaised and (LSource.SizeCalls=1),'failed Size query was hidden'); + LSource.FailSize := False; + Check(LBuffered.Size=24,'Size query did not retry after failure'); + Check(LSource.SizeCalls=2,'failed length was cached'); + LSource.FailSize := True; + Check(LBuffered.Size=24,'known cached length queried a failing source'); + Check(LSource.SizeCalls=2,'known cached length performed additional I/O'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedNegativeSizeIsRetried(AReadBuffering: Boolean = True); +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LSource.NegativeSize := True; + LBuffered := TBufferedStream.Create(LSource,8,False,True,AReadBuffering); + try + Check((LBuffered.Size=-1) and (LBuffered.Size=-1),'negative size sentinel was changed'); + Check(LSource.SizeCalls=2,'negative length was cached'); + LSource.NegativeSize := False; + Check(LBuffered.Size=24,'valid size did not replace a negative result'); + LSource.NegativeSize := True; + Check(LBuffered.Size=24,'valid cached size was not retained'); + Check(LSource.SizeCalls=3,'cached size queried the source again'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSizeAfterOverwriteAndAppend(AReadBuffering: Boolean = True); +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; + LBytes: array[0..3] of Byte; +begin + FillChar(LBytes,SizeOf(LBytes),99); + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True,AReadBuffering); + try + Check(LBuffered.Size=24,'cannot seed length cache'); + CheckPattern(LBuffered,1,0); + LBuffered.WriteBuffer(LBytes,SizeOf(LBytes)); + Check(LSource.SizeCalls=1,'overwrite proactively queried Size'); + Check(LBuffered.Size=24,'overwrite changed the source length'); + Check(LSource.SizeCalls=2,'overwrite retained a possibly stale size'); + LBuffered.Seek(Int64(0),soEnd); + LBuffered.WriteBuffer(LBytes,3); + Check(LSource.SizeCalls=2,'append proactively queried Size'); + Check(LBuffered.Size=27,'append did not refresh the cached size'); + Check(LSource.SizeCalls=3,'append did not invalidate size'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSizeAfterWriteFailure(AMode: Integer; AReadBuffering: Boolean = True); +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; + LBytes: array[0..4] of Byte; + LRaised: Boolean; +begin + FillChar(LBytes,SizeOf(LBytes),99); + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True,AReadBuffering); + try + Check(LBuffered.Size=24,'cannot seed failing-write length cache'); + LBuffered.Seek(Int64(0),soEnd); + case AMode of + 0: LSource.MaxWrite := 2; + 1: LSource.NegativeWriteResult := True; + 2: begin + LSource.FailWrite := True; + LSource.MoveBeforeWriteFailure := True; + end; + end; + LRaised := False; + try + case AMode of + 0: Check(LBuffered.Write(LBytes,SizeOf(LBytes))=2,'short write result changed'); + 1: Check(LBuffered.Write(LBytes,SizeOf(LBytes))=-1,'negative write result changed'); + 2: LBuffered.Write(LBytes,SizeOf(LBytes)); + end; + except + on E: ESourceFailure do LRaised := E.Message='source-write'; + end; + Check((AMode<>2) or LRaised,'partial write exception was changed'); + Check(LSource.SizeCalls=1,'failing write performed hidden Size I/O'); + LSource.FailWrite := False; + LSource.NegativeWriteResult := False; + Check(LBuffered.Size=26,'partial write retained the pre-write cached length'); + Check(LSource.SizeCalls=2,'partial write did not force a new Size query'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSizeAfterResize(AReadBuffering: Boolean = True); +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True,AReadBuffering); + try + Check(LBuffered.Size=24,'cannot seed resize length cache'); + CheckPattern(LBuffered,1,0); + LBuffered.Size := 7; + Check(LSource.SizeCalls=1,'truncate proactively queried Size'); + Check(LBuffered.Size=7,'truncate retained the old cached size'); + LBuffered.Size := 31; + Check(LSource.SizeCalls=2,'expansion proactively queried Size'); + Check(LBuffered.Size=31,'expansion retained the old cached size'); + Check((LSource.SizeCalls=3) and (LSource.ResizeCalls=2),'resize did not reload each actual source length'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSizeAfterFailedAndIgnoredResize(AReadBuffering: Boolean = True); +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; + LRaised: Boolean; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True,AReadBuffering); + try + Check(LBuffered.Size=24,'cannot seed partial-resize length cache'); + LSource.FailResizeAfterMutation := True; + LRaised := False; + try + LBuffered.Size := 9; + except + on E: ESourceFailure do LRaised := E.Message='source-resize-after-mutation'; + end; + Check(LRaised,'partial resize exception was changed'); + Check(LSource.SizeCalls=1,'failing resize queried metadata during unwind'); + LSource.FailResizeAfterMutation := False; + Check(LBuffered.Size=9,'failed partial resize retained stale size'); + LSource.IgnoreResize := True; + LBuffered.Size := 99; + Check(LSource.SizeCalls=2,'ignored resize proactively queried Size'); + Check(LBuffered.Size=9,'requested size was cached without source confirmation'); + Check((LSource.SizeCalls=3) and (LSource.ResizeCalls=2),'failed/no-op resize did not requery actual length'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSizeAfterFailedSynchronization; +var LSource: TSizeObservedStream; + LBuffered: TBufferedStream; + B: Byte; + I: Integer; + LRaised: Boolean; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True); + try + Check(LBuffered.Size=24,'cannot seed rejected-mutation length cache'); + CheckPattern(LBuffered,1,0); + LSource.FailSeek := True; + B := 99; + for I := 0 to 1 do begin + LRaised := False; + try + if I=0 then LBuffered.Write(B,1) + else LBuffered.Size := 9; + except + on E: ESourceFailure do LRaised := E.Message='source-seek'; + end; + Check(LRaised,'failed synchronization was hidden by size caching'); + end; + Check((LSource.WriteCalls=0) and (LSource.ResizeCalls=0),'rejected direction change mutated source'); + Check(LSource.SizeCalls=1,'rejected direction change queried source size'); + CheckPattern(LBuffered,3,1); + Check(LSource.ReadCalls=1,'rejected direction change discarded unread data'); + LSource.FailSeek := False; + Check(LBuffered.Size=24,'rejected mutation left an incorrect size'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckCachedSparseLengthAndZeroWrite(AReadBuffering: Boolean = True); +var LSource: TCountedSparseStream; + LBuffered: TBufferedStream; + LExpected: Int64; + B: Byte; +begin + LSource := TCountedSparseStream.Create; + try + LExpected := (Int64(1) shl 40)+123; + LBuffered := TBufferedStream.Create(LSource,16,False,True,AReadBuffering); + try + Check((LBuffered.Size=LExpected) and (LBuffered.Size=LExpected),'cached sparse size was truncated'); + Check(LSource.SizeCalls=1,'sparse length was not cached'); + LBuffered.Position := LExpected-5; + Check(LBuffered.Read(B,1)=1,'sparse cached stream was not readable'); + Check(LBuffered.Write(B,0)=0,'zero write result changed'); + Check(LBuffered.Size=LExpected,'read/zero-write changed cached length'); + Check(LSource.SizeCalls=1,'read/zero-write invalidated the length cache'); + Check(LSource.ReadCalls=1,'cached sparse read queried the input repeatedly'); + if AReadBuffering then + Check(LSource.LargestRead=16,'buffered sparse read lost its configured capacity') + else Check(LSource.LargestRead=1,'length-only caching enabled byte read-ahead'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDefaultBufferingEnabled; +var LSource: TObservedStream; LBuffered: TBufferedStream; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8); + try + Check(LBuffered.EnableReadBuffering and LBuffered.EnableStreamLengthCaching,'buffering flags were not both enabled by default'); + Check((LBuffered.Size=24) and (LBuffered.Size=24) and (LSource.SizeCalls=1),'default length cache was not reused'); + CheckPattern(LBuffered,1,0); + Check((LSource.ReadCalls=1) and (LSource.LargestRead=8),'default buffering no longer reads ahead'); + CheckPattern(LBuffered,7,1); + Check(LSource.ReadCalls=1,'default buffer did not serve retained bytes'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisabledConstructorDirectReads; +var LSource: TObservedStream; LBuffered: TBufferedStream; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + Check(not LBuffered.EnableReadBuffering,'constructor ignored disabled buffering'); + Check(LBuffered.BufferSize=8,'disabled constructor lost configured buffer size'); + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'disabled constructor probed the input'); + CheckPattern(LBuffered,3,0); + Check((LSource.ReadCalls=1) and (LSource.LargestRead=3),'disabled read changed the requested count'); + CheckPattern(LBuffered,14,3); + Check((LSource.ReadCalls=2) and (LSource.LargestRead=14),'disabled large read was buffered or split'); + Check((LSource.SeekCalls=0) and (LSource.SizeCalls=0),'disabled reads queried metadata'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisabledShortReadsAndEOF; +var LSource: TObservedStream; LBuffered: TBufferedStream; LBytes: array[0..31] of Byte; LCount: Integer; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LSource.MaxRead := 3; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + LCount := LBuffered.Read(LBytes[0],Longint(10)); + Check((LCount=3) and (LBytes[0]=0) and (LBytes[2]=2),'disabled short read changed source data/count'); + Check((LSource.ReadCalls=1) and (LSource.LargestRead=10),'disabled short read retried the input'); + LSource.MaxRead := 0; + CheckPattern(LBuffered,21,3); + Check(LBuffered.Read(LBytes[0],Longint(10))=0,'disabled EOF did not return zero'); + Check(LSource.ReadCalls=3,'disabled short read or EOF made extra attempts'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisabledForwardOnlyReads; +var LSource: TForwardStream; LBuffered: TBufferedStream; +begin + LSource := TForwardStream.Create; + try + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + CheckPattern(LBuffered,1,0); + CheckPattern(LBuffered,5,1); + Check((LSource.ReadCalls=2) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'disabled forward reads required unsupported capabilities'); + LBuffered.EnableReadBuffering := True; + Check((LSource.SeekCalls=0) and (LSource.SizeCalls=0),'enabling forward input probed metadata'); + CheckPattern(LBuffered,1,6); + CheckPattern(LBuffered,7,7); + Check(LSource.ReadCalls=3,'reenabled forward input did not read ahead'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisableUnreadRejectedWithoutIO; +var LSource: TObservedStream; LBuffered: TBufferedStream; LRaised: Boolean; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8); + try + CheckPattern(LBuffered,1,0); + LSource.ResetCounts; + LRaised := False; + try + LBuffered.EnableReadBuffering := False; + except + on E: EStreamError do LRaised := True; + end; + Check(LRaised and LBuffered.EnableReadBuffering,'disabling unread data was not rejected atomically'); + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0) and (LSource.WriteCalls=0),'rejected disable performed source I/O'); + CheckPattern(LBuffered,7,1); + Check(LSource.ReadCalls=0,'rejected disable lost retained bytes'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckFlushUnreadDataAllowsDisabling; +var LSource: TObservedStream; LBuffered: TBufferedStream; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8); + try + CheckPattern(LBuffered,1,0); + LSource.ResetCounts; + LBuffered.FlushUnreadData; + Check((LSource.SeekCalls=1) and (LSource.ReadCalls=0),'explicit flush did not perform just the required rewind'); + Check(LBuffered.EnableReadBuffering,'flushing changed the buffering mode'); + LBuffered.EnableReadBuffering := False; + Check((not LBuffered.EnableReadBuffering) and (LSource.SeekCalls=1),'disable after flush performed extra I/O'); + CheckPattern(LBuffered,1,1); + Check((LSource.ReadCalls=1) and (LSource.LargestRead=1),'flushed cursor did not continue in direct mode'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckEmptyAndRepeatedTogglesDoNoIO; +var LSource: TObservedStream; LBuffered: TBufferedStream; I: Integer; +begin + LSource := TObservedStream.Create; + try + LSource.FailRead := True; + LSource.FailSeek := True; + LSource.FailSize := True; + LBuffered := TBufferedStream.Create(LSource,8); + try + for I := 1 to 4 do begin + LBuffered.EnableReadBuffering := True; + LBuffered.EnableReadBuffering := False; + LBuffered.EnableReadBuffering := False; + LBuffered.FlushUnreadData; + LBuffered.EnableReadBuffering := True; + end; + Check(LBuffered.EnableReadBuffering,'empty toggles lost the requested final mode'); + Check(LBuffered.BufferSize=8,'toggling discarded the configured buffer capacity'); + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'empty/no-op toggle or flush probed the input'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckReenablePreservesDirectCursor; +var LSource: TObservedStream; LBuffered: TBufferedStream; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.Position := 5; + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + CheckPattern(LBuffered,3,5); + LBuffered.EnableReadBuffering := True; + Check(LSource.SeekCalls=0,'reenabling queried the source cursor'); + CheckPattern(LBuffered,1,8); + CheckPattern(LBuffered,7,9); + Check(LSource.ReadCalls=2,'reenabling did not retain one buffer'); + LSource.ResetCounts; + LBuffered.FlushUnreadData; + LBuffered.EnableReadBuffering := False; + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0),'consumed-buffer disable/flush performed I/O'); + CheckPattern(LBuffered,2,16); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisabledSparseSeekOrigins; +var LSource: TSparseStream; LBuffered: TBufferedStream; LStart, LLength: Int64; LBytes: array[0..1] of Byte; I: Integer; +begin + LSource := TSparseStream.Create; + try + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + LStart := (Int64(1) shl 40)+17; + LLength := LSource.Size; + Check(LBuffered.Seek(LStart,soBeginning)=LStart,'disabled beginning seek truncated a 64-bit offset'); + Check(LBuffered.Read(LBytes[0],Longint(2))=2,'disabled sparse read failed'); + for I := 0 to 1 do + Check(LBytes[I]=Byte(((LStart+I) xor ((LStart+I) shr 8)) and 255),'disabled sparse bytes differ'); + Check(LBuffered.Seek(-1,soCurrent)=LStart+1,'disabled current seek was incorrect'); + Check(LBuffered.Seek(-2,soEnd)=LLength-2,'disabled end seek was incorrect'); + Check(LBuffered.Read(LBytes[0],Longint(2))=2,'disabled sparse end read failed'); + Check(LSource.LargestRead=2,'disabled sparse read allocated/read ahead'); + Check(LBuffered.Position=LLength,'disabled Position was not the source cursor'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisabledWriteAndResize; +var LSource: TObservedStream; LGold: TMemoryStream; LBuffered: TBufferedStream; LData: array[0..1] of Byte; LRaised: Boolean; +begin + LSource := TObservedStream.Create; + LGold := TMemoryStream.Create; + try + Seed(LSource,Pattern(24)); + Seed(LGold,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + LBuffered.Position := 3; + LGold.Position := 3; + LData[0] := 201; + LData[1] := 202; + Check(LBuffered.Write(LData[0],Longint(2))=LGold.Write(LData[0],Longint(2)),'disabled write count differs'); + Check(LSource.ReadCalls=0,'disabled write read the input'); + LBuffered.Size := 12; + LGold.Size := 12; + Check(LBuffered.Position=LSource.Position,'disabled resize changed source cursor semantics'); + CheckMemoryEqual(LSource,LGold); + LBuffered.Position := 11; + Check(LBuffered.Read(LData[0],Longint(2))=1,'disabled truncate did not affect following reads'); + LSource.FailWrite := True; + LRaised := False; + try + LBuffered.Write(LData[0],Longint(1)); + except + on E: ESourceFailure do LRaised := E.Message='source-write'; + end; + Check(LRaised,'disabled write changed the source exception'); + LSource.FailWrite := False; + LSource.FailResize := True; + LRaised := False; + try + LBuffered.Size := 7; + except + on E: ESourceFailure do LRaised := E.Message='source-resize'; + end; + Check(LRaised,'disabled resize changed the source exception'); + LSource.FailResize := False; + finally + LBuffered.Free; + end; + finally + LGold.Free; + LSource.Free; + end; +end; + +procedure CheckDisabledReadExceptionIdentity; +var LSource: TInjectedReadFailureStream; LBuffered: TBufferedStream; LExpected: Exception; LRaised: Boolean; B: Byte; +begin + LSource := TInjectedReadFailureStream.Create; + try + LExpected := ESourceFailure.Create('same-exception-object'); + LSource.Failure := LExpected; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + LRaised := False; + try + LBuffered.Read(B,Longint(1)); + except + on E: Exception do begin + LRaised := True; + Check(E=LExpected,'disabled read replaced the exception object'); + end; + end; + Check(LRaised and (LSource.ReadCalls=1),'disabled read failed to forward exactly one source attempt'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisabledNegativePartialRead; +var LSource: TObservedStream; LBuffered: TBufferedStream; LData: array[0..3] of Byte; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + LSource.NegativeReadResult := True; + Check(LBuffered.Read(LData[0],Longint(4))=-1,'disabled read changed a negative source result'); + Check(LSource.ReadCalls=1,'negative disabled read retried the source'); + LSource.NegativeReadResult := False; + Check(LBuffered.Position=2,'disabled read concealed partial source advancement'); + LBuffered.EnableReadBuffering := True; + CheckPattern(LBuffered,2,2); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckFailedFlushPreservesUnread; +var LSource: TObservedStream; LBuffered: TBufferedStream; LRaised: Boolean; +begin + LSource := TObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8); + try + CheckPattern(LBuffered,1,0); + LSource.ResetCounts; + LSource.FailSeek := True; + LRaised := False; + try + LBuffered.FlushUnreadData; + except + on E: ESourceFailure do LRaised := E.Message='source-seek'; + end; + Check(LRaised and LBuffered.EnableReadBuffering,'failed flush changed exception or mode'); + Check((LSource.SeekCalls=1) and (LSource.ReadCalls=0),'failed flush performed extra I/O'); + LRaised := False; + try + LBuffered.EnableReadBuffering := False; + except + on E: EStreamError do LRaised := True; + end; + Check(LRaised and (LSource.SeekCalls=1),'disable after failed flush attempted an automatic rewind'); + CheckPattern(LBuffered,7,1); + Check(LSource.ReadCalls=0,'failed flush discarded unread bytes'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckForwardOnlyFlushPreservesUnread(ANegativeSeek: Boolean); +var LSource: TForwardStream; LBuffered: TBufferedStream; LRaised: Boolean; +begin + LSource := TForwardStream.Create; + try + LSource.ReturnNegativeSeek := ANegativeSeek; + LBuffered := TBufferedStream.Create(LSource,8); + try + CheckPattern(LBuffered,1,0); + LRaised := False; + try + LBuffered.EnableReadBuffering := False; + except + on E: EStreamError do LRaised := True; + end; + Check(LRaised and (LSource.SeekCalls=0),'unread forward disable touched the source'); + LRaised := False; + try + LBuffered.FlushUnreadData; + except + on E: EStreamError do LRaised := True; + end; + Check(LRaised and LBuffered.EnableReadBuffering,'failed forward flush changed mode'); + Check((LSource.SeekCalls=1) and (LSource.ReadCalls=1),'forward flush retried/read input'); + CheckPattern(LBuffered,7,1); + Check(LSource.ReadCalls=1,'failed forward flush lost retained bytes'); + LBuffered.FlushUnreadData; + LBuffered.EnableReadBuffering := False; + Check(LSource.SeekCalls=1,'empty forward flush/disable attempted seeking'); + CheckPattern(LBuffered,1,8); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDisabledReadBufferingCachesLength; +var LSource: TSizeObservedStream; LBuffered: TBufferedStream; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True,False); + try + Check(LBuffered.EnableStreamLengthCaching and not LBuffered.EnableReadBuffering,'constructor flags were not independent'); + Check((LSource.SizeCalls=0) and (LSource.SeekCalls=0),'disabled cached constructor probed metadata'); + Check(LBuffered.Size=24,'disabled size query was incorrect'); + Check(LBuffered.Size=24,'direct-mode cached length changed'); + Check(LSource.SizeCalls=1,'disabled read buffering bypassed length caching'); + LSource.Size := 31; + Check(LBuffered.Size=24,'length caching polled external growth in direct mode'); + Check(LSource.SizeCalls=1,'direct-mode cache queried the source again'); + LBuffered.EnableStreamLengthCaching := False; + Check(LBuffered.Size=31,'disabling length caching did not query the source'); + Check(LSource.SizeCalls=2,'disabled length caching did not forward Size'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckReenableInvalidatesSizeAndPosition; +var LSource: TSizeObservedStream; LBuffered: TBufferedStream; LCalls: Integer; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True); + try + Check((LBuffered.Size=24) and (LBuffered.Position=0),'initial cached metadata was incorrect'); + LBuffered.EnableReadBuffering := False; + LBuffered.Position := 11; + LSource.Size := 31; + Check(LBuffered.Size=31,'disabled size retained the old enabled cache'); + LSource.ResetCounts; + LBuffered.EnableReadBuffering := True; + Check((LSource.SizeCalls=0) and (LSource.SeekCalls=0),'reenabling proactively queried metadata'); + Check(LBuffered.Position=11,'reenabling retained the old logical cursor'); + Check(LBuffered.Size=31,'reenabling retained the old cached length'); + LCalls := LSource.SizeCalls; + Check((LBuffered.Size=31) and (LSource.SizeCalls=LCalls),'reenabled size cache was not reused'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckToggledOwnership; +var LSource: TObservedStream; LBuffered: TBufferedStream; LDisposed, I: Integer; LOwns: Boolean; +begin + for I := 0 to 1 do begin + LDisposed := 0; + LOwns := I=1; + LSource := TObservedStream.Create(@LDisposed); + try + Seed(LSource,Pattern(24)); + LBuffered := TBufferedStream.Create(LSource,8,LOwns,False,False); + try + CheckPattern(LBuffered,1,0); + LBuffered.EnableReadBuffering := True; + CheckPattern(LBuffered,1,1); + LBuffered.FlushUnreadData; + LBuffered.EnableReadBuffering := False; + Check(LBuffered.OwnsStream=LOwns,'toggle changed stream ownership'); + finally + LBuffered.Free; + if LOwns then LSource := nil; + end; + if LOwns then Check(LDisposed=1,'owned toggled source was not disposed once') + else Check(LDisposed=0,'borrowed toggled source was disposed'); + finally + LSource.Free; + end; + Check(LDisposed=1,'toggled source lifetime leaked or disposed twice'); + end; +end; + +procedure CheckAlternatingModesMatchMemoryStream; +var LSource: TObservedStream; LGold: TMemoryStream; LBuffered: TBufferedStream; LActual, LExpected: array[0..15] of Byte; I, LCount, LRead, LGoldRead, LPosition: Integer; +begin + LSource := TObservedStream.Create; + LGold := TMemoryStream.Create; + try + Seed(LSource,Pattern(64)); + Seed(LGold,Pattern(64)); + LBuffered := TBufferedStream.Create(LSource,8); + try + for I := 0 to 49 do begin + if (I mod 3)=0 then begin + if LBuffered.EnableReadBuffering then LBuffered.FlushUnreadData; + LBuffered.EnableReadBuffering := not LBuffered.EnableReadBuffering; + end; + if (I mod 7)=0 then begin + LPosition := (I*13) mod 64; + LBuffered.Position := LPosition; + LGold.Position := LPosition; + end; + LCount := (I mod 9)+1; + LRead := LBuffered.Read(LActual[0],Longint(LCount)); + LGoldRead := LGold.Read(LExpected[0],Longint(LCount)); + Check(LRead=LGoldRead,'alternating modes returned a different read count'); + if LRead>0 then Check(CompareMem(@LActual[0],@LExpected[0],LRead),'alternating modes changed bytes'); + Check(LBuffered.Position=LGold.Position,'alternating modes changed logical position'); + end; + finally + LBuffered.Free; + end; + finally + LGold.Free; + LSource.Free; + end; +end; + +procedure CheckDisabledArgumentValidation; +var LSource: TObservedStream; LBuffered: TBufferedStream; B: Byte; LRaised: Boolean; +begin + LSource := TObservedStream.Create; + try + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + B := 77; + Check((LBuffered.Read(B,Longint(0))=0) and (LBuffered.Write(B,Longint(0))=0),'disabled zero counts changed results'); + LRaised := False; + try + LBuffered.Read(B,Longint(-1)); + except + on E: EArgumentOutOfRangeException do LRaised := True; + end; + Check(LRaised,'disabled negative read was accepted'); + LRaised := False; + try + LBuffered.Write(B,Longint(-1)); + except + on E: EArgumentOutOfRangeException do LRaised := True; + end; + Check(LRaised,'disabled negative write was accepted'); + LRaised := False; + try + LBuffered.Size := -1; + except + on E: EArgumentOutOfRangeException do LRaised := True; + end; + Check(LRaised,'disabled negative size was accepted'); + Check((LSource.ReadCalls=0) and (LSource.WriteCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'disabled invalid/empty operations performed I/O'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckLengthCachingTogglesWithUnreadBytes; +var LSource: TSizeObservedStream; LBuffered: TBufferedStream; LReads, LSeeks, LSizes: Integer; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8); + try + Check(LBuffered.Size=24,'cannot prime the default length cache'); + CheckPattern(LBuffered,1,0); + LReads := LSource.ReadCalls; + LSeeks := LSource.SeekCalls; + LSizes := LSource.SizeCalls; + LBuffered.EnableStreamLengthCaching := False; + LBuffered.EnableStreamLengthCaching := False; + Check(LBuffered.EnableReadBuffering and not LBuffered.EnableStreamLengthCaching,'length toggle changed read buffering'); + Check((LSource.ReadCalls=LReads) and (LSource.SeekCalls=LSeeks) and (LSource.SizeCalls=LSizes),'length toggle performed I/O'); + LSource.Size := 31; + Check((LBuffered.Size=31) and (LBuffered.Size=31),'disabled length cache did not observe the source'); + Check(LSource.SizeCalls=LSizes+2,'disabled length cache did not forward both queries'); + LBuffered.EnableStreamLengthCaching := True; + Check(LBuffered.Size=31,'reenabled length cache reused its previous value'); + LSizes := LSource.SizeCalls; + LBuffered.EnableStreamLengthCaching := True; + Check((LBuffered.Size=31) and (LSource.SizeCalls=LSizes),'no-op length enable discarded the cached value'); + CheckPattern(LBuffered,7,1); + Check(LSource.ReadCalls=LReads,'length toggles discarded unread buffered bytes'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckConvenienceModesAreAtomic; +var LSource: TSizeObservedStream; LBuffered: TBufferedStream; LRaised: Boolean; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + LBuffered.EnableBuffering; + Check(LBuffered.EnableReadBuffering and LBuffered.EnableStreamLengthCaching,'EnableBuffering did not enable both flags'); + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'EnableBuffering probed input'); + Check(LBuffered.Size=24,'cannot prime convenience length cache'); + CheckPattern(LBuffered,1,0); + LSource.ResetCounts; + LRaised := False; + try + LBuffered.DisableBuffering; + except + on E: EStreamError do LRaised := True; + end; + Check(LRaised and LBuffered.EnableReadBuffering and LBuffered.EnableStreamLengthCaching,'rejected convenience disable changed a flag'); + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'rejected convenience disable performed I/O'); + Check((LBuffered.Size=24) and (LSource.SizeCalls=0),'rejected convenience disable invalidated the length cache'); + LBuffered.FlushUnreadData; + LBuffered.DisableBuffering; + Check(not LBuffered.EnableReadBuffering and not LBuffered.EnableStreamLengthCaching,'DisableBuffering did not disable both flags'); + Check(LBuffered.BufferSize=8,'convenience disable lost configured buffer capacity'); + LSource.ResetCounts; + Check((LBuffered.Size=24) and (LBuffered.Size=24) and (LSource.SizeCalls=2),'disabled convenience mode cached length'); + LSource.ResetCounts; + LBuffered.EnableBuffering; + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'convenience reenable probed input'); + Check((LBuffered.Size=24) and (LBuffered.Size=24) and (LSource.SizeCalls=1),'convenience reenable did not cache new length'); + CheckPattern(LBuffered,1,1); + Check(LSource.LargestRead=8,'convenience reenable did not restore configured read-ahead'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckIndependentFlagCombination(AReadBuffering, ALengthCaching: Boolean); +var LSource: TSizeObservedStream; LBuffered: TBufferedStream; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,ALengthCaching,AReadBuffering); + try + Check((LSource.ReadCalls=0) and (LSource.SeekCalls=0) and (LSource.SizeCalls=0),'flag combination probed input during construction'); + Check((LBuffered.EnableReadBuffering=AReadBuffering) and (LBuffered.EnableStreamLengthCaching=ALengthCaching),'constructor coupled independent flags'); + CheckPattern(LBuffered,1,0); + if AReadBuffering then + Check(LSource.LargestRead=8,'read buffering did not read ahead') + else Check(LSource.LargestRead=1,'length caching enabled read-ahead'); + Check((LBuffered.Size=24) and (LBuffered.Size=24),'flag combination changed Size'); + if ALengthCaching then + Check(LSource.SizeCalls=1,'enabled length caching queried input twice') + else Check(LSource.SizeCalls=2,'disabled length caching retained a length'); + Check(LBuffered.Position=1,'size queries changed the logical read cursor'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckLengthCachingTogglesWithoutReadBuffer; +var LSource: TSizeObservedStream; LBuffered: TBufferedStream; LCalls: Integer; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,False,False); + try + Check((LBuffered.Size=24) and (LBuffered.Size=24) and (LSource.SizeCalls=2),'uncached direct queries did not forward'); + LBuffered.EnableStreamLengthCaching := True; + Check(not LBuffered.EnableReadBuffering,'enabling length caching allocated read buffering'); + Check((LBuffered.Size=24) and (LBuffered.Size=24) and (LSource.SizeCalls=3),'length-only enable did not cache a fresh query'); + LBuffered.EnableStreamLengthCaching := True; + Check((LBuffered.Size=24) and (LSource.SizeCalls=3),'no-op enable discarded length-only cache'); + LBuffered.EnableStreamLengthCaching := False; + LSource.Size := 31; + Check((LBuffered.Size=31) and (LSource.SizeCalls=4),'length-only disable reused an old value'); + LBuffered.EnableStreamLengthCaching := True; + Check((LBuffered.Size=31) and (LSource.SizeCalls=5),'length-only reenable reused an old value'); + LCalls := LSource.SizeCalls; + LBuffered.EnableReadBuffering := True; + Check(LSource.SizeCalls=LCalls,'read mode transition queried length eagerly'); + Check(LBuffered.EnableStreamLengthCaching and (LBuffered.Size=31),'read mode transition disabled length caching'); + LCalls := LSource.SizeCalls; + Check((LBuffered.Size=31) and (LSource.SizeCalls=LCalls),'read mode transition did not retain the new cached value'); + LBuffered.EnableReadBuffering := False; + Check(LBuffered.EnableStreamLengthCaching,'read disable changed the independent length flag'); + Check((LBuffered.Size=31) and (LSource.SizeCalls=LCalls+1),'read disable did not refresh invalidated metadata lazily'); + LCalls := LSource.SizeCalls; + Check((LBuffered.Size=31) and (LSource.SizeCalls=LCalls),'read disable bypassed the new length cache'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +procedure CheckDirectCachedLengthSurvivesRejectedMutations; +var LSource: TSizeObservedStream; LBuffered: TBufferedStream; B: Byte; LRaised: Boolean; +begin + LSource := TSizeObservedStream.Create; + try + Seed(LSource,Pattern(24)); + LSource.ResetCounts; + LBuffered := TBufferedStream.Create(LSource,8,False,True,False); + try + Check(LBuffered.Size=24,'cannot prime direct-mode length cache'); + B := 99; + Check(LBuffered.Write(B,0)=0,'zero direct write changed its result'); + LRaised := False; + try + LBuffered.Write(B,-1); + except + on E: EArgumentOutOfRangeException do LRaised := True; + end; + Check(LRaised,'negative direct write was accepted'); + LRaised := False; + try + LBuffered.Size := -1; + except + on E: EArgumentOutOfRangeException do LRaised := True; + end; + Check(LRaised,'negative direct resize was accepted'); + Check((LSource.WriteCalls=0) and (LSource.ResizeCalls=0),'rejected direct mutations reached the input'); + Check((LBuffered.Size=24) and (LSource.SizeCalls=1),'rejected direct mutations invalidated known length'); + finally + LBuffered.Free; + end; + finally + LSource.Free; + end; +end; + +function BufferedStreamCheckName(AIndex: Integer): String; +const Names: array[1..BufferedStreamCheckCount] of String = ( + 'NilSourceRejected','NonpositiveBufferRejectedWithoutOwnership','BorrowedSourceRemainsAlive','OwnedSourceDisposedOnce', + 'NonzeroInitialPosition','LogicalAndPhysicalCursors','SingleByteReadsCollapseSourceCalls','AllBufferBoundariesAndEOF', + 'EmptyStreamEOF','ZeroCountsPerformNoIO','NegativeCountsRejectedBeforeIO','ReadBufferCrossesBoundaries', + 'ShortReadsDoNotLoseData','AllSeekOrigins','SeekWithinRetainedWindow','SeekOutsideWindowAndBack', + 'SeekBeyondEOFDoesNotResize','WriteAfterReadUsesLogicalCursor','ReadAfterWriteRejectsStaleBuffer','ShortWriteReturnsActualCount', + 'SourceReadFailurePreserved','SourceWriteFailurePreserved','TruncatePreservesCursorSemantics','ExpandThenWritePreservesData', + 'ResizeFailurePreserved','SizeFailurePreserved','ReadOnlySourceStillReadable','Sparse64BitOffsetsAndBoundedReads', + 'DeterministicDifferentialOperations','OwnedSourceDisposedAfterReadError','WriteThroughDoesNotRead', + 'ForwardOnlyReadsPreserveCapabilities','BorrowedDestroyDoesNotSeek','ExplicitSynchronizationAlignsCursor', + 'UnsupportedSourceCapabilitiesPropagate','ForwardReadThenWriteFailsBeforeMutation','ConsumedForwardBufferCanWrite', + 'ForwardDirectWriteNeedsNoSeek','FailedForwardSynchronizationRetainsBuffer','FailedExplicitSynchronizationRetainsBuffer', + 'ForwardResizeFailsBeforeMutation','PartialReadFailureInvalidatesCursor','PartialWriteFailureInvalidatesCursor', + 'PartialSeekFailureInvalidatesCursor','NegativePositionIsNotAdjustedOrCached','NegativeSeekAndSizeRetainUnreadData', + 'NegativeSynchronizationPreventsMutation','NegativeReadResultInvalidatesCursor','NegativeWriteResultInvalidatesCursor', + 'PositiveShortReadMakesOneSourceAttempt','UncachedSizeQueriesRemainDynamic','CachedSizeQueriesSourceOnce', + 'CachedZeroLengthQueriesSourceOnce','CachedConstructionDoesNotProbe','CachedSizeExceptionIsRetried', + 'CachedNegativeSizeIsRetried','CachedSizeAfterOverwriteAndAppend','CachedSizeAfterShortWrite', + 'CachedSizeAfterNegativePartialWrite','CachedSizeAfterThrowingPartialWrite','CachedSizeAfterTruncateAndExpand', + 'CachedSizeAfterFailedAndIgnoredResize','CachedSizeAfterFailedSynchronization','CachedSparseLengthAndZeroWrite', + 'DefaultBufferingEnabled','DisabledConstructorDirectReads','DisabledShortReadsAndEOF','DisabledForwardOnlyReads', + 'DisableUnreadRejectedWithoutIO','FlushUnreadDataAllowsDisabling','EmptyAndRepeatedTogglesDoNoIO', + 'ReenablePreservesDirectCursor','DisabledSparseSeekOrigins','DisabledWriteAndResize','DisabledReadExceptionIdentity', + 'DisabledNegativePartialRead','FailedFlushPreservesUnread','ForwardOnlyFlushPreservesUnread','NegativeFlushPreservesUnread', + 'DisabledReadBufferingCachesLength','ReenableInvalidatesSizeAndPosition','ToggledOwnership','AlternatingModesMatchMemoryStream', + 'DisabledArgumentValidation','LengthCachingTogglesWithUnreadBytes','ConvenienceModesAreAtomic', + 'DirectReadsUncachedLength','DirectReadsCachedLength','BufferedReadsUncachedLength','BufferedReadsCachedLength', + 'DirectCachedLengthAfterOverwriteAndAppend','DirectCachedLengthAfterShortWrite','DirectCachedLengthAfterNegativePartialWrite', + 'DirectCachedLengthAfterThrowingPartialWrite','DirectCachedLengthAfterTruncateAndExpand','DirectCachedLengthAfterFailedAndIgnoredResize', + 'DirectCachedNegativeSizeIsRetried','DirectCachedSizeExceptionIsRetried','DirectCachedSparseLengthAndZeroWrite', + 'LengthCachingTogglesWithoutReadBuffer','DirectCachedLengthSurvivesRejectedMutations'); +begin + Check((AIndex>=Low(Names)) and (AIndex<=High(Names)),'invalid check index'); + Result := Names[AIndex]; +end; + +procedure RunBufferedStreamCheck(AIndex: Integer); +begin + case AIndex of + 1: CheckArguments(False); + 2: CheckArguments(True); + 3: CheckOwnership(False,False); + 4: CheckOwnership(True,False); + 5: CheckReadScenario(0); + 6: CheckReadScenario(1); + 7: CheckReadScenario(2); + 8: CheckReadScenario(4); + 9: CheckReadScenario(3); + 10: CheckReadScenario(5); + 11: CheckReadScenario(6); + 12: CheckReadScenario(7); + 13: CheckReadScenario(8); + 14: CheckSeekScenario(0); + 15: CheckSeekScenario(1); + 16: CheckSeekScenario(2); + 17: CheckSeekScenario(3); + 18: CheckMutation(0); + 19: CheckMutation(1); + 20: CheckMutation(2); + 21: CheckSourceFailure(0); + 22: CheckSourceFailure(1); + 23: CheckMutation(3); + 24: CheckMutation(4); + 25: CheckSourceFailure(2); + 26: CheckSourceFailure(3); + 27: CheckSourceFailure(4); + 28: CheckSparse; + 29: CheckMixed; + 30: CheckOwnership(True,True); + 31: CheckWriteOnly; + 32: CheckForwardOnly(0); + 33: CheckBorrowedDestruction; + 34: CheckExplicitSynchronization(False); + 35: CheckForwardOnly(1); + 36: CheckForwardOnly(2); + 37: CheckForwardOnly(3); + 38: CheckForwardOnly(4); + 39: CheckForwardOnly(5); + 40: CheckExplicitSynchronization(True); + 41: CheckForwardOnly(6); + 42: CheckPartialFailure(0); + 43: CheckPartialFailure(1); + 44: CheckPartialFailure(2); + 45: CheckNegativeSeekResult(0); + 46: CheckNegativeSeekResult(1); + 47: CheckNegativeSeekResult(2); + 48: CheckNegativeIoResult(False); + 49: CheckNegativeIoResult(True); + 50: CheckSingleSourceRead; + 51: CheckUncachedSizeIsDynamic; + 52: CheckCachedSizeQueriesOnce(False); + 53: CheckCachedSizeQueriesOnce(True); + 54: CheckCachedConstructionDoesNotProbe; + 55: CheckCachedSizeExceptionIsRetried; + 56: CheckCachedNegativeSizeIsRetried; + 57: CheckCachedSizeAfterOverwriteAndAppend; + 58: CheckCachedSizeAfterWriteFailure(0); + 59: CheckCachedSizeAfterWriteFailure(1); + 60: CheckCachedSizeAfterWriteFailure(2); + 61: CheckCachedSizeAfterResize; + 62: CheckCachedSizeAfterFailedAndIgnoredResize; + 63: CheckCachedSizeAfterFailedSynchronization; + 64: CheckCachedSparseLengthAndZeroWrite; + 65: CheckDefaultBufferingEnabled; + 66: CheckDisabledConstructorDirectReads; + 67: CheckDisabledShortReadsAndEOF; + 68: CheckDisabledForwardOnlyReads; + 69: CheckDisableUnreadRejectedWithoutIO; + 70: CheckFlushUnreadDataAllowsDisabling; + 71: CheckEmptyAndRepeatedTogglesDoNoIO; + 72: CheckReenablePreservesDirectCursor; + 73: CheckDisabledSparseSeekOrigins; + 74: CheckDisabledWriteAndResize; + 75: CheckDisabledReadExceptionIdentity; + 76: CheckDisabledNegativePartialRead; + 77: CheckFailedFlushPreservesUnread; + 78: CheckForwardOnlyFlushPreservesUnread(False); + 79: CheckForwardOnlyFlushPreservesUnread(True); + 80: CheckDisabledReadBufferingCachesLength; + 81: CheckReenableInvalidatesSizeAndPosition; + 82: CheckToggledOwnership; + 83: CheckAlternatingModesMatchMemoryStream; + 84: CheckDisabledArgumentValidation; + 85: CheckLengthCachingTogglesWithUnreadBytes; + 86: CheckConvenienceModesAreAtomic; + 87: CheckIndependentFlagCombination(False,False); + 88: CheckIndependentFlagCombination(False,True); + 89: CheckIndependentFlagCombination(True,False); + 90: CheckIndependentFlagCombination(True,True); + 91: CheckCachedSizeAfterOverwriteAndAppend(False); + 92: CheckCachedSizeAfterWriteFailure(0,False); + 93: CheckCachedSizeAfterWriteFailure(1,False); + 94: CheckCachedSizeAfterWriteFailure(2,False); + 95: CheckCachedSizeAfterResize(False); + 96: CheckCachedSizeAfterFailedAndIgnoredResize(False); + 97: CheckCachedNegativeSizeIsRetried(False); + 98: CheckCachedSizeExceptionIsRetried(False); + 99: CheckCachedSparseLengthAndZeroWrite(False); + 100: CheckLengthCachingTogglesWithoutReadBuffer; + 101: CheckDirectCachedLengthSurvivesRejectedMutations; + else + raise EArgumentOutOfRangeException.Create('Invalid buffered stream check index'); + end; +end; + +end. diff --git a/src/tests/experimental/framework/UStreams.Tests.pas b/src/tests/experimental/framework/UStreams.Tests.pas new file mode 100644 index 000000000..456b0f96a --- /dev/null +++ b/src/tests/experimental/framework/UStreams.Tests.pas @@ -0,0 +1,625 @@ +unit UStreams.Tests; + +{ Copyright © 2026 Herman Schoenfeld <https://www.sphere10.com> + Distributed under the MIT software license; see LICENSE. } + +{$MODE Delphi}{$H+} +interface +uses fpcunit, testregistry; +type + TBufferedStreamTests = class(TTestCase) + published + procedure NilSourceRejected; + procedure NonpositiveBufferRejectedWithoutOwnership; + procedure BorrowedSourceRemainsAlive; + procedure OwnedSourceDisposedOnce; + procedure NonzeroInitialPosition; + procedure LogicalAndPhysicalCursors; + procedure SingleByteReadsCollapseSourceCalls; + procedure AllBufferBoundariesAndEOF; + procedure EmptyStreamEOF; + procedure ZeroCountsPerformNoIO; + procedure NegativeCountsRejectedBeforeIO; + procedure ReadBufferCrossesBoundaries; + procedure ShortReadsDoNotLoseData; + procedure AllSeekOrigins; + procedure SeekWithinRetainedWindow; + procedure SeekOutsideWindowAndBack; + procedure SeekBeyondEOFDoesNotResize; + procedure WriteAfterReadUsesLogicalCursor; + procedure ReadAfterWriteRejectsStaleBuffer; + procedure ShortWriteReturnsActualCount; + procedure SourceReadFailurePreserved; + procedure SourceWriteFailurePreserved; + procedure TruncatePreservesCursorSemantics; + procedure ExpandThenWritePreservesData; + procedure ResizeFailurePreserved; + procedure SizeFailurePreserved; + procedure ReadOnlySourceStillReadable; + procedure Sparse64BitOffsetsAndBoundedReads; + procedure DeterministicDifferentialOperations; + procedure OwnedSourceDisposedAfterReadError; + procedure WriteThroughDoesNotRead; + procedure ForwardOnlyReadsPreserveCapabilities; + procedure BorrowedDestroyDoesNotSeek; + procedure ExplicitSynchronizationAlignsCursor; + procedure UnsupportedSourceCapabilitiesPropagate; + procedure ForwardReadThenWriteFailsBeforeMutation; + procedure ConsumedForwardBufferCanWrite; + procedure ForwardDirectWriteNeedsNoSeek; + procedure FailedForwardSynchronizationRetainsBuffer; + procedure FailedExplicitSynchronizationRetainsBuffer; + procedure ForwardResizeFailsBeforeMutation; + procedure PartialReadFailureInvalidatesCursor; + procedure PartialWriteFailureInvalidatesCursor; + procedure PartialSeekFailureInvalidatesCursor; + procedure NegativePositionIsNotAdjustedOrCached; + procedure NegativeSeekAndSizeRetainUnreadData; + procedure NegativeSynchronizationPreventsMutation; + procedure NegativeReadResultInvalidatesCursor; + procedure NegativeWriteResultInvalidatesCursor; + procedure PositiveShortReadMakesOneSourceAttempt; + procedure UncachedSizeQueriesRemainDynamic; + procedure CachedSizeQueriesSourceOnce; + procedure CachedZeroLengthQueriesSourceOnce; + procedure CachedConstructionDoesNotProbe; + procedure CachedSizeExceptionIsRetried; + procedure CachedNegativeSizeIsRetried; + procedure CachedSizeAfterOverwriteAndAppend; + procedure CachedSizeAfterShortWrite; + procedure CachedSizeAfterNegativePartialWrite; + procedure CachedSizeAfterThrowingPartialWrite; + procedure CachedSizeAfterTruncateAndExpand; + procedure CachedSizeAfterFailedAndIgnoredResize; + procedure CachedSizeAfterFailedSynchronization; + procedure CachedSparseLengthAndZeroWrite; + procedure DefaultBufferingEnabled; + procedure DisabledConstructorDirectReads; + procedure DisabledShortReadsAndEOF; + procedure DisabledForwardOnlyReads; + procedure DisableUnreadRejectedWithoutIO; + procedure FlushUnreadDataAllowsDisabling; + procedure EmptyAndRepeatedTogglesDoNoIO; + procedure ReenablePreservesDirectCursor; + procedure DisabledSparseSeekOrigins; + procedure DisabledWriteAndResize; + procedure DisabledReadExceptionIdentity; + procedure DisabledNegativePartialRead; + procedure FailedFlushPreservesUnread; + procedure ForwardOnlyFlushPreservesUnread; + procedure NegativeFlushPreservesUnread; + procedure DisabledReadBufferingCachesLength; + procedure ReenableInvalidatesSizeAndPosition; + procedure ToggledOwnership; + procedure AlternatingModesMatchMemoryStream; + procedure DisabledArgumentValidation; + procedure LengthCachingTogglesWithUnreadBytes; + procedure ConvenienceModesAreAtomic; + procedure DirectReadsUncachedLength; + procedure DirectReadsCachedLength; + procedure BufferedReadsUncachedLength; + procedure BufferedReadsCachedLength; + procedure DirectCachedLengthAfterOverwriteAndAppend; + procedure DirectCachedLengthAfterShortWrite; + procedure DirectCachedLengthAfterNegativePartialWrite; + procedure DirectCachedLengthAfterThrowingPartialWrite; + procedure DirectCachedLengthAfterTruncateAndExpand; + procedure DirectCachedLengthAfterFailedAndIgnoredResize; + procedure DirectCachedNegativeSizeIsRetried; + procedure DirectCachedSizeExceptionIsRetried; + procedure DirectCachedSparseLengthAndZeroWrite; + procedure LengthCachingTogglesWithoutReadBuffer; + procedure DirectCachedLengthSurvivesRejectedMutations; + end; + +implementation +uses UStreams.Checks; + +procedure TBufferedStreamTests.NilSourceRejected; +begin + RunBufferedStreamCheck(1); +end; + +procedure TBufferedStreamTests.NonpositiveBufferRejectedWithoutOwnership; +begin + RunBufferedStreamCheck(2); +end; + +procedure TBufferedStreamTests.BorrowedSourceRemainsAlive; +begin + RunBufferedStreamCheck(3); +end; + +procedure TBufferedStreamTests.OwnedSourceDisposedOnce; +begin + RunBufferedStreamCheck(4); +end; + +procedure TBufferedStreamTests.NonzeroInitialPosition; +begin + RunBufferedStreamCheck(5); +end; + +procedure TBufferedStreamTests.LogicalAndPhysicalCursors; +begin + RunBufferedStreamCheck(6); +end; + +procedure TBufferedStreamTests.SingleByteReadsCollapseSourceCalls; +begin + RunBufferedStreamCheck(7); +end; + +procedure TBufferedStreamTests.AllBufferBoundariesAndEOF; +begin + RunBufferedStreamCheck(8); +end; + +procedure TBufferedStreamTests.EmptyStreamEOF; +begin + RunBufferedStreamCheck(9); +end; + +procedure TBufferedStreamTests.ZeroCountsPerformNoIO; +begin + RunBufferedStreamCheck(10); +end; + +procedure TBufferedStreamTests.NegativeCountsRejectedBeforeIO; +begin + RunBufferedStreamCheck(11); +end; + +procedure TBufferedStreamTests.ReadBufferCrossesBoundaries; +begin + RunBufferedStreamCheck(12); +end; + +procedure TBufferedStreamTests.ShortReadsDoNotLoseData; +begin + RunBufferedStreamCheck(13); +end; + +procedure TBufferedStreamTests.AllSeekOrigins; +begin + RunBufferedStreamCheck(14); +end; + +procedure TBufferedStreamTests.SeekWithinRetainedWindow; +begin + RunBufferedStreamCheck(15); +end; + +procedure TBufferedStreamTests.SeekOutsideWindowAndBack; +begin + RunBufferedStreamCheck(16); +end; + +procedure TBufferedStreamTests.SeekBeyondEOFDoesNotResize; +begin + RunBufferedStreamCheck(17); +end; + +procedure TBufferedStreamTests.WriteAfterReadUsesLogicalCursor; +begin + RunBufferedStreamCheck(18); +end; + +procedure TBufferedStreamTests.ReadAfterWriteRejectsStaleBuffer; +begin + RunBufferedStreamCheck(19); +end; + +procedure TBufferedStreamTests.ShortWriteReturnsActualCount; +begin + RunBufferedStreamCheck(20); +end; + +procedure TBufferedStreamTests.SourceReadFailurePreserved; +begin + RunBufferedStreamCheck(21); +end; + +procedure TBufferedStreamTests.SourceWriteFailurePreserved; +begin + RunBufferedStreamCheck(22); +end; + +procedure TBufferedStreamTests.TruncatePreservesCursorSemantics; +begin + RunBufferedStreamCheck(23); +end; + +procedure TBufferedStreamTests.ExpandThenWritePreservesData; +begin + RunBufferedStreamCheck(24); +end; + +procedure TBufferedStreamTests.ResizeFailurePreserved; +begin + RunBufferedStreamCheck(25); +end; + +procedure TBufferedStreamTests.SizeFailurePreserved; +begin + RunBufferedStreamCheck(26); +end; + +procedure TBufferedStreamTests.ReadOnlySourceStillReadable; +begin + RunBufferedStreamCheck(27); +end; + +procedure TBufferedStreamTests.Sparse64BitOffsetsAndBoundedReads; +begin + RunBufferedStreamCheck(28); +end; + +procedure TBufferedStreamTests.DeterministicDifferentialOperations; +begin + RunBufferedStreamCheck(29); +end; + +procedure TBufferedStreamTests.OwnedSourceDisposedAfterReadError; +begin + RunBufferedStreamCheck(30); +end; + +procedure TBufferedStreamTests.WriteThroughDoesNotRead; +begin + RunBufferedStreamCheck(31); +end; + +procedure TBufferedStreamTests.ForwardOnlyReadsPreserveCapabilities; +begin + RunBufferedStreamCheck(32); +end; + +procedure TBufferedStreamTests.BorrowedDestroyDoesNotSeek; +begin + RunBufferedStreamCheck(33); +end; + +procedure TBufferedStreamTests.ExplicitSynchronizationAlignsCursor; +begin + RunBufferedStreamCheck(34); +end; + +procedure TBufferedStreamTests.UnsupportedSourceCapabilitiesPropagate; +begin + RunBufferedStreamCheck(35); +end; + +procedure TBufferedStreamTests.ForwardReadThenWriteFailsBeforeMutation; +begin + RunBufferedStreamCheck(36); +end; + +procedure TBufferedStreamTests.ConsumedForwardBufferCanWrite; +begin + RunBufferedStreamCheck(37); +end; + +procedure TBufferedStreamTests.ForwardDirectWriteNeedsNoSeek; +begin + RunBufferedStreamCheck(38); +end; + +procedure TBufferedStreamTests.FailedForwardSynchronizationRetainsBuffer; +begin + RunBufferedStreamCheck(39); +end; + +procedure TBufferedStreamTests.FailedExplicitSynchronizationRetainsBuffer; +begin + RunBufferedStreamCheck(40); +end; + +procedure TBufferedStreamTests.ForwardResizeFailsBeforeMutation; +begin + RunBufferedStreamCheck(41); +end; + +procedure TBufferedStreamTests.PartialReadFailureInvalidatesCursor; +begin + RunBufferedStreamCheck(42); +end; + +procedure TBufferedStreamTests.PartialWriteFailureInvalidatesCursor; +begin + RunBufferedStreamCheck(43); +end; + +procedure TBufferedStreamTests.PartialSeekFailureInvalidatesCursor; +begin + RunBufferedStreamCheck(44); +end; + +procedure TBufferedStreamTests.NegativePositionIsNotAdjustedOrCached; +begin + RunBufferedStreamCheck(45); +end; + +procedure TBufferedStreamTests.NegativeSeekAndSizeRetainUnreadData; +begin + RunBufferedStreamCheck(46); +end; + +procedure TBufferedStreamTests.NegativeSynchronizationPreventsMutation; +begin + RunBufferedStreamCheck(47); +end; + +procedure TBufferedStreamTests.NegativeReadResultInvalidatesCursor; +begin + RunBufferedStreamCheck(48); +end; + +procedure TBufferedStreamTests.NegativeWriteResultInvalidatesCursor; +begin + RunBufferedStreamCheck(49); +end; + +procedure TBufferedStreamTests.PositiveShortReadMakesOneSourceAttempt; +begin + RunBufferedStreamCheck(50); +end; + +procedure TBufferedStreamTests.UncachedSizeQueriesRemainDynamic; +begin + RunBufferedStreamCheck(51); +end; + +procedure TBufferedStreamTests.CachedSizeQueriesSourceOnce; +begin + RunBufferedStreamCheck(52); +end; + +procedure TBufferedStreamTests.CachedZeroLengthQueriesSourceOnce; +begin + RunBufferedStreamCheck(53); +end; + +procedure TBufferedStreamTests.CachedConstructionDoesNotProbe; +begin + RunBufferedStreamCheck(54); +end; + +procedure TBufferedStreamTests.CachedSizeExceptionIsRetried; +begin + RunBufferedStreamCheck(55); +end; + +procedure TBufferedStreamTests.CachedNegativeSizeIsRetried; +begin + RunBufferedStreamCheck(56); +end; + +procedure TBufferedStreamTests.CachedSizeAfterOverwriteAndAppend; +begin + RunBufferedStreamCheck(57); +end; + +procedure TBufferedStreamTests.CachedSizeAfterShortWrite; +begin + RunBufferedStreamCheck(58); +end; + +procedure TBufferedStreamTests.CachedSizeAfterNegativePartialWrite; +begin + RunBufferedStreamCheck(59); +end; + +procedure TBufferedStreamTests.CachedSizeAfterThrowingPartialWrite; +begin + RunBufferedStreamCheck(60); +end; + +procedure TBufferedStreamTests.CachedSizeAfterTruncateAndExpand; +begin + RunBufferedStreamCheck(61); +end; + +procedure TBufferedStreamTests.CachedSizeAfterFailedAndIgnoredResize; +begin + RunBufferedStreamCheck(62); +end; + +procedure TBufferedStreamTests.CachedSizeAfterFailedSynchronization; +begin + RunBufferedStreamCheck(63); +end; + +procedure TBufferedStreamTests.CachedSparseLengthAndZeroWrite; +begin + RunBufferedStreamCheck(64); +end; + +procedure TBufferedStreamTests.DefaultBufferingEnabled; +begin + RunBufferedStreamCheck(65); +end; + +procedure TBufferedStreamTests.DisabledConstructorDirectReads; +begin + RunBufferedStreamCheck(66); +end; + +procedure TBufferedStreamTests.DisabledShortReadsAndEOF; +begin + RunBufferedStreamCheck(67); +end; + +procedure TBufferedStreamTests.DisabledForwardOnlyReads; +begin + RunBufferedStreamCheck(68); +end; + +procedure TBufferedStreamTests.DisableUnreadRejectedWithoutIO; +begin + RunBufferedStreamCheck(69); +end; + +procedure TBufferedStreamTests.FlushUnreadDataAllowsDisabling; +begin + RunBufferedStreamCheck(70); +end; + +procedure TBufferedStreamTests.EmptyAndRepeatedTogglesDoNoIO; +begin + RunBufferedStreamCheck(71); +end; + +procedure TBufferedStreamTests.ReenablePreservesDirectCursor; +begin + RunBufferedStreamCheck(72); +end; + +procedure TBufferedStreamTests.DisabledSparseSeekOrigins; +begin + RunBufferedStreamCheck(73); +end; + +procedure TBufferedStreamTests.DisabledWriteAndResize; +begin + RunBufferedStreamCheck(74); +end; + +procedure TBufferedStreamTests.DisabledReadExceptionIdentity; +begin + RunBufferedStreamCheck(75); +end; + +procedure TBufferedStreamTests.DisabledNegativePartialRead; +begin + RunBufferedStreamCheck(76); +end; + +procedure TBufferedStreamTests.FailedFlushPreservesUnread; +begin + RunBufferedStreamCheck(77); +end; + +procedure TBufferedStreamTests.ForwardOnlyFlushPreservesUnread; +begin + RunBufferedStreamCheck(78); +end; + +procedure TBufferedStreamTests.NegativeFlushPreservesUnread; +begin + RunBufferedStreamCheck(79); +end; + +procedure TBufferedStreamTests.DisabledReadBufferingCachesLength; +begin + RunBufferedStreamCheck(80); +end; + +procedure TBufferedStreamTests.ReenableInvalidatesSizeAndPosition; +begin + RunBufferedStreamCheck(81); +end; + +procedure TBufferedStreamTests.ToggledOwnership; +begin + RunBufferedStreamCheck(82); +end; + +procedure TBufferedStreamTests.AlternatingModesMatchMemoryStream; +begin + RunBufferedStreamCheck(83); +end; + +procedure TBufferedStreamTests.DisabledArgumentValidation; +begin + RunBufferedStreamCheck(84); +end; + +procedure TBufferedStreamTests.LengthCachingTogglesWithUnreadBytes; +begin + RunBufferedStreamCheck(85); +end; + +procedure TBufferedStreamTests.ConvenienceModesAreAtomic; +begin + RunBufferedStreamCheck(86); +end; + +procedure TBufferedStreamTests.DirectReadsUncachedLength; +begin + RunBufferedStreamCheck(87); +end; + +procedure TBufferedStreamTests.DirectReadsCachedLength; +begin + RunBufferedStreamCheck(88); +end; + +procedure TBufferedStreamTests.BufferedReadsUncachedLength; +begin + RunBufferedStreamCheck(89); +end; + +procedure TBufferedStreamTests.BufferedReadsCachedLength; +begin + RunBufferedStreamCheck(90); +end; + +procedure TBufferedStreamTests.DirectCachedLengthAfterOverwriteAndAppend; +begin + RunBufferedStreamCheck(91); +end; + +procedure TBufferedStreamTests.DirectCachedLengthAfterShortWrite; +begin + RunBufferedStreamCheck(92); +end; + +procedure TBufferedStreamTests.DirectCachedLengthAfterNegativePartialWrite; +begin + RunBufferedStreamCheck(93); +end; + +procedure TBufferedStreamTests.DirectCachedLengthAfterThrowingPartialWrite; +begin + RunBufferedStreamCheck(94); +end; + +procedure TBufferedStreamTests.DirectCachedLengthAfterTruncateAndExpand; +begin + RunBufferedStreamCheck(95); +end; + +procedure TBufferedStreamTests.DirectCachedLengthAfterFailedAndIgnoredResize; +begin + RunBufferedStreamCheck(96); +end; + +procedure TBufferedStreamTests.DirectCachedNegativeSizeIsRetried; +begin + RunBufferedStreamCheck(97); +end; + +procedure TBufferedStreamTests.DirectCachedSizeExceptionIsRetried; +begin + RunBufferedStreamCheck(98); +end; + +procedure TBufferedStreamTests.DirectCachedSparseLengthAndZeroWrite; +begin + RunBufferedStreamCheck(99); +end; + +procedure TBufferedStreamTests.LengthCachingTogglesWithoutReadBuffer; +begin + RunBufferedStreamCheck(100); +end; + +procedure TBufferedStreamTests.DirectCachedLengthSurvivesRejectedMutations; +begin + RunBufferedStreamCheck(101); +end; + +initialization + RegisterTest(TBufferedStreamTests); +end. diff --git a/src/tests/experimental/framework/UVisualGrid.Loading.Checks.pas b/src/tests/experimental/framework/UVisualGrid.Loading.Checks.pas new file mode 100644 index 000000000..22468721c --- /dev/null +++ b/src/tests/experimental/framework/UVisualGrid.Loading.Checks.pas @@ -0,0 +1,353 @@ +unit UVisualGrid.Loading.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Real native loading checks around asynchronous publication and the grid's + own five-second throttle. Only the integer provider and its gate are fixtures. } + +{$IFDEF FPC}{$mode delphi}{$H+}{$ENDIF} + +interface + +uses Classes; + +type + TGridLoadingScenario = (lsInternalLayoutFinishesLoading, lsNewClientKeepsLoading, + lsManualCardSurvivesInternalLayout, lsErrorAndManualLayers, + lsSynchronousErrorAndRecovery); + +procedure CheckVisualGridLoading(AScenario: TGridLoadingScenario; ALog: TStrings = nil); + +implementation + +uses + {$IFNDEF FPC}Winapi.Windows,{$ENDIF} + SysUtils, Types, Variants, Forms, Controls, StdCtrls, ExtCtrls, Graphics, + SyncObjs, Generics.Collections, UCommon, UCommon.UI, UCommon.Data, UVisualGrid; + +type + TLoadingSource = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const ABindingName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; override; + public + Count, Calls: Integer; + UnknownTotal, HoldNext, FailRead, ReplaceBeforeFailure: Boolean; + LastParams: TPageFetchParams; + Entered, ReleaseRead: TEvent; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + end; + + TLoadingObserver = class(TComponent) + public + Grid: TVisualGrid; + Completions: Integer; + FirstWaiting, FirstUpdating, FirstOverlay: Boolean; + FirstRows, FirstPageSize: Integer; + FirstCaption: String; + procedure Completed(Sender: TObject); + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create('Grid loading: ' + AMessage); +end; + +procedure PumpFor(AMilliseconds: Cardinal); +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + CheckSynchronize(0); + Application.ProcessMessages; + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure Settle(AGrid: TVisualGrid; ATimeout: Cardinal = 2000); +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + ATimeout; + repeat + PumpFor(10); + if not AGrid.IsUpdating and not AGrid.RefreshThrottle.Waiting then + begin + PumpFor(100); + if not AGrid.IsUpdating and not AGrid.RefreshThrottle.Waiting then + Exit; + end; + until GetTickCount64 >= LUntil; + raise Exception.Create('Grid loading fixture did not settle'); +end; + +constructor TLoadingSource.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + Count := 240; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, True, ''); +end; + +destructor TLoadingSource.Destroy; +begin + ReleaseRead.Free; + Entered.Free; + inherited Destroy; +end; + +function TLoadingSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id', True)); +end; + +function TLoadingSource.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const ABindingName: String): Variant; +begin + Result := AItem; +end; + +procedure TLoadingSource.FetchAll(const AContainer: TList<Integer>); +var + I: Integer; +begin + if FailRead then + raise Exception.Create('Loading fixture provider error'); + for I := 0 to Count - 1 do + AContainer.Add(I); +end; + +function TLoadingSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; +begin + Inc(Calls); + LastParams := AParams; + if FailRead and ReplaceBeforeFailure then + begin + ADataTable.Columns := GetColumns; + SetLength(ADataTable.Rows, 1); + ADataTable.Rows[0] := TDataRow.New(PtrInt(ClassInfo), ADataTable.Columns); + TDataRowData(ADataTable.Rows[0])['Id'] := -999; + raise Exception.Create('Loading fixture provider error'); + end; + if HoldNext then + begin + HoldNext := False; + Entered.SetEvent; + Require(ReleaseRead.WaitFor(5000) = wrSignaled, 'held provider was not released'); + end; + Result := inherited FetchPage(AParams, ADataTable); + if UnknownTotal then + Result.TotalDataCount := -1; +end; + +procedure TLoadingObserver.Completed(Sender: TObject); +begin + Inc(Completions); + if Completions <> 1 then + Exit; + FirstWaiting := Grid.RefreshThrottle.Waiting; + FirstUpdating := Grid.IsUpdating; + FirstOverlay := TPanel(Grid.FindComponent('LoadingOverlay')).Visible; + FirstCaption := TLabel(Grid.FindComponent('LoadingStatus')).Caption; + FirstRows := Grid.RowCount; + FirstPageSize := Grid.PageSize; +end; + +procedure CheckVisualGridLoading(AScenario: TGridLoadingScenario; ALog: TStrings); +var + LForm: TForm; + LGrid: TVisualGrid; + LSource: TLoadingSource; + LObserver: TLoadingObserver; + LPanel: TPanel; + LImage: Graphics.TBitmap; + LInitialCapacity, LBeforeCalls, LBeforeRows, LPass: Integer; + LUntil: UInt64; + LError: String; + + procedure AwaitFirstCompletion; + begin + LUntil := GetTickCount64 + 2000; + while (LObserver.Completions = 0) and (GetTickCount64 < LUntil) do + PumpFor(10); + Require(LObserver.Completions = 1, 'accepted rows did not publish before the quiet interval'); + end; + + procedure BeginHeldRead(AThrottle: Boolean); + begin + LGrid.FetchDataInThread := True; + LSource.Entered.ResetEvent; + LSource.ReleaseRead.ResetEvent; + LSource.HoldNext := True; + LObserver.Completions := 0; + LBeforeCalls := LSource.Calls; + if AThrottle then + begin + LGrid.RefreshThrottle.Interval := TTimeSpan.FromSeconds(5); + LGrid.RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; + end; + LGrid.RefreshGrid; + Require(LSource.Entered.WaitFor(1000) = wrSignaled, 'actual worker never entered the provider'); + PumpFor(250); + Require(LPanel.Visible, 'held fetch did not show its delayed automatic card'); + end; + +begin + LForm := TForm.CreateNew(nil); + LSource := nil; + LImage := nil; + try + LForm.SetBounds(80, 90, 640, 440); + LSource := TLoadingSource.Create(LForm); + LGrid := TVisualGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.FetchDataInThread := False; + LGrid.AutoPageSize := True; + LGrid.Options := [vgoAutoHidePaging, vgoAutoHideSearchPanel]; + LGrid.AddColumn('Id').Binding := 'Id'; + LObserver := TLoadingObserver.Create(LForm); + LObserver.Grid := LGrid; + LGrid.OnFinishedUpdating := LObserver.Completed; + LPanel := LGrid.FindComponent('LoadingOverlay') as TPanel; + LForm.Show; + PumpFor(50); + LGrid.DataSource := LSource; + Settle(LGrid); + LInitialCapacity := LGrid.PageSize; + Require(LInitialCapacity > 2, 'native fixture needs multiple visible rows'); + + if AScenario = lsSynchronousErrorAndRecovery then + begin + LBeforeRows := LGrid.RowCount; + Require(Integer(LGrid.Rows[0].Id) = 0, 'initial synchronous page has the wrong identity'); + for LPass := 0 to 1 do + begin + LSource.FailRead := True; + LSource.ReplaceBeforeFailure := LPass = 1; + LObserver.Completions := 0; + LError := ''; + try + LGrid.RefreshGrid; + except + on E: Exception do + LError := E.Message; + end; + Require(LError = 'Loading fixture provider error', + 'synchronous failure did not preserve the original exception'); + Require(LObserver.Completions = 0, 'failed synchronous fetch published completion'); + Require(not LGrid.IsUpdating, 'failed synchronous fetch retained worker state'); + Require((LGrid.RowCount = LBeforeRows) and (Integer(LGrid.Rows[0].Id) = 0), + 'failed synchronous output replaced the last accepted rows'); + PumpFor(250); + Require(LPanel.Visible and + (TLabel(LGrid.FindComponent('LoadingStatus')).Caption = LError), + 'failed synchronous fetch retained its loading card instead of its error'); + Require(not TLoadingCircle(LGrid.FindComponent('LoadingCircle')).Active, + 'failed synchronous fetch retained animation'); + LGrid.ShowLoading('Manual owner', 'Manual subtitle', nil, True); + LGrid.HideLoading; + Require(LPanel.Visible and + (TLabel(LGrid.FindComponent('LoadingStatus')).Caption = LError), + 'manual presentation erased the synchronous error'); + LSource.FailRead := False; + LGrid.RefreshGrid; + Settle(LGrid); + Require((LObserver.Completions = 1) and not LPanel.Visible and + (LGrid.RowCount = LBeforeRows) and (Integer(LGrid.Rows[0].Id) = 0), + 'successful synchronous retry did not publish and clear the error'); + end; + end + else if AScenario = lsErrorAndManualLayers then + begin + LSource.FailRead := True; + BeginHeldRead(False); + LSource.ReleaseRead.SetEvent; + LUntil := GetTickCount64 + 2000; + while LGrid.IsUpdating and (GetTickCount64 < LUntil) do + PumpFor(10); + Require(not LGrid.IsUpdating and LPanel.Visible, 'provider error did not finish into its error card'); + Require(Pos('Loading fixture provider error', TLabel(LGrid.FindComponent('LoadingStatus')).Caption) > 0, + 'provider error text was not retained'); + LGrid.ShowLoading('Manual owner', 'Manual subtitle', nil, True); + Require(TLabel(LGrid.FindComponent('LoadingStatus')).Caption = 'Manual owner', + 'manual card did not override an error'); + LGrid.HideLoading; + Require(LPanel.Visible and + (Pos('Loading fixture provider error', TLabel(LGrid.FindComponent('LoadingStatus')).Caption) > 0), + 'hiding manual presentation erased the provider error'); + LSource.FailRead := False; + LGrid.RefreshGrid; + Settle(LGrid); + Require(not LPanel.Visible, 'successful recovery retained an automatic/error card'); + end + else + begin + // Unknown totals are a supported provider contract. A partial final page + // hides search chrome; that native capacity change requires an internal + // refit rather than the known-complete-result no-query optimization. + LSource.Count := LInitialCapacity - 1; + LSource.UnknownTotal := True; + BeginHeldRead(True); + if AScenario = lsNewClientKeepsLoading then + LGrid.PageIndex := 1; + if AScenario = lsManualCardSurvivesInternalLayout then + begin + LImage := Graphics.TBitmap.Create; + LImage.SetSize(8, 8); + LImage.Canvas.Brush.Color := clRed; + LImage.Canvas.FillRect(Types.Rect(0, 0, 8, 8)); + LGrid.ShowLoading('Please Wait', 'Manual synchronization', LImage, True); + end; + LSource.ReleaseRead.SetEvent; + AwaitFirstCompletion; + Require(LObserver.FirstWaiting and not LObserver.FirstUpdating, + 'completion must retain the throttle wait while finishing its worker'); + Require(LObserver.FirstRows = LSource.Count, 'accepted partial rows were not published'); + Require(LObserver.FirstPageSize > LInitialCapacity, + 'the actual result did not expose extra native rows for an internal refit'); + Require(LSource.Calls = LBeforeCalls + 1, 'internal refit queried before quiet finish'); + case AScenario of + lsInternalLayoutFinishesLoading: + begin + Require(not LObserver.FirstOverlay and not LPanel.Visible, + 'completed data retained Updating solely for an internal layout request'); + Settle(LGrid, 6500); + Require(LSource.Calls = LBeforeCalls + 2, 'quiet finish did not admit exactly one internal refit'); + Require(not LPanel.Visible and (LGrid.RowCount = LSource.Count), + 'final internal refit changed accepted rows or left its card visible'); + end; + lsNewClientKeepsLoading: + begin + Require(LObserver.FirstOverlay and LPanel.Visible, + 'a genuinely newer client request lost its waiting feedback'); + LGrid.RefreshThrottle.Cancel; + Require(not LPanel.Visible, 'cancelling pending client work left its card visible'); + end; + lsManualCardSurvivesInternalLayout: + begin + Require(LObserver.FirstOverlay and LPanel.Visible and + (LObserver.FirstCaption = 'Please Wait'), 'internal completion dismissed caller-owned loading'); + Require(TImage(LGrid.FindComponent('LoadingImage')).Visible, + 'caller-owned custom loading image was lost'); + LGrid.HideLoading; + Require(LGrid.RefreshThrottle.Waiting and not LPanel.Visible, + 'hiding manual loading revealed an obsolete automatic card'); + LGrid.RefreshThrottle.Cancel; + end; + end; + end; + if ALog <> nil then + ALog.Add('PASS native loading scenario=' + IntToStr(Ord(AScenario))); + finally + if Assigned(LSource) then + LSource.ReleaseRead.SetEvent; + LForm.Free; + LImage.Free; + end; +end; + +end. diff --git a/src/tests/experimental/framework/UVisualGrid.Paging.Checks.pas b/src/tests/experimental/framework/UVisualGrid.Paging.Checks.pas new file mode 100644 index 000000000..ed7f43c92 --- /dev/null +++ b/src/tests/experimental/framework/UVisualGrid.Paging.Checks.pas @@ -0,0 +1,1203 @@ +unit UVisualGrid.Paging.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. + Shared native paging checks for Delphi/VCL and Lazarus/LCL. The source and + controls are real; only the in-memory records and a worker gate are supplied. } + +{$IFDEF FPC}{$mode delphi}{$H+}{$ENDIF} + +interface + +uses Classes, UVisualGrid; + +type + TGridPagingScenario = (psSingleFromUnknownTotal, psModeTransitions, + psCompleteSearchAndSort, psEmptyAndOnePage, psSuspendedLatestMode, + psDiscardOldWorker, psDisabledPaging, psModeChangeScrollOrigin, + psSuspendedModeScrollOrigin, psHeldModeScrollOrigin); + +procedure CheckVisualGridPaging(AScenario: TGridPagingScenario; ALog: TStrings = nil); +procedure CheckVisualGridFooter(AGrid: TVisualGrid; APpi: Integer; ALog: TStrings = nil); +procedure CheckVisualGridFooterCompaction(ALog: TStrings = nil); +procedure ChooseVisualGridPageMode(AGrid: TVisualGrid; AIndex: Integer; AKeyboard: Boolean = False); + +implementation + +uses SysUtils, Types, Math, Variants, Forms, Controls, StdCtrls, Grids, Graphics, SyncObjs, + Buttons, {$IFDEF FPC}Menus{$ELSE}Vcl.Menus{$ENDIF}, ExtCtrls, + Generics.Collections, UCommon, UCommon.UI, UCommon.Data + {$IFDEF MSWINDOWS}, Windows, Messages{$ENDIF}; + +type + TPagingSource = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const ABindingName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; override; + public + Count, Calls: Integer; + LastParams: TPageFetchParams; + HoldNext: Boolean; + Entered, ReleaseRead: TEvent; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + end; + + TPagingGrid = class(TVisualGrid) + public + function ModeControl: TBitBtn; + function ModeMenu: TPopupMenu; + function ManualEditor: TEdit; + function Navigator: TEdit; + function Footer: TWinControl; + function FirstButton: TButton; + function LastButton: TButton; + function NavigationPanel: TWinControl; + function PageCountLabel: TLabel; + function FooterControls: TArray<TControl>; + function Total: Integer; + function TotalLabel: TLabel; + function Pages: Integer; + function Capacity: Integer; + function LayoutPending: Boolean; + procedure SetFooterMetadata(ATotal, APages: Integer); + procedure ChooseMode(AIndex: Integer); + procedure Search(const AText: String); + end; + + TPagingObserver = class(TComponent) + public + Grid: TPagingGrid; + WatchSingle, InvalidSinglePublication: Boolean; + SinglePublications, ExpectedRows: Integer; + procedure Completed(Sender: TObject); + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +{$IFDEF MSWINDOWS} +type + TNativePagingHost = class(TForm) + public + constructor Create(AOwner: TComponent); override; + end; + + TNativePageModePicker = class(TComponent) + private + FButton: TBitBtn; + FMenu: TPopupMenu; + FTimer: TTimer; + FIndex, FCommands: Integer; + FWindow, FCommandOwner: HWND; + FNativeMenu: NativeUInt; + FNativeCommand: Cardinal; + FDispatched, FTimedOut: Boolean; + FCallbackError: String; + FDeadline: UInt64; + FPrevious: TNotifyEvent; + procedure Tick(Sender: TObject); + procedure Selected(Sender: TObject); + public + constructor Create(AButton: TBitBtn; AMenu: TPopupMenu; AIndex: Integer); reintroduce; + destructor Destroy; override; + procedure Run(AKeyboard: Boolean); + end; + +constructor TNativePagingHost.Create(AOwner: TComponent); +begin + inherited CreateNew(AOwner); +end; + +procedure EnsureNativePagingHost; +var LHost: TNativePagingHost; +begin + if Assigned(Application.MainForm) then Exit; + // LCL's native popup callback requires a real MainForm in a console runner. + Application.CreateForm(TNativePagingHost,LHost); +end; + +function FindOwnedModePopup(AWindow: HWND; AData: LPARAM): BOOL; stdcall; +const CT_MN_GETHMENU = $01E1; // winuser.h; used by VCL native menu styles. +var LPicker: TNativePageModePicker; LClass: array[0..31] of Char; +begin + Result := True; + LPicker := TNativePageModePicker(Pointer(AData)); + try + if GetClassName(AWindow,@LClass[0],Length(LClass)) = 0 then Exit; + if StrPas(PChar(@LClass[0])) = '#32768' then begin + LPicker.FNativeMenu := NativeUInt(SendMessage(AWindow,CT_MN_GETHMENU,0,0)); + if LPicker.FNativeMenu = NativeUInt(LPicker.FMenu.Handle) then begin + LPicker.FWindow := AWindow; + Result := False; + end; + end; + except + on E: Exception do begin + LPicker.FCallbackError := E.ClassName+': '+E.Message; + Result := False; + end; + end; +end; + +constructor TNativePageModePicker.Create(AButton: TBitBtn; AMenu: TPopupMenu; AIndex: Integer); +begin + inherited Create(nil); + FButton := AButton; + FMenu := AMenu; + FIndex := AIndex; + FPrevious := FMenu.Items[FIndex].OnClick; + FMenu.Items[FIndex].OnClick := Selected; + FTimer := TTimer.Create(Self); + FTimer.Enabled := False; + FTimer.Interval := 20; + FTimer.OnTimer := Tick; +end; + +destructor TNativePageModePicker.Destroy; +begin + FTimer.Enabled := False; + FMenu.Items[FIndex].OnClick := FPrevious; + inherited Destroy; +end; + +procedure TNativePageModePicker.Selected(Sender: TObject); +begin + Inc(FCommands); + if Assigned(FPrevious) then FPrevious(Sender); +end; + +procedure TNativePageModePicker.Tick(Sender: TObject); +var I: Integer; LState: Cardinal; LProcess: DWORD; LCaption: array[0..127] of Char; +begin + if GetTickCount64 >= FDeadline then begin + FTimedOut := True; + FTimer.Enabled := False; + EndMenu; + Exit; + end; + if FDispatched then Exit; + EnumThreadWindows(GetCurrentThreadId,@FindOwnedModePopup,LPARAM(Self)); + if FCallbackError <> '' then begin + FTimer.Enabled := False; + EndMenu; + Exit; + end; + if FWindow = 0 then Exit; + try + Require(GetMenuItemCount(FMenu.Handle) = 3,'Native popup must contain exactly three modes'); + for I := 0 to FMenu.Items.Count-1 do begin + LState := GetMenuState(FMenu.Handle,I,MF_BYPOSITION); + Require(LState <> High(Cardinal),'Native popup item state is unavailable'); + Require(((LState and MF_CHECKED) <> 0) = FMenu.Items[I].Checked, + 'Native menu check mark does not match its current mode'); + end; + FNativeCommand := GetMenuItemID(FMenu.Handle,FIndex); + Require(FNativeCommand = FMenu.Items[FIndex].Command,'Native mode command ID does not match its item'); + LState := GetMenuState(FMenu.Handle,FIndex,MF_BYPOSITION); + Require((LState and (MF_DISABLED or MF_GRAYED)) = 0,'Requested native mode item is disabled'); + Require(GetMenuString(FMenu.Handle,FIndex,@LCaption[0],Length(LCaption),MF_BYPOSITION) > 0, + 'Native mode item has no caption'); + Require(StrPas(PChar(@LCaption[0])) = FMenu.Items[FIndex].Caption, + 'Native mode item caption does not match its requested item'); + // Use the registered native command owner from each toolkit's popup code. + FCommandOwner := {$IFDEF FPC}Application.Handle{$ELSE}Vcl.Menus.PopupList.Window{$ENDIF}; + Require((FCommandOwner <> 0) and + (GetWindowThreadProcessId(FCommandOwner,@LProcess) = GetCurrentThreadId) and + (LProcess = GetCurrentProcessId),'Native popup command owner is not this test thread/process'); + FDispatched := True; + FTimer.Enabled := False; + // This is native command dispatch, not synthetic hardware selection. It + // exercises the real HWND menu routing without touching the input desktop. + EndMenu; + SendMessage(FCommandOwner,WM_COMMAND,WPARAM(FNativeCommand),0); + except + on E: Exception do begin + FCallbackError := E.ClassName+': '+E.Message; + FTimer.Enabled := False; + EndMenu; + end; + end; +end; + +procedure TNativePageModePicker.Run(AKeyboard: Boolean); +begin + FButton.SetFocus; + FDeadline := GetTickCount64+2000; + FTimer.Enabled := True; + try + if AKeyboard then begin + SendMessage(FButton.Handle,WM_KEYDOWN,VK_SPACE,1 or (MapVirtualKey(VK_SPACE,0) shl 16)); + SendMessage(FButton.Handle,WM_KEYUP,VK_SPACE,1 or (MapVirtualKey(VK_SPACE,0) shl 16) or $C0000000); + end else FButton.Click; + finally + FTimer.Enabled := False; + end; + Require(FCallbackError = '', 'Native mode command failed: '+FCallbackError); + Require(FDispatched and not FTimedOut and (FCommands = 1), + Format('Native mode command: dispatched=%s timeout=%s callbacks=%d wanted=%d popup=%s menu=%s owner=%s command=%d', + [BoolToStr(FDispatched,True),BoolToStr(FTimedOut,True),FCommands,FIndex, + IntToHex(NativeUInt(FWindow),SizeOf(NativeUInt)*2), + IntToHex(FNativeMenu,SizeOf(NativeUInt)*2), + IntToHex(NativeUInt(FCommandOwner),SizeOf(NativeUInt)*2),FNativeCommand])); +end; +{$ENDIF} + +procedure ChooseVisualGridPageMode(AGrid: TVisualGrid; AIndex: Integer; AKeyboard: Boolean); +var LButton: TBitBtn; LMenu: TPopupMenu; + {$IFDEF MSWINDOWS}LPicker: TNativePageModePicker;{$ENDIF} +begin + LButton := AGrid.FindComponent('PageModeButton') as TBitBtn; + LMenu := AGrid.FindComponent('PageModeMenu') as TPopupMenu; + Require(Assigned(LButton) and Assigned(LMenu) and Assigned(LButton.OnClick), + 'Actual paging icon button and popup are not wired'); + Require((AIndex >= 0) and (AIndex < LMenu.Items.Count),'Mode menu index is invalid'); + {$IFDEF MSWINDOWS} + EnsureNativePagingHost; + LPicker := TNativePageModePicker.Create(LButton,LMenu,AIndex); + try + LPicker.Run(AKeyboard); + finally + LPicker.Free; + end; + {$ELSE} + raise ENotSupportedException.Create('Native popup command checks require Windows'); + {$ENDIF} + Require(Ord(AGrid.PageMode) = AIndex,'Native menu command selected the wrong page mode'); +end; + +procedure PumpFor(AMilliseconds: Cardinal); +var + LUntil: UInt64; + {$IF defined(FPC) and defined(MSWINDOWS)} + LMessage: TMsg; + LDrained: Integer; + {$ENDIF} +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + CheckSynchronize(0); + Application.ProcessMessages; + {$IF defined(FPC) and defined(MSWINDOWS)} + // LCL's manual new-input gate can skip queued timers after a native menu. + // Drain this test thread's messages, retaining quit and the existing budget. + LDrained := 0; + while (LDrained < 256) and PeekMessage(LMessage,0,0,0,PM_REMOVE) do begin + if LMessage.message = WM_QUIT then begin + PostQuitMessage(Integer(LMessage.wParam)); + Exit; + end; + TranslateMessage(LMessage); + DispatchMessageW(LMessage); + Inc(LDrained); + if GetTickCount64 >= LUntil then Break; + end; + {$ENDIF} + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure Settle(AGrid: TVisualGrid); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + 5000; + repeat + PumpFor(25); + // Data can finish before the deferred native viewport reflow. Keep the + // existing deadline and judge capacity only after that layout task retires. + if not AGrid.IsUpdating and not AGrid.RefreshThrottle.Waiting and not TPagingGrid(AGrid).LayoutPending then begin + PumpFor(100); + if not AGrid.IsUpdating and not AGrid.RefreshThrottle.Waiting and not TPagingGrid(AGrid).LayoutPending then Exit; + end; + until GetTickCount64 >= LUntil; + raise Exception.Create('Native paging did not settle before its bounded deadline'); +end; + +constructor TPagingSource.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + Count := 240; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, True, ''); +end; + +destructor TPagingSource.Destroy; +begin + ReleaseRead.Free; + Entered.Free; + inherited Destroy; +end; + +function TPagingSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id', True), TDataColumn.From('Name')); +end; + +function TPagingSource.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const ABindingName: String): Variant; +begin + if ABindingName = 'Id' then Result := AItem + else Result := Format('record %.4d', [AItem]); +end; + +procedure TPagingSource.FetchAll(const AContainer: TList<Integer>); +var I: Integer; +begin + for I := 0 to Count - 1 do AContainer.Add(I); +end; + +function TPagingSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; +begin + Inc(Calls); + LastParams := AParams; + if HoldNext then begin + HoldNext := False; + Entered.SetEvent; + Require(ReleaseRead.WaitFor(5000) = wrSignaled, 'Held native paging provider was not released'); + end; + Result := inherited FetchPage(AParams, ADataTable); +end; + +function TPagingGrid.ModeControl: TBitBtn; +begin + Result := FPageModeButton; +end; + +function TPagingGrid.ModeMenu: TPopupMenu; +begin + Result := FPageModeMenu; +end; + +function TPagingGrid.ManualEditor: TEdit; +begin + Result := FPageSizeEdit; +end; + +function TPagingGrid.Navigator: TEdit; +begin + Result := FPageIndexEdit; +end; + +function TPagingGrid.Footer: TWinControl; +begin + Result := FBottomPanel; +end; + +function TPagingGrid.FirstButton: TButton; +begin + Result := FButtonFirst; +end; + +function TPagingGrid.LastButton: TButton; +begin + Result := FButtonLast; +end; + +function TPagingGrid.NavigationPanel: TWinControl; +begin + Result := FBottomRightPanel; +end; + +function TPagingGrid.PageCountLabel: TLabel; +begin + Result := FPageCountLabel; +end; + +function TPagingGrid.FooterControls: TArray<TControl>; +begin + Result := TArray<TControl>.Create(FAllRecordsCountLabel, FPageSizeLabel, + FPageSizeEdit, FPageModeButton, FButtonFirst, FButtonPrevious, + FPageIndexEdit, FPageCountLabel, FButtonNext, FButtonLast); +end; + +function TPagingGrid.Total: Integer; +begin + Result := FTotalDataCount; +end; + +function TPagingGrid.TotalLabel: TLabel; +begin + Result := FAllRecordsCountLabel; +end; + +procedure TPagingGrid.SetFooterMetadata(ATotal, APages: Integer); +begin + // Exercise native footer measurements without allocating a six-digit dataset. + FTotalDataCount := ATotal; + FPageCount := APages; + FPageIndex := 0; + FAllRecordsCountLabel.Caption := Format('Total: %d',[ATotal]); + FPageCountLabel.Caption := Format('/%d',[APages]); + FPageIndexEdit.Text := '1'; + RefreshPageIndexAndGridInterface; +end; + +function TPagingGrid.Pages: Integer; +begin + Result := FPageCount; +end; + +function TPagingGrid.Capacity: Integer; +begin + Result := ClientRowCount; +end; + +function TPagingGrid.LayoutPending: Boolean; +begin + Result := FDelayedBoundsChangeTimer.Enabled; +end; + +procedure TPagingGrid.ChooseMode(AIndex: Integer); +begin + ChooseVisualGridPageMode(Self,AIndex); +end; + +procedure TPagingGrid.Search(const AText: String); +begin + FSearchEdit.Text := AText; + SearchButtonClick(Self); +end; + +procedure TPagingObserver.Completed(Sender: TObject); +begin + if WatchSingle and (Grid.PageMode = vpmSingle) then begin + Inc(SinglePublications); + if Grid.RowCount <> ExpectedRows then InvalidSinglePublication := True; + end; +end; + +function RowId(AGrid: TVisualGrid; AIndex: Integer): Integer; +begin + Result := Integer(TDataRowData(AGrid.Rows[AIndex])['Id']); +end; + +function EffectivelyEnabled(AControl: TControl): Boolean; +begin + Result := True; + while Assigned(AControl) do begin + if not AControl.Enabled then Exit(False); + AControl := AControl.Parent; + end; +end; + +function NativeBounds(AControl: TControl): Types.TRect; +var + LPoint: Types.TPoint; +begin + {$IFDEF MSWINDOWS} + if (AControl is TWinControl) and TWinControl(AControl).HandleAllocated then begin + Require(Windows.GetWindowRect(TWinControl(AControl).Handle, Result), + 'Cannot read an actual footer control window rectangle'); + Exit; + end; + {$ENDIF} + LPoint := AControl.Parent.ClientToScreen(AControl.BoundsRect.TopLeft); + Result := Types.Rect(LPoint.X, LPoint.Y, LPoint.X + AControl.Width, + LPoint.Y + AControl.Height); +end; + +function RectText(const ARect: Types.TRect): String; +begin + Result := Format('%d,%d,%d,%d',[ARect.Left,ARect.Top,ARect.Right,ARect.Bottom]); +end; +function NativeClientBounds(AControl: TWinControl): Types.TRect; +var LOrigin: Types.TPoint; +begin + LOrigin := Types.Point(0,0); + {$IFDEF MSWINDOWS} + if AControl.HandleAllocated then begin + Require(Windows.GetClientRect(AControl.Handle,Result),'Cannot read native footer ancestor client bounds'); + Require(Windows.ClientToScreen(AControl.Handle,LOrigin),'Cannot locate native footer ancestor client bounds'); + Windows.OffsetRect(Result,LOrigin.X,LOrigin.Y); + Exit; + end; + {$ENDIF} + LOrigin := AControl.ClientToScreen(LOrigin); + Result := Types.Rect(LOrigin.X,LOrigin.Y,LOrigin.X+AControl.ClientWidth,LOrigin.Y+AControl.ClientHeight); +end; +procedure CheckFooterGeometry(AGrid: TPagingGrid; APpi, ALogicalWidth: Integer; ALog: TStrings); +var + LMode, LFirst, LRect, LOther, LFooter: Types.TRect; + LOrigin: Types.TPoint; + LControls: TArray<TControl>; + LBitmap: Graphics.TBitmap; + LGap, I, J: Integer; + LAncestor: TWinControl; + LAncestorClient: Types.TRect; + LCheckpoint: String; +begin + LCheckpoint := Format('footer PPI=%d logicalWidth=%d mode=%d', + [APpi, ALogicalWidth, Ord(AGrid.PageMode)]); + Require(AGrid.Footer.Visible and AGrid.ModeControl.Visible, + LCheckpoint + ': selector must remain visible'); + LMode := NativeBounds(AGrid.ModeControl); + Require(AGrid.ModeControl.Width = AGrid.ModeControl.Height, LCheckpoint+': mode button must stay square'); + Require(AGrid.ModeControl.Caption = '', LCheckpoint+': mode button must remain icon-only'); + Require(not AGrid.ModeControl.Glyph.Empty, LCheckpoint+': mode button glyph must be present'); + LOrigin := AGrid.Footer.ClientToScreen(Types.Point(0, 0)); + LFooter := Types.Rect(LOrigin.X, LOrigin.Y, LOrigin.X + AGrid.Footer.ClientWidth, + LOrigin.Y + AGrid.Footer.ClientHeight); + if AGrid.FirstButton.Visible then begin + LFirst := NativeBounds(AGrid.FirstButton); + LGap := LFirst.Left - LMode.Right; + if Assigned(ALog) then + ALog.Add(Format('%s button=%d,%d,%d,%d first=%d,%d,%d,%d gap=%d', + [LCheckpoint, LMode.Left, LMode.Top, LMode.Right, LMode.Bottom, + LFirst.Left, LFirst.Top, LFirst.Right, LFirst.Bottom, LGap])); + Require((LGap > 0) and (LGap <= (8 * APpi + 48) div 96), + LCheckpoint + ': icon button must sit immediately left of the first-page button'); + Require(Abs((LMode.Top + LMode.Bottom) - (LFirst.Top + LFirst.Bottom)) <= 2, + LCheckpoint + ': icon button and first-page button must share a vertical center'); + Require(AGrid.ModeControl.Parent = AGrid.FirstButton.Parent, + LCheckpoint + ': icon button and navigation must move in one native parent'); + Require(AGrid.LastButton.Visible and AGrid.Navigator.Visible, + LCheckpoint + ': compact paging must keep its last-page and page-entry controls'); + end; + Require(AGrid.ModeControl.Parent = AGrid.NavigationPanel, + LCheckpoint + ': selector must retain its navigation parent, including Single'); + if Assigned(ALog) then + ALog.Add(Format('%s footerLogical=%s footerNativeClient=%s navLogical=%s navNativeClient=%s fontHeight=%d fontPPI=%d', + [LCheckpoint,RectText(AGrid.Footer.BoundsRect),RectText(NativeClientBounds(AGrid.Footer)), + RectText(AGrid.NavigationPanel.BoundsRect),RectText(NativeClientBounds(AGrid.NavigationPanel)), + AGrid.Font.Height,AGrid.Font.PixelsPerInch])); + LControls := AGrid.FooterControls; + for I := 0 to High(LControls) do begin + if not LControls[I].Visible then Continue; + LRect := NativeBounds(LControls[I]); + Require((LRect.Right > LRect.Left) and (LRect.Bottom > LRect.Top) and + (LRect.Left >= LFooter.Left) and (LRect.Right <= LFooter.Right) and + (LRect.Top >= LFooter.Top) and (LRect.Bottom <= LFooter.Bottom), + LCheckpoint + ': visible footer control is clipped: ' + LControls[I].ClassName); + LAncestor := LControls[I].Parent; + while Assigned(LAncestor) and (LAncestor <> AGrid.Parent) do begin + LAncestorClient := NativeClientBounds(LAncestor); + Require((LRect.Left >= LAncestorClient.Left) and (LRect.Top >= LAncestorClient.Top) and + (LRect.Right <= LAncestorClient.Right) and (LRect.Bottom <= LAncestorClient.Bottom), + LCheckpoint + Format(': footer control #%d %s [%d,%d,%d,%d] is clipped by native ancestor %s [%d,%d,%d,%d]', + [I,LControls[I].ClassName,LRect.Left,LRect.Top,LRect.Right,LRect.Bottom,LAncestor.ClassName, + LAncestorClient.Left,LAncestorClient.Top,LAncestorClient.Right,LAncestorClient.Bottom])); + LAncestor := LAncestor.Parent; + end; + for J := I + 1 to High(LControls) do begin + if not LControls[J].Visible then Continue; + LOther := NativeBounds(LControls[J]); + Require((LRect.Right <= LOther.Left) or (LOther.Right <= LRect.Left) or + (LRect.Bottom <= LOther.Top) or (LOther.Bottom <= LRect.Top), + LCheckpoint + Format(': footer controls overlap: #%d %s [%d,%d,%d,%d] / #%d %s [%d,%d,%d,%d]', + [I, LControls[I].ClassName, LRect.Left, LRect.Top, LRect.Right, LRect.Bottom, + J, LControls[J].ClassName, LOther.Left, LOther.Top, LOther.Right, LOther.Bottom])); + end; + end; + if AGrid.PageCountLabel.Visible then begin + LBitmap := Graphics.TBitmap.Create; + try + LBitmap.Canvas.Font.Assign(AGrid.PageCountLabel.Font); + Require(AGrid.PageCountLabel.Width >= + LBitmap.Canvas.TextWidth(AGrid.PageCountLabel.Caption), + LCheckpoint + ': native page-count text is truncated'); + LBitmap.Canvas.Font.Assign(AGrid.Navigator.Font); + Require(AGrid.Navigator.ClientWidth >= + LBitmap.Canvas.TextWidth(AGrid.Navigator.Text) + 2, + LCheckpoint + ': native page-entry text is truncated'); + finally + LBitmap.Free; + end; + end; +end; + +procedure CheckVisualGridFooter(AGrid: TVisualGrid; APpi: Integer; ALog: TStrings); +begin + CheckFooterGeometry(TPagingGrid(AGrid),APpi,(AGrid.Width*96+APpi div 2) div APpi,ALog); +end; + +procedure CheckVisualGridFooterCompaction(ALog: TStrings); +const + LPpis: array[0..2] of Integer = (96,120,144); + LWidths: array[0..3] of Integer = (320,343,240,900); + LModes: array[0..2] of TVisualGridPageMode = (vpmAuto,vpmManual,vpmSingle); +var + LForm: TForm; + LGrid: TPagingGrid; + LPpiIndex, LWidthIndex, LModeIndex, LPpi: Integer; + LTotal, LNavigation: TRect; + LCheckpoint: String; +begin + for LPpiIndex := Low(LPpis) to High(LPpis) do begin + LPpi := LPpis[LPpiIndex]; + LForm := TForm.CreateNew(nil); + try + LForm.Font.Name := 'Tahoma'; + LForm.Font.Height := -11; + LForm.ClientWidth := 343; + LForm.ClientHeight := 330; + LGrid := TPagingGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.CanSearch := False; + LGrid.Options := [vgoAutoHidePaging]; + LGrid.RefreshSuspended := True; + if LPpi <> 96 then begin + {$IFDEF FPC}LForm.ScaleBy(LPpi,96);{$ELSE}LForm.ScaleForPPI(LPpi);{$ENDIF} + end; + LForm.Show; + PumpFor(100); + for LWidthIndex := Low(LWidths) to High(LWidths) do begin + LForm.ClientWidth := (LWidths[LWidthIndex]*LPpi+48) div 96; + for LModeIndex := Low(LModes) to High(LModes) do begin + LGrid.PageMode := LModes[LModeIndex]; + LGrid.SetFooterMetadata(186108,26587); + PumpFor(100); + LGrid.SetFooterMetadata(186108,26587); + CheckFooterGeometry(LGrid,LPpi,LWidths[LWidthIndex],ALog); + LCheckpoint := Format('large total PPI=%d width=%d mode=%d', + [LPpi,LWidths[LWidthIndex],Ord(LModes[LModeIndex])]); + LTotal := NativeBounds(LGrid.TotalLabel); + if LModes[LModeIndex] = vpmSingle then + LNavigation := NativeBounds(LGrid.ModeControl) + else + LNavigation := NativeBounds(LGrid.FirstButton); + if (LModes[LModeIndex] = vpmSingle) or + (LWidths[LWidthIndex] = 900) or + ((LModes[LModeIndex] = vpmAuto) and (LWidths[LWidthIndex] >= 320)) then + Require(Abs((LTotal.Top+LTotal.Bottom)-(LNavigation.Top+LNavigation.Bottom)) <= 2, + LCheckpoint+': compact footer should keep total and navigation centered on one row'); + if (LModes[LModeIndex] <> vpmSingle) and (LWidths[LWidthIndex] = 240) then + Require(LTotal.Bottom <= LNavigation.Top, + LCheckpoint+': footer must wrap cleanly when compact controls cannot fit'); + if Assigned(ALog) then + ALog.Add('PASS '+LCheckpoint); + end; + end; + finally + LForm.Free; + end; + end; +end; + +procedure CheckFooterHostedFirstShow(ALog: TStrings); +const + InitialPpis: array[0..2] of Integer = (96,120,144); + ChangedPpis: array[0..4] of Integer = (144,120,96,120,144); +var + LForm: TForm; + LGroup: TGroupBox; + LPanel: TPanel; + LGrid: TPagingGrid; + LSource: TPagingSource; + LPpi, LCase, LMode, LScalePass, LNextPpi, LBeforeCalls, LWidthCase: Integer; +begin + for LCase := Low(InitialPpis) to High(InitialPpis) do begin + LPpi := InitialPpis[LCase]; + LForm := TForm.CreateNew(nil); + try + LForm.Font.Name := 'Tahoma'; + LForm.Font.Height := -11; + LForm.ClientWidth := 340; + LForm.ClientHeight := 330; + LGroup := TGroupBox.Create(LForm); + LGroup.Parent := LForm; + LGroup.Align := alClient; + LGroup.Caption := 'My Accounts'; + LPanel := TPanel.Create(LForm); + LPanel.Parent := LGroup; + LPanel.Align := alClient; + LPanel.BevelOuter := bvNone; + LSource := TPagingSource.Create(LForm); + LSource.Count := 277; + LSource.SnapshotCacheEnabled := True; + LGrid := TPagingGrid.Create(LForm); + LGrid.PageMode := vpmAuto; + LGrid.FetchDataInThread := True; + LGrid.RefreshThrottle.Mode := temNone; + LGrid.Options := [vgoAutoHidePaging,vgoAutoHideSearchPanel]; + LGrid.AddColumn('Id').Binding := 'Id'; + LGrid.AddColumn('Name').Binding := 'Name'; + LGrid.Align := alClient; + LSource.HoldNext := True; + LSource.ReleaseRead.ResetEvent; + LGrid.DataSource := LSource; + LPanel.AddControlDockCenter(LGrid); + if LPpi <> 96 then begin + {$IFDEF FPC}LForm.ScaleBy(LPpi,96);{$ELSE}LForm.ScaleForPPI(LPpi);{$ENDIF} + end; + LForm.Show; + try + PumpFor(200); + CheckFooterGeometry(LGrid,LPpi,340,ALog); + Require(LSource.Entered.WaitFor(1000) = wrSignaled,'First-show provider was not actually held'); + LBeforeCalls := LSource.Calls; + for LWidthCase := 0 to 1 do begin + if LWidthCase = 0 then LForm.ClientWidth := (240*LPpi+48) div 96 + else LForm.ClientWidth := (340*LPpi+48) div 96; + PumpFor(150); + Require(LGrid.IsUpdating,'Busy footer fixture released its provider too early'); + Require(LSource.Calls = LBeforeCalls,'Visual footer reflow queried a busy provider'); + CheckFooterGeometry(LGrid,LPpi,LForm.ClientWidth*96 div LPpi,ALog); + if Assigned(ALog) then ALog.Add(Format('PASS held footer resize PPI=%d width=%d sourceCalls=%d', + [LPpi,LForm.ClientWidth,LSource.Calls])); + end; + finally + LSource.ReleaseRead.SetEvent; + end; + Settle(LGrid); + CheckFooterGeometry(LGrid,LPpi,340,ALog); + for LMode := Ord(vpmSingle) to Ord(vpmManual) do begin + LGrid.PageMode := TVisualGridPageMode(LMode); + Settle(LGrid); + CheckFooterGeometry(LGrid,LPpi,340,ALog); + end; + // Grow and shrink the same native hierarchy; a fresh form per DPI alone + // misses stale minimum sizes and first-fetch rounding in hosted grids. + for LScalePass := Low(ChangedPpis) to High(ChangedPpis) do begin + LNextPpi := ChangedPpis[LScalePass]; + if LNextPpi <> LPpi then begin + {$IFDEF FPC}LForm.ScaleBy(LNextPpi,LPpi);{$ELSE}LForm.ScaleForPPI(LNextPpi);{$ENDIF} + LPpi := LNextPpi; + end; + for LMode := Ord(vpmSingle) to Ord(vpmManual) do begin + LGrid.PageMode := TVisualGridPageMode(LMode); + Settle(LGrid); + CheckFooterGeometry(LGrid,LPpi,340,ALog); + end; + end; + finally + LForm.Free; + end; + end; +end; + +procedure CheckFooterAnchoringMatrix(ALog: TStrings); +const + Widths: array[0..6] of Integer = (340, 240, 900, 300, 240, 400, 900); + Modes: array[0..2] of TVisualGridPageMode = (vpmAuto, vpmManual, vpmSingle); +var + LForm: TForm; + LGrid: TPagingGrid; + LSource: TPagingSource; + LPpi, LPass, LWidth, LMode: Integer; +begin + for LPass := 0 to 2 do begin + LPpi := 96 + 48 * LPass; + LForm := TForm.CreateNew(nil); + try + LForm.ClientWidth := 900; + LForm.ClientHeight := 560; + LSource := TPagingSource.Create(LForm); + // A real five-digit page count, without repeatedly rebuilding its raw rows. + LSource.Count := 10001; + LSource.SnapshotCacheEnabled := True; + LGrid := TPagingGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.FetchDataInThread := False; + LGrid.CanSearch := False; + LGrid.ManualPageSize := 1; + LGrid.Options := [vgoAutoHidePaging]; + LGrid.AddColumn('Id').Binding := 'Id'; + LGrid.AddColumn('Name').Binding := 'Name'; + LForm.Show; + if LPpi <> 96 then begin + {$IFDEF FPC}LForm.ScaleBy(LPpi, 96);{$ELSE}LForm.ScaleForPPI(LPpi);{$ENDIF} + end; + PumpFor(50); + LGrid.DataSource := LSource; + Settle(LGrid); + for LWidth := Low(Widths) to High(Widths) do begin + LForm.ClientWidth := (Widths[LWidth] * LPpi + 48) div 96; + for LMode := Low(Modes) to High(Modes) do begin + LGrid.PageMode := Modes[LMode]; + Settle(LGrid); + CheckFooterGeometry(LGrid, LPpi, Widths[LWidth], ALog); + if Modes[LMode] = vpmManual then begin + LGrid.LastButton.Click; + Settle(LGrid); + Require((LGrid.PageIndex = 10000) and (LGrid.Navigator.Text = '10001'), + 'The compact last-page button must retain its real navigation action'); + CheckFooterGeometry(LGrid, LPpi, Widths[LWidth], ALog); + end; + end; + end; + finally + LForm.Free; + end; + end; +end; + +procedure CheckVisualGridPaging(AScenario: TGridPagingScenario; ALog: TStrings); +var LForm: TForm; LGrid: TPagingGrid; LSource: TPagingSource; LObserver: TPagingObserver; + LBefore, LWidth, LPage, LSize, I: Integer; LRect, LClient: TRect; + LPoint: TPoint; + + procedure CheckSelector; + var LModeIndex: Integer; + begin + Require((LGrid.ModeMenu.Items.Count = 3) and + (LGrid.ModeMenu.Items[0].Caption = 'Single') and + (LGrid.ModeMenu.Items[1].Caption = 'Auto') and + (LGrid.ModeMenu.Items[2].Caption = 'Manual'), 'Paging mode labels/order must be Single, Auto, Manual'); + for LModeIndex := 0 to 2 do + Require(LGrid.ModeMenu.Items[LModeIndex].Checked = (LModeIndex = Ord(LGrid.PageMode)), + 'Exactly the current page mode must be checked'); + Require((LGrid.ModeControl.Caption = '') and not LGrid.ModeControl.Glyph.Empty and + (LGrid.ModeControl.Width = LGrid.ModeControl.Height), + 'Paging selector must be a square icon button, not a text dropdown'); + Require(LGrid.ModeControl.ShowHint and + (Pos(LGrid.ModeMenu.Items[Ord(LGrid.PageMode)].Caption,LGrid.ModeControl.Hint) > 0), + 'Paging icon tooltip must explain its current mode'); + Require(LGrid.Footer.Visible and LGrid.ModeControl.Visible, + 'The mode selector must remain reachable, including an empty or single-page result'); + LPoint := LGrid.ModeControl.Parent.ClientToScreen(LGrid.ModeControl.BoundsRect.TopLeft); + LRect := Types.Rect(LPoint.X, LPoint.Y, LPoint.X + LGrid.ModeControl.Width, + LPoint.Y + LGrid.ModeControl.Height); + LPoint := LGrid.Footer.ClientToScreen(Types.Point(0, 0)); + LClient := Types.Rect(LPoint.X, LPoint.Y, LPoint.X + LGrid.Footer.ClientWidth, + LPoint.Y + LGrid.Footer.ClientHeight); + Require((LRect.Left >= LClient.Left) and (LRect.Right <= LClient.Right) and + (LRect.Top >= LClient.Top) and (LRect.Bottom <= LClient.Bottom), + 'Paging selector extends outside the actual footer client rectangle'); + end; + + procedure CheckAll(AExpected: Integer); + begin + Require((LSource.LastParams.PageSize <= 0) and + (LSource.LastParams.PageIndex = 0), 'Single must request all matches independently of a cached total'); + Require((LGrid.RowCount = AExpected) and (LGrid.Total = AExpected), + 'Single did not display the complete source result'); + if AExpected > 0 then + Require((LGrid.PageIndex = 0) and (LGrid.Pages = 1), 'A complete nonempty result must occupy one page'); + Require(not LGrid.ManualEditor.Visible and not LGrid.Navigator.Visible, + 'Single must hide finite page-size and navigation controls'); + CheckSelector; + end; + + procedure CheckModeScrollTransitions(AKind: Integer); + var + LFrom, LTo, LOther: TVisualGridPageMode; + LBeforeCalls, LTopRow, LLeftCol: Integer; + LCell: TRect; + LCheckpoint: String; + + procedure MoveToBottomRight; + begin + LGrid.InternalDrawGrid.Row := LGrid.InternalDrawGrid.RowCount - 1; + LGrid.InternalDrawGrid.TopRow := LGrid.InternalDrawGrid.RowCount - 1; + LGrid.InternalDrawGrid.LeftCol := LGrid.InternalDrawGrid.ColCount - 1; + end; + + begin + LGrid.SelectionType := stRow; + LGrid.ManualPageSize := 80; + LGrid.Columns[1].Width := 1500; + for LFrom := Low(TVisualGridPageMode) to High(TVisualGridPageMode) do + for LTo := Low(TVisualGridPageMode) to High(TVisualGridPageMode) do + begin + if LFrom = LTo then + Continue; + LCheckpoint := Format('mode scroll %d>%d kind=%d', [Ord(LFrom), Ord(LTo), AKind]); + LGrid.FetchDataInThread := False; + LGrid.PageMode := LFrom; + Settle(LGrid); + if LFrom <> vpmSingle then + begin + LGrid.PageIndex := LGrid.Pages - 1; + Settle(LGrid); + end; + MoveToBottomRight; + Settle(LGrid); + Require(LGrid.InternalDrawGrid.LeftCol > LGrid.InternalDrawGrid.FixedCols, + LCheckpoint + ': fixture did not actually scroll horizontally'); + if LFrom <> vpmAuto then + Require(LGrid.InternalDrawGrid.TopRow > LGrid.InternalDrawGrid.FixedRows, + LCheckpoint + ': fixture did not actually scroll to later data rows'); + LTopRow := LGrid.InternalDrawGrid.TopRow; + LLeftCol := LGrid.InternalDrawGrid.LeftCol; + LBeforeCalls := LSource.Calls; + LGrid.PageMode := LFrom; + PumpFor(100); + Require((LSource.Calls = LBeforeCalls) and + (LGrid.InternalDrawGrid.TopRow = LTopRow) and + (LGrid.InternalDrawGrid.LeftCol = LLeftCol), + LCheckpoint + ': assigning the existing mode changed scroll or queried'); + if AKind = 1 then + begin + LGrid.RefreshSuspended := True; + LGrid.PageMode := LTo; + // Movement while an old page remains visible must not erase the + // pending new-mode origin when the retained result is finally loaded. + MoveToBottomRight; + PumpFor(100); + Require(LSource.Calls = LBeforeCalls, LCheckpoint + ': suspended mode queried'); + LGrid.RefreshSuspended := False; + end + else if AKind = 2 then + begin + LGrid.FetchDataInThread := True; + LSource.Entered.ResetEvent; + LSource.ReleaseRead.ResetEvent; + LSource.HoldNext := True; + LGrid.RefreshGrid; + Require(LSource.Entered.WaitFor(1000) = wrSignaled, + LCheckpoint + ': original worker did not enter'); + LOther := TVisualGridPageMode(3 - Ord(LFrom) - Ord(LTo)); + LGrid.PageMode := LOther; + LGrid.PageMode := LTo; + MoveToBottomRight; + LSource.ReleaseRead.SetEvent; + end + else + LGrid.PageMode := LTo; + Settle(LGrid); + Require((LGrid.PageMode = LTo) and (LGrid.PageIndex = 0), + LCheckpoint + ': new mode did not publish page zero'); + Require((LGrid.InternalDrawGrid.TopRow = LGrid.InternalDrawGrid.FixedRows) and + (LGrid.InternalDrawGrid.LeftCol = LGrid.InternalDrawGrid.FixedCols), + Format('%s: native scroll origin is row=%d col=%d, expected row=%d col=%d', + [LCheckpoint, LGrid.InternalDrawGrid.TopRow, LGrid.InternalDrawGrid.LeftCol, + LGrid.InternalDrawGrid.FixedRows, LGrid.InternalDrawGrid.FixedCols])); + Require((LGrid.RowCount > 0) and (RowId(LGrid, 0) = 0), + LCheckpoint + ': new page does not contain the first record'); + LCell := LGrid.InternalDrawGrid.CellRect(LGrid.InternalDrawGrid.FixedCols, + LGrid.InternalDrawGrid.FixedRows); + Require((LCell.Left >= 0) and (LCell.Top >= 0) and + (LCell.Bottom <= LGrid.InternalDrawGrid.ClientHeight) and + (LCell.Right > LCell.Left) and (LCell.Bottom > LCell.Top), + LCheckpoint + ': first native data cell is offscreen or empty'); + if ALog <> nil then + ALog.Add('PASS ' + LCheckpoint); + end; + end; + +begin + LForm := TForm.CreateNew(nil); + LSource := nil; + try + LForm.SetBounds(60, 70, 780, 560); + LSource := TPagingSource.Create(LForm); + LObserver := TPagingObserver.Create(LForm); + LGrid := TPagingGrid.Create(LForm); + LObserver.Grid := LGrid; + LGrid.OnFinishedUpdating := LObserver.Completed; + LGrid.FetchDataInThread := False; + if AScenario = psSingleFromUnknownTotal then LGrid.PageMode := vpmSingle; + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.ManualPageSize := 25; + LGrid.SortMode := smSingleColumn; + LGrid.Options := [vgoColSizing, vgoAutoHidePaging, vgoAutoHideSearchPanel, + vgoSortDirectionAllowNone]; + with LGrid.AddColumn('Id') do begin + Binding := 'Id'; + Width := 127; + Filters := SORTABLE_NUMERIC_FILTER; + end; + with LGrid.AddColumn('Name') do begin + Binding := 'Name'; + Width := 273; + Filters := SORTABLE_TEXT_FILTER; + end; + LForm.Show; + PumpFor(50); + LGrid.DataSource := LSource; + Settle(LGrid); + case AScenario of + psSingleFromUnknownTotal: begin + CheckAll(240); + Require(LGrid.InternalDrawGrid.RowCount = 241, 'Single needs a native header and every matching row'); + {$IFDEF MSWINDOWS} + Require((GetWindowLongPtr(LGrid.InternalDrawGrid.Handle, GWL_STYLE) and WS_VSCROLL) <> 0, + 'All-row mode must expose the actual native vertical scrollbar'); + {$ENDIF} + LSource.Count := 370; + LGrid.RefreshGrid; + Settle(LGrid); + CheckAll(370); + Require(RowId(LGrid, 369) = 369, 'Single truncated a growing result to its previous total'); + end; + psModeTransitions: begin + Require(LGrid.PageMode = vpmManual, 'Existing default must remain Manual'); + LGrid.PageIndex := 3; + Settle(LGrid); + LWidth := LGrid.Columns[1].Width; + LGrid.ChooseMode(0); + Settle(LGrid); + Require(LGrid.PageMode = vpmSingle, 'Mode menu did not select Single'); + CheckAll(240); + LBefore := LSource.Calls; + LGrid.PageMode := vpmSingle; + PumpFor(200); + Require(LSource.Calls = LBefore, 'Assigning the same mode fetched again'); + LGrid.ChooseMode(0); + Settle(LGrid); + Require(LSource.Calls = LBefore, 'Selecting the current native menu item fetched again'); + ChooseVisualGridPageMode(LGrid,1,True); + Settle(LGrid); + Require((LGrid.PageMode = vpmAuto) and (LGrid.PageIndex = 0), 'Auto transition must start at page zero'); + Require((LGrid.PageSize = LGrid.Capacity) and + not LGrid.ManualEditor.Visible and LGrid.Navigator.Visible, + 'Auto must fit complete native rows and show only navigation'); + Require(LGrid.InternalDrawGrid.ScrollBars = {$IFDEF FPC}ssAutoHorizontal{$ELSE}ssHorizontal{$ENDIF}, + 'Auto must allow horizontal scrolling without a vertical partial row'); + LGrid.Columns[1].Width := 1500; + Settle(LGrid); + {$IFDEF MSWINDOWS} + Require((GetWindowLongPtr(LGrid.InternalDrawGrid.Handle, GWL_STYLE) and WS_HSCROLL) <> 0, + 'Wide Auto columns must expose the actual native horizontal scrollbar'); + Require((GetWindowLongPtr(LGrid.InternalDrawGrid.Handle, GWL_STYLE) and WS_VSCROLL) = 0, + 'Automatic paging must not expose a vertical scrollbar'); + {$ENDIF} + Require(LGrid.PageSize = LGrid.Capacity, + 'Auto did not refit complete rows after the horizontal scrollbar appeared'); + LGrid.Columns[1].Width := LWidth; + Settle(LGrid); + {$IFDEF MSWINDOWS} + Require((GetWindowLongPtr(LGrid.InternalDrawGrid.Handle, GWL_STYLE) and + (WS_HSCROLL or WS_VSCROLL)) = 0, + 'Fitting Auto columns must hide the horizontal bar and keep vertical scrolling disabled'); + {$ENDIF} + Require(LGrid.PageSize = LGrid.Capacity, + 'Auto did not refit complete rows after hiding the horizontal scrollbar'); + LGrid.PageIndex := 2; + Settle(LGrid); + LPage := LGrid.PageIndex; + LForm.Height := LForm.Height + 67; + Settle(LGrid); + Require((LGrid.PageIndex = LPage) and (LGrid.PageSize = LGrid.Capacity), + 'Auto resize must retain the numerical page and refit complete rows'); + LGrid.ChooseMode(2); + Settle(LGrid); + Require((LGrid.PageMode = vpmManual) and (LGrid.PageIndex = 0) and + (LGrid.PageSize = 25) and (LGrid.ManualPageSize = 25), + 'Returning to Manual lost its independent remembered size'); + Require(LGrid.ManualEditor.Visible and LGrid.Navigator.Visible, + 'Manual controls were not restored'); + Require(LGrid.Columns[1].Width = LWidth, 'Mode transitions changed an established column width'); + LGrid.AutoPageSize := True; + Settle(LGrid); + Require(LGrid.PageMode = vpmAuto, 'Legacy AutoPageSize did not map to Auto'); + LGrid.AutoPageSize := False; + Settle(LGrid); + Require((LGrid.PageMode = vpmManual) and (LGrid.ManualPageSize = 25), + 'Disabling legacy AutoPageSize did not restore Manual size'); + LGrid.ShowAllData := True; + Settle(LGrid); + CheckAll(240); + LGrid.ShowAllData := False; + Settle(LGrid); + Require((LGrid.PageMode = vpmManual) and (LGrid.PageSize = 25), + 'Disabling legacy ShowAllData did not restore Manual size'); + CheckSelector; + CheckFooterHostedFirstShow(ALog); + CheckFooterAnchoringMatrix(ALog); + end; + psCompleteSearchAndSort: begin + LGrid.PageMode := vpmSingle; + Settle(LGrid); + LGrid.Columns[0].SortDirection := sdDescending; + Settle(LGrid); + Require((LGrid.RowCount = 240) and (RowId(LGrid, 0) = 239) and + (RowId(LGrid, 239) = 0), 'Single sorting did not include the entire source'); + LGrid.Search('235'); + Settle(LGrid); + Require((LGrid.PageIndex = 0) and (LGrid.RowCount = 1) and + (RowId(LGrid, 0) = 235), 'Single search did not filter the complete source'); + CheckSelector; + LGrid.Search(''); + Settle(LGrid); + CheckAll(240); + end; + psEmptyAndOnePage: begin + for I := 0 to 1 do begin + LSource.Count := I; + LGrid.RefreshGrid; + Settle(LGrid); + LGrid.PageMode := vpmSingle; + Settle(LGrid); + CheckAll(I); + for LSize := 0 to 2 do begin + LGrid.ChooseMode(LSize); + LForm.ClientWidth := 290; + Settle(LGrid); + CheckSelector; + CheckFooterGeometry(LGrid, 96, 290, ALog); + end; + LForm.ClientWidth := 780; + end; + end; + psSuspendedLatestMode: begin + LGrid.RefreshSuspended := True; + LBefore := LSource.Calls; + LGrid.ChooseMode(0); + LGrid.ChooseMode(1); + LGrid.ChooseMode(2); + LForm.ClientWidth := 300; + PumpFor(50); + CheckFooterGeometry(LGrid, 96, 300, ALog); + LGrid.ManualPageSize := 17; + LGrid.ChooseMode(0); + LForm.Height := LForm.Height + 80; + PumpFor(300); + Require(LSource.Calls = LBefore, 'Changing mode/size/layout while suspended queried its source'); + LGrid.RefreshThrottle.Cancel; + LGrid.RefreshSuspended := False; + Settle(LGrid); + Require(LSource.Calls = LBefore + 1, 'Resume did not fetch exactly one latest mode'); + CheckAll(240); + LGrid.ShowLoading('Please Wait', 'Test hold', nil, True); + Require(not EffectivelyEnabled(LGrid.ModeControl), 'Disabled loading must gate the mode button'); + LBefore := LSource.Calls; + LGrid.ModeMenu.Items[2].Click; + Require((LGrid.PageMode = vpmSingle) and (LSource.Calls = LBefore), + 'A loading-disabled menu command changed mode or queried data'); + LGrid.HideLoading; + Require(EffectivelyEnabled(LGrid.ModeControl), 'Hiding the manual card did not restore the mode button'); + end; + psDiscardOldWorker: begin + LGrid.FetchDataInThread := True; + LSource.Entered.ResetEvent; + LSource.ReleaseRead.ResetEvent; + LSource.HoldNext := True; + LGrid.RefreshGrid; + Require(LSource.Entered.WaitFor(1000) = wrSignaled, 'Old finite provider never entered its real worker'); + LObserver.ExpectedRows := 240; + LObserver.WatchSingle := True; + // The public API remains available while native fetch input is gated. + LGrid.PageMode := vpmSingle; + LSource.ReleaseRead.SetEvent; + Settle(LGrid); + CheckAll(240); + Require((LObserver.SinglePublications > 0) and not LObserver.InvalidSinglePublication, + 'An obsolete finite page was published after switching to Single'); + Require(LGrid.ManualPageSize = 25, 'Late finite result overwrote manual size memory'); + end; + psModeChangeScrollOrigin: CheckModeScrollTransitions(0); + psSuspendedModeScrollOrigin: CheckModeScrollTransitions(1); + psHeldModeScrollOrigin: CheckModeScrollTransitions(2); + psDisabledPaging: begin + LGrid.CanPage := False; + for I := 0 to 2 do begin + LGrid.PageMode := TVisualGridPageMode(I); + LGrid.RefreshGrid; + Settle(LGrid); + Require((LSource.LastParams.PageSize <= 0) and (LGrid.RowCount = 240), + 'CanPage=False must query all rows independently of the retained mode'); + Require(not LGrid.Navigator.Visible and not LGrid.ManualEditor.Visible and + not LGrid.ModeControl.Visible, + 'Paging-disabled grid must hide finite controls'); + end; + LGrid.CanPage := True; + LGrid.PageMode := vpmManual; + Settle(LGrid); + Require((LGrid.ManualPageSize = 25) and (LGrid.RowCount = 25), + 'Re-enabling paging lost the configured manual size'); + end; + end; + if ALog <> nil then ALog.Add('PASS native paging scenario=' + IntToStr(Ord(AScenario))); + finally + if Assigned(LSource) then LSource.ReleaseRead.SetEvent; + LForm.Free; + end; +end; + +end. diff --git a/src/tests/experimental/framework/UVisualGrid.Refresh.Tests.pas b/src/tests/experimental/framework/UVisualGrid.Refresh.Tests.pas new file mode 100644 index 000000000..0b35683ed --- /dev/null +++ b/src/tests/experimental/framework/UVisualGrid.Refresh.Tests.pas @@ -0,0 +1,1749 @@ +unit UVisualGrid.Refresh.Tests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. + Generated rows exercise native layout and worker callbacks without product dependencies. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, Types, Forms, Controls, StdCtrls, ExtCtrls, Graphics, SyncObjs, Variants, Generics.Collections, + fpcunit, testregistry, UCommon, UCommon.UI, UCommon.Data, UVisualGrid, + UVisualGrid.Viewport.Checks, UVisualGrid.Paging.Checks, UVisualGrid.Worker.Checks, + UVisualGrid.Loading.Checks, UVisualGrid.Selection.Checks; + +type + TVisualGridSelectionEventTests = class(TTestCase) + published + procedure OneNativeClickPublishesOneSelectedEvent; + procedure RepeatedNativeClickPreservesToggleDeselection; + procedure ConsumerClearPublishesOneActualEmptyFollowUp; + procedure ProgrammaticAndNoHandlerSelectionRemainCoherent; + end; + + TVisualGridLoadingLifecycleTests = class(TTestCase) + published + procedure InternalCapacityRefitDoesNotRetainUpdating; + procedure GenuineQueuedClientRetainsItsLoadingCard; + procedure ManualImageSurvivesInternalPublication; + procedure ManualAndErrorLayersRemainIndependent; + procedure SynchronousFailureRetainsRowsAndCanRetry; + end; + + TVisualGridPagingTests = class(TTestCase) + published + procedure LegacyShowAllDoesNotDependOnCachedTotal; + procedure SynchronousSourceCanBeAssignedBeforeParenting; + procedure SingleLoadsTheWholeUnknownAndGrowingResult; + procedure DropdownModesRememberManualSizeAndFitAutomaticRows; + procedure LargeTotalsCompactBeforeWrapping; + procedure SingleSearchAndSortUseTheWholeResult; + procedure SelectorRemainsReachableForEmptyAndOnePageResults; + procedure SuspendedModesResumeOnlyTheLatestIntent; + procedure SwitchingModesRejectsAnOldFiniteWorker; + procedure DisablingPagingQueriesTheWholeResultInEveryMode; + procedure EveryModeChangeRestoresNativeScrollOrigin; + procedure SuspendedModeChangesRestoreScrollAfterPublication; + procedure HeldWorkersCannotRestoreAnOldModeScrollPosition; + end; + + TVisualGridViewportTests = class(TTestCase) + published + procedure UniformRowsFitAtNativePixelBoundaries; + procedure HeaderAndVariableRowsUseTheirNativeHeights; + procedure DpiScaledRowsFitAtNativePixelBoundaries; + procedure HorizontalScrollbarReducesTheNativeViewport; + procedure UnfetchedRowsStillContributePageCapacity; + procedure AutomaticPageSizeTracksNativeResizing; + end; + + { Reused by product tests which add their own notification source. } + TVisualGridRefreshFixture = class(TTestCase) + protected + FForm: TForm; + FGrid: TVisualGrid; + FSource: TComponent; + FCompletions, FSelections: Integer; + procedure SetUp; override; + procedure TearDown; override; + procedure PumpFor(AMilliseconds: Cardinal); + procedure Settle; + procedure Completed(Sender: TObject); + procedure Selected(Sender: TObject; constref ASelection: TVisualGridSelection); + function FetchCount: Integer; + end; + + TVisualGridRefreshTests = class(TVisualGridRefreshFixture) + published + procedure SamePageRepopulationDoesNotRaiseSelectionRefresh; + procedure QueuedRefreshesCoalesceOnceThenStayIdle; + procedure AutoPageLayoutTransitionsSettleWithoutRepeatedFetching; + procedure NarrowAndWideColumnsSettleAfterHeldResize; + procedure CompleteResultsReuseRowsWhenPageCapacityChanges; + procedure SmallResultAutoHideCompletesExactlyOnce; + procedure NormalizedUnchangedPageSizeDoesNotFetch; + procedure TinyClientAreaUsesOneRowWithoutRepeatedFetching; + procedure ManualPageSizeSurvivesHiddenSearchPanel; + procedure ManualLoadingCopiesArtworkAndDoesNotFetch; + procedure ManualLoadingBlocksInputButAllowsProgrammaticRefresh; + procedure ManualLoadingSurvivesFetchCompletionAndError; + procedure HidingManualLoadingRetainsActiveFetchRestrictions; + procedure ManualLoadingPreservesCallerDisabledGrid; + procedure NonblockingManualLoadingRespectsFetchRestrictions; + procedure ManualLoadingCanBeRequestedBeforeParenting; + end; + + TVisualGridQueuedCompletionTests = class(TVisualGridRefreshFixture) + private + FRequestNextPage: Boolean; + procedure DisableTimers(AOwner: TComponent); + procedure PumpCompletionOnly(AMilliseconds: Cardinal); + procedure AwaitCompletionOnly; + procedure CompleteAndRequestNext(Sender: TObject); + published + {$IF defined(MSWINDOWS) and defined(VISUALGRID_TEST_WORKER_START)} + procedure WorkerCancelledBeforeExecutionRetiresAndPreservesOtherReceivers; + procedure PublicCancellationBeforeStartKeepsTheLatestRequest; + {$ENDIF} + procedure CancelActiveFetchNeverJoinsOrPublishesAHeldQuery; + procedure CancelledFetchRetainsTheLatestFiveSecondAdmission; + procedure CancelledProviderExceptionCannotPublishAnErrorCard; + procedure FinishedWorkerPublishesWithAllTimersDisabled; + procedure FailedWorkerPublishesAndRetriesWithoutTimers; + procedure DisposingQueuedReceiverPreservesOtherGridCompletion; + procedure QueuedCompletionAcceptsANewClientRequest; + procedure SourceReplacementDoesNotJoinANewHeldWorkerFromAStaleWake; + end; + + TVisualGridRequestThrottleTests = class(TVisualGridRefreshFixture) + private + FWaitStarts, FWaitEnds: Integer; + FRequestOnCompletion: Boolean; + procedure WaitingChanged(Sender: TObject); + procedure RequestCompleted(Sender: TObject); + procedure EnableThrottle(AInterval: Integer = 200); + procedure AwaitQuiet; + function PageCalls: Integer; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure ContinuousClientRequestsAdmitOnlyLeadingAndFinalPages; + procedure SingleBusyLeadingRequestWaitsForItsOwnQuietFinish; + procedure BusyInputsRetainTheLatestPageParameters; + procedure PageSearchAndSortRequestsShareOneAdmission; + procedure SourceReplacementUsesOnlyTheLatestAdmittedSource; + procedure ResizeDuringWaitingIsVisualAndRetainsLatestCapacity; + procedure CompletedResultLayoutCannotStartAnotherBurst; + procedure FinishedUpdatingClientRequestIsNewInput; + procedure ANewBurstAfterQuietLeadsImmediately; + procedure CancelCannotReleaseABusyWaitingRequest; + procedure ModeChangeCancelsPendingPageWork; + procedure OwnerDestructionCannotReleaseCancelledWork; + procedure PartialResultLayoutRetainsLatestAdmittedCapacity; + procedure ClearingSourcePublishesEmptyRowsWithoutLayoutReplay; + procedure SuspendedInputsRetainLatestParametersUntilImmediateResume; + procedure SuspendedResizeChangesGeometryWithoutFetching; + procedure SuspensionCancelsWorkerWithoutJoiningOrPublishingError; + procedure ResumeBeforeCancelledWorkerRetiresFetchesOnlyLatestPage; + procedure ResuspensionCannotReviveACancelledWorkerResult; + procedure SuspendedQuietCompletionCannotAdmitAQuery; + procedure SuspendedSourceReplacementUsesOnlyLatestSource; + procedure SuspensionCooperativelyReleasesTheProviderBeforeResume; + procedure QueuedRequestShowsLoadingBeforeFetchAdmission; + procedure CancelledQueuedRequestHidesLoading; + procedure ManualLoadingOverridesQueuedRefreshPresentation; + procedure DeferredSingleInputRetainsLoadingThroughQuietAdmission; + end; + +implementation + +procedure TVisualGridSelectionEventTests.OneNativeClickPublishesOneSelectedEvent; +begin + CheckVisualGridSelection(ssNativeClickEvents); +end; + +procedure TVisualGridSelectionEventTests.RepeatedNativeClickPreservesToggleDeselection; +begin + CheckVisualGridSelection(ssRepeatedClickToggle); +end; + +procedure TVisualGridSelectionEventTests.ConsumerClearPublishesOneActualEmptyFollowUp; +begin + CheckVisualGridSelection(ssConsumerClearSelection); +end; + +procedure TVisualGridSelectionEventTests.ProgrammaticAndNoHandlerSelectionRemainCoherent; +begin + CheckVisualGridSelection(ssProgrammaticAndNoHandler); +end; + +procedure TVisualGridLoadingLifecycleTests.InternalCapacityRefitDoesNotRetainUpdating; +begin + CheckVisualGridLoading(lsInternalLayoutFinishesLoading); +end; + +procedure TVisualGridLoadingLifecycleTests.GenuineQueuedClientRetainsItsLoadingCard; +begin + CheckVisualGridLoading(lsNewClientKeepsLoading); +end; + +procedure TVisualGridLoadingLifecycleTests.ManualImageSurvivesInternalPublication; +begin + CheckVisualGridLoading(lsManualCardSurvivesInternalLayout); +end; + +procedure TVisualGridLoadingLifecycleTests.ManualAndErrorLayersRemainIndependent; +begin + CheckVisualGridLoading(lsErrorAndManualLayers); +end; + +procedure TVisualGridLoadingLifecycleTests.SynchronousFailureRetainsRowsAndCanRetry; +begin + CheckVisualGridLoading(lsSynchronousErrorAndRecovery); +end; + +procedure TVisualGridPagingTests.LegacyShowAllDoesNotDependOnCachedTotal; +begin + CheckVisualGridShowAllCompatibility; +end; + +procedure TVisualGridPagingTests.SynchronousSourceCanBeAssignedBeforeParenting; +begin + CheckVisualGridDeferredParenting; +end; + +procedure TVisualGridPagingTests.SingleLoadsTheWholeUnknownAndGrowingResult; +begin + CheckVisualGridPaging(psSingleFromUnknownTotal); +end; + +procedure TVisualGridPagingTests.DropdownModesRememberManualSizeAndFitAutomaticRows; +begin + CheckVisualGridPaging(psModeTransitions); +end; + +procedure TVisualGridPagingTests.LargeTotalsCompactBeforeWrapping; +begin + CheckVisualGridFooterCompaction; +end; + +procedure TVisualGridPagingTests.SingleSearchAndSortUseTheWholeResult; +begin + CheckVisualGridPaging(psCompleteSearchAndSort); +end; + +procedure TVisualGridPagingTests.SelectorRemainsReachableForEmptyAndOnePageResults; +begin + CheckVisualGridPaging(psEmptyAndOnePage); +end; + +procedure TVisualGridPagingTests.SuspendedModesResumeOnlyTheLatestIntent; +begin + CheckVisualGridPaging(psSuspendedLatestMode); +end; + +procedure TVisualGridPagingTests.SwitchingModesRejectsAnOldFiniteWorker; +begin + CheckVisualGridPaging(psDiscardOldWorker); +end; + +procedure TVisualGridPagingTests.EveryModeChangeRestoresNativeScrollOrigin; +begin + CheckVisualGridPaging(psModeChangeScrollOrigin); +end; + +procedure TVisualGridPagingTests.SuspendedModeChangesRestoreScrollAfterPublication; +begin + CheckVisualGridPaging(psSuspendedModeScrollOrigin); +end; + +procedure TVisualGridPagingTests.HeldWorkersCannotRestoreAnOldModeScrollPosition; +begin + CheckVisualGridPaging(psHeldModeScrollOrigin); +end; + +procedure TVisualGridPagingTests.DisablingPagingQueriesTheWholeResultInEveryMode; +begin + CheckVisualGridPaging(psDisabledPaging); +end; + +procedure TVisualGridViewportTests.UniformRowsFitAtNativePixelBoundaries; +begin + CheckVisualGridViewport(vsUniformRows); +end; + +procedure TVisualGridViewportTests.HeaderAndVariableRowsUseTheirNativeHeights; +begin + CheckVisualGridViewport(vsVariableRows); +end; + +procedure TVisualGridViewportTests.DpiScaledRowsFitAtNativePixelBoundaries; +begin + CheckVisualGridViewport(vsDpiScaling); +end; + +procedure TVisualGridViewportTests.HorizontalScrollbarReducesTheNativeViewport; +begin + CheckVisualGridViewport(vsHorizontalScrollbar); +end; + +procedure TVisualGridViewportTests.UnfetchedRowsStillContributePageCapacity; +begin + CheckVisualGridViewport(vsUnfetchedRows); +end; + +procedure TVisualGridViewportTests.AutomaticPageSizeTracksNativeResizing; +begin + CheckVisualGridViewport(vsAutomaticResize); +end; + +type + TGridRefreshSource = class(TCustomDataSource<Integer>) + public + FetchCount, ItemCount, PageCallCount: LongInt; + LastParams: TPageFetchParams; + Entered, ReleaseRead, Returned, CancelObserved: TEvent; + BlockRead, FailRead, CooperativeRead: Boolean; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + function GetColumns: TDataColumns; override; + function GetItemField(constref AItem: Integer; const AName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + function FetchPage(constref AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; override; + end; + + TRequestGridAccess = class(TVisualGrid) + public + procedure Search(const AText: String); + function PublishedTotalCount: Integer; + end; + + TLoadingGridAccess = class(TVisualGrid) + function ManualRegionsEnabled: Boolean; + function FetchFieldsEnabled: Boolean; + end; + +function TRequestGridAccess.PublishedTotalCount: Integer; +begin + Result := FTotalDataCount; +end; + +function TLoadingGridAccess.ManualRegionsEnabled: Boolean; +begin + Result := FTopPanel.Enabled and FTopPanelMultiSearch.Enabled and FBottomPanel.Enabled; +end; + +function TLoadingGridAccess.FetchFieldsEnabled: Boolean; +begin + Result := FDrawGrid.Enabled and FMultiSearchCheckComboBox.Enabled and + FBottomRightPanel.Enabled and not FSearchEdit.ReadOnly and + not FPageIndexEdit.ReadOnly and not FPageSizeEdit.ReadOnly; +end; + +constructor TGridRefreshSource.Create(AOwner: TComponent); +begin + inherited; + ItemCount := 1000; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); + Returned := TEvent.Create(nil, True, False, ''); + CancelObserved := TEvent.Create(nil, True, False, ''); +end; + +destructor TGridRefreshSource.Destroy; +begin + Entered.Free; + ReleaseRead.Free; + Returned.Free; + CancelObserved.Free; + inherited; +end; + +function TGridRefreshSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id', True), + TDataColumn.From('Name'), TDataColumn.From('Balance'), + TDataColumn.From('Block'), TDataColumn.From('Type'), TDataColumn.From('Payload')); +end; + +function TGridRefreshSource.GetItemField(constref AItem: Integer; const AName: String): Variant; +begin + Result := AItem; +end; + +procedure TGridRefreshSource.FetchAll(const AContainer: TList<Integer>); +var I, LCount: Integer; +begin + InterlockedIncrement(FetchCount); + Entered.SetEvent; + if BlockRead then begin + if CooperativeRead then begin + while ReleaseRead.WaitFor(10) <> wrSignaled do + if TThread.CheckTerminated then begin + CancelObserved.SetEvent; + Abort; + end; + end else if ReleaseRead.WaitFor(5000) <> wrSignaled then + raise Exception.Create('Grid refresh fixture release timed out'); + end; + if FailRead then raise Exception.Create('Controlled data-source failure'); + LCount := InterlockedCompareExchange(ItemCount, 0, 0); + for I := 0 to LCount - 1 do AContainer.Add(I); +end; + +function TGridRefreshSource.FetchPage(constref AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; +begin + LastParams := AParams; + InterlockedIncrement(PageCallCount); + try Result := inherited; + finally Returned.SetEvent; end; +end; + +procedure TRequestGridAccess.Search(const AText: String); +begin + FSearchEdit.Text := AText; + SearchButtonClick(FSearchButton); +end; + +procedure TVisualGridRefreshFixture.SetUp; +begin + FForm := TForm.CreateNew(nil); + FForm.SetBounds(80, 90, 640, 440); + FSource := TGridRefreshSource.Create(FForm); + FGrid := TRequestGridAccess.Create(FForm); + FGrid.Parent := FForm; + FGrid.Align := alClient; + FGrid.FetchDataInThread := True; + FGrid.AutoPageSize := True; + FGrid.Options := [vgoColAutoFill, vgoColSizing, vgoSortDirectionAllowNone, + vgoAutoHidePaging, vgoAutoHideSearchPanel]; + FGrid.SelectionType := stMultiRow; + FGrid.DeselectionType := dtDefault; + FGrid.AddColumn('Id').Binding := 'Id'; + FGrid.OnFinishedUpdating := Completed; + FGrid.OnSelection := Selected; + FForm.Show; + FGrid.DataSource := TGridRefreshSource(FSource); + Settle; +end; + +procedure TVisualGridRefreshFixture.TearDown; +begin + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + if Assigned(FGrid) then FGrid.DataSource := nil; + FForm.Free; +end; + +procedure TVisualGridRefreshFixture.PumpFor(AMilliseconds: Cardinal); +var LUntil: QWord; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TVisualGridRefreshFixture.Settle; +var LUntil, LStableSince: QWord; LFetches: Integer; +begin + LUntil := GetTickCount64 + 5000; + LStableSince := GetTickCount64; + LFetches := InterlockedCompareExchange(TGridRefreshSource(FSource).FetchCount, 0, 0); + repeat + PumpFor(10); + if FGrid.IsUpdating or + (InterlockedCompareExchange(TGridRefreshSource(FSource).FetchCount, 0, 0) <> LFetches) then begin + LStableSince := GetTickCount64; + LFetches := InterlockedCompareExchange(TGridRefreshSource(FSource).FetchCount, 0, 0); + end; + if (not FGrid.IsUpdating) and (GetTickCount64 - LStableSince >= 350) then Exit; + until GetTickCount64 >= LUntil; + Fail('Auto-page grid did not settle; fetches=' + IntToStr(TGridRefreshSource(FSource).FetchCount)); +end; + +procedure TVisualGridRefreshFixture.Completed(Sender: TObject); +begin + Inc(FCompletions); +end; + +procedure TVisualGridRefreshFixture.Selected(Sender: TObject; constref ASelection: TVisualGridSelection); +begin + Inc(FSelections); +end; + +function TVisualGridRefreshFixture.FetchCount: Integer; +begin + Result := InterlockedCompareExchange(TGridRefreshSource(FSource).FetchCount, 0, 0); +end; + +procedure TVisualGridRefreshTests.SamePageRepopulationDoesNotRaiseSelectionRefresh; +var LSelections: Integer; +begin + // First population deliberately suppresses selection. Re-enable a real row + // mode after that population, as a user interaction would do. + FGrid.SelectionType := stRow; + FGrid.InternalDrawGrid.Row := 2; + PumpFor(20); + AssertTrue('The real grid must have a selected data row', Length(FGrid.SelectedRows) > 0); + LSelections := FSelections; + FGrid.RefreshGrid; + Settle; + AssertEquals('Repopulating a page must not masquerade as another user selection', + LSelections, FSelections); +end; + +procedure TVisualGridRefreshTests.QueuedRefreshesCoalesceOnceThenStayIdle; +var I, LFetches: Integer; +begin + LFetches := TGridRefreshSource(FSource).FetchCount; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + FGrid.RefreshGrid; + AssertTrue(TGridRefreshSource(FSource).Entered.WaitFor(2000) = wrSignaled); + for I := 1 to 50 do FGrid.RefreshGrid; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + Settle; + AssertEquals('Many explicit requests during one fetch must retain only one follow-up', + LFetches + 2, TGridRefreshSource(FSource).FetchCount); + LFetches := TGridRefreshSource(FSource).FetchCount; + PumpFor(400); + AssertEquals('A drained pending request must not turn into periodic fetching', LFetches, TGridRefreshSource(FSource).FetchCount); +end; + +procedure TVisualGridRefreshTests.AutoPageLayoutTransitionsSettleWithoutRepeatedFetching; +const LCounts: array[0..4] of Integer = (0, 1, 17, 28, 1000); +var I, LFetches: Integer; +begin + for I := Low(LCounts) to High(LCounts) do begin + InterlockedExchange(TGridRefreshSource(FSource).ItemCount, LCounts[I]); + LFetches := TGridRefreshSource(FSource).FetchCount; + FGrid.RefreshGrid; + Settle; + AssertTrue('A layout transition may resize its page but must reach a stable layout', + TGridRefreshSource(FSource).FetchCount - LFetches <= 4); + LFetches := TGridRefreshSource(FSource).FetchCount; + PumpFor(250); + AssertEquals('Auto-hidden paging/search panels must not sustain a fetch loop', + LFetches, TGridRefreshSource(FSource).FetchCount); + end; +end; + +procedure TVisualGridRefreshTests.NarrowAndWideColumnsSettleAfterHeldResize; +var I, LFetches, LPageSize: Integer; +begin + FGrid.Columns[0].Width := 100; + FGrid.AddColumn('Name').Binding := 'Name'; + FGrid.Columns[1].Width := 120; + FGrid.AddColumn('Balance').Binding := 'Balance'; + FGrid.Columns[2].Width := 100; + FForm.SetBounds(80, 90, 340, 480); + Settle; + for I := 0 to 1 do begin + if I = 1 then begin + FGrid.AddColumn('Block').Binding := 'Block'; + FGrid.AddColumn('Type').Binding := 'Type'; + FGrid.AddColumn('Payload').Binding := 'Payload'; + FGrid.Columns[3].Width := 150; + FGrid.Columns[4].Width := 150; + FGrid.Columns[5].Width := 300; + Settle; + end; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + TGridRefreshSource(FSource).Entered.ResetEvent; + FGrid.RefreshGrid; + AssertTrue(TGridRefreshSource(FSource).Entered.WaitFor(2000) = wrSignaled); + FForm.Height := 490; + PumpFor(60); + FForm.Height := 470; + PumpFor(60); + FForm.Height := 480; + PumpFor(60); + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + Settle; + LFetches := TGridRefreshSource(FSource).FetchCount; + LPageSize := FGrid.PageSize; + PumpFor(700); + AssertEquals('Scrollbar changes must not alternate page-size fetches after a held resize', + LFetches, TGridRefreshSource(FSource).FetchCount); + AssertEquals('The final auto page size must remain stable', LPageSize, FGrid.PageSize); + end; +end; + +procedure TVisualGridRefreshTests.CompleteResultsReuseRowsWhenPageCapacityChanges; +var I, LFetches, LCompleted: Integer; +begin + FGrid.AutoPageSize := False; + Settle; + for I := 0 to 1 do begin + InterlockedExchange(TGridRefreshSource(FSource).ItemCount, I); + FGrid.RefreshGrid; + Settle; + AssertEquals('The complete result is already present', I, FGrid.RowCount); + LFetches := FetchCount; + LCompleted := FCompletions; + FGrid.PageSize := FGrid.PageSize + 5; + Settle; + AssertEquals('Capacity growth must reuse a complete empty or small result', + LFetches, FetchCount); + AssertEquals('A layout-only capacity change must not signal fetch completion', + LCompleted, FCompletions); + AssertEquals(I, FGrid.RowCount); + end; + InterlockedExchange(TGridRefreshSource(FSource).ItemCount, 1000); + FGrid.PageSize := 10; + Settle; + FGrid.RefreshGrid; + Settle; + LFetches := FetchCount; + FGrid.PageSize := 11; + Settle; + AssertEquals('A partially populated page still fetches its newly exposed row', + LFetches + 1, FetchCount); + AssertEquals(11, FGrid.RowCount); +end; + +procedure TVisualGridRefreshTests.SmallResultAutoHideCompletesExactlyOnce; +var LFetches, LCompleted: Integer; +begin + LFetches := FetchCount; + LCompleted := FCompletions; + InterlockedExchange(TGridRefreshSource(FSource).ItemCount, 0); + FGrid.RefreshGrid; + Settle; + AssertEquals('Hiding paging/search panels must not refetch an empty completed result', + LFetches + 1, FetchCount); + AssertEquals('One completed fetch signals exactly once after layout settles', + LCompleted + 1, FCompletions); + AssertEquals(0, FGrid.RowCount); +end; + +procedure TVisualGridRefreshTests.NormalizedUnchangedPageSizeDoesNotFetch; +var LFetches, LPageSize, LCompleted: Integer; +begin + LFetches := FetchCount; + LPageSize := FGrid.PageSize; + LCompleted := FCompletions; + FGrid.PageSize := -1; + FGrid.PageSize := 0; + Settle; + AssertEquals('Invalid sizes normalize to the existing page without another query', + LFetches, FetchCount); + AssertEquals(LPageSize, FGrid.PageSize); + AssertEquals(LCompleted, FCompletions); +end; + +procedure TVisualGridRefreshTests.TinyClientAreaUsesOneRowWithoutRepeatedFetching; +var LFetches, LSmallFetches, LSmallPageSize: Integer; +begin + FGrid.CanSearch := False; + Settle; + LFetches := FetchCount; + try + // A hidden/partially laid-out host can temporarily expose no drawable rows. + FGrid.Align := alNone; + FGrid.SetBounds(0, 0, FForm.ClientWidth, 1); + PumpFor(900); + LSmallFetches := FetchCount - LFetches; + LSmallPageSize := FGrid.PageSize; + finally + FGrid.Align := alClient; + FGrid.CanSearch := True; + Settle; + end; + AssertEquals('A tiny viewport retains one valid data-row capacity', 1, LSmallPageSize); + AssertTrue('A tiny viewport must not sustain queries for the same effective size', + LSmallFetches <= 2); +end; + +procedure TVisualGridRefreshTests.ManualPageSizeSurvivesHiddenSearchPanel; +var LFetches: Integer; +begin + FGrid.AutoPageSize := False; + FGrid.PageSize := 7; + Settle; + FGrid.CanSearch := False; + Settle; + LFetches := FetchCount; + FGrid.RefreshGrid; + Settle; + AssertEquals('A hidden search panel must not be mistaken for changed paging visibility', + 7, FGrid.PageSize); + AssertEquals('An unchanged manual page requests only its explicit refresh', + LFetches + 1, FetchCount); +end; + +procedure TVisualGridRefreshTests.ManualLoadingCopiesArtworkAndDoesNotFetch; +var LBitmap: TBitmap; LImage: TImage; LFetches, LCompletions: Integer; +begin + LFetches := FetchCount; + LCompletions := FCompletions; + LBitmap := TBitmap.Create; + try + LBitmap.SetSize(8, 8); + LBitmap.Canvas.Brush.Color := clRed; + LBitmap.Canvas.FillRect(Rect(0, 0, 8, 8)); + FGrid.ShowLoading('Held title', 'A separate task is running', LBitmap); + LBitmap.Canvas.Brush.Color := clBlue; + LBitmap.Canvas.FillRect(Rect(0, 0, 8, 8)); + finally + LBitmap.Free; + end; + PumpFor(300); + AssertTrue(FGrid.LoadingVisible); + AssertFalse('A manual hold is not a fetch worker', FGrid.IsUpdating); + AssertEquals('Held title', TLabel(FGrid.FindComponent('LoadingStatus')).Caption); + AssertEquals('A separate task is running', TLabel(FGrid.FindComponent('LoadingSubtitle')).Caption); + AssertTrue(fsBold in TLabel(FGrid.FindComponent('LoadingStatus')).Font.Style); + AssertFalse(fsBold in TLabel(FGrid.FindComponent('LoadingSubtitle')).Font.Style); + LImage := TImage(FGrid.FindComponent('LoadingImage')); + AssertTrue(LImage.Visible); + AssertEquals(8, LImage.Picture.Width); + AssertEquals('The caller can mutate/free its original graphic', + ColorToRGB(clRed), ColorToRGB(LImage.Picture.Bitmap.Canvas.Pixels[1, 1])); + AssertFalse(TLoadingCircle(FGrid.FindComponent('LoadingCircle')).Visible); + FGrid.ShowLoading('Replacement title'); + AssertEquals('Replacement title', TLabel(FGrid.FindComponent('LoadingStatus')).Caption); + AssertFalse(LImage.Visible); + AssertTrue(TLoadingCircle(FGrid.FindComponent('LoadingCircle')).Active); + AssertFalse(TLabel(FGrid.FindComponent('LoadingSubtitle')).Visible); + FGrid.HideLoading; + FGrid.HideLoading; + AssertFalse(FGrid.LoadingVisible); + AssertFalse(TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); + AssertEquals('Presentation must not query the source', LFetches, FetchCount); + AssertEquals(LCompletions, FCompletions); +end; + +procedure TVisualGridRefreshTests.ManualLoadingBlocksInputButAllowsProgrammaticRefresh; +var LFetches: Integer; +begin + LFetches := FetchCount; + FGrid.ShowLoading('Waiting', 'Inputs are unavailable'); + AssertTrue('The grid root stays enabled', FGrid.Enabled); + AssertFalse(TLoadingGridAccess(FGrid).ManualRegionsEnabled); + AssertFalse(TLoadingGridAccess(FGrid).FetchFieldsEnabled); + AssertFalse(FGrid.InternalDrawGrid.Enabled); + AssertEquals(LFetches, FetchCount); + FGrid.RefreshGrid; + Settle; + AssertEquals('An explicit programmatic refresh remains possible', LFetches + 1, FetchCount); + AssertTrue('Completion does not release the caller hold', FGrid.LoadingVisible); + AssertFalse(TLoadingGridAccess(FGrid).ManualRegionsEnabled); + AssertFalse(FGrid.InternalDrawGrid.Enabled); + FGrid.HideLoading; + AssertTrue(TLoadingGridAccess(FGrid).ManualRegionsEnabled); + AssertTrue(TLoadingGridAccess(FGrid).FetchFieldsEnabled); +end; + +procedure TVisualGridRefreshTests.ManualLoadingSurvivesFetchCompletionAndError; +begin + FGrid.ShowLoading('Independent operation', 'Keep this message'); + FGrid.RefreshGrid; + Settle; + AssertTrue(FGrid.LoadingVisible); + AssertEquals('Independent operation', TLabel(FGrid.FindComponent('LoadingStatus')).Caption); + TGridRefreshSource(FSource).FailRead := True; + FGrid.RefreshGrid; + Settle; + AssertTrue('An automatic fetch error must not clear manual state', FGrid.LoadingVisible); + AssertEquals('Keep this message', TLabel(FGrid.FindComponent('LoadingSubtitle')).Caption); + FGrid.HideLoading; + AssertFalse(FGrid.LoadingVisible); + AssertTrue('Releasing the hold exposes the actual fetch error', TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); + AssertEquals('Controlled data-source failure', TLabel(FGrid.FindComponent('LoadingStatus')).Caption); + AssertEquals(ColorToRGB(clRed), ColorToRGB(TLabel(FGrid.FindComponent('LoadingStatus')).Font.Color)); + TGridRefreshSource(FSource).FailRead := False; + FGrid.RefreshGrid; + Settle; + AssertFalse(TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); +end; + +procedure TVisualGridRefreshTests.HidingManualLoadingRetainsActiveFetchRestrictions; +begin + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + FGrid.ShowLoading('Manual title'); + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + FGrid.HideLoading; + AssertFalse(FGrid.LoadingVisible); + AssertTrue('The real worker remains active', FGrid.IsUpdating); + AssertFalse('Hide cannot enable an active fetch', FGrid.InternalDrawGrid.Enabled); + PumpFor(350); + AssertTrue('The delayed automatic card still appears', TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); + AssertTrue(TLoadingCircle(FGrid.FindComponent('LoadingCircle')).Active); + AssertTrue(TLabel(FGrid.FindComponent('LoadingStatus')).Caption <> 'Manual title'); + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + Settle; + AssertFalse(TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); + AssertTrue(TLoadingGridAccess(FGrid).FetchFieldsEnabled); +end; + +procedure TVisualGridRefreshTests.ManualLoadingPreservesCallerDisabledGrid; +var LFetches: Integer; +begin + LFetches := FetchCount; + FGrid.Enabled := False; + FGrid.ShowLoading('Caller disabled'); + AssertFalse(FGrid.Enabled); + FGrid.HideLoading; + AssertFalse('Hide does not enable a caller-disabled root', FGrid.Enabled); + FGrid.ShowLoading('Nonblocking', '', nil, False); + FGrid.HideLoading; + AssertFalse(FGrid.Enabled); + AssertEquals(LFetches, FetchCount); + FGrid.Enabled := True; +end; + +procedure TVisualGridRefreshTests.NonblockingManualLoadingRespectsFetchRestrictions; +begin + FGrid.ShowLoading('Background information', '', nil, False); + AssertTrue(TLoadingGridAccess(FGrid).ManualRegionsEnabled); + AssertTrue(TLoadingGridAccess(FGrid).FetchFieldsEnabled); + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + AssertFalse(FGrid.InternalDrawGrid.Enabled); + FGrid.ShowLoading('Updated information', '', nil, False); + AssertFalse('A nonblocking manual state cannot override fetch restrictions', FGrid.InternalDrawGrid.Enabled); + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + Settle; + AssertTrue(FGrid.LoadingVisible); + AssertTrue(TLoadingGridAccess(FGrid).FetchFieldsEnabled); + FGrid.HideLoading; +end; + +procedure TVisualGridRefreshTests.ManualLoadingCanBeRequestedBeforeParenting; +var LUnparented: TVisualGrid; LFetches: Integer; +begin + LFetches := FetchCount; + LUnparented := TVisualGrid.Create(FForm); + try + LUnparented.ShowLoading('Before parenting', 'State is retained'); + AssertTrue(LUnparented.LoadingVisible); + AssertFalse('Presentation must not request an unparented native window', LUnparented.HandleAllocated); + LUnparented.SetBounds(0, 0, 320, 180); + LUnparented.Parent := FForm; + PumpFor(50); + AssertTrue(TPanel(LUnparented.FindComponent('LoadingOverlay')).Visible); + AssertEquals('Before parenting', TLabel(LUnparented.FindComponent('LoadingStatus')).Caption); + LUnparented.HideLoading; + AssertFalse(LUnparented.LoadingVisible); + finally + LUnparented.Free; + end; + AssertEquals(LFetches, FetchCount); +end; + +procedure TVisualGridQueuedCompletionTests.DisableTimers(AOwner: TComponent); +var I: Integer; +begin + for I := 0 to AOwner.ComponentCount - 1 do begin + if AOwner.Components[I] is TTimer then + TTimer(AOwner.Components[I]).Enabled := False; + DisableTimers(AOwner.Components[I]); + end; +end; + +procedure TVisualGridQueuedCompletionTests.PumpCompletionOnly(AMilliseconds: Cardinal); +var LUntil: QWord; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + // Deliberately do not pump window messages or any TTimer. The provider's + // queued main-thread completion must be sufficient to publish its page. + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TVisualGridQueuedCompletionTests.AwaitCompletionOnly; +var LUntil: QWord; +begin + LUntil := GetTickCount64 + 1500; + while FGrid.IsUpdating and (GetTickCount64 < LUntil) do + PumpCompletionOnly(1); + AssertFalse('A worker must publish through its queued notification, without a timer', + FGrid.IsUpdating); +end; + +{$IF defined(MSWINDOWS) and defined(VISUALGRID_TEST_WORKER_START)} +procedure TVisualGridQueuedCompletionTests.WorkerCancelledBeforeExecutionRetiresAndPreservesOtherReceivers; +begin + CheckVisualGridEarlyWorkerCancellation; +end; +{$ENDIF} + +procedure TVisualGridQueuedCompletionTests.CancelActiveFetchNeverJoinsOrPublishesAHeldQuery; +begin + CheckVisualGridActiveCancellation(acsHeldProvider); +end; + +procedure TVisualGridQueuedCompletionTests.CancelledFetchRetainsTheLatestFiveSecondAdmission; +begin + CheckVisualGridActiveCancellation(acsQuietAdmission); +end; + +procedure TVisualGridQueuedCompletionTests.CancelledProviderExceptionCannotPublishAnErrorCard; +begin + CheckVisualGridActiveCancellation(acsCancelledError); +end; + +{$IF defined(MSWINDOWS) and defined(VISUALGRID_TEST_WORKER_START)} +procedure TVisualGridQueuedCompletionTests.PublicCancellationBeforeStartKeepsTheLatestRequest; +begin + CheckVisualGridEarlyActiveCancellation; +end; +{$ENDIF} + +procedure TVisualGridQueuedCompletionTests.FinishedWorkerPublishesWithAllTimersDisabled; +var LBefore: Integer; +begin + FGrid.AutoPageSize := False; + Settle; + LBefore := FCompletions; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + DisableTimers(FGrid); + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + AwaitCompletionOnly; + AssertEquals('The actual worker publishes exactly once', LBefore + 1, FCompletions); + AssertTrue('Finished rows are readable', FGrid.RowCount > 0); + PumpCompletionOnly(50); + AssertEquals('A queued wake cannot publish the same worker twice', LBefore + 1, FCompletions); +end; + +procedure TVisualGridQueuedCompletionTests.FailedWorkerPublishesAndRetriesWithoutTimers; +var LBefore: Integer; +begin + FGrid.AutoPageSize := False; + Settle; + LBefore := FCompletions; + TGridRefreshSource(FSource).FailRead := True; + FGrid.RefreshGrid; + DisableTimers(FGrid); + AwaitCompletionOnly; + AssertEquals('A failed fetch is not a successful page completion', LBefore, FCompletions); + AssertTrue('The prior rows survive failure', FGrid.RowCount > 0); + AssertEquals('Controlled data-source failure', TLabel(FGrid.FindComponent('LoadingStatus')).Caption); + TGridRefreshSource(FSource).FailRead := False; + FGrid.RefreshGrid; + DisableTimers(FGrid); + AwaitCompletionOnly; + AssertEquals('The new actual worker can recover without polling', LBefore + 1, FCompletions); +end; + +procedure TVisualGridQueuedCompletionTests.DisposingQueuedReceiverPreservesOtherGridCompletion; +var LOtherSource: TGridRefreshSource; LOther: TVisualGrid; LBefore: Integer; + LUntil: QWord; +begin + FGrid.AutoPageSize := False; + Settle; + LOtherSource := TGridRefreshSource.Create(FForm); + LOther := TVisualGrid.Create(FForm); + LOther.Parent := FForm; + LOther.AutoPageSize := False; + LOther.PageSize := 5; + LOther.AddColumn('Id').Binding := 'Id'; + LOther.FetchDataInThread := True; + LOther.OnFinishedUpdating := Completed; + LOtherSource.BlockRead := True; + try + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + TGridRefreshSource(FSource).Returned.ResetEvent; + FGrid.RefreshGrid; + LOther.DataSource := LOtherSource; + AssertEquals(Ord(wrSignaled), Ord(LOtherSource.Entered.WaitFor(1000))); + DisableTimers(FGrid); + DisableTimers(LOther); + LBefore := FCompletions; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + LOtherSource.ReleaseRead.SetEvent; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Returned.WaitFor(1000))); + AssertEquals(Ord(wrSignaled), Ord(LOtherSource.Returned.WaitFor(1000))); + Sleep(20); // Producers can queue; the main thread has not pumped them. + FreeAndNil(FGrid); + LUntil := GetTickCount64 + 1500; + while LOther.IsUpdating and (GetTickCount64 < LUntil) do PumpCompletionOnly(1); + AssertFalse('Disposing one receiver cannot remove another grid completion', LOther.IsUpdating); + AssertEquals('Only the surviving grid publishes', LBefore + 1, FCompletions); + PumpCompletionOnly(30); + AssertEquals('Disposed queued work stays cancelled', LBefore + 1, FCompletions); + finally + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + LOtherSource.ReleaseRead.SetEvent; + LOther.Free; + end; +end; + +procedure TVisualGridQueuedCompletionTests.CompleteAndRequestNext(Sender: TObject); +begin + Completed(Sender); + if FRequestNextPage then begin + FRequestNextPage := False; + FGrid.PageIndex := 2; + end; +end; + +procedure TVisualGridQueuedCompletionTests.QueuedCompletionAcceptsANewClientRequest; +var LBefore: Integer; +begin + FGrid.AutoPageSize := False; + FGrid.PageSize := 10; + Settle; + LBefore := FCompletions; + FRequestNextPage := True; + FGrid.OnFinishedUpdating := CompleteAndRequestNext; + FGrid.RefreshGrid; + DisableTimers(FGrid); + AwaitCompletionOnly; + AssertEquals('A user completion refresh is new client work', LBefore + 2, FCompletions); + AssertEquals('The latest requested page is published', 2, FGrid.PageIndex); + AssertEquals(20, Integer(FGrid.Cells[0, 0])); +end; + +procedure TVisualGridQueuedCompletionTests.SourceReplacementDoesNotJoinANewHeldWorkerFromAStaleWake; +var LOtherSource: TGridRefreshSource; LStarted: QWord; +begin + FGrid.AutoPageSize := False; + FGrid.PageSize := 30; + Settle; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + TGridRefreshSource(FSource).Entered.ResetEvent; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + LOtherSource := TGridRefreshSource.Create(FForm); + LOtherSource.BlockRead := True; + LOtherSource.ItemCount := 23; + try + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + FGrid.DataSource := LOtherSource; + AssertEquals(Ord(wrSignaled), Ord(LOtherSource.Entered.WaitFor(1000))); + DisableTimers(FGrid); + LStarted := GetTickCount64; + PumpCompletionOnly(80); + AssertTrue('An obsolete wake must not join the replacement provider', + GetTickCount64 - LStarted < 400); + AssertTrue('The new held worker remains independently active', FGrid.IsUpdating); + LOtherSource.ReleaseRead.SetEvent; + AwaitCompletionOnly; + AssertEquals(23, TRequestGridAccess(FGrid).PublishedTotalCount); + AssertTrue((FGrid.RowCount > 0) and (FGrid.RowCount <= FGrid.PageSize)); + finally + LOtherSource.ReleaseRead.SetEvent; + FGrid.DataSource := TGridRefreshSource(FSource); + end; +end; + +procedure TVisualGridRequestThrottleTests.SetUp; +begin + inherited; + FWaitStarts := 0; + FWaitEnds := 0; + FRequestOnCompletion := False; + FGrid.AutoPageSize := False; + FGrid.PageSize := 10; + Settle; + FGrid.FetchDataInThread := False; + FGrid.OnFinishedUpdating := RequestCompleted; + FGrid.RefreshThrottle.OnWaitingChanged.Add(WaitingChanged); +end; + +procedure TVisualGridRequestThrottleTests.TearDown; +begin + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + FGrid.RefreshThrottle.OnWaitingChanged.Remove(WaitingChanged); + FGrid.RefreshThrottle.Mode := temNone; + inherited; +end; + +procedure TVisualGridRequestThrottleTests.WaitingChanged(Sender: TObject); +begin + AssertTrue(Sender = FGrid.RefreshThrottle); + if FGrid.RefreshThrottle.Waiting then Inc(FWaitStarts) else Inc(FWaitEnds); +end; + +procedure TVisualGridRequestThrottleTests.RequestCompleted(Sender: TObject); +begin + Completed(Sender); + if FRequestOnCompletion then begin + FRequestOnCompletion := False; + FGrid.RefreshGrid; + end; +end; + +procedure TVisualGridRequestThrottleTests.EnableThrottle(AInterval: Integer); +begin + FGrid.RefreshThrottle.Interval := TTimeSpan.FromMilliseconds(AInterval); + FGrid.RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; +end; + +function TVisualGridRequestThrottleTests.PageCalls: Integer; +begin + Result := InterlockedCompareExchange(TGridRefreshSource(FSource).PageCallCount,0,0); +end; + +procedure TVisualGridRequestThrottleTests.AwaitQuiet; +var LDeadline: QWord; +begin + LDeadline := GetTickCount64 + 5000; + repeat + PumpFor(10); + if not FGrid.RefreshThrottle.Waiting and not FGrid.IsUpdating then begin + PumpFor(60); + if not FGrid.RefreshThrottle.Waiting and not FGrid.IsUpdating then Exit; + end; + until GetTickCount64 >= LDeadline; + Fail('The actual grid did not finish its own quiet request and page fetch'); +end; + +procedure TVisualGridRequestThrottleTests.ContinuousClientRequestsAdmitOnlyLeadingAndFinalPages; +var LBefore,I: Integer; +begin + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshGrid; + AssertEquals(LBefore+1,PageCalls); + for I := 1 to 12 do begin + PumpFor(50); + FGrid.RefreshGrid; + AssertTrue(FGrid.RefreshThrottle.Waiting); + AssertEquals('Continuous inputs must not produce a middle page query',LBefore+1,PageCalls); + AssertEquals(0,FWaitEnds); + end; + PumpFor(100); + AssertEquals(LBefore+1,PageCalls); + AwaitQuiet; + AssertEquals(LBefore+2,PageCalls); + AssertEquals(1,FWaitStarts); + AssertEquals(1,FWaitEnds); +end; + +procedure TVisualGridRequestThrottleTests.SingleBusyLeadingRequestWaitsForItsOwnQuietFinish; +var LBefore: Integer; +begin + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + LBefore := PageCalls; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled),Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + EnableThrottle(1000); + FGrid.RefreshGrid; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + PumpFor(400); + AssertFalse('The older worker must have completed',FGrid.IsUpdating); + AssertTrue(FGrid.RefreshThrottle.Waiting); + AssertEquals('Worker completion cannot bypass a waiting single leading input',LBefore+1,PageCalls); + AwaitQuiet; + AssertEquals(LBefore+2,PageCalls); + AssertEquals(1,FWaitStarts); + AssertEquals(1,FWaitEnds); +end; + +procedure TVisualGridRequestThrottleTests.BusyInputsRetainTheLatestPageParameters; +var LBefore: Integer; +begin + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + LBefore := PageCalls; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled),Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + EnableThrottle(800); + FGrid.RefreshGrid; + FGrid.Visible := False; + FGrid.PageSize := 17; + FGrid.PageIndex := 3; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + PumpFor(400); + AssertTrue('Hidden inputs still reset the same owned throttle',FGrid.RefreshThrottle.Waiting); + AssertEquals(LBefore+1,PageCalls); + FGrid.Visible := True; + AwaitQuiet; + AssertEquals(LBefore+2,PageCalls); + AssertEquals(17,TGridRefreshSource(FSource).LastParams.PageSize); + AssertEquals(3,TGridRefreshSource(FSource).LastParams.PageIndex); + AssertEquals(51,Integer(FGrid.Cells[0,0])); +end; + +procedure TVisualGridRequestThrottleTests.PageSearchAndSortRequestsShareOneAdmission; +var LBefore: Integer; +begin + FGrid.Columns[0].Filters := [vgfNumericGT,vgfSortable]; + FGrid.SortMode := smSingleColumn; + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshGrid; + FGrid.PageSize := 25; + FGrid.PageIndex := 2; + TRequestGridAccess(FGrid).Search('>400'); + FGrid.Columns[0].SortDirection := sdDescending; + AssertEquals(LBefore+1,PageCalls); + PumpFor(100); + AssertEquals(LBefore+1,PageCalls); + AwaitQuiet; + AssertEquals('Paging, search and sort must produce one latest query',LBefore+2,PageCalls); + AssertEquals(25,TGridRefreshSource(FSource).LastParams.PageSize); + AssertEquals(2,TGridRefreshSource(FSource).LastParams.PageIndex); + AssertEquals(949,Integer(FGrid.Cells[0,0])); +end; + +procedure TVisualGridRequestThrottleTests.SourceReplacementUsesOnlyTheLatestAdmittedSource; +var LBefore: Integer; LNewSource: TGridRefreshSource; +begin + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshGrid; + LNewSource := TGridRefreshSource.Create(FForm); + LNewSource.ItemCount := 30; + FGrid.DataSource := LNewSource; + FGrid.PageSize := 7; + AssertEquals(LBefore+1,PageCalls); + AssertEquals(0,LNewSource.PageCallCount); + AwaitQuiet; + AssertEquals(1,LNewSource.PageCallCount); + AssertEquals(7,LNewSource.LastParams.PageSize); + AssertEquals(7,FGrid.RowCount); +end; + +procedure TVisualGridRequestThrottleTests.ResizeDuringWaitingIsVisualAndRetainsLatestCapacity; +var LBefore,LSize: Integer; +begin + FGrid.AutoPageSize := True; + Settle; + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshGrid; + FForm.Height := FForm.Height+40; + PumpFor(60); + FForm.Height := FForm.Height+50; + PumpFor(60); + LSize := FGrid.PageSize; + AssertTrue(FGrid.RefreshThrottle.Waiting); + AssertEquals('Resize while Waiting must update geometry without querying data',LBefore+1,PageCalls); + AssertEquals(0,FWaitEnds); + AwaitQuiet; + AssertEquals(LBefore+2,PageCalls); + AssertEquals(LSize,TGridRefreshSource(FSource).LastParams.PageSize); + AssertEquals(LSize,FGrid.RowCount); + PumpFor(400); + AssertEquals('Settled geometry cannot restart another burst/query',LBefore+2,PageCalls); + AssertEquals(1,FWaitStarts); +end; + +procedure TVisualGridRequestThrottleTests.CompletedResultLayoutCannotStartAnotherBurst; +var LBefore: Integer; +begin + FGrid.AutoPageSize := True; + Settle; + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).ItemCount := 0; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled),Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + PumpFor(400); + AssertFalse('Own quiet finish is independent of the held page fetch',FGrid.RefreshThrottle.Waiting); + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + AwaitQuiet; + PumpFor(500); + AssertEquals('Applying empty-page chrome must not notify or refetch itself',LBefore+1,PageCalls); + AssertEquals(1,FWaitStarts); + AssertEquals(1,FWaitEnds); + AssertEquals(0,FGrid.RowCount); +end; + +procedure TVisualGridRequestThrottleTests.FinishedUpdatingClientRequestIsNewInput; +var LBefore: Integer; +begin + EnableThrottle; + LBefore := PageCalls; + FRequestOnCompletion := True; + FGrid.RefreshGrid; + AssertEquals(LBefore+1,PageCalls); + AssertTrue(FGrid.RefreshThrottle.Waiting); + AwaitQuiet; + AssertEquals('A consumer completion callback remains a new client request',LBefore+2,PageCalls); + AssertEquals(1,FWaitStarts); + AssertEquals(1,FWaitEnds); +end; + +procedure TVisualGridRequestThrottleTests.ANewBurstAfterQuietLeadsImmediately; +var LBefore: Integer; +begin + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshGrid; + FGrid.RefreshGrid; + AwaitQuiet; + AssertEquals(LBefore+2,PageCalls); + FGrid.RefreshGrid; + AssertEquals('A new independent request must lead before returning',LBefore+3,PageCalls); + AssertEquals(2,FWaitStarts); +end; + +procedure TVisualGridRequestThrottleTests.CancelCannotReleaseABusyWaitingRequest; +var LBefore: Integer; +begin + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + LBefore := PageCalls; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled),Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + EnableThrottle(800); + FGrid.RefreshGrid; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + PumpFor(400); + AssertTrue(FGrid.RefreshThrottle.Waiting); + AssertFalse(FGrid.IsUpdating); + FGrid.RefreshThrottle.Cancel; + PumpFor(1000); + AssertFalse(FGrid.RefreshThrottle.Waiting); + AssertEquals('Cancel must not release a deferred leading page',LBefore+1,PageCalls); + FGrid.RefreshGrid; + PumpFor(100); + AssertEquals('A new request after cancellation still leads immediately',LBefore+2,PageCalls); + AwaitQuiet; +end; + +procedure TVisualGridRequestThrottleTests.ModeChangeCancelsPendingPageWork; +var LBefore: Integer; +begin + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshGrid; + FGrid.PageSize := 17; + FGrid.RefreshThrottle.Mode := temNone; + PumpFor(400); + AssertEquals('A mode change cancels old work rather than admitting it',LBefore+1,PageCalls); + FGrid.RefreshGrid; + AssertEquals(LBefore+2,PageCalls); + AssertEquals(17,TGridRefreshSource(FSource).LastParams.PageSize); +end; + +procedure TVisualGridRequestThrottleTests.OwnerDestructionCannotReleaseCancelledWork; +var LGrid: TVisualGrid; LSource: TGridRefreshSource; +begin + LSource := TGridRefreshSource.Create(FForm); + LGrid := TVisualGrid.Create(FForm); + try + LGrid.Parent := FForm; + LGrid.SetBounds(0,0,300,200); + LGrid.PageSize := 10; + LGrid.AddColumn('Id').Binding := 'Id'; + LSource.BlockRead := True; + LGrid.DataSource := LSource; + AssertEquals(Ord(wrSignaled),Ord(LSource.Entered.WaitFor(1000))); + LGrid.RefreshThrottle.Interval := TTimeSpan.FromMilliseconds(800); + LGrid.RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; + LGrid.RefreshGrid; + LSource.ReleaseRead.SetEvent; + PumpFor(400); + AssertTrue(LGrid.RefreshThrottle.Waiting); + LGrid.RefreshThrottle.Cancel; + finally + LSource.ReleaseRead.SetEvent; + LGrid.Free; + end; + PumpFor(1000); + AssertEquals('Cancelling and destroying the owner must not start another page',1,LSource.PageCallCount); +end; + +procedure TVisualGridRequestThrottleTests.PartialResultLayoutRetainsLatestAdmittedCapacity; +var LBefore,LCount,LInitialCapacity,LNativeCapacity,LPitch: Integer; + LFirst,LSecond: TRect; +begin + FGrid.AutoPageSize := True; + Settle; + LInitialCapacity := FGrid.PageSize; + AssertTrue('The geometry fixture needs at least three initial data rows',LInitialCapacity > 2); + // The mode selector retains its footer. A one-page result instead hides + // search chrome, exposing actual extra rows before the final admission. + LCount := LInitialCapacity-1; + TGridRefreshSource(FSource).ItemCount := LCount; + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + EnableThrottle(1000); + LBefore := PageCalls; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled),Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + FGrid.RefreshGrid; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + PumpFor(400); + AssertTrue(FGrid.RefreshThrottle.Waiting); + AssertEquals('Applying partial-page chrome cannot query during Waiting',LBefore+1,PageCalls); + AssertEquals(0,FWaitEnds); + // Derive capacity from native cell geometry rather than the grid's sizing + // implementation or an assumption that the entire footer disappears. + LFirst := FGrid.InternalDrawGrid.CellRect(0,FGrid.InternalDrawGrid.FixedRows); + LSecond := FGrid.InternalDrawGrid.CellRect(0,FGrid.InternalDrawGrid.FixedRows+1); + LPitch := LSecond.Top-LFirst.Top; + AssertTrue('Native rows need a positive pitch',LPitch > 0); + AssertTrue('The first native data row must fit',LFirst.Bottom <= FGrid.InternalDrawGrid.ClientHeight); + LNativeCapacity := 1+(FGrid.InternalDrawGrid.ClientHeight-LFirst.Bottom) div LPitch; + AssertTrue('Internal search layout must expose additional complete rows',LNativeCapacity > LInitialCapacity); + AwaitQuiet; + AssertEquals('The final admitted page contains every matching row',LCount,FGrid.RowCount); + AssertEquals('The final query uses independently measured settled capacity', + LNativeCapacity,TGridRefreshSource(FSource).LastParams.PageSize); + AssertEquals('Own layout must not turn into another request/burst',1,FWaitStarts); + AssertEquals(1,FWaitEnds); + AssertEquals('The one latest final query must already include settled capacity',LBefore+2,PageCalls); +end; + +procedure TVisualGridRequestThrottleTests.ClearingSourcePublishesEmptyRowsWithoutLayoutReplay; +var LBefore: Integer; +begin + LBefore := PageCalls; + FGrid.DataSource := nil; + AssertEquals('A cleared source must publish an empty page',0,FGrid.RowCount); + PumpFor(300); + AssertEquals('Clearing a source has no provider query to replay',LBefore,PageCalls); + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + FGrid.DataSource := TGridRefreshSource(FSource); + AssertEquals(Ord(wrSignaled),Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + EnableThrottle(500); + FGrid.RefreshGrid; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + FGrid.DataSource := nil; + AssertTrue(FGrid.RefreshThrottle.Waiting); + AwaitQuiet; + AssertEquals('A waiting clear request must retain its latest nil source',0,FGrid.RowCount); + AssertEquals(LBefore+1,PageCalls); +end; + +procedure TVisualGridRequestThrottleTests.SuspendedInputsRetainLatestParametersUntilImmediateResume; +var LBefore: Integer; +begin + FGrid.Columns[0].Filters := [vgfNumericGT, vgfSortable]; + FGrid.SortMode := smSingleColumn; + EnableThrottle(5000); + LBefore := PageCalls; + FGrid.RefreshSuspended := True; + FGrid.RefreshGrid; + FGrid.PageSize := 25; + FGrid.PageIndex := 2; + TRequestGridAccess(FGrid).Search('>400'); + FGrid.Columns[0].SortDirection := sdDescending; + AssertTrue('Client inputs still reach the independent throttle', FGrid.RefreshThrottle.Waiting); + AssertEquals('Suspension must reject even the leading fetch', LBefore, PageCalls); + FGrid.RefreshThrottle.Cancel; + AssertEquals('Cancelling the quiet interval while suspended cannot query', LBefore, PageCalls); + FGrid.RefreshSuspended := False; + AssertEquals('Resume admits one page immediately, without a five-second wait', LBefore + 1, PageCalls); + AssertEquals(25, TGridRefreshSource(FSource).LastParams.PageSize); + AssertEquals(2, TGridRefreshSource(FSource).LastParams.PageIndex); + AssertEquals('Latest search and sort survive suspension', 949, Integer(FGrid.Cells[0, 0])); + AssertFalse('Internal resume does not notify the throttle again', FGrid.RefreshThrottle.Waiting); + PumpFor(300); + AssertEquals('The retained request is delivered once', LBefore + 1, PageCalls); +end; + +procedure TVisualGridRequestThrottleTests.SuspendedResizeChangesGeometryWithoutFetching; +var LBefore, LSize: Integer; +begin + FGrid.AutoPageSize := True; + Settle; + EnableThrottle(5000); + LBefore := PageCalls; + FGrid.RefreshSuspended := True; + FGrid.RefreshGrid; + FForm.Height := FForm.Height + 80; + PumpFor(60); + FForm.Height := FForm.Height + 60; + PumpFor(60); + LSize := FGrid.PageSize; + AssertEquals('Paused resizing is visual only', LBefore, PageCalls); + FGrid.RefreshThrottle.Cancel; + FGrid.RefreshSuspended := False; + AssertEquals(LBefore + 1, PageCalls); + AssertEquals(LSize, TGridRefreshSource(FSource).LastParams.PageSize); + AssertEquals(LSize, FGrid.RowCount); + PumpFor(300); + AssertEquals('The result layout cannot replay an older capacity', LBefore + 1, PageCalls); +end; + +procedure TVisualGridRequestThrottleTests.SuspensionCancelsWorkerWithoutJoiningOrPublishingError; +var LBefore, LCompleted, LRow: Integer; LStarted, LDeadline: QWord; +begin + FGrid.FetchDataInThread := True; + LRow := Integer(FGrid.Cells[0, 0]); + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).FailRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + LBefore := PageCalls; + LCompleted := FCompletions; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + LStarted := GetTickCount64; + FGrid.RefreshSuspended := True; + AssertTrue('Suspension cannot join the held provider on the UI thread', GetTickCount64 - LStarted < 150); + FGrid.ShowLoading('Please Wait', 'Synchronizing'); + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + LDeadline := GetTickCount64 + 1500; + while FGrid.IsUpdating and (GetTickCount64 < LDeadline) do PumpFor(1); + AssertFalse('Cancelled work is retired by its completion notification', FGrid.IsUpdating); + AssertEquals('A cancelled error cannot publish a page', LCompleted, FCompletions); + AssertEquals('Old visible data survives cancellation', LRow, Integer(FGrid.Cells[0, 0])); + AssertTrue('Worker retirement cannot clear caller-owned sync presentation', FGrid.LoadingVisible); + AssertEquals('Cancelled errors cannot replace the held title', 'Please Wait', + TLabel(FGrid.FindComponent('LoadingStatus')).Caption); + AssertEquals('Retiring the worker cannot fetch while suspended', LBefore + 1, PageCalls); + TGridRefreshSource(FSource).FailRead := False; + TGridRefreshSource(FSource).BlockRead := False; + FGrid.RefreshThrottle.Cancel; + FGrid.RefreshSuspended := False; + Settle; + AssertEquals(LBefore + 2, PageCalls); + AssertEquals(LCompleted + 1, FCompletions); + FGrid.HideLoading; +end; + +procedure TVisualGridRequestThrottleTests.ResumeBeforeCancelledWorkerRetiresFetchesOnlyLatestPage; +var LBefore, LCompleted: Integer; +begin + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + LBefore := PageCalls; + LCompleted := FCompletions; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + FGrid.RefreshSuspended := True; + EnableThrottle(5000); + FGrid.PageSize := 17; + FGrid.PageIndex := 3; + FGrid.RefreshThrottle.Cancel; + FGrid.RefreshSuspended := False; + AssertEquals('Resume must not start a second concurrent worker', LBefore + 1, PageCalls); + TGridRefreshSource(FSource).BlockRead := False; + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + Settle; + AssertEquals('Old cancellation is followed by one latest query', LBefore + 2, PageCalls); + AssertEquals('Only the replacement page is published', LCompleted + 1, FCompletions); + AssertEquals(17, TGridRefreshSource(FSource).LastParams.PageSize); + AssertEquals(3, TGridRefreshSource(FSource).LastParams.PageIndex); + AssertEquals(51, Integer(FGrid.Cells[0, 0])); + AssertFalse(FGrid.RefreshThrottle.Waiting); +end; + +procedure TVisualGridRequestThrottleTests.ResuspensionCannotReviveACancelledWorkerResult; +var LBefore, LCompleted, LRow: Integer; LDeadline: QWord; +begin + FGrid.FetchDataInThread := True; + LRow := Integer(FGrid.Cells[0, 0]); + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + LBefore := PageCalls; + LCompleted := FCompletions; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + FGrid.RefreshSuspended := True; + FGrid.PageIndex := 2; + FGrid.RefreshThrottle.Cancel; + FGrid.RefreshSuspended := False; + FGrid.RefreshSuspended := True; + FGrid.ShowLoading('Please Wait', 'Synchronizing'); + TGridRefreshSource(FSource).ReleaseRead.SetEvent; + LDeadline := GetTickCount64 + 1500; + while FGrid.IsUpdating and (GetTickCount64 < LDeadline) do PumpFor(1); + AssertFalse(FGrid.IsUpdating); + AssertEquals('A true/false/true flip cannot publish the cancelled result', LCompleted, FCompletions); + AssertEquals(LRow, Integer(FGrid.Cells[0, 0])); + AssertEquals('Resuspension prevents the deferred fetch too', LBefore + 1, PageCalls); + AssertTrue(FGrid.LoadingVisible); + TGridRefreshSource(FSource).BlockRead := False; + FGrid.PageIndex := 4; + FGrid.RefreshThrottle.Cancel; + FGrid.RefreshSuspended := False; + Settle; + AssertEquals(LBefore + 2, PageCalls); + AssertEquals(LCompleted + 1, FCompletions); + AssertEquals(40, Integer(FGrid.Cells[0, 0])); + FGrid.HideLoading; +end; + +procedure TVisualGridRequestThrottleTests.SuspendedQuietCompletionCannotAdmitAQuery; +var LBefore: Integer; +begin + EnableThrottle; + LBefore := PageCalls; + FGrid.RefreshSuspended := True; + FGrid.RefreshGrid; + FGrid.PageIndex := 3; + PumpFor(500); + AssertFalse('The generic throttle still owns and finishes its timer', FGrid.RefreshThrottle.Waiting); + AssertEquals('Quiet completion cannot bypass suspension', LBefore, PageCalls); + FGrid.RefreshSuspended := False; + AssertEquals(LBefore + 1, PageCalls); + AssertEquals(30, Integer(FGrid.Cells[0, 0])); + PumpFor(300); + AssertEquals(LBefore + 1, PageCalls); +end; + +procedure TVisualGridRequestThrottleTests.SuspendedSourceReplacementUsesOnlyLatestSource; +var LBefore: Integer; LNewSource: TGridRefreshSource; +begin + EnableThrottle(5000); + LBefore := PageCalls; + FGrid.RefreshSuspended := True; + LNewSource := TGridRefreshSource.Create(FForm); + LNewSource.ItemCount := 30; + FGrid.DataSource := LNewSource; + FGrid.PageSize := 7; + FGrid.PageIndex := 2; + AssertEquals(LBefore, PageCalls); + AssertEquals(0, LNewSource.PageCallCount); + FGrid.RefreshThrottle.Cancel; + FGrid.RefreshSuspended := False; + AssertEquals('Only the newest source receives the resumed query', 1, LNewSource.PageCallCount); + AssertEquals(LBefore, PageCalls); + AssertEquals(14, Integer(FGrid.Cells[0, 0])); + FGrid.RefreshThrottle.Mode := temNone; + FGrid.DataSource := TGridRefreshSource(FSource); +end; + +procedure TVisualGridRequestThrottleTests.SuspensionCooperativelyReleasesTheProviderBeforeResume; +var LBefore, LCompleted: Integer; LDeadline: QWord; +begin + FGrid.FetchDataInThread := True; + TGridRefreshSource(FSource).BlockRead := True; + TGridRefreshSource(FSource).CooperativeRead := True; + TGridRefreshSource(FSource).Entered.ResetEvent; + TGridRefreshSource(FSource).ReleaseRead.ResetEvent; + LBefore := PageCalls; + LCompleted := FCompletions; + FGrid.RefreshGrid; + AssertEquals(Ord(wrSignaled), Ord(TGridRefreshSource(FSource).Entered.WaitFor(1000))); + FGrid.RefreshSuspended := True; + AssertEquals('The provider observes the worker cancellation without manual gate release', + Ord(wrSignaled), Ord(TGridRefreshSource(FSource).CancelObserved.WaitFor(1000))); + LDeadline := GetTickCount64 + 1500; + while FGrid.IsUpdating and (GetTickCount64 < LDeadline) do PumpFor(1); + AssertFalse(FGrid.IsUpdating); + AssertEquals('Abort is not a completed page or a visible error', LCompleted, FCompletions); + TGridRefreshSource(FSource).BlockRead := False; + FGrid.PageIndex := 5; + FGrid.RefreshThrottle.Cancel; + FGrid.RefreshSuspended := False; + Settle; + AssertEquals('The next worker acquires the same real data-source lock after Abort finally', + LBefore + 2, PageCalls); + AssertEquals(LCompleted + 1, FCompletions); + AssertEquals(50, Integer(FGrid.Cells[0, 0])); +end; + +procedure TVisualGridRequestThrottleTests.QueuedRequestShowsLoadingBeforeFetchAdmission; +var LCalls: Integer; +begin + EnableThrottle(900); + FGrid.RefreshGrid; + LCalls := TGridRefreshSource(FSource).PageCallCount; + FGrid.PageIndex := 1; + PumpFor(250); + AssertTrue(FGrid.RefreshThrottle.Waiting); + AssertEquals('Waiting must not fetch the new selection',LCalls, + TGridRefreshSource(FSource).PageCallCount); + AssertTrue('Queued work needs visible feedback before the fetch starts', + TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); + AssertTrue(TLoadingCircle(FGrid.FindComponent('LoadingCircle')).Active); + PumpFor(800); + AssertEquals(LCalls+1,TGridRefreshSource(FSource).PageCallCount); + AssertFalse('A completed synchronous fetch also clears its indicator', + TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); +end; + +procedure TVisualGridRequestThrottleTests.CancelledQueuedRequestHidesLoading; +begin + EnableThrottle(900); + FGrid.RefreshGrid; + FGrid.PageIndex := 1; + PumpFor(250); + AssertTrue(TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); + FGrid.RefreshThrottle.Cancel; + AssertFalse('Cancel retires waiting feedback without starting a provider', + TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); +end; + +procedure TVisualGridRequestThrottleTests.ManualLoadingOverridesQueuedRefreshPresentation; +begin + EnableThrottle(900); + FGrid.RefreshGrid; + FGrid.PageIndex := 1; + FGrid.ShowLoading('Please Wait','Synchronizing'); + PumpFor(250); + AssertEquals('Please Wait',TLabel(FGrid.FindComponent('LoadingStatus')).Caption); + FGrid.RefreshThrottle.Cancel; + AssertTrue('Ordinary cancellation must not dismiss caller-owned artwork', + TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); + FGrid.HideLoading; + AssertFalse(TPanel(FGrid.FindComponent('LoadingOverlay')).Visible); +end; + +procedure TVisualGridRequestThrottleTests.DeferredSingleInputRetainsLoadingThroughQuietAdmission; +var LUntil: QWord; LSource: TGridRefreshSource; LPanel: TPanel; +begin + LSource := TGridRefreshSource(FSource); + FGrid.FetchDataInThread := True; + LSource.BlockRead := True; + LSource.ReleaseRead.ResetEvent; + LSource.Entered.ResetEvent; + FGrid.RefreshGrid; + try + LUntil := GetTickCount64+2000; + while (LSource.Entered.WaitFor(0)<>wrSignaled) and (GetTickCount64<LUntil) do PumpFor(5); + AssertTrue('Original provider entered its actual read',LSource.Entered.WaitFor(0)=wrSignaled); + EnableThrottle(900); + FGrid.PageIndex := 1; + LSource.ReleaseRead.SetEvent; + LUntil := GetTickCount64+2000; + while FGrid.IsUpdating and (GetTickCount64<LUntil) do PumpFor(5); + AssertFalse(FGrid.IsUpdating); + LSource.ReleaseRead.ResetEvent; + PumpFor(250); + LPanel := TPanel(FGrid.FindComponent('LoadingOverlay')); + AssertTrue('Deferred single input already has visible feedback',LPanel.Visible); + LUntil := GetTickCount64+2000; + while FGrid.RefreshThrottle.Waiting and (GetTickCount64<LUntil) do PumpFor(5); + AssertFalse(FGrid.RefreshThrottle.Waiting); + AssertTrue('Genuine quiet finish admitted the retained held provider',FGrid.IsUpdating); + AssertTrue('Admission must retain an already visible card without another delay',LPanel.Visible); + finally LSource.ReleaseRead.SetEvent; end; +end; + +initialization + RegisterTest(TVisualGridSelectionEventTests); + RegisterTest(TVisualGridLoadingLifecycleTests); + RegisterTest(TVisualGridPagingTests); + RegisterTest(TVisualGridViewportTests); + RegisterTest(TVisualGridRefreshTests); + RegisterTest(TVisualGridRequestThrottleTests); + RegisterTest(TVisualGridQueuedCompletionTests); +end. diff --git a/src/tests/experimental/framework/UVisualGrid.Selection.Checks.pas b/src/tests/experimental/framework/UVisualGrid.Selection.Checks.pas new file mode 100644 index 000000000..8bfbf02ab --- /dev/null +++ b/src/tests/experimental/framework/UVisualGrid.Selection.Checks.pas @@ -0,0 +1,243 @@ +unit UVisualGrid.Selection.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Shared selection checks use native mouse messages and the actual grid event + path. The integer source is the only data fixture; no wallet is involved. } + +{$IFDEF FPC}{$mode delphi}{$H+}{$ENDIF} + +interface + +uses Classes; + +type + TGridSelectionScenario = (ssNativeClickEvents, ssRepeatedClickToggle, + ssConsumerClearSelection, ssProgrammaticAndNoHandler); + +procedure CheckVisualGridSelection(AScenario: TGridSelectionScenario; ALog: TStrings = nil); + +implementation + +uses + SysUtils, Types, Variants, Forms, Controls, Generics.Collections, + UCommon.Data, UVisualGrid + {$IFDEF MSWINDOWS}, Windows, Messages{$ENDIF}; + +type + TSelectionSource = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const AColumnName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + end; + + TSelectionObserver = class(TComponent) + public + Grid: TVisualGrid; + Pages, Rows: TArray<Integer>; + ClearOnNextSelection: Boolean; + procedure Selected(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); + procedure Reset; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create('Grid selection: ' + AMessage); +end; + +procedure PumpMessages; +begin + CheckSynchronize(0); + Application.ProcessMessages; +end; + +function TSelectionSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id', True)); +end; + +function TSelectionSource.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const AColumnName: String): Variant; +begin + Result := AItem; +end; + +procedure TSelectionSource.FetchAll(const AContainer: TList<Integer>); +var + I: Integer; +begin + for I := 0 to 5 do + AContainer.Add(I); +end; + +procedure TSelectionObserver.Reset; +begin + Pages := nil; + Rows := nil; +end; + +procedure TSelectionObserver.Selected(Sender: TObject; {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection); +var + LIndex: Integer; +begin + LIndex := Length(Pages); + SetLength(Pages, LIndex + 1); + SetLength(Rows, LIndex + 1); + Pages[LIndex] := ASelection.Page; + if ASelection.Page < 0 then + Rows[LIndex] := -1 + else + Rows[LIndex] := ASelection.Row; + if ClearOnNextSelection and (ASelection.Page >= 0) then + begin + ClearOnNextSelection := False; + Grid.ClearSelection(True); + end; +end; + +procedure NativeClickRow(AGrid: TVisualGrid; ARow: Integer); +{$IFDEF MSWINDOWS} +var + LRect: TRect; + LX, LY: Integer; + LPosition: LPARAM; +{$ENDIF} +begin + {$IFDEF MSWINDOWS} + LRect := AGrid.InternalDrawGrid.CellRect(0, ARow); + LX := LRect.Left + (LRect.Right - LRect.Left) div 2; + LY := LRect.Top + (LRect.Bottom - LRect.Top) div 2; + Require((LX >= 0) and (LY >= 0) and + (LX < AGrid.InternalDrawGrid.ClientWidth) and + (LY < AGrid.InternalDrawGrid.ClientHeight), 'clicked row is outside the native client'); + LPosition := LPARAM((Cardinal(LY) shl 16) or (Cardinal(LX) and $FFFF)); + // Deliver both native button messages to this owned grid window. No global + // cursor/input state or direct selection-event invocation is used. + SendMessage(AGrid.InternalDrawGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, LPosition); + SendMessage(AGrid.InternalDrawGrid.Handle, WM_LBUTTONUP, 0, LPosition); + PumpMessages; + {$ELSE} + raise Exception.Create('Native mouse selection checks require the Windows widgetset'); + {$ENDIF} +end; + +procedure CheckVisualGridSelection(AScenario: TGridSelectionScenario; ALog: TStrings); +var + LForm: TForm; + LGrid: TVisualGrid; + LSource: TSelectionSource; + LObserver: TSelectionObserver; + + procedure RequireEvent(ACount, AIndex, APage, ARow: Integer; const AContext: String); + begin + Require(Length(LObserver.Pages) = ACount, + AContext + ': expected ' + IntToStr(ACount) + ' events, received ' + + IntToStr(Length(LObserver.Pages))); + Require((LObserver.Pages[AIndex] = APage) and + (LObserver.Rows[AIndex] = ARow), AContext + ': wrong selection payload'); + end; + +begin + {$IFNDEF MSWINDOWS} + if AScenario in [ssNativeClickEvents, ssRepeatedClickToggle] then + begin + if Assigned(ALog) then + ALog.Add('SKIP native mouse selection requires the Windows widgetset'); + Exit; + end; + {$ENDIF} + LForm := TForm.CreateNew(nil); + try + LForm.SetBounds(80, 90, 640, 440); + LSource := TSelectionSource.Create(LForm); + LGrid := TVisualGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.FetchDataInThread := False; + LGrid.PageMode := vpmManual; + LGrid.ManualPageSize := 20; + LGrid.SelectionType := stRow; + LGrid.DeselectionType := dtDefault; + LGrid.AddColumn('Id').Binding := 'Id'; + LGrid.DataSource := LSource; + LForm.Show; + PumpMessages; + Require(not LGrid.IsUpdating and (LGrid.RowCount = 6), 'fixture did not publish its six rows'); + LGrid.ClearSelection(True); + LObserver := TSelectionObserver.Create(LForm); + LObserver.Grid := LGrid; + LGrid.OnSelection := LObserver.Selected; + + case AScenario of + ssNativeClickEvents: + begin + NativeClickRow(LGrid, 1); + RequireEvent(1, 0, 0, 0, 'first native click'); + Require(Length(LGrid.SelectedRows) = 1, 'first click did not retain one selected row'); + NativeClickRow(LGrid, 2); + RequireEvent(2, 1, 0, 1, 'second-row native click'); + Require(Integer(TDataRowData(LGrid.SelectedRows[0])['Id']) = 1, + 'second native click retained the wrong record'); + end; + ssRepeatedClickToggle: + begin + NativeClickRow(LGrid, 2); + RequireEvent(1, 0, 0, 1, 'initial toggle selection'); + NativeClickRow(LGrid, 2); + RequireEvent(2, 1, -1, -1, 'repeated native click'); + Require(Length(LGrid.SelectedRows) = 0, 'repeated click did not clear selection'); + NativeClickRow(LGrid, 2); + RequireEvent(3, 2, 0, 1, 'native click after deselection'); + end; + ssConsumerClearSelection: + begin + LGrid.OnSelection := nil; + LGrid.DeselectionType := dtNone; + LGrid.InternalDrawGrid.Row := 1; + LObserver.Reset; + LObserver.ClearOnNextSelection := True; + LGrid.OnSelection := LObserver.Selected; + // A real native selection change enters the same event path. The + // handler's explicit clear must publish one genuine empty follow-up. + LGrid.InternalDrawGrid.Row := 3; + PumpMessages; + RequireEvent(2, 0, 0, 2, 'callback selected event'); + RequireEvent(2, 1, -1, -1, 'callback clear follow-up'); + Require(Length(LGrid.SelectedRows) = 0, 'callback clear was not retained'); + LGrid.OnSelection := nil; + LGrid.SelectionType := stNone; + LGrid.SelectionType := stRow; + LObserver.Reset; + LGrid.OnSelection := LObserver.Selected; + LGrid.InternalDrawGrid.Row := 4; + RequireEvent(1, 0, 0, 3, 'event guard after consumer clear'); + end; + ssProgrammaticAndNoHandler: + begin + LGrid.OnSelection := nil; + LGrid.DeselectionType := dtNone; + LGrid.InternalDrawGrid.Row := 1; + LObserver.Reset; + LGrid.OnSelection := LObserver.Selected; + LGrid.InternalDrawGrid.Row := 2; + RequireEvent(1, 0, 0, 1, 'programmatic native selection'); + LGrid.ClearSelection(True); + RequireEvent(2, 1, -1, -1, 'programmatic explicit clear'); + Require(Length(LGrid.SelectedRows) = 0, 'explicit clear retained rows'); + LGrid.OnSelection := nil; + LGrid.SelectionType := stNone; + LGrid.SelectionType := stRow; + LGrid.InternalDrawGrid.Row := 3; + Require(Length(LGrid.SelectedRows) = 1, 'no-handler selection did not retain its row'); + LGrid.ClearSelection(True); + Require(Length(LGrid.SelectedRows) = 0, 'no-handler clear retained its native cursor as selection'); + end; + end; + if Assigned(ALog) then + ALog.Add('PASS native grid selection scenario ' + IntToStr(Ord(AScenario))); + finally + LForm.Free; + end; +end; + +end. diff --git a/src/tests/experimental/framework/UVisualGrid.Tests.pas b/src/tests/experimental/framework/UVisualGrid.Tests.pas new file mode 100644 index 000000000..d33b4851d --- /dev/null +++ b/src/tests/experimental/framework/UVisualGrid.Tests.pas @@ -0,0 +1,121 @@ +unit UVisualGrid.Tests; + +{ Copyright © 2026 Herman Schoenfeld + Distributed under the MIT software license; see LICENSE. } + +{$mode delphi} + +interface + +uses + Classes, SysUtils, Types, Variants, Forms, Grids, Generics.Collections, + fpcunit, testregistry, UCommon.Data, UVisualGrid; + +type + TVisualGridSelectionTests = class(TTestCase) + private + FForm: TForm; + FGrid: TVisualGrid; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure DisjointAndOverlappingRangesReturnUniqueRows; + procedure ClearSelectionWithoutHandlerReturnsNoRows; + end; + +implementation + +type + TSelectionSource = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + function GetItemField(constref AItem: Integer; const AColumnName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + end; + + TGridSelectionAccess = class(Grids.TDrawGrid) + public + procedure RetainRange; + end; + +function TSelectionSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id',True)); +end; + +function TSelectionSource.GetItemField(constref AItem: Integer; const AColumnName: String): Variant; +begin + Result := AItem; +end; + +procedure TSelectionSource.FetchAll(const AContainer: TList<Integer>); +var i: Integer; +begin + for i := 0 to 4 do AContainer.Add(i); +end; + +procedure TGridSelectionAccess.RetainRange; +begin + AddSelectedRange; +end; + +procedure TVisualGridSelectionTests.SetUp; +var LSource: TSelectionSource; +begin + inherited SetUp; + FForm := TForm.CreateNew(nil); + LSource := TSelectionSource.Create(FForm); + FGrid := TVisualGrid.Create(FForm); + FGrid.Parent := FForm; + FGrid.FetchDataInThread := False; + FGrid.AutoPageSize := False; + FGrid.PageSize := 5; + FGrid.AddColumn('Id').Binding := 'Id'; + FGrid.DataSource := LSource; + // The first source assignment establishes the paging and search layout. + FGrid.PageSize := 5; + FGrid.SelectionType := stMultiRow; + FGrid.DeselectionType := dtNone; + FGrid.InternalDrawGrid.RangeSelectMode := rsmMulti; +end; + +procedure TVisualGridSelectionTests.TearDown; +begin + FForm.Free; + inherited TearDown; +end; + +procedure TVisualGridSelectionTests.DisjointAndOverlappingRangesReturnUniqueRows; +var LRows: TArray<Variant>; +begin + FGrid.InternalDrawGrid.Selection := Rect(0,1,0,2); + TGridSelectionAccess(FGrid.InternalDrawGrid).RetainRange; + FGrid.InternalDrawGrid.Selection := Rect(0,2,0,3); + TGridSelectionAccess(FGrid.InternalDrawGrid).RetainRange; + FGrid.InternalDrawGrid.Selection := Rect(0,5,0,5); + AssertEquals('Fetched five rows',5,FGrid.RowCount); + AssertEquals('Native grid has a header and five rows',6,FGrid.InternalDrawGrid.RowCount); + AssertEquals('Current range reaches fifth data row',5,FGrid.InternalDrawGrid.Selection.Bottom); + LRows := FGrid.SelectedRows; + AssertEquals('Each selected row appears once',4,Length(LRows)); + AssertEquals(0,Integer(TDataRowData(LRows[0])['Id'])); + AssertEquals(1,Integer(TDataRowData(LRows[1])['Id'])); + AssertEquals(2,Integer(TDataRowData(LRows[2])['Id'])); + AssertEquals(4,Integer(TDataRowData(LRows[3])['Id'])); +end; + +procedure TVisualGridSelectionTests.ClearSelectionWithoutHandlerReturnsNoRows; +begin + AssertFalse('No client selection callback is installed',Assigned(FGrid.OnSelection)); + FGrid.SelectionType := stRow; + FGrid.InternalDrawGrid.Row := 2; + AssertEquals('A row is selected before clearing',1,Length(FGrid.SelectedRows)); + FGrid.ClearSelection(True); + AssertEquals('The retained native cursor is not a selected row',0,Length(FGrid.SelectedRows)); +end; + +initialization + RegisterTest(TVisualGridSelectionTests); + +end. diff --git a/src/tests/experimental/framework/UVisualGrid.Viewport.Checks.pas b/src/tests/experimental/framework/UVisualGrid.Viewport.Checks.pas new file mode 100644 index 000000000..0f56d6049 --- /dev/null +++ b/src/tests/experimental/framework/UVisualGrid.Viewport.Checks.pas @@ -0,0 +1,263 @@ +unit UVisualGrid.Viewport.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see LICENSE. + The oracle is the native CellRect, not a second copy of the capacity formula. } + +{$IFDEF FPC}{$mode delphi}{$H+}{$ENDIF} + +interface + +uses Classes; + +type + TGridViewportScenario = (vsUniformRows, vsVariableRows, vsDpiScaling, + vsHorizontalScrollbar, vsUnfetchedRows, vsAutomaticResize); + +procedure CheckVisualGridViewport(AScenario: TGridViewportScenario; ALog: TStrings = nil); +procedure CheckVisualGridShowAllCompatibility(ALog: TStrings = nil); +procedure CheckVisualGridDeferredParenting(ALog: TStrings = nil); + +implementation + +uses SysUtils, Types, Math, Variants, Forms, Controls, StdCtrls, Grids, + Generics.Collections, UCommon.Data, UVisualGrid; + +type + TViewportGrid = class(TVisualGrid) + public + function NativeGrid: {$IFDEF FPC}Grids.{$ELSE}UVisualGrid.{$ENDIF}TDrawGrid; + function Capacity: Integer; + procedure ApplyLayout; + end; + + TShowAllSource = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const ABindingName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + public + Count: Integer; + end; + +function TShowAllSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id', True)); +end; + +function TShowAllSource.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const ABindingName: String): Variant; +begin + Result := AItem; +end; + +procedure TShowAllSource.FetchAll(const AContainer: TList<Integer>); +var I: Integer; +begin + for I := 0 to Count - 1 do AContainer.Add(I); +end; + +function TViewportGrid.NativeGrid: {$IFDEF FPC}Grids.{$ELSE}UVisualGrid.{$ENDIF}TDrawGrid; +begin + Result := FDrawGrid; +end; + +function TViewportGrid.Capacity: Integer; +begin + Result := ClientRowCount; +end; + +procedure TViewportGrid.ApplyLayout; +begin + LayoutChanged; +end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure CheckVisualGridShowAllCompatibility(ALog: TStrings); +var LForm: TForm; LGrid: TVisualGrid; LSource: TShowAllSource; +begin + LForm := TForm.CreateNew(nil); + try + LForm.SetBounds(80, 90, 640, 480); + LSource := TShowAllSource.Create(LForm); + LSource.Count := 240; + LGrid := TVisualGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.FetchDataInThread := False; + LGrid.AddColumn('Id').Binding := 'Id'; + LForm.Show; + Application.ProcessMessages; + // This existing API must not turn an unknown total into a finite page. + LGrid.ShowAllData := True; + LGrid.DataSource := LSource; + Application.ProcessMessages; + Require(LGrid.RowCount = 240, 'ShowAllData before source assignment did not load all 240 records'); + LSource.Count := 370; + LGrid.RefreshGrid; + Application.ProcessMessages; + Require(LGrid.RowCount = 370, 'ShowAllData truncated growth to the previous known total'); + if Assigned(ALog) then ALog.Add('PASS legacy ShowAllData unknown total and growing complete result'); + finally + LForm.Free; + end; +end; + +procedure CheckVisualGridDeferredParenting(ALog: TStrings); +var LForm: TForm; LGrid: TViewportGrid; LSource: TShowAllSource; +begin + LForm := TForm.CreateNew(nil); + try + LForm.SetBounds(80, 90, 640, 480); + LSource := TShowAllSource.Create(LForm); + LSource.Count := 3; + LGrid := TViewportGrid.Create(LForm); + LGrid.FetchDataInThread := False; + LGrid.CanSearch := True; + LGrid.SearchMode := smMulti; + with LGrid.AddColumn('Id') do begin + Binding := 'Id'; + Width := 180; + Filters := SORTABLE_NUMERIC_FILTER; + end; + // Component ownership is deliberately not window parenting. The public + // data API must retain data/column intent without asking VCL for a HWND. + LGrid.DataSource := LSource; + Require(not Assigned(LGrid.Parent), 'Deferred-parent fixture acquired a parent before source assignment'); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LForm.Show; + Application.ProcessMessages; + LGrid.ApplyLayout; + Application.ProcessMessages; + Require((LGrid.RowCount = 3) and (LGrid.ColCount = 1), + 'Docking did not retain the source data and columns assigned before parenting'); + Require(LGrid.NativeGrid.HandleAllocated and + (LGrid.NativeGrid.RowCount = 4) and (LGrid.NativeGrid.ClientWidth > 0), + 'Docking did not replay native row/column geometry after its parent existed'); + Require(LGrid.Columns[0].Width > 0, 'Deferred column geometry was lost'); + if Assigned(ALog) then ALog.Add('PASS synchronous data assignment before window parenting and deferred native layout'); + finally + LForm.Free; + end; +end; + +procedure CheckVisualGridViewport(AScenario: TGridViewportScenario; ALog: TStrings); +var + LForm: TForm; + LGrid: TViewportGrid; + LNative: {$IFDEF FPC}Grids.{$ELSE}UVisualGrid.{$ENDIF}TDrawGrid; + LPpi, LPass, LLastPass, LRow, LFixed, LBottom, LDelta, LExpected, LActual: Integer; + LLabel: String; + LRect: TRect; + LRowRects: array[0..127] of TRect; + + function FullyVisibleRows: Integer; + var I: Integer; R: TRect; + begin + Result := 0; + for I := LFixed to High(LRowRects) do begin + R := LRowRects[I]; + if (R.Bottom > LNative.ClientHeight) then Break; + if (R.Bottom > R.Top) and (R.Top >= 0) then Inc(Result); + end; + Result := Max(1,Result); + end; + +begin + LLastPass := 0; + if AScenario in [vsDpiScaling,vsUniformRows] then LLastPass := 2; + for LPass := 0 to LLastPass do begin + LPpi := 96; + if AScenario = vsDpiScaling then Inc(LPpi,48*LPass); + LForm := TForm.CreateNew(nil); + try + LForm.SetBounds(80,90,720,560); + LGrid := TViewportGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.SetBounds(0,0,640,480); + LGrid.AddColumn('Id').Binding := 'Id'; + LForm.Show; + Application.ProcessMessages; + if LPpi <> 96 then begin + {$IFDEF FPC}LForm.ScaleBy(LPpi,96);{$ELSE}LForm.ScaleForPPI(LPpi);{$ENDIF} + Application.ProcessMessages; + end; + LNative := LGrid.NativeGrid; + LNative.Align := alNone; + LNative.SetBounds(0,0,500,900); + LNative.ScrollBars := ssNone; + LNative.RowCount := 128; + if AScenario = vsUniformRows then LNative.GridLineWidth := 1+LPass; + LNative.FixedRows := 1; + LNative.TopRow := LNative.FixedRows; + if AScenario = vsVariableRows then LNative.FixedRows := 2; + LFixed := LNative.FixedRows; + for LRow := 0 to LNative.RowCount - 1 do + LNative.RowHeights[LRow] := LNative.DefaultRowHeight; + if AScenario = vsVariableRows then begin + LNative.RowHeights[0] := 37; + LNative.RowHeights[1] := 19; + LNative.RowHeights[LFixed] := 31; + LNative.RowHeights[LFixed+1] := 47; + LNative.RowHeights[LFixed+3] := 13; + end; + if AScenario = vsHorizontalScrollbar then begin + LNative.ColWidths[0] := 1400; + LNative.ScrollBars := ssHorizontal; + Application.ProcessMessages; + Require(LNative.ClientHeight < LNative.Height, + 'Horizontal scrollbar fixture did not reduce the native client viewport'); + end; + + // Measure the fifth complete data row at a tall native viewport first. + // Then test its exact bottom and both adjacent pixel boundaries. + // VCL CellRect clips a partially visible last cell, so keep the original + // uncut native rectangles as the oracle when shrinking the viewport. + for LRow := 0 to High(LRowRects) do LRowRects[LRow] := LNative.CellRect(0,LRow); + LBottom := LRowRects[LFixed+4].Bottom; + for LDelta := -1 to 1 do begin + LNative.ClientHeight := LBottom + LDelta; + Application.ProcessMessages; + LExpected := FullyVisibleRows; + LActual := LGrid.Capacity; + LLabel := Format('scenario=%d PPI=%d delta=%d client=%d row=%d line=%d', + [Ord(AScenario),LPpi,LDelta,LNative.ClientHeight, + LNative.DefaultRowHeight,LNative.GridLineWidth]); + Require(LActual = LExpected,Format('%s: expected %d complete rows, got %d', + [LLabel,LExpected,LActual])); + Require(LExpected = 5 + Ord(LDelta < 0) * -1, + LLabel + ': native boundary oracle did not describe 4/5/5 rows'); + LRect := LRowRects[LFixed+LActual-1]; + Require(LRect.Bottom <= LNative.ClientHeight,LLabel + ': last admitted row is clipped'); + if AScenario = vsUnfetchedRows then begin + LNative.RowCount := LFixed+1; + Require(LGrid.Capacity = LExpected,LLabel + ': capacity depends on fetched row count'); + LNative.RowCount := 128; + end; + if ALog <> nil then ALog.Add('PASS viewport ' + LLabel); + end; + + if AScenario = vsAutomaticResize then begin + LNative.Align := alClient; + LGrid.AutoPageSize := True; + for LDelta := -1 to 1 do begin + LGrid.Height := 300 + 37 * LDelta; + Application.ProcessMessages; + LGrid.ApplyLayout; + Require(LGrid.PageSize = FullyVisibleRows, + Format('AutoPageSize after resize: page=%d native=%d capacity=%d rows=%d client=%d', + [LGrid.PageSize,FullyVisibleRows,LGrid.Capacity,LNative.RowCount,LNative.ClientHeight])); + if ALog <> nil then ALog.Add('PASS automatic resized page ' + IntToStr(LGrid.PageSize)); + end; + end; + finally + LForm.Free; + end; + end; +end; + +end. diff --git a/src/tests/experimental/framework/UVisualGrid.Worker.Checks.pas b/src/tests/experimental/framework/UVisualGrid.Worker.Checks.pas new file mode 100644 index 000000000..c23926a20 --- /dev/null +++ b/src/tests/experimental/framework/UVisualGrid.Worker.Checks.pas @@ -0,0 +1,592 @@ +unit UVisualGrid.Worker.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Native worker lifecycle checks use generated rows and an owned test process. + The dedicated test-only constructor hook is restored on every exit path. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type + TGridActiveCancellationScenario = (acsHeldProvider, acsQuietAdmission, acsCancelledError); + +procedure CheckVisualGridEarlyWorkerCancellation; +procedure CheckVisualGridEarlyActiveCancellation; +procedure CheckVisualGridActiveCancellation(AScenario: TGridActiveCancellationScenario); + +implementation + +uses Classes, SysUtils, Forms, Controls, StdCtrls, ExtCtrls, Variants, SyncObjs, Generics.Collections, + UCommon, UCommon.UI, UCommon.Data, UVisualGrid + {$IFDEF MSWINDOWS}, Windows{$ENDIF}; + +{$IF defined(MSWINDOWS) and defined(VISUALGRID_TEST_WORKER_START)} +type + TWorkerSource = class(TCustomDataSource<Integer>) + public + Calls, LastPage: LongInt; + function GetColumns: TDataColumns; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const AName: String): Variant; override; + procedure FetchAll(const AItems: TList<Integer>); override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ATable: TDataTable): TPageFetchResult; override; + end; + + TWorkerCheck = class(TComponent) + private + FForm: TForm; + FGrids: array[0..1] of TVisualGrid; + FSources: array[0..1] of TWorkerSource; + FCompletions: array[0..1] of Integer; + FCancelNextWorker: Boolean; + FForcedCancellations: Integer; + FCancelNextThroughApi: Boolean; + FApiPreStartCalls: Integer; + procedure BeforeWorkerStart(Sender: TObject); + procedure BeforeActiveCancelStart(Sender: TObject); + procedure Completed(Sender: TObject); + procedure DisableTimers(AOwner: TComponent); + procedure Pump; + procedure AwaitCompletion(AIndex, AExpected: Integer); + procedure CheckResume; + procedure CheckClose; + public + constructor Create; reintroduce; + destructor Destroy; override; + procedure Run; + procedure CheckActiveCancelBeforeStart; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +function TWorkerSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id',True)); +end; + +function TWorkerSource.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const AName: String): Variant; +begin + Result := AItem; +end; + +procedure TWorkerSource.FetchAll(const AItems: TList<Integer>); +var I: Integer; +begin + for I := 0 to 99 do AItems.Add(I); +end; + +function TWorkerSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ATable: TDataTable): TPageFetchResult; +begin + LastPage := AParams.PageIndex; + InterlockedIncrement(Calls); + Result := inherited FetchPage(AParams,ATable); +end; + +constructor TWorkerCheck.Create; +var I: Integer; +begin + inherited Create(nil); + FForm := TForm.CreateNew(nil); + FForm.SetBounds(40,50,1000,500); + for I := 0 to High(FGrids) do begin + FSources[I] := TWorkerSource.Create(Self); + FGrids[I] := TVisualGrid.Create(nil); + FGrids[I].Parent := FForm; + FGrids[I].SetBounds(8+I*480,8,470,420); + FGrids[I].FetchDataInThread := False; + FGrids[I].PageSize := 10; + FGrids[I].CanSearch := False; + FGrids[I].AddColumn('Id').Binding := 'Id'; + FGrids[I].OnFinishedUpdating := Completed; + FGrids[I].DataSource := FSources[I]; + end; + FForm.Show; + Pump; +end; + +destructor TWorkerCheck.Destroy; +var I: Integer; +begin + for I := 0 to High(FGrids) do FreeAndNil(FGrids[I]); + FForm.Free; + inherited Destroy; +end; + +procedure TWorkerCheck.BeforeWorkerStart(Sender: TObject); +begin + if not FCancelNextWorker then Exit; + Require(Sender is TThread,'The constructor hook did not receive its actual worker'); + FCancelNextWorker := False; + Inc(FForcedCancellations); + TThread(Sender).Terminate; +end; + +procedure TWorkerCheck.BeforeActiveCancelStart(Sender: TObject); +begin + if not FCancelNextThroughApi then + Exit; + FCancelNextThroughApi := False; + Inc(FApiPreStartCalls); + Require(Sender is TThread, 'The API hook did not receive its actual worker'); + {$IFNDEF VISUALGRID_ACTIVE_CANCEL_BASELINE} + FGrids[0].CancelActiveFetch; + {$ENDIF} + // Constructor-time cancellation must preserve the next ordinary request; + // no suspension, scheduler priority or message dispatch is involved. + FGrids[0].PageIndex := 6; + Require(not FGrids[0].RefreshSuspended, 'Early API cancellation changed suspension'); +end; + +procedure TWorkerCheck.Completed(Sender: TObject); +var I: Integer; +begin + for I := 0 to High(FGrids) do + if Sender = FGrids[I] then begin + Inc(FCompletions[I]); + Exit; + end; + raise Exception.Create('A disposed grid receiver was invoked'); +end; + +procedure TWorkerCheck.DisableTimers(AOwner: TComponent); +var I: Integer; +begin + for I := 0 to AOwner.ComponentCount-1 do begin + if AOwner.Components[I] is TTimer then TTimer(AOwner.Components[I]).Enabled := False; + DisableTimers(AOwner.Components[I]); + end; +end; + +procedure TWorkerCheck.Pump; +var I: Integer; +begin + for I := 0 to High(FGrids) do + if Assigned(FGrids[I]) then DisableTimers(FGrids[I]); + CheckSynchronize(0); + Application.ProcessMessages; + Sleep(1); +end; + +procedure TWorkerCheck.AwaitCompletion(AIndex, AExpected: Integer); +var LUntil: UInt64; +begin + LUntil := GetTickCount64+2000; + repeat + Pump; + if not FGrids[AIndex].IsUpdating and (FCompletions[AIndex] = AExpected) then Exit; + until GetTickCount64 >= LUntil; + raise Exception.Create(Format('Early cancellation stranded grid: busy=%s calls=%d completions=%d expected=%d', + [BoolToStr(FGrids[AIndex].IsUpdating,True),FSources[AIndex].Calls,FCompletions[AIndex],AExpected])); +end; + +procedure TWorkerCheck.CheckResume; +var LPrevious: TNotifyEvent; LCalls,LCompleted,LForced: Integer; +begin + LCalls := FSources[0].Calls; + LCompleted := FCompletions[0]; + LForced := FForcedCancellations; + FGrids[0].FetchDataInThread := True; + LPrevious := VisualGridBeforeWorkerStart; + VisualGridBeforeWorkerStart := BeforeWorkerStart; + FCancelNextWorker := True; + try + FGrids[0].RefreshGrid; + Require(FForcedCancellations = LForced+1,'The constructor did not force the pre-start cancellation'); + FGrids[0].RefreshSuspended := True; + FGrids[0].PageIndex := 4; + FGrids[0].RefreshSuspended := False; + Require(FSources[0].Calls = LCalls,'The pre-start-cancelled worker entered its provider'); + finally + FCancelNextWorker := False; + VisualGridBeforeWorkerStart := LPrevious; + end; + AwaitCompletion(0,LCompleted+1); + Require((FSources[0].Calls = LCalls+1) and (FSources[0].LastPage = 4) and + (FGrids[0].PageIndex = 4) and (FGrids[0].RowCount = 10), + 'Early cancellation did not publish exactly one latest admitted page'); + Pump; + Require((FSources[0].Calls = LCalls+1) and (FCompletions[0] = LCompleted+1), + 'Retiring an early-cancelled worker published an extra query or completion'); +end; + +procedure TWorkerCheck.CheckClose; +var LPrevious: TNotifyEvent; LCalls,LSecondCalls,LSecondCompleted,LForced: Integer; +begin + LCalls := FSources[0].Calls; + LSecondCalls := FSources[1].Calls; + LSecondCompleted := FCompletions[1]; + LForced := FForcedCancellations; + FGrids[1].FetchDataInThread := True; + LPrevious := VisualGridBeforeWorkerStart; + VisualGridBeforeWorkerStart := BeforeWorkerStart; + try + FGrids[1].RefreshGrid; + FCancelNextWorker := True; + FGrids[0].RefreshGrid; + Require(FForcedCancellations = LForced+1,'Receiver disposal did not force the pre-start cancellation'); + FGrids[0].RefreshSuspended := True; + // The independent provider may finish; its UI completion remains queued. + Require((FSources[0].Calls = LCalls) and (FCompletions[1] = LSecondCompleted), + 'The cancelled provider ran or the independent receiver was pumped early'); + FreeAndNil(FGrids[0]); + finally + FCancelNextWorker := False; + VisualGridBeforeWorkerStart := LPrevious; + end; + AwaitCompletion(1,LSecondCompleted+1); + Require((FSources[0].Calls = LCalls) and (FSources[1].Calls = LSecondCalls+1), + 'Early-cancelled disposal fetched stale data or lost another receiver'); + Pump; +end; + +procedure TWorkerCheck.CheckActiveCancelBeforeStart; +var + LPrevious: TNotifyEvent; + LCalls, LCompleted: Integer; +begin + LCalls := FSources[0].Calls; + LCompleted := FCompletions[0]; + FGrids[0].FetchDataInThread := True; + LPrevious := VisualGridBeforeWorkerStart; + VisualGridBeforeWorkerStart := BeforeActiveCancelStart; + FCancelNextThroughApi := True; + try + FGrids[0].RefreshGrid; + Require(FApiPreStartCalls = 1, 'The public API was not invoked before Start'); + Require(FGrids[0].IsUpdating and not FGrids[0].RefreshSuspended, + 'Early cancellation synchronously retired or suspended its worker'); + finally + FCancelNextThroughApi := False; + VisualGridBeforeWorkerStart := LPrevious; + end; + AwaitCompletion(0, LCompleted + 1); + Require((FSources[0].Calls = LCalls + 1) and (FSources[0].LastPage = 6) and + (FGrids[0].PageIndex = 6) and (FGrids[0].RowCount = 10) and + (Integer(TDataRowData(FGrids[0].Rows[0])['Id']) = 60), + 'Pre-start API cancellation entered its old provider or lost the latest page'); + Pump; + Require((FSources[0].Calls = LCalls + 1) and + (FCompletions[0] = LCompleted + 1), 'Pre-start cancellation published extra data'); +end; + +procedure TWorkerCheck.Run; +begin + CheckResume; + CheckClose; +end; +{$ENDIF} + +procedure CheckVisualGridEarlyWorkerCancellation; +{$IF defined(MSWINDOWS) and defined(VISUALGRID_TEST_WORKER_START)} +var LCheck: TWorkerCheck; +begin + LCheck := TWorkerCheck.Create; + try + LCheck.Run; + finally + LCheck.Free; + end; +end; +{$ELSE} +begin + raise ENotSupportedException.Create('This native worker regression requires Windows and VISUALGRID_TEST_WORKER_START'); +end; +{$ENDIF} + +procedure CheckVisualGridEarlyActiveCancellation; +{$IF defined(MSWINDOWS) and defined(VISUALGRID_TEST_WORKER_START)} +var + LCheck: TWorkerCheck; +begin + LCheck := TWorkerCheck.Create; + try + LCheck.CheckActiveCancelBeforeStart; + finally + LCheck.Free; + end; +end; +{$ELSE} +begin + raise ENotSupportedException.Create('The direct pre-start API check requires Windows and VISUALGRID_TEST_WORKER_START'); +end; +{$ENDIF} + +type + TActiveCancelSource = class(TCustomDataSource<Integer>) + private + FReadPrefix: Integer; + protected + function GetColumns: TDataColumns; override; + function GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const AName: String): Variant; override; + procedure FetchAll(const AItems: TList<Integer>); override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ATable: TDataTable): TPageFetchResult; override; + public + Prefix, Calls, HoldNext: LongInt; + Cooperative, RaiseObsoleteError: Boolean; + Entered, ReleaseRead, CancelObserved: TEvent; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + end; + + TActiveCancelObserver = class(TComponent) + public + Grid: TVisualGrid; + Completions: Integer; + PublishedOldQuery: Boolean; + procedure Completed(Sender: TObject); + end; + +procedure RequireActiveCancellation(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create('Active fetch cancellation: ' + AMessage); +end; + +procedure PumpActiveCancellation(AMilliseconds: Cardinal); +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + CheckSynchronize(0); + Application.ProcessMessages; + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +constructor TActiveCancelSource.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, True, ''); + CancelObserved := TEvent.Create(nil, True, False, ''); +end; + +destructor TActiveCancelSource.Destroy; +begin + CancelObserved.Free; + ReleaseRead.Free; + Entered.Free; + inherited Destroy; +end; + +function TActiveCancelSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id', True)); +end; + +function TActiveCancelSource.GetItemField({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AItem: Integer; const AName: String): Variant; +begin + Result := AItem; +end; + +procedure TActiveCancelSource.FetchAll(const AItems: TList<Integer>); +var + I: Integer; +begin + for I := 0 to 59 do + AItems.Add(FReadPrefix + I); +end; + +function TActiveCancelSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; var ATable: TDataTable): TPageFetchResult; +begin + FReadPrefix := InterlockedCompareExchange(Prefix, 0, 0); + InterlockedIncrement(Calls); + if InterlockedExchange(HoldNext, 0) <> 0 then + begin + Entered.SetEvent; + if Cooperative then + begin + while ReleaseRead.WaitFor(10) <> wrSignaled do + if TThread.CheckTerminated then + begin + CancelObserved.SetEvent; + Abort; + end; + end + else + RequireActiveCancellation(ReleaseRead.WaitFor(5000) = wrSignaled, + 'held provider release timed out'); + if RaiseObsoleteError then + raise Exception.Create('Obsolete cancelled provider sentinel error'); + end; + Result := inherited FetchPage(AParams, ATable); +end; + +procedure TActiveCancelObserver.Completed(Sender: TObject); +begin + Inc(Completions); + if (Grid.RowCount > 0) and + (Integer(TDataRowData(Grid.Rows[0])['Id']) >= 1000) and + (Integer(TDataRowData(Grid.Rows[0])['Id']) < 2000) then + PublishedOldQuery := True; +end; + +procedure CheckVisualGridActiveCancellation(AScenario: TGridActiveCancellationScenario); +var + LForm: TForm; + LGrid: TVisualGrid; + LSource: TActiveCancelSource; + LObserver: TActiveCancelObserver; + LCalls: Integer; + LUntil, LStarted, LNewestInput: UInt64; + LNotify: TDateTime; + + procedure CancelActive; + begin + {$IFNDEF VISUALGRID_ACTIVE_CANCEL_BASELINE} + LGrid.CancelActiveFetch; + {$ENDIF} + end; + + procedure RequireOriginalRows(const AContext: String); + begin + RequireActiveCancellation((LGrid.RowCount = 10) and + (Integer(TDataRowData(LGrid.Rows[0])['Id']) = 0), + AContext + ': accepted rows were lost or replaced by the cancelled query'); + end; + +begin + LForm := TForm.CreateNew(nil); + LSource := nil; + try + LForm.SetBounds(40, 50, 640, 440); + LSource := TActiveCancelSource.Create(LForm); + LGrid := TVisualGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.FetchDataInThread := False; + LGrid.PageMode := vpmManual; + LGrid.ManualPageSize := 10; + LGrid.CanSearch := False; + LGrid.AddColumn('Id').Binding := 'Id'; + LGrid.DataSource := LSource; + LObserver := TActiveCancelObserver.Create(LForm); + LObserver.Grid := LGrid; + LGrid.OnFinishedUpdating := LObserver.Completed; + LForm.Show; + PumpActiveCancellation(50); + RequireOriginalRows('initial page'); + LCalls := LSource.Calls; + LNotify := LGrid.RefreshThrottle.LastClientNotify; + CancelActive; + RequireActiveCancellation((LSource.Calls = LCalls) and not LGrid.IsUpdating and + not LGrid.RefreshSuspended and not LGrid.RefreshThrottle.Waiting and + (LGrid.RefreshThrottle.LastClientNotify = LNotify), + 'an idle cancellation changed refresh state'); + + LSource.Cooperative := AScenario = acsQuietAdmission; + LSource.RaiseObsoleteError := AScenario = acsCancelledError; + if AScenario = acsQuietAdmission then + begin + LGrid.RefreshThrottle.Interval := TTimeSpan.FromSeconds(5); + LGrid.RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; + end; + InterlockedExchange(LSource.Prefix, 1000); + LSource.ReleaseRead.ResetEvent; + InterlockedExchange(LSource.HoldNext, 1); + LGrid.FetchDataInThread := True; + LGrid.RefreshGrid; + RequireActiveCancellation(LSource.Entered.WaitFor(1000) = wrSignaled, + 'the real worker never entered its held provider'); + if AScenario = acsHeldProvider then + LGrid.ShowLoading('Manual owner', 'Retain this card', nil, True); + LNotify := LGrid.RefreshThrottle.LastClientNotify; + LStarted := GetTickCount64; + CancelActive; + RequireActiveCancellation(GetTickCount64 - LStarted < 250, + 'CancelActiveFetch waited for the provider'); + RequireActiveCancellation(LGrid.IsUpdating and not LGrid.RefreshSuspended and + (LGrid.RefreshThrottle.LastClientNotify = LNotify), + 'cancellation retired synchronously or notified/changed refresh policy'); + RequireActiveCancellation(LGrid.RefreshThrottle.Waiting = (AScenario = acsQuietAdmission), + 'cancellation changed the existing Waiting state'); + RequireOriginalRows('while cancellation is pending'); + if AScenario = acsHeldProvider then + RequireActiveCancellation(LGrid.LoadingVisible and + (TLabel(LGrid.FindComponent('LoadingStatus')).Caption = 'Manual owner'), + 'cancellation changed the caller-owned loading card'); + + if AScenario = acsCancelledError then + begin + // Inspect the cancelled error before scheduling a replacement which could + // otherwise hide a leaked error card immediately in BeginLoading. + LSource.ReleaseRead.SetEvent; + LUntil := GetTickCount64 + 2000; + while LGrid.IsUpdating and (GetTickCount64 < LUntil) do + PumpActiveCancellation(10); + RequireActiveCancellation(not LGrid.IsUpdating, 'cancelled failing worker did not retire'); + RequireOriginalRows('after cancelled provider error'); + RequireActiveCancellation((LObserver.Completions = 0) and not LObserver.PublishedOldQuery, + 'cancelled failing worker published a completion or old rows'); + RequireActiveCancellation(not TPanel(LGrid.FindComponent('LoadingOverlay')).Visible and + (Pos('Obsolete cancelled provider sentinel error', + TLabel(LGrid.FindComponent('LoadingStatus')).Caption) = 0), + 'cancelled ordinary exception leaked its obsolete error card'); + end; + + InterlockedExchange(LSource.Prefix, 2000); + LNewestInput := GetTickCount64; + LGrid.PageIndex := 2; + if AScenario in [acsHeldProvider, acsCancelledError] then + LSource.ReleaseRead.SetEvent + else + begin + RequireActiveCancellation(LSource.CancelObserved.WaitFor(1000) = wrSignaled, + 'the cooperative provider did not receive cancellation'); + LUntil := GetTickCount64 + 2000; + while LGrid.IsUpdating and (GetTickCount64 < LUntil) do + PumpActiveCancellation(10); + RequireActiveCancellation(not LGrid.IsUpdating and LGrid.RefreshThrottle.Waiting, + 'cancelled worker did not retire independently of its pending quiet interval'); + RequireOriginalRows('after cooperative retirement'); + RequireActiveCancellation((LObserver.Completions = 0) and + (LSource.Calls = LCalls + 1), 'cancelled query published or latest input fetched early'); + LNotify := LGrid.RefreshThrottle.LastClientNotify; + CancelActive; + RequireActiveCancellation((LGrid.RefreshThrottle.LastClientNotify = LNotify) and + LGrid.RefreshThrottle.Waiting, 'idle cancellation reset or ended the pending wait'); + while GetTickCount64 - LNewestInput < 4700 do + begin + PumpActiveCancellation(10); + RequireActiveCancellation((LObserver.Completions = 0) and + (LSource.Calls = LCalls + 1), 'newest query bypassed five quiet seconds'); + end; + end; + LUntil := GetTickCount64 + 2500; + while ((LObserver.Completions <> 1) or LGrid.IsUpdating or + LGrid.RefreshThrottle.Waiting) and (GetTickCount64 < LUntil) do + PumpActiveCancellation(10); + RequireActiveCancellation((LObserver.Completions = 1) and not LGrid.IsUpdating and + not LGrid.RefreshThrottle.Waiting and not LObserver.PublishedOldQuery, + 'cancelled result published or latest query did not complete exactly once'); + RequireActiveCancellation((LSource.Calls = LCalls + 2) and (LGrid.PageIndex = 2) and + (LGrid.RowCount = 10) and + (Integer(TDataRowData(LGrid.Rows[0])['Id']) = 2020), + 'latest pending page/source configuration was lost'); + if AScenario = acsQuietAdmission then + RequireActiveCancellation(GetTickCount64 - LNewestInput >= 4980, + 'latest publication was earlier than the five-second quiet contract'); + if AScenario = acsHeldProvider then + begin + RequireActiveCancellation(LGrid.LoadingVisible and + (TLabel(LGrid.FindComponent('LoadingStatus')).Caption = 'Manual owner'), + 'worker retirement removed the manual card'); + LGrid.HideLoading; + RequireActiveCancellation(not TPanel(LGrid.FindComponent('LoadingOverlay')).Visible, + 'completed cancellation left an obsolete automatic/error card'); + end; + PumpActiveCancellation(50); + RequireActiveCancellation((LSource.Calls = LCalls + 2) and (LObserver.Completions = 1), + 'retiring the cancelled worker caused another query/publication'); + finally + if Assigned(LSource) then + LSource.ReleaseRead.SetEvent; + LForm.Free; + end; +end; + +end. diff --git a/src/tests/experimental/framework/UWizard.TestScreens.pas b/src/tests/experimental/framework/UWizard.TestScreens.pas new file mode 100644 index 000000000..710e50def --- /dev/null +++ b/src/tests/experimental/framework/UWizard.TestScreens.pas @@ -0,0 +1,38 @@ +unit UWizard.TestScreens; + +{$mode delphi} +{$H+} + +interface + +uses + Classes, SysUtils, Variants, UWizard; + +type + { Keep reference-model screens in another unit: FPC emits their generic + specialization separately from the wizard manager's specialization. } + TReferenceWizardScreen = class(TWizardForm<TGenericWizardBag>) + public + InitializedName : String; + InitializeCount : Integer; + constructor Create(AOwner : TComponent); override; + procedure Initialize; override; + end; + + TReferenceWizardFirstScreen = class(TReferenceWizardScreen); + TReferenceWizardSecondScreen = class(TReferenceWizardScreen); + +implementation + +constructor TReferenceWizardScreen.Create(AOwner : TComponent); +begin + inherited CreateNew(AOwner); +end; + +procedure TReferenceWizardScreen.Initialize; +begin + InitializedName := VarToStr(Model['Name']); + inc(InitializeCount); +end; + +end. diff --git a/src/tests/experimental/framework/UWizard.Tests.pas b/src/tests/experimental/framework/UWizard.Tests.pas new file mode 100644 index 000000000..d59b85cb7 --- /dev/null +++ b/src/tests/experimental/framework/UWizard.Tests.pas @@ -0,0 +1,865 @@ +unit UWizard.Tests; + +{$mode delphi} +{$H+} +{$codepage UTF8} + +interface + +uses + Classes, SysUtils, Variants, Forms, Controls, fpcunit, testregistry, UCommon, UWizard, UWizard.TestScreens; + +type + { Forms created without resources keep these tests independent of the designer. } + TProbeWizardScreen = class(TWizardForm<Integer>) + protected + procedure SetParent(NewParent : TWinControl); override; + public + InitializedModel : Integer; + InitializeCount : Integer; + NextCount : Integer; + PreviousCount : Integer; + AllowNext : boolean; + FailureMessage : String; + InjectedScreen : TComponentClass; + NextAction : TNotifyEvent; + class var DestroyCount : Integer; + class var VisibleDetachCount : Integer; + class var HiddenDetachCount : Integer; + constructor Create(AOwner : TComponent); override; + destructor Destroy; override; + procedure Initialize; override; + procedure OnNext; override; + procedure OnPrevious; override; + function Validate(out message : String) : boolean; override; + end; + + TFirstWizardScreen = class(TProbeWizardScreen); + TSecondWizardScreen = class(TProbeWizardScreen); + TThirdWizardScreen = class(TProbeWizardScreen); + TFailingWizardScreen = class(TProbeWizardScreen) + public + procedure Initialize; override; + end; + + TProbeWizard = class(TWizard<Integer>) + private + function GetTestHost : TWizardHostForm; + protected + procedure ShowHost; override; + procedure ShowError(const message : String); override; + function FinishRequested(out message : String) : boolean; override; + function CancelRequested(out message : String) : boolean; override; + public + Run : TNotifyEvent; + FinishAction : TNotifyEvent; + CancelAction : TNotifyEvent; + AllowFinish : boolean; + AllowCancel : boolean; + FailureMessage : String; + FinishCount : Integer; + CancelCount : Integer; + LastError : String; + constructor Create(const screens : array of TComponentClass); + property TestHost : TWizardHostForm read GetTestHost; + end; + + TReferenceProbeWizard = class(TActionWizard<TGenericWizardBag>) + private + function GetTestHost : TWizardHostForm; + protected + procedure ShowHost; override; + public + Run : TNotifyEvent; + property TestHost : TWizardHostForm read GetTestHost; + end; + + TWizardTests = class(TTestCase) + private + FWizard : TProbeWizard; + FReferenceWizard : TReferenceProbeWizard; + FFinishedCount : Integer; + procedure RunWizard(const screens : array of TComponentClass; action : TNotifyEvent); + procedure VerifyInitializationAndNavigation(Sender : TObject); + procedure VerifyValidation(Sender : TObject); + procedure VerifyDynamicBranch(Sender : TObject); + procedure VerifyFinishVeto(Sender : TObject); + procedure VerifyFinishException(Sender : TObject); + procedure VerifyCancelException(Sender : TObject); + procedure VerifyCancellation(Sender : TObject); + procedure VerifyCancellationRecheck(Sender : TObject); + procedure VerifyReentry(Sender : TObject); + procedure VerifyReplaceAndRemove(Sender : TObject); + procedure VerifyBacktracking(Sender : TObject); + procedure VerifyExplicitFinish(Sender : TObject); + procedure VerifyHostClose(Sender : TObject); + procedure RejectNestedNavigation(Sender : TObject); + procedure RejectNestedTerminal(Sender : TObject); + procedure DisableCancellation(Sender : TObject); + procedure RaiseCallbackError(Sender : TObject); + procedure CountFinished(Sender : TObject); + procedure VerifyReferenceModel(Sender : TObject); + procedure VerifyHiddenDetachment(Sender : TObject); + procedure VerifyNativeStringMessages(Sender : TObject); + protected + procedure TearDown; override; + published + procedure InitializesWithModelAndPreservesValueNavigation; + procedure ValidationRunsBeforeNextAndInjection; + procedure DynamicSuccessorIsRequiredAndCanBeRetried; + procedure RejectedFinishStaysActiveAndAcceptedFinishRunsOnce; + procedure FailedFinishCanBeRetried; + procedure FailedCancellationCanBeRetried; + procedure CancellationHonorsVetoAndTerminalState; + procedure CancellationRechecksPermissionAfterCallback; + procedure NavigationAndTerminalCallbacksRejectReentry; + procedure PathReplacementRemovalAndSnapshots; + procedure PreviousRestoresBranchBeforeReinjection; + procedure ExplicitFinishValidatesAndRejectsRemainingScreens; + procedure HostCloseUsesCancellationButFinishedCloseDoesNot; + procedure RemovedScreensRemainOwned; + procedure ActionWizardCaptionsUseConstructorArguments; + procedure InvalidScreenRequestsPreservePath; + procedure EmptyConstructionDoesNotLeak; + procedure UnstartedWizardCannotCompleteWithAnUnassignedModel; + procedure InitializationFailureReleasesHostAndRetainsScreenOwnership; + procedure ReferenceModelScreensAcrossUnitsNavigateAndFinish; + procedure NavigationAndHostCleanupHideScreensBeforeDetaching; + procedure NativeStringMessagesAndCaptionsPreserveText; + end; + +implementation + +constructor TProbeWizardScreen.Create(AOwner : TComponent); +begin + inherited CreateNew(AOwner); + AllowNext := true; +end; + +destructor TProbeWizardScreen.Destroy; +begin + inc(DestroyCount); + inherited Destroy; +end; + +procedure TProbeWizardScreen.SetParent(NewParent : TWinControl); +begin + if (Parent <> nil) and (NewParent = nil) then begin + if Visible then begin + inc(VisibleDetachCount); + // Prevent a failing regression from creating a visible desktop window. + Hide; + raise Exception.Create('Wizard screen detached before it was hidden'); + end; + inc(HiddenDetachCount); + end; + inherited SetParent(NewParent); +end; + +procedure TProbeWizardScreen.Initialize; +begin + inc(InitializeCount); + InitializedModel := Model; +end; + +procedure TProbeWizardScreen.OnNext; +begin + inc(NextCount); + Model := Model + 1; + if Assigned(NextAction) then NextAction(self); + if Assigned(InjectedScreen) then + UpdatePath(ptInject, [InjectedScreen]); +end; + +procedure TProbeWizardScreen.OnPrevious; +begin + inc(PreviousCount); + Model := Model + 1; +end; + +function TProbeWizardScreen.Validate(out message : String) : boolean; +begin + message := ''; + Result := AllowNext; + if NOT Result then begin + message := FailureMessage; + if message = '' then message := 'Choose a branch'; + end; +end; + +procedure TFailingWizardScreen.Initialize; +begin + inherited Initialize; + raise Exception.Create('Cannot initialize screen'); +end; + +constructor TProbeWizard.Create(const screens : array of TComponentClass); +begin + inherited Create(nil, screens); + AllowFinish := true; + AllowCancel := true; +end; + +function TProbeWizard.GetTestHost : TWizardHostForm; +begin + Result := Host; +end; + +procedure TProbeWizard.ShowHost; +begin + if Assigned(Run) then Run(self); +end; + +procedure TProbeWizard.ShowError(const message : String); +begin + LastError := message; +end; + +function TProbeWizard.FinishRequested(out message : String) : boolean; +begin + inc(FinishCount); + if Assigned(FinishAction) then FinishAction(self); + Result := AllowFinish; + message := ''; + if NOT Result then begin + message := FailureMessage; + if message = '' then message := 'Keep editing'; + end; +end; + +function TProbeWizard.CancelRequested(out message : String) : boolean; +begin + inc(CancelCount); + if Assigned(CancelAction) then CancelAction(self); + Result := AllowCancel; + message := ''; + if NOT Result then begin + message := FailureMessage; + if message = '' then message := 'Cannot cancel yet'; + end; +end; + +function TReferenceProbeWizard.GetTestHost : TWizardHostForm; +begin + Result := Host; +end; + +procedure TReferenceProbeWizard.ShowHost; +begin + if Assigned(Run) then Run(self); +end; + +procedure TWizardTests.TearDown; +begin + FreeAndNil(FWizard); + FreeAndNil(FReferenceWizard); + inherited TearDown; +end; + +procedure TWizardTests.RunWizard(const screens : array of TComponentClass; action : TNotifyEvent); +begin + FWizard := TProbeWizard.Create(screens); + FWizard.Run := action; + FWizard.Start(42); + AssertTrue('Host must be released after Start', FWizard.TestHost = nil); +end; + +procedure TWizardTests.InitializesWithModelAndPreservesValueNavigation; +begin + RunWizard([TFirstWizardScreen, TSecondWizardScreen], VerifyInitializationAndNavigation); +end; + +procedure TWizardTests.VerifyInitializationAndNavigation(Sender : TObject); +var + message : String; + first, second : TProbeWizardScreen; +begin + first := TProbeWizardScreen(FWizard.CurrentScreen); + AssertEquals(42, first.InitializedModel); + AssertEquals(1, first.InitializeCount); + AssertEquals(CT_WIZARD_DEFAULT_NEXT, FWizard.TestHost.NextText); + AssertTrue(FWizard.TestHost.HidePrevious); + AssertTrue(FWizard.TryNext(message)); + second := TProbeWizardScreen(FWizard.CurrentScreen); + AssertEquals(42, second.InitializedModel); + AssertEquals(1, second.InitializeCount); + AssertEquals(43, FWizard.Model); + AssertEquals(1, FWizard.CurrentScreenIndex); + AssertEquals(CT_WIZARD_DEFAULT_FINISH, FWizard.TestHost.NextText); + AssertFalse(FWizard.TestHost.HidePrevious); + FWizard.TestHost.FPreviousButton.Click; + AssertTrue(FWizard.CurrentScreen = first); + AssertEquals(44, FWizard.Model); + AssertEquals(1, second.PreviousCount); + AssertEquals(1, first.InitializeCount); +end; + +procedure TWizardTests.ValidationRunsBeforeNextAndInjection; +begin + RunWizard([TFirstWizardScreen], VerifyValidation); +end; + +procedure TWizardTests.VerifyValidation(Sender : TObject); +var + message : String; + first : TProbeWizardScreen; +begin + first := TProbeWizardScreen(FWizard.CurrentScreen); + first.Traits := [wstAssumeHasNext]; + first.InjectedScreen := TSecondWizardScreen; + first.AllowNext := false; + AssertFalse(FWizard.TryNext(message)); + AssertEquals('Choose a branch', message); + AssertEquals(0, first.NextCount); + AssertEquals(1, Length(FWizard.Screens)); + AssertEquals(42, FWizard.Model); + first.AllowNext := true; + AssertTrue(FWizard.TryNext(message)); + AssertTrue(FWizard.CurrentScreen is TSecondWizardScreen); + AssertEquals(1, first.NextCount); + AssertEquals(0, FWizard.FinishCount); + AssertEquals(43, TProbeWizardScreen(FWizard.CurrentScreen).InitializedModel); +end; + +procedure TWizardTests.DynamicSuccessorIsRequiredAndCanBeRetried; +begin + FWizard := TProbeWizard.Create([TFirstWizardScreen]); + // Traits are live metadata and must affect the very first displayed button. + TProbeWizardScreen(FWizard.Components[0]).Traits := [wstAssumeHasNext]; + FWizard.Run := VerifyDynamicBranch; + FWizard.Start(42); +end; + +procedure TWizardTests.VerifyDynamicBranch(Sender : TObject); +var + message : String; + first : TProbeWizardScreen; +begin + first := TProbeWizardScreen(FWizard.CurrentScreen); + AssertTrue(FWizard.HasNext); + AssertEquals(CT_WIZARD_DEFAULT_NEXT, FWizard.TestHost.NextText); + AssertFalse(FWizard.TryNext(message)); + AssertTrue(message <> ''); + AssertEquals(0, FWizard.CurrentScreenIndex); + AssertEquals(Ord(wsActive), Ord(FWizard.State)); + AssertFalse(FWizard.IsBusy); + AssertEquals(0, FWizard.FinishCount); + first.InjectedScreen := TSecondWizardScreen; + FWizard.TestHost.FNextButton.Click; + AssertTrue(FWizard.CurrentScreen is TSecondWizardScreen); + AssertEquals(2, first.NextCount); + FWizard.TestHost.FNextButton.Click; + AssertEquals(Ord(wsFinished), Ord(FWizard.State)); +end; + +procedure TWizardTests.RejectedFinishStaysActiveAndAcceptedFinishRunsOnce; +var + message : String; +begin + RunWizard([TFirstWizardScreen], VerifyFinishVeto); + AssertTrue(FWizard.Finish(message)); + AssertFalse(FWizard.TryNext(message)); + AssertFalse(FWizard.TryPrevious(message)); + AssertEquals(2, FWizard.FinishCount); +end; + +procedure TWizardTests.CountFinished(Sender : TObject); +begin + inc(FFinishedCount); +end; + +procedure TWizardTests.VerifyFinishVeto(Sender : TObject); +var + message : String; +begin + FWizard.FinishedEvent.Add(CountFinished); + FWizard.AllowFinish := false; + FWizard.TestHost.FNextButton.Click; + AssertEquals('Keep editing', FWizard.LastError); + AssertEquals(Ord(wsActive), Ord(FWizard.State)); + AssertEquals(0, FWizard.TestHost.ModalResult); + AssertEquals(0, FFinishedCount); + FWizard.AllowFinish := true; + FWizard.TestHost.FNextButton.Click; + AssertEquals(Ord(wsFinished), Ord(FWizard.State)); + AssertEquals(mrOK, FWizard.TestHost.ModalResult); + AssertTrue(FWizard.Finish(message)); + AssertFalse(FWizard.Cancel(message)); + AssertEquals(2, FWizard.FinishCount); + AssertEquals(0, FWizard.CancelCount); + AssertEquals(1, FFinishedCount); +end; + +procedure TWizardTests.RaiseCallbackError(Sender : TObject); +begin + raise Exception.Create('Retry completion'); +end; + +procedure TWizardTests.FailedFinishCanBeRetried; +begin + RunWizard([TFirstWizardScreen], VerifyFinishException); +end; + +procedure TWizardTests.VerifyFinishException(Sender : TObject); +var + message : String; + raised : boolean; +begin + FWizard.FinishAction := RaiseCallbackError; + raised := false; + try + FWizard.TryNext(message); + except + on E : Exception do begin + raised := true; + AssertEquals('Retry completion', E.Message); + end; + end; + AssertTrue(raised); + AssertFalse(FWizard.IsBusy); + AssertEquals(Ord(wsActive), Ord(FWizard.State)); + FWizard.FinishAction := nil; + AssertTrue(FWizard.TryNext(message)); + AssertEquals(2, FWizard.FinishCount); +end; + +procedure TWizardTests.FailedCancellationCanBeRetried; +var + message : String; +begin + RunWizard([TFirstWizardScreen], VerifyCancelException); + AssertTrue('Accepted cancellation remains idempotent after the host closes', FWizard.Cancel(message)); + AssertFalse(FWizard.TryNext(message)); + AssertFalse(FWizard.TryPrevious(message)); + AssertEquals(2, FWizard.CancelCount); +end; + +procedure TWizardTests.VerifyCancelException(Sender : TObject); +var + message : String; + raised : boolean; +begin + FWizard.CancelAction := RaiseCallbackError; + raised := false; + try + FWizard.Cancel(message); + except + on E : Exception do begin + raised := true; + AssertEquals('Retry completion', E.Message); + end; + end; + AssertTrue(raised); + AssertFalse(FWizard.IsBusy); + AssertEquals(Ord(wsActive), Ord(FWizard.State)); + FWizard.CancelAction := nil; + AssertTrue(FWizard.Cancel(message)); + AssertTrue(FWizard.Cancel(message)); + AssertEquals(2, FWizard.CancelCount); +end; + +procedure TWizardTests.CancellationHonorsVetoAndTerminalState; +begin + RunWizard([TFirstWizardScreen], VerifyCancellation); +end; + +procedure TWizardTests.VerifyCancellation(Sender : TObject); +var + message : String; +begin + FWizard.IsCancellable := false; + AssertFalse(FWizard.Cancel(message)); + AssertEquals(0, FWizard.CancelCount); + FWizard.IsCancellable := true; + FWizard.AllowCancel := false; + AssertFalse(FWizard.Cancel(message)); + AssertEquals('Cannot cancel yet', message); + AssertEquals(Ord(wsActive), Ord(FWizard.State)); + FWizard.AllowCancel := true; + AssertTrue(FWizard.Cancel(message)); + AssertTrue(FWizard.Cancel(message)); + AssertFalse(FWizard.Finish(message)); + AssertEquals(2, FWizard.CancelCount); + AssertEquals(0, FWizard.FinishCount); + AssertEquals(Ord(wsCancelled), Ord(FWizard.State)); +end; + +procedure TWizardTests.DisableCancellation(Sender : TObject); +begin + FWizard.IsCancellable := false; +end; + +procedure TWizardTests.CancellationRechecksPermissionAfterCallback; +begin + RunWizard([TFirstWizardScreen], VerifyCancellationRecheck); +end; + +procedure TWizardTests.VerifyCancellationRecheck(Sender : TObject); +var + message : String; +begin + FWizard.CancelAction := DisableCancellation; + AssertFalse(FWizard.Cancel(message)); + AssertEquals(Ord(wsActive), Ord(FWizard.State)); + AssertEquals(0, FWizard.TestHost.ModalResult); + FWizard.CancelAction := nil; + FWizard.IsCancellable := true; + AssertTrue(FWizard.Cancel(message)); +end; + +procedure TWizardTests.NavigationAndTerminalCallbacksRejectReentry; +begin + RunWizard([TFirstWizardScreen, TSecondWizardScreen], VerifyReentry); +end; + +procedure TWizardTests.RejectNestedNavigation(Sender : TObject); +var + message : String; +begin + AssertTrue(FWizard.IsBusy); + AssertFalse(FWizard.TryNext(message)); + AssertFalse(FWizard.TryPrevious(message)); + AssertFalse(FWizard.Finish(message)); + AssertFalse(FWizard.Cancel(message)); +end; + +procedure TWizardTests.RejectNestedTerminal(Sender : TObject); +var + message : String; +begin + AssertEquals(Ord(wsFinishing), Ord(FWizard.State)); + AssertFalse(FWizard.Finish(message)); + AssertFalse(FWizard.Cancel(message)); +end; + +procedure TWizardTests.VerifyReentry(Sender : TObject); +var + message : String; +begin + TProbeWizardScreen(FWizard.CurrentScreen).NextAction := RejectNestedNavigation; + AssertTrue(FWizard.TryNext(message)); + AssertEquals(1, FWizard.CurrentScreenIndex); + FWizard.FinishAction := RejectNestedTerminal; + AssertTrue(FWizard.TryNext(message)); + AssertEquals(1, FWizard.FinishCount); + AssertEquals(0, FWizard.CancelCount); +end; + +procedure TWizardTests.PathReplacementRemovalAndSnapshots; +begin + RunWizard([TFirstWizardScreen, TSecondWizardScreen], VerifyReplaceAndRemove); +end; + +procedure TWizardTests.VerifyReplaceAndRemove(Sender : TObject); +var + message : String; + path : TWizardScreenPath; +begin + path := FWizard.Screens; + path[0] := TThirdWizardScreen; + AssertTrue(FWizard.Screens[0] = TFirstWizardScreen); + FWizard.UpdatePath(ptInject, [TThirdWizardScreen]); + FWizard.UpdatePath(ptInject, [TThirdWizardScreen]); + AssertEquals(3, Length(FWizard.Screens)); + FWizard.UpdatePath(ptRemoveNext, [TThirdWizardScreen]); + AssertEquals(2, Length(FWizard.Screens)); + AssertTrue(FWizard.TryNext(message)); + FWizard.UpdatePath(ptReplaceAll, [TThirdWizardScreen]); + AssertEquals(0, FWizard.CurrentScreenIndex); + AssertTrue(FWizard.CurrentScreen is TThirdWizardScreen); + AssertFalse(FWizard.HasPrevious); + AssertFalse(FWizard.HasNext); +end; + +procedure TWizardTests.PreviousRestoresBranchBeforeReinjection; +begin + RunWizard([TFirstWizardScreen], VerifyBacktracking); +end; + +procedure TWizardTests.VerifyBacktracking(Sender : TObject); +var + message : String; + first : TProbeWizardScreen; +begin + first := TProbeWizardScreen(FWizard.CurrentScreen); + first.Traits := [wstAssumeHasNext]; + first.InjectedScreen := TSecondWizardScreen; + AssertTrue(FWizard.TryNext(message)); + AssertEquals(2, Length(FWizard.Screens)); + // An identical replacement must not discard the saved pre-branch path. + FWizard.UpdatePath(ptReplaceAll, FWizard.Screens); + AssertTrue(FWizard.TryPrevious(message)); + AssertEquals(1, Length(FWizard.Screens)); + first.InjectedScreen := TThirdWizardScreen; + AssertTrue(FWizard.TryNext(message)); + AssertTrue(FWizard.CurrentScreen is TThirdWizardScreen); + AssertEquals(2, Length(FWizard.Screens)); +end; + +procedure TWizardTests.ExplicitFinishValidatesAndRejectsRemainingScreens; +begin + RunWizard([TFirstWizardScreen, TSecondWizardScreen], VerifyExplicitFinish); +end; + +procedure TWizardTests.VerifyExplicitFinish(Sender : TObject); +var + message : String; +begin + AssertFalse(FWizard.Finish(message)); + AssertEquals(0, FWizard.FinishCount); + AssertTrue(FWizard.TryNext(message)); + TProbeWizardScreen(FWizard.CurrentScreen).AllowNext := false; + AssertFalse(FWizard.Finish(message)); + AssertEquals('Choose a branch', message); + AssertEquals(0, FWizard.FinishCount); + TProbeWizardScreen(FWizard.CurrentScreen).AllowNext := true; + AssertTrue(FWizard.Finish(message)); + AssertEquals(1, FWizard.FinishCount); +end; + +procedure TWizardTests.HostCloseUsesCancellationButFinishedCloseDoesNot; +begin + RunWizard([TFirstWizardScreen], VerifyHostClose); +end; + +procedure TWizardTests.VerifyHostClose(Sender : TObject); +var + message : String; + canClose : boolean; +begin + FWizard.IsCancellable := false; + FWizard.TestHost.FormCloseQuery(FWizard.TestHost, canClose); + AssertFalse(canClose); + AssertEquals(0, FWizard.CancelCount); + AssertTrue(FWizard.TryNext(message)); + FWizard.TestHost.FormCloseQuery(FWizard.TestHost, canClose); + AssertTrue(canClose); + AssertEquals(0, FWizard.CancelCount); +end; + +procedure TWizardTests.RemovedScreensRemainOwned; +var + before : Integer; +begin + before := TProbeWizardScreen.DestroyCount; + FWizard := TProbeWizard.Create([TFirstWizardScreen]); + FWizard.UpdatePath(ptInject, [TSecondWizardScreen]); + FWizard.RemoveScreen(TSecondWizardScreen); + AssertEquals(before, TProbeWizardScreen.DestroyCount); + FreeAndNil(FWizard); + AssertEquals(before + 2, TProbeWizardScreen.DestroyCount); +end; + +procedure TWizardTests.ActionWizardCaptionsUseConstructorArguments; +var + wizard : TActionWizard<Integer>; +begin + wizard := TActionWizard<Integer>.Create(nil, 'Example setup', '&Install', [TFirstWizardScreen], nil, nil); + try + AssertEquals('Example setup', wizard.TitleText); + AssertEquals('&Install', wizard.FinishText); + finally + wizard.Free; + end; +end; + +procedure TWizardTests.InvalidScreenRequestsPreservePath; +var + raised : boolean; +begin + FWizard := TProbeWizard.Create([TFirstWizardScreen]); + raised := false; + try + FWizard.UpdatePath(ptReplaceAll, []); + except + on E : Exception do raised := true; + end; + AssertTrue(raised); + AssertEquals(1, Length(FWizard.Screens)); + raised := false; + try + FWizard.UpdatePath(ptReplaceAll, [TComponent]); + except + on E : Exception do raised := true; + end; + AssertTrue(raised); + AssertTrue(FWizard.Screens[0] = TFirstWizardScreen); + raised := false; + try + FWizard.UpdatePath(ptReplaceAll, [TReferenceWizardFirstScreen]); + except + on E : Exception do raised := true; + end; + AssertTrue('Wizard screens with another model type must be rejected', raised); + AssertTrue(FWizard.Screens[0] = TFirstWizardScreen); +end; + +procedure TWizardTests.EmptyConstructionDoesNotLeak; +var + raised : boolean; +begin + raised := false; + try + FWizard := TProbeWizard.Create([]); + except + on E : Exception do raised := true; + end; + AssertTrue(raised); +end; + +procedure TWizardTests.UnstartedWizardCannotCompleteWithAnUnassignedModel; +var + message : String; + raised : boolean; +begin + FWizard := TProbeWizard.Create([TFirstWizardScreen]); + raised := false; + try + FWizard.Finish(message); + except + on E : Exception do raised := true; + end; + AssertTrue(raised); + raised := false; + try + FWizard.Cancel(message); + except + on E : Exception do raised := true; + end; + AssertTrue(raised); + AssertEquals(0, FWizard.FinishCount); + AssertEquals(0, FWizard.CancelCount); +end; + +procedure TWizardTests.InitializationFailureReleasesHostAndRetainsScreenOwnership; +var + before : Integer; + raised : boolean; +begin + before := TProbeWizardScreen.DestroyCount; + FWizard := TProbeWizard.Create([TFailingWizardScreen]); + raised := false; + try + FWizard.Start(42); + except + on E : Exception do begin + raised := true; + AssertEquals('Cannot initialize screen', E.Message); + end; + end; + AssertTrue(raised); + AssertTrue(FWizard.TestHost = nil); + AssertEquals(before, TProbeWizardScreen.DestroyCount); + FreeAndNil(FWizard); + AssertEquals(before + 1, TProbeWizardScreen.DestroyCount); +end; + +procedure TWizardTests.ReferenceModelScreensAcrossUnitsNavigateAndFinish; +var + model : TGenericWizardBag; +begin + model := TGenericWizardBag.Create; + try + model.Add('Name', 'Reference model'); + FReferenceWizard := TReferenceProbeWizard.Create(nil, 'Cross-unit wizard', '&Done', + [TReferenceWizardFirstScreen, TReferenceWizardSecondScreen], nil, nil); + FReferenceWizard.Run := VerifyReferenceModel; + FReferenceWizard.Start(model); + AssertEquals(Ord(wsFinished), Ord(FReferenceWizard.State)); + AssertTrue(FReferenceWizard.TestHost = nil); + FreeAndNil(FReferenceWizard); + AssertEquals('Reference model', VarToStr(model['Name'])); + finally + model.Free; + end; +end; + +procedure TWizardTests.VerifyReferenceModel(Sender : TObject); +var + first : TReferenceWizardScreen; +begin + first := TReferenceWizardScreen(FReferenceWizard.CurrentScreen); + AssertEquals('Reference model', first.InitializedName); + AssertEquals(1, first.InitializeCount); + FReferenceWizard.TestHost.FNextButton.Click; + AssertTrue(FReferenceWizard.CurrentScreen is TReferenceWizardSecondScreen); + AssertEquals('Reference model', TReferenceWizardScreen(FReferenceWizard.CurrentScreen).InitializedName); + FReferenceWizard.TestHost.FPreviousButton.Click; + AssertTrue(TObject(FReferenceWizard.CurrentScreen) = first); + AssertEquals(1, first.InitializeCount); + FReferenceWizard.TestHost.FNextButton.Click; + FReferenceWizard.TestHost.FNextButton.Click; + AssertEquals(Ord(wsFinished), Ord(FReferenceWizard.State)); +end; + +procedure TWizardTests.NavigationAndHostCleanupHideScreensBeforeDetaching; +var + visibleBefore, hiddenBefore : Integer; +begin + visibleBefore := TProbeWizardScreen.VisibleDetachCount; + hiddenBefore := TProbeWizardScreen.HiddenDetachCount; + RunWizard([TFirstWizardScreen, TSecondWizardScreen], VerifyHiddenDetachment); + AssertEquals('Navigation and final host cleanup must never detach a visible form', + visibleBefore, TProbeWizardScreen.VisibleDetachCount); + AssertEquals('Next, Previous and final host cleanup each detach a hidden screen', + hiddenBefore + 3, TProbeWizardScreen.HiddenDetachCount); +end; + +procedure TWizardTests.VerifyHiddenDetachment(Sender : TObject); +var + first, second : TProbeWizardScreen; +begin + first := TProbeWizardScreen(FWizard.CurrentScreen); + AssertTrue(first.Visible); + FWizard.TestHost.FNextButton.Click; + second := TProbeWizardScreen(FWizard.CurrentScreen); + AssertFalse(first.Visible); + AssertTrue(first.Parent = nil); + AssertTrue(second.Visible); + FWizard.TestHost.FPreviousButton.Click; + AssertFalse(second.Visible); + AssertTrue(second.Parent = nil); + AssertTrue(first.Visible); +end; + +procedure TWizardTests.NativeStringMessagesAndCaptionsPreserveText; +begin + FWizard := TProbeWizard.Create([TFirstWizardScreen]); + FWizard.TitleText := 'Résumé — 漢字'; + FWizard.FinishText := FWizard.TitleText; + FWizard.Run := VerifyNativeStringMessages; + FWizard.Start(42); + AssertTrue(FWizard.TestHost = nil); +end; + +procedure TWizardTests.VerifyNativeStringMessages(Sender : TObject); +var + LMessage, LText : String; + LScreen : TProbeWizardScreen; +begin + // Native String out parameters must pass captions and errors without ANSI adapters. + LText := FWizard.TitleText; + AssertEquals(LText, FWizard.TestHost.TitleText); + AssertEquals(LText, FWizard.TestHost.NextText); + LScreen := TProbeWizardScreen(FWizard.CurrentScreen); + LScreen.AllowNext := False; + LScreen.FailureMessage := LText; + AssertFalse(FWizard.Finish(LMessage)); + AssertEquals(LText, LMessage); + LScreen.AllowNext := True; + FWizard.AllowFinish := False; + FWizard.FailureMessage := LText; + AssertFalse(FWizard.Finish(LMessage)); + AssertEquals(LText, LMessage); + AssertEquals(1, FWizard.FinishCount); + FWizard.AllowCancel := False; + AssertFalse(FWizard.Cancel(LMessage)); + AssertEquals(LText, LMessage); + FWizard.AllowCancel := True; + AssertTrue(FWizard.Cancel(LMessage)); +end; + +initialization + RegisterTest(TWizardTests); + +end. diff --git a/src/tests/experimental/native/LICENSE b/src/tests/experimental/native/LICENSE new file mode 100644 index 000000000..0444b44f3 --- /dev/null +++ b/src/tests/experimental/native/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Herman Schoenfeld + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/tests/experimental/native/PrivateDesktopBroker.lpr b/src/tests/experimental/native/PrivateDesktopBroker.lpr new file mode 100644 index 000000000..54d2531b4 --- /dev/null +++ b/src/tests/experimental/native/PrivateDesktopBroker.lpr @@ -0,0 +1,309 @@ +program PrivateDesktopBroker; + +{ Copyright (c) 2026 Herman Schoenfeld <https://www.sphere10.com> + + Based on the PascalFramework native wizard regression broker. The broker + never switches the input desktop; all commands target only its own QA child. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + THIS LICENSE HEADER MUST NOT BE REMOVED. +} + +{$mode delphi} + +uses + Windows, SysUtils, Classes; + +const + CQAExecutionContinuous = DWORD($80000000); + CQAExecutionSystemRequired = DWORD($00000001); + +// This API is absent from the older FPC Windows declarations used by this QA +// runner. Keep its standard Win32 ABI explicit without another unit dependency. +function QASetThreadExecutionState(AFlags: DWORD): DWORD; stdcall; + external 'kernel32.dll' name 'SetThreadExecutionState'; + +var + ActualPID: DWORD; + ActualLines: TStringList; + ActualAbout, ActualMain: HWND; + ActualKnownMain: HWND; + ActualConfirmation, ActualYes: HWND; + ActualYesLeft: Integer; + ActualSawAbout, ActualSawMain, ActualInteractiveMain: Boolean; + ActualCloseRequested: Boolean; + +function FindActualYes(AWindow: HWND; AData: LPARAM): BOOL; stdcall; +var + title: array[0..127] of Char; + messageResult: DWORD_PTR; + bounds: TRect; +begin + FillChar(title, SizeOf(title), 0); + SendMessageTimeout(AWindow, WM_GETTEXT, Length(title), LPARAM(@title[0]), + SMTO_ABORTIFHUNG or SMTO_BLOCK, 100, @messageResult); + GetWindowRect(AWindow, bounds); + ActualLines.Add(Format(' child=%d caption=%s enabled=%s rect=%d,%d,%d,%d style=%x id=%d', + [AWindow, String(title), BoolToStr(IsWindowEnabled(AWindow), true), bounds.Left, bounds.Top, + bounds.Right, bounds.Bottom, GetWindowLongPtr(AWindow, GWL_STYLE), GetDlgCtrlID(AWindow)])); + if (StringReplace(String(title), '&', '', [rfReplaceAll]) = 'Yes') and IsWindowEnabled(AWindow) then + ActualYes := AWindow; + // LCL owner-drawn BitBtn captions are not exported by WM_GETTEXT in the + // shipping build. Its known Yes/No prompt lays out Yes before No. + if (ActualYes = 0) or (String(title) = '') then begin + GetWindowRect(AWindow, bounds); + if IsWindowEnabled(AWindow) and (bounds.Left < ActualYesLeft) then begin + ActualYes := AWindow; + ActualYesLeft := bounds.Left; + end; + end; + Result := true; +end; + +function SnapshotActualWindow(AWindow: HWND; AData: LPARAM): BOOL; stdcall; +var + pid: DWORD; + title, kind: array[0..511] of Char; + bounds: TRect; +begin + Result := true; + GetWindowThreadProcessId(AWindow, @pid); + if (pid <> ActualPID) or not IsWindowVisible(AWindow) then Exit; + GetWindowText(AWindow, title, Length(title)); + GetClassName(AWindow, kind, Length(kind)); + GetWindowRect(AWindow, bounds); + ActualLines.Add(Format('HWND=%d class=%s title=%s enabled=%s owner=%d rect=%d,%d,%d,%d style=%x', + [AWindow, String(kind), String(title), BoolToStr(IsWindowEnabled(AWindow), true), + GetWindow(AWindow, GW_OWNER), bounds.Left, bounds.Top, bounds.Right, bounds.Bottom, + GetWindowLongPtr(AWindow, GWL_STYLE)])); + // The shipping build uses LCL's generic native "Window" class. + if String(title) = 'About...' then begin + ActualAbout := AWindow; + ActualSawAbout := true; + end; + // An initialization error can reuse Application.Title. Require the actual + // ownerless wallet before recording successful startup or requesting Close. + if (Pos('Pascal Coin Wallet', String(title)) = 1) and + (GetWindow(AWindow, GW_OWNER) = 0) and + ((String(kind) = 'TFRMMainForm') or (Pos('JSON-RPC', String(title)) > 0)) then begin + ActualMain := AWindow; + ActualKnownMain := AWindow; + ActualSawMain := true; + ActualInteractiveMain := ActualInteractiveMain or IsWindowEnabled(AWindow); + end; + // VCL uses its standard Confirm title; LCL keeps the supplied wallet title. + // Accept either only for our own requested quit and the known main owner. + if ActualCloseRequested and (ActualKnownMain <> 0) and + (GetWindow(AWindow, GW_OWNER) = ActualKnownMain) and + ((String(title) = 'Exit PascalCoin') or + ((String(title) = 'Confirm') and (Pos('TMessageForm', String(kind)) = 1))) then begin + ActualConfirmation := AWindow; + ActualYes := 0; + ActualYesLeft := High(Integer); + EnumChildWindows(AWindow, @FindActualYes, 0); + end; +end; + +function RunActualExecutable(const AExecutable, ALogRoot, AMode: String): DWORD; +var + desktop: HDESK; + startup: TStartupInfo; + child: TProcessInformation; + desktopName, desktopPath, command, dataDirectory, logFile, previous, snapshot, windowSnapshot: String; + started: QWord; + closedAbout, closedMain: Boolean; +begin + desktopName := 'PascalCoinExecutableQA_' + IntToStr(GetCurrentProcessId); + dataDirectory := IncludeTrailingPathDelimiter(GetTempDir) + desktopName; + if DirectoryExists(dataDirectory) then raise Exception.Create('Refusing to reuse test data'); + logFile := IncludeTrailingPathDelimiter(ALogRoot) + AMode + '.log'; + desktop := CreateDesktop(PChar(desktopName), nil, nil, 0, GENERIC_ALL, nil); + if desktop = 0 then RaiseLastOSError; + ActualLines := TStringList.Create; + try + FillChar(startup, SizeOf(startup), 0); + FillChar(child, SizeOf(child), 0); + startup.cb := SizeOf(startup); + desktopPath := 'WinSta0\' + desktopName; + startup.lpDesktop := PChar(desktopPath); + startup.dwFlags := STARTF_USESHOWWINDOW; + startup.wShowWindow := SW_SHOW; + command := '"' + AExecutable + '" "--data-dir=' + dataDirectory + '" --offline'; + if AMode = 'actual-no-dialogs' then command := command + ' --no-startup-dialogs'; + if not CreateProcess(nil, PChar(command), nil, nil, false, 0, nil, nil, @startup, @child) then + RaiseLastOSError; + try + ActualPID := child.dwProcessId; + started := GetTickCount64; + closedAbout := false; + closedMain := false; + ActualSawAbout := false; + ActualSawMain := false; + ActualInteractiveMain := false; + ActualKnownMain := 0; + ActualCloseRequested := false; + previous := ''; + repeat + ActualAbout := 0; + ActualMain := 0; + ActualConfirmation := 0; + ActualLines.Clear; + EnumDesktopWindows(desktop, @SnapshotActualWindow, 0); + snapshot := ActualLines.Text; + if snapshot <> previous then begin + windowSnapshot := snapshot; + ActualLines.Insert(0, 'elapsed=' + IntToStr(GetTickCount64 - started)); + if FileExists(logFile) then begin + snapshot := ActualLines.Text; + ActualLines.LoadFromFile(logFile); + ActualLines.Add(snapshot); + end; + ActualLines.SaveToFile(logFile); + previous := windowSnapshot; + end; + if (not closedAbout) and (GetTickCount64 - started > 3000) and (ActualAbout <> 0) then begin + PostMessage(ActualAbout, WM_CLOSE, 0, 0); + closedAbout := true; + end; + if (not closedMain) and (GetTickCount64 - started > 6000) and (ActualMain <> 0) then begin + ActualCloseRequested := true; + PostMessage(ActualMain, WM_CLOSE, 0, 0); + closedMain := true; + end; + if ActualConfirmation <> 0 then begin + if ActualYes <> 0 then + PostMessage(ActualConfirmation, WM_COMMAND, + (BN_CLICKED shl 16) or (GetDlgCtrlID(ActualYes) and $FFFF), LPARAM(ActualYes)); + end; + if WaitForSingleObject(child.hProcess, 150) = WAIT_OBJECT_0 then break; + until GetTickCount64 - started > 10000; + if WaitForSingleObject(child.hProcess, 0) <> WAIT_OBJECT_0 then begin + TerminateProcess(child.hProcess, 124); + WaitForSingleObject(child.hProcess, 5000); + end; + if not GetExitCodeProcess(child.hProcess, Result) then RaiseLastOSError; + if Result = 0 then begin + if ActualSawAbout or (not ActualSawMain) or (not ActualInteractiveMain) then + Result := 1; + end; + WriteLn('Actual EXE test PID ', child.dwProcessId, ', private desktop ', desktopName, + ', exit ', Result, ', snapshot ', logFile); + finally + CloseHandle(child.hThread); + CloseHandle(child.hProcess); + end; + finally + ActualLines.Free; + CloseDesktop(desktop); + end; +end; + +function RunProbe(const AExecutable, ALogRoot, AMode : String) : DWORD; +var + LDesktop : HDESK; + LStartup : TStartupInfo; + LChild : TProcessInformation; + LDesktopName, LDesktopPath, LCommand, LLogPath, LDataDirectory : String; + LWaitResult : DWORD; +begin + Result := 1; + LDesktopName := 'PascalCoinWalletQA_' + IntToStr(GetCurrentProcessId) + '_' + AMode; + LDesktopPath := 'WinSta0\' + LDesktopName; + if AMode = 'units' then + LLogPath := ALogRoot + else + LLogPath := IncludeTrailingPathDelimiter(ALogRoot) + AMode + '.log'; + // Every child receives a new, disposable data directory under the OS temp + // directory. No configured user wallet path is consulted or passed through. + LDataDirectory := IncludeTrailingPathDelimiter(GetTempDir) + LDesktopName; + if (AMode <> 'units') and (DirectoryExists(LDataDirectory) or FileExists(LDataDirectory)) then + raise Exception.Create('Refusing to reuse a QA data directory: ' + LDataDirectory); + LDesktop := CreateDesktop(PChar(LDesktopName), nil, nil, 0, GENERIC_ALL, nil); + if LDesktop = 0 then RaiseLastOSError; + try + FillChar(LStartup, SizeOf(LStartup), 0); + FillChar(LChild, SizeOf(LChild), 0); + LStartup.cb := SizeOf(LStartup); + LStartup.lpDesktop := PChar(LDesktopPath); + LStartup.dwFlags := STARTF_USESHOWWINDOW; + LStartup.wShowWindow := SW_SHOW; + if AMode = 'units' then + LCommand := '"' + AExecutable + '" "' + LLogPath + '"' + else + LCommand := '"' + AExecutable + '" --probe-mode=' + AMode + + ' "--probe-log=' + LLogPath + '" "--data-dir=' + LDataDirectory + + '" --offline --no-startup-dialogs'; + if not CreateProcess(nil, PChar(LCommand), nil, nil, false, 0, nil, nil, @LStartup, @LChild) then + RaiseLastOSError; + try + WriteLn('Started own QA child PID ', LChild.dwProcessId, ' on ', LDesktopPath, + '; input desktop unchanged; data: ', LDataDirectory); + // The complete suite includes real pending-save waits, large collections + // and heap tracing; measured runs can exceed ten minutes. Individual test + // deadlines and the short native UI probe budget remain independent. + if AMode = 'units' then + LWaitResult := WaitForSingleObject(LChild.hProcess, 1800000) + else + LWaitResult := WaitForSingleObject(LChild.hProcess, 30000); + if LWaitResult = WAIT_TIMEOUT then begin + TerminateProcess(LChild.hProcess, 124); + WaitForSingleObject(LChild.hProcess, 5000); + WriteLn('Watchdog terminated only QA child PID ', LChild.dwProcessId); + Result := 124; + end else begin + if LWaitResult <> WAIT_OBJECT_0 then RaiseLastOSError; + if not GetExitCodeProcess(LChild.hProcess, Result) then RaiseLastOSError; + WriteLn('QA child exited ', Result, '; log: ', LLogPath); + end; + finally + CloseHandle(LChild.hThread); + CloseHandle(LChild.hProcess); + end; + finally + CloseDesktop(LDesktop); + end; +end; + +var + LExitCode : DWORD; + LPreviousExecutionState: DWORD; +begin + if (ParamCount <> 2) and (ParamCount <> 3) then Halt(64); + if (ParamCount = 3) and (ParamStr(3) <> '--wallet-startup') and + (ParamStr(3) <> '--actual-first-run') and (ParamStr(3) <> '--actual-no-dialogs') then Halt(64); + LExitCode := 1; + // Keep only this owned QA run awake: long real-time assertions are invalid + // across system sleep. This thread-scoped request never changes a power plan + // or keeps the display on, and is restored before the broker exits. + LPreviousExecutionState := QASetThreadExecutionState(CQAExecutionContinuous or CQAExecutionSystemRequired); + if LPreviousExecutionState=0 then RaiseLastOSError; + try + try + if ParamCount = 2 then + LExitCode := RunProbe(ExpandFileName(ParamStr(1)), ExpandFileName(ParamStr(2)), 'units') + else if (ParamStr(3) = '--actual-first-run') or (ParamStr(3) = '--actual-no-dialogs') then begin + LExitCode := RunActualExecutable(ExpandFileName(ParamStr(1)), ExpandFileName(ParamStr(2)), Copy(ParamStr(3), 3, MaxInt)); + end else begin + if not DirectoryExists(ParamStr(2)) then + raise Exception.Create('The QA log directory must already exist.'); + LExitCode := RunProbe(ExpandFileName(ParamStr(1)), ExpandFileName(ParamStr(2)), 'loaded'); + if LExitCode = 0 then + LExitCode := RunProbe(ExpandFileName(ParamStr(1)), ExpandFileName(ParamStr(2)), 'close-loading'); + if LExitCode = 0 then + LExitCode := RunProbe(ExpandFileName(ParamStr(1)), ExpandFileName(ParamStr(2)), 'close-before-load'); + if LExitCode = 0 then + LExitCode := RunProbe(ExpandFileName(ParamStr(1)), ExpandFileName(ParamStr(2)), 'slow-close'); + end; + except + on E : Exception do begin + WriteLn('Private desktop QA failed: ', E.Message); + LExitCode := 1; + end; + end; + finally + QASetThreadExecutionState(LPreviousExecutionState or CQAExecutionContinuous); + end; + Halt(LExitCode); +end. diff --git a/src/tests/experimental/native/README.md b/src/tests/experimental/native/README.md new file mode 100644 index 000000000..68dadf6b5 --- /dev/null +++ b/src/tests/experimental/native/README.md @@ -0,0 +1,114 @@ +# Native experimental wallet startup checks + +These checks run the experimental wallet's startup and close paths offline on a +private Windows desktop. [Other native checks](#other-native-checks) run +separately. + +## Run the checks + +Build the [shipping wallet](../../../gui-experimental/README.md), +`src/PascalCoinWalletExperimental.exe`, then run from the repository root: + +```powershell +& src/tests/experimental/native/RunWalletStartupNativeTests.ps1 +``` + +The script builds `PrivateDesktopBroker.exe` and `WalletStartupProbe.exe` into +the output folder with FPC and the LCL only. Exit code 0 means every process +passed. + +- `-FpcPath` and `-LazarusDirectory` locate a Windows x64 FPC and Lazarus. +- `-OutputDirectory <new-directory>` replaces the default + `src/tests/experimental/lib/native-<guid>` folder. +- `-WalletExecutable <path>` selects another shipping wallet. + +The output folder holds build and broker logs, `<process>.log` files and, in +`temporary-data`, the data directories. + +## Private-desktop broker + +`PrivateDesktopBroker.exe <executable> <log file>` runs a probe on a new private +desktop, passes it only the log path and returns its exit code. It never +switches the input desktop or targets other processes. It stops such a probe +after 30 minutes, startup probes after 30 seconds and shipping runs after 10 seconds. + +## Processes + +Every process gets a new temporary `--data-dir` and `--offline`. None loads the +normal wallet or connects to peers. + +- `loaded` completes startup on empty data and raises `Loaded` once. It checks + window layout, screen switches, maximize and restore, and footer buttons at + normal and enlarged DPI. It also checks Wallet Keys artwork, modeless focus, + offline enforcement, key changes, classic tools and callbacks while the + mempool is held. It quits through Close and Yes. +- `close-loading` shuts down just after the loader starts, and + `close-before-load` shuts down from `AppStarted`, before the worker starts. + Both require zero `Loaded` callbacks and a fully released application. +- `slow-close` closes the window while a startup task is running. The window + must stay enabled with wallet commands disabled, and cancellation must finish + without a `Loaded` callback. +- `actual-first-run` and `actual-no-dialogs` start the shipping wallet with and + without startup dialogs. Each needs a visible, enabled main window and no + automatic About dialog, then closes through the exit confirmation. + +## Large-dataset and performance checks + +The script uses empty-chain data. For large datasets: + +1. Restore a separate copy of the public chain and saved pending buffer. Leave + the source unchanged. +2. Select a public key that owns at least 326,387 accounts in that copy and has + history rows among the roughly 185,000 pending operations. +3. Check account and history fetches, UI heartbeats, repeated view switches, + native icon handles and the white status label below the logo. +4. Compare the copied pending buffer's hash before and after the run. + +Do not import the normal wallet's private keys. + +For update performance, measure cold loading, warm paging and a backend +notification burst separately. Count `FetchAll` calls and grid completions. +Record working and private memory at the same checkpoints before and after a +change. The [regression test guide](../README.md) and +[grid refresh demo](grid-refresh/README.md) cover paging, totals, refresh and +loading cards. + +## Other native checks + +`RunWalletRegressionTests.ps1 -Probe <name> -OutputDirectory <new-directory>` runs +the `classic`, `connection-restart`, `pending-protocol`, `buffered-frames`, +`node-sync` or `mempool-lock` probe with FPC. + +- [Wallet grid refresh demo](grid-refresh/README.md) covers synchronization + bursts and fetch counters. +- [Send PASC presentation](grid-refresh/README.md#send-pasc-presentation) + captures every reachable wizard step at 100% and 150%. +- [Account selector](account-selector/README.md) covers key filtering, paged + confirmed accounts and type statistics. +- [Classic compatibility probe](classic/README.md) covers local restore, five + validated blocks, grids, accounts and native Close. +- [AbstractMem storage](abstractmem-storage/README.md) +- [Block application and pending replay](block-throughput/README.md) +- [Buffered adoption](buffered-adoption/README.md) +- [Checkpoint storage](storage-recovery/README.md) +- [Connection restart](connection-restart/README.md) +- [Log memo batches](log-memo-batch/README.md) +- [Logger GUI handoff](log-gui-handoff/README.md) +- [Node synchronization](node-sync/README.md) +- [Pascal Framework components](common/README.md) +- [Pending protocol](pending-protocol/README.md) +- [Received block backlog](network-backlog/README.md) +- [Safebox download shutdown](download-stop/README.md) +- [Safebox recovery](safebox-recovery/README.md) + +These helpers run in Delphi and FPC: + +- [Core and lifetime](network-lifetime/README.md) +- [Buffered frames](network-backlog/BufferedBlockFrameProbe.md) +- [Cardinal ordering](ordered-cardinal/README.md) +- [Stored history](stored-history/README.md) + +## License + +The broker is adapted from PascalFramework's native wizard test infrastructure. +Its MIT license is included in this directory. diff --git a/src/tests/experimental/native/RunWalletRegressionTests.ps1 b/src/tests/experimental/native/RunWalletRegressionTests.ps1 new file mode 100644 index 000000000..0a278d8f9 --- /dev/null +++ b/src/tests/experimental/native/RunWalletRegressionTests.ps1 @@ -0,0 +1,155 @@ +<# +.SYNOPSIS +Runs a distinct native wallet regression on an isolated Windows desktop. +.DESCRIPTION +Uses the existing acceptance helpers, fresh data and the private desktop broker. +The protocol probe compiles a verified visibility-only copy of UNetProtocol; +shipping source and method bodies remain unchanged. +#> +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [Parameter(Mandatory = $true)] + [ValidateSet('classic', 'connection-restart', 'pending-protocol', 'buffered-frames', 'node-sync', 'mempool-lock')] + [string] $Probe, + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string] $LazarusDirectory = $(if ($c = Get-Command lazbuild -ErrorAction SilentlyContinue) { Split-Path -Parent $c.Source } else { '' }), + [Parameter(Mandatory = $true)] + [string] $OutputDirectory +) + +$ErrorActionPreference = 'Stop' +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +$lazarus = (Resolve-Path -LiteralPath $LazarusDirectory).Path +$source = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../../..')).Path +$targetInfo = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if ($LASTEXITCODE -ne 0 -or $targetInfo.Count -ne 2 -or $targetInfo[1] -ne 'win64') { + throw 'The native wallet regressions require a Windows x64 FPC compiler.' +} +$target = $targetInfo -join '-' +$programs = @{ + 'classic' = 'classic/ClassicWalletProbe.lpr' + 'connection-restart' = 'connection-restart/ConnectionRestartProbe.lpr' + 'pending-protocol' = 'pending-protocol/PendingProtocolProbe.lpr' + 'buffered-frames' = 'network-backlog/BufferedBlockFrameProbe.lpr' + 'node-sync' = 'node-sync/WalletNodeSynchronizationProbe.lpr' + 'mempool-lock' = 'mempool-lock/MempoolLockProbe.lpr' +} +$program = Join-Path $PSScriptRoot $programs[$Probe] +$output = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $output) { throw "Choose a new output directory: $output" } +$units = Join-Path $output 'units' +$brokerUnits = Join-Path $output 'broker-units' +$temporaryData = Join-Path $output 'temporary-data' +New-Item -ItemType Directory -Path $units, $brokerUnits, $temporaryData -Force | Out-Null +$executable = Join-Path $output ([IO.Path]::GetFileNameWithoutExtension($program) + '.exe') +$fixture = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../fixtures/mainnet-blocks-0-4.bin')).Path +Copy-Item -LiteralPath $fixture -Destination $output +$crypto = @((Join-Path $source 'libcrypto-1_1-x64.dll'), + (Join-Path (Split-Path -Parent $source) 'windows-dll/libcrypto-1_1-x64.dll')) | + Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 +if (-not $crypto) { throw 'The bundled OpenSSL 1.1 x64 runtime is missing.' } +Copy-Item -LiteralPath $crypto -Destination $output + +$arguments = @('-B', '-Mdelphi', '-O1', '-gl', + '-dBorland', '-dVer150', '-dDelphi7', '-dCompiler6_Up', '-dPUREPASCAL', + "-FU$units", "-FE$output", "-o$executable", + "-Fi$source/libraries/generics.collections/inc") +if ($Probe -ne 'classic') { + $arguments += @('-Cr', '-Co') +} +# Classic retains its shipping arithmetic settings; the other core probes are checked. +if ($Probe -in @('classic', 'node-sync')) { $arguments += '-WG' } +if ($Probe -eq 'pending-protocol') { + $accessOutput = Join-Path $output 'protocol-access' + $accessCore = & (Join-Path $PSScriptRoot 'pending-protocol/NewPendingProtocolAccessCopy.ps1') -OutputDirectory $accessOutput + $arguments += @('-dPASCAL_PROTOCOL_INTERNAL_CHECKS', "-Fu$accessCore") +} +$arguments += @("-Fu$([IO.Path]::GetDirectoryName($program))", + "-Fu$lazarus/lcl/units/$target/win32", "-Fu$lazarus/lcl/units/$target", + "-Fu$lazarus/components/lazutils/lib/$target", "-Fu$lazarus/packager/units/$target") +$directories = @('core', 'gui-experimental', 'gui-experimental/core.utils', + 'gui-experimental/wizards/wallet', 'gui-experimental/wizards/operations', 'gui-classic', + 'libraries/simplebaselib4pascal', 'libraries/hashlib4pascal', 'libraries/cryptolib4pascal', + 'libraries/synapse', 'libraries/sphere10', 'libraries/generics.collections', + 'libraries/pascalcoin', 'libraries/paszlib', 'libraries/abstractmem', 'libraries/regex', + 'libraries/gnugettext') +if ($Probe -eq 'classic') { + # Both GUIs contain UFRMWallet; keep the Classic probe on its shipping graph. + $directories = @('gui-classic') + @($directories | Where-Object { + $_ -ne 'gui-classic' -and $_ -notlike 'gui-experimental*' + }) +} +foreach ($directory in $directories) { $arguments += "-Fu$source/$directory" } +$arguments += $program +$arguments | Set-Content -LiteralPath (Join-Path $output 'compiler-arguments.txt') +& $compiler @arguments *> (Join-Path $output 'build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'build.log') -Tail 60 + exit $LASTEXITCODE +} +$broker = Join-Path $output 'PrivateDesktopBroker.exe' +& $compiler '-Mdelphi' '-O2' '-gl' "-FU$brokerUnits" "-o$broker" ` + (Join-Path $PSScriptRoot 'PrivateDesktopBroker.lpr') *> (Join-Path $output 'broker-build.log') +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'broker-build.log') -Tail 40 + exit $LASTEXITCODE +} +$log = Join-Path $output 'tests.log' +$previousTemp, $previousTmp = $env:TEMP, $env:TMP +$previousFixture = $env:PASCAL_CLASSIC_BLOCK_FIXTURE +$previousProtocolCase = $env:PASCAL_PENDING_PROTOCOL_ONLY +$previousPowCase = $env:PASCAL_PENDING_POW_NONEMPTY_ONLY +try { + $env:TEMP = $temporaryData + $env:TMP = $temporaryData + $env:PASCAL_CLASSIC_BLOCK_FIXTURE = $fixture + Remove-Item Env:PASCAL_PENDING_PROTOCOL_ONLY -ErrorAction SilentlyContinue + Remove-Item Env:PASCAL_PENDING_POW_NONEMPTY_ONLY -ErrorAction SilentlyContinue + & $broker $executable $log *> (Join-Path $output 'broker.log') + $status = $LASTEXITCODE +} finally { + $env:TEMP, $env:TMP = $previousTemp, $previousTmp + $env:PASCAL_CLASSIC_BLOCK_FIXTURE = $previousFixture + $env:PASCAL_PENDING_PROTOCOL_ONLY = $previousProtocolCase + $env:PASCAL_PENDING_POW_NONEMPTY_ONLY = $previousPowCase +} +if (Test-Path -LiteralPath $log) { + $report = [IO.File]::ReadAllText($log) + Write-Output $report + if ($report -match '(?m)^FAIL') { $status = 1 } + if ($Probe -eq 'pending-protocol' -and + [regex]::Matches($report, '(?m)^PASS pending protocol case=\d+\s*$').Count -ne 9) { + Write-Error 'All nine pending protocol cases must run.' -ErrorAction Continue + $status = 1 + } +} else { + Write-Error 'The native regression did not produce its report.' -ErrorAction Continue + $status = 1 +} +$heap = "$log.heap.log" +if ($status -eq 0 -and (-not (Test-Path -LiteralPath $heap) -or + [IO.File]::ReadAllText($heap) -notmatch '(?m)^0 unfreed memory blocks')) { + Write-Error 'The native regression did not produce a clean heap report.' -ErrorAction Continue + $status = 1 +} +if ($Probe -eq 'pending-protocol') { + # Recompute both fixed digests independently of the Pascal serializer. + $python = @('python', 'python3') | ForEach-Object { + Get-Command $_ -CommandType Application -ErrorAction SilentlyContinue + } | Where-Object Source -NotLike '*\WindowsApps\*' | Select-Object -First 1 + if (-not $python) { throw 'Python 3 is required for the independent proof-of-work vectors.' } + & $python.Source (Join-Path $PSScriptRoot 'pending-protocol/ProofOfWorkVectors.py') ` + *> (Join-Path $output 'proof-of-work-vectors.json') + if ($LASTEXITCODE -ne 0) { $status = 1 } +} +@{ + Probe = $Probe + ExitCode = $status + ExecutableSha256 = (Get-FileHash -LiteralPath $executable -Algorithm SHA256).Hash + FixtureSha256 = (Get-FileHash -LiteralPath $fixture -Algorithm SHA256).Hash + CheckedArithmetic = ($Probe -ne 'classic') + VisibilityOnlyProtocolCopy = ($Probe -eq 'pending-protocol') +} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $output 'verification.json') +if ($status -ne 0) { Get-Content -LiteralPath (Join-Path $output 'broker.log') -Tail 25 } +Write-Host "Native $Probe regression evidence: $output" +exit $status diff --git a/src/tests/experimental/native/RunWalletStartupNativeTests.ps1 b/src/tests/experimental/native/RunWalletStartupNativeTests.ps1 new file mode 100644 index 000000000..fd3cfa320 --- /dev/null +++ b/src/tests/experimental/native/RunWalletStartupNativeTests.ps1 @@ -0,0 +1,126 @@ +param( + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string] $LazarusDirectory = $(if ($c = Get-Command lazbuild -ErrorAction SilentlyContinue) { Split-Path -Parent $c.Source } else { '' }), + [string] $OutputDirectory, + [string] $WalletExecutable +) + +$ErrorActionPreference = 'Stop' +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +$lazarus = (Resolve-Path -LiteralPath $LazarusDirectory).Path +$source = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..\..')).Path +if (-not $WalletExecutable) { $WalletExecutable = Join-Path $source 'PascalCoinWalletExperimental.exe' } +$shippingExecutable = (Resolve-Path -LiteralPath $WalletExecutable).Path +$targetInfo = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if ($LASTEXITCODE -ne 0 -or $targetInfo.Count -ne 2 -or $targetInfo[1] -ne 'win64') { + throw 'The native wallet startup probe requires a Windows x64 FPC compiler.' +} +$target = "$($targetInfo[0])-$($targetInfo[1])" +if (-not $OutputDirectory) { + $OutputDirectory = Join-Path $PSScriptRoot ('..\lib\native-' + [guid]::NewGuid().ToString('N')) +} +New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +$output = (Resolve-Path -LiteralPath $OutputDirectory).Path +$brokerUnits = Join-Path $output 'broker-units' +$probeUnits = Join-Path $output 'probe-units' +New-Item -ItemType Directory -Path $brokerUnits, $probeUnits -Force | Out-Null +$brokerExecutable = Join-Path $output 'PrivateDesktopBroker.exe' +$probeExecutable = Join-Path $output 'WalletStartupProbe.exe' +$brokerBuildLog = Join-Path $output 'broker-build.log' +$probeBuildLog = Join-Path $output 'probe-build.log' +$brokerRunLog = Join-Path $output 'broker-run.log' + +& $compiler '-MDelphi' '-O1' '-gl' "-FU$brokerUnits" "-FE$output" "-o$brokerExecutable" ` + "$PSScriptRoot\PrivateDesktopBroker.lpr" *> $brokerBuildLog +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath $brokerBuildLog -Tail 50 + exit $LASTEXITCODE +} +$probeArguments = @( + '-B', '-MDelphi', '-O1', '-gl', '-WG', + '-dBorland', '-dVer150', '-dDelphi7', '-dCompiler6_Up', '-dPUREPASCAL', + "-Fu$lazarus\lcl\units\$target", "-Fu$lazarus\lcl\units\$target\win32", + "-Fu$lazarus\components\lazutils\lib\$target", "-Fu$lazarus\packager\units\$target", + "-Fi$source\libraries\generics.collections\inc", "-FU$probeUnits", "-FE$output", "-o$probeExecutable" +) +$unitDirectories = @( + 'core', 'gui-experimental', 'libraries\simplebaselib4pascal', 'libraries\hashlib4pascal', + 'libraries\cryptolib4pascal', 'libraries\synapse', 'libraries\sphere10', + 'libraries\generics.collections', 'libraries\pascalcoin', 'gui-experimental\core.utils', + 'gui-experimental\wizards\wallet', 'gui-experimental\wizards\operations', + 'libraries\paszlib', 'gui-classic', 'libraries\abstractmem', 'libraries\regex' +) +foreach ($directory in $unitDirectories) { + $probeArguments += "-Fu$source\$directory" +} +$probeArguments += "$PSScriptRoot\WalletStartupProbe.lpr" +& $compiler @probeArguments *> $probeBuildLog +if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath $probeBuildLog -Tail 60 + exit $LASTEXITCODE +} +# Stage the bundled runtime without relying on an untracked source-tree DLL. +$crypto = Join-Path (Split-Path -Parent $source) 'windows-dll\libcrypto-1_1-x64.dll' +if (-not (Test-Path -LiteralPath $crypto)) { + $crypto = Join-Path $source 'libcrypto-1_1-x64.dll' +} +if (-not (Test-Path -LiteralPath $crypto)) { throw "OpenSSL runtime not found: $crypto" } +Copy-Item -LiteralPath $crypto -Destination $output +$shippingDirectory = Join-Path $output 'shipping' +New-Item -ItemType Directory -Path $shippingDirectory -Force | Out-Null +$stagedShipping = Join-Path $shippingDirectory ([IO.Path]::GetFileName($shippingExecutable)) +$shippingHash = (Get-FileHash -LiteralPath $shippingExecutable -Algorithm SHA256).Hash +Copy-Item -LiteralPath $shippingExecutable -Destination $stagedShipping +Copy-Item -LiteralPath $crypto -Destination $shippingDirectory +$stagedHash = (Get-FileHash -LiteralPath $stagedShipping -Algorithm SHA256).Hash +if ($stagedHash -ne $shippingHash) { throw 'The staged shipping wallet differs from its built input.' } +@{ + Source = $shippingExecutable + Staged = $stagedShipping + SourceSha256 = $shippingHash + StagedSha256 = $stagedHash + RuntimeSha256 = (Get-FileHash -LiteralPath $crypto -Algorithm SHA256).Hash + WorkingDirectory = $shippingDirectory +} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $output 'shipping-runtime.json') +# The broker refuses to reuse its data directories and never switches desktops. +$testTemp = Join-Path $output 'temporary-data' +New-Item -ItemType Directory -Path $testTemp -Force | Out-Null +$previousTemp = $env:TEMP +$previousTmp = $env:TMP +try { + $env:TEMP = $testTemp + $env:TMP = $testTemp + & $brokerExecutable $probeExecutable $output '--wallet-startup' *> $brokerRunLog + $nativeExitCode = $LASTEXITCODE +} finally { + $env:TEMP = $previousTemp + $env:TMP = $previousTmp +} +Get-Content -LiteralPath $brokerRunLog +foreach ($mode in @('loaded', 'close-loading', 'close-before-load', 'slow-close')) { + $log = Join-Path $output "$mode.log" + if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log } +} +if ($nativeExitCode -eq 0) { + try { + $env:TEMP = $testTemp + $env:TMP = $testTemp + # Both application-directory and current-directory resolution use this isolated pair. + Push-Location -LiteralPath $shippingDirectory + try { + foreach ($mode in @('actual-first-run', 'actual-no-dialogs')) { + & $brokerExecutable $stagedShipping $output "--$mode" >> $brokerRunLog + if ($LASTEXITCODE -ne 0) { $nativeExitCode = $LASTEXITCODE } + Get-Content -LiteralPath (Join-Path $output "$mode.log") + if ($nativeExitCode -ne 0) { break } + } + } finally { + Pop-Location + } + } finally { + $env:TEMP = $previousTemp + $env:TMP = $previousTmp + } +} +Write-Host "Native wallet startup evidence: $output" +exit $nativeExitCode diff --git a/src/tests/experimental/native/SetCiDisplayMode.ps1 b/src/tests/experimental/native/SetCiDisplayMode.ps1 new file mode 100644 index 000000000..9262aeaa0 --- /dev/null +++ b/src/tests/experimental/native/SetCiDisplayMode.ps1 @@ -0,0 +1,161 @@ +<# +.SYNOPSIS +Prepares an ephemeral GitHub Windows runner for native 150-percent UI checks. +.DESCRIPTION +Only GitHub-hosted runners may change mode. InspectOnly reads modes without +changing the local display. No registry, DPI or persistent display settings change. +#> +param( + [switch] $InspectOnly, + [string] $ReportPath +) + +$ErrorActionPreference = 'Stop' +if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { + throw 'This native test display helper requires Windows.' +} +if (-not $InspectOnly -and + ($env:RUNNER_ENVIRONMENT -ne 'github-hosted' -or $env:GITHUB_ACTIONS -ne 'true')) { + throw 'Display changes are restricted to ephemeral GitHub-hosted runners. Use -InspectOnly locally.' +} + +if (-not ('PascalCoinCiDisplay' -as [type])) { + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; + +public static class PascalCoinCiDisplay { + // DEVMODEW has a fixed 220-byte layout on both Windows architectures. + // See Microsoft wingdi.h/DEVMODEW; unused display/printer fields stay opaque. + [StructLayout(LayoutKind.Explicit, Size = 220)] + public struct Mode { + [FieldOffset(68)] public ushort Size; + [FieldOffset(70)] public ushort DriverExtra; + [FieldOffset(72)] public uint Fields; + [FieldOffset(168)] public uint BitsPerPixel; + [FieldOffset(172)] public uint Width; + [FieldOffset(176)] public uint Height; + [FieldOffset(184)] public uint Frequency; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Rect { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll", EntryPoint = "EnumDisplaySettingsW", + CharSet = CharSet.Unicode, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool NativeEnumDisplaySettings(string device, int index, ref Mode mode); + + [DllImport("user32.dll", EntryPoint = "ChangeDisplaySettingsExW", + CharSet = CharSet.Unicode, ExactSpelling = true)] + private static extern int NativeChangeDisplaySettings(string device, ref Mode mode, + IntPtr window, uint flags, IntPtr parameter); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SystemParametersInfoW(uint action, uint parameter, + out Rect rectangle, uint flags); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetProcessDPIAware(); + + public static Mode ReadMode(int index) { + var mode = new Mode(); + mode.Size = (ushort)Marshal.SizeOf(typeof(Mode)); + if (!NativeEnumDisplaySettings(null, index, ref mode)) { + throw new InvalidOperationException("Cannot read the current display mode."); + } + return mode; + } + + public static Mode[] AvailableModes() { + var modes = new System.Collections.Generic.List<Mode>(); + for (var index = 0; ; index++) { + var mode = new Mode(); + mode.Size = (ushort)Marshal.SizeOf(typeof(Mode)); + if (!NativeEnumDisplaySettings(null, index, ref mode)) { + return modes.ToArray(); + } + modes.Add(mode); + } + } + + public static int ApplyMode(Mode mode, uint flags) { + // Keep the native null device pointer inside C#: PowerShell can coerce + // a null string argument to an invalid empty display-device name. + mode.Size = (ushort)Marshal.SizeOf(typeof(Mode)); + return NativeChangeDisplaySettings(null, ref mode, IntPtr.Zero, flags, IntPtr.Zero); + } + + public static Rect WorkArea() { + Rect rectangle; + if (!SystemParametersInfoW(0x0030, 0, out rectangle, 0)) { + throw new InvalidOperationException("Cannot read the native work area."); + } + return rectangle; + } +} +'@ +} + +# This affects only this helper process, so reported rectangles use device pixels. +[void][PascalCoinCiDisplay]::SetProcessDPIAware() +$before = [PascalCoinCiDisplay]::ReadMode(-1) +$beforeArea = [PascalCoinCiDisplay]::WorkArea() +$modes = @([PascalCoinCiDisplay]::AvailableModes() | Where-Object { + $_.BitsPerPixel -ge 32 -and $_.Width -ge 1920 -and $_.Height -ge 1080 +}) + +$changed = $false +try { + if (-not $InspectOnly -and + ($before.Width -lt 1920 -or $before.Height -lt 1080)) { + # Prefer Full HD; retain a driver's supported alternative if it tests valid. + $requested = [PascalCoinCiDisplay]::ReadMode(-1) + $requested.Width = 1920 + $requested.Height = 1080 + $candidates = @($requested) + @($modes | Sort-Object Width, Height, Frequency) + $selected = $false + foreach ($candidate in $candidates) { + $candidate.Fields = 0x001C0000 # DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT + $candidate.DriverExtra = 0 + $candidate.BitsPerPixel = 32 + $test = [PascalCoinCiDisplay]::ApplyMode($candidate, 2) + if ($test -ne 0) { continue } + # Flags=0 applies dynamically; CDS_UPDATEREGISTRY is intentionally absent. + $apply = [PascalCoinCiDisplay]::ApplyMode($candidate, 0) + if ($apply -eq 0) { + $selected = $true + $changed = $true + break + } + } + if (-not $selected) { throw 'No supported Full HD test display mode could be applied.' } + } + + $after = [PascalCoinCiDisplay]::ReadMode(-1) + $area = [PascalCoinCiDisplay]::WorkArea() + if (-not $InspectOnly -and + (($area.Right - $area.Left) -lt 1200 -or ($area.Bottom - $area.Top) -lt 1020)) { + throw 'The native test work area is too small for the fixed-size 150-percent dialogs.' + } +} finally { + $after = [PascalCoinCiDisplay]::ReadMode(-1) + $area = [PascalCoinCiDisplay]::WorkArea() + $report = [ordered]@{ + InspectOnly = [bool]$InspectOnly + Changed = $changed + Before = @{ Width = $before.Width; Height = $before.Height; WorkArea = $beforeArea } + After = @{ Width = $after.Width; Height = $after.Height; WorkArea = $area } + SupportedFullHdModes = @($modes | Select-Object Width, Height, BitsPerPixel, Frequency -Unique) + } + $json = $report | ConvertTo-Json -Depth 5 + if ($ReportPath) { $json | Set-Content -LiteralPath $ReportPath } + Write-Output $json +} diff --git a/src/tests/experimental/native/UWalletFooterChecks.pas b/src/tests/experimental/native/UWalletFooterChecks.pas new file mode 100644 index 000000000..1832f1443 --- /dev/null +++ b/src/tests/experimental/native/UWalletFooterChecks.pas @@ -0,0 +1,164 @@ +unit UWalletFooterChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Native footer checks include the status panel's painted bevel. The status + window's ClientRect alone includes that border and is not safe child space. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses Classes, Types, Controls, ComCtrls; + +type + TWalletFooterGeometry = record + ClientRect, PanelRect, ContentRect, ToolbarRect: TRect; + HorizontalBorder, VerticalBorder, PanelSpacing: Integer; + function Description: String; + end; + +function ReadWalletFooterGeometry(AStatus: TStatusBar; AToolbar: TToolBar): TWalletFooterGeometry; +procedure RequireWalletFooterContent(const AGeometry: TWalletFooterGeometry; const ACheckpoint: String); +// Uses the real native control paint; returns protected border pixels changed +// by the toolbar. The caller can save a complete form image independently. +function CheckWalletFooterPaint(AStatus: TStatusBar; AToolbar: TToolBar; + const AOutputFile: String): Integer; + +implementation + +uses + Windows, Messages, CommCtrl, SysUtils, Forms, Graphics, Themes, + {$IFDEF FPC}LMessages,{$ENDIF} Math; + +function RectText(const ARect: TRect): String; +begin + Result := Format('%d,%d,%d,%d', [ARect.Left, ARect.Top, ARect.Right, ARect.Bottom]); +end; + +function TWalletFooterGeometry.Description: String; +begin + Result := Format('client=%s panel=%s content=%s toolbar=%s borders=%d,%d,%d', + [RectText(ClientRect), RectText(PanelRect), RectText(ContentRect), RectText(ToolbarRect), + HorizontalBorder, VerticalBorder, PanelSpacing]); +end; + +function ReadWalletFooterGeometry(AStatus: TStatusBar; AToolbar: TToolBar): TWalletFooterGeometry; +var + LBorders: array[0..2] of Integer; + LInset, LThemed: TRect; + LDetails: TThemedElementDetails; +begin + FillChar(LBorders, SizeOf(LBorders), 0); + Result := Default(TWalletFooterGeometry); + Result.ClientRect := AStatus.ClientRect; + if AStatus.Panels.Count = 0 then raise Exception.Create('Footer has no native status panels'); + if SendMessage(AStatus.Handle, SB_GETRECT, AStatus.Panels.Count - 1, LPARAM(@Result.PanelRect)) = 0 then + raise Exception.Create('Cannot query native last status panel'); + SendMessage(AStatus.Handle, SB_GETBORDERS, 0, LPARAM(@LBorders[0])); + Result.HorizontalBorder := LBorders[0]; + Result.VerticalBorder := LBorders[1]; + Result.PanelSpacing := LBorders[2]; + Result.ContentRect := Result.PanelRect; + {$IFDEF FPC} + LDetails := ThemeServices.GetElementDetails(tsPane); + LThemed := ThemeServices.ContentRect(0, LDetails, Result.PanelRect); + {$ELSE} + LDetails := StyleServices.GetElementDetails(tsPane); + if not StyleServices.GetElementContentRect(0, LDetails, Result.PanelRect, LThemed) then + LThemed := Result.PanelRect; + {$ENDIF} + IntersectRect(Result.ContentRect, Result.ContentRect, LThemed); + // Both native widgetsets paint the pane bevel on SB_GETRECT before drawing + // its contents; VCL's own owner-draw path explicitly insets this by one. + if AStatus.Panels[AStatus.Panels.Count - 1].Bevel <> pbNone then begin + LInset := Result.PanelRect; + InflateRect(LInset, -1, -1); + IntersectRect(Result.ContentRect, Result.ContentRect, LInset); + end; + GetWindowRect(AToolbar.Handle, Result.ToolbarRect); + MapWindowPoints(0, AStatus.Handle, Result.ToolbarRect, 2); +end; + +procedure RequireWalletFooterContent(const AGeometry: TWalletFooterGeometry; const ACheckpoint: String); +begin + with AGeometry do + if (ToolbarRect.Left < ContentRect.Left) or (ToolbarRect.Top < ContentRect.Top) or + (ToolbarRect.Right > ContentRect.Right) or (ToolbarRect.Bottom > ContentRect.Bottom) then + raise Exception.Create(ACheckpoint + ': footer paints over status-panel content/border; ' + Description); +end; + +function PaintStatus(AStatus: TStatusBar; AToolbar: TToolBar): TBitmap; +var + LOrigin: TPoint; + {$IFNDEF FPC}LForm: TCustomForm; LWhole: TBitmap;{$ELSE}LSavedDC: Integer; LToolbar: TRect;{$ENDIF} +begin + Result := TBitmap.Create; + Result.PixelFormat := pf24bit; + Result.SetSize(AStatus.ClientWidth, AStatus.ClientHeight); + Result.Canvas.Brush.Color := clFuchsia; + Result.Canvas.FillRect(Types.Rect(0, 0, Result.Width, Result.Height)); + {$IFNDEF FPC} + LForm := GetParentForm(AStatus); + LOrigin := Types.Point(0, 0); + MapWindowPoints(AStatus.Handle, LForm.Handle, LOrigin, 1); + LWhole := LForm.GetFormImage; + try + LWhole.PixelFormat := pf24bit; + Result.Canvas.CopyRect(Types.Rect(0, 0, Result.Width, Result.Height), LWhole.Canvas, + Types.Rect(LOrigin.X, LOrigin.Y, LOrigin.X + Result.Width, LOrigin.Y + Result.Height)); + finally LWhole.Free; end; + {$ELSE} + // Native status painting omits the LCL toolbar's graphic children. Invoke + // its real paint handler into the same DC at the native child coordinates. + SendMessage(AStatus.Handle, WM_PRINT, WPARAM(Result.Canvas.Handle), PRF_CLIENT or PRF_ERASEBKGND); + if AToolbar.Visible then begin + GetWindowRect(AToolbar.Handle, LToolbar); + MapWindowPoints(0, AStatus.Handle, LToolbar, 2); + LSavedDC := SaveDC(Result.Canvas.Handle); + try + SetViewportOrgEx(Result.Canvas.Handle, LToolbar.Left, LToolbar.Top, nil); + IntersectClipRect(Result.Canvas.Handle, 0, 0, AToolbar.Width, AToolbar.Height); + AToolbar.Perform(LM_PAINT, WPARAM(Result.Canvas.Handle), 0); + finally RestoreDC(Result.Canvas.Handle, LSavedDC); end; + end; + {$ENDIF} +end; + +function CheckWalletFooterPaint(AStatus: TStatusBar; AToolbar: TToolBar; + const AOutputFile: String): Integer; +var + LGeometry: TWalletFooterGeometry; + LWithToolbar, LWithoutToolbar: TBitmap; + X, Y, LInsideChanges: Integer; + LVisible: Boolean; +begin + LGeometry := ReadWalletFooterGeometry(AStatus, AToolbar); + LWithToolbar := PaintStatus(AStatus, AToolbar); + LWithoutToolbar := nil; + LVisible := AToolbar.Visible; + try + LWithToolbar.SaveToFile(AOutputFile); + AToolbar.Visible := false; + LWithoutToolbar := PaintStatus(AStatus, AToolbar); + Result := 0; LInsideChanges := 0; + for Y := 0 to LWithToolbar.Height - 1 do + for X := Max(0, LGeometry.ToolbarRect.Left) to Min(LWithToolbar.Width - 1, LGeometry.ToolbarRect.Right - 1) do + if LWithToolbar.Canvas.Pixels[X, Y] <> LWithoutToolbar.Canvas.Pixels[X, Y] then begin + if (Y < LGeometry.ContentRect.Top) or (Y >= LGeometry.ContentRect.Bottom) then Inc(Result) + else Inc(LInsideChanges); + end; + if LInsideChanges < 10 then + raise Exception.Create('Footer paint capture did not include actual toolbar icons'); + finally + AToolbar.Visible := LVisible; + LWithToolbar.Free; + LWithoutToolbar.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/UWalletKeysIconChecks.pas b/src/tests/experimental/native/UWalletKeysIconChecks.pas new file mode 100644 index 000000000..ddf0cb830 --- /dev/null +++ b/src/tests/experimental/native/UWalletKeysIconChecks.pas @@ -0,0 +1,155 @@ +unit UWalletKeysIconChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Checks the actual Wallet Keys button on a fresh, offline test wallet. The + caller supplies a directory owned by its private-desktop test run. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses Classes; + +// Requires an empty wallet without a password and restores that state on exit. +// Saves each button through its native paint handler and returns pixel counts. +function CheckWalletKeysIcons(AOwner: TComponent; const AOutputDirectory: String): String; + +implementation + +uses + Windows, Messages, SysUtils, Types, Forms, Graphics, UWallet, UConst, UFRMWalletKeys, + UCoreUtils, UAccounts, UBaseTypes, UPCDataTypes, + {$IFDEF FPC}LMessages,{$ENDIF} + {$IFDEF FPC}Generics.Defaults{$ELSE}System.Generics.Defaults{$ENDIF}; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create('Wallet Keys icons: ' + AMessage); +end; + +function CheckWalletKeysIcons(AOwner: TComponent; const AOutputDirectory: String): String; +var + LForm: TFRMWalletKeys; + LOutput: String; + + function CaptureButton: TBitmap; + begin + Result := TBitmap.Create; + try + Result.PixelFormat := pf24bit; + Result.SetSize(LForm.btnLock.Width, LForm.btnLock.Height); + Result.Canvas.Brush.Color := LForm.Color; + Result.Canvas.FillRect(Rect(0, 0, Result.Width, Result.Height)); + // TGraphicControl's paint message uses the supplied DC and invokes the + // real native speed-button Paint. LCL GetFormImage is black on a hidden + // Windows desktop, so do not use a screen copy as pixel evidence. + LForm.btnLock.Perform({$IFDEF FPC}LM_PAINT{$ELSE}WM_PAINT{$ENDIF}, + WPARAM(Result.Canvas.Handle), 0); + except + Result.Free; + raise; + end; + end; + + function CheckState(const AName, ACaption: String; AEnabled: Boolean; + AImageIndex: Integer): Integer; + var + LWithIcon, LWithoutIcon: TBitmap; + X, Y, LSavedIndex: Integer; + begin + // Deliver the same refresh message posted by the real wallet-change event. + SendMessage(LForm.Handle, CM_PC_WalletChanged, 0, 0); + Require(LForm.lblKeysEncrypted.Caption = ACaption, AName + ': incorrect state caption'); + Require(LForm.btnLock.Enabled = AEnabled, AName + ': incorrect enabled state'); + Require(LForm.ilIcons.Count = 2, AName + ': expected both original lock images'); + Require((LForm.ilIcons.Width = 64) and (LForm.ilIcons.Height = 64), + AName + ': changed image dimensions'); + Require(LForm.btnLock.Images = LForm.ilIcons, AName + ': native image-list binding missing'); + Require(LForm.btnLock.ImageIndex = AImageIndex, AName + ': incorrect lock image'); + Require(LForm.btnLock.Glyph.Empty, AName + ': unexpected legacy bitmap glyph'); + Require(LForm.btnLock.NumGlyphs = 1, AName + ': unexpected glyph frame count'); + + LWithIcon := CaptureButton; + LWithoutIcon := nil; + LSavedIndex := LForm.btnLock.ImageIndex; + try + LWithIcon.PixelFormat := pf24bit; + LWithIcon.SaveToFile(LOutput + AName + '-button.bmp'); + // Compare against the same native button without its image. Keeping the + // image-list binding intact avoids changing its native rendering owner. + LForm.btnLock.ImageIndex := -1; + LWithoutIcon := CaptureButton; + LWithoutIcon.PixelFormat := pf24bit; + Result := 0; + for Y := 6 to LForm.btnLock.Height - 7 do + for X := 6 to LForm.btnLock.Width - 7 do + if LWithIcon.Canvas.Pixels[X, Y] <> LWithoutIcon.Canvas.Pixels[X, Y] then + Inc(Result); + Require(Result >= 100, AName + ': padlock is visually blank'); + finally + LForm.btnLock.ImageIndex := LSavedIndex; + LWithIcon.Free; + LWithoutIcon.Free; + end; + end; + +var + LDisabled, LUnlocked, LLocked, LReopened: Integer; + LKey: TAccountKey; + LExpected: UInt32; + I: Integer; +begin + Require((TWallet.Keys.Count = 0) and not TWallet.Keys.HasPassword, + 'requires a fresh empty wallet without a password'); + LOutput := IncludeTrailingPathDelimiter(AOutputDirectory); + Require(DirectoryExists(LOutput), 'output directory does not exist'); + // A native Delphi string hash may be negative. Exercise its high bit with + // range checks enabled rather than relying on a randomly generated key. + LKey := Default(TAccountKey); + LKey.EC_OpenSSL_NID := CT_Default_EC_OpenSSL_NID; + LExpected := 0; + for I := 0 to 4095 do begin + LKey.x.FromString('Native account hash regression ' + IntToStr(I)); + LExpected := UInt32(TEqualityComparer<AnsiString>.Default.GetHashCode( + IntToStr(LKey.EC_OpenSSL_NID) + LKey.x.ToString + LKey.y.ToString)); + if (LExpected and $80000000) <> 0 then + Break; + end; + Require((LExpected and $80000000) <> 0, 'high-bit string hash fixture not found'); + Require(TAccountKeyEqualityComparer.CalcHashCode(LKey) = LExpected, + 'account-key comparer did not preserve the signed string hash bits'); + LForm := TFRMWalletKeys.Create(AOwner); + try + LForm.Show; + LDisabled := CheckState('wallet-keys-no-password', 'Wallet has no password', false, 1); + TWallet.GenerateNewKey('Disposable native icon QA key', CT_Default_EC_OpenSSL_NID); + TWallet.Keys.WalletPassword := 'Disposable native icon QA password'; + Require(TWallet.Keys.HasPassword and TWallet.Keys.IsValidPassword, 'encryption failed'); + LUnlocked := CheckState('wallet-keys-unlocked', 'Wallet is unlocked', true, 1); + Require(TWallet.Keys.LockWallet, 'locking failed'); + LLocked := CheckState('wallet-keys-locked', 'Wallet is locked', true, 0); + TWallet.Keys.WalletPassword := 'Disposable native icon QA password'; + Require(TWallet.Keys.IsValidPassword, 'unlocking failed'); + LReopened := CheckState('wallet-keys-unlocked-again', 'Wallet is unlocked', true, 1); + Result := Format('WALLET KEYS ICONS: native images=2, 64x64, painted pixels disabled=%d, unlocked=%d, locked=%d, reopened=%d; high-bit hash=%s', + [LDisabled, LUnlocked, LLocked, LReopened, IntToHex(LExpected, 8)]); + finally + LForm.Free; + // Delete persists each removal in the owned QA file, including an error + // raised while the encrypted wallet was locked. + while TWallet.Keys.Count > 0 do + TWallet.Keys.Delete(TWallet.Keys.Count - 1); + TWallet.Keys.Clear; + TWallet.Keys.WalletPassword := ''; + Require((TWallet.Keys.Count = 0) and not TWallet.Keys.HasPassword, + 'did not restore the original empty wallet'); + end; +end; + +end. diff --git a/src/tests/experimental/native/UWalletSettingsStatusChecks.pas b/src/tests/experimental/native/UWalletSettingsStatusChecks.pas new file mode 100644 index 000000000..3e001f1df --- /dev/null +++ b/src/tests/experimental/native/UWalletSettingsStatusChecks.pas @@ -0,0 +1,91 @@ +unit UWalletSettingsStatusChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Settings-only footer regression using the real controller subscription order. + Distributed under the MIT software license; see the accompanying LICENSE. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UFRMMainForm; + +// Requires a fresh owned process with networking restricted to the unavailable +// fixed loopback peer, an ephemeral listener, and RPC/mining disabled. +function CheckSettingsOnlyPortStatus(AMain: TFRMMainForm): String; + +implementation + +uses Classes, SysUtils, UCommon, USettings, UUserInterface, UNetProtocol; + +type + TSettingsStatusObserver = class + public + StateEvents, StatisticsEvents: Integer; + procedure StateChanged(Sender: TObject); + procedure StatisticsChanged(Sender: TObject); + end; + +procedure TSettingsStatusObserver.StateChanged(Sender: TObject); +begin + Inc(StateEvents); +end; + +procedure TSettingsStatusObserver.StatisticsChanged(Sender: TObject); +begin + Inc(StatisticsEvents); +end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('Settings-only footer: ' + AMessage); +end; + +function CheckSettingsOnlyPortStatus(AMain: TFRMMainForm): String; +var + LObserver: TSettingsStatusObserver; + LPort: Word; + LState: TUserInterfaceState; + LCaption: String; +begin + Require(TUserInterface.Started and not TUserInterface.Offline, 'requires the actual online settings branch'); + Require(TSettings.TryConnectOnlyWithThisFixedServers = '127.0.0.1:1', 'fixed loopback peer policy is missing'); + Require(TSettings.PeerCache = '', 'requires an empty saved peer cache'); + Require(not TSettings.JsonRpcPortEnabled and not TSettings.JsonRpcMinerServerActive, 'RPC/mining must be disabled'); + Require(TNetData.NetData.NetStatistics.ActiveConnections = 0, 'requires zero connected peers'); + Require(TUserInterface.Node.NetServer.Active, 'requires an active owned ephemeral listener'); + LPort := TSettings.InternetServerPort; + Require(LPort = 0, 'requires the configured ephemeral listener port'); + LState := TUserInterface.State; + LObserver := TSettingsStatusObserver.Create; + TUserInterface.StateChanged.Add(LObserver.StateChanged); + TUserInterface.NetStatisticsChanged.Add(LObserver.StatisticsChanged); + try + // Do not pump messages or invoke any UI event here. The controller's + // settings receiver must apply the value before MainForm reads it. + TSettings.InternetServerPort := 43219; + TSettings.Save; + LCaption := AMain.sbStatusBar.Panels[0].Text; + Require(TUserInterface.Node.NetServer.Port = 43219, 'controller did not apply the new configured port'); + Require(LCaption = 'Active (port: 43219)', 'native footer retained the old listener port'); + Require(TUserInterface.State = LState, 'the test changed UI enum state'); + Require((LObserver.StateEvents = 0) and (LObserver.StatisticsEvents = 0), + 'another state/statistics notification masked the settings-only update'); + TSettings.InternetServerPort := LPort; + TSettings.Save; + Require(AMain.sbStatusBar.Panels[0].Text = 'Active (port: 0)', 'restoring settings did not update the native footer'); + Require((TUserInterface.State = LState) and (LObserver.StateEvents = 0) and + (LObserver.StatisticsEvents = 0), 'restoring settings required another UI event'); + Result := 'SETTINGS-ONLY PORT: 0 -> 43219 -> 0; immediate native footer; unchanged state; zero state/statistics callbacks; no message/timer dispatch'; + finally + TUserInterface.StateChanged.Remove(LObserver.StateChanged); + TUserInterface.NetStatisticsChanged.Remove(LObserver.StatisticsChanged); + LObserver.Free; + if TSettings.InternetServerPort <> LPort then begin + TSettings.InternetServerPort := LPort; + TSettings.Save; + end; + end; +end; + +end. diff --git a/src/tests/experimental/native/UWalletSyncDisplayChecks.pas b/src/tests/experimental/native/UWalletSyncDisplayChecks.pas new file mode 100644 index 000000000..4e1d20dea --- /dev/null +++ b/src/tests/experimental/native/UWalletSyncDisplayChecks.pas @@ -0,0 +1,209 @@ +unit UWalletSyncDisplayChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Native first-show regression for the real synchronization control. + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$I ..\..\..\config.inc} + +interface + +uses UFRMMainForm; + +// The caller supplies the fresh offline wallet main form, already in wallet +// mode. Only the real queued mode handler is delivered; no periodic timer is +// pumped between seeding the old design captions and checking the first frame. +function CheckSyncFirstDisplay(AMain: TFRMMainForm): String; +function CheckSyncWriterIndependence(AMain: TFRMMainForm): String; + +implementation + +uses Windows, SysUtils, Classes, Forms, SyncObjs, UCommon, UUserInterface, UNode, + UThread, UBlockChain, UCTRLSyncronization, + {$IFDEF USE_ABSTRACTMEM_BLOCKCHAIN_STORAGE}UAbstractMemBlockchainStorage{$ELSE}UFileStorage{$ENDIF}; + +type + TGatedSync = class(TCTRLSyncronization) + protected + procedure ReadSecondarySnapshot(ANode: TNode; out ASnapshot: TSyncNodeSnapshot); override; + public + Entered, ReleaseRead: TEvent; + ReadCalls: LongInt; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + end; + TCoreWriter = class(TPCThread) + private FNode: TNode; + protected procedure BCExecute; override; + public + Completed: TEvent; + ErrorMessage: String; + constructor Create(ANode: TNode); + destructor Destroy; override; + end; + +constructor TGatedSync.Create(AOwner: TComponent); +begin + inherited; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); +end; + +destructor TGatedSync.Destroy; +begin + ReleaseRead.SetEvent; + inherited; + Entered.Free; + ReleaseRead.Free; +end; + +procedure TGatedSync.ReadSecondarySnapshot(ANode: TNode; out ASnapshot: TSyncNodeSnapshot); +begin + InterlockedIncrement(ReadCalls); + Entered.SetEvent; + while ReleaseRead.WaitFor(10) <> wrSignaled do + if TThread.CheckTerminated then Exit; + inherited; +end; + +constructor TCoreWriter.Create(ANode: TNode); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + Completed := TEvent.Create(nil, True, False, ''); +end; + +destructor TCoreWriter.Destroy; +begin + WaitFor; + Completed.Free; + inherited; +end; + +procedure TCoreWriter.BCExecute; +var L: TPCOperationsComp; +begin + try + try + L := FNode.LockMempoolWrite; + try + if not Assigned(L) then raise Exception.Create('No actual mempool'); + finally FNode.UnlockMempoolWrite; end; + except on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; end; + finally Completed.SetEvent; end; +end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('Sync first display: ' + AMessage); +end; + +function CheckSyncFirstDisplay(AMain: TFRMMainForm): String; +var LStarted, LDeadline: UInt64; LAverage: String; S: TNodeSynchronizationStatus; +begin + Require(AMain.Mode = wmWallet, 'requires the hidden synchronization view'); + // Exercise the actual startup configuration, not a replacement test reader. + {$IFDEF USE_ABSTRACTMEM_BLOCKCHAIN_STORAGE} + Require(TUserInterface.Node.Bank.Storage is TAbstractMemBlockchainStorageSecondary, + 'experimental startup did not select Classic''s configured indexed reader'); + Require(TAbstractMemBlockchainStorage(TUserInterface.Node.Bank.Storage).AutoFlushCache, + 'startup left automatic index flushing disabled after loading'); + {$ELSE} + Require(TUserInterface.Node.Bank.Storage is TFileStorage, + 'startup did not retain the configured raw-file fallback'); + {$ENDIF} + {$IFDEF USE_ABSTRACTMEM} + Require(TUserInterface.Node.Bank.SafeBox.PCAbstractMem.AccountCache.MaxRegisters = 250000, + 'experimental startup did not apply its bounded account-cache budget'); + {$ENDIF} + S := TUserInterface.SynchronizationStatus; + Require((S.LocalBlockCount = 0) and S.HasMempoolSummary, 'requires fresh initialized cached data'); + LAverage := AMain.SyncControl.lblTimeAverage.Caption; + AMain.SyncControl.lblTotalBlocksValue.Caption := '000'; + AMain.SyncControl.lblTotalAccountsValue.Caption := '000'; + AMain.SyncControl.lblPendingOperationsValue.Caption := '000'; + AMain.SyncControl.lblBlockTargetValue.Caption := '000'; + AMain.SyncControl.lblTimeAverage.Caption := '000'; + LStarted := GetTickCount64; + TUserInterface.ShowSyncDialog; + AMain.Perform(CM_PC_ModeChanged, 0, 0); + Require(GetTickCount64 - LStarted < 500, 'show blocked instead of rendering the available snapshot'); + Require(AMain.SyncControl.IsVisible, 'the real mode handler did not show the control'); + Require(AMain.SyncControl.lblTotalBlocksValue.Caption = '(none)', 'first frame retained the block designer caption'); + Require(AMain.SyncControl.lblTotalAccountsValue.Caption = '0', 'first frame retained the account designer caption'); + Require(AMain.SyncControl.lblPendingOperationsValue.Caption = '0', 'first frame retained the pending designer caption'); + Require(Length(AMain.SyncControl.lblBlockTargetValue.Caption) = 8, 'first frame did not render the real compact target'); + Require((AMain.SyncControl.lblTimeAverage.Caption = LAverage) or + (AMain.SyncControl.lblTimeAverage.Caption = 'Calculating...'), + 'first frame did not retain cached or pending secondary statistics'); + // The first frame above was checked without pumping. A missing initial + // secondary result must then really publish, rather than remain Calculating. + LDeadline := GetTickCount64 + 2000; + while (Pos('Last 100:', AMain.SyncControl.lblTimeAverage.Caption) <> 1) and + (GetTickCount64 < LDeadline) do begin + CheckSynchronize(0); + Application.ProcessMessages; + Sleep(1); + end; + Require(Pos('Last 100:', AMain.SyncControl.lblTimeAverage.Caption) = 1, + 'Initial secondary statistics never published'); + LAverage := AMain.SyncControl.lblTimeAverage.Caption; + AMain.SyncControl.lblTimeAverage.Caption := '000'; + TUserInterface.ShowWallet; + AMain.Perform(CM_PC_ModeChanged, 0, 0); + TUserInterface.ShowSyncDialog; + AMain.Perform(CM_PC_ModeChanged, 0, 0); + Require(AMain.SyncControl.lblTimeAverage.Caption = LAverage, + 'Re-entering the view did not immediately restore its completed averages'); + Result := Format('SYNC FIRST FRAME: synchronous actual mode/show, %d ms; blocks=%s accounts=%s pending=%s target=%s', + [GetTickCount64 - LStarted, AMain.SyncControl.lblTotalBlocksValue.Caption, + AMain.SyncControl.lblTotalAccountsValue.Caption, AMain.SyncControl.lblPendingOperationsValue.Caption, + AMain.SyncControl.lblBlockTargetValue.Caption]); +end; + +function CheckSyncWriterIndependence(AMain: TFRMMainForm): String; +var C: TGatedSync; W: TCoreWriter; T, Deadline: UInt64; +begin + C := TGatedSync.Create(AMain); + W := nil; + try + C.RefreshNodeState; + Require(C.Entered.WaitFor(2000) = wrSignaled, 'Actual secondary worker did not reach its read delegate'); + W := TCoreWriter.Create(TUserInterface.Node); + T := GetTickCount64; + W.Start; + Require(W.Completed.WaitFor(1000) = wrSignaled, + 'Actual core writer waited for the gated UI snapshot to release its mempool read'); + Require(W.ErrorMessage = '', W.ErrorMessage); + Require(C.ReleaseRead.WaitFor(0) <> wrSignaled, 'Snapshot gate was released before core progress'); + Result := Format('REVERSE INDEPENDENCE: actual core writer completed in %d ms while actual secondary snapshot remained gated', + [GetTickCount64 - T]); + C.ReleaseRead.SetEvent; + Deadline := GetTickCount64 + 2000; + repeat + CheckSynchronize(0); + Application.ProcessMessages; + if Pos('Last 100:', C.lblTimeAverage.Caption) = 1 then Break; + Sleep(1); + until GetTickCount64 >= Deadline; + Require(Pos('Last 100:', C.lblTimeAverage.Caption) = 1, 'Real bank read did not publish after gate release'); + C.RefreshNodeState; + TUserInterface.SynchronizationChanged.Invoke(nil); + Deadline := GetTickCount64 + 200; + repeat CheckSynchronize(0); Application.ProcessMessages; Sleep(1); + until GetTickCount64 >= Deadline; + Require(InterlockedCompareExchange(C.ReadCalls, 0, 0) = 1, + 'Same-tip status or view entry repeated secondary bank reads'); + finally + // Release prerequisites before joining on failed RED checks as well. + C.ReleaseRead.SetEvent; + W.Free; + C.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/WalletStartupProbe.lpr b/src/tests/experimental/native/WalletStartupProbe.lpr new file mode 100644 index 000000000..a5fd0d625 --- /dev/null +++ b/src/tests/experimental/native/WalletStartupProbe.lpr @@ -0,0 +1,1392 @@ +program WalletStartupProbe; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Actual wallet startup checks on a private Windows desktop with fresh temp data. + The probe refuses the input desktop, normal wallet folders and online mode. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$mode delphi} + +uses + Interfaces, Forms, Controls, ExtCtrls, StdCtrls, ComCtrls, Graphics, Windows, SysUtils, Classes, StrUtils, + UWalletStartup, UWalletStartupTask, UUserInterface, UFRMMainForm, UCTRLWallet, UCTRLSyncronization, + USettings, UWallet, UNode, UNetProtocol, UAppParams, UConst, UCommon, UMemory, UThread, SyncObjs, + UFRMMessages, UFRMLogs, UFRMBlockExplorer, UFRMAccountExplorer, UFRMOperationsExplorer, + UCTRLBanner, UVisualGrid, UWalletKeysIconChecks, UWalletFooterChecks, UWalletSyncDisplayChecks, + UAccounts, UProgressDisplay; + +type + + TPanelAccess = class(TPanel) + public + function ContentRect : TRect; + end; + + TVisualGridAccess = class(TVisualGrid); + TMainFormAccess = class(TFRMMainForm); + + TSlowStartupTask = class(TWalletStartupTask) + private + FEntered : TEvent; + protected + procedure ExecuteLoad; override; + public + constructor Create; + destructor Destroy; override; + end; + + TBusyMempoolThread = class(TPCThread) + private + FNode : TNode; + FWatchdogReleased : LongInt; + protected + procedure BCExecute; override; + public + Entered, ReleaseLock, Released : TEvent; + constructor Create(ANode : TNode); + destructor Destroy; override; + function WatchdogReleased : Boolean; + end; + + TStartupHarness = class(TComponent) + private + FDisposables : TDisposables; + FLog : TStringList; + FLogPath, FMode, FDataDirectory : String; + FMainForm : TFRMMainForm; + FTimer : TTimer; + FModalTimer : TTimer; + FModalSeen : Boolean; + FModalDeadline : QWord; + FModalFailure : String; + FLoadedCount, FPhase : Integer; + FProgressSeen : Boolean; + FLastProgress : TProgressInfo; + FSnapshotNotifications : Integer; + FDeadline : QWord; + FInsideTick, FExited : Boolean; + FFailure : String; + FHost : HWND; + FHostLeft, FHostTop : Integer; + FOriginalMinHeight, FOriginalMaxHeight : Integer; + FOriginalMinWidth, FOriginalMaxWidth : Integer; + FOriginalBorderIcons : TBorderIcons; + FSlowTask : TSlowStartupTask; + FQuitBusy : TBusyMempoolThread; + FFooterConnectivityClick, FFooterSyncClick : TNotifyEvent; + FFooterConnectivityClicks, FFooterSyncClicks : Integer; + procedure FooterConnectivityClick(Sender : TObject); + procedure FooterSyncClick(Sender : TObject); + procedure Log(const AText : String); + procedure Require(ACondition : Boolean; const AMessage : String); + procedure OnLoaded(Sender : TObject); + procedure OnLoading(Sender : TObject; const AProgress : TProgressInfo); + procedure OnAppStarted(Sender : TObject); + procedure OnException(Sender : TObject; E : Exception); + procedure Tick(Sender : TObject); + procedure InspectWorkbench(Sender : TObject); + procedure InspectQuit(Sender : TObject); + procedure CheckOffline(const ACheckpoint : String); + procedure CheckNative(const ACheckpoint : String; AMode : TFRMMainFormMode; + ARequireRestoredPosition : Boolean = true); + procedure CheckFrameLayout(const ACheckpoint : String; ARequireRestoredPosition : Boolean); + procedure CheckFooterActions; + procedure CheckFooterDpiCycle; + procedure CheckResizeCycle(const ACheckpoint : String; AMode : TFRMMainFormMode; + ALoading : Boolean = false); + procedure PumpWindowChanges; + procedure CheckModelessFocus; + procedure CheckBusyQuit; + procedure CheckClassicIntegration; + procedure CheckBusyStatisticsRefresh; + procedure CheckEventDrivenStatuses; + procedure OnSyncSnapshotChanged(Sender: TObject); + procedure CheckShutdown; + procedure SeedSettings; + procedure StopWallet; + public + constructor Create(const AMode, ALogPath, ADataDirectory : String); + destructor Destroy; override; + procedure Run(const AOptions : TWalletStartupOptions); + end; + +var + GAllowedWindow, GUnexpectedWindow : HWND; + GUnexpectedClass, GUnexpectedCaption : String; + GSlowLoadCancelled : Boolean; + +constructor TSlowStartupTask.Create; +begin + inherited Create; + FEntered := TEvent.Create(nil, true, false, ''); +end; + +destructor TSlowStartupTask.Destroy; +begin + CancelAndWait; + FEntered.Free; + inherited; +end; + +procedure TSlowStartupTask.ExecuteLoad; +var + LNode : TNode; + LProgress : TProgressInfo; +begin + LNode := TUserInterface.Node; + try + LNode.LockMempoolWrite; + try + LNode.Bank.SafeBox.StartThreadSafe; + try + // Real restore events carry block metadata as well as callback counters. + LProgress := TProgressInfo.Create(psReadingBlockchain, 1, 100); + LProgress.BlockNumber := 1; + LProgress.BlockCount := 100; + ReportProgress(LProgress); + FEntered.SetEvent; + while not Cancelled do Sleep(10); + CheckCancelled; + finally + LNode.Bank.SafeBox.EndThreadSave; + end; + finally + LNode.UnlockMempoolWrite; + end; + finally + GSlowLoadCancelled := Cancelled; + end; +end; + +constructor TBusyMempoolThread.Create(ANode : TNode); +begin + FNode := ANode; + Entered := TEvent.Create(nil, true, false, ''); + ReleaseLock := TEvent.Create(nil, true, false, ''); + Released := TEvent.Create(nil, true, false, ''); + inherited Create(true); + FreeOnTerminate := false; + Start; +end; + +destructor TBusyMempoolThread.Destroy; +begin + ReleaseLock.SetEvent; + WaitFor; + Entered.Free; + ReleaseLock.Free; + Released.Free; + inherited; +end; + +function TBusyMempoolThread.WatchdogReleased : Boolean; +begin + Result := InterlockedCompareExchange(FWatchdogReleased, 0, 0) <> 0; +end; + +procedure TBusyMempoolThread.BCExecute; +begin + if not FNode.TryLockNode(1000) then begin + Released.SetEvent; + Exit; + end; + try + Entered.SetEvent; + // A broken UI refresh must fail the probe instead of hanging its desktop. + if ReleaseLock.WaitFor(2000) <> wrSignaled then + InterlockedExchange(FWatchdogReleased, 1); + finally + FNode.UnlockNode; + Released.SetEvent; + end; +end; + +function DesktopName(ADesktop : HDESK) : String; +var + LName : array[0..255] of Char; + LNeeded : DWORD; +begin + FillChar(LName, SizeOf(LName), 0); + if not GetUserObjectInformation(ADesktop, UOI_NAME, @LName[0], SizeOf(LName), LNeeded) then + RaiseLastOSError; + Result := StrPas(@LName[0]); +end; + +function FindUnexpectedWindow(AWindow : HWND; AParameter : LPARAM) : WINBOOL; stdcall; +var + LProcessId : DWORD; + LWindowClass : array[0..127] of Char; + LWindowCaption : array[0..255] of Char; +begin + Result := true; + LProcessId := 0; + GetWindowThreadProcessId(AWindow, @LProcessId); + if (LProcessId <> GetCurrentProcessId) or (not Windows.IsWindowVisible(AWindow)) or + (AWindow = GAllowedWindow) then Exit; + FillChar(LWindowClass, SizeOf(LWindowClass), 0); + GetClassName(AWindow, @LWindowClass[0], Length(LWindowClass)); + // Windows may create its own IME overlay in a GUI process on a private desktop. + if (StrPas(@LWindowClass[0]) = 'UAC_InputIndicatorOverlayWnd') or + (StrPas(@LWindowClass[0]) = 'UAC Input Indicator') then Exit; + GUnexpectedWindow := AWindow; + GUnexpectedClass := StrPas(@LWindowClass[0]); + FillChar(LWindowCaption, SizeOf(LWindowCaption), 0); + GetWindowText(AWindow, @LWindowCaption[0], Length(LWindowCaption)); + GUnexpectedCaption := StrPas(@LWindowCaption[0]); + Result := false; +end; + +function TPanelAccess.ContentRect : TRect; +begin + Result := ClientRect; + AdjustClientRect(Result); +end; + +constructor TStartupHarness.Create(const AMode, ALogPath, ADataDirectory : String); +begin + inherited Create(nil); + FMode := AMode; + FLogPath := ALogPath; + FDataDirectory := ADataDirectory; + FLog := FDisposables.AddObject(TStringList.Create) as TStringList; + FTimer := TTimer.Create(Self); + FTimer.Enabled := false; + FTimer.Interval := 50; + FTimer.OnTimer := Tick; + FModalTimer := TTimer.Create(Self); + FModalTimer.Enabled := false; + FModalTimer.Interval := 25; + FModalTimer.OnTimer := InspectWorkbench; +end; + +destructor TStartupHarness.Destroy; +begin + FTimer.Enabled := false; + FModalTimer.Enabled := false; + TUserInterface.Loaded.Remove(OnLoaded); + TUserInterface.AppStarted.Remove(OnAppStarted); + TUserInterface.Loading.Remove(OnLoading); + inherited; +end; + +procedure TStartupHarness.InspectWorkbench(Sender : TObject); +var + LWorkbench : TFRMOperationsExplorer; + i : Integer; +begin + LWorkbench := nil; + for i := 0 to Screen.FormCount - 1 do + if (Screen.Forms[i] is TFRMOperationsExplorer) and Screen.Forms[i].Visible then + LWorkbench := TFRMOperationsExplorer(Screen.Forms[i]); + if not Assigned(LWorkbench) then begin + if GetTickCount64 >= FModalDeadline then begin + FModalFailure := 'The composed RAW workbench did not enter its modal loop'; + FModalTimer.Enabled := false; + Application.Terminate; + end; + Exit; + end; + FModalTimer.Enabled := false; + try + Require(LWorkbench.SourceNode = TUserInterface.Node, 'Workbench received the wrong node'); + Require(LWorkbench.SourceWalletKeys = TWallet.Keys, 'Workbench received the wrong wallet keys'); + Require(LWorkbench.PopupParent = FMainForm, 'Workbench is not owned by its main popup host'); + Require(LWorkbench.Offline and (not LWorkbench.MiRPCCalls.Enabled) and + (not LWorkbench.ActExecuteOperation.Enabled), 'Workbench outbound actions bypassed offline policy'); + // A state refresh during a modal loop must not globally re-enable its host. + TUserInterface.StateChanged.Invoke(nil); + Require(not Windows.IsWindowEnabled(FMainForm.Handle), 'UI refresh broke modal host input ownership'); + FModalSeen := true; + except + on E : Exception do FModalFailure := E.Message; + end; + LWorkbench.ModalResult := mrCancel; +end; + +procedure TStartupHarness.Log(const AText : String); +begin + FLog.Add(IntToStr(GetTickCount64) + ' ' + AText); + FLog.SaveToFile(FLogPath); +end; + +procedure TStartupHarness.Require(ACondition : Boolean; const AMessage : String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure TStartupHarness.OnLoaded(Sender : TObject); +begin + Inc(FLoadedCount); + Log('EVENT Loaded ' + IntToStr(FLoadedCount)); +end; + +procedure TStartupHarness.OnLoading(Sender : TObject; const AProgress : TProgressInfo); +begin + Require(GetCurrentThreadID = MainThreadID, 'Typed progress escaped main-thread dispatch'); + FLastProgress := AProgress; + FProgressSeen := true; +end; + +procedure TStartupHarness.OnAppStarted(Sender : TObject); +begin + if FMode = 'close-before-load' then begin + Require(TUserInterface.State = uisLoading, 'Expected loading state in AppStarted'); + StopWallet; + end; +end; + +procedure TStartupHarness.OnException(Sender : TObject; E : Exception); +begin + FFailure := E.ClassName + ': ' + E.Message; + Log('UNHANDLED ' + FFailure); + FTimer.Enabled := false; + StopWallet; +end; + +procedure TStartupHarness.CheckOffline(const ACheckpoint : String); +begin + Require(TUserInterface.Offline, ACheckpoint + ': offline option lost'); + Require(not TNetData.NetData.NetConnectionsActive, ACheckpoint + ': connections enabled'); + Require(not TUserInterface.Node.NetServer.Active, ACheckpoint + ': peer listener enabled'); + Require(not TNetData.NetData.IsDiscoveringServers, ACheckpoint + ': discovery enabled'); + Require(TNetData.NetData.ConnectionsCountAll = 0, ACheckpoint + ': network connection created'); + Require(TNetData.NetData.NodeServersAddresses.Count = 0, ACheckpoint + ': cached peers supplied offline'); + Require(not TUserInterface.RPCServer.Active, ACheckpoint + ': RPC enabled'); + Require(Assigned(TUserInterface.PoolMiningServer) and + (not TUserInterface.PoolMiningServer.Active), ACheckpoint + ': mining enabled'); + Require(TSettings.JsonRpcPortEnabled, ACheckpoint + ': saved RPC preference overwritten'); + Require(TSettings.JsonRpcMinerServerActive, ACheckpoint + ': saved mining preference overwritten'); + Require(TSettings.PeerCache = '127.0.0.1:1', ACheckpoint + ': saved peers overwritten'); + Require(TWallet.Keys.Count = 0, ACheckpoint + ': offline startup generated a wallet key'); + Require(not FMainForm.tbtnConnectivity.Enabled, ACheckpoint + ': connectivity button enabled offline'); + Log('OFFLINE ' + ACheckpoint + ': peers=0, connections=0, listeners/RPC/mining inactive, saved preferences preserved'); +end; + +procedure TStartupHarness.CheckNative(const ACheckpoint : String; AMode : TFRMMainFormMode; + ARequireRestoredPosition : Boolean); +var + LActive, LInactive : TCustomForm; + LPanel : TPanel; + LHostRect, LChildRect, LContent : TRect; + LOrigin : TPoint; +begin + Require(FMainForm.Visible and FMainForm.Enabled, ACheckpoint + ': main form hidden or disabled'); + Require(GetWindowRect(FMainForm.Handle, LHostRect), ACheckpoint + ': no host rectangle'); + if FHost = 0 then begin + FHost := FMainForm.Handle; + FHostLeft := LHostRect.Left; + FHostTop := LHostRect.Top; + end; + Require(FMainForm.Handle = FHost, ACheckpoint + ': main HWND changed'); + if ARequireRestoredPosition then + Require((LHostRect.Left = FHostLeft) and (LHostRect.Top = FHostTop), + ACheckpoint + ': main form did not preserve its restored top-left'); + Require(FMainForm.Mode = AMode, ACheckpoint + ': wrong application mode'); + Require(not FMainForm.SyncControl.ShowSplash, ACheckpoint + ': synchronization reopened splash'); + Require(FMainForm.paWalletPanel.ControlCount = 1, ACheckpoint + ': wallet was not created once'); + if AMode = wmWallet then begin + LPanel := FMainForm.paWalletPanel; + LActive := LPanel.Controls[0] as TCTRLWallet; + LInactive := FMainForm.SyncControl; + end else begin + LPanel := FMainForm.paSyncPanel; + LActive := FMainForm.SyncControl; + LInactive := FMainForm.paWalletPanel.Controls[0] as TCTRLWallet; + end; + Require(Windows.IsWindowVisible(LActive.Handle), ACheckpoint + ': active screen hidden'); + Require(not Windows.IsWindowVisible(LInactive.Handle), ACheckpoint + ': inactive screen remained visible'); + Require(LActive.Parent = LPanel, ACheckpoint + ': wrong LCL screen parent'); + Require(Windows.GetParent(LActive.Handle) = LPanel.Handle, ACheckpoint + ': wrong native screen parent'); + Require((GetWindowLongPtr(LActive.Handle, GWL_STYLE) and WS_CHILD) <> 0, + ACheckpoint + ': active screen became a standalone popup'); + LContent := TPanelAccess(LPanel).ContentRect; + Require((LActive.Left = LContent.Left) and (LActive.Top = LContent.Top) and + (LActive.Width = LContent.Right - LContent.Left) and + (LActive.Height = LContent.Bottom - LContent.Top), ACheckpoint + ': logical screen bounds do not fill panel'); + LOrigin := Point(LContent.Left, LContent.Top); + Require(Windows.ClientToScreen(LPanel.Handle, LOrigin), ACheckpoint + ': no native panel origin'); + Require(GetWindowRect(LActive.Handle, LChildRect), ACheckpoint + ': no native child bounds'); + Require((LChildRect.Left = LOrigin.X) and (LChildRect.Top = LOrigin.Y), + ACheckpoint + ': native screen moved outside panel'); + Require((LChildRect.Right - LChildRect.Left = LContent.Right - LContent.Left) and + (LChildRect.Bottom - LChildRect.Top = LContent.Bottom - LContent.Top), + ACheckpoint + ': native screen size does not fill panel'); + GAllowedWindow := FHost; + GUnexpectedWindow := 0; + EnumWindows(@FindUnexpectedWindow, 0); + Require(GUnexpectedWindow = 0, ACheckpoint + ': unexpected visible top-level window ' + + IntToStr(GUnexpectedWindow) + ' class=' + GUnexpectedClass + ' caption=' + GUnexpectedCaption); + CheckFrameLayout(ACheckpoint, ARequireRestoredPosition); + Log(Format('NATIVE %s: stable HWND=%d at %d,%d; child=%d,%d,%d,%d; inactive hidden', + [ACheckpoint, FHost, LHostRect.Left, LHostRect.Top, LChildRect.Left, LChildRect.Top, + LChildRect.Right, LChildRect.Bottom])); +end; + +procedure TStartupHarness.PumpWindowChanges; +var LUntil: QWord; +begin + // Native size messages and existing delayed layout timers must both settle. + LUntil := GetTickCount64 + 100; + repeat + Application.ProcessMessages; + CheckSynchronize; + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TStartupHarness.CheckFrameLayout(const ACheckpoint : String; + ARequireRestoredPosition : Boolean); +var + LBanner: TCTRLBanner; + LHostRect, LBannerRect, LHeaderContent, LStatusRect, LToolbarRect: TRect; + LOrigin, LStatusOrigin: TPoint; + LStatusClient: TRect; + LButton: TToolButton; + LButtons: array[0..2] of TToolButton; + LFooterGeometry: TWalletFooterGeometry; + i: Integer; +begin + Require(GetWindowRect(FMainForm.Handle, LHostRect), ACheckpoint + ': no native main bounds'); + if FHost = 0 then begin + FHost := FMainForm.Handle; + FHostLeft := LHostRect.Left; + FHostTop := LHostRect.Top; + end; + Require(FMainForm.Handle = FHost, ACheckpoint + ': main HWND changed while resizing'); + if ARequireRestoredPosition then + Require((LHostRect.Left = FHostLeft) and (LHostRect.Top = FHostTop), + ACheckpoint + ': restored main position changed'); + Require((FMainForm.paLogoPanel.Left = 0) and (FMainForm.paLogoPanel.Top = 0) and + (FMainForm.paLogoPanel.Width = FMainForm.ClientWidth), + ACheckpoint + ': header does not span the form client width'); + Require(FMainForm.paLogoPanel.ControlCount = 1, ACheckpoint + ': missing or duplicate banner'); + Require(FMainForm.paLogoPanel.Controls[0] is TCTRLBanner, ACheckpoint + ': wrong header control'); + LBanner := TCTRLBanner(FMainForm.paLogoPanel.Controls[0]); + LHeaderContent := TPanelAccess(FMainForm.paLogoPanel).ContentRect; + Require((LBanner.Left = LHeaderContent.Left) and (LBanner.Top = LHeaderContent.Top) and + (LBanner.Width = LHeaderContent.Right - LHeaderContent.Left) and + (LBanner.Height = LHeaderContent.Bottom - LHeaderContent.Top), + ACheckpoint + ': banner leaves an uncovered header region'); + LOrigin := Point(LHeaderContent.Left, LHeaderContent.Top); + Require(Windows.ClientToScreen(FMainForm.paLogoPanel.Handle, LOrigin), + ACheckpoint + ': no native header origin'); + Require(GetWindowRect(LBanner.Handle, LBannerRect), ACheckpoint + ': no native banner bounds'); + Require((Windows.GetParent(LBanner.Handle) = FMainForm.paLogoPanel.Handle) and + (LBannerRect.Left = LOrigin.X) and (LBannerRect.Top = LOrigin.Y) and + (LBannerRect.Right - LBannerRect.Left = LBanner.Width) and + (LBannerRect.Bottom - LBannerRect.Top = LBanner.Height), + ACheckpoint + ': native banner does not fill its header host'); + Require((LBanner.imBackground.Left = 0) and (LBanner.imBackground.Top = 0) and + (LBanner.imBackground.Width = LBanner.ClientWidth) and + (LBanner.imBackground.Height = LBanner.ClientHeight), + ACheckpoint + ': background image leaves a blank header region'); + Require(Abs(LBanner.ClientWidth - LBanner.cbLanguage.Left - LBanner.cbLanguage.Width - + LBanner.imLogo.Left) <= 1, ACheckpoint + ': language selector lost its right inset'); + Require((LBanner.cbLanguage.Top >= 0) and + (LBanner.cbLanguage.Top + LBanner.cbLanguage.Height <= LBanner.ClientHeight) and + (Abs(2 * LBanner.cbLanguage.Top + LBanner.cbLanguage.Height - LBanner.ClientHeight) <= 1), + ACheckpoint + ': language selector is outside or off-center in header'); + Require((FMainForm.sbStatusBar.Parent = FMainForm) and + (FMainForm.sbStatusBar.Left = 0) and + (FMainForm.sbStatusBar.Width = FMainForm.ClientWidth) and + (FMainForm.sbStatusBar.Top + FMainForm.sbStatusBar.Height = FMainForm.ClientHeight), + ACheckpoint + ': status bar is detached from the form bottom'); + Require((FMainForm.tbStatusToolBar.Parent = FMainForm.sbStatusBar) and + (Windows.GetParent(FMainForm.tbStatusToolBar.Handle) = FMainForm.sbStatusBar.Handle), + ACheckpoint + ': footer icons do not belong to the status bar'); + LStatusClient := FMainForm.sbStatusBar.ClientRect; + LStatusOrigin := Point(LStatusClient.Left, LStatusClient.Top); + Require(Windows.ClientToScreen(FMainForm.sbStatusBar.Handle, LStatusOrigin), + ACheckpoint + ': no native status client origin'); + LStatusRect := Rect(LStatusOrigin.X, LStatusOrigin.Y, + LStatusOrigin.X + FMainForm.sbStatusBar.ClientWidth, + LStatusOrigin.Y + FMainForm.sbStatusBar.ClientHeight); + Require(GetWindowRect(FMainForm.tbStatusToolBar.Handle, LToolbarRect), + ACheckpoint + ': no native footer toolbar bounds'); + Require((LToolbarRect.Left >= LStatusRect.Left) and (LToolbarRect.Top >= LStatusRect.Top) and + (LToolbarRect.Right <= LStatusRect.Right) and (LToolbarRect.Bottom <= LStatusRect.Bottom), + ACheckpoint + ': footer icons float outside the native status bar'); + Require(LStatusRect.Right - LToolbarRect.Right <= 32, + ACheckpoint + ': footer icons did not follow the right edge'); + LFooterGeometry := ReadWalletFooterGeometry(FMainForm.sbStatusBar, FMainForm.tbStatusToolBar); + Log('FOOTER CONTENT ' + ACheckpoint + ': ' + LFooterGeometry.Description); + RequireWalletFooterContent(LFooterGeometry, ACheckpoint); + Require(FMainForm.tbStatusToolBar.ButtonCount = 3, ACheckpoint + ': unexpected footer button count'); + LButtons[0] := FMainForm.tbtnWalletLock; + LButtons[1] := FMainForm.tbtnConnectivity; + LButtons[2] := FMainForm.tbtnSync; + for i := 0 to High(LButtons) do begin + LButton := LButtons[i]; + Require(FMainForm.tbStatusToolBar.Buttons[i] = LButton, + ACheckpoint + ': footer order differs from its visible positions'); + Require((LButton.Left >= 0) and (LButton.Top >= 0) and + (LButton.Left + LButton.Width <= FMainForm.tbStatusToolBar.ClientWidth) and + (LButton.Top + LButton.Height <= FMainForm.tbStatusToolBar.ClientHeight), + ACheckpoint + ': footer button is clipped'); + Require((LButton.Width >= FMainForm.ilSmallIcons.Width) and + (LButton.Height >= FMainForm.ilSmallIcons.Height + 2), + ACheckpoint + ': footer clips the original icon'); + Require(FMainForm.tbStatusToolBar.ControlAtPos(Point(LButton.Left + LButton.Width div 2, + LButton.Top + LButton.Height div 2), true) = LButton, + ACheckpoint + ': footer icon center identifies a different button'); + end; + Log(Format('LAYOUT %s: header=%dx%d language=%d,%d; status=%d,%d,%d,%d toolbar=%d,%d,%d,%d', + [ACheckpoint, LBanner.Width, LBanner.Height, LBanner.cbLanguage.Left, LBanner.cbLanguage.Top, + LStatusRect.Left, LStatusRect.Top, LStatusRect.Right, LStatusRect.Bottom, + LToolbarRect.Left, LToolbarRect.Top, LToolbarRect.Right, LToolbarRect.Bottom])); +end; + +procedure TStartupHarness.FooterConnectivityClick(Sender : TObject); +begin + Require(Sender = FMainForm.tbtnConnectivity, 'Connectivity icon dispatched to a different button'); + Inc(FFooterConnectivityClicks); + FFooterConnectivityClick(Sender); +end; + +procedure TStartupHarness.FooterSyncClick(Sender : TObject); +begin + Require(Sender = FMainForm.tbtnSync, 'Synchronization icon dispatched to a different button'); + Inc(FFooterSyncClicks); + FFooterSyncClick(Sender); +end; + +procedure TStartupHarness.CheckFooterActions; +var + LConnectivityEnabled, LWasConnected: Boolean; + LMode: TFRMMainFormMode; + LExpected: TNotifyEvent; + LOldNetworkImage: Integer; + LOldNetworkHint: String; + i: Integer; + + procedure NativeClick(AButton: TToolButton); + var + LPosition: LPARAM; + begin + LPosition := MakeLParam(AButton.Left + AButton.Width div 2, + AButton.Top + AButton.Height div 2); + SendMessage(FMainForm.tbStatusToolBar.Handle, WM_LBUTTONDOWN, MK_LBUTTON, LPosition); + SendMessage(FMainForm.tbStatusToolBar.Handle, WM_LBUTTONUP, 0, LPosition); + PumpWindowChanges; + end; + +begin + Require(TUserInterface.Offline, 'Footer hit checks require an offline owned fixture'); + FFooterConnectivityClick := FMainForm.tbtnConnectivity.OnClick; + FFooterSyncClick := FMainForm.tbtnSync.OnClick; + LExpected := FMainForm.tbtnConnectivityClick; + Require((TMethod(FFooterConnectivityClick).Code = TMethod(LExpected).Code) and + (TMethod(FFooterConnectivityClick).Data = TMethod(LExpected).Data), + 'Connectivity button has the wrong production handler'); + LExpected := FMainForm.tbtnSyncClick; + Require((TMethod(FFooterSyncClick).Code = TMethod(LExpected).Code) and + (TMethod(FFooterSyncClick).Data = TMethod(LExpected).Data), + 'Synchronization button has the wrong production handler'); + Require((FMainForm.tbStatusToolBar.Images = FMainForm.ilSmallIcons) and + (FMainForm.tbtnConnectivity.ImageIndex = 1) and (FMainForm.tbtnSync.ImageIndex = 4), + 'Disconnected/synchronization footer uses the wrong original artwork'); + LConnectivityEnabled := FMainForm.tbtnConnectivity.Enabled; + LOldNetworkImage := FMainForm.tbtnConnectivity.ImageIndex; + LOldNetworkHint := FMainForm.tbtnConnectivity.Hint; + LWasConnected := TNetData.NetData.NetConnectionsActive; + LMode := FMainForm.Mode; + FMainForm.tbtnConnectivity.OnClick := FooterConnectivityClick; + FMainForm.tbtnSync.OnClick := FooterSyncClick; + try + // Both original connection glyphs must keep the same native hit target. + // Only presentation is simulated; the real offline handler is delegated. + FMainForm.tbtnConnectivity.Enabled := true; + for i := 0 to 1 do begin + FMainForm.tbtnConnectivity.ImageIndex := i; + if i = 0 then FMainForm.tbtnConnectivity.Hint := 'Network is active. Click to deactivate.' + else FMainForm.tbtnConnectivity.Hint := 'Network is inactive. Click to activate.'; + PumpWindowChanges; + FFooterConnectivityClicks := 0; + FFooterSyncClicks := 0; + NativeClick(FMainForm.tbtnConnectivity); + Require((FFooterConnectivityClicks = 1) and (FFooterSyncClicks = 0), + 'Clicking a connection glyph invoked synchronization'); + Require(FMainForm.Mode = LMode, 'Connection glyph changed the displayed screen'); + Require(TNetData.NetData.NetConnectionsActive = LWasConnected, + 'Footer probe changed offline connectivity'); + end; + FFooterConnectivityClicks := 0; + FFooterSyncClicks := 0; + NativeClick(FMainForm.tbtnSync); + Require((FFooterSyncClicks = 1) and (FFooterConnectivityClicks = 0), + 'Clicking the synchronization glyph invoked connectivity'); + Require(FMainForm.Mode <> LMode, 'Synchronization glyph did not switch the screen'); + Require(TNetData.NetData.NetConnectionsActive = LWasConnected, + 'Synchronization glyph changed connectivity'); + NativeClick(FMainForm.tbtnSync); + Require((FFooterSyncClicks = 2) and (FFooterConnectivityClicks = 0) and + (FMainForm.Mode = LMode), 'Second synchronization click did not restore the screen'); + finally + FMainForm.tbtnConnectivity.OnClick := FFooterConnectivityClick; + FMainForm.tbtnSync.OnClick := FFooterSyncClick; + FMainForm.tbtnConnectivity.ImageIndex := LOldNetworkImage; + FMainForm.tbtnConnectivity.Hint := LOldNetworkHint; + FMainForm.tbtnConnectivity.Enabled := LConnectivityEnabled; + end; + CheckOffline('after native footer icon clicks'); + Log('FOOTER: original 16px icons fit; connected/inactive glyph hit targets match connectivity; synchronization native clicks switch views twice without changing networking'); +end; + +procedure TStartupHarness.CheckResizeCycle(const ACheckpoint : String; + AMode : TFRMMainFormMode; ALoading : Boolean); +var + LRestored, LAfter: TRect; + LHeight, LWidth: Integer; + LMinMax: TMinMaxInfo; +begin + Require(FMainForm.WindowState = wsNormal, ACheckpoint + ': expected restored starting state'); + Require(GetWindowRect(FMainForm.Handle, LRestored), ACheckpoint + ': cannot save restored bounds'); + CheckFrameLayout(ACheckpoint + '-normal', true); + Require(CheckWalletFooterPaint(FMainForm.sbStatusBar, FMainForm.tbStatusToolBar, + ChangeFileExt(FLogPath, '') + '-' + ACheckpoint + '-footer.bmp') = 0, + ACheckpoint + ': toolbar changes painted status-panel border pixels'); + if ALoading then begin + LHeight := FMainForm.Height; + LWidth := FMainForm.Width; + Require((FMainForm.Constraints.MinHeight = LHeight) and + (FMainForm.Constraints.MaxHeight = LHeight), ACheckpoint + ': startup height is not fixed'); + Require((FMainForm.Constraints.MinWidth = FOriginalMinWidth) and + (FMainForm.Constraints.MaxWidth = FOriginalMaxWidth), ACheckpoint + ': startup changed width constraints'); + Require(not (biMaximize in FMainForm.BorderIcons), ACheckpoint + ': startup maximize command is enabled'); + FillChar(LMinMax, SizeOf(LMinMax), 0); + SendMessage(FMainForm.Handle, WM_GETMINMAXINFO, 0, LPARAM(@LMinMax)); + Require((LMinMax.ptMinTrackSize.Y > 0) and + (LMinMax.ptMinTrackSize.Y = LMinMax.ptMaxTrackSize.Y), + ACheckpoint + ': native vertical tracking is not fixed'); + FMainForm.SetBounds(FMainForm.Left, FMainForm.Top, LWidth + 96, LHeight + 96); + PumpWindowChanges; + Require((FMainForm.Width = LWidth + 96) and (FMainForm.Height = LHeight), + ACheckpoint + ': startup must permit width changes and reject height changes'); + Require(FMainForm.SyncControl.ShowSplash, ACheckpoint + ': width change dismissed controlled splash'); + CheckFrameLayout(ACheckpoint + '-wider-fixed-height', true); + SendMessage(FMainForm.Handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0); + PumpWindowChanges; + Require((not Windows.IsZoomed(FMainForm.Handle)) and (FMainForm.Height = LHeight), + ACheckpoint + ': native maximize changed the startup height/state'); + FMainForm.SetBounds(LRestored.Left, LRestored.Top, LWidth, LHeight); + PumpWindowChanges; + CheckFrameLayout(ACheckpoint + '-restored', true); + Log('STARTUP SIZING: horizontal resize allowed; height and native maximize locked'); + Exit; + end; + Require((FMainForm.Constraints.MinHeight = FOriginalMinHeight) and + (FMainForm.Constraints.MaxHeight = FOriginalMaxHeight) and + (FMainForm.BorderIcons = FOriginalBorderIcons), ACheckpoint + ': startup sizing policy was not restored'); + FMainForm.WindowState := wsMaximized; + PumpWindowChanges; + Require(Windows.IsZoomed(FMainForm.Handle), ACheckpoint + ': native host did not maximize'); + if ALoading then begin + Require(FMainForm.SyncControl.ShowSplash, ACheckpoint + ': resize dismissed controlled splash'); + CheckFrameLayout(ACheckpoint + '-maximized', false); + end else + CheckNative(ACheckpoint + '-maximized', AMode, false); + FMainForm.WindowState := wsNormal; + PumpWindowChanges; + Require(not Windows.IsZoomed(FMainForm.Handle), ACheckpoint + ': native host did not restore'); + Require(GetWindowRect(FMainForm.Handle, LAfter), ACheckpoint + ': cannot read restored bounds'); + Require((LAfter.Left = LRestored.Left) and (LAfter.Top = LRestored.Top) and + (LAfter.Right = LRestored.Right) and (LAfter.Bottom = LRestored.Bottom), + ACheckpoint + ': maximize/restore changed the saved normal rectangle'); + if ALoading then + CheckFrameLayout(ACheckpoint + '-restored', true) + else + CheckNative(ACheckpoint + '-restored', AMode); +end; + +procedure TStartupHarness.CheckFooterDpiCycle; +var + LHeight, LWidth: Integer; + LGeometry: TWalletFooterGeometry; +begin + LHeight := FMainForm.sbStatusBar.Height; + LWidth := FMainForm.Width; + FMainForm.AutoAdjustLayout(lapAutoAdjustForDPI, 96, 144, LWidth, LWidth * 3 div 2); + PumpWindowChanges; + LGeometry := ReadWalletFooterGeometry(FMainForm.sbStatusBar, FMainForm.tbStatusToolBar); + RequireWalletFooterContent(LGeometry, '144PPI'); + Require(CheckWalletFooterPaint(FMainForm.sbStatusBar, FMainForm.tbStatusToolBar, + ChangeFileExt(FLogPath, '') + '-144PPI-footer.bmp') = 0, + '144PPI toolbar changes painted status-panel border pixels'); + Log('FOOTER DPI144: ' + LGeometry.Description); + FMainForm.AutoAdjustLayout(lapAutoAdjustForDPI, 144, 96, FMainForm.Width, LWidth); + PumpWindowChanges; + LGeometry := ReadWalletFooterGeometry(FMainForm.sbStatusBar, FMainForm.tbStatusToolBar); + RequireWalletFooterContent(LGeometry, '96PPI-restored'); + Require(FMainForm.sbStatusBar.Height = LHeight, 'Footer remained oversized after 96->144->96 DPI cycle'); + Require(CheckWalletFooterPaint(FMainForm.sbStatusBar, FMainForm.tbStatusToolBar, + ChangeFileExt(FLogPath, '') + '-96PPI-footer.bmp') = 0, + 'Restored toolbar changes painted status-panel border pixels'); + Log('FOOTER DPI96 RESTORED: ' + LGeometry.Description); +end; + +procedure TStartupHarness.StopWallet; +begin + if FExited then Exit; + FExited := true; + FTimer.Enabled := false; + TUserInterface.ExitApplication; + FMainForm := nil; +end; + +procedure TStartupHarness.CheckEventDrivenStatuses; +var + LDeadline: QWord; +begin + // Exercise the actual node and network notifier threads. One producer input + // must update the controls; pumping only delivers queued work, never data. + FMainForm.SyncControl.lblTotalBlocksValue.Caption := 'Await node notification'; + FMainForm.SyncControl.lblPendingOperationsValue.Caption := 'Await account notification'; + FMainForm.sbStatusBar.Panels[1].Text := 'Await network notification'; + TUserInterface.Node.NotifyBlocksChanged; + TNetData.NetData.NotifyStatisticsChanged; + LDeadline := GetTickCount64 + 2000; + repeat + CheckSynchronize(10); + Application.ProcessMessages; + if (FMainForm.SyncControl.lblTotalBlocksValue.Caption = '(none)') and + (Pos('Connections:', FMainForm.sbStatusBar.Panels[1].Text) = 1) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(FMainForm.SyncControl.lblTotalBlocksValue.Caption = '(none)', + 'Actual node notification did not refresh the synchronization block count'); + Require(Pos('Connections:', FMainForm.sbStatusBar.Panels[1].Text) = 1, + 'Actual network notification did not refresh connection statistics'); + FMainForm.SyncControl.lblPendingOperationsValue.Caption := 'Await account notification'; + TUserInterface.SynchronizationChanged.Invoke(nil); + LDeadline := GetTickCount64 + 2000; + repeat + CheckSynchronize(10); + Application.ProcessMessages; + if FMainForm.SyncControl.lblPendingOperationsValue.Caption = '0' then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(FMainForm.SyncControl.lblPendingOperationsValue.Caption = '0', + 'Synchronization status did not immediately publish cached pending operations'); + FMainForm.SyncControl.lblNodeStatus.Caption := 'Await state notification'; + TUserInterface.StateChanged.Invoke(nil); + Require(FMainForm.SyncControl.lblNodeStatus.Caption = 'Offline mode', + 'State-change event did not refresh the visible synchronization status'); + FMainForm.SyncControl.lblMinersClientsValue.Caption := 'Await miner notification'; + FMainForm.SyncControl.lblBlocksFound.Caption := 'Await miner notification'; + Require(Assigned(TUserInterface.PoolMiningServer.OnStatusChanged), + 'Mining server status event is not bridged to the UI'); + TUserInterface.PoolMiningServer.OnStatusChanged(TUserInterface.PoolMiningServer); + Require((FMainForm.SyncControl.lblMinersClientsValue.Caption = 'JSON-RPC server not active') and + (FMainForm.SyncControl.lblBlocksFound.Caption = '0'), + 'Mining-status event did not refresh miner status and wins immediately'); + Log('EVENT-DRIVEN STATUS: actual node/network notifier threads updated native sync/footer; account/state/mining events updated their labels without a UI polling timer'); +end; + +procedure TStartupHarness.OnSyncSnapshotChanged(Sender: TObject); +begin + Inc(FSnapshotNotifications); +end; + +procedure TStartupHarness.CheckBusyStatisticsRefresh; +var + LBusy : TBusyMempoolThread; + LPreviousCount, LPreviousTarget : String; + LDeadline: QWord; + LPublished: Integer; +begin + Require(FMainForm.SyncControl.IsVisible, 'Busy statistics test requires a visible sync screen'); + Require(FMainForm.SyncControl.HasNodeSnapshot, 'Busy statistics test requires a completed snapshot'); + LPreviousCount := FMainForm.SyncControl.lblPendingOperationsValue.Caption; + LPreviousTarget := FMainForm.SyncControl.lblBlockTargetValue.Caption; + FSnapshotNotifications := 0; + FMainForm.SyncControl.SnapshotChanged.Add(OnSyncSnapshotChanged); + LBusy := TBusyMempoolThread.Create(TUserInterface.Node); + try + Require(LBusy.Entered.WaitFor(2000) = wrSignaled, 'Statistics fixture did not acquire mempool'); + TUserInterface.ShowWallet; + FMainForm.Perform(CM_PC_ModeChanged, 0, 0); + TUserInterface.ShowSyncDialog; + FMainForm.Perform(CM_PC_ModeChanged, 0, 0); + Require((not LBusy.WatchdogReleased) and (LBusy.Released.WaitFor(0) <> wrSignaled), + 'Initial sync show waited for the busy mempool instead of retaining its snapshot'); + Require((FMainForm.SyncControl.lblPendingOperationsValue.Caption = LPreviousCount) and + (FMainForm.SyncControl.lblBlockTargetValue.Caption = LPreviousTarget), + 'Initial sync show replaced the previous snapshot while core was busy'); + TUserInterface.AccountsChanged.Invoke(nil); + TUserInterface.BlocksChanged.Invoke(nil); + Require((not LBusy.WatchdogReleased) and (LBusy.Released.WaitFor(0) <> wrSignaled), + 'Visible sync data notifications waited for the busy mempool'); + Require((FMainForm.SyncControl.lblPendingOperationsValue.Caption = LPreviousCount) and + (FMainForm.SyncControl.lblBlockTargetValue.Caption = LPreviousTarget), + 'Busy statistics refresh replaced the previous snapshot'); + // Basic facts are cached; view/account requests do not wait for this writer. + // The explicit block event may schedule one independent secondary Bank read. + Require(FSnapshotNotifications > 0, 'Cached basic status event was not published'); + LBusy.ReleaseLock.SetEvent; + LBusy.WaitFor; + Require(not LBusy.WatchdogReleased, 'Statistics fixture needed its fail-safe release'); + Require(FMainForm.sbStatusBar.Panels[2].Text = FMainForm.SyncControl.ChainStatusText, + 'Cached synchronization status did not update the main footer'); + // Allow the block-triggered secondary request to settle, then prove idle + // and byte-statistics inputs do not restart it. + LDeadline := GetTickCount64 + 200; + repeat + CheckSynchronize(10); + Application.ProcessMessages; + Sleep(1); + until GetTickCount64 >= LDeadline; + LPublished := FSnapshotNotifications; + TUserInterface.NetStatisticsChanged.Invoke(nil); + LDeadline := GetTickCount64 + 500; + repeat + CheckSynchronize(10); + Application.ProcessMessages; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(FSnapshotNotifications = LPublished, + 'Idle/unchanged byte statistics restarted a full node snapshot'); + finally + FMainForm.SyncControl.SnapshotChanged.Remove(OnSyncSnapshotChanged); + LBusy.Free; + end; + Log('BUSY STATISTICS: cached basic labels/footer and retained secondary values stayed available while mempool was held; idle/byte statistics caused no additional snapshot'); +end; + +procedure TStartupHarness.CheckModelessFocus; +var + LWallet: TCTRLWallet; + LGrid: TVisualGrid; + LDialog: TForm; + LEdit: TEdit; + LOverlay: TPanel; + I: Integer; +begin + LWallet := FMainForm.paWalletPanel.Controls[0] as TCTRLWallet; + LGrid := nil; + for I := 0 to LWallet.ComponentCount - 1 do + if LWallet.Components[I] is TVisualGrid then begin + LGrid := TVisualGrid(LWallet.Components[I]); + Break; + end; + Require(Assigned(LGrid), 'Modeless focus test found no real wallet visual grid'); + LDialog := TForm.CreateNew(Self); + try + LDialog.Caption := 'Disposable modeless focus check'; + LDialog.BorderStyle := bsDialog; + LDialog.PopupMode := pmExplicit; + LDialog.PopupParent := FMainForm; + LDialog.Position := poMainFormCenter; + LDialog.SetBounds(120, 130, 360, 160); + LEdit := TEdit.Create(LDialog); + LEdit.Parent := LDialog; + LEdit.SetBounds(20, 20, 280, 24); + LDialog.Show; + Windows.SetActiveWindow(LDialog.Handle); + LEdit.SetFocus; + Require(Windows.GetActiveWindow = LDialog.Handle, + 'Modeless fixture did not become the owning thread active window'); + TUserInterface.StateChanged.Invoke(nil); + TUserInterface.BlocksChanged.Invoke(nil); + PumpWindowChanges; + Require(Windows.GetActiveWindow = LDialog.Handle, + 'Background refresh activated main above its modeless dialog'); + TVisualGridAccess(LGrid).BeginLoading; + Sleep(220); // Exercise the real loading card after its intentional delay. + TVisualGridAccess(LGrid).UpdateLoading; + LOverlay := LGrid.FindComponent('LoadingOverlay') as TPanel; + Require(Assigned(LOverlay) and LOverlay.Visible, + 'Delayed loading-card focus check did not show the real overlay'); + Require(Windows.GetActiveWindow = LDialog.Handle, + 'Loading overlay activated main above its modeless dialog'); + TVisualGridAccess(LGrid).ShowLoadingError('Disposable controlled fetch failure'); + Require(LOverlay.Visible and (Windows.GetActiveWindow = LDialog.Handle), + 'Error overlay activated main above its modeless dialog'); + Require(Windows.GetAncestor(Windows.GetFocus, GA_ROOT) = LDialog.Handle, + 'Grid overlay moved keyboard focus away from the modeless dialog'); + finally + TVisualGridAccess(LGrid).EndLoading; + LDialog.Free; + end; + Log('MODELESS FOCUS: real backend/UI callbacks and delayed loading/error cards preserved dialog activation and keyboard focus'); +end; + +procedure TStartupHarness.InspectQuit(Sender : TObject); +var + LDialog: TCustomForm; + LYes: TCustomButton; + I, J: Integer; +begin + LDialog := nil; + LYes := nil; + for I := 0 to Screen.FormCount - 1 do + if Screen.Forms[I].Visible and (Screen.Forms[I].Caption = 'Exit PascalCoin') then begin + LDialog := Screen.Forms[I]; + Break; + end; + if not Assigned(LDialog) then begin + if GetTickCount64 >= FModalDeadline then begin + FModalFailure := 'Native Quit did not enter its confirmation dialog'; + FModalTimer.Enabled := false; + Application.Terminate; + end; + Exit; + end; + FModalTimer.Enabled := false; + try + Require(Assigned(FQuitBusy) and + (FQuitBusy.Released.WaitFor(0) <> wrSignaled), 'Quit fixture released its core lock early'); + TUserInterface.StateChanged.Invoke(nil); + TUserInterface.BlocksChanged.Invoke(nil); + Require((not FQuitBusy.WatchdogReleased) and + (FQuitBusy.Released.WaitFor(0) <> wrSignaled), + 'Quit confirmation callbacks blocked on the busy core'); + Require(not Windows.IsWindowEnabled(FMainForm.Handle), + 'Backend refresh re-enabled the main window behind Quit confirmation'); + for J := 0 to LDialog.ComponentCount - 1 do + if (LDialog.Components[J] is TCustomButton) and + (TCustomButton(LDialog.Components[J]).ModalResult = mrYes) then begin + LYes := TCustomButton(LDialog.Components[J]); + Break; + end; + Require(Assigned(LYes), 'Quit confirmation has no real Yes button'); + // This fixture borrows the node. Join it before Yes permits node disposal; + // slow-close separately verifies cancellation while the loader owns locks. + FQuitBusy.ReleaseLock.SetEvent; + FQuitBusy.WaitFor; + Require(not FQuitBusy.WatchdogReleased, 'Busy Quit needed its fail-safe release'); + FModalSeen := true; + LYes.Click; + except + on E: Exception do begin + FModalFailure := E.Message; + LDialog.ModalResult := mrNo; + end; + end; +end; + +procedure TStartupHarness.CheckBusyQuit; +begin + FQuitBusy := TBusyMempoolThread.Create(TUserInterface.Node); + try + Require(FQuitBusy.Entered.WaitFor(2000) = wrSignaled, 'Quit fixture did not acquire mempool'); + FModalSeen := false; + FModalFailure := ''; + FModalDeadline := GetTickCount64 + 1500; + FModalTimer.OnTimer := InspectQuit; + FModalTimer.Enabled := true; + // Actual Close -> confirmation -> real Yes -> posted controller shutdown. + FMainForm.Close; + Require(FModalFailure = '', FModalFailure); + Require(FModalSeen, 'Quit confirmation was not inspected'); + FExited := true; + FTimer.Enabled := false; + finally + FModalTimer.Enabled := false; + FModalTimer.OnTimer := InspectWorkbench; + FreeAndNil(FQuitBusy); + end; + Log('BUSY QUIT: real Close/Yes accepted while background callbacks were core-busy; borrowed fixture joined before node disposal'); +end; + +procedure TStartupHarness.CheckClassicIntegration; +var + LMessages : TFRMMessages; + LLogs : TFRMLogs; + LBlocks : TFRMBlockExplorer; + LAccounts : TFRMAccountExplorer; + LMessageCount, i : Integer; + LDeadline : QWord; + LRejected : Boolean; +begin + Require(Assigned(FMainForm.miAdvancedOperation) and Assigned(FMainForm.miOperationsWorkbench) and + Assigned(FMainForm.miRPCCalls), 'Classic advanced tools are missing from the main menu'); + Require(not FMainForm.miRPCCalls.Enabled, 'RPC calls menu is enabled offline'); + Require(FMainForm.miOperationsWorkbench.Enabled, 'Offline workbench preparation is disabled'); + LRejected := false; + try + TUserInterface.ShowRPCCallsDialog(FMainForm); + except + on E : EUserInterface do LRejected := true; + end; + Require(LRejected, 'Programmatic RPC tool invocation bypassed offline policy'); + FModalSeen := false; + FModalFailure := ''; + FModalDeadline := GetTickCount64 + 3000; + FModalTimer.Enabled := true; + TUserInterface.ShowOperationsWorkbench(FMainForm); + Require(FModalFailure = '', FModalFailure); + Require(FModalSeen, 'RAW workbench composition was not inspected'); + Require(Windows.IsWindowEnabled(FMainForm.Handle), 'Closing the workbench did not restore main input'); + + TSettings.MinFutureBlocksToDownloadNewSafebox := 1234; + TSettings.AllowDownloadNewCheckpointIfOlderThan := true; + TSettings.Save; + Require(TNetData.NetData.MinFutureBlocksToDownloadNewSafebox = 1234, + 'Checkpoint download threshold was not applied'); + TSettings.AllowDownloadNewCheckpointIfOlderThan := false; + TSettings.Save; + Require(TNetData.NetData.MinFutureBlocksToDownloadNewSafebox = 0, + 'Checkpoint downloads were not disabled'); + + TSettings.ShowLogs := true; + TSettings.Save; + LLogs := nil; + LMessages := nil; + for i := 0 to Screen.FormCount - 1 do begin + if Screen.Forms[i] is TFRMLogs then LLogs := TFRMLogs(Screen.Forms[i]); + if Screen.Forms[i] is TFRMMessages then LMessages := TFRMMessages(Screen.Forms[i]); + end; + Require(FMainForm.miLogs.Visible and Assigned(LLogs) and Assigned(TUserInterface.Log.OnNewLog), + 'Show Logs did not enable the menu and log capture'); + TSettings.ShowLogs := false; + TSettings.Save; + Require((not FMainForm.miLogs.Visible) and (not LLogs.Visible) and + (not Assigned(TUserInterface.Log.OnNewLog)), 'Hidden logs remained visible or subscribed'); + + Require(Assigned(LMessages) and (not LMessages.Visible), 'Message history is not subscribed before first open'); + LMessageCount := LMessages.memoMessages.Lines.Count; + TUserInterface.NodeMessageEvent.Invoke(nil, 'Disposable QA message before first open'); + Require(LMessages.memoMessages.Lines.Count = LMessageCount + 1, 'Hidden incoming message was lost'); + Require(FMainForm.SyncControl.MessagesNotificationText <> '', 'Hidden incoming message did not set unread state'); + TUserInterface.ShowMessagesForm; + Require(LMessages.Visible and (FMainForm.SyncControl.MessagesNotificationText = ''), + 'Opening messages did not clear unread state'); + TUserInterface.NodeMessageEvent.Invoke(nil, 'Disposable QA message while open'); + Require(FMainForm.SyncControl.MessagesNotificationText = '', 'Visible messages left stale unread state'); + LMessages.Close; + Require(not LMessages.Visible, 'Closing messages did not hide its persistent history'); + TUserInterface.NodeMessageEvent.Invoke(nil, 'Disposable QA message after closing'); + Require(LMessages.memoMessages.Lines.Count = LMessageCount + 3, 'Closing messages discarded later history'); + TUserInterface.NetConnectionsUpdated.Invoke(nil); + Require(LMessages.lbNetConnections.Items.Count = 0, 'Offline message recipients were not refreshed'); + TUserInterface.ShowMessagesForm; + Require(LMessages.memoMessages.Lines.Count = LMessageCount + 3, 'Reopening messages lost session history'); + Require(FMainForm.SyncControl.MessagesNotificationText = '', 'Reopening messages did not clear unread state'); + LMessages.Close; + + TUserInterface.ShowBlockExplorer; + LBlocks := nil; + for i := 0 to Screen.FormCount - 1 do + if Screen.Forms[i] is TFRMBlockExplorer then LBlocks := TFRMBlockExplorer(Screen.Forms[i]); + Require(Assigned(LBlocks) and LBlocks.Visible, 'Block explorer did not open'); + LBlocks.ebHashRateBackBlocks.Text := '0'; + LBlocks.ebHashRateBackBlocksExit(nil); + Require((LBlocks.ebHashRateBackBlocks.Text = '1') and (TSettings.HashRateAvgBlocksCount = 1), + 'Block hashrate window did not clamp its lower bound'); + LBlocks.ebHashRateBackBlocks.Text := '2000'; + LBlocks.ebHashRateBackBlocksExit(nil); + Require((LBlocks.ebHashRateBackBlocks.Text = '1000') and (TSettings.HashRateAvgBlocksCount = 1000), + 'Block hashrate window did not clamp its upper bound'); + LBlocks.cbHashRateUnits.ItemIndex := Ord(hr_Mega); + LBlocks.cbHashRateUnitsChange(nil); + LBlocks.Close; + Application.ProcessMessages; + TUserInterface.ShowBlockExplorer; + LBlocks := nil; + for i := 0 to Screen.FormCount - 1 do + if Screen.Forms[i] is TFRMBlockExplorer then LBlocks := TFRMBlockExplorer(Screen.Forms[i]); + Require(Assigned(LBlocks) and (LBlocks.ebHashRateBackBlocks.Text = '1000') and + (LBlocks.cbHashRateUnits.ItemIndex = Ord(hr_Mega)), 'Reopened block explorer lost hashrate preferences'); + LBlocks.Close; + Application.ProcessMessages; + + TUserInterface.ShowAccountExplorer; + LAccounts := nil; + for i := 0 to Screen.FormCount - 1 do + if Screen.Forms[i] is TFRMAccountExplorer then LAccounts := TFRMAccountExplorer(Screen.Forms[i]); + Require(Assigned(LAccounts) and Assigned(LAccounts.miFindAccount.OnClick), 'Account Find command is not wired'); + LAccounts.lblAccountsCount.Caption := 'stale'; + LAccounts.Refresh; + LDeadline := GetTickCount64 + 3000; + while (LAccounts.lblAccountsCount.Caption <> '0') and (GetTickCount64 < LDeadline) do begin + Application.ProcessMessages; + Sleep(1); + end; + Require(LAccounts.lblAccountsCount.Caption = '0', 'Account totals did not refresh for the empty safebox'); + LAccounts.Close; + Application.ProcessMessages; + Log('CLASSIC INTEGRATION: advanced tools exposed; offline RPC guarded; checkpoint/log settings applied; persistent message history/unread/recipients; block clamp/preferences/reopen; account Find/totals verified'); +end; + +procedure TStartupHarness.CheckShutdown; +begin + CheckSynchronize; + Require(not TUserInterface.Started, 'Application still started after shutdown'); + Require(not Assigned(TUserInterface.Node), 'Node not released after shutdown'); + Require(not TNode.NodeExists, 'Node singleton remained after shutdown'); + Require(not Assigned(TUserInterface.RPCServer), 'RPC server not released after shutdown'); + Require(not Assigned(TUserInterface.PoolMiningServer), 'Mining server not released after shutdown'); + Require(Screen.FormCount = 0, 'Forms remained after shutdown: ' + IntToStr(Screen.FormCount)); + GAllowedWindow := 0; + GUnexpectedWindow := 0; + EnumWindows(@FindUnexpectedWindow, 0); + Require(GUnexpectedWindow = 0, 'Visible application window remained after shutdown'); + Log('SHUTDOWN: loader joined, queued callbacks drained, wallet/node/servers/forms released'); +end; + +procedure TStartupHarness.Tick(Sender : TObject); +var + LCancel : TButton; + LStatusText : TLabel; +begin + if FInsideTick then Exit; + FInsideTick := true; + try + try + Require(GetTickCount64 < FDeadline, 'Startup probe timed out'); + Require(TUserInterface.State <> uisError, TUserInterface.StateText); + if FMode = 'slow-close' then begin + if FPhase = 0 then begin + Require(FSlowTask.FEntered.WaitFor(0) = wrSignaled, 'Controlled loader did not enter'); + // The task's signal belongs to the worker; wait for its real queued + // progress event before checking the receiving native label. + if not FProgressSeen then Exit; + Require(FLoadedCount = 0, 'Controlled loader completed unexpectedly'); + Require(TUserInterface.State = uisLoading, 'Controlled loader left loading state'); + Require(FMainForm.Visible and FMainForm.Enabled and Windows.IsWindowEnabled(FMainForm.Handle), + 'Loading disables the native main window and its Close input'); + Require(FMainForm.SyncControl.ShowSplash, 'Controlled loading splash is not visible'); + Require(not FMainForm.tbtnSync.Enabled, 'Wallet navigation enabled before local load'); + Require(not FMainForm.miOptions.Enabled, 'Options enabled before local load'); + Require(not FMainForm.miPrivateKeys.Enabled, 'Key commands enabled before local load'); + Require(Windows.IsWindowVisible(FMainForm.SyncControl.Handle), 'Loading screen has no visible child window'); + FMainForm.SetBounds(80, 90, 1000, 680); + PumpWindowChanges; + CheckResizeCycle('loading', wmSync, true); + LStatusText := FMainForm.SyncControl.FindComponent('lblLoadingStatus') as TLabel; + Require((FLastProgress.Stage = psReadingBlockchain) and + (FLastProgress.CurrentPosition = 1) and (FLastProgress.TotalCount = 100), + 'Typed startup progress lost its stage or counters'); + Require(Assigned(LStatusText) and + (LStatusText.Caption = FormatProgressInfo(FLastProgress)), + 'Native splash did not format the queued progress record at its UI boundary'); + Require(LStatusText.Caption = 'Reading blockchain block 1/100 ' + + Format('%.2f', [1.0]) + '%', + 'Native 1/100 progress did not display one percent'); + Log('TYPED_PROGRESS queued stage and raw counts displayed once on main thread'); + Require(Assigned(LStatusText) and (LStatusText.Font.Color = clWhite) and + (LStatusText.Font.Size >= 16) and LStatusText.Transparent, + 'Loading status lost its large transparent white text'); + LCancel := FMainForm.SyncControl.FindComponent('btnCancelLoading') as TButton; + Require(Assigned(LCancel) and LCancel.Enabled and Windows.IsWindowVisible(LCancel.Handle), + 'Loading has no usable Cancel startup button'); + // These must not query or write locked/partially restored core state. + TUserInterface.BlocksChanged.Invoke(nil); + TUserInterface.StateChanged.Invoke(nil); + TSettings.MinerName := 'Pending disposable QA payload while loading'; + TSettings.Save; + Log('SLOW LOAD: safebox/mempool locks held; UI refresh/settings stayed responsive; native host enabled; local data commands disabled; posting its own native SC_CLOSE'); + PostMessage(FMainForm.Handle, WM_SYSCOMMAND, SC_CLOSE, 0); + Inc(FPhase); + end; + Exit; + end; + if FLoadedCount = 0 then Exit; + Require(FLoadedCount = 1, 'Loaded event occurred more than once'); + case FPhase of + 0: begin + Require(TUserInterface.Node.Bank.BlocksCount = 0, 'Fresh test chain is not empty'); + Require(TUserInterface.Node.Bank.AccountsCount = 0, 'Fresh test safebox has accounts'); + Require(SameFileName(TNode.GetPascalCoinDataFolder, FDataDirectory), 'Wrong data directory'); + CheckOffline('loaded'); + FMainForm.SetBounds(80, 90, 1000, 680); + PumpWindowChanges; + CheckNative('wallet-initial', wmWallet); + CheckResizeCycle('wallet', wmWallet); + CheckFooterDpiCycle; + CheckFooterActions; + CheckClassicIntegration; + CheckModelessFocus; + // These are calls into our own QA application, never desktop input. + FMainForm.tbtnConnectivityClick(nil); + TSettings.JsonRpcPortEnabled := true; + TSettings.JsonRpcMinerServerActive := true; + TSettings.InternetServerPort := TSettings.InternetServerPort + 1; + TSettings.JsonRpcMinerServerPort := TSettings.JsonRpcMinerServerPort + 1; + TSettings.Save; + CheckOffline('after connectivity click and settings changes'); + Log(CheckWalletKeysIcons(FMainForm, ExtractFilePath(FLogPath))); + TWallet.GenerateNewKey('Disposable native QA key', CT_Default_EC_OpenSSL_NID); + Require(TWallet.Keys.Count = 1, 'Generated QA key was not added'); + Log('KEY LIFECYCLE: generated one disposable key while the wallet control exists'); + Log(CheckSyncFirstDisplay(FMainForm)); + end; + 1: begin + CheckNative('sync-first', wmSync); + CheckResizeCycle('sync', wmSync); + CheckEventDrivenStatuses; + CheckBusyStatisticsRefresh; + Log(CheckSyncWriterIndependence(FMainForm)); + TUserInterface.ShowWallet; + end; + 2: begin + CheckNative('wallet-return', wmWallet); + TWallet.DeleteKey(TWallet.Keys.Key[0]); + Require(TWallet.Keys.Count = 0, 'Generated QA key was not removed'); + Log('KEY LIFECYCLE: removed disposable key while the wallet control exists'); + Log(CheckSyncFirstDisplay(FMainForm)); + end; + 3: begin + CheckNative('sync-reopen', wmSync); + TUserInterface.ShowWallet; + end; + 4: begin + CheckNative('wallet-final', wmWallet); + CheckOffline('after mode transitions'); + CheckBusyQuit; + end; + end; + Inc(FPhase); + except + on E : Exception do OnException(Self, E); + end; + finally + FInsideTick := false; + end; +end; + +procedure TStartupHarness.Run(const AOptions : TWalletStartupOptions); +var + LInputDesktop : HDESK; + LCurrentDesktop, LInputDesktopName : String; +begin + LCurrentDesktop := DesktopName(GetThreadDesktop(GetCurrentThreadId)); + Require(AnsiStartsText('PascalCoinWalletQA_', LCurrentDesktop), 'Refusing to run outside private QA desktop'); + LInputDesktop := OpenInputDesktop(0, false, DESKTOP_READOBJECTS); + if LInputDesktop = 0 then RaiseLastOSError; + try + LInputDesktopName := DesktopName(LInputDesktop); + Require(not SameText(LCurrentDesktop, LInputDesktopName), 'Refusing to run on the input desktop'); + finally + CloseDesktop(LInputDesktop); + end; + Log('SAFETY: own desktop=' + LCurrentDesktop + '; input desktop=' + LInputDesktopName); + Require(AOptions.Offline and AOptions.NoStartupDialogs, 'Offline and no-startup-dialogs are required'); + Require(AOptions.DataDirectory <> '', 'Explicit data directory is required'); + Require(AnsiStartsText(IncludeTrailingPathDelimiter(ExpandFileName(GetTempDir)), FDataDirectory), + 'Refusing data directory outside OS temp'); + Require(AnsiStartsText('PascalCoinWalletQA_', ExtractFileName(FDataDirectory)), 'Wrong disposable directory prefix'); + Require(not DirectoryExists(FDataDirectory), 'Refusing to reuse a data directory'); + Require(ForceDirectories(FDataDirectory), 'Cannot create disposable directory'); + SeedSettings; + TUserInterface.ConfigureStartup(AOptions); + Require(SameFileName(TNode.GetPascalCoinDataFolder, FDataDirectory), 'Data directory not configured before forms'); + Application.Initialize; + Application.MainFormOnTaskBar := true; + Application.OnException := OnException; + Application.Title := 'PascalCoin private startup QA'; + Application.CreateForm(TFRMMainForm, FMainForm); + Require((FMainForm.Constraints.MinHeight = FMainForm.Height) and + (FMainForm.Constraints.MaxHeight = FMainForm.Height) and + (not (biMaximize in FMainForm.BorderIcons)), 'FormCreate did not lock startup height'); + // Capture the real resource defaults before relocking the pending startup. + TMainFormAccess(FMainForm).SetStartupHeightLocked(false); + FOriginalMinHeight := FMainForm.Constraints.MinHeight; + FOriginalMaxHeight := FMainForm.Constraints.MaxHeight; + FOriginalMinWidth := FMainForm.Constraints.MinWidth; + FOriginalMaxWidth := FMainForm.Constraints.MaxWidth; + FOriginalBorderIcons := FMainForm.BorderIcons; + TMainFormAccess(FMainForm).SetStartupHeightLocked(true); + TUserInterface.Loaded.Add(OnLoaded); + TUserInterface.Loading.Add(OnLoading); + TUserInterface.AppStarted.Add(OnAppStarted); + FDeadline := GetTickCount64 + 15000; + Log('START ' + FMode + ': data=' + FDataDirectory); + if FMode = 'slow-close' then begin + FSlowTask := TSlowStartupTask.Create; + TUserInterface.StartApplication(FMainForm, FSlowTask); + end else + TUserInterface.StartApplication(FMainForm); + if FMode = 'close-before-load' then begin + Require(FExited, 'Early close did not occur'); + Require(FLoadedCount = 0, 'Loaded notification escaped early close'); + end else if FMode = 'close-loading' then begin + Require(TUserInterface.Started, 'Startup failed before close-during-loading'); + Require(TUserInterface.State = uisLoading, 'Expected pending local-load notification'); + Require(FLoadedCount = 0, 'Loaded fired before event loop'); + StopWallet; + Require(FLoadedCount = 0, 'Loaded notification escaped close-during-loading'); + end else begin + Require(TUserInterface.Started, 'Startup failed: ' + TUserInterface.StateText); + FTimer.Enabled := true; + Application.Run; + FTimer.Enabled := false; + if FMode = 'slow-close' then begin + FMainForm := nil; + FSlowTask := nil; // Owned and joined by the actual controller. + FExited := true; + Require(FPhase = 1, 'Native close was not exercised'); + Require(FLoadedCount = 0, 'Loaded callback escaped slow-load cancellation'); + Require(GSlowLoadCancelled, 'Native close did not cancel and join controlled local load'); + end else begin + FMainForm := nil; // The actual Close/Yes path has released the form. + Require(FLoadedCount = 1, 'Expected one Loaded event'); + Require(FPhase = 5, 'Not all native mode transitions completed'); + end; + end; + if FFailure <> '' then + raise Exception.Create(FFailure); + CheckShutdown; + Log('PASS ' + FMode); +end; + +procedure TStartupHarness.SeedSettings; +var + LScoped : TDisposables; + LSeedSettings : TAppParams; +begin + // The scope releases the file before normal wallet settings open it. + LSeedSettings := LScoped.AddObject(TAppParams.Create(nil)) as TAppParams; + LSeedSettings.FileName := IncludeTrailingPathDelimiter(FDataDirectory) + 'AppParams.prm'; + LSeedSettings.ParamByName[CT_PARAM_JSONRPCEnabled].SetAsBoolean(true); + LSeedSettings.ParamByName[CT_PARAM_JSONRPCMinerServerActive].SetAsBoolean(true); + LSeedSettings.ParamByName[CT_PARAM_PeerCache].SetAsString('127.0.0.1:1'); + LSeedSettings.ParamByName[CT_PARAM_TryToConnectOnlyWithThisFixedServers].SetAsString('127.0.0.1:1'); +end; + +var + LArguments : array of String; + LOptions : TWalletStartupOptions; + LError, LMode, LLogPath, LArgument : String; + LHarness : TStartupHarness; + i, LArgumentCount : Integer; +begin + LMode := ''; + LLogPath := ''; + SetLength(LArguments, ParamCount); + LArgumentCount := 0; + for i := 1 to ParamCount do begin + LArgument := ParamStr(i); + if AnsiStartsStr('--probe-mode=', LArgument) then + LMode := Copy(LArgument, Length('--probe-mode=') + 1, MaxInt) + else if AnsiStartsStr('--probe-log=', LArgument) then + LLogPath := Copy(LArgument, Length('--probe-log=') + 1, MaxInt) + else begin + LArguments[LArgumentCount] := LArgument; + Inc(LArgumentCount); + end; + end; + SetLength(LArguments, LArgumentCount); + if (not TryParseWalletStartupOptions(LArguments, LOptions, LError)) or + (not ((LMode = 'loaded') or (LMode = 'close-loading') or (LMode = 'close-before-load') or (LMode = 'slow-close'))) or + (LLogPath = '') then Halt(64); + LHarness := TStartupHarness.Create(LMode, LLogPath, LOptions.DataDirectory); + try + try + LHarness.Run(LOptions); + except + on E : Exception do begin + LHarness.Log('FAIL ' + E.ClassName + ': ' + E.Message); + if TUserInterface.Started then + LHarness.StopWallet; + ExitCode := 1; + end; + end; + finally + LHarness.Free; + end; +end. diff --git a/src/tests/experimental/native/abstractmem-storage/README.md b/src/tests/experimental/native/abstractmem-storage/README.md new file mode 100644 index 000000000..01712f17a --- /dev/null +++ b/src/tests/experimental/native/abstractmem-storage/README.md @@ -0,0 +1,12 @@ +# AbstractMem block-storage failure checks + +Six offline checks test failure handling in the optional AbstractMem block and +operation index. They cover false completion flushes, partial writes racing a +flush, shutdown during a worker failure, failed-queue retirement, suppression of +unsafe close retries and a successful queue drain. They use a new data directory +and generated keys. + +The [FPCUnit fixtures](../../README.md#fpcunit-fixtures) register them through +`TAbstractMemStorageFailureTests`. For a standalone Delphi or FPC probe, call +`RunAbstractMemStorageFailureChecks(LogPath)` and return its exit code. Logs and +fixtures go beside `LogPath`. A passing log ends with `PASS 6`. diff --git a/src/tests/experimental/native/abstractmem-storage/UAbstractMemStorageFailureChecks.pas b/src/tests/experimental/native/abstractmem-storage/UAbstractMemStorageFailureChecks.pas new file mode 100644 index 000000000..06cfc714a --- /dev/null +++ b/src/tests/experimental/native/abstractmem-storage/UAbstractMemStorageFailureChecks.pas @@ -0,0 +1,487 @@ +unit UAbstractMemStorageFailureChecks; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Checks the optional AbstractMem operation index with real storage workers and + owned temporary files. No existing wallet, peers or private keys are opened. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$I ../../../../config.inc} + +interface + +type + TAbstractMemStorageFailureCheck = (amscFalseCompletionFlush, + amscBackgroundWriteFailure, amscFailedQueueDrain, + amscFailedCleanupPreservesMarker, amscSuccessfulDrain, amscInFlightShutdown); + +function RunAbstractMemStorageFailureCheck(ACheck: TAbstractMemStorageFailureCheck; const ALogPath: String): Integer; +function RunAbstractMemStorageFailureChecks(const ALogPath: String): Integer; + +implementation + +uses Classes, SysUtils, SyncObjs, TypInfo, UNode, UBlockChain, UAccounts, + UAbstractMemBlockchainStorage, UAbstractMem, UFileMem, UConst, UCrypto, + UPCDataTypes, UOpTransaction; + +type + TWriteControl = class + public + MainThread, ProducerThread: TThreadID; + Entered, ReleaseWrite: TEvent; + PauseWorker, FailWorker, ShortFlush: Boolean; + FailureHits, SkippedCloseFlush: Integer; + constructor Create; + destructor Destroy; override; + function IsWorker: Boolean; + end; + + // FPC resolves protected nested types through a descendant access alias, + // rather than through the declaring ancestor's qualified name. + TStorageFileAccess = class(TAbstractMemBlockchainStorage) + public type + TGuardedFileMem = {$IFDEF FPC}TStorageFileMem{$ELSE}TAbstractMemBlockchainStorage.TStorageFileMem{$ENDIF}; + end; + + TFailureStorage = class(TAbstractMemBlockchainStorageSecondary) + protected type + TFailureFileMem = class(TStorageFileAccess.TGuardedFileMem) + protected + function AbsoluteWrite(const AAbsolutePosition: Int64; const ABuffer; ASize: Integer): Integer; override; + function ShouldFlushOnDestroy: Boolean; override; + public + function Write(const APosition: Int64; const ABuffer; ASize: Integer): Integer; override; + end; + protected + function CreateFileMem(const AFileName: String; AReadOnly: Boolean): TFileMem; override; + public + Control: TWriteControl; + end; + + TStorageAction = (saSave, saFlush, saStop, saCloseNode); + TStorageActionThread = class(TThread) + public + Storage: TFailureStorage; + Node: TNode; + Operations: TPCOperationsComp; + Action: TStorageAction; + Entered, Done: TEvent; + ErrorMessage: String; + constructor Create; + destructor Destroy; override; + procedure Execute; override; + end; + + TStorageFixture = class + private + FNode: TNode; + FStorage: TFailureStorage; + FControl: TWriteControl; + FKey: TECPrivateKey; + FOperations: TPCOperationsComp; + FPreviousDirectory, FDirectory: String; + procedure Require(ACondition: Boolean; const AMessage: String); + procedure PrepareOperations; + procedure WaitForFailure; + procedure CheckFalseFlush(AClose: Boolean); + procedure CheckWorker(ACheck: TAbstractMemStorageFailureCheck); + procedure CheckSuccessfulDrain; + public + constructor Create(const ALogPath: String); + destructor Destroy; override; + procedure Run(ACheck: TAbstractMemStorageFailureCheck); + end; + +constructor TWriteControl.Create; +begin + MainThread := TThread.CurrentThread.ThreadID; + Entered := TEvent.Create(nil,True,False,''); + ReleaseWrite := TEvent.Create(nil,True,False,''); +end; + +destructor TWriteControl.Destroy; +begin + ReleaseWrite.Free; + Entered.Free; + inherited; +end; + +function TWriteControl.IsWorker: Boolean; +begin + Result := (TThread.CurrentThread.ThreadID<>MainThread) and + (TThread.CurrentThread.ThreadID<>ProducerThread); +end; + +function TFailureStorage.CreateFileMem(const AFileName: String; AReadOnly: Boolean): TFileMem; +begin + Result := TFailureFileMem.Create(Self,AFileName,AReadOnly); +end; + +function TFailureStorage.TFailureFileMem.Write(const APosition: Int64; const ABuffer; ASize: Integer): Integer; +var LControl: TWriteControl; +begin + LControl := TFailureStorage(FStorage).Control; + if Assigned(LControl) and LControl.PauseWorker and LControl.IsWorker and + (APosition>64) then begin + LControl.Entered.SetEvent; + if LControl.ReleaseWrite.WaitFor(10000)<>wrSignaled then + raise EWriteError.Create('Owned worker write gate timed out'); + end; + Result := inherited Write(APosition,ABuffer,ASize); +end; + +function TFailureStorage.TFailureFileMem.AbsoluteWrite(const AAbsolutePosition: Int64; const ABuffer; ASize: Integer): Integer; +var LControl: TWriteControl; +begin + LControl := TFailureStorage(FStorage).Control; + if Assigned(LControl) and LControl.ShortFlush and (ASize>64) then + Exit(0); + if Assigned(LControl) and LControl.FailWorker and LControl.IsWorker and + (AAbsolutePosition>64) then begin + Inc(LControl.FailureHits); + // A real partial payload reaches the file after its unstable marker was + // flushed. Cleanup must not later certify the incomplete logical index. + if ASize>1 then inherited AbsoluteWrite(AAbsolutePosition,ABuffer,ASize div 2); + raise EWriteError.Create('Injected AbstractMem background index write failure'); + end; + Result := inherited AbsoluteWrite(AAbsolutePosition,ABuffer,ASize); +end; + +function TFailureStorage.TFailureFileMem.ShouldFlushOnDestroy: Boolean; +begin + Result := inherited ShouldFlushOnDestroy; + if not Result and Assigned(TFailureStorage(FStorage).Control) then + Inc(TFailureStorage(FStorage).Control.SkippedCloseFlush); +end; + +constructor TStorageActionThread.Create; +begin + Entered := TEvent.Create(nil,True,False,''); + Done := TEvent.Create(nil,True,False,''); + inherited Create(True); + FreeOnTerminate := False; +end; + +destructor TStorageActionThread.Destroy; +begin + WaitFor; + Done.Free; + Entered.Free; + inherited; +end; + +procedure TStorageActionThread.Execute; +begin + try + try + if Action=saSave then Storage.Control.ProducerThread := ThreadID; + Entered.SetEvent; + case Action of + saSave: Storage.SaveBlockChainBlock(Operations); + saFlush: Storage.FlushCache; + saStop: Storage.UseMultithread := False; + saCloseNode: Node.Free; + end; + except + on E: Exception do ErrorMessage := E.ClassName+': '+E.Message; + end; + finally + Done.SetEvent; + end; +end; + +procedure TStorageFixture.Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('AbstractMem storage: '+AMessage); +end; + +constructor TStorageFixture.Create(const ALogPath: String); +var LID: TGUID; +begin + inherited Create; + Require(not TNode.NodeExists,'fixture requires its own node'); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(ExtractFilePath(ExpandFileName(ALogPath)))+ + 'owned-am-storage-'+GUIDToString(LID); + Require(not DirectoryExists(FDirectory),'fixture directory must be new'); + Require(ForceDirectories(FDirectory),'cannot create fixture directory'); + TNode.SetPascalCoinDataFolder(FDirectory); + FControl := TWriteControl.Create; + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FNode := TNode.Create(nil); + FNode.Bank.StorageClass := TFailureStorage; + FStorage := TFailureStorage(FNode.Bank.Storage); + FStorage.Control := FControl; + Require(FStorage.Initialize,'optional backend initialization failed'); + FStorage.AutoFlushCache := False; + FOperations := TPCOperationsComp.Create(nil); + PrepareOperations; +end; + +destructor TStorageFixture.Destroy; +begin + if Assigned(FControl) then FControl.ReleaseWrite.SetEvent; + FOperations.Free; + FNode.Free; + FKey.Free; + FControl.Free; + TNode.SetPascalCoinDataFolder(FPreviousDirectory); + inherited; +end; + +procedure TStorageFixture.PrepareOperations; +var I: Integer; LOperation: TOpTransaction; LBlock: TOperationBlock; +begin + for I := 1 to 64 do begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_5,0,I,1,FKey, + 1,1,CT_TOperationPayload_NUL); + try + Require(FOperations.OperationsHashTree.AddOperationToHashTree(LOperation), + 'cannot prepare distinct stored operations'); + finally + LOperation.Free; + end; + end; + LBlock := CT_OperationBlock_NUL; + LBlock.block := 0; + LBlock.account_key := FKey.PublicKey; + LBlock.protocol_version := CT_PROTOCOL_5; + LBlock.protocol_available := CT_PROTOCOL_5; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.fee := 64; + LBlock.operations_hash := FOperations.OperationsHashTree.HashTree; + FOperations.SetOperationBlock(LBlock); +end; + +procedure TStorageFixture.WaitForFailure; +var I: Integer; +begin + for I := 1 to 500 do begin + if FNode.HasStorageFailure then Exit; + Sleep(10); + end; + Require(False,'background failure did not reach the canonical node'); +end; + +function ReadBytes(const AFileName: String): TBytes; +var LFile: TFileStream; +begin + LFile := TFileStream.Create(AFileName,fmOpenRead+fmShareDenyNone); + try + SetLength(Result,LFile.Size); + if Length(Result)>0 then LFile.ReadBuffer(Result[0],Length(Result)); + finally + LFile.Free; + end; +end; + +function SameBytes(const ALeft, ARight: TBytes): Boolean; +begin + Result := Length(ALeft)=Length(ARight); + if Result and (Length(ALeft)>0) then + Result := CompareMem(@ALeft[0],@ARight[0],Length(ALeft)); +end; + +procedure TStorageFixture.CheckFalseFlush(AClose: Boolean); +var LZone: TAMZone; LBuffer: array[0..127] of Byte; LRaised: Boolean; + LFileName: String; LBefore, LAfter: TBytes; +begin + FStorage.UseMultithread := False; + FStorage.FlushCache; + if not AClose then FStorage.FileMem.UseCache := False; + LZone := FStorage.FileMem.New(128); + FillChar(LBuffer,SizeOf(LBuffer),42); + if AClose then FStorage.FileMem.Write(LZone.position,LBuffer,SizeOf(LBuffer)); + FControl.ShortFlush := True; + if not AClose then begin + // An unrepaired uncached range makes the real FileMem flush return False. + // The owner has already captured its first write error and must also reject + // a subsequent completion, rather than acknowledge that False as success. + LRaised := False; + try + FStorage.FileMem.Write(LZone.position,LBuffer,SizeOf(LBuffer)); + except + on E: EAbstractMem do LRaised := True; + end; + Require(LRaised,'uncached short-write fixture did not fail'); + Require(not FStorage.FileMem.FlushCache,'failed uncached range did not produce a false flush'); + end; + FStorage.AutoFlushCache := True; + LRaised := False; + try + FStorage.FinalizedUpdating; + except + on E: Exception do begin + if not ((E is EWriteError) or (E is EAbstractMem)) then raise; + LRaised := True; + end; + end; + Require(LRaised,'false completion flush was ignored'); + Require(FNode.HasStorageFailure,'false flush did not latch node failure'); + Require(FNode.GetSynchronizationStatus.StorageFailure.FileName=FStorage.FileMem.FileName, + 'failure must name the actual .am_blocks file'); + if not AClose then Exit; + LFileName := FStorage.FileMem.FileName; + LBefore := ReadBytes(LFileName); + Require((Length(LBefore)>6) and (LBefore[6]=0),'failed flush must leave an unstable marker'); + FControl.ShortFlush := False; + FreeAndNil(FNode); + FStorage := nil; + LAfter := ReadBytes(LFileName); + Require(SameBytes(LBefore,LAfter),'close retried or certified the incomplete cached index'); + Require(FControl.SkippedCloseFlush=1,'backend did not suppress unsafe destructor retry'); +end; + +procedure TStorageFixture.CheckWorker(ACheck: TAbstractMemStorageFailureCheck); +var LSave, LOther: TStorageActionThread; LBackupName, LIndexName: String; + LBackupBefore, LIndexBefore: TBytes; LRaised: Boolean; +begin + FStorage.FileMem.UseCache := False; + FControl.PauseWorker := True; + FControl.FailWorker := True; + LSave := TStorageActionThread.Create; + LOther := nil; + try + LSave.Storage := FStorage; + LSave.Operations := FOperations; + LSave.Action := saSave; + LSave.Start; + Require(FControl.Entered.WaitFor(5000)=wrSignaled,'real indexing worker did not reach the write gate'); + Require(LSave.Done.WaitFor(5000)=wrSignaled,'producer did not finish while indexing was gated'); + Require(LSave.ErrorMessage='','raw block producer failed before indexing: '+LSave.ErrorMessage); + Require(FStorage.PendingToSave>0,'fixture did not retain queued index jobs'); + LBackupName := TPCBank.GetStorageFolder('')+PathDelim+'BlockChainStream.blocks'; + LIndexName := FStorage.FileMem.FileName; + LBackupBefore := ReadBytes(LBackupName); + Require(Length(LBackupBefore)>0,'raw-block backup was not persisted'); + LOther := TStorageActionThread.Create; + LOther.Storage := FStorage; + if ACheck=amscInFlightShutdown then begin + LOther.Action := saCloseNode; + LOther.Node := FNode; + end else if ACheck=amscFailedQueueDrain then LOther.Action := saStop + else LOther.Action := saFlush; + LOther.Start; + Require(LOther.Entered.WaitFor(5000)=wrSignaled,'competing owner did not start'); + Require(LOther.Done.WaitFor(50)=wrTimeout,'competing flush/close did not wait for the in-flight write'); + FControl.ReleaseWrite.SetEvent; + Require(LOther.Done.WaitFor(5000)=wrSignaled,'failure left a flush or cooperative shutdown blocked'); + if ACheck=amscInFlightShutdown then begin + FNode := nil; + FStorage := nil; + Require(LOther.ErrorMessage='','node shutdown failed: '+LOther.ErrorMessage); + Require(not TNode.NodeExists,'node survived cooperative shutdown'); + end else begin + WaitForFailure; + if ACheck=amscBackgroundWriteFailure then + Require(LOther.ErrorMessage<>'','an admitted competing flush certified a failed index') + else Require(LOther.ErrorMessage='','failed queue teardown raised: '+LOther.ErrorMessage); + FStorage.UseMultithread := False; + LRaised := False; + try + FStorage.SaveBlockChainBlock(FOperations); + except + on E: EWriteError do LRaised := True; + end; + Require(LRaised,'a later block/index write bypassed the failure latch'); + LIndexBefore := ReadBytes(LIndexName); + FreeAndNil(FNode); + FStorage := nil; + Require(SameBytes(LIndexBefore,ReadBytes(LIndexName)), + 'failed index was retried or modified during cleanup'); + end; + Require(FControl.FailureHits>0,'no actual background payload write failed'); + Require(FControl.SkippedCloseFlush=1,'unsafe failed-index close flush was not suppressed'); + Require(SameBytes(LBackupBefore,ReadBytes(LBackupName)), + 'failed indexing changed the previous raw-block backup'); + LIndexBefore := ReadBytes(LIndexName); + Require((Length(LIndexBefore)>6) and (LIndexBefore[6]=0), + 'failed logical index was marked stable'); + finally + FControl.ReleaseWrite.SetEvent; + LOther.Free; + LSave.Free; + end; +end; + +procedure TStorageFixture.CheckSuccessfulDrain; +var LLoaded: TPCOperationsComp; LFileName: String; LBytes: TBytes; +begin + Require(FStorage.SaveBlockChainBlock(FOperations),'normal save failed'); + FStorage.UseMultithread := False; + Require(FStorage.PendingToSave=0,'normal shutdown did not drain the index queue'); + FStorage.FlushCache; + Require(not FNode.HasStorageFailure,'normal drain latched a false failure'); + LLoaded := TPCOperationsComp.Create(nil); + try + Require(FStorage.LoadBlockChainBlock(LLoaded,0),'normal saved block is unreadable'); + Require(LLoaded.Count=64,'normal drain lost operations'); + finally + LLoaded.Free; + end; + LFileName := FStorage.FileMem.FileName; + FreeAndNil(FNode); + FStorage := nil; + LBytes := ReadBytes(LFileName); + Require((Length(LBytes)>6) and (LBytes[6]=1),'successful close did not persist a stable index'); +end; + +procedure TStorageFixture.Run(ACheck: TAbstractMemStorageFailureCheck); +begin + case ACheck of + amscFalseCompletionFlush: CheckFalseFlush(False); + amscBackgroundWriteFailure: CheckWorker(ACheck); + amscFailedQueueDrain: CheckWorker(ACheck); + amscFailedCleanupPreservesMarker: CheckFalseFlush(True); + amscSuccessfulDrain: CheckSuccessfulDrain; + amscInFlightShutdown: CheckWorker(ACheck); + end; +end; + +function RunAbstractMemStorageFailureCheck(ACheck: TAbstractMemStorageFailureCheck; const ALogPath: String): Integer; +var LFixture: TStorageFixture; LLog: TStringList; +begin + Result := 1; + LLog := TStringList.Create; + try + LLog.Add('CHECK '+GetEnumName(TypeInfo(TAbstractMemStorageFailureCheck),Ord(ACheck))); + try + LFixture := TStorageFixture.Create(ALogPath); + try + LFixture.Run(ACheck); + finally + LFixture.Free; + end; + LLog.Add('PASS'); + Result := 0; + except + on E: Exception do LLog.Add('FAIL '+E.ClassName+': '+E.Message); + end; + LLog.SaveToFile(ALogPath); + finally + LLog.Free; + end; +end; + +function RunAbstractMemStorageFailureChecks(const ALogPath: String): Integer; +var LCheck: TAbstractMemStorageFailureCheck; LLog: TStringList; +begin + Result := 0; + LLog := TStringList.Create; + try + for LCheck := Low(TAbstractMemStorageFailureCheck) to High(TAbstractMemStorageFailureCheck) do begin + if RunAbstractMemStorageFailureCheck(LCheck,ALogPath+'.case-'+IntToStr(Ord(LCheck))+'.log')<>0 then + Result := 1; + LLog.Add('Completed '+GetEnumName(TypeInfo(TAbstractMemStorageFailureCheck),Ord(LCheck))); + LLog.SaveToFile(ALogPath); + end; + if Result=0 then LLog.Add('PASS 6') else LLog.Add('FAIL'); + LLog.SaveToFile(ALogPath); + finally + LLog.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/account-selector/AccountSelectorNativeProbe.dpr b/src/tests/experimental/native/account-selector/AccountSelectorNativeProbe.dpr new file mode 100644 index 000000000..782faf010 --- /dev/null +++ b/src/tests/experimental/native/account-selector/AccountSelectorNativeProbe.dpr @@ -0,0 +1,39 @@ +program AccountSelectorNativeProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Use the existing owned private-desktop broker and one absolute fresh log path. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +uses {$IFDEF FPC}Interfaces,{$ENDIF} Forms, Classes, SysUtils, UCrypto, + UAccountSelector.Checks; + +var LLog: TStringList; + {$IFDEF FPC}I: Integer;{$ENDIF} +begin + ExitCode := 1; + Application.Initialize; + LLog := TStringList.Create; + try + try + if ParamCount <> 1 then raise Exception.Create('Pass one absolute log filename'); + TCrypto.InitCrypto; + CheckAccountSelector(LLog); + LLog.Add('PASS complete experimental account selector checks'); + ExitCode := 0; + except + on E: Exception do begin + LLog.Add('FAIL '+E.ClassName+': '+E.Message); + {$IFDEF FPC} + // Keep checked failures attributable to the actual storage/UI operation. + LLog.Add(BackTraceStrFunc(ExceptAddr)); + for I := 0 to ExceptFrameCount-1 do + LLog.Add(BackTraceStrFunc(ExceptFrames[I])); + {$ENDIF} + end; + end; + if ParamCount = 1 then LLog.SaveToFile(ParamStr(1)); + finally + LLog.Free; + end; +end. diff --git a/src/tests/experimental/native/account-selector/README.md b/src/tests/experimental/native/account-selector/README.md new file mode 100644 index 000000000..4cc23e83f --- /dev/null +++ b/src/tests/experimental/native/account-selector/README.md @@ -0,0 +1,33 @@ +# Experimental account selector + +[AccountSelectorNativeProbe.dpr](AccountSelectorNativeProbe.dpr) tests the +experimental account-selection form, visual grid and confirmed-account source. +It creates no node, wallet file or peer connection. + +Build it with Delphi (VCL) and Lazarus/FPC, using the experimental wallet's +source paths and compiler settings plus this directory. Enable range and +overflow checks (`-Cr -Co` for FPC). FPC also needs the matching LCL, LazUtils +and embedded Generics.Collections paths. Keep each compiler's output separate, +with the matching OpenSSL runtime beside each EXE. + +Run each EXE through the [private-desktop broker](../README.md#private-desktop-broker), +passing one absolute log filename in a new folder: + +```powershell +& $Broker $DelphiSelectorExe '<new-folder>\delphi.log' +& $Broker $FpcSelectorExe '<new-folder>\fpc.log' +``` + +Success writes `PASS complete experimental account selector checks` and exits +with code 0. Failure writes `FAIL` and exits nonzero. + +The [checks](UAccountSelector.Checks.pas) cover: + +- sale-price ordering +- snapshot caching +- key selection, refreshed only by reassigning `WalletKeys` +- filters and totals +- rapid key changes +- retries +- background type statistics +- closing mid-read diff --git a/src/tests/experimental/native/account-selector/UAccountSelector.Checks.pas b/src/tests/experimental/native/account-selector/UAccountSelector.Checks.pas new file mode 100644 index 000000000..d3411d056 --- /dev/null +++ b/src/tests/experimental/native/account-selector/UAccountSelector.Checks.pas @@ -0,0 +1,766 @@ +unit UAccountSelector.Checks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Actual experimental controls and confirmed storage; only storage reads can + be held or made obsolete. No singleton node, wallet files or peers are used. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses Classes; + +procedure CheckAccountSelector(ALog: TStrings); + +implementation + +uses {$IFDEF MSWINDOWS}Windows,{$ENDIF} + SysUtils, Types, Forms, Controls, StdCtrls, ExtCtrls, SyncObjs, Variants, + Generics.Collections, UAccounts, UBaseTypes, UCrypto, UConst, UPCDataTypes, UWallet, + UCommon.Data, UCommon.UI, UVisualGrid, UNode, UAccountSelectorData, UFRMAccountSelect, + UFRMMemoText, UPCAccountsOrdenations, UAbstractMem; + +type + TSalePriceOrderAccess = class(TAccountsOrderedBySalePrice) + public + function CompareUsing(ARead: TCallReturnAccount; ALeft, ARight: Integer; + ASearching: Boolean; APrice: UInt64): Integer; + end; + + TSalePriceRecords = class + public + Prices: array[0..6] of UInt64; + function ReadAccount(ANumber: Integer; var AAccount: TAccount): Boolean; + end; + + TSelectorSafeBox = class(TPCSafeBox) + public + procedure Build(const AFirst, ASecond, AForeign: TAccountKey); + end; + + TControlledSelectorSource = class(TAccountSelectorDataSource) + protected + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ATable: TDataTable): TPageFetchResult; override; + function ReadChain(ASafeBox: TPCSafeBox): TAccountSelectorChain; override; + procedure ReadKeyNumbers(const AChain: TAccountSelectorChain; + const AKey: TAccountKey; const ANumbers: TList<Cardinal>); override; + procedure ReadAccounts(const AChain: TAccountSelectorChain; + const ANumbers: TArray<Cardinal>; out ARows: TArray<TAccount>); override; + public + Entered, ReleaseRead: TEvent; + HoldReads, ChangeEveryRead: Boolean; + ReadCalls, KeyCalls, RecordsRead, LargestRead: Integer; + FetchCalls, FetchFinished, StoragePhase: Integer; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + procedure Hold; + procedure Release; + end; + + TSelectorForm = class(TFRMAccountSelect) + private + FTestSafeBox: TPCSafeBox; + protected + function AccountSafeBox: TPCSafeBox; override; + function CreateAccountSource: TAccountSelectorDataSource; override; + procedure ShowTypeStatsReport(const AStats: TAccountSelectorTypeStats); override; + public + Reports, Heartbeats, Publications: Integer; + LastStats: TAccountSelectorTypeStats; + ReportText: String; + ReportObserved: Boolean; + ExpectedOwner: TAccountKey; + WatchOwner, WrongPublication: Boolean; + SavedCompletion: TNotifyEvent; + constructor CreateForSafeBox(ASafeBox: TPCSafeBox); + procedure AfterConstruction; override; + function Source: TControlledSelectorSource; + procedure Beat(Sender: TObject); + procedure Completed(Sender: TObject); + procedure Choose(const AName: String); + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure PumpFor(AMilliseconds: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64+AMilliseconds; + repeat + CheckSynchronize(0); + Application.ProcessMessages; + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure Settle(AForm: TSelectorForm); +var LUntil: UInt64; +begin + LUntil := GetTickCount64+5000; + repeat + PumpFor(25); + if not AForm.AccountGrid.IsUpdating and not AForm.StatsRunning then begin + PumpFor(100); + if not AForm.AccountGrid.IsUpdating and not AForm.StatsRunning then Exit; + end; + until GetTickCount64 >= LUntil; + raise Exception.Create(Format('Account selector deadline: updating=%s suspended=%s waiting=%s manual=%s stats=%s reads=%d keys=%d records=%d fetch=%d finished=%d phase=%d beats=%d publications=%d rows=%d', + [BoolToStr(AForm.AccountGrid.IsUpdating,True),BoolToStr(AForm.AccountGrid.RefreshSuspended,True), + BoolToStr(AForm.AccountGrid.RefreshThrottle.Waiting,True),BoolToStr(AForm.AccountGrid.LoadingVisible,True), + BoolToStr(AForm.StatsRunning,True),AForm.Source.ReadCalls,AForm.Source.KeyCalls,AForm.Source.RecordsRead, + AForm.Source.FetchCalls,AForm.Source.FetchFinished,AForm.Source.StoragePhase, + AForm.Heartbeats,AForm.Publications,AForm.AccountGrid.RowCount])); +end; + +function TSalePriceOrderAccess.CompareUsing(ARead: TCallReturnAccount; + ALeft, ARight: Integer; ASearching: Boolean; APrice: UInt64): Integer; +var LRead: TCallReturnAccount; LNumber: Integer; LInfo: TAccountInfo; +begin + LRead := FCallReturnAccount; + LNumber := FSearching_AccountNumber; + LInfo := FSearching_AccountInfo; + try + FCallReturnAccount := ARead; + FSearching_AccountNumber := -1; + if ASearching then begin + FSearching_AccountNumber := ALeft; + FSearching_AccountInfo.price := APrice; + end; + Result := DoCompareData(ALeft,ARight); + finally + FCallReturnAccount := LRead; + FSearching_AccountNumber := LNumber; + FSearching_AccountInfo := LInfo; + end; +end; + +function TSalePriceRecords.ReadAccount(ANumber: Integer; var AAccount: TAccount): Boolean; +begin + AAccount := CT_Account_NUL; + Result := (ANumber >= Low(Prices)) and (ANumber <= High(Prices)); + if Result then AAccount.accountInfo.price := Prices[ANumber]; +end; + +procedure CheckSalePriceOrder(ASafeBox: TPCSafeBox; ALog: TStrings); +var LRecords: TSalePriceRecords; LOrder: TSalePriceOrderAccess; + I,J,LExpected,LActual: Integer; +begin + LRecords := TSalePriceRecords.Create; + try + LRecords.Prices[0] := 0; + LRecords.Prices[1] := 1; + LRecords.Prices[2] := UInt64(High(Int64)); + LRecords.Prices[3] := UInt64(High(Int64))+1; + LRecords.Prices[4] := High(UInt64)-1; + LRecords.Prices[5] := High(UInt64); + LRecords.Prices[6] := High(UInt64); + LOrder := TSalePriceOrderAccess(ASafeBox.AccountsOrderedBySalePrice); + // Exercise the real comparer in both stored and replacement-search modes. + for I := Low(LRecords.Prices) to High(LRecords.Prices) do + for J := Low(LRecords.Prices) to High(LRecords.Prices) do begin + if LRecords.Prices[I] < LRecords.Prices[J] then LExpected := -1 + else if LRecords.Prices[I] > LRecords.Prices[J] then LExpected := 1 + else if I < J then LExpected := -1 + else if I > J then LExpected := 1 + else LExpected := 0; + LActual := LOrder.CompareUsing(LRecords.ReadAccount,I,J,False,0); + Require(LActual = LExpected,'Stored sale-price ordering failed across the full UInt64 range'); + if LRecords.Prices[I] < LRecords.Prices[J] then LExpected := -1 + else LExpected := 1; + LActual := LOrder.CompareUsing(LRecords.ReadAccount,100+I,J,True,LRecords.Prices[I]); + Require(LActual = LExpected,'Replacement sale-price ordering lost the price or account tie-breaker'); + Require(LOrder.CompareUsing(LRecords.ReadAccount,I,I,True,High(UInt64)) = 0, + 'An account compared with itself must remain equal during replacement'); + end; + ALog.Add('PASS full-range UInt64 sale-price order, account tie-breaker and replacement search'); + finally + LRecords.Free; + end; +end; + +procedure TSelectorSafeBox.Build(const AFirst, ASecond, AForeign: TAccountKey); +var LBlock: TOperationBlock; LAccount: TAccount; I: Integer; +begin + for I := 0 to 3 do begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AFirst; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I*CT_NewLineSecondsAvg; + AddNew(LBlock); + end; + for I := 0 to AccountsCount-1 do begin + LAccount := Account(I); + if I >= 10 then LAccount.accountInfo.accountKey := ASecond; + if I = 19 then LAccount.accountInfo.accountKey := AForeign; + LAccount.balance := (I+1)*10000; + LAccount.name.FromString('selector-'+IntToStr(I)); + case I of + 1: LAccount.account_type := 42; + 2: LAccount.account_type := 9999; + 3: LAccount.account_type := 10000; + 4: LAccount.account_type := 65535; + else + LAccount.account_type := 0; + end; + if I in [1,11,19] then begin + LAccount.accountInfo.state := as_ForSale; + LAccount.accountInfo.new_publicKey := ASecond; + LAccount.accountInfo.price := 10000; + end; + if I = 12 then begin + LAccount.accountInfo.state := as_ForSale; + LAccount.accountInfo.new_publicKey := CT_AccountInfo_NUL.accountKey; + LAccount.accountInfo.price := 10000; + end; + UpdateAccount(I,LAccount.accountInfo,LAccount.name,LAccount.account_type, + LAccount.balance,LAccount.n_operation,LAccount.account_data,LAccount.account_seal, + aus_transaction_commit,0,0,False,False); + end; +end; + +constructor TControlledSelectorSource.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + Entered := TEvent.Create(nil,True,False,''); + ReleaseRead := TEvent.Create(nil,True,True,''); +end; + +destructor TControlledSelectorSource.Destroy; +begin + if Assigned(ReleaseRead) then ReleaseRead.SetEvent; + Entered.Free; + ReleaseRead.Free; + inherited Destroy; +end; + +procedure TControlledSelectorSource.Hold; +begin + Entered.ResetEvent; + ReleaseRead.ResetEvent; + HoldReads := True; +end; + +procedure TControlledSelectorSource.Release; +begin + HoldReads := False; + ReleaseRead.SetEvent; +end; + +function TControlledSelectorSource.FetchPage( + {$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ATable: TDataTable): TPageFetchResult; +begin + InterlockedIncrement(FetchCalls); + InterlockedExchange(StoragePhase,1); + try + Result := inherited FetchPage(AParams,ATable); + finally + InterlockedIncrement(FetchFinished); + InterlockedExchange(StoragePhase,9); + end; +end; + +function TControlledSelectorSource.ReadChain(ASafeBox: TPCSafeBox): TAccountSelectorChain; +begin + InterlockedExchange(StoragePhase,2); + Result := inherited ReadChain(ASafeBox); + InterlockedExchange(StoragePhase,3); +end; + +procedure TControlledSelectorSource.ReadKeyNumbers(const AChain: TAccountSelectorChain; + const AKey: TAccountKey; const ANumbers: TList<Cardinal>); +begin + InterlockedIncrement(KeyCalls); + inherited ReadKeyNumbers(AChain,AKey,ANumbers); +end; + +procedure TControlledSelectorSource.ReadAccounts(const AChain: TAccountSelectorChain; + const ANumbers: TArray<Cardinal>; out ARows: TArray<TAccount>); +var LUntil: UInt64; +begin + InterlockedIncrement(ReadCalls); + InterlockedExchange(StoragePhase,4); + if ChangeEveryRead then + raise EAccountSelectorSnapshotChanged.Create('Controlled obsolete chain revision'); + if HoldReads then begin + Entered.SetEvent; + LUntil := GetTickCount64+5000; + while ReleaseRead.WaitFor(10) <> wrSignaled do begin + CheckAccountSelectorCancelled; + Require(GetTickCount64 < LUntil,'Controlled selector read was never released'); + end; + end; + CheckAccountSelectorCancelled; + InterlockedExchangeAdd(RecordsRead,Length(ANumbers)); + if Length(ANumbers) > LargestRead then LargestRead := Length(ANumbers); + inherited ReadAccounts(AChain,ANumbers,ARows); + InterlockedExchange(StoragePhase,5); +end; + +constructor TSelectorForm.CreateForSafeBox(ASafeBox: TPCSafeBox); +begin + FTestSafeBox := ASafeBox; + inherited Create(nil); +end; + +procedure TSelectorForm.AfterConstruction; +var LTimer: TTimer; +begin + // LCL creates resource-backed controls in AfterConstruction; VCL is ready too. + inherited AfterConstruction; + SetBounds(80,90,900,560); + SavedCompletion := AccountGrid.OnFinishedUpdating; + AccountGrid.OnFinishedUpdating := Completed; + LTimer := TTimer.Create(Self); + LTimer.Interval := 20; + LTimer.OnTimer := Beat; + LTimer.Enabled := True; +end; + +function TSelectorForm.AccountSafeBox: TPCSafeBox; +begin + Result := FTestSafeBox; +end; + +function TSelectorForm.CreateAccountSource: TAccountSelectorDataSource; +begin + Result := TControlledSelectorSource.Create(Self); +end; + +function TSelectorForm.Source: TControlledSelectorSource; +begin + Result := TControlledSelectorSource(AccountSource); +end; + +procedure TSelectorForm.Beat(Sender: TObject); +var I: Integer; LReport: TFRMMemoText; +begin + Inc(Heartbeats); + for I := 0 to ComponentCount-1 do + if Components[I] is TFRMMemoText then begin + LReport := TFRMMemoText(Components[I]); + if LReport.Visible then begin + Require(LReport.Caption = 'Account Type Stats','The original statistics report form was not presented'); + ReportText := LReport.DataText; + ReportObserved := True; + LReport.ModalResult := mrOk; + end; + end; +end; + +procedure TSelectorForm.Completed(Sender: TObject); +var I: Integer; LNumber: Cardinal; LAccount: TAccount; +begin + if Assigned(SavedCompletion) then SavedCompletion(Sender); + Inc(Publications); + if not WatchOwner then Exit; + for I := 0 to AccountGrid.RowCount-1 do begin + LNumber := Cardinal(TDataRowData(AccountGrid.Rows[I])['AccountNumber']); + LAccount := FTestSafeBox.Account(LNumber); + if not TAccountComp.EqualAccountKeys(LAccount.accountInfo.accountKey,ExpectedOwner) then + WrongPublication := True; + end; +end; + +procedure TSelectorForm.Choose(const AName: String); +var LIndex: Integer; +begin + LIndex := cbMyPrivateKeys.Items.IndexOf(AName); + Require(LIndex >= 0,'The requested public-key choice is absent: '+AName); + cbMyPrivateKeys.ItemIndex := LIndex; + Require(Assigned(cbMyPrivateKeys.OnChange),'Key dropdown has no native selection-change binding'); + cbMyPrivateKeys.OnChange(cbMyPrivateKeys); +end; + +procedure TSelectorForm.ShowTypeStatsReport(const AStats: TAccountSelectorTypeStats); +begin + Require(TThread.CurrentThread.ThreadID = MainThreadID,'Report presentation ran off the UI thread'); + LastStats := AStats; + ReportObserved := False; + // Observe and close the actual modal report through the fixture heartbeat; + // production still creates and displays its original TFRMMemoText. + inherited ShowTypeStatsReport(AStats); + Require(ReportObserved,'The actual modal type-statistics report was never visible'); + Inc(Reports); +end; + +procedure AssertOwner(AForm: TSelectorForm; const AKey: TAccountKey; AExpectedCount: Integer); +var I: Integer; LNumber: Cardinal; +begin + Require(AForm.AccountGrid.RowCount = AExpectedCount,'Selected public key produced the wrong row count'); + Require(AForm.lblAccountsCount.Caption = IntToStr(AExpectedCount),'Selected key count label is stale'); + for I := 0 to AForm.AccountGrid.RowCount-1 do begin + LNumber := Cardinal(TDataRowData(AForm.AccountGrid.Rows[I])['AccountNumber']); + Require(TAccountComp.EqualAccountKeys(AForm.FTestSafeBox.Account(LNumber).accountInfo.accountKey,AKey), + 'A sorted key choice displayed accounts belonging to another owner'); + end; +end; + +procedure LegacyReport(ASafeBox: TPCSafeBox; AText: TStrings); +type + TLegacyTotal = record + Accounts: Integer; + Balance: Int64; + end; +var LZero,LInvalid: TLegacyTotal; LDigits: array[0..6,1..4,0..9] of TLegacyTotal; + LAccount: TAccount; C,I,J,K: Integer; +begin + FillChar(LZero,SizeOf(LZero),0); + FillChar(LInvalid,SizeOf(LInvalid),0); + FillChar(LDigits,SizeOf(LDigits),0); + for C := 0 to ASafeBox.AccountsCount-1 do begin + LAccount := ASafeBox.Account(C); + if LAccount.account_type = 0 then begin + Inc(LZero.Accounts); + Inc(LZero.Balance,LAccount.balance); + end else if LAccount.account_type < 10000 then begin + Inc(LInvalid.Accounts); + Inc(LInvalid.Balance,LAccount.balance); + end; + K := (LAccount.account_type mod 100000) div 10000; + for I := 0 to 9 do begin + J := (LAccount.account_type mod 10000) div 1000; + if J = I then begin + Inc(LDigits[K,1,J].Accounts); + Inc(LDigits[K,1,J].Balance,LAccount.balance); + end; + J := (LAccount.account_type mod 1000) div 100; + if J = I then begin + Inc(LDigits[K,2,J].Accounts); + Inc(LDigits[K,2,J].Balance,LAccount.balance); + end; + J := (LAccount.account_type mod 100) div 10; + if J = I then begin + Inc(LDigits[K,3,J].Accounts); + Inc(LDigits[K,3,J].Balance,LAccount.balance); + end; + J := LAccount.account_type mod 10; + if J = I then begin + Inc(LDigits[K,4,J].Accounts); + Inc(LDigits[K,4,J].Balance,LAccount.balance); + end; + end; + end; + AText.Clear; + AText.Add('*** Account type stats ***'); + AText.Add(Format('Current block:%d accounts:%d coins:%s', + [ASafeBox.BlocksCount,ASafeBox.AccountsCount,TAccountComp.FormatMoney(ASafeBox.TotalBalance)])); + AText.Add(''); + AText.Add('** Basic stats **'); + AText.Add(Format('Account type=0 accounts:%d coins:%s',[LZero.Accounts,TAccountComp.FormatMoney(LZero.Balance)])); + AText.Add(Format('Account type [1..9999] accounts:%d coins:%s',[LInvalid.Accounts,TAccountComp.FormatMoney(LInvalid.Balance)])); + AText.Add(''); + AText.Add('*** Stats by group ***'); + for K := 0 to 6 do begin + AText.Add(''); + AText.Add(Format('** Group [%d..%d] **',[K*10000,(K+1)*10000-1])); + for J := 0 to 9 do AText.Add(Format('Account type like %d%d*** accounts:%d coins:%s', + [K,J,LDigits[K,1,J].Accounts,TAccountComp.FormatMoney(LDigits[K,1,J].Balance)])); + for J := 0 to 9 do AText.Add(Format('Account type like %d*%d** accounts:%d coins:%s', + [K,J,LDigits[K,2,J].Accounts,TAccountComp.FormatMoney(LDigits[K,2,J].Balance)])); + for J := 0 to 9 do AText.Add(Format('Account type like %d**%d* accounts:%d coins:%s', + [K,J,LDigits[K,3,J].Accounts,TAccountComp.FormatMoney(LDigits[K,3,J].Balance)])); + for J := 0 to 9 do AText.Add(Format('Account type like %d***%d accounts:%d coins:%s', + [K,J,LDigits[K,4,J].Accounts,TAccountComp.FormatMoney(LDigits[K,4,J].Balance)])); + end; +end; + +procedure CheckGenericSnapshotInvalidation(ASafeBox: TPCSafeBox; + const AFirst,ASecond,AEmpty: TAccountKey; ALog: TStrings); +var LSource: TControlledSelectorSource; LAdapter: IDataSource; + LFilter: TAccountSelectorFilter; LCriteria: TFilterCriteria; + LParams: TPageFetchParams; LResult: TPageFetchResult; LTable: TDataTable; + LBefore: Integer; +begin + LSource := TControlledSelectorSource.Create(nil); + LAdapter := LSource; + try + LSource.SnapshotCacheEnabled := True; + LFilter := Default(TAccountSelectorFilter); + LFilter.MyAccounts := True; + LFilter.SpecificOwner := True; + LFilter.MaxBalance := -1; + SetLength(LFilter.OwnerKeys,1); + LFilter.OwnerKeys[0] := AFirst; + SetLength(LCriteria,1); + LCriteria[0] := Default(TColumnFilter); + LCriteria[0].ColumnName := 'AccountNumber'; + LCriteria[0].Filter := vgfNumericGTE; + LCriteria[0].Sort := sdDescending; + SetLength(LCriteria[0].Values,1); + LCriteria[0].Values[0] := 0; + LParams := TPageFetchParams.Create(0,-1,LCriteria,foAnd); + LSource.Configure(ASafeBox,LFilter); + // These criteria deliberately enter the inherited generic snapshot cache. + LResult := LAdapter.FetchPage(LParams,LTable); + Require((LResult.TotalDataCount = 10) and (Length(LTable.Rows) = 10) and + (Integer(TDataRowData(LTable.Rows[0])['AccountNumber']) = 9), + 'Inherited generic search/sort did not use the selected owner snapshot'); + LFilter.OwnerKeys[0] := ASecond; + LSource.Configure(ASafeBox,LFilter); + LResult := LAdapter.FetchPage(LParams,LTable); + Require((LResult.TotalDataCount = 9) and (Length(LTable.Rows) = 9) and + (Integer(TDataRowData(LTable.Rows[0])['AccountNumber']) = 18), + 'Configure reused an obsolete inherited generic snapshot after a key change'); + LFilter.OwnerKeys[0] := AEmpty; + LSource.Configure(ASafeBox,LFilter); + LResult := LAdapter.FetchPage(LParams,LTable); + Require((LResult.TotalDataCount = 0) and (Length(LTable.Rows) = 0), + 'The inherited cache retained an old owner for an empty selected key'); + LFilter.MyAccounts := False; + LFilter.SpecificOwner := False; + LFilter.OwnerKeys := nil; + LSource.Configure(ASafeBox,LFilter); + LParams.PageSize := 3; + LParams.PageIndex := 2; + LResult := LAdapter.FetchPage(LParams,LTable); + Require((LResult.TotalDataCount = 20) and (LResult.PageCount = 7) and + (Length(LTable.Rows) = 3) and + (Integer(TDataRowData(LTable.Rows[0])['AccountNumber']) = 13) and + (Integer(TDataRowData(LTable.Rows[2])['AccountNumber']) = 11), + 'Generic sorting/paging did not apply to the complete selected query'); + LBefore := LSource.RecordsRead; + LParams.PageIndex := 3; + LResult := LAdapter.FetchPage(LParams,LTable); + Require((LSource.RecordsRead = LBefore) and (LResult.TotalDataCount = 20), + 'A warm generic page reread storage instead of its valid snapshot'); + ALog.Add('PASS inherited snapshot cache invalidates across key/empty/All configurations'); + finally + LAdapter := nil; + LSource.Free; + end; +end; + +procedure CheckAccountSelector(ALog: TStrings); +var LSafeBox: TSelectorSafeBox; LKeys: TWalletKeys; LFirst,LSecond,LEmpty,LForeign: TECPrivateKey; + LForm: TSelectorForm; LBefore,LBeats,LPublications,LWalletHandlers: Integer; LStarted: UInt64; + LExpected: TStringList; LTask: TAccountSelectorStatsTask; +begin + Require(not TNode.NodeExists,'The selector probe must not initialize the singleton node'); + LSafeBox := nil; + LKeys := nil; + LFirst := TECPrivateKey.Create; + LSecond := TECPrivateKey.Create; + LEmpty := TECPrivateKey.Create; + LForeign := TECPrivateKey.Create; + LForm := nil; + LExpected := TStringList.Create; + try + LFirst.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LSecond.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LEmpty.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LForeign.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LSafeBox := TSelectorSafeBox.Create; + LSafeBox.Build(LFirst.PublicKey,LSecond.PublicKey,LForeign.PublicKey); + CheckSalePriceOrder(LSafeBox,ALog); + CheckGenericSnapshotInvalidation(LSafeBox,LFirst.PublicKey,LSecond.PublicKey,LEmpty.PublicKey,ALog); + LKeys := TWalletKeys.Create(nil); + LKeys.AddPublicKey('Zulu owner',LFirst.PublicKey); + LKeys.AddPublicKey('Alpha owner',LSecond.PublicKey); + LKeys.AddPublicKey('Empty owner',LEmpty.PublicKey); + LWalletHandlers := Length(LKeys.OnChanged.Handlers)+Length(LKeys.OnChanged.MainThreadHandlers); + LForm := TSelectorForm.CreateForSafeBox(LSafeBox); + LForm.WalletKeys := LKeys; + Require(Length(LKeys.OnChanged.Handlers)+Length(LKeys.OnChanged.MainThreadHandlers) = LWalletHandlers, + 'The selector added an unsafe live-wallet UI subscription'); + LForm.AccountGrid.PageMode := vpmSingle; + LForm.Show; + Settle(LForm); + Require(LForm.AccountGrid.RowCount = 20,'Global All did not display the complete confirmed fixture'); + Require(LForm.Source.RecordsRead <= 40,'Unfiltered All materialized the chain more than its requested rows'); + LForm.cbMyAccounts.Checked := True; + LForm.cbMyAccounts.OnClick(LForm.cbMyAccounts); + Settle(LForm); + Require(LForm.cbMyPrivateKeys.ItemIndex = 0,'Initial wallet choice must explicitly select All'); + LBefore := LForm.Source.KeyCalls; + LForm.Choose('Alpha owner'); + Settle(LForm); + AssertOwner(LForm,LSecond.PublicKey,9); + Require(LForm.Source.KeyCalls > LBefore,'My Accounts did not use the existing public-key index'); + LForm.Choose('Zulu owner'); + Settle(LForm); + AssertOwner(LForm,LFirst.PublicKey,10); + LBefore := LForm.Source.RecordsRead; + LForm.Choose('Empty owner'); + Settle(LForm); + Require((LForm.AccountGrid.RowCount = 0) and (LForm.lblAccountsCount.Caption = '0'), + 'An empty selected key must not mean all wallet or global accounts'); + Require(LForm.Source.RecordsRead = LBefore,'An absent key unexpectedly scanned account records'); + LKeys.Delete(LKeys.IndexOfAccountKey(LEmpty.PublicKey)); + LForm.WalletKeys := LKeys; + Settle(LForm); + Require((LForm.cbMyPrivateKeys.ItemIndex > 0) and (LForm.AccountGrid.RowCount = 0), + 'Removing the selected key silently broadened its scope to All'); + LKeys.AddPublicKey('Empty owner',LEmpty.PublicKey); + LForm.WalletKeys := LKeys; + Settle(LForm); + Require(LForm.AccountGrid.RowCount = 0,'Restoring an empty key selected unrelated accounts'); + ALog.Add('PASS sorted key identity, disjoint owners, indexed membership, empty key and All'); + + LForm.Choose('Alpha owner'); + LForm.cbAccountsBalance.Checked := True; + LForm.ebMinBalance.Text := '15.0000'; + LForm.ebMaxBalance.Text := ''; + LForm.cbAccountsName.Checked := True; + LForm.ebAccountName.Text := 'selector-1'; + LForm.bbSearch.OnClick(LForm.bbSearch); + Settle(LForm); + Require((LForm.AccountGrid.RowCount = 5) and + (LForm.lblAccountsBalance.Caption = TAccountComp.FormatMoney(850000)), + 'Name and balance filters changed their matching accounts or aggregate'); + LForm.cbAccountsBalance.Checked := False; + LForm.cbAccountsName.Checked := False; + LForm.cbOnlyForPrivateSaleToMe.Checked := True; + LForm.Choose('(All)'); + Settle(LForm); + Require(LForm.AccountGrid.RowCount = 3, + 'Wallet All/private-sale destination semantics must include a nonwallet current owner'); + LForm.Choose('Zulu owner'); + Settle(LForm); + Require(LForm.AccountGrid.RowCount = 1,'A specific owner did not restrict private-sale results'); + LForm.cbOnlyForPublicSale.Checked := True; + LForm.bbSearch.OnClick(LForm.bbSearch); + Settle(LForm); + Require(LForm.AccountGrid.RowCount = 0,'Contradictory public/private sale filters must remain conjunctive'); + LForm.cbOnlyForPrivateSaleToMe.Checked := False; + LForm.cbOnlyForSale.Checked := True; + LForm.Choose('Alpha owner'); + Settle(LForm); + Require((LForm.AccountGrid.RowCount = 1) and + (Cardinal(TDataRowData(LForm.AccountGrid.Rows[0])['AccountNumber']) = 12), + 'Public sale filtering changed the confirmed result'); + LForm.cbOnlyForSale.Checked := False; + LForm.cbOnlyForPublicSale.Checked := False; + LForm.DefaultAccount := 11; + LForm.AllowSelect := True; + LForm.AccountGrid.ManualPageSize := 1; + LForm.AccountGrid.PageMode := vpmManual; + LForm.bbSearch.OnClick(LForm.bbSearch); + Settle(LForm); + Require((LForm.AccountGrid.PageIndex = 1) and (LForm.GetSelected = 11) and LForm.bbSelect.Enabled, + 'Default account was not navigated to and selected on a one-row page'); + LForm.DefaultAccount := -1; + LForm.AccountGrid.PageMode := vpmSingle; + LForm.Choose('Empty owner'); + Settle(LForm); + ALog.Add('PASS name/balance/sale combinations, complete totals and default account selection'); + + LForm.Source.Hold; + LForm.Choose('Zulu owner'); + Require(LForm.Source.Entered.WaitFor(1000) = wrSignaled,'Controlled old-key provider did not enter'); + LStarted := GetTickCount64; + LForm.Choose('Alpha owner'); + LForm.Choose('Empty owner'); + LForm.Choose('Zulu owner'); + LForm.Choose('Alpha owner'); + Require(GetTickCount64-LStarted < 400,'Key changes waited for an obsolete provider'); + LForm.ExpectedOwner := LSecond.PublicKey; + LForm.WatchOwner := True; + LPublications := LForm.Publications; + LForm.Source.Release; + Settle(LForm); + AssertOwner(LForm,LSecond.PublicKey,9); + Require((LForm.Publications > LPublications) and not LForm.WrongPublication, + 'A stale rapid-key result was published'); + LForm.WatchOwner := False; + ALog.Add('PASS held provider remains nonblocking and only latest key rows publish'); + + LForm.Source.ChangeEveryRead := True; + LBefore := LForm.Source.ReadCalls; + LForm.AccountGrid.RefreshGrid; + Settle(LForm); + Require((LForm.Source.ReadCalls = LBefore+3) and not LForm.AccountGrid.LoadingVisible and + not TLoadingCircle(LForm.AccountGrid.FindComponent('LoadingCircle')).Active, + 'A continuously changing search retained unbounded work or a spinning loading card'); + Require(Pos('Please search again', + TLabel(LForm.AccountGrid.FindComponent('LoadingStatus')).Caption) > 0, + 'Search retry exhaustion did not expose the existing retryable grid error'); + LForm.Source.ChangeEveryRead := False; + LForm.AccountGrid.RefreshGrid; + Settle(LForm); + AssertOwner(LForm,LSecond.PublicKey,9); + ALog.Add('PASS bounded search retry, nonspinning error and successful retry'); + + LForm.Source.Hold; + LBefore := LForm.Reports; + LBeats := LForm.Heartbeats; + LStarted := GetTickCount64; + LForm.bbTypeStats.OnClick(LForm.bbTypeStats); + Require(GetTickCount64-LStarted < 200,'Type statistics blocked its UI click'); + Require(LForm.Source.Entered.WaitFor(1000) = wrSignaled,'Statistics did not enter its owned worker'); + Require(LForm.StatsRunning and LForm.AccountGrid.LoadingVisible, + 'Statistics must show the existing visual-grid loading presentation'); + Require(TPanel(LForm.AccountGrid.FindComponent('LoadingOverlay')).Visible and + TLoadingCircle(LForm.AccountGrid.FindComponent('LoadingCircle')).Active, + 'Statistics loading did not activate the actual shared loading controls'); + PumpFor(150); + Require((LForm.Heartbeats > LBeats+2) and (LForm.Reports = LBefore), + 'The UI did not remain responsive while statistics storage was held'); + LForm.Source.Release; + Settle(LForm); + Require((LForm.Reports = LBefore+1) and not LForm.AccountGrid.LoadingVisible, + 'Statistics completion did not clear the loading card and present once'); + LegacyReport(LSafeBox,LExpected); + Require(LForm.ReportText = LExpected.Text,'Global type statistics changed a legacy report line or aggregate'); + Require(LForm.LastStats.Chain.Accounts = 20,'Type statistics incorrectly used the selected-key subset'); + Require(LForm.Source.LargestRead <= 128,'Storage reads exceeded the cooperative chunk bound'); + ALog.Add('PASS background global statistics, native loading, responsive UI and exact legacy report'); + + LForm.Source.ChangeEveryRead := True; + LBefore := LForm.Source.ReadCalls; + LTask := TAccountSelectorStatsTask.Create(LForm.Source); + try + LTask.Start; + LStarted := GetTickCount64; + while not LTask.Finished and (GetTickCount64-LStarted < 2000) do PumpFor(10); + Require(LTask.Finished,'Continuous chain changes retained an unbounded statistics retry'); + Require((LForm.Source.ReadCalls = LBefore+3) and (Pos('Please try again',LTask.ErrorMessage) > 0), + 'Statistics snapshot retries must end after three attempts with a retryable error'); + finally + LTask.Free; + end; + LForm.Source.ChangeEveryRead := False; + ALog.Add('PASS continuous chain changes finish after bounded retry attempts'); + + LForm.Source.Hold; + LForm.bbTypeStats.OnClick(LForm.bbTypeStats); + Require(LForm.Source.Entered.WaitFor(1000) = wrSignaled,'Close fixture statistics never entered'); + LStarted := GetTickCount64; + FreeAndNil(LForm); + Require(GetTickCount64-LStarted < 1000,'Closing statistics waited for a UI synchronization or busy loop'); + PumpFor(100); + ALog.Add('PASS close cooperatively retires held statistics and queued receivers'); + LForm := TSelectorForm.CreateForSafeBox(LSafeBox); + LForm.Source.Hold; + LForm.WalletKeys := LKeys; + LForm.Show; + Require(LForm.Source.Entered.WaitFor(1000) = wrSignaled,'Close fixture page provider never entered'); + LStarted := GetTickCount64; + FreeAndNil(LForm); + Require(GetTickCount64-LStarted < 1000,'Closing the selector did not cancel its held grid provider'); + PumpFor(100); + Require(Length(LKeys.OnChanged.Handlers)+Length(LKeys.OnChanged.MainThreadHandlers) = LWalletHandlers, + 'Closing selector receivers changed the wallet notification contract'); + ALog.Add('PASS close cooperatively retires held grid query without wallet subscriptions'); + Require(not TNode.NodeExists,'Account selector created a hidden node/network dependency'); + finally + if Assigned(LForm) then LForm.Source.Release; + LForm.Free; + LExpected.Free; + LKeys.Free; + LSafeBox.Free; + LForeign.Free; + LEmpty.Free; + LSecond.Free; + LFirst.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/block-throughput/BlockThroughputProbe.dpr b/src/tests/experimental/native/block-throughput/BlockThroughputProbe.dpr new file mode 100644 index 000000000..6e61add6f --- /dev/null +++ b/src/tests/experimental/native/block-throughput/BlockThroughputProbe.dpr @@ -0,0 +1,13 @@ +program BlockThroughputProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses SysUtils, UBlockThroughputChecks; +begin + if ParamCount <> 1 then Halt(64); + try ExitCode := RunBlockThroughputChecks(GetEnvironmentVariable('PASCAL_BLOCK_BENCH_DATA'), + ExpandFileName(ParamStr(1))); + except on E: Exception do begin + WriteLn(E.ClassName, ': ', E.Message); + ExitCode := 1; + end; end; +end. \ No newline at end of file diff --git a/src/tests/experimental/native/block-throughput/BlockThroughputProbe.dproj b/src/tests/experimental/native/block-throughput/BlockThroughputProbe.dproj new file mode 100644 index 000000000..af347086e --- /dev/null +++ b/src/tests/experimental/native/block-throughput/BlockThroughputProbe.dproj @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{13656789-C199-485B-9BF3-A747646478D2}</ProjectGuid> + <MainSource>BlockThroughputProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\block-throughput\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\block-throughput\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UBlockThroughputChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">BlockThroughputProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/block-throughput/BlockThroughputProbe.lpr b/src/tests/experimental/native/block-throughput/BlockThroughputProbe.lpr new file mode 100644 index 000000000..e847924dc --- /dev/null +++ b/src/tests/experimental/native/block-throughput/BlockThroughputProbe.lpr @@ -0,0 +1,10 @@ +program BlockThroughputProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses SysUtils, UBlockThroughputChecks; +begin + if ParamCount <> 1 then Halt(64); + try ExitCode := RunBlockThroughputChecks(GetEnvironmentVariable('PASCAL_BLOCK_BENCH_DATA'), + ExpandFileName(ParamStr(1))); + except on E: Exception do begin WriteLn(E.ClassName, ': ', E.Message); ExitCode := 1; end; end; +end. \ No newline at end of file diff --git a/src/tests/experimental/native/block-throughput/PendingCatchupProbe.dpr b/src/tests/experimental/native/block-throughput/PendingCatchupProbe.dpr new file mode 100644 index 000000000..dcaaca670 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/PendingCatchupProbe.dpr @@ -0,0 +1,20 @@ +program PendingCatchupProbe; +{$APPTYPE CONSOLE} +uses SysUtils, UPendingCatchupChecks; +var LCase: String; LOrdinal: Integer; +begin + ReportMemoryLeaksOnShutdown:=True; + if ParamCount<>1 then Halt(64); + try + LCase:=GetEnvironmentVariable('PASCAL_PENDING_CATCHUP_CASE'); + if LCase='' then ExitCode:=RunPendingCatchupChecks(ParamStr(1)) + else begin + LOrdinal:=StrToInt(LCase); + if (LOrdinal<Ord(Low(TPendingCatchupCheck))) or (LOrdinal>Ord(High(TPendingCatchupCheck))) then Halt(64); + ExitCode:=RunPendingCatchupCheck(TPendingCatchupCheck(LOrdinal),ParamStr(1)); + end; + except on E:Exception do begin + WriteLn(E.ClassName,': ',E.Message); + ExitCode:=1; + end; end; +end. diff --git a/src/tests/experimental/native/block-throughput/PendingCatchupProbe.dproj b/src/tests/experimental/native/block-throughput/PendingCatchupProbe.dproj new file mode 100644 index 000000000..49a2c3cf5 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/PendingCatchupProbe.dproj @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{1B4BE304-C1A4-42EF-ACDA-B5FFD09CDB03}</ProjectGuid> + <MainSource>PendingCatchupProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\pending-catchup\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\pending-catchup\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UPendingCatchupChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">PendingCatchupProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/block-throughput/PendingCatchupProbe.lpr b/src/tests/experimental/native/block-throughput/PendingCatchupProbe.lpr new file mode 100644 index 000000000..ab25b22e8 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/PendingCatchupProbe.lpr @@ -0,0 +1,20 @@ +program PendingCatchupProbe; +{$mode delphi} +uses heaptrc, SysUtils, UPendingCatchupChecks; +var LCase: String; LOrdinal: Integer; +begin + if ParamCount<>1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + try + LCase:=GetEnvironmentVariable('PASCAL_PENDING_CATCHUP_CASE'); + if LCase='' then ExitCode:=RunPendingCatchupChecks(ParamStr(1)) + else begin + LOrdinal:=StrToInt(LCase); + if (LOrdinal<Ord(Low(TPendingCatchupCheck))) or (LOrdinal>Ord(High(TPendingCatchupCheck))) then Halt(64); + ExitCode:=RunPendingCatchupCheck(TPendingCatchupCheck(LOrdinal),ParamStr(1)); + end; + except on E:Exception do begin + WriteLn(E.ClassName,': ',E.Message); + ExitCode:=1; + end; end; +end. diff --git a/src/tests/experimental/native/block-throughput/PendingSaveProbe.dpr b/src/tests/experimental/native/block-throughput/PendingSaveProbe.dpr new file mode 100644 index 000000000..ac3ddc03e --- /dev/null +++ b/src/tests/experimental/native/block-throughput/PendingSaveProbe.dpr @@ -0,0 +1,11 @@ +program PendingSaveProbe; +{$APPTYPE CONSOLE} +uses SysUtils, UPendingSaveChecks; +begin + if ParamCount<>1 then Halt(64); + try ExitCode:=RunPendingSaveChecks(ExpandFileName(ParamStr(1))); + except on E:Exception do begin + WriteLn(E.ClassName,': ',E.Message); + ExitCode:=1; + end; end; +end. diff --git a/src/tests/experimental/native/block-throughput/PendingSaveProbe.dproj b/src/tests/experimental/native/block-throughput/PendingSaveProbe.dproj new file mode 100644 index 000000000..b3acaf01e --- /dev/null +++ b/src/tests/experimental/native/block-throughput/PendingSaveProbe.dproj @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{7E95622F-A68C-4620-AE68-1FD58150D1ED}</ProjectGuid> + <MainSource>PendingSaveProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\pending-save\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\pending-save\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UPendingSaveChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">PendingSaveProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/block-throughput/PendingSaveProbe.lpr b/src/tests/experimental/native/block-throughput/PendingSaveProbe.lpr new file mode 100644 index 000000000..387786da6 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/PendingSaveProbe.lpr @@ -0,0 +1,10 @@ +program PendingSaveProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, UPendingSaveChecks; +begin + if ParamCount<>1 then Halt(64); + {$IFDEF FPC}SetHeapTraceOutput(ParamStr(1)+'.heap.log');{$ENDIF} + try ExitCode:=RunPendingSaveChecks(ExpandFileName(ParamStr(1))); + except on E:Exception do begin WriteLn(E.ClassName,': ',E.Message); ExitCode:=1; end; end; +end. diff --git a/src/tests/experimental/native/block-throughput/README.md b/src/tests/experimental/native/block-throughput/README.md new file mode 100644 index 000000000..bb86584e2 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/README.md @@ -0,0 +1,143 @@ +# Block application and pending replay + +These Windows console probes test the core's block application, pending replay +and pending saves. They never connect to peers or use private wallet keys. Put +OpenSSL 1.1 beside each executable and pass it the path of a new log file. Each +exits with 0 on success. All but the throughput probe create data folders under +`TEMP`, and their FPC builds write `<logfile>.heap.log`. + +## Pending catch-up + +`PendingCatchupProbe.dproj` (Delphi) and `PendingCatchupProbe.lpr` (FPC) test +the node's deferred pending maintenance in ten generated-data cases. Set +`PASCAL_PENDING_CATCHUP_CASE` to a case number to run only that case: + +0. Buffered commits +1. Remote target across batch gaps +2. Confirmed and pending conflicts +3. Unavailable reads and admission +4. New work during final replay +5. Retired and reconnected owners +6. Pending-file publication on Quit +7. Empty scopes +8. Automatic retry after a failed replay +9. Quit after a failed replay + +Both projects and the FPCUnit wrapper share +[`UPendingCatchupChecks.pas`](UPendingCatchupChecks.pas). Point `TEMP` and `TMP` +at a test folder: + +```powershell +$env:TEMP = '<new-folder>' +$env:TMP = $env:TEMP +New-Item -ItemType Directory -Path $env:TEMP -Force +.\PendingCatchupProbe.exe <new-folder>\checks.log +``` + +## Operation sanitizer + +`SanitizeOperationsProbe.dproj` (Delphi) and `SanitizeOperationsProbe.lpr` (FPC) +run nine check groups with generated accounts and signed operations: + +- All-valid tree retention +- Invalid nonces and duplicates +- Delete-created index gaps +- Zero-fee signer limits +- Input preservation after replay exceptions +- Shared operation-registry ownership +- Rejected recovery metadata and empty pools +- Reference-lookup boundaries and first duplicates +- Signature caches across protocols + +[`UBlockSanitizeTests.pas`](../../core/UBlockSanitizeTests.pas) registers each +group as an FPCUnit test. Build the probe with range and overflow checks. + +```powershell +.\SanitizeOperationsProbe.exe <new-folder>\checks.log +``` + +## Pending saves + +`PendingSaveProbe.dproj` and `PendingSaveProbe.lpr` run five saver and +persistence checks. +[`UPendingSaveLifecycleTests.pas`](../../startup/UPendingSaveLifecycleTests.pas) +registers them with FPCUnit. The checks cover dirty shutdown, a save failure +racing termination, sanitizer removals, publication failures and a failed final +publication. + +Each check writes `<logfile>.<number>.log`. Allow up to two minutes. + +## Block throughput + +`BlockThroughputProbe.lpr` and [`BlockThroughputProbe.dproj`](BlockThroughputProbe.dproj) +time pending replay on a prepared public-chain clone. Build them with consistent +optimization and without heap tracing. + +Set `PASCAL_BLOCK_BENCH_DATA` to the clone's root, never a working wallet +directory. The root needs a `.block-throughput-owned` marker. Its `Data` folder +needs `BlockChainStream.blocks`, `safebox.am_safebox` and a `pendingbuffer.ops` +with 185,831 pending operations. + +```powershell +$env:PASCAL_BLOCK_BENCH_DATA = '<chain-copy-folder>' +.\BlockThroughputProbe.exe <chain-copy-folder>\throughput.log +``` + +By default the probe times three sanitizations and checks that the output is +unchanged. It reports write-lock admission, body wall time and process CPU time. +Optional variables: + +- `PASCAL_BLOCK_BENCH_ASSERT_RETENTION=1` fails the run if the all-valid fast + path replaces the original tree. +- `PASCAL_BLOCK_BENCH_PHASES=1` runs one sanitization, then replay, tree-rebuild, + copy, serialization and pending-save experiments whose times are not additive. +- `PASCAL_BLOCK_BENCH_ACCOUNT_CACHE` sets the account cache to `50000` or + `250000` records. + +### Mixed-rejection mode + +`PASCAL_BLOCK_BENCH_CONFIRM_EVERY=1000` commits every 1,000th pending operation +in a simulated block, then times three sanitizations. This changes the clone's +safebox. + +### Real-block mode + +Set `PASCAL_BLOCK_BENCH_REAL_BLOCKS` to a folder holding public-chain blocks +`block-841529.bin` through `block-841533.bin` in network format. The probe +applies them to the 841,529-block clone, timing each block call and the final +pending reconciliation. Restore the changed clone after the process exits. + +`PASCAL_BLOCK_BENCH_ANNOUNCED_LAST=1` applies the fifth block as a node +announcement. + +## Results + +Results are core runs without a UI or active network, on the frozen +841,529-block dataset with a 21,183,301-byte pending file. Timings exclude the +probe's checks. + +### Sanitization + +Three FPC sanitizations took 7,407 / 7,375 / 7,422 ms. Before the +unchanged-operation index fast path, they took 10,281 / 10,000 / 10,063 ms, and +phase experiments measured 6,093 ms of replay and 3,078 ms of tree rebuilding. + +### Before deferred catch-up replay + +With the 250,000-record account cache, the five block calls took +4,000 / 3,578 / 3,515 / 3,453 / 3,469 ms, plus a 266 ms final safebox flush. +The mixed-rejection mode removed 186 confirmed operations and took +3,187 / 3,203 / 3,234 ms. These are FPC runs. + +### With deferred catch-up replay + +Both runs, built with range and overflow checks, kept all 185,831 pending +operations: + +| Compiler and route | Block calls, ms | Final pending reconciliation | +|---|---|---| +| FPC, five bulk calls | 578 / 250 / 265 / 250 / 219 | 4,016 ms | +| Delphi Win64, four bulk calls and one node announcement | 375 / 63 / 31 / 62 / 500 | 3,047 ms | + +Each row is one run, not a compiler-speed comparison. The FPC run predates the +proof-of-work empty-payload write guards. diff --git a/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.dpr b/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.dpr new file mode 100644 index 000000000..e40500745 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.dpr @@ -0,0 +1,11 @@ +program SanitizeOperationsProbe; +{$APPTYPE CONSOLE} +uses SysUtils, UMempoolSanitizeChecks; +begin + if ParamCount <> 1 then Halt(64); + try ExitCode := RunMempoolSanitizeChecks(ExpandFileName(ParamStr(1))); + except on E: Exception do begin + WriteLn(E.ClassName, ': ', E.Message); + ExitCode := 1; + end; end; +end. diff --git a/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.dproj b/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.dproj new file mode 100644 index 000000000..32a3c4146 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.dproj @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{94EA05C4-E619-489B-9571-A2F8BD83DDD1}</ProjectGuid> + <MainSource>SanitizeOperationsProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\mempool-sanitize\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\mempool-sanitize\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UMempoolSanitizeChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">SanitizeOperationsProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.lpr b/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.lpr new file mode 100644 index 000000000..897cf08c6 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/SanitizeOperationsProbe.lpr @@ -0,0 +1,10 @@ +program SanitizeOperationsProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, UMempoolSanitizeChecks; +begin + if ParamCount <> 1 then Halt(64); + {$IFDEF FPC}SetHeapTraceOutput(ParamStr(1) + '.heap.log');{$ENDIF} + try ExitCode := RunMempoolSanitizeChecks(ExpandFileName(ParamStr(1))); + except on E: Exception do begin WriteLn(E.ClassName, ': ', E.Message); ExitCode := 1; end; end; +end. diff --git a/src/tests/experimental/native/block-throughput/UAccountCachePolicyChecks.pas b/src/tests/experimental/native/block-throughput/UAccountCachePolicyChecks.pas new file mode 100644 index 000000000..603bef5d4 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/UAccountCachePolicyChecks.pas @@ -0,0 +1,273 @@ +unit UAccountCachePolicyChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Existing cache configuration must survive storage replacement. Fixtures use + generated metadata and new TEMP files; they do not test block consensus. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$I ..\..\..\..\config.inc} + +interface + +procedure CheckDefaultAccountCacheLimit; +procedure CheckAccountCacheLimitSurvivesMemoryReload; +procedure CheckAccountCacheLimitSurvivesFileReloadAndClear; +procedure CheckFailedSafeboxReplacementPreservesFinallyException; +procedure CheckFailedSafeboxReplacementPreservesDisposalException; +procedure CheckFailedSafeboxConstructionPreservesException; +procedure CheckNormalSafeboxClearRetainsItsExistingSemantics; + +implementation + +uses Classes, SysUtils, UAccounts, UPCDataTypes, UCrypto, UConst, UMemory; + +type + TCachePolicySafeBox = class(TPCSafeBox) + procedure Build(const AKey: TAccountKey; AType: Word); + end; + + TFailedReplacementSafeBox = class(TPCSafeBox) + constructor Create(const ALockedFile: String); reintroduce; + end; + + TFailureOwnerMode = (fomFinally, fomDisposables, fomConstructor); + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('Account cache policy: '+AMessage); +end; + +procedure TCachePolicySafeBox.Build(const AKey: TAccountKey; AType: Word); +var B: TOperationBlock; A: TAccount; +begin + B := CT_OperationBlock_NUL; + B.account_key := AKey; + B.protocol_version := CT_PROTOCOL_1; + B.protocol_available := CT_PROTOCOL_6; + B.compact_target := CT_MinCompactTarget_v1; + B.reward := TPascalCoinProtocol.GetRewardForNewLine(0); + AddNew(B); + A := Account(0); + A.account_type := AType; + UpdateAccount(A.account,A.accountInfo,A.name,A.account_type,A.balance, + A.n_operation,A.account_data,A.account_seal,aus_transaction_commit, + A.updated_on_block_passive_mode,A.updated_on_block_active_mode,False,False); +end; + +constructor TFailedReplacementSafeBox.Create(const ALockedFile: String); +begin + inherited Create; + // A failed descendant constructor invokes the same inherited destructor as + // a caller disposing an already-created safebox after a failed replacement. + SetSafeboxFileName(ALockedFile); +end; + +procedure FailReplacementWithFinally(const ALockedFile: String); +var S: TPCSafeBox; +begin + S := TPCSafeBox.Create; + try + S.SetSafeboxFileName(ALockedFile); + finally + S.Free; + end; +end; + +procedure FailReplacementWithDisposables(const ALockedFile: String); +var D: TDisposables; + S: TPCSafeBox; +begin + S := D.AddObject(TPCSafeBox.Create) as TPCSafeBox; + S.SetSafeboxFileName(ALockedFile); +end; + +procedure FailReplacementDuringConstruction(const ALockedFile: String); +var S: TPCSafeBox; +begin + S := TFailedReplacementSafeBox.Create(ALockedFile); + S.Free; +end; + +procedure CheckFailedReplacement(AOwnerMode: TFailureOwnerMode); +const CMarker: array[0..7] of Byte = ($51,$41,$2D,$4F,$57,$4E,$45,$44); +var ID: TGUID; + Directory, FileName, ExpectedClass, ExpectedMessage, ActualClass, ActualMessage: String; + Blocker, Unexpected: TFileStream; + LReadBack: array[0..7] of Byte; +begin + TCrypto.InitCrypto; + Require(CreateGUID(ID)=0,'cannot create replacement-failure identity'); + Directory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP'))+'SafeboxCleanup-'+GUIDToString(ID); + Require(not DirectoryExists(Directory),'refusing an existing cleanup fixture'); + Require(ForceDirectories(Directory),'cannot create cleanup fixture'); + FileName := IncludeTrailingPathDelimiter(Directory)+'locked.safebox'; + Blocker := TFileStream.Create(FileName,fmCreate or fmShareExclusive); + try + Blocker.WriteBuffer(CMarker,SizeOf(CMarker)); + ExpectedClass := ''; + ExpectedMessage := ''; + try + // Match the production file-open mode and retain its actual localized OS + // error. The fixture never exhausts the disk or touches a user data file. + Unexpected := TFileStream.Create(FileName,fmOpenReadWrite+fmShareDenyWrite); + Unexpected.Free; + except + on E: EFOpenError do begin + ExpectedClass := E.ClassName; + ExpectedMessage := E.Message; + end; + end; + Require(ExpectedClass<>'','the exclusive fixture did not reject another writer'); + ActualClass := ''; + ActualMessage := ''; + try + case AOwnerMode of + fomFinally: FailReplacementWithFinally(FileName); + fomDisposables: FailReplacementWithDisposables(FileName); + fomConstructor: FailReplacementDuringConstruction(FileName); + end; + except + on E: Exception do begin + ActualClass := E.ClassName; + ActualMessage := E.Message; + end; + end; + Require(ActualClass=ExpectedClass,'cleanup replaced '+ExpectedClass+' with '+ActualClass+': '+ActualMessage); + Require(ActualMessage=ExpectedMessage,'cleanup replaced the original file-open diagnostic'); + Require(Blocker.Size=SizeOf(CMarker),'failed replacement resized the locked file'); + Blocker.Position := 0; + Blocker.ReadBuffer(LReadBack,SizeOf(LReadBack)); + Require(CompareMem(@LReadBack[0],@CMarker[0],SizeOf(CMarker)),'failed replacement changed locked-file bytes'); + finally + Blocker.Free; + end; +end; + +procedure CheckFailedSafeboxReplacementPreservesFinallyException; +begin + CheckFailedReplacement(fomFinally); +end; + +procedure CheckFailedSafeboxReplacementPreservesDisposalException; +begin + CheckFailedReplacement(fomDisposables); +end; + +procedure CheckFailedSafeboxConstructionPreservesException; +begin + CheckFailedReplacement(fomConstructor); +end; + +procedure CheckNormalSafeboxClearRetainsItsExistingSemantics; +var S: TCachePolicySafeBox; + Key: TECPrivateKey; +begin + TCrypto.InitCrypto; + Key := TECPrivateKey.Create; + try + Key.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + S := TCachePolicySafeBox.Create; + try + S.PCAbstractMem.AccountCache.MaxRegisters := 250000; + S.Build(Key.PublicKey,321); + Require(S.GetMinimumAvailableSnapshotBlock>=0,'normal fixture did not create owned snapshot records'); + S.Clear; + Require(S.GetMinimumAvailableSnapshotBlock=-1,'Clear retained owned snapshot records'); + // Existing AbstractMem Clear resets metadata/snapshots; it does not erase + // the current backend's records. Preserve that public behavior exactly. + Require((S.BlocksCount=1) and (S.Account(0).account_type=321),'Clear changed the existing AbstractMem record semantics'); + Require((S.TotalBalance=0) and (S.WorkSum=0) and (S.CurrentProtocol=CT_PROTOCOL_1),'Clear no longer resets public summary metadata'); + Require(S.PCAbstractMem.AccountCache.MaxRegisters=250000,'Clear changed the configured cache policy'); + S.Clear; + Require(S.GetMinimumAvailableSnapshotBlock=-1,'repeated Clear recreated snapshots'); + finally + S.Free; + end; + finally + Key.Free; + end; +end; + +procedure CheckDefaultAccountCacheLimit; +var S: TPCSafeBox; +begin + TCrypto.InitCrypto; + S := TPCSafeBox.Create; + try + Require(S.PCAbstractMem.AccountCache.MaxRegisters=50000, + 'core/classic default changed'); + finally S.Free; end; +end; + +procedure CheckAccountCacheLimitSurvivesMemoryReload; +var S: TPCSafeBox; +begin + TCrypto.InitCrypto; + S := TPCSafeBox.Create; + try + S.PCAbstractMem.AccountCache.MaxRegisters := 250000; + S.SetSafeboxFileName(''); + Require(S.PCAbstractMem.AccountCache.MaxRegisters=250000, + 'memory replacement lost the configured limit'); + S.Clear; + Require(S.PCAbstractMem.AccountCache.MaxRegisters=250000, + 'safebox clear lost the configured limit'); + S.PCAbstractMem.AccountCache.MaxRegisters := 0; + S.SetSafeboxFileName(''); + Require(S.PCAbstractMem.AccountCache.MaxRegisters=0, + 'the existing zero-limit configuration was replaced by a default'); + finally S.Free; end; +end; + +procedure CheckAccountCacheLimitSurvivesFileReloadAndClear; +var + D: TDisposables; + Key: TECPrivateKey; + Source,Target: TCachePolicySafeBox; + ID: TGUID; + Directory,FirstFile,SecondFile: String; + + procedure SaveFixture(const AFile: String; AType: Word); + begin + Require(not FileExists(AFile),'refusing an existing fixture file'); + Source := TCachePolicySafeBox.Create; + try + Source.Build(Key.PublicKey,AType); + Source.SaveCheckpointing(AFile); + finally Source.Free; end; + end; + +begin + TCrypto.InitCrypto; + CreateGUID(ID); + Directory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP'))+ + 'AccountCachePolicy-'+GUIDToString(ID); + Require(not DirectoryExists(Directory),'refusing an existing fixture directory'); + Require(ForceDirectories(Directory),'cannot create fixture directory'); + FirstFile := IncludeTrailingPathDelimiter(Directory)+'first.safebox'; + SecondFile := IncludeTrailingPathDelimiter(Directory)+'second.safebox'; + Key := D.AddObject(TECPrivateKey.Create) as TECPrivateKey; + Key.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + SaveFixture(FirstFile,111); + SaveFixture(SecondFile,222); + Target := D.AddObject(TCachePolicySafeBox.Create) as TCachePolicySafeBox; + Target.PCAbstractMem.AccountCache.MaxRegisters := 250000; + Target.SetSafeboxFileName(FirstFile); + Require((Target.BlocksCount=1) and (Target.Account(0).account_type=111), + 'first saved fixture did not reopen'); + Require(Target.PCAbstractMem.AccountCache.MaxRegisters=250000, + 'file replacement lost the configured limit'); + Target.SetSafeboxFileName(SecondFile); + Require((Target.BlocksCount=1) and (Target.Account(0).account_type=222), + 'old cache entries leaked into a different storage file'); + Require(Target.PCAbstractMem.AccountCache.MaxRegisters=250000, + 'second file replacement lost the configured limit'); + // The core clear API deletes/recreates only this newly generated fixture. + Target.ClearSafeboxfile; + Require((Target.BlocksCount=0) and + (Target.PCAbstractMem.AccountCache.MaxRegisters=250000), + 'file clear failed to retain the bounded cache policy'); + Require(FileExists(FirstFile),'clearing the second fixture touched the first'); +end; + +end. diff --git a/src/tests/experimental/native/block-throughput/UBlockThroughputChecks.pas b/src/tests/experimental/native/block-throughput/UBlockThroughputChecks.pas new file mode 100644 index 000000000..21ec53e22 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/UBlockThroughputChecks.pas @@ -0,0 +1,721 @@ +unit UBlockThroughputChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Measures actual core work in an explicitly prepared public-data clone. + No wallet controls, replacement operation methods or external peers. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunBlockThroughputChecks(const ADataDirectory, ALogPath: String): Integer; + +implementation + +uses {$IFDEF FPC}Windows,{$ELSE}Winapi.Windows,{$ENDIF} + Classes, SysUtils, UNode, UBlockChain, UAccounts, UFileStorage, UThread, + UBaseTypes, UCrypto, UNetProtocol, UConst, UPCDataTypes, SyncObjs, + UOpTransaction, UOrderedList; + +type + TBlockThroughputChecks = class + private + FLog: TStringList; + FLogPath: String; + FLastProgress: TTickCount; + FRealCompletion: TEvent; + procedure Log(const AText: String); + procedure Require(AValue: Boolean; const AMessage: String); + procedure Progress(Sender: TObject; const AProgress: TProgressInfo); + procedure ActualSynchronizationChanged(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); + procedure MeasurePhases(ANode: TNode); + procedure MeasureMixedRejections(ANode: TNode; AConfirmEvery: Integer); + procedure MeasureRealBlocks(ANode: TNode; const AFixtureDirectory: String); + procedure VerifyStoredRealBlocks(const ADataDirectory,AFixtureDirectory: String); + public + constructor Create(const ALogPath: String); + destructor Destroy; override; + procedure Run(const ADataDirectory: String); + end; + + TDenseObservedChangeInfo = class(TOpChangeAccountInfo) + class function CopySignedOperation(AOriginal: TOpChangeAccountInfo): TDenseObservedChangeInfo; + function DoOperation(APrevious: TAccountPreviousBlockInfo; + ATransaction: TPCSafeBoxTransaction; var AErrors: String): Boolean; override; + end; + +var GDenseObservedTransaction: TPCSafeBoxTransaction; + GDenseObservedAttempts: LongInt; + +class function TDenseObservedChangeInfo.CopySignedOperation( + AOriginal: TOpChangeAccountInfo): TDenseObservedChangeInfo; +var LStream: TMemoryStream; +begin + Result := TDenseObservedChangeInfo.Create(AOriginal.ProtocolVersion); + try + LStream := TMemoryStream.Create; + try + if not AOriginal.SaveToNettransfer(LStream) then + raise Exception.Create('Cannot serialize the existing signed observation operation'); + LStream.Position := 0; + if not Result.LoadFromNettransfer(LStream) then + raise Exception.Create('Cannot decode the existing signed observation operation'); + finally LStream.Free; end; + // Preserve the existing validated signature/cache; only inherited validator + // entry is observed. The public wire/storage bytes remain unchanged. + Result.FHasValidSignature := AOriginal.HasValidSignature; + Result.FUsedPubkeyForSignature := TDenseObservedChangeInfo(AOriginal).FUsedPubkeyForSignature; + Result.FBufferedSha256 := AOriginal.Sha256; + Result.FBufferedRipeMD160 := AOriginal.RipeMD160; + Result.DiscoveredOnBlock := AOriginal.DiscoveredOnBlock; + Result.ResendOnBlock := AOriginal.ResendOnBlock; + Result.ResendCount := AOriginal.ResendCount; + except Result.Free; raise; end; +end; + +function TDenseObservedChangeInfo.DoOperation(APrevious: TAccountPreviousBlockInfo; + ATransaction: TPCSafeBoxTransaction; var AErrors: String): Boolean; +begin + if ATransaction=GDenseObservedTransaction then + InterlockedIncrement(GDenseObservedAttempts); + Result := inherited DoOperation(APrevious,ATransaction,AErrors); +end; + +constructor TBlockThroughputChecks.Create(const ALogPath: String); +begin + inherited Create; + FLog := TStringList.Create; + FLogPath := ALogPath; +end; + +destructor TBlockThroughputChecks.Destroy; +begin + FLog.Free; + inherited; +end; + +procedure TBlockThroughputChecks.Log(const AText: String); +begin + FLog.Add(FormatDateTime('hh:nn:ss.zzz', Now) + ' ' + AText); + FLog.SaveToFile(FLogPath); +end; + +procedure TBlockThroughputChecks.Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure TBlockThroughputChecks.Progress(Sender: TObject; const AProgress: TProgressInfo); +begin + if TPlatform.GetElapsedMilliseconds(FLastProgress) < 1000 then Exit; + FLastProgress := TPlatform.GetTickCount; + Log(Format('RESTORE stage=%d current=%d total=%d', + [Ord(AProgress.Stage), AProgress.CurrentPosition, AProgress.TotalCount])); +end; + +procedure TBlockThroughputChecks.ActualSynchronizationChanged(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); +begin + if Assigned(FRealCompletion) and AStatus.PendingOperationsAvailable then + FRealCompletion.SetEvent; +end; + +procedure TBlockThroughputChecks.MeasurePhases(ANode: TNode); +var LMempool: TPCOperationsComp; LTree: TOperationsHashTree; + LReplay: TPCSafeBoxTransaction; LPrevious: TAccountPreviousBlockInfo; + LStream: TMemoryStream; LStarted: TTickCount; + I: Integer; LError: String; +begin + // These are independent real-core experiments, not replacement sanitizer + // logic. Exclusive ownership also excludes the background pending-file save. + LMempool := ANode.LockMempoolWrite; + try + LReplay := TPCSafeBoxTransaction.Create(ANode.Bank.SafeBox); + LPrevious := TAccountPreviousBlockInfo.Create; + try + LStarted := TPlatform.GetTickCount; + for I := 0 to LMempool.Count - 1 do + Require(LMempool.OperationsHashTree.GetOperation(I).DoOperation(LPrevious, LReplay, LError), + 'Real operation replay failed: ' + LError); + Log('PHASE actual_DoOperation_replay_ms=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + Require((LReplay.TotalBalance = LMempool.SafeBoxTransaction.TotalBalance) and + (LReplay.TotalFee = LMempool.SafeBoxTransaction.TotalFee), 'Replay aggregate mismatch'); + finally + LStarted := TPlatform.GetTickCount; + LPrevious.Free; + LReplay.Free; + Log('PHASE replay_cleanup_ms=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + end; + + LTree := TOperationsHashTree.Create; + try + LTree.Max0feeOperationsBySigner := LMempool.OperationsHashTree.Max0feeOperationsBySigner; + LStarted := TPlatform.GetTickCount; + for I := 0 to LMempool.Count - 1 do begin + Require(LTree.CanAddOperationToHashTree(LMempool.Operation[I]), 'Eligibility mismatch'); + Require(LTree.AddOperationToHashTree(LMempool.Operation[I]), 'Index rebuild rejected an operation'); + end; + Log('PHASE eligibility_and_actual_tree_rebuild_ms=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + LStarted := TPlatform.GetTickCount; + Require(TBaseType.Equals(LTree.HashTree, LMempool.OperationsHashTree.HashTree), 'Rebuilt tree hash/order mismatch'); + Log('PHASE rebuilt_hash_ms=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + finally + LStarted := TPlatform.GetTickCount; + LTree.Free; + Log('PHASE rebuilt_tree_cleanup_ms=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + end; + + LTree := TOperationsHashTree.Create; + try + LStarted := TPlatform.GetTickCount; + LTree.CopyFromHashTree(LMempool.OperationsHashTree); + Log('PHASE existing_CopyFromHashTree_ms=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + Require((LTree.OperationsCount = LMempool.Count) and + TBaseType.Equals(LTree.HashTree, LMempool.OperationsHashTree.HashTree), 'Existing clone mismatch'); + finally LTree.Free; end; + + LStream := TMemoryStream.Create; + try + LStarted := TPlatform.GetTickCount; + Require(LMempool.OperationsHashTree.SaveOperationsHashTreeToStream(LStream, True), 'Serialization failed'); + Log(Format('PHASE memory_serialization_ms=%d bytes=%d', + [TPlatform.GetElapsedMilliseconds(LStarted), LStream.Size])); + finally LStream.Free; end; + LStarted := TPlatform.GetTickCount; + ANode.Bank.Storage.SavePendingBufferOperations(LMempool.OperationsHashTree); + Log('PHASE actual_buffered_pending_save_ms=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + finally ANode.UnlockMempoolWrite; end; +end; + +procedure TBlockThroughputChecks.MeasureMixedRejections(ANode: TNode; AConfirmEvery: Integer); +var LPool: TPCOperationsComp; LOriginal,LSelected,LExpected: TOperationsHashTree; + LTransaction: TPCSafeBoxTransaction; LPrevious: TAccountPreviousBlockInfo; + LHeader: TOperationBlock; LError: String; LStream: TMemoryStream; + LStarted: TTickCount; I,J,LSample,LInputCount,LConfirmed,LOutputCount,LPosition: Integer; + LOutputHash,LBytesHash,LCurrentBytesHash: TRawBytes; + LOutputBalance,LRegistryCount: Int64; + LCreate,LExit,LKernel,LUser: TFileTime; + LCPUBefore,LCPUAfter: UInt64; + + function CPUTime: UInt64; + begin + if not GetProcessTimes(GetCurrentProcess,LCreate,LExit,LKernel,LUser) then RaiseLastOSError; + Result := ((UInt64(LKernel.dwHighDateTime) shl 32) or LKernel.dwLowDateTime) + + ((UInt64(LUser.dwHighDateTime) shl 32) or LUser.dwLowDateTime); + end; + +begin + // Keep background persistence outside the experiment and retain all input + // registrations between samples. Only SanitizeOperations is timed below. + LPool := ANode.LockMempoolWrite; + try + LInputCount := LPool.Count; + Require(LInputCount=185831,'Mixed fixture requires the complete real pending pool'); + LOriginal := TOperationsHashTree.Create; + try + LOriginal.CopyFromHashTree(LPool.OperationsHashTree); + LRegistryCount := TPCOperationsStorage.PCOperationsStorage.Count; + LTransaction := TPCSafeBoxTransaction.Create(ANode.Bank.SafeBox); + LPrevious := TAccountPreviousBlockInfo.Create; + LSelected := TOperationsHashTree.Create; + try + LConfirmed := 0; + for I := 0 to LInputCount-1 do + if (I mod AConfirmEvery)=0 then begin + Require(LOriginal.GetOperation(I).DoOperation(LPrevious,LTransaction,LError), + 'Simulated confirmation uses real valid pending operations: '+LError); + Require(LSelected.AddOperationToHashTree(LOriginal.GetOperation(I)), + 'Selected confirmation operation registration'); + Inc(LConfirmed); + end; + Require(LConfirmed>0,'Mixed fixture must confirm a nonempty real subset'); + LHeader := LPool.OperationBlock; + LHeader.block := ANode.Bank.BlocksCount; + LHeader.account_key := ANode.Bank.SafeBox.Account(0).accountInfo.accountKey; + LHeader.timestamp := ANode.Bank.LastOperationBlock.timestamp+1; + LHeader.nonce := 0; + LHeader.operations_hash := LSelected.HashTree; + LHeader.fee := LTransaction.TotalFee; + SetLength(LHeader.proof_of_work,32); + FillChar(LHeader.proof_of_work[0],32,0); + Require(LTransaction.Commit(LHeader,LError),'Actual selected-operation commit failed: '+LError); + Log(Format('SIMULATED_CONFIRMATION operations=%d every=%d new_blocks=%d no_PoW_validation=TRUE', + [LConfirmed,AConfirmEvery,ANode.Bank.BlocksCount])); + finally LSelected.Free; LPrevious.Free; LTransaction.Free; end; + LOutputCount := -1; + LOutputBalance := 0; + for LSample := 1 to 3 do begin + // This restores the exact original real objects/order; normal public + // tree copying is preparation, not the optimized timed implementation. + if LSample>1 then LPool.OperationsHashTree.CopyFromHashTree(LOriginal); + Require(LPool.Count=LInputCount,'Every mixed sample restores the same complete input'); + LCPUBefore := CPUTime; + LStarted := TPlatform.GetTickCount; + LPool.SanitizeOperations; + LCPUAfter := CPUTime; + Log(Format('MIXED_SANITIZE sample=%d body_ms=%d cpu_ms=%d input=%d output=%d', + [LSample,TPlatform.GetElapsedMilliseconds(LStarted),(LCPUAfter-LCPUBefore) div 10000,LInputCount,LPool.Count])); + Require((LPool.Count>0) and (LPool.Count<LInputCount), + 'Mixed sample must actually take a nonempty rejected-operation path'); + Require(LInputCount-LPool.Count>=LConfirmed,'Every confirmed old operation must be rejected'); + LExpected := TOperationsHashTree.Create; + LStream := TMemoryStream.Create; + try + LExpected.Max0feeOperationsBySigner := LPool.OperationsHashTree.Max0feeOperationsBySigner; + LPosition := 0; + for I := 0 to LPool.Count-1 do begin + while (LPosition<LInputCount) and + (LOriginal.GetOperation(LPosition)<>LPool.Operation[I]) do Inc(LPosition); + Require(LPosition<LInputCount,'Rejected-path output must preserve original operation identity/order'); + Inc(LPosition); + Require(LExpected.AddOperationToHashTree(LPool.Operation[I]), + 'Independent normal insertion must admit the accepted output'); + end; + Require(TBaseType.Equals(LExpected.HashTree,LPool.OperationsHashTree.HashTree), + 'Rejected-path ordered hash differs from normal insertion'); + Require((LExpected.TotalAmount=LPool.OperationsHashTree.TotalAmount) and + (LExpected.TotalFee=LPool.OperationsHashTree.TotalFee) and + (LExpected.TotalFee=LPool.OperationBlock.fee) and + (LExpected.TotalFee=LPool.SafeBoxTransaction.TotalFee),'Rejected-path aggregate mismatch'); + Require(LExpected.HasOpRecoverOperations=LPool.OperationsHashTree.HasOpRecoverOperations, + 'Rejected-path recover metadata mismatch'); + for J := 0 to LPool.Count-1 do begin + Require(LExpected.IndexOfOperation(LPool.Operation[J])= + LPool.OperationsHashTree.IndexOfOperation(LPool.Operation[J]),'Rejected-path SHA representative'); + Require(LExpected.IndexOfOpReference(LPool.Operation[J].GetOpReference)= + LPool.OperationsHashTree.IndexOfOpReference(LPool.Operation[J].GetOpReference), + 'Rejected-path operation-reference representative'); + Require(LExpected.CountOperationsBySameSignerWithoutFee(LPool.Operation[J].SignerAccount)= + LPool.OperationsHashTree.CountOperationsBySameSignerWithoutFee(LPool.Operation[J].SignerAccount), + 'Rejected-path zero-fee signer metadata'); + end; + Require(LPool.OperationsHashTree.SaveOperationsHashTreeToStream(LStream,True),'Mixed output serialization'); + LCurrentBytesHash := TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size); + if LSample=1 then begin + LOutputCount := LPool.Count; + LOutputHash := Copy(LPool.OperationsHashTree.HashTree); + LBytesHash := Copy(LCurrentBytesHash); + LOutputBalance := LPool.SafeBoxTransaction.TotalBalance; + end else begin + Require(LPool.Count=LOutputCount,'Mixed sample accepted count must repeat exactly'); + Require(TBaseType.Equals(LOutputHash,LPool.OperationsHashTree.HashTree),'Mixed sample ordered hash'); + Require(TBaseType.Equals(LBytesHash,LCurrentBytesHash),'Mixed sample exact serialized output'); + Require(LPool.SafeBoxTransaction.TotalBalance=LOutputBalance,'Mixed replay balance repeats exactly'); + end; + Log(Format('MIXED_OUTPUT sample=%d count=%d ordered_sha=%s bytes=%d bytes_sha=%s balance=%d fee=%d', + [LSample,LPool.Count,TCrypto.ToHexaString(LPool.OperationsHashTree.HashTree),LStream.Size, + TCrypto.ToHexaString(LCurrentBytesHash),LPool.SafeBoxTransaction.TotalBalance,LPool.SafeBoxTransaction.TotalFee])); + finally LStream.Free; LExpected.Free; end; + Require(TPCOperationsStorage.PCOperationsStorage.Count=LRegistryCount, + 'Repeated rejection replay must not grow the operation registry'); + end; + Log('PASS mixed rejection: actual DoOperation/Commit with simulated confirmation; normal-insertion indexes, order/hash/bytes/fee/signer/registry exact; no PoW or network throughput claim'); + finally LOriginal.Free; end; + finally ANode.UnlockMempoolWrite; end; +end; + +procedure TBlockThroughputChecks.MeasureRealBlocks(ANode: TNode; const AFixtureDirectory: String); +var LBlock: TPCOperationsComp; LPool: TPCOperationsComp; + LInput,LObservedTree: TOperationsHashTree; LReplay: TPCOperationsComp; + LStream,LExpectedBytes: TMemoryStream; LAffected: TOrderedList<Cardinal>; + LNotify: TNodeNotifyEvents; LObserved: TDenseObservedChangeInfo; + LOriginalHash,LOriginalBytesHash: TRawBytes; + LHeader: TOperationBlock; LFileName,LError: String; + I,J,LObservedIndex,LInitialCount,LBlockOperations,LExpectedResend,LResent: Integer; + LAnnouncedLast,LAccepted: Boolean; + LInitialTip: Cardinal; LUntil: UInt64; + LRequest,LStarted,LAdmitted,LFinished: TTickCount; + LCreate,LExit,LKernel,LUser: TFileTime; + LCPUBefore,LCPUAfter: UInt64; + + function CPUTime: UInt64; + begin + if not GetProcessTimes(GetCurrentProcess,LCreate,LExit,LKernel,LUser) then RaiseLastOSError; + Result := ((UInt64(LKernel.dwHighDateTime) shl 32) or LKernel.dwLowDateTime) + + ((UInt64(LUser.dwHighDateTime) shl 32) or LUser.dwLowDateTime); + end; + +begin + Require(ANode.Bank.BlocksCount=841529,'Real fixture must start at the frozen public tip 841529'); + LAnnouncedLast := GetEnvironmentVariable('PASCAL_BLOCK_BENCH_ANNOUNCED_LAST')='1'; + LBlock := TPCOperationsComp.Create(ANode.Bank); + LInput := TOperationsHashTree.Create; + LObservedTree := TOperationsHashTree.Create; + LNotify := nil; + FRealCompletion := TEvent.Create(nil,True,False,''); + try + LPool := ANode.LockMempoolWrite; + try + LInitialTip := LPool.OperationBlock.block; + LInitialCount := LPool.Count; + Require(LInitialCount=185831,'Real catch-up fixture requires the complete pending input'); + LInput.CopyFromHashTree(LPool.OperationsHashTree); + LOriginalHash := Copy(LInput.HashTree); + LStream := TMemoryStream.Create; + try + Require(LInput.SaveOperationsHashTreeToStream(LStream,True),'Serialize original complete pending input'); + LOriginalBytesHash := TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size); + finally LStream.Free; end; + LObservedIndex := -1; + for J := 0 to LInput.OperationsCount-1 do + if LInput.GetOperation(J) is TOpChangeAccountInfo then begin + LObservedIndex := J; + Break; + end; + Require(LObservedIndex>=0,'A real signed change-info operation is required for replay observation'); + LObserved := TDenseObservedChangeInfo.CopySignedOperation( + TOpChangeAccountInfo(LInput.GetOperation(LObservedIndex))); + try + LObservedTree.Max0feeOperationsBySigner := LInput.Max0feeOperationsBySigner; + for J := 0 to LInput.OperationsCount-1 do + if J=LObservedIndex then + Require(LObservedTree.AddOperationToHashTree(LObserved),'Register inherited replay observation') + else + Require(LObservedTree.AddOperationToHashTree(LInput.GetOperation(J)),'Retain original real operation'); + finally LObserved.Free; end; + LStream := TMemoryStream.Create; + try + Require(LObservedTree.SaveOperationsHashTreeToStream(LStream,True),'Serialize observed pending input'); + Require(TBaseType.Equals(LOriginalBytesHash,TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size)), + 'Adding replay observation must preserve every pending storage byte'); + finally LStream.Free; end; + LPool.OperationsHashTree.CopyFromHashTree(LObservedTree); + LInput.CopyFromHashTree(LObservedTree); + // Resend metadata is not part of the operation's wire/storage bytes. + // Controlled zeros make the legacy bounded selection deterministic. + for J := 0 to LPool.Count-1 do begin + LPool.Operation[J].ResendCount := 0; + LPool.Operation[J].ResendOnBlock := 0; + end; + Require(TBaseType.Equals(LOriginalHash,LPool.OperationsHashTree.HashTree),'Observed ordered input hash is unchanged'); + GDenseObservedTransaction := LPool.SafeBoxTransaction; + InterlockedExchange(GDenseObservedAttempts,0); + finally ANode.UnlockMempoolWrite; end; + LNotify := TNodeNotifyEvents.Create(nil); + LNotify.OnSynchronizationChanged := ActualSynchronizationChanged; + LNotify.Node := ANode; + LUntil := GetTickCount64+100; + repeat CheckSynchronize(0); Sleep(1); until GetTickCount64>=LUntil; + FRealCompletion.ResetEvent; + ANode.BeginBlockChainUpdate; + try + for I := 841529 to 841533 do begin + LFileName := IncludeTrailingPathDelimiter(AFixtureDirectory)+'block-'+IntToStr(I)+'.bin'; + LStream := TMemoryStream.Create; + try + LStream.LoadFromFile(LFileName); + Require(LBlock.LoadBlockFromStream(LStream,LError),'Actual network-format block decode: '+LError); + finally LStream.Free; end; + LHeader := LBlock.OperationBlock; + LBlockOperations := LBlock.Count; + Require((LHeader.block=Cardinal(I)) and (LHeader.protocol_version=5),'Actual source block/header sequence'); + LRequest := TPlatform.GetTickCount; + Require(ANode.TryLockNode(3000),'Actual received-block writer admission'); + LAdmitted := TPlatform.GetTickCount; + try + LCPUBefore := CPUTime; + LStarted := TPlatform.GetTickCount; + // Normal core bank validation computes PoW, executes the actual block, + // persists it and invokes the actual node's deferred-pending subscriber. + if LAnnouncedLast and (I=841533) then + LAccepted := ANode.AddNewBlockChain(nil,LBlock,LError) + else + LAccepted := ANode.Bank.AddNewBlockChainBlock(LBlock,0,LError); + Require(LAccepted,'Actual full block acceptance: '+LError); + LCPUAfter := CPUTime; + LFinished := TPlatform.GetTickCount; + Require(ANode.Bank.BlocksCount=Cardinal(I+1),'Actual committed height'); + Require(TPCOperationsComp.EqualsOperationBlock(LHeader,ANode.Bank.LastOperationBlock), + 'Committed header must equal the extracted actual public header'); + Require(not ANode.PendingOperationsAvailable,'Pending balances remain unavailable through the received batch'); + Require(not ANode.GetSynchronizationStatus.PendingOperationsAvailable,'Cached availability agrees with the node'); + Require(ANode.GetSynchronizationStatus.LocalBlockCount=Cardinal(I+1),'Cached confirmed tip advances immediately'); + Require(LPool.OperationBlock.block=LInitialTip,'No per-block sanitizer may replace the deferred pending header'); + Require(not LPool.SafeBoxTransaction.CheckIntegrity,'The deferred overlay is not exposed as current'); + Require(InterlockedCompareExchange(GDenseObservedAttempts,0,0)=0,'Actual pending validator must not replay per block'); + Require((LPool.Count=LInitialCount) and TBaseType.Equals(LOriginalHash,LPool.OperationsHashTree.HashTree), + 'Catch-up retains the exact original operation sequence'); + for J := 0 to LPool.Count-1 do + Require((LPool.Operation[J].ResendCount=0) and (LPool.Operation[J].ResendOnBlock=0), + 'Neither bulk blocks nor a deferred announcement may collect stale pending resends'); + Log(Format('REAL_BLOCK block=%d operations=%d admission_ms=%d body_ms=%d cpu_ms=%d retained_pending=%d observed_replays=%d', + [I,LBlockOperations,Int64(LAdmitted-LRequest),Int64(LFinished-LStarted), + (LCPUAfter-LCPUBefore) div 10000,LInitialCount,InterlockedCompareExchange(GDenseObservedAttempts,0,0)])); + finally ANode.UnlockNode; end; + end; + LStarted := TPlatform.GetTickCount; + ANode.Bank.SafeBox.PCAbstractMem.FlushCache; + Log('REAL_BATCH final_safebox_flush_ms='+IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + finally + LStarted := TPlatform.GetTickCount; + LCPUBefore := CPUTime; + ANode.EndBlockChainUpdate; + end; + LUntil := GetTickCount64+30000; + repeat + CheckSynchronize(0); + if FRealCompletion.WaitFor(0)=wrSignaled then Break; + Sleep(1); + until GetTickCount64>=LUntil; + Require(FRealCompletion.WaitFor(0)=wrSignaled,'Actual maintenance completion must arrive without a polling timer'); + Require(ANode.PendingOperationsAvailable,'The final complete pending overlay is available'); + Require(InterlockedCompareExchange(GDenseObservedAttempts,0,0)=1,'Observed real validator runs in exactly one final replay'); + Log(Format('REAL_FINAL reconcile_wait_ms=%d process_cpu_ms=%d observed_replays=%d', + [TPlatform.GetElapsedMilliseconds(LStarted),(CPUTime-LCPUBefore) div 10000, + InterlockedCompareExchange(GDenseObservedAttempts,0,0)])); + LPool := ANode.LockMempoolRead; + try + Require((LPool.Count=LInitialCount) and (LPool.OperationBlock.block=ANode.Bank.BlocksCount), + 'Final pending count/header are coherent with the committed confirmed tip'); + Require(LPool.SafeBoxTransaction.CheckIntegrity,'Final pending overlay integrity'); + LResent := 0; + for J := 0 to LPool.Count-1 do begin + if LAnnouncedLast and (J<CT_MaxResendMemPoolOperations) then LExpectedResend:=1 + else LExpectedResend:=0; + Require(LPool.Operation[J].ResendCount=LExpectedResend, + 'Only the original bounded announced-block resend selection increments once'); + if LExpectedResend=1 then begin + Inc(LResent); + Require(LPool.Operation[J].ResendOnBlock=Int64(LPool.OperationBlock.block), + 'Selected resend metadata uses the final coherent pending header'); + end else Require(LPool.Operation[J].ResendOnBlock=0,'Bulk catch-up does not invent a resend intent'); + end; + Log(Format('REAL_RESENDS announced_last=%s selected=%d retained=%d', + [BoolToStr(LAnnouncedLast,True),LResent,LPool.Count])); + LReplay := TPCOperationsComp.Create(ANode.Bank); + LStream := TMemoryStream.Create; + LExpectedBytes := TMemoryStream.Create; + LAffected := TOrderedList<Cardinal>.Create(False,TComparison_Cardinal); + try + // The oracle performs independent ordinary validation/insertion on the + // final confirmed base. All of this work is outside block/replay timing. + Require(LReplay.AddOperations(LInput,LError)=LInitialCount,'Independent final full pending replay: '+LError); + Require(TBaseType.Equals(LReplay.OperationsHashTree.HashTree,LPool.OperationsHashTree.HashTree), + 'Final ordered hash matches normal validation/insertion'); + for J := 0 to LPool.Count-1 do begin + Require(LPool.Operation[J]=LInput.GetOperation(J),'Every final registered operation retains its exact order'); + Require(LReplay.OperationsHashTree.IndexOfOperation(LPool.Operation[J])= + LPool.OperationsHashTree.IndexOfOperation(LPool.Operation[J]),'Final SHA representative'); + Require(LReplay.OperationsHashTree.IndexOfOpReference(LPool.Operation[J].GetOpReference)= + LPool.OperationsHashTree.IndexOfOpReference(LPool.Operation[J].GetOpReference),'Final reference representative'); + LPool.Operation[J].AffectedAccounts(LAffected); + end; + for J := 0 to LAffected.Count-1 do + Require(TAccountComp.EqualAccounts(LReplay.SafeBoxTransaction.Account(LAffected[J]), + LPool.SafeBoxTransaction.Account(LAffected[J])),'Final pending account/seal equals full independent replay'); + Require(LReplay.SafeBoxTransaction.TotalFee=LPool.SafeBoxTransaction.TotalFee,'Final replay fees'); + Require(LReplay.SafeBoxTransaction.TotalBalance=LPool.SafeBoxTransaction.TotalBalance,'Final replay balance'); + Require(LPool.OperationsHashTree.SaveOperationsHashTreeToStream(LStream,True),'Final pending serialization'); + Require(LReplay.OperationsHashTree.SaveOperationsHashTreeToStream(LExpectedBytes,True),'Independent oracle serialization'); + Require((LStream.Size=LExpectedBytes.Size) and CompareMem(LStream.Memory,LExpectedBytes.Memory,LStream.Size), + 'Every final pending storage byte matches independent full replay'); + Require(TBaseType.Equals(LOriginalBytesHash,TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size)), + 'These five historical blocks preserve all original pending bytes'); + Require(InterlockedCompareExchange(GDenseObservedAttempts,0,0)=1,'The independent oracle must not count as node replay'); + Log(Format('REAL_PENDING_FINAL count=%d affected_accounts=%d hash=%s bytes=%d bytes_sha=%s bank_hash=%s', + [LPool.Count,LAffected.Count,TCrypto.ToHexaString(LPool.OperationsHashTree.HashTree),LStream.Size, + TCrypto.ToHexaString(TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size)), + TCrypto.ToHexaString(ANode.Bank.SafeBox.SafeBoxHash)])); + finally LAffected.Free; LExpectedBytes.Free; LStream.Free; LReplay.Free; end; + finally ANode.UnlockMempoolRead; end; + finally + if Assigned(LNotify) then LNotify.Node := nil; + LNotify.Free; + FreeAndNil(FRealCompletion); + GDenseObservedTransaction := nil; + LObservedTree.Free; + LInput.Free; + LBlock.Free; + end; + Log('PASS five actual PoW-validated blocks; no per-block pending replay; one final inherited-validator observation; full final account/seal/index/bytes oracle'); +end; + +procedure TBlockThroughputChecks.VerifyStoredRealBlocks(const ADataDirectory,AFixtureDirectory: String); +var LStorage: TFileStorage; LBlock: TPCOperationsComp; + LActual,LOriginal: TMemoryStream; I: Integer; +begin + LStorage := TFileStorage.Create(nil); + LBlock := TPCOperationsComp.Create(nil); + LActual := TMemoryStream.Create; + LOriginal := TMemoryStream.Create; + try + LStorage.ReadOnly := True; + LStorage.SetBlockChainFile(AnsiString(IncludeTrailingPathDelimiter(ADataDirectory)+'Data'+PathDelim+'BlockChainStream.blocks')); + Require(LStorage.Initialize,'Reopen actual stored chain read-only after closing the writer'); + for I := 841529 to 841533 do begin + Require(LStorage.LoadBlockChainBlock(LBlock,I),'Reopen each actual accepted block'); + LActual.Clear; + Require(LBlock.SaveBlockToStream(False,LActual),'Actual stored block network serialization'); + LOriginal.LoadFromFile(IncludeTrailingPathDelimiter(AFixtureDirectory)+'block-'+IntToStr(I)+'.bin'); + Require((LActual.Size=LOriginal.Size) and CompareMem(LActual.Memory,LOriginal.Memory,LActual.Size), + 'Reopened actual block differs from its original extracted network bytes'); + Log(Format('REAL_STORAGE block=%d bytes=%d sha256=%s', + [I,LActual.Size,TCrypto.ToHexaString(TCrypto.DoSha256(PAnsiChar(LActual.Memory),LActual.Size))])); + end; + Log('PASS actual persisted five-block reopen and all original network bytes/hashes'); + finally LOriginal.Free; LActual.Free; LBlock.Free; LStorage.Free; end; +end; + +procedure TBlockThroughputChecks.Run(const ADataDirectory: String); +var LNode: TNode; LMempool: TPCOperationsComp; + LStarted, LAdmitted, LFinished: TTickCount; + LHash,LSerializedHash: TRawBytes; LBlocks: Cardinal; LCount, I, J, LSamples, LCacheBudget: Integer; + LOriginalTree: TOperationsHashTree; + LOriginalOperations: array of TPCOperation; + LOriginalSHAIndexes,LOriginalReferenceIndexes: array of Integer; + LAmount,LFee: Int64; LStream: TMemoryStream; + LReadyEvent: TEvent; LReadyName: String; + LRealFixture: String; LVerifyReal: Boolean; + LCreate, LExit, LKernel, LUser: TFileTime; + LCPUBefore, LCPUAfter: UInt64; + + function CPUTime: UInt64; + begin + if not GetProcessTimes(GetCurrentProcess, LCreate, LExit, LKernel, LUser) then + RaiseLastOSError; + Result := ((UInt64(LKernel.dwHighDateTime) shl 32) or LKernel.dwLowDateTime) + + ((UInt64(LUser.dwHighDateTime) shl 32) or LUser.dwLowDateTime); + end; + +begin + LVerifyReal := False; + LRealFixture := GetEnvironmentVariable('PASCAL_BLOCK_BENCH_REAL_BLOCKS'); + Require(ADataDirectory <> '', 'PASCAL_BLOCK_BENCH_DATA must name an owned prepared clone'); + Require(FileExists(IncludeTrailingPathDelimiter(ADataDirectory) + '.block-throughput-owned'), + 'The explicit data directory is missing its owned-clone marker'); + Require(FileExists(IncludeTrailingPathDelimiter(ADataDirectory) + 'Data\safebox.am_safebox'), + 'Prepared safebox is missing'); + Require(FileExists(IncludeTrailingPathDelimiter(ADataDirectory) + 'Data\BlockChainStream.blocks'), + 'Prepared blockchain stream is missing'); + Require(FileExists(IncludeTrailingPathDelimiter(ADataDirectory) + 'Data\pendingbuffer.ops'), + 'Prepared pending operations are missing'); + Require(not TNode.NodeExists, 'The throughput probe requires its own process/node'); + Log('DATA ' + ADataDirectory); + TCrypto.InitCrypto; + TNode.SetPascalCoinDataFolder(ADataDirectory); + LNode := TNode.Create(nil); + try + LNode.BroadcastData := False; + LCacheBudget := StrToIntDef(GetEnvironmentVariable('PASCAL_BLOCK_BENCH_ACCOUNT_CACHE'),0); + if LCacheBudget>0 then begin + Require((LCacheBudget=50000) or (LCacheBudget=250000),'Use one of the reviewed account cache budgets'); + LNode.Bank.SafeBox.PCAbstractMem.AccountCache.MaxRegisters := LCacheBudget; + end; + TNetData.NetData.NetConnectionsActive := False; + Require(not LNode.NetServer.Active, 'The throughput probe must not listen'); + LNode.Bank.StorageClass := TFileStorage; + Require(LNode.Bank.Storage.Initialize, 'Storage initialization failed'); + LStarted := TPlatform.GetTickCount; + Log('RESTORE starting actual InitSafeboxAndOperations'); + LNode.InitSafeboxAndOperations($FFFFFFFF, Progress); + Log('RESTORE completed milliseconds=' + IntToStr(TPlatform.GetElapsedMilliseconds(LStarted))); + Log('CACHE after_restore='+IntToStr(LNode.Bank.SafeBox.PCAbstractMem.AccountCache.MaxRegisters)); + if LCacheBudget>0 then Require(LNode.Bank.SafeBox.PCAbstractMem.AccountCache.MaxRegisters=LCacheBudget, + 'Restore must preserve the explicitly configured account cache budget'); + LReadyName := GetEnvironmentVariable('PASCAL_BLOCK_BENCH_READY_EVENT'); + if LReadyName<>'' then begin + LReadyEvent := TEvent.Create(nil,True,False,LReadyName); + try + Log('READY_GATE '+LReadyName); + Require(LReadyEvent.WaitFor(60000)=wrSignaled,'Owned performance measurement gate timed out'); + finally LReadyEvent.Free; end; + end; + if LRealFixture<>'' then begin + MeasureRealBlocks(LNode,LRealFixture); + LVerifyReal := True; + Exit; + end; + I := StrToIntDef(GetEnvironmentVariable('PASCAL_BLOCK_BENCH_CONFIRM_EVERY'),0); + if I>0 then begin + MeasureMixedRejections(LNode,I); + Exit; + end; + LMempool := LNode.LockMempoolRead; + try + LBlocks := LNode.Bank.BlocksCount; + LCount := LMempool.Count; + LHash := Copy(LMempool.OperationsHashTree.HashTree); + LOriginalTree := LMempool.OperationsHashTree; + LAmount := LOriginalTree.TotalAmount; + LFee := LOriginalTree.TotalFee; + SetLength(LOriginalOperations,LCount); + SetLength(LOriginalSHAIndexes,LCount); + SetLength(LOriginalReferenceIndexes,LCount); + for J := 0 to LCount-1 do begin + LOriginalOperations[J] := LMempool.Operation[J]; + LOriginalSHAIndexes[J] := LOriginalTree.IndexOfOperation(LOriginalOperations[J]); + LOriginalReferenceIndexes[J] := LOriginalTree.IndexOfOpReference(LOriginalOperations[J].GetOpReference); + end; + LStream := TMemoryStream.Create; + try + Require(LOriginalTree.SaveOperationsHashTreeToStream(LStream,True),'Initial serialization failed'); + LSerializedHash := TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size); + Log(Format('OUTPUT original_serialized_bytes=%d sha256=%s amount=%d fee=%d', + [LStream.Size,TCrypto.ToHexaString(LSerializedHash),LAmount,LFee])); + finally LStream.Free; end; + Log(Format('BASELINE blocks=%d pending=%d protocol=%d', + [LBlocks, LCount, LMempool.OperationBlock.protocol_version])); + Require(LCount = 185831, 'The public-data baseline did not restore all 185831 pending operations'); + finally LNode.UnlockMempoolRead; end; + if SameText(GetEnvironmentVariable('PASCAL_BLOCK_BENCH_PHASES'), '1') then LSamples := 1 + else LSamples := 3; + for I := 1 to LSamples do begin + LStarted := TPlatform.GetTickCount; + LMempool := LNode.LockMempoolWrite; + LAdmitted := TPlatform.GetTickCount; + try + LCPUBefore := CPUTime; + LMempool.SanitizeOperations; + LCPUAfter := CPUTime; + LFinished := TPlatform.GetTickCount; + Require(LMempool.Count = LCount, 'Sanitization changed the pending-operation count'); + Require(TBaseType.Equals(LHash, LMempool.OperationsHashTree.HashTree), + 'Sanitization changed operation order or hash'); + Require(LNode.Bank.BlocksCount = LBlocks, 'The probe unexpectedly accepted a block'); + // These output checks run after the timed body. They compare the exact + // real ordered objects, both public indexes and the stored byte format. + if GetEnvironmentVariable('PASCAL_BLOCK_BENCH_ASSERT_RETENTION')='1' then + Require(LMempool.OperationsHashTree=LOriginalTree,'All-valid sanitizer replaced the original tree'); + Require((LMempool.OperationsHashTree.TotalAmount=LAmount) and + (LMempool.OperationsHashTree.TotalFee=LFee),'Sanitization changed aggregate amounts'); + for J := 0 to LCount-1 do begin + Require(LMempool.Operation[J]=LOriginalOperations[J],'Sanitization changed registered operation order'); + Require(LMempool.OperationsHashTree.IndexOfOperation(LOriginalOperations[J])=LOriginalSHAIndexes[J], + 'Sanitization changed a SHA lookup'); + Require(LMempool.OperationsHashTree.IndexOfOpReference(LOriginalOperations[J].GetOpReference)= + LOriginalReferenceIndexes[J],'Sanitization changed an operation-reference lookup'); + end; + LStream := TMemoryStream.Create; + try + Require(LMempool.OperationsHashTree.SaveOperationsHashTreeToStream(LStream,True),'Output serialization failed'); + Require(TBaseType.Equals(LSerializedHash,TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size)), + 'Sanitization changed exact pending serialization bytes'); + finally LStream.Free; end; + finally LNode.UnlockMempoolWrite; end; + Log(Format('SANITIZE sample=%d admission_ms=%d body_ms=%d cpu_ms=%d pending=%d', + [I, Int64(LAdmitted - LStarted), + Int64(LFinished - LAdmitted), + (LCPUAfter - LCPUBefore) div 10000, LCount])); + end; + Log(Format('PASS %d actual sanitizations; count/identity/order/hash/both-indexes/amount/fee/serialized-bytes/local-tip preserved; no UI/network activity', [LSamples])); + if SameText(GetEnvironmentVariable('PASCAL_BLOCK_BENCH_PHASES'), '1') then MeasurePhases(LNode); + finally + LNode.Free; + if LVerifyReal then VerifyStoredRealBlocks(ADataDirectory,LRealFixture); + end; +end; + +function RunBlockThroughputChecks(const ADataDirectory, ALogPath: String): Integer; +var LChecks: TBlockThroughputChecks; +begin + Result := 1; + if FileExists(ALogPath) then raise Exception.Create('Refusing an existing throughput log'); + LChecks := TBlockThroughputChecks.Create(ALogPath); + try + try LChecks.Run(ADataDirectory); Result := 0; + except on E: Exception do LChecks.Log('FAIL ' + E.ClassName + ': ' + E.Message); end; + finally LChecks.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/block-throughput/UChangeAccountInfoReplayChecks.pas b/src/tests/experimental/native/block-throughput/UChangeAccountInfoReplayChecks.pas new file mode 100644 index 000000000..013573060 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/UChangeAccountInfoReplayChecks.pas @@ -0,0 +1,205 @@ +unit UChangeAccountInfoReplayChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Actual signed operations on generated in-memory accounts. No node, listener, + existing keys or replacement operation implementation is used. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +procedure CheckChangeInfoSelfTarget; +procedure CheckChangeInfoDistinctTarget; +procedure CheckChangeInfoRepeatedAndRollback; +procedure CheckChangeInfoRejectsWithoutMutation; + +implementation + +uses Classes, SysUtils, UMemory, UAccounts, UBlockChain, UOpTransaction, + UPCDataTypes, UCrypto, UBaseTypes, UConst; + +type + TChangeInfoSafeBox = class(TPCSafeBox) + public + procedure Build(const AKey: TAccountKey); + procedure Seed(const AAccount: TAccount); + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('Change-info replay: '+AMessage); +end; + +procedure TChangeInfoSafeBox.Build(const AKey: TAccountKey); +var LBlock: TOperationBlock; I: Integer; +begin + for I := 0 to 159 do begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I*CT_NewLineSecondsAvg; + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + DoUpgradeToProtocol6; +end; + +procedure TChangeInfoSafeBox.Seed(const AAccount: TAccount); +begin + UpdateAccount(AAccount.account,AAccount.accountInfo,AAccount.name, + AAccount.account_type,AAccount.balance,AAccount.n_operation, + AAccount.account_data,AAccount.account_seal,aus_transaction_commit, + 0,0,False,False); +end; + +function MakeAccount(ANumber: Cardinal; const AKey: TAccountKey; + const AName: String): TAccount; +begin + Result := CT_Account_NUL; + Result.account := ANumber; + Result.accountInfo.state := as_Normal; + Result.accountInfo.accountKey := AKey.GetCopy; + Result.balance := 100000; + Result.n_operation := 7; + Result.account_type := 42; + Result.name := TEncoding.ASCII.GetBytes(AName); + Result.account_data := TEncoding.ASCII.GetBytes('original data'); +end; + +function ExpectedSeal(const ABefore: TAccount; AOperation: TPCOperation; + const ASafeboxHash: TRawBytes): TRawBytes; +var LStream: TMemoryStream; LAccount: TAccount; LOpID: TRawBytes; +begin + // Independent byte-level oracle for the protocol's pre-change account seal. + LStream := TMemoryStream.Create; + try + LAccount := ABefore.GetCopy; + LAccount.SerializeAccount(LStream,CT_PROTOCOL_6); + LOpID := AOperation.GetOpID; + if Length(LOpID)>0 then LStream.WriteBuffer(LOpID[0],Length(LOpID)); + if Length(ASafeboxHash)>0 then LStream.WriteBuffer(ASafeboxHash[0],Length(ASafeboxHash)); + Result := TCrypto.DoRipeMD160AsRaw(TCrypto.DoSha256(LStream.Memory,LStream.Size)); + finally LStream.Free; end; +end; + +procedure CheckCase(AKind: Integer); +var + LDisposables: TDisposables; + LOwner,LNextOwner: TECPrivateKey; + LSafeBox: TChangeInfoSafeBox; + LTransaction: TPCSafeBoxTransaction; + LPrevious: TAccountPreviousBlockInfo; + LOperation,LSecond: TPCOperation; + LBaseSigner,LBaseTarget,LSigner,LTarget,LFirstResult,LBeforeSecond: TAccount; + LNewName,LNewData,LSafeboxHash: TRawBytes; + LTargetNumber,LOperationNumber: Cardinal; + LError: String; +begin + TCrypto.InitCrypto; + LOwner := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LNextOwner := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LOwner.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LNextOwner.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LSafeBox := LDisposables.AddObject(TChangeInfoSafeBox.Create) as TChangeInfoSafeBox; + LSafeBox.Build(LOwner.PublicKey); + LSafeBox.Seed(MakeAccount(100,LOwner.PublicKey,'original_signer')); + LSafeBox.Seed(MakeAccount(101,LOwner.PublicKey,'original_target')); + LBaseSigner := LSafeBox.Account(100); + LTargetNumber := 100; + if AKind=1 then LTargetNumber := 101; + LBaseTarget := LSafeBox.Account(LTargetNumber); + LSafeboxHash := Copy(LSafeBox.SafeBoxHash); + LTransaction := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + LPrevious := LDisposables.AddObject(TAccountPreviousBlockInfo.Create) as TAccountPreviousBlockInfo; + LNewName := TEncoding.ASCII.GetBytes('replayed_account'); + LNewData := TEncoding.ASCII.GetBytes('new account data'); + LOperationNumber := 8; + if AKind=3 then LOperationNumber := 99; + LOperation := LDisposables.AddObject(TOpChangeAccountInfo.CreateChangeAccountInfo( + CT_PROTOCOL_6,100,LOperationNumber,LTargetNumber,LOwner, + True,LNextOwner.PublicKey,True,LNewName,True,99,True,LNewData, + 100,CT_TOperationPayload_NUL)) as TPCOperation; + if AKind=3 then begin + Require(not LOperation.DoOperation(LPrevious,LTransaction,LError), + 'an invalid operation number must be rejected'); + Require(LError='Invalid n_operation','wrong rejection reason: '+LError); + Require(LTransaction.TotalFee=0,'rejection changed transaction fees'); + Require(TAccountComp.EqualAccounts(LBaseSigner,LTransaction.Account(100)), + 'a rejected self-change altered its transaction account'); + end else begin + Require(LOperation.DoOperation(LPrevious,LTransaction,LError),LError); + LSigner := LTransaction.Account(100); + LTarget := LTransaction.Account(LTargetNumber); + Require((LSigner.n_operation=8) and (LSigner.balance=99900), + 'signer nonce or fee changed more than once'); + Require(LTransaction.TotalFee=100,'the operation fee must be applied exactly once'); + Require(TAccountComp.EqualAccountKeys(LNextOwner.PublicKey,LTarget.accountInfo.accountKey), + 'target owner was not changed'); + Require(TBaseType.Equals(LNewName,LTarget.name) and + TBaseType.Equals(LNewData,LTarget.account_data) and (LTarget.account_type=99), + 'target name/data/type were not applied'); + Require(TBaseType.Equals(ExpectedSeal(LBaseSigner,LOperation,LSafeboxHash),LSigner.account_seal), + 'signer seal differs from its original account bytes'); + Require(TBaseType.Equals(ExpectedSeal(LBaseTarget,LOperation,LSafeboxHash),LTarget.account_seal), + 'target seal differs from its original account bytes'); + if AKind=1 then begin + Require((LTarget.balance=LBaseTarget.balance) and (LTarget.n_operation=LBaseTarget.n_operation), + 'a distinct target was charged the signer fee or nonce'); + Require(TAccountComp.EqualAccountKeys(LOwner.PublicKey,LSigner.accountInfo.accountKey) and + TBaseType.Equals(LBaseSigner.name,LSigner.name) and + TBaseType.Equals(LBaseSigner.account_data,LSigner.account_data) and + (LSigner.account_type=LBaseSigner.account_type), + 'a distinct target change leaked into its signer'); + end; + if AKind=2 then begin + LFirstResult := LTarget.GetCopy; + LBeforeSecond := LTarget.GetCopy; + LSecond := LDisposables.AddObject(TOpChangeAccountInfo.CreateChangeAccountInfo( + CT_PROTOCOL_6,100,9,100,LNextOwner,False,CT_TECDSA_Public_Nul, + True,TEncoding.ASCII.GetBytes('second_edit'),True,100,False,nil, + 200,CT_TOperationPayload_NUL)) as TPCOperation; + Require(LSecond.DoOperation(LPrevious,LTransaction,LError),LError); + LTarget := LTransaction.Account(100); + Require((LTarget.n_operation=9) and (LTarget.balance=99700) and + (LTransaction.TotalFee=300),'a repeated edit ignored the pending account state'); + Require(TBaseType.Equals(ExpectedSeal(LBeforeSecond,LSecond,LSafeboxHash),LTarget.account_seal), + 'a repeated edit sealed stale base state'); + LTransaction.Rollback; + LPrevious.Clear; + Require(TAccountComp.EqualAccounts(LBaseSigner,LTransaction.Account(100)) and + (LTransaction.TotalFee=0),'rollback did not restore the original account'); + Require(LOperation.DoOperation(LPrevious,LTransaction,LError),LError); + Require(TAccountComp.EqualAccounts(LFirstResult,LTransaction.Account(100)), + 'replay after rollback changed the accepted account result'); + end; + end; + Require(TAccountComp.EqualAccounts(LBaseSigner,LSafeBox.Account(100)) and + TAccountComp.EqualAccounts(LBaseTarget,LSafeBox.Account(LTargetNumber)) and + TBaseType.Equals(LSafeboxHash,LSafeBox.SafeBoxHash), + 'transactional replay mutated the underlying safebox or hash'); +end; + +procedure CheckChangeInfoSelfTarget; +begin + CheckCase(0); +end; + +procedure CheckChangeInfoDistinctTarget; +begin + CheckCase(1); +end; + +procedure CheckChangeInfoRepeatedAndRollback; +begin + CheckCase(2); +end; + +procedure CheckChangeInfoRejectsWithoutMutation; +begin + CheckCase(3); +end; + +end. diff --git a/src/tests/experimental/native/block-throughput/UMempoolSanitizeChecks.pas b/src/tests/experimental/native/block-throughput/UMempoolSanitizeChecks.pas new file mode 100644 index 000000000..815d62037 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/UMempoolSanitizeChecks.pas @@ -0,0 +1,585 @@ +unit UMempoolSanitizeChecks; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Generated, signed operations exercise the actual sanitizer and registry. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type + TMempoolSanitizeCheck = (mscAllValid, mscRejectedAndDuplicates, + mscDeletedDuplicateIndex, mscZeroFeeSignerLimit, mscReplayException, + mscRegistryOwnership, mscRecoverRejection, mscReferenceBoundaries, + mscProtocolSignatureCaches); + +function RunMempoolSanitizeChecks(const ALogPath: String): Integer; +function RunMempoolSanitizeCheck(ACheck: TMempoolSanitizeCheck; + const ALogPath: String): Integer; + +implementation + +uses Classes, SysUtils, UAccounts, UBlockChain, UOpTransaction, UCrypto, + UBaseTypes, UConst, UPCDataTypes, UNode; + +type + TGeneratedSanitizeSafeBox = class(TPCSafeBox) + procedure Build(const AKey: TAccountKey); + end; + + EExpectedReplayFailure = class(Exception); + TThrowingTransaction = class(TOpTransaction) + function DoOperation(APrevious: TAccountPreviousBlockInfo; + ATransaction: TPCSafeBoxTransaction; var AErrors: String): Boolean; override; + end; + + TReferenceBoundaryTransaction = class(TOpTransaction) + public + function SignerAccount: Cardinal; override; + function N_Operation: Cardinal; override; + procedure ForceLookupCollision; + end; + + TMempoolSanitizeChecks = class + private + FLog: TStringList; + FPath: String; + FBank: TPCBank; + FPool: TPCOperationsComp; + FKey: TECPrivateKey; + FChanges: Integer; + procedure Check(AValue: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure Changed(Sender: TObject); + procedure Reset; + function Add(ANonce: Cardinal; AFee: UInt64 = 1): TPCOperation; + procedure AssertSequence(const AExpected: array of TPCOperation); + procedure CheckAllValid; + procedure CheckRejectedAndDuplicates; + procedure CheckDeletedDuplicateIndex; + procedure CheckZeroFeeSignerLimit; + procedure CheckReplayException; + procedure CheckRegistryOwnership; + procedure CheckRecoverRejection; + procedure CheckReferenceBoundaries; + procedure CheckProtocolSignatureCaches; + public + constructor Create(const APath: String); + destructor Destroy; override; + procedure Run; + procedure RunOne(ACheck: TMempoolSanitizeCheck); + end; + +var GThrowReplayOnce: Boolean; + +procedure TGeneratedSanitizeSafeBox.Build(const AKey: TAccountKey); +var I: Integer; LBlock: TOperationBlock; LAccount: TAccount; +begin + for I := 0 to 159 do begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I * CT_NewLineSecondsAvg; + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + LAccount := Account(0); + UpdateAccount(0,LAccount.accountInfo,LAccount.name,LAccount.account_type, + 1000000,0,LAccount.account_data,LAccount.account_seal, + aus_transaction_commit,0,0,False,False); +end; + +function TThrowingTransaction.DoOperation(APrevious: TAccountPreviousBlockInfo; + ATransaction: TPCSafeBoxTransaction; var AErrors: String): Boolean; +begin + if GThrowReplayOnce then begin + GThrowReplayOnce := False; + raise EExpectedReplayFailure.Create('Injected failure after an accepted prefix'); + end; + Result := inherited DoOperation(APrevious,ATransaction,AErrors); +end; + +function TReferenceBoundaryTransaction.SignerAccount: Cardinal; +begin + case inherited N_Operation of + 2: Result := $80000000; + 3: Result := High(Cardinal); + else Result := 0; + end; +end; + +function TReferenceBoundaryTransaction.N_Operation: Cardinal; +begin + case inherited N_Operation of + 3,4: Result := High(Cardinal); + else Result := 0; + end; +end; + +procedure TReferenceBoundaryTransaction.ForceLookupCollision; +begin + // Controlled lookup-key boundaries/collision only; inherited DoOperation + // still executes and validates the distinct, correctly signed transactions. + SetLength(FBufferedSha256,32); + FillChar(FBufferedSha256[0],32,$A5); +end; + +constructor TMempoolSanitizeChecks.Create(const APath: String); +begin + inherited Create; + FPath := APath; + FLog := TStringList.Create; + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FBank := TPCBank.Create(nil); + TGeneratedSanitizeSafeBox(FBank.SafeBox).Build(FKey.PublicKey); + FPool := TPCOperationsComp.Create(nil); + FPool.Bank := FBank; +end; + +destructor TMempoolSanitizeChecks.Destroy; +begin + FPool.Free; + FBank.Free; + FKey.Free; + FLog.Free; + inherited; +end; + +procedure TMempoolSanitizeChecks.Check(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure TMempoolSanitizeChecks.Log(const AMessage: String); +begin + FLog.Add(AMessage); + FLog.SaveToFile(FPath); +end; + +procedure TMempoolSanitizeChecks.Changed(Sender: TObject); +begin + Inc(FChanges); +end; + +procedure TMempoolSanitizeChecks.Reset; +begin + GThrowReplayOnce := False; + FPool.OperationsHashTree.OnChanged := nil; + FPool.Clear(True); + // Deliberately admit dirty input, as an existing pending tree can predate + // the current protocol's limits. Sanitize must enforce the new limits. + FPool.OperationsHashTree.Max0feeOperationsBySigner := -1; + FChanges := 0; +end; + +function TMempoolSanitizeChecks.Add(ANonce: Cardinal; AFee: UInt64): TPCOperation; +var LOperation: TPCOperation; +begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_5,0,ANonce,1, + FKey,1,AFee,CT_TOperationPayload_NUL); + try + Check(FPool.OperationsHashTree.AddOperationToHashTree(LOperation),'Raw pending insertion'); + Result := FPool.OperationsHashTree.GetOperation(FPool.Count-1); + finally LOperation.Free; end; +end; + +procedure TMempoolSanitizeChecks.AssertSequence(const AExpected: array of TPCOperation); +var I: Integer; LExpected: TOperationsHashTree; LAmount,LFee: Int64; +begin + Check(FPool.Count=Length(AExpected),'Accepted operation count'); + LExpected := TOperationsHashTree.Create; + try + LAmount := 0; + LFee := 0; + for I := 0 to High(AExpected) do begin + Check(FPool.Operation[I]=AExpected[I],'Replay order and registered operation identity'); + Check(FPool.OperationsHashTree.IndexOfOperation(AExpected[I])=I,'SHA lookup position'); + Check(FPool.OperationsHashTree.IndexOfOpReference(AExpected[I].GetOpReference)=I, + 'Operation reference lookup position'); + Check(LExpected.AddOperationToHashTree(AExpected[I]),'Expected sequence insertion'); + Inc(LAmount,AExpected[I].OperationAmount); + Inc(LFee,AExpected[I].OperationFee); + end; + Check(TBaseType.Equals(LExpected.HashTree,FPool.OperationsHashTree.HashTree),'Ordered operation hash'); + Check(TBaseType.Equals(FPool.OperationBlock.operations_hash,FPool.OperationsHashTree.HashTree), + 'Operation header hash'); + Check(FPool.OperationsHashTree.TotalAmount=LAmount,'Total amount'); + Check(FPool.OperationsHashTree.TotalFee=LFee,'Total fee'); + Check(FPool.OperationBlock.fee=LFee,'Operation header fee'); + Check(FPool.SafeBoxTransaction.TotalFee=LFee,'Replayed account transaction fee'); + Check(FBank.SafeBox.Account(0).n_operation=0,'Replay never commits the confirmed bank'); + finally LExpected.Free; end; +end; + +procedure TMempoolSanitizeChecks.CheckAllValid; +var LOld: TOperationsHashTree; LOne,LTwo,LThree: TPCOperation; +begin + Reset; + LOne := Add(1); LTwo := Add(2); LThree := Add(3); + LOld := FPool.OperationsHashTree; + LOld.OnChanged := Changed; + FPool.SanitizeOperations; + Check(FPool.OperationsHashTree=LOld,'All-valid replay retains original tree owner'); + Check(FChanges=0,'Sanitization emits no new changed callback'); + Check(not Assigned(FPool.OperationsHashTree.OnChanged),'Legacy replacement clears changed callback'); + AssertSequence([LOne,LTwo,LThree]); + FPool.SanitizeOperations; + Check(FPool.OperationsHashTree=LOld,'Repeated all-valid replay retains indexes'); + AssertSequence([LOne,LTwo,LThree]); + Log('PASS all-valid order/hash/fee/header/callback and repeated retention'); +end; + +procedure TMempoolSanitizeChecks.CheckRejectedAndDuplicates; +var LOld: TOperationsHashTree; LOne,LTwo: TPCOperation; +begin + Reset; + LOne := Add(1); + Add(3); // Incorrect nonce; replay must continue with the valid next operation. + LTwo := Add(2); + LOld := FPool.OperationsHashTree; + FPool.SanitizeOperations; + Check(FPool.OperationsHashTree<>LOld,'Rejection publishes the materialized replacement'); + AssertSequence([LOne,LTwo]); + Reset; + LOne := Add(1); + Check(FPool.OperationsHashTree.AddOperationToHashTree(LOne),'Duplicate input insertion'); + Add(1,2); // Same operation reference, different signature/hash/fee. + LTwo := Add(2); + FPool.SanitizeOperations; + AssertSequence([LOne,LTwo]); + Log('PASS invalid nonce, duplicate SHA/reference and later valid operation'); +end; + +procedure TMempoolSanitizeChecks.CheckDeletedDuplicateIndex; +var LOld: TOperationsHashTree; LOne: TPCOperation; +begin + Reset; + LOne := Add(1); + Check(FPool.OperationsHashTree.AddOperationToHashTree(LOne),'Duplicate insertion before delete'); + FPool.OperationsHashTree.Delete(0); + Check(FPool.OperationsHashTree.IndexOfOperation(LOne)=-1,'Actual deleted duplicate leaves absent SHA representative'); + Check(FPool.OperationsHashTree.IndexOfOpReference(LOne.GetOpReference)=-1, + 'Actual deleted duplicate leaves absent reference representative'); + LOld := FPool.OperationsHashTree; + FPool.SanitizeOperations; + Check(FPool.OperationsHashTree<>LOld,'Invalid lookup indexes prohibit fast retention'); + AssertSequence([LOne]); + Log('PASS actual duplicate Delete gap repaired by legacy fallback'); +end; + +procedure TMempoolSanitizeChecks.CheckZeroFeeSignerLimit; +var LOld: TOperationsHashTree; LOne,LTwo,LCandidate: TPCOperation; +begin + Reset; + LOne := Add(1,0); + LOld := FPool.OperationsHashTree; + FPool.SanitizeOperations; + Check(FPool.OperationsHashTree=LOld,'Zero-fee all-valid tree retained across limit change'); + Check(FPool.OperationsHashTree.Max0feeOperationsBySigner=1,'Current protocol zero-fee limit adopted'); + Check(FPool.OperationsHashTree.CountOperationsBySameSignerWithoutFee(0)=1,'Signer metadata adopted'); + AssertSequence([LOne]); + LCandidate := TOpTransaction.CreateTransaction(CT_PROTOCOL_5,0,2,1,FKey,1,0,CT_TOperationPayload_NUL); + try Check(not FPool.OperationsHashTree.CanAddOperationToHashTree(LCandidate),'Second zero-fee signer request rejected'); + finally LCandidate.Free; end; + Reset; + LOne := Add(1,0); Add(2,0); LTwo := Add(2,1); + FPool.SanitizeOperations; + AssertSequence([LOne,LTwo]); + Check(FPool.OperationsHashTree.CountOperationsBySameSignerWithoutFee(0)=1,'Fallback zero-fee signer metadata'); + Log('PASS protocol fee limit change, signer metadata and fee-paying continuation'); +end; + +procedure TMempoolSanitizeChecks.CheckReplayException; +var LOne,LThrow: TPCOperation; LRaised: Boolean; + LOriginal,LAfter: TMemoryStream; LOriginalTree: TOperationsHashTree; +begin + Reset; + LOne := Add(1); + LThrow := TThrowingTransaction.CreateTransaction(CT_PROTOCOL_5,0,2,1,FKey,1,1,CT_TOperationPayload_NUL); + try Check(FPool.OperationsHashTree.AddOperationToHashTree(LThrow),'Throwing operation inserted'); + finally LThrow.Free; end; + LThrow := FPool.Operation[1]; + LOriginalTree := FPool.OperationsHashTree; + LOriginal := TMemoryStream.Create; + LAfter := TMemoryStream.Create; + try + Check(LOriginalTree.SaveOperationsHashTreeToStream(LOriginal,True),'Capture complete exception input'); + GThrowReplayOnce := True; + LRaised := False; + try FPool.SanitizeOperations; + except on E: EExpectedReplayFailure do LRaised := True; end; + Check(LRaised,'Replay exception propagates'); + Check(FPool.OperationsHashTree=LOriginalTree,'A failed replay retains the complete original tree owner'); + Check((FPool.Count=2) and (FPool.Operation[0]=LOne) and (FPool.Operation[1]=LThrow), + 'A failed replay retains every registered operation in its original order'); + Check(FPool.OperationsHashTree.IndexOfOperation(LThrow)=1,'Failed replay preserves its SHA representative'); + Check(FPool.OperationsHashTree.IndexOfOpReference(LThrow.GetOpReference)=1,'Failed replay preserves its reference representative'); + Check(FPool.OperationsHashTree.SaveOperationsHashTreeToStream(LAfter,True),'Serialize complete retained input'); + Check((LOriginal.Size=LAfter.Size) and CompareMem(LOriginal.Memory,LAfter.Memory,LOriginal.Size), + 'An exception must not replace the complete input with a partial accepted prefix'); + // The account overlay is not consumed after an exception. A complete retry + // rebuilds it from the retained sequence before account reads may resume. + FPool.SanitizeOperations; + AssertSequence([LOne,LThrow]); + Log('PASS replay exception preserves complete indexed input and releases locks for a successful retry'); + finally LAfter.Free; LOriginal.Free; end; +end; + +procedure TMempoolSanitizeChecks.CheckRegistryOwnership; +var LClone: TOperationsHashTree; LOne: TPCOperation; LCount: Integer; +begin + Reset; + LCount := TPCOperationsStorage.PCOperationsStorage.Count; + LOne := Add(1); + LClone := TOperationsHashTree.Create; + try + LClone.CopyFromHashTree(FPool.OperationsHashTree); + FPool.SanitizeOperations; + AssertSequence([LOne]); + Check(TPCOperationsStorage.PCOperationsStorage.Count=LCount+1,'Replay retains one registered operation'); + FPool.Clear(True); + Check(LClone.GetOperation(0)=LOne,'External tree survives sanitizer and pool release'); + Check(LClone.IndexOfOperation(LOne)=0,'External tree lookup remains valid'); + finally LClone.Free; end; + Check(TPCOperationsStorage.PCOperationsStorage.Count=LCount,'Final owner releases registry entry'); + LOne := Add(1); + LClone := TOperationsHashTree.Create; + try LClone.CopyFromHashTree(FPool.OperationsHashTree); + finally LClone.Free; end; + FPool.SanitizeOperations; + AssertSequence([LOne]); + Log('PASS external operation owners survive either release order without registry growth'); +end; + +procedure TMempoolSanitizeChecks.CheckRecoverRejection; +var LOne,LRecover: TPCOperation; +begin + Reset; + LOne := Add(1); + LRecover := TOpRecoverFounds.Create(CT_PROTOCOL_5,0,2,1,FKey.PublicKey); + try Check(FPool.OperationsHashTree.AddOperationToHashTree(LRecover),'Recover input inserted'); + finally LRecover.Free; end; + Check(FPool.OperationsHashTree.HasOpRecoverOperations,'Input recover flag set'); + FPool.SanitizeOperations; + AssertSequence([LOne]); + Check(not FPool.OperationsHashTree.HasOpRecoverOperations,'Rejected recover does not leak metadata'); + Reset; + FPool.SanitizeOperations; + AssertSequence([]); + Log('PASS rejected recover metadata and empty replay'); +end; + +procedure TMempoolSanitizeChecks.CheckReferenceBoundaries; +var I: Integer; LOriginal: TOperationsHashTree; LOperation: TReferenceBoundaryTransaction; + LAccepted: array[0..4] of TPCOperation; LExpected: TOperationsHashTree; +begin + Reset; + Check(FPool.OperationsHashTree.IndexOfOpReference(High(UInt64))=-1,'Empty UInt64 lookup'); + for I := 1 to 5 do begin + LOperation := TReferenceBoundaryTransaction.CreateTransaction(CT_PROTOCOL_5,0,I,1, + FKey,1,1,CT_TOperationPayload_NUL); + try + LOperation.ForceLookupCollision; + Check(FPool.OperationsHashTree.AddOperationToHashTree(LOperation),'Mixed-half reference insertion'); + LAccepted[I-1] := FPool.Operation[I-1]; + finally LOperation.Free; end; + end; + Add(99); // Force the bulk materialization path after a real nonce rejection. + LOriginal := FPool.OperationsHashTree; + FPool.SanitizeOperations; + Check(FPool.OperationsHashTree<>LOriginal,'Rejected input materializes the bulk indexes'); + Check(FPool.Count=5,'All five inherited transactions replayed exactly'); + Check(FPool.OperationsHashTree.IndexOfOpReference(0)=0,'Duplicate zero-reference keeps first operation'); + Check(FPool.OperationsHashTree.IndexOfOpReference(UInt64(1) shl 63)=1,'High-bit boundary lookup'); + Check(FPool.OperationsHashTree.IndexOfOpReference(High(UInt64))=2,'Maximum UInt64 lookup'); + Check(FPool.OperationsHashTree.IndexOfOpReference(High(Cardinal))=3,'Maximum low-half lookup'); + Check(FPool.OperationsHashTree.IndexOfOpReference((UInt64(1) shl 63)+1)=-1,'Absent mixed-half reference'); + LExpected := TOperationsHashTree.Create; + try + for I := 0 to 4 do begin + Check(FPool.Operation[I]=LAccepted[I],'Bulk sorting never reorders the operations'); + Check(FPool.OperationsHashTree.IndexOfOperation(LAccepted[I])=0,'SHA collision keeps first operation'); + Check(LExpected.AddOperationToHashTree(LAccepted[I]),'Expected boundary sequence'); + end; + Check(TBaseType.Equals(LExpected.HashTree,FPool.OperationsHashTree.HashTree),'Bulk ordered hash retained'); + Check(FPool.SafeBoxTransaction.Account(0).n_operation=5,'Inherited replay advanced the actual nonce five times'); + finally LExpected.Free; end; + Log('PASS UInt64 zero/high-bit/max boundaries and first duplicate SHA/reference representatives'); +end; + +procedure TMempoolSanitizeChecks.CheckProtocolSignatureCaches; +var LSource,LCandidate: TPCOperation; + LSourceTree,LTargetTree: TOperationsHashTree; + LTransaction: TPCSafeBoxTransaction; LPrevious: TAccountPreviousBlockInfo; + LStream: TMemoryStream; LError,LPreviousDirectory,LDirectory,LTemporaryRoot: String; + LNode: TNode; LPool,LTarget: TPCOperationsComp; LHeader: TOperationBlock; LID: TGUID; + + function UnverifiedClone(AProtocol: Word): TPCOperation; + begin + Result := TOpTransaction.Create(AProtocol); + try + LStream.Clear; + Check(LSource.SaveToNettransfer(LStream),'Source wire serialization'); + LStream.Position := 0; + Check(Result.LoadFromNettransfer(LStream),'Cross-version identical wire loading'); + Check(TBaseType.Equals(LSource.Sha256,Result.Sha256),'Counterexample has the same operation wire hash'); + Check(not Result.HasValidSignature,'Loaded clone starts unverified'); + except Result.Free; raise; end; + end; + +begin + Reset; + LSource := TOpTransaction.CreateTransaction(CT_PROTOCOL_3,0,1,1,FKey,1,1,CT_TOperationPayload_NUL); + LSourceTree := TOperationsHashTree.Create; + LTargetTree := TOperationsHashTree.Create; + LStream := TMemoryStream.Create; + try + LTransaction := TPCSafeBoxTransaction.Create(FBank.SafeBox); + LPrevious := TAccountPreviousBlockInfo.Create; + try + Check(LSource.DoOperation(LPrevious,LTransaction,LError),'Source signature verifies through actual execution: '+LError); + Check(LSource.HasValidSignature,'Source verified cache is populated'); + finally LPrevious.Free; LTransaction.Free; end; + Check(LSourceTree.AddOperationToHashTree(LSource),'Verified source registration'); + LCandidate := UnverifiedClone(CT_PROTOCOL_4); + try + Check(not TBaseType.Equals(LSource.GetDigestToSign,LCandidate.GetDigestToSign), + 'Identical wire/hash has different actual protocol signing digest'); + Check(LTargetTree.AddOperationToHashTree(LCandidate),'Cross-version target registration'); + finally LCandidate.Free; end; + LSourceTree.MarkVerifiedECDSASignatures(LTargetTree); + Check(not LTargetTree.GetOperation(0).HasValidSignature,'Tree must not copy cache across operation protocols'); + LTransaction := TPCSafeBoxTransaction.Create(FBank.SafeBox); + LPrevious := TAccountPreviousBlockInfo.Create; + try Check(not LTargetTree.GetOperation(0).DoOperation(LPrevious,LTransaction,LError), + 'Original signature is actually invalid under the different protocol digest'); + finally LPrevious.Free; LTransaction.Free; end; + LTargetTree.ClearHastThree; + LCandidate := UnverifiedClone(CT_PROTOCOL_3); + try Check(LTargetTree.AddOperationToHashTree(LCandidate),'Same-protocol target registration'); + finally LCandidate.Free; end; + LSourceTree.MarkVerifiedECDSASignatures(LTargetTree); + Check(LTargetTree.GetOperation(0).HasValidSignature,'Same-protocol tree cache reuse remains enabled'); + + Check(not TNode.NodeExists,'Node cache check requires an isolated node lifetime'); + LTemporaryRoot := GetEnvironmentVariable('TEMP'); + Check(LTemporaryRoot<>'','TEMP must identify the owned cache-check directory'); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(LTemporaryRoot)+'SignatureCache-'+GUIDToString(LID); + ForceDirectories(LDirectory); + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(LDirectory); + try + LNode := TNode.Create(nil); + try + LNode.BroadcastData := False; + Check(not LNode.NetServer.Active,'Signature cache check never listens'); + LPool := LNode.LockMempoolWrite; + try + LHeader := LPool.OperationBlock; + LHeader.protocol_version := CT_PROTOCOL_3; + LPool.SetOperationBlock(LHeader); + Check(LPool.OperationsHashTree.AddOperationToHashTree(LSourceTree.GetOperation(0)), + 'Actual node verified source registration'); + finally LNode.UnlockMempoolWrite; end; + LTarget := TPCOperationsComp.Create(nil); + try + LCandidate := UnverifiedClone(CT_PROTOCOL_3); + try Check(LTarget.OperationsHashTree.AddOperationToHashTree(LCandidate),'Actual node target registration'); + finally LCandidate.Free; end; + LHeader := LTarget.OperationBlock; + LHeader.protocol_version := CT_PROTOCOL_4; + LTarget.SetOperationBlock(LHeader); + LNode.MarkVerifiedECDSASignaturesFromMemPool(LTarget); + Check(not LTarget.Operation[0].HasValidSignature,'Node rejects differing block protocols even when operations match'); + LHeader.protocol_version := CT_PROTOCOL_3; + LTarget.SetOperationBlock(LHeader); + LNode.MarkVerifiedECDSASignaturesFromMemPool(LTarget); + Check(LTarget.Operation[0].HasValidSignature,'Node same-block/same-operation protocol cache reuse remains enabled'); + LTarget.Clear(True); + LCandidate := UnverifiedClone(CT_PROTOCOL_4); + try Check(LTarget.OperationsHashTree.AddOperationToHashTree(LCandidate),'Mixed-operation node target'); + finally LCandidate.Free; end; + LHeader := LTarget.OperationBlock; + LHeader.protocol_version := CT_PROTOCOL_3; + LTarget.SetOperationBlock(LHeader); + LNode.MarkVerifiedECDSASignaturesFromMemPool(LTarget); + Check(not LTarget.Operation[0].HasValidSignature,'Node also retains the per-operation protocol guard'); + finally LTarget.Free; end; + finally LNode.Free; end; + finally TNode.SetPascalCoinDataFolder(LPreviousDirectory); end; + Log('PASS same-wire/different-digest cache rejection and same-protocol positives through tree and actual Node'); + finally LStream.Free; LTargetTree.Free; LSourceTree.Free; LSource.Free; end; +end; + +procedure TMempoolSanitizeChecks.Run; +var LCheck: TMempoolSanitizeCheck; +begin + for LCheck := Low(TMempoolSanitizeCheck) to High(TMempoolSanitizeCheck) do + RunOne(LCheck); + Log('PASS all nine sanitizer/signature-cache groups; no peers or wallet data'); +end; + +procedure TMempoolSanitizeChecks.RunOne(ACheck: TMempoolSanitizeCheck); +var LCount: Integer; +begin + Reset; + LCount := TPCOperationsStorage.PCOperationsStorage.Count; + case ACheck of + mscAllValid: CheckAllValid; + mscRejectedAndDuplicates: CheckRejectedAndDuplicates; + mscDeletedDuplicateIndex: CheckDeletedDuplicateIndex; + mscZeroFeeSignerLimit: CheckZeroFeeSignerLimit; + mscReplayException: CheckReplayException; + mscRegistryOwnership: CheckRegistryOwnership; + mscRecoverRejection: CheckRecoverRejection; + mscReferenceBoundaries: CheckReferenceBoundaries; + mscProtocolSignatureCaches: CheckProtocolSignatureCaches; + end; + Reset; + Check(TPCOperationsStorage.PCOperationsStorage.Count=LCount,'All fixture registry references released'); +end; + +function RunMempoolSanitizeCheck(ACheck: TMempoolSanitizeCheck; + const ALogPath: String): Integer; +var LChecks: TMempoolSanitizeChecks; +begin + Result := 1; + LChecks := TMempoolSanitizeChecks.Create(ALogPath); + try + try + LChecks.RunOne(ACheck); + Result := 0; + except + on E: Exception do begin + LChecks.Log('FAIL ' + E.ClassName + ': ' + E.Message); + raise; + end; + end; + finally LChecks.Free; end; +end; + +function RunMempoolSanitizeChecks(const ALogPath: String): Integer; +var LChecks: TMempoolSanitizeChecks; +begin + Result := 1; + LChecks := TMempoolSanitizeChecks.Create(ALogPath); + try + try + LChecks.Run; + Result := 0; + except + on E: Exception do begin + LChecks.Log('FAIL ' + E.ClassName + ': ' + E.Message); + raise; + end; + end; + finally LChecks.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/block-throughput/UPendingCatchupChecks.pas b/src/tests/experimental/native/block-throughput/UPendingCatchupChecks.pas new file mode 100644 index 000000000..03d512d84 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/UPendingCatchupChecks.pas @@ -0,0 +1,870 @@ +unit UPendingCatchupChecks; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Real pending validation, bank subscribers and the owned maintenance worker. + Generated commits below deliberately do not assert proof-of-work acceptance; + the separate public five-block fixture exercises that boundary. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type + TPendingCatchupCheck = (pccBufferedCommits, pccInterBatchTarget, + pccMixedRejections, pccUnavailableContracts, pccNewWorkDuringReplay, + pccRetiredScopes, pccShutdownPackage, pccEmptyScope, + pccReplayFailureRetry, pccReplayFailureShutdown); + +function RunPendingCatchupCheck(ACheck: TPendingCatchupCheck; + const ALogPath: String): Integer; +function RunPendingCatchupChecks(const ALogPath: String): Integer; + +implementation + +uses {$IFDEF FPC}Windows,{$ELSE}Winapi.Windows,{$ENDIF} + Classes, SysUtils, SyncObjs, UNode, UAccounts, UBlockChain, UFileStorage, + UOpTransaction, UThread, UCrypto, UConst, UBaseTypes, UPCDataTypes, UNetProtocol; + +type + TCatchupSafeBox = class(TPCSafeBox) + procedure Build(const AKey: TAccountKey); + end; + + TCatchupFileStorage = class(TFileStorage) + protected + function DoLoadBlockChain(AOperations: TPCOperationsComp; ABlock: Cardinal): Boolean; override; + function GetFirstBlockNumber: Int64; override; + function GetLastBlockNumber: Int64; override; + end; + + TReplayObservation = class + public + Transaction: TPCSafeBoxTransaction; + Attempts, HoldNext, ThrowOnAttempt: LongInt; + Entered, ReleaseReplay, ThrowReached: TEvent; + constructor Create; + destructor Destroy; override; + procedure Reset; + function Count: Integer; + end; + + TCountingTransaction = class(TOpTransaction) + function DoOperation(APrevious: TAccountPreviousBlockInfo; + ATransaction: TPCSafeBoxTransaction; var AErrors: String): Boolean; override; + end; + + TCatchupScopeThread = class(TPCThread) + private + FNode: TNode; + FPeer: TObject; + protected + procedure BCExecute; override; + public + Entered, ReleaseScope, Completed: TEvent; + StaleTargetAtExit: Boolean; + RequireUnavailableRead, ReadWasUnavailable: Boolean; + ErrorMessage: String; + constructor Create(ANode: TNode; APeer: TObject); + destructor Destroy; override; + end; + + TPendingCatchupChecks = class + private + FNode: TNode; + FKey: TECPrivateKey; + FPeer, FSecondPeer: TObject; + FNotify: TNodeNotifyEvents; + FObservation: TReplayObservation; + FSeeds: TOperationsHashTree; + FTemplateHeader: TOperationBlock; + FComplete: TEvent; + FLog: TStringList; + FPath, FDirectory, FPreviousDirectory: String; + FStopping, FDirectorySet: Boolean; + FAvailableCallbacks: Integer; + FCallbackThread: UInt64; + procedure Check(AValue: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure SynchronizationChanged(Sender: TObject; const AStatus: TNodeSynchronizationStatus); + procedure Pump(AMilliseconds: Cardinal); + procedure WaitAvailable(ATimeout: Cardinal = 3000); + procedure Seed; + procedure StartSession(ATargetOffset: Cardinal); + procedure CommitGenerated(AConfirmFirst: Boolean = False); + procedure CheckUnavailable; + procedure CheckPool(ACount: Integer); + function PendingBytes: TMemoryStream; + function OracleBytes(AExpectedCount: Integer = 2): TMemoryStream; + procedure CheckRetainedPackage(AExpected: TMemoryStream); + procedure CheckFinalAccounts(AExpectedCount: Integer); + procedure BufferedCommits; + procedure InterBatchTarget; + procedure MixedRejections; + procedure UnavailableContracts; + procedure NewWorkDuringReplay; + procedure RetiredScopes; + procedure ShutdownPackage; + procedure EmptyScope; + procedure ReplayFailureRetry; + procedure ReplayFailureShutdown; + public + constructor Create(const APath: String); + destructor Destroy; override; + procedure Run(ACheck: TPendingCatchupCheck); + end; + +var GReplayObservation: TReplayObservation; + +constructor TReplayObservation.Create; +begin + inherited Create; + Entered := TEvent.Create(nil,True,False,''); + ReleaseReplay := TEvent.Create(nil,True,False,''); + ThrowReached := TEvent.Create(nil,True,False,''); +end; + +destructor TReplayObservation.Destroy; +begin + ThrowReached.Free; + ReleaseReplay.Free; + Entered.Free; + inherited; +end; + +procedure TReplayObservation.Reset; +begin + InterlockedExchange(Attempts,0); +end; + +function TReplayObservation.Count: Integer; +begin + Result := InterlockedCompareExchange(Attempts,0,0); +end; + +function TCountingTransaction.DoOperation(APrevious: TAccountPreviousBlockInfo; + ATransaction: TPCSafeBoxTransaction; var AErrors: String): Boolean; +var LAttempt: LongInt; +begin + // Count the inherited validator only for the real node's pending overlay. + // An independent confirmed transaction/oracle must not inflate this count. + if Assigned(GReplayObservation) and + (ATransaction=GReplayObservation.Transaction) then begin + LAttempt := InterlockedIncrement(GReplayObservation.Attempts); + if InterlockedCompareExchange(GReplayObservation.HoldNext,0,1)=1 then begin + GReplayObservation.Entered.SetEvent; + if GReplayObservation.ReleaseReplay.WaitFor(5000)<>wrSignaled then + raise Exception.Create('Pending replay fixture gate timed out'); + end; + if InterlockedCompareExchange(GReplayObservation.ThrowOnAttempt,0,LAttempt)=LAttempt then begin + GReplayObservation.ThrowReached.SetEvent; + raise Exception.Create('Owned one-shot pending replay failure after an accepted prefix'); + end; + end; + Result := inherited DoOperation(APrevious,ATransaction,AErrors); +end; + +constructor TCatchupScopeThread.Create(ANode: TNode; APeer: TObject); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FPeer := APeer; + Entered := TEvent.Create(nil,True,False,''); + ReleaseScope := TEvent.Create(nil,True,False,''); + Completed := TEvent.Create(nil,True,False,''); +end; + +destructor TCatchupScopeThread.Destroy; +begin + Terminate; + ReleaseScope.SetEvent; + if Suspended then Start; + WaitFor; + Completed.Free; + ReleaseScope.Free; + Entered.Free; + inherited; +end; + +procedure TCatchupScopeThread.BCExecute; +var LPool: TPCOperationsComp; +begin + try + try + if Terminated then Exit; + if RequireUnavailableRead then begin + try + LPool := FNode.LockMempoolRead; + FNode.UnlockMempoolRead; + except on EPendingOperationsUnavailable do ReadWasUnavailable:=True; end; + if not ReadWasUnavailable then + raise Exception.Create('A busy reader obtained the pending account overlay'); + end; + FNode.BeginBlockChainUpdate; + try + Entered.SetEvent; + if ReleaseScope.WaitFor(5000)<>wrSignaled then + raise Exception.Create('Pending physical-scope fixture timed out'); + if StaleTargetAtExit then FNode.SetSynchronizationTarget(FPeer,999999); + finally FNode.EndBlockChainUpdate; end; + except on E: Exception do ErrorMessage := E.ClassName+': '+E.Message; end; + finally Completed.SetEvent; end; +end; + +procedure TCatchupSafeBox.Build(const AKey: TAccountKey); +var I: Integer; LHeader: TOperationBlock; +begin + for I := 0 to 159 do begin + LHeader := CT_OperationBlock_NUL; + LHeader.block := I; + LHeader.account_key := AKey; + LHeader.protocol_version := CT_PROTOCOL_1; + LHeader.protocol_available := CT_PROTOCOL_6; + LHeader.compact_target := CT_MinCompactTarget_v1; + LHeader.timestamp := I*CT_NewLineSecondsAvg; + // Use the normal mining reward so the first account of each generated + // block is funded without creating an unbalanced transaction fee delta. + LHeader.reward := TPascalCoinProtocol.GetRewardForNewLine(I); + AddNew(LHeader); + end; + DoUpgradeToProtocol5; +end; + +function TCatchupFileStorage.DoLoadBlockChain(AOperations: TPCOperationsComp; + ABlock: Cardinal): Boolean; +begin + if ABlock=Bank.BlocksCount-1 then begin + AOperations.Clear(True); + AOperations.SetOperationBlock(Bank.SafeBox.GetBlockInfo(ABlock)); + Result := True; + end else Result := inherited DoLoadBlockChain(AOperations,ABlock); +end; + +function TCatchupFileStorage.GetFirstBlockNumber: Int64; +begin + Result := 0; +end; + +function TCatchupFileStorage.GetLastBlockNumber: Int64; +begin + Result := Int64(Bank.BlocksCount)-1; +end; + +constructor TPendingCatchupChecks.Create(const APath: String); +var LID: TGUID; LPool: TPCOperationsComp; +begin + inherited Create; + FPath := APath; + FLog := TStringList.Create; + Check(not TNode.NodeExists,'Each catch-up scenario requires an owned fresh node'); + Check(GetEnvironmentVariable('TEMP')<>'','TEMP must name an owned test directory'); + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP'))+ + 'PendingCatchup-'+GUIDToString(LID); + ForceDirectories(FDirectory); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + FDirectorySet := True; + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FPeer := TObject.Create; + FSecondPeer := TObject.Create; + FComplete := TEvent.Create(nil,True,False,''); + FObservation := TReplayObservation.Create; + GReplayObservation := FObservation; + FSeeds := TOperationsHashTree.Create; + FNode := TNode.Create(nil); + FNode.BroadcastData := False; + TNetData.NetData.NetConnectionsActive := False; + FNode.SetSynchronizationNetworkActive(False); + Check(not FNode.NetServer.Active,'The catch-up fixture must not listen'); + FNode.Bank.StorageClass := TCatchupFileStorage; + Check(FNode.Bank.Storage.Initialize,'Generated fixture storage initialization'); + TCatchupSafeBox(FNode.Bank.SafeBox).Build(FKey.PublicKey); + FNode.Bank.UpdateValuesFromSafebox; + LPool := FNode.LockMempoolRead; + try + FObservation.Transaction := LPool.SafeBoxTransaction; + FTemplateHeader := LPool.OperationBlock; + finally FNode.UnlockMempoolRead; end; + FNotify := TNodeNotifyEvents.Create(nil); + FNotify.OnSynchronizationChanged := SynchronizationChanged; + FNotify.Node := FNode; + Pump(100); + FComplete.ResetEvent; + FAvailableCallbacks := 0; + Log('READY generated bank; owned data='+FDirectory); +end; + +destructor TPendingCatchupChecks.Destroy; +begin + FStopping := True; + if Assigned(FObservation) then FObservation.ReleaseReplay.SetEvent; + if Assigned(FNotify) then FNotify.Node := nil; + FNotify.Free; + FNode.Free; + FSeeds.Free; + GReplayObservation := nil; + FObservation.Free; + FComplete.Free; + FSecondPeer.Free; + FPeer.Free; + FKey.Free; + if FDirectorySet then TNode.SetPascalCoinDataFolder(FPreviousDirectory); + FLog.Free; + inherited; +end; + +procedure TPendingCatchupChecks.Check(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure TPendingCatchupChecks.Log(const AMessage: String); +begin + FLog.Add(FormatDateTime('hh:nn:ss.zzz',Now)+' '+AMessage); + FLog.SaveToFile(FPath); +end; + +procedure TPendingCatchupChecks.SynchronizationChanged(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); +begin + if FStopping then Exit; + FCallbackThread := TThread.CurrentThread.ThreadID; + if AStatus.PendingOperationsAvailable then begin + Inc(FAvailableCallbacks); + FComplete.SetEvent; + end; +end; + +procedure TPendingCatchupChecks.Pump(AMilliseconds: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64+AMilliseconds; + repeat CheckSynchronize(0); Sleep(1); until GetTickCount64>=LUntil; +end; + +procedure TPendingCatchupChecks.WaitAvailable(ATimeout: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64+ATimeout; + repeat + CheckSynchronize(0); + if FComplete.WaitFor(0)=wrSignaled then begin + Check(FNode.PendingOperationsAvailable,'Completion event must publish available pending data'); + Check(FNode.GetSynchronizationStatus.PendingOperationsAvailable,'Cached status must agree with availability'); + Check(FCallbackThread=MainThreadID,'Typed completion must use the normal UI notifier'); + Exit; + end; + Sleep(1); + until GetTickCount64>=LUntil; + raise Exception.Create('No event-driven pending completion within the owned deadline'); +end; + +procedure TPendingCatchupChecks.Seed; +var LOp: TPCOperation; LError: String; I: Integer; LSigner,LNonce: Cardinal; + LAdded: Boolean; +begin + for I := 0 to 2 do begin + if I<2 then begin + LSigner:=0; + LNonce:=I+1; + end + else begin + LSigner:=5; + LNonce:=1; + end; + LOp := TCountingTransaction.CreateTransaction(CT_PROTOCOL_5,LSigner,LNonce,1, + FKey,1,1,CT_TOperationPayload_NUL); + try + Check(FSeeds.AddOperationToHashTree(LOp),'Register the real signed seed'); + LAdded := FNode.AddOperation(nil,FSeeds.GetOperation(I),LError); + Check(LAdded,'Real pending admission '+IntToStr(I)+': '+LError); + finally LOp.Free; end; + end; + CheckPool(3); + FObservation.Reset; +end; + +procedure TPendingCatchupChecks.StartSession(ATargetOffset: Cardinal); +begin + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + FNode.SetSynchronizationTarget(FPeer,FNode.Bank.BlocksCount+ATargetOffset); + Pump(20); + FComplete.ResetEvent; + FAvailableCallbacks := 0; +end; + +procedure TPendingCatchupChecks.CommitGenerated(AConfirmFirst: Boolean); +var LTransaction: TPCSafeBoxTransaction; LPrevious: TAccountPreviousBlockInfo; + LSelected: TOperationsHashTree; LHeader: TOperationBlock; LError: String; +begin + Check(FNode.TryLockNode(3000),'Confirmed bank write admission'); + try + LTransaction := TPCSafeBoxTransaction.Create(FNode.Bank.SafeBox); + LPrevious := TAccountPreviousBlockInfo.Create; + LSelected := TOperationsHashTree.Create; + try + if AConfirmFirst then begin + Check(FSeeds.GetOperation(0).DoOperation(LPrevious,LTransaction,LError), + 'Actual confirmed transaction execution: '+LError); + Check(LSelected.AddOperationToHashTree(FSeeds.GetOperation(0)),'Register actual confirmed operation'); + end; + LHeader := FTemplateHeader; + LHeader.block := FNode.Bank.BlocksCount; + LHeader.account_key := FKey.PublicKey; + LHeader.timestamp := FNode.Bank.LastOperationBlock.timestamp+1; + LHeader.initial_safe_box_hash := FNode.Bank.SafeBox.SafeBoxHash; + LHeader.previous_proof_of_work := FNode.Bank.LastOperationBlock.proof_of_work; + LHeader.fee := LTransaction.TotalFee; + LHeader.operations_hash := LSelected.HashTree; + SetLength(LHeader.proof_of_work,32); + FillChar(LHeader.proof_of_work[0],32,0); + Check(LTransaction.Commit(LHeader,LError),'Generated confirmed commit: '+LError); + // This invokes the real node bank subscriber. No sanitizer/status event + // is called by the fixture, and ordinary block validation is not replaced. + FNode.Bank.UpdateValuesFromSafebox; + finally LSelected.Free; LPrevious.Free; LTransaction.Free; end; + finally FNode.UnlockNode; end; +end; + +procedure TPendingCatchupChecks.CheckUnavailable; +var LStatus: TNodeSynchronizationStatus; +begin + LStatus := FNode.GetSynchronizationStatus; + Check(not FNode.PendingOperationsAvailable,'Pending overlay must remain unavailable'); + Check(not LStatus.PendingOperationsAvailable,'Typed status must remain unavailable'); + Check(LStatus.State=nssSynchronizing,'Unavailable overlay is synchronization work'); +end; + +procedure TPendingCatchupChecks.CheckPool(ACount: Integer); +var LPool: TPCOperationsComp; +begin + LPool := FNode.LockMempoolRead; + try + Check(LPool.Count=ACount,'Exact pending operation count'); + Check(LPool.OperationBlock.block=FNode.Bank.BlocksCount,'Pending header uses the current confirmed tip'); + Check(LPool.SafeBoxTransaction.CheckIntegrity,'Pending overlay integrity matches the current confirmed base'); + finally FNode.UnlockMempoolRead; end; +end; + +function TPendingCatchupChecks.PendingBytes: TMemoryStream; +var LPool: TPCOperationsComp; +begin + Result := TMemoryStream.Create; + try + LPool := FNode.LockMempoolRead; + try Check(LPool.OperationsHashTree.SaveOperationsHashTreeToStream(Result,True),'Serialize coherent pending package'); + finally FNode.UnlockMempoolRead; end; + except Result.Free; raise; end; +end; + +function TPendingCatchupChecks.OracleBytes(AExpectedCount: Integer): TMemoryStream; +var LPool: TPCOperationsComp; LError: String; +begin + Result := TMemoryStream.Create; + try + LPool := TPCOperationsComp.Create(FNode.Bank); + try + Check(LPool.AddOperations(FSeeds,LError)=AExpectedCount,'Independent normal insertion accepted count'); + Check(LPool.OperationsHashTree.SaveOperationsHashTreeToStream(Result,True),'Serialize normal-insertion oracle'); + finally LPool.Free; end; + except Result.Free; raise; end; +end; + +procedure TPendingCatchupChecks.CheckRetainedPackage(AExpected: TMemoryStream); +var LPool: TPCOperationsComp; LActual: TMemoryStream; I: Integer; +begin + Check(FNode.TryLockMempoolOperationsRead(3000,LPool),'Failed replay must release its writer'); + try + Check(LPool.Count=FSeeds.OperationsCount,'A failed replay retains every original pending operation'); + for I := 0 to FSeeds.OperationsCount-1 do begin + Check(LPool.Operation[I]=FSeeds.GetOperation(I),'A failed replay retains original registered order'); + Check(LPool.OperationsHashTree.IndexOfOperation(FSeeds.GetOperation(I))=I,'A failed replay preserves its SHA index'); + Check(LPool.OperationsHashTree.IndexOfOpReference(FSeeds.GetOperation(I).GetOpReference)=I, + 'A failed replay preserves its reference index'); + end; + LActual := TMemoryStream.Create; + try + Check(LPool.OperationsHashTree.SaveOperationsHashTreeToStream(LActual,True),'Serialize complete retained input'); + Check((AExpected.Size=LActual.Size) and CompareMem(AExpected.Memory,LActual.Memory,AExpected.Size), + 'A failed replay must not publish the accepted prefix as a smaller pending package'); + finally LActual.Free; end; + finally FNode.UnlockMempoolRead; end; +end; + +procedure TPendingCatchupChecks.CheckFinalAccounts(AExpectedCount: Integer); +var LPool,LOracle: TPCOperationsComp; LError: String; I: Integer; +const CAccounts: array[0..2] of Cardinal = (0,1,5); +begin + LOracle := TPCOperationsComp.Create(FNode.Bank); + try + Check(LOracle.AddOperations(FSeeds,LError)=AExpectedCount,'Independent complete account replay count'); + LPool := FNode.LockMempoolRead; + try + for I := Low(CAccounts) to High(CAccounts) do + Check(TAccountComp.EqualAccounts(LOracle.SafeBoxTransaction.Account(CAccounts[I]), + LPool.SafeBoxTransaction.Account(CAccounts[I])),'Final account, nonce and seal match independent normal replay'); + Check(LOracle.SafeBoxTransaction.TotalBalance=LPool.SafeBoxTransaction.TotalBalance,'Final aggregate balance'); + Check(LOracle.SafeBoxTransaction.TotalFee=LPool.SafeBoxTransaction.TotalFee,'Final aggregate fee'); + finally FNode.UnlockMempoolRead; end; + finally LOracle.Free; end; +end; + +procedure TPendingCatchupChecks.BufferedCommits; +var I: Integer; LInitial: Cardinal; +begin + Seed; + LInitial := FNode.Bank.BlocksCount; + StartSession(3); + FNode.BeginBlockChainUpdate; + try + for I := 1 to 3 do begin + CommitGenerated; + CheckUnavailable; + Check(FNode.GetSynchronizationStatus.LocalBlockCount=LInitial+Cardinal(I), + 'Confirmed counts must advance while pending replay is deferred'); + Check(FObservation.Count=0,'A received block must not replay pending operations'); + end; + finally FNode.EndBlockChainUpdate; end; + WaitAvailable; + Check(FObservation.Count=3,'The final worker must replay the three input operations once'); + CheckPool(3); +end; + +procedure TPendingCatchupChecks.InterBatchTarget; +begin + Seed; + StartSession(2); + FNode.BeginBlockChainUpdate; + try CommitGenerated; finally FNode.EndBlockChainUpdate; end; + Pump(200); + CheckUnavailable; + Check(FObservation.Count=0,'A target ahead must defer replay across a silent inter-batch gap'); + Check(FNode.GetSynchronizationStatus.ActiveTransfers=0,'This fixture really exercises an empty physical gap'); + FNode.BeginBlockChainUpdate; + try CommitGenerated; finally FNode.EndBlockChainUpdate; end; + WaitAvailable; + Check(FObservation.Count=3,'Reaching the selected target admits one final replay'); + CheckPool(3); +end; + +procedure TPendingCatchupChecks.MixedRejections; +var LExpected,LActual: TMemoryStream; LPool: TPCOperationsComp; +begin + Seed; + StartSession(2); + FNode.BeginBlockChainUpdate; + try + CommitGenerated(True); + CommitGenerated; + Check(FObservation.Count=0,'Confirmed execution must not be counted as pending replay'); + finally FNode.EndBlockChainUpdate; end; + WaitAvailable; + Check(FObservation.Count=3,'One final full input pass includes the rejected first nonce'); + CheckPool(2); + CheckFinalAccounts(2); + LExpected := OracleBytes; + LActual := PendingBytes; + try + Check((LExpected.Size=LActual.Size) and CompareMem(LExpected.Memory,LActual.Memory,LExpected.Size), + 'Final accepted order/hash/fees/storage bytes equal independent normal insertion'); + finally LActual.Free; LExpected.Free; end; + LPool := FNode.LockMempoolRead; + try + Check(LPool.Operation[0]=FSeeds.GetOperation(1),'The second nonce remains first after confirmation'); + Check(LPool.Operation[1]=FSeeds.GetOperation(2),'The independent signer remains second'); + Check(LPool.OperationsHashTree.IndexOfOperation(FSeeds.GetOperation(0))=-1,'Confirmed SHA is removed'); + Check(LPool.OperationsHashTree.IndexOfOperation(FSeeds.GetOperation(1))=0,'Remaining SHA index is exact'); + Check(LPool.OperationsHashTree.IndexOfOpReference(FSeeds.GetOperation(2).GetOpReference)=1,'Remaining reference index is exact'); + Check(LPool.SafeBoxTransaction.TotalFee=2,'Final pending fees exclude the confirmed operation'); + finally FNode.UnlockMempoolRead; end; +end; + +procedure TPendingCatchupChecks.UnavailableContracts; +var LPool: TPCOperationsComp; LOp: TPCOperation; LTree: TOperationsHashTree; + LError: String; I: Integer; LRaised: Boolean; +begin + Seed; + StartSession(1); + FNode.BeginBlockChainUpdate; + try + CommitGenerated; + CheckUnavailable; + LPool := nil; + Check(not FNode.TryLockMempoolRead(0,LPool),'TryRead must decline unavailable balances'); + Check(LPool=nil,'A failed TryRead must not leak the old overlay'); + for I := 0 to 2 do begin + LRaised := False; + try + case I of + 0: begin + LPool:=FNode.LockMempoolRead; + FNode.UnlockMempoolRead; + end; + 1: begin + LPool:=FNode.LockMempoolWrite; + FNode.UnlockMempoolWrite; + end; + 2: begin + LPool:=FNode.LockMempoolUpgradeableRead; + FNode.UnlockMempoolUpgradeableRead; + end; + end; + except on EPendingOperationsUnavailable do LRaised:=True; end; + Check(LRaised,'Each public pending scope must report typed unavailability'); + end; + Check(FNode.TryLockMempoolOperationsRead(0,LPool),'Raw operation metadata remains accessible without account deltas'); + try Check(LPool.Count=3,'Busy reads must not erase the retained operation sequence'); + finally FNode.UnlockMempoolRead; end; + LOp := TCountingTransaction.CreateTransaction(CT_PROTOCOL_5,0,3,1,FKey,1,1,CT_TOperationPayload_NUL); + LTree := TOperationsHashTree.Create; + try + Check(LTree.AddOperationToHashTree(LOp),'Register valid new operation for admission refusal'); + Check(FNode.AddOperations(nil,LTree,nil,LError)<0,'Batch admission must return explicit unavailable result'); + Check(LError<>'','Unavailable admission supplies a reason'); + Check(not FNode.AddOperation(nil,LOp,LError),'Single operation admission must also decline'); + Check(FObservation.Count=0,'Unavailable admission must not execute pending validation'); + finally LTree.Free; LOp.Free; end; + finally FNode.EndBlockChainUpdate; end; + WaitAvailable; + CheckPool(3); + LOp := TCountingTransaction.CreateTransaction(CT_PROTOCOL_5,0,3,1,FKey,1,1,CT_TOperationPayload_NUL); + try Check(FNode.AddOperation(nil,LOp,LError),'Normal admission resumes against the reconciled overlay: '+LError); + finally LOp.Free; end; + CheckPool(4); +end; + +procedure TPendingCatchupChecks.NewWorkDuringReplay; +var LNew: TCatchupScopeThread; LPool: TPCOperationsComp; +begin + Seed; + StartSession(1); + FNode.BeginBlockChainUpdate; + try CommitGenerated; finally + InterlockedExchange(FObservation.HoldNext,1); + FNode.EndBlockChainUpdate; + end; + Check(FObservation.Entered.WaitFor(3000)=wrSignaled,'The actual final maintenance replay must reach the gate'); + LNew := TCatchupScopeThread.Create(FNode,FPeer); + try + LNew.RequireUnavailableRead := True; + LNew.Start; + try + Check(LNew.Entered.WaitFor(1000)=wrSignaled,'A new physical producer must not wait for pending replay'); + Check(LNew.ReadWasUnavailable,'A busy reader must fail before waiting on the held replay writer'); + CheckUnavailable; + finally FObservation.ReleaseReplay.SetEvent; end; + // A raw read waits for that first complete replay, without exposing its + // account overlay while the new physical scope is still active. + Check(FNode.TryLockMempoolOperationsRead(3000,LPool),'The first complete replay releases its writer'); + FNode.UnlockMempoolRead; + Pump(100); + CheckUnavailable; + Check(FAvailableCallbacks=0,'A new physical scope prevents a premature completion callback'); + Check(FObservation.Count=3,'Only the first final replay has run'); + CommitGenerated; + Check(FObservation.Count=3,'New confirmed work is deferred again'); + LNew.ReleaseScope.SetEvent; + Check(LNew.Completed.WaitFor(3000)=wrSignaled,'The new physical scope must drain'); + Check(LNew.ErrorMessage='','The new producer must complete normally: '+LNew.ErrorMessage); + WaitAvailable; + Check(FObservation.Count=6,'The new confirmed base receives its own single final replay'); + CheckPool(3); + finally FObservation.ReleaseReplay.SetEvent; LNew.Free; end; +end; + +procedure TPendingCatchupChecks.RetiredScopes; +var LOld,LNew: TCatchupScopeThread; +begin + Seed; + StartSession(2); + LOld := TCatchupScopeThread.Create(FNode,FPeer); + LNew := nil; + try + LOld.StaleTargetAtExit := True; + LOld.Start; + Check(LOld.Entered.WaitFor(3000)=wrSignaled,'Old physical scope entered'); + CommitGenerated; + FNode.SetSynchronizationNetworkActive(False); + CheckUnavailable; + Check(FObservation.Count=0,'Network-off must retain the actual buffered scope without replaying'); + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FSecondPeer,True); + LNew := TCatchupScopeThread.Create(FNode,FSecondPeer); + LNew.Start; + Check(LNew.Entered.WaitFor(3000)=wrSignaled,'Reconnected physical scope entered'); + LOld.ReleaseScope.SetEvent; + Check(LOld.Completed.WaitFor(3000)=wrSignaled,'Old scope drained'); + Check(LOld.ErrorMessage='','Old producer completed: '+LOld.ErrorMessage); + Check(FNode.GetSynchronizationStatus.ActiveTransfers=1,'Old completion must not drain the new physical owner'); + Check(not FNode.GetSynchronizationStatus.HasRemoteTarget,'Stale producer cannot revive its selected target'); + CheckUnavailable; + Check(FObservation.Count=0,'An old completion cannot trigger a replay under new physical work'); + LNew.ReleaseScope.SetEvent; + Check(LNew.Completed.WaitFor(3000)=wrSignaled,'New scope drained'); + Check(LNew.ErrorMessage='','New producer completed: '+LNew.ErrorMessage); + WaitAvailable; + Check(FObservation.Count=3,'Both generations drain before one coherent replay'); + CheckPool(3); + finally LNew.Free; LOld.Free; end; +end; + +procedure TPendingCatchupChecks.ShutdownPackage; +var LBefore,LExpected,LActual: TMemoryStream; LMatchesBefore,LMatchesFinal: Boolean; +begin + Seed; + LBefore := PendingBytes; + LExpected := nil; + LActual := TMemoryStream.Create; + try + StartSession(10); + FNode.BeginBlockChainUpdate; + try CommitGenerated(True); finally FNode.EndBlockChainUpdate; end; + CheckUnavailable; + Check(FObservation.Count=0,'A selected target ahead must not replay before Quit'); + LExpected := OracleBytes; + FStopping := True; + FNotify.Node := nil; + FreeAndNil(FNode); + LActual.LoadFromFile(FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops'); + // Shutdown may publish the retained original sequence or a fully finished + // final reconciliation. Both restart safely; a partial package does not. + LMatchesBefore := (LBefore.Size=LActual.Size) and CompareMem(LBefore.Memory,LActual.Memory,LBefore.Size); + LMatchesFinal := (LExpected.Size=LActual.Size) and CompareMem(LExpected.Memory,LActual.Memory,LExpected.Size); + Check(LMatchesBefore or LMatchesFinal,'Quit must persist one exact complete pending package'); + finally LActual.Free; LExpected.Free; LBefore.Free; end; +end; + +procedure TPendingCatchupChecks.EmptyScope; +begin + FNode.BeginBlockChainUpdate; + try CheckUnavailable; finally FNode.EndBlockChainUpdate; end; + WaitAvailable; + Check(FObservation.Count=0,'An unchanged empty scope must not invent validation work'); + CheckPool(0); +end; + +procedure TPendingCatchupChecks.ReplayFailureRetry; +var LOriginal,LExpected,LActual: TMemoryStream; +begin + Seed; + LOriginal := PendingBytes; + LExpected := nil; + LActual := nil; + try + StartSession(1); + FNode.BeginBlockChainUpdate; + try CommitGenerated; finally + InterlockedExchange(FObservation.ThrowOnAttempt,2); + FNode.EndBlockChainUpdate; + end; + Check(FObservation.ThrowReached.WaitFor(3000)=wrSignaled,'The second real pending validator must inject the failure'); + CheckRetainedPackage(LOriginal); + Pump(200); + CheckUnavailable; + Check(FObservation.Count=2,'A failed pending replay must not retry in a hot loop'); + Check(FAvailableCallbacks=0,'A failed partial account overlay must never be published as available'); + // No new block/peer/refresh event follows the failure. The actual owned + // maintenance backoff must wake and retry its retained complete input. + WaitAvailable(15000); + Check(FObservation.Count=5,'The retry validates all three original operations after two failed-pass attempts'); + CheckPool(3); + CheckFinalAccounts(3); + LExpected := OracleBytes(3); + LActual := PendingBytes; + Check((LExpected.Size=LActual.Size) and CompareMem(LExpected.Memory,LActual.Memory,LExpected.Size), + 'Successful automatic retry publishes exact independently validated bytes'); + finally LActual.Free; LExpected.Free; LOriginal.Free; end; +end; + +procedure TPendingCatchupChecks.ReplayFailureShutdown; +var LOriginal,LActual: TMemoryStream; +begin + Seed; + LOriginal := PendingBytes; + LActual := TMemoryStream.Create; + try + StartSession(1); + FNode.BeginBlockChainUpdate; + try CommitGenerated; finally + InterlockedExchange(FObservation.ThrowOnAttempt,2); + FNode.EndBlockChainUpdate; + end; + Check(FObservation.ThrowReached.WaitFor(3000)=wrSignaled,'Injected pending failure must precede Quit'); + CheckRetainedPackage(LOriginal); + CheckUnavailable; + FStopping := True; + FNotify.Node := nil; + FreeAndNil(FNode); + LActual.LoadFromFile(FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops'); + Check((LOriginal.Size=LActual.Size) and CompareMem(LOriginal.Memory,LActual.Memory,LOriginal.Size), + 'Quit after a failed replay must persist every original pending operation'); + finally LActual.Free; LOriginal.Free; end; +end; + +procedure TPendingCatchupChecks.Run(ACheck: TPendingCatchupCheck); +begin + case ACheck of + pccBufferedCommits: BufferedCommits; + pccInterBatchTarget: InterBatchTarget; + pccMixedRejections: MixedRejections; + pccUnavailableContracts: UnavailableContracts; + pccNewWorkDuringReplay: NewWorkDuringReplay; + pccRetiredScopes: RetiredScopes; + pccShutdownPackage: ShutdownPackage; + pccEmptyScope: EmptyScope; + pccReplayFailureRetry: ReplayFailureRetry; + pccReplayFailureShutdown: ReplayFailureShutdown; + end; + Log('PASS catch-up scenario='+IntToStr(Ord(ACheck))+' actual pending validation attempts='+IntToStr(FObservation.Count)); +end; + +function RunPendingCatchupCheck(ACheck: TPendingCatchupCheck; + const ALogPath: String): Integer; +var LChecks: TPendingCatchupChecks; LLog: TStringList; +begin + Result := 1; + if FileExists(ALogPath) then raise Exception.Create('Refusing an existing catch-up scenario log'); + LChecks := nil; + try + try + LChecks := TPendingCatchupChecks.Create(ALogPath); + LChecks.Run(ACheck); + Result := 0; + except on E: Exception do begin + LLog := TStringList.Create; + try + if FileExists(ALogPath) then LLog.LoadFromFile(ALogPath); + LLog.Add('FAIL '+E.ClassName+': '+E.Message); + LLog.SaveToFile(ALogPath); + finally LLog.Free; end; + end; end; + finally LChecks.Free; end; +end; + +function RunPendingCatchupChecks(const ALogPath: String): Integer; +var LCheck: TPendingCatchupCheck; LLog: TStringList; +begin + Result := 1; + if FileExists(ALogPath) then raise Exception.Create('Refusing an existing catch-up summary'); + LLog := TStringList.Create; + try + for LCheck := Low(TPendingCatchupCheck) to High(TPendingCatchupCheck) do begin + if RunPendingCatchupCheck(LCheck,ALogPath+'.'+IntToStr(Ord(LCheck)))<>0 then begin + LLog.Add('FAIL scenario='+IntToStr(Ord(LCheck))+'; see its owned log'); + LLog.SaveToFile(ALogPath); + Exit; + end; + LLog.Add('PASS scenario='+IntToStr(Ord(LCheck))); + LLog.SaveToFile(ALogPath); + end; + LLog.Add('PASS pending catch-up: 10'); + LLog.SaveToFile(ALogPath); + Result := 0; + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/block-throughput/UPendingSaveChecks.pas b/src/tests/experimental/native/block-throughput/UPendingSaveChecks.pas new file mode 100644 index 000000000..b018beb57 --- /dev/null +++ b/src/tests/experimental/native/block-throughput/UPendingSaveChecks.pas @@ -0,0 +1,574 @@ +unit UPendingSaveChecks; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Actual Node.AddOperations, bank notification, saver thread and file storage. + Only the generated bank and observing storage/failure boundary are test data. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type + TPendingSaveCheck = (pscFinalDirty, pscRetryAfterFailure, pscSanitizeRemoval, + pscAtomicStorage, pscFinalPublicationFailure); + +function RunPendingSaveChecks(const ALogPath: String): Integer; +function RunPendingSaveCheck(ACheck: TPendingSaveCheck; const ALogPath: String): Integer; + +implementation + +uses Classes, SysUtils, SyncObjs, UAccounts, UBlockChain, UNode, UFileStorage, + UCrypto, UOpTransaction, UThread, UConst, ULog, UPCDataTypes, UBaseTypes; + +type + TSaveObservation = class + private + FLock: SyncObjs.TCriticalSection; + FLog: TStringList; + FPath: String; + FAttempts,FSuccesses,FLastCount,FFailuresRemaining,FLoggedFailures: Integer; + FHoldErrorLog: Boolean; + public + Succeeded,Failed,ErrorLogged,ReleaseErrorLog: TEvent; + constructor Create(const APath: String); + destructor Destroy; override; + procedure Log(const AText: String); + function BeginSave: Boolean; + procedure Saved(ACount: Integer); + procedure FailNextSave; + procedure ArmErrorLogGate; + procedure CoreLog(AType: TLogType; ATime: TDateTime; AThread: TThreadID; + const ASender,AText: String); + function LastCount: Integer; + function Attempts: Integer; + function LoggedFailures: Integer; + end; + + TObservedFileStorage = class(TFileStorage) + protected + procedure DoSavePendingBufferOperations(ATree: TOperationsHashTree); override; + function DoLoadBlockChain(AOperations: TPCOperationsComp; ABlock: Cardinal): Boolean; override; + function GetFirstBlockNumber: Int64; override; + function GetLastBlockNumber: Int64; override; + end; + + TPendingSaveSafeBox = class(TPCSafeBox) + procedure Build(const AKey: TAccountKey); + procedure ConfirmFirstOperation; + end; + + TSerializationFailureTransaction = class(TOpTransaction) + protected + function SaveOpToStream(AStream: TStream; AExtended: Boolean): Boolean; override; + end; + + TPendingSaveChecks = class + private + FNode: TNode; + FKey: TECPrivateKey; + FObservation: TSaveObservation; + FCoreLog: TLog; + FDirectory,FPreviousDirectory: String; + procedure Check(AValue: Boolean; const AText: String); + procedure AddOperation(ANonce: Cardinal); + function PendingBytes: TMemoryStream; + procedure StopNode; + procedure CheckStoredBytes(AExpected: TStream); + procedure FirstSave; + procedure FinalDirty; + procedure RetryAfterFailure; + procedure SanitizeRemoval; + procedure AtomicStorage; + procedure FinalPublicationFailure; + public + constructor Create(const ALogPath: String); + destructor Destroy; override; + procedure Run(ACheck: TPendingSaveCheck); + end; + +var GObservation: TSaveObservation; + GFailSerialization: Boolean; + +function TSerializationFailureTransaction.SaveOpToStream(AStream: TStream; + AExtended: Boolean): Boolean; +begin + if GFailSerialization then + raise EWriteError.Create('Injected serialization failure before temporary-file write'); + Result := inherited SaveOpToStream(AStream,AExtended); +end; + +constructor TSaveObservation.Create(const APath: String); +begin + inherited Create; + FPath := APath; + FLock := SyncObjs.TCriticalSection.Create; + FLog := TStringList.Create; + Succeeded := TEvent.Create(nil,False,False,''); + Failed := TEvent.Create(nil,False,False,''); + ErrorLogged := TEvent.Create(nil,False,False,''); + ReleaseErrorLog := TEvent.Create(nil,True,False,''); +end; + +destructor TSaveObservation.Destroy; +begin + ReleaseErrorLog.Free; + ErrorLogged.Free; + Failed.Free; + Succeeded.Free; + FLog.Free; + FLock.Free; + inherited; +end; + +procedure TSaveObservation.Log(const AText: String); +begin + FLock.Acquire; + try + FLog.Add(FormatDateTime('hh:nn:ss.zzz',Now)+' '+AText); + FLog.SaveToFile(FPath); + finally FLock.Release; end; +end; + +function TSaveObservation.BeginSave: Boolean; +begin + FLock.Acquire; + try + Inc(FAttempts); + Result := FFailuresRemaining>0; + if Result then Dec(FFailuresRemaining); + finally FLock.Release; end; + if Result then begin + Log('INJECT one save failure before storage mutation'); + Failed.SetEvent; + end; +end; + +procedure TSaveObservation.Saved(ACount: Integer); +begin + FLock.Acquire; + try + Inc(FSuccesses); + FLastCount := ACount; + finally FLock.Release; end; + Log('STORED actual pending count='+IntToStr(ACount)); + Succeeded.SetEvent; +end; + +procedure TSaveObservation.FailNextSave; +begin + FLock.Acquire; + try FFailuresRemaining := 1; + finally FLock.Release; end; +end; + +procedure TSaveObservation.ArmErrorLogGate; +begin + FLock.Acquire; + try + ReleaseErrorLog.ResetEvent; + FHoldErrorLog := True; + finally FLock.Release; end; +end; + +procedure TSaveObservation.CoreLog(AType: TLogType; ATime: TDateTime; + AThread: TThreadID; const ASender,AText: String); +var LHold: Boolean; +begin + if (AType<>lterror) or (Pos('Cannot save pending operations:',AText)=0) then Exit; + FLock.Acquire; + try + Inc(FLoggedFailures); + LHold := FHoldErrorLog; + FHoldErrorLog := False; + finally FLock.Release; end; + Log('CORE reported '+ASender+': '+AText); + ErrorLogged.SetEvent; + if LHold then ReleaseErrorLog.WaitFor(5000); +end; + +function TSaveObservation.LastCount: Integer; +begin + FLock.Acquire; + try Result := FLastCount; + finally FLock.Release; end; +end; + +function TSaveObservation.Attempts: Integer; +begin + FLock.Acquire; + try Result := FAttempts; + finally FLock.Release; end; +end; + +function TSaveObservation.LoggedFailures: Integer; +begin + FLock.Acquire; + try Result := FLoggedFailures; + finally FLock.Release; end; +end; + +procedure TObservedFileStorage.DoSavePendingBufferOperations(ATree: TOperationsHashTree); +begin + if GObservation.BeginSave then + raise EWriteError.Create('pending-save fixture failure'); + inherited DoSavePendingBufferOperations(ATree); + GObservation.Saved(ATree.OperationsCount); +end; + +function TObservedFileStorage.DoLoadBlockChain(AOperations: TPCOperationsComp; + ABlock: Cardinal): Boolean; +begin + // The generated confirmed bank has no mined chain file. Supply its current + // header for public UpdateValuesFromSafebox, which invokes the real bank + // subscribers and sanitizer. This fixture does not assert PoW acceptance. + if ABlock=Bank.BlocksCount-1 then begin + AOperations.Clear(True); + AOperations.SetOperationBlock(Bank.SafeBox.GetBlockInfo(ABlock)); + Result := True; + end else Result := inherited DoLoadBlockChain(AOperations,ABlock); +end; + +function TObservedFileStorage.GetFirstBlockNumber: Int64; +begin + Result := 0; +end; + +function TObservedFileStorage.GetLastBlockNumber: Int64; +begin + Result := Int64(Bank.BlocksCount)-1; +end; + +procedure TPendingSaveSafeBox.Build(const AKey: TAccountKey); +var I: Integer; LBlock: TOperationBlock; LAccount: TAccount; +begin + for I := 0 to 159 do begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I*CT_NewLineSecondsAvg; + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + LAccount := Account(0); + UpdateAccount(0,LAccount.accountInfo,LAccount.name,LAccount.account_type, + 1000000,0,LAccount.account_data,LAccount.account_seal, + aus_transaction_commit,0,0,False,False); +end; + +procedure TPendingSaveSafeBox.ConfirmFirstOperation; +var LAccount: TAccount; +begin + LAccount := Account(0); + UpdateAccount(0,LAccount.accountInfo,LAccount.name,LAccount.account_type, + LAccount.balance-2,1,LAccount.account_data,LAccount.account_seal, + aus_transaction_commit,0,0,False,False); +end; + +constructor TPendingSaveChecks.Create(const ALogPath: String); +var LID: TGUID; LTemporaryRoot: String; +begin + inherited Create; + FObservation := TSaveObservation.Create(ALogPath); + GObservation := FObservation; + FCoreLog := TLog.Create(nil); + FCoreLog.OnInThreadNewLog := FObservation.CoreLog; + Check(not TNode.NodeExists,'Each saver fixture requires a fresh node'); + CreateGUID(LID); + LTemporaryRoot := GetEnvironmentVariable('TEMP'); + Check(LTemporaryRoot<>'','TEMP must identify an owned test directory'); + FDirectory := IncludeTrailingPathDelimiter(LTemporaryRoot)+'PendingSave-'+GUIDToString(LID); + ForceDirectories(FDirectory); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FNode := TNode.Create(nil); + FNode.BroadcastData := False; + Check(not FNode.NetServer.Active,'The saver fixture must never listen'); + FNode.Bank.StorageClass := TObservedFileStorage; + Check(FNode.Bank.Storage.Initialize,'Real file storage initialization'); + TPendingSaveSafeBox(FNode.Bank.SafeBox).Build(FKey.PublicKey); + FNode.Bank.UpdateValuesFromSafebox; + FObservation.Log('READY generated bank and actual saver; data='+FDirectory); +end; + +destructor TPendingSaveChecks.Destroy; +begin + FNode.Free; + FCoreLog.Free; + GObservation := nil; + FKey.Free; + TNode.SetPascalCoinDataFolder(FPreviousDirectory); + FObservation.Free; + inherited; +end; + +procedure TPendingSaveChecks.Check(AValue: Boolean; const AText: String); +begin + if not AValue then raise Exception.Create(AText); +end; + +procedure TPendingSaveChecks.AddOperation(ANonce: Cardinal); +var LOperation: TPCOperation; LError: String; +begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_5,0,ANonce,1, + FKey,1,1,CT_TOperationPayload_NUL); + try + Check(FNode.AddOperation(nil,LOperation,LError),'Real AddOperation failed: '+LError); + finally LOperation.Free; end; +end; + +function TPendingSaveChecks.PendingBytes: TMemoryStream; +var LPool: TPCOperationsComp; +begin + Result := TMemoryStream.Create; + try + LPool := FNode.LockMempoolRead; + try Check(LPool.OperationsHashTree.SaveOperationsHashTreeToStream(Result,True),'Expected pending serialization'); + finally FNode.UnlockMempoolRead; end; + except Result.Free; raise; end; +end; + +procedure TPendingSaveChecks.StopNode; +var LStarted: TTickCount; +begin + LStarted := TPlatform.GetTickCount; + FreeAndNil(FNode); + Check(TPlatform.GetElapsedMilliseconds(LStarted)<3000,'Dirty saver shutdown must be bounded'); +end; + +procedure TPendingSaveChecks.CheckStoredBytes(AExpected: TStream); +var LActual: TMemoryStream; LExpected: TMemoryStream; +begin + LActual := TMemoryStream.Create; + LExpected := TMemoryStream.Create; + try + LActual.LoadFromFile(FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops'); + AExpected.Position := 0; + LExpected.CopyFrom(AExpected,AExpected.Size); + Check(LExpected.Size=LActual.Size,'Latest saved pending package length'); + Check(CompareMem(LExpected.Memory,LActual.Memory,LExpected.Size), + 'Latest saved pending package must contain exact current operations'); + finally LExpected.Free; LActual.Free; end; +end; + +procedure TPendingSaveChecks.FirstSave; +begin + AddOperation(1); + Check(FObservation.Succeeded.WaitFor(20000)=wrSignaled,'First actual saver completion'); + Check(FObservation.LastCount=1,'First durable package contains one actual accepted operation'); +end; + +procedure TPendingSaveChecks.FinalDirty; +var LExpected: TMemoryStream; +begin + FirstSave; + AddOperation(2); + LExpected := PendingBytes; + try + StopNode; + CheckStoredBytes(LExpected); + Check(FObservation.LastCount=2,'Final quiet-wait shutdown flushed the latest accepted operation'); + finally LExpected.Free; end; + FObservation.Log('PASS final dirty save while quiet-waiting, actual accepted operations and bounded natural shutdown'); +end; + +procedure TPendingSaveChecks.RetryAfterFailure; +var LExpected: TMemoryStream; LWorker: TPCThread; +begin + FirstSave; + FObservation.FailNextSave; + FObservation.ArmErrorLogGate; + AddOperation(2); + LExpected := PendingBytes; + try + Check(FObservation.Failed.WaitFor(20000)=wrSignaled,'Injected actual saver failure reached'); + Check(FObservation.ErrorLogged.WaitFor(3000)=wrSignaled,'Core reports the failed save'); + try + // Request the actual worker's normal cooperative termination while its + // running-attempt error callback is held. This deterministically reaches + // the attempt-start/shutdown race without writing private dirty flags. + LWorker := TPCThread.GetThreadByClass(TSaveMempoolOperationsThread,nil); + Check(Assigned(LWorker),'The actual saver still exists after reported failure'); + LWorker.Terminate; + finally FObservation.ReleaseErrorLog.SetEvent; end; + StopNode; + CheckStoredBytes(LExpected); + Check(FObservation.Attempts>=3,'Failed save was retried by the actual saver'); + Check(FObservation.LastCount=2,'Retry stored the latest complete package'); + finally FObservation.ReleaseErrorLog.SetEvent; LExpected.Free; end; + FObservation.Log('PASS one failed save is reported and retried, with latest bytes preserved on shutdown'); +end; + +procedure TPendingSaveChecks.SanitizeRemoval; +var LPool: TPCOperationsComp; LExpected: TMemoryStream; +begin + FirstSave; + LPool := FNode.LockMempoolWrite; + try + TPendingSaveSafeBox(FNode.Bank.SafeBox).ConfirmFirstOperation; + FNode.Bank.UpdateValuesFromSafebox; + Check(LPool.Count=0,'Actual bank notification sanitized the now-confirmed pending operation'); + finally FNode.UnlockMempoolWrite; end; + LExpected := PendingBytes; + try + Check(FObservation.Succeeded.WaitFor(20000)=wrSignaled, + 'Sanitization removal scheduled an actual save without new AddOperations'); + Check(FObservation.LastCount=0,'Removal save stores the current empty pool'); + StopNode; + CheckStoredBytes(LExpected); + finally LExpected.Free; end; + FObservation.Log('PASS actual sanitizer removal marks durable pending state dirty through bank notification'); +end; + +procedure TPendingSaveChecks.AtomicStorage; +var LOldTree,LNewTree,LRead: TOperationsHashTree; + LOperation: TPCOperation; LExpected: TMemoryStream; + LFileName: String; LReader: TFileStream; LRaised: Boolean; + LSearch: TSearchRec; + + procedure RequireNoTemporaryFiles; + var LFound: Boolean; + begin + LFound := FindFirst(LFileName+'.*.tmp',faAnyFile,LSearch)=0; + if LFound then FindClose(LSearch); + Check(not LFound,'Failed publication cleans only its owned temporary package'); + end; + + procedure CheckLoadedPackage(ATree: TOperationsHashTree); + begin + LRead.ClearHastThree; + FNode.Bank.Storage.LoadPendingBufferOperations(LRead); + Check(LRead.OperationsCount=ATree.OperationsCount,'Cached file getter reopens the expected complete package'); + Check(TBaseType.Equals(LRead.HashTree,ATree.HashTree),'Reopened package ordered hash'); + end; + +begin + LOldTree := TOperationsHashTree.Create; + LNewTree := TOperationsHashTree.Create; + LRead := TOperationsHashTree.Create; + LExpected := TMemoryStream.Create; + try + LFileName := FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops'; + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_5,0,1,1,FKey,1,1,CT_TOperationPayload_NUL); + try Check(LOldTree.AddOperationToHashTree(LOperation),'Original atomic package operation'); + finally LOperation.Free; end; + LNewTree.CopyFromHashTree(LOldTree); + LOperation := TSerializationFailureTransaction.CreateTransaction(CT_PROTOCOL_5,0,2,1,FKey,1,1,CT_TOperationPayload_NUL); + try Check(LNewTree.AddOperationToHashTree(LOperation),'Replacement atomic package operation'); + finally LOperation.Free; end; + FNode.Bank.Storage.SavePendingBufferOperations(LOldTree); + CheckLoadedPackage(LOldTree); // Establish a cached handle before failure. + GFailSerialization := True; + LRaised := False; + try + try FNode.Bank.Storage.SavePendingBufferOperations(LNewTree); + except on E: EWriteError do LRaised := True; end; + finally GFailSerialization := False; end; + Check(LRaised,'Serialization failure propagates before temporary write'); + CheckLoadedPackage(LOldTree); + RequireNoTemporaryFiles; + // Close the cached handle by successfully republishing the unchanged tree. + FNode.Bank.Storage.SavePendingBufferOperations(LOldTree); + {$IFDEF MSWINDOWS} + LReader := TFileStream.Create(LFileName,fmOpenRead+fmShareDenyNone); + try + LRaised := False; + try FNode.Bank.Storage.SavePendingBufferOperations(LNewTree); + except + on E: EOSError do begin + Check(E.ErrorCode in [5,32],'Publication failed at the intended Windows sharing boundary'); + LRaised := True; + end; + end; + Check(LRaised,'An open handle denying deletion rejects atomic publication'); + finally LReader.Free; end; + RequireNoTemporaryFiles; + CheckLoadedPackage(LOldTree); + {$ENDIF} + FNode.Bank.Storage.SavePendingBufferOperations(LNewTree); + CheckLoadedPackage(LNewTree); + Check(LNewTree.SaveOperationsHashTreeToStream(LExpected,True),'Expected replacement serialization'); + StopNode; + CheckStoredBytes(LExpected); + RequireNoTemporaryFiles; + FObservation.Log('PASS serialization failure and post-write replacement failure preserve complete bytes, cleanup and cached reopen'); + finally + GFailSerialization := False; + LExpected.Free; LRead.Free; LNewTree.Free; LOldTree.Free; + end; +end; + +procedure TPendingSaveChecks.FinalPublicationFailure; +{$IFDEF MSWINDOWS} +var LPrevious: TMemoryStream; LReader: TFileStream; +{$ENDIF} +begin + {$IFDEF MSWINDOWS} + FirstSave; + LPrevious := PendingBytes; + try + // This is a genuine post-write filesystem failure, not a replaced saver: + // the reader allows reads/writes but denies atomic deletion/replacement. + LReader := TFileStream.Create(FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops', + fmOpenRead+fmShareDenyNone); + try + AddOperation(2); + StopNode; + Check(FObservation.ErrorLogged.WaitFor(3000)=wrSignaled,'A failed final publication is reported'); + Check(FObservation.Attempts=2,'Final publication failure exits after one bounded attempt'); + CheckStoredBytes(LPrevious); + finally LReader.Free; end; + finally LPrevious.Free; end; + FObservation.Log('PASS actual failed final atomic publication reports error, retains last complete bytes and exits naturally'); + {$ELSE} + FObservation.Log('Windows-only sharing-denial final-publication fixture is not exercised on this platform'); + {$ENDIF} +end; + +procedure TPendingSaveChecks.Run(ACheck: TPendingSaveCheck); +begin + case ACheck of + pscFinalDirty: FinalDirty; + pscRetryAfterFailure: RetryAfterFailure; + pscSanitizeRemoval: SanitizeRemoval; + pscAtomicStorage: AtomicStorage; + pscFinalPublicationFailure: FinalPublicationFailure; + end; +end; + +function RunPendingSaveCheck(ACheck: TPendingSaveCheck; const ALogPath: String): Integer; +var LChecks: TPendingSaveChecks; +begin + Result := 1; + LChecks := TPendingSaveChecks.Create(ALogPath); + try + try LChecks.Run(ACheck); Result := 0; + except on E: Exception do begin + LChecks.FObservation.Log('FAIL '+E.ClassName+': '+E.Message); + raise; + end; end; + finally LChecks.Free; end; +end; + +function RunPendingSaveChecks(const ALogPath: String): Integer; +var LCheck: TPendingSaveCheck; LLog: TStringList; LPath: String; +begin + Result := 1; + LLog := TStringList.Create; + try + for LCheck := Low(TPendingSaveCheck) to High(TPendingSaveCheck) do begin + LPath := ALogPath+'.'+IntToStr(Ord(LCheck))+'.log'; + if RunPendingSaveCheck(LCheck,LPath)<>0 then Exit; + LLog.Add('PASS pending-save scenario '+IntToStr(Ord(LCheck))+'; '+LPath); + LLog.SaveToFile(ALogPath); + end; + Result := 0; + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.dpr b/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.dpr new file mode 100644 index 000000000..39231808d --- /dev/null +++ b/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.dpr @@ -0,0 +1,30 @@ +program BufferedAdoptionProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE CONSOLE}{$ENDIF} + +uses {$IFDEF FPC}{$IFNDEF BUFFERED_ADOPTION_PERFORMANCE}heaptrc,{$ENDIF}{$ENDIF} SysUtils, UBufferedAdoptionChecks; + +begin + {$IFDEF FPC} + {$IFNDEF BUFFERED_ADOPTION_PERFORMANCE} + if ParamCount>0 then SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + {$ENDIF} + {$ELSE} + ReportMemoryLeaksOnShutdown := True; + {$ENDIF} + if not (ParamCount in [1,2]) then Halt(64); + try + if ParamCount=2 then begin + if ParamStr(2)<>'perf' then Halt(64); + ExitCode := RunBufferedAdoptionPerformance(ExpandFileName(ParamStr(1))); + end else + ExitCode := RunBufferedAdoptionChecks(ExpandFileName(ParamStr(1))); + except + on E: Exception do begin + WriteLn(E.ClassName+': '+E.Message); + ExitCode := 1; + end; + end; +end. diff --git a/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.dproj b/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.dproj new file mode 100644 index 000000000..cb6aecc73 --- /dev/null +++ b/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.dproj @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{BA9798D8-AE57-4D60-95F9-141EC705A9D9}</ProjectGuid> + <MainSource>BufferedAdoptionProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\buffered-adoption\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\buffered-adoption\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\buffered-adoption\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\buffered-adoption\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UBufferedAdoptionChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">BufferedAdoptionProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.lpr b/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.lpr new file mode 100644 index 000000000..39231808d --- /dev/null +++ b/src/tests/experimental/native/buffered-adoption/BufferedAdoptionProbe.lpr @@ -0,0 +1,30 @@ +program BufferedAdoptionProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE CONSOLE}{$ENDIF} + +uses {$IFDEF FPC}{$IFNDEF BUFFERED_ADOPTION_PERFORMANCE}heaptrc,{$ENDIF}{$ENDIF} SysUtils, UBufferedAdoptionChecks; + +begin + {$IFDEF FPC} + {$IFNDEF BUFFERED_ADOPTION_PERFORMANCE} + if ParamCount>0 then SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + {$ENDIF} + {$ELSE} + ReportMemoryLeaksOnShutdown := True; + {$ENDIF} + if not (ParamCount in [1,2]) then Halt(64); + try + if ParamCount=2 then begin + if ParamStr(2)<>'perf' then Halt(64); + ExitCode := RunBufferedAdoptionPerformance(ExpandFileName(ParamStr(1))); + end else + ExitCode := RunBufferedAdoptionChecks(ExpandFileName(ParamStr(1))); + except + on E: Exception do begin + WriteLn(E.ClassName+': '+E.Message); + ExitCode := 1; + end; + end; +end. diff --git a/src/tests/experimental/native/buffered-adoption/README.md b/src/tests/experimental/native/buffered-adoption/README.md new file mode 100644 index 000000000..195ad03a6 --- /dev/null +++ b/src/tests/experimental/native/buffered-adoption/README.md @@ -0,0 +1,27 @@ +# Buffered input adoption checks + +Eleven offline checks cover pending-package round trips, cancellation and atomic +replacement, block-history reads, the 1,000-header boundary, damaged inputs and +RandomHash table decoding. + +Shipping builds define `ASSUME_VALID_POW_OLD_PROTOCOLS`, so +`TPascalCoinProtocol.AllowUseHardcodedRandomHashTable` returns early and the log +shows `RANDOMHASH_ENTRY=shipping-early-return`. Testing its table branch needs a +separate build. + +Run on Windows x64 with a new output directory: + +```powershell +./Run-BufferedAdoptionChecks.ps1 -OutputDirectory '<new-folder>' +``` + +The script finds FPC on `PATH` unless you pass `-FpcPath`, and takes OpenSSL +1.1 from `src` or `windows-dll`. + +For Delphi, build `BufferedAdoptionProbe.dproj`, set `TEMP` to a test folder and +pass an output-log filename. Put `libcrypto-1_1-x64.dll` and +`src/tests/experimental/fixtures/mainnet-blocks-0-4.bin` beside the executable. + +For timing, define `BUFFERED_ADOPTION_PERFORMANCE` in FPC builds, set +`BUFFERED_ADOPTION_PENDING_SAMPLE` to one sample path for all compared builds +and run `BufferedAdoptionProbe <log> perf`. diff --git a/src/tests/experimental/native/buffered-adoption/Run-BufferedAdoptionChecks.ps1 b/src/tests/experimental/native/buffered-adoption/Run-BufferedAdoptionChecks.ps1 new file mode 100644 index 000000000..f727b710f --- /dev/null +++ b/src/tests/experimental/native/buffered-adoption/Run-BufferedAdoptionChecks.ps1 @@ -0,0 +1,45 @@ +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [Parameter(Mandatory=$true)][string]$OutputDirectory, + [string]$FpcPath=$(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }) +) +$ErrorActionPreference='Stop' +if(-not $FpcPath){throw 'Free Pascal was not found on PATH; pass -FpcPath.'} +$compiler=(Get-Command $FpcPath -ErrorAction Stop).Source +$target=((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if($LASTEXITCODE-ne0-or$target.Count-ne2-or$target[0]-ne'x86_64'-or$target[1]-ne'win64'){ + throw 'This native runner requires Windows x64 Free Pascal.' +} +$source=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../../../..')).Path +$output=[IO.Path]::GetFullPath($OutputDirectory) +if(Test-Path -LiteralPath $output){throw 'Choose a fresh output directory.'} +foreach($directory in @('units','temporary-data')){New-Item -ItemType Directory -Path (Join-Path $output $directory) -Force|Out-Null} +$exe=Join-Path $output 'BufferedAdoptionProbe.exe' +$arguments=@('-B','-MDelphi','-O2','-gl','-Cr','-Co','-Ci','-dBorland','-dVer150','-dDelphi7','-dCompiler6_Up','-dPUREPASCAL',('-FU'+(Join-Path $output 'units')),('-FE'+$output),('-o'+$exe),('-Fi'+(Join-Path $source 'libraries\generics.collections\inc')),('-Fu'+$PSScriptRoot)) +foreach($directory in @('core','libraries\simplebaselib4pascal','libraries\hashlib4pascal','libraries\cryptolib4pascal','libraries\synapse','libraries\sphere10','libraries\generics.collections','libraries\pascalcoin','libraries\paszlib','libraries\abstractmem','libraries\regex')){ + $arguments+='-Fu'+(Join-Path $source $directory) +} +$arguments+=Join-Path $PSScriptRoot 'BufferedAdoptionProbe.lpr' +$arguments|Set-Content -LiteralPath (Join-Path $output 'compiler-arguments.txt') +& $compiler @arguments *> (Join-Path $output 'build.log') +if($LASTEXITCODE-ne0){Get-Content -LiteralPath (Join-Path $output 'build.log') -Tail 40;throw 'Buffered adoption probe did not compile.'} +# Prefer a deployed runtime, then the tracked copy, so a clean checkout also runs. +$crypto=Join-Path $source 'libcrypto-1_1-x64.dll' +if(-not(Test-Path -LiteralPath $crypto)){$crypto=Join-Path (Split-Path -Parent $source) 'windows-dll\libcrypto-1_1-x64.dll'} +if(-not(Test-Path -LiteralPath $crypto)){throw "OpenSSL runtime not found: $crypto"} +Copy-Item -LiteralPath $crypto -Destination $output +Copy-Item -LiteralPath (Join-Path $source 'tests\experimental\fixtures\mainnet-blocks-0-4.bin') -Destination $output +$oldTemp,$oldTmp=$env:TEMP,$env:TMP +try{ + $env:TEMP=Join-Path $output 'temporary-data';$env:TMP=$env:TEMP + $log=Join-Path $output 'checks.log' + & $exe $log *> (Join-Path $output 'stdout.log') + $code=$LASTEXITCODE +}finally{$env:TEMP,$env:TMP=$oldTemp,$oldTmp} +if(-not(Test-Path -LiteralPath $log)){Get-Content -LiteralPath (Join-Path $output 'stdout.log');throw 'Buffered adoption probe omitted its log.'} +$text=[IO.File]::ReadAllText($log) +$heap=[IO.File]::ReadAllText($log+'.heap.log') +Write-Output $text +if($code-ne0-or$text-match'(?m)^FAIL'-or$text-notmatch'(?m)^PASS 11\s*$'-or$heap-notmatch'(?m)^0 unfreed memory blocks'){ + throw 'Buffered adoption checks failed; retained evidence is in the output directory.' +} diff --git a/src/tests/experimental/native/buffered-adoption/UBufferedAdoptionChecks.pas b/src/tests/experimental/native/buffered-adoption/UBufferedAdoptionChecks.pas new file mode 100644 index 000000000..3dffa6430 --- /dev/null +++ b/src/tests/experimental/native/buffered-adoption/UBufferedAdoptionChecks.pas @@ -0,0 +1,645 @@ +unit UBufferedAdoptionChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Small actual-core checks for buffered pending, history and table input. + The header-boundary records test storage serialization, not consensus admission. } + +{$IFDEF FPC}{$MODE Delphi}{$H+}{$ENDIF} +{$I ../../../../config.inc} + +interface + +const BufferedAdoptionCheckCount = 11; +procedure CheckBufferedAdoption(AIndex: Integer); +function RunBufferedAdoptionChecks(const ALogPath: String): Integer; +function RunBufferedAdoptionPerformance(const ALogPath: String): Integer; + +implementation + +uses Classes, SysUtils, {$IFDEF MSWINDOWS}Windows,{$ENDIF} UCrypto, UAccounts, UBlockChain, UFileStorage, + UNode, UOpTransaction, UConst, UPCDataTypes, UBaseTypes, UPCHardcodedRandomHashTable; + +type + TStoredBlocks = array of TBytes; + + TAdoptionSafeBox = class(TPCSafeBox) + public + procedure UseStorageProtocol(AProtocol: Word); + end; + + TBufferedAdoptionFixture = class + private + FDirectory, FPreviousDirectory: String; + FChangedDirectory: Boolean; + FKey: TECPrivateKey; + FCancelPending: Boolean; + procedure PendingProgress(Sender: TObject; const AProgress: TProgressInfo); + procedure FillPending(ATree: TOperationsHashTree; AProtocol: Word; ACount: Integer); + procedure SeedPendingBank(ABank: TPCBank; AProtocol: Word); + procedure Pending(AProtocol: Word; ACancel: Boolean); + procedure PopulateHistory(AStorage: TFileStorage; out AExpected: TStoredBlocks); + procedure History; + procedure HeaderBoundary; + procedure MalformedHistory; + procedure RandomTable(AKind: Integer); + {$IFDEF MSWINDOWS} + procedure Performance(ALines: TStrings); + procedure PendingPerformance(ALines: TStrings); + {$ENDIF} + public + constructor Create; + destructor Destroy; override; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('Buffered adoption: '+AMessage); +end; + +function AdoptionCheckName(AIndex: Integer): String; +const Names: array[1..BufferedAdoptionCheckCount] of String = ( + 'PendingProtocol5RoundTripAndReplacement','PendingProtocol5CancellationAndRetry', + 'HistoryReadSaveDeleteAndReopen','HistoryHeaderGroupBoundary', + 'RandomHashCanonicalTable','RandomHashInvalidMagic','RandomHashTruncatedSeal','RandomHashMismatchedSeal', + 'MalformedHistoryReadOnlyPreservesBytes','PendingProtocol6RoundTripAndReplacement','PendingProtocol6CancellationAndRetry'); +begin + Result := Names[AIndex]; +end; + +procedure TAdoptionSafeBox.UseStorageProtocol(AProtocol: Word); +begin + // This fixture exercises the current saved-operation format, without replaying + // years of blocks merely to reach its protocol. It does not admit a new block. + Require(AProtocol in [CT_PROTOCOL_5,CT_PROTOCOL_6],'unsupported fixture protocol'); + Require(DoUpgradeToProtocol5,'cannot select protocol5 storage'); + if AProtocol=CT_PROTOCOL_6 then Require(DoUpgradeToProtocol6,'cannot select protocol6 storage'); +end; + +function FileBytes(const APath: String): TBytes; +var LFile: TFileStream; LSize: Int64; +begin + LFile := TFileStream.Create(APath,fmOpenRead or fmShareDenyNone); + try + LSize := LFile.Size; + Require((LSize>=0) and (LSize<=4*1024*1024),'fixture exceeded its 4 MiB bound'); + SetLength(Result,Integer(LSize)); + if LSize>0 then LFile.ReadBuffer(Result[0],Longint(LSize)); + finally + LFile.Free; + end; +end; + +procedure WriteBytes(const APath: String; const ABytes: TBytes); +var LFile: TFileStream; +begin + LFile := TFileStream.Create(APath,fmCreate); + try + if Length(ABytes)>0 then LFile.WriteBuffer(ABytes[0],Longint(Length(ABytes))); + finally + LFile.Free; + end; +end; + +function SameBytes(const ALeft, ARight: TBytes): Boolean; +begin + Result := Length(ALeft)=Length(ARight); + if Result and (Length(ALeft)>0) then Result := CompareMem(@ALeft[0],@ARight[0],Length(ALeft)); +end; + +function StoredBytes(AOperations: TPCOperationsComp): TBytes; +var LMemory: TMemoryStream; +begin + LMemory := TMemoryStream.Create; + try + Require(AOperations.SaveBlockToStorage(LMemory),'cannot serialize expected stored block'); + SetLength(Result,Integer(LMemory.Size)); + if Length(Result)>0 then Move(LMemory.Memory^,Result[0],Length(Result)); + finally + LMemory.Free; + end; +end; + +constructor TBufferedAdoptionFixture.Create; +var LId: TGUID; +begin + inherited Create; + Require(not TNode.NodeExists,'fixture requires an independent node directory'); + Require(SysUtils.GetEnvironmentVariable('TEMP')<>'','runner must supply its owned TEMP directory'); + Require(CreateGUID(LId)=0,'cannot create fixture identity'); + FDirectory := IncludeTrailingPathDelimiter(SysUtils.GetEnvironmentVariable('TEMP'))+'BufferedAdoption-'+GUIDToString(LId); + Require(not DirectoryExists(FDirectory) and ForceDirectories(FDirectory),'cannot create fresh fixture directory'); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + FChangedDirectory := True; + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); +end; + +destructor TBufferedAdoptionFixture.Destroy; +begin + FKey.Free; + if FChangedDirectory then TNode.SetPascalCoinDataFolder(FPreviousDirectory); + // Retain these small owned files beside the test logs for before/after review. + inherited Destroy; +end; + +procedure TBufferedAdoptionFixture.PendingProgress(Sender: TObject; const AProgress: TProgressInfo); +begin + Require(AProgress.Stage=psReadingOperations,'pending stage changed'); + if FCancelPending then Abort; +end; + +procedure TBufferedAdoptionFixture.FillPending(ATree: TOperationsHashTree; AProtocol: Word; ACount: Integer); +var I: Integer; LOperation: TPCOperation; +begin + for I := 1 to ACount do begin + LOperation := TOpTransaction.CreateTransaction(AProtocol,100,I,101,FKey,100,1,CT_TOperationPayload_NUL); + try + Require(ATree.AddOperationToHashTree(LOperation),'cannot create signed pending fixture'); + finally + LOperation.Free; + end; + end; +end; + +procedure TBufferedAdoptionFixture.Pending(AProtocol: Word; ACancel: Boolean); +var LBank: TPCBank; LStorage: TFileStorage; LOriginal, LLoaded, LRaw: TOperationsHashTree; LPath, LError: String; LBefore: TBytes; LFile: TFileStream; I: Integer; LRaised: Boolean; +begin + LBank := TPCBank.Create(nil); + LStorage := nil; + LOriginal := TOperationsHashTree.Create; + LLoaded := TOperationsHashTree.Create; + LRaw := TOperationsHashTree.Create; + try + SeedPendingBank(LBank,AProtocol); + LStorage := TFileStorage.Create(nil); + LStorage.Bank := LBank; + FillPending(LOriginal,LBank.SafeBox.CurrentProtocol,8); + LStorage.SavePendingBufferOperations(LOriginal); + LPath := IncludeTrailingPathDelimiter(TPCBank.GetStorageFolder(LBank.Orphan))+'pendingbuffer.ops'; + LBefore := FileBytes(LPath); + Require(Length(LBefore)>0,'actual pending save produced no package'); + LFile := TFileStream.Create(LPath,fmOpenRead or fmShareDenyWrite); + try + Require(LRaw.LoadOperationsHashTreeFromStream(LFile,True,LBank.SafeBox.CurrentProtocol,LBank.SafeBox.CurrentProtocol,nil,LError),'raw pending fixture decode failed at protocol '+IntToStr(LBank.SafeBox.CurrentProtocol)+': '+LError); + Require(LRaw.OperationsCount=8,'raw pending fixture decoded a different operation count'); + finally + LFile.Free; + end; + if ACancel then begin + FCancelPending := True; + LRaised := False; + try + LStorage.LoadPendingBufferOperations(LLoaded,PendingProgress); + except + on E: EAbort do LRaised := True; + end; + Require(LRaised,'actual pending load did not propagate cancellation'); + FCancelPending := False; + end; + LStorage.LoadPendingBufferOperations(LLoaded,PendingProgress); + Require(LLoaded.OperationsCount=LOriginal.OperationsCount,'actual pending restore lost operations: expected '+IntToStr(LOriginal.OperationsCount)+' actual '+IntToStr(LLoaded.OperationsCount)); + for I := 0 to LOriginal.OperationsCount-1 do + Require(TBaseType.Equals(LOriginal.GetOperation(I).Sha256,LLoaded.GetOperation(I).Sha256),'pending operation digest changed'); + // The existing pending reader intentionally holds an exclusive file lease. + // Close it before independently comparing the persisted package bytes. + FreeAndNil(LStorage); + Require(SameBytes(LBefore,FileBytes(LPath)),'pending cancellation/restore altered package bytes'); + LStorage := TFileStorage.Create(nil); + LStorage.Bank := LBank; + LLoaded.ClearHastThree; + LStorage.LoadPendingBufferOperations(LLoaded); + // Replacement retires the retained reader before the next atomic package. + LOriginal.ClearHastThree; + FillPending(LOriginal,LBank.SafeBox.CurrentProtocol,3); + LStorage.SavePendingBufferOperations(LOriginal); + LLoaded.ClearHastThree; + LStorage.LoadPendingBufferOperations(LLoaded); + Require(LLoaded.OperationsCount=3,'pending replacement reused stale buffered bytes'); + for I := 0 to 2 do + Require(TBaseType.Equals(LOriginal.GetOperation(I).Sha256,LLoaded.GetOperation(I).Sha256),'replacement pending digest differs'); + finally + LRaw.Free; + LStorage.Free; + LLoaded.Free; + LOriginal.Free; + LBank.Free; + end; +end; + +procedure TBufferedAdoptionFixture.SeedPendingBank(ABank: TPCBank; AProtocol: Word); +var LFile: TFileStream; LOperations: TPCOperationsComp; LCount: Cardinal; LError: String; +begin + ABank.StorageClass := TFileStorage; + LFile := TFileStream.Create(IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'mainnet-blocks-0-4.bin',fmOpenRead or fmShareDenyWrite); + LOperations := TPCOperationsComp.Create(ABank); + try + LFile.ReadBuffer(LCount,SizeOf(LCount)); + Require((LCount=5) and LOperations.LoadBlockFromStream(LFile,LError),'cannot decode the actual genesis fixture'); + Require(ABank.AddNewBlockChainBlock(LOperations,0,LError),'cannot seed pending bank: '+LError); + TAdoptionSafeBox(ABank.SafeBox).UseStorageProtocol(AProtocol); + Require(ABank.SafeBox.CurrentProtocol=AProtocol,'pending fixture did not reach the selected storage protocol'); + finally + LOperations.Free; + LFile.Free; + end; +end; + +procedure TBufferedAdoptionFixture.PopulateHistory(AStorage: TFileStorage; out AExpected: TStoredBlocks); +var LFixture: TFileStream; LOperations: TPCOperationsComp; LCount: Cardinal; LError: String; I: Integer; +begin + LFixture := TFileStream.Create(IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'mainnet-blocks-0-4.bin',fmOpenRead or fmShareDenyWrite); + LOperations := TPCOperationsComp.Create(nil); + try + LFixture.ReadBuffer(LCount,SizeOf(LCount)); + Require(LCount=5,'committed public block fixture count changed'); + SetLength(AExpected,5); + for I := 0 to 4 do begin + LError := ''; + Require(LOperations.LoadBlockFromStream(LFixture,LError),'public block decode failed: '+LError); + Require(LOperations.OperationBlock.block=Cardinal(I),'public fixture order changed'); + AExpected[I] := StoredBytes(LOperations); + Require(AStorage.SaveBlockChainBlock(LOperations),'actual file storage save failed'); + end; + finally + LOperations.Free; + LFixture.Free; + end; +end; + +procedure TBufferedAdoptionFixture.History; +const Order: array[0..4] of Integer = (4,0,3,1,2); +var LStorage: TFileStorage; LOperations: TPCOperationsComp; LExpected: TStoredBlocks; LInitial: TBytes; LPath: String; I, LIndex: Integer; +begin + LPath := IncludeTrailingPathDelimiter(FDirectory)+'history.blocks'; + LStorage := TFileStorage.Create(nil); + LOperations := TPCOperationsComp.Create(nil); + try + LStorage.SetBlockChainFile(AnsiString(LPath)); + PopulateHistory(LStorage,LExpected); + LStorage.SetBlockChainFile(AnsiString(LPath)); + Require(LStorage.Initialize,'history reopen initialization failed'); + LInitial := FileBytes(LPath); + Require((LStorage.FirstBlock=0) and (LStorage.LastBlock=4),'history reopen lost its range'); + for I := Low(Order) to High(Order) do begin + LIndex := Order[I]; + Require(LStorage.BlockExists(LIndex),'history index lost a stored block'); + Require(LStorage.LoadBlockChainBlock(LOperations,LIndex),'actual out-of-order history load failed'); + Require(SameBytes(LExpected[LIndex],StoredBytes(LOperations)),'history payload bytes changed'); + end; + LStorage.SetBlockChainFile(AnsiString(LPath)); + Require(SameBytes(LInitial,FileBytes(LPath)),'history reads rewrote the file'); + LStorage.DeleteBlockChainBlocks(3); + Require((LStorage.LastBlock=2) and not LStorage.BlockExists(3),'history delete retained removed index rows'); + for I := 0 to 2 do begin + Require(LStorage.LoadBlockChainBlock(LOperations,I),'history delete lost retained blocks'); + Require(SameBytes(LExpected[I],StoredBytes(LOperations)),'history delete changed retained payload'); + end; + // Recreate all rows through the same production writer and compare the file. + LStorage.SetBlockChainFile(AnsiString(LPath)); + LStorage.EraseStorage; + PopulateHistory(LStorage,LExpected); + LStorage.SetBlockChainFile(AnsiString(LPath)); + Require(SameBytes(LInitial,FileBytes(LPath)),'history save/reopen changed canonical file bytes'); + LStorage.ReadOnly := True; + Require(LStorage.Initialize,'read-only history initialization failed'); + Require(LStorage.LoadBlockChainBlock(LOperations,4),'read-only history load failed'); + Require(SameBytes(LExpected[4],StoredBytes(LOperations)),'read-only history payload differs'); + finally + LOperations.Free; + LStorage.Free; + end; +end; + +procedure TBufferedAdoptionFixture.HeaderBoundary; +var LStorage: TFileStorage; LOperations, LLoaded: TPCOperationsComp; LBlock: TOperationBlock; I: Integer; LPath: String; +begin + LPath := IncludeTrailingPathDelimiter(FDirectory)+'header-boundary.blocks'; + LStorage := TFileStorage.Create(nil); + LOperations := TPCOperationsComp.Create(nil); + LLoaded := TPCOperationsComp.Create(nil); + try + LStorage.SetBlockChainFile(AnsiString(LPath)); + LBlock := TPCOperationsComp.GetFirstBlock; + for I := 0 to 1001 do begin + LBlock.block := I; + // The existing testing setter changes only this generated record, not + // production consensus checks; storage parsing is the subject here. + LOperations.SetOperationBlock(LBlock); + Require(LStorage.SaveBlockChainBlock(LOperations),'cannot build the second header group'); + end; + LStorage.SetBlockChainFile(AnsiString(LPath)); + Require(LStorage.Initialize,'boundary reopen initialization failed'); + Require((LStorage.FirstBlock=0) and (LStorage.LastBlock=1001),'header-group reopen lost its range'); + Require(LStorage.LoadBlockChainBlock(LLoaded,1000),'second header group cannot be read'); + Require(LLoaded.OperationBlock.block=1000,'second header group points to the wrong record'); + Require(LStorage.LoadBlockChainBlock(LLoaded,999),'previous header group cannot be revisited'); + Require(LLoaded.OperationBlock.block=999,'previous header group points to the wrong record'); + LStorage.DeleteBlockChainBlocks(1000); + LStorage.SetBlockChainFile(AnsiString(LPath)); + Require(LStorage.Initialize,'boundary delete/reopen initialization failed'); + Require((LStorage.LastBlock=999) and not LStorage.BlockExists(1000),'boundary delete/reopen retained removed rows'); + finally + LLoaded.Free; + LOperations.Free; + LStorage.Free; + end; +end; + +procedure TBufferedAdoptionFixture.RandomTable(AKind: Integer); +var LTable, LRaw: TPCHardcodedRandomHashTable; LFile: TFileStream; LBytes: TBytes; LHash, LDigest, LExpected, LActual: TRawBytes; LPath: String; I, J: Integer; LRawAccepted, LPublicAccepted, LExpectedAccepted: Boolean; +begin + LPath := IncludeTrailingPathDelimiter(FDirectory)+'random-table.bin'; + LTable := TPCHardcodedRandomHashTable.Create; + LRaw := TPCHardcodedRandomHashTable.Create; + try + for I := 0 to 2 do begin + SetLength(LHash,32); + for J := 0 to 31 do LHash[J] := Byte(I*32+J); + LDigest.FromString('buffered-adoption-'+IntToStr(I)); + LTable.AddRandomHash(LHash,LDigest,False); + end; + LExpected := LTable.GetHardcodedSha256; + LFile := TFileStream.Create(LPath,fmCreate); + try + LTable.SaveToStream(LFile); + finally + LFile.Free; + end; + LBytes := FileBytes(LPath); + case AKind of + 1: LBytes[2] := LBytes[2] xor $01; + 2: SetLength(LBytes,Length(LBytes)-8); + 3: LBytes[Length(LBytes)-1] := LBytes[Length(LBytes)-1] xor $01; + end; + WriteBytes(LPath,LBytes); + LFile := TFileStream.Create(LPath,fmOpenRead or fmShareDenyWrite); + try + LRawAccepted := LRaw.LoadFromStream(LFile,LActual); + finally + LFile.Free; + end; + Require(LRawAccepted=(AKind=0),'raw canonical-table fixture has an unexpected outcome'); + LPublicAccepted := TPascalCoinProtocol.AllowUseHardcodedRandomHashTable(LPath,LExpected); + {$IFDEF ASSUME_VALID_POW_OLD_PROTOCOLS} + // The shipping configuration returns before opening the table file. + LExpectedAccepted := False; + {$ELSE} + LExpectedAccepted := LRawAccepted; + {$ENDIF} + Require(LPublicAccepted=LExpectedAccepted,'actual RandomHash entry changed raw-loader acceptance'); + Require(SameBytes(LBytes,FileBytes(LPath)),'RandomHash load altered the input file'); + if AKind=0 then begin + LExpected[0] := LExpected[0] xor $01; + Require(not TPascalCoinProtocol.AllowUseHardcodedRandomHashTable(LPath,LExpected),'wrong caller-supplied table hash was accepted'); + Require(not TPascalCoinProtocol.AllowUseHardcodedRandomHashTable(LPath+'.missing',LExpected),'missing table was accepted'); + end; + finally + LRaw.Free; + LTable.Free; + end; +end; + +procedure TBufferedAdoptionFixture.MalformedHistory; +var LPath: String; LStorage: TFileStorage; LExpected: TStoredBlocks; LBytes: TBytes; LRaised, LAccepted: Boolean; LLease: TFileStream; LOperations: TPCOperationsComp; I: Integer; +begin + for I := 0 to 1 do begin + LPath := IncludeTrailingPathDelimiter(FDirectory)+'malformed-'+IntToStr(I)+'.blocks'; + LStorage := TFileStorage.Create(nil); + try + LStorage.SetBlockChainFile(AnsiString(LPath)); + PopulateHistory(LStorage,LExpected); + finally + LStorage.Free; + end; + LBytes := FileBytes(LPath); + if I=0 then SetLength(LBytes,63) + else SetLength(LBytes,Length(LBytes)-8); + WriteBytes(LPath,LBytes); + LStorage := TFileStorage.Create(nil); + try + LStorage.SetBlockChainFile(AnsiString(LPath)); + LStorage.ReadOnly := True; + LRaised := False; + LAccepted := False; + try + LAccepted := LStorage.Initialize; + except + on E: Exception do LRaised := True; + end; + if I=0 then + Require(LRaised or not LAccepted,'read-only malformed header was accepted') + else if LAccepted then begin + // Index initialization need not decode the final body; requesting that + // truncated payload must retain the existing False result. + LOperations := TPCOperationsComp.Create(nil); + try + Require(not LStorage.LoadBlockChainBlock(LOperations,4),'truncated history payload was accepted'); + finally + LOperations.Free; + end; + end; + finally + LStorage.Free; + end; + Require(SameBytes(LBytes,FileBytes(LPath)),'read-only malformed history was repaired or rewritten'); + // The normal owner must release its raw lease even after initialization fails. + LLease := TFileStream.Create(LPath,fmOpenReadWrite or fmShareExclusive); + LLease.Free; + end; +end; + +{$IFDEF MSWINDOWS} +procedure TBufferedAdoptionFixture.Performance(ALines: TStrings); +const Iterations = 1000; +var LStorage: TFileStorage; LOperations: TPCOperationsComp; LExpected: TStoredBlocks; LPath: String; LInput: TBytes; LHash: TRawBytes; LFrequency, LStart, LFinish, LChecksum: Int64; LSeconds: Double; I, J, LRepeat: Integer; +begin + LPath := IncludeTrailingPathDelimiter(FDirectory)+'performance.blocks'; + LStorage := TFileStorage.Create(nil); + LOperations := TPCOperationsComp.Create(nil); + try + LStorage.SetBlockChainFile(AnsiString(LPath)); + PopulateHistory(LStorage,LExpected); + LStorage.SetBlockChainFile(AnsiString(LPath)); + Require(LStorage.Initialize,'cannot open the history sample'); + LInput := FileBytes(LPath); + LHash := TCrypto.DoSha256(LInput); + ALines.Add('INPUT_BYTES='+IntToStr(Length(LInput))); + ALines.Add('INPUT_SHA256='+LHash.ToHexaString); + Require(QueryPerformanceFrequency(LFrequency),'QPC frequency unavailable'); + for LRepeat := 1 to 3 do begin + LChecksum := 0; + QueryPerformanceCounter(LStart); + for I := 1 to Iterations do + for J := 0 to 4 do begin + Require(LStorage.LoadBlockChainBlock(LOperations,J),'timed actual history load failed'); + Inc(LChecksum,LOperations.OperationBlock.block); + end; + QueryPerformanceCounter(LFinish); + LSeconds := LFinish-LStart; + LSeconds := LSeconds/LFrequency; + Require(LChecksum=Iterations*10,'timed history result checksum differs'); + ALines.Add('HISTORY_REPEAT='+IntToStr(LRepeat)+';LOADS='+IntToStr(Iterations*5)+';SECONDS='+FloatToStr(LSeconds)+';CHECKSUM='+IntToStr(LChecksum)); + end; + for J := 0 to 4 do begin + Require(LStorage.LoadBlockChainBlock(LOperations,J),'post-timing history validation failed'); + Require(SameBytes(LExpected[J],StoredBytes(LOperations)),'post-timing payload bytes differ'); + end; + Require(SameBytes(LInput,FileBytes(LPath)),'timed history reads changed input bytes'); + finally + LOperations.Free; + LStorage.Free; + end; +end; + +procedure TBufferedAdoptionFixture.PendingPerformance(ALines: TStrings); +const Iterations = 25; OperationCount = 128; +var LBank: TPCBank; LStorage: TFileStorage; LOriginal, LLoaded: TOperationsHashTree; LPath, LSharedSample, LError: String; LSample: TFileStream; LInput: TBytes; LHash: TRawBytes; LFrequency, LStart, LFinish, LChecksum: Int64; LSeconds: Double; I, LRepeat: Integer; +begin + LBank := TPCBank.Create(nil); + LStorage := nil; + LOriginal := TOperationsHashTree.Create; + LLoaded := TOperationsHashTree.Create; + try + SeedPendingBank(LBank,CT_PROTOCOL_5); + LStorage := TFileStorage.Create(nil); + LStorage.Bank := LBank; + LPath := IncludeTrailingPathDelimiter(TPCBank.GetStorageFolder(LBank.Orphan))+'pendingbuffer.ops'; + LSharedSample := SysUtils.GetEnvironmentVariable('BUFFERED_ADOPTION_PENDING_SAMPLE'); + Require(LSharedSample<>'','performance runner must provide one shared owned pending sample path'); + if FileExists(LSharedSample) then begin + LSample := TFileStream.Create(LSharedSample,fmOpenRead or fmShareDenyWrite); + try + Require(LOriginal.LoadOperationsHashTreeFromStream(LSample,True,LBank.SafeBox.CurrentProtocol,LBank.SafeBox.CurrentProtocol,nil,LError),'cannot decode the shared pending sample: '+LError); + finally + LSample.Free; + end; + Require(LOriginal.OperationsCount=OperationCount,'shared pending sample count changed'); + WriteBytes(LPath,FileBytes(LSharedSample)); + end else begin + // Generate once, then reuse these exact signed bytes for both compilers + // and both source versions; ECDSA signatures are otherwise nondeterministic. + FillPending(LOriginal,LBank.SafeBox.CurrentProtocol,OperationCount); + LStorage.SavePendingBufferOperations(LOriginal); + WriteBytes(LSharedSample,FileBytes(LPath)); + end; + LInput := FileBytes(LPath); + LHash := TCrypto.DoSha256(LInput); + ALines.Add('PENDING_PROTOCOL='+IntToStr(LBank.SafeBox.CurrentProtocol)); + ALines.Add('PENDING_INPUT_BYTES='+IntToStr(Length(LInput))); + ALines.Add('PENDING_INPUT_SHA256='+LHash.ToHexaString); + Require(QueryPerformanceFrequency(LFrequency),'QPC frequency unavailable'); + for LRepeat := 1 to 3 do begin + LChecksum := 0; + QueryPerformanceCounter(LStart); + for I := 1 to Iterations do begin + LLoaded.ClearHastThree; + LStorage.LoadPendingBufferOperations(LLoaded); + Inc(LChecksum,LLoaded.OperationsCount); + end; + QueryPerformanceCounter(LFinish); + LSeconds := LFinish-LStart; + LSeconds := LSeconds/LFrequency; + Require(LChecksum=Iterations*OperationCount,'timed pending operation count differs'); + ALines.Add('PENDING_REPEAT='+IntToStr(LRepeat)+';LOADS='+IntToStr(Iterations)+';OPERATIONS='+IntToStr(OperationCount)+';SECONDS='+FloatToStr(LSeconds)+';CHECKSUM='+IntToStr(LChecksum)); + end; + for I := 0 to OperationCount-1 do + Require(TBaseType.Equals(LOriginal.GetOperation(I).Sha256,LLoaded.GetOperation(I).Sha256),'timed pending operation digest differs'); + FreeAndNil(LStorage); + Require(SameBytes(LInput,FileBytes(LPath)),'timed pending loads changed saved bytes'); + finally + LLoaded.Free; + LOriginal.Free; + LStorage.Free; + LBank.Free; + end; +end; + +{$ENDIF} + +procedure CheckBufferedAdoption(AIndex: Integer); +var LFixture: TBufferedAdoptionFixture; +begin + LFixture := TBufferedAdoptionFixture.Create; + try + case AIndex of + 1: LFixture.Pending(CT_PROTOCOL_5,False); + 2: LFixture.Pending(CT_PROTOCOL_5,True); + 3: LFixture.History; + 4: LFixture.HeaderBoundary; + 5: LFixture.RandomTable(0); + 6: LFixture.RandomTable(1); + 7: LFixture.RandomTable(2); + 8: LFixture.RandomTable(3); + 9: LFixture.MalformedHistory; + 10: LFixture.Pending(CT_PROTOCOL_6,False); + 11: LFixture.Pending(CT_PROTOCOL_6,True); + else + raise EArgumentOutOfRangeException.Create('Invalid buffered-adoption check'); + end; + finally + LFixture.Free; + end; +end; + +function RunBufferedAdoptionChecks(const ALogPath: String): Integer; +var LLines: TStringList; I: Integer; +begin + Result := 0; + LLines := TStringList.Create; + try + for I := 1 to BufferedAdoptionCheckCount do begin + try + CheckBufferedAdoption(I); + LLines.Add('PASS '+IntToStr(I)+' '+AdoptionCheckName(I)); + except + on E: Exception do begin + Inc(Result); + LLines.Add('FAIL '+IntToStr(I)+' '+AdoptionCheckName(I)+' '+E.ClassName+': '+E.Message); + end; + end; + end; + {$IFDEF ASSUME_VALID_POW_OLD_PROTOCOLS} + LLines.Add('RANDOMHASH_ENTRY=shipping-early-return'); + {$ELSE} + LLines.Add('RANDOMHASH_ENTRY=table-entry-exercised'); + {$ENDIF} + if Result=0 then LLines.Add('PASS '+IntToStr(BufferedAdoptionCheckCount)); + LLines.SaveToFile(ALogPath); + finally + LLines.Free; + end; +end; + +function RunBufferedAdoptionPerformance(const ALogPath: String): Integer; +{$IFDEF MSWINDOWS} +var LLines: TStringList; LFixture: TBufferedAdoptionFixture; +begin + Result := 0; + LLines := TStringList.Create; + LFixture := TBufferedAdoptionFixture.Create; + try + try + LFixture.Performance(LLines); + LFixture.PendingPerformance(LLines); + LLines.Add('PASS actual history performance'); + except + on E: Exception do begin + Result := 1; + LLines.Add('FAIL '+E.ClassName+': '+E.Message); + end; + end; + LLines.SaveToFile(ALogPath); + finally + LFixture.Free; + LLines.Free; + end; +end; + +{$ELSE} +begin + raise Exception.Create('The optional QPC performance probe requires Windows.'); +end; +{$ENDIF} +end. diff --git a/src/tests/experimental/native/classic/ClassicWalletProbe.dpr b/src/tests/experimental/native/classic/ClassicWalletProbe.dpr new file mode 100644 index 000000000..f5c0eff98 --- /dev/null +++ b/src/tests/experimental/native/classic/ClassicWalletProbe.dpr @@ -0,0 +1,10 @@ +program ClassicWalletProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses UClassicWalletChecks; + +begin + ReportMemoryLeaksOnShutdown := True; + ExitCode := RunClassicWalletProbe; +end. diff --git a/src/tests/experimental/native/classic/ClassicWalletProbe.dproj b/src/tests/experimental/native/classic/ClassicWalletProbe.dproj new file mode 100644 index 000000000..dc09042db --- /dev/null +++ b/src/tests/experimental/native/classic/ClassicWalletProbe.dproj @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{3CC9A0A5-C9D4-4BD7-A144-96DFB022E722}</ProjectGuid> + <MainSource>ClassicWalletProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\gui-classic;..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex;..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_ResourcePath>..\..\..\..;..\..\..\..\gui-classic</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\classic-compatibility\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\classic-compatibility\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <!-- Match the existing Classic application's arithmetic settings. The + independent core probes exercise checked lock/transport behavior. --> + <DCC_RangeChecking>false</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>false</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UClassicWalletChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">ClassicWalletProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/classic/ClassicWalletProbe.lpi b/src/tests/experimental/native/classic/ClassicWalletProbe.lpi new file mode 100644 index 000000000..40401fdde --- /dev/null +++ b/src/tests/experimental/native/classic/ClassicWalletProbe.lpi @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<CONFIG> + <ProjectOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <General> + <Flags><MainUnitHasCreateFormStatements Value="False"/><MainUnitHasTitleStatement Value="False"/></Flags> + <SessionStorage Value="InProjectDir"/> + <MainUnit Value="0"/> + <Title Value="ClassicWalletProbe"/> + <ResourceType Value="res"/> + </General> + <BuildModes Count="1"><Item1 Name="Debug" Default="True"/></BuildModes> + <RequiredPackages Count="1"><Item1><PackageName Value="LCL"/></Item1></RequiredPackages> + <Units Count="1"><Unit0><Filename Value="ClassicWalletProbe.lpr"/><IsPartOfProject Value="True"/></Unit0></Units> + </ProjectOptions> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <Target><Filename Value="..\..\lib\classic-compatibility\FPC\ClassicWalletProbe"/></Target> + <SearchPaths> + <IncludeFiles Value="..\..\..\..\libraries\generics.collections\inc"/> + <OtherUnitFiles Value="..\..\..\..\gui-classic;..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex;..\..\..\..\libraries\generics.collections"/> + <UnitOutputDirectory Value="..\..\lib\classic-compatibility\FPC\units"/> + </SearchPaths> + <Parsing><SyntaxOptions><SyntaxMode Value="Delphi"/></SyntaxOptions></Parsing> + <CodeGeneration><Checks><RangeChecks Value="False"/><OverflowChecks Value="False"/></Checks></CodeGeneration> + <Linking><Debugging><GenerateDebugInfo Value="True"/><UseExternalDbgSyms Value="False"/></Debugging><Options><Win32GraphicApp Value="True"/></Options></Linking> + <Other><CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL"/></Other> + </CompilerOptions> +</CONFIG> diff --git a/src/tests/experimental/native/classic/ClassicWalletProbe.lpr b/src/tests/experimental/native/classic/ClassicWalletProbe.lpr new file mode 100644 index 000000000..1d872a14e --- /dev/null +++ b/src/tests/experimental/native/classic/ClassicWalletProbe.lpr @@ -0,0 +1,13 @@ +program ClassicWalletProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$MODE Delphi} + +uses heaptrc, Interfaces, UClassicWalletChecks; + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + ExitCode := RunClassicWalletProbe; +end. diff --git a/src/tests/experimental/native/classic/README.md b/src/tests/experimental/native/classic/README.md new file mode 100644 index 000000000..802583b47 --- /dev/null +++ b/src/tests/experimental/native/classic/README.md @@ -0,0 +1,37 @@ +# Classic GUI compatibility probe + +This Windows probe runs the classic wallet form, `TFRMWallet`, on five mainnet +blocks. It checks: + +- activation and local restore +- validated commits, committed-tip labels and block-grid rows +- key selection while idle and during catch-up +- native Close while an account filter is busy +- gettext cleanup of translated forms (Delphi only) + +Run the Lazarus/FPC probe from the repository root: + +```powershell +& src\tests\experimental\native\RunWalletRegressionTests.ps1 -Probe classic -OutputDirectory <new-directory> +``` + +`-FpcPath` and `-LazarusDirectory` select a Windows x64 FPC and Lazarus. + +Alternatively, build `ClassicWalletProbe.lpi` with Lazarus (LCL) or +`ClassicWalletProbe.dproj` with Delphi (VCL). Build and run each compiler and +architecture separately. + +1. Place the matching OpenSSL DLL beside the executable. +2. Set `PASCAL_CLASSIC_BLOCK_FIXTURE` to the full path of + `src/tests/experimental/fixtures/mainnet-blocks-0-4.bin`, the + [five-block fixture](../../fixtures/README.md). +3. Run the executable through the + [private-desktop broker](../README.md#private-desktop-broker), passing one + absolute log filename in a new folder. + +Exit code 0 means all checks passed and the form closed normally. Captures and +the Lazarus heap report go beside the log. + +The probe keeps its data in a new folder beside the log and never reads a user's +wallet or configuration. It disables outbound connections, RPC and mining, but +the classic wallet still listens on an ephemeral port. diff --git a/src/tests/experimental/native/classic/UClassicWalletChecks.pas b/src/tests/experimental/native/classic/UClassicWalletChecks.pas new file mode 100644 index 000000000..9ea0387e2 --- /dev/null +++ b/src/tests/experimental/native/classic/UClassicWalletChecks.pas @@ -0,0 +1,713 @@ +unit UClassicWalletChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ENDIF} + +interface + +function RunClassicWalletProbe: Integer; + +implementation + +uses + Windows, Messages, Classes, SysUtils, SyncObjs, Forms, Graphics, UFRMWallet, UGridUtils, + UAccounts, UBlockChain, UNode, UNetProtocol, UThread, UCrypto, UWallet, + USettings, UAppParams, UConst, UPCDataTypes, UBaseTypes; + +type + // Access only: Application creates the unchanged production form class and + // loads its real DFM/LFM. No renderer, loader or readiness method is replaced. + TClassicAccess = class(TFRMWallet); + TClassicAccounts = array of TAccount; + TClassicGridAccess = class(TAccountsGrid) + public + function ReadDisplayedAccount(AAccountNumber: Integer; out AAccount: TAccount; + out ABlocksCount, AAccountsCount: Integer): Boolean; + end; + TClassicChecks = class; + + TClassicBlockTask = class(TPCThread) + private + FChecks: TClassicChecks; + protected + procedure BCExecute; override; + public + Step, ContinueStep, Done: TEvent; + Completed: LongInt; + ErrorText: String; + constructor Create(AChecks: TClassicChecks); + destructor Destroy; override; + end; + + TClassicChecks = class(TComponent) + private + FLog: TStringList; + FLogFile, FDirectory, FOldDirectory, FExceptionText: String; + FPreviousException: TExceptionEvent; + FForm: TFRMWallet; + FNode: TNode; + FBlocks: array[0..4] of TPCOperationsComp; + FTask: TClassicBlockTask; + FActivationThread: TThread; + FActivationHandle: THandle; + FActivationDone: Boolean; + FPreviousTerminate: TNotifyEvent; + FSynchronizationPeer: TObject; + FEmptyWatchKey: TAccountKey; + FExpectedFirst, FExpectedSecond, FExpectedAll, FExpectedEmpty: TClassicAccounts; + procedure Log(const AText: String); + procedure Pump; + procedure Await(AEvent: TEvent; AMilliseconds: Cardinal; const AReason: String); + procedure OnException(Sender: TObject; E: Exception); + procedure OnActivationTerminated(Sender: TObject); + procedure LoadFixture; + procedure SeedOwnedSettings; + procedure CaptureActivation; + procedure ObserveActivation; + procedure JoinActivation; + procedure CheckNoUnexpectedForm; + procedure CheckFinalAccounts; + procedure SelectWatchKey(AKeyIndex: Integer); + procedure CheckDisplayedAccounts(const AExpected: TClassicAccounts; + const APhase: String; AWhileSynchronizing: Boolean); + procedure CheckKeySelectionDuringCatchup; + procedure Capture(const AName: String); + procedure CloseOwnedForm; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + constructor Create(const ALogFile: String); + destructor Destroy; override; + procedure Run; + end; + +procedure Require(ACondition: Boolean; const AReason: String); +begin + if not ACondition then raise Exception.Create(AReason); +end; + +function DesktopName(AHandle: THandle): String; +var LBuffer: array[0..255] of Char; LRequired: DWORD; +begin + if not GetUserObjectInformation(AHandle, UOI_NAME, @LBuffer[0], + SizeOf(LBuffer), LRequired) then RaiseLastOSError; + Result := StrPas(PChar(@LBuffer[0])); +end; + +function TClassicGridAccess.ReadDisplayedAccount(AAccountNumber: Integer; + out AAccount: TAccount; out ABlocksCount, AAccountsCount: Integer): Boolean; +begin + // Exercise the exact reader used by the unchanged production cell renderer. + Result := BufferGetAccount(AAccountNumber, AAccount, ABlocksCount, AAccountsCount); +end; + +constructor TClassicBlockTask.Create(AChecks: TClassicChecks); +begin + inherited Create(True); + FreeOnTerminate := False; + FChecks := AChecks; + Step := TEvent.Create(nil, True, False, ''); + ContinueStep := TEvent.Create(nil, False, False, ''); + Done := TEvent.Create(nil, True, False, ''); +end; + +destructor TClassicBlockTask.Destroy; +begin + Terminate; + ContinueStep.SetEvent; + WaitFor; + Done.Free; + ContinueStep.Free; + Step.Free; + inherited; +end; + +procedure TClassicBlockTask.BCExecute; +var I: Integer; LError: String; LOperations: TPCOperationsComp; +begin + try + try + for I := 0 to High(FChecks.FBlocks) do begin + if Terminated then Exit; + // As in the normal network decoder, validation needs a transaction + // bound to the actual current bank. The immutable file-only objects + // intentionally have no bank or validation shortcut attached. + LOperations := TPCOperationsComp.Create(FChecks.FNode.Bank); + try + LOperations.CopyFrom(FChecks.FBlocks[I]); + if not FChecks.FNode.AddNewBlockChain(nil, LOperations, LError) then + raise Exception.Create('Actual historic block ' + IntToStr(I) + + ' failed at local count=' + IntToStr(FChecks.FNode.Bank.BlocksCount) + ': ' + LError); + finally LOperations.Free; end; + InterlockedExchange(Completed, I + 1); + Step.SetEvent; + Require(ContinueStep.WaitFor(30000) = wrSignaled, + 'Classic UI did not release its committed-block gate'); + end; + except on E: Exception do ErrorText := E.ClassName + ': ' + E.Message + + ' address=' + IntToHex(NativeUInt(ExceptAddr), SizeOf(Pointer) * 2); end; + finally Done.SetEvent; end; +end; + +constructor TClassicChecks.Create(const ALogFile: String); +begin + inherited Create(nil); + FLogFile := ExpandFileName(ALogFile); + FLog := TStringList.Create; + FSynchronizationPeer := TObject.Create; + FOldDirectory := TNode.GetPascalCoinDataFolder; + FDirectory := IncludeTrailingPathDelimiter(ExtractFilePath(FLogFile)) + 'classic-owned-data'; + Require(not DirectoryExists(FDirectory), 'Refusing an existing Classic data directory'); + Require(ForceDirectories(FDirectory), 'Cannot create the owned Classic data directory'); + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TClassicChecks.Destroy; +var I: Integer; LUntil: UInt64; +begin + // Finish test-owned producers before the real form frees its node and bank. + CaptureActivation; + if Assigned(FTask) then begin + FTask.Terminate; + FTask.ContinueStep.SetEvent; + LUntil := GetTickCount64 + 15000; + while (FTask.Done.WaitFor(0) <> wrSignaled) and (GetTickCount64 < LUntil) do begin + CheckSynchronize(1); + Application.ProcessMessages; + end; + FTask.Free; + end; + if Assigned(FActivationThread) and not FActivationDone then + FActivationThread.Terminate; + JoinActivation; + if Assigned(FForm) then begin + if Assigned(FNode) then FNode.SetSynchronizationNetworkActive(False); + FForm.Free; + FRMWallet := nil; + end; + if FActivationHandle <> 0 then CloseHandle(FActivationHandle); + Application.OnException := FPreviousException; + for I := Low(FBlocks) to High(FBlocks) do FBlocks[I].Free; + FSynchronizationPeer.Free; + TNode.SetPascalCoinDataFolder(FOldDirectory); + FLog.Free; + inherited; +end; + +procedure TClassicChecks.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited; + if (Operation = opRemove) and (AComponent = FForm) then FForm := nil; +end; + +procedure TClassicChecks.Log(const AText: String); +begin + FLog.Add(UIntToStr(GetTickCount64) + ' ' + AText); + FLog.SaveToFile(FLogFile); +end; + +procedure TClassicChecks.OnException(Sender: TObject; E: Exception); +{$IFDEF FPC}var I: Integer;{$ENDIF} +begin + FExceptionText := E.ClassName + ': ' + E.Message; + Log('APPLICATION_EXCEPTION ' + FExceptionText + ' address=' + IntToHex(NativeUInt(ExceptAddr), SizeOf(Pointer)*2)); + {$IFDEF FPC} + Log(BackTraceStrFunc(ExceptAddr)); + for I := 0 to ExceptFrameCount - 1 do Log(BackTraceStrFunc(ExceptFrames[I])); + {$ENDIF} +end; + +procedure TClassicChecks.CheckNoUnexpectedForm; +var I: Integer; +begin + for I := 0 to Screen.FormCount - 1 do + Require((Screen.Forms[I] = FForm) or not Screen.Forms[I].Visible, + 'Unexpected visible Classic dialog: ' + Screen.Forms[I].ClassName); +end; + +procedure TClassicChecks.Pump; +begin + CaptureActivation; + try + CheckSynchronize(1); + Application.ProcessMessages; + finally CaptureActivation; end; + Require(FExceptionText = '', 'Classic application raised ' + FExceptionText); + CheckNoUnexpectedForm; + Sleep(1); +end; + +procedure TClassicChecks.Await(AEvent: TEvent; AMilliseconds: Cardinal; + const AReason: String); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + Pump; + if AEvent.WaitFor(0) = wrSignaled then Exit; + if Assigned(FTask) and (FTask.Done.WaitFor(0) = wrSignaled) then + raise Exception.Create(AReason + '; worker=' + FTask.ErrorText); + until GetTickCount64 >= LUntil; + if Assigned(FTask) then + raise Exception.Create(AReason + '; worker=' + FTask.ErrorText) + else raise Exception.Create(AReason); +end; + +procedure TClassicChecks.LoadFixture; +var LFile: TFileStream; LCount: Cardinal; I: Integer; LError, LFileName: String; +begin + LFileName := ExpandFileName(GetEnvironmentVariable('PASCAL_CLASSIC_BLOCK_FIXTURE')); + Require(FileExists(LFileName), 'Supply the public five-block fixture via PASCAL_CLASSIC_BLOCK_FIXTURE'); + LFile := TFileStream.Create(LFileName, fmOpenRead or fmShareDenyNone); + try + LFile.ReadBuffer(LCount, SizeOf(LCount)); + Require(LCount = 5, 'Expected the five provenance-checked historic mainnet blocks'); + for I := Low(FBlocks) to High(FBlocks) do begin + FBlocks[I] := TPCOperationsComp.Create(nil); + Require(FBlocks[I].LoadBlockFromStream(LFile, LError), LError); + Require(FBlocks[I].OperationBlock.block = Cardinal(I), 'Historic block order differs'); + end; + Require(LFile.Position = LFile.Size, 'Unexpected historic fixture suffix'); + finally LFile.Free; end; +end; + +procedure TClassicChecks.SeedOwnedSettings; +var LParams: TAppParams; LKeys: TWalletKeysExt; LGeneratedKey: TECPrivateKey; + I: Integer; +begin + // The real FormCreate calls TSettings.Load once. Seed its own file directly; + // never invoke the experimental CLI parser or the user's default data path. + TNode.SetPascalCoinDataFolder(FDirectory); + LParams := TAppParams.Create(nil); + try + LParams.FileName := IncludeTrailingPathDelimiter(FDirectory) + 'AppParams.prm'; + LParams.ParamByName[CT_PARAM_FirstTime].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_InternetServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_JSONRPCEnabled].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerActive].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_TryToConnectOnlyWithThisFixedServers].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_PeerCache].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_ShowModalMessages].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_SaveLogFiles].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_MinerPrivateKeyType].SetAsInteger(Ord(mpk_Selected)); + finally LParams.Free; end; + // Derive an unowned public key in memory, then discard its private material + // before the actual wallet is constructed. Persist only its public half. + LGeneratedKey := TECPrivateKey.Create; + try + LGeneratedKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FEmptyWatchKey := LGeneratedKey.PublicKey.GetCopy; + for I := Low(FBlocks) to High(FBlocks) do + Require(not TAccountComp.EqualAccountKeys(FEmptyWatchKey, + FBlocks[I].OperationBlock.account_key), + 'The empty public watch key must not own a fixture account'); + finally + LGeneratedKey.Free; + end; + LKeys := TWalletKeysExt.Create(nil); + try + LKeys.WalletFileName := IncludeTrailingPathDelimiter(FDirectory) + 'WalletKeys.dat'; + Require(not TAccountComp.EqualAccountKeys(FBlocks[0].OperationBlock.account_key, + FBlocks[2].OperationBlock.account_key), 'Fixture watch keys must have distinct owners'); + LKeys.AddPublicKey('Historic public watch key A', FBlocks[0].OperationBlock.account_key); + LKeys.AddPublicKey('Historic public watch key B', FBlocks[2].OperationBlock.account_key); + LKeys.AddPublicKey('Generated empty public watch key', FEmptyWatchKey); + Require(LKeys.Count = 3, 'Fixture must contain three distinct public watch keys'); + for I := 0 to LKeys.Count - 1 do + Require(not LKeys[I].HasPrivateKey, 'Fixture must persist no private key'); + finally LKeys.Free; end; + TNetData.NetData.NetConnectionsActive := False; +end; + +procedure TClassicChecks.OnActivationTerminated(Sender: TObject); +begin + FActivationDone := True; + FActivationThread := nil; + if Assigned(FPreviousTerminate) then FPreviousTerminate(Sender); +end; + +procedure TClassicChecks.CaptureActivation; +var LThread: TThread; +begin + if (FActivationHandle <> 0) or Assigned(FActivationThread) or + FActivationDone or not Assigned(FForm) then Exit; + LThread := TThread(TClassicAccess(FForm).FThreadActivate); + if not Assigned(LThread) then Exit; + // Capture as soon as Show/activation exposes the worker, before readiness + // assertions; Show and every pump also capture in finally. The actual + // fresh Classic loader synchronizes its initialization text before reading + // storage, then waits at <=1 block. No fixture producer has started yet. + // Even an initialization failure after that point must finish before the + // real FormDestroy can free its node. Preserve FreeOnTerminate throughout. + Require(LThread.FreeOnTerminate, 'Classic activation ownership changed'); + FPreviousTerminate := LThread.OnTerminate; + LThread.OnTerminate := OnActivationTerminated; + FActivationThread := LThread; + Require(DuplicateHandle(GetCurrentProcess, LThread.Handle, GetCurrentProcess, + @FActivationHandle, SYNCHRONIZE, False, 0), 'Cannot retain the activation completion handle'); +end; + +procedure TClassicChecks.ObserveActivation; +var LUntil: UInt64; +begin + CaptureActivation; + LUntil := GetTickCount64 + 20000; + repeat + Pump; + FNode := TClassicAccess(FForm).FNode; + if Assigned(FNode) and FNode.NetServer.Active then Break; + until GetTickCount64 >= LUntil; + Require(Assigned(FNode) and FNode.NetServer.Active, + 'Actual Classic activation did not finish local restore and start its listener'); + Require(FNode.Bank.BlocksCount = 0, 'Fresh Classic restore was not empty'); + Require(FActivationHandle <> 0, 'The actual Classic activation worker was not observed'); + Require(not TNetData.NetData.NetConnectionsActive, 'Classic activation enabled outbound contacts'); + Require(FNode.NetServer.Port = 0, 'Classic listener did not retain its ephemeral port policy'); + Log('READY actual Classic local restore; outbound disabled; ephemeral listener; real activation worker observed'); +end; + +procedure TClassicChecks.JoinActivation; +var LUntil: UInt64; +begin + if (FActivationHandle = 0) and not Assigned(FActivationThread) then Exit; + LUntil := GetTickCount64 + 10000; + repeat + CheckSynchronize(1); + Application.ProcessMessages; + if FActivationHandle <> 0 then begin + if WaitForSingleObject(FActivationHandle, 0) = WAIT_OBJECT_0 then Exit; + end else if FActivationDone then Exit; + Sleep(1); + until GetTickCount64 >= LUntil; + raise Exception.Create('Actual Classic activation worker did not finish cooperatively'); +end; + +procedure TClassicChecks.CheckFinalAccounts; +var I, LIndex: Integer; LAccount: TAccount; +begin + // Capture the confirmed oracle after the real producer has completed. No + // fixture account, balance or key membership is supplied to the renderer. + for I := 0 to FNode.Bank.AccountsCount - 1 do begin + LAccount := FNode.Bank.SafeBox.Account(I); + if TAccountComp.EqualAccountKeys(LAccount.accountInfo.accountKey, + FBlocks[0].OperationBlock.account_key) then begin + LIndex := Length(FExpectedFirst); + SetLength(FExpectedFirst, LIndex + 1); + FExpectedFirst[LIndex] := LAccount; + end else if TAccountComp.EqualAccountKeys(LAccount.accountInfo.accountKey, + FBlocks[2].OperationBlock.account_key) then begin + LIndex := Length(FExpectedSecond); + SetLength(FExpectedSecond, LIndex + 1); + FExpectedSecond[LIndex] := LAccount; + end else Continue; + LIndex := Length(FExpectedAll); + SetLength(FExpectedAll, LIndex + 1); + FExpectedAll[LIndex] := LAccount; + end; + Require(Length(FExpectedFirst) = CT_AccountsPerBlock, + 'Public watch key A must own its five actual historic accounts'); + Require(Length(FExpectedSecond) = 2 * CT_AccountsPerBlock, + 'Public watch key B must own ten different actual historic accounts'); + Require(Length(FExpectedAll) = 3 * CT_AccountsPerBlock, + 'The two public watch keys must have disjoint confirmed account sets'); + FForm.PageControl.ActivePage := FForm.tsMyAccounts; + FForm.PageControlChange(FForm.PageControl); + Require(FForm.cbExploreMyAccounts.Checked, 'Classic must initially filter its own watch keys'); + Require(FForm.WalletKeys.Count = 3, 'Ordinary Classic startup created another wallet key'); + for I := 0 to FForm.WalletKeys.Count - 1 do + Require(not FForm.WalletKeys[I].HasPrivateKey, + 'Classic fixture unexpectedly contains private material'); + SelectWatchKey(0); + CheckDisplayedAccounts(FExpectedFirst, 'idle A', False); + SelectWatchKey(1); + CheckDisplayedAccounts(FExpectedSecond, 'idle B', False); + SelectWatchKey(0); + CheckDisplayedAccounts(FExpectedFirst, 'idle A again', False); + SelectWatchKey(2); + CheckDisplayedAccounts(FExpectedEmpty, 'idle empty watch key', False); + SelectWatchKey(-1); + CheckDisplayedAccounts(FExpectedAll, 'idle All', False); + Capture('classic-accounts.bmp'); +end; + +procedure TClassicChecks.SelectWatchKey(AKeyIndex: Integer); +var I, LWalletIndex: Integer; LKey: TAccountKey; +begin + Require(Assigned(FForm.cbMyPrivateKeys.OnChange), + 'Classic key dropdown must retain its actual production change handler'); + if AKeyIndex < 0 then begin + FForm.cbMyPrivateKeys.ItemIndex := 0; + end else begin + case AKeyIndex of + 0: LKey := FBlocks[0].OperationBlock.account_key; + 1: LKey := FBlocks[2].OperationBlock.account_key; + 2: LKey := FEmptyWatchKey; + else + raise Exception.Create('Unknown Classic fixture watch key'); + end; + LWalletIndex := FForm.WalletKeys.IndexOfAccountKey(LKey); + Require(LWalletIndex >= 0, 'The requested public fixture owner is absent from the wallet'); + I := FForm.cbMyPrivateKeys.Items.IndexOfObject(TObject(PtrInt(LWalletIndex))); + Require(I > 0, 'The actual Classic dropdown omitted a public fixture owner'); + FForm.cbMyPrivateKeys.ItemIndex := I; + end; + FForm.cbMyPrivateKeys.OnChange(FForm.cbMyPrivateKeys); +end; + +procedure TClassicChecks.CheckDisplayedAccounts(const AExpected: TClassicAccounts; + const APhase: String; AWhileSynchronizing: Boolean); +var LGrid: TAccountsGrid; LUntil: UInt64; I, LBlocks, LAccounts, LExpectedRows: Integer; + LMatches: Boolean; LAccount: TAccount; LBalance: Int64; +begin + LGrid := TClassicAccess(FForm).FAccountsGrid; + LBalance := 0; + // Keep the expectation in explicit Int64 arithmetic on both native targets. + for I := 0 to High(AExpected) do + LBalance := LBalance + Int64(AExpected[I].balance); + LExpectedRows := Length(AExpected) + 1; + if Length(AExpected) = 0 then LExpectedRows := 2; + LUntil := GetTickCount64 + 10000; + repeat + Pump; + if AWhileSynchronizing then begin + Require(FNode.GetSynchronizationStatus.State = nssSynchronizing, + APhase + ': catch-up ended before the selection was displayed'); + Require(not FNode.PendingOperationsAvailable, + APhase + ': pending data became available before the selection was displayed'); + end; + LMatches := (AWhileSynchronizing or FNode.PendingOperationsAvailable) and + not LGrid.IsUpdatingData and + (LGrid.AccountsCount = Length(AExpected)) and + (FForm.dgAccounts.RowCount = LExpectedRows) and + (FForm.lblAccountsCount.Caption = IntToStr(Length(AExpected))) and + (FForm.lblAccountsBalance.Caption = TAccountComp.FormatMoney(LBalance)); + if LMatches then + for I := 0 to High(AExpected) do + if LGrid.AccountNumber(I + 1) <> AExpected[I].account then begin + LMatches := False; + Break; + end; + if LMatches then Break; + until GetTickCount64 >= LUntil; + Require(LMatches, APhase + ': actual Classic selection/totals did not publish; expected=' + + IntToStr(Length(AExpected)) + ' actual=' + IntToStr(LGrid.AccountsCount) + + ' count_label=' + FForm.lblAccountsCount.Caption + + ' balance_label=' + FForm.lblAccountsBalance.Caption); + Require(LGrid.AccountsBalance = LBalance, APhase + ': selected confirmed balance differs'); + if Length(AExpected) = 0 then + Require(LGrid.AccountNumber(1) < 0, + APhase + ': an empty watch key must not fall back to all wallet accounts'); + for I := 0 to High(AExpected) do begin + LAccount := CT_Account_NUL; + Require(TClassicGridAccess(LGrid).ReadDisplayedAccount(AExpected[I].account, + LAccount, LBlocks, LAccounts), + APhase + ': actual cell reader withheld account data while catch-up was active'); + Require((LAccount.account = AExpected[I].account) and + (LAccount.balance = AExpected[I].balance) and + (LAccount.n_operation = AExpected[I].n_operation) and + TAccountComp.EqualAccountInfos(LAccount.accountInfo, AExpected[I].accountInfo), + APhase + ': actual cell reader returned another owner, balance or account record'); + Require((LBlocks = FNode.Bank.BlocksCount) and (LAccounts = FNode.Bank.AccountsCount), + APhase + ': actual cell reader published stale confirmed-tip metadata'); + end; + FForm.dgAccounts.Invalidate; + FForm.dgAccounts.Update; + Log('PASS ' + APhase + ' exact_ids=' + IntToStr(Length(AExpected)) + + ' balance=' + TAccountComp.FormatMoney(LBalance) + + ' pending_available=' + BoolToStr(FNode.PendingOperationsAvailable, True)); +end; + +procedure TClassicChecks.CheckKeySelectionDuringCatchup; +var LUntil: UInt64; +begin + // A selected remote target exercises the actual deferred-pending state. The + // peer is an owned identity only: transport remains disabled throughout. + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FSynchronizationPeer, True); + FNode.SetSynchronizationTarget(FSynchronizationPeer, FNode.Bank.BlocksCount + 20); + try + Require((FNode.GetSynchronizationStatus.State = nssSynchronizing) and + not FNode.PendingOperationsAvailable, 'Catch-up must make the pending overlay unavailable'); + Require(not TNetData.NetData.NetConnectionsActive, + 'A typed catch-up fixture must not enable outbound connections'); + // Existing rows must still paint complete confirmed records, rather than + // going blank because their pending overlay is temporarily unavailable. + CheckDisplayedAccounts(FExpectedAll, 'catch-up existing All', True); + SelectWatchKey(0); + CheckDisplayedAccounts(FExpectedFirst, 'catch-up A', True); + SelectWatchKey(1); + CheckDisplayedAccounts(FExpectedSecond, 'catch-up B', True); + SelectWatchKey(0); + CheckDisplayedAccounts(FExpectedFirst, 'catch-up A again', True); + SelectWatchKey(2); + CheckDisplayedAccounts(FExpectedEmpty, 'catch-up empty watch key', True); + SelectWatchKey(-1); + CheckDisplayedAccounts(FExpectedAll, 'catch-up All', True); + // No message pump separates these requests. Only the last captured owner + // may remain after canceled/retired workers publish their completions. + SelectWatchKey(0); + SelectWatchKey(1); + SelectWatchKey(0); + CheckDisplayedAccounts(FExpectedFirst, 'catch-up rapid A/B/A', True); + Capture('classic-accounts-catchup.bmp'); + finally + FNode.SetSynchronizationNetworkActive(False); + end; + LUntil := GetTickCount64 + 10000; + repeat + Pump; + if FNode.PendingOperationsAvailable then Break; + until GetTickCount64 >= LUntil; + Require(FNode.PendingOperationsAvailable, 'Classic pending overlay did not reconcile after catch-up'); + CheckDisplayedAccounts(FExpectedFirst, 'reconciled latest A', False); +end; + +procedure TClassicChecks.Capture(const AName: String); +var LBitmap: TBitmap; +begin + FForm.Update; + LBitmap := FForm.GetFormImage; + try + Require((LBitmap.Width > 0) and (LBitmap.Height > 0), 'Classic capture is empty'); + LBitmap.SaveToFile(IncludeTrailingPathDelimiter(ExtractFilePath(FLogFile)) + AName); + finally LBitmap.Free; end; +end; + +procedure TClassicChecks.CloseOwnedForm; +var LUntil: UInt64; +begin + Require(Assigned(FForm), 'Classic form is already absent before native Close'); + Require(PostMessage(FForm.Handle, WM_CLOSE, 0, 0), 'Cannot post native Classic Close'); + LUntil := GetTickCount64 + 10000; + repeat + Pump; + if Application.Terminated then Break; + until GetTickCount64 >= LUntil; + Require(Application.Terminated, 'Actual Classic main-form Close did not terminate the application'); + // Classic has no confirmation in its existing close flow. Freeing the actual + // form runs its unchanged FormDestroy and owned node/network shutdown. + FForm.Free; + FRMWallet := nil; + Require(not TNode.NodeExists and not TNetData.NetDataExists, + 'Classic FormDestroy left its owned core singletons alive'); + Require(FExceptionText = '', 'Classic close raised ' + FExceptionText); + Log('PASS native Classic Close and unchanged FormDestroy completed; no confirmation invented'); +end; + +procedure TClassicChecks.Run; +var I: Integer; LUntil: UInt64; LExpectedLabel: String; +begin + Require(not TNode.NodeExists and not TNetData.NetDataExists, 'Classic probe requires fresh core singletons'); + TCrypto.InitCrypto; + LoadFixture; + SeedOwnedSettings; + Application.CreateForm(TFRMWallet, FRMWallet); + FForm := FRMWallet; + FForm.FreeNotification(Self); + {$IFNDEF FPC} + if FForm.ClassType <> TFRMWallet then begin + // Translation must hook BeforeDestruction itself. Patching another VMT + // slot leaves freed controls registered for unsafe finalization reads. + Require(PPointer(NativeInt(FForm.ClassType) + vmtBeforeDestruction)^ <> + PPointer(NativeInt(TFRMWallet) + vmtBeforeDestruction)^, + 'Translated Classic form did not hook its actual BeforeDestruction slot'); + Log('PASS native translated Classic destruction hook'); + end; + {$ENDIF} + try FForm.Show; + finally CaptureActivation; end; + ObserveActivation; + FTask := TClassicBlockTask.Create(Self); + FTask.Start; + for I := 0 to 4 do begin + Await(FTask.Step, 20000, 'Historic block task did not reach its next atomic commit'); + Require(FTask.ErrorText = '', FTask.ErrorText); + Require(InterlockedCompareExchange(FTask.Completed, 0, 0) = I + 1, + 'Historic block task skipped a commit gate'); + Require(FNode.Bank.BlocksCount = Cardinal(I + 1), 'Classic core committed height differs'); + if I >= 1 then begin + LUntil := GetTickCount64 + 10000; + repeat + Pump; + if FForm.PageControl.Enabled and FActivationDone then Break; + until GetTickCount64 >= LUntil; + Require(FForm.PageControl.Enabled and FActivationDone, + 'The real Classic local-load worker did not publish its loaded form'); + if I = 1 then begin + JoinActivation; + // Isolate actual block notifications for the later commits. The Classic + // production timer policy is unchanged; it is disabled only in this test. + FForm.TimerUpdateStatus.Enabled := False; + FForm.PageControl.ActivePage := FForm.tsBlockChain; + FForm.PageControlChange(FForm.PageControl); + end; + if I >= 2 then begin + LExpectedLabel := IntToStr(I + 1) + ' (0..' + IntToStr(I) + ')'; + LUntil := GetTickCount64 + 5000; + repeat + Pump; + if (FForm.lblCurrentBlock.Caption = LExpectedLabel) and + (FForm.dgBlockChainExplorer.RowCount = I + 2) then Break; + until GetTickCount64 >= LUntil; + Require(FForm.lblCurrentBlock.Caption = LExpectedLabel, + 'Classic committed block label did not update through its real Node observer'); + Require(FForm.dgBlockChainExplorer.RowCount = I + 2, + 'Classic real block-grid worker did not publish the committed rows'); + end; + end; + Require(not TNetData.NetData.NetConnectionsActive, 'Classic smoke enabled outbound networking'); + Log('COMMIT block=' + IntToStr(I) + ' core_blocks=' + IntToStr(FNode.Bank.BlocksCount) + + ' label=' + FForm.lblCurrentBlock.Caption + ' block_grid_rows=' + IntToStr(FForm.dgBlockChainExplorer.RowCount)); + FTask.Step.ResetEvent; + FTask.ContinueStep.SetEvent; + end; + Await(FTask.Done, 10000, 'Classic historic block task did not finish'); + Require(FTask.ErrorText = '', FTask.ErrorText); + FreeAndNil(FTask); + Require(FNode.Bank.BlocksCount = 5, 'Classic smoke did not preserve all five actual valid blocks'); + Require(FNode.Bank.AccountsCount = 5 * CT_AccountsPerBlock, 'Classic smoke account count differs'); + Require(TNetData.NetData.NetStatistics.ActiveConnections = 0, 'Classic smoke contacted a peer'); + Require(not TClassicAccess(FForm).FPoolMiningServer.Active and + not TSettings.JsonRpcPortEnabled, 'Classic smoke started an unrelated service'); + Capture('classic-blocks.bmp'); + CheckFinalAccounts; + CheckKeySelectionDuringCatchup; + // Queue a real asynchronous filter and close immediately. Native destruction + // must join any active/retiring reader before releasing its node and wallet. + SelectWatchKey(1); + Require(TClassicAccess(FForm).FAccountsGrid.IsUpdatingData, + 'The native Close regression must retain an active or retiring account filter'); + Log('CLOSE filter_request_pending=' + + BoolToStr(TClassicAccess(FForm).FAccountsGrid.IsUpdatingData, True)); + CloseOwnedForm; + Log('RESULT 0 Classic actual-form, five valid blocks, three public watch keys, idle/catch-up selection, cell data and natural Close'); +end; + +function RunClassicWalletProbe: Integer; +var LInput: THandle; LChecks: TClassicChecks; +begin + Result := 1; + if ParamCount <> 1 then Exit; + LInput := OpenInputDesktop(0, False, DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + Require(not SameText(DesktopName(GetThreadDesktop(GetCurrentThreadId)), DesktopName(LInput)), + 'Run the Classic smoke through the owned private-desktop broker'); + finally CloseDesktop(LInput); end; + Require(not FileExists(ParamStr(1)), 'Refusing an existing Classic probe log'); + Application.Initialize; + Application.MainFormOnTaskbar := True; + Application.Title := 'Classic compatibility native probe'; + LChecks := TClassicChecks.Create(ParamStr(1)); + try + try + LChecks.Run; + Result := 0; + except on E: Exception do LChecks.Log('FAIL ' + E.ClassName + ': ' + E.Message); end; + finally LChecks.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/common/DataRowVariantProbe.dpr b/src/tests/experimental/native/common/DataRowVariantProbe.dpr new file mode 100644 index 000000000..7f48a297b --- /dev/null +++ b/src/tests/experimental/native/common/DataRowVariantProbe.dpr @@ -0,0 +1,241 @@ +program DataRowVariantProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. } + +{$APPTYPE CONSOLE} + +uses + Winapi.Windows, System.SysUtils, System.Classes, System.Variants, + System.Generics.Collections, Vcl.Forms, Vcl.Controls, + UCommon, UCommon.Data, UVisualGrid, UWizardStringChecks; + +type + TRowSchema = class end; + + TNativeRows = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + function GetItemField(const AItem: Integer; const ABindingName: String): Variant; override; + end; + + TUnicodeRows = class(TCustomDataSource<Integer>) + protected + function GetColumns: TDataColumns; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + function GetItemField(const AItem: Integer; const ABindingName: String): Variant; override; + end; + +function UnicodeBinding(ASecond: Boolean): String; +begin + if ASecond then Result := #$65E5#$672C#$D83D#$DE80 + else Result := #$4E2D#$6587#$D83D#$DE80; +end; + +function UnicodePayload: String; +begin + Result := #$03A9#$0416#$6F22#$D83D#$DE80; +end; + +function TUnicodeRows.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From(UnicodeBinding(False), True), + TDataColumn.From(UnicodeBinding(True))); +end; + +procedure TUnicodeRows.FetchAll(const AContainer: TList<Integer>); +begin + AContainer.Add(2); + AContainer.Add(1); +end; + +function TUnicodeRows.GetItemField(const AItem: Integer; const ABindingName: String): Variant; +begin + if ABindingName = UnicodeBinding(False) then Result := AItem + else if ABindingName = UnicodeBinding(True) then Result := UnicodePayload + IntToStr(AItem) + else raise Exception.Create('Unicode binding changed before native field lookup'); +end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +function TNativeRows.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Value', True), + TDataColumn.From('Title')); +end; + +procedure TNativeRows.FetchAll(const AContainer: TList<Integer>); +begin + AContainer.Add(17); + AContainer.Add(29); +end; + +function TNativeRows.GetItemField(const AItem: Integer; const ABindingName: String): Variant; +begin + if ABindingName = 'Value' then Result := AItem + else if ABindingName = 'Title' then Result := 'row-' + IntToStr(AItem) + else raise Exception.Create('Unexpected native probe column'); +end; + +procedure CheckPositionalRows(ALog: TStrings); +var LRow, LCopy, LTemporary: Variant; LIndex, LImplicitInteger: Integer; +begin + LRow := TDataRow.New(PtrInt(TRowSchema), TDataColumns.Create( + TDataColumn.From('Value', True), TDataColumn.From('Title'))); + LRow.Value := 17; + LRow.Title := 'saved'; + LRow.__KEY := 29; + + Require(Integer(LRow._(0)) = 17, 'Explicit Integer positional conversion failed'); + ALog.Add('PASS explicit Integer(Row._(0))'); + LTemporary := LRow._(0); + Require(VarIsNumeric(LTemporary) and (Integer(LTemporary) = 17), + 'Positional Variant temporary failed'); + ALog.Add('PASS Variant temporary := Row._(0)'); + LImplicitInteger := LRow._(0); + Require(LImplicitInteger = 17, 'Implicit positional Integer assignment failed'); + ALog.Add('PASS implicit Integer assignment := Row._(0)'); + LIndex := 0; + Require(Integer(LRow._(LIndex)) = 17, 'Runtime Integer positional index failed'); + LIndex := 1; + Require(String(LRow._(LIndex)) = 'saved', 'Runtime index selected the wrong column'); + ALog.Add('PASS runtime Integer index and text positional value'); + + Require((Integer(LRow.Value) = 17) and (String(LRow.Title) = 'saved'), + 'Named columns changed during positional access'); + Require((Integer(LRow.__KEY) = 29) and (Integer(LRow._(2)) = 29), + 'Independent key slot must follow declared columns'); + LRow.__KEY := 31; + Require((Integer(LRow.Value) = 17) and (Integer(LRow._(0)) = 17), + 'Changing the key changed the first declared column'); + ALog.Add('PASS named columns and independent positional key slot'); + + LCopy := LRow; + LCopy.Value := 71; + LCopy.Title := 'copied'; + LCopy.__KEY := 97; + Require((Integer(LRow._(0)) = 17) and (String(LRow._(1)) = 'saved') and + (Integer(LRow.__KEY) = 31), 'Variant copy modified the original row'); + Require((Integer(LCopy._(0)) = 71) and (String(LCopy._(1)) = 'copied') and + (Integer(LCopy.__KEY) = 97), 'Copied row values did not round trip'); + ALog.Add('PASS independent copied row values and key'); +end; + +procedure CheckNativeGridCells(ALog: TStrings); +var LForm: TForm; LSource: TNativeRows; LGrid: TVisualGrid; +begin + LForm := TForm.CreateNew(nil); + try + LForm.SetBounds(80, 90, 640, 440); + LSource := TNativeRows.Create(LForm); + LGrid := TVisualGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.FetchDataInThread := False; + LGrid.AutoPageSize := False; + LGrid.PageSize := 2; + LGrid.AddColumn('Value').Binding := 'Value'; + LGrid.AddColumn('Title').Binding := 'Title'; + LGrid.DataSource := LSource; + Require(LGrid.RowCount = 2, 'Native grid did not fetch its two fixture rows'); + Require((Integer(LGrid.Cells[0, 0]) = 17) and + (String(LGrid.Cells[1, 0]) = 'row-17') and + (Integer(LGrid.Cells[0, 1]) = 29) and + (String(LGrid.Cells[1, 1]) = 'row-29'), + 'Actual TVisualGrid.Cells positional getter failed'); + Require((Integer(LGrid.Rows[0].__KEY) = 17) and + (Integer(LGrid.Rows[1].__KEY) = 29), 'Native grid source keys changed'); + ALog.Add('PASS actual synchronous VCL TVisualGrid.Cells columns/rows and keys'); + finally + LForm.Free; + end; +end; + +procedure CheckUnicodeBindingsAndCells(ALog: TStrings); +var + LForm: TForm; + LSource: TUnicodeRows; + LGrid: TVisualGrid; + LColumn: TVisualColumn; + LRow, LCopy: Variant; + LData, LCopyData: TDataRowData; +begin + LForm := TForm.CreateNew(nil); + try + LForm.SetBounds(80, 90, 640, 440); + LSource := TUnicodeRows.Create(LForm); + LGrid := TVisualGrid.Create(LForm); + LGrid.Parent := LForm; + LGrid.Align := alClient; + LGrid.FetchDataInThread := False; + LGrid.AutoPageSize := False; + LGrid.PageSize := 2; + LGrid.SortMode := smNone; + LColumn := LGrid.AddColumn(UnicodeBinding(False)); + LColumn.Binding := UnicodeBinding(False); + LColumn.SortBinding := UnicodeBinding(False); + LColumn.DisplayBinding := UnicodeBinding(False); + LGrid.AddColumn(UnicodeBinding(True)).Binding := UnicodeBinding(True); + Require((LColumn.Name = UnicodeBinding(False)) and + (LColumn.Binding = UnicodeBinding(False)) and + (LColumn.SortBinding = UnicodeBinding(False)) and + (LColumn.DisplayBinding = UnicodeBinding(False)), + 'Native String column name or bindings lost Unicode characters'); + LGrid.DataSource := LSource; + Require((LGrid.RowCount = 2) and (Integer(LGrid.Cells[0, 0]) = 2) and + (String(LGrid.Cells[1, 0]) = UnicodePayload + '2'), + 'Actual Unicode-bound native grid values failed'); + LRow := LGrid.Rows[0]; + LData := TDataRowData(LRow); + Require(LData.HasData(UnicodeBinding(False)) and LData.HasData(UnicodeBinding(True)), + 'Unicode column map lost distinct names'); + Require((Integer(LData[UnicodeBinding(False)]) = 2) and + (String(LData[UnicodeBinding(True)]) = UnicodePayload + '2') and + (Integer(LData['__KEY']) = 2), 'Unicode-bound row fields/key failed'); + LCopy := LRow; + LCopyData := TDataRowData(LCopy); + LCopyData[UnicodeBinding(True)] := UnicodePayload + ' copy'; + Require((String(LData[UnicodeBinding(True)]) = UnicodePayload + '2') and + (String(LCopyData[UnicodeBinding(True)]) = UnicodePayload + ' copy'), + 'Copied Unicode-bound row changed the original'); + ALog.Add('PASS native String Unicode column names/bindings and actual VCL grid cells'); + ALog.Add('PASS Unicode row map/value/key and independent copied row'); + finally + LForm.Free; + end; +end; + +var LLog: TStringList; LLogPath: String; +begin + if ParamCount > 1 then Halt(64); + LLogPath := ParamStr(1); + if LLogPath = '' then + LLogPath := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'DataRowVariantProbe-' + IntToStr(GetCurrentProcessId) + '.log'; + ReportMemoryLeaksOnShutdown := True; + LLog := TStringList.Create; + try + try + Application.Initialize; + CheckPositionalRows(LLog); + CheckNativeGridCells(LLog); + CheckUnicodeBindingsAndCells(LLog); + CheckNativeWizardStrings(LLog); + LLog.Add('PASS all native positional Variant and grid compatibility checks'); + ExitCode := 0; + except + on E: Exception do begin + LLog.Add('FAIL ' + E.ClassName + ': ' + E.Message); + ExitCode := 1; + end; + end; + LLog.SaveToFile(LLogPath); + WriteLn(LLog.Text); + finally + LLog.Free; + end; +end. diff --git a/src/tests/experimental/native/common/DataRowVariantProbe.dproj b/src/tests/experimental/native/common/DataRowVariantProbe.dproj new file mode 100644 index 000000000..00f2b3eab --- /dev/null +++ b/src/tests/experimental/native/common/DataRowVariantProbe.dproj @@ -0,0 +1,57 @@ +<!-- Copyright © 2026 Herman Schoenfeld https://sphere10.com. MIT license. --> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{5D010B3B-F484-4F4F-A2DE-9B3303469821}</ProjectGuid> + <MainSource>DataRowVariantProbe.dpr</MainSource> + <Config Condition="'$(Config)'==''">Debug</Config> + <Platform Condition="'$(Platform)'==''">Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Console</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\libraries\sphere10;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\libraries\sphere10;$(DCC_IncludePath)</DCC_IncludePath> + <DCC_ResourcePath>..\..\lib\Delphi\$(Platform)\data-row-variant\$(Config)\res;..\..\..\..\libraries\sphere10;$(DCC_ResourcePath)</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\Delphi\$(Platform)\data-row-variant\$(Config)\obj</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\Delphi\$(Platform)\data-row-variant\$(Config)</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\Delphi\$(Platform)\data-row-variant\$(Config)\res</BRCC_OutputDir> + <BRCC_IncludePath>..\..\..\..\libraries\sphere10;$(BRCC_IncludePath)</BRCC_IncludePath> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <BuildConfiguration Include="Base"> + <Key>Base</Key> + </BuildConfiguration> + <BuildConfiguration Include="Debug"> + <Key>Debug</Key> + <CfgParent>Base</CfgParent> + </BuildConfiguration> + <BuildConfiguration Include="Release"> + <Key>Release</Key> + <CfgParent>Base</CfgParent> + </BuildConfiguration> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">DataRowVariantProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/common/GridLoadingProbe.dpr b/src/tests/experimental/native/common/GridLoadingProbe.dpr new file mode 100644 index 000000000..f719dfec6 --- /dev/null +++ b/src/tests/experimental/native/common/GridLoadingProbe.dpr @@ -0,0 +1,57 @@ +program GridLoadingProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. } + +uses + Winapi.Windows, System.SysUtils, System.Classes, Vcl.Forms, + UVisualGrid, UGridLoadingProbe; + +type + TProbeForm = class(TForm) + public + constructor Create(AOwner: TComponent); override; + end; + +constructor TProbeForm.Create(AOwner: TComponent); +begin + inherited CreateNew(AOwner); +end; + +var + LForm: TProbeForm; + LLogPath: String; +begin + if ParamCount > 1 then Halt(64); + LLogPath := ParamStr(1); + if LLogPath = '' then + LLogPath := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'GridLoadingProbe-' + IntToStr(GetCurrentProcessId) + '.log'; + try + ReportMemoryLeaksOnShutdown := True; + Application.Initialize; + Application.MainFormOnTaskBar := True; + Application.CreateForm(TProbeForm, LForm); + try + LForm.SetBounds(80, 90, 640, 440); + LForm.Caption := 'Grid loading regression'; + InitializeGridLoadingProbe(LForm, LLogPath); + LForm.Show; + Application.Run; + if GridLoadingProbePassed then ExitCode := 0 else ExitCode := 1; + finally + LForm.Free; + end; + except + on E: Exception do begin + with TStringList.Create do + try + Text := 'FAIL initialization ' + E.ClassName + ': ' + E.Message; + SaveToFile(LLogPath); + finally + Free; + end; + ExitCode := 2; + end; + end; +end. \ No newline at end of file diff --git a/src/tests/experimental/native/common/GridLoadingProbe.dproj b/src/tests/experimental/native/common/GridLoadingProbe.dproj new file mode 100644 index 000000000..df049232f --- /dev/null +++ b/src/tests/experimental/native/common/GridLoadingProbe.dproj @@ -0,0 +1,39 @@ +<!-- Copyright © 2026 Herman Schoenfeld https://sphere10.com. MIT license. --> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{77894FC6-A1E1-4334-AB66-CE28E3F4192C}</ProjectGuid> + <MainSource>GridLoadingProbe.dpr</MainSource> + <Config Condition="'$(Config)'==''">Debug</Config> + <Platform Condition="'$(Platform)'==''">Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\libraries\sphere10;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\libraries\sphere10;$(DCC_IncludePath)</DCC_IncludePath> + <DCC_ResourcePath>..\..\lib\Delphi\$(Platform)\grid-loading\$(Config)\res;..\..\..\..\libraries\sphere10;$(DCC_ResourcePath)</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\Delphi\$(Platform)\grid-loading\$(Config)\obj</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\Delphi\$(Platform)\grid-loading\$(Config)</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\Delphi\$(Platform)\grid-loading\$(Config)\res</BRCC_OutputDir> + <BRCC_IncludePath>..\..\..\..\libraries\sphere10;$(BRCC_IncludePath)</BRCC_IncludePath> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <BuildConfiguration Include="Base"><Key>Base</Key></BuildConfiguration> + <BuildConfiguration Include="Debug"><Key>Debug</Key><CfgParent>Base</CfgParent></BuildConfiguration> + <BuildConfiguration Include="Release"><Key>Release</Key><CfgParent>Base</CfgParent></BuildConfiguration> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject><Delphi.Personality><Source><Source Name="MainSource">GridLoadingProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/common/GridThrottleProbe.dpr b/src/tests/experimental/native/common/GridThrottleProbe.dpr new file mode 100644 index 000000000..9a1dc45c4 --- /dev/null +++ b/src/tests/experimental/native/common/GridThrottleProbe.dpr @@ -0,0 +1,783 @@ +program GridThrottleProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. + Runs real VCL grid controls and generated data on the caller's desktop. } + +{$APPTYPE CONSOLE} + +uses + Winapi.Windows, System.SysUtils, System.Classes, System.Variants, + System.SyncObjs, System.Generics.Collections, Vcl.Forms, Vcl.Controls, + Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Graphics, + UCommon, UCommon.UI, UCommon.Data, UVisualGrid, UVisualGrid.Worker.Checks; + +type + TProbeSource = class(TCustomDataSource<Integer>) + public + PageCalls: LongInt; + ItemCount: Integer; + LastParams: TPageFetchParams; + Entered, ReleaseRead, CancelObserved: TEvent; + BlockRead, FailRead, CooperativeRead: Boolean; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + function GetColumns: TDataColumns; override; + function GetItemField(const AItem: Integer; const AName: String): Variant; override; + procedure FetchAll(const AContainer: TList<Integer>); override; + function FetchPage(const AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; override; + end; + + TProbeGrid = class(TVisualGrid) + public + procedure Search(const AText: String); + end; + + TGridProbe = class(TComponent) + private + FForm: TForm; + FGrids: array[0..1] of TProbeGrid; + FSources: array[0..1] of TProbeSource; + FStarts, FEnds, FAdmissions, FCompletions: array[0..1] of Integer; + FRequestOnCompletion: Boolean; + FFailure: String; + FLogPath: String; + procedure Log(const AText: String); + procedure Require(AValue: Boolean; const AMessage: String); + procedure OnException(Sender: TObject; E: Exception); + function GridIndex(Sender: TObject; AThrottle: Boolean): Integer; + procedure WaitingChanged(Sender: TObject); + procedure Admitted0(Sender: TObject); + procedure Admitted1(Sender: TObject); + procedure Completed(Sender: TObject); + procedure Pump(AMilliseconds: Cardinal); + procedure AwaitIdle(AIndex: Integer; AQuiet: Boolean = True); + procedure AwaitEntered(AIndex: Integer); + procedure Reset; + procedure EnableThrottle(AIndex: Integer); + function Calls(AIndex: Integer): Integer; + procedure CheckMixedRequestsAndIndependentTimers; + procedure CheckBusySingleAndLatestParameters; + procedure CheckHeldResultAndCompletionInput; + procedure CheckCancelModeAndDisposal; + procedure CheckParentlessAndHiddenRequests; + procedure CheckWorkerNotificationWithoutTimers; + procedure CheckRefreshSuspension; + procedure CheckQueuedLoading; + public + constructor Create(AForm: TForm; const ALogPath: String); reintroduce; + destructor Destroy; override; + procedure Run; + end; + +constructor TProbeSource.Create(AOwner: TComponent); +begin + inherited; + ItemCount := 1000; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); + CancelObserved := TEvent.Create(nil, True, False, ''); +end; + +destructor TProbeSource.Destroy; +begin + Entered.Free; + ReleaseRead.Free; + CancelObserved.Free; + inherited; +end; + +function TProbeSource.GetColumns: TDataColumns; +begin + Result := TDataColumns.Create(TDataColumn.From('Id', True)); +end; + +function TProbeSource.GetItemField(const AItem: Integer; const AName: String): Variant; +begin + Result := AItem; +end; + +procedure TProbeSource.FetchAll(const AContainer: TList<Integer>); +var I: Integer; +begin + // Inherited FetchPage owns the real provider lock around this callback. + if BlockRead and CooperativeRead then + while ReleaseRead.WaitFor(10) <> wrSignaled do + if TThread.CheckTerminated then begin + CancelObserved.SetEvent; + Abort; + end; + for I := 0 to ItemCount - 1 do AContainer.Add(I); +end; + +function TProbeSource.FetchPage(const AParams: TPageFetchParams; var ADataTable: TDataTable): TPageFetchResult; +begin + LastParams := AParams; + InterlockedIncrement(PageCalls); + Entered.SetEvent; + if BlockRead and not CooperativeRead and (ReleaseRead.WaitFor(15000) <> wrSignaled) then + raise Exception.Create('Owned native source gate timed out'); + if FailRead then raise Exception.Create('Owned cancelled provider failure'); + Result := inherited; +end; + +procedure TProbeGrid.Search(const AText: String); +begin + FSearchEdit.Text := AText; + SearchButtonClick(FSearchButton); +end; + +constructor TGridProbe.Create(AForm: TForm; const ALogPath: String); +var I: Integer; +begin + inherited Create(nil); + FForm := AForm; + FLogPath := ALogPath; + with TFileStream.Create(FLogPath, fmCreate) do Free; + Application.OnException := OnException; + for I := 0 to 1 do begin + FSources[I] := TProbeSource.Create(Self); + FGrids[I] := TProbeGrid.Create(nil); + FGrids[I].Parent := FForm; + FGrids[I].SetBounds(8 + I * 530, 8, 520, 600); + FGrids[I].FetchDataInThread := False; + FGrids[I].PageSize := 10; + FGrids[I].Options := [vgoColAutoFill, vgoColSizing, vgoSortDirectionAllowNone, + vgoAutoHidePaging, vgoAutoHideSearchPanel]; + FGrids[I].AddColumn('Id').Binding := 'Id'; + FGrids[I].Columns[0].Filters := [vgfNumericGT, vgfSortable]; + FGrids[I].SortMode := smSingleColumn; + FGrids[I].OnFinishedUpdating := Completed; + FGrids[I].RefreshThrottle.OnWaitingChanged.Add(WaitingChanged); + FGrids[I].DataSource := FSources[I]; + end; + FGrids[0].RefreshThrottle.Add(Admitted0); + FGrids[1].RefreshThrottle.Add(Admitted1); +end; + +destructor TGridProbe.Destroy; +var I: Integer; +begin + Application.OnException := nil; + for I := 0 to 1 do if Assigned(FSources[I]) then FSources[I].ReleaseRead.SetEvent; + for I := 0 to 1 do begin + FGrids[I].Free; + FGrids[I] := nil; + end; + inherited; +end; + +procedure TGridProbe.Log(const AText: String); +var F: TFileStream; B: TBytes; +begin + F := TFileStream.Create(FLogPath, fmOpenReadWrite or fmShareDenyNone); + try + F.Position := F.Size; + B := TEncoding.UTF8.GetBytes(UIntToStr(GetTickCount64) + ' ' + AText + #13#10); + F.WriteBuffer(B[0], Length(B)); + finally F.Free; end; +end; + +procedure TGridProbe.Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure TGridProbe.OnException(Sender: TObject; E: Exception); +begin + FFailure := E.ClassName + ': ' + E.Message; + Log('ASYNC ' + FFailure + ' ExceptAddr=' + IntToHex(NativeUInt(ExceptAddr),16) + + ' ImageBase=' + IntToHex(NativeUInt(HInstance),16) + ' grid0 NativeRow=' + IntToStr(FGrids[0].InternalDrawGrid.Row) + + ' NativeRowCount=' + IntToStr(FGrids[0].InternalDrawGrid.RowCount) + + ' DataRows=' + IntToStr(FGrids[0].RowCount) + + ' selectionType=' + IntToStr(Ord(FGrids[0].SelectionType))); +end; + +function TGridProbe.GridIndex(Sender: TObject; AThrottle: Boolean): Integer; +var I: Integer; +begin + for I := 0 to 1 do + if Assigned(FGrids[I]) and (((not AThrottle) and (Sender = FGrids[I])) or + (AThrottle and (Sender = FGrids[I].RefreshThrottle))) then Exit(I); + raise Exception.Create('Unexpected native grid event sender'); +end; + +procedure TGridProbe.WaitingChanged(Sender: TObject); +var I: Integer; +begin + I := GridIndex(Sender, True); + if FGrids[I].RefreshThrottle.Waiting then begin + Inc(FStarts[I]); + FGrids[I].ShowLoading('Please wait', 'Grid ' + IntToStr(I)); + end else begin + Inc(FEnds[I]); + FGrids[I].HideLoading; + end; + Require(FGrids[I].LoadingVisible = FGrids[I].RefreshThrottle.Waiting, + 'The sender grid card differs from its own Waiting state'); +end; + +procedure TGridProbe.Admitted0(Sender: TObject); +begin + Inc(FAdmissions[0]); +end; + +procedure TGridProbe.Admitted1(Sender: TObject); +begin + Inc(FAdmissions[1]); +end; + +procedure TGridProbe.Completed(Sender: TObject); +var I: Integer; +begin + I := GridIndex(Sender, False); + Inc(FCompletions[I]); + if (I = 1) and FRequestOnCompletion then begin + FRequestOnCompletion := False; + FGrids[I].RefreshGrid; + end; +end; + +procedure TGridProbe.Pump(AMilliseconds: Cardinal); +var UntilTick: UInt64; +begin + UntilTick := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + if FFailure <> '' then raise Exception.Create('Asynchronous native failure: ' + FFailure); + Sleep(1); + until GetTickCount64 >= UntilTick; +end; + +procedure TGridProbe.AwaitIdle(AIndex: Integer; AQuiet: Boolean); +var UntilTick: UInt64; +begin + UntilTick := GetTickCount64 + 8000; + repeat + Pump(10); + if not FGrids[AIndex].IsUpdating and + ((not AQuiet) or not FGrids[AIndex].RefreshThrottle.Waiting) then begin + Pump(60); + if not FGrids[AIndex].IsUpdating and + ((not AQuiet) or not FGrids[AIndex].RefreshThrottle.Waiting) then Exit; + end; + until GetTickCount64 >= UntilTick; + raise Exception.Create('The actual grid did not settle: ' + IntToStr(AIndex)); +end; + +procedure TGridProbe.AwaitEntered(AIndex: Integer); +var UntilTick: UInt64; +begin + UntilTick := GetTickCount64 + 1500; + while FSources[AIndex].Entered.WaitFor(0) <> wrSignaled do begin + Require(GetTickCount64 < UntilTick, 'Actual native fetch did not enter'); + Pump(5); + end; +end; + +procedure TGridProbe.Reset; +var I: Integer; +begin + FRequestOnCompletion := False; + for I := 0 to 1 do begin + FSources[I].ReleaseRead.SetEvent; + FGrids[I].RefreshThrottle.Mode := temNone; + AwaitIdle(I); + FSources[I].BlockRead := False; + FSources[I].FailRead := False; + FSources[I].CooperativeRead := False; + FGrids[I].RefreshSuspended := False; + FSources[I].ItemCount := 1000; + FSources[I].InvalidateSnapshot; + FGrids[I].FetchDataInThread := False; + FGrids[I].Visible := True; + FGrids[I].AutoPageSize := False; + FGrids[I].Columns[0].SortDirection := sdNone; + FGrids[I].Search(''); + FGrids[I].PageSize := 10; + FGrids[I].PageIndex := 0; + FGrids[I].RefreshGrid; + AwaitIdle(I); + FStarts[I] := 0; FEnds[I] := 0; FAdmissions[I] := 0; FCompletions[I] := 0; + end; +end; + +procedure TGridProbe.EnableThrottle(AIndex: Integer); +begin + FGrids[AIndex].RefreshThrottle.Interval := TTimeSpan.FromSeconds(5); + FGrids[AIndex].RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; +end; + +function TGridProbe.Calls(AIndex: Integer): Integer; +begin + Result := InterlockedCompareExchange(FSources[AIndex].PageCalls, 0, 0); +end; + +procedure TGridProbe.CheckMixedRequestsAndIndependentTimers; +var Before0, Before1, Capacity, I: Integer; + Last0, Last1, UntilTick: UInt64; + B: TBitmap; +begin + Reset; + FGrids[1].AutoPageSize := True; + AwaitIdle(1); + EnableThrottle(0); EnableThrottle(1); + Before0 := Calls(0); Before1 := Calls(1); + FGrids[0].RefreshGrid; FGrids[1].RefreshGrid; + Require((Calls(0) = Before0 + 1) and (Calls(1) = Before1 + 1), 'Each free grid must lead immediately'); + FGrids[0].PageSize := 25; FGrids[0].PageIndex := 2; + FGrids[0].Search('>400'); FGrids[0].Columns[0].SortDirection := sdDescending; + FGrids[1].Height := FGrids[1].Height + 40; + Pump(50); + Capacity := FGrids[1].PageSize; + Last1 := GetTickCount64; + for I := 0 to 9 do begin + Pump(100); + FGrids[0].RefreshGrid; + end; + Last0 := GetTickCount64; + Require((Calls(0) = Before0 + 1) and (Calls(1) = Before1 + 1), + 'Mixed refresh/page/filter/sort/resize inputs bypassed the owned quiet timer'); + Require(FGrids[0].LoadingVisible and FGrids[1].LoadingVisible, 'Independent custom cards are missing'); + Require(TLabel(FGrids[0].FindComponent('LoadingSubtitle')).Caption = 'Grid 0', 'Wrong custom card subtitle'); + B := FForm.GetFormImage; + try B.SaveToFile(ChangeFileExt(FLogPath, '-waiting.bmp')); finally B.Free; end; + UntilTick := Last1 + 6500; + while FGrids[1].RefreshThrottle.Waiting do begin + Require(GetTickCount64 < UntilTick, 'The independent grid timer did not finish'); + Pump(10); + end; + Require(GetTickCount64 - Last0 < 5000, 'Independent timer assertion missed its observation window'); + Require(FGrids[0].RefreshThrottle.Waiting and not FGrids[1].LoadingVisible, + 'One grid finishing ended the other grid waiting state'); + Require((Calls(0) = Before0 + 1) and (Calls(1) = Before1 + 2), 'Wrong independent leading/final query counts'); + AwaitIdle(0); AwaitIdle(1); + Require(Calls(0) = Before0 + 2, 'Mixed inputs must issue exactly one final query'); + Require((FSources[0].LastParams.PageSize = 25) and (FSources[0].LastParams.PageIndex = 2), 'Latest page parameters were lost'); + Require(Integer(FGrids[0].Cells[0,0]) = 949, 'Latest search and descending sort were not applied'); + Require((FSources[1].LastParams.PageSize = Capacity) and (FGrids[1].RowCount = Capacity), 'Resize did not retain the latest capacity'); + Require((FStarts[0] = 1) and (FEnds[0] = 1) and (FStarts[1] = 1) and (FEnds[1] = 1), 'Waiting edges restarted internally'); + Pump(300); + Require((Calls(0) = Before0 + 2) and (Calls(1) = Before1 + 2), 'Settled result chrome restarted a query'); + Log('PASS mixed RefreshGrid/page/filter/sort/resize; latest rows/params; independent five-second timers/cards'); +end; + +procedure TGridProbe.CheckBusySingleAndLatestParameters; +var Before0, Before1, I: Integer; +begin + Reset; + Before0 := Calls(0); Before1 := Calls(1); + for I := 0 to 1 do begin + FGrids[I].FetchDataInThread := True; + FSources[I].Entered.ResetEvent; FSources[I].ReleaseRead.ResetEvent; FSources[I].BlockRead := True; + FGrids[I].RefreshGrid; AwaitEntered(I); + FAdmissions[I] := 0; // Exclude the deliberately started temNone worker. + EnableThrottle(I); FGrids[I].RefreshGrid; + end; + FGrids[1].Visible := False; + FGrids[1].PageSize := 17; FGrids[1].PageIndex := 3; + Pump(200); + for I := 0 to 1 do FSources[I].ReleaseRead.SetEvent; + AwaitIdle(0, False); AwaitIdle(1, False); + Require(FGrids[0].RefreshThrottle.Waiting and FGrids[1].RefreshThrottle.Waiting, 'Busy inputs lost their quiet wait'); + Require((Calls(0) = Before0 + 1) and (Calls(1) = Before1 + 1), 'Older worker completion admitted a waiting request'); + FGrids[1].Visible := True; + AwaitIdle(0); AwaitIdle(1); + Require((Calls(0) = Before0 + 2) and (Calls(1) = Before1 + 2), 'Quiet finish failed to release one latest busy request'); + Require((FAdmissions[0] = 1) and (FStarts[0] = 1) and (FEnds[0] = 1), 'Single busy leading input delivered twice'); + Require((FSources[1].LastParams.PageSize = 17) and (FSources[1].LastParams.PageIndex = 3) and + (Integer(FGrids[1].Cells[0,0]) = 51), 'Hidden busy inputs lost the latest page'); + Log('PASS single busy leading request; no replay before quiet; hidden latest page parameters'); +end; + +procedure TGridProbe.CheckHeldResultAndCompletionInput; +var Before0, Before1, CompletionsBefore0: Integer; UntilTick: UInt64; +begin + Reset; + FGrids[0].AutoPageSize := True; + AwaitIdle(0); + // Empty publication must safely clamp a previously selected native row. + FGrids[0].SelectionType := stNone; + FGrids[0].SelectionType := stRow; + FGrids[0].InternalDrawGrid.Row := FGrids[0].InternalDrawGrid.FixedRows + 2; + Require(Length(FGrids[0].SelectedRows) = 1, 'Empty-result case needs a valid prior selection'); + FSources[0].ItemCount := 0; FSources[0].InvalidateSnapshot; + CompletionsBefore0 := FCompletions[0]; + EnableThrottle(0); EnableThrottle(1); + Before0 := Calls(0); Before1 := Calls(1); + FGrids[0].FetchDataInThread := True; + FSources[0].Entered.ResetEvent; FSources[0].ReleaseRead.ResetEvent; FSources[0].BlockRead := True; + FGrids[0].RefreshGrid; AwaitEntered(0); + FGrids[0].RefreshGrid; + FRequestOnCompletion := True; + FGrids[1].RefreshGrid; + UntilTick := GetTickCount64 + 6500; + while FGrids[0].RefreshThrottle.Waiting or FGrids[1].RefreshThrottle.Waiting do begin + Require(GetTickCount64 < UntilTick, 'Held query prevented its own quiet lifecycle finish'); + Pump(10); + end; + Require(FGrids[0].IsUpdating and not FGrids[0].LoadingVisible and + not FGrids[0].InternalDrawGrid.Enabled, 'Own timer must clear manual card while automatic fetch remains gated'); + Require(Calls(0) = Before0 + 1, 'Final admission launched a second query while the old worker was active'); + Require(Calls(1) = Before1 + 2, 'A client request inside OnFinishedUpdating was not admitted as input'); + FSources[0].ReleaseRead.SetEvent; + AwaitIdle(0); AwaitIdle(1); + Require((Calls(0) = Before0 + 2) and (FGrids[0].RowCount = 0), 'Held final page replay did not publish the empty result'); + Log(Format('Empty publication starts=%d ends=%d completions=%d baseline=%d nativeRow=%d nativeCount=%d dataRows=%d', + [FStarts[0],FEnds[0],FCompletions[0],CompletionsBefore0,FGrids[0].InternalDrawGrid.Row, + FGrids[0].InternalDrawGrid.RowCount,FGrids[0].RowCount])); + Require((FStarts[0] = 1) and (FEnds[0] = 1) and (FCompletions[0] = CompletionsBefore0 + 2), + 'Worker replay/result chrome became another input'); + Require((FGrids[0].InternalDrawGrid.Row < FGrids[0].InternalDrawGrid.RowCount) and + (Length(FGrids[0].SelectedRows) = 0), 'Empty result retained an invalid native current row/selection'); + Pump(350); + Require((Calls(0) = Before0 + 2) and (Calls(1) = Before1 + 2), 'Empty result chrome repeatedly fetched'); + FGrids[0].RefreshThrottle.Mode := temNone; + FSources[0].BlockRead := False; FSources[0].ItemCount := 1; FSources[0].InvalidateSnapshot; + FGrids[0].RefreshGrid; AwaitIdle(0); + Require((FGrids[0].RowCount = 1) and (Integer(FGrids[0].Cells[0,0]) = 0) and + (FGrids[0].InternalDrawGrid.Row < FGrids[0].InternalDrawGrid.RowCount), + 'A one-row result after the empty page retained invalid native coordinates'); + Log('PASS held worker finishes Waiting independently; latest replay; selected-to-empty-to-one native rows; result chrome; client completion input'); +end; + +procedure TGridProbe.CheckCancelModeAndDisposal; +var Before0, Before1, BeforeDisposed, I: Integer; + DisposedGrid: TProbeGrid; DisposedSource: TProbeSource; +begin + Reset; + Before0 := Calls(0); Before1 := Calls(1); + for I := 0 to 1 do begin + FGrids[I].FetchDataInThread := True; + FSources[I].Entered.ResetEvent; FSources[I].ReleaseRead.ResetEvent; FSources[I].BlockRead := True; + FGrids[I].RefreshGrid; AwaitEntered(I); + FAdmissions[I] := 0; // Exclude the deliberately started temNone worker. + EnableThrottle(I); FGrids[I].RefreshGrid; + end; + FGrids[0].RefreshThrottle.Cancel; + FGrids[1].RefreshThrottle.Mode := temNone; + for I := 0 to 1 do FSources[I].ReleaseRead.SetEvent; + AwaitIdle(0); AwaitIdle(1); + DisposedSource := TProbeSource.Create(Self); + DisposedGrid := TProbeGrid.Create(nil); + try + DisposedGrid.Parent := FForm; DisposedGrid.SetBounds(0,0,200,120); + DisposedGrid.PageSize := 10; DisposedGrid.FetchDataInThread := True; + DisposedGrid.AddColumn('Id').Binding := 'Id'; + DisposedSource.BlockRead := True; DisposedGrid.DataSource := DisposedSource; + Require(DisposedSource.Entered.WaitFor(1000) = wrSignaled, 'Owned disposal source did not enter'); + DisposedGrid.RefreshThrottle.Interval := TTimeSpan.FromSeconds(5); + DisposedGrid.RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; + DisposedGrid.RefreshGrid; DisposedGrid.RefreshThrottle.Cancel; + BeforeDisposed := DisposedSource.PageCalls; + finally + DisposedSource.ReleaseRead.SetEvent; + DisposedGrid.Free; + end; + Pump(5200); + Require((Calls(0) = Before0 + 1) and (Calls(1) = Before1 + 1), 'Cancel/mode change released a pending request or result-layout replay'); + Require(DisposedSource.PageCalls = BeforeDisposed, 'Disposal admitted cancelled data work'); + Require(not FGrids[0].LoadingVisible and not FGrids[1].LoadingVisible, 'Cancel/mode change left a manual card'); + FGrids[0].RefreshGrid; AwaitIdle(0, False); + Require(Calls(0) = Before0 + 2, 'Fresh input after Cancel did not lead immediately'); + FGrids[0].RefreshThrottle.Cancel; + FGrids[1].RefreshGrid; AwaitIdle(1); + Require(Calls(1) = Before1 + 2, 'Fresh non-throttled input after mode change failed'); + Log('PASS Cancel/mode change/disposal admit no cancelled page; fresh input still leads'); +end; + +procedure TGridProbe.CheckParentlessAndHiddenRequests; +var Before0, Before1: Integer; +begin + Reset; + FGrids[0].FetchDataInThread := True; + FGrids[1].FetchDataInThread := True; + EnableThrottle(0); EnableThrottle(1); + Before0 := Calls(0); Before1 := Calls(1); + // The wallet may detach an existing grid while showing a first-account view. + // Admission and its owned timer must still accept requests in that state. + FGrids[0].SelectionType := stNone; + FGrids[0].SelectionType := stRow; + FGrids[0].InternalDrawGrid.Row := FGrids[0].InternalDrawGrid.FixedRows + 2; + Require(Length(FGrids[0].SelectedRows) = 1, 'The pre-detach grid must have a valid selected row'); + FGrids[0].Parent := nil; + FGrids[1].Visible := False; + FGrids[0].RefreshGrid; FGrids[1].RefreshGrid; + Pump(300); + Require(FGrids[0].RefreshThrottle.Waiting and FGrids[1].RefreshThrottle.Waiting, + 'Parentless/hidden refresh inputs were not admitted'); + Require((FAdmissions[0] = 1) and (FAdmissions[1] = 1), 'Parentless/hidden leading admission was vetoed'); + FGrids[0].RefreshGrid; FGrids[1].RefreshGrid; + Pump(350); + Require((Calls(0) <= Before0 + 1) and (Calls(1) <= Before1 + 1), + 'A detached/hidden waiting request queried before quiet'); + FGrids[0].Parent := FForm; + FGrids[1].Visible := True; + Require(FGrids[0].RefreshThrottle.Waiting and FGrids[1].RefreshThrottle.Waiting, + 'Reparenting or showing a grid prematurely ended its wait'); + AwaitIdle(0); AwaitIdle(1); + Require((Calls(0) >= Before0 + 1) and (Calls(0) <= Before0 + 2) and + (Calls(1) >= Before1 + 1) and (Calls(1) <= Before1 + 2), + 'Parentless/hidden requests were lost or duplicated on reattachment'); + Require((FGrids[0].RowCount = 10) and (FGrids[1].RowCount = 10) and + (Integer(FGrids[0].Cells[0,0]) = 0) and (Integer(FGrids[1].Cells[0,0]) = 0), + 'Reattached grids did not render their admitted rows'); + Require((FStarts[0] = 1) and (FEnds[0] = 1) and (FStarts[1] = 1) and (FEnds[1] = 1), + 'Detaching a grid restarted its logical burst'); + Log('PASS parentless/hidden refresh admission/timer; no native parent exception; latest rows on reattachment'); +end; +procedure TGridProbe.CheckWorkerNotificationWithoutTimers; +var LBefore, LCompleted: Integer; LDeadline: UInt64; +begin + Reset; + FGrids[0].FetchDataInThread := True; + FSources[0].Entered.ResetEvent; + FSources[0].ReleaseRead.ResetEvent; + FSources[0].BlockRead := True; + LBefore := Calls(0); + LCompleted := FCompletions[0]; + FGrids[0].RefreshGrid; + Require(FSources[0].Entered.WaitFor(1500) = wrSignaled, 'Notification-only fetch did not enter'); + Require(FGrids[0].IsUpdating, 'Notification-only fetch was not held'); + FSources[0].ReleaseRead.SetEvent; + LDeadline := GetTickCount64 + 2000; + repeat + // Deliberately do not dispatch the Windows message queue: a WM_TIMER + // completion poll cannot satisfy this assertion. Only queued worker + // notification via the existing main-thread dispatcher may publish. + CheckSynchronize(10); + if not FGrids[0].IsUpdating then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(not FGrids[0].IsUpdating, 'Worker completion still requires a native timer'); + Require((Calls(0) = LBefore + 1) and (FCompletions[0] = LCompleted + 1), + 'Notification-only completion did not publish exactly one actual result'); + Require(Integer(FGrids[0].Cells[0,0]) = 0, 'Notification-only result rows were not published'); + Log('PASS actual worker result/rows/completion via CheckSynchronize only; no Windows message or WM_TIMER dispatch'); +end; + +procedure TGridProbe.CheckRefreshSuspension; +var LBefore, LCompleted: Integer; LStarted, LDeadline: UInt64; +begin + Reset; + EnableThrottle(0); + LBefore := Calls(0); + FGrids[0].RefreshSuspended := True; + FGrids[0].RefreshGrid; + FGrids[0].PageSize := 25; + FGrids[0].PageIndex := 2; + FGrids[0].Search('>400'); + FGrids[0].Columns[0].SortDirection := sdDescending; + FGrids[0].SetBounds(10, 15, 490, 580); + Pump(80); + Require(Calls(0) = LBefore, 'Suspension admitted a leading or resized provider query'); + Require(FGrids[0].RefreshThrottle.Waiting, 'Suspension swallowed an independent throttle input'); + FGrids[1].RefreshGrid; + Require(not FGrids[1].RefreshSuspended, 'Suspension leaked into the other grid'); + FGrids[0].RefreshThrottle.Cancel; + Require(Calls(0) = LBefore, 'Throttle cancellation bypassed the suspension guard'); + LStarted := GetTickCount64; + FGrids[0].RefreshSuspended := False; + Require(GetTickCount64 - LStarted < 1000, 'Resume waited for the old five-second quiet interval'); + Require(Calls(0) = LBefore + 1, 'Resume did not admit exactly one latest page'); + Require((FSources[0].LastParams.PageSize = 25) and (FSources[0].LastParams.PageIndex = 2), + 'Resume lost the latest page intent'); + Require(Integer(FGrids[0].Cells[0,0]) = 949, 'Resume lost the latest search/sort intent'); + Require(not FGrids[0].RefreshThrottle.Waiting, 'Internal resume became a new throttle input'); + Log('PASS suspended latest page/search/sort/resize; immediate one-query resume; other grid independent'); + + Reset; + FGrids[0].FetchDataInThread := True; + FSources[0].BlockRead := True; + FSources[0].Entered.ResetEvent; + FSources[0].ReleaseRead.ResetEvent; + LBefore := Calls(0); + LCompleted := FCompletions[0]; + FGrids[0].RefreshGrid; + AwaitEntered(0); + LStarted := GetTickCount64; + FGrids[0].RefreshSuspended := True; + Require(GetTickCount64 - LStarted < 150, 'Suspension joined an unfinished provider on the UI thread'); + FGrids[0].PageSize := 17; + FGrids[0].PageIndex := 3; + FGrids[0].RefreshThrottle.Cancel; + FGrids[0].RefreshSuspended := False; + FGrids[0].RefreshSuspended := True; + FGrids[0].ShowLoading('Please Wait', 'Synchronizing'); + FSources[0].FailRead := True; + FSources[0].ReleaseRead.SetEvent; + AwaitIdle(0); + Require((Calls(0) = LBefore + 1) and (FCompletions[0] = LCompleted), + 'True/false/true revived cancelled results or released deferred work'); + Require(FGrids[0].LoadingVisible and + (TLabel(FGrids[0].FindComponent('LoadingStatus')).Caption = 'Please Wait'), + 'Cancelled failure replaced caller-owned presentation'); + Require(Integer(FGrids[0].Cells[0,0]) = 0, 'Cancelled failure replaced retained rows'); + FSources[0].FailRead := False; + FSources[0].BlockRead := False; + FGrids[0].RefreshThrottle.Cancel; + FGrids[0].RefreshSuspended := False; + AwaitIdle(0); + Require((Calls(0) = LBefore + 2) and (FCompletions[0] = LCompleted + 1), + 'Retired cancellation was not followed by exactly one latest page'); + Require(Integer(FGrids[0].Cells[0,0]) = 51, 'Retirement lost the latest deferred page'); + FGrids[0].HideLoading; + Log('PASS active cancellation without UI join; false/true completion race; ignored errors; latest deferred page'); + + Reset; + FGrids[0].FetchDataInThread := True; + FSources[0].BlockRead := True; + FSources[0].CooperativeRead := True; + FSources[0].Entered.ResetEvent; + FSources[0].ReleaseRead.ResetEvent; + FGrids[0].RefreshGrid; + AwaitEntered(0); + FGrids[0].RefreshSuspended := True; + Require(FSources[0].CancelObserved.WaitFor(1500) = wrSignaled, + 'Actual provider did not observe cooperative worker termination'); + LDeadline := GetTickCount64 + 1500; + while FGrids[0].IsUpdating and (GetTickCount64 < LDeadline) do CheckSynchronize(10); + Require(not FGrids[0].IsUpdating, 'Cancelled worker retirement depended on a polling timer'); + FSources[0].BlockRead := False; + FGrids[0].RefreshThrottle.Cancel; + FGrids[0].RefreshSuspended := False; + AwaitIdle(0); + Log('PASS cooperative provider Abort/finally and queued retirement; next query reacquires the real source lock'); +end; + +procedure TGridProbe.CheckQueuedLoading; +var LBefore: Integer; LPanel: TPanel; LCircle: TLoadingCircle; LUntil: UInt64; +begin + Reset; + // Other groups intentionally bind a manual card to Waiting. This group + // verifies the component's automatic presentation without that test policy. + FGrids[0].RefreshThrottle.OnWaitingChanged.Remove(WaitingChanged); + try + FGrids[0].RefreshThrottle.Interval := TTimeSpan.FromMilliseconds(900); + FGrids[0].RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; + FGrids[0].RefreshGrid; + LBefore := Calls(0); + FGrids[0].PageIndex := 1; + Pump(250); + LPanel := TPanel(FGrids[0].FindComponent('LoadingOverlay')); + LCircle := TLoadingCircle(FGrids[0].FindComponent('LoadingCircle')); + Require(FGrids[0].RefreshThrottle.Waiting and (Calls(0)=LBefore), + 'Queued input fetched before its quiet interval'); + Require(LPanel.Visible and LCircle.Active,'Queued input has no loading feedback'); + Pump(800); + Require((Calls(0)=LBefore+1) and not LPanel.Visible, + 'Completed queued input did not retire its loading feedback'); + Log('PASS waiting input shows an animated card before admission; completed synchronous fetch hides it'); + + FGrids[0].RefreshGrid; + FGrids[0].PageIndex := 2; + Pump(250); + Require(LPanel.Visible,'Second queued input has no loading feedback'); + FGrids[0].RefreshThrottle.Cancel; + Require(not LPanel.Visible,'Cancelled queued input left a loading card'); + Log('PASS cancelling queued input hides its ordinary loading card'); + + FGrids[0].RefreshGrid; + FGrids[0].PageIndex := 3; + FGrids[0].ShowLoading('Please Wait','Synchronizing'); + Pump(250); + Require(TLabel(FGrids[0].FindComponent('LoadingStatus')).Caption='Please Wait', + 'Queued input replaced caller-owned artwork'); + FGrids[0].RefreshThrottle.Cancel; + Require(LPanel.Visible,'Cancellation dismissed caller-owned artwork'); + FGrids[0].HideLoading; + Require(not LPanel.Visible,'Manual card did not hide'); + Log('PASS caller-owned synchronization card takes precedence and survives ordinary cancellation'); + + Reset; + FGrids[0].FetchDataInThread := True; + FSources[0].BlockRead := True; + FSources[0].ReleaseRead.ResetEvent; + FSources[0].Entered.ResetEvent; + FGrids[0].RefreshGrid; + try + AwaitEntered(0); + FGrids[0].RefreshThrottle.Interval := TTimeSpan.FromMilliseconds(900); + FGrids[0].RefreshThrottle.Mode := temNotifyOnEventBurstStartAndFinished; + FGrids[0].PageIndex := 1; + FSources[0].ReleaseRead.SetEvent; + AwaitIdle(0,False); + FSources[0].ReleaseRead.ResetEvent; + Pump(250); + Require(LPanel.Visible,'Deferred single request has no feedback'); + LUntil := GetTickCount64+2000; + while FGrids[0].RefreshThrottle.Waiting and (GetTickCount64<LUntil) do Pump(5); + Require(not FGrids[0].RefreshThrottle.Waiting and FGrids[0].IsUpdating, + 'Quiet finish did not admit the deferred held provider'); + Require(LPanel.Visible and LCircle.Active, + 'Quiet admission restarted the delay on an already visible loading card'); + finally FSources[0].ReleaseRead.SetEvent; end; + AwaitIdle(0); + Require(not LPanel.Visible,'Finished deferred request retained its loading card'); + Log('PASS deferred single input retains its visible loading card through quiet admission into a held worker'); + finally + FGrids[0].RefreshThrottle.OnWaitingChanged.Add(WaitingChanged); + end; +end; + +procedure TGridProbe.Run; +begin + if SameText(GetEnvironmentVariable('PASCAL_GRID_EARLY_CANCEL_ONLY'), '1') then begin + CheckVisualGridEarlyWorkerCancellation; + Log('PASS cancelled-before-execution worker retires and preserves other grid receivers'); + Exit; + end; + if SameText(GetEnvironmentVariable('PASCAL_GRID_LOADING_ONLY'), '1') then begin + CheckQueuedLoading; + Exit; + end; + if SameText(GetEnvironmentVariable('PASCAL_GRID_SUSPENSION_ONLY'), '1') then begin + CheckRefreshSuspension; + Exit; + end; + if SameText(GetEnvironmentVariable('PASCAL_GRID_COMPLETION_ONLY'), '1') then begin + CheckWorkerNotificationWithoutTimers; + Exit; + end; + CheckVisualGridEarlyWorkerCancellation; + Log('PASS cancelled-before-execution worker retires and preserves other grid receivers'); + CheckRefreshSuspension; + CheckWorkerNotificationWithoutTimers; + CheckParentlessAndHiddenRequests; + CheckMixedRequestsAndIndependentTimers; + CheckBusySingleAndLatestParameters; + CheckHeldResultAndCompletionInput; + CheckCancelModeAndDisposal; + Log('PASS all actual VCL grid admission checks'); +end; + +var Form: TForm; Probe: TGridProbe; LogPath: String; +begin + if ParamCount > 1 then Halt(64); + LogPath := ParamStr(1); + if LogPath = '' then LogPath := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'GridThrottleProbe-' + IntToStr(GetCurrentProcessId) + '.log'; + Form := nil; Probe := nil; + try + ReportMemoryLeaksOnShutdown := True; + Application.Initialize; + Form := TForm.CreateNew(nil); + Form.SetBounds(60,70,1080,690); + Form.Caption := 'Generic grid request throttle regression'; + Form.Show; + Probe := TGridProbe.Create(Form,LogPath); + try Probe.Run; ExitCode := 0; + finally FreeAndNil(Probe); end; + except + on E: Exception do begin + with TStringList.Create do + try + if FileExists(LogPath) then LoadFromFile(LogPath); + Add('FAIL ' + E.ClassName + ': ' + E.Message); SaveToFile(LogPath); + finally Free; end; + ExitCode := 1; + end; + end; + Probe.Free; Form.Free; +end. diff --git a/src/tests/experimental/native/common/GridThrottleProbe.dproj b/src/tests/experimental/native/common/GridThrottleProbe.dproj new file mode 100644 index 000000000..33ddf27bc --- /dev/null +++ b/src/tests/experimental/native/common/GridThrottleProbe.dproj @@ -0,0 +1,40 @@ +<!-- Copyright © 2026 Herman Schoenfeld https://sphere10.com. MIT license. --> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{21604B67-51B7-42C9-85F1-B085DFB9BB1C}</ProjectGuid> + <MainSource>GridThrottleProbe.dpr</MainSource> + <Config Condition="'$(Config)'==''">Debug</Config> + <Platform Condition="'$(Platform)'==''">Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Console</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_Define>VISUALGRID_TEST_WORKER_START;$(DCC_Define)</DCC_Define> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\framework;..\..\..\..\libraries\sphere10;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\libraries\sphere10;$(DCC_IncludePath)</DCC_IncludePath> + <DCC_ResourcePath>..\..\lib\Delphi\$(Platform)\grid-throttle\$(Config)\res;..\..\..\..\libraries\sphere10;$(DCC_ResourcePath)</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\Delphi\$(Platform)\grid-throttle\$(Config)\obj</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\Delphi\$(Platform)\grid-throttle\$(Config)</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\Delphi\$(Platform)\grid-throttle\$(Config)\res</BRCC_OutputDir> + <BRCC_IncludePath>..\..\..\..\libraries\sphere10;$(BRCC_IncludePath)</BRCC_IncludePath> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <BuildConfiguration Include="Base"><Key>Base</Key></BuildConfiguration> + <BuildConfiguration Include="Debug"><Key>Debug</Key><CfgParent>Base</CfgParent></BuildConfiguration> + <BuildConfiguration Include="Release"><Key>Release</Key><CfgParent>Base</CfgParent></BuildConfiguration> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject><Delphi.Personality><Source><Source Name="MainSource">GridThrottleProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/common/NativeThrottleProbe.dpr b/src/tests/experimental/native/common/NativeThrottleProbe.dpr new file mode 100644 index 000000000..815fb1ea7 --- /dev/null +++ b/src/tests/experimental/native/common/NativeThrottleProbe.dpr @@ -0,0 +1,276 @@ +program NativeThrottleProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT license; see the repository LICENSE. } + +{$APPTYPE CONSOLE} +uses System.SysUtils, System.Classes, System.JSON, System.DateUtils, System.IOUtils, + Winapi.Windows, Vcl.Forms, Vcl.ExtCtrls, UCommon, UCommon.UI; +type + EControlledTrailingError = class(Exception); + TProbe = class; + TThrottleCase = class(TComponent) + private + FProbe: TProbe; + FName: String; + FThrottle: TThrottledEvent; + FTimer: TTimer; + FOriginalTimer: TNotifyEvent; + FTimes: TArray<Cardinal>; + FNext, FNotifications, FCalls, FLeading, FTrailing, FTimers, FEarlyTimers, FEarlyCalls: Integer; + FWaitingStarts, FWaitingStops, FFinished, FWaitingErrors, FExceptions, FUnexpectedErrors: Integer; + FLastInput: UInt64; + FInNotify, FPeriodic, FLastWaiting, FActionDone, FModeRestored: Boolean; + procedure Delivered(Sender: TObject); + procedure WaitingChanged(Sender: TObject); + procedure BurstFinished(Sender: TObject); + procedure TimerFired(Sender: TObject); + procedure Trace(const Kind: String); + public + constructor Create(AProbe: TProbe; const AName: String; AMode: TThrottledEventMode; const ATimes: TArray<Cardinal>); + procedure Tick(AElapsed: UInt64); + function Validate: TJSONObject; + end; + TProbe = class(TComponent) + private + FLog: TFileStream; + FOutput: String; + FStarted: UInt64; + FCases: TArray<TThrottleCase>; + public + constructor Create(const AOutput: String); reintroduce; + destructor Destroy; override; + procedure Log(const S: String); + function Run: Boolean; + property Started: UInt64 read FStarted; + end; +function Sequence(Step, Finish: Cardinal): TArray<Cardinal>; +var I: Integer; +begin + SetLength(Result, Finish div Step + 1); + for I:=0 to High(Result) do Result[I]:=Cardinal(I)*Step; +end; +constructor TThrottleCase.Create(AProbe: TProbe; const AName: String; AMode: TThrottledEventMode; const ATimes: TArray<Cardinal>); +var I: Integer; +begin + inherited Create(AProbe); FProbe:=AProbe; FName:=AName; FTimes:=Copy(ATimes); + FPeriodic:=AMode=temNotifyEveryInterval; + FThrottle:=TThrottledEvent.Create(Self); + FThrottle.Interval:=TTimeSpan.FromSeconds(5); + FThrottle.Mode:=AMode; FThrottle.Add(Delivered); + FThrottle.OnWaitingChanged.Add(WaitingChanged); + FThrottle.OnBurstFinished.Add(BurstFinished); + for I:=0 to FThrottle.ComponentCount-1 do + if FThrottle.Components[I] is TTimer then FTimer:=TTimer(FThrottle.Components[I]); + if not Assigned(FTimer) then raise Exception.Create('Owned native timer absent'); + FOriginalTimer:=FTimer.OnTimer; FTimer.OnTimer:=TimerFired; + FProbe.Log(FName+' CONFIG interval_ms='+FloatToStr(FThrottle.Interval.TotalMilliseconds)+ + ';interval_ticks='+IntToStr(FThrottle.Interval.Ticks)+';mode='+IntToStr(Ord(AMode))); +end; +procedure TThrottleCase.Trace(const Kind: String); +begin + FProbe.Log(Format('%s %s notify=%d call=%d leading=%d trailing=%d timers=%d '+ + 'timer_enabled=%s timer_ms=%d quiet_tick_ms=%d quiet_span_ms=%.3f '+ + 'client_datetime=%.12f actual_datetime=%.12f suppressed=%d waiting=%s burst_active=%s '+ + 'waiting_on=%d waiting_off=%d finished=%d exceptions=%d', + [FName,Kind,FNotifications,FCalls,FLeading,FTrailing,FTimers, + BoolToStr(FTimer.Enabled,True),FTimer.Interval,GetTickCount64-FLastInput, + TTimeSpan.Subtract(Now,FThrottle.LastClientNotify).TotalMilliseconds, + Double(FThrottle.LastClientNotify),Double(FThrottle.LastActualNotify),FThrottle.SuppressedInvocations, + BoolToStr(FThrottle.Waiting,True),BoolToStr(FThrottle.BurstActive,True), + FWaitingStarts,FWaitingStops,FFinished,FExceptions])); +end; +procedure TThrottleCase.Tick(AElapsed: UInt64); +begin + if not FActionDone and(AElapsed>=1000)and + ((FName='cancel-recovery')or(FName='mode-change-recovery'))then begin + FActionDone:=True; + if FName='cancel-recovery'then FThrottle.Cancel + else FThrottle.Mode:=temNotifyEveryInterval; + if FThrottle.Waiting or FTimer.Enabled or(FFinished<>0)then Inc(FWaitingErrors); + Trace('CANCEL_OR_MODE_CHANGE'); + end; + if(FName='mode-change-recovery')and not FModeRestored and(AElapsed>=1500)then begin + FModeRestored:=True; + FThrottle.Mode:=temNotifyOnEventBurstStartAndFinished; + if FThrottle.Waiting or FTimer.Enabled then Inc(FWaitingErrors); + Trace('MODE_RESTORED'); + end; + while (FNext<Length(FTimes)) and(AElapsed>=FTimes[FNext]) do begin + Inc(FNotifications); Inc(FNext); FLastInput:=GetTickCount64; + FInNotify:=True; + try FThrottle.Notify; finally FInNotify:=False; end; + if FThrottle.Waiting<>FThrottle.BurstActive then Inc(FWaitingErrors); + if FThrottle.Waiting=FPeriodic then Inc(FWaitingErrors); + Trace('NOTIFY'); + end; +end; +procedure TThrottleCase.WaitingChanged(Sender: TObject); +begin + if(FThrottle.Waiting<>FThrottle.BurstActive)or(FLastWaiting=FThrottle.Waiting)then + Inc(FWaitingErrors); + FLastWaiting:=FThrottle.Waiting; + if FLastWaiting then Inc(FWaitingStarts)else Inc(FWaitingStops); + Trace('WAITING_CHANGED'); +end; +procedure TThrottleCase.BurstFinished(Sender: TObject); +begin + Inc(FFinished); + if FThrottle.Waiting or FThrottle.BurstActive then Inc(FWaitingErrors); + Trace('BURST_FINISHED'); +end; +procedure TThrottleCase.Delivered(Sender: TObject); +begin + Inc(FCalls); + if(FThrottle.Waiting<>FThrottle.BurstActive)or(FThrottle.Waiting=FPeriodic)then + Inc(FWaitingErrors); + if FInNotify then Inc(FLeading) else begin + Inc(FTrailing); + if GetTickCount64-FLastInput<4980 then Inc(FEarlyCalls); + end; + Trace('INVOKE'); + if(FName='throwing-trailing-recovery')and not FInNotify and(FExceptions=0)then + raise EControlledTrailingError.Create('Controlled ordinary trailing callback failure'); +end; +procedure TThrottleCase.TimerFired(Sender: TObject); +begin + Inc(FTimers); + if(not FPeriodic)and(GetTickCount64-FLastInput<4980)then Inc(FEarlyTimers); + Trace('TIMER_BEFORE'); + try + FOriginalTimer(Sender); + except + on E:EControlledTrailingError do begin + Inc(FExceptions); + if FThrottle.Waiting or FTimer.Enabled then Inc(FWaitingErrors); + Trace('CONTROLLED_TRAILING_EXCEPTION'); + end; + on E:Exception do begin + Inc(FUnexpectedErrors); + FProbe.Log(FName+' UNEXPECTED_EXCEPTION '+E.ClassName+': '+E.Message); + end; + end; + if FThrottle.Waiting<>FThrottle.BurstActive then Inc(FWaitingErrors); + Trace('TIMER_AFTER'); +end; +function TThrottleCase.Validate: TJSONObject; +var DeliveryPass, PhysicalPass, WaitingPass: Boolean; + ExpectedCalls, ExpectedBursts, ExpectedFinished, ExpectedExceptions: Integer; +begin + ExpectedCalls:=2; + ExpectedBursts:=1;ExpectedFinished:=1;ExpectedExceptions:=0; + if FName='two-bursts' then ExpectedCalls:=4; + if FName='finished-only-100ms' then ExpectedCalls:=1; + if FName='throwing-trailing-recovery'then begin + ExpectedCalls:=3;ExpectedExceptions:=1; + end; + if(FName='two-bursts')or(FName='single-input-lifecycle')or + (FName='throwing-trailing-recovery')then begin + ExpectedBursts:=2;ExpectedFinished:=2; + end; + if(FName='cancel-recovery')or(FName='mode-change-recovery')then ExpectedBursts:=2; + if FPeriodic then begin + ExpectedCalls:=5; + ExpectedBursts:=0; + ExpectedFinished:=0; + end; + DeliveryPass:=(FCalls=ExpectedCalls)and(FNotifications=Length(FTimes)); + if FPeriodic then DeliveryPass:=DeliveryPass and(FLeading=1) + else DeliveryPass:=DeliveryPass and(FEarlyCalls=0); + if FName='two-bursts' then DeliveryPass:=DeliveryPass and(FLeading=2)and(FTrailing=2); + if FName='finished-only-100ms' then DeliveryPass:=DeliveryPass and(FLeading=0)and(FTrailing=1); + if(FName='single-input-lifecycle')or(FName='cancel-recovery')or + (FName='mode-change-recovery')then + DeliveryPass:=DeliveryPass and(FLeading=2)and(FTrailing=0); + if FName='throwing-trailing-recovery'then + DeliveryPass:=DeliveryPass and(FLeading=2)and(FTrailing=1); + PhysicalPass:=FEarlyTimers=0; + WaitingPass:=(FWaitingErrors=0)and(FUnexpectedErrors=0)and + (FWaitingStarts=ExpectedBursts)and(FWaitingStops=ExpectedBursts)and + (FFinished=ExpectedFinished)and(FExceptions=ExpectedExceptions)and + not FThrottle.Waiting and not FThrottle.BurstActive; + Result:=TJSONObject.Create; + Result.AddPair('case',FName); + Result.AddPair('notifications',TJSONNumber.Create(FNotifications)); + Result.AddPair('invocations',TJSONNumber.Create(FCalls)); + Result.AddPair('leading',TJSONNumber.Create(FLeading)); + Result.AddPair('trailing',TJSONNumber.Create(FTrailing)); + Result.AddPair('physicalTimerFires',TJSONNumber.Create(FTimers)); + Result.AddPair('physicalFiresBeforeQuiet',TJSONNumber.Create(FEarlyTimers)); + Result.AddPair('deliveredBeforeQuiet',TJSONNumber.Create(FEarlyCalls)); + Result.AddPair('waitingStarts',TJSONNumber.Create(FWaitingStarts)); + Result.AddPair('waitingStops',TJSONNumber.Create(FWaitingStops)); + Result.AddPair('finished',TJSONNumber.Create(FFinished)); + Result.AddPair('controlledTrailingExceptions',TJSONNumber.Create(FExceptions)); + Result.AddPair('waitingErrors',TJSONNumber.Create(FWaitingErrors)); + Result.AddPair('unexpectedErrors',TJSONNumber.Create(FUnexpectedErrors)); + Result.AddPair('deliveryPass',TJSONBool.Create(DeliveryPass)); + Result.AddPair('physicalTimerResetPass',TJSONBool.Create(PhysicalPass)); + Result.AddPair('waitingPass',TJSONBool.Create(WaitingPass)); + Result.AddPair('pass',TJSONBool.Create(DeliveryPass and PhysicalPass and WaitingPass)); +end; +constructor TProbe.Create(const AOutput: String); +begin + inherited Create(nil); FOutput:=IncludeTrailingPathDelimiter(ExtractFilePath(ExpandFileName(AOutput))); + FLog:=TFileStream.Create(AOutput,fmCreate or fmShareDenyNone); + FStarted:=GetTickCount64; + Log('NATIVE Delphi13 VCL real TTimer and production TThrottledEvent; interval5seconds'); + Log(Format('NATIVE_TIME_SPAN days=%.12f milliseconds=%.3f; known_4900_ms=%.3f', + [TTimeSpan.FromSeconds(5).TotalDays,TTimeSpan.FromSeconds(5).TotalMilliseconds, + TTimeSpan.Subtract(Now,Now-TTimeSpan.FromMilliseconds(4900).TotalDays).TotalMilliseconds])); + SetLength(FCases,10); + FCases[0]:=TThrottleCase.Create(Self,'continuous-100ms',temNotifyOnEventBurstStartAndFinished,Sequence(100,16000)); + FCases[1]:=TThrottleCase.Create(Self,'continuous-1s',temNotifyOnEventBurstStartAndFinished,Sequence(1000,16000)); + FCases[2]:=TThrottleCase.Create(Self,'late-4.9s',temNotifyOnEventBurstStartAndFinished,TArray<Cardinal>.Create(0,100,4900)); + FCases[3]:=TThrottleCase.Create(Self,'two-bursts',temNotifyOnEventBurstStartAndFinished,TArray<Cardinal>.Create(0,1000,9000,10000)); + FCases[4]:=TThrottleCase.Create(Self,'finished-only-100ms',temNotifyOnEventBurstFinished,Sequence(100,16000)); + FCases[5]:=TThrottleCase.Create(Self,'periodic-control-100ms',temNotifyEveryInterval,Sequence(100,16000)); + FCases[6]:=TThrottleCase.Create(Self,'single-input-lifecycle',temNotifyOnEventBurstStartAndFinished,TArray<Cardinal>.Create(0,6000)); + FCases[7]:=TThrottleCase.Create(Self,'throwing-trailing-recovery',temNotifyOnEventBurstStartAndFinished,TArray<Cardinal>.Create(0,100,6000)); + FCases[8]:=TThrottleCase.Create(Self,'cancel-recovery',temNotifyOnEventBurstStartAndFinished,TArray<Cardinal>.Create(0,100,2000)); + FCases[9]:=TThrottleCase.Create(Self,'mode-change-recovery',temNotifyOnEventBurstStartAndFinished,TArray<Cardinal>.Create(0,100,2000)); +end; +destructor TProbe.Destroy; +begin + inherited; + FLog.Free; +end; +procedure TProbe.Log(const S: String); +var B:TBytes; +begin + B:=TEncoding.UTF8.GetBytes(UIntToStr(GetTickCount64-FStarted)+' '+S+#13#10); + FLog.WriteBuffer(B[0],Length(B)); +end; +function TProbe.Run: Boolean; +var I: Integer; Root, Item: TJSONObject; Items:TJSONArray; +begin + while GetTickCount64-FStarted<23500 do begin + for I:=0 to High(FCases)do FCases[I].Tick(GetTickCount64-FStarted); + Application.ProcessMessages; Sleep(1); + end; + Result:=True;Root:=TJSONObject.Create;Items:=TJSONArray.Create; + try + for I:=0 to High(FCases)do begin + Item:=FCases[I].Validate;Items.AddElement(Item); + Result:=Result and Item.GetValue<Boolean>('pass'); + end; + Root.AddPair('pass',TJSONBool.Create(Result));Root.AddPair('cases',Items); + Root.AddPair('elapsedMs',TJSONNumber.Create(GetTickCount64-FStarted)); + TFile.WriteAllText(FOutput+'result.json',Root.ToJSON,TEncoding.UTF8); + Log('RESULT '+Root.ToJSON); + finally Root.Free;end; +end; +var Probe:TProbe;Code:Integer; +begin + Code:=2; + try + if ParamCount<>1 then raise Exception.Create('Expected trace logfile in an existing output directory'); + Application.Initialize; + Probe:=TProbe.Create(ParamStr(1)); + try if Probe.Run then Code:=0 else Code:=1;finally Probe.Free;end; + except on E:Exception do begin + WriteLn(E.ClassName+': '+E.Message); + Code:=2; + end;end; + ExitCode:=Code; +end. diff --git a/src/tests/experimental/native/common/NativeThrottleProbe.dproj b/src/tests/experimental/native/common/NativeThrottleProbe.dproj new file mode 100644 index 000000000..e66a0d422 --- /dev/null +++ b/src/tests/experimental/native/common/NativeThrottleProbe.dproj @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{5FD952E3-C27D-4CB5-8F34-1040A6373B9A}</ProjectGuid> + <MainSource>NativeThrottleProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Console</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\libraries\sphere10</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\native-throttle\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\native-throttle\$(Platform)\$(Config)\bin</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\native-throttle\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup><DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile></ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">NativeThrottleProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/common/README.md b/src/tests/experimental/native/common/README.md new file mode 100644 index 000000000..3703c262b --- /dev/null +++ b/src/tests/experimental/native/common/README.md @@ -0,0 +1,146 @@ +# Native component checks + +These Delphi probes and shared checks test Sphere 10 Pascal Framework +components. Each probe is mirrored in standalone PascalFramework. No probe +needs wallet data, network access or OpenSSL. + +## Build and run + +Build each probe's `.dproj` in Delphi for Win64 Debug. Run `ScopeProbe` and +`GridLoadingProbe` from Delphi, and the others through the +[private-desktop broker](../README.md#private-desktop-broker), which never switches the input desktop. + +For `NativeThrottleProbe`, run these commands from the repository root with +Free Pascal on PATH: + +```powershell +New-Item src\tests\experimental\lib\native-throttle\broker,<new-folder> -ItemType Directory -Force +fpc -FEsrc\tests\experimental\lib\native-throttle\broker -FUsrc\tests\experimental\lib\native-throttle\broker src\tests\experimental\native\PrivateDesktopBroker.lpr +.\src\tests\experimental\lib\native-throttle\broker\PrivateDesktopBroker.exe ` + .\src\tests\experimental\lib\native-throttle\Win64\Debug\bin\NativeThrottleProbe.exe ` + <new-folder>\trace.log +``` + +For other broker runs, substitute the probe's EXE and log path. Pass a log path +in a test folder, or point `TEMP` and `TMP` at one. + +| Probe | Arguments | Report | Exit codes | +|---|---|---|---| +| `ScopeProbe` | — | PASS and FAIL lines on standard output | 0 pass, 1 failure | +| `GridLoadingProbe` | Optional log path | Log, default `%TEMP%\GridLoadingProbe-<PID>.log`, with bitmap captures beside it | 0 pass, 1 assertion failure, 2 initialization failure, 64 extra argument | +| `NativeThrottleProbe` | Trace-file path | Trace, with `result.json` beside it | Nonzero on failure | +| `DataRowVariantProbe`, `GridThrottleProbe` | Optional log path | PASS and FAIL log, default `%TEMP%\<ProbeName>-<PID>.log` | 0 pass, 1 assertion or runtime failure, 64 extra argument | +| `ReadWriteLockProbe` | Log path | PASS and FAIL log | 0 pass, 1 failure, 64 missing or extra argument | + +## Managed scopes and disposal bags + +[ScopeProbe.dproj](ScopeProbe.dproj) runs the same 63 scope, disposal-bag and +lock-scope assertions as the FPCUnit fixtures. Use Delphi 13, Win64 Debug or +Release. Delphi reports leaks at shutdown. FPC can run with heap tracing +(`-gh`). + +The shared checks are in [UScope.Checks](../../framework/UScope.Checks.pas), +[UDisposables.Scope.Checks](../../../UDisposables.Scope.Checks.pas) and +[UReadWriteScope.Checks](../../framework/UReadWriteScope.Checks.pas). + +## Read-write locks + +[ReadWriteLockProbe.dproj](ReadWriteLockProbe.dproj) checks the `TReadWriteLock` +contract in seven groups, including recursion, promotion, timeouts and +wrong-thread release. It builds into +`src/tests/experimental/lib/native-read-write/<Platform>/<Config>/bin`. + +## Custom loading cards + +[GridLoadingProbe.dproj](GridLoadingProbe.dproj) is a VCL application that +checks custom grid loading cards, including artwork, input gating, error +precedence and focus in modeless windows. It builds into +`src/tests/experimental/lib/Delphi/<Platform>/grid-loading/<Config>`. + +[UVisualGrid.Refresh.Tests](../../framework/UVisualGrid.Refresh.Tests.pas) +covers the same API in Lazarus. + +## Five-second throttle + +[NativeThrottleProbe.dproj](NativeThrottleProbe.dproj) exercises +`TThrottledEvent` and its VCL timer. Its [source](NativeThrottleProbe.dpr) runs +ten scenarios at once in about 24 seconds. Input every 100 ms or every second +must give one leading call, then one final call after five quiet seconds. Other +scenarios cover finished-only and periodic modes, and recovery after a callback +exception, cancellation or mode change. + +FPCUnit timer regressions are in each library's `UCommon.UI.Tests.pas`. + +## Data-row Variant compatibility + +[DataRowVariantProbe.dproj](DataRowVariantProbe.dproj) checks positional +data-row access and native text in Delphi. Its +[source](DataRowVariantProbe.dpr) covers `Integer(Row._(0))`, row copies, +synchronous `TVisualGrid.Cells[x,y]` and Unicode text. It builds into +`src/tests/experimental/lib/Delphi/<Platform>/data-row-variant/<Config>`. + +FPC 3.2.0 and 3.2.2 dispatch regressions are in the `UCommon.Data.Tests` +fixture. + +## Per-grid request admission + +[GridThrottleProbe.dproj](GridThrottleProbe.dproj) checks that each VCL grid +admits requests through its own five-second timer and that the latest +parameters win. A full run of its [source](GridThrottleProbe.dpr) covers eight +groups. Set the selector beside a group to run only that group: + +- Refresh suspension (`PASCAL_GRID_SUSPENSION_ONLY=1`) +- Parentless and hidden populated grids +- Mixed inputs and independent timers +- Busy single and latest requests +- Held result and completion input +- Cancel, mode and disposal +- Completion without timer dispatch (`PASCAL_GRID_COMPLETION_ONLY=1`) +- Cancellation before execution (`PASCAL_GRID_EARLY_CANCEL_ONLY=1`) + +`PASCAL_GRID_LOADING_ONLY=1` runs four queued-loading checks that the full run +omits. Unset selectors for a full run. Selectors also work through the broker. +For example, run only cancellation before execution directly from the +repository root: + +```powershell +$env:PASCAL_GRID_EARLY_CANCEL_ONLY = '1' +.\src\tests\experimental\lib\Delphi\Win64\grid-throttle\Debug\GridThrottleProbe.exe <new-folder>\GridEarlyCancel.log +``` + +The probe builds into +`src/tests/experimental/lib/Delphi/<Platform>/grid-throttle/<Config>`. + +[`RefreshSuspended`](../../../../libraries/sphere10/UVisualGrid.pas) pauses a +grid's request admission. Use it on the UI thread. While suspended, the grid +keeps the latest request, asks active work to cancel and ignores its results and +errors. To resume at once, cancel `RefreshThrottle` while still suspended, then +clear `RefreshSuspended`. One latest query starts after any canceled worker +retires. + +## Shared grid checks + +- `UVisualGrid.Paging.Checks` tests all six paging-mode changes from a scrolled + result. +- `UVisualGrid.Loading.Checks` has five FPCUnit cases for loading and error + cards during throttled layout changes and after provider failures. +- `UVisualGrid.Selection.Checks` tests four row-click and selection-clearing + cases. Run them with `PASCAL_GRID_SELECTION_ONLY=1` on PascalFramework's + `GridViewportProbe`, or through FPCUnit `TVisualGridSelectionEventTests`. + Mouse cases use no global input. +- The [worker checks](../../framework/UVisualGrid.Worker.Checks.pas) cover + cancellation before execution and three public `CancelActiveFetch` cases. Run + the three with `PASCAL_GRID_ACTIVE_CANCEL_ONLY=1` on `GridViewportProbe`, or + as the active-cancellation methods in `TVisualGridQueuedCompletionTests`. + With `VISUALGRID_TEST_WORKER_START` defined, the selector also tests calling + `CancelActiveFetch` before `Start`. + +`GridThrottleProbe` and the +[experimental FPCUnit runner](../../RunExperimentalWalletTests.ps1) define +`VISUALGRID_TEST_WORKER_START`. Normal applications and packages omit it. Use +separate compiled-unit output for builds with and without it. + +## Recorded results + +`DataRowVariantProbe` has passed in Delphi, as has `GridThrottleProbe` in full +and with its suspension, loading and early-cancel selectors. diff --git a/src/tests/experimental/native/common/ReadWriteLockProbe.dpr b/src/tests/experimental/native/common/ReadWriteLockProbe.dpr new file mode 100644 index 000000000..64396aa19 --- /dev/null +++ b/src/tests/experimental/native/common/ReadWriteLockProbe.dpr @@ -0,0 +1,308 @@ +program ReadWriteLockProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Actual native Delphi checks for the public generic lock contract. + Distributed under the MIT software license; see the accompanying LICENSE. } +{$APPTYPE CONSOLE} + +uses Winapi.Windows, System.SysUtils, System.Classes, System.SyncObjs, + System.Generics.Collections, UConcurrency; + +type + TAction = (acRead, acWrite, acUpgrade, acPromote, acReleaseRead, acReleaseWrite, acReleaseUpgrade); + TActor = class(TThread) + private FLock:TReadWriteLock; FAction:TAction; FTimeout:Integer; + protected procedure Execute; override; + public Entered, ReleaseGate, Promote, Promoted, Done:TEvent; + Acquired:Boolean; ErrorClass, ExpectedError:String; OwnR,OwnW,OwnU:Integer; + Elapsed:UInt64; + constructor Create(ALock:TReadWriteLock; AAction:TAction; ATimeout:Integer); + destructor Destroy; override; + end; + TProbe = class + private L:TReadWriteLock; Actors:TList<TActor>; LogPath:String; + procedure Require(AValue:Boolean; const AMessage:String); + procedure Log(const AText:String); + function Worker(AAction:TAction; ATimeout:Integer=1500; const AExpectedError:String=''):TActor; + procedure Await(AEvent:TEvent; const AMessage:String); + procedure WaitCount(AKind,AValue:Integer); + procedure Clean; + procedure Reset(APolicy:TLockRecursionPolicy=lrpNoRecursion); + procedure ExpectRecursion(AAction:TAction); + procedure ConcurrentReaders; + procedure ExclusiveWriter; + procedure RecursionPolicies; + procedure UpgradeAndDowngrade; + procedure PromotionQueue; + procedure TimeoutsAndDispose; + procedure WrongThreadAndInvalidTimeout; + public constructor Create(const ALogPath:String); + destructor Destroy; override; + procedure Run; + end; +constructor TActor.Create(ALock:TReadWriteLock; AAction:TAction; ATimeout:Integer); +begin + inherited Create(True); FreeOnTerminate:=False; + FLock:=ALock; FAction:=AAction; FTimeout:=ATimeout; + Entered:=TEvent.Create(nil,True,False,''); ReleaseGate:=TEvent.Create(nil,True,False,''); + Promote:=TEvent.Create(nil,True,False,''); Promoted:=TEvent.Create(nil,True,False,''); + Done:=TEvent.Create(nil,True,False,''); +end; +destructor TActor.Destroy; +begin + Terminate; ReleaseGate.SetEvent; Promote.SetEvent; WaitFor; + Entered.Free; ReleaseGate.Free; Promote.Free; Promoted.Free; Done.Free; inherited; +end; +procedure TActor.Execute; +var T:UInt64; +begin + T:=GetTickCount64; + try + try + case FAction of + acRead: Acquired:=FLock.TryAcquireReadLock(FTimeout); + acWrite: Acquired:=FLock.TryAcquireWriteLock(FTimeout); + acUpgrade,acPromote: Acquired:=FLock.TryAcquireUpgradeableReadLock(FTimeout); + acReleaseRead: FLock.ReleaseReadLock; + acReleaseWrite: FLock.ReleaseWriteLock; + acReleaseUpgrade: FLock.ReleaseUpgradeableReadLock; + end; + Elapsed:=GetTickCount64-T; + if Acquired then begin + try + OwnR:=FLock.RecursiveReadCount; OwnW:=FLock.RecursiveWriteCount; OwnU:=FLock.RecursiveUpgradeCount; + Entered.SetEvent; + if FAction=acPromote then begin + if Promote.WaitFor(1500)<>wrSignaled then raise Exception.Create('Promotion request gate timed out'); + FLock.AcquireWriteLock; + try + Promoted.SetEvent; + while not Terminated do if ReleaseGate.WaitFor(10)=wrSignaled then Break; + finally FLock.ReleaseWriteLock; end; + end else + while not Terminated do if ReleaseGate.WaitFor(10)=wrSignaled then Break; + finally + case FAction of + acRead: FLock.ReleaseReadLock; + acWrite: FLock.ReleaseWriteLock; + acUpgrade,acPromote: FLock.ReleaseUpgradeableReadLock; + end; + end; + end; + except on E:Exception do ErrorClass:=E.ClassName; end; + finally Done.SetEvent; end; +end; +constructor TProbe.Create(const ALogPath:String); +begin + inherited Create; + LogPath:=ALogPath; + Actors:=TList<TActor>.Create; +end; +destructor TProbe.Destroy; +begin + Clean; + Actors.Free; + inherited; +end; +procedure TProbe.Require(AValue:Boolean; const AMessage:String); +begin + if not AValue then + raise Exception.Create(AMessage); +end; +procedure TProbe.Log(const AText:String); +var F:TextFile; +begin + AssignFile(F,LogPath); if FileExists(LogPath) then Append(F) else Rewrite(F); + try WriteLn(F,GetTickCount64,' ',AText); finally CloseFile(F); end; +end; +function TProbe.Worker(AAction:TAction; ATimeout:Integer; const AExpectedError:String):TActor; +begin + Result:=TActor.Create(L,AAction,ATimeout); Result.ExpectedError:=AExpectedError; + Actors.Add(Result); Result.Start; +end; +procedure TProbe.Await(AEvent:TEvent; const AMessage:String); +begin + Require(AEvent.WaitFor(2000)=wrSignaled,AMessage); +end; +procedure TProbe.WaitCount(AKind,AValue:Integer); +var T:UInt64; N:Integer; +begin + T:=GetTickCount64+1500; + repeat + case AKind of + 0: N:=L.WaitingReadCount; + 1: N:=L.WaitingWriteCount;else N:=L.WaitingUpgradeCount; + end; + if N=AValue then Exit; + Sleep(1); + until GetTickCount64>=T; + raise Exception.Create('Actual waiter count did not reach '+IntToStr(AValue)); +end; +procedure TProbe.Clean; +var A:TActor; Err:String; +begin + if Assigned(L) then begin + while L.RecursiveWriteCount>0 do L.ReleaseWriteLock; + while L.RecursiveReadCount>0 do L.ReleaseReadLock; + while L.RecursiveUpgradeCount>0 do L.ReleaseUpgradeableReadLock; + end; + for A in Actors do begin + A.Terminate; + A.ReleaseGate.SetEvent; + A.Promote.SetEvent; + end; + Err:=''; + for A in Actors do begin + A.WaitFor; + if (A.ErrorClass<>A.ExpectedError) and (Err='') then + Err:='Actor exception '+A.ErrorClass+' expected '+A.ExpectedError; + end; + for A in Actors do A.Free; + Actors.Clear; + FreeAndNil(L); + if Err<>'' then raise Exception.Create(Err); +end; +procedure TProbe.Reset(APolicy:TLockRecursionPolicy); +begin + Clean; + L:=TReadWriteLock.Create(APolicy); +end; +procedure TProbe.ExpectRecursion(AAction:TAction); +var Failed:Boolean; +begin + Failed:=False; + try + case AAction of + acRead: L.TryAcquireReadLock(0); + acWrite: L.TryAcquireWriteLock(0); + acUpgrade: L.TryAcquireUpgradeableReadLock(0); + end; + except on E:ELockRecursionException do Failed:=True;end; + Require(Failed,'Forbidden recursion/promotion did not raise ELockRecursionException'); +end; +procedure TProbe.ConcurrentReaders; +var A,B:TActor; +begin + Reset; + A:=Worker(acRead); Await(A.Entered,'First native reader'); + B:=Worker(acRead); Await(B.Entered,'Concurrent native reader'); + Require((L.CurrentReadCount=2) and not L.IsReadLockHeld,'Reader count/ownership is not thread-specific'); + Require((A.OwnR=1) and (B.OwnR=1),'Each reader must own one recursion'); + Log('PASS concurrent native readers and thread-specific ownership/counts'); +end; +procedure TProbe.ExclusiveWriter; +var W,R,W2,U:TActor; +begin + Reset; W:=Worker(acWrite); Await(W.Entered,'Native writer'); + R:=Worker(acRead,0);W2:=Worker(acWrite,0);U:=Worker(acUpgrade,0); + Await(R.Done,'Read zero-timeout');Await(W2.Done,'Write zero-timeout');Await(U.Done,'Upgrade zero-timeout'); + Require(not R.Acquired and not W2.Acquired and not U.Acquired,'Writer allowed concurrent foreign ownership'); + Require((L.WaitingReadCount=0) and (L.WaitingWriteCount=0) and (L.WaitingUpgradeCount=0),'Zero-timeout registered a waiter'); + Log('PASS writer exclusion across all modes and zero-timeout cleanup'); +end; +procedure TProbe.RecursionPolicies; +begin + Reset; + L.AcquireReadLock;ExpectRecursion(acRead);ExpectRecursion(acWrite);ExpectRecursion(acUpgrade);L.ReleaseReadLock; + L.AcquireWriteLock;ExpectRecursion(acWrite);ExpectRecursion(acRead);ExpectRecursion(acUpgrade);L.ReleaseWriteLock; + L.AcquireUpgradeableReadLock;L.AcquireReadLock;ExpectRecursion(acWrite);L.ReleaseReadLock; + Require(L.TryAcquireWriteLock(0),'Default upgrader could not promote without an explicit reader');L.ReleaseWriteLock;L.ReleaseUpgradeableReadLock; + Reset(lrpSupportsRecursion); + L.AcquireWriteLock;L.AcquireWriteLock;L.AcquireReadLock;L.AcquireUpgradeableReadLock; + Require((L.RecursiveWriteCount=2) and (L.RecursiveReadCount=1) and (L.RecursiveUpgradeCount=1),'Supported recursion counts differ'); + L.ReleaseUpgradeableReadLock;L.ReleaseReadLock;L.ReleaseWriteLock;L.ReleaseWriteLock; + L.AcquireReadLock;L.AcquireReadLock;Require(L.CurrentReadCount=1,'Recursive read counted as multiple owners'); + ExpectRecursion(acWrite);ExpectRecursion(acUpgrade);L.ReleaseReadLock;L.ReleaseReadLock; + Log('PASS default/supported recursion and forbidden ordinary-read promotion'); +end; +procedure TProbe.UpgradeAndDowngrade; +var R,W:TActor; +begin + Reset(lrpSupportsRecursion); + L.AcquireUpgradeableReadLock;L.AcquireReadLock; + R:=Worker(acRead);Await(R.Entered,'Foreign reader shares upgradeable'); + Require(not L.TryAcquireWriteLock(60),'Promotion ignored foreign reader'); + Require(L.IsUpgradeableReadLockHeld and not L.IsWriteLockHeld,'Timed-out promotion lost ownership'); + R.ReleaseGate.SetEvent;Await(R.Done,'Foreign reader release'); + Require(L.TryAcquireWriteLock(500),'Promotion failed after last foreign reader left'); + L.ReleaseUpgradeableReadLock;L.ReleaseWriteLock; + Require(L.IsReadLockHeld and not L.IsWriteLockHeld,'Write-to-read downgrade lost reader'); + W:=Worker(acWrite,0);Await(W.Done,'Downgrade writer test');Require(not W.Acquired,'Downgrade lost reader exclusion'); + L.ReleaseReadLock; + Log('PASS upgrade timeout/retry and write-to-read downgrade retains exclusion'); +end; +procedure TProbe.PromotionQueue; +var U,R:TActor; +begin + Reset; + L.AcquireReadLock;U:=Worker(acPromote);Await(U.Entered,'Upgradeable reader'); + U.Promote.SetEvent;WaitCount(1,1); + R:=Worker(acRead);WaitCount(0,1); + Require(U.Promoted.WaitFor(0)=wrTimeout,'Promotion overlapped foreign reader'); + L.ReleaseReadLock;Await(U.Promoted,'Queued upgrade promotion'); + Require(R.Entered.WaitFor(0)=wrTimeout,'New reader bypassed queued promoted writer'); + U.ReleaseGate.SetEvent;Await(U.Done,'Promoted writer release');Await(R.Entered,'Queued reader recovery'); + Log('PASS queued promotion and reader recovery without ownership loss'); +end; +procedure TProbe.TimeoutsAndDispose; +var R:TActor; Rejected:Boolean; +begin + Reset;L.AcquireWriteLock; + R:=Worker(acRead,90);Await(R.Done,'Timed reader'); + Require(not R.Acquired and (R.Elapsed>=75) and (R.Elapsed<1000),'Finite reader timeout was not bounded'); + Require(L.WaitingReadCount=0,'Timed waiter remained registered'); + R:=Worker(acRead,-1);WaitCount(0,1); + Rejected:=False;try L.Dispose;except on E:ESynchronizationLockException do Rejected:=True;end; + Require(Rejected and L.IsWriteLockHeld and (L.WaitingReadCount=1),'Rejected Dispose changed owners/waiters'); + L.ReleaseWriteLock;Await(R.Entered,'Waiter recovery after rejected Dispose'); + R.ReleaseGate.SetEvent;Await(R.Done,'Infinite reader release'); + L.Dispose;Rejected:=False; + try L.TryAcquireReadLock(0);except on E:ELockDisposedException do Rejected:=True;end; + Require(Rejected,'Disposed lock accepted an acquisition'); + Log('PASS finite/infinite timeout, rejected Dispose, waiter recovery and disposed rejection'); +end; +procedure TProbe.WrongThreadAndInvalidTimeout; +var A:TAction; W:TActor; Rejected:Boolean; +begin + for A:=acReleaseRead to acReleaseUpgrade do begin + Reset; + case A of + acReleaseRead: L.AcquireReadLock; + acReleaseWrite: L.AcquireWriteLock; + acReleaseUpgrade: L.AcquireUpgradeableReadLock; + end; + W:=Worker(A,0,'ESynchronizationLockException');Await(W.Done,'Wrong-thread release'); + Require(W.ErrorClass='ESynchronizationLockException','Wrong-thread release was accepted'); + case A of + acReleaseRead: Require(L.IsReadLockHeld,'Wrong-thread lost read'); + acReleaseWrite: Require(L.IsWriteLockHeld,'Wrong-thread lost write'); + acReleaseUpgrade: Require(L.IsUpgradeableReadLockHeld,'Wrong-thread lost upgrade'); + end; + end; + Reset; + Rejected:=False;try L.TryAcquireReadLock(-2);except on E:EArgumentOutOfRangeException do Rejected:=True;end; + Require(Rejected and (L.CurrentReadCount=0),'Invalid timeout altered ownership'); + Log('PASS wrong-thread release protection and invalid timeout rejection'); +end; +procedure TProbe.Run; +begin + ConcurrentReaders;ExclusiveWriter;RecursionPolicies;UpgradeAndDowngrade; + PromotionQueue;TimeoutsAndDispose;WrongThreadAndInvalidTimeout;Clean; + Log('PASS all seven native read/write lock contract groups'); +end; +var Probe:TProbe; +begin + if ParamCount<>1 then Halt(64); + Probe:=nil; + try + ReportMemoryLeaksOnShutdown:=True; + Probe:=TProbe.Create(ExpandFileName(ParamStr(1))); + try Probe.Run;finally FreeAndNil(Probe);end; + ExitCode:=0; + except on E:Exception do begin + with TStringList.Create do + try if FileExists(ParamStr(1)) then LoadFromFile(ParamStr(1));Add('FAIL '+E.ClassName+': '+E.Message);SaveToFile(ParamStr(1));finally Free;end; + ExitCode:=1; + end;end; + Probe.Free; +end. diff --git a/src/tests/experimental/native/common/ReadWriteLockProbe.dproj b/src/tests/experimental/native/common/ReadWriteLockProbe.dproj new file mode 100644 index 000000000..669eda13d --- /dev/null +++ b/src/tests/experimental/native/common/ReadWriteLockProbe.dproj @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{E5D6969F-D5EC-492E-B915-304FCE4FD509}</ProjectGuid> + <MainSource>ReadWriteLockProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Console</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\libraries\sphere10</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\native-read-write\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\native-read-write\$(Platform)\$(Config)\bin</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\native-read-write\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup><DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile></ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">ReadWriteLockProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/common/ScopeProbe.dpr b/src/tests/experimental/native/common/ScopeProbe.dpr new file mode 100644 index 000000000..0d5ecbf63 --- /dev/null +++ b/src/tests/experimental/native/common/ScopeProbe.dpr @@ -0,0 +1,31 @@ +program ScopeProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$APPTYPE CONSOLE} + +uses SysUtils, UScope.Checks, UDisposables.Scope.Checks, UReadWriteScope.Checks; + +var I: Integer; +begin + {$IFNDEF FPC}ReportMemoryLeaksOnShutdown := True;{$ENDIF} + try + for I := 1 to ScopeCheckCount do begin + RunScopeCheck(I); + WriteLn('PASS scope: ', ScopeCheckName(I)); + end; + RunDisposablesScopeChecks; + WriteLn('PASS disposal bag: ', DisposablesScopeCheckCount, ' checks'); + for I := 1 to ReadWriteScopeCheckCount do begin + RunReadWriteScopeCheck(I); + WriteLn('PASS lock scope: ', ReadWriteScopeCheckName(I)); + end; + WriteLn('PASS all ', ScopeCheckCount + DisposablesScopeCheckCount + + ReadWriteScopeCheckCount, ' shared scope checks'); + ExitCode := 0; + except + on E: Exception do begin + WriteLn('FAIL ', E.ClassName, ': ', E.Message); + ExitCode := 1; + end; + end; +end. diff --git a/src/tests/experimental/native/common/ScopeProbe.dproj b/src/tests/experimental/native/common/ScopeProbe.dproj new file mode 100644 index 000000000..9adb8a1df --- /dev/null +++ b/src/tests/experimental/native/common/ScopeProbe.dproj @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{849A6B35-8E44-4218-ACBA-BF4B61B5F051}</ProjectGuid> + <MainSource>ScopeProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Console</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\framework;..\..\..;..\..\..\..\libraries\sphere10</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\native-scopes\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\native-scopes\$(Platform)\$(Config)\bin</DCC_ExeOutput> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup><DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile></ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">ScopeProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/common/UGridLoadingProbe.pas b/src/tests/experimental/native/common/UGridLoadingProbe.pas new file mode 100644 index 000000000..5b6c9d5a9 --- /dev/null +++ b/src/tests/experimental/native/common/UGridLoadingProbe.pas @@ -0,0 +1,211 @@ +unit UGridLoadingProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Delphi/VCL manual grid loading regressions with generated rows and owned forms. + Distributed under the MIT software license; see the accompanying LICENSE. } + + +interface +uses Classes,Forms; +procedure InitializeGridLoadingProbe(AForm:TForm;const ALog:String); +function GridLoadingProbePassed:Boolean; +implementation +uses SysUtils,Windows,Controls,ExtCtrls,StdCtrls,Graphics,SyncObjs,Generics.Collections,Variants, + UCommon.Data,UVisualGrid,UCommon.UI; +type + TSource=class(TCustomDataSource<Integer>) + public FetchCount:LongInt;BlockRead,FailRead:Boolean;Entered,ReleaseRead:TEvent; + constructor Create(AOwner:TComponent);override;destructor Destroy;override; + function GetColumns:TDataColumns;override; + function GetItemField(const AItem:Integer;const AName:String):Variant;override; + procedure FetchAll(const AContainer:TList<Integer>);override; + end; + TGridAccess=class(TVisualGrid) + public function RegionsEnabled:Boolean; + end; + TProbe=class(TComponent) + private FForm:TForm;FGrid:TVisualGrid;FSource:TSource;FTimer:TTimer; + FPhase:Integer;FInTick:Boolean;FDeadline,FAt:UInt64;FCount:Integer; + procedure Require(C:Boolean;const S:String);procedure Tick(Sender:TObject); + function Card:TPanel;function Title:TLabel;function Subtitle:TLabel; + procedure CheckGraphic;procedure CheckUnparentedFocus; + public constructor Create(AForm:TForm);reintroduce;destructor Destroy;override; + end; +var LogPath:String;Passed:Boolean; +procedure Log(const S:String); +var F:TFileStream;B:TBytes; +begin + if FileExists(LogPath)then F:=TFileStream.Create(LogPath,fmOpenReadWrite or fmShareDenyNone)else F:=TFileStream.Create(LogPath,fmCreate); + try F.Position:=F.Size;B:=TEncoding.UTF8.GetBytes(UIntToStr(GetTickCount64)+' '+S+#13#10);F.WriteBuffer(B[0],Length(B));finally F.Free;end; +end; +constructor TSource.Create(AOwner:TComponent); +begin + inherited; + Entered:=TEvent.Create(nil,True,False,''); + ReleaseRead:=TEvent.Create(nil,True,False,''); +end; +destructor TSource.Destroy; +begin + Entered.Free; + ReleaseRead.Free; + inherited; +end; +function TSource.GetColumns:TDataColumns; +begin + Result:=TDataColumns.Create(TDataColumn.From('Id',True)); +end; +function TSource.GetItemField(const AItem:Integer;const AName:String):Variant; +begin + Result:=AItem; +end; +procedure TSource.FetchAll(const AContainer:TList<Integer>); +var I:Integer; +begin + InterlockedIncrement(FetchCount);Entered.SetEvent; + if BlockRead and(ReleaseRead.WaitFor(5000)<>wrSignaled)then raise Exception.Create('Native source gate timeout'); + if FailRead then raise Exception.Create('Native controlled fetch failure'); + for I:=0 to 99 do AContainer.Add(I); +end; +function TGridAccess.RegionsEnabled:Boolean; +begin + Result:=FTopPanel.Enabled and FTopPanelMultiSearch.Enabled and FBottomPanel.Enabled; +end; +constructor TProbe.Create(AForm:TForm); +begin + inherited Create(AForm);FForm:=AForm;FDeadline:=GetTickCount64+12000; + FSource:=TSource.Create(AForm);FGrid:=TVisualGrid.Create(AForm); + FGrid.ShowLoading('Before parenting','Held before native handles'); + Require(FGrid.LoadingVisible and not FGrid.HandleAllocated,'Unparented manual Show created a native grid handle'); + FGrid.Parent:=AForm;FGrid.Align:=alClient;FGrid.FetchDataInThread:=True; + FGrid.AddColumn('Id').Binding:='Id';FGrid.DataSource:=FSource; + FTimer:=TTimer.Create(Self);FTimer.Interval:=30;FTimer.OnTimer:=Tick; +end; +destructor TProbe.Destroy; +begin + if Assigned(FTimer) then FTimer.Enabled:=False; + if Assigned(FSource) then FSource.ReleaseRead.SetEvent; + inherited; +end; +procedure TProbe.Require(C:Boolean;const S:String); +begin + if not C then raise Exception.Create(S); +end; +function TProbe.Card:TPanel; +begin + Result:=FGrid.FindComponent('LoadingOverlay')as TPanel; +end; +function TProbe.Title:TLabel; +begin + Result:=FGrid.FindComponent('LoadingStatus')as TLabel; +end; +function TProbe.Subtitle:TLabel; +begin + Result:=FGrid.FindComponent('LoadingSubtitle')as TLabel; +end; +procedure TProbe.CheckGraphic; +var B:TBitmap;I:TImage; +begin + FCount:=FSource.FetchCount;B:=TBitmap.Create; + try B.SetSize(8,8);B.Canvas.Brush.Color:=clRed;B.Canvas.FillRect(Rect(0,0,8,8)); + FGrid.ShowLoading('Custom title','Custom subtitle',B); + B.Canvas.Brush.Color:=clBlue;B.Canvas.FillRect(Rect(0,0,8,8)); + finally B.Free;end; + I:=FGrid.FindComponent('LoadingImage')as TImage; + Require(FGrid.LoadingVisible and Card.Visible,'Manual card missing'); + Require((Title.Caption='Custom title')and(Subtitle.Caption='Custom subtitle'),'Manual labels missing'); + Require((fsBold in Title.Font.Style)and not(fsBold in Subtitle.Font.Style),'Manual font styles wrong'); + Require(I.Visible and(I.Picture.Width=8)and(ColorToRGB(I.Picture.Bitmap.Canvas.Pixels[1,1])=ColorToRGB(clRed)),'Graphic not copied'); + Require(not TLoadingCircle(FGrid.FindComponent('LoadingCircle')).Visible,'Custom image did not replace circle'); + Require(FGrid.Enabled and not FGrid.InternalDrawGrid.Enabled and not TGridAccess(FGrid).RegionsEnabled,'Manual hold did not gate complete input regions'); + Require(FSource.FetchCount=FCount,'Show initiated a query'); + B:=FForm.GetFormImage;try B.SaveToFile(ChangeFileExt(LogPath,'-custom.bmp'));finally B.Free;end; + FGrid.Enabled:=False;FGrid.HideLoading;Require(not FGrid.Enabled,'Hide enabled caller-disabled grid');FGrid.Enabled:=True; + FGrid.ShowLoading('Manual hold','A worker may complete'); + Log('PASS custom labels/imagecopy/input regions/callerdisabled/noquery'); +end; +procedure TProbe.CheckUnparentedFocus; +var D:TForm;E:TEdit;G:TVisualGrid;Active,Focus:HWND; +begin + D:=TForm.CreateNew(FForm);G:=nil; + try + E:=TEdit.Create(D);E.Parent:=D;D.SetBounds(200,180,300,150);D.Show;D.BringToFront;E.SetFocus; + Active:=GetActiveWindow;Focus:=GetFocus; + Require((Active=D.Handle)and(Focus=E.Handle),'Owned modeless dialog did not acquire focus'); + G:=TVisualGrid.Create(FForm);G.ShowLoading('Unparented again','Do not activate the host'); + Require(not G.HandleAllocated,'Unparented Show allocated HWND'); + G.SetBounds(0,0,320,180);G.Parent:=FForm; + Require((GetActiveWindow=Active)and(GetFocus=Focus),'Parenting held overlay activated background main form'); + FGrid.ShowLoading('Background changed','Preserve modeless keyboard focus'); + Require((GetActiveWindow=Active)and(GetFocus=Focus),'Showing manual card activated background main form'); + G.HideLoading;G.Free;G:=nil; + Require((GetActiveWindow=Active)and(GetFocus=Focus),'Hide changed modeless activation'); + Log('PASS unparentedstate/attach/modelessactivation/keyboardfocus'); + finally G.Free;D.Free;end; +end; +procedure TProbe.Tick(Sender:TObject); +var B:TBitmap; +begin + if FInTick then Exit;FInTick:=True; + try + try + Require(GetTickCount64<FDeadline,'Native manual-loading fixture timeout'); + case FPhase of + 0:begin + if FGrid.IsUpdating then Exit; + Require(FGrid.LoadingVisible and(Title.Caption='Before parenting'),'Initial fetch cleared unparented manual state'); + Require(not FGrid.IsUpdating,'Manual hold changed worker IsUpdating');CheckGraphic; + FSource.BlockRead:=True;FSource.Entered.ResetEvent;FGrid.RefreshGrid;FPhase:=1; + end; + 1:begin + if FSource.Entered.WaitFor(0)<>wrSignaled then Exit; + Require(FGrid.IsUpdating and FGrid.LoadingVisible,'Held manual worker state missing'); + FGrid.HideLoading;Require(FGrid.IsUpdating and not FGrid.LoadingVisible and not FGrid.InternalDrawGrid.Enabled,'Hide overrode worker/input state'); + FAt:=GetTickCount64+350;FPhase:=2; + end; + 2:begin + if GetTickCount64<FAt then Exit; + Require(Card.Visible and TLoadingCircle(FGrid.FindComponent('LoadingCircle')).Active,'Delayed automatic card not restored'); + FSource.ReleaseRead.SetEvent;FPhase:=3; + end; + 3:begin + if FGrid.IsUpdating then Exit; + Require(not Card.Visible and FGrid.InternalDrawGrid.Enabled,'Automatic completion did not restore controls'); + FGrid.ShowLoading('Held across error','Independent subtitle');FSource.FailRead:=True;FGrid.RefreshGrid;FPhase:=4; + end; + 4:begin + if FGrid.IsUpdating then Exit; + Require(FGrid.LoadingVisible and(Title.Caption='Held across error'),'Fetch error overwrote manual text'); + FGrid.HideLoading;Require(Card.Visible and(Title.Caption='Native controlled fetch failure'),'Hide did not expose fetch error'); + FSource.FailRead:=False;FGrid.ShowLoading('Nonblocking','',nil,False);FGrid.RefreshGrid;FPhase:=5; + end; + 5:begin + if FGrid.IsUpdating then Exit; + Require(FGrid.LoadingVisible and FGrid.InternalDrawGrid.Enabled and TGridAccess(FGrid).RegionsEnabled,'Nonblocking overlay changed idle input state'); + CheckUnparentedFocus;FGrid.HideLoading;Require(not Card.Visible,'Final hide retained idle card'); + B:=FForm.GetFormImage;try B.SaveToFile(ChangeFileExt(LogPath,'.bmp'));finally B.Free;end; + Passed:=True;Log('PASS manual/automatic/error layering; sourcefetches='+IntToStr(FSource.FetchCount));FTimer.Enabled:=False;Application.Terminate; + end; + end; + except on E:Exception do begin + Log('FAIL '+E.ClassName+': '+E.Message); + Passed:=False; + FTimer.Enabled:=False; + FSource.ReleaseRead.SetEvent; + Application.Terminate; + end;end; + finally FInTick:=False;end; +end; +procedure InitializeGridLoadingProbe(AForm:TForm;const ALog:String); +var F:TFileStream; +begin + LogPath:=ALog; + F:=TFileStream.Create(LogPath,fmCreate); + F.Free; + TProbe.Create(AForm); +end; +function GridLoadingProbePassed:Boolean; +begin + Result:=Passed; +end; +end. \ No newline at end of file diff --git a/src/tests/experimental/native/common/UWizardStringChecks.pas b/src/tests/experimental/native/common/UWizardStringChecks.pas new file mode 100644 index 000000000..3eb57445f --- /dev/null +++ b/src/tests/experimental/native/common/UWizardStringChecks.pas @@ -0,0 +1,112 @@ +unit UWizardStringChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. } + +{$IFDEF FPC}{$mode delphi}{$H+}{$codepage UTF8}{$ENDIF} + +interface + +uses Classes; + +procedure CheckNativeWizardStrings(ALog: TStrings); + +implementation + +uses SysUtils, UMemory, UWizard; + +const + CT_NativeText: String = 'Résumé — 漢字'; + +type + TStringScreen = class(TWizardForm<Integer>) + public + RejectValidation: Boolean; + constructor Create(AOwner: TComponent); override; + function Validate(out message: String): Boolean; override; + end; + + TStringWizard = class(TWizard<Integer>) + private + FAllowCancel: Boolean; + FFinishCalls: Integer; + protected + procedure ShowHost; override; + procedure ShowError(const message: String); override; + function FinishRequested(out message: String): Boolean; override; + function CancelRequested(out message: String): Boolean; override; + public + constructor Create; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +constructor TStringScreen.Create(AOwner: TComponent); +begin + inherited CreateNew(AOwner); + RejectValidation := True; +end; + +function TStringScreen.Validate(out message: String): Boolean; +begin + message := CT_NativeText; + Result := not RejectValidation; +end; + +constructor TStringWizard.Create; +begin + inherited Create(nil, [TStringScreen]); + TitleText := CT_NativeText; + FinishText := CT_NativeText; +end; + +procedure TStringWizard.ShowHost; +var LMessage: String; +begin + // Exercise real host captions and all out-message paths using native String. + Require(Host.TitleText = CT_NativeText, 'Wizard title lost native text'); + Require(Host.NextText = CT_NativeText, 'Wizard button lost native text'); + Require(not Finish(LMessage), 'Validation must reject finishing'); + Require((LMessage = CT_NativeText) and (FFinishCalls = 0), + 'Validation message changed or executed the finish callback'); + TStringScreen(CurrentScreen).RejectValidation := False; + Require(not Finish(LMessage), 'Finish callback must reject finishing'); + Require((LMessage = CT_NativeText) and (FFinishCalls = 1), + 'Finish message changed or its callback ran more than once'); + Require(not Cancel(LMessage), 'Cancel callback must reject cancellation'); + Require(LMessage = CT_NativeText, 'Cancel message lost native text'); + FAllowCancel := True; + Require(Cancel(LMessage), 'Accepted cancellation did not complete'); +end; + +procedure TStringWizard.ShowError(const message: String); +begin + raise Exception.Create('Unexpected wizard dialog: ' + message); +end; + +function TStringWizard.FinishRequested(out message: String): Boolean; +begin + Inc(FFinishCalls); + message := CT_NativeText; + Result := False; +end; + +function TStringWizard.CancelRequested(out message: String): Boolean; +begin + message := CT_NativeText; + Result := FAllowCancel; +end; + +procedure CheckNativeWizardStrings(ALog: TStrings); +var LScope: TDisposables; LWizard: TStringWizard; +begin + LWizard := LScope.AddObject(TStringWizard.Create) as TStringWizard; + LWizard.Start(1); + Require(LWizard.State = wsCancelled, 'Wizard did not retain its terminal state'); + ALog.Add('PASS native String wizard captions, validation, finish and cancel messages'); +end; + +end. diff --git a/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.dpr b/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.dpr new file mode 100644 index 000000000..a099cd70d --- /dev/null +++ b/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.dpr @@ -0,0 +1,8 @@ +program ConnectionRestartProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$APPTYPE CONSOLE} +uses System.SysUtils, UConnectionRestartChecks; +begin + if ParamCount<>1 then Halt(64); + ExitCode := RunConnectionRestartChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.dproj b/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.dproj new file mode 100644 index 000000000..fecc45a09 --- /dev/null +++ b/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.dproj @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{1F2568C7-6FEB-4564-8E98-A0C514DDDCA3}</ProjectGuid> + <MainSource>ConnectionRestartProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Console</AppType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\generics.collections\inc</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\connection-restart\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\connection-restart\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UConnectionRestartChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">ConnectionRestartProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.lpr b/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.lpr new file mode 100644 index 000000000..3fc47f10a --- /dev/null +++ b/src/tests/experimental/native/connection-restart/ConnectionRestartProbe.lpr @@ -0,0 +1,9 @@ +program ConnectionRestartProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$mode delphi} +uses heaptrc, SysUtils, UConnectionRestartChecks; +begin + if ParamCount<>1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunConnectionRestartChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/connection-restart/README.md b/src/tests/experimental/native/connection-restart/README.md new file mode 100644 index 000000000..4c4970cd4 --- /dev/null +++ b/src/tests/experimental/native/connection-restart/README.md @@ -0,0 +1,14 @@ +# Connection restart regression + +This probe checks that switching networking off and on retries a recently tried +peer at once, while automatic retries keep their backoff. + +Build `ConnectionRestartProbe.lpr` (FPC) or `ConnectionRestartProbe.dpr` +(Delphi) with the usual core and library search paths. Put OpenSSL 1.1 beside +the executable and point `TEMP` and `TMP` at a test folder. Run the probe +through the [private-desktop broker](../README.md#private-desktop-broker) with a new +absolute result-log path. +`../RunWalletRegressionTests.ps1 -Probe connection-restart` does all of this. + +The probe uses its own data folder and loopback ports. It never opens a user's +wallet or contacts public peers. diff --git a/src/tests/experimental/native/connection-restart/UConnectionRestartChecks.pas b/src/tests/experimental/native/connection-restart/UConnectionRestartChecks.pas new file mode 100644 index 000000000..e4ca36524 --- /dev/null +++ b/src/tests/experimental/native/connection-restart/UConnectionRestartChecks.pas @@ -0,0 +1,170 @@ +unit UConnectionRestartChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Explicit network restart retries recent peers; automatic discovery retains + its existing backoff. All sockets and node state belong to this probe. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunConnectionRestartChecks(const ALogFile: String): Integer; + +implementation + +uses Windows, Classes, SysUtils, SyncObjs, blcksock, UNode, UNetProtocol, UThread, + UPCDataTypes, UCrypto, UTime; + +type + TQuickDiscoveryWorker = class(TThreadDiscoverConnection) + protected + procedure BCExecute; override; + end; + + TRestartNetData = class(TNetData) + private + FCreatedWorkers: LongInt; + protected + function CreateDiscoveryWorker(const AAddress: TNodeServerAddress): TThreadDiscoverConnection; override; + public + function CreatedWorkers: LongInt; + end; + +procedure Require(AValue: Boolean; const AReason: String); +begin + if not AValue then raise Exception.Create(AReason); +end; + +procedure TQuickDiscoveryWorker.BCExecute; +var I, LJitter: Integer; +begin + // Keep the actual production connect/HELLO/finalization path. Choose only + // its random initial jitter so a regression is not hidden by a long delay. + for I := 0 to 100000 do begin + RandSeed := I; + LJitter := Random(1000); + if (LJitter<10) and (Random(5)=0) then begin + RandSeed := I; + Break; + end; + end; + inherited BCExecute; +end; + +function TRestartNetData.CreateDiscoveryWorker(const AAddress: TNodeServerAddress): TThreadDiscoverConnection; +begin + InterlockedIncrement(FCreatedWorkers); + Result := TQuickDiscoveryWorker.Create(Self,AAddress); +end; + +function TRestartNetData.CreatedWorkers: LongInt; +begin + Result := InterlockedCompareExchange(FCreatedWorkers,0,0); +end; + +function CheckRestart(AExplicitRestart: Boolean): Cardinal; +var LNode: TNode; LNet: TRestartNetData; LListener, LPeer: TTCPBlockSocket; + LAddress: TNodeServerAddress; LFixed, LCandidates: TNodeServerAddressArray; + LOldDirectory, LDirectory, LHello: String; LID: TGUID; LUntil, LStarted: UInt64; +begin + Result := 0; + Require(not TNode.NodeExists and not TNetData.NetDataExists, + Format('Restart probe needs fresh owned singletons (node:%s network:%s)',[ + BoolToStr(TNode.NodeExists,True),BoolToStr(TNetData.NetDataExists,True)])); + Require(GetEnvironmentVariable('TEMP')<>'','Provide an owned test temporary directory'); + LOldDirectory := TNode.GetPascalCoinDataFolder; + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP'))+'ConnectionRestart-'+GUIDToString(LID); + Require(not DirectoryExists(LDirectory),'Refusing an existing restart data directory'); + ForceDirectories(LDirectory); + TNode.SetPascalCoinDataFolder(LDirectory); + LNode := nil; LNet := nil; LListener := nil; LPeer := nil; + try + TCrypto.InitCrypto; + // Register the owned network before the node's saver/notification workers + // can request the global network singleton during node construction. + LNet := TRestartNetData.Create(nil); + Require(TNetData.NetData=LNet,'Restart fixture did not register its owned network singleton'); + LNode := TNode.Create(nil); + LNode.NetServer.Port := 0; + LNet.NetConnectionsActive := False; + LListener := TTCPBlockSocket.Create; + LListener.CreateSocket; + LListener.Bind('127.0.0.1','0'); + Require(LListener.LastError=0,'Cannot reserve owned loopback listener'); + LListener.Listen; + Require(LListener.LastError=0,'Cannot listen on owned loopback port'); + // For the automatic case, turn ON before supplying a candidate. Its later + // discovery calls must retain the established retry delay. + if not AExplicitRestart then LNet.NetConnectionsActive := True; + SetLength(LFixed,1); + LFixed[0].ip := '127.0.0.1'; + LFixed[0].port := LListener.GetLocalSinPort; + // Publish the candidate and its prior attempt atomically. The existing + // core maintenance worker must not discover it before backoff is seeded. + LNet.NodeServersAddresses.LockList; + try + LNet.DiscoverFixedServersOnly(LFixed); + Require(LNet.NodeServersAddresses.GetNodeServerAddress(LFixed[0].ip,LFixed[0].port,True,LAddress), + 'Cannot prepare recent peer attempt'); + LAddress.last_attempt_to_connect := Now; + LAddress.total_failed_attemps_to_connect := 3; + // Keep this genuinely recent peer eligible for the normal retained-peer + // retry rules; cleanup otherwise deletes never-successful failed peers. + LAddress.last_connection_by_me := UnivDateTimeToUnix(DateTime2UnivDateTime(Now)); + LNet.NodeServersAddresses.SetNodeServerAddress(LAddress); + finally LNet.NodeServersAddresses.UnlockList; end; + LCandidates := Copy(LFixed,0,Length(LFixed)); + LNet.NodeServersAddresses.GetNodeServersToConnnect(1,True,LCandidates); + Require(Length(LCandidates)=0,'The seeded recent attempt did not establish normal backoff'); + if AExplicitRestart then begin + LStarted := GetTickCount64; + LNet.NetConnectionsActive := True; + Require(LNet.CreatedWorkers>0,'Explicit OFF -> ON skipped its first discovery pass for a recent peer'); + LUntil := GetTickCount64+5000; + repeat + CheckSynchronize(0); + if LListener.CanRead(10) then Break; + Sleep(1); + until GetTickCount64>=LUntil; + Require(LListener.CanRead(0),'Restart did not make an actual loopback connection'); + LPeer := TTCPBlockSocket.Create; + LPeer.Socket := LListener.Accept; + LHello := String(LPeer.RecvPacket(1000)); + Require(Length(LHello)>0,'Restarted discovery did not send its actual HELLO'); + Result := Cardinal(GetTickCount64-LStarted); + end else begin + LNet.DiscoverServers; + Require(LNet.CreatedWorkers=0,'Automatic discovery discarded normal retry backoff'); + Require(not LListener.CanRead(100),'Automatic discovery connected during normal retry backoff'); + end; + finally + if Assigned(LNet) then LNet.NetConnectionsActive := False; + LNet.Free; + LNode.Free; + LPeer.Free; + LListener.Free; + TNode.SetPascalCoinDataFolder(LOldDirectory); + end; +end; + +function RunConnectionRestartChecks(const ALogFile: String): Integer; +var LLog: TStringList; LElapsed: Cardinal; +begin + Require(not FileExists(ALogFile),'Refusing an existing restart result log'); + Result := 1; + LLog := TStringList.Create; + try + try + CheckRestart(False); + LLog.Add('PASS automatic discovery preserves backoff'); + LLog.SaveToFile(ALogFile); + LElapsed := CheckRestart(True); + LLog.Add(Format('PASS explicit OFF -> ON starts first discovery, connects recent peer and sends actual HELLO; milliseconds:%d',[LElapsed])); + Result := 0; + except on E: Exception do LLog.Add('FAIL '+E.ClassName+': '+E.Message); end; + LLog.SaveToFile(ALogFile); + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.dpr b/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.dpr new file mode 100644 index 000000000..81c874b5a --- /dev/null +++ b/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.dpr @@ -0,0 +1,9 @@ +program DownloadSafeboxStopProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses UDownloadSafeboxStopChecks; + +begin + ExitCode := RunDownloadSafeboxStopProbe; +end. diff --git a/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.dproj b/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.dproj new file mode 100644 index 000000000..35c9b3dbd --- /dev/null +++ b/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.dproj @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{32962DC9-BF89-44B7-A809-F009C32F2507}</ProjectGuid> + <MainSource>DownloadSafeboxStopProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\download-stop\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\download-stop\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UDownloadSafeboxStopChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">DownloadSafeboxStopProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.lpr b/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.lpr new file mode 100644 index 000000000..ff24b0c8a --- /dev/null +++ b/src/tests/experimental/native/download-stop/DownloadSafeboxStopProbe.lpr @@ -0,0 +1,13 @@ +program DownloadSafeboxStopProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, UDownloadSafeboxStopChecks; + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + ExitCode := RunDownloadSafeboxStopProbe; +end. diff --git a/src/tests/experimental/native/download-stop/README.md b/src/tests/experimental/native/download-stop/README.md new file mode 100644 index 000000000..0edd705d4 --- /dev/null +++ b/src/tests/experimental/native/download-stop/README.md @@ -0,0 +1,24 @@ +# Safebox download shutdown regression + +Checks that stopping a safebox download causes no deadlock, lets in-flight +writes finish, deletes partial files, keeps saved files unchanged and publishes +no archive. It also covers destroying an empty downloader. + +Run the Lazarus/FPC probe from the repository root: + +```powershell +& src\tests\experimental\RunWalletDownloadStopTests.ps1 +``` + +The script builds into a new folder and runs the probe through the +[private-desktop broker](../README.md#private-desktop-broker). Pass +`-OutputDirectory <new-directory>` to choose the output folder. `-FpcPath` and +`-LazarusDirectory` select a Windows x64 FPC and Lazarus. The log and heap +report must show a natural exit and no leaks. + +The [Delphi project](DownloadSafeboxStopProbe.dproj) uses the same +[checks](UDownloadSafeboxStopChecks.pas). Build it with Delphi and put the +matching OpenSSL runtime beside the EXE. Run it through the broker with one +absolute log filename. + +The probe uses a new data folder and loopback-only discovery. diff --git a/src/tests/experimental/native/download-stop/UDownloadSafeboxStopChecks.pas b/src/tests/experimental/native/download-stop/UDownloadSafeboxStopChecks.pas new file mode 100644 index 000000000..126471697 --- /dev/null +++ b/src/tests/experimental/native/download-stop/UDownloadSafeboxStopChecks.pas @@ -0,0 +1,348 @@ +unit UDownloadSafeboxStopChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunDownloadSafeboxStopProbe: Integer; + +implementation + +uses + Windows, Classes, SysUtils, SyncObjs, + {$IFDEF FPC}Generics.Collections,{$ELSE}System.Generics.Collections,{$ENDIF} + UMemory, UThread, UNode, UNetProtocol, UChunk, UConst, UAccounts, UCrypto, + UPCDownloadSafebox, UPCDataTypes; + +type + TDownloadStopState = class + private + FLogFile: String; + FLog: TStringList; + FLogLock: SyncObjs.TCriticalSection; + public + Gate: TEvent; + Threads: TList<TPCThread>; + StartedCount, EnteredCount, CompletedCount, DestroyedCount: Integer; + PreparedWritesCount, CompletedWritesCount, WritesAfterCancellationCount: Integer; + StreamsAliveAtExitCount, StreamsAliveBeforeDestroyCount, DeletedStreamCount: Integer; + JoinCount, CompletedJoinsCount, FreedAfterAllJoinsCount, WorkerErrors: Integer; + AllTerminatedBeforeFirstJoin: Boolean; + constructor Create(const ALogFile: String); + destructor Destroy; override; + procedure Log(const AMessage: String); + procedure StopNetwork(Sender: TObject; const AProgress: TProgressInfo); + end; + + TDownloadAccess = class(TPCDownloadSafebox) + public type + TChunkThread = {$IFDEF FPC}TDownloadSBThread{$ELSE}TPCDownloadSafebox.TDownloadSBThread{$ENDIF}; + end; + + TObservedDownload = class(TDownloadAccess) + protected type + TBlockedChunk = class(TDownloadAccess.TChunkThread) + private + FState: TDownloadStopState; + FWriteCompleted, FWorkerCompleted: Boolean; + protected + procedure BCExecute; override; + public + constructor Create(AOwner: TObservedDownload; AState: TDownloadStopState; + ABlockStart, ACount: Cardinal); reintroduce; + destructor Destroy; override; + end; + private + FState: TDownloadStopState; + protected + function CreateChunkThread(ABlockStart, ACount: Cardinal): TDownloadAccess.TChunkThread; override; + procedure WaitForChunkThread(AThread: TThread); override; + public + constructor Create(AState: TDownloadStopState); reintroduce; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +function ReadFileBytes(const AFileName: String): AnsiString; +var LBag: TDisposables; LStream: TFileStream; +begin + LStream := LBag.AddObject(TFileStream.Create(AFileName, fmOpenRead or fmShareDenyNone)) as TFileStream; + SetLength(Result, LStream.Size); + if Length(Result) > 0 then LStream.ReadBuffer(Result[1], Length(Result)); +end; + +procedure WriteFileBytes(const AFileName: String; const ABytes: AnsiString); +var LBag: TDisposables; LStream: TFileStream; +begin + LStream := LBag.AddObject(TFileStream.Create(AFileName, fmCreate)) as TFileStream; + if Length(ABytes) > 0 then LStream.WriteBuffer(ABytes[1], Length(ABytes)); + Require(FlushFileBuffers(LStream.Handle), 'Cannot flush the isolated saved-file sentinel'); +end; + +function DesktopName(AHandle: THandle): String; +var LBuffer: array[0..255] of Char; LRequired: DWORD; +begin + FillChar(LBuffer, SizeOf(LBuffer), 0); + if not GetUserObjectInformation(AHandle, UOI_NAME, @LBuffer[0], SizeOf(LBuffer), LRequired) then + RaiseLastOSError; + Result := StrPas(PChar(@LBuffer[0])); +end; + +procedure RequirePrivateDesktop; +var LInput: THandle; LName: String; +begin + LInput := OpenInputDesktop(0, false, DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + LName := DesktopName(GetThreadDesktop(GetCurrentThreadId)); + Require((Pos('PascalCoin', LName) = 1) and not SameText(LName, DesktopName(LInput)), + 'Run this probe through the existing private-desktop broker'); + finally CloseDesktop(LInput); end; +end; + +constructor TDownloadStopState.Create(const ALogFile: String); +begin + inherited Create; + FLogFile := ALogFile; + FLog := TStringList.Create; + FLogLock := SyncObjs.TCriticalSection.Create; + Gate := TEvent.Create(nil, true, false, ''); + Threads := TList<TPCThread>.Create; +end; + +destructor TDownloadStopState.Destroy; +begin + Threads.Free; + Gate.Free; + FLogLock.Free; + FLog.Free; + inherited; +end; + +procedure TDownloadStopState.Log(const AMessage: String); +begin + FLogLock.Acquire; + try + FLog.Add(AMessage); + FLog.SaveToFile(FLogFile); + finally FLogLock.Release; end; +end; + +procedure TDownloadStopState.StopNetwork(Sender: TObject; const AProgress: TProgressInfo); +var LDeadline: UInt64; +begin + Require(AProgress.Stage = psDownloadingSafeboxChunks, + 'The actual downloader must identify its stage without presentation text'); + Require((AProgress.CurrentPosition = 0) and (AProgress.TotalCount = 2), + 'Both gated chunks must remain pending in the raw progress counters'); + Require(Threads.Count = 2, 'The public downloader must create both real chunk workers'); + LDeadline := GetTickCount64 + 2000; + while (StartedCount < Threads.Count) and (GetTickCount64 < LDeadline) do Sleep(1); + Require(StartedCount = Threads.Count, 'Both inherited workers must reach the test gate'); + Require(PreparedWritesCount = Threads.Count, 'Both actual temporary streams must have an in-flight write'); + Require(CompletedWritesCount = 0, 'The gated writes must still be unfinished when cancellation starts'); + Require(EnteredCount = 0, 'No worker may enter its production retry before the join'); + Require(TNetData.NetData.ConnectionsCountAll = 0, 'This regression requires no peer connection'); + Log('NETWORK OFF: both real chunk workers gated before inherited retry'); + TNetData.NetData.NetConnectionsActive := false; +end; + +constructor TObservedDownload.TBlockedChunk.Create(AOwner: TObservedDownload; + AState: TDownloadStopState; ABlockStart, ACount: Cardinal); +begin + FState := AState; + inherited Create(AOwner, ABlockStart, ACount); +end; + +procedure TObservedDownload.TBlockedChunk.BCExecute; +const + CT_PREFIX: AnsiString = 'IN-FLIGHT CHUNK WRITE'#10; + CT_COMPLETED: AnsiString = 'NORMAL WRITE COMPLETION AFTER CANCELLATION'#10; +var LExpected: AnsiString; +begin + try + // This is a test write to the real owned stream, not a valid protocol chunk. + Stream.WriteBuffer(CT_PREFIX[1], Length(CT_PREFIX)); + InterlockedIncrement(FState.PreparedWritesCount); + InterlockedIncrement(FState.StartedCount); + FState.Gate.WaitFor(INFINITE); + Require(Terminated, 'The join must request cooperative cancellation before releasing the write'); + Stream.WriteBuffer(CT_COMPLETED[1], Length(CT_COMPLETED)); + Require(FlushFileBuffers(Stream.Handle), 'The actual temporary stream must remain open through write completion'); + LExpected := CT_PREFIX + CT_COMPLETED; + Require(ReadFileBytes(Stream.FileName) = LExpected, 'The complete flushed write must be readable before the worker exits'); + FWriteCompleted := true; + InterlockedIncrement(FState.CompletedWritesCount); + InterlockedIncrement(FState.WritesAfterCancellationCount); + FState.Log('WRITE COMPLETE after cooperative cancellation; actual temporary file still open'); + InterlockedIncrement(FState.EnteredCount); + FState.Log('WORKER ENTER inherited retry (which acquires the owner chunks lock)'); + // Keep the real retry and list acquisition; it may clear a cancelled chunk. + inherited; + Require(FileExists(Stream.FileName), 'The worker must retain its temporary file until normal exit'); + Require(FlushFileBuffers(Stream.Handle), 'The worker must retain its live stream handle until normal exit'); + FWorkerCompleted := true; + InterlockedIncrement(FState.StreamsAliveAtExitCount); + InterlockedIncrement(FState.CompletedCount); + FState.Log('WORKER EXIT inherited retry; owned stream still alive'); + except + on E: Exception do begin + InterlockedIncrement(FState.WorkerErrors); + FState.Log('WORKER FAIL ' + E.ClassName + ': ' + E.Message); + end; + end; +end; + +destructor TObservedDownload.TBlockedChunk.Destroy; +var LFileName: String; +begin + LFileName := Stream.FileName; + if FWriteCompleted and FWorkerCompleted and FileExists(LFileName) and + FlushFileBuffers(Stream.Handle) then InterlockedIncrement(FState.StreamsAliveBeforeDestroyCount); + if FState.CompletedJoinsCount = FState.Threads.Count then + InterlockedIncrement(FState.FreedAfterAllJoinsCount); + inherited; + if not FileExists(LFileName) then InterlockedIncrement(FState.DeletedStreamCount); + InterlockedIncrement(FState.DestroyedCount); +end; + +constructor TObservedDownload.Create(AState: TDownloadStopState); +begin + FState := AState; + inherited Create; +end; + +function TObservedDownload.CreateChunkThread(ABlockStart, + ACount: Cardinal): TDownloadAccess.TChunkThread; +begin + Result := TBlockedChunk.Create(Self, FState, ABlockStart, ACount); + FState.Threads.Add(Result); +end; + +procedure TObservedDownload.WaitForChunkThread(AThread: TThread); +var LThread: TPCThread; +begin + Inc(FState.JoinCount); + if FState.JoinCount = 1 then begin + FState.AllTerminatedBeforeFirstJoin := true; + for LThread in FState.Threads do + FState.AllTerminatedBeforeFirstJoin := + FState.AllTerminatedBeforeFirstJoin and LThread.Terminated; + FState.Log('FIRST JOIN: all workers terminated=' + BoolToStr(FState.AllTerminatedBeforeFirstJoin, true)); + // Releasing only at the real join deterministically exposes lock-held joins. + FState.Gate.SetEvent; + end; + inherited; + Inc(FState.CompletedJoinsCount); + FState.Log('JOIN COMPLETE ' + IntToStr(FState.JoinCount)); +end; + +procedure CheckTemporaryStreamsReleased(const ADirectory: String); +var LSearch: TSearchRec; +begin + if FindFirst(IncludeTrailingPathDelimiter(ADirectory) + 'Temp' + PathDelim + '*.tmp', + faAnyFile, LSearch) = 0 then begin + FindClose(LSearch); + raise Exception.Create('A downloader-owned temporary chunk stream was not released'); + end; +end; + +procedure CheckDownloaderStop(AState: TDownloadStopState; const ADirectory: String); +const CT_SAVED_BYTES: AnsiString = 'PREEXISTING SAVED SAFEBOX SENTINEL - MUST REMAIN UNCHANGED'#10; +var LDownload: TObservedDownload; LChunks: TPCSafeboxChunks; + LOperationBlock: TOperationBlock; LStarted, LElapsed: UInt64; LResult: Boolean; + LSavedFile, LUnpublishedFile: String; +begin + LChunks := TPCSafeboxChunks.Create; + try + LOperationBlock := CT_OperationBlock_NUL; + LOperationBlock.block := 2 * CT_MAX_SAFEBOXCHUNK_BLOCKS; + Require(LOperationBlock.block mod CT_BankToDiskEveryNBlocks = 0, + 'The fixture block count must satisfy the real downloader validation'); + LSavedFile := IncludeTrailingPathDelimiter(TNode.Node.Bank.GetStorageFolder('')) + 'safebox_100.safebox'; + LUnpublishedFile := IncludeTrailingPathDelimiter(TNode.Node.Bank.GetStorageFolder('')) + + 'safebox_' + UIntToStr(LOperationBlock.block) + '.safebox'; + Require(not FileExists(LSavedFile) and not FileExists(LUnpublishedFile), 'Both archive paths must be fresh test paths'); + WriteFileBytes(LSavedFile, CT_SAVED_BYTES); + TNode.Node.NetServer.Port := 0; + TNode.Node.NetServer.Active := true; + Require(TNode.Node.NetServer.Active, 'The isolated ephemeral server must start'); + TNode.Node.BeginBlockChainUpdate; + try + LDownload := TObservedDownload.Create(AState); + try + LDownload.OnProgressNotify := AState.StopNetwork; + LResult := LDownload.DownloadSafebox(nil, LOperationBlock, LChunks); + Require(not LResult, 'Network-off must cancel the unfinished download'); + Require(LChunks.Count = 0, 'Cancelled temporary chunks must never be transferred for publication'); + Require(not TNode.Node.NetServer.Active, 'The actual network-off path must stop the server'); + finally + LStarted := GetTickCount64; + LDownload.Free; + LElapsed := GetTickCount64 - LStarted; + AState.Log('DESTRUCTOR RETURN elapsed=' + UIntToStr(LElapsed)); + end; + finally TNode.Node.EndBlockChainUpdate; end; + Require(LElapsed < 3000, 'Joining cancelled no-peer workers must finish promptly'); + Require(not TNode.Node.IsUpdatingBlockChain, 'Downloader shutdown must release its owner core-work scope'); + Require(AState.AllTerminatedBeforeFirstJoin, 'Every worker must be terminated before the first join'); + Require(AState.JoinCount = 2, 'Both actual worker joins must complete'); + Require(AState.EnteredCount = 2, 'Both workers must enter the inherited retry'); + Require(AState.CompletedCount = 2, 'Both inherited production retries must return'); + Require(AState.DestroyedCount = 2, 'Both downloader-owned workers must be destroyed'); + Require(AState.WorkerErrors = 0, 'Both actual worker writes and inherited retries must succeed normally'); + Require(AState.CompletedWritesCount = 2, 'Both in-flight writes must complete before worker cleanup'); + Require(AState.WritesAfterCancellationCount = 2, 'Cancellation must be a cooperative flag, allowing the active writes to finish'); + Require(AState.StreamsAliveAtExitCount = 2, 'Both streams must remain open until normal worker exit'); + Require(AState.StreamsAliveBeforeDestroyCount = 2, 'Both completed workers must still own their streams before destruction'); + Require(AState.FreedAfterAllJoinsCount = 2, 'No worker stream may be freed until every worker has joined'); + Require(AState.DeletedStreamCount = 2, 'Owned partial stream files must be removed during worker destruction'); + Require(TNetData.NetData.ConnectionsCountAll = 0, 'No peer connection may be introduced'); + Require(ReadFileBytes(LSavedFile) = CT_SAVED_BYTES, 'Cancellation must leave the preexisting saved file byte-for-byte unchanged'); + Require(not FileExists(LUnpublishedFile), 'Cancellation must not publish a final safebox archive'); + CheckTemporaryStreamsReleased(ADirectory); + AState.Log('PASS two inherited chunk workers acquired their owner list during shutdown; all joins, streams and core-work scope released'); + AState.Log('PASS active temporary writes completed and flushed before normal worker exit/destruction; partial files removed only after joins'); + AState.Log('PASS existing saved-file sentinel bytes unchanged; no cancelled safebox archive published (not a protocol durability test)'); + finally LChunks.Free; end; +end; + +function RunDownloadSafeboxStopProbe: Integer; +var LBag: TDisposables; LState: TDownloadStopState; + LDirectory: String; LID: TGUID; LEmpty: TPCDownloadSafebox; + LServers: TNodeServerAddressArray; +begin + Result := 1; + if ParamCount <> 1 then Exit; + LState := LBag.AddObject(TDownloadStopState.Create(ExpandFileName(ParamStr(1)))) as TDownloadStopState; + try + RequirePrivateDesktop; + Require(not TNode.NodeExists and not TNetData.NetDataExists, 'The regression requires a dedicated fresh process'); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + 'PascalCoinDownloadStop-' + GUIDToString(LID); + Require(not DirectoryExists(LDirectory), 'The test data directory must be new'); + Require(ForceDirectories(LDirectory), 'Cannot create isolated test data'); + TNode.SetPascalCoinDataFolder(LDirectory); + TCrypto.InitCrypto; + TNode.Node; + // Background discovery must stay on loopback even if this check runs slowly. + TNode.DecodeIpStringToNodeServerAddressArray('127.0.0.1:1', LServers); + TNetData.NetData.DiscoverFixedServersOnly(LServers); + Require(TNetData.NetData.ConnectionsCountAll = 0, 'The fresh node must have no peers'); + LState.Log('START isolated actual downloader shutdown; data=' + LDirectory); + LEmpty := TPCDownloadSafebox.Create; + LEmpty.Free; + LState.Log('PASS empty downloader destruction'); + CheckDownloaderStop(LState, LDirectory); + Result := 0; + except + on E: Exception do LState.Log('FAIL ' + E.ClassName + ': ' + E.Message); + end; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/README.md b/src/tests/experimental/native/grid-refresh/README.md new file mode 100644 index 000000000..e795d7260 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/README.md @@ -0,0 +1,146 @@ +# Wallet grid refresh test + +This mock wallet runs the production main form and grids over generated account +and history records. It checks that: + +- accounts refresh at once (`RefreshThrottle.Mode = temNone`) +- operations fetch only the latest request after 250 ms of quiet + (`temNotifyOnEventBurstFinished`) +- both grids show **Please Wait / Synchronizing** and run no queries during node + synchronization + +Focused cases check wallet tools and dialogs. + +![Native Delphi demo holding both grids during mock synchronization](grid-refresh-waiting.png) + +## Build and run + +Open [WalletGridRefreshDemo.lpi](WalletGridRefreshDemo.lpi) in Lazarus or +[WalletGridRefreshDemo.dproj](WalletGridRefreshDemo.dproj) in Delphi. Lazarus +builds `WalletGridRefreshDemoFPC.exe` and Delphi builds `WalletGridRefreshDemo.exe`, +both into `src` beside the OpenSSL runtime. + +From the repository root, run: + +```powershell +& .\src\tests\experimental\RunWalletGridRefreshTests.ps1 -Demo +``` + +The script builds the demo with Windows x64 FPC and runs it through the +[private-desktop broker](../README.md#private-desktop-broker). Pass `-FpcPath` and `-LazarusDirectory` +to select FPC and Lazarus, and `-OutputDirectory <new-directory>` to choose +where results go. The default run covers the first four +[focused cases](#focused-cases). + +## Interactive controls + +Run an executable without arguments to use these controls. Transaction and +detail popups stay disabled. + +- **Start sync (100 ms)** starts node synchronization with backend events, while + both grids run no queries. +- **Stop** ends it and loads the latest local data. +- **Busy block batch (6.5 s)** shows the same pause during one long block batch. +- **Slow provider (7 seconds)**, **Gate next fetch** and **Release fetch** show + the loading state. +- **Cycle row counts** switches between empty, one-row, two-row and larger + results. +- **Reset** stops synchronization, releases held fetches and restores 200 + records. + +## Focused cases + +Set one of these environment variables before an automatic run. Each runs only +its case group: + +- `PASCAL_WALLET_KEY_NOTIFICATIONS_ONLY=1` checks wallet-key notifications from + a worker thread. +- `PASCAL_WALLET_CONSECUTIVE_SELECTION_ONLY=1` makes 20 alternating account + clicks and a six-click burst. Each of the 20 must show its account's history + within one second. +- `PASCAL_WALLET_SELECTION_COMPLETION_ONLY=1` selects two accounts in a row, each + with 268 generated operations. +- `PASCAL_WALLET_REFRESH_LIFECYCLE_ONLY=1` checks refresh timing, held fetches + and the synchronization pause. +- `PASCAL_WALLET_SELECTION_ONLY=1` checks selection, loading, overview totals and + native banner and event presentation. +- `PASCAL_WALLET_SAFEBOX_LOG_ONLY=1` checks structured safebox progress, Info + logging and node status. +- `PASCAL_WALLET_SYNC_PAINT_ONLY=1` checks synchronization presentation. +- `PASCAL_WALLET_TOOLS_ONLY=1` runs + [UWalletToolsDialogChecks](UWalletToolsDialogChecks.pas). It opens all ten + Tools menu entries and the Change All Key wizard, and checks the Overview + layout at normal and 150% scale. Inspect wizard grid cells in the + `*-native-grid.bmp` captures. It and the combined cases below need the same + files as [Send PASC](#send-pasc-presentation). +- `PASCAL_WALLET_CHANGE_INFO_UI_ONLY=1` checks single-account and batch Change + Account Info at 100% and 150%. It needs the same files as + [Send PASC](#send-pasc-presentation) and saves eight screen BMPs beside its + log. Inspect each compiler's images and require + `PASS native Change Account Info presentation` and exit code zero. +- `PASCAL_WALLET_CHANGE_INFO_APPROVAL_ONLY=1` runs six Change Account Info + wizard cases at 100% and 150%, including skipped-account consent and + single-payer fee selection. It saves each screen beside its log. +- `PASCAL_WALLET_OPERATION_MESSAGE_ONLY=1` runs 13 operation-message dialog + cases at 100% and 150% and saves captures beside the test log. +- `PASCAL_WALLET_ADVANCED_UI_ONLY=1` captures all six Advanced Operation tabs at + 100% and 150%. It needs a 1920x1080 or larger desktop. +- `PASCAL_WALLET_SEND_UI_ONLY=1` runs + [Send PASC presentation](#send-pasc-presentation). + +Combine these with `PASCAL_WALLET_TOOLS_ONLY=1`: + +- `PASCAL_WALLET_OPERATION_INPUT_ONLY=1` checks which input opens operation + details, the wallet footers at 100%, 125% and 150%, and the + [operation-information layout](UWalletOperationInfoLayoutChecks.pas). Build + with `-O2`, as the script does. +- `PASCAL_WALLET_HISTORY_DEPTH_ONLY=1` checks that the Recent Operations picker + passes `FetchAll` a depth of 2,016 for 7 Days, 8,640 for 30 Days and + `High(Cardinal)` for Maximum. +- `PASCAL_WALLET_BLOCK_TOOLS_ONLY=1` checks Block Explorer **Refresh (F5)** and + **Reset block filters**. Its `<main-log-stem>.tools.log` ends with + `PASS actual Block Explorer Tools refresh/reset checks`. + +These checks have their own entry points: + +- [batch preparation checks](../../operations/UChangeInfoBatchChecks.pas), through + [FPCUnit](../../operations/UChangeInfoBatchTests.pas) or, under Delphi, + `RunChangeInfoBatchChecks(LogFile)` +- [stored-history checks](../stored-history/README.md) +- [FPCUnit fixture](../../ui/UWalletGridIntegrationTests.pas) +- [network-off probe](WalletNetworkOffProbe.dproj) +- [received-block backlog checks](../network-backlog/README.md) + +## Send PASC presentation + +Run [UWalletSendPresentationChecks](UWalletSendPresentationChecks.pas) through +the broker with only `PASCAL_WALLET_SEND_UI_ONLY=1` set. Build the target, create +a new output directory, and put +[mainnet-blocks-0-4.bin](../../fixtures/mainnet-blocks-0-4.bin) and the matching +OpenSSL runtime beside the executable. For example: + +```powershell +$previous = $env:PASCAL_WALLET_SEND_UI_ONLY +$env:PASCAL_WALLET_SEND_UI_ONLY = '1' +try { + & $Broker $WalletGridRefreshExe '<new-folder>\tests.log' +} finally { + $env:PASCAL_WALLET_SEND_UI_ONLY = $previous +} +``` + +Six routes cover the default fee, a custom fee, and public, sender-key, +recipient-key and password-encrypted payloads, at 100% and 150% font scale. + +BMPs named by route, step and scale go beside `<main-log-stem>.send-ui.log`. A +pass writes `PASS native Send PASC presentation` and exits with code zero. A +failure exits nonzero. Run both the Delphi (VCL) and Lazarus (LCL) targets and +inspect each one's BMPs and log. + +## Safety + +- Every run uses a new temporary data folder, never your wallet. +- Runs with `PASCAL_WALLET_TOOLS_ONLY=1` go online with settings restricted to + the loopback endpoint `127.0.0.1:1` and never connect to public peers. Other + runs stay offline. +- No case signs, submits or broadcasts an operation. diff --git a/src/tests/experimental/native/grid-refresh/USyncFlickerChecks.pas b/src/tests/experimental/native/grid-refresh/USyncFlickerChecks.pas new file mode 100644 index 000000000..03060dfb0 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/USyncFlickerChecks.pas @@ -0,0 +1,544 @@ +unit USyncFlickerChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Observes actual synchronization captions, Info logs and native paint messages. + The separate erase challenge exposes an intermediate blank frame; it is not + presented as proof that a live event caused that exact screen frame. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UWalletGridMocks; + +procedure CheckSynchronizationPainting(AMain: TMockMainForm; const ALogPath: String); +procedure CheckSafeboxProgressLogging(AMain: TMockMainForm; const ALogPath: String); + +implementation + +uses Windows, Messages, Classes, SysUtils, Types, Forms, Controls, Graphics, + SyncObjs, UNode, UAccounts, UUserInterface, UCTRLSyncronization, + UProgressDisplay, ULog, UThread, UConst, UNetProtocol, UCommon; + +type + TPaintObserver = class + private + FControl: TWinControl; + FOriginal: TWndMethod; + procedure Observe(var AMessage: TMessage); + public + Paints, FullPaints, Erases, ExternalErases, ShowMessages, PositionMessages: Integer; + constructor Create(AControl: TWinControl); + destructor Destroy; override; + function Summary: String; + end; + + TSafeboxLogEntry = record + Kind: TLogType; + ThreadID: {$IFDEF FPC}TThreadID{$ELSE}UConst.TThreadID{$ENDIF}; + Source, Text: String; + end; + + TSafeboxProgressObserver = class(TComponent) + private + FSource: TLog; + FPreviousLog: TNewLogEvent; + FLock: TCriticalSection; + FEntries: array of TSafeboxLogEntry; + FUIEvents: Integer; + FAttached: Boolean; + procedure OnLog(AKind: TLogType; ATime: TDateTime; + AThread: {$IFDEF FPC}TThreadID{$ELSE}UConst.TThreadID{$ENDIF}; const ASource, AText: String); + procedure OnUIChanged(Sender: TObject); + function GetCount: Integer; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + constructor Create; + destructor Destroy; override; + procedure Attach; + procedure Detach; + procedure Check(AExpectedCount: Integer; const AExpectedText: String); + property Count: Integer read GetCount; + property UIEvents: Integer read FUIEvents; + end; + + TSafeboxProgressProducer = class(TPCThread) + private + FNode: TNode; + FProgress: TProgressInfo; + protected + procedure BCExecute; override; + public + Completed: TEvent; + Error: String; + constructor Create(ANode: TNode; const AProgress: TProgressInfo); + destructor Destroy; override; + end; + +var + SafeboxProgressObserver: TSafeboxProgressObserver; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure Log(const APath, AText: String); +var LLines: TStringList; +begin + LLines := TStringList.Create; + try + if FileExists(APath) then LLines.LoadFromFile(APath); + LLines.Add(FormatDateTime('hh:nn:ss.zzz',Now)+' '+AText); + LLines.SaveToFile(APath); + finally LLines.Free; end; +end; + +procedure Pump(AMilliseconds: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64+AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64>=LUntil; +end; + +constructor TSafeboxProgressObserver.Create; +begin + inherited Create(nil); + FLock := TCriticalSection.Create; +end; + +destructor TSafeboxProgressObserver.Destroy; +begin + Detach; + FLock.Free; + inherited; +end; + +procedure TSafeboxProgressObserver.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited; + if (Operation=opRemove) and (AComponent=FSource) then FSource := nil; +end; + +procedure TSafeboxProgressObserver.Attach; +begin + Require(not FAttached,'Safebox log observer is already attached'); + FSource := TUserInterface.Log; + Require(Assigned(FSource),'Actual application log is missing'); + FSource.FreeNotification(Self); + FPreviousLog := FSource.OnInThreadNewLog; + FLock.Acquire; + try + SetLength(FEntries,0); + FUIEvents := 0; + finally + FLock.Release; + end; + FSource.OnInThreadNewLog := OnLog; + TUserInterface.SynchronizationChanged.Add(OnUIChanged); + FAttached := True; +end; + +procedure TSafeboxProgressObserver.Detach; +begin + if not FAttached then Exit; + TUserInterface.SynchronizationChanged.Remove(OnUIChanged); + if Assigned(FSource) then FSource.OnInThreadNewLog := FPreviousLog; + FAttached := False; +end; + +procedure TSafeboxProgressObserver.OnLog(AKind: TLogType; ATime: TDateTime; + AThread: {$IFDEF FPC}TThreadID{$ELSE}UConst.TThreadID{$ENDIF}; const ASource, AText: String); +var I: Integer; +begin + if Assigned(FPreviousLog) then FPreviousLog(AKind,ATime,AThread,ASource,AText); + if Pos('Downloading Safebox chunks ',AText)<>1 then Exit; + FLock.Acquire; + try + I := Length(FEntries); + SetLength(FEntries,I+1); + FEntries[I].Kind := AKind; + FEntries[I].ThreadID := AThread; + FEntries[I].Source := ASource; + FEntries[I].Text := AText; + finally + FLock.Release; + end; +end; + +procedure TSafeboxProgressObserver.OnUIChanged(Sender: TObject); +begin + Inc(FUIEvents); +end; + +function TSafeboxProgressObserver.GetCount: Integer; +begin + FLock.Acquire; + try + Result := Length(FEntries); + finally + FLock.Release; + end; +end; + +procedure TSafeboxProgressObserver.Check(AExpectedCount: Integer; const AExpectedText: String); +var I: Integer; +begin + FLock.Acquire; + try + Require(Length(FEntries)=AExpectedCount,Format( + 'Expected %d safebox Info messages, received %d',[AExpectedCount,Length(FEntries)])); + for I := 0 to High(FEntries) do begin + Require(FEntries[I].Kind=ltinfo,'Chunk-counter progress was not logged at Info'); + Require(FEntries[I].Source='TNetData','Chunk-counter log lost its network progress source'); + Require(FEntries[I].ThreadID=MainThreadID,'Progress formatting/logging ran on its producer thread'); + end; + if AExpectedCount>0 then + Require(FEntries[High(FEntries)].Text=AExpectedText,'Safebox Info text differs from the visible counter'); + finally + FLock.Release; + end; +end; + +constructor TSafeboxProgressProducer.Create(ANode: TNode; const AProgress: TProgressInfo); +begin + FNode := ANode; + FProgress := AProgress; + Completed := TEvent.Create(nil,True,False,''); + inherited Create(True); + FreeOnTerminate := False; +end; + +destructor TSafeboxProgressProducer.Destroy; +begin + WaitFor; + Completed.Free; + inherited; +end; + +procedure TSafeboxProgressProducer.BCExecute; +begin + try + try + FNode.SetSynchronizationProgress(FProgress); + except + on E: Exception do Error := E.ClassName+': '+E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TPaintObserver.Create(AControl: TWinControl); +begin + inherited Create; + FControl := AControl; + FOriginal := FControl.WindowProc; + FControl.WindowProc := Observe; +end; + +destructor TPaintObserver.Destroy; +begin + FControl.WindowProc := FOriginal; + inherited; +end; + +procedure TPaintObserver.Observe(var AMessage: TMessage); +var LUpdate, LClient: TRect; +begin + case AMessage.Msg of + WM_PAINT: begin + Inc(Paints); + LClient := FControl.ClientRect; + if GetUpdateRect(FControl.Handle,LUpdate,False) and + (LUpdate.Left<=LClient.Left) and (LUpdate.Top<=LClient.Top) and + (LUpdate.Right>=LClient.Right) and (LUpdate.Bottom>=LClient.Bottom) then + Inc(FullPaints); + end; + WM_ERASEBKGND: begin + Inc(Erases); + if AMessage.LParam=0 then Inc(ExternalErases); + end; + WM_SHOWWINDOW: Inc(ShowMessages); + WM_WINDOWPOSCHANGED: Inc(PositionMessages); + end; + FOriginal(AMessage); +end; + +function TPaintObserver.Summary: String; +begin + Result := Format('paints=%d full_client_paints=%d erases=%d external_erases=%d show_messages=%d position_messages=%d', + [Paints,FullPaints,Erases,ExternalErases,ShowMessages,PositionMessages]); +end; + +{$IFNDEF FPC} +function PaintSurface(AControl: TWinControl): TBitmap; +begin + Result := TBitmap.Create; + Result.PixelFormat := pf32bit; + Result.SetSize(AControl.ClientWidth,AControl.ClientHeight); + Result.Canvas.Brush.Color := clFuchsia; + Result.Canvas.FillRect(AControl.ClientRect); + // PaintTo includes native graphic children; plain WM_PRINT omits VCL labels. + AControl.PaintTo(Result.Canvas.Handle,0,0); +end; + +function ChangedPixels(AFirst, ASecond: TBitmap): Integer; +type TPixelRow = array[0..$FFFFFF] of Cardinal; PPixelRow = ^TPixelRow; +var X,Y: Integer; LFirst,LSecond: PPixelRow; +begin + Require((AFirst.Width=ASecond.Width) and (AFirst.Height=ASecond.Height), + 'Paint comparison dimensions differ'); + Result := 0; + for Y := 0 to AFirst.Height-1 do begin + LFirst := AFirst.ScanLine[Y]; LSecond := ASecond.ScanLine[Y]; + for X := 0 to AFirst.Width-1 do + if (LFirst[X] and $FFFFFF)<>(LSecond[X] and $FFFFFF) then Inc(Result); + end; +end; + +function CheckEraseBoundary(AControl: TWinControl; const AName, APath: String): Integer; +var LBefore,LExternal,LInternal: TBitmap; LInternalChanges: Integer; +begin + LBefore := PaintSurface(AControl); + LExternal := TBitmap.Create; + LInternal := TBitmap.Create; + try + LExternal.Assign(LBefore); LInternal.Assign(LBefore); + // The native external erase has lParam=0. A buffered internal erase uses + // the same DC in both parameters and may clear only its offscreen buffer. + AControl.Perform(WM_ERASEBKGND,WPARAM(LExternal.Canvas.Handle),0); + AControl.Perform(WM_ERASEBKGND,WPARAM(LInternal.Canvas.Handle),LPARAM(LInternal.Canvas.Handle)); + Result := ChangedPixels(LBefore,LExternal); + LInternalChanges := ChangedPixels(LBefore,LInternal); + LBefore.SaveToFile(ChangeFileExt(APath,'.'+AName+'-painted.bmp')); + LExternal.SaveToFile(ChangeFileExt(APath,'.'+AName+'-external-erase.bmp')); + LInternal.SaveToFile(ChangeFileExt(APath,'.'+AName+'-buffer-erase.bmp')); + Log(APath,Format('ERASE_CHALLENGE %s external_changed_pixels=%d internal_changed_pixels=%d', + [AName,Result,LInternalChanges])); + finally LInternal.Free; LExternal.Free; LBefore.Free; end; +end; +{$ENDIF} + +procedure CheckSynchronizationPainting(AMain: TMockMainForm; const ALogPath: String); +var LSync: TCTRLSyncronization; LControls: array[0..3] of TWinControl; + LParents: array[0..3] of TWinControl; LObservers: array[0..3] of TPaintObserver; + I,J,LEraseFailures: Integer; LProgress: TProgressInfo; LUntil: UInt64; + LExpected: String; LWasUpdating: Boolean; +begin + LSync := AMain.SyncControl; + Require(Assigned(LSync),'Actual main synchronization control is missing'); + TUserInterface.ShowSyncDialog; + Pump(200); + Require(LSync.Visible and AMain.paSyncPanel.Visible and LSync.paSync.Visible, + 'Actual synchronization details did not become visible'); + Require(not LSync.paSplash.Visible,'Synchronization details are hidden behind the splash'); + LControls[0] := AMain.paSyncPanel; LControls[1] := LSync; + LControls[2] := LSync.paSync; LControls[3] := LSync.GroupBox1; + for I := Low(LControls) to High(LControls) do begin + LParents[I] := LControls[I].Parent; + LObservers[I] := TPaintObserver.Create(LControls[I]); + end; + LWasUpdating := False; + try + TUserInterface.Node.BeginBlockChainUpdate; + LWasUpdating := True; + for I := 1 to 16 do begin + LProgress := TProgressInfo.Create(psDownloadingBlockchain,I,16); + LProgress.BlockNumber := I; + LProgress.PeerAddress := '127.0.0.1:4004'; + TUserInterface.Node.SetSynchronizationProgress(LProgress); + TUserInterface.Node.NotifyBlocksChanged; + LExpected := FormatProgressInfo(LProgress); + LUntil := GetTickCount64+2000; + repeat Pump(5) until (LSync.lblNodeStatus.Caption=LExpected) or (GetTickCount64>=LUntil); + Require(LSync.lblNodeStatus.Caption=LExpected,'A real progress event did not publish the latest details'); + Require(LSync.DisplayedSynchronizationStatus.Progress.CurrentPosition=I, + 'The details retained an obsolete node progress record'); + for J := Low(LControls) to High(LControls) do begin + Require(LControls[J].Visible,'A synchronization update hid '+LControls[J].Name); + Require(LControls[J].Parent=LParents[J],'A synchronization update reparented '+LControls[J].Name); + // Flush only pending native invalidations; this does not invalidate + // the view or replace its normal rendering and event handlers. + UpdateWindow(LControls[J].Handle); + end; + Pump(15); + end; + for I := Low(LControls) to High(LControls) do + Log(ALogPath,'LIVE_EVENTS '+LControls[I].Name+' '+LObservers[I].Summary); + Log(ALogPath,'PASS sixteen actual node progress/block events retained current visible details and stable parents'); + LEraseFailures := 0; + {$IFNDEF FPC} + // Exercise the actual details surfaces. The outer host is observed above; + // it owns a child window covering its client and is not an erase target. + // Report every layer before failing so diagnosis does not stop at the form. + for I := 1 to High(LControls) do + if CheckEraseBoundary(LControls[I],LControls[I].Name,ALogPath)>0 then Inc(LEraseFailures); + Require(LEraseFailures=0,'Standalone native background erase exposes a blank intermediate details frame on '+IntToStr(LEraseFailures)+' surfaces'); + Log(ALogPath,'PASS external native erases preserve the complete details frame; internal erase diagnostics retained'); + {$ELSE} + Log(ALogPath,'PASS Lazarus actual-event visibility/caption checks; VCL erase-boundary challenge is Delphi-only'); + {$ENDIF} + finally + if LWasUpdating then TUserInterface.Node.EndBlockChainUpdate; + Pump(100); + for I := High(LObservers) downto Low(LObservers) do LObservers[I].Free; + end; +end; + +procedure CheckSafeboxProgressLogging(AMain: TMockMainForm; const ALogPath: String); +var LProgress, LSaving: TProgressInfo; LNode: TNode; LSync: TCTRLSyncronization; + LProducer: TSafeboxProgressProducer; LPeer: TObject; + LScope: Boolean; LBefore, I: Integer; LUntil: UInt64; LLastText: String; + + procedure AwaitUI(ABefore: Integer; const AExpectedText: String); + var LDeadline: UInt64; + begin + LDeadline := GetTickCount64+3000; + repeat + Pump(5); + until ((SafeboxProgressObserver.UIEvents>ABefore) and + (LSync.lblNodeStatus.Caption=AExpectedText)) or (GetTickCount64>=LDeadline); + Require(SafeboxProgressObserver.UIEvents>ABefore,'Actual typed node event did not reach the UI'); + Require(LSync.lblNodeStatus.Caption=AExpectedText,'Actual synchronization label did not show current progress'); + end; + + procedure Publish(const AProgress: TProgressInfo; AExpectedCount: Integer; + const AExpectedText: String; const ALastLogText: String); + var LBefore: Integer; + begin + LBefore := SafeboxProgressObserver.UIEvents; + LNode.SetSynchronizationProgress(AProgress); + AwaitUI(LBefore,AExpectedText); + SafeboxProgressObserver.Check(AExpectedCount,ALastLogText); + end; + + function ChunkText(ACurrent, ATotal, APercentage: Integer): String; + begin + Result := Format('Downloading Safebox chunks %d/%d ',[ACurrent,ATotal])+ + FormatFloat('0.00',APercentage)+'%'; + end; + +begin + LNode := TUserInterface.Node; + LSync := AMain.SyncControl; + Require(Assigned(LNode) and Assigned(LSync),'Actual node/synchronization control is missing'); + TUserInterface.ShowSyncDialog; + Pump(100); + Require(LSync.Visible and LSync.paSync.Visible,'Actual synchronization details are not visible'); + if not Assigned(SafeboxProgressObserver) then + SafeboxProgressObserver := TSafeboxProgressObserver.Create; + SafeboxProgressObserver.Attach; + LScope := False; + LProducer := nil; + LPeer := TObject.Create; + try + LNode.BeginBlockChainUpdate; + LScope := True; + LProgress := TProgressInfo.Create(psDownloadingSafeboxChunks,0,4); + LProgress.ChunksCount := 4; + LProgress.BlockCount := 120000; + LLastText := ChunkText(0,4,0); + Publish(LProgress,1,LLastText,LLastText); + for I := 1 to 3 do Publish(LProgress,1,LLastText,LLastText); + Log(ALogPath,'PASS initial counter logged once at Info; repeated actual UI deliveries add no logs'); + + LProgress.CurrentPosition := 1; + LLastText := ChunkText(1,4,25); + Publish(LProgress,2,LLastText,LLastText); + // Only cached synchronization facts change. The actual fresh wallet stays + // offline; its networking owner must never acquire a real peer here. + Require(not TNetData.NetData.NetConnectionsActive,'Fixture transport must remain offline'); + LBefore := SafeboxProgressObserver.UIEvents; + LNode.SetSynchronizationNetworkActive(True); + AwaitUI(LBefore,LLastText); + SafeboxProgressObserver.Check(2,LLastText); + LBefore := SafeboxProgressObserver.UIEvents; + LNode.SetSynchronizationPeerConnected(LPeer,True); + AwaitUI(LBefore,LLastText); + Require(LNode.GetSynchronizationStatus.PeerCount=1,'Unrelated peer event did not change cached peer count'); + SafeboxProgressObserver.Check(2,LLastText); + LBefore := SafeboxProgressObserver.UIEvents; + LNode.SetSynchronizationPeerConnected(LPeer,False); + AwaitUI(LBefore,LLastText); + Require(LNode.GetSynchronizationStatus.PeerCount=0,'Unrelated peer removal did not change cached peer count'); + SafeboxProgressObserver.Check(2,LLastText); + LBefore := SafeboxProgressObserver.UIEvents; + LNode.SetSynchronizationNetworkActive(False); + AwaitUI(LBefore,LLastText); + SafeboxProgressObserver.Check(2,LLastText); + Require(not TNetData.NetData.NetConnectionsActive,'Cached status changes activated real networking'); + Log(ALogPath,'PASS increment logs the displayed counter; unrelated peer changes do not repeat it'); + + LProgress.TotalCount := 8; + LProgress.ChunksCount := 8; + LLastText := 'Downloading Safebox chunks 1/8 '+FormatFloat('0.00',12.5)+'%'; + Publish(LProgress,3,LLastText,LLastText); + LSaving := TProgressInfo.Create(psSavingSafebox); + LSaving.ChunksCount := 8; + LSaving.FileName := 'checkpoint.safebox'; + Publish(LSaving,3,'Saving Safebox 8 chunks to checkpoint.safebox ',LLastText); + Publish(LProgress,4,LLastText,LLastText); + Log(ALogPath,'PASS changed total logs current percentage; another phase emits no chunk log and resets deduplication'); + + LProgress.CurrentPosition := 2; + LLastText := ChunkText(2,8,25); + LBefore := SafeboxProgressObserver.UIEvents; + LProducer := TSafeboxProgressProducer.Create(LNode,LProgress); + LProducer.Start; + // Do not pump UI work until the real producer returns. Logging must happen + // on the existing UI dispatch, never hold the producer behind a log sink. + Require(LProducer.Completed.WaitFor(2000)=wrSignaled, + 'Progress producer waited for the non-pumping UI thread'); + Require(LProducer.Error='',LProducer.Error); + Require(SafeboxProgressObserver.UIEvents=LBefore,'UI delivery occurred before its dispatcher was pumped'); + SafeboxProgressObserver.Check(4,'Downloading Safebox chunks 1/8 '+FormatFloat('0.00',12.5)+'%'); + AwaitUI(LBefore,LLastText); + SafeboxProgressObserver.Check(5,LLastText); + FreeAndNil(LProducer); + Log(ALogPath,'PASS producer returns before UI delivery; Info formatting/logging executes on main thread'); + + LBefore := SafeboxProgressObserver.UIEvents; + LNode.EndBlockChainUpdate; + LScope := False; + LUntil := GetTickCount64+3000; + repeat Pump(5) until ((LNode.GetSynchronizationStatus.State<>nssSynchronizing) and + (SafeboxProgressObserver.UIEvents>LBefore)) or (GetTickCount64>=LUntil); + Require(LNode.GetSynchronizationStatus.State<>nssSynchronizing,'Completed physical session did not leave synchronization'); + Require(SafeboxProgressObserver.UIEvents>LBefore,'Completed session did not notify the actual UI'); + SafeboxProgressObserver.Check(5,LLastText); + LNode.BeginBlockChainUpdate; + LScope := True; + Publish(LProgress,6,LLastText,LLastText); + Publish(LProgress,6,LLastText,LLastText); + Log(ALogPath,'PASS a new physical session logs the same counter again, then suppresses its duplicates'); + Log(ALogPath,'PASS SAFEBOX INFO: six Info records match real labels and network source; duplicate, phase, session and nonblocking checks passed'); + finally + // The observer stays alive until unit finalization: restoring a borrowed + // callback alone does not prove no background logger already captured it. + if Assigned(LProducer) then begin + LUntil := GetTickCount64+3000; + repeat Pump(5) until (LProducer.Completed.WaitFor(0)=wrSignaled) or (GetTickCount64>=LUntil); + LProducer.Free; + end; + LNode.SetSynchronizationPeerConnected(LPeer,False); + LNode.SetSynchronizationNetworkActive(False); + if LScope then LNode.EndBlockChainUpdate; + Pump(100); + SafeboxProgressObserver.Detach; + LPeer.Free; + end; +end; + +initialization + SafeboxProgressObserver := nil; + +finalization + SafeboxProgressObserver.Free; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletAdvancedPresentationChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletAdvancedPresentationChecks.pas new file mode 100644 index 000000000..d8de3dd96 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletAdvancedPresentationChecks.pas @@ -0,0 +1,372 @@ +unit UWalletAdvancedPresentationChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Presentation checks use the actual Advanced Operation controls without a node + or wallet. They never invoke Execute or construct a financial operation. + The fixed-size dialog at 150% needs a Full HD test viewport; a 1024x720 + hosted desktop cannot contain its 1140x975 client area and native frame. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +procedure CheckAdvancedOperationPresentation(const ALogPath: String); + +implementation + +uses + Windows, Messages, Classes, SysUtils, Types, Math, Forms, Controls, StdCtrls, + ExtCtrls, ComCtrls, Graphics, {$IFDEF FPC}LMessages,{$ENDIF} + UMemory, UNode, UFRMOperation; + +type + TControlFontAccess = class(TControl) + public + property Font; + end; + + TGroupContentAccess = class(TGroupBox); + + TAdvancedPresentationChecks = class(TComponent) + private + FLog: TStringList; + FLogPath, FError: String; + FPreviousException: TExceptionEvent; + procedure Require(ACondition: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure OnException(Sender: TObject; E: Exception); + procedure Pump; + procedure CheckControls(AParent: TWinControl; ACanvas: TCanvas; + const AContext: String; ACheckOverlap: Boolean); + procedure Capture(AForm: TForm; const AName: String); + procedure RunScale(ANumerator, ADenominator: Integer); + public + constructor Create(const ALogPath: String); reintroduce; + destructor Destroy; override; + procedure Run; + end; + +constructor TAdvancedPresentationChecks.Create(const ALogPath: String); +begin + inherited Create(nil); + FLogPath := ALogPath; + FLog := TStringList.Create; + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TAdvancedPresentationChecks.Destroy; +begin + Application.OnException := FPreviousException; + FLog.Free; + inherited Destroy; +end; + +procedure TAdvancedPresentationChecks.Require(ACondition: Boolean; + const AMessage: String); +begin + if not ACondition then + raise Exception.Create(AMessage); +end; + +procedure TAdvancedPresentationChecks.Log(const AMessage: String); +begin + FLog.Add(AMessage); + FLog.SaveToFile(FLogPath); +end; + +procedure TAdvancedPresentationChecks.OnException(Sender: TObject; E: Exception); +begin + if FError = '' then + FError := E.ClassName + ': ' + E.Message; +end; + +procedure TAdvancedPresentationChecks.Pump; +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + 50; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LUntil; + Require(FError = '', FError); +end; + +procedure TAdvancedPresentationChecks.CheckControls(AParent: TWinControl; + ACanvas: TCanvas; const AContext: String; ACheckOverlap: Boolean); +var + LContent, LOverlap, LText: TRect; + A, B: TControl; + I, J, LNeeded: Integer; + LCaption: String; +begin + LContent := AParent.ClientRect; + if AParent is TGroupBox then + TGroupContentAccess(AParent).AdjustClientRect(LContent); + for I := 0 to AParent.ControlCount - 1 do + begin + A := AParent.Controls[I]; + if not A.Visible then + Continue; + if (A is TLabel) and (TLabel(A).Caption = '') then + Continue; + Require((A.Left >= LContent.Left) and (A.Top >= LContent.Top) and + (A.Left + A.Width <= LContent.Right + 1) and + (A.Top + A.Height <= LContent.Bottom + 1), + Format('%s: %s exceeds native content (%d,%d,%d,%d in %d,%d,%d,%d)', + [AContext, A.Name, A.Left, A.Top, A.Width, A.Height, + LContent.Left, LContent.Top, LContent.Right, LContent.Bottom])); + ACanvas.Font.Assign(TControlFontAccess(A).Font); + if A is TLabel then + begin + LCaption := StringReplace(TLabel(A).Caption, '&', '', [rfReplaceAll]); + LText := Rect(0, 0, A.Width, 0); + if TLabel(A).WordWrap then + DrawText(ACanvas.Handle, PChar(LCaption), Length(LCaption), LText, + DT_CALCRECT or DT_NOPREFIX or DT_WORDBREAK) + else + DrawText(ACanvas.Handle, PChar(LCaption), Length(LCaption), LText, + DT_CALCRECT or DT_NOPREFIX); + Require((LText.Right - LText.Left <= A.Width + 1) and + (LText.Bottom - LText.Top <= A.Height + 1), + AContext + ': label text is clipped in ' + A.Name); + end; + if A is TRadioButton then + begin + LCaption := StringReplace(TRadioButton(A).Caption, '&', '', [rfReplaceAll]); + LNeeded := ACanvas.TextWidth(LCaption) + GetSystemMetrics(SM_CXMENUCHECK) + 8; + Require(A.Width >= LNeeded, + Format('%s: %s truncates its radio caption (%d < %d)', + [AContext, A.Name, A.Width, LNeeded])); + Require(A.Height >= ACanvas.TextHeight('Wg'), AContext + ': radio font is clipped'); + end; + if A is TCustomEdit then + Require(TWinControl(A).ClientHeight >= ACanvas.TextHeight('Wg'), + AContext + ': edit font is clipped in ' + A.Name); + if ACheckOverlap then + for J := I + 1 to AParent.ControlCount - 1 do + begin + B := AParent.Controls[J]; + if not B.Visible or ((B is TLabel) and (TLabel(B).Caption = '')) then + Continue; + if IntersectRect(LOverlap, A.BoundsRect, B.BoundsRect) then + Require((LOverlap.Right - LOverlap.Left <= 1) or + (LOverlap.Bottom - LOverlap.Top <= 1), + AContext + ': overlapping controls ' + A.Name + ' and ' + B.Name); + end; + end; +end; + +procedure TAdvancedPresentationChecks.Capture(AForm: TForm; const AName: String); +var + LBitmap: Graphics.TBitmap; + X, Y, LInk: Integer; + LBackground: TColor; +{$IFDEF FPC} + LCanvasDC: HDC; + + procedure PaintControl(AControl: TWinControl); + var + LSaved, I: Integer; + LOrigin, LWindowOrigin: TPoint; + LWindow: TRect; + begin + if not AControl.HandleAllocated or not IsWindowVisible(AControl.Handle) then + Exit; + LSaved := SaveDC(LCanvasDC); + try + GetWindowRect(AControl.Handle, LWindow); + LOrigin := AForm.ScreenToClient(LWindow.TopLeft); + LWindowOrigin := LOrigin; + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + if AControl is TPageControl then + begin + // LCL's logical client excludes the tab strip. Paint the real Win32 + // tab control in its full window coordinates before its page children. + IntersectClipRect(LCanvasDC, 0, 0, LWindow.Right - LWindow.Left, + LWindow.Bottom - LWindow.Top); + SendMessage(AControl.Handle, WM_PRINT, WPARAM(LCanvasDC), + PRF_CLIENT or PRF_NONCLIENT or PRF_ERASEBKGND); + end + else + begin + SendMessage(AControl.Handle, WM_PRINT, WPARAM(LCanvasDC), PRF_NONCLIENT); + LOrigin := AForm.ScreenToClient(AControl.ClientToScreen(Types.Point(0, 0))); + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + if AControl is TCustomGroupBox then + IntersectClipRect(LCanvasDC, LWindowOrigin.X - LOrigin.X, + LWindowOrigin.Y - LOrigin.Y, LWindowOrigin.X - LOrigin.X + + LWindow.Right - LWindow.Left, LWindowOrigin.Y - LOrigin.Y + + LWindow.Bottom - LWindow.Top) + else + IntersectClipRect(LCanvasDC, 0, 0, AControl.ClientWidth, AControl.ClientHeight); + AControl.Perform(LM_ERASEBKGND, WPARAM(LCanvasDC), 0); + AControl.Perform(LM_PAINT, WPARAM(LCanvasDC), 0); + if not (AControl is TCustomControl) and not (AControl is TCustomForm) and + not (AControl is TCustomGroupBox) then + SendMessage(AControl.Handle, WM_PRINTCLIENT, WPARAM(LCanvasDC), PRF_CLIENT); + end; + for I := 0 to AControl.ControlCount - 1 do + if AControl.Controls[I] is TWinControl then + PaintControl(TWinControl(AControl.Controls[I])); + finally + RestoreDC(LCanvasDC, LSaved); + end; + end; +{$ENDIF} +begin + AForm.Repaint; + UpdateWindow(AForm.Handle); +{$IFDEF FPC} + LBitmap := Graphics.TBitmap.Create; +{$ELSE} + LBitmap := AForm.GetFormImage; +{$ENDIF} + try +{$IFDEF FPC} + LBitmap.PixelFormat := pf24bit; + LBitmap.SetSize(AForm.ClientWidth, AForm.ClientHeight); + LBitmap.Canvas.Lock; + try + // Initialize the bitmap before asking the real LCL controls to paint. + LBitmap.Canvas.Brush.Color := AForm.GetRGBColorResolvingParent; + LBitmap.Canvas.FillRect(AForm.ClientRect); + LCanvasDC := LBitmap.Canvas.Handle; + PaintControl(AForm); + finally + LBitmap.Canvas.Unlock; + end; +{$ENDIF} + LBitmap.SaveToFile(ChangeFileExt(FLogPath, '') + '.' + AName + '.bmp'); + LBackground := ColorToRGB(LBitmap.Canvas.Pixels[0, 0]); + LInk := 0; + for Y := 0 to (LBitmap.Height - 1) div 4 do + for X := 0 to (LBitmap.Width - 1) div 4 do + if ColorToRGB(LBitmap.Canvas.Pixels[X * 4, Y * 4]) <> LBackground then + Inc(LInk); + Require(LInk > 100, AName + ': native capture is blank'); + finally + LBitmap.Free; + end; +end; + +procedure TAdvancedPresentationChecks.RunScale(ANumerator, ADenominator: Integer); +const + CPageNames: array[0..5] of String = ('tsTransaction', 'tsChangePrivateKey', + 'tsListAccount', 'tsDelistAccount', 'tsBuyAccount', 'tsChangeInfo'); +var + LBag: TDisposables; + LForm: TFRMOperation; + LCanvas: TControlCanvas; + LWork, LWindow: TRect; + I: Integer; + LContext: String; +begin + Require(not TNode.NodeExists, 'Advanced UI fixture must not initialize a node'); + LForm := LBag.AddObject(TFRMOperation.Create(nil, nil)) as TFRMOperation; + LCanvas := LBag.AddObject(TControlCanvas.Create) as TControlCanvas; + LCanvas.Control := LForm; + Require(LForm.PageControlOpType.PageCount = Length(CPageNames), + 'Advanced Operation must retain all six tabs'); + Require(Assigned(LForm.tsDelistAccount) and + (LForm.FindComponent('tsDelistAccount') = LForm.tsDelistAccount), + 'Delist resource is not bound to its published operation field'); + // Cancel receives focus before display; no account validation or execution + // occurs when this presentation-only form is closed without a node. + LForm.ActiveControl := LForm.bbCancel; + // Match UpdateAccountsInfo's empty-sender presentation without reading a bank. + LForm.memoAccounts.Visible := False; + LForm.lblAccountsCount.Visible := False; + LForm.ebSenderAccount.Text := ''; + LForm.lblAccountBalance.Caption := '0.0000'; + if ANumerator <> ADenominator then + LForm.ScaleBy(ANumerator, ADenominator); + LForm.Position := poDesigned; + LWork := Screen.PrimaryMonitor.WorkareaRect; + LForm.Left := LWork.Left + 12; + LForm.Top := LWork.Top + 12; + LForm.Show; + Pump; + GetWindowRect(LForm.Handle, LWindow); + Log(Format('GEOMETRY scale=%d ppi=%d client=%dx%d native=%d,%d,%d,%d workarea=%d,%d,%d,%d', + [ANumerator * 100 div ADenominator, LForm.PixelsPerInch, + LForm.ClientWidth, LForm.ClientHeight, LWindow.Left, LWindow.Top, + LWindow.Right, LWindow.Bottom, LWork.Left, LWork.Top, LWork.Right, + LWork.Bottom])); + Require((LWindow.Left >= LWork.Left) and (LWindow.Top >= LWork.Top) and + (LWindow.Right <= LWork.Right) and (LWindow.Bottom <= LWork.Bottom), + 'Advanced Operation window exceeds the native work area'); + for I := 0 to High(CPageNames) do + begin + Require(LForm.PageControlOpType.Pages[I].Name = CPageNames[I], + 'Advanced operation tab/resource identity changed'); + LForm.PageControlOpType.ActivePageIndex := I; + Pump; + // The explicit nil-node constructor intentionally has no eligible sender. + // Reveal the operation page solely to inspect its real native controls. + LForm.tsOperation.Visible := True; + LForm.PageControlLocked.ActivePage := LForm.tsOperation; + LForm.rbEncryptedWithOldEC.Caption := 'Encrypted with sender public key'; + LForm.rbEncryptedWithEC.Caption := 'Encrypted with destination public key'; + LForm.lblPayloadLength.Caption := '(1024b -> 1040b)'; + LForm.lblEncryptionErrors.Caption := 'Payload status'; + // Production payload changes finish by laying out the refreshed captions. + // The empty sender list keeps this real handler independent of node data. + LForm.memoPayloadClick(nil); + LCanvas.Font.Assign(LForm.rbEncryptedWithEC.Font); + Log(Format('RADIO after layout width=%d text=%d glyph=%d font=%d formfont=%d', + [LForm.rbEncryptedWithEC.Width, + LCanvas.TextWidth(LForm.rbEncryptedWithEC.Caption), + GetSystemMetrics(SM_CXMENUCHECK), LForm.rbEncryptedWithEC.Font.Height, + LForm.Font.Height])); + LForm.Repaint; + Pump; + Log(Format('RADIO after paint width=%d%s', [LForm.rbEncryptedWithEC.Width, + {$IFDEF FPC}' autosize=' + BoolToStr(LForm.rbEncryptedWithEC.AutoSize, True){$ELSE}''{$ENDIF}])); + LContext := Format('advanced-%s-%d', [CPageNames[I], ANumerator * 100 div ADenominator]); + Require(LForm.PageControlOpType.Showing and LForm.gbPayload.Showing, + LContext + ': operation page is not visible'); + CheckControls(LForm, LCanvas, LContext, True); + CheckControls(LForm.tsOperation, LCanvas, LContext, True); + CheckControls(LForm.PageControlOpType.ActivePage, LCanvas, LContext, True); + CheckControls(LForm.gbPayload, LCanvas, LContext, True); + Require((LForm.bbCancel.Top + LForm.bbCancel.Height <= LForm.ClientHeight) and + (LForm.bbExecute.Top + LForm.bbExecute.Height <= LForm.ClientHeight) and + (LForm.bbCancel.Left >= LForm.bbExecute.Left + LForm.bbExecute.Width), + LContext + ': footer buttons are clipped or overlap'); + Capture(LForm, LContext); + Log('PASS ' + LContext + ' native caption/content/font/bounds'); + end; + LForm.Close; + Pump; + Require(not LForm.Visible, 'Advanced Operation remained open after Close'); + Require(not TNode.NodeExists, 'Advanced presentation unexpectedly created a node'); +end; + +procedure TAdvancedPresentationChecks.Run; +begin + try + RunScale(1, 1); + RunScale(3, 2); + Log('PASS Advanced Operation all six tabs at 100 and 150 percent; no node or execution'); + except + on E: Exception do + begin + Log('FAIL ' + E.ClassName + ': ' + E.Message); + raise; + end; + end; +end; + +procedure CheckAdvancedOperationPresentation(const ALogPath: String); +var + LBag: TDisposables; + LChecks: TAdvancedPresentationChecks; +begin + LChecks := LBag.AddObject(TAdvancedPresentationChecks.Create(ALogPath)) as TAdvancedPresentationChecks; + LChecks.Run; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletConsecutiveSelectionChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletConsecutiveSelectionChecks.pas new file mode 100644 index 000000000..51a6ca214 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletConsecutiveSelectionChecks.pas @@ -0,0 +1,356 @@ +unit UWalletConsecutiveSelectionChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. + Distributed under the MIT software license; see the accompanying LICENSE. } + +{ Native regression for account-selection refreshes through the existing grid + throttle. Independent selections publish within a short bound; a scroll burst + fetches only its final account after 250 ms of silence. Records alone are + synthetic; the wallet handler, + typed providers, cache, worker and native controls remain wired. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UWalletGridMocks; + +procedure CheckConsecutiveNativeSelections(AMain: TMockMainForm; const ALogPath: String); + +implementation + +uses Windows, Messages, Classes, SysUtils, Types, Variants, Generics.Collections, + Forms, Controls, ExtCtrls, + UCommon.UI, UVisualGrid; + +type + TConsecutiveSelectionObserver = class + private + FMain: TMockMainForm; + FLog: TStringList; + FPreviousFinished: TNotifyEvent; + FIndex, FExpectedAccount, FBaselineFetches: Integer; + FInputTick, FPublicationTick, FCardHideTick, FAdmissionTick, FProviderEndTick: UInt64; + FPublications, FAllPublications, FIncorrectPublications: Integer; + FWaitingBefore: Boolean; + FCardReopened: Boolean; + FSpacing: String; + function CurrentRowsMatch: Boolean; + procedure FinishedUpdating(Sender: TObject); + procedure CheckCard; + procedure Pump(AMilliseconds: Cardinal); + procedure NativeClick(ARow: Integer); + procedure CheckRapidBurst; + public + constructor Create(AMain: TMockMainForm); + destructor Destroy; override; + procedure Run(const ALogPath: String); + end; + +procedure Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then + raise Exception.Create(AMessage); +end; + +constructor TConsecutiveSelectionObserver.Create(AMain: TMockMainForm); +begin + inherited Create; + FMain := AMain; + FLog := TStringList.Create; + FPreviousFinished := FMain.MockWallet.OperationsGrid.OnFinishedUpdating; + FMain.MockWallet.OperationsGrid.OnFinishedUpdating := FinishedUpdating; +end; + +destructor TConsecutiveSelectionObserver.Destroy; +begin + FMain.MockWallet.OperationsGrid.OnFinishedUpdating := FPreviousFinished; + FLog.Free; + inherited; +end; + +function TConsecutiveSelectionObserver.CurrentRowsMatch: Boolean; +var + I: Integer; + LGrid: TVisualGrid; +begin + Result := False; + LGrid := FMain.MockWallet.OperationsGrid; + if (LGrid.RowCount = 0) or + (Length(FMain.MockWallet.OperationsSource.Accounts) <> 1) or + (FMain.MockWallet.OperationsSource.Accounts[0] <> Cardinal(FExpectedAccount)) then + Exit; + for I := 0 to LGrid.RowCount - 1 do + if Cardinal(LGrid.Rows[I].AccountNumber) <> Cardinal(FExpectedAccount) then + Exit; + Result := True; +end; + +procedure TConsecutiveSelectionObserver.CheckCard; +var + LPanel: TPanel; + LCircle: TLoadingCircle; +begin + if (FIndex = 0) or (FPublicationTick = 0) then + Exit; + LPanel := TPanel(FMain.MockWallet.OperationsGrid.FindComponent('LoadingOverlay')); + LCircle := TLoadingCircle(FMain.MockWallet.OperationsGrid.FindComponent('LoadingCircle')); + Require(Assigned(LPanel) and Assigned(LCircle), 'Real loading components are absent'); + if not LPanel.Visible and not LCircle.Active then + begin + if FCardHideTick = 0 then + FCardHideTick := GetTickCount64; + end + else if FCardHideTick <> 0 then + FCardReopened := True; +end; + +procedure TConsecutiveSelectionObserver.FinishedUpdating(Sender: TObject); +var + LMetrics: TMockFetchMetrics; + LFirstAccount: String; +begin + if Assigned(FPreviousFinished) then + FPreviousFinished(Sender); + if FIndex = 0 then + Exit; + LMetrics := FMain.Model.Metrics(mdkOperations); + Inc(FAllPublications); + LFirstAccount := '(empty)'; + if FMain.MockWallet.OperationsGrid.RowCount > 0 then + LFirstAccount := VarToStr(FMain.MockWallet.OperationsGrid.Rows[0].AccountNumber); + FLog.Add(Format('PUBLICATION click=%d elapsed_ms=%d rows=%d first_account=%s source_matches=%s fetches=%d waiting=%s', + [FIndex, GetTickCount64 - FInputTick, FMain.MockWallet.OperationsGrid.RowCount, + LFirstAccount, + BoolToStr(CurrentRowsMatch, True), LMetrics.FetchCount, + BoolToStr(FMain.MockWallet.OperationsGrid.RefreshThrottle.Waiting, True)])); + if CurrentRowsMatch and (LMetrics.FetchCount > FBaselineFetches) and + (LMetrics.LastStartedTick >= FInputTick) then + begin + Inc(FPublications); + if FPublicationTick = 0 then + begin + FPublicationTick := GetTickCount64; + FAdmissionTick := LMetrics.LastStartedTick; + FProviderEndTick := LMetrics.LastCompletedTick; + end; + CheckCard; + end + else + begin + Inc(FIncorrectPublications); + FLog.Add(Format('CALLBACK_VIOLATION click=%d stale_or_wrong_identity=True', [FIndex])); + end; +end; + +procedure TConsecutiveSelectionObserver.Pump(AMilliseconds: Cardinal); +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + CheckCard; + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TConsecutiveSelectionObserver.NativeClick(ARow: Integer); +var + LCell: TRect; + LPoint: LPARAM; + LSelected: TArray<Variant>; +begin + LCell := FMain.MockWallet.AccountsGrid.InternalDrawGrid.CellRect(0, ARow + 1); + LPoint := LPARAM(((LCell.Top + LCell.Bottom) div 2) shl 16) or + LPARAM((LCell.Left + LCell.Right) div 2); + SendMessage(FMain.MockWallet.AccountsGrid.InternalDrawGrid.Handle, WM_MOUSEMOVE, 0, LPoint); + SendMessage(FMain.MockWallet.AccountsGrid.InternalDrawGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, LPoint); + SendMessage(FMain.MockWallet.AccountsGrid.InternalDrawGrid.Handle, WM_LBUTTONUP, 0, LPoint); + LSelected := FMain.MockWallet.AccountsGrid.SelectedRows; + Require((Length(LSelected) = 1) and (Cardinal(LSelected[0].__KEY) = Cardinal(ARow)), + 'Actual native click did not select precisely its intended account'); +end; + +procedure TConsecutiveSelectionObserver.CheckRapidBurst; +var I: Integer; LUntil: UInt64; LPanel: TPanel; LMetrics: TMockFetchMetrics; +begin + FIndex := 21; + FPublications := 0; + FAllPublications := 0; + FIncorrectPublications := 0; + FPublicationTick := 0; + FCardHideTick := 0; + FAdmissionTick := 0; + FCardReopened := False; + FBaselineFetches := FMain.Model.Metrics(mdkOperations).FetchCount; + LPanel := TPanel(FMain.MockWallet.OperationsGrid.FindComponent('LoadingOverlay')); + Require(Assigned(LPanel),'Real operations loading panel is absent'); + Require(FMain.MockWallet.OperationsGrid.RefreshThrottle.Mode=temNotifyOnEventBurstFinished, + 'Actual wallet operations grid is not using its existing burst-finished throttle'); + for I := 0 to 5 do begin + FExpectedAccount := I mod 2; + FInputTick := GetTickCount64; + NativeClick(FExpectedAccount); + Pump(80); + Require(FMain.Model.Metrics(mdkOperations).FetchCount=FBaselineFetches, + 'Operations provider was queried before account selection became quiet'); + Require(FPublicationTick=0,'An intermediate account was published during scrolling'); + end; + while GetTickCount64-FInputTick<230 do begin + Pump(2); + Require(FMain.Model.Metrics(mdkOperations).FetchCount=FBaselineFetches, + 'Selection throttle did not reset its quiet interval on every input'); + end; + LUntil := GetTickCount64+1500; + while ((FPublicationTick=0) or (FCardHideTick=0)) and (GetTickCount64<LUntil) do Pump(2); + Require((FPublicationTick<>0) and CurrentRowsMatch,'Final selected account was not published'); + LMetrics := FMain.Model.Metrics(mdkOperations); + Require((LMetrics.FetchCount=FBaselineFetches+1) and (FPublications=1) and + (FAllPublications=1) and (FIncorrectPublications=0), + 'Scroll burst did not coalesce to exactly one final-account fetch/publication'); + Require((FAdmissionTick>=FInputTick+240) and (FAdmissionTick<FInputTick+1000), + 'Latest operations fetch did not follow the configured quiet interval'); + Require((FCardHideTick<>0) and not FCardReopened, + 'Completed latest-account publication retained or reopened its loading card'); + FLog.Add(Format('PASS scroll burst: six native selections, one latest query after %dms, loading hidden after %dms', + [FAdmissionTick-FInputTick,FCardHideTick-FInputTick])); +end; + +procedure TConsecutiveSelectionObserver.Run(const ALogPath: String); +const + ClickCount = 20; + ImmediateLimitMilliseconds = 1000; +var + I, LSlow, LWrongFetchCount, LWrongPublicationCount, LWrongIdentityCount, LWrongCardCount: Integer; + LUntil, LAdmission, LProviderCompleted, LPublication, LCard: UInt64; + LMetrics: TMockFetchMetrics; + LSpacing: String; + + procedure FinalizePreviousClick; + begin + LMetrics := FMain.Model.Metrics(mdkOperations); + Require(FAdmissionTick >= FInputTick, 'Provider admission belongs to an older input'); + Require(FProviderEndTick >= FAdmissionTick, 'Provider timing is inconsistent'); + LAdmission := FAdmissionTick - FInputTick; + LProviderCompleted := FProviderEndTick - FInputTick; + LPublication := FPublicationTick - FInputTick; + LCard := FCardHideTick - FInputTick; + CheckCard; + FLog.Add(Format('CLICK_RESULT click=%d account=%d admission_ms=%d provider_end_ms=%d publication_ms=%d card_hide_ms=%d card_reopened=%s fetch_delta=%d current_publications=%d all_publications=%d wrong_identity=%d waiting_before=%s waiting_after=%s spacing=%s', + [FIndex, FExpectedAccount, LAdmission, LProviderCompleted, LPublication, LCard, + BoolToStr(FCardReopened, True), + LMetrics.FetchCount - FBaselineFetches, FPublications, FAllPublications, + FIncorrectPublications, BoolToStr(FWaitingBefore, True), + BoolToStr(FMain.MockWallet.OperationsGrid.RefreshThrottle.Waiting, True), FSpacing])); + if LPublication > ImmediateLimitMilliseconds then + begin + Inc(LSlow); + FLog.Add(Format('FAIL click=%d current history publication exceeded new immediate-selection bound %dms: %dms', + [FIndex, ImmediateLimitMilliseconds, LPublication])); + end; + if LMetrics.FetchCount <> FBaselineFetches + 1 then + Inc(LWrongFetchCount); + if (FPublications <> 1) or (FAllPublications <> 1) then + Inc(LWrongPublicationCount); + if FIncorrectPublications <> 0 then + Inc(LWrongIdentityCount); + if FCardReopened or (FCardHideTick - FPublicationTick > 200) then + Inc(LWrongCardCount); + Require(CurrentRowsMatch, 'Published native rows no longer match current selected account after inter-click observation'); + end; +begin + LSlow := 0; + LWrongFetchCount := 0; + LWrongPublicationCount := 0; + LWrongIdentityCount := 0; + LWrongCardCount := 0; + FLog.Add('CONTRACT operations RefreshThrottle burst-finished250ms only; existing loading behavior unchanged; no product selection timer'); + FLog.Add('SCOPE actual main/mock wallet composition and native clicks; only account/history records synthetic; no signing/network/data clone'); + FMain.Model.DelayMilliseconds := 0; + FMain.MockWallet.OperationsSource.OperationsPerAccount := 268; + try + // Initial setup alone may settle to quiet. No per-click loop waits for it. + LUntil := GetTickCount64 + 10000; + while (FMain.MockWallet.AccountsGrid.IsUpdating or + FMain.MockWallet.OperationsGrid.IsUpdating or + FMain.MockWallet.AccountsGrid.RefreshThrottle.Waiting or + FMain.MockWallet.OperationsGrid.RefreshThrottle.Waiting) and + (GetTickCount64 < LUntil) do + Pump(5); + Require(not FMain.MockWallet.AccountsGrid.IsUpdating and + not FMain.MockWallet.OperationsGrid.IsUpdating and + not FMain.MockWallet.AccountsGrid.RefreshThrottle.Waiting and + not FMain.MockWallet.OperationsGrid.RefreshThrottle.Waiting, + 'Initial real grids did not settle to quiet'); + for I := 1 to ClickCount do + begin + LSpacing := 'consecutive-after-current-publication'; + if (I = 17) or (I = 19) then + begin + LSpacing := 'spaced-5200ms-after-current-publication'; + Pump(5200); + end + else + Pump(25); + // Keep the preceding observer armed throughout spacing. Late callbacks + // must not disappear merely because no new click has been sent yet. + if FIndex <> 0 then + FinalizePreviousClick; + FIndex := I; + FSpacing := LSpacing; + FExpectedAccount := (I - 1) mod 2; + FPublicationTick := 0; + FCardHideTick := 0; + FAdmissionTick := 0; + FProviderEndTick := 0; + FPublications := 0; + FAllPublications := 0; + FIncorrectPublications := 0; + FCardReopened := False; + LMetrics := FMain.Model.Metrics(mdkOperations); + FBaselineFetches := LMetrics.FetchCount; + FWaitingBefore := FMain.MockWallet.OperationsGrid.RefreshThrottle.Waiting; + FInputTick := GetTickCount64; + FLog.Add(Format('INPUT click=%d account=%d tick=%d waiting_before=%s spacing=%s', + [I, FExpectedAccount, FInputTick, BoolToStr(FWaitingBefore, True), LSpacing])); + NativeClick(FExpectedAccount); + LUntil := GetTickCount64 + 8000; + while ((FPublicationTick = 0) or (FCardHideTick = 0)) and + (GetTickCount64 < LUntil) do + Pump(2); + Require(FPublicationTick <> 0, 'No current-account publication within eight-second observation bound'); + Require(FCardHideTick <> 0, 'Current-account publication retained its loading card'); + Require(CurrentRowsMatch, 'Published native rows do not match current selected account'); + end; + // This is one final grace period, not a quiet wait between requested pages. + // It detects duplicate trailing publications/queries after the last page. + Pump(5200); + FinalizePreviousClick; + FLog.Add(Format('RESULT clicks=%d slow=%d wrong_fetch_count=%d wrong_publication_count=%d wrong_identity_count=%d wrong_card_count=%d', + [ClickCount, LSlow, LWrongFetchCount, LWrongPublicationCount, LWrongIdentityCount, LWrongCardCount])); + Require((LSlow = 0) and (LWrongFetchCount = 0) and (LWrongPublicationCount = 0) and + (LWrongIdentityCount = 0) and (LWrongCardCount = 0), + 'Consecutive native selections failed the newly requested immediate-response policy'); + FLog.Add('PASS 20 independent selections: bounded publication, current rows, one fetch and no stale loading card'); + CheckRapidBurst; + finally + FIndex := 0; + FLog.SaveToFile(ALogPath); + end; +end; + +procedure CheckConsecutiveNativeSelections(AMain: TMockMainForm; const ALogPath: String); +var + LObserver: TConsecutiveSelectionObserver; +begin + LObserver := TConsecutiveSelectionObserver.Create(AMain); + try + LObserver.Run(ALogPath); + finally + LObserver.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletGridMocks.pas b/src/tests/experimental/native/grid-refresh/UWalletGridMocks.pas new file mode 100644 index 000000000..cdb6ca4e6 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletGridMocks.pas @@ -0,0 +1,664 @@ +unit UWalletGridMocks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Test-only providers for the experimental wallet. The real control owns these + typed sources through its normal factories; its scheduler, invalidation, + caches, selection callbacks, page population and balance worker are inherited. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses + Classes, SysUtils, SyncObjs, Generics.Collections, UCommon, UCommon.Data, + UDataSources, UWalletStartupTask, UCoreObjects, UPCDataTypes, UBlockChain, + UCTRLWallet, UFRMMainForm, UVisualGrid; + +type + TMockDataKind = (mdkAccounts, mdkOperations); + + TMockFetchMetrics = record + FetchCount, CompletedCount, RawReadCount, PageAccountReadCount: Integer; + BalanceAccountReadCount, SnapshotReadCount, MembershipReadCount: Integer; + ActiveFetches, LastRevision: Integer; + LastStartedTick, LastCompletedTick: UInt64; + end; + + TMockWalletSnapshot = record + Revision, RowCount: Integer; + end; + + TMockWalletData = class(TComponent) + private + FLock: TCriticalSection; + FFetchGate: TEvent; + FBalanceGate: TEvent; + FEvents: TStringList; + FRevision, FRowCount, FDelayMilliseconds: Integer; + FGateFetches, FShuttingDown: Boolean; + FMetrics: array[TMockDataKind] of TMockFetchMetrics; + function GetRevision: Integer; + function GetRowCount: Integer; + procedure SetRowCount(AValue: Integer); + function GetDelayMilliseconds: Integer; + procedure SetDelayMilliseconds(AValue: Integer); + function GetGateFetches: Boolean; + procedure SetGateFetches(AValue: Boolean); + function GetShuttingDown: Boolean; + public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + procedure SetData(ARevision, ARowCount: Integer); + procedure Advance(ARowCount: Integer = -1); + procedure Reset; + procedure ReleaseFetches; + procedure HoldBalanceSnapshots; + procedure ReleaseBalanceSnapshots; + function AwaitBalanceSnapshot(ATask: TWalletStartupTask): Boolean; + procedure Shutdown; + function Snapshot: TMockWalletSnapshot; + function Metrics(AKind: TMockDataKind): TMockFetchMetrics; + procedure ResetMetrics; + procedure LogEvent(const AMessage: String); + procedure DrainEvents(AOutput: TStrings); + procedure BeginFetch(AKind: TMockDataKind); + procedure FinishFetch(AKind: TMockDataKind; ARevision: Integer); + procedure CountRawRead(AKind: TMockDataKind); + procedure CountMembershipRead; + procedure CountAccountRead(ABalance: Boolean); + procedure CountSnapshotRead; + function AccountKey: TAccountKey; + function Account(AID: Cardinal; const ASnapshot: TMockWalletSnapshot): TAccount; + property Revision: Integer read GetRevision; + property RowCount: Integer read GetRowCount write SetRowCount; + property DelayMilliseconds: Integer read GetDelayMilliseconds write SetDelayMilliseconds; + property GateFetches: Boolean read GetGateFetches write SetGateFetches; + property ShuttingDown: Boolean read GetShuttingDown; + end; + + TMockMyAccountsDataSource = class(TMyAccountsDataSource) + private + FModel: TMockWalletData; + protected + procedure BeginReadSnapshot(AIncludePending: Boolean; out AContext: TAccountReadContext); override; + function BeginBalanceSnapshot(ATask: TWalletStartupTask; AGeneration: Int64; + AIncludePending: Boolean; out AContext: TAccountReadContext): Boolean; override; + procedure EndReadSnapshot(const AContext: TAccountReadContext); override; + function WalletKeys(const AContext: TAccountReadContext): TArray<TAccountKey>; override; + function KeyAccountCount(const AKey: TAccountKey; const AContext: TAccountReadContext): Integer; override; + function ReadKeyPrefix(const AKey: TAccountKey; ACount: Integer; + const AContext: TAccountReadContext): TArray<Cardinal>; override; + function OwnershipChanges(const AContext: TAccountReadContext): TArray<TAccountOwnershipChange>; override; + function ReadPageAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; override; + function ReadBalanceAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; override; + public + constructor CreateForModel(AOwner: TComponent; AModel: TMockWalletData); + procedure FetchAll(const AContainer: TList<TAccount>); override; + end; + + TMockAccountsOperationsDataSource = class(TAccountsOperationsDataSource) + private + FModel: TMockWalletData; + FLastFetchBlockDepth: Cardinal; + FRawRevision: Integer; + FOperationsPerAccount: Integer; + protected + function FetchWalletAccounts: TArray<Cardinal>; override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; override; + public + constructor CreateForModel(AOwner: TComponent; AModel: TMockWalletData); + procedure FetchAll(const AContainer: TList<TOperationResume>); override; + property LastFetchBlockDepth: Cardinal read FLastFetchBlockDepth; + property OperationsPerAccount: Integer read FOperationsPerAccount write FOperationsPerAccount; + end; + + TMockWallet = class(TCTRLWallet) + private + FModel: TMockWalletData; + FAccountsSource: TMockMyAccountsDataSource; + FOperationsSource: TMockAccountsOperationsDataSource; + FAccountRefreshCount, FOperationRefreshCount: Integer; + FLastAccountRefreshTick, FLastOperationRefreshTick: UInt64; + procedure OnAccountsRefresh(Sender: TObject); + procedure OnOperationsRefresh(Sender: TObject); + protected + function CreateAccountsDataSource: TMyAccountsDataSource; override; + function CreateOperationsDataSource: TAccountsOperationsDataSource; override; + public + constructor CreateForModel(AOwner: TComponent; AModel: TMockWalletData); + procedure AfterConstruction; override; + property Model: TMockWalletData read FModel; + property AccountsSource: TMockMyAccountsDataSource read FAccountsSource; + property OperationsSource: TMockAccountsOperationsDataSource read FOperationsSource; + property AccountsGrid; + property OperationsGrid; + property AccountRefreshCount: Integer read FAccountRefreshCount; + property OperationRefreshCount: Integer read FOperationRefreshCount; + property LastAccountRefreshTick: UInt64 read FLastAccountRefreshTick; + property LastOperationRefreshTick: UInt64 read FLastOperationRefreshTick; + end; + + TMockMainForm = class(TFRMMainForm) + private + FModel: TMockWalletData; + function GetMockWallet: TMockWallet; + protected + function CreateWalletControl: TCTRLWallet; override; + public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + property Model: TMockWalletData read FModel; + property MockWallet: TMockWallet read GetMockWallet; + end; + +implementation + +uses + Windows, Math, DateUtils, Variants, UBaseTypes, UConst, UAccounts, UMemory; + +function KindName(AKind: TMockDataKind): String; +begin + if AKind = mdkAccounts then Result := 'accounts' else Result := 'operations'; +end; + +function ContextSnapshot(const AContext: TAccountReadContext): TMockWalletSnapshot; +var LSeparator: Integer; +begin + LSeparator := Pos('/', AContext.StateID); + Result.Revision := StrToInt(Copy(AContext.StateID, 1, LSeparator - 1)); + Result.RowCount := StrToInt(Copy(AContext.StateID, LSeparator + 1, MaxInt)); +end; + +function TextRevision(const AText: String): Integer; +var LEnd: Integer; +begin + Result := -1; + if Pos('mock revision=', AText) <> 1 then Exit; + LEnd := Pos(' account=', AText); + if LEnd > 0 then Result := StrToInt(Copy(AText, 15, LEnd - 15)); +end; + +constructor TMockWalletData.Create(AOwner: TComponent); +begin + inherited; + FLock := SyncObjs.TCriticalSection.Create; + FFetchGate := TEvent.Create(nil, true, true, ''); + FBalanceGate := TEvent.Create(nil, true, true, ''); + FEvents := TStringList.Create; + FRowCount := 200; +end; + +destructor TMockWalletData.Destroy; +begin + Shutdown; + FEvents.Free; + FFetchGate.Free; + FBalanceGate.Free; + FLock.Free; + inherited; +end; + +function TMockWalletData.Snapshot: TMockWalletSnapshot; +begin + FLock.Acquire; + try Result.Revision := FRevision; Result.RowCount := FRowCount; + finally FLock.Release; end; +end; + +function TMockWalletData.GetRevision: Integer; +begin + Result := Snapshot.Revision; +end; + +function TMockWalletData.GetRowCount: Integer; +begin + Result := Snapshot.RowCount; +end; + +procedure TMockWalletData.SetRowCount(AValue: Integer); +begin + Advance(AValue); +end; + +procedure TMockWalletData.SetData(ARevision, ARowCount: Integer); +begin + if (ARevision < 0) or (ARowCount < 0) then + raise EArgumentException.Create('Mock revision and row count must be nonnegative'); + FLock.Acquire; + try FRevision := ARevision; FRowCount := ARowCount; + finally FLock.Release; end; +end; + +procedure TMockWalletData.Advance(ARowCount: Integer); +begin + FLock.Acquire; + try + Inc(FRevision); + if ARowCount >= 0 then FRowCount := ARowCount; + finally FLock.Release; end; +end; + +procedure TMockWalletData.Reset; +begin + ReleaseFetches; + DelayMilliseconds := 0; + Advance(200); +end; + +function TMockWalletData.GetDelayMilliseconds: Integer; +begin + FLock.Acquire; + try Result := FDelayMilliseconds; finally FLock.Release; end; +end; + +procedure TMockWalletData.SetDelayMilliseconds(AValue: Integer); +begin + if AValue < 0 then raise EArgumentException.Create('Mock delay must be nonnegative'); + FLock.Acquire; + try FDelayMilliseconds := AValue; finally FLock.Release; end; +end; + +function TMockWalletData.GetGateFetches: Boolean; +begin + FLock.Acquire; + try Result := FGateFetches; finally FLock.Release; end; +end; + +procedure TMockWalletData.SetGateFetches(AValue: Boolean); +begin + FLock.Acquire; + try + FGateFetches := AValue; + if AValue then FFetchGate.ResetEvent else FFetchGate.SetEvent; + finally FLock.Release; end; +end; + +procedure TMockWalletData.ReleaseFetches; +begin + SetGateFetches(false); +end; + +procedure TMockWalletData.HoldBalanceSnapshots; +begin + FBalanceGate.ResetEvent; +end; + +procedure TMockWalletData.ReleaseBalanceSnapshots; +begin + FBalanceGate.SetEvent; +end; + +function TMockWalletData.AwaitBalanceSnapshot(ATask: TWalletStartupTask): Boolean; +begin + // Pause only the fake persisted snapshot read; retain the production balance + // worker, generation checks and queued completion notification. + while FBalanceGate.WaitFor(10)<>wrSignaled do + if ATask.Cancelled or ShuttingDown then Exit(False); + Result := not ATask.Cancelled and not ShuttingDown; +end; + +function TMockWalletData.GetShuttingDown: Boolean; +begin + FLock.Acquire; + try Result := FShuttingDown; finally FLock.Release; end; +end; + +procedure TMockWalletData.Shutdown; +begin + FLock.Acquire; + try + FShuttingDown := true; + FGateFetches := false; + FFetchGate.SetEvent; + FBalanceGate.SetEvent; + finally FLock.Release; end; +end; + +function TMockWalletData.Metrics(AKind: TMockDataKind): TMockFetchMetrics; +begin + FLock.Acquire; + try Result := FMetrics[AKind]; finally FLock.Release; end; +end; + +procedure TMockWalletData.ResetMetrics; +begin + FLock.Acquire; + try + if (FMetrics[mdkAccounts].ActiveFetches <> 0) or (FMetrics[mdkOperations].ActiveFetches <> 0) then + raise EInvalidOperation.Create('Wait for mock fetches before resetting metrics'); + FillChar(FMetrics, SizeOf(FMetrics), 0); + finally FLock.Release; end; +end; + +procedure TMockWalletData.LogEvent(const AMessage: String); +begin + FLock.Acquire; + try FEvents.Add(UIntToStr(GetTickCount64) + ' ' + AMessage); + finally FLock.Release; end; +end; + +procedure TMockWalletData.DrainEvents(AOutput: TStrings); +begin + FLock.Acquire; + try AOutput.AddStrings(FEvents); FEvents.Clear; + finally FLock.Release; end; +end; + +procedure TMockWalletData.BeginFetch(AKind: TMockDataKind); +var LStarted: UInt64; LDelay: Integer; +begin + FLock.Acquire; + try + Inc(FMetrics[AKind].FetchCount); + Inc(FMetrics[AKind].ActiveFetches); + FMetrics[AKind].LastStartedTick := GetTickCount64; + LDelay := FDelayMilliseconds; + FEvents.Add(UIntToStr(GetTickCount64) + ' FETCH ' + KindName(AKind) + + ' #' + IntToStr(FMetrics[AKind].FetchCount) + ' revision=' + IntToStr(FRevision)); + finally FLock.Release; end; + LStarted := GetTickCount64; + try + repeat + if ShuttingDown or TThread.CheckTerminated then + raise EAbort.Create('Mock fetch canceled'); + if (FFetchGate.WaitFor(10) = wrSignaled) and + (GetTickCount64 - LStarted >= UInt64(LDelay)) then Break; + Sleep(10); + until false; + except + FinishFetch(AKind, -1); + raise; + end; +end; + +procedure TMockWalletData.FinishFetch(AKind: TMockDataKind; ARevision: Integer); +begin + FLock.Acquire; + try + Dec(FMetrics[AKind].ActiveFetches); + Inc(FMetrics[AKind].CompletedCount); + FMetrics[AKind].LastRevision := ARevision; + FMetrics[AKind].LastCompletedTick := GetTickCount64; + FEvents.Add(UIntToStr(GetTickCount64) + ' DONE ' + KindName(AKind) + + ' revision=' + IntToStr(ARevision)); + finally FLock.Release; end; +end; + +procedure TMockWalletData.CountRawRead(AKind: TMockDataKind); +begin + FLock.Acquire; + try Inc(FMetrics[AKind].RawReadCount); finally FLock.Release; end; +end; + +procedure TMockWalletData.CountMembershipRead; +begin + FLock.Acquire; + try Inc(FMetrics[mdkOperations].MembershipReadCount); finally FLock.Release; end; +end; + +procedure TMockWalletData.CountAccountRead(ABalance: Boolean); +begin + FLock.Acquire; + try + if ABalance then Inc(FMetrics[mdkAccounts].BalanceAccountReadCount) + else Inc(FMetrics[mdkAccounts].PageAccountReadCount); + finally FLock.Release; end; +end; + +procedure TMockWalletData.CountSnapshotRead; +begin + FLock.Acquire; + try Inc(FMetrics[mdkAccounts].SnapshotReadCount); finally FLock.Release; end; +end; + +function TMockWalletData.AccountKey: TAccountKey; +begin + Result := Default(TAccountKey); + Result.EC_OpenSSL_NID := CT_Default_EC_OpenSSL_NID; + SetLength(Result.x, 32); SetLength(Result.y, 32); + Result.x[31] := 1; Result.y[31] := 2; +end; + +function TMockWalletData.Account(AID: Cardinal; const ASnapshot: TMockWalletSnapshot): TAccount; +begin + Result := Default(TAccount); + Result.account := AID; + Result.accountInfo.state := as_Normal; + Result.accountInfo.accountKey := AccountKey; + Result.balance := 10000 + AID; + Result.name.FromString('mock revision=' + IntToStr(ASnapshot.Revision) + ' account=' + UIntToStr(AID)); +end; + +constructor TMockMyAccountsDataSource.CreateForModel(AOwner: TComponent; AModel: TMockWalletData); +begin + FModel := AModel; + inherited Create(AOwner); +end; + +procedure TMockMyAccountsDataSource.BeginReadSnapshot(AIncludePending: Boolean; out AContext: TAccountReadContext); +var LSnapshot: TMockWalletSnapshot; +begin + FModel.CountSnapshotRead; + LSnapshot := FModel.Snapshot; + AContext := Default(TAccountReadContext); + AContext.StateID := IntToStr(LSnapshot.Revision) + '/' + IntToStr(LSnapshot.RowCount); + AContext.BlockTip := 1000000 + LSnapshot.Revision; +end; + +function TMockMyAccountsDataSource.BeginBalanceSnapshot(ATask: TWalletStartupTask; + AGeneration: Int64; AIncludePending: Boolean; out AContext: TAccountReadContext): Boolean; +begin + Result := FModel.AwaitBalanceSnapshot(ATask); + if Result then BeginReadSnapshot(AIncludePending, AContext); +end; + +procedure TMockMyAccountsDataSource.EndReadSnapshot(const AContext: TAccountReadContext); +begin +end; + +function TMockMyAccountsDataSource.WalletKeys(const AContext: TAccountReadContext): TArray<TAccountKey>; +begin + SetLength(Result, 1); + Result[0] := FModel.AccountKey; +end; + +function TMockMyAccountsDataSource.KeyAccountCount(const AKey: TAccountKey; + const AContext: TAccountReadContext): Integer; +begin + Result := ContextSnapshot(AContext).RowCount; +end; + +function TMockMyAccountsDataSource.ReadKeyPrefix(const AKey: TAccountKey; + ACount: Integer; const AContext: TAccountReadContext): TArray<Cardinal>; +var I: Integer; +begin + SetLength(Result, Min(ACount, ContextSnapshot(AContext).RowCount)); + for I := 0 to High(Result) do Result[I] := I; +end; + +function TMockMyAccountsDataSource.OwnershipChanges(const AContext: TAccountReadContext): TArray<TAccountOwnershipChange>; +begin + Result := nil; +end; + +function TMockMyAccountsDataSource.ReadPageAccount(AAccountNumber: Cardinal; + const AContext: TAccountReadContext): TAccount; +var LSnapshot: TMockWalletSnapshot; +begin + FModel.CountAccountRead(false); LSnapshot := ContextSnapshot(AContext); + Result := FModel.Account(AAccountNumber, LSnapshot); +end; + +function TMockMyAccountsDataSource.ReadBalanceAccount(AAccountNumber: Cardinal; + const AContext: TAccountReadContext): TAccount; +begin + FModel.CountAccountRead(true); + Result := FModel.Account(AAccountNumber, ContextSnapshot(AContext)); +end; + +function TMockMyAccountsDataSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; +var LRevision: Integer; +begin + FModel.BeginFetch(mdkAccounts); + LRevision := -1; + try + Result := inherited FetchPage(AParams, ADataTable); + if Length(ADataTable.Rows) > 0 then LRevision := TextRevision(VarToStr(ADataTable.Rows[0].Name)); + finally FModel.FinishFetch(mdkAccounts, LRevision); end; +end; + +procedure TMockMyAccountsDataSource.FetchAll(const AContainer: TList<TAccount>); +begin + FModel.CountRawRead(mdkAccounts); + inherited; +end; + +constructor TMockAccountsOperationsDataSource.CreateForModel(AOwner: TComponent; AModel: TMockWalletData); +begin + FModel := AModel; + FOperationsPerAccount := 1; + inherited Create(AOwner); +end; + +function TMockAccountsOperationsDataSource.FetchWalletAccounts: TArray<Cardinal>; +var I: Integer; LSnapshot: TMockWalletSnapshot; +begin + FModel.CountMembershipRead; LSnapshot := FModel.Snapshot; + SetLength(Result, LSnapshot.RowCount); + for I := 0 to High(Result) do Result[I] := I; +end; + +procedure TMockAccountsOperationsDataSource.FetchAll(const AContainer: TList<TOperationResume>); +var + LSnapshot: TMockWalletSnapshot; LAccounts: TArray<Cardinal>; + LOperation: TOperationResume; LAccount: Cardinal; I, J, LIndex: Integer; +begin + // Observe the actual configured query input for native history-picker checks. + FLastFetchBlockDepth := BlockDepth; + FModel.CountRawRead(mdkOperations); + RefreshWalletAccounts; + LAccounts := Accounts; + LSnapshot := FModel.Snapshot; FRawRevision := LSnapshot.Revision; + for I := 0 to High(LAccounts) do begin + if TThread.CheckTerminated or FModel.ShuttingDown then raise EAbort.Create('Mock history canceled'); + LAccount := LAccounts[I]; + if LAccount >= Cardinal(LSnapshot.RowCount) then Continue; + // Dense history remains a provider fixture; selection and paging use the + // production adapters. Other scenarios retain one operation per account. + for J := 0 to FOperationsPerAccount - 1 do begin + LIndex := I * FOperationsPerAccount + J; + LOperation := Default(TOperationResume); + LOperation.valid := true; LOperation.Block := 1000000 + LSnapshot.Revision; + LOperation.NOpInsideBlock := LIndex; LOperation.OpType := 1; + LOperation.time := DateTimeToUnix(Now, false) - 60; + LOperation.AffectedAccount := LAccount; LOperation.SignerAccount := LAccount; + LOperation.Amount := 8; LOperation.Balance := 10000 + LAccount; + LOperation.OperationTxt := 'mock revision=' + IntToStr(LSnapshot.Revision) + ' account=' + UIntToStr(LAccount); + if J = 0 then + LOperation.OperationHash.FromString('mock-' + IntToStr(LSnapshot.Revision) + '-' + UIntToStr(LAccount)) + else + LOperation.OperationHash.FromString('mock-' + IntToStr(LSnapshot.Revision) + '-' + UIntToStr(LAccount) + '-' + IntToStr(J)); + AContainer.Add(LOperation); + end; + end; +end; + +function TMockAccountsOperationsDataSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; +begin + FModel.BeginFetch(mdkOperations); + try Result := inherited FetchPage(AParams, ADataTable); + finally FModel.FinishFetch(mdkOperations, FRawRevision); end; +end; + +constructor TMockWallet.CreateForModel(AOwner: TComponent; AModel: TMockWalletData); +begin + FModel := AModel; + inherited Create(AOwner); +end; + +procedure TMockWallet.AfterConstruction; +begin + inherited; + // LCL runs the form's real OnCreate from AfterConstruction. Apply mock-only + // action restrictions after it has constructed the actual grids. + // The data is synthetic. Preserve grid selection and configuration while + // making monetary operations unavailable in this explicitly labelled demo. + miSendPASC.Enabled := false; + miBuyAccount.Enabled := false; + miChangeKey.Enabled := false; + miChangeAccountInfo.Enabled := false; + miEnlistAccountsForSale.Enabled := false; + miDelistAccountsFromSale.Enabled := false; + // Transaction/detail popups query the real node using the selected account. + // Those actions are outside a synthetic dataset. Keep all selection, fetch, + // paging and completion callbacks wired to the actual wallet implementation. + AccountsGrid.OnPreparePopupMenu := nil; + OperationsGrid.OnPreparePopupMenu := nil; + // Count the actual grid-owned admission events independently of raw queries. + // Do not replace the production refresh, rendering or readiness paths. + AccountsGrid.RefreshThrottle.Add(OnAccountsRefresh); + OperationsGrid.RefreshThrottle.Add(OnOperationsRefresh); +end; + +function TMockWallet.CreateAccountsDataSource: TMyAccountsDataSource; +begin + FAccountsSource := TMockMyAccountsDataSource.CreateForModel(Self, FModel); + Result := FAccountsSource; +end; + +function TMockWallet.CreateOperationsDataSource: TAccountsOperationsDataSource; +begin + FOperationsSource := TMockAccountsOperationsDataSource.CreateForModel(Self, FModel); + Result := FOperationsSource; +end; + +procedure TMockWallet.OnAccountsRefresh(Sender: TObject); +begin + Inc(FAccountRefreshCount); + FLastAccountRefreshTick := GetTickCount64; + FModel.LogEvent('ACCOUNTS_ADMITTED #' + IntToStr(FAccountRefreshCount)); +end; + +procedure TMockWallet.OnOperationsRefresh(Sender: TObject); +begin + Inc(FOperationRefreshCount); + FLastOperationRefreshTick := GetTickCount64; + FModel.LogEvent('OPERATIONS_ADMITTED #' + IntToStr(FOperationRefreshCount)); +end; + +constructor TMockMainForm.Create(AOwner: TComponent); +begin + inherited; + FModel := TMockWalletData.Create(Self); + if SameText(SysUtils.GetEnvironmentVariable('PASCAL_WALLET_SELECTION_ONLY'),'1') then + FModel.HoldBalanceSnapshots; + Caption := 'MOCK DATA - PascalCoin grid refresh test (offline, disposable wallet)'; +end; + +destructor TMockMainForm.Destroy; +begin + if Assigned(FModel) then FModel.Shutdown; + inherited; +end; + +function TMockMainForm.CreateWalletControl: TCTRLWallet; +begin + Result := TMockWallet.CreateForModel(Self, FModel); +end; + +function TMockMainForm.GetMockWallet: TMockWallet; +begin + Result := WalletControl as TMockWallet; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletGridRefreshHarness.pas b/src/tests/experimental/native/grid-refresh/UWalletGridRefreshHarness.pas new file mode 100644 index 000000000..3f8cda376 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletGridRefreshHarness.pas @@ -0,0 +1,718 @@ +unit UWalletGridRefreshHarness; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Interactive and automatic tests of the real experimental wallet composition. + Every run creates fresh temporary data. Ordinary checks run offline; Tools + checks permit loopback discovery only so they can open the real RPC dialog. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunWalletGridRefreshDemo: Integer; + +implementation + +uses + Windows, Messages, SysUtils, Classes, Forms, Controls, StdCtrls, ExtCtrls, + Graphics, Variants, {$IFNDEF FPC}Vcl.Imaging.pngimage,{$ENDIF} + UThread, UNode, UUserInterface, UWalletStartup, + UFRMMainForm, UCommon.Data, UMemory, UWalletGridMocks, UVisualGrid, + UCTRLWallet, UWalletSelectionLoadingChecks, UWalletConsecutiveSelectionChecks, USyncFlickerChecks, UWalletToolsDialogChecks, + UWalletSendPresentationChecks, UWalletOperationMessageChecks, UWalletAdvancedPresentationChecks, + UWalletKeyNotificationChecks; + +type + TGridInputAccess = class(TVisualGrid) + public + procedure Search(const AExpression: String); + end; + + TMockBlockProducer = class(TPCThread) + private + FNode: TNode; + FModel: TMockWalletData; + FCycleRows: Boolean; + FCount: LongInt; + protected + procedure BCExecute; override; + public + constructor Create(ANode: TNode; AModel: TMockWalletData; ACycleRows: Boolean); + procedure StopAndJoin; + property Count: LongInt read FCount; + end; + + TAutomaticPhase = (apStartup, apGapHold, apGapResume, apFinished); + + TGridRefreshHarness = class(TComponent) + private + FMain: TMockMainForm; + FProducer: TMockBlockProducer; + FTimer: TTimer; + FPanel: TPanel; + FLogView: TMemo; + FStatus, FExpectation: TLabel; + FCycleRows, FSlowFetch: TCheckBox; + FRows: TComboBox; + FAllLog: TStringList; + FPreviousDestroy: TNotifyEvent; + FAutomatic, FPassed, FWired, FWorkActive, FInTick, FManualGap: Boolean; + FWaitingImageSaved: Boolean; + FLogPath: String; + FPhase: TAutomaticPhase; + FNotifications, FFinalRevision: Integer; + FStarted, FLastChange, FStopTick, FHoldStarted: UInt64; + FLastAccounts, FLastOperations: Integer; + procedure Tick(Sender: TObject); + procedure MainDestroy(Sender: TObject); + procedure Log(const AMessage: String); + procedure DrainLog; + procedure UpdateCounters; + procedure Require(ACondition: Boolean; const AMessage: String); + procedure NotifyBlock; + procedure StopProducer; + procedure EndWork; + procedure StartContinuous(Sender: TObject); + procedure StopContinuous(Sender: TObject); + procedure StartBusyGap(Sender: TObject); + procedure ToggleFetchGate(Sender: TObject); + procedure ReleaseFetchGate(Sender: TObject); + procedure ResetDemo(Sender: TObject); + procedure ConfigureDelay(Sender: TObject); + procedure ConfigureRows(Sender: TObject); + procedure CheckNodeLoading(const ACheckpoint: String); + procedure CaptureWaitingDemo; + procedure AutomaticTick; + function GridsIdle: Boolean; + function CreateButton(const ACaption: String; ALeft, ATop, AWidth: Integer; + AOnClick: TNotifyEvent): TButton; + public + constructor Create(AMain: TMockMainForm; AAutomatic: Boolean; const ALogPath: String); reintroduce; + destructor Destroy; override; + end; + +var + AutomaticPassed: Boolean; + +procedure TGridInputAccess.Search(const AExpression: String); +begin + FSearchEdit.Text := AExpression; + SearchButtonClick(Self); +end; + +constructor TMockBlockProducer.Create(ANode: TNode; AModel: TMockWalletData; ACycleRows: Boolean); +begin + inherited Create(true); + FNode := ANode; + FModel := AModel; + FCycleRows := ACycleRows; +end; + +procedure TMockBlockProducer.BCExecute; +const CSizes: array[0..3] of Integer = (0, 1, 2, 200); +var LRows: Integer; +begin + while not Terminated and not FModel.ShuttingDown do begin + if FCycleRows then LRows := CSizes[(FCount div 4) mod Length(CSizes)] else LRows := -1; + FModel.Advance(LRows); + InterlockedIncrement(FCount); + FNode.NotifyBlocksChanged; + Sleep(100); + end; +end; + +procedure TMockBlockProducer.StopAndJoin; +begin + Terminate; + WaitFor; +end; + +constructor TGridRefreshHarness.Create(AMain: TMockMainForm; AAutomatic: Boolean; const ALogPath: String); +begin + inherited Create(AMain); + FMain := AMain; + FAutomatic := AAutomatic; + FLogPath := ALogPath; + FStarted := GetTickCount64; + FAllLog := TStringList.Create; + FPreviousDestroy := FMain.OnDestroy; + FMain.OnDestroy := MainDestroy; + + FPanel := TPanel.Create(Self); + FPanel.Parent := FMain.paWalletPanel; + FPanel.Align := alTop; + FPanel.Height := 126; + FPanel.BevelOuter := bvNone; + FPanel.Caption := ''; + // The host panel keeps a large startup-placeholder font. Use the root form's + // normal UI font for the actual test controls instead of inheriting it. + FPanel.ParentFont := false; + FPanel.Font.Assign(FMain.Font); + CreateButton('Start sync (100 ms)', 8, 4, 180, StartContinuous); + CreateButton('Stop', 194, 4, 70, StopContinuous); + CreateButton('Busy block batch (6.5 s)', 270, 4, 175, StartBusyGap); + CreateButton('Gate next fetch', 451, 4, 130, ToggleFetchGate); + CreateButton('Release fetch', 587, 4, 120, ReleaseFetchGate); + CreateButton('Reset', 713, 4, 80, ResetDemo); + + FCycleRows := TCheckBox.Create(Self); + FCycleRows.Parent := FPanel; + FCycleRows.SetBounds(8, 38, 220, 24); + FCycleRows.Caption := 'Cycle row counts: 0 / 1 / 2 / 200'; + FCycleRows.Checked := true; + FSlowFetch := TCheckBox.Create(Self); + FSlowFetch.Parent := FPanel; + FSlowFetch.SetBounds(240, 38, 210, 24); + FSlowFetch.Caption := 'Slow provider (7 seconds)'; + FSlowFetch.OnClick := ConfigureDelay; + FRows := TComboBox.Create(Self); + FRows.Parent := FPanel; + FRows.Style := csDropDownList; + FRows.SetBounds(465, 38, 120, 24); + FRows.Items.CommaText := '0,1,2,200,1000'; + FRows.ItemIndex := 3; + FRows.OnChange := ConfigureRows; + FStatus := TLabel.Create(Self); + FStatus.Parent := FPanel; + FStatus.SetBounds(8, 68, 1120, 24); + FStatus.AutoSize := false; + FExpectation := TLabel.Create(Self); + FExpectation.Parent := FPanel; + FExpectation.SetBounds(8, 94, 1120, 24); + FExpectation.AutoSize := false; + FExpectation.Caption := 'Node synchronization pauses queries; finishing it immediately loads the latest data.'; + FLogView := TMemo.Create(Self); + FLogView.Parent := FMain.paWalletPanel; + FLogView.Align := alBottom; + FLogView.Height := 116; + FLogView.ReadOnly := true; + FLogView.ScrollBars := ssVertical; + FLogView.ParentFont := false; + FLogView.Font.Assign(FMain.Font); + FLogView.Font.Name := 'Consolas'; + FLogView.Font.Size := 9; + + FTimer := TTimer.Create(Self); + FTimer.Interval := 50; + FTimer.OnTimer := Tick; + Log('START mock wallet: fresh disposable data, isolated discovery; actual factories, typed sources and inherited rendering'); +end; + +destructor TGridRefreshHarness.Destroy; +begin + FTimer.Enabled := false; + StopProducer; + EndWork; + FMain.Model.ReleaseFetches; + FAllLog.Free; + inherited; +end; + +procedure TGridRefreshHarness.MainDestroy(Sender: TObject); +begin + // Stop the producer before the root frees its wallet subscriptions/sources. + FTimer.Enabled := false; + StopProducer; + EndWork; + FMain.Model.ReleaseFetches; + if Assigned(FPreviousDestroy) then FPreviousDestroy(Sender); +end; + +function TGridRefreshHarness.CreateButton(const ACaption: String; ALeft, ATop, AWidth: Integer; + AOnClick: TNotifyEvent): TButton; +begin + Result := TButton.Create(Self); + Result.Parent := FPanel; + Result.SetBounds(ALeft, ATop, AWidth, 28); + Result.Caption := ACaption; + Result.OnClick := AOnClick; + Result.Enabled := not FAutomatic; +end; + +procedure TGridRefreshHarness.Log(const AMessage: String); +begin + FMain.Model.LogEvent(AMessage); +end; + +procedure TGridRefreshHarness.DrainLog; +var LScope: TDisposables; LLines: TStringList; +begin + LLines := LScope.AddObject(TStringList.Create) as TStringList; + FMain.Model.DrainEvents(LLines); + if LLines.Count = 0 then Exit; + FAllLog.AddStrings(LLines); + FAllLog.SaveToFile(FLogPath); + FLogView.Lines.AddStrings(LLines); + while FLogView.Lines.Count > 100 do FLogView.Lines.Delete(0); + FLogView.SelStart := Length(FLogView.Text); + FLogView.Perform(EM_SCROLLCARET, 0, 0); +end; + +procedure TGridRefreshHarness.Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure TGridRefreshHarness.NotifyBlock; +begin + Inc(FNotifications); + TUserInterface.Node.NotifyBlocksChanged; +end; + +procedure TGridRefreshHarness.StopProducer; +begin + if not Assigned(FProducer) then Exit; + FProducer.StopAndJoin; + Inc(FNotifications, FProducer.Count); + FreeAndNil(FProducer); +end; + +procedure TGridRefreshHarness.EndWork; +begin + if not FWorkActive then Exit; + TUserInterface.Node.EndBlockChainUpdate; + FWorkActive := false; +end; + +procedure TGridRefreshHarness.StartContinuous(Sender: TObject); +begin + if not FWired or Assigned(FProducer) or FWorkActive or FManualGap then + Exit; + ConfigureDelay(Self); + TUserInterface.Node.BeginBlockChainUpdate; + FWorkActive := True; + FProducer := TMockBlockProducer.Create(TUserInterface.Node, FMain.Model, FCycleRows.Checked); + FProducer.Start; + Log('SYNC producer started: actual node lifecycle pauses queries while notifications arrive every 100ms'); +end; + +procedure TGridRefreshHarness.StopContinuous(Sender: TObject); +begin + StopProducer; + EndWork; + FManualGap := False; + FPhase := apStartup; + FStopTick := GetTickCount64; + Log('SYNC producer stopped: actual lifecycle completion resumes the latest data without a quiet timer'); +end; + +procedure TGridRefreshHarness.StartBusyGap(Sender: TObject); +begin + if not FWired or FWorkActive or FManualGap or Assigned(FProducer) then + Exit; + FManualGap := True; + TUserInterface.Node.BeginBlockChainUpdate; + FWorkActive := True; + FMain.Model.Advance(200); + FFinalRevision := FMain.Model.Revision; + NotifyBlock; + FHoldStarted := GetTickCount64; + FPhase := apGapHold; + Log('BUSY BATCH: actual node synchronization remains active for 6.5s without provider queries'); +end; + +procedure TGridRefreshHarness.ToggleFetchGate(Sender: TObject); +begin + FMain.Model.GateFetches := not FMain.Model.GateFetches; + Log('GATE enabled=' + BoolToStr(FMain.Model.GateFetches, true)); +end; + +procedure TGridRefreshHarness.ReleaseFetchGate(Sender: TObject); +begin + FMain.Model.ReleaseFetches; + Log('GATE released'); +end; + +procedure TGridRefreshHarness.ResetDemo(Sender: TObject); +begin + FManualGap := false; + FPhase := apStartup; + StopProducer; + EndWork; + FMain.Model.Reset; + FSlowFetch.Checked := false; + FRows.ItemIndex := 3; + NotifyBlock; + Log('RESET: 200 rows, no provider delay, gates open; counters remain cumulative'); +end; + +procedure TGridRefreshHarness.ConfigureDelay(Sender: TObject); +begin + if FSlowFetch.Checked then FMain.Model.DelayMilliseconds := 7000 + else FMain.Model.DelayMilliseconds := 0; +end; + +procedure TGridRefreshHarness.ConfigureRows(Sender: TObject); +begin + FMain.Model.Advance(StrToInt(FRows.Text)); + NotifyBlock; +end; + +function TGridRefreshHarness.GridsIdle: Boolean; +begin + Result := Assigned(FMain.MockWallet) and + not FMain.MockWallet.AccountsGrid.IsUpdating and + not FMain.MockWallet.OperationsGrid.IsUpdating; +end; + +procedure TGridRefreshHarness.UpdateCounters; +var A, O: TMockFetchMetrics; LNotifications: Integer; +begin + A := FMain.Model.Metrics(mdkAccounts); + O := FMain.Model.Metrics(mdkOperations); + LNotifications := FNotifications; + if Assigned(FProducer) then Inc(LNotifications, FProducer.Count); + if Assigned(FMain.MockWallet) then + FStatus.Caption := Format('Notifications %d | admitted A/O %d/%d | page fetches A/O %d/%d | account page / balance reads %d/%d | operation raw reads %d', + [LNotifications, FMain.MockWallet.AccountRefreshCount, FMain.MockWallet.OperationRefreshCount, A.FetchCount, O.FetchCount, + A.PageAccountReadCount, A.BalanceAccountReadCount, O.RawReadCount]); + if FWorkActive then + FExpectation.Caption := 'Actual node synchronization owns both Please Wait / Synchronizing cards and pauses queries.' + else + FExpectation.Caption := Format('Revision %d, rows %d | Waiting A/O %s/%s | gate %s | delay %d ms', + [FMain.Model.Revision, FMain.Model.RowCount, BoolToStr(FMain.MockWallet.AccountsGrid.RefreshThrottle.Waiting, true), + BoolToStr(FMain.MockWallet.OperationsGrid.RefreshThrottle.Waiting, true), + BoolToStr(FMain.Model.GateFetches, true), FMain.Model.DelayMilliseconds]); +end; + +procedure TGridRefreshHarness.CheckNodeLoading(const ACheckpoint: String); +var LSync: Boolean; +begin + LSync := TUserInterface.Node.GetSynchronizationStatus.State = nssSynchronizing; + Require((FMain.MockWallet.AccountsGrid.RefreshSuspended = LSync) and + (FMain.MockWallet.OperationsGrid.RefreshSuspended = LSync), + ACheckpoint + ': actual grid suspension differs from node synchronization'); + Require((FMain.MockWallet.AccountsGrid.LoadingVisible = LSync) and + (FMain.MockWallet.OperationsGrid.LoadingVisible = LSync), + ACheckpoint + ': actual manual cards differ from node synchronization'); +end; + +procedure TGridRefreshHarness.CaptureWaitingDemo; +{$IFNDEF FPC} +var LScope: TDisposables; LBitmap: TBitmap; LPNG: TPngImage; LPath: String; +{$ENDIF} +begin + if FWaitingImageSaved then Exit; + FWaitingImageSaved := true; + {$IFNDEF FPC} + LBitmap := LScope.AddObject(FMain.GetFormImage) as TBitmap; + LBitmap.PixelFormat := pf24bit; + LPNG := LScope.AddObject(TPngImage.Create) as TPngImage; + LPNG.Assign(LBitmap); + LPath := IncludeTrailingPathDelimiter(ExtractFilePath(FLogPath)) + 'grid-refresh-waiting.png'; + LPNG.SaveToFile(LPath); + Log('SCREENSHOT actual native synchronization pause: ' + LPath); + {$ENDIF} +end; + +procedure TGridRefreshHarness.AutomaticTick; +var + LNow: UInt64; + A, O: TMockFetchMetrics; + LMarker: String; +begin + LNow := GetTickCount64; + if FAutomatic then + Require(LNow - FStarted < 117000, 'Automatic rendered-wallet deadline exceeded'); + case FPhase of + apStartup: + begin + A := FMain.Model.Metrics(mdkAccounts); + O := FMain.Model.Metrics(mdkOperations); + if (FLastAccounts <> A.FetchCount) or (FLastOperations <> O.FetchCount) then + begin + FLastAccounts := A.FetchCount; + FLastOperations := O.FetchCount; + FLastChange := LNow; + end; + if not GridsIdle or (LNow - FLastChange < 1500) then + Exit; + if SameText(GetEnvironmentVariable('PASCAL_WALLET_KEY_NOTIFICATIONS_ONLY'), '1') then + begin + CheckWalletKeyNotifications(FMain, ChangeFileExt(FLogPath, '.key-notifications.log')); + LMarker := 'PASS actual wallet and explorer background key notification checks'; + end + else if SameText(GetEnvironmentVariable('PASCAL_WALLET_CONSECUTIVE_SELECTION_ONLY'), '1') then + begin + CheckConsecutiveNativeSelections(FMain, ChangeFileExt(FLogPath, '.consecutive-selection.log')); + LMarker := 'PASS actual wallet 20 consecutive native account selection checks'; + end + else if SameText(GetEnvironmentVariable('PASCAL_WALLET_REFRESH_LIFECYCLE_ONLY'), '1') then + begin + CheckWalletRefreshLifecycle(FMain, ChangeFileExt(FLogPath, '.refresh-lifecycle.log')); + LMarker := 'PASS actual wallet node lifecycle and 250 ms operations refresh checks'; + end + else if SameText(GetEnvironmentVariable('PASCAL_WALLET_SELECTION_COMPLETION_ONLY'), '1') then + begin + CheckWalletSelectionCompletion(FMain, ChangeFileExt(FLogPath, '.selection-completion.log')); + LMarker := 'PASS actual wallet selected-account completion loading checks'; + end + else if SameText(GetEnvironmentVariable('PASCAL_WALLET_SELECTION_ONLY'), '1') then + begin + CheckWalletSelectionLoadingAndBanner(FMain, ChangeFileExt(FLogPath, '.selection.log')); + LMarker := 'PASS actual wallet selection/loading and banner checks'; + end + else + begin + CheckWalletKeyNotifications(FMain, ChangeFileExt(FLogPath, '.key-notifications.log')); + CheckWalletSelectionCompletion(FMain, ChangeFileExt(FLogPath, '.selection-completion.log')); + CheckConsecutiveNativeSelections(FMain, ChangeFileExt(FLogPath, '.consecutive-selection.log')); + CheckWalletRefreshLifecycle(FMain, ChangeFileExt(FLogPath, '.refresh-lifecycle.log')); + LMarker := 'PASS actual wallet 250 ms operations admission, background coalescing and synchronization lifecycle checks'; + end; + FPassed := True; + AutomaticPassed := True; + FPhase := apFinished; + FTimer.Enabled := False; + Log(LMarker); + DrainLog; + PostMessage(FMain.Handle, CM_PC_Terminate, 0, 0); + end; + apGapHold: + begin + if LNow - FHoldStarted >= 250 then + begin + CheckNodeLoading('Interactive active node work'); + CaptureWaitingDemo; + end; + if LNow - FHoldStarted >= 6500 then + begin + FFinalRevision := FMain.Model.Revision; + EndWork; + FStopTick := LNow; + FPhase := apGapResume; + Log('BUSY BATCH finished: the actual node lifecycle now resumes current local data'); + end; + end; + apGapResume: + begin + Require(LNow - FStopTick < 15000, 'Interactive resumed provider did not finish'); + if FMain.MockWallet.AccountsGrid.RefreshSuspended or + FMain.MockWallet.OperationsGrid.RefreshSuspended or not GridsIdle then + Exit; + CheckNodeLoading('Interactive resumed data'); + if FMain.Model.RowCount > 0 then + begin + Require((FMain.MockWallet.AccountsGrid.RowCount > 0) and + (FMain.MockWallet.OperationsGrid.RowCount > 0), 'Interactive resume lost current data'); + Require(Pos('mock revision=' + IntToStr(FFinalRevision) + ' ', + String(FMain.MockWallet.AccountsGrid.Rows[0].Name)) = 1, + 'Interactive account resume retained an old revision'); + Require(Pos('mock revision=' + IntToStr(FFinalRevision) + ' ', + String(FMain.MockWallet.OperationsGrid.Rows[0].Description)) = 1, + 'Interactive history resume retained an old revision'); + end; + FManualGap := False; + FPhase := apStartup; + Log('PASS interactive node lifecycle: queries paused during work and resumed without a quiet timer'); + end; + apFinished: ; + end; +end; + +procedure TGridRefreshHarness.Tick(Sender: TObject); +var LExpectedSource: IDataSource; LToolsChecks: String; +begin + if FInTick then Exit; + FInTick := true; + try + try + if not FWired then begin + if TUserInterface.State = uisLoading then Exit; + Require(TUserInterface.State <> uisError, 'Fresh offline startup failed'); + FMain.Mode := wmWallet; + if not Assigned(FMain.MockWallet) then Exit; + LExpectedSource := FMain.MockWallet.AccountsSource; + Require(FMain.MockWallet.AccountsGrid.DataSource = LExpectedSource, + 'Accounts grid bypassed the factory-created private wallet source'); + LExpectedSource := FMain.MockWallet.OperationsSource; + Require(FMain.MockWallet.OperationsGrid.DataSource = LExpectedSource, + 'Operations grid bypassed the factory-created private wallet source'); + FMain.SetBounds(FMain.Left, FMain.Top, 1220, 940); + Require(FMain.Height >= 850, 'Startup height lock was not released before laying out the mock demo'); + FWired := true; FLastChange := GetTickCount64; + Log('READY actual root wallet and its two typed private providers; monetary mock actions disabled'); + end; + if FAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_SAFEBOX_LOG_ONLY'),'1') then begin + CheckSafeboxProgressLogging(FMain,ChangeFileExt(FLogPath,'.safebox-info.log')); + FPassed := True; + AutomaticPassed := True; + FPhase := apFinished; + FTimer.Enabled := False; + Log('PASS actual safebox progress Info logging checks'); + DrainLog; + PostMessage(FMain.Handle,CM_PC_Terminate,0,0); + Exit; + end; + if FAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_SYNC_PAINT_ONLY'),'1') then begin + CheckSynchronizationPainting(FMain,ChangeFileExt(FLogPath,'.sync-paint.log')); + FPassed := True; + AutomaticPassed := True; + FPhase := apFinished; + FTimer.Enabled := False; + Log('PASS actual synchronization event/paint checks'); + DrainLog; + PostMessage(FMain.Handle,CM_PC_Terminate,0,0); + Exit; + end; + if FAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_TOOLS_ONLY'),'1') then begin + LToolsChecks := CheckWalletToolsDialogs(FMain,ChangeFileExt(FLogPath,'.tools.log')); + FPassed := True; + AutomaticPassed := True; + FPhase := apFinished; + FTimer.Enabled := False; + Log('PASS '+LToolsChecks); + DrainLog; + PostMessage(FMain.Handle,CM_PC_Terminate,0,0); + Exit; + end; + if FAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_CHANGE_INFO_APPROVAL_ONLY'),'1') then begin + CheckChangeAccountInfoApproval(FMain,ChangeFileExt(FLogPath,'.change-info-approval.log')); + FPassed := True; + AutomaticPassed := True; + FPhase := apFinished; + FTimer.Enabled := False; + Log('PASS native Change Info approval: six cases, explicit skipped-target consent and optional payer'); + DrainLog; + PostMessage(FMain.Handle,CM_PC_Terminate,0,0); + Exit; + end; + if FAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_CHANGE_INFO_UI_ONLY'),'1') then begin + CheckChangeAccountInfoPresentation(FMain,ChangeFileExt(FLogPath,'.change-info-ui.log')); + FPassed := True; + AutomaticPassed := True; + FPhase := apFinished; + FTimer.Enabled := False; + Log('PASS native Change Account Info presentation: single/batch Selected and Details at 100/150 percent'); + DrainLog; + PostMessage(FMain.Handle,CM_PC_Terminate,0,0); + Exit; + end; + if FAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_SEND_UI_ONLY'),'1') then begin + CheckSendPascPresentation(FMain,ChangeFileExt(FLogPath,'.send-ui.log')); + FPassed := True; + AutomaticPassed := True; + FPhase := apFinished; + FTimer.Enabled := False; + Log('PASS native Send PASC presentation: six routes, all reachable screens at 100/150 percent'); + DrainLog; + PostMessage(FMain.Handle,CM_PC_Terminate,0,0); + Exit; + end; + if FAutomatic or FManualGap then AutomaticTick; + UpdateCounters; + DrainLog; + except + on E: Exception do begin + StopProducer; EndWork; FMain.Model.ReleaseFetches; + Log('FAIL ' + E.ClassName + ': ' + E.Message); + DrainLog; + FTimer.Enabled := false; + if FAutomatic then PostMessage(FMain.Handle, CM_PC_Terminate, 0, 0) + else FExpectation.Caption := 'FAILED: ' + E.Message; + end; + end; + finally FInTick := false; end; +end; + +function DesktopName(AHandle: THandle): String; +var LNeeded: DWORD; LBuffer: array[0..255] of Char; +begin + if not GetUserObjectInformation(AHandle, UOI_NAME, @LBuffer[0], SizeOf(LBuffer), LNeeded) then + RaiseLastOSError; + Result := StrPas(PChar(@LBuffer[0])); +end; + +procedure RequirePrivateDesktop; +var LInput: HDESK; +begin + LInput := OpenInputDesktop(0, false, DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + if SameText(DesktopName(GetThreadDesktop(GetCurrentThreadId)), DesktopName(LInput)) then + raise Exception.Create('Automatic UI checks require the existing private-desktop test broker'); + finally CloseDesktop(LInput); end; +end; + +function RunWalletGridRefreshDemo: Integer; +var + LMain: TMockMainForm; + LOptions: TWalletStartupOptions; + LArguments: array of String; + LAutomatic, LToolsOnly: Boolean; + LDirectory, LLogPath, LError: String; + LID: TGUID; +begin + Result := 1; + LAutomatic := ParamCount <> 0; + LToolsOnly := LAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_TOOLS_ONLY'),'1'); + if ParamCount > 1 then raise EArgumentException.Create('Use no arguments for the interactive demo, or one automatic log filename'); + if LAutomatic then RequirePrivateDesktop; + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'PascalCoinWalletQA_GridRefresh_' + StringReplace(StringReplace(GUIDToString(LID), '{', '', []), '}', '', []); + if DirectoryExists(LDirectory) then raise Exception.Create('Refusing an existing mock wallet directory'); + ForceDirectories(LDirectory); + if not LAutomatic or (ParamStr(1) = '--auto') then LLogPath := LDirectory + PathDelim + 'grid-refresh.log' + else if Pos('--auto=', ParamStr(1)) = 1 then LLogPath := ExpandFileName(Copy(ParamStr(1), 8, MaxInt)) + else if Pos('--', ParamStr(1)) = 1 then raise EArgumentException.Create('Unsupported demo argument') + else LLogPath := ExpandFileName(ParamStr(1)); + if FileExists(LLogPath) then raise Exception.Create('Refusing an existing test log'); + if not DirectoryExists(ExtractFilePath(LLogPath)) then raise Exception.Create('The test output directory must exist'); + if LAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_OPERATION_MESSAGE_ONLY'), '1') then + begin + // Message-only checks enter before wallet startup, mock providers or a node. + Application.Initialize; + Application.MainFormOnTaskBar := True; + Application.Title := 'Pascal Coin Wallet, JSON-RPC Miner & Explorer'; + try + CheckWalletOperationMessages(nil, LLogPath); + Result := 0; + except + Result := 1; + end; + Exit; + end; + if LAutomatic and SameText(GetEnvironmentVariable('PASCAL_WALLET_ADVANCED_UI_ONLY'), '1') then + begin + // Advanced presentation also runs without starting a wallet or node. + Application.Initialize; + Application.MainFormOnTaskBar := True; + Application.Title := 'Pascal Coin Wallet, JSON-RPC Miner & Explorer'; + try + CheckAdvancedOperationPresentation(LLogPath); + Result := 0; + except + Result := 1; + end; + Exit; + end; + if LToolsOnly then begin + SeedWalletToolsSettings(LDirectory); + SetLength(LArguments, 2); + LArguments[1] := '--no-startup-dialogs'; + end else begin + SetLength(LArguments, 3); + LArguments[1] := '--offline'; + LArguments[2] := '--no-startup-dialogs'; + end; + LArguments[0] := '--data-dir=' + LDirectory; + if not TryParseWalletStartupOptions(LArguments, LOptions, LError) then raise Exception.Create(LError); + TUserInterface.ConfigureStartup(LOptions); + Application.Initialize; + Application.MainFormOnTaskBar := true; + Application.CreateForm(TMockMainForm, LMain); + TGridRefreshHarness.Create(LMain, LAutomatic, LLogPath); + try + TUserInterface.StartApplication(LMain); + if TUserInterface.Started then Application.Run; + if not LAutomatic or AutomaticPassed then Result := 0; + finally TUserInterface.ExitApplication; end; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletKeyNotificationChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletKeyNotificationChecks.pas new file mode 100644 index 000000000..e79eb5ffd --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletKeyNotificationChecks.pas @@ -0,0 +1,150 @@ +unit UWalletKeyNotificationChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Exercises the actual wallet key notification and native receiver lifetime. + No key, wallet file, account balance or transaction is changed. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UWalletGridMocks; + +procedure CheckWalletKeyNotifications(AMain: TMockMainForm; const ALogPath: String); + +implementation + +uses Windows, Classes, SysUtils, SyncObjs, Forms, UWallet, UCommon, UCoreObjects, + UFRMAccountExplorer, UCellRenderers; + +type + TWalletKeyProducer = class(TThread) + private + FKeys: TWalletKeys; + protected + procedure Execute; override; + public + Completed: TEvent; + ErrorText: String; + constructor Create(AKeys: TWalletKeys); + destructor Destroy; override; + end; + +constructor TWalletKeyProducer.Create(AKeys: TWalletKeys); +begin + inherited Create(True); + FKeys := AKeys; + Completed := TEvent.Create(nil, True, False, ''); +end; + +destructor TWalletKeyProducer.Destroy; +begin + inherited Destroy; + Completed.Free; +end; + +procedure TWalletKeyProducer.Execute; +begin + try + try + FKeys.OnChanged.Invoke(FKeys); + except + on E: Exception do + ErrorText := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +procedure Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then + raise Exception.Create(AMessage); +end; + +procedure Pump; +begin + CheckSynchronize(0); + Application.ProcessMessages; + Sleep(1); +end; + +procedure NotifyFromWorker; +var + LWorker: TWalletKeyProducer; +begin + LWorker := TWalletKeyProducer.Create(TWallet.Keys); + try + LWorker.Start; + // Do not pump: a synchronous UI wait or direct control update must be + // observable before any posted notification is permitted to run. + Require(LWorker.Completed.WaitFor(1000) = wrSignaled, + 'Wallet key notification blocked its background producer'); + Require(LWorker.ErrorText = '', 'Wallet key producer failed: ' + LWorker.ErrorText); + finally + LWorker.WaitFor; + LWorker.Free; + end; +end; + +procedure CheckWalletKeyNotifications(AMain: TMockMainForm; const ALogPath: String); +const + CT_Sentinel = 'key-notification must wait for the UI'; +var + LExplorer: TFRMAccountExplorer; + LRetiringWallet: TMockWallet; + LUntil: UInt64; + LLog: TStringList; +begin + Require(TCellRenderers.OperationShortHash('12345678') = '12345678', + 'An eight-character operation hash must remain unchanged'); + Require(TCellRenderers.OperationShortHash('123456789ABC') = '1234...9ABC', + 'Short operation hashes must retain their actual final four characters'); + LExplorer := TFRMAccountExplorer.Create(nil); + LRetiringWallet := nil; + LLog := TStringList.Create; + try + LExplorer.Show; + AMain.MockWallet.HandleNeeded; + LExplorer.HandleNeeded; + AMain.MockWallet.cbAccounts.Items.Add(CT_Sentinel); + LExplorer.cbMyPrivateKeys.Items.Add(CT_Sentinel); + NotifyFromWorker; + Require(AMain.MockWallet.cbAccounts.Items.IndexOf(CT_Sentinel) >= 0, + 'Background wallet notification mutated the main key combo before UI delivery'); + Require(LExplorer.cbMyPrivateKeys.Items.IndexOf(CT_Sentinel) >= 0, + 'Background wallet notification mutated the explorer key combo before UI delivery'); + LUntil := GetTickCount64 + 3000; + repeat + Pump; + until ((AMain.MockWallet.cbAccounts.Items.IndexOf(CT_Sentinel) < 0) and + (LExplorer.cbMyPrivateKeys.Items.IndexOf(CT_Sentinel) < 0)) or + (GetTickCount64 >= LUntil); + Require(AMain.MockWallet.cbAccounts.Items.IndexOf(CT_Sentinel) < 0, + 'Main wallet did not refresh on its posted key notification'); + Require(LExplorer.cbMyPrivateKeys.Items.IndexOf(CT_Sentinel) < 0, + 'Accounts Explorer did not refresh on its posted key notification'); + LLog.Add('PASS background key notification returns without UI pumping; both real key combos refresh only on UI delivery'); + + // Queue another delivery, then destroy both types of receiver without + // pumping. The native window lifetime must discard those pending messages. + LRetiringWallet := TMockWallet.CreateForModel(nil, AMain.Model); + LRetiringWallet.HandleNeeded; + NotifyFromWorker; + FreeAndNil(LExplorer); + FreeAndNil(LRetiringWallet); + LUntil := GetTickCount64 + 200; + repeat + Pump; + until GetTickCount64 >= LUntil; + LLog.Add('PASS pending background key messages are safe when wallet and explorer receivers are destroyed before delivery'); + LLog.SaveToFile(ALogPath); + finally + LRetiringWallet.Free; + LExplorer.Free; + LLog.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletNetworkOffChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletNetworkOffChecks.pas new file mode 100644 index 000000000..bb529200f --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletNetworkOffChecks.pas @@ -0,0 +1,498 @@ +unit UWalletNetworkOffChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Exercises the real connectivity action and wallet refresh lifecycle with + controlled data. Only loopback discovery is permitted in this fresh wallet. + Distributed under the MIT software license; see the accompanying LICENSE. +} + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunWalletNetworkOffProbe: Integer; + +implementation + +uses + Windows, Classes, SysUtils, Forms, Generics.Collections, Variants, + UWalletGridMocks, UUserInterface, UWalletStartup, UNode, UNetProtocol, + UAppParams, USettings, UMemory, UCommon, UCommon.UI, UVisualGrid; + +type + TGridGeometryAccess = class(TVisualGrid) + public + function HasPendingLayout: Boolean; + end; + + TNetworkOffChecks = class + private + FMain: TMockMainForm; + FModel: TMockWalletData; + FLog: TStringList; + FLogPath, FAsyncError: String; + FPreviousException: TExceptionEvent; + FWorkActive: Boolean; + procedure Log(const AText: String); + procedure OnException(Sender: TObject; E: Exception); + procedure Pump(AMilliseconds: Cardinal); + procedure WaitForFetches; + procedure Settle; + procedure WaitForRefresh(AAccounts, AOperations: Integer; ATimeout: Cardinal = 8000); + function Grid(AKind: TMockDataKind): TVisualGrid; + procedure CheckGridState(AKind: TMockDataKind); + procedure CheckNodePresentation; + procedure CheckRows(ARevision: Integer); + procedure CheckConnections; + function Notify(ARevision: Integer): UInt64; + function Burst(AFirstRevision: Integer): UInt64; + procedure ClickConnectivity(AExpectedActive: Boolean); + procedure BusyPagesThenNetworkOff; + procedure ActiveWorkThenNetworkOff; + public + constructor Create(const ALogPath: String); + destructor Destroy; override; + procedure Run; + end; + +function TGridGeometryAccess.HasPendingLayout: Boolean; +begin + Result := Assigned(FDelayedBoundsChangeTimer) and FDelayedBoundsChangeTimer.Enabled; +end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +function DesktopName(AHandle: THandle): String; +var LBuffer: array[0..255] of Char; LRequired: DWORD; +begin + FillChar(LBuffer, SizeOf(LBuffer), 0); + if not GetUserObjectInformation(AHandle, UOI_NAME, @LBuffer[0], SizeOf(LBuffer), LRequired) then + RaiseLastOSError; + Result := StrPas(PChar(@LBuffer[0])); +end; + +procedure RequirePrivateDesktop; +var LInput: THandle; LName: String; +begin + LInput := OpenInputDesktop(0, false, DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + LName := DesktopName(GetThreadDesktop(GetCurrentThreadId)); + Require((Pos('PascalCoin', LName) = 1) and not SameText(LName, DesktopName(LInput)), + 'Run this probe through the existing private-desktop broker'); + finally CloseDesktop(LInput); end; +end; + +procedure SeedLoopbackSettings(const ADirectory: String); +var LBag: TDisposables; LParams: TAppParams; +begin + LParams := LBag.AddObject(TAppParams.Create(nil)) as TAppParams; + LParams.FileName := IncludeTrailingPathDelimiter(ADirectory) + 'AppParams.prm'; + LParams.ParamByName[CT_PARAM_TryToConnectOnlyWithThisFixedServers].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_PeerCache].SetAsString(''); + LParams.ParamByName[CT_PARAM_InternetServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_JSONRPCEnabled].SetAsBoolean(false); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerActive].SetAsBoolean(false); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_MinerPrivateKeyType].SetAsInteger(Ord(mpk_Random)); + LParams.ParamByName[CT_PARAM_ShowLogs].SetAsBoolean(false); + LParams.ParamByName[CT_PARAM_SaveLogFiles].SetAsBoolean(false); +end; + +constructor TNetworkOffChecks.Create(const ALogPath: String); +begin + inherited Create; + FLogPath := ALogPath; + FLog := TStringList.Create; + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TNetworkOffChecks.Destroy; +begin + try + if FWorkActive then begin + TUserInterface.Node.EndBlockChainUpdate; + FWorkActive := false; + end; + if Assigned(FModel) then FModel.ReleaseFetches; + if Assigned(FMain) then begin + TUserInterface.ExitApplication; + FMain := nil; + FModel := nil; + end; + finally + Application.OnException := FPreviousException; + FLog.Free; + inherited; + end; +end; + +procedure TNetworkOffChecks.Log(const AText: String); +begin + if Assigned(FModel) then FModel.DrainEvents(FLog); + FLog.Add(UIntToStr(GetTickCount64) + ' ' + AText); + FLog.SaveToFile(FLogPath); +end; + +procedure TNetworkOffChecks.OnException(Sender: TObject; E: Exception); +begin + if FAsyncError <> '' then Exit; + FAsyncError := E.ClassName + ': ' + E.Message; + Log('ASYNC ' + FAsyncError); +end; + +procedure TNetworkOffChecks.Pump(AMilliseconds: Cardinal); +var LDeadline: UInt64; +begin + LDeadline := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + if FAsyncError <> '' then raise Exception.Create(FAsyncError); + Sleep(1); + until GetTickCount64 >= LDeadline; +end; + +procedure TNetworkOffChecks.WaitForFetches; +var LDeadline: UInt64; +begin + LDeadline := GetTickCount64 + 3000; + while ((FModel.Metrics(mdkAccounts).ActiveFetches <> 1) or + (FModel.Metrics(mdkOperations).ActiveFetches <> 1)) and + (GetTickCount64 < LDeadline) do Pump(10); + Require((FModel.Metrics(mdkAccounts).ActiveFetches = 1) and + (FModel.Metrics(mdkOperations).ActiveFetches = 1), 'Both real grid workers must enter their fetch gates'); +end; + +procedure TNetworkOffChecks.Settle; +var + LDeadline, LStable: UInt64; + LLastAccounts, LLastOperations, LLastAccountPageSize, LLastOperationPageSize: Integer; +begin + LDeadline := GetTickCount64 + 8000; + LStable := GetTickCount64; + LLastAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LLastOperations := FModel.Metrics(mdkOperations).FetchCount; + LLastAccountPageSize := Grid(mdkAccounts).PageSize; + LLastOperationPageSize := Grid(mdkOperations).PageSize; + repeat + Pump(10); + if Grid(mdkAccounts).IsUpdating or Grid(mdkOperations).IsUpdating or + Grid(mdkAccounts).RefreshThrottle.Waiting or Grid(mdkOperations).RefreshThrottle.Waiting or + (FModel.Metrics(mdkAccounts).ActiveFetches <> 0) or + (FModel.Metrics(mdkOperations).ActiveFetches <> 0) or + (FModel.Metrics(mdkAccounts).FetchCount <> LLastAccounts) or + (FModel.Metrics(mdkOperations).FetchCount <> LLastOperations) or + TGridGeometryAccess(Grid(mdkAccounts)).HasPendingLayout or + TGridGeometryAccess(Grid(mdkOperations)).HasPendingLayout or + (Grid(mdkAccounts).PageSize <> LLastAccountPageSize) or + (Grid(mdkOperations).PageSize <> LLastOperationPageSize) then + LStable := GetTickCount64; + LLastAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LLastOperations := FModel.Metrics(mdkOperations).FetchCount; + LLastAccountPageSize := Grid(mdkAccounts).PageSize; + LLastOperationPageSize := Grid(mdkOperations).PageSize; + if GetTickCount64 - LStable >= 400 then Exit; + until GetTickCount64 >= LDeadline; + raise Exception.Create('The real wallet grid workers and populated geometry did not settle'); +end; + +procedure TNetworkOffChecks.WaitForRefresh(AAccounts, AOperations: Integer; ATimeout: Cardinal); +var LDeadline: UInt64; +begin + LDeadline := GetTickCount64 + ATimeout; + while ((FMain.MockWallet.AccountRefreshCount < AAccounts) or + (FMain.MockWallet.OperationRefreshCount < AOperations)) and + (GetTickCount64 < LDeadline) do Pump(10); + Require((FMain.MockWallet.AccountRefreshCount = AAccounts) and + (FMain.MockWallet.OperationRefreshCount = AOperations), + Format('Expected per-grid admissions %d/%d, observed %d/%d', + [AAccounts, AOperations, FMain.MockWallet.AccountRefreshCount, + FMain.MockWallet.OperationRefreshCount])); +end; + +function TNetworkOffChecks.Grid(AKind: TMockDataKind): TVisualGrid; +begin + if AKind = mdkAccounts then Result := FMain.MockWallet.AccountsGrid + else Result := FMain.MockWallet.OperationsGrid; +end; + +procedure TNetworkOffChecks.CheckGridState(AKind: TMockDataKind); +begin + if AKind=mdkAccounts then begin + Require(Grid(AKind).RefreshThrottle.Mode=temNone,'The accounts grid must admit immediately'); + Require(not Grid(AKind).RefreshThrottle.Waiting,'Accounts must not enter a quiet-period wait'); + end else + Require((Grid(AKind).RefreshThrottle.Mode=temNotifyOnEventBurstFinished) and + (Grid(AKind).RefreshThrottle.Interval.TotalMilliseconds=250), + 'The operations grid must use its existing 250 ms trailing quiet interval'); + Require(Grid(AKind).LoadingVisible = + (TUserInterface.SynchronizationStatus.State=nssSynchronizing), + 'The manual card must follow actual node synchronization'); + Require(Grid(AKind).RefreshSuspended = + (TUserInterface.SynchronizationStatus.State=nssSynchronizing), + 'Query suspension must follow actual node synchronization'); +end; + +procedure TNetworkOffChecks.CheckNodePresentation; +begin + CheckGridState(mdkAccounts); + CheckGridState(mdkOperations); +end; + +procedure TNetworkOffChecks.CheckRows(ARevision: Integer); +var LRow: Variant; LPrefix: String; +begin + LPrefix := 'mock revision=' + IntToStr(ARevision) + ' '; + Require((FMain.MockWallet.AccountsGrid.RowCount > 0) and + (FMain.MockWallet.OperationsGrid.RowCount > 0), 'Both real grids must publish rows'); + LRow := FMain.MockWallet.AccountsGrid.Rows[0]; + Require(Pos(LPrefix, String(LRow.Name)) = 1, 'Accounts did not publish the latest revision'); + LRow := FMain.MockWallet.OperationsGrid.Rows[0]; + Require(Pos(LPrefix, String(LRow.Description)) = 1, 'Operations did not publish the latest revision'); +end; + +procedure TNetworkOffChecks.CheckConnections; +var LConnections: TList<TNetConnection>; LConnection: TNetConnection; LAddress: String; +begin + Require(TSettings.TryConnectOnlyWithThisFixedServers = '127.0.0.1:1', 'Discovery must remain restricted to the fixed loopback peer'); + Require(not TSettings.JsonRpcPortEnabled and not TSettings.JsonRpcMinerServerActive, + 'RPC and mining must remain disabled'); + LConnections := TNetData.NetData.NetConnections.LockList; + try + for LConnection in LConnections do begin + LAddress := LConnection.ClientRemoteAddr; + Require((LAddress = '') or (LAddress = 'NIL') or (LAddress = '127.0.0.1') or + (Pos('127.0.0.1:', LAddress) = 1), + 'A connection escaped loopback: ' + LAddress); + Require(not LConnection.Connected, 'The unavailable loopback peer must not establish a connection'); + end; + finally TNetData.NetData.NetConnections.UnlockList; end; + Require(TNetData.NetData.NetStatistics.ActiveConnections = 0, 'This probe must have no connected peers'); +end; + +function TNetworkOffChecks.Notify(ARevision: Integer): UInt64; +begin + FModel.SetData(ARevision, 200); + Result := GetTickCount64; + TUserInterface.Node.NotifyBlocksChanged; +end; + +function TNetworkOffChecks.Burst(AFirstRevision: Integer): UInt64; +var I: Integer; +begin + Result := 0; + for I := 0 to 6 do begin + Result := Notify(AFirstRevision + I); + Pump(50); + end; +end; + +procedure TNetworkOffChecks.ClickConnectivity(AExpectedActive: Boolean); +var LStarted, LReturned: UInt64; +begin + Require(not TUserInterface.Offline, 'The actual connectivity handler must not take its offline early return'); + Require(TNetData.NetData.NetConnectionsActive <> AExpectedActive, 'Connectivity must actually change on this click'); + LStarted := GetTickCount64; + FMain.tbtnConnectivityClick(FMain.tbtnConnectivity); + LReturned := GetTickCount64; + Require(LReturned - LStarted < 400, 'The actual toolbar action blocked the main thread'); + // The controller publishes intent to its owned worker. Wait for the actual + // core transition, without repeatedly invoking any notification or handler. + while (TNetData.NetData.NetConnectionsActive <> AExpectedActive) and + (GetTickCount64 - LStarted < 3000) do Pump(10); + Require(TNetData.NetData.NetConnectionsActive = AExpectedActive, 'The actual toolbar action did not toggle networking'); + Log('ACTUAL connectivity action active=' + BoolToStr(AExpectedActive, true) + + ' handler_ms=' + UIntToStr(LReturned - LStarted)); +end; + +procedure TNetworkOffChecks.BusyPagesThenNetworkOff; +var + LLastInput, LIndependentInput: UInt64; + LAccounts, LOperations, LAccountAdmissions, LOperationAdmissions: Integer; +begin + // Initial result totals and native scrollbars can change page capacity. + // Settle that observed layout before isolating backend coalescing counts. + Settle; + CheckNodePresentation; + CheckRows(FModel.Revision); + FModel.GateFetches := true; + Notify(900); + WaitForFetches; + CheckNodePresentation; + LAccountAdmissions := FMain.MockWallet.AccountRefreshCount; + LOperationAdmissions := FMain.MockWallet.OperationRefreshCount; + LAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LOperations := FModel.Metrics(mdkOperations).FetchCount; + LLastInput := Burst(1000); + CheckNodePresentation; + Require(FMain.MockWallet.AccountRefreshCount>LAccountAdmissions, + 'Busy accounts must record each delivered backend input immediately'); + Require((FMain.MockWallet.OperationRefreshCount=LOperationAdmissions) and + Grid(mdkOperations).RefreshThrottle.Waiting, + 'Repeated backend input must restart operations silence instead of firing during the burst'); + ClickConnectivity(false); + Pump(250); + Require((FModel.Metrics(mdkAccounts).FetchCount = LAccounts) and + (FModel.Metrics(mdkOperations).FetchCount = LOperations), + 'New inputs cannot start parallel queries while initial workers are held'); + FModel.ReleaseFetches; + Settle; + Require((FModel.Metrics(mdkAccounts).FetchCount = LAccounts + 1) and + (FModel.Metrics(mdkOperations).FetchCount = LOperations + 1), + 'Worker retirement must issue one latest coalesced page per grid'); + Require(GetTickCount64 - LLastInput < 2500, + 'Busy-worker retirement added an obsolete multi-second delay to the latest pages'); + CheckNodePresentation; + CheckRows(1006); + CheckConnections; + + LAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LOperations := FModel.Metrics(mdkOperations).FetchCount; + LAccountAdmissions := FMain.MockWallet.AccountRefreshCount; + LOperationAdmissions := FMain.MockWallet.OperationRefreshCount; + LIndependentInput := Notify(1500); + WaitForRefresh(LAccountAdmissions + 1, LOperationAdmissions + 1, 1800); + Require((FMain.MockWallet.LastAccountRefreshTick - LIndependentInput < 1800) and + (FMain.MockWallet.LastOperationRefreshTick - LIndependentInput < 1800), + 'An independent input was delayed after network-off'); + Settle; + CheckRows(1500); + Pump(250); + Require((FModel.Metrics(mdkAccounts).FetchCount = LAccounts + 1) and + (FModel.Metrics(mdkOperations).FetchCount = LOperations + 1), + 'A completed independent input must not manufacture another page query'); + CheckNodePresentation; + Log('PASS actual network-off: immediate accounts, 250 ms quiet operations, busy-worker coalescing and independent inputs'); +end; + +procedure TNetworkOffChecks.ActiveWorkThenNetworkOff; +var + LEndTick, LDeadline: UInt64; + LAccountAdmissions, LOperationAdmissions, LAccounts, LOperations: Integer; +begin + ClickConnectivity(true); + LAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LOperations := FModel.Metrics(mdkOperations).FetchCount; + FModel.GateFetches := true; + TUserInterface.Node.BeginBlockChainUpdate; + FWorkActive := true; + try + LDeadline := GetTickCount64 + 3000; + repeat + Pump(10); + if Grid(mdkAccounts).RefreshSuspended and Grid(mdkOperations).RefreshSuspended then Break; + until GetTickCount64 >= LDeadline; + Require(Grid(mdkAccounts).LoadingVisible and Grid(mdkOperations).LoadingVisible, + 'Physical buffered work must show both synchronization cards'); + Burst(2000); + ClickConnectivity(false); + // Received work outlives transport activity and any former five-second + // presentation timeout. Only the actual node lifecycle may release it. + Pump(5500); + Require(TUserInterface.Node.IsUpdatingBlockChain and + (TUserInterface.SynchronizationStatus.State = nssSynchronizing), + 'Transport off cannot finish actual buffered processing'); + CheckNodePresentation; + Require(not Grid(mdkAccounts).IsUpdating and not Grid(mdkOperations).IsUpdating, + 'Suspended requests must not start page workers'); + Require((FModel.Metrics(mdkAccounts).FetchCount = LAccounts) and + (FModel.Metrics(mdkOperations).FetchCount = LOperations), + 'Silence cannot query while buffered processing remains'); + Require(not Grid(mdkAccounts).InternalDrawGrid.Enabled and + not Grid(mdkOperations).InternalDrawGrid.Enabled, + 'The node-owned synchronization card must block inputs'); + LAccountAdmissions := FMain.MockWallet.AccountRefreshCount; + LOperationAdmissions := FMain.MockWallet.OperationRefreshCount; + finally + LEndTick := GetTickCount64; + TUserInterface.Node.EndBlockChainUpdate; + FWorkActive := false; + end; + WaitForFetches; + WaitForRefresh(LAccountAdmissions+1,LOperationAdmissions+1,1800); + Require(GetTickCount64 - LEndTick < 1800, + 'Backlog completion must resume retained pages without an obsolete multi-second delay'); + CheckNodePresentation; + Require(not Grid(mdkAccounts).LoadingVisible and not Grid(mdkOperations).LoadingVisible, + 'Both manual cards must clear when physical processing finishes'); + Require(Grid(mdkAccounts).IsUpdating and Grid(mdkOperations).IsUpdating, + 'The resumed pages must remain in their controlled fetch gate'); + Require((FModel.Metrics(mdkAccounts).FetchCount = LAccounts + 1) and + (FModel.Metrics(mdkOperations).FetchCount = LOperations + 1), + 'Ending the backlog must execute one latest retained page per grid'); + // EndBlockChainUpdate emits one real BlocksChanged event when pending + // state is available. The notifier delivers it before the final resume. + Require((FMain.MockWallet.AccountRefreshCount = LAccountAdmissions + 1) and + (FMain.MockWallet.OperationRefreshCount = LOperationAdmissions + 1), + 'The real node completion event must add one input per grid before resume'); + FModel.ReleaseFetches; + Settle; + CheckRows(2006); + CheckConnections; + Log('PASS actual network-off preserves synchronization until backlog end, then one latest page resumes'); +end; + +procedure TNetworkOffChecks.Run; +var LDirectory: String; LID: TGUID; LOptions: TWalletStartupOptions; LDeadline: UInt64; +begin + RequirePrivateDesktop; + Require(not TNode.NodeExists and not TNetData.NetDataExists, 'A dedicated process must start without node/network singletons'); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + 'PascalCoinNetworkOff-' + GUIDToString(LID); + Require(not DirectoryExists(LDirectory), 'The test data directory must be new'); + Require(ForceDirectories(LDirectory), 'Cannot create the test data directory'); + SeedLoopbackSettings(LDirectory); + LOptions := Default(TWalletStartupOptions); + LOptions.DataDirectory := LDirectory; + LOptions.Offline := false; + LOptions.NoStartupDialogs := true; + TUserInterface.ConfigureStartup(LOptions); + Application.CreateForm(TMockMainForm, FMain); + FModel := FMain.Model; + FMain.Caption := 'MOCK DATA - loopback-only connectivity regression'; + TUserInterface.StartApplication(FMain); + if not TUserInterface.Started then begin + FMain := nil; FModel := nil; + raise Exception.Create('The actual controller did not start'); + end; + FMain.Show; + LDeadline := GetTickCount64 + 12000; + repeat + Pump(10); + Require(TUserInterface.State <> uisError, 'Fresh loopback-only startup failed'); + if Assigned(FMain.MockWallet) then Break; + until GetTickCount64 >= LDeadline; + Require(Assigned(FMain.MockWallet), 'The actual main wallet was not constructed'); + Require(TSettings.TryConnectOnlyWithThisFixedServers = '127.0.0.1:1', 'The controller did not load the fixed loopback restriction'); + while (not TNetData.NetData.NetConnectionsActive) and + (GetTickCount64 < LDeadline) do Pump(10); + Require(TNetData.NetData.NetConnectionsActive, 'The actual runtime must start with connectivity enabled'); + CheckConnections; + Log('START fresh loopback-only online-policy controller; RPC/mining off; real startup geometry settles before gated backend queries'); + BusyPagesThenNetworkOff; + ActiveWorkThenNetworkOff; + Log('PASS all real network-off regressions'); +end; + +function RunWalletNetworkOffProbe: Integer; +var LBag: TDisposables; LChecks: TNetworkOffChecks; +begin + Result := 1; + if ParamCount <> 1 then Exit; + Application.Initialize; + LChecks := LBag.AddObject(TNetworkOffChecks.Create(ExpandFileName(ParamStr(1)))) as TNetworkOffChecks; + try + LChecks.Run; + Result := 0; + except + on E: Exception do LChecks.Log('FAIL ' + E.ClassName + ': ' + E.Message); + end; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletOperationDetailsChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletOperationDetailsChecks.pas new file mode 100644 index 000000000..4c073859c --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletOperationDetailsChecks.pas @@ -0,0 +1,618 @@ +unit UWalletOperationDetailsChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Exercises the real wallet grid, input handlers and explicit Details action. + Only the modal boundary is recorded; synthetic history is never looked up in + the real node, signed or submitted. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +procedure CheckWalletOperationDetails(const ALogPath: String); + +implementation + +uses + Windows, Messages, Classes, SysUtils, Types, Math, Variants, Generics.Collections, + Forms, Controls, StdCtrls, ExtCtrls, Graphics, Buttons, + {$IFDEF FPC}Menus{$ELSE}Vcl.Menus{$ENDIF}, ActnList, Grids, + {$IFDEF FPC}LMessages,{$ENDIF} UMemory, UVisualGrid, + UCTRLWallet, UWalletGridMocks, UDataSources, UBlockChain, UPCDataTypes, UCrypto, + UVisualGrid.Paging.Checks; + +type + TDetailsGridAccess = class(TVisualGrid) + public + function FooterDescription: String; + end; + + TDetailsOperationsSource = class(TMockAccountsOperationsDataSource) + public + procedure FetchAll(const AContainer: TList<TOperationResume>); override; + end; + + TDetailsWallet = class(TCTRLWallet) + private + FModel: TMockWalletData; + FDetailsCount: Integer; + FLastDetailsHash: String; + protected + function CreateAccountsDataSource: TMyAccountsDataSource; override; + function CreateOperationsDataSource: TAccountsOperationsDataSource; override; + procedure ShowOperationDetails(const AOperationHash: String); override; + public + constructor CreateForModel(AOwner: TComponent; AModel: TMockWalletData); + property AccountsGrid; + property OperationsGrid; + property DetailsCount: Integer read FDetailsCount; + property LastDetailsHash: String read FLastDetailsHash; + end; + + TOperationDetailsChecks = class(TComponent) + private + FHost: TForm; + FWallet: TDetailsWallet; + FModel: TMockWalletData; + FLog: TStringList; + FLogPath, FError: String; + FPreviousException: TExceptionEvent; + FPopupTimer: TTimer; + FPopupWindow: HWND; + FPopupDeadline: UInt64; + FPopupDispatched: Boolean; + procedure Require(AValue: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure OnException(Sender: TObject; E: Exception); + procedure Pump(AMilliseconds: Cardinal); + procedure AwaitRows; + procedure ClickRow(ARow: Integer; ARightButton: Boolean = False); + procedure CheckPassiveInput(const AScale: String); + procedure CheckExplicitDetails(const AScale: String); + procedure CheckUnavailable(const AScale: String); + procedure CheckToolbarBounds(const AScale: String); + procedure PopupTick(Sender: TObject); + function SelectedHash: String; + function DetailsButton: TBitBtn; + function DetailsAction: TAction; + procedure Capture(const ASuffix: String); + public + constructor Create(const ALogPath: String); reintroduce; + destructor Destroy; override; + procedure Run; + end; + +function TDetailsGridAccess.FooterDescription: String; +var + LClient: Windows.TRect; +begin + Windows.GetClientRect(FBottomPanel.Handle, LClient); + Result := Format('footer logical=%d,%d,%d,%d nativeClient=%d,%d navigation=%d,%d,%d,%d mode=%d,%d,%d,%d fontHeight=%d fontDpi=%d', + [FBottomPanel.Left, FBottomPanel.Top, FBottomPanel.Width, FBottomPanel.Height, + LClient.Right - LClient.Left, LClient.Bottom - LClient.Top, + FBottomRightPanel.Left, FBottomRightPanel.Top, FBottomRightPanel.Width, FBottomRightPanel.Height, + FPageModeButton.Left, FPageModeButton.Top, FPageModeButton.Width, FPageModeButton.Height, + FBottomPanel.Font.Height, FBottomPanel.Font.PixelsPerInch]); +end; + +procedure TDetailsOperationsSource.FetchAll(const AContainer: TList<TOperationResume>); +var + LOperation: TOperationResume; + LAccount, LCounter: Cardinal; + I: Integer; +begin + inherited; + // Valid, distinct hash syntax passes through the real typed source adapter. + // It identifies fixture rows only; the modal seam prevents real-node lookup. + for I := 0 to AContainer.Count - 1 do begin + LOperation := AContainer[I]; + LAccount := LOperation.AffectedAccount; + LCounter := 100 + I; + LOperation.n_operation := LCounter; + SetLength(LOperation.OperationHash, 32); + FillChar(LOperation.OperationHash[0], 32, I + 1); + Move(LOperation.Block, LOperation.OperationHash[0], 4); + Move(LAccount, LOperation.OperationHash[4], 4); + Move(LCounter, LOperation.OperationHash[8], 4); + if I = AContainer.Count - 1 then + SetLength(LOperation.OperationHash, 0); + AContainer[I] := LOperation; + end; +end; + +constructor TDetailsWallet.CreateForModel(AOwner: TComponent; AModel: TMockWalletData); +begin + FModel := AModel; + inherited Create(AOwner); +end; + +function TDetailsWallet.CreateAccountsDataSource: TMyAccountsDataSource; +begin + Result := TMockMyAccountsDataSource.CreateForModel(Self, FModel); +end; + +function TDetailsWallet.CreateOperationsDataSource: TAccountsOperationsDataSource; +begin + Result := TDetailsOperationsSource.CreateForModel(Self, FModel); + TDetailsOperationsSource(Result).OperationsPerAccount := 8; +end; + +procedure TDetailsWallet.ShowOperationDetails(const AOperationHash: String); +begin + Inc(FDetailsCount); + FLastDetailsHash := AOperationHash; +end; + +function FindOwnedDetailsPopup(AWindow: HWND; AData: LPARAM): BOOL; stdcall; +const + CT_MN_GETHMENU = $01E1; +var + LChecks: TOperationDetailsChecks; + LClassName: array[0..31] of Char; +begin + Result := True; + LChecks := TOperationDetailsChecks(Pointer(AData)); + if (GetClassName(AWindow, @LClassName[0], Length(LClassName)) <> 0) and + (StrPas(PChar(@LClassName[0])) = '#32768') and + (NativeUInt(SendMessage(AWindow, CT_MN_GETHMENU, 0, 0)) = + NativeUInt(LChecks.FWallet.mnuOperationsPopup.Handle)) then begin + LChecks.FPopupWindow := AWindow; + Result := False; + end; +end; + +constructor TOperationDetailsChecks.Create(const ALogPath: String); +begin + inherited Create(nil); + FLogPath := ALogPath; + FLog := TStringList.Create; + FPreviousException := Application.OnException; + Application.OnException := OnException; + FPopupTimer := TTimer.Create(Self); + FPopupTimer.Enabled := False; + FPopupTimer.Interval := 20; + FPopupTimer.OnTimer := PopupTick; +end; + +destructor TOperationDetailsChecks.Destroy; +begin + FPopupTimer.Enabled := False; + Application.OnException := FPreviousException; + FHost.Free; + FLog.Free; + inherited Destroy; +end; + +procedure TOperationDetailsChecks.Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure TOperationDetailsChecks.Log(const AMessage: String); +begin + FLog.Add(AMessage); + FLog.SaveToFile(ChangeFileExt(FLogPath, '.wallet-operation-details.log')); +end; + +procedure TOperationDetailsChecks.OnException(Sender: TObject; E: Exception); +begin + if FError = '' then FError := E.ClassName + ': ' + E.Message; + EndMenu; +end; + +procedure TOperationDetailsChecks.Pump(AMilliseconds: Cardinal); +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + if FError <> '' then raise Exception.Create(FError); + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TOperationDetailsChecks.AwaitRows; +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + 8000; + repeat + Pump(5); + until (not FWallet.OperationsGrid.IsUpdating and + (FWallet.OperationsGrid.RowCount = 40)) or (GetTickCount64 >= LUntil); + Require(not FWallet.OperationsGrid.IsUpdating and + (FWallet.OperationsGrid.RowCount = 40), 'Details fixture did not publish all forty provider rows'); +end; + +function TOperationDetailsChecks.DetailsButton: TBitBtn; +begin + Require(FWallet.FindComponent('OperationDetailsButton') is TBitBtn, + 'The wallet has no explicit operation Details button'); + Result := TBitBtn(FWallet.FindComponent('OperationDetailsButton')); +end; + +function TOperationDetailsChecks.DetailsAction: TAction; +begin + Require(FWallet.FindComponent('OperationDetailsAction') is TAction, + 'The wallet has no shared operation Details action'); + Result := TAction(FWallet.FindComponent('OperationDetailsAction')); +end; + +function TOperationDetailsChecks.SelectedHash: String; +var + LRows: TArray<Variant>; +begin + LRows := FWallet.OperationsGrid.SelectedRows; + Require(Length(LRows) = 1, 'The native input did not leave exactly one operation selected'); + Result := VarToStr(LRows[0].__KEY); + Require(TPCOperation.IsValidOperationHash(Result), 'Provider did not publish a valid operation hash'); +end; + +procedure TOperationDetailsChecks.ClickRow(ARow: Integer; ARightButton: Boolean); +var + LGrid: TDrawGrid; + LCell: TRect; + LPoint: LPARAM; +begin + LGrid := FWallet.OperationsGrid.InternalDrawGrid; + LGrid.TopRow := ARow + 1; + LCell := LGrid.CellRect(0, ARow + 1); + Require((LCell.Top >= 0) and (LCell.Bottom <= LGrid.ClientHeight), + 'Requested native operation cell is not visible'); + LPoint := LPARAM(Cardinal(((LCell.Left + LCell.Right) div 2) and $FFFF) or + (Cardinal(((LCell.Top + LCell.Bottom) div 2) and $FFFF) shl 16)); + if ARightButton then begin + SendMessage(LGrid.Handle, WM_RBUTTONDOWN, MK_RBUTTON, LPoint); + SendMessage(LGrid.Handle, WM_RBUTTONUP, 0, LPoint); + end else begin + SendMessage(LGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, LPoint); + SendMessage(LGrid.Handle, WM_LBUTTONUP, 0, LPoint); + end; + Pump(25); +end; + +procedure TOperationDetailsChecks.CheckPassiveInput(const AScale: String); +var + LBefore, LBeforeRow, LBeforeTop: Integer; + LGrid: TDrawGrid; + LPoint: TPoint; + LPointParam: LPARAM; + LHash: String; +begin + LBefore := FWallet.DetailsCount; + FWallet.OperationsGrid.ClearSelection(True); + ClickRow(0); + LHash := SelectedHash; + Require(DetailsButton.Enabled, 'Selecting a valid operation did not enable Details'); + Require(FWallet.DetailsCount = LBefore, 'An ordinary operation click opened Details'); + ClickRow(1); + Require(SelectedHash <> LHash, 'Second native click did not select a distinct operation'); + Require(FWallet.DetailsCount = LBefore, 'Selecting a second operation opened Details'); + LGrid := FWallet.OperationsGrid.InternalDrawGrid; + LGrid.SetFocus; + LBeforeRow := LGrid.Row; + SendMessage(LGrid.Handle, WM_KEYDOWN, VK_DOWN, 1); + SendMessage(LGrid.Handle, WM_KEYUP, VK_DOWN, LPARAM($C0000001)); + Pump(25); + Require(LGrid.Row = LBeforeRow + 1, 'Native Down key did not change operation selection'); + Require(FWallet.DetailsCount = LBefore, 'Keyboard operation navigation opened Details'); + LBeforeTop := LGrid.TopRow; + LBeforeRow := LGrid.Row; + LPoint := LGrid.ClientToScreen(Types.Point(LGrid.ClientWidth div 2, LGrid.ClientHeight div 2)); + LPointParam := LPARAM(Cardinal(LPoint.X and $FFFF) or (Cardinal(LPoint.Y and $FFFF) shl 16)); + SendMessage(LGrid.Handle, WM_MOUSEWHEEL, WPARAM(Cardinal($FF880000)), LPointParam); + Pump(25); + Require((LGrid.TopRow > LBeforeTop) or (LGrid.Row > LBeforeRow), + 'Native wheel did not scroll or advance operation selection'); + Require(FWallet.DetailsCount = LBefore, 'Mouse-wheel operation navigation opened Details'); + Log('PASS ' + AScale + ': native row clicks, keyboard and wheel make no Details requests'); +end; + +procedure TOperationDetailsChecks.PopupTick(Sender: TObject); +var + LMenu: HMENU; + LCommand, LState: Cardinal; + LOwner: HWND; + LProcess: DWORD; + LCaption: array[0..127] of Char; + LIndex: Integer; +begin + if GetTickCount64 >= FPopupDeadline then begin + FError := 'Timed out observing the actual operation Details popup'; + FPopupTimer.Enabled := False; + EndMenu; + Exit; + end; + if FPopupDispatched then Exit; + EnumThreadWindows(GetCurrentThreadId, @FindOwnedDetailsPopup, LPARAM(Self)); + if FPopupWindow = 0 then Exit; + try + LMenu := FWallet.mnuOperationsPopup.Handle; + LIndex := FWallet.miOperationInfo.MenuIndex; + LCommand := GetMenuItemID(LMenu, LIndex); + Require(LCommand = FWallet.miOperationInfo.Command, 'Details native command does not match its menu item'); + LState := GetMenuState(LMenu, LIndex, MF_BYPOSITION); + Require((LState <> High(Cardinal)) and ((LState and (MF_DISABLED or MF_GRAYED)) = 0), + 'The native Details command is disabled for a valid selection'); + Require(GetMenuString(LMenu, LIndex, @LCaption[0], Length(LCaption), MF_BYPOSITION) > 0, + 'The native Details command has no caption'); + // VCL may assign a native accelerator ("&Details"). Compare the visible + // caption through the toolkit's own mnemonic parser, retaining exact text. + Require(StripHotkey(StrPas(PChar(@LCaption[0]))) = 'Details', + 'The context-menu action is not named Details: ' + StrPas(PChar(@LCaption[0]))); + LOwner := {$IFDEF FPC}Application.Handle{$ELSE}Vcl.Menus.PopupList.Window{$ENDIF}; + Require((LOwner <> 0) and + (GetWindowThreadProcessId(LOwner, @LProcess) = GetCurrentThreadId) and + (LProcess = GetCurrentProcessId), 'Details menu command owner is outside the test'); + FPopupDispatched := True; + FPopupTimer.Enabled := False; + // Dispatch through the toolkit's actual native menu-command window. No + // process-wide input or switch to the user's desktop is needed. + EndMenu; + SendMessage(LOwner, WM_COMMAND, WPARAM(LCommand), 0); + except + on E: Exception do OnException(Self, E); + end; +end; + +procedure TOperationDetailsChecks.CheckExplicitDetails(const AScale: String); +var + LBefore: Integer; + LHash: String; +begin + FWallet.OperationsGrid.ClearSelection(True); + ClickRow(0); + LHash := SelectedHash; + LBefore := FWallet.DetailsCount; + SendMessage(DetailsButton.Handle, BM_CLICK, 0, 0); + Pump(25); + Require((FWallet.DetailsCount = LBefore + 1) and (FWallet.LastDetailsHash = LHash), + 'The explicit Details button did not request the selected operation exactly once'); + Require(SelectedHash = LHash, 'Details button cleared or changed the selection'); + FPopupWindow := 0; + FPopupDispatched := False; + FPopupDeadline := GetTickCount64 + 3000; + FPopupTimer.Enabled := True; + try + ClickRow(0, True); + finally + FPopupTimer.Enabled := False; + EndMenu; + end; + Require(FPopupDispatched, 'Right-click did not show and dispatch the native Details menu'); + Require((FWallet.DetailsCount = LBefore + 2) and (FWallet.LastDetailsHash = LHash), + 'The native Details menu did not request the selected operation exactly once'); + Require(SelectedHash = LHash, 'The Details context menu cleared or changed the selection'); + Log('PASS ' + AScale + ': native Details button and right-click command each request the correct hash once; selection retained'); +end; + +procedure TOperationDetailsChecks.CheckUnavailable(const AScale: String); +var + LBefore: Integer; + LAdmissionDeadline: UInt64; +begin + LBefore := FWallet.DetailsCount; + FWallet.OperationsGrid.ClearSelection(True); + Require(not DetailsButton.Enabled and not DetailsAction.Enabled, + 'Details remains enabled with no operation selection'); + FWallet.miOperationInfoClick(Self); + Require(FWallet.DetailsCount = LBefore, 'Details opened without an operation selection'); + ClickRow(39); + Require(not DetailsButton.Enabled and not FWallet.miOperationInfo.Enabled, + 'A row without a valid operation hash enabled Details'); + FWallet.miOperationInfoClick(Self); + Require(FWallet.DetailsCount = LBefore, 'Details opened for an invalid operation hash'); + FWallet.OperationsGrid.ClearSelection(True); + ClickRow(0); + FWallet.OperationsGrid.ShowLoading('Please Wait', 'Synchronizing'); + try + // Native action updates run during the application idle cycle; invoke the + // same update contract while the controlled test pumps messages explicitly. + DetailsAction.Update; + Require(not DetailsButton.Enabled and not FWallet.miOperationInfo.Enabled, + 'Loading did not disable both Details entry points'); + FWallet.miOperationInfoClick(Self); + Require(FWallet.DetailsCount = LBefore, 'Details opened while the grid was loading'); + finally + FWallet.OperationsGrid.HideLoading; + end; + DetailsAction.Update; + Require(DetailsButton.Enabled, 'Details did not become available after loading ended'); + FModel.GateFetches := True; + try + FWallet.OperationsGrid.RefreshGrid; + DetailsAction.Update; + Require(not DetailsButton.Enabled and not FWallet.miOperationInfo.Enabled, + 'Quiet-period admission left stale operation Details enabled'); + // The configured quiet-period throttle owns admission; pump its timer + // before asserting the held worker state, without bypassing production. + LAdmissionDeadline := GetTickCount64+2000; + while not FWallet.OperationsGrid.IsUpdating and (GetTickCount64<LAdmissionDeadline) do Pump(5); + Require(FWallet.OperationsGrid.IsUpdating, 'Controlled operation page did not start a worker after quiet-period admission'); + DetailsAction.Update; + Require(not DetailsButton.Enabled and not FWallet.miOperationInfo.Enabled, + 'A running page fetch did not disable both Details entry points'); + FWallet.miOperationInfoClick(Self); + Require(FWallet.DetailsCount = LBefore, 'Details opened while its page was fetching'); + finally + FModel.GateFetches := False; + FModel.ReleaseFetches; + end; + AwaitRows; + DetailsAction.Update; + Require(DetailsButton.Enabled, 'Details did not become available when its fetch completed'); + Log('PASS ' + AScale + ': no selection, invalid hash, manual loading and active fetching disable Details and prevent dispatch'); +end; + +procedure TOperationDetailsChecks.CheckToolbarBounds(const AScale: String); +var + LButton: TBitBtn; + LHistory: TComboBox; + LButtonRect, LHistoryRect: Types.TRect; +begin + LButton := DetailsButton; + Require(FWallet.OperationsGrid.FindComponent('OperationsHistory') is TComboBox, + 'The original history picker is missing from the operations toolbar'); + LHistory := TComboBox(FWallet.OperationsGrid.FindComponent('OperationsHistory')); + Require((LButton.Parent = LHistory.Parent) and + (FWallet.OperationsGrid.WidgetControl = LButton.Parent), + 'History and Details are not in the same supported widget region'); + Require((LButton.Left >= 0) and (LButton.Top >= 0) and + (LButton.Left + LButton.Width <= LButton.Parent.ClientWidth) and + (LButton.Top + LButton.Height <= LButton.Parent.ClientHeight), + 'The Details button is clipped by its native parent'); + Require(LHistory.Left + LHistory.Width <= LButton.Left, + 'The Details button overlaps the history picker'); + Require((LButton.Height=LHistory.Height) and (LButton.Width=LButton.Height), + 'Details must be square and match the native history picker height'); + Require(GetWindowRect(LButton.Handle,LButtonRect) and GetWindowRect(LHistory.Handle,LHistoryRect), + 'Cannot inspect native history/Details rectangles'); + Require((LButtonRect.Top=LHistoryRect.Top) and (LButtonRect.Bottom=LHistoryRect.Bottom), + 'History and Details native top/bottom edges are not aligned'); + Require(not LButton.Glyph.Empty, 'The Details button has no visible information glyph'); + Log('PASS ' + AScale + ': Details icon and history picker fit without overlap'); +end; + +procedure TOperationDetailsChecks.Capture(const ASuffix: String); +var + LBag: TDisposables; + LBitmap: TBitmap; + LBackground: TColor; + LInk, LIconInk, X, Y: Integer; + LIconOrigin: TPoint; + {$IFDEF FPC} + LCanvasDC: HDC; + + procedure PaintNativeControl(AControl: TWinControl); + var + LSaved, I: Integer; + LOrigin, LWindowOrigin: TPoint; + LWindow: TRect; + begin + if not AControl.HandleAllocated or not IsWindowVisible(AControl.Handle) then Exit; + LSaved := SaveDC(LCanvasDC); + try + GetWindowRect(AControl.Handle, LWindow); + LWindowOrigin := FHost.ScreenToClient(LWindow.TopLeft); + SetViewportOrgEx(LCanvasDC, LWindowOrigin.X, LWindowOrigin.Y, nil); + SendMessage(AControl.Handle, WM_PRINT, WPARAM(LCanvasDC), PRF_NONCLIENT); + LOrigin := FHost.ScreenToClient(AControl.ClientToScreen(Types.Point(0, 0))); + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + if AControl is TCustomGroupBox then + IntersectClipRect(LCanvasDC, LWindowOrigin.X - LOrigin.X, + LWindowOrigin.Y - LOrigin.Y, LWindowOrigin.X - LOrigin.X + + LWindow.Right - LWindow.Left, LWindowOrigin.Y - LOrigin.Y + + LWindow.Bottom - LWindow.Top) + else + IntersectClipRect(LCanvasDC, 0, 0, AControl.ClientWidth, AControl.ClientHeight); + AControl.Perform(LM_ERASEBKGND, WPARAM(LCanvasDC), 0); + AControl.Perform(LM_PAINT, WPARAM(LCanvasDC), 0); + if not (AControl is TCustomControl) and not (AControl is TCustomForm) and + not (AControl is TCustomGroupBox) then + SendMessage(AControl.Handle, WM_PRINTCLIENT, WPARAM(LCanvasDC), PRF_CLIENT); + for I := 0 to AControl.ControlCount - 1 do + if AControl.Controls[I] is TWinControl then + PaintNativeControl(TWinControl(AControl.Controls[I])); + finally + RestoreDC(LCanvasDC, LSaved); + end; + end; + {$ENDIF} +begin + {$IFDEF FPC} + LBitmap := LBag.AddObject(TBitmap.Create) as TBitmap; + LBitmap.PixelFormat := pf24bit; + LBitmap.SetSize(FHost.ClientWidth, FHost.ClientHeight); + LBitmap.Canvas.Brush.Color := FHost.GetRGBColorResolvingParent; + LBitmap.Canvas.FillRect(FHost.ClientRect); + LCanvasDC := LBitmap.Canvas.Handle; + // Direct LCL form PaintTo can return black on a non-input desktop. Paint + // each actual native child into the initialized client bitmap instead. + PaintNativeControl(FHost); + {$ELSE} + LBitmap := LBag.AddObject(FHost.GetFormImage) as TBitmap; + {$ENDIF} + LBackground := ColorToRGB(LBitmap.Canvas.Pixels[0, 0]); + LInk := 0; + for Y := 0 to (LBitmap.Height - 1) div 4 do + for X := 0 to (LBitmap.Width - 1) div 4 do + if ColorToRGB(LBitmap.Canvas.Pixels[X * 4, Y * 4]) <> LBackground then Inc(LInk); + Require(LInk > 100, 'Details capture is blank'); + LIconOrigin := FHost.ScreenToClient(DetailsButton.ClientToScreen(Types.Point(0, 0))); + LIconInk := 0; + for Y := LIconOrigin.Y to LIconOrigin.Y + DetailsButton.ClientHeight - 1 do + for X := LIconOrigin.X to LIconOrigin.X + DetailsButton.ClientWidth - 1 do begin + Require(ColorToRGB(LBitmap.Canvas.Pixels[X, Y]) <> ColorToRGB(clFuchsia), + 'Details icon exposes a magenta transparency mask'); + if ColorToRGB(LBitmap.Canvas.Pixels[X, Y]) = ColorToRGB(clNavy) then Inc(LIconInk); + end; + Require(LIconInk >= 8, 'The native Details button did not paint its information icon'); + LBitmap.SaveToFile(ChangeFileExt(FLogPath, '.wallet-operation-details-' + ASuffix + '.bmp')); +end; + +procedure TOperationDetailsChecks.Run; +var + LOriginalBounds: TRect; + + procedure CheckScale(const AScale: String); + var + LScalePpi: Integer; + begin + AwaitRows; + LScalePpi := StrToInt(AScale) * 96 div 100; + CheckVisualGridFooter(FWallet.AccountsGrid, LScalePpi, FLog); + CheckVisualGridFooter(FWallet.OperationsGrid, LScalePpi, FLog); + Log('GEOMETRY ' + AScale + ': ' + TDetailsGridAccess(FWallet.OperationsGrid).FooterDescription); + CheckPassiveInput(AScale); + CheckExplicitDetails(AScale); + CheckUnavailable(AScale); + CheckToolbarBounds(AScale); + Capture(AScale); + end; + +begin + FHost := TForm.CreateNew(nil); + FHost.Caption := 'Wallet operation Details verification'; + FHost.Font.Name := 'Tahoma'; + FHost.Font.Height := -11; + FHost.SetBounds(30, 30, 1120, 610); + FModel := TMockWalletData.Create(FHost); + FModel.SetData(7501, 5); + FWallet := TDetailsWallet.CreateForModel(FHost, FModel); + FWallet.Font.Assign(FHost.Font); + FWallet.Parent := FHost; + FWallet.Align := alClient; + FWallet.OperationsGrid.PageMode := vpmSingle; + FHost.Show; + FWallet.Show; + CheckScale('100'); + LOriginalBounds := FHost.BoundsRect; + FHost.ScaleBy(5, 4); + try + CheckScale('125'); + finally + FHost.ScaleBy(4, 5); + FHost.BoundsRect := LOriginalBounds; + end; + FHost.ScaleBy(3, 2); + try + CheckScale('150'); + finally + FHost.ScaleBy(2, 3); + FHost.BoundsRect := LOriginalBounds; + end; + Log('PASS main-wallet Details: selection is passive; only explicit actions open details'); +end; + +procedure CheckWalletOperationDetails(const ALogPath: String); +var + LBag: TDisposables; + LChecks: TOperationDetailsChecks; +begin + LChecks := LBag.AddObject(TOperationDetailsChecks.Create(ALogPath)) as TOperationDetailsChecks; + LChecks.Run; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletOperationInfoLayoutChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletOperationInfoLayoutChecks.pas new file mode 100644 index 000000000..b73d0a0bc --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletOperationInfoLayoutChecks.pas @@ -0,0 +1,359 @@ +unit UWalletOperationInfoLayoutChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Exercises actual experimental Operation Information controls with large + operation counters and monetary values. It never searches or executes an + operation, changes wallet settings, or broadcasts. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +procedure CheckWalletOperationInfoLayout(const ALogPath: String); + +implementation + +uses Windows, Messages, Classes, SysUtils, Types, Math, Forms, Controls, + StdCtrls, ExtCtrls, ComCtrls, Graphics, {$IFDEF FPC}LCLIntf, LMessages,{$ENDIF} + UMemory, UBlockChain, UFRMPayloadDecoder; + +type + TOperationInfoLayoutChecks = class(TComponent) + private + FLog: TStringList; + FLogPath, FError: String; + FPreviousException: TExceptionEvent; + procedure Require(ACondition: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure OnException(Sender: TObject; E: Exception); + procedure Pump; + procedure CheckLayout(AForm: TFRMPayloadDecoder; const AContext: String); + procedure Capture(AForm: TForm; const AName: String); + procedure RunCase(AScale, ACase: Integer); + public + constructor Create(const ALogPath: String); reintroduce; + destructor Destroy; override; + procedure Run; + end; + +function CaptionUnicode(const ACaption: String): UnicodeString; +begin + {$IFDEF FPC} + Result := UTF8Decode(ACaption); + {$ELSE} + Result := ACaption; + {$ENDIF} +end; + +constructor TOperationInfoLayoutChecks.Create(const ALogPath: String); +begin + inherited Create(nil); + FLogPath := ChangeFileExt(ALogPath, '') + '.operation-info-layout.log'; + FLog := TStringList.Create; + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TOperationInfoLayoutChecks.Destroy; +begin + Application.OnException := FPreviousException; + FLog.Free; + inherited Destroy; +end; + +procedure TOperationInfoLayoutChecks.Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create(AMessage); +end; + +procedure TOperationInfoLayoutChecks.Log(const AMessage: String); +begin + FLog.Add(AMessage); + FLog.SaveToFile(FLogPath); +end; + +procedure TOperationInfoLayoutChecks.OnException(Sender: TObject; E: Exception); +begin + if FError = '' then + FError := E.ClassName + ': ' + E.Message; +end; + +procedure TOperationInfoLayoutChecks.Pump; +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + 60; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LUntil; + Require(FError = '', FError); +end; + +procedure TOperationInfoLayoutChecks.CheckLayout(AForm: TFRMPayloadDecoder; const AContext: String); +var + I, J: Integer; + A, B: TControl; + LText, LOverlap, LWindow, LWork: TRect; + LCanvas: TControlCanvas; + LBag: TDisposables; +begin + LCanvas := LBag.AddObject(TControlCanvas.Create) as TControlCanvas; + LCanvas.Control := AForm; + for I := 0 to AForm.ControlCount - 1 do begin + A := AForm.Controls[I]; + if not A.Visible or ((A is TLabel) and (TLabel(A).Caption = '')) then + Continue; + Require((A.Left >= 0) and (A.Top >= 0) and (A.Left + A.Width <= AForm.ClientWidth) and (A.Top + A.Height <= AForm.ClientHeight), + AContext + ': control exceeds the form client: ' + A.Name); + if A is TLabel then begin + LCanvas.Font.Assign(TLabel(A).Font); + LText := Types.Rect(0, 0, A.Width, 0); + // LCL label text is UTF-8; its widgetset DrawText converts to native + // Unicode. Windows.DrawText's ANSI path mismeasures translated captions. + if TLabel(A).WordWrap then + DrawText(LCanvas.Handle, PChar(TLabel(A).Caption), Length(TLabel(A).Caption), LText, DT_CALCRECT or DT_NOPREFIX or DT_WORDBREAK) + else + DrawText(LCanvas.Handle, PChar(TLabel(A).Caption), Length(TLabel(A).Caption), LText, DT_CALCRECT or DT_NOPREFIX); + Require((LText.Right <= A.Width + 1) and (LText.Bottom <= A.Height + 1), AContext + ': text clipped in ' + A.Name); + Log(Format('LABEL %s %s=%s rect=%d,%d,%d,%d text=%d,%d', [AContext, A.Name, TLabel(A).Caption, A.Left, A.Top, A.Width, A.Height, LText.Right, LText.Bottom])); + end; + for J := I + 1 to AForm.ControlCount - 1 do begin + B := AForm.Controls[J]; + if not B.Visible or ((B is TLabel) and (TLabel(B).Caption = '')) then + Continue; + if IntersectRect(LOverlap, A.BoundsRect, B.BoundsRect) then + Require((LOverlap.Right <= LOverlap.Left) or (LOverlap.Bottom <= LOverlap.Top), AContext + ': overlapping controls ' + A.Name + '/' + B.Name); + end; + end; + Require(AForm.PageControl.Top > AForm.lblOperationTxt.Top + AForm.lblOperationTxt.Height, AContext + ': payload overlaps operation text'); + Require(AForm.bbClose.Top >= AForm.PageControl.Top + AForm.PageControl.Height, AContext + ': Close overlaps payload'); + GetWindowRect(AForm.Handle, LWindow); + LWork := AForm.Monitor.WorkareaRect; + Require((LWindow.Left >= LWork.Left) and (LWindow.Top >= LWork.Top) and (LWindow.Right <= LWork.Right) and (LWindow.Bottom <= LWork.Bottom), AContext + ': window exceeds native workarea'); + Log(Format('PASS %s client=%dx%d native=%d,%d,%d,%d', [AContext, AForm.ClientWidth, AForm.ClientHeight, LWindow.Left, LWindow.Top, LWindow.Right, LWindow.Bottom])); +end; + +procedure TOperationInfoLayoutChecks.Capture(AForm: TForm; const AName: String); +var + LBitmap: Graphics.TBitmap; + X, Y, LInk: Integer; + LBackground: TColor; +{$IFDEF FPC} + LCanvasDC: HDC; + + procedure PaintControl(AControl: TWinControl); + var + LSaved, I: Integer; + LOrigin, LWindowOrigin: TPoint; + LWindow: TRect; + begin + if not AControl.HandleAllocated or not IsWindowVisible(AControl.Handle) then + Exit; + LSaved := SaveDC(LCanvasDC); + try + GetWindowRect(AControl.Handle, LWindow); + LOrigin := AForm.ScreenToClient(LWindow.TopLeft); + LWindowOrigin := LOrigin; + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + if AControl is TPageControl then + begin + // LCL's logical client excludes the tab strip. Paint the real Win32 + // tab control in its full window coordinates before its page children. + IntersectClipRect(LCanvasDC, 0, 0, LWindow.Right - LWindow.Left, + LWindow.Bottom - LWindow.Top); + SendMessage(AControl.Handle, WM_PRINT, WPARAM(LCanvasDC), + PRF_CLIENT or PRF_NONCLIENT or PRF_ERASEBKGND); + end + else + begin + SendMessage(AControl.Handle, WM_PRINT, WPARAM(LCanvasDC), PRF_NONCLIENT); + LOrigin := AForm.ScreenToClient(AControl.ClientToScreen(Types.Point(0, 0))); + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + if AControl is TCustomGroupBox then + IntersectClipRect(LCanvasDC, LWindowOrigin.X - LOrigin.X, + LWindowOrigin.Y - LOrigin.Y, LWindowOrigin.X - LOrigin.X + + LWindow.Right - LWindow.Left, LWindowOrigin.Y - LOrigin.Y + + LWindow.Bottom - LWindow.Top) + else + IntersectClipRect(LCanvasDC, 0, 0, AControl.ClientWidth, AControl.ClientHeight); + AControl.Perform(LM_ERASEBKGND, WPARAM(LCanvasDC), 0); + AControl.Perform(LM_PAINT, WPARAM(LCanvasDC), 0); + if not (AControl is TCustomControl) and not (AControl is TCustomForm) and + not (AControl is TCustomGroupBox) then + SendMessage(AControl.Handle, WM_PRINTCLIENT, WPARAM(LCanvasDC), PRF_CLIENT); + end; + for I := 0 to AControl.ControlCount - 1 do + if AControl.Controls[I] is TWinControl then + PaintControl(TWinControl(AControl.Controls[I])); + finally + RestoreDC(LCanvasDC, LSaved); + end; + end; +{$ENDIF} +begin + AForm.Repaint; + UpdateWindow(AForm.Handle); +{$IFDEF FPC} + LBitmap := Graphics.TBitmap.Create; +{$ELSE} + LBitmap := AForm.GetFormImage; +{$ENDIF} + try +{$IFDEF FPC} + LBitmap.PixelFormat := pf24bit; + LBitmap.SetSize(AForm.ClientWidth, AForm.ClientHeight); + LBitmap.Canvas.Lock; + try + // Initialize the bitmap before asking the real LCL controls to paint. + LBitmap.Canvas.Brush.Color := AForm.GetRGBColorResolvingParent; + LBitmap.Canvas.FillRect(AForm.ClientRect); + LCanvasDC := LBitmap.Canvas.Handle; + PaintControl(AForm); + finally + LBitmap.Canvas.Unlock; + end; +{$ENDIF} + LBitmap.SaveToFile(ChangeFileExt(FLogPath, '') + '.' + AName + '.bmp'); + LBackground := ColorToRGB(LBitmap.Canvas.Pixels[0, 0]); + LInk := 0; + for Y := 0 to (LBitmap.Height - 1) div 4 do + for X := 0 to (LBitmap.Width - 1) div 4 do + if ColorToRGB(LBitmap.Canvas.Pixels[X * 4, Y * 4]) <> LBackground then + Inc(LInk); + Require(LInk > 100, AName + ': native capture is blank'); + finally + LBitmap.Free; + end; +end; + +procedure TOperationInfoLayoutChecks.RunCase(AScale, ACase: Integer); +var + LForm: TFRMPayloadDecoder; + LBag: TDisposables; + LResume: TOperationResume; + LWork: TRect; + LContext, LBlock: String; + LHeight, LPageHeight, LExtraHeight, I: Integer; +begin + LForm := LBag.AddObject(TFRMPayloadDecoder.Create(nil)) as TFRMPayloadDecoder; + LResume := CT_TOperationResume_NUL; + LResume.valid := True; + LResume.Block := 1127529; + LResume.NOpInsideBlock := 1; + LResume.n_operation := 186837; + LResume.time := 1793490000; + LResume.AffectedAccount := 4029; + LResume.SignerAccount := 4029; + LResume.DestAccount := 4195; + LResume.OperationTxt := 'Changed type of account 4195-65'; + LResume.Fee := -1; + SetLength(LResume.OperationHash, 32); + if ACase > 0 then begin + LResume.Block := High(Cardinal); + LResume.NOpInsideBlock := High(Integer) - 1; + LResume.n_operation := High(Cardinal); + LResume.AffectedAccount := High(Cardinal); + LResume.SignerAccount := High(Cardinal); + LResume.DestAccount := High(Cardinal) - 1; + LResume.Amount := High(Int64); + LResume.Fee := -High(Int64); + LResume.OperationTxt := 'Changed account information and ownership for account 4294967295, including its public key, type and transaction payload.'; + end; + if ACase = 3 then + LResume.AffectedAccount := High(Cardinal) - 1; + LForm.Init(LResume); + LBlock := IntToStr(LResume.Block) + '/' + IntToStr(LResume.NOpInsideBlock + 1) + ' ' + IntToStr(LResume.n_operation); + Require(LForm.lblBlock.Caption = LBlock, 'Operation block/index/counter text changed'); + if ACase = 2 then begin + // Set translated text before first Show. Production DoShow must lay it out; + // calling its layout/Resize directly would conceal a first-show defect. + LForm.Label1.Caption := 'Block und Operation:'; + LForm.Label6.Caption := 'Datum und Uhrzeit:'; + LForm.lblDateTime.Caption := 'Wednesday, 31 December 2026 23:59:59'; + LForm.lblSenderCaption.Caption := 'Absenderkonto:'; + LForm.lblReceiverCaption.Caption := 'Empfängerkonto:'; + LForm.lblAmountCaption.Caption := 'Gesamtbetrag:'; + LForm.lblFeeCaption.Caption := 'Transaktionsgebühr:'; + Require(Pos(UnicodeString(WideChar($00FC)), CaptionUnicode(LForm.lblFeeCaption.Caption)) > 0, 'Fee caption lost its translated Unicode character'); + Require(Pos(UnicodeString(WideChar($00E4)), CaptionUnicode(LForm.lblReceiverCaption.Caption)) > 0, 'Receiver caption lost its translated Unicode character'); + end; + LForm.ActiveControl := LForm.bbClose; + if AScale <> 100 then + LForm.ScaleBy(AScale, 100); + LForm.Position := poDesigned; + LWork := Screen.PrimaryMonitor.WorkareaRect; + LForm.Left := LWork.Left + 8; + LForm.Top := LWork.Top + 8; + LForm.Show; + Pump; + LContext := Format('operation-info-%d-case%d-first-show', [AScale, ACase]); + CheckLayout(LForm, LContext); + Capture(LForm, LContext); + if ACase = 0 then begin + LHeight := LForm.ClientHeight; + LPageHeight := LForm.PageControl.Height; + LExtraHeight := MulDiv(80, AScale, 100); + LForm.ClientHeight := LHeight + LExtraHeight; + Pump; + Require(LForm.ClientHeight = LHeight + LExtraHeight, 'Details layout undid a larger client height'); + Require(LForm.PageControl.Height = LPageHeight + LExtraHeight, 'Payload did not receive the extra client height'); + CheckLayout(LForm, LContext + '-larger'); + LForm.ClientHeight := LHeight; + Pump; + CheckLayout(LForm, LContext + '-restored'); + LResume.n_operation := High(Cardinal); + LResume.Block := High(Cardinal); + LResume.NOpInsideBlock := High(Integer) - 1; + LForm.OpResume := LResume; + Pump; + CheckLayout(LForm, LContext + '-updated'); + LHeight := LForm.ClientHeight; + for I := 0 to 4 do + LForm.OpResume := LResume; + Pump; + Require(LForm.ClientHeight = LHeight, 'Repeated details updates grew the dialog'); + // Both directions must reflow through actual scaling/resize notifications. + if AScale = 150 then begin + LForm.ScaleBy(2, 3); + Pump; + CheckLayout(LForm, LContext + '-scale-down'); + LForm.ScaleBy(3, 2); + Pump; + CheckLayout(LForm, LContext + '-scale-up'); + end else begin + LForm.ScaleBy(150, AScale); + Pump; + CheckLayout(LForm, LContext + '-scale-up'); + LForm.ScaleBy(AScale, 150); + Pump; + CheckLayout(LForm, LContext + '-scale-down'); + end; + end; + LForm.Close; + Pump; + Require(not LForm.Visible, 'Operation Information remained visible after Close'); +end; + +procedure TOperationInfoLayoutChecks.Run; +var + LScale, LCase: Integer; +begin + for LScale in [100, 125, 150] do + for LCase := 0 to 3 do + RunCase(LScale, LCase); + Log('PASS 12 Operation Information first-show cases, live updates and DPI roundtrips'); +end; + +procedure CheckWalletOperationInfoLayout(const ALogPath: String); +var + LBag: TDisposables; + LChecks: TOperationInfoLayoutChecks; +begin + LChecks := LBag.AddObject(TOperationInfoLayoutChecks.Create(ALogPath)) as TOperationInfoLayoutChecks; + LChecks.Run; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletOperationMessageChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletOperationMessageChecks.pas new file mode 100644 index 000000000..2443362a5 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletOperationMessageChecks.pas @@ -0,0 +1,714 @@ +unit UWalletOperationMessageChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Native operation-message checks without a node, wallet, signing or broadcast. + Only owned dialogs are observed, scaled, captured and closed. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses Forms; + +procedure CheckWalletOperationMessages(AParent: TForm; const ALogPath: String); + +implementation + +uses + Windows, Messages, Classes, SysUtils, Types, Math, Controls, StdCtrls, + ExtCtrls, Buttons, Graphics, {$IFDEF FPC}LMessages,{$ENDIF} + UFRMMemoText, UMemory, UNode; + +type + TOperationMessageHost = class(TForm) + public + constructor Create(AOwner: TComponent); override; + end; + + TMessageAction = (maNo, maEnter, maEscape, maClose, maYes); + TMessageMode = (mmConfirm, mmResult, mmInput, mmLegacyReadOnly); + + TOperationMessageChecks = class(TComponent) + private + FParent: TForm; + FTimer: TTimer; + FLog: TStringList; + FLogPath, FTitle, FPrompt, FDetails, FError, FCaseName, FPhase: String; + FObserved: TFRMMemoText; + FPreviousException: TExceptionEvent; + FAction: TMessageAction; + FMode: TMessageMode; + FDeadline: UInt64; + FProcessing, FActionSent, FScaled, FSmall, FCapture, FConstrained: Boolean; + FObservedCount: Integer; + procedure Require(ACondition: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure LogException(E: Exception); + procedure OnException(Sender: TObject; E: Exception); + procedure TimerTick(Sender: TObject); + procedure LogHandles(AForm: TFRMMemoText; const AStage: String); + procedure CheckLayout(AForm: TFRMMemoText; AInitial: Boolean); + procedure Capture(AForm: TFRMMemoText); + procedure RunCase(const AName: String; AMode: TMessageMode; + AAction: TMessageAction; AScaled, ASmall, ACapture: Boolean; + AConstrained: Boolean = False); + procedure SendAction(AForm: TFRMMemoText); + protected + procedure Notification(AComponent: TComponent; AOperation: TOperation); override; + public + constructor Create(AParent: TForm; const ALogPath: String); reintroduce; + destructor Destroy; override; + procedure Run; + end; + +function NormalizeMemoText(const AText: String): String; +begin + Result := StringReplace(AText, #13#10, #10, [rfReplaceAll]); + Result := StringReplace(Result, #13, #10, [rfReplaceAll]); + while (Length(Result) > 0) and (Result[Length(Result)] = #10) do + Delete(Result, Length(Result), 1); +end; + +constructor TOperationMessageHost.Create(AOwner: TComponent); +var + LWork: TRect; +begin + inherited CreateNew(AOwner); + Caption := 'Operation message native fixture'; + // Legacy dialogs center on the host; keep that smaller host away from edges. + LWork := Screen.PrimaryMonitor.WorkareaRect; + SetBounds(LWork.Left + (LWork.Right - LWork.Left - 640) div 2, + LWork.Top + (LWork.Bottom - LWork.Top - 480) div 2, 640, 480); +end; + +constructor TOperationMessageChecks.Create(AParent: TForm; const ALogPath: String); +var + LLines: TStringList; + I: Integer; +begin + inherited Create(nil); + FParent := AParent; + FLogPath := ALogPath; + FLog := TStringList.Create; + FTimer := TTimer.Create(Self); + FTimer.Enabled := False; + FTimer.Interval := 20; + FTimer.OnTimer := TimerTick; + FPreviousException := Application.OnException; + Application.OnException := OnException; + LLines := TStringList.Create; + try + for I := 1 to 267 do + LLines.Add(Format('Operation %3.3d/267: Account %d type 1 -> 2; ', [I, I]) + + StringOfChar('W', 700) + ' END-' + IntToStr(I)); + FDetails := LLines.Text; + while (Length(FDetails) > 0) and (FDetails[Length(FDetails)] in [#13, #10]) do + Delete(FDetails, Length(FDetails), 1); + finally + LLines.Free; + end; + FPrompt := 'Execute 267 operations?' + sLineBreak + + 'Total fee: 0.0267 PASC' + sLineBreak + + 'These operations will be transmitted to the network.'; +end; + +destructor TOperationMessageChecks.Destroy; +begin + FTimer.Enabled := False; + if Assigned(FObserved) then + FObserved.RemoveFreeNotification(Self); + Application.OnException := FPreviousException; + FLog.Free; + inherited Destroy; +end; + +procedure TOperationMessageChecks.Notification(AComponent: TComponent; + AOperation: TOperation); +begin + inherited Notification(AComponent, AOperation); + if (AOperation = opRemove) and (AComponent = FObserved) then + FObserved := nil; +end; + +procedure TOperationMessageChecks.Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create(FCaseName + ': ' + AMessage); +end; + +procedure TOperationMessageChecks.Log(const AMessage: String); +begin + FLog.Add(AMessage); + FLog.SaveToFile(FLogPath); +end; + +procedure TOperationMessageChecks.LogException(E: Exception); +{$IFDEF FPC} +var + I: Integer; +{$ENDIF} +begin + Log('EXCEPTION phase=' + FPhase + ' case=' + FCaseName + ' ' + E.ClassName + ': ' + E.Message); +{$IFDEF FPC} + Log('FRAME ' + BackTraceStrFunc(ExceptAddr)); + for I := 0 to ExceptFrameCount - 1 do + Log('FRAME ' + BackTraceStrFunc(ExceptFrames[I])); +{$ELSE} + Log('ADDRESS ' + IntToHex(NativeUInt(ExceptAddr), SizeOf(Pointer) * 2)); +{$ENDIF} +end; + +procedure TOperationMessageChecks.OnException(Sender: TObject; E: Exception); +var + I: Integer; +begin + if FError = '' then + begin + FError := E.ClassName + ': ' + E.Message; + LogException(E); + end; + if Assigned(FObserved) and FObserved.HandleAllocated then + PostMessage(FObserved.Handle, WM_CLOSE, 0, 0) + else + for I := 0 to Screen.FormCount - 1 do + if (Screen.Forms[I] is TFRMMemoText) and Screen.Forms[I].Visible and + Screen.Forms[I].HandleAllocated then + PostMessage(Screen.Forms[I].Handle, WM_CLOSE, 0, 0); +end; + +procedure TOperationMessageChecks.CheckLayout(AForm: TFRMMemoText; AInitial: Boolean); +var + LWork, LWindow, LClient, LMemo, LFooter, LButton, LSummary: TRect; + LYes: TComponent; + LPrompt: TComponent; + LStyle: NativeInt; + LActualText, LSelectedText: String; + LLineCount: Integer; + + procedure Step(const AName: String); + begin + FPhase := 'layout ' + AName; + Log('CHECK ' + FCaseName + ' ' + AName); + end; + + procedure CheckContained(const AOuter, AInner: TRect; const AName: String); + var + LContained: Boolean; + begin + LContained := (AInner.Left >= AOuter.Left) and (AInner.Top >= AOuter.Top) and + (AInner.Right <= AOuter.Right) and (AInner.Bottom <= AOuter.Bottom); + if not LContained then + begin + Log(Format('CONTAINMENT %s %s outer=%d,%d,%d,%d inner=%d,%d,%d,%d', + [FCaseName, AName, AOuter.Left, AOuter.Top, AOuter.Right, AOuter.Bottom, + AInner.Left, AInner.Top, AInner.Right, AInner.Bottom])); + LogHandles(AForm, 'containment failure ' + AName); + end; + Require(LContained, AName + ' extends beyond its container'); + end; + +begin + LLineCount := 0; + LActualText := ''; + Step('native bounds'); + Require(AForm.HandleAllocated and IsWindowVisible(AForm.Handle) and + (fsModal in AForm.FormState), 'dialog is not an actual visible modal window'); + GetWindowRect(AForm.Handle, LWindow); + LWork := AForm.Monitor.WorkareaRect; + if FConstrained and not AInitial then + begin + // Restrict only this fixture's allowed rectangle, never the user's display. + LWork.Right := Min(LWork.Right, LWork.Left + 1024); + LWork.Bottom := Min(LWork.Bottom, LWork.Top + 720); + end; + CheckContained(LWork, LWindow, 'dialog/workarea'); + LClient.TopLeft := AForm.ClientToScreen(Types.Point(0, 0)); + LClient.BottomRight := AForm.ClientToScreen(Types.Point(AForm.ClientWidth, AForm.ClientHeight)); + GetWindowRect(AForm.Memo.Handle, LMemo); + GetWindowRect(AForm.pnlBottom.Handle, LFooter); + GetWindowRect(AForm.bbCancel.Handle, LButton); + CheckContained(LClient, LMemo, 'memo'); + CheckContained(LClient, LFooter, 'footer'); + CheckContained(LFooter, LButton, 'No/Close button'); + Require(LMemo.Bottom <= LFooter.Top, 'memo overlaps the button footer'); + Require(AForm.bbCancel.Visible and AForm.bbCancel.Enabled and + (LButton.Right > LButton.Left) and (LButton.Bottom > LButton.Top), + 'No/Close button is missing or unusable'); + Step('native bounds checked'); + LPrompt := AForm.FindComponent('lblPrompt'); + LYes := AForm.FindComponent('bbConfirmYes'); + if FMode in [mmConfirm, mmResult] then + begin + Step('read memo text'); + LActualText := AForm.Memo.Lines.Text; + Step('memo text returned'); + Require(NormalizeMemoText(LActualText) = NormalizeMemoText(FDetails), + 'long operation text was truncated or modified'); + Step('read memo line count'); + LLineCount := AForm.Memo.Lines.Count; + Step('memo line count returned'); + Require(LLineCount = 267, 'all 267 operation lines were not retained'); + Require(AForm.Memo.ReadOnly and not AForm.AllowInput, 'operation memo accepts edits'); + Require((AForm.Memo.ScrollBars = ssBoth) and not AForm.Memo.WordWrap, + 'long operations require vertical and horizontal scrolling without wrapping'); + LStyle := GetWindowLongPtr(AForm.Memo.Handle, GWL_STYLE); + Require((LStyle and WS_VSCROLL <> 0) and (LStyle and WS_HSCROLL <> 0), + 'native memo scrollbars are missing'); + Step('summary and buttons'); + Require((LPrompt is TLabel) and TLabel(LPrompt).Visible and + (TLabel(LPrompt).Caption = FPrompt), 'fixed summary is missing or changed'); + LSummary.TopLeft := TLabel(LPrompt).ClientToScreen(Types.Point(0, 0)); + LSummary.BottomRight := TLabel(LPrompt).ClientToScreen( + Types.Point(TLabel(LPrompt).Width, TLabel(LPrompt).Height)); + CheckContained(LClient, LSummary, 'summary'); + Require(LSummary.Bottom <= LMemo.Top, 'fixed summary overlaps the operation list'); + Require(AForm.bbCancel.Default and AForm.bbCancel.Cancel, + 'No/Close must be the default and Escape button'); + if FMode = mmConfirm then + begin + Require((LYes is TBitBtn) and TBitBtn(LYes).Visible and TBitBtn(LYes).Enabled, + 'confirmation has no affirmative button'); + Require(not TBitBtn(LYes).Default and (TBitBtn(LYes).ModalResult = mrYes), + 'Yes is default or has an incorrect modal result'); + Require(AForm.bbCancel.ModalResult = mrNo, 'confirmation default is not No'); + GetWindowRect(TBitBtn(LYes).Handle, LButton); + CheckContained(LFooter, LButton, 'Yes button'); + end + else + Require(not Assigned(LYes) or not TControl(LYes).Visible, + 'result dialog exposes a confirmation Yes button'); + // Verify selection/copy access through the actual readonly native memo. + // Reading SelText avoids changing the user's clipboard. + Step('select all memo'); + AForm.Memo.SelectAll; + Step('read selected memo text'); + LSelectedText := AForm.Memo.SelText; + Step('selected memo text returned'); + Require(NormalizeMemoText(LSelectedText) = NormalizeMemoText(FDetails), + 'readonly operation list is not fully selectable/copyable'); + Step('clear memo selection'); + AForm.Memo.SelStart := 0; + AForm.Memo.SelLength := 0; + Step('memo selection cleared'); + end + else + begin + Require(not AForm.pnlPrompt.Showing, + 'new confirmation summary leaked into a legacy memo mode'); + if FMode = mmInput then + Require(not AForm.Memo.ReadOnly and AForm.AllowInput, 'legacy input mode lost editability') + else + Require(AForm.Memo.ReadOnly and not AForm.AllowInput, 'legacy readonly mode changed'); + end; + Step('complete'); + if AInitial then + Log('INITIAL ' + FCaseName + ' bounded to actual monitor workarea') + else + Log(Format('LAYOUT %s client=%dx%d lines=%d text=%d', + [FCaseName, AForm.ClientWidth, AForm.ClientHeight, + LLineCount, Length(LActualText)])); +end; + +procedure TOperationMessageChecks.Capture(AForm: TFRMMemoText); +var + LBitmap: Graphics.TBitmap; + LOrigin: TPoint; + LLeft, LTop, LRight, LBottom, X, Y, LLight, LDark: Integer; + LColor: LongInt; +{$IFDEF FPC} + LCanvasDC: HDC; + + procedure PaintControl(AControl: TWinControl); + var + LSaved, I: Integer; + LOrigin: TPoint; + LWindow: TRect; + begin + if not AControl.HandleAllocated or not IsWindowVisible(AControl.Handle) then + Exit; + LSaved := SaveDC(LCanvasDC); + try + GetWindowRect(AControl.Handle, LWindow); + LOrigin := AForm.ScreenToClient(LWindow.TopLeft); + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + SendMessage(AControl.Handle, WM_PRINT, WPARAM(LCanvasDC), PRF_NONCLIENT); + LOrigin := AForm.ScreenToClient(AControl.ClientToScreen(Types.Point(0, 0))); + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + IntersectClipRect(LCanvasDC, 0, 0, AControl.ClientWidth, AControl.ClientHeight); + AControl.Perform(LM_ERASEBKGND, WPARAM(LCanvasDC), 0); + AControl.Perform(LM_PAINT, WPARAM(LCanvasDC), 0); + if not (AControl is TCustomControl) and not (AControl is TCustomForm) then + SendMessage(AControl.Handle, WM_PRINTCLIENT, WPARAM(LCanvasDC), PRF_CLIENT); + for I := 0 to AControl.ControlCount - 1 do + if AControl.Controls[I] is TWinControl then + PaintControl(TWinControl(AControl.Controls[I])); + finally + RestoreDC(LCanvasDC, LSaved); + end; + end; +{$ENDIF} +begin + AForm.Repaint; + UpdateWindow(AForm.Handle); +{$IFDEF FPC} + LBitmap := Graphics.TBitmap.Create; +{$ELSE} + LBitmap := AForm.GetFormImage; +{$ENDIF} + try +{$IFDEF FPC} + LBitmap.PixelFormat := pf24bit; + LBitmap.SetSize(AForm.ClientWidth, AForm.ClientHeight); + LBitmap.Canvas.Lock; + try + LBitmap.Canvas.Brush.Color := AForm.GetRGBColorResolvingParent; + LBitmap.Canvas.FillRect(AForm.ClientRect); + LCanvasDC := LBitmap.Canvas.Handle; + PaintControl(AForm); + finally + LBitmap.Canvas.Unlock; + end; +{$ENDIF} + // Reject blank captures; native memo text must appear in the saved image. + LOrigin := AForm.ScreenToClient(AForm.Memo.ClientToScreen(Types.Point(0, 0))); + LLeft := Max(0, LOrigin.X + 2); + LTop := Max(0, LOrigin.Y + 2); + LRight := Min(LBitmap.Width - 1, LLeft + 240); + LBottom := Min(LBitmap.Height - 1, LTop + 120); + LLight := 0; + LDark := 0; + Y := LTop; + while Y <= LBottom do + begin + X := LLeft; + while X <= LRight do + begin + LColor := ColorToRGB(LBitmap.Canvas.Pixels[X, Y]); + if (GetRValue(LColor) > 220) and (GetGValue(LColor) > 220) and + (GetBValue(LColor) > 220) then + Inc(LLight); + if (GetRValue(LColor) < 100) and (GetGValue(LColor) < 100) and + (GetBValue(LColor) < 100) then + Inc(LDark); + Inc(X, 2); + end; + Inc(Y, 2); + end; + Log(Format('PIXELS %s memo_light=%d memo_dark=%d', [FCaseName, LLight, LDark])); + Require((LLight > 100) and (LDark > 20), 'native memo capture is blank or lacks operation text'); + LBitmap.SaveToFile(ChangeFileExt(FLogPath, '.' + FCaseName + '.bmp')); + finally + LBitmap.Free; + end; +end; + +procedure TOperationMessageChecks.SendAction(AForm: TFRMMemoText); +var + LButton: TBitBtn; + LTarget: HWND; + LKey: WPARAM; +begin + case FAction of + maNo: SendMessage(AForm.bbCancel.Handle, BM_CLICK, 0, 0); + maYes: + begin + LButton := AForm.FindComponent('bbConfirmYes') as TBitBtn; + SendMessage(LButton.Handle, BM_CLICK, 0, 0); + end; + maClose: PostMessage(AForm.Handle, WM_CLOSE, 0, 0); + maEnter, maEscape: + begin + if FAction = maEnter then + LKey := VK_RETURN + else + LKey := VK_ESCAPE; + if Assigned(AForm.ActiveControl) then + LTarget := AForm.ActiveControl.Handle + else + LTarget := AForm.Handle; + PostMessage(LTarget, WM_KEYDOWN, LKey, 0); + PostMessage(LTarget, WM_KEYUP, LKey, 0); + end; + end; +end; + +procedure TOperationMessageChecks.LogHandles(AForm: TFRMMemoText; const AStage: String); +var + LFormHandle, LMemoHandle: HWND; + LYes: TComponent; + + procedure LogControl(AControl: TWinControl); + var + LNative, LClient: TRect; + LHandle: HWND; + begin + LNative := Types.Rect(0, 0, 0, 0); + LClient := Types.Rect(0, 0, 0, 0); + LHandle := 0; + if AControl.HandleAllocated then + begin + LHandle := AControl.Handle; + GetWindowRect(LHandle, LNative); + GetClientRect(LHandle, LClient); + end; + Log(Format('BOUNDS %s %s %s logical=%d,%d,%d,%d client=%dx%d native=%d,%d,%d,%d native_client=%d,%d,%d,%d hwnd=%s anchors=%d%d%d%d', + [FCaseName, AStage, AControl.Name, AControl.Left, AControl.Top, + AControl.Width, AControl.Height, AControl.ClientWidth, AControl.ClientHeight, + LNative.Left, LNative.Top, LNative.Right, LNative.Bottom, + LClient.Left, LClient.Top, LClient.Right, LClient.Bottom, + IntToHex(NativeUInt(LHandle), SizeOf(Pointer) * 2), + Ord(akLeft in AControl.Anchors), Ord(akTop in AControl.Anchors), + Ord(akRight in AControl.Anchors), Ord(akBottom in AControl.Anchors)])); + end; + +begin + LFormHandle := 0; + LMemoHandle := 0; + if AForm.HandleAllocated then + LFormHandle := AForm.Handle; + if AForm.Memo.HandleAllocated then + LMemoHandle := AForm.Memo.Handle; + Log(Format('HANDLES %s %s form=%s memo=%s processing=%s', + [FCaseName, AStage, IntToHex(NativeUInt(LFormHandle), SizeOf(Pointer) * 2), + IntToHex(NativeUInt(LMemoHandle), SizeOf(Pointer) * 2), BoolToStr(FProcessing, True)])); + LogControl(AForm); + LogControl(AForm.pnlBottom); + LogControl(AForm.bbCancel); + LYes := AForm.FindComponent('bbConfirmYes'); + if LYes is TWinControl then + LogControl(TWinControl(LYes)); +end; + +procedure TOperationMessageChecks.TimerTick(Sender: TObject); +var + I: Integer; + LForm: TFRMMemoText; + LWork, LWindow: TRect; + LWidth, LHeight, LFrameWidth, LFrameHeight: Integer; +begin + if FProcessing then + Exit; + FProcessing := True; + try + try + if GetTickCount64 >= FDeadline then + raise Exception.Create(FCaseName + ': timed out observing or closing the actual dialog'); + if FActionSent then + Exit; + LForm := nil; + for I := 0 to Screen.FormCount - 1 do + if (Screen.Forms[I] is TFRMMemoText) and Screen.Forms[I].Visible and + (Screen.Forms[I].Caption = FTitle) then + begin + LForm := TFRMMemoText(Screen.Forms[I]); + Break; + end; + if not Assigned(LForm) then + Exit; + Require(not Assigned(FObserved), 'dialog was observed twice before its lifetime ended'); + FPhase := 'observed native modal'; + FObserved := LForm; + LForm.FreeNotification(Self); + Inc(FObservedCount); + FPhase := 'initial layout'; + CheckLayout(LForm, True); + FPhase := 'owned form scaling'; + LogHandles(LForm, 'before scaling'); + GetWindowRect(LForm.Handle, LWindow); + // Measure before scaling: a small monitor may cap the native HWND while + // LCL still holds the larger pending logical size until its resize event. + LFrameWidth := Max(0, LWindow.Right - LWindow.Left - LForm.Width); + LFrameHeight := Max(0, LWindow.Bottom - LWindow.Top - LForm.Height); + if FScaled then + LForm.ScaleBy(3, 2); + LogHandles(LForm, 'after scaling'); + if FScaled or FSmall or FConstrained then + begin + LWork := LForm.Monitor.WorkareaRect; + if FConstrained then + begin + LWork.Right := Min(LWork.Right, LWork.Left + 1024); + LWork.Bottom := Min(LWork.Bottom, LWork.Top + 720); + end; + // A form's logical size is client-only in LCL, but outer-sized in VCL. + // Preserve the full native frame when resizing this owned test dialog. + LWidth := Min(LForm.Width, + Max(1, LWork.Right - LWork.Left - LFrameWidth)); + LHeight := Min(LForm.Height, + Max(1, LWork.Bottom - LWork.Top - LFrameHeight)); + if FSmall then + begin + LWidth := Min(LWidth, 900); + LHeight := Min(LHeight, 650); + end; + // Scale only this owned form. The monitor/workarea and desktop stay intact. + LogHandles(LForm, 'before owned bounds'); + LForm.SetBounds(LWork.Left + + (LWork.Right - LWork.Left - LWidth - LFrameWidth) div 2, + LWork.Top + (LWork.Bottom - LWork.Top - LHeight - LFrameHeight) div 2, + LWidth, LHeight); + // LCL posts OnResize through its ordinary delayed-event queue. Let that + // layout finish while the observer guard prevents nested test actions. + Application.ProcessMessages; + CheckSynchronize(0); + end; + LogHandles(LForm, 'after owned bounds'); + FPhase := 'final layout'; + CheckLayout(LForm, False); + FPhase := 'native capture'; + if FCapture then + Capture(LForm); + if FMode = mmInput then + begin + LForm.Memo.SelStart := Length(LForm.Memo.Text); + LForm.Memo.SelText := ' local edit'; + Require(Pos('local edit', LForm.Memo.Text) > 0, 'legacy input memo is not editable'); + end; + FPhase := 'native action'; + FActionSent := True; + SendAction(LForm); + except + on E: Exception do + OnException(Self, E); + end; + finally + FProcessing := False; + end; +end; + +procedure TOperationMessageChecks.RunCase(const AName: String; + AMode: TMessageMode; AAction: TMessageAction; AScaled, ASmall, ACapture: Boolean; + AConstrained: Boolean); +var + LResult: Boolean; + LInput: String; + LLegacy: TFRMMemoText; +begin + FCaseName := AName; + FPhase := 'before public dialog helper'; + Log('BEGIN ' + AName + ' parent=' + FParent.ClassName + + ' parent_handle=' + BoolToStr(FParent.HandleAllocated, True) + + ' chars=' + IntToStr(Length(FDetails))); + FMode := AMode; + if AMode = mmResult then + FPrompt := 'Successfully executed 267 operations.' + sLineBreak + 'Total fee: 0.0267 PASC' + else + FPrompt := 'Execute 267 operations?' + sLineBreak + + 'Total fee: 0.0267 PASC' + sLineBreak + + 'These operations will be transmitted to the network.'; + FAction := AAction; + FScaled := AScaled; + FSmall := ASmall; + FCapture := ACapture; + FConstrained := AConstrained; + FTitle := Application.Title + ' - ' + AName; + FError := ''; + FActionSent := False; + FObservedCount := 0; + FDeadline := GetTickCount64 + 10000; + FTimer.Enabled := True; + try + case AMode of + mmConfirm: + begin + if AScaled then + LResult := ConfirmMemoQuery(nil, FTitle, FPrompt, FDetails) + else + LResult := ConfirmMemoQuery(FParent, FTitle, FPrompt, FDetails); + Require(LResult = (AAction = maYes), 'confirmation returned an incorrect approval result'); + end; + mmResult: + ShowResultMemo(FParent, FTitle, FPrompt, FDetails); + mmInput: + begin + LInput := 'Original editable text'; + Require(not InputMemoQuery(FTitle, True, LInput), 'cancelled legacy input was accepted'); + Require(LInput = 'Original editable text', 'cancelled legacy input changed its caller value'); + end; + mmLegacyReadOnly: + begin + LLegacy := TFRMMemoText.Create(FParent); + try + LLegacy.InitData(FTitle, 'Legacy readonly information'); + LLegacy.ShowModal; + finally + LLegacy.Free; + end; + end; + end; + finally + FTimer.Enabled := False; + end; + Require(FError = '', FError); + Require(FObservedCount = 1, 'expected exactly one actual dialog lifetime'); + Require(FActionSent, 'no native dialog action was dispatched'); + Require(not Assigned(FObserved), 'dialog outlived its public helper'); + Require(not TNode.NodeExists, 'a message-only check unexpectedly created a node'); + Log('PASS ' + AName); +end; + +procedure TOperationMessageChecks.Run; +begin + Log('Native operation messages: 267 long lines; no wallet, signing, broadcast or clipboard writes.'); + try + RunCase('confirm-no-100', mmConfirm, maNo, False, False, True); + RunCase('confirm-default-enter-100', mmConfirm, maEnter, False, False, False); + RunCase('confirm-escape-150', mmConfirm, maEscape, True, False, False); + RunCase('confirm-close-150', mmConfirm, maClose, True, False, False); + RunCase('confirm-yes-100', mmConfirm, maYes, False, False, False); + RunCase('confirm-yes-150', mmConfirm, maYes, True, False, True); + RunCase('confirm-small-owned-150', mmConfirm, maNo, True, True, True); + RunCase('confirm-constrained-100', mmConfirm, maNo, False, False, True, True); + RunCase('confirm-constrained-150', mmConfirm, maNo, True, False, True, True); + RunCase('result-close-100', mmResult, maNo, False, False, True); + RunCase('result-enter-150', mmResult, maEnter, True, False, True); + RunCase('legacy-input-cancel', mmInput, maClose, False, False, False); + RunCase('legacy-readonly-close', mmLegacyReadOnly, maNo, False, False, False); + Log('PASS all 13 native operation-message cases'); + except + on E: Exception do + begin + LogException(E); + Log('FAIL ' + E.ClassName + ': ' + E.Message); + raise; + end; + end; +end; + +procedure CheckWalletOperationMessages(AParent: TForm; const ALogPath: String); +var + LBag: TDisposables; + LChecks: TOperationMessageChecks; + LHost: TOperationMessageHost; +{$IFDEF FPC} + LPreviousFrameCount: LongInt; +{$ENDIF} +begin + if TNode.NodeExists then + raise Exception.Create('Operation message checks require a node-free host'); + if not Assigned(AParent) then + begin + if not Assigned(Application.MainForm) then + begin + Application.CreateForm(TOperationMessageHost, LHost); + AParent := LHost; + end + else + AParent := Application.MainForm; + end; + LChecks := LBag.AddObject(TOperationMessageChecks.Create(AParent, ALogPath)) as TOperationMessageChecks; +{$IFDEF FPC} + // Keep the complete native creation caller chain in this owned QA process. + LPreviousFrameCount := System.RaiseMaxFrameCount; + System.RaiseMaxFrameCount := 64; + try +{$ENDIF} + LChecks.Run; +{$IFDEF FPC} + finally + System.RaiseMaxFrameCount := LPreviousFrameCount; + end; +{$ENDIF} +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletSelectionLoadingChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletSelectionLoadingChecks.pas new file mode 100644 index 000000000..846e2d8f9 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletSelectionLoadingChecks.pas @@ -0,0 +1,480 @@ +unit UWalletSelectionLoadingChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Uses the real native account selection, wallet source binding and grid queue. + Only the persisted account/history records come from the existing mock model. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UWalletGridMocks; + +procedure CheckWalletSelectionLoadingAndBanner(AMain: TMockMainForm; const ALogPath: String); +procedure CheckWalletSelectionCompletion(AMain: TMockMainForm; const ALogPath: String); +procedure CheckWalletRefreshLifecycle(AMain: TMockMainForm; const ALogPath: String); + +implementation + +uses Windows, Messages, Classes, SysUtils, Types, Variants, Forms, Controls, + StdCtrls, ExtCtrls, Graphics, UVisualGrid, UCommon.UI, UNode, + UUserInterface, UCTRLBanner, UCTRLSyncronization, UAccounts; + +procedure Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure PumpFor(AMilliseconds: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure Log(const ALogPath, AMessage: String); +var LLines: TStringList; +begin + LLines := TStringList.Create; + try + if FileExists(ALogPath) then LLines.LoadFromFile(ALogPath); + LLines.Add(FormatDateTime('hh:nn:ss.zzz',Now)+' '+AMessage); + LLines.SaveToFile(ALogPath); + finally LLines.Free; end; +end; + +procedure ClickAccount(AMain: TMockMainForm; ARow: Integer); +var LCell: TRect; LPoint: LPARAM; +begin + LCell := AMain.MockWallet.AccountsGrid.InternalDrawGrid.CellRect(0,ARow+1); + LPoint := LPARAM(((LCell.Top+LCell.Bottom) div 2) shl 16) or + LPARAM((LCell.Left+LCell.Right) div 2); + SendMessage(AMain.MockWallet.AccountsGrid.InternalDrawGrid.Handle, + WM_MOUSEMOVE,0,LPoint); + SendMessage(AMain.MockWallet.AccountsGrid.InternalDrawGrid.Handle, + WM_LBUTTONDOWN,MK_LBUTTON,LPoint); + SendMessage(AMain.MockWallet.AccountsGrid.InternalDrawGrid.Handle, + WM_LBUTTONUP,0,LPoint); +end; + +procedure AwaitGrid(AGrid: TVisualGrid; AWaitForQuiet: Boolean); +var LUntil: UInt64; +begin + LUntil := GetTickCount64+10000; + while (AGrid.IsUpdating or (AWaitForQuiet and AGrid.RefreshThrottle.Waiting)) and + (GetTickCount64<LUntil) do PumpFor(5); + Require(not AGrid.IsUpdating,'Native provider did not finish'); + if AWaitForQuiet then Require(not AGrid.RefreshThrottle.Waiting,'Native throttle did not finish'); +end; + +procedure CheckBanner(AMain: TMockMainForm; const ALogPath: String); +var LBanner: TCTRLBanner; I, LLeft, LRight, LWidth: Integer; + LImage, LWithoutLabel: TBitmap; LOriginalWidth, X, Y, LChangedPixels: Integer; + LOriginalNetworkActive: Boolean; LPeer: TObject; LUntil: UInt64; + + function PaintBanner: TBitmap; + begin + Result := TBitmap.Create; + Result.PixelFormat := pf24bit; + Result.SetSize(LBanner.ClientWidth,LBanner.ClientHeight); + Result.Canvas.Brush.Color := clFuchsia; + Result.Canvas.FillRect(Types.Rect(0,0,Result.Width,Result.Height)); + // LCL GetFormImage adds PRF_CHECKVISIBLE and can return black on an owned + // inactive desktop. VCL PaintTo additionally paints graphic children which + // its plain WM_PRINT handler omits. Both execute actual control painting. + {$IFDEF FPC} + SendMessage(LBanner.Handle,WM_PRINT,WPARAM(Result.Canvas.Handle), + PRF_CLIENT or PRF_CHILDREN or PRF_ERASEBKGND); + {$ELSE} + LBanner.PaintTo(Result.Canvas.Handle,0,0); + {$ENDIF} + end; + + procedure CheckBounds(const ACheckpoint: String); + begin + LBanner.FormResize(LBanner); + Require(LBanner.NodeStatusLabel.Parent=LBanner,ACheckpoint+': label is not a banner child'); + LLeft := LBanner.lblClientVersion.Left+LBanner.lblClientVersion.Width+LBanner.imLogo.Left; + LRight := LBanner.cbLanguage.Left-LBanner.imLogo.Left; + LWidth := LBanner.NodeStatusLabel.Width; + Require(LBanner.NodeStatusLabel.Visible and (LWidth>0),ACheckpoint+': status label is not visible'); + Require((LBanner.NodeStatusLabel.Left>=LLeft) and + (LBanner.NodeStatusLabel.Left+LWidth<=LRight),ACheckpoint+': status overlaps brand or language selector'); + Require((LBanner.NodeStatusLabel.Top>=0) and + (LBanner.NodeStatusLabel.Top+LBanner.NodeStatusLabel.Height<=LBanner.ClientHeight), + ACheckpoint+': status extends outside banner'); + Require(LBanner.NodeStatusLabel.Left=LLeft,ACheckpoint+': status must start after the brand'); + Require(LBanner.NodeStatusLabel.Alignment=taLeftJustify, + ACheckpoint+': status text must be left-aligned'); + if TUserInterface.State=uisActive then + Require(LBanner.NodeStatusLabel.Caption='',ACheckpoint+': normal online status must be blank') + else + Require(LBanner.NodeStatusLabel.Caption=TCTRLSyncronization.CurrentNodeStatusText, + ACheckpoint+': banner text disagrees with synchronization presentation'); + Require(LBanner.NodeStatusLabel.Font.Color=clWhite,ACheckpoint+': status must be white'); + Require(LBanner.NodeStatusLabel.Font.Size>=16,ACheckpoint+': status font must be at least 16 points'); + Log(ALogPath,Format('BANNER %s client=%d label=%d,%d,%d,%d caption=%s', + [ACheckpoint,LBanner.ClientWidth,LBanner.NodeStatusLabel.Left,LBanner.NodeStatusLabel.Top, + LBanner.NodeStatusLabel.Width,LBanner.NodeStatusLabel.Height,LBanner.NodeStatusLabel.Caption])); + end; + +begin + LBanner := nil; + for I := 0 to AMain.ComponentCount-1 do + if AMain.Components[I] is TCTRLBanner then LBanner := TCTRLBanner(AMain.Components[I]); + Require(Assigned(LBanner),'Actual main form has no banner'); + LOriginalNetworkActive := TUserInterface.Node.GetSynchronizationStatus.NetworkActive; + Require(not LOriginalNetworkActive,'Banner fixture must start with its owned offline node'); + LPeer := TObject.Create; + try + // Publish real cached node status without opening sockets. Assigning the + // UI state directly is invalid: the details view restores offline status. + TUserInterface.Node.SetSynchronizationNetworkActive(True); + TUserInterface.Node.SetSynchronizationPeerConnected(LPeer,True); + LUntil := GetTickCount64+3000; + while (TUserInterface.State<>uisActive) and (GetTickCount64<LUntil) do PumpFor(5); + Require(TUserInterface.State=uisActive,'Connected idle node did not publish online state'); + Require(LBanner.NodeStatusLabel.Caption='', + 'Normal online transition did not clear the banner'); + Require(TCTRLSyncronization.CurrentNodeStatusText='Active', + 'Banner-only suppression changed the synchronization details'); + finally + TUserInterface.Node.SetSynchronizationPeerConnected(LPeer,False); + TUserInterface.Node.SetSynchronizationNetworkActive(LOriginalNetworkActive); + LPeer.Free; + PumpFor(250); + end; + Log(ALogPath,'PASS normal online state clears only the banner; status is white, left-aligned and enlarged'); + CheckBounds('normal'); + LOriginalWidth := AMain.Width; + AMain.Width := 1640; + PumpFor(100); + CheckBounds('wide'); + AMain.Width := 1040; + PumpFor(100); + CheckBounds('narrow'); + AMain.Width := LOriginalWidth; + LBanner.ScaleBy(144,96); + LBanner.FormResize(LBanner); + PumpFor(100); + CheckBounds('144dpi'); + LBanner.ScaleBy(96,144); + LBanner.FormResize(LBanner); + PumpFor(100); + CheckBounds('96dpi-return'); + + TUserInterface.Node.BeginBlockChainUpdate; + try + PumpFor(250); + Require(LBanner.NodeStatusLabel.Caption='Synchronizing', + 'Banner did not immediately follow the actual node synchronization event'); + CheckBounds('synchronizing-event'); + finally TUserInterface.Node.EndBlockChainUpdate; end; + PumpFor(250); + CheckBounds('synchronization-finished-event'); + Require(LBanner.NodeStatusLabel.Caption<>'Synchronizing', + 'Banner retained synchronization after the actual empty scope finished'); + LImage := PaintBanner; + LWithoutLabel := nil; + try + LBanner.NodeStatusLabel.Visible := False; + try LWithoutLabel := PaintBanner; + finally LBanner.NodeStatusLabel.Visible := True; end; + LChangedPixels := 0; + Y := LBanner.NodeStatusLabel.Top; + while Y<LBanner.NodeStatusLabel.Top+LBanner.NodeStatusLabel.Height do begin + X := LBanner.NodeStatusLabel.Left; + while X<LBanner.NodeStatusLabel.Left+LBanner.NodeStatusLabel.Width do begin + if LImage.Canvas.Pixels[X,Y]<>LWithoutLabel.Canvas.Pixels[X,Y] then Inc(LChangedPixels); + Inc(X,2); + end; + Inc(Y,2); + end; + LImage.SaveToFile(ChangeFileExt(ALogPath,'.banner-native.bmp')); + LWithoutLabel.SaveToFile(ChangeFileExt(ALogPath,'.banner-without-label.bmp')); + Require(LChangedPixels>5,'Native status text did not paint; blank/black captures are not visual proof'); + finally LWithoutLabel.Free; LImage.Free; end; + Log(ALogPath,'PASS banner native parent, left-aligned bounds, white enlarged text, normal/wide/narrow and both DPI directions; event-owned caption; label pixels='+IntToStr(LChangedPixels)); +end; + +procedure CheckOverviewLoading(AMain: TMockMainForm; const ALogPath: String); +var LUntil: UInt64; LPreviousPASC,LPreviousPASA: String; + + procedure AwaitTotal(const AExpected: String); + begin + LUntil := GetTickCount64+9000; + while (AMain.MockWallet.lblTotalPASA.Caption<>AExpected) and + (GetTickCount64<LUntil) do PumpFor(5); + Require(AMain.MockWallet.lblTotalPASA.Caption=AExpected,'Completed overview did not publish '+AExpected); + end; + +begin + Require((AMain.MockWallet.lblTotalPASC.Caption='-') and + (AMain.MockWallet.lblTotalPASA.Caption='-'), + 'Overview displayed a number before its first completed balance snapshot'); + Log(ALogPath,'PASS initial Total PASC/PASA remain dashes while the real first summary worker is held'); + AMain.Model.ReleaseBalanceSnapshots; + AwaitTotal('200'); + LPreviousPASC := AMain.MockWallet.lblTotalPASC.Caption; + LPreviousPASA := AMain.MockWallet.lblTotalPASA.Caption; + Require(LPreviousPASC<>'-','First completed nonzero balance remained unknown'); + AMain.Model.HoldBalanceSnapshots; + try + AMain.Model.SetData(6000,0); + TUserInterface.Node.NotifyBlocksChanged; + PumpFor(100); + AwaitGrid(AMain.MockWallet.AccountsGrid,True); + AwaitGrid(AMain.MockWallet.OperationsGrid,True); + Require((AMain.MockWallet.lblTotalPASC.Caption=LPreviousPASC) and + (AMain.MockWallet.lblTotalPASA.Caption=LPreviousPASA), + 'A newer held summary replaced the last completed nonzero totals'); + Log(ALogPath,'PASS completed nonzero overview is retained while a newer real summary worker is held'); + finally AMain.Model.ReleaseBalanceSnapshots; end; + AwaitTotal('0'); + Require(AMain.MockWallet.lblTotalPASC.Caption=TAccountComp.FormatMoney(0), + 'A genuine completed zero balance was displayed as unknown'); + Log(ALogPath,'PASS an actual completed empty summary may publish numeric zero'); + AMain.Model.SetData(6001,200); + TUserInterface.Node.NotifyBlocksChanged; + PumpFor(100); + AwaitGrid(AMain.MockWallet.AccountsGrid,True); + AwaitGrid(AMain.MockWallet.OperationsGrid,True); + AwaitTotal('200'); +end; + +procedure RequireSelectedAccount(AMain: TMockMainForm; AAccount: Cardinal); +var + I: Integer; + LAccounts: TArray<Cardinal>; +begin + LAccounts := AMain.MockWallet.OperationsSource.Accounts; + Require(not AMain.MockWallet.OperationsSource.UseWalletAccounts and + (Length(LAccounts) = 1) and (LAccounts[0] = AAccount), + 'Actual selection did not bind precisely its selected account'); + Require(AMain.MockWallet.OperationsGrid.RowCount > 0, + 'The selected account has no accepted native history rows'); + for I := 0 to AMain.MockWallet.OperationsGrid.RowCount - 1 do + Require(Cardinal(AMain.MockWallet.OperationsGrid.Rows[I].AccountNumber) = AAccount, + 'Accepted history belongs to an obsolete account selection'); +end; + +procedure CheckWalletSelectionCompletion(AMain: TMockMainForm; const ALogPath: String); +var + LGrid: TVisualGrid; + LPanel: TPanel; + LCircle: TLoadingCircle; + LBefore, LAccount: Integer; + LStarted, LUntil: UInt64; +begin + LGrid := AMain.MockWallet.OperationsGrid; + AwaitGrid(AMain.MockWallet.AccountsGrid, True); + AwaitGrid(LGrid, True); + Require((AMain.MockWallet.AccountsGrid.RefreshThrottle.Mode = temNone) and + (LGrid.RefreshThrottle.Mode = temNotifyOnEventBurstFinished) and + (LGrid.RefreshThrottle.Interval.TotalMilliseconds=250), + 'Accounts must admit immediately; operations require 250 ms burst silence'); + AMain.MockWallet.OperationsSource.OperationsPerAccount := 268; + AMain.Model.DelayMilliseconds := 0; + LPanel := TPanel(LGrid.FindComponent('LoadingOverlay')); + LCircle := TLoadingCircle(LGrid.FindComponent('LoadingCircle')); + for LAccount := 0 to 1 do + begin + LBefore := AMain.Model.Metrics(mdkOperations).FetchCount; + LStarted := GetTickCount64; + AMain.Model.GateFetches := True; + try + ClickAccount(AMain, LAccount); + PumpFor(100); + Require(LGrid.RefreshThrottle.Waiting and + (AMain.Model.Metrics(mdkOperations).FetchCount=LBefore), + 'Native selection queried operations before the 250 ms quiet interval'); + LUntil := GetTickCount64 + 1000; + while (AMain.Model.Metrics(mdkOperations).FetchCount = LBefore) and + (GetTickCount64 < LUntil) do + PumpFor(5); + Require(LGrid.IsUpdating and + (AMain.Model.Metrics(mdkOperations).FetchCount = LBefore + 1), + 'Native selection did not admit exactly one provider after its quiet interval'); + Require(GetTickCount64-LStarted>=250,'Native selection bypassed operations quiet admission'); + PumpFor(250); + Require(LPanel.Visible and LCircle.Active, + 'A genuinely held selected-account query has no animated loading feedback'); + Require(not LGrid.RefreshThrottle.Waiting, 'The operations quiet timer did not finish before its held provider ran'); + finally + AMain.Model.ReleaseFetches; + end; + AwaitGrid(LGrid, True); + Require(GetTickCount64 - LStarted < 2000, + 'Fast selected-account history waited for an obsolete throttle interval'); + RequireSelectedAccount(AMain, Cardinal(LAccount)); + Require(not LPanel.Visible and not LCircle.Active, + 'Accepted selected-account rows retained automatic loading'); + PumpFor(50); + Require((AMain.Model.Metrics(mdkOperations).FetchCount = LBefore + 1) and + not LPanel.Visible and not LCircle.Active, + 'Selection completion duplicated its query or reopened loading'); + Log(ALogPath, Format('PASS native account=%d after 250 ms quiet: 268 operations, accepted page=%d rows, completion=%dms, loading cleared on publication', + [LAccount, LGrid.RowCount, GetTickCount64 - LStarted])); + end; +end; + +procedure CheckWalletRefreshLifecycle(AMain: TMockMainForm; const ALogPath: String); +var + A, O: TVisualGrid; + LA, LO: TMockFetchMetrics; + LBeforeA, LBeforeO, LRevision, I: Integer; + LStart, LUntil: UInt64; + LScopeActive: Boolean; + + function CurrentRevisionVisible: Boolean; + var + LPrefix: String; + begin + Result := False; + if A.IsUpdating or O.IsUpdating or A.RefreshThrottle.Waiting or O.RefreshThrottle.Waiting or + (A.RowCount = 0) or (O.RowCount = 0) then + Exit; + LPrefix := 'mock revision=' + IntToStr(LRevision) + ' '; + Result := (Pos(LPrefix, String(A.Rows[0].Name)) = 1) and + (Pos(LPrefix, String(O.Rows[0].Description)) = 1); + end; + + procedure AwaitRevision; + begin + LUntil := GetTickCount64 + 3000; + while not CurrentRevisionVisible and (GetTickCount64 < LUntil) do + PumpFor(5); + Require(CurrentRevisionVisible, 'Real wallet grids did not publish the latest backend revision'); + Require(not A.RefreshThrottle.Waiting and not O.RefreshThrottle.Waiting, + 'The current pages did not settle after their configured operations quiet interval'); + end; + + procedure RequireSyncCards; + begin + Require(A.RefreshSuspended and O.RefreshSuspended and A.LoadingVisible and O.LoadingVisible, + 'Actual node synchronization did not own both grid pauses/cards'); + Require((TLabel(A.FindComponent('LoadingStatus')).Caption = 'Please Wait') and + (TLabel(O.FindComponent('LoadingStatus')).Caption = 'Please Wait') and + (TLabel(A.FindComponent('LoadingSubtitle')).Caption = 'Synchronizing') and + (TLabel(O.FindComponent('LoadingSubtitle')).Caption = 'Synchronizing'), + 'Node-owned synchronization card text was replaced by ordinary loading'); + end; + +begin + A := AMain.MockWallet.AccountsGrid; + O := AMain.MockWallet.OperationsGrid; + AMain.Model.ReleaseFetches; + AMain.Model.DelayMilliseconds := 0; + AMain.MockWallet.OperationsSource.OperationsPerAccount := 268; + AwaitGrid(A, False); + AwaitGrid(O, False); + Require((A.RefreshThrottle.Mode = temNone) and (O.RefreshThrottle.Mode = temNotifyOnEventBurstFinished) and + (O.RefreshThrottle.Interval.TotalMilliseconds=250), + 'Lifecycle checks require immediate accounts and 250 ms quiet operations'); + LBeforeA := AMain.Model.Metrics(mdkAccounts).FetchCount; + LBeforeO := AMain.Model.Metrics(mdkOperations).FetchCount; + AMain.Model.Advance(200); + LRevision := AMain.Model.Revision; + LStart := GetTickCount64; + TUserInterface.Node.NotifyBlocksChanged; + AwaitRevision; + Require(GetTickCount64 - LStart < 2000, 'An idle backend event waited for five-second admission'); + Require((AMain.Model.Metrics(mdkAccounts).FetchCount > LBeforeA) and + (AMain.Model.Metrics(mdkOperations).FetchCount > LBeforeO), + 'An idle backend event did not reach both production providers'); + Log(ALogPath, 'PASS idle backend event refreshes accounts immediately and operations after 250 ms quiet'); + + // Ordinary backend inputs coalesce while one real provider is held. They do + // not start parallel workers, and completion admits one latest pending page. + LBeforeA := AMain.Model.Metrics(mdkAccounts).FetchCount; + LBeforeO := AMain.Model.Metrics(mdkOperations).FetchCount; + AMain.Model.GateFetches := True; + try + AMain.Model.Advance(200); + TUserInterface.Node.NotifyBlocksChanged; + LUntil := GetTickCount64 + 2000; + repeat + PumpFor(5); + LA := AMain.Model.Metrics(mdkAccounts); + LO := AMain.Model.Metrics(mdkOperations); + until ((LA.ActiveFetches = 1) and (LO.ActiveFetches = 1)) or (GetTickCount64 >= LUntil); + Require((LA.ActiveFetches = 1) and (LO.ActiveFetches = 1), 'Both actual providers were not held'); + for I := 1 to 4 do + begin + AMain.Model.Advance(200); + TUserInterface.Node.NotifyBlocksChanged; + PumpFor(60); + end; + LRevision := AMain.Model.Revision; + PumpFor(150); + Require((AMain.Model.Metrics(mdkAccounts).FetchCount = LBeforeA + 1) and + (AMain.Model.Metrics(mdkOperations).FetchCount = LBeforeO + 1), + 'Backend input started another worker before the held provider retired'); + finally + AMain.Model.ReleaseFetches; + end; + AwaitRevision; + PumpFor(100); + Require((AMain.Model.Metrics(mdkAccounts).FetchCount = LBeforeA + 2) and + (AMain.Model.Metrics(mdkOperations).FetchCount = LBeforeO + 2), + 'Held backend inputs did not coalesce to exactly one latest replay'); + Log(ALogPath, 'PASS held old providers retain one latest backend request after operations quiet admission without parallel queries'); + + LScopeActive := False; + try + TUserInterface.Node.BeginBlockChainUpdate; + LScopeActive := True; + LUntil := GetTickCount64 + 2000; + while (not A.RefreshSuspended or not O.RefreshSuspended) and + (GetTickCount64 < LUntil) do + PumpFor(5); + RequireSyncCards; + LBeforeA := AMain.Model.Metrics(mdkAccounts).FetchCount; + LBeforeO := AMain.Model.Metrics(mdkOperations).FetchCount; + for I := 1 to 6 do + begin + AMain.Model.Advance(200); + TUserInterface.Node.NotifyBlocksChanged; + PumpFor(60); + RequireSyncCards; + Require((AMain.Model.Metrics(mdkAccounts).FetchCount = LBeforeA) and + (AMain.Model.Metrics(mdkOperations).FetchCount = LBeforeO), + 'A provider queried during the node-owned synchronization pause'); + end; + LRevision := AMain.Model.Revision; + O.PageIndex := 1; + PumpFor(100); + RequireSyncCards; + Require((AMain.Model.Metrics(mdkAccounts).FetchCount = LBeforeA) and + (AMain.Model.Metrics(mdkOperations).FetchCount = LBeforeO), + 'A paused page change queried its provider'); + Log(ALogPath, 'PASS synchronization lifecycle holds both cards and performs zero provider queries despite backend/page inputs'); + finally + if LScopeActive then + TUserInterface.Node.EndBlockChainUpdate; + end; + LStart := GetTickCount64; + AwaitRevision; + Require(GetTickCount64 - LStart < 2000, 'Node completion imposed an obsolete quiet interval'); + Require(not A.RefreshSuspended and not O.RefreshSuspended and not A.LoadingVisible and not O.LoadingVisible, + 'Node completion did not release the actual manual pauses/cards'); + Require(O.PageIndex = 1, 'Synchronization resume lost the latest requested page'); + Require(not TPanel(A.FindComponent('LoadingOverlay')).Visible and + not TPanel(O.FindComponent('LoadingOverlay')).Visible, + 'Current resumed pages retain an ordinary/manual/error loading card'); + PumpFor(200); + Log(ALogPath, 'PASS node completion publishes the latest revision/page, settles operations quiet admission and releases synchronization cards'); +end; + +procedure CheckWalletSelectionLoadingAndBanner(AMain: TMockMainForm; const ALogPath: String); +begin + CheckOverviewLoading(AMain, ALogPath); + CheckWalletSelectionCompletion(AMain, ALogPath); + CheckBanner(AMain, ALogPath); +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletSendPresentationChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletSendPresentationChecks.pas new file mode 100644 index 000000000..1f8737435 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletSendPresentationChecks.pas @@ -0,0 +1,1166 @@ +unit UWalletSendPresentationChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Exercises actual operation-wizard routes on fresh offline data, captures native + screens at two font scales, and always cancels before Finish or signing. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UWalletGridMocks; + +procedure CheckSendPascPresentation(AMain: TMockMainForm; const ALogPath: String); +procedure CheckChangeAccountInfoPresentation(AMain: TMockMainForm; const ALogPath: String); +procedure CheckChangeAccountInfoApproval(AMain: TMockMainForm; const ALogPath: String); + +implementation + +uses Windows, Messages, Classes, SysUtils, Types, Math, Forms, Controls, Graphics, + StdCtrls, ExtCtrls, Buttons, Menus, {$IFDEF FPC}LMessages,{$ENDIF} + UMemory, UNode, UBlockChain, UFileStorage, UAccounts, UWallet, UConst, + UUserInterface, UCoreObjects, UCoreUtils, UWizard, UVisualGrid, UWIZSendPASC, + UWIZOperationSelected, UWIZSendPASC_Details, UWIZOperationFee_Custom, + UWIZOperationPayload_Encryption, UWIZOperationPayload_Password, + UWIZOperationPayload_Content, UWIZOperationConfirmation, UWIZOperationSigner_Select, + UWIZOperation, UWIZChangeAccountInfo, UWIZChangeAccountInfo_Details, + UWIZChangeAccountInfo_Skipped, UCommon, UBaseTypes, UPCDataTypes, UCommon.Collections; + +type + TControlFontAccess = class(TControl) + public + property Font; + property Color; + {$IFDEF FPC} + property BaseBounds; + {$ENDIF} + end; + + TGroupBoxContentAccess = class(TGroupBox); + + TNativePixel = packed record + Blue, Green, Red: Byte; + end; + TNativePixelRow = array[0..32767] of TNativePixel; + PNativePixelRow = ^TNativePixelRow; + + TSendCase = (scDefault, scCustomFee, scPayloadPlain, scPayloadSender, + scPayloadRecipient, scPayloadPassword); + TOperationWizard = TWizard<TWIZOperationsModel>; + TInfoApprovalCase = (iacMixed, iacAllSkipped, iacNoSkipped, iacSingleName, iacFeeSelf, iacFeeCommon); + + TApprovalSafeBox = class(TPCSafeBox) + public + procedure BuildFunded(const AKey: TAccountKey); + end; + + TApprovalStorage = class(TFileStorage) + protected + function GetFirstBlockNumber: Int64; override; + function GetLastBlockNumber: Int64; override; + function DoLoadBlockChain(AOperations: TPCOperationsComp; ABlock: Cardinal): Boolean; override; + end; + + TSendPresentationChecks = class(TComponent) + private + FMain: TMockMainForm; + FTimer: TTimer; + FLog: TStringList; + FLogPath, FError: String; + FCase: TSendCase; + FDeadline: UInt64; + FCancelled, FInside: Boolean; + FChangeAccountInfo: Boolean; + FInfoApproval: Boolean; + FInfoCase: TInfoApprovalCase; + FExpectedAccountCount: Integer; + FPreviousException: TExceptionEvent; + procedure Require(ACondition: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure Pump(AMilliseconds: Cardinal); + procedure OnException(Sender: TObject; E: Exception); + procedure Tick(Sender: TObject); + procedure LoadGenesis; + procedure BuildApprovalBank; + procedure Advance(AWizard: TOperationWizard); + procedure CheckScreen(AWizard: TOperationWizard; AClass: TComponentClass; const AStep: String); + procedure CheckLayout(AParent: TWinControl; ACanvas: TCanvas; const ACheckpoint: String); + procedure CheckGridPainting(AGrid: TVisualGrid; const ACheckpoint: String); + procedure LogGeometry(AControl: TWinControl; const ACheckpoint: String); + procedure CheckText(AControl: TControl; ACanvas: TCanvas; const ACheckpoint: String); + procedure RunFlow(AWizard: TWIZSendPASCWizard); + procedure RunChangeInfoFlow(AWizard: TWIZChangeAccountInfoWizard); + procedure RunApprovalFlow(AWizard: TWIZChangeAccountInfoWizard); + procedure CheckInfoConfirmation(AWizard: TWIZChangeAccountInfoWizard; const AStep: String); + procedure CloseOwnedWizard; + function IsExpectedWizard(AOwner: TComponent): Boolean; + function CaseName: String; + function PaintNativeForm(AForm: TForm): TBitmap; + public + constructor Create(AMain: TMockMainForm; const ALogPath: String); reintroduce; + destructor Destroy; override; + procedure Run; + procedure RunChangeAccountInfo; + procedure RunChangeInfoApproval; + end; + +procedure TApprovalSafeBox.BuildFunded(const AKey: TAccountKey); +var + I: Integer; + LBlock: TOperationBlock; +begin + // Existing native fixtures build a mature in-memory bank through protected + // AddNew, without bypassing account eligibility or touching real wallet data. + for I := 0 to 159 do + begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I * CT_NewLineSecondsAvg; + LBlock.reward := TPascalCoinProtocol.GetRewardForNewLine(I); + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + DoUpgradeToProtocol6; +end; + +function TApprovalStorage.GetFirstBlockNumber: Int64; +begin + Result := 0; +end; + +function TApprovalStorage.GetLastBlockNumber: Int64; +begin + Result := Int64(Bank.BlocksCount) - 1; +end; + +function TApprovalStorage.DoLoadBlockChain(AOperations: TPCOperationsComp; ABlock: Cardinal): Boolean; +begin + Result := ABlock < Bank.BlocksCount; + if Result then + begin + AOperations.Clear(True); + AOperations.SetOperationBlock(Bank.SafeBox.GetBlockInfo(ABlock)); + end; +end; + +function PaintNativeControl(AControl: TWinControl): TBitmap; +var + LDC: HDC; +begin + Result := TBitmap.Create; + try + Result.PixelFormat := pf24bit; + Result.SetSize(AControl.ClientWidth, AControl.ClientHeight); + Result.Canvas.Lock; + try + {$IFDEF FPC} + Result.Canvas.Brush.Color := AControl.GetRGBColorResolvingParent; + {$ELSE} + Result.Canvas.Brush.Color := TControlFontAccess(AControl).Color; + {$ENDIF} + Result.Canvas.FillRect(AControl.ClientRect); + LDC := Result.Canvas.Handle; + {$IFDEF FPC} + AControl.Perform(LM_ERASEBKGND, WPARAM(LDC), 0); + AControl.Perform(LM_PAINT, WPARAM(LDC), 0); + {$ELSE} + AControl.PaintTo(LDC, 0, 0); + {$ENDIF} + if AControl is TCustomComboBox then + SendMessage(AControl.Handle, WM_PRINTCLIENT, WPARAM(LDC), PRF_CLIENT); + finally + Result.Canvas.Unlock; + end; + except + Result.Free; + raise; + end; +end; + +procedure TSendPresentationChecks.LogGeometry(AControl: TWinControl; const ACheckpoint: String); +var + LClient, LWindow: TRect; + LDC: HDC; + LPPI: Integer; + {$IFDEF FPC}LBase: TRect;{$ENDIF} +begin + LClient := AControl.ClientRect; + LWindow := AControl.BoundsRect; + LPPI := 0; + if AControl.HandleAllocated then begin + GetClientRect(AControl.Handle, LClient); + GetWindowRect(AControl.Handle, LWindow); + LDC := GetDC(AControl.Handle); + if LDC <> 0 then begin + LPPI := GetDeviceCaps(LDC, LOGPIXELSY); + ReleaseDC(AControl.Handle, LDC); + end; + end; + Log(Format('GEOMETRY %s %s:%s bounds=%d,%d,%d,%d logical_client=%d,%d native_client=%d,%d,%d,%d native_window=%d,%d,%d,%d align=%d constraints=%d,%d,%d,%d font_height=%d device_ppi=%d', + [ACheckpoint, AControl.Name, AControl.ClassName, AControl.Left, AControl.Top, + AControl.Width, AControl.Height, AControl.ClientWidth, AControl.ClientHeight, + LClient.Left, LClient.Top, LClient.Right, LClient.Bottom, + LWindow.Left, LWindow.Top, LWindow.Right, LWindow.Bottom, + Ord(AControl.Align), AControl.Constraints.MinWidth, AControl.Constraints.MinHeight, + AControl.Constraints.MaxWidth, AControl.Constraints.MaxHeight, + TControlFontAccess(AControl).Font.Height, LPPI])); + {$IFDEF FPC} + LBase := TControlFontAccess(AControl).BaseBounds; + Log(Format('BASE %s %s=%d,%d,%d,%d', [ACheckpoint, AControl.Name, + LBase.Left, LBase.Top, LBase.Right, LBase.Bottom])); + {$ENDIF} +end; + +constructor TSendPresentationChecks.Create(AMain: TMockMainForm; const ALogPath: String); +begin + inherited Create(nil); + FMain := AMain; + FExpectedAccountCount := 1; + FLogPath := ALogPath; + FLog := TStringList.Create; + FTimer := TTimer.Create(Self); + FTimer.Enabled := False; + FTimer.Interval := 20; + FTimer.OnTimer := Tick; + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TSendPresentationChecks.Destroy; +begin + FTimer.Enabled := False; + Application.OnException := FPreviousException; + FLog.Free; + inherited Destroy; +end; + +procedure TSendPresentationChecks.Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create(AMessage); +end; + +procedure TSendPresentationChecks.Log(const AMessage: String); +begin + FLog.Add(FormatDateTime('hh:nn:ss.zzz', Now) + ' ' + AMessage); + FLog.SaveToFile(FLogPath); +end; + +function TSendPresentationChecks.CaseName: String; +begin + if FInfoApproval then + begin + case FInfoCase of + iacMixed: Result := 'mixed'; + iacAllSkipped: Result := 'all-skipped'; + iacNoSkipped: Result := 'no-skipped'; + iacSingleName: Result := 'single-name'; + iacFeeSelf: Result := 'fee-self'; + iacFeeCommon: Result := 'fee-common'; + end; + Exit; + end; + if FChangeAccountInfo then begin + if FExpectedAccountCount = 1 then + Result := 'single' + else + Result := 'batch'; + Exit; + end; + case FCase of + scDefault: Result := 'default'; + scCustomFee: Result := 'custom-fee'; + scPayloadPlain: Result := 'payload-plain'; + scPayloadSender: Result := 'payload-sender'; + scPayloadRecipient: Result := 'payload-recipient'; + scPayloadPassword: Result := 'payload-password'; + end; +end; + +procedure TSendPresentationChecks.Pump(AMilliseconds: Cardinal); +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Require(FError = '', FError); + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TSendPresentationChecks.CloseOwnedWizard; +var + I: Integer; + LMessage: String; +begin + for I := Screen.FormCount - 1 downto 0 do + if (Screen.Forms[I] is TWizardHostForm) and + IsExpectedWizard(Screen.Forms[I].Owner) then + TOperationWizard(Screen.Forms[I].Owner).Cancel(LMessage); +end; + +function TSendPresentationChecks.IsExpectedWizard(AOwner: TComponent): Boolean; +begin + if FChangeAccountInfo then + Result := AOwner is TWIZChangeAccountInfoWizard + else + Result := AOwner is TWIZSendPASCWizard; +end; + +procedure TSendPresentationChecks.OnException(Sender: TObject; E: Exception); +begin + if FError = '' then + FError := E.ClassName + ': ' + E.Message; + CloseOwnedWizard; +end; + +procedure TSendPresentationChecks.LoadGenesis; +var + LBag: TDisposables; + LStream: TFileStream; + LBlock: TPCOperationsComp; + LCount: Cardinal; + LMessage: String; + LUntil: UInt64; +begin + Require(TUserInterface.Offline, 'Send presentation requires the private offline startup'); + TNode.Node.BroadcastData := False; + TNode.Node.UpdateBlockchain := False; + Require(TNode.Node.Bank.BlocksCount = 0, 'Send presentation requires fresh empty data'); + TNode.Node.Bank.StorageClass := TFileStorage; + LStream := LBag.AddObject(TFileStream.Create(ExtractFilePath(ParamStr(0)) + + 'mainnet-blocks-0-4.bin', fmOpenRead or fmShareDenyNone)) as TFileStream; + LBlock := LBag.AddObject(TPCOperationsComp.Create(nil)) as TPCOperationsComp; + LStream.ReadBuffer(LCount, SizeOf(LCount)); + Require(LCount = 5, 'Unexpected public genesis fixture'); + LBlock.Bank := TNode.Node.Bank; + Require(LBlock.LoadBlockFromStream(LStream, LMessage), LMessage); + Require(LBlock.OperationBlock.block = 0, 'Expected public genesis block'); + Require(TNode.Node.AddNewBlockChain(nil, LBlock, LMessage), LMessage); + LUntil := GetTickCount64 + 8000; + while not TNode.Node.GetSynchronizationStatus.PendingOperationsAvailable and + (GetTickCount64 < LUntil) do + Pump(10); + Require(TNode.Node.GetSynchronizationStatus.PendingOperationsAvailable, + 'Public genesis pending state did not settle'); + Require(TWallet.Keys.Count = 0, 'Send presentation must never use an existing wallet key'); + TWallet.GenerateNewKey('Disposable native Send UI QA key', CT_Default_EC_OpenSSL_NID); + Require(TNode.Node.GetAccount(0).balance > 10001, 'Genesis sender lacks display-test funds'); + Log('GENESIS public block 0 validated; fresh generated wallet key; offline, no broadcast'); +end; + +procedure TSendPresentationChecks.BuildApprovalBank; +begin + Require(TUserInterface.Offline, 'Approval checks require private offline startup'); + Require((TNode.Node.Bank.BlocksCount = 0) and (TWallet.Keys.Count = 0), 'Approval bank must start empty'); + TNode.Node.BroadcastData := False; + TNode.Node.UpdateBlockchain := False; + TWallet.GenerateNewKey('Disposable batch approval QA key', CT_Default_EC_OpenSSL_NID); + TNode.Node.Bank.StorageClass := TApprovalStorage; + TApprovalSafeBox(TNode.Node.Bank.SafeBox).BuildFunded(TWallet.Keys[0].AccountKey); + TNode.Node.Bank.UpdateValuesFromSafebox; + Pump(100); + Require(TNode.Node.Bank.BlocksCount = 160, 'Approval bank did not reach account maturity'); + Require(not TAccountComp.IsAccountBlockedByProtocol(0, TNode.Node.Bank.BlocksCount), + 'Approval fixture must use eligible accounts, not bypass signer validation'); + Log('BANK mature generated local bank; fresh owned key; no network, signing or broadcast'); +end; + +function TSendPresentationChecks.PaintNativeForm(AForm: TForm): TBitmap; +{$IFDEF FPC} +var + LCanvasDC: HDC; + + procedure PaintControl(AControl: TWinControl); + var + LSaved, I: Integer; + LOrigin, LWindowOrigin: TPoint; + LWindow: TRect; + begin + if not AControl.HandleAllocated or not IsWindowVisible(AControl.Handle) then Exit; + LSaved := SaveDC(LCanvasDC); + try + GetWindowRect(AControl.Handle, LWindow); + LOrigin := AForm.ScreenToClient(LWindow.TopLeft); + LWindowOrigin := LOrigin; + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + SendMessage(AControl.Handle, WM_PRINT, WPARAM(LCanvasDC), PRF_NONCLIENT); + LOrigin := AForm.ScreenToClient(AControl.ClientToScreen(Types.Point(0, 0))); + SetViewportOrgEx(LCanvasDC, LOrigin.X, LOrigin.Y, nil); + if AControl is TCustomGroupBox then + // LCL's logical client excludes the caption/frame, but its real paint + // handler also draws that native frame outside the logical client. + IntersectClipRect(LCanvasDC, LWindowOrigin.X - LOrigin.X, LWindowOrigin.Y - LOrigin.Y, + LWindowOrigin.X - LOrigin.X + LWindow.Right - LWindow.Left, + LWindowOrigin.Y - LOrigin.Y + LWindow.Bottom - LWindow.Top) + else + IntersectClipRect(LCanvasDC, 0, 0, AControl.ClientWidth, AControl.ClientHeight); + AControl.Perform(LM_ERASEBKGND, WPARAM(LCanvasDC), 0); + AControl.Perform(LM_PAINT, WPARAM(LCanvasDC), 0); + // GroupBox's LM_PAINT already maps its inset LCL origin to Win32's full + // window; printing it again here would shift its frame and graphic labels. + if not (AControl is TCustomControl) and not (AControl is TCustomForm) and + not (AControl is TCustomGroupBox) then + SendMessage(AControl.Handle, WM_PRINTCLIENT, WPARAM(LCanvasDC), PRF_CLIENT); + for I := 0 to AControl.ControlCount - 1 do + if AControl.Controls[I] is TWinControl then + PaintControl(TWinControl(AControl.Controls[I])); + finally + RestoreDC(LCanvasDC, LSaved); + end; + end; +{$ENDIF} +begin + {$IFNDEF FPC} + // GetFormImage locks its canvas throughout recursive VCL painting, keeping + // temporary child canvases from recycling the destination native DC. + Result := AForm.GetFormImage; + Result.PixelFormat := pf24bit; + {$ELSE} + Result := TBitmap.Create; + try + Result.PixelFormat := pf24bit; + Result.SetSize(AForm.ClientWidth, AForm.ClientHeight); + // Same native widget/graphic-child path verified by FrameworkDemoPagingCapture. + Result.Canvas.Brush.Color := AForm.GetRGBColorResolvingParent; + Result.Canvas.FillRect(AForm.ClientRect); + LCanvasDC := Result.Canvas.Handle; + PaintControl(AForm); + except + Result.Free; + raise; + end; + {$ENDIF} +end; + +procedure TSendPresentationChecks.CheckGridPainting(AGrid: TVisualGrid; const ACheckpoint: String); +var + LBag: TDisposables; + LBitmap, LModeBitmap: TBitmap; + LRect: TRect; + LBackground: TNativePixel; + LRow: PNativePixelRow; + LMode: TBitBtn; + LMenu: TPopupMenu; + X, Y, I, LInk: Integer; +begin + Require(AGrid.InternalDrawGrid.HandleAllocated and + (AGrid.InternalDrawGrid.ColCount >= 2) and (AGrid.InternalDrawGrid.RowCount >= 2), + ACheckpoint + ': native grid geometry has no header/account row'); + LBitmap := LBag.AddObject(PaintNativeControl(AGrid.InternalDrawGrid)) as TBitmap; + LBitmap.SaveToFile(ChangeFileExt(FLogPath, '.' + ACheckpoint + '-native-grid.bmp')); + Require(LBitmap.Width <= High(TNativePixelRow) + 1, 'Native grid exceeds pixel-check bounds'); + for I := 0 to FExpectedAccountCount do begin + LRect := AGrid.InternalDrawGrid.CellRect(0, I); + Require((LRect.Left >= 0) and (LRect.Top >= 0) and + (LRect.Right <= LBitmap.Width) and (LRect.Bottom <= LBitmap.Height) and + (LRect.Right - LRect.Left > 8) and (LRect.Bottom - LRect.Top > 4), + ACheckpoint + ': native header/account cell is clipped'); + InflateRect(LRect, -3, -2); + LRow := LBitmap.ScanLine[LRect.Bottom - 1]; + LBackground := LRow^[LRect.Right - 1]; + LInk := 0; + for Y := LRect.Top to LRect.Bottom - 1 do begin + LRow := LBitmap.ScanLine[Y]; + for X := LRect.Left to LRect.Right - 1 do + if (Abs(Integer(LRow^[X].Red) - LBackground.Red) + + Abs(Integer(LRow^[X].Green) - LBackground.Green) + + Abs(Integer(LRow^[X].Blue) - LBackground.Blue)) > 90 then + Inc(LInk); + end; + Log(Format('GRID PIXELS %s row=%d ink=%d rect=%d,%d,%d,%d', + [ACheckpoint, I, LInk, LRect.Left, LRect.Top, LRect.Right, LRect.Bottom])); + Require(LInk >= 3, ACheckpoint + ': native header/account cell painted no text'); + end; + Require(AGrid.FindComponent('PageModeButton') is TBitBtn, 'Native grid mode button is missing'); + Require(AGrid.FindComponent('PageModeMenu') is TPopupMenu, 'Native grid mode menu is missing'); + LMode := TBitBtn(AGrid.FindComponent('PageModeButton')); + LMenu := TPopupMenu(AGrid.FindComponent('PageModeMenu')); + Require((LMode.Width = LMode.Height) and (LMode.Caption = '') and not LMode.Glyph.Empty, + ACheckpoint + ': paging selector must remain a square icon button'); + Require(LMenu.Items.Count = 3, ACheckpoint + ': paging menu lost a mode'); + for I := 0 to 2 do begin + Require(LMenu.Items[I].Tag = I, ACheckpoint + ': paging menu has the wrong mode tag'); + Require(LMenu.Items[I].Checked = (I = Ord(AGrid.PageMode)), + ACheckpoint + ': paging menu check does not reflect the grid mode'); + case I of + 0: Require(LMenu.Items[I].Caption = 'Single', 'Missing Single paging item'); + 1: Require(LMenu.Items[I].Caption = 'Auto', 'Missing Auto paging item'); + 2: Require(LMenu.Items[I].Caption = 'Manual', 'Missing Manual paging item'); + end; + end; + Require(Pos(LMenu.Items[Ord(AGrid.PageMode)].Caption, LMode.Hint) > 0, + ACheckpoint + ': paging button hint lost its current mode'); + LModeBitmap := LBag.AddObject(PaintNativeControl(LMode)) as TBitmap; + LModeBitmap.SaveToFile(ChangeFileExt(FLogPath, '.' + ACheckpoint + '-native-mode.bmp')); + LRect := Types.Rect(3, 3, LModeBitmap.Width - 3, LModeBitmap.Height - 3); + Require((LRect.Right > LRect.Left) and (LRect.Bottom > LRect.Top), + ACheckpoint + ': paging button has no native icon interior'); + LRow := LModeBitmap.ScanLine[LRect.Top]; + LBackground := LRow^[LRect.Left]; + LInk := 0; + for Y := LRect.Top to LRect.Bottom - 1 do begin + LRow := LModeBitmap.ScanLine[Y]; + for X := LRect.Left to LRect.Right - 1 do + if (Abs(Integer(LRow^[X].Red) - LBackground.Red) + + Abs(Integer(LRow^[X].Green) - LBackground.Green) + + Abs(Integer(LRow^[X].Blue) - LBackground.Blue)) > 90 then + Inc(LInk); + end; + Require(LInk >= 3, ACheckpoint + ': native paging button painted no icon'); + Log(Format('GRID MODE PIXELS %s icon_ink=%d', [ACheckpoint, LInk])); + Log(Format('GRID MODE %s entries=%d checked=%d button=%dx%d hint=%s', + [ACheckpoint, LMenu.Items.Count, Ord(AGrid.PageMode), LMode.Width, LMode.Height, LMode.Hint])); +end; + +procedure TSendPresentationChecks.CheckText(AControl: TControl; ACanvas: TCanvas; const ACheckpoint: String); +var + LText: String; + LRect: TRect; + LFlags: Cardinal; + LExtra, LCanvasHeight, LCharY: Integer; + LDC: HDC; + LFont, LSavedFont: HGDIOBJ; + LMetrics: TTextMetric; + LClient, LFormat: TRect; + LPosition: LRESULT; +begin + LExtra := 0; + LFlags := DT_CALCRECT or DT_NOPREFIX; + if AControl is TLabel then begin + LText := TLabel(AControl).Caption; + if TLabel(AControl).WordWrap then + LFlags := LFlags or DT_WORDBREAK; + end else if AControl is TCheckBox then begin + LText := TCheckBox(AControl).Caption; + LExtra := GetSystemMetrics(SM_CXMENUCHECK) + 6; + end else if AControl is TRadioButton then begin + LText := TRadioButton(AControl).Caption; + LExtra := GetSystemMetrics(SM_CXMENUCHECK) + 6; + end else if AControl is TEdit then begin + ACanvas.Font.Assign(TControlFontAccess(AControl).Font); + LCanvasHeight := ACanvas.TextHeight('Mg'); + LDC := GetDC(TEdit(AControl).Handle); + Require(LDC <> 0, 'Cannot inspect the native edit text metrics'); + try + LFont := HGDIOBJ(SendMessage(TEdit(AControl).Handle, WM_GETFONT, 0, 0)); + LSavedFont := 0; + if LFont <> 0 then + LSavedFont := SelectObject(LDC, LFont); + try + Require(GetTextMetrics(LDC, LMetrics), 'Cannot inspect the native edit font'); + finally + if LSavedFont <> 0 then + SelectObject(LDC, LSavedFont); + end; + GetClientRect(TEdit(AControl).Handle, LClient); + LFormat := LClient; + SendMessage(TEdit(AControl).Handle, EM_GETRECT, 0, LPARAM(@LFormat)); + LPosition := SendMessage(TEdit(AControl).Handle, EM_POSFROMCHAR, 0, 0); + LCharY := (LPosition shr 16) and $FFFF; + if LCharY > High(SmallInt) then + Dec(LCharY, 65536); + Log(Format('EDIT %s %s text=%s font_height=%d font_size=%d canvas_height=%d native_height=%d ascent=%d descent=%d leading=%d client=%d,%d,%d,%d format=%d,%d,%d,%d character_y=%d outer_height=%d', + [ACheckpoint, AControl.Name, TEdit(AControl).Text, + TEdit(AControl).Font.Height, TEdit(AControl).Font.Size, LCanvasHeight, + LMetrics.tmHeight, LMetrics.tmAscent, LMetrics.tmDescent, LMetrics.tmInternalLeading, + LClient.Left, LClient.Top, LClient.Right, LClient.Bottom, + LFormat.Left, LFormat.Top, LFormat.Right, LFormat.Bottom, LCharY, AControl.Height])); + finally + ReleaseDC(TEdit(AControl).Handle, LDC); + end; + Require(AControl.Height >= LCanvasHeight + 4, + ACheckpoint + ': edit font is clipped in ' + AControl.Name); + Exit; + end else + Exit; + if LText = '' then Exit; + ACanvas.Font.Assign(TControlFontAccess(AControl).Font); + LRect := Types.Rect(0, 0, Max(1, AControl.Width - LExtra), 0); + DrawText(ACanvas.Handle, PChar(LText), Length(LText), LRect, LFlags); + Require((LRect.Right + LExtra <= AControl.Width + 2) and + (LRect.Bottom <= AControl.Height + 2), + Format('%s: caption clipped in %s, text %dx%d control %dx%d', + [ACheckpoint, AControl.Name, LRect.Right + LExtra, LRect.Bottom, AControl.Width, AControl.Height])); +end; + +procedure TSendPresentationChecks.CheckLayout(AParent: TWinControl; ACanvas: TCanvas; const ACheckpoint: String); +var + LContent, LOverlap: TRect; + A, B: TControl; + I, J, LCaptionWidth: Integer; + LHeaderSummary: Boolean; +begin + LContent := AParent.ClientRect; + LCaptionWidth := 0; + if AParent is TGroupBox then begin + TGroupBoxContentAccess(AParent).AdjustClientRect(LContent); + ACanvas.Font.Assign(TGroupBox(AParent).Font); + LCaptionWidth := ACanvas.TextWidth(TGroupBox(AParent).Caption) + 20; + end; + for I := 0 to AParent.ControlCount - 1 do begin + A := AParent.Controls[I]; + if not A.Visible then Continue; + // Existing selected-account summaries deliberately share the caption band + // to its right; the actual group caption and summary must not intersect. + LHeaderSummary := (AParent is TGroupBox) and (A is TLabel) and + (A.Top < LContent.Top) and (A.Left >= LCaptionWidth); + Require((A.Left >= LContent.Left) and (A.Top >= LContent.Top - Ord(LHeaderSummary) * LContent.Top) and + (A.Left + A.Width <= LContent.Right + 1) and (A.Top + A.Height <= LContent.Bottom + 1), + Format('%s: %s exceeds %s native content (%d,%d,%d,%d in %d,%d,%d,%d)', + [ACheckpoint, A.Name, AParent.Name, A.Left, A.Top, A.Width, A.Height, + LContent.Left, LContent.Top, LContent.Right, LContent.Bottom])); + CheckText(A, ACanvas, ACheckpoint); + for J := I + 1 to AParent.ControlCount - 1 do begin + B := AParent.Controls[J]; + if not B.Visible then Continue; + if IntersectRect(LOverlap, A.BoundsRect, B.BoundsRect) then + Require((LOverlap.Right - LOverlap.Left <= 1) or (LOverlap.Bottom - LOverlap.Top <= 1), + ACheckpoint + ': overlapping controls ' + A.Name + ' and ' + B.Name); + if (A is TWinControl) and (B is TWinControl) and TWinControl(A).TabStop and TWinControl(B).TabStop then + Require(TWinControl(A).TabOrder <> TWinControl(B).TabOrder, + ACheckpoint + ': duplicate keyboard tab order in ' + AParent.Name); + end; + if A is TVisualGrid then begin + Require((TVisualGrid(A).RowCount = FExpectedAccountCount) and (TVisualGrid(A).ColCount >= 2) and + TVisualGrid(A).InternalDrawGrid.Showing and IsWindowVisible(TVisualGrid(A).InternalDrawGrid.Handle), + ACheckpoint + ': actual selected/confirmation grid is not visible and populated'); + CheckGridPainting(TVisualGrid(A), ACheckpoint); + end else if A is TWinControl then + CheckLayout(TWinControl(A), ACanvas, ACheckpoint); + end; +end; + +procedure TSendPresentationChecks.CheckScreen(AWizard: TOperationWizard; AClass: TComponentClass; const AStep: String); +var + LBag: TDisposables; + LBitmap: TBitmap; + LHost: TCustomForm; + LAncestor: TWinControl; + LBounds: TRect; + LUntil: UInt64; + LTag: String; + I, LScale: Integer; + {$IFDEF FPC}LBasePPI: Integer;{$ENDIF} + LPath: TWizardScreenPath; +begin + Require(AWizard.CurrentScreen.InheritsFrom(AClass), CaseName + ': wrong actual route step for ' + AStep); + LPath := AWizard.Screens; + if not FChangeAccountInfo then + for I := 0 to High(LPath) do + Require(not LPath[I].InheritsFrom(TWIZOperationSigner_Select), 'Send PASC unexpectedly injected a signer step'); + LHost := GetParentForm(AWizard.CurrentScreen); + // Hosted screens are forms themselves; walk to the real modal host. + if LHost = AWizard.CurrentScreen then + LHost := GetParentForm(AWizard.CurrentScreen.Parent); + Require(LHost is TWizardHostForm, 'Send screen has no actual wizard host'); + LUntil := GetTickCount64 + 1500; + while not AWizard.CurrentScreen.Showing and (GetTickCount64 < LUntil) do + Pump(10); + Require(AWizard.CurrentScreen.Showing and IsWindowVisible(AWizard.CurrentScreen.Handle), 'Send screen did not become native-visible'); + LBounds := LHost.BoundsRect; + {$IFDEF FPC}LBasePPI := LHost.PixelsPerInch;{$ENDIF} + LAncestor := AWizard.CurrentScreen; + while Assigned(LAncestor) do begin + LogGeometry(LAncestor, CaseName + '-' + AStep + '-before-scaling'); + LAncestor := LAncestor.Parent; + end; + for LScale := 0 to 1 do begin + if LScale = 1 then begin + {$IFDEF FPC} + // Native DPI adjustment uses the docked panel's current bounds. LCL + // ScaleBy instead scales its original 360x64 design base and collapses it. + LHost.AutoAdjustLayout(lapAutoAdjustForDPI, LBasePPI, MulDiv(LBasePPI, 3, 2), 0, 0); + Require(LHost.PixelsPerInch = MulDiv(LBasePPI, 3, 2), 'LCL did not adopt the requested DPI'); + {$ELSE} + LHost.ScaleBy(3, 2); + {$ENDIF} + end; + try + Pump(50); + LTag := CaseName + '-' + AStep + '-' + IntToStr(100 + LScale * 50); + LAncestor := AWizard.CurrentScreen; + while Assigned(LAncestor) do begin + LogGeometry(LAncestor, LTag); + LAncestor := LAncestor.Parent; + end; + LBitmap := LBag.AddObject(PaintNativeForm(AWizard.CurrentScreen)) as TBitmap; + LBitmap.SaveToFile(ChangeFileExt(FLogPath, '.' + LTag + '.bmp')); + CheckLayout(AWizard.CurrentScreen, LBitmap.Canvas, LTag); + Log('SCREEN ' + LTag + ': visible native content, captions, bounds, no overlaps and keyboard order passed'); + finally + if LScale = 1 then begin + {$IFDEF FPC} + LHost.AutoAdjustLayout(lapAutoAdjustForDPI, LHost.PixelsPerInch, LBasePPI, 0, 0); + Require(LHost.PixelsPerInch = LBasePPI, 'LCL did not restore the original DPI'); + {$ELSE} + LHost.ScaleBy(2, 3); + {$ENDIF} + LHost.BoundsRect := LBounds; + Pump(50); + end; + end; + end; +end; + +procedure TSendPresentationChecks.Advance(AWizard: TOperationWizard); +var + LMessage: String; +begin + Require(not (AWizard.CurrentScreen is TWIZOperationConfirmation), 'QA must never invoke Finish'); + Require(AWizard.TryNext(LMessage), CaseName + ': actual Continue rejected: ' + LMessage); +end; + +procedure TSendPresentationChecks.RunFlow(AWizard: TWIZSendPASCWizard); +var + LDetails: TWIZSendPASC_Details; + LEncryption: TWIZOperationPayload_Encryption; + LCustom, LPayload: Boolean; + LMessage: String; +begin + CheckScreen(AWizard, TWIZOperationSelected, 'selected'); + Advance(AWizard); + Require(AWizard.CurrentScreen is TWIZSendPASC_Details, 'Send route did not present Details'); + LDetails := TWIZSendPASC_Details(AWizard.CurrentScreen); + LDetails.txtRecipient.Text := TAccountComp.AccountNumberToAccountTxtNumber(2); + LDetails.txtAmount.Text := '1.0000'; + LCustom := FCase in [scCustomFee, scPayloadSender, scPayloadPassword]; + LPayload := FCase in [scPayloadPlain, scPayloadSender, scPayloadRecipient, scPayloadPassword]; + LDetails.chkCustomFee.Checked := LCustom; + LDetails.chkPayload.Checked := LPayload; + CheckScreen(AWizard, TWIZSendPASC_Details, 'details'); + Advance(AWizard); + if LCustom then begin + Require(AWizard.CurrentScreen is TWIZOperationFee_Custom, 'Custom fee step was not injected'); + TWIZOperationFee_Custom(AWizard.CurrentScreen).Fee := 1; + TWIZOperationFee_Custom(AWizard.CurrentScreen).cbOverrideDefaultFee.Checked := False; + CheckScreen(AWizard, TWIZOperationFee_Custom, 'fee'); + Advance(AWizard); + end; + if LPayload then begin + Require(AWizard.CurrentScreen is TWIZOperationPayload_Encryption, 'Payload encryption step was not injected'); + LEncryption := TWIZOperationPayload_Encryption(AWizard.CurrentScreen); + case FCase of + scPayloadPlain: LEncryption.rbNotEncrypted.Checked := True; + scPayloadSender: LEncryption.rbEncryptedWithSender.Checked := True; + scPayloadRecipient: LEncryption.rbEncryptedWithRecipient.Checked := True; + scPayloadPassword: LEncryption.rbEncryptedWithPassword.Checked := True; + end; + CheckScreen(AWizard, TWIZOperationPayload_Encryption, 'encryption'); + Advance(AWizard); + if FCase = scPayloadPassword then begin + Require(AWizard.CurrentScreen is TWIZOperationPayload_Password, 'Password step was not injected'); + TWIZOperationPayload_Password(AWizard.CurrentScreen).edtPassword.Text := 'Disposable native UI password'; + CheckScreen(AWizard, TWIZOperationPayload_Password, 'password'); + Advance(AWizard); + end; + Require(AWizard.CurrentScreen is TWIZOperationPayload_Content, 'Payload content step was not injected'); + TWIZOperationPayload_Content(AWizard.CurrentScreen).mmoPayload.Lines.Text := 'Native Send PASC presentation regression.'; + CheckScreen(AWizard, TWIZOperationPayload_Content, 'content'); + Advance(AWizard); + end; + CheckScreen(AWizard, TWIZOperationConfirmation, 'confirmation'); + Require(AWizard.Model.SendPASC.DestinationAccount.account = 2, 'Recipient changed during cosmetic navigation'); + Require(AWizard.Model.SendPASC.SingleAmountToSend = 10000, 'Amount changed during cosmetic navigation'); + Require(AWizard.Cancel(LMessage), 'Send wizard did not accept Cancel: ' + LMessage); + Require(AWizard.State = wsCancelled, 'Send wizard did not enter cancelled state'); + FCancelled := True; + Log('ROUTE ' + CaseName + ': actual Continue/validation/injection and Cancel passed; signer correctly unreachable'); +end; + +procedure TSendPresentationChecks.RunChangeInfoFlow(AWizard: TWIZChangeAccountInfoWizard); +var + LDetails: TWIZChangeAccountInfo_Details; + LMessage: String; +begin + Require(AWizard.Model.Account.Count = FExpectedAccountCount, 'Unexpected real Change Account Info selection'); + CheckScreen(AWizard, TWIZOperationSelected, 'selected'); + Advance(AWizard); + Require(AWizard.CurrentScreen is TWIZChangeAccountInfo_Details, 'Change Account Info did not present Details'); + LDetails := TWIZChangeAccountInfo_Details(AWizard.CurrentScreen); + if FExpectedAccountCount = 1 then begin + Require(LDetails.gbName.Enabled, 'Single-account Name group is disabled'); + LDetails.txtName.Text := 'native-info-qa'; + end else + Require(not LDetails.gbName.Enabled, 'Batch Name group must retain its existing disabled state'); + LDetails.txtType.Text := '12345'; + LDetails.chkCustomFee.Checked := True; + LDetails.chkPayload.Checked := True; + CheckScreen(AWizard, TWIZChangeAccountInfo_Details, 'details'); + Require(LDetails.txtType.Text = '12345', 'Visual scaling changed the typed account type'); + Require(LDetails.chkCustomFee.Checked and LDetails.chkPayload.Checked, 'Visual scaling changed the selected options'); + if FExpectedAccountCount = 1 then + Require(LDetails.txtName.Text = 'native-info-qa', 'Visual scaling changed the typed account name'); + // This cosmetic regression stops on Details, before validation/preparation + // can inject signer steps or attempt any operation execution. + Require(AWizard.Cancel(LMessage), 'Change Account Info rejected Cancel: ' + LMessage); + Require(AWizard.State = wsCancelled, 'Change Account Info did not enter cancelled state'); + FCancelled := True; + Log('ROUTE ' + CaseName + ': actual Selected/Details and Cancel passed; no Finish, signing or broadcast'); +end; + +procedure TSendPresentationChecks.CheckInfoConfirmation(AWizard: TWIZChangeAccountInfoWizard; const AStep: String); +var + I: Integer; + LGrid: TVisualGrid; + LExpectedPayer: Cardinal; + LFees: Int64; +begin + FExpectedAccountCount := Length(AWizard.Model.ChangeAccountInfo.TargetAccounts); + CheckScreen(AWizard, TWIZOperationConfirmation, AStep); + LGrid := nil; + for I := 0 to AWizard.CurrentScreen.ComponentCount - 1 do + if AWizard.CurrentScreen.Components[I] is TVisualGrid then + LGrid := TVisualGrid(AWizard.CurrentScreen.Components[I]); + Require(Assigned(LGrid), 'Confirmation has no actual visual grid'); + LFees := 0; + for I := 0 to FExpectedAccountCount - 1 do + begin + Require(Cardinal(LGrid.Rows[I].AccountNumber) = AWizard.Model.ChangeAccountInfo.TargetAccounts[I].account, + 'Confirmation contains a skipped account or changed target order'); + LExpectedPayer := AWizard.Model.ChangeAccountInfo.TargetAccounts[I].account; + if AWizard.Model.ChangeAccountInfo.UseSingleFeePayer then + LExpectedPayer := AWizard.Model.ChangeAccountInfo.FeePayerAccount.account; + Require(String(LGrid.Rows[I].Signer) = TAccountComp.AccountNumberToAccountTxtNumber(LExpectedPayer), + 'Confirmation displays the wrong fee payer'); + Require(Int64(LGrid.Rows[I].Fee) = -AWizard.Model.Fee.SingleOperationFee, + 'Confirmation fee differs from the selected fee'); + Inc(LFees, -Int64(LGrid.Rows[I].Fee)); + end; + Require(LFees = FExpectedAccountCount * AWizard.Model.Fee.SingleOperationFee, + 'Skipped changes contributed to confirmation fees'); +end; + +procedure TSendPresentationChecks.RunApprovalFlow(AWizard: TWIZChangeAccountInfoWizard); +var + LDetails: TWIZChangeAccountInfo_Details; + LReview: TWIZChangeAccountInfo_Skipped; + LFee: TWIZOperationFee_Custom; + LOriginal: TArray<TAccount>; + LPayerA, LPayerB: TAccount; + LPath: TWizardScreenPath; + LMessage: String; + I, LOriginalCount: Integer; + + procedure CheckReview(const AStep: String); + var + LExpectedSkipped: Integer; + LPrefix: String; + begin + Require(AWizard.CurrentScreen is TWIZChangeAccountInfo_Skipped, 'Skipped accounts were not explained before continuing'); + LReview := TWIZChangeAccountInfo_Skipped(AWizard.CurrentScreen); + LExpectedSkipped := 1; + if FInfoCase = iacAllSkipped then LExpectedSkipped := 2; + Require(AWizard.Model.ChangeAccountInfo.SkippedCount = LExpectedSkipped, + 'Review has the wrong number of skipped accounts'); + Require(AWizard.Model.ChangeAccountInfo.SkippedCount = + Length(AWizard.Model.Account.SelectedAccounts) - Length(AWizard.Model.ChangeAccountInfo.TargetAccounts), + 'Review count differs from the original and filtered selections'); + if LExpectedSkipped = 1 then + LPrefix := '1 selected account already' + else + LPrefix := IntToStr(LExpectedSkipped) + ' selected accounts already'; + Require(Pos(LPrefix, LReview.lblSummary.Caption) = 1, 'Review displays the wrong skipped-account count'); + Require(Pos(IntToStr(AWizard.Model.ChangeAccountInfo.NewType), LReview.lblSummary.Caption) > 0, + 'Review omits the requested account type'); + Require(Pos('No operation fee is charged for skipped changes', LReview.lblFees.Caption) > 0, + 'Review omits the skipped-fee explanation'); + Require(not LReview.chkApprove.Checked and not AWizard.Model.ChangeAccountInfo.SkippedAccountsApproved, + 'A previous approval leaked into the current review'); + CheckScreen(AWizard, TWIZChangeAccountInfo_Skipped, AStep); + Require(not AWizard.TryNext(LMessage), 'Unchecked skip approval allowed continuation'); + Require(AWizard.CurrentScreen = LReview, 'Rejected approval left the review screen'); + end; + + procedure ApproveReview; + begin + LReview.chkApprove.Checked := True; + Advance(AWizard); + Require(AWizard.Model.ChangeAccountInfo.SkippedAccountsApproved, 'Explicit skip approval was not retained'); + end; + + procedure BackToDetails; + var + LSteps: Integer; + begin + LSteps := 0; + while not (AWizard.CurrentScreen is TWIZChangeAccountInfo_Details) do + begin + Require(LSteps < 6, 'Back did not return to Details'); + Require(AWizard.TryPrevious(LMessage), 'Actual Back failed: ' + LMessage); + Inc(LSteps); + end; + LDetails := TWIZChangeAccountInfo_Details(AWizard.CurrentScreen); + end; + +begin + LOriginalCount := AWizard.Model.Account.Count; + AWizard.Model.Account.SelectedAccounts[0].account_type := 0; + if LOriginalCount > 1 then + AWizard.Model.Account.SelectedAccounts[1].account_type := 1; + LOriginal := TArrayTool<TAccount>.Copy(AWizard.Model.Account.SelectedAccounts); + CheckScreen(AWizard, TWIZOperationSelected, 'selected'); + Advance(AWizard); + LDetails := TWIZChangeAccountInfo_Details(AWizard.CurrentScreen); + LDetails.chkCustomFee.Checked := FInfoCase in [iacFeeSelf, iacFeeCommon]; + LDetails.chkPayload.Checked := False; + if FInfoCase = iacSingleName then + begin + LDetails.txtName.Text := 'single-info-qa'; + LDetails.txtType.Text := '0'; + end + else if FInfoCase = iacAllSkipped then + begin + AWizard.Model.Account.SelectedAccounts[1].account_type := 0; + LOriginal[1].account_type := 0; + LDetails.txtType.Text := '0'; + end + else if FInfoCase in [iacNoSkipped, iacFeeSelf] then + LDetails.txtType.Text := '2' + else + LDetails.txtType.Text := '1'; + CheckScreen(AWizard, TWIZChangeAccountInfo_Details, 'details'); + Advance(AWizard); + if FInfoCase in [iacMixed, iacAllSkipped, iacFeeCommon] then + begin + CheckReview('skip-review'); + if FInfoCase = iacAllSkipped then + begin + Require(Length(AWizard.Model.ChangeAccountInfo.TargetAccounts) = 0, 'All-skipped batch has an update target'); + Require(not LReview.chkApprove.Enabled, 'All-skipped review enables approval'); + LReview.chkApprove.Checked := True; + Require(not AWizard.TryNext(LMessage), 'All-skipped batch reached a fee or confirmation step'); + end + else + ApproveReview; + end; + if FInfoCase in [iacFeeSelf, iacFeeCommon] then + begin + Require(AWizard.CurrentScreen is TWIZOperationFee_Custom, 'Custom fee did not use the existing fee screen'); + LFee := TWIZOperationFee_Custom(AWizard.CurrentScreen); + LFee.cbOverrideDefaultFee.Checked := False; + LFee.Fee := 1; + Require(LFee.chkSingleFeePayer.Visible and not LFee.chkSingleFeePayer.Checked, + 'Change Info fee page must start with optional common payer unchecked'); + if FInfoCase = iacFeeCommon then + begin + LPayerA := TNode.Node.GetAccount(5); + LPayerB := TNode.Node.GetAccount(10); + LPayerA.balance := 3; + LPayerB.balance := 4; + AWizard.Model.Signer.SignerCandidates := TArray<TAccount>.Create(LPayerA, LPayerB); + LFee.SingleFeePayerChange(nil); + LFee.chkSingleFeePayer.Checked := True; + LFee.SingleFeePayerChange(nil); + Require(LFee.cbFeePayer.ItemIndex = 0, 'Multiple eligible payers were chosen without user selection'); + Require(not AWizard.TryNext(LMessage), 'Checked common-payer placeholder passed validation'); + LFee.cbFeePayer.ItemIndex := 1; + LFee.FeePayerChange(nil); + LFee.Fee := 2; + Require(AWizard.Model.ChangeAccountInfo.FeePayerSelected and + (AWizard.Model.ChangeAccountInfo.FeePayerAccount.account = 5), 'Changing fee lost a still-eligible explicit payer'); + LFee.Fee := 4; + Require((LFee.cbFeePayer.Items.Count = 2) and (LFee.cbFeePayer.ItemIndex = 0) and + not AWizard.Model.ChangeAccountInfo.FeePayerSelected, + 'Fee increase silently replaced the user-selected payer with another account'); + LFee.Fee := 5; + Require((LFee.cbFeePayer.Items.Count = 1) and not AWizard.Model.ChangeAccountInfo.FeePayerSelected, + 'An ineligible payer survived a fee increase'); + Require(not AWizard.TryNext(LMessage), 'Checked common payer with no eligible account passed validation'); + LFee.chkSingleFeePayer.Checked := False; + LFee.SingleFeePayerChange(nil); + Require(LFee.Validate(LMessage), 'Unchecked self-pay path depends on a common-payer selection'); + LFee.Fee := 1; + LFee.chkSingleFeePayer.Checked := True; + LFee.SingleFeePayerChange(nil); + Require(LFee.cbFeePayer.ItemIndex = 0, 'Invalidated payer was silently replaced'); + LFee.cbFeePayer.ItemIndex := 1; + LFee.FeePayerChange(nil); + end; + CheckScreen(AWizard, TWIZOperationFee_Custom, 'fee-and-payer'); + Advance(AWizard); + end; + if FInfoCase <> iacAllSkipped then + begin + LPath := AWizard.Screens; + for I := 0 to High(LPath) do + Require(not LPath[I].InheritsFrom(TWIZOperationSigner_Select), 'Change Info injected a separate payer screen'); + CheckInfoConfirmation(AWizard, 'confirmation'); + if FInfoCase = iacMixed then + begin + BackToDetails; + LDetails.txtType.Text := '2'; + Advance(AWizard); + Require(AWizard.Model.ChangeAccountInfo.SkippedCount = 0, 'New type retained old skipped accounts'); + Require(not AWizard.Model.ChangeAccountInfo.SkippedAccountsApproved, 'New type retained old approval'); + LPath := AWizard.Screens; + for I := 0 to High(LPath) do + Require(not LPath[I].InheritsFrom(TWIZChangeAccountInfo_Skipped), 'No-skip route retained the obsolete review step'); + CheckInfoConfirmation(AWizard, 'changed-type-confirmation'); + BackToDetails; + LDetails.txtType.Text := '0'; + Advance(AWizard); + CheckReview('changed-type-review'); + ApproveReview; + Require(AWizard.Model.ChangeAccountInfo.TargetAccounts[0].account = 5, 'Changed type retained the old approved target'); + CheckInfoConfirmation(AWizard, 'reapproved-confirmation'); + end; + end; + Require(AWizard.Model.Account.Count = LOriginalCount, 'Filtering replaced the original selection count'); + for I := 0 to High(LOriginal) do + Require(TAccountComp.EqualAccounts(LOriginal[I], AWizard.Model.Account.SelectedAccounts[I]), + 'Filtering mutated an original selected account'); + Require(AWizard.Cancel(LMessage), 'Approval case rejected Cancel: ' + LMessage); + Require(AWizard.State = wsCancelled, 'Approval case did not cancel'); + FCancelled := True; + Log('APPROVAL ' + CaseName + ': target filtering, explicit consent, fee rows and Cancel passed'); +end; + +procedure TSendPresentationChecks.Tick(Sender: TObject); +var + I: Integer; + LHost: TForm; +begin + if FInside then Exit; + try + Require(GetTickCount64 < FDeadline, 'Timed out waiting for the actual operation wizard'); + LHost := nil; + for I := 0 to Screen.FormCount - 1 do + if (Screen.Forms[I] is TWizardHostForm) and Screen.Forms[I].Visible and + IsExpectedWizard(Screen.Forms[I].Owner) then + LHost := Screen.Forms[I]; + if not Assigned(LHost) then Exit; + FInside := True; + FTimer.Enabled := False; + if FInfoApproval then + RunApprovalFlow(TWIZChangeAccountInfoWizard(LHost.Owner)) + else if FChangeAccountInfo then + RunChangeInfoFlow(TWIZChangeAccountInfoWizard(LHost.Owner)) + else + RunFlow(TWIZSendPASCWizard(LHost.Owner)); + except + on E: Exception do begin + if FError = '' then FError := E.ClassName + ': ' + E.Message; + CloseOwnedWizard; + end; + end; +end; + +procedure TSendPresentationChecks.RunChangeInfoApproval; +var + LCase: TInfoApprovalCase; +begin + FChangeAccountInfo := True; + FInfoApproval := True; + BuildApprovalBank; + for LCase := Low(TInfoApprovalCase) to High(TInfoApprovalCase) do + begin + FInfoCase := LCase; + FExpectedAccountCount := 2; + if LCase = iacSingleName then FExpectedAccountCount := 1; + FInside := False; + FCancelled := False; + FDeadline := GetTickCount64 + 10000; + FTimer.Enabled := True; + try + if FExpectedAccountCount = 1 then + TUserInterface.ShowChangeAccountInfoDialog([0]) + else + TUserInterface.ShowChangeAccountInfoDialog([0, 5]); + finally + FTimer.Enabled := False; + end; + Require(FError = '', FError); + Require(FCancelled, 'Approval case did not cancel'); + Require((TNode.Node.Bank.BlocksCount = 160) and (TNode.Node.MempoolOperationsCount = 0), + 'Approval UI submitted an operation or changed the bank'); + end; + Log('PASS native Change Info approval: six cases, skip consent, filtered fees, optional payer, Back/type changes, 100/150 percent; no Finish, signing or broadcast'); +end; + +procedure TSendPresentationChecks.RunChangeAccountInfo; +var + LCount: Integer; +begin + FChangeAccountInfo := True; + LoadGenesis; + for LCount := 1 to 2 do begin + FExpectedAccountCount := LCount; + FInside := False; + FCancelled := False; + FDeadline := GetTickCount64 + 6000; + FTimer.Enabled := True; + try + if FExpectedAccountCount = 1 then + TUserInterface.ShowChangeAccountInfoDialog([0]) + else + TUserInterface.ShowChangeAccountInfoDialog([0, 1]); + finally + FTimer.Enabled := False; + end; + Require(FError = '', FError); + Require(FCancelled, 'Change Account Info did not cancel its Details screen'); + Require((TNode.Node.Bank.BlocksCount = 1) and (TNode.Node.MempoolOperationsCount = 0), + 'Change Account Info cosmetic checks changed core state'); + end; + Log('PASS native Change Account Info presentation: single/batch Selected and Details at 100/150 percent; no Finish, signing or broadcast'); +end; + +procedure TSendPresentationChecks.Run; +var + LCase: TSendCase; +begin + LoadGenesis; + for LCase := Low(TSendCase) to High(TSendCase) do begin + FCase := LCase; + FInside := False; + FCancelled := False; + FDeadline := GetTickCount64 + 6000; + FTimer.Enabled := True; + try + TUserInterface.ShowSendDialog([0]); + finally + FTimer.Enabled := False; + end; + Require(FError = '', FError); + Require(FCancelled, 'Send route did not reach and cancel its confirmation'); + Require((TNode.Node.Bank.BlocksCount = 1) and (TNode.Node.MempoolOperationsCount = 0), + 'Send cosmetic checks changed core state'); + end; + Log('PASS native Send PASC presentation: six routes, all reachable screens at 100/150 percent; no Finish, signing or broadcast'); +end; + +procedure CheckSendPascPresentation(AMain: TMockMainForm; const ALogPath: String); +var + LBag: TDisposables; + LChecks: TSendPresentationChecks; +begin + LChecks := LBag.AddObject(TSendPresentationChecks.Create(AMain, ALogPath)) as TSendPresentationChecks; + LChecks.Run; +end; + +procedure CheckChangeAccountInfoPresentation(AMain: TMockMainForm; const ALogPath: String); +var + LBag: TDisposables; + LChecks: TSendPresentationChecks; +begin + LChecks := LBag.AddObject(TSendPresentationChecks.Create(AMain, ALogPath)) as TSendPresentationChecks; + LChecks.RunChangeAccountInfo; +end; + +procedure CheckChangeAccountInfoApproval(AMain: TMockMainForm; const ALogPath: String); +var + LBag: TDisposables; + LChecks: TSendPresentationChecks; +begin + LChecks := LBag.AddObject(TSendPresentationChecks.Create(AMain, ALogPath)) as TSendPresentationChecks; + LChecks.RunChangeInfoApproval; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/UWalletToolsDialogChecks.pas b/src/tests/experimental/native/grid-refresh/UWalletToolsDialogChecks.pas new file mode 100644 index 000000000..321fe90fa --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/UWalletToolsDialogChecks.pas @@ -0,0 +1,1197 @@ +unit UWalletToolsDialogChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Runs actual wallet menu handlers and modal wizard presentation on the private + QA desktop. Its fresh node can contact loopback only and never broadcasts. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UWalletGridMocks; + +procedure SeedWalletToolsSettings(const ADirectory: String); +function CheckWalletToolsDialogs(AMain: TMockMainForm; const ALogPath: String): String; + +implementation + +uses Windows, Messages, Classes, SysUtils, Types, Math, Variants, Generics.Collections, Forms, Controls, + StdCtrls, ExtCtrls, Menus, Graphics, Grids, {$IFDEF FPC}LMessages,{$ENDIF} + UAppParams, USettings, UMemory, UCommon, UGridUtils, Buttons, + UNode, UNetProtocol, UUserInterface, UFRMMainForm, UVisualGrid, UWizard, + UFRMAccountExplorer, UFRMPendingOperations, UFRMBlockExplorer, UFRMOperationExplorer, + UFRMOperationsExplorer, UFRMRPCCalls, UFRMLogs, UFRMNodes, UFRMMessages, UFRMPayloadDecoder, + UBlockChain, UFileStorage, UCrypto, UConst, UCoreObjects, UCoreUtils, UWIZOperation, + UWIZChangeKey, UWIZOperationSelected, UWIZOperationConfirmation, + UWalletOperationDetailsChecks, UWalletOperationInfoLayoutChecks; + +type + TGroupBoxAccess = class(TGroupBox); + + TMockWalletPopupAccess = class(TMockWallet) + public + function NativeAccountPopupHandler: TPreparePopupMenuEvent; + end; + + TChangeKeyWizardAccess = class(TWIZChangeKeyWizard) + public + procedure PresentConfirmation; + procedure PresentSelectedAgain; + end; + + TProbeStage = (psIdle, psModal, psPopup, psWizard, psOperationInput, psDone); + + TWalletToolsChecks = class(TComponent) + private + FMain: TMockMainForm; + FTimer: TTimer; + FLog: TStringList; + FLogPath, FExpectedClass, FError: String; + FExpectedForm: TFormClass; + FStage: TProbeStage; + FDeadline: UInt64; + FSeen, FPopupPrepared, FWizardCancelled, FLoggedModal: Boolean; + FOperationDialogCount, FExpectedOperation: Integer; + FLastDecodedOperation: Cardinal; + FInputCase: String; + FObservedOperationDialog: TForm; + FObservingOperationDialog: Boolean; + FPreviousException: TExceptionEvent; + FPreviousPrepare: TPreparePopupMenuEvent; + procedure Require(AValue: Boolean; const AMessage: String); + procedure Log(const AMessage: String); + procedure Pump(AMilliseconds: Cardinal); + procedure CheckErrors; + procedure LogModalWindows(const ACheckpoint: String); + procedure OnException(Sender: TObject; E: Exception); + procedure TimerTick(Sender: TObject); + procedure PreparePopup(Sender: TObject; + {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection; + out APopupMenu: TPopupMenu); + procedure CloseProbeDialogs; + procedure CheckConnections; + procedure LoadGenesis; + procedure CheckMenu(AItem: TMenuItem; AFormClass: TFormClass; AModal: Boolean); + procedure CheckMenus; + procedure CheckBlockTools; + procedure CheckOperationInput; + procedure CheckHistoryDepth; + procedure CheckWizard; + procedure CheckWizardGrid(AWizard: TWIZChangeKeyWizard; const ACheckpoint: String); + procedure CheckWizardPaint(AGrid: TVisualGrid; const ACheckpoint: String); + procedure CheckOverview(const ACheckpoint: String); + function PaintControl(AControl: TWinControl): TBitmap; + procedure Capture(AControl: TWinControl; const ASuffix: String); + function VisibleForm(AFormClass: TFormClass): TForm; + protected + procedure Notification(AComponent: Classes.TComponent; + AOperation: Classes.TOperation); override; + public + constructor Create(AMain: TMockMainForm; const ALogPath: String); reintroduce; + destructor Destroy; override; + procedure Run; + procedure RunBlockTools; + procedure RunOperationInput; + procedure RunHistoryDepth; + end; + +procedure SeedWalletToolsSettings(const ADirectory: String); +var + LBag: TDisposables; + LParams: TAppParams; +begin + LParams := LBag.AddObject(TAppParams.Create(nil)) as TAppParams; + LParams.FileName := IncludeTrailingPathDelimiter(ADirectory) + 'AppParams.prm'; + // RPC's form requires online policy, but discovery remains strictly private. + LParams.ParamByName[CT_PARAM_TryToConnectOnlyWithThisFixedServers].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_PeerCache].SetAsString(''); + LParams.ParamByName[CT_PARAM_InternetServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_JSONRPCEnabled].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerActive].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_MinerPrivateKeyType].SetAsInteger(Ord(mpk_Random)); + LParams.ParamByName[CT_PARAM_ShowLogs].SetAsBoolean(True); + LParams.ParamByName[CT_PARAM_SaveLogFiles].SetAsBoolean(False); +end; + +procedure TChangeKeyWizardAccess.PresentConfirmation; +begin + PresentScreen(CreateScreen(TWIZOperationConfirmation)); +end; + +procedure TChangeKeyWizardAccess.PresentSelectedAgain; +begin + PresentScreen(CurrentScreen); +end; + +function TMockWalletPopupAccess.NativeAccountPopupHandler: TPreparePopupMenuEvent; +begin + Result := OnPrepareAccountPopupMenu; +end; + +constructor TWalletToolsChecks.Create(AMain: TMockMainForm; const ALogPath: String); +begin + inherited Create(nil); + FMain := AMain; + FLogPath := ALogPath; + FLog := TStringList.Create; + FTimer := TTimer.Create(Self); + FTimer.Enabled := False; + FTimer.Interval := 20; + FTimer.OnTimer := TimerTick; + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TWalletToolsChecks.Destroy; +begin + FTimer.Enabled := False; + Application.OnException := FPreviousException; + FLog.Free; + inherited Destroy; +end; + +procedure TWalletToolsChecks.Notification(AComponent: Classes.TComponent; + AOperation: Classes.TOperation); +begin + inherited Notification(AComponent, AOperation); + if (AOperation = opRemove) and (AComponent = FObservedOperationDialog) then + FObservedOperationDialog := nil; +end; + +procedure TWalletToolsChecks.Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then + raise Exception.Create(AMessage); +end; + +procedure TWalletToolsChecks.Log(const AMessage: String); +begin + FLog.Add(FormatDateTime('hh:nn:ss.zzz', Now) + ' ' + AMessage); + FLog.SaveToFile(FLogPath); +end; + +procedure TWalletToolsChecks.CheckErrors; +begin + Require(FError = '', FError); +end; + +procedure TWalletToolsChecks.LogModalWindows(const ACheckpoint: String); +var + I: Integer; + LForm: TForm; + LNativeVisible: Boolean; +begin + Log(Format('MODAL %s expected=%s forms=%d active=%s', + [ACheckpoint, FExpectedClass, Screen.FormCount, IntToHex(GetActiveWindow, SizeOf(Pointer) * 2)])); + for I := 0 to Screen.FormCount - 1 do begin + LForm := Screen.Forms[I]; + LNativeVisible := LForm.HandleAllocated and IsWindowVisible(LForm.Handle); + Log(Format('MODAL FORM %s name=%s visible=%s showing=%s native=%s modal=%s caption=%s', + [LForm.ClassName, LForm.Name, BoolToStr(LForm.Visible, True), + BoolToStr(LForm.Showing, True), BoolToStr(LNativeVisible, True), + BoolToStr(fsModal in LForm.FormState, True), LForm.Caption])); + end; +end; + +procedure TWalletToolsChecks.Pump(AMilliseconds: Cardinal); +var + LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + CheckErrors; + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TWalletToolsChecks.OnException(Sender: TObject; E: Exception); +begin + if FError = '' then + FError := E.ClassName + ': ' + E.Message; + CloseProbeDialogs; +end; + +function TWalletToolsChecks.VisibleForm(AFormClass: TFormClass): TForm; +var + I: Integer; +begin + Result := nil; + for I := 0 to Screen.FormCount - 1 do + // GNUgettext's !dx proxy keeps the original class as its parent. Match the + // actual form type so translation cannot hide a legitimate native window. + if Screen.Forms[I].InheritsFrom(AFormClass) and Screen.Forms[I].Visible then + Exit(Screen.Forms[I]); +end; + +procedure TWalletToolsChecks.CloseProbeDialogs; +var + I: Integer; +begin + EndMenu; + for I := Screen.FormCount - 1 downto 0 do + if (Screen.Forms[I] <> FMain) and Screen.Forms[I].Visible and + (fsModal in Screen.Forms[I].FormState) then + Screen.Forms[I].ModalResult := mrCancel; +end; + +procedure TWalletToolsChecks.CheckConnections; +var + LConnections: TList<TNetConnection>; + LConnection: TNetConnection; + LAddress: String; + I: Integer; +begin + Require(TSettings.TryConnectOnlyWithThisFixedServers = '127.0.0.1:1', + 'Tools fixture lost its loopback-only discovery setting'); + Require(not TSettings.JsonRpcPortEnabled and not TSettings.JsonRpcMinerServerActive, + 'Tools fixture unexpectedly enabled an RPC or miner service'); + LConnections := TNetData.NetData.NetConnections.LockList; + try + for I := 0 to LConnections.Count - 1 do begin + LConnection := LConnections[I]; + LAddress := LConnection.ClientRemoteAddr; + Require((LAddress = '') or (LAddress = 'NIL') or (LAddress = '127.0.0.1') or + (Pos('127.0.0.1:', LAddress) = 1), 'Tools fixture attempted an external peer: ' + LAddress); + Require(not LConnection.Connected, 'Tools fixture connected to a peer'); + end; + finally + TNetData.NetData.NetConnections.UnlockList; + end; + Require(TNetData.NetData.NetStatistics.ActiveConnections = 0, + 'Tools fixture has an active peer'); +end; + +procedure TWalletToolsChecks.LoadGenesis; +var + LBag: TDisposables; + LStream: TFileStream; + LBlock: TPCOperationsComp; + LCount: Cardinal; + LMessage: String; + LUntil: UInt64; +begin + TNode.Node.BroadcastData := False; + TNode.Node.UpdateBlockchain := False; + Require(TNode.Node.Bank.BlocksCount = 0, 'Tools fixture requires a fresh empty bank'); + TNode.Node.Bank.StorageClass := TFileStorage; + LStream := LBag.AddObject(TFileStream.Create(ExtractFilePath(ParamStr(0)) + + 'mainnet-blocks-0-4.bin', fmOpenRead or fmShareDenyNone)) as TFileStream; + LBlock := LBag.AddObject(TPCOperationsComp.Create(nil)) as TPCOperationsComp; + LStream.ReadBuffer(LCount, SizeOf(LCount)); + Require(LCount = 5, 'Unexpected public genesis fixture'); + LBlock.Bank := TNode.Node.Bank; + Require(LBlock.LoadBlockFromStream(LStream, LMessage), LMessage); + Require(LBlock.OperationBlock.block = 0, 'Fixture must begin with public genesis'); + Require(TNode.Node.AddNewBlockChain(nil, LBlock, LMessage), LMessage); + Require(TNode.Node.Bank.BlocksCount = 1, 'Public genesis did not pass normal validation'); + LUntil := GetTickCount64 + 8000; + while not TNode.Node.GetSynchronizationStatus.PendingOperationsAvailable and + (GetTickCount64 < LUntil) do + Pump(5); + Require(TNode.Node.GetSynchronizationStatus.PendingOperationsAvailable, + 'Genesis pending-state reconciliation did not finish'); + Pump(100); + Log('GENESIS validated public block 0, five real accounts; broadcasting disabled'); +end; + +procedure TWalletToolsChecks.CheckMenu(AItem: TMenuItem; AFormClass: TFormClass; AModal: Boolean); +var + LForm: TForm; + LUntil: UInt64; + LClassName: String; +begin + LClassName := AFormClass.ClassName; + Require(AItem.Enabled and Assigned(AItem.OnClick), AItem.Name + ' is not an enabled menu action'); + Log('MENU ENTER ' + AItem.Name + ' expected=' + LClassName); + if AModal then begin + FExpectedClass := LClassName; + FExpectedForm := AFormClass; + FSeen := False; + FLoggedModal := False; + FStage := psModal; + FDeadline := GetTickCount64 + 6000; + FTimer.Enabled := True; + try + AItem.Click; + Log('MENU RETURN ' + AItem.Name); + finally + FTimer.Enabled := False; + FStage := psIdle; + end; + CheckErrors; + Require(FSeen, AItem.Name + ' did not open its expected modal form'); + end else begin + AItem.Click; + LUntil := GetTickCount64 + 6000; + repeat + Pump(10); + LForm := VisibleForm(AFormClass); + until Assigned(LForm) or (GetTickCount64 >= LUntil); + Require(Assigned(LForm), AItem.Name + ' did not open ' + LClassName); + Require(LForm.HandleAllocated and IsWindowVisible(LForm.Handle), LClassName + ' has no visible native window'); + Capture(LForm, '.' + AItem.Name + '.bmp'); + LForm.Close; + Pump(30); + Require(not Assigned(VisibleForm(AFormClass)), LClassName + ' remained visible after Close'); + end; + Log('MENU ' + AItem.Name + ' opened ' + LClassName + ' and closed normally'); +end; + +procedure TWalletToolsChecks.CheckBlockTools; +var + LForm: TFRMBlockExplorer; + LGrid: TBlockChainGrid; + LUntil: UInt64; + LMenu: HMENU; + LHashRateBlocks: Integer; + LUnits: TShowHashRateAs; + I: Integer; + + procedure AwaitRealRefresh; + begin + LUntil := GetTickCount64+3000; + repeat + Pump(10); + until (LForm.dgBlockChainExplorer.RowCount = 2) or (GetTickCount64 >= LUntil); + Require(LForm.dgBlockChainExplorer.RowCount = 2, + 'Block Tools did not repopulate the actual grid from the validated genesis block'); + end; + +begin + Require(FMain.miBlockExplorer.Enabled,'The real Blocks menu is disabled'); + FMain.miBlockExplorer.Click; + Pump(50); + LForm := TFRMBlockExplorer(VisibleForm(TFRMBlockExplorer)); + Require(Assigned(LForm),'The actual Blocks action did not open Block Explorer'); + try + Require((LForm.miTools.Count = 2) and Assigned(LForm.miRefresh.OnClick) and + Assigned(LForm.miResetBlockFilters.OnClick),'Block Explorer Tools is empty or unwired'); + LMenu := GetSubMenu(GetMenu(LForm.Handle),0); + Require((LMenu <> 0) and (GetMenuItemCount(LMenu) = 2), + 'The native Tools submenu does not contain both actions'); + LGrid := nil; + for I := 0 to LForm.ComponentCount-1 do + if LForm.Components[I] is TBlockChainGrid then + LGrid := TBlockChainGrid(LForm.Components[I]); + Require(Assigned(LGrid),'The actual block-grid controller is missing'); + LHashRateBlocks := LGrid.HashRateAverageBlocksCount; + LUnits := LGrid.HashRateAs; + LForm.ebBlockChainBlockStart.Text := '0'; + LForm.ebBlockChainBlockEnd.Text := '0'; + LForm.ebBlockChainBlockStartExit(nil); + AwaitRealRefresh; + // A real worker must replace this sentinel; an Invalidate alone cannot pass. + LForm.dgBlockChainExplorer.RowCount := 6; + LForm.miRefresh.Click; + AwaitRealRefresh; + Require((LGrid.BlockStart = 0) and (LGrid.BlockEnd = 0) and + (LForm.ebBlockChainBlockStart.Text = '0') and (LForm.ebBlockChainBlockEnd.Text = '0'), + 'Refresh changed the selected block range'); + LForm.dgBlockChainExplorer.RowCount := 6; + LForm.miResetBlockFilters.Click; + AwaitRealRefresh; + Require((LGrid.BlockStart < 0) and (LGrid.BlockEnd < 0) and + (LForm.ebBlockChainBlockStart.Text = '') and (LForm.ebBlockChainBlockEnd.Text = ''), + 'Reset did not clear both displayed and applied range bounds'); + Require((LGrid.HashRateAverageBlocksCount = LHashRateBlocks) and (LGrid.HashRateAs = LUnits), + 'Reset changed the hash-rate settings'); + LForm.dgBlockChainExplorer.RowCount := 6; + LForm.miResetBlockFilters.Click; + AwaitRealRefresh; + Capture(LForm,'.block-tools.bmp'); + Log('PASS Block Explorer Tools native submenu, Refresh, reset both bounds, repeated reset and preserved hash-rate settings'); + finally + LForm.Close; + Pump(50); + end; +end; + +procedure TWalletToolsChecks.CheckOperationInput; +var + LForm: TFRMOperationExplorer; + LGrid: Grids.TDrawGrid; + LController: TOperationsGrid; + LResume: TOperationResume; + LPoint, LScreenPoint, LOtherPoint: TPoint; + LCell: TRect; + LBeforeDialogs, LBeforeTop, LBeforeRow, I: Integer; + + function PointParam(const APoint: TPoint): LPARAM; + begin + Result := LPARAM(Cardinal(APoint.X and $FFFF) or + (Cardinal(APoint.Y and $FFFF) shl 16)); + end; + + procedure Arm(const ACase: String; AExpectedCount: Integer; + AExpectedOperation: Integer = -1); + begin + FInputCase := ACase; + FExpectedOperation := AExpectedOperation; + LBeforeDialogs := FOperationDialogCount; + FStage := psOperationInput; + FDeadline := GetTickCount64 + 4000; + FTimer.Enabled := True; + Log(Format('INPUT %s expected_dialogs=%d expected_operation=%d', + [ACase, AExpectedCount, AExpectedOperation])); + end; + + procedure Complete(AExpectedCount: Integer); + begin + try + Pump(120); + Require(FOperationDialogCount - LBeforeDialogs = AExpectedCount, + Format('%s opened %d operation dialogs; expected %d', + [FInputCase, FOperationDialogCount - LBeforeDialogs, AExpectedCount])); + if AExpectedCount > 0 then + Require(FLastDecodedOperation = Cardinal(FExpectedOperation), + FInputCase + ' opened the wrong operation'); + Require(not Assigned(FObservedOperationDialog), + FInputCase + ' left its operation dialog alive after modal cancellation'); + Log('PASS operation input ' + FInputCase); + finally + FTimer.Enabled := False; + FStage := psIdle; + end; + end; + + procedure ClickAt(const APoint: TPoint; ARightButton: Boolean); + begin + if ARightButton then begin + SendMessage(LGrid.Handle, WM_RBUTTONDOWN, MK_RBUTTON, PointParam(APoint)); + SendMessage(LGrid.Handle, WM_RBUTTONUP, 0, PointParam(APoint)); + end else begin + SendMessage(LGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, PointParam(APoint)); + SendMessage(LGrid.Handle, WM_LBUTTONUP, 0, PointParam(APoint)); + end; + end; + +begin + FMain.miOperationsExplorer.Click; + Pump(50); + LForm := TFRMOperationExplorer(VisibleForm(TFRMOperationExplorer)); + Require(Assigned(LForm), 'The actual Operations Explorer menu did not open its form'); + try + LController := nil; + for I := 0 to LForm.ComponentCount - 1 do + if LForm.Components[I] is TOperationsGrid then + LController := TOperationsGrid(LForm.Components[I]); + Require(Assigned(LController), 'The real operation-grid controller is missing'); + LGrid := LForm.dgOperationsExplorer; + // Only presentation records are needed. Detaching refresh prevents node + // events replacing them; no fabricated transaction is signed or submitted. + LController.Node := nil; + LController.OperationsResume.Clear; + for I := 0 to 39 do begin + LResume := CT_TOperationResume_NUL; + LResume.valid := True; + LResume.Block := 0; + LResume.NOpInsideBlock := I; + LResume.AffectedAccount := I mod 5; + LResume.n_operation := 100 + I; + LResume.OperationTxt := 'Native operation input ' + IntToStr(I); + LController.OperationsResume.Add(LResume); + end; + LGrid.RowCount := LController.OperationsResume.Count + LGrid.FixedRows; + LGrid.SetFocus; + Arm('programmatic-selection', 0); + LGrid.Row := 12; + LGrid.TopRow := 10; + Complete(0); + Require(LGrid.HandleAllocated and IsWindowVisible(LGrid.Handle), + 'Operation input requires the actual visible native grid'); + Require(Assigned(LGrid.OnDrawCell), 'The operation grid lost its real drawing handler'); + + LPoint := Point(LGrid.ClientWidth div 3, LGrid.ClientHeight div 2); + LScreenPoint := LGrid.ClientToScreen(LPoint); + LBeforeTop := LGrid.TopRow; + LBeforeRow := LGrid.Row; + Arm('wheel-down', 0); + SendMessage(LGrid.Handle, WM_MOUSEWHEEL, WPARAM(Cardinal($FF880000)), + PointParam(LScreenPoint)); + Complete(0); + Require((LGrid.TopRow > LBeforeTop) or (LGrid.Row > LBeforeRow), + 'Native wheel-down did not scroll or advance the operation selection'); + + LBeforeTop := LGrid.TopRow; + LBeforeRow := LGrid.Row; + Arm('wheel-up', 0); + SendMessage(LGrid.Handle, WM_MOUSEWHEEL, WPARAM(Cardinal($00780000)), + PointParam(LScreenPoint)); + Complete(0); + Require((LGrid.TopRow < LBeforeTop) or (LGrid.Row < LBeforeRow), + 'Native wheel-up did not scroll or move the operation selection back'); + + LBeforeRow := LGrid.Row; + Arm('keyboard-down', 0); + SendMessage(LGrid.Handle, WM_KEYDOWN, VK_DOWN, 1); + SendMessage(LGrid.Handle, WM_KEYUP, VK_DOWN, LPARAM($C0000001)); + Complete(0); + Require(LGrid.Row = LBeforeRow + 1, 'Native Down did not change operation selection'); + Arm('keyboard-up', 0); + SendMessage(LGrid.Handle, WM_KEYDOWN, VK_UP, 1); + SendMessage(LGrid.Handle, WM_KEYUP, VK_UP, LPARAM($C0000001)); + Complete(0); + Require(LGrid.Row = LBeforeRow, 'Native Up did not restore operation selection'); + + LBeforeTop := LGrid.TopRow; + Arm('scrollbar-down-command', 0); + // Exercise the native scrollbar command path without moving the user's + // pointer or dispatching process-wide keyboard input on their desktop. + SendMessage(LGrid.Handle, WM_VSCROLL, SB_LINEDOWN, 0); + Complete(0); + Require(LGrid.TopRow > LBeforeTop, 'Native scrollbar command did not scroll'); + + LGrid.TopRow := 1; + LGrid.Row := 2; + Pump(30); + LCell := LGrid.CellRect(0, 3); + LPoint := Point((LCell.Left + LCell.Right) div 2, + (LCell.Top + LCell.Bottom) div 2); + Arm('right-click-row', 0); + ClickAt(LPoint, True); + Complete(0); + Arm('orphan-mouse-up', 0); + SendMessage(LGrid.Handle, WM_LBUTTONUP, 0, PointParam(LPoint)); + Complete(0); + Arm('drag-to-another-row', 0); + SendMessage(LGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, PointParam(LPoint)); + LCell := LGrid.CellRect(0, 4); + LOtherPoint := Point((LCell.Left + LCell.Right) div 2, + (LCell.Top + LCell.Bottom) div 2); + SendMessage(LGrid.Handle, WM_MOUSEMOVE, MK_LBUTTON, PointParam(LOtherPoint)); + SendMessage(LGrid.Handle, WM_LBUTTONUP, 0, PointParam(LOtherPoint)); + Complete(0); + Arm('drag-within-same-cell', 0); + LOtherPoint := Point(LPoint.X + GetSystemMetrics(SM_CXDRAG) + 2, LPoint.Y); + Require(LOtherPoint.X < LCell.Right, 'Same-cell drag fixture left its column'); + SendMessage(LGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, PointParam(LPoint)); + SendMessage(LGrid.Handle, WM_MOUSEMOVE, MK_LBUTTON, PointParam(LOtherPoint)); + SendMessage(LGrid.Handle, WM_LBUTTONUP, 0, PointParam(LOtherPoint)); + Complete(0); + Arm('left-click-row', 1, 102); + ClickAt(LPoint, False); + Complete(1); + Require(LGrid.Row = 3, 'Left-click did not select the clicked operation row'); + Arm('repeat-left-click-selected-row', 1, 102); + ClickAt(LPoint, False); + Complete(1); + + LCell := LGrid.CellRect(0, 0); + LPoint := Point((LCell.Left + LCell.Right) div 2, + (LCell.Top + LCell.Bottom) div 2); + Arm('header-click', 0); + ClickAt(LPoint, False); + Complete(0); + + Arm('empty-grid-reset', 0); + LController.OperationsResume.Clear; + LGrid.RowCount := 2; + LGrid.TopRow := 1; + LGrid.Row := 1; + Complete(0); + Arm('empty-placeholder-row', 0); + LCell := LGrid.CellRect(0, 1); + ClickAt(Point((LCell.Left + LCell.Right) div 2, + (LCell.Top + LCell.Bottom) div 2), False); + Complete(0); + Arm('blank-client-area', 0); + ClickAt(Point(20, LGrid.ClientHeight - 8), False); + Complete(0); + Capture(LForm, '.operation-input-empty.bmp'); + Log('PASS Operations Explorer native wheel, keyboard and scrollbar do not open details; only populated-row left clicks do'); + finally + FTimer.Enabled := False; + FStage := psIdle; + LForm.Close; + Pump(50); + end; +end; + +procedure TWalletToolsChecks.RunOperationInput; +begin + CheckConnections; + LoadGenesis; + CheckOperationInput; + CheckWalletOperationDetails(FLogPath); + CheckWalletOperationInfoLayout(FLogPath); + CheckConnections; + CheckErrors; + Log('PASS Operations Explorer click-only details and explicit main-wallet Details checks'); +end; + +procedure TWalletToolsChecks.CheckHistoryDepth; +const + CDepths: array[0..2] of Cardinal = (2016, 8640, High(Cardinal)); +var + LWallet: TMockWallet; + LGrid: TVisualGrid; + LDuration: TComboBox; + LCell: TRect; + LPoint: LPARAM; + LUntil: UInt64; + LCompleted, LHitCol, LHitRow, I: Integer; + LAccounts: TArray<Cardinal>; + + procedure LogSelection(const AStage: String); + var + LSelection: TVisualGridSelection; + LSelected, LSource: TArray<Cardinal>; + LSelectedText, LSourceText: String; + J: Integer; + begin + LSelection := LGrid.Selection; + LSelected := LWallet.SelectedAccounts; + LSource := LWallet.OperationsSource.Accounts; + LSelectedText := ''; + LSourceText := ''; + for J := 0 to High(LSelected) do + LSelectedText := LSelectedText + UIntToStr(LSelected[J]) + ','; + for J := 0 to High(LSource) do + LSourceText := LSourceText + UIntToStr(LSource[J]) + ','; + Log(Format('HISTORY SELECTION %s index=%d page=%d row=%d count=%d selected=[%s] source=[%s] all_wallet=%s mode=%d deselection=%d', + [AStage, I, LSelection.Page, LSelection.Row, LSelection.RowCount, + LSelectedText, LSourceText, BoolToStr(LWallet.OperationsSource.UseWalletAccounts, True), + Ord(LWallet.OperationsMode), Ord(LGrid.DeselectionType)])); + end; + + procedure WaitForOperationQuery(ABefore: Integer); + begin + LUntil := GetTickCount64 + 8000; + repeat + Pump(5); + until ((FMain.Model.Metrics(mdkOperations).CompletedCount > ABefore) and + not LWallet.OperationsGrid.IsUpdating) or (GetTickCount64 >= LUntil); + Require((FMain.Model.Metrics(mdkOperations).CompletedCount > ABefore) and + not LWallet.OperationsGrid.IsUpdating, + 'The actual main-wallet history query did not complete'); + end; + +begin + LWallet := FMain.MockWallet; + FMain.Mode := wmWallet; + FMain.Model.SetData(7401, 5); + LGrid := LWallet.AccountsGrid; + // This scenario needs all five public accounts, independently of the native + // viewport. Auto capacity is covered by the separate grid component tests. + LGrid.PageMode := vpmSingle; + LGrid.RefreshThrottle.Cancel; + LGrid.RefreshGrid; + LUntil := GetTickCount64 + 8000; + while (LGrid.IsUpdating or (LGrid.RowCount <> 5)) and + (GetTickCount64 < LUntil) do + Pump(5); + Require(not LGrid.IsUpdating and (LGrid.RowCount = 5), + 'History picker requires the actual five-row account grid'); + for I := 0 to 4 do + Require(Cardinal(LGrid.Rows[I].__KEY) = Cardinal(I), + 'The history picker must use all five public account IDs'); + Require(LWallet.OperationsGrid.FindComponent('OperationsHistory') is TComboBox, + 'The actual operations-grid duration widget is missing'); + LDuration := TComboBox(LWallet.OperationsGrid.FindComponent('OperationsHistory')); + Require(Assigned(LDuration.OnChange), 'The actual history picker is unwired'); + for I := 0 to High(CDepths) do begin + // Reset a preceding request without replacing production callbacks; + // the operations picker still waits for its configured 250 ms silence. + LWallet.OperationsGrid.RefreshThrottle.Cancel; + LCompleted := FMain.Model.Metrics(mdkOperations).CompletedCount; + LDuration.ItemIndex := I; + LDuration.OnChange(LDuration); + Require(LWallet.OperationsSource.BlockDepth = CDepths[I], + 'History picker configured the wrong block depth'); + WaitForOperationQuery(LCompleted); + Require(LWallet.OperationsSource.LastFetchBlockDepth = CDepths[I], + 'The actual operation query did not receive the picker depth'); + + LWallet.OperationsGrid.RefreshThrottle.Cancel; + LCompleted := FMain.Model.Metrics(mdkOperations).CompletedCount; + // A small native viewport may scroll even this complete five-row result. + // Keep the actual target visible before checking its hit test and click. + LGrid.InternalDrawGrid.TopRow := I + 2; + LCell := LGrid.InternalDrawGrid.CellRect(0, I + 2); + Require((LCell.Left >= 0) and (LCell.Top >= 0) and + (LCell.Right <= LGrid.InternalDrawGrid.ClientWidth) and + (LCell.Bottom <= LGrid.InternalDrawGrid.ClientHeight), + 'The intended history account cell is not fully visible'); + LGrid.InternalDrawGrid.MouseToCell((LCell.Left + LCell.Right) div 2, + (LCell.Top + LCell.Bottom) div 2, LHitCol, LHitRow); + Require((LHitCol = 0) and (LHitRow = I + 2), + 'The native account hit test differs from the intended row'); + Log(Format('HISTORY HIT index=%d cell=%d,%d,%d,%d client=%dx%d hit=%d,%d top_row=%d expected_account=%d actual_row_key=%s', + [I, LCell.Left, LCell.Top, LCell.Right, LCell.Bottom, + LGrid.InternalDrawGrid.ClientWidth, LGrid.InternalDrawGrid.ClientHeight, + LHitCol, LHitRow, LGrid.InternalDrawGrid.TopRow, I + 1, + VarToStr(LGrid.Rows[I + 1].__KEY)])); + LogSelection('before'); + LPoint := LPARAM(Cardinal(((LCell.Left + LCell.Right) div 2) and $FFFF) or + (Cardinal(((LCell.Top + LCell.Bottom) div 2) and $FFFF) shl 16)); + SendMessage(LGrid.InternalDrawGrid.Handle, WM_MOUSEMOVE, 0, LPoint); + SendMessage(LGrid.InternalDrawGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, LPoint); + SendMessage(LGrid.InternalDrawGrid.Handle, WM_LBUTTONUP, 0, LPoint); + WaitForOperationQuery(LCompleted); + LogSelection('after'); + LAccounts := LWallet.OperationsSource.Accounts; + Require((Length(LAccounts) = 1) and (LAccounts[0] = Cardinal(I + 1)), + 'The actual account-row click did not reach the operations source'); + Require(not LWallet.OperationsSource.UseWalletAccounts, + 'Selected-account history unexpectedly queried all wallet accounts'); + Require((LWallet.OperationsSource.BlockDepth = CDepths[I]) and + (LWallet.OperationsSource.LastFetchBlockDepth = CDepths[I]), + 'Changing the selected account reset the requested history depth'); + Log(Format('PASS actual history picker index=%d configured=%s fetched=%s selected_account=%d', + [I, UIntToStr(LWallet.OperationsSource.BlockDepth), + UIntToStr(LWallet.OperationsSource.LastFetchBlockDepth), I + 1])); + end; + Capture(LWallet, '.history-depth-maximum.bmp'); + Log('PASS actual main-wallet 7 Days, 30 Days and Maximum query depths survive account selection'); +end; + +procedure TWalletToolsChecks.RunHistoryDepth; +begin + CheckConnections; + LoadGenesis; + CheckHistoryDepth; + CheckConnections; + CheckErrors; + Log('PASS actual main-wallet history-depth picker checks'); +end; + +procedure TWalletToolsChecks.RunBlockTools; +begin + CheckConnections; + LoadGenesis; + CheckBlockTools; + CheckConnections; + CheckErrors; + Log('PASS actual Block Explorer Tools refresh/reset checks'); +end; + +procedure TWalletToolsChecks.CheckMenus; +begin + CheckMenu(FMain.miAccountExplorer, TFRMAccountExplorer, False); + CheckMenu(FMain.miPendingOperations, TFRMPendingOperations, False); + CheckMenu(FMain.miBlockExplorer, TFRMBlockExplorer, False); + CheckMenu(FMain.miOperationsExplorer, TFRMOperationExplorer, False); + CheckMenu(FMain.miOperationsWorkbench, TFRMOperationsExplorer, True); + CheckMenu(FMain.miRPCCalls, TFRMRPCCalls, True); + CheckMenu(FMain.miLogs, TFRMLogs, False); + CheckMenu(FMain.miNodes, TFRMNodes, False); + CheckMenu(FMain.miMessages, TFRMMessages, False); + Require(FMain.miSyncDialog.Enabled, 'Synchronization menu is disabled'); + FMain.miSyncDialog.Click; + Pump(50); + Require((FMain.Mode = wmSync) and FMain.SyncControl.Visible and FMain.paSyncPanel.Visible, + 'Synchronization menu did not show the usual embedded synchronization view'); + Capture(FMain.SyncControl, '.miSyncDialog.bmp'); + FMain.Mode := wmWallet; + Pump(50); + Log('MENU miSyncDialog opened the embedded synchronization view and returned to wallet'); +end; + +procedure TWalletToolsChecks.PreparePopup(Sender: TObject; + {$IFDEF FPC}constref{$ELSE}const{$ENDIF} ASelection: TVisualGridSelection; + out APopupMenu: TPopupMenu); +begin + FPreviousPrepare(Sender, ASelection, APopupMenu); + Require(ASelection.RowCount = 2, 'Native right-click lost the two selected accounts'); + Require(APopupMenu = FMain.MockWallet.mnuAccountsPopup, 'Native right-click prepared the wrong popup'); + Require(FMain.MockWallet.miChangeKey.Caption = 'Change All Key', 'Native popup did not offer Change All Key'); + FPopupPrepared := True; +end; + +procedure TWalletToolsChecks.CheckWizardGrid(AWizard: TWIZChangeKeyWizard; const ACheckpoint: String); +var + LGrid: TVisualGrid; + LControl: TWinControl; + LMode: TBitBtn; + LModeMenu: TPopupMenu; + LUntil: UInt64; + LReady: Boolean; + I: Integer; +begin + LGrid := nil; + for I := 0 to AWizard.CurrentScreen.ComponentCount - 1 do + if AWizard.CurrentScreen.Components[I] is TVisualGrid then + LGrid := TVisualGrid(AWizard.CurrentScreen.Components[I]); + Require(Assigned(LGrid), ACheckpoint + ': actual wizard screen has no grid'); + LUntil := GetTickCount64 + 1500; + repeat + LReady := AWizard.CurrentScreen.Showing and LGrid.Showing and + LGrid.HandleAllocated and LGrid.InternalDrawGrid.HandleAllocated; + if LReady then + LReady := IsWindowVisible(LGrid.Handle) and IsWindowVisible(LGrid.InternalDrawGrid.Handle); + if not LReady then + Pump(10); + until LReady or (GetTickCount64 >= LUntil); + LControl := LGrid.InternalDrawGrid; + while Assigned(LControl) do begin + Log(Format('WIZARD CHAIN %s class=%s visible=%s showing=%s bounds=%d,%d,%d,%d', + [ACheckpoint, LControl.ClassName, BoolToStr(LControl.Visible, True), + BoolToStr(LControl.Showing, True), LControl.Left, LControl.Top, LControl.Width, LControl.Height])); + LControl := LControl.Parent; + end; + Require(LReady, ACheckpoint + ': current screen and native grid did not become visible'); + Require(Assigned(LGrid.Parent) and LGrid.HandleAllocated and + IsWindow(LGrid.InternalDrawGrid.Handle), ACheckpoint + ': wizard grid has no native parent'); + Require((LGrid.ColCount >= 2) and (LGrid.InternalDrawGrid.ColCount = LGrid.ColCount) and + (LGrid.InternalDrawGrid.Columns.Count = LGrid.ColCount), ACheckpoint + ': native columns are missing'); + Require(LGrid.RowCount = 2, ACheckpoint + ': wizard did not fetch both selected accounts'); + Require(LGrid.InternalDrawGrid.RowCount >= 3, ACheckpoint + ': native grid has no header and two rows'); + Require(LGrid.FindComponent('PageModeButton') is TBitBtn, ACheckpoint + ': mode button is missing'); + Require(LGrid.FindComponent('PageModeMenu') is TPopupMenu, ACheckpoint + ': mode menu is missing'); + LMode := TBitBtn(LGrid.FindComponent('PageModeButton')); + LModeMenu := TPopupMenu(LGrid.FindComponent('PageModeMenu')); + Log(Format('WIZARD MODE %s items=%d mode=%d hint=%s metadata_columns=%d native_columns=%d native_rows=%d', + [ACheckpoint, LModeMenu.Items.Count, Ord(LGrid.PageMode), LMode.Hint, + LGrid.ColCount, LGrid.InternalDrawGrid.ColCount, LGrid.InternalDrawGrid.RowCount])); + Require((LModeMenu.Items.Count = 3) and LModeMenu.Items[Ord(LGrid.PageMode)].Checked, + ACheckpoint + ': native mode menu does not reflect PageMode'); + for I := 0 to 1 do begin + Require(Cardinal(LGrid.Rows[I].AccountNumber) = Cardinal(I), ACheckpoint + ': incorrect account row'); + Require(UInt64(LGrid.Rows[I].Balance) = TNode.Node.GetAccount(I).balance, + ACheckpoint + ': account balance differs from the real public genesis account'); + end; + CheckWizardPaint(LGrid, ACheckpoint); + Capture(LGrid, '.' + ACheckpoint + '-composite-grid.bmp'); + Capture(AWizard.CurrentScreen, '.' + ACheckpoint + '.bmp'); + Log('WIZARD ' + ACheckpoint + ': parented native grid contains both public genesis accounts'); +end; + +procedure TWalletToolsChecks.CheckWizardPaint(AGrid: TVisualGrid; const ACheckpoint: String); +var + LBag: TDisposables; + LBitmap: TBitmap; + LRect: TRect; + LBackground, LPixel: TColor; + LHeaderPixels, LRowPixels, LCount, LRow, X, Y: Integer; +begin + // Paint the actual native draw grid directly, independently of recursive + // form capture, and inspect cell interiors so grid lines cannot pass alone. + LBitmap := LBag.AddObject(PaintControl(AGrid.InternalDrawGrid)) as TBitmap; + LBitmap.SaveToFile(ChangeFileExt(FLogPath, '.' + ACheckpoint + '-native-grid.bmp')); + LHeaderPixels := 0; + LRowPixels := 0; + for LRow := 0 to 2 do begin + LRect := AGrid.InternalDrawGrid.CellRect(0, LRow); + Require((LRect.Right > LRect.Left + 8) and (LRect.Bottom > LRect.Top + 4) and + (LRect.Left >= 0) and (LRect.Top >= 0) and + (LRect.Right <= AGrid.InternalDrawGrid.ClientWidth) and + (LRect.Bottom <= AGrid.InternalDrawGrid.ClientHeight), + ACheckpoint + ': header or row cell does not fit the visible native client'); + Log(Format('WIZARD CELL %s row=%d rect=%d,%d,%d,%d', + [ACheckpoint, LRow, LRect.Left, LRect.Top, LRect.Right, LRect.Bottom])); + InflateRect(LRect, -3, -2); + LBackground := ColorToRGB(LBitmap.Canvas.Pixels[LRect.Right - 1, LRect.Bottom - 1]); + LCount := 0; + for Y := LRect.Top to LRect.Bottom - 1 do + for X := LRect.Left to LRect.Right - 1 do begin + LPixel := ColorToRGB(LBitmap.Canvas.Pixels[X, Y]); + if (Abs(Integer(GetRValue(LPixel)) - Integer(GetRValue(LBackground))) + + Abs(Integer(GetGValue(LPixel)) - Integer(GetGValue(LBackground))) + + Abs(Integer(GetBValue(LPixel)) - Integer(GetBValue(LBackground)))) > 90 then + Inc(LCount); + end; + if LRow = 0 then + LHeaderPixels := LCount + else + Inc(LRowPixels, LCount); + Require(LCount >= 3, ACheckpoint + ': native header or account row painted no cell text'); + end; + Log(Format('WIZARD PIXELS %s header=%d account_rows=%d', + [ACheckpoint, LHeaderPixels, LRowPixels])); +end; + +procedure TWalletToolsChecks.TimerTick(Sender: TObject); +var + LForm: TForm; + LWizard: TWIZChangeKeyWizard; + LKey: TECPrivateKey; + LMessage: String; + LBag: TDisposables; +begin + if FStage in [psIdle, psDone] then + Exit; + try + if FStage = psOperationInput then begin + if FObservingOperationDialog then begin + Log('OPERATION OBSERVER reentry while capturing ' + FInputCase); + Exit; + end; + Require(GetTickCount64 < FDeadline, + 'Timed out observing native operation input ' + FInputCase); + LForm := VisibleForm(TFRMPayloadDecoder); + if Assigned(LForm) then begin + Log(Format('OPERATION OBSERVER %s instance=%s hwnd=%s modal_result=%d observed=%s', + [FInputCase, IntToHex(NativeUInt(LForm), SizeOf(Pointer) * 2), + IntToHex(NativeUInt(LForm.Handle), SizeOf(Pointer) * 2), + LForm.ModalResult, BoolToStr(LForm = FObservedOperationDialog, True)])); + if LForm = FObservedOperationDialog then + Exit; + FObservingOperationDialog := True; + try + Require((fsModal in LForm.FormState) and LForm.HandleAllocated and + IsWindowVisible(LForm.Handle), 'Operation details is not a visible native modal form'); + // A native paint may pump another timer tick before ShowModal hides + // this same form. Count its lifetime once, including allocator reuse. + FObservedOperationDialog := LForm; + LForm.FreeNotification(Self); + Inc(FOperationDialogCount); + Require(FOperationDialogCount < 8, 'Operation input repeatedly reopened its dialog'); + FLastDecodedOperation := TFRMPayloadDecoder(LForm).OpResume.n_operation; + Require(TFRMPayloadDecoder(LForm).OpResume.valid, + FInputCase + ' opened details for an empty operation'); + Capture(LForm, '.operation-input-' + FInputCase + '.bmp'); + LForm.ModalResult := mrCancel; + finally + FObservingOperationDialog := False; + end; + end; + Exit; + end; + if (FStage = psModal) and not FLoggedModal then begin + FLoggedModal := True; + LogModalWindows('first timer observation'); + end; + if (FStage = psModal) and (GetTickCount64 >= FDeadline) then + LogModalWindows('deadline'); + Require(GetTickCount64 < FDeadline, 'Timed out waiting for the actual menu or wizard window'); + case FStage of + psModal: + begin + LForm := VisibleForm(FExpectedForm); + if not Assigned(LForm) then + Exit; + Require((fsModal in LForm.FormState) and LForm.HandleAllocated and + IsWindowVisible(LForm.Handle), FExpectedClass + ' is not a visible native modal form'); + Capture(LForm, '.' + FExpectedClass + '.bmp'); + FSeen := True; + FStage := psDone; + LForm.ModalResult := mrCancel; + end; + psPopup: + begin + if not FPopupPrepared then + Exit; + EndMenu; + FStage := psIdle; + end; + psWizard: + begin + LForm := VisibleForm(TWizardHostForm); + if not Assigned(LForm) then + Exit; + Require(LForm.Owner is TWIZChangeKeyWizard, 'Change All Key did not open its actual wizard'); + LWizard := TWIZChangeKeyWizard(LForm.Owner); + FStage := psDone; + Require(LWizard.CurrentScreen is TWIZOperationSelected, 'Wizard did not begin on selected accounts'); + CheckWizardGrid(LWizard, 'wizard-selected'); + TChangeKeyWizardAccess(LWizard).PresentSelectedAgain; + CheckWizardGrid(LWizard, 'wizard-selected-repeat'); + // Disposable destination material is never imported into the wallet + // or used for signing; only its public key reaches confirmation. + LKey := LBag.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LWizard.Model.ChangeKey.ChangeKeyMode := akaTransferAccountOwnership; + LWizard.Model.TransferAccount.AccountKey := LKey.PublicKey; + LWizard.Model.Fee.SingleOperationFee := 0; + LWizard.Model.Signer.OperationSigningMode := akaPrimary; + LWizard.Model.Signer.SignerAccount := LWizard.Model.Account.SelectedAccounts[0]; + TChangeKeyWizardAccess(LWizard).PresentConfirmation; + CheckWizardGrid(LWizard, 'wizard-confirmation'); + Require(LWizard.Cancel(LMessage), 'Wizard Cancel failed: ' + LMessage); + Require(LWizard.State = wsCancelled, 'Wizard did not enter its cancelled state'); + FWizardCancelled := True; + end; + end; + except + on E: Exception do begin + if FError = '' then + FError := E.ClassName + ': ' + E.Message; + FStage := psDone; + CloseProbeDialogs; + end; + end; +end; + +procedure TWalletToolsChecks.CheckWizard; +var + LGrid: TVisualGrid; + LCell: TRect; + LPoint: LPARAM; + LUntil: UInt64; + I: Integer; + LPreviousPopup: TPreparePopupMenuEvent; + LPreviousEnabled: Boolean; +begin + FMain.Model.SetData(7301, 5); + LGrid := FMain.MockWallet.AccountsGrid; + // Native popup/wizard checks need these five IDs, not an Auto-sized slice. + LGrid.PageMode := vpmSingle; + LGrid.RefreshThrottle.Cancel; + LGrid.RefreshGrid; + LUntil := GetTickCount64 + 8000; + while (LGrid.IsUpdating or LGrid.RefreshThrottle.Waiting or (LGrid.RowCount <> 5)) and + (GetTickCount64 < LUntil) do + Pump(5); + Require(not LGrid.IsUpdating and (LGrid.RowCount = 5), 'Mock wallet did not display five public account IDs'); + for I := 0 to 4 do + Require(Cardinal(LGrid.Rows[I].__KEY) = Cardinal(I), + 'The native wizard must use all five public account IDs'); + LCell := LGrid.InternalDrawGrid.CellRect(0, 1); + LPoint := LPARAM(((LCell.Top + LCell.Bottom) div 2) shl 16) or + LPARAM((LCell.Left + LCell.Right) div 2); + SendMessage(LGrid.InternalDrawGrid.Handle, WM_MOUSEMOVE, 0, LPoint); + SendMessage(LGrid.InternalDrawGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, LPoint); + SendMessage(LGrid.InternalDrawGrid.Handle, WM_LBUTTONUP, 0, LPoint); + // LCL exposes a rectangle setter; the VCL adapter extends its native range. + {$IFDEF FPC} + LGrid.InternalDrawGrid.Selection := Types.Rect(0, 1, 0, 2); + {$ELSE} + LGrid.InternalDrawGrid.MoveExtend(True, 0, 2); + {$ENDIF} + Require(Length(LGrid.SelectedRows) = 2, 'Native account selection did not contain two rows'); + LPreviousPopup := LGrid.OnPreparePopupMenu; + LPreviousEnabled := FMain.MockWallet.miChangeKey.Enabled; + // The ordinary synthetic demo intentionally disables monetary popups. Borrow + // the actual wallet method only for this genesis-backed, cancel-only check. + FPreviousPrepare := TMockWalletPopupAccess(FMain.MockWallet).NativeAccountPopupHandler(); + Require(Assigned(FPreviousPrepare) and + (TMethod(FPreviousPrepare).Data = Pointer(FMain.MockWallet)), 'Actual account popup handler is not bound to this wallet'); + LGrid.OnPreparePopupMenu := PreparePopup; + FMain.MockWallet.miChangeKey.Enabled := True; + FPopupPrepared := False; + FWizardCancelled := False; + FStage := psPopup; + FDeadline := GetTickCount64 + 6000; + FTimer.Enabled := True; + try + // Dismiss the native popup before invoking its actual action; the timer is + // then free to observe the wizard inside ShowModal on both VCL and LCL. + SendMessage(LGrid.InternalDrawGrid.Handle, WM_RBUTTONUP, 0, LPoint); + CheckErrors; + Require(FPopupPrepared, 'Native right-click did not prepare the account popup'); + FStage := psWizard; + FDeadline := GetTickCount64 + 6000; + FMain.MockWallet.miChangeKey.Click; + finally + FTimer.Enabled := False; + FStage := psIdle; + LGrid.OnPreparePopupMenu := LPreviousPopup; + FMain.MockWallet.miChangeKey.Enabled := LPreviousEnabled; + end; + CheckErrors; + Require(FPopupPrepared and FWizardCancelled, 'Native Change All Key did not present and cancel both wizard screens'); + Require(TNode.Node.Bank.BlocksCount = 1, 'Wizard inspection changed confirmed chain state'); + Require(TNode.Node.MempoolOperationsCount = 0, 'Wizard inspection submitted an operation'); + Log('WIZARD actual native two-account popup, Change All Key, selected/repeated/confirmation screens, Cancel; no Finish or broadcast'); +end; + +function TWalletToolsChecks.PaintControl(AControl: TWinControl): TBitmap; +begin + Result := TBitmap.Create; + try + Result.PixelFormat := pf24bit; + Result.SetSize(AControl.ClientWidth, AControl.ClientHeight); + {$IFDEF FPC} + // Native group-box WM_PRINT returns no pixels on the private LCL desktop. + // Dispatch its real paint handler, including graphic labels, into the DC. + Result.Canvas.Brush.Color := AControl.GetRGBColorResolvingParent; + Result.Canvas.FillRect(AControl.ClientRect); + AControl.Perform(LM_ERASEBKGND, WPARAM(Result.Canvas.Handle), 0); + AControl.Perform(LM_PAINT, WPARAM(Result.Canvas.Handle), 0); + {$ELSE} + AControl.PaintTo(Result.Canvas.Handle, 0, 0); + {$ENDIF} + except + Result.Free; + raise; + end; +end; + +procedure TWalletToolsChecks.Capture(AControl: TWinControl; const ASuffix: String); +var + LBag: TDisposables; + LBitmap: TBitmap; +begin + LBitmap := LBag.AddObject(PaintControl(AControl)) as TBitmap; + LBitmap.SaveToFile(ChangeFileExt(FLogPath, ASuffix)); +end; + +procedure TWalletToolsChecks.CheckOverview(const ACheckpoint: String); +var + LGroup: TGroupBox; + LContent: TRect; + LTop, LBottom, LUpperGap, LLowerGap: Integer; +begin + LGroup := FMain.MockWallet.GroupBox1; + Require(LGroup.HandleAllocated, ACheckpoint + ': Overview group has no native window'); + // Measure the native usable content rather than repeating the layout formula. + LContent := LGroup.ClientRect; + TGroupBoxAccess(LGroup).AdjustClientRect(LContent); + with FMain.MockWallet do begin + Require(Label1.Top = lblTotalPASC.Top, ACheckpoint + ': PASC label pair is misaligned'); + Require(Label2.Top = lblTotalPASA.Top, ACheckpoint + ': PASA label pair is misaligned'); + LTop := Min(Label1.Top, lblTotalPASC.Top); + LBottom := Max(Label2.Top + Label2.Height, lblTotalPASA.Top + lblTotalPASA.Height); + Require(Max(Label1.Top + Label1.Height, lblTotalPASC.Top + lblTotalPASC.Height) < + Min(Label2.Top, lblTotalPASA.Top), ACheckpoint + ': Overview rows overlap'); + end; + LUpperGap := LTop - LContent.Top; + LLowerGap := LContent.Bottom - LBottom; + Require((LUpperGap >= 0) and (LLowerGap >= 0) and (Abs(LUpperGap - LLowerGap) <= 1), + Format('%s: Overview labels are not centered in native content (gaps %d/%d)', + [ACheckpoint, LUpperGap, LLowerGap])); + Capture(LGroup, '.overview-' + ACheckpoint + '.bmp'); + Log(Format('OVERVIEW %s native content=%d..%d labels=%d..%d gaps=%d/%d', + [ACheckpoint, LContent.Top, LContent.Bottom, LTop, LBottom, LUpperGap, LLowerGap])); +end; + +procedure TWalletToolsChecks.Run; +var + LBounds: TRect; +begin + CheckConnections; + LoadGenesis; + CheckMenus; + CheckOverview('default'); + LBounds := FMain.BoundsRect; + FMain.ScaleBy(3, 2); + try + Pump(100); + CheckOverview('scaled-150'); + finally + FMain.ScaleBy(2, 3); + FMain.BoundsRect := LBounds; + Pump(100); + end; + CheckWizard; + CheckConnections; + CheckErrors; + Log('PASS actual Tools menu forms, native Change All Key wizard/cancel and Overview geometry'); +end; + +function CheckWalletToolsDialogs(AMain: TMockMainForm; const ALogPath: String): String; +var + LBag: TDisposables; + LChecks: TWalletToolsChecks; +begin + // Returns the name of the checks that ran, for the caller's main-log marker. + LChecks := LBag.AddObject(TWalletToolsChecks.Create(AMain, ALogPath)) as TWalletToolsChecks; + if SameText(GetEnvironmentVariable('PASCAL_WALLET_HISTORY_DEPTH_ONLY'),'1') then begin + LChecks.RunHistoryDepth; + Result := 'actual main-wallet history-depth picker checks'; + end else if SameText(GetEnvironmentVariable('PASCAL_WALLET_OPERATION_INPUT_ONLY'),'1') then begin + LChecks.RunOperationInput; + Result := 'Operations Explorer click-only details and explicit main-wallet Details checks'; + end else if SameText(GetEnvironmentVariable('PASCAL_WALLET_BLOCK_TOOLS_ONLY'),'1') then begin + LChecks.RunBlockTools; + Result := 'actual Block Explorer Tools refresh/reset checks'; + end else begin + LChecks.Run; + Result := 'actual Tools forms, Change All Key wizard and Overview geometry checks'; + end; +end; + +end. diff --git a/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.Assets.rc b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.Assets.rc new file mode 100644 index 000000000..ecb20d7df --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.Assets.rc @@ -0,0 +1,2 @@ +// Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +MAINICON ICON "../../../../../resources/wallet/PascalCoin.ico" diff --git a/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.dpr b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.dpr new file mode 100644 index 000000000..87d4f0619 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.dpr @@ -0,0 +1,18 @@ +program WalletGridRefreshDemo; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Fresh offline mock wallet demo. Use the existing private-desktop broker and + one output log filename to execute its automatic refresh scenarios. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +uses UWalletGridRefreshHarness; + +{$R 'WalletGridRefreshDemo.Assets.res'} + +begin + ExitCode := RunWalletGridRefreshDemo; +end. diff --git a/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.dproj b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.dproj new file mode 100644 index 000000000..93460c836 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.dproj @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{9385C8A8-5B45-4F9E-A9E8-7530385B160C}</ProjectGuid> + <MainSource>WalletGridRefreshDemo.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext;..\..\framework</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\grid-refresh\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\grid-refresh\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\grid-refresh\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="WalletGridRefreshDemo.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletGridMocks.pas" /> + <DCCReference Include="UWalletGridRefreshHarness.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">WalletGridRefreshDemo.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.lpi b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.lpi new file mode 100644 index 000000000..3dfec3ebb --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.lpi @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<CONFIG> + <ProjectOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <General> + <Flags> + <MainUnitHasUsesSectionForAllUnits Value="False"/> + <MainUnitHasCreateFormStatements Value="False"/> + <MainUnitHasTitleStatement Value="False"/> + </Flags> + <SessionStorage Value="InProjectDir"/> + <MainUnit Value="0"/> + <Title Value="PascalCoin mock grid refresh demo"/> + <ResourceType Value="res"/> + </General> + <BuildModes Count="1"><Item1 Name="Default" Default="True"/></BuildModes> + <RequiredPackages Count="1"><Item1><PackageName Value="LCL"/></Item1></RequiredPackages> + <Units Count="3"> + <Unit0><Filename Value="WalletGridRefreshDemo.lpr"/><IsPartOfProject Value="True"/></Unit0> + <Unit1><Filename Value="UWalletGridMocks.pas"/><IsPartOfProject Value="True"/></Unit1> + <Unit2><Filename Value="UWalletGridRefreshHarness.pas"/><IsPartOfProject Value="True"/></Unit2> + </Units> + </ProjectOptions> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <Target><Filename Value="..\..\..\..\WalletGridRefreshDemoFPC"/></Target> + <SearchPaths> + <IncludeFiles Value="$(ProjOutDir);..\..\..\..\libraries\generics.collections\inc"/> + <OtherUnitFiles Value="..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex;..\..\framework"/> + <UnitOutputDirectory Value="..\..\lib\grid-refresh\FPC\$(TargetCPU)-$(TargetOS)\units"/> + </SearchPaths> + <Parsing><SyntaxOptions><SyntaxMode Value="Delphi"/></SyntaxOptions></Parsing> + <CodeGeneration><Checks><RangeChecks Value="True"/><OverflowChecks Value="True"/></Checks></CodeGeneration> + <Linking> + <Debugging><GenerateDebugInfo Value="True"/></Debugging> + <Options><Win32><GraphicApplication Value="True"/></Win32></Options> + </Linking> + <Other><CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL"/></Other> + </CompilerOptions> +</CONFIG> diff --git a/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.lpr b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.lpr new file mode 100644 index 000000000..2b16cb119 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletGridRefreshDemo.lpr @@ -0,0 +1,20 @@ +program WalletGridRefreshDemo; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Fresh offline mock wallet demo. Use the existing private-desktop broker and + one output log filename to execute its automatic refresh scenarios. + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$mode delphi} + +uses Interfaces, UWalletGridRefreshHarness; + +{$R WalletGridRefreshDemo.Assets.rc} + +begin + ExitCode := RunWalletGridRefreshDemo; +end. diff --git a/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.dpr b/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.dpr new file mode 100644 index 000000000..7de9192af --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.dpr @@ -0,0 +1,11 @@ +program WalletNetworkOffProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses UWalletNetworkOffChecks; + +{$R WalletGridRefreshDemo.Assets.res WalletGridRefreshDemo.Assets.rc} + +begin + ExitCode := RunWalletNetworkOffProbe; +end. diff --git a/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.dproj b/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.dproj new file mode 100644 index 000000000..7d1f172d9 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.dproj @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{D326217C-56B3-4654-8C19-4A27322B932F}</ProjectGuid> + <MainSource>WalletNetworkOffProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-off\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-off\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-off\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="WalletGridRefreshDemo.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletGridMocks.pas" /> + <DCCReference Include="UWalletNetworkOffChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">WalletNetworkOffProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.lpr b/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.lpr new file mode 100644 index 000000000..a4b55c7d3 --- /dev/null +++ b/src/tests/experimental/native/grid-refresh/WalletNetworkOffProbe.lpr @@ -0,0 +1,15 @@ +program WalletNetworkOffProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletNetworkOffChecks; + +{$R WalletGridRefreshDemo.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + ExitCode := RunWalletNetworkOffProbe; +end. diff --git a/src/tests/experimental/native/grid-refresh/grid-refresh-waiting.png b/src/tests/experimental/native/grid-refresh/grid-refresh-waiting.png new file mode 100644 index 000000000..bffa9bed6 Binary files /dev/null and b/src/tests/experimental/native/grid-refresh/grid-refresh-waiting.png differ diff --git a/src/tests/experimental/native/log-gui-handoff/LogGuiHandoffProbe.dpr b/src/tests/experimental/native/log-gui-handoff/LogGuiHandoffProbe.dpr new file mode 100644 index 000000000..c48581c38 --- /dev/null +++ b/src/tests/experimental/native/log-gui-handoff/LogGuiHandoffProbe.dpr @@ -0,0 +1,28 @@ +program LogGuiHandoffProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE CONSOLE}{$ENDIF} + +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, ULogGuiHandoffChecks; + +begin + {$IFDEF FPC} + if ParamCount>0 then + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + {$ELSE} + ReportMemoryLeaksOnShutdown := True; + {$ENDIF} + if not (ParamCount in [1,2]) then + Halt(64); + if (ParamCount=2) and (ParamStr(2)<>'producer') then + Halt(64); + try + ExitCode := RunLogGuiHandoffChecks(ExpandFileName(ParamStr(1)),ParamCount=2); + except + on E: Exception do begin + WriteLn(E.ClassName+': '+E.Message); + ExitCode := 1; + end; + end; +end. diff --git a/src/tests/experimental/native/log-gui-handoff/LogGuiHandoffProbe.lpr b/src/tests/experimental/native/log-gui-handoff/LogGuiHandoffProbe.lpr new file mode 100644 index 000000000..c48581c38 --- /dev/null +++ b/src/tests/experimental/native/log-gui-handoff/LogGuiHandoffProbe.lpr @@ -0,0 +1,28 @@ +program LogGuiHandoffProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE CONSOLE}{$ENDIF} + +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, ULogGuiHandoffChecks; + +begin + {$IFDEF FPC} + if ParamCount>0 then + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + {$ELSE} + ReportMemoryLeaksOnShutdown := True; + {$ENDIF} + if not (ParamCount in [1,2]) then + Halt(64); + if (ParamCount=2) and (ParamStr(2)<>'producer') then + Halt(64); + try + ExitCode := RunLogGuiHandoffChecks(ExpandFileName(ParamStr(1)),ParamCount=2); + except + on E: Exception do begin + WriteLn(E.ClassName+': '+E.Message); + ExitCode := 1; + end; + end; +end. diff --git a/src/tests/experimental/native/log-gui-handoff/README.md b/src/tests/experimental/native/log-gui-handoff/README.md new file mode 100644 index 000000000..48913831e --- /dev/null +++ b/src/tests/experimental/native/log-gui-handoff/README.md @@ -0,0 +1,14 @@ +# Logger GUI handoff checks + +Six checks verify that the logger releases its producer queue before calling a +GUI receiver. They also cover FIFO and reentrant next-batch delivery, callback +and batch-hook exceptions, empty batches, unsubscribe cleanup, and legacy +callbacks and file output. + +Build `LogGuiHandoffProbe.lpr` (FPC) or `LogGuiHandoffProbe.dpr` (Delphi) and +pass a new report path. Exit code zero and `PASS 6` mean success. The probe +starts no node, wallet or network connection. + +`ULogGuiHandoffTests` registers the same checks in `ExperimentalWalletTests` and +the [complete Windows test runner](../../README.md), logging details to +`LogGuiHandoff-<GUID>.log` in `TEMP`. diff --git a/src/tests/experimental/native/log-memo-batch/ClassicLogMemoBatchProbe.dpr b/src/tests/experimental/native/log-memo-batch/ClassicLogMemoBatchProbe.dpr new file mode 100644 index 000000000..cfd161ce5 --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/ClassicLogMemoBatchProbe.dpr @@ -0,0 +1,14 @@ +program ClassicLogMemoBatchProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE GUI}{$ENDIF} + +uses {$IFDEF FPC}Interfaces,{$ENDIF} SysUtils, UClassicLogMemoBatchChecks; + +begin + {$IFNDEF FPC}ReportMemoryLeaksOnShutdown := True;{$ENDIF} + if ParamCount<>1 then + Halt(64); + ExitCode := RunClassicLogMemoBatchChecks(ExpandFileName(ParamStr(1))); +end. diff --git a/src/tests/experimental/native/log-memo-batch/ClassicLogMemoBatchProbe.lpr b/src/tests/experimental/native/log-memo-batch/ClassicLogMemoBatchProbe.lpr new file mode 100644 index 000000000..cfd161ce5 --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/ClassicLogMemoBatchProbe.lpr @@ -0,0 +1,14 @@ +program ClassicLogMemoBatchProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE GUI}{$ENDIF} + +uses {$IFDEF FPC}Interfaces,{$ENDIF} SysUtils, UClassicLogMemoBatchChecks; + +begin + {$IFNDEF FPC}ReportMemoryLeaksOnShutdown := True;{$ENDIF} + if ParamCount<>1 then + Halt(64); + ExitCode := RunClassicLogMemoBatchChecks(ExpandFileName(ParamStr(1))); +end. diff --git a/src/tests/experimental/native/log-memo-batch/LogMemoBatchBroker.lpr b/src/tests/experimental/native/log-memo-batch/LogMemoBatchBroker.lpr new file mode 100644 index 000000000..f11460182 --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/LogMemoBatchBroker.lpr @@ -0,0 +1,64 @@ +program LogMemoBatchBroker; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Runs only the requested owned probe on a separate, undisplayed desktop. + It waits for natural exit and never terminates a thread or process. } + +{$MODE Delphi} + +uses Windows, SysUtils; + +function QASetThreadExecutionState(AFlags: DWORD): DWORD; stdcall; external 'kernel32.dll' name 'SetThreadExecutionState'; + +var + LDesktop: HDESK; + LStartup: TStartupInfo; + LChild: TProcessInformation; + LName, LDesktopPath, LExe, LLog, LCommand: String; + LExit, LPreviousExecutionState: DWORD; +begin + if ParamCount<>2 then + Halt(64); + LExe := ExpandFileName(ParamStr(1)); + LLog := ExpandFileName(ParamStr(2)); + if not SameText(ExtractFilePath(LExe),ExtractFilePath(LLog)) or FileExists(LLog) then + Halt(65); + LPreviousExecutionState := QASetThreadExecutionState($80000001); + if LPreviousExecutionState=0 then + RaiseLastOSError; + try + LName := 'LogMemoBatchQA_'+IntToStr(GetCurrentProcessID); + LDesktop := CreateDesktop(PChar(LName),nil,nil,0,GENERIC_ALL,nil); + if LDesktop=0 then + RaiseLastOSError; + try + FillChar(LStartup,SizeOf(LStartup),0); + FillChar(LChild,SizeOf(LChild),0); + LStartup.cb := SizeOf(LStartup); + LDesktopPath := 'WinSta0\'+LName; + LStartup.lpDesktop := PChar(LDesktopPath); + LStartup.dwFlags := STARTF_USESHOWWINDOW; + LStartup.wShowWindow := SW_HIDE; + LCommand := '"'+LExe+'" "'+LLog+'"'; + if not CreateProcess(nil,PChar(LCommand),nil,nil,False,0,nil,nil,@LStartup,@LChild) then + RaiseLastOSError; + try + WriteLn('OWNED PROBE PID=',LChild.dwProcessId,' desktop=',LDesktopPath,' inputDesktopUnchanged=True'); + Flush(Output); + if WaitForSingleObject(LChild.hProcess,INFINITE)<>WAIT_OBJECT_0 then + RaiseLastOSError; + if not GetExitCodeProcess(LChild.hProcess,LExit) then + RaiseLastOSError; + WriteLn('NATURAL EXIT code=',LExit); + finally + CloseHandle(LChild.hThread); + CloseHandle(LChild.hProcess); + end; + finally + CloseDesktop(LDesktop); + end; + finally + QASetThreadExecutionState(LPreviousExecutionState or $80000000); + end; + Halt(LExit); +end. diff --git a/src/tests/experimental/native/log-memo-batch/LogMemoBatchProbe.dpr b/src/tests/experimental/native/log-memo-batch/LogMemoBatchProbe.dpr new file mode 100644 index 000000000..68566aa6f --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/LogMemoBatchProbe.dpr @@ -0,0 +1,14 @@ +program LogMemoBatchProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE GUI}{$ENDIF} + +uses {$IFDEF FPC}Interfaces,{$ENDIF} SysUtils, ULogMemoBatchChecks; + +begin + {$IFNDEF FPC}ReportMemoryLeaksOnShutdown := True;{$ENDIF} + if ParamCount<>1 then + Halt(64); + ExitCode := RunLogMemoBatchChecks(ExpandFileName(ParamStr(1))); +end. diff --git a/src/tests/experimental/native/log-memo-batch/LogMemoBatchProbe.lpr b/src/tests/experimental/native/log-memo-batch/LogMemoBatchProbe.lpr new file mode 100644 index 000000000..68566aa6f --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/LogMemoBatchProbe.lpr @@ -0,0 +1,14 @@ +program LogMemoBatchProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$MODE Delphi}{$ELSE}{$APPTYPE GUI}{$ENDIF} + +uses {$IFDEF FPC}Interfaces,{$ENDIF} SysUtils, ULogMemoBatchChecks; + +begin + {$IFNDEF FPC}ReportMemoryLeaksOnShutdown := True;{$ENDIF} + if ParamCount<>1 then + Halt(64); + ExitCode := RunLogMemoBatchChecks(ExpandFileName(ParamStr(1))); +end. diff --git a/src/tests/experimental/native/log-memo-batch/README.md b/src/tests/experimental/native/log-memo-batch/README.md new file mode 100644 index 000000000..195c320a4 --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/README.md @@ -0,0 +1,18 @@ +# Log memo batch regression + +Checks that the experimental log form shows each batch in one memo update. It +keeps formatting, the 300-line limit, multiline text, debug filtering and +immediate direct callbacks. Empty and fully filtered batches change nothing. +`-Classic` checks the classic form. + +```powershell +.\RunLogMemoBatchChecks.ps1 -OutputDirectory <new-folder> +.\RunLogMemoBatchChecks.ps1 -Classic -OutputDirectory <another-new-folder> +``` + +The output directory must be new. The probe runs offline with new data, and the +script gives it a private desktop. + +For Delphi, build `LogMemoBatchProbe.dpr` or `ClassicLogMemoBatchProbe.dpr` with +the matching wallet's search paths. Put the OpenSSL runtime beside it and pass +only a new output-log path. diff --git a/src/tests/experimental/native/log-memo-batch/RunLogMemoBatchChecks.ps1 b/src/tests/experimental/native/log-memo-batch/RunLogMemoBatchChecks.ps1 new file mode 100644 index 000000000..a6c4a0a46 --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/RunLogMemoBatchChecks.ps1 @@ -0,0 +1,60 @@ +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [string]$FpcPath=$(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string]$LazarusDirectory=$(if ($c = Get-Command lazbuild -ErrorAction SilentlyContinue) { Split-Path -Parent $c.Source } else { '' }), + [Parameter(Mandatory=$true)][string]$OutputDirectory, + [switch]$Classic +) +$ErrorActionPreference='Stop' +$compiler=(Resolve-Path -LiteralPath $FpcPath).Path +$lazarus=(Resolve-Path -LiteralPath $LazarusDirectory).Path +$source=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../../../..')).Path +$targetInfo=((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if($LASTEXITCODE-ne0-or$targetInfo.Count-ne2-or$targetInfo[0]-ne'x86_64'-or$targetInfo[1]-ne'win64'){ + throw 'The native memo regression requires Windows x64 Free Pascal.' +} +$target=$targetInfo -join '-' +$output=[IO.Path]::GetFullPath($OutputDirectory) +if(Test-Path -LiteralPath $output){throw 'Choose a fresh output directory.'} +foreach($directory in @('units','broker-units','temporary-data')){ + New-Item -ItemType Directory -Path (Join-Path $output $directory) -Force|Out-Null +} +$programName=if($Classic){'ClassicLogMemoBatchProbe'}else{'LogMemoBatchProbe'} +$exe=Join-Path $output ($programName+'.exe') +$arguments=@('-B','-MDelphi','-Scghi','-O2','-WG','-dRELEASE','-dLCL','-dLCLwin32', + '-dBorland','-dVer150','-dDelphi7','-dCompiler6_Up','-dPUREPASCAL', + ('-FU'+(Join-Path $output 'units')),('-FE'+$output),('-o'+$exe), + ('-Fi'+(Join-Path $source 'libraries/generics.collections/inc')),('-Fu'+$PSScriptRoot), + ('-Fu'+$lazarus+'/lcl/units/'+$target+'/win32'),('-Fu'+$lazarus+'/lcl/units/'+$target), + ('-Fu'+$lazarus+'/components/lazutils/lib/'+$target),('-Fu'+$lazarus+'/packager/units/'+$target)) +$directories=@('core','gui-experimental','gui-experimental/core.utils','gui-experimental/wizards/wallet', + 'gui-experimental/wizards/operations','gui-classic','libraries/simplebaselib4pascal','libraries/hashlib4pascal', + 'libraries/cryptolib4pascal','libraries/synapse','libraries/sphere10','libraries/generics.collections', + 'libraries/pascalcoin','libraries/paszlib','libraries/abstractmem','libraries/regex','libraries/gnugettext') +if($Classic){$directories=@('gui-classic')+@($directories|Where-Object{$_-ne'gui-classic'-and$_-notlike'gui-experimental*'})} +foreach($directory in $directories){ + $arguments+='-Fu'+(Join-Path $source $directory) +} +$arguments+=Join-Path $PSScriptRoot ($programName+'.lpr') +$arguments|Set-Content -LiteralPath (Join-Path $output 'compiler-arguments.txt') +& $compiler @arguments *> (Join-Path $output 'build.log') +if($LASTEXITCODE-ne0){Get-Content -LiteralPath (Join-Path $output 'build.log') -Tail 40;throw 'Native memo probe did not compile.'} +# Use the tracked runtime so these probes also run from a clean checkout. +$crypto=Join-Path (Split-Path -Parent $source) 'windows-dll/libcrypto-1_1-x64.dll' +Copy-Item -LiteralPath $crypto -Destination $output +$broker=Join-Path $output 'LogMemoBatchBroker.exe' +& $compiler '-MDelphi' '-O2' ('-FU'+(Join-Path $output 'broker-units')) ('-o'+$broker) (Join-Path $PSScriptRoot 'LogMemoBatchBroker.lpr') *> (Join-Path $output 'broker-build.log') +if($LASTEXITCODE-ne0){throw 'Native memo probe broker did not compile.'} +$oldTemp,$oldTmp=$env:TEMP,$env:TMP +try{ + $env:TEMP=Join-Path $output 'temporary-data';$env:TMP=$env:TEMP + $log=Join-Path $output 'checks.log' + & $broker $exe $log *> (Join-Path $output 'runtime-broker.log') + $code=$LASTEXITCODE +}finally{$env:TEMP,$env:TMP=$oldTemp,$oldTmp} +if(-not(Test-Path -LiteralPath $log)){throw 'Native memo probe omitted its result log.'} +$text=[IO.File]::ReadAllText($log) +Write-Output $text +if($code-ne0-or$text-match'(?m)^FAIL'-or$text-notmatch'(?m)^PASS 8\s*$'){ + throw 'Native memo batch checks failed; evidence is retained in the output directory.' +} diff --git a/src/tests/experimental/native/log-memo-batch/UClassicLogMemoBatchChecks.pas b/src/tests/experimental/native/log-memo-batch/UClassicLogMemoBatchChecks.pas new file mode 100644 index 000000000..79ce01ba5 --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/UClassicLogMemoBatchChecks.pas @@ -0,0 +1,345 @@ +unit UClassicLogMemoBatchChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Verifies real log-form publication and retained text through native memo + messages. The reference memo applies the original per-record retention rules. } + +{$IFDEF FPC}{$MODE Delphi}{$H+}{$ENDIF} + +interface + +function RunClassicLogMemoBatchChecks(const ALogPath: String): Integer; + +implementation + +uses + {$IFDEF FPC}Windows,{$ELSE}Winapi.Windows, Winapi.Messages,{$ENDIF} + SysUtils, Classes, Forms, Controls, StdCtrls, UAppParams, UMemory, UCommon, + UFRMWallet, USettings, UNetProtocol, UCrypto, ULog, UNode, + UConst; + +type + TClassicLogAccess = class(TFRMWallet); + + TLogMemoChecks = class + private + FForm: TFRMWallet; + FReferenceHost: TForm; + FReference: TMemo; + FLog: TLog; + FDispatch: TThreadSafeLogEvent; + FPreviousRecord: TNewLogEvent; + FPreviousBegin, FPreviousEnd: TNotifyEvent; + FPreviousGlobal: Boolean; + FPreviousWindowProc: Pointer; + {$IFNDEF FPC} + FPreviousMemoProc: TWndMethod; + {$ENDIF} + FMemoHandle: HWND; + FSetText, FReplaceSelection, FDeliveredBatches: Integer; + FLines: TStringList; + {$IFNDEF FPC} + procedure MemoDispatch(var AMessage: TMessage); + {$ENDIF} + procedure Receive(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); + procedure BeginBatch(Sender: TObject); + procedure EndBatch(Sender: TObject); + procedure ReferenceAppend(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); + procedure CheckBatch(const AName: String); + procedure CheckUnchangedBatch(const AName: String); + procedure Install; + procedure Uninstall; + public + constructor Create; + destructor Destroy; override; + procedure Run; + end; + +var ActiveChecks: TLogMemoChecks; + +procedure Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then + raise Exception.Create(AMessage); +end; + +function MemoMessage(AWindow: HWND; AMessage: UINT; AWParam: WPARAM; ALParam: LPARAM): LRESULT; stdcall; +begin + if AMessage=WM_SETTEXT then + Inc(ActiveChecks.FSetText) + else if AMessage=EM_REPLACESEL then + Inc(ActiveChecks.FReplaceSelection); + Result := CallWindowProcW(ActiveChecks.FPreviousWindowProc,AWindow,AMessage,AWParam,ALParam); +end; + +{$IFNDEF FPC} +procedure TLogMemoChecks.MemoDispatch(var AMessage: TMessage); +begin + // VCL Perform calls the saved native edit procedure directly; a window + // subclass misses those messages. Observe its real control dispatch instead. + if AMessage.Msg=WM_SETTEXT then + Inc(FSetText) + else if AMessage.Msg=EM_REPLACESEL then + Inc(FReplaceSelection); + FPreviousMemoProc(AMessage); +end; +{$ENDIF} + +constructor TLogMemoChecks.Create; +begin + inherited Create; + FLines := TStringList.Create; +end; + +destructor TLogMemoChecks.Destroy; +begin + Uninstall; + FDispatch.Free; + FReferenceHost.Free; + FLines.Free; + inherited Destroy; +end; + +procedure TLogMemoChecks.Install; +begin + FForm := FRMWallet; + Require(Assigned(FForm),'actual Classic form was not created'); + FLog := TClassicLogAccess(FForm).FLog; + Require(Assigned(FLog) and Assigned(FLog.OnNewLog),'actual log-form subscription is missing'); + FPreviousRecord := FLog.OnNewLog; + FPreviousBegin := FLog.OnNewLogBatchBegin; + FPreviousEnd := FLog.OnNewLogBatchEnd; + FPreviousGlobal := FLog.ProcessGlobalLogs; + FLog.ProcessGlobalLogs := False; + FDispatch := TThreadSafeLogEvent.Create(True); + FDispatch.FLog := FLog; + // Drain ordinary startup records before taking the independent text baseline. + FDispatch.SynchronizedProcess; + FReferenceHost := TForm.CreateNew(nil); + FReference := TMemo.Create(FReferenceHost); + FReference.Parent := FReferenceHost; + FReference.WordWrap := FForm.memoLogs.WordWrap; + FReference.Font.Assign(FForm.memoLogs.Font); + FReference.BoundsRect := FForm.memoLogs.BoundsRect; + FReference.HandleNeeded; + FReference.Text := FForm.memoLogs.Text; + FLog.OnNewLog := Receive; + FLog.OnNewLogBatchBegin := BeginBatch; + FLog.OnNewLogBatchEnd := EndBatch; + FForm.memoLogs.HandleNeeded; + FMemoHandle := FForm.memoLogs.Handle; + {$IFDEF FPC} + ActiveChecks := Self; + SetLastError(0); + FPreviousWindowProc := Pointer(SetWindowLongPtrW(FMemoHandle,GWLP_WNDPROC,NativeInt(@MemoMessage))); + Require(Assigned(FPreviousWindowProc),'cannot subclass the actual native memo'); + {$ELSE} + FPreviousMemoProc := FForm.memoLogs.WindowProc; + FForm.memoLogs.WindowProc := MemoDispatch; + {$ENDIF} +end; + +procedure TLogMemoChecks.Uninstall; +begin + {$IFNDEF FPC} + if Assigned(FPreviousMemoProc) then begin + FForm.memoLogs.WindowProc := FPreviousMemoProc; + FPreviousMemoProc := nil; + end; + {$ENDIF} + if Assigned(FPreviousWindowProc) then begin + if IsWindow(FMemoHandle) then + SetWindowLongPtrW(FMemoHandle,GWLP_WNDPROC,NativeInt(FPreviousWindowProc)); + FPreviousWindowProc := nil; + ActiveChecks := nil; + end; + if Assigned(FLog) then begin + FLog.OnNewLog := FPreviousRecord; + FLog.OnNewLogBatchBegin := FPreviousBegin; + FLog.OnNewLogBatchEnd := FPreviousEnd; + FLog.ProcessGlobalLogs := FPreviousGlobal; + FLog := nil; + end; +end; + +procedure TLogMemoChecks.ReferenceAppend(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); +var LPrefix: String; +begin + if (AType=ltdebug) and not FForm.cbShowDebugLogs.Checked then + Exit; + if AThreadID=MainThreadID then + LPrefix := ' MAIN:' + else + LPrefix := ' TID:'; + // This bounded native reference preserves the old platform newline and + // trim semantics, including continuation/empty lines within one log record. + if FReference.Lines.Count>300 then + while FReference.Lines.Count>250 do + FReference.Lines.Delete(0); + FReference.Lines.Add(FormatDateTime('dd/mm/yyyy hh:nn:ss.zzz',ATime)+LPrefix+IntToHex(PtrInt(AThreadID),8)+' ['+CT_LogType[AType]+'] <'+ASender+'> '+AText); +end; + +procedure TLogMemoChecks.Receive(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); +var LBefore: Integer; +begin + LBefore := FReference.Lines.Count; + ReferenceAppend(AType,ATime,AThreadID,ASender,AText); + if (Pos(#13,AText)>0) or (Pos(#10,AText)>0) then + FLines.Add(Format('REFERENCE newline lines before=%d after=%d text=%s',[LBefore,FReference.Lines.Count,StringReplace(StringReplace(AText,#13,'\r',[rfReplaceAll]),#10,'\n',[rfReplaceAll])])); + FPreviousRecord(AType,ATime,AThreadID,ASender,AText); +end; + +procedure TLogMemoChecks.BeginBatch(Sender: TObject); +begin + Inc(FDeliveredBatches); + if Assigned(FPreviousBegin) then + FPreviousBegin(Sender); +end; + +procedure TLogMemoChecks.EndBatch(Sender: TObject); +begin + if Assigned(FPreviousEnd) then + FPreviousEnd(Sender); +end; + +procedure TLogMemoChecks.CheckBatch(const AName: String); +var LBefore: Integer; +begin + Require(FForm.memoLogs.Handle=FMemoHandle,'native memo handle changed unexpectedly'); + FSetText := 0; + FReplaceSelection := 0; + LBefore := FDeliveredBatches; + FDispatch.SynchronizedProcess; + Require(FDeliveredBatches=LBefore+1,AName+': expected one actual logger delivery'); + Require((FSetText=1) and (FReplaceSelection=0),Format('%s: expected one WM_SETTEXT and no native per-entry edits, got WM_SETTEXT=%d EM_REPLACESEL=%d',[AName,FSetText,FReplaceSelection])); + if FForm.memoLogs.Text<>FReference.Text then begin + FLines.Add('ACTUAL TAIL '+StringReplace(StringReplace(Copy(FForm.memoLogs.Text,Length(FForm.memoLogs.Text)-900,901),#13,'\r',[rfReplaceAll]),#10,'\n',[rfReplaceAll])); + FLines.Add('EXPECTED TAIL '+StringReplace(StringReplace(Copy(FReference.Text,Length(FReference.Text)-900,901),#13,'\r',[rfReplaceAll]),#10,'\n',[rfReplaceAll])); + end; + Require(FForm.memoLogs.Text=FReference.Text,AName+': native retained text/order differs from the original per-record rules'); + Require(FForm.memoLogs.Lines.Count=FReference.Lines.Count,AName+': native retained line count differs'); + FLines.Add(Format('PASS %s: one publication, retained lines=%d',[AName,FForm.memoLogs.Lines.Count])); +end; + +procedure TLogMemoChecks.CheckUnchangedBatch(const AName: String); +begin + FSetText := 0; + FReplaceSelection := 0; + FDispatch.SynchronizedProcess; + Require((FSetText=0) and (FReplaceSelection=0),AName+': unexpected native text publication'); + Require(FForm.memoLogs.Text=FReference.Text,AName+': retained text changed'); + FLines.Add('PASS '+AName+': no native text publication'); +end; + +procedure TLogMemoChecks.Run; +var I: Integer; LTime: TDateTime; +begin + Install; + FForm.cbShowDebugLogs.Checked := False; + for I := 1 to 350 do + FLog.NotifyNewLog(ltinfo,'publication-probe',Format('record %.3d',[I])); + CheckBatch('retention across 300/250 boundaries'); + for I := 1 to 8 do begin + FLog.NotifyNewLog(ltdebug,'publication-probe','filtered debug'); + FLog.NotifyNewLog(ltinfo,'publication-probe','visible info '+IntToStr(I)); + end; + CheckBatch('debug excluded'); + FLog.NotifyNewLog(ltdebug,'publication-probe','only filtered debug'); + CheckUnchangedBatch('all-filtered batch'); + CheckUnchangedBatch('empty batch'); + FForm.cbShowDebugLogs.Checked := True; + FLog.NotifyNewLog(ltdebug,'publication-probe','included debug'); + FLog.NotifyNewLog(ltinfo,'publication-probe','line one'#13#10'line two'); + FLog.NotifyNewLog(ltinfo,'publication-probe','line three'#10'line four'); + FLog.NotifyNewLog(ltinfo,'publication-probe','trailing continuation'#13#10); + FLog.NotifyNewLog(ltinfo,'publication-probe','bare carriage'#13'return'); + FLog.NotifyNewLog(ltinfo,'publication-probe','double carriage'#13#13#10'line'); + FLog.NotifyNewLog(ltinfo,'publication-probe','blank line'#13#10#13#10'end'); + CheckBatch('debug included and multiline'); + // Call the real public handler outside the logger's brackets. Publication + // must remain immediate and the following batch must preserve that text. + LTime := EncodeDate(2026,11,3)+EncodeTime(12,34,56,789); + ReferenceAppend(ltinfo,LTime,MainThreadID,'publication-probe','direct outside batch'); + TClassicLogAccess(FForm).OnNewLog(ltinfo,LTime,MainThreadID,'publication-probe','direct outside batch'); + Require(FForm.memoLogs.Text=FReference.Text,'direct OnNewLog did not publish the original text immediately'); + FLines.Add('PASS direct OnNewLog outside batch'); + FLog.NotifyNewLog(ltinfo,'publication-probe','after direct callback'); + CheckBatch('batch after direct callback'); + FForm.memoLogs.Text := 'manual tail'; + FReference.Text := 'manual tail'; + FLog.NotifyNewLog(ltinfo,'publication-probe','after unterminated text'); + FLog.NotifyNewLog(ltinfo,'publication-probe','continued'#13#10'line'); + FLog.NotifyNewLog(ltinfo,'publication-probe','ends in separator'#13#10); + FLog.NotifyNewLog(ltinfo,'publication-probe','last record'); + CheckBatch('unterminated manual tail and subsequent multiline'); + FLines.Add('PASS 8'); +end; + +procedure SeedSettings(const ADirectory: String); +var LBag: TDisposables; LParams: TAppParams; +begin + LParams := LBag.AddObject(TAppParams.Create(nil)) as TAppParams; + LParams.FileName := IncludeTrailingPathDelimiter(ADirectory)+'AppParams.prm'; + LParams.ParamByName[CT_PARAM_FirstTime].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_InternetServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_JSONRPCEnabled].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerActive].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_TryToConnectOnlyWithThisFixedServers].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_PeerCache].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_ShowModalMessages].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_ShowLogs].SetAsBoolean(True); + LParams.ParamByName[CT_PARAM_SaveLogFiles].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_SaveDebugLogs].SetAsBoolean(False); +end; + +function RunClassicLogMemoBatchChecks(const ALogPath: String): Integer; +var LChecks: TLogMemoChecks; LDirectory, LOldDirectory: String; +begin + Result := 1; + Require(not FileExists(ALogPath),'refusing an existing probe log'); + LDirectory := ExpandFileName(ALogPath+'.owned-profile'); + Require(not DirectoryExists(LDirectory),'refusing an existing data profile'); + Require(ForceDirectories(LDirectory),'cannot create owned empty profile'); + Require(not TNode.NodeExists and not TNetData.NetDataExists,'Classic probe requires fresh core singletons'); + LOldDirectory := TNode.GetPascalCoinDataFolder; + LChecks := TLogMemoChecks.Create; + try + try + // FormCreate loads settings itself. Redirect its owner before any form, + // node or network object can inspect a wallet or persistence directory. + TNode.SetPascalCoinDataFolder(LDirectory); + SeedSettings(LDirectory); + TCrypto.InitCrypto; + TNetData.NetData.NetConnectionsActive := False; + Application.Initialize; + Application.MainFormOnTaskBar := True; + Application.CreateForm(TFRMWallet,FRMWallet); + // No Show/activation: the real logger and memo are sufficient here; + // starting the Classic chain loader is outside this focused regression. + Require(not TClassicLogAccess(FRMWallet).FIsActivated and not Assigned(TClassicLogAccess(FRMWallet).FThreadActivate),'Classic activation unexpectedly started'); + Require(not TNetData.NetData.NetConnectionsActive,'Classic networking was unexpectedly enabled'); + LChecks.Run; + Result := 0; + except + on E: Exception do + LChecks.FLines.Add('FAIL '+E.ClassName+': '+E.Message); + end; + finally + try + LChecks.Uninstall; + FreeAndNil(FRMWallet); + Require(not TNode.NodeExists and not TNetData.NetDataExists,'Classic form did not release its owned core singletons'); + except + on E: Exception do begin + Result := 1; + LChecks.FLines.Add('FAIL cleanup '+E.ClassName+': '+E.Message); + end; + end; + TNode.SetPascalCoinDataFolder(LOldDirectory); + LChecks.FLines.SaveToFile(ALogPath); + LChecks.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/log-memo-batch/ULogMemoBatchChecks.pas b/src/tests/experimental/native/log-memo-batch/ULogMemoBatchChecks.pas new file mode 100644 index 000000000..30f33f6b0 --- /dev/null +++ b/src/tests/experimental/native/log-memo-batch/ULogMemoBatchChecks.pas @@ -0,0 +1,341 @@ +unit ULogMemoBatchChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Verifies real log-form publication and retained text through native memo + messages. The reference memo applies the original per-record retention rules. } + +{$IFDEF FPC}{$MODE Delphi}{$H+}{$ENDIF} + +interface + +function RunLogMemoBatchChecks(const ALogPath: String): Integer; + +implementation + +uses + {$IFDEF FPC}Windows,{$ELSE}Winapi.Windows, Winapi.Messages,{$ENDIF} + SysUtils, Classes, Forms, Controls, StdCtrls, UAppParams, UMemory, UCommon, + UWalletStartup, UUserInterface, UFRMMainForm, UFRMLogs, ULog, UNode, + UConst; + +type + TLogMemoChecks = class + private + FForm: TFRMLogs; + FReferenceHost: TForm; + FReference: TMemo; + FLog: TLog; + FDispatch: TThreadSafeLogEvent; + FPreviousRecord: TNewLogEvent; + FPreviousBegin, FPreviousEnd: TNotifyEvent; + FPreviousGlobal: Boolean; + FPreviousWindowProc: Pointer; + {$IFNDEF FPC} + FPreviousMemoProc: TWndMethod; + {$ENDIF} + FMemoHandle: HWND; + FSetText, FReplaceSelection, FDeliveredBatches: Integer; + FLoaded: Boolean; + FLines: TStringList; + {$IFNDEF FPC} + procedure MemoDispatch(var AMessage: TMessage); + {$ENDIF} + procedure Loaded(Sender: TObject); + procedure Receive(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); + procedure BeginBatch(Sender: TObject); + procedure EndBatch(Sender: TObject); + procedure ReferenceAppend(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); + procedure CheckBatch(const AName: String); + procedure CheckUnchangedBatch(const AName: String); + procedure Install; + procedure Uninstall; + public + constructor Create; + destructor Destroy; override; + procedure Run; + end; + +var ActiveChecks: TLogMemoChecks; + +procedure Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then + raise Exception.Create(AMessage); +end; + +function MemoMessage(AWindow: HWND; AMessage: UINT; AWParam: WPARAM; ALParam: LPARAM): LRESULT; stdcall; +begin + if AMessage=WM_SETTEXT then + Inc(ActiveChecks.FSetText) + else if AMessage=EM_REPLACESEL then + Inc(ActiveChecks.FReplaceSelection); + Result := CallWindowProcW(ActiveChecks.FPreviousWindowProc,AWindow,AMessage,AWParam,ALParam); +end; + +{$IFNDEF FPC} +procedure TLogMemoChecks.MemoDispatch(var AMessage: TMessage); +begin + // VCL Perform calls the saved native edit procedure directly; a window + // subclass misses those messages. Observe its real control dispatch instead. + if AMessage.Msg=WM_SETTEXT then + Inc(FSetText) + else if AMessage.Msg=EM_REPLACESEL then + Inc(FReplaceSelection); + FPreviousMemoProc(AMessage); +end; +{$ENDIF} + +constructor TLogMemoChecks.Create; +begin + inherited Create; + FLines := TStringList.Create; +end; + +destructor TLogMemoChecks.Destroy; +begin + Uninstall; + FDispatch.Free; + FReferenceHost.Free; + FLines.Free; + inherited Destroy; +end; + +procedure TLogMemoChecks.Loaded(Sender: TObject); +begin + FLoaded := True; +end; + +procedure TLogMemoChecks.Install; +var I: Integer; +begin + TUserInterface.ShowLogsForm; + for I := 0 to Screen.FormCount-1 do + if Screen.Forms[I] is TFRMLogs then + FForm := TFRMLogs(Screen.Forms[I]); + Require(Assigned(FForm),'actual ShowLogs form was not created'); + FLog := TUserInterface.Log; + Require(Assigned(FLog) and Assigned(FLog.OnNewLog),'actual log-form subscription is missing'); + FPreviousRecord := FLog.OnNewLog; + FPreviousBegin := FLog.OnNewLogBatchBegin; + FPreviousEnd := FLog.OnNewLogBatchEnd; + FPreviousGlobal := FLog.ProcessGlobalLogs; + FLog.ProcessGlobalLogs := False; + FDispatch := TThreadSafeLogEvent.Create(True); + FDispatch.FLog := FLog; + // Drain ordinary startup records before taking the independent text baseline. + FDispatch.SynchronizedProcess; + FReferenceHost := TForm.CreateNew(nil); + FReference := TMemo.Create(FReferenceHost); + FReference.Parent := FReferenceHost; + FReference.WordWrap := FForm.memoLogs.WordWrap; + FReference.Font.Assign(FForm.memoLogs.Font); + FReference.BoundsRect := FForm.memoLogs.BoundsRect; + FReference.HandleNeeded; + FReference.Text := FForm.memoLogs.Text; + FLog.OnNewLog := Receive; + FLog.OnNewLogBatchBegin := BeginBatch; + FLog.OnNewLogBatchEnd := EndBatch; + FForm.memoLogs.HandleNeeded; + FMemoHandle := FForm.memoLogs.Handle; + {$IFDEF FPC} + ActiveChecks := Self; + SetLastError(0); + FPreviousWindowProc := Pointer(SetWindowLongPtrW(FMemoHandle,GWLP_WNDPROC,NativeInt(@MemoMessage))); + Require(Assigned(FPreviousWindowProc),'cannot subclass the actual native memo'); + {$ELSE} + FPreviousMemoProc := FForm.memoLogs.WindowProc; + FForm.memoLogs.WindowProc := MemoDispatch; + {$ENDIF} +end; + +procedure TLogMemoChecks.Uninstall; +begin + {$IFNDEF FPC} + if Assigned(FPreviousMemoProc) then begin + FForm.memoLogs.WindowProc := FPreviousMemoProc; + FPreviousMemoProc := nil; + end; + {$ENDIF} + if Assigned(FPreviousWindowProc) then begin + if IsWindow(FMemoHandle) then + SetWindowLongPtrW(FMemoHandle,GWLP_WNDPROC,NativeInt(FPreviousWindowProc)); + FPreviousWindowProc := nil; + ActiveChecks := nil; + end; + if Assigned(FLog) then begin + FLog.OnNewLog := FPreviousRecord; + FLog.OnNewLogBatchBegin := FPreviousBegin; + FLog.OnNewLogBatchEnd := FPreviousEnd; + FLog.ProcessGlobalLogs := FPreviousGlobal; + FLog := nil; + end; +end; + +procedure TLogMemoChecks.ReferenceAppend(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); +var LPrefix: String; +begin + if (AType=ltdebug) and not FForm.cbShowDebugLogs.Checked then + Exit; + if AThreadID=MainThreadID then + LPrefix := 'MAIN:' + else + LPrefix := 'TID:'; + // This bounded native reference preserves the old platform newline and + // trim semantics, including continuation/empty lines within one log record. + if FReference.Lines.Count>300 then + while FReference.Lines.Count>250 do + FReference.Lines.Delete(0); + FReference.Lines.Add(Format('%s %s %s [%s] <%s> %s',[FormatDateTime('dd/mm/yyyy hh:nn:ss.zzz',ATime),LPrefix,IntToHex(PtrInt(AThreadID),8),CT_LogType[AType],ASender,AText])); +end; + +procedure TLogMemoChecks.Receive(AType: TLogType; ATime: TDateTime; AThreadID: TThreadID; const ASender, AText: String); +var LBefore: Integer; +begin + LBefore := FReference.Lines.Count; + ReferenceAppend(AType,ATime,AThreadID,ASender,AText); + if (Pos(#13,AText)>0) or (Pos(#10,AText)>0) then + FLines.Add(Format('REFERENCE newline lines before=%d after=%d text=%s',[LBefore,FReference.Lines.Count,StringReplace(StringReplace(AText,#13,'\r',[rfReplaceAll]),#10,'\n',[rfReplaceAll])])); + FPreviousRecord(AType,ATime,AThreadID,ASender,AText); +end; + +procedure TLogMemoChecks.BeginBatch(Sender: TObject); +begin + Inc(FDeliveredBatches); + if Assigned(FPreviousBegin) then + FPreviousBegin(Sender); +end; + +procedure TLogMemoChecks.EndBatch(Sender: TObject); +begin + if Assigned(FPreviousEnd) then + FPreviousEnd(Sender); +end; + +procedure TLogMemoChecks.CheckBatch(const AName: String); +var LBefore: Integer; +begin + Require(FForm.memoLogs.Handle=FMemoHandle,'native memo handle changed unexpectedly'); + FSetText := 0; + FReplaceSelection := 0; + LBefore := FDeliveredBatches; + FDispatch.SynchronizedProcess; + Require(FDeliveredBatches=LBefore+1,AName+': expected one actual logger delivery'); + Require((FSetText=1) and (FReplaceSelection=0),Format('%s: expected one WM_SETTEXT and no native per-entry edits, got WM_SETTEXT=%d EM_REPLACESEL=%d',[AName,FSetText,FReplaceSelection])); + if FForm.memoLogs.Text<>FReference.Text then begin + FLines.Add('ACTUAL TAIL '+StringReplace(StringReplace(Copy(FForm.memoLogs.Text,Length(FForm.memoLogs.Text)-900,901),#13,'\r',[rfReplaceAll]),#10,'\n',[rfReplaceAll])); + FLines.Add('EXPECTED TAIL '+StringReplace(StringReplace(Copy(FReference.Text,Length(FReference.Text)-900,901),#13,'\r',[rfReplaceAll]),#10,'\n',[rfReplaceAll])); + end; + Require(FForm.memoLogs.Text=FReference.Text,AName+': native retained text/order differs from the original per-record rules'); + Require(FForm.memoLogs.Lines.Count=FReference.Lines.Count,AName+': native retained line count differs'); + FLines.Add(Format('PASS %s: one publication, retained lines=%d',[AName,FForm.memoLogs.Lines.Count])); +end; + +procedure TLogMemoChecks.CheckUnchangedBatch(const AName: String); +begin + FSetText := 0; + FReplaceSelection := 0; + FDispatch.SynchronizedProcess; + Require((FSetText=0) and (FReplaceSelection=0),AName+': unexpected native text publication'); + Require(FForm.memoLogs.Text=FReference.Text,AName+': retained text changed'); + FLines.Add('PASS '+AName+': no native text publication'); +end; + +procedure TLogMemoChecks.Run; +var I: Integer; LTime: TDateTime; +begin + Install; + FForm.cbShowDebugLogs.Checked := False; + for I := 1 to 350 do + FLog.NotifyNewLog(ltinfo,'publication-probe',Format('record %.3d',[I])); + CheckBatch('retention across 300/250 boundaries'); + for I := 1 to 8 do begin + FLog.NotifyNewLog(ltdebug,'publication-probe','filtered debug'); + FLog.NotifyNewLog(ltinfo,'publication-probe','visible info '+IntToStr(I)); + end; + CheckBatch('debug excluded'); + FLog.NotifyNewLog(ltdebug,'publication-probe','only filtered debug'); + CheckUnchangedBatch('all-filtered batch'); + CheckUnchangedBatch('empty batch'); + FForm.cbShowDebugLogs.Checked := True; + FLog.NotifyNewLog(ltdebug,'publication-probe','included debug'); + FLog.NotifyNewLog(ltinfo,'publication-probe','line one'#13#10'line two'); + FLog.NotifyNewLog(ltinfo,'publication-probe','line three'#10'line four'); + FLog.NotifyNewLog(ltinfo,'publication-probe','trailing continuation'#13#10); + FLog.NotifyNewLog(ltinfo,'publication-probe','bare carriage'#13'return'); + FLog.NotifyNewLog(ltinfo,'publication-probe','double carriage'#13#13#10'line'); + FLog.NotifyNewLog(ltinfo,'publication-probe','blank line'#13#10#13#10'end'); + CheckBatch('debug included and multiline'); + // Call the real public handler outside the logger's brackets. Publication + // must remain immediate and the following batch must preserve that text. + LTime := EncodeDate(2026,11,3)+EncodeTime(12,34,56,789); + ReferenceAppend(ltinfo,LTime,MainThreadID,'publication-probe','direct outside batch'); + FForm.OnNewLog(ltinfo,LTime,MainThreadID,'publication-probe','direct outside batch'); + Require(FForm.memoLogs.Text=FReference.Text,'direct OnNewLog did not publish the original text immediately'); + FLines.Add('PASS direct OnNewLog outside batch'); + FLog.NotifyNewLog(ltinfo,'publication-probe','after direct callback'); + CheckBatch('batch after direct callback'); + FForm.memoLogs.Text := 'manual tail'; + FReference.Text := 'manual tail'; + FLog.NotifyNewLog(ltinfo,'publication-probe','after unterminated text'); + FLog.NotifyNewLog(ltinfo,'publication-probe','continued'#13#10'line'); + FLog.NotifyNewLog(ltinfo,'publication-probe','ends in separator'#13#10); + FLog.NotifyNewLog(ltinfo,'publication-probe','last record'); + CheckBatch('unterminated manual tail and subsequent multiline'); + FLines.Add('PASS 8'); +end; + +procedure SeedSettings(const ADirectory: String); +var LBag: TDisposables; LParams: TAppParams; +begin + LParams := LBag.AddObject(TAppParams.Create(nil)) as TAppParams; + LParams.FileName := IncludeTrailingPathDelimiter(ADirectory)+'AppParams.prm'; + LParams.ParamByName['ShowLogs'].SetAsBoolean(True); + LParams.ParamByName['SaveLogFiles'].SetAsBoolean(False); + LParams.ParamByName['SaveDebugLogs'].SetAsBoolean(False); +end; + +function RunLogMemoBatchChecks(const ALogPath: String): Integer; +var LChecks: TLogMemoChecks; LMain: TFRMMainForm; LOptions: TWalletStartupOptions; LArguments: array of String; LDirectory, LError: String; LStarted: UInt64; +begin + Result := 1; + Require(not FileExists(ALogPath),'refusing an existing probe log'); + LDirectory := ExpandFileName(ALogPath+'.owned-profile'); + Require(not DirectoryExists(LDirectory),'refusing an existing data profile'); + Require(ForceDirectories(LDirectory),'cannot create owned empty profile'); + SeedSettings(LDirectory); + SetLength(LArguments,3); + LArguments[0] := '--data-dir='+LDirectory; + LArguments[1] := '--offline'; + LArguments[2] := '--no-startup-dialogs'; + Require(TryParseWalletStartupOptions(LArguments,LOptions,LError),LError); + TUserInterface.ConfigureStartup(LOptions); + Application.Initialize; + Application.MainFormOnTaskBar := True; + Application.CreateForm(TFRMMainForm,LMain); + LChecks := TLogMemoChecks.Create; + TUserInterface.Loaded.Add(LChecks.Loaded); + try + try + TUserInterface.StartApplication(LMain); + LStarted := GetTickCount64; + while not LChecks.FLoaded and (GetTickCount64-LStarted<10000) do begin + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + end; + Require(LChecks.FLoaded,'owned empty offline startup did not finish'); + LChecks.Run; + Result := 0; + except + on E: Exception do + LChecks.FLines.Add('FAIL '+E.ClassName+': '+E.Message); + end; + LChecks.FLines.SaveToFile(ALogPath); + finally + TUserInterface.Loaded.Remove(LChecks.Loaded); + LChecks.Free; + TUserInterface.ExitApplication; + end; +end; + +end. diff --git a/src/tests/experimental/native/mempool-lock/MempoolLockProbe.dpr b/src/tests/experimental/native/mempool-lock/MempoolLockProbe.dpr new file mode 100644 index 000000000..1d3f5af01 --- /dev/null +++ b/src/tests/experimental/native/mempool-lock/MempoolLockProbe.dpr @@ -0,0 +1,13 @@ +program MempoolLockProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, UMempoolLockChecks; +begin + if ParamCount <> 1 then Halt(64); + {$IFDEF FPC}SetHeapTraceOutput(ParamStr(1)+'.heap.log');{$ENDIF} + try ExitCode := RunMempoolLockChecks(ExpandFileName(ParamStr(1))); + except on E: Exception do begin + WriteLn(E.ClassName, ': ', E.Message); + ExitCode := 1; + end; end; +end. diff --git a/src/tests/experimental/native/mempool-lock/MempoolLockProbe.dproj b/src/tests/experimental/native/mempool-lock/MempoolLockProbe.dproj new file mode 100644 index 000000000..20f9cb0ac --- /dev/null +++ b/src/tests/experimental/native/mempool-lock/MempoolLockProbe.dproj @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{BDDD6408-60DC-4D42-B011-C8AEC120524D}</ProjectGuid> + <MainSource>MempoolLockProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\mempool-lock\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\mempool-lock\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UMempoolLockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">MempoolLockProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/mempool-lock/MempoolLockProbe.lpr b/src/tests/experimental/native/mempool-lock/MempoolLockProbe.lpr new file mode 100644 index 000000000..d5bbd2976 --- /dev/null +++ b/src/tests/experimental/native/mempool-lock/MempoolLockProbe.lpr @@ -0,0 +1,10 @@ +program MempoolLockProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, UMempoolLockChecks; +begin + if ParamCount <> 1 then Halt(64); + {$IFDEF FPC}SetHeapTraceOutput(ParamStr(1)+'.heap.log');{$ENDIF} + try ExitCode := RunMempoolLockChecks(ExpandFileName(ParamStr(1))); + except on E: Exception do begin WriteLn(E.ClassName, ': ', E.Message); ExitCode := 1; end; end; +end. diff --git a/src/tests/experimental/native/mempool-lock/UMempoolLockChecks.pas b/src/tests/experimental/native/mempool-lock/UMempoolLockChecks.pas new file mode 100644 index 000000000..b30583b2f --- /dev/null +++ b/src/tests/experimental/native/mempool-lock/UMempoolLockChecks.pas @@ -0,0 +1,553 @@ +unit UMempoolLockChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Deterministic integration checks for the actual node mempool lock API. + Distributed under the MIT software license; see the accompanying LICENSE. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +// Run in an owned isolated process. Actor gates verify actual Node ownership, +// including balanced upgradeable-read and nested-write release on failure. +function RunMempoolLockChecks(const ALogPath: String): Integer; + +implementation + +uses {$IFNDEF FPC}Winapi.Windows,{$ELSE}Windows,{$ENDIF} + Classes, SysUtils, SyncObjs, Generics.Collections, UNode, UThread, UBlockChain, + UCrypto, UNetProtocol, UOperationReadHashChecks, UMinerTemplateUpgradeChecks; + +type + TLockAction = (laRead, laWrite, laNestedWrite, laCancelableRead, + laUpgradeable, laPromotingUpgradeable); + + EExpectedUpgradeFailure = class(Exception); + + TLockCounters = class + public + Readers, Writers, Violations: LongInt; + end; + + TLockActor = class(TPCThread) + private + FNode: TNode; + FAction: TLockAction; + FCounters: TLockCounters; + FStarted: Boolean; + procedure Hold; + procedure CountRead(AEnter: Boolean); + procedure CountWrite(AEnter: Boolean); + protected + procedure BCExecute; override; + public + Entered, ReleaseLock, Completed, Attempted: TEvent; + Promote, Writing, LeaveWrite, AfterWrite: TEvent; + Mempool: TPCOperationsComp; + ErrorMessage: String; + constructor Create(ANode: TNode; AAction: TLockAction; ACounters: TLockCounters); + destructor Destroy; override; + procedure Start; reintroduce; + end; + + TLockChecks = class + private + FNode: TNode; + FLogPath: String; + FCounters: TLockCounters; + FActors: TList<TLockActor>; + function Actor(AAction: TLockAction): TLockActor; + procedure StopActors; + procedure Require(AValue: Boolean; const AMessage: String); + procedure Log(const AText: String); + procedure Entered(AActor: TLockActor; const AMessage: String); + procedure CheckReaders; + procedure CheckExclusion; + procedure CheckNestedWriter; + procedure CheckTryAcquisition; + procedure CheckCancellation; + procedure CheckUpgradeableSharing; + procedure CheckUpgradeablePromotion; + procedure CheckUpgradeableException; + procedure CheckUpgradeableCancellation; + public + constructor Create(ANode: TNode; const ALogPath: String); + destructor Destroy; override; + procedure Run; + end; + +constructor TLockActor.Create(ANode: TNode; AAction: TLockAction; ACounters: TLockCounters); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FAction := AAction; + FCounters := ACounters; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseLock := TEvent.Create(nil, True, False, ''); + Completed := TEvent.Create(nil, True, False, ''); + Attempted := TEvent.Create(nil, True, False, ''); + Promote := TEvent.Create(nil, True, False, ''); + Writing := TEvent.Create(nil, True, False, ''); + LeaveWrite := TEvent.Create(nil, True, False, ''); + AfterWrite := TEvent.Create(nil, True, False, ''); +end; + +procedure TLockActor.Start; +begin + if FStarted then raise EThread.Create('Lock actor already started'); + FStarted := True; + inherited Start; +end; + +destructor TLockActor.Destroy; +begin + Terminate; + ReleaseLock.SetEvent; + Promote.SetEvent; + LeaveWrite.SetEvent; + if not FStarted then Start; + WaitFor; + AfterWrite.Free; + LeaveWrite.Free; + Writing.Free; + Promote.Free; + Attempted.Free; + Completed.Free; + ReleaseLock.Free; + Entered.Free; + inherited; +end; + +procedure TLockActor.CountRead(AEnter: Boolean); +begin + if AEnter then begin + InterlockedIncrement(FCounters.Readers); + if InterlockedCompareExchange(FCounters.Writers, 0, 0) <> 0 then + InterlockedIncrement(FCounters.Violations); + end else InterlockedDecrement(FCounters.Readers); +end; + +procedure TLockActor.CountWrite(AEnter: Boolean); +begin + if AEnter then begin + if (InterlockedIncrement(FCounters.Writers) <> 1) or + (InterlockedCompareExchange(FCounters.Readers, 0, 0) <> 0) then + InterlockedIncrement(FCounters.Violations); + end else InterlockedDecrement(FCounters.Writers); +end; + +procedure TLockActor.Hold; +begin + Entered.SetEvent; + while not Terminated do + if ReleaseLock.WaitFor(10) = wrSignaled then Break; +end; + +procedure TLockActor.BCExecute; +var LNested: TPCOperationsComp; LCountedRead: Boolean; +begin + try + try + if Terminated then Exit; + case FAction of + laRead: begin + DebugStep := 'acquire reader'; + Mempool := FNode.LockMempoolRead; + CountRead(True); + try Hold; + finally CountRead(False); FNode.UnlockMempoolRead; end; + end; + laWrite: begin + DebugStep := 'acquire writer'; + Mempool := FNode.LockMempoolWrite; + CountWrite(True); + try Hold; + finally CountWrite(False); FNode.UnlockMempoolWrite; end; + end; + laNestedWrite: begin + DebugStep := 'outer writer'; + Mempool := FNode.LockMempoolWrite; + CountWrite(True); + try + DebugStep := 'recursive writer'; + LNested := FNode.LockMempoolWrite; + try + if LNested <> Mempool then raise Exception.Create('Nested writer changed mempool'); + DebugStep := 'writer-owned reader'; + LNested := FNode.LockMempoolRead; + try + if LNested <> Mempool then raise Exception.Create('Writer-owned reader changed mempool'); + DebugStep := 'writer-owned try writer'; + if not FNode.TryLockNode(0) then raise Exception.Create('Nested exclusive try failed'); + FNode.UnlockNode; + DebugStep := 'writer-owned try reader'; + if not FNode.TryLockMempoolRead(0, LNested) then + raise Exception.Create('Writer-owned shared try failed'); + FNode.UnlockMempoolRead; + finally FNode.UnlockMempoolRead; end; + finally FNode.UnlockMempoolWrite; end; + DebugStep := 'nested sequence completed'; + Hold; + finally CountWrite(False); FNode.UnlockMempoolWrite; end; + end; + laCancelableRead: begin + DebugStep := 'cooperative timed acquisition'; + Entered.SetEvent; + while not Terminated do begin + if FNode.TryLockMempoolRead(20, LNested) then begin + FNode.UnlockMempoolRead; + Break; + end; + Attempted.SetEvent; + end; + end; + laUpgradeable: begin + DebugStep := 'acquire upgradeable reader'; + Mempool := FNode.LockMempoolUpgradeableRead; + CountRead(True); + try Hold; + finally CountRead(False); FNode.UnlockMempoolUpgradeableRead; end; + end; + laPromotingUpgradeable: begin + DebugStep := 'acquire promotable reader'; + Mempool := FNode.LockMempoolUpgradeableRead; + CountRead(True); + LCountedRead := True; + try + Entered.SetEvent; + while not Terminated do + if Promote.WaitFor(10) = wrSignaled then Break; + if Terminated then Exit; + // Ownership is still upgradeable while waiting, but its own read + // must not be counted as a foreign reader during exclusive work. + CountRead(False); + LCountedRead := False; + DebugStep := 'promote to nested writer'; + LNested := FNode.LockMempoolWrite; + CountWrite(True); + try + if LNested <> Mempool then raise Exception.Create('Promotion changed mempool'); + Writing.SetEvent; + while not Terminated do + if LeaveWrite.WaitFor(10) = wrSignaled then Break; + finally CountWrite(False); FNode.UnlockMempoolWrite; end; + CountRead(True); + LCountedRead := True; + DebugStep := 'outer upgradeable retained after writer release'; + AfterWrite.SetEvent; + Hold; + finally + if LCountedRead then CountRead(False); + FNode.UnlockMempoolUpgradeableRead; + end; + end; + end; + except on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; end; + finally Completed.SetEvent; end; +end; + +constructor TLockChecks.Create(ANode: TNode; const ALogPath: String); +begin + inherited Create; + FNode := ANode; + FLogPath := ALogPath; + FCounters := TLockCounters.Create; + FActors := TList<TLockActor>.Create; +end; + +destructor TLockChecks.Destroy; +begin + StopActors; + FActors.Free; + FCounters.Free; + inherited; +end; + +function TLockChecks.Actor(AAction: TLockAction): TLockActor; +begin + Result := TLockActor.Create(FNode, AAction, FCounters); + FActors.Add(Result); + Result.Start; +end; + +procedure TLockChecks.StopActors; +var A: TLockActor; LError: String; +begin + // Release ALL holders before joining ANY waiter, including failed checks. + for A in FActors do begin + A.Terminate; + A.ReleaseLock.SetEvent; + A.Promote.SetEvent; + A.LeaveWrite.SetEvent; + end; + LError := ''; + for A in FActors do begin + A.WaitFor; + if (LError = '') and (A.ErrorMessage <> '') then LError := A.ErrorMessage; + end; + for A in FActors do A.Free; + FActors.Clear; + if LError <> '' then raise Exception.Create('Actual lock actor failed: ' + LError); +end; + +procedure TLockChecks.Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +procedure TLockChecks.Log(const AText: String); +var F: TextFile; +begin + AssignFile(F, FLogPath); + if FileExists(FLogPath) then Append(F) else Rewrite(F); + try WriteLn(F, GetTickCount64, ' ', AText); finally CloseFile(F); end; +end; + +procedure TLockChecks.Entered(AActor: TLockActor; const AMessage: String); +begin + Require(AActor.Entered.WaitFor(1500) = wrSignaled, + AMessage + '; step=' + AActor.DebugStep + '; error=' + AActor.ErrorMessage); +end; + +procedure TLockChecks.CheckReaders; +var A, B: TLockActor; +begin + A := Actor(laRead); Entered(A, 'First reader did not enter'); + B := Actor(laRead); + Require(B.Entered.WaitFor(350) = wrSignaled, + 'Second actual Node reader was serialized behind the first reader'); + Require(A.ReleaseLock.WaitFor(0) <> wrSignaled, 'First reader gate was released early'); + Require(A.Mempool = B.Mempool, 'Readers received different mempools'); +end; + +procedure TLockChecks.CheckExclusion; +var A, B, W, R, W2: TLockActor; +begin + A := Actor(laRead); Entered(A, 'First exclusion reader did not enter'); + B := Actor(laRead); Entered(B, 'Second exclusion reader did not enter'); + W := Actor(laWrite); + Require(W.Entered.WaitFor(100) = wrTimeout, 'Writer overlapped two readers'); + A.ReleaseLock.SetEvent; + Require(A.Completed.WaitFor(1000) = wrSignaled, 'First reader did not leave'); + Require(W.Entered.WaitFor(100) = wrTimeout, 'Writer entered before the last reader left'); + B.ReleaseLock.SetEvent; Entered(W, 'Writer did not progress after all readers left'); + R := Actor(laRead); W2 := Actor(laWrite); + Require(R.Entered.WaitFor(100) = wrTimeout, 'Reader overlapped exclusive writer'); + Require(W2.Entered.WaitFor(100) = wrTimeout, 'Two writers overlapped'); + // Do not impose a reader/writer preference policy on the remaining race. + R.ReleaseLock.SetEvent; W2.ReleaseLock.SetEvent; W.ReleaseLock.SetEvent; + Require(R.Completed.WaitFor(1500) = wrSignaled, 'Reader did not progress after writer release'); + Require(W2.Completed.WaitFor(1500) = wrSignaled, 'Next writer did not progress after writer release'); +end; + +procedure TLockChecks.CheckNestedWriter; +var W, R: TLockActor; +begin + W := Actor(laNestedWrite); Entered(W, 'Existing recursive writer/read/write path deadlocked'); + R := Actor(laRead); + Require(R.Entered.WaitFor(100) = wrTimeout, 'Nested reader prematurely released outer writer exclusion'); + W.ReleaseLock.SetEvent; Entered(R, 'Outer writer remained held after balanced nested releases'); +end; + +procedure TLockChecks.CheckTryAcquisition; +var W, R: TLockActor; L: TPCOperationsComp; T: UInt64; Acquired: Boolean; +begin + W := Actor(laWrite); Entered(W, 'Try-lock writer did not enter'); + T := GetTickCount64; Acquired := FNode.TryLockNode(0); + if Acquired then FNode.UnlockNode; + Require(not Acquired, 'Exclusive zero-wait try overlapped a writer'); + Require(GetTickCount64-T < 150, 'Exclusive zero-wait try blocked'); + T := GetTickCount64; Acquired := FNode.TryLockNode(60); + if Acquired then FNode.UnlockNode; + Require(not Acquired, 'Timed exclusive try overlapped a writer'); + Require((GetTickCount64-T >= 45) and (GetTickCount64-T < 1000), 'Exclusive timeout was not finite'); + L := TPCOperationsComp(Pointer(1)); T := GetTickCount64; Acquired := FNode.TryLockMempoolRead(0, L); + if Acquired then FNode.UnlockMempoolRead; + Require(not Acquired and not Assigned(L), 'Failed shared try must return nil without acquiring'); + Require(GetTickCount64-T < 150, 'Shared zero-wait try blocked'); + T := GetTickCount64; Acquired := FNode.TryLockMempoolRead(60, L); + if Acquired then FNode.UnlockMempoolRead; + Require(not Acquired and not Assigned(L), 'Timed shared try overlapped a writer'); + Require((GetTickCount64-T >= 45) and (GetTickCount64-T < 1000), 'Shared timeout was not finite'); + W.ReleaseLock.SetEvent; Require(W.Completed.WaitFor(1000) = wrSignaled, 'Try-lock writer did not leave'); + R := Actor(laRead); Entered(R, 'Shared try fixture reader did not enter'); + Require(FNode.TryLockMempoolRead(0, L), 'Shared nonblocking try was serialized behind another reader'); + try Require(L = R.Mempool, 'Shared try returned another mempool'); + finally FNode.UnlockMempoolRead; end; + Acquired := FNode.TryLockNode(0); + if Acquired then FNode.UnlockNode; + Require(not Acquired, 'Legacy TryLockNode lost exclusive semantics'); +end; + +procedure TLockChecks.CheckCancellation; +var W, C: TLockActor; T: UInt64; +begin + W := Actor(laWrite); Entered(W, 'Cancellation writer did not enter'); + C := Actor(laCancelableRead); Entered(C, 'Cancelable acquisition did not start'); + Require(C.Attempted.WaitFor(500) = wrSignaled, 'Cancelable actor never completed a bounded failed acquisition'); + T := GetTickCount64; C.Terminate; + Require(C.Completed.WaitFor(500) = wrSignaled, 'Canceled reader waited for owner release'); + Require(GetTickCount64-T < 500, 'Cancellation exceeded its bounded try interval'); + Require(W.ReleaseLock.WaitFor(0) <> wrSignaled, 'Cancellation fixture released its writer early'); + W.ReleaseLock.SetEvent; +end; + +procedure TLockChecks.CheckUpgradeableSharing; +var U, R, U2: TLockActor; +begin + U := Actor(laUpgradeable); Entered(U, 'First upgradeable reader did not enter'); + R := Actor(laRead); Entered(R, 'Ordinary reader was serialized behind upgradeable reader'); + U2 := Actor(laUpgradeable); + Require(U2.Entered.WaitFor(100) = wrTimeout, 'Two upgradeable readers overlapped'); + Require(U.Mempool = R.Mempool, 'Upgradeable and ordinary reader saw different mempools'); + U.ReleaseLock.SetEvent; + Entered(U2, 'Second upgradeable reader did not progress after first release'); + Require(R.ReleaseLock.WaitFor(0) <> wrSignaled, 'Reader sharing gate was released early'); +end; + +procedure TLockChecks.CheckUpgradeablePromotion; +var U, R, R2, U2: TLockActor; +begin + U := Actor(laPromotingUpgradeable); Entered(U, 'Promotable reader did not enter'); + R := Actor(laRead); Entered(R, 'Reader did not coexist before promotion'); + U.Promote.SetEvent; + Require(U.Writing.WaitFor(100) = wrTimeout, 'Upgrade entered writer while foreign reader remained'); + R.ReleaseLock.SetEvent; + Require(U.Writing.WaitFor(1500) = wrSignaled, 'Upgrade did not become writer after reader release'); + R2 := Actor(laRead); U2 := Actor(laUpgradeable); + Require(R2.Entered.WaitFor(100) = wrTimeout, 'Foreign reader overlapped upgraded writer'); + Require(U2.Entered.WaitFor(100) = wrTimeout, 'Foreign upgrader overlapped upgraded writer'); + U.LeaveWrite.SetEvent; + Require(U.AfterWrite.WaitFor(1500) = wrSignaled, 'Nested writer release did not complete'); + Entered(R2, 'Ordinary reader did not resume after nested writer release'); + Require(U2.Entered.WaitFor(100) = wrTimeout, 'ExitWrite also released the outer upgradeable lock'); + U.ReleaseLock.SetEvent; + Entered(U2, 'Balanced outer upgradeable release did not admit next upgrader'); +end; + +procedure TLockChecks.CheckUpgradeableException; +var U2, R: TLockActor; L, LWrite: TPCOperationsComp; LRaised: Boolean; +begin + L := FNode.LockMempoolUpgradeableRead; + try + LRaised := False; + try + LWrite := FNode.LockMempoolWrite; + try + Require(LWrite = L, 'Exception fixture changed mempool during promotion'); + raise EExpectedUpgradeFailure.Create('Intentional work failure'); + finally FNode.UnlockMempoolWrite; end; + except on E: EExpectedUpgradeFailure do LRaised := True; end; + Require(LRaised, 'Exception cleanup fixture did not execute failing work'); + R := Actor(laRead); Entered(R, 'Exception cleanup left nested writer held'); + U2 := Actor(laUpgradeable); + Require(U2.Entered.WaitFor(100) = wrTimeout, 'Exception cleanup discarded outer upgradeable ownership'); + finally FNode.UnlockMempoolUpgradeableRead; end; + Entered(U2, 'Outer upgradeable release did not recover after work exception'); +end; + +procedure TLockChecks.CheckUpgradeableCancellation; +var U, W: TLockActor; +begin + U := Actor(laUpgradeable); Entered(U, 'Cancelable upgradeable holder did not enter'); + W := Actor(laWrite); + Require(W.Entered.WaitFor(100) = wrTimeout, 'Writer overlapped upgradeable holder'); + U.Terminate; + Require(U.Completed.WaitFor(500) = wrSignaled, 'Upgradeable holder ignored cooperative termination'); + Require(U.ReleaseLock.WaitFor(0) <> wrSignaled, 'Cancellation test released upgradeable gate early'); + Entered(W, 'Canceled upgradeable holder leaked ownership and blocked writer'); +end; + +procedure TLockChecks.Run; +var I: Integer; Names: array[0..8] of String; +begin + Names[0] := 'concurrent readers'; Names[1] := 'writer exclusion/progress'; + Names[2] := 'nested writer/read/write'; Names[3] := 'timed/nonblocking acquisition'; + Names[4] := 'cooperative cancellation'; + Names[5] := 'upgradeable and ordinary read sharing'; + Names[6] := 'upgradeable promotion and balanced nested release'; + Names[7] := 'upgraded work exception cleanup'; + Names[8] := 'upgradeable cooperative cancellation'; + for I := 0 to 8 do begin + Log('START ' + Names[I]); + try + case I of + 0: CheckReaders; + 1: CheckExclusion; + 2: CheckNestedWriter; + 3: CheckTryAcquisition; + 4: CheckCancellation; + 5: CheckUpgradeableSharing; + 6: CheckUpgradeablePromotion; + 7: CheckUpgradeableException; + 8: CheckUpgradeableCancellation; + end; + finally StopActors; end; + Require(FCounters.Violations = 0, 'Actual lock allowed forbidden reader/writer overlap'); + Require((FCounters.Readers = 0) and (FCounters.Writers = 0), 'Unbalanced actor lock release'); + Log('PASS ' + Names[I]); + end; +end; + +function RunMempoolLockChecks(const ALogPath: String): Integer; +var Node: TNode; NetData: TNetData; Checks: TLockChecks; DirectoryName, PreviousDirectory: String; + ID: TGUID; BeforeThreads: Integer; +begin + Result := 1; Node := nil; NetData := nil; Checks := nil; + if FileExists(ALogPath) then raise Exception.Create('Refusing to append an earlier probe result'); + if TNode.NodeExists or TNetData.NetDataExists then + raise Exception.Create('Probe requires fresh node/network singletons'); + PreviousDirectory := TNode.GetPascalCoinDataFolder; + CreateGUID(ID); + DirectoryName := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + 'PascalCoinMempoolLock-' + GUIDToString(ID); + if DirectoryExists(DirectoryName) then raise Exception.Create('Refusing existing test data'); + ForceDirectories(DirectoryName); + TNode.SetPascalCoinDataFolder(DirectoryName); + BeforeThreads := TPCThread.ThreadCount; + try + TCrypto.InitCrypto; + Checks := TLockChecks.Create(nil, ALogPath); + Checks.Log('START published operation hash readiness'); + CheckPublishedOperationReadHashes; + Checks.Log('PASS published operation hash readiness'); + Checks.Log('START cached hash-tree lock synchronization'); + CheckCachedTreeReadTakesExistingLock; + Checks.Log('PASS cached hash-tree lock synchronization'); + Checks.Log('START parallel operation/hash-tree read consistency'); + CheckParallelOperationReadHashes; + Checks.Log('PASS parallel operation/hash-tree read consistency'); + FreeAndNil(Checks); + Node := TNode.Create(nil); + Node.BroadcastData := False; + NetData := TNetData.NetData; + NetData.NetConnectionsActive := False; + if Node.NetServer.Active or (NetData.NetStatistics.ActiveConnections <> 0) then + raise Exception.Create('Fresh lock checks must not enable listeners or peers'); + Checks := TLockChecks.Create(Node, ALogPath); + Checks.Log('Actual Node lock checks; generated empty data; no listeners, peers or broadcasts'); + if SameText(GetEnvironmentVariable('PASCAL_MEMPOOL_COMPARISON'), 'exclusive') then + Checks.Log('Reconstructed exclusive-lock comparison, not a historical source snapshot'); + try + Checks.Run; + Checks.Log('START actual miner-template conditional upgrade'); + CheckMinerTemplateUpgrades(Node); + Checks.Log('PASS actual miner-template conditional upgrade'); + Result := 0; + except on E: Exception do Checks.Log('FAIL ' + E.ClassName + ': ' + E.Message); end; + finally + Checks.Free; + NetData.Free; + Node.Free; + TNode.SetPascalCoinDataFolder(PreviousDirectory); + end; + if Result = 0 then begin + Checks := TLockChecks.Create(nil, ALogPath); + try + Checks.Require(TPCThread.ThreadCount = BeforeThreads, 'Owned node/actors remained after joined shutdown'); + Checks.Log('PASS joined node shutdown; all actual lock actors and core node worker released'); + finally Checks.Free; end; + end; +end; + +end. diff --git a/src/tests/experimental/native/mempool-lock/UMinerTemplateUpgradeChecks.pas b/src/tests/experimental/native/mempool-lock/UMinerTemplateUpgradeChecks.pas new file mode 100644 index 000000000..747b74b5d --- /dev/null +++ b/src/tests/experimental/native/mempool-lock/UMinerTemplateUpgradeChecks.pas @@ -0,0 +1,225 @@ +unit UMinerTemplateUpgradeChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. + + Exercise the actual inactive mining pool's template admission. No listener, + peer, wallet key or synthetic replacement of the production fill is used. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses UNode; + +procedure CheckMinerTemplateUpgrades(ANode: TNode); + +implementation + +uses Classes, SysUtils, SyncObjs, UThread, UPoolMining, UBlockChain, UPCDataTypes, + UNetProtocol; + +type + TMinerPoolAccess = class(TPoolMiningServer) + public + procedure FillTemplate; + end; + + THeldMempoolReader = class(TPCThread) + private + FNode: TNode; + protected + procedure BCExecute; override; + public + Entered, ReleaseRead, Completed: TEvent; + ErrorMessage: String; + constructor Create(ANode: TNode); + destructor Destroy; override; + end; + + TMinerFillThread = class(TPCThread) + private + FPool: TMinerPoolAccess; + protected + procedure BCExecute; override; + public + Attempted, Completed: TEvent; + ErrorMessage: String; + constructor Create(APool: TMinerPoolAccess); + destructor Destroy; override; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure TMinerPoolAccess.FillTemplate; +begin + inherited FillMinerOperations; +end; + +constructor THeldMempoolReader.Create(ANode: TNode); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); + Completed := TEvent.Create(nil, True, False, ''); +end; + +destructor THeldMempoolReader.Destroy; +begin + Terminate; + ReleaseRead.SetEvent; + if Suspended then Start; + WaitFor; + Completed.Free; + ReleaseRead.Free; + Entered.Free; + inherited; +end; + +procedure THeldMempoolReader.BCExecute; +begin + try + try + if Terminated then Exit; + FNode.LockMempoolRead; + try + Entered.SetEvent; + while not Terminated do + if ReleaseRead.WaitFor(10) = wrSignaled then Break; + finally + FNode.UnlockMempoolRead; + end; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TMinerFillThread.Create(APool: TMinerPoolAccess); +begin + inherited Create(True); + FreeOnTerminate := False; + FPool := APool; + Attempted := TEvent.Create(nil, True, False, ''); + Completed := TEvent.Create(nil, True, False, ''); +end; + +destructor TMinerFillThread.Destroy; +begin + Terminate; + if Suspended then Start; + WaitFor; + Completed.Free; + Attempted.Free; + inherited; +end; + +procedure TMinerFillThread.BCExecute; +begin + try + try + if Terminated then Exit; + Attempted.SetEvent; + FPool.FillTemplate; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +procedure CheckFillAgainstReader(ANode: TNode; APool: TMinerPoolAccess; + ARequiresWrite: Boolean); +var + LReader: THeldMempoolReader; + LFill: TMinerFillThread; +begin + LReader := nil; + LFill := nil; + try + LReader := THeldMempoolReader.Create(ANode); + LReader.Start; + Require(LReader.Entered.WaitFor(3000) = wrSignaled, + 'Ordinary reader did not acquire the actual mempool'); + LFill := TMinerFillThread.Create(APool); + LFill.Start; + Require(LFill.Attempted.WaitFor(3000) = wrSignaled, + 'Actual miner fill did not start'); + if ARequiresWrite then begin + Require(LFill.Completed.WaitFor(250) = wrTimeout, + 'Changed miner template rebuilt while another reader held the mempool'); + LReader.ReleaseRead.SetEvent; + Require(LReader.Completed.WaitFor(3000) = wrSignaled, + 'Ordinary reader did not release the mempool'); + Require(LFill.Completed.WaitFor(3000) = wrSignaled, + 'Actual miner template did not complete after promotion became possible'); + end else begin + Require(LFill.Completed.WaitFor(3000) = wrSignaled, + 'Unchanged miner template unnecessarily requested exclusive ownership'); + Require(LReader.Completed.WaitFor(0) = wrTimeout, + 'Unchanged-template check did not retain a concurrent ordinary reader'); + end; + Require(LFill.ErrorMessage = '', LFill.ErrorMessage); + Require(LReader.ErrorMessage = '', LReader.ErrorMessage); + finally + // Release the prerequisite reader before joining a possibly promoting fill. + LReader.Free; + LFill.Free; + end; +end; + +procedure CheckMinerTemplateUpgrades(ANode: TNode); +var + LPool: TMinerPoolAccess; + LMempool: TPCOperationsComp; + LOriginal, LChanged: TOperationBlock; +begin + Require(Assigned(ANode) and (TNode.Node = ANode), + 'Mining check requires the existing isolated node singleton'); + Require(not ANode.NetServer.Active, + 'Mining check must not run with a network listener'); + LMempool := ANode.LockMempoolRead; + try + Require((LMempool.Count = 0) and (ANode.Bank.SafeBox.AccountsCount = 0), + 'Mining check requires generated empty node data'); + LOriginal := LMempool.OperationBlock; + finally + ANode.UnlockMempoolRead; + end; + LPool := nil; + try + LPool := TMinerPoolAccess.Create; + Require(not LPool.Active and (LPool.ClientsCount = 0), + 'Mining check must keep the actual pool inactive'); + LPool.FillTemplate; + CheckFillAgainstReader(ANode, LPool, False); + LMempool := ANode.LockMempoolWrite; + try + LChanged := LMempool.OperationBlock; + LChanged.block_payload := TEncoding.ASCII.GetBytes('isolated miner upgrade check'); + LMempool.SetOperationBlock(LChanged); + finally + ANode.UnlockMempoolWrite; + end; + CheckFillAgainstReader(ANode, LPool, True); + CheckFillAgainstReader(ANode, LPool, False); + finally + LPool.Free; + LMempool := ANode.LockMempoolWrite; + try + LMempool.SetOperationBlock(LOriginal); + finally + ANode.UnlockMempoolWrite; + end; + end; +end; + +end. diff --git a/src/tests/experimental/native/mempool-lock/UOperationReadHashChecks.pas b/src/tests/experimental/native/mempool-lock/UOperationReadHashChecks.pas new file mode 100644 index 000000000..e0a7e2ba8 --- /dev/null +++ b/src/tests/experimental/native/mempool-lock/UOperationReadHashChecks.pas @@ -0,0 +1,299 @@ +unit UOperationReadHashChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. + + Shared Delphi/FPC checks for immutable published operation digests and the + existing hash-tree lock. No node, wallet data or network is required. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +procedure CheckPublishedOperationReadHashes; +procedure CheckCachedTreeReadTakesExistingLock; +procedure CheckParallelOperationReadHashes; + +implementation + +uses + Classes, SysUtils, SyncObjs, UThread, UMemory, UBlockChain, UOpTransaction, + UCrypto, UConst, UBaseTypes, UPCDataTypes; + +type + TOperationCacheAccess = class(TPCOperation) + public + function HasReadHashes: Boolean; + end; + + THashReadThread = class(TPCThread) + private + FTree: TOperationsHashTree; + FStart: TEvent; + FExpected: TRawBytes; + FIterations: Integer; + protected + procedure BCExecute; override; + public + Entered, Completed: TEvent; + ErrorMessage: String; + constructor Create(ATree: TOperationsHashTree; AStart: TEvent; + const AExpected: TRawBytes; AIterations: Integer); + destructor Destroy; override; + end; + + THeldTreeWriter = class(TPCThread) + private + FTree: TOperationsHashTree; + FOperation: TPCOperation; + procedure HoldChange(Sender: TObject); + protected + procedure BCExecute; override; + public + Entered, ReleaseChange, Completed: TEvent; + ErrorMessage: String; + constructor Create(ATree: TOperationsHashTree; AOperation: TPCOperation); + destructor Destroy; override; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +function SigningKey: TECPrivateKey; +begin + TCrypto.InitCrypto; + Result := TECPrivateKey.Create; + try + Result.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + except + Result.Free; + raise; + end; +end; + +function Transaction(AKey: TECPrivateKey; ANOperation: Cardinal): TPCOperation; +begin + Result := TOpTransaction.CreateTransaction(CT_PROTOCOL_6, 0, ANOperation, 1, + AKey, 100, 1, CT_TOperationPayload_NUL); +end; + +function TOperationCacheAccess.HasReadHashes: Boolean; +begin + Result := (Length(FBufferedSha256) = 32) and + (Length(FBufferedRipeMD160) = 20); +end; + +constructor THashReadThread.Create(ATree: TOperationsHashTree; AStart: TEvent; + const AExpected: TRawBytes; AIterations: Integer); +begin + inherited Create(True); + FreeOnTerminate := False; + FTree := ATree; + FStart := AStart; + FExpected := Copy(AExpected); + FIterations := AIterations; + Entered := TEvent.Create(nil, True, False, ''); + Completed := TEvent.Create(nil, True, False, ''); +end; + +destructor THashReadThread.Destroy; +begin + Terminate; + if Assigned(FStart) then FStart.SetEvent; + if Suspended then Start; + WaitFor; + Completed.Free; + Entered.Free; + inherited; +end; + +procedure THashReadThread.BCExecute; +var + I: Integer; + LHash: TRawBytes; + LOperation: TPCOperation; + LResume: TOperationResume; +begin + try + try + Require(FStart.WaitFor(5000) = wrSignaled, 'Hash reader start timed out'); + Entered.SetEvent; + for I := 0 to FIterations - 1 do begin + if Terminated then Exit; + LHash := FTree.HashTree; + Require(Length(LHash) = 32, 'A reader observed an incomplete tree digest'); + if Length(FExpected) > 0 then + Require(TBaseType.Equals(FExpected, LHash), 'Parallel tree digest changed'); + LOperation := FTree.GetOperation(I mod FTree.OperationsCount); + Require(TOperationCacheAccess(LOperation).HasReadHashes, + 'Published operation digest caches are not initialized'); + Require(TPCOperation.OperationToOperationResume(0, LOperation, False, + LOperation.SignerAccount, LResume), 'Operation resume conversion failed'); + end; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor THeldTreeWriter.Create(ATree: TOperationsHashTree; + AOperation: TPCOperation); +begin + inherited Create(True); + FreeOnTerminate := False; + FTree := ATree; + FOperation := AOperation; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseChange := TEvent.Create(nil, True, False, ''); + Completed := TEvent.Create(nil, True, False, ''); +end; + +destructor THeldTreeWriter.Destroy; +begin + Terminate; + if Assigned(ReleaseChange) then ReleaseChange.SetEvent; + if Suspended then Start; + WaitFor; + Completed.Free; + ReleaseChange.Free; + Entered.Free; + inherited; +end; + +procedure THeldTreeWriter.HoldChange(Sender: TObject); +begin + // The real public notification runs while InternalAdd retains its list lock. + Entered.SetEvent; + Require(ReleaseChange.WaitFor(5000) = wrSignaled, 'Held tree writer timed out'); +end; + +procedure THeldTreeWriter.BCExecute; +begin + try + if Terminated then Exit; + FTree.OnChanged := HoldChange; + try + Require(FTree.AddOperationToHashTree(FOperation), 'Held tree write failed'); + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + FTree.OnChanged := nil; + Completed.SetEvent; + end; +end; + +procedure CheckPublishedOperationReadHashes; +var + LKey: TECPrivateKey; + LOperation, LPublished: TPCOperation; + LFirst, LSecond: TOperationsHashTree; + LScope: TDisposables; +begin + LKey := LScope.AddObject(SigningKey) as TECPrivateKey; + LOperation := LScope.AddObject(Transaction(LKey, 1)) as TPCOperation; + LFirst := LScope.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LSecond := LScope.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + Require(LFirst.AddOperationToHashTree(LOperation), 'Initial publication failed'); + LPublished := LFirst.GetOperation(0); + // Inspect without calling digest getters, which would conceal a cold cache. + Require(TOperationCacheAccess(LPublished).HasReadHashes, + 'An operation was published before both read digest caches were complete'); + Require(LSecond.AddOperationToHashTree(LPublished), 'Alias publication failed'); + Require(LSecond.GetOperation(0) = LPublished, 'Existing storage identity was not reused'); + Require(TOperationCacheAccess(LSecond.GetOperation(0)).HasReadHashes, + 'A shared operation alias lost its initialized read digests'); + Require(TBaseType.Equals(LPublished.Sha256, + TCrypto.DoSha256(LPublished.GetBufferForOpHash(True))), 'Published SHA256 is incorrect'); + Require(TBaseType.Equals(LPublished.RipeMD160, + TCrypto.DoRipeMD160AsRaw(LPublished.GetBufferForOpHash(True))), 'Published RIPEMD160 is incorrect'); +end; + +procedure CheckCachedTreeReadTakesExistingLock; +var + LKey: TECPrivateKey; + LTree: TOperationsHashTree; + LFirst, LNext: TPCOperation; + LWarmHash: TRawBytes; + LStart: TEvent; + LWriter: THeldTreeWriter; + LReader: THashReadThread; + LScope: TDisposables; +begin + LKey := LScope.AddObject(SigningKey) as TECPrivateKey; + LTree := LScope.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LFirst := LScope.AddObject(Transaction(LKey, 1)) as TPCOperation; + LNext := LScope.AddObject(Transaction(LKey, 2)) as TPCOperation; + Require(LTree.AddOperationToHashTree(LFirst), 'Initial tree write failed'); + LWarmHash := LTree.HashTree; + Require(Length(LWarmHash) = 32, 'Initial tree cache did not warm'); + LStart := LScope.AddObject(TEvent.Create(nil, True, False, '')) as TEvent; + LWriter := LScope.AddObject(THeldTreeWriter.Create(LTree, LNext)) as THeldTreeWriter; + LReader := LScope.AddObject(THashReadThread.Create(LTree, LStart, nil, 1)) as THashReadThread; + LWriter.Start; + LReader.Start; + try + Require(LWriter.Entered.WaitFor(5000) = wrSignaled, 'Writer did not hold the real tree lock'); + LStart.SetEvent; + Require(LReader.Entered.WaitFor(5000) = wrSignaled, 'Reader did not enter its query'); + Require(LReader.Completed.WaitFor(250) = wrTimeout, + 'A cached digest read bypassed an active tree writer'); + finally + LWriter.ReleaseChange.SetEvent; + LStart.SetEvent; + LWriter.WaitFor; + LReader.WaitFor; + end; + Require(LWriter.ErrorMessage = '', LWriter.ErrorMessage); + Require(LReader.ErrorMessage = '', LReader.ErrorMessage); +end; + +procedure CheckParallelOperationReadHashes; +var + LKey: TECPrivateKey; + LTree, LReference: TOperationsHashTree; + LOperation: TPCOperation; + LReaders: array[0..3] of THashReadThread; + LStart: TEvent; + LExpected: TRawBytes; + I: Integer; + LScope: TDisposables; +begin + LKey := LScope.AddObject(SigningKey) as TECPrivateKey; + LTree := LScope.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LReference := LScope.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + for I := 1 to 64 do begin + LOperation := Transaction(LKey, I); + try + Require(LTree.AddOperationToHashTree(LOperation), 'Parallel fixture publication failed'); + finally + LOperation.Free; + end; + end; + // Warm only the separate reference: the readers race on a cold source tree. + LReference.CopyFromHashTree(LTree); + LExpected := LReference.HashTree; + LStart := LScope.AddObject(TEvent.Create(nil, True, False, '')) as TEvent; + for I := 0 to High(LReaders) do begin + LReaders[I] := LScope.AddObject(THashReadThread.Create(LTree, LStart, + LExpected, 128)) as THashReadThread; + LReaders[I].Start; + end; + LStart.SetEvent; + try + for I := 0 to High(LReaders) do begin + Require(LReaders[I].Completed.WaitFor(5000) = wrSignaled, + 'A parallel digest reader did not finish'); + Require(LReaders[I].ErrorMessage = '', LReaders[I].ErrorMessage); + end; + finally + LStart.SetEvent; + for I := 0 to High(LReaders) do LReaders[I].WaitFor; + end; +end; + +end. diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockBacklog.Assets.rc b/src/tests/experimental/native/network-backlog/BufferedBlockBacklog.Assets.rc new file mode 100644 index 000000000..fc1f4b20a --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockBacklog.Assets.rc @@ -0,0 +1,2 @@ +MAINICON ICON "../../../../../resources/wallet/PascalCoin.ico" +HISTORIC_BUFFERED_BLOCKS RCDATA "../../fixtures/mainnet-blocks-0-4.bin" diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.dpr b/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.dpr new file mode 100644 index 000000000..f0884b02c --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.dpr @@ -0,0 +1,11 @@ +program BufferedBlockBacklogProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses UWalletBufferedBlockChecks; + +{$R 'BufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunBufferedBlockBacklogProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.dproj b/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.dproj new file mode 100644 index 000000000..4894c254c --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{7E0C4074-998D-4F6C-BFB1-918DA51DF5A2}</ProjectGuid> + <MainSource>BufferedBlockBacklogProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-backlog\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-backlog\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-backlog\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">BufferedBlockBacklogProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.lpr b/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.lpr new file mode 100644 index 000000000..86161d543 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockBacklogProbe.lpr @@ -0,0 +1,15 @@ +program BufferedBlockBacklogProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletBufferedBlockChecks; + +{$R BufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunBufferedBlockBacklogProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.dpr b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.dpr new file mode 100644 index 000000000..d4ec09e95 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.dpr @@ -0,0 +1,9 @@ +program BufferedBlockFrameProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$APPTYPE CONSOLE} +uses System.SysUtils, UBufferedBlockFrameChecks; +{$R BufferedBlockBacklog.Assets.res} +begin + if ParamCount <> 1 then Halt(64); + ExitCode := RunBufferedBlockFrameChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.dproj b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.dproj new file mode 100644 index 000000000..b4d8294be --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.dproj @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{6426D7E1-595D-4AD5-B17E-239A5DB05D96}</ProjectGuid> + <MainSource>BufferedBlockFrameProbe.dpr</MainSource> + <Config>Debug</Config><Platform>Win64</Platform><Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms><AppType>Console</AppType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\generics.collections\inc</DCC_IncludePath> + <DCC_ResourcePath>..\..\lib\buffered-frames\Delphi\$(Platform)\$(Config)\res;$(DCC_ResourcePath)</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\buffered-frames\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\buffered-frames\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\buffered-frames\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation><DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UBufferedBlockFrameChecks.pas" /> + </ItemGroup> + <ProjectExtensions><Borland.Personality>Delphi.Personality.12</Borland.Personality><BorlandProject><Delphi.Personality><Source><Source Name="MainSource">BufferedBlockFrameProbe.dpr</Source></Source></Delphi.Personality><Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms></BorlandProject></ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.lpr b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.lpr new file mode 100644 index 000000000..a3814cb1f --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.lpr @@ -0,0 +1,10 @@ +program BufferedBlockFrameProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$mode delphi} +uses heaptrc, SysUtils, UBufferedBlockFrameChecks; +{$R BufferedBlockBacklog.Assets.rc} +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + ExitCode := RunBufferedBlockFrameChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.md b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.md new file mode 100644 index 000000000..d3722578a --- /dev/null +++ b/src/tests/experimental/native/network-backlog/BufferedBlockFrameProbe.md @@ -0,0 +1,45 @@ +# Buffered block frame regression + +`BufferedBlockFrameProbe.dproj` (Delphi) and `BufferedBlockFrameProbe.lpr` (FPC) +run the same 15 scenarios in `UBufferedBlockFrameChecks.pas`. Use the existing +native private-desktop broker, passing the probe executable and a new absolute +result-log path. Keep the existing OpenSSL 1.1 DLL beside the executable and set +`TEMP`/`TMP` to an owned empty test directory. The fixtures create fresh data and +use loopback sockets, generated state and the public historic blocks 0–4; they +do not open a user's wallet or connect to external peers. + +The actual incoming and outgoing processing owners receive a GetBlocks response +for blocks 0–3 followed by an unsolicited full NewBlock for block 4. Tests prove +the second complete frame is locally received before switching networking off, +in both the transport buffer and the parser suffix. They check all five real +validated commits, exact reopened bytes and terminal storage height, physical +synchronization scopes, delivered status events, zero live-peer counters after +retirement, and no new request or reserved control callback during the drain. +They also require exactly one `blocks.log` update per accepted block, preserving +the original source, peer, payload, timestamps, age and target columns for both +catch-up batches and unsolicited announcements. Failed or unprocessed suffixes +must not produce accepted-block entries. +One informational record identifies the received batch's count, height range, +payload byte count and peer before validation or acceptance. This distinguishes +already received work from later per-block acceptance without logging each read. + +Additional cases cover orderly EOF; Quit between atomic blocks; locally complete +zero-reference fast propagation; a missing-reference announcement without a new +fetch; malformed/full higher-protocol frames followed by a valid block; a first +buffered frame disconnected before parsing; and a genuine matching response +under a recursive legacy connection lock. The matching case exercises the actual +request/parser/processor/drain mechanism, not full orphan-chain selection. + +A transport subclass gates the return from the real inherited `ReceiveBuf` +before publication. Receive-idle must wait for those returned bytes, then the +same transport object must receive again after a sequential reconnect. No +concurrent reconnect guarantee is inferred from that case. + +The EOF fixture first proves exact received commits, persisted bytes and retired +physical scopes, then asks the network owner to switch off and waits for normal +owned retirement. It does not establish a passive EOF retirement deadline +or change the GUI's separate five-second shutdown assertion. Status evidence combines per-commit +physical counts, coalesced callbacks and the enclosing drain scope; it does not +claim instruction-level absence of every transient state under unrelated +simultaneous inputs. The incomplete suffix and missing fast references cannot be +completed offline and do not authorize another network read or request. diff --git a/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.dpr b/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.dpr new file mode 100644 index 000000000..98937977c --- /dev/null +++ b/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.dpr @@ -0,0 +1,11 @@ +program ConnectedMinerProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses Forms, UWalletBufferedBlockChecks; + +{$R 'BufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunConnectedMinerProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.dproj b/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.dproj new file mode 100644 index 000000000..0ea744ca0 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{AF0F95F7-C3E4-4565-A408-F8E29938C7DA}</ProjectGuid> + <MainSource>ConnectedMinerProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-backlog-connected-miner\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-backlog-connected-miner\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-backlog-connected-miner\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">ConnectedMinerProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.lpr b/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.lpr new file mode 100644 index 000000000..f82733395 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/ConnectedMinerProbe.lpr @@ -0,0 +1,15 @@ +program ConnectedMinerProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletBufferedBlockChecks; + +{$R BufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunConnectedMinerProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.dpr b/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.dpr new file mode 100644 index 000000000..0eac0f828 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.dpr @@ -0,0 +1,11 @@ +program IncomingBufferedBlockProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses Forms, UWalletBufferedBlockChecks; + +{$R 'BufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunIncomingBufferedBlockProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.dproj b/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.dproj new file mode 100644 index 000000000..4d25c3fcb --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{5E2C8D11-EFA3-48F1-8979-427CB576CBA0}</ProjectGuid> + <MainSource>IncomingBufferedBlockProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-backlog-incoming\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-backlog-incoming\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-backlog-incoming\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">IncomingBufferedBlockProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.lpr b/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.lpr new file mode 100644 index 000000000..9d7d6d829 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingBufferedBlockProbe.lpr @@ -0,0 +1,15 @@ +program IncomingBufferedBlockProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletBufferedBlockChecks; + +{$R BufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunIncomingBufferedBlockProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.dpr b/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.dpr new file mode 100644 index 000000000..5d2709068 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.dpr @@ -0,0 +1,11 @@ +program IncomingSettingsProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses Forms, UWalletBufferedBlockChecks; + +{$R 'BufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunIncomingSettingsProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.dproj b/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.dproj new file mode 100644 index 000000000..1cebaedd1 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{CD3B161D-6918-4708-A0C8-940E913E96A6}</ProjectGuid> + <MainSource>IncomingSettingsProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-backlog-incomingsettingsprobe\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-backlog-incomingsettingsprobe\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-backlog-incomingsettingsprobe\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">IncomingSettingsProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.lpr b/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.lpr new file mode 100644 index 000000000..a8d31beeb --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingSettingsProbe.lpr @@ -0,0 +1,15 @@ +program IncomingSettingsProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletBufferedBlockChecks; + +{$R BufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunIncomingSettingsProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.dpr b/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.dpr new file mode 100644 index 000000000..ba2da1268 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.dpr @@ -0,0 +1,11 @@ +program IncomingShutdownProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses Forms, UWalletBufferedBlockChecks; + +{$R 'BufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunIncomingShutdownProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.dproj b/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.dproj new file mode 100644 index 000000000..3db160b2f --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{61525ABD-6378-4F7C-9A07-F6574478AC1C}</ProjectGuid> + <MainSource>IncomingShutdownProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-backlog-incomingshutdownprobe\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-backlog-incomingshutdownprobe\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-backlog-incomingshutdownprobe\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">IncomingShutdownProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.lpr b/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.lpr new file mode 100644 index 000000000..e83b3f208 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/IncomingShutdownProbe.lpr @@ -0,0 +1,15 @@ +program IncomingShutdownProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletBufferedBlockChecks; + +{$R BufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunIncomingShutdownProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklog.Assets.rc b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklog.Assets.rc new file mode 100644 index 000000000..233a37798 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklog.Assets.rc @@ -0,0 +1,2 @@ +MAINICON ICON "../../../../../resources/wallet/PascalCoin.ico" +LARGE_BUFFERED_BLOCKS RCDATA "../../fixtures/mainnet-blocks-841529-841533.bin" diff --git a/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.dpr b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.dpr new file mode 100644 index 000000000..b55f9339f --- /dev/null +++ b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.dpr @@ -0,0 +1,15 @@ +program LargeBufferedBlockBacklogProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFNDEF LARGE_BACKLOG_PROBE} +{$MESSAGE ERROR 'Build the dedicated project with LARGE_BACKLOG_PROBE defined.'} +{$ENDIF} + +uses Forms, UWalletBufferedBlockChecks; + +{$R 'LargeBufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunLargeBufferedBlockProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.dproj b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.dproj new file mode 100644 index 000000000..022844b73 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.dproj @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{B2B4F41B-91D4-40D0-8D05-0F70890DD74A}</ProjectGuid> + <MainSource>LargeBufferedBlockBacklogProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\large-network-backlog\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\large-network-backlog\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\large-network-backlog\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + <DCC_Define>LARGE_BACKLOG_PROBE;$(DCC_Define)</DCC_Define> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="LargeBufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + <None Include="UWalletLargeBufferedBlockChecks.inc" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">LargeBufferedBlockBacklogProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.lpr b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.lpr new file mode 100644 index 000000000..80e9c8711 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/LargeBufferedBlockBacklogProbe.lpr @@ -0,0 +1,19 @@ +program LargeBufferedBlockBacklogProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} +{$IFNDEF LARGE_BACKLOG_PROBE} +{$FATAL Build with the global -dLARGE_BACKLOG_PROBE compiler option.} +{$ENDIF} + +// Match shipping allocation behavior for the dense public dataset. The small +// backlog scenarios and full wallet suite retain their independent heap checks. +uses Interfaces, UWalletBufferedBlockChecks; + +{$R LargeBufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + ExitCode := RunLargeBufferedBlockProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.dpr b/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.dpr new file mode 100644 index 000000000..fbc211f1d --- /dev/null +++ b/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.dpr @@ -0,0 +1,11 @@ +program OutgoingBufferedBlockProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses Forms, UWalletBufferedBlockChecks; + +{$R 'BufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunOutgoingBufferedBlockProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.dproj b/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.dproj new file mode 100644 index 000000000..d0e7bb7a5 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{3A61E086-EE92-49D1-AD6C-FFBDDE1E50BC}</ProjectGuid> + <MainSource>OutgoingBufferedBlockProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-backlog-owner\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-backlog-owner\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-backlog-owner\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">OutgoingBufferedBlockProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.lpr b/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.lpr new file mode 100644 index 000000000..908cc1dd4 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/OutgoingBufferedBlockProbe.lpr @@ -0,0 +1,15 @@ +program OutgoingBufferedBlockProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletBufferedBlockChecks; + +{$R BufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunOutgoingBufferedBlockProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.dpr b/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.dpr new file mode 100644 index 000000000..186955ebd --- /dev/null +++ b/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.dpr @@ -0,0 +1,11 @@ +program OutgoingShutdownProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses Forms, UWalletBufferedBlockChecks; + +{$R 'BufferedBlockBacklog.Assets.res'} + +begin + ExitCode := RunOutgoingShutdownProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.dproj b/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.dproj new file mode 100644 index 000000000..4004f951f --- /dev/null +++ b/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{72827126-5A27-44C0-ACCD-C532A01DBBFD}</ProjectGuid> + <MainSource>OutgoingShutdownProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\network-backlog-shutdown\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\network-backlog-shutdown\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\network-backlog-shutdown\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="BufferedBlockBacklog.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletBufferedBlockChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">OutgoingShutdownProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.lpr b/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.lpr new file mode 100644 index 000000000..b413c4293 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/OutgoingShutdownProbe.lpr @@ -0,0 +1,15 @@ +program OutgoingShutdownProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletBufferedBlockChecks; + +{$R BufferedBlockBacklog.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode := RunOutgoingShutdownProbe; +end. diff --git a/src/tests/experimental/native/network-backlog/README.md b/src/tests/experimental/native/network-backlog/README.md new file mode 100644 index 000000000..b77de20cc --- /dev/null +++ b/src/tests/experimental/native/network-backlog/README.md @@ -0,0 +1,71 @@ +# Received block backlog and wallet status + +These regressions feed the embedded +[small public-chain fixture](../../fixtures/README.md) to the experimental wallet +over a loopback socket. They check its status, grids and storage while the +backlog drains. No check loads existing wallet data or private keys. + +Run the FPC target from the repository root: + +```powershell +.\src\tests\experimental\RunWalletGridRefreshTests.ps1 -Backlog ` + -OutputDirectory <new-folder> +``` + +The output directory must be new. Each scenario runs on a private desktop and +must exit naturally with a clean FPC heap report. + +For Delphi, build a project below in the IDE and run it through the +[private-desktop broker](../README.md#private-desktop-broker) with a new absolute log-file path. Each +project also has an FPC `.lpr` entry point. + +## Scenarios + +| Case | Delphi project | Scenario | +|---|---|---| +| `direct` | `BufferedBlockBacklogProbe.dproj` | Calls the response handler directly and checks nested requests online and offline | +| `synchronization` | `BufferedBlockBacklogProbe.dproj` | Both synchronization cards stay visible through a quiet gap longer than six seconds, then clear | +| `outgoing` | `OutgoingBufferedBlockProbe.dproj` | OFF must not truncate the client's buffered batch | +| `outgoing-shutdown` | `OutgoingShutdownProbe.dproj` | Shutdown finishes within 15 seconds and stops between complete blocks | +| `incoming` | `IncomingBufferedBlockProbe.dproj` | OFF returns promptly while all received blocks drain | +| `incoming-settings` | `IncomingSettingsProbe.dproj` | Latest settings win after rapid OFF, ON, OFF, port and name changes | +| `incoming-shutdown` | `IncomingShutdownProbe.dproj` | As `outgoing-shutdown`, while OFF waits for an incoming batch | +| `connected-miner` | `ConnectedMinerProbe.dproj` | With a loopback miner, notifications reach the UI promptly while the bank writer is held | + +`BufferedBlockBacklogProbe` runs the case named in `PASCALCOIN_BACKLOG_CASE`, or +`direct` when it is unset. Other projects ignore the variable. + +## Optional large public-chain fixture + +`LargeBufferedBlockBacklogProbe.dproj` and its FPC `.lpr` entry point run the +main form against a chain copy at block count **841529** with **185831** pending +operations. The embedded +[five public blocks](../../fixtures/mainnet-blocks-841529-841533.json) advance +it to **841534**. + +Prepare a new app root that holds a `LargeBacklog.owned` marker and a `Data` +junction to that chain copy, but no `WalletKeys.dat` or `AppParams.prm`. Confirm +that `Data` resolves to the copy. The run changes the copy, so reserve it for +this run alone. The test never uses a live wallet directory. + +```powershell +.\src\tests\experimental\RunWalletGridRefreshTests.ps1 -LargeBacklog ` + -LargeDataRoot <large-wallet-copy> ` + -OutputDirectory <another-new-folder> +``` + +Without the script, set `PASCALCOIN_LARGE_BACKLOG_ROOT` to the app root and run +the executable through the broker with a new absolute log-file path. A direct +FPC compilation also needs `-dLARGE_BACKLOG_PROBE`. + +The run requires that: + +- the initial pages show at least 326,387 owned accounts and 1,000 + pending-history rows +- both grids stay covered without new queries while the batch drains after OFF +- main-message dispatch stays below 400 ms +- the pending operations stay unchanged +- all five blocks and 185,831 pending operations reopen after shutdown + +Keep before-and-after hashes of the chain files, and restore the copy before +the next run. diff --git a/src/tests/experimental/native/network-backlog/UBufferedBlockFrameChecks.pas b/src/tests/experimental/native/network-backlog/UBufferedBlockFrameChecks.pas new file mode 100644 index 000000000..89f9387a1 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/UBufferedBlockFrameChecks.pas @@ -0,0 +1,906 @@ +unit UBufferedBlockFrameChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Actual incoming/outgoing socket owners receive a GetBlocks response followed + by a full unsolicited NewBlock. Gates observe receipt, not simulated commits. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type + TBufferedFrameScenario = (bfsOutgoingTransportOff, bfsIncomingTransportOff, + bfsOutgoingParserOff, bfsIncomingParserOff, bfsOutgoingEOF, bfsIncomingEOF, + bfsOutgoingShutdown, bfsIncomingShutdown, bfsOutgoingFastComplete, + bfsIncomingFastMissing, bfsOutgoingMalformed, bfsIncomingBadProtocol, + bfsOutgoingFirstBufferedOff, bfsOutgoingMatchedOff, bfsReceivePublication); + +procedure RunBufferedFrameScenario(AScenario: TBufferedFrameScenario); +function RunBufferedBlockFrameChecks(const ALogFile: String): Integer; + +implementation + +uses Windows, Classes, SysUtils, SyncObjs, Generics.Collections, blcksock, + UNode, UNetProtocol, UTCPIP, UThread, UBlockChain, UAccounts, UFileStorage, + UConst, UCrypto, UPCDataTypes, UBaseTypes; + +type + TPeerSafeBox = class(TPCSafeBox) + public + procedure AddHeader(const AHeader: TOperationBlock); + end; + + TFrameCase = class; + TMatchedResponseTask = class(TPCThread) + private + FOwner: TFrameCase; + FStarted: Boolean; + protected + procedure BCExecute; override; + public + Done: TEvent; + Error: String; + constructor Create(AOwner: TFrameCase); + destructor Destroy; override; + end; + + TFrameConnectionAccess = class(TNetConnection) + public + procedure Apply(const AHeader: TNetHeaderData; AData: TStream); + procedure FinishBuffered; + end; + + TNetworkOff = class(TPCThread) + private + FOwner: TFrameCase; + FStarted: Boolean; + protected + procedure BCExecute; override; + public + Done: TEvent; + Error: String; + constructor Create(AOwner: TFrameCase); + destructor Destroy; override; + end; + + TFrameCase = class(TComponent) + private + FNode: TNode; + FNet: TNetData; + FConnection: TNetConnection; + FTransport: TBufferedNetTcpIpClient; + FPeer, FListener: TTCPBlockSocket; + FBankNotify: TPCBankNotify; + FNodeNotify: TNodeNotifyEvents; + FFirstCommit, FRelease: TEvent; + FOff: TNetworkOff; + FMatchedTask: TMatchedResponseTask; + FMatched: Boolean; + FRequestID: Cardinal; + FOldDirectory, FDirectory: String; + FExpectedLogSender, FExpectedLogRemote: String; + FExpectedBatchBytes: Int64; + FCommits, FReservedCalls, FDestroyed: LongInt; + FIdleBeforeDrain: Boolean; + FObserving: Boolean; + FExpected: Integer; + FFast, FMissing, FMalformed, FBadProtocol: Boolean; + FWire: TMemoryStream; + FBlocks: array[0..4] of TPCOperationsComp; + FPeerSafeBox: TPeerSafeBox; + procedure Committed(Sender: TObject); + procedure SynchronizationChanged(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); + procedure Reserved(NetData: TNetData; SenderConnection: TNetConnection; + const HeaderData: TNetHeaderData; ReceivedData, ResponseData: TStream); + procedure Await(AEvent: TEvent; ATimeout: Cardinal; const AReason: String); + procedure Open(AIncoming: Boolean); + procedure LoadBlocks; + procedure CaptureFrame(AType: TNetTransferType; AOperation: Word; + ARequest: Cardinal; AResult: TStream); + procedure BuildFrames(AFirst, ASecond: TMemoryStream); + procedure SendWire(AWire: TMemoryStream); + function BufferedBytes: Int64; + procedure CheckSecondReceived(ABytes: Int64; AParserSuffix: Boolean); + procedure CheckStoredBlocks; + procedure CheckBlockLog; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + constructor Create; + destructor Destroy; override; + procedure Run(AScenario: TBufferedFrameScenario); + end; + + TGateReadClient = class(TBufferedNetTcpIpClient) + protected + function ReceiveBuf(var Buf; BufSize: Integer): Integer; override; + public + Entered, ReleaseRead: TEvent; + GateNext: Boolean; + constructor Create; + destructor Destroy; override; + end; + + TReadQuiescenceTask = class(TPCThread) + private + FStarted: Boolean; + protected + procedure BCExecute; override; + public + Client: TGateReadClient; + Paused, Done: TEvent; + Captured, Error: String; + constructor Create(AClient: TGateReadClient); + destructor Destroy; override; + end; + +var GLog: TStringList; GLogFile: String; + +procedure Log(const AText: String); +begin + if not Assigned(GLog) then Exit; + GLog.Add(AText); + GLog.SaveToFile(GLogFile); +end; + +procedure Require(AValue: Boolean; const AReason: String); +begin + if not AValue then raise Exception.Create(AReason); +end; + +procedure Pump; +begin + CheckSynchronize(0); + Sleep(1); +end; + +procedure TFrameConnectionAccess.Apply(const AHeader: TNetHeaderData; AData: TStream); +begin + DoProcess_GetBlocks_Response(AHeader,AData); +end; + +procedure TFrameConnectionAccess.FinishBuffered; +begin + DrainReceivedBlockFrames; +end; + +constructor TMatchedResponseTask.Create(AOwner: TFrameCase); +begin + inherited Create(True); + FreeOnTerminate:=False; + FOwner:=AOwner; + Done:=TEvent.Create(nil,True,False,''); +end; + +destructor TMatchedResponseTask.Destroy; +begin + if FStarted then WaitFor; + Done.Free; + inherited; +end; + +procedure TMatchedResponseTask.BCExecute; +var LSend,LReceived: TMemoryStream; LHeader: TNetHeaderData; LStart,LEnd: Cardinal; +begin + LSend:=TMemoryStream.Create; LReceived:=TMemoryStream.Create; + try + try + FOwner.FNode.BeginBlockChainUpdate; + try + Require(FOwner.FNet.ConnectionLock(Self,FOwner.FConnection,1000), + 'Matched request cannot acquire its real recursive processing lock'); + try + LStart:=0; LEnd:=3; LSend.WriteBuffer(LStart,4); LSend.WriteBuffer(LEnd,4); + FOwner.FNet.RegisterRequest(FOwner.FConnection,CT_NetOp_GetBlocks,FOwner.FRequestID); + Require(FOwner.FConnection.DoSendAndWaitForResponse(CT_NetOp_GetBlocks, + FOwner.FRequestID,LSend,LReceived,5000,LHeader),'The real matching response did not arrive'); + Require((LHeader.operation=CT_NetOp_GetBlocks) and + (LHeader.request_id=FOwner.FRequestID),'The matching branch returned the wrong frame'); + TFrameConnectionAccess(FOwner.FConnection).Apply(LHeader,LReceived); + finally + try TFrameConnectionAccess(FOwner.FConnection).FinishBuffered; + finally FOwner.FNet.ConnectionUnlock(FOwner.FConnection); end; + end; + finally FOwner.FNode.EndBlockChainUpdate; end; + except on E: Exception do Error:=E.ClassName+': '+E.Message; end; + finally LReceived.Free; LSend.Free; Done.SetEvent; end; +end; + +constructor TGateReadClient.Create; +begin + inherited Create(nil); + Entered := TEvent.Create(nil,True,False,''); + ReleaseRead := TEvent.Create(nil,True,False,''); +end; + +destructor TGateReadClient.Destroy; +begin + ReleaseRead.SetEvent; + inherited; + ReleaseRead.Free; + Entered.Free; +end; + +function TGateReadClient.ReceiveBuf(var Buf; BufSize: Integer): Integer; +begin + Result := inherited ReceiveBuf(Buf,BufSize); + if (Result>0) and GateNext then begin + GateNext := False; + Entered.SetEvent; + Require(ReleaseRead.WaitFor(5000)=wrSignaled,'The real returned-read gate expired'); + end; +end; + +constructor TReadQuiescenceTask.Create(AClient: TGateReadClient); +begin + inherited Create(True); + FreeOnTerminate := False; + Client := AClient; + Paused := TEvent.Create(nil,True,False,''); + Done := TEvent.Create(nil,True,False,''); +end; + +destructor TReadQuiescenceTask.Destroy; +begin + if FStarted then WaitFor; + Done.Free; Paused.Free; + inherited; +end; + +procedure TReadQuiescenceTask.BCExecute; +var LBuffer: TMemoryStream; LBytes: RawByteString; +begin + try + try + Client.PauseReceiving; + try + Client.Disconnect; + Paused.SetEvent; + Client.WaitForReceiveIdle; + LBuffer := Client.ReadBufferLock; + try + SetLength(LBytes,LBuffer.Size); + if Length(LBytes)>0 then Move(LBuffer.Memory^,LBytes[1],Length(LBytes)); + finally Client.ReadBufferUnlock; end; + Captured := String(LBytes); + finally Client.ResumeReceiving; end; + except on E: Exception do Error := E.ClassName+': '+E.Message; end; + finally Done.SetEvent; end; +end; + +procedure CheckReceivePublication; +const CFirst: RawByteString='returned-before-append'; CSecond: RawByteString='reconnected-input'; +var LClient: TGateReadClient; LListener, LPeer: TTCPBlockSocket; + LTask: TReadQuiescenceTask; LBuffer: TMemoryStream; LBytes: RawByteString; LUntil: UInt64; +begin + LClient:=nil; LPeer:=nil; LTask:=nil; + LListener:=TTCPBlockSocket.Create; + try + LListener.CreateSocket; LListener.Bind('127.0.0.1','0'); LListener.Listen; + Require(LListener.LastError=0,'Cannot open the receive-publication raw peer'); + LClient:=TGateReadClient.Create; + LClient.RemoteHost:='127.0.0.1'; LClient.RemotePort:=LListener.GetLocalSinPort; + LClient.GateNext:=True; + Require(LClient.Connect,'Cannot connect the real buffered transport'); + Require(LListener.CanRead(1000),'Receive-publication peer did not accept'); + LPeer:=TTCPBlockSocket.Create; LPeer.Socket:=LListener.Accept; + Require(LPeer.SendBuffer(@CFirst[1],Length(CFirst))=Length(CFirst),'Cannot send returned-read bytes'); + Require(LClient.Entered.WaitFor(3000)=wrSignaled,'The inherited socket read did not return'); + LTask:=TReadQuiescenceTask.Create(LClient); LTask.Start; LTask.FStarted:=True; + Require(LTask.Paused.WaitFor(1000)=wrSignaled,'The owner did not pause/disconnect'); + Require(LTask.Done.WaitFor(200)=wrTimeout,'Idle was declared before returned bytes were published'); + LClient.ReleaseRead.SetEvent; + Require(LTask.Done.WaitFor(3000)=wrSignaled,'Receive publication did not release the idle waiter'); + Require((LTask.Error='') and (LTask.Captured=String(CFirst)), + 'Disconnect lost bytes returned by the real inherited socket read: '+LTask.Error); + LTask.Free; LTask:=nil; + LBuffer:=LClient.ReadBufferLock; + try LBuffer.Size:=0; finally LClient.ReadBufferUnlock; end; + LPeer.Free; LPeer:=nil; + Require(LClient.Connect,'Balanced resume did not permit same-object reconnect'); + Require(LListener.CanRead(1000),'Reconnect did not reach the raw peer'); + LPeer:=TTCPBlockSocket.Create; LPeer.Socket:=LListener.Accept; + Require(LPeer.SendBuffer(@CSecond[1],Length(CSecond))=Length(CSecond),'Cannot send reconnected bytes'); + LUntil:=GetTickCount64+3000; + repeat + LBuffer:=LClient.ReadBufferLock; + try + SetLength(LBytes,LBuffer.Size); + if Length(LBytes)>0 then Move(LBuffer.Memory^,LBytes[1],Length(LBytes)); + finally LClient.ReadBufferUnlock; end; + if LBytes=CSecond then Break; + Pump; + until GetTickCount64>=LUntil; + Require(LBytes=CSecond,'The reused IO worker did not receive after reconnect'); + Log('PASS real returned-read publication waits + same-object reconnect'); + finally + if Assigned(LClient) then LClient.ReleaseRead.SetEvent; + LTask.Free; LClient.Free; LPeer.Free; LListener.Free; + end; +end; + +procedure TPeerSafeBox.AddHeader(const AHeader: TOperationBlock); +begin + AddNew(AHeader); +end; + +constructor TNetworkOff.Create(AOwner: TFrameCase); +begin + inherited Create(True); + FreeOnTerminate := False; + FOwner := AOwner; + Done := TEvent.Create(nil, True, False, ''); +end; + +destructor TNetworkOff.Destroy; +begin + if FStarted then WaitFor; + Done.Free; + inherited; +end; + +procedure TNetworkOff.BCExecute; +begin + try + try FOwner.FNet.NetConnectionsActive := False; + except on E: Exception do Error := E.ClassName + ': ' + E.Message; end; + finally Done.SetEvent; end; +end; + +constructor TFrameCase.Create; +begin + inherited Create(nil); + FFirstCommit := TEvent.Create(nil, True, False, ''); + FRelease := TEvent.Create(nil, True, False, ''); + FWire := TMemoryStream.Create; +end; + +destructor TFrameCase.Destroy; +var I: Integer; +begin + FObserving := False; + if Assigned(FNet) then FNet.BeginShutdown; + FRelease.SetEvent; + FOff.Free; + FMatchedTask.Free; + if Assigned(FNet) then begin + FNet.NetConnectionsActive := False; + FNet.Free; + end; + // Keep commit observers and their gate alive until every actual owner and + // its callback has joined, including cleanup after a failed assertion. + if Assigned(FNodeNotify) then FNodeNotify.Node := nil; + FNodeNotify.Free; + FBankNotify.Free; + FNode.Free; + FPeer.Free; + FListener.Free; + for I := 0 to 4 do FBlocks[I].Free; + FPeerSafeBox.Free; + FWire.Free; + FRelease.Free; + FFirstCommit.Free; + if FOldDirectory <> '' then TNode.SetPascalCoinDataFolder(FOldDirectory); + inherited; +end; + +procedure TFrameCase.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited; + if (Operation = opRemove) and (AComponent = FConnection) then + InterlockedExchange(FDestroyed, 1); +end; + +procedure TFrameCase.Committed(Sender: TObject); +var LCount: Integer; LStatus: TNodeSynchronizationStatus; +begin + LCount := InterlockedIncrement(FCommits); + LStatus:=FNode.GetSynchronizationStatus; + Require((LStatus.State = nssSynchronizing) and (LStatus.ActiveTransfers>0), + 'A committed buffered block lost its physical synchronization scope'); + Log(Format('COMMITTED block=%d physical_scopes=%d',[LCount-1,LStatus.ActiveTransfers])); + if LCount = 1 then begin + FFirstCommit.SetEvent; + Require(FRelease.WaitFor(10000) = wrSignaled, 'First commit gate expired'); + end; +end; + +procedure TFrameCase.SynchronizationChanged(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); +begin + if FObserving and (InterlockedCompareExchange(FCommits, 0, 0) < FExpected) and + (AStatus.State <> nssSynchronizing) then FIdleBeforeDrain := True; +end; + +procedure TFrameCase.Reserved(NetData: TNetData; SenderConnection: TNetConnection; + const HeaderData: TNetHeaderData; ReceivedData, ResponseData: TStream); +begin + InterlockedIncrement(FReservedCalls); +end; + +procedure TFrameCase.Await(AEvent: TEvent; ATimeout: Cardinal; const AReason: String); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + ATimeout; + repeat + Pump; + if AEvent.WaitFor(0) = wrSignaled then Exit; + until GetTickCount64 >= LUntil; + raise Exception.Create(AReason); +end; + +procedure TFrameCase.Open(AIncoming: Boolean); +var LID: TGUID; LDirectory: String; LPort: Word; LUntil: UInt64; + LConnections: TList<TNetConnection>; LConnection: TNetConnection; + LFixed: TNodeServerAddressArray; +begin + Require(not TNode.NodeExists and not TNetData.NetDataExists, + 'A frame scenario requires fresh owned core singletons'); + FOldDirectory := TNode.GetPascalCoinDataFolder; + CreateGUID(LID); + Require(GetEnvironmentVariable('TEMP')<>'','Provide an owned temporary test directory'); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'BufferedFrames-' + GUIDToString(LID); + ForceDirectories(LDirectory); + FDirectory := LDirectory; + TNode.SetPascalCoinDataFolder(LDirectory); + TCrypto.InitCrypto; + FNode := TNode.Create(nil); + FNode.NodeLogFilename := FDirectory + PathDelim + 'blocks.log'; + FNode.BroadcastData := False; + FNode.Bank.StorageClass := TFileStorage; + FNet := TNetData.NetData; + FNet.NetConnectionsActive := False; + TNode.DecodeIpStringToNodeServerAddressArray('127.0.0.1:1', LFixed); + FNet.DiscoverFixedServersOnly(LFixed); + FNet.OnProcessReservedAreaMessage := Reserved; + FListener := TTCPBlockSocket.Create; + FListener.CreateSocket; + FListener.Bind('127.0.0.1', '0'); + Require(FListener.LastError = 0, 'Cannot reserve an owned loopback port'); + LPort := FListener.GetLocalSinPort; + if AIncoming then begin + FListener.CloseSocket; + FNode.NetServer.Port := LPort; + end else begin + FListener.Listen; + Require(FListener.LastError = 0, 'Cannot listen on the owned raw peer'); + FNode.NetServer.Port := 0; + end; + FNet.NetConnectionsActive := True; + FPeer := TTCPBlockSocket.Create; + if AIncoming then begin + FPeer.Connect('127.0.0.1', IntToStr(LPort)); + Require(FPeer.LastError = 0, 'Cannot connect to the real incoming owner'); + LUntil := GetTickCount64 + 3000; + repeat + LConnections := FNet.NetConnections.LockList; + try + for LConnection in LConnections do + if LConnection is TNetServerClient then FConnection := LConnection; + finally FNet.NetConnections.UnlockList; end; + if Assigned(FConnection) then Break; + Pump; + until GetTickCount64 >= LUntil; + end else begin + FConnection := TNetClient.Create(nil); + Require(FConnection.ConnectTo('127.0.0.1', LPort), 'Cannot connect the real outgoing owner'); + Require(FListener.CanRead(1000), 'Raw peer did not accept the outgoing owner'); + FPeer.Socket := FListener.Accept; + end; + Require(Assigned(FConnection), 'The real transport owner did not register its connection'); + FExpectedLogSender := FConnection.ClassName; + FExpectedLogRemote := FConnection.ClientRemoteAddr; + FConnection.FreeNotification(Self); + FTransport := TBufferedNetTcpIpClient(FConnection.Client); + FBankNotify := TPCBankNotify.Create(Self); + FBankNotify.Bank := FNode.Bank; + FBankNotify.OnNewBlock := Committed; + FNodeNotify := TNodeNotifyEvents.Create(nil); + FNodeNotify.OnSynchronizationChanged := SynchronizationChanged; + FNodeNotify.Node := FNode; +end; + +procedure TFrameCase.LoadBlocks; +var LSource: TResourceStream; LCount: Cardinal; I: Integer; LError: String; +begin + LSource := TResourceStream.Create(HInstance, 'HISTORIC_BUFFERED_BLOCKS', RT_RCDATA); + try + LSource.ReadBuffer(LCount, SizeOf(LCount)); + Require(LCount = 5, 'Expected the provenance-checked five historic blocks'); + FPeerSafeBox := TPeerSafeBox.Create; + for I := 0 to 4 do begin + FBlocks[I] := TPCOperationsComp.Create(nil); + Require(FBlocks[I].LoadBlockFromStream(LSource, LError), LError); + Require(FBlocks[I].OperationBlock.block = Cardinal(I), 'Wrong historic block order'); + FPeerSafeBox.AddHeader(FBlocks[I].OperationBlock); + end; + Require(LSource.Position = LSource.Size, 'Unexpected historic fixture suffix'); + finally LSource.Free; end; +end; + +procedure TFrameCase.CaptureFrame(AType: TNetTransferType; AOperation: Word; + ARequest: Cardinal; AResult: TStream); +var LUntil: UInt64; LPacket: RawByteString; LAt, LReadAt: Int64; + LHeader: TNetHeaderData; LPayload: TMemoryStream; LMore: Boolean; +begin + LPayload := TMemoryStream.Create; + try + LUntil := GetTickCount64 + 3000; + repeat + LAt := FWire.Position; + while TNetData.ExtractHeaderInfo(FWire, LHeader, LPayload, LMore) do begin + if (LHeader.header_type = AType) and (LHeader.operation = AOperation) and + (LHeader.request_id = ARequest) then begin + FWire.Position := LAt; + AResult.CopyFrom(FWire, 22 + LHeader.buffer_data_length); + Exit; + end; + LAt := FWire.Position; + end; + if FPeer.CanRead(10) then begin + LPacket := FPeer.RecvPacket(10); + if Length(LPacket) > 0 then begin + LReadAt := FWire.Position; + FWire.Position := FWire.Size; + FWire.WriteBuffer(LPacket[1], Length(LPacket)); + FWire.Position := LReadAt; + end; + end; + Pump; + until GetTickCount64 >= LUntil; + raise Exception.Create('The actual sender did not emit the expected wire frame'); + finally LPayload.Free; end; +end; + +procedure TFrameCase.BuildFrames(AFirst, ASecond: TMemoryStream); +var LRequest, LStart, LEnd, LCount: Cardinal; I: Integer; + LPayload, LRequestWire, LControl: TMemoryStream; LOpcode, LBadVersion: Word; + LReference: UInt64; +begin + LPayload := TMemoryStream.Create; + LRequestWire := TMemoryStream.Create; + LControl := TMemoryStream.Create; + try + LRequest := FNet.NewRequestId; + FRequestID:=LRequest; + LStart := 0; LEnd := 3; + LPayload.WriteBuffer(LStart, 4); LPayload.WriteBuffer(LEnd, 4); + if not FMatched then begin + FNet.RegisterRequest(FConnection, CT_NetOp_GetBlocks, LRequest); + FConnection.DoSend(ntp_request, CT_NetOp_GetBlocks, 0, LRequest, LPayload); + CaptureFrame(ntp_request, CT_NetOp_GetBlocks, LRequest, LRequestWire); + end; + LPayload.Clear; + LCount := 4; LPayload.WriteBuffer(LCount, 4); + for I := 0 to 3 do Require(FBlocks[I].SaveBlockToStream(False, LPayload), 'Cannot save response block'); + FExpectedBatchBytes := LPayload.Size; + FConnection.DoSend(ntp_response, CT_NetOp_GetBlocks, 0, LRequest, LPayload); + CaptureFrame(ntp_response, CT_NetOp_GetBlocks, LRequest, AFirst); + LPayload.Clear; + Require(FBlocks[4].SaveBlockToStream(FFast, LPayload), 'Cannot save NewBlock'); + TStreamOp.WriteAnsiString(LPayload, FPeerSafeBox.AggregatedHashrate.RawValue); + LOpcode:=CT_NetOp_NewBlock; + if FFast then begin + LOpcode:=CT_NetOp_NewBlock_Fast_Propagation; + LCount:=0; if FMissing then LCount:=1; + LPayload.WriteBuffer(LCount,SizeOf(LCount)); + if FMissing then begin + LReference:=High(UInt64); + LPayload.WriteBuffer(LReference,SizeOf(LReference)); + end; + end; + if FMalformed then begin + LPayload.Clear; + LCount:=0; + LPayload.WriteBuffer(LCount,2); + end; + FConnection.DoSend(ntp_autosend, LOpcode, 0, 0, LPayload); + CaptureFrame(ntp_autosend, LOpcode, 0, ASecond); + if FBadProtocol then begin + LBadVersion:=CT_NetProtocol_Available+1; + ASecond.Position:=14; ASecond.WriteBuffer(LBadVersion,SizeOf(LBadVersion)); + ASecond.Position:=ASecond.Size; + end; + if FMalformed or FBadProtocol then begin + // A valid complete block after rejected data must not bypass the normal + // session invalidation through a disconnected second acceptance path. + LPayload.Clear; Require(FBlocks[4].SaveBlockToStream(False,LPayload),'Cannot save trailing valid block'); + TStreamOp.WriteAnsiString(LPayload,FPeerSafeBox.AggregatedHashrate.RawValue); + FConnection.DoSend(ntp_autosend,CT_NetOp_NewBlock,0,0,LPayload); + CaptureFrame(ntp_autosend,CT_NetOp_NewBlock,0,ASecond); + end; + // A complete non-block control frame and an incomplete suffix must not + // become fresh protocol work when the disconnected owner drains blocks. + FConnection.DoSend(ntp_request, CT_NetOp_Reserved_Start, 0, 123, nil); + CaptureFrame(ntp_request, CT_NetOp_Reserved_Start, 123, LControl); + ASecond.CopyFrom(LControl, 0); + LStart := CT_MagicNetIdentification; + ASecond.WriteBuffer(LStart, 3); + finally LControl.Free; LRequestWire.Free; LPayload.Free; end; +end; + +procedure TFrameCase.SendWire(AWire: TMemoryStream); +begin + Require(FPeer.SendBuffer(AWire.Memory, AWire.Size) = AWire.Size, + 'Raw peer did not send the complete production frame'); +end; + +function TFrameCase.BufferedBytes: Int64; +var LBuffer: TMemoryStream; +begin + LBuffer := FTransport.ReadBufferLock; + try Result := LBuffer.Size; finally FTransport.ReadBufferUnlock; end; +end; + +procedure TFrameCase.CheckSecondReceived(ABytes: Int64; AParserSuffix: Boolean); +var LUntil: UInt64; LHeader: TNetHeaderData; LMore, LComplete: Boolean; LSavedPosition: Int64; + LCopy, LPayload, LBuffer: TMemoryStream; +begin + LUntil := GetTickCount64 + 3000; + repeat + if FTransport.BytesReceived >= ABytes then Break; + Pump; + until GetTickCount64 >= LUntil; + Require(FTransport.BytesReceived >= ABytes, 'Complete second wire frame was not received locally'); + if AParserSuffix then begin + Require(BufferedBytes = 0, 'Coalesced frames were not transferred into the parser suffix'); + Log('RECEIVED complete NEW_BLOCK4 retained in parser suffix'); + end else begin + LCopy := TMemoryStream.Create; LPayload := TMemoryStream.Create; + try + // ReceiveBuf counts bytes before the receive thread appends them to the + // buffer, so wait until the retained bytes hold the complete frame. + LUntil := GetTickCount64 + 3000; + repeat + LCopy.Clear; LPayload.Clear; + LBuffer := FTransport.ReadBufferLock; + try + LSavedPosition := LBuffer.Position; + try LCopy.CopyFrom(LBuffer, 0); finally LBuffer.Position := LSavedPosition; end; + finally FTransport.ReadBufferUnlock; end; + LCopy.Position := 0; + LComplete := TNetData.ExtractHeaderInfo(LCopy, LHeader, LPayload, LMore) and + (LHeader.operation in [CT_NetOp_NewBlock,CT_NetOp_NewBlock_Fast_Propagation]) and + (LHeader.header_type = ntp_autosend); + if LComplete or (GetTickCount64 >= LUntil) then Break; + Pump; + until False; + Require(LComplete, 'The retained transport bytes do not contain the complete NewBlock frame'); + Log('RECEIVED complete announcement frame retained in transport buffer'); + finally LPayload.Free; LCopy.Free; end; + end; +end; + +procedure TFrameCase.CheckStoredBlocks; +var LBank: TPCBank; LStorage: TFileStorage; LBlock: TPCOperationsComp; + LExpected, LActual: TMemoryStream; I: Integer; +begin + LBank := TPCBank.Create(nil); + LBlock := TPCOperationsComp.Create(nil); + LExpected := TMemoryStream.Create; LActual := TMemoryStream.Create; + try + LBank.StorageClass := TFileStorage; + LStorage := TFileStorage(LBank.Storage); + LStorage.ReadOnly := True; + LStorage.SetBlockChainFile(AnsiString(FDirectory + PathDelim + 'Data' + + PathDelim + 'BlockChainStream.blocks')); + Require(LStorage.Initialize, 'Cannot reopen the actual stored blocks read-only'); + Require((LStorage.FirstBlock=0) and (LStorage.LastBlock=FExpected-1), + 'Stored terminal height includes missing or extra atomic blocks'); + for I := 0 to FExpected-1 do begin + Require(LStorage.LoadBlockChainBlock(LBlock, I), 'A committed buffered block was not stored'); + LExpected.Clear; LActual.Clear; + Require(FBlocks[I].SaveBlockToStream(False, LExpected), 'Cannot serialize the expected historic block'); + Require(LBlock.SaveBlockToStream(False, LActual), 'Cannot serialize the reopened block'); + Require((LExpected.Size=LActual.Size) and + CompareMem(LExpected.Memory,LActual.Memory,LExpected.Size), + 'A reopened buffered block differs from its exact original network bytes'); + end; + finally LActual.Free; LExpected.Free; LBlock.Free; LBank.Free; end; + Log('REOPENED exact stored blocks=' + IntToStr(FExpected)); +end; + +procedure TFrameCase.CheckBlockLog; +var LLines, LUpdates, LColumns: TStringList; LLine, LPrefix, LMessage: String; + LStream: TFileStream; + I, LMarker, LBatchCount: Integer; LAcceptedAt, LAge: Int64; +begin + LLines := TStringList.Create; + LUpdates := TStringList.Create; + LColumns := TStringList.Create; + try + // The logger deliberately retains its writable stream. Read with shared + // access rather than TStringList.LoadFromFile's writer-denying default. + LStream := TFileStream.Create(FDirectory + PathDelim + 'blocks.log', + fmOpenRead or fmShareDenyNone); + try LLines.LoadFromStream(LStream); finally LStream.Free; end; + LBatchCount := 0; + for I := 0 to LLines.Count-1 do begin + if Pos('Block batch received:',LLines[I])>0 then begin + Inc(LBatchCount); + Require(LUpdates.Count=0,'Block receipt was logged after acceptance'); + LPrefix := Format('[Info] <%s> Block batch received: Count:4 First:0 Last:3 PayloadBytes:%d Peer:%s',[ + FExpectedLogSender,FExpectedBatchBytes,FExpectedLogRemote]); + Require(Pos(LPrefix,LLines[I])>0,'Block receipt lost its count/range/payload/peer facts'); + end; + if Pos(' [Update] ',LLines[I])>0 then LUpdates.Add(LLines[I]); + end; + Require(LBatchCount=1,'Block receipt log was omitted or duplicated'); + Require(LUpdates.Count=FExpected, + 'Block audit log omitted or duplicated an accepted batch/announced block'); + LColumns.Delimiter := ';'; + LColumns.StrictDelimiter := True; + for I := 0 to FExpected-1 do begin + LLine := LUpdates[I]; + Require(Pos('[Update] <' + FExpectedLogSender + '> ',LLine)>0, + 'Block audit log changed the original source class'); + LMarker := Pos('> ',LLine); + LMessage := Copy(LLine,LMarker+2,MaxInt); + LPrefix := Format(';%d;%s;%s;;%d;',[FBlocks[I].OperationBlock.block, + FExpectedLogRemote,FBlocks[I].OperationBlock.block_payload.ToPrintable, + FBlocks[I].OperationBlock.timestamp]); + Require(Copy(LMessage,1,Length(LPrefix))=LPrefix, + 'Block audit log lost the retained header or changed original columns'); + LColumns.DelimitedText := Copy(LMessage,Length(LPrefix)+1,MaxInt); + Require((LColumns.Count=3) and TryStrToInt64(LColumns[0],LAcceptedAt) and + TryStrToInt64(LColumns[1],LAge),'Block audit log has invalid time columns'); + Require(LAge= LAcceptedAt-Int64(FBlocks[I].OperationBlock.timestamp), + 'Block audit log changed its age-at-acceptance column'); + Require(LColumns[2]=IntToHex(FBlocks[I].OperationBlock.compact_target,8), + 'Block audit log changed the original target column'); + end; + finally LColumns.Free; LUpdates.Free; LLines.Free; end; + Log('BLOCK LOG exact accepted entries=' + IntToStr(FExpected)); +end; + +procedure TFrameCase.Run(AScenario: TBufferedFrameScenario); +var LFirst, LSecond, LCombined: TMemoryStream; LIncoming, LParser, LEOF, LShutdown: Boolean; + LUntil: UInt64; LRequestBefore: Cardinal; LStatus: TNodeSynchronizationStatus; + LStoredChecked: Boolean; +begin + LIncoming := AScenario in [bfsIncomingTransportOff, bfsIncomingParserOff, + bfsIncomingEOF, bfsIncomingShutdown,bfsIncomingFastMissing,bfsIncomingBadProtocol]; + LParser := AScenario in [bfsOutgoingParserOff, bfsIncomingParserOff,bfsOutgoingFirstBufferedOff]; + LEOF := AScenario in [bfsOutgoingEOF, bfsIncomingEOF]; + LShutdown := AScenario in [bfsOutgoingShutdown, bfsIncomingShutdown]; + LStoredChecked := False; + FExpected := 5; if LShutdown then FExpected := 1; + FFast:=AScenario in [bfsOutgoingFastComplete,bfsIncomingFastMissing]; + FMissing:=AScenario=bfsIncomingFastMissing; + FMalformed:=AScenario=bfsOutgoingMalformed; + FBadProtocol:=AScenario=bfsIncomingBadProtocol; + FMatched:=AScenario=bfsOutgoingMatchedOff; + if FMissing or FMalformed or FBadProtocol then FExpected:=4; + Open(LIncoming); LoadBlocks; + LFirst := TMemoryStream.Create; LSecond := TMemoryStream.Create; LCombined := TMemoryStream.Create; + try + BuildFrames(LFirst, LSecond); + if FMatched then begin + FMatchedTask:=TMatchedResponseTask.Create(Self); + FMatchedTask.Start; FMatchedTask.FStarted:=True; + // The response is reflected only after this real outstanding request + // crossed the socket. This exercises matching, not a second response. + CaptureFrame(ntp_request,CT_NetOp_GetBlocks,FRequestID,LCombined); + LCombined.Clear; + end; + if LParser then begin + Require(FNet.ConnectionLock(Self, FConnection, 1000), 'Cannot gate the real parser before receipt'); + try + LCombined.CopyFrom(LFirst, 0); LCombined.CopyFrom(LSecond, 0); + SendWire(LCombined); + LUntil := GetTickCount64 + 3000; + repeat Pump; until (BufferedBytes = LCombined.Size) or (GetTickCount64 >= LUntil); + Require(BufferedBytes = LCombined.Size, 'Coalesced frames not fully received before parser release'); + if AScenario=bfsOutgoingFirstBufferedOff then begin + FOff:=TNetworkOff.Create(Self); FOff.Start; FOff.FStarted:=True; + LUntil:=GetTickCount64+1000; + repeat Pump; until not FNet.NetConnectionsActive or (GetTickCount64>=LUntil); + Require(not FNet.NetConnectionsActive,'First-buffer-only disconnect was not published'); + end; + finally FNet.ConnectionUnlock(FConnection); end; + end else SendWire(LFirst); + Await(FFirstCommit, 5000, 'Actual owner did not commit the first response block'); + if not LParser then SendWire(LSecond); + CheckSecondReceived(LFirst.Size + LSecond.Size, LParser); + FObserving := True; + LRequestBefore := FNet.NewRequestId; + if LShutdown then FNet.BeginShutdown; + if LEOF then FPeer.CloseSocket + else if not Assigned(FOff) then begin + FOff := TNetworkOff.Create(Self); FOff.Start; FOff.FStarted := True; + LUntil := GetTickCount64 + 1000; + repeat Pump; until not FNet.NetConnectionsActive or (GetTickCount64 >= LUntil); + Require(not FNet.NetConnectionsActive, 'OFF was not published before draining'); + end; + FRelease.SetEvent; + if Assigned(FOff) then begin + Await(FOff.Done, 5000, 'Network owner did not finish OFF'); + Require(FOff.Error = '', FOff.Error); + end; + if Assigned(FMatchedTask) then begin + Await(FMatchedTask.Done,5000,'The real matching-response owner did not finish'); + Require(FMatchedTask.Error='',FMatchedTask.Error); + end; + if LEOF then begin + LUntil := GetTickCount64+5000; + repeat + Pump; + LStatus := FNode.GetSynchronizationStatus; + if (FNode.Bank.BlocksCount=Cardinal(FExpected)) and + (LStatus.ActiveTransfers=0) and not FNode.IsUpdatingBlockChain then Break; + until GetTickCount64>=LUntil; + Require((FNode.Bank.BlocksCount=Cardinal(FExpected)) and + (FCommits=FExpected) and (LStatus.ActiveTransfers=0) and not FNode.IsUpdatingBlockChain, + 'EOF did not finish the exact received blocks before cleanup'); + CheckStoredBlocks; + LStoredChecked := True; + // Passive outgoing cleanup has an age grace and can wait for discovery. + // AFTER proving the EOF drain, ask the existing owner to close normally; + // never dereference the now-disconnected borrowed connection pointer. + FOff:=TNetworkOff.Create(Self); FOff.Start; FOff.FStarted:=True; + Await(FOff.Done,5000,'EOF cleanup owner did not finish OFF'); + Require(FOff.Error='',FOff.Error); + end; + LUntil := GetTickCount64 + 5000; + repeat + Pump; + LStatus := FNode.GetSynchronizationStatus; + // Peer statistics settle just after the owner's destroyed flag, so wait for both. + if not FNode.IsUpdatingBlockChain and (InterlockedCompareExchange(FDestroyed, 0, 0) <> 0) and + (FNet.NetStatistics.ActiveConnections = 0) and (FNet.NetStatistics.ServersConnections = 0) and + (FNet.NetStatistics.ServersConnectionsWithResponse = 0) then Break; + until GetTickCount64 >= LUntil; + Log(Format('RESULT case=%d committed=%d blocks=%d destroyed=%d active=%d reserved=%d', + [Ord(AScenario), FCommits, FNode.Bank.BlocksCount, FDestroyed, + LStatus.ActiveTransfers, FReservedCalls])); + Require(FNode.Bank.BlocksCount = Cardinal(FExpected), + 'The real owner discarded a complete already-received block frame'); + Require(FCommits = FExpected, 'Unexpected atomic block commit count'); + Require(not FIdleBeforeDrain, 'Node became available between retained block frames'); + Require(FReservedCalls = 0, 'Disconnected drain dispatched a non-block control callback'); + Require(FNet.NewRequestId = LRequestBefore + 1, 'Disconnected drain allocated a new request'); + Require(LStatus.ActiveTransfers = 0, 'Buffered physical scope did not retire'); + Require(not FNode.IsUpdatingBlockChain,'The node still owns a physical block scope after retirement'); + Require(FDestroyed <> 0, 'The completed real transport owner did not retire'); + Require((FNet.NetStatistics.ActiveConnections=0) and + (FNet.NetStatistics.ServersConnections=0) and + (FNet.NetStatistics.ServersConnectionsWithResponse=0), + 'Buffered disconnect resurrected or unbalanced live-peer statistics'); + if not LStoredChecked then CheckStoredBlocks; + CheckBlockLog; + Log('PASS case=' + IntToStr(Ord(AScenario))); + finally LCombined.Free; LSecond.Free; LFirst.Free; end; +end; + +procedure RunBufferedFrameScenario(AScenario: TBufferedFrameScenario); +var LCase: TFrameCase; +begin + if AScenario=bfsReceivePublication then begin + CheckReceivePublication; + Exit; + end; + LCase := TFrameCase.Create; + try LCase.Run(AScenario); finally LCase.Free; end; +end; + +function RunBufferedBlockFrameChecks(const ALogFile: String): Integer; +var LScenario: TBufferedFrameScenario; LFailed: Boolean; +begin + Result := 1; + Require(not FileExists(ALogFile), 'Refusing an existing frame probe log'); + GLogFile := ALogFile; GLog := TStringList.Create; + try + LFailed := False; + for LScenario := Low(TBufferedFrameScenario) to High(TBufferedFrameScenario) do + try RunBufferedFrameScenario(LScenario); + except on E: Exception do begin + LFailed := True; + Log('FAIL case=' + IntToStr(Ord(LScenario)) + ' ' + E.ClassName + ': ' + E.Message); + end; end; + if not LFailed then Result := 0; + finally GLog.Free; GLog := nil; end; +end; + +end. diff --git a/src/tests/experimental/native/network-backlog/UWalletBufferedBlockChecks.pas b/src/tests/experimental/native/network-backlog/UWalletBufferedBlockChecks.pas new file mode 100644 index 000000000..6e0d14538 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/UWalletBufferedBlockChecks.pas @@ -0,0 +1,1392 @@ +unit UWalletBufferedBlockChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Receives an immutable historic batch through the actual network processor. + Only the notification after each completed bank commit is gated for observation. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunBufferedBlockBacklogProbe: Integer; +function RunOutgoingBufferedBlockProbe: Integer; +function RunIncomingBufferedBlockProbe: Integer; +function RunOutgoingShutdownProbe: Integer; +function RunIncomingShutdownProbe: Integer; +function RunIncomingSettingsProbe: Integer; +function RunConnectedMinerProbe: Integer; +{$IFDEF LARGE_BACKLOG_PROBE} +function RunLargeBufferedBlockProbe: Integer; +{$ENDIF} + +implementation + +uses Windows, Classes, SysUtils, SyncObjs, Forms, StdCtrls, Controls, ExtCtrls, Types, Generics.Collections, + blcksock, UNode, UNetProtocol, UThread, UBlockChain, UAccounts, UPCDataTypes, + UFileStorage, UCrypto, UConst, UWallet, UUserInterface, UWalletStartup, + UFRMMainForm, UCTRLWallet, UCTRLSyncronization, UAppParams, UCommon, + USettings, UMemory, UVisualGrid, UCommon.UI, UTime, UPoolMining, UJSONFunctions, ULog + {$IFDEF LARGE_BACKLOG_PROBE}, UDataSources, UCommon.Data, UOrderedList, UBaseTypes{$ENDIF}; + +const + CHistoricCount = 5; + CHistoricTimes: array[0..CHistoricCount-1] of Cardinal = + (1470934813,1470935312,1470935390,1470935433,1470935652); + +type + TWalletAccess = class(TCTRLWallet) + public + property AccountsGrid; + property OperationsGrid; + end; + + TMainAccess = class(TFRMMainForm) + public + property WalletControl; + end; + + TBufferedResponseConnection = class(TNetConnection) + public + procedure ProcessReceivedBatch(AData: TStream); + function CallerProcessingTerminated: Boolean; + end; + + TThreadAccess = class(TThread) + public + property Terminated; + end; + + TCommitObservation = class(TComponent) + private + FBankNotify: TPCBankNotify; + FReleasing: LongInt; + FOfflineAtCommit: Boolean; + procedure OnCommitted(Sender: TObject); + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + CommitCount, ConnectionDestroyed: LongInt; + FirstOffRequestID: Cardinal; + ProcessingThread: TThread; + CheckDeferredOperationsAtCommit: Boolean; + FirstCommitTick: UInt64; + Committed, ContinueBatch: TEvent; + constructor Create(AOfflineAtCommit: Boolean); + destructor Destroy; override; + procedure ReleaseAll; + end; + + TDelayedGateRelease = class(TPCThread) + private + FObservation: TCommitObservation; + FWaitForCommit: Boolean; + protected + procedure BCExecute; override; + public + constructor Create(AObservation: TCommitObservation; AWaitForCommit: Boolean = False); + end; + + TReceivedBatchTask = class(TPCThread) + private + FConnection: TBufferedResponseConnection; + FData: TStream; + FStarted: Boolean; + protected + procedure BCExecute; override; + public + ErrorMessage: String; + Completed: TEvent; + constructor Create(AConnection: TBufferedResponseConnection; AData: TStream); + destructor Destroy; override; + procedure StartOwned; + end; + + TBufferedBlockChecks = class + private + FMain: TFRMMainForm; + FDialog: TForm; + FConnection: TNetConnection; + FOutgoingOwner: Boolean; + FIncomingOwner: Boolean; + FShutdownAfterFirst: Boolean; + FExerciseSettings: Boolean; + FSynchronizationLifecycle: Boolean; + FConnectedMiner, FMonitorMinerNotifications: Boolean; + FMiner: TPoolMinerClient; + FMinerPort: Word; + FMinerObserver: TNodeNotifyEvents; + FExternalRelease: TDelayedGateRelease; + FBlockNotificationTick, FOperationNotificationTick, FMaximumPump: UInt64; + FBlockWhileWriterHeld, FOperationsWhileWriterHeld: Boolean; + FExpectedMiningKey: TAccountKey; + FMiningWalletKeyCount: Integer; + FObservation: TCommitObservation; + FListener, FAccepted: TTCPBlockSocket; + FTask: TReceivedBatchTask; + FData: TResourceStream; + FLog: TStringList; + FLogPath, FError, FDataDirectory: String; + FPreviousException: TExceptionEvent; + FDiagnosticLog: TLog; + procedure Log(const AText: String); + procedure OnException(Sender: TObject; E: Exception); + procedure Pump; + procedure WaitCommit(AExpected: Integer); + procedure CheckOnlineCallerRequest; + procedure ReflectBufferedResponse; + procedure CheckOwnerWhileGated; + procedure OpenIncomingConnection; + procedure CheckIncomingNetworkOff; + procedure ConfigureMiningFixture; + procedure ConnectControlledMiner; + procedure QueueMinerOperationsNotification; + procedure AwaitMinerBlockJob(AExpectedBlock: Cardinal); + procedure OnMinerBlockNotification(Sender: TObject); + procedure OnMinerOperationsNotification(Sender: TObject); + function ActualWriterHeld: Boolean; + procedure CheckExplicitShutdown; + procedure CheckCommittedDisplay(AExpected: Integer); + procedure CheckNativeSynchronizationCards(const ACheckpoint: String); + procedure CheckSynchronizationResponses; + procedure CheckNoNewRequests; + procedure CheckReopenedStorage(AExpected: Integer); + function Wallet: TWalletAccess; + public + constructor Create(const ALogPath: String; AOutgoingOwner, AIncomingOwner, AShutdownAfterFirst, AExerciseSettings, AConnectedMiner: Boolean; ASynchronizationLifecycle: Boolean = False); + destructor Destroy; override; + procedure Run; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure TBufferedResponseConnection.ProcessReceivedBatch(AData: TStream); +var LHeader: TNetHeaderData; +begin + LHeader := Default(TNetHeaderData); + LHeader.header_type := ntp_response; + LHeader.operation := CT_NetOp_GetBlocks; + AData.Position := 0; + // Inherited decoding, PoW/signature validation, atomic commits and cleanup. + DoProcess_GetBlocks_Response(LHeader,AData); +end; + +function TBufferedResponseConnection.CallerProcessingTerminated: Boolean; +begin + Result := BlockProcessingTerminated; +end; + +constructor TCommitObservation.Create(AOfflineAtCommit: Boolean); +begin + inherited Create(nil); + FOfflineAtCommit := AOfflineAtCommit; + Committed := TEvent.Create(nil,True,False,''); + ContinueBatch := TEvent.Create(nil,False,False,''); + FBankNotify := TPCBankNotify.Create(Self); + FBankNotify.Bank := TNode.Node.Bank; + FBankNotify.OnNewBlock := OnCommitted; +end; + +destructor TCommitObservation.Destroy; +begin + ReleaseAll; + FBankNotify.Free; + ContinueBatch.Free; + Committed.Free; + inherited; +end; + +procedure TCommitObservation.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited; + if (Operation = opRemove) and (AComponent is TNetConnection) then + InterlockedExchange(ConnectionDestroyed,1); +end; + +procedure TCommitObservation.ReleaseAll; +begin + InterlockedExchange(FReleasing,1); + ContinueBatch.SetEvent; +end; + +procedure TCommitObservation.OnCommitted(Sender: TObject); +var LCount: Integer; LDeadline: UInt64; LOperations: TOperationsHashTree; LError: String; +begin + ProcessingThread := TThread.CurrentThread; + LCount := InterlockedIncrement(CommitCount); + if LCount = 1 then begin + FirstCommitTick := GetTickCount64; + if CheckDeferredOperationsAtCommit then begin + // Catch-up intentionally rejects even empty pending admission. Its real + // operations notification was queued before the received batch started. + LOperations := TOperationsHashTree.Create; + try + Require(TNode.Node.AddOperations(nil,LOperations,nil,LError) = -1, + 'Pending admission must remain unavailable during received-block processing'); + Require(LError = 'Pending operations are unavailable during blockchain synchronization', + 'Pending admission must report the deferred-state reason'); + finally LOperations.Free; end; + end; + // Exercise the real switch from its actual bank-commit callback. The + // outgoing destroy worker remains free to run; this fixture holds no net lock. + if FOfflineAtCommit then TNetData.NetData.NetConnectionsActive := False; + FirstOffRequestID := TNetData.NetData.NewRequestId; + end; + if InterlockedCompareExchange(FReleasing,0,0) <> 0 then Exit; + Committed.SetEvent; + LDeadline := GetTickCount64+10000; + while ContinueBatch.WaitFor(10) <> wrSignaled do begin + if InterlockedCompareExchange(FReleasing,0,0) <> 0 then Exit; + if GetTickCount64 >= LDeadline then + raise Exception.Create('The main-thread committed-snapshot check did not release its gate'); + end; + Committed.ResetEvent; +end; + +constructor TDelayedGateRelease.Create(AObservation: TCommitObservation; AWaitForCommit: Boolean); +begin + inherited Create(True); + FreeOnTerminate := False; + FObservation := AObservation; + FWaitForCommit := AWaitForCommit; +end; + +procedure TDelayedGateRelease.BCExecute; +begin + // Bound even the old synchronous listener join without forcing any thread. + if FWaitForCommit then + while FObservation.Committed.WaitFor(10) <> wrSignaled do + if Terminated then Exit; + if Terminated then Exit; + Sleep(1200); + FObservation.ReleaseAll; +end; + +constructor TReceivedBatchTask.Create(AConnection: TBufferedResponseConnection; AData: TStream); +begin + inherited Create(True); + FreeOnTerminate := False; + FConnection := AConnection; + FData := AData; + Completed := TEvent.Create(nil,True,False,''); +end; + +destructor TReceivedBatchTask.Destroy; +begin + Terminate; + if not FStarted then StartOwned; + WaitFor; + Completed.Free; + inherited; +end; + +procedure TReceivedBatchTask.StartOwned; +begin + FStarted := True; + Start; +end; + +procedure TReceivedBatchTask.BCExecute; +begin + try + try + if not Terminated then FConnection.ProcessReceivedBatch(FData); + except + on E: Exception do ErrorMessage := E.ClassName+': '+E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TBufferedBlockChecks.Create(const ALogPath: String; AOutgoingOwner, AIncomingOwner, AShutdownAfterFirst, AExerciseSettings, AConnectedMiner: Boolean; ASynchronizationLifecycle: Boolean); +begin + inherited Create; + FLogPath := ALogPath; + FOutgoingOwner := AOutgoingOwner; + FIncomingOwner := AIncomingOwner; + FShutdownAfterFirst := AShutdownAfterFirst; + FExerciseSettings := AExerciseSettings; + FConnectedMiner := AConnectedMiner; + // Only the multi-case BufferedBlockBacklogProbe reads PASCALCOIN_BACKLOG_CASE; + // dedicated probes keep their own flow even when the variable is left set. + FSynchronizationLifecycle := ASynchronizationLifecycle; + FLog := TStringList.Create; + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TBufferedBlockChecks.Destroy; +begin + // Join the actual processor before its connection, data, node or forms die. + FMonitorMinerNotifications := False; + if Assigned(FObservation) then FObservation.ReleaseAll; + if Assigned(FExternalRelease) then begin + FExternalRelease.Terminate; + FExternalRelease.WaitFor; + FExternalRelease.Free; + end; + FMinerObserver.Free; + FMiner.Free; + FTask.Free; + if not (FOutgoingOwner or FIncomingOwner) then FConnection.Free; + FAccepted.Free; + FListener.Free; + FData.Free; + if Assigned(FMain) then TUserInterface.ExitApplication; + FObservation.Free; + FDiagnosticLog.Free; + Application.OnException := FPreviousException; + FLog.Free; + inherited; +end; + +procedure TBufferedBlockChecks.Log(const AText: String); +begin + FLog.Add(UIntToStr(GetTickCount64)+' '+AText); + FLog.SaveToFile(FLogPath); +end; + +procedure TBufferedBlockChecks.OnException(Sender: TObject; E: Exception); +begin + if FError = '' then FError := E.ClassName+': '+E.Message; +end; + +procedure TBufferedBlockChecks.Pump; +var LStarted, LElapsed: UInt64; +begin + LStarted := GetTickCount64; + Application.ProcessMessages; + CheckSynchronize(0); + LElapsed := GetTickCount64-LStarted; + if FMonitorMinerNotifications then begin + if LElapsed > FMaximumPump then FMaximumPump := LElapsed; + if LElapsed >= 400 then begin + Log('MINER actual UI dispatch duration='+UIntToStr(LElapsed)+'ms'); + Require(False,'An actual connected-miner node callback blocked the UI for >=400ms'); + end; + end; + Require(FError = '',FError); +end; + +function TBufferedBlockChecks.Wallet: TWalletAccess; +begin + Result := TWalletAccess(TMainAccess(FMain).WalletControl); +end; + +procedure TBufferedBlockChecks.WaitCommit(AExpected: Integer); +var LDeadline: UInt64; +begin + LDeadline := GetTickCount64+10000; + repeat + Pump; + if Assigned(FTask) then Require(FTask.ErrorMessage = '',FTask.ErrorMessage); + if (InterlockedCompareExchange(FObservation.CommitCount,0,0) = AExpected) and + (FObservation.Committed.WaitFor(0) = wrSignaled) then Exit; + if Assigned(FTask) and (FTask.Completed.WaitFor(0) = wrSignaled) then Break; + if InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0 then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + raise Exception.CreateFmt('Buffered processor did not commit historic block %d',[AExpected-1]); +end; + +procedure TBufferedBlockChecks.CheckCommittedDisplay(AExpected: Integer); +var LDeadline: UInt64; S, LDisplayed: TNodeSynchronizationStatus; LFooter, LTime: String; +begin + LDeadline := GetTickCount64+3000; + repeat + Pump; + S := TUserInterface.Node.GetSynchronizationStatus; + LDisplayed := FMain.SyncControl.DisplayedSynchronizationStatus; + if (LDisplayed.LocalBlockCount = Cardinal(AExpected)) and + (LDisplayed.State = nssSynchronizing) and (LDisplayed.NetworkActive=FSynchronizationLifecycle) and + (Pos('Blocks: '+IntToStr(AExpected)+' |',FMain.sbStatusBar.Panels[2].Text) = 1) and + Wallet.AccountsGrid.RefreshSuspended and Wallet.OperationsGrid.RefreshSuspended and + Wallet.AccountsGrid.LoadingVisible and Wallet.OperationsGrid.LoadingVisible then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(TUserInterface.Node.Bank.BlocksCount = Cardinal(AExpected),'Actual bank count differs from completed commits'); + Require((S.State = nssSynchronizing) and (S.ActiveTransfers > 0) and + (S.NetworkActive=FSynchronizationLifecycle),'Received work did not retain the expected node synchronization state'); + Require((S.LocalBlockCount = Cardinal(AExpected)) and + (S.LastBlockTimestamp = CHistoricTimes[AExpected-1]), + 'The node cached metadata must describe the completed bank commit'); + Require((LDisplayed.State = nssSynchronizing) and + (LDisplayed.LocalBlockCount = S.LocalBlockCount) and + (LDisplayed.LastBlockTimestamp = S.LastBlockTimestamp), + 'The actual sync view did not render the current cached committed record'); + Require(FMain.SyncControl.lblTotalBlocksValue.Caption = + IntToStr(AExpected)+' (0..'+IntToStr(AExpected-1)+')', + 'The basic sync block label disagrees with its displayed record'); + LFooter := FMain.sbStatusBar.Panels[2].Text; + LTime := FormatDateTime('dd/mm/yyyy hh:nn:ss', + UnivDateTime2LocalDateTime(UnixToUnivDateTime(CHistoricTimes[AExpected-1]))); + Require((LFooter = FMain.SyncControl.ChainStatusText) and + (Pos('Blocks: '+IntToStr(AExpected)+' |',LFooter) = 1) and + (Pos(LTime,LFooter) > 0) and (FSynchronizationLifecycle or (Pos('Network off',LFooter) > 0)), + 'Footer count/time/network policy must use the same committed record as sync labels'); + Require(Wallet.AccountsGrid.RefreshSuspended and Wallet.OperationsGrid.RefreshSuspended and + Wallet.AccountsGrid.LoadingVisible and Wallet.OperationsGrid.LoadingVisible, + Format('Both actual main grids must remain suspended/covered until backlog drains: paused=%s/%s cards=%s/%s', + [BoolToStr(Wallet.AccountsGrid.RefreshSuspended,True),BoolToStr(Wallet.OperationsGrid.RefreshSuspended,True), + BoolToStr(Wallet.AccountsGrid.LoadingVisible,True),BoolToStr(Wallet.OperationsGrid.LoadingVisible,True)])); + CheckNativeSynchronizationCards('committed-'+IntToStr(AExpected)); + Require(GetActiveWindow = FDialog.Handle,'A committed-block update raised the main form over its modeless dialog'); + Log('COMMITTED '+IntToStr(AExpected)+' synchronizing networkActive='+BoolToStr(S.NetworkActive,True)+'; '+LFooter); +end; + +procedure TBufferedBlockChecks.CheckNativeSynchronizationCards(const ACheckpoint: String); +var LGrids: array[0..1] of TVisualGrid; LPanel: TPanel; LCircle: TLoadingCircle; + LTitle, LSubtitle: TLabel; LBounds, LClient: TRect; I: Integer; +begin + LGrids[0] := Wallet.AccountsGrid; + LGrids[1] := Wallet.OperationsGrid; + for I := Low(LGrids) to High(LGrids) do begin + LPanel := LGrids[I].FindComponent('LoadingOverlay') as TPanel; + LCircle := LGrids[I].FindComponent('LoadingCircle') as TLoadingCircle; + LTitle := LGrids[I].FindComponent('LoadingStatus') as TLabel; + LSubtitle := LGrids[I].FindComponent('LoadingSubtitle') as TLabel; + Require(LGrids[I].RefreshSuspended and LGrids[I].LoadingVisible, + ACheckpoint+': real GET_BLOCKS lost its grid suspension/card'); + Require(LPanel.Visible and LPanel.HandleAllocated and IsWindowVisible(LPanel.Handle), + ACheckpoint+': manual flag did not produce a visible native overlay'); + Require(LCircle.Active and LCircle.Visible and LCircle.HandleAllocated and IsWindowVisible(LCircle.Handle), + ACheckpoint+': the native synchronization spinner is hidden'); + Require(LTitle.Visible and (LTitle.Caption='Please Wait') and LSubtitle.Visible and + (LSubtitle.Caption='Synchronizing'),ACheckpoint+': synchronization text was replaced by ordinary loading'); + GetWindowRect(LPanel.Handle,LBounds); + GetClientRect(LPanel.Parent.Handle,LClient); + MapWindowPoints(LPanel.Parent.Handle,0,LClient,2); + Require((LBounds.Right>LBounds.Left) and (LBounds.Bottom>LBounds.Top) and + (LBounds.Left>=LClient.Left) and (LBounds.Top>=LClient.Top) and + (LBounds.Right<=LClient.Right) and (LBounds.Bottom<=LClient.Bottom), + ACheckpoint+': native synchronization overlay lies outside its grid client'); + end; +end; + +procedure TBufferedBlockChecks.CheckSynchronizationResponses; +var LFirst, LSecond: TMemoryStream; LBlock: TPCOperationsComp; + LSplit: Int64; LCount: Cardinal; LDeadline: UInt64; LStatus: TNodeSynchronizationStatus; + LBag: TDisposables; LError: String; I: Integer; + + procedure AwaitBatch; + var LUntil: UInt64; + begin + LUntil := GetTickCount64+5000; + repeat + Pump; + Require(FTask.ErrorMessage='',FTask.ErrorMessage); + if FTask.Completed.WaitFor(0)=wrSignaled then Break; + Sleep(1); + until GetTickCount64>=LUntil; + Require(FTask.Completed.WaitFor(0)=wrSignaled,'The actual GET_BLOCKS response did not finish'); + FreeAndNil(FTask); + end; + +begin + // Split the immutable resource at a decoder-confirmed block boundary. Both + // responses retain the exact historic block bytes and production validation. + LFirst := LBag.AddObject(TMemoryStream.Create) as TMemoryStream; + LSecond := LBag.AddObject(TMemoryStream.Create) as TMemoryStream; + LBlock := LBag.AddObject(TPCOperationsComp.Create(TUserInterface.Node.Bank)) as TPCOperationsComp; + FData.Position := SizeOf(Cardinal); + for I := 0 to 1 do + Require(LBlock.LoadBlockFromStream(FData,LError),LError); + LSplit := FData.Position; + LCount := 2; + LFirst.WriteBuffer(LCount,SizeOf(LCount)); + FData.Position := SizeOf(Cardinal); + LFirst.CopyFrom(FData,LSplit-FData.Position); + LCount := CHistoricCount-2; + LSecond.WriteBuffer(LCount,SizeOf(LCount)); + FData.Position := LSplit; + LSecond.CopyFrom(FData,FData.Size-FData.Position); + LBag.RemoveObject(LBlock); + FreeAndNil(LBlock); + // Select this actual connected loopback peer through the same public node + // API used by Send_GetBlocks; never synthesize a synchronization state. + TUserInterface.Node.SetSynchronizationTarget(FConnection,CHistoricCount); + try + FTask := TReceivedBatchTask.Create(TBufferedResponseConnection(FConnection),LFirst); + FTask.StartOwned; + for I := 1 to 2 do begin + WaitCommit(I); + CheckCommittedDisplay(I); + FObservation.ContinueBatch.SetEvent; + end; + AwaitBatch; + LStatus := TUserInterface.Node.GetSynchronizationStatus; + Require((LStatus.State=nssSynchronizing) and LStatus.NetworkActive and + (LStatus.ActiveTransfers=0) and (LStatus.LocalBlockCount=2) and + (LStatus.TargetBlockCount=CHistoricCount),'The selected real peer target did not retain synchronization between responses'); + LDeadline := GetTickCount64+6200; + repeat + Pump; + CheckNativeSynchronizationCards('real-response-gap'); + Sleep(20); + until GetTickCount64>=LDeadline; + Log('PASS actual GET_BLOCKS gap: local2/target5, zero active transfers, both native Synchronizing cards retained for6.2s'); + FTask := TReceivedBatchTask.Create(TBufferedResponseConnection(FConnection),LSecond); + FTask.StartOwned; + for I := 3 to CHistoricCount do begin + WaitCommit(I); + CheckCommittedDisplay(I); + FObservation.ContinueBatch.SetEvent; + end; + AwaitBatch; + LDeadline := GetTickCount64+5000; + repeat + Pump; + LStatus := TUserInterface.Node.GetSynchronizationStatus; + if (LStatus.State=nssIdle) and not Wallet.AccountsGrid.RefreshSuspended and + not Wallet.OperationsGrid.RefreshSuspended and not Wallet.AccountsGrid.IsUpdating and + not Wallet.OperationsGrid.IsUpdating and not Wallet.OperationsGrid.RefreshThrottle.Waiting then Break; + Sleep(1); + until GetTickCount64>=LDeadline; + Require((LStatus.State=nssIdle) and (LStatus.LocalBlockCount=CHistoricCount), + 'The actual final GET_BLOCKS response did not complete the selected target'); + Require(not Wallet.AccountsGrid.RefreshSuspended and not Wallet.OperationsGrid.RefreshSuspended and + not (Wallet.AccountsGrid.FindComponent('LoadingOverlay') as TPanel).Visible and + not (Wallet.OperationsGrid.FindComponent('LoadingOverlay') as TPanel).Visible, + 'Completed real GET_BLOCKS synchronization retained a suspended grid or overlay'); + Log('PASS actual GET_BLOCKS target completion: local5/target5, idle, both grids resumed and native cards removed'); + finally + FObservation.ReleaseAll; + FreeAndNil(FTask); + end; + FreeAndNil(FConnection); + FreeAndNil(FAccepted); + FreeAndNil(FListener); + TUserInterface.ExitApplication; + FMain := nil; + FDialog := nil; + CheckReopenedStorage(CHistoricCount); +end; + +procedure TBufferedBlockChecks.CheckNoNewRequests; +var LRequestID, LBefore, LAfter: Cardinal; LHeader: TNetHeaderData; + LSend, LReceive: TMemoryStream; LDeadline: UInt64; +begin + Require(not TBufferedResponseConnection(FConnection).CallerProcessingTerminated, + 'An external/main caller must not be treated as a terminated processing thread'); + LAfter := TNetData.NetData.NewRequestId; + Require(LAfter = FObservation.FirstOffRequestID+1,'A new network request was allocated after network-off while the backlog drained'); + LBefore := TNetData.NetData.NewRequestId; + Require(not FConnection.ConnectTo('127.0.0.1',FListener.GetLocalSinPort), + 'Network-off must reject a new connection attempt'); + LRequestID := 0; + Require(not FConnection.Send_GetBlocks(CHistoricCount,5,LRequestID), + 'Network-off must reject a new block request'); + LSend := TMemoryStream.Create; + LReceive := TMemoryStream.Create; + try + LHeader := Default(TNetHeaderData); + LDeadline := GetTickCount64; + Require(not FConnection.DoSendAndWaitForResponse(CT_NetOp_GetBlocks,0, + LSend,LReceive,1000,LHeader),'Network-off must reject a nested request/wait'); + Require(GetTickCount64-LDeadline < 500,'A denied request must not wait for a network timeout'); + finally + LReceive.Free; + LSend.Free; + end; + LAfter := TNetData.NetData.NewRequestId; + Require(LAfter = LBefore+1,'Denied requests must not allocate fresh request IDs'); + Log('PASS already received blocks finished; new connect/GetBlocks/nested wait requests rejected'); +end; + +procedure TBufferedBlockChecks.CheckOnlineCallerRequest; +var LSend, LReceive, LWire: TMemoryStream; LHeader: TNetHeaderData; + LStart, LEnd, LRequestID: Cardinal; LDeadline: UInt64; LBytesBefore, LReadAt: Int64; + LPacket: RawByteString; LNeedMore, LFound: Boolean; +begin + {$IFNDEF FPC} + Require(TThread.CurrentThread.ExternalThread, + 'The native main-thread regression must exercise the RTL external-thread path'); + {$ENDIF} + Require(TNetData.NetData.NetConnectionsActive and + not TBufferedResponseConnection(FConnection).CallerProcessingTerminated, + 'An online external/main caller must not raise or report processing cancellation'); + LDeadline := GetTickCount64+1000; + repeat Pump; Sleep(1); until (FConnection.Client.BytesSent > 0) or (GetTickCount64 >= LDeadline); + Require(FConnection.Client.BytesSent > 0,'The initial real loopback HELLO was not sent'); + LBytesBefore := FConnection.Client.BytesSent; + LSend := TMemoryStream.Create; + LReceive := TMemoryStream.Create; + LWire := TMemoryStream.Create; + try + LStart := 0; + LEnd := CHistoricCount-1; + LSend.WriteBuffer(LStart,SizeOf(LStart)); + LSend.WriteBuffer(LEnd,SizeOf(LEnd)); + LRequestID := TNetData.NetData.NewRequestId; + LHeader := Default(TNetHeaderData); + // The raw owned peer intentionally has no response yet. A zero-timeout + // public request still exercises actual online admission and transmission. + Require(not FConnection.DoSendAndWaitForResponse(CT_NetOp_GetBlocks, + LRequestID,LSend,LReceive,0,LHeader),'The raw peer unexpectedly replied'); + LFound := False; + LDeadline := GetTickCount64+1000; + repeat + Pump; + if FAccepted.CanRead(10) then begin + LPacket := FAccepted.RecvPacket(10); + if Length(LPacket) > 0 then begin + LReadAt := LWire.Position; + LWire.Position := LWire.Size; + LWire.WriteBuffer(LPacket[1],Length(LPacket)); + LWire.Position := LReadAt; + // Decode the real loopback bytes with the production framing parser. + // This skips the initial HELLO and identifies our exact request ID. + while TNetData.ExtractHeaderInfo(LWire,LHeader,LReceive,LNeedMore) do + if (LHeader.header_type = ntp_request) and + (LHeader.operation = CT_NetOp_GetBlocks) and + (LHeader.request_id = LRequestID) then begin + Require(LReceive.Size = 2*SizeOf(Cardinal),'The real GetBlocks payload has the wrong size'); + LReceive.ReadBuffer(LStart,SizeOf(LStart)); + LReceive.ReadBuffer(LEnd,SizeOf(LEnd)); + Require((LStart = 0) and (LEnd = CHistoricCount-1), + 'The real loopback request did not carry our block range'); + LFound := True; + Break; + end; + end; + end; + until LFound or (GetTickCount64 >= LDeadline); + Require(LFound,'The owned peer did not receive the exact online GetBlocks frame/request ID'); + finally + LWire.Free; + LReceive.Free; + LSend.Free; + end; + LDeadline := GetTickCount64+1000; + repeat Pump; Sleep(1); until (FConnection.Client.BytesSent > LBytesBefore) or (GetTickCount64 >= LDeadline); + Require(FConnection.Client.BytesSent > LBytesBefore, + 'The actual online public request was not transmitted to its owned loopback peer'); + Log('PASS main/external caller transmitted a real loopback block request while online'); +end; + +procedure TBufferedBlockChecks.ReflectBufferedResponse; +var LWire, LPayload: TMemoryStream; LHeader: TNetHeaderData; + LPacket: RawByteString; LDeadline: UInt64; LAt: Int64; + LNeedMore, LFound: Boolean; +const CResponseID = 7654321; +begin + LWire := TMemoryStream.Create; + LPayload := TMemoryStream.Create; + try + FData.Position := 0; + // Serialize with the production sender, then reflect the exact frame over + // the real socket. Only the client's owned thread decodes/processes it. + FConnection.DoSend(ntp_response,CT_NetOp_GetBlocks,0,CResponseID,FData); + LFound := False; + LDeadline := GetTickCount64+3000; + repeat + Pump; + if FAccepted.CanRead(10) then begin + LPacket := FAccepted.RecvPacket(10); + if Length(LPacket) > 0 then begin + LAt := LWire.Position; + LWire.Position := LWire.Size; + LWire.WriteBuffer(LPacket[1],Length(LPacket)); + LWire.Position := LAt; + repeat + LAt := LWire.Position; + if not TNetData.ExtractHeaderInfo(LWire,LHeader,LPayload,LNeedMore) then Break; + if (LHeader.header_type = ntp_response) and + (LHeader.operation = CT_NetOp_GetBlocks) and + (LHeader.request_id = CResponseID) then begin + Require(LPayload.Size = FData.Size,'The real response changed the historic payload'); + if FConnectedMiner then QueueMinerOperationsNotification; + Require(FAccepted.SendBuffer(PByte(LWire.Memory)+LAt, + Integer(LWire.Position-LAt)) = Integer(LWire.Position-LAt), + 'The raw loopback peer could not send the complete real response frame'); + if FConnectedMiner then + // Do not pump the queued notification until the real bank + // callback holds its writer; no producer waits for the UI. + Require(FObservation.Committed.WaitFor(3000) = wrSignaled, + 'The connected-miner processor did not reach its committed writer gate'); + LFound := True; + Break; + end; + until False; + end; + end; + until LFound or (GetTickCount64 >= LDeadline); + Require(LFound,'The actual client did not serialize the historic response'); + finally + LPayload.Free; + LWire.Free; + end; + Log('PASS reflected exact production GetBlocks response into actual socket-owned processor'); +end; + +procedure TBufferedBlockChecks.CheckOwnerWhileGated; +var LDeadline: UInt64; LTerminated: Boolean; +begin + Require(Assigned(FObservation.ProcessingThread),'The actual processor thread was not captured'); + Require(FObservation.ProcessingThread is TNetClientThread, + 'The received batch must execute on the actual client-owned thread'); + LDeadline := GetTickCount64+1200; + repeat Pump; Sleep(1); until GetTickCount64 >= LDeadline; + // The processor is alive inside OnCommitted throughout this read. A real + // destroyer may be joining it, but cannot finish freeing it before release. + LTerminated := TThreadAccess(FObservation.ProcessingThread).Terminated; + Log('OWNER gate after1200ms terminated='+BoolToStr(LTerminated,True)+ + ' destroyed='+IntToStr(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0))+ + ' committed='+IntToStr(InterlockedCompareExchange(FObservation.CommitCount,0,0))); + Require(not LTerminated, + 'Real outgoing connection cleanup terminated its owned processor during a received batch'); + Require(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) = 0, + 'The real outgoing connection was destroyed before its buffered work drained'); +end; + +procedure TBufferedBlockChecks.OpenIncomingConnection; +var LPort: Word; LDeadline: UInt64; LConnections: TList<TNetConnection>; + LConnection: TNetConnection; +begin + // Ask the OS for a currently unused loopback port; never use the user's port. + FListener := TTCPBlockSocket.Create; + FListener.CreateSocket; + FListener.Bind('127.0.0.1','0'); + Require(FListener.LastError = 0,'Cannot reserve an owned listener port'); + LPort := FListener.GetLocalSinPort; + FListener.CloseSocket; + // Use the real settings event so the UI's desired port and the actual + // serialized listener owner agree before a later toolbar request. + TSettings.InternetServerPort := LPort; + TSettings.Save; + LDeadline := GetTickCount64+3000; + repeat + Pump; + if TUserInterface.Node.NetServer.Active and + (TUserInterface.Node.NetServer.Port = LPort) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(TUserInterface.Node.NetServer.Active and + (TUserInterface.Node.NetServer.Port = LPort),'Owned listener settings were not applied'); + FAccepted := TTCPBlockSocket.Create; + FAccepted.Connect('127.0.0.1',IntToStr(LPort)); + Require(FAccepted.LastError = 0,'Cannot connect the raw peer to the actual listener'); + LDeadline := GetTickCount64+3000; + repeat + Pump; + LConnections := TNetData.NetData.NetConnections.LockList; + try + for LConnection in LConnections do + if LConnection is TNetServerClient then begin + FConnection := LConnection; + FConnection.FreeNotification(FObservation); + Break; + end; + finally + TNetData.NetData.NetConnections.UnlockList; + end; + if Assigned(FConnection) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(Assigned(FConnection),'The actual listener did not create its server connection'); +end; + +procedure TBufferedBlockChecks.ConfigureMiningFixture; +var LDeadline: UInt64; LOperations: TPCOperationsComp; LApplied: Boolean; + LPortSocket: TTCPBlockSocket; +begin + // This secret belongs only to the fresh disposable wallet. Ordinary settings + // must reuse its eligible reward key even in NewEachTime mode; no mined-block + // callback is being simulated by these port/name changes. + TWallet.GenerateNewKey('Disposable buffered-settings mining key',CT_Default_EC_OpenSSL_NID); + TSettings.MinerPrivateKeyType := mpk_NewEachTime; + LPortSocket := TTCPBlockSocket.Create; + try + LPortSocket.CreateSocket; + LPortSocket.Bind('127.0.0.1','0'); + Require(LPortSocket.LastError = 0,'Cannot reserve the owned mining-listener port'); + FMinerPort := LPortSocket.GetLocalSinPort; + finally LPortSocket.Free; end; + TSettings.JsonRpcMinerServerPort := FMinerPort; + TSettings.JsonRpcMinerServerActive := True; + TSettings.MinerName := 'Initial buffered mining payload'; + FExpectedMiningKey := TWallet.MiningKey; + FMiningWalletKeyCount := TWallet.Keys.Count; + TSettings.Save; + LApplied := False; + LDeadline := GetTickCount64+3000; + repeat + Pump; + if TUserInterface.PoolMiningServer.Active and + TUserInterface.Node.TryLockMempoolRead(0,LOperations) then begin + try + LApplied := TAccountComp.EqualAccountKeys(LOperations.AccountKey,FExpectedMiningKey); + finally + TUserInterface.Node.UnlockMempoolRead; + end; + end; + if LApplied then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(LApplied,'The actual configured mining reward key was not applied'); + Require(TWallet.Keys.Count = FMiningWalletKeyCount, + 'Ordinary mining configuration unexpectedly generated another wallet key'); + Log('MINING configured ON with one disposable eligible key; ordinary NewEachTime rotation forbidden'); +end; + +procedure TBufferedBlockChecks.ConnectControlledMiner; +var LDeadline: UInt64; LJSON: TPCJSONObject; LMethod: String; LFound: Boolean; +begin + FMiner := TPoolMinerClient.Create(nil); + FMiner.RemoteHost := '127.0.0.1'; + FMiner.RemotePort := FMinerPort; + Require(FMiner.Connect,'The controlled miner could not connect to its actual pool listener'); + LJSON := nil; + try + LFound := False; + LDeadline := GetTickCount64+3000; + repeat + Pump; + LMethod := ''; + // Match the production miner loop: Assign appends array items, so every + // received message needs its own object rather than previous job data. + FreeAndNil(LJSON); + LJSON := TPCJSONObject.Create; + if FMiner.DoProcessBuffer(nil,10,False,LMethod,LJSON) and + (LJSON.AsString('method','') = CT_PoolMining_Method_MINER_NOTIFY) then begin + FMiner.DoProcessJSONObject(LJSON,LMethod); + LFound := Length(FMiner.MinerValuesForWork.part1) > 0; + end; + if LFound then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(LFound and (TUserInterface.PoolMiningServer.ClientsCount = 1), + 'The actual connected miner did not consume its production initial job'); + finally LJSON.Free; end; + FMinerObserver := TNodeNotifyEvents.Create(nil); + FMinerObserver.OnBlocksChanged := OnMinerBlockNotification; + FMinerObserver.OnOperationsChanged := OnMinerOperationsNotification; + FMinerObserver.Node := TUserInterface.Node; + Log('MINER actual connected client consumed production miner-notify job block='+ + IntToStr(FMiner.MinerValuesForWork.block)); +end; + +procedure TBufferedBlockChecks.QueueMinerOperationsNotification; +var LOperations: TOperationsHashTree; LError: String; +begin + // Queue through the public producer while pending state is coherent. The UI + // must not dispatch this notification until the received block holds WRITE. + Require(TUserInterface.Node.PendingOperationsAvailable, + 'The controlled operations producer must run before catch-up starts'); + LOperations := TOperationsHashTree.Create; + try + Require(TUserInterface.Node.AddOperations(nil,LOperations,nil,LError) = 0,LError); + finally LOperations.Free; end; + Log('MINER queued actual operations notification before received-block processing'); +end; + +function TBufferedBlockChecks.ActualWriterHeld: Boolean; +var LUnexpectedWrite: Boolean; +begin + LUnexpectedWrite := TUserInterface.Node.TryLockNode(0); + if LUnexpectedWrite then TUserInterface.Node.UnlockNode; + Result := not LUnexpectedWrite; +end; + +procedure TBufferedBlockChecks.AwaitMinerBlockJob(AExpectedBlock: Cardinal); +var LDeadline: UInt64; LJSON: TPCJSONObject; LMethod: String; LFound: Boolean; +begin + LJSON := nil; + try + LFound := False; + Log('MINER awaiting latest block='+IntToStr(AExpectedBlock)+ + ' previous='+IntToStr(FMiner.MinerValuesForWork.block)+ + ' connected='+BoolToStr(FMiner.Connected,True)+ + ' clients='+IntToStr(TUserInterface.PoolMiningServer.ClientsCount)+ + ' active='+BoolToStr(TUserInterface.PoolMiningServer.Active,True)); + LDeadline := GetTickCount64+3000; + repeat + Pump; + LMethod := ''; + FreeAndNil(LJSON); + LJSON := TPCJSONObject.Create; + if FMiner.DoProcessBuffer(nil,10,False,LMethod,LJSON) and + (LJSON.AsString('method','') = CT_PoolMining_Method_MINER_NOTIFY) then begin + FMiner.DoProcessJSONObject(LJSON,LMethod); + Log('MINER received production job block='+IntToStr(FMiner.MinerValuesForWork.block)); + LFound := (FMiner.MinerValuesForWork.block = AExpectedBlock) and + (Length(FMiner.MinerValuesForWork.part1) > 0); + end; + if LFound then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + // Real committed-block events clear the old job cache. An unchanged empty + // operations notification alone is not expected to bypass the 10/30s rules. + if not LFound then Log('MINER timeout last='+IntToStr(FMiner.MinerValuesForWork.block)+ + ' connected='+BoolToStr(FMiner.Connected,True)+ + ' clients='+IntToStr(TUserInterface.PoolMiningServer.ClientsCount)+ + ' active='+BoolToStr(TUserInterface.PoolMiningServer.Active,True)); + Require(LFound,'The actual miner did not receive a job for the latest committed block'); + Log('MINER consumed latest production job block='+IntToStr(AExpectedBlock)); + finally LJSON.Free; end; +end; + +procedure TBufferedBlockChecks.OnMinerBlockNotification(Sender: TObject); +begin + if not FMonitorMinerNotifications then Exit; + if not Assigned(FObservation) then Exit; + if FObservation.FirstCommitTick = 0 then Exit; + if FBlockNotificationTick = 0 then begin + FBlockNotificationTick := GetTickCount64; + FBlockWhileWriterHeld := ActualWriterHeld; + end; +end; + +procedure TBufferedBlockChecks.OnMinerOperationsNotification(Sender: TObject); +begin + if not FMonitorMinerNotifications then Exit; + if not Assigned(FObservation) then Exit; + if FObservation.FirstCommitTick = 0 then Exit; + if FOperationNotificationTick = 0 then begin + FOperationNotificationTick := GetTickCount64; + FOperationsWhileWriterHeld := ActualWriterHeld; + end; +end; + +procedure TBufferedBlockChecks.CheckIncomingNetworkOff; +var LRelease: TDelayedGateRelease; LStarted, LElapsed, LDeadline, LSettingsElapsed: UInt64; + LCount: Integer; LPort: Word; LPortSocket: TTCPBlockSocket; + LOperations: TPCOperationsComp; + LConfigurationApplied: Boolean; +const CChangedMinerName = 'Buffered ownership latest payload'; +begin + WaitCommit(1); + Require(TNetData.NetData.NetConnectionsActive,'Incoming fixture must begin online'); + if FConnectedMiner then begin + LDeadline := GetTickCount64+400; + repeat + Pump; + if (FBlockNotificationTick <> 0) and (FOperationNotificationTick <> 0) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require((FBlockNotificationTick <> 0) and (FOperationNotificationTick <> 0), + 'Actual block/operations notifications did not reach the UI within400ms'); + Require(FBlockWhileWriterHeld and FOperationsWhileWriterHeld, + 'The connected-miner notification check did not execute while the actual bank writer was held'); + Require((FBlockNotificationTick-FObservation.FirstCommitTick < 400) and + (FOperationNotificationTick-FObservation.FirstCommitTick < 400), + 'Actual node notifications waited for a connected miner/core lock'); + Log('MINER actual block/operations UI latency='+ + UIntToStr(FBlockNotificationTick-FObservation.FirstCommitTick)+'/'+ + UIntToStr(FOperationNotificationTick-FObservation.FirstCommitTick)+ + 'ms maxUIpump='+UIntToStr(FMaximumPump)+'ms while bankWRITE held'); + end; + Require(TUserInterface.Node.GetSynchronizationStatus.State = nssSynchronizing, + 'The real incoming processor did not enter its physical backlog scope'); + LRelease := TDelayedGateRelease.Create(FObservation); + LSettingsElapsed := 0; + LPort := TUserInterface.Node.NetServer.Port; + try + LRelease.Start; + LStarted := GetTickCount64; + // Invoke the actual published toolbar handler on the real UI thread. + // The independent gate actor bounds a blocking old listener shutdown. + FMain.tbtnConnectivityClick(FMain.tbtnConnectivity); + if FExerciseSettings then begin + FMain.tbtnConnectivityClick(FMain.tbtnConnectivity); + FMain.tbtnConnectivityClick(FMain.tbtnConnectivity); + end; + LElapsed := GetTickCount64-LStarted; + if FExerciseSettings then begin + LPortSocket := TTCPBlockSocket.Create; + try + LPortSocket.CreateSocket; + LPortSocket.Bind('127.0.0.1','0'); + Require(LPortSocket.LastError = 0,'Cannot reserve the next owned settings port'); + LPort := LPortSocket.GetLocalSinPort; + finally LPortSocket.Free; end; + LStarted := GetTickCount64; + TSettings.InternetServerPort := LPort; + TSettings.MinerName := CChangedMinerName; + TSettings.Save; + LSettingsElapsed := GetTickCount64-LStarted; + end; + LRelease.WaitFor; + finally + LRelease.Free; + end; + LDeadline := GetTickCount64+5000; + repeat + Pump; + if (InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0) and + not TUserInterface.Node.IsUpdatingBlockChain and + (TUserInterface.Node.NetServer.Port = LPort) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + LCount := InterlockedCompareExchange(FObservation.CommitCount,0,0); + Log('INCOMING actual toolbar OFF duration='+UIntToStr(LElapsed)+ + 'ms settings='+UIntToStr(LSettingsElapsed)+'ms committed='+IntToStr(LCount)+' destroyed='+ + IntToStr(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0))); + Require(not TNetData.NetData.NetConnectionsActive,'Actual toolbar did not switch networking off'); + Require(LCount = CHistoricCount,'Listener shutdown discarded an already received block tail'); + Require(LElapsed < 400,'Actual UI network-off waited for buffered listener work'); + Require(LSettingsElapsed < 400,'Actual port settings event waited for a buffered core writer'); + Require(TUserInterface.Node.NetServer.Port = LPort,'The latest queued port did not win'); + if FExerciseSettings then begin + // A port is applied before the same worker finishes the copied mining + // snapshot. Await its observable result, not a mid-task field transition. + LDeadline := GetTickCount64+3000; + repeat + Pump; + LConfigurationApplied := False; + if TUserInterface.Node.TryLockMempoolRead(0,LOperations) then begin + try + LConfigurationApplied := + (TEncoding.ANSI.GetString(LOperations.BlockPayload) = CChangedMinerName) and + TAccountComp.EqualAccountKeys(LOperations.AccountKey,FExpectedMiningKey); + finally + TUserInterface.Node.UnlockMempoolRead; + end; + end; + LConfigurationApplied := LConfigurationApplied and + TUserInterface.PoolMiningServer.Active and + TAccountComp.EqualAccountKeys(TUserInterface.PoolMiningServer.MinerAccountKey,FExpectedMiningKey) and + (TEncoding.ANSI.GetString(TUserInterface.PoolMiningServer.MinerPayload) = CChangedMinerName); + if LConfigurationApplied then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(LConfigurationApplied,'The latest copied node/pool mining snapshot was not applied'); + LOperations := TUserInterface.Node.LockMempoolRead; + try + Require(TEncoding.ANSI.GetString(LOperations.BlockPayload) = CChangedMinerName, + 'The owned settings worker did not apply the copied latest miner payload'); + Require(TAccountComp.EqualAccountKeys(LOperations.AccountKey,FExpectedMiningKey), + 'The copied mining reward key was not retained through the settings updates'); + finally + TUserInterface.Node.UnlockMempoolRead; + end; + Require(TSettings.JsonRpcMinerServerActive and TUserInterface.PoolMiningServer.Active, + 'The settings test must exercise the actual configured-on mining branch'); + Require(TWallet.Keys.Count = FMiningWalletKeyCount, + 'Ordinary port/name settings rotated or added a NewEachTime wallet key'); + end; + Require(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0, + 'The drained incoming connection was not retired'); + FConnection := nil; + if FExerciseSettings then begin + FMain.tbtnConnectivityClick(FMain.tbtnConnectivity); + LDeadline := GetTickCount64+3000; + repeat + Pump; + if TNetData.NetData.NetConnectionsActive and TUserInterface.Node.NetServer.Active and + (FMain.sbStatusBar.Panels[0].Text = 'Active (port: '+IntToStr(LPort)+')') then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(FMain.sbStatusBar.Panels[0].Text = 'Active (port: '+IntToStr(LPort)+')', + 'The port-only completion did not publish the actual listener/footer state'); + FMain.tbtnConnectivityClick(FMain.tbtnConnectivity); + LDeadline := GetTickCount64+3000; + repeat Pump; Sleep(1); until + (not TNetData.NetData.NetConnectionsActive and not TUserInterface.Node.NetServer.Active) or + (GetTickCount64 >= LDeadline); + Require(not TNetData.NetData.NetConnectionsActive,'Final independent OFF was not applied'); + Log('PASS rapid OFF/ON/OFF latest port and subsequent real listener/footer ON/OFF'); + end; + if FConnectedMiner then AwaitMinerBlockJob(CHistoricCount); +end; + +procedure TBufferedBlockChecks.CheckExplicitShutdown; +var LRelease: TDelayedGateRelease; LStarted, LElapsed: UInt64; +begin + WaitCommit(1); + if not FIncomingOwner then begin + CheckCommittedDisplay(1); + CheckOwnerWhileGated; + end; + LRelease := TDelayedGateRelease.Create(FObservation); + try + LRelease.Start; + if FIncomingOwner then begin + FMain.tbtnConnectivityClick(FMain.tbtnConnectivity); + LStarted := GetTickCount64+500; + repeat Pump; Sleep(1); until not TNetData.NetData.NetConnectionsActive or + (GetTickCount64 >= LStarted); + Require(not TNetData.NetData.NetConnectionsActive, + 'The real pending listener-stop task did not publish OFF before shutdown'); + end; + LStarted := GetTickCount64; + // Exercise the production application shutdown, not a direct thread kill. + // Its shutdown latch is published before listener/client joins begin. + TUserInterface.ExitApplication; + LElapsed := GetTickCount64-LStarted; + FMain := nil; + FDialog := nil; + FConnection := nil; + LRelease.WaitFor; + finally + LRelease.Free; + end; + Log('SHUTDOWN actual ExitApplication duration='+UIntToStr(LElapsed)+ + 'ms committed='+IntToStr(InterlockedCompareExchange(FObservation.CommitCount,0,0))+ + ' destroyed='+IntToStr(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0))); + // This bound only has to catch a hang. Hosted CI took up to about 6 s here, + // because the incoming case also waits for its listener to stop. + Require(LElapsed < 15000,'Explicit shutdown did not join the buffered processor promptly'); + Require(InterlockedCompareExchange(FObservation.CommitCount,0,0) = 1, + 'Explicit application shutdown must cancel between complete atomic blocks'); + Require(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0, + 'Explicit shutdown returned before its transport-owned connection was retired'); + Log('PASS actual ExitApplication joined in '+UIntToStr(LElapsed)+ + 'ms and stopped between blocks; one complete block persisted'); + CheckReopenedStorage(1); +end; + +procedure TBufferedBlockChecks.CheckReopenedStorage(AExpected: Integer); +var LBank: TPCBank; +begin + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + // Match startup's complete restore entrypoint, not RestoreBank's safebox- + // selection phase alone, which does not populate the cached bank header. + LBank.DiskRestoreFromOperations(AExpected-1,nil); + Require(LBank.BlocksCount = Cardinal(AExpected),'Reopened storage lost or added a received block'); + Require(LBank.LastOperationBlock.timestamp = CHistoricTimes[AExpected-1], + 'Reopened storage has the wrong committed tip'); + finally LBank.Free; end; + Log('PASS persisted/reopened '+IntToStr(AExpected)+' fully validated historical blocks'); +end; + +procedure TBufferedBlockChecks.Run; +var LID: TGUID; LOptions: TWalletStartupOptions; LParams: TAppParams; LDeadline: UInt64; + I: Integer; LBlock: TPCOperationsComp; LCount: Cardinal; LError: String; +begin + Require(not TNode.NodeExists,'The backlog fixture needs its own fresh controller'); + if FConnectedMiner then begin + // Use an independent instance of the existing logger so real settings + // changes cannot disable the diagnostic sink. Its lifetime spans startup + // through joined application shutdown; the normal logger is unchanged. + FDiagnosticLog := TLog.Create(nil); + FDiagnosticLog.SaveTypes := CT_TLogTypes_ALL; + FDiagnosticLog.FileName := FLogPath+'.core.log'; + end; + CreateGUID(LID); + FDataDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP'))+'BufferedBlocks-'+GUIDToString(LID); + Require(not DirectoryExists(FDataDirectory),'Refusing existing runtime data'); + Require(ForceDirectories(FDataDirectory),'Cannot create owned runtime data'); + LParams := TAppParams.Create(nil); + try + LParams.FileName := IncludeTrailingPathDelimiter(FDataDirectory)+'AppParams.prm'; + LParams.ParamByName[CT_PARAM_TryToConnectOnlyWithThisFixedServers].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_PeerCache].SetAsString(''); + LParams.ParamByName[CT_PARAM_InternetServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_JSONRPCEnabled].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerActive].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_MinerPrivateKeyType].SetAsInteger(Ord(mpk_Random)); + LParams.ParamByName[CT_PARAM_SaveLogFiles].SetAsBoolean(False); + finally LParams.Free; end; + LOptions := Default(TWalletStartupOptions); + LOptions.DataDirectory := FDataDirectory; + LOptions.NoStartupDialogs := True; + LOptions.Offline := False; + TUserInterface.ConfigureStartup(LOptions); + Application.CreateForm(TFRMMainForm,FMain); + TUserInterface.StartApplication(FMain); + LDeadline := GetTickCount64+10000; + repeat + Pump; + Require(TUserInterface.State <> uisError,'Fresh loopback-only startup failed'); + if (Wallet <> nil) and (TUserInterface.State <> uisLoading) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(Wallet <> nil,'Actual main wallet did not load'); + LDeadline := GetTickCount64+3000; + repeat Pump; Sleep(1); until TNetData.NetData.NetConnectionsActive or (GetTickCount64 >= LDeadline); + Require(TNetData.NetData.NetConnectionsActive,'The owned online startup task did not complete'); + TUserInterface.Node.NetServer.Active := False; + TNetData.NetData.MinServersConnected := 0; + TUserInterface.Node.BroadcastData := False; + FData := TResourceStream.Create(HInstance,'HISTORIC_BUFFERED_BLOCKS',RT_RCDATA); + FData.ReadBuffer(LCount,SizeOf(LCount)); + Require(LCount = CHistoricCount,'Wrong historical batch resource'); + LBlock := TPCOperationsComp.Create(TUserInterface.Node.Bank); + try + for I := 0 to CHistoricCount-1 do begin + Require(LBlock.LoadBlockFromStream(FData,LError),LError); + Require(LBlock.OperationBlock.block = Cardinal(I),'Historical fixture height mismatch'); + TWallet.Keys.AddPublicKey('Historic public miner '+IntToStr(I),LBlock.OperationBlock.account_key); + end; + finally LBlock.Free; end; + FData.Position := 0; + if FExerciseSettings then ConfigureMiningFixture; + if FConnectedMiner then ConnectControlledMiner; + FObservation := TCommitObservation.Create(not FIncomingOwner and not FSynchronizationLifecycle); + FObservation.CheckDeferredOperationsAtCommit := FConnectedMiner; + if FIncomingOwner then OpenIncomingConnection + else begin + FListener := TTCPBlockSocket.Create; + FListener.CreateSocket; + FListener.Bind('127.0.0.1','0'); + FListener.Listen; + Require(FListener.LastError = 0,'Cannot open the owned raw loopback listener'); + if FOutgoingOwner then FConnection := TNetClient.Create(nil) + else FConnection := TBufferedResponseConnection.Create(nil); + if FOutgoingOwner then FConnection.FreeNotification(FObservation); + Require(FConnection.ConnectTo('127.0.0.1',FListener.GetLocalSinPort),'The real core client did not connect to loopback'); + Require(FListener.CanRead(1000),'The owned raw loopback server did not receive the connection'); + FAccepted := TTCPBlockSocket.Create; + FAccepted.Socket := FListener.Accept; + Require(FAccepted.LastError = 0,'Cannot accept the owned loopback peer'); + end; + // The separate direct-entry case covers synchronous external callers. Here + // the client's real receiver owns that socket and may be reading already. + if not (FOutgoingOwner or FIncomingOwner or FSynchronizationLifecycle) then CheckOnlineCallerRequest; + FMain.Show; + FDialog := TForm.CreateNew(FMain); + FDialog.Caption := 'Owned modeless focus regression'; + FDialog.Show; + SetActiveWindow(FDialog.Handle); + if FSynchronizationLifecycle then begin + CheckSynchronizationResponses; + Exit; + end; + if FConnectedMiner then begin + FExternalRelease := TDelayedGateRelease.Create(FObservation,True); + FExternalRelease.Start; + FMonitorMinerNotifications := True; + end; + if FOutgoingOwner or FIncomingOwner then ReflectBufferedResponse + else begin + FTask := TReceivedBatchTask.Create(TBufferedResponseConnection(FConnection),FData); + FTask.StartOwned; + end; + if FShutdownAfterFirst then begin + CheckExplicitShutdown; + Exit; + end; + if FIncomingOwner then CheckIncomingNetworkOff + else + for I := 1 to CHistoricCount do begin + WaitCommit(I); + CheckCommittedDisplay(I); + if FOutgoingOwner and (I = 1) then CheckOwnerWhileGated; + FObservation.ContinueBatch.SetEvent; + end; + LDeadline := GetTickCount64+5000; + if FOutgoingOwner then begin + repeat Pump; Sleep(1); until + (InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0) or + (GetTickCount64 >= LDeadline); + Require(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0, + 'The actual destroy worker did not retire the drained outgoing connection'); + FConnection := nil; + end else if not FIncomingOwner then begin + repeat Pump; Sleep(1); until (FTask.Completed.WaitFor(0)=wrSignaled) or (GetTickCount64>=LDeadline); + Require(FTask.Completed.WaitFor(0)=wrSignaled,'Received batch did not finish'); + Require(FTask.ErrorMessage = '',FTask.ErrorMessage); + end; + LDeadline := GetTickCount64+3000; + repeat + Pump; + if (TUserInterface.Node.GetSynchronizationStatus.State = nssOffline) and + not Wallet.AccountsGrid.RefreshSuspended and not Wallet.OperationsGrid.RefreshSuspended then Break; + Sleep(1); + until GetTickCount64>=LDeadline; + Require(TUserInterface.Node.GetSynchronizationStatus.State = nssOffline,'Network-off must become available once the received batch drains'); + Require(not Wallet.AccountsGrid.LoadingVisible and not Wallet.OperationsGrid.LoadingVisible, + 'The actual main cards must clear after the received batch drains'); + if not (FOutgoingOwner or FIncomingOwner) then CheckNoNewRequests + else if FOutgoingOwner then Require(TNetData.NetData.NewRequestId = FObservation.FirstOffRequestID+1, + 'The outgoing drain allocated a new request after network-off'); + FreeAndNil(FTask); + if not (FOutgoingOwner or FIncomingOwner) then FreeAndNil(FConnection); + FreeAndNil(FAccepted); + FreeAndNil(FListener); + FMonitorMinerNotifications := False; + if Assigned(FMinerObserver) then begin + FMinerObserver.OnBlocksChanged := nil; + FMinerObserver.OnOperationsChanged := nil; + FMinerObserver.Node := nil; + FreeAndNil(FMinerObserver); + end; + // Keep the controlled miner connected through actual application shutdown. + // Its accepted server worker must retire before the pool's jobs/template die. + TUserInterface.ExitApplication; + FMain := nil; + FDialog := nil; + CheckReopenedStorage(CHistoricCount); +end; + +{$IFDEF LARGE_BACKLOG_PROBE} +{$I UWalletLargeBufferedBlockChecks.inc} +{$ENDIF} + +function RunChecks(AOutgoingOwner, AIncomingOwner, AShutdownAfterFirst, AExerciseSettings, AConnectedMiner, ASynchronizationLifecycle: Boolean): Integer; +var LChecks: TBufferedBlockChecks; LInput: THandle; LName, LInputName: array[0..255] of Char; LRequired: DWORD; +begin + Result := 1; + if ParamCount <> 1 then Exit; + LInput := OpenInputDesktop(0,False,DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + if not GetUserObjectInformation(GetThreadDesktop(GetCurrentThreadId),UOI_NAME,@LName[0],SizeOf(LName),LRequired) then RaiseLastOSError; + if not GetUserObjectInformation(LInput,UOI_NAME,@LInputName[0],SizeOf(LInputName),LRequired) then RaiseLastOSError; + Require(not SameText(StrPas(PChar(@LName[0])),StrPas(PChar(@LInputName[0]))), + 'Use the existing owned private-desktop broker'); + finally CloseDesktop(LInput); end; + Require(not FileExists(ParamStr(1)),'Refusing an existing test log'); + Application.Initialize; + LChecks := TBufferedBlockChecks.Create(ExpandFileName(ParamStr(1)),AOutgoingOwner, + AIncomingOwner,AShutdownAfterFirst,AExerciseSettings,AConnectedMiner,ASynchronizationLifecycle); + try + try LChecks.Run; Result := 0; + except on E: Exception do LChecks.Log('FAIL '+E.ClassName+': '+E.Message); end; + finally LChecks.Free; end; +end; + +function RunBufferedBlockBacklogProbe: Integer; +var LScenario: String; +begin + // The public runner compiles once, then gives each scenario a fresh process + // and controller through the existing one-logfile private-desktop broker. + LScenario := LowerCase(GetEnvironmentVariable('PASCALCOIN_BACKLOG_CASE')); + if (LScenario = '') or (LScenario = 'direct') or (LScenario = 'synchronization') then + Result := RunChecks(False,False,False,False,False,LScenario = 'synchronization') + else if LScenario = 'outgoing' then Result := RunOutgoingBufferedBlockProbe + else if LScenario = 'outgoing-shutdown' then Result := RunOutgoingShutdownProbe + else if LScenario = 'incoming' then Result := RunIncomingBufferedBlockProbe + else if LScenario = 'incoming-settings' then Result := RunIncomingSettingsProbe + else if LScenario = 'incoming-shutdown' then Result := RunIncomingShutdownProbe + else if LScenario = 'connected-miner' then Result := RunConnectedMinerProbe + else Result := 64; +end; + +function RunOutgoingBufferedBlockProbe: Integer; +begin + Result := RunChecks(True,False,False,False,False,False); +end; + +function RunIncomingBufferedBlockProbe: Integer; +begin + Result := RunChecks(False,True,False,False,False,False); +end; + +function RunOutgoingShutdownProbe: Integer; +begin + Result := RunChecks(True,False,True,False,False,False); +end; + +function RunIncomingShutdownProbe: Integer; +begin + Result := RunChecks(False,True,True,False,False,False); +end; + +function RunIncomingSettingsProbe: Integer; +begin + Result := RunChecks(False,True,False,True,False,False); +end; + +function RunConnectedMinerProbe: Integer; +begin + Result := RunChecks(False,True,False,True,True,False); +end; + +end. diff --git a/src/tests/experimental/native/network-backlog/UWalletLargeBufferedBlockChecks.inc b/src/tests/experimental/native/network-backlog/UWalletLargeBufferedBlockChecks.inc new file mode 100644 index 000000000..15d716991 --- /dev/null +++ b/src/tests/experimental/native/network-backlog/UWalletLargeBufferedBlockChecks.inc @@ -0,0 +1,800 @@ +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Optional large-data variant of the same real main/provider/transport checks. + Include in UWalletBufferedBlockChecks only for its dedicated large probe; + UDataSources and UCommon.Data must be in that unit's implementation uses. } + +const + CLargeInitialHeight = 841529; + CLargeFinalHeight = 841534; + CLargePendingCount = 185831; + CLargeMinimumPendingHistory = 1000; + CLargePendingSHA256 = '53CBE8BB9E9CB01F24CA6799B715BE68101D953F8818A8235FE600E0BBA056F5'; + CLargePendingOrderedHash = 'D06C9B3339B4A91647DACE47D09027A4BCD9BC03E6989414687675C5243A2938'; + CLargeBlockFixtureSHA256 = 'BA574C8A608115157FD8B89D8F8843D723D9B9265554C5BBB7EB11B50F5E6FDC'; + +type + TLargePublicOwnerSelection = class(TPCThread) + private + FReady: TEvent; + FStarted: Boolean; + FKeys: TArray<TAccountKey>; + FCounts: TArray<Integer>; + FTotalOwned, FSampled: Integer; + FError: String; + protected + procedure BCExecute; override; + public + constructor Create; + destructor Destroy; override; + procedure StartSelection; + property Ready: TEvent read FReady; + property Keys: TArray<TAccountKey> read FKeys; + property Counts: TArray<Integer> read FCounts; + property TotalOwned: Integer read FTotalOwned; + property Sampled: Integer read FSampled; + property Error: String read FError; + end; + + TLargeCoreAccountsSource = class(TMyAccountsDataSource) + public + Requests, CompletedRequests, LastTotal, LastRequestedPage: LongInt; + LastFirstAccount: Cardinal; + LastError: String; + protected + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; override; + end; + + TLargeCoreOperationsSource = class(TAccountsOperationsDataSource) + public + Requests, CompletedRequests, LastTotal, LastRequestedPage: LongInt; + LastFirstOperationHash: String; + LastPendingCount: Integer; + LastPendingAtTip: Cardinal; + LastError: String; + protected + procedure FetchAll(const AContainer: TList<TOperationResume>); override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; override; + end; + + TLargeCoreWallet = class(TCTRLWallet) + private + FObservedAccounts: TLargeCoreAccountsSource; + FObservedOperations: TLargeCoreOperationsSource; + protected + function CreateAccountsDataSource: TMyAccountsDataSource; override; + function CreateOperationsDataSource: TAccountsOperationsDataSource; override; + public + property AccountsGrid; + property OperationsGrid; + property ObservedAccounts: TLargeCoreAccountsSource read FObservedAccounts; + property ObservedOperations: TLargeCoreOperationsSource read FObservedOperations; + end; + + TLargeCoreMain = class(TFRMMainForm) + protected + function CreateWalletControl: TCTRLWallet; override; + end; + + TLargeBufferedBlockChecks = class(TBufferedBlockChecks) + private + FHeaders: array[0..CHistoricCount-1] of TOperationBlock; + FPausedAccounts, FPausedOperations: Integer; + FPausedCompletedAccounts, FPausedCompletedOperations: Integer; + FInitialFirstAccount: Cardinal; + FLargestDispatch: UInt64; + function LargeWallet: TLargeCoreWallet; + procedure LogViewState(const APhase: String); + procedure PumpResponsive; + procedure WaitLargeCommit(AExpected: Integer); + procedure AwaitInitialPages; + procedure AwaitSparsePages; + procedure CheckPending(ANode: TNode; const APhase: String); + procedure CheckLargeCommit(ACommitted: Integer); + procedure CheckFinalPages; + procedure CheckLargeStorage; + public + procedure RunLarge; + end; + +function LargeProviderException(E: Exception): String; +{$IFDEF FPC}var I: Integer;{$ENDIF} +begin + Result := E.ClassName+': '+E.Message; + {$IFDEF FPC} + if Assigned(BackTraceStrFunc) then begin + Result := Result+LineEnding+BackTraceStrFunc(ExceptAddr); + for I := 0 to ExceptFrameCount-1 do + Result := Result+LineEnding+BackTraceStrFunc(ExceptFrames[I]); + end; + {$ENDIF} +end; + +constructor TLargePublicOwnerSelection.Create; +begin + inherited Create(True); + FreeOnTerminate := False; + FReady := TEvent.Create(nil,True,False,''); +end; + +destructor TLargePublicOwnerSelection.Destroy; +begin + Terminate; + if FStarted then WaitFor; + FReady.Free; + inherited; +end; + +procedure TLargePublicOwnerSelection.StartSelection; +begin + Start; + FStarted := True; +end; + +procedure TLargePublicOwnerSelection.BCExecute; +var + LNode: TNode; + LPool: TPCOperationsComp; + LSafeBox: TPCSafeBox; + LKeys: TList<TAccountKey>; + LCounts: TList<Integer>; + LAffected: TOrderedList<Cardinal>; + LOperation: TPCOperation; + LScope: TDisposables; + I, J, LPosition: Integer; + + procedure AddOwner(AAccount: Cardinal; AMinimumCount, AMaximumCount: Integer); + var LAccount: TAccount; LKey: TAccountKey; LOwned, K: Integer; + begin + if Terminated then raise EAbort.Create('Public-owner selection cancelled'); + LAccount := LSafeBox.Account(AAccount); + LKey := LAccount.accountInfo.accountKey; + LOwned := LSafeBox.OrderedAccountKeysList.GetAccountsUsingThisKey(LKey).Count; + Inc(FSampled); + if (LOwned < AMinimumCount) or (LOwned > AMaximumCount) then Exit; + for K := 0 to LKeys.Count-1 do + if TAccountComp.EqualAccountKeys(LKeys[K],LKey) then Exit; + LKeys.Add(LKey); + LCounts.Add(LOwned); + Inc(FTotalOwned,LOwned); + end; +begin + try + try + LKeys := LScope.AddObject(TList<TAccountKey>.Create) as TList<TAccountKey>; + LCounts := LScope.AddObject(TList<Integer>.Create) as TList<Integer>; + LAffected := LScope.AddObject(TOrderedList<Cardinal>.Create(False,TComparison_Cardinal)) as TOrderedList<Cardinal>; + LNode := TNode.Node; + LPool := LNode.LockMempoolRead; + try + LSafeBox := LNode.Bank.SafeBox; + LSafeBox.StartThreadSafe; + try + Require(LPool.Count = CLargePendingCount,'Wrong pool for actual public-owner selection'); + // Reuse the proven dense fixture's first/middle/last pending signer + // owners, then affected accounts and a bounded public-chain fallback. + for I := 0 to 2 do begin + LPosition := (Int64(LPool.Count-1)*I) div 2; + LOperation := LPool.OperationsHashTree.GetOperation(LPosition); + AddOwner(LOperation.SignerAccount,1,500000); + end; + if FTotalOwned < 326387 then + for I := 0 to 2 do begin + LPosition := (Int64(LPool.Count-1)*I) div 2; + LOperation := LPool.OperationsHashTree.GetOperation(LPosition); + LAffected.Clear; + LOperation.AffectedAccounts(LAffected); + for J := 0 to LAffected.Count-1 do AddOwner(LAffected[J],1,500000); + end; + if FTotalOwned < 326387 then + for I := 0 to 2047 do begin + AddOwner((Int64(LSafeBox.AccountsCount-1)*I) div 2047,12000,500000); + if FTotalOwned >= 326387 then Break; + end; + Require(FTotalOwned >= 326387,'The real public owners do not cover326387 accounts'); + FKeys := LKeys.ToArray; + FCounts := LCounts.ToArray; + finally LSafeBox.EndThreadSave; end; + finally LNode.UnlockMempoolRead; end; + except + on E: Exception do FError := LargeProviderException(E); + end; + finally FReady.SetEvent; end; +end; + +function TLargeCoreAccountsSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; +begin + InterlockedIncrement(Requests); + LastError := ''; + try + Result := inherited FetchPage(AParams,ADataTable); + except + on E: Exception do begin + LastError := LargeProviderException(E); + raise; + end; + end; + LastTotal := Result.TotalDataCount; + LastRequestedPage := AParams.PageIndex; + if Length(ADataTable.Rows) > 0 then LastFirstAccount := ADataTable.Rows[0].AccountNumber; + InterlockedIncrement(CompletedRequests); +end; + +function TLargeCoreOperationsSource.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; +begin + InterlockedIncrement(Requests); + LastError := ''; + try + Result := inherited FetchPage(AParams,ADataTable); + except + on E: Exception do begin + LastError := LargeProviderException(E); + raise; + end; + end; + LastTotal := Result.TotalDataCount; + LastRequestedPage := AParams.PageIndex; + LastFirstOperationHash := ''; + if Length(ADataTable.Rows) > 0 then LastFirstOperationHash := ADataTable.Rows[0].OPHASH; + InterlockedIncrement(CompletedRequests); +end; + +procedure TLargeCoreOperationsSource.FetchAll(const AContainer: TList<TOperationResume>); +var LTipBefore, LTipAfter: Cardinal; I, LPendingCount: Integer; +begin + LTipBefore := TNode.Node.GetSynchronizationStatus.LocalBlockCount; + inherited FetchAll(AContainer); + LTipAfter := TNode.Node.GetSynchronizationStatus.LocalBlockCount; + LastPendingAtTip := High(Cardinal); + LastPendingCount := 0; + if LTipBefore <> LTipAfter then Exit; + LPendingCount := 0; + // The real pending adapter stamps the next block number; stored operations + // are below it. Count only an unchanged-tip inherited result, outside locks. + for I := 0 to AContainer.Count-1 do + if AContainer[I].Block = LTipAfter then Inc(LPendingCount); + LastPendingCount := LPendingCount; + LastPendingAtTip := LTipAfter; +end; + +function TLargeCoreWallet.CreateAccountsDataSource: TMyAccountsDataSource; +begin + FObservedAccounts := TLargeCoreAccountsSource.Create(Self); + Result := FObservedAccounts; +end; + +function TLargeCoreWallet.CreateOperationsDataSource: TAccountsOperationsDataSource; +begin + FObservedOperations := TLargeCoreOperationsSource.Create(Self); + Result := FObservedOperations; +end; + +function TLargeCoreMain.CreateWalletControl: TCTRLWallet; +begin + Result := TLargeCoreWallet.Create(Self); +end; + +function TLargeBufferedBlockChecks.LargeWallet: TLargeCoreWallet; +begin + Result := TLargeCoreWallet(TMainAccess(FMain).WalletControl); +end; + +procedure TLargeBufferedBlockChecks.LogViewState(const APhase: String); +var LStatus: TNodeSynchronizationStatus; +begin + Log(Format('LARGE VIEW %s mode=%d mainVisible=%s walletVisible=%s accountsMode=%d parents=%s/%s rows=%d/%d requests=%d/%d completed=%d/%d updating=%s/%s', + [APhase,Ord(FMain.Mode),BoolToStr(FMain.Visible,True),BoolToStr(LargeWallet.Visible,True), + Ord(LargeWallet.AccountsMode),BoolToStr(Assigned(LargeWallet.AccountsGrid.Parent),True), + BoolToStr(Assigned(LargeWallet.OperationsGrid.Parent),True), + LargeWallet.AccountsGrid.RowCount,LargeWallet.OperationsGrid.RowCount, + LargeWallet.ObservedAccounts.Requests,LargeWallet.ObservedOperations.Requests, + LargeWallet.ObservedAccounts.CompletedRequests,LargeWallet.ObservedOperations.CompletedRequests, + BoolToStr(LargeWallet.AccountsGrid.IsUpdating,True),BoolToStr(LargeWallet.OperationsGrid.IsUpdating,True)])); + LStatus := TUserInterface.Node.GetSynchronizationStatus; + Log(Format('LARGE ADMISSION %s state=%d transfers=%d waiting=%s/%s suspended=%s/%s total=%d/%d errors=[%s]/[%s]', + [APhase,Ord(LStatus.State),LStatus.ActiveTransfers, + BoolToStr(LargeWallet.AccountsGrid.RefreshThrottle.Waiting,True), + BoolToStr(LargeWallet.OperationsGrid.RefreshThrottle.Waiting,True), + BoolToStr(LargeWallet.AccountsGrid.RefreshSuspended,True), + BoolToStr(LargeWallet.OperationsGrid.RefreshSuspended,True), + LargeWallet.ObservedAccounts.LastTotal,LargeWallet.ObservedOperations.LastTotal, + LargeWallet.ObservedAccounts.LastError,LargeWallet.ObservedOperations.LastError])); +end; + +procedure TLargeBufferedBlockChecks.PumpResponsive; +var LStarted, LElapsed: UInt64; +begin + LStarted := GetTickCount64; + Pump; + LElapsed := GetTickCount64-LStarted; + if LElapsed > FLargestDispatch then FLargestDispatch := LElapsed; + if LElapsed >= 400 then Log('LARGE main dispatch='+UIntToStr(LElapsed)+'ms'); + Require(LElapsed < 400,'The actual large-data main dispatch blocked for >=400ms'); +end; + +procedure TLargeBufferedBlockChecks.WaitLargeCommit(AExpected: Integer); +var LDeadline: UInt64; +begin + LDeadline := GetTickCount64+30000; + repeat + PumpResponsive; + if (InterlockedCompareExchange(FObservation.CommitCount,0,0) = AExpected) and + (FObservation.Committed.WaitFor(0) = wrSignaled) then Exit; + if InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0 then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + raise Exception.CreateFmt('The actual large processor did not commit block%d', + [CLargeInitialHeight+AExpected-1]); +end; + +procedure TLargeBufferedBlockChecks.AwaitInitialPages; +var LDeadline: UInt64; +begin + LDeadline := GetTickCount64+120000; + repeat + PumpResponsive; + if not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating and + ((LargeWallet.ObservedAccounts.LastError <> '') or (LargeWallet.ObservedOperations.LastError <> '')) then begin + LogViewState('inherited provider exception'); + raise Exception.Create('Actual provider error: '+LargeWallet.ObservedAccounts.LastError+' / '+ + LargeWallet.ObservedOperations.LastError); + end; + if not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating and + not LargeWallet.AccountsGrid.RefreshThrottle.Waiting and + not LargeWallet.OperationsGrid.RefreshThrottle.Waiting and + (LargeWallet.AccountsGrid.RowCount > 0) and (LargeWallet.OperationsGrid.RowCount > 0) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + LogViewState('initial page await finished'); + Require((LargeWallet.AccountsGrid.RowCount > 0) and (LargeWallet.OperationsGrid.RowCount > 0) and + not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating and + not LargeWallet.AccountsGrid.RefreshThrottle.Waiting and + not LargeWallet.OperationsGrid.RefreshThrottle.Waiting, + 'The actual large-data providers did not populate both initial main pages'); + Require(LargeWallet.ObservedAccounts.LastTotal >= 326387, + 'The sampled public owners must exercise the known large ownership set'); + Require((LargeWallet.ObservedOperations.LastPendingAtTip = CLargeInitialHeight) and + (LargeWallet.ObservedOperations.LastPendingCount >= CLargeMinimumPendingHistory), + 'The actual history provider must materialize substantial pending history at the frozen tip'); + Require(LargeWallet.OperationsMode = womAllAccounts, + 'The large history fixture must retain actual all-wallet membership'); + Require(FMain.Visible and (FMain.Mode = wmWallet) and LargeWallet.Visible and + Assigned(LargeWallet.AccountsGrid.Parent) and Assigned(LargeWallet.OperationsGrid.Parent), + 'The large checks must exercise the visible real main wallet and its parented grids'); + FInitialFirstAccount := LargeWallet.AccountsGrid.Rows[0].AccountNumber; + Log(Format('LARGE initial real accounts=%d history=%d pending-history=%d rows=%d/%d fetches=%d/%d', + [LargeWallet.ObservedAccounts.LastTotal,LargeWallet.ObservedOperations.LastTotal, + LargeWallet.ObservedOperations.LastPendingCount, + LargeWallet.AccountsGrid.RowCount,LargeWallet.OperationsGrid.RowCount, + LargeWallet.ObservedAccounts.Requests,LargeWallet.ObservedOperations.Requests])); +end; + +procedure TLargeBufferedBlockChecks.AwaitSparsePages; +var LDeadline: UInt64; LPool: TPCOperationsComp; LAccount: TAccount; +begin + LDeadline := GetTickCount64+120000; + repeat + PumpResponsive; + if not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating and + ((LargeWallet.ObservedAccounts.LastError <> '') or (LargeWallet.ObservedOperations.LastError <> '')) then begin + LogViewState('sparse inherited provider exception'); + raise Exception.Create('Actual sparse provider error: '+LargeWallet.ObservedAccounts.LastError+' / '+ + LargeWallet.ObservedOperations.LastError); + end; + if not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating and + not LargeWallet.AccountsGrid.RefreshThrottle.Waiting and not LargeWallet.OperationsGrid.RefreshThrottle.Waiting and + (LargeWallet.ObservedAccounts.Requests >= 2) and (LargeWallet.ObservedOperations.Requests >= 2) and + (LargeWallet.ObservedAccounts.CompletedRequests = LargeWallet.ObservedAccounts.Requests) and + (LargeWallet.ObservedOperations.CompletedRequests = LargeWallet.ObservedOperations.Requests) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + LogViewState('sparse page await finished'); + Require(not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating and + not LargeWallet.AccountsGrid.RefreshThrottle.Waiting and not LargeWallet.OperationsGrid.RefreshThrottle.Waiting and + (LargeWallet.ObservedAccounts.Requests >= 2) and (LargeWallet.ObservedOperations.Requests >= 2) and + (LargeWallet.ObservedAccounts.CompletedRequests = LargeWallet.ObservedAccounts.Requests) and + (LargeWallet.ObservedOperations.CompletedRequests = LargeWallet.ObservedOperations.Requests), + 'The sparse actual providers did not complete admitted queries'); + Require((LargeWallet.AccountsGrid.RowCount = 1) and (LargeWallet.ObservedAccounts.LastTotal = 1) and + (LargeWallet.ObservedAccounts.LastFirstAccount = 111),'The sparse account result is not the real account111'); + LPool := TUserInterface.Node.LockMempoolRead; + try LAccount := LPool.SafeBoxTransaction.Account(111); + finally TUserInterface.Node.UnlockMempoolRead; end; + Require(LAccount.balance > High(Cardinal),'The sparse oracle must exercise the actual wide balance'); + Require((UInt64(LargeWallet.AccountsGrid.Rows[0].Balance) = LAccount.balance) and + (Cardinal(LargeWallet.AccountsGrid.Rows[0].NOperation) = LAccount.n_operation) and + (Cardinal(LargeWallet.AccountsGrid.Rows[0].LastUpdatedBlock) = LAccount.GetLastUpdatedBlock), + 'The sparse page narrowed the real balance or changed account metadata'); + Log(Format('SPARSE real account111 balance=%s history=%d successful fetches=%d/%d', + [UIntToStr(LAccount.balance),LargeWallet.ObservedOperations.LastTotal, + LargeWallet.ObservedAccounts.CompletedRequests,LargeWallet.ObservedOperations.CompletedRequests])); +end; + +procedure TLargeBufferedBlockChecks.CheckPending(ANode: TNode; const APhase: String); +var LPool: TPCOperationsComp; LStream: TMemoryStream; LHash: String; +begin + LStream := TMemoryStream.Create; + try + LPool := ANode.LockMempoolRead; + try + Require(LPool.Count = CLargePendingCount,APhase+': actual pending count changed'); + Require(SameText(TCrypto.ToHexaString(LPool.OperationsHashTree.HashTree),CLargePendingOrderedHash), + APhase+': actual pending operation order changed'); + Require(LPool.OperationsHashTree.SaveOperationsHashTreeToStream(LStream,True), + APhase+': actual pending serialization failed'); + finally ANode.UnlockMempoolRead; end; + LHash := TCrypto.ToHexaString(TCrypto.DoSha256(PAnsiChar(LStream.Memory),LStream.Size)); + Require(SameText(LHash,CLargePendingSHA256),APhase+': serialized pending bytes changed'); + Log(APhase+' pending='+IntToStr(CLargePendingCount)+' SHA256='+LHash); + finally LStream.Free; end; +end; + +procedure TLargeBufferedBlockChecks.CheckLargeCommit(ACommitted: Integer); +var LDeadline: UInt64; LHeight: Cardinal; LStatus, LDisplayed: TNodeSynchronizationStatus; + LFooter, LTime: String; +begin + LHeight := CLargeInitialHeight+Cardinal(ACommitted); + LDeadline := GetTickCount64+3000; + repeat + PumpResponsive; + LStatus := TUserInterface.Node.GetSynchronizationStatus; + LDisplayed := FMain.SyncControl.DisplayedSynchronizationStatus; + if (LDisplayed.LocalBlockCount = LHeight) and + (LDisplayed.State = nssSynchronizing) and not LDisplayed.NetworkActive and + LargeWallet.AccountsGrid.RefreshSuspended and LargeWallet.OperationsGrid.RefreshSuspended then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require((LStatus.LocalBlockCount = LHeight) and (LStatus.State = nssSynchronizing) and + (LStatus.ActiveTransfers > 0) and not LStatus.NetworkActive and + (LStatus.PendingOperationsCount = CLargePendingCount) and + (LStatus.LastBlockTimestamp = FHeaders[ACommitted-1].timestamp), + 'The cached real node status must describe this exact complete large-data commit'); + Require((LDisplayed.LocalBlockCount = LStatus.LocalBlockCount) and + (LDisplayed.LastBlockTimestamp = LStatus.LastBlockTimestamp) and + (LDisplayed.State = LStatus.State), + 'The actual large sync view rendered stale committed core metadata'); + Require(FMain.SyncControl.lblTotalBlocksValue.Caption = + IntToStr(LHeight)+' (0..'+IntToStr(LHeight-1)+')', + 'The actual large sync block label disagrees with its cached record'); + LFooter := FMain.sbStatusBar.Panels[2].Text; + LTime := FormatDateTime('dd/mm/yyyy hh:nn:ss', + UnivDateTime2LocalDateTime(UnixToUnivDateTime(LStatus.LastBlockTimestamp))); + Require((LFooter = FMain.SyncControl.ChainStatusText) and + (Pos('Blocks: '+IntToStr(LHeight)+' |',LFooter) = 1) and + (Pos(LTime,LFooter) > 0) and (Pos('Network off',LFooter) > 0), + 'The actual large footer must use the same current count/time/off record'); + Require(LargeWallet.AccountsGrid.RefreshSuspended and LargeWallet.OperationsGrid.RefreshSuspended and + LargeWallet.AccountsGrid.LoadingVisible and LargeWallet.OperationsGrid.LoadingVisible and + (FMain.Mode = wmWallet) and LargeWallet.Visible, + 'Both actual large main grids must remain paused/covered throughout received processing'); + Require(GetActiveWindow = FDialog.Handle,'A large commit stole modeless focus'); + if ACommitted = 1 then begin + FPausedAccounts := LargeWallet.ObservedAccounts.Requests; + FPausedOperations := LargeWallet.ObservedOperations.Requests; + FPausedCompletedAccounts := LargeWallet.ObservedAccounts.CompletedRequests; + FPausedCompletedOperations := LargeWallet.ObservedOperations.CompletedRequests; + // These are real client refresh requests. Their latest parameters must be + // retained without provider execution while the node owns its backlog. + LargeWallet.AccountsGrid.PageIndex := 2; + LargeWallet.AccountsGrid.PageIndex := 1; + LargeWallet.OperationsGrid.PageIndex := 1; + LargeWallet.OperationsGrid.PageIndex := 0; + end; + Require((LargeWallet.ObservedAccounts.Requests = FPausedAccounts) and + (LargeWallet.ObservedOperations.Requests = FPausedOperations), + 'An actual large provider query started while physical backlog work remained'); + Log('LARGE COMMITTED '+IntToStr(LHeight)+' '+LFooter); +end; + +procedure TLargeBufferedBlockChecks.CheckFinalPages; +var LDeadline: UInt64; +begin + LDeadline := GetTickCount64+120000; + repeat + PumpResponsive; + if not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating and + not LargeWallet.AccountsGrid.RefreshSuspended and not LargeWallet.OperationsGrid.RefreshSuspended and + not LargeWallet.AccountsGrid.LoadingVisible and not LargeWallet.OperationsGrid.LoadingVisible and + (LargeWallet.ObservedAccounts.Requests > FPausedAccounts) and + (LargeWallet.ObservedOperations.Requests > FPausedOperations) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require(TUserInterface.Node.GetSynchronizationStatus.State = nssOffline, + 'The large local wallet must become available when received processing drains'); + Require(not LargeWallet.AccountsGrid.RefreshSuspended and not LargeWallet.OperationsGrid.RefreshSuspended and + not LargeWallet.AccountsGrid.LoadingVisible and not LargeWallet.OperationsGrid.LoadingVisible and + not LargeWallet.AccountsGrid.IsUpdating and not LargeWallet.OperationsGrid.IsUpdating, + 'The actual large main pages did not finish after backlog drain'); + Require((LargeWallet.ObservedAccounts.Requests = FPausedAccounts+1) and + (LargeWallet.ObservedOperations.Requests = FPausedOperations+1), + 'Backlog drain must execute one retained latest real query per grid'); + Require((LargeWallet.ObservedAccounts.CompletedRequests = FPausedCompletedAccounts+1) and + (LargeWallet.ObservedOperations.CompletedRequests = FPausedCompletedOperations+1), + 'Both final real provider queries must complete successfully, not leave retained rows after failure'); + Require((LargeWallet.ObservedAccounts.LastRequestedPage = 1) and + (LargeWallet.ObservedOperations.LastRequestedPage = 0), + 'Backlog drain did not execute the latest real page parameters'); + Require((LargeWallet.AccountsGrid.RowCount > 0) and (LargeWallet.OperationsGrid.RowCount > 0), + 'The resumed actual large providers returned no visible rows'); + Require((LargeWallet.ObservedOperations.LastPendingAtTip = CLargeFinalHeight) and + (LargeWallet.ObservedOperations.LastPendingCount >= CLargeMinimumPendingHistory), + 'The final real history query did not invalidate its raw snapshot and materialize current pending rows'); + Require((Cardinal(LargeWallet.AccountsGrid.Rows[0].AccountNumber) = + LargeWallet.ObservedAccounts.LastFirstAccount) and + (LargeWallet.ObservedAccounts.LastFirstAccount <> FInitialFirstAccount) and + (String(LargeWallet.OperationsGrid.Rows[0].OPHASH) = + LargeWallet.ObservedOperations.LastFirstOperationHash), + 'The visible final pages must publish the newly completed real provider rows'); + Log(Format('LARGE latest real pages published fetches=%d/%d maxUIpump=%dms', + [LargeWallet.ObservedAccounts.Requests,LargeWallet.ObservedOperations.Requests,FLargestDispatch])); +end; + +procedure TLargeBufferedBlockChecks.CheckLargeStorage; +var LBank: TPCBank; LStorage: TFileStorage; LBlock: TPCOperationsComp; + LPending: TOperationsHashTree; LActual, LExpected: TMemoryStream; + I: Integer; LLength: Int64; LError, LHash: String; +begin + LBank := nil; + LBlock := nil; + LActual := nil; + LExpected := nil; + LPending := nil; + try + // The pending reader resolves Bank.Orphan even with an explicit chain + // filename. Keep its real bank owner alive until every reader is freed. + LBank := TPCBank.Create(nil); + LBank.StorageClass := TFileStorage; + LStorage := TFileStorage(LBank.Storage); + Require(SameText(ExcludeTrailingPathDelimiter(ExpandFileName(TNode.GetPascalCoinDataFolder)), + ExcludeTrailingPathDelimiter(ExpandFileName(FDataDirectory))), + 'The reopened bank must retain the owned fixture data-directory context'); + LBlock := TPCOperationsComp.Create(nil); + LActual := TMemoryStream.Create; + LExpected := TMemoryStream.Create; + LPending := TOperationsHashTree.Create; + LStorage.ReadOnly := True; + LStorage.SetBlockChainFile(AnsiString(IncludeTrailingPathDelimiter(FDataDirectory)+ + 'Data'+PathDelim+'BlockChainStream.blocks')); + Require(LStorage.Initialize,'Cannot reopen the owned large stored chain read-only'); + // Pending decoding uses the owner's actual protocol. Restore that owner + // through the normal bank path rather than a fresh protocol1 placeholder. + Log('LARGE BEGIN read-only bank restore'); + LBank.DiskRestoreFromOperations(CLargeFinalHeight-1,nil); + Log('LARGE END read-only bank restore'); + Require((LBank.BlocksCount = CLargeFinalHeight) and + (LBank.LastOperationBlock.timestamp = FHeaders[CHistoricCount-1].timestamp), + 'The reopened large bank must retain the exact committed height and timestamp'); + FData.Position := SizeOf(Cardinal); + for I := 0 to CHistoricCount-1 do begin + LLength := FData.Position; + Require(LBlock.LoadBlockFromStream(FData,LError),LError); + LLength := FData.Position-LLength; + LExpected.Clear; + FData.Position := FData.Position-LLength; + LExpected.CopyFrom(FData,LLength); + Require(LStorage.LoadBlockChainBlock(LBlock,CLargeInitialHeight+Cardinal(I)), + 'Reopened large chain is missing one received block'); + LActual.Clear; + Require(LBlock.SaveBlockToStream(False,LActual),'Cannot serialize the reopened received block'); + Require((LActual.Size = LExpected.Size) and CompareMem(LActual.Memory,LExpected.Memory,LActual.Size), + 'A reopened large received block differs from its original public network bytes'); + Log('LARGE reopened exact public block '+IntToStr(CLargeInitialHeight+Cardinal(I))); + end; + LStorage.LoadPendingBufferOperations(LPending); + Require((LPending.OperationsCount = CLargePendingCount) and + SameText(TCrypto.ToHexaString(LPending.HashTree),CLargePendingOrderedHash), + 'Reopened large pending storage lost operations or changed their order'); + LActual.Clear; + Require(LPending.SaveOperationsHashTreeToStream(LActual,True), + 'Cannot serialize the reopened large pending package'); + LHash := TCrypto.ToHexaString(TCrypto.DoSha256(PAnsiChar(LActual.Memory),LActual.Size)); + Require(SameText(LHash,CLargePendingSHA256), + 'Reopened large pending storage differs from the original185831-operation package'); + finally LPending.Free; LExpected.Free; LActual.Free; LBlock.Free; LBank.Free; end; + Log('PASS LARGE five full-PoW/bank-validated blocks and exact185831-operation package reopened'); +end; + +procedure TLargeBufferedBlockChecks.RunLarge; +var LOptions: TWalletStartupOptions; LParams: TAppParams; LDeadline: UInt64; + LCount: Cardinal; LBlock: TPCOperationsComp; LSelection: TLargePublicOwnerSelection; LAccount: TAccount; + LError: String; I: Integer; LSparseDiagnostic: Boolean; +begin + Require(not TNode.NodeExists,'The large fixture needs its own fresh controller'); + FDataDirectory := ExcludeTrailingPathDelimiter(ExpandFileName( + GetEnvironmentVariable('PASCALCOIN_LARGE_BACKLOG_ROOT'))); + Require((GetEnvironmentVariable('PASCALCOIN_LARGE_BACKLOG_ROOT') <> '') and + DirectoryExists(FDataDirectory),'Provide the precreated owned large app root'); + Require(FileExists(FDataDirectory+PathDelim+'LargeBacklog.owned'), + 'The large runtime requires the explicitly leased owned-data marker'); + Require(DirectoryExists(FDataDirectory+PathDelim+'Data'), + 'The large owned app root has no leased Data directory'); + Require((GetFileAttributes(PChar(FDataDirectory+PathDelim+'Data')) and FILE_ATTRIBUTE_REPARSE_POINT) <> 0, + 'The large runtime must reuse its explicitly leased Data junction, not copy the chain'); + Require(not FileExists(FDataDirectory+PathDelim+'WalletKeys.dat') and + not FileExists(FDataDirectory+PathDelim+'AppParams.prm'), + 'Refusing existing wallet keys or settings in the large app root'); + LParams := TAppParams.Create(nil); + try + LParams.FileName := FDataDirectory+PathDelim+'AppParams.prm'; + LParams.ParamByName[CT_PARAM_TryToConnectOnlyWithThisFixedServers].SetAsString('127.0.0.1:1'); + LParams.ParamByName[CT_PARAM_PeerCache].SetAsString(''); + LParams.ParamByName[CT_PARAM_InternetServerPort].SetAsInteger(0); + LParams.ParamByName[CT_PARAM_JSONRPCEnabled].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_JSONRPCMinerServerActive].SetAsBoolean(False); + LParams.ParamByName[CT_PARAM_MinerPrivateKeyType].SetAsInteger(Ord(mpk_Random)); + LParams.ParamByName[CT_PARAM_SaveLogFiles].SetAsBoolean(False); + finally LParams.Free; end; + LOptions := Default(TWalletStartupOptions); + LOptions.DataDirectory := FDataDirectory; + LOptions.NoStartupDialogs := True; + LOptions.Offline := False; + TUserInterface.ConfigureStartup(LOptions); + Application.CreateForm(TLargeCoreMain,FMain); + TUserInterface.StartApplication(FMain); + FMain.Show; + LDeadline := GetTickCount64+180000; + repeat + PumpResponsive; + Require(TUserInterface.State <> uisError,'The owned real large-data startup failed'); + if (LargeWallet <> nil) and (TUserInterface.State <> uisLoading) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + Require((LargeWallet <> nil) and (TUserInterface.State <> uisLoading), + 'The real185831-operation startup did not complete'); + Require(TUserInterface.Node.Bank.BlocksCount = CLargeInitialHeight,'Wrong owned large-chain initial tip'); + Require(TWallet.Keys.Count = 0,'The large app must begin with no wallet keys'); + TNetData.NetData.MinServersConnected := 0; + TUserInterface.Node.BroadcastData := False; + CheckPending(TUserInterface.Node,'LARGE BEFORE'); + LSparseDiagnostic := GetEnvironmentVariable('PASCALCOIN_LARGE_BACKLOG_SPARSE_DIAGNOSTIC') = '1'; + if LSparseDiagnostic then begin + // Preserve the sparse-owner RED independently of the dense selection; it + // exposes inherited provider failures before any buffered block is sent. + LAccount := TUserInterface.Node.Bank.SafeBox.Account(111); + TWallet.Keys.AddPublicKey('Public account111 sparse diagnostic',LAccount.accountInfo.accountKey); + Require((TWallet.Keys.Count = 1) and not Assigned(TWallet.Keys.Key[0].PrivateKey), + 'The sparse diagnostic must contain only its public watch key'); + Log('SPARSE111 diagnostic: require successful inherited queries and exact wide balance; zero history is valid'); + end else begin + LSelection := TLargePublicOwnerSelection.Create; + try + LSelection.StartSelection; + LDeadline := GetTickCount64+30000; + repeat PumpResponsive; Sleep(1); until + (LSelection.Ready.WaitFor(0) = wrSignaled) or (GetTickCount64 >= LDeadline); + Require(LSelection.Ready.WaitFor(0) = wrSignaled,'Actual public-owner selection timed out'); + LSelection.WaitFor; + Require(LSelection.Error = '',LSelection.Error); + Log(Format('LARGE real public owners=%d owned=%d sampled=%d', + [Length(LSelection.Keys),LSelection.TotalOwned,LSelection.Sampled])); + for I := 0 to Length(LSelection.Keys)-1 do begin + Log(Format('LARGE public owner%d accounts=%d',[I,LSelection.Counts[I]])); + TWallet.Keys.AddPublicKey('Public pending-owner fixture '+IntToStr(I),LSelection.Keys[I]); + end; + Require(TWallet.Keys.Count = Length(LSelection.Keys),'Wrong public-only key count'); + for I := 0 to TWallet.Keys.Count-1 do + Require(not Assigned(TWallet.Keys.Key[I].PrivateKey),'The fixture must never load private keys'); + finally LSelection.Free; end; + end; + LogViewState('before public wallet entry'); + // A low-level public-key import is not a user navigation action. Enter the + // real wallet view and its Show All account selector before awaiting pages. + TUserInterface.ShowWallet; + PumpResponsive; + if LargeWallet.AccountsMode <> wamMyAccounts then begin + LargeWallet.cbAccounts.ItemIndex := 0; + LargeWallet.cbAccountsChange(LargeWallet.cbAccounts); + PumpResponsive; + end; + LogViewState('after public wallet entry'); + if LSparseDiagnostic then begin + AwaitSparsePages; + CheckPending(TUserInterface.Node,'SPARSE AFTER'); + TUserInterface.ExitApplication; + FMain := nil; + FDialog := nil; + Log('PASS SPARSE successful actual queries, exact64-bit balance and natural controller shutdown'); + Exit; + end; + AwaitInitialPages; + LDeadline := GetTickCount64+3000; + repeat PumpResponsive; Sleep(1); until TNetData.NetData.NetConnectionsActive or (GetTickCount64 >= LDeadline); + Require(TNetData.NetData.NetConnectionsActive,'Owned loopback-only networking did not enable'); + // This scenario owns only an outgoing loopback client. Match the small + // outgoing setup and retire the unused listener before receiving its batch. + TUserInterface.Node.NetServer.Active := False; + FData := TResourceStream.Create(HInstance,'LARGE_BUFFERED_BLOCKS',RT_RCDATA); + Require(FData.Size = 4465,'The large embedded response must contain only count+five provenance-checked public blocks'); + Require(SameText(TCrypto.ToHexaString(TCrypto.DoSha256(PAnsiChar(FData.Memory),FData.Size)), + CLargeBlockFixtureSHA256),'The embedded large public block fixture failed its provenance hash'); + FData.ReadBuffer(LCount,SizeOf(LCount)); + Require(LCount = CHistoricCount,'Wrong large historic response count'); + LBlock := TPCOperationsComp.Create(TUserInterface.Node.Bank); + try + for I := 0 to CHistoricCount-1 do begin + Require(LBlock.LoadBlockFromStream(FData,LError),LError); + FHeaders[I] := LBlock.OperationBlock; + Require(FHeaders[I].block = CLargeInitialHeight+Cardinal(I),'Wrong large public block sequence'); + end; + Require(FData.Position = FData.Size,'Unexpected bytes after the five large public blocks'); + finally LBlock.Free; end; + FData.Position := 0; + FObservation := TCommitObservation.Create(True); + FListener := TTCPBlockSocket.Create; + FListener.CreateSocket; + FListener.Bind('127.0.0.1','0'); + FListener.Listen; + Require(FListener.LastError = 0,'Cannot open the large fixture raw loopback listener'); + FConnection := TNetClient.Create(nil); + FConnection.FreeNotification(FObservation); + Require(FConnection.ConnectTo('127.0.0.1',FListener.GetLocalSinPort), + 'The real large-data transport owner did not connect to loopback'); + Require(FListener.CanRead(1000),'The raw large-data peer did not receive the connection'); + FAccepted := TTCPBlockSocket.Create; + FAccepted.Socket := FListener.Accept; + Require(FAccepted.LastError = 0,'Cannot accept the real large-data loopback client'); + FDialog := TForm.CreateNew(FMain); + FDialog.Caption := 'Owned large-data modeless focus regression'; + FDialog.Show; + SetActiveWindow(FDialog.Handle); + ReflectBufferedResponse; + for I := 1 to CHistoricCount do begin + WaitLargeCommit(I); + CheckLargeCommit(I); + if I = 1 then begin + LDeadline := GetTickCount64+1200; + repeat PumpResponsive; Sleep(1); until GetTickCount64 >= LDeadline; + Require(Assigned(FObservation.ProcessingThread) and + (FObservation.ProcessingThread is TNetClientThread), + 'The large response did not execute on the actual outgoing client owner'); + Require(not TThreadAccess(FObservation.ProcessingThread).Terminated and + (InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) = 0), + 'Network-off cleanup cancelled or destroyed the large buffered processor'); + end; + FObservation.ContinueBatch.SetEvent; + end; + LDeadline := GetTickCount64+10000; + repeat PumpResponsive; Sleep(1); until + (InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0) or (GetTickCount64 >= LDeadline); + Require(InterlockedCompareExchange(FObservation.ConnectionDestroyed,0,0) <> 0, + 'The actual large-data transport owner did not retire after backlog drain'); + FConnection := nil; + Require(TNetData.NetData.NewRequestId = FObservation.FirstOffRequestID+1, + 'The large-data drain allocated a new download request after network-off'); + Require(TUserInterface.Node.Bank.BlocksCount = CLargeFinalHeight,'The large bank did not finish all five received blocks'); + CheckFinalPages; + CheckPending(TUserInterface.Node,'LARGE AFTER'); + FreeAndNil(FAccepted); + FreeAndNil(FListener); + TUserInterface.ExitApplication; + FMain := nil; + FDialog := nil; + CheckLargeStorage; +end; + +function RunLargeBufferedBlockProbe: Integer; +var LChecks: TLargeBufferedBlockChecks; LInput: THandle; + LName, LInputName: array[0..255] of Char; LRequired: DWORD; +begin + Result := 1; + if ParamCount <> 1 then Exit; + LInput := OpenInputDesktop(0,False,DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + if not GetUserObjectInformation(GetThreadDesktop(GetCurrentThreadId),UOI_NAME, + @LName[0],SizeOf(LName),LRequired) then RaiseLastOSError; + if not GetUserObjectInformation(LInput,UOI_NAME,@LInputName[0],SizeOf(LInputName),LRequired) then RaiseLastOSError; + Require(not SameText(StrPas(PChar(@LName[0])),StrPas(PChar(@LInputName[0]))), + 'Use the existing owned private-desktop broker'); + finally CloseDesktop(LInput); end; + Require(not FileExists(ParamStr(1)),'Refusing an existing large test log'); + Application.Initialize; + LChecks := TLargeBufferedBlockChecks.Create(ExpandFileName(ParamStr(1)),True,False,False,False,False); + try + try LChecks.RunLarge; Result := 0; + except on E: Exception do LChecks.Log('FAIL '+LargeProviderException(E)); end; + finally LChecks.Free; end; +end; diff --git a/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.dpr b/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.dpr new file mode 100644 index 000000000..a96feb1cd --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.dpr @@ -0,0 +1,8 @@ +program CoreSynchronizationProbe; +{$APPTYPE CONSOLE} +uses SysUtils, UCoreSynchronizationChecks; +begin + ReportMemoryLeaksOnShutdown:=True; + if ParamCount<>1 then Halt(64); + ExitCode:=RunCoreSynchronizationChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.dproj b/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.dproj new file mode 100644 index 000000000..37774768b --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.dproj @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{52422A56-3823-4226-BD09-69768488E80A}</ProjectGuid> + <MainSource>CoreSynchronizationProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\block-throughput;..\node-sync;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\network-lifetime\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\network-lifetime\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UCoreSynchronizationChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject><Delphi.Personality><Source> + <Source Name="MainSource">CoreSynchronizationProbe.dpr</Source> + </Source></Delphi.Personality><Platforms> + <Platform value="Win32">True</Platform><Platform value="Win64">True</Platform> + </Platforms></BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.lpr b/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.lpr new file mode 100644 index 000000000..a9ce39bd7 --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/CoreSynchronizationProbe.lpr @@ -0,0 +1,8 @@ +program CoreSynchronizationProbe; +{$mode delphi} +uses heaptrc, SysUtils, UCoreSynchronizationChecks; +begin + if ParamCount<>1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode:=RunCoreSynchronizationChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.dpr b/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.dpr new file mode 100644 index 000000000..7d82df02d --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.dpr @@ -0,0 +1,8 @@ +program DownloadLifetimeProbe; +{$APPTYPE CONSOLE} +uses SysUtils, UDownloadLifetimeChecks; +begin + ReportMemoryLeaksOnShutdown:=True; + if ParamCount<>1 then Halt(64); + ExitCode:=RunDownloadLifetimeChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.dproj b/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.dproj new file mode 100644 index 000000000..3896d44c1 --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.dproj @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{93E018E2-73C5-4A26-9A4D-CC1E40845D79}</ProjectGuid> + <MainSource>DownloadLifetimeProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\block-throughput;..\node-sync;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\network-lifetime\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\network-lifetime\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UDownloadLifetimeChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject><Delphi.Personality><Source> + <Source Name="MainSource">DownloadLifetimeProbe.dpr</Source> + </Source></Delphi.Personality><Platforms> + <Platform value="Win32">True</Platform><Platform value="Win64">True</Platform> + </Platforms></BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.lpr b/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.lpr new file mode 100644 index 000000000..fcaf6c04c --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/DownloadLifetimeProbe.lpr @@ -0,0 +1,8 @@ +program DownloadLifetimeProbe; +{$mode delphi} +uses heaptrc, SysUtils, UDownloadLifetimeChecks; +begin + if ParamCount<>1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode:=RunDownloadLifetimeChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/network-lifetime/README.md b/src/tests/experimental/native/network-lifetime/README.md new file mode 100644 index 000000000..8c637db9f --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/README.md @@ -0,0 +1,48 @@ +# Download and node lifetime checks + +[DownloadLifetimeProbe.dproj](DownloadLifetimeProbe.dproj) and its +[FPC entry point](DownloadLifetimeProbe.lpr) run seven lifetime +[scenarios](UDownloadLifetimeChecks.pas): + +- selected outgoing connection and factory failure +- incoming retirement +- owner shutdown +- node-owned callbacks +- pending discovery +- running discovery +- client registered after an empty scan + +They open loopback sockets and write data under the log folder. They never +load a user's wallet, contact public peers or submit operations. + +The [combined Delphi project](CoreSynchronizationProbe.dproj) and +[FPC program](CoreSynchronizationProbe.lpr) run 57 checks: + +- the lifetime scenarios +- sanitizer, signature, cache-policy and signed change-info checks +- 17 [node synchronization checks](../node-sync/README.md#core-lifecycle-probe) +- three [legacy connection checks](ULegacyConnectionLifetimeChecks.pas) +- ten [pending catch-up checks](../block-throughput/UPendingCatchupChecks.pas) + +## Run the probes + +Build Win64 Debug in Delphi, put OpenSSL 1.1 beside the executable and point +`TEMP` and `TMP` at a test folder. Create the parent folder, then pass one +new log-file path: + +```powershell +.\CoreSynchronizationProbe.exe <new-folder>\checks.log +``` + +A successful run exits 0 and writes a summary and per-scenario logs. FPC also +writes a heap report. + +[`RunExperimentalWalletTests.ps1`](../../RunExperimentalWalletTests.ps1) also +runs each lifetime scenario and legacy check as an FPCUnit case. + +## Ownership rules + +The node owns its server callbacks and drains them before freeing its +resources. Application shutdown publishes `BeginShutdown`, stops and joins the +shared network services, then frees the node. A retained connection stays +alive, but its mutable fields still need synchronized access. diff --git a/src/tests/experimental/native/network-lifetime/UCoreSynchronizationChecks.pas b/src/tests/experimental/native/network-lifetime/UCoreSynchronizationChecks.pas new file mode 100644 index 000000000..e26296c26 --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/UCoreSynchronizationChecks.pas @@ -0,0 +1,63 @@ +unit UCoreSynchronizationChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Shared native entry point: reuse the public acceptance helpers rather than + copy algorithms or create a separate compiler graph for each core fixture. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunCoreSynchronizationChecks(const ALogFile: String): Integer; + +implementation + +uses Classes, SysUtils, UMempoolSanitizeChecks, UNodeSynchronizationChecks, + UAccountCachePolicyChecks, UChangeAccountInfoReplayChecks, UDownloadLifetimeChecks, + ULegacyConnectionLifetimeChecks, UPendingCatchupChecks; + +function RunCoreSynchronizationChecks(const ALogFile: String): Integer; +var LLog: TStringList; +begin + Result:=1; + if FileExists(ALogFile) then raise Exception.Create('Refusing an existing core summary'); + LLog:=TStringList.Create; + try + try + if RunMempoolSanitizeChecks(ALogFile+'.sanitize')<>0 then + raise Exception.Create('Sanitizer/signature acceptance failed; see scenario logs'); + LLog.Add('PASS sanitizer/signatures: 9'); LLog.SaveToFile(ALogFile); + CheckDefaultAccountCacheLimit; + CheckAccountCacheLimitSurvivesMemoryReload; + CheckAccountCacheLimitSurvivesFileReloadAndClear; + CheckFailedSafeboxReplacementPreservesFinallyException; + CheckFailedSafeboxReplacementPreservesDisposalException; + CheckFailedSafeboxConstructionPreservesException; + CheckNormalSafeboxClearRetainsItsExistingSemantics; + LLog.Add('PASS account cache policy and cleanup: 7'); + LLog.SaveToFile(ALogFile); + CheckChangeInfoSelfTarget; + CheckChangeInfoDistinctTarget; + CheckChangeInfoRepeatedAndRollback; + CheckChangeInfoRejectsWithoutMutation; + LLog.Add('PASS signed change-info replay: 4'); LLog.SaveToFile(ALogFile); + if RunNodeSynchronizationChecks(ALogFile+'.node')<>0 then + raise Exception.Create('Node status acceptance failed; see scenario logs'); + LLog.Add('PASS node status: 17'); LLog.SaveToFile(ALogFile); + if RunDownloadLifetimeChecks(ALogFile+'.lifetime')<>0 then + raise Exception.Create('Download/node ownership acceptance failed; see scenario logs'); + LLog.Add('PASS download/node ownership: 7'); LLog.SaveToFile(ALogFile); + if RunLegacyConnectionLifetimeChecks(ALogFile+'.legacy')<>0 then + raise Exception.Create('Legacy connection ownership failed; see scenario logs'); + LLog.Add('PASS legacy connection ownership: 3'); LLog.SaveToFile(ALogFile); + if RunPendingCatchupChecks(ALogFile+'.catchup')<>0 then + raise Exception.Create('Pending catch-up acceptance failed; see scenario logs'); + LLog.Add('PASS pending catch-up: 10'); LLog.SaveToFile(ALogFile); + LLog.Add('PASS total: 57'); + Result:=0; + except on E: Exception do LLog.Add('FAIL '+E.ClassName+': '+E.Message); end; + LLog.SaveToFile(ALogFile); + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/network-lifetime/UDownloadLifetimeChecks.pas b/src/tests/experimental/native/network-lifetime/UDownloadLifetimeChecks.pas new file mode 100644 index 000000000..637996869 --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/UDownloadLifetimeChecks.pas @@ -0,0 +1,762 @@ +unit UDownloadLifetimeChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + The real downloader owns its leased snapshot and executes BCExecute. Only its + candidate choice and the subsequent download boundary are gated. Actual sockets, + client destruction, listener retirement and owner shutdown remain in production. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type TDownloadLifetimeCheck = (dlcOutgoing, dlcIncoming, dlcShutdown, dlcNodeCallbacks, + dlcPendingDiscovery, dlcActiveDiscovery, dlcLateConnection); + +function RunDownloadLifetimeChecks(const ALogFile: String): Integer; +function RunDownloadLifetimeCheck(ACheck: TDownloadLifetimeCheck; const ALogFile: String): Integer; + +implementation + +uses Classes, SysUtils, SyncObjs, Generics.Collections, blcksock, synsock, + UThread, UBaseTypes, UNetProtocol, UTCPIP, UNode, UCrypto; + +type + TCheckState = class + public + Selected: TNetConnection; + SelectedTransport: TNetTcpIpClient; + Entered, ReleaseBody, BodyFinished, OwnerGone: TEvent; + SelectedGone, OtherGone, TransportGone: TEvent; + EmptyScan, ReleaseScan: TEvent; + Destroyer: TNetClientsDestroyThread; + FailFactory, HoldEmptyScan: Boolean; + Factories, DiscoveryClients: Integer; + constructor Create; + destructor Destroy; override; + end; + + TGateWorker = class(TThreadGetNewBlockChainFromClient) + private + FState: TCheckState; + protected + function SelectConnection(const AConnections: TList<TNetConnection>): TNetConnection; override; + public + constructor Create(AOwner: TNetData; AState: TCheckState); + end; + + TGateNetData = class(TNetData) + private + FState: TCheckState; + protected + function CreateClientsDestroyThread: TNetClientsDestroyThread; override; + function CreateBlockChainWorker: TThreadGetNewBlockChainFromClient; override; + public + constructor Create(AState: TCheckState); reintroduce; + destructor Destroy; override; + procedure StartDownload; + procedure GetNewBlockChainFromClient(Connection: TNetConnection; const why: String); override; + end; + + TObservedDestroyThread = class(TNetClientsDestroyThread) + private + FState: TCheckState; + FObserved: Boolean; + protected + procedure RegistryScanCompleted(AEmpty: Boolean); override; + public + constructor Create(AOwner: TNetData; AState: TCheckState); + end; + + TDrainWaitAction = class(TPCThread) + private + FState: TCheckState; + FDone: TEvent; + protected + procedure BCExecute; override; + public + constructor Create(AState: TCheckState); + destructor Destroy; override; + property Done: TEvent read FDone; + end; + + TGateDiscoveryWorker = class(TThreadDiscoverConnection) + private + FState: TCheckState; + protected + procedure BCExecute; override; + public + constructor Create(AOwner: TNetData; const AAddress: TNodeServerAddress; + AState: TCheckState); + end; + + TDiscoveryNetData = class(TGateNetData) + private + FHoldStart: Boolean; + protected + function CreateDiscoveryWorker(const AAddress: TNodeServerAddress): TThreadDiscoverConnection; override; + function CreateDiscoveryClient: TNetClient; override; + procedure StartDiscoveryWorker(AWorker: TThreadDiscoverConnection); override; + public + procedure StartDiscovery(AHoldStart: Boolean); + end; + + TDeathObserver = class(TComponent) + private + FState: TCheckState; + FOther: TComponent; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + constructor Create(AState: TCheckState; AOther: TComponent); reintroduce; + end; + + TPeer = class(TPCThread) + private + FListener, FSocket: TTCPBlockSocket; + FPort: Word; + protected + procedure BCExecute; override; + public + constructor Create; + destructor Destroy; override; + property Port: Word read FPort; + end; + + TStopAction = class(TPCThread) + private + FOwner: TGateNetData; + FDestroyOwner: Boolean; + FDone: TEvent; + FError: String; + protected + procedure BCExecute; override; + public + constructor Create(AOwner: TGateNetData; ADestroyOwner: Boolean); + destructor Destroy; override; + property Done: TEvent read FDone; + property Error: String read FError; + end; + + TFinalizedReader = class + public + Done: TEvent; + Error: String; + constructor Create; + destructor Destroy; override; + procedure Finalized(Sender: TNetTcpIpServer; AClient: TNetTcpIpClient); + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +function Signaled(AEvent: TEvent): Boolean; +begin + Result := AEvent.WaitFor(0)=wrSignaled; +end; + +procedure Await(AEvent: TEvent; AMilliseconds: Cardinal; const AMessage: String); +var LStart: TTickCount; +begin + LStart := TPlatform.GetTickCount; + while not Signaled(AEvent) and + (TPlatform.GetElapsedMilliseconds(LStart)<AMilliseconds) do begin + CheckSynchronize; + Sleep(5); + end; + Require(Signaled(AEvent),AMessage); +end; + +procedure Pump(AMilliseconds: Cardinal); +var LStart: TTickCount; +begin + LStart := TPlatform.GetTickCount; + repeat CheckSynchronize; Sleep(5); + until TPlatform.GetElapsedMilliseconds(LStart)>=AMilliseconds; +end; + +constructor TCheckState.Create; +begin + inherited; + Entered:=TEvent.Create(nil,True,False,''); + ReleaseBody:=TEvent.Create(nil,True,False,''); + BodyFinished:=TEvent.Create(nil,True,False,''); + OwnerGone:=TEvent.Create(nil,True,False,''); + SelectedGone:=TEvent.Create(nil,True,False,''); + OtherGone:=TEvent.Create(nil,True,False,''); + TransportGone:=TEvent.Create(nil,True,False,''); + EmptyScan:=TEvent.Create(nil,True,False,''); + ReleaseScan:=TEvent.Create(nil,True,False,''); +end; + +destructor TCheckState.Destroy; +begin + ReleaseScan.Free; EmptyScan.Free; + TransportGone.Free; OtherGone.Free; SelectedGone.Free; OwnerGone.Free; + BodyFinished.Free; ReleaseBody.Free; Entered.Free; + inherited; +end; + +constructor TGateWorker.Create(AOwner: TNetData; AState: TCheckState); +begin + FState:=AState; + inherited Create(AOwner); +end; + +function TGateWorker.SelectConnection(const AConnections: TList<TNetConnection>): TNetConnection; +begin + Require(AConnections.IndexOf(FState.Selected)>=0,'Selected live socket was not leased'); + Result:=FState.Selected; +end; + +constructor TGateNetData.Create(AState: TCheckState); +begin + FState:=AState; + inherited Create(nil); +end; + +destructor TGateNetData.Destroy; +var LState: TCheckState; +begin + LState:=FState; + inherited; + // State belongs to the fixture and outlives both owner and worker. + LState.OwnerGone.SetEvent; +end; + +function TGateNetData.CreateBlockChainWorker: TThreadGetNewBlockChainFromClient; +begin + Inc(FState.Factories); + if FState.FailFactory then begin + FState.FailFactory:=False; + raise EAbort.Create('Expected downloader factory failure'); + end; + Result:=TGateWorker.Create(Self,FState); +end; + +function TGateNetData.CreateClientsDestroyThread: TNetClientsDestroyThread; +begin + if FState.HoldEmptyScan then begin + Result:=TObservedDestroyThread.Create(Self,FState); + FState.Destroyer:=Result; + end else Result:=inherited CreateClientsDestroyThread; +end; + +constructor TObservedDestroyThread.Create(AOwner: TNetData; AState: TCheckState); +begin + FState:=AState; + inherited Create(AOwner); +end; + +procedure TObservedDestroyThread.RegistryScanCompleted(AEmpty: Boolean); +begin + if AEmpty and not FObserved then begin + FObserved:=True; + FState.EmptyScan.SetEvent; + FState.ReleaseScan.WaitFor(High(Cardinal)); + end; +end; + +constructor TDrainWaitAction.Create(AState: TCheckState); +begin + FState:=AState; + FDone:=TEvent.Create(nil,True,False,''); + inherited Create(False); + FreeOnTerminate:=False; +end; + +procedure TDrainWaitAction.BCExecute; +begin + try FState.Destroyer.WaitForTerminatedAllConnections; + finally FDone.SetEvent; end; +end; + +destructor TDrainWaitAction.Destroy; +begin + WaitFor; + FDone.Free; + inherited; +end; + +procedure TGateNetData.StartDownload; +begin + StartBlockChainWorker; +end; + +constructor TGateDiscoveryWorker.Create(AOwner: TNetData; + const AAddress: TNodeServerAddress; AState: TCheckState); +begin + FState:=AState; + inherited Create(AOwner,AAddress); +end; + +procedure TGateDiscoveryWorker.BCExecute; +begin + // Hold a real owned worker before its production shutdown/admission check. + // No client factory or network algorithm is replaced by this gate. + FState.Entered.SetEvent; + FState.ReleaseBody.WaitFor(High(Cardinal)); + try + inherited; + finally + FState.BodyFinished.SetEvent; + end; +end; + +function TDiscoveryNetData.CreateDiscoveryWorker( + const AAddress: TNodeServerAddress): TThreadDiscoverConnection; +begin + Inc(FState.Factories); + Result:=TGateDiscoveryWorker.Create(Self,AAddress,FState); +end; + +function TDiscoveryNetData.CreateDiscoveryClient: TNetClient; +begin + Inc(FState.DiscoveryClients); + Result:=inherited CreateDiscoveryClient; +end; + +procedure TDiscoveryNetData.StartDiscoveryWorker(AWorker: TThreadDiscoverConnection); +begin + if FHoldStart then FState.Entered.SetEvent + else inherited StartDiscoveryWorker(AWorker); +end; + +procedure TDiscoveryNetData.StartDiscovery(AHoldStart: Boolean); +var LAddresses: TNodeServerAddressArray; +begin + FHoldStart:=AHoldStart; + SetLength(LAddresses,1); + LAddresses[0]:=CT_TNodeServerAddress_NUL; + LAddresses[0].ip:='127.0.0.1'; + LAddresses[0].port:=1; + StartDiscoveryWorkers(LAddresses); +end; + +procedure TGateNetData.GetNewBlockChainFromClient(Connection: TNetConnection; const why: String); +begin + Require(Connection=FState.Selected,'Worker changed its selected connection'); + FState.SelectedTransport:=Connection.Client; + FState.Entered.SetEvent; + FState.ReleaseBody.WaitFor(High(Cardinal)); + // Do not dereference a prematurely destroyed pointer on a failing regression. + // The main-thread assertions report that failure; cleanup must still join. + if not Signaled(FState.SelectedGone) then + Require(Connection.Client=FState.SelectedTransport,'Leased transport was replaced'); + FState.BodyFinished.SetEvent; +end; + +constructor TDeathObserver.Create(AState: TCheckState; AOther: TComponent); +begin + inherited Create(nil); + FState:=AState; + FOther:=AOther; + if Assigned(AOther) then AOther.FreeNotification(Self); + AState.Selected.FreeNotification(Self); + AState.Selected.Client.FreeNotification(Self); +end; + +procedure TDeathObserver.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited; + if Operation<>opRemove then Exit; + if AComponent=FState.Selected then FState.SelectedGone.SetEvent; + if AComponent=FOther then FState.OtherGone.SetEvent; + if AComponent=FState.SelectedTransport then FState.TransportGone.SetEvent; +end; + +constructor TPeer.Create; +begin + FListener:=TTCPBlockSocket.Create; + FListener.CreateSocket; + FListener.Bind('127.0.0.1','0'); + Require(FListener.LastError=0,'Cannot bind owned loopback peer'); + FListener.Listen; + Require(FListener.LastError=0,'Cannot listen on owned loopback peer'); + FPort:=FListener.GetLocalSinPort; + // All state is ready before the automatic start. Delphi starts this thread + // in AfterConstruction; calling Start inside the constructor starts it twice. + inherited Create(False); + FreeOnTerminate:=False; +end; + +procedure TPeer.BCExecute; +begin + while not Terminated and not FListener.CanRead(20) do ; + if Terminated then Exit; + FSocket:=TTCPBlockSocket.Create; + try + FSocket.Socket:=FListener.Accept; + while not Terminated do begin + if FSocket.CanRead(20) then FSocket.RecvPacket(20); + Sleep(1); + end; + finally FreeAndNil(FSocket); end; +end; + +destructor TPeer.Destroy; +begin + Terminate; + WaitFor; + FListener.Free; + inherited; +end; + +constructor TStopAction.Create(AOwner: TGateNetData; ADestroyOwner: Boolean); +begin + FOwner:=AOwner; + FDestroyOwner:=ADestroyOwner; + FDone:=TEvent.Create(nil,True,False,''); + inherited Create(False); + FreeOnTerminate:=False; +end; + +procedure TStopAction.BCExecute; +begin + try + try + if FDestroyOwner then FOwner.Free + else FOwner.NetConnectionsActive:=False; + except on E: Exception do FError:=E.ClassName+': '+E.Message; end; + finally FDone.SetEvent; end; +end; + +destructor TStopAction.Destroy; +begin + WaitFor; + FDone.Free; + inherited; +end; + +constructor TFinalizedReader.Create; +begin + inherited; + Done:=TEvent.Create(nil,True,False,''); +end; + +destructor TFinalizedReader.Destroy; +begin + Done.Free; + inherited; +end; + +procedure TFinalizedReader.Finalized(Sender: TNetTcpIpServer; AClient: TNetTcpIpClient); +var LLocked: Boolean; LPending: TObject; +begin + LLocked:=False; + try + try + // A real accepted-client finalizer is still an owned node callback. Its + // node resources must remain alive until the listener has joined it. + LPending:=TNode.Node.LockMempoolRead; + LLocked:=True; + Require(Assigned(LPending),'Finalizer lost the pending template'); + finally + if LLocked then TNode.Node.UnlockMempoolRead; + end; + except on E: Exception do Error:=E.ClassName+': '+E.Message; end; + Done.SetEvent; +end; + +procedure Prepare(AOwner: TGateNetData); +begin + AOwner.NetConnectionsActive:=False; + AOwner.MinServersConnected:=0; + AOwner.MaxServersConnected:=0; + TNode.Node.UpdateBlockchain:=True; + TNode.Node.NetServer.Port:=0; + AOwner.NetConnectionsActive:=True; +end; + +procedure OutgoingAndFactoryCheck; +var S: TCheckState; N: TGateNetData; P1,P2: TPeer; + C1,C2: TNetClient; O: TDeathObserver; LFailed: Boolean; +begin + S:=TCheckState.Create; N:=nil; P1:=nil; P2:=nil; O:=nil; + try + N:=TGateNetData.Create(S); Prepare(N); + P1:=TPeer.Create; P2:=TPeer.Create; + C1:=TNetClient.Create(nil); C2:=TNetClient.Create(nil); + Require(C1.ConnectTo('127.0.0.1',P1.Port),'First outgoing peer failed'); + Require(C2.ConnectTo('127.0.0.1',P2.Port),'Second outgoing peer failed'); + S.Selected:=C1; S.SelectedTransport:=C1.Client; + O:=TDeathObserver.Create(S,C2); + S.FailFactory:=True; LFailed:=False; + try N.StartDownload; except on E: EAbort do LFailed:=True; end; + Require(LFailed,'Factory failure was hidden'); + N.StartDownload; + Await(S.Entered,3000,'Downloader did not start after factory failure'); + Require(S.Factories=2,'Unexpected downloader admission count'); + N.NetConnectionsActive:=False; + Await(S.OtherGone,2500,'Unselected connection was retained by another download'); + Pump(700); + Require(not Signaled(S.SelectedGone),'Selected connection was freed while leased'); + Require(not Signaled(S.TransportGone),'Selected transport was freed while leased'); + S.ReleaseBody.SetEvent; + Await(S.BodyFinished,3000,'Selected download did not complete'); + Await(S.SelectedGone,3000,'Selected connection did not retire after release'); + finally + S.ReleaseBody.SetEvent; + N.Free; + O.Free; P2.Free; P1.Free; + if TNode.NodeExists then TNode.Node.Free; + S.Free; + end; +end; + +procedure IncomingRetirementCheck; +var S: TCheckState; N: TGateNetData; Socket: TTCPBlockSocket; + C: TNetConnection; O: TDeathObserver; Stop: TStopAction; + LStart: TTickCount; +begin + S:=TCheckState.Create; N:=nil; Socket:=nil; O:=nil; Stop:=nil; + try + N:=TGateNetData.Create(S); Prepare(N); + // Pick a real ephemeral port without reading private listener fields. + Socket:=TTCPBlockSocket.Create; Socket.CreateSocket; + Socket.Bind('127.0.0.1','0'); + Require(Socket.LastError=0,'Cannot reserve incoming test port'); + N.NetConnectionsActive:=False; + TNode.Node.NetServer.Port:=Socket.GetLocalSinPort; + Socket.CloseSocket; + N.NetConnectionsActive:=True; + LStart:=TPlatform.GetTickCount; + repeat + Socket.CloseSocket; + Socket.Connect('127.0.0.1',IntToStr(TNode.Node.NetServer.Port)); + if Socket.LastError=0 then Break; + Pump(10); + until TPlatform.GetElapsedMilliseconds(LStart)>2000; + Require(Socket.LastError=0,'Incoming loopback socket failed'); + LStart:=TPlatform.GetTickCount; C:=nil; + repeat + if N.GetConnection(0,C) and (C is TNetServerClient) then Break; + Pump(10); + until TPlatform.GetElapsedMilliseconds(LStart)>3000; + Require(Assigned(C) and (C is TNetServerClient),'Incoming wrapper was not accepted'); + S.Selected:=C; S.SelectedTransport:=C.Client; + O:=TDeathObserver.Create(S,nil); + N.StartDownload; Await(S.Entered,3000,'Incoming candidate was not leased'); + Stop:=TStopAction.Create(N,False); + Pump(1200); + Require(not Signaled(Stop.Done),'Listener retired before its selected lease ended'); + Require(not Signaled(S.SelectedGone),'Incoming wrapper was freed while leased'); + Require(not Signaled(S.TransportGone),'Incoming transport was replaced while leased'); + S.ReleaseBody.SetEvent; + Await(Stop.Done,4000,'Listener did not finish after incoming lease release'); + Require(Stop.Error='',Stop.Error); + Await(S.SelectedGone,2000,'Incoming wrapper did not retire'); + finally + S.ReleaseBody.SetEvent; + Stop.Free; N.Free; + O.Free; Socket.Free; + if TNode.NodeExists then TNode.Node.Free; + S.Free; + end; +end; + +procedure ShutdownJoinCheck; +var S: TCheckState; N: TGateNetData; P: TPeer; C: TNetClient; + O: TDeathObserver; Stop: TStopAction; LFactories: Integer; +begin + S:=TCheckState.Create; N:=nil; P:=nil; O:=nil; Stop:=nil; + try + N:=TGateNetData.Create(S); Prepare(N); + P:=TPeer.Create; C:=TNetClient.Create(nil); + Require(C.ConnectTo('127.0.0.1',P.Port),'Shutdown loopback peer failed'); + S.Selected:=C; S.SelectedTransport:=C.Client; + O:=TDeathObserver.Create(S,nil); + N.StartDownload; Await(S.Entered,3000,'Shutdown worker did not acquire lease'); + N.BeginShutdown; + LFactories:=S.Factories; N.StartDownload; + Require(S.Factories=LFactories,'Shutdown admitted a new downloader'); + Stop:=TStopAction.Create(N,True); N:=nil; + Pump(900); + Require(not Signaled(S.OwnerGone),'Owner freed shared state before worker completion'); + Require(not Signaled(S.SelectedGone),'Shutdown freed a selected connection before release'); + S.ReleaseBody.SetEvent; + Await(Stop.Done,4000,'Shutdown did not join downloader after release'); + Require(Stop.Error='',Stop.Error); + Require(Signaled(S.BodyFinished) and Signaled(S.OwnerGone), + 'Owner completion did not follow downloader completion'); + Require(Signaled(S.SelectedGone),'Shutdown left its selected connection alive'); + finally + S.ReleaseBody.SetEvent; + Stop.Free; N.Free; + O.Free; P.Free; + if TNode.NodeExists then TNode.Node.Free; + S.Free; + end; +end; + +procedure OwnedNodeCallbackCheck; +var S: TCheckState; N: TGateNetData; Socket: TTCPBlockSocket; + Reader: TFinalizedReader; C: TNetConnection; LStart: TTickCount; +begin + S:=TCheckState.Create; N:=nil; Socket:=nil; Reader:=nil; + try + N:=TGateNetData.Create(S); Prepare(N); + Socket:=TTCPBlockSocket.Create; Socket.CreateSocket; + Socket.Bind('127.0.0.1','0'); + Require(Socket.LastError=0,'Cannot reserve callback test port'); + N.NetConnectionsActive:=False; + TNode.Node.NetServer.Port:=Socket.GetLocalSinPort; + Socket.CloseSocket; + N.NetConnectionsActive:=True; + Reader:=TFinalizedReader.Create; + TNode.Node.NetServer.OnClientFinalized:=Reader.Finalized; + LStart:=TPlatform.GetTickCount; + repeat + Socket.CloseSocket; + Socket.Connect('127.0.0.1',IntToStr(TNode.Node.NetServer.Port)); + if Socket.LastError=0 then Break; + Pump(10); + until TPlatform.GetElapsedMilliseconds(LStart)>2000; + Require(Socket.LastError=0,'Callback test connection failed'); + LStart:=TPlatform.GetTickCount; C:=nil; + repeat + if N.GetConnection(0,C) and (C is TNetServerClient) then Break; + Pump(10); + until TPlatform.GetElapsedMilliseconds(LStart)>3000; + Require(Assigned(C) and (C is TNetServerClient),'Callback test was not accepted'); + // Stop shared producers first, then test the node's own resource order. + // This does not make the node an owner of the external NetData singleton. + N.BeginShutdown; + TNode.Node.Free; + Await(Reader.Done,1000,'Accepted-client finalizer did not run'); + Require(Reader.Error='','Node resources retired before its callback: '+Reader.Error); + finally + N.Free; + if TNode.NodeExists then TNode.Node.Free; + Reader.Free; Socket.Free; S.Free; + end; +end; + +procedure DiscoveryShutdownCheck(AHoldStart: Boolean); +var S: TCheckState; N: TDiscoveryNetData; Stop: TStopAction; + LFactories: Integer; +begin + S:=TCheckState.Create; N:=nil; Stop:=nil; + try + N:=TDiscoveryNetData.Create(S); Prepare(N); + N.StartDiscovery(AHoldStart); + Await(S.Entered,3000,'Discovery was not registered before its start boundary'); + Require(S.Factories=1,'Unexpected discovery admission count'); + Require(S.DiscoveryClients=0,'Discovery gate was bypassed'); + N.BeginShutdown; + LFactories:=S.Factories; + N.StartDiscovery(False); + Require(S.Factories=LFactories,'Shutdown admitted a new discovery worker'); + // A pending thread may be started solely to let RTL destruction join it. + // Its inherited body must see shutdown before creating a client. + if AHoldStart then S.ReleaseBody.SetEvent; + Stop:=TStopAction.Create(N,True); N:=nil; + if not AHoldStart then begin + Pump(250); + Require(not Signaled(S.OwnerGone),'Owner freed resources while discovery was running'); + S.ReleaseBody.SetEvent; + end; + Await(Stop.Done,4000,'Shutdown did not join its discovery worker'); + Require(Stop.Error='',Stop.Error); + Require(Signaled(S.OwnerGone),'Discovery owner did not finish'); + Require(S.DiscoveryClients=0,'Retired discovery created a late client'); + // Pending workers may be skipped completely; a started worker must finish. + if not AHoldStart then + Require(Signaled(S.BodyFinished),'Started discovery outlived its owner'); + finally + S.ReleaseBody.SetEvent; + Stop.Free; N.Free; + if TNode.NodeExists then TNode.Node.Free; + S.Free; + end; +end; + +procedure LateConnectionDrainCheck; +var S: TCheckState; N: TGateNetData; C: TNetClient; + O: TDeathObserver; Drain: TDrainWaitAction; +begin + S:=TCheckState.Create; N:=nil; O:=nil; Drain:=nil; + try + S.HoldEmptyScan:=True; + N:=TGateNetData.Create(S); Prepare(N); + Await(S.EmptyScan,3000,'Owned destroyer never examined the empty registry'); + // Register after the actual empty scan while that one destroyer is paused. + // A cached empty flag would return before this new client is retired. + C:=TNetClient.Create(nil); + S.Selected:=C; S.SelectedTransport:=C.Client; + O:=TDeathObserver.Create(S,nil); + N.NetConnectionsActive:=False; + Drain:=TDrainWaitAction.Create(S); + Pump(250); + Require(not Signaled(Drain.Done),'Drain trusted an old empty scan over a live client'); + Require(not Signaled(S.SelectedGone),'Paused destroyer unexpectedly retired the client'); + S.ReleaseScan.SetEvent; + Await(Drain.Done,4000,'Drain did not finish after late client retirement'); + Await(S.SelectedGone,1000,'Late client survived the registry drain'); + finally + S.ReleaseScan.SetEvent; + Drain.Free; N.Free; O.Free; + if TNode.NodeExists then TNode.Node.Free; + S.Free; + end; +end; + +function RunDownloadLifetimeCheck(ACheck: TDownloadLifetimeCheck; const ALogFile: String): Integer; +var LLog: TStringList; LRoot: String; +begin + Result:=1; + Require(not FileExists(ALogFile),'Refusing an existing lifetime log'); + LLog:=TStringList.Create; + try + // Other core acceptance fixtures may leave the process-wide services alive. + // Retire that generated fixture context in the normal ownership order before + // installing our virtual-factory owner; never change singleton registration + // to make a second independent owner capture the first owner's connections. + if TNetData.NetDataExists then TNetData.NetData.BeginShutdown; + if TNode.NodeExists then TNode.Node.NetServer.Active:=False; + if TNetData.NetDataExists then TNetData.NetData.Free; + if TNode.NodeExists then TNode.Node.Free; + TCrypto.InitCrypto; + LRoot:=ChangeFileExt(ALogFile,'')+'-data'; + Require(not DirectoryExists(LRoot),'Refusing an existing lifetime data directory'); + ForceDirectories(LRoot); + TNode.SetPascalCoinDataFolder(LRoot); + case ACheck of + dlcOutgoing: OutgoingAndFactoryCheck; + dlcIncoming: IncomingRetirementCheck; + dlcShutdown: ShutdownJoinCheck; + dlcNodeCallbacks: OwnedNodeCallbackCheck; + dlcPendingDiscovery: DiscoveryShutdownCheck(True); + dlcActiveDiscovery: DiscoveryShutdownCheck(False); + dlcLateConnection: LateConnectionDrainCheck; + end; + LLog.Add(Format('PASS %d',[Ord(ACheck)+1])); LLog.SaveToFile(ALogFile); + Result:=0; + except on E: Exception do begin + LLog.Add('FAIL '+E.ClassName+': '+E.Message); LLog.SaveToFile(ALogFile); + end; end; + LLog.Free; +end; + +function RunDownloadLifetimeChecks(const ALogFile: String): Integer; +var LCheck: TDownloadLifetimeCheck; LLog: TStringList; +begin + Result:=1; + Require(not FileExists(ALogFile),'Refusing an existing lifetime summary'); + LLog:=TStringList.Create; + try + for LCheck:=Low(TDownloadLifetimeCheck) to High(TDownloadLifetimeCheck) do begin + if RunDownloadLifetimeCheck(LCheck,ALogFile+'.'+IntToStr(Ord(LCheck)+1)+'.log')<>0 then begin + LLog.Add(Format('FAIL %d; see scenario log',[Ord(LCheck)+1])); + LLog.SaveToFile(ALogFile); + Exit; + end; + LLog.Add(Format('PASS %d',[Ord(LCheck)+1])); + LLog.SaveToFile(ALogFile); + end; + Result:=0; + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/network-lifetime/ULegacyConnectionLifetimeChecks.pas b/src/tests/experimental/native/network-lifetime/ULegacyConnectionLifetimeChecks.pas new file mode 100644 index 000000000..0a57c7554 --- /dev/null +++ b/src/tests/experimental/native/network-lifetime/ULegacyConnectionLifetimeChecks.pas @@ -0,0 +1,383 @@ +unit ULegacyConnectionLifetimeChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Retirement is gated before inherited destruction, so a failing lifetime check + never dereferences a freed connection or destroys an owned critical section. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +type TLegacyConnectionLifetimeCheck = (llcHeldLock, llcRecursiveLock, llcTimedAdmission); + +function RunLegacyConnectionLifetimeChecks(const ALogFile: String): Integer; +function RunLegacyConnectionLifetimeCheck(ACheck: TLegacyConnectionLifetimeCheck; + const ALogFile: String): Integer; + +implementation + +uses Classes, SysUtils, SyncObjs, blcksock, synsock, UThread, UBaseTypes, + UNetProtocol, UNode, UCrypto; + +type + TLegacyState = class + public + Entered, Retired, AllowDestroy, Gone: TEvent; + constructor Create; + destructor Destroy; override; + end; + + TObservedConnection = class(TNetConnection) + private + FState: TLegacyState; + public + constructor Create(AState: TLegacyState); reintroduce; + destructor Destroy; override; + end; + + TDeathObserver = class(TComponent) + private + FState: TLegacyState; + FTarget: TComponent; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + constructor Create(AState: TLegacyState; ATarget: TComponent); reintroduce; + end; + + TPeer = class(TPCThread) + private + FListener: TTCPBlockSocket; + FPort: Word; + protected + procedure BCExecute; override; + public + constructor Create; + destructor Destroy; override; + property Port: Word read FPort; + end; + + TFreeConnection = class(TPCThread) + private + FConnection: TNetConnection; + protected + procedure BCExecute; override; + public + Done: TEvent; + Error: String; + constructor Create(AConnection: TNetConnection); + destructor Destroy; override; + end; + + TTimedLock = class(TPCThread) + private + FOwner: TNetData; + FConnection: TNetConnection; + FTimeout: Cardinal; + protected + procedure BCExecute; override; + public + Done: TEvent; + Acquired: Boolean; + Elapsed: Cardinal; + Error: String; + constructor Create(AOwner: TNetData; AConnection: TNetConnection; ATimeout: Cardinal); + destructor Destroy; override; + end; + +procedure Require(AValue: Boolean; const AMessage: String); +begin + if not AValue then raise Exception.Create(AMessage); +end; + +function Signaled(AEvent: TEvent): Boolean; +begin + Result := AEvent.WaitFor(0) = wrSignaled; +end; + +procedure Await(AEvent: TEvent; const AMessage: String); +var LStart: TTickCount; +begin + LStart := TPlatform.GetTickCount; + while not Signaled(AEvent) and (TPlatform.GetElapsedMilliseconds(LStart)<3000) do begin + CheckSynchronize; + Sleep(1); + end; + Require(Signaled(AEvent),AMessage); +end; + +constructor TLegacyState.Create; +begin + inherited; + Entered := TEvent.Create(nil,true,false,''); + Retired := TEvent.Create(nil,true,false,''); + AllowDestroy := TEvent.Create(nil,true,false,''); + Gone := TEvent.Create(nil,true,false,''); +end; + +destructor TLegacyState.Destroy; +begin + Gone.Free; AllowDestroy.Free; Retired.Free; Entered.Free; + inherited; +end; + +constructor TObservedConnection.Create(AState: TLegacyState); +begin + FState := AState; + inherited Create(nil); +end; + +destructor TObservedConnection.Destroy; +begin + FState.Entered.SetEvent; + // The real production retirement must not finish while a successful legacy + // ConnectionLock is held. Hold fields alive afterwards even on the old RED. + RetireDownloadUsage; + FState.Retired.SetEvent; + FState.AllowDestroy.WaitFor(5000); + inherited; +end; + +constructor TDeathObserver.Create(AState: TLegacyState; ATarget: TComponent); +begin + inherited Create(nil); + FState := AState; + FTarget := ATarget; + ATarget.FreeNotification(Self); +end; + +procedure TDeathObserver.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited; + if (Operation=opRemove) and (AComponent=FTarget) then FState.Gone.SetEvent; +end; + +constructor TPeer.Create; +begin + FListener := TTCPBlockSocket.Create; + FListener.CreateSocket; + FListener.Bind('127.0.0.1','0'); + Require(FListener.LastError=0,'Loopback bind failed'); + FListener.Listen; + Require(FListener.LastError=0,'Loopback listen failed'); + FPort := FListener.GetLocalSinPort; + inherited Create(false); + FreeOnTerminate := false; +end; + +procedure TPeer.BCExecute; +var LSocket: TTCPBlockSocket; +begin + while not Terminated and not FListener.CanRead(20) do ; + if Terminated then Exit; + LSocket := TTCPBlockSocket.Create; + try + LSocket.Socket := FListener.Accept; + while not Terminated do begin + if LSocket.CanRead(20) then LSocket.RecvPacket(20); + Sleep(1); + end; + finally LSocket.Free; end; +end; + +destructor TPeer.Destroy; +begin + Terminate; + WaitFor; + FListener.Free; + inherited; +end; + +constructor TFreeConnection.Create(AConnection: TNetConnection); +begin + FConnection := AConnection; + Done := TEvent.Create(nil,true,false,''); + inherited Create(false); + FreeOnTerminate := false; +end; + +procedure TFreeConnection.BCExecute; +begin + try + try FConnection.Free; + except on E: Exception do Error := E.ClassName+': '+E.Message; end; + finally Done.SetEvent; end; +end; + +destructor TFreeConnection.Destroy; +begin + WaitFor; + Done.Free; + inherited; +end; + +constructor TTimedLock.Create(AOwner: TNetData; AConnection: TNetConnection; ATimeout: Cardinal); +begin + FOwner := AOwner; + FConnection := AConnection; + FTimeout := ATimeout; + Done := TEvent.Create(nil,true,false,''); + inherited Create(false); + FreeOnTerminate := false; +end; + +procedure TTimedLock.BCExecute; +var LStart: TTickCount; +begin + LStart := TPlatform.GetTickCount; + try + try + Acquired := FOwner.ConnectionLock(Self,FConnection,FTimeout); + if Acquired then FOwner.ConnectionUnlock(FConnection); + except on E: Exception do Error := E.ClassName+': '+E.Message; end; + finally + Elapsed := TPlatform.GetElapsedMilliseconds(LStart); + Done.SetEvent; + end; +end; + +destructor TTimedLock.Destroy; +begin + WaitFor; + Done.Free; + inherited; +end; + +procedure CheckLifetime(AOwner: TNetData; ADepth: Integer; ATestTimeout: Boolean); +var S: TLegacyState; P: TPeer; C: TObservedConnection; + O: TDeathObserver; F: TFreeConnection; T: TTimedLock; + LHeld, I: Integer; + LPremature: Boolean; +begin + S := TLegacyState.Create; P := nil; C := nil; O := nil; F := nil; T := nil; + LHeld := 0; + try + P := TPeer.Create; + C := TObservedConnection.Create(S); + O := TDeathObserver.Create(S,C); + Require(C.ConnectTo('127.0.0.1',P.Port),'Actual loopback connection failed'); + for I := 1 to ADepth do begin + Require(AOwner.ConnectionLock(nil,C,100),'Legacy lock was not acquired'); + Inc(LHeld); + end; + if ATestTimeout then begin + T := TTimedLock.Create(AOwner,C,40); + Await(T.Done,'Timed admission did not return'); + Require(T.Error='','Timed admission raised: '+T.Error); + Require(not T.Acquired,'Writer acquired another thread''s held connection lock'); + Require((T.Elapsed>=40) and (T.Elapsed<500),'Timed admission exceeded its finite bound'); + FreeAndNil(T); + T := TTimedLock.Create(AOwner,C,0); + Await(T.Done,'Nonblocking admission did not return'); + Require((T.Error='') and not T.Acquired and (T.Elapsed<100),'Nonblocking admission was not finite'); + FreeAndNil(T); + end; + F := TFreeConnection.Create(C); + Await(S.Entered,'Actual retirement did not start'); + LPremature := S.Retired.WaitFor(200)=wrSignaled; + while LHeld>0 do begin + AOwner.ConnectionUnlock(C); + Dec(LHeld); + if (LHeld>0) and (S.Retired.WaitFor(100)=wrSignaled) then LPremature := true; + end; + Await(S.Retired,'Balanced releases did not retire the connection'); + S.AllowDestroy.SetEvent; + Await(F.Done,'Connection destruction did not complete'); + C := nil; + Require(F.Error='','Actual destruction raised: '+F.Error); + Require(Signaled(S.Gone),'External lifetime observer was not notified'); + Require(not LPremature,'Production retirement passed a successfully held legacy connection lock'); + finally + // Until AllowDestroy is set, the test gate keeps the real record/lock alive + // even on RED. Never unlock or inspect a pointer after its Gone notification. + while LHeld>0 do begin + AOwner.ConnectionUnlock(C); + Dec(LHeld); + end; + S.AllowDestroy.SetEvent; + if Assigned(F) then begin + F.Free; + C := nil; + end; + T.Free; + C.Free; + O.Free; + P.Free; + S.Free; + end; +end; + +function RunLegacyConnectionLifetimeCheck(ACheck: TLegacyConnectionLifetimeCheck; + const ALogFile: String): Integer; +var LLog: TStringList; LRoot: String; N: TNetData; LFixed: TNodeServerAddressArray; +begin + Result := 1; + Require(not FileExists(ALogFile),'Refusing an existing legacy lifetime log'); + LLog := TStringList.Create; N := nil; + try + try + if TNetData.NetDataExists then TNetData.NetData.BeginShutdown; + if TNode.NodeExists then TNode.Node.NetServer.Active := false; + if TNetData.NetDataExists then TNetData.NetData.Free; + if TNode.NodeExists then TNode.Node.Free; + TCrypto.InitCrypto; + LRoot := ALogFile+'-data'; + Require(not DirectoryExists(LRoot),'Refusing existing generated data'); + ForceDirectories(LRoot); + TNode.SetPascalCoinDataFolder(LRoot); + N := TNetData.NetData; + N.NetConnectionsActive := false; + SetLength(LFixed,1); LFixed[0] := CT_TNodeServerAddress_NUL; + LFixed[0].ip := '127.0.0.1'; LFixed[0].port := 1; + N.DiscoverFixedServersOnly(LFixed); + TNode.Node.UpdateBlockchain := false; + TNode.Node.NetServer.Port := 0; + N.NetConnectionsActive := true; + case ACheck of + llcHeldLock: begin + CheckLifetime(N,1,false); + LLog.Add('PASS actual retirement waits for legacy lock release'); + end; + llcRecursiveLock: begin + CheckLifetime(N,2,false); + LLog.Add('PASS nested legacy locks retain lifetime until both releases'); + end; + llcTimedAdmission: begin + CheckLifetime(N,1,true); + LLog.Add('PASS timeout/nonblocking failures release admission pins'); + end; + end; + Result := 0; + except on E: Exception do LLog.Add('FAIL '+E.ClassName+': '+E.Message); end; + finally + if Assigned(N) then begin + N.BeginShutdown; + N.Free; + end; + if TNode.NodeExists then TNode.Node.Free; + LLog.SaveToFile(ALogFile); + LLog.Free; + end; +end; + +function RunLegacyConnectionLifetimeChecks(const ALogFile: String): Integer; +var LCheck: TLegacyConnectionLifetimeCheck; LLog: TStringList; +begin + Result := 1; + Require(not FileExists(ALogFile),'Refusing an existing legacy lifetime summary'); + LLog := TStringList.Create; + try + for LCheck := Low(TLegacyConnectionLifetimeCheck) to High(TLegacyConnectionLifetimeCheck) do begin + if RunLegacyConnectionLifetimeCheck(LCheck,ALogFile+'.'+IntToStr(Ord(LCheck)+1)+'.log')<>0 then begin + LLog.Add(Format('FAIL %d; see scenario log',[Ord(LCheck)+1])); + LLog.SaveToFile(ALogFile); + Exit; + end; + LLog.Add(Format('PASS %d',[Ord(LCheck)+1])); + LLog.SaveToFile(ALogFile); + end; + Result := 0; + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.dpr b/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.dpr new file mode 100644 index 000000000..786a58ece --- /dev/null +++ b/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.dpr @@ -0,0 +1,16 @@ +program NodeSynchronizationCoreProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, UNodeSynchronizationChecks; +begin + if ParamCount <> 1 then Halt(64); + {$IFDEF FPC}SetHeapTraceOutput(ParamStr(1) + '.heap.log');{$ENDIF} + try + ExitCode := RunNodeSynchronizationChecks(ExpandFileName(ParamStr(1))); + except + on E: Exception do begin + WriteLn(E.ClassName, ': ', E.Message); + ExitCode := 1; + end; + end; +end. \ No newline at end of file diff --git a/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.dproj b/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.dproj new file mode 100644 index 000000000..91456b363 --- /dev/null +++ b/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.dproj @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{E52E7FA9-46B9-4979-9F70-66B1E75E4635}</ProjectGuid> + <MainSource>NodeSynchronizationCoreProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\node-sync-core\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\node-sync-core\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UNodeSynchronizationChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">NodeSynchronizationCoreProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.lpr b/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.lpr new file mode 100644 index 000000000..f74712748 --- /dev/null +++ b/src/tests/experimental/native/node-sync/NodeSynchronizationCoreProbe.lpr @@ -0,0 +1,13 @@ +program NodeSynchronizationCoreProbe; +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$APPTYPE CONSOLE} +uses {$IFDEF FPC}heaptrc,{$ENDIF} SysUtils, UNodeSynchronizationChecks; +begin + if ParamCount <> 1 then Halt(64); + {$IFDEF FPC}SetHeapTraceOutput(ParamStr(1) + '.heap.log');{$ENDIF} + try + ExitCode := RunNodeSynchronizationChecks(ExpandFileName(ParamStr(1))); + except + on E: Exception do begin WriteLn(E.ClassName, ': ', E.Message); ExitCode := 1; end; + end; +end. \ No newline at end of file diff --git a/src/tests/experimental/native/node-sync/README.md b/src/tests/experimental/native/node-sync/README.md new file mode 100644 index 000000000..916bdea2d --- /dev/null +++ b/src/tests/experimental/native/node-sync/README.md @@ -0,0 +1,52 @@ +# Node synchronization checks + +These probes generate state in a new folder under `TEMP`. They never touch +existing wallet data, open a network listener or connect to external peers. + +## Core lifecycle probe + +This probe mirrors the seventeen synchronization lifecycle cases of +[UNodeSynchronizationTests](../../core/UNodeSynchronizationTests.pas). The +[Delphi](NodeSynchronizationCoreProbe.dpr) and +[Free Pascal entry points](NodeSynchronizationCoreProbe.lpr) run the same +[checks](UNodeSynchronizationChecks.pas) without FPCUnit or wallet controls. + +Open [NodeSynchronizationCoreProbe.dproj](NodeSynchronizationCoreProbe.dproj) +in Delphi and build Win64 Debug. Output goes to +`src/tests/experimental/lib/node-sync-core/Delphi/<Platform>/<Config>`. Put +OpenSSL 1.1 beside the executable. + +Use a test folder for `TEMP`, `TMP` and the log. Pass one log-file +argument, or run the probe through the +[private-desktop broker](../README.md#private-desktop-broker) with its `EXE` and `LOG` +arguments. + +A successful run exits 0 and logs a pass per case. A check failure exits 1. + +The cases cover: + +- **Synchronization lifetime.** A selected target survives gaps between + batches. With the network off or no peers left, downloads stop and remote + targets clear. Synchronization still lasts until buffered work drains, even + across a reconnect. +- **Completion.** Reaching the target completes synchronization, and a failing + block listener cannot lose the final status. +- **Notifier order.** The synchronizing pause and storage errors precede block, + operation, message and key inputs. The offline resume follows them. +- **Storage failures.** After a failed checkpoint, block save or flush, the + node latches the first error until restart. It keeps earlier data and rejects + new work, but buffered work finishes. Listeners get the error on the main + thread without blocking the worker. Only storage errors latch. + +## Wallet probe + +[WalletNodeSynchronizationProbe](WalletNodeSynchronizationProbe.lpr) checks the +main form on a real node: synchronization cards, account-cache order, cancelled +grid reads and the storage-failure banner. +`../RunWalletRegressionTests.ps1 -Probe node-sync` builds and runs it through +the broker. + +## Recorded results + +On 30 October 2026, Delphi Win64 and FPC 3.2.0 builds passed the eight cases +then present, and FPC reported zero unfreed blocks. diff --git a/src/tests/experimental/native/node-sync/UNodeSynchronizationChecks.pas b/src/tests/experimental/native/node-sync/UNodeSynchronizationChecks.pas new file mode 100644 index 000000000..543f6c7de --- /dev/null +++ b/src/tests/experimental/native/node-sync/UNodeSynchronizationChecks.pas @@ -0,0 +1,1043 @@ +unit UNodeSynchronizationChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Plain native checks of the actual node synchronization facts and notifier. + Generated empty-node data and owned peer identities only; no sockets or keys + from an existing wallet are used. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunNodeSynchronizationChecks(const ALogPath: String): Integer; + +implementation + +uses {$IFNDEF FPC}Winapi.Windows,{$ELSE}Windows,{$ENDIF} + Classes, SysUtils, UNode, UAccounts, UBlockChain, + UCrypto, UNetProtocol, SyncObjs, UThread, UConst, UPCDataTypes, UOpTransaction, UFileStorage; + +type + TNodeSynchronizationChecks = class + private + FNode: TNode; + FNotify: TNodeNotifyEvents; + FWatch: TOrderedAccountKeysList; + FPeer, FSecondPeer: TObject; + FKey: TECPrivateKey; + FOperations: TOperationsHashTree; + FTrace: TStringList; + FLast: TNodeSynchronizationStatus; + FCallbacks: Integer; + FCallbackThread: UInt64; + FStopping: Boolean; + FExpectOperationCallback: Boolean; + FThrowNextBlock: Boolean; + FThrownBlockCallbacks: Integer; + FPreviousDirectory: String; + FDirectorySet: Boolean; + procedure Check(AValue: Boolean); overload; + procedure Check(const AMessage: String; AValue: Boolean); overload; + procedure CheckFalse(AValue: Boolean); overload; + procedure CheckFalse(const AMessage: String; AValue: Boolean); overload; + procedure CheckEqual(AExpected, AActual: Int64); overload; + procedure CheckEqual(const AMessage: String; AExpected, AActual: Int64); overload; + procedure CheckEqual(const AExpected, AActual: String); overload; + procedure Fail(const AMessage: String); + procedure OnSynchronization(Sender: TObject; const AStatus: TNodeSynchronizationStatus); + procedure OnBlocks(Sender: TObject); + procedure OnOperations(Sender: TObject); + procedure OnKeys(Sender: TObject); + procedure OnMessage(Sender: TNetConnection; AMessage: String); + procedure PumpFor(AMilliseconds: Cardinal); + procedure WaitForState(AState: TNodeSynchronizationState); + procedure StartSynchronizing; + procedure QueueBackendCallbacks; + procedure WaitForCombinedCallbacks; + protected + procedure SetUp; + procedure TearDown; + public + procedure SelectedTargetSurvivesInterBatchSilence; + procedure NetworkOffWaitsForBufferedWork; + procedure DisconnectClearsTargetsBeforeReconnect; + procedure SynchronizingCallbackPrecedesBackendInputs; + procedure OfflineCallbackFollowsEveryBackendInput; + procedure ActualWriteSummaryCompletesSelectedTarget; + procedure FinalStatusSurvivesSubscriberException; + procedure BufferedWorkSurvivesReconnectUntilEveryScopeDrains; + procedure StorageFailureLatchesFirstCauseUntilRestart; + procedure StorageFailureNotificationNeverWaitsForUi; + procedure StorageFailureRetiresBufferedWorkSafely; + procedure StorageFailurePrecedesBackendCallbacks; + procedure NonStorageExceptionsDoNotLatchFailure; + procedure ActualCheckpointSaveFailureLatchesCanonicalNode; + procedure ActualFalseBlockSaveLatchesWithoutAcknowledging; + procedure SafeboxFlushFailureLatchesBeforeUiDelivery; + procedure FalseBlockCacheFlushLatchesBeforeUiDelivery; + end; + + +type + TSynchronizationSafeBoxAccess = class(TPCSafeBox) + public + procedure AddGeneratedBlock(const AKey: TAccountKey); + end; + + TSynchronizationScopeThread = class(TPCThread) + private + FNode: TNode; + FPeer: TObject; + protected + procedure BCExecute; override; + public + Entered, ReleaseScope, Completed, ExerciseRetiredScope, RetiredScopeChecked: TEvent; + ErrorMessage: String; + constructor Create(ANode: TNode; APeer: TObject); + destructor Destroy; override; + end; + + TOwnedOutOfMemory = class(EOutOfMemory) + public + procedure FreeInstance; override; + end; + + TFalseBlockStorage = class(TFileStorage) + protected + function DoSaveBlockChain(Operations: TPCOperationsComp): Boolean; override; + public + SaveCalls: Integer; + end; + +type + TStorageFlushNode = class(TNode) + protected + procedure DoFlushSafebox; override; + function DoFlushBlockStorage: Boolean; override; + public + SafeboxFault: Integer; + BlockFlushFails: Boolean; + end; + +procedure TStorageFlushNode.DoFlushSafebox; +begin + case SafeboxFault of + 1: raise EAbort.Create('Synthetic flush cancellation'); + 2: raise EWriteError.Create('Synthetic safebox flush failure'); + else + inherited; + end; +end; + +function TStorageFlushNode.DoFlushBlockStorage: Boolean; +begin + if BlockFlushFails then Result := False + else Result := inherited DoFlushBlockStorage; +end; + +procedure TOwnedOutOfMemory.FreeInstance; +begin + // RTL heap exceptions normally represent protected preallocated instances. + // This synthetic test object is normally allocated and must own its cleanup. + CleanupInstance; + FreeMem(Pointer(Self)); +end; + +function TFalseBlockStorage.DoSaveBlockChain(Operations: TPCOperationsComp): Boolean; +begin + Inc(SaveCalls); + Result := False; +end; + +function ReadStorageFixture(const AFileName: String): TBytes; +var LFile: TFileStream; +begin + Result := Nil; + LFile := TFileStream.Create(AFileName,fmOpenRead+fmShareDenyNone); + try + SetLength(Result,LFile.Size); + if Length(Result)>0 then LFile.ReadBuffer(Result[0],Length(Result)); + finally + LFile.Free; + end; +end; + +function StorageFixtureEquals(const ALeft, ARight: TBytes): Boolean; +begin + Result := Length(ALeft)=Length(ARight); + if Result and (Length(ALeft)>0) then Result := CompareMem(@ALeft[0],@ARight[0],Length(ALeft)); +end; + +type + TStorageFailureThread = class(TPCThread) + private + FNode: TNode; + protected + procedure BCExecute; override; + public + Completed: TEvent; + ErrorText: String; + constructor Create(ANode: TNode); + destructor Destroy; override; + end; + +constructor TStorageFailureThread.Create(ANode: TNode); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + Completed := TEvent.Create(nil,True,False,''); +end; + +destructor TStorageFailureThread.Destroy; +begin + Terminate; + if Suspended then Start; + WaitFor; + Completed.Free; + inherited; +end; + +procedure TStorageFailureThread.BCExecute; +var LError: EWriteError; +begin + try + try + LError := EWriteError.Create('owned injected disk full'); + try + FNode.ReportStorageFailure(sfsCheckpoint,'owned-checkpoint.am_safebox',LError); + finally + LError.Free; + end; + except + on E: Exception do ErrorText := E.ClassName+': '+E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TSynchronizationScopeThread.Create(ANode: TNode; APeer: TObject); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FPeer := APeer; + Entered := TEvent.Create(nil,True,False,''); + ReleaseScope := TEvent.Create(nil,True,False,''); + Completed := TEvent.Create(nil,True,False,''); + ExerciseRetiredScope := TEvent.Create(nil,True,False,''); + RetiredScopeChecked := TEvent.Create(nil,True,False,''); +end; + +destructor TSynchronizationScopeThread.Destroy; +begin + Terminate; + ReleaseScope.SetEvent; + if Suspended then Start; + WaitFor; + RetiredScopeChecked.Free; + ExerciseRetiredScope.Free; + Completed.Free; + ReleaseScope.Free; + Entered.Free; + inherited; +end; + +procedure TSynchronizationScopeThread.BCExecute; +var LProgress: TProgressInfo; +begin + try + try + if Terminated then Exit; + FNode.BeginBlockChainUpdate; + try + Entered.SetEvent; + while not Terminated do begin + if ExerciseRetiredScope.WaitFor(0) = wrSignaled then begin + ExerciseRetiredScope.ResetEvent; + FNode.BeginBlockChainUpdate; + try + FNode.SetSynchronizationTarget(FPeer,1000); + LProgress := TProgressInfo.Create(psDownloadingBlockchain); + LProgress.BlockNumber := 777; + FNode.SetSynchronizationProgress(LProgress); + finally FNode.EndBlockChainUpdate; end; + RetiredScopeChecked.SetEvent; + end; + if ReleaseScope.WaitFor(10) = wrSignaled then Break; + end; + finally FNode.EndBlockChainUpdate; end; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally Completed.SetEvent; end; +end; + +procedure TSynchronizationSafeBoxAccess.AddGeneratedBlock(const AKey: TAccountKey); +var LBlock: TOperationBlock; +begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := BlocksCount; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + AddNew(LBlock); +end; + +procedure TNodeSynchronizationChecks.SetUp; +var LID: TGUID; LDirectory: String; LOperation: TPCOperation; I: Integer; +begin + CheckFalse('Each state fixture owns a fresh node',TNode.NodeExists); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + 'NodeSynchronization-' + GUIDToString(LID); + CheckFalse(DirectoryExists(LDirectory)); + ForceDirectories(LDirectory); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(LDirectory); + FDirectorySet := True; + TCrypto.InitCrypto; + FStopping := False; + FThrowNextBlock := False; + FThrownBlockCallbacks := 0; + FCallbacks := 0; + FTrace := TStringList.Create; + FPeer := TObject.Create; + FSecondPeer := TObject.Create; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FOperations := TOperationsHashTree.Create; + for I := 1 to 3 do begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_1,1000,I,1001, + FKey,100,1,CT_TOperationPayload_NUL); + try FOperations.AddOperationToHashTree(LOperation); + finally LOperation.Free; end; + end; + FNode := TStorageFlushNode.Create(nil); + FNode.BroadcastData := False; + CheckFalse('The state fixture never opens a listener',FNode.NetServer.Active); + FWatch := TOrderedAccountKeysList.Create(FNode.Bank.SafeBox,False); + FNotify := TNodeNotifyEvents.Create(nil); + FNotify.OnSynchronizationChanged := OnSynchronization; + FNotify.OnBlocksChanged := OnBlocks; + FNotify.OnOperationsChanged := OnOperations; + FNotify.OnKeyActivity := OnKeys; + FNotify.OnNodeMessageEvent := OnMessage; + FNotify.WatchKeys := FWatch; + FNotify.Node := FNode; + FNode.SetSynchronizationNetworkActive(False); + WaitForState(nssOffline); +end; + +procedure TNodeSynchronizationChecks.TearDown; +begin + FStopping := True; + if Assigned(FNode) then FNode.SetSynchronizationNetworkActive(False); + if Assigned(FNotify) then FNotify.Node := nil; + FreeAndNil(FNotify); + FreeAndNil(FWatch); + FreeAndNil(FNode); + FreeAndNil(FOperations); + FreeAndNil(FKey); + FreeAndNil(FSecondPeer); + FreeAndNil(FPeer); + FreeAndNil(FTrace); + if FDirectorySet then TNode.SetPascalCoinDataFolder(FPreviousDirectory); +end; + +procedure TNodeSynchronizationChecks.OnSynchronization(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); +begin + if FStopping then Exit; + FLast := AStatus; + Inc(FCallbacks); + FCallbackThread := TThread.CurrentThread.ThreadID; + FTrace.Add('sync'); +end; + +procedure TNodeSynchronizationChecks.OnBlocks(Sender: TObject); +begin + if FStopping then Exit; + FTrace.Add('blocks'); + if FThrowNextBlock then begin + FThrowNextBlock := False; + Inc(FThrownBlockCallbacks); + raise Exception.Create('Owned one-shot block subscriber failure'); + end; +end; + +procedure TNodeSynchronizationChecks.OnOperations(Sender: TObject); +begin + if not FStopping then FTrace.Add('operations'); +end; + +procedure TNodeSynchronizationChecks.OnKeys(Sender: TObject); +begin + if not FStopping then FTrace.Add('keys'); +end; + +procedure TNodeSynchronizationChecks.OnMessage(Sender: TNetConnection; AMessage: String); +begin + if not FStopping then FTrace.Add('message'); +end; + +procedure TNodeSynchronizationChecks.PumpFor(AMilliseconds: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LUntil; +end; + +procedure TNodeSynchronizationChecks.WaitForState(AState: TNodeSynchronizationState); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + 3000; + repeat + CheckSynchronize(0); + if (FCallbacks > 0) and (FLast.State = AState) then begin + CheckEqual('Existing notifier keeps UI listeners on their owner thread', + UInt64(MainThreadID),FCallbackThread); + Exit; + end; + Sleep(1); + until GetTickCount64 >= LUntil; + Fail('The actual node notifier did not publish its current synchronization state'); +end; + +procedure TNodeSynchronizationChecks.StartSynchronizing; +begin + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + FNode.SetSynchronizationTarget(FPeer,FNode.GetSynchronizationStatus.LocalBlockCount+10); + WaitForState(nssSynchronizing); +end; + +procedure TNodeSynchronizationChecks.SelectedTargetSurvivesInterBatchSilence; +var LStatus: TNodeSynchronizationStatus; +begin + StartSynchronizing; + FNode.BeginBlockChainUpdate; + FNode.EndBlockChainUpdate; + PumpFor(6200); + LStatus := FNode.GetSynchronizationStatus; + Check('No transfer activity is needed between batches',LStatus.ActiveTransfers = 0); + Check('The selected peer target survives a gap longer than five seconds',LStatus.HasRemoteTarget); + Check(LStatus.TargetBlockCount > LStatus.LocalBlockCount); + Check('Elapsed silence does not complete synchronization',LStatus.State = nssSynchronizing); + Check(FLast.State = nssSynchronizing); +end; + +procedure TNodeSynchronizationChecks.NetworkOffWaitsForBufferedWork; +var LStatus: TNodeSynchronizationStatus; +begin + StartSynchronizing; + FNode.BeginBlockChainUpdate; + try + FNode.SetSynchronizationNetworkActive(False); + LStatus := FNode.GetSynchronizationStatus; + Check('Already buffered work is still synchronization',LStatus.State = nssSynchronizing); + CheckFalse(LStatus.NetworkActive); + CheckFalse('Stopping transport discards the remote download target',LStatus.HasRemoteTarget); + CheckEqual(0,LStatus.PeerCount); + CheckEqual('The active physical scope is not retired by network off',1,LStatus.ActiveTransfers); + Check(FNode.IsUpdatingBlockChain); + WaitForState(nssSynchronizing); + finally FNode.EndBlockChainUpdate; end; + LStatus := FNode.GetSynchronizationStatus; + CheckEqual('Local availability resumes only after buffered work drains',0,LStatus.ActiveTransfers); + Check(LStatus.State = nssOffline); + WaitForState(nssOffline); +end; + +procedure TNodeSynchronizationChecks.DisconnectClearsTargetsBeforeReconnect; +var LStatus: TNodeSynchronizationStatus; +begin + StartSynchronizing; + FNode.SetSynchronizationPeerConnected(FSecondPeer,True); + FNode.SetSynchronizationTarget(FSecondPeer,5); + FNode.BeginBlockChainUpdate; + try + FNode.SetSynchronizationPeerConnected(FPeer,False); + Check(FNode.GetSynchronizationStatus.State = nssSynchronizing); + FNode.SetSynchronizationPeerConnected(FSecondPeer,False); + LStatus := FNode.GetSynchronizationStatus; + Check('No peers stops downloads but does not finish buffered work',LStatus.State = nssSynchronizing); + CheckFalse(LStatus.HasRemoteTarget); + CheckEqual(0,LStatus.PeerCount); + CheckEqual(1,LStatus.ActiveTransfers); + WaitForState(nssSynchronizing); + finally FNode.EndBlockChainUpdate; end; + WaitForState(nssDisconnected); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LStatus := FNode.GetSynchronizationStatus; + Check('Reconnect is available when no buffered work remains',LStatus.State = nssIdle); + CheckFalse('Reconnecting has no remembered target',LStatus.HasRemoteTarget); + WaitForState(nssIdle); +end; + +procedure TNodeSynchronizationChecks.QueueBackendCallbacks; +var LError: String; +begin + FWatch.Clear; + FNode.NotifyBlocksChanged; + // Exercise real admission as well: catch-up now reports unavailability before + // validating the absent senders, while the offline path rejects them normally. + FExpectOperationCallback := FNode.PendingOperationsAvailable; + if FExpectOperationCallback then + CheckEqual(0,FNode.AddOperations(nil,FOperations,nil,LError)) + else + Check(FNode.AddOperations(nil,FOperations,nil,LError)<0); + // Unavailable admission changes no operations and must not invent a pending + // data notification. The offline path still exercises that real callback. + FNode.NotifyNetClientMessage(nil,'owned notification-order check'); +end; + +procedure TNodeSynchronizationChecks.WaitForCombinedCallbacks; +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + 3000; + repeat + CheckSynchronize(0); + if (FTrace.IndexOf('sync') >= 0) and (FTrace.IndexOf('blocks') >= 0) and + ((not FExpectOperationCallback) or (FTrace.IndexOf('operations') >= 0)) and (FTrace.IndexOf('message') >= 0) and + (FTrace.IndexOf('keys') >= 0) then Exit; + Sleep(1); + until GetTickCount64 >= LUntil; + Fail('The actual combined notifier batch was incomplete: ' + FTrace.CommaText); +end; + +procedure TNodeSynchronizationChecks.SynchronizingCallbackPrecedesBackendInputs; +begin + StartSynchronizing; + PumpFor(200); + FTrace.Clear; + FNode.SetSynchronizationTarget(FPeer,20); + QueueBackendCallbacks; + WaitForCombinedCallbacks; + CheckEqual('Pause arrives before every backend request',0,FTrace.IndexOf('sync')); + CheckEqual('Unavailable admission must not publish an operation change',-1,FTrace.IndexOf('operations')); +end; + +procedure TNodeSynchronizationChecks.OfflineCallbackFollowsEveryBackendInput; +begin + StartSynchronizing; + PumpFor(200); + FTrace.Clear; + FNode.SetSynchronizationNetworkActive(False); + QueueBackendCallbacks; + WaitForCombinedCallbacks; + CheckEqual('Resume follows blocks, operations, messages and key activity', + FTrace.Count-1,FTrace.IndexOf('sync')); + Check(FLast.State = nssOffline); +end; + +procedure TNodeSynchronizationChecks.ActualWriteSummaryCompletesSelectedTarget; +var LMempool: TPCOperationsComp; LStatus: TNodeSynchronizationStatus; +begin + // Retain this empty fixture's pool before catch-up makes account scopes busy. + // Confirmed bank mutation uses the node's ordinary bank writer scope below. + LMempool := FNode.LockMempoolRead; + FNode.UnlockMempoolRead; + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + FNode.SetSynchronizationTarget(FPeer,1); + WaitForState(nssSynchronizing); + Check(FNode.TryLockNode(3000)); + try + TSynchronizationSafeBoxAccess(FNode.Bank.SafeBox).AddGeneratedBlock(FKey.PublicKey); + LMempool.Clear(True); + finally FNode.UnlockNode; end; + LStatus := FNode.GetSynchronizationStatus; + Check(LStatus.HasMempoolSummary); + CheckEqual(1,Int64(LStatus.LocalBlockCount)); + Check('Actual write release updates the cached local tip',LStatus.State = nssIdle); + WaitForState(nssIdle); +end; + +procedure TNodeSynchronizationChecks.BufferedWorkSurvivesReconnectUntilEveryScopeDrains; +var LOld, LNew: TSynchronizationScopeThread; LStatus: TNodeSynchronizationStatus; +begin + LOld := nil; + LNew := nil; + try + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LOld := TSynchronizationScopeThread.Create(FNode,FPeer); + LOld.Start; + CheckEqual(Ord(wrSignaled),Ord(LOld.Entered.WaitFor(3000))); + Check(FNode.GetSynchronizationStatus.State = nssSynchronizing); + FNode.SetSynchronizationPeerConnected(FPeer,False); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LStatus := FNode.GetSynchronizationStatus; + Check('Reconnect cannot finish already buffered processing',LStatus.State = nssSynchronizing); + CheckEqual(1,LStatus.ActiveTransfers); + Check(FNode.IsUpdatingBlockChain); + LOld.ExerciseRetiredScope.SetEvent; + CheckEqual(Ord(wrSignaled),Ord(LOld.RetiredScopeChecked.WaitFor(3000))); + LStatus := FNode.GetSynchronizationStatus; + Check('Nested buffered work preserves synchronization',LStatus.State = nssSynchronizing); + CheckEqual('The completed nested scope retains its outer processing scope',1,LStatus.ActiveTransfers); + CheckFalse('Stale producers cannot restore a peer download target',LStatus.HasRemoteTarget); + LNew := TSynchronizationScopeThread.Create(FNode,FPeer); + LNew.Start; + CheckEqual(Ord(wrSignaled),Ord(LNew.Entered.WaitFor(3000))); + CheckEqual('Both physical processing scopes remain active',2,FNode.GetSynchronizationStatus.ActiveTransfers); + LOld.ReleaseScope.SetEvent; + CheckEqual(Ord(wrSignaled),Ord(LOld.Completed.WaitFor(3000))); + CheckEqual('',LOld.ErrorMessage); + LStatus := FNode.GetSynchronizationStatus; + CheckEqual('An old End cannot release the new processing scope',1,LStatus.ActiveTransfers); + Check(LStatus.State = nssSynchronizing); + LNew.ReleaseScope.SetEvent; + CheckEqual(Ord(wrSignaled),Ord(LNew.Completed.WaitFor(3000))); + CheckEqual('',LNew.ErrorMessage); + CheckEqual(0,FNode.GetSynchronizationStatus.ActiveTransfers); + Check(FNode.GetSynchronizationStatus.State = nssIdle); + CheckFalse(FNode.IsUpdatingBlockChain); + WaitForState(nssIdle); + finally + LNew.Free; + LOld.Free; + end; +end; + +procedure TNodeSynchronizationChecks.StorageFailureLatchesFirstCauseUntilRestart; +var LError: Exception; LStatus: TNodeSynchronizationStatus; LProgress: TProgressInfo; + LReason: String; LMempool, LBlock: TPCOperationsComp; LRaised: Boolean; +begin + LError := EWriteError.Create('owned first storage failure'); + try + FNode.ReportStorageFailure(sfsCheckpoint,'first-checkpoint.am_safebox',LError); + finally + LError.Free; + end; + LError := EOSError.Create('owned later failure'); + try + FNode.ReportStorageFailure(sfsBlockStream,'later.blocks',LError); + finally + LError.Free; + end; + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + FNode.SetSynchronizationTarget(FPeer,1000); + LProgress := TProgressInfo.Create(psDownloadingBlockchain); + LProgress.BlockNumber := 999; + FNode.SetSynchronizationProgress(LProgress); + FNode.BeginBlockChainUpdate; + FNode.EndBlockChainUpdate; + LStatus := FNode.GetSynchronizationStatus; + Check(FNode.HasStorageFailure); + Check(LStatus.State=nssStorageError); + Check(LStatus.StorageFailure.Stage=sfsCheckpoint); + CheckEqual('first-checkpoint.am_safebox',LStatus.StorageFailure.FileName); + CheckEqual('EWriteError',LStatus.StorageFailure.ErrorClass); + CheckEqual('owned first storage failure',LStatus.StorageFailure.ErrorMessage); + CheckFalse(FNode.CanRequestBlockChainData); + CheckFalse(FNode.PendingOperationsAvailable); + CheckFalse(FNode.IsReady(LReason)); + CheckEqual('owned first storage failure',LReason); + CheckFalse(FNode.IsBlockChainValid(LReason)); + CheckEqual('owned first storage failure',LReason); + Check(FNode.AddOperations(nil,FOperations,nil,LReason)<0); + CheckEqual('owned first storage failure',LReason); + LBlock := TPCOperationsComp.Create(FNode.Bank); + try + CheckFalse(FNode.AddNewBlockChain(nil,LBlock,LReason)); + CheckEqual('owned first storage failure',LReason); + finally + LBlock.Free; + end; + CheckFalse(FNode.TryLockMempoolRead(0,LMempool)); + LRaised := False; + try + LMempool := FNode.LockMempoolWrite; + FNode.UnlockMempoolWrite; + except + on E: ENodeStorageUnavailable do LRaised := True; + end; + Check('Storage failure prevents new operation writers',LRaised); + CheckEqual(0,Int64(FNode.Bank.BlocksCount)); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationChecks.StorageFailureNotificationNeverWaitsForUi; +var LThread: TStorageFailureThread; LBefore: Integer; +begin + LBefore := FCallbacks; + LThread := TStorageFailureThread.Create(FNode); + try + LThread.Start; + // No CheckSynchronize here: a producer must finish even while its actual + // main-thread subscriber cannot run. The event record owns copied strings. + CheckEqual(Ord(wrSignaled),Ord(LThread.Completed.WaitFor(3000))); + CheckEqual('',LThread.ErrorText); + CheckEqual(LBefore,FCallbacks); + Check(FNode.GetSynchronizationStatus.State=nssStorageError); + WaitForState(nssStorageError); + CheckEqual('owned injected disk full',FLast.StorageFailure.ErrorMessage); + finally + LThread.Free; + end; +end; + +procedure TNodeSynchronizationChecks.StorageFailureRetiresBufferedWorkSafely; +var LThread: TSynchronizationScopeThread; LError: Exception; +begin + FNode.SetSynchronizationNetworkActive(True); + FNode.SetSynchronizationPeerConnected(FPeer,True); + LThread := TSynchronizationScopeThread.Create(FNode,FPeer); + try + LThread.Start; + CheckEqual(Ord(wrSignaled),Ord(LThread.Entered.WaitFor(3000))); + LError := EWriteError.Create('owned buffered storage failure'); + try + FNode.ReportStorageFailure(sfsBlockStream,'owned.blocks',LError); + finally + LError.Free; + end; + CheckEqual(1,FNode.GetSynchronizationStatus.ActiveTransfers); + Check('Failure has precedence while physical work retires',FNode.GetSynchronizationStatus.State=nssStorageError); + CheckFalse(FNode.CanRequestBlockChainData); + LThread.ReleaseScope.SetEvent; + CheckEqual(Ord(wrSignaled),Ord(LThread.Completed.WaitFor(3000))); + CheckEqual('',LThread.ErrorMessage); + CheckEqual(0,FNode.GetSynchronizationStatus.ActiveTransfers); + Check('Draining work never clears the storage failure',FNode.GetSynchronizationStatus.State=nssStorageError); + WaitForState(nssStorageError); + finally + LThread.Free; + end; +end; + +procedure TNodeSynchronizationChecks.StorageFailurePrecedesBackendCallbacks; +var LError: Exception; +begin + FTrace.Clear; + LError := EWriteError.Create('owned callback-order storage failure'); + try + FNode.ReportStorageFailure(sfsCheckpoint,'owned-checkpoint',LError); + finally + LError.Free; + end; + QueueBackendCallbacks; + WaitForCombinedCallbacks; + CheckEqual('Storage error is delivered before backend refresh requests',0,FTrace.IndexOf('sync')); + Check(FLast.State=nssStorageError); + CheckEqual(-1,FTrace.IndexOf('operations')); +end; + +procedure TNodeSynchronizationChecks.NonStorageExceptionsDoNotLatchFailure; +var LError: Exception; I: Integer; +begin + for I := 0 to 2 do begin + case I of + 0: LError := TOwnedOutOfMemory.Create('owned exhaustion'); + 1: LError := EAbort.Create('owned cancellation'); + else + LError := EAccessViolation.Create('owned programmer fault'); + end; + try + FNode.ReportStorageFailure(sfsUnknown,'not-a-storage-failure',LError); + finally + LError.Free; + end; + CheckFalse(FNode.HasStorageFailure); + Check(FNode.GetSynchronizationStatus.State=nssOffline); + end; +end; + +procedure TNodeSynchronizationChecks.ActualCheckpointSaveFailureLatchesCanonicalNode; +var LDirectory, LDestination, LPrevious, LReason: String; + LBefore: TBytes; LFailed: Boolean; LStatus: TNodeSynchronizationStatus; + LIncoming: TPCOperationsComp; +begin + FNode.Bank.StorageClass := TFileStorage; + TSynchronizationSafeBoxAccess(FNode.Bank.SafeBox).AddGeneratedBlock(FKey.PublicKey); + LDirectory := FNode.Bank.GetStorageFolder(''); + LPrevious := LDirectory+PathDelim+'previous.am_safebox'; + FNode.Bank.SafeBox.SaveCheckpointing(LPrevious); + LBefore := ReadStorageFixture(LPrevious); + LDestination := FNode.Bank.GetSafeboxCheckpointingFileName(LDirectory,FNode.Bank.BlocksCount); + Check(ForceDirectories(LDestination)); + LFailed := False; + try + FNode.Bank.SaveBank(True); + except + on E: EOSError do LFailed := True; + end; + Check('Real atomic publication to a directory must fail',LFailed); + LStatus := FNode.GetSynchronizationStatus; + Check(LStatus.State=nssStorageError); + Check(LStatus.StorageFailure.Stage=sfsCheckpoint); + CheckEqual(LDestination,LStatus.StorageFailure.FileName); + Check('The complete earlier checkpoint is preserved',StorageFixtureEquals(LBefore,ReadStorageFixture(LPrevious))); + Check(DirectoryExists(LDestination)); + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + CheckFalse(FNode.AddNewBlockChain(nil,LIncoming,LReason)); + CheckFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + finally + LIncoming.Free; + end; + CheckEqual(1,Int64(FNode.Bank.BlocksCount)); + CheckFalse(FNode.CanRequestBlockChainData); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationChecks.ActualFalseBlockSaveLatchesWithoutAcknowledging; +var LPrevious, LReason: String; LBefore: TBytes; LFailed: Boolean; + LIncoming: TPCOperationsComp; LStorage: TFalseBlockStorage; + LCount, I: Integer; LStatus: TNodeSynchronizationStatus; +begin + LPrevious := FNode.Bank.GetStorageFolder('')+PathDelim+'previous-empty.am_safebox'; + FNode.Bank.SafeBox.SaveCheckpointing(LPrevious); + LBefore := ReadStorageFixture(LPrevious); + FNode.Bank.StorageClass := TFalseBlockStorage; + LStorage := TFalseBlockStorage(FNode.Bank.Storage); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + for I := 0 to FOperations.OperationsCount-1 do + LIncoming.OperationsHashTree.AddOperationToHashTree(FOperations.GetOperation(I)); + LCount := LIncoming.Count; + Check(LCount>0); + LFailed := False; + try + FNode.Bank.Storage.SaveBlockChainBlock(LIncoming); + except + on E: EWriteError do LFailed := True; + end; + Check('False storage acknowledgement must become a real write error',LFailed); + CheckEqual(1,LStorage.SaveCalls); + CheckEqual(LCount,LIncoming.Count); + LStatus := FNode.GetSynchronizationStatus; + Check(LStatus.State=nssStorageError); + Check(LStatus.StorageFailure.Stage=sfsBlockStream); + Check(Pos('BlockChainStream.blocks',LStatus.StorageFailure.FileName)>0); + Check('Earlier checkpoint bytes remain intact',StorageFixtureEquals(LBefore,ReadStorageFixture(LPrevious))); + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + CheckFalse(FNode.AddNewBlockChain(nil,LIncoming,LReason)); + CheckFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + CheckEqual(1,LStorage.SaveCalls); + CheckEqual(LCount,LIncoming.Count); + CheckEqual(0,Int64(FNode.Bank.BlocksCount)); + WaitForState(nssStorageError); + finally + LIncoming.Free; + end; +end; + +procedure TNodeSynchronizationChecks.SafeboxFlushFailureLatchesBeforeUiDelivery; +var LFileName, LReason: String; LStatus: TNodeSynchronizationStatus; + LIncoming: TPCOperationsComp; LBefore: TBytes; +begin + LFileName := FNode.Bank.GetStorageFolder('')+PathDelim+'flush-source.am_safebox'; + FNode.Bank.SafeBox.SetSafeboxFileName(LFileName); + LBefore := ReadStorageFixture(LFileName); + TStorageFlushNode(FNode).SafeboxFault := 1; + try + FNode.FlushSafebox; + Fail('Synthetic cancellation must propagate'); + except + on E: EAbort do CheckEqual('Synthetic flush cancellation',E.Message); + end; + CheckFalse('Cancellation cannot latch a storage error',FNode.HasStorageFailure); + TStorageFlushNode(FNode).SafeboxFault := 2; + try + FNode.FlushSafebox; + Fail('Synthetic safebox write error must propagate'); + except + on E: EWriteError do CheckEqual('Synthetic safebox flush failure',E.Message); + end; + // No UI pump occurred between the actual flush boundary and these guards. + LStatus := FNode.GetSynchronizationStatus; + Check(LStatus.State=nssStorageError); + Check(LStatus.StorageFailure.Stage=sfsSafeboxFlush); + CheckEqual(LFileName,LStatus.StorageFailure.FileName); + CheckFalse(FNode.CanRequestBlockChainData); + Check('The pre-flush source bytes remain intact',StorageFixtureEquals(LBefore,ReadStorageFixture(LFileName))); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + CheckFalse(FNode.AddNewBlockChain(Nil,LIncoming,LReason)); + CheckFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + finally + LIncoming.Free; + end; + TStorageFlushNode(FNode).SafeboxFault := 0; + FNode.FlushSafebox; + FNode.SetSynchronizationNetworkActive(True); + CheckEqual('Synthetic safebox flush failure',FNode.GetSynchronizationStatus.StorageFailure.ErrorMessage); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationChecks.FalseBlockCacheFlushLatchesBeforeUiDelivery; +var LStatus: TNodeSynchronizationStatus; LReason: String; + LIncoming: TPCOperationsComp; +begin + FNode.Bank.StorageClass := TFileStorage; + TStorageFlushNode(FNode).BlockFlushFails := True; + try + FNode.FlushBlockStorage; + Fail('False block-cache flush must become a write error'); + except + on E: EWriteError do Check(Pos('Cannot flush blockchain storage',E.Message)>0); + end; + LStatus := FNode.GetSynchronizationStatus; + Check(LStatus.State=nssStorageError); + Check(LStatus.StorageFailure.Stage=sfsBlockStream); + CheckEqual('EWriteError',LStatus.StorageFailure.ErrorClass); + CheckFalse(FNode.CanRequestBlockChainData); + LIncoming := TPCOperationsComp.Create(FNode.Bank); + try + CheckFalse(FNode.AddNewBlockChain(Nil,LIncoming,LReason)); + CheckFalse(FNode.Bank.AddNewBlockChainBlock(LIncoming,High(Cardinal),LReason)); + finally + LIncoming.Free; + end; + TStorageFlushNode(FNode).BlockFlushFails := False; + FNode.FlushBlockStorage; + FNode.SetSynchronizationNetworkActive(False); + FNode.SetSynchronizationNetworkActive(True); + CheckEqual(LStatus.StorageFailure.ErrorMessage,FNode.GetSynchronizationStatus.StorageFailure.ErrorMessage); + WaitForState(nssStorageError); +end; + +procedure TNodeSynchronizationChecks.FinalStatusSurvivesSubscriberException; +begin + StartSynchronizing; + PumpFor(200); + FTrace.Clear; + FThrowNextBlock := True; + // Both flags are set without pumping the UI. The actual notifier must run + // the final status callback even when the preceding block subscriber fails. + FNode.SetSynchronizationNetworkActive(False); + FNode.NotifyBlocksChanged; + WaitForState(nssOffline); + CheckEqual('The expected subscriber failure was exercised exactly once',1,FThrownBlockCallbacks); + Check('No additional producer event was needed to deliver final status', + (FTrace.IndexOf('blocks') >= 0) and (FTrace.IndexOf('sync') > FTrace.IndexOf('blocks'))); + CheckEqual(0,FNode.GetSynchronizationStatus.ActiveTransfers); +end; + +procedure TNodeSynchronizationChecks.Check(AValue: Boolean); +begin + Check('Synchronization assertion failed', AValue); +end; + +procedure TNodeSynchronizationChecks.Check(const AMessage: String; AValue: Boolean); +begin + if not AValue then Fail(AMessage); +end; + +procedure TNodeSynchronizationChecks.CheckFalse(AValue: Boolean); +begin + Check(not AValue); +end; + +procedure TNodeSynchronizationChecks.CheckFalse(const AMessage: String; AValue: Boolean); +begin + Check(AMessage, not AValue); +end; + +procedure TNodeSynchronizationChecks.CheckEqual(AExpected, AActual: Int64); +begin + CheckEqual('Synchronization values differ', AExpected, AActual); +end; + +procedure TNodeSynchronizationChecks.CheckEqual(const AMessage: String; + AExpected, AActual: Int64); +begin + Check(Format('%s: expected %d, got %d', [AMessage, AExpected, AActual]), AExpected = AActual); +end; + +procedure TNodeSynchronizationChecks.CheckEqual(const AExpected, AActual: String); +begin + Check('Expected "' + AExpected + '", got "' + AActual + '"', AExpected = AActual); +end; + +procedure TNodeSynchronizationChecks.Fail(const AMessage: String); +begin + raise Exception.Create(AMessage); +end; + +function RunNodeSynchronizationChecks(const ALogPath: String): Integer; +const CNames: array[0..16] of String = ( + 'Selected target survives 6200 ms between batches', + 'Network off keeps synchronization until buffered work drains', + 'Last disconnect clears targets and waits for buffered work', + 'Synchronization pause precedes all backend inputs', + 'Offline resume follows blocks, operations, messages and keys', + 'Actual write summary completes the selected target', + 'Buffered work survives reconnect until every physical scope drains', + 'Final status survives a failing backend subscriber without new input', + 'Storage failure retains first cause and rejects admission until restart', + 'Storage failure producer never waits for UI delivery', + 'Storage failure lets owned buffered scopes retire without resuming', + 'Storage failure precedes every backend refresh callback', + 'Cancellation exhaustion and programmer faults are not storage errors', + 'Actual canonical checkpoint publication failure latches until restart', + 'False actual block storage acknowledgement latches without clearing operations', + 'Post-commit safebox flush failure latches before UI delivery', + 'False post-batch block-cache flush latches before UI delivery'); +var LLog: TStringList; LChecks: TNodeSynchronizationChecks; + I: Integer; LStarted: UInt64; +begin + Result := 1; + LLog := TStringList.Create; + try + try + for I := Low(CNames) to High(CNames) do begin + LLog.Add('START ' + CNames[I]); + LLog.SaveToFile(ALogPath); + LStarted := GetTickCount64; + LChecks := TNodeSynchronizationChecks.Create; + try + try + LChecks.SetUp; + case I of + 0: LChecks.SelectedTargetSurvivesInterBatchSilence; + 1: LChecks.NetworkOffWaitsForBufferedWork; + 2: LChecks.DisconnectClearsTargetsBeforeReconnect; + 3: LChecks.SynchronizingCallbackPrecedesBackendInputs; + 4: LChecks.OfflineCallbackFollowsEveryBackendInput; + 5: LChecks.ActualWriteSummaryCompletesSelectedTarget; + 6: LChecks.BufferedWorkSurvivesReconnectUntilEveryScopeDrains; + 7: LChecks.FinalStatusSurvivesSubscriberException; + 8: LChecks.StorageFailureLatchesFirstCauseUntilRestart; + 9: LChecks.StorageFailureNotificationNeverWaitsForUi; + 10: LChecks.StorageFailureRetiresBufferedWorkSafely; + 11: LChecks.StorageFailurePrecedesBackendCallbacks; + 12: LChecks.NonStorageExceptionsDoNotLatchFailure; + 13: LChecks.ActualCheckpointSaveFailureLatchesCanonicalNode; + 14: LChecks.ActualFalseBlockSaveLatchesWithoutAcknowledging; + 15: LChecks.SafeboxFlushFailureLatchesBeforeUiDelivery; + 16: LChecks.FalseBlockCacheFlushLatchesBeforeUiDelivery; + end; + finally + LChecks.TearDown; + end; + finally + LChecks.Free; + end; + LLog.Add(Format('PASS %s (%d ms)', [CNames[I], GetTickCount64 - LStarted])); + LLog.SaveToFile(ALogPath); + end; + LLog.Add('PASS all seventeen actual core synchronization lifecycle checks'); + Result := 0; + except + on E: Exception do LLog.Add('FAIL ' + E.ClassName + ': ' + E.Message); + end; + LLog.SaveToFile(ALogPath); + finally + LLog.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/node-sync/UWalletNodeSynchronizationChecks.pas b/src/tests/experimental/native/node-sync/UWalletNodeSynchronizationChecks.pas new file mode 100644 index 000000000..4bf1f0ca6 --- /dev/null +++ b/src/tests/experimental/native/node-sync/UWalletNodeSynchronizationChecks.pas @@ -0,0 +1,1093 @@ +unit UWalletNodeSynchronizationChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Actual main-form/provider wiring with generated core accounts. The only + artificial delay is inside inherited account membership, after real core + locks have been acquired. No row, snapshot, grid refresh or lock is mocked. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunWalletNodeSynchronizationProbe: Integer; + +implementation + +uses Windows, Classes, SysUtils, SyncObjs, Forms, StdCtrls, Variants, Graphics, Controls, ExtCtrls, Types, + {$IFDEF FPC}LMessages,{$ENDIF} + UNode, UNetProtocol, UThread, UAccounts, UPCDataTypes, UBlockChain, UCrypto, + UConst, UOpTransaction, UWallet, UUserInterface, UWalletStartup, + UFRMMainForm, UCTRLWallet, UDataSources, UCoreObjects, UCommon.Data, + UVisualGrid, UMemory, UCommon.UI, UPCAbstractMem, UProgressDisplay, UCTRLBanner; + +type + TQueryKind = (qkPage, qkAggregate); + + TQueryGate = class + public + Armed: LongInt; + Entered, ReleaseRead, CancelObserved, Exited: TEvent; + constructor Create; + destructor Destroy; override; + procedure Arm; + procedure Hold; + end; + + TCoreQueryObservations = class + public + Gates: array[TQueryKind] of TQueryGate; + AccountQueries, OperationQueries, AggregateMemberships, BalanceReads: LongInt; + LastTotal, LastPageIndex, LastRequestedPageIndex, LastPageSize: Integer; + FirstAccount, MinimumAccount, MaximumAccount: Cardinal; + LastRowCount: Integer; + RealMempoolObserved: Boolean; + constructor Create; + destructor Destroy; override; + procedure ReleaseAll; + end; + + TObservedCoreAccounts = class(TMyAccountsDataSource) + private + FObserved: TCoreQueryObservations; + protected + function OwnershipChanges(const AContext: TAccountReadContext): TArray<TAccountOwnershipChange>; override; + function ReadBalanceAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; override; + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; override; + public + constructor CreateObserved(AOwner: TComponent; AObserved: TCoreQueryObservations); + end; + + TObservedCoreOperations = class(TAccountsOperationsDataSource) + private + FObserved: TCoreQueryObservations; + protected + function FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; override; + public + constructor CreateObserved(AOwner: TComponent; AObserved: TCoreQueryObservations); + end; + + TCoreSynchronizationWallet = class(TCTRLWallet) + private + FObserved: TCoreQueryObservations; + FCoreAccounts: TObservedCoreAccounts; + protected + function CreateAccountsDataSource: TMyAccountsDataSource; override; + function CreateOperationsDataSource: TAccountsOperationsDataSource; override; + public + constructor CreateObserved(AOwner: TComponent; AObserved: TCoreQueryObservations); + property AccountsGrid; + property OperationsGrid; + property CoreAccounts: TObservedCoreAccounts read FCoreAccounts; + end; + + TCoreSynchronizationMain = class(TFRMMainForm) + private + FObserved: TCoreQueryObservations; + FSynchronizeBeforeFirstWallet, FInitialSynchronizationActive: Boolean; + function GetCoreWallet: TCoreSynchronizationWallet; + protected + function CreateWalletControl: TCTRLWallet; override; + public + procedure EndInitialSynchronization; + property Observed: TCoreQueryObservations read FObserved write FObserved; + property SynchronizeBeforeFirstWallet: Boolean read FSynchronizeBeforeFirstWallet write FSynchronizeBeforeFirstWallet; + property InitialSynchronizationActive: Boolean read FInitialSynchronizationActive; + property CoreWallet: TCoreSynchronizationWallet read GetCoreWallet; + end; + + TGeneratedSafeBoxAccess = class(TPCSafeBox) + public + procedure Seed(const ALargeKey, ASmallKey: TAccountKey); + end; + + TActualNodeWriter = class(TPCThread) + private + FNode: TNode; + FTree: TOperationsHashTree; + procedure OnProgress(Sender: TObject; const AProgress: TProgressInfo); + protected + procedure BCExecute; override; + public + Prepared, Completed: TEvent; + ErrorMessage: String; + constructor Create(ANode: TNode; AKey: TECPrivateKey); + destructor Destroy; override; + end; + + TStorageFailureReporter = class(TPCThread) + private + FNode: TNode; + FFileName: String; + protected + procedure BCExecute; override; + public + Completed: TEvent; + ErrorMessage: String; + constructor Create(ANode: TNode; const AFileName: String); + destructor Destroy; override; + end; + + TWalletNodeSynchronizationChecks = class + private + FMain: TCoreSynchronizationMain; + FObserved: TCoreQueryObservations; + FKey, FSmallKey: TECPrivateKey; + FPeer: TObject; + FLog: TStringList; + FLogPath, FAsyncError, FPhase: String; + FPreviousException: TExceptionEvent; + procedure Log(const AText: String); + procedure OnException(Sender: TObject; E: Exception); + procedure Pump; + procedure PumpFor(AMilliseconds: Cardinal); + procedure WaitEvent(AEvent: TEvent; const ADescription: String; ATimeout: Cardinal = 5000); + procedure WaitIdle; + procedure WaitSuspended(AValue: Boolean); + procedure StartSynchronization; + procedure RequireBannerStatus(const ACheckpoint: String; const AExpected: String); + procedure RequireSynchronizationCards(const ACheckpoint: String; ACapture: Boolean = False); + procedure CheckInitialSynchronization; + procedure CheckAccountCacheOrdering; + procedure SeedActualAccounts; + procedure ChooseSmallKey; + procedure CheckHeldRead(AKind: TQueryKind); + procedure CheckStorageFailure; + public + constructor Create(const ALogPath: String); + destructor Destroy; override; + procedure Run; + end; + +threadvar + AccountPageSource: Pointer; + SeedBlockIndex: Integer; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +constructor TQueryGate.Create; +begin + inherited; + Entered := TEvent.Create(nil,True,False,''); + ReleaseRead := TEvent.Create(nil,True,False,''); + CancelObserved := TEvent.Create(nil,True,False,''); + Exited := TEvent.Create(nil,True,False,''); +end; + +destructor TQueryGate.Destroy; +begin + ReleaseRead.Free; + CancelObserved.Free; + Exited.Free; + Entered.Free; + inherited; +end; + +procedure TQueryGate.Arm; +begin + Entered.ResetEvent; + ReleaseRead.ResetEvent; + CancelObserved.ResetEvent; + Exited.ResetEvent; + InterlockedExchange(Armed,1); +end; + +procedure TQueryGate.Hold; +begin + if InterlockedCompareExchange(Armed,0,1) <> 1 then Exit; + Entered.SetEvent; + while ReleaseRead.WaitFor(10) <> wrSignaled do + if TThread.CheckTerminated then begin + CancelObserved.SetEvent; + Break; + end; +end; + +constructor TCoreQueryObservations.Create; +var K: TQueryKind; +begin + inherited; + for K := Low(TQueryKind) to High(TQueryKind) do Gates[K] := TQueryGate.Create; +end; + +destructor TCoreQueryObservations.Destroy; +var K: TQueryKind; +begin + for K := Low(TQueryKind) to High(TQueryKind) do Gates[K].Free; + inherited; +end; + +procedure TCoreQueryObservations.ReleaseAll; +var K: TQueryKind; +begin + for K := Low(TQueryKind) to High(TQueryKind) do Gates[K].ReleaseRead.SetEvent; +end; + +constructor TObservedCoreAccounts.CreateObserved(AOwner: TComponent; AObserved: TCoreQueryObservations); +begin + FObserved := AObserved; + inherited Create(AOwner); +end; + +function TObservedCoreAccounts.OwnershipChanges(const AContext: TAccountReadContext): TArray<TAccountOwnershipChange>; +var K: TQueryKind; +begin + if AccountPageSource = Pointer(Self) then K := qkPage else begin + K := qkAggregate; + InterlockedIncrement(FObserved.AggregateMemberships); + end; + FObserved.RealMempoolObserved := Assigned(AContext.Mempool); + // The production FetchPage/CalculateBalance try/finally is already active. + // Cancellation returns through inherited membership and releases real locks. + FObserved.Gates[K].Hold; + Result := inherited OwnershipChanges(AContext); +end; + +function TObservedCoreAccounts.ReadBalanceAccount(AAccountNumber: Cardinal; + const AContext: TAccountReadContext): TAccount; +begin + InterlockedIncrement(FObserved.BalanceReads); + Result := inherited ReadBalanceAccount(AAccountNumber,AContext); +end; + +function TObservedCoreAccounts.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; +var LPrevious: Pointer; I: Integer; LAccount: Cardinal; +begin + InterlockedIncrement(FObserved.AccountQueries); + LPrevious := AccountPageSource; + AccountPageSource := Pointer(Self); + try + Result := inherited FetchPage(AParams,ADataTable); + FObserved.LastTotal := Result.TotalDataCount; + FObserved.LastPageIndex := Result.PageIndex; + FObserved.LastRequestedPageIndex := AParams.PageIndex; + FObserved.LastPageSize := AParams.PageSize; + FObserved.LastRowCount := Length(ADataTable.Rows); + FObserved.MinimumAccount := High(Cardinal); + FObserved.MaximumAccount := 0; + if Length(ADataTable.Rows) > 0 then + FObserved.FirstAccount := Cardinal(ADataTable.Rows[0].AccountNumber); + for I := 0 to High(ADataTable.Rows) do begin + LAccount := Cardinal(ADataTable.Rows[I].AccountNumber); + if LAccount < FObserved.MinimumAccount then FObserved.MinimumAccount := LAccount; + if LAccount > FObserved.MaximumAccount then FObserved.MaximumAccount := LAccount; + end; + finally + AccountPageSource := LPrevious; + FObserved.Gates[qkPage].Exited.SetEvent; + end; +end; + +constructor TObservedCoreOperations.CreateObserved(AOwner: TComponent; AObserved: TCoreQueryObservations); +begin + FObserved := AObserved; + inherited Create(AOwner); +end; + +function TObservedCoreOperations.FetchPage({$IFDEF FPC}constref{$ELSE}const{$ENDIF} AParams: TPageFetchParams; + var ADataTable: TDataTable): TPageFetchResult; +begin + InterlockedIncrement(FObserved.OperationQueries); + Result := inherited FetchPage(AParams,ADataTable); +end; + +constructor TCoreSynchronizationWallet.CreateObserved(AOwner: TComponent; AObserved: TCoreQueryObservations); +begin + FObserved := AObserved; + inherited Create(AOwner); +end; + +function TCoreSynchronizationWallet.CreateAccountsDataSource: TMyAccountsDataSource; +begin + FCoreAccounts := TObservedCoreAccounts.CreateObserved(Self,FObserved); + Result := FCoreAccounts; +end; + +function TCoreSynchronizationWallet.CreateOperationsDataSource: TAccountsOperationsDataSource; +begin + Result := TObservedCoreOperations.CreateObserved(Self,FObserved); +end; + +function TCoreSynchronizationMain.CreateWalletControl: TCTRLWallet; +begin + if FSynchronizeBeforeFirstWallet then begin + FSynchronizeBeforeFirstWallet := False; + // Enter the real node lifecycle before the lazy control can read its first + // snapshot. This covers manual loading requested before grid parenting. + TUserInterface.Node.BeginBlockChainUpdate; + FInitialSynchronizationActive := True; + Require(TUserInterface.Node.GetSynchronizationStatus.State=nssSynchronizing, + 'The actual node must already be synchronizing before wallet construction'); + end; + Result := TCoreSynchronizationWallet.CreateObserved(Self,FObserved); +end; + +procedure TCoreSynchronizationMain.EndInitialSynchronization; +begin + if not FInitialSynchronizationActive then Exit; + FInitialSynchronizationActive := False; + TUserInterface.Node.EndBlockChainUpdate; +end; + +function TCoreSynchronizationMain.GetCoreWallet: TCoreSynchronizationWallet; +begin + Result := TCoreSynchronizationWallet(WalletControl); +end; + +procedure TGeneratedSafeBoxAccess.Seed(const ALargeKey, ASmallKey: TAccountKey); +var LBlock: TOperationBlock; I: Integer; +begin + for I := 0 to 51 do begin + SeedBlockIndex := I; + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := ALargeKey; + if I = 51 then LBlock.account_key := ASmallKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + AddNew(LBlock); + end; +end; + +constructor TActualNodeWriter.Create(ANode: TNode; AKey: TECPrivateKey); +var LOperation: TPCOperation; +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FTree := TOperationsHashTree.Create; + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_1,1000000,1,1000001, + AKey,1,1,CT_TOperationPayload_NUL); + try FTree.AddOperationToHashTree(LOperation); + finally LOperation.Free; end; + Prepared := TEvent.Create(nil,True,False,''); + Completed := TEvent.Create(nil,True,False,''); +end; + +destructor TActualNodeWriter.Destroy; +begin + Terminate; + if Suspended then Start; + WaitFor; + Completed.Free; + Prepared.Free; + FTree.Free; + inherited; +end; + +procedure TActualNodeWriter.OnProgress(Sender: TObject; const AProgress: TProgressInfo); +begin + if Terminated then Abort; + if AProgress.Stage = psPreparingOperations then Prepared.SetEvent; +end; + +procedure TActualNodeWriter.BCExecute; +var LError: String; +begin + try + try + if not Terminated then FNode.AddOperations(nil,FTree,nil,LError,OnProgress); + except on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; end; + finally Completed.SetEvent; end; +end; + +constructor TStorageFailureReporter.Create(ANode: TNode; const AFileName: String); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FFileName := AFileName; + Completed := TEvent.Create(Nil,True,False,''); +end; + +destructor TStorageFailureReporter.Destroy; +begin + Terminate; + if Suspended then Start; + WaitFor; + Completed.Free; + inherited; +end; + +procedure TStorageFailureReporter.BCExecute; +var LError: EWriteError; +begin + try + try + LError := EWriteError.Create('Synthetic checkpoint write failure'); + try + FNode.ReportStorageFailure(sfsCheckpoint,FFileName,LError); + finally + LError.Free; + end; + except + on E: Exception do ErrorMessage := E.ClassName+': '+E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TWalletNodeSynchronizationChecks.Create(const ALogPath: String); +begin + inherited Create; + FLogPath := ALogPath; + FLog := TStringList.Create; + FObserved := TCoreQueryObservations.Create; + FPeer := TObject.Create; + FKey := TECPrivateKey.Create; + FSmallKey := TECPrivateKey.Create; + FPreviousException := Application.OnException; + Application.OnException := OnException; +end; + +destructor TWalletNodeSynchronizationChecks.Destroy; +begin + FObserved.ReleaseAll; + if Assigned(FMain) then begin + FMain.EndInitialSynchronization; + if TUserInterface.Started then + TUserInterface.Node.SetSynchronizationNetworkActive(False); + TUserInterface.ExitApplication; + FMain := nil; + end; + Application.OnException := FPreviousException; + FSmallKey.Free; + FKey.Free; + FPeer.Free; + FObserved.Free; + FLog.Free; + inherited; +end; + +procedure TWalletNodeSynchronizationChecks.Log(const AText: String); +begin + FLog.Add(UIntToStr(GetTickCount64) + ' ' + AText); + FLog.SaveToFile(FLogPath); +end; + +procedure TWalletNodeSynchronizationChecks.OnException(Sender: TObject; E: Exception); +begin + if FAsyncError = '' then FAsyncError := E.ClassName + ': ' + E.Message; +end; + +procedure TWalletNodeSynchronizationChecks.Pump; +begin + Application.ProcessMessages; + CheckSynchronize(0); + Require(FAsyncError = '',FAsyncError); +end; + +procedure TWalletNodeSynchronizationChecks.PumpFor(AMilliseconds: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat Pump; Sleep(1); until GetTickCount64 >= LUntil; +end; + +procedure TWalletNodeSynchronizationChecks.WaitEvent(AEvent: TEvent; + const ADescription: String; ATimeout: Cardinal); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + ATimeout; + repeat + Pump; + if AEvent.WaitFor(0) = wrSignaled then Exit; + Sleep(1); + until GetTickCount64 >= LUntil; + raise Exception.Create(ADescription); +end; + +procedure TWalletNodeSynchronizationChecks.WaitIdle; +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + 15000; + repeat + Pump; + if Assigned(FMain.CoreWallet) and + not FMain.CoreWallet.AccountsGrid.IsUpdating and + not FMain.CoreWallet.OperationsGrid.IsUpdating and + not FMain.CoreWallet.AccountsGrid.RefreshThrottle.Waiting and + not FMain.CoreWallet.OperationsGrid.RefreshThrottle.Waiting then Exit; + Sleep(1); + until GetTickCount64 >= LUntil; + raise Exception.Create('Actual wallet grids did not finish their admitted requests'); +end; + +procedure TWalletNodeSynchronizationChecks.WaitSuspended(AValue: Boolean); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + 3000; + repeat + Pump; + if (FMain.CoreWallet.AccountsGrid.RefreshSuspended = AValue) and + (FMain.CoreWallet.OperationsGrid.RefreshSuspended = AValue) then Exit; + Sleep(1); + until GetTickCount64 >= LUntil; + raise Exception.Create('Actual wallet did not follow the node synchronization event'); +end; + +procedure TWalletNodeSynchronizationChecks.StartSynchronization; +var LNode: TNode; +begin + LNode := TUserInterface.Node; + LNode.SetSynchronizationNetworkActive(True); + LNode.SetSynchronizationPeerConnected(FPeer,True); + LNode.SetSynchronizationTarget(FPeer,LNode.GetSynchronizationStatus.LocalBlockCount+20); + WaitSuspended(True); +end; + +procedure TWalletNodeSynchronizationChecks.RequireBannerStatus(const ACheckpoint: String; const AExpected: String); +var LBanner: TCTRLBanner; I: Integer; +begin + LBanner := nil; + for I := 0 to FMain.ComponentCount-1 do + if FMain.Components[I] is TCTRLBanner then + LBanner := TCTRLBanner(FMain.Components[I]); + Require(Assigned(LBanner),ACheckpoint+': the actual banner is missing'); + Require(LBanner.NodeStatusLabel.Caption=AExpected,ACheckpoint+': the banner status caption changed'); + // Do not call FormResize here: that would repair the missing deferred + // AutoSize notification instead of testing the real first-show event path. + Require(LBanner.NodeStatusLabel.Left=LBanner.lblClientVersion.Left+ + LBanner.lblClientVersion.Width+LBanner.imLogo.Left, + ACheckpoint+': status did not follow the actual version label width'); + Require((LBanner.NodeStatusLabel.Alignment=taLeftJustify) and + (LBanner.NodeStatusLabel.Font.Color=clWhite),ACheckpoint+': status must stay white and left aligned'); +end; + +procedure TWalletNodeSynchronizationChecks.RequireSynchronizationCards(const ACheckpoint: String; ACapture: Boolean); +var + LGrids: array[0..1] of TVisualGrid; + LPanel: TPanel; + LCircle: TLoadingCircle; + LTitle, LSubtitle: TLabel; + LPanelBounds, LParentBounds: TRect; + LBitmap: TBitmap; + LBag: TDisposables; + I, X, Y, LInk: Integer; + {$IFDEF FPC} + LCanvasDC: HDC; + + procedure PaintNativeControl(AControl: TWinControl); + var + LSaved, J: Integer; + LOrigin, LWindowOrigin: TPoint; + LWindow: TRect; + begin + if not AControl.HandleAllocated or not IsWindowVisible(AControl.Handle) then Exit; + LSaved := SaveDC(LCanvasDC); + try + GetWindowRect(AControl.Handle,LWindow); + LWindowOrigin := LPanel.ScreenToClient(LWindow.TopLeft); + SetViewportOrgEx(LCanvasDC,LWindowOrigin.X,LWindowOrigin.Y,nil); + SendMessage(AControl.Handle,WM_PRINT,WPARAM(LCanvasDC),PRF_NONCLIENT); + LOrigin := LPanel.ScreenToClient(AControl.ClientToScreen(Types.Point(0,0))); + SetViewportOrgEx(LCanvasDC,LOrigin.X,LOrigin.Y,nil); + IntersectClipRect(LCanvasDC,0,0,AControl.ClientWidth,AControl.ClientHeight); + AControl.Perform(LM_ERASEBKGND,WPARAM(LCanvasDC),0); + AControl.Perform(LM_PAINT,WPARAM(LCanvasDC),0); + if not (AControl is TCustomControl) then + SendMessage(AControl.Handle,WM_PRINTCLIENT,WPARAM(LCanvasDC),PRF_CLIENT); + for J := 0 to AControl.ControlCount-1 do + if AControl.Controls[J] is TWinControl then + PaintNativeControl(TWinControl(AControl.Controls[J])); + finally + RestoreDC(LCanvasDC,LSaved); + end; + end; + {$ENDIF} +begin + Require(TUserInterface.Node.GetSynchronizationStatus.State=nssSynchronizing, + ACheckpoint+': the real node ended synchronization unexpectedly'); + Require(FMain.Mode=wmWallet,ACheckpoint+': the wallet page is not selected'); + Require(IsWindowVisible(FMain.Handle),ACheckpoint+': the actual main window is hidden'); + LGrids[0] := FMain.CoreWallet.AccountsGrid; + LGrids[1] := FMain.CoreWallet.OperationsGrid; + for I := Low(LGrids) to High(LGrids) do begin + LPanel := LGrids[I].FindComponent('LoadingOverlay') as TPanel; + LCircle := LGrids[I].FindComponent('LoadingCircle') as TLoadingCircle; + LTitle := LGrids[I].FindComponent('LoadingStatus') as TLabel; + LSubtitle := LGrids[I].FindComponent('LoadingSubtitle') as TLabel; + Require(LGrids[I].RefreshSuspended and LGrids[I].LoadingVisible, + ACheckpoint+': grid '+IntToStr(I)+' lost its node-owned suspension/card'); + Require(LPanel.Visible and LPanel.HandleAllocated and IsWindowVisible(LPanel.Handle), + ACheckpoint+': grid '+IntToStr(I)+' manual flag is set but the native overlay is hidden'); + Require(LCircle.Active and LCircle.Visible and LCircle.HandleAllocated and IsWindowVisible(LCircle.Handle), + ACheckpoint+': grid '+IntToStr(I)+' native loading circle is not visible and active'); + Require(LTitle.Visible and (LTitle.Caption='Please Wait') and + LSubtitle.Visible and (LSubtitle.Caption='Synchronizing'), + ACheckpoint+': grid '+IntToStr(I)+' synchronization captions are missing'); + Require((LTitle.Width>0) and (LTitle.Height>0) and (LSubtitle.Width>0) and (LSubtitle.Height>0), + ACheckpoint+': grid '+IntToStr(I)+' synchronization text has empty bounds'); + GetWindowRect(LPanel.Handle,LPanelBounds); + GetClientRect(LPanel.Parent.Handle,LParentBounds); + MapWindowPoints(LPanel.Parent.Handle,0,LParentBounds,2); + Require((LPanelBounds.Right>LPanelBounds.Left) and (LPanelBounds.Bottom>LPanelBounds.Top) and + (LPanelBounds.Left>=LParentBounds.Left) and (LPanelBounds.Top>=LParentBounds.Top) and + (LPanelBounds.Right<=LParentBounds.Right) and (LPanelBounds.Bottom<=LParentBounds.Bottom), + ACheckpoint+': grid '+IntToStr(I)+' native overlay lies outside its visible client'); + if ACapture then begin + LBitmap := LBag.AddObject(TBitmap.Create) as TBitmap; + LBitmap.PixelFormat := pf24bit; + LBitmap.SetSize(LPanel.Width,LPanel.Height); + LBitmap.Canvas.Brush.Color := LPanel.Color; + LBitmap.Canvas.FillRect(Types.Rect(0,0,LBitmap.Width,LBitmap.Height)); + {$IFDEF FPC} + // Reuse the verified native-child paint route: inactive-desktop form + // screenshots can otherwise be black despite visible native controls. + LCanvasDC := LBitmap.Canvas.Handle; + PaintNativeControl(LPanel); + {$ELSE} + LPanel.PaintTo(LBitmap.Canvas.Handle,0,0); + {$ENDIF} + LInk := 0; + for Y := 0 to LBitmap.Height-1 do + for X := 0 to LBitmap.Width-1 do + if (ColorToRGB(LBitmap.Canvas.Pixels[X,Y])<>ColorToRGB(clWhite)) and + (ColorToRGB(LBitmap.Canvas.Pixels[X,Y])<>ColorToRGB(clFuchsia)) then Inc(LInk); + Require(LInk>100,ACheckpoint+': grid '+IntToStr(I)+' actual overlay painting is blank'); + LBitmap.SaveToFile(ChangeFileExt(FLogPath,'.'+ACheckpoint+'-grid'+IntToStr(I)+'.bmp')); + end; + end; +end; + +procedure TWalletNodeSynchronizationChecks.CheckInitialSynchronization; +var LAccounts, LOperations, LMemberships, I: Integer; LUntil: UInt64; + LBanner: TCTRLBanner; LOriginalBounds: TRect; +begin + FPhase := 'synchronizing-before-lazy-wallet'; + Require(FMain.InitialSynchronizationActive,'The node scope did not precede the first lazy wallet construction'); + FMain.Show; + TUserInterface.ShowWallet; + WaitSuspended(True); + PumpFor(100); + RequireSynchronizationCards('first-show',True); + RequireBannerStatus('first-show-100','Synchronizing'); + LBanner := nil; + for I := 0 to FMain.ComponentCount-1 do + if FMain.Components[I] is TCTRLBanner then + LBanner := TCTRLBanner(FMain.Components[I]); + LOriginalBounds := LBanner.BoundsRect; + LBanner.ScaleBy(5,4); + try + PumpFor(25); + RequireBannerStatus('first-show-125','Synchronizing'); + finally + LBanner.ScaleBy(4,5); + LBanner.BoundsRect := LOriginalBounds; + end; + LBanner.ScaleBy(3,2); + try + PumpFor(25); + RequireBannerStatus('first-show-150','Synchronizing'); + finally + LBanner.ScaleBy(2,3); + LBanner.BoundsRect := LOriginalBounds; + end; + PumpFor(25); + RequireBannerStatus('first-show-return-100','Synchronizing'); + LAccounts := FObserved.AccountQueries; + LOperations := FObserved.OperationQueries; + LMemberships := FObserved.AggregateMemberships; + Require((LAccounts=0) and (LOperations=0) and (LMemberships=0), + 'First construction admitted a provider/aggregate during synchronization'); + FMain.CoreWallet.OperationsHistory := woh30Days; + FMain.CoreWallet.AccountsGrid.RefreshGrid; + FMain.CoreWallet.OperationsGrid.RefreshGrid; + FMain.Width := FMain.Width+40; + // No new lifecycle notification repairs presentation during this pause. + // The selected synchronization session must retain both native cards. + LUntil := GetTickCount64+6200; + repeat + PumpFor(100); + RequireSynchronizationCards('inter-batch-observation'); + until GetTickCount64>=LUntil; + RequireSynchronizationCards('inter-batch-quiet',True); + Require((FObserved.AccountQueries=LAccounts) and (FObserved.OperationQueries=LOperations) and + (FObserved.AggregateMemberships=LMemberships),'Inter-batch silence admitted a provider during synchronization'); + FMain.EndInitialSynchronization; + WaitSuspended(False); + WaitIdle; + Require((FObserved.AccountQueries>LAccounts) and (FObserved.OperationQueries>LOperations), + 'Node completion did not resume the actual latest grid requests'); + Require(not (FMain.CoreWallet.AccountsGrid.FindComponent('LoadingOverlay') as TPanel).Visible and + not (FMain.CoreWallet.OperationsGrid.FindComponent('LoadingOverlay') as TPanel).Visible, + 'Completed initial synchronization retained an actual overlay'); + Log('PASS node synchronizing before lazy wallet construction: both native cards/captions/bounds visible, inter-batch quiet retains cards without queries, completion resumes'); +end; + +procedure TWalletNodeSynchronizationChecks.CheckAccountCacheOrdering; +const CNumbers: array[0..3] of Cardinal = (0, 5, $80000000, High(Cardinal)); +var LMemory: TPCAbstractMem; LAccount, LFound: TAccount; I: Integer; +begin + FPhase := 'actual-account-cache-ordering'; + LMemory := TPCAbstractMem.Create('',False); + try + // Use the actual cache and its private production comparison callback. + // Reversed 0/5 and both sides of the signed boundary must remain ordered. + for I := High(CNumbers) downto Low(CNumbers) do begin + LAccount := CT_Account_NUL; + LAccount.account := CNumbers[I]; + LAccount.balance := I+1; + LMemory.AccountCache.Add(LAccount); + end; + for I := Low(CNumbers) to High(CNumbers) do begin + LAccount := CT_Account_NUL; + LAccount.account := CNumbers[I]; + Require(LMemory.AccountCache.Find(LAccount,LFound), + 'The actual account cache lost a Cardinal boundary key'); + Require((LFound.account = CNumbers[I]) and (LFound.balance = UInt64(I+1)), + 'The actual account cache comparison aliased distinct keys'); + end; + LAccount := CT_Account_NUL; + LAccount.account := 1; + Require(not LMemory.AccountCache.Find(LAccount,LFound), + 'The actual cache must reject an absent account'); + LAccount.account := High(Cardinal); + LMemory.AccountCache.Remove(LAccount); + Require(not LMemory.AccountCache.Find(LAccount,LFound), + 'The actual cache must remove only the requested boundary account'); + LAccount.account := 0; + Require(LMemory.AccountCache.Find(LAccount,LFound), + 'Removing a high account must preserve account zero'); + finally LMemory.Free; end; + Log('PASS actual account-cache ordering:0,5,80000000,FFFFFFFF/reversed reads/removal'); +end; + +procedure TWalletNodeSynchronizationChecks.SeedActualAccounts; +var LNode: TNode; LMempool: TPCOperationsComp; LUntil: UInt64; + LTotalLabel: TLabel; +begin + LNode := TUserInterface.Node; + FPhase := 'generate-large-owner'; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FPhase := 'generate-small-owner'; + FSmallKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FPhase := 'acquire-seed-writer'; + LMempool := LNode.LockMempoolWrite; + try + Require(LNode.Bank.SafeBox.AccountsCount = 0,'Fresh data must be empty before seeding'); + FPhase := 'seed-actual-safebox'; + TGeneratedSafeBoxAccess(LNode.Bank.SafeBox).Seed(FKey.PublicKey,FSmallKey.PublicKey); + FPhase := 'clear-actual-mempool'; + LMempool.Clear(True); + finally LNode.UnlockMempoolWrite; end; + FPhase := 'assign-wallet-safebox'; + TWallet.Keys.SafeBox := LNode.Bank.SafeBox; + FPhase := 'add-large-watch-key'; + TWallet.Keys.AddPublicKey('Large generated owner',FKey.PublicKey); + FPhase := 'add-small-watch-key'; + TWallet.Keys.AddPublicKey('Small generated owner',FSmallKey.PublicKey); + FPhase := 'publish-generated-records'; + TUserInterface.ShowWallet; + LNode.NotifyBlocksChanged; + LTotalLabel := FMain.CoreWallet.FindComponent('lblTotalPASA') as TLabel; + LUntil := GetTickCount64+15000; + repeat + Pump; + if (FObserved.LastTotal = 52*CT_AccountsPerBlock) and + (LTotalLabel.Caption = IntToStr(52*CT_AccountsPerBlock)) then Break; + Sleep(1); + until GetTickCount64 >= LUntil; + WaitIdle; + Require(FObserved.LastTotal = 52*CT_AccountsPerBlock,'Actual provider must enumerate all generated accounts'); + Require(LTotalLabel.Caption = IntToStr(52*CT_AccountsPerBlock), + 'The actual aggregate must complete before its later cancellation test'); + Log('READY actual main/private sources:260generated core records,two watch-only keys,no transport'); +end; + +procedure TWalletNodeSynchronizationChecks.ChooseSmallKey; +var LCombo: TComboBox; I: Integer; +begin + LCombo := FMain.CoreWallet.FindComponent('cbAccounts') as TComboBox; + for I := 0 to LCombo.Items.Count-1 do + if Pos('Small generated owner',LCombo.Items[I]) > 0 then begin + LCombo.ItemIndex := I; + Require(Assigned(LCombo.OnChange),'Actual key selector must retain its handler'); + LCombo.OnChange(LCombo); + Exit; + end; + raise Exception.Create('The real wallet key selector did not include its generated small key'); +end; + +procedure TWalletNodeSynchronizationChecks.CheckHeldRead(AKind: TQueryKind); +var LWriter: TActualNodeWriter; LGate: TQueryGate; LSummary: TBalanceSummary; + LAccounts, LOperations, LMemberships, LExpectedStart, LExpectedCount: Integer; + LStarted: UInt64; LWriteAdmitted: Boolean; +begin + WaitIdle; + LGate := FObserved.Gates[AKind]; + LGate.Arm; + if AKind = qkPage then FMain.CoreWallet.AccountsGrid.RefreshGrid + else begin + FMain.CoreWallet.CoreAccounts.InvalidateWalletData; + FMain.CoreWallet.CoreAccounts.TryGetBalanceSummary(LSummary); + end; + WaitEvent(LGate.Entered,'The actual provider never entered its real membership scope'); + Require(FObserved.RealMempoolObserved,'The gate must retain an actual mempool read snapshot'); + LWriteAdmitted := TUserInterface.Node.TryLockNode(0); + if LWriteAdmitted then TUserInterface.Node.UnlockNode; + Require(not LWriteAdmitted,'Actual provider did not retain its real core reader'); + LWriter := TActualNodeWriter.Create(TUserInterface.Node,FKey); + try + LWriter.Start; + WaitEvent(LWriter.Prepared,'Actual Node.AddOperations did not reach write preparation'); + Require(LWriter.Completed.WaitFor(100) = wrTimeout,'Writer unexpectedly bypassed the held actual reader'); + LStarted := GetTickCount64; + StartSynchronization; + WaitEvent(LGate.CancelObserved,'NodeSync did not cancel the actual source worker'); + WaitEvent(LWriter.Completed,'The canceled source did not release the actual node writer'); + Require(LWriter.ErrorMessage = '',LWriter.ErrorMessage); + Require(LGate.ReleaseRead.WaitFor(0) = wrTimeout,'The fixture must not manually release the read gate'); + Log('WRITER PROGRESSED after real cancellation kind=' + IntToStr(Ord(AKind)) + + ' elapsed=' + UIntToStr(GetTickCount64-LStarted)); + Require(FMain.CoreWallet.AccountsGrid.LoadingVisible and + FMain.CoreWallet.OperationsGrid.LoadingVisible,'Both actual grids must retain the synchronization card'); + RequireSynchronizationCards('held-read-'+IntToStr(Ord(AKind))); + LAccounts := FObserved.AccountQueries; + LOperations := FObserved.OperationQueries; + LMemberships := FObserved.AggregateMemberships; + if AKind = qkPage then begin + ChooseSmallKey; + FMain.CoreWallet.AccountsGrid.PageIndex := 2; + FMain.Width := FMain.Width+80; + FMain.CoreWallet.OperationsHistory := woh30Days; + TUserInterface.Node.NotifyBlocksChanged; + PumpFor(6200); + end else PumpFor(500); + RequireSynchronizationCards('held-read-quiet-'+IntToStr(Ord(AKind))); + Require(FObserved.AccountQueries = LAccounts,'No account query may execute while NodeSync is active'); + Require(FObserved.OperationQueries = LOperations,'No operations query may execute while NodeSync is active'); + Require(FObserved.AggregateMemberships = LMemberships,'No aggregate may restart while NodeSync is active'); + if AKind = qkPage then TUserInterface.Node.SetSynchronizationNetworkActive(False) + else TUserInterface.Node.SetSynchronizationPeerConnected(FPeer,False); + WaitSuspended(False); + WaitIdle; + Require(FObserved.AccountQueries = LAccounts+1,'Resume must admit exactly one latest real account query'); + Require(FObserved.OperationQueries = LOperations+1,'Resume must admit exactly one latest real operations query'); + Require(FObserved.LastTotal = CT_AccountsPerBlock,'The resumed query must use the latest selected key'); + Log('ROWS first=' + UIntToStr(FObserved.FirstAccount) + + ' min=' + UIntToStr(FObserved.MinimumAccount) + + ' max=' + UIntToStr(FObserved.MaximumAccount) + + ' count=' + IntToStr(FObserved.LastRowCount) + + ' requestedPage=' + IntToStr(FObserved.LastRequestedPageIndex) + + ' page=' + IntToStr(FObserved.LastPageIndex) + + ' capacity=' + IntToStr(FObserved.LastPageSize)); + // The requested page may be clipped after changing to a five-account key. + // Verify that exact real slice rather than demanding the whole data set. + LExpectedStart := FObserved.LastPageIndex*FObserved.LastPageSize; + LExpectedCount := CT_AccountsPerBlock-LExpectedStart; + if LExpectedCount > FObserved.LastPageSize then + LExpectedCount := FObserved.LastPageSize; + Require((LExpectedCount > 0) and + (FObserved.LastRowCount = LExpectedCount) and + (FObserved.MinimumAccount = Cardinal(51*CT_AccountsPerBlock+LExpectedStart)) and + (FObserved.MaximumAccount = Cardinal(51*CT_AccountsPerBlock+LExpectedStart+LExpectedCount-1)), + 'The resumed rows must be actual small-key core accounts'); + Require(not FMain.CoreWallet.AccountsGrid.LoadingVisible and + not FMain.CoreWallet.OperationsGrid.LoadingVisible,'Node completion must remove both manual cards'); + Log('PASS latest real query resumed once kind=' + IntToStr(Ord(AKind)) + + ' rows=' + IntToStr(FObserved.LastTotal)); + finally + FObserved.ReleaseAll; + LWriter.Free; + end; +end; + +procedure TWalletNodeSynchronizationChecks.CheckStorageFailure; +var LNode: TNode; LReporter: TStorageFailureReporter; LUntil: UInt64; + LExpected, LFileName: String; LStatus: TNodeSynchronizationStatus; + LBanner: TCTRLBanner; I, LAccounts, LOperations, LMemberships: Integer; + LFailure: EWriteError; LProgress: TProgressInfo; LLoading: TProgressNotifyMany; + LBitmap: TBitmap; + + procedure AssertPresentation; + begin + Require(TUserInterface.State=uisError,'Storage error must remain the application state'); + Require(TUserInterface.StateText=LExpected,'The first storage failure details changed'); + Require(FMain.Mode=wmSync,'A storage failure must retain the visible error view'); + Require(FMain.SyncControl.lblNodeStatus.Caption='Error: '+LExpected,'Sync details did not display the exact first storage failure: '+FMain.SyncControl.lblNodeStatus.Caption); + Require(FMain.SyncControl.lblNodeStatus.Font.Color=clRed,'Storage error details must be visibly red'); + Require(FMain.sbStatusBar.Panels[2].Text=LExpected,'The footer replaced the storage failure with connectivity text'); + Require(LBanner.NodeStatusLabel.Caption='Error: '+LExpected,'The banner did not retain the first storage failure'); + RequireBannerStatus('storage-error','Error: '+LExpected); + Require(FMain.SyncControl.DisplayedSynchronizationStatus.State=nssStorageError,'The sync view retained an older node-state record'); + Require(not FMain.tbtnConnectivity.Enabled,'The error state must disable the connectivity command'); + Require(FMain.CoreWallet.AccountsGrid.RefreshSuspended and FMain.CoreWallet.OperationsGrid.RefreshSuspended,'Storage failure resumed a suspended grid'); + Require((FMain.CoreWallet.AccountsGrid.FindComponent('LoadingStatus') as TLabel).Caption='Storage error','Accounts card did not identify the storage error'); + Require((FMain.CoreWallet.OperationsGrid.FindComponent('LoadingStatus') as TLabel).Caption='Storage error','Operations card did not identify the storage error'); + Require((FMain.CoreWallet.AccountsGrid.FindComponent('LoadingSubtitle') as TLabel).Caption='Restart required','Accounts card did not require restart'); + Require((FMain.CoreWallet.OperationsGrid.FindComponent('LoadingSubtitle') as TLabel).Caption='Restart required','Operations card did not require restart'); + end; + +begin + FPhase := 'actual-storage-error'; + WaitIdle; + LNode := TUserInterface.Node; + LBanner := Nil; + for I := 0 to FMain.ComponentCount-1 do + if FMain.Components[I] is TCTRLBanner then LBanner := TCTRLBanner(FMain.Components[I]); + Require(Assigned(LBanner),'Actual main banner is missing'); + FObserved.Gates[qkPage].Arm; + FMain.CoreWallet.AccountsGrid.RefreshGrid; + WaitEvent(FObserved.Gates[qkPage].Entered,'Storage-failure test did not hold an actual provider read'); + LFileName := IncludeTrailingPathDelimiter(ExtractFilePath(FLogPath))+'owned-checkpoint.am_safebox'; + LReporter := TStorageFailureReporter.Create(LNode,LFileName); + try + LReporter.Start; + // Deliberately do not pump UI callbacks while the producer reports failure. + // A synchronous main-thread notification would leave this worker blocked. + Require(LReporter.Completed.WaitFor(1000)=wrSignaled,'Storage reporting blocked behind a real UI/provider reader'); + Require(LReporter.ErrorMessage='',LReporter.ErrorMessage); + Require(TUserInterface.State<>uisError,'Storage reporting synchronously invoked the UI'); + LStatus := LNode.GetSynchronizationStatus; + Require(LStatus.State=nssStorageError,'The node did not publish its storage-error state'); + LExpected := FormatStorageFailure(LStatus.StorageFailure); + Require(Pos('Synthetic checkpoint write failure',LExpected)>0,'Expected original error text is missing'); + // Exercise a loader completion racing ahead of the queued failure event. + // State is already guarded, but the first diagnostic still must be published. + TUserInterface.State := uisLoaded; + WaitEvent(FObserved.Gates[qkPage].CancelObserved,'Storage error did not cancel the actual active provider'); + WaitEvent(FObserved.Gates[qkPage].Exited,'The canceled storage-error query did not retire'); + LUntil := GetTickCount64+5000; + repeat + Pump; + if (TUserInterface.State=uisError) and (FMain.Mode=wmSync) then Break; + Sleep(1); + until GetTickCount64>=LUntil; + AssertPresentation; + LAccounts := FObserved.AccountQueries; + LOperations := FObserved.OperationQueries; + LMemberships := FObserved.AggregateMemberships; + // Use real typed node inputs, including both network settings, and a late + // UI loading delivery. None may clear a restart-only storage failure. + LNode.SetSynchronizationNetworkActive(False); + LNode.SetSynchronizationNetworkActive(True); + LNode.SetSynchronizationPeerConnected(FPeer,True); + LNode.SetSynchronizationTarget(FPeer,LStatus.LocalBlockCount+30); + LProgress := TProgressInfo.Create(psDownloadingBlockchain,1,1); + LProgress.BlockNumber := LStatus.LocalBlockCount+1; + LNode.SetSynchronizationProgress(LProgress); + LLoading := TUserInterface.Loading; + LLoading.Invoke(LNode,LProgress); + TUserInterface.State := uisLoaded; + TUserInterface.ToggleConnectivity; + FMain.Mode := wmWallet; + FMain.CoreWallet.AccountsGrid.RefreshGrid; + FMain.CoreWallet.OperationsGrid.RefreshGrid; + FMain.Width := FMain.Width+20; + LFailure := EWriteError.Create('A later error must not replace the first failure'); + try + LNode.ReportStorageFailure(sfsBlockStream,'different-owned-file',LFailure); + finally + LFailure.Free; + end; + PumpFor(500); + AssertPresentation; + Require(not LNode.CanRequestBlockChainData,'The failed node resumed block downloads'); + Require(not TUserInterface.RPCServer.Active,'The failed node reopened its RPC listener'); + Require(not TUserInterface.PoolMiningServer.Active,'The failed node reopened its miner listener'); + Require(FObserved.AccountQueries=LAccounts,'A late input resumed accounts after storage failure'); + Require(FObserved.OperationQueries=LOperations,'A late input resumed operations after storage failure'); + Require(FObserved.AggregateMemberships=LMemberships,'A late input restarted balance reads after storage failure'); + LBitmap := FMain.GetFormImage; + try + LBitmap.SaveToFile(ChangeFileExt(FLogPath,'.storage-error.bmp')); + finally + LBitmap.Free; + end; + Log('PASS actual storage failure: nonblocking producer, real read cancellation, persistent banner/sync/footer, restart-only cards, zero late queries'); + finally + FObserved.ReleaseAll; + LReporter.Free; + end; +end; + +procedure TWalletNodeSynchronizationChecks.Run; +var LOptions: TWalletStartupOptions; LArguments: array of String; + LDirectory, LError: String; LID: TGUID; LUntil: UInt64; LQueries: Integer; +begin + FPhase := 'fresh-options'; + TCrypto.InitCrypto; + CheckAccountCacheOrdering; + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'PascalCoinNodeSync-' + GUIDToString(LID); + Require(not DirectoryExists(LDirectory),'Owned data directory must be fresh'); + ForceDirectories(LDirectory); + SetLength(LArguments,3); + LArguments[0] := '--data-dir='+LDirectory; + LArguments[1] := '--offline'; + LArguments[2] := '--no-startup-dialogs'; + Require(TryParseWalletStartupOptions(LArguments,LOptions,LError),LError); + FPhase := 'configure-actual-controller'; + TUserInterface.ConfigureStartup(LOptions); + FPhase := 'create-actual-main'; + Application.CreateForm(TCoreSynchronizationMain,FMain); + FMain.Observed := FObserved; + FMain.SynchronizeBeforeFirstWallet := True; + FPhase := 'start-actual-controller'; + TUserInterface.StartApplication(FMain); + FPhase := 'await-actual-startup'; + LUntil := GetTickCount64+10000; + repeat + Pump; + if Assigned(FMain.CoreWallet) and (TUserInterface.State <> uisLoading) and + (TUserInterface.State <> uisError) then Break; + Sleep(1); + until GetTickCount64 >= LUntil; + Require(Assigned(FMain.CoreWallet) and (TUserInterface.State <> uisLoading) and + (TUserInterface.State <> uisError),'The actual fresh controller did not load'); + CheckInitialSynchronization; + SeedActualAccounts; + FPhase := 'actual-page-cancellation'; + CheckHeldRead(qkPage); + FPhase := 'actual-aggregate-cancellation'; + CheckHeldRead(qkAggregate); + CheckStorageFailure; + FPhase := 'actual-shutdown'; + LQueries := FObserved.AccountQueries + FObserved.OperationQueries; + TUserInterface.ExitApplication; + FMain := nil; + Require(FObserved.AccountQueries+FObserved.OperationQueries = LQueries, + 'Stopping must not admit retained source requests'); + Log('PASS stopping joined the actual UI/source workers without a new fetch'); +end; + +function DesktopName(AHandle: THandle): String; +var LBuffer: array[0..255] of Char; LRequired: DWORD; +begin + if not GetUserObjectInformation(AHandle,UOI_NAME,@LBuffer[0],SizeOf(LBuffer),LRequired) then RaiseLastOSError; + Result := StrPas(PChar(@LBuffer[0])); +end; + +function RunWalletNodeSynchronizationProbe: Integer; +var LChecks: TWalletNodeSynchronizationChecks; LInput: THandle; +begin + Result := 1; + if ParamCount <> 1 then Exit; + LInput := OpenInputDesktop(0,False,DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + Require(not SameText(DesktopName(GetThreadDesktop(GetCurrentThreadId)),DesktopName(LInput)), + 'This regression requires the existing owned private-desktop broker'); + finally CloseDesktop(LInput); end; + Require(not FileExists(ParamStr(1)),'Refusing an existing probe log'); + Application.Initialize; + LChecks := TWalletNodeSynchronizationChecks.Create(ExpandFileName(ParamStr(1))); + try + try LChecks.Run; Result := 0; + except on E: Exception do LChecks.Log('FAIL '+E.ClassName+': '+E.Message+ + ' phase='+LChecks.FPhase+' block='+IntToStr(SeedBlockIndex)+ + ' address='+IntToHex(NativeUInt(ExceptAddr),SizeOf(Pointer)*2)+ + ' module='+IntToHex(NativeUInt(GetModuleHandle(nil)),SizeOf(Pointer)*2)); end; + finally LChecks.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.dpr b/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.dpr new file mode 100644 index 000000000..d81a5b67d --- /dev/null +++ b/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.dpr @@ -0,0 +1,11 @@ +program WalletNodeSynchronizationProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses UWalletNodeSynchronizationChecks; + +{$R 'WalletGridRefreshDemo.Assets.res'} + +begin + ExitCode := RunWalletNodeSynchronizationProbe; +end. diff --git a/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.dproj b/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.dproj new file mode 100644 index 000000000..ac9d24a58 --- /dev/null +++ b/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.dproj @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{9D46BE91-7C33-48F3-A9D6-6C7B87D1595E}</ProjectGuid> + <MainSource>WalletNodeSynchronizationProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\node-sync-wallet\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\node-sync-wallet\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\..\..</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\node-sync-wallet\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <RcCompile Include="..\grid-refresh\WalletGridRefreshDemo.Assets.rc" /> + <RcCompile Include="..\..\..\..\libraries\sphere10\UVisualGrid.VCL.rc" /> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="UWalletNodeSynchronizationChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">WalletNodeSynchronizationProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.lpr b/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.lpr new file mode 100644 index 000000000..2bd1bbfe0 --- /dev/null +++ b/src/tests/experimental/native/node-sync/WalletNodeSynchronizationProbe.lpr @@ -0,0 +1,15 @@ +program WalletNodeSynchronizationProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, Interfaces, UWalletNodeSynchronizationChecks; + +{$R ..\grid-refresh\WalletGridRefreshDemo.Assets.rc} + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + ExitCode := RunWalletNodeSynchronizationProbe; +end. diff --git a/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.dpr b/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.dpr new file mode 100644 index 000000000..8d22ec4a1 --- /dev/null +++ b/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.dpr @@ -0,0 +1,9 @@ +program OrderedCardinalProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$APPTYPE CONSOLE} +uses UOrderedCardinalChecks; +begin + ReportMemoryLeaksOnShutdown := True; + if ParamCount <> 1 then Halt(64); + ExitCode := RunOrderedCardinalChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.dproj b/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.dproj new file mode 100644 index 000000000..d53be3488 --- /dev/null +++ b/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.dproj @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{78D633C7-9AC7-4940-881D-9359630293FD}</ProjectGuid> + <MainSource>OrderedCardinalProbe.dpr</MainSource> + <Config>Debug</Config><Platform>Win64</Platform><Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms><AppType>Console</AppType> + <FrameworkType>None</FrameworkType><ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi</DCC_Namespace> + <DCC_UnitSearchPath>..\..\..\..\libraries\abstractmem</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\libraries\abstractmem</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\ordered-cardinal\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\ordered-cardinal\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking><DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup><DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile></ItemGroup> + <ProjectExtensions><Borland.Personality>Delphi.Personality.12</Borland.Personality><BorlandProject><Delphi.Personality><Source><Source Name="MainSource">OrderedCardinalProbe.dpr</Source></Source></Delphi.Personality><Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms></BorlandProject></ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.lpr b/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.lpr new file mode 100644 index 000000000..5df28f600 --- /dev/null +++ b/src/tests/experimental/native/ordered-cardinal/OrderedCardinalProbe.lpr @@ -0,0 +1,9 @@ +program OrderedCardinalProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$MODE Delphi} +uses heaptrc, UOrderedCardinalChecks; +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + ExitCode := RunOrderedCardinalChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/ordered-cardinal/README.md b/src/tests/experimental/native/ordered-cardinal/README.md new file mode 100644 index 000000000..eaab98048 --- /dev/null +++ b/src/tests/experimental/native/ordered-cardinal/README.md @@ -0,0 +1,16 @@ +# Full-range Cardinal ordering + +`OrderedCardinalProbe` checks ordering in AbstractMem's `TComparison_Cardinal` +and `TOrderedList<Cardinal>`. It runs the same groups as the FPCUnit +`TOrderedCardinalTests`: + +- 25 boundary pairs +- 120 insertion, find and removal permutations +- duplicate and missing values + +The [Delphi project](OrderedCardinalProbe.dproj) enables range and overflow +checks. For FPC, build in Delphi mode with `-Cr -Co`, a separate unit output +folder and the AbstractMem unit path. + +Both programs take one new absolute log filename. FPC builds write a heap trace +beside it. The probe creates no node, wallet, sockets or chain data. diff --git a/src/tests/experimental/native/ordered-cardinal/UOrderedCardinalChecks.pas b/src/tests/experimental/native/ordered-cardinal/UOrderedCardinalChecks.pas new file mode 100644 index 000000000..c764737f2 --- /dev/null +++ b/src/tests/experimental/native/ordered-cardinal/UOrderedCardinalChecks.pas @@ -0,0 +1,181 @@ +unit UOrderedCardinalChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Full-range ordering through the existing AbstractMem collection used by + affected-account history. No replacement collection or comparator is tested. } + +{$IFDEF FPC}{$MODE Delphi}{$ENDIF} + +interface + +type + TOrderedCardinalCheck = (occBoundaryPairs, occInsertionPermutations, + occDuplicateAndMissingValues); + +procedure CheckOrderedCardinal(ACheck: TOrderedCardinalCheck); +function RunOrderedCardinalChecks(const ALogFile: String): Integer; + +implementation + +uses Classes, SysUtils, UOrderedList; + +const + CValues: array[0..4] of Cardinal = (0, 1, $7FFFFFFF, $80000000, $FFFFFFFF); + CNames: array[TOrderedCardinalCheck] of String = + ('all 25 full-Cardinal boundary pairs', + 'all 120 actual-list insertion/find/removal permutations', + 'actual-list duplicate policy and missing-value insertion positions'); + +type + TBoundaryOrder = array[0..4] of Integer; + TBoundaryMembership = array[0..4] of Boolean; + +procedure Require(ACondition: Boolean; const AReason: String); +begin + if not ACondition then raise Exception.Create(AReason); +end; + +procedure CheckBoundaryPairs; +var I, J, LExpected, LActual: Integer; +begin + for I := Low(CValues) to High(CValues) do + for J := Low(CValues) to High(CValues) do begin + // These constants are independently ordered by their specification, + // including the signed/unsigned boundary. Do not subtract the values. + LExpected := 0; + if I < J then LExpected := -1 else if I > J then LExpected := 1; + LActual := TComparison_Cardinal(CValues[I], CValues[J]); + Require(((LExpected = 0) and (LActual = 0)) or + ((LExpected < 0) and (LActual < 0)) or + ((LExpected > 0) and (LActual > 0)), Format('Compare(%s,%s) expected sign %d, got %d', + [UIntToStr(CValues[I]), UIntToStr(CValues[J]), LExpected, LActual])); + end; +end; + +procedure CheckMembers(AList: TOrderedList<Cardinal>; const APresent: TBoundaryMembership); +var I, LRow, LFound: Integer; +begin + LRow := 0; + for I := Low(CValues) to High(CValues) do + if APresent[I] then begin + Require(LRow < AList.Count, 'Actual list omitted an inserted boundary value'); + Require(AList[LRow] = CValues[I], 'Actual list is not in full Cardinal order'); + Require(AList.Find(CValues[I], LFound) and (LFound = LRow), + 'Actual list Find did not locate the ordered boundary value'); + Require(AList.IndexOf(CValues[I]) = LRow, 'Actual list IndexOf disagrees with Find'); + Inc(LRow); + end else begin + Require(not AList.Find(CValues[I], LFound), 'Actual list retained a removed boundary value'); + Require(AList.IndexOf(CValues[I]) = -1, 'Actual list IndexOf found an absent value'); + end; + Require(AList.Count = LRow, 'Actual list contains extra values'); +end; + +procedure CheckPermutations; +var LOrder: TBoundaryOrder; LPermutationCount, I: Integer; + + procedure RunPermutation; + var LList: TOrderedList<Cardinal>; LPresent: TBoundaryMembership; J: Integer; + begin + FillChar(LPresent, SizeOf(LPresent), 0); + LList := TOrderedList<Cardinal>.Create(False, TComparison_Cardinal); + try + for J := Low(LOrder) to High(LOrder) do begin + Require(LList.Add(CValues[LOrder[J]]) >= 0, 'A distinct boundary insertion was rejected'); + LPresent[LOrder[J]] := True; + CheckMembers(LList, LPresent); + end; + for J := High(LOrder) downto Low(LOrder) do begin + LList.Remove(CValues[LOrder[J]]); + LPresent[LOrder[J]] := False; + CheckMembers(LList, LPresent); + end; + Require(LList.Count = 0, 'Actual list did not empty after all boundary removals'); + finally LList.Free; end; + Inc(LPermutationCount); + end; + + procedure Permute(APosition: Integer); + var J, LTemporary: Integer; + begin + if APosition > High(LOrder) then begin + RunPermutation; + Exit; + end; + for J := APosition to High(LOrder) do begin + LTemporary := LOrder[APosition]; LOrder[APosition] := LOrder[J]; LOrder[J] := LTemporary; + Permute(APosition + 1); + LTemporary := LOrder[APosition]; LOrder[APosition] := LOrder[J]; LOrder[J] := LTemporary; + end; + end; + +begin + LPermutationCount := 0; + for I := Low(LOrder) to High(LOrder) do LOrder[I] := I; + Permute(0); + Require(LPermutationCount = 120, 'The actual-list fixture did not execute all five-value permutations'); +end; + +procedure CheckDuplicateAndMissingValues; +var LList: TOrderedList<Cardinal>; I, LFound: Integer; +begin + LList := TOrderedList<Cardinal>.Create(False, TComparison_Cardinal); + try + for I := High(CValues) downto Low(CValues) do LList.Add(CValues[I]); + for I := Low(CValues) to High(CValues) do + Require(LList.Add(CValues[I]) = -1, 'Disallowed duplicate insertion changed policy'); + Require(LList.Count = Length(CValues), 'Disallowed duplicates changed the list count'); + Require(not LList.Find(2, LFound) and (LFound = 2), 'Missing low value has the wrong insertion position'); + Require(not LList.Find($80000001, LFound) and (LFound = 4), 'Missing unsigned value has the wrong insertion position'); + LList.Remove(2); + Require(LList.Count = Length(CValues), 'Removing a missing value changed the list'); + finally LList.Free; end; + LList := TOrderedList<Cardinal>.Create(True, TComparison_Cardinal); + try + for I := High(CValues) downto Low(CValues) do begin + LList.Add(CValues[I]); LList.Add(CValues[I]); + end; + Require(LList.Count = 2 * Length(CValues), 'Allowed duplicates were not retained'); + for I := Low(CValues) to High(CValues) do begin + Require((LList[2 * I] = CValues[I]) and (LList[2 * I + 1] = CValues[I]), + 'Allowed duplicates lost full Cardinal ordering'); + end; + LList.Remove(High(Cardinal)); + Require((LList.Count = 9) and LList.Find(High(Cardinal), LFound), 'Single removal deleted all allowed duplicates'); + LList.Remove(High(Cardinal), True); + Require((LList.Count = 8) and not LList.Find(High(Cardinal), LFound), 'Remove-all left an unsigned duplicate'); + LList.Clear; + Require((LList.Count = 0) and not LList.Find(0, LFound) and (LFound = 0), + 'Actual empty-list Find/Clear behavior changed'); + finally LList.Free; end; +end; + +procedure CheckOrderedCardinal(ACheck: TOrderedCardinalCheck); +begin + case ACheck of + occBoundaryPairs: CheckBoundaryPairs; + occInsertionPermutations: CheckPermutations; + occDuplicateAndMissingValues: CheckDuplicateAndMissingValues; + end; +end; + +function RunOrderedCardinalChecks(const ALogFile: String): Integer; +var LLog: TStringList; LCheck: TOrderedCardinalCheck; +begin + Result := 0; + LLog := TStringList.Create; + try + for LCheck := Low(TOrderedCardinalCheck) to High(TOrderedCardinalCheck) do + try + CheckOrderedCardinal(LCheck); + LLog.Add('PASS ' + CNames[LCheck]); + except on E: Exception do begin + Result := 1; + LLog.Add('FAIL ' + CNames[LCheck] + ': ' + E.ClassName + ': ' + E.Message); + end; end; + LLog.Add('RESULT ' + IntToStr(Result)); + LLog.SaveToFile(ALogFile); + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/native/pending-protocol/NewPendingProtocolAccessCopy.ps1 b/src/tests/experimental/native/pending-protocol/NewPendingProtocolAccessCopy.ps1 new file mode 100644 index 000000000..8ff0ee211 --- /dev/null +++ b/src/tests/experimental/native/pending-protocol/NewPendingProtocolAccessCopy.ps1 @@ -0,0 +1,42 @@ +param([Parameter(Mandatory=$true)][string]$OutputDirectory) +$ErrorActionPreference = 'Stop' + +# Keep shipping visibility unchanged. Extended native checks compile this copy +# after moving three declarations; every other source character must match. +$destination = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $destination) { throw 'Choose a new output directory.' } +$source = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..\core\UNetProtocol.pas')) +$text = [IO.File]::ReadAllText($source) +$pattern = '(?m)^ (?:Procedure|Function) (?:DoProcess_GetPendingOperations_Request\(|DoProcess_GetPendingOperations;|ReadTcpClientBuffer\()[^\r\n]*\r?\n' +$declarations = [regex]::Matches($text, $pattern) +if ($declarations.Count -ne 3) { throw 'Expected exactly three protocol declarations.' } +$copy = $text +$lines = @() +foreach ($declaration in $declarations) { + $copy = $copy.Replace($declaration.Value, '') + $lines += $declaration.Value.TrimEnd([char[]]"`r`n") +} +$withoutDeclarations = $copy +$anchor = [regex]::Match($copy, '(?m)^ protected\r?\n(?= // Existing processing owners finish locally held block frames without)') +if (-not $anchor.Success) { throw 'Expected the protected transport section.' } +$newline = if ($anchor.Value.EndsWith("`r`n")) { "`r`n" } else { "`n" } +$insert = $anchor.Value + ($lines -join $newline) + $newline +$copy = $copy.Remove($anchor.Index, $anchor.Length).Insert($anchor.Index, $insert) +if (([regex]::Matches($copy, $pattern).Count -ne 3) -or + ([regex]::Replace($copy, $pattern, '') -cne $withoutDeclarations)) { + throw 'Protocol access copy changed more than declaration visibility.' +} +$coreDirectory = Join-Path $destination 'src\core' +New-Item -ItemType Directory -Path $coreDirectory | Out-Null +$target = Join-Path $coreDirectory 'UNetProtocol.pas' +[IO.File]::WriteAllText($target, $copy, [Text.UTF8Encoding]::new($false)) +Copy-Item -LiteralPath (Join-Path (Split-Path (Split-Path $source)) 'config.inc') -Destination (Join-Path $destination 'src\config.inc') +[pscustomobject]@{ + source = $source + sourceSha256 = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash + copy = $target + copySha256 = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash + movedDeclarations = 3 + otherSourceTextUnchanged = $true +} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $destination 'source-equivalence.json') +Write-Output $coreDirectory diff --git a/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.dpr b/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.dpr new file mode 100644 index 000000000..8375ca78a --- /dev/null +++ b/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.dpr @@ -0,0 +1,8 @@ +program PendingProtocolProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$APPTYPE CONSOLE} +uses System.SysUtils, UPendingProtocolChecks; +begin + if ParamCount<>1 then Halt(64); + ExitCode:=RunPendingProtocolChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.dproj b/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.dproj new file mode 100644 index 000000000..9f909755c --- /dev/null +++ b/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.dproj @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{CA60E715-D278-4358-88CF-FD1B4C7CFA65}</ProjectGuid> + <MainSource>PendingProtocolProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Console</AppType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\generics.collections\inc</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\pending-protocol\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\pending-protocol\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UPendingProtocolChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">PendingProtocolProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.lpr b/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.lpr new file mode 100644 index 000000000..60d473a3a --- /dev/null +++ b/src/tests/experimental/native/pending-protocol/PendingProtocolProbe.lpr @@ -0,0 +1,9 @@ +program PendingProtocolProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$mode delphi} +uses heaptrc, SysUtils, UPendingProtocolChecks; +begin + if ParamCount<>1 then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + ExitCode:=RunPendingProtocolChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/pending-protocol/ProofOfWorkVectors.py b/src/tests/experimental/native/pending-protocol/ProofOfWorkVectors.py new file mode 100644 index 000000000..e9847b9bb --- /dev/null +++ b/src/tests/experimental/native/pending-protocol/ProofOfWorkVectors.py @@ -0,0 +1,33 @@ +"""Independent protocol-1 SHA256d vectors for the checked native regression. + +Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +Run with Python 3; this script does not import or execute PascalCoin code. +""" + +import hashlib +import json +import struct + + +def sha256d(data): + return hashlib.sha256(hashlib.sha256(data).digest()).hexdigest().upper() + + +# Standard secp256k1 generator coordinates: a fixed, valid public key. +x = bytes.fromhex("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798") +y = bytes.fromhex("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8") +key = struct.pack("<HH", 714, len(x)) + x + struct.pack("<H", len(y)) + y +part1 = struct.pack("<I", 42) + key + struct.pack("<QHHI", 500000, 1, 6, 0x19000000) +# The operations tree is empty; protocol 1 hashes only the fee's low four bytes. +part3 = bytes(range(32)) + hashlib.sha256(b"").digest() + struct.pack("<I", 7) +tail = struct.pack("<II", 1710000000, 123456) +expected = ( + "73E7793B06F15AC2B31520C315B3DC8C638BB34B2F2010E50976AE68F042C7B7", + "3B274E21701CDD8721E209C76FCD079985D601F9630729F53970D75762E1CA53", +) +vectors = [] +for payload, digest in zip((b"", b"pending-protocol-qa"), expected): + serialized = part1 + payload + part3 + tail + assert sha256d(serialized) == digest + vectors.append({"payload_hex": payload.hex(), "input_hex": serialized.hex(), "sha256d": digest}) +print(json.dumps(vectors, indent=2)) diff --git a/src/tests/experimental/native/pending-protocol/README.md b/src/tests/experimental/native/pending-protocol/README.md new file mode 100644 index 000000000..503b745d0 --- /dev/null +++ b/src/tests/experimental/native/pending-protocol/README.md @@ -0,0 +1,31 @@ +# Pending protocol availability regression + +Checks the peer protocol and mining server while pending operations are +unavailable. It never contacts public peers or opens a user's wallet or data. + +Build `PendingProtocolProbe.lpr` (FPC) or `PendingProtocolProbe.dpr` (Delphi) +with the usual core and library search paths. Enable range and overflow +checking (`-Cr -Co` for FPC). Put OpenSSL 1.1 and +[mainnet-blocks-0-4.bin](../../fixtures/mainnet-blocks-0-4.bin) beside the +executable, and point `TEMP` and `TMP` at a test folder. Run it through the +[private-desktop broker](../README.md#private-desktop-broker) with a new absolute +result-log path. FPC's heap report must show zero unfreed blocks. + +The default build runs six cases: + +- three miner availability cases +- concurrent job retirement +- notification-lease shutdown +- proof-of-work vectors, reproducible with `python ProofOfWorkVectors.py` + +Three more cover legacy decoding of zero available operations with the +free-account refusal reply, and error replies to pending count and page +requests. To run all nine: + +1. Run `NewPendingProtocolAccessCopy.ps1 -OutputDirectory <new-directory>`. +2. Put its returned core directory before the normal core search path. +3. Define `PASCAL_PROTOCOL_INTERNAL_CHECKS`. +4. Use a new compiler output directory. + +`../RunWalletRegressionTests.ps1 -Probe pending-protocol` does all of this, +requires all nine cases and recomputes the vectors with Python. diff --git a/src/tests/experimental/native/pending-protocol/UPendingProtocolChecks.pas b/src/tests/experimental/native/pending-protocol/UPendingProtocolChecks.pas new file mode 100644 index 000000000..304cb6593 --- /dev/null +++ b/src/tests/experimental/native/pending-protocol/UPendingProtocolChecks.pas @@ -0,0 +1,563 @@ +unit UPendingProtocolChecks; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$IFDEF FPC}{$mode delphi}{$ENDIF} +interface +function RunPendingProtocolChecks(const ALogFile: String): Integer; +implementation +uses Windows, Classes, SysUtils, SyncObjs, Generics.Collections, blcksock, UNode, UNetProtocol, + UThread, UBlockChain, UAccounts, UOpTransaction, UPoolMining, + UPCTNetDataExtraMessages, UCrypto, UConst, UPCDataTypes, UBaseTypes, ULog, UFileStorage; +type + TAccessConnection = class(TNetConnection) + {$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} + procedure Serve(const AHeader: TNetHeaderData; AData: TStream); + procedure RequestPending; + procedure Prime; + {$ENDIF} + end; + TAccessNetData = class(TNetData) + procedure ServeExtra(AConnection: TNetConnection; const AHeader: TNetHeaderData; + ARequest, AResponse: TStream); + end; + TJobSnapshotPool = class(TPoolMiningServer) + private + FArm, FRequestHeld: LongInt; + protected + procedure CaptureJobValues(Operations: TPCOperationsComp; + out AValues: TMinerValuesForWork; out AJobsCount, AOperationsCount: Integer); override; + public + Captured, Replacement, ReleaseCapture: TEvent; + CapturedValues, ReplacementValues: TMinerValuesForWork; + constructor Create; override; + destructor Destroy; override; + procedure Arm; + end; + {$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} + TPendingRequest = class(TPCThread) + Connection: TAccessConnection; + Done: TEvent; + Error: String; + constructor Create(AConnection: TAccessConnection); + destructor Destroy; override; + procedure BCExecute; override; + end; + {$ENDIF} + TFixture = class + Node: TNode; + Net: TAccessNetData; + Connection: TAccessConnection; + Peer: TTCPBlockSocket; + Buffer: TMemoryStream; + Key: TECPrivateKey; + Scope: Boolean; + OldDirectory: String; + constructor Create; + destructor Destroy; override; + procedure BeginWork; + procedure EndWork; + procedure SeedRawOperation; + procedure LoadGenesis; + end; +procedure Require(AValue: Boolean; const AReason: String); +begin + if not AValue then raise Exception.Create(AReason); +end; +procedure Pump; +begin + CheckSynchronize(0); + Sleep(1); +end; + +constructor TJobSnapshotPool.Create; +begin + inherited; + Captured := TEvent.Create(nil,True,False,''); + Replacement := TEvent.Create(nil,True,False,''); + ReleaseCapture := TEvent.Create(nil,True,False,''); +end; + +destructor TJobSnapshotPool.Destroy; +begin + // Release a failed test's actual accepted-client callback before joining it. + ReleaseCapture.SetEvent; + inherited; + ReleaseCapture.Free; + Replacement.Free; + Captured.Free; +end; + +procedure TJobSnapshotPool.Arm; +begin + InterlockedExchange(FArm,1); +end; + +procedure TJobSnapshotPool.CaptureJobValues(Operations: TPCOperationsComp; + out AValues: TMinerValuesForWork; out AJobsCount, AOperationsCount: Integer); +begin + inherited CaptureJobValues(Operations,AValues,AJobsCount,AOperationsCount); + if not Assigned(Operations) and (InterlockedCompareExchange(FArm,0,1)=1) then begin + CapturedValues := AValues; + InterlockedExchange(FRequestHeld,1); + Captured.SetEvent; + // Hold the real request after capture, with no pool/node/socket lock. + // The separate actual pool worker must be able to retire its borrowed job. + if ReleaseCapture.WaitFor(10000)<>wrSignaled then + raise Exception.Create('Timed out releasing the captured miner request'); + InterlockedExchange(FRequestHeld,0); + end else if Assigned(Operations) and + (InterlockedCompareExchange(FRequestHeld,0,0)<>0) then begin + ReplacementValues := AValues; + Replacement.SetEvent; + end; +end; +{$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} +procedure TAccessConnection.Serve(const AHeader: TNetHeaderData; AData: TStream); +begin + DoProcess_GetPendingOperations_Request(AHeader,AData); +end; +procedure TAccessConnection.RequestPending; +begin + DoProcess_GetPendingOperations; +end; +procedure TAccessConnection.Prime; +var H: TNetHeaderData; S: TMemoryStream; LUntil: UInt64; LRead: Boolean; +begin + S:=TMemoryStream.Create; + try + // The real decoder may return after a single empty 100ms receive wait. + // Retry its public read boundary while the owned receiver starts up. + LUntil:=GetTickCount64+3000; + repeat + LRead:=ReadTcpClientBuffer(100,H,S); + if not LRead then Pump; + until LRead or not Connected or (GetTickCount64>=LUntil); + Require(LRead,'Cannot prime the actual protocol decoder'); + finally S.Free; end; +end; +{$ENDIF} +procedure TAccessNetData.ServeExtra(AConnection: TNetConnection; const AHeader: TNetHeaderData; + ARequest, AResponse: TStream); +begin + DoProcessReservedAreaMessage(AConnection,AHeader,ARequest,AResponse); +end; +{$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} +constructor TPendingRequest.Create(AConnection: TAccessConnection); +begin + Connection:=AConnection; Done:=TEvent.Create(nil,True,False,''); + inherited Create(True); FreeOnTerminate:=False; +end; +destructor TPendingRequest.Destroy; +begin + Terminate; + if Done.WaitFor(0)<>wrSignaled then Connection.Connected:=False; + WaitFor; Done.Free; inherited; +end; +procedure TPendingRequest.BCExecute; +begin + try try Connection.RequestPending; + except on E: Exception do Error:=E.ClassName+': '+E.Message; end; + finally Done.SetEvent; end; +end; +{$ENDIF} +procedure SendFrame(APeer: TTCPBlockSocket; AOperation: Word; ARequest: Cardinal; + AError: Word; APayload: TMemoryStream); +var S: TMemoryStream; C: Cardinal; W: Word; +begin + S:=TMemoryStream.Create; + try + C:=CT_MagicNetIdentification; S.WriteBuffer(C,4); + W:=CT_MagicResponse; S.WriteBuffer(W,2); + W:=AOperation; S.WriteBuffer(W,2); W:=AError; S.WriteBuffer(W,2); + S.WriteBuffer(ARequest,4); + W:=CT_NetProtocol_Version; S.WriteBuffer(W,2); + W:=CT_NetProtocol_Available; S.WriteBuffer(W,2); + C:=APayload.Size; S.WriteBuffer(C,4); + APayload.Position:=0; S.CopyFrom(APayload,APayload.Size); + Require(APeer.SendBuffer(S.Memory,Integer(S.Size))=S.Size,'Cannot send the owned protocol frame'); + finally S.Free; end; +end; +function ReadFrame(APeer: TTCPBlockSocket; ABuffer: TMemoryStream; + out AHeader: TNetHeaderData; APayload: TMemoryStream; ATimeout: Cardinal): Boolean; +var LUntil: UInt64; LBytes: RawByteString; LMore: Boolean; LRest: TMemoryStream; +begin + Result:=False; LUntil:=GetTickCount64+ATimeout; + repeat + ABuffer.Position:=0; + if TNetData.ExtractHeaderInfo(ABuffer,AHeader,APayload,LMore) then begin + LRest:=TMemoryStream.Create; + try + if ABuffer.Position<ABuffer.Size then LRest.CopyFrom(ABuffer,ABuffer.Size-ABuffer.Position); + ABuffer.Clear; LRest.Position:=0; ABuffer.CopyFrom(LRest,LRest.Size); + finally LRest.Free; end; + APayload.Position:=0; Exit(True); + end; + if APeer.CanRead(10) then begin + LBytes:=APeer.RecvPacket(100); + if Length(LBytes)>0 then begin + ABuffer.Position:=ABuffer.Size; ABuffer.WriteBuffer(LBytes[1],Length(LBytes)); + end; + end; + Pump; + until GetTickCount64>=LUntil; +end; +constructor TFixture.Create; +var LListener: TTCPBlockSocket; LID: TGUID; LDirectory: String; S: TMemoryStream; +begin + Require(not TNode.NodeExists and not TNetData.NetDataExists,'Protocol checks need fresh owned singletons'); + OldDirectory:=TNode.GetPascalCoinDataFolder; CreateGUID(LID); + LDirectory:=IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP'))+'PendingProtocol-'+GUIDToString(LID); + Require(not DirectoryExists(LDirectory),'Refusing an existing protocol data directory'); + ForceDirectories(LDirectory); TNode.SetPascalCoinDataFolder(LDirectory); + TCrypto.InitCrypto; Net:=TAccessNetData.Create(nil); Node:=TNode.Create(nil); + Node.NetServer.Port:=0; Net.NetConnectionsActive:=False; + Buffer:=TMemoryStream.Create; Key:=TECPrivateKey.Create; + Key.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + Net.NetConnectionsActive:=True; + LListener:=TTCPBlockSocket.Create; + try + LListener.CreateSocket; LListener.Bind('127.0.0.1','0'); LListener.Listen; + Require(LListener.LastError=0,'Cannot listen on the owned raw peer'); + Connection:=TAccessConnection.Create(nil); + Require(Connection.ConnectTo('127.0.0.1',LListener.GetLocalSinPort),'Cannot connect the actual transport'); + Require(LListener.CanRead(1000),'Owned listener did not accept the transport'); + Peer:=TTCPBlockSocket.Create; Peer.Socket:=LListener.Accept; + finally LListener.Free; end; + {$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} + // Decode a benign response through the real transport to establish the + // available protocol version; this base connection has no competing parser. + S:=TMemoryStream.Create; + try SendFrame(Peer,CT_NetOp_GetBlockHeaders,0,0,S); Connection.Prime; + finally S.Free; end; + {$ENDIF} +end; +destructor TFixture.Destroy; +begin + if Scope then EndWork; + Peer.Free; Connection.Free; + if Assigned(Net) then Net.NetConnectionsActive:=False; + Net.Free; Node.Free; Key.Free; Buffer.Free; + TNode.SetPascalCoinDataFolder(OldDirectory); inherited; +end; +procedure TFixture.BeginWork; +begin + Node.BeginBlockChainUpdate; + Scope:=True; + Require(not Node.PendingOperationsAvailable,'Scope did not hide pending overlay'); +end; +procedure TFixture.EndWork; +begin + Node.EndBlockChainUpdate; + Scope:=False; +end; +procedure TFixture.SeedRawOperation; +var LOp: TPCOperation; LPool: TPCOperationsComp; LError: String; +begin + // A signed raw package entry makes zero-available replies distinguishable + // from the retained raw count. This case tests wire availability, not account + // admission; the empty confirmed bank is never used to validate this entry. + LOp:=TOpTransaction.CreateTransaction(CT_PROTOCOL_1,0,1,1,Key,1,1,CT_TOperationPayload_NUL); + try + LPool:=Node.LockMempoolWrite; + try Require(LPool.AddOperation(False,LOp,LError),LError); + finally Node.UnlockMempoolWrite; end; + finally LOp.Free; end; +end; +procedure TFixture.LoadGenesis; +var S: TFileStream; B: TPCOperationsComp; C: Cardinal; E: String; +begin + Node.Bank.StorageClass:=TFileStorage; + S:=TFileStream.Create(ExtractFilePath(ParamStr(0))+'mainnet-blocks-0-4.bin',fmOpenRead or fmShareDenyNone); + B:=TPCOperationsComp.Create(nil); + try + S.ReadBuffer(C,4); Require(C=5,'Unexpected public historic fixture'); + B.bank:=Node.Bank; Require(B.LoadBlockFromStream(S,E),E); + Require(B.OperationBlock.block=0,'Expected the real public genesis block'); + Require(Node.AddNewBlockChain(nil,B,E),E); + Require(Node.Bank.BlocksCount=1,'Public genesis did not commit through actual validation'); + finally B.Free; S.Free; end; +end; +{$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} +procedure CheckBusyWire; +var F: TFixture; H,R: TNetHeaderData; Q,S: TMemoryStream; B: Byte; C: Cardinal; + Tree: TOperationsHashTree; E: String; P,PC: Integer; Extra: TPCTNetDataExtraMessages; W: Word; +begin + F:=TFixture.Create; Q:=TMemoryStream.Create; S:=TMemoryStream.Create; + try + F.SeedRawOperation; F.BeginWork; + H:=Default(TNetHeaderData); H.header_type:=ntp_request; H.operation:=CT_NetOp_GetPendingOperations; + for B:=1 to 2 do begin + Q.Clear; Q.WriteBuffer(B,1); C:=0; + if B=2 then begin + Q.WriteBuffer(C,4); + C:=10; + Q.WriteBuffer(C,4); + end; + H.request_id:=F.Net.NewRequestId; Q.Position:=0; F.Connection.Serve(H,Q); + repeat Require(ReadFrame(F.Peer,F.Buffer,R,S,2000),'No pending response'); until R.header_type=ntp_response; + Require((R.request_id=H.request_id) and not R.is_error,'Unavailable pending response broke legacy wire compatibility'); + if B=1 then begin + Require((S.Size=4) and (S.Read(C,4)=4) and (C=0),'Legacy count decoder did not see zero available operations'); + end else begin + Tree:=TOperationsHashTree.Create; + try + Require(Tree.LoadOperationsHashTreeFromStream(S,False,CT_PROTOCOL_1,CT_PROTOCOL_1,nil, + CT_AllowPropagate0feeOperations,P,PC,E),E); + Require(Tree.OperationsCount=0,'Legacy tree decoder did not see an empty available package'); + finally Tree.Free; end; + end; + Require(F.Connection.Connected,'A normal unavailable response disconnected the peer'); + end; + Extra:=TPCTNetDataExtraMessages.Create(F.Node,F.Net,nil); + try + Q.Clear; S.Clear; TStreamOp.WriteAccountKey(Q,F.Key.PublicKey); Q.Position:=0; + H.operation:=CT_NetProtocol_Extra_NetOp_GIVE_ME_AN_ACCOUNT; + F.Net.ServeExtra(F.Connection,H,Q,S); S.Position:=0; + Require((S.Size=2) and (S.Read(W,2)=2) and (W=0),'Catch-up did not use the existing free-account refusal reply'); + finally Extra.Free; end; + finally S.Free; Q.Free; F.Free; end; +end; +procedure CheckMatchingError(AErrorOnPage: Boolean); +var F: TFixture; T: TPendingRequest; H: TNetHeaderData; S,Q: TMemoryStream; + C: Cardinal; B: Byte; LUntil: UInt64; +begin + F:=TFixture.Create; T:=nil; S:=TMemoryStream.Create; Q:=TMemoryStream.Create; + try + // Pending requests need a validated local tip that includes the peer's tip. + F.LoadGenesis; + T:=TPendingRequest.Create(F.Connection); T.Start; + repeat Require(ReadFrame(F.Peer,F.Buffer,H,S,2000),'Requester did not send a pending request'); + until H.operation=CT_NetOp_GetPendingOperations; + Require(S.Read(B,1)=1,'Missing pending request kind'); + if AErrorOnPage then begin + Require(B=1,'First pending request is not a count'); C:=1; Q.WriteBuffer(C,4); + SendFrame(F.Peer,H.operation,H.request_id,0,Q); + Require(ReadFrame(F.Peer,F.Buffer,H,S,2000),'Requester did not ask for the available page'); + Require((H.operation=CT_NetOp_GetPendingOperations) and (S.Read(B,1)=1) and (B=2),'Missing pending page request'); + end; + Q.Clear; TStreamOp.WriteAnsiString(Q,TEncoding.ASCII.GetBytes('Temporary pending unavailability')); + SendFrame(F.Peer,H.operation,H.request_id,CT_NetError_NotAvailable,Q); + LUntil:=GetTickCount64+1500; + repeat Pump; until (T.Done.WaitFor(0)=wrSignaled) or (GetTickCount64>=LUntil); + Require(T.Done.WaitFor(0)=wrSignaled,'Matching protocol error was parsed as another pending request'); + Require(T.Error='',T.Error); + Require(F.Connection.Connected,'Matching protocol error was misclassified as malformed peer data'); + finally T.Free; Q.Free; S.Free; F.Free; end; +end; +{$ENDIF} +function AwaitJSON(APeer: TTCPBlockSocket; var ABuffer: String; const APhase: String): String; +var LUntil: UInt64; I: Integer; +begin + LUntil:=GetTickCount64+4000; + repeat + I:=Pos(#10,ABuffer); + if I>0 then begin + Result:=Copy(ABuffer,1,I-1); + Delete(ABuffer,1,I); + Exit; + end; + if APeer.CanRead(10) then ABuffer:=ABuffer+String(APeer.RecvPacket(100)); + Pump; + until GetTickCount64>=LUntil; + raise Exception.Create('No JSON response from the actual miner owner: '+APhase); +end; +procedure CheckBusyMiner(ASeedBank: Boolean = True; AEmptyPayload: Boolean = False); +var F: TFixture; Pool: TPoolMiningServer; Miner, Reservation: TTCPBlockSocket; + Port: Word; Payload: TRawBytes; Buffer, Line: String; +begin + F:=TFixture.Create; Pool:=nil; Miner:=nil; Reservation:=TTCPBlockSocket.Create; + try + if ASeedBank then F.LoadGenesis; + F.BeginWork; Reservation.CreateSocket; Reservation.Bind('127.0.0.1','0'); + Require(Reservation.LastError=0,'Cannot reserve an owned mining port'); + Port:=Reservation.GetLocalSinPort; Reservation.CloseSocket; + Pool:=TPoolMiningServer.Create; Pool.Port:=Port; + if AEmptyPayload then Payload:=nil else Payload.FromString('pending-protocol-qa'); + TLog.NewLog(ltinfo,'PendingProtocolQA',Format('Miner fixture blocks:%d payloadbytes:%d',[ + F.Node.Bank.BlocksCount,Length(Payload)])); + Pool.UpdateAccountAndPayload(F.Key.PublicKey,Payload); Pool.Active:=True; + Miner:=TTCPBlockSocket.Create; Miner.Connect('127.0.0.1',IntToStr(Port)); + Require(Miner.LastError=0,'Cannot connect to the actual mining owner'); + Miner.SendString('{"method":"miner-notify","params":[],"id":17}'+#10); + Buffer:=''; Line:=StringReplace(AwaitJSON(Miner,Buffer,'busy request error'),' ','',[rfReplaceAll]); + Require((Pos('"id":17',Line)>0) and (Pos('"error":',Line)>0) and + (Pos('"error":null',Line)=0),'Busy miner request did not receive its normal JSON RPC error'); + Require(Pool.ClientsCount=1,'Busy miner request disconnected its connection'); + F.EndWork; Line:=AwaitJSON(Miner,Buffer,'resumed mining job'); + Require((Pos('miner-notify',Line)>0) and (Pos('"part1":',Line)>0), + 'The same miner connection did not receive a real job after availability'); + Require(Pool.ClientsCount=1,'Miner did not remain connected through availability'); + finally + if Assigned(Miner) then Miner.CloseSocket; + Pool.Free; Miner.Free; Reservation.Free; F.Free; + end; +end; +procedure CheckNotificationLease; +var F: TFixture; LConnections: TList<TNetConnection>; LOperations: TOperationsHashTree; + I: Integer; LStarted: UInt64; +begin + F:=TFixture.Create; LConnections:=TList<TNetConnection>.Create; + LOperations:=TOperationsHashTree.Create; + try + F.Net.AcquireDownloadConnections(LConnections); + Require((LConnections.Count=1) and (LConnections[0]=F.Connection), + 'Cannot acquire the existing connection lifetime lease'); + Require(F.Net.ConnectionLock(F,F.Connection,0),'Cannot pin the notification enqueue boundary'); + try + LConnections[0]:=nil; + TThreadNodeNotifyOperations.Create(F.Connection,LOperations,F.Net); + finally F.Net.ConnectionUnlock(F.Connection); end; + LStarted:=GetTickCount64; + F.Net.BeginShutdown; + F.Connection.Connected:=False; + FreeAndNil(F.Connection); + Require(F.Net.ConnectionsCountAll=0,'Transferred notifier lease left a retired connection registered'); + Require(GetTickCount64-LStarted<5000,'Owned notification did not cooperate with orderly shutdown'); + finally + for I:=0 to LConnections.Count-1 do + if Assigned(LConnections[I]) then F.Net.ReleaseDownloadConnection(LConnections[I]); + LOperations.Free; LConnections.Free; F.Free; + end; +end; + +procedure CheckMinerJobRetirement; +var F: TFixture; Pool: TJobSnapshotPool; Miner, Reservation: TTCPBlockSocket; + Port: Word; LOldPayload, LNewPayload: TRawBytes; Buffer, Line: String; + LUntil: UInt64; +begin + F := TFixture.Create; + Pool := nil; + Miner := nil; + Reservation := TTCPBlockSocket.Create; + try + F.LoadGenesis; + Reservation.CreateSocket; + Reservation.Bind('127.0.0.1','0'); + Require(Reservation.LastError=0,'Cannot reserve the snapshot mining port'); + Port := Reservation.GetLocalSinPort; + Reservation.CloseSocket; + LOldPayload.FromString('snapshot-old'); + LNewPayload.FromString('snapshot-new'); + Pool := TJobSnapshotPool.Create; + Pool.Port := Port; + Pool.UpdateAccountAndPayload(F.Key.PublicKey,LOldPayload); + Pool.Active := True; + Miner := TTCPBlockSocket.Create; + Miner.Connect('127.0.0.1',IntToStr(Port)); + Require(Miner.LastError=0,'Cannot connect the snapshot miner'); + Buffer := ''; + Line := AwaitJSON(Miner,Buffer,'initial snapshot job'); + Require(Pos('miner-notify',Line)>0,'The snapshot miner has no initial job'); + Pool.Arm; + Miner.SendString('{"method":"miner-notify","params":[],"id":88}'+#10); + LUntil := GetTickCount64+4000; + repeat Pump; until (Pool.Captured.WaitFor(0)=wrSignaled) or (GetTickCount64>=LUntil); + Require(Pool.Captured.WaitFor(0)=wrSignaled,'The actual miner request did not capture its job'); + Require(TBaseType.Equals(Pool.CapturedValues.payload_start,LOldPayload), + 'The request did not capture the original job payload'); + Pool.UpdateAccountAndPayload(F.Key.PublicKey,LNewPayload); + F.Node.NotifyBlocksChanged; + LUntil := GetTickCount64+4000; + repeat Pump; until (Pool.Replacement.WaitFor(0)=wrSignaled) or (GetTickCount64>=LUntil); + Require(Pool.Replacement.WaitFor(0)=wrSignaled, + 'Job retirement was blocked by the paused miner request'); + Require(TBaseType.Equals(Pool.ReplacementValues.payload_start,LNewPayload), + 'The real pool worker did not clear and replace the old job'); + Require(TBaseType.Equals(Pool.CapturedValues.payload_start,LOldPayload), + 'Clearing the pool changed the independently captured request'); + Pool.ReleaseCapture.SetEvent; + LUntil := GetTickCount64+4000; + repeat + Line := StringReplace(AwaitJSON(Miner,Buffer,'retired snapshot response'),' ','',[rfReplaceAll]); + until (Pos('"id":88',Line)>0) or (GetTickCount64>=LUntil); + Require((Pos('"id":88',Line)>0) and + (Pos('"payload_start":"'+TCrypto.ToHexaString(LOldPayload)+'"',Line)>0), + 'The actual response used a retired/replacement job instead of its captured value'); + Require(Pos('"part1":"'+TCrypto.ToHexaString(Pool.CapturedValues.part1)+'"',Line)>0, + 'The response digest prefix differs from its captured job'); + Require(Pos('"part3":"'+TCrypto.ToHexaString(Pool.CapturedValues.part3)+'"',Line)>0, + 'The response digest suffix differs from its captured job'); + Require(Pool.ClientsCount=1,'Concurrent job retirement disconnected the miner'); + finally + if Assigned(Pool) then Pool.ReleaseCapture.SetEvent; + if Assigned(Miner) then Miner.CloseSocket; + Pool.Free; + Miner.Free; + Reservation.Free; + F.Free; + end; +end; + +procedure CheckProofOfWorkVectors; +const + CExpected: array[0..1] of String = ( + '73E7793B06F15AC2B31520C315B3DC8C638BB34B2F2010E50976AE68F042C7B7', + '3B274E21701CDD8721E209C76FCD079985D601F9630729F53970D75762E1CA53'); +var B: TOperationBlock; O: TPCOperationsComp; K: TAccountKey; + Payload, LReference: TRawBytes; I, J: Integer; +begin + // Fixed SHA256d vectors were computed independently with Python struct/hashlib + // from the protocol's little-endian fields, not from the method under test. + K:=CT_TECDSA_Public_Nul; K.EC_OpenSSL_NID:=714; + Require(TCrypto.HexaToRaw('79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798',K.x),'Invalid fixed X'); + Require(TCrypto.HexaToRaw('483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8',K.y),'Invalid fixed Y'); + for I:=0 to 1 do begin + if (I=0) and (GetEnvironmentVariable('PASCAL_PENDING_POW_NONEMPTY_ONLY')='1') then Continue; + O:=TPCOperationsComp.Create(nil); + try + B:=CT_OperationBlock_NUL; B.block:=42; B.reward:=500000; B.fee:=7; + B.protocol_version:=CT_PROTOCOL_1; B.protocol_available:=6; B.compact_target:=$19000000; + SetLength(B.initial_safe_box_hash,32); + for J:=0 to 31 do B.initial_safe_box_hash[J]:=J; + O.SetOperationBlock(B); O.AccountKey:=K; + if I=0 then Payload:=nil else Payload.FromString('pending-protocol-qa'); + Require(O.Update_And_RecalcPOW(123456,1710000000,Payload),'Fixed vector did not recalculate'); + Require(SameText(TCrypto.ToHexaString(O.OperationBlock.proof_of_work),CExpected[I]), + 'Independent proof-of-work digest mismatch, payload='+IntToStr(I)); + TPascalCoinProtocol.CalcProofOfWork(O.OperationBlock,LReference); + Require(SameText(TCrypto.ToHexaString(LReference),CExpected[I]), + 'Independent full-serializer digest mismatch, payload='+IntToStr(I)); + Require(TBaseType.Equals(LReference,O.OperationBlock.proof_of_work), + 'Cached and full proof-of-work serializers differ'); + finally O.Free; end; + end; +end; + +function RunPendingProtocolChecks(const ALogFile: String): Integer; +var LLog: TStringList; I, LOnly: Integer; LCoreLog: TLog; +begin + Require(not FileExists(ALogFile),'Refusing an existing protocol result log'); + Result:=0; LLog:=TStringList.Create; LCoreLog:=TLog.Create(nil); + try + LCoreLog.ProcessGlobalLogs:=True; LCoreLog.SaveTypes:=CT_TLogTypes_ALL; + LCoreLog.FileName:=ALogFile+'.core.log'; + LOnly:=StrToIntDef(GetEnvironmentVariable('PASCAL_PENDING_PROTOCOL_ONLY'),-1); + {$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} + for I:=0 to 8 do begin + {$ELSE} + for I:=3 to 8 do begin + {$ENDIF} + if (LOnly>=0) and (I<>LOnly) then Continue; + try + case I of + {$IFDEF PASCAL_PROTOCOL_INTERNAL_CHECKS} + 0: CheckBusyWire; + 1: CheckMatchingError(False); + 2: CheckMatchingError(True); + {$ENDIF} + 3: CheckBusyMiner; + 4: CheckNotificationLease; + 5: CheckBusyMiner(False,False); + 6: CheckBusyMiner(False,True); + 7: CheckProofOfWorkVectors; + 8: CheckMinerJobRetirement; + end; + LLog.Add('PASS pending protocol case='+IntToStr(I)); + except on E: Exception do begin + Result:=1; + LLog.Add('FAIL case='+IntToStr(I)+' '+E.ClassName+': '+E.Message); + end; end; + LLog.SaveToFile(ALogFile); + end; + finally LCoreLog.Free; LLog.Free; end; +end; +end. diff --git a/src/tests/experimental/native/safebox-recovery/README.md b/src/tests/experimental/native/safebox-recovery/README.md new file mode 100644 index 000000000..b7411390b --- /dev/null +++ b/src/tests/experimental/native/safebox-recovery/README.md @@ -0,0 +1,38 @@ +# Interrupted safebox recovery + +`SafeboxRecoveryProbe` checks recovery from an interrupted safebox download. It +never opens the user's wallet, contacts public peers or switches the desktop. + +Run the FPC suite from the repository root: + +```powershell +& src\tests\experimental\RunWalletSafeboxRecoveryTests.ps1 -OutputDirectory <new-directory> +``` + +The script builds the probe and its [private-desktop broker](../README.md#private-desktop-broker), then +checks the result log, natural exit and heap report. The heap report and data +directories go beside the log. `-FpcPath` selects another Windows x64 compiler. + +The [Lazarus project](SafeboxRecoveryProbe.lpi) and +[Delphi project](SafeboxRecoveryProbe.dproj) run the same checks. For Delphi, +place the matching OpenSSL runtime beside the EXE. Run it only through the +script's `PrivateDesktopBroker.exe`, passing the EXE and a new absolute log +path. + +The probe's exit code is the number of failed cases, or `99` for a fatal fixture +error. The result log ends with `RESULT cases=8 failures=N`. + +## Cases + +- A late `GET_SAFEBOX` response is discarded without disconnecting or + blacklisting the peer. +- Socket EOF detaches a discovery endpoint during a download lease. +- After its only peer disconnects, the downloader retries its chunk request on a + replacement socket. +- EOF during chunk decompression lets validation finish before the worker joins. +- A start request during a download queues one follow-up download, without a + second HELLO. +- A peer-ahead HELLO on an empty chain defers pending-operation exchange. +- An invalid `GET_SAFEBOX` autosend logs its rejection and disconnects the peer. +- An invalid safebox chunk logs its rejection, disconnects the peer and + publishes no archive. diff --git a/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.dpr b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.dpr new file mode 100644 index 000000000..c4e745fb2 --- /dev/null +++ b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.dpr @@ -0,0 +1,10 @@ +program SafeboxRecoveryProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +uses USafeboxRecoveryChecks; + +begin + if ParamCount <> 1 then Halt(64); + ExitCode := RunSafeboxRecoveryChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.dproj b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.dproj new file mode 100644 index 000000000..254e39bc5 --- /dev/null +++ b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.dproj @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{10E80BBC-A1B6-4AB2-8AE4-CF51FC242E6D}</ProjectGuid> + <MainSource>SafeboxRecoveryProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>None</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_DcuOutput>..\..\lib\safebox-recovery\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\safebox-recovery\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"><MainSource>MainSource</MainSource></DelphiCompile> + <DCCReference Include="USafeboxRecoveryChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality><Source><Source Name="MainSource">SafeboxRecoveryProbe.dpr</Source></Source></Delphi.Personality> + <Platforms><Platform value="Win32">True</Platform><Platform value="Win64">True</Platform></Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> diff --git a/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.lpi b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.lpi new file mode 100644 index 000000000..6e0116f88 --- /dev/null +++ b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.lpi @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<CONFIG> + <ProjectOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <General> + <Flags><MainUnitHasCreateFormStatements Value="False"/><MainUnitHasTitleStatement Value="False"/></Flags> + <SessionStorage Value="InProjectDir"/> + <MainUnit Value="0"/> + <Title Value="SafeboxRecoveryProbe"/> + </General> + <BuildModes Count="1"><Item1 Name="Debug" Default="True"/></BuildModes> + <Units Count="2"> + <Unit0><Filename Value="SafeboxRecoveryProbe.lpr"/><IsPartOfProject Value="True"/></Unit0> + <Unit1><Filename Value="USafeboxRecoveryChecks.pas"/><IsPartOfProject Value="True"/></Unit1> + </Units> + </ProjectOptions> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <Target><Filename Value="..\..\lib\safebox-recovery\FPC\SafeboxRecoveryProbe"/></Target> + <SearchPaths> + <IncludeFiles Value="..\..\..\..\libraries\generics.collections\inc"/> + <OtherUnitFiles Value="..\..\..\..\core;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex;..\..\..\..\libraries\generics.collections"/> + <UnitOutputDirectory Value="..\..\lib\safebox-recovery\FPC\units"/> + </SearchPaths> + <Parsing><SyntaxOptions><SyntaxMode Value="Delphi"/></SyntaxOptions></Parsing> + <CodeGeneration><Checks><RangeChecks Value="True"/><OverflowChecks Value="True"/></Checks></CodeGeneration> + <Linking><Debugging><GenerateDebugInfo Value="True"/><UseExternalDbgSyms Value="False"/></Debugging><Options><Win32GraphicApp Value="True"/></Options></Linking> + <Other><CustomOptions Value="-dBorland -dVer150 -dDelphi7 -dCompiler6_Up -dPUREPASCAL"/></Other> + </CompilerOptions> +</CONFIG> diff --git a/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.lpr b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.lpr new file mode 100644 index 000000000..da08517c5 --- /dev/null +++ b/src/tests/experimental/native/safebox-recovery/SafeboxRecoveryProbe.lpr @@ -0,0 +1,13 @@ +program SafeboxRecoveryProbe; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +uses heaptrc, USafeboxRecoveryChecks; + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + ExitCode := RunSafeboxRecoveryChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/safebox-recovery/USafeboxRecoveryChecks.pas b/src/tests/experimental/native/safebox-recovery/USafeboxRecoveryChecks.pas new file mode 100644 index 000000000..4013350b3 --- /dev/null +++ b/src/tests/experimental/native/safebox-recovery/USafeboxRecoveryChecks.pas @@ -0,0 +1,1122 @@ +unit USafeboxRecoveryChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Real loopback transports exercise interrupted safebox requests, endpoint + discovery eligibility, request ownership and download-worker handoff. Synthetic + metadata reaches the transport only; it never represents a valid checkpoint. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +function RunSafeboxRecoveryChecks(const ALogFile: String): Integer; + +implementation + +uses Windows, Classes, SysUtils, SyncObjs, Generics.Collections, blcksock, + {$IFDEF FPC}{$IFDEF VER3_2}zStream,{$ELSE}paszlib_zStream,{$ENDIF}{$ELSE}ZLib,{$ENDIF} + UNode, UNetProtocol, UTCPIP, UThread, UBaseTypes, UConst, UAccounts, UBlockChain, UFileStorage, + UCrypto, UChunk, UPCDownloadSafebox, UPCTemporalFileStream, UPCDataTypes, ULog; + +const + CT_Deadline = 5000; + CT_Sentinel: AnsiString = 'Existing completed archive sentinel: do not replace'; + +type + TRecoveryCase = class; + + TRecoveryState = class + public + FirstBody, ReleaseBody, SecondBody: TEvent; + WorkerReady, WriteEntered, ReleaseWrite: TEvent; + Calls, Factories, ChunkEntries, ChunkExits, ChunkJoins: LongInt; + constructor Create; + destructor Destroy; override; + end; + + THandoffWorker = class(TThreadGetNewBlockChainFromClient) + protected + function SelectConnection(const AConnections: TList<TNetConnection>): TNetConnection; override; + end; + + TRecoveryNet = class(TNetData) + private + FState: TRecoveryState; + protected + function CreateBlockChainWorker: TThreadGetNewBlockChainFromClient; override; + public + constructor Create(AState: TRecoveryState); reintroduce; + procedure RequestDownload; + procedure GetNewBlockChainFromClient(Connection: TNetConnection; const why: String); override; + end; + + TLateResponseTask = class(TPCThread) + private + FCase: TRecoveryCase; + protected + procedure BCExecute; override; + public + Done: TEvent; + FirstID, SecondID: Cardinal; + FirstResult, SecondResult: Boolean; + Error: String; + constructor Create(ACase: TRecoveryCase); + destructor Destroy; override; + end; + + TDownloadAccess = class(TPCDownloadSafebox) + public type + TChunkThread = {$IFDEF FPC}TDownloadSBThread{$ELSE}TPCDownloadSafebox.TDownloadSBThread{$ENDIF}; + end; + + TGateChunkStream = class(TPCTemporalFileStream) + private + FState: TRecoveryState; + FEntered: LongInt; + public + constructor Create(AState: TRecoveryState); reintroduce; + function Write(const Buffer; Count: LongInt): LongInt; override; + end; + + TObservedChunk = class(TDownloadAccess.TChunkThread) + private + FState: TRecoveryState; + protected + procedure BCExecute; override; + public + constructor Create(AOwner: TPCDownloadSafebox; AStart, ACount: Cardinal; + AState: TRecoveryState); reintroduce; + end; + + TObservedDownload = class(TPCDownloadSafebox) + private + FState: TRecoveryState; + protected + function CreateChunkThread(ABlockStart, ACount: Cardinal): TDownloadAccess.TChunkThread; override; + procedure WaitForChunkThread(AThread: TThread); override; + public + constructor Create(AState: TRecoveryState); reintroduce; + end; + + TDownloadTask = class(TPCThread) + private + FCase: TRecoveryCase; + FGateValidation: Boolean; + protected + procedure BCExecute; override; + public + Done: TEvent; + DownloadResult: Boolean; + ChunksPublished: Integer; + Error: String; + constructor Create(ACase: TRecoveryCase; AGateValidation: Boolean); + destructor Destroy; override; + end; + + TRecoveryCase = class(TComponent) + private + FState: TRecoveryState; + FNet: TRecoveryNet; + FNode: TNode; + FListener, FPeer: TTCPBlockSocket; + FOld, FReplacement: TNetConnection; + FLeases: TList<TNetConnection>; + FWire: TMemoryStream; + FOldDirectory, FDirectory: String; + FPort: Word; + FOldGone, FReplacementGone: LongInt; + FLate: TLateResponseTask; + FDownload: TDownloadTask; + FCoreLog: TLog; + FProtocolRejections, FPacketRejections, FChunkRejections: LongInt; + procedure CoreLog(AType: TLogType; ATime: TDateTime; AThread: TThreadID; + const ASender, AText: String); + procedure PinActiveConnections; + procedure ReleasePins; + procedure AcceptConnection(AReplacement: Boolean); + procedure AcceptDiscoveredReplacement; + procedure DropPeer; + procedure AwaitDisconnected(AConnection: TNetConnection); + procedure AwaitEndpointDetached(var AAddress: TNodeServerAddress); + function AwaitRequest(ATimeout: Cardinal; AOperation: Word = CT_NetOp_GetSafeBox; + ARequired: Boolean = True): Cardinal; + procedure SendFrame(AType, AOperation: Word; ARequest: Cardinal; + AError: Word; APayload: TMemoryStream); + procedure SendReply(ARequest: Cardinal; AError: Word; APayload: TMemoryStream); + procedure CheckCancelledFiles; + protected + procedure Notification(AComponent: TComponent; Operation: TOperation); override; + public + constructor Create(const ABaseDirectory, AName: String); + destructor Destroy; override; + procedure CheckLateResponse; + procedure CheckEndpointReplacement; + procedure CheckDownloaderRetry; + procedure CheckChunkRequestLease; + procedure CheckBusyHandoff; + procedure CheckEarlyPendingExchange; + procedure CheckInvalidPacketType; + procedure CheckMatchingInvalidChunk; + end; + +var + GLog: TStringList; + GLogFile: String; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure Log(const AMessage: String); +begin + GLog.Add(AMessage); + GLog.SaveToFile(GLogFile); +end; + +procedure Pump; +begin + CheckSynchronize(0); + Sleep(1); +end; + +procedure Await(AEvent: TEvent; AMilliseconds: Cardinal; const AMessage: String); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + AMilliseconds; + repeat + if AEvent.WaitFor(0) = wrSignaled then Exit; + Pump; + until GetTickCount64 >= LUntil; + raise Exception.Create(AMessage); +end; + +function DesktopName(AHandle: THandle): String; +var LBuffer: array[0..255] of Char; LRequired: DWORD; +begin + FillChar(LBuffer, SizeOf(LBuffer), 0); + if not GetUserObjectInformation(AHandle, UOI_NAME, @LBuffer[0], SizeOf(LBuffer), LRequired) then + RaiseLastOSError; + Result := StrPas(PChar(@LBuffer[0])); +end; + +procedure RequirePrivateDesktop; +var LInput: THandle; LName: String; +begin + LInput := OpenInputDesktop(0, False, DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + LName := DesktopName(GetThreadDesktop(GetCurrentThreadId)); + Require((Pos('PascalCoin', LName) = 1) and not SameText(LName, DesktopName(LInput)), + 'Run through the private PascalCoin desktop broker'); + finally + CloseDesktop(LInput); + end; +end; + +constructor TRecoveryState.Create; +begin + inherited Create; + FirstBody := TEvent.Create(nil, True, False, ''); + ReleaseBody := TEvent.Create(nil, True, False, ''); + SecondBody := TEvent.Create(nil, True, False, ''); + WorkerReady := TEvent.Create(nil, True, False, ''); + WriteEntered := TEvent.Create(nil, True, False, ''); + ReleaseWrite := TEvent.Create(nil, True, False, ''); +end; + +destructor TRecoveryState.Destroy; +begin + ReleaseWrite.Free; + WriteEntered.Free; + WorkerReady.Free; + SecondBody.Free; + ReleaseBody.Free; + FirstBody.Free; + inherited Destroy; +end; + +function THandoffWorker.SelectConnection(const AConnections: TList<TNetConnection>): TNetConnection; +begin + // Candidate choice is deterministic; snapshot acquisition/leases stay real. + if AConnections.Count > 0 then Result := AConnections[0] + else Result := nil; +end; + +constructor TRecoveryNet.Create(AState: TRecoveryState); +begin + FState := AState; + inherited Create(nil); +end; + +function TRecoveryNet.CreateBlockChainWorker: TThreadGetNewBlockChainFromClient; +begin + InterlockedIncrement(FState.Factories); + Result := THandoffWorker.Create(Self); +end; + +procedure TRecoveryNet.RequestDownload; +begin + StartBlockChainWorker; +end; + +procedure TRecoveryNet.GetNewBlockChainFromClient(Connection: TNetConnection; const why: String); +var LCall: LongInt; +begin + // Only the expensive download body is a gate. Production worker admission, + // completion publication and its entire connection lease remain unchanged. + LCall := InterlockedIncrement(FState.Calls); + if LCall = 1 then begin + FState.FirstBody.SetEvent; + Require(FState.ReleaseBody.WaitFor(15000) = wrSignaled, 'Handoff body gate expired'); + end else FState.SecondBody.SetEvent; +end; + +constructor TLateResponseTask.Create(ACase: TRecoveryCase); +begin + FCase := ACase; + Done := TEvent.Create(nil, True, False, ''); + FirstID := ACase.FNet.NewRequestId; + SecondID := ACase.FNet.NewRequestId; + inherited Create(True); + FreeOnTerminate := False; +end; + +destructor TLateResponseTask.Destroy; +begin + WaitFor; + Done.Free; + inherited Destroy; +end; + +procedure TLateResponseTask.BCExecute; +var LSend, LReceive: TMemoryStream; LHeader: TNetHeaderData; LValue: Cardinal; +begin + LSend := TMemoryStream.Create; + LReceive := TMemoryStream.Create; + try + try + Require(FCase.FNet.ConnectionLock(Self, FCase.FOld, 1000), 'Cannot lock request owner'); + try + // A timeout leaves an old response possible while the next request waits. + FirstResult := FCase.FOld.DoSendAndWaitForResponse(CT_NetOp_GetSafeBox, + FirstID, LSend, LReceive, 200, LHeader); + SecondResult := FCase.FOld.DoSendAndWaitForResponse(CT_NetOp_GetSafeBox, + SecondID, LSend, LReceive, 3000, LHeader); + if SecondResult then begin + Require((LHeader.request_id = SecondID) and (not LHeader.is_error), + 'Old response was mistaken for the current response'); + LReceive.Position := 0; + LReceive.ReadBuffer(LValue, SizeOf(LValue)); + Require(LValue = $C0FFEE, 'Matching response payload was not preserved'); + end; + finally + FCase.FNet.ConnectionUnlock(FCase.FOld); + end; + except + on E: Exception do Error := E.ClassName + ': ' + E.Message; + end; + finally + LReceive.Free; + LSend.Free; + Done.SetEvent; + end; +end; + +constructor TGateChunkStream.Create(AState: TRecoveryState); +begin + FState := AState; + inherited Create('RECOVERY_VALIDATION_'); +end; + +function TGateChunkStream.Write(const Buffer; Count: LongInt): LongInt; +begin + // Gate a real decompression write, after matching-response parsing but before + // invalid safebox-header rejection dereferences the same peer again. + if (Count > 0) and (InterlockedCompareExchange(FEntered, 1, 0) = 0) then begin + FState.WriteEntered.SetEvent; + Require(FState.ReleaseWrite.WaitFor(15000) = wrSignaled, 'Chunk write gate expired'); + end; + Result := inherited Write(Buffer, Count); +end; + +constructor TObservedChunk.Create(AOwner: TPCDownloadSafebox; AStart, ACount: Cardinal; + AState: TRecoveryState); +begin + FState := AState; + inherited Create(AOwner, AStart, ACount); +end; + +procedure TObservedChunk.BCExecute; +begin + Require(FState.WorkerReady.WaitFor(15000) = wrSignaled, 'Chunk stream installation gate expired'); + // A failed factory still owns the downloader's chunk-list lock. Its + // unpublished worker must exit before inherited retry tries that same lock. + if Terminated then Exit; + InterlockedIncrement(FState.ChunkEntries); + try + inherited BCExecute; + finally + InterlockedIncrement(FState.ChunkExits); + end; +end; + +constructor TObservedDownload.Create(AState: TRecoveryState); +begin + FState := AState; + inherited Create; +end; + +function TObservedDownload.CreateChunkThread(ABlockStart, ACount: Cardinal): TDownloadAccess.TChunkThread; +var LOriginal: TPCTemporalFileStream; LWorker: TObservedChunk; +begin + LWorker := TObservedChunk.Create(Self, ABlockStart, ACount, FState); + try + // The real worker is suspended at its construction gate until its owned + // destination is installed; its selection and retry loop are inherited. + LOriginal := LWorker.Stream; + LWorker.Stream := TGateChunkStream.Create(FState); + LOriginal.Free; + FState.WorkerReady.SetEvent; + Result := LWorker; + except + // A factory failure has not transferred ownership to the downloader. Join + // this unpublished worker before its borrowed downloader can be destroyed. + LWorker.Terminate; + FState.WorkerReady.SetEvent; + LWorker.WaitFor; + LWorker.Free; + raise; + end; +end; + +procedure TObservedDownload.WaitForChunkThread(AThread: TThread); +begin + inherited WaitForChunkThread(AThread); + InterlockedIncrement(FState.ChunkJoins); +end; + +constructor TDownloadTask.Create(ACase: TRecoveryCase; AGateValidation: Boolean); +begin + FCase := ACase; + FGateValidation := AGateValidation; + Done := TEvent.Create(nil, True, False, ''); + inherited Create(True); + FreeOnTerminate := False; +end; + +destructor TDownloadTask.Destroy; +begin + WaitFor; + Done.Free; + inherited Destroy; +end; + +procedure TDownloadTask.BCExecute; +var LDownload: TPCDownloadSafebox; LChunks: TPCSafeboxChunks; + LOperation: TOperationBlock; +begin + LDownload := nil; + LChunks := TPCSafeboxChunks.Create; + try + try + FCase.FNode.BeginBlockChainUpdate; + try + if FGateValidation then LDownload := TObservedDownload.Create(FCase.FState) + else LDownload := TPCDownloadSafebox.Create; + try + LOperation := CT_OperationBlock_NUL; + LOperation.block := CT_BankToDiskEveryNBlocks; + // This satisfies chunk partitioning only. No synthetic archive is + // accepted; all requests are interrupted before publication. + DownloadResult := LDownload.DownloadSafebox(Self, LOperation, LChunks); + ChunksPublished := LChunks.Count; + finally + LDownload.Free; + end; + finally + FCase.FNode.EndBlockChainUpdate; + end; + except + on E: Exception do Error := E.ClassName + ': ' + E.Message; + end; + finally + LChunks.Free; + Done.SetEvent; + end; +end; + +constructor TRecoveryCase.Create(const ABaseDirectory, AName: String); +var LID: TGUID; LFixed: TNodeServerAddressArray; LSaved: TFileStream; + LAddress: TNodeServerAddress; +begin + inherited Create(nil); + Require(not TNode.NodeExists and not TNetData.NetDataExists, + 'Each recovery case must own fresh node/network singletons'); + FOldDirectory := TNode.GetPascalCoinDataFolder; + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(ABaseDirectory) + AName + GUIDToString(LID); + Require(not DirectoryExists(FDirectory), 'Refusing an existing data directory'); + ForceDirectories(FDirectory); + TNode.SetPascalCoinDataFolder(FDirectory); + FState := TRecoveryState.Create; + FLeases := TList<TNetConnection>.Create; + FWire := TMemoryStream.Create; + // Install the observer before any network owner starts and retain it until + // all owners join. Its callback publishes counters without entering UI code. + FCoreLog := TLog.Create(nil); + FCoreLog.OnInThreadNewLog := CoreLog; + FCoreLog.FileName := FDirectory + PathDelim + 'core.log'; + TCrypto.InitCrypto; + FNet := TRecoveryNet.Create(FState); + // Network activation setters ask for TNode.Node. Publish the owned node + // before invoking them, so setup cannot create a second node implicitly. + FNode := TNode.Create(nil); + FNode.Bank.StorageClass := TFileStorage; + Log('SETUP owned node and network created'); + FNet.NetConnectionsActive := False; + FNet.MinServersConnected := 0; + FNet.MaxServersConnected := 0; + FNode.BroadcastData := False; + FNode.UpdateBlockchain := False; + FNode.NodeLogFilename := FDirectory + PathDelim + 'blocks.log'; + FNode.NetServer.Port := 0; + FListener := TTCPBlockSocket.Create; + FListener.CreateSocket; + FListener.Bind('127.0.0.1', '0'); + Require(FListener.LastError = 0, 'Cannot bind an owned loopback peer'); + FListener.Listen; + Require(FListener.LastError = 0, 'Cannot listen on an owned loopback peer'); + FPort := FListener.GetLocalSinPort; + // Reserve only this owned initial endpoint before activation's immediate + // discovery pass. ConnectTo then executes the normal production mapping and + // socket path. There is no unrelated placeholder discovery completion that + // could spuriously provide the handoff test's second blockchain start. + FOld := TNetClient.Create(nil); + FOld.FreeNotification(Self); + Require(FNet.NodeServersAddresses.GetNodeServerAddress('127.0.0.1', FPort, True, LAddress), + 'Cannot reserve the owned initial connection endpoint'); + LAddress.netConnection := FOld; + FNet.NodeServersAddresses.SetNodeServerAddress(LAddress); + TNode.DecodeIpStringToNodeServerAddressArray('127.0.0.1:' + IntToStr(FPort), LFixed); + FNet.DiscoverFixedServersOnly(LFixed); + FNet.NetConnectionsActive := True; + AcceptConnection(False); + Require(not FNet.IsDiscoveringServers and (FState.Factories = 0), + 'Initial fixture setup admitted an unrelated discovery/blockchain worker'); + // Retain normal automatic backoff until the retry case deliberately ages + // this owned address. EOF cannot race an automatic reconnect ahead of that + // case's endpoint-detachment observation or the negative rejection checks. + Require(FNet.NodeServersAddresses.GetNodeServerAddress('127.0.0.1', FPort, False, LAddress), + 'Owned initial endpoint disappeared after connecting'); + LAddress.last_attempt_to_connect := Now; + FNet.NodeServersAddresses.SetNodeServerAddress(LAddress); + Log('SETUP leased outgoing loopback connection accepted'); + LSaved := TFileStream.Create(FDirectory + PathDelim + 'completed.safebox', fmCreate); + try + LSaved.WriteBuffer(CT_Sentinel[1], Length(CT_Sentinel)); + Require(FlushFileBuffers(LSaved.Handle), 'Cannot flush saved-file sentinel'); + finally + LSaved.Free; + end; +end; + +destructor TRecoveryCase.Destroy; +begin + // Release every test gate before joining. Cancellation never kills a thread + // or frees a destination while inherited retry/validation still uses it. + if Assigned(FState) then begin + FState.ReleaseBody.SetEvent; + FState.WorkerReady.SetEvent; + FState.ReleaseWrite.SetEvent; + end; + if Assigned(FDownload) then FDownload.Terminate; + if Assigned(FLate) then FLate.Terminate; + if Assigned(FPeer) then FPeer.CloseSocket; + FDownload.Free; + FLate.Free; + if Assigned(FNet) then FNet.BeginShutdown; + ReleasePins; + if Assigned(FNet) then begin + FNet.NetConnectionsActive := False; + FNet.Free; + end; + FNode.Free; + FCoreLog.Free; + FPeer.Free; + FListener.Free; + FWire.Free; + FLeases.Free; + FState.Free; + if FOldDirectory <> '' then TNode.SetPascalCoinDataFolder(FOldDirectory); + inherited Destroy; +end; + +procedure TRecoveryCase.CoreLog(AType: TLogType; ATime: TDateTime; AThread: TThreadID; + const ASender, AText: String); +begin + if (AType <> ltError) or (ASender <> 'TNetClient') or + (Pos('Disconecting ', AText) <> 1) then Exit; + InterlockedIncrement(FProtocolRejections); + if (Pos(' > Received ', AText) > 0) and (Pos('GET_SAFEBOX', AText) > 0) then + InterlockedIncrement(FPacketRejections); + if Pos('Invalid received chunk:', AText) > 0 then + InterlockedIncrement(FChunkRejections); +end; + +procedure TRecoveryCase.Notification(AComponent: TComponent; Operation: TOperation); +begin + inherited Notification(AComponent, Operation); + if Operation = opRemove then begin + if AComponent = FOld then InterlockedExchange(FOldGone, 1); + if AComponent = FReplacement then InterlockedExchange(FReplacementGone, 1); + end; +end; + +procedure TRecoveryCase.PinActiveConnections; +var LNew: TList<TNetConnection>; LConnection: TNetConnection; +begin + LNew := TList<TNetConnection>.Create; + try + FNet.AcquireDownloadConnections(LNew); + for LConnection in LNew do FLeases.Add(LConnection); + finally + LNew.Free; + end; +end; + +procedure TRecoveryCase.ReleasePins; +var I: Integer; +begin + if not Assigned(FLeases) then Exit; + for I := FLeases.Count - 1 downto 0 do + FNet.ReleaseDownloadConnection(FLeases[I]); + FLeases.Clear; +end; + +procedure TRecoveryCase.AcceptConnection(AReplacement: Boolean); +var LConnection: TNetConnection; +begin + if (not AReplacement) and Assigned(FOld) then LConnection := FOld + else begin + LConnection := TNetClient.Create(nil); + LConnection.FreeNotification(Self); + end; + if AReplacement then FReplacement := LConnection + else FOld := LConnection; + Require(LConnection.ConnectTo('127.0.0.1', FPort), 'Cannot connect the actual outgoing owner'); + Require(FListener.CanRead(1000), 'Loopback peer did not receive connection'); + FreeAndNil(FPeer); + FPeer := TTCPBlockSocket.Create; + FPeer.Socket := FListener.Accept; + Require(FListener.LastError = 0, 'Cannot accept loopback connection'); + FWire.Clear; + // The owner's existing long-lived lease is the essential regression setup: + // a dropped socket must become reconnectable before this object is destroyed. + PinActiveConnections; +end; + +procedure TRecoveryCase.AwaitDisconnected(AConnection: TNetConnection); +var LUntil: UInt64; +begin + LUntil := GetTickCount64 + CT_Deadline; + repeat + if not AConnection.Connected then Exit; + Pump; + until GetTickCount64 >= LUntil; + raise Exception.Create('Actual outgoing transport did not observe peer EOF'); +end; + +procedure TRecoveryCase.AcceptDiscoveredReplacement; +var LAddress: TNodeServerAddress; LFixed: TNodeServerAddressArray; + LUntil: UInt64; LConnections: TList<TNetConnection>; LConnection: TNetConnection; +begin + AwaitEndpointDetached(LAddress); + // Age only owned fixture metadata. Production discovery admission and its + // randomized worker start remain intact, including the mapping regression. + LAddress.last_attempt_to_connect := Now - EncodeTime(0, 4, 0, 0); + LAddress.total_failed_attemps_to_connect := 0; + FNet.NodeServersAddresses.SetNodeServerAddress(LAddress); + TNode.DecodeIpStringToNodeServerAddressArray('127.0.0.1:' + IntToStr(FPort), LFixed); + FNet.DiscoverFixedServersOnly(LFixed); + FNet.MinServersConnected := 1; + FNet.MaxServersConnected := 1; + LUntil := GetTickCount64 + 20000; + repeat + if FListener.CanRead(10) then Break; + Pump; + until GetTickCount64 >= LUntil; + Require(FListener.CanRead(0), 'Existing background discovery did not reconnect the sole dropped peer within 20 seconds'); + FreeAndNil(FPeer); + FPeer := TTCPBlockSocket.Create; + FPeer.Socket := FListener.Accept; + FWire.Clear; + LConnections := TList<TNetConnection>.Create; + try + LUntil := GetTickCount64 + CT_Deadline; + repeat + FNet.AcquireDownloadConnections(LConnections); + for LConnection in LConnections do begin + if (not Assigned(FReplacement)) and (LConnection <> FOld) and + (LConnection.Client.RemotePort = FPort) then begin + FReplacement := LConnection; + FReplacement.FreeNotification(Self); + FLeases.Add(LConnection); + end else FNet.ReleaseDownloadConnection(LConnection); + end; + LConnections.Clear; + if Assigned(FReplacement) then Break; + Pump; + until GetTickCount64 >= LUntil; + Require(Assigned(FReplacement), 'Discovery socket did not publish a pinnable replacement connection'); + finally + LConnections.Free; + end; +end; + +procedure TRecoveryCase.AwaitEndpointDetached(var AAddress: TNodeServerAddress); +var LUntil: UInt64; +begin + // Connected becomes false immediately before the disconnect callback runs. + // Wait for that callback's own address update rather than racing it with a + // fixture read/modify/write that could accidentally restore a stale pointer. + LUntil := GetTickCount64 + CT_Deadline; + repeat + Require(FNet.NodeServersAddresses.GetNodeServerAddress('127.0.0.1', FPort, False, AAddress), + 'Known endpoint disappeared'); + if AAddress.netConnection = nil then Exit; + Pump; + until GetTickCount64 >= LUntil; + raise Exception.Create('Disconnected leased owner still occupies the endpoint discovery mapping'); +end; + +procedure TRecoveryCase.DropPeer; +begin + FPeer.CloseSocket; +end; + +function TRecoveryCase.AwaitRequest(ATimeout: Cardinal; AOperation: Word; + ARequired: Boolean): Cardinal; +var LUntil: UInt64; LPacket: RawByteString; LReadAt: Int64; + LHeader: TNetHeaderData; LPayload: TMemoryStream; LMore: Boolean; +begin + Result := 0; + LPayload := TMemoryStream.Create; + try + LUntil := GetTickCount64 + ATimeout; + repeat + while TNetData.ExtractHeaderInfo(FWire, LHeader, LPayload, LMore) do begin + if (LHeader.header_type = ntp_request) and + (LHeader.operation = AOperation) then Exit(LHeader.request_id); + end; + if FPeer.CanRead(10) then begin + LPacket := FPeer.RecvPacket(10); + if Length(LPacket) > 0 then begin + LReadAt := FWire.Position; + FWire.Position := FWire.Size; + FWire.WriteBuffer(LPacket[1], Length(LPacket)); + FWire.Position := LReadAt; + end; + end; + Pump; + until GetTickCount64 >= LUntil; + if ARequired then raise Exception.Create('Inherited transport did not issue ' + + TNetData.OperationToText(AOperation) + ' before deadline'); + finally + LPayload.Free; + end; +end; + +procedure TRecoveryCase.SendFrame(AType, AOperation: Word; ARequest: Cardinal; + AError: Word; APayload: TMemoryStream); +var LWire: TMemoryStream; LMagic, LLength: Cardinal; LType, LVersion: Word; + LPacket: RawByteString; +begin + LWire := TMemoryStream.Create; + try + LMagic := CT_MagicNetIdentification; + LType := AType; + LVersion := CT_NetProtocol_Version; + LWire.WriteBuffer(LMagic, SizeOf(LMagic)); + LWire.WriteBuffer(LType, SizeOf(LType)); + LType := AOperation; + LWire.WriteBuffer(LType, SizeOf(LType)); + LWire.WriteBuffer(AError, SizeOf(AError)); + LWire.WriteBuffer(ARequest, SizeOf(ARequest)); + LWire.WriteBuffer(LVersion, SizeOf(LVersion)); + LVersion := CT_NetProtocol_Available; + LWire.WriteBuffer(LVersion, SizeOf(LVersion)); + LLength := APayload.Size; + LWire.WriteBuffer(LLength, SizeOf(LLength)); + APayload.Position := 0; + LWire.CopyFrom(APayload, APayload.Size); + SetLength(LPacket, LWire.Size); + LWire.Position := 0; + LWire.ReadBuffer(LPacket[1], Length(LPacket)); + FPeer.SendString(LPacket); + Require(FPeer.LastError = 0, 'Raw peer failed to send response'); + finally + LWire.Free; + end; +end; + +procedure TRecoveryCase.SendReply(ARequest: Cardinal; AError: Word; APayload: TMemoryStream); +begin + SendFrame(CT_MagicResponse, CT_NetOp_GetSafeBox, ARequest, AError, APayload); +end; + +procedure TRecoveryCase.CheckCancelledFiles; +var LStream: TFileStream; LBytes: AnsiString; LSearch: TSearchRec; +begin + Require(not FDownload.DownloadResult, 'Cancelled transport-only fixture published a safebox'); + Require(FDownload.ChunksPublished = 0, 'Cancelled chunks were transferred for publication'); + Require(FDownload.Error = '', FDownload.Error); + Require(not FileExists(FNode.Bank.GetStorageFolder('') + PathDelim + 'safebox_100.safebox'), + 'An interrupted chunk published a final archive'); + LStream := TFileStream.Create(FDirectory + PathDelim + 'completed.safebox', fmOpenRead); + try + SetLength(LBytes, LStream.Size); + LStream.ReadBuffer(LBytes[1], Length(LBytes)); + Require(LBytes = CT_Sentinel, 'Existing completed-file sentinel changed'); + finally + LStream.Free; + end; + if FindFirst(FDirectory + PathDelim + 'Temp' + PathDelim + '*.tmp', faAnyFile, LSearch) = 0 then begin + FindClose(LSearch); + raise Exception.Create('Downloader left a partial temporary stream after joining'); + end; + Require(FNode.GetSynchronizationStatus.ActiveTransfers = 0, + 'Cancelled downloader left a physical synchronization scope open'); +end; + +procedure TRecoveryCase.CheckLateResponse; +var LPayload: TMemoryStream; LRequest, LValue: Cardinal; +begin + FLate := TLateResponseTask.Create(Self); + FLate.Start; + Log('LATE awaiting first outgoing request'); + Require(AwaitRequest(CT_Deadline) = FLate.FirstID, 'Missing first request'); + Log('LATE first request observed; awaiting timeout and second request'); + LRequest := AwaitRequest(CT_Deadline); + Require(LRequest = FLate.SecondID, 'Timed-out request did not yield the second request'); + LPayload := TMemoryStream.Create; + try + LValue := $BAD; + LPayload.WriteBuffer(LValue, SizeOf(LValue)); + SendReply(FLate.FirstID, 0, LPayload); + LPayload.Clear; + LValue := $C0FFEE; + LPayload.WriteBuffer(LValue, SizeOf(LValue)); + SendReply(FLate.SecondID, 0, LPayload); + Log('LATE obsolete and matching responses sent'); + Await(FLate.Done, CT_Deadline, 'Matching response wait did not finish'); + Require(FLate.Error = '', FLate.Error); + Require(not FLate.FirstResult and FLate.SecondResult, + 'An obsolete GET_SAFEBOX reply interrupted the current matching reply'); + Require(FOld.Connected, 'Obsolete response disconnected a healthy peer'); + Require(InterlockedCompareExchange(FProtocolRejections, 0, 0) = 0, + 'Obsolete response entered invalid-client rejection'); + Require(not FNet.NodeServersAddresses.IsBlackListed('127.0.0.1'), + 'Obsolete response blacklisted a healthy peer'); + Log('PASS late response: timed-out GET_SAFEBOX discarded; current reply consumed, peer connected and not blacklisted'); + finally + LPayload.Free; + end; +end; + +procedure TRecoveryCase.CheckEndpointReplacement; +var LAddress: TNodeServerAddress; LCandidates: TNodeServerAddressArray; + LUntil: UInt64; +begin + DropPeer; + AwaitDisconnected(FOld); + Require(InterlockedCompareExchange(FOldGone, 0, 0) = 0, + 'The fixture must retain its disconnected old owner lease'); + AwaitEndpointDetached(LAddress); + // Preserve automatic backoff semantics, but age this owned test address so + // eligibility can be asserted without waiting three real minutes. + FNet.NodeServersAddresses.LockList; + try + LAddress.last_attempt_to_connect := Now - EncodeTime(0, 4, 0, 0); + LAddress.total_failed_attemps_to_connect := 0; + FNet.NodeServersAddresses.SetNodeServerAddress(LAddress); + SetLength(LCandidates, 1); + LCandidates[0] := LAddress; + FNet.NodeServersAddresses.GetNodeServersToConnnect(1, True, LCandidates); + Require((Length(LCandidates) = 1) and (LCandidates[0].port = FPort), + 'Actual discovery candidate filter cannot admit the sole dropped endpoint'); + finally + // This case replaces the socket explicitly to test old-destructor mapping. + // Restore backoff before admitting the independent background discovery; + // the separate downloader case exercises its automatic reconnect path. + try + LAddress.last_attempt_to_connect := Now; + FNet.NodeServersAddresses.SetNodeServerAddress(LAddress); + finally + FNet.NodeServersAddresses.UnlockList; + end; + end; + AcceptConnection(True); + // The first element pins only the old owner. Release it while keeping the + // replacement's pin: old destruction must not erase the new pointer mapping. + Require(FLeases[0] = FOld, 'Old connection lease setup changed'); + FOld.FinalizeConnection; + FNet.ReleaseDownloadConnection(FOld); + FLeases.Delete(0); + LUntil := GetTickCount64 + CT_Deadline; + repeat + if InterlockedCompareExchange(FOldGone, 0, 0) <> 0 then Break; + Pump; + until GetTickCount64 >= LUntil; + Require(InterlockedCompareExchange(FOldGone, 0, 0) = 1, + 'Old disconnected owner did not retire after releasing its final lease'); + Require(FNet.NodeServersAddresses.GetNodeServerAddress('127.0.0.1', FPort, False, LAddress) + and (LAddress.netConnection = FReplacement), + 'Old destructor erased its replacement endpoint binding'); + Require(FReplacement.Connected, 'Replacement socket did not remain connected'); + Log('PASS endpoint: leased EOF owner detached, real candidate filter admitted endpoint, old destructor preserved replacement mapping'); +end; + +procedure TRecoveryCase.CheckDownloaderRetry; +var LFirst, LSecond: Cardinal; +begin + FDownload := TDownloadTask.Create(Self, False); + FDownload.Start; + LFirst := AwaitRequest(CT_Deadline); + DropPeer; + AwaitDisconnected(FOld); + AcceptDiscoveredReplacement; + LSecond := AwaitRequest(CT_Deadline); + Require(LSecond <> LFirst, 'Inherited chunk retry reused its interrupted request ID'); + FDownload.Terminate; + DropPeer; + Await(FDownload.Done, CT_Deadline, 'Actual inherited retry did not cancel cooperatively'); + CheckCancelledFiles; + Log(Format('PASS downloader: automatic background discovery reconnected sole peer; real GET_SAFEBOX request %d retried as %d; cancelled workers joined, no archive published', + [LFirst, LSecond])); +end; + +procedure BuildInvalidHeaderChunk(AResult: TMemoryStream); +var LCompressed: TMemoryStream; LZlib: TCompressionStream; + LData: array[0..4095] of Byte; LLength: Cardinal; +begin + FillChar(LData, SizeOf(LData), 0); + LCompressed := TMemoryStream.Create; + try + LZlib := TCompressionStream.Create(clFastest, LCompressed); + try + LZlib.WriteBuffer(LData[0], SizeOf(LData)); + finally + LZlib.Free; + end; + TStreamOp.WriteAnsiString(AResult, TEncoding.ASCII.GetBytes(CT_SafeBoxChunkIdentificator)); + AResult.WriteBuffer(CT_SafeBoxBankVersion, SizeOf(CT_SafeBoxBankVersion)); + LLength := SizeOf(LData); + AResult.WriteBuffer(LLength, SizeOf(LLength)); + LLength := LCompressed.Size; + AResult.WriteBuffer(LLength, SizeOf(LLength)); + LCompressed.Position := 0; + AResult.CopyFrom(LCompressed, LCompressed.Size); + finally + LCompressed.Free; + end; +end; + +procedure TRecoveryCase.CheckChunkRequestLease; +var LPayload: TMemoryStream; LRequest: Cardinal; LAcquired: Boolean; +begin + FDownload := TDownloadTask.Create(Self, True); + FDownload.Start; + LRequest := AwaitRequest(CT_Deadline); + LPayload := TMemoryStream.Create; + try + BuildInvalidHeaderChunk(LPayload); + SendReply(LRequest, 0, LPayload); + Await(FState.WriteEntered, CT_Deadline, 'Real chunk decompression did not reach its destination'); + LAcquired := FNet.ConnectionLock(Self, FOld, 0); + if LAcquired then FNet.ConnectionUnlock(FOld); + // Keep the fixture's independent lifetime pin even in RED; this allows a + // failed ownership assertion to unwind safely rather than causing a UAF. + DropPeer; + AwaitDisconnected(FOld); + FDownload.Terminate; + FState.ReleaseWrite.SetEvent; + Await(FDownload.Done, CT_Deadline, 'In-flight chunk validation did not finish before cleanup'); + Require(not LAcquired, 'Chunk worker released its processing lock before response validation finished'); + Require((FState.ChunkEntries = 1) and (FState.ChunkExits = 1) and (FState.ChunkJoins = 1), + 'The actual single chunk worker did not complete and join exactly once'); + CheckCancelledFiles; + Log('PASS chunk lease: actual decompression retained request lock through peer EOF; cooperative validation returned before stream cleanup'); + finally + LPayload.Free; + end; +end; + +procedure TRecoveryCase.CheckBusyHandoff; +var LUntil: UInt64; +begin + FNode.UpdateBlockchain := True; + FNet.RequestDownload; + Await(FState.FirstBody, CT_Deadline, 'Initial real blockchain owner did not enter its body'); + FNet.RequestDownload; + Require((FState.Calls = 1) and (FState.Factories = 1), + 'A busy start request must not create a concurrent blockchain worker'); + FState.ReleaseBody.SetEvent; + Await(FState.SecondBody, CT_Deadline, + 'Busy replacement-start intent was lost after previous worker completed'); + // Let that body return and observe five nominal 100-ms maintenance periods. + // A pending intent left latched must not keep creating further successors. + LUntil := GetTickCount64 + 500; + repeat + Pump; + until GetTickCount64 >= LUntil; + Require((FState.Calls = 2) and (FState.Factories = 2), + 'Exactly one successor must run without another HELLO or explicit start'); + Log('PASS handoff: start while busy retained; exactly one successor admitted after normal completion without repeating HELLO'); +end; + +procedure TRecoveryCase.CheckEarlyPendingExchange; +var LHello: TMemoryStream; LKey: TECPrivateKey; LOperation: TOperationBlock; + LPort: Word; LTimestamp, LCount, LRequest: Cardinal; LWork: UInt64; + LUntil: UInt64; +begin + Require(FNode.Bank.BlocksCount = 0, 'Early pending check requires an empty local chain'); + LHello := TMemoryStream.Create; + LKey := TECPrivateKey.Create; + try + LRequest := AwaitRequest(CT_Deadline, CT_NetOp_Hello); + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LPort := FPort; + LHello.WriteBuffer(LPort, SizeOf(LPort)); + TStreamOp.WriteAnsiString(LHello, TAccountComp.AccountKey2RawString(LKey.PublicKey)); + LTimestamp := FNet.NetworkAdjustedTime.GetAdjustedTime; + LHello.WriteBuffer(LTimestamp, SizeOf(LTimestamp)); + LOperation := CT_OperationBlock_NUL; + LOperation.block := 100; + LOperation.protocol_version := CT_PROTOCOL_5; + LOperation.protocol_available := CT_BlockChain_Protocol_Available; + LOperation.account_key := LKey.PublicKey; + Require(TPCOperationsComp.SaveOperationBlockToStream(LOperation, LHello), + 'Cannot serialize peer-ahead HELLO operation header'); + LCount := 0; + LHello.WriteBuffer(LCount, SizeOf(LCount)); + TStreamOp.WriteAnsiString(LHello, TEncoding.ASCII.GetBytes('RecoveryPeer')); + LWork := 0; + LHello.WriteBuffer(LWork, SizeOf(LWork)); + TStreamOp.WriteAnsiString(LHello, TEncoding.ASCII.GetBytes('1')); + SendFrame(CT_MagicResponse, CT_NetOp_Hello, LRequest, 0, LHello); + LUntil := GetTickCount64 + CT_Deadline; + repeat + if FOld.ClientAppVersion = 'RecoveryPeer' then Break; + Pump; + until GetTickCount64 >= LUntil; + Require(FOld.ClientAppVersion = 'RecoveryPeer', 'Actual HELLO decoder did not accept owned peer metadata'); + // The real parser used to query pending operations immediately after this + // HELLO, before a newly scheduled blockchain worker hid pending state. + LRequest := AwaitRequest(700, CT_NetOp_GetPendingOperations, False); + Require(LRequest = 0, 'Empty/behind local chain requested peer pending operations before catch-up'); + Require(FOld.Connected and not FNet.NodeServersAddresses.IsBlackListed('127.0.0.1'), + 'Deferring pending exchange must preserve the healthy peer'); + Log('PASS pending: actual peer-ahead HELLO on empty local chain did not request pending operations before catch-up'); + finally + LKey.Free; + LHello.Free; + end; +end; + +procedure TRecoveryCase.CheckInvalidPacketType; +var LPayload: TMemoryStream; +begin + LPayload := TMemoryStream.Create; + try + SendFrame(CT_MagicAutoSend, CT_NetOp_GetSafeBox, FNet.NewRequestId, 0, LPayload); + AwaitDisconnected(FOld); + Require(InterlockedCompareExchange(FPacketRejections, 0, 0) = 1, + 'Malformed GET_SAFEBOX autosend did not enter its exact invalid-client rejection'); + Require(not FNet.NodeServersAddresses.IsBlackListed('127.0.0.1'), + 'Invalid-client rejection must preserve the existing loopback blacklist exemption'); + Log('PASS negative packet: malformed GET_SAFEBOX autosend logged exact invalid-client rejection and disconnected; loopback exemption preserved'); + finally + LPayload.Free; + end; +end; + +procedure TRecoveryCase.CheckMatchingInvalidChunk; +var LPayload: TMemoryStream; LRequest: Cardinal; +begin + FDownload := TDownloadTask.Create(Self, False); + FDownload.Start; + LRequest := AwaitRequest(CT_Deadline); + LPayload := TMemoryStream.Create; + try + BuildInvalidHeaderChunk(LPayload); + SendReply(LRequest, 0, LPayload); + AwaitDisconnected(FOld); + Require(InterlockedCompareExchange(FChunkRejections, 0, 0) = 1, + 'Matching invalid safebox header did not enter its exact invalid-chunk rejection'); + Require(not FNet.NodeServersAddresses.IsBlackListed('127.0.0.1'), + 'Invalid chunk rejection must preserve the existing loopback blacklist exemption'); + FDownload.Terminate; + Await(FDownload.Done, CT_Deadline, 'Invalid chunk retry did not cancel cooperatively'); + CheckCancelledFiles; + Log('PASS negative chunk: matching GET_SAFEBOX with invalid safebox header rejected; no archive published'); + finally + LPayload.Free; + end; +end; + +function RunSafeboxRecoveryChecks(const ALogFile: String): Integer; +var LCase: TRecoveryCase; I: Integer; LName, LBase: String; +begin + Result := 0; + GLogFile := ExpandFileName(ALogFile); + GLog := TStringList.Create; + try + RequirePrivateDesktop; + LBase := IncludeTrailingPathDelimiter(ExtractFilePath(GLogFile)) + 'data'; + ForceDirectories(LBase); + for I := 0 to 7 do begin + case I of + 0: LName := 'late-response'; + 1: LName := 'endpoint-replacement'; + 2: LName := 'downloader-retry'; + 3: LName := 'chunk-request-lease'; + 4: LName := 'busy-handoff'; + 5: LName := 'early-pending-exchange'; + 6: LName := 'invalid-packet-type'; + 7: LName := 'matching-invalid-chunk'; + end; + Log('BEGIN ' + LName); + LCase := nil; + try + LCase := TRecoveryCase.Create(LBase, LName); + case I of + 0: LCase.CheckLateResponse; + 1: LCase.CheckEndpointReplacement; + 2: LCase.CheckDownloaderRetry; + 3: LCase.CheckChunkRequestLease; + 4: LCase.CheckBusyHandoff; + 5: LCase.CheckEarlyPendingExchange; + 6: LCase.CheckInvalidPacketType; + 7: LCase.CheckMatchingInvalidChunk; + end; + except + on E: Exception do begin + Inc(Result); + Log('FAIL ' + LName + ': ' + E.ClassName + ': ' + E.Message); + end; + end; + LCase.Free; + Log('END ' + LName); + end; + Log(Format('RESULT cases=8 failures=%d', [Result])); + except + on E: Exception do begin + Result := 99; + Log('FATAL ' + E.ClassName + ': ' + E.Message); + end; + end; + GLog.Free; +end; + +end. diff --git a/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.dpr b/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.dpr new file mode 100644 index 000000000..95ad17595 --- /dev/null +++ b/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.dpr @@ -0,0 +1,33 @@ +program CheckpointStorageProbe; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$APPTYPE CONSOLE} + +uses SysUtils, UNode, UCheckpointStorageChecks; + +var LCheck: Integer; LID: TGUID; LDirectory: String; + +begin + ReportMemoryLeaksOnShutdown := True; + if not (ParamCount in [1,2]) then Halt(64); + try + if CreateGUID(LID)<>0 then raise Exception.Create('Cannot create owned node directory identity'); + LDirectory := IncludeTrailingPathDelimiter(ExtractFilePath(ExpandFileName(ParamStr(1))))+'node-'+GUIDToString(LID); + if DirectoryExists(LDirectory) or not ForceDirectories(LDirectory) then raise Exception.Create('Cannot create fresh owned node directory'); + TNode.SetPascalCoinDataFolder(LDirectory); + try + if ParamCount=2 then begin + if not TryStrToInt(ParamStr(2),LCheck) or (LCheck<0) or (LCheck>Ord(High(TCheckpointStorageCheck))) then Halt(64); + ExitCode := RunCheckpointStorageCheck(TCheckpointStorageCheck(LCheck),ExpandFileName(ParamStr(1))); + end else ExitCode := RunCheckpointStorageChecks(ExpandFileName(ParamStr(1))); + finally + if TNode.NodeExists then TNode.Node.Free; + end; + except + on E: Exception do begin + WriteLn(E.ClassName,': ',E.Message); + ExitCode := 1; + end; + end; +end. diff --git a/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.dproj b/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.dproj new file mode 100644 index 000000000..48b83d696 --- /dev/null +++ b/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.dproj @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{A47E7B21-E353-4AB7-B16A-6E36FC8B7D3B}</ProjectGuid> + <MainSource>CheckpointStorageProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\storage-recovery\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\storage-recovery\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\storage-recovery\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\storage-recovery\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UCheckpointStorageChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">CheckpointStorageProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.lpr b/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.lpr new file mode 100644 index 000000000..e4f620e7c --- /dev/null +++ b/src/tests/experimental/native/storage-recovery/CheckpointStorageProbe.lpr @@ -0,0 +1,35 @@ +program CheckpointStorageProbe; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} +{$APPTYPE CONSOLE} + +uses heaptrc, {$IFDEF UNIX}cthreads,{$ENDIF}SysUtils, UNode, UCheckpointStorageChecks; + +var LCheck: Integer; LID: TGUID; LDirectory: String; + +begin + if not (ParamCount in [1,2]) then Halt(64); + SetHeapTraceOutput(ParamStr(1)+'.heap.log'); + try + if CreateGUID(LID)<>0 then raise Exception.Create('Cannot create owned node directory identity'); + LDirectory := IncludeTrailingPathDelimiter(ExtractFilePath(ExpandFileName(ParamStr(1))))+'node-'+GUIDToString(LID); + if DirectoryExists(LDirectory) or not ForceDirectories(LDirectory) then raise Exception.Create('Cannot create fresh owned node directory'); + // Install the process-wide fallback before any fixture or core helper runs. + TNode.SetPascalCoinDataFolder(LDirectory); + try + if ParamCount=2 then begin + if not TryStrToInt(ParamStr(2),LCheck) or (LCheck<0) or (LCheck>Ord(High(TCheckpointStorageCheck))) then Halt(64); + ExitCode := RunCheckpointStorageCheck(TCheckpointStorageCheck(LCheck),ExpandFileName(ParamStr(1))); + end else ExitCode := RunCheckpointStorageChecks(ExpandFileName(ParamStr(1))); + finally + if TNode.NodeExists then TNode.Node.Free; + end; + except + on E: Exception do begin + WriteLn(E.ClassName,': ',E.Message); + ExitCode := 1; + end; + end; +end. diff --git a/src/tests/experimental/native/storage-recovery/README.md b/src/tests/experimental/native/storage-recovery/README.md new file mode 100644 index 000000000..a96cff006 --- /dev/null +++ b/src/tests/experimental/native/storage-recovery/README.md @@ -0,0 +1,35 @@ +# Checkpoint storage checks + +These 43 checks test saving, recovering and admitting safebox checkpoints, +using small generated safeboxes. + +`ExperimentalWalletTests.lpr` registers the checks. `CheckpointStorageProbe.lpr` +(FPC) and `CheckpointStorageProbe.dproj` (Delphi) also run them. Run all 43 with +a new output directory: + +```powershell +.\src\tests\experimental\native\storage-recovery\Run-CheckpointStorageChecks.ps1 -OutputDirectory <new-folder> +``` + +`-FpcPath` selects another Windows x64 compiler. A passing log ends with +`PASS 43`. + +For Delphi, place the OpenSSL runtime and +[`fixtures/mainnet-blocks-0-4.bin`](../../fixtures/README.md) beside the probe. +Set `TEMP` to a test folder and pass a result-log filename as the first +argument. An optional second argument selects one check by zero-based ordinal. + +No check opens a wallet, starts networking or removes files outside its fixture +directory. + +## Coverage + +- injected temporary-file creation, short-write, flush, validation and + replacement failures, which must leave the previous checkpoint and source + safebox unchanged +- a Windows handle denying atomic replacement +- recovery from malformed and truncated candidates, legacy files without block + bodies and bad main or checkpoint files +- failed live-file writes, with and without caching +- admission of checkpoints beyond local history or the restore limit +- progress and cancellation during legacy checkpoint inspection diff --git a/src/tests/experimental/native/storage-recovery/Run-CheckpointStorageChecks.ps1 b/src/tests/experimental/native/storage-recovery/Run-CheckpointStorageChecks.ps1 new file mode 100644 index 000000000..d05012a1d --- /dev/null +++ b/src/tests/experimental/native/storage-recovery/Run-CheckpointStorageChecks.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS +Tests real checkpoint I/O failures and local safebox recovery with tiny fixtures. +.DESCRIPTION +Uses a fresh output/TEMP directory and private desktop. No real wallet data, +public peers or installed wallet processes are used. +#> +# Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [Parameter(Mandatory = $true)] [string] $OutputDirectory +) + +$ErrorActionPreference = 'Stop' +$compiler = (Resolve-Path -LiteralPath $FpcPath).Path +$target = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' +if ($LASTEXITCODE -ne 0 -or $target.Count -ne 2 -or $target[0] -ne 'x86_64' -or $target[1] -ne 'win64') { + throw 'The native storage runner requires a Windows x64 Free Pascal target.' +} +$source = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../../../..')).Path +$tests = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../..')).Path +$output = [IO.Path]::GetFullPath($OutputDirectory) +if (Test-Path -LiteralPath $output) { throw "Choose a new output directory: $output" } +$units = Join-Path $output 'units' +$brokerUnits = Join-Path $output 'broker-units' +$temporary = Join-Path $output 'temporary-data' +New-Item -ItemType Directory -Path $units, $brokerUnits, $temporary -Force | Out-Null +$executable = Join-Path $output 'CheckpointStorageProbe.exe' +$arguments = @('-B', '-MDelphi', '-O2', '-gl', '-Cr', '-Co', '-Ci', + '-dBorland', '-dVer150', '-dDelphi7', '-dCompiler6_Up', '-dPUREPASCAL', + "-FU$units", "-FE$output", "-o$executable", + "-Fi$source\libraries\generics.collections\inc", "-Fu$PSScriptRoot") +foreach ($directory in @('core', 'libraries\simplebaselib4pascal', + 'libraries\hashlib4pascal', 'libraries\cryptolib4pascal', 'libraries\synapse', + 'libraries\sphere10', 'libraries\generics.collections', + 'libraries\pascalcoin', 'libraries\paszlib', 'libraries\abstractmem', 'libraries\regex')) { + $arguments += "-Fu$source\$directory" +} +$arguments += Join-Path $PSScriptRoot 'CheckpointStorageProbe.lpr' +$arguments | Set-Content -LiteralPath (Join-Path $output 'compiler-arguments.txt') +$previousTemp, $previousTmp = $env:TEMP, $env:TMP +try { + $env:TEMP, $env:TMP = $temporary, $temporary + & $compiler @arguments *> (Join-Path $output 'build.log') + if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'build.log') -Tail 60 + exit $LASTEXITCODE + } + $crypto = Join-Path $source 'libcrypto-1_1-x64.dll' + if (-not (Test-Path -LiteralPath $crypto)) { + $crypto = Join-Path (Split-Path -Parent $source) 'windows-dll\libcrypto-1_1-x64.dll' + } + Copy-Item -LiteralPath $crypto -Destination $output + Copy-Item -LiteralPath (Join-Path $tests 'fixtures/mainnet-blocks-0-4.bin') -Destination $output + $broker = Join-Path $output 'PrivateDesktopBroker.exe' + & $compiler '-MDelphi' '-O2' '-gl' "-FU$brokerUnits" "-o$broker" ` + (Join-Path $PSScriptRoot '../PrivateDesktopBroker.lpr') *> (Join-Path $output 'broker-build.log') + if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'broker-build.log') -Tail 40 + exit $LASTEXITCODE + } + $log = Join-Path $output 'tests.log' + $previousErrorAction = $ErrorActionPreference + try { + # Windows PowerShell wraps native stderr as error records. Preserve its + # exit code and full diagnostics even when the child reports a failure. + $ErrorActionPreference = 'Continue' + & $broker $executable $log *> (Join-Path $output 'broker.log') + $status = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorAction + } + if (-not (Test-Path -LiteralPath $log)) { throw 'Storage checks did not produce a result log.' } + $text = [IO.File]::ReadAllText($log) + Write-Output $text + $caseSource = [IO.File]::ReadAllText((Join-Path $PSScriptRoot 'UCheckpointStorageChecks.pas')) + $caseEnum = [regex]::Match($caseSource, '(?s)TCheckpointStorageCheck\s*=\s*\((.*?)\);').Groups[1].Value + $expectedChecks = [regex]::Matches($caseEnum, '\bcsc\w+\b').Count + if ($expectedChecks -le 0) { throw 'Cannot identify the storage regression count.' } + if ($text -notmatch ('(?m)^PASS '+$expectedChecks+'\s*$') -or $text -match '(?m)^FAIL') { $status = 1 } + $heap = "$log.heap.log" + if ($status -eq 0 -and (-not (Test-Path -LiteralPath $heap) -or + [IO.File]::ReadAllText($heap) -notmatch '(?m)^0 unfreed memory blocks')) { + Write-Error 'Storage checks did not produce a clean heap report.' -ErrorAction Continue + $status = 1 + } + if ($status -ne 0) { + Get-Content -LiteralPath (Join-Path $output 'broker.log') -Tail 30 + Get-ChildItem -LiteralPath $output -Filter 'tests.case-*.log' -File | ForEach-Object { + if ([IO.File]::ReadAllText($_.FullName) -match '(?m)^FAIL') { Get-Content -LiteralPath $_.FullName } + } + } + Write-Host "Checkpoint storage evidence: $output" + exit $status +} finally { + $env:TEMP, $env:TMP = $previousTemp, $previousTmp +} diff --git a/src/tests/experimental/native/storage-recovery/UCheckpointStorageChecks.pas b/src/tests/experimental/native/storage-recovery/UCheckpointStorageChecks.pas new file mode 100644 index 000000000..a5669db74 --- /dev/null +++ b/src/tests/experimental/native/storage-recovery/UCheckpointStorageChecks.pas @@ -0,0 +1,1460 @@ +unit UCheckpointStorageChecks; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Exercises checkpoint publication, malformed-candidate recovery and the live + file's failed-flush marker through real core storage. Every file belongs to a + newly generated test directory; no existing wallet data is opened. + Ahead checkpoints must be skipped without suppressing evidence of corruption; + replay and explicit restore limits retain their exact confirmed state. + + Public types in this unit + TCheckpointStorageCheck | Selects one independently repeatable storage check. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} +{$I ../../../../config.inc} + +interface + +type + TCheckpointStorageCheck = (cscFactoryFailure, cscPartialWrite, cscFlushFailure, + cscValidationFailure, cscPublicationFailure, cscAbsentTargetFailures, + cscSuccessfulRetry, cscMemoryPublication, cscEmptyPublication, + cscAnalyzeMissing, cscAnalyzeTruncated, cscAnalyzeMalformed, + cscHealthyMainBadCheckpoint, cscCorruptMainGoodCheckpoint, + cscAllCorruptCandidates, cscCheckpointThenLocalReplay, + cscFailedFlushException, cscFailedFlushShortWrite, cscSharingDenied, + cscUncachedWriteException, cscUncachedShortWrite, cscUncachedFailedClose, + cscUncachedPartialRetry, cscCachedFailedClose, + cscHealthyMainBadLegacyBody, cscOlderLegacySurvivesBadNewerBody, + cscUnstableHeaderRetryBeforeLaterPayload, cscLegacyRestoreValidationProgress, + cscLegacyRestoreCancellation, cscAheadLegacyReplaysLocalHistory, + cscAheadLegacyRespectsRestoreLimit, cscAheadAbstractReplaysLocalHistory, + cscAheadAbstractRespectsRestoreLimit, cscCorruptMainWithAheadLegacy, + cscCorruptMainWithAheadAbstract, cscInvalidLegacyHeaderOnly, + cscInvalidLegacyBodyOnly, cscMalformedLegacyBesideAheadLegacy, + cscMalformedLegacyBesideAheadAbstract, cscInvalidEligibleBodyBesideAhead, + cscAheadLiveAbstractOnly, cscEligibleLegacyBesideAhead, + cscAheadLegacyBodyNotInspected); + +function RunCheckpointStorageCheck(ACheck: TCheckpointStorageCheck; const ALogPath: String): Integer; +function RunCheckpointStorageChecks(const ALogPath: String): Integer; + +implementation + +uses Classes, SysUtils, TypInfo, UAccounts, UBlockChain, UNode, UFileStorage, + UPCAbstractMem, UPCDataTypes, UAbstractMem, UFileMem, UCacheMem, UCrypto, UConst, UBaseTypes, ULog; + +type + TCheckpointFailure = (cfNone, cfCreate, cfPartialWrite, cfFlush, cfValidation, cfPublish); + + TAheadRestoreCase = (arcReplay, arcHeightLimit, arcCorruptMain, arcMalformedPeer, + arcInvalidHeaderOnly, arcInvalidBodyOnly, arcInvalidEligibleBody, + arcAheadLiveOnly, arcEligibleFallback, arcAheadBodyOnly); + + TCheckpointAdmissionLog = class(TLog) + protected + procedure DoLog(ALogType: TLogType; const ASender, AText: String); override; + public + Text: String; + end; + + TShortCheckpointStream = class(TFileStream) + public + function Write(const Buffer; Count: Longint): Longint; override; + end; + + TFailureCheckpointMem = class(TPCAbstractMem) + protected + function CreateCheckpointStream(const AFileName: String): TFileStream; override; + procedure FlushCheckpointStream(AStream: TFileStream); override; + procedure PublishCheckpointFile(const ATemporaryFileName, ADestinationFileName: String); override; + public + Failure: TCheckpointFailure; + FailureHits: Integer; + end; + + TCheckpointSafeBox = class(TPCSafeBox) + public + procedure Build(const AKey: TAccountKey; AType: Word); + end; + + TFailureFileMem = class(TFileMem) + protected + function AbsoluteWrite(const AAbsolutePosition: Int64; const ABuffer; ASize: Integer): Integer; override; + public + RaiseOnWrite, ShortOnWrite, PartialBeforeException: Boolean; + HeaderFailuresRemaining: Integer; + FailureHits: Integer; + function Stable: Boolean; + end; + + TCheckpointFixture = class + private + FDirectory, FSourceName, FTargetName, FForeignTemporary, FPreviousDirectory: String; + FOwnDirectory, FOwnNodeDirectory: Boolean; + FCloseLogs: Integer; + FCloseLogText: String; + FRestoreIntegrityCount: Integer; + FCancelLegacyRestore: Boolean; + FAdmissionIntegrityCount: Integer; + FAdmissionValidationBlocks: Cardinal; + FKey: TECPrivateKey; + FSource: TFailureCheckpointMem; + procedure SeedFile(const AFileName: String; AType: Word); + procedure PrepareSource; + procedure CheckFailure(AFailure: TCheckpointFailure; AExistingTarget: Boolean); + procedure CheckAnalyze(AEmpty: Boolean); + procedure CheckRestore(AMode: Integer); + procedure CheckReplay; + procedure CheckLegacyBodyRecovery(AHealthyMain: Boolean); + procedure CheckLegacyRestoreProgress(ACancel: Boolean); + procedure ObserveRestoreProgress(Sender: TObject; const AProgress: TProgressInfo); + procedure CheckAheadRecovery(AAbstractCheckpoint: Boolean; ACase: TAheadRestoreCase); + procedure ObserveAdmissionProgress(Sender: TObject; const AProgress: TProgressInfo); + procedure CheckFailedFlush(AShortWrite: Boolean); + procedure CheckUncachedWrite(AShortWrite: Boolean); + procedure CheckUncachedClose; + procedure CheckUncachedPartialRetry; + procedure CheckCachedClose; + procedure CheckUnstableHeaderRetry; + procedure ObserveStorageLog(ASender: TObject; const AText: String); + procedure CheckNoOwnedTemporary; + procedure CheckSavedType(const AFileName: String; AType: Word); + public + constructor Create(const ALogPath: String); + destructor Destroy; override; + procedure Run(ACheck: TCheckpointStorageCheck); + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then raise Exception.Create('Checkpoint storage: '+AMessage); +end; + +procedure TCheckpointAdmissionLog.DoLog(ALogType: TLogType; const ASender, AText: String); +begin + if (ALogType=ltinfo) and (ASender='TPCBank') then Text := Text+AText+sLineBreak; +end; + +function ReadBytes(const AFileName: String): TBytes; +var LFile: TFileStream; +begin + LFile := TFileStream.Create(AFileName,fmOpenRead+fmShareDenyNone); + try + Require(LFile.Size<=32*1024*1024,'fixture unexpectedly exceeds 32 MiB'); + SetLength(Result,Integer(LFile.Size)); + if Length(Result)>0 then LFile.ReadBuffer(Result[0],Length(Result)); + finally + LFile.Free; + end; +end; + +procedure WriteBytes(const AFileName: String; const ABytes: TBytes); +var LFile: TFileStream; +begin + LFile := TFileStream.Create(AFileName,fmCreate); + try + if Length(ABytes)>0 then LFile.WriteBuffer(ABytes[0],Length(ABytes)); + finally + LFile.Free; + end; +end; + +function SameBytes(const ALeft, ARight: TBytes): Boolean; +begin + Result := Length(ALeft)=Length(ARight); + if Result and (Length(ALeft)>0) then Result := CompareMem(@ALeft[0],@ARight[0],Length(ALeft)); +end; + +procedure RemoveOwnedDirectory(const ADirectory: String); +var LSearch: TSearchRec; LName: String; +begin + // This helper receives only a fresh GUID fixture directory created below. + // Never enumerate or delete outside that owned directory. + if FindFirst(IncludeTrailingPathDelimiter(ADirectory)+'*',faAnyFile,LSearch)=0 then begin + try + repeat + if (LSearch.Name<>'.') and (LSearch.Name<>'..') then begin + LName := IncludeTrailingPathDelimiter(ADirectory)+LSearch.Name; + if (LSearch.Attr and faDirectory)<>0 then RemoveOwnedDirectory(LName) + else SysUtils.DeleteFile(LName); + end; + until FindNext(LSearch)<>0; + finally + FindClose(LSearch); + end; + end; + RemoveDir(ADirectory); +end; + +function TShortCheckpointStream.Write(const Buffer; Count: Longint): Longint; +var LShort: Longint; +begin + LShort := Count div 2; + if LShort=Count then LShort := 0; + Result := inherited Write(Buffer,LShort); +end; + +function TFailureCheckpointMem.CreateCheckpointStream(const AFileName: String): TFileStream; +begin + case Failure of + cfCreate: begin + Inc(FailureHits); + raise EWriteError.Create('Injected checkpoint temporary creation failure'); + end; + cfPartialWrite: begin + Inc(FailureHits); + Result := TShortCheckpointStream.Create(AFileName,fmCreate+fmShareExclusive); + end; + else + Result := inherited CreateCheckpointStream(AFileName); + end; +end; + +procedure TFailureCheckpointMem.FlushCheckpointStream(AStream: TFileStream); +var LWrongHeader: array[0..63] of Byte; +begin + case Failure of + cfFlush: begin + Inc(FailureHits); + raise EWriteError.Create('Injected checkpoint flush failure'); + end; + cfValidation: begin + Inc(FailureHits); + FillChar(LWrongHeader,SizeOf(LWrongHeader),0); + AStream.Position := 0; + AStream.WriteBuffer(LWrongHeader,SizeOf(LWrongHeader)); + end; + end; + inherited FlushCheckpointStream(AStream); +end; + +procedure TFailureCheckpointMem.PublishCheckpointFile(const ATemporaryFileName, ADestinationFileName: String); +begin + if Failure=cfPublish then begin + Inc(FailureHits); + raise EWriteError.Create('Injected atomic checkpoint replacement failure'); + end; + inherited PublishCheckpointFile(ATemporaryFileName,ADestinationFileName); +end; + +procedure TCheckpointSafeBox.Build(const AKey: TAccountKey; AType: Word); +var LBlock: TOperationBlock; LAccount: TAccount; +begin + LBlock := CT_OperationBlock_NUL; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.reward := TPascalCoinProtocol.GetRewardForNewLine(0); + AddNew(LBlock); + LAccount := Account(0); + UpdateAccount(0,LAccount.accountInfo,LAccount.name,AType,LAccount.balance,LAccount.n_operation,LAccount.account_data,LAccount.account_seal,aus_transaction_commit,LAccount.updated_on_block_passive_mode,LAccount.updated_on_block_active_mode,False,False); +end; + +function TFailureFileMem.AbsoluteWrite(const AAbsolutePosition: Int64; const ABuffer; ASize: Integer): Integer; +begin + if (AAbsolutePosition=0) and (ASize<=64) and (HeaderFailuresRemaining>0) then begin + Dec(HeaderFailuresRemaining); + Inc(FailureHits); + raise EWriteError.Create('Injected initial unstable-header write failure'); + end; + // Permit the small header marker write, but fail the actual dirty cache cell. + if ((AAbsolutePosition>0) or (ASize>64)) and (RaiseOnWrite or ShortOnWrite) then begin + Inc(FailureHits); + if RaiseOnWrite then begin + if PartialBeforeException then inherited AbsoluteWrite(AAbsolutePosition,ABuffer,ASize div 2); + raise EWriteError.Create('Injected live cache flush failure'); + end; + Exit(inherited AbsoluteWrite(AAbsolutePosition,ABuffer,ASize div 2)); + end; + Result := inherited AbsoluteWrite(AAbsolutePosition,ABuffer,ASize); +end; + +function TFailureFileMem.Stable: Boolean; +begin + Result := IsAbstractMemInfoStable; +end; + +constructor TCheckpointFixture.Create(const ALogPath: String); +var LID: TGUID; +begin + inherited Create; + Require(GetEnvironmentVariable('TEMP')<>'','the caller must supply an owned TEMP directory'); + Require(CreateGUID(LID)=0,'cannot allocate fixture identity'); + FDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP'))+'CheckpointStorage-'+GUIDToString(LID); + Require(not DirectoryExists(FDirectory),'refusing an existing fixture directory'); + Require(ForceDirectories(FDirectory),'cannot create owned fixture directory'); + FOwnDirectory := True; + FSourceName := IncludeTrailingPathDelimiter(FDirectory)+'source.am_safebox'; + FTargetName := IncludeTrailingPathDelimiter(FDirectory)+'checkpoint6.am_safebox'; + FForeignTemporary := FTargetName+'.unrelated.tmp'; + Require(not TNode.NodeExists,'the storage fixture requires its own node lifetime'); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + FOwnNodeDirectory := True; + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); +end; + +destructor TCheckpointFixture.Destroy; +begin + FSource.Free; + // A core helper must not create a hidden singleton. If it does, retire it + // before restoring the directory so even its saver remains inside the fixture. + if FOwnNodeDirectory and TNode.NodeExists then TNode.Node.Free; + FKey.Free; + if FOwnNodeDirectory then TNode.SetPascalCoinDataFolder(FPreviousDirectory); + if FOwnDirectory then RemoveOwnedDirectory(FDirectory); + inherited; +end; + +procedure TCheckpointFixture.SeedFile(const AFileName: String; AType: Word); +var LSafeBox: TCheckpointSafeBox; +begin + Require(not FileExists(AFileName),'refusing an existing fixture seed'); + LSafeBox := TCheckpointSafeBox.Create; + try + LSafeBox.Build(FKey.PublicKey,AType); + LSafeBox.SaveCheckpointing(AFileName); + finally + LSafeBox.Free; + end; + Require(not TNode.NodeExists,'bank/storage check unexpectedly created the global node'); +end; + +procedure TCheckpointFixture.PrepareSource; +begin + SeedFile(FSourceName,111); + FSource := TFailureCheckpointMem.Create(FSourceName,False); + FSource.FlushCache; +end; + +procedure TCheckpointFixture.CheckNoOwnedTemporary; +var LSearch: TSearchRec; +begin + if FindFirst(FTargetName+'.*.tmp',faAnyFile,LSearch)=0 then begin + try + repeat + Require(SameFileName(IncludeTrailingPathDelimiter(FDirectory)+LSearch.Name,FForeignTemporary),'temporary checkpoint leaked after publication/failure: '+LSearch.Name); + until FindNext(LSearch)<>0; + finally + FindClose(LSearch); + end; + end; +end; + +procedure TCheckpointFixture.CheckSavedType(const AFileName: String; AType: Word); +var LSaved: TPCAbstractMem; +begin + LSaved := TPCAbstractMem.Create(AFileName,True); + try + Require((LSaved.BlocksCount=1) and (LSaved.AccountsCount=5),'published checkpoint has wrong counts'); + Require(LSaved.GetAccount(0).account_type=AType,'published checkpoint contains stale account data'); + Require(LSaved.BufferBlocksHash.Length=32,'published checkpoint has wrong block-hash buffer length'); + finally + LSaved.Free; + end; +end; + +procedure TCheckpointFixture.CheckFailure(AFailure: TCheckpointFailure; AExistingTarget: Boolean); +var LSourceBefore, LTargetBefore, LForeign: TBytes; LRaised: Boolean; +begin + if FSource=nil then PrepareSource; + if AExistingTarget and not FileExists(FTargetName) then SeedFile(FTargetName,222); + if not AExistingTarget then Require(not FileExists(FTargetName),'absent-target case already has a checkpoint'); + LSourceBefore := ReadBytes(FSourceName); + if AExistingTarget then LTargetBefore := ReadBytes(FTargetName); + SetLength(LForeign,4); + LForeign[0] := 91; + LForeign[1] := 92; + LForeign[2] := 93; + LForeign[3] := 94; + WriteBytes(FForeignTemporary,LForeign); + FSource.Failure := AFailure; + FSource.FailureHits := 0; + LRaised := False; + try + try + FSource.SaveToFile(FTargetName); + except + on E: EStreamError do LRaised := True; + on E: EPCAbstractMem do LRaised := True; + on E: EOSError do LRaised := True; + end; + finally + FSource.Failure := cfNone; + end; + Require(LRaised,'injected failed checkpoint was reported as success'); + Require(FSource.FailureHits>0,'the intended failure boundary was not reached'); + Require(SameBytes(LSourceBefore,ReadBytes(FSourceName)),'failed checkpoint modified the live source'); + if AExistingTarget then begin + Require(SameBytes(LTargetBefore,ReadBytes(FTargetName)),'failed checkpoint replaced/truncated the previous good file'); + CheckSavedType(FTargetName,222); + end else Require(not FileExists(FTargetName),'failed first checkpoint left a published partial file'); + Require(SameBytes(LForeign,ReadBytes(FForeignTemporary)),'cleanup modified an unrelated temporary file'); + CheckNoOwnedTemporary; +end; + +procedure TCheckpointFixture.CheckAnalyze(AEmpty: Boolean); +var LBytes, LBefore, LHeader: TBytes; LFile: TFileStream; + LCount, LBlocks, LAccounts, LVariant: Integer; + LReader: TPCAbstractMem; LHeaderZone: TAMZone; LBlocksZone, LAccountsZone: UInt32; + + procedure RejectReadOnly(const AExpected: TBytes); + var LRepeat: Integer; + begin + for LRepeat := 1 to 3 do begin + LCount := 123456; + Require(not TPCAbstractMem.AnalyzeFile(FTargetName,LCount),'malformed/truncated candidate was accepted'); + Require(LCount=0,'rejected candidate retained a stale block count'); + end; + Require(SameBytes(AExpected,ReadBytes(FTargetName)),'read-only candidate analysis modified invalid evidence'); + end; + +begin + if AEmpty then begin + SetLength(LBytes,64); + FillChar(LBytes[0],Length(LBytes),0); + WriteBytes(FTargetName,LBytes); + end else begin + SeedFile(FTargetName,111); + LFile := TFileStream.Create(FTargetName,fmOpenReadWrite); + try + LFile.Size := LFile.Size div 2; + finally + LFile.Free; + end; + end; + LBefore := ReadBytes(FTargetName); + // Repeat malformed construction to expose cleanup leaks in heap tracing. + RejectReadOnly(LBefore); + if AEmpty then begin + SeedFile(FSourceName,111); + LBefore := ReadBytes(FSourceName); + LReader := TPCAbstractMem.Create(FSourceName,True); + try + Require(LReader.AbstractMem.ReadFirstData(LHeaderZone,LHeader),'cannot locate the valid fixture header'); + Require(Length(LHeader)>=24,'fixture has a short core header'); + Move(LHeader[16],LBlocksZone,4); + Move(LHeader[20],LAccountsZone,4); + finally + LReader.Free; + end; + Require((Int64(LBlocksZone)+12<=Length(LBefore)) and (Int64(LAccountsZone)+12<=Length(LBefore)),'fixture list headers are outside the file'); + // Preserve the AbstractMem header while corrupting only persisted counts. + // Reject these before count arithmetic or a block-hash allocation can run. + for LVariant := 0 to 3 do begin + case LVariant of + 0: begin + LBlocks := -1; + LAccounts := 5; + end; + 1: begin + LBlocks := High(Integer); + LAccounts := 5; + end; + 2: begin + LBlocks := 1; + LAccounts := 4; + end; + else + LBlocks := 100000000; + LAccounts := 500000000; + end; + LBytes := Copy(LBefore,0,Length(LBefore)); + Move(LBlocks,LBytes[LBlocksZone+8],4); + Move(LAccounts,LBytes[LAccountsZone+8],4); + WriteBytes(FTargetName,LBytes); + RejectReadOnly(LBytes); + end; + end; +end; + +procedure TCheckpointFixture.CheckRestore(AMode: Integer); +var LPreviousDirectory, LMain, LCheckpoint, LError: String; LBank: TPCBank; + LBad, LMainBefore, LCheckpointBefore: TBytes; LRaised: Boolean; LCount: Integer; +begin + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + try + Require(ForceDirectories(IncludeTrailingPathDelimiter(FDirectory)+'Data'),'cannot create owned data directory'); + LMain := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'safebox.am_safebox'; + LCheckpoint := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'checkpoint6.am_safebox'; + SetLength(LBad,64); + FillChar(LBad[0],Length(LBad),0); + if AMode=0 then begin + SeedFile(LMain,111); + WriteBytes(LCheckpoint,LBad); + end else if AMode=1 then begin + WriteBytes(LMain,LBad); + SeedFile(LCheckpoint,111); + end else begin + WriteBytes(LMain,LBad); + WriteBytes(LCheckpoint,LBad); + end; + LMainBefore := ReadBytes(LMain); + LCheckpointBefore := ReadBytes(LCheckpoint); + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + LRaised := False; + LError := ''; + try + LBank.RestoreBank(-1,'',nil); + except + on E: Exception do begin + Require(not (E is EAccessViolation),'candidate failure became a cleanup access violation'); + LRaised := True; + LError := E.ClassName+': '+E.Message; + end; + end; + if AMode=2 then begin + Require(LRaised,'all-invalid existing storage was silently replaced with an empty chain'); + Require(Pos('No readable safebox or checkpoint',LError)>0,'all-invalid storage did not report the recovery failure clearly: '+LError); + end + else begin + Require(not LRaised,'a rejected candidate prevented valid safebox recovery: '+LError); + Require((LBank.BlocksCount=1) and (LBank.SafeBox.Account(0).account_type=111),'recovery chose the wrong safebox'); + end; + finally + LBank.Free; + end; + Require(SameBytes(LCheckpointBefore,ReadBytes(LCheckpoint)),'recovery modified its source/rejected checkpoint'); + if AMode=2 then Require(SameBytes(LMainBefore,ReadBytes(LMain)),'failed recovery overwrote the original invalid live file'); + if AMode=0 then Require(SameBytes(LMainBefore,ReadBytes(LMain)),'bad checkpoint changed the healthy main safebox'); + if AMode=1 then begin + Require(TPCAbstractMem.AnalyzeFile(LMain,LCount) and (LCount=1),'recovered live file did not reopen'); + end; + finally + TNode.SetPascalCoinDataFolder(LPreviousDirectory); + end; +end; + +procedure TCheckpointFixture.CheckReplay; +var LPreviousDirectory, LCheckpoint, LMain, LFixtureName: String; LBank: TPCBank; + LBlocks: TFileStream; LOperations: TPCOperationsComp; LCount: Cardinal; + LError: String; I: Integer; LCheckpointBefore, LBad: TBytes; +begin + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + try + LFixtureName := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'mainnet-blocks-0-4.bin'; + Require(FileExists(LFixtureName),'committed public block fixture is missing beside the test executable'); + LCheckpoint := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'checkpoint0.am_safebox'; + LMain := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'safebox.am_safebox'; + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + LBlocks := TFileStream.Create(LFixtureName,fmOpenRead+fmShareDenyWrite); + LOperations := TPCOperationsComp.Create(LBank); + try + LBlocks.ReadBuffer(LCount,SizeOf(LCount)); + Require(LCount=5,'public block fixture count changed'); + for I := 0 to 4 do begin + LError := ''; + Require(LOperations.LoadBlockFromStream(LBlocks,LError),'public block decode failed: '+LError); + Require(LOperations.OperationBlock.block=Cardinal(I),'public block fixture order changed'); + Require(LBank.AddNewBlockChainBlock(LOperations,0,LError),'public block did not validate: '+LError); + if I=2 then LBank.SafeBox.SaveCheckpointing(LCheckpoint); + end; + finally + LOperations.Free; + LBlocks.Free; + end; + Require(LBank.BlocksCount=5,'fixture did not persist all five blocks'); + finally + LBank.Free; + end; + LCheckpointBefore := ReadBytes(LCheckpoint); + SetLength(LBad,64); + FillChar(LBad[0],Length(LBad),0); + WriteBytes(LMain,LBad); + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + LBank.DiskRestoreFromOperations(4,nil); + Require(LBank.BlocksCount=5,'recovery did not replay local blocks after the valid checkpoint'); + Require(LBank.LastOperationBlock.block=4,'replayed bank has the wrong tip'); + finally + LBank.Free; + end; + Require(SameBytes(LCheckpointBefore,ReadBytes(LCheckpoint)),'local replay modified the last good recovery checkpoint'); + finally + TNode.SetPascalCoinDataFolder(LPreviousDirectory); + end; +end; + +procedure TCheckpointFixture.CheckLegacyBodyRecovery(AHealthyMain: Boolean); +var LPreviousDirectory, LMain, LOlder, LNewer, LFixtureName, LError: String; + LBank: TPCBank; + LBlocks: TFileStream; + LOperations: TPCOperationsComp; + LCount: Cardinal; + I: Integer; + LExpectedHash: TRawBytes; + LMainBefore, LOlderBefore, LNewerBefore: TBytes; + LHeader: TPCSafeBoxHeader; + + procedure SaveLegacy(const AFileName: String; AWithoutBody: Boolean); + var LFull: TMemoryStream; + LFile: TFileStream; + LStreamHeader: TPCSafeBoxHeader; + LHeaderSize: Int64; + LEndOffset: Cardinal; + J: Integer; + begin + LFull := TMemoryStream.Create; + try + LBank.SafeBox.SaveSafeBoxToAStream(LFull,0,LBank.BlocksCount-1); + LFull.Position := 0; + Require(TPCSafeBox.LoadSafeBoxStreamHeader(LFull,LStreamHeader), + 'cannot read the real legacy fixture header'); + LHeaderSize := LFull.Position; + LFile := TFileStream.Create(AFileName,fmCreate); + try + LFull.Position := 0; + if not AWithoutBody then LFile.CopyFrom(LFull,LFull.Size) + else begin + // Retain a genuine newer header and its final hash, but remove the + // block bodies. Header-only inspection still accepts this candidate; + // full loading must reject it before it can outrank intact storage. + LFile.CopyFrom(LFull,LHeaderSize); + LEndOffset := (LStreamHeader.blocksCount+1)*SizeOf(Cardinal); + for J := 0 to Integer(LStreamHeader.blocksCount) do + LFile.WriteBuffer(LEndOffset,SizeOf(LEndOffset)); + TStreamOp.WriteAnsiString(LFile,LStreamHeader.safeBoxHash); + end; + finally + LFile.Free; + end; + finally + LFull.Free; + end; + end; + +begin + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + try + Require(ForceDirectories(IncludeTrailingPathDelimiter(FDirectory)+'Data'), + 'cannot create the owned legacy recovery directory'); + LMain := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'safebox.am_safebox'; + LOlder := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'recovery-old.safebox'; + LNewer := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'recovery-new.safebox'; + LFixtureName := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'mainnet-blocks-0-4.bin'; + Require(FileExists(LFixtureName),'the public five-block fixture is missing'); + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + LBlocks := TFileStream.Create(LFixtureName,fmOpenRead+fmShareDenyWrite); + LOperations := TPCOperationsComp.Create(LBank); + try + LBlocks.ReadBuffer(LCount,SizeOf(LCount)); + Require(LCount=5,'the public block fixture count changed'); + for I := 0 to 4 do begin + LError := ''; + Require(LOperations.LoadBlockFromStream(LBlocks,LError),'public legacy block decode failed: '+LError); + Require(LBank.AddNewBlockChainBlock(LOperations,0,LError),'public legacy block did not validate: '+LError); + if I=2 then begin + LExpectedHash := Copy(LBank.SafeBox.SafeBoxHash); + if AHealthyMain then LBank.SafeBox.SaveCheckpointing(LMain) + else SaveLegacy(LOlder,False); + end; + end; + SaveLegacy(LNewer,True); + Require(LBank.LoadBankFileInfo(LNewer,LHeader) and (LHeader.blocksCount=5), + 'the bad-body fixture did not pass the real header-only candidate check'); + finally + LOperations.Free; + LBlocks.Free; + end; + finally + LBank.Free; + end; + LNewerBefore := ReadBytes(LNewer); + if AHealthyMain then LMainBefore := ReadBytes(LMain) + else begin + Require(not FileExists(LMain),'the legacy-only fixture unexpectedly has a live safebox'); + LOlderBefore := ReadBytes(LOlder); + end; + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + Require(LBank.RestoreBank(-1,'',nil),'recovery did not accept the valid older storage'); + Require((LBank.BlocksCount=3) and (LBank.SafeBox.AccountsCount=15), + 'the corrupt newer legacy body displaced the valid three-block state'); + Require(TBaseType.Equals(LExpectedHash,LBank.SafeBox.SafeBoxHash), + 'legacy fallback restored a different confirmed safebox hash'); + finally + LBank.Free; + end; + Require(SameBytes(LNewerBefore,ReadBytes(LNewer)), + 'recovery modified the rejected legacy candidate'); + if AHealthyMain then + Require(SameBytes(LMainBefore,ReadBytes(LMain)), + 'a bad newer legacy body changed the healthy live safebox') + else + Require(SameBytes(LOlderBefore,ReadBytes(LOlder)), + 'legacy fallback modified its valid recovery source'); + finally + TNode.SetPascalCoinDataFolder(LPreviousDirectory); + end; +end; + +procedure TCheckpointFixture.ObserveRestoreProgress(Sender: TObject; const AProgress: TProgressInfo); +begin + if AProgress.Stage<>psCheckingSafeboxIntegrity then Exit; + Require((AProgress.CurrentPosition=3) and (AProgress.TotalCount=3) and + (AProgress.StartBlock=0) and (AProgress.EndBlock=2) and (AProgress.BlockCount=3), + 'legacy validation reported incorrect block progress'); + Inc(FRestoreIntegrityCount); + if FCancelLegacyRestore then raise EAbort.Create('Owned legacy restore cancellation'); +end; + +procedure TCheckpointFixture.CheckLegacyRestoreProgress(ACancel: Boolean); +var LMain, LLegacy, LFixtureName, LError: String; + LBank: TPCBank; + LNode: TNode; + LBlocks, LLegacyStream: TFileStream; + LOperations: TPCOperationsComp; + LCount: Cardinal; + LPublishedCount, I: Integer; + LExpectedHash, LOriginalHash: TRawBytes; + LMainBefore, LLegacyBefore: TBytes; + LAborted, LRestored: Boolean; +begin + Require(ForceDirectories(IncludeTrailingPathDelimiter(FDirectory)+'Data'), + 'cannot create the owned legacy progress directory'); + LMain := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'safebox.am_safebox'; + LLegacy := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'recovery-progress.safebox'; + LFixtureName := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'mainnet-blocks-0-4.bin'; + Require(FileExists(LFixtureName),'the public legacy progress block fixture is missing'); + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + LBlocks := TFileStream.Create(LFixtureName,fmOpenRead+fmShareDenyWrite); + try + LOperations := TPCOperationsComp.Create(LBank); + try + LBlocks.ReadBuffer(LCount,SizeOf(LCount)); + Require(LCount=5,'the public legacy progress fixture count changed'); + for I := 0 to 2 do begin + LError := ''; + Require(LOperations.LoadBlockFromStream(LBlocks,LError),'legacy progress block decode failed: '+LError); + Require(LBank.AddNewBlockChainBlock(LOperations,0,LError),'legacy progress block did not validate: '+LError); + if I=0 then LBank.SafeBox.SaveCheckpointing(LMain); + end; + LExpectedHash := Copy(LBank.SafeBox.SafeBoxHash); + LLegacyStream := TFileStream.Create(LLegacy,fmCreate); + try + LBank.SafeBox.SaveSafeBoxToAStream(LLegacyStream,0,LBank.BlocksCount-1); + finally + LLegacyStream.Free; + end; + finally + LOperations.Free; + end; + finally + LBlocks.Free; + end; + finally + LBank.Free; + end; + LMainBefore := ReadBytes(LMain); + LLegacyBefore := ReadBytes(LLegacy); + LNode := TNode.Create(nil); + try + Require(TNode.Node=LNode,'the restore fixture is not using the canonical node'); + LNode.Bank.StorageClass := TFileStorage; + // Install the intact one-block state before the newer legacy candidate is + // inspected. Cancellation must leave this same live bank and file intact. + Require(LNode.Bank.RestoreBank(0,'',nil),'cannot restore the original one-block safebox'); + Require((LNode.Bank.BlocksCount=1) and (LNode.Bank.SafeBox.AccountsCount=5), + 'the original safebox fixture has incorrect counts'); + LOriginalHash := Copy(LNode.Bank.SafeBox.SafeBoxHash); + FRestoreIntegrityCount := 0; + FCancelLegacyRestore := ACancel; + LAborted := False; + LRestored := False; + try + LRestored := LNode.Bank.RestoreBank(-1,'',ObserveRestoreProgress); + except + on E: EAbort do begin + Require(E.Message='Owned legacy restore cancellation','restore replaced the original cancellation'); + LAborted := True; + end; + end; + Require(not LNode.HasStorageFailure,'legacy progress cancellation latched a canonical storage failure'); + if ACancel then begin + Require(LAborted and not LRestored,'legacy validation cancellation did not propagate EAbort'); + Require(FRestoreIntegrityCount=1,'legacy validation continued after cancellation'); + Require((LNode.Bank.BlocksCount=1) and (LNode.Bank.SafeBox.AccountsCount=5), + 'cancelled legacy inspection displaced the live safebox'); + Require(TBaseType.Equals(LOriginalHash,LNode.Bank.SafeBox.SafeBoxHash), + 'cancelled legacy inspection changed the live safebox hash'); + end else begin + Require(LRestored and not LAborted,'valid legacy restore did not complete'); + // The tiny fixture always emits its final integrity event: one for the + // read-only candidate inspection and one for the eventual secure restore. + Require(FRestoreIntegrityCount=2,'legacy candidate inspection did not forward progress'); + Require((LNode.Bank.BlocksCount=3) and (LNode.Bank.SafeBox.AccountsCount=15), + 'legacy progress restore published incorrect counts'); + Require(TBaseType.Equals(LExpectedHash,LNode.Bank.SafeBox.SafeBoxHash), + 'legacy progress restore published a different safebox hash'); + end; + finally + LNode.Free; + end; + Require(SameBytes(LLegacyBefore,ReadBytes(LLegacy)),'legacy progress modified its original checkpoint'); + if ACancel then + Require(SameBytes(LMainBefore,ReadBytes(LMain)),'cancelled legacy restore modified the live file') + else + Require(TPCAbstractMem.AnalyzeFile(LMain,LPublishedCount) and (LPublishedCount=3), + 'completed legacy restore did not publish a readable three-block safebox'); +end; + +procedure TCheckpointFixture.ObserveAdmissionProgress(Sender: TObject; const AProgress: TProgressInfo); +begin + if AProgress.Stage<>psCheckingSafeboxIntegrity then Exit; + Require(FAdmissionValidationBlocks>0,'an ahead checkpoint reached secure body validation'); + Require((AProgress.CurrentPosition=FAdmissionValidationBlocks) and + (AProgress.TotalCount=FAdmissionValidationBlocks),'eligible checkpoint validation reported the wrong range'); + Inc(FAdmissionIntegrityCount); +end; + +procedure TCheckpointFixture.CheckAheadRecovery(AAbstractCheckpoint: Boolean; ACase: TAheadRestoreCase); +var LMain, LAhead, LOther, LFixtureName, LHistory, LError: String; + LBank: TPCBank; + LOperations: TPCOperationsComp; + LBlocks: TFileStream; + LLog: TCheckpointAdmissionLog; + LCount: Cardinal; + LExpectedHash, LTwoBlockHash, LThreeBlockHash, LFourBlockHash: TRawBytes; + LAheadBefore, LOtherBefore, LMainBefore, LHistoryBefore, LBad: TBytes; + LExpectedBlocks, LRestoreLimit, I: Integer; + LRaised, LExpectFailure, LHasAhead: Boolean; + + procedure SaveLegacy(const AFileName: String; ARemoveBody: Boolean); + var LFull: TMemoryStream; + LFile: TFileStream; + LHeader: TPCSafeBoxHeader; + LHeaderSize: Int64; + LEndOffset: Cardinal; + J: Integer; + begin + LFull := TMemoryStream.Create; + try + LBank.SafeBox.SaveSafeBoxToAStream(LFull,0,LBank.BlocksCount-1); + LFull.Position := 0; + Require(TPCSafeBox.LoadSafeBoxStreamHeader(LFull,LHeader),'cannot read the owned legacy fixture header'); + LHeaderSize := LFull.Position; + LFile := TFileStream.Create(AFileName,fmCreate); + try + LFull.Position := 0; + if not ARemoveBody then LFile.CopyFrom(LFull,LFull.Size) + else begin + // Leave a valid header/footer so only an eligible secure body pass + // can reject this candidate; an out-of-range body must never be read. + LFile.CopyFrom(LFull,LHeaderSize); + LEndOffset := (LHeader.blocksCount+1)*SizeOf(Cardinal); + for J := 0 to Integer(LHeader.blocksCount) do LFile.WriteBuffer(LEndOffset,SizeOf(LEndOffset)); + TStreamOp.WriteAnsiString(LFile,LHeader.safeBoxHash); + end; + finally + LFile.Free; + end; + finally + LFull.Free; + end; + end; + +begin + Require(ForceDirectories(IncludeTrailingPathDelimiter(FDirectory)+'Data'),'cannot create the owned admission directory'); + LMain := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'safebox.am_safebox'; + LAhead := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'checkpoint930225'; + if AAbstractCheckpoint then LAhead := LAhead+'.am_safebox' else LAhead := LAhead+'.safebox'; + LOther := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'eligible.safebox'; + LHistory := IncludeTrailingPathDelimiter(FDirectory)+'Data'+PathDelim+'BlockChainStream.blocks'; + LFixtureName := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)))+'mainnet-blocks-0-4.bin'; + LRestoreLimit := 4; + if ACase=arcHeightLimit then LRestoreLimit := 1; + LExpectedBlocks := 3; + if ACase=arcReplay then LExpectedBlocks := 4; + if ACase=arcHeightLimit then LExpectedBlocks := 2; + LExpectFailure := ACase in [arcCorruptMain,arcMalformedPeer,arcInvalidHeaderOnly, + arcInvalidBodyOnly,arcInvalidEligibleBody,arcAheadLiveOnly]; + LHasAhead := not (ACase in [arcInvalidHeaderOnly,arcInvalidBodyOnly,arcAheadLiveOnly]); + FAdmissionValidationBlocks := 0; + FAdmissionIntegrityCount := 0; + if ACase=arcEligibleFallback then FAdmissionValidationBlocks := 2; + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + LBlocks := TFileStream.Create(LFixtureName,fmOpenRead+fmShareDenyWrite); + LOperations := TPCOperationsComp.Create(LBank); + try + LBlocks.ReadBuffer(LCount,SizeOf(LCount)); + Require(LCount=5,'the public admission fixture count changed'); + for I := 0 to 4 do begin + LError := ''; + Require(LOperations.LoadBlockFromStream(LBlocks,LError),'admission fixture decode failed: '+LError); + Require(LBank.AddNewBlockChainBlock(LOperations,0,LError),'admission fixture validation failed: '+LError); + if I=1 then begin + LTwoBlockHash := Copy(LBank.SafeBox.SafeBoxHash); + if ACase in [arcEligibleFallback,arcInvalidBodyOnly,arcInvalidEligibleBody] then + SaveLegacy(LOther,ACase<>arcEligibleFallback); + end; + if I=2 then LThreeBlockHash := Copy(LBank.SafeBox.SafeBoxHash); + if I=3 then LFourBlockHash := Copy(LBank.SafeBox.SafeBoxHash); + end; + if LHasAhead then begin + if AAbstractCheckpoint then LBank.SafeBox.SaveCheckpointing(LAhead) + else SaveLegacy(LAhead,ACase=arcAheadBodyOnly); + end; + if ACase=arcAheadLiveOnly then LBank.SafeBox.SaveCheckpointing(LMain); + // Replay cases reproduce the exact one-block gap: five checkpoint blocks + // but only four local blocks. Other cases retain their independent limits. + if ACase=arcReplay then LBank.Storage.DeleteBlockChainBlocks(4) + else if ACase<>arcHeightLimit then LBank.Storage.DeleteBlockChainBlocks(3); + finally + LOperations.Free; + LBlocks.Free; + end; + finally + LBank.Free; + end; + if ACase<>arcAheadLiveOnly then begin + if FileExists(LMain) then Require(SysUtils.DeleteFile(LMain),'cannot retire the owned setup safebox'); + end; + SetLength(LBad,64); + FillChar(LBad[0],Length(LBad),0); + if ACase=arcCorruptMain then WriteBytes(LMain,LBad); + if ACase in [arcMalformedPeer,arcInvalidHeaderOnly] then WriteBytes(LOther,LBad); + if LHasAhead then LAheadBefore := ReadBytes(LAhead); + if FileExists(LOther) then LOtherBefore := ReadBytes(LOther); + if FileExists(LMain) then LMainBefore := ReadBytes(LMain); + LHistoryBefore := ReadBytes(LHistory); + case LExpectedBlocks of + 2: LExpectedHash := LTwoBlockHash; + 4: LExpectedHash := LFourBlockHash; + else + LExpectedHash := LThreeBlockHash; + end; + LLog := TCheckpointAdmissionLog.Create(nil); + try + LBank := TPCBank.Create(nil); + try + LBank.StorageClass := TFileStorage; + LRaised := False; + try + LBank.DiskRestoreFromOperations(LRestoreLimit,ObserveAdmissionProgress); + except + on E: Exception do begin + Require(not (E is EAccessViolation),'admission failure became a cleanup access violation'); + LRaised := True; + LError := E.Message; + end; + end; + if LExpectFailure then begin + Require(LRaised,'unreadable or ahead-only live storage was silently replaced'); + Require(Pos('No readable safebox',LError)>0,'admission failed outside the protected storage boundary: '+LError); + end else begin + Require(not LRaised,'an ineligible checkpoint blocked replay: '+LError); + Require((LBank.BlocksCount=Cardinal(LExpectedBlocks)) and + (LBank.AccountsCount=Cardinal(LExpectedBlocks*5)),'replay did not stop at the exact eligible local height'); + Require(LBank.LastOperationBlock.block=Cardinal(LExpectedBlocks-1),'replay has the wrong tip'); + Require(TBaseType.Equals(LExpectedHash,LBank.SafeBox.SafeBoxHash),'replay produced a different confirmed safebox hash'); + Require(Pos(LAhead,LLog.Text)>0,'the informational skip diagnostic omitted the checkpoint filename'); + if ACase=arcEligibleFallback then + Require(FAdmissionIntegrityCount=2,'the eligible legacy fallback bypassed secure inspection or restore'); + end; + finally + LBank.Free; + end; + finally + LLog.Free; + end; + if LHasAhead then Require(SameBytes(LAheadBefore,ReadBytes(LAhead)),'admission changed the skipped checkpoint'); + if Length(LOtherBefore)>0 then Require(SameBytes(LOtherBefore,ReadBytes(LOther)),'admission changed another recovery source'); + if Length(LMainBefore)>0 then Require(SameBytes(LMainBefore,ReadBytes(LMain)),'admission overwrote the original protected live safebox'); + if LExpectFailure and (Length(LMainBefore)=0) then + Require(not FileExists(LMain),'failed admission published a new live safebox'); + Require(SameBytes(LHistoryBefore,ReadBytes(LHistory)),'admission/replay changed the local block history'); +end; + +procedure TCheckpointFixture.CheckFailedFlush(AShortWrite: Boolean); +var LMem: TFailureFileMem; LReopened: TFileMem; LZone: TAMZone; + LExpected, LRead: array[0..127] of Byte; LPhysical: TBytes; LRaised, LResult: Boolean; +begin + LMem := TFailureFileMem.Create(FSourceName,False); + try + LZone := LMem.New(SizeOf(LExpected)); + FillChar(LExpected,SizeOf(LExpected),17); + LMem.Write(LZone.position,LExpected,SizeOf(LExpected)); + Require(LMem.FlushCache,'initial live-file fixture flush failed'); + FillChar(LExpected,SizeOf(LExpected),83); + LMem.Write(LZone.position,LExpected,SizeOf(LExpected)); + LMem.ShortOnWrite := AShortWrite; + LMem.RaiseOnWrite := not AShortWrite; + LRaised := False; + LResult := True; + try + LResult := LMem.FlushCache; + except + on E: EWriteError do LRaised := True; + on E: EAbstractMem do LRaised := True; + end; + Require(LMem.FailureHits>0,'the dirty cache write was not exercised'); + if AShortWrite then Require(LRaised or not LResult,'a short cache write was reported successful') + else Require(LRaised,'a cache write exception was swallowed'); + Require(not LMem.Stable,'failed cache flush was marked stable in memory'); + LPhysical := ReadBytes(FSourceName); + Require((Length(LPhysical)>6) and (LPhysical[6]=0),'failed cache flush published a stable on-disk header'); + LMem.ShortOnWrite := False; + LMem.RaiseOnWrite := False; + Require(LMem.FlushCache,'retry did not flush the retained dirty cache'); + Require(LMem.Stable,'successful retry did not restore the stable marker'); + finally + LMem.ShortOnWrite := False; + LMem.RaiseOnWrite := False; + LMem.Free; + end; + LPhysical := ReadBytes(FSourceName); + Require((Length(LPhysical)>6) and (LPhysical[6]=1),'successful retry did not persist a stable header'); + LReopened := TFileMem.Create(FSourceName,True); + try + Require(LReopened.Read(LZone.position,LRead,SizeOf(LRead))=SizeOf(LRead),'retried live file could not be read'); + Require(CompareMem(@LExpected[0],@LRead[0],SizeOf(LRead)),'retried live data was lost'); + finally + LReopened.Free; + end; +end; + +procedure TCheckpointFixture.CheckUncachedWrite(AShortWrite: Boolean); +var LMem: TFailureFileMem; LReopened: TFileMem; LZone: TAMZone; + LExpected, LRead: array[0..127] of Byte; LPhysical: TBytes; LRaised: Boolean; +begin + LMem := TFailureFileMem.Create(FSourceName,False); + try + LZone := LMem.New(SizeOf(LExpected)); + FillChar(LExpected,SizeOf(LExpected),17); + LMem.Write(LZone.position,LExpected,SizeOf(LExpected)); + Require(LMem.FlushCache,'initial uncached-write fixture flush failed'); + LMem.UseCache := False; + FillChar(LExpected,SizeOf(LExpected),83); + LMem.PartialBeforeException := True; + LMem.ShortOnWrite := AShortWrite; + LMem.RaiseOnWrite := not AShortWrite; + LRaised := False; + try + LMem.Write(LZone.position,LExpected,SizeOf(LExpected)); + except + on E: EWriteError do LRaised := True; + on E: EAbstractMem do LRaised := True; + end; + Require(LRaised and (LMem.FailureHits>0),'uncached partial write was reported successful or not exercised'); + Require(not LMem.Stable,'failed uncached write was marked stable in memory'); + LPhysical := ReadBytes(FSourceName); + Require((Length(LPhysical)>LZone.position+127) and (LPhysical[6]=0),'uncached failure retained a stable physical header'); + Require((LPhysical[LZone.position]=83) and (LPhysical[LZone.position+127]=17),'uncached injection did not write only part of the actual payload'); + LMem.ShortOnWrite := False; + LMem.RaiseOnWrite := False; + LMem.Write(LZone.position,LExpected,SizeOf(LExpected)); + Require(LMem.FlushCache,'uncached retry did not flush the corrected payload'); + Require(LMem.Stable,'successful uncached retry did not restore the stable marker'); + finally + LMem.ShortOnWrite := False; + LMem.RaiseOnWrite := False; + LMem.Free; + end; + LPhysical := ReadBytes(FSourceName); + Require((Length(LPhysical)>6) and (LPhysical[6]=1),'uncached retry did not persist the stable marker'); + LReopened := TFileMem.Create(FSourceName,True); + try + Require(LReopened.Read(LZone.position,LRead,SizeOf(LRead))=SizeOf(LRead),'uncached retry file could not be read'); + Require(CompareMem(@LExpected[0],@LRead[0],SizeOf(LRead)),'uncached retry did not persist the complete payload'); + finally + LReopened.Free; + end; +end; + +procedure TCheckpointFixture.CheckUncachedClose; +var LMem: TFailureFileMem; LReadOnly: TFileMem; LExclusive: TFileStream; + LZone: TAMZone; LBuffer: array[0..127] of Byte; + LFileName: String; LPhysical: TBytes; LRaised: Boolean; LMode: Integer; +begin + for LMode := 0 to 1 do begin + LFileName := FSourceName+'.failed-close-'+IntToStr(LMode); + LMem := TFailureFileMem.Create(LFileName,False); + try + LZone := LMem.New(SizeOf(LBuffer)); + FillChar(LBuffer,SizeOf(LBuffer),17); + LMem.Write(LZone.position,LBuffer,SizeOf(LBuffer)); + Require(LMem.FlushCache,'failed-close fixture did not persist its initial state'); + LMem.UseCache := False; + LMem.PartialBeforeException := True; + LMem.RaiseOnWrite := LMode=0; + LMem.ShortOnWrite := LMode=1; + FillChar(LBuffer,SizeOf(LBuffer),83); + LRaised := False; + try + LMem.Write(LZone.position,LBuffer,SizeOf(LBuffer)); + except + on E: EWriteError do LRaised := True; + on E: EAbstractMem do LRaised := True; + end; + Require(LRaised and (LMem.FailureHits>0),'failed-close case did not exercise the partial payload write'); + LMem.RaiseOnWrite := False; + LMem.ShortOnWrite := False; + Require(not LMem.FlushCache,'flush committed an uncached failure without a payload retry'); + LPhysical := ReadBytes(LFileName); + Require(LPhysical[6]=0,'no-retry flush published a stable header'); + // The write and explicit flush already reported failure. Destruction must + // release normally; throwing again can leak the instance on FPC. + FreeAndNil(LMem); + finally + LMem.Free; + end; + // Successful exclusive open proves that the exceptional destructor released + // its real native handle rather than leaking it to keep the test passing. + LExclusive := TFileStream.Create(LFileName,fmOpenRead+fmShareExclusive); + LExclusive.Free; + Require(SameBytes(LPhysical,ReadBytes(LFileName)),'exceptional close changed the incomplete-file evidence'); + LReadOnly := TFileMem.Create(LFileName,True); + try + Require(not LReadOnly.HeaderInitialized,'read-only reopen accepted the failed stable marker'); + finally + LReadOnly.Free; + end; + Require(SameBytes(LPhysical,ReadBytes(LFileName)),'read-only inspection changed the failed file'); + end; +end; + +procedure TCheckpointFixture.ObserveStorageLog(ASender: TObject; const AText: String); +begin + Inc(FCloseLogs); + FCloseLogText := FCloseLogText+AText+sLineBreak; +end; + +procedure TCheckpointFixture.CheckCachedClose; +var LMem: TFailureFileMem; LCache: TCacheMem; LExclusive: TFileStream; + LZone: TAMZone; LBuffer: array[0..127] of Byte; LPhysical: TBytes; +begin + LMem := TFailureFileMem.Create(FSourceName,False); + try + LZone := LMem.New(SizeOf(LBuffer)); + FillChar(LBuffer,SizeOf(LBuffer),17); + LMem.Write(LZone.position,LBuffer,SizeOf(LBuffer)); + Require(LMem.FlushCache,'cached-close fixture initial flush failed'); + FillChar(LBuffer,SizeOf(LBuffer),83); + LMem.Write(LZone.position,LBuffer,SizeOf(LBuffer)); + LCache := LMem.LockCache; + try + LCache.OnLog := ObserveStorageLog; + finally + LMem.UnlockCache; + end; + LMem.RaiseOnWrite := True; + LMem.PartialBeforeException := True; + // Here the first I/O failure occurs during automatic close. It must be + // diagnosed through the existing cache log hook, while releasing normally. + FreeAndNil(LMem); + finally + LMem.Free; + end; + Require((FCloseLogs>0) and (Pos(FSourceName,FCloseLogText)>0),'automatic close lost the storage failure filename/diagnostic'); + LExclusive := TFileStream.Create(FSourceName,fmOpenRead+fmShareExclusive); + LExclusive.Free; + LPhysical := ReadBytes(FSourceName); + Require((Length(LPhysical)>6) and (LPhysical[6]=0),'failed cached close certified incomplete data as stable'); +end; + +procedure TCheckpointFixture.CheckUncachedPartialRetry; +var LMem: TFailureFileMem; LReadOnly: TFileMem; LFirst, LSecond: TAMZone; + LBuffer: array[0..127] of Byte; LBytes, LComplete, LRead: TBytes; + I, LSpan: Integer; LRaised: Boolean; LPosition: Int64; +begin + LMem := TFailureFileMem.Create(FSourceName,False); + try + LFirst := LMem.New(SizeOf(LBuffer)); + LSecond := LMem.New(SizeOf(LBuffer)); + FillChar(LBuffer,SizeOf(LBuffer),17); + LMem.Write(LFirst.position,LBuffer,SizeOf(LBuffer)); + LMem.Write(LSecond.position,LBuffer,SizeOf(LBuffer)); + Require(LMem.FlushCache,'partial-retry fixture initial flush failed'); + LMem.UseCache := False; + FillChar(LBuffer,SizeOf(LBuffer),83); + LMem.ShortOnWrite := True; + for I := 0 to 1 do begin + if I=0 then LPosition := LFirst.position else LPosition := LSecond.position; + LRaised := False; + try + LMem.Write(LPosition,LBuffer,SizeOf(LBuffer)); + except + on E: EWriteError do LRaised := True; + on E: EAbstractMem do LRaised := True; + end; + Require(LRaised,'a disjoint short-write failure was swallowed'); + end; + Require(LMem.FailureHits>=2,'both failed payload regions were not exercised'); + LMem.ShortOnWrite := False; + LMem.Write(LFirst.position,LBuffer,SizeOf(LBuffer)); + Require(not LMem.FlushCache,'retry of the first region forgot the second failed region'); + LMem.Write(LSecond.position,LBuffer,SizeOf(LBuffer)); + Require(not LMem.FlushCache,'separate retries incorrectly cleared the conservative union'); + LBytes := ReadBytes(FSourceName); + Require(LBytes[6]=0,'partial union retry persisted a stable marker'); + LSpan := Integer(LSecond.position+SizeOf(LBuffer)-LFirst.position); + LComplete := Copy(LBytes,Integer(LFirst.position),LSpan); + // Include the unchanged allocation metadata between the two payloads. One + // complete covering retry retires the conservative failed-write union. + LMem.Write(LFirst.position,LComplete[0],Length(LComplete)); + Require(LMem.FlushCache and LMem.Stable,'complete covering retry did not restore stable state'); + finally + LMem.ShortOnWrite := False; + LMem.RaiseOnWrite := False; + LMem.Free; + end; + LBytes := ReadBytes(FSourceName); + Require(LBytes[6]=1,'covering retry did not persist the final stable marker'); + LReadOnly := TFileMem.Create(FSourceName,True); + try + SetLength(LRead,Length(LComplete)); + Require(LReadOnly.Read(LFirst.position,LRead[0],Length(LRead))=Length(LRead),'covering retry could not reopen'); + Require(SameBytes(LComplete,LRead),'covering retry lost payload/allocation bytes'); + finally + LReadOnly.Free; + end; +end; + +procedure TCheckpointFixture.CheckUnstableHeaderRetry; +var LMode: Integer; + LMem, LReopened: TFailureFileMem; + LCache: TCacheMem; + LFirst, LSecond: TAMZone; + LFileName: String; + LBytes: TBytes; + LOldValue, LFirstValue, LSecondValue, LReadValue: Cardinal; + LRaised: Boolean; +begin + for LMode := 0 to 1 do begin + LFileName := IncludeTrailingPathDelimiter(FDirectory)+'header-retry-'+IntToStr(LMode)+'.am'; + LOldValue := 11; + LFirstValue := 1234; + LSecondValue := 5678; + LMem := TFailureFileMem.Create(LFileName,False); + try + LMem.UseCache := LMode=0; + LFirst := LMem.New(128); + LSecond := LMem.New(128); + LMem.Write(LFirst.position,LOldValue,SizeOf(LOldValue)); + LMem.Write(LSecond.position,LOldValue,SizeOf(LOldValue)); + Require(LMem.FlushCache,'cannot establish the stable header retry fixture'); + LBytes := ReadBytes(LFileName); + Require((Length(LBytes)>6) and (LBytes[6]=1),'header retry fixture was not physically stable'); + LMem.HeaderFailuresRemaining := 1; + LRaised := False; + try + LMem.Write(LFirst.position,LFirstValue,SizeOf(LFirstValue)); + if LMode=0 then begin + // Exercise actual cache eviction's callback, not FileMem.FlushCache's + // separate explicit header write. The first invalidation must fail. + LCache := LMem.LockCache; + try + LCache.FlushCache; + finally + LMem.UnlockCache; + end; + end; + except + on E: EWriteError do LRaised := True; + end; + Require(LRaised and (LMem.HeaderFailuresRemaining=0) and (LMem.FailureHits=1), + 'the initial unstable-header write did not fail at the intended boundary'); + Require(LMem.Write(LSecond.position,LSecondValue,SizeOf(LSecondValue))=SizeOf(LSecondValue), + 'the different payload retry did not complete'); + if LMode=0 then begin + LCache := LMem.LockCache; + try + Require(LCache.FlushCache,'the later cached payload could not retry'); + finally + LMem.UnlockCache; + end; + end; + LBytes := ReadBytes(LFileName); + Require((Length(LBytes)>6) and (LBytes[6]=0), + 'a later payload write skipped durable invalidation after the first header failure'); + if LMode=1 then + Require(LMem.Write(LFirst.position,LFirstValue,SizeOf(LFirstValue))=SizeOf(LFirstValue), + 'the originally rejected uncached write could not be repaired'); + // Normal close now owns all complete bytes; it must finish the pending + // marker protocol before read-only reopening can accept the repaired data. + finally + LMem.Free; + end; + LReopened := TFailureFileMem.Create(LFileName,True); + try + Require(LReopened.HeaderInitialized,'repaired header retry did not close as a valid file'); + Require(LReopened.Read(LFirst.position,LReadValue,SizeOf(LReadValue))=SizeOf(LReadValue), + 'cannot reopen the first repaired payload'); + Require(LReadValue=LFirstValue,'the first repaired payload changed'); + Require(LReopened.Read(LSecond.position,LReadValue,SizeOf(LReadValue))=SizeOf(LReadValue), + 'cannot reopen the later payload'); + Require(LReadValue=LSecondValue,'the later payload changed'); + finally + LReopened.Free; + end; + end; +end; + +procedure TCheckpointFixture.Run(ACheck: TCheckpointStorageCheck); +var LFailure: TCheckpointFailure; LMem: TPCAbstractMem; LCount: Integer; + LBefore: TBytes; LReader: TFileStream; LRaised: Boolean; +begin + case ACheck of + cscFactoryFailure: CheckFailure(cfCreate,True); + cscPartialWrite: CheckFailure(cfPartialWrite,True); + cscFlushFailure: CheckFailure(cfFlush,True); + cscValidationFailure: CheckFailure(cfValidation,True); + cscPublicationFailure: CheckFailure(cfPublish,True); + cscAbsentTargetFailures: begin + for LFailure := cfCreate to cfPublish do CheckFailure(LFailure,False); + end; + cscSuccessfulRetry: begin + CheckFailure(cfPartialWrite,True); + FSource.SaveToFile(FTargetName); + CheckSavedType(FTargetName,111); + CheckNoOwnedTemporary; + end; + cscMemoryPublication: begin + SeedFile(FTargetName,111); + CheckSavedType(FTargetName,111); + CheckNoOwnedTemporary; + end; + cscEmptyPublication: begin + LMem := TPCAbstractMem.Create('',False); + try + LMem.SaveToFile(FTargetName); + finally + LMem.Free; + end; + Require(TPCAbstractMem.AnalyzeFile(FTargetName,LCount) and (LCount=0),'valid empty checkpoint was rejected'); + CheckNoOwnedTemporary; + end; + cscAnalyzeMissing: begin + LCount := 42; + Require(not TPCAbstractMem.AnalyzeFile(FTargetName,LCount),'missing checkpoint was accepted'); + Require((LCount=0) and not FileExists(FTargetName),'missing-file analysis mutated filesystem/count'); + end; + cscAnalyzeTruncated: CheckAnalyze(False); + cscAnalyzeMalformed: CheckAnalyze(True); + cscHealthyMainBadCheckpoint: CheckRestore(0); + cscCorruptMainGoodCheckpoint: CheckRestore(1); + cscAllCorruptCandidates: CheckRestore(2); + cscCheckpointThenLocalReplay: CheckReplay; + cscFailedFlushException: CheckFailedFlush(False); + cscFailedFlushShortWrite: CheckFailedFlush(True); + cscUncachedWriteException: CheckUncachedWrite(False); + cscUncachedShortWrite: CheckUncachedWrite(True); + cscUncachedFailedClose: CheckUncachedClose; + cscUncachedPartialRetry: CheckUncachedPartialRetry; + cscCachedFailedClose: CheckCachedClose; + cscHealthyMainBadLegacyBody: CheckLegacyBodyRecovery(True); + cscOlderLegacySurvivesBadNewerBody: CheckLegacyBodyRecovery(False); + cscUnstableHeaderRetryBeforeLaterPayload: CheckUnstableHeaderRetry; + cscLegacyRestoreValidationProgress: CheckLegacyRestoreProgress(False); + cscLegacyRestoreCancellation: CheckLegacyRestoreProgress(True); + cscAheadLegacyReplaysLocalHistory: CheckAheadRecovery(False,arcReplay); + cscAheadLegacyRespectsRestoreLimit: CheckAheadRecovery(False,arcHeightLimit); + cscAheadAbstractReplaysLocalHistory: CheckAheadRecovery(True,arcReplay); + cscAheadAbstractRespectsRestoreLimit: CheckAheadRecovery(True,arcHeightLimit); + cscCorruptMainWithAheadLegacy: CheckAheadRecovery(False,arcCorruptMain); + cscCorruptMainWithAheadAbstract: CheckAheadRecovery(True,arcCorruptMain); + cscInvalidLegacyHeaderOnly: CheckAheadRecovery(False,arcInvalidHeaderOnly); + cscInvalidLegacyBodyOnly: CheckAheadRecovery(False,arcInvalidBodyOnly); + cscMalformedLegacyBesideAheadLegacy: CheckAheadRecovery(False,arcMalformedPeer); + cscMalformedLegacyBesideAheadAbstract: CheckAheadRecovery(True,arcMalformedPeer); + cscInvalidEligibleBodyBesideAhead: CheckAheadRecovery(False,arcInvalidEligibleBody); + cscAheadLiveAbstractOnly: CheckAheadRecovery(True,arcAheadLiveOnly); + cscEligibleLegacyBesideAhead: CheckAheadRecovery(False,arcEligibleFallback); + cscAheadLegacyBodyNotInspected: CheckAheadRecovery(False,arcAheadBodyOnly); + cscSharingDenied: begin + {$IFDEF MSWINDOWS} + PrepareSource; + SeedFile(FTargetName,222); + LBefore := ReadBytes(FTargetName); + LReader := TFileStream.Create(FTargetName,fmOpenRead+fmShareDenyNone); + try + LRaised := False; + try + FSource.SaveToFile(FTargetName); + except + on E: EOSError do begin + Require(E.ErrorCode in [5,32],'publication failed outside the Windows sharing boundary'); + LRaised := True; + end; + end; + Require(LRaised,'an existing handle denying deletion did not prevent replacement'); + Require(SameBytes(LBefore,ReadBytes(FTargetName)),'sharing failure modified the previous checkpoint'); + finally + LReader.Free; + end; + CheckNoOwnedTemporary; + FSource.SaveToFile(FTargetName); + CheckSavedType(FTargetName,111); + {$ENDIF} + end; + end; + Require(not TNode.NodeExists,'bank/storage check unexpectedly created the global node'); +end; + +function RunCheckpointStorageCheck(ACheck: TCheckpointStorageCheck; const ALogPath: String): Integer; +var LFixture: TCheckpointFixture; LLog: TStringList; +begin + Result := 1; + LLog := TStringList.Create; + try + LLog.Add('CHECK '+GetEnumName(TypeInfo(TCheckpointStorageCheck),Ord(ACheck))); + try + LFixture := TCheckpointFixture.Create(ALogPath); + try + LFixture.Run(ACheck); + finally + LFixture.Free; + end; + LLog.Add('PASS'); + Result := 0; + except + on E: Exception do begin + LLog.Add('FAIL '+E.ClassName+': '+E.Message); + end; + end; + LLog.SaveToFile(ALogPath); + finally + LLog.Free; + end; +end; + +function RunCheckpointStorageChecks(const ALogPath: String): Integer; +var LCheck: TCheckpointStorageCheck; LLog: TStringList; LCasePath: String; +begin + Result := 0; + LLog := TStringList.Create; + try + for LCheck := Low(TCheckpointStorageCheck) to High(TCheckpointStorageCheck) do begin + LCasePath := ChangeFileExt(ALogPath,'.case-'+IntToStr(Ord(LCheck))+'.log'); + if RunCheckpointStorageCheck(LCheck,LCasePath)<>0 then Result := 1; + LLog.Add(GetEnumName(TypeInfo(TCheckpointStorageCheck),Ord(LCheck))+' '+LCasePath); + LLog.SaveToFile(ALogPath); + end; + if Result=0 then LLog.Add('PASS '+IntToStr(Ord(High(TCheckpointStorageCheck))+1)) + else LLog.Add('FAIL'); + LLog.SaveToFile(ALogPath); + finally + LLog.Free; + end; +end; + +end. diff --git a/src/tests/experimental/native/stored-history/README.md b/src/tests/experimental/native/stored-history/README.md new file mode 100644 index 000000000..4fb6ecb5f --- /dev/null +++ b/src/tests/experimental/native/stored-history/README.md @@ -0,0 +1,16 @@ +# Generated stored-history checks + +These checks test history queries against generated stored blocks and live +pending operations. The shared [`UStoredHistoryChecks`](UStoredHistoryChecks.pas) +fixture runs under [FPCUnit](../../README.md#fpcunit-fixtures) and in `StoredHistoryProbe`. Its +fifteen groups cover signed 64-bit balances, reverse traversal, finite and +Maximum history, paging, tip-only and empty queries, pending rows, operation +hashes and layered storage. +[`ULivePendingHistoryChecks`](ULivePendingHistoryChecks.pas) checks the live +pending account index. + +Build [`StoredHistoryProbe.dproj`](StoredHistoryProbe.dproj) in Delphi, or +`StoredHistoryProbe.lpr` with FPC range and overflow checks, from the current +product, core and library sources. Pass the probe one new absolute log path, and +point `TEMP` at a test folder. A passing log ends with `RESULT 0`. The fixtures +use new node directories and generated keys, and open no peers or wallet. diff --git a/src/tests/experimental/native/stored-history/StoredHistoryProbe.dpr b/src/tests/experimental/native/stored-history/StoredHistoryProbe.dpr new file mode 100644 index 000000000..59036ae88 --- /dev/null +++ b/src/tests/experimental/native/stored-history/StoredHistoryProbe.dpr @@ -0,0 +1,9 @@ +program StoredHistoryProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$APPTYPE CONSOLE} +uses UStoredHistoryChecks; +begin + ReportMemoryLeaksOnShutdown := True; + if ParamCount <> 1 then Halt(64); + ExitCode := RunStoredHistoryChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/stored-history/StoredHistoryProbe.dproj b/src/tests/experimental/native/stored-history/StoredHistoryProbe.dproj new file mode 100644 index 000000000..f58c9a99c --- /dev/null +++ b/src/tests/experimental/native/stored-history/StoredHistoryProbe.dproj @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <ProjectGuid>{4eedcc0f-fcbe-4053-a0bc-8b6c84f43d25}</ProjectGuid> + <MainSource>StoredHistoryProbe.dpr</MainSource> + <Config>Debug</Config> + <Platform>Win64</Platform> + <Base>True</Base> + <TargetedPlatforms>3</TargetedPlatforms> + <AppType>Application</AppType> + <FrameworkType>VCL</FrameworkType> + <ProjectVersion>20.3</ProjectVersion> + <DCC_Namespace>System;Winapi;Vcl;Vcl.Imaging;Data;Xml</DCC_Namespace> + <DCC_UnitAlias>Generics.Collections=System.Generics.Collections;Generics.Defaults=System.Generics.Defaults;$(DCC_UnitAlias)</DCC_UnitAlias> + <DCC_UnitSearchPath>..\..\..\..\core;..\..\..\..\gui-experimental;..\..\..\..\gui-experimental\core.utils;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations;..\..\..\..\gui-classic;..\..\..\..\libraries\sphere10;..\..\..\..\libraries\simplebaselib4pascal;..\..\..\..\libraries\hashlib4pascal;..\..\..\..\libraries\cryptolib4pascal;..\..\..\..\libraries\synapse;..\..\..\..\libraries\pascalcoin;..\..\..\..\libraries\paszlib;..\..\..\..\libraries\abstractmem;..\..\..\..\libraries\regex</DCC_UnitSearchPath> + <DCC_IncludePath>..\..\..\..\core;..\..\..\..\libraries\generics.collections\inc;..\..\..\..\libraries\sphere10</DCC_IncludePath> + <DCC_UnitSearchPath>$(DCC_UnitSearchPath);..\..\..\..\libraries\gnugettext</DCC_UnitSearchPath> + <DCC_ResourcePath>..\..\lib\stored-history\Delphi\$(Platform)\$(Config)\res;..\..\..\..;..\..\..\..\gui-experimental;..\..\..\..\gui-classic;..\..\..\..\gui-experimental\wizards\wallet;..\..\..\..\gui-experimental\wizards\operations</DCC_ResourcePath> + <DCC_DcuOutput>..\..\lib\stored-history\Delphi\$(Platform)\$(Config)\dcu</DCC_DcuOutput> + <DCC_ExeOutput>..\..\lib\stored-history\Delphi\$(Platform)\$(Config)</DCC_ExeOutput> + <BRCC_OutputDir>..\..\lib\stored-history\Delphi\$(Platform)\$(Config)\res</BRCC_OutputDir> + <DCC_DebugInformation>true</DCC_DebugInformation> + <DCC_RangeChecking>true</DCC_RangeChecking> + <DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck> + </PropertyGroup> + <ItemGroup> + <DelphiCompile Include="$(MainSource)"> + <MainSource>MainSource</MainSource> + </DelphiCompile> + <DCCReference Include="UStoredHistoryChecks.pas" /> + </ItemGroup> + <ProjectExtensions> + <Borland.Personality>Delphi.Personality.12</Borland.Personality> + <BorlandProject> + <Delphi.Personality> + <Source> + <Source Name="MainSource">StoredHistoryProbe.dpr</Source> + </Source> + </Delphi.Personality> + <Platforms> + <Platform value="Win32">True</Platform> + <Platform value="Win64">True</Platform> + </Platforms> + </BorlandProject> + </ProjectExtensions> + <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" /> +</Project> \ No newline at end of file diff --git a/src/tests/experimental/native/stored-history/StoredHistoryProbe.lpr b/src/tests/experimental/native/stored-history/StoredHistoryProbe.lpr new file mode 100644 index 000000000..fa5df25a9 --- /dev/null +++ b/src/tests/experimental/native/stored-history/StoredHistoryProbe.lpr @@ -0,0 +1,10 @@ +program StoredHistoryProbe; +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } +{$MODE Delphi} +uses heaptrc, Interfaces, Forms, UStoredHistoryChecks; +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + Application.Initialize; + ExitCode := RunStoredHistoryChecks(ParamStr(1)); +end. diff --git a/src/tests/experimental/native/stored-history/ULivePendingHistoryChecks.pas b/src/tests/experimental/native/stored-history/ULivePendingHistoryChecks.pas new file mode 100644 index 000000000..c8f72e3c9 --- /dev/null +++ b/src/tests/experimental/native/stored-history/ULivePendingHistoryChecks.pas @@ -0,0 +1,260 @@ +unit ULivePendingHistoryChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Checks the live pending-history wrapper against actual node admissions. The + generated bank and node use an owned temporary folder, without peers or wallet. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +procedure CheckLivePendingHistory; + +implementation + +uses + Classes, SysUtils, Generics.Collections, UMemory, UCrypto, UAccounts, + UBlockChain, UOpTransaction, UTxMultiOperation, UPCDataTypes, UCoreUtils, + UNode, UConst, UBaseTypes, UOrderedList; + +type + TLivePendingHistorySafeBox = class(TPCSafeBox) + public + procedure BuildFunded(const AKey: TAccountKey); + end; + + TCountingLiveHistoryTransaction = class(TOpTransaction) + public + class var AffectedCalls: Integer; + procedure AffectedAccounts(AList: TOrderedList<Cardinal>); override; + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create('Live pending history: ' + AMessage); +end; + +procedure TLivePendingHistorySafeBox.BuildFunded(const AKey: TAccountKey); +var + LBlock: TOperationBlock; + I: Integer; +begin + // Creation-block update metadata remains genuine: it is also used by the + // previous-update index when these signed operations enter the live pool. + for I := 0 to 159 do + begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I * CT_NewLineSecondsAvg; + if I in [20, 21, 22] then + LBlock.reward := 1000000; + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + DoUpgradeToProtocol6; +end; + +procedure TCountingLiveHistoryTransaction.AffectedAccounts( + AList: TOrderedList<Cardinal>); +begin + Inc(AffectedCalls); + inherited AffectedAccounts(AList); +end; + +procedure RequireSameRows(const AExpected, AActual: TArray<TOperationResume>); +var + I: Integer; +begin + Require(Length(AExpected) = Length(AActual), 'history row count differs from the generic helper'); + for I := 0 to High(AExpected) do + begin + Require((AExpected[I].AffectedAccount = AActual[I].AffectedAccount) and + (AExpected[I].NOpInsideBlock = AActual[I].NOpInsideBlock) and + (AExpected[I].Block = AActual[I].Block) and + (AExpected[I].OpType = AActual[I].OpType) and + (AExpected[I].OpSubtype = AActual[I].OpSubtype) and + (AExpected[I].SignerAccount = AActual[I].SignerAccount) and + (AExpected[I].DestAccount = AActual[I].DestAccount) and + (AExpected[I].Balance = AActual[I].Balance) and + (AExpected[I].Amount = AActual[I].Amount) and + (AExpected[I].Fee = AActual[I].Fee) and + TBaseType.Equals(AExpected[I].OperationHash, AActual[I].OperationHash) and + TBaseType.Equals(AExpected[I].OriginalPayload.payload_raw, + AActual[I].OriginalPayload.payload_raw), + 'history order, duplicate account context or value changed at row ' + IntToStr(I)); + end; +end; + +procedure CheckLivePendingHistory; +var + LDisposables: TDisposables; + LKey: TECPrivateKey; + LNode: TNode; + LPool: TPCOperationsComp; + LSubmitted: TOperationsHashTree; + LAffected: TOrderedList<Cardinal>; + LOperation: TPCOperation; + LRows: TArray<TOperationResume>; + LSender: TMultiOpSender; + LReceiver: TMultiOpReceiver; + LChanger: TMultiOpChangeInfo; + LSenders: TMultiOpSenders; + LReceivers: TMultiOpReceivers; + LChangers: TMultiOpChangesInfo; + LPreviousDirectory, LDirectory, LError: String; + LID, LDataID: TGUID; + I, J, LAdded: Integer; + + procedure Add(AOperation: TPCOperation); + begin + try + Require(LSubmitted.AddOperationToHashTree(AOperation), 'could not stage a generated signed operation'); + finally + AOperation.Free; + end; + end; + + procedure AddTransaction(ASender, ANonce, ATarget: Cardinal; AAmount: UInt64); + begin + Add(TCountingLiveHistoryTransaction.CreateTransaction(CT_PROTOCOL_6, + ASender, ANonce, ATarget, LKey, AAmount, 10, CT_TOperationPayload_NUL)); + end; + + procedure CompareQuery(const AAccounts: array of Cardinal; + ASkip: Integer = 0; ATake: Integer = MaxInt); + var + LExpected, LActual: TArray<TOperationResume>; + begin + // This independent existing helper deliberately has no touched-index gate. + // It is the oracle for every row, including duplicate requested accounts. + LPool := LNode.LockMempoolRead; + try + LExpected := TCoreTool.GetPendingOperationsAffectingAccounts( + LPool.OperationsHashTree, LPool.SafeBoxTransaction, + LPool.OperationBlock.block, AAccounts, ASkip, ATake); + finally + LNode.UnlockMempoolRead; + end; + Require(Length(LExpected) > 0, 'touched-account oracle unexpectedly returned no rows'); + TCountingLiveHistoryTransaction.AffectedCalls := 0; + LActual := LNode.GetPendingOperationsAffectingAccounts(AAccounts, ASkip, ATake); + RequireSameRows(LExpected, LActual); + Require(TCountingLiveHistoryTransaction.AffectedCalls = 4, + 'touched query did not retain exactly one pass over admitted transactions'); + end; + +begin + Require(not TNode.NodeExists, 'the fixture requires an isolated node lifetime'); + Require(GetEnvironmentVariable('TEMP') <> '', 'TEMP must identify the owned fixture directory'); + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'LivePendingHistory-' + GUIDToString(LID); + Require(ForceDirectories(LDirectory), 'could not create the owned fixture directory'); + TCrypto.InitCrypto; + LKey := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LSubmitted := LDisposables.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LAffected := LDisposables.AddObject(TOrderedList<Cardinal>.Create(False, + TComparison_Cardinal)) as TOrderedList<Cardinal>; + LNode := nil; + TNode.SetPascalCoinDataFolder(LDirectory); + try + LNode := TNode.Create(nil); + LNode.BroadcastData := False; + Require(not LNode.NetServer.Active, 'the fixture must not listen on the network'); + TLivePendingHistorySafeBox(LNode.Bank.SafeBox).BuildFunded(LKey.PublicKey); + LPool := LNode.LockMempoolWrite; + try + // The private generated bank was built without publishing a network block. + // Reconcile its empty pool through the same normal core replay operation. + LPool.SanitizeOperations; + Require(LPool.OperationBlock.block = 160, 'live pool was not advanced to the generated bank'); + Require(LPool.OperationBlock.protocol_version = CT_PROTOCOL_6, 'live pool protocol was not reconciled'); + finally + LNode.UnlockMempoolWrite; + end; + + AddTransaction(100, 1, 101, 100); + AddTransaction(105, 1, 106, 200); + AddTransaction(100, 2, 102, 300); + AddTransaction(110, 1, 101, 400); + Add(TOpChangeAccountInfo.CreateChangeAccountInfo(CT_PROTOCOL_6, + 100, 3, 115, LKey, False, CT_TECDSA_Public_Nul, False, nil, + True, 99, False, nil, 10, CT_TOperationPayload_NUL)); + LDataID := Default(TGUID); + Add(TOpData.CreateOpData(CT_PROTOCOL_6, 105, 110, 116, LKey, + 2, 1, 0, LDataID, 3, 2, CT_TOperationPayload_NUL)); + LSender := CT_TMultiOpSender_NUL; + LSender.Account := 110; + LSender.N_Operation := 2; + LSender.Amount := 10; + LReceiver := CT_TMultiOpReceiver_NUL; + LReceiver.Account := 117; + LReceiver.Amount := 9; + LChanger := CT_TMultiOpChangeInfo_NUL; + LChanger.Account := 118; + LChanger.N_Operation := 1; + LChanger.Changes_type := [account_type]; + LChanger.New_Type := 77; + LSenders := TMultiOpSenders.Create(LSender); + LReceivers := TMultiOpReceivers.Create(LReceiver); + LChangers := TMultiOpChangesInfo.Create(LChanger); + Add(TOpMultiOperation.CreateMultiOperation(CT_PROTOCOL_6, + LSenders, LReceivers, LChangers, [LKey], [LKey])); + + LError := ''; + LAdded := LNode.AddOperations(nil, LSubmitted, nil, LError); + Require(LAdded = 7, 'actual node did not admit all generated operations: ' + LError); + LPool := LNode.LockMempoolRead; + try + Require(LPool.Count = 7, 'the live pool lost an admitted operation'); + for I := 0 to LPool.Count - 1 do + begin + LOperation := LPool.Operation[I]; + if I < 4 then + Require(LOperation is TCountingLiveHistoryTransaction, + 'actual node admission erased the counting transaction subclass'); + LAffected.Clear; + LOperation.AffectedAccounts(LAffected); + for J := 0 to LAffected.Count - 1 do + Require(LPool.PreviousUpdatedBlocks.IndexOfAccount(LAffected[J]) >= 0, + 'the live touched index omitted an admitted operation account'); + end; + Require(LPool.PreviousUpdatedBlocks.IndexOfAccount(119) < 0, + 'the untouched fixture account was modified'); + finally + LNode.UnlockMempoolRead; + end; + + TCountingLiveHistoryTransaction.AffectedCalls := 0; + LRows := LNode.GetPendingOperationsAffectingAccounts([119, 120, 119], 0, MaxInt); + Require(Length(LRows) = 0, 'untouched accounts returned pending rows'); + Require(TCountingLiveHistoryTransaction.AffectedCalls = 0, + 'untouched account selection scanned the entire live pending pool'); + LRows := LNode.GetPendingOperationsAffectingAccounts([], 0, MaxInt); + Require(Length(LRows) = 0, 'empty account selection returned rows'); + LRows := LNode.GetPendingOperationsAffectingAccounts([100], 0, 0); + Require(Length(LRows) = 0, 'zero-take query returned rows'); + Require(TCountingLiveHistoryTransaction.AffectedCalls = 0, + 'empty or zero-take query scanned the live pending pool'); + CompareQuery([101]); + CompareQuery([100]); + CompareQuery([119, 101, 119, 100, 101]); + CompareQuery([119, 101, 119, 100, 101], 1, 3); + CompareQuery([115, 116, 117, 118]); + CompareQuery([105, 106, 110]); + finally + // Join the node's owned workers before restoring the process data directory; + // no worker may accidentally save into the user's normal wallet folder. + LNode.Free; + TNode.SetPascalCoinDataFolder(LPreviousDirectory); + end; +end; + +end. diff --git a/src/tests/experimental/native/stored-history/UStoredHistoryChecks.pas b/src/tests/experimental/native/stored-history/UStoredHistoryChecks.pas new file mode 100644 index 000000000..366071f36 --- /dev/null +++ b/src/tests/experimental/native/stored-history/UStoredHistoryChecks.pas @@ -0,0 +1,733 @@ +unit UStoredHistoryChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Shared actual generated stored-history fixture for FPCUnit and native Delphi. + Core validates and commits the signed operations; this is not a PoW archive. } + +{$IFDEF FPC}{$MODE Delphi}{$ENDIF} + +interface + +type + TStoredHistoryCheck = (shcWideBalance, shcReverseBoundary, shcAllHistory, + shcActualPage, shcFiniteDepth, shcTipOnly, shcOldAccount, + shcFinitePage, shcZeroTake, shcFiniteWithPending, shcLivePendingFastPath, + shcActualBlockHashes, shcLayeredBeforePrimary, shcLayeredPrimaryGap, + shcLayeredMissingBlock); + +procedure CheckStoredHistory(ACheck: TStoredHistoryCheck); +function RunStoredHistoryChecks(const ALogFile: String): Integer; + +implementation + +uses Classes, SysUtils, Generics.Collections, UCrypto, UAccounts, UBlockChain, + UOpTransaction, UPCDataTypes, UCoreUtils, UNode, UConst, UBaseTypes, + UDataSources, UCommon.Data, UFileStorage, ULivePendingHistoryChecks, + UAbstractMemBlockchainStorage; + +type + TStoredHistorySafeBox = class(TPCSafeBox) + public + procedure BuildFunded(const AKey: TAccountKey; AInitialBalance: Int64); + end; + + TStoredHistoryStorage = class(TFileStorage) + protected + function GetFirstBlockNumber: Int64; override; + function GetLastBlockNumber: Int64; override; + function DoLoadBlockChain(Operations: TPCOperationsComp; + Block: Cardinal): Boolean; override; + function DoBlockExists(Block: Cardinal): Boolean; override; + public + StoredBlock: TPCOperationsComp; + LinkedBlocks: TObjectList<TPCOperationsComp>; + LoadedBlocks: TList<Cardinal>; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + end; + + TStoredHistoryPageSource = class(TAccountsOperationsDataSource) + public + function ReadPage(out ATable: TDataTable): TPageFetchResult; + end; + + TStoredHistoryFixture = class + private + FKey: TECPrivateKey; + FNode: TNode; + FSnapshot: TPCSafeBoxTransaction; + FPrevious: TAccountPreviousBlockInfo; + FStoredBlock: TPCOperationsComp; + FLinkedBlocks: TObjectList<TPCOperationsComp>; + FPreviousDirectory: String; + procedure BuildStoredHistory(AInitialBalance: Int64; AFirstAmount, + ASecondAmount: UInt64); + procedure BuildLinkedHistory; + procedure PrepareLayeredHistory(AIncludeFirstInPrimary, AIncludeMiddleInLegacy: Boolean); + function History(ABlockDepth: Cardinal; ASkip: Integer = 0; + ATake: Integer = MaxInt): TArray<TOperationResume>; + procedure RequireLoaded(const AExpected: array of Cardinal); + public + constructor Create; + destructor Destroy; override; + procedure StoredBalanceAboveCardinalRetainsExactValue; + procedure ReverseSpendCrossesCardinalBoundary; + procedure AllHistoryDepthIncludesTheStoredBlock; + procedure ActualHistoryPagePublishesSigned64BitBalances; + procedure FiniteDepthStopsAtTheInclusiveBoundary; + procedure ZeroDepthReadsTheTipOnly; + procedure OldAccountAvoidsStoredReads; + procedure FinitePagesStopOnceTheRequestedRowsAreFound; + procedure ZeroTakeAvoidsStoredReads; + procedure FiniteHistoryRetainsPendingOperations; + procedure StoredOperationHashesUseActualBlockHeight; + procedure MaximumReadsLegacyHistoryBeforeThePrimaryRange; + procedure MaximumReadsLegacyHistoryInsideAPrimaryGap; + procedure MissingLegacyBlockDoesNotPublishAnotherBlocksOperations; + end; + +procedure RequireTrue(const AMessage: String; ACondition: Boolean); overload; +begin + if not ACondition then raise Exception.Create(AMessage); +end; + +procedure RequireTrue(ACondition: Boolean); overload; +begin + RequireTrue('Actual stored-history fixture assertion failed', ACondition); +end; + +procedure RequireFalse(const AMessage: String; ACondition: Boolean); +begin + RequireTrue(AMessage, not ACondition); +end; + +procedure RequireEquals(const AMessage: String; AExpected, AActual: Int64); overload; +begin + if AExpected <> AActual then raise Exception.Create(AMessage + + ': expected ' + IntToStr(AExpected) + ', actual ' + IntToStr(AActual)); +end; + +procedure RequireEquals(AExpected, AActual: Int64); overload; +begin + RequireEquals('Actual stored-history value differs', AExpected, AActual); +end; +procedure TStoredHistorySafeBox.BuildFunded(const AKey: TAccountKey; + AInitialBalance: Int64); +var LBlock: TOperationBlock; I: Integer; +begin + // Generated rewards fund account 100 without manually changing the safebox's + // aggregate fee accounting; the later signed spends can use real Commit. + for I := 0 to 159 do begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I * CT_NewLineSecondsAvg; + if I = 20 then LBlock.reward := AInitialBalance; + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + DoUpgradeToProtocol6; +end; + +constructor TStoredHistoryStorage.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + LoadedBlocks := TList<Cardinal>.Create; +end; + +destructor TStoredHistoryStorage.Destroy; +begin + LoadedBlocks.Free; + inherited Destroy; +end; + +function TStoredHistoryStorage.GetFirstBlockNumber: Int64; +begin + Result := 160; +end; + +function TStoredHistoryStorage.GetLastBlockNumber: Int64; +begin + if Assigned(LinkedBlocks) and (LinkedBlocks.Count > 0) then + Result := LinkedBlocks[LinkedBlocks.Count - 1].OperationBlock.block + else + Result := 160; +end; + +function TStoredHistoryStorage.DoLoadBlockChain(Operations: TPCOperationsComp; + Block: Cardinal): Boolean; +var + I: Integer; +begin + LoadedBlocks.Add(Block); + if Assigned(LinkedBlocks) then begin + for I := 0 to LinkedBlocks.Count - 1 do + if LinkedBlocks[I].OperationBlock.block = Block then begin + Operations.CopyFrom(LinkedBlocks[I]); + Exit(True); + end; + Exit(False); + end; + Result := Assigned(StoredBlock) and (Block = StoredBlock.OperationBlock.block); + if Result then Operations.CopyFrom(StoredBlock); +end; + +function TStoredHistoryStorage.DoBlockExists(Block: Cardinal): Boolean; +var I: Integer; +begin + // Match the fixture's stored blocks without recording an actual block load. + if Assigned(LinkedBlocks) then begin + for I := 0 to LinkedBlocks.Count - 1 do + if LinkedBlocks[I].OperationBlock.block = Block then Exit(True); + Exit(False); + end; + Result := Assigned(StoredBlock) and (StoredBlock.OperationBlock.block = Block); +end; + +function TStoredHistoryPageSource.ReadPage(out ATable: TDataTable): TPageFetchResult; +begin + Result := FetchPage(TPageFetchParams.Create(0, 25, nil, foAnd), ATable); +end; + +constructor TStoredHistoryFixture.Create; +var LID: TGUID; LDirectory: String; +begin + inherited Create; + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + RequireFalse('Stored history owns a fresh node without peers', TNode.NodeExists); + RequireTrue('Supply an owned TEMP directory', GetEnvironmentVariable('TEMP') <> ''); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetEnvironmentVariable('TEMP')) + + 'WalletStoredHistory-' + GUIDToString(LID); + ForceDirectories(LDirectory); + TNode.SetPascalCoinDataFolder(LDirectory); + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FNode := TNode.Create(nil); + FNode.BroadcastData := False; + FNode.Bank.StorageClass := TStoredHistoryStorage; + FPrevious := TAccountPreviousBlockInfo.Create; + FStoredBlock := TPCOperationsComp.Create(nil); + FLinkedBlocks := TObjectList<TPCOperationsComp>.Create(True); + TStoredHistoryStorage(FNode.Bank.Storage).StoredBlock := FStoredBlock; +end; + +destructor TStoredHistoryFixture.Destroy; +begin + // The node owns the storage adapter; its borrowed block remains alive until + // the node and its saver have stopped. No global wallet or network is opened. + FSnapshot.Free; + FNode.Free; + FLinkedBlocks.Free; + FStoredBlock.Free; + FPrevious.Free; + FKey.Free; + TNode.SetPascalCoinDataFolder(FPreviousDirectory); + inherited; +end; + +procedure TStoredHistoryFixture.BuildStoredHistory(AInitialBalance: Int64; + AFirstAmount, ASecondAmount: UInt64); +var + LBlock: TOperationBlock; + LOperation: TOpTransaction; + LError: String; + I: Integer; + LAmounts: array[0..1] of UInt64; +begin + TStoredHistorySafeBox(FNode.Bank.SafeBox).BuildFunded(FKey.PublicKey, + AInitialBalance); + FSnapshot := TPCSafeBoxTransaction.Create(FNode.Bank.SafeBox); + LAmounts[0] := AFirstAmount; + LAmounts[1] := ASecondAmount; + for I := 0 to 1 do begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_6, 100, I+1, + 101, FKey, LAmounts[I], 10, CT_TOperationPayload_NUL); + try + LError := ''; + RequireTrue('Stored fixture operations pass actual core validation: ' + LError, + LOperation.DoOperation(FPrevious, FSnapshot, LError)); + RequireTrue(FStoredBlock.OperationsHashTree.AddOperationToHashTree(LOperation)); + finally LOperation.Free; end; + end; + FStoredBlock.PreviousUpdatedBlocks.CopyFrom(FPrevious); + LBlock := CT_OperationBlock_NUL; + LBlock.block := FNode.Bank.BlocksCount; + LBlock.account_key := FKey.PublicKey; + LBlock.protocol_version := CT_PROTOCOL_6; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := LBlock.block * CT_NewLineSecondsAvg; + LBlock.fee := 20; + LBlock.operations_hash := FStoredBlock.OperationsHashTree.HashTree; + FStoredBlock.SetOperationBlock(LBlock); + // Commit the real signed-operation snapshot, then return its exact operation + // block through storage. This tests history, not proof-of-work validation. + LError := ''; + RequireTrue('The stored snapshot commits with consistent balances and fees: ' + LError, + FSnapshot.Commit(LBlock, LError)); +end; + +procedure TStoredHistoryFixture.BuildLinkedHistory; +var + LStored: TPCOperationsComp; + LBlock: TOperationBlock; + LOperation: TOpTransaction; + LError: String; + LBlockIndex, LOperationIndex, LOperationCount: Integer; + LRecipient: Cardinal; + LAmount: UInt64; +begin + TStoredHistorySafeBox(FNode.Bank.SafeBox).BuildFunded(FKey.PublicKey, 10000); + TStoredHistoryStorage(FNode.Bank.Storage).LinkedBlocks := FLinkedBlocks; + for LBlockIndex := 0 to 2 do begin + FreeAndNil(FSnapshot); + FSnapshot := TPCSafeBoxTransaction.Create(FNode.Bank.SafeBox); + FPrevious.Clear; + LStored := TPCOperationsComp.Create(nil); + FLinkedBlocks.Add(LStored); + if LBlockIndex = 0 then + LOperationCount := 2 + else + LOperationCount := 1; + for LOperationIndex := 0 to LOperationCount - 1 do begin + if (LBlockIndex = 0) and (LOperationIndex = 1) then begin + LRecipient := 102; + LAmount := 100; + end else begin + LRecipient := 101; + LAmount := UInt64(LBlockIndex + 1) * 10; + end; + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_6, 100, + FSnapshot.Account(100).n_operation + 1, LRecipient, FKey, + LAmount, 1, CT_TOperationPayload_NUL); + try + LError := ''; + RequireTrue('Linked fixture operation validates: ' + LError, + LOperation.DoOperation(FPrevious, FSnapshot, LError)); + RequireTrue(LStored.OperationsHashTree.AddOperationToHashTree(LOperation)); + finally + LOperation.Free; + end; + end; + LStored.PreviousUpdatedBlocks.CopyFrom(FPrevious); + LBlock := CT_OperationBlock_NUL; + LBlock.block := FNode.Bank.BlocksCount; + LBlock.account_key := FKey.PublicKey; + LBlock.protocol_version := CT_PROTOCOL_6; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := LBlock.block * CT_NewLineSecondsAvg; + LBlock.fee := LOperationCount; + LBlock.operations_hash := LStored.OperationsHashTree.HashTree; + LStored.SetOperationBlock(LBlock); + LError := ''; + RequireTrue('Linked signed snapshot commits: ' + LError, + FSnapshot.Commit(LBlock, LError)); + end; + // Reuse the bank's normal post-commit notification so pending history and + // its operation template observe the same final chain as the stored reader. + FNode.Bank.UpdateValuesFromSafebox; + TStoredHistoryStorage(FNode.Bank.Storage).LoadedBlocks.Clear; +end; + +procedure TStoredHistoryFixture.PrepareLayeredHistory(AIncludeFirstInPrimary, AIncludeMiddleInLegacy: Boolean); +var LStorage: TAbstractMemBlockchainStorageSecondary; I: Integer; +begin + BuildLinkedHistory; + // Use both real file formats: older operations exist only in the legacy + // archive, just as after restoring a checkpoint before creating its index. + FNode.Bank.StorageClass := TAbstractMemBlockchainStorageSecondary; + LStorage := TAbstractMemBlockchainStorageSecondary(FNode.Bank.Storage); + RequireTrue('The layered history storage initializes in its fresh directory', LStorage.Initialize); + LStorage.UseMultithread := False; + LStorage.AutoFlushCache := False; + // Fix the legacy archive's first block before saving the newer primary tip. + RequireTrue('The legacy archive starts at the oldest generated block', LStorage.AuxStorage.SaveBlockChainBlock(FLinkedBlocks[0])); + if AIncludeFirstInPrimary then + RequireTrue('The primary index saves the oldest generated block', LStorage.SaveBlockChainBlock(FLinkedBlocks[0])); + RequireTrue('The primary index saves the newest generated block', LStorage.SaveBlockChainBlock(FLinkedBlocks[2])); + // Rewriting an older legacy block truncates its following archive. Populate + // that archive last, in ascending order, to retain the intended middle block. + for I := 0 to FLinkedBlocks.Count - 1 do + if (I <> 1) or AIncludeMiddleInLegacy then + RequireTrue('The legacy archive saves its generated signed block', LStorage.AuxStorage.SaveBlockChainBlock(FLinkedBlocks[I])); + RequireTrue('The fixture must retain the intended legacy middle block or gap', LStorage.AuxStorage.BlockExists(161) = AIncludeMiddleInLegacy); + RequireEquals('The primary index ends at the generated tip', 162, LStorage.LastBlock); + if AIncludeFirstInPrimary then + RequireEquals('The primary range surrounds its missing middle block', 160, LStorage.FirstBlock) + else + RequireEquals('The primary index starts after the older account update', 162, LStorage.FirstBlock); + RequireEquals('The legacy archive begins before the primary-only tip', 160, LStorage.AuxStorage.FirstBlock); +end; + +procedure TStoredHistoryFixture.RequireLoaded(const AExpected: array of Cardinal); +var + LLoaded: TList<Cardinal>; + I: Integer; +begin + LLoaded := TStoredHistoryStorage(FNode.Bank.Storage).LoadedBlocks; + RequireEquals('Stored history must make only the expected block reads', + Length(AExpected), LLoaded.Count); + for I := 0 to High(AExpected) do + RequireEquals('Unexpected stored block traversal at index ' + IntToStr(I), + AExpected[I], LLoaded[I]); +end; + +function TStoredHistoryFixture.History(ABlockDepth: Cardinal; ASkip, + ATake: Integer): TArray<TOperationResume>; +begin + Result := FNode.GetStoredOperationsAffectingAccounts([100], ABlockDepth, ASkip, ATake); +end; + +procedure TStoredHistoryFixture.FiniteDepthStopsAtTheInclusiveBoundary; +var + LRows: TArray<TOperationResume>; + LSource: TStoredHistoryPageSource; + LTable: TDataTable; + LResult: TPageFetchResult; +begin + BuildLinkedHistory; + LRows := History(1); + RequireEquals('Recent and boundary operations are included', 2, Length(LRows)); + RequireEquals(162, LRows[0].Block); + RequireEquals(161, LRows[1].Block); + RequireLoaded([162, 161]); + TStoredHistoryStorage(FNode.Bank.Storage).LoadedBlocks.Clear; + LSource := TStoredHistoryPageSource.Create(nil); + try + LSource.Accounts := TArray<Cardinal>.Create(100); + LSource.BlockDepth := 1; + LResult := LSource.ReadPage(LTable); + RequireEquals('The actual provider forwards the finite history depth', 2, + LResult.TotalDataCount); + RequireEquals(2, Length(LTable.Rows)); + RequireLoaded([162, 161]); + LSource.BlockDepth := High(Cardinal); + LResult := LSource.ReadPage(LTable); + RequireEquals('Maximum depth includes older linked operations', 4, + LResult.TotalDataCount); + RequireEquals(4, Length(LTable.Rows)); + RequireTrue('Maximum must actually read the older linked block', + TStoredHistoryStorage(FNode.Bank.Storage).LoadedBlocks.Contains(160)); + LRows := History(High(Cardinal)); + RequireEquals('The direct wrapper must accept the full unsigned depth', 4, + Length(LRows)); + RequireEquals(160, LRows[2].Block); + finally + LSource.Free; + end; +end; + +procedure TStoredHistoryFixture.ZeroDepthReadsTheTipOnly; +var + LRows: TArray<TOperationResume>; +begin + BuildLinkedHistory; + LRows := History(0); + RequireEquals('Depth zero includes only the tip operation', 1, Length(LRows)); + RequireEquals(162, LRows[0].Block); + RequireLoaded([162]); +end; + +procedure TStoredHistoryFixture.OldAccountAvoidsStoredReads; +var + LRows: TArray<TOperationResume>; +begin + BuildLinkedHistory; + LRows := FNode.GetStoredOperationsAffectingAccounts([102], 1, 0, MaxInt); + RequireEquals('The selected account was last updated before the cutoff', 0, + Length(LRows)); + RequireLoaded([]); +end; + +procedure TStoredHistoryFixture.FinitePagesStopOnceTheRequestedRowsAreFound; +var + LRows: TArray<TOperationResume>; +begin + BuildLinkedHistory; + LRows := History(2, 0, 1); + RequireEquals('Taking one recent row must return it', 1, Length(LRows)); + RequireEquals(162, LRows[0].Block); + RequireLoaded([162]); + TStoredHistoryStorage(FNode.Bank.Storage).LoadedBlocks.Clear; + LRows := History(2, 1, 1); + RequireEquals('Skip/take must return the next linked operation', 1, Length(LRows)); + RequireEquals(161, LRows[0].Block); + RequireLoaded([162, 161]); +end; + +procedure TStoredHistoryFixture.ZeroTakeAvoidsStoredReads; +var + LRows: TArray<TOperationResume>; +begin + BuildLinkedHistory; + LRows := History(2, 0, 0); + RequireEquals('A zero-row query has no stored rows', 0, Length(LRows)); + RequireLoaded([]); + LRows := FNode.GetStoredOperationsAffectingAccounts([], 2, 0, MaxInt); + RequireEquals('An empty selection has no stored rows', 0, Length(LRows)); + RequireLoaded([]); +end; + +procedure TStoredHistoryFixture.FiniteHistoryRetainsPendingOperations; +var + LOperation: TOpTransaction; + LSource: TStoredHistoryPageSource; + LRows: TList<TOperationResume>; + LError: String; +begin + BuildLinkedHistory; + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_6, 100, + FNode.Bank.SafeBox.Account(100).n_operation + 1, 101, FKey, + 7, 1, CT_TOperationPayload_NUL); + try + LError := ''; + RequireTrue('The real pending operation must be admitted: ' + LError, + FNode.AddOperation(nil, LOperation, LError)); + finally + LOperation.Free; + end; + TStoredHistoryStorage(FNode.Bank.Storage).LoadedBlocks.Clear; + LSource := TStoredHistoryPageSource.Create(nil); + LRows := TList<TOperationResume>.Create; + try + LSource.Accounts := TArray<Cardinal>.Create(100); + LSource.BlockDepth := 0; + LSource.FetchAll(LRows); + RequireEquals('Finite stored history must retain the live pending row', 2, + LRows.Count); + RequireEquals(163, LRows[0].Block); + RequireEquals(162, LRows[1].Block); + RequireLoaded([162]); + finally + LRows.Free; + LSource.Free; + end; +end; + +procedure TStoredHistoryFixture.StoredOperationHashesUseActualBlockHeight; +var + LRows: TArray<TOperationResume>; + LStored: TPCOperationsComp; + LExpectedHash: TRawBytes; + I, J: Integer; +begin + BuildLinkedHistory; + LRows := History(High(Cardinal)); + RequireEquals('All four linked operations must have their stored hashes', + 4, Length(LRows)); + for I := 0 to High(LRows) do + begin + LStored := nil; + for J := 0 to FLinkedBlocks.Count - 1 do + if FLinkedBlocks[J].OperationBlock.block = LRows[I].Block then + begin + LStored := FLinkedBlocks[J]; + Break; + end; + RequireTrue('The returned operation must identify a generated stored block', + Assigned(LStored)); + RequireTrue('The returned operation index must address that stored block', + (LRows[I].NOpInsideBlock >= 0) and (LRows[I].NOpInsideBlock < LStored.Count)); + // The block height is encoded into each hash. Checking the displayed Block + // alone cannot detect conversion with a local loop index instead of height. + LExpectedHash := TPCOperation.OperationHashValid( + LStored.Operation[LRows[I].NOpInsideBlock], LStored.OperationBlock.block); + RequireTrue('Stored operation hash must encode its actual block height', + TBaseType.Equals(LExpectedHash, LRows[I].OperationHash)); + if (LStored.OperationBlock.block > 0) and + (LStored.OperationBlock.block < CT_Protocol_Upgrade_v2_MinBlock) then + begin + LExpectedHash := TPCOperation.OperationHash_OLD( + LStored.Operation[LRows[I].NOpInsideBlock], LStored.OperationBlock.block); + RequireTrue('Legacy stored operation hash must use the same actual height', + TBaseType.Equals(LExpectedHash, LRows[I].OperationHash_OLD)); + end + else + RequireEquals('Stored blocks outside the legacy range have no legacy hash', + 0, Length(LRows[I].OperationHash_OLD)); + end; +end; + +procedure TStoredHistoryFixture.StoredBalanceAboveCardinalRetainsExactValue; +var LRows: TArray<TOperationResume>; LInitial: Int64; +begin + LInitial := Int64(High(Cardinal)) + 1000; + BuildStoredHistory(LInitial, 50, 200); + LRows := History(0); + RequireEquals(2, Length(LRows)); + RequireEquals('Latest committed balance must not narrow to 32 bits', + LInitial - 270, LRows[0].Balance); + RequireEquals('Previous committed balance remains exact', + LInitial - 60, LRows[1].Balance); + RequireEquals(1, LRows[0].NOpInsideBlock); + RequireEquals(0, LRows[1].NOpInsideBlock); +end; + +procedure TStoredHistoryFixture.ReverseSpendCrossesCardinalBoundary; +var LRows: TArray<TOperationResume>; LInitial: Int64; +begin + LInitial := Int64(High(Cardinal)) + 150; + BuildStoredHistory(LInitial, 50, 200); + LRows := History(0); + RequireEquals(2, Length(LRows)); + RequireEquals('Current balance fits 32 bits before the reverse traversal', + Int64(High(Cardinal)) - 120, LRows[0].Balance); + RequireEquals('Reversing the latest spend crosses the old map limit', + Int64(High(Cardinal)) + 90, LRows[1].Balance); + RequireEquals(Int64(-200), LRows[0].Amount); + RequireEquals(Int64(-10), LRows[0].Fee); +end; + +procedure TStoredHistoryFixture.AllHistoryDepthIncludesTheStoredBlock; +var LRows: TArray<TOperationResume>; +begin + BuildStoredHistory(1000, 50, 200); + LRows := History(FNode.Bank.BlocksCount); + RequireEquals('All-history depth includes the generated stored operations', + 2, Length(LRows)); + RequireEquals(Int64(730), LRows[0].Balance); + RequireEquals(Int64(940), LRows[1].Balance); +end; + +procedure TStoredHistoryFixture.ActualHistoryPagePublishesSigned64BitBalances; +var LSource: TStoredHistoryPageSource; LTable: TDataTable; + LResult: TPageFetchResult; LInitial: Int64; +begin + LInitial := Int64(High(Cardinal)) + 1000; + BuildStoredHistory(LInitial, 50, 200); + LSource := TStoredHistoryPageSource.Create(nil); + try + LSource.Accounts := TArray<Cardinal>.Create(100); + LSource.BlockDepth := FNode.Bank.BlocksCount; + LResult := LSource.ReadPage(LTable); + RequireEquals('The inherited provider materializes both stored operations', + 2, LResult.TotalDataCount); + RequireEquals(2, Length(LTable.Rows)); + RequireEquals('The actual table retains the exact signed balance', + LInitial - 270, Int64(LTable.Rows[0].Balance)); + RequireEquals(LInitial - 60, Int64(LTable.Rows[1].Balance)); + finally LSource.Free; end; +end; + +procedure TStoredHistoryFixture.MaximumReadsLegacyHistoryBeforeThePrimaryRange; +var LRows: TArray<TOperationResume>; LSource: TStoredHistoryPageSource; LTable: TDataTable; LResult: TPageFetchResult; +begin + PrepareLayeredHistory(False, True); + LRows := FNode.GetStoredOperationsAffectingAccounts([102], High(Cardinal), 0, MaxInt); + RequireEquals('Maximum finds an account last updated before the primary index begins', 1, Length(LRows)); + RequireEquals('The returned legacy operation retains its actual height', 160, LRows[0].Block); + RequireEquals('The returned legacy operation belongs to the selected account', 102, LRows[0].AffectedAccount); + LSource := TStoredHistoryPageSource.Create(nil); + try + LSource.Accounts := TArray<Cardinal>.Create(100); + LSource.BlockDepth := High(Cardinal); + LResult := LSource.ReadPage(LTable); + RequireEquals('The actual grid source counts all primary and legacy history', 4, LResult.TotalDataCount); + RequireEquals('The actual grid source publishes all available rows', 4, Length(LTable.Rows)); + LSource.BlockDepth := 2016; + LResult := LSource.ReadPage(LTable); + RequireEquals('Seven-day depth retains all rows in this short local chain', 4, LResult.TotalDataCount); + LSource.BlockDepth := 8640; + LResult := LSource.ReadPage(LTable); + RequireEquals('Thirty-day depth retains all rows in this short local chain', 4, LResult.TotalDataCount); + finally + LSource.Free; + end; +end; + +procedure TStoredHistoryFixture.MaximumReadsLegacyHistoryInsideAPrimaryGap; +var LRows: TArray<TOperationResume>; +begin + PrepareLayeredHistory(True, True); + LRows := History(High(Cardinal)); + RequireEquals('Maximum includes the block held only by legacy storage inside the primary range', 4, Length(LRows)); + RequireEquals('The newest history row is the primary tip', 162, LRows[0].Block); + RequireEquals('The missing primary block is loaded before its operations are scanned', 161, LRows[1].Block); + RequireEquals('Both operations from the oldest linked block remain available', 160, LRows[2].Block); + RequireEquals(160, LRows[3].Block); + LRows := History(1); + RequireEquals('Layered storage still honors the finite inclusive cutoff', 2, Length(LRows)); + RequireEquals(162, LRows[0].Block); + RequireEquals(161, LRows[1].Block); + LRows := History(0); + RequireEquals('Layered storage still honors tip-only history', 1, Length(LRows)); + RequireEquals(162, LRows[0].Block); +end; + +procedure TStoredHistoryFixture.MissingLegacyBlockDoesNotPublishAnotherBlocksOperations; +var LRows: TArray<TOperationResume>; +begin + PrepareLayeredHistory(True, False); + // A genuine archive gap must stop that account's linked traversal, rather + // than reusing the previous block component and publishing duplicate rows. + LRows := History(High(Cardinal)); + RequireEquals('A block absent from both stores does not invent history', 1, Length(LRows)); + RequireEquals('Only the actual available tip operation is returned', 162, LRows[0].Block); +end; + +procedure CheckStoredHistory(ACheck: TStoredHistoryCheck); +var LFixture: TStoredHistoryFixture; +begin + if ACheck = shcLivePendingFastPath then begin + CheckLivePendingHistory; + Exit; + end; + LFixture := TStoredHistoryFixture.Create; + try + case ACheck of + shcWideBalance: LFixture.StoredBalanceAboveCardinalRetainsExactValue; + shcReverseBoundary: LFixture.ReverseSpendCrossesCardinalBoundary; + shcAllHistory: LFixture.AllHistoryDepthIncludesTheStoredBlock; + shcActualPage: LFixture.ActualHistoryPagePublishesSigned64BitBalances; + shcFiniteDepth: LFixture.FiniteDepthStopsAtTheInclusiveBoundary; + shcTipOnly: LFixture.ZeroDepthReadsTheTipOnly; + shcOldAccount: LFixture.OldAccountAvoidsStoredReads; + shcFinitePage: LFixture.FinitePagesStopOnceTheRequestedRowsAreFound; + shcZeroTake: LFixture.ZeroTakeAvoidsStoredReads; + shcFiniteWithPending: LFixture.FiniteHistoryRetainsPendingOperations; + shcActualBlockHashes: LFixture.StoredOperationHashesUseActualBlockHeight; + shcLayeredBeforePrimary: LFixture.MaximumReadsLegacyHistoryBeforeThePrimaryRange; + shcLayeredPrimaryGap: LFixture.MaximumReadsLegacyHistoryInsideAPrimaryGap; + shcLayeredMissingBlock: LFixture.MissingLegacyBlockDoesNotPublishAnotherBlocksOperations; + end; + finally LFixture.Free; end; +end; + +function RunStoredHistoryChecks(const ALogFile: String): Integer; +var LLog: TStringList; LCheck: TStoredHistoryCheck; +const CNames: array[TStoredHistoryCheck] of String = + ('stored balance above Cardinal', 'reverse spend across Cardinal boundary', + 'all-history depth', 'actual inherited history page signed64 balances', + 'finite inclusive history depth and actual provider', 'depth zero reads tip only', + 'old selected account avoids stored reads', 'finite pages stop linked traversal', + 'zero take and empty selection avoid stored reads', + 'finite history retains live pending operations', 'live pending index fast path', + 'stored operation hashes encode actual block heights', + 'Maximum reads legacy history before the primary range', + 'Maximum reads legacy history inside a primary gap', + 'missing legacy block does not publish another blocks operations'); +begin + Result := 0; + LLog := TStringList.Create; + try + for LCheck := Low(TStoredHistoryCheck) to High(TStoredHistoryCheck) do + try + CheckStoredHistory(LCheck); + LLog.Add('PASS ' + CNames[LCheck]); + except on E: Exception do begin + Result := 1; + LLog.Add('FAIL ' + CNames[LCheck] + ': ' + E.ClassName + ': ' + E.Message + + ' address=' + IntToHex(NativeUInt(ExceptAddr), SizeOf(Pointer) * 2)); + end; end; + LLog.Add('RESULT ' + IntToStr(Result)); + LLog.SaveToFile(ALogFile); + finally LLog.Free; end; +end; + +end. diff --git a/src/tests/experimental/operations/UChangeInfoBatchChecks.pas b/src/tests/experimental/operations/UChangeInfoBatchChecks.pas new file mode 100644 index 000000000..9d211741f --- /dev/null +++ b/src/tests/experimental/operations/UChangeInfoBatchChecks.pas @@ -0,0 +1,592 @@ +unit UChangeInfoBatchChecks; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Portable change-type batch checks with generated keys and an in-memory + safebox. The real preparation, signature and core operation paths are used. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +procedure CheckChangeInfoMixedTargets; +procedure CheckChangeInfoCommonSigner; +procedure CheckChangeInfoSecondarySigner; +procedure CheckChangeInfoSkippedUnusableAccount; +procedure CheckChangeInfoChangedFunding; +procedure CheckChangeInfoPreservedSnapshots; +procedure CheckChangeInfoTargetSelection; +procedure CheckChangeInfoApprovedTargets; +procedure CheckChangeInfoNoChanges; +procedure CheckChangeInfoMalformedRequests; +function RunChangeInfoBatchChecks(const ALogFile: String): Integer; + +implementation + +uses + Classes, SysUtils, UMemory, UAccounts, UBlockChain, UOpTransaction, + UPCDataTypes, UCrypto, UBaseTypes, UConst, UWallet, UCoreObjects, UCoreUtils; + +type + TChangeInfoCheck = procedure; + + TChangeInfoBatchSafeBox = class(TPCSafeBox) + public + procedure Build(const AKey: TAccountKey); + procedure Seed(const AAccount: TAccount); + end; + + TChangeInfoBatchFixture = class + private + FDisposables: TDisposables; + function CopyAccounts(const AAccounts: TArray<TAccount>): TArray<TAccount>; + procedure CheckRequestUnchanged(const AAccounts, ASigners: TArray<TAccount>); + public + Owner, Other: TECPrivateKey; + Keys: TWalletKeys; + Request: TWalletOperationRequest; + Tree: TOperationsHashTree; + Error: String; + constructor Create; + destructor Destroy; override; + function Account(ANumber: Cardinal; AType: Word; ABalance: UInt64; + const AKey: TAccountKey): TAccount; + procedure Prepare; + procedure Reject; + procedure Validate(const ATargets, ASigners, ANonces: array of Cardinal; + ATypeOnly: Boolean = True); + end; + +procedure Require(ACondition: Boolean; const AMessage: String); +begin + if not ACondition then + raise Exception.Create('Change-info batch: ' + AMessage); +end; + +procedure TChangeInfoBatchSafeBox.Build(const AKey: TAccountKey); +var + LBlock: TOperationBlock; + I: Integer; +begin + for I := 0 to 159 do + begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := I; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := I * CT_NewLineSecondsAvg; + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + DoUpgradeToProtocol6; +end; + +procedure TChangeInfoBatchSafeBox.Seed(const AAccount: TAccount); +begin + UpdateAccount(AAccount.account, AAccount.accountInfo, AAccount.name, + AAccount.account_type, AAccount.balance, AAccount.n_operation, + AAccount.account_data, AAccount.account_seal, aus_transaction_commit, + 0, 0, False, False); +end; + +constructor TChangeInfoBatchFixture.Create; +begin + inherited Create; + TCrypto.InitCrypto; + Owner := FDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + Other := FDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + Owner.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + Other.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + Keys := FDisposables.AddObject(TWalletKeys.Create(nil)) as TWalletKeys; + Keys.AddPrivateKey('Generated batch test owner', Owner); + Request := Default(TWalletOperationRequest); + Request.OperationType := omtChangeInfo; + Request.Accounts := TArray<TAccount>.Create( + Account(100, 7, 1000, Owner.PublicKey), + Account(101, 7, 1000, Owner.PublicKey), + Account(102, 7, 1000, Owner.PublicKey)); + Request.SignerAccounts := CopyAccounts(Request.Accounts); + Request.Fee := 100; + Request.NewType := 9; + Request.NewName := TEncoding.ASCII.GetBytes('unused_batch_name'); + Request.SigningMode := akaPrimary; + Request.PayloadMode := pemNotEncrypt; + Request.PayloadContent := 'type change'; +end; + +destructor TChangeInfoBatchFixture.Destroy; +begin + Tree.Free; + inherited Destroy; +end; + +function TChangeInfoBatchFixture.Account(ANumber: Cardinal; AType: Word; + ABalance: UInt64; const AKey: TAccountKey): TAccount; +begin + Result := CT_Account_NUL; + Result.account := ANumber; + Result.accountInfo.state := as_Normal; + Result.accountInfo.accountKey := AKey.GetCopy; + Result.balance := ABalance; + Result.n_operation := 7; + Result.account_type := AType; + Result.name := TEncoding.ASCII.GetBytes('batch_account_' + IntToStr(ANumber)); + Result.account_data := TEncoding.ASCII.GetBytes('preserved data ' + IntToStr(ANumber)); +end; + +function TChangeInfoBatchFixture.CopyAccounts( + const AAccounts: TArray<TAccount>): TArray<TAccount>; +var + I: Integer; +begin + SetLength(Result, Length(AAccounts)); + for I := 0 to High(AAccounts) do + Result[I] := AAccounts[I].GetCopy; +end; + +procedure TChangeInfoBatchFixture.CheckRequestUnchanged( + const AAccounts, ASigners: TArray<TAccount>); +var + I: Integer; +begin + Require(Length(AAccounts) = Length(Request.Accounts), 'caller selection length changed'); + Require(Length(ASigners) = Length(Request.SignerAccounts), 'caller signer length changed'); + for I := 0 to High(AAccounts) do + Require(TAccountComp.EqualAccounts(AAccounts[I], Request.Accounts[I]), + 'caller account snapshot/order changed at ' + IntToStr(I)); + for I := 0 to High(ASigners) do + Require(TAccountComp.EqualAccounts(ASigners[I], Request.SignerAccounts[I]), + 'caller signer snapshot/order changed at ' + IntToStr(I)); +end; + +procedure TChangeInfoBatchFixture.Prepare; +var + LAccounts, LSigners: TArray<TAccount>; + LResult: Boolean; +begin + FreeAndNil(Tree); + LAccounts := CopyAccounts(Request.Accounts); + LSigners := CopyAccounts(Request.SignerAccounts); + LResult := TWIZOperationsHelper.PrepareOperations(Request, CT_PROTOCOL_6, + 160, Keys, Tree, Error); + CheckRequestUnchanged(LAccounts, LSigners); + Require(LResult, 'preparation rejected a valid request: ' + Error); + Require(Assigned(Tree), 'successful preparation returned no operations'); + Require(Error = '', 'successful preparation retained an error: ' + Error); +end; + +procedure TChangeInfoBatchFixture.Reject; +var + LAccounts, LSigners: TArray<TAccount>; + LResult: Boolean; +begin + FreeAndNil(Tree); + LAccounts := CopyAccounts(Request.Accounts); + LSigners := CopyAccounts(Request.SignerAccounts); + LResult := TWIZOperationsHelper.PrepareOperations(Request, CT_PROTOCOL_6, + 160, Keys, Tree, Error); + CheckRequestUnchanged(LAccounts, LSigners); + Require(not LResult, 'invalid request was accepted'); + Require(not Assigned(Tree), 'rejection leaked a partial operation tree'); + Require(Error <> '', 'rejection supplied no reason'); +end; + +procedure TChangeInfoBatchFixture.Validate( + const ATargets, ASigners, ANonces: array of Cardinal; ATypeOnly: Boolean); +var + LDisposables: TDisposables; + LSafeBox: TChangeInfoBatchSafeBox; + LTransaction: TPCSafeBoxTransaction; + LPrevious: TAccountPreviousBlockInfo; + LSnapshots: TArray<TAccount>; + LOperation: TOpChangeAccountInfo; + LBefore, LAfter: TAccount; + LError: String; + I, J, LSignerUses: Integer; + LChanged: Boolean; + + procedure AddSnapshot(const AAccount: TAccount); + var + K: Integer; + begin + for K := 0 to High(LSnapshots) do + if LSnapshots[K].account = AAccount.account then + Exit; + LSafeBox.Seed(AAccount); + SetLength(LSnapshots, Length(LSnapshots) + 1); + LSnapshots[High(LSnapshots)] := LSafeBox.Account(AAccount.account).GetCopy; + end; + +begin + Require(Length(ATargets) = Length(ASigners), 'invalid expected signer list'); + Require(Length(ATargets) = Length(ANonces), 'invalid expected counter list'); + Require(Tree.OperationsCount = Length(ATargets), 'wrong number of changed targets'); + Require(Tree.TotalFee = Int64(Length(ATargets)) * Request.Fee, 'skipped targets were charged fees'); + LSafeBox := LDisposables.AddObject(TChangeInfoBatchSafeBox.Create) as TChangeInfoBatchSafeBox; + LSafeBox.Build(Owner.PublicKey); + for I := 0 to High(Request.Accounts) do + AddSnapshot(Request.Accounts[I]); + for I := 0 to High(Request.SignerAccounts) do + AddSnapshot(Request.SignerAccounts[I]); + LTransaction := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + LPrevious := LDisposables.AddObject(TAccountPreviousBlockInfo.Create) as TAccountPreviousBlockInfo; + for I := 0 to Tree.OperationsCount - 1 do + begin + Require(Tree.GetOperation(I) is TOpChangeAccountInfo, 'unexpected prepared operation class'); + LOperation := TOpChangeAccountInfo(Tree.GetOperation(I)); + Require(LOperation.DestinationAccount = ATargets[I], 'changed target order differs from selection'); + Require(LOperation.SignerAccount = ASigners[I], 'wrong fee payer'); + Require(LOperation.N_Operation = ANonces[I], 'skipped target consumed an operation counter'); + Require(LOperation.OperationFee = Request.Fee, 'wrong individual fee'); + if ATypeOnly then + begin + Require(LOperation.Data.changes_type = [account_type], 'batch changed a name, owner or data'); + Require(LOperation.Data.new_type = Request.NewType, 'wrong requested account type'); + end + else + Require(LOperation.Data.changes_type = [account_name], 'name-only change altered another field'); + Require(TCrypto.ECDSAVerify(Owner.PublicKey, LOperation.GetDigestToSign, + LOperation.Data.sign), 'signature did not verify with the generated owner key'); + Require(not TCrypto.ECDSAVerify(Other.PublicKey, LOperation.GetDigestToSign, + LOperation.Data.sign), 'unrelated key verified a prepared signature'); + LError := ''; + Require(LOperation.DoOperation(LPrevious, LTransaction, LError), + 'core rejected the signed operation: ' + LError); + end; + Require(LTransaction.TotalFee = Tree.TotalFee, 'core charged a different total fee'); + for I := 0 to High(LSnapshots) do + begin + LBefore := LSnapshots[I]; + LAfter := LTransaction.Account(LBefore.account); + LChanged := False; + LSignerUses := 0; + for J := 0 to High(ATargets) do + begin + LChanged := LChanged or (ATargets[J] = LBefore.account); + if ASigners[J] = LBefore.account then + Inc(LSignerUses); + end; + if LChanged and ATypeOnly then + Require(LAfter.account_type = Request.NewType, 'core did not apply the new type') + else + Require(LAfter.account_type = LBefore.account_type, 'skipped account type changed'); + if LChanged and not ATypeOnly then + Require(TBaseType.Equals(Request.NewName, LAfter.name), 'single account name was not changed') + else + Require(TBaseType.Equals(LBefore.name, LAfter.name), 'account name changed'); + Require(TBaseType.Equals(LBefore.account_data, LAfter.account_data), 'account data changed'); + Require(TAccountComp.EqualAccountKeys(LBefore.accountInfo.accountKey, + LAfter.accountInfo.accountKey), 'account owner changed'); + Require(LAfter.balance = LBefore.balance - UInt64(Int64(LSignerUses) * Request.Fee), + 'fee charged to a skipped/non-signing account'); + Require(LAfter.n_operation = LBefore.n_operation + Cardinal(LSignerUses), + 'counter changed outside actual signer use'); + Require(TAccountComp.EqualAccounts(LBefore, LSafeBox.Account(LBefore.account)), + 'replay mutated the committed safebox'); + end; +end; + +procedure CheckChangeInfoMixedTargets; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; + LTypes: array[0..1] of Word; + LTargets: TArray<Cardinal>; + LNonces: TArray<Cardinal>; + I, J, K: Integer; +begin + LTypes[0] := 0; + LTypes[1] := High(Word); + // Keep the already-matching account first in the first case: this is the + // historical whole-batch rejection, before later changed targets were read. + for I := 0 to High(LTypes) do + for J := 0 to 2 do + begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.NewType := LTypes[I]; + for K := 0 to 2 do + LFixture.Request.Accounts[K].account_type := 1; + LFixture.Request.Accounts[J].account_type := LTypes[I]; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + SetLength(LTargets, 0); + for K := 0 to 2 do + if K <> J then + begin + SetLength(LTargets, Length(LTargets) + 1); + LTargets[High(LTargets)] := LFixture.Request.Accounts[K].account; + end; + LNonces := TArray<Cardinal>.Create(8, 8); + LFixture.Prepare; + LFixture.Validate(LTargets, LTargets, LNonces); + end; +end; + +procedure CheckChangeInfoCommonSigner; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; +begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[0].account_type := LFixture.Request.NewType; + LFixture.Request.Accounts[0].balance := 200; + LFixture.Request.Accounts[1].balance := 0; + LFixture.Request.Accounts[2].balance := 0; + LFixture.Request.SignerAccounts := TArray<TAccount>.Create(LFixture.Request.Accounts[0]); + LFixture.Prepare; + // A matching account may still be the explicitly selected common fee payer. + LFixture.Validate([101, 102], [100, 100], [8, 9]); +end; + +procedure CheckChangeInfoSecondarySigner; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; +begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[0].account_type := LFixture.Request.NewType; + LFixture.Request.Accounts[0].balance := 0; + LFixture.Request.Accounts[1].balance := 100; + LFixture.Request.Accounts[2].balance := 0; + LFixture.Request.SignerAccounts := TArray<TAccount>.Create( + LFixture.Account(103, 7, 100, LFixture.Owner.PublicKey)); + LFixture.Request.SigningMode := akaSecondary; + LFixture.Prepare; + LFixture.Validate([101, 102], [101, 103], [8, 8]); +end; + +procedure CheckChangeInfoSkippedUnusableAccount; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; +begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[0] := LFixture.Account(100, 9, 0, LFixture.Other.PublicKey); + LFixture.Request.Accounts[0].n_operation := High(Cardinal); + LFixture.Request.Accounts[0].accountInfo.state := as_ForSale; + LFixture.Request.Accounts[0].accountInfo.new_publicKey := LFixture.Owner.PublicKey; + LFixture.Request.Accounts[0].accountInfo.locked_until_block := 200; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Prepare; + LFixture.Validate([101, 102], [101, 102], [8, 8]); +end; + +procedure CheckChangeInfoChangedFunding; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; +begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[0].account_type := LFixture.Request.NewType; + LFixture.Request.Accounts[1].balance := 100; + LFixture.Request.Accounts[2].balance := 99; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Reject; + LFixture.Request.Accounts[2].balance := 100; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Prepare; + LFixture.Validate([101, 102], [101, 102], [8, 8]); + LFixture.Request.Accounts[0].balance := 199; + LFixture.Request.SignerAccounts := TArray<TAccount>.Create(LFixture.Request.Accounts[0]); + LFixture.Reject; + LFixture.Request.Accounts[0].balance := 200; + LFixture.Request.SignerAccounts := TArray<TAccount>.Create(LFixture.Request.Accounts[0]); + LFixture.Prepare; + LFixture.Validate([101, 102], [100, 100], [8, 9]); +end; + +procedure CheckChangeInfoPreservedSnapshots; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; + LCallerSelection: TArray<TAccount>; +begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[0].account_type := LFixture.Request.NewType; + LFixture.Request.Accounts[1].account_type := LFixture.Request.NewType; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LCallerSelection := Copy(LFixture.Request.Accounts); + LFixture.Prepare; + // One remaining target is still a batch: a form's name edit must not rename it. + LFixture.Validate([102], [102], [8]); + Require(TAccountComp.EqualAccounts(LCallerSelection[0], LFixture.Request.Accounts[0]) and + TAccountComp.EqualAccounts(LCallerSelection[1], LFixture.Request.Accounts[1]) and + TAccountComp.EqualAccounts(LCallerSelection[2], LFixture.Request.Accounts[2]), + 'caller-owned selection was changed by filtering'); +end; + +procedure CheckChangeInfoTargetSelection; +{$IFNDEF CHANGEINFO_BATCH_BASELINE} +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; + LTargets: TArray<TAccount>; +{$ENDIF} +begin +{$IFNDEF CHANGEINFO_BATCH_BASELINE} + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[1].account_type := LFixture.Request.NewType; + LTargets := TWIZOperationsHelper.GetChangeAccountInfoTargets( + LFixture.Request.Accounts, LFixture.Request.NewType); + Require((Length(LTargets) = 2) and (LTargets[0].account = 100) and + (LTargets[1].account = 102), 'target selection did not preserve filtered order'); + LTargets[0].account := 999; + Require(LFixture.Request.Accounts[0].account = 100, 'target selection shared the outer array'); + LFixture.Request.Accounts := TArray<TAccount>.Create(LFixture.Request.Accounts[1]); + LTargets := TWIZOperationsHelper.GetChangeAccountInfoTargets( + LFixture.Request.Accounts, LFixture.Request.NewType); + Require((Length(LTargets) = 1) and (LTargets[0].account = 101), + 'single-account name changes lost their target'); + LTargets[0].account := 999; + Require(LFixture.Request.Accounts[0].account = 101, 'single target shared the outer array'); + LFixture.Request.Accounts := TArray<TAccount>.Create( + LFixture.Request.Accounts[0], LFixture.Request.Accounts[0]); + LTargets := TWIZOperationsHelper.GetChangeAccountInfoTargets( + LFixture.Request.Accounts, LFixture.Request.NewType); + Require(Length(LTargets) = 0, 'an all-matching batch retained a target'); + LTargets := TWIZOperationsHelper.GetChangeAccountInfoTargets(nil, 0); + Require(Length(LTargets) = 0, 'empty selection acquired a target'); +{$ENDIF} +end; + +procedure CheckChangeInfoApprovedTargets; +{$IFNDEF CHANGEINFO_BATCH_BASELINE} +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; + LTemporary: TAccount; +{$ENDIF} +begin +{$IFNDEF CHANGEINFO_BATCH_BASELINE} + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[0].account_type := LFixture.Request.NewType; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Request.ApprovedChangeInfoTargets := TWIZOperationsHelper.GetChangeAccountInfoTargets( + LFixture.Request.Accounts, LFixture.Request.NewType); + // The approval records identity, not stale balances or operation counters. + LFixture.Request.ApprovedChangeInfoTargets[0].balance := 0; + LFixture.Request.ApprovedChangeInfoTargets[0].n_operation := 1; + LFixture.Prepare; + LFixture.Validate([101, 102], [101, 102], [8, 8]); + LTemporary := LFixture.Request.ApprovedChangeInfoTargets[0]; + LFixture.Request.ApprovedChangeInfoTargets[0] := LFixture.Request.ApprovedChangeInfoTargets[1]; + LFixture.Request.ApprovedChangeInfoTargets[1] := LTemporary; + LFixture.Reject; + LFixture.Request.ApprovedChangeInfoTargets := TWIZOperationsHelper.GetChangeAccountInfoTargets( + LFixture.Request.Accounts, LFixture.Request.NewType); + LFixture.Request.Accounts[1].account_type := LFixture.Request.NewType; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Reject; + LFixture.Request.Accounts[0].account_type := 7; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Keys.Clear; + LFixture.Reject; + Require(Pos('review', LowerCase(LFixture.Error)) > 0, + 'stale approval was checked after private-key lookup'); +{$ENDIF} +end; + +procedure CheckChangeInfoNoChanges; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; + I: Integer; +begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + for I := 0 to High(LFixture.Request.Accounts) do + LFixture.Request.Accounts[I].account_type := LFixture.Request.NewType; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Reject; + LFixture.Request.Accounts := TArray<TAccount>.Create(LFixture.Request.Accounts[0]); + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Request.NewName := Copy(LFixture.Request.Accounts[0].name); + LFixture.Reject; + // The single-account editor must still allow a real name-only change. + LFixture.Request.NewName := TEncoding.ASCII.GetBytes('renamed_single_account'); + LFixture.Prepare; + Require(LFixture.Tree.OperationsCount = 1, 'single name change was lost'); + Require(TOpChangeAccountInfo(LFixture.Tree.GetOperation(0)).Data.changes_type = [account_name], + 'single name change acquired an unintended type change'); + LFixture.Validate([100], [100], [8], False); +end; + +procedure CheckChangeInfoMalformedRequests; +var + LDisposables: TDisposables; + LFixture: TChangeInfoBatchFixture; + LOriginalAccounts, LOriginalSigners: TArray<TAccount>; +begin + LFixture := LDisposables.AddObject(TChangeInfoBatchFixture.Create) as TChangeInfoBatchFixture; + LFixture.Request.Accounts[0].account_type := LFixture.Request.NewType; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LOriginalAccounts := Copy(LFixture.Request.Accounts); + LOriginalSigners := Copy(LFixture.Request.SignerAccounts); + LFixture.Request.Accounts := TArray<TAccount>.Create(LOriginalAccounts[0], LOriginalAccounts[0]); + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Reject; + LFixture.Request.Accounts := TArray<TAccount>.Create(LOriginalAccounts[1], LOriginalAccounts[1]); + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Reject; + LFixture.Request.Accounts := Copy(LOriginalAccounts); + LFixture.Request.SignerAccounts := nil; + LFixture.Reject; + LFixture.Request.SignerAccounts := TArray<TAccount>.Create(LOriginalSigners[0], LOriginalSigners[1]); + LFixture.Reject; + LFixture.Request.SignerAccounts := TArray<TAccount>.Create( + LFixture.Account(103, 7, 1000, LFixture.Other.PublicKey)); + LFixture.Reject; + LFixture.Request.SignerAccounts := Copy(LOriginalSigners); + LFixture.Request.Accounts[1].n_operation := High(Cardinal); + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Reject; + LFixture.Request.Accounts := Copy(LOriginalAccounts); + LFixture.Request.Accounts[1].accountInfo.state := as_ForSale; + LFixture.Request.Accounts[1].accountInfo.new_publicKey := LFixture.Other.PublicKey; + LFixture.Request.Accounts[1].accountInfo.locked_until_block := 200; + LFixture.Request.SignerAccounts := Copy(LFixture.Request.Accounts); + LFixture.Reject; + LFixture.Request.Accounts := Copy(LOriginalAccounts); + LFixture.Request.SignerAccounts := Copy(LOriginalSigners); + LFixture.Keys.Clear; + LFixture.Keys.AddPublicKey('Generated watch-only key', LFixture.Owner.PublicKey); + LFixture.Reject; +end; + +function RunChangeInfoBatchChecks(const ALogFile: String): Integer; +var + LDisposables: TDisposables; + LLog: TStringList; + + procedure Run(const AName: String; ACheck: TChangeInfoCheck); + begin + ACheck; + LLog.Add('PASS ' + AName); + LLog.SaveToFile(ALogFile); + end; + +begin + LLog := LDisposables.AddObject(TStringList.Create) as TStringList; + Result := 1; + try + Run('mixed-target-order-and-type-boundaries', CheckChangeInfoMixedTargets); + Run('common-signer-skipped-target-fees-and-counters', CheckChangeInfoCommonSigner); + Run('secondary-signer-own-funds-before-fallback', CheckChangeInfoSecondarySigner); + Run('skipped-watch-only-locked-exhausted-target', CheckChangeInfoSkippedUnusableAccount); + Run('only-changed-targets-require-funding', CheckChangeInfoChangedFunding); + Run('caller-snapshots-and-batch-names-preserved', CheckChangeInfoPreservedSnapshots); +{$IFNDEF CHANGEINFO_BATCH_BASELINE} + Run('target-selection-order-and-independent-array', CheckChangeInfoTargetSelection); + Run('approved-target-identities-and-stale-set', CheckChangeInfoApprovedTargets); +{$ENDIF} + Run('all-unchanged-and-single-no-op-rejected', CheckChangeInfoNoChanges); + Run('duplicates-missing-signers-and-invalid-changed-targets', CheckChangeInfoMalformedRequests); + LLog.Add('PASS complete change-info batch preparation checks'); + Result := 0; + except + on E: Exception do + LLog.Add('FAIL ' + E.ClassName + ': ' + E.Message); + end; + LLog.SaveToFile(ALogFile); +end; + +end. diff --git a/src/tests/experimental/operations/UChangeInfoBatchTests.pas b/src/tests/experimental/operations/UChangeInfoBatchTests.pas new file mode 100644 index 000000000..ab967fac2 --- /dev/null +++ b/src/tests/experimental/operations/UChangeInfoBatchTests.pas @@ -0,0 +1,83 @@ +unit UChangeInfoBatchTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$mode delphi} + +interface + +uses fpcunit, testregistry; + +type + TChangeInfoBatchTests = class(TTestCase) + published + procedure MixedTargetsKeepOrderAndBoundaryTypes; + procedure CommonSignerPaysOnlyChangedTargets; + procedure SecondarySignerUsesTargetBeforeFallback; + procedure SkippedTargetsNeedNoFundsKeyOrCounter; + procedure ChangedTargetsMustBeFunded; + procedure CallerSnapshotsAndNamesArePreserved; + procedure TargetSelectionKeepsOrderAndOwnArray; + procedure ApprovalRejectsAChangedTargetSet; + procedure AllUnchangedAndSingleNoOpAreRejected; + procedure MalformedSelectionsAreRejected; + end; + +implementation + +uses UChangeInfoBatchChecks; + +procedure TChangeInfoBatchTests.MixedTargetsKeepOrderAndBoundaryTypes; +begin + CheckChangeInfoMixedTargets; +end; + +procedure TChangeInfoBatchTests.CommonSignerPaysOnlyChangedTargets; +begin + CheckChangeInfoCommonSigner; +end; + +procedure TChangeInfoBatchTests.SecondarySignerUsesTargetBeforeFallback; +begin + CheckChangeInfoSecondarySigner; +end; + +procedure TChangeInfoBatchTests.SkippedTargetsNeedNoFundsKeyOrCounter; +begin + CheckChangeInfoSkippedUnusableAccount; +end; + +procedure TChangeInfoBatchTests.ChangedTargetsMustBeFunded; +begin + CheckChangeInfoChangedFunding; +end; + +procedure TChangeInfoBatchTests.CallerSnapshotsAndNamesArePreserved; +begin + CheckChangeInfoPreservedSnapshots; +end; + +procedure TChangeInfoBatchTests.TargetSelectionKeepsOrderAndOwnArray; +begin + CheckChangeInfoTargetSelection; +end; + +procedure TChangeInfoBatchTests.ApprovalRejectsAChangedTargetSet; +begin + CheckChangeInfoApprovedTargets; +end; + +procedure TChangeInfoBatchTests.AllUnchangedAndSingleNoOpAreRejected; +begin + CheckChangeInfoNoChanges; +end; + +procedure TChangeInfoBatchTests.MalformedSelectionsAreRejected; +begin + CheckChangeInfoMalformedRequests; +end; + +initialization + RegisterTest(TChangeInfoBatchTests); + +end. diff --git a/src/tests/experimental/operations/UOperationPreparationTests.pas b/src/tests/experimental/operations/UOperationPreparationTests.pas new file mode 100644 index 000000000..82d97de86 --- /dev/null +++ b/src/tests/experimental/operations/UOperationPreparationTests.pas @@ -0,0 +1,788 @@ +unit UOperationPreparationTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$mode delphi} + +interface + +uses + Classes, SysUtils, fpcunit, testregistry, UCoreUtils, UCoreObjects, UWallet, + UAccounts, UBlockChain, UOpTransaction, UCrypto, UPCDataTypes, UBaseTypes; + +type + // Shared in-memory fixture for preparation and RAW/offline signing tests. + TOperationSafeBox = class(TPCSafeBox) + public + procedure Build(const AKey: TAccountKey); + procedure Seed(const AAccount: TAccount); + end; + + // Every test uses generated in-memory keys and explicit account snapshots. + // Preparation never loads TWallet.Keys, creates TNode.Node or broadcasts. + TOperationPreparationTests = class(TTestCase) + private + FKeys: TWalletKeys; + FOwner, FRecipient: TECPrivateKey; + FRequest: TWalletOperationRequest; + FTree: TOperationsHashTree; + FError: string; + function Account(ANumber: Cardinal; ABalance: UInt64; AKey: TECPrivateKey): TAccount; + procedure Prepare; + procedure Reject; + procedure AssertSigned(AOperation: TPCOperation); + procedure SetOperation(AType: TExecuteOperationType); + procedure ValidatePreparedBatch; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure SendSpecifiedAmountAndFeeAreSigned; + procedure SendAllBalancePreservesFee; + procedure SendRejectsAmountPlusFeeOverBalance; + procedure RejectsNegativeAmountAndFee; + procedure RejectsDuplicateAccounts; + procedure RejectsWatchOnlySigner; + procedure RejectsLockedSigner; + procedure RejectsDifferentOwnerSigner; + procedure EmptyPayloadClearsPreviousPayload; + procedure RejectsOversizedPayload; + procedure PasswordEncryptionRequiresPassword; + procedure ChangeKeyUsesCommonSignerLastWithoutReorderingInput; + procedure ChangeKeyProtocolOneInitializesFee; + procedure CommonSignerPaysCumulativeFeesAndCounters; + procedure CommonSignerInsufficientForBatchFailsAtomically; + procedure SecondarySignerPaysOnlyForUnderfundedTargets; + procedure PublicSaleClearsBuyerKeyAndLock; + procedure PrivateSaleUsesBuyerKeyAndFutureLock; + procedure PrivateSaleRejectsExpiredLock; + procedure DelistSignsSaleAccount; + procedure ChangeInfoEncodesPayloadWithoutChangingAccountData; + procedure ChangeInfoRejectsMissingSigner; + procedure ChangeInfoRejectsNoChanges; + procedure BuySignsWithFundingKeyAndSupportsPrefundedTarget; + procedure BuyRejectsInsufficientPriceFunding; + procedure BuyRejectsWrongPrivateSaleRecipient; + procedure BuyRejectsExpiredPrivateSale; + procedure AllSixPreparedOperationsPassCoreValidation; + procedure EmptyRawBytesSerializationRetainsLengthPrefix; + procedure EmptyAccountSealConvertsToZeroPaddedBytes; + procedure AbstractMemNullAndPopulatedKeysRoundTrip; + procedure PayloadEncryptionModesRoundTrip; + procedure PurchaseRecipientPayloadUsesNewOwnerKey; + procedure CorePreflightLeavesPendingSnapshotUnchanged; + procedure AdvancedSwapsClaimsAndAccountDataPassCoreValidation; + end; + +implementation + +uses UConst, UMemory, UAbstractMem, UPCAbstractMemAccountKeys, UPCEncryption; + +procedure TOperationSafeBox.Build(const AKey: TAccountKey); +var + LBlock: TOperationBlock; + i: Integer; +begin + for i := 0 to 159 do + begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := i; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.timestamp := i * CT_NewLineSecondsAvg; + AddNew(LBlock); + end; + DoUpgradeToProtocol5; + DoUpgradeToProtocol6; +end; + +procedure TOperationSafeBox.Seed(const AAccount: TAccount); +begin + UpdateAccount(AAccount.account, AAccount.accountInfo, AAccount.name, + AAccount.account_type, AAccount.balance, AAccount.n_operation, + AAccount.account_data, AAccount.account_seal, aus_transaction_commit, + 0, 0, False, False); +end; + +function TOperationPreparationTests.Account(ANumber: Cardinal; ABalance: UInt64; AKey: TECPrivateKey): TAccount; +begin + Result := CT_Account_NUL; + Result.account := ANumber; + Result.accountInfo.state := as_Normal; + Result.accountInfo.accountKey := AKey.PublicKey; + Result.balance := ABalance; + Result.n_operation := 7; +end; + +procedure TOperationPreparationTests.SetUp; +begin + TCrypto.InitCrypto; + FOwner := TECPrivateKey.Create; + FRecipient := TECPrivateKey.Create; + FOwner.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FRecipient.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FKeys := TWalletKeys.Create(nil); + FKeys.AddPrivateKey('Ephemeral test key', FOwner); + FTree := nil; + FRequest := Default(TWalletOperationRequest); + FRequest.OperationType := omtSendPasc; + FRequest.Accounts := TArray<TAccount>.Create(Account(100, 100000, FOwner)); + FRequest.SignerAccounts := Copy(FRequest.Accounts); + FRequest.DestinationAccount := Account(101, 0, FRecipient); + FRequest.SellerAccount := Account(102, 0, FRecipient); + FRequest.Amount := 25000; + FRequest.Fee := 100; + FRequest.SendMode := akaSpecifiedAmount; + FRequest.SigningMode := akaPrimary; + FRequest.PayloadMode := pemNotEncrypt; + FRequest.PublicKey := FRecipient.PublicKey; + FRequest.SalePrice := 50000; + FRequest.NewType := 1; +end; + +procedure TOperationPreparationTests.TearDown; +begin + FTree.Free; + FKeys.Free; + FRecipient.Free; + FOwner.Free; +end; + +procedure TOperationPreparationTests.Prepare; +begin + FreeAndNil(FTree); + AssertTrue(FError, TWIZOperationsHelper.PrepareOperations(FRequest, CT_PROTOCOL_6, 1000, FKeys, FTree, FError)); + AssertNotNull(FTree); + AssertEquals('', FError); +end; + +procedure TOperationPreparationTests.Reject; +begin + FreeAndNil(FTree); + AssertFalse(TWIZOperationsHelper.PrepareOperations(FRequest, CT_PROTOCOL_6, 1000, FKeys, FTree, FError)); + AssertNull('No partially prepared batch may escape on failure', FTree); + AssertTrue('A rejected request must explain the error', FError <> ''); +end; + +procedure TOperationPreparationTests.SetOperation(AType: TExecuteOperationType); +begin + FRequest.OperationType := AType; + if AType = omtDelistAccountFromSale then + begin + FRequest.Accounts[0].accountInfo.state := as_ForSale; + FRequest.Accounts[0].accountInfo.price := FRequest.SalePrice; + end; + if AType = omtBuyAccount then + begin + FRequest.DestinationAccount.accountInfo.state := as_ForSale; + FRequest.DestinationAccount.accountInfo.price := FRequest.SalePrice; + FRequest.DestinationAccount.accountInfo.account_to_pay := 102; + end; +end; + +procedure TOperationPreparationTests.ValidatePreparedBatch; +var + LDisposables: TDisposables; + LSafeBox: TOperationSafeBox; + LTransaction: TPCSafeBoxTransaction; + LPrevious: TAccountPreviousBlockInfo; + LError: string; + i, j: Integer; + LSelected: Boolean; +begin + LSafeBox := LDisposables.AddObject(TOperationSafeBox.Create) as TOperationSafeBox; + LSafeBox.Build(FOwner.PublicKey); + for i := 0 to High(FRequest.Accounts) do + LSafeBox.Seed(FRequest.Accounts[i]); + for i := 0 to High(FRequest.SignerAccounts) do + begin + LSelected := False; + for j := 0 to High(FRequest.Accounts) do + LSelected := LSelected or (FRequest.SignerAccounts[i].account = FRequest.Accounts[j].account); + if not LSelected then + LSafeBox.Seed(FRequest.SignerAccounts[i]); + end; + LSafeBox.Seed(FRequest.SellerAccount); + LTransaction := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + LPrevious := LDisposables.AddObject(TAccountPreviousBlockInfo.Create) as TAccountPreviousBlockInfo; + LError := ''; + for i := 0 to FTree.OperationsCount - 1 do + AssertTrue('Core rejected prepared batch: ' + LError, + FTree.GetOperation(i).DoOperation(LPrevious, LTransaction, LError)); + AssertEquals(FTree.TotalFee, LTransaction.TotalFee); +end; + +procedure TOperationPreparationTests.AssertSigned(AOperation: TPCOperation); +var + LSignature: TECDSA_SIG; +begin + if AOperation is TOpTransaction then + LSignature := TOpTransaction(AOperation).Data.sign + else if AOperation is TOpChangeKey then + LSignature := TOpChangeKey(AOperation).Data.sign + else if AOperation is TOpListAccount then + LSignature := TOpListAccount(AOperation).Data.sign + else if AOperation is TOpChangeAccountInfo then + LSignature := TOpChangeAccountInfo(AOperation).Data.sign + else + Fail('Unexpected operation type'); + AssertTrue('The operation must verify against the funding/owner key', + TCrypto.ECDSAVerify(FOwner.PublicKey, AOperation.GetDigestToSign, LSignature)); + AssertFalse('An unrelated key must not verify the signature', + TCrypto.ECDSAVerify(FRecipient.PublicKey, AOperation.GetDigestToSign, LSignature)); +end; + +procedure TOperationPreparationTests.SendSpecifiedAmountAndFeeAreSigned; +begin + Prepare; + AssertEquals(Int64(25000), FTree.GetOperation(0).OperationAmount); + AssertEquals(Int64(100), FTree.GetOperation(0).OperationFee); + AssertEquals(8, Integer(FTree.GetOperation(0).N_Operation)); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.SendAllBalancePreservesFee; +begin + FRequest.SendMode := akaAllBalance; + Prepare; + AssertEquals(Int64(99900), FTree.GetOperation(0).OperationAmount); + AssertEquals(Int64(100), FTree.GetOperation(0).OperationFee); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.SendRejectsAmountPlusFeeOverBalance; +begin + FRequest.Amount := 99950; + Reject; +end; + +procedure TOperationPreparationTests.RejectsNegativeAmountAndFee; +begin + FRequest.Amount := -1; + Reject; + FRequest.Amount := 100; + FRequest.Fee := -1; + Reject; + FRequest.Fee := CT_MaxTransactionFee + 1; + Reject; +end; + +procedure TOperationPreparationTests.RejectsDuplicateAccounts; +begin + FRequest.Accounts := TArray<TAccount>.Create(FRequest.Accounts[0], FRequest.Accounts[0]); + Reject; +end; + +procedure TOperationPreparationTests.RejectsWatchOnlySigner; +begin + FKeys.Clear; + FKeys.AddPublicKey('Watch only', FOwner.PublicKey); + Reject; +end; + +procedure TOperationPreparationTests.RejectsLockedSigner; +begin + FRequest.Accounts[0].accountInfo.state := as_ForSale; + FRequest.Accounts[0].accountInfo.new_publicKey := FRecipient.PublicKey; + FRequest.Accounts[0].accountInfo.locked_until_block := 1050; + Reject; +end; + +procedure TOperationPreparationTests.RejectsDifferentOwnerSigner; +begin + SetOperation(omtChangeKey); + FRequest.SignerAccounts[0] := Account(103, 100000, FRecipient); + Reject; +end; + +procedure TOperationPreparationTests.EmptyPayloadClearsPreviousPayload; +begin + FRequest.PayloadContent := 'previous payload'; + Prepare; + AssertTrue(Length(FTree.GetOperation(0).OperationPayload.payload_raw) > 0); + FRequest.PayloadContent := ''; + Prepare; + AssertEquals(0, Integer(FTree.GetOperation(0).OperationPayload.payload_type)); + AssertEquals(0, Length(FTree.GetOperation(0).OperationPayload.payload_raw)); +end; + +procedure TOperationPreparationTests.RejectsOversizedPayload; +begin + FRequest.PayloadContent := StringOfChar('x', CT_MaxPayloadSize + 1); + Reject; +end; + +procedure TOperationPreparationTests.PasswordEncryptionRequiresPassword; +begin + FRequest.PayloadMode := pemEncryptWithPassword; + FRequest.PayloadContent := 'secret'; + Reject; +end; + +procedure TOperationPreparationTests.ChangeKeyUsesCommonSignerLastWithoutReorderingInput; +begin + SetOperation(omtChangeKey); + FRequest.Accounts := TArray<TAccount>.Create(Account(100, 100000, FOwner), Account(103, 1000, FOwner)); + Prepare; + AssertEquals(100, Integer(FRequest.Accounts[0].account)); + AssertEquals(103, Integer(FTree.GetOperation(0).DestinationAccount)); + AssertEquals(100, Integer(FTree.GetOperation(1).DestinationAccount)); + AssertEquals(8, Integer(FTree.GetOperation(0).N_Operation)); + AssertEquals(9, Integer(FTree.GetOperation(1).N_Operation)); + AssertSigned(FTree.GetOperation(0)); + AssertSigned(FTree.GetOperation(1)); + ValidatePreparedBatch; +end; + +procedure TOperationPreparationTests.ChangeKeyProtocolOneInitializesFee; +begin + SetOperation(omtChangeKey); + AssertTrue(FError, TWIZOperationsHelper.PrepareOperations(FRequest, CT_PROTOCOL_1, 1000, FKeys, FTree, FError)); + AssertEquals(Int64(100), FTree.GetOperation(0).OperationFee); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.CommonSignerPaysCumulativeFeesAndCounters; +begin + SetOperation(omtEnlistAccountForSale); + FRequest.Accounts := TArray<TAccount>.Create(Account(103, 0, FOwner), Account(104, 0, FOwner)); + FRequest.SignerAccounts := TArray<TAccount>.Create(Account(100, 200, FOwner)); + Prepare; + AssertEquals(Int64(200), FTree.TotalFee); + AssertEquals(8, Integer(FTree.GetOperation(0).N_Operation)); + AssertEquals(9, Integer(FTree.GetOperation(1).N_Operation)); + AssertSigned(FTree.GetOperation(1)); + ValidatePreparedBatch; +end; + +procedure TOperationPreparationTests.CommonSignerInsufficientForBatchFailsAtomically; +begin + SetOperation(omtEnlistAccountForSale); + FRequest.Accounts := TArray<TAccount>.Create(Account(103, 0, FOwner), Account(104, 0, FOwner)); + FRequest.SignerAccounts := TArray<TAccount>.Create(Account(100, 199, FOwner)); + Reject; +end; + +procedure TOperationPreparationTests.SecondarySignerPaysOnlyForUnderfundedTargets; +begin + SetOperation(omtChangeKey); + FRequest.SigningMode := akaSecondary; + FRequest.Accounts := TArray<TAccount>.Create(Account(103, 100, FOwner), Account(104, 0, FOwner)); + FRequest.SignerAccounts := TArray<TAccount>.Create(Account(100, 100, FOwner)); + Prepare; + AssertEquals(103, Integer(FTree.GetOperation(0).SignerAccount)); + AssertEquals(100, Integer(FTree.GetOperation(1).SignerAccount)); + AssertEquals(Int64(200), FTree.TotalFee); + AssertSigned(FTree.GetOperation(0)); + AssertSigned(FTree.GetOperation(1)); + ValidatePreparedBatch; +end; + +procedure TOperationPreparationTests.PublicSaleClearsBuyerKeyAndLock; +var LData: TOpListAccountData; +begin + SetOperation(omtEnlistAccountForSale); + FRequest.SaleMode := akaPublicSale; + FRequest.LockedUntilBlock := 1500; + Prepare; + LData := TOpListAccount(FTree.GetOperation(0)).Data; + AssertEquals(Ord(as_ForSale), Ord(LData.account_state)); + AssertTrue(TAccountComp.IsNullAccountKey(LData.new_public_key)); + AssertEquals(0, Integer(LData.locked_until_block)); + AssertEquals(102, Integer(LData.account_to_pay)); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.PrivateSaleUsesBuyerKeyAndFutureLock; +var LData: TOpListAccountData; +begin + SetOperation(omtEnlistAccountForSale); + FRequest.SaleMode := akaPrivateSale; + FRequest.LockedUntilBlock := 1050; + Prepare; + LData := TOpListAccount(FTree.GetOperation(0)).Data; + AssertTrue(TAccountComp.EqualAccountKeys(FRecipient.PublicKey, LData.new_public_key)); + AssertEquals(1050, Integer(LData.locked_until_block)); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.PrivateSaleRejectsExpiredLock; +begin + SetOperation(omtEnlistAccountForSale); + FRequest.SaleMode := akaPrivateSale; + FRequest.LockedUntilBlock := 1000; + Reject; +end; + +procedure TOperationPreparationTests.DelistSignsSaleAccount; +begin + SetOperation(omtDelistAccountFromSale); + Prepare; + AssertTrue(FTree.GetOperation(0) is TOpDelistAccountForSale); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.ChangeInfoEncodesPayloadWithoutChangingAccountData; +var LData: TOpChangeAccountInfoData; +begin + SetOperation(omtChangeInfo); + FRequest.NewName := TEncoding.ASCII.GetBytes('test-account'); + FRequest.PayloadContent := 'change-info payload'; + Prepare; + LData := TOpChangeAccountInfo(FTree.GetOperation(0)).Data; + AssertTrue(TBaseType.Equals(FRequest.NewName, LData.new_name)); + AssertTrue(TBaseType.Equals(TEncoding.ASCII.GetBytes(FRequest.PayloadContent), LData.payload.payload_raw)); + AssertEquals(0, Integer(LData.payload.payload_type)); + AssertFalse('The wizard does not change account data', account_data in LData.changes_type); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.ChangeInfoRejectsMissingSigner; +begin + SetOperation(omtChangeInfo); + FRequest.SignerAccounts := nil; + Reject; +end; + +procedure TOperationPreparationTests.ChangeInfoRejectsNoChanges; +begin + SetOperation(omtChangeInfo); + FRequest.NewType := 0; + Reject; +end; + +procedure TOperationPreparationTests.BuySignsWithFundingKeyAndSupportsPrefundedTarget; +var LData: TOpTransactionData; +begin + SetOperation(omtBuyAccount); + FRequest.DestinationAccount.balance := 30000; + Prepare; + LData := TOpTransaction(FTree.GetOperation(0)).Data; + AssertEquals(Ord(buy_account), Ord(LData.opTransactionStyle)); + AssertEquals(Int64(25000), Int64(LData.amount)); + AssertEquals(Int64(50000), Int64(LData.AccountPrice)); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.BuyRejectsInsufficientPriceFunding; +begin + SetOperation(omtBuyAccount); + Reject; +end; + +procedure TOperationPreparationTests.BuyRejectsWrongPrivateSaleRecipient; +begin + SetOperation(omtBuyAccount); + FRequest.DestinationAccount.balance := 30000; + FRequest.DestinationAccount.accountInfo.new_publicKey := FOwner.PublicKey; + FRequest.DestinationAccount.accountInfo.locked_until_block := 1050; + Reject; +end; + +procedure TOperationPreparationTests.BuyRejectsExpiredPrivateSale; +begin + SetOperation(omtBuyAccount); + FRequest.DestinationAccount.balance := 30000; + FRequest.DestinationAccount.accountInfo.new_publicKey := FRecipient.PublicKey; + FRequest.DestinationAccount.accountInfo.locked_until_block := 999; + Reject; +end; + +procedure TOperationPreparationTests.AllSixPreparedOperationsPassCoreValidation; +const + LTypes: array[0..5] of TExecuteOperationType = (omtSendPasc, omtChangeKey, + omtEnlistAccountForSale, omtDelistAccountFromSale, omtChangeInfo, omtBuyAccount); +var + LType: TExecuteOperationType; + + procedure CheckOperation(AType: TExecuteOperationType); + var + LDisposables: TDisposables; + LSafeBox: TOperationSafeBox; + LTransaction: TPCSafeBoxTransaction; + LPrevious: TAccountPreviousBlockInfo; + LAccount: TAccount; + LError: string; + i: Integer; + begin + FRequest.Accounts := TArray<TAccount>.Create(Account(100, 100000, FOwner)); + FRequest.SignerAccounts := Copy(FRequest.Accounts); + FRequest.DestinationAccount := Account(101, 30000, FRecipient); + SetOperation(AType); + Prepare; + LSafeBox := LDisposables.AddObject(TOperationSafeBox.Create) as TOperationSafeBox; + LSafeBox.Build(FOwner.PublicKey); + LSafeBox.Seed(FRequest.Accounts[0]); + LSafeBox.Seed(FRequest.DestinationAccount); + LSafeBox.Seed(FRequest.SellerAccount); + LTransaction := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + LPrevious := LDisposables.AddObject(TAccountPreviousBlockInfo.Create) as TAccountPreviousBlockInfo; + LError := ''; + for i := 0 to FTree.OperationsCount - 1 do + AssertTrue('Core rejected ' + FTree.GetOperation(i).ClassName + ': ' + LError, + FTree.GetOperation(i).DoOperation(LPrevious, LTransaction, LError)); + LAccount := LTransaction.Account(100); + AssertEquals(8, Integer(LAccount.n_operation)); + AssertEquals(Int64(100), LTransaction.TotalFee); + if AType = omtSendPasc then + AssertEquals(Int64(55000), Int64(LTransaction.Account(101).balance)); + if AType = omtEnlistAccountForSale then + AssertTrue(TAccountComp.IsAccountForPublicSale(LAccount.accountInfo)); + if AType = omtDelistAccountFromSale then + AssertEquals(Ord(as_Normal), Ord(LAccount.accountInfo.state)); + if AType = omtChangeKey then + AssertTrue(TAccountComp.EqualAccountKeys(FRecipient.PublicKey, LAccount.accountInfo.accountKey)); + if AType = omtChangeInfo then + AssertEquals(1, LAccount.account_type); + if AType = omtBuyAccount then + begin + AssertTrue(TAccountComp.EqualAccountKeys(FRecipient.PublicKey, LTransaction.Account(101).accountInfo.accountKey)); + AssertEquals(Int64(50000), Int64(LTransaction.Account(102).balance)); + AssertEquals(Int64(5000), Int64(LTransaction.Account(101).balance)); + end; + + end; + +begin + for LType in LTypes do + CheckOperation(LType); +end; + +procedure TOperationPreparationTests.EmptyRawBytesSerializationRetainsLengthPrefix; +var + LDisposables: TDisposables; + LRaw, LEncoded: TRawBytes; + LStream: TMemoryStream; + LPosition: Integer; +begin + LRaw := nil; + LEncoded := LRaw.ToSerialized; + AssertEquals(2, Length(LEncoded)); + AssertEquals(0, Integer(LEncoded[0])); + AssertEquals(0, Integer(LEncoded[1])); + LPosition := 0; + AssertTrue(LRaw.LoadFromTBytes(LEncoded, LPosition)); + AssertEquals(2, LPosition); + AssertEquals(0, Length(LRaw)); + LEncoded := nil; + LPosition := 0; + LRaw.SaveInsideTBytes(LEncoded, LPosition); + AssertEquals(2, Length(LEncoded)); + AssertEquals(2, LPosition); + LStream := LDisposables.AddObject(TMemoryStream.Create) as TMemoryStream; + LRaw.ToSerialized(LStream); + AssertEquals(Int64(2), LStream.Size); + LRaw.FromStream(LStream, 2, 0); + AssertEquals(0, Length(LRaw)); + LStream.Position := 0; + SetLength(LEncoded, 2); + LStream.ReadBuffer(LEncoded[0], Length(LEncoded)); + AssertEquals(0, Integer(LEncoded[0])); + AssertEquals(0, Integer(LEncoded[1])); + LRaw := TEncoding.ASCII.GetBytes('abc'); + LEncoded := LRaw.ToSerialized; + AssertEquals(5, Length(LEncoded)); + AssertEquals(3, Integer(LEncoded[0])); + AssertEquals(Ord('a'), Integer(LEncoded[2])); + +end; + +procedure TOperationPreparationTests.EmptyAccountSealConvertsToZeroPaddedBytes; +var + LRaw: TRawBytes; + L20: T20Bytes; + L32: T32Bytes; + i: Integer; +begin + LRaw := nil; + L20 := TBaseType.To20Bytes(LRaw); + L32 := TBaseType.To32Bytes(LRaw); + for i := Low(L20) to High(L20) do + AssertEquals(0, Integer(L20[i])); + for i := Low(L32) to High(L32) do + AssertEquals(0, Integer(L32[i])); + LRaw := TEncoding.ASCII.GetBytes('x'); + L20 := TBaseType.To20Bytes(LRaw); + AssertEquals(Ord('x'), Integer(L20[0])); + AssertEquals(0, Integer(L20[1])); +end; + +procedure TOperationPreparationTests.AbstractMemNullAndPopulatedKeysRoundTrip; +var + LDisposables: TDisposables; + LMemory: TMem; + LWritten, LRead: TAbstractMemAccountKeyNode; + LZone: TAMZone; + i: Integer; +begin + LMemory := LDisposables.AddObject(TMem.Create(0, False)) as TMem; + for i := 0 to 1 do + begin + LWritten.Clear; + if i = 1 then + LWritten.accountKey := FOwner.PublicKey; + LZone := LMemory.New(LWritten.GetSize(LMemory)); + LWritten.myPosition := LZone.position; + LWritten.WriteToMem(LMemory); + LRead.ReadFromMem(LWritten.myPosition, LMemory); + AssertTrue(TAccountComp.EqualAccountKeys(LWritten.accountKey, LRead.accountKey)); + AssertEquals(Int64(LWritten.myPosition), Int64(LRead.myPosition)); + end; +end; + +procedure TOperationPreparationTests.PayloadEncryptionModesRoundTrip; +var + LMode: TPayloadEncryptionMode; + LDecrypted: TRawBytes; +begin + FRequest.PayloadContent := 'private operation payload'; + FRequest.PayloadPassword := 'ephemeral test password'; + for LMode := pemEncryptWithSender to pemEncryptWithPassword do + begin + FRequest.PayloadMode := LMode; + Prepare; + case LMode of + pemEncryptWithSender: AssertTrue(TPCEncryption.DoPascalCoinECIESDecrypt(FOwner.PrivateKey, FTree.GetOperation(0).OperationPayload.payload_raw, LDecrypted)); + pemEncryptWithRecipient: AssertTrue(TPCEncryption.DoPascalCoinECIESDecrypt(FRecipient.PrivateKey, FTree.GetOperation(0).OperationPayload.payload_raw, LDecrypted)); + pemEncryptWithPassword: + AssertTrue(TPCEncryption.DoPascalCoinAESDecrypt(FTree.GetOperation(0).OperationPayload.payload_raw, + TEncoding.ANSI.GetBytes(FRequest.PayloadPassword), LDecrypted)); + end; + AssertTrue(TBaseType.Equals(TEncoding.ANSI.GetBytes(FRequest.PayloadContent), LDecrypted)); + AssertSigned(FTree.GetOperation(0)); + end; +end; + +procedure TOperationPreparationTests.PurchaseRecipientPayloadUsesNewOwnerKey; +var + LDecrypted: TRawBytes; +begin + SetOperation(omtBuyAccount); + FRequest.DestinationAccount.balance := 30000; + FRequest.DestinationAccount.accountInfo.accountKey := FOwner.PublicKey; + FRequest.PayloadMode := pemEncryptWithRecipient; + FRequest.PayloadContent := 'new owner private payload'; + Prepare; + AssertTrue(TPCEncryption.DoPascalCoinECIESDecrypt(FRecipient.PrivateKey, + FTree.GetOperation(0).OperationPayload.payload_raw, LDecrypted)); + AssertTrue(TBaseType.Equals(TEncoding.ANSI.GetBytes(FRequest.PayloadContent), LDecrypted)); + AssertFalse('The former owner must not decrypt the new owner payload', + TPCEncryption.DoPascalCoinECIESDecrypt(FOwner.PrivateKey, FTree.GetOperation(0).OperationPayload.payload_raw, LDecrypted)); + AssertSigned(FTree.GetOperation(0)); +end; + +procedure TOperationPreparationTests.CorePreflightLeavesPendingSnapshotUnchanged; +var + LDisposables: TDisposables; + LSafeBox: TOperationSafeBox; + LSnapshot: TPCSafeBoxTransaction; +begin + Prepare; + LSafeBox := LDisposables.AddObject(TOperationSafeBox.Create) as TOperationSafeBox; + LSafeBox.Build(FOwner.PublicKey); + LSafeBox.Seed(FRequest.Accounts[0]); + LSafeBox.Seed(FRequest.DestinationAccount); + LSnapshot := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + AssertTrue(TWIZOperationsHelper.ValidatePreparedOperations(FTree, LSnapshot, FError)); + AssertEquals(7, Integer(LSnapshot.Account(100).n_operation)); + AssertEquals(Int64(100000), Int64(LSnapshot.Account(100).balance)); + AssertEquals(Int64(0), LSnapshot.TotalFee); + AssertFalse(TWIZOperationsHelper.ValidatePreparedOperations(nil, LSnapshot, FError)); +end; + +procedure TOperationPreparationTests.AdvancedSwapsClaimsAndAccountDataPassCoreValidation; + + procedure CheckOperation(AKind: Integer); + var + LDisposables: TDisposables; + LSafeBox: TOperationSafeBox; + LSnapshot: TPCSafeBoxTransaction; + LPrevious: TAccountPreviousBlockInfo; + LOperation: TPCOperation; + LPayload: TOperationPayload; + LSecret, LData: TRawBytes; + LHashLock: T32Bytes; + LState: TAccountState; + LNewKey: TAccountKey; + LPrice: UInt64; + LPayee: Cardinal; + LError: String; + begin + LSafeBox := LDisposables.AddObject(TOperationSafeBox.Create) as TOperationSafeBox; + LSafeBox.Build(FOwner.PublicKey); + LSafeBox.Seed(Account(100, 100000, FOwner)); + LSafeBox.Seed(Account(102, 0, FRecipient)); + LSafeBox.Seed(Account(103, 1000, FOwner)); + LSnapshot := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + LPrevious := LDisposables.AddObject(TAccountPreviousBlockInfo.Create) as TAccountPreviousBlockInfo; + LPayload := CT_TOperationPayload_NUL; + if AKind = 2 then + begin + LData := TEncoding.ASCII.GetBytes('account data'); + LOperation := LDisposables.AddObject(TOpChangeAccountInfo.CreateChangeAccountInfo(CT_PROTOCOL_6, + 100, 8, 100, FOwner, False, CT_TECDSA_Public_Nul, False, nil, + False, 0, True, LData, 100, LPayload)) as TPCOperation; + AssertTrue(LOperation.DoOperation(LPrevious, LSnapshot, LError)); + AssertTrue(TBaseType.Equals(LData, LSnapshot.Account(100).account_data)); + Exit; + end; + LSecret := TEncoding.ASCII.GetBytes('ephemeral atomic swap secret'); + LHashLock := TAccountComp.CalculateHashLock(LSecret); + if AKind = 0 then + begin + LState := as_ForAtomicAccountSwap; + LNewKey := FRecipient.PublicKey; + LPrice := 0; + LPayee := 100; + end + else + begin + LState := as_ForAtomicCoinSwap; + LNewKey := CT_TECDSA_Public_Nul; + LPrice := 50000; + LPayee := 102; + end; + LOperation := LDisposables.AddObject(TOpListAccountForSaleOrSwap.CreateListAccountForSaleOrSwap( + CT_PROTOCOL_6, LState, 100, 8, 100, LPrice, 100, LPayee, LNewKey, + 200, FOwner, LHashLock, LPayload)) as TPCOperation; + AssertTrue(LOperation.DoOperation(LPrevious, LSnapshot, LError)); + AssertEquals(Ord(LState), Ord(LSnapshot.Account(100).accountInfo.state)); + LPayload.payload_raw := TEncoding.ASCII.GetBytes('wrong secret'); + if AKind = 0 then + LOperation := LDisposables.AddObject(TOpBuyAccount.CreateBuy(CT_PROTOCOL_6, + 100, 9, 100, 100, 0, 0, 100, FRecipient.PublicKey, FRecipient, LPayload)) as TPCOperation + else + LOperation := LDisposables.AddObject(TOpTransaction.CreateTransaction(CT_PROTOCOL_6, + 103, 8, 100, FOwner, 0, 100, LPayload)) as TPCOperation; + AssertFalse('A wrong swap secret must not claim the account/funds', LOperation.DoOperation(LPrevious, LSnapshot, LError)); + LPayload.payload_raw := LSecret; + if AKind = 0 then + LOperation := LDisposables.AddObject(TOpBuyAccount.CreateBuy(CT_PROTOCOL_6, + 100, 9, 100, 100, 0, 0, 100, FRecipient.PublicKey, FRecipient, LPayload)) as TPCOperation + else + LOperation := LDisposables.AddObject(TOpTransaction.CreateTransaction(CT_PROTOCOL_6, + 103, 8, 100, FOwner, 0, 100, LPayload)) as TPCOperation; + AssertTrue(LOperation.DoOperation(LPrevious, LSnapshot, LError)); + AssertEquals(Ord(as_Normal), Ord(LSnapshot.Account(100).accountInfo.state)); + if AKind = 0 then + AssertTrue(TAccountComp.EqualAccountKeys(FRecipient.PublicKey, LSnapshot.Account(100).accountInfo.accountKey)) + else + AssertEquals(Int64(50000), Int64(LSnapshot.Account(102).balance)); + end; + +begin + CheckOperation(0); + CheckOperation(1); + CheckOperation(2); +end; + +initialization + RegisterTest(TOperationPreparationTests); + +end. diff --git a/src/tests/experimental/operations/UOperationWizardTests.pas b/src/tests/experimental/operations/UOperationWizardTests.pas new file mode 100644 index 000000000..19108b059 --- /dev/null +++ b/src/tests/experimental/operations/UOperationWizardTests.pas @@ -0,0 +1,392 @@ +unit UOperationWizardTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + Operation wizard tests use generated, memory-only wallet keys. They never + load the global wallet, initialize a node, or submit operations. } + +{$mode delphi} +{$H+} + +interface + +uses + Classes, SysUtils, fpcunit, testregistry, UCrypto, UWallet, UPCDataTypes, + UWIZOperation, UCoreObjects; + +type + TOperationWizardTests = class(TTestCase) + private + FKeys: TWalletKeys; + FKey: TECPrivateKey; + FOtherKey: TECPrivateKey; + FUnlockCalls: Integer; + procedure Unlock(Sender: TObject); + procedure CancelUnlock(Sender: TObject); + procedure WrongPassword(Sender: TObject); + procedure UnlockThenRaise(Sender: TObject); + procedure ProtectWallet; + function Account(ANumber: Cardinal; ABalance: UInt64; const AKey: TAccountKey): TAccount; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure PasswordlessWalletDoesNotAcquireUnlock; + procedure CanceledUnlockStaysLocked; + procedure WrongPasswordStaysLocked; + procedure AcquiredUnlockRelocksOnce; + procedure ExistingUnlockedWalletKeepsItsState; + procedure SeparateModelsKeepIndependentOwnership; + procedure ModelDisposalRelocksOwnedUnlock; + procedure WalletDisposalInvalidatesBorrowedReference; + procedure CallbackFailureRetainsUnlockOwnership; + procedure CommonSignerMustOwnEveryTarget; + procedure CommonSignerMustCoverCumulativeFees; + procedure LockedSignerIsExcluded; + procedure SecondaryModeOnlyFundsPoorTargets; + procedure SecondaryModeIncludesPayersOwnFee; + procedure NegativeFeeHasNoSignerCandidates; + procedure FeeAboveProtocolLimitHasNoSignerCandidates; + procedure LockedTargetCannotUseSecondaryMode; + procedure ImmatureSignerIsExcluded; + procedure ImmatureTargetCannotUseSecondaryMode; + end; + +implementation + +uses UConst; + +procedure TOperationWizardTests.SetUp; +begin + inherited SetUp; + FKey := nil; + FOtherKey := nil; + FKeys := nil; + try + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FOtherKey := TECPrivateKey.Create; + FOtherKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FKeys := TWalletKeys.Create(nil); + FKeys.AddPrivateKey('Generated test key', FKey); + // Establish password validity through the same decryption path as loading + // a wallet stream. An empty password remains valid and does not lock it. + FKeys.LockWallet; + FUnlockCalls := 0; + except + FreeAndNil(FKeys); + FreeAndNil(FOtherKey); + FreeAndNil(FKey); + raise; + end; +end; + +procedure TOperationWizardTests.TearDown; +begin + FKeys.Free; + FOtherKey.Free; + FKey.Free; + inherited TearDown; +end; + +procedure TOperationWizardTests.ProtectWallet; +begin + FKeys.WalletPassword := 'operation-test-password'; + AssertTrue('Generated wallet locks', FKeys.LockWallet); +end; + +procedure TOperationWizardTests.Unlock(Sender: TObject); +begin + Inc(FUnlockCalls); + FKeys.WalletPassword := 'operation-test-password'; +end; + +procedure TOperationWizardTests.CancelUnlock(Sender: TObject); +begin + Inc(FUnlockCalls); +end; + +procedure TOperationWizardTests.WrongPassword(Sender: TObject); +begin + Inc(FUnlockCalls); + FKeys.WalletPassword := 'incorrect-password'; +end; + +procedure TOperationWizardTests.UnlockThenRaise(Sender: TObject); +begin + Unlock(Sender); + raise Exception.Create('Simulated dialog failure after unlocking'); +end; + +function TOperationWizardTests.Account(ANumber: Cardinal; ABalance: UInt64; const AKey: TAccountKey): TAccount; +begin + Result := CT_Account_NUL; + Result.account := ANumber; + Result.balance := ABalance; + Result.accountInfo.state := as_Normal; + Result.accountInfo.accountKey := AKey; +end; + +procedure TOperationWizardTests.PasswordlessWalletDoesNotAcquireUnlock; +var LScope: TWalletUnlockScope; LMessage: String; +begin + LScope := TWalletUnlockScope.Create(nil); + try + AssertTrue(LScope.TryAcquire(FKeys, Unlock, LMessage)); + AssertFalse(LScope.OwnsUnlock); + LScope.Release; + AssertTrue(FKeys.IsValidPassword); + AssertEquals(0, FUnlockCalls); + AssertTrue(Assigned(FKeys.Key[0].PrivateKey)); + finally + LScope.Free; + end; +end; + +procedure TOperationWizardTests.CanceledUnlockStaysLocked; +var LScope: TWalletUnlockScope; LMessage: String; +begin + ProtectWallet; + LScope := TWalletUnlockScope.Create(nil); + try + AssertFalse(LScope.TryAcquire(FKeys, CancelUnlock, LMessage)); + AssertTrue('Rejection explains the locked wallet', LMessage <> ''); + AssertFalse(LScope.OwnsUnlock); + AssertFalse(FKeys.IsValidPassword); + AssertEquals(1, FUnlockCalls); + finally + LScope.Free; + end; +end; + +procedure TOperationWizardTests.WrongPasswordStaysLocked; +var LScope: TWalletUnlockScope; LMessage: String; +begin + ProtectWallet; + LScope := TWalletUnlockScope.Create(nil); + try + AssertFalse(LScope.TryAcquire(FKeys, WrongPassword, LMessage)); + AssertFalse(LScope.OwnsUnlock); + AssertFalse(Assigned(FKeys.Key[0].PrivateKey)); + finally + LScope.Free; + end; +end; + +procedure TOperationWizardTests.AcquiredUnlockRelocksOnce; +var LScope: TWalletUnlockScope; LMessage: String; +begin + ProtectWallet; + LScope := TWalletUnlockScope.Create(nil); + try + AssertTrue(LScope.TryAcquire(FKeys, Unlock, LMessage)); + AssertTrue(LScope.OwnsUnlock); + AssertTrue(Assigned(FKeys.Key[0].PrivateKey)); + AssertTrue('Retry does not reprompt', LScope.TryAcquire(FKeys, Unlock, LMessage)); + AssertEquals(1, FUnlockCalls); + LScope.Release; + AssertFalse(FKeys.IsValidPassword); + FKeys.WalletPassword := 'operation-test-password'; + LScope.Release; + AssertTrue('Repeated release does not lock another unlock', FKeys.IsValidPassword); + finally + LScope.Free; + end; +end; + +procedure TOperationWizardTests.ExistingUnlockedWalletKeepsItsState; +var LModel: TWIZOperationsModel; LMessage: String; +begin + ProtectWallet; + Unlock(nil); + LModel := TWIZOperationsModel.Create(nil, omtSendPasc); + try + AssertTrue(LModel.TryUnlockWallet(FKeys, CancelUnlock, LMessage)); + AssertFalse(LModel.UnlockScope.OwnsUnlock); + finally + LModel.Free; + end; + AssertTrue(FKeys.IsValidPassword); + AssertEquals(1, FUnlockCalls); +end; + +procedure TOperationWizardTests.SeparateModelsKeepIndependentOwnership; +var LFirst, LSecond: TWIZOperationsModel; LMessage: String; +begin + ProtectWallet; + LFirst := TWIZOperationsModel.Create(nil, omtSendPasc); + try + AssertTrue(LFirst.TryUnlockWallet(FKeys, Unlock, LMessage)); + LSecond := TWIZOperationsModel.Create(nil, omtChangeKey); + try + AssertTrue(LSecond.TryUnlockWallet(FKeys, CancelUnlock, LMessage)); + AssertFalse(LSecond.UnlockScope.OwnsUnlock); + AssertTrue('Creating another wizard preserves ownership', LFirst.UnlockScope.OwnsUnlock); + finally + LSecond.Free; + end; + AssertTrue(FKeys.IsValidPassword); + LFirst.RelockWallet; + AssertFalse(FKeys.IsValidPassword); + finally + LFirst.Free; + end; +end; + +procedure TOperationWizardTests.ModelDisposalRelocksOwnedUnlock; +var LModel: TWIZOperationsModel; LMessage: String; +begin + ProtectWallet; + LModel := TWIZOperationsModel.Create(nil, omtSendPasc); + try + AssertTrue(LModel.TryUnlockWallet(FKeys, Unlock, LMessage)); + finally + LModel.Free; + end; + AssertFalse(FKeys.IsValidPassword); + AssertFalse(Assigned(FKeys.Key[0].PrivateKey)); +end; + +procedure TOperationWizardTests.WalletDisposalInvalidatesBorrowedReference; +var LScope: TWalletUnlockScope; LMessage: String; +begin + ProtectWallet; + LScope := TWalletUnlockScope.Create(nil); + try + AssertTrue(LScope.TryAcquire(FKeys, Unlock, LMessage)); + FreeAndNil(FKeys); + AssertFalse(LScope.OwnsUnlock); + LScope.Release; + finally + LScope.Free; + end; +end; + +procedure TOperationWizardTests.CallbackFailureRetainsUnlockOwnership; +var LScope: TWalletUnlockScope; LMessage: String; LRaised: Boolean; +begin + ProtectWallet; + LScope := TWalletUnlockScope.Create(nil); + try + LRaised := False; + try + LScope.TryAcquire(FKeys, UnlockThenRaise, LMessage); + except + on E: Exception do LRaised := True; + end; + AssertTrue(LRaised); + AssertTrue(LScope.OwnsUnlock); + finally + LScope.Free; + end; + AssertFalse(FKeys.IsValidPassword); +end; + +procedure TOperationWizardTests.CommonSignerMustOwnEveryTarget; +var LCandidates, LTargets, LResult: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 100, FKey.PublicKey), Account(2, 100, FOtherKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey)); + LResult := TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary); + AssertEquals(1, Length(LResult)); + AssertEquals(1, Integer(LResult[0].account)); + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey), Account(4, 0, FOtherKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary))); +end; + +procedure TOperationWizardTests.CommonSignerMustCoverCumulativeFees; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 19, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey), Account(4, 0, FKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary))); + LCandidates[0].balance := 20; + AssertEquals(1, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary))); +end; + +procedure TOperationWizardTests.LockedSignerIsExcluded; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 100, FKey.PublicKey)); + LCandidates[0].accountInfo.state := as_ForSale; + LCandidates[0].accountInfo.locked_until_block := 1000; + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary))); + AssertEquals(1, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1001, 10, akaPrimary))); +end; + +procedure TOperationWizardTests.SecondaryModeOnlyFundsPoorTargets; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 10, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey), Account(4, 10, FOtherKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary))); + AssertEquals(1, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaSecondary))); +end; + +procedure TOperationWizardTests.SecondaryModeIncludesPayersOwnFee; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 10, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(LCandidates[0], Account(4, 0, FKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaSecondary))); + LCandidates[0].balance := 20; + LTargets[0] := LCandidates[0]; + AssertEquals(1, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaSecondary))); +end; + +procedure TOperationWizardTests.NegativeFeeHasNoSignerCandidates; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 100, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, -1, akaPrimary))); +end; + +procedure TOperationWizardTests.FeeAboveProtocolLimitHasNoSignerCandidates; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, UInt64(CT_MaxTransactionFee) + 1, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, CT_MaxTransactionFee + 1, akaPrimary))); +end; + +procedure TOperationWizardTests.LockedTargetCannotUseSecondaryMode; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 100, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(3, 10, FKey.PublicKey)); + LTargets[0].accountInfo.state := as_ForSale; + LTargets[0].accountInfo.locked_until_block := 1000; + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaSecondary))); + AssertEquals(1, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1001, 10, akaSecondary))); +end; + +procedure TOperationWizardTests.ImmatureSignerIsExcluded; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(5000, 100, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(3, 0, FKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary))); + LCandidates[0].account := 1; + AssertEquals(1, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaPrimary))); +end; + +procedure TOperationWizardTests.ImmatureTargetCannotUseSecondaryMode; +var LCandidates, LTargets: TArray<TAccount>; +begin + LCandidates := TArray<TAccount>.Create(Account(1, 100, FKey.PublicKey)); + LTargets := TArray<TAccount>.Create(Account(5000, 10, FKey.PublicKey)); + AssertEquals(0, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaSecondary))); + LTargets[0].account := 3; + AssertEquals(1, Length(TWIZOperationsModel.FilterSignerCandidates(LCandidates, LTargets, 1000, 10, akaSecondary))); +end; + +initialization + RegisterTest(TOperationWizardTests); + +end. diff --git a/src/tests/experimental/operations/UWalletParityTests.pas b/src/tests/experimental/operations/UWalletParityTests.pas new file mode 100644 index 000000000..f2472d8ea --- /dev/null +++ b/src/tests/experimental/operations/UWalletParityTests.pas @@ -0,0 +1,624 @@ +unit UWalletParityTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + Keys and RAW packages are generated in memory. These tests never load the + global wallet/node, contact RPC or submit operations. Native forms run on the + regression runner's private Windows desktop. } + +{$mode delphi} + +interface + +uses + Classes, SysUtils, Forms, fpcunit, testregistry, UCrypto, UWallet, + UPCDataTypes, UBlockChain, UTxMultiOperation, UWizard, UWIZAddKey, UFRMOperationsExplorer; + +type + TParityKeyWizard = class(TWIZAddKeyWizard) + public + Run: TNotifyEvent; + protected + procedure ShowHost; override; + end; + + TWalletParityTests = class(TTestCase) + private + FKey: TECPrivateKey; + procedure PublicKeyNavigation(Sender: TObject); + procedure PrivateKeyNavigation(Sender: TObject); + procedure CurveNavigation(Sender: TObject); + procedure RejectPackagePresentation(Sender: TObject); + procedure RunKeyWizard(ACallback: TNotifyEvent); + function RawTransaction(AAmount: UInt64): String; + function RawOperation(AOperation: TPCOperation): String; + function Multioperation(AReceived: UInt64 = 1900): TOpMultiOperation; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure UnencryptedExportsRoundTripForEveryCurve; + procedure ProtectedAndLegacyEmptyPasswordExportsRoundTrip; + procedure BareSecretImportRemainsCompatible; + procedure PrivateKeyRejectsWrongPasswordAndMalformedInput; + procedure PublicKeyAcceptsExportedAndRawFormats; + procedure PublicKeyRejectsMalformedInput; + procedure PublicKeyWizardRetainsInputAndNameOnBack; + procedure PrivateKeyWizardRetainsInputPasswordAndNameOnBack; + procedure KeyWizardRetainsCurveOnBack; + procedure AdvancedFormContainsCurrentClassicControlsWithoutANode; + procedure OfflineWorkbenchBlocksExecutionAndRpcHandlers; + procedure RawWorkbenchImportsExportsAndDeduplicatesPackages; + procedure RawWorkbenchRejectsConflictsWithoutChangingItsPackage; + procedure MemoEditorSupportsEditableRawInput; + procedure MarketFiltersIncludeAtomicSwapsAndIncomingKeys; + procedure OfflinePartialSignaturesMergeAndPassCoreValidation; + procedure MergedSignatureEncodingStillRequiresCoreVerification; + procedure RawImportPresentationFailureIsSeparateFromCommit; + procedure LateRawConflictDoesNotLeakStagedSignatures; + end; + +implementation + +uses + UConst, UBaseTypes, UAccounts, UOpTransaction, UPCEncryption, + UMemory, UNode, UFRMOperation, UFRMMemoText, UAccountSelectorData, + Generics.Collections, UOperationPreparationTests, UWIZAddKey_Start, + UWIZAddKey_GenerateOrImport, UWIZAddKey_SelectEncryption, + UWIZAddKey_ImportPubKey, UWIZAddKey_ImportPrivKey, UWIZAddKey_EnterName; + +type + TParitySelectorSource = class(TAccountSelectorDataSource) + protected + function ReadChain(ASafeBox: TPCSafeBox): TAccountSelectorChain; override; + procedure ReadAccounts(const AChain: TAccountSelectorChain; + const ANumbers: TArray<Cardinal>; out ARows: TArray<TAccount>); override; + public + Account: TAccount; + end; + +function TParitySelectorSource.ReadChain(ASafeBox: TPCSafeBox): TAccountSelectorChain; +begin + Result := Default(TAccountSelectorChain); + Result.Accounts := 1; +end; + +procedure TParitySelectorSource.ReadAccounts(const AChain: TAccountSelectorChain; + const ANumbers: TArray<Cardinal>; out ARows: TArray<TAccount>); +var + I: Integer; +begin + SetLength(ARows, Length(ANumbers)); + for I := 0 to High(ARows) do + ARows[I] := Account; +end; + +procedure TParityKeyWizard.ShowHost; +var + LMessage: String; +begin + Host.Show; + if Assigned(Run) then Run(Self); + Cancel(LMessage); +end; + +procedure TWalletParityTests.SetUp; +begin + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); +end; + +procedure TWalletParityTests.TearDown; +begin + FKey.Free; +end; + +procedure TWalletParityTests.UnencryptedExportsRoundTripForEveryCurve; +const + LCurves: array[0..3] of Word = (CT_NID_secp256k1, CT_NID_secp384r1, + CT_NID_sect283k1, CT_NID_secp521r1); +var + LDisposables: TDisposables; + LKey, LImported: TECPrivateKey; + LWalletKey: TWalletKey; + LText, LError: String; + LCurve: Word; +begin + for LCurve in LCurves do + begin + LKey := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LKey.GenerateRandomPrivateKey(LCurve); + LWalletKey := CT_TWalletKey_NUL; + LWalletKey.PrivateKey := LKey; + LText := TWallet.ExportPrivateKey(LWalletKey, ''); + AssertEquals(LKey.ExportToRaw.ToHexaString, LText); + AssertTrue(TWallet.TryDecryptPrivateKey(' ' + LText + LineEnding, '', LImported, LError)); + LDisposables.AddObject(LImported); + AssertTrue(TAccountComp.EqualAccountKeys(LKey.PublicKey, LImported.PublicKey)); + end; +end; + +procedure TWalletParityTests.ProtectedAndLegacyEmptyPasswordExportsRoundTrip; +var + LDisposables: TDisposables; + LWalletKey: TWalletKey; + LImported: TECPrivateKey; + LText, LError: String; +begin + LWalletKey := CT_TWalletKey_NUL; + LWalletKey.PrivateKey := FKey; + LText := TWallet.ExportPrivateKey(LWalletKey, 'export password'); + AssertTrue(TWallet.TryDecryptPrivateKey(LText, 'export password', LImported, LError)); + LDisposables.AddObject(LImported); + AssertTrue(TAccountComp.EqualAccountKeys(FKey.PublicKey, LImported.PublicKey)); + LText := TPCEncryption.DoPascalCoinAESEncrypt(FKey.ExportToRaw, nil).ToHexaString; + AssertTrue(TWallet.TryDecryptPrivateKey(LText, '', LImported, LError)); + LDisposables.AddObject(LImported); + AssertTrue(TAccountComp.EqualAccountKeys(FKey.PublicKey, LImported.PublicKey)); +end; + +procedure TWalletParityTests.BareSecretImportRemainsCompatible; +var + LDisposables: TDisposables; + LImported: TECPrivateKey; + LText, LError: String; +begin + LText := FKey.PrivateKeyAsRaw.ToHexaString; + while Length(LText) < 64 do LText := '0' + LText; + AssertTrue(TWallet.TryDecryptPrivateKey(LText, '', LImported, LError)); + LDisposables.AddObject(LImported); + AssertTrue(TAccountComp.EqualAccountKeys(FKey.PublicKey, LImported.PublicKey)); +end; + +procedure TWalletParityTests.PrivateKeyRejectsWrongPasswordAndMalformedInput; +var + LWalletKey: TWalletKey; + LImported: TECPrivateKey; + LText, LError: String; +begin + LWalletKey := CT_TWalletKey_NUL; + LWalletKey.PrivateKey := FKey; + LText := TWallet.ExportPrivateKey(LWalletKey, 'correct password'); + AssertFalse(TWallet.TryDecryptPrivateKey(LText, 'wrong password', LImported, LError)); + AssertNull(LImported); + AssertTrue(LError <> ''); + AssertFalse(TWallet.TryDecryptPrivateKey('not hexadecimal', '', LImported, LError)); + AssertNull(LImported); + LText := FKey.ExportToRaw.ToHexaString; + AssertFalse(TWallet.TryDecryptPrivateKey(Copy(LText, 1, Length(LText) - 2), '', LImported, LError)); + AssertNull(LImported); +end; + +procedure TWalletParityTests.PublicKeyAcceptsExportedAndRawFormats; +var + LImported: TAccountKey; + LError: String; +begin + AssertTrue(TWallet.TryParsePublicKey(TAccountComp.AccountPublicKeyExport(FKey.PublicKey), LImported, LError)); + AssertTrue(TAccountComp.EqualAccountKeys(FKey.PublicKey, LImported)); + AssertTrue(TWallet.TryParsePublicKey(TAccountComp.AccountKey2RawString(FKey.PublicKey).ToHexaString + LineEnding, LImported, LError)); + AssertTrue(TAccountComp.EqualAccountKeys(FKey.PublicKey, LImported)); +end; + +procedure TWalletParityTests.PublicKeyRejectsMalformedInput; +var + LImported: TAccountKey; + LError: String; +begin + AssertFalse(TWallet.TryParsePublicKey('', LImported, LError)); + AssertFalse(TWallet.TryParsePublicKey('not a key', LImported, LError)); + AssertFalse(TWallet.TryParsePublicKey('CA0200000000', LImported, LError)); + AssertTrue(TAccountComp.IsNullAccountKey(LImported)); +end; + +procedure TWalletParityTests.RunKeyWizard(ACallback: TNotifyEvent); +var + LDisposables: TDisposables; + LWizard: TParityKeyWizard; + LModel: TWIZAddKeyModel; +begin + LWizard := LDisposables.AddObject(TParityKeyWizard.Create(nil)) as TParityKeyWizard; + LModel := LDisposables.AddObject(TWIZAddKeyModel.Create(nil)) as TWIZAddKeyModel; + LWizard.Run := ACallback; + LWizard.Start(LModel); +end; + +procedure TWalletParityTests.PublicKeyNavigation(Sender: TObject); +var + LWizard: TParityKeyWizard; + LMessage, LText: String; +begin + LWizard := TParityKeyWizard(Sender); + TWIZAddKey_Start(LWizard.CurrentScreen).rbPublicKey.Checked := True; + AssertTrue(LWizard.TryNext(LMessage)); + LText := TAccountComp.AccountKey2RawString(FKey.PublicKey).ToHexaString; + TWIZAddKey_ImportPubKey(LWizard.CurrentScreen).txtPublicKey.Text := LText; + AssertTrue(LWizard.TryNext(LMessage)); + TWIZAddKey_EnterName(LWizard.CurrentScreen).txtName.Text := 'Retained name'; + AssertTrue(LWizard.TryPrevious(LMessage)); + AssertEquals(LText, Trim(TWIZAddKey_ImportPubKey(LWizard.CurrentScreen).txtPublicKey.Text)); + AssertTrue(LWizard.TryNext(LMessage)); + AssertEquals('Retained name', TWIZAddKey_EnterName(LWizard.CurrentScreen).txtName.Text); +end; + +procedure TWalletParityTests.PrivateKeyNavigation(Sender: TObject); +var + LWizard: TParityKeyWizard; + LMessage, LText: String; +begin + LWizard := TParityKeyWizard(Sender); + AssertTrue(LWizard.TryNext(LMessage)); + TWIZAddKey_GenerateOrImport(LWizard.CurrentScreen).rbPublicKey.Checked := True; + AssertTrue(LWizard.TryNext(LMessage)); + LText := FKey.ExportToRaw.ToHexaString; + TWIZAddKey_ImportPrivKey(LWizard.CurrentScreen).txtPrivateKey.Text := LText; + TWIZAddKey_ImportPrivKey(LWizard.CurrentScreen).txtPassword.Text := 'Retained password'; + AssertTrue(LWizard.TryNext(LMessage)); + TWIZAddKey_EnterName(LWizard.CurrentScreen).txtName.Text := 'Private name'; + AssertTrue(LWizard.TryPrevious(LMessage)); + AssertEquals(LText, Trim(TWIZAddKey_ImportPrivKey(LWizard.CurrentScreen).txtPrivateKey.Text)); + AssertEquals('Retained password', TWIZAddKey_ImportPrivKey(LWizard.CurrentScreen).txtPassword.Text); + AssertTrue(LWizard.TryNext(LMessage)); + AssertEquals('Private name', TWIZAddKey_EnterName(LWizard.CurrentScreen).txtName.Text); +end; + +procedure TWalletParityTests.CurveNavigation(Sender: TObject); +var + LWizard: TParityKeyWizard; + LCurve: TWIZAddKey_SelectEncryption; + LMessage: String; + LIndex: Integer; +begin + LWizard := TParityKeyWizard(Sender); + AssertTrue(LWizard.TryNext(LMessage)); + AssertTrue(LWizard.TryNext(LMessage)); + LCurve := TWIZAddKey_SelectEncryption(LWizard.CurrentScreen); + LIndex := LCurve.rgKeyType.Items.IndexOfObject(TObject(CT_NID_secp384r1)); + AssertTrue(LIndex >= 0); + LCurve.rgKeyType.ItemIndex := LIndex; + AssertTrue(LWizard.TryNext(LMessage)); + AssertTrue(LWizard.TryPrevious(LMessage)); + AssertEquals(LIndex, TWIZAddKey_SelectEncryption(LWizard.CurrentScreen).rgKeyType.ItemIndex); +end; + +procedure TWalletParityTests.PublicKeyWizardRetainsInputAndNameOnBack; +begin + RunKeyWizard(PublicKeyNavigation); +end; + +procedure TWalletParityTests.PrivateKeyWizardRetainsInputPasswordAndNameOnBack; +begin + RunKeyWizard(PrivateKeyNavigation); +end; + +procedure TWalletParityTests.KeyWizardRetainsCurveOnBack; +begin + RunKeyWizard(CurveNavigation); +end; + +procedure TWalletParityTests.AdvancedFormContainsCurrentClassicControlsWithoutANode; +var + LDisposables: TDisposables; + LForm: TFRMOperation; + LHadNode: Boolean; +begin + LHadNode := TNode.NodeExists; + LForm := LDisposables.AddObject(TFRMOperation.Create(nil, nil)) as TFRMOperation; + AssertNotNull(LForm.rbListAccountForAccountSwap); + AssertNotNull(LForm.rbListAccountForCoinSwap); + AssertNotNull(LForm.ebHashLock); + AssertNotNull(LForm.btnHashLock); + AssertNotNull(LForm.ebChangeAccountData); + AssertNotNull(LForm.memoEPASA); + AssertEquals(3, LForm.cbPayloadDataInputType.Items.Count); + AssertEquals(6, LForm.PageControlOpType.PageCount); + AssertEquals(LHadNode, TNode.NodeExists); +end; + +procedure TWalletParityTests.OfflineWorkbenchBlocksExecutionAndRpcHandlers; +var + LDisposables: TDisposables; + LForm: TFRMOperationsExplorer; +begin + LForm := LDisposables.AddObject(TFRMOperationsExplorer.Create(nil)) as TFRMOperationsExplorer; + AssertFalse(LForm.Offline); + LForm.Offline := True; + AssertFalse(LForm.MiRPCCalls.Enabled); + AssertFalse(LForm.ActExecuteOperation.Enabled); + AssertFalse(LForm.bbExecuteOperation.Enabled); + // These would otherwise enter RPC or fail on the intentionally absent node. + LForm.MiRPCCallsClick(nil); + LForm.ActExecuteOperationExecute(nil); + LForm.ActAddTxToMultioperationUpdate(LForm.ActAddTx); + LForm.AddOpChangeToMultioperationUpdate(LForm.ActAddOpChange); + AssertTrue(LForm.ActAddTx.Enabled); + AssertTrue(LForm.ActAddOpChange.Enabled); +end; + +function TWalletParityTests.RawTransaction(AAmount: UInt64): String; +var + LDisposables: TDisposables; + LOperation: TPCOperation; +begin + LOperation := LDisposables.AddObject(TOpTransaction.CreateTransaction(CT_PROTOCOL_6, + 100, 8, 101, FKey, AAmount, 100, CT_TOperationPayload_NUL)) as TPCOperation; + Result := RawOperation(LOperation); +end; + +function TWalletParityTests.RawOperation(AOperation: TPCOperation): String; +var + LDisposables: TDisposables; + LTree: TOperationsHashTree; + LStream: TMemoryStream; + LRaw: TRawBytes; +begin + LTree := LDisposables.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + AssertTrue(LTree.AddOperationToHashTree(AOperation)); + LStream := LDisposables.AddObject(TMemoryStream.Create) as TMemoryStream; + LTree.SaveOperationsHashTreeToStream(LStream, False); + LRaw.FromStream(LStream); + Result := LRaw.ToHexaString; +end; + +function TWalletParityTests.Multioperation(AReceived: UInt64): TOpMultiOperation; +var + LSenders: TMultiOpSenders; + LReceivers: TMultiOpReceivers; +begin + SetLength(LSenders, 2); + LSenders[0] := CT_TMultiOpSender_NUL; + LSenders[0].Account := 100; + LSenders[0].Amount := 1000; + LSenders[0].N_Operation := 8; + LSenders[1] := LSenders[0]; + LSenders[1].Account := 103; + SetLength(LReceivers, 1); + LReceivers[0] := CT_TMultiOpReceiver_NUL; + LReceivers[0].Account := 101; + LReceivers[0].Amount := AReceived; + Result := TOpMultiOperation.Create(CT_PROTOCOL_6); + AssertTrue(Result.AddTx(LSenders, LReceivers, False)); +end; + +procedure TWalletParityTests.RawWorkbenchImportsExportsAndDeduplicatesPackages; +var + LDisposables: TDisposables; + LForm: TFRMOperationsExplorer; + LRaw, LError: String; +begin + LForm := LDisposables.AddObject(TFRMOperationsExplorer.Create(nil)) as TFRMOperationsExplorer; + LForm.Offline := True; + LRaw := RawTransaction(1000); + if not LForm.TryImportRawOperations(LRaw, LError) then Fail('RAW import failed: ' + LError); + AssertEquals(1, LForm.Operations.OperationsCount); + AssertEquals('RAW export must retain its serialized bytes', LRaw, Trim(LForm.mOperationExport.Text)); + if not LForm.TryImportRawOperations(LRaw, LError) then Fail('Repeated RAW import failed: ' + LError); + AssertEquals(1, LForm.Operations.OperationsCount); +end; + +procedure TWalletParityTests.RawWorkbenchRejectsConflictsWithoutChangingItsPackage; +var + LDisposables: TDisposables; + LForm: TFRMOperationsExplorer; + LRaw, LError: String; +begin + LForm := LDisposables.AddObject(TFRMOperationsExplorer.Create(nil)) as TFRMOperationsExplorer; + LRaw := RawTransaction(1000); + if not LForm.TryImportRawOperations(LRaw, LError) then Fail('Initial RAW import failed: ' + LError); + AssertFalse(LForm.TryImportRawOperations(RawTransaction(2000), LError)); + AssertTrue(LError <> ''); + AssertEquals(1, LForm.Operations.OperationsCount); + AssertEquals(LRaw, Trim(LForm.mOperationExport.Text)); + AssertFalse(LForm.TryImportRawOperations('not hexadecimal', LError)); + AssertFalse(LForm.TryImportRawOperations(LRaw + 'FF', LError)); + AssertEquals(1, LForm.Operations.OperationsCount); +end; + +procedure TWalletParityTests.MemoEditorSupportsEditableRawInput; +var + LDisposables: TDisposables; + LForm: TFRMMemoText; +begin + LForm := LDisposables.AddObject(TFRMMemoText.Create(nil)) as TFRMMemoText; + LForm.InitData('RAW input', '1234'); + LForm.AllowInput := True; + AssertFalse(LForm.Memo.ReadOnly); + LForm.DataText := '5678'; + AssertEquals('5678', Trim(LForm.DataText)); + LForm.AllowInput := False; + AssertTrue(LForm.Memo.ReadOnly); +end; + +procedure TWalletParityTests.MarketFiltersIncludeAtomicSwapsAndIncomingKeys; +var + LDisposables: TDisposables; + LSource: TParitySelectorSource; + LSearch: TAccountSelectorFilter; + LRows: TList<TAccount>; + + procedure CheckMatch(AExpected: Boolean); + begin + // Exercise the provider actually used by the selector, rather than a + // second copy of the sale predicates retained only for this regression. + LRows.Clear; + LSource.Configure(nil, LSearch); + LSource.FetchAll(LRows); + AssertEquals(Ord(AExpected), LRows.Count); + end; + +begin + LSource := LDisposables.AddObject(TParitySelectorSource.Create(nil)) as TParitySelectorSource; + LRows := LDisposables.AddObject(TList<TAccount>.Create) as TList<TAccount>; + LSearch := Default(TAccountSelectorFilter); + LSearch.MaxBalance := -1; + LSearch.OnlyForSale := True; + LSource.Account := CT_Account_NUL; + LSource.Account.accountInfo.state := as_ForAtomicCoinSwap; + CheckMatch(True); + LSource.Account.accountInfo.state := as_ForAtomicAccountSwap; + CheckMatch(True); + LSearch.OnlyForPrivateSaleToMe := True; + LSearch.WalletKeys := TArray<TAccountKey>.Create(FKey.PublicKey); + LSource.Account.accountInfo.new_publicKey := FKey.PublicKey; + CheckMatch(True); + LSearch.OnlyForPublicSale := True; + CheckMatch(False); + LSearch.OnlyForPrivateSaleToMe := False; + LSource.Account.accountInfo.state := as_ForSale; + LSource.Account.accountInfo.new_publicKey := CT_TECDSA_Public_Nul; + CheckMatch(True); +end; + +procedure TWalletParityTests.OfflinePartialSignaturesMergeAndPassCoreValidation; +var + LDisposables: TDisposables; + LOtherKey: TECPrivateKey; + LFirst, LSecond, LMerged, LConflict: TOpMultiOperation; + LForm: TFRMOperationsExplorer; + LSafeBox: TOperationSafeBox; + LSnapshot: TPCSafeBoxTransaction; + LPrevious: TAccountPreviousBlockInfo; + LAccount: TAccount; + LRaw, LError: String; +begin + LOtherKey := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LOtherKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LFirst := LDisposables.AddObject(Multioperation) as TOpMultiOperation; + LSecond := LDisposables.AddObject(Multioperation) as TOpMultiOperation; + AssertEquals(1, LFirst.DoSignMultiOperationSigner(CT_PROTOCOL_6, 100, FKey)); + AssertEquals(1, LSecond.DoSignMultiOperationSigner(CT_PROTOCOL_6, 103, LOtherKey)); + LForm := LDisposables.AddObject(TFRMOperationsExplorer.Create(nil)) as TFRMOperationsExplorer; + LForm.Offline := True; + if not LForm.TryImportRawOperations(RawOperation(LFirst), LError) then Fail(LError); + if not LForm.TryImportRawOperations(RawOperation(LSecond), LError) then Fail(LError); + AssertEquals(1, LForm.Operations.OperationsCount); + LMerged := LForm.Operations.GetOperation(0) as TOpMultiOperation; + AssertTrue(TCrypto.ECDSAVerify(FKey.PublicKey, LMerged.GetDigestToSign, LMerged.Data.txSenders[0].Signature)); + AssertTrue(TCrypto.ECDSAVerify(LOtherKey.PublicKey, LMerged.GetDigestToSign, LMerged.Data.txSenders[1].Signature)); + LRaw := Trim(LForm.mOperationExport.Text); + if not LForm.TryImportRawOperations(RawOperation(LSecond), LError) then Fail(LError); + AssertEquals('Repeated partial signatures must be idempotent', LRaw, Trim(LForm.mOperationExport.Text)); + LMerged := LForm.Operations.GetOperation(0) as TOpMultiOperation; + LConflict := LDisposables.AddObject(Multioperation(1800)) as TOpMultiOperation; + AssertEquals(-1, LMerged.MergeMissingSignaturesFrom(LConflict)); + AssertFalse(LForm.TryImportRawOperations(RawOperation(LConflict), LError)); + AssertEquals(LRaw, Trim(LForm.mOperationExport.Text)); + + LSafeBox := LDisposables.AddObject(TOperationSafeBox.Create) as TOperationSafeBox; + LSafeBox.Build(FKey.PublicKey); + LAccount := CT_Account_NUL; + LAccount.accountInfo.state := as_Normal; + LAccount.accountInfo.accountKey := FKey.PublicKey; + LAccount.account := 100; + LAccount.balance := 100000; + LAccount.n_operation := 7; + LSafeBox.Seed(LAccount); + LAccount.account := 101; + LAccount.balance := 0; + LSafeBox.Seed(LAccount); + LAccount.account := 103; + LAccount.balance := 100000; + LAccount.accountInfo.accountKey := LOtherKey.PublicKey; + LSafeBox.Seed(LAccount); + LSnapshot := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + LPrevious := LDisposables.AddObject(TAccountPreviousBlockInfo.Create) as TAccountPreviousBlockInfo; + AssertTrue(LMerged.CheckSignatures(LSnapshot, LError)); + AssertTrue(LMerged.DoOperation(LPrevious, LSnapshot, LError)); + AssertEquals(Int64(1900), Int64(LSnapshot.Account(101).balance)); + AssertEquals(Int64(100), LSnapshot.TotalFee); +end; + +procedure TWalletParityTests.MergedSignatureEncodingStillRequiresCoreVerification; +var + LDisposables: TDisposables; + LFirst, LWrongSigner: TOpMultiOperation; + LSafeBox: TOperationSafeBox; + LSnapshot: TPCSafeBoxTransaction; + LOtherKey: TECPrivateKey; + LAccount: TAccount; + LError: String; +begin + LOtherKey := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LOtherKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LFirst := LDisposables.AddObject(Multioperation) as TOpMultiOperation; + LWrongSigner := LDisposables.AddObject(Multioperation) as TOpMultiOperation; + LFirst.DoSignMultiOperationSigner(CT_PROTOCOL_6, 100, FKey); + LWrongSigner.DoSignMultiOperationSigner(CT_PROTOCOL_6, 103, FKey); + AssertEquals(1, LFirst.MergeMissingSignaturesFrom(LWrongSigner)); + AssertEquals(0, LFirst.MergeMissingSignaturesFrom(LWrongSigner)); + LSafeBox := LDisposables.AddObject(TOperationSafeBox.Create) as TOperationSafeBox; + LSafeBox.Build(FKey.PublicKey); + LAccount := CT_Account_NUL; + LAccount.account := 100; + LAccount.balance := 100000; + LAccount.n_operation := 7; + LAccount.accountInfo.state := as_Normal; + LAccount.accountInfo.accountKey := FKey.PublicKey; + LSafeBox.Seed(LAccount); + LAccount.account := 103; + LAccount.accountInfo.accountKey := LOtherKey.PublicKey; + LSafeBox.Seed(LAccount); + LSnapshot := LDisposables.AddObject(TPCSafeBoxTransaction.Create(LSafeBox)) as TPCSafeBoxTransaction; + AssertFalse('A well-sized signature from a different owner must fail core verification', LFirst.CheckSignatures(LSnapshot, LError)); +end; + +procedure TWalletParityTests.RejectPackagePresentation(Sender: TObject); +begin + raise Exception.Create('Fixture presentation failure'); +end; + +procedure TWalletParityTests.RawImportPresentationFailureIsSeparateFromCommit; +var + LDisposables: TDisposables; + LForm: TFRMOperationsExplorer; + LError: String; +begin + LForm := LDisposables.AddObject(TFRMOperationsExplorer.Create(nil)) as TFRMOperationsExplorer; + LForm.Operations.OnChanged := RejectPackagePresentation; + AssertTrue(LForm.TryImportRawOperations(RawTransaction(1000), LError)); + AssertEquals(1, LForm.Operations.OperationsCount); + AssertTrue(Pos('imported', LError) > 0); + AssertTrue(Pos('Fixture presentation failure', LError) > 0); + AssertFalse(LForm.TryImportRawOperations('malformed', LError)); + AssertEquals(1, LForm.Operations.OperationsCount); +end; + +procedure TWalletParityTests.LateRawConflictDoesNotLeakStagedSignatures; +var + LDisposables: TDisposables; + LFirst, LSecond: TOpMultiOperation; + LConflict: TPCOperation; + LPackage: TOperationsHashTree; + LForm: TFRMOperationsExplorer; + LStream: TMemoryStream; + LBytes: TRawBytes; + LRaw, LError: String; +begin + LFirst := LDisposables.AddObject(Multioperation) as TOpMultiOperation; + LSecond := LDisposables.AddObject(Multioperation) as TOpMultiOperation; + LFirst.DoSignMultiOperationSigner(CT_PROTOCOL_6, 100, FKey); + LSecond.DoSignMultiOperationSigner(CT_PROTOCOL_6, 103, FKey); + LForm := LDisposables.AddObject(TFRMOperationsExplorer.Create(nil)) as TFRMOperationsExplorer; + AssertTrue(LForm.TryImportRawOperations(RawOperation(LFirst), LError)); + LRaw := Trim(LForm.mOperationExport.Text); + LPackage := LDisposables.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LPackage.AddOperationToHashTree(LSecond); + LConflict := LDisposables.AddObject(TOpTransaction.CreateTransaction(CT_PROTOCOL_6, + 100, 8, 101, FKey, 5000, 100, CT_TOperationPayload_NUL)) as TPCOperation; + LPackage.AddOperationToHashTree(LConflict); + LStream := LDisposables.AddObject(TMemoryStream.Create) as TMemoryStream; + LPackage.SaveOperationsHashTreeToStream(LStream, False); + LBytes.FromStream(LStream); + AssertFalse(LForm.TryImportRawOperations(LBytes.ToHexaString, LError)); + AssertEquals(LRaw, Trim(LForm.mOperationExport.Text)); + AssertEquals(0, Length((LForm.Operations.GetOperation(0) as TOpMultiOperation).Data.txSenders[1].Signature.r)); +end; + +initialization + RegisterTest(TWalletParityTests); + +end. diff --git a/src/tests/experimental/startup/UBufferedAdoptionTests.pas b/src/tests/experimental/startup/UBufferedAdoptionTests.pas new file mode 100644 index 000000000..5c421cd46 --- /dev/null +++ b/src/tests/experimental/startup/UBufferedAdoptionTests.pas @@ -0,0 +1,88 @@ +unit UBufferedAdoptionTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$MODE Delphi}{$H+} + +interface + +uses fpcunit, testregistry; + +type + TBufferedAdoptionTests = class(TTestCase) + published + procedure PendingProtocol5RoundTripAndReplacement; + procedure PendingProtocol5CancellationAndRetry; + procedure HistoryReadSaveDeleteAndReopen; + procedure HistoryHeaderGroupBoundary; + procedure RandomHashCanonicalTable; + procedure RandomHashInvalidMagic; + procedure RandomHashTruncatedSeal; + procedure RandomHashMismatchedSeal; + procedure MalformedHistoryReadOnlyPreservesBytes; + procedure PendingProtocol6RoundTripAndReplacement; + procedure PendingProtocol6CancellationAndRetry; + end; + +implementation + +uses UBufferedAdoptionChecks; + +procedure TBufferedAdoptionTests.PendingProtocol5RoundTripAndReplacement; +begin + CheckBufferedAdoption(1); +end; + +procedure TBufferedAdoptionTests.PendingProtocol5CancellationAndRetry; +begin + CheckBufferedAdoption(2); +end; + +procedure TBufferedAdoptionTests.HistoryReadSaveDeleteAndReopen; +begin + CheckBufferedAdoption(3); +end; + +procedure TBufferedAdoptionTests.HistoryHeaderGroupBoundary; +begin + CheckBufferedAdoption(4); +end; + +procedure TBufferedAdoptionTests.RandomHashCanonicalTable; +begin + CheckBufferedAdoption(5); +end; + +procedure TBufferedAdoptionTests.RandomHashInvalidMagic; +begin + CheckBufferedAdoption(6); +end; + +procedure TBufferedAdoptionTests.RandomHashTruncatedSeal; +begin + CheckBufferedAdoption(7); +end; + +procedure TBufferedAdoptionTests.RandomHashMismatchedSeal; +begin + CheckBufferedAdoption(8); +end; + +procedure TBufferedAdoptionTests.MalformedHistoryReadOnlyPreservesBytes; +begin + CheckBufferedAdoption(9); +end; + +procedure TBufferedAdoptionTests.PendingProtocol6RoundTripAndReplacement; +begin + CheckBufferedAdoption(10); +end; + +procedure TBufferedAdoptionTests.PendingProtocol6CancellationAndRetry; +begin + CheckBufferedAdoption(11); +end; + +initialization + RegisterTest(TBufferedAdoptionTests); +end. diff --git a/src/tests/experimental/startup/UCheckpointStorageTests.pas b/src/tests/experimental/startup/UCheckpointStorageTests.pas new file mode 100644 index 000000000..d2c74697a --- /dev/null +++ b/src/tests/experimental/startup/UCheckpointStorageTests.pas @@ -0,0 +1,292 @@ +unit UCheckpointStorageTests; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + Registers the same real-storage regressions used by the FPC/Delphi probe. } + +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses SysUtils, fpcunit, testregistry, UCheckpointStorageChecks; + +type + TCheckpointStorageTests = class(TTestCase) + private + procedure RunCheck(ACheck: TCheckpointStorageCheck); + published + procedure TemporaryCreateFailurePreservesPreviousCheckpoint; + procedure ShortWritePreservesPreviousCheckpoint; + procedure FlushFailurePreservesPreviousCheckpoint; + procedure InvalidTemporaryPreservesPreviousCheckpoint; + procedure AtomicReplaceFailurePreservesPreviousCheckpoint; + procedure FailedFirstCheckpointDoesNotPublishPartialFiles; + procedure FailedCheckpointCanRetryAndReopen; + procedure MemoryCheckpointPublishesCompleteState; + procedure EmptyCheckpointCanReopen; + procedure MissingCandidateDoesNotCreateFiles; + procedure TruncatedCandidateIsRejectedWithoutMutation; + procedure MalformedCandidateIsRejectedWithoutMutation; + procedure HealthyMainSurvivesCorruptCheckpoint; + procedure CorruptMainRecoversFromValidCheckpoint; + procedure AllCorruptCandidatesFailWithoutOverwritingEvidence; + procedure ValidCheckpointReplaysLaterLocalBlocks; + procedure FailedLiveFlushExceptionKeepsUnstableMarker; + procedure FailedLiveShortWriteKeepsUnstableMarker; + procedure FailedUncachedWriteExceptionKeepsUnstableMarker; + procedure FailedUncachedShortWriteKeepsUnstableMarker; + procedure FailedUncachedCloseDoesNotPublishStableState; + procedure UncachedPartialRetriesKeepAllFailedRegions; + procedure FailedAutomaticCachedCloseLogsAndReleases; + procedure HealthyMainSurvivesNewerLegacyHeaderWithInvalidBody; + procedure OlderLegacySurvivesNewerHeaderWithInvalidBody; + procedure FailedUnstableHeaderMustRetryBeforeLaterPayload; + procedure LegacyCandidateValidationReportsProgress; + procedure CancelledLegacyCandidatePreservesLiveStorage; + procedure AheadLegacyReplaysLocalHistory; + procedure AheadLegacyRespectsRestoreLimit; + procedure AheadAbstractReplaysLocalHistory; + procedure AheadAbstractRespectsRestoreLimit; + procedure CorruptMainWithAheadLegacy; + procedure CorruptMainWithAheadAbstract; + procedure InvalidLegacyHeaderOnly; + procedure InvalidLegacyBodyOnly; + procedure MalformedLegacyBesideAheadLegacy; + procedure MalformedLegacyBesideAheadAbstract; + procedure InvalidEligibleBodyBesideAhead; + procedure AheadLiveAbstractOnly; + procedure EligibleLegacyBesideAhead; + procedure AheadLegacyBodyNotInspected; + {$IFDEF MSWINDOWS} + procedure RealSharingFailurePreservesPreviousCheckpoint; + {$ENDIF} + end; + +implementation + +procedure TCheckpointStorageTests.RunCheck(ACheck: TCheckpointStorageCheck); +var LID: TGUID; +begin + CreateGUID(LID); + AssertEquals(0,RunCheckpointStorageCheck(ACheck,IncludeTrailingPathDelimiter(GetTempDir)+'CheckpointStorage-'+GUIDToString(LID)+'.log')); +end; + +procedure TCheckpointStorageTests.TemporaryCreateFailurePreservesPreviousCheckpoint; +begin + RunCheck(cscFactoryFailure); +end; + +procedure TCheckpointStorageTests.ShortWritePreservesPreviousCheckpoint; +begin + RunCheck(cscPartialWrite); +end; + +procedure TCheckpointStorageTests.FlushFailurePreservesPreviousCheckpoint; +begin + RunCheck(cscFlushFailure); +end; + +procedure TCheckpointStorageTests.InvalidTemporaryPreservesPreviousCheckpoint; +begin + RunCheck(cscValidationFailure); +end; + +procedure TCheckpointStorageTests.AtomicReplaceFailurePreservesPreviousCheckpoint; +begin + RunCheck(cscPublicationFailure); +end; + +procedure TCheckpointStorageTests.FailedFirstCheckpointDoesNotPublishPartialFiles; +begin + RunCheck(cscAbsentTargetFailures); +end; + +procedure TCheckpointStorageTests.FailedCheckpointCanRetryAndReopen; +begin + RunCheck(cscSuccessfulRetry); +end; + +procedure TCheckpointStorageTests.MemoryCheckpointPublishesCompleteState; +begin + RunCheck(cscMemoryPublication); +end; + +procedure TCheckpointStorageTests.EmptyCheckpointCanReopen; +begin + RunCheck(cscEmptyPublication); +end; + +procedure TCheckpointStorageTests.MissingCandidateDoesNotCreateFiles; +begin + RunCheck(cscAnalyzeMissing); +end; + +procedure TCheckpointStorageTests.TruncatedCandidateIsRejectedWithoutMutation; +begin + RunCheck(cscAnalyzeTruncated); +end; + +procedure TCheckpointStorageTests.MalformedCandidateIsRejectedWithoutMutation; +begin + RunCheck(cscAnalyzeMalformed); +end; + +procedure TCheckpointStorageTests.HealthyMainSurvivesCorruptCheckpoint; +begin + RunCheck(cscHealthyMainBadCheckpoint); +end; + +procedure TCheckpointStorageTests.CorruptMainRecoversFromValidCheckpoint; +begin + RunCheck(cscCorruptMainGoodCheckpoint); +end; + +procedure TCheckpointStorageTests.AllCorruptCandidatesFailWithoutOverwritingEvidence; +begin + RunCheck(cscAllCorruptCandidates); +end; + +procedure TCheckpointStorageTests.ValidCheckpointReplaysLaterLocalBlocks; +begin + RunCheck(cscCheckpointThenLocalReplay); +end; + +procedure TCheckpointStorageTests.FailedLiveFlushExceptionKeepsUnstableMarker; +begin + RunCheck(cscFailedFlushException); +end; + +procedure TCheckpointStorageTests.FailedLiveShortWriteKeepsUnstableMarker; +begin + RunCheck(cscFailedFlushShortWrite); +end; + +procedure TCheckpointStorageTests.FailedUncachedWriteExceptionKeepsUnstableMarker; +begin + RunCheck(cscUncachedWriteException); +end; + +procedure TCheckpointStorageTests.FailedUncachedShortWriteKeepsUnstableMarker; +begin + RunCheck(cscUncachedShortWrite); +end; + +procedure TCheckpointStorageTests.FailedUncachedCloseDoesNotPublishStableState; +begin + RunCheck(cscUncachedFailedClose); +end; + +procedure TCheckpointStorageTests.UncachedPartialRetriesKeepAllFailedRegions; +begin + RunCheck(cscUncachedPartialRetry); +end; + +procedure TCheckpointStorageTests.FailedAutomaticCachedCloseLogsAndReleases; +begin + RunCheck(cscCachedFailedClose); +end; + +procedure TCheckpointStorageTests.HealthyMainSurvivesNewerLegacyHeaderWithInvalidBody; +begin + RunCheck(cscHealthyMainBadLegacyBody); +end; + +procedure TCheckpointStorageTests.OlderLegacySurvivesNewerHeaderWithInvalidBody; +begin + RunCheck(cscOlderLegacySurvivesBadNewerBody); +end; + +procedure TCheckpointStorageTests.FailedUnstableHeaderMustRetryBeforeLaterPayload; +begin + RunCheck(cscUnstableHeaderRetryBeforeLaterPayload); +end; + +{$IFDEF MSWINDOWS} +procedure TCheckpointStorageTests.RealSharingFailurePreservesPreviousCheckpoint; +begin + RunCheck(cscSharingDenied); +end; +{$ENDIF} + +procedure TCheckpointStorageTests.LegacyCandidateValidationReportsProgress; +begin + RunCheck(cscLegacyRestoreValidationProgress); +end; + +procedure TCheckpointStorageTests.CancelledLegacyCandidatePreservesLiveStorage; +begin + RunCheck(cscLegacyRestoreCancellation); +end; + +procedure TCheckpointStorageTests.AheadLegacyReplaysLocalHistory; +begin + RunCheck(cscAheadLegacyReplaysLocalHistory); +end; + +procedure TCheckpointStorageTests.AheadLegacyRespectsRestoreLimit; +begin + RunCheck(cscAheadLegacyRespectsRestoreLimit); +end; + +procedure TCheckpointStorageTests.AheadAbstractReplaysLocalHistory; +begin + RunCheck(cscAheadAbstractReplaysLocalHistory); +end; + +procedure TCheckpointStorageTests.AheadAbstractRespectsRestoreLimit; +begin + RunCheck(cscAheadAbstractRespectsRestoreLimit); +end; + +procedure TCheckpointStorageTests.CorruptMainWithAheadLegacy; +begin + RunCheck(cscCorruptMainWithAheadLegacy); +end; + +procedure TCheckpointStorageTests.CorruptMainWithAheadAbstract; +begin + RunCheck(cscCorruptMainWithAheadAbstract); +end; + +procedure TCheckpointStorageTests.InvalidLegacyHeaderOnly; +begin + RunCheck(cscInvalidLegacyHeaderOnly); +end; + +procedure TCheckpointStorageTests.InvalidLegacyBodyOnly; +begin + RunCheck(cscInvalidLegacyBodyOnly); +end; + +procedure TCheckpointStorageTests.MalformedLegacyBesideAheadLegacy; +begin + RunCheck(cscMalformedLegacyBesideAheadLegacy); +end; + +procedure TCheckpointStorageTests.MalformedLegacyBesideAheadAbstract; +begin + RunCheck(cscMalformedLegacyBesideAheadAbstract); +end; + +procedure TCheckpointStorageTests.InvalidEligibleBodyBesideAhead; +begin + RunCheck(cscInvalidEligibleBodyBesideAhead); +end; + +procedure TCheckpointStorageTests.AheadLiveAbstractOnly; +begin + RunCheck(cscAheadLiveAbstractOnly); +end; + +procedure TCheckpointStorageTests.EligibleLegacyBesideAhead; +begin + RunCheck(cscEligibleLegacyBesideAhead); +end; + +procedure TCheckpointStorageTests.AheadLegacyBodyNotInspected; +begin + RunCheck(cscAheadLegacyBodyNotInspected); +end; + +initialization + RegisterTest(TCheckpointStorageTests); +end. diff --git a/src/tests/experimental/startup/UPendingCleanupTests.pas b/src/tests/experimental/startup/UPendingCleanupTests.pas new file mode 100644 index 000000000..4ae7c9717 --- /dev/null +++ b/src/tests/experimental/startup/UPendingCleanupTests.pas @@ -0,0 +1,439 @@ +unit UPendingCleanupTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + Distributed under the MIT software license, see the accompanying LICENSE. + Generated operations, in-memory lists and disposable files only. } + +{$mode delphi} + +interface + +uses + Classes, SysUtils, fpcunit, testregistry, UBaseTypes, UPCOrderedLists, + UNode, UBlockChain, UCrypto, UAccounts; + +type + TOrderedRawListCleanupTests = class(TTestCase) + private + FList : TOrderedRawList; + function Raw(AValue : Integer) : TRawBytes; + procedure Fill(ACount : Integer); + procedure AssertOriginal; + procedure Reject(const AIndexes : array of Integer); + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure EmptyRequestLeavesValuesAndTags; + procedure DeleteAllReleasesOwnedEntries; + procedure InterleavedBatchesRetainValuesAndTags; + procedure InvalidRequestsAreAtomic; + procedure EndRangesRetainSearchableMiddle; + procedure LargeBatchPreservesUnrelatedReservations; + end; + + TPendingFinalizationTests = class(TTestCase) + private + FDirectory, FPreviousDirectory : String; + FKey : TECPrivateKey; + FTree, FNestedTree : TOperationsHashTree; + FNode : TNode; + FCancelStage : TProgressStage; + FObservedStages : set of TProgressStage; + FExpectedProgressThread : TThreadID; + FReservedTotal, FNestedCount : Integer; + FRunNested : Boolean; + procedure OnProgress(Sender : TObject; const AProgress : TProgressInfo); + procedure ExpectCancellation(ATree : TOperationsHashTree); + procedure CreateNode; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure CanceledBatchCanReserveAgain; + procedure NestedCanceledBatchesPreserveOuterReservations; + procedure FinalizingCancellationStillReleasesReservations; + procedure ActualNodeReportsTypedStagesSynchronously; + procedure BufferedSavePreservesExactBytesAndTruncatesOldTail; + procedure BufferedSaveUsesBoundedFileWrites; + procedure EmptyAndPopulatedProofDigestRetainsBytes; + procedure ZeroAndNonzeroBigNumbersRetainByteEncoding; + end; + +implementation + +uses + Windows, UConst, UOpTransaction, UFileStorage, UPCDataTypes; + +type + TProcessIOCounts = record + ReadOperationCount, WriteOperationCount, OtherOperationCount : UInt64; + ReadTransferCount, WriteTransferCount, OtherTransferCount : UInt64; + end; + +function ReadProcessIOCounts(AProcess : THandle; out ACounts : TProcessIOCounts) : BOOL; + stdcall; external 'kernel32.dll' name 'GetProcessIoCounters'; + +function TOrderedRawListCleanupTests.Raw(AValue : Integer) : TRawBytes; +begin + SetLength(Result,4); + Result[0] := Byte(AValue shr 24); + Result[1] := Byte(AValue shr 16); + Result[2] := Byte(AValue shr 8); + Result[3] := Byte(AValue); +end; + +procedure TOrderedRawListCleanupTests.Fill(ACount : Integer); +var i : Integer; +begin + for i := 0 to ACount-1 do FList.Add(Raw(i),1000+i); +end; + +procedure TOrderedRawListCleanupTests.SetUp; +begin + inherited; + FList := TOrderedRawList.Create; +end; + +procedure TOrderedRawListCleanupTests.TearDown; +begin + FList.Free; + inherited; +end; + +procedure TOrderedRawListCleanupTests.AssertOriginal; +var i : Integer; +begin + AssertEquals(8,FList.Count); + for i := 0 to 7 do begin + AssertTrue(TBaseType.Equals(Raw(i),FList.Get(i))); + AssertEquals(1000+i,FList.GetTag(i)); + end; +end; + +procedure TOrderedRawListCleanupTests.Reject(const AIndexes : array of Integer); +var LRejected : Boolean; +begin + LRejected := False; + try + FList.DeleteIndexes(AIndexes); + except + on E : EOrderedList do LRejected := True; + end; + AssertTrue('Invalid bulk requests must be rejected',LRejected); + AssertOriginal; +end; + +procedure TOrderedRawListCleanupTests.EmptyRequestLeavesValuesAndTags; +begin + FList.DeleteIndexes([]); + AssertEquals(0,FList.Count); + Fill(8); + FList.DeleteIndexes([]); + AssertOriginal; +end; + +procedure TOrderedRawListCleanupTests.DeleteAllReleasesOwnedEntries; +begin + Fill(8); + FList.DeleteIndexes([0,1,2,3,4,5,6,7]); + AssertEquals(0,FList.Count); + Fill(8); + AssertOriginal; +end; + +procedure TOrderedRawListCleanupTests.InterleavedBatchesRetainValuesAndTags; +var i : Integer; +begin + Fill(8); + FList.DeleteIndexes([0,2,4,6]); + AssertEquals(4,FList.Count); + for i := 0 to 3 do begin + AssertTrue(TBaseType.Equals(Raw(i*2+1),FList.Get(i))); + AssertEquals(1000+i*2+1,FList.GetTag(i)); + AssertEquals(i,FList.IndexOf(Raw(i*2+1))); + AssertEquals(-1,FList.IndexOf(Raw(i*2))); + end; +end; + +procedure TOrderedRawListCleanupTests.InvalidRequestsAreAtomic; +begin + Fill(8); + Reject([-1]); + Reject([0,8]); + Reject([1,1]); + Reject([4,2]); +end; + +procedure TOrderedRawListCleanupTests.EndRangesRetainSearchableMiddle; +begin + Fill(8); + FList.DeleteIndexes([0,1,6,7]); + AssertEquals(4,FList.Count); + AssertTrue(TBaseType.Equals(Raw(2),FList.Get(0))); + AssertTrue(TBaseType.Equals(Raw(5),FList.Get(3))); + FList.Add(Raw(0),-1); + FList.Remove(Raw(3)); + AssertEquals(4,FList.Count); + AssertEquals(-1,FList.GetTag(0)); + AssertEquals(-1,FList.IndexOf(Raw(3))); +end; + +procedure TOrderedRawListCleanupTests.LargeBatchPreservesUnrelatedReservations; +const CT_COUNT = 185831; +var LIndexes : array of Integer; + i : Integer; +begin + Fill(CT_COUNT*2); + SetLength(LIndexes,CT_COUNT); + for i := 0 to High(LIndexes) do LIndexes[i] := i*2; + FList.DeleteIndexes(LIndexes); + AssertEquals(CT_COUNT,FList.Count); + for i := 0 to FList.Count-1 do begin + AssertTrue(TBaseType.Equals(Raw(i*2+1),FList.Get(i))); + AssertEquals(1000+i*2+1,FList.GetTag(i)); + end; +end; + +procedure TPendingFinalizationTests.SetUp; +var LID : TGUID; + LOperation : TPCOperation; + i : Integer; +begin + inherited; + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(GetTempDir)+'PendingCleanup-'+GUIDToString(LID); + ForceDirectories(FDirectory); + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(FDirectory); + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FTree := TOperationsHashTree.Create; + FNestedTree := TOperationsHashTree.Create; + for i := 1 to 3 do begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_6,100,i,101,FKey,100,1,CT_TOperationPayload_NUL); + try + AssertTrue(FTree.AddOperationToHashTree(LOperation)); + if i>=2 then AssertTrue(FNestedTree.AddOperationToHashTree(LOperation)); + finally + LOperation.Free; + end; + end; + FNode := nil; + FCancelStage := psValidatingSignatures; + FObservedStages := []; + FExpectedProgressThread := GetCurrentThreadID; + FReservedTotal := -1; + FNestedCount := 0; + FRunNested := False; +end; + +procedure TPendingFinalizationTests.TearDown; +begin + FNode.Free; + FNestedTree.Free; + FTree.Free; + FKey.Free; + TNode.SetPascalCoinDataFolder(FPreviousDirectory); + SysUtils.DeleteFile(FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops'); + RemoveDir(FDirectory+PathDelim+'Data'); + RemoveDir(FDirectory); + inherited; +end; + +procedure TPendingFinalizationTests.CreateNode; +begin + AssertFalse('Tests must not reuse a global node',TNode.NodeExists); + FNode := TNode.Create(nil); + FNode.BroadcastData := False; +end; + +procedure TPendingFinalizationTests.ExpectCancellation(ATree : TOperationsHashTree); +var LError : String; + LCanceled : Boolean; +begin + LCanceled := False; + try + FNode.AddOperations(nil,ATree,nil,LError,OnProgress); + except + on E : EAbort do LCanceled := True; + end; + AssertTrue('The requested progress cancellation must propagate',LCanceled); +end; + +procedure TPendingFinalizationTests.OnProgress(Sender : TObject; const AProgress : TProgressInfo); +begin + AssertTrue('Actual node owns its progress callback',Sender = FNode); + AssertEquals('Core callback and cancellation remain on the invoking thread', + QWord(FExpectedProgressThread),QWord(GetCurrentThreadID)); + Include(FObservedStages,AProgress.Stage); + AssertTrue('The raw operation counts remain bounded', + (AProgress.CurrentPosition >= 0) and + (AProgress.CurrentPosition <= AProgress.TotalCount)); + if (AProgress.Stage=FCancelStage) and (AProgress.CurrentPosition=0) then begin + FReservedTotal := AProgress.TotalCount; + if FRunNested then begin + FRunNested := False; + // The outer batch owns hashes1..3. Both overlapping nested batches must + // reserve nothing, and their cleanup must leave the outer owner intact. + ExpectCancellation(FNestedTree); + AssertEquals(0,FReservedTotal); + Inc(FNestedCount); + ExpectCancellation(FNestedTree); + AssertEquals(0,FReservedTotal); + Inc(FNestedCount); + end; + Abort; + end; +end; + +procedure TPendingFinalizationTests.CanceledBatchCanReserveAgain; +begin + CreateNode; + ExpectCancellation(FTree); + AssertEquals(3,FReservedTotal); + ExpectCancellation(FTree); + AssertEquals('Canceled reservations must not suppress the retry',3,FReservedTotal); +end; + +procedure TPendingFinalizationTests.NestedCanceledBatchesPreserveOuterReservations; +begin + CreateNode; + FRunNested := True; + ExpectCancellation(FTree); + AssertEquals(2,FNestedCount); + ExpectCancellation(FTree); + AssertEquals('The outer batch must release its own reservations',3,FReservedTotal); +end; + +procedure TPendingFinalizationTests.FinalizingCancellationStillReleasesReservations; +begin + CreateNode; + // Invalid senders in an empty safebox reach finalization without accepting + // an operation; no persistent wallet, chain or broadcast is involved. + FCancelStage := psFinalizingOperations; + ExpectCancellation(FTree); + AssertEquals(3,FReservedTotal); + FCancelStage := psValidatingSignatures; + ExpectCancellation(FTree); + AssertEquals('Finalization cancellation must still release reservations',3,FReservedTotal); + AssertFalse(FileExists(FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops')); +end; + +procedure TPendingFinalizationTests.ActualNodeReportsTypedStagesSynchronously; +var LError : String; +begin + CreateNode; + FCancelStage := psNone; + // These generated senders are absent from the empty safebox. The real core + // still prepares, validates, applies and finalizes without accepting them. + AssertEquals(0,FNode.AddOperations(nil,FTree,nil,LError,OnProgress)); + AssertTrue('Preparation is a typed stage',psPreparingOperations in FObservedStages); + AssertTrue('Signature validation is a typed stage',psValidatingSignatures in FObservedStages); + AssertTrue('Application is a typed stage',psApplyingOperations in FObservedStages); + AssertTrue('Finalization is a typed stage',psFinalizingOperations in FObservedStages); +end; + +procedure TPendingFinalizationTests.BufferedSavePreservesExactBytesAndTruncatesOldTail; +var LBank : TPCBank; + LStorage : TFileStorage; + LExpected, LActual : TMemoryStream; + LFileName : String; +begin + LBank := TPCBank.Create(nil); + LStorage := TFileStorage.Create(nil); + LExpected := TMemoryStream.Create; + LActual := TMemoryStream.Create; + try + LStorage.Bank := LBank; + AssertTrue(FTree.SaveOperationsHashTreeToStream(LExpected,True)); + LStorage.SavePendingBufferOperations(FTree); + FreeAndNil(LStorage); // Release the exclusive file before inspecting bytes. + LFileName := FDirectory+PathDelim+'Data'+PathDelim+'pendingbuffer.ops'; + LActual.LoadFromFile(LFileName); + AssertEquals(LExpected.Size,LActual.Size); + AssertTrue(CompareMem(LExpected.Memory,LActual.Memory,LExpected.Size)); + FTree.ClearHastThree; + LExpected.Clear; + AssertTrue(FTree.SaveOperationsHashTreeToStream(LExpected,True)); + LStorage := TFileStorage.Create(nil); + LStorage.Bank := LBank; + LStorage.SavePendingBufferOperations(FTree); + FreeAndNil(LStorage); + LActual.LoadFromFile(LFileName); + AssertEquals('A shorter package must truncate the old tail',LExpected.Size,LActual.Size); + AssertTrue(CompareMem(LExpected.Memory,LActual.Memory,LExpected.Size)); + finally + LActual.Free; + LExpected.Free; + LStorage.Free; + LBank.Free; + end; +end; + +procedure TPendingFinalizationTests.BufferedSaveUsesBoundedFileWrites; +var LBank : TPCBank; + LStorage : TFileStorage; + LBefore, LAfter : TProcessIOCounts; +begin + LBank := TPCBank.Create(nil); + LStorage := TFileStorage.Create(nil); + try + LStorage.Bank := LBank; + AssertTrue(ReadProcessIOCounts(GetCurrentProcess,LBefore)); + LStorage.SavePendingBufferOperations(FTree); + AssertTrue(ReadProcessIOCounts(GetCurrentProcess,LAfter)); + AssertTrue('Saving a small package must use bulk file writes, not each serialized field', + LAfter.WriteOperationCount-LBefore.WriteOperationCount<8); + finally + LStorage.Free; + LBank.Free; + end; +end; + +procedure TPendingFinalizationTests.EmptyAndPopulatedProofDigestRetainsBytes; +const + CT_EXPECTED : array[1..2,0..1] of String = ( + ('44332211','01020304AABBCC44332211'), + ('4433221100000000','01020304AABBCC44332211000000001020304050')); +var LBlock : TOperationBlock; + LActual : TRawBytes; + LProtocol, LPopulated : Integer; +begin + for LProtocol := 1 to 2 do begin + for LPopulated := 0 to 1 do begin + LBlock := CT_OperationBlock_NUL; + if LProtocol=1 then LBlock.protocol_version := CT_PROTOCOL_1 + else LBlock.protocol_version := CT_PROTOCOL_6; + LBlock.fee := $11223344; + if LPopulated=1 then begin + LBlock.initial_safe_box_hash := TCrypto.HexaToRaw('01020304'); + LBlock.operations_hash := TCrypto.HexaToRaw('AABBCC'); + LBlock.previous_proof_of_work := TCrypto.HexaToRaw('1020304050'); + end; + TPascalCoinProtocol.CalcProofOfWork_Part3(LBlock,LActual); + AssertTrue('The existing proof digest input bytes must remain identical', + TBaseType.Equals(TCrypto.HexaToRaw(CT_EXPECTED[LProtocol,LPopulated]),LActual)); + end; + end; +end; + +procedure TPendingFinalizationTests.ZeroAndNonzeroBigNumbersRetainByteEncoding; +var LNumber : TBigNum; +begin + LNumber := TBigNum.Create(0); + try + AssertEquals('Zero retains the existing empty unsigned encoding',0,Length(LNumber.RawValue)); + LNumber.Value := $01020304; + AssertTrue('Nonzero BIGNUM byte order must remain unchanged', + TBaseType.Equals(TCrypto.HexaToRaw('01020304'),LNumber.RawValue)); + finally + LNumber.Free; + end; +end; + +initialization + RegisterTest(TOrderedRawListCleanupTests); + RegisterTest(TPendingFinalizationTests); +end. diff --git a/src/tests/experimental/startup/UPendingRestoreTests.pas b/src/tests/experimental/startup/UPendingRestoreTests.pas new file mode 100644 index 000000000..7964e45ac --- /dev/null +++ b/src/tests/experimental/startup/UPendingRestoreTests.pas @@ -0,0 +1,325 @@ +unit UPendingRestoreTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. } + +{$mode delphi} + +interface + +uses + Classes, SysUtils, fpcunit, testregistry, UCrypto, UAccounts, UBlockChain; + +type + TPendingRestoreTests = class(TTestCase) + private + FKey : TECPrivateKey; + FOriginal, FLoaded : TOperationsHashTree; + FStream : TMemoryStream; + FOriginalBytes : TBytes; + FProgressCount, FChangedCount : Integer; + FFirstCurrent, FLastCurrent, FLastTotal : Int64; + FCancelAt : Int64; + FDelayFirstProgress : Boolean; + FExpectedProgressThread : TThreadID; + procedure OnProgress(Sender : TObject; const AProgress : TProgressInfo); + procedure OnChanged(Sender : TObject); + procedure SnapshotSource; + procedure AssertSourceUnchanged; + procedure AssertSameOperations; + procedure AssertNotificationsRestored; + procedure LoadAndExpectCancellation; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure LegacyStoredRoundTripWithoutProgress; + procedure ReportsDecodeStartAndCompletion; + procedure CancellationBeforeDecodeRestoresNotifications; + procedure CancellationDuringDecodePreservesSource; + procedure CanceledDecodeCanBeRetried; + procedure EmptySavedTreeReportsCompletion; + procedure TruncatedStreamDoesNotReportSuccessfulCompletion; + procedure LegacyStorageOverrideIsPreserved; + end; + +implementation + +uses + UConst, UOpTransaction, UPCDataTypes, UBaseTypes, UFileStorage, UMemory; + +type + // An existing storage implementation can retain its original override. + TPendingStorageOverride = class(TFileStorage) + protected + procedure DoLoadPendingBufferOperations(OperationsHashTree : TOperationsHashTree); override; + public + Calls : Integer; + RaiseOnLoad : Boolean; + end; + +procedure TPendingStorageOverride.DoLoadPendingBufferOperations(OperationsHashTree : TOperationsHashTree); +begin + Inc(Calls); + if RaiseOnLoad then Abort; +end; + +procedure TPendingRestoreTests.SetUp; +var + LOperation : TPCOperation; + i : Integer; +begin + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FOriginal := TOperationsHashTree.Create; + FLoaded := TOperationsHashTree.Create; + FStream := TMemoryStream.Create; + FProgressCount := 0; + FChangedCount := 0; + FCancelAt := -1; + FDelayFirstProgress := False; + FExpectedProgressThread := GetCurrentThreadID; + FFirstCurrent := -1; + FLastCurrent := -1; + FLastTotal := -1; + FLoaded.OnChanged := OnChanged; + for i := 1 to 8 do begin + LOperation := TOpTransaction.CreateTransaction(CT_PROTOCOL_6,100,i,101, + FKey,100,1,CT_TOperationPayload_NUL); + try + AssertTrue(FOriginal.AddOperationToHashTree(LOperation)); + finally + LOperation.Free; + end; + end; + AssertTrue(FOriginal.SaveOperationsHashTreeToStream(FStream,True)); + FStream.Position := 0; + SnapshotSource; +end; + +procedure TPendingRestoreTests.TearDown; +begin + FStream.Free; + FLoaded.Free; + FOriginal.Free; + FKey.Free; +end; + +procedure TPendingRestoreTests.OnProgress(Sender : TObject; const AProgress : TProgressInfo); +begin + AssertTrue('Progress is attributed to its operation tree',Sender = FLoaded); + AssertTrue('The core identifies decoding without a formatted message', + AProgress.Stage = psReadingOperations); + AssertEquals('Core callbacks stay synchronous on the invoking thread', + QWord(FExpectedProgressThread),QWord(GetCurrentThreadID)); + AssertTrue('A progress count stays within its total', + (AProgress.CurrentPosition >= 0) and + (AProgress.CurrentPosition <= AProgress.TotalCount)); + if FProgressCount = 0 then FFirstCurrent := AProgress.CurrentPosition; + Inc(FProgressCount); + FLastCurrent := AProgress.CurrentPosition; + FLastTotal := AProgress.TotalCount; + // Model a slow first notification so a mid-stream checkpoint is exercised + // deterministically even with this small, generated in-memory fixture. + if (FProgressCount = 1) and FDelayFirstProgress then Sleep(250); + if (FCancelAt >= 0) and (AProgress.CurrentPosition >= FCancelAt) then Abort; +end; + +procedure TPendingRestoreTests.OnChanged(Sender : TObject); +begin + Inc(FChangedCount); +end; + +procedure TPendingRestoreTests.SnapshotSource; +begin + SetLength(FOriginalBytes,FStream.Size); + if FStream.Size > 0 then Move(FStream.Memory^,FOriginalBytes[0],FStream.Size); +end; + +procedure TPendingRestoreTests.AssertSourceUnchanged; +begin + AssertEquals('Restore must not alter the saved package',Length(FOriginalBytes),FStream.Size); + if Length(FOriginalBytes) > 0 then + AssertTrue('Saved operation bytes changed',CompareMem(@FOriginalBytes[0],FStream.Memory,Length(FOriginalBytes))); +end; + +procedure TPendingRestoreTests.AssertSameOperations; +var + i : Integer; +begin + AssertEquals(FOriginal.OperationsCount,FLoaded.OperationsCount); + for i := 0 to FOriginal.OperationsCount-1 do begin + AssertTrue('Signed operation digest changed on restore', + TBaseType.Equals(FOriginal.GetOperation(i).Sha256,FLoaded.GetOperation(i).Sha256)); + AssertEquals(FOriginal.GetOperation(i).N_Operation,FLoaded.GetOperation(i).N_Operation); + end; +end; + +procedure TPendingRestoreTests.AssertNotificationsRestored; +var + LExpected, LActual : TNotifyEvent; +begin + LExpected := OnChanged; + LActual := FLoaded.OnChanged; + AssertTrue('Decode lost the original change callback', + (TMethod(LExpected).Code = TMethod(LActual).Code) and + (TMethod(LExpected).Data = TMethod(LActual).Data)); +end; + +procedure TPendingRestoreTests.LoadAndExpectCancellation; +var + LError : String; + LCanceled : Boolean; +begin + LCanceled := False; + try + FLoaded.LoadOperationsHashTreeFromStream(FStream,True,CT_PROTOCOL_6, + CT_PROTOCOL_6,Nil,LError,OnProgress); + except + on E : EAbort do LCanceled := True; + end; + AssertTrue('A progress cancellation must reach the startup owner',LCanceled); + AssertEquals('Canceled decoding must not announce successful loading',0,FChangedCount); + AssertNotificationsRestored; + AssertSourceUnchanged; +end; + +procedure TPendingRestoreTests.LegacyStoredRoundTripWithoutProgress; +var + LError : String; +begin + // Existing callers need neither a callback nor a new serialization format. + AssertTrue(FLoaded.LoadOperationsHashTreeFromStream(FStream,True, + CT_PROTOCOL_6,CT_PROTOCOL_6,Nil,LError)); + AssertEquals('',LError); + AssertEquals(1,FChangedCount); + AssertSameOperations; + AssertSourceUnchanged; +end; + +procedure TPendingRestoreTests.ReportsDecodeStartAndCompletion; +var + LError : String; + LOperationsCount, LProcessedCount : Integer; +begin + AssertTrue(FLoaded.LoadOperationsHashTreeFromStream(FStream,True, + CT_PROTOCOL_6,CT_PROTOCOL_6,Nil,True,LOperationsCount,LProcessedCount, + LError,OnProgress)); + AssertTrue('A fast restore still reports both endpoints',FProgressCount >= 2); + AssertEquals(Int64(0),FFirstCurrent); + AssertEquals(Int64(8),FLastCurrent); + AssertEquals(Int64(8),FLastTotal); + AssertEquals(8,LOperationsCount); + AssertEquals(8,LProcessedCount); + AssertEquals(1,FChangedCount); + AssertSameOperations; + AssertNotificationsRestored; + AssertSourceUnchanged; +end; + +procedure TPendingRestoreTests.CancellationBeforeDecodeRestoresNotifications; +begin + FCancelAt := 0; + LoadAndExpectCancellation; + AssertEquals(0,FLoaded.OperationsCount); + AssertTrue('Change callbacks remain usable after cancellation', + FLoaded.AddOperationToHashTree(FOriginal.GetOperation(0))); + AssertEquals(1,FChangedCount); +end; + +procedure TPendingRestoreTests.CancellationDuringDecodePreservesSource; +begin + FCancelAt := 1; + FDelayFirstProgress := True; + LoadAndExpectCancellation; + AssertTrue('Cancellation should occur after decoding starts',FLoaded.OperationsCount > 0); + AssertTrue('Cancellation should not wait for the whole package', + FLoaded.OperationsCount < FOriginal.OperationsCount); +end; + +procedure TPendingRestoreTests.CanceledDecodeCanBeRetried; +var + LError : String; +begin + FCancelAt := 1; + FDelayFirstProgress := True; + LoadAndExpectCancellation; + FLoaded.ClearHastThree; + FStream.Position := 0; + FCancelAt := -1; + FDelayFirstProgress := False; + FChangedCount := 0; + AssertTrue(FLoaded.LoadOperationsHashTreeFromStream(FStream,True, + CT_PROTOCOL_6,CT_PROTOCOL_6,Nil,LError,OnProgress)); + AssertEquals(1,FChangedCount); + AssertSameOperations; + AssertSourceUnchanged; +end; + +procedure TPendingRestoreTests.EmptySavedTreeReportsCompletion; +var + LError : String; +begin + FOriginal.ClearHastThree; + FStream.Clear; + AssertTrue(FOriginal.SaveOperationsHashTreeToStream(FStream,True)); + AssertEquals(Int64(SizeOf(Cardinal)),FStream.Size); + FStream.Position := 0; + SnapshotSource; + AssertTrue(FLoaded.LoadOperationsHashTreeFromStream(FStream,True, + CT_PROTOCOL_6,CT_PROTOCOL_6,Nil,LError,OnProgress)); + AssertTrue(FProgressCount >= 2); + AssertEquals(Int64(0),FFirstCurrent); + AssertEquals(Int64(0),FLastCurrent); + AssertEquals(Int64(0),FLastTotal); + AssertEquals(0,FLoaded.OperationsCount); + AssertSourceUnchanged; +end; + +procedure TPendingRestoreTests.TruncatedStreamDoesNotReportSuccessfulCompletion; +var + LError : String; +begin + FStream.Size := FStream.Size-1; + FStream.Position := 0; + SnapshotSource; + AssertFalse(FLoaded.LoadOperationsHashTreeFromStream(FStream,True, + CT_PROTOCOL_6,CT_PROTOCOL_6,Nil,LError,OnProgress)); + AssertTrue(LError <> ''); + AssertEquals(Int64(0),FFirstCurrent); + AssertTrue('Invalid data must not report successful completion',FLastCurrent < FLastTotal); + AssertEquals(0,FChangedCount); + AssertNotificationsRestored; + AssertSourceUnchanged; +end; + +procedure TPendingRestoreTests.LegacyStorageOverrideIsPreserved; +var + LDisposables : TDisposables; + LStorage : TPendingStorageOverride; + LCanceled : Boolean; +begin + // No bank or files are needed by the legacy override. The public wrapper + // must dispatch it on callback, exception and ordinary no-callback paths. + LStorage := LDisposables.AddObject(TPendingStorageOverride.Create(nil)) as TPendingStorageOverride; + LStorage.RaiseOnLoad := True; + LCanceled := False; + try + LStorage.LoadPendingBufferOperations(FLoaded,OnProgress); + except + on E : EAbort do LCanceled := True; + end; + AssertTrue(LCanceled); + AssertEquals(1,LStorage.Calls); + AssertEquals('Only the storage override owns its progress reporting',0,FProgressCount); + LStorage.RaiseOnLoad := False; + LStorage.LoadPendingBufferOperations(FLoaded); + AssertEquals(2,LStorage.Calls); +end; + +initialization + RegisterTest(TPendingRestoreTests); +end. diff --git a/src/tests/experimental/startup/UPendingSaveLifecycleTests.pas b/src/tests/experimental/startup/UPendingSaveLifecycleTests.pas new file mode 100644 index 000000000..a722fbec3 --- /dev/null +++ b/src/tests/experimental/startup/UPendingSaveLifecycleTests.pas @@ -0,0 +1,64 @@ +unit UPendingSaveLifecycleTests; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. + FPCUnit delegates to the same actual node/storage checks as the native probe. } +{$IFDEF FPC}{$mode delphi}{$ENDIF} + +interface + +uses SysUtils, fpcunit, testregistry, UPendingSaveChecks; + +type + TPendingSaveLifecycleTests = class(TTestCase) + private + procedure RunCheck(ACheck: TPendingSaveCheck); + published + procedure FinalDirtyOperationIsSavedDuringQuietWaitShutdown; + procedure FailedSaveIsReportedAndRetried; + procedure BankSanitizationRemovalSchedulesSave; + procedure AtomicFileFailuresPreserveCompletePackageAndReopen; + {$IFDEF MSWINDOWS} + procedure FailedFinalPublicationReportsErrorAndExitsWithPreviousPackage; + {$ENDIF} + end; + +implementation + +procedure TPendingSaveLifecycleTests.RunCheck(ACheck: TPendingSaveCheck); +var LID: TGUID; +begin + CreateGUID(LID); + AssertEquals(0,RunPendingSaveCheck(ACheck,IncludeTrailingPathDelimiter(GetTempDir)+ + 'PendingSaveCheck-'+GUIDToString(LID)+'.log')); +end; + +procedure TPendingSaveLifecycleTests.FinalDirtyOperationIsSavedDuringQuietWaitShutdown; +begin + RunCheck(pscFinalDirty); +end; + +procedure TPendingSaveLifecycleTests.FailedSaveIsReportedAndRetried; +begin + RunCheck(pscRetryAfterFailure); +end; + +procedure TPendingSaveLifecycleTests.BankSanitizationRemovalSchedulesSave; +begin + RunCheck(pscSanitizeRemoval); +end; + +procedure TPendingSaveLifecycleTests.AtomicFileFailuresPreserveCompletePackageAndReopen; +begin + RunCheck(pscAtomicStorage); +end; + +{$IFDEF MSWINDOWS} +procedure TPendingSaveLifecycleTests.FailedFinalPublicationReportsErrorAndExitsWithPreviousPackage; +begin + RunCheck(pscFinalPublicationFailure); +end; +{$ENDIF} + +initialization + RegisterTest(TPendingSaveLifecycleTests); +end. diff --git a/src/tests/experimental/startup/UWalletStartupTests.pas b/src/tests/experimental/startup/UWalletStartupTests.pas new file mode 100644 index 000000000..acfdd43d8 --- /dev/null +++ b/src/tests/experimental/startup/UWalletStartupTests.pas @@ -0,0 +1,378 @@ +unit UWalletStartupTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. +} + +{$mode delphi} + +interface + +uses + Classes, SysUtils, SyncObjs, fpcunit, testregistry, UWalletStartup, UWalletStartupTask, UThread, UAccounts; + +type + + TWalletStartupOptionsTests = class(TTestCase) + published + procedure DefaultsPreserveNormalStartup; + procedure ParsesIsolatedOfflineStartup; + procedure ResolvesRelativeDataDirectory; + procedure RejectsEmptyDataDirectory; + procedure RejectsDuplicateDataDirectory; + procedure RejectsUnknownArguments; + procedure HelpDoesNotCreateDataDirectory; + end; + + TWalletStartupTaskTests = class(TTestCase) + private + FProgressCount, FCompletedCount : Integer; + FCallbackThread : TThreadID; + FProgress : TProgressInfo; + procedure OnProgress(Sender : TObject; const AProgress : TProgressInfo); + procedure OnCompleted(Sender : TObject); + procedure WaitUntilFinished(ATask : TWalletStartupTask); + protected + procedure SetUp; override; + published + procedure SuccessfulLoadDispatchesOnMainThread; + procedure FailureIsReportedByCompletion; + procedure CancellationDiscardsQueuedNotifications; + procedure CancellationWaitsForRunningLoad; + procedure CancellationBeforeStartSkipsLoad; + procedure DestructionDiscardsQueuedNotifications; + procedure TaskCannotStartTwice; + end; + +implementation + +type + + TTestStartupTask = class(TWalletStartupTask) + private + FEntered : TEvent; + FShouldBlock, FShouldFail : Boolean; + FLoadCount : Integer; + FWorkerThread : TThreadID; + protected + procedure ExecuteLoad; override; + public + constructor Create(AShouldBlock : Boolean = false; AShouldFail : Boolean = false); + destructor Destroy; override; + end; + +{ TWalletStartupOptionsTests } + +procedure TWalletStartupOptionsTests.DefaultsPreserveNormalStartup; +var + LOptions : TWalletStartupOptions; + LError : String; +begin + AssertTrue(TryParseWalletStartupOptions([], LOptions, LError)); + AssertEquals('', LError); + AssertEquals('', LOptions.DataDirectory); + AssertFalse(LOptions.Offline); + AssertFalse(LOptions.NoStartupDialogs); + AssertFalse(LOptions.ShowHelp); +end; + +procedure TWalletStartupOptionsTests.ParsesIsolatedOfflineStartup; +var + LOptions : TWalletStartupOptions; + LError, LDirectory : String; +begin + LDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'PascalCoin test directory'; + AssertTrue(TryParseWalletStartupOptions(['--data-dir=' + LDirectory, '--offline', + '--no-startup-dialogs'], LOptions, LError)); + AssertEquals(ExpandFileName(LDirectory), LOptions.DataDirectory); + AssertTrue(LOptions.Offline); + AssertTrue(LOptions.NoStartupDialogs); + AssertFalse(LOptions.ShowHelp); +end; + +procedure TWalletStartupOptionsTests.ResolvesRelativeDataDirectory; +var + LOptions : TWalletStartupOptions; + LError : String; +begin + AssertTrue(TryParseWalletStartupOptions(['--data-dir=wallet-test-data'], LOptions, LError)); + AssertEquals(ExpandFileName('wallet-test-data'), LOptions.DataDirectory); +end; + +procedure TWalletStartupOptionsTests.RejectsEmptyDataDirectory; +var + LOptions : TWalletStartupOptions; + LError : String; +begin + AssertFalse(TryParseWalletStartupOptions(['--data-dir='], LOptions, LError)); + AssertTrue(Pos('non-empty', LError) > 0); + AssertFalse(TryParseWalletStartupOptions(['--data-dir= '], LOptions, LError)); +end; + +procedure TWalletStartupOptionsTests.RejectsDuplicateDataDirectory; +var + LOptions : TWalletStartupOptions; + LError : String; +begin + AssertFalse(TryParseWalletStartupOptions(['--data-dir=first', '--data-dir=second'], LOptions, LError)); + AssertTrue(Pos('only once', LError) > 0); +end; + +procedure TWalletStartupOptionsTests.RejectsUnknownArguments; +var + LOptions : TWalletStartupOptions; + LError : String; +begin + AssertFalse(TryParseWalletStartupOptions(['--offine'], LOptions, LError)); + AssertTrue(Pos('--offine', LError) > 0); + AssertFalse(TryParseWalletStartupOptions(['--offline=true'], LOptions, LError)); + AssertFalse(TryParseWalletStartupOptions(['unexpected.dat'], LOptions, LError)); +end; + +procedure TWalletStartupOptionsTests.HelpDoesNotCreateDataDirectory; +var + LOptions : TWalletStartupOptions; + LError, LDirectory : String; + LId : TGUID; +begin + CreateGUID(LId); + LDirectory := IncludeTrailingPathDelimiter(GetTempDir) + GUIDToString(LId); + AssertFalse(DirectoryExists(LDirectory)); + AssertTrue(TryParseWalletStartupOptions(['--help', '--data-dir=' + LDirectory], LOptions, LError)); + AssertTrue(LOptions.ShowHelp); + AssertFalse(DirectoryExists(LDirectory)); + AssertTrue(Pos('--offline', WalletStartupHelp) > 0); + AssertTrue(TryParseWalletStartupOptions(['-h'], LOptions, LError)); + AssertTrue(LOptions.ShowHelp); +end; + +{ TTestStartupTask } + +constructor TTestStartupTask.Create(AShouldBlock : Boolean; AShouldFail : Boolean); +begin + inherited Create; + FEntered := TEvent.Create(nil, true, false, ''); + FShouldBlock := AShouldBlock; + FShouldFail := AShouldFail; +end; + +destructor TTestStartupTask.Destroy; +begin + CancelAndWait; + FEntered.Free; + inherited; +end; + +procedure TTestStartupTask.ExecuteLoad; +var LProgress : TProgressInfo; +begin + FWorkerThread := GetCurrentThreadID; + Inc(FLoadCount); + FEntered.SetEvent; + while FShouldBlock and (not Cancelled) do + Sleep(1); + CheckCancelled; + ReportProgress(TProgressInfo.Create(psReadingBlockchain,5,10)); + LProgress := TProgressInfo.Create(psFinalizingRestore,10,10); + LProgress.Context := pcCheckingNewSafebox; + LProgress.BlockNumber := 42; + LProgress.BlockCount := 84; + LProgress.StartBlock := 7; + LProgress.EndBlock := 13; + LProgress.OperationsCount := 17; + LProgress.ChunksCount := 2; + LProgress.FileName := 'generated-checkpoint'; + LProgress.PeerAddress := '127.0.0.1'; + ReportProgress(LProgress); + if FShouldFail then + raise Exception.Create('test restore failure'); +end; + +{ TWalletStartupTaskTests } + +procedure TWalletStartupTaskTests.SetUp; +begin + inherited; + FProgressCount := 0; + FCompletedCount := 0; + FCallbackThread := 0; + FProgress := TProgressInfo.Create(psNone); +end; + +procedure TWalletStartupTaskTests.OnProgress(Sender : TObject; const AProgress : TProgressInfo); +begin + Inc(FProgressCount); + FCallbackThread := GetCurrentThreadID; + FProgress := AProgress; +end; + +procedure TWalletStartupTaskTests.OnCompleted(Sender : TObject); +begin + Inc(FCompletedCount); + FCallbackThread := GetCurrentThreadID; +end; + +procedure TWalletStartupTaskTests.WaitUntilFinished(ATask : TWalletStartupTask); +var + LDeadline : QWord; +begin + LDeadline := GetTickCount64 + 3000; + while (not ATask.Finished) and (GetTickCount64 < LDeadline) do + Sleep(1); + AssertTrue('Startup task did not finish', ATask.Finished); +end; + +procedure TWalletStartupTaskTests.SuccessfulLoadDispatchesOnMainThread; +var + LTask : TTestStartupTask; +begin + LTask := TTestStartupTask.Create; + try + LTask.OnProgress := OnProgress; + LTask.OnCompleted := OnCompleted; + LTask.Start; + WaitUntilFinished(LTask); + AssertEquals(0, FCompletedCount); + AssertEquals(0, FProgressCount); + CheckSynchronize; + AssertEquals(1, FCompletedCount); + AssertEquals('Repeated progress is coalesced', 1, FProgressCount); + AssertTrue('The latest typed stage is retained',FProgress.Stage = psFinalizingRestore); + AssertTrue('The raw context is retained',FProgress.Context = pcCheckingNewSafebox); + AssertEquals(Int64(10), FProgress.CurrentPosition); + AssertEquals(Int64(10), FProgress.TotalCount); + AssertEquals(Int64(42), FProgress.BlockNumber); + AssertEquals(Int64(84), FProgress.BlockCount); + AssertEquals(Int64(7), FProgress.StartBlock); + AssertEquals(Int64(13), FProgress.EndBlock); + AssertEquals(Int64(17), FProgress.OperationsCount); + AssertEquals(Int64(2), FProgress.ChunksCount); + AssertEquals('generated-checkpoint',FProgress.FileName); + AssertEquals('127.0.0.1',FProgress.PeerAddress); + AssertEquals(QWord(GetCurrentThreadID), QWord(FCallbackThread)); + AssertTrue(LTask.FWorkerThread <> GetCurrentThreadID); + AssertEquals('', LTask.ErrorMessage); + AssertEquals(1, LTask.FLoadCount); + finally + LTask.Free; + end; +end; + +procedure TWalletStartupTaskTests.FailureIsReportedByCompletion; +var + LTask : TTestStartupTask; +begin + LTask := TTestStartupTask.Create(false, true); + try + LTask.OnCompleted := OnCompleted; + LTask.Start; + WaitUntilFinished(LTask); + CheckSynchronize; + AssertEquals(1, FCompletedCount); + AssertTrue(Pos('test restore failure', LTask.ErrorMessage) > 0); + AssertEquals(QWord(GetCurrentThreadID), QWord(FCallbackThread)); + finally + LTask.Free; + end; +end; + +procedure TWalletStartupTaskTests.CancellationDiscardsQueuedNotifications; +var + LTask : TTestStartupTask; +begin + LTask := TTestStartupTask.Create; + try + LTask.OnProgress := OnProgress; + LTask.OnCompleted := OnCompleted; + LTask.Start; + WaitUntilFinished(LTask); + LTask.CancelAndWait; + CheckSynchronize; + AssertEquals(0, FProgressCount); + AssertEquals(0, FCompletedCount); + AssertTrue(LTask.Cancelled); + finally + LTask.Free; + end; +end; + +procedure TWalletStartupTaskTests.CancellationWaitsForRunningLoad; +var + LTask : TTestStartupTask; +begin + LTask := TTestStartupTask.Create(true); + try + LTask.OnCompleted := OnCompleted; + LTask.Start; + AssertEquals(Ord(wrSignaled), Ord(LTask.FEntered.WaitFor(3000))); + AssertTrue('Startup work is tracked by the node thread infrastructure', + TPCThread.GetThreadByClass(TTestStartupTask, nil) = LTask); + LTask.CancelAndWait; + AssertTrue(LTask.Finished); + AssertNull(TPCThread.GetThreadByClass(TTestStartupTask, nil)); + CheckSynchronize; + AssertEquals(1, LTask.FLoadCount); + AssertEquals(0, FCompletedCount); + AssertEquals('', LTask.ErrorMessage); + finally + LTask.Free; + end; +end; + +procedure TWalletStartupTaskTests.CancellationBeforeStartSkipsLoad; +var + LTask : TTestStartupTask; +begin + LTask := TTestStartupTask.Create; + try + LTask.CancelAndWait; + AssertTrue(LTask.Finished); + AssertEquals(0, LTask.FLoadCount); + AssertEquals('', LTask.ErrorMessage); + finally + LTask.Free; + end; +end; + +procedure TWalletStartupTaskTests.DestructionDiscardsQueuedNotifications; +var + LTask : TTestStartupTask; +begin + LTask := TTestStartupTask.Create; + LTask.OnProgress := OnProgress; + LTask.OnCompleted := OnCompleted; + LTask.Start; + WaitUntilFinished(LTask); + LTask.Free; + CheckSynchronize; + AssertEquals(0, FProgressCount); + AssertEquals(0, FCompletedCount); +end; + +procedure TWalletStartupTaskTests.TaskCannotStartTwice; +var + LTask : TTestStartupTask; + LRaised : Boolean; +begin + LTask := TTestStartupTask.Create; + try + LTask.Start; + LRaised := false; + try + LTask.Start; + except + on E : EThread do LRaised := true; + end; + AssertTrue(LRaised); + finally + LTask.Free; + end; +end; + +initialization + RegisterTest(TWalletStartupOptionsTests); + RegisterTest(TWalletStartupTaskTests); + +end. diff --git a/src/tests/experimental/ui/LanguageTestForm.lfm b/src/tests/experimental/ui/LanguageTestForm.lfm new file mode 100644 index 000000000..8784ff48d --- /dev/null +++ b/src/tests/experimental/ui/LanguageTestForm.lfm @@ -0,0 +1,6 @@ +object LanguageTestForm: TLanguageTestForm + Caption = 'Options' + Width = 320 + Height = 160 + Visible = False +end diff --git a/src/tests/experimental/ui/UWalletAccountEnumerationTests.pas b/src/tests/experimental/ui/UWalletAccountEnumerationTests.pas new file mode 100644 index 000000000..bc01219fa --- /dev/null +++ b/src/tests/experimental/ui/UWalletAccountEnumerationTests.pas @@ -0,0 +1,238 @@ +unit UWalletAccountEnumerationTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, Generics.Collections, fpcunit, testregistry, + UAbstractMem, UAbstractBTree, UAbstractMemBTree, UFileMem, + UPCAbstractMemAccountKeys; + +type + TCountingAccounts = class(TAccountsUsingThisKey) + public + NodeReads : Integer; + function GetNode(AIdentify : TAbstractMemPosition) : + TAbstractBTree<TAbstractMemPosition,TAbstractMemPosition>.TAbstractBTreeNode; override; + end; + + TWalletAccountEnumerationTests = class(TTestCase) + private + FDirectory, FFileName : String; + FMemory : TFileMem; + FTree : TCountingAccounts; + FZone : TAMZone; + FValues : TList<Cardinal>; + procedure AddRange(ACount : Integer); + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure EmptyAndZeroCountClearDestination; + procedure BoundedRangeReturnsExactlyRequestedCount; + procedure CardinalValuesRetainUnsignedRange; + procedure RejectsInvalidAccountNumber; + procedure LargePersistedSnapshotUsesLinearTraversal; + procedure WalletAccountNumbersAreSortedAndUnique; + end; + +implementation + +uses UCrypto, UAccounts, UBlockChain, UWallet, UNode, UConst, UPCDataTypes, + UCoreUtils; + +type + TEnumerationSafeBox = class(TPCSafeBox) + public + procedure Seed(const AFirstKey, ASecondKey : TAccountKey; ABlocks : Integer); + end; + +function TCountingAccounts.GetNode(AIdentify : TAbstractMemPosition) : + TAbstractBTree<TAbstractMemPosition,TAbstractMemPosition>.TAbstractBTreeNode; +begin + Inc(NodeReads); + Result := inherited GetNode(AIdentify); +end; + +procedure TEnumerationSafeBox.Seed(const AFirstKey, ASecondKey : TAccountKey; ABlocks : Integer); +var LBlock : TOperationBlock; + i : Integer; +begin + for i := 0 to ABlocks-1 do begin + LBlock := CT_OperationBlock_NUL; + LBlock.block := i; + if i mod 2=0 then LBlock.account_key := AFirstKey + else LBlock.account_key := ASecondKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + AddNew(LBlock); + end; +end; + +procedure TWalletAccountEnumerationTests.SetUp; +var LID : TGUID; +begin + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'WalletEnumeration-' + GUIDToString(LID); + ForceDirectories(FDirectory); + FFileName := IncludeTrailingPathDelimiter(FDirectory) + 'accounts.am'; + FMemory := TFileMem.Create(FFileName,False); + FMemory.Initialize(True,4); + FZone := FMemory.New(TAbstractMemBTree.MinAbstractMemInitialPositionSize(FMemory)); + FTree := TCountingAccounts.Create(FMemory,FZone,False); + FValues := TList<Cardinal>.Create; +end; + +procedure TWalletAccountEnumerationTests.TearDown; +begin + FValues.Free; + FTree.Free; + FMemory.Free; + DeleteFile(FFileName); + DeleteFile(IncludeTrailingPathDelimiter(FDirectory) + 'WalletKeys.dat'); + RemoveDir(FDirectory); +end; + +procedure TWalletAccountEnumerationTests.AddRange(ACount : Integer); +var i : Integer; +begin + for i := ACount-1 downto 0 do + AssertTrue(FTree.Add(i*3+100)); +end; + +procedure TWalletAccountEnumerationTests.EmptyAndZeroCountClearDestination; +begin + FValues.Add(99); + AssertEquals(0,FTree.FillList(0,10,FValues)); + AssertEquals(0,FValues.Count); + AddRange(5); + FValues.Add(99); + AssertEquals(0,FTree.FillList(0,0,FValues)); + AssertEquals(0,FValues.Count); +end; + +procedure TWalletAccountEnumerationTests.BoundedRangeReturnsExactlyRequestedCount; +begin + AddRange(12); + FValues.Add(99); + AssertEquals(1,FTree.FillList(3,1,FValues)); + AssertEquals(109,Integer(FValues[0])); + AssertEquals(4,FTree.FillList(2,4,FValues)); + AssertEquals(4,FValues.Count); + AssertEquals(106,Integer(FValues[0])); + AssertEquals(115,Integer(FValues[3])); + AssertEquals(2,FTree.FillList(10,9,FValues)); + AssertEquals(130,Integer(FValues[0])); + AssertEquals(133,Integer(FValues[1])); + AssertEquals(0,FTree.FillList(12,1,FValues)); + AssertEquals(0,FValues.Count); +end; + +procedure TWalletAccountEnumerationTests.CardinalValuesRetainUnsignedRange; +begin + AssertTrue(FTree.Add(Int64(High(Cardinal))-2)); + AssertTrue(FTree.Add(Int64(High(Cardinal)))); + AssertEquals(2,FTree.FillList(0,FTree.Count,FValues)); + AssertEquals(Int64(High(Cardinal))-2,Int64(FValues[0])); + AssertEquals(Int64(High(Cardinal)),Int64(FValues[1])); +end; + +procedure TWalletAccountEnumerationTests.RejectsInvalidAccountNumber; +var LRaised : Boolean; +begin + AssertTrue(FTree.Add(Int64(High(Cardinal))+1)); + LRaised := False; + try + FTree.FillList(0,1,FValues); + except + on EPCAbstractMemAccountKeys do LRaised := True; + end; + AssertTrue('An invalid persisted account number must not truncate',LRaised); +end; + +procedure TWalletAccountEnumerationTests.LargePersistedSnapshotUsesLinearTraversal; +const LCount = 8192; +var i : Integer; +begin + AddRange(LCount); + FreeAndNil(FTree); + AssertTrue(FMemory.FlushCache); + FreeAndNil(FMemory); + FMemory := TFileMem.Create(FFileName,False); + FTree := TCountingAccounts.Create(FMemory,FZone,False); + AssertEquals(LCount,FTree.Count); + FTree.NodeReads := 0; + AssertEquals(LCount,FTree.FillList(0,FTree.Count,FValues)); + AssertEquals(LCount,FValues.Count); + for i := 0 to LCount-1 do + AssertEquals(i*3+100,Integer(FValues[i])); + AssertTrue('A complete snapshot must not restart at the B-tree root for every index', + FTree.NodeReads < LCount*4); +end; + +procedure TWalletAccountEnumerationTests.WalletAccountNumbersAreSortedAndUnique; +var LFirst, LSecond : TECPrivateKey; + LSafeBox : TEnumerationSafeBox; + LNumbers : TArray<Cardinal>; + LCountedFirst, LCountedSecond : TCountingAccounts; + i : Integer; + + function CountTree(const AKey : TAccountKey) : TCountingAccounts; + var LKeys : TPCAbstractMemAccountKeys; + LSearch, LEntry : TAccountKeyByPosition; + LInitialZone : TAMZone; + begin + LKeys := LSafeBox.PCAbstractMem.AccountKeys; + LKeys.GetAccountsUsingThisKey(AKey); + LSearch.Clear; + LSearch.position := LKeys.GetPositionOfKey(AKey,False); + AssertTrue(LKeys.AccountKeyByPositionCache.Find(LSearch,LEntry)); + LInitialZone := LEntry.accountsUsingThisKey.InitialZone; + LKeys.AccountKeyByPositionCache.Remove(LEntry); + Result := TCountingAccounts.Create(LSafeBox.PCAbstractMem.AbstractMem,LInitialZone,False); + LEntry.accountsUsingThisKey := Result; + LKeys.AccountKeyByPositionCache.Add(LEntry); // Existing cache owns the wrapper. + Result.NodeReads := 0; + end; + +begin + TCrypto.InitCrypto; + LFirst := TECPrivateKey.Create; + LSecond := TECPrivateKey.Create; + LSafeBox := nil; + TNode.SetPascalCoinDataFolder(FDirectory); + TWallet.Load; + try + LFirst.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LSecond.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LSafeBox := TEnumerationSafeBox.Create; + LSafeBox.Seed(LFirst.PublicKey,LSecond.PublicKey,128); + TWallet.Keys.SafeBox := LSafeBox; + TWallet.Keys.AddPublicKey('Second generated key',LSecond.PublicKey); + TWallet.Keys.AddPublicKey('First generated key',LFirst.PublicKey); + TWallet.Keys.AddPublicKey('Repeated generated key',LFirst.PublicKey); + LCountedFirst := CountTree(LFirst.PublicKey); + LCountedSecond := CountTree(LSecond.PublicKey); + LNumbers := TCoreTool.GetUserAccountNumbers; + AssertEquals(128*CT_AccountsPerBlock,Length(LNumbers)); + for i := 0 to High(LNumbers) do AssertEquals(i,Integer(LNumbers[i])); + AssertTrue('Wallet enumeration must use the persistent bulk cursor, not indexed Get', + LCountedFirst.NodeReads + LCountedSecond.NodeReads < Length(LNumbers)*4); + finally + TWallet.Unload; + LSafeBox.Free; + LSecond.Free; + LFirst.Free; + TNode.SetPascalCoinDataFolder(''); + end; +end; + +initialization + RegisterTest(TWalletAccountEnumerationTests); +end. diff --git a/src/tests/experimental/ui/UWalletExplorerSchedulingTests.pas b/src/tests/experimental/ui/UWalletExplorerSchedulingTests.pas new file mode 100644 index 000000000..5bc89c0c9 --- /dev/null +++ b/src/tests/experimental/ui/UWalletExplorerSchedulingTests.pas @@ -0,0 +1,337 @@ +unit UWalletExplorerSchedulingTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license, see the accompanying LICENSE. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, fpcunit, testregistry; + +type + TWalletExplorerSchedulingTests = class(TTestCase) + private + procedure CheckProducerBurst(ADeferredLeading: Boolean); + procedure CheckIndependentDeadline(AQueuedNodeInput: Boolean); + published + procedure DeferredLeadingSurvivesBusyViewAndQuietTimer; + procedure DeferredLeadingResumesOnShowWithoutPolling; + procedure ContinuousRequestsResetTheOwnedQuietTimer; + procedure UnrelatedNodeWorkCannotPostponeQuietRefreshRequests; + procedure QueuedNodeCallbackStartsANewBurstAfterOwnedDeadline; + end; + +implementation + +uses Windows, Forms, ExtCtrls, UThread, UNode, UCrypto, UCommon, UCommon.UI, + UFRMAccountExplorer; + +type + TExplorerScheduler = class(TFRMAccountExplorer) + private + FNotifier: TNodeNotifyEvents; + procedure OnNodeChanged(Sender: TObject); + protected + function CanRefreshBackend: Boolean; override; + procedure RefreshBackend; override; + public + Ready, AcceptNodeRequests: Boolean; + RefreshCount, NodeCallbackCount: Integer; + LastRefreshTick, LastRequestTick: QWord; + constructor Create(ANode: TNode); + destructor Destroy; override; + procedure ReadyChanged; + procedure Request; + function ThrottleActive: Boolean; + procedure PumpFor(AMilliseconds: Cardinal); + procedure DeliverTimerBeforeQueuedNodeCallback; + end; + + TExplorerProducer = class(TPCThread) + private + FNode: TNode; + FStop: LongInt; + protected + procedure BCExecute; override; + public + LastProducedTick: QWord; + constructor Create(ANode: TNode); + procedure Stop; + end; + +const CT_INTERVAL = 200; + +constructor TExplorerScheduler.Create(ANode: TNode); +begin + // Control view readiness and rendering, keeping the actual Explorer scheduler + // and real core notifications. No wallet keys or chain records are loaded. + inherited CreateNew(nil); + Ready := True; + AcceptNodeRequests := True; + FNotifier := TNodeNotifyEvents.Create(Self); + FNotifier.Node := ANode; + FNotifier.OnBlocksChanged := OnNodeChanged; + InitializeBackendRefresh; + BackendRefreshEvent.Interval := TTimeSpan.FromMilliseconds(CT_INTERVAL); +end; + +destructor TExplorerScheduler.Destroy; +begin + Ready := False; + FNotifier.OnBlocksChanged := nil; + FreeAndNil(FNotifier); + inherited; +end; + +function TExplorerScheduler.CanRefreshBackend: Boolean; +begin + Result := Ready; +end; + +procedure TExplorerScheduler.RefreshBackend; +begin + Inc(RefreshCount); + LastRefreshTick := GetTickCount64; +end; + +procedure TExplorerScheduler.OnNodeChanged(Sender: TObject); +begin + Inc(NodeCallbackCount); + if AcceptNodeRequests then Request; +end; + +procedure TExplorerScheduler.Request; +begin + LastRequestTick := GetTickCount64; + ScheduleBackendRefresh; +end; + +function TExplorerScheduler.ThrottleActive: Boolean; +begin + Result := BackendRefreshEvent.BurstActive; +end; + +procedure TExplorerScheduler.ReadyChanged; +begin + OnViewReady(Self); +end; + +procedure TExplorerScheduler.PumpFor(AMilliseconds: Cardinal); +var LDeadline: QWord; +begin + LDeadline := GetTickCount64+AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64>=LDeadline; +end; + +procedure TExplorerScheduler.DeliverTimerBeforeQueuedNodeCallback; +var I: Integer; LTimer: TTimer; +begin + for I := 0 to BackendRefreshEvent.ComponentCount-1 do + if BackendRefreshEvent.Components[I] is TTimer then begin + LTimer := TTimer(BackendRefreshEvent.Components[I]); + if not LTimer.Enabled then raise Exception.Create('Expected an armed Explorer timer'); + LTimer.OnTimer(LTimer); + Exit; + end; + raise Exception.Create('Explorer throttle has no owned timer'); +end; + +constructor TExplorerProducer.Create(ANode: TNode); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; +end; + +procedure TExplorerProducer.BCExecute; +begin + while InterlockedCompareExchange(FStop,0,0)=0 do begin + FNode.NotifyBlocksChanged; + LastProducedTick := GetTickCount64; + Sleep(10); + end; +end; + +procedure TExplorerProducer.Stop; +begin + InterlockedExchange(FStop,1); + WaitFor; +end; + +procedure TWalletExplorerSchedulingTests.CheckProducerBurst(ADeferredLeading: Boolean); +var + LNode: TNode; + LExplorer: TExplorerScheduler; + LProducer: TExplorerProducer; + LPreviousDirectory, LDirectory: String; + LID: TGUID; + LDeadline: QWord; +begin + AssertFalse('The fixture must not reuse a global node',TNode.NodeExists); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetTempDir)+'ExplorerSchedule-'+GUIDToString(LID); + ForceDirectories(LDirectory); + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(LDirectory); + LNode := nil; + LExplorer := nil; + LProducer := nil; + try + TCrypto.InitCrypto; + LNode := TNode.Create(nil); + LNode.BroadcastData := False; + LExplorer := TExplorerScheduler.Create(LNode); + LExplorer.Ready := not ADeferredLeading; + LNode.NotifyBlocksChanged; + LDeadline := GetTickCount64+2500; + while (LExplorer.NodeCallbackCount=0) and (GetTickCount64<LDeadline) do + LExplorer.PumpFor(2); + AssertEquals('The real node must deliver the initial input',1,LExplorer.NodeCallbackCount); + if ADeferredLeading then AssertEquals(0,LExplorer.RefreshCount) + else AssertEquals('An eligible Explorer leading request is immediate',1,LExplorer.RefreshCount); + + LProducer := TExplorerProducer.Create(LNode); + LProducer.Start; + LDeadline := GetTickCount64+2500; + while (LExplorer.NodeCallbackCount<2) and (GetTickCount64<LDeadline) do + LExplorer.PumpFor(2); + AssertTrue('Producer callbacks reached the actual Explorer scheduler',LExplorer.NodeCallbackCount>=2); + if ADeferredLeading then begin + LExplorer.Ready := True; + LExplorer.ReadyChanged; + AssertEquals('A busy view retains its one ready leading request',1,LExplorer.RefreshCount); + end; + + LExplorer.PumpFor(500); + AssertEquals('Continuing actual refresh inputs must not cause periodic publications',1,LExplorer.RefreshCount); + LProducer.Stop; + LDeadline := GetTickCount64+2500; + while (LExplorer.RefreshCount<2) and (GetTickCount64<LDeadline) do + LExplorer.PumpFor(2); + AssertEquals('The settled burst delivers its one final refresh',2,LExplorer.RefreshCount); + AssertTrue('The final Explorer refresh follows the last input to its own throttle', + LExplorer.LastRefreshTick-LExplorer.LastRequestTick>=CT_INTERVAL-20); + LExplorer.PumpFor(CT_INTERVAL+50); + AssertEquals('There is no duplicate settled refresh',2,LExplorer.RefreshCount); + finally + if Assigned(LProducer) then begin + LProducer.Stop; + LProducer.Free; + end; + LExplorer.Free; + LNode.Free; + TNode.SetPascalCoinDataFolder(LPreviousDirectory); + RemoveDir(LDirectory); + end; +end; + +procedure TWalletExplorerSchedulingTests.DeferredLeadingSurvivesBusyViewAndQuietTimer; +begin + CheckProducerBurst(True); +end; + +procedure TWalletExplorerSchedulingTests.DeferredLeadingResumesOnShowWithoutPolling; +var LExplorer: TExplorerScheduler; +begin + LExplorer := TExplorerScheduler.Create(nil); + try + LExplorer.Ready := False; + LExplorer.Request; + AssertEquals('The hidden busy view retains its admitted input', 0, LExplorer.RefreshCount); + LExplorer.Ready := True; + LExplorer.Show; + AssertEquals('Actual DoShow releases admitted work without a poll', 1, LExplorer.RefreshCount); + LExplorer.PumpFor(CT_INTERVAL + 50); + AssertEquals('Visibility did not manufacture another input', 1, LExplorer.RefreshCount); + finally + LExplorer.Free; + end; +end; + +procedure TWalletExplorerSchedulingTests.ContinuousRequestsResetTheOwnedQuietTimer; +begin + CheckProducerBurst(False); +end; + +procedure TWalletExplorerSchedulingTests.CheckIndependentDeadline(AQueuedNodeInput: Boolean); +var LNode: TNode; LExplorer: TExplorerScheduler; LProducer: TExplorerProducer; + LPreviousDirectory, LDirectory: String; LID: TGUID; +begin + AssertFalse('The fixture must not reuse a global node',TNode.NodeExists); + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetTempDir)+'ExplorerOwnDeadline-'+GUIDToString(LID); + ForceDirectories(LDirectory); + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(LDirectory); + LNode := nil; + LExplorer := nil; + LProducer := nil; + try + TCrypto.InitCrypto; + LNode := TNode.Create(nil); + LNode.BroadcastData := False; + LExplorer := TExplorerScheduler.Create(LNode); + LExplorer.AcceptNodeRequests := AQueuedNodeInput; + LNode.BeginBlockChainUpdate; + try + LExplorer.Request; + LExplorer.Request; + AssertEquals('The first own input delivers immediately',1,LExplorer.RefreshCount); + LProducer := TExplorerProducer.Create(LNode); + LProducer.Start; + if AQueuedNodeInput then begin + // An external notification that has not reached Request is not an + // input to the throttle. A due native timer may run before it. + Sleep(CT_INTERVAL+100); + AssertEquals('Core callbacks are queued until the UI pumps',0,LExplorer.NodeCallbackCount); + LExplorer.DeliverTimerBeforeQueuedNodeCallback; + end else + // Observe real producer timestamps without translating those + // unrelated notifications into refresh requests for this view. + LExplorer.PumpFor(CT_INTERVAL+200); + AssertTrue('Core work remains active independently',LNode.IsUpdatingBlockChain); + AssertEquals('Core state cannot postpone an eligible own quiet refresh',2,LExplorer.RefreshCount); + AssertFalse('Only the component input deadline controls completion',LExplorer.ThrottleActive); + LProducer.Stop; + if AQueuedNodeInput then begin + LExplorer.PumpFor(2); + AssertTrue('The queued real callback was delivered',LExplorer.NodeCallbackCount>0); + AssertEquals('Its later request begins a new immediate burst',3,LExplorer.RefreshCount); + end else + AssertTrue('Unrelated real callbacks were delivered',LExplorer.NodeCallbackCount>0); + LExplorer.PumpFor(CT_INTERVAL+100); + AssertEquals('No duplicate final request is manufactured', + 2+Ord(AQueuedNodeInput),LExplorer.RefreshCount); + finally + if Assigned(LProducer) then begin + LProducer.Stop; + FreeAndNil(LProducer); + end; + LNode.EndBlockChainUpdate; + end; + finally + LExplorer.Free; + LNode.Free; + TNode.SetPascalCoinDataFolder(LPreviousDirectory); + RemoveDir(LDirectory); + end; +end; + +procedure TWalletExplorerSchedulingTests.UnrelatedNodeWorkCannotPostponeQuietRefreshRequests; +begin + CheckIndependentDeadline(False); +end; + +procedure TWalletExplorerSchedulingTests.QueuedNodeCallbackStartsANewBurstAfterOwnedDeadline; +begin + CheckIndependentDeadline(True); +end; + +initialization + RegisterTest(TWalletExplorerSchedulingTests); +end. diff --git a/src/tests/experimental/ui/UWalletExplorerTests.pas b/src/tests/experimental/ui/UWalletExplorerTests.pas new file mode 100644 index 000000000..267730dbe --- /dev/null +++ b/src/tests/experimental/ui/UWalletExplorerTests.pas @@ -0,0 +1,1385 @@ +unit UWalletExplorerTests; + +{ Copyright (c) 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. + Generated snapshots and gated in-memory reads never connect to peers. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, SyncObjs, Forms, Generics.Collections, fpcunit, + testregistry, UExplorerAccountSnapshots, UPCDataTypes, UGridUtils, UNode, + UBlockChain, UFileStorage; + +type + TExplorerSnapshotFixture = class(TExplorerAccountSnapshots) + private + FLock: TCriticalSection; + FBaseBalance: Int64; + FBlockRead, FIgnoreCancellation, FReadOnMain, FCancelled, FFailRead: Boolean; + FCalls: Integer; + FRequests: TArray<Cardinal>; + protected + function ReadSnapshot(const AAccounts: TArray<Cardinal>; + out ARows: TArray<TAccount>; out AAccountCount, ABlockCount: Integer; + out ABalance: Int64): Boolean; override; + public + Entered, ReleaseRead: TEvent; + constructor Create; + destructor Destroy; override; + procedure Configure(ABaseBalance: Int64; ABlock, AIgnoreCancellation: Boolean); + procedure BlockNext(ABaseBalance: Int64; AIgnoreCancellation: Boolean = False); + procedure BlockNextError; + function Calls: Integer; + function Requests: TArray<Cardinal>; + function ReadOnMain: Boolean; + function Cancelled: Boolean; + end; + + TExplorerOperationStorage = class(TFileStorage) + private + FCalls, FLastAccount, FCancelled: LongInt; + protected + function DoGetAccountOperations(AAccount, AMaxDepth, AStartOperation, + AMaxOperations, ASearchBackwardsStartingAtBlock: Integer; + const AOperationsResumeList: TOperationsResumeList): Boolean; override; + public + Entered, ReleaseRead: TEvent; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + function Calls: Integer; + function LastAccount: Integer; + function Cancelled: Boolean; + end; + + TExplorerAccountsGrid = class(TAccountsGrid) + public + function Read(AAccountNumber: Integer; out AAccount: TAccount; + out ABlockCount, AAccountCount: Integer): Boolean; + end; + + TWalletExplorerTests = class(TTestCase) + private + FSource: TExplorerSnapshotFixture; + FObserved: TList<Int64>; + FWrongCallbackThread: Boolean; + FCompletionCount: Integer; + FRefreshStateCount: Integer; + FRefreshStateWasBusy: Boolean; + FNode: TNode; + FOperations: TOperationsGrid; + FStorage: TExplorerOperationStorage; + FAccounts: TExplorerAccountsGrid; + FPreviousDirectory, FDirectory: String; + FOwnsNetData: Boolean; + FReadReady: Boolean; + FReadCalls, FSummaryCalls: Integer; + FFilterEntered, FFilterRelease, FFilterExited: TEvent; + FIndexRelease: TEvent; + FProcessedAdds: LongInt; + FReleasedFilterOnMain: Boolean; + procedure FilterProgress(Sender: TObject); + procedure ReleaseFilter; + procedure ReleaseIndex; + procedure SelectedBalanceUpdated(Sender: TObject); + procedure NewSelectedAccounts(ABlocks: Integer); + procedure PumpSelectedAccounts; + procedure Updated(Sender: TObject); + procedure SnapshotCompleted(Sender: TObject); + procedure AccountRefreshStateChanged(Sender: TObject); + procedure NewSnapshots; + procedure PumpUntilCalls(ACount: Integer); + procedure PumpUntilIdle; + procedure PumpOperations(AAccount: Cardinal); + procedure NewOperations; + function ReadAccount(Sender: TObject; AAccountNumber: Integer; + out AAccount: TAccount; out ABlockCount, AAccountCount: Integer): Boolean; + function ReadSummary(Sender: TObject; out AAccountCount: Integer; + out ABalance: Int64): Boolean; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure BlockedReadLeavesCachedLookupsResponsive; + procedure PreviousRowsRemainUntilReplacementIsReady; + procedure StaleGenerationCannotPublish; + procedure FailedSnapshotCompletesWithoutPublishingOrRetrying; + procedure VisibleReadRequestsAreBounded; + procedure StopCancelsBlockedReadWithoutPublishing; + procedure LegacyOperationsCoalesceWithoutWaitingOnUI; + procedure LegacyOperationsDestructionJoinsCancelledRead; + procedure LegacyAccountReadersUseSnapshotCallbacks; + procedure RetiredAccountFiltersRemainBoundedAndJoinBeforeNodeHandoff; + procedure RetiredFilterToNodePublishesIdleAfterEarlierSnapshotCompletion; + procedure StoredHistoryCancellationStopsAfterTheCurrentBlockRead; + procedure SelectedTotalsReadMoreThanTheRowCacheOffTheUIThread; + procedure SelectedTotalsRetainCompletedValueAndCancelObsoleteRequests; + procedure CapturedKeySelectionDoesNotWaitForIndexAndJoinsCancelledResolver; + procedure CapturedMissingKeyIsEmptyAndLegacyAllKeysRemainAvailable; + procedure CapturedKeyCancellationStopsInsideTheRecordChunk; + end; + +implementation + +uses UAccounts, UConst, UCrypto, UNetProtocol, UThread, UPCOrderedLists; + +type + TExplorerSafeBoxAccess = class(TPCSafeBox) + public + procedure SeedGenesis(const AKey: TAccountKey); + procedure SeedBlocks(const AKey: TAccountKey; ACount: Integer; AReward: Int64); + end; + + TSelectedSafeBoxHolder = class(TThread) + private + FSafeBox: TPCSafeBox; + protected + procedure Execute; override; + public + Entered, ReleaseRead: TEvent; + TimedOut: Boolean; + constructor Create(ASafeBox: TPCSafeBox); + destructor Destroy; override; + end; + + TAccountKeyIndexHolder = class(TThread) + private + FKeys: TOrderedAccountKeysList; + protected + procedure Execute; override; + public + Entered, ReleaseRead: TEvent; + TimedOut: Boolean; + constructor Create(AKeys: TOrderedAccountKeysList); + destructor Destroy; override; + end; + + TQueuedFilterRelease = class(TThread) + private + FAction: TThreadMethod; + protected + procedure Execute; override; + public + Entered: TEvent; + constructor Create(AAction: TThreadMethod); + destructor Destroy; override; + end; + + TStoredHistoryGate = class(TFileStorage) + protected + function GetFirstBlockNumber: Int64; override; + function GetLastBlockNumber: Int64; override; + function DoLoadBlockChain(Operations: TPCOperationsComp; Block: Cardinal): Boolean; override; + public + Entered, ReleaseRead: TEvent; + Loads: Integer; + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + end; + + TStoredHistoryReader = class(TThread) + private + FStorage: TStorage; + protected + procedure Execute; override; + public + Completed: TEvent; + Rows: TOperationsResumeList; + Cancelled: Boolean; + ErrorMessage: String; + constructor Create(AStorage: TStorage); + destructor Destroy; override; + end; + +procedure TExplorerSafeBoxAccess.SeedGenesis(const AKey: TAccountKey); +begin + SeedBlocks(AKey, 1, 0); +end; + +procedure TExplorerSafeBoxAccess.SeedBlocks(const AKey: TAccountKey; + ACount: Integer; AReward: Int64); +var LBlock: TOperationBlock; + I: Integer; +begin + LBlock := CT_OperationBlock_NUL; + LBlock.account_key := AKey; + LBlock.protocol_version := CT_PROTOCOL_1; + LBlock.protocol_available := CT_PROTOCOL_6; + LBlock.compact_target := CT_MinCompactTarget_v1; + LBlock.reward := AReward; + for I := 1 to ACount do begin + LBlock.block := BlocksCount; + AddNew(LBlock); + end; +end; + +constructor TSelectedSafeBoxHolder.Create(ASafeBox: TPCSafeBox); +begin + inherited Create(True); + FreeOnTerminate := False; + FSafeBox := ASafeBox; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); + Start; +end; + +destructor TSelectedSafeBoxHolder.Destroy; +begin + ReleaseRead.SetEvent; + WaitFor; + Entered.Free; + ReleaseRead.Free; + inherited; +end; + +procedure TSelectedSafeBoxHolder.Execute; +begin + FSafeBox.StartThreadSafe; + try + Entered.SetEvent; + TimedOut := ReleaseRead.WaitFor(10000) <> wrSignaled; + finally FSafeBox.EndThreadSave; end; +end; + +constructor TAccountKeyIndexHolder.Create(AKeys: TOrderedAccountKeysList); +begin + inherited Create(True); + FreeOnTerminate := False; + FKeys := AKeys; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); + Start; +end; + +destructor TAccountKeyIndexHolder.Destroy; +begin + ReleaseRead.SetEvent; + WaitFor; + Entered.Free; + ReleaseRead.Free; + inherited; +end; + +procedure TAccountKeyIndexHolder.Execute; +begin + FKeys.Lock; + try + Entered.SetEvent; + TimedOut := ReleaseRead.WaitFor(10000) <> wrSignaled; + finally FKeys.Unlock; end; +end; + +constructor TQueuedFilterRelease.Create(AAction: TThreadMethod); +begin + inherited Create(True); + FreeOnTerminate := False; + FAction := AAction; + Entered := TEvent.Create(nil, True, False, ''); + Start; +end; + +destructor TQueuedFilterRelease.Destroy; +begin + WaitFor; + Entered.Free; + inherited; +end; + +procedure TQueuedFilterRelease.Execute; +begin + Entered.SetEvent; + Synchronize(FAction); +end; + +constructor TStoredHistoryGate.Create(AOwner: TComponent); +begin + inherited; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); +end; + +destructor TStoredHistoryGate.Destroy; +begin + Entered.Free; + ReleaseRead.Free; + inherited; +end; + +function TStoredHistoryGate.GetFirstBlockNumber: Int64; +begin + Result := 0; +end; + +function TStoredHistoryGate.GetLastBlockNumber: Int64; +begin + Result := 0; +end; + +function TStoredHistoryGate.DoLoadBlockChain(Operations: TPCOperationsComp; Block: Cardinal): Boolean; +begin + Inc(Loads); + Entered.SetEvent; + if ReleaseRead.WaitFor(10000) <> wrSignaled then + raise Exception.Create('Stored block read gate timed out'); + Result := True; +end; + +constructor TStoredHistoryReader.Create(AStorage: TStorage); +begin + inherited Create(True); + FreeOnTerminate := False; + FStorage := AStorage; + Completed := TEvent.Create(nil, True, False, ''); + Rows := TOperationsResumeList.Create; + Start; +end; + +destructor TStoredHistoryReader.Destroy; +begin + Terminate; + WaitFor; + Rows.Free; + Completed.Free; + inherited; +end; + +procedure TStoredHistoryReader.Execute; +begin + try + try + FStorage.GetAccountOperations(0, -1, 0, 5000, 0, Rows); + except + on E: EAbort do Cancelled := True; + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally Completed.SetEvent; end; +end; + +constructor TExplorerSnapshotFixture.Create; +begin + inherited Create(nil, nil); + FLock := TCriticalSection.Create; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, True, ''); + Configure(1000, False, False); +end; + +destructor TExplorerSnapshotFixture.Destroy; +begin + // Even an intentionally uncooperative stale read must finish before its + // fixture events and lock disappear, including when an assertion fails. + ReleaseRead.SetEvent; + Stop; + Entered.Free; + ReleaseRead.Free; + FLock.Free; + inherited; +end; + +procedure TExplorerSnapshotFixture.Configure(ABaseBalance: Int64; + ABlock, AIgnoreCancellation: Boolean); +begin + FLock.Acquire; + try + FBaseBalance := ABaseBalance; + FBlockRead := ABlock; + FIgnoreCancellation := AIgnoreCancellation; + FFailRead := False; + finally FLock.Release; end; +end; + +procedure TExplorerSnapshotFixture.BlockNext(ABaseBalance: Int64; + AIgnoreCancellation: Boolean); +begin + Entered.ResetEvent; + ReleaseRead.ResetEvent; + Configure(ABaseBalance, True, AIgnoreCancellation); +end; + +procedure TExplorerSnapshotFixture.BlockNextError; +begin + BlockNext(0, True); + FLock.Acquire; + try FFailRead := True; finally FLock.Release; end; +end; + +function TExplorerSnapshotFixture.ReadSnapshot(const AAccounts: TArray<Cardinal>; + out ARows: TArray<TAccount>; out AAccountCount, ABlockCount: Integer; + out ABalance: Int64): Boolean; +var LBalance: Int64; LBlock, LIgnore, LFail: Boolean; I: Integer; +begin + FLock.Acquire; + try + Inc(FCalls); + FRequests := Copy(AAccounts); + FReadOnMain := FReadOnMain or (TThread.CurrentThread.ThreadID = MainThreadID); + LBalance := FBaseBalance; + LBlock := FBlockRead; + LIgnore := FIgnoreCancellation; + LFail := FFailRead; + finally FLock.Release; end; + Entered.SetEvent; + while LBlock and (ReleaseRead.WaitFor(10) <> wrSignaled) do + if (not LIgnore) and TThread.CheckTerminated then begin + FLock.Acquire; + try FCancelled := True; finally FLock.Release; end; + Exit(False); + end; + if (not LIgnore) and TThread.CheckTerminated then Exit(False); + if LFail then raise EInvalidOperation.Create('Obsolete snapshot failure'); + AAccountCount := 4200000; + ABlockCount := 840000; + ABalance := LBalance * AAccountCount; + SetLength(ARows, Length(AAccounts)); + for I := 0 to High(AAccounts) do begin + ARows[I] := CT_Account_NUL; + ARows[I].account := AAccounts[I]; + ARows[I].balance := UInt64(LBalance) + AAccounts[I]; + end; + Result := True; +end; + +function TExplorerSnapshotFixture.Calls: Integer; +begin + FLock.Acquire; try Result := FCalls; finally FLock.Release; end; +end; + +function TExplorerSnapshotFixture.Requests: TArray<Cardinal>; +begin + FLock.Acquire; try Result := Copy(FRequests); finally FLock.Release; end; +end; + +function TExplorerSnapshotFixture.ReadOnMain: Boolean; +begin + FLock.Acquire; try Result := FReadOnMain; finally FLock.Release; end; +end; + +function TExplorerSnapshotFixture.Cancelled: Boolean; +begin + FLock.Acquire; try Result := FCancelled; finally FLock.Release; end; +end; + +constructor TExplorerOperationStorage.Create(AOwner: TComponent); +begin + inherited; + Entered := TEvent.Create(nil, True, False, ''); + ReleaseRead := TEvent.Create(nil, True, False, ''); +end; + +destructor TExplorerOperationStorage.Destroy; +begin + ReleaseRead.SetEvent; + Entered.Free; + ReleaseRead.Free; + inherited; +end; + +function TExplorerOperationStorage.DoGetAccountOperations(AAccount, AMaxDepth, + AStartOperation, AMaxOperations, ASearchBackwardsStartingAtBlock: Integer; + const AOperationsResumeList: TOperationsResumeList): Boolean; +var LRow: TOperationResume; +begin + if TThread.CurrentThread.ThreadID = MainThreadID then + raise Exception.Create('History was fetched on the UI thread'); + InterlockedExchange(FLastAccount, AAccount); + InterlockedIncrement(FCalls); + Entered.SetEvent; + while ReleaseRead.WaitFor(10) <> wrSignaled do + if TThread.CheckTerminated then begin + InterlockedExchange(FCancelled, 1); + Abort; + end; + if TThread.CheckTerminated then Abort; + LRow := CT_TOperationResume_NUL; + LRow.AffectedAccount := AAccount; + LRow.Amount := 10000 + AAccount; + AOperationsResumeList.Add(LRow); + Result := True; +end; + +function TExplorerOperationStorage.Calls: Integer; +begin + Result := InterlockedCompareExchange(FCalls, 0, 0); +end; + +function TExplorerOperationStorage.LastAccount: Integer; +begin + Result := InterlockedCompareExchange(FLastAccount, 0, 0); +end; + +function TExplorerOperationStorage.Cancelled: Boolean; +begin + Result := InterlockedCompareExchange(FCancelled, 0, 0) <> 0; +end; + +function TExplorerAccountsGrid.Read(AAccountNumber: Integer; out AAccount: TAccount; + out ABlockCount, AAccountCount: Integer): Boolean; +begin + Result := BufferGetAccount(AAccountNumber, AAccount, ABlockCount, AAccountCount); +end; + +procedure TWalletExplorerTests.SetUp; +begin + FObserved := TList<Int64>.Create; + FCompletionCount := 0; + FRefreshStateCount := 0; + FRefreshStateWasBusy := False; +end; + +procedure TWalletExplorerTests.TearDown; +begin + if Assigned(FFilterRelease) then FFilterRelease.SetEvent; + if Assigned(FStorage) then FStorage.ReleaseRead.SetEvent; + FOperations.Free; + FAccounts.Free; + if FOwnsNetData and TNetData.NetDataExists then TNetData.NetData.Free; + FNode.Free; + FSource.Free; + FFilterEntered.Free; + FFilterRelease.Free; + FFilterExited.Free; + FObserved.Free; + if FDirectory <> '' then begin + TNode.SetPascalCoinDataFolder(FPreviousDirectory); + RemoveDir(FDirectory); + end; +end; + +procedure TWalletExplorerTests.Updated(Sender: TObject); +var LCount: Integer; LBalance: Int64; +begin + FWrongCallbackThread := FWrongCallbackThread or + (TThread.CurrentThread.ThreadID <> MainThreadID); + if FSource.TryGetSummary(LCount, LBalance) then FObserved.Add(LBalance); +end; + +procedure TWalletExplorerTests.NewSnapshots; +begin + FSource := TExplorerSnapshotFixture.Create; + FSource.OnUpdated := Updated; + FSource.OnCompleted := SnapshotCompleted; +end; + +procedure TWalletExplorerTests.SnapshotCompleted(Sender: TObject); +begin + FWrongCallbackThread := FWrongCallbackThread or + (TThread.CurrentThread.ThreadID <> MainThreadID); + Inc(FCompletionCount); +end; + +procedure TWalletExplorerTests.AccountRefreshStateChanged(Sender: TObject); +begin + FWrongCallbackThread := FWrongCallbackThread or + (TThread.CurrentThread.ThreadID <> MainThreadID); + FRefreshStateWasBusy := FRefreshStateWasBusy or FAccounts.IsUpdatingData; + Inc(FRefreshStateCount); +end; + +procedure TWalletExplorerTests.FailedSnapshotCompletesWithoutPublishingOrRetrying; +var LRow: TAccount; LBlocks, LCount: Integer; LDeadline: QWord; +begin + NewSnapshots; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilIdle; + AssertEquals(1, FCompletionCount); + AssertEquals(1, FObserved.Count); + FSource.BlockNextError; + FSource.Invalidate; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilCalls(2); + FSource.ReleaseRead.SetEvent; + LDeadline := GetTickCount64 + 4000; + repeat + Application.ProcessMessages; + Sleep(1); + until (not FSource.Updating) or (GetTickCount64 >= LDeadline); + AssertFalse('The failed read must retire', FSource.Updating); + AssertTrue('A failed read must retain its diagnostic', FSource.ErrorMessage <> ''); + AssertEquals('Terminal failure must release event-driven readiness', 2, FCompletionCount); + AssertEquals('Failure must not announce a replacement data snapshot', 1, FObserved.Count); + AssertFalse(FWrongCallbackThread); + LDeadline := GetTickCount64 + 300; + repeat + Application.ProcessMessages; + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertEquals('Completion alone must not start another read', 2, FSource.Calls); +end; + +procedure TWalletExplorerTests.PumpUntilCalls(ACount: Integer); +var LDeadline: QWord; +begin + LDeadline := GetTickCount64 + 4000; + repeat + Application.ProcessMessages; + if FSource.Calls >= ACount then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertEquals('The owned reader must enter its gate', ACount, FSource.Calls); + AssertFalse('Snapshot reads must run off the main thread', FSource.ReadOnMain); +end; + +procedure TWalletExplorerTests.PumpUntilIdle; +var LDeadline: QWord; +begin + LDeadline := GetTickCount64 + 4000; + while FSource.Updating and (GetTickCount64 < LDeadline) do begin + Application.ProcessMessages; + Sleep(1); + end; + AssertFalse('Snapshot publication must finish', FSource.Updating); + AssertFalse('Snapshot callbacks must run on the main thread', FWrongCallbackThread); + AssertEquals('', FSource.ErrorMessage); +end; + +procedure TWalletExplorerTests.BlockedReadLeavesCachedLookupsResponsive; +var LRow: TAccount; LBlocks, LCount, I: Integer; LBalance: Int64; LStart: QWord; +begin + NewSnapshots; + FSource.BlockNext(1000); + AssertFalse(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + PumpUntilCalls(1); + LStart := GetTickCount64; + for I := 1 to 1000 do begin + AssertFalse(FSource.TryGetSummary(LCount, LBalance)); + AssertFalse(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + end; + AssertTrue('Painting must not wait behind the gated worker', GetTickCount64 - LStart < 500); + AssertEquals(0, FObserved.Count); + FSource.ReleaseRead.SetEvent; + PumpUntilIdle; + AssertTrue(FSource.TryGetSummary(LCount, LBalance)); + AssertEquals(4200000, LCount); + AssertEquals(Int64(4200000000), LBalance); + AssertTrue(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + AssertEquals(Int64(1007), Int64(LRow.balance)); +end; + +procedure TWalletExplorerTests.PreviousRowsRemainUntilReplacementIsReady; +var LRow: TAccount; LBlocks, LCount: Integer; LBalance: Int64; +begin + NewSnapshots; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilIdle; + FSource.BlockNext(2000); + FSource.Invalidate; + AssertTrue(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + PumpUntilCalls(2); + AssertTrue(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + AssertEquals(Int64(1007), Int64(LRow.balance)); + AssertTrue(FSource.TryGetSummary(LCount, LBalance)); + AssertEquals(Int64(4200000000), LBalance); + FSource.ReleaseRead.SetEvent; + PumpUntilIdle; + AssertTrue(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + AssertEquals(Int64(2007), Int64(LRow.balance)); + AssertEquals(2, FObserved.Count); +end; + +procedure TWalletExplorerTests.StaleGenerationCannotPublish; +var LRow: TAccount; LBlocks, LCount: Integer; +begin + NewSnapshots; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilIdle; + FSource.BlockNext(2000, True); + FSource.Invalidate; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilCalls(2); + // The old read deliberately returns success even after cancellation. Only + // the real component's generation check can prevent stale publication. + FSource.Configure(3000, False, False); + FSource.Invalidate; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + FSource.ReleaseRead.SetEvent; + PumpUntilIdle; + AssertEquals(3, FSource.Calls); + AssertEquals(2, FObserved.Count); + AssertEquals(Int64(4200000000), FObserved[0]); + AssertEquals(Int64(12600000000), FObserved[1]); + AssertTrue(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + AssertEquals(Int64(3007), Int64(LRow.balance)); + // Obsolete failures must be rejected just like obsolete successful rows. + FSource.BlockNextError; + FSource.Invalidate; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilCalls(4); + FSource.Configure(4000, False, False); + FSource.Invalidate; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + FSource.ReleaseRead.SetEvent; + PumpUntilIdle; + AssertEquals('', FSource.ErrorMessage); + AssertTrue(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); + AssertEquals(Int64(4007), Int64(LRow.balance)); +end; + +procedure TWalletExplorerTests.VisibleReadRequestsAreBounded; +var LRow: TAccount; LBlocks, LCount, I: Integer; LRequested: TArray<Cardinal>; +begin + NewSnapshots; + FSource.BlockNext(1000); + // No message pumping here: all requests precede the first real timer tick. + for I := 0 to 9999 do FSource.TryGetAccount(I, LRow, LBlocks, LCount); + PumpUntilCalls(1); + LRequested := FSource.Requests; + AssertEquals('Read only the bounded visible batch', 128, Length(LRequested)); + for I := 0 to High(LRequested) do AssertEquals(I, Integer(LRequested[I])); + FSource.Stop; +end; + +procedure TWalletExplorerTests.StopCancelsBlockedReadWithoutPublishing; +var LRow: TAccount; LBlocks, LCount: Integer; LStart: QWord; +begin + NewSnapshots; + FSource.BlockNext(1000); + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilCalls(1); + LStart := GetTickCount64; + FSource.Stop; + AssertTrue('Stop must cancel the blocked reader', GetTickCount64 - LStart < 1000); + AssertTrue(FSource.Cancelled); + AssertFalse(FSource.Updating); + FSource.ReleaseRead.SetEvent; + Application.ProcessMessages; + CheckSynchronize; + AssertEquals('No callback may publish after Stop', 0, FObserved.Count); + AssertFalse(FSource.TryGetAccount(7, LRow, LBlocks, LCount)); +end; + +procedure TWalletExplorerTests.NewOperations; +var LID: TGUID; +begin + TCrypto.InitCrypto; + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'WalletExplorer-' + GUIDToString(LID); + ForceDirectories(FDirectory); + TNode.SetPascalCoinDataFolder(FDirectory); + FOwnsNetData := not TNetData.NetDataExists; + FNode := TNode.Create(nil); + FNode.BroadcastData := False; + FNode.Bank.StorageClass := TExplorerOperationStorage; + FStorage := TExplorerOperationStorage(FNode.Bank.Storage); + FOperations := TOperationsGrid.Create(nil); + FOperations.AsynchronousUpdates := True; + FOperations.AutoRefreshOnNodeEvents := False; + FOperations.MustShowAlwaysAnAccount := True; + FOperations.AccountNumber := -1; + FOperations.Node := FNode; +end; + +procedure TWalletExplorerTests.PumpOperations(AAccount: Cardinal); +var LDeadline: QWord; +begin + LDeadline := GetTickCount64 + 4000; + repeat + Application.ProcessMessages; + if (FOperations.OperationsResume.Count = 1) and + (FOperations.OperationsResume[0].AffectedAccount = AAccount) then Exit; + Sleep(1); + until GetTickCount64 >= LDeadline; + Fail('The latest operation request was not published'); +end; + +procedure TWalletExplorerTests.LegacyOperationsCoalesceWithoutWaitingOnUI; +var LDeadline, LStart: QWord; LWorker: TOperationsGridUpdateThread; +begin + NewOperations; + FOperations.AccountNumber := 7; + LDeadline := GetTickCount64 + 4000; + while (FStorage.Calls = 0) and (GetTickCount64 < LDeadline) do begin + Application.ProcessMessages; Sleep(1); + end; + AssertEquals(1, FStorage.Calls); + LWorker := TOperationsGridUpdateThread(TPCThread.GetThreadByClass(TOperationsGridUpdateThread, nil)); + AssertTrue(Assigned(LWorker)); + FStorage.ReleaseRead.SetEvent; + LWorker.WaitFor; + AssertEquals('The initial request must complete without an error', '', LWorker.FError); + AssertTrue('The initial request must be publishable', LWorker.FSucceeded); + PumpOperations(7); + FStorage.ReleaseRead.ResetEvent; + FStorage.Entered.ResetEvent; + FOperations.AccountNumber := 9; + LDeadline := GetTickCount64 + 4000; + while (FStorage.Calls < 2) and (GetTickCount64 < LDeadline) do begin + Application.ProcessMessages; Sleep(1); + end; + AssertEquals(2, FStorage.Calls); + LStart := GetTickCount64; + FOperations.AccountNumber := 11; + AssertTrue('Changing selection must not join the blocked request', GetTickCount64 - LStart < 500); + AssertEquals('Keep the last displayed rows while the request is blocked', + 7, Integer(FOperations.OperationsResume[0].AffectedAccount)); + FStorage.ReleaseRead.SetEvent; + PumpOperations(11); + AssertEquals(1, FOperations.OperationsResume.Count); + AssertEquals(11, FStorage.LastAccount); +end; + +procedure TWalletExplorerTests.LegacyOperationsDestructionJoinsCancelledRead; +var LDeadline, LStart: QWord; +begin + NewOperations; + FOperations.AccountNumber := 7; + LDeadline := GetTickCount64 + 4000; + while (FStorage.Calls = 0) and (GetTickCount64 < LDeadline) do begin + Application.ProcessMessages; Sleep(1); + end; + AssertEquals(1, FStorage.Calls); + LStart := GetTickCount64; + FreeAndNil(FOperations); + AssertTrue('Destruction must cancel and join the blocked history job', GetTickCount64 - LStart < 1000); + AssertTrue(FStorage.Cancelled); + Application.ProcessMessages; + CheckSynchronize; + AssertEquals(1, FStorage.Calls); +end; + +function TWalletExplorerTests.ReadAccount(Sender: TObject; AAccountNumber: Integer; + out AAccount: TAccount; out ABlockCount, AAccountCount: Integer): Boolean; +begin + Inc(FReadCalls); + AAccount := CT_Account_NUL; + AAccount.account := AAccountNumber; + AAccount.balance := 16537985; + ABlockCount := 840000; + AAccountCount := 4200000; + Result := FReadReady; +end; + +function TWalletExplorerTests.ReadSummary(Sender: TObject; out AAccountCount: Integer; + out ABalance: Int64): Boolean; +begin + Inc(FSummaryCalls); + AAccountCount := 4200000; + ABalance := 5108944375; + Result := FReadReady; +end; + +procedure TWalletExplorerTests.LegacyAccountReadersUseSnapshotCallbacks; +var LRow: TAccount; LBlocks, LCount: Integer; +begin + NewOperations; + FAccounts := TExplorerAccountsGrid.Create(nil); + FAccounts.OnReadAccount := ReadAccount; + FAccounts.OnReadSummary := ReadSummary; + FAccounts.Node := FNode; + FReadReady := False; + AssertFalse(FAccounts.Read(77, LRow, LBlocks, LCount)); + AssertEquals(0, FAccounts.AccountsCount); + FReadReady := True; + AssertTrue(FAccounts.Read(77, LRow, LBlocks, LCount)); + AssertEquals(77, Integer(LRow.account)); + AssertEquals(Int64(16537985), Int64(LRow.balance)); + AssertEquals(840000, LBlocks); + AssertEquals(4200000, LCount); + FAccounts.UpdateData; + AssertEquals(4200000, FAccounts.AccountsCount); + AssertEquals(Int64(5108944375), FAccounts.AccountsBalance); + AssertEquals(2, FReadCalls); + AssertTrue(FSummaryCalls > 0); + AssertEquals('The real node is empty; counts came from the callback', 0, Integer(FNode.Bank.AccountsCount)); +end; + +procedure TWalletExplorerTests.FilterProgress(Sender: TObject); +begin + InterlockedIncrement(FProcessedAdds); + FFilterEntered.SetEvent; + if FFilterRelease.WaitFor(10000) <> wrSignaled then + raise Exception.Create('Account filter progress gate timed out'); + FFilterExited.SetEvent; +end; + +procedure TWalletExplorerTests.ReleaseFilter; +begin + FReleasedFilterOnMain := TThread.CurrentThread.ThreadID = MainThreadID; + FFilterRelease.SetEvent; +end; + +procedure TWalletExplorerTests.ReleaseIndex; +begin + FReleasedFilterOnMain := TThread.CurrentThread.ThreadID = MainThreadID; + FIndexRelease.SetEvent; +end; + +procedure TWalletExplorerTests.RetiredAccountFiltersRemainBoundedAndJoinBeforeNodeHandoff; +var + LKey: TECPrivateKey; + LWorker: TAccountsGridUpdateThread; + LRelease: TQueuedFilterRelease; + LFilter: TAccountsGridFilter; + LDeadline, LStarted: QWord; + I: Integer; +begin + NewOperations; + LKey := TECPrivateKey.Create; + try + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + TExplorerSafeBoxAccess(FNode.Bank.SafeBox).SeedGenesis(LKey.PublicKey); + finally LKey.Free; end; + FFilterEntered := TEvent.Create(nil, True, False, ''); + FFilterRelease := TEvent.Create(nil, True, False, ''); + FFilterExited := TEvent.Create(nil, True, False, ''); + FAccounts := TExplorerAccountsGrid.Create(nil); + FAccounts.Node := FNode; + LFilter := CT_TAccountsGridFilter_NUL; + LFilter.MinBalance := 0; + FAccounts.AccountsGridFilter := LFilter; + // Pause account access before the query starts, then attach the existing + // processed-list event. Its first Add supplies a deterministic worker gate. + FNode.Bank.SafeBox.StartThreadSafe; + try + FAccounts.AccountsGridDatasource := acds_NodeFiltered; + LDeadline := GetTickCount64 + 3000; + repeat + LWorker := TAccountsGridUpdateThread(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil)); + if Assigned(LWorker) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertTrue('The real account filter worker must start', Assigned(LWorker)); + LWorker.FProcessedList.OnListChanged := FilterProgress; + finally FNode.Bank.SafeBox.EndThreadSave; end; + AssertTrue('The worker must reach the account-add gate', + FFilterEntered.WaitFor(3000) = wrSignaled); + LStarted := GetTickCount64; + for I := 1 to 100 do begin + LFilter.MaxBalance := I; + FAccounts.AccountsGridFilter := LFilter; + end; + AssertTrue('Normal filter changes must not join the gated old request', + GetTickCount64 - LStarted < 500); + AssertFalse('Cancellation must not spawn a second account filter query', + Assigned(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, LWorker))); + LRelease := TQueuedFilterRelease.Create(ReleaseFilter); + try + AssertTrue(LRelease.Entered.WaitFor(3000) = wrSignaled); + // Do not pump the queued release here. A correct Node handoff must join + // the retired worker, servicing synchronization until that join completes. + FAccounts.Node := nil; + AssertTrue('The handoff must wait for the retired reader', FReleasedFilterOnMain); + AssertTrue(FFilterExited.WaitFor(0) = wrSignaled); + AssertFalse(Assigned(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil))); + Application.ProcessMessages; + AssertFalse('Node removal also cancels the coalesced replacement', FAccounts.IsUpdatingData); + finally + FFilterRelease.SetEvent; + LRelease.Free; + end; +end; + +procedure TWalletExplorerTests.RetiredFilterToNodePublishesIdleAfterEarlierSnapshotCompletion; +var LKey: TECPrivateKey; LWorker: TAccountsGridUpdateThread; + LFilter: TAccountsGridFilter; LDeadline: QWord; + LRow: TAccount; LBlocks, LCount, LCalls: Integer; +begin + NewOperations; + LKey := TECPrivateKey.Create; + try + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + TExplorerSafeBoxAccess(FNode.Bank.SafeBox).SeedGenesis(LKey.PublicKey); + finally LKey.Free; end; + FFilterEntered := TEvent.Create(nil, True, False, ''); + FFilterRelease := TEvent.Create(nil, True, False, ''); + FFilterExited := TEvent.Create(nil, True, False, ''); + FAccounts := TExplorerAccountsGrid.Create(nil); + FAccounts.Node := FNode; + FAccounts.OnReadSummary := ReadSummary; + FAccounts.OnRefreshStateChanged := AccountRefreshStateChanged; + LFilter := CT_TAccountsGridFilter_NUL; + LFilter.MinBalance := 0; + FAccounts.AccountsGridFilter := LFilter; + FNode.Bank.SafeBox.StartThreadSafe; + try + FAccounts.AccountsGridDatasource := acds_NodeFiltered; + LDeadline := GetTickCount64 + 3000; + repeat + LWorker := TAccountsGridUpdateThread(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil)); + if Assigned(LWorker) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertTrue('A real filtered-account reader must start', Assigned(LWorker)); + LWorker.FProcessedList.OnListChanged := FilterProgress; + finally FNode.Bank.SafeBox.EndThreadSave; end; + AssertEquals('The actual reader reaches its processed-account gate', + Ord(wrSignaled), Ord(FFilterEntered.WaitFor(3000))); + + // Switching to the unfiltered node view cancels the old worker but must not + // wait for it on the UI thread or invent a replacement filter query. + FAccounts.AccountsGridDatasource := acds_Node; + AssertTrue(LWorker.Terminated); + AssertTrue('A cancelled reader still counts until its owner retires it', FAccounts.IsUpdatingData); + AssertEquals(0, FRefreshStateCount); + + // Reproduce the missing ordering: the independent snapshot completion arrives + // while the cancelled filter remains held. It cannot be the later idle signal. + NewSnapshots; + FSource.TryGetAccount(7, LRow, LBlocks, LCount); + PumpUntilIdle; + AssertEquals(1, FCompletionCount); + AssertTrue('The snapshot completed before the account reader retired', FAccounts.IsUpdatingData); + AssertEquals(0, FRefreshStateCount); + LCalls := FSource.Calls; + FFilterRelease.SetEvent; + LDeadline := GetTickCount64 + 4000; + while FAccounts.IsUpdatingData and (GetTickCount64 < LDeadline) do begin + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + end; + AssertFalse('The existing retirement owner must become idle', FAccounts.IsUpdatingData); + AssertEquals('The real busy-to-idle transition notifies exactly once', 1, FRefreshStateCount); + AssertFalse('Readiness is committed before the callback', FRefreshStateWasBusy); + AssertFalse('The callback runs on the main thread', FWrongCallbackThread); + AssertEquals(Ord(wrSignaled), Ord(FFilterExited.WaitFor(0))); + AssertFalse('No cancelled/replacement filter remains', + Assigned(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil))); + LDeadline := GetTickCount64 + 150; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertEquals('Idle observation cannot create another completion', 1, FRefreshStateCount); + AssertEquals('Readiness does not invalidate/refetch snapshots', LCalls, FSource.Calls); +end; + +procedure TWalletExplorerTests.StoredHistoryCancellationStopsAfterTheCurrentBlockRead; +var + LBank: TPCBank; + LKey: TECPrivateKey; + LStorage: TStoredHistoryGate; + LWorker: TStoredHistoryReader; +begin + TCrypto.InitCrypto; + LBank := TPCBank.Create(nil); + LKey := TECPrivateKey.Create; + try + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + TExplorerSafeBoxAccess(LBank.SafeBox).SeedGenesis(LKey.PublicKey); + LBank.StorageClass := TStoredHistoryGate; + LStorage := TStoredHistoryGate(LBank.Storage); + LWorker := TStoredHistoryReader.Create(LStorage); + try + AssertTrue('The real stored-history traversal must reach its first block', + LStorage.Entered.WaitFor(3000) = wrSignaled); + LWorker.Terminate; + LStorage.ReleaseRead.SetEvent; + AssertTrue(LWorker.Completed.WaitFor(3000) = wrSignaled); + LWorker.WaitFor; + AssertTrue('Cancellation must escape immediately after the active I/O completes', LWorker.Cancelled); + AssertEquals('', LWorker.ErrorMessage); + AssertEquals('No later block may be read', 1, LStorage.Loads); + AssertEquals('No partial history may be published', 0, LWorker.Rows.Count); + finally + LStorage.ReleaseRead.SetEvent; + LWorker.Free; + end; + finally + LKey.Free; + LBank.Free; + end; +end; + +procedure TWalletExplorerTests.SelectedBalanceUpdated(Sender: TObject); +begin + FWrongCallbackThread := FWrongCallbackThread or + (TThread.CurrentThread.ThreadID <> MainThreadID); + FObserved.Add(FAccounts.AccountsBalance); +end; + +procedure TWalletExplorerTests.NewSelectedAccounts(ABlocks: Integer); +var LKey: TECPrivateKey; +begin + NewOperations; + LKey := TECPrivateKey.Create; + try + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + TExplorerSafeBoxAccess(FNode.Bank.SafeBox).SeedBlocks(LKey.PublicKey, ABlocks, 10000); + finally LKey.Free; end; + FAccounts := TExplorerAccountsGrid.Create(nil); + FAccounts.OnReadAccount := ReadAccount; + FAccounts.AccountsGridDatasource := acds_InternalList; + FAccounts.Node := FNode; + FAccounts.OnUpdated := SelectedBalanceUpdated; +end; + +procedure TWalletExplorerTests.PumpSelectedAccounts; +var LDeadline: QWord; +begin + LDeadline := GetTickCount64 + 4000; + repeat + Application.ProcessMessages; + CheckSynchronize; + if not FAccounts.IsUpdatingData then Exit; + Sleep(1); + until GetTickCount64 >= LDeadline; + Fail('Selected-account total did not finish'); +end; + +procedure TWalletExplorerTests.SelectedTotalsReadMoreThanTheRowCacheOffTheUIThread; +var LIDs: TOrderedCardinalList; LHolder: TSelectedSafeBoxHolder; + I: Integer; LStarted: QWord; +begin + NewSelectedAccounts(120); + LHolder := TSelectedSafeBoxHolder.Create(FNode.Bank.SafeBox); + try + AssertTrue(LHolder.Entered.WaitFor(3000) = wrSignaled); + LIDs := FAccounts.LockAccountsList; + for I := 0 to 599 do LIDs.Add(I); + LStarted := GetTickCount64; + FAccounts.UnlockAccountsList; + AssertTrue('Adding 600 selected accounts must not wait for a held core lock', + GetTickCount64 - LStarted < 500); + AssertEquals(600, FAccounts.AccountsCount); + AssertTrue(FAccounts.IsUpdatingData); + AssertEquals('The cache callback is reserved for UI painting', 0, FReadCalls); + LHolder.ReleaseRead.SetEvent; + PumpSelectedAccounts; + AssertEquals('All 120 rewarded accounts must contribute beyond the 512-row cache', + Int64(1200000), FAccounts.AccountsBalance); + AssertEquals('Publishing a total must preserve all selected IDs', 600, FAccounts.AccountsCount); + AssertFalse(FWrongCallbackThread); + AssertFalse(LHolder.TimedOut); + finally LHolder.Free; end; +end; + +procedure TWalletExplorerTests.SelectedTotalsRetainCompletedValueAndCancelObsoleteRequests; +var LIDs: TOrderedCardinalList; LHolder: TSelectedSafeBoxHolder; + LWorker: TAccountsGridUpdateThread; LStarted, LDeadline: QWord; I: Integer; +begin + NewSelectedAccounts(3); + LIDs := FAccounts.LockAccountsList; + LIDs.Add(0); + FAccounts.UnlockAccountsList; + PumpSelectedAccounts; + AssertEquals(Int64(10000), FAccounts.AccountsBalance); + LHolder := TSelectedSafeBoxHolder.Create(FNode.Bank.SafeBox); + try + AssertTrue(LHolder.Entered.WaitFor(3000) = wrSignaled); + LIDs := FAccounts.LockAccountsList; + LIDs.Add(5); + FAccounts.UnlockAccountsList; + // The previous completed worker may still be retiring until the next + // component timer. Wait for the replacement to own the blocked read. + LDeadline := GetTickCount64 + 3000; + repeat + Application.ProcessMessages; + LWorker := TAccountsGridUpdateThread(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil)); + if Assigned(LWorker) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertTrue('The replacement total must have an owned worker', Assigned(LWorker)); + LStarted := GetTickCount64; + for I := 1 to 100 do begin + LIDs := FAccounts.LockAccountsList; + LIDs.Add(10); + FAccounts.UnlockAccountsList; + end; + AssertTrue('Replacing pending selected totals must not join the old read', + GetTickCount64 - LStarted < 500); + AssertFalse('Only one selected total reader may be active or retiring', + Assigned(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, LWorker))); + AssertEquals('Keep the completed total while its replacement is blocked', + Int64(10000), FAccounts.AccountsBalance); + LHolder.ReleaseRead.SetEvent; + PumpSelectedAccounts; + AssertEquals(Int64(30000), FAccounts.AccountsBalance); + for I := 0 to FObserved.Count - 1 do + AssertTrue('Canceled or partial totals must never be published', FObserved[I] <> 20000); + AssertFalse(FWrongCallbackThread); + AssertFalse(LHolder.TimedOut); + LIDs := FAccounts.LockAccountsList; + LIDs.Clear; + FAccounts.UnlockAccountsList; + AssertEquals('An empty selection needs no core query', Int64(0), FAccounts.AccountsBalance); + PumpSelectedAccounts; + finally LHolder.Free; end; +end; + +procedure TWalletExplorerTests.CapturedKeySelectionDoesNotWaitForIndexAndJoinsCancelledResolver; +var + LFirst, LLast: TECPrivateKey; + LKeys: TOrderedAccountKeysList; + LHolder: TAccountKeyIndexHolder; + LRelease: TQueuedFilterRelease; + LFilter: TAccountsGridFilter; + LWorker: TAccountsGridUpdateThread; + LStarted, LDeadline: QWord; + I: Integer; +begin + NewOperations; + LFirst := TECPrivateKey.Create; + LLast := TECPrivateKey.Create; + LKeys := nil; LHolder := nil; LRelease := nil; + try + LFirst.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LLast.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + TExplorerSafeBoxAccess(FNode.Bank.SafeBox).SeedBlocks(LFirst.PublicKey, 3, 10000); + TExplorerSafeBoxAccess(FNode.Bank.SafeBox).SeedBlocks(LLast.PublicKey, 1, 10000); + LKeys := TOrderedAccountKeysList.Create(FNode.Bank.SafeBox, False); + LKeys.AddAccountKeys([LFirst.PublicKey, LLast.PublicKey]); + FAccounts := TExplorerAccountsGrid.Create(nil); + FAccounts.Node := nil; + FAccounts.AccountsGridDatasource := acds_NodeFiltered; + LFilter := CT_TAccountsGridFilter_NUL; + LFilter.OrderedAccountsKeyList := LKeys; + LFilter.MinBalance := 0; + LFilter.UseAccountKey := True; + LFilter.AccountKey := LFirst.PublicKey; + FAccounts.AccountsGridFilter := LFilter; + LHolder := TAccountKeyIndexHolder.Create(LKeys); + AssertTrue(LHolder.Entered.WaitFor(3000) = wrSignaled); + LStarted := GetTickCount64; + FAccounts.Node := FNode; + for I := 1 to 100 do begin + if Odd(I) then LFilter.AccountKey := LFirst.PublicKey.GetCopy + else LFilter.AccountKey := LLast.PublicKey.GetCopy; + FAccounts.AccountsGridFilter := LFilter; + end; + AssertTrue('Captured key changes must return while another thread owns the index', + GetTickCount64 - LStarted < 500); + AssertTrue(FAccounts.IsUpdatingData); + // The pending request owns its bytes, rather than borrowing mutable arrays. + LFilter.AccountKey.x[0] := LFilter.AccountKey.x[0] xor $FF; + LHolder.ReleaseRead.SetEvent; + PumpSelectedAccounts; + AssertEquals('Only the latest selected owner may be published', 5, FAccounts.AccountsCount); + AssertEquals(Int64(15), FAccounts.AccountNumber(1)); + AssertEquals(Int64(10000), FAccounts.AccountsBalance); + AssertFalse(LHolder.TimedOut); + FreeAndNil(LHolder); + + LHolder := TAccountKeyIndexHolder.Create(LKeys); + AssertTrue(LHolder.Entered.WaitFor(3000) = wrSignaled); + LFilter.AccountKey := LFirst.PublicKey; + FAccounts.AccountsGridFilter := LFilter; + LDeadline := GetTickCount64 + 3000; + repeat + Application.ProcessMessages; + LWorker := TAccountsGridUpdateThread(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil)); + if Assigned(LWorker) and LWorker.IsProcessing then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertTrue('The captured-key resolver must start on the owned worker', + Assigned(LWorker) and LWorker.IsProcessing); + LFilter.AccountKey := LLast.PublicKey; + FAccounts.AccountsGridFilter := LFilter; + AssertFalse('A blocked resolver may have only one coalesced replacement', + Assigned(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, LWorker))); + FIndexRelease := LHolder.ReleaseRead; + LRelease := TQueuedFilterRelease.Create(ReleaseIndex); + AssertTrue(LRelease.Entered.WaitFor(3000) = wrSignaled); + FAccounts.Node := nil; + AssertTrue('Node handoff must join the canceled resolver and dispatch its release', + FReleasedFilterOnMain); + AssertFalse(Assigned(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil))); + AssertFalse(FAccounts.IsUpdatingData); + finally + if Assigned(LHolder) then LHolder.ReleaseRead.SetEvent; + LRelease.Free; + FIndexRelease := nil; + if Assigned(FAccounts) then FAccounts.Node := nil; + LHolder.Free; + LKeys.Free; + LLast.Free; + LFirst.Free; + end; +end; + +procedure TWalletExplorerTests.CapturedMissingKeyIsEmptyAndLegacyAllKeysRemainAvailable; +var LMissing: TECPrivateKey; LFilter: TAccountsGridFilter; + LKeys: TOrderedAccountKeysList; +begin + NewSelectedAccounts(1); + FAccounts.Node := nil; + LMissing := TECPrivateKey.Create; + LKeys := TOrderedAccountKeysList.Create(FNode.Bank.SafeBox, True); + try + LMissing.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + LFilter := CT_TAccountsGridFilter_NUL; + LFilter.OrderedAccountsKeyList := LKeys; + LFilter.MinBalance := 0; + LFilter.UseAccountKey := True; + LFilter.AccountKey := LMissing.PublicKey; + FAccounts.AccountsGridDatasource := acds_NodeFiltered; + FAccounts.AccountsGridFilter := LFilter; + FAccounts.Node := FNode; + PumpSelectedAccounts; + AssertEquals('A missing captured key must not turn into the all-keys sentinel', 0, FAccounts.AccountsCount); + AssertEquals(Int64(0), FAccounts.AccountsBalance); + LFilter.UseAccountKey := False; + LFilter.indexAccountsKeyList := -1; + FAccounts.AccountsGridFilter := LFilter; + PumpSelectedAccounts; + AssertEquals('Existing negative-index callers still enumerate all keys', 5, FAccounts.AccountsCount); + AssertEquals(Int64(10000), FAccounts.AccountsBalance); + LFilter.indexAccountsKeyList := 0; + FAccounts.AccountsGridFilter := LFilter; + PumpSelectedAccounts; + AssertEquals('Existing indexed callers retain their selected owner', 5, FAccounts.AccountsCount); + finally + FAccounts.Node := nil; + LKeys.Free; + LMissing.Free; + end; +end; + +procedure TWalletExplorerTests.CapturedKeyCancellationStopsInsideTheRecordChunk; +var LKey: TECPrivateKey; LFilter: TAccountsGridFilter; + LWorker: TAccountsGridUpdateThread; LDeadline: QWord; + LKeys: TOrderedAccountKeysList; +begin + NewOperations; + LKey := TECPrivateKey.Create; + LKeys := nil; + try + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + TExplorerSafeBoxAccess(FNode.Bank.SafeBox).SeedBlocks(LKey.PublicKey, 120, 10000); + LKeys := TOrderedAccountKeysList.Create(FNode.Bank.SafeBox, False); + LKeys.AddAccountKey(LKey.PublicKey); + FFilterEntered := TEvent.Create(nil, True, False, ''); + FFilterRelease := TEvent.Create(nil, True, False, ''); + FFilterExited := TEvent.Create(nil, True, False, ''); + FAccounts := TExplorerAccountsGrid.Create(nil); + FAccounts.Node := nil; + FAccounts.AccountsGridDatasource := acds_NodeFiltered; + LFilter := CT_TAccountsGridFilter_NUL; + LFilter.OrderedAccountsKeyList := LKeys; + LFilter.MinBalance := 0; + LFilter.UseAccountKey := True; + LFilter.AccountKey := LKey.PublicKey; + FAccounts.AccountsGridFilter := LFilter; + FNode.Bank.SafeBox.StartThreadSafe; + try + FAccounts.Node := FNode; + LDeadline := GetTickCount64 + 3000; + repeat + LWorker := TAccountsGridUpdateThread(TPCThread.GetThreadByClass(TAccountsGridUpdateThread, nil)); + if Assigned(LWorker) then Break; + Sleep(1); + until GetTickCount64 >= LDeadline; + AssertTrue('The key-filter worker must start', Assigned(LWorker)); + LWorker.FProcessedList.OnListChanged := FilterProgress; + finally FNode.Bank.SafeBox.EndThreadSave; end; + AssertTrue(FFilterEntered.WaitFor(3000) = wrSignaled); + LFilter.AccountKey := CT_TECDSA_Public_Nul; + FAccounts.AccountsGridFilter := LFilter; + FFilterRelease.SetEvent; + PumpSelectedAccounts; + AssertEquals('Cancellation must stop after the current record, inside its chunk', + 1, InterlockedCompareExchange(FProcessedAdds, 0, 0)); + AssertEquals('The canceled partial owner list must never be published', 0, FAccounts.AccountsCount); + finally + if Assigned(FFilterRelease) then FFilterRelease.SetEvent; + if Assigned(FAccounts) then FAccounts.Node := nil; + LKeys.Free; + LKey.Free; + end; +end; + +initialization + RegisterTest(TWalletExplorerTests); +end. diff --git a/src/tests/experimental/ui/UWalletGridIntegrationTests.pas b/src/tests/experimental/ui/UWalletGridIntegrationTests.pas new file mode 100644 index 000000000..a26388f3f --- /dev/null +++ b/src/tests/experimental/ui/UWalletGridIntegrationTests.pas @@ -0,0 +1,817 @@ +unit UWalletGridIntegrationTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Exercises the real main form, wallet control, cached data sources and both + rendered grids. Only the stored account/history records are mocked. + + Distributed under the MIT software license; see the accompanying LICENSE. +} + +{$mode delphi} + +interface + +uses + Classes, SysUtils, Forms, fpcunit, testregistry, UWalletGridMocks; + +type + TWalletGridIntegrationTests = class(TTestCase) + private + FMain: TMockMainForm; + FWallet: TMockWallet; + FModel: TMockWalletData; + FProducer: TThread; + FDirectory, FPreviousDirectory, FAsyncError: String; + FEvents: TStringList; + FPreviousExceptionHandler: TExceptionEvent; + procedure OnException(Sender: TObject; E: Exception); + procedure PumpFor(AMilliseconds: Cardinal); + procedure Settle; + procedure WaitForRefresh(ACount: Integer; ATimeout: Cardinal = 8000); + procedure CheckRenderedRevision(ARevision: Integer); + procedure CheckRefreshState(const ACheckpoint: String; + AExpectIdle: Boolean = false; AAutomaticUpdating: Boolean = false); + procedure CheckFetchCounts(const ACheckpoint: String; AAccounts, AOperations: Integer); + procedure Log(const AMessage: String); + function NotifyChange(ARevision: Integer): QWord; + procedure CheckProductionWiringAndCachedPaging; + procedure CheckEventDrivenOverview; + procedure CheckSingleInputLoading; + procedure CheckContinuousInputs; + procedure CheckSilentProcessingGap; + procedure CheckSlowFetchDuringContinuousInputs; + procedure CheckImmediateInputsAndIndependentGrids; + procedure CheckHiddenAccountsRequests; + protected + procedure SetUp; override; + procedure TearDown; override; + published + // The controller has a process lifetime. Keep these scenarios together in + // the dedicated runner rather than changing globals in the general suite. + procedure MainGridsCoalesceOperationsAndPauseForNodeWork; + end; + +implementation + +uses + Windows, Variants, Types, Controls, StdCtrls, ExtCtrls, Buttons, UThread, UNode, + UUserInterface, UWalletStartup, UVisualGrid, UCommon, UCommon.UI, UCommon.Data, + UCTRLWallet, UAccounts, UCoreUtils; + +type + TGridInputAccess = class(TVisualGrid) + public + procedure Search(const AExpression: String); + function HasPendingLayout: Boolean; + end; + + TGridUpdateProducer = class(TPCThread) + private + FNode: TNode; + FModel: TMockWalletData; + FStop: LongInt; + protected + procedure BCExecute; override; + public + InputCount: Integer; + constructor Create(ANode: TNode; AModel: TMockWalletData); + destructor Destroy; override; + procedure Stop; + end; + +procedure TGridInputAccess.Search(const AExpression: String); +begin + FSearchEdit.Text := AExpression; + SearchButtonClick(Self); +end; + +function TGridInputAccess.HasPendingLayout: Boolean; +begin + Result := Assigned(FDelayedBoundsChangeTimer) and FDelayedBoundsChangeTimer.Enabled; +end; + +function DesktopName(ADesktop: THandle): String; +var + LName: array[0..255] of Char; + LNeeded: DWORD; +begin + FillChar(LName, SizeOf(LName), 0); + if not GetUserObjectInformation(ADesktop, UOI_NAME, @LName[0], SizeOf(LName), LNeeded) then + RaiseLastOSError; + Result := LName; +end; + +procedure RequirePrivateDesktop; +var + LInput: THandle; + LCurrentName, LInputName: String; +begin + LInput := OpenInputDesktop(0, false, DESKTOP_READOBJECTS); + if LInput = 0 then RaiseLastOSError; + try + LCurrentName := DesktopName(GetThreadDesktop(GetCurrentThreadId)); + LInputName := DesktopName(LInput); + if SameText(LCurrentName, LInputName) or (Pos('PascalCoin', LCurrentName) <> 1) then + raise Exception.Create('Run the grid integration test through the existing private-desktop broker'); + finally + CloseDesktop(LInput); + end; +end; + +constructor TGridUpdateProducer.Create(ANode: TNode; AModel: TMockWalletData); +begin + inherited Create(true); + FreeOnTerminate := false; + FNode := ANode; + FModel := AModel; +end; + +destructor TGridUpdateProducer.Destroy; +begin + Stop; + inherited; +end; + +procedure TGridUpdateProducer.BCExecute; +begin + while InterlockedCompareExchange(FStop, 0, 0) = 0 do begin + FModel.Advance; + FNode.NotifyBlocksChanged; + Inc(InputCount); + Sleep(100); + end; +end; + +procedure TGridUpdateProducer.Stop; +begin + InterlockedExchange(FStop, 1); + WaitFor; +end; + +procedure TWalletGridIntegrationTests.OnException(Sender: TObject; E: Exception); +var + I: Integer; +begin + if FAsyncError <> '' then Exit; + FAsyncError := E.ClassName + ': ' + E.Message; + // Preserve the original GUI exception location before FPCUnit records the + // assertion in PumpFor. This also makes asynchronous startup failures useful. + if Assigned(FEvents) then begin + FEvents.Add('ASYNC ' + FAsyncError); + if Assigned(BackTraceStrFunc) then begin + FEvents.Add(BackTraceStrFunc(ExceptAddr)); + for I := 0 to ExceptFrameCount - 1 do + FEvents.Add(BackTraceStrFunc(ExceptFrames[I])); + end; + FEvents.SaveToFile(ParamStr(1) + '.async.log'); + end; +end; + +procedure TWalletGridIntegrationTests.Log(const AMessage: String); +begin + FEvents.Add(UIntToStr(GetTickCount64) + ' CHECK ' + AMessage); +end; + +procedure TWalletGridIntegrationTests.PumpFor(AMilliseconds: Cardinal); +var + LDeadline: QWord; +begin + LDeadline := GetTickCount64 + AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + if Assigned(FModel) then FModel.DrainEvents(FEvents); + if FAsyncError <> '' then Fail('Asynchronous main-wallet error: ' + FAsyncError); + Sleep(1); + until GetTickCount64 >= LDeadline; +end; + +procedure TWalletGridIntegrationTests.Settle; +var + LDeadline, LStableSince: QWord; + LAccounts, LOperations, LLastAccounts, LLastOperations: TMockFetchMetrics; + LLastAccountPageSize, LLastOperationPageSize: Integer; +begin + LDeadline := GetTickCount64 + 10000; + LStableSince := GetTickCount64; + LLastAccounts := FModel.Metrics(mdkAccounts); + LLastOperations := FModel.Metrics(mdkOperations); + LLastAccountPageSize := FWallet.AccountsGrid.PageSize; + LLastOperationPageSize := FWallet.OperationsGrid.PageSize; + repeat + PumpFor(10); + LAccounts := FModel.Metrics(mdkAccounts); + LOperations := FModel.Metrics(mdkOperations); + if FWallet.AccountsGrid.IsUpdating or FWallet.OperationsGrid.IsUpdating or + FWallet.AccountsGrid.RefreshThrottle.Waiting or FWallet.OperationsGrid.RefreshThrottle.Waiting or + (LAccounts.ActiveFetches <> 0) or (LOperations.ActiveFetches <> 0) or + (LAccounts.FetchCount <> LLastAccounts.FetchCount) or + (LOperations.FetchCount <> LLastOperations.FetchCount) or + TGridInputAccess(FWallet.AccountsGrid).HasPendingLayout or + TGridInputAccess(FWallet.OperationsGrid).HasPendingLayout or + (FWallet.AccountsGrid.PageSize <> LLastAccountPageSize) or + (FWallet.OperationsGrid.PageSize <> LLastOperationPageSize) then + LStableSince := GetTickCount64; + LLastAccounts := LAccounts; + LLastOperations := LOperations; + // First population can change footer/scrollbar capacity. Observe that + // geometry settling before taking exact-query-count baselines. + LLastAccountPageSize := FWallet.AccountsGrid.PageSize; + LLastOperationPageSize := FWallet.OperationsGrid.PageSize; + if GetTickCount64 - LStableSince >= 400 then Exit; + until GetTickCount64 >= LDeadline; + Fail('The real wallet grids did not settle within ten seconds'); +end; + +procedure TWalletGridIntegrationTests.WaitForRefresh(ACount: Integer; ATimeout: Cardinal); +var + LDeadline: QWord; +begin + LDeadline := GetTickCount64 + ATimeout; + while (FWallet.AccountRefreshCount < ACount) and (GetTickCount64 < LDeadline) do PumpFor(10); + AssertEquals('The actual account-grid admission must have the expected count', ACount, FWallet.AccountRefreshCount); +end; + +procedure TWalletGridIntegrationTests.CheckFetchCounts(const ACheckpoint: String; + AAccounts, AOperations: Integer); +begin + AssertEquals(ACheckpoint + ': account queries', AAccounts, FModel.Metrics(mdkAccounts).FetchCount); + AssertEquals(ACheckpoint + ': operation queries', AOperations, FModel.Metrics(mdkOperations).FetchCount); +end; + +procedure TWalletGridIntegrationTests.CheckRenderedRevision(ARevision: Integer); +var + LRow: Variant; + LExpected: String; +begin + AssertTrue('Account grid must display a fetched row', FWallet.AccountsGrid.RowCount > 0); + AssertTrue('Operation grid must display a fetched row', FWallet.OperationsGrid.RowCount > 0); + LExpected := 'mock revision=' + IntToStr(ARevision) + ' '; + LRow := FWallet.AccountsGrid.Rows[0]; + AssertTrue('Account rendering must use the final invalidated snapshot: ' + String(LRow.Name), + Pos(LExpected, String(LRow.Name)) = 1); + LRow := FWallet.OperationsGrid.Rows[0]; + AssertTrue('Operation rendering must use the final invalidated snapshot: ' + String(LRow.Description), + Pos(LExpected, String(LRow.Description)) = 1); + AssertEquals('Account fetch must observe the final revision', ARevision, + FModel.Metrics(mdkAccounts).LastRevision); + AssertEquals('Operation fetch must observe the final revision', ARevision, + FModel.Metrics(mdkOperations).LastRevision); + Log('Both real grids display revision ' + IntToStr(ARevision)); +end; + +procedure TWalletGridIntegrationTests.CheckRefreshState(const ACheckpoint: String; + AExpectIdle: Boolean; AAutomaticUpdating: Boolean); + + procedure CheckGrid(AGrid: TVisualGrid; const AName: String); + var + I, LEditCount, LButtonCount: Integer; + LInputAllowed, LNodeSynchronizing: Boolean; + LComponent: TComponent; + LPanel: TPanel; + LTitle, LSubtitle: TLabel; + + function InputEnabled(AControl: TControl): Boolean; + begin + Result := true; + while Assigned(AControl) do begin + if not AControl.Enabled then Exit(false); + if AControl = AGrid then Exit; + AControl := AControl.Parent; + end; + end; + + begin + LNodeSynchronizing := TUserInterface.SynchronizationStatus.State=nssSynchronizing; + if AGrid=FWallet.AccountsGrid then begin + AssertEquals(ACheckpoint+': accounts admit immediately',Ord(temNone),Ord(AGrid.RefreshThrottle.Mode)); + AssertFalse(ACheckpoint+': accounts have no quiet-time wait',AGrid.RefreshThrottle.Waiting); + end else begin + AssertEquals(ACheckpoint+': operations deliver after burst silence',Ord(temNotifyOnEventBurstFinished),Ord(AGrid.RefreshThrottle.Mode)); + AssertTrue(ACheckpoint+': operations quiet interval is exactly 250 ms',AGrid.RefreshThrottle.Interval.TotalMilliseconds=250); + if AExpectIdle then AssertFalse(ACheckpoint+': completed operations are no longer waiting',AGrid.RefreshThrottle.Waiting); + end; + AssertEquals(ACheckpoint+': '+AName+' node manual card',LNodeSynchronizing,AGrid.LoadingVisible); + AssertEquals(ACheckpoint+': '+AName+' node query suspension',LNodeSynchronizing,AGrid.RefreshSuspended); + AssertTrue('The loading animation retains an enabled grid owner', AGrid.Enabled); + LInputAllowed := not (LNodeSynchronizing or AAutomaticUpdating or AGrid.IsUpdating); + AssertEquals(ACheckpoint + ': ' + AName + ' row input', LInputAllowed, + AGrid.InternalDrawGrid.Enabled); + if AExpectIdle then + AssertFalse('A held loading presentation is independent of an active query', AGrid.IsUpdating); + LPanel := AGrid.FindComponent('LoadingOverlay') as TPanel; + AssertTrue('The actual grid owns its loading card', Assigned(LPanel)); + // Automatic page loading retains its own delayed card after a manual hold + // ends. Its visibility is independent of the synchronization presentation. + if not AAutomaticUpdating and not AGrid.IsUpdating and not AGrid.RefreshThrottle.Waiting then + AssertEquals(ACheckpoint + ': ' + AName + ' visible loading card', LNodeSynchronizing, LPanel.Visible); + if LNodeSynchronizing then begin + LTitle := AGrid.FindComponent('LoadingStatus') as TLabel; + LSubtitle := AGrid.FindComponent('LoadingSubtitle') as TLabel; + AssertTrue(Assigned(LTitle) and Assigned(LSubtitle)); + AssertEquals('The actual title reflects the requested synchronization state', 'Please Wait', LTitle.Caption); + AssertEquals('The actual subtitle reflects the requested synchronization state', 'Synchronizing', LSubtitle.Caption); + AssertTrue('The synchronization subtitle is visible', LSubtitle.Visible); + end; + + // The grid owns its existing search, page-size and page-index controls. + // Walk their real parent chain to check the complete input-region gates; + // a disabled draw grid alone would leave search and paging interactive. + LEditCount := 0; + LButtonCount := 0; + for I := 0 to AGrid.ComponentCount - 1 do begin + LComponent := AGrid.Components[I]; + if LComponent is TEdit then begin + Inc(LEditCount); + // Ordinary fetches make edits read-only; only the manual hold + // disables entire input regions. Navigation has its own disabled parent. + if LNodeSynchronizing then + AssertFalse(ACheckpoint + ': ' + AName + ' held search/paging input', + InputEnabled(TEdit(LComponent))) + else if LInputAllowed then + AssertTrue(ACheckpoint + ': ' + AName + ' ready search/paging input', + InputEnabled(TEdit(LComponent))); + AssertEquals(ACheckpoint + ': ' + AName + ' search/paging read-only', not LInputAllowed, + TEdit(LComponent).ReadOnly); + end else if (LComponent is TButton) or (LComponent is TSpeedButton) then begin + Inc(LButtonCount); + if LNodeSynchronizing then + AssertFalse(ACheckpoint + ': ' + AName + ' search/paging action', InputEnabled(TControl(LComponent))); + end; + end; + AssertTrue('Check search, page-size and page-index input', LEditCount >= 3); + AssertTrue('Check the native page navigation and search buttons', LButtonCount >= 5); + end; + +begin + CheckGrid(FWallet.AccountsGrid, 'accounts'); + CheckGrid(FWallet.OperationsGrid, 'operations'); + Log(ACheckpoint + ': accounts immediate, operations wait for 250 ms silence; node owns synchronization'); +end; + +function TWalletGridIntegrationTests.NotifyChange(ARevision: Integer): QWord; +begin + FModel.SetData(ARevision, 200); + Result := GetTickCount64; + TUserInterface.Node.NotifyBlocksChanged; + Log('Node input revision=' + IntToStr(ARevision) + ' input_tick=' + UIntToStr(Result)); +end; + +procedure TWalletGridIntegrationTests.SetUp; +var + LID: TGUID; + LOptions: TWalletStartupOptions; + LDeadline: QWord; +begin + inherited; + RequirePrivateDesktop; + AssertFalse('The dedicated fixture must start without an existing node', TNode.NodeExists); + FEvents := TStringList.Create; + FPreviousExceptionHandler := Application.OnException; + Application.OnException := OnException; + FPreviousDirectory := TNode.GetPascalCoinDataFolder; + try + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'WalletGridIntegration-' + GUIDToString(LID); + AssertFalse('Fresh test data must not already exist', DirectoryExists(FDirectory)); + LOptions := Default(TWalletStartupOptions); + LOptions.DataDirectory := FDirectory; + LOptions.Offline := true; + LOptions.NoStartupDialogs := true; + TUserInterface.ConfigureStartup(LOptions); + Application.CreateForm(TMockMainForm, FMain); + FModel := FMain.Model; + TUserInterface.StartApplication(FMain); + if not TUserInterface.Started then begin + // Failed controller startup already releases its form graph. + FMain := nil; + FModel := nil; + Fail('The actual application controller did not start: ' + TUserInterface.StateText); + end; + FMain.Show; + LDeadline := GetTickCount64 + 10000; + repeat + PumpFor(10); + if TUserInterface.State = uisError then Fail('Mock wallet startup failed: ' + TUserInterface.StateText); + if Assigned(FMain.MockWallet) then Break; + until GetTickCount64 >= LDeadline; + FWallet := FMain.MockWallet; + AssertTrue('The main form must create the actual mock-backed wallet through its factory', Assigned(FWallet)); + Settle; + Log('Actual main form/controller started offline with typed mock providers'); + except + // FPCUnit does not call TearDown when SetUp itself raises. + TearDown; + raise; + end; +end; + +procedure TWalletGridIntegrationTests.TearDown; +begin + try + if Assigned(FProducer) then begin + TGridUpdateProducer(FProducer).Stop; + FreeAndNil(FProducer); + end; + if Assigned(FModel) then begin + FModel.ReleaseFetches; + FModel.ReleaseBalanceSnapshots; + FModel.DrainEvents(FEvents); + end; + if Assigned(FMain) then begin + TUserInterface.ExitApplication; + FMain := nil; + FWallet := nil; + FModel := nil; + end; + if Assigned(FEvents) then FEvents.SaveToFile(ParamStr(1) + '.events.log'); + finally + Application.OnException := FPreviousExceptionHandler; + FEvents.Free; + FEvents := nil; + TNode.SetPascalCoinDataFolder(FPreviousDirectory); + inherited; + end; +end; + +procedure TWalletGridIntegrationTests.CheckProductionWiringAndCachedPaging; +var + LBefore, LAfter: TMockFetchMetrics; + LAccountsSource, LOperationsSource: IDataSource; + LCell: TRect; + LPoint: LPARAM; + LRow: Variant; +begin + LAccountsSource := FWallet.AccountsSource; + LOperationsSource := FWallet.OperationsSource; + AssertTrue('Accounts must use the same typed source held by the wallet', + FWallet.AccountsGrid.DataSource = LAccountsSource); + AssertTrue('Operations must use the same typed source held by the wallet', + FWallet.OperationsGrid.DataSource = LOperationsSource); + AssertTrue('Account snapshot cache must remain enabled', FWallet.AccountsSource.SnapshotCacheEnabled); + AssertTrue('History snapshot cache must remain enabled', FWallet.OperationsSource.SnapshotCacheEnabled); + AssertTrue(FWallet.AccountsGrid.FetchDataInThread and FWallet.OperationsGrid.FetchDataInThread); + AssertTrue(FWallet.AccountsGrid.AutoPageSize and FWallet.OperationsGrid.AutoPageSize); + AssertTrue(Assigned(FWallet.AccountsGrid.OnFinishedUpdating) and Assigned(FWallet.AccountsGrid.OnSelection)); + AssertTrue(Assigned(FWallet.OperationsGrid.OnSelection)); + AssertEquals('The real wallet account grid admits inputs immediately',Ord(temNone), + Ord(FWallet.AccountsGrid.RefreshThrottle.Mode)); + AssertEquals('The real wallet operations grid coalesces a burst until silence',Ord(temNotifyOnEventBurstFinished), + Ord(FWallet.OperationsGrid.RefreshThrottle.Mode)); + AssertTrue('The real operations quiet interval is exactly 250 ms',FWallet.OperationsGrid.RefreshThrottle.Interval.TotalMilliseconds=250); + CheckRenderedRevision(FModel.Revision); + CheckRefreshState('Initial loaded grids', true); + + LBefore := FModel.Metrics(mdkAccounts); + FWallet.AccountsGrid.PageIndex := 1; + Settle; + LAfter := FModel.Metrics(mdkAccounts); + AssertEquals('Changing an account page requests one page', LBefore.FetchCount + 1, LAfter.FetchCount); + AssertEquals('Account paging reads only the displayed records', FWallet.AccountsGrid.RowCount, + LAfter.PageAccountReadCount - LBefore.PageAccountReadCount); + AssertTrue('One visible page must not read all two hundred accounts', FWallet.AccountsGrid.RowCount < 200); + FWallet.AccountsGrid.PageIndex := 0; + Settle; + + LBefore := FModel.Metrics(mdkOperations); + FWallet.OperationsGrid.PageIndex := 1; + Settle; + LAfter := FModel.Metrics(mdkOperations); + AssertEquals('Changing a history page requests one page', LBefore.FetchCount + 1, LAfter.FetchCount); + AssertEquals('Warm history paging retains the raw snapshot', LBefore.RawReadCount, LAfter.RawReadCount); + AssertEquals('Warm history paging retains resolved membership', LBefore.MembershipReadCount, LAfter.MembershipReadCount); + FWallet.OperationsGrid.PageIndex := 0; + Settle; + + // Click the actual draw grid: the production selection handler must switch + // history membership through the same typed source used by node refreshes. + LCell := FWallet.AccountsGrid.InternalDrawGrid.CellRect(0, 1); + LPoint := LPARAM(((LCell.Top + LCell.Bottom) div 2) shl 16) or + LPARAM((LCell.Left + LCell.Right) div 2); + Windows.SendMessage(FWallet.AccountsGrid.InternalDrawGrid.Handle, WM_LBUTTONDOWN, MK_LBUTTON, LPoint); + Windows.SendMessage(FWallet.AccountsGrid.InternalDrawGrid.Handle, WM_LBUTTONUP, 0, LPoint); + Settle; + AssertFalse('Real account selection must switch off all-wallet history membership', + FWallet.OperationsSource.UseWalletAccounts); + AssertEquals('Only the clicked account is selected', 1, Length(FWallet.OperationsSource.Accounts)); + AssertEquals('The real history grid displays only the selected account', 1, FWallet.OperationsGrid.RowCount); + LRow := FWallet.OperationsGrid.Rows[0]; + AssertEquals('The visible selected operation belongs to the clicked first account', 0, Integer(LRow.AccountNumber)); + FWallet.AccountsGrid.ClearSelection(true); + Settle; + AssertTrue('Clearing selection restores all-wallet history membership', FWallet.OperationsSource.UseWalletAccounts); + AssertTrue('All-wallet history returns more than one operation', FWallet.OperationsGrid.RowCount > 1); + PumpFor(1100); + Log('Production wiring, actual selection callbacks, account page-only reads and history snapshot reuse passed'); +end; + +procedure TWalletGridIntegrationTests.CheckEventDrivenOverview; +var + LReads, LAdmissions: Integer; + LDeadline: QWord; +begin + AssertEquals('Initial totals publish from the aggregate completion event', + '200', FWallet.lblTotalPASA.Caption); + LReads := FModel.Metrics(mdkAccounts).BalanceAccountReadCount; + LAdmissions := FWallet.AccountRefreshCount; + // Hold storage, not grid admission. Pages finish while the independently + // owned aggregate retains the last complete overview. + FModel.HoldBalanceSnapshots; + try + FModel.SetData(4800, 37); + TUserInterface.Node.NotifyBlocksChanged; + WaitForRefresh(LAdmissions + 1, 1800); + Settle; + CheckRefreshState('Completed page with an unfinished overview', true); + AssertEquals('A held aggregate cannot read records', LReads, + FModel.Metrics(mdkAccounts).BalanceAccountReadCount); + AssertEquals('Retain the last completed overview until the worker finishes', + '200', FWallet.lblTotalPASA.Caption); + finally + FModel.ReleaseBalanceSnapshots; + end; + LDeadline := GetTickCount64 + 3000; + while (FWallet.lblTotalPASA.Caption <> '37') and + (GetTickCount64 < LDeadline) do PumpFor(10); + AssertEquals('Worker completion publishes the aggregate without polling', + '37', FWallet.lblTotalPASA.Caption); + AssertEquals('The worker completion paints the complete balance', + TAccountComp.FormatMoney(370666), FWallet.lblTotalPASC.Caption); + AssertEquals('One aggregate visits exactly the latest wallet membership', + LReads + 37, FModel.Metrics(mdkAccounts).BalanceAccountReadCount); + LAdmissions := FWallet.AccountRefreshCount; + FModel.SetData(4801, 200); + TUserInterface.Node.NotifyBlocksChanged; + WaitForRefresh(LAdmissions + 1, 1800); + Settle; + LDeadline := GetTickCount64 + 3000; + while (FWallet.lblTotalPASA.Caption <> '200') and + (GetTickCount64 < LDeadline) do PumpFor(10); + AssertEquals('The next completed generation replaces the overview', + '200', FWallet.lblTotalPASA.Caption); + Log('Overview retains complete totals while storage is held and publishes through worker completion'); +end; + +procedure TWalletGridIntegrationTests.CheckSingleInputLoading; +var + LBaseRefresh, LAccounts, LOperations: Integer; + LInputTick, LDeadline: QWord; + LOverlay: TPanel; +begin + LBaseRefresh := FWallet.AccountRefreshCount; + FModel.GateFetches := true; + LInputTick := NotifyChange(500); + WaitForRefresh(LBaseRefresh + 1, 1800); + LDeadline := GetTickCount64 + 1800; + while ((FModel.Metrics(mdkAccounts).ActiveFetches <> 1) or + (FModel.Metrics(mdkOperations).ActiveFetches <> 1)) and + (GetTickCount64 < LDeadline) do PumpFor(10); + AssertEquals('One held account provider',1,FModel.Metrics(mdkAccounts).ActiveFetches); + AssertEquals('One held operations provider',1,FModel.Metrics(mdkOperations).ActiveFetches); + PumpFor(250); + CheckRefreshState('Single input has real providers in flight'); + LOverlay := FWallet.OperationsGrid.FindComponent('LoadingOverlay') as TPanel; + AssertTrue('A held ordinary query shows its actual loading card',LOverlay.Visible); + FModel.ReleaseFetches; + Settle; + AssertTrue('Operations admission waits at least 250 ms after the input',FWallet.LastOperationRefreshTick-LInputTick >= 250); + AssertTrue('Publication does not add an obsolete multi-second delay',GetTickCount64-LInputTick < 2500); + LAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LOperations := FModel.Metrics(mdkOperations).FetchCount; + CheckRenderedRevision(500); + CheckRefreshState('Accepted data clears loading and enables inputs',true); + PumpFor(250); + AssertEquals('A completed input has one admission',LBaseRefresh+1,FWallet.AccountRefreshCount); + CheckFetchCounts('Completion does not manufacture a trailing page',LAccounts,LOperations); + Log('Accounts start immediately; operations start after 250 ms quiet and clear loading on publication'); +end; + +procedure TWalletGridIntegrationTests.CheckContinuousInputs; +var + LBaseRefresh, LAccounts, LOperations: Integer; + LLastInput: QWord; +begin + LBaseRefresh := FWallet.AccountRefreshCount; + NotifyChange(1000); + WaitForRefresh(LBaseRefresh+1,1800); + Settle; + LAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LOperations := FModel.Metrics(mdkOperations).FetchCount; + FProducer := TGridUpdateProducer.Create(TUserInterface.Node,FModel); + FProducer.Start; + PumpFor(700); + TGridUpdateProducer(FProducer).Stop; + AssertTrue('The real background producer delivers repeated inputs', + TGridUpdateProducer(FProducer).InputCount >= 4); + FreeAndNil(FProducer); + AssertTrue('Inputs continue entering account admission during the burst', + FWallet.AccountRefreshCount > LBaseRefresh+1); + AssertTrue('Idle account providers are not delayed until a quiet deadline', + FModel.Metrics(mdkAccounts).FetchCount > LAccounts); + AssertEquals('Continuous inputs restart operations silence without querying during the burst', + LOperations,FModel.Metrics(mdkOperations).FetchCount); + AssertTrue('Operations are still waiting after the continuous burst',FWallet.OperationsGrid.RefreshThrottle.Waiting); + AssertTrue('Only one account provider can run at a time', + FModel.Metrics(mdkAccounts).ActiveFetches <= 1); + AssertTrue('Only one operations provider can run at a time', + FModel.Metrics(mdkOperations).ActiveFetches <= 1); + LLastInput := NotifyChange(1999); + Settle; + AssertTrue('The latest burst input completes after its bounded quiet interval',GetTickCount64-LLastInput < 2500); + CheckRenderedRevision(1999); + CheckRefreshState('Latest continuous-input pages are ready',true); + Log('Continuous inputs refresh accounts and reset the operations quiet deadline; the latest operations page follows silence'); +end; + +procedure TWalletGridIntegrationTests.CheckSilentProcessingGap; +var + LBaseRefresh,LAccounts,LOperations,LAccountAdmissionsBeforeEnd,LOperationAdmissionsBeforeEnd: Integer; + LEndTick,LDeadline: QWord; +begin + LBaseRefresh := FWallet.AccountRefreshCount; + NotifyChange(2000); + WaitForRefresh(LBaseRefresh+1,1800); + Settle; + CheckRefreshState('Loaded data before physical work',true); + LAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LOperations := FModel.Metrics(mdkOperations).FetchCount; + TUserInterface.Node.BeginBlockChainUpdate; + try + LDeadline := GetTickCount64+3000; + repeat + PumpFor(10); + if FWallet.AccountsGrid.RefreshSuspended and FWallet.OperationsGrid.RefreshSuspended then Break; + until GetTickCount64 >= LDeadline; + AssertTrue('Actual physical work pauses both grids', + FWallet.AccountsGrid.RefreshSuspended and FWallet.OperationsGrid.RefreshSuspended); + NotifyChange(2999); + PumpFor(5500); + AssertTrue('Suspended inputs are still admitted immediately',FWallet.AccountRefreshCount > LBaseRefresh+1); + AssertTrue('Physical processing remains active through notification silence',TUserInterface.Node.IsUpdatingBlockChain); + CheckFetchCounts('A silent backlog cannot start a suspended provider',LAccounts,LOperations); + CheckRenderedRevision(2000); + CheckRefreshState('Only the node lifecycle holds synchronization through silence',true); + LAccountAdmissionsBeforeEnd := FWallet.AccountRefreshCount; + LOperationAdmissionsBeforeEnd := FWallet.OperationRefreshCount; + finally + LEndTick := GetTickCount64; + TUserInterface.Node.EndBlockChainUpdate; + end; + LDeadline := GetTickCount64+1800; + repeat + PumpFor(10); + if (FModel.Metrics(mdkAccounts).FetchCount=LAccounts+1) and + (FModel.Metrics(mdkOperations).FetchCount=LOperations+1) then Break; + until GetTickCount64>=LDeadline; + CheckFetchCounts('Backlog end starts one latest page per grid',LAccounts+1,LOperations+1); + AssertTrue('Backlog completion starts retained pages immediately',GetTickCount64-LEndTick < 1800); + Settle; + // EndBlockChainUpdate emits a real BlocksChanged event while pending state + // is available. It is admitted before node resume and coalesces to one page. + AssertEquals('The real completion event admits one account input', + LAccountAdmissionsBeforeEnd+1,FWallet.AccountRefreshCount); + AssertEquals('The real completion event admits one operations input', + LOperationAdmissionsBeforeEnd+1,FWallet.OperationRefreshCount); + CheckRenderedRevision(2999); + CheckRefreshState('Actual processing end clears the node card',true); + Log('Actual node backlog alone holds queries/cards; completion resumes one latest page without a timer'); +end; + +procedure TWalletGridIntegrationTests.CheckSlowFetchDuringContinuousInputs; +var + LBaseRefresh,LAccounts,LOperations,LAccountAdmissions,LOperationAdmissions: Integer; + LDeadline,LLastInput: QWord; +begin + LBaseRefresh := FWallet.AccountRefreshCount; + FModel.GateFetches := true; + NotifyChange(3000); + WaitForRefresh(LBaseRefresh+1,1800); + LDeadline := GetTickCount64+2500; + while ((FModel.Metrics(mdkAccounts).ActiveFetches <> 1) or + (FModel.Metrics(mdkOperations).ActiveFetches <> 1)) and + (GetTickCount64<LDeadline) do PumpFor(10); + AssertEquals('The account provider is held',1,FModel.Metrics(mdkAccounts).ActiveFetches); + AssertEquals('The operations provider is held',1,FModel.Metrics(mdkOperations).ActiveFetches); + LAccounts := FModel.Metrics(mdkAccounts).FetchCount; + LOperations := FModel.Metrics(mdkOperations).FetchCount; + FProducer := TGridUpdateProducer.Create(TUserInterface.Node,FModel); + FProducer.Start; + PumpFor(700); + TGridUpdateProducer(FProducer).Stop; + AssertTrue('Repeated inputs reach the held actual grids',TGridUpdateProducer(FProducer).InputCount >= 4); + FreeAndNil(FProducer); + AssertTrue('A busy grid records inputs immediately',FWallet.AccountRefreshCount > LBaseRefresh+1); + CheckFetchCounts('Held providers do not start parallel queries',LAccounts,LOperations); + CheckRefreshState('Ordinary loading covers held providers'); + LAccountAdmissions := FWallet.AccountRefreshCount; + LOperationAdmissions := FWallet.OperationRefreshCount; + LLastInput := NotifyChange(3999); + // Node delivery is asynchronous. Observe a new admission after the final + // input; an earlier callback can share its tick and is not a delivery fence. + // Keep both providers held until the final input has reached both grids. + LDeadline := GetTickCount64+1800; + while ((FWallet.AccountRefreshCount<=LAccountAdmissions) or + (FWallet.OperationRefreshCount<=LOperationAdmissions)) and + (GetTickCount64<LDeadline) do PumpFor(5); + AssertTrue('The final input reaches account admission before releasing its provider', + FWallet.AccountRefreshCount>LAccountAdmissions); + AssertTrue('The final input reaches operations admission before releasing its provider', + FWallet.OperationRefreshCount>LOperationAdmissions); + CheckFetchCounts('The final input coalesces behind the held workers',LAccounts,LOperations); + FModel.ReleaseFetches; + Settle; + AssertTrue('Retiring the worker does not add a quiet wait',GetTickCount64-LLastInput < 2500); + CheckFetchCounts('Only one latest query follows each held provider',LAccounts+1,LOperations+1); + CheckRenderedRevision(3999); + CheckRefreshState('Latest held-provider pages clear ordinary loading',true); + Log('Busy providers retain one latest request, run no parallel fetch and publish immediately after retirement'); +end; + +procedure TWalletGridIntegrationTests.CheckImmediateInputsAndIndependentGrids; +var + LAccountAdmissions,LOperationAdmissions,LLatestPage: Integer; + LInputTick,LDeadline: QWord; +begin + NotifyChange(4500); + Settle; + LAccountAdmissions := FWallet.AccountRefreshCount; + LOperationAdmissions := FWallet.OperationRefreshCount; + FModel.GateFetches := true; + // Exercise real page/filter/resize entry points while the first provider is + // held. Visual geometry changes immediately; only the latest query is kept. + LInputTick := GetTickCount64; + FWallet.AccountsGrid.PageIndex := 1; + TGridInputAccess(FWallet.AccountsGrid).Search('mock%'); + FMain.SetBounds(FMain.Left,FMain.Top,FMain.Width+24,FMain.Height+48); + LLatestPage := FWallet.AccountsGrid.PageIndex; + PumpFor(100); + AssertTrue('Account page/filter/resize inputs enter admission immediately', + FWallet.AccountRefreshCount > LAccountAdmissions); + AssertEquals('A resized operations viewport waits for its quiet interval',LOperationAdmissions,FWallet.OperationRefreshCount); + AssertTrue('The operations viewport has a queued quiet-period request',FWallet.OperationsGrid.RefreshThrottle.Waiting); + LDeadline := GetTickCount64+1800; + while (FWallet.OperationRefreshCount=LOperationAdmissions) and (GetTickCount64<LDeadline) do PumpFor(5); + AssertEquals('The resized operations viewport delivers once after silence',LOperationAdmissions+1,FWallet.OperationRefreshCount); + AssertTrue('Account client changes cannot create parallel providers', + FModel.Metrics(mdkAccounts).ActiveFetches <= 1); + AssertTrue('Operations resize cannot create parallel providers', + FModel.Metrics(mdkOperations).ActiveFetches <= 1); + CheckRefreshState('Mixed actual client inputs retain one operations quiet delivery'); + FModel.ReleaseFetches; + Settle; + AssertTrue('Latest client parameters complete within the bounded quiet interval',GetTickCount64-LInputTick < 2500); + AssertEquals('The latest requested account page is retained',LLatestPage,FWallet.AccountsGrid.PageIndex); + CheckRenderedRevision(4500); + LAccountAdmissions := FWallet.AccountRefreshCount; + LOperationAdmissions := FWallet.OperationRefreshCount; + FWallet.OperationsHistory := woh30Days; + AssertFalse('A history-only request cannot show a node synchronization card',FWallet.OperationsGrid.LoadingVisible); + Settle; + AssertEquals('Independent history input does not admit account work',LAccountAdmissions,FWallet.AccountRefreshCount); + AssertEquals('The independent operations input is delivered once',LOperationAdmissions+1,FWallet.OperationRefreshCount); + AssertEquals('The provider receives the selected thirty-day depth', + Integer(TTimeSpan.FromDays(30).TotalBlockCount),Integer(FWallet.OperationsSource.LastFetchBlockDepth)); + CheckRefreshState('Independent history input completes after its operations quiet interval',true); + Log('Actual page/filter/resize inputs and independent history-depth changes passed'); +end; + +procedure TWalletGridIntegrationTests.CheckHiddenAccountsRequests; +var + LReturnedTick: QWord; + LAccountSource: IDataSource; +begin + LAccountSource := FWallet.AccountsGrid.DataSource; + FWallet.AccountsMode := wamFirstAccount; + AssertFalse('The get-account view detaches the actual account grid',Assigned(FWallet.AccountsGrid.Parent)); + NotifyChange(4600); + PumpFor(250); + AssertTrue('Hidden input retains the same source',LAccountSource=FWallet.AccountsGrid.DataSource); + AssertFalse('A hidden ordinary refresh cannot show a node card',FWallet.AccountsGrid.LoadingVisible); + AssertFalse('A hidden ordinary refresh has no quiet-time wait',FWallet.AccountsGrid.RefreshThrottle.Waiting); + LReturnedTick := GetTickCount64; + FWallet.AccountsMode := wamMyAccounts; + AssertTrue('Returning reparents the same account grid',Assigned(FWallet.AccountsGrid.Parent)); + Settle; + AssertTrue('Returning publishes without a quiet-time delay',GetTickCount64-LReturnedTick < 2500); + CheckRenderedRevision(4600); + CheckRefreshState('Hidden input publishes the latest rows after reparenting',true); + Log('Actual get-account view, hidden node input and immediate returning view passed'); +end; + +procedure TWalletGridIntegrationTests.MainGridsCoalesceOperationsAndPauseForNodeWork; +begin + CheckProductionWiringAndCachedPaging; + CheckEventDrivenOverview; + CheckSingleInputLoading; + CheckContinuousInputs; + CheckSilentProcessingGap; + CheckSlowFetchDuringContinuousInputs; + CheckImmediateInputsAndIndependentGrids; + CheckHiddenAccountsRequests; +end; + +initialization + RegisterTest(TWalletGridIntegrationTests); + +end. diff --git a/src/tests/experimental/ui/UWalletGridRefreshTests.pas b/src/tests/experimental/ui/UWalletGridRefreshTests.pas new file mode 100644 index 000000000..2a87099a5 --- /dev/null +++ b/src/tests/experimental/ui/UWalletGridRefreshTests.pas @@ -0,0 +1,109 @@ +unit UWalletGridRefreshTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. + The shared fixture covers grid behavior; this unit adds the real node producer. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, Forms, fpcunit, testregistry, UNode, UCrypto, + UNetProtocol, UVisualGrid.Refresh.Tests; + +implementation + +type + TWalletGridRefreshTests = class(TVisualGridRefreshFixture) + private + FNodeCallbacks: Integer; + procedure NodeChanged(Sender: TObject); + published + procedure SettledGridDoesNotFetchOnBackendNotificationTimers; + end; + + TGridNotificationProducer = class(TThread) + private + FNode: TNode; + FStop: LongInt; + protected + procedure Execute; override; + public + constructor Create(ANode: TNode); + procedure Stop; + end; + +constructor TGridNotificationProducer.Create(ANode: TNode); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; +end; + +procedure TGridNotificationProducer.Execute; +begin + while InterlockedCompareExchange(FStop, 0, 0) = 0 do begin + FNode.NotifyBlocksChanged; + Sleep(10); + end; +end; + +procedure TGridNotificationProducer.Stop; +begin + InterlockedExchange(FStop, 1); + WaitFor; +end; + +procedure TWalletGridRefreshTests.NodeChanged(Sender: TObject); +begin + Inc(FNodeCallbacks); +end; + +procedure TWalletGridRefreshTests.SettledGridDoesNotFetchOnBackendNotificationTimers; +var LNotifier: TNodeNotifyEvents; LProducer: TGridNotificationProducer; + LFetches, LCompleted: Integer; + LNode: TNode; LOwnsNetData: Boolean; LPreviousDirectory, LDirectory: String; + LID: TGUID; +begin + AssertFalse('The fixture must not reuse a global node', TNode.NodeExists); + TCrypto.InitCrypto; + CreateGUID(LID); + LDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'GridRefresh-' + GUIDToString(LID); + ForceDirectories(LDirectory); + LPreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(LDirectory); + LOwnsNetData := not TNetData.NetDataExists; + LNode := TNode.Create(nil); + LNode.BroadcastData := False; + LNotifier := TNodeNotifyEvents.Create(nil); + LNotifier.Node := LNode; + LNotifier.OnBlocksChanged := NodeChanged; + LNotifier.OnOperationsChanged := NodeChanged; + LProducer := TGridNotificationProducer.Create(LNode); + try + LFetches := FetchCount; + LCompleted := FCompletions; + LProducer.Start; + PumpFor(1300); + LProducer.Stop; + PumpFor(150); + AssertTrue('Real queued producer notifications must reach the UI', FNodeCallbacks > 5); + AssertEquals('Producer and grid timers alone must not request another page', LFetches, FetchCount); + AssertEquals(LCompleted, FCompletions); + AssertFalse(FGrid.IsUpdating); + finally + LProducer.Stop; + LProducer.Free; + LNotifier.OnBlocksChanged := nil; + LNotifier.OnOperationsChanged := nil; + LNotifier.Free; + if LOwnsNetData and TNetData.NetDataExists then TNetData.NetData.Free; + LNode.Free; + TNode.SetPascalCoinDataFolder(LPreviousDirectory); + RemoveDir(LDirectory); + end; +end; + +initialization + RegisterTest(TWalletGridRefreshTests); +end. diff --git a/src/tests/experimental/ui/UWalletHistoryTests.pas b/src/tests/experimental/ui/UWalletHistoryTests.pas new file mode 100644 index 000000000..0bd911fc1 --- /dev/null +++ b/src/tests/experimental/ui/UWalletHistoryTests.pas @@ -0,0 +1,615 @@ +unit UWalletHistoryTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + Generated operations exercise pending history without a node, and stored + history through a fresh owned node/storage fixture. No peers are contacted. } + +{$mode delphi} + +interface + +uses + Classes, SysUtils, SyncObjs, fpcunit, testregistry, Generics.Collections, UCrypto, + UAccounts, UBlockChain, UOpTransaction, UPCDataTypes, UCoreUtils, + UOperationPreparationTests, UOrderedList, UNode; + +type + TCountingHistoryTransaction = class(TOpTransaction) + public + class var AffectedCalls: Integer; + class var DelayAffectedMS: Integer; + class var AffectedEntered: TEvent; + procedure AffectedAccounts(AList: TOrderedList<Cardinal>); override; + end; + + TWalletHistoryTests = class(TTestCase) + private + FKey: TECPrivateKey; + FSafeBox: TOperationSafeBox; + FSnapshot: TPCSafeBoxTransaction; + FPrevious: TAccountPreviousBlockInfo; + FTree: TOperationsHashTree; + procedure AddTransaction(ATree: TOperationsHashTree; ASender, ANOperation, + AReceiver: Cardinal; AAmount: UInt64); + function History(const AAccounts: array of Cardinal; ASkip: Integer = 0; + ATake: Integer = MaxInt): TArray<TOperationResume>; + procedure AssertRow(const ARow: TOperationResume; AAccount: Cardinal; AIndex: Integer); + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure PreservesAccountMajorOrderAndActualOperationIndexes; + procedure UsesPendingBalanceForEachAffectedAccount; + procedure DuplicateRequestedAccountsRetainTheirRows; + procedure PagesAcrossAccountBoundariesWithoutOverflow; + procedure EmptyAndZeroTakeAvoidOperationTraversal; + procedure ManyAccountsWalkThePendingTreeOnce; + procedure TerminatedHistoryFetchJoinsAndDoesNotCachePartialRows; + end; + + TWalletHistorySourceTests = class(TTestCase) + published + procedure SettingsRemainResponsiveDuringBlockedFetch; + end; + + TWalletStoredHistoryTests = class(TTestCase) + published + procedure StoredBalanceAboveCardinalRetainsExactValue; + procedure ReverseSpendCrossesCardinalBoundary; + procedure AllHistoryDepthIncludesTheStoredBlock; + procedure ActualHistoryPagePublishesSigned64BitBalances; + procedure FiniteDepthStopsAtTheInclusiveBoundary; + procedure ZeroDepthReadsTheTipOnly; + procedure OldAccountAvoidsStoredReads; + procedure FinitePagesStopOnceTheRequestedRowsAreFound; + procedure ZeroTakeAvoidsStoredReads; + procedure FiniteHistoryRetainsPendingOperations; + procedure LivePendingIndexAvoidsUnrelatedTraversal; + procedure StoredOperationHashesUseActualBlockHeight; + procedure MaximumReadsLegacyHistoryBeforeThePrimaryRange; + procedure MaximumReadsLegacyHistoryInsideAPrimaryGap; + procedure MissingLegacyBlockDoesNotPublishAnotherBlocksOperations; + end; + +implementation + +uses UConst, UBaseTypes, UMemory, UThread, UDataSources, UCommon.Data, + UFileStorage, UStoredHistoryChecks; + +type + TCancellableHistorySource = class(TOperationsDataSourceBase) + private + FTree: TOperationsHashTree; + FSnapshot: TPCSafeBoxTransaction; + public + Fetches: Integer; + constructor Create(ATree: TOperationsHashTree; ASnapshot: TPCSafeBoxTransaction); + procedure FetchAll(const AContainer: TList<TOperationResume>); override; + function Page: Integer; + end; + +procedure TWalletStoredHistoryTests.StoredBalanceAboveCardinalRetainsExactValue; +begin + CheckStoredHistory(shcWideBalance); +end; + +procedure TWalletStoredHistoryTests.ReverseSpendCrossesCardinalBoundary; +begin + CheckStoredHistory(shcReverseBoundary); +end; + +procedure TWalletStoredHistoryTests.AllHistoryDepthIncludesTheStoredBlock; +begin + CheckStoredHistory(shcAllHistory); +end; + +procedure TWalletStoredHistoryTests.ActualHistoryPagePublishesSigned64BitBalances; +begin + CheckStoredHistory(shcActualPage); +end; + +procedure TWalletStoredHistoryTests.FiniteDepthStopsAtTheInclusiveBoundary; +begin + CheckStoredHistory(shcFiniteDepth); +end; + +procedure TWalletStoredHistoryTests.ZeroDepthReadsTheTipOnly; +begin + CheckStoredHistory(shcTipOnly); +end; + +procedure TWalletStoredHistoryTests.OldAccountAvoidsStoredReads; +begin + CheckStoredHistory(shcOldAccount); +end; + +procedure TWalletStoredHistoryTests.FinitePagesStopOnceTheRequestedRowsAreFound; +begin + CheckStoredHistory(shcFinitePage); +end; + +procedure TWalletStoredHistoryTests.ZeroTakeAvoidsStoredReads; +begin + CheckStoredHistory(shcZeroTake); +end; + +procedure TWalletStoredHistoryTests.FiniteHistoryRetainsPendingOperations; +begin + CheckStoredHistory(shcFiniteWithPending); +end; + +procedure TWalletStoredHistoryTests.LivePendingIndexAvoidsUnrelatedTraversal; +begin + CheckStoredHistory(shcLivePendingFastPath); +end; + +procedure TWalletStoredHistoryTests.StoredOperationHashesUseActualBlockHeight; +begin + CheckStoredHistory(shcActualBlockHashes); +end; + +procedure TWalletStoredHistoryTests.MaximumReadsLegacyHistoryBeforeThePrimaryRange; +begin + CheckStoredHistory(shcLayeredBeforePrimary); +end; + +procedure TWalletStoredHistoryTests.MaximumReadsLegacyHistoryInsideAPrimaryGap; +begin + CheckStoredHistory(shcLayeredPrimaryGap); +end; + +procedure TWalletStoredHistoryTests.MissingLegacyBlockDoesNotPublishAnotherBlocksOperations; +begin + CheckStoredHistory(shcLayeredMissingBlock); +end; + +type + TCancellableHistoryThread = class(TThread) + private + FSource: TCancellableHistorySource; + protected + procedure Execute; override; + public + Completed: TEvent; + Cancelled: Boolean; + Rows: Integer; + ErrorMessage: String; + constructor Create(ASource: TCancellableHistorySource); + destructor Destroy; override; + end; + + TBlockedHistorySource = class(TAccountsOperationsDataSource) + public + Entered, ReleaseFetch: TEvent; + constructor Create; + destructor Destroy; override; + procedure FetchAll(const AContainer: TList<TOperationResume>); override; + procedure FetchTestPage; + end; + + THistoryFetchThread = class(TPCThread) + private + FSource: TBlockedHistorySource; + protected + procedure BCExecute; override; + public + ErrorMessage: String; + constructor Create(ASource: TBlockedHistorySource); + end; + + THistorySettingsThread = class(TPCThread) + private + FSource: TBlockedHistorySource; + protected + procedure BCExecute; override; + public + Completed: TEvent; + ErrorMessage: String; + constructor Create(ASource: TBlockedHistorySource); + destructor Destroy; override; + end; + +constructor TCancellableHistorySource.Create(ATree: TOperationsHashTree; + ASnapshot: TPCSafeBoxTransaction); +begin + inherited Create(nil); + FTree := ATree; + FSnapshot := ASnapshot; + SnapshotCacheEnabled := True; +end; + +procedure TCancellableHistorySource.FetchAll(const AContainer: TList<TOperationResume>); +begin + Inc(Fetches); + AContainer.AddRange(TCoreTool.GetPendingOperationsAffectingAccounts( + FTree, FSnapshot, 160, [100,101], 0, MaxInt)); +end; + +function TCancellableHistorySource.Page: Integer; +var LTable: TDataTable; LResult: TPageFetchResult; +begin + LResult := FetchPage(TPageFetchParams.Create(0,25,nil,foAnd),LTable); + Result := LResult.TotalDataCount; +end; + +constructor TCancellableHistoryThread.Create(ASource: TCancellableHistorySource); +begin + inherited Create(True); + FreeOnTerminate := False; + FSource := ASource; + Completed := TEvent.Create(nil,True,False,''); + Start; +end; + +destructor TCancellableHistoryThread.Destroy; +begin + Terminate; + WaitFor; + Completed.Free; + inherited Destroy; +end; + +procedure TCancellableHistoryThread.Execute; +begin + try + try + Rows := FSource.Page; + except + on E: EAbort do Cancelled := True; + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TBlockedHistorySource.Create; +begin + inherited Create(nil); + Entered := TEvent.Create(nil,True,False,''); + ReleaseFetch := TEvent.Create(nil,True,False,''); +end; + +destructor TBlockedHistorySource.Destroy; +begin + Entered.Free; + ReleaseFetch.Free; + inherited Destroy; +end; + +procedure TBlockedHistorySource.FetchAll(const AContainer: TList<TOperationResume>); +begin + // This runs under the real base FetchPage lock, without opening a node. + Entered.SetEvent; + if ReleaseFetch.WaitFor(10000) <> wrSignaled then + raise Exception.Create('History fetch gate timed out'); +end; + +procedure TBlockedHistorySource.FetchTestPage; +var LTable: TDataTable; +begin + FetchPage(TPageFetchParams.Create(0,25,nil,foAnd),LTable); +end; + +constructor THistoryFetchThread.Create(ASource: TBlockedHistorySource); +begin + inherited Create(True); + FreeOnTerminate := False; + FSource := ASource; + Start; +end; + +procedure THistoryFetchThread.BCExecute; +begin + try + FSource.FetchTestPage; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; +end; + +constructor THistorySettingsThread.Create(ASource: TBlockedHistorySource); +begin + inherited Create(True); + FreeOnTerminate := False; + FSource := ASource; + Completed := TEvent.Create(nil,True,False,''); + Start; +end; + +destructor THistorySettingsThread.Destroy; +begin + inherited Destroy; + Completed.Free; +end; + +procedure THistorySettingsThread.BCExecute; +var LAccounts: TArray<Cardinal>; +begin + try + try + FSource.Accounts := TArray<Cardinal>.Create(100,101); + LAccounts := FSource.Accounts; + if (Length(LAccounts) <> 2) or (LAccounts[0] <> 100) or (LAccounts[1] <> 101) then + raise Exception.Create('History account selection was not updated'); + LAccounts[0] := 999; + LAccounts := FSource.Accounts; + if LAccounts[0] <> 100 then + raise Exception.Create('History getter exposed the stored account array'); + FSource.BlockDepth := 400; + if FSource.BlockDepth <> 400 then + raise Exception.Create('History block depth was not updated'); + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +procedure TCountingHistoryTransaction.AffectedAccounts(AList: TOrderedList<Cardinal>); +begin + Inc(AffectedCalls); + if Assigned(AffectedEntered) then AffectedEntered.SetEvent; + if DelayAffectedMS > 0 then Sleep(DelayAffectedMS); + inherited AffectedAccounts(AList); +end; + +procedure TWalletHistoryTests.TerminatedHistoryFetchJoinsAndDoesNotCachePartialRows; +var + LSource: TCancellableHistorySource; + LWorker: TCancellableHistoryThread; + LEntered: TEvent; + LStarted: QWord; +begin + LEntered := TEvent.Create(nil,True,False,''); + LSource := TCancellableHistorySource.Create(FTree,FSnapshot); + LWorker := nil; + try + TCountingHistoryTransaction.DelayAffectedMS := 250; + TCountingHistoryTransaction.AffectedEntered := LEntered; + LWorker := TCancellableHistoryThread.Create(LSource); + AssertEquals('The actual pending-history query must start',Ord(wrSignaled),Ord(LEntered.WaitFor(3000))); + LStarted := GetTickCount64; + LWorker.Terminate; + AssertEquals('Termination must join without releasing any test gate',Ord(wrSignaled),Ord(LWorker.Completed.WaitFor(1500))); + LWorker.WaitFor; + AssertTrue('History traversal must observe the owning worker termination',LWorker.Cancelled); + AssertEquals('',LWorker.ErrorMessage); + AssertEquals(0,LWorker.Rows); + AssertTrue('History cancellation must stop before traversing the remaining operations',GetTickCount64-LStarted<1000); + TCountingHistoryTransaction.AffectedEntered := nil; + TCountingHistoryTransaction.DelayAffectedMS := 0; + AssertEquals('A later normal query must rebuild the complete history',4,LSource.Page); + AssertEquals('Cancelled snapshots must not enter the cache',2,LSource.Fetches); + AssertEquals(4,LSource.Page); + AssertEquals('The complete replacement snapshot remains reusable',2,LSource.Fetches); + finally + if Assigned(LWorker) then begin + LWorker.Terminate; + LWorker.WaitFor; + LWorker.Free; + end; + TCountingHistoryTransaction.AffectedEntered := nil; + TCountingHistoryTransaction.DelayAffectedMS := 0; + LSource.Free; + LEntered.Free; + end; +end; + +procedure TWalletHistoryTests.SetUp; +var + LAccount: TAccount; + LError: String; + i: Integer; +begin + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FSafeBox := TOperationSafeBox.Create; + FSafeBox.Build(FKey.PublicKey); + for i := 100 to 115 do begin + LAccount := FSafeBox.Account(i); + LAccount.balance := UInt64(i - 99) * 1000000; + LAccount.n_operation := 0; + FSafeBox.Seed(LAccount); + end; + FSnapshot := TPCSafeBoxTransaction.Create(FSafeBox); + FPrevious := TAccountPreviousBlockInfo.Create; + FTree := TOperationsHashTree.Create; + AddTransaction(FTree,100,1,101,100); + AddTransaction(FTree,102,1,103,200); + AddTransaction(FTree,100,2,102,300); + AddTransaction(FTree,104,1,101,400); + for i := 0 to FTree.OperationsCount - 1 do begin + LError := ''; + AssertTrue('Fixture operation must pass actual core validation: ' + LError, + FTree.GetOperation(i).DoOperation(FPrevious,FSnapshot,LError)); + end; + TCountingHistoryTransaction.AffectedCalls := 0; +end; + +procedure TWalletHistoryTests.TearDown; +begin + FTree.Free; + FPrevious.Free; + FSnapshot.Free; + FSafeBox.Free; + FKey.Free; +end; + +procedure TWalletHistoryTests.AddTransaction(ATree: TOperationsHashTree; + ASender, ANOperation, AReceiver: Cardinal; AAmount: UInt64); +var LOperation: TCountingHistoryTransaction; +begin + LOperation := TCountingHistoryTransaction.CreateTransaction(CT_PROTOCOL_6, + ASender,ANOperation,AReceiver,FKey,AAmount,10,CT_TOperationPayload_NUL); + try + AssertTrue(ATree.AddOperationToHashTree(LOperation)); + finally + LOperation.Free; + end; +end; + +function TWalletHistoryTests.History(const AAccounts: array of Cardinal; + ASkip, ATake: Integer): TArray<TOperationResume>; +begin + Result := TCoreTool.GetPendingOperationsAffectingAccounts(FTree,FSnapshot, + 160,AAccounts,ASkip,ATake); +end; + +procedure TWalletHistoryTests.AssertRow(const ARow: TOperationResume; + AAccount: Cardinal; AIndex: Integer); +begin + AssertEquals('Affected account context',Int64(AAccount),Int64(ARow.AffectedAccount)); + AssertEquals('Original tree operation index',AIndex,ARow.NOpInsideBlock); + AssertEquals('Pending block context',160,Integer(ARow.Block)); + AssertTrue('Operation hash is available',Length(ARow.OperationHash) > 0); +end; + +procedure TWalletHistoryTests.PreservesAccountMajorOrderAndActualOperationIndexes; +var LRows: TArray<TOperationResume>; +begin + LRows := History([100,101]); + AssertEquals(4,Length(LRows)); + AssertRow(LRows[0],100,2); + AssertRow(LRows[1],100,0); + AssertRow(LRows[2],101,3); + AssertRow(LRows[3],101,0); + AssertEquals('One affected-account evaluation per pending operation',4, + TCountingHistoryTransaction.AffectedCalls); +end; + +procedure TWalletHistoryTests.UsesPendingBalanceForEachAffectedAccount; +var LRows: TArray<TOperationResume>; +begin + LRows := History([100,101]); + AssertEquals(Int64(999580),LRows[0].Balance); + AssertEquals(Int64(999580),LRows[1].Balance); + AssertEquals(Int64(2000500),LRows[2].Balance); + AssertEquals(Int64(2000500),LRows[3].Balance); + AssertEquals(Int64(-300),LRows[0].Amount); + AssertEquals(Int64(-10),LRows[0].Fee); + AssertEquals(Int64(400),LRows[2].Amount); + AssertEquals(Int64(0),LRows[2].Fee); +end; + +procedure TWalletHistoryTests.DuplicateRequestedAccountsRetainTheirRows; +var LRows: TArray<TOperationResume>; +begin + LRows := History([101,100,101]); + AssertEquals(6,Length(LRows)); + AssertRow(LRows[0],101,3); + AssertRow(LRows[1],101,0); + AssertRow(LRows[2],100,2); + AssertRow(LRows[3],100,0); + AssertRow(LRows[4],101,3); + AssertRow(LRows[5],101,0); + AssertEquals('Duplicate query accounts do not repeat tree scanning',4, + TCountingHistoryTransaction.AffectedCalls); +end; + +procedure TWalletHistoryTests.PagesAcrossAccountBoundariesWithoutOverflow; +var LRows: TArray<TOperationResume>; +begin + LRows := History([100,101],1,2); + AssertEquals(2,Length(LRows)); + AssertRow(LRows[0],100,0); + AssertRow(LRows[1],101,3); + LRows := History([100,101],3,MaxInt); + AssertEquals(1,Length(LRows)); + AssertRow(LRows[0],101,0); + LRows := History([100,101],MaxInt,MaxInt); + AssertEquals('Paging arithmetic must not overflow',0,Length(LRows)); +end; + +procedure TWalletHistoryTests.EmptyAndZeroTakeAvoidOperationTraversal; +var + LRows: TArray<TOperationResume>; + LScope: TDisposables; + LEmpty: TOperationsHashTree; +begin + LRows := History([]); + AssertEquals(0,Length(LRows)); + LRows := History([100],0,0); + AssertEquals(0,Length(LRows)); + LEmpty := LScope.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + LRows := TCoreTool.GetPendingOperationsAffectingAccounts(LEmpty,nil,160,[100],0,MaxInt); + AssertEquals(0,Length(LRows)); + AssertEquals(0,TCountingHistoryTransaction.AffectedCalls); +end; + +procedure TWalletHistoryTests.ManyAccountsWalkThePendingTreeOnce; +var + LScope: TDisposables; + LTree: TOperationsHashTree; + LAccounts: TArray<Cardinal>; + LRows: TArray<TOperationResume>; + i: Integer; +begin + LTree := LScope.AddObject(TOperationsHashTree.Create) as TOperationsHashTree; + for i := 0 to 511 do + AddTransaction(LTree,100 + Cardinal(i mod 16),1 + Cardinal(i div 16), + 500 + Cardinal(i mod 16),1 + UInt64(i)); + SetLength(LAccounts,32); + for i := 0 to 15 do begin + LAccounts[i] := 100 + Cardinal(i); + LAccounts[i + 16] := 500 + Cardinal(i); + end; + TCountingHistoryTransaction.AffectedCalls := 0; + LRows := TCoreTool.GetPendingOperationsAffectingAccounts(LTree,FSnapshot, + 160,LAccounts,0,MaxInt); + AssertEquals('Both owned sender/receiver contexts are retained',1024,Length(LRows)); + AssertEquals('Query size must not multiply pending traversal',512, + TCountingHistoryTransaction.AffectedCalls); + AssertRow(LRows[0],100,496); + AssertRow(LRows[512],500,496); +end; + +procedure TWalletHistorySourceTests.SettingsRemainResponsiveDuringBlockedFetch; +var + LSource: TBlockedHistorySource; + LFetch: THistoryFetchThread; + LSettings: THistorySettingsThread; +begin + LSource := TBlockedHistorySource.Create; + LFetch := nil; + LSettings := nil; + try + LFetch := THistoryFetchThread.Create(LSource); + AssertEquals('Fetch must hold the base data-source lock',Ord(wrSignaled), + Ord(LSource.Entered.WaitFor(5000))); + LSettings := THistorySettingsThread.Create(LSource); + AssertEquals('All history settings accessors must complete while fetch is blocked', + Ord(wrSignaled),Ord(LSettings.Completed.WaitFor(3000))); + AssertEquals('The fetch gate must remain closed until settings have completed', + Ord(wrTimeout),Ord(LSource.ReleaseFetch.WaitFor(0))); + AssertEquals('',LSettings.ErrorMessage); + LSource.ReleaseFetch.SetEvent; + LFetch.WaitFor; + AssertEquals('',LFetch.ErrorMessage); + finally + // Open the gate even after an assertion fails, then join before freeing + // either the data source or its worker-owned events. + LSource.ReleaseFetch.SetEvent; + if Assigned(LSettings) then begin + LSettings.WaitFor; + LSettings.Free; + end; + if Assigned(LFetch) then begin + LFetch.WaitFor; + LFetch.Free; + end; + LSource.Free; + end; +end; + +initialization + RegisterTest(TWalletHistoryTests); + RegisterTest(TWalletHistorySourceTests); + RegisterTest(TWalletStoredHistoryTests); +end. diff --git a/src/tests/experimental/ui/UWalletInfoTests.pas b/src/tests/experimental/ui/UWalletInfoTests.pas new file mode 100644 index 000000000..61ecc6de4 --- /dev/null +++ b/src/tests/experimental/ui/UWalletInfoTests.pas @@ -0,0 +1,136 @@ +unit UWalletInfoTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, fpcunit, testregistry, UCrypto, UBlockChain, UPCDataTypes; + +type + TWalletInfoTests = class(TTestCase) + private + FKey: TECPrivateKey; + function Account: TAccount; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure AccountDetailsIncludeDataSealAndJson; + procedure PrivateSaleDetailsUseSaleState; + procedure SwapDetailsIncludeHashAndCounterparty; + procedure MultioperationDetailsIncludeAllParticipants; + procedure ShortOperationHashesKeepTheirPrefixAndSuffix; + end; + +implementation + +uses UAccounts, UConst, UCoreUtils, UBaseTypes, UCellRenderers; + +procedure TWalletInfoTests.ShortOperationHashesKeepTheirPrefixAndSuffix; +begin + AssertEquals('', TCellRenderers.OperationShortHash('')); + AssertEquals('12345678', TCellRenderers.OperationShortHash('12345678')); + AssertEquals('1234...6789', TCellRenderers.OperationShortHash('123456789')); + AssertEquals('1234...9ABC', TCellRenderers.OperationShortHash('123456789ABC')); +end; + +procedure TWalletInfoTests.SetUp; +begin + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); +end; + +procedure TWalletInfoTests.TearDown; +begin + FKey.Free; +end; + +function TWalletInfoTests.Account: TAccount; +begin + Result := CT_Account_NUL; + Result.account := 123; + Result.accountInfo.state := as_Normal; + Result.accountInfo.accountKey := FKey.PublicKey; +end; + +procedure TWalletInfoTests.AccountDetailsIncludeDataSealAndJson; +var + value: TAccount; + text: String; +begin + value := Account; + value.account_data := TCrypto.HexaToRaw('00FF0102'); + value.account_seal := TCrypto.HexaToRaw('01020304'); + text := value.GetInfoText(Cardinal(0)); + AssertTrue(Pos('Account Data: ' + value.account_data.ToHexaString, text) > 0); + AssertTrue(Pos('Account Seal: ' + value.account_seal.ToHexaString, text) > 0); + AssertTrue(Pos('ACCOUNT JSON:', text) > 0); + AssertTrue(Pos('"data"', text) > 0); + AssertTrue(Pos('"seal"', text) > 0); +end; + +procedure TWalletInfoTests.PrivateSaleDetailsUseSaleState; +var + value: TAccount; + text: String; +begin + value := Account; + value.accountInfo.state := as_ForSale; + value.accountInfo.new_publicKey := FKey.PublicKey; + value.accountInfo.price := 20000; + value.accountInfo.locked_until_block := 50; + text := value.GetInfoText(Cardinal(0)); + AssertTrue(Pos('** Private sale **', text) > 0); + AssertTrue(Pos('New Base58 Public key:', text) > 0); + AssertTrue(Pos('PURCHASE IS SECURE UNTIL BLOCK 50', text) > 0); +end; + +procedure TWalletInfoTests.SwapDetailsIncludeHashAndCounterparty; +var + value: TAccount; + text: String; +begin + value := Account; + value.accountInfo.state := as_ForAtomicCoinSwap; + value.accountInfo.price := 10000; + value.accountInfo.account_to_pay := 456; + value.accountInfo.hashed_secret := TCrypto.DoSha256(TEncoding.UTF8.GetBytes('Generated fixture secret')); + text := value.GetInfoText(Cardinal(0)); + AssertTrue(Pos('Atomic Coin Swap', text) > 0); + AssertTrue(Pos(TAccountComp.AccountNumberToAccountTxtNumber(456), text) > 0); + AssertTrue(Pos(value.accountInfo.hashed_secret.ToHexaString, text) > 0); + AssertTrue(Pos('"coin_swap"', text) > 0); +end; + +procedure TWalletInfoTests.MultioperationDetailsIncludeAllParticipants; +var + operation: TOperationResume; + text: String; +begin + operation := CT_TOperationResume_NUL; + operation.valid := true; + operation.isMultiOperation := true; + SetLength(operation.Senders, 1); + SetLength(operation.Receivers, 1); + SetLength(operation.Changers, 1); + operation.Senders[0].Account := 123; + operation.Receivers[0].Account := 456; + operation.Changers[0].Account := 789; + text := operation.GetInfoText(Cardinal(0)); + AssertTrue(Pos('Multioperation: 1 senders, 1 receivers, 1 account changes', text) > 0); + AssertTrue(Pos('OPERATION JSON:', text) > 0); + AssertTrue(Pos('"senders"', text) > 0); + AssertTrue(Pos('"receivers"', text) > 0); + AssertTrue(Pos('"changers"', text) > 0); + AssertTrue(Pos('789', text) > 0); +end; + +initialization + RegisterTest(TWalletInfoTests); +end. diff --git a/src/tests/experimental/ui/UWalletLanguageTests.pas b/src/tests/experimental/ui/UWalletLanguageTests.pas new file mode 100644 index 000000000..dd5ec2f6e --- /dev/null +++ b/src/tests/experimental/ui/UWalletLanguageTests.pas @@ -0,0 +1,175 @@ +unit UWalletLanguageTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, Forms, fpcunit, testregistry, UWalletLanguages; + +type + TLanguageTestForm = class(TForm) + end; + + TWalletLanguageTests = class(TTestCase) + private + FDirectory: String; + procedure WriteCatalog(const ACode, ATranslation: String); + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure CatalogCanBeParsedByNativeTranslator; + procedure ListsOnlyUsableCatalogsAndEnglish; + procedure MissingCatalogFallsBackWithoutChangingPreference; + procedure PreferenceRoundTripsThroughExistingAppParams; + procedure NativeTranslatorHandlesExistingAndFutureForms; + procedure RejectsInvalidLanguagePreference; + end; + +implementation + +uses UMemory, UAppParams, LResources, Translations; + +{$R LanguageTestForm.lfm} + +procedure TWalletLanguageTests.SetUp; +var + id: TGUID; +begin + CreateGUID(id); + FDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'PascalCoinLanguageTest-' + GUIDToString(id); + ForceDirectories(FDirectory); +end; + +procedure TWalletLanguageTests.TearDown; +const + codes: array[0..2] of String = ('nl', 'de', 'fr'); +var + code, path: String; +begin + // The native LCL owns this translator; detach the fixture before deleting files. + FreeAndNil(LRSTranslator); + for code in codes do begin + path := FDirectory + PathDelim + code + PathDelim + 'LC_MESSAGES'; + DeleteFile(path + PathDelim + 'default.po'); + RemoveDir(path); + RemoveDir(FDirectory + PathDelim + code); + end; + DeleteFile(FDirectory + PathDelim + 'AppParams.prm'); + RemoveDir(FDirectory); +end; + +procedure TWalletLanguageTests.WriteCatalog(const ACode, ATranslation: String); +var + disposed: TDisposables; + text: TStringList; + path: String; +begin + path := FDirectory + PathDelim + ACode + PathDelim + 'LC_MESSAGES'; + ForceDirectories(path); + text := disposed.AddObject(TStringList.Create) as TStringList; + text.Add('msgid ""'); + text.Add('msgstr ""'); + text.Add('"Content-Type: text/plain; charset=UTF-8\n"'); + text.Add(''); + text.Add('#: LanguageTestForm.Caption'); + text.Add('msgid "Options"'); + text.Add('msgstr "' + ATranslation + '"'); + text.Add(''); + text.SaveToFile(path + PathDelim + 'default.po'); +end; + +procedure TWalletLanguageTests.ListsOnlyUsableCatalogsAndEnglish; +var + languages: TArray<TWalletLanguage>; +begin + WriteCatalog('nl', 'Instellingen'); + WriteCatalog('de', ''); + WriteCatalog('fr', ''); + languages := TWalletLanguages.Available(FDirectory); + AssertEquals(2, Length(languages)); + AssertEquals('en', languages[0].Code); + AssertEquals('nl', languages[1].Code); + AssertEquals('Nederlands', languages[1].DisplayName); +end; + +procedure TWalletLanguageTests.CatalogCanBeParsedByNativeTranslator; +var + po: TPOFile; + disposed: TDisposables; +begin + WriteCatalog('nl', 'Instellingen'); + po := disposed.AddObject(TPOFile.Create(FDirectory + PathDelim + + 'nl' + PathDelim + 'LC_MESSAGES' + PathDelim + 'default.po')) as TPOFile; + AssertEquals('Instellingen', po.Translate('', 'Options')); + AssertEquals(1, po.Statistics.Translated); +end; + +procedure TWalletLanguageTests.MissingCatalogFallsBackWithoutChangingPreference; +var + params: TAppParams; + disposed: TDisposables; +begin + params := disposed.AddObject(TAppParams.Create(nil)) as TAppParams; + params.FileName := FDirectory + PathDelim + 'AppParams.prm'; + TWalletLanguages.WritePreference('fr', params); + AssertEquals('en', TWalletLanguages.Apply(TWalletLanguages.ReadPreference(params), FDirectory)); + AssertEquals('fr', TWalletLanguages.ReadPreference(params)); +end; + +procedure TWalletLanguageTests.PreferenceRoundTripsThroughExistingAppParams; +var + params: TAppParams; + disposed: TDisposables; +begin + params := disposed.AddObject(TAppParams.Create(nil)) as TAppParams; + params.FileName := FDirectory + PathDelim + 'AppParams.prm'; + TWalletLanguages.WritePreference('nl', params); + disposed.RemoveObject(params); + params.Free; + params := disposed.AddObject(TAppParams.Create(nil)) as TAppParams; + params.FileName := FDirectory + PathDelim + 'AppParams.prm'; + AssertEquals('nl', TWalletLanguages.ReadPreference(params)); +end; + +procedure TWalletLanguageTests.NativeTranslatorHandlesExistingAndFutureForms; +var + before, after: TLanguageTestForm; + disposed: TDisposables; +begin + WriteCatalog('nl', 'Instellingen'); + before := disposed.AddObject(TLanguageTestForm.Create(nil)) as TLanguageTestForm; + AssertEquals('Options', before.Caption); + AssertEquals('nl', TWalletLanguages.Apply('nl', FDirectory)); + AssertEquals('Instellingen', before.Caption); + after := disposed.AddObject(TLanguageTestForm.Create(nil)) as TLanguageTestForm; + AssertEquals('Instellingen', after.Caption); +end; + +procedure TWalletLanguageTests.RejectsInvalidLanguagePreference; +var + params: TAppParams; + disposed: TDisposables; + rejected: Boolean; +begin + params := disposed.AddObject(TAppParams.Create(nil)) as TAppParams; + params.FileName := FDirectory + PathDelim + 'AppParams.prm'; + TWalletLanguages.WritePreference('en', params); + rejected := false; + try + TWalletLanguages.WritePreference('..\outside', params); + except + on E: EArgumentException do rejected := true; + end; + AssertTrue(rejected); + AssertEquals('en', TWalletLanguages.ReadPreference(params)); +end; + +initialization + RegisterTest(TWalletLanguageTests); +end. diff --git a/src/tests/experimental/ui/UWalletMempoolReadSafetyTests.pas b/src/tests/experimental/ui/UWalletMempoolReadSafetyTests.pas new file mode 100644 index 000000000..53720f26c --- /dev/null +++ b/src/tests/experimental/ui/UWalletMempoolReadSafetyTests.pas @@ -0,0 +1,42 @@ +unit UWalletMempoolReadSafetyTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Distributed under the MIT software license; see the accompanying LICENSE. } + +{$mode delphi} + +interface + +uses fpcunit, testregistry; + +type + TWalletMempoolReadSafetyTests = class(TTestCase) + published + procedure PublishedOperationsHaveCompleteReadHashes; + procedure CachedTreeReadsRespectActiveWriters; + procedure ParallelReadersSeeCompleteStableDigests; + end; + +implementation + +uses UOperationReadHashChecks; + +procedure TWalletMempoolReadSafetyTests.PublishedOperationsHaveCompleteReadHashes; +begin + CheckPublishedOperationReadHashes; +end; + +procedure TWalletMempoolReadSafetyTests.CachedTreeReadsRespectActiveWriters; +begin + CheckCachedTreeReadTakesExistingLock; +end; + +procedure TWalletMempoolReadSafetyTests.ParallelReadersSeeCompleteStableDigests; +begin + CheckParallelOperationReadHashes; +end; + +initialization + RegisterTest(TWalletMempoolReadSafetyTests); + +end. diff --git a/src/tests/experimental/ui/UWalletMiningTests.pas b/src/tests/experimental/ui/UWalletMiningTests.pas new file mode 100644 index 000000000..1ace3da8a --- /dev/null +++ b/src/tests/experimental/ui/UWalletMiningTests.pas @@ -0,0 +1,321 @@ +unit UWalletMiningTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + Only generated keys and a disposable wallet file are used. No global wallet, + settings file, node, network connection or mined transaction is required. } + +{$mode delphi} + +interface + +uses + Classes, SysUtils, fpcunit, testregistry, UWallet, USettings, UCrypto, + UPCDataTypes, UBaseTypes; + +type + TWalletMiningTests = class(TTestCase) + private + FKeys: TWalletKeys; + FPrivate, FOtherPrivate, FWatch: TECPrivateKey; + FDirectory, FFileName: String; + function WalletBytes: TBytes; + procedure AssertKey(const AExpected, AActual: TAccountKey); + procedure AssertUnchanged(const ABefore: TBytes; ACount: Integer); + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure MissingSelectedUsesExistingPrivateKey; + procedure RandomNeverSelectsWatchOnly; + procedure RandomReselectsAfterMinedBlocksWithoutCreatingKeys; + procedure LockedPrivateKeyRemainsUsable; + procedure WatchOnlyWalletDoesNotGenerate; + procedure EmptyWalletDoesNotGenerate; + procedure NewEachTimeOnlyRotatesAfterMinedBlock; + procedure SelectionAndDeletionRevalidateCachedKey; + procedure ReadOnlyWalletDoesNotRotate; + procedure OrdinaryRefreshAndReloadLeaveFileUntouched; + procedure ExplicitRotationUsesWalletEncryption; + end; + +implementation + +uses UAccounts, UConst, UMemory; + +procedure TWalletMiningTests.SetUp; +var LID: TGUID; +begin + TCrypto.InitCrypto; + CreateGUID(LID); + FDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'PascalCoinMiningTest-' + GUIDToString(LID); + ForceDirectories(FDirectory); + FFileName := FDirectory + PathDelim + 'generated-wallet.dat'; + FKeys := TWalletKeys.Create(nil); + FKeys.WalletFileName := FFileName; + FPrivate := TECPrivateKey.Create; + FPrivate.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FOtherPrivate := TECPrivateKey.Create; + FOtherPrivate.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FWatch := TECPrivateKey.Create; + FWatch.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); +end; + +procedure TWalletMiningTests.TearDown; +begin + FKeys.Free; + FWatch.Free; + FOtherPrivate.Free; + FPrivate.Free; + DeleteFile(FFileName); + RemoveDir(FDirectory); +end; + +function TWalletMiningTests.WalletBytes: TBytes; +var + LFile: TFileStream; + LDisposables: TDisposables; +begin + LFile := LDisposables.AddObject(TFileStream.Create(FFileName, + fmOpenRead or fmShareDenyNone)) as TFileStream; + SetLength(Result, LFile.Size); + if Length(Result) > 0 then LFile.ReadBuffer(Result[0], Length(Result)); +end; + +procedure TWalletMiningTests.AssertKey(const AExpected, AActual: TAccountKey); +begin + AssertTrue('Expected the existing reward public key', TAccountComp.EqualAccountKeys(AExpected, AActual)); +end; + +procedure TWalletMiningTests.AssertUnchanged(const ABefore: TBytes; ACount: Integer); +begin + AssertEquals('Normal selection must not add wallet keys', ACount, FKeys.Count); + AssertTrue('Normal selection must not write the wallet file', TBaseType.Equals(ABefore, WalletBytes)); +end; + +procedure TWalletMiningTests.MissingSelectedUsesExistingPrivateKey; +var + LKey: TAccountKey; + LBefore: TBytes; +begin + FKeys.AddPrivateKey('Generated existing private key', FPrivate); + LBefore := WalletBytes; + AssertTrue(FKeys.TryGetMiningKey(mpk_Selected, + TAccountComp.AccountKey2RawString(FOtherPrivate.PublicKey), LKey)); + AssertKey(FPrivate.PublicKey, LKey); + AssertTrue('Malformed saved bytes must fall back without parsing or generation', + FKeys.TryGetMiningKey(mpk_Selected, TBytes.Create($FF, 1, 2), LKey)); + AssertKey(FPrivate.PublicKey, LKey); + AssertUnchanged(LBefore, 1); +end; + +procedure TWalletMiningTests.RandomNeverSelectsWatchOnly; +var + LKey: TAccountKey; + LBefore: TBytes; + i: Integer; +begin + FKeys.AddPublicKey('Generated watch-only key', FWatch.PublicKey); + FKeys.AddPublicKey('Another watch-only key', FOtherPrivate.PublicKey); + FKeys.AddPrivateKey('Generated eligible key', FPrivate); + LBefore := WalletBytes; + for i := 1 to 100 do begin + AssertTrue(FKeys.TryGetMiningKey(mpk_Random, nil, LKey)); + AssertKey(FPrivate.PublicKey, LKey); + end; + AssertTrue('A selected watch-only key also falls back to the eligible private key', + FKeys.TryGetMiningKey(mpk_Selected, TAccountComp.AccountKey2RawString(FWatch.PublicKey), LKey)); + AssertKey(FPrivate.PublicKey, LKey); + AssertUnchanged(LBefore, 3); +end; + +procedure TWalletMiningTests.RandomReselectsAfterMinedBlocksWithoutCreatingKeys; +var + LKey, LCurrent: TAccountKey; + LBefore: TBytes; + // FPC's RTL seed is unsigned; preserve its high bit in checked test builds. + LSavedSeed: Cardinal; + LSeenFirst, LSeenSecond: Boolean; + i: Integer; +begin + FKeys.AddPrivateKey('First generated reward key', FPrivate); + FKeys.AddPrivateKey('Second generated reward key', FOtherPrivate); + LBefore := WalletBytes; + LSavedSeed := RandSeed; + try + RandSeed := 12345; + LSeenFirst := False; + LSeenSecond := False; + for i := 1 to 100 do begin + AssertTrue(FKeys.TryGetMiningKey(mpk_Random, nil, LCurrent, True)); + LSeenFirst := LSeenFirst or TAccountComp.EqualAccountKeys(FPrivate.PublicKey, LCurrent); + LSeenSecond := LSeenSecond or TAccountComp.EqualAccountKeys(FOtherPrivate.PublicKey, LCurrent); + AssertTrue(FKeys.TryGetMiningKey(mpk_Random, nil, LKey)); + AssertKey(LCurrent, LKey); + AssertTrue(FKeys.TryGetMiningKey(mpk_Random, nil, LKey)); + AssertKey(LCurrent, LKey); + end; + AssertTrue('Actual blocks must be able to select either existing private key', LSeenFirst and LSeenSecond); + AssertUnchanged(LBefore, 2); + finally + RandSeed := LSavedSeed; + end; +end; + +procedure TWalletMiningTests.LockedPrivateKeyRemainsUsable; +var + LKey: TAccountKey; + LBefore: TBytes; +begin + FKeys.AddPrivateKey('Generated encrypted key', FPrivate); + FKeys.WalletPassword := 'disposable QA password'; + AssertTrue(FKeys.LockWallet); + AssertFalse(FKeys.IsValidPassword); + AssertFalse(Assigned(FKeys[0].PrivateKey)); + LBefore := WalletBytes; + AssertTrue(FKeys.TryGetMiningKey(mpk_Selected, + TAccountComp.AccountKey2RawString(FPrivate.PublicKey), LKey)); + AssertKey(FPrivate.PublicKey, LKey); + AssertTrue('Locked NewEachTime retains a public reward key without generating a secret', + FKeys.TryGetMiningKey(mpk_NewEachTime, nil, LKey, True)); + AssertKey(FPrivate.PublicKey, LKey); + AssertUnchanged(LBefore, 1); +end; + +procedure TWalletMiningTests.WatchOnlyWalletDoesNotGenerate; +var + LMode: TMinerPrivateKeyType; + LKey: TAccountKey; + LBefore: TBytes; +begin + FKeys.AddPublicKey('Generated public-only key', FWatch.PublicKey); + LBefore := WalletBytes; + for LMode := Low(TMinerPrivateKeyType) to High(TMinerPrivateKeyType) do begin + AssertFalse(FKeys.TryGetMiningKey(LMode, + TAccountComp.AccountKey2RawString(FWatch.PublicKey), LKey)); + AssertKey(CT_TECDSA_Public_Nul, LKey); + end; + AssertUnchanged(LBefore, 1); +end; + +procedure TWalletMiningTests.EmptyWalletDoesNotGenerate; +var + LMode: TMinerPrivateKeyType; + LKey: TAccountKey; + LBefore: TBytes; +begin + LBefore := WalletBytes; + for LMode := Low(TMinerPrivateKeyType) to High(TMinerPrivateKeyType) do begin + AssertFalse(FKeys.TryGetMiningKey(LMode, nil, LKey)); + AssertKey(CT_TECDSA_Public_Nul, LKey); + end; + AssertUnchanged(LBefore, 0); +end; + +procedure TWalletMiningTests.NewEachTimeOnlyRotatesAfterMinedBlock; +var + LKey, LRotated: TAccountKey; + LBefore: TBytes; + i: Integer; +begin + FKeys.AddPrivateKey('Generated original reward key', FPrivate); + LBefore := WalletBytes; + for i := 1 to 20 do begin + AssertTrue(FKeys.TryGetMiningKey(mpk_NewEachTime, nil, LKey)); + AssertKey(FPrivate.PublicKey, LKey); + end; + AssertUnchanged(LBefore, 1); + AssertTrue(FKeys.TryGetMiningKey(mpk_NewEachTime, nil, LRotated, True)); + AssertEquals('One explicit rotation adds exactly one key', 2, FKeys.Count); + AssertFalse(TAccountComp.EqualAccountKeys(FPrivate.PublicKey, LRotated)); + LBefore := WalletBytes; + for i := 1 to 20 do begin + AssertTrue(FKeys.TryGetMiningKey(mpk_NewEachTime, nil, LKey)); + AssertKey(LRotated, LKey); + end; + AssertUnchanged(LBefore, 2); +end; + +procedure TWalletMiningTests.SelectionAndDeletionRevalidateCachedKey; +var LKey: TAccountKey; +begin + FKeys.AddPrivateKey('First generated key', FPrivate); + FKeys.AddPrivateKey('Second generated key', FOtherPrivate); + AssertTrue(FKeys.TryGetMiningKey(mpk_Selected, + TAccountComp.AccountKey2RawString(FPrivate.PublicKey), LKey)); + AssertKey(FPrivate.PublicKey, LKey); + AssertTrue(FKeys.TryGetMiningKey(mpk_Selected, + TAccountComp.AccountKey2RawString(FOtherPrivate.PublicKey), LKey)); + AssertKey(FOtherPrivate.PublicKey, LKey); + FKeys.Delete(FKeys.IndexOfAccountKey(FOtherPrivate.PublicKey)); + AssertTrue(FKeys.TryGetMiningKey(mpk_Selected, + TAccountComp.AccountKey2RawString(FOtherPrivate.PublicKey), LKey)); + AssertKey(FPrivate.PublicKey, LKey); + FKeys.Delete(0); + AssertFalse(FKeys.TryGetMiningKey(mpk_Random, nil, LKey)); + AssertKey(CT_TECDSA_Public_Nul, LKey); +end; + +procedure TWalletMiningTests.ReadOnlyWalletDoesNotRotate; +var + LKey: TAccountKey; + LBefore: TBytes; +begin + FKeys.AddPrivateKey('Generated read-only reward key', FPrivate); + FKeys.IsReadOnly := True; + LBefore := WalletBytes; + AssertTrue(FKeys.TryGetMiningKey(mpk_NewEachTime, nil, LKey, True)); + AssertKey(FPrivate.PublicKey, LKey); + AssertUnchanged(LBefore, 1); +end; + +procedure TWalletMiningTests.OrdinaryRefreshAndReloadLeaveFileUntouched; +var + LKey: TAccountKey; + LBefore: TBytes; + LMode: TMinerPrivateKeyType; + i: Integer; +begin + FKeys.AddPrivateKey('Generated existing reward key', FPrivate); + FKeys.AddPublicKey('Generated watch-only key', FWatch.PublicKey); + LBefore := WalletBytes; + for i := 1 to 10 do + for LMode := Low(TMinerPrivateKeyType) to High(TMinerPrivateKeyType) do + AssertTrue(FKeys.TryGetMiningKey(LMode, nil, LKey)); + AssertUnchanged(LBefore, 2); + FreeAndNil(FKeys); + FKeys := TWalletKeys.Create(nil); + FKeys.WalletFileName := FFileName; + AssertTrue(FKeys.TryGetMiningKey(mpk_NewEachTime, nil, LKey)); + AssertKey(FPrivate.PublicKey, LKey); + AssertUnchanged(LBefore, 2); +end; + +procedure TWalletMiningTests.ExplicitRotationUsesWalletEncryption; +var + LKey: TAccountKey; + LBefore: TBytes; + i: Integer; +begin + FKeys.AddPrivateKey('Generated encrypted reward key', FPrivate); + FKeys.WalletPassword := 'disposable mining password'; + AssertTrue(FKeys.TryGetMiningKey(mpk_NewEachTime, nil, LKey, True)); + AssertEquals(2, FKeys.Count); + LBefore := WalletBytes; + FreeAndNil(FKeys); + FKeys := TWalletKeys.Create(nil); + FKeys.WalletFileName := FFileName; + AssertFalse('A rotated secret must not be encrypted with an empty password', FKeys.IsValidPassword); + for i := 0 to FKeys.Count - 1 do AssertFalse(Assigned(FKeys[i].PrivateKey)); + AssertUnchanged(LBefore, 2); + FKeys.WalletPassword := 'disposable mining password'; + AssertTrue(FKeys.IsValidPassword); + AssertTrue(Assigned(FKeys[FKeys.IndexOfAccountKey(LKey)].PrivateKey)); +end; + +initialization + RegisterTest(TWalletMiningTests); +end. diff --git a/src/tests/experimental/ui/UWalletPagingTests.pas b/src/tests/experimental/ui/UWalletPagingTests.pas new file mode 100644 index 000000000..bff54a734 --- /dev/null +++ b/src/tests/experimental/ui/UWalletPagingTests.pas @@ -0,0 +1,620 @@ +unit UWalletPagingTests; + +{ Copyright © 2026 Herman Schoenfeld + Distributed under the MIT software license, see the accompanying LICENSE. + Synthetic key indexes count account reads without constructing a node. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, SyncObjs, fpcunit, testregistry, UCrypto, UAccounts, + UPCDataTypes, UDataSources, UCommon.Data, UCoreObjects, UWalletStartupTask; + +type + TWalletPagingTests = class(TTestCase) + private + FCompletionCount: Integer; + FCompletionThread: TThreadID; + FCompletedSummary: TBalanceSummary; + FCompletedSummaryReady: Boolean; + procedure OnBalanceCompleted(Sender: TObject); + procedure WaitForBalanceCompletion; + published + procedure InterfacePagingReadsOnlyTheRequestedRecords; + procedure SmallKeyDoesNotEnumerateOrReadTheLargeKey; + procedure SmallKeyCustomSortReadsOnlyItsOwnRecords; + procedure PendingOwnershipChangesCorrectMembershipAndCount; + procedure ConsecutivePagesGrowIndexPrefixesGeometrically; + procedure FullQueryKeepsItsCapturedPendingTip; + procedure GlobalTotalsAreIndependentOfTheSelectedKey; + procedure ChangedCoreStateDiscardsAggregateWithoutRetryChurn; + procedure DestroyCancelsAndJoinsTheAggregateWorker; + procedure AggregateCompletionPublishesWithoutPolling; + procedure AggregateCompletionCanRequestTheLatestGeneration; + procedure DestroyRemovesAggregateCompletionCallbacks; + procedure TerminatedCustomQueryJoinsAndDoesNotCachePartialRows; + procedure TerminatedSortJoinsAndDoesNotCachePartialQuery; + end; + +implementation + +uses UMemory, UCommon, UConst, Variants, Generics.Collections, Math; + +procedure TWalletPagingTests.OnBalanceCompleted(Sender: TObject); +begin + Inc(FCompletionCount); + FCompletionThread := TThread.CurrentThread.ThreadID; + FCompletedSummaryReady := TMyAccountsDataSource(Sender).TryGetBalanceSummary(FCompletedSummary); +end; + +procedure TWalletPagingTests.WaitForBalanceCompletion; +var LDeadline: QWord; +begin + LDeadline := GetTickCount64 + 5000; + repeat + // Deliver queued completion events; do not poll/start the aggregate here. + CheckSynchronize(0); + if FCompletedSummaryReady then Exit; + if GetTickCount64 >= LDeadline then Fail('The aggregate completion event did not publish'); + Sleep(1); + until False; +end; + +type + TCountingPagedAccounts = class(TMyAccountsDataSource) + private + function MakeAccount(ANumber: Cardinal): TAccount; + protected + procedure BeginReadSnapshot(AIncludePending: Boolean; out AContext: TAccountReadContext); override; + function BeginBalanceSnapshot(ATask: TWalletStartupTask; AGeneration: Int64; + AIncludePending: Boolean; out AContext: TAccountReadContext): Boolean; override; + procedure EndReadSnapshot(const AContext: TAccountReadContext); override; + function WalletKeys(const AContext: TAccountReadContext): TArray<TAccountKey>; override; + function KeyAccountCount(const AKey: TAccountKey; const AContext: TAccountReadContext): Integer; override; + function ReadKeyPrefix(const AKey: TAccountKey; ACount: Integer; + const AContext: TAccountReadContext): TArray<Cardinal>; override; + function OwnershipChanges(const AContext: TAccountReadContext): TArray<TAccountOwnershipChange>; override; + function ReadPageAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; override; + function ReadBalanceAccount(AAccountNumber: Cardinal; const AContext: TAccountReadContext): TAccount; override; + function GetItemField(constref AItem: TAccount; const ABindingName: String): Variant; override; + public + Keys: TArray<TAccountKey>; + Changes: TArray<TAccountOwnershipChange>; + LargeCount, SmallCount: Integer; + PageReads, BalanceReads, LargePrefixReads, SmallPrefixReads: Integer; + LargestPrefixRequested, PrefixNumbersRead, FullFetches: Integer; + DelayBalance, ChangeStateAfter: Integer; + DelayPage, DelayField, FieldReads: Integer; + Tip, UpdatedAt: Cardinal; + StateID: String; + BalanceEntered: TEvent; + PageEntered: TEvent; + FieldEntered: TEvent; + constructor Create; + destructor Destroy; override; + procedure FetchAll(const AContainer: TList<TAccount>); override; + function Page(AIndex, ASize: Integer; const AFilters: TFilterCriteria; var ATable: TDataTable): TPageFetchResult; + end; + + TCancelledPageThread = class(TThread) + private + FSource: TCountingPagedAccounts; + protected + procedure Execute; override; + public + Completed: TEvent; + Cancelled: Boolean; + ErrorMessage: String; + constructor Create(ASource: TCountingPagedAccounts); + destructor Destroy; override; + end; + +constructor TCountingPagedAccounts.Create; +var LKey: TECPrivateKey; i: Integer; LScope: TDisposables; +begin + inherited Create(nil); + TCrypto.InitCrypto; + SetLength(Keys, 3); + LKey := LScope.AddObject(TECPrivateKey.Create) as TECPrivateKey; + for i := 0 to High(Keys) do begin + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + Keys[i] := LKey.PublicKey; + end; + LargeCount := 1000; + SmallCount := 3; + Tip := 10; + UpdatedAt := 11; + StateID := 'stable'; + SnapshotCacheEnabled := True; + BalanceEntered := TEvent.Create(nil, True, False, ''); + PageEntered := TEvent.Create(nil, True, False, ''); + FieldEntered := TEvent.Create(nil, True, False, ''); +end; + +destructor TCountingPagedAccounts.Destroy; +begin + // The base owns and joins its task before fixture events are released. + inherited Destroy; + BalanceEntered.Free; + PageEntered.Free; + FieldEntered.Free; +end; + +procedure TCountingPagedAccounts.BeginReadSnapshot(AIncludePending: Boolean; + out AContext: TAccountReadContext); +begin + AContext := Default(TAccountReadContext); + AContext.BlockTip := Tip; + AContext.StateID := StateID; +end; + +function TCountingPagedAccounts.BeginBalanceSnapshot(ATask: TWalletStartupTask; + AGeneration: Int64; AIncludePending: Boolean; out AContext: TAccountReadContext): Boolean; +begin + Result := not ATask.Cancelled; + if Result then BeginReadSnapshot(AIncludePending, AContext); +end; + +procedure TCountingPagedAccounts.EndReadSnapshot(const AContext: TAccountReadContext); +begin +end; + +function TCountingPagedAccounts.WalletKeys(const AContext: TAccountReadContext): TArray<TAccountKey>; +begin + Result := Keys; +end; + +function TCountingPagedAccounts.KeyAccountCount(const AKey: TAccountKey; + const AContext: TAccountReadContext): Integer; +begin + if TAccountComp.EqualAccountKeys(AKey, Keys[0]) then Result := LargeCount + else if TAccountComp.EqualAccountKeys(AKey, Keys[1]) then Result := SmallCount + else Result := 0; +end; + +function TCountingPagedAccounts.ReadKeyPrefix(const AKey: TAccountKey; + ACount: Integer; const AContext: TAccountReadContext): TArray<Cardinal>; +var i, LCount: Integer; LSmall: Boolean; +begin + LCount := Min(ACount, KeyAccountCount(AKey, AContext)); + LSmall := TAccountComp.EqualAccountKeys(AKey, Keys[1]); + if LSmall then Inc(SmallPrefixReads) else Inc(LargePrefixReads); + if LCount > LargestPrefixRequested then LargestPrefixRequested := LCount; + Inc(PrefixNumbersRead, LCount); + SetLength(Result, LCount); + for i := 0 to LCount - 1 do + if LSmall then Result[i] := i * 2 + 1 else Result[i] := i * 2; +end; + +function TCountingPagedAccounts.OwnershipChanges(const AContext: TAccountReadContext): TArray<TAccountOwnershipChange>; +begin + Result := Changes; +end; + +function TCountingPagedAccounts.MakeAccount(ANumber: Cardinal): TAccount; +var i: Integer; +begin + Result := Default(TAccount); + Result.account := ANumber; + Result.balance := UInt64(ANumber) + 100; + Result.updated_on_block_active_mode := UpdatedAt; + Result.updated_on_block_passive_mode := UpdatedAt; + if (ANumber mod 2 = 1) and (ANumber < Cardinal(SmallCount * 2)) then + Result.accountInfo.accountKey := Keys[1] + else + Result.accountInfo.accountKey := Keys[0]; + for i := 0 to High(Changes) do + if Changes[i].Account = ANumber then Result.accountInfo.accountKey := Changes[i].NewKey; +end; + +function TCountingPagedAccounts.ReadPageAccount(AAccountNumber: Cardinal; + const AContext: TAccountReadContext): TAccount; +begin + InterlockedIncrement(PageReads); + PageEntered.SetEvent; + if DelayPage > 0 then Sleep(DelayPage); + Result := MakeAccount(AAccountNumber); +end; + +function TCountingPagedAccounts.ReadBalanceAccount(AAccountNumber: Cardinal; + const AContext: TAccountReadContext): TAccount; +begin + InterlockedIncrement(BalanceReads); + BalanceEntered.SetEvent; + if DelayBalance > 0 then Sleep(DelayBalance); + if (ChangeStateAfter > 0) and (BalanceReads = ChangeStateAfter) then + StateID := 'changed'; + Result := MakeAccount(AAccountNumber); +end; + +procedure TCountingPagedAccounts.FetchAll(const AContainer: TList<TAccount>); +begin + Inc(FullFetches); + inherited FetchAll(AContainer); +end; + +function TCountingPagedAccounts.GetItemField(constref AItem: TAccount; + const ABindingName: String): Variant; +begin + if ABindingName='Balance' then begin + Inc(FieldReads); + FieldEntered.SetEvent; + if DelayField>0 then Sleep(DelayField); + end; + Result := inherited GetItemField(AItem,ABindingName); +end; + +function TCountingPagedAccounts.Page(AIndex, ASize: Integer; + const AFilters: TFilterCriteria; var ATable: TDataTable): TPageFetchResult; +var LInterface: IDataSource; +begin + // Exercise actual interface dispatch to the new virtual specialization. + LInterface := Self; + Result := LInterface.FetchPage(TPageFetchParams.Create(AIndex, ASize, AFilters, foAnd), ATable); +end; + +function SortBy(const AName: String; ADirection: TSortDirection): TFilterCriteria; +begin + SetLength(Result, 1); + Result[0] := Default(TColumnFilter); + Result[0].ColumnName := AName; + Result[0].Filter := vgfSortable; + Result[0].Sort := ADirection; +end; + +constructor TCancelledPageThread.Create(ASource: TCountingPagedAccounts); +begin + inherited Create(True); + FreeOnTerminate := False; + FSource := ASource; + Completed := TEvent.Create(nil,True,False,''); + Start; +end; + +destructor TCancelledPageThread.Destroy; +begin + Terminate; + WaitFor; + Completed.Free; + inherited Destroy; +end; + +procedure TCancelledPageThread.Execute; +var LTable: TDataTable; +begin + try + try + FSource.Page(0,25,SortBy('Balance',sdAscending),LTable); + except + on E: EAbort do Cancelled := True; + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +procedure TWalletPagingTests.TerminatedCustomQueryJoinsAndDoesNotCachePartialRows; +var LSource: TCountingPagedAccounts; LWorker: TCancelledPageThread; + LTable: TDataTable; LResult: TPageFetchResult; LStarted: QWord; +begin + LSource := TCountingPagedAccounts.Create; + LWorker := nil; + try + LSource.LargeCount := 5; + LSource.SmallCount := 3; + LSource.DelayPage := 100; + LWorker := TCancelledPageThread.Create(LSource); + AssertEquals(Ord(wrSignaled),Ord(LSource.PageEntered.WaitFor(3000))); + LStarted := GetTickCount64; + LWorker.Terminate; + AssertEquals('The owned custom-query worker must join without releasing a test gate', + Ord(wrSignaled),Ord(LWorker.Completed.WaitFor(1500))); + LWorker.WaitFor; + AssertTrue(LWorker.Cancelled); + AssertEquals('',LWorker.ErrorMessage); + AssertTrue(GetTickCount64-LStarted<1000); + AssertTrue('Cancellation must stop before every account record is read',LSource.PageReads<8); + LSource.DelayPage := 0; + LResult := LSource.Page(0,25,SortBy('Balance',sdAscending),LTable); + AssertEquals(8,LResult.TotalDataCount); + AssertEquals('Cancelled custom snapshots must be rebuilt',2,LSource.FullFetches); + LResult := LSource.Page(0,25,SortBy('Balance',sdAscending),LTable); + AssertEquals(8,LResult.TotalDataCount); + AssertEquals('Completed snapshots remain reusable',2,LSource.FullFetches); + finally + if Assigned(LWorker) then begin + LWorker.Terminate; + LWorker.WaitFor; + LWorker.Free; + end; + LSource.Free; + end; +end; + +procedure TWalletPagingTests.TerminatedSortJoinsAndDoesNotCachePartialQuery; +var LSource: TCountingPagedAccounts; LWorker: TCancelledPageThread; + LTable: TDataTable; LResult: TPageFetchResult; LFields: Integer; +begin + LSource := TCountingPagedAccounts.Create; + LWorker := nil; + try + LSource.LargeCount := 5; + LSource.SmallCount := 3; + LSource.DelayField := 50; + LWorker := TCancelledPageThread.Create(LSource); + AssertEquals(Ord(wrSignaled),Ord(LSource.FieldEntered.WaitFor(3000))); + AssertEquals('Record loading must finish before this sort-phase cancellation',8,LSource.PageReads); + LWorker.Terminate; + AssertEquals('Sort cancellation must join without releasing a test gate', + Ord(wrSignaled),Ord(LWorker.Completed.WaitFor(1500))); + LWorker.WaitFor; + AssertTrue(LWorker.Cancelled); + AssertEquals('',LWorker.ErrorMessage); + LFields := LSource.FieldReads; + LSource.DelayField := 0; + LResult := LSource.Page(0,25,SortBy('Balance',sdAscending),LTable); + AssertEquals(8,LResult.TotalDataCount); + AssertTrue('The cancelled query must evaluate its sort again',LSource.FieldReads-LFields>8); + AssertEquals('The complete raw snapshot remains valid after cancelling only its query',1,LSource.FullFetches); + LResult := LSource.Page(0,25,SortBy('Balance',sdAscending),LTable); + AssertEquals(8,LResult.TotalDataCount); + AssertEquals(1,LSource.FullFetches); + finally + if Assigned(LWorker) then begin + LWorker.Terminate; + LWorker.WaitFor; + LWorker.Free; + end; + LSource.Free; + end; +end; + +procedure TWalletPagingTests.InterfacePagingReadsOnlyTheRequestedRecords; +var LSource: TCountingPagedAccounts; LTable: TDataTable; LResult: TPageFetchResult; LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 326387; + LResult := LSource.Page(0, 25, nil, LTable); + AssertEquals(326390, LResult.TotalDataCount); + AssertEquals(25, Length(LTable.Rows)); + AssertEquals(25, LSource.PageReads); + AssertEquals(0, LSource.BalanceReads); + AssertEquals(0, LSource.FullFetches); + AssertTrue('First page must not enumerate every account ID', LSource.PrefixNumbersRead <= 67); + LSource.Page(1, 25, SortBy('AccountNumber', sdAscending), LTable); + AssertEquals(50, LSource.PageReads); + AssertEquals(0, LSource.FullFetches); +end; + +procedure TWalletPagingTests.SmallKeyDoesNotEnumerateOrReadTheLargeKey; +var LSource: TCountingPagedAccounts; LTable: TDataTable; LResult: TPageFetchResult; LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 457633; + LSource.FilterKeys := TArray<TAccountKey>.Create(LSource.Keys[1]); + LResult := LSource.Page(0, 25, nil, LTable); + AssertEquals(3, LResult.TotalDataCount); + AssertEquals(3, LSource.PageReads); + AssertEquals(0, LSource.LargePrefixReads); + AssertEquals(0, LSource.BalanceReads); + AssertEquals(1, Integer(LTable.Rows[0].AccountNumber)); +end; + +procedure TWalletPagingTests.SmallKeyCustomSortReadsOnlyItsOwnRecords; +var LSource: TCountingPagedAccounts; LTable: TDataTable; LResult: TPageFetchResult; LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 457633; + LSource.FilterKeys := TArray<TAccountKey>.Create(LSource.Keys[1]); + LResult := LSource.Page(0, 25, SortBy('Balance', sdDescending), LTable); + AssertEquals(3, LResult.TotalDataCount); + AssertEquals(3, LSource.PageReads); + AssertEquals(0, LSource.LargePrefixReads); + AssertEquals(5, Integer(LTable.Rows[0].AccountNumber)); + LSource.Page(0, 1, SortBy('Balance', sdDescending), LTable); + AssertEquals('Warm custom-sort page must reuse its selected snapshot', 3, LSource.PageReads); +end; + +procedure TWalletPagingTests.PendingOwnershipChangesCorrectMembershipAndCount; +var LSource: TCountingPagedAccounts; LTable: TDataTable; LResult: TPageFetchResult; LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.FilterKeys := TArray<TAccountKey>.Create(LSource.Keys[1]); + SetLength(LSource.Changes, 2); + LSource.Changes[0].Account := 0; + LSource.Changes[0].NewKey := LSource.Keys[1]; + LSource.Changes[1].Account := 1; + LSource.Changes[1].NewKey := LSource.Keys[0]; + LResult := LSource.Page(0, 25, nil, LTable); + AssertEquals(3, LResult.TotalDataCount); + AssertEquals(3, LSource.PageReads); + AssertEquals(0, LSource.LargePrefixReads); + AssertEquals(0, Integer(LTable.Rows[0].AccountNumber)); + AssertEquals(3, Integer(LTable.Rows[1].AccountNumber)); + AssertEquals(5, Integer(LTable.Rows[2].AccountNumber)); +end; + +procedure TWalletPagingTests.ConsecutivePagesGrowIndexPrefixesGeometrically; +var LSource: TCountingPagedAccounts; LTable: TDataTable; i: Integer; LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 10000; + LSource.FilterKeys := TArray<TAccountKey>.Create(LSource.Keys[0]); + for i := 0 to 15 do LSource.Page(i, 25, nil, LTable); + AssertEquals(400, LSource.PageReads); + AssertTrue('Index prefixes must grow geometrically, not restart for every page', + LSource.LargePrefixReads <= 5); + AssertTrue(LSource.PrefixNumbersRead < 1100); +end; + +procedure TWalletPagingTests.FullQueryKeepsItsCapturedPendingTip; +var LSource: TCountingPagedAccounts; LTable: TDataTable; LFilters: TFilterCriteria; LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.FilterKeys := TArray<TAccountKey>.Create(LSource.Keys[1]); + LFilters := SortBy('Balance', sdDescending); + LSource.Page(0, 25, LFilters, LTable); + AssertTrue(Boolean(LTable.Rows[0].IsPending)); + LSource.Tip := 12; + LSource.Page(0, 25, nil, LTable); + AssertFalse(Boolean(LTable.Rows[0].IsPending)); + LSource.Page(0, 25, LFilters, LTable); + AssertTrue('Cached full view must keep its original pending-color tip', Boolean(LTable.Rows[0].IsPending)); +end; + +procedure TWalletPagingTests.GlobalTotalsAreIndependentOfTheSelectedKey; +var + LSource: TCountingPagedAccounts; + LTable: TDataTable; + LSummary: TBalanceSummary; + LDeadline: QWord; + LReads: Integer; + LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 50; + LSource.FilterKeys := TArray<TAccountKey>.Create(LSource.Keys[1]); + LSource.Page(0, 25, nil, LTable); + AssertEquals(0, LSource.BalanceReads); + LDeadline := GetTickCount64 + 5000; + while not LSource.TryGetBalanceSummary(LSummary) do begin + if GetTickCount64 > LDeadline then Fail('Aggregate did not finish'); + CheckSynchronize(0); + Sleep(5); + end; + AssertEquals(53, Integer(LSummary.TotalPASA)); + AssertEquals(Int64(7759), Int64(LSummary.TotalPASC)); + LReads := LSource.BalanceReads; + LSource.FilterKeys := TArray<TAccountKey>.Create(LSource.Keys[0]); + LSource.Page(0, 25, nil, LTable); + AssertTrue(LSource.TryGetBalanceSummary(LSummary)); + AssertEquals('Changing key must not restart global totals', LReads, LSource.BalanceReads); +end; + +procedure TWalletPagingTests.ChangedCoreStateDiscardsAggregateWithoutRetryChurn; +var + LSource: TCountingPagedAccounts; + LSummary: TBalanceSummary; + LDeadline: QWord; + LReads, i: Integer; + LScope: TDisposables; +begin + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 300; + LSource.ChangeStateAfter := 10; + LSource.TryGetBalanceSummary(LSummary); + AssertEquals(Ord(wrSignaled), Ord(LSource.BalanceEntered.WaitFor(5000))); + LDeadline := GetTickCount64 + 5000; + while LSource.BalanceReads < 128 do begin + if GetTickCount64 > LDeadline then Fail('Aggregate did not finish its coherent chunk'); + Sleep(1); + end; + AssertFalse('Mixed-state sum must not publish', LSource.TryGetBalanceSummary(LSummary)); + LReads := LSource.BalanceReads; + for i := 0 to 4 do begin + CheckSynchronize(0); + Sleep(10); + AssertFalse(LSource.TryGetBalanceSummary(LSummary)); + end; + AssertEquals('A discarded generation must wait for the next backend invalidation', LReads, LSource.BalanceReads); + LSource.ChangeStateAfter := 0; + LSource.InvalidateWalletData; + LDeadline := GetTickCount64 + 5000; + while not LSource.TryGetBalanceSummary(LSummary) do begin + if GetTickCount64 > LDeadline then Fail('New aggregate generation did not finish'); + CheckSynchronize(0); + Sleep(5); + end; + AssertEquals(303, Integer(LSummary.TotalPASA)); +end; + +procedure TWalletPagingTests.AggregateCompletionPublishesWithoutPolling; +var + LSource: TCountingPagedAccounts; + LSummary: TBalanceSummary; + LScope: TDisposables; +begin + FCompletionCount := 0; + FCompletedSummaryReady := False; + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 50; + LSource.OnBalanceSummaryCompleted := OnBalanceCompleted; + AssertFalse('The initial request starts asynchronous work', LSource.TryGetBalanceSummary(LSummary)); + WaitForBalanceCompletion; + AssertEquals('One completed task produces one event', 1, FCompletionCount); + AssertEquals('Completion is delivered on the control owner thread', + Int64(MainThreadID), Int64(FCompletionThread)); + AssertEquals(53, Integer(FCompletedSummary.TotalPASA)); + AssertEquals(Int64(7759), Int64(FCompletedSummary.TotalPASC)); + AssertEquals(53, LSource.BalanceReads); +end; + +procedure TWalletPagingTests.AggregateCompletionCanRequestTheLatestGeneration; +var + LSource: TCountingPagedAccounts; + LSummary: TBalanceSummary; + LScope: TDisposables; +begin + FCompletionCount := 0; + FCompletedSummaryReady := False; + LSource := LScope.AddObject(TCountingPagedAccounts.Create) as TCountingPagedAccounts; + LSource.LargeCount := 300; + LSource.DelayBalance := 1; + LSource.OnBalanceSummaryCompleted := OnBalanceCompleted; + AssertFalse(LSource.TryGetBalanceSummary(LSummary)); + AssertEquals(Ord(wrSignaled), Ord(LSource.BalanceEntered.WaitFor(5000))); + LSource.InvalidateWalletData; + WaitForBalanceCompletion; + AssertEquals('Discarded work reports completion before the latest task starts', + 2, FCompletionCount); + AssertEquals(303, Integer(FCompletedSummary.TotalPASA)); +end; + +procedure TWalletPagingTests.DestroyRemovesAggregateCompletionCallbacks; +var + LSource: TCountingPagedAccounts; + LSummary: TBalanceSummary; +begin + FCompletionCount := 0; + FCompletedSummaryReady := False; + LSource := TCountingPagedAccounts.Create; + try + LSource.LargeCount := 1; + LSource.OnBalanceSummaryCompleted := OnBalanceCompleted; + LSource.TryGetBalanceSummary(LSummary); + AssertEquals(Ord(wrSignaled), Ord(LSource.BalanceEntered.WaitFor(5000))); + // Leave the UI queue undelivered while the small aggregate completes. + Sleep(30); + finally + LSource.Free; + end; + CheckSynchronize(0); + AssertEquals('A destroyed source cannot notify a released receiver', 0, FCompletionCount); +end; + +procedure TWalletPagingTests.DestroyCancelsAndJoinsTheAggregateWorker; +var + LSource: TCountingPagedAccounts; + LSummary: TBalanceSummary; + LStarted: QWord; +begin + LSource := TCountingPagedAccounts.Create; + try + LSource.LargeCount := 50000; + LSource.DelayBalance := 1; + LSource.TryGetBalanceSummary(LSummary); + AssertEquals(Ord(wrSignaled), Ord(LSource.BalanceEntered.WaitFor(5000))); + finally + LStarted := GetTickCount64; + LSource.Free; + end; + AssertTrue('Aggregate cancellation must join after a bounded chunk', + GetTickCount64 - LStarted < 1500); +end; + +initialization + RegisterTest(TWalletPagingTests); + +end. diff --git a/src/tests/experimental/ui/UWalletProgressDisplayTests.pas b/src/tests/experimental/ui/UWalletProgressDisplayTests.pas new file mode 100644 index 000000000..baad08bbb --- /dev/null +++ b/src/tests/experimental/ui/UWalletProgressDisplayTests.pas @@ -0,0 +1,351 @@ +unit UWalletProgressDisplayTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. } + +{$IFDEF FPC}{$mode delphi}{$H+}{$ENDIF} + +interface + +uses Classes, SysUtils, fpcunit, testregistry, UAccounts, UNode, UProgressDisplay; + +type + TWalletProgressDisplayTests = class(TTestCase) + private + FSavedFormatSettings: TFormatSettings; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure MeasurementsAreFormattedOnce; + procedure DisplayBlockMetadataPreservesRawPosition; + procedure UnknownTotalsDoNotClaimCompletion; + procedure LargeCountersDoNotOverflowPercentage; + procedure DownloadMetadataIsFormattedAtTheBoundary; + procedure CreationClearsMetadataAndEveryStageHasPresentation; + procedure OriginalCoreTemplatesAreExact; + procedure OriginalGuiDecoratorsAreExact; + procedure OriginalNetworkDecoratorsAreExact; + procedure OriginalDaemonLogPrefixIsExact; + procedure UnknownTotalsKeepOriginalSeparator; + procedure OriginalPercentageIsNotClipped; + procedure OriginalPercentageRoundingAndLocaleArePreserved; + procedure ZeroBlockMetadataIsNotDiscarded; + procedure EmptyStorageFailureHasNoPresentation; + procedure StorageFailurePreservesOriginalDetails; + procedure EveryStorageFailureStageHasPresentation; + procedure StorageFailureFormattingDoesNotMutateData; + procedure StorageFailureWithoutOptionalDetailsRemainsClear; + end; + +implementation + +procedure TWalletProgressDisplayTests.SetUp; +begin + inherited; + FSavedFormatSettings := DefaultFormatSettings; + DefaultFormatSettings.DecimalSeparator := '.'; +end; + +procedure TWalletProgressDisplayTests.TearDown; +begin + DefaultFormatSettings := FSavedFormatSettings; + inherited; +end; + +procedure TWalletProgressDisplayTests.MeasurementsAreFormattedOnce; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psPreparingOperations, 2, 4); + AssertEquals('Preparing operations 2/4', FormatProgressMessage(L)); + AssertEquals('Preparing operations 2/4 50.00%', FormatProgressInfo(L)); + AssertEquals(Int64(2), L.CurrentPosition); + AssertEquals(Int64(4), L.TotalCount); +end; + +procedure TWalletProgressDisplayTests.DisplayBlockMetadataPreservesRawPosition; +var L: TProgressInfo; LText: String; +begin + // The safebox decoder historically reports an index with an extra sentinel + // in its numeric total. Display metadata must not silently alter that data. + L := TProgressInfo.Create(psReadingSafebox, 5, 11); + L.BlockNumber := 6; + L.BlockCount := 10; + LText := FormatProgressInfo(L); + AssertEquals('Reading Safebox block 6/10 45.45%', LText); + AssertEquals('Raw counters were duplicated after display metadata', 0, Pos('5/11', LText)); + AssertEquals(Int64(5), L.CurrentPosition); + AssertEquals(Int64(11), L.TotalCount); +end; + +procedure TWalletProgressDisplayTests.UnknownTotalsDoNotClaimCompletion; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psFinalizingRestore); + AssertEquals('Finalizing local restore... ', FormatProgressInfo(L)); + L.TotalCount := -1; + AssertEquals('Finalizing local restore... ', FormatProgressInfo(L)); + L := TProgressInfo.Create(psNone, 1, 1); + AssertEquals('', FormatProgressInfo(L)); + AssertEquals('', FormatProgressMessage(L)); + AssertEquals('', FormatProgressLogInfo(L)); + L := TProgressInfo.Create(psApplyingOperations, 10, 10); + AssertEquals('Applying operations 10/10 100.00%', FormatProgressInfo(L)); + AssertEquals('A phase percentage must not invent an overall completion stage', + Ord(psApplyingOperations), Ord(L.Stage)); +end; + +procedure TWalletProgressDisplayTests.LargeCountersDoNotOverflowPercentage; +var L: TProgressInfo; LText: String; +begin + L := TProgressInfo.Create(psReadingOperations, High(Int64) div 2, High(Int64)); + LText := FormatProgressInfo(L); + AssertEquals('Reading operations 4611686018427387903/9223372036854775807 50.00%', LText); + AssertEquals('Formatting modified the raw position', High(Int64) div 2, L.CurrentPosition); +end; + +procedure TWalletProgressDisplayTests.DownloadMetadataIsFormattedAtTheBoundary; +var L: TProgressInfo; LFileName: String; +begin + L := TProgressInfo.Create(psSavingSafebox); + L.ChunksCount := 12; + LFileName := PathDelim + 'owned' + PathDelim + 'generated' + PathDelim + 'safebox_100.safebox'; + L.FileName := LFileName; + AssertEquals('Saving Safebox 12 chunks to safebox_100.safebox', FormatProgressMessage(L)); + AssertEquals('Saving Safebox 12 chunks to safebox_100.safebox ', FormatProgressInfo(L)); + AssertEquals(LFileName, L.FileName); + L := TProgressInfo.Create(psReceivedSafebox); + L.BlockCount := 100; + L.PeerAddress := '127.0.0.1:4004'; + AssertEquals('Received new safebox with 100 blocks from 127.0.0.1:4004', FormatProgressInfo(L)); + L := TProgressInfo.Create(psReadingOperations, 1, 2); + L.Context := pcCheckingNewSafebox; + AssertEquals('Checking new safebox: Reading operations 1/2 50.00%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psReadingBlockBatch); + L.StartBlock := 7; + L.EndBlock := 9; + L.OperationsCount := 12; + AssertEquals('Reading blocks from 7 to 9 with 12 operations ', FormatProgressInfo(L)); +end; + +procedure TWalletProgressDisplayTests.CreationClearsMetadataAndEveryStageHasPresentation; +var L: TProgressInfo; S: TProgressStage; +begin + L := TProgressInfo.Create(psSavingSafebox, 2, 3); + L.FileName := 'old filename'; + L.PeerAddress := 'old peer'; + L.Context := pcCheckingNewSafebox; + L.ChunksCount := 9; + L := TProgressInfo.Create(psFinalizingRestore); + AssertEquals('', L.FileName); + AssertEquals('', L.PeerAddress); + AssertEquals(Ord(pcNone), Ord(L.Context)); + AssertEquals(Int64(0), L.CurrentPosition); + AssertEquals(Int64(0), L.TotalCount); + AssertEquals(Int64(0), L.BlockNumber); + AssertEquals(Int64(0), L.BlockCount); + AssertEquals(Int64(0), L.StartBlock); + AssertEquals(Int64(0), L.EndBlock); + AssertEquals(Int64(0), L.OperationsCount); + AssertEquals(Int64(0), L.ChunksCount); + for S := Succ(Low(TProgressStage)) to High(TProgressStage) do + AssertTrue('No presentation for typed progress stage ' + IntToStr(Ord(S)), + FormatProgressInfo(TProgressInfo.Create(S)) <> ''); +end; + +procedure TWalletProgressDisplayTests.OriginalCoreTemplatesAreExact; + procedure Expect(AStage: TProgressStage; const AExpected: String); + var L: TProgressInfo; + begin + L := TProgressInfo.Create(AStage, 2, 7); + L.BlockNumber := 6; L.BlockCount := 10; + L.StartBlock := 7; L.EndBlock := 9; L.OperationsCount := 12; + L.ChunksCount := 8; + L.FileName := 'owned' + PathDelim + 'safebox_100.safebox'; + L.PeerAddress := '127.0.0.1:4004'; + AssertEquals(AExpected, FormatProgressMessage(L)); + AssertEquals(Int64(2), L.CurrentPosition); + AssertEquals(Int64(7), L.TotalCount); + end; +begin + // Literal oracles come from the removed producers, not this formatter. + Expect(psNone, ''); + Expect(psReadingSafebox, 'Reading Safebox block 6/10'); + Expect(psValidatingOperationBlocks, 'Validating OperationBlock info 2/7'); + Expect(psCheckingSafeboxIntegrity, 'Checking Safebox integrity'); + Expect(psReadingBlockBatch, 'Reading blocks from 7 to 9 with 12 operations'); + Expect(psReadingBlockchain, 'Reading blockchain block 6/10'); + Expect(psLoadingPendingOperations, 'Loading saved pending operations'); + Expect(psReadingOperations, 'Reading operations 2/7'); + Expect(psPreparingOperations, 'Preparing operations 2/7'); + Expect(psValidatingSignatures, 'Validating signatures 2/7'); + Expect(psApplyingOperations, 'Applying operations 2/7'); + Expect(psFinalizingOperations, 'Finalizing operations 2/7'); + Expect(psFinalizingRestore, 'Finalizing local restore...'); + Expect(psDownloadingSafeboxChunks, 'Downloading Safebox chunks 2/8'); + Expect(psSavingSafebox, 'Saving Safebox 8 chunks to safebox_100.safebox'); + Expect(psReceivedSafebox, 'Received new safebox with 10 blocks from 127.0.0.1:4004'); + Expect(psDownloadingBlockchain, 'Downloading block 6 from 127.0.0.1:4004'); +end; + +procedure TWalletProgressDisplayTests.OriginalGuiDecoratorsAreExact; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psValidatingOperationBlocks, 2, 7); + AssertEquals('Validating OperationBlock info 2/7 28.57%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psReadingOperations, 1, 3); + AssertEquals('Reading operations 1/3 33.33%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psPreparingOperations, 2, 3); + AssertEquals('Preparing operations 2/3 66.67%', FormatProgressInfo(L)); + L.Stage := psValidatingSignatures; + AssertEquals('Validating signatures 2/3 66.67%', FormatProgressInfo(L)); + L.Stage := psApplyingOperations; + AssertEquals('Applying operations 2/3 66.67%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psFinalizingOperations, 0, 3); + AssertEquals('Finalizing operations 0/3 0.00%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psCheckingSafeboxIntegrity, 9, 9); + AssertEquals('Checking Safebox integrity 100.00%', FormatProgressInfo(L)); +end; + +procedure TWalletProgressDisplayTests.OriginalNetworkDecoratorsAreExact; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psDownloadingSafeboxChunks, 2, 8); + L.ChunksCount := 10; + AssertEquals('Downloading Safebox chunks 2/10 25.00%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psReadingSafebox, 5, 11); + L.BlockNumber := 6; L.BlockCount := 10; + L.Context := pcCheckingNewSafebox; + AssertEquals('Checking new safebox: Reading Safebox block 6/10 45.45%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psReceivedSafebox, 1, 2); + L.BlockCount := 100; L.PeerAddress := '127.0.0.1:4004'; + AssertEquals('Received new safebox with 100 blocks from 127.0.0.1:4004', FormatProgressInfo(L)); + L := TProgressInfo.Create(psDownloadingBlockchain); + L.BlockNumber := 99; L.PeerAddress := '127.0.0.1:4004'; + AssertEquals('Downloading block 99 from 127.0.0.1:4004', FormatProgressInfo(L)); +end; + +procedure TWalletProgressDisplayTests.OriginalDaemonLogPrefixIsExact; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psReadingSafebox, 5, 11); + L.BlockNumber := 6; L.BlockCount := 10; + AssertEquals('Progress (5/11): Reading Safebox block 6/10', FormatProgressLogInfo(L)); + L := TProgressInfo.Create(psPreparingOperations, 2, 4); + AssertEquals('Progress (2/4): Preparing operations 2/4', FormatProgressLogInfo(L)); + L := TProgressInfo.Create(psLoadingPendingOperations); + AssertEquals('Progress (0/0): Loading saved pending operations', FormatProgressLogInfo(L)); + L := TProgressInfo.Create(psNone, 1, 1); + AssertEquals('', FormatProgressLogInfo(L)); +end; + +procedure TWalletProgressDisplayTests.UnknownTotalsKeepOriginalSeparator; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psLoadingPendingOperations); + AssertEquals('Loading saved pending operations ', FormatProgressInfo(L)); + L := TProgressInfo.Create(psCheckingSafeboxIntegrity); + L.Context := pcCheckingNewSafebox; + AssertEquals('Checking new safebox: Checking Safebox integrity ', FormatProgressInfo(L)); + L := TProgressInfo.Create(psFinalizingOperations, 0, 0); + AssertEquals('Finalizing operations 0/0 ', FormatProgressInfo(L)); + L.TotalCount := -1; + AssertEquals('Finalizing operations 0/-1 ', FormatProgressInfo(L)); +end; + +procedure TWalletProgressDisplayTests.OriginalPercentageIsNotClipped; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psPreparingOperations, 15, 10); + AssertEquals('Preparing operations 15/10 150.00%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psReadingOperations, -1, 10); + AssertEquals('Reading operations -1/10 -10.00%', FormatProgressInfo(L)); + AssertEquals(Int64(-1), L.CurrentPosition); + AssertEquals(Int64(10), L.TotalCount); +end; + +procedure TWalletProgressDisplayTests.OriginalPercentageRoundingAndLocaleArePreserved; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psValidatingOperationBlocks, 2, 3); + AssertEquals('Validating OperationBlock info 2/3 66.67%', FormatProgressInfo(L)); + DefaultFormatSettings.DecimalSeparator := ','; + AssertEquals('Validating OperationBlock info 2/3 66,67%', FormatProgressInfo(L)); + L := TProgressInfo.Create(psDownloadingSafeboxChunks, 1, 3); + L.ChunksCount := 3; + AssertEquals('Downloading Safebox chunks 1/3 33,33%', FormatProgressInfo(L)); +end; + +procedure TWalletProgressDisplayTests.ZeroBlockMetadataIsNotDiscarded; +var L: TProgressInfo; +begin + L := TProgressInfo.Create(psReadingBlockchain); + AssertEquals('Reading blockchain block 0/0', FormatProgressMessage(L)); + AssertEquals('Reading blockchain block 0/0 ', FormatProgressInfo(L)); + L := TProgressInfo.Create(psReadingSafebox); + AssertEquals('Reading Safebox block 0/0 ', FormatProgressInfo(L)); +end; + +procedure TWalletProgressDisplayTests.EmptyStorageFailureHasNoPresentation; +var LFailure: TNodeStorageFailure; +begin + LFailure := Default(TNodeStorageFailure); + AssertEquals('',FormatStorageFailure(LFailure)); + LFailure.ErrorMessage := 'Unused stale detail'; + LFailure.FileName := 'old-checkpoint'; + AssertEquals('',FormatStorageFailure(LFailure)); +end; + +procedure TWalletProgressDisplayTests.StorageFailurePreservesOriginalDetails; +var LFailure: TNodeStorageFailure; +begin + LFailure := Default(TNodeStorageFailure); + LFailure.HasFailure := True; + LFailure.Stage := sfsCheckpoint; + LFailure.FileName := 'owned'+PathDelim+'checkpoint6.am_safebox'; + LFailure.ErrorClass := 'EWriteError'; + LFailure.ErrorMessage := 'Stream write error'; + AssertEquals('Storage error (restart required) during checkpoint save: EWriteError: Stream write error ['+LFailure.FileName+']',FormatStorageFailure(LFailure)); +end; + +procedure TWalletProgressDisplayTests.EveryStorageFailureStageHasPresentation; +const LExpected: array[TNodeStorageFailureStage] of String = ('local storage','safebox flush','checkpoint save','block storage','pending operations save','local blockchain restore'); +var LFailure: TNodeStorageFailure; LStage: TNodeStorageFailureStage; +begin + LFailure := Default(TNodeStorageFailure); + LFailure.HasFailure := True; + for LStage := Low(TNodeStorageFailureStage) to High(TNodeStorageFailureStage) do begin + LFailure.Stage := LStage; + AssertEquals('Storage error (restart required) during '+LExpected[LStage],FormatStorageFailure(LFailure)); + end; +end; + +procedure TWalletProgressDisplayTests.StorageFailureFormattingDoesNotMutateData; +var LFailure: TNodeStorageFailure; +begin + LFailure := Default(TNodeStorageFailure); + LFailure.HasFailure := True; + LFailure.Stage := sfsBlockStream; + LFailure.FileName := 'owned'+PathDelim+'BlockChainStream.blocks'; + LFailure.ErrorClass := 'EOSError'; + LFailure.ErrorMessage := 'Original operating system detail'; + FormatStorageFailure(LFailure); + AssertTrue(LFailure.HasFailure); + AssertEquals(Ord(sfsBlockStream),Ord(LFailure.Stage)); + AssertEquals('owned'+PathDelim+'BlockChainStream.blocks',LFailure.FileName); + AssertEquals('EOSError',LFailure.ErrorClass); + AssertEquals('Original operating system detail',LFailure.ErrorMessage); +end; + +procedure TWalletProgressDisplayTests.StorageFailureWithoutOptionalDetailsRemainsClear; +var LFailure: TNodeStorageFailure; +begin + LFailure := Default(TNodeStorageFailure); + LFailure.HasFailure := True; + LFailure.Stage := sfsUnknown; + AssertEquals('Storage error (restart required) during local storage',FormatStorageFailure(LFailure)); +end; + +initialization + RegisterTest(TWalletProgressDisplayTests); + +end. diff --git a/src/tests/experimental/ui/UWalletRenderTests.pas b/src/tests/experimental/ui/UWalletRenderTests.pas new file mode 100644 index 000000000..863cf607d --- /dev/null +++ b/src/tests/experimental/ui/UWalletRenderTests.pas @@ -0,0 +1,273 @@ +unit UWalletRenderTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. + + Synthetic account snapshots and bitmap canvases exercise grid painting + without opening a wallet, constructing a node or contacting peers. } + +{$mode delphi} + +interface + +uses + Classes, SysUtils, Graphics, Variants, fpcunit, testregistry, + Generics.Collections, UCrypto, UPCDataTypes, UDataSources, UCommon.Data; + +type + TRenderAccountSource = class(TAccountsDataSourceBase) + public + Items: TArray<TAccount>; + FixtureTip: Cardinal; + FetchCount: Integer; + procedure SetSnapshotTip(ATip: Cardinal); + procedure FetchAll(const AContainer: TList<TAccount>); override; + function Page(AIndex: Integer; const AFilters: TFilterCriteria = nil): TDataTable; + end; + + TWalletRenderTests = class(TTestCase) + private + FSource: TRenderAccountSource; + FKey: TECPrivateKey; + function Account(AActiveBlock, APassiveBlock: Cardinal): TAccount; + function BalanceColor(const ARow: Variant; AValue: Int64): TColor; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure AccountPendingUsesCapturedBoundary; + procedure FetchedAccountsContainBooleanPendingFlag; + procedure CachedPagesRetainTipUntilInvalidated; + procedure PendingFlagControlsBalanceColor; + procedure ExplicitFlagOverridesOperationTimestamp; + procedure OperationsFallbackUsesRowTime; + procedure AccountPaintingIgnoresLiveBlockHeight; + procedure UnsignedBalancesUsePascFormatting; + end; + +implementation + +uses + Types, UAccounts, UConst, UCellRenderers; + +procedure TRenderAccountSource.SetSnapshotTip(ATip: Cardinal); +begin + FSnapshotBlockTip := ATip; +end; + +procedure TRenderAccountSource.FetchAll(const AContainer: TList<TAccount>); +begin + Inc(FetchCount); + FSnapshotBlockTip := FixtureTip; + AContainer.AddRange(Items); +end; + +function TRenderAccountSource.Page(AIndex: Integer; const AFilters: TFilterCriteria): TDataTable; +begin + FetchPage(TPageFetchParams.Create(AIndex, 1, AFilters, foAnd), Result); +end; + +procedure TWalletRenderTests.SetUp; +begin + TCrypto.InitCrypto; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FSource := TRenderAccountSource.Create(nil); +end; + +procedure TWalletRenderTests.TearDown; +begin + FSource.Free; + FKey.Free; +end; + +function TWalletRenderTests.Account(AActiveBlock, APassiveBlock: Cardinal): TAccount; +begin + Result := CT_Account_NUL; + Result.accountInfo.state := as_Normal; + Result.accountInfo.accountKey := FKey.PublicKey; + Result.balance := 12345; + Result.updated_on_block_active_mode := AActiveBlock; + Result.updated_on_block_passive_mode := APassiveBlock; +end; + +function TWalletRenderTests.BalanceColor(const ARow: Variant; AValue: Int64): TColor; +var + LBitmap: TBitmap; + LHandled: Boolean; +begin + LBitmap := TBitmap.Create; + try + LBitmap.SetSize(200, 40); + LBitmap.Canvas.Font.Color := clBlack; + LHandled := False; + TCellRenderers.PASC_CheckPendingBalance(nil, 0, 0, LBitmap.Canvas, + Rect(0, 0, 200, 40), [], AValue, ARow, LHandled); + AssertTrue('Balance renderer must draw the numeric cell', LHandled); + Result := LBitmap.Canvas.Font.Color; + finally + LBitmap.Free; + end; +end; + +procedure TWalletRenderTests.AccountPendingUsesCapturedBoundary; +var LAccount: TAccount; +begin + FSource.SetSnapshotTip(10); + LAccount := Account(9, 11); + AssertTrue('Passive pending updates count', Boolean(FSource.GetItemField(LAccount, 'IsPending'))); + LAccount := Account(11, 9); + AssertTrue('Active pending updates count', Boolean(FSource.GetItemField(LAccount, 'IsPending'))); + LAccount := Account(10, 9); + AssertFalse('An update at the captured tip is confirmed', Boolean(FSource.GetItemField(LAccount, 'IsPending'))); + FSource.SetSnapshotTip(0); + LAccount := Account(0, 0); + AssertFalse('An empty chain boundary must not wrap', Boolean(FSource.GetItemField(LAccount, 'IsPending'))); + LAccount := Account(1, 0); + AssertTrue(Boolean(FSource.GetItemField(LAccount, 'IsPending'))); +end; + +procedure TWalletRenderTests.FetchedAccountsContainBooleanPendingFlag; +var LTable: TDataTable; +begin + FSource.FixtureTip := 10; + FSource.Items := TArray<TAccount>.Create(Account(11, 9)); + LTable := FSource.Page(0); + AssertEquals(1, Length(LTable.Rows)); + AssertEquals('Pending status is a row boolean', varBoolean, VarType(LTable.Rows[0].IsPending)); + AssertTrue(Boolean(LTable.Rows[0].IsPending)); + AssertEquals(11, Integer(LTable.Rows[0].LastUpdatedBlock)); +end; + +procedure TWalletRenderTests.CachedPagesRetainTipUntilInvalidated; +var + LFirst, LWarm, LRefreshed: TDataTable; + LFilters: TFilterCriteria; +begin + FSource.SnapshotCacheEnabled := True; + FSource.FixtureTip := 10; + FSource.Items := TArray<TAccount>.Create(Account(11, 0), Account(11, 0)); + FSource.Items[1].account := 1; + LFirst := FSource.Page(0); + FSource.FixtureTip := 11; + FSource.Items[0].updated_on_block_active_mode := 12; + LWarm := FSource.Page(1); + AssertEquals('A warm page must retain its raw snapshot', 1, FSource.FetchCount); + AssertTrue('A later live tip cannot recolor a cached account', Boolean(LWarm.Rows[0].IsPending)); + SetLength(LFilters, 1); + LFilters[0].ColumnName := 'AccountNumber'; + LFilters[0].Sort := sdDescending; + LFilters[0].Filter := vgfSortable; + LWarm := FSource.Page(0, LFilters); + AssertEquals('Changing sort reuses the captured tip and accounts', 1, FSource.FetchCount); + AssertEquals(1, Integer(LWarm.Rows[0].AccountNumber)); + AssertTrue(Boolean(LWarm.Rows[0].IsPending)); + FSource.InvalidateSnapshot; + LRefreshed := FSource.Page(1); + AssertEquals(2, FSource.FetchCount); + AssertFalse('New raw values use the new tip', Boolean(LRefreshed.Rows[0].IsPending)); + LRefreshed := FSource.Page(0); + AssertEquals(12, Integer(LRefreshed.Rows[0].LastUpdatedBlock)); + AssertTrue('New pending values and tip belong to one snapshot', Boolean(LRefreshed.Rows[0].IsPending)); + AssertTrue('Already returned rows retain their original classification', Boolean(LFirst.Rows[0].IsPending)); +end; + +procedure TWalletRenderTests.PendingFlagControlsBalanceColor; +var LRow: Variant; +begin + LRow := TDataRow.New(-310001, TDataColumns.Create(TDataColumn.From('IsPending'))); + LRow.IsPending := True; + AssertEquals(clBlue, BalanceColor(LRow, 12345)); + LRow.IsPending := False; + AssertEquals(clGreen, BalanceColor(LRow, 12345)); + AssertEquals(clRed, BalanceColor(LRow, -12345)); + AssertEquals(clDefault, BalanceColor(LRow, 0)); +end; + +procedure TWalletRenderTests.ExplicitFlagOverridesOperationTimestamp; +var LRow: Variant; +begin + LRow := TDataRow.New(-310002, TDataColumns.Create( + TDataColumn.From('IsPending'), TDataColumn.From('UnixTime'))); + LRow.IsPending := False; + LRow.UnixTime := 0; + AssertEquals(clGreen, BalanceColor(LRow, 12345)); + LRow.IsPending := True; + LRow.UnixTime := 12345; + AssertEquals(clBlue, BalanceColor(LRow, 12345)); +end; + +procedure TWalletRenderTests.OperationsFallbackUsesRowTime; +var LRow: Variant; +begin + LRow := TDataRow.New(-310003, TDataColumns.Create(TDataColumn.From('UnixTime'))); + LRow.UnixTime := 0; + AssertEquals(clBlue, BalanceColor(LRow, 12345)); + LRow.UnixTime := 12345; + AssertEquals(clGreen, BalanceColor(LRow, 12345)); +end; + +procedure TWalletRenderTests.AccountPaintingIgnoresLiveBlockHeight; +var LRow: Variant; +begin + LRow := TDataRow.New(-310004, TDataColumns.Create( + TDataColumn.From('IsPending'), TDataColumn.From('LastUpdatedBlock'))); + LRow.IsPending := False; + LRow.LastUpdatedBlock := High(Cardinal); + // No node exists in this fixture. Paint must consume the supplied row flag. + AssertEquals(clGreen, BalanceColor(LRow, 12345)); +end; + +procedure TWalletRenderTests.UnsignedBalancesUsePascFormatting; +var + LRow, LUnsigned, LSigned: Variant; + LActual, LExpected: TBitmap; + LHandled: Boolean; + LValue: UInt64; + I, X, Y: Integer; +begin + LRow := TDataRow.New(-310005, TDataColumns.Create(TDataColumn.From('IsPending'))); + LRow.IsPending := False; + LActual := TBitmap.Create; + LExpected := TBitmap.Create; + try + LActual.SetSize(200, 40); + LExpected.SetSize(200, 40); + for I := 0 to 1 do begin + if I = 0 then LValue := 16537985 else LValue := 8; + LUnsigned := LValue; + LSigned := Int64(LValue); + AssertEquals(varQWord, VarType(LUnsigned)); + LActual.Canvas.Brush.Color := clWhite; + LActual.Canvas.FillRect(Rect(0, 0, 200, 40)); + LExpected.Canvas.Brush.Color := clWhite; + LExpected.Canvas.FillRect(Rect(0, 0, 200, 40)); + LActual.Canvas.Font.Color := clBlack; + LExpected.Canvas.Font.Color := clBlack; + LHandled := False; + TCellRenderers.PASC_CheckPendingBalance(nil, 0, 0, LActual.Canvas, + Rect(0, 0, 200, 40), [], LUnsigned, LRow, LHandled); + AssertTrue('Unsigned account balances must use the PASC renderer', LHandled); + AssertEquals(clGreen, LActual.Canvas.Font.Color); + LHandled := False; + TCellRenderers.PASC_CheckPendingBalance(nil, 0, 0, LExpected.Canvas, + Rect(0, 0, 200, 40), [], LSigned, LRow, LHandled); + AssertTrue(LHandled); + for Y := 0 to 39 do + for X := 0 to 199 do + AssertEquals('Unsigned and signed monetary values must draw the same formatted text', + LExpected.Canvas.Pixels[X, Y], LActual.Canvas.Pixels[X, Y]); + end; + AssertEquals('1,653.7985', TAccountComp.FormatMoney(16537985)); + AssertEquals('0.0008', TAccountComp.FormatMoney(8)); + finally + LExpected.Free; + LActual.Free; + end; +end; + +initialization + RegisterTest(TWalletRenderTests); +end. diff --git a/src/tests/experimental/ui/UWalletRestoreTests.pas b/src/tests/experimental/ui/UWalletRestoreTests.pas new file mode 100644 index 000000000..f8e8ca568 --- /dev/null +++ b/src/tests/experimental/ui/UWalletRestoreTests.pas @@ -0,0 +1,229 @@ +unit UWalletRestoreTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + + Distributed under the MIT software license, see the accompanying file LICENSE + or visit http://www.opensource.org/licenses/mit-license.php. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, fpcunit, testregistry, UWallet, UCrypto; + +type + TWalletRestoreTests = class(TTestCase) + private + FDirectory, FBackup: String; + FKey: TECPrivateKey; + FKeyEventCount: Integer; + FKeyEventSawIndex: Boolean; + procedure SaveBackup(const APassword: String); + procedure OnKeyAdded(Sender: TObject); + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure WrongPasswordReturnsEmptyCounts; + procedure RestoreAndRepeatCountDuplicates; + procedure PrivateBackupUpgradesWatchOnlyKey; + procedure LockedWalletRejectsRestoreAndGeneration; + procedure UnloadReleasesWalletFile; + procedure PasswordlessKeySurvivesSaveAndReload; + procedure KeyAddedEventSeesUpdatedAccountIndex; + end; + +implementation + +uses UNode, UConst, UAccounts, UBlockChain, UPCDataTypes, UCommon; + +type + TKeyEventSafeBox = class(TPCSafeBox) + procedure Seed(const AKey: TAccountKey); + end; + +procedure TKeyEventSafeBox.Seed(const AKey: TAccountKey); +var + block: TOperationBlock; +begin + block := CT_OperationBlock_NUL; + block.account_key := AKey; + block.protocol_version := CT_PROTOCOL_1; + block.protocol_available := CT_PROTOCOL_6; + block.compact_target := CT_MinCompactTarget_v1; + AddNew(block); +end; + +procedure TWalletRestoreTests.SetUp; +var + id: TGUID; +begin + CreateGUID(id); + FDirectory := IncludeTrailingPathDelimiter(GetTempDir) + 'PascalCoinRestoreTest-' + GUIDToString(id); + ForceDirectories(FDirectory); + FBackup := FDirectory + PathDelim + 'generated-backup.wallet'; + // No node is constructed. Every file belongs to this generated fixture. + TNode.SetPascalCoinDataFolder(FDirectory); + TWallet.Load; + FKey := TECPrivateKey.Create; + FKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); +end; + +procedure TWalletRestoreTests.TearDown; +begin + TWallet.Unload; + FreeAndNil(FKey); + DeleteFile(FBackup); + DeleteFile(FDirectory + PathDelim + 'WalletKeys.dat'); + RemoveDir(FDirectory); + TNode.SetPascalCoinDataFolder(''); +end; + +procedure TWalletRestoreTests.SaveBackup(const APassword: String); +var + keys: TWalletKeys; + stream: TFileStream; +begin + keys := TWalletKeys.Create(nil); + try + keys.WalletPassword := APassword; + keys.AddPrivateKey('Generated fixture key', FKey); + stream := TFileStream.Create(FBackup, fmCreate); + try + keys.SaveToStream(stream); + finally + stream.Free; + end; + finally + keys.Free; + end; +end; + +procedure TWalletRestoreTests.WrongPasswordReturnsEmptyCounts; +var + restored: TRestoreWalletResult; +begin + SaveBackup('fixture password'); + restored := TWallet.RestoreWallet(FBackup, 'wrong'); + AssertFalse(restored.Success); + AssertEquals(0, restored.TotalKeysFound); + AssertEquals(0, restored.ImportedPrivateKeys); + AssertEquals(0, restored.ImportedPublicKeys); + AssertEquals(0, restored.Duplicates); + AssertEquals(0, TWallet.Keys.Count); +end; + +procedure TWalletRestoreTests.RestoreAndRepeatCountDuplicates; +var + restored: TRestoreWalletResult; +begin + SaveBackup('fixture password'); + restored := TWallet.RestoreWallet(FBackup, 'fixture password'); + AssertTrue(restored.Success); + AssertEquals(1, restored.TotalKeysFound); + AssertEquals(1, restored.ImportedPrivateKeys); + AssertEquals(0, restored.Duplicates); + restored := TWallet.RestoreWallet(FBackup, 'fixture password'); + AssertTrue(restored.Success); + AssertEquals(1, restored.Duplicates); + AssertEquals(0, restored.ImportedPrivateKeys); + AssertEquals(1, TWallet.Keys.Count); +end; + +procedure TWalletRestoreTests.PrivateBackupUpgradesWatchOnlyKey; +var + restored: TRestoreWalletResult; +begin + TWallet.Keys.AddPublicKey('Watch-only fixture', FKey.PublicKey); + SaveBackup(''); + restored := TWallet.RestoreWallet(FBackup, ''); + AssertTrue(restored.Success); + AssertEquals(1, restored.ImportedPrivateKeys); + AssertEquals(0, restored.Duplicates); + AssertEquals(1, TWallet.Keys.Count); + AssertTrue(TWallet.Keys[0].HasPrivateKey); + AssertTrue(Assigned(TWallet.Keys[0].PrivateKey)); +end; + +procedure TWalletRestoreTests.LockedWalletRejectsRestoreAndGeneration; +var + rejected: Boolean; +begin + SaveBackup(''); + TWallet.Keys.AddPrivateKey('Existing fixture', FKey); + TWallet.Keys.WalletPassword := 'target password'; + TWallet.Keys.LockWallet; + rejected := false; + try + TWallet.RestoreWallet(FBackup, ''); + except + on E: Exception do rejected := Pos('locked', LowerCase(E.Message)) > 0; + end; + AssertTrue('Restore must not encrypt new keys with an unknown target password', rejected); + rejected := false; + try + TWallet.GenerateNewKey('Should not be created', CT_Default_EC_OpenSSL_NID); + except + on E: Exception do rejected := Pos('locked', LowerCase(E.Message)) > 0; + end; + AssertTrue('Key generation must require the current wallet password', rejected); + TWallet.Keys.WalletPassword := 'target password'; + AssertTrue(TWallet.Keys.IsValidPassword); + AssertEquals(1, TWallet.Keys.Count); +end; + +procedure TWalletRestoreTests.UnloadReleasesWalletFile; +begin + TWallet.Keys.AddPrivateKey('Fixture', FKey); + TWallet.Unload; + AssertTrue(DeleteFile(FDirectory + PathDelim + 'WalletKeys.dat')); + // Repeated shutdown is safe, and the same directory can be reopened. + TWallet.Unload; + TWallet.Load; + AssertEquals(0, TWallet.Keys.Count); +end; + +procedure TWalletRestoreTests.PasswordlessKeySurvivesSaveAndReload; +begin + TWallet.Keys.AddPrivateKey('Fixture', FKey); + TWallet.Unload; + TWallet.Load; + AssertEquals(1, TWallet.Keys.Count); + AssertTrue(TWallet.Keys.IsValidPassword); + AssertTrue(Assigned(TWallet.Keys[0].PrivateKey)); + AssertTrue(TAccountComp.EqualAccountKeys(FKey.PublicKey, TWallet.Keys[0].AccountKey)); +end; + +procedure TWalletRestoreTests.OnKeyAdded(Sender: TObject); +begin + Inc(FKeyEventCount); + FKeyEventSawIndex := TWallet.Keys.AccountsKeyList.IndexOfAccountKey(FKey.PublicKey) >= 0; +end; + +procedure TWalletRestoreTests.KeyAddedEventSeesUpdatedAccountIndex; +var + safebox: TKeyEventSafeBox; +begin + safebox := TKeyEventSafeBox.Create; + try + safebox.Seed(FKey.PublicKey); + TWallet.Keys.SafeBox := safebox; + FKeyEventCount := 0; + FKeyEventSawIndex := false; + TWallet.Keys.OnChanged.Add(OnKeyAdded); + try + TWallet.Keys.AddPrivateKey('Account-owning generated fixture', FKey); + AssertEquals('One completed key mutation raises one notification', 1, FKeyEventCount); + AssertTrue('UI listeners must see the completed account-key index', FKeyEventSawIndex); + finally + TWallet.Keys.OnChanged.Remove(OnKeyAdded); + end; + finally + TWallet.Keys.SafeBox := nil; + safebox.Free; + end; +end; + +initialization + RegisterTest(TWalletRestoreTests); +end. diff --git a/src/tests/experimental/ui/UWalletSnapshotTests.pas b/src/tests/experimental/ui/UWalletSnapshotTests.pas new file mode 100644 index 000000000..89485e872 --- /dev/null +++ b/src/tests/experimental/ui/UWalletSnapshotTests.pas @@ -0,0 +1,291 @@ +unit UWalletSnapshotTests; + +{ Copyright © 2026 Herman Schoenfeld + Distributed under the MIT software license, see the accompanying LICENSE. + These fixtures use synthetic memberships and generated keys, without a node. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, SyncObjs, fpcunit, testregistry, Generics.Collections, + UCrypto, UAccounts, UPCDataTypes, UBaseTypes, UDataSources, UCommon.Data; + +type + TMembershipHistorySource = class(TAccountsOperationsDataSource) + protected + function FetchWalletAccounts: TArray<Cardinal>; override; + public + FixtureAccounts: TArray<Cardinal>; + ResolveCount: Integer; + BlockResolution: Boolean; + Entered, ReleaseResolution: TEvent; + constructor Create; + destructor Destroy; override; + procedure Resolve; + end; + + TBlockedAccountsSource = class(TAccountsDataSource) + public + Entered, ReleaseFetch: TEvent; + constructor Create; + destructor Destroy; override; + procedure FetchAll(const AContainer: TList<TAccount>); override; + procedure Fetch; + end; + + TWalletSnapshotTests = class(TTestCase) + private + FHistory: TMembershipHistorySource; + FAccounts: TBlockedAccountsSource; + FSettingsKey: TAccountKey; + procedure ChangeHistorySettings; + procedure ChangeAccountSettings; + published + procedure WalletMembershipResolvesOnlyWhenMarkedDirty; + procedure MembershipSettingsDoNotWaitForResolution; + procedure AccountSettingsDoNotWaitForFetch; + end; + +implementation + +uses UCommon, UConst, UMemory, UThread; + +type + TSnapshotActionThread = class(TPCThread) + private + FAction: TThreadMethod; + protected + procedure BCExecute; override; + public + Completed: TEvent; + ErrorMessage: String; + constructor Create(AAction: TThreadMethod); + destructor Destroy; override; + end; + +constructor TSnapshotActionThread.Create(AAction: TThreadMethod); +begin + inherited Create(True); + FreeOnTerminate := False; + FAction := AAction; + Completed := TEvent.Create(nil, True, False, ''); + Start; +end; + +destructor TSnapshotActionThread.Destroy; +begin + Completed.Free; + inherited Destroy; +end; + +procedure TSnapshotActionThread.BCExecute; +begin + try + try + FAction; + except + on E: Exception do ErrorMessage := E.ClassName + ': ' + E.Message; + end; + finally + Completed.SetEvent; + end; +end; + +constructor TMembershipHistorySource.Create; +begin + inherited Create(nil); + Entered := TEvent.Create(nil, True, False, ''); + ReleaseResolution := TEvent.Create(nil, True, False, ''); +end; + +destructor TMembershipHistorySource.Destroy; +begin + Entered.Free; + ReleaseResolution.Free; + inherited Destroy; +end; + +function TMembershipHistorySource.FetchWalletAccounts: TArray<Cardinal>; +begin + Inc(ResolveCount); + Result := TArrayTool<Cardinal>.Copy(FixtureAccounts); + if BlockResolution then begin + Entered.SetEvent; + if ReleaseResolution.WaitFor(10000) <> wrSignaled then + raise Exception.Create('Membership resolution gate timed out'); + end; +end; + +procedure TMembershipHistorySource.Resolve; +begin + RefreshWalletAccounts; +end; + +constructor TBlockedAccountsSource.Create; +begin + inherited Create(nil); + Entered := TEvent.Create(nil, True, False, ''); + ReleaseFetch := TEvent.Create(nil, True, False, ''); +end; + +destructor TBlockedAccountsSource.Destroy; +begin + Entered.Free; + ReleaseFetch.Free; + inherited Destroy; +end; + +procedure TBlockedAccountsSource.FetchAll(const AContainer: TList<TAccount>); +begin + Entered.SetEvent; + if ReleaseFetch.WaitFor(10000) <> wrSignaled then + raise Exception.Create('Account fetch gate timed out'); +end; + +procedure TBlockedAccountsSource.Fetch; +var LTable: TDataTable; +begin + FetchPage(TPageFetchParams.Create(0, 25, nil, foAnd), LTable); +end; + +procedure TWalletSnapshotTests.ChangeHistorySettings; +begin + FHistory.InvalidateWalletAccounts; + FHistory.UseWalletAccounts := False; + FHistory.Accounts := TArray<Cardinal>.Create(77); + if FHistory.Accounts[0] <> 77 then + raise Exception.Create('Explicit membership was not retained'); + FHistory.BlockDepth := 300; + if FHistory.BlockDepth <> 300 then + raise Exception.Create('History depth was not retained'); + FHistory.UseWalletAccounts := True; +end; + +procedure TWalletSnapshotTests.ChangeAccountSettings; +var LKeys: TArray<TAccountKey>; +begin + FAccounts.FilterKeys := TArray<TAccountKey>.Create(FSettingsKey); + LKeys := FAccounts.FilterKeys; + if Length(LKeys) <> 1 then + raise Exception.Create('Account filter was not retained'); + LKeys := nil; + FAccounts.IncludePending := True; + if not FAccounts.IncludePending then + raise Exception.Create('Pending preference was not retained'); + FAccounts.InvalidateSnapshot; +end; + +procedure TWalletSnapshotTests.WalletMembershipResolvesOnlyWhenMarkedDirty; +var + LSource: TMembershipHistorySource; + LNumbers: TArray<Cardinal>; + LDisposables: TDisposables; +begin + LSource := LDisposables.AddObject(TMembershipHistorySource.Create) as TMembershipHistorySource; + LSource.FixtureAccounts := TArray<Cardinal>.Create(1, 2, 3); + LSource.UseWalletAccounts := True; + AssertEquals(0, LSource.ResolveCount); // The UI setter must not enumerate. + LSource.Resolve; + LSource.Resolve; + AssertEquals(1, LSource.ResolveCount); + LNumbers := LSource.Accounts; + AssertEquals(1, Integer(LNumbers[0])); + LNumbers[0] := 999; + LNumbers := LSource.Accounts; + AssertEquals('Getter returns an independent array', 1, Integer(LNumbers[0])); + LSource.FixtureAccounts := TArray<Cardinal>.Create(4, 5, 6); // Same cardinality. + LSource.InvalidateSnapshot; + LSource.Resolve; + AssertEquals('Ordinary data changes do not re-enumerate membership', 1, LSource.ResolveCount); + LSource.InvalidateWalletAccounts; + LSource.Resolve; + AssertEquals(2, LSource.ResolveCount); + LNumbers := LSource.Accounts; + AssertEquals('Same-count replacements must be refreshed', 4, Integer(LNumbers[0])); + LSource.UseWalletAccounts := False; + LSource.Accounts := TArray<Cardinal>.Create(77); + LSource.Resolve; + AssertEquals(2, LSource.ResolveCount); + LNumbers := LSource.Accounts; + AssertEquals(77, Integer(LNumbers[0])); +end; + +procedure TWalletSnapshotTests.MembershipSettingsDoNotWaitForResolution; +var + LResolve, LSettings: TSnapshotActionThread; +begin + FHistory := TMembershipHistorySource.Create; + try + FHistory.FixtureAccounts := TArray<Cardinal>.Create(1, 2, 3); + FHistory.UseWalletAccounts := True; + FHistory.BlockResolution := True; + LResolve := nil; + LSettings := nil; + try + LResolve := TSnapshotActionThread.Create(FHistory.Resolve); + AssertEquals(Ord(wrSignaled), Ord(FHistory.Entered.WaitFor(5000))); + LSettings := TSnapshotActionThread.Create(ChangeHistorySettings); + AssertEquals('Settings waited for membership resolution', Ord(wrSignaled), + Ord(LSettings.Completed.WaitFor(500))); + AssertEquals('', LSettings.ErrorMessage); + AssertEquals(Ord(wrTimeout), Ord(FHistory.ReleaseResolution.WaitFor(0))); + finally + FHistory.ReleaseResolution.SetEvent; + if Assigned(LResolve) then LResolve.WaitFor; + if Assigned(LSettings) then LSettings.WaitFor; + LSettings.Free; + LResolve.Free; + end; + AssertEquals('Stale resolution must not publish into the new settings', 0, Length(FHistory.Accounts)); + FHistory.BlockResolution := False; + FHistory.FixtureAccounts := TArray<Cardinal>.Create(41, 42, 43); + FHistory.Resolve; + AssertEquals(2, FHistory.ResolveCount); + AssertEquals(41, Integer(FHistory.Accounts[0])); + finally + FHistory.Free; + FHistory := nil; + end; +end; + +procedure TWalletSnapshotTests.AccountSettingsDoNotWaitForFetch; +var + LFetch, LSettings: TSnapshotActionThread; + LKey: TECPrivateKey; + LDisposables: TDisposables; +begin + TCrypto.InitCrypto; + LKey := LDisposables.AddObject(TECPrivateKey.Create) as TECPrivateKey; + LKey.GenerateRandomPrivateKey(CT_Default_EC_OpenSSL_NID); + FSettingsKey := LKey.PublicKey; + FAccounts := TBlockedAccountsSource.Create; + try + FAccounts.SnapshotCacheEnabled := True; + LFetch := nil; + LSettings := nil; + try + LFetch := TSnapshotActionThread.Create(FAccounts.Fetch); + AssertEquals(Ord(wrSignaled), Ord(FAccounts.Entered.WaitFor(5000))); + LSettings := TSnapshotActionThread.Create(ChangeAccountSettings); + AssertEquals('Account settings waited for FetchPage', Ord(wrSignaled), + Ord(LSettings.Completed.WaitFor(500))); + AssertEquals('', LSettings.ErrorMessage); + AssertEquals(Ord(wrTimeout), Ord(FAccounts.ReleaseFetch.WaitFor(0))); + finally + FAccounts.ReleaseFetch.SetEvent; + if Assigned(LFetch) then LFetch.WaitFor; + if Assigned(LSettings) then LSettings.WaitFor; + LSettings.Free; + LFetch.Free; + end; + finally + FAccounts.Free; + FAccounts := nil; + end; +end; + +initialization + RegisterTest(TWalletSnapshotTests); + +end. diff --git a/src/tests/experimental/ui/UWalletViewTests.pas b/src/tests/experimental/ui/UWalletViewTests.pas new file mode 100644 index 000000000..06491ed6f --- /dev/null +++ b/src/tests/experimental/ui/UWalletViewTests.pas @@ -0,0 +1,281 @@ +unit UWalletViewTests; + +{ Copyright (c) 2026 by Herman Schoenfeld + Distributed under the MIT software license; see the accompanying LICENSE. + Physical synchronization scopes own node busy state. Grid admission and optional + per-grid quiet timers are tested separately from that work accounting. } + +{$mode delphi} + +interface + +uses Classes, SysUtils, Forms, fpcunit, testregistry; + +type + TWalletViewTests = class(TTestCase) + published + procedure NestedBlockWorkEndsAfterLastScope; + procedure ConcurrentBlockWorkRemainsBusyUntilBothWorkersFinish; + procedure FailedBlockWorkReleasesItsPhysicalScope; + end; + +implementation + +uses Windows, SyncObjs, UThread, UNode, UCrypto; + +type + TCoreNodeObserver = class(TComponent) + private + FNotifier: TNodeNotifyEvents; + procedure SynchronizationChanged(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); + public + SynchronizationCallbackCount: Integer; + LastStatus: TNodeSynchronizationStatus; + constructor Create(ANode: TNode); reintroduce; + destructor Destroy; override; + procedure PumpFor(AMilliseconds: Cardinal); + end; + + TBlockWorkThread = class(TPCThread) + private + FNode: TNode; + FEntered, FReleaseWork: TEvent; + protected + procedure BCExecute; override; + public + WatchdogReleased: Boolean; + constructor Create(ANode: TNode); + destructor Destroy; override; + procedure WaitForEntry; + procedure Stop; + end; + +procedure OpenBackendTest(out ANode: TNode; out APreviousDirectory, + ADirectory: String); +var LID: TGUID; +begin + if TNode.NodeExists then + raise Exception.Create('The core-work fixture must not reuse a global node'); + CreateGUID(LID); + ADirectory := IncludeTrailingPathDelimiter(GetTempDir)+'WalletBlockWork-'+GUIDToString(LID); + ForceDirectories(ADirectory); + APreviousDirectory := TNode.GetPascalCoinDataFolder; + TNode.SetPascalCoinDataFolder(ADirectory); + try + TCrypto.InitCrypto; + ANode := TNode.Create(nil); + ANode.BroadcastData := False; + except + TNode.SetPascalCoinDataFolder(APreviousDirectory); + RemoveDir(ADirectory); + raise; + end; +end; + +procedure CloseBackendTest(var ANode: TNode; const APreviousDirectory, + ADirectory: String); +begin + FreeAndNil(ANode); + TNode.SetPascalCoinDataFolder(APreviousDirectory); + RemoveDir(ADirectory); +end; + +constructor TBlockWorkThread.Create(ANode: TNode); +begin + inherited Create(True); + FreeOnTerminate := False; + FNode := ANode; + FEntered := TEvent.Create(nil,True,False,''); + FReleaseWork := TEvent.Create(nil,True,False,''); +end; + +destructor TBlockWorkThread.Destroy; +begin + Stop; + FEntered.Free; + FReleaseWork.Free; + inherited; +end; + +procedure TBlockWorkThread.BCExecute; +begin + FNode.BeginBlockChainUpdate; + try + FEntered.SetEvent; + WatchdogReleased := FReleaseWork.WaitFor(20000) <> wrSignaled; + finally + FNode.EndBlockChainUpdate; + end; +end; + +procedure TBlockWorkThread.WaitForEntry; +begin + if FEntered.WaitFor(2500) <> wrSignaled then + raise Exception.Create('The block-work fixture did not enter its update scope'); +end; + +procedure TBlockWorkThread.Stop; +begin + FReleaseWork.SetEvent; + WaitFor; +end; + +constructor TCoreNodeObserver.Create(ANode: TNode); +begin + inherited Create(nil); + FNotifier := TNodeNotifyEvents.Create(Self); + FNotifier.Node := ANode; + FNotifier.OnSynchronizationChanged := SynchronizationChanged; +end; + +destructor TCoreNodeObserver.Destroy; +begin + FNotifier.OnSynchronizationChanged := nil; + inherited; +end; + +procedure TCoreNodeObserver.SynchronizationChanged(Sender: TObject; + const AStatus: TNodeSynchronizationStatus); +begin + Inc(SynchronizationCallbackCount); + LastStatus := AStatus; +end; + +procedure TCoreNodeObserver.PumpFor(AMilliseconds: Cardinal); +var LDeadline: QWord; +begin + LDeadline := GetTickCount64+AMilliseconds; + repeat + Application.ProcessMessages; + CheckSynchronize(0); + Sleep(1); + until GetTickCount64>=LDeadline; +end; + +procedure TWalletViewTests.NestedBlockWorkEndsAfterLastScope; +var + LNode: TNode; + LObserver: TCoreNodeObserver; + LDirectory, LPreviousDirectory: String; +begin + OpenBackendTest(LNode,LPreviousDirectory,LDirectory); + LObserver := nil; + try + LObserver := TCoreNodeObserver.Create(LNode); + LNode.BeginBlockChainUpdate; + try + LNode.BeginBlockChainUpdate; + try + AssertEquals('Each nested scope owns one physical reservation',2, + LNode.GetSynchronizationStatus.ActiveTransfers); + AssertTrue(LNode.IsUpdatingBlockChain); + LObserver.PumpFor(150); + AssertEquals('The status event reports the same physical reservations',2, + LObserver.LastStatus.ActiveTransfers); + AssertEquals(Ord(nssSynchronizing),Ord(LObserver.LastStatus.State)); + finally + LNode.EndBlockChainUpdate; + end; + LObserver.PumpFor(150); + AssertTrue('Ending an inner scope must leave outer work active',LNode.IsUpdatingBlockChain); + AssertEquals(1,LNode.GetSynchronizationStatus.ActiveTransfers); + AssertEquals(1,LObserver.LastStatus.ActiveTransfers); + finally + LNode.EndBlockChainUpdate; + end; + AssertFalse(LNode.IsUpdatingBlockChain); + AssertEquals('The final exit releases physical work immediately',0, + LNode.GetSynchronizationStatus.ActiveTransfers); + LObserver.PumpFor(150); + AssertTrue('Actual synchronization callbacks must have been delivered', + LObserver.SynchronizationCallbackCount>0); + AssertEquals(0,LObserver.LastStatus.ActiveTransfers); + AssertTrue(LObserver.LastStatus.State<>nssSynchronizing); + finally + LObserver.Free; + CloseBackendTest(LNode,LPreviousDirectory,LDirectory); + end; +end; + +procedure TWalletViewTests.ConcurrentBlockWorkRemainsBusyUntilBothWorkersFinish; +var + LNode: TNode; + LObserver: TCoreNodeObserver; + LFirst, LSecond: TBlockWorkThread; + LDirectory, LPreviousDirectory: String; +begin + OpenBackendTest(LNode,LPreviousDirectory,LDirectory); + LObserver := nil; + LFirst := nil; + LSecond := nil; + try + LObserver := TCoreNodeObserver.Create(LNode); + LFirst := TBlockWorkThread.Create(LNode); + LFirst.Start; + LFirst.WaitForEntry; + LSecond := TBlockWorkThread.Create(LNode); + LSecond.Start; + LSecond.WaitForEntry; + AssertEquals('Both producers own a physical reservation',2, + LNode.GetSynchronizationStatus.ActiveTransfers); + LFirst.Stop; + LObserver.PumpFor(150); + AssertTrue('One completed producer must not clear another producer',LNode.IsUpdatingBlockChain); + AssertEquals(1,LNode.GetSynchronizationStatus.ActiveTransfers); + AssertEquals(1,LObserver.LastStatus.ActiveTransfers); + LSecond.Stop; + AssertFalse(LNode.IsUpdatingBlockChain); + AssertEquals(0,LNode.GetSynchronizationStatus.ActiveTransfers); + LObserver.PumpFor(150); + AssertEquals(0,LObserver.LastStatus.ActiveTransfers); + AssertTrue(LObserver.LastStatus.State<>nssSynchronizing); + AssertFalse(LFirst.WatchdogReleased); + AssertFalse(LSecond.WatchdogReleased); + finally + LSecond.Free; + LFirst.Free; + LObserver.Free; + CloseBackendTest(LNode,LPreviousDirectory,LDirectory); + end; +end; + +procedure TWalletViewTests.FailedBlockWorkReleasesItsPhysicalScope; +var + LNode: TNode; + LObserver: TCoreNodeObserver; + LDirectory, LPreviousDirectory: String; + LFailed: Boolean; +begin + OpenBackendTest(LNode,LPreviousDirectory,LDirectory); + LObserver := nil; + try + LObserver := TCoreNodeObserver.Create(LNode); + LFailed := False; + try + LNode.BeginBlockChainUpdate; + try + LObserver.PumpFor(150); + raise EAbort.Create('Controlled invalid-block validation failure'); + finally + LNode.EndBlockChainUpdate; + end; + except + on E: EAbort do LFailed := True; + end; + AssertTrue('The controlled validation failure must execute',LFailed); + AssertFalse('Failure cleanup must balance the active-work scope',LNode.IsUpdatingBlockChain); + AssertEquals(0,LNode.GetSynchronizationStatus.ActiveTransfers); + LObserver.PumpFor(150); + AssertEquals('The completion event must release the physical busy state',0, + LObserver.LastStatus.ActiveTransfers); + AssertTrue(LObserver.LastStatus.State<>nssSynchronizing); + finally + LObserver.Free; + CloseBackendTest(LNode,LPreviousDirectory,LDirectory); + end; +end; + +initialization + RegisterTest(TWalletViewTests); +end. diff --git a/src/tests/experimental/ui/WalletGridRefreshTests.lpr b/src/tests/experimental/ui/WalletGridRefreshTests.lpr new file mode 100644 index 000000000..7e57e78f8 --- /dev/null +++ b/src/tests/experimental/ui/WalletGridRefreshTests.lpr @@ -0,0 +1,35 @@ +program WalletGridRefreshTests; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + Dedicated process for the real controller's integration fixture. } + +{$mode delphi} + +uses + heaptrc, Interfaces, Forms, SysUtils, fpcunit, testregistry, plaintestreport, + UWalletGridIntegrationTests, UMemory; + +procedure RunTests; +var + LResults: TTestResult; + LWriter: TPlainResultsWriter; + LDisposed: TDisposables; +begin + LWriter := LDisposed.AddObject(TPlainResultsWriter.Create(nil)) as TPlainResultsWriter; + LResults := LDisposed.AddObject(TTestResult.Create) as TTestResult; + LWriter.FileName := ParamStr(1); + LResults.AddListener(LWriter); + GetTestRegistry.Run(LResults); + LWriter.WriteResult(LResults); + if (LResults.NumberOfErrors = 0) and (LResults.NumberOfFailures = 0) then + ExitCode := 0 + else + ExitCode := 1; +end; + +begin + if ParamCount <> 1 then Halt(64); + SetHeapTraceOutput(ParamStr(1) + '.heap.log'); + Application.Initialize; + RunTests; +end. diff --git a/tools/ConvertLazarusImageList.lpr b/tools/ConvertLazarusImageList.lpr new file mode 100644 index 000000000..7bb090a67 --- /dev/null +++ b/tools/ConvertLazarusImageList.lpr @@ -0,0 +1,125 @@ +program ConvertLazarusImageList; + +{ Copyright © 2026 Herman Schoenfeld <https://sphere10.com> + + Converts a Lazarus image-list property stream to the native Windows stream + consumed by VCL. Existing LCL readers and the Windows image-list serializer + retain the original artwork; this tool does not redraw or rescale it. + + Distributed under the MIT software license, see the accompanying LICENSE. +} + +{$mode delphi}{$H+} + +uses + Interfaces, Forms, Controls, Classes, SysUtils, Windows, ActiveX, CommCtrl, ImgList; + +function ReadBitmapPixels(ABitmap: HBITMAP; out AWidth, AHeight: Integer): TBytes; +var + LBitmap: Windows.TBitmap; + LInfo: Windows.TBitmapInfo; + LDC: HDC; +begin + Result := nil; + if GetObject(ABitmap, SizeOf(LBitmap), @LBitmap) = 0 then RaiseLastOSError; + AWidth := LBitmap.bmWidth; + AHeight := Abs(LBitmap.bmHeight); + FillChar(LInfo, SizeOf(LInfo), 0); + LInfo.bmiHeader.biSize := SizeOf(LInfo.bmiHeader); + LInfo.bmiHeader.biWidth := AWidth; + LInfo.bmiHeader.biHeight := -AHeight; + LInfo.bmiHeader.biPlanes := 1; + LInfo.bmiHeader.biBitCount := 32; + LInfo.bmiHeader.biCompression := BI_RGB; + SetLength(Result, AWidth * AHeight * 4); + LDC := GetDC(0); + try + if Windows.GetDIBits(LDC, ABitmap, 0, AHeight, @Result[0], LInfo, + DIB_RGB_COLORS) = 0 then RaiseLastOSError; + finally + ReleaseDC(0, LDC); + end; +end; + +procedure VerifyPixels(AOriginal, AConverted: HIMAGELIST; AWidth, AHeight: Integer); +var + LBefore, LAfter: IMAGEINFO; + LBeforePixels, LAfterPixels: TBytes; + LBeforeWidth, LBeforeHeight, LAfterWidth, LAfterHeight: Integer; + i, y, LPart, LBeforeOffset, LAfterOffset: Integer; + LBeforeBitmap, LAfterBitmap: HBITMAP; +begin + if ImageList_GetImageCount(AOriginal) <> ImageList_GetImageCount(AConverted) then + raise Exception.Create('Image-list conversion changed the image count'); + for i := 0 to ImageList_GetImageCount(AOriginal) - 1 do begin + if not ImageList_GetImageInfo(AOriginal, i, @LBefore) or + not ImageList_GetImageInfo(AConverted, i, @LAfter) then RaiseLastOSError; + for LPart := 0 to 1 do begin + if LPart = 0 then begin + LBeforeBitmap := LBefore.hbmImage; + LAfterBitmap := LAfter.hbmImage; + end else begin + LBeforeBitmap := LBefore.hbmMask; + LAfterBitmap := LAfter.hbmMask; + end; + if (LBeforeBitmap = 0) and (LAfterBitmap = 0) then Continue; + if (LBeforeBitmap = 0) or (LAfterBitmap = 0) then + raise Exception.Create('Image-list conversion changed the transparency mask'); + LBeforePixels := ReadBitmapPixels(LBeforeBitmap, LBeforeWidth, LBeforeHeight); + LAfterPixels := ReadBitmapPixels(LAfterBitmap, LAfterWidth, LAfterHeight); + for y := 0 to AHeight - 1 do begin + LBeforeOffset := ((LBefore.rcImage.Top + y) * LBeforeWidth + LBefore.rcImage.Left) * 4; + LAfterOffset := ((LAfter.rcImage.Top + y) * LAfterWidth + LAfter.rcImage.Left) * 4; + if not CompareMem(@LBeforePixels[LBeforeOffset], @LAfterPixels[LAfterOffset], AWidth * 4) then + raise Exception.CreateFmt('Image-list conversion changed pixels/mask in image %d', [i]); + end; + end; + end; +end; + +var + LImages: TImageList; + LResolution: TCustomImageListResolution; + LInput, LOutput: TMemoryStream; + LAdapter: IStream; + LNative, LRoundTrip: HIMAGELIST; +begin + if ParamCount <> 2 then begin + WriteLn(ErrOutput, 'Usage: ConvertLazarusImageList input.bin output.bin'); + Halt(64); + end; + Application.Initialize; + LImages := TImageList.Create(nil); + LInput := TMemoryStream.Create; + LOutput := TMemoryStream.Create; + LRoundTrip := 0; + try + LInput.LoadFromFile(ParamStr(1)); + LImages.ReadData(LInput); + LResolution := LImages.ResolutionByIndex[0]; + if LResolution.Count = 0 then raise Exception.Create('An empty image list cannot be converted'); + // The Win32 widgetset already owns this native list, using the exact image + // bytes loaded by LCL. Keep LImages alive until serialization and comparison + // finish; its reference handle must not be freed by this tool. + // ReadData loads the serialized resolution's own dimensions. Access that + // resolution directly instead of asking the list's default 16-pixel width + // for an automatically resized copy. + LNative := LResolution.Reference.Handle; + LAdapter := TStreamAdapter.Create(LOutput, soReference); + if not ImageList_Write(LNative, LAdapter) then + raise Exception.Create('Windows could not serialize the image list'); + LOutput.Position := 0; + LRoundTrip := ImageList_Read(LAdapter); + if LRoundTrip = 0 then raise Exception.Create('Windows could not read its converted image list'); + VerifyPixels(LNative, LRoundTrip, LResolution.Width, LResolution.Height); + LOutput.SaveToFile(ParamStr(2)); + WriteLn(Format('Converted %d images, %dx%d; native pixels/masks round-trip verified', + [LResolution.Count, LResolution.Width, LResolution.Height])); + finally + if LRoundTrip <> 0 then ImageList_Destroy(LRoundTrip); + LAdapter := nil; + LOutput.Free; + LInput.Free; + LImages.Free; + end; +end. diff --git a/tools/SyncDelphiForms.ps1 b/tools/SyncDelphiForms.ps1 new file mode 100644 index 000000000..4f7dcb8b0 --- /dev/null +++ b/tools/SyncDelphiForms.ps1 @@ -0,0 +1,248 @@ +<# +.SYNOPSIS +Generates VCL DFM companions from the canonical Lazarus LFM resources. +.DESCRIPTION +Maps the supported LCL controls and properties to VCL, preserves PNG artwork +bytes, and uses the existing LCL/Win32 image-list readers and serializer for +native image-list streams. Original LFM files are never modified. The native +converter is compiled into the ignored tools/lib directory when needed. + +Use -AssetsOnly or -PropertiesOnly to repair existing DFM companions without +regenerating their other layout settings. Both switches may be combined. +.EXAMPLE +.\SyncDelphiForms.ps1 -AssetsOnly -PropertiesOnly +#> +# Copyright © 2026 Herman Schoenfeld <https://sphere10.com>. MIT license. +param( + [string] $SourceDirectory = (Join-Path $PSScriptRoot '..\src\gui-experimental'), + [switch] $AssetsOnly, + [switch] $PropertiesOnly, + [string] $ImageListConverter, + [string] $FpcPath = $(if ($c = Get-Command fpc -ErrorAction SilentlyContinue) { $c.Source } else { '' }), + [string] $LazarusDirectory = $(if ($c = Get-Command lazbuild -ErrorAction SilentlyContinue) { Split-Path -Parent $c.Source } else { '' }) +) + +$ErrorActionPreference = 'Stop' +$source = (Resolve-Path -LiteralPath $SourceDirectory).Path +$encoding = [Text.UTF8Encoding]::new($true) +$converterSource = Join-Path $PSScriptRoot 'ConvertLazarusImageList.lpr' +$conversionOutput = Join-Path $PSScriptRoot 'lib\delphi-forms' +$conversionWork = Join-Path $conversionOutput ('assets-' + [guid]::NewGuid().ToString('N')) +$script:DelphiImageListConverter = $ImageListConverter +$script:DelphiPictureCount = 0 +$script:DelphiImageListCount = 0 + +function ConvertFrom-ResourceHex([string] $Hex) { + $compact = $Hex -replace '\s', '' + if (($compact.Length % 2) -ne 0) { throw 'A resource contains an incomplete hex byte.' } + $bytes = [byte[]]::new($compact.Length / 2) + for ($index = 0; $index -lt $bytes.Length; $index++) { + $bytes[$index] = [Convert]::ToByte($compact.Substring($index * 2, 2), 16) + } + return ,$bytes +} + +function Get-ImageListConverter { + if ($script:DelphiImageListConverter) { + return (Resolve-Path -LiteralPath $script:DelphiImageListConverter).Path + } + $compiler = (Get-Command $FpcPath -ErrorAction Stop).Source + $lazarus = (Resolve-Path -LiteralPath $LazarusDirectory).Path + $targetInfo = ((& $compiler '-iTP' '-iTO') -join ' ').Trim() -split '\s+' + if ($LASTEXITCODE -ne 0 -or $targetInfo.Count -ne 2 -or $targetInfo[1] -notmatch '^win(32|64)$') { + throw 'Image-list conversion requires a Windows Free Pascal compiler and matching Lazarus units.' + } + $target = $targetInfo -join '-' + $units = Join-Path $conversionOutput ('units-' + $target) + New-Item -ItemType Directory -Path $conversionOutput, $units -Force | Out-Null + $executable = Join-Path $conversionOutput 'ConvertLazarusImageList.exe' + if (-not (Test-Path -LiteralPath $executable) -or + (Get-Item -LiteralPath $executable).LastWriteTimeUtc -lt (Get-Item -LiteralPath $converterSource).LastWriteTimeUtc) { + $arguments = @( + '-MDelphi', '-O1', '-gl', + "-Fu$lazarus\lcl\units\$target", "-Fu$lazarus\lcl\units\$target\win32", + "-Fu$lazarus\components\lazutils\lib\$target", "-Fu$lazarus\packager\units\$target", + "-FU$units", "-FE$conversionOutput", "-o$executable", $converterSource + ) + & $compiler @arguments *> (Join-Path $conversionOutput 'converter-build.log') + if ($LASTEXITCODE -ne 0) { + Get-Content -LiteralPath (Join-Path $conversionOutput 'converter-build.log') -Tail 40 + throw 'The native image-list converter failed to compile.' + } + } + $script:DelphiImageListConverter = $executable + return $executable +} + +function ConvertTo-DelphiAsset([byte[]] $Bytes, [string] $Property, [string] $AssetName) { + if ($Property -eq 'Picture.Data' -and $Bytes.Length -gt 5) { + $nameLength = [int]$Bytes[0] + if ($Bytes.Length -gt ($nameLength + 5)) { + $className = [Text.Encoding]::ASCII.GetString($Bytes, 1, $nameLength) + if ($className -eq 'TPortableNetworkGraphic') { + $dataStart = 1 + $nameLength + 4 + $declaredSize = [BitConverter]::ToUInt32($Bytes, 1 + $nameLength) + if ($declaredSize -ne ($Bytes.Length - $dataStart)) { + throw "PNG length mismatch in $AssetName." + } + $signature = [BitConverter]::ToString($Bytes, $dataStart, 8) + if ($signature -ne '89-50-4E-47-0D-0A-1A-0A') { throw "Invalid PNG in $AssetName." } + # VCL TPicture stores its graphic class name followed by the + # PNG itself. Lazarus adds a size prefix which VCL does not use. + $vclName = [Text.Encoding]::ASCII.GetBytes('TPngImage') + $result = [byte[]]::new(1 + $vclName.Length + $declaredSize) + $result[0] = $vclName.Length + [Array]::Copy($vclName, 0, $result, 1, $vclName.Length) + [Array]::Copy($Bytes, $dataStart, $result, 1 + $vclName.Length, $declaredSize) + $script:DelphiPictureCount++ + return ,$result + } + } + } + if ($Property -eq 'Bitmap' -and $Bytes.Length -gt 2 -and + $Bytes[0] -eq 0x4C -and ($Bytes[1] -eq 0x69 -or $Bytes[1] -eq 0x7A)) { + $converter = Get-ImageListConverter + New-Item -ItemType Directory -Path $conversionWork -Force | Out-Null + $inputFile = Join-Path $conversionWork ($AssetName + '.lazarus.bin') + $outputFile = Join-Path $conversionWork ($AssetName + '.vcl.bin') + [IO.File]::WriteAllBytes($inputFile, $Bytes) + & $converter $inputFile $outputFile | Write-Host + if ($LASTEXITCODE -ne 0) { throw "Image-list conversion failed for $AssetName." } + $script:DelphiImageListCount++ + return ,([IO.File]::ReadAllBytes($outputFile)) + } + return ,$Bytes +} + +function Convert-DelphiAssets([string] $Text, [string] $BaseName) { + $assetIndex = [int[]]@(0) + $pattern = '(?m)^(?<indent>[ \t]*)(?<property>Picture\.Data|Bitmap)\s*=\s*\{(?<hex>[0-9A-Fa-f\s]+)\}' + return [regex]::Replace($Text, $pattern, { + param($match) + $assetIndex[0]++ + $bytes = ConvertFrom-ResourceHex $match.Groups['hex'].Value + $property = $match.Groups['property'].Value + $converted = ConvertTo-DelphiAsset $bytes $property ($BaseName + '-' + $assetIndex[0]) + if ([Convert]::ToBase64String($bytes) -eq [Convert]::ToBase64String($converted)) { return $match.Value } + $hex = [BitConverter]::ToString($converted).Replace('-', '') + $indent = $match.Groups['indent'].Value + $builder = [Text.StringBuilder]::new() + [void]$builder.Append($indent + $property + " = {`r`n") + for ($index = 0; $index -lt $hex.Length; $index += 64) { + [void]$builder.Append($indent + ' ' + $hex.Substring($index, [Math]::Min(64, $hex.Length - $index)) + "`r`n") + } + [void]$builder.Append($indent + '}') + return $builder.ToString() + }) +} + +function Convert-DelphiProperties([string] $Text) { + $inputLines = $Text -split '\r?\n' + $outputLines = [Collections.Generic.List[string]]::new() + $objects = [Collections.Generic.List[object]]::new() + for ($index = 0; $index -lt $inputLines.Length; $index++) { + $line = $inputLines[$index] + if ($line -match '^(\s*)(?:object|inherited)\s+\w+:\s*(\w+)') { + $kind = $Matches[2] + $indent = $Matches[1].Length + $hasClick = $false + $hasPasswordChar = $false + for ($next = $index + 1; $next -lt $inputLines.Length; $next++) { + if ($inputLines[$next] -match ('^' + (' ' * $indent) + 'end\s*$')) { break } + if ($inputLines[$next] -match ('^' + (' ' * ($indent + 2)) + 'OnClick\s*=')) { $hasClick = $true } + if ($inputLines[$next] -match ('^' + (' ' * ($indent + 2)) + 'PasswordChar\s*=')) { $hasPasswordChar = $true } + } + $objects.Add([pscustomobject]@{ Kind = $kind; Indent = $indent; HasClick = $hasClick; HasPasswordChar = $hasPasswordChar }) + $outputLines.Add($line) + continue + } + if ($objects.Count -gt 0) { + $current = $objects[$objects.Count - 1] + if ($current.Kind -in @('TCheckBox', 'TRadioButton') -and $line -match '^\s*OnChange\s*=') { + # VCL check/radio state changes use OnClick. If both were + # supplied, keep the existing click handler rather than write + # duplicate event properties. The current account selector's + # two handlers both call its same SearchFiltered method. + if ($current.HasClick) { continue } + $line = $line -replace '^(\s*)OnChange\s*=', '${1}OnClick =' + } + if ($current.Kind -in @('TSpeedButton', 'TGroupBox', 'TComboBox') -and $line -match '^\s*AutoSize\s*=') { continue } + if ($current.Kind -in @('TOpenDialog', 'TSaveDialog') -and $line -match '^\s*(Width|Height)\s*=') { continue } + if ($current.Kind -eq 'TRadioGroup' -and $line -match '^\s*AutoFill\s*=') { continue } + if ($current.Kind -eq 'TDrawGrid' -and $line -match '^\s*(ExtendedSelect|TitleFont\.\w+)\s*=') { continue } + if ($current.Kind -eq 'TEdit' -and $line -match '^\s*EchoMode\s*=\s*(\w+)') { + if ($Matches[1] -eq 'emPassword' -and -not $current.HasPasswordChar) { + $outputLines.Add((' ' * ($current.Indent + 2)) + "PasswordChar = '*'") + } + continue + } + if ($line -match '^(\s*)end\s*$' -and $Matches[1].Length -eq $current.Indent) { + $objects.RemoveAt($objects.Count - 1) + } + } + $outputLines.Add($line) + } + return $outputLines -join "`r`n" +} + +# Keep Lazarus resources canonical; generate paired VCL resources for Delphi. +foreach ($file in Get-ChildItem -LiteralPath $source -Filter '*.lfm' -Recurse) { + if ($file.FullName -match '[\\/](backup|\.backup)[\\/]') { continue } + if ($file.Name -eq 'UFRMWallet.lfm') { continue } # Historical, unused wallet. + $destination = [IO.Path]::ChangeExtension($file.FullName, '.dfm') + if ($AssetsOnly -or $PropertiesOnly) { + if (-not (Test-Path -LiteralPath $destination)) { throw "No paired DFM exists: $destination" } + $text = [IO.File]::ReadAllText($destination) + $converted = $text + if ($PropertiesOnly) { $converted = Convert-DelphiProperties $converted } + if ($AssetsOnly) { $converted = Convert-DelphiAssets $converted $file.BaseName } + if ($converted -cne $text) { [IO.File]::WriteAllText($destination, $converted, $encoding) } + continue + } + $objects = [Collections.Generic.List[object]]::new() + $lines = [Collections.Generic.List[string]]::new() + foreach ($line in [IO.File]::ReadAllLines($file.FullName)) { + if ($line -match '^(\s*)(?:object|inherited)\s+\w+:\s*(\w+)') { + $kind = $Matches[2] + $indent = $Matches[1].Length + $objects.Add([pscustomobject]@{ Kind = $kind; Indent = $indent }) + $line = $line.Replace(': TPairSplitterSide', ': TPanel').Replace(': TPairSplitter', ': TPanel') + $line = $line.Replace(': TFloatSpinEdit', ': TNumberBox') + $line = $line.Replace(': TApplicationProperties', ': TApplicationEvents') + $lines.Add($line) + if ($kind -eq 'TPairSplitter') { + $lines.Add((' ' * ($indent + 2)) + 'BevelOuter = bvNone') + } elseif ($kind -eq 'TPairSplitterSide') { + $lines.Add((' ' * ($indent + 2)) + 'BevelOuter = bvNone') + $align = if ($line -match 'PairSplitterSide1:') { 'alLeft' } else { 'alClient' } + $lines.Add((' ' * ($indent + 2)) + 'Align = ' + $align) + } elseif ($kind -eq 'TFloatSpinEdit') { + $lines.Add((' ' * ($indent + 2)) + 'Mode = nbmFloat') + } + continue + } + if ($line -match '^\s*(LCLVersion|BorderSpacing\.\w+|AnchorSide\w+\.\w+|ChildSizing\.\w+)\s*=') { continue } + if ($objects.Count -gt 0) { + $current = $objects[$objects.Count - 1] + if ($objects.Count -gt 1 -and $line -match '^\s*Client(Width|Height)\s*=') { continue } + if ($current.Kind -eq 'TPairSplitter' -and $line -match '^\s*Position\s*=') { continue } + if ($current.Kind -eq 'TFloatSpinEdit') { + $line = $line -replace '^(\s*)Increment\s*=', '${1}SmallStep =' + $line = $line -replace '^(\s*)DecimalPlaces\s*=', '${1}Decimal =' + } + if ($file.Name -eq 'UFRMMainForm.lfm' -and $objects.Count -eq 1) { + $line = $line.Replace('FormStyle = fsMDIForm', 'FormStyle = fsNormal') + } + if ($line -match '^(\s*)end\s*$' -and $Matches[1].Length -eq $current.Indent) { + $objects.RemoveAt($objects.Count - 1) + } + } + $line = $line.Replace('ssAutoBoth', 'ssBoth').Replace('ssAutoVertical', 'ssVertical').Replace('ssAutoHorizontal', 'ssHorizontal') + $lines.Add($line) + } + $text = ($lines -join "`r`n") + "`r`n" + $text = Convert-DelphiProperties $text + [IO.File]::WriteAllText($destination, (Convert-DelphiAssets $text $file.BaseName), $encoding) +} +Write-Host ("Converted {0} PNG picture streams and {1} image lists; original LFM assets remain unchanged." -f + $script:DelphiPictureCount, $script:DelphiImageListCount)